/** * Hermes binary probe helper. * * Mirrors the probeClaudeCli pattern from packages/dashboard/src/claude-cli-probe.ts. * Never throws — all failures are captured as `available: false` with a reason. */ import { spawn } from "node:child_process"; /** Default probe timeout in milliseconds. */ const DEFAULT_PROBE_TIMEOUT_MS = 2000; /** * Probe for the hermes binary. * * Runs ` --version` with a short timeout. Use this from * the dashboard status endpoint to check binary presence without crashing. * * @param opts.binaryPath - Override the binary path (default: "hermes"). * @param opts.timeoutMs - Override probe timeout in ms (default: 2000). */ export async function probeHermesBinary(opts) { const startedAt = Date.now(); const binary = typeof opts?.binaryPath === "string" && opts.binaryPath.trim().length > 0 ? opts.binaryPath.trim() : "hermes"; const timeoutMs = opts?.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS; const resolvedPath = await tryResolveBinaryPath(binary); return new Promise((resolvePromise) => { const finish = (result) => { resolvePromise({ ...result, probeDurationMs: Date.now() - startedAt }); }; let settled = false; const child = spawn(resolvedPath ?? binary, ["--version"], { stdio: ["ignore", "pipe", "pipe"], }); const timer = setTimeout(() => { if (settled) return; settled = true; try { child.kill("SIGKILL"); } catch { // Process already gone. } finish({ available: false, binaryPath: resolvedPath, reason: `Probe timed out after ${timeoutMs}ms`, }); }, timeoutMs); let stdout = ""; let stderr = ""; child.stdout?.on("data", (chunk) => { stdout += chunk.toString("utf-8"); }); child.stderr?.on("data", (chunk) => { stderr += chunk.toString("utf-8"); }); child.on("error", (err) => { if (settled) return; settled = true; clearTimeout(timer); const isNotFound = err.code === "ENOENT"; finish({ available: false, binaryPath: resolvedPath, reason: isNotFound ? `\`${binary}\` not found on PATH` : err.message, }); }); child.on("close", (code) => { if (settled) return; settled = true; clearTimeout(timer); if (code === 0) { finish({ available: true, version: stdout.trim() || undefined, binaryPath: resolvedPath, }); } else { finish({ available: false, binaryPath: resolvedPath, reason: stderr.trim() || `hermes --version exited with code ${String(code)}`, }); } }); }); } /** * Best-effort path resolution via `which` (POSIX) or `where` (Windows). * Returns undefined on failure — the spawn above is the actual authority. */ async function tryResolveBinaryPath(binary) { return new Promise((resolvePromise) => { const which = process.platform === "win32" ? "where" : "which"; const child = spawn(which, [binary], { stdio: ["ignore", "pipe", "ignore"] }); let out = ""; child.stdout?.on("data", (chunk) => { out += chunk.toString("utf-8"); }); child.on("error", () => resolvePromise(undefined)); child.on("close", (code) => { if (code === 0) { const first = out.trim().split(/\r?\n/)[0]; resolvePromise(first?.length ? first : undefined); } else { resolvePromise(undefined); } }); }); } //# sourceMappingURL=probe.js.map