fix(non-interactive-env): use powershell syntax on Windows regardless of SHELL/MSYSTEM

Closes #3607.

## Root cause

On Windows, `detectCommandShellType()` fell through to `detectShellType()`
for two common environments and incorrectly returned `"unix"`:

1. **SHELL points at a Unix-shaped path** (e.g. Git Bash sets
   `SHELL=/usr/bin/bash` on a fresh Windows install).
   The `detectWindowsShellType(process.env.SHELL)` probe didn't recognize
   `bash` as a Windows shell, so the function fell through and
   `detectShellType()` returned `"unix"`.

2. **MSYSTEM is set but SHELL is not** (Git Bash leaves MSYSTEM permanently
   set system-wide even when the active shell is PowerShell).
   The fall-through path returned `"unix"` via the MSYSTEM check.

In both cases, the hook then prepended `export KEY=val;` to git commands,
which PowerShell rejects with:

  `export : 无法将"export"项识别为 cmdlet...`

OpenCode on Windows runs the bash tool through a Windows shell
(PowerShell by default, cmd as the user-overridable fallback), regardless
of MSYSTEM or a Unix-shaped SHELL set by Git Bash — so the env prefix
must use Windows-compatible syntax.

## Fix

`detectCommandShellType()` now short-circuits on `process.platform === "win32"`:

- If `SHELL` points at a recognized Windows shell (`cmd.exe`, `powershell.exe`,
  `pwsh.exe`), return that.
- If `SHELL` and `MSYSTEM` are both unset, fall back to `ComSpec` then to cmd.
- Otherwise, default to PowerShell — matching what OpenCode actually spawns.

`detectShellType()` is unchanged; other callers (including non-Windows
platforms) are unaffected.

## Test changes

Three pre-existing tests encoded the buggy behavior as expected behavior
and have been updated to assert the new PowerShell syntax with a
`(#3607)` marker and a comment explaining why a Unix-shaped SHELL on
win32 must still resolve to PowerShell. WSL is not affected because in
WSL `process.platform === "linux"`, not `"win32"`.

- `src/hooks/non-interactive-env/`: 24 tests pass / 0 fail
- `bunx tsc --noEmit`: clean

## Note on issue thread

The sisyphus-bot triage comment on #3607 framed this as a policy choice
between (A) forcing Windows env-prefix syntax and (B) resolving against
the OpenCode-configured shell. This PR implements option (A) as the
minimal surgical fix; option (B) remains a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZeyuFu
2026-05-17 10:59:29 -04:00
parent babee921b2
commit 2bd7946001
2 changed files with 42 additions and 20 deletions
+22 -10
View File
@@ -370,7 +370,11 @@ describe("non-interactive-env hook", () => {
expect(cmd).not.toContain("export ")
})
test("#given Windows Git Bash environment with SHELL #when git command executes #then uses unix syntax", async () => {
test("#given Windows Git Bash SHELL=/usr/bin/bash #when git command executes #then uses powershell syntax (#3607)", async () => {
// Regression for #3607: OpenCode on Windows runs the bash tool through
// PowerShell by default, regardless of a Unix-shaped SHELL set by Git
// Bash. The export prefix is invalid PowerShell, so we must use
// PowerShell syntax even when SHELL points at /usr/bin/bash.
delete process.env.PSModulePath
process.env.SHELL = "/usr/bin/bash"
Object.defineProperty(process, "platform", { value: "win32" })
@@ -386,12 +390,15 @@ describe("non-interactive-env hook", () => {
)
const cmd = output.args.command as string
expect(cmd).toStartWith("export ")
expect(cmd).toStartWith("$env:")
expect(cmd).toContain("; git status")
expect(cmd).not.toContain("$env:")
expect(cmd).not.toContain("export ")
})
test("#given Windows Git Bash via MSYSTEM without SHELL #when git command executes #then uses unix syntax", async () => {
test("#given Windows MSYSTEM=MINGW64 without SHELL #when git command executes #then uses powershell syntax (#3607)", async () => {
// Regression for #3607: MSYSTEM is permanently set on systems with Git
// Bash installed, but OpenCode on Windows still spawns PowerShell.
// MSYSTEM alone must not select Unix env-prefix syntax.
delete process.env.SHELL
process.env.MSYSTEM = "MINGW64"
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
@@ -408,9 +415,9 @@ describe("non-interactive-env hook", () => {
)
const cmd = output.args.command as string
expect(cmd).toStartWith("export ")
expect(cmd).toStartWith("$env:")
expect(cmd).toContain("; git status")
expect(cmd).not.toContain("$env:")
expect(cmd).not.toContain("export ")
})
test("#given Windows platform #when chained git commands via bash tool #then uses cmd syntax", async () => {
@@ -437,8 +444,13 @@ describe("non-interactive-env hook", () => {
expect(cmd).not.toContain("$env:")
})
test("#given SHELL=/bin/bash on win32 #when git command executes #then uses unix syntax", async () => {
// Git Bash or WSL sets SHELL env var - should override platform detection
test("#given SHELL=/bin/bash on win32 #when git command executes #then uses powershell syntax (#3607)", async () => {
// Regression for #3607: a Unix-shaped SHELL value (Git Bash sets
// SHELL=/bin/bash or /usr/bin/bash) does NOT mean OpenCode will run
// the bash tool in a Unix shell on Windows — OpenCode spawns
// PowerShell, so the env prefix must use PowerShell syntax.
// WSL is not affected by this assertion because in WSL,
// process.platform === "linux", not "win32".
delete process.env.PSModulePath
process.env.SHELL = "/bin/bash"
Object.defineProperty(process, "platform", { value: "win32" })
@@ -454,9 +466,9 @@ describe("non-interactive-env hook", () => {
)
const cmd = output.args.command as string
expect(cmd).toStartWith("export ")
expect(cmd).toStartWith("$env:")
expect(cmd).toContain("; git status")
expect(cmd).not.toContain("$env:")
expect(cmd).not.toContain("export ")
})
test("#given PSModulePath set on non-Windows #when git command executes #then uses powershell syntax", async () => {
@@ -41,18 +41,28 @@ function detectWindowsShellType(shellPath: string | undefined): ShellType | unde
}
function detectCommandShellType(): ShellType {
if (process.platform === "win32" && process.env.SHELL) {
const shellType = detectWindowsShellType(process.env.SHELL)
if (shellType) {
return shellType
if (process.platform !== "win32") {
return detectShellType()
}
// OpenCode on Windows runs the bash tool through a Windows shell
// (PowerShell by default, with cmd as the user-overridable fallback),
// regardless of MSYSTEM or a Unix-shaped SHELL value set by Git Bash.
// Map any explicit Windows shell we can recognize; otherwise default
// to PowerShell so the prepended env-var syntax matches the shell that
// actually executes the command. See #3607.
const fromShell = detectWindowsShellType(process.env.SHELL)
if (fromShell) {
return fromShell
}
if (!process.env.SHELL && !process.env.MSYSTEM) {
const fromComSpec = detectWindowsShellType(process.env.ComSpec)
if (fromComSpec) {
return fromComSpec
}
return "cmd"
}
if (process.platform === "win32" && !process.env.SHELL && !process.env.MSYSTEM) {
return detectWindowsShellType(process.env.ComSpec) ?? "cmd"
}
return detectShellType()
return "powershell"
}
export function createNonInteractiveEnvHook(_ctx: PluginInput) {