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

This commit is contained in:
gsxdsm
2026-04-16 20:12:09 -07:00
parent 00918dcff2
commit 35c89af4b6
15 changed files with 210 additions and 180 deletions

View File

@@ -0,0 +1,34 @@
import path from "node:path";
import { homedir } from "node:os";
export interface StoredAuthProvider {
type: string;
key?: string;
access?: string;
refresh?: string;
expires?: number;
accountId?: string;
}
export function getFusionAgentDir(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
return path.join(home, ".fusion", "agent");
}
export function getFusionAuthPath(home = process.env.HOME || process.env.USERPROFILE || homedir()): string {
return path.join(getFusionAgentDir(home), "auth.json");
}
export function getAuthFileCandidates(
cwd = process.cwd(),
home = process.env.HOME || process.env.USERPROFILE || homedir(),
): string[] {
return [
path.join(home, ".fusion", "agent", "auth.json"),
path.join(home, ".fusion", "auth.json"),
path.join(cwd, ".fusion", "agent", "auth.json"),
path.join(cwd, ".fusion", "auth.json"),
path.join(home, ".pi", "agent", "auth.json"),
path.join(home, ".pi", "auth.json"),
];
}

View File

@@ -63,9 +63,22 @@ import {
} from "./api-error.js";
import { rateLimit, RATE_LIMITS } from "./rate-limit.js";
import { resolvePluginManifest } from "./plugin-routes.js";
import { getAuthFileCandidates, getFusionAuthPath, type StoredAuthProvider } from "./auth-paths.js";
const TASK_DETAIL_ACTIVITY_LOG_LIMIT = 500;
async function readStoredAuthProvidersFromDisk(): Promise<Record<string, StoredAuthProvider>> {
for (const authJsonPath of getAuthFileCandidates()) {
try {
const authContent = await fsReadFile(authJsonPath, "utf-8");
return JSON.parse(authContent) as Record<string, StoredAuthProvider>;
} catch {
// Try the next candidate, preferring .fusion and falling back to legacy .pi.
}
}
return {};
}
/**
* Minimal interface matching pi-coding-agent's ModelRegistry API surface
* used by the models route. Avoids a direct dependency on the pi-coding-agent package.
@@ -2673,7 +2686,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
// Import AuthStorage and write credentials
const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
const authStorage = AuthStorage.create();
const authStorage = AuthStorage.create(getFusionAuthPath());
const receivedProviders: string[] = [];
for (const [providerId, credential] of Object.entries(providers)) {
@@ -2733,20 +2746,7 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
// Get local node ID
const localPeerInfo = await central.getLocalPeerInfo();
// Import AuthStorage and read credentials
const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
const authStorage = AuthStorage.create();
// Read auth.json directly (async to avoid blocking event loop)
const authJsonPath = `${process.env.HOME || process.env.USERPROFILE}/.pi/agent/auth.json`;
let allProviders: Record<string, { type: string; key?: string; access?: string; refresh?: string; expires?: number; accountId?: string }> = {};
try {
const authContent = await fsReadFile(authJsonPath, "utf-8");
allProviders = JSON.parse(authContent);
} catch {
// Auth file doesn't exist - export empty
}
const allProviders = await readStoredAuthProvidersFromDisk();
// Filter to only API-key-based providers (skip OAuth)
const apiKeyProviders: Record<string, { type: string; key: string }> = {};
@@ -14944,23 +14944,14 @@ export function createApiRoutes(store: TaskStore, options?: ServerOptions): Rout
// Import AuthStorage
const { AuthStorage } = await import("@mariozechner/pi-coding-agent");
const authStorage = AuthStorage.create();
const authStorage = AuthStorage.create(getFusionAuthPath());
if (direction === "push") {
// Get OAuth provider IDs to exclude
const oauthProviders = authStorage.getOAuthProviders();
const oauthIds = new Set(oauthProviders.map((p) => p.id));
// Read auth.json directly to get all providers (async to avoid blocking event loop)
const authJsonPath = `${process.env.HOME || process.env.USERPROFILE}/.pi/agent/auth.json`;
let allProviders: Record<string, { type: string; key?: string; access?: string; refresh?: string; expires?: number; accountId?: string }> = {};
try {
const authContent = await fsReadFile(authJsonPath, "utf-8");
allProviders = JSON.parse(authContent);
} catch {
// Auth file doesn't exist or is unreadable - sync empty
}
const allProviders = await readStoredAuthProvidersFromDisk();
// Filter to only API-key-based providers (skip OAuth)
const apiKeyProviders: Record<string, { type: string; key: string }> = {};
@@ -17201,7 +17192,7 @@ function registerModelsRoute(router: Router, modelRegistry?: ModelRegistryLike,
/**
* Register authentication status, login, and logout routes.
* Uses pi-coding-agent's AuthStorage for credential management.
* If no AuthStorage is provided, creates one internally (reads from ~/.pi/agent/auth.json).
* If no AuthStorage is provided, authentication routes return an unavailable status.
*/
function registerAuthRoutes(router: Router, authStorage?: AuthStorageLike): void {
// Use injected AuthStorage or fail gracefully if not provided.

View File

@@ -1,8 +1,13 @@
import { readFile, writeFile, access, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { homedir } from "node:os";
import { join, resolve } from "node:path";
const SCRIPTS_FILE = join(homedir(), ".pi", "fusion", "scripts.json");
function projectScriptsFile(projectDir: string): string {
return join(resolve(projectDir), ".fusion", "scripts.json");
}
function legacyProjectScriptsFile(projectDir: string): string {
return join(resolve(projectDir), ".pi", "fusion", "scripts.json");
}
interface ScriptsData {
scripts: Record<string, string>;
@@ -11,17 +16,28 @@ interface ScriptsData {
class ScriptStore {
private scripts: Record<string, string> = {};
private filePath: string;
private legacyFilePath?: string;
constructor(filePath: string) {
constructor(filePath: string, legacyFilePath?: string) {
this.filePath = filePath;
this.legacyFilePath = legacyFilePath;
}
async load(): Promise<void> {
const paths = this.legacyFilePath ? [this.filePath, this.legacyFilePath] : [this.filePath];
try {
await access(this.filePath);
const content = await readFile(this.filePath, "utf-8");
const data = JSON.parse(content) as ScriptsData;
this.scripts = data.scripts || {};
for (const path of paths) {
try {
await access(path);
const content = await readFile(path, "utf-8");
const data = JSON.parse(content) as ScriptsData;
this.scripts = data.scripts || {};
return;
} catch {
// Try the next candidate.
}
}
this.scripts = {};
} catch {
// File doesn't exist or is invalid - start with empty scripts
this.scripts = {};
@@ -57,18 +73,22 @@ class ScriptStore {
}
}
let storeInstance: ScriptStore | null = null;
const storeInstances = new Map<string, ScriptStore>();
export async function loadScriptStore(): Promise<ScriptStore> {
if (!storeInstance) {
storeInstance = new ScriptStore(SCRIPTS_FILE);
await storeInstance.load();
export async function loadScriptStore(projectDir: string): Promise<ScriptStore> {
const scriptsFile = projectScriptsFile(projectDir);
let store = storeInstances.get(scriptsFile);
if (!store) {
store = new ScriptStore(scriptsFile, legacyProjectScriptsFile(projectDir));
storeInstances.set(scriptsFile, store);
await store.load();
}
return storeInstance;
return store;
}
export function resetScriptStore(): void {
storeInstance = null;
storeInstances.clear();
}
export { projectScriptsFile, legacyProjectScriptsFile };
export type { ScriptStore };

View File

@@ -2,6 +2,7 @@ import * as path from "node:path";
import { readFile } from "node:fs/promises";
import * as https from "node:https";
import * as child_process from "node:child_process";
import { getAuthFileCandidates } from "./auth-paths.js";
function execFileAsync(
file: string,
@@ -228,19 +229,7 @@ function decodeJwtPayload(token: string): any {
}
}
// ── Pi auth storage reader ──────────────────────────────────────────────────
function getAuthFileCandidates(): string[] {
const home = process.env.HOME || "~";
return [
path.join(home, ".pi", "agent", "auth.json"),
path.join(home, ".pi", "auth.json"),
path.join(home, ".fusion", "agent", "auth.json"),
path.join(home, ".fusion", "auth.json"),
path.join(process.cwd(), ".fusion", "agent", "auth.json"),
path.join(process.cwd(), ".fusion", "auth.json"),
];
}
// ── Auth storage reader ──────────────────────────────────────────────────
async function readAuthKeyFromFile(authPath: string, provider: string): Promise<string | null> {
try {