import type { Command } from 'commander'; import { withAuth } from './with-auth.js'; import { selectItem } from './select-dialog.js'; import { fetchMissions, fetchMission, createMission, updateMission, fetchMissionTasks, createMissionTask, updateMissionTask, fetchProjects, } from '../tui/gateway-api.js'; import type { MissionInfo, MissionTaskInfo } from '../tui/gateway-api.js'; function formatMission(m: MissionInfo): string { return `${m.name} — ${m.status}${m.phase ? ` (${m.phase})` : ''}`; } function showMissionDetail(m: MissionInfo) { console.log(` ID: ${m.id}`); console.log(` Name: ${m.name}`); console.log(` Status: ${m.status}`); console.log(` Phase: ${m.phase ?? '—'}`); console.log(` Project: ${m.projectId ?? '—'}`); console.log(` Description: ${m.description ?? '—'}`); console.log(` Created: ${new Date(m.createdAt).toLocaleString()}`); } function showTaskDetail(t: MissionTaskInfo) { console.log(` ID: ${t.id}`); console.log(` Status: ${t.status}`); console.log(` Description: ${t.description ?? '—'}`); console.log(` Notes: ${t.notes ?? '—'}`); console.log(` PR: ${t.pr ?? '—'}`); console.log(` Created: ${new Date(t.createdAt).toLocaleString()}`); } export function registerMissionCommand(program: Command) { const cmd = program .command('mission') .description('Manage missions') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .option('--list', 'List all missions') .option('--init', 'Create a new mission') .option('--plan ', 'Run PRD wizard for a mission') .option('--update ', 'Update a mission') .option('--project ', 'Scope to project') .argument('[id]', 'Show mission detail by ID') .configureHelp({ sortSubcommands: true }) .action( async ( id: string | undefined, opts: { gateway: string; list?: boolean; init?: boolean; plan?: string; update?: string; project?: string; }, ) => { const auth = await withAuth(opts.gateway); if (opts.list) { return listMissions(auth.gateway, auth.cookie); } if (opts.init) { return initMission(auth.gateway, auth.cookie); } if (opts.plan) { return planMission(auth.gateway, auth.cookie, opts.plan, opts.project); } if (opts.update) { return updateMissionWizard(auth.gateway, auth.cookie, opts.update); } if (id) { return showMission(auth.gateway, auth.cookie, id); } // Default: interactive select return interactiveSelect(auth.gateway, auth.cookie); }, ); // Task subcommand cmd .command('task') .description('Manage mission tasks') .option('-g, --gateway ', 'Gateway URL', 'http://localhost:14242') .option('--list', 'List tasks for a mission') .option('--new', 'Create a task') .option('--update ', 'Update a task') .option('--mission ', 'Mission ID or name') .argument('[taskId]', 'Show task detail') .action( async ( taskId: string | undefined, taskOpts: { gateway: string; list?: boolean; new?: boolean; update?: string; mission?: string; }, ) => { const auth = await withAuth(taskOpts.gateway); const missionId = await resolveMissionId(auth.gateway, auth.cookie, taskOpts.mission); if (!missionId) return; if (taskOpts.list) { return listTasks(auth.gateway, auth.cookie, missionId); } if (taskOpts.new) { return createTaskWizard(auth.gateway, auth.cookie, missionId); } if (taskOpts.update) { return updateTaskWizard(auth.gateway, auth.cookie, missionId, taskOpts.update); } if (taskId) { return showTask(auth.gateway, auth.cookie, missionId, taskId); } return listTasks(auth.gateway, auth.cookie, missionId); }, ); return cmd; } async function resolveMissionByName( gateway: string, cookie: string, idOrName: string, ): Promise { const missions = await fetchMissions(gateway, cookie); return missions.find((m) => m.id === idOrName || m.name === idOrName); } async function resolveMissionId( gateway: string, cookie: string, idOrName?: string, ): Promise { if (idOrName) { const mission = await resolveMissionByName(gateway, cookie, idOrName); if (!mission) { console.error(`Mission "${idOrName}" not found.`); return undefined; } return mission.id; } // Interactive select const missions = await fetchMissions(gateway, cookie); const selected = await selectItem(missions, { message: 'Select a mission:', render: formatMission, emptyMessage: 'No missions found. Create one with `mosaic mission --init`.', }); return selected?.id; } async function listMissions(gateway: string, cookie: string) { const missions = await fetchMissions(gateway, cookie); if (missions.length === 0) { console.log('No missions found.'); return; } console.log(`Missions (${missions.length}):\n`); for (const m of missions) { const phase = m.phase ? ` [${m.phase}]` : ''; console.log(` ${m.name} ${m.status}${phase} ${m.id.slice(0, 8)}`); } } async function showMission(gateway: string, cookie: string, id: string) { try { const mission = await fetchMission(gateway, cookie, id); showMissionDetail(mission); } catch { // Try resolving by name const m = await resolveMissionByName(gateway, cookie, id); if (!m) { console.error(`Mission "${id}" not found.`); process.exit(1); } showMissionDetail(m); } } async function interactiveSelect(gateway: string, cookie: string) { const missions = await fetchMissions(gateway, cookie); const selected = await selectItem(missions, { message: 'Select a mission:', render: formatMission, emptyMessage: 'No missions found. Create one with `mosaic mission --init`.', }); if (selected) { showMissionDetail(selected); } } async function initMission(gateway: string, cookie: string) { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); try { const name = await ask('Mission name: '); if (!name.trim()) { console.error('Name is required.'); return; } // Project selection const projects = await fetchProjects(gateway, cookie); let projectId: string | undefined; if (projects.length > 0) { const selected = await selectItem(projects, { message: 'Assign to project (required):', render: (p) => `${p.name} (${p.status})`, emptyMessage: 'No projects found.', }); if (selected) projectId = selected.id; } const description = await ask('Description (optional): '); const mission = await createMission(gateway, cookie, { name: name.trim(), projectId, description: description.trim() || undefined, status: 'planning', }); console.log(`\nMission "${mission.name}" created (${mission.id}).`); } finally { rl.close(); } } async function planMission( gateway: string, cookie: string, idOrName: string, _projectIdOrName?: string, ) { const mission = await resolveMissionByName(gateway, cookie, idOrName); if (!mission) { console.error(`Mission "${idOrName}" not found.`); process.exit(1); } console.log(`Planning mission: ${mission.name}\n`); try { const { runPrdWizard } = await import('@mosaicstack/prdy'); await runPrdWizard({ name: mission.name, projectPath: process.cwd(), interactive: true, }); } catch (err) { console.error(`PRD wizard failed: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } } async function updateMissionWizard(gateway: string, cookie: string, idOrName: string) { const mission = await resolveMissionByName(gateway, cookie, idOrName); if (!mission) { console.error(`Mission "${idOrName}" not found.`); process.exit(1); } const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); try { console.log(`Updating mission: ${mission.name}\n`); const name = await ask(`Name [${mission.name}]: `); const description = await ask(`Description [${mission.description ?? 'none'}]: `); const status = await ask(`Status [${mission.status}]: `); const updates: Record = {}; if (name.trim()) updates['name'] = name.trim(); if (description.trim()) updates['description'] = description.trim(); if (status.trim()) updates['status'] = status.trim(); if (Object.keys(updates).length === 0) { console.log('No changes.'); return; } const updated = await updateMission(gateway, cookie, mission.id, updates); console.log(`\nMission "${updated.name}" updated.`); } finally { rl.close(); } } // ── Task operations ── async function listTasks(gateway: string, cookie: string, missionId: string) { const tasks = await fetchMissionTasks(gateway, cookie, missionId); if (tasks.length === 0) { console.log('No tasks found.'); return; } console.log(`Tasks (${tasks.length}):\n`); for (const t of tasks) { const desc = t.description ? ` — ${t.description.slice(0, 60)}` : ''; console.log(` ${t.id.slice(0, 8)} ${t.status}${desc}`); } } async function showTask(gateway: string, cookie: string, missionId: string, taskId: string) { const tasks = await fetchMissionTasks(gateway, cookie, missionId); const task = tasks.find((t) => t.id === taskId); if (!task) { console.error(`Task "${taskId}" not found.`); process.exit(1); } showTaskDetail(task); } async function createTaskWizard(gateway: string, cookie: string, missionId: string) { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); try { const description = await ask('Task description: '); if (!description.trim()) { console.error('Description is required.'); return; } const status = await ask('Status [not-started]: '); const task = await createMissionTask(gateway, cookie, missionId, { description: description.trim(), status: status.trim() || 'not-started', }); console.log(`\nTask created (${task.id}).`); } finally { rl.close(); } } async function updateTaskWizard( gateway: string, cookie: string, missionId: string, taskId: string, ) { const readline = await import('node:readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const ask = (q: string): Promise => new Promise((resolve) => rl.question(q, resolve)); try { const status = await ask('New status: '); const notes = await ask('Notes (optional): '); const pr = await ask('PR (optional): '); const updates: Record = {}; if (status.trim()) updates['status'] = status.trim(); if (notes.trim()) updates['notes'] = notes.trim(); if (pr.trim()) updates['pr'] = pr.trim(); if (Object.keys(updates).length === 0) { console.log('No changes.'); return; } const updated = await updateMissionTask(gateway, cookie, missionId, taskId, updates); console.log(`\nTask ${updated.id.slice(0, 8)} updated (${updated.status}).`); } finally { rl.close(); } }