import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { artifactsRoot } from "./artifact-root.ts"; import type { ViewerWindowMode } from "./viewer-launcher.ts"; /** * Persisted viewer preferences (Phase C/D UX). Lets a user set how `/viewer` * behaves once and have it stick across sessions, instead of re-exporting an * env var each time. Stored as a tiny JSON file alongside the artifact store * (rebrand-safe path), separate from the artifacts themselves. * * { "viewerMode": "app" | "browser" | "none", "autoOpen": boolean } * * - `viewerMode` — how the viewer opens. Launch precedence: * env override > this saved setting > built-in default (`app`). * - `autoOpen` — whether a successful render auto-shows the artifact * (reusing an open window). Defaults to `true` when unset. */ export type ViewerModePreference = ViewerWindowMode; interface ViewerConfig { viewerMode?: ViewerModePreference; autoOpen?: boolean; } const VALID_MODES: readonly ViewerModePreference[] = ["app", "browser", "none"]; function viewerConfigPath(): string { return join(artifactsRoot(), "config.json"); } export function isViewerMode(value: unknown): value is ViewerModePreference { return ( typeof value === "string" && VALID_MODES.includes(value as ViewerModePreference) ); } /** Read the whole config, or `{}` when missing/corrupt. Never throws. */ async function readConfig(path: string): Promise { let raw: string; try { raw = await readFile(path, "utf8"); } catch { return {}; } try { const parsed = JSON.parse(raw) as ViewerConfig; return parsed && typeof parsed === "object" ? parsed : {}; } catch { return {}; } } /** Merge a patch into the config, creating the store directory if needed. */ async function writeConfig(patch: ViewerConfig, path: string): Promise { const next = { ...(await readConfig(path)), ...patch }; await mkdir(join(path, ".."), { recursive: true }); await writeFile(path, `${JSON.stringify(next, null, 2)}\n`); } /** Saved viewer mode, or `undefined` when unset/invalid. */ export async function readViewerMode( path = viewerConfigPath(), ): Promise { const { viewerMode } = await readConfig(path); return isViewerMode(viewerMode) ? viewerMode : undefined; } export async function writeViewerMode( mode: ViewerModePreference, path = viewerConfigPath(), ): Promise { await writeConfig({ viewerMode: mode }, path); } /** Whether render auto-open is enabled. Defaults to `true` when unset. */ export async function readAutoOpen( path = viewerConfigPath(), ): Promise { const { autoOpen } = await readConfig(path); return typeof autoOpen === "boolean" ? autoOpen : true; } export async function writeAutoOpen( enabled: boolean, path = viewerConfigPath(), ): Promise { await writeConfig({ autoOpen: enabled }, path); }