Skip to content

Commit df6bb59

Browse files
committed
Merge branch 'alert-autofix-11'
2 parents 8314d99 + b1a02e3 commit df6bb59

2 files changed

Lines changed: 79 additions & 4 deletions

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@
8080
"react": "^19.2.4",
8181
"ripgrep-node": "^1.0.0",
8282
"tiktoken": "^1.0.22",
83-
"ws": "^8.19.0"
83+
"ws": "^8.19.0",
84+
"shell-quote": "^1.8.3"
8485
},
8586
"devDependencies": {
8687
"@types/fs-extra": "^11.0.4",

src/tools/bash.ts

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,88 @@
11
import { ConfirmationService } from "../utils/confirmation-service";
22
import { ToolResult } from "../types/index";
3-
import { exec } from "child_process";
3+
import { spawn } from "child_process";
44
import { promisify } from "util";
5+
import * as shellQuote from "shell-quote";
56

6-
const execAsync = promisify(exec);
7+
const promisifyFn = promisify; // kept in case other parts of this file evolve to use promisify
78

89
export class BashTool {
910
private currentDirectory: string = process.cwd();
1011
private confirmationService = ConfirmationService.getInstance();
1112

13+
private async runCommandSafely(
14+
command: string,
15+
options: { cwd?: string; timeout?: number; maxBuffer?: number },
16+
): Promise<{ stdout: string; stderr: string }> {
17+
const parsed = shellQuote.parse(command);
18+
const argv = parsed
19+
.filter(token => typeof token === "string")
20+
.map(token => token as string);
21+
22+
if (argv.length === 0) {
23+
return { stdout: "", stderr: "" };
24+
}
25+
26+
const cmd = argv[0];
27+
const args = argv.slice(1);
28+
29+
return await new Promise((resolve, reject) => {
30+
const child = spawn(cmd, args, {
31+
cwd: options.cwd,
32+
shell: false,
33+
});
34+
35+
let stdout = "";
36+
let stderr = "";
37+
const maxBuffer = options.maxBuffer ?? 1024 * 1024;
38+
const timeout = options.timeout ?? 30000;
39+
40+
let killedForTimeout = false;
41+
let killedForBuffer = false;
42+
43+
const timer = setTimeout(() => {
44+
killedForTimeout = true;
45+
child.kill();
46+
}, timeout);
47+
48+
child.stdout?.on("data", (data: Buffer | string) => {
49+
const chunk = data.toString();
50+
if (stdout.length + chunk.length > maxBuffer) {
51+
killedForBuffer = true;
52+
child.kill();
53+
return;
54+
}
55+
stdout += chunk;
56+
});
57+
58+
child.stderr?.on("data", (data: Buffer | string) => {
59+
const chunk = data.toString();
60+
if (stderr.length + chunk.length > maxBuffer) {
61+
killedForBuffer = true;
62+
child.kill();
63+
return;
64+
}
65+
stderr += chunk;
66+
});
67+
68+
child.on("error", error => {
69+
clearTimeout(timer);
70+
reject(error);
71+
});
72+
73+
child.on("close", code => {
74+
clearTimeout(timer);
75+
if (killedForTimeout) {
76+
return reject(new Error("Command timed out"));
77+
}
78+
if (killedForBuffer) {
79+
return reject(new Error("Command output exceeded buffer limit"));
80+
}
81+
resolve({ stdout, stderr });
82+
});
83+
});
84+
}
85+
1286
async execute(command: string, timeout: number = 30000): Promise<ToolResult> {
1387
try {
1488
// Check if user has already accepted bash commands for this session
@@ -53,7 +127,7 @@ export class BashTool {
53127
}
54128
}
55129

56-
const { stdout, stderr } = await execAsync(command, {
130+
const { stdout, stderr } = await this.runCommandSafely(command, {
57131
cwd: this.currentDirectory,
58132
timeout,
59133
maxBuffer: 1024 * 1024,

0 commit comments

Comments
 (0)