import { readFileSync } from "node:fs"; import { join } from "node:path"; import { piDataDir } from "./paths"; export const CONFIG_FILE_NAME = "pi-tools.json"; const LEGACY_CONFIG_FILE_NAME = "pi-fff.json"; export const VALID_MODES = ["tools-and-ui", "tools-only", "override"] as const; export type FffMode = (typeof VALID_MODES)[number]; export interface FffConfig { $schema?: string; mode?: FffMode; frecencyDbPath?: string; historyDbPath?: string; enableFsRootScanning?: boolean; enableHomeDirScanning?: boolean; } const CONFIG_KEYS = new Set([ "$schema", "mode", "frecencyDbPath", "historyDbPath", "enableFsRootScanning", "enableHomeDirScanning", ]); export function loadConfig(agentDir = piDataDir()): FffConfig { let configPath = join(agentDir, CONFIG_FILE_NAME); const legacyConfigPath = join(agentDir, LEGACY_CONFIG_FILE_NAME); let contents: string; try { contents = readFileSync(configPath, "utf8"); } catch (error: unknown) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") { throw new Error( `Could not read pi-tools config at ${configPath}: ${errorMessage(error)}`, ); } try { contents = readFileSync(legacyConfigPath, "utf8"); configPath = legacyConfigPath; } catch (legacyError: unknown) { if ((legacyError as NodeJS.ErrnoException).code === "ENOENT") return {}; throw new Error( `Could not read pi-tools config at ${legacyConfigPath}: ${errorMessage(legacyError)}`, ); } } let parsed: unknown; try { parsed = JSON.parse(contents); } catch (error: unknown) { throw invalidConfig(configPath, `not valid JSON (${errorMessage(error)})`); } if (!isRecord(parsed)) { throw invalidConfig(configPath, "expected a JSON object"); } for (const key of Object.keys(parsed)) { if (!CONFIG_KEYS.has(key as keyof FffConfig)) { throw invalidConfig(configPath, `unknown option "${key}"`); } } if (parsed.mode !== undefined && !VALID_MODES.includes(parsed.mode as FffMode)) { throw invalidConfig(configPath, `"mode" must be one of ${VALID_MODES.join(", ")}`); } validateString(configPath, parsed, "$schema"); validateString(configPath, parsed, "frecencyDbPath"); validateString(configPath, parsed, "historyDbPath"); validateBoolean(configPath, parsed, "enableFsRootScanning"); validateBoolean(configPath, parsed, "enableHomeDirScanning"); return parsed as FffConfig; } function invalidConfig(configPath: string, reason: string): Error { return new Error(`Invalid pi-tools config at ${configPath}: ${reason}`); } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function validateString( configPath: string, config: Record, key: "$schema" | "frecencyDbPath" | "historyDbPath", ): void { const value = config[key]; if (value !== undefined && (typeof value !== "string" || value.length === 0)) { throw invalidConfig(configPath, `"${key}" must be a non-empty string`); } } function validateBoolean( configPath: string, config: Record, key: "enableFsRootScanning" | "enableHomeDirScanning", ): void { const value = config[key]; if (value !== undefined && typeof value !== "boolean") { throw invalidConfig(configPath, `"${key}" must be a boolean`); } }