From 14d198377f12e75b96fae8ef89ac5cb0bd0c97a1 Mon Sep 17 00:00:00 2001 From: artale Date: Mon, 15 Jun 2026 04:06:33 +0200 Subject: [PATCH] fix: route Claude CLI through cross-platform runner --- src/core/claude-cli-caller.ts | 33 +++++++++------------------------ src/core/cli-runner.test.ts | 7 +++++++ src/core/cli-runner.ts | 24 ++++++++++++++++++------ 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/core/claude-cli-caller.ts b/src/core/claude-cli-caller.ts index c5a22ef..d3195d4 100644 --- a/src/core/claude-cli-caller.ts +++ b/src/core/claude-cli-caller.ts @@ -2,32 +2,17 @@ * Claude CLI Caller — shells out to the local Claude CLI with the prompt on stdin. * Uses Claude Code desktop auth; no API key required. */ -import { spawn } from "node:child_process"; +import { runCli } from "./cli-runner.js"; export async function callClaudeCli(model: string, prompt: string): Promise { - return new Promise((resolve, reject) => { - // ponytail: shell is for Windows .cmd shim; model is guarded before interpolation. - if (!/^[a-zA-Z0-9._-]+$/.test(model)) { - reject(new Error(`callClaudeCli: unsafe model id "${model}"`)); - return; - } + if (!/^[a-zA-Z0-9._-]+$/.test(model)) { + throw new Error(`callClaudeCli: unsafe model id "${model}"`); + } - const child = spawn(`claude --print --model ${model}`, { - shell: true, - timeout: 120_000, - }); - - let stdout = ""; - let stderr = ""; - child.stdout.on("data", (d) => { stdout += d.toString(); }); - child.stderr.on("data", (d) => { stderr += d.toString(); }); - child.on("error", (err) => reject(new Error(`claude CLI spawn failed: ${err.message}`))); - child.on("close", (code) => { - if (code === 0) resolve(stdout.trim()); - else reject(new Error(`claude CLI exited with code ${code}${stderr ? ` | stderr: ${stderr.slice(0, 400)}` : ""}`)); - }); - - child.stdin.write(prompt); - child.stdin.end(); + const result = await runCli("claude", ["--print", "--model", model], 120_000, { + stdin: prompt, + windowsCmdShim: true, }); + if (result.startsWith("[CLI ")) throw new Error(result); + return result; } diff --git a/src/core/cli-runner.test.ts b/src/core/cli-runner.test.ts index 46d87d1..a14cb37 100644 --- a/src/core/cli-runner.test.ts +++ b/src/core/cli-runner.test.ts @@ -11,4 +11,11 @@ describe("runCli", () => { const out = await runCli(process.execPath, ["-e", "console.log('bad'); process.exit(7)"], 10_000); expect(out).toContain("CLI exited 7"); }); + + it("pipes stdin", async () => { + const out = await runCli(process.execPath, ["-e", "process.stdin.pipe(process.stdout)"], 10_000, { + stdin: "hello stdin", + }); + expect(out).toBe("hello stdin"); + }); }); diff --git a/src/core/cli-runner.ts b/src/core/cli-runner.ts index d656bb0..de1dab6 100644 --- a/src/core/cli-runner.ts +++ b/src/core/cli-runner.ts @@ -2,15 +2,25 @@ import { spawn } from "node:child_process"; const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; -function isCmdShim(bin: string): boolean { - return process.platform === "win32" && /(?:\.cmd|\.bat)$/i.test(bin); +export interface RunCliOptions { + stdin?: string; + windowsCmdShim?: boolean; } -export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promise { +function isCmdShim(bin: string, force = false): boolean { + return process.platform === "win32" && (force || /(?:\.cmd|\.bat)$/i.test(bin)); +} + +export function runCli( + bin: string, + args: string[], + timeoutMs = 120_000, + options: RunCliOptions = {}, +): Promise { return new Promise((resolve) => { - // ponytail: only .cmd/.bat need cmd.exe; real .exe paths keep true argv semantics. - const command = isCmdShim(bin) ? process.env.ComSpec ?? "cmd.exe" : bin; - const commandArgs = isCmdShim(bin) ? ["/d", "/s", "/c", bin, ...args] : args; + // ponytail: only Windows command shims need cmd.exe; real .exe paths keep argv semantics. + const command = isCmdShim(bin, options.windowsCmdShim) ? process.env.ComSpec ?? "cmd.exe" : bin; + const commandArgs = isCmdShim(bin, options.windowsCmdShim) ? ["/d", "/s", "/c", bin, ...args] : args; const child = spawn(command, commandArgs, { shell: false, timeout: timeoutMs, @@ -44,5 +54,7 @@ export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promis } resolve(`[CLI exited ${code ?? "unknown"}: ${(stderr || stdout).slice(0, 400) || "no output"}]`); }); + + if (options.stdin !== undefined) child.stdin.end(options.stdin); }); }