fix(FN-1952): restore pi auth and extension loading
This commit is contained in:
@@ -459,6 +459,7 @@ vi.mock("@fusion/core", () => ({
|
||||
PluginLoader: mocks.pluginLoaderCtor,
|
||||
GlobalSettingsStore: vi.fn().mockImplementation(() => mocks.globalSettingsStoreInstance),
|
||||
resolveGlobalDir: vi.fn().mockReturnValue("/home/user/.fusion"),
|
||||
getEnabledPiExtensionPaths: vi.fn(() => []),
|
||||
DaemonTokenManager: vi.fn().mockImplementation(() => ({
|
||||
getToken: vi.fn().mockImplementation(() => Promise.resolve(mocks.globalSettingsData.daemonToken as string | undefined)),
|
||||
generateToken: vi.fn().mockImplementation(() => {
|
||||
|
||||
@@ -150,6 +150,7 @@ vi.mock("@fusion/core", () => ({
|
||||
getPlugin: vi.fn(),
|
||||
getLoadedPlugins: vi.fn().mockReturnValue([]),
|
||||
})),
|
||||
getEnabledPiExtensionPaths: vi.fn(() => []),
|
||||
getTaskMergeBlocker: vi.fn().mockReturnValue(undefined),
|
||||
syncInsightExtractionAutomation: mockSyncInsightExtraction,
|
||||
INSIGHT_EXTRACTION_SCHEDULE_NAME: "Memory Insight Extraction",
|
||||
@@ -479,7 +480,11 @@ describe("runDashboard — AuthStorage & ModelRegistry wiring", () => {
|
||||
|
||||
await runDashboard(0, {});
|
||||
|
||||
expect(mockDiscoverAndLoadExtensions).toHaveBeenCalledWith([], expect.any(String), undefined);
|
||||
expect(mockDiscoverAndLoadExtensions).toHaveBeenCalledWith(
|
||||
[],
|
||||
expect.any(String),
|
||||
expect.stringContaining(".fusion/disabled-auto-extension-discovery"),
|
||||
);
|
||||
expect(mockModelRegistry.registerProvider).toHaveBeenCalledWith(
|
||||
"custom-anthropic",
|
||||
expect.objectContaining({ models: [{ id: "claude-sonnet-4-5" }] }),
|
||||
@@ -786,8 +791,7 @@ describe("runDashboard — multi-project cwd/default engine resolution", () => {
|
||||
expect(ProjectEngineManager).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Verify CentralCore.getProjectByPath was called with cwd
|
||||
const centralCore = centralInstances[0];
|
||||
expect(centralCore.getProjectByPath).toHaveBeenCalled();
|
||||
expect(centralInstances.some((instance) => instance.getProjectByPath.mock.calls.length > 0)).toBe(true);
|
||||
|
||||
// Verify createServer received an engine (the cwd/default engine)
|
||||
const serverOpts = (createServer as ReturnType<typeof vi.fn>).mock.calls[0][1];
|
||||
|
||||
@@ -495,6 +495,7 @@ vi.mock("@fusion/core", () => ({
|
||||
CentralCore: mocks.centralCoreCtor,
|
||||
PluginStore: mocks.pluginStoreCtor,
|
||||
PluginLoader: mocks.pluginLoaderCtor,
|
||||
getEnabledPiExtensionPaths: vi.fn(() => []),
|
||||
getTaskMergeBlocker: vi.fn().mockReturnValue(null),
|
||||
syncInsightExtractionAutomation: mocks.syncInsightExtractionAutomationMock,
|
||||
INSIGHT_EXTRACTION_SCHEDULE_NAME: "Memory Insight Extraction",
|
||||
|
||||
@@ -1,8 +1,17 @@
|
||||
import { homedir } from "node:os";
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
export function getFusionAgentDir(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
|
||||
return join(home, ".fusion", "agent");
|
||||
}
|
||||
|
||||
export function getLegacyAgentDir(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
|
||||
return join(home, ".pi", "agent");
|
||||
}
|
||||
|
||||
export function getFusionAuthPath(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
|
||||
return join(home, ".fusion", "agent", "auth.json");
|
||||
return join(getFusionAgentDir(home), "auth.json");
|
||||
}
|
||||
|
||||
export function getLegacyAuthPaths(home = process.env.HOME || process.env.USERPROFILE || homedir()): string[] {
|
||||
@@ -11,3 +20,36 @@ export function getLegacyAuthPaths(home = process.env.HOME || process.env.USERPR
|
||||
join(home, ".pi", "auth.json"),
|
||||
];
|
||||
}
|
||||
|
||||
export function getFusionModelsPath(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
|
||||
return join(getFusionAgentDir(home), "models.json");
|
||||
}
|
||||
|
||||
export function getLegacyModelsPaths(home = process.env.HOME || process.env.USERPROFILE || homedir()): string[] {
|
||||
return [
|
||||
join(home, ".pi", "agent", "models.json"),
|
||||
join(home, ".pi", "models.json"),
|
||||
];
|
||||
}
|
||||
|
||||
export function getModelRegistryModelsPath(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
|
||||
const fusionModelsPath = getFusionModelsPath(home);
|
||||
if (existsSync(fusionModelsPath)) {
|
||||
return fusionModelsPath;
|
||||
}
|
||||
|
||||
return getLegacyModelsPaths(home).find((modelsPath) => existsSync(modelsPath)) ?? fusionModelsPath;
|
||||
}
|
||||
|
||||
export function getPackageManagerAgentDir(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
|
||||
const fusionAgentDir = getFusionAgentDir(home);
|
||||
if (
|
||||
existsSync(join(fusionAgentDir, "settings.json")) ||
|
||||
existsSync(join(fusionAgentDir, "extensions"))
|
||||
) {
|
||||
return fusionAgentDir;
|
||||
}
|
||||
|
||||
const legacyAgentDir = getLegacyAgentDir(home);
|
||||
return existsSync(legacyAgentDir) ? legacyAgentDir : fusionAgentDir;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
CentralCore,
|
||||
PluginStore,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
DaemonTokenManager,
|
||||
GlobalSettingsStore,
|
||||
resolveGlobalDir,
|
||||
getEnabledPiExtensionPaths,
|
||||
} from "@fusion/core";
|
||||
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
|
||||
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath } from "@fusion/dashboard";
|
||||
@@ -28,7 +30,6 @@ import {
|
||||
DefaultPackageManager,
|
||||
ModelRegistry,
|
||||
discoverAndLoadExtensions,
|
||||
getAgentDir,
|
||||
createExtensionRuntime,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
@@ -38,12 +39,21 @@ import {
|
||||
import { promptForPort } from "./port-prompt.js";
|
||||
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
|
||||
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||
import { getFusionAuthPath, getLegacyAuthPaths } from "./auth-paths.js";
|
||||
import { getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
|
||||
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
let daemonStartTime = 0;
|
||||
let daemonDbHealthCheck: (() => boolean) | null = null;
|
||||
|
||||
async function resolveRuntimeProjectPath(): Promise<string> {
|
||||
try {
|
||||
return (await resolveProject(undefined)).projectPath;
|
||||
} catch {
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human-readable string
|
||||
*/
|
||||
@@ -198,7 +208,7 @@ export async function runDaemon(opts: DaemonOptions = {}) {
|
||||
}
|
||||
|
||||
const selectedHost = opts.host ?? "0.0.0.0";
|
||||
const cwd = process.cwd();
|
||||
const cwd = await resolveRuntimeProjectPath();
|
||||
|
||||
// ── CentralCore: global coordination + ntfy project ID lookup ─────────
|
||||
let ntfyProjectId: string | undefined;
|
||||
@@ -328,13 +338,13 @@ export async function runDaemon(opts: DaemonOptions = {}) {
|
||||
const authStorage = AuthStorage.create(getFusionAuthPath());
|
||||
const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths());
|
||||
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
|
||||
const modelRegistry = new ModelRegistry(mergedAuthStorage);
|
||||
const modelRegistry = new ModelRegistry(mergedAuthStorage, getModelRegistryModelsPath());
|
||||
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
|
||||
|
||||
// PackageManager may be used for skills adapter even if extension loading fails
|
||||
let packageManager: DefaultPackageManager | undefined;
|
||||
try {
|
||||
const agentDir = getAgentDir();
|
||||
const agentDir = getPackageManagerAgentDir();
|
||||
packageManager = new DefaultPackageManager({
|
||||
cwd,
|
||||
agentDir,
|
||||
@@ -346,9 +356,9 @@ export async function runDaemon(opts: DaemonOptions = {}) {
|
||||
.map((r) => r.path);
|
||||
|
||||
const extensionsResult = await discoverAndLoadExtensions(
|
||||
packageExtensionPaths,
|
||||
[...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
|
||||
cwd,
|
||||
undefined,
|
||||
join(cwd, ".fusion", "disabled-auto-extension-discovery"),
|
||||
);
|
||||
|
||||
for (const { path, error } of extensionsResult.errors) {
|
||||
|
||||
@@ -156,6 +156,7 @@ vi.mock("@fusion/core", () => ({
|
||||
emit: emitter.emit.bind(emitter),
|
||||
};
|
||||
}),
|
||||
getEnabledPiExtensionPaths: vi.fn(() => []),
|
||||
getTaskMergeBlocker: vi.fn((task: any) => {
|
||||
if (task.column !== "in-review") return `task is in '${task.column}', must be in 'in-review'`;
|
||||
if (task.paused) return "task is paused";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { TaskStore, AutomationStore, CentralCore, AgentStore, PluginStore, PluginLoader, getTaskMergeBlocker } from "@fusion/core";
|
||||
import { join } from "node:path";
|
||||
import { TaskStore, AutomationStore, CentralCore, AgentStore, PluginStore, PluginLoader, getTaskMergeBlocker, getEnabledPiExtensionPaths } from "@fusion/core";
|
||||
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath } from "@fusion/dashboard";
|
||||
import { aiMergeTask, MissionAutopilot, MissionExecutionLoop, HeartbeatMonitor, HeartbeatTriggerScheduler, type WakeContext, ProjectEngineManager, PeerExchangeService } from "@fusion/engine";
|
||||
import { AuthStorage, DefaultPackageManager, ModelRegistry, discoverAndLoadExtensions, getAgentDir, createExtensionRuntime } from "@mariozechner/pi-coding-agent";
|
||||
import { AuthStorage, DefaultPackageManager, ModelRegistry, discoverAndLoadExtensions, createExtensionRuntime } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
getMergeStrategy,
|
||||
processPullRequestMergeTask,
|
||||
@@ -10,7 +11,8 @@ import {
|
||||
import { promptForPort } from "./port-prompt.js";
|
||||
import { createReadOnlyProviderSettingsView, createProjectSettingsPersistence } from "./provider-settings.js";
|
||||
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||
import { getFusionAuthPath, getLegacyAuthPaths } from "./auth-paths.js";
|
||||
import { getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
|
||||
// Re-export for backward compatibility with tests
|
||||
export { promptForPort };
|
||||
@@ -183,6 +185,14 @@ function setDiagnosticStoreListenerCheck(check: () => Record<string, number>): v
|
||||
diagnosticStoreListenerCheck = check;
|
||||
}
|
||||
|
||||
async function resolveRuntimeProjectPath(): Promise<string> {
|
||||
try {
|
||||
return (await resolveProject(undefined)).projectPath;
|
||||
} catch {
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runDashboard(port: number, opts: { paused?: boolean; dev?: boolean; interactive?: boolean; open?: boolean } = {}) {
|
||||
ensureProcessDiagnostics();
|
||||
|
||||
@@ -199,7 +209,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const cwd = process.cwd();
|
||||
const cwd = await resolveRuntimeProjectPath();
|
||||
const store = new TaskStore(cwd);
|
||||
await store.init();
|
||||
await store.watch();
|
||||
@@ -365,7 +375,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
const authStorage = AuthStorage.create(getFusionAuthPath());
|
||||
const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths());
|
||||
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
|
||||
const modelRegistry = new ModelRegistry(mergedAuthStorage);
|
||||
const modelRegistry = new ModelRegistry(mergedAuthStorage, getModelRegistryModelsPath());
|
||||
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
|
||||
|
||||
// PackageManager may be used for skills adapter even if extension loading fails
|
||||
@@ -374,7 +384,7 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
// Resolve extension paths from pi settings packages (npm, git, local).
|
||||
// This picks up extensions like @howaboua/pi-glm-via-anthropic that
|
||||
// register custom providers (e.g. glm-5.1) via registerProvider().
|
||||
const agentDir = getAgentDir();
|
||||
const agentDir = getPackageManagerAgentDir();
|
||||
packageManager = new DefaultPackageManager({
|
||||
cwd,
|
||||
agentDir,
|
||||
@@ -385,8 +395,12 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
|
||||
.filter((r) => r.enabled)
|
||||
.map((r) => r.path);
|
||||
|
||||
// Load all extensions: filesystem-discovered + package-resolved
|
||||
const extensionsResult = await discoverAndLoadExtensions(packageExtensionPaths, cwd, undefined);
|
||||
// Load all enabled extensions: Fusion/Pi filesystem-discovered + package-resolved.
|
||||
const extensionsResult = await discoverAndLoadExtensions(
|
||||
[...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
|
||||
cwd,
|
||||
join(cwd, ".fusion", "disabled-auto-extension-discovery"),
|
||||
);
|
||||
|
||||
for (const { path, error } of extensionsResult.errors) {
|
||||
console.log(`[extensions] Failed to load ${path}: ${error}`);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
*/
|
||||
|
||||
import type { AddressInfo } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import {
|
||||
CentralCore,
|
||||
PluginStore,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
DaemonTokenManager,
|
||||
GlobalSettingsStore,
|
||||
resolveGlobalDir,
|
||||
getEnabledPiExtensionPaths,
|
||||
} from "@fusion/core";
|
||||
import type { AutomationRunResult, ScheduledTask } from "@fusion/core";
|
||||
import { createServer, GitHubClient, createSkillsAdapter, getProjectSettingsPath } from "@fusion/dashboard";
|
||||
@@ -29,7 +31,6 @@ import {
|
||||
DefaultPackageManager,
|
||||
ModelRegistry,
|
||||
discoverAndLoadExtensions,
|
||||
getAgentDir,
|
||||
createExtensionRuntime,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
@@ -39,13 +40,22 @@ import {
|
||||
import { promptForPort } from "./port-prompt.js";
|
||||
import { createReadOnlyProviderSettingsView, createProjectSettingsPersistence } from "./provider-settings.js";
|
||||
import { createReadOnlyAuthFileStorage, mergeAuthStorageReads, wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
|
||||
import { getFusionAuthPath, getLegacyAuthPaths } from "./auth-paths.js";
|
||||
import { getFusionAuthPath, getLegacyAuthPaths, getModelRegistryModelsPath, getPackageManagerAgentDir } from "./auth-paths.js";
|
||||
import { resolveProject } from "../project-context.js";
|
||||
|
||||
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
let diagnosticIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
let serveStartTime = 0;
|
||||
let serveDbHealthCheck: (() => boolean) | null = null;
|
||||
|
||||
async function resolveRuntimeProjectPath(): Promise<string> {
|
||||
try {
|
||||
return (await resolveProject(undefined)).projectPath;
|
||||
} catch {
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human-readable string
|
||||
*/
|
||||
@@ -209,7 +219,7 @@ export async function runServe(
|
||||
}
|
||||
|
||||
const selectedHost = opts.host ?? "0.0.0.0";
|
||||
const cwd = process.cwd();
|
||||
const cwd = await resolveRuntimeProjectPath();
|
||||
|
||||
// ── CentralCore: global coordination + ntfy project ID lookup ─────────
|
||||
//
|
||||
@@ -387,13 +397,13 @@ export async function runServe(
|
||||
const authStorage = AuthStorage.create(getFusionAuthPath());
|
||||
const legacyAuthStorage = createReadOnlyAuthFileStorage(getLegacyAuthPaths());
|
||||
const mergedAuthStorage = mergeAuthStorageReads(authStorage, [legacyAuthStorage]);
|
||||
const modelRegistry = new ModelRegistry(mergedAuthStorage);
|
||||
const modelRegistry = new ModelRegistry(mergedAuthStorage, getModelRegistryModelsPath());
|
||||
const dashboardAuthStorage = wrapAuthStorageWithApiKeyProviders(mergedAuthStorage, modelRegistry);
|
||||
|
||||
// PackageManager may be used for skills adapter even if extension loading fails
|
||||
let packageManager: DefaultPackageManager | undefined;
|
||||
try {
|
||||
const agentDir = getAgentDir();
|
||||
const agentDir = getPackageManagerAgentDir();
|
||||
packageManager = new DefaultPackageManager({
|
||||
cwd,
|
||||
agentDir,
|
||||
@@ -405,9 +415,9 @@ export async function runServe(
|
||||
.map((r) => r.path);
|
||||
|
||||
const extensionsResult = await discoverAndLoadExtensions(
|
||||
packageExtensionPaths,
|
||||
[...getEnabledPiExtensionPaths(cwd), ...packageExtensionPaths],
|
||||
cwd,
|
||||
undefined,
|
||||
join(cwd, ".fusion", "disabled-auto-extension-discovery"),
|
||||
);
|
||||
|
||||
for (const { path, error } of extensionsResult.errors) {
|
||||
|
||||
@@ -14,8 +14,9 @@ import {
|
||||
isGhAvailable,
|
||||
runGhJsonAsync,
|
||||
} from "@fusion/core/gh-cli";
|
||||
import { resolve, basename, extname } from "node:path";
|
||||
import { resolve, basename, extname, join } from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { existsSync } from "node:fs";
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────
|
||||
@@ -36,19 +37,39 @@ const MIME_TYPES: Record<string, string> = {
|
||||
".xml": "application/xml",
|
||||
};
|
||||
|
||||
/** Cache stores per cwd to avoid re-init on every tool call. */
|
||||
function resolveProjectRoot(cwd: string): string {
|
||||
let current = resolve(cwd);
|
||||
while (true) {
|
||||
if (existsSync(join(current, ".fusion"))) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const parent = resolve(current, "..");
|
||||
if (parent === current) {
|
||||
return resolve(cwd);
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/** Cache stores per project root to avoid re-init on every tool call. */
|
||||
const storeCache = new Map<string, TaskStore>();
|
||||
|
||||
async function getStore(cwd: string): Promise<TaskStore> {
|
||||
const existing = storeCache.get(cwd);
|
||||
const projectRoot = resolveProjectRoot(cwd);
|
||||
const existing = storeCache.get(projectRoot);
|
||||
if (existing) return existing;
|
||||
|
||||
const store = new TaskStore(cwd);
|
||||
const store = new TaskStore(projectRoot);
|
||||
await store.init();
|
||||
storeCache.set(cwd, store);
|
||||
storeCache.set(projectRoot, store);
|
||||
return store;
|
||||
}
|
||||
|
||||
function getFusionDir(cwd: string): string {
|
||||
return join(resolveProjectRoot(cwd), ".fusion");
|
||||
}
|
||||
|
||||
function formatTaskLine(t: Task): string {
|
||||
const label =
|
||||
t.title || t.description.slice(0, 60) + (t.description.length > 60 ? "…" : "");
|
||||
@@ -1534,7 +1555,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const { AgentStore, AGENT_VALID_TRANSITIONS } = await import("@fusion/core");
|
||||
|
||||
const agentStore = new AgentStore({ rootDir: ctx.cwd + "/.fusion" });
|
||||
const agentStore = new AgentStore({ rootDir: getFusionDir(ctx.cwd) });
|
||||
await agentStore.init();
|
||||
|
||||
const agent = await agentStore.getAgent(params.id);
|
||||
@@ -1597,7 +1618,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
||||
const { AgentStore, AGENT_VALID_TRANSITIONS } = await import("@fusion/core");
|
||||
|
||||
const agentStore = new AgentStore({ rootDir: ctx.cwd + "/.fusion" });
|
||||
const agentStore = new AgentStore({ rootDir: getFusionDir(ctx.cwd) });
|
||||
await agentStore.init();
|
||||
|
||||
const agent = await agentStore.getAgent(params.id);
|
||||
@@ -1759,7 +1780,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
|
||||
// Execute via spawn
|
||||
const child = spawn("npx", npxArgs, {
|
||||
cwd: ctx.cwd,
|
||||
cwd: resolveProjectRoot(ctx.cwd),
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
@@ -1864,7 +1885,7 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
|
||||
// Find the fn binary: prefer local node_modules, then global
|
||||
const child = spawn("fn", ["dashboard", "--port", String(port)], {
|
||||
cwd: ctx.cwd,
|
||||
cwd: resolveProjectRoot(ctx.cwd),
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
detached: false,
|
||||
env: { ...process.env },
|
||||
|
||||
Reference in New Issue
Block a user