(args: Record<string, unknown>, context: ToolContext)
| 7 | const MAX_OUTPUT_CHARS = 30_000 |
| 8 | |
| 9 | export async function executeCommand(args: Record<string, unknown>, context: ToolContext): Promise<ToolResult> { |
| 10 | const command = String(args.command ?? "").trim() |
| 11 | if (!command) { |
| 12 | return { text: "FAILED: command is empty", isError: true } |
| 13 | } |
| 14 | const cwd = args.cwd ? resolveWorkspacePath(context.cwd, String(args.cwd)) : context.cwd |
| 15 | |
| 16 | return new Promise<ToolResult>((resolve) => { |
| 17 | const child = spawn(getShell(), getShellRunArgs(command), { |
| 18 | cwd, |
| 19 | env: { ...process.env, TERM: "dumb" }, |
| 20 | stdio: ["ignore", "pipe", "pipe"], |
| 21 | // cmd.exe parses the command string itself; pre-quoting would corrupt it. |
| 22 | windowsVerbatimArguments: process.platform === "win32", |
| 23 | }) |
| 24 | |
| 25 | let output = "" |
| 26 | let truncated = false |
| 27 | const append = (data: Buffer) => { |
| 28 | if (output.length < MAX_OUTPUT_CHARS) { |
| 29 | output += data.toString() |
| 30 | if (output.length >= MAX_OUTPUT_CHARS) { |
| 31 | output = output.slice(0, MAX_OUTPUT_CHARS) |
| 32 | truncated = true |
| 33 | } |
| 34 | } else { |
| 35 | truncated = true |
| 36 | } |
| 37 | } |
| 38 | child.stdout.on("data", append) |
| 39 | child.stderr.on("data", append) |
| 40 | |
| 41 | const timer = setTimeout(() => { |
| 42 | child.kill("SIGTERM") |
| 43 | setTimeout(() => child.kill("SIGKILL"), 3000) |
| 44 | }, COMMAND_TIMEOUT_MS) |
| 45 | |
| 46 | child.on("error", (error) => { |
| 47 | clearTimeout(timer) |
| 48 | resolve({ text: `FAILED to start command: ${error.message}`, isError: true }) |
| 49 | }) |
| 50 | |
| 51 | child.on("close", (code, signal) => { |
| 52 | clearTimeout(timer) |
| 53 | let text = output.trim() || "(no output)" |
| 54 | if (truncated) text += `\n\n(Output truncated at ${MAX_OUTPUT_CHARS} characters.)` |
| 55 | if (signal) { |
| 56 | text += `\n\nCommand terminated by signal ${signal} (timeout is ${COMMAND_TIMEOUT_MS / 1000}s).` |
| 57 | } else { |
| 58 | text += `\n\nExit code: ${code}` |
| 59 | } |
| 60 | resolve({ text, isError: code !== 0 && code !== null }) |
| 61 | }) |
| 62 | }) |
| 63 | } |
nothing calls this directly
no test coverage detected