Move all hand-synced cross-app wire contracts into one built workspace package, @boocode/contracts, consumed by server/web/coder/coder-web via workspace:* + a per-subpath exports map. The ws-frames and provider-config Zod schemas are schema-first (z.infer); MessageMetadata, ErrorReason, AgentSessionConfig, the provider snapshot types, and WorktreeRiskReport are each single-sourced. Deletes the byte-identical copies and their parity tests, fixes a live AgentSessionConfig drift (coder dead copy removed, unified to the web required/nullable shape), removes the dead pending_change WS arms in the fallback SPA, and inverts the build order (contracts builds first) across root build, Dockerfile, and the coder deploy docs. Reverses the shared-package decision declined in v2.5.12. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87 lines
2.8 KiB
TypeScript
87 lines
2.8 KiB
TypeScript
/**
|
|
* 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<string, ProviderOverride> = { ...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');
|
|
}
|