From 1f958f44fa2bfbb099fda1639040b236fe89e34c Mon Sep 17 00:00:00 2001 From: Mark L <73659136+liuxiaopai-ai@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:01:24 +0800 Subject: [PATCH] fix(cli): report binary startup failures with actionable diagnostics (#220) --- cli/bin/hapi.cjs | 110 +++++++++++++++++++++++++++--------- cli/src/bin/hapiBin.test.ts | 64 +++++++++++++++++++++ 2 files changed, 147 insertions(+), 27 deletions(-) create mode 100644 cli/src/bin/hapiBin.test.ts diff --git a/cli/bin/hapi.cjs b/cli/bin/hapi.cjs index e3738e4f..d18e4218 100755 --- a/cli/bin/hapi.cjs +++ b/cli/bin/hapi.cjs @@ -5,44 +5,100 @@ const path = require('path'); const platform = process.platform; const arch = process.arch; -const pkgName = `@twsxtd/hapi-${platform}-${arch}`; -function getBinaryPath() { +function getBinaryPath(platformName = platform, archName = arch) { + const pkgName = `@twsxtd/hapi-${platformName}-${archName}`; + try { // Try to find the platform-specific package const pkgPath = require.resolve(`${pkgName}/package.json`); - const binName = platform === 'win32' ? 'hapi.exe' : 'hapi'; + const binName = platformName === 'win32' ? 'hapi.exe' : 'hapi'; return path.join(path.dirname(pkgPath), 'bin', binName); } catch (e) { return null; } } -const binPath = getBinaryPath(); - -if (!binPath) { - console.error(`Unsupported platform: ${platform}-${arch}`); - console.error(''); - console.error('Supported platforms:'); - console.error(' - darwin-arm64 (macOS Apple Silicon)'); - console.error(' - darwin-x64 (macOS Intel)'); - console.error(' - linux-arm64'); - console.error(' - linux-x64'); - console.error(' - win32-x64'); - console.error(''); - console.error('You can download the binary manually from:'); - console.error(' https://github.com/tiann/hapi/releases'); - process.exit(1); +function formatCommand(binPath, args) { + return [binPath, ...args].map((arg) => JSON.stringify(String(arg))).join(' '); } -try { - execFileSync(binPath, process.argv.slice(2), { stdio: 'inherit' }); -} catch (e) { - // If the binary execution fails, exit with the same code - if (e.status !== undefined) { - process.exit(e.status); +function normalizeExecError(error) { + return { + status: typeof error?.status === 'number' ? error.status : null, + signal: typeof error?.signal === 'string' ? error.signal : null, + message: error?.message ? String(error.message) : null, + }; +} + +function reportExecutionFailure(error, binPath, args, log = console.error) { + const { status, signal, message } = normalizeExecError(error); + + log(`Failed to execute: ${formatCommand(binPath, args)}`); + + if (signal) { + log(`Binary terminated by signal ${signal}.`); } - // For other errors (e.g., binary not found), print and exit - console.error(`Failed to execute ${binPath}:`, e.message); - process.exit(1); + + if (status !== null) { + log(`Binary exited with status ${status}.`); + } + + if (message) { + log(message); + } + + return { status, signal }; } + +function main() { + const binPath = getBinaryPath(); + + if (!binPath) { + console.error(`Unsupported platform: ${platform}-${arch}`); + console.error(''); + console.error('Supported platforms:'); + console.error(' - darwin-arm64 (macOS Apple Silicon)'); + console.error(' - darwin-x64 (macOS Intel)'); + console.error(' - linux-arm64'); + console.error(' - linux-x64'); + console.error(' - win32-x64'); + console.error(''); + console.error('You can download the binary manually from:'); + console.error(' https://github.com/tiann/hapi/releases'); + process.exit(1); + } + + const args = process.argv.slice(2); + + try { + execFileSync(binPath, args, { stdio: 'inherit' }); + } catch (error) { + const { status, signal } = reportExecutionFailure(error, binPath, args); + + if (status !== null) { + process.exit(status); + } + + if (signal) { + try { + process.kill(process.pid, signal); + } catch { + // ignore unsupported/invalid signal names on this platform + } + } + + process.exit(1); + } +} + +if (require.main === module) { + main(); +} + +module.exports = { + formatCommand, + getBinaryPath, + normalizeExecError, + reportExecutionFailure, +}; diff --git a/cli/src/bin/hapiBin.test.ts b/cli/src/bin/hapiBin.test.ts new file mode 100644 index 00000000..9f447974 --- /dev/null +++ b/cli/src/bin/hapiBin.test.ts @@ -0,0 +1,64 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const require = createRequire(import.meta.url); +const binModulePath = path.resolve(process.cwd(), 'bin/hapi.cjs'); +const { formatCommand, normalizeExecError, reportExecutionFailure } = require(binModulePath); + +describe('hapi binary launcher error reporting', () => { + it('formats command with shell-safe JSON quoting', () => { + const command = formatCommand('/tmp/hapi', ['serve', '--name', 'my agent']); + expect(command).toBe('"/tmp/hapi" "serve" "--name" "my agent"'); + }); + + it('normalizes child process execution errors', () => { + const normalized = normalizeExecError({ + status: 132, + signal: 'SIGILL', + message: 'Command failed: /tmp/hapi', + }); + + expect(normalized).toEqual({ + status: 132, + signal: 'SIGILL', + message: 'Command failed: /tmp/hapi', + }); + }); + + it('reports execution details before exit handling', () => { + const lines: string[] = []; + const log = (line: string) => lines.push(line); + + const result = reportExecutionFailure( + { + status: 132, + signal: 'SIGILL', + message: 'Illegal instruction (core dumped)', + }, + '/tmp/hapi', + ['serve', '--port', '3000'], + log, + ); + + expect(result).toEqual({ status: 132, signal: 'SIGILL' }); + expect(lines).toEqual([ + 'Failed to execute: "/tmp/hapi" "serve" "--port" "3000"', + 'Binary terminated by signal SIGILL.', + 'Binary exited with status 132.', + 'Illegal instruction (core dumped)', + ]); + }); + + it('handles unknown failures with generic output', () => { + const lines: string[] = []; + + const result = reportExecutionFailure({}, '/tmp/hapi', [], (line: string) => { + lines.push(line); + }); + + expect(result).toEqual({ status: null, signal: null }); + expect(lines).toEqual(['Failed to execute: "/tmp/hapi"']); + }); +});