Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
156 lines
5.0 KiB
TypeScript
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);
|
|
}
|