import { spawn } from "node:child_process"; import { materializeWindowsSpawnProgram, resolveWindowsSpawnProgram, } from "openclaw/plugin-sdk/windows-spawn"; export type CliSpawnInvocation = { command: string; argv: string[]; shell?: boolean; windowsHide?: boolean; }; export function resolveCliSpawnInvocation(params: { command: string; args: string[]; env: NodeJS.ProcessEnv; packageName: string; }): CliSpawnInvocation { const program = resolveWindowsSpawnProgram({ command: params.command, platform: process.platform, env: params.env, execPath: process.execPath, packageName: params.packageName, allowShellFallback: false, }); return materializeWindowsSpawnProgram(program, params.args); } export async function runCliCommand(params: { commandSummary: string; spawnInvocation: CliSpawnInvocation; env: NodeJS.ProcessEnv; cwd: string; timeoutMs?: number; maxOutputChars: number; discardStdout?: boolean; }): Promise<{ stdout: string; stderr: string }> { return await new Promise((resolve, reject) => { const child = spawn(params.spawnInvocation.command, params.spawnInvocation.argv, { env: params.env, cwd: params.cwd, shell: params.spawnInvocation.shell, windowsHide: params.spawnInvocation.windowsHide, }); let stdout = ""; let stderr = ""; let stdoutTruncated = false; let stderrTruncated = false; const discardStdout = params.discardStdout === true; const timer = params.timeoutMs ? setTimeout(() => { child.kill("SIGKILL"); reject(new Error(`${params.commandSummary} timed out after ${params.timeoutMs}ms`)); }, params.timeoutMs) : null; child.stdout.on("data", (data) => { if (discardStdout) { return; } const next = appendOutputWithCap(stdout, data.toString("utf8"), params.maxOutputChars); stdout = next.text; stdoutTruncated = stdoutTruncated || next.truncated; }); child.stderr.on("data", (data) => { const next = appendOutputWithCap(stderr, data.toString("utf8"), params.maxOutputChars); stderr = next.text; stderrTruncated = stderrTruncated || next.truncated; }); child.on("error", (err) => { if (timer) { clearTimeout(timer); } reject(err); }); child.on("close", (code) => { if (timer) { clearTimeout(timer); } if (!discardStdout && (stdoutTruncated || stderrTruncated)) { reject( new Error( `${params.commandSummary} produced too much output (limit ${params.maxOutputChars} chars)`, ), ); return; } if (code === 0) { resolve({ stdout, stderr }); } else { reject(new Error(`${params.commandSummary} failed (code ${code}): ${stderr || stdout}`)); } }); }); } function appendOutputWithCap( current: string, chunk: string, maxChars: number, ): { text: string; truncated: boolean } { const appended = current + chunk; if (appended.length <= maxChars) { return { text: appended, truncated: false }; } return { text: appended.slice(-maxChars), truncated: true }; }