/** * Cross-platform Happy CLI spawning utility * * ## Background * * We built a command-line JavaScript program with the entrypoint at `dist/index.mjs`. * This needs to be run with `node`, but we want to hide deprecation warnings and other * noise from end users by passing specific flags: `--no-warnings --no-deprecation`. * * Users don't care about these technical details - they just want a clean experience * with no warning output when using Happy. * * ## The Wrapper Strategy * * We created a wrapper script `bin/happy.mjs` with a shebang `#!/usr/bin/env node`. * This allows direct execution on Unix systems and NPM automatically generates * Windows-specific wrapper scripts (`happy.cmd` and `happy.ps1`) when it sees * the `bin` field in package.json pointing to a JavaScript file with a shebang. * * The wrapper script either directly execs `dist/index.mjs` with the flags we want, * or imports it directly if Node.js already has the right flags. * * ## Execution Chains * * **Unix/Linux/macOS:** * 1. User runs `happy` command * 2. Shell directly executes `bin/happy.mjs` (shebang: `#!/usr/bin/env node`) * 3. `bin/happy.mjs` either execs `node --no-warnings --no-deprecation dist/index.mjs` or imports `dist/index.mjs` directly * * **Windows:** * 1. User runs `happy` command * 2. NPM wrapper (`happy.cmd`) calls `node bin/happy.mjs` * 3. `bin/happy.mjs` either execs `node --no-warnings --no-deprecation dist/index.mjs` or imports `dist/index.mjs` directly * * ## The Spawning Problem * * When our code needs to spawn Happy cli as a subprocess (for daemon processes), * we were trying to execute `bin/happy.mjs` directly. This fails on Windows * because Windows doesn't understand shebangs - you get an `EFTYPE` error. * * ## The Solution * * Since we know exactly what needs to happen (run `dist/index.mjs` with specific * Node.js flags), we can bypass all the wrapper layers and do it directly: * * `spawn('node', ['--no-warnings', '--no-deprecation', 'dist/index.mjs', ...args])` * * This works on all platforms and achieves the same result without any of the * middleman steps that were providing workarounds for Windows vs Linux differences. */ import { spawn, SpawnOptions, type ChildProcess } from 'child_process'; import { join } from 'node:path'; import { projectPath } from '@/projectPath'; import { logger } from '@/ui/logger'; import { existsSync } from 'node:fs'; /** * Spawn the Happy CLI with the given arguments in a cross-platform way. * * This function bypasses the wrapper script (bin/happy.mjs) and spawns the * actual CLI entrypoint (dist/index.mjs) directly with Node.js, ensuring * compatibility across all platforms including Windows. * * @param args - Arguments to pass to the Happy CLI * @param options - Spawn options (same as child_process.spawn) * @returns ChildProcess instance */ export function spawnHappyCLI(args: string[], options: SpawnOptions = {}): ChildProcess { const projectRoot = projectPath(); const entrypoint = join(projectRoot, 'dist', 'index.mjs'); let directory: string | URL | undefined; if ('cwd' in options) { directory = options.cwd } else { directory = process.cwd() } // Note: We're actually executing 'node' with the calculated entrypoint path below, // bypassing the 'happy' wrapper that would normally be found in the shell's PATH. // However, we log it as 'happy' here because other engineers are typically looking // for when "happy" 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}`); // Use the same Node.js flags that the wrapper script uses const nodeArgs = [ '--no-warnings', '--no-deprecation', entrypoint, ...args ]; // Sanity check of the entrypoint path exists if (!existsSync(entrypoint)) { const errorMessage = `Entrypoint ${entrypoint} does not exist`; logger.debug(`[SPAWN HAPI CLI] ${errorMessage}`); throw new Error(errorMessage); } return spawn('node', nodeArgs, options); }