- Add cw merge queue <taskId> to queue tasks for merge - Add cw merge status to show merge queue status - Add cw merge next to show next task ready to merge - Add cw coordinate [--target <branch>] to process all ready merges
656 lines
22 KiB
TypeScript
656 lines
22 KiB
TypeScript
/**
|
|
* Codewalk District 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 { ProcessManager, ProcessRegistry } from '../process/index.js';
|
|
import { LogManager } from '../logging/index.js';
|
|
import { createDefaultTrpcClient } from './trpc-client.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): 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);
|
|
|
|
// Create dependencies
|
|
const registry = new ProcessRegistry();
|
|
const processManager = new ProcessManager(registry);
|
|
const logManager = new LogManager();
|
|
|
|
// Create and start server
|
|
const server = new CoordinationServer(
|
|
{ port: serverPort },
|
|
processManager,
|
|
logManager
|
|
);
|
|
|
|
try {
|
|
await server.start();
|
|
} catch (error) {
|
|
console.error('Failed to start server:', (error as Error).message);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Install graceful shutdown handlers
|
|
const shutdown = new GracefulShutdown(server, processManager, logManager);
|
|
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 option (global flag)
|
|
program
|
|
.option('-s, --server', 'Start the coordination server')
|
|
.option('-p, --port <number>', 'Port for the server (default: 3847, env: CW_PORT)', parseInt);
|
|
|
|
// 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');
|
|
}
|
|
});
|
|
|
|
// 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')
|
|
.requiredOption('--name <name>', 'Human-readable name for the agent (e.g., gastown)')
|
|
.requiredOption('--task <taskId>', 'Task ID to assign to agent')
|
|
.option('--cwd <path>', 'Working directory for agent')
|
|
.action(async (prompt: string, options: { name: string; task: string; cwd?: string }) => {
|
|
try {
|
|
const client = createDefaultTrpcClient();
|
|
const agent = await client.spawnAgent.mutate({
|
|
name: options.name,
|
|
taskId: options.task,
|
|
prompt,
|
|
cwd: options.cwd,
|
|
});
|
|
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 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> <response>
|
|
agentCommand
|
|
.command('resume <name> <response>')
|
|
.description('Resume an agent that is waiting for input')
|
|
.action(async (name: string, response: string) => {
|
|
try {
|
|
const client = createDefaultTrpcClient();
|
|
const result = await client.resumeAgent.mutate({ name, prompt: response });
|
|
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 --plan <planId>
|
|
taskCommand
|
|
.command('list')
|
|
.description('List tasks for a plan')
|
|
.requiredOption('--plan <planId>', 'Plan ID to list tasks for')
|
|
.action(async (options: { plan: string }) => {
|
|
try {
|
|
const client = createDefaultTrpcClient();
|
|
const tasks = await client.listTasks.query({ planId: options.plan });
|
|
if (tasks.length === 0) {
|
|
console.log('No tasks found for this plan');
|
|
return;
|
|
}
|
|
|
|
// Count by status
|
|
const pending = tasks.filter(t => t.status === 'pending' || t.status === 'pending_approval').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(` Plan: ${task.planId}`);
|
|
console.log(` Description: ${task.description ?? '(none)'}`);
|
|
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);
|
|
}
|
|
});
|
|
|
|
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;
|
|
|
|
await startServer(port);
|
|
// Server runs indefinitely until signal
|
|
return;
|
|
}
|
|
|
|
// Normal CLI processing
|
|
const program = createCli();
|
|
program.parse(process.argv);
|
|
}
|