import { homedir } from "node:os"; import { join } from "node:path"; import type { ConfigError, ConfigPaths, LoadOptions, LoadResult, Scope, ValidatedSource } from "./types"; import { CONFIG_DIR_NAME } from "./types"; import { ConfigValidationError, freezeConfig, mergeSources, validateSource } from "./schema"; export function configPaths(cwd: string, homeDir = homedir()): ConfigPaths { return { global: join(homeDir, CONFIG_DIR_NAME, "agent", "fallback-models.json"), project: join(cwd, CONFIG_DIR_NAME, "fallback-models.json") }; } export interface ReadSource { scope: Scope; path: string; raw?: Record; value?: ValidatedSource; error?: ConfigError; } const readSource = async (fs: LoadOptions["fs"], scope: Scope, path: string): Promise => { try { const text = await fs.read(path); if (text === undefined) return { scope, path }; const raw = JSON.parse(text); return { scope, path, raw, value: validateSource(raw, path) }; } catch (error) { const message = error instanceof ConfigValidationError ? error.message : `${path}: invalid JSON`; return { scope, path, error: { scope, path, message } }; } }; export async function readSources(options: LoadOptions): Promise<[ReadSource, ReadSource]> { const paths = configPaths(options.cwd, options.homeDir); return Promise.all([readSource(options.fs, "global", paths.global), readSource(options.fs, "project", paths.project)]); } export async function loadConfig(options: LoadOptions): Promise { const paths = configPaths(options.cwd, options.homeDir); const [global, project] = await readSources(options); const errors: ConfigError[] = [global.error, project.error].filter((error): error is ConfigError => !!error); // mergeSources is total for already validated sources (AD-3): the project-over-global overlay cannot // synthesize a local violation, so an unexpected merge exception is a programming failure, not // evidence that the project source is invalid — it must surface rather than silently drop the // project source the way the old merged-cycle catch did. const config = mergeSources(global.value, project.value); for (const error of errors) options.ui?.notify(error.message); return { config, errors: Object.freeze(errors), paths }; }