diff --git a/packages/cli/src/commands/gateway.ts b/packages/cli/src/commands/gateway.ts index 5d5a8e8..b6e4b08 100644 --- a/packages/cli/src/commands/gateway.ts +++ b/packages/cli/src/commands/gateway.ts @@ -1,10 +1,12 @@ import { createInterface } from 'node:readline'; -import { writeFileSync } from 'node:fs'; -import { resolve } from 'node:path'; +import { spawn } from 'node:child_process'; +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; import type { Command } from 'commander'; import { DEFAULT_LOCAL_CONFIG, DEFAULT_TEAM_CONFIG, + loadConfig, type MosaicConfig, type StorageTier, } from '@mosaic/config'; @@ -64,6 +66,37 @@ async function runInit(opts: { tier?: string; output: string }): Promise { console.log(' 2. Run: pnpm --filter @mosaic/gateway exec tsx src/main.ts'); } +const PID_FILE = resolve(process.cwd(), '.mosaic/gateway.pid'); + +function writePidFile(pid: number): void { + const dir = dirname(PID_FILE); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + writeFileSync(PID_FILE, String(pid)); +} + +function readPidFile(): number | null { + if (!existsSync(PID_FILE)) return null; + const raw = readFileSync(PID_FILE, 'utf-8').trim(); + const pid = Number(raw); + return Number.isFinite(pid) ? pid : null; +} + +function isProcessRunning(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function printConfigSummary(config: MosaicConfig): void { + console.log(` Tier: ${config.tier}`); + console.log(` Storage: ${config.storage.type}`); + console.log(` Queue: ${config.queue.type}`); + console.log(` Memory: ${config.memory.type}`); +} + export function registerGatewayCommand(program: Command): void { const gateway = program.command('gateway').description('Gateway management commands'); @@ -75,4 +108,91 @@ export function registerGatewayCommand(program: Command): void { .action(async (opts: { tier?: string; output: string }) => { await runInit(opts); }); + + gateway + .command('start') + .description('Start the Mosaic gateway process') + .option('--port ', 'Port to listen on (overrides config)') + .option('--daemon', 'Run in background and write PID to .mosaic/gateway.pid') + .action((opts: { port?: string; daemon?: boolean }) => { + const config = loadConfig(); + const port = opts.port ?? '4000'; + + console.log('Starting gateway…'); + printConfigSummary(config); + console.log(` Port: ${port}`); + + const entryPoint = resolve(process.cwd(), 'apps/gateway/src/main.ts'); + const env = { ...process.env, GATEWAY_PORT: port }; + + if (opts.daemon) { + const child = spawn('npx', ['tsx', entryPoint], { + env, + stdio: 'ignore', + detached: true, + }); + + child.unref(); + + if (child.pid) { + writePidFile(child.pid); + console.log(`\nGateway started in background (PID ${child.pid})`); + console.log(`PID file: ${PID_FILE}`); + } + } else { + const child = spawn('npx', ['tsx', entryPoint], { + env, + stdio: 'inherit', + }); + + child.on('exit', (code) => { + process.exit(code ?? 0); + }); + } + }); + + gateway + .command('stop') + .description('Stop the running gateway process') + .action(() => { + const pid = readPidFile(); + + if (pid === null) { + console.error('No PID file found at', PID_FILE); + process.exit(1); + } + + if (!isProcessRunning(pid)) { + console.log(`Process ${pid} is not running. Removing stale PID file.`); + unlinkSync(PID_FILE); + return; + } + + process.kill(pid, 'SIGTERM'); + unlinkSync(PID_FILE); + console.log(`Gateway stopped (PID ${pid})`); + }); + + gateway + .command('status') + .description('Show gateway process status') + .action(() => { + const config = loadConfig(); + const pid = readPidFile(); + + if (pid !== null && isProcessRunning(pid)) { + console.log('Gateway: running'); + console.log(` PID: ${pid}`); + } else { + console.log('Gateway: stopped'); + if (pid !== null) { + console.log(` (stale PID file for ${pid})`); + unlinkSync(PID_FILE); + } + } + + console.log(''); + console.log('Config:'); + printConfigSummary(config); + }); }