/** * CLI flag parsing and config precedence for the soak runner. * * Precedence (later wins): * runner defaults → scenario.defaultConfig → env vars → CLI flags */ export type WatchMode = 'none' | 'dashboard' | 'tiled'; export interface CliArgs { scenario?: string; accounts?: number; rooms?: number; cpusPerRoom?: number; gamesPerRoom?: number; holes?: number; watch?: WatchMode; dashboardPort?: number; target?: string; runId?: string; dryRun?: boolean; listOnly?: boolean; } const VALID_WATCH: WatchMode[] = ['none', 'dashboard', 'tiled']; function parseInt10(s: string, name: string): number { const n = parseInt(s, 10); if (Number.isNaN(n)) throw new Error(`Invalid integer for ${name}: ${s}`); return n; } export function parseArgs(argv: string[]): CliArgs { const out: CliArgs = {}; for (const arg of argv) { if (arg === '--list') { out.listOnly = true; continue; } if (arg === '--dry-run') { out.dryRun = true; continue; } const m = arg.match(/^--([a-z][a-z0-9-]*)=(.*)$/); if (!m) continue; const [, key, value] = m; switch (key) { case 'scenario': out.scenario = value; break; case 'accounts': out.accounts = parseInt10(value, '--accounts'); break; case 'rooms': out.rooms = parseInt10(value, '--rooms'); break; case 'cpus-per-room': out.cpusPerRoom = parseInt10(value, '--cpus-per-room'); break; case 'games-per-room': out.gamesPerRoom = parseInt10(value, '--games-per-room'); break; case 'holes': out.holes = parseInt10(value, '--holes'); break; case 'watch': if (!VALID_WATCH.includes(value as WatchMode)) { throw new Error( `Invalid --watch value: ${value} (expected ${VALID_WATCH.join('|')})`, ); } out.watch = value as WatchMode; break; case 'dashboard-port': out.dashboardPort = parseInt10(value, '--dashboard-port'); break; case 'target': out.target = value; break; case 'run-id': out.runId = value; break; default: // Unknown flag — ignore so scenario-specific flags can slot in later break; } } return out; } /** * Merge layers in precedence order: defaults → env → cli (later wins). */ export function mergeConfig( cli: Record, env: Record, defaults: Record, ): Record { const merged: Record = { ...defaults }; // Env overlay — SOAK_UPPER_SNAKE → lowerCamel in cli space. const envMap: Record = { SOAK_HOLES: 'holes', SOAK_ROOMS: 'rooms', SOAK_ACCOUNTS: 'accounts', SOAK_CPUS_PER_ROOM: 'cpusPerRoom', SOAK_GAMES_PER_ROOM: 'gamesPerRoom', SOAK_WATCH: 'watch', SOAK_DASHBOARD_PORT: 'dashboardPort', }; const numericKeys = /^(holes|rooms|accounts|cpusPerRoom|gamesPerRoom|dashboardPort)$/; for (const [envKey, cfgKey] of Object.entries(envMap)) { const v = env[envKey]; if (v !== undefined) { merged[cfgKey] = numericKeys.test(cfgKey) ? parseInt(v, 10) : v; } } // CLI overlay — wins over env and defaults. for (const [k, v] of Object.entries(cli)) { if (v !== undefined) merged[k] = v; } return merged; }