/** * Codewalkers CLI * * Commander-based CLI with help system and version display. * Supports server mode via --server flag. */ import { Command } from 'commander'; import { VERSION } from '../index.js'; import { CoordinationServer } from '../server/index.js'; import { GracefulShutdown } from '../server/shutdown.js'; import { createDefaultTrpcClient } from './trpc-client.js'; import { createContainer } from '../container.js'; import { findWorkspaceRoot, writeCwrc, defaultCwConfig } from '../config/index.js'; import { createModuleLogger } from '../logger/index.js'; import { backfillMetricsFromPath } from '../scripts/backfill-metrics.js'; import { getDbPath } from '../db/index.js'; /** Environment variable for custom port */ const CW_PORT_ENV = 'CW_PORT'; /** * Starts the coordination server in foreground mode. * Server runs until terminated via SIGTERM/SIGINT. */ async function startServer(port?: number, debug?: boolean): Promise { // Get port from option, env var, or default const serverPort = port ?? (process.env[CW_PORT_ENV] ? parseInt(process.env[CW_PORT_ENV], 10) : undefined); const log = createModuleLogger('server'); // Create full dependency graph const container = await createContainer({ debug }); // Create and start server const server = new CoordinationServer( { port: serverPort }, container.processManager, container.logManager, container.eventBus, container.toContextDeps(), ); try { await server.start(); } catch (error) { log.fatal({ err: error }, 'failed to start server'); console.error('Failed to start server:', (error as Error).message); process.exit(1); } // Install graceful shutdown handlers const shutdown = new GracefulShutdown(server, container.processManager, container.logManager, container.previewManager); shutdown.install(); } /** * Creates and configures the CLI program. * * @param serverHandler - Optional handler to be called for server mode * @returns Configured Commander program ready for parsing */ export function createCli(serverHandler?: (port?: number) => Promise): Command { const program = new Command(); program .name('cw') .description('Multi-agent workspace for orchestrating multiple Claude Code agents') .version(VERSION, '-v, --version', 'Display version number'); // Server mode options (global flags) program .option('-s, --server', 'Start the coordination server') .option('-p, --port ', 'Port for the server (default: 3847, env: CW_PORT)', parseInt) .option('-d, --debug', 'Enable debug mode (archive agent workdirs before cleanup)'); // Handle the case where --server is provided without a command // This makes --server work as a standalone action program.hook('preAction', async (_thisCommand, _actionCommand) => { const opts = program.opts(); if (opts.server && serverHandler) { await serverHandler(opts.port); process.exit(0); } }); // Status command - shows workspace status via tRPC program .command('status') .description('Show workspace status') .action(async () => { try { const client = createDefaultTrpcClient(); const health = await client.health.query(); console.log('Coordination Server Status'); console.log('=========================='); console.log(`Status: ${health.status}`); console.log(`Uptime: ${health.uptime}s`); console.log(`Processes: ${health.processCount}`); } catch { console.log('Server not running or unreachable. Start with: cw --server'); } }); // Init command - create .cwrc in current directory program .command('init') .description('Initialize a cw workspace in the current directory') .action(() => { const cwd = process.cwd(); const existing = findWorkspaceRoot(cwd); if (existing && existing === cwd) { console.log(`Workspace already initialized at ${cwd}`); return; } if (existing) { console.log(`Parent workspace found at ${existing}`); console.log(`Creating nested workspace at ${cwd}`); } writeCwrc(cwd, defaultCwConfig()); console.log(`Initialized cw workspace at ${cwd}`); }); // ID generation command (stateless — no server, no tRPC) program .command('id') .description('Generate a unique nanoid (works offline, no server needed)') .option('-n, --count ', 'Number of IDs to generate', '1') .action(async (options: { count: string }) => { const { nanoid } = await import('nanoid'); const count = parseInt(options.count, 10) || 1; for (let i = 0; i < count; i++) { console.log(nanoid()); } }); // Backfill metrics command (standalone — no server, no tRPC) program .command('backfill-metrics') .description('Populate agent_metrics table from existing agent_log_chunks (run once after upgrading)') .option('--db ', 'Path to the SQLite database file (defaults to configured DB path)') .action(async (options: { db?: string }) => { const dbPath = options.db ?? getDbPath(); console.log(`Backfilling metrics from ${dbPath}...`); try { await backfillMetricsFromPath(dbPath); } catch (error) { console.error('Backfill failed:', (error as Error).message); process.exit(1); } }); // Agent command group const agentCommand = program .command('agent') .description('Manage agents'); // cw agent spawn --name --task agentCommand .command('spawn ') .description('Spawn a new agent to work on a task') .option('--name ', 'Human-readable name for the agent (auto-generated if omitted)') .requiredOption('--task ', 'Task ID to assign to agent') .option('--cwd ', 'Working directory for agent') .option('--provider ', 'Agent provider (claude, codex, gemini, cursor, auggie, amp, opencode)') .option('--initiative ', 'Initiative ID (creates worktrees for all linked projects)') .action(async (prompt: string, options: { name?: string; task: string; cwd?: string; provider?: string; initiative?: string }) => { try { const client = createDefaultTrpcClient(); const agent = await client.spawnAgent.mutate({ name: options.name, taskId: options.task, prompt, cwd: options.cwd, provider: options.provider, initiativeId: options.initiative, }); console.log(`Agent '${agent.name}' spawned`); console.log(` ID: ${agent.id}`); console.log(` Task: ${agent.taskId}`); console.log(` Status: ${agent.status}`); console.log(` Worktree: ${agent.worktreeId}`); } catch (error) { console.error('Failed to spawn agent:', (error as Error).message); process.exit(1); } }); // cw agent stop agentCommand .command('stop ') .description('Stop a running agent by name') .action(async (name: string) => { try { const client = createDefaultTrpcClient(); const result = await client.stopAgent.mutate({ name }); console.log(`Agent '${result.name}' stopped`); } catch (error) { console.error('Failed to stop agent:', (error as Error).message); process.exit(1); } }); // cw agent delete agentCommand .command('delete ') .description('Delete an agent and clean up its workdir, branches, and logs') .action(async (name: string) => { try { const client = createDefaultTrpcClient(); const result = await client.deleteAgent.mutate({ name }); console.log(`Agent '${result.name}' deleted`); } catch (error) { console.error('Failed to delete agent:', (error as Error).message); process.exit(1); } }); // cw agent list agentCommand .command('list') .description('List all agents') .action(async () => { try { const client = createDefaultTrpcClient(); const agents = await client.listAgents.query(); if (agents.length === 0) { console.log('No agents found'); return; } console.log('Agents:'); for (const agent of agents) { const status = agent.status === 'waiting_for_input' ? 'WAITING' : agent.status.toUpperCase(); console.log(` ${agent.name} [${status}] - ${agent.taskId}`); } } catch (error) { console.error('Failed to list agents:', (error as Error).message); process.exit(1); } }); // cw agent get agentCommand .command('get ') .description('Get agent details by name') .action(async (name: string) => { try { const client = createDefaultTrpcClient(); const agent = await client.getAgent.query({ name }); if (!agent) { console.log(`Agent '${name}' not found`); return; } console.log(`Agent: ${agent.name}`); console.log(` ID: ${agent.id}`); console.log(` Task: ${agent.taskId}`); console.log(` Session: ${agent.sessionId ?? '(none)'}`); console.log(` Worktree: ${agent.worktreeId}`); console.log(` Status: ${agent.status}`); console.log(` Created: ${agent.createdAt}`); console.log(` Updated: ${agent.updatedAt}`); } catch (error) { console.error('Failed to get agent:', (error as Error).message); process.exit(1); } }); // cw agent resume // Accepts either JSON object {"q1": "answer1", "q2": "answer2"} or single answer agentCommand .command('resume ') .description('Resume an agent with answers (JSON object or single answer string)') .action(async (name: string, answersInput: string) => { try { const client = createDefaultTrpcClient(); // Try parsing as JSON first, fallback to single answer format let answers: Record; try { const parsed = JSON.parse(answersInput); if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { answers = parsed; } else { // Not a valid object, treat as single answer answers = { q1: answersInput }; } } catch { // Not valid JSON, treat as single answer with default question ID answers = { q1: answersInput }; } const result = await client.resumeAgent.mutate({ name, answers }); console.log(`Agent '${result.name}' resumed`); } catch (error) { console.error('Failed to resume agent:', (error as Error).message); process.exit(1); } }); // cw agent result agentCommand .command('result ') .description('Get agent execution result') .action(async (name: string) => { try { const client = createDefaultTrpcClient(); const result = await client.getAgentResult.query({ name }); if (!result) { console.log('No result available (agent may still be running)'); return; } console.log(`Result: ${result.success ? 'SUCCESS' : 'FAILED'}`); console.log(` Message: ${result.message}`); if (result.filesModified?.length) { console.log(` Files modified: ${result.filesModified.join(', ')}`); } } catch (error) { console.error('Failed to get result:', (error as Error).message); process.exit(1); } }); // Task command group const taskCommand = program .command('task') .description('Manage tasks'); // cw task list --parent | --phase | --initiative taskCommand .command('list') .description('List tasks (by parent task, phase, or initiative)') .option('--parent ', 'Parent task ID (for child tasks)') .option('--phase ', 'Phase ID') .option('--initiative ', 'Initiative ID') .action(async (options: { parent?: string; phase?: string; initiative?: string }) => { try { const client = createDefaultTrpcClient(); let tasks; if (options.parent) { tasks = await client.listTasks.query({ parentTaskId: options.parent }); } else if (options.phase) { tasks = await client.listPhaseTasks.query({ phaseId: options.phase }); } else if (options.initiative) { tasks = await client.listInitiativeTasks.query({ initiativeId: options.initiative }); } else { console.error('One of --parent, --phase, or --initiative is required'); process.exit(1); } if (tasks.length === 0) { console.log('No tasks found'); return; } // Count by status const pending = tasks.filter(t => t.status === 'pending').length; const inProgress = tasks.filter(t => t.status === 'in_progress').length; const completed = tasks.filter(t => t.status === 'completed').length; const blocked = tasks.filter(t => t.status === 'blocked').length; console.log('Tasks:'); console.log(''); for (const task of tasks) { const statusLabel = task.status === 'in_progress' ? 'IN_PROGRESS' : task.status.toUpperCase(); const priorityLabel = task.priority === 'high' ? '[HIGH]' : task.priority === 'low' ? '[low]' : ''; console.log(` ${task.order}. ${task.name} [${statusLabel}] ${task.type} ${priorityLabel}`); } console.log(''); console.log(`Summary: ${pending} pending, ${inProgress} in progress, ${completed} completed, ${blocked} blocked`); } catch (error) { console.error('Failed to list tasks:', (error as Error).message); process.exit(1); } }); // cw task get taskCommand .command('get ') .description('Get task details by ID') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); const task = await client.getTask.query({ id: taskId }); console.log(`Task: ${task.name}`); console.log(` ID: ${task.id}`); console.log(` Phase: ${task.phaseId ?? '(none)'}`); console.log(` Initiative: ${task.initiativeId ?? '(none)'}`); console.log(` Parent Task: ${task.parentTaskId ?? '(none)'}`); console.log(` Description: ${task.description ?? '(none)'}`); console.log(` Category: ${task.category}`); console.log(` Type: ${task.type}`); console.log(` Priority: ${task.priority}`); console.log(` Status: ${task.status}`); console.log(` Order: ${task.order}`); console.log(` Created: ${task.createdAt}`); console.log(` Updated: ${task.updatedAt}`); } catch (error) { console.error('Failed to get task:', (error as Error).message); process.exit(1); } }); // cw task status taskCommand .command('status ') .description('Update task status (pending, in_progress, completed, blocked)') .action(async (taskId: string, status: string) => { const validStatuses = ['pending', 'in_progress', 'completed', 'blocked']; if (!validStatuses.includes(status)) { console.error(`Invalid status: ${status}`); console.error(`Valid statuses: ${validStatuses.join(', ')}`); process.exit(1); } try { const client = createDefaultTrpcClient(); const task = await client.updateTaskStatus.mutate({ id: taskId, status: status as 'pending' | 'in_progress' | 'completed' | 'blocked', }); console.log(`Task '${task.name}' status updated to: ${task.status}`); } catch (error) { console.error('Failed to update task status:', (error as Error).message); process.exit(1); } }); // cw task add --agent-id taskCommand .command('add ') .description('Create a sibling task in the agent\'s current phase') .requiredOption('--agent-id ', 'Agent ID creating the task') .option('--description ', 'Task description') .option('--category ', 'Task category (execute, research, verify, ...)') .action(async (name: string, options: { agentId: string; description?: string; category?: string }) => { try { const client = createDefaultTrpcClient(); const task = await client.createTaskForAgent.mutate({ agentId: options.agentId, name, description: options.description, category: options.category as 'execute' | undefined, }); console.log(`Created task: ${task.name}`); console.log(` ID: ${task.id}`); console.log(` Phase: ${task.phaseId}`); console.log(` Status: ${task.status}`); } catch (error) { console.error('Failed to create task:', (error as Error).message); process.exit(1); } }); // Message command group const messageCommand = program .command('message') .description('View agent messages and questions'); // cw message list [--agent ] [--status ] messageCommand .command('list') .description('List messages from agents') .option('--agent ', 'Filter by agent ID') .option('--status ', 'Filter by status (pending, read, responded)') .action(async (options: { agent?: string; status?: string }) => { // Validate status if provided if (options.status && !['pending', 'read', 'responded'].includes(options.status)) { console.error(`Invalid status: ${options.status}`); console.error('Valid statuses: pending, read, responded'); process.exit(1); } try { const client = createDefaultTrpcClient(); const messages = await client.listMessages.query({ agentId: options.agent, status: options.status as 'pending' | 'read' | 'responded' | undefined, }); if (messages.length === 0) { console.log('No messages found'); return; } // Count pending const pendingCount = messages.filter(m => m.status === 'pending').length; if (pendingCount > 0) { console.log(`(${pendingCount} pending)\n`); } console.log('Messages:'); console.log(''); for (const msg of messages) { const idShort = msg.id.slice(0, 8); const agentId = msg.senderId?.slice(0, 8) ?? 'user'; const content = msg.content.length > 50 ? msg.content.slice(0, 47) + '...' : msg.content; const statusLabel = msg.status.toUpperCase(); const createdAt = new Date(msg.createdAt).toLocaleString(); console.log(` ${idShort} ${agentId} ${msg.type} [${statusLabel}] ${createdAt}`); console.log(` ${content}`); } } catch (error) { console.error('Failed to list messages:', (error as Error).message); process.exit(1); } }); // cw message read messageCommand .command('read ') .description('Read full message content') .action(async (messageId: string) => { try { const client = createDefaultTrpcClient(); const message = await client.getMessage.query({ id: messageId }); console.log(`Message: ${message.id}`); console.log(` From: ${message.senderType} (${message.senderId ?? 'user'})`); console.log(` To: ${message.recipientType} (${message.recipientId ?? 'user'})`); console.log(` Type: ${message.type}`); console.log(` Status: ${message.status}`); console.log(` Created: ${new Date(message.createdAt).toLocaleString()}`); console.log(''); console.log('Content:'); console.log(message.content); if (message.status === 'pending' && message.requiresResponse) { console.log(''); console.log('---'); console.log('This message requires a response.'); console.log(`Use: cw message respond ${message.id} ""`); } } catch (error) { console.error('Failed to read message:', (error as Error).message); process.exit(1); } }); // cw message respond messageCommand .command('respond ') .description('Respond to a message') .action(async (messageId: string, response: string) => { try { const client = createDefaultTrpcClient(); const responseMessage = await client.respondToMessage.mutate({ id: messageId, response, }); console.log(`Response recorded (${responseMessage.id.slice(0, 8)})`); console.log(''); console.log('If the agent is waiting, you may want to resume it:'); console.log(' cw agent list (to see waiting agents)'); console.log(' cw agent resume ""'); } catch (error) { console.error('Failed to respond to message:', (error as Error).message); process.exit(1); } }); // Dispatch command group const dispatchCommand = program .command('dispatch') .description('Control task dispatch queue'); // cw dispatch queue dispatchCommand .command('queue ') .description('Queue a task for dispatch') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); await client.queueTask.mutate({ taskId }); console.log(`Task '${taskId}' queued for dispatch`); } catch (error) { console.error('Failed to queue task:', (error as Error).message); process.exit(1); } }); // cw dispatch next dispatchCommand .command('next') .description('Dispatch next available task to an agent') .action(async () => { try { const client = createDefaultTrpcClient(); const result = await client.dispatchNext.mutate(); if (result.success) { console.log('Task dispatched successfully'); console.log(` Task: ${result.taskId}`); console.log(` Agent: ${result.agentId}`); } else { console.log('Dispatch failed'); console.log(` Task: ${result.taskId || '(none)'}`); console.log(` Reason: ${result.reason}`); } } catch (error) { console.error('Failed to dispatch:', (error as Error).message); process.exit(1); } }); // cw dispatch status dispatchCommand .command('status') .description('Show dispatch queue status') .action(async () => { try { const client = createDefaultTrpcClient(); const state = await client.getQueueState.query(); console.log('Dispatch Queue Status'); console.log('====================='); console.log(`Queued: ${state.queued.length}`); console.log(`Ready: ${state.ready.length}`); console.log(`Blocked: ${state.blocked.length}`); if (state.ready.length > 0) { console.log(''); console.log('Ready tasks:'); for (const task of state.ready) { const priority = task.priority === 'high' ? '[HIGH]' : task.priority === 'low' ? '[low]' : ''; console.log(` ${task.taskId} ${priority}`); } } if (state.blocked.length > 0) { console.log(''); console.log('Blocked tasks:'); for (const bt of state.blocked) { console.log(` ${bt.taskId}: ${bt.reason}`); } } } catch (error) { console.error('Failed to get queue status:', (error as Error).message); process.exit(1); } }); // cw dispatch complete dispatchCommand .command('complete ') .description('Mark a task as complete') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); await client.completeTask.mutate({ taskId }); console.log(`Task '${taskId}' marked as complete`); } catch (error) { console.error('Failed to complete task:', (error as Error).message); process.exit(1); } }); // Merge command group const mergeCommand = program .command('merge') .description('Manage merge queue'); // cw merge queue mergeCommand .command('queue ') .description('Queue a completed task for merge') .action(async (taskId: string) => { try { const client = createDefaultTrpcClient(); await client.queueMerge.mutate({ taskId }); console.log(`Task '${taskId}' queued for merge`); } catch (error) { console.error('Failed to queue for merge:', (error as Error).message); process.exit(1); } }); // cw merge status mergeCommand .command('status') .description('Show merge queue status') .action(async () => { try { const client = createDefaultTrpcClient(); const state = await client.getMergeQueueStatus.query(); console.log('Merge Queue Status'); console.log('=================='); console.log(`Queued: ${state.queued.length}`); console.log(`In Progress: ${state.inProgress.length}`); console.log(`Merged: ${state.merged.length}`); console.log(`Conflicted: ${state.conflicted.length}`); if (state.conflicted.length > 0) { console.log(''); console.log('Conflicts:'); for (const c of state.conflicted) { console.log(` ${c.taskId}: ${c.conflicts.join(', ')}`); } } } catch (error) { console.error('Failed to get merge queue status:', (error as Error).message); process.exit(1); } }); // cw merge next mergeCommand .command('next') .description('Show next task ready to merge') .action(async () => { try { const client = createDefaultTrpcClient(); const next = await client.getNextMergeable.query(); if (next) { console.log(`Next mergeable: ${next.taskId} (priority: ${next.priority})`); } else { console.log('No tasks ready to merge'); } } catch (error) { console.error('Failed to get next mergeable:', (error as Error).message); process.exit(1); } }); // Coordinate command - process all ready merges program .command('coordinate') .description('Process all ready merges in dependency order') .option('-t, --target ', 'Target branch for merges', 'main') .action(async (options: { target: string }) => { try { const client = createDefaultTrpcClient(); console.log(`Processing merges to ${options.target}...`); const { results } = await client.processMerges.mutate({ targetBranch: options.target, }); const succeeded = results.filter(r => r.success).length; const conflicted = results.filter(r => !r.success).length; console.log(''); console.log('Results:'); console.log(` Merged: ${succeeded}`); console.log(` Conflicts: ${conflicted}`); if (conflicted > 0) { console.log(''); console.log('Conflicted tasks (bounce-back tasks created):'); for (const r of results.filter(r => !r.success)) { console.log(` ${r.taskId}: ${r.conflicts?.join(', ')}`); } } } catch (error) { console.error('Failed to process merges:', (error as Error).message); process.exit(1); } }); // Initiative command group const initiativeCommand = program .command('initiative') .description('Manage initiatives'); // cw initiative create initiativeCommand .command('create ') .description('Create a new initiative') .option('--project ', 'Project IDs to associate') .action(async (name: string, options: { project?: string[] }) => { try { const client = createDefaultTrpcClient(); const initiative = await client.createInitiative.mutate({ name, projectIds: options.project, }); console.log(`Created initiative: ${initiative.id}`); console.log(` Name: ${initiative.name}`); if (options.project && options.project.length > 0) { console.log(` Projects: ${options.project.length} associated`); } } catch (error) { console.error('Failed to create initiative:', (error as Error).message); process.exit(1); } }); // cw initiative list initiativeCommand .command('list') .description('List all initiatives') .option('-s, --status ', 'Filter by status (active, completed, archived)') .action(async (options: { status?: string }) => { // Validate status if provided const validStatuses = ['active', 'completed', 'archived']; if (options.status && !validStatuses.includes(options.status)) { console.error(`Invalid status: ${options.status}`); console.error(`Valid statuses: ${validStatuses.join(', ')}`); process.exit(1); } try { const client = createDefaultTrpcClient(); const initiatives = await client.listInitiatives.query( options.status ? { status: options.status as 'active' | 'completed' | 'archived' } : undefined ); if (initiatives.length === 0) { console.log('No initiatives found'); return; } for (const init of initiatives) { console.log(`${init.id} ${init.status.padEnd(10)} ${init.name}`); } } catch (error) { console.error('Failed to list initiatives:', (error as Error).message); process.exit(1); } }); // cw initiative get initiativeCommand .command('get ') .description('Get initiative details') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); const initiative = await client.getInitiative.query({ id }); console.log(`ID: ${initiative.id}`); console.log(`Name: ${initiative.name}`); console.log(`Status: ${initiative.status}`); console.log(`Created: ${new Date(initiative.createdAt).toISOString()}`); } catch (error) { console.error('Failed to get initiative:', (error as Error).message); process.exit(1); } }); // cw initiative phases initiativeCommand .command('phases ') .description('List phases for an initiative') .action(async (initiativeId: string) => { try { const client = createDefaultTrpcClient(); const phases = await client.listPhases.query({ initiativeId }); if (phases.length === 0) { console.log('No phases found'); return; } for (const phase of phases) { const idx = phases.indexOf(phase); console.log(`${(idx + 1).toString().padStart(2)}. ${phase.name} [${phase.status}]`); if (phase.content) { console.log(` ${phase.content}`); } } } catch (error) { console.error('Failed to list phases:', (error as Error).message); process.exit(1); } }); // Architect command group const architectCommand = program .command('architect') .description('Architect agent workflow'); // cw architect discuss architectCommand .command('discuss ') .description('Start discussion phase for an initiative') .option('--name ', 'Agent name (auto-generated if omitted)') .option('-c, --context ', 'Initial context') .action(async (initiativeId: string, options: { name?: string; context?: string }) => { try { const client = createDefaultTrpcClient(); const agent = await client.spawnArchitectDiscuss.mutate({ name: options.name, initiativeId, context: options.context, }); console.log(`Started architect agent in discuss mode`); console.log(` Agent: ${agent.name} (${agent.id})`); console.log(` Mode: ${agent.mode}`); console.log(` Initiative: ${initiativeId}`); } catch (error) { console.error('Failed to start discuss:', (error as Error).message); process.exit(1); } }); // cw architect plan architectCommand .command('plan ') .description('Plan phases for an initiative') .option('--name ', 'Agent name (auto-generated if omitted)') .option('-s, --summary ', 'Context summary from discuss phase') .action(async (initiativeId: string, options: { name?: string; summary?: string }) => { try { const client = createDefaultTrpcClient(); const agent = await client.spawnArchitectPlan.mutate({ name: options.name, initiativeId, contextSummary: options.summary, }); console.log(`Started architect agent in plan mode`); console.log(` Agent: ${agent.name} (${agent.id})`); console.log(` Mode: ${agent.mode}`); console.log(` Initiative: ${initiativeId}`); } catch (error) { console.error('Failed to start plan:', (error as Error).message); process.exit(1); } }); // cw architect detail architectCommand .command('detail ') .description('Detail a phase into tasks') .option('--name ', 'Agent name (auto-generated if omitted)') .option('-t, --task-name ', 'Name for the detail task') .option('-c, --context ', 'Additional context') .action(async (phaseId: string, options: { name?: string; taskName?: string; context?: string }) => { try { const client = createDefaultTrpcClient(); const agent = await client.spawnArchitectDetail.mutate({ name: options.name, phaseId, taskName: options.taskName, context: options.context, }); console.log(`Started architect agent in detail mode`); console.log(` Agent: ${agent.name} (${agent.id})`); console.log(` Mode: ${agent.mode}`); console.log(` Phase: ${phaseId}`); } catch (error) { console.error('Failed to start detail:', (error as Error).message); process.exit(1); } }); // Phase command group const phaseCommand = program .command('phase') .description('Phase dependency and dispatch management'); // cw phase add-dependency --phase --depends-on phaseCommand .command('add-dependency') .description('Add a dependency between phases') .requiredOption('--phase ', 'Phase that depends on another') .requiredOption('--depends-on ', 'Phase that must complete first') .action(async (options: { phase: string; dependsOn: string }) => { try { const client = createDefaultTrpcClient(); await client.createPhaseDependency.mutate({ phaseId: options.phase, dependsOnPhaseId: options.dependsOn, }); console.log(`Added dependency: phase ${options.phase} depends on ${options.dependsOn}`); } catch (error) { console.error('Failed to add dependency:', (error as Error).message); process.exit(1); } }); // cw phase dependencies phaseCommand .command('dependencies ') .description('List dependencies for a phase') .action(async (phaseId: string) => { try { const client = createDefaultTrpcClient(); const result = await client.getPhaseDependencies.query({ phaseId }); if (result.dependencies.length === 0) { console.log('No dependencies'); return; } console.log('Dependencies:'); for (const dep of result.dependencies) { console.log(` ${dep}`); } } catch (error) { console.error('Failed to get dependencies:', (error as Error).message); process.exit(1); } }); // cw phase queue phaseCommand .command('queue ') .description('Queue a phase for execution') .action(async (phaseId: string) => { try { const client = createDefaultTrpcClient(); await client.queuePhase.mutate({ phaseId }); console.log(`Phase ${phaseId} queued for execution`); } catch (error) { console.error('Failed to queue phase:', (error as Error).message); process.exit(1); } }); // cw phase dispatch phaseCommand .command('dispatch') .description('Dispatch next available phase') .action(async () => { try { const client = createDefaultTrpcClient(); const result = await client.dispatchNextPhase.mutate(); if (result.success) { console.log('Phase dispatched successfully'); console.log(` Phase: ${result.phaseId}`); } else { console.log('Dispatch failed'); console.log(` Phase: ${result.phaseId || '(none)'}`); console.log(` Reason: ${result.reason}`); } } catch (error) { console.error('Failed to dispatch phase:', (error as Error).message); process.exit(1); } }); // cw phase queue-status phaseCommand .command('queue-status') .description('Show phase queue status') .action(async () => { try { const client = createDefaultTrpcClient(); const state = await client.getPhaseQueueState.query(); console.log('Phase Queue Status'); console.log('=================='); console.log(`Queued: ${state.queued.length}`); console.log(`Ready: ${state.ready.length}`); console.log(`Blocked: ${state.blocked.length}`); if (state.ready.length > 0) { console.log(''); console.log('Ready phases:'); for (const phase of state.ready) { console.log(` ${phase.phaseId}`); } } if (state.blocked.length > 0) { console.log(''); console.log('Blocked phases:'); for (const bp of state.blocked) { console.log(` ${bp.phaseId}: ${bp.reason}`); } } } catch (error) { console.error('Failed to get queue status:', (error as Error).message); process.exit(1); } }); // Project command group const projectCommand = program .command('project') .description('Manage registered projects'); // cw project register --name --url projectCommand .command('register') .description('Register a git repository as a project') .requiredOption('--name ', 'Project name') .requiredOption('--url ', 'Git repository URL') .action(async (options: { name: string; url: string }) => { try { const client = createDefaultTrpcClient(); const project = await client.registerProject.mutate({ name: options.name, url: options.url, }); console.log(`Registered project: ${project.id}`); console.log(` Name: ${project.name}`); console.log(` URL: ${project.url}`); if (project.hasPreviewConfig) { console.log(' Preview: .cw-preview.yml detected — preview deployments ready'); } else { console.log(' Preview: No .cw-preview.yml found. Run `cw preview setup` for instructions.'); } } catch (error) { console.error('Failed to register project:', (error as Error).message); process.exit(1); } }); // cw project list projectCommand .command('list') .description('List all registered projects') .action(async () => { try { const client = createDefaultTrpcClient(); const projects = await client.listProjects.query(); if (projects.length === 0) { console.log('No projects registered'); return; } for (const p of projects) { console.log(`${p.id} ${p.name} ${p.url}`); } } catch (error) { console.error('Failed to list projects:', (error as Error).message); process.exit(1); } }); // cw project delete projectCommand .command('delete ') .description('Delete a registered project') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); await client.deleteProject.mutate({ id }); console.log(`Deleted project: ${id}`); } catch (error) { console.error('Failed to delete project:', (error as Error).message); process.exit(1); } }); // cw project sync [name] --all projectCommand .command('sync [name]') .description('Sync project clone(s) from remote') .option('--all', 'Sync all projects') .action(async (name: string | undefined, options: { all?: boolean }) => { try { const client = createDefaultTrpcClient(); if (options.all) { const results = await client.syncAllProjects.mutate(); for (const r of results) { const status = r.success ? 'ok' : `FAILED: ${r.error}`; console.log(`${r.projectName}: ${status}`); } } else if (name) { const projects = await client.listProjects.query(); const project = projects.find((p) => p.name === name || p.id === name); if (!project) { console.error(`Project not found: ${name}`); process.exit(1); } const result = await client.syncProject.mutate({ id: project.id }); if (result.success) { console.log(`Synced ${result.projectName}: fetched=${result.fetched}, fast-forwarded=${result.fastForwarded}`); } else { console.error(`Sync failed: ${result.error}`); process.exit(1); } } else { console.error('Specify a project name or use --all'); process.exit(1); } } catch (error) { console.error('Failed to sync:', (error as Error).message); process.exit(1); } }); // cw project status [name] projectCommand .command('status [name]') .description('Show sync status for a project') .action(async (name: string | undefined) => { try { const client = createDefaultTrpcClient(); const projects = await client.listProjects.query(); const targets = name ? projects.filter((p) => p.name === name || p.id === name) : projects; if (targets.length === 0) { console.log(name ? `Project not found: ${name}` : 'No projects registered'); return; } for (const project of targets) { const status = await client.getProjectSyncStatus.query({ id: project.id }); const fetchedStr = status.lastFetchedAt ? new Date(status.lastFetchedAt).toLocaleString() : 'never'; console.log(`${project.name}:`); console.log(` Last fetched: ${fetchedStr}`); console.log(` Ahead: ${status.ahead} Behind: ${status.behind}`); } } catch (error) { console.error('Failed to get status:', (error as Error).message); process.exit(1); } }); // Account command group const accountCommand = program .command('account') .description('Manage provider accounts for agent spawning'); // cw account add accountCommand .command('add') .description('Extract current Claude login and register as an account') .option('--provider ', 'Provider name', 'claude') .option('--email ', 'Email (for manual registration without auto-extract)') .option('--token ', 'Setup token from `claude setup-token` (requires --email)') .action(async (options: { provider: string; email?: string; token?: string }) => { try { const client = createDefaultTrpcClient(); if (options.token) { // Token-based registration if (!options.email) { console.error('Error: --email is required when using --token'); process.exit(1); } const credentials = JSON.stringify({ claudeAiOauth: { accessToken: options.token }, }); const configJson = JSON.stringify({ hasCompletedOnboarding: true }); const existing = await client.listAccounts.query(); const alreadyRegistered = existing.find((a: any) => a.email === options.email); if (alreadyRegistered) { await client.updateAccountAuth.mutate({ id: alreadyRegistered.id, configJson, credentials, }); console.log(`Updated credentials for account: ${alreadyRegistered.id}`); console.log(` Email: ${options.email}`); console.log(` Token updated (setup token)`); return; } const account = await client.addAccount.mutate({ email: options.email, provider: options.provider, configJson, credentials, }); console.log(`Registered account: ${account.id}`); console.log(` Email: ${account.email}`); console.log(` Provider: ${account.provider}`); console.log(` Auth: setup token`); } else if (options.email) { // Manual registration — guard against duplicates const existing = await client.listAccounts.query(); const alreadyRegistered = existing.find((a: any) => a.email === options.email); if (alreadyRegistered) { console.log(`Account '${options.email}' already registered (${alreadyRegistered.id})`); return; } const account = await client.addAccount.mutate({ email: options.email, provider: options.provider, }); console.log(`Registered account: ${account.id}`); console.log(` Email: ${account.email}`); console.log(` Provider: ${account.provider}`); } else { // Auto-extract from current Claude login const { extractCurrentClaudeAccount } = await import('../agent/accounts/index.js'); const extracted = await extractCurrentClaudeAccount(); // Check if already registered const existing = await client.listAccounts.query(); const alreadyRegistered = existing.find((a: any) => a.email === extracted.email); if (alreadyRegistered) { // Compare refresh tokens to detect staleness let credentialsChanged = true; try { const dbCreds = alreadyRegistered.credentials ? JSON.parse(alreadyRegistered.credentials) : null; const sourceCreds = JSON.parse(extracted.credentials); const dbRefreshToken = dbCreds?.claudeAiOauth?.refreshToken; const sourceRefreshToken = sourceCreds?.claudeAiOauth?.refreshToken; credentialsChanged = dbRefreshToken !== sourceRefreshToken; } catch { // Parse error — assume changed, update to be safe } // Upsert: always update to be safe await client.updateAccountAuth.mutate({ id: alreadyRegistered.id, configJson: JSON.stringify(extracted.configJson), credentials: extracted.credentials, }); if (credentialsChanged) { console.log(`Updated credentials for account: ${alreadyRegistered.id}`); console.log(` Email: ${extracted.email}`); console.log(` Refresh token changed (source had fresher credentials)`); } else { console.log(`Credentials current for account: ${alreadyRegistered.id}`); console.log(` Email: ${extracted.email}`); console.log(` Refresh token unchanged`); } return; } // Create DB record with credentials stored in DB const account = await client.addAccount.mutate({ email: extracted.email, provider: options.provider, configJson: JSON.stringify(extracted.configJson), credentials: extracted.credentials, }); console.log(`Registered account: ${account.id}`); console.log(` Email: ${account.email}`); console.log(` Provider: ${account.provider}`); } } catch (error) { console.error('Failed to add account:', (error as Error).message); process.exit(1); } }); // cw account list accountCommand .command('list') .description('List all registered accounts') .action(async () => { try { const client = createDefaultTrpcClient(); const accounts = await client.listAccounts.query(); if (accounts.length === 0) { console.log('No accounts registered'); return; } for (const acct of accounts) { const status = acct.isExhausted ? 'EXHAUSTED' : 'AVAILABLE'; const until = acct.exhaustedUntil ? ` (until ${new Date(acct.exhaustedUntil).toLocaleString()})` : ''; console.log(`${acct.id} ${acct.email} ${acct.provider} [${status}${until}]`); } } catch (error) { console.error('Failed to list accounts:', (error as Error).message); process.exit(1); } }); // cw account remove accountCommand .command('remove ') .description('Remove an account') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); await client.removeAccount.mutate({ id }); console.log(`Removed account: ${id}`); } catch (error) { console.error('Failed to remove account:', (error as Error).message); process.exit(1); } }); // cw account refresh accountCommand .command('refresh') .description('Clear expired exhaustion flags') .action(async () => { try { const client = createDefaultTrpcClient(); const result = await client.refreshAccounts.mutate(); console.log(`Cleared ${result.cleared} expired exhaustions`); } catch (error) { console.error('Failed to refresh accounts:', (error as Error).message); process.exit(1); } }); // cw account extract accountCommand .command('extract') .description('Extract current Claude credentials for use with the UI (does not require server)') .option('--email ', 'Verify extracted account matches this email') .action(async (options: { email?: string }) => { try { const { extractCurrentClaudeAccount } = await import('../agent/accounts/index.js'); const extracted = await extractCurrentClaudeAccount(); if (options.email && extracted.email !== options.email) { console.error(`Account '${options.email}' not found (active account is '${extracted.email}')`); process.exit(1); return; } const output = { email: extracted.email, configJson: JSON.stringify(extracted.configJson), credentials: extracted.credentials, }; console.log(JSON.stringify(output, null, 2)); } catch (error) { console.error('Failed to extract account:', (error as Error).message); process.exit(1); } }); // Preview command group const previewCommand = program .command('preview') .description('Manage Docker-based preview deployments'); // cw preview start --initiative --project --branch [--phase ] // cw preview start --agent (agent-simplified: server resolves everything) previewCommand .command('start') .description('Start a preview deployment') .option('--initiative ', 'Initiative ID') .option('--project ', 'Project ID') .option('--branch ', 'Branch to deploy') .option('--phase ', 'Phase ID') .option('--agent ', 'Agent ID (server resolves initiative/project/branch)') .action(async (options: { initiative?: string; project?: string; branch?: string; phase?: string; agent?: string }) => { try { const client = createDefaultTrpcClient(); console.log('Starting preview deployment...'); let preview; if (options.agent) { preview = await client.startPreviewForAgent.mutate({ agentId: options.agent }); } else { if (!options.initiative || !options.project || !options.branch) { console.error('Either --agent or all of --initiative, --project, --branch are required'); process.exit(1); } preview = await client.startPreview.mutate({ initiativeId: options.initiative, projectId: options.project, branch: options.branch, phaseId: options.phase, }); } console.log(`Preview started: ${preview.id}`); console.log(` URL: ${preview.url}`); console.log(` Branch: ${preview.branch}`); console.log(` Mode: ${preview.mode}`); console.log(` Status: ${preview.status}`); console.log(` Services: ${preview.services.map(s => `${s.name} (${s.state})`).join(', ')}`); } catch (error) { console.error('Failed to start preview:', (error as Error).message); process.exit(1); } }); // cw preview stop // cw preview stop --agent previewCommand .command('stop [previewId]') .description('Stop a preview deployment') .option('--agent ', 'Stop preview by agent ID') .action(async (previewId: string | undefined, options: { agent?: string }) => { try { const client = createDefaultTrpcClient(); if (options.agent) { await client.stopPreviewByAgent.mutate({ agentId: options.agent }); console.log(`Previews for agent '${options.agent}' stopped`); } else if (previewId) { await client.stopPreview.mutate({ previewId }); console.log(`Preview '${previewId}' stopped`); } else { console.error('Either or --agent is required'); process.exit(1); } } catch (error) { console.error('Failed to stop preview:', (error as Error).message); process.exit(1); } }); // cw preview list [--initiative ] previewCommand .command('list') .description('List active preview deployments') .option('--initiative ', 'Filter by initiative ID') .action(async (options: { initiative?: string }) => { try { const client = createDefaultTrpcClient(); const previews = await client.listPreviews.query( options.initiative ? { initiativeId: options.initiative } : undefined, ); if (previews.length === 0) { console.log('No active previews'); return; } for (const p of previews) { console.log(`${p.id} ${p.url} ${p.branch} ${p.mode} [${p.status.toUpperCase()}]`); } } catch (error) { console.error('Failed to list previews:', (error as Error).message); process.exit(1); } }); // cw preview status previewCommand .command('status ') .description('Get preview deployment status') .action(async (previewId: string) => { try { const client = createDefaultTrpcClient(); const preview = await client.getPreviewStatus.query({ previewId }); if (!preview) { console.log(`Preview '${previewId}' not found`); return; } console.log(`Preview: ${preview.id}`); console.log(` URL: ${preview.url}`); console.log(` Branch: ${preview.branch}`); console.log(` Mode: ${preview.mode}`); console.log(` Status: ${preview.status}`); console.log(` Initiative: ${preview.initiativeId}`); console.log(` Project: ${preview.projectId}`); if (preview.services.length > 0) { console.log(' Services:'); for (const svc of preview.services) { console.log(` ${svc.name}: ${svc.state} (health: ${svc.health})`); } } } catch (error) { console.error('Failed to get preview status:', (error as Error).message); process.exit(1); } }); // cw preview setup [--auto --project ] previewCommand .command('setup') .description('Show preview deployment setup instructions') .option('--auto', 'Auto-create initiative and spawn agent to set up preview config') .option('--project ', 'Project ID (required with --auto)') .option('--provider ', 'Agent provider (optional, with --auto)') .action(async (options: { auto?: boolean; project?: string; provider?: string }) => { if (!options.auto) { console.log(`Preview Deployment Setup ======================== Prerequisites: - Docker installed and running - Project registered in Codewalkers (cw project register) Step 1: Add .cw-preview.yml to your project root Minimal (single service with Dockerfile): version: 1 services: app: build: . port: 3000 Multi-service (frontend + API + database): version: 1 services: frontend: build: context: . dockerfile: apps/web/Dockerfile port: 3000 route: / healthcheck: path: / backend: build: context: . dockerfile: packages/api/Dockerfile port: 8080 route: /api healthcheck: path: /health db: image: postgres:16-alpine internal: true env: POSTGRES_PASSWORD: preview Step 2: Commit .cw-preview.yml to your repo Step 3: Start a preview cw preview start --initiative --project --branch Without .cw-preview.yml, previews auto-discover: 1. docker-compose.yml / compose.yml 2. Dockerfile (single service, port 3000) Full reference: docs/preview.md`); return; } // --auto mode if (!options.project) { console.error('--project is required with --auto'); process.exit(1); } try { const client = createDefaultTrpcClient(); // Look up project const projects = await client.listProjects.query(); const project = projects.find((p: { id: string }) => p.id === options.project); if (!project) { console.error(`Project '${options.project}' not found`); process.exit(1); } // Create initiative const initiative = await client.createInitiative.mutate({ name: `Preview setup: ${project.name}`, projectIds: [project.id], }); // Create root page with setup guide const rootPage = await client.getRootPage.query({ initiativeId: initiative.id }); const { markdownToTiptapJson } = await import('../agent/markdown-to-tiptap.js'); const setupMarkdown = `# Preview Setup for ${project.name} Analyze the project structure and create a \`.cw-preview.yml\` configuration file for Docker-based preview deployments. ## What to do 1. Examine the project's build system, frameworks, and service architecture 2. Identify all services (frontend, backend, database, etc.) 3. Create a \`.cw-preview.yml\` at the project root with appropriate service definitions 4. Include dev mode configuration for hot-reload support 5. Add healthcheck endpoints where applicable 6. Commit the file to the repository ## Reference See the Codewalkers documentation for .cw-preview.yml format and options.`; await client.updatePage.mutate({ id: rootPage.id, content: JSON.stringify(markdownToTiptapJson(setupMarkdown)), }); // Spawn refine agent const spawnInput: { initiativeId: string; instruction: string; provider?: string } = { initiativeId: initiative.id, instruction: 'Analyze the project and create a .cw-preview.yml for preview deployments. Follow the setup guide in the initiative pages.', }; if (options.provider) { spawnInput.provider = options.provider; } const agent = await client.spawnArchitectRefine.mutate(spawnInput); console.log(`Created initiative: ${initiative.id}`); console.log(`Spawned agent: ${agent.id} (${agent.name})`); } catch (error) { console.error('Failed to set up preview:', (error as Error).message); process.exit(1); } }); // ========================================================================= // Inter-agent conversation commands // ========================================================================= // cw listen --agent-id program .command('listen') .description('Listen for pending conversations via SSE subscription') .requiredOption('--agent-id ', 'Agent ID to listen for') .action(async (options: { agentId: string }) => { try { const client = createDefaultTrpcClient(); const subscription = client.onPendingConversation.subscribe( { agentId: options.agentId }, { onData(envelope) { const conv = envelope.data; console.log(JSON.stringify({ conversationId: conv.conversationId, fromAgentId: conv.fromAgentId, question: conv.question, phaseId: conv.phaseId, taskId: conv.taskId, })); subscription.unsubscribe(); process.exit(0); }, onError(err) { console.error('Failed to listen:', err.message); subscription.unsubscribe(); process.exit(1); }, }, ); } catch (error) { console.error('Failed to listen:', (error as Error).message); process.exit(1); } }); // cw ask --from --agent-id|--phase-id|--task-id program .command('ask ') .description('Ask a question to another agent and wait for the answer via SSE') .requiredOption('--from ', 'Your agent ID (the asker)') .option('--agent-id ', 'Target agent ID') .option('--phase-id ', 'Target phase ID (find running agent in phase)') .option('--task-id ', 'Target task ID (find running agent for task)') .action(async (question: string, options: { from: string; agentId?: string; phaseId?: string; taskId?: string; }) => { try { const client = createDefaultTrpcClient(); // Create the conversation const conversation = await client.createConversation.mutate({ fromAgentId: options.from, toAgentId: options.agentId, phaseId: options.phaseId, taskId: options.taskId, question, }); // Subscribe for answer via SSE const subscription = client.onConversationAnswer.subscribe( { conversationId: conversation.id }, { onData(envelope) { console.log(envelope.data.answer); subscription.unsubscribe(); process.exit(0); }, onError(err) { console.error('Failed to ask:', err.message); subscription.unsubscribe(); process.exit(1); }, }, ); } catch (error) { console.error('Failed to ask:', (error as Error).message); process.exit(1); } }); // cw answer --conversation-id program .command('answer ') .description('Answer a pending conversation') .requiredOption('--conversation-id ', 'Conversation ID to answer') .action(async (answer: string, options: { conversationId: string }) => { try { const client = createDefaultTrpcClient(); await client.answerConversation.mutate({ id: options.conversationId, answer, }); console.log(JSON.stringify({ conversationId: options.conversationId, status: 'answered' })); } catch (error) { console.error('Failed to answer:', (error as Error).message); process.exit(1); } }); // ── Errand commands ──────────────────────────────────────────────── const errandCommand = program .command('errand') .description('Manage lightweight interactive agent sessions for small changes'); errandCommand .command('start ') .description('Start a new errand session') .requiredOption('--project ', 'Project ID') .option('--base ', 'Base branch to create errand from (default: main)') .action(async (description: string, options: { project: string; base?: string }) => { if (description.length > 200) { console.error(`Error: description must be ≤200 characters (${description.length} given)`); process.exit(1); } try { const client = createDefaultTrpcClient(); const errand = await client.errand.create.mutate({ description, projectId: options.project, baseBranch: options.base, }); console.log('Errand started'); console.log(` ID: ${errand.id}`); console.log(` Branch: ${errand.branch}`); console.log(` Agent: ${errand.agentId}`); } catch (error) { console.error('Failed to start errand:', (error as Error).message); process.exit(1); } }); errandCommand .command('list') .description('List errands') .option('--project ', 'Filter by project') .option('--status ', 'Filter by status: active|pending_review|conflict|merged|abandoned') .action(async (options: { project?: string; status?: string }) => { try { const client = createDefaultTrpcClient(); const errands = await client.errand.list.query({ projectId: options.project, status: options.status as any, }); if (errands.length === 0) { console.log('No errands found'); return; } for (const e of errands) { const desc = e.description.length > 60 ? e.description.slice(0, 57) + '...' : e.description; console.log([e.id.slice(0, 8), desc, e.branch, e.status, e.agentAlias ?? '-'].join('\t')); } } catch (error) { console.error('Failed to list errands:', (error as Error).message); process.exit(1); } }); errandCommand .command('chat ') .description('Deliver a message to the running errand agent') .action(async (id: string, message: string) => { try { const client = createDefaultTrpcClient(); await client.errand.sendMessage.mutate({ id, message }); // No stdout on success — agent response appears in UI log stream } catch (error) { console.error((error as Error).message); process.exit(1); } }); errandCommand .command('diff ') .description('Print unified git diff between base branch and errand branch') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); const { diff } = await client.errand.diff.query({ id }); if (diff) process.stdout.write(diff); // Empty diff: no output, exit 0 — not an error } catch (error) { const msg = (error as Error).message; if (msg.includes('not found') || msg.includes('NOT_FOUND')) { console.error(`Errand ${id} not found`); } else { console.error(msg); } process.exit(1); } }); errandCommand .command('complete ') .description('Mark errand as done and ready for review') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); await client.errand.complete.mutate({ id }); console.log(`Errand ${id} marked as ready for review`); } catch (error) { console.error((error as Error).message); process.exit(1); } }); errandCommand .command('merge ') .description('Merge errand branch into target branch') .option('--target ', 'Target branch (default: baseBranch stored in DB)') .action(async (id: string, options: { target?: string }) => { try { const client = createDefaultTrpcClient(); const errand = await client.errand.get.query({ id }); await client.errand.merge.mutate({ id, target: options.target }); const target = options.target ?? errand.baseBranch; console.log(`Merged ${errand.branch} into ${target}`); } catch (error) { const err = error as any; const conflictFiles: string[] | undefined = err?.data?.conflictFiles ?? err?.shape?.data?.conflictFiles; if (conflictFiles) { console.error(`Merge conflict in ${conflictFiles.length} file(s):`); for (const f of conflictFiles) console.error(` ${f}`); console.error(`Run: cw errand resolve ${id}`); } else { console.error((error as Error).message); } process.exit(1); } }); errandCommand .command('resolve ') .description('Print worktree path and conflicting files for manual resolution') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); const errand = await client.errand.get.query({ id }); if (errand.status !== 'conflict') { console.error(`Errand ${id} is not in conflict (status: ${errand.status})`); process.exit(1); } // projectPath is added to errand.get by Task 1; cast until type is updated const projectPath = (errand as any).projectPath as string | null | undefined; const worktreePath = projectPath ? `${projectPath}/.cw-worktrees/${id}` : `.cw-worktrees/${id}`; console.log(`Resolve conflicts in worktree: ${worktreePath}`); console.log('Conflicting files:'); for (const f of (errand as any).conflictFiles ?? []) { console.log(` ${f}`); } console.log('After resolving: stage and commit changes in the worktree, then run:'); console.log(` cw errand merge ${id}`); } catch (error) { console.error((error as Error).message); process.exit(1); } }); errandCommand .command('abandon ') .description('Stop agent, remove worktree and branch, keep DB record as abandoned') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); await client.errand.abandon.mutate({ id }); console.log(`Errand ${id} abandoned`); } catch (error) { console.error((error as Error).message); process.exit(1); } }); errandCommand .command('delete ') .description('Stop agent, remove worktree, delete branch, and delete DB record') .action(async (id: string) => { try { const client = createDefaultTrpcClient(); await client.errand.delete.mutate({ id }); console.log(`Errand ${id} deleted`); } catch (error) { console.error((error as Error).message); process.exit(1); } }); return program; } /** * Runs the CLI, handling server mode and commands. */ export async function runCli(): Promise { // Check for server flag early, before Commander processes const hasServerFlag = process.argv.includes('--server') || process.argv.includes('-s'); if (hasServerFlag) { // Get port from args if present const portIndex = process.argv.findIndex(arg => arg === '-p' || arg === '--port'); const port = portIndex !== -1 && process.argv[portIndex + 1] ? parseInt(process.argv[portIndex + 1], 10) : undefined; const debug = process.argv.includes('--debug') || process.argv.includes('-d'); await startServer(port, debug); // Server runs indefinitely until signal return; } // Normal CLI processing const program = createCli(); program.parse(process.argv); }