/** * Cross-platform HAPI CLI spawning utility * * ## Background * * HAPI CLI runs in two modes: * 1. **Compiled binary**: A single executable built with `bun build --compile` * 2. **Development mode**: Running TypeScript directly via `bun` * * ## Execution Modes * * **Compiled Binary (Production):** * - The executable is self-contained and runs directly * - `process.execPath` points to the compiled binary itself * - No additional entrypoint needed - just pass args to `process.execPath` * * **Development Mode:** * - Running via `bun src/index.ts` * - Spawn child processes using the same runtime with `src/index.ts` entrypoint * * ## Cross-Platform Support * * This utility handles spawning HAPI CLI subprocesses (for daemon processes) * in a cross-platform way, detecting the current runtime mode and using * the appropriate command and arguments. */ import { spawn, SpawnOptions, type ChildProcess } from 'child_process'; import { join } from 'node:path'; import { isBunCompiled, projectPath } from '@/projectPath'; import { logger } from '@/ui/logger'; import { existsSync } from 'node:fs'; /** * Resolve the TypeScript entrypoint for development mode. */ function resolveEntrypoint(projectRoot: string): string { const srcEntrypoint = join(projectRoot, 'src', 'index.ts'); if (existsSync(srcEntrypoint)) { return srcEntrypoint; } throw new Error('No CLI entrypoint found (expected src/index.ts)'); } export interface HappyCliCommand { command: string; args: string[]; } export function getHappyCliCommand(args: string[]): HappyCliCommand { // Compiled binary mode: just use the executable directly if (isBunCompiled()) { return { command: process.execPath, args }; } // Development mode: spawn with TypeScript entrypoint const projectRoot = projectPath(); const entrypoint = resolveEntrypoint(projectRoot); const isBunRuntime = Boolean((process.versions as Record).bun); if (isBunRuntime) { // Bun can run TypeScript directly return { command: process.execPath, args: [entrypoint, ...args] }; } // Node.js fallback: preserve execArgv (for compatibility) return { command: process.execPath, args: [...process.execArgv, entrypoint, ...args] }; } export function spawnHappyCLI(args: string[], options: SpawnOptions = {}): ChildProcess { let directory: string | URL | undefined; if ('cwd' in options) { directory = options.cwd } else { directory = process.cwd() } // Note: We're executing the current runtime with the calculated entrypoint path below, // bypassing the 'hapi' wrapper that would normally be found in the shell's PATH. // However, we log it as 'hapi' here because other engineers are typically looking // for when "hapi" was started and don't care about the underlying node process // details and flags we use to achieve the same result. const fullCommand = `hapi ${args.join(' ')}`; logger.debug(`[SPAWN HAPI CLI] Spawning: ${fullCommand} in ${directory}`); const { command: spawnCommand, args: spawnArgs } = getHappyCliCommand(args); // Sanity check that the entrypoint path exists if (!isBunCompiled()) { const entrypoint = spawnArgs.find((arg) => arg.endsWith('index.ts')); if (entrypoint && !existsSync(entrypoint)) { const errorMessage = `Entrypoint ${entrypoint} does not exist`; logger.debug(`[SPAWN HAPI CLI] ${errorMessage}`); throw new Error(errorMessage); } } return spawn(spawnCommand, spawnArgs, options); }