(cmd: string, args: string[], options: ExecOptions = {})
| 182 | * 2. options.env (call-specific env vars) |
| 183 | */ |
| 184 | export async function execCommand(cmd: string, args: string[], options: ExecOptions = {}): Promise<ExecResult> { |
| 185 | const { cwd, timeout = 30000, maxBuffer = 50 * 1024 * 1024, env: callEnv } = options |
| 186 | |
| 187 | const mergedEnv = { ...process.env, ...callEnv } |
| 188 | |
| 189 | return new Promise((resolve) => { |
| 190 | let stdout = "" |
| 191 | let stderr = "" |
| 192 | let killed = false |
| 193 | let resolved = false |
| 194 | |
| 195 | const child = spawn(cmd, args, { |
| 196 | cwd, |
| 197 | env: mergedEnv, |
| 198 | stdio: ["ignore", "pipe", "pipe"], |
| 199 | }) |
| 200 | |
| 201 | const timeoutId = |
| 202 | timeout > 0 |
| 203 | ? setTimeout(() => { |
| 204 | killed = true |
| 205 | child.kill("SIGTERM") |
| 206 | }, timeout) |
| 207 | : null |
| 208 | |
| 209 | child.stdout?.on("data", (data) => { |
| 210 | if (stdout.length < maxBuffer) { |
| 211 | stdout += data.toString() |
| 212 | } |
| 213 | }) |
| 214 | |
| 215 | child.stderr?.on("data", (data) => { |
| 216 | if (stderr.length < maxBuffer) { |
| 217 | stderr += data.toString() |
| 218 | } |
| 219 | }) |
| 220 | |
| 221 | child.on("error", (err) => { |
| 222 | if (resolved) return |
| 223 | resolved = true |
| 224 | if (timeoutId) clearTimeout(timeoutId) |
| 225 | resolve({ |
| 226 | stdout: "", |
| 227 | stderr: err.message, |
| 228 | success: false, |
| 229 | code: null, |
| 230 | }) |
| 231 | }) |
| 232 | |
| 233 | child.on("close", (code) => { |
| 234 | if (resolved) return |
| 235 | resolved = true |
| 236 | if (timeoutId) clearTimeout(timeoutId) |
| 237 | resolve({ |
| 238 | stdout: stdout.replace(/\r\n/g, "\n"), |
| 239 | stderr: stderr.replace(/\r\n/g, "\n"), |
| 240 | success: code === 0 && !killed, |
| 241 | code, |
no test coverage detected