import { promises as fs, readFileSync } from "node:fs"; import { getAgentDir } from "@earendil-works/pi-coding-agent"; import { dirname, join } from "node:path"; import type { IconMode } from "./icons.js"; export const CONFIG_PATH = join(getAgentDir(), "liuovo.json"); /** Config path redirect for hermetic tests and sandboxed runs; unset means the user path. */ export function defaultConfigPath(): string { return process.env.PI_LIUOVO_CONFIG ?? CONFIG_PATH; } export type LiuovoStateSource = "environment" | "file" | "default"; export interface LiuovoState { enabled: boolean; source: LiuovoStateSource; } export type LiuovoMode = "default" | "reasoning" | "result"; /** Parse the documented boolean forms; unknown values do not override config. */ export function parseEnabled(value: unknown): boolean | undefined { if (typeof value === "boolean") return value; if (typeof value !== "string") return undefined; switch (value.trim().toLowerCase()) { case "1": case "true": case "on": case "yes": return true; case "0": case "false": case "off": case "no": return false; default: return undefined; } } /** Resolve startup state. A valid environment override always wins. */ export function loadLiuovoState( options: { envValue?: string; configPath?: string } = {} ): LiuovoState { const envValue = options.envValue ?? process.env.PI_LIUOVO; const envEnabled = parseEnabled(envValue); if (envEnabled !== undefined) return { enabled: envEnabled, source: "environment" }; const configPath = options.configPath ?? defaultConfigPath(); try { const parsed = JSON.parse(readFileSync(configPath, "utf8")); if (typeof parsed?.enabled === "boolean") return { enabled: parsed.enabled, source: "file" }; } catch { // Missing, unreadable, and malformed config all preserve the enabled default. } return { enabled: true, source: "default" }; } /** Resolve decorative icon visibility; only an explicit false disables them. */ export function loadLiuovoIcons(configPath = defaultConfigPath()): boolean { try { const parsed = JSON.parse(readFileSync(configPath, "utf8")); if ( parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.icons === false ) return false; } catch { // Missing, unreadable, and malformed config all preserve visible icons. } return true; } export function loadLiuovoIconMode(configPath = defaultConfigPath()): IconMode { try { const mode = JSON.parse(readFileSync(configPath, "utf8"))?.iconMode; if (mode === "nerd" || mode === "ascii") return mode; } catch { /* Invalid/missing preferences retain automatic selection. */ } return "auto"; } export function saveLiuovoIconMode(mode: IconMode, configPath = defaultConfigPath()): Promise { return updateConfig({ iconMode: mode }, configPath); } export function loadLiuovoMode(configPath = defaultConfigPath()): LiuovoMode { try { const mode = JSON.parse(readFileSync(configPath, "utf8"))?.mode; if (mode === "reasoning" || mode === "result") return mode; } catch { // Missing and malformed config preserve the default layout. } return "default"; } async function updateConfig( update: Record, configPath: string ): Promise { let current: Record = {}; try { const parsed = JSON.parse(readFileSync(configPath, "utf8")); if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) current = parsed; } catch { // Missing and malformed config are replaced with valid extension config. } const directory = dirname(configPath); const temporaryPath = `${configPath}.${process.pid}.${Date.now()}.tmp`; await fs.mkdir(directory, { recursive: true }); try { await fs.writeFile( temporaryPath, `${JSON.stringify({ ...current, ...update }, null, 2)}\n`, "utf8" ); await fs.rename(temporaryPath, configPath); } catch (error) { await fs.rm(temporaryPath, { force: true }).catch(() => {}); throw error; } } /** Persist only extension-owned settings outside the package checkout. */ export async function saveLiuovoEnabled( enabled: boolean, configPath = defaultConfigPath() ): Promise { await updateConfig({ enabled }, configPath); } export async function saveLiuovoMode( mode: LiuovoMode, configPath = defaultConfigPath() ): Promise { await updateConfig({ mode }, configPath); } export async function saveLiuovoIcons( icons: boolean, configPath = defaultConfigPath() ): Promise { await updateConfig({ icons }, configPath); }