/** * v2.3 provider config file (`/data/coder-providers.json`) — schema + loader. * * Layers config-backed overrides/custom-ACP entries over the hardcoded built-ins * (see provider-config-registry.ts). Loading NEVER throws at startup (design.md * §2.1): a missing file, invalid JSON, or schema mismatch all fall back to * `{ providers: {} }` (built-ins only, all enabled). * * Schemas are defined once in @boocode/contracts/provider-config and re-exported * here so existing importers (routes, tests, registry) don't need path changes. */ import { readFileSync, writeFileSync } from 'node:fs'; import { ProviderOverrideSchema, CoderProvidersFileSchema, ProviderConfigPatchSchema, type ProviderOverride, type CoderProvidersFile, type ProviderConfigPatch, } from '@boocode/contracts/provider-config'; export { ProviderOverrideSchema, CoderProvidersFileSchema, ProviderConfigPatchSchema, type ProviderOverride, type CoderProvidersFile, type ProviderConfigPatch, }; /** * Shallow per-id merge (design.md §6.2 / Paseo `patchConfig`). Each key in * `patch.providers` REPLACES that id's override object wholesale (NOT a deep * field merge); a `null` value DELETES the override. Returns a new object — * never mutates `current`. The result is a plain CoderProvidersFile (no nulls), * which the route re-validates against CoderProvidersFileSchema before save. */ export function mergeProviderConfigPatch( current: CoderProvidersFile, patch: ProviderConfigPatch, ): CoderProvidersFile { const providers: Record = { ...current.providers }; for (const [id, override] of Object.entries(patch.providers)) { if (override === null) { delete providers[id]; } else { providers[id] = override; } } return { providers }; } /** Read + parse + validate. Falls back to built-ins-only on any failure; never throws. */ export function load(path: string): CoderProvidersFile { let raw: string; try { raw = readFileSync(path, 'utf8'); } catch { // Missing file → built-ins only. Expected, not an error. return { providers: {} }; } let json: unknown; try { json = JSON.parse(raw); } catch (err) { console.error(`provider-config: invalid JSON in ${path} — using built-ins only`, err); return { providers: {} }; } const parsed = CoderProvidersFileSchema.safeParse(json); if (!parsed.success) { console.error( `provider-config: schema validation failed for ${path} — using built-ins only`, parsed.error.flatten(), ); return { providers: {} }; } return parsed.data; } /** Write the config back to disk (used by the Phase 4 PATCH route). */ export function save(path: string, config: CoderProvidersFile): void { writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, 'utf8'); }