/** * Canonical ~/.pi/agent/* path helpers and JSON file I/O. * * Both pi-credential-vault and pi-multicodex read and write files under * the agent directory. This module provides a single, permission-aware * set of helpers so path construction and file I/O are not duplicated. */ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { mkdir, readFile, stat, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { getAgentDir } from "@mariozechner/pi-coding-agent"; // --------------------------------------------------------------------------- // Path helpers // --------------------------------------------------------------------------- /** * Resolve a path relative to the agent directory (~/.pi/agent/). * * When called with no arguments, returns the agent directory itself. */ export function getAgentPath(...segments: string[]): string { return join(getAgentDir(), ...segments); } /** * Get the canonical path to the shared settings file. * * Returns `~/.pi/agent/settings.json`. */ export function getAgentSettingsPath(): string { return getAgentPath("settings.json"); } /** * Get the canonical path to the native auth credentials file. * * Returns `~/.pi/agent/auth.json`. */ export function getAgentAuthPath(): string { return getAgentPath("auth.json"); } // --------------------------------------------------------------------------- // Directory helpers // --------------------------------------------------------------------------- /** * Ensure the parent directory of a file path exists. * * Creates intermediate directories as needed with mode 0o755. * No-op if the directory already exists. */ export function ensureParentDir(filePath: string): void { const dir = dirname(filePath); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true, mode: 0o755 }); } } /** * Async variant of ensureParentDir. */ export async function ensureParentDirAsync(filePath: string): Promise { const dir = dirname(filePath); try { await stat(dir); } catch { await mkdir(dir, { recursive: true, mode: 0o755 }); } } // --------------------------------------------------------------------------- // JSON file I/O (sync) // --------------------------------------------------------------------------- /** * Read a JSON file and return its contents as a plain object. * * Returns an empty object if the file does not exist, is not valid JSON, * or does not contain a JSON object at the top level. */ export function readJsonObjectFile(filePath: string): Record { try { if (!existsSync(filePath)) { return {}; } const raw = readFileSync(filePath, "utf-8"); const parsed: unknown = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return {}; } return parsed as Record; } catch { return {}; } } /** * Write a plain object to a JSON file. * * Creates parent directories if they do not exist. * Uses 2-space indentation for human readability. */ export function writeJsonObjectFile( filePath: string, data: Record, ): void { ensureParentDir(filePath); writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8"); } // --------------------------------------------------------------------------- // JSON file I/O (async) // --------------------------------------------------------------------------- /** * Async variant of readJsonObjectFile. */ export async function readJsonObjectFileAsync( filePath: string, ): Promise> { try { const stats = await stat(filePath).catch(() => undefined); if (!stats) { return {}; } const raw = await readFile(filePath, "utf-8"); const parsed: unknown = JSON.parse(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return {}; } return parsed as Record; } catch { return {}; } } /** * Async variant of writeJsonObjectFile. */ export async function writeJsonObjectFileAsync( filePath: string, data: Record, ): Promise { await ensureParentDirAsync(filePath); await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8"); }