feat(01-05): add HTTP server with health endpoint and PID file

- CoordinationServer class using node:http
- GET /health returns status, uptime, processCount
- GET /status returns full server state and process list
- PID file at ~/.cw/server.pid prevents duplicate servers
- CLI --server flag and --port option for server mode
- CW_PORT env var support for custom port
This commit is contained in:
Lukas May
2026-01-30 13:22:35 +01:00
parent f117227fed
commit bec46aa234
4 changed files with 429 additions and 6 deletions

View File

@@ -2,17 +2,66 @@
* 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 { ProcessManager, ProcessRegistry } from '../process/index.js';
import { LogManager } from '../logging/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): 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);
}
// Import shutdown handler (will be implemented in Task 2)
// For now, just handle basic signals
const handleSignal = async (signal: string) => {
console.log(`\nReceived ${signal}, shutting down...`);
await server.stop();
await processManager.stopAll();
process.exit(0);
};
process.on('SIGTERM', () => handleSignal('SIGTERM'));
process.on('SIGINT', () => handleSignal('SIGINT'));
}
/**
* 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(): Command {
export function createCli(serverHandler?: (port?: number) => Promise<void>): Command {
const program = new Command();
program
@@ -20,6 +69,21 @@ export function createCli(): Command {
.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);
}
});
// Placeholder commands - will be implemented in later phases
program
.command('status')
@@ -44,3 +108,27 @@ export function createCli(): Command {
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);
}