feat: add auto-start server feature for CLI

- Added /health endpoint to server for readiness checks (no auth required)
- Created autoStartServer module that auto-starts server when:
  - HAPI_SERVER_URL not set (using default localhost:3006)
  - cliApiToken exists in settings (server previously used)
  - Port 3006 is not currently listening
- Server runs as child process (not daemon) and exits when CLI exits
- Integrated maybeAutoStartServer() into hapi, codex, and gemini commands
This commit is contained in:
weishu
2026-01-02 16:21:14 +08:00
parent 11c95126e3
commit 94823b7a8b
3 changed files with 172 additions and 0 deletions
+4
View File
@@ -27,6 +27,7 @@ import { handleConnectCommand } from './commands/connect'
import { spawnHappyCLI } from './utils/spawnHappyCLI'
import { execFileSync } from 'node:child_process'
import { initializeToken } from './ui/tokenInit'
import { maybeAutoStartServer } from './utils/autoStartServer'
import { ensureRuntimeAssets } from './runtime/assets'
import { runHappyMcpStdioBridge } from './codex/happyMcpStdioBridge'
import { withBunRuntimeEnv } from './utils/bunRuntime'
@@ -140,6 +141,7 @@ import { getCliArgs } from './utils/cliArgs'
}
await initializeToken();
await maybeAutoStartServer();
await authAndSetupMachineIfNeeded();
await runCodex(options);
// Do not force exit here; allow instrumentation to show lingering handles
@@ -169,6 +171,7 @@ import { getCliArgs } from './utils/cliArgs'
registerGeminiAgent(yolo);
await initializeToken();
await maybeAutoStartServer();
await authAndSetupMachineIfNeeded();
await runAgentSession({ agentType: 'gemini', startedBy });
} catch (error) {
@@ -408,6 +411,7 @@ ${chalk.bold.cyan('Claude Code Options (from `claude --help`):')}
// Normal flow - auth and machine setup
await initializeToken();
await maybeAutoStartServer();
await authAndSetupMachineIfNeeded();
// Always auto-start daemon for simplicity
+165
View File
@@ -0,0 +1,165 @@
/**
* Auto-start server module
*
* Automatically starts the HAPI server when CLI is launched
* if specific conditions are met:
* 1. HAPI_SERVER_URL is not set (using default localhost:3006)
* 2. cliApiToken exists in settings.json (server was previously started)
* 3. Port 3006 is not currently listening
*/
import chalk from 'chalk'
import { createConnection } from 'node:net'
import { configuration } from '@/configuration'
import { readSettings } from '@/persistence'
import { spawnHappyCLI } from '@/utils/spawnHappyCLI'
import { logger } from '@/ui/logger'
const DEFAULT_SERVER_PORT = 3006
const SERVER_STARTUP_TIMEOUT_MS = 10000
const POLL_INTERVAL_MS = 200
const PORT_CHECK_TIMEOUT_MS = 1000
/**
* Check if a port is currently listening (cross-platform)
*/
async function checkPortListening(port: number, host: string = '127.0.0.1'): Promise<boolean> {
return new Promise((resolve) => {
const socket = createConnection({ port, host })
const cleanup = () => {
socket.removeAllListeners()
socket.destroy()
}
socket.setTimeout(PORT_CHECK_TIMEOUT_MS)
socket.on('connect', () => {
cleanup()
resolve(true)
})
socket.on('error', () => {
cleanup()
resolve(false)
})
socket.on('timeout', () => {
cleanup()
resolve(false)
})
})
}
/**
* Check if server is ready via health endpoint
*/
async function checkServerHealth(url: string): Promise<boolean> {
try {
const response = await fetch(`${url}/health`, {
signal: AbortSignal.timeout(1000)
})
return response.ok
} catch {
return false
}
}
/**
* Wait for server to become ready
*/
async function waitForServerReady(
url: string,
maxWaitMs: number = SERVER_STARTUP_TIMEOUT_MS,
pollIntervalMs: number = POLL_INTERVAL_MS
): Promise<boolean> {
const startTime = Date.now()
while (Date.now() - startTime < maxWaitMs) {
if (await checkServerHealth(url)) {
logger.debug(`[AUTO-START] Server ready after ${Date.now() - startTime}ms`)
return true
}
await new Promise(resolve => setTimeout(resolve, pollIntervalMs))
}
return false
}
/**
* Determine if server should be auto-started
*/
async function shouldAutoStartServer(): Promise<boolean> {
// Condition 1: HAPI_SERVER_URL not set (using default localhost:3006)
if (process.env.HAPI_SERVER_URL) {
logger.debug('[AUTO-START] HAPI_SERVER_URL is set, skipping auto-start')
return false
}
// Condition 2: cliApiToken exists in settings.json (server was previously started)
const settings = await readSettings()
if (!settings.cliApiToken) {
logger.debug('[AUTO-START] No cliApiToken in settings, skipping auto-start')
return false
}
// Condition 3: Port 3006 is not currently listening
const isListening = await checkPortListening(DEFAULT_SERVER_PORT)
if (isListening) {
logger.debug('[AUTO-START] Port 3006 already in use, skipping auto-start')
return false
}
return true
}
/**
* Start server as a child process (will exit when CLI exits)
*/
function startServerAsChild(): void {
const serverProcess = spawnHappyCLI(['server'], {
detached: false,
stdio: 'ignore',
env: process.env
})
logger.debug(`[AUTO-START] Server process spawned with PID ${serverProcess.pid}`)
// Ensure server is killed when CLI exits
process.on('exit', () => {
serverProcess.kill()
})
}
/**
* Main entry point: auto-start server if conditions are met
*/
export async function maybeAutoStartServer(): Promise<void> {
try {
const shouldStart = await shouldAutoStartServer()
if (!shouldStart) {
return
}
logger.debug('[AUTO-START] Starting server automatically...')
console.log(chalk.gray('Starting HAPI server in background...'))
startServerAsChild()
const isReady = await waitForServerReady(configuration.serverUrl)
if (!isReady) {
console.log(chalk.yellow('Warning: Server did not start within expected time'))
console.log(chalk.gray(' Try running `hapi server` manually to see errors'))
return
}
console.log(chalk.green('HAPI server started'))
} catch (error) {
logger.debug('[AUTO-START] Error during server auto-start', error)
console.log(chalk.yellow('Warning: Failed to auto-start server'))
if (error instanceof Error) {
console.log(chalk.gray(` Error: ${error.message}`))
}
}
}