mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-22 18:45:19 +02:00
fix: resolve claude.exe for acpx on windows to dodge spawn einval (#554)
This commit is contained in:
parent
47d7100368
commit
e594b667bf
2 changed files with 44 additions and 1 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { z, ZodType } from "zod";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import { createReadStream, existsSync, readFileSync } from "fs";
|
||||
import { createInterface } from "readline";
|
||||
import { execSync } from "child_process";
|
||||
import { glob } from "glob";
|
||||
|
|
@ -67,6 +67,44 @@ const LLMPARSE_MIME_TYPES: Record<string, string> = {
|
|||
'.tiff': 'image/tiff',
|
||||
};
|
||||
|
||||
// Windows-only workaround: the Claude ACP bridge spawns CLAUDE_CODE_EXECUTABLE
|
||||
// without `shell: true`, and Node refuses to spawn .cmd files that way (EINVAL).
|
||||
// When the LLM invokes acpx via executeCommand, pre-resolve claude's real .exe
|
||||
// from the npm-shim layout and inject it via env so the bridge can spawn it.
|
||||
function resolveClaudeExeOnWindows(): string | undefined {
|
||||
const pathDirs = (process.env.PATH ?? '').split(';');
|
||||
for (const dir of pathDirs) {
|
||||
const trimmed = dir.trim();
|
||||
if (!trimmed) continue;
|
||||
const cmdPath = path.join(trimmed, 'claude.cmd');
|
||||
if (!existsSync(cmdPath)) continue;
|
||||
const exeFromLayout = path.join(trimmed, 'node_modules', '@anthropic-ai', 'claude-code', 'bin', 'claude.exe');
|
||||
if (existsSync(exeFromLayout)) return exeFromLayout;
|
||||
try {
|
||||
const content = readFileSync(cmdPath, 'utf-8');
|
||||
const absMatch = content.match(/[A-Z]:[\\/][^\s"]*claude\.exe/i);
|
||||
if (absMatch && existsSync(absMatch[0])) return absMatch[0];
|
||||
const relMatch = content.match(/%~dp0[\\/]?([^\s"%]+claude\.exe)/i);
|
||||
if (relMatch) {
|
||||
const resolved = path.join(trimmed, relMatch[1]);
|
||||
if (existsSync(resolved)) return resolved;
|
||||
}
|
||||
} catch {
|
||||
// ignore shim parse failures
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function envForCommand(command: string): NodeJS.ProcessEnv | undefined {
|
||||
if (process.platform !== 'win32') return undefined;
|
||||
if (!/\bacpx\b/.test(command)) return undefined;
|
||||
if (process.env.CLAUDE_CODE_EXECUTABLE) return undefined;
|
||||
const exe = resolveClaudeExeOnWindows();
|
||||
if (!exe) return undefined;
|
||||
return { ...process.env, CLAUDE_CODE_EXECUTABLE: exe };
|
||||
}
|
||||
|
||||
export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
||||
loadSkill: {
|
||||
description: "Load a Rowboat skill definition into context by fetching its guidance string",
|
||||
|
|
@ -963,11 +1001,14 @@ export const BuiltinTools: z.infer<typeof BuiltinToolsSchema> = {
|
|||
// };
|
||||
// }
|
||||
|
||||
const envOverride = envForCommand(command);
|
||||
|
||||
// Use abortable version when we have a signal
|
||||
if (ctx?.signal) {
|
||||
const { promise, process: proc } = executeCommandAbortable(command, {
|
||||
cwd: workingDir,
|
||||
signal: ctx.signal,
|
||||
env: envOverride,
|
||||
onData: (chunk: string) => {
|
||||
ctx.publish({
|
||||
runId: ctx.runId,
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ export function executeCommandAbortable(
|
|||
maxBuffer?: number;
|
||||
signal?: AbortSignal;
|
||||
onData?: (chunk: string) => void;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
): { promise: Promise<AbortableCommandResult>; process: ChildProcess } {
|
||||
// Check if already aborted before spawning
|
||||
|
|
@ -166,6 +167,7 @@ export function executeCommandAbortable(
|
|||
const proc = spawn(command, [], {
|
||||
shell,
|
||||
cwd: options?.cwd,
|
||||
env: options?.env,
|
||||
detached: process.platform !== 'win32', // Create process group on Unix
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue