fix(core): spawn shell non-interactively without sourcing rc files

ShellSelect.args() ran zsh/bash with -l and explicitly sourced .zshrc/.bashrc,
loading user functions and aliases that can shadow builtins and return non-zero
exit codes, breaking && chains. Match the old tool behavior: plain non-login
non-interactive shell -c command with cwd passed via spawn options.
This commit is contained in:
Dax Raad 2026-06-29 12:05:34 -04:00
parent 381d67572e
commit 01edae4a7f
4 changed files with 8 additions and 37 deletions

View file

@ -159,7 +159,7 @@ export const layer = Layer.effect(
const cwd = input.cwd ?? location.directory
const configShell = Config.latest(yield* config.entries(), "shell")
const shell = ShellSelect.preferred(configShell)
const args = ShellSelect.args(shell, input.command, cwd)
const args = ShellSelect.args(shell, input.command)
const file = path.join(outputDir, `${id}.out`)
const env = {
...process.env,

View file

@ -163,37 +163,10 @@ function info(file: string): Item {
}
}
export function args(file: string, command: string, cwd: string) {
export function args(file: string, command: string) {
const n = name(file)
if (n === "nu" || n === "fish") return ["-c", command]
if (n === "zsh") {
return [
"-l",
"-c",
`
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
cd -- "$1"
eval ${JSON.stringify(command)}
`,
"opencode",
cwd,
]
}
if (n === "bash") {
return [
"-l",
"-c",
`
shopt -s expand_aliases
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
cd -- "$1"
eval ${JSON.stringify(command)}
`,
"opencode",
cwd,
]
}
if (n === "zsh" || n === "bash") return ["-c", command]
if (n === "cmd") return ["/c", command]
if (ps(file)) return ["-NoProfile", "-Command", command]
return ["-c", command]

View file

@ -55,12 +55,10 @@ describe("shell", () => {
})
test("builds command args per shell family", () => {
expect(ShellSelect.args("/bin/sh", "echo hi", "/tmp")).toEqual(["-c", "echo hi"])
expect(ShellSelect.args("/usr/bin/fish", "echo hi", "/tmp")).toEqual(["-c", "echo hi"])
const zsh = ShellSelect.args("/bin/zsh", "echo hi", "/tmp")
expect(zsh[0]).toBe("-l")
expect(zsh[1]).toBe("-c")
expect(zsh.at(-1)).toBe("/tmp")
expect(ShellSelect.args("/bin/sh", "echo hi")).toEqual(["-c", "echo hi"])
expect(ShellSelect.args("/usr/bin/fish", "echo hi")).toEqual(["-c", "echo hi"])
expect(ShellSelect.args("/bin/zsh", "echo hi")).toEqual(["-c", "echo hi"])
expect(ShellSelect.args("/bin/bash", "echo hi")).toEqual(["-c", "echo hi"])
})
if (process.platform === "win32") {

View file

@ -521,7 +521,7 @@ export const layer = Layer.effect(
const cfg = yield* config.get()
const sh = ShellSelect.preferred(cfg.shell)
const args = ShellSelect.args(sh, input.command, cwd)
const args = ShellSelect.args(sh, input.command)
let output = ""
let aborted = false