Wires errand command group into CLI with start, list, chat, diff, complete, merge, resolve, abandon, and delete subcommands. All commands call tRPC procedures via createDefaultTrpcClient(). The start command validates description length client-side (≤200 chars) before making any network calls. 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.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);
|
|
}
|