| 74 | } |
| 75 | |
| 76 | private runCommand(cmd: string, ctx: ToolContext, timeoutMs: number): Promise<ToolResult> { |
| 77 | return new Promise(resolve => { |
| 78 | // cross-spawn handles Windows shell quoting and path escaping correctly. |
| 79 | // shell:true lets us run a raw command string with pipes, redirects, etc. |
| 80 | const proc = crossSpawn(cmd, [], { |
| 81 | cwd: ctx.cwd, |
| 82 | env: { ...process.env, FORCE_COLOR: '0' }, |
| 83 | shell: true, |
| 84 | signal: ctx.signal, |
| 85 | }); |
| 86 | |
| 87 | const stdoutChunks: Buffer[] = []; |
| 88 | const stderrChunks: Buffer[] = []; |
| 89 | let stdoutBytes = 0; |
| 90 | let stderrBytes = 0; |
| 91 | let truncated = false; |
| 92 | const timer = setTimeout(() => { |
| 93 | try { proc.kill('SIGTERM'); } catch {} |
| 94 | setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, 2000); |
| 95 | }, timeoutMs); |
| 96 | |
| 97 | proc.stdout?.on('data', (chunk: Buffer) => { |
| 98 | if (stdoutBytes < MAX_OUTPUT_BYTES) { |
| 99 | stdoutChunks.push(chunk); |
| 100 | stdoutBytes += chunk.length; |
| 101 | } else { |
| 102 | truncated = true; |
| 103 | } |
| 104 | // Stream lines to UI |
| 105 | const text = chunk.toString('utf-8'); |
| 106 | for (const line of text.split('\n')) { |
| 107 | if (line) ctx.emit({ type: 'shell-stdout', line }); |
| 108 | } |
| 109 | }); |
| 110 | |
| 111 | proc.stderr?.on('data', (chunk: Buffer) => { |
| 112 | if (stderrBytes < MAX_OUTPUT_BYTES) { |
| 113 | stderrChunks.push(chunk); |
| 114 | stderrBytes += chunk.length; |
| 115 | } else { |
| 116 | truncated = true; |
| 117 | } |
| 118 | const text = chunk.toString('utf-8'); |
| 119 | for (const line of text.split('\n')) { |
| 120 | if (line) ctx.emit({ type: 'shell-stderr', line }); |
| 121 | } |
| 122 | }); |
| 123 | |
| 124 | proc.on('error', err => { |
| 125 | clearTimeout(timer); |
| 126 | resolve({ content: `[SHELL_ERROR] ${err.message}`, isError: true }); |
| 127 | }); |
| 128 | |
| 129 | proc.on('close', (code, signal) => { |
| 130 | clearTimeout(timer); |
| 131 | const stdout = Buffer.concat(stdoutChunks).toString('utf-8'); |
| 132 | const stderr = Buffer.concat(stderrChunks).toString('utf-8'); |
| 133 | |