diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 9d851922..a4da4c2b 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -1,5 +1,5 @@ import { randomBytes } from "node:crypto"; -import { createWriteStream } from "node:fs"; +import { createWriteStream, existsSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { AgentTool } from "@mariozechner/pi-agent-core"; @@ -40,6 +40,11 @@ export function createBashTool(cwd: string): AgentTool { ) => { return new Promise((resolve, reject) => { const { shell, args } = getShellConfig(); + + if (!existsSync(cwd)) { + throw new Error(`Working directory does not exist: ${cwd}\nCannot execute bash commands.`); + } + const child = spawn(shell, [...args, command], { cwd, detached: true, @@ -119,6 +124,17 @@ export function createBashTool(cwd: string): AgentTool { child.stderr.on("data", handleData); } + // Handle shell spawn errors to prevent session from crashing + child.on("error", (err) => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + } + if (signal) { + signal.removeEventListener("abort", onAbort); + } + reject(err); + }); + // Handle process exit child.on("close", (code) => { if (timeoutHandle) { diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index d4601eb9..fac3f61a 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -1,14 +1,15 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; -import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { bashTool } from "../src/core/tools/bash.js"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { bashTool, createBashTool } from "../src/core/tools/bash.js"; import { editTool } from "../src/core/tools/edit.js"; import { findTool } from "../src/core/tools/find.js"; import { grepTool } from "../src/core/tools/grep.js"; import { lsTool } from "../src/core/tools/ls.js"; import { readTool } from "../src/core/tools/read.js"; import { writeTool } from "../src/core/tools/write.js"; +import * as shellModule from "../src/utils/shell.js"; // Helper to extract text from content blocks function getTextOutput(result: any): string { @@ -277,6 +278,27 @@ describe("Coding Agent Tools", () => { /timed out/i, ); }); + + it("should throw error when cwd does not exist", async () => { + const nonexistentCwd = "/this/directory/definitely/does/not/exist/12345"; + + const bashToolWithBadCwd = createBashTool(nonexistentCwd); + + await expect(bashToolWithBadCwd.execute("test-call-11", { command: "echo test" })).rejects.toThrow( + /Working directory does not exist/, + ); + }); + + it("should handle process spawn errors", async () => { + vi.spyOn(shellModule, "getShellConfig").mockReturnValueOnce({ + shell: "/nonexistent-shell-path-xyz123", + args: ["-c"], + }); + + const bashWithBadShell = createBashTool(testDir); + + await expect(bashWithBadShell.execute("test-call-12", { command: "echo test" })).rejects.toThrow(/ENOENT/); + }); }); describe("grep tool", () => {