/** * Process helpers for harness onboarding: the PATH every harness process gets * and a shell-free command runner. * * PATH: when `npm install -g` hits EACCES, Crewly installs the harness under a * user-owned prefix (`/npm-global`). Its `bin` dir is prepended to * PATH for everything Crewly spawns — status probes, the login broker and agent * sessions — so a harness installed there is found without touching the * user's shell profile. Claude Code's native installer dir (`~/.local/bin`) is * appended when missing, because a backend started by launchd/systemd often * lacks it. * * @module services/harness/harness-exec.utils */ import { spawn } from 'child_process'; import type { RunCommand } from './harness.types.js'; /** * The user-owned npm prefix used when a global install is not permitted. * * @returns Absolute path, e.g. `~/.crewly/npm-global` */ export declare function getUserNpmPrefix(): string; /** * The bin directory of the user-owned npm prefix. * * @returns Absolute path, e.g. `~/.crewly/npm-global/bin` */ export declare function getUserNpmBinDir(): string; /** * Build the PATH value harness processes run with. * * Prepends the user npm prefix bin dir and appends `~/.local/bin`, each only * when not already present. Order of the existing entries is preserved. * * @param currentPath - Existing PATH (may be undefined) * @param homeDir - Home directory (injectable for tests) * @returns The PATH string * * @example * ```ts * buildHarnessPath('/usr/bin', '/home/me'); * // '/home/me/.crewly/npm-global/bin:/usr/bin:/home/me/.local/bin' * ``` */ export declare function buildHarnessPath(currentPath: string | undefined, homeDir?: string): string; /** * PATH for running `npm`: the harness PATH plus the directory of the running * Node binary, where npm lives in nvm / installer layouts. A backend started * by launchd or systemd often has neither on its PATH. * * @param currentPath - Existing PATH * @param homeDir - Home directory (injectable for tests) * @param nodeBinDir - Directory of the Node binary (injectable for tests) * @returns The PATH string */ export declare function buildNpmPath(currentPath: string | undefined, homeDir?: string, nodeBinDir?: string): string; /** * Copy an environment with PATH set to {@link buildHarnessPath}. * * @param env - Source environment (defaults to process.env) * @returns A new environment object */ export declare function withHarnessPath(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv; /** * Find an executable on a PATH, like `which`. * * @param command - Bare command name (or an absolute path, returned when executable) * @param envPath - PATH to search * @param isExecutable - Executable check (injectable for tests) * @returns Absolute path, or null when not found */ export declare function resolveExecutable(command: string, envPath: string | undefined, isExecutable?: (file: string) => boolean): string | null; /** Injectable seams of {@link createRunCommand}. */ export interface RunCommandDeps { spawn?: typeof spawn; now?: () => number; /** Whether a pid still exists (default: `process.kill(pid, 0)`) */ isAlive?: (pid: number) => boolean; /** Receives the rate-limited stuck-executable warning */ warn?: (message: string, data: Record) => void; } /** * Build a command runner with per-executable pile-up protection. * * On macOS a process stuck in exec / Gatekeeper verification survives SIGKILL * until the kernel call returns, so killing a timed-out probe does not free * it, and every new probe of that executable hangs too (hundreds of `codex` * processes). The runner therefore remembers killed-but-still-alive children * per executable and, while any exist, returns `{ code: null, error }` * without spawning. It also caps concurrent timeout-bounded runs per * executable. Callers already treat `code: null` as "could not run". * * @param deps - Injectable seams (tests) * @returns A {@link RunCommand} */ export declare function createRunCommand(deps?: RunCommandDeps): RunCommand; /** * Run a command without a shell and collect its output. * * Never rejects: a spawn failure (ENOENT), timeout, or a refusal because an * earlier run of the same executable is still stuck comes back as * `{ code: null, error }`. The command runs in its own process group and a * timeout kills the whole group: harness CLIs such as `codex` are Node * wrappers around a native binary, and SIGKILL on the wrapper alone left the * native child running forever. See {@link createRunCommand} for the * pile-up guard. Secrets must go through `options.stdin`, never through * `args`, so they cannot appear in `ps` output. * * @param command - Executable * @param args - Arguments * @param options - env, timeout, stdin, streaming output callback * @returns Exit code and output */ export declare const runCommand: RunCommand; //# sourceMappingURL=harness-exec.utils.d.ts.map