refactor(startup): unify dev and cli daemon startup through runner

This commit is contained in:
Mohamed Boudra
2026-02-16 03:22:03 +00:00
parent 5bd6cc11de
commit 04a06197de
5 changed files with 87 additions and 171 deletions
@@ -1,9 +1,8 @@
import { spawn } from 'node:child_process'
import { spawn, spawnSync } from 'node:child_process'
import { closeSync, existsSync, openSync, readFileSync, rmSync } from 'node:fs'
import { createRequire } from 'node:module'
import path from 'node:path'
import { loadConfig, resolvePaseoHome } from '@getpaseo/server'
import { applySherpaLoaderEnv } from './sherpa-env.js'
export interface DaemonStartOptions {
port?: string
@@ -100,6 +99,22 @@ function buildRunnerArgs(options: DaemonStartOptions): string[] {
return args
}
function buildChildEnv(options: DaemonStartOptions): NodeJS.ProcessEnv {
const childEnv: NodeJS.ProcessEnv = { ...process.env }
if (options.home) {
childEnv.PASEO_HOME = options.home
}
if (options.listen) {
childEnv.PASEO_LISTEN = options.listen
} else if (options.port) {
childEnv.PASEO_LISTEN = `127.0.0.1:${options.port}`
}
if (options.allowedHosts) {
childEnv.PASEO_ALLOWED_HOSTS = options.allowedHosts
}
return childEnv
}
function resolveDaemonRunnerEntry(): string {
const serverExportPath = require.resolve('@getpaseo/server')
let currentDir = path.dirname(serverExportPath)
@@ -280,19 +295,7 @@ export async function startLocalDaemonDetached(
throw new Error('Cannot use --listen and --port together')
}
const childEnv: NodeJS.ProcessEnv = { ...process.env }
applySherpaLoaderEnv(childEnv)
if (options.home) {
childEnv.PASEO_HOME = options.home
}
if (options.listen) {
childEnv.PASEO_LISTEN = options.listen
} else if (options.port) {
childEnv.PASEO_LISTEN = `127.0.0.1:${options.port}`
}
if (options.allowedHosts) {
childEnv.PASEO_ALLOWED_HOSTS = options.allowedHosts
}
const childEnv = buildChildEnv(options)
const paseoHome = resolvePaseoHome(childEnv)
const logPath = path.join(paseoHome, DAEMON_LOG_FILENAME)
@@ -358,6 +361,29 @@ export async function startLocalDaemonDetached(
}
}
export function startLocalDaemonForeground(options: DaemonStartOptions): number {
if (options.listen && options.port) {
throw new Error('Cannot use --listen and --port together')
}
const childEnv = buildChildEnv(options)
const daemonRunnerEntry = resolveDaemonRunnerEntry()
const result = spawnSync(
process.execPath,
[...process.execArgv, daemonRunnerEntry, ...buildRunnerArgs(options)],
{
env: childEnv,
stdio: 'inherit',
}
)
if (result.error) {
throw result.error
}
return result.status ?? 1
}
export async function stopLocalDaemon(
options: StopLocalDaemonOptions = {}
): Promise<StopLocalDaemonResult> {
@@ -1,53 +0,0 @@
import { spawnSync } from 'node:child_process'
import {
applySherpaLoaderEnv as applySherpaLoaderEnvFromServer,
resolveSherpaLoaderEnv,
} from '@getpaseo/server'
const SHERPA_ENV_BOOTSTRAPPED = 'PASEO_SHERPA_ENV_BOOTSTRAPPED'
export function applySherpaLoaderEnv(env: NodeJS.ProcessEnv): {
changed: boolean
key: 'LD_LIBRARY_PATH' | 'DYLD_LIBRARY_PATH' | 'PATH' | null
libDir: string | null
} {
const result = applySherpaLoaderEnvFromServer(env)
return {
changed: result.changed,
key: result.key,
libDir: result.libDir,
}
}
export function reexecWithSherpaLoaderEnvIfNeeded(): never | void {
const resolved = resolveSherpaLoaderEnv()
if (!resolved) {
return
}
if (process.env[SHERPA_ENV_BOOTSTRAPPED] === '1') {
return
}
const envCopy: NodeJS.ProcessEnv = { ...process.env }
const applied = applySherpaLoaderEnvFromServer(envCopy)
const updatedValue = applied.key ? envCopy[applied.key] ?? '' : ''
if (applied.key && updatedValue === (process.env[applied.key] ?? '')) {
return
}
const env: NodeJS.ProcessEnv = {
...envCopy,
[SHERPA_ENV_BOOTSTRAPPED]: '1',
}
const result = spawnSync(process.execPath, process.argv.slice(1), {
env,
stdio: 'inherit',
})
if (result.error) {
throw result.error
}
process.exit(result.status ?? 1)
}
+3 -95
View File
@@ -1,18 +1,10 @@
import { Command } from 'commander'
import chalk from 'chalk'
import {
createPaseoDaemon,
loadConfig,
resolvePaseoHome,
createRootLogger,
loadPersistedConfig,
} from '@getpaseo/server'
import type { CliConfigOverrides } from '@getpaseo/server'
import {
startLocalDaemonForeground,
startLocalDaemonDetached,
type DaemonStartOptions as StartOptions,
} from './local-daemon.js'
import { reexecWithSherpaLoaderEnvIfNeeded } from './sherpa-env.js'
import { getErrorMessage } from '../../utils/errors.js'
export type { DaemonStartOptions as StartOptions } from './local-daemon.js'
@@ -35,34 +27,6 @@ export function startCommand(): Command {
})
}
function toCliOverrides(options: StartOptions): CliConfigOverrides {
const cliOverrides: CliConfigOverrides = {}
if (options.listen) {
cliOverrides.listen = options.listen
} else if (options.port) {
cliOverrides.listen = `127.0.0.1:${options.port}`
}
if (options.relay === false) {
cliOverrides.relayEnabled = false
}
if (options.allowedHosts) {
const raw = options.allowedHosts.trim()
cliOverrides.allowedHosts =
raw.toLowerCase() === 'true'
? true
: raw.split(',').map(h => h.trim()).filter(Boolean)
}
if (options.mcp === false) {
cliOverrides.mcpEnabled = false
}
return cliOverrides
}
export async function runStart(options: StartOptions): Promise<void> {
if (options.listen && options.port) {
console.error(chalk.red('Cannot use --listen and --port together'))
@@ -79,65 +43,9 @@ export async function runStart(options: StartOptions): Promise<void> {
}
return
}
reexecWithSherpaLoaderEnvIfNeeded()
if (options.home) {
process.env.PASEO_HOME = options.home
}
let paseoHome: string
let logger: ReturnType<typeof createRootLogger>
let config: ReturnType<typeof loadConfig>
try {
paseoHome = resolvePaseoHome()
const persistedConfig = loadPersistedConfig(paseoHome)
logger = createRootLogger(persistedConfig)
config = loadConfig(paseoHome, { cli: toCliOverrides(options) })
} catch (err) {
exitWithError(getErrorMessage(err))
}
let daemon: Awaited<ReturnType<typeof createPaseoDaemon>>
try {
daemon = await createPaseoDaemon(config, logger)
} catch (err) {
const message = getErrorMessage(err)
exitWithError(`Failed to initialize daemon: ${message}`)
}
let shuttingDown = false
const handleShutdown = async (signal: string) => {
if (shuttingDown) {
logger.info('Forcing exit...')
process.exit(1)
}
shuttingDown = true
logger.info(`${signal} received, shutting down gracefully... (press Ctrl+C again to force exit)`)
const forceExit = setTimeout(() => {
logger.warn('Forcing shutdown - HTTP server didn\'t close in time')
process.exit(1)
}, 10000)
try {
await daemon.stop()
clearTimeout(forceExit)
logger.info('Server closed')
process.exit(0)
} catch (err) {
clearTimeout(forceExit)
logger.error({ err }, 'Shutdown failed')
process.exit(1)
}
}
process.on('SIGTERM', () => handleShutdown('SIGTERM'))
process.on('SIGINT', () => handleShutdown('SIGINT'))
try {
await daemon.start()
const status = startLocalDaemonForeground(options)
process.exit(status)
} catch (err) {
const message = getErrorMessage(err)
exitWithError(`Failed to start daemon: ${message}`)