FN-6218: restore pi upgrade compatibility
Restore Pi-upgraded workspaces to keep Fusion resources accessible and recover title generation when configured models go stale. - Mark read-only Fusion/Pi provider settings views as project-trusted so extension loading keeps working after Pi upgrades. - Retry task title summarization with automatic model resolution when the configured provider model is missing from the Pi registry. - Cover provider settings trust behavior and stale summarizer fallback paths with regression tests. - Document the read-only settings trust contract and add a patch changeset. Files changed: .changeset/fn-6218-pi-upgrade-regressions.md | 5 + docs/settings-reference.md | 2 + docs/task-management.md | 2 + .../commands/__tests__/provider-settings.test.ts | 16 ++++ packages/cli/src/commands/provider-settings.ts | 5 + packages/core/src/__tests__/ai-summarize.test.ts | 102 +++++++++++++++++++++ packages/core/src/ai-summarize.ts | 85 ++++++++++++----- .../src/__tests__/pi-create-fn-agent.test.ts | 32 +++++++ packages/engine/src/pi.ts | 7 +- 9 files changed, 233 insertions(+), 23 deletions(-) Fusion-Task-Id: FN-6218 Fusion-Task-Lineage: 3d6aca32-a56a-43a0-a57b-8e6abc839ce9
This commit is contained in:
5
.changeset/fn-6218-pi-upgrade-regressions.md
Normal file
5
.changeset/fn-6218-pi-upgrade-regressions.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix pi 0.79 extension discovery compatibility and retry stale title-summarizer model ids with automatic model resolution.
|
||||||
@@ -837,6 +837,8 @@ Project-scoped model lane used for task title auto-summarization, GitHub trackin
|
|||||||
5. Global `defaultProvider` + `defaultModelId`
|
5. Global `defaultProvider` + `defaultModelId`
|
||||||
6. Automatic provider/model resolution
|
6. Automatic provider/model resolution
|
||||||
|
|
||||||
|
If the configured title summarizer provider/model is stale and no longer exists in the pi model registry, title generation logs a warning with the stale id and retries once with automatic provider/model resolution. Other AI failures (auth, empty output, unavailable engine) still fail normally.
|
||||||
|
|
||||||
> **Note:** Runtime fallback precedence logic is implemented in engine and dashboard routes. The hierarchies above reflect current runtime behavior.
|
> **Note:** Runtime fallback precedence logic is implemented in engine and dashboard routes. The hierarchies above reflect current runtime behavior.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -855,6 +855,8 @@ Users can apply presets at task creation; manual model selection can override th
|
|||||||
|
|
||||||
When `autoSummarizeTitles` is enabled and a task has a long untitled description, Fusion can auto-generate a concise title. This applies to tasks created from the dashboard/API as well as tasks created by agents and tooling flows (`fn_task_create`, delegated tasks, and triage-created child tasks). GitHub tracking now waits for the `createTask`-level summarizer (explicit or auto-attached from settings) to settle before filing, then uses that resulting title and falls back to deterministic description-derived title generation only when summarization is unavailable.
|
When `autoSummarizeTitles` is enabled and a task has a long untitled description, Fusion can auto-generate a concise title. This applies to tasks created from the dashboard/API as well as tasks created by agents and tooling flows (`fn_task_create`, delegated tasks, and triage-created child tasks). GitHub tracking now waits for the `createTask`-level summarizer (explicit or auto-attached from settings) to settle before filing, then uses that resulting title and falls back to deterministic description-derived title generation only when summarization is unavailable.
|
||||||
|
|
||||||
|
If a configured title summarizer model is stale after a pi upgrade, Fusion logs a warning naming that provider/model and retries once with automatic model resolution before falling back to deterministic title generation. Genuine AI-service failures are not masked by this retry.
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Board/task cards + quick entry
|
### Board/task cards + quick entry
|
||||||
|
|||||||
@@ -46,6 +46,22 @@ describe("createReadOnlyProviderSettingsView", () => {
|
|||||||
shared: "fusion",
|
shared: "fusion",
|
||||||
});
|
});
|
||||||
expect(view.getNpmCommand()).toEqual(["pnpm"]);
|
expect(view.getNpmCommand()).toEqual(["pnpm"]);
|
||||||
|
expect(view.isProjectTrusted()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("exposes project trust for pi package-manager discovery consumers", async () => {
|
||||||
|
const root = tempWorkspace("fusion-provider-settings-");
|
||||||
|
const cwd = join(root, "project");
|
||||||
|
const agentDir = join(root, "agent");
|
||||||
|
mkdirSync(agentDir, { recursive: true });
|
||||||
|
|
||||||
|
const view = createReadOnlyProviderSettingsView(cwd, agentDir);
|
||||||
|
const discoveryConsumer = {
|
||||||
|
resolve: vi.fn(async () => view.isProjectTrusted()),
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(discoveryConsumer.resolve()).resolves.toBe(true);
|
||||||
|
expect(typeof view.isProjectTrusted()).toBe("boolean");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty project settings when .fusion/settings.json does not exist", () => {
|
it("returns empty project settings when .fusion/settings.json does not exist", () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export interface PackageManagerSettingsView {
|
|||||||
getGlobalSettings(): Record<string, unknown>;
|
getGlobalSettings(): Record<string, unknown>;
|
||||||
getProjectSettings(): Record<string, unknown>;
|
getProjectSettings(): Record<string, unknown>;
|
||||||
getNpmCommand(): string[] | undefined;
|
getNpmCommand(): string[] | undefined;
|
||||||
|
isProjectTrusted(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function siblingAgentDir(agentDir: string, siblingRoot: ".fusion" | ".pi"): string | undefined {
|
function siblingAgentDir(agentDir: string, siblingRoot: ".fusion" | ".pi"): string | undefined {
|
||||||
@@ -47,6 +48,10 @@ export function createReadOnlyProviderSettingsView(cwd: string, agentDir: string
|
|||||||
getNpmCommand: () => Array.isArray(mergedSettings.npmCommand)
|
getNpmCommand: () => Array.isArray(mergedSettings.npmCommand)
|
||||||
? [...mergedSettings.npmCommand]
|
? [...mergedSettings.npmCommand]
|
||||||
: undefined,
|
: undefined,
|
||||||
|
// Pi's SettingsManager defaults projects to trusted. Fusion workspaces are
|
||||||
|
// user-owned, so preserve pre-upgrade behavior and keep project-scoped
|
||||||
|
// .fusion resources loadable through the read-only settings view.
|
||||||
|
isProjectTrusted: () => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -255,6 +255,108 @@ describe("ai-summarize", () => {
|
|||||||
const title = await summarizeTitle("a".repeat(201), "/tmp");
|
const title = await summarizeTitle("a".repeat(201), "/tmp");
|
||||||
expect(title).toBe("Refactor merger title fallback");
|
expect(title).toBe("Refactor merger title fallback");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("retries stale configured model ids with automatic resolution and logs the stale id", async () => {
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const createFnAgent = vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(new Error(
|
||||||
|
"Configured model fireworksai/accounts/fireworks/routers/kimi-k2p5-turbo (primary selection) "
|
||||||
|
+ "was not found in the pi model registry.",
|
||||||
|
))
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: {
|
||||||
|
messages: [{ role: "assistant", content: "Fix pi upgrade regressions" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
getFnAgentMock.mockResolvedValue(createFnAgent);
|
||||||
|
|
||||||
|
const title = await summarizeTitle(
|
||||||
|
"a".repeat(201),
|
||||||
|
"/tmp",
|
||||||
|
"fireworksai",
|
||||||
|
"accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(title).toBe("Fix pi upgrade regressions");
|
||||||
|
expect(createFnAgent).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
||||||
|
defaultProvider: "fireworksai",
|
||||||
|
defaultModelId: "accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||||
|
}));
|
||||||
|
expect(createFnAgent).toHaveBeenNthCalledWith(2, expect.not.objectContaining({
|
||||||
|
defaultProvider: expect.any(String),
|
||||||
|
defaultModelId: expect.any(String),
|
||||||
|
}));
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("fireworksai/accounts/fireworks/routers/kimi-k2p5-turbo"));
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps valid configured model ids on the primary summarizer path", async () => {
|
||||||
|
const createFnAgent = vi.fn().mockResolvedValue({
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: {
|
||||||
|
messages: [{ role: "assistant", content: "Keep configured model" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
getFnAgentMock.mockResolvedValue(createFnAgent);
|
||||||
|
|
||||||
|
const title = await summarizeTitle("a".repeat(201), "/tmp", "anthropic", "claude-sonnet-4-5");
|
||||||
|
|
||||||
|
expect(title).toBe("Keep configured model");
|
||||||
|
expect(createFnAgent).toHaveBeenCalledTimes(1);
|
||||||
|
expect(createFnAgent).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
defaultProvider: "anthropic",
|
||||||
|
defaultModelId: "claude-sonnet-4-5",
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when stale-model automatic resolution also fails", async () => {
|
||||||
|
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const createFnAgent = vi
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValueOnce(new Error(
|
||||||
|
"Configured model fireworksai/accounts/fireworks/routers/kimi-k2p5-turbo (primary selection) "
|
||||||
|
+ "was not found in the pi model registry.",
|
||||||
|
))
|
||||||
|
.mockRejectedValueOnce(new Error("No model selected"));
|
||||||
|
getFnAgentMock.mockResolvedValue(createFnAgent);
|
||||||
|
|
||||||
|
await expect(summarizeTitle(
|
||||||
|
"a".repeat(201),
|
||||||
|
"/tmp",
|
||||||
|
"fireworksai",
|
||||||
|
"accounts/fireworks/routers/kimi-k2p5-turbo",
|
||||||
|
)).resolves.toBeNull();
|
||||||
|
expect(createFnAgent).toHaveBeenCalledTimes(2);
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("retrying with automatic model resolution"));
|
||||||
|
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Automatic title summarizer fallback"));
|
||||||
|
warnSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mask genuine AI service errors", async () => {
|
||||||
|
getFnAgentMock.mockResolvedValue(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
session: {
|
||||||
|
prompt: vi.fn().mockResolvedValue(undefined),
|
||||||
|
dispose: vi.fn(),
|
||||||
|
state: {
|
||||||
|
error: "authentication failed",
|
||||||
|
messages: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(summarizeTitle("a".repeat(201), "/tmp", "anthropic", "claude-sonnet-4-5"))
|
||||||
|
.rejects.toThrow("AI session error: authentication failed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sanitizeTitle", () => {
|
describe("sanitizeTitle", () => {
|
||||||
|
|||||||
@@ -198,31 +198,22 @@ export function validateDescription(description: unknown): string {
|
|||||||
/** Debug flag for AI operations */
|
/** Debug flag for AI operations */
|
||||||
const DEBUG = process.env.FUSION_DEBUG_AI === "true";
|
const DEBUG = process.env.FUSION_DEBUG_AI === "true";
|
||||||
|
|
||||||
/**
|
function isConfiguredModelNotFoundError(error: unknown): boolean {
|
||||||
* Summarize a task description into a concise title using AI.
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
* @param description - The task description to summarize (must be 201-2000 chars)
|
return /Configured model .+ was not found in the pi model registry/.test(message);
|
||||||
* @param rootDir - Project root directory for AI agent context
|
}
|
||||||
* @param provider - Optional AI model provider (e.g., "anthropic")
|
|
||||||
* @param modelId - Optional AI model ID (e.g., "claude-sonnet-4-5")
|
function formatConfiguredModel(provider?: string, modelId?: string): string {
|
||||||
* @returns The generated title (guaranteed ≤60 characters), or null if validation fails
|
return provider && modelId ? `${provider}/${modelId}` : "unknown configured model";
|
||||||
*/
|
}
|
||||||
export async function summarizeTitle(
|
|
||||||
|
async function runTitleSummarizer(
|
||||||
|
createFnAgent: NonNullable<Awaited<ReturnType<typeof getFnAgent>>>,
|
||||||
description: string,
|
description: string,
|
||||||
rootDir: string,
|
rootDir: string,
|
||||||
provider?: string,
|
provider?: string,
|
||||||
modelId?: string
|
modelId?: string,
|
||||||
): Promise<string | null> {
|
): Promise<string> {
|
||||||
// Validate description length first
|
|
||||||
if (description.length <= 200) {
|
|
||||||
return null; // Too short for summarization
|
|
||||||
}
|
|
||||||
|
|
||||||
const createFnAgent = await getFnAgent();
|
|
||||||
if (!createFnAgent) {
|
|
||||||
if (DEBUG) console.log("[ai-summarize] AI engine not available");
|
|
||||||
throw new AiServiceError("AI engine not available");
|
|
||||||
}
|
|
||||||
|
|
||||||
const agentOptions: {
|
const agentOptions: {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
systemPrompt: string;
|
systemPrompt: string;
|
||||||
@@ -324,6 +315,56 @@ export async function summarizeTitle(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarize a task description into a concise title using AI.
|
||||||
|
* @param description - The task description to summarize (must be 201-2000 chars)
|
||||||
|
* @param rootDir - Project root directory for AI agent context
|
||||||
|
* @param provider - Optional AI model provider (e.g., "anthropic")
|
||||||
|
* @param modelId - Optional AI model ID (e.g., "claude-sonnet-4-5")
|
||||||
|
* @returns The generated title (guaranteed ≤60 characters), or null if validation fails
|
||||||
|
*/
|
||||||
|
export async function summarizeTitle(
|
||||||
|
description: string,
|
||||||
|
rootDir: string,
|
||||||
|
provider?: string,
|
||||||
|
modelId?: string
|
||||||
|
): Promise<string | null> {
|
||||||
|
// Validate description length first
|
||||||
|
if (description.length <= 200) {
|
||||||
|
return null; // Too short for summarization
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFnAgent = await getFnAgent();
|
||||||
|
if (!createFnAgent) {
|
||||||
|
if (DEBUG) console.log("[ai-summarize] AI engine not available");
|
||||||
|
throw new AiServiceError("AI engine not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await runTitleSummarizer(createFnAgent, description, rootDir, provider, modelId);
|
||||||
|
} catch (err) {
|
||||||
|
if (!provider || !modelId || !isConfiguredModelNotFoundError(err)) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const staleModel = formatConfiguredModel(provider, modelId);
|
||||||
|
console.warn(
|
||||||
|
`[ai-summarize] Configured title summarizer model ${staleModel} was not found in the pi model registry; `
|
||||||
|
+ "retrying with automatic model resolution.",
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await runTitleSummarizer(createFnAgent, description, rootDir);
|
||||||
|
} catch (retryError) {
|
||||||
|
const message = retryError instanceof Error ? retryError.message : String(retryError);
|
||||||
|
console.warn(
|
||||||
|
`[ai-summarize] Automatic title summarizer fallback after stale model ${staleModel} failed: ${message}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** System prompt for AI merge commit summary generation. */
|
/** System prompt for AI merge commit summary generation. */
|
||||||
export const MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT = `You summarize merge commits for a task management system.
|
export const MERGE_COMMIT_SUMMARIZE_SYSTEM_PROMPT = `You summarize merge commits for a task management system.
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const readFileSyncMock = vi.fn((_path?: any) => "{}");
|
|||||||
const realpathSyncNativeMock = vi.fn((path: PathLike) => String(path));
|
const realpathSyncNativeMock = vi.fn((path: PathLike) => String(path));
|
||||||
const readCustomProvidersMock = vi.fn(() => []);
|
const readCustomProvidersMock = vi.fn(() => []);
|
||||||
const packageManagerCwdCapture = vi.fn();
|
const packageManagerCwdCapture = vi.fn();
|
||||||
|
const packageManagerSettingsCapture = vi.fn();
|
||||||
|
|
||||||
// Route async `exec` through the `execSync` mock so the promisify bridge works.
|
// Route async `exec` through the `execSync` mock so the promisify bridge works.
|
||||||
// Use Symbol.for("nodejs.util.promisify.custom") directly to avoid async imports
|
// Use Symbol.for("nodejs.util.promisify.custom") directly to avoid async imports
|
||||||
@@ -107,10 +108,15 @@ vi.mock("@earendil-works/pi-coding-agent", () => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
DefaultPackageManager: class {
|
DefaultPackageManager: class {
|
||||||
|
private readonly settingsManager: any;
|
||||||
|
|
||||||
constructor(options: any) {
|
constructor(options: any) {
|
||||||
packageManagerCwdCapture(options?.cwd);
|
packageManagerCwdCapture(options?.cwd);
|
||||||
|
packageManagerSettingsCapture(options?.settingsManager);
|
||||||
|
this.settingsManager = options?.settingsManager;
|
||||||
}
|
}
|
||||||
async resolve() {
|
async resolve() {
|
||||||
|
this.settingsManager.isProjectTrusted();
|
||||||
return packageManagerResolveMock();
|
return packageManagerResolveMock();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1272,6 +1278,32 @@ describe("createFnAgent", () => {
|
|||||||
expect(createAgentSessionMock).toHaveBeenCalledTimes(1);
|
expect(createAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes project trust on the read-only pi settings view", async () => {
|
||||||
|
const { createReadOnlyPiSettingsView } = await import("../pi.js");
|
||||||
|
|
||||||
|
const view = createReadOnlyPiSettingsView("/tmp", "/mock-agent-dir");
|
||||||
|
|
||||||
|
expect(() => view.isProjectTrusted()).not.toThrow();
|
||||||
|
expect(view.isProjectTrusted()).toBe(true);
|
||||||
|
expect(typeof view.isProjectTrusted()).toBe("boolean");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes a project-trusted settings view through package-manager discovery", async () => {
|
||||||
|
const { createFnAgent } = await import("../pi.js");
|
||||||
|
|
||||||
|
await createFnAgent({
|
||||||
|
cwd: "/tmp",
|
||||||
|
systemPrompt: "test",
|
||||||
|
tools: "readonly",
|
||||||
|
});
|
||||||
|
|
||||||
|
const settingsView = packageManagerSettingsCapture.mock.calls.at(-1)?.[0];
|
||||||
|
expect(settingsView).toEqual(expect.objectContaining({ isProjectTrusted: expect.any(Function) }));
|
||||||
|
expect(settingsView.isProjectTrusted()).toBe(true);
|
||||||
|
expect(packageManagerResolveMock).toHaveBeenCalled();
|
||||||
|
expect(createAgentSessionMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("registers extension providers before resolving configured models", async () => {
|
it("registers extension providers before resolving configured models", async () => {
|
||||||
packageManagerResolveMock.mockResolvedValueOnce({
|
packageManagerResolveMock.mockResolvedValueOnce({
|
||||||
extensions: [{ enabled: true, path: "/extensions/zai-provider" }],
|
extensions: [{ enabled: true, path: "/extensions/zai-provider" }],
|
||||||
|
|||||||
@@ -1080,6 +1080,7 @@ interface PackageManagerSettingsView {
|
|||||||
getGlobalSettings(): Record<string, any>;
|
getGlobalSettings(): Record<string, any>;
|
||||||
getProjectSettings(): Record<string, any>;
|
getProjectSettings(): Record<string, any>;
|
||||||
getNpmCommand(): string[] | undefined;
|
getNpmCommand(): string[] | undefined;
|
||||||
|
isProjectTrusted(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function readJsonObject(path: string): Record<string, any> {
|
function readJsonObject(path: string): Record<string, any> {
|
||||||
@@ -1258,7 +1259,7 @@ function siblingAgentDir(agentDir: string, siblingRoot: ".fusion" | ".pi"): stri
|
|||||||
return join(dirname(dirname(agentDir)), siblingRoot, "agent");
|
return join(dirname(dirname(agentDir)), siblingRoot, "agent");
|
||||||
}
|
}
|
||||||
|
|
||||||
function createReadOnlyPiSettingsView(cwd: string, agentDir: string): PackageManagerSettingsView {
|
export function createReadOnlyPiSettingsView(cwd: string, agentDir: string): PackageManagerSettingsView {
|
||||||
const projectRoot = resolvePiExtensionProjectRoot(cwd);
|
const projectRoot = resolvePiExtensionProjectRoot(cwd);
|
||||||
const fusionAgentDir = agentDir.includes(`${join(".fusion", "agent")}`)
|
const fusionAgentDir = agentDir.includes(`${join(".fusion", "agent")}`)
|
||||||
? agentDir
|
? agentDir
|
||||||
@@ -1279,6 +1280,10 @@ function createReadOnlyPiSettingsView(cwd: string, agentDir: string): PackageMan
|
|||||||
getNpmCommand: () => Array.isArray(mergedSettings.npmCommand)
|
getNpmCommand: () => Array.isArray(mergedSettings.npmCommand)
|
||||||
? [...mergedSettings.npmCommand]
|
? [...mergedSettings.npmCommand]
|
||||||
: undefined,
|
: undefined,
|
||||||
|
// Pi's SettingsManager defaults projects to trusted. Fusion workspaces are
|
||||||
|
// user-owned, so preserve pre-upgrade behavior and keep project-scoped
|
||||||
|
// .fusion resources loadable through the read-only settings view.
|
||||||
|
isProjectTrusted: () => true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user