/** * 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). */ import { readFileSync, writeFileSync } from 'node:fs'; import { z } from 'zod'; // Schemas verbatim from design.md §2.2. export const ProviderOverrideSchema = z.object({ extends: z.enum(['acp']).optional(), // v2.3: only 'acp' for custom; built-ins omit extends label: z.string().min(1).optional(), description: z.string().optional(), command: z.array(z.string().min(1)).min(1).optional(), // [binary, ...args] env: z.record(z.string()).optional(), enabled: z.boolean().optional(), // default true order: z.number().int().optional(), // UI sort key models: z.array(z.object({ id: z.string(), label: z.string() })).optional(), additionalModels: z.array(z.object({ id: z.string(), label: z.string() })).optional(), }); export const CoderProvidersFileSchema = z.object({ providers: z.record(ProviderOverrideSchema).default({}), }); export type ProviderOverride = z.infer; export type CoderProvidersFile = z.infer; /** 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'); }