fix: route Claude CLI through cross-platform runner
This commit is contained in:
parent
3ccd535ead
commit
14d198377f
|
|
@ -2,32 +2,17 @@
|
||||||
* Claude CLI Caller — shells out to the local Claude CLI with the prompt on stdin.
|
* Claude CLI Caller — shells out to the local Claude CLI with the prompt on stdin.
|
||||||
* Uses Claude Code desktop auth; no API key required.
|
* 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<string> {
|
export async function callClaudeCli(model: string, prompt: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
if (!/^[a-zA-Z0-9._-]+$/.test(model)) {
|
||||||
// ponytail: shell is for Windows .cmd shim; model is guarded before interpolation.
|
throw new Error(`callClaudeCli: unsafe model id "${model}"`);
|
||||||
if (!/^[a-zA-Z0-9._-]+$/.test(model)) {
|
}
|
||||||
reject(new Error(`callClaudeCli: unsafe model id "${model}"`));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const child = spawn(`claude --print --model ${model}`, {
|
const result = await runCli("claude", ["--print", "--model", model], 120_000, {
|
||||||
shell: true,
|
stdin: prompt,
|
||||||
timeout: 120_000,
|
windowsCmdShim: true,
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
if (result.startsWith("[CLI ")) throw new Error(result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,11 @@ describe("runCli", () => {
|
||||||
const out = await runCli(process.execPath, ["-e", "console.log('bad'); process.exit(7)"], 10_000);
|
const out = await runCli(process.execPath, ["-e", "console.log('bad'); process.exit(7)"], 10_000);
|
||||||
expect(out).toContain("CLI exited 7");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,25 @@ import { spawn } from "node:child_process";
|
||||||
|
|
||||||
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
function isCmdShim(bin: string): boolean {
|
export interface RunCliOptions {
|
||||||
return process.platform === "win32" && /(?:\.cmd|\.bat)$/i.test(bin);
|
stdin?: string;
|
||||||
|
windowsCmdShim?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function runCli(bin: string, args: string[], timeoutMs = 120_000): Promise<string> {
|
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<string> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
// ponytail: only .cmd/.bat need cmd.exe; real .exe paths keep true argv semantics.
|
// ponytail: only Windows command shims need cmd.exe; real .exe paths keep argv semantics.
|
||||||
const command = isCmdShim(bin) ? process.env.ComSpec ?? "cmd.exe" : bin;
|
const command = isCmdShim(bin, options.windowsCmdShim) ? process.env.ComSpec ?? "cmd.exe" : bin;
|
||||||
const commandArgs = isCmdShim(bin) ? ["/d", "/s", "/c", bin, ...args] : args;
|
const commandArgs = isCmdShim(bin, options.windowsCmdShim) ? ["/d", "/s", "/c", bin, ...args] : args;
|
||||||
const child = spawn(command, commandArgs, {
|
const child = spawn(command, commandArgs, {
|
||||||
shell: false,
|
shell: false,
|
||||||
timeout: timeoutMs,
|
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"}]`);
|
resolve(`[CLI exited ${code ?? "unknown"}: ${(stderr || stdout).slice(0, 400) || "no output"}]`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (options.stdin !== undefined) child.stdin.end(options.stdin);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue