fix(code-mode): drive agents from local install, drop bundled engines

Skip the platform-native engine packages (~230 MB each) when staging the ACP adapters and point each adapter at the user's local claude/codex via CLAUDE_CODE_EXECUTABLE / CODEX_PATH, erroring clearly when neither is installed. The codex resolver mirrors claude's login-shell probe so nvm/fnm installs resolve on macOS/Linux. Shrinks each installer ~460 MB.
This commit is contained in:
Gagancreates 2026-06-11 00:43:00 +05:30
parent fffa34bf4e
commit e6c5a13d1b
3 changed files with 116 additions and 11 deletions

View file

@ -3,6 +3,7 @@ import * as path from 'path';
import { fileURLToPath } from 'url';
import type { CodingAgent } from './types.js';
import { resolveClaudeExecutable } from './claude-exec.js';
import { resolveCodexExecutable } from './codex-exec.js';
const require = createRequire(import.meta.url);
@ -63,14 +64,36 @@ export function getAgentLaunchSpec(agent: CodingAgent): AgentLaunchSpec {
const entry = resolveAdapterEntry(ADAPTER_PACKAGE[agent]);
const env: NodeJS.ProcessEnv = { ...process.env };
// Point the Claude adapter at the real claude executable. On Windows this is
// mandatory (Node can't spawn the .cmd shim — EINVAL); on macOS/Linux it's a
// PATH safety net for GUI launches. Resolver is a no-op when claude isn't found,
// leaving the adapter to do its own lookup. (Codex relies on PATH for now — wire
// an equivalent when we add Codex support.)
// Point each adapter at the user's LOCAL agent executable. We intentionally do not
// bundle the agents' native engines (~230 MB each) into packaged builds — the
// adapters fall back to a bundled engine only when these are unset, and we strip
// those binaries during packaging (see apps/main/forge.config.cjs). So a local
// install is required; throw a clear error instead of letting the adapter fail
// cryptically on the absent bundled engine.
if (agent === 'claude' && !env.CLAUDE_CODE_EXECUTABLE) {
// On Windows resolving the real .exe is also mandatory: Node can't spawn the
// .cmd shim (EINVAL). On macOS/Linux it doubles as a PATH safety net for GUI
// launches that don't inherit the login shell's PATH.
const exe = resolveClaudeExecutable();
if (exe) env.CLAUDE_CODE_EXECUTABLE = exe;
if (!exe) {
throw new Error(
'Claude Code CLI not found. Install it (`npm i -g @anthropic-ai/claude-code`) to use Claude in code mode.',
);
}
env.CLAUDE_CODE_EXECUTABLE = exe;
}
if (agent === 'codex' && !env.CODEX_PATH) {
// codex-acp spawns this with shell:true on Windows (a .cmd shim is fine) and
// via PATH on unix. Without CODEX_PATH the adapter tries its bundled engine,
// which we don't ship — so resolve the local install or fail clearly.
const exe = resolveCodexExecutable();
if (!exe) {
throw new Error(
'Codex CLI not found. Install it (`npm i -g @openai/codex`) to use Codex in code mode.',
);
}
env.CODEX_PATH = exe;
}
// We spawn the adapter with process.execPath. Inside Electron's main process

View file

@ -0,0 +1,65 @@
import { execSync } from 'child_process';
import * as path from 'path';
import { existsSync } from 'fs';
import { commonInstallPaths } from '../status.js';
let cached: string | undefined;
// Resolve the user's local `codex` launcher to hand the codex-acp adapter via
// CODEX_PATH. We deliberately do NOT bundle Codex's ~230 MB native engine — the
// adapter only falls back to a bundled `@openai/codex` when CODEX_PATH is unset, so
// pointing it at the local install keeps packaged builds small.
//
// Unlike claude (which the adapter spawns directly, hitting the Windows .cmd EINVAL
// trap), codex-acp spawns this with `shell: true` on Windows and via PATH on unix — so
// a `.cmd` shim is fine and we don't need to dig out a raw `.exe`. We still resolve an
// explicit path because Electron's runtime PATH can omit npm/pnpm global bin dirs even
// when the user's shell has them. Returns undefined if codex can't be found — callers
// then surface a clear "Codex CLI not found" error.
export function resolveCodexExecutable(): string | undefined {
if (cached) return cached;
const resolved = process.platform === 'win32' ? resolveCodexOnWindows() : resolveCodexOnUnix();
if (resolved) cached = resolved;
return resolved;
}
// Windows: scan PATH (for codex.cmd/.exe) plus well-known npm/pnpm global bin dirs that
// Electron's runtime PATH can omit. No login-shell trick here; codex-acp spawns with
// shell:true so a `.cmd` shim is fine.
function resolveCodexOnWindows(): string | undefined {
const exts = ['.cmd', '.exe', ''];
const pathDirs = (process.env.PATH ?? '')
.split(path.delimiter)
.map((d) => d.trim())
.filter(Boolean);
const fromPath = pathDirs.flatMap((dir) => exts.map((ext) => path.join(dir, `codex${ext}`)));
for (const candidate of [...fromPath, ...commonInstallPaths('codex')]) {
if (existsSync(candidate)) return candidate;
}
return undefined;
}
// macOS/Linux: GUI-launched Electron apps (Dock/Finder) often don't inherit the login
// shell's PATH, so a node-version-manager install (nvm/fnm/asdf) won't be on
// process.env.PATH. Ask a login shell first — it sees the user's full PATH — then fall
// back to scanning the runtime PATH and well-known install dirs. Mirrors
// resolveClaudeBinaryUnix in claude-exec.ts.
function resolveCodexOnUnix(): string | undefined {
// Primary: a login shell sees the user's full PATH (~/.zprofile, nvm, homebrew, …).
try {
const out = execSync("/bin/sh -lc 'command -v codex'", { timeout: 5000, encoding: 'utf-8' }).trim();
if (out && existsSync(out)) return out;
} catch {
// not found on the login-shell PATH
}
// Fallback: scan the runtime PATH and well-known install locations directly.
const pathDirs = (process.env.PATH ?? '')
.split(path.delimiter)
.map((d) => d.trim())
.filter(Boolean);
const fromPath = pathDirs.map((dir) => path.join(dir, 'codex'));
for (const candidate of [...fromPath, ...commonInstallPaths('codex')]) {
if (existsSync(candidate)) return candidate;
}
return undefined;
}