/** * Config loading — merges JSON config file with CLI args. * CLI args always win. Required paths are validated here. */ import { readFile } from 'fs/promises'; import { join } from 'path'; const REQUIRED_PATHS = ['source', 'prev', 'pend', 'deltas']; const DEFAULTS = { backend: 'zstd', }; export async function loadConfig(args) { let fileConfig = {}; if (args.config) { try { const raw = await readFile(args.config, 'utf8'); fileConfig = JSON.parse(raw); } catch (err) { console.error(`Error reading config file ${args.config}: ${err.message}`); process.exit(1); } } // CLI args override file config, file config overrides defaults const config = { ...DEFAULTS, ...fileConfig, ...filterDefined(args) }; // Expand --base into --prev/--pend/--deltas, explicit flags take priority if (config.base) { config.prev ??= join(config.base, 'previous'); config.pend ??= join(config.base, 'pending'); config.deltas ??= join(config.base, 'deltas'); } // Guard: refuse to run if any required path is missing if (config.command === 'run') { const missing = REQUIRED_PATHS.filter(k => !config[k]); if (missing.length > 0) { console.error(`Error: missing required options: ${missing.map(k => `--${k}`).join(', ')}`); console.error('Provide them as CLI flags or in a --config JSON file.'); process.exit(1); } } return config; } function filterDefined(obj) { return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)); }