/** * Search `PATH` for the executable `name`, returning its absolute path or null. * An argument that already looks like a path is checked as-is. Pure over * (name, env, exists) — inject `exists` in tests. */ export declare function resolveBin(name: string, env?: NodeJS.ProcessEnv, exists?: (p: string) => boolean): string | null; /** Bytes read to classify a launcher. The whole placeholder is 500 B. */ export declare const BIN_HEAD_BYTES = 512; export type BinKind = "usable" | "stub" | "missing"; /** What a cheap look at a launcher file yields: its size and its first bytes. */ export interface BinSample { size: number; head: string; } /** * Tell the npm placeholder from something we can actually run. Pure. * * Deliberately CONSERVATIVE: it condemns only what it positively recognises as * the placeholder, and calls everything else usable. The tempting rule — "a * real claude is an ELF/Mach-O of hundreds of MB, so anything else is broken" — * would also condemn every legitimate small launcher: pnpm's shell shim, * volta/asdf/mise shims, npm's `.cmd` shim on Windows, a user's own wrapper. * Those work fine, and refusing to spawn them would be a worse bug than the one * this fixes. Same rule as invariant 27: assert only what you observed. * * No execution probe. Running `claude --version` before each spawn would cost a * process launch per session start, and it would not even be sound: the answer * is stale the moment it returns, and the window this guards against is * milliseconds wide. A stat plus a 512-byte read costs microseconds and is * exactly as (un)raceable, so it buys the safety at none of the price. */ export declare function classifyBin(sample: BinSample | null): BinKind; /** Read the first {@link BIN_HEAD_BYTES} of a file, or null if it isn't there. */ export declare function sampleBin(p: string): BinSample | null; /** * The optional dependency holding the native binary for a platform, mirroring * the PLATFORMS map of `install.cjs`. Returns null for a platform the package * does not publish — the caller then has no fallback, which is the truth. */ export declare function platformPkg(platform: string, arch: string, musl?: boolean): { pkg: string; bin: string; } | null; /** * Where the native binary may sit, given the REAL path of the launcher * (`.../@anthropic-ai/claude-code/bin/claude.exe`). Pure: it only builds * candidates, the caller decides which one exists. * * The `node_modules` walk is not decoration. npm hoists an optional dependency * to the top level as readily as it nests it, and the layout differs between a * global install, `npx`, and the managed `~/.shadok-ai/app`. Assuming the nested * layout is precisely the mistake that made `node-pty-fix.ts`'s chmod silently * miss on every non-dev install (see CLAUDE.md) — so we reproduce node's own * resolution instead: every ancestor directory, each with `node_modules/`. */ export declare function nativeBinCandidates(launcherRealPath: string, pkg: string, bin: string): string[]; export type ClaudeBin = { ok: true; path: string; via: "launcher" | "native"; } | { ok: false; reason: "missing" | "stub"; }; export interface FindClaudeDeps { /** Find the launcher now (typically resolveBin("claude")). */ resolve: () => string | null; /** Follow symlinks (typically fs.realpathSync); may throw, caller guards. */ realpath: (p: string) => string; /** Cheap look at a file (typically sampleBin). */ sample: (p: string) => BinSample | null; platform: string; arch: string; musl: boolean; } /** * Resolve a claude we can actually run: the launcher when it is real, else the * native binary the launcher is only a placeholder for. Pure over injected * deps, so the whole decision is unit-tested without touching the real FS. */ export declare function findClaudeBin(deps: FindClaudeDeps): ClaudeBin; export interface RetryOptions { /** Total attempts, including the first. */ tries?: number; delayMs?: number; sleep?: (ms: number) => Promise; } /** * {@link findClaudeBin}, retried briefly. The failure this guards is a * TRANSIENT window in someone else's postinstall (unlink → relink), so a hard * refusal on the first look would report a healthy install as broken. Bounded * on purpose — a couple of seconds at worst, and only on the spawn path, never * at boot (same rule as `ensureTmux` / `ensureSshIdentity`: nothing here may * hold the server back from serving). */ export declare function findClaudeBinWithRetry(deps: FindClaudeDeps, opts?: RetryOptions): Promise; /** * Is this the kernel refusing to run a binary that is being rewritten? * * ETXTBSY — "text file busy", where "text" is the old Unix word for a * program's CODE segment, nothing to do with text files. The kernel refuses * both directions: writing a file that is executing, and executing a file that * is being written. It is the second that reaches us. * * Every `@anthropic-ai/claude-code` upgrade rewrites a ~214 MB native binary in * place, so the window is SECONDS wide rather than microseconds, and a machine * that follows releases hits it repeatedly — three times in one day here, each * time killing an agent spawn and reporting it as a death. * * This is a DIFFERENT failure from invariant 32, and the difference decides the * cure. There the file is the wrong thing (the npm placeholder) and reinstalling * makes it worse, so we say what is wrong and stop. Here the file is exactly * right and merely busy — the condition is transient by construction, resolves * on its own, and the only possible mistake is concluding too early. * * `findClaudeBinWithRetry` cannot help: it retries a STAT, and a busy binary * stats perfectly. You only learn at `execve`. */ export declare function isBinaryBusyError(e: unknown): boolean; /** Marks a spawn that failed because the pane's command never got to run. */ export declare function binaryBusyError(what: string): Error; export declare const BINARY_BUSY_TRIES = 6; export declare const BINARY_BUSY_DELAY_MS = 500; /** * Runs a pilot's synchronous `start()`, waiting out a binary that is mid-upgrade. * * The retry lives HERE and not inside `start()` because both pilots' `start()` * are synchronous: waiting there would block the event loop for every other * agent. `attachPilot` is already async, so this is the first place that can * afford to wait at all. * * Only a busy binary is retried. Any other failure is rethrown at once — a * missing binary or a bad path does not get better by being asked six times, * and burying it under three seconds of retries would hide the real message. */ export declare function startWithBusyRetry(start: () => void, opts?: { tries?: number; delayMs?: number; sleep?: (ms: number) => Promise; onRetry?: (attempt: number, tries: number) => void; }): Promise; /** Live deps for {@link findClaudeBin}, reading the real filesystem. */ export declare function liveClaudeDeps(): FindClaudeDeps; /** * The claude to spawn. The resolved binary when we have one — which is NOT * always the launcher, see {@link findClaudeBin} — else the bare name, so a * layout we failed to understand behaves exactly as it did before. * * Re-validated on each call: a cached path rots the moment claude-code * upgrades, and one stat is a rounding error next to the process spawn every * caller is about to pay for. */ export declare function claudeCommand(): string; /** Record a path already resolved (by `ensureClaude`), so we don't look twice. */ export declare function rememberClaudeBin(p: string | null): void; /** The manual-install fallback message, shown when the auto-install can't help. */ export declare function claudeMissingMessage(detail: string): string; /** * Why a spawn was refused when `claude` IS on PATH. The point is to say what is * wrong instead of letting a bare `posix_spawnp failed` — or the placeholder's * own exit 1 — reach the user, in the spirit of `describeStuckScreen`. */ export declare function claudeStubMessage(): string; export interface EnsureClaudeDeps { /** Locate a RUNNABLE claude now. Called again after an install. */ find: () => Promise; /** Perform the global install (typically `npm i -g @anthropic-ai/claude-code`). */ install: () => Promise; /** Surface progress (server log / a line to the client). */ notify: (line: string) => void; } export type EnsureClaudeResult = { ok: true; path: string; } | { ok: false; error: string; }; /** * Make a runnable claude CLI available, installing it ONCE if it is missing. * Returns the path to spawn — which is NOT always the launcher: when the * launcher is the placeholder we hand back the native binary behind it. * Otherwise a clear, actionable error, never the raw `posix_spawnp failed` a * bare spawn would throw, and never the placeholder's opaque exit 1. * * A placeholder is NOT a reason to reinstall: the package is plainly installed, * so `npm i -g` would neither be the missing-CLI case nor a safe move while * someone else's postinstall is mid-rewrite. We say what is wrong instead. */ export declare function ensureClaude(deps: EnsureClaudeDeps): Promise;