Files
mosaic/packages/coord/src/cli.ts
2026-03-07 01:32:10 +00:00

156 lines
5.0 KiB
TypeScript

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 <name>', 'Mission name')
.option('--project <path>', 'Project path')
.option('--prefix <prefix>', 'Task prefix')
.option('--milestones <comma-separated>', 'Milestone names')
.option('--quality-gates <command>', 'Quality gate command')
.option('--version <semver>', 'Milestone version')
.option('--description <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 <path>', 'Project path')
.requiredOption('--task <id>', 'Task id')
.option('--runtime <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 <path>', 'Project path')
.requiredOption('--task <id>', 'Task id')
.option('--runtime <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 <path>', 'Project path')
.option('--format <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<void> {
const program = buildCoordCli();
await program.parseAsync(argv);
}