fix(FN-1952): use fusion storage for pi config
This commit is contained in:
34
packages/dashboard/src/auth-paths.ts
Normal file
34
packages/dashboard/src/auth-paths.ts
Normal 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"),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user