/** * Persistence for the `/search-engine` choice. * * Deliberately the same shape, and the same atomic write, as the harness's * browser-target store: a sibling temp file plus rename, so a crash mid-write * cannot leave a half-written engine name behind. Its own file rather than a * shared settings blob, so a corrupt engine choice cannot cost anything else. * * Like the CDP target, the stored value is the raw string a user would have put * in the environment variable ("ddg", "bing", …), not a pre-parsed id. One * vocabulary, one resolution path in engine-target.ts, and a stored value can * never mean something the variable could not. * * Every read failure degrades to "nothing stored" rather than throwing. */ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { dirname, join } from "node:path"; import { agentDir } from "./paths.ts"; const CURRENT_VERSION = 1; const engineFilePath = (): string => join(agentDir(), "castle-cdp-search-engine.json"); type EngineFile = { readonly version: 1; /** Raw CCS_SEARCH_ENGINE-style value, or null when cleared. */ readonly engine: string | null; readonly savedAt: string; }; /** * The persisted engine, or null when nothing is stored, the file is missing, or * it cannot be parsed. A file written by a newer version is treated as "nothing * stored" rather than guessed at. */ export const readStoredEngine = async (): Promise => { let raw: string; try { raw = await readFile(engineFilePath(), "utf8"); } catch { return null; } try { const parsed: unknown = JSON.parse(raw); if (typeof parsed !== "object" || parsed === null) return null; const file = parsed as Record; if (file["version"] !== CURRENT_VERSION) return null; const engine = file["engine"]; if (typeof engine !== "string") return null; const trimmed = engine.trim(); return trimmed.length > 0 ? trimmed : null; } catch { return null; } }; const write = async (engine: string | null): Promise => { const path = engineFilePath(); const tmp = `${path}.${randomUUID()}.tmp`; const payload: EngineFile = { version: CURRENT_VERSION, engine, savedAt: new Date().toISOString() }; try { await mkdir(dirname(path), { recursive: true }); await writeFile(tmp, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); await rename(tmp, path); } catch (e) { await unlink(tmp).catch(() => {}); throw new Error(`could not save search engine to ${path}: ${e instanceof Error ? e.message : String(e)}`); } }; /** Persist the chosen engine. */ export const writeStoredEngine = (engine: string): Promise => write(engine); /** Clear the choice, restoring the built-in default. */ export const clearStoredEngine = (): Promise => write(null); /** Exposed so the command can tell the user where the choice lives. */ export const storedEngineLocation = (): string => engineFilePath();