import { randomUUID } from "node:crypto"; import { mkdir, readdir, readFile, rename, unlink, writeFile, } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join, posix, win32 } from "node:path"; export interface HostIdentity { hostId: string; alias: string; } export interface SessionRecord { id: string; key: string; name?: string; cwd: string; created: string; } export interface HostIndex extends HostIdentity { version: 1; sessions: SessionRecord[]; } export interface HostCatalog { version: 1; hosts: HostIdentity[]; } export interface CatalogCache { refreshedAt: string; catalog: HostCatalog; indexes: HostIndex[]; } export interface StatePaths { identity: string; cache: string; pendingDir: string; } function platformRoot( kind: "state" | "cache", env: NodeJS.ProcessEnv, platform: NodeJS.Platform, home: string, ): string { if (platform === "win32") { const paths = win32; const override = kind === "state" ? env.XDG_STATE_HOME : env.XDG_CACHE_HOME; if (override?.trim()) return paths.join(override, "pi", "bak"); return paths.join( env.LOCALAPPDATA ?? paths.join(home, "AppData", "Local"), "pi", "bak", kind, ); } if (platform === "darwin") { const override = kind === "state" ? env.XDG_STATE_HOME : env.XDG_CACHE_HOME; if (override?.trim()) return posix.join(override, "pi", "bak"); return posix.join( home, "Library", kind === "state" ? "Application Support" : "Caches", "pi", "bak", ); } const override = kind === "state" ? env.XDG_STATE_HOME : env.XDG_CACHE_HOME; const fallback = kind === "state" ? posix.join(home, ".local", "state") : posix.join(home, ".cache"); return posix.join(override?.trim() || fallback, "pi", "bak"); } export function statePaths( env: NodeJS.ProcessEnv = process.env, platform: NodeJS.Platform = process.platform, home = homedir(), ): StatePaths { const paths = platform === "win32" ? win32 : posix; const state = platformRoot("state", env, platform, home); return { identity: paths.join(state, "identity.json"), cache: paths.join( platformRoot("cache", env, platform, home), "catalog.json", ), pendingDir: paths.join(state, "pending"), }; } export function normalizeAlias(value: string): string | undefined { const alias = value.trim().toLowerCase(); return /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/.test(alias) ? alias : undefined; } export function newIdentity(alias: string): HostIdentity { return { hostId: randomUUID(), alias }; } export async function readJson(path: string): Promise { try { return JSON.parse(await readFile(path, "utf8")) as T; } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined; throw error; } } export interface PendingEntry { id: string; path: string; } function pendingMarker(directory: string, id: string): string { return join(directory, `${id}.json`); } export async function markPending( directory: string, path: string, ): Promise { const entry = { id: randomUUID(), path }; await atomicWriteJson(pendingMarker(directory, entry.id), entry); return entry; } export async function clearPending( directory: string, id: string, ): Promise { await unlink(pendingMarker(directory, id)).catch((error) => { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }); } export async function readPending(directory: string): Promise { let names: string[]; try { names = await readdir(directory); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return []; throw error; } const entries: PendingEntry[] = []; for (const name of names) { if (!name.endsWith(".json")) continue; const marker = await readJson<{ id?: unknown; path?: unknown }>( join(directory, name), ); if ( typeof marker?.id === "string" && `${marker.id}.json` === name && typeof marker.path === "string" ) entries.push({ id: marker.id, path: marker.path }); } return entries; } export async function writeJsonExclusive( path: string, value: unknown, ): Promise { await mkdir(dirname(path), { recursive: true, mode: 0o700 }); try { await writeFile(path, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: "wx", }); return true; } catch (error) { if ((error as NodeJS.ErrnoException).code === "EEXIST") return false; throw error; } } export async function atomicWriteJson( path: string, value: unknown, ): Promise { await mkdir(dirname(path), { recursive: true, mode: 0o700 }); const temporary = join(dirname(path), `.${process.pid}-${randomUUID()}.tmp`); await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 }); await rename(temporary, path); }