import { randomUUID } from "node:crypto"; import { chmod, mkdir, open, readFile, rename, stat, unlink, writeFile, } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import type { ProviderCloneDefinition, ProviderCloneStore } from "./types.js"; import { PROVIDER_ID_PATTERN } from "./validation.js"; export const CLONE_STORE_FILENAME = "provider-clones.json"; const STORE_LOCK_TIMEOUT_MS = 5_000; const STORE_LOCK_RETRY_MS = 25; const STORE_LOCK_STALE_MS = 30_000; export class CloneStoreError extends Error { readonly storePath: string; constructor(message: string, storePath: string, options?: ErrorOptions) { super(message, options); this.name = "CloneStoreError"; this.storePath = storePath; } } export interface CloneStoreOperationOptions { signal?: AbortSignal; } export function emptyCloneStore(): ProviderCloneStore { return { version: 1, clones: [] }; } export function getCloneStorePath( env: NodeJS.ProcessEnv = process.env, homeDirectory = homedir(), ): string { let agentDirectory = env.PI_CODING_AGENT_DIR || join(homeDirectory, ".pi", "agent"); if (agentDirectory === "~") { agentDirectory = homeDirectory; } else if (agentDirectory.startsWith("~/")) { agentDirectory = join(homeDirectory, agentDirectory.slice(2)); } return join(agentDirectory, CLONE_STORE_FILENAME); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function parseDefinition(value: unknown, index: number): ProviderCloneDefinition { if (!isRecord(value)) { throw new Error(`clones[${index}] must be an object`); } const { sourceId, targetId, createdAt } = value; if (typeof sourceId !== "string" || !PROVIDER_ID_PATTERN.test(sourceId)) { throw new Error(`clones[${index}].sourceId is not a valid provider ID`); } if (typeof targetId !== "string" || !PROVIDER_ID_PATTERN.test(targetId)) { throw new Error(`clones[${index}].targetId is not a valid provider ID`); } if (sourceId === targetId) { throw new Error(`clones[${index}] has identical source and target IDs`); } if ( typeof createdAt !== "string" || !Number.isFinite(Date.parse(createdAt)) || new Date(createdAt).toISOString() !== createdAt ) { throw new Error(`clones[${index}].createdAt must be an ISO date string`); } return { sourceId, targetId, createdAt }; } export function parseCloneStore(value: unknown): ProviderCloneStore { if (!isRecord(value)) { throw new Error("store must be an object"); } if (value.version !== 1) { throw new Error(`unsupported store version: ${String(value.version)}`); } if (!Array.isArray(value.clones)) { throw new Error("clones must be an array"); } const clones = value.clones.map(parseDefinition); const targetIds = new Set(); for (const definition of clones) { if (targetIds.has(definition.targetId)) { throw new Error(`duplicate clone target ID: ${definition.targetId}`); } targetIds.add(definition.targetId); } for (const definition of clones) { if (targetIds.has(definition.sourceId)) { throw new Error( `clone-of-clone definitions are not supported: ${definition.sourceId} -> ${definition.targetId}`, ); } } return { version: 1, clones }; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function isMissingFileError(error: unknown): boolean { return isRecord(error) && error.code === "ENOENT"; } function isAlreadyExistsError(error: unknown): boolean { return isRecord(error) && error.code === "EEXIST"; } function createAbortError(): Error { const error = new Error("Provider clone store operation aborted"); error.name = "AbortError"; return error; } function isAbortError(error: unknown): boolean { return ( error instanceof Error && (error.name === "AbortError" || (error as Error & { code?: unknown }).code === "ABORT_ERR") ); } function throwIfAborted(signal: AbortSignal | undefined): void { if (signal?.aborted) throw createAbortError(); } function delay(milliseconds: number, signal?: AbortSignal): Promise { if (!signal) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } if (signal.aborted) return Promise.reject(createAbortError()); return new Promise((resolve, reject) => { const onAbort = () => { clearTimeout(timer); signal.removeEventListener("abort", onAbort); reject(createAbortError()); }; const timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); resolve(); }, milliseconds); signal.addEventListener("abort", onAbort, { once: true }); }); } async function acquireStoreLock( storePath: string, signal?: AbortSignal, ): Promise<() => Promise> { const directory = dirname(storePath); const lockPath = `${storePath}.lock`; const deadline = Date.now() + STORE_LOCK_TIMEOUT_MS; throwIfAborted(signal); await mkdir(directory, { recursive: true }); for (;;) { throwIfAborted(signal); try { const handle = await open(lockPath, "wx", 0o600); try { throwIfAborted(signal); await handle.writeFile(`${process.pid}\n`, "utf8"); throwIfAborted(signal); } catch (error) { await handle.close().catch(() => undefined); await unlink(lockPath).catch(() => undefined); throw error; } return async () => { await handle.close().catch(() => undefined); await unlink(lockPath).catch(() => undefined); }; } catch (error) { if (isAbortError(error)) throw error; if (!isAlreadyExistsError(error)) { throw new CloneStoreError( `Unable to save provider clone store "${storePath}": unable to acquire update lock: ${errorMessage(error)}`, storePath, { cause: error }, ); } try { const lockStat = await stat(lockPath); if (Date.now() - lockStat.mtimeMs > STORE_LOCK_STALE_MS) { await unlink(lockPath); continue; } } catch (lockError) { if (isMissingFileError(lockError)) continue; } if (Date.now() >= deadline) { throw new CloneStoreError( `Unable to save provider clone store "${storePath}": timed out waiting for update lock.`, storePath, ); } await delay(STORE_LOCK_RETRY_MS, signal); } } } export async function loadCloneStore(storePath = getCloneStorePath()): Promise { let contents: string; try { contents = await readFile(storePath, "utf8"); } catch (error) { if (isMissingFileError(error)) return emptyCloneStore(); throw new CloneStoreError( `Unable to read provider clone store "${storePath}": ${errorMessage(error)}`, storePath, { cause: error }, ); } try { return parseCloneStore(JSON.parse(contents) as unknown); } catch (error) { throw new CloneStoreError( `Invalid provider clone store "${storePath}": ${errorMessage(error)}`, storePath, { cause: error }, ); } } export async function updateCloneStore( update: ( current: ProviderCloneStore, ) => ProviderCloneStore | Promise, storePath = getCloneStorePath(), options: CloneStoreOperationOptions = {}, ): Promise { const release = await acquireStoreLock(storePath, options.signal); try { throwIfAborted(options.signal); const next = await update(await loadCloneStore(storePath)); throwIfAborted(options.signal); await saveCloneStore(next, storePath, options); throwIfAborted(options.signal); return next; } finally { await release(); } } export async function saveCloneStore( store: ProviderCloneStore, storePath = getCloneStorePath(), options: CloneStoreOperationOptions = {}, ): Promise { let validated: ProviderCloneStore; try { validated = parseCloneStore(store); } catch (error) { throw new CloneStoreError( `Refusing to write invalid provider clone store "${storePath}": ${errorMessage(error)}`, storePath, { cause: error }, ); } throwIfAborted(options.signal); const directory = dirname(storePath); const temporaryPath = join( directory, `.${CLONE_STORE_FILENAME}.${process.pid}.${randomUUID()}.tmp`, ); try { await mkdir(directory, { recursive: true }); throwIfAborted(options.signal); await writeFile(temporaryPath, `${JSON.stringify(validated, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600, }); throwIfAborted(options.signal); await chmod(temporaryPath, 0o600); throwIfAborted(options.signal); await rename(temporaryPath, storePath); } catch (error) { await unlink(temporaryPath).catch(() => undefined); if (isAbortError(error)) throw error; throw new CloneStoreError( `Unable to save provider clone store "${storePath}": ${errorMessage(error)}`, storePath, { cause: error }, ); } }