- Register errandProcedures in appRouter (was defined but never spread) - Fix nullable projectId guard in errand delete/abandon procedures - Add sendUserMessage stub to MockAgentManager in headquarters and radar-procedures tests (AgentManager interface gained this method) - Add missing qualityReview field to Initiative fixture in file-io test (schema gained this column from the quality-review phase) - Cast conflictFiles access in CLI errand resolve command Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1948 lines
69 KiB
TypeScript
1948 lines
69 KiB
TypeScript
/**
|
|
* 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';
|
|
|
|
/** 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<void> {
|
|
// 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<void>): 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 <number>', '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 <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());
|
|
}
|
|
});
|
|
|
|
// Agent command group
|
|
const agentCommand = program
|
|
.command('agent')
|
|
.description('Manage agents');
|
|
|
|
// cw agent spawn --name <name> --task <taskId> <prompt>
|
|
agentCommand
|
|
.command('spawn <prompt>')
|
|
.description('Spawn a new agent to work on a task')
|
|
.option('--name <name>', 'Human-readable name for the agent (auto-generated if omitted)')
|
|
.requiredOption('--task <taskId>', 'Task ID to assign to agent')
|
|
.option('--cwd <path>', 'Working directory for agent')
|
|
.option('--provider <provider>', 'Agent provider (claude, codex, gemini, cursor, auggie, amp, opencode)')
|
|
.option('--initiative <id>', '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 <name>
|
|
agentCommand
|
|
.command('stop <name>')
|
|
.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 <name>
|
|
agentCommand
|
|
.command('delete <name>')
|
|
.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 <name>
|
|
agentCommand
|
|
.command('get <name>')
|
|
.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 <name> <answers>
|
|
// Accepts either JSON object {"q1": "answer1", "q2": "answer2"} or single answer
|
|
agentCommand
|
|
.command('resume <name> <answers>')
|
|
.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<string, string>;
|
|
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 <name>
|
|
agentCommand
|
|
.command('result <name>')
|
|
.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 <parentTaskId> | --phase <phaseId> | --initiative <initiativeId>
|
|
taskCommand
|
|
.command('list')
|
|
.description('List tasks (by parent task, phase, or initiative)')
|
|
.option('--parent <parentTaskId>', 'Parent task ID (for child tasks)')
|
|
.option('--phase <phaseId>', 'Phase ID')
|
|
.option('--initiative <initiativeId>', '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 <taskId>
|
|
taskCommand
|
|
.command('get <taskId>')
|
|
.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 <taskId> <status>
|
|
taskCommand
|
|
.command('status <taskId> <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);
|
|
}
|
|
});
|
|
|
|
// Message command group
|
|
const messageCommand = program
|
|
.command('message')
|
|
.description('View agent messages and questions');
|
|
|
|
// cw message list [--agent <agentId>] [--status <status>]
|
|
messageCommand
|
|
.command('list')
|
|
.description('List messages from agents')
|
|
.option('--agent <agentId>', 'Filter by agent ID')
|
|
.option('--status <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 <messageId>
|
|
messageCommand
|
|
.command('read <messageId>')
|
|
.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} "<your response>"`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to read message:', (error as Error).message);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// cw message respond <messageId> <response>
|
|
messageCommand
|
|
.command('respond <messageId> <response>')
|
|
.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 <name> "<response>"');
|
|
} 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 <taskId>
|
|
dispatchCommand
|
|
.command('queue <taskId>')
|
|
.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 <taskId>
|
|
dispatchCommand
|
|
.command('complete <taskId>')
|
|
.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 <taskId>
|
|
mergeCommand
|
|
.command('queue <taskId>')
|
|
.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 <branch>', '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 <name>
|
|
initiativeCommand
|
|
.command('create <name>')
|
|
.description('Create a new initiative')
|
|
.option('--project <ids...>', '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 <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 <id>
|
|
initiativeCommand
|
|
.command('get <id>')
|
|
.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 <initiative-id>
|
|
initiativeCommand
|
|
.command('phases <initiativeId>')
|
|
.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 <initiative-id>
|
|
architectCommand
|
|
.command('discuss <initiativeId>')
|
|
.description('Start discussion phase for an initiative')
|
|
.option('--name <name>', 'Agent name (auto-generated if omitted)')
|
|
.option('-c, --context <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 <initiative-id>
|
|
architectCommand
|
|
.command('plan <initiativeId>')
|
|
.description('Plan phases for an initiative')
|
|
.option('--name <name>', 'Agent name (auto-generated if omitted)')
|
|
.option('-s, --summary <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 <phase-id>
|
|
architectCommand
|
|
.command('detail <phaseId>')
|
|
.description('Detail a phase into tasks')
|
|
.option('--name <name>', 'Agent name (auto-generated if omitted)')
|
|
.option('-t, --task-name <taskName>', 'Name for the detail task')
|
|
.option('-c, --context <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 <id> --depends-on <id>
|
|
phaseCommand
|
|
.command('add-dependency')
|
|
.description('Add a dependency between phases')
|
|
.requiredOption('--phase <id>', 'Phase that depends on another')
|
|
.requiredOption('--depends-on <id>', '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 <phaseId>
|
|
phaseCommand
|
|
.command('dependencies <phaseId>')
|
|
.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 <phaseId>
|
|
phaseCommand
|
|
.command('queue <phaseId>')
|
|
.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 <name> --url <url>
|
|
projectCommand
|
|
.command('register')
|
|
.description('Register a git repository as a project')
|
|
.requiredOption('--name <name>', 'Project name')
|
|
.requiredOption('--url <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 <id>
|
|
projectCommand
|
|
.command('delete <id>')
|
|
.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>', 'Provider name', 'claude')
|
|
.option('--email <email>', 'Email (for manual registration without auto-extract)')
|
|
.option('--token <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 <id>
|
|
accountCommand
|
|
.command('remove <id>')
|
|
.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 <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 <id> --project <id> --branch <branch> [--phase <id>]
|
|
// cw preview start --agent <id> (agent-simplified: server resolves everything)
|
|
previewCommand
|
|
.command('start')
|
|
.description('Start a preview deployment')
|
|
.option('--initiative <id>', 'Initiative ID')
|
|
.option('--project <id>', 'Project ID')
|
|
.option('--branch <branch>', 'Branch to deploy')
|
|
.option('--phase <id>', 'Phase ID')
|
|
.option('--agent <id>', '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 <previewId>
|
|
// cw preview stop --agent <id>
|
|
previewCommand
|
|
.command('stop [previewId]')
|
|
.description('Stop a preview deployment')
|
|
.option('--agent <id>', '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 <previewId> or --agent <id> is required');
|
|
process.exit(1);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to stop preview:', (error as Error).message);
|
|
process.exit(1);
|
|
}
|
|
});
|
|
|
|
// cw preview list [--initiative <id>]
|
|
previewCommand
|
|
.command('list')
|
|
.description('List active preview deployments')
|
|
.option('--initiative <id>', '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 <previewId>
|
|
previewCommand
|
|
.command('status <previewId>')
|
|
.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 <id>]
|
|
previewCommand
|
|
.command('setup')
|
|
.description('Show preview deployment setup instructions')
|
|
.option('--auto', 'Auto-create initiative and spawn agent to set up preview config')
|
|
.option('--project <id>', 'Project ID (required with --auto)')
|
|
.option('--provider <name>', '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 <id> --project <id> --branch <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 <id> 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 <id>
|
|
program
|
|
.command('listen')
|
|
.description('Listen for pending conversations via SSE subscription')
|
|
.requiredOption('--agent-id <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 <question> --from <agentId> --agent-id|--phase-id|--task-id <target>
|
|
program
|
|
.command('ask <question>')
|
|
.description('Ask a question to another agent and wait for the answer via SSE')
|
|
.requiredOption('--from <agentId>', 'Your agent ID (the asker)')
|
|
.option('--agent-id <id>', 'Target agent ID')
|
|
.option('--phase-id <id>', 'Target phase ID (find running agent in phase)')
|
|
.option('--task-id <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 <answer> --conversation-id <id>
|
|
program
|
|
.command('answer <answer>')
|
|
.description('Answer a pending conversation')
|
|
.requiredOption('--conversation-id <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>')
|
|
.description('Start a new errand session')
|
|
.requiredOption('--project <id>', 'Project ID')
|
|
.option('--base <branch>', '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 <id>', 'Filter by project')
|
|
.option('--status <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 <id> <message>')
|
|
.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 <id>')
|
|
.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 <id>')
|
|
.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 <id>')
|
|
.description('Merge errand branch into target branch')
|
|
.option('--target <branch>', '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 <id>')
|
|
.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 <id>')
|
|
.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 <id>')
|
|
.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<void> {
|
|
// 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);
|
|
}
|