feat(wave3): @mosaic/coord TypeScript orchestrator (#6)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #6.
This commit is contained in:
155
packages/coord/src/cli.ts
Normal file
155
packages/coord/src/cli.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user