mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-27 20:29:44 +02:00
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:
parent
fffa34bf4e
commit
e6c5a13d1b
3 changed files with 116 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
65
apps/x/packages/core/src/code-mode/acp/codex-exec.ts
Normal file
65
apps/x/packages/core/src/code-mode/acp/codex-exec.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue