diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index 671829c0f8..19e8330f62 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -177,16 +177,23 @@ export const BashTool = Tool.define("bash", async () => { { cwd, sessionID: ctx.sessionID, callID: ctx.callID }, { env: {} }, ) - const proc = spawn(params.command, { - shell, + const env = { + ...process.env, + ...shellEnv.env, + } + const opts = { cwd, - env: { - ...process.env, - ...shellEnv.env, - }, - stdio: ["ignore", "pipe", "pipe"], + env, + stdio: ["ignore", "pipe", "pipe"] as const, detached: process.platform !== "win32", - }) + } + const proc = + process.platform === "win32" && ["pwsh", "powershell"].includes(name.toLowerCase()) + ? spawn(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", params.command], opts) + : spawn(params.command, { + ...opts, + shell, + }) let output = "" @@ -243,6 +250,10 @@ export const BashTool = Tool.define("bash", async () => { proc.once("exit", () => { exited = true + }) + + proc.once("close", () => { + exited = true cleanup() resolve() }) diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index ad4441af9b..62e7048d12 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -23,7 +23,14 @@ const ctx = { const projectRoot = path.join(__dirname, "../..") const bin = process.execPath.replaceAll("\\", "/") const file = path.join(projectRoot, "test/tool/fixtures/output.ts").replaceAll("\\", "/") -const fill = (mode: "lines" | "bytes", n: number) => `${bin} ${file} ${mode} ${n}` +const kind = () => path.win32.basename(process.env.SHELL || "", ".exe").toLowerCase() +const fill = (mode: "lines" | "bytes", n: number) => { + if (["pwsh", "powershell"].includes(kind())) { + if (mode === "lines") return `1..${n} | ForEach-Object { $_ }` + return `Write-Output ('a' * ${n})` + } + return `${bin} ${file} ${mode} ${n}` +} const shells = (() => { if (process.platform !== "win32") { const shell = process.env.SHELL || Bun.which("bash") || "/bin/sh" @@ -322,10 +329,13 @@ describe("tool.bash permissions", () => { requests.push(req) }, } - await bash.execute({ command: "cat > /tmp/output.txt", description: "Redirect ls output" }, testCtx) + const command = ["pwsh", "powershell"].includes(kind()) + ? "Write-Output test > output.txt" + : "cat > /tmp/output.txt" + await bash.execute({ command, description: "Redirect ls output" }, testCtx) const bashReq = requests.find((r) => r.permission === "bash") expect(bashReq).toBeDefined() - expect(bashReq!.patterns).toContain("cat > /tmp/output.txt") + expect(bashReq!.patterns).toContain(command) }, }) }) @@ -431,7 +441,7 @@ describe("tool.bash truncation", () => { expect(filepath).toBeTruthy() const saved = await Filesystem.readText(filepath) - const lines = saved.trim().split("\n") + const lines = saved.trim().split(/\r?\n/) expect(lines.length).toBe(lineCount) expect(lines[0]).toBe("1") expect(lines[lineCount - 1]).toBe(String(lineCount))