import { readFileSync } from 'fs'; import { parse } from 'yaml'; import { configSchema, type Config } from './schema.js'; function expandEnvVars(value: string): string { return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => { const envValue = process.env[envVar]; if (envValue === undefined) { throw new Error(`Environment variable ${envVar} is not set`); } return envValue; }); } function expandEnvVarsInObject(obj: unknown): unknown { if (typeof obj === 'string') { return expandEnvVars(obj); } if (Array.isArray(obj)) { return obj.map(expandEnvVarsInObject); } if (obj !== null && typeof obj === 'object') { const result: Record = {}; for (const [key, value] of Object.entries(obj)) { result[key] = expandEnvVarsInObject(value); } return result; } return obj; } /** * Deep merge two plain objects. Overlay values override base values. * Arrays in overlay replace base arrays (not concatenated). * null in overlay explicitly sets key to null. * Only plain objects recurse; arrays, primitives, and null are overwritten. */ export function deepMerge( base: Record, overlay: Record, ): Record { const result = { ...base }; for (const [key, value] of Object.entries(overlay)) { if ( value !== null && typeof value === 'object' && !Array.isArray(value) && typeof result[key] === 'object' && result[key] !== null && !Array.isArray(result[key]) ) { result[key] = deepMerge( result[key] as Record, value as Record, ); } else { result[key] = value; } } return result; } /** * Load and validate a Flynn config from YAML file(s). * If overlayPath is provided, the overlay YAML is deep-merged on top of * the base config BEFORE env var expansion and Zod validation. */ export function loadConfig(configPath: string, overlayPath?: string): Config { const rawContent = readFileSync(configPath, 'utf-8'); let rawConfig = parse(rawContent); if (overlayPath) { const overlayContent = readFileSync(overlayPath, 'utf-8'); const overlayConfig = parse(overlayContent); if (overlayConfig && typeof overlayConfig === 'object') { rawConfig = deepMerge( rawConfig as Record, overlayConfig as Record, ); } } const expandedConfig = expandEnvVarsInObject(rawConfig); return configSchema.parse(expandedConfig); }