fix(FN-1952): use fusion storage for pi config

This commit is contained in:
gsxdsm
2026-04-16 20:12:09 -07:00
parent cc7f7bd6f8
commit 5adb013c4e
15 changed files with 210 additions and 180 deletions

View File

@@ -0,0 +1,7 @@
import { homedir } from "node:os";
import { join } from "node:path";
export function getFusionAuthPath(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
return join(home, ".fusion", "agent", "auth.json");
}

View File

@@ -38,6 +38,7 @@ import {
import { promptForPort } from "./port-prompt.js";
import { createReadOnlyProviderSettingsView } from "./provider-settings.js";
import { wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getFusionAuthPath } from "./auth-paths.js";
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
let daemonStartTime = 0;
@@ -324,7 +325,7 @@ export async function runDaemon(opts: DaemonOptions = {}) {
const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop();
const automationStore = cwdEngine.getAutomationStore();
const authStorage = AuthStorage.create();
const authStorage = AuthStorage.create(getFusionAuthPath());
const modelRegistry = new ModelRegistry(authStorage);
// PackageManager may be used for skills adapter even if extension loading fails

View File

@@ -10,6 +10,7 @@ import {
import { promptForPort } from "./port-prompt.js";
import { createReadOnlyProviderSettingsView, createProjectSettingsPersistence } from "./provider-settings.js";
import { wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getFusionAuthPath } from "./auth-paths.js";
// Re-export for backward compatibility with tests
export { promptForPort };
@@ -357,11 +358,11 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
});
// ── Auth & model wiring ────────────────────────────────────────────
// AuthStorage manages OAuth/API-key credentials (stored in ~/.pi/agent/auth.json).
// AuthStorage manages OAuth/API-key credentials (stored in ~/.fusion/agent/auth.json).
// ModelRegistry discovers available models from configured providers.
// Passing these to createServer enables the dashboard's Authentication
// tab (login/logout) and Model selector.
const authStorage = AuthStorage.create();
const authStorage = AuthStorage.create(getFusionAuthPath());
const modelRegistry = new ModelRegistry(authStorage);
// PackageManager may be used for skills adapter even if extension loading fails

View File

@@ -124,7 +124,7 @@ describe("init command", () => {
expect(existsSync(join(fusionDir, "fusion.db"))).toBe(true);
});
it("should add .fusion to .gitignore when it doesn't exist", async () => {
it("should add local storage directories to .gitignore when it doesn't exist", async () => {
const gitignorePath = join(tempProjectDir, ".gitignore");
expect(existsSync(gitignorePath)).toBe(false);
@@ -133,9 +133,10 @@ describe("init command", () => {
expect(existsSync(gitignorePath)).toBe(true);
const content = readFileSync(gitignorePath, "utf-8");
expect(content).toContain(".fusion");
expect(content).toContain(".pi");
});
it("should append .fusion to existing .gitignore", async () => {
it("should append local storage directories to existing .gitignore", async () => {
const gitignorePath = join(tempProjectDir, ".gitignore");
writeFileSync(gitignorePath, "node_modules\ndist\n");
@@ -145,9 +146,23 @@ describe("init command", () => {
expect(content).toContain("node_modules");
expect(content).toContain("dist");
expect(content).toContain(".fusion");
expect(content).toContain(".pi");
});
it("should not duplicate .fusion in .gitignore (idempotent)", async () => {
it("should not duplicate local storage directories in .gitignore (idempotent)", async () => {
const gitignorePath = join(tempProjectDir, ".gitignore");
writeFileSync(gitignorePath, "node_modules\n.fusion\n.pi\n");
await runInit({ path: tempProjectDir });
const content = readFileSync(gitignorePath, "utf-8");
const fusionMatches = content.match(/\.fusion/g);
const piMatches = content.match(/\.pi/g);
expect(fusionMatches).toHaveLength(1);
expect(piMatches).toHaveLength(1);
});
it("should add .pi when .fusion is already ignored", async () => {
const gitignorePath = join(tempProjectDir, ".gitignore");
writeFileSync(gitignorePath, "node_modules\n.fusion\n");
@@ -155,6 +170,8 @@ describe("init command", () => {
const content = readFileSync(gitignorePath, "utf-8");
const fusionMatches = content.match(/\.fusion/g);
const piMatches = content.match(/\.pi/g);
expect(fusionMatches).toHaveLength(1);
expect(piMatches).toHaveLength(1);
});
});

View File

@@ -75,8 +75,8 @@ export async function runInit(options: InitOptions = {}): Promise<void> {
console.log(` ✓ Created .fusion/ directory`);
}
// Add .fusion to .gitignore
await addFusionToGitignore(cwd);
// Add local Fusion/Pi storage directories to .gitignore
await addLocalStorageToGitignore(cwd);
// Create fusion.db (empty SQLite file)
if (!existsSync(dbPath)) {
@@ -164,10 +164,10 @@ async function detectProjectName(dir: string): Promise<string> {
}
/**
* Add .fusion to .gitignore if not already present.
* Idempotent: only adds if not already in the file.
* Add local Fusion/Pi storage directories to .gitignore if not already present.
* Idempotent: only adds missing entries.
*/
async function addFusionToGitignore(cwd: string): Promise<void> {
async function addLocalStorageToGitignore(cwd: string): Promise<void> {
const gitignorePath = join(cwd, ".gitignore");
let content = "";
@@ -179,17 +179,19 @@ async function addFusionToGitignore(cwd: string): Promise<void> {
}
}
// Check if .fusion is already in the file
const lines = content.split(/\r?\n/);
if (lines.some((line) => line.trim() === ".fusion")) {
return; // Already present, skip
const existingEntries = new Set(lines.map((line) => line.trim()));
const missingEntries = [".fusion", ".pi"].filter((entry) => !existingEntries.has(entry));
if (missingEntries.length === 0) {
return;
}
// Append .fusion to .gitignore
const newContent = content.endsWith("\n") ? content + ".fusion\n" : content + "\n.fusion\n";
const prefix = content.length === 0 || content.endsWith("\n") ? "" : "\n";
const newContent = `${content}${prefix}${missingEntries.join("\n")}\n`;
try {
writeFileSync(gitignorePath, newContent);
console.log(` ✓ Added .fusion to .gitignore`);
console.log(` ✓ Added ${missingEntries.join(" and ")} to .gitignore`);
} catch {
// Best-effort: don't fail init if we can't write to .gitignore
console.log(` ⚠ Could not update .gitignore (best-effort)`);

View File

@@ -39,6 +39,7 @@ import {
import { promptForPort } from "./port-prompt.js";
import { createReadOnlyProviderSettingsView, createProjectSettingsPersistence } from "./provider-settings.js";
import { wrapAuthStorageWithApiKeyProviders } from "./provider-auth.js";
import { getFusionAuthPath } from "./auth-paths.js";
const DIAGNOSTIC_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
let diagnosticIntervalHandle: ReturnType<typeof setInterval> | null = null;
@@ -383,7 +384,7 @@ export async function runServe(
const missionExecutionLoop = cwdEngine.getRuntime().getMissionExecutionLoop();
const automationStore = cwdEngine.getAutomationStore();
const authStorage = AuthStorage.create();
const authStorage = AuthStorage.create(getFusionAuthPath());
const modelRegistry = new ModelRegistry(authStorage);
// PackageManager may be used for skills adapter even if extension loading fails

View File

@@ -204,6 +204,6 @@ export async function runSkillsInstall(
}
console.log(
`Installed skill from ${source}. Skills are discovered from .pi/skills/ and .agents/skills/.`,
`Installed skill from ${source}. Skills are discovered from .fusion/skills/, legacy .pi/skills/, and .agents/skills/.`,
);
}