import { existsSync, readFileSync } from 'node:fs'; import type { Command } from 'commander'; import { runGates } from './gate-runner.js'; import { MACPCapabilityError, type MacpErrorCode } from './errors.js'; /** * Load gates from a spec: an existing file (JSON gates array, a JSON object * with `quality_gates`, a JSON gate object, or one command per line) or an * inline command string. Fails closed with a typed capability error when the * spec contains no executable gate definition. */ function loadGateSpec(spec: string): unknown[] { if (existsSync(spec)) { const raw = readFileSync(spec, 'utf-8'); try { const parsed = JSON.parse(raw) as unknown; if (Array.isArray(parsed)) { if (parsed.length === 0) { throw new MACPCapabilityError( 'MACP_NO_COMMAND', 'gate-spec', `gate spec file '${spec}' contains an empty gates array`, ); } return parsed; } if (typeof parsed === 'object' && parsed !== null) { const obj = parsed as Record; if (Array.isArray(obj['quality_gates'])) { return obj['quality_gates']; } return [parsed]; } throw new MACPCapabilityError( 'MACP_NO_COMMAND', 'gate-spec', `gate spec file '${spec}' parsed to ${typeof parsed} — expected a gates array, a task with quality_gates, or a gate object`, ); } catch (exc) { if (exc instanceof MACPCapabilityError) throw exc; // Not JSON — treat each non-empty line as a command gate. const lines = raw .split('\n') .map((l) => l.trim()) .filter((l) => l.length > 0); if (lines.length > 0) return lines; throw new MACPCapabilityError( 'MACP_NO_COMMAND', 'gate-spec', `gate spec file '${spec}' contains no gates`, ); } } if (spec.trim().length > 0) return [spec]; throw new MACPCapabilityError('MACP_NO_COMMAND', 'gate-spec', 'gate spec is empty'); } /** Print a typed not-implemented failure and exit nonzero (RI-N2 fail-closed). */ function notImplemented(subcommand: string, capability: string, hint: string): void { const err = new MACPCapabilityError( 'MACP_NOT_IMPLEMENTED', capability, `${subcommand} is not implemented in @mosaicstack/macp yet (${capability} capability absent) — ${hint}`, ); console.error(`[macp] ${subcommand}: ${err.message} [${err.code}]`); process.exitCode = 1; } /** * Register macp subcommands on an existing Commander program. * This avoids cross-package Commander version mismatches by using the * caller's Command instance directly. */ export function registerMacpCommand(parent: Command): void { const macp = parent.command('macp').description('MACP task and gate management'); // ─── tasks ─────────────────────────────────────────────────────────────── const tasks = macp.command('tasks').description('Manage MACP tasks'); tasks .command('list') .description('List MACP tasks') .option( '--status ', 'Filter by task status (pending|running|gated|completed|failed|escalated)', ) .option( '--type ', 'Filter by task type (coding|deploy|research|review|documentation|infrastructure)', ) .action((opts: { status?: string; type?: string }) => { // unimplemented capability — a failure, never a success (RI-N2) if (opts.status) { console.log(` status filter: ${opts.status}`); } if (opts.type) { console.log(` type filter: ${opts.type}`); } notImplemented('tasks list', 'task-persistence', 'use the macp package programmatically'); }); // ─── submit ────────────────────────────────────────────────────────────── macp .command('submit ') .description('Submit a task from a JSON/YAML spec file') .action((specPath: string) => { // unimplemented capability — a failure, never a success (RI-N2) console.log(` spec path: ${specPath}`); console.log(' task id: (unavailable — no MACP server connected)'); console.log(' status: (unavailable — no MACP server connected)'); notImplemented('submit', 'macp-server', 'use the macp package programmatically'); }); // ─── gate ──────────────────────────────────────────────────────────────── macp .command('gate ') .description('Run a gate from a spec string or file path (wraps runGate/runGates)') .option('--fail-on ', 'Gate fail-on mode: ai|fail|both|none', 'fail') .option('--cwd ', 'Working directory for gate execution', process.cwd()) .option('--log ', 'Path to write gate log output', '/tmp/macp-gate.log') .option('--timeout ', 'Gate timeout in seconds', '60') .option( '--simulate', 'Simulate gates instead of executing them; results are typed simulated and never satisfy a check', ) .action( ( spec: string, opts: { failOn: string; cwd: string; log: string; timeout: string; simulate?: boolean }, ) => { let gates: unknown[]; try { gates = loadGateSpec(spec); } catch (exc) { if (exc instanceof MACPCapabilityError) { console.error(`[macp] gate: ${exc.message} [${exc.code}]`); } else { console.error(`[macp] gate: ${String(exc)}`); } process.exitCode = 1; return; } const timeoutSec = Number.parseInt(opts.timeout, 10) || 60; const eventsPath = `${opts.log}.events.ndjson`; const { state, gateResults } = runGates( gates, opts.cwd, opts.log, timeoutSec, eventsPath, 'macp-cli-gate', { simulate: opts.simulate, }, ); for (const r of gateResults) { const label = r.command || r.type; const reason = r.reason ? ` — ${r.reason}` : ''; console.log(`[macp] gate ${r.status}: ${label}${reason}`); } if (opts.simulate) { console.log( '[macp] SIMULATED run — every result is typed simulated and can never satisfy a gate, dependency, or release check', ); } // Simulated runs may complete (exit 0) only because the caller // explicitly passed --simulate; the typed state stays 'simulated'. process.exitCode = state === 'passed' || state === 'simulated' ? 0 : 1; }, ); // ─── events ────────────────────────────────────────────────────────────── const events = macp.command('events').description('Stream MACP events'); events .command('tail') .description('Tail MACP events from the event log (wraps event emitter)') .option('--file ', 'Path to the MACP events NDJSON file') .option('--follow', 'Follow the file for new events (like tail -f)') .action((opts: { file?: string; follow?: boolean }) => { // unimplemented capability — a failure, never a success (RI-N2) if (opts.file) { console.log(` file: ${opts.file}`); } if (opts.follow) { console.log(' mode: follow'); } notImplemented('events tail', 'event-source', 'use the macp package programmatically'); }); } // Re-export so CLI consumers can surface typed capability codes. export type { MacpErrorCode };