import { Command } from 'commander'; import { createMission, loadMission } from './mission.js'; import { runTask, resumeTask } from './runner.js'; import { getMissionStatus } from './status.js'; import type { MissionStatusSummary } from './types.js'; interface InitCommandOptions { readonly name: string; readonly project?: string; readonly prefix?: string; readonly milestones?: string; readonly qualityGates?: string; readonly version?: string; readonly description?: string; readonly force?: boolean; } interface RunCommandOptions { readonly project: string; readonly task: string; readonly runtime?: 'claude' | 'codex'; readonly print?: boolean; } interface StatusCommandOptions { readonly project: string; readonly format?: 'json' | 'table'; } function parseMilestones(value: string | undefined): string[] { if (value === undefined || value.trim().length === 0) { return []; } return value .split(',') .map((entry) => entry.trim()) .filter((entry) => entry.length > 0); } function renderStatusTable(status: MissionStatusSummary): string { const lines = [ `Mission: ${status.mission.name} (${status.mission.id})`, `Status: ${status.mission.status}`, `Project: ${status.mission.projectPath}`, `Milestones: ${status.milestones.completed}/${status.milestones.total} completed`, `Tasks: total=${status.tasks.total}, done=${status.tasks.done}, in-progress=${status.tasks.inProgress}, pending=${status.tasks.pending}, blocked=${status.tasks.blocked}, cancelled=${status.tasks.cancelled}`, `Next task: ${status.nextTaskId ?? '—'}`, `Active session: ${status.activeSession?.sessionId ?? 'none'}`, ]; return lines.join('\n'); } export function buildCoordCli(): Command { const program = new Command(); program .name('mosaic') .description('Mosaic CLI') .exitOverride(); const coord = program.command('coord').description('Mission coordination commands'); coord .command('init') .description('Initialize orchestrator mission state') .requiredOption('--name ', 'Mission name') .option('--project ', 'Project path') .option('--prefix ', 'Task prefix') .option('--milestones ', 'Milestone names') .option('--quality-gates ', 'Quality gate command') .option('--version ', 'Milestone version') .option('--description ', 'Mission description') .option('--force', 'Overwrite active mission') .action(async (options: InitCommandOptions) => { const mission = await createMission({ name: options.name, projectPath: options.project, prefix: options.prefix, milestones: parseMilestones(options.milestones), qualityGates: options.qualityGates, version: options.version, description: options.description, force: options.force, }); console.log( JSON.stringify( { ok: true, missionId: mission.id, projectPath: mission.projectPath, }, null, 2, ), ); }); coord .command('run') .description('Run a mission task') .requiredOption('--project ', 'Project path') .requiredOption('--task ', 'Task id') .option('--runtime ', 'Runtime (claude|codex)') .option('--print', 'Print launch command only') .action(async (options: RunCommandOptions) => { const mission = await loadMission(options.project); const run = await runTask(mission, options.task, { runtime: options.runtime, mode: options.print === true ? 'print-only' : 'interactive', }); console.log(JSON.stringify(run, null, 2)); }); coord .command('resume') .description('Resume a mission task after stale/dead session lock') .requiredOption('--project ', 'Project path') .requiredOption('--task ', 'Task id') .option('--runtime ', 'Runtime (claude|codex)') .option('--print', 'Print launch command only') .action(async (options: RunCommandOptions) => { const mission = await loadMission(options.project); const run = await resumeTask(mission, options.task, { runtime: options.runtime, mode: options.print === true ? 'print-only' : 'interactive', }); console.log(JSON.stringify(run, null, 2)); }); coord .command('status') .description('Show mission status') .requiredOption('--project ', 'Project path') .option('--format ', 'Output format (table|json)', 'table') .action(async (options: StatusCommandOptions) => { const mission = await loadMission(options.project); const status = await getMissionStatus(mission); if (options.format === 'json') { console.log(JSON.stringify(status, null, 2)); } else { console.log(renderStatusTable(status)); } }); return program; } export async function runCoordCli(argv: readonly string[] = process.argv): Promise { const program = buildCoordCli(); await program.parseAsync(argv); }