diff --git a/packages/opencode/src/tool/shell/runner.ts b/packages/opencode/src/tool/shell/runner.ts index 0c4f7ee05d..2adbf77b14 100644 --- a/packages/opencode/src/tool/shell/runner.ts +++ b/packages/opencode/src/tool/shell/runner.ts @@ -11,9 +11,11 @@ export function preview(text: string) { } export namespace ShellRunner { - function wrap(name: string, command: string) { - if (name !== "powershell" && name !== "pwsh") return command - return `${command}; if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE }; if ($?) { exit 0 }; exit 1` + function preserveExitCode(command: string) { + return `${command} +if ($null -ne $LASTEXITCODE) { exit $LASTEXITCODE } +if ($?) { exit 0 } +exit 1` } export async function shellEnv(ctx: Tool.Context, cwd: string) { @@ -26,7 +28,7 @@ export namespace ShellRunner { export function launch(shell: string, name: string, command: string, cwd: string, env: NodeJS.ProcessEnv) { if (process.platform === "win32" && (name === "powershell" || name === "pwsh")) { - return spawn(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", wrap(name, command)], { + return spawn(shell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", preserveExitCode(command)], { cwd, env, stdio: ["ignore", "pipe", "pipe"], diff --git a/packages/opencode/test/tool/shell.test.ts b/packages/opencode/test/tool/shell.test.ts index 53dee21311..39b54a7a26 100644 --- a/packages/opencode/test/tool/shell.test.ts +++ b/packages/opencode/test/tool/shell.test.ts @@ -115,6 +115,15 @@ const each = (name: string, fn: (item: { label: string; shell: string }) => Prom } } +const eachps = (name: string, fn: (item: { label: string; shell: string }) => Promise) => { + for (const item of ps) { + test( + `${name} [${item.label}]`, + withShell(item, () => fn(item)), + ) + } +} + const capture = (requests: Array>, stop?: Error) => ({ ...ctx, ask: async (req: Omit) => { @@ -1018,6 +1027,40 @@ describe("tool.shell runtime", () => { }) }) + eachps("preserves native exit code with trailing comment", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await getTool() + const result = await bash.execute( + { + command: `${js("process.exit(42)")} # keep wrapper separate`, + description: "Trailing comment exit", + }, + ctx, + ) + expect(result.metadata.exit).toBe(42) + }, + }) + }) + + eachps("returns non-zero exit for powershell cmdlet errors", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const bash = await getTool() + const result = await bash.execute( + { + command: "Write-Error x", + description: "Cmdlet error exit", + }, + ctx, + ) + expect(result.metadata.exit).toBe(1) + }, + }) + }) + each("streams metadata updates progressively", async () => { await Instance.provide({ directory: projectRoot,