Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions core/tools/implementations/runTerminalCommand.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import iconv from "iconv-lite";
import childProcess from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import { ContinueError, ContinueErrorReason } from "../../util/errors";
// Automatically decode the buffer according to the platform to avoid garbled Chinese
Expand All @@ -26,9 +27,25 @@ function getShellCommand(command: string): { shell: string; args: string[] } {
args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command],
};
} else {
// Unix/macOS: Use login shell to source .bashrc/.zshrc etc.
const userShell = process.env.SHELL || "/bin/bash";
return { shell: userShell, args: ["-l", "-c", command] };
// Unix/macOS: prefer configured shell, but gracefully fall back when unavailable
// (e.g. minimal dev containers without /bin/bash).
const configuredShell = process.env.SHELL;
const unixFallbacks = ["/bin/bash", "/bin/sh", "/bin/ash"];
const shellCandidates = configuredShell
? [configuredShell, ...unixFallbacks]
: unixFallbacks;

const shell =
shellCandidates.find((candidate) => {
if (!candidate) return false;
if (!candidate.startsWith("/")) {
// Non-absolute shells are resolved by PATH at spawn time.
return true;
}
return fs.existsSync(candidate);
}) ?? "/bin/sh";

return { shell, args: ["-l", "-c", command] };
}
}

Expand Down
25 changes: 25 additions & 0 deletions core/tools/implementations/runTerminalCommand.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,31 @@ describe("runTerminalCommandImpl", () => {
expect(result[0].status).toBe("Command completed");
});

it("should fall back to an available shell when SHELL is invalid", async () => {
if (process.platform === "win32") {
return;
}

const originalShell = process.env.SHELL;
process.env.SHELL = "/definitely-not-a-real-shell";

try {
const result = await runTerminalCommandImpl(
{ command: `node -e "console.log('fallback shell works')"` },
createMockExtras(),
);

expect(result[0].status).toBe("Command completed");
expect(result[0].content).toContain("fallback shell works");
} finally {
if (originalShell === undefined) {
delete process.env.SHELL;
} else {
process.env.SHELL = originalShell;
}
}
});

it("should stream output when onPartialOutput is provided", async () => {
// This test uses Node to create a command that outputs data incrementally
const command = `node -e "
Expand Down
Loading