fix(hooks): use unix shell syntax for bash tool on all platforms (#1015)
The bash tool always runs in a Unix-like shell (bash/sh), even on Windows (via Git Bash, WSL, etc.), so we should always use unix export syntax instead of detecting the shell type dynamically. Fixes #983 Fixes #889 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
262c7118da
commit
15c4637e0a
@ -178,7 +178,11 @@ describe("non-interactive-env hook", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("cross-platform shell support", () => {
|
describe("bash tool always uses unix shell syntax", () => {
|
||||||
|
// The bash tool always runs in a Unix-like shell (bash/sh), even on Windows
|
||||||
|
// (via Git Bash, WSL, etc.), so we should always use unix export syntax.
|
||||||
|
// This fixes GitHub issues #983 and #889.
|
||||||
|
|
||||||
test("#given macOS platform #when git command executes #then uses unix export syntax", async () => {
|
test("#given macOS platform #when git command executes #then uses unix export syntax", async () => {
|
||||||
delete process.env.PSModulePath
|
delete process.env.PSModulePath
|
||||||
process.env.SHELL = "/bin/zsh"
|
process.env.SHELL = "/bin/zsh"
|
||||||
@ -221,7 +225,9 @@ describe("non-interactive-env hook", () => {
|
|||||||
expect(cmd).toContain("; git commit")
|
expect(cmd).toContain("; git commit")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given Windows with PowerShell #when git command executes #then uses powershell $env syntax", async () => {
|
test("#given Windows with PowerShell env #when bash tool git command executes #then still uses unix export syntax", async () => {
|
||||||
|
// Even when PSModulePath is set (indicating PowerShell environment),
|
||||||
|
// the bash tool runs in a Unix-like shell, so we use export syntax
|
||||||
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
||||||
Object.defineProperty(process, "platform", { value: "win32" })
|
Object.defineProperty(process, "platform", { value: "win32" })
|
||||||
|
|
||||||
@ -236,13 +242,16 @@ describe("non-interactive-env hook", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const cmd = output.args.command as string
|
const cmd = output.args.command as string
|
||||||
expect(cmd).toContain("$env:")
|
// Should use unix export syntax, NOT PowerShell $env: syntax
|
||||||
|
expect(cmd).toStartWith("export ")
|
||||||
expect(cmd).toContain("; git status")
|
expect(cmd).toContain("; git status")
|
||||||
expect(cmd).not.toStartWith("export ")
|
expect(cmd).not.toContain("$env:")
|
||||||
expect(cmd).not.toContain("set ")
|
expect(cmd).not.toContain("set ")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given Windows without PowerShell #when git command executes #then uses cmd set syntax", async () => {
|
test("#given Windows without SHELL env #when bash tool git command executes #then still uses unix export syntax", async () => {
|
||||||
|
// Even when detectShellType() would return "cmd" (no SHELL, no PSModulePath, win32),
|
||||||
|
// the bash tool runs in a Unix-like shell, so we use export syntax
|
||||||
delete process.env.PSModulePath
|
delete process.env.PSModulePath
|
||||||
delete process.env.SHELL
|
delete process.env.SHELL
|
||||||
Object.defineProperty(process, "platform", { value: "win32" })
|
Object.defineProperty(process, "platform", { value: "win32" })
|
||||||
@ -258,14 +267,18 @@ describe("non-interactive-env hook", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const cmd = output.args.command as string
|
const cmd = output.args.command as string
|
||||||
expect(cmd).toContain("set ")
|
// Should use unix export syntax, NOT cmd.exe set syntax
|
||||||
expect(cmd).toContain("&&")
|
expect(cmd).toStartWith("export ")
|
||||||
expect(cmd).not.toStartWith("export ")
|
expect(cmd).toContain("; git log")
|
||||||
|
expect(cmd).not.toContain("set ")
|
||||||
|
expect(cmd).not.toContain("&&")
|
||||||
expect(cmd).not.toContain("$env:")
|
expect(cmd).not.toContain("$env:")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given PowerShell #when values contain quotes #then escapes correctly", async () => {
|
test("#given Windows Git Bash environment #when git command executes #then uses unix export syntax", async () => {
|
||||||
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
// Simulating Git Bash on Windows: SHELL might be set to /usr/bin/bash
|
||||||
|
delete process.env.PSModulePath
|
||||||
|
process.env.SHELL = "/usr/bin/bash"
|
||||||
Object.defineProperty(process, "platform", { value: "win32" })
|
Object.defineProperty(process, "platform", { value: "win32" })
|
||||||
|
|
||||||
const hook = createNonInteractiveEnvHook(mockCtx)
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
||||||
@ -279,32 +292,16 @@ describe("non-interactive-env hook", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const cmd = output.args.command as string
|
const cmd = output.args.command as string
|
||||||
expect(cmd).toMatch(/\$env:\w+='[^']*'/)
|
expect(cmd).toStartWith("export ")
|
||||||
|
expect(cmd).toContain("; git status")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given cmd.exe #when values contain spaces #then escapes correctly", async () => {
|
test("#given any platform #when chained git commands via bash tool #then uses unix export syntax", async () => {
|
||||||
|
// Even on Windows, chained commands should use unix syntax
|
||||||
delete process.env.PSModulePath
|
delete process.env.PSModulePath
|
||||||
delete process.env.SHELL
|
delete process.env.SHELL
|
||||||
Object.defineProperty(process, "platform", { value: "win32" })
|
Object.defineProperty(process, "platform", { value: "win32" })
|
||||||
|
|
||||||
const hook = createNonInteractiveEnvHook(mockCtx)
|
|
||||||
const output: { args: Record<string, unknown>; message?: string } = {
|
|
||||||
args: { command: "git status" },
|
|
||||||
}
|
|
||||||
|
|
||||||
await hook["tool.execute.before"](
|
|
||||||
{ tool: "bash", sessionID: "test", callID: "1" },
|
|
||||||
output
|
|
||||||
)
|
|
||||||
|
|
||||||
const cmd = output.args.command as string
|
|
||||||
expect(cmd).toMatch(/set \w+="[^"]*"/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("#given PowerShell #when chained git commands #then env vars apply to all commands", async () => {
|
|
||||||
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
|
||||||
Object.defineProperty(process, "platform", { value: "win32" })
|
|
||||||
|
|
||||||
const hook = createNonInteractiveEnvHook(mockCtx)
|
const hook = createNonInteractiveEnvHook(mockCtx)
|
||||||
const output: { args: Record<string, unknown>; message?: string } = {
|
const output: { args: Record<string, unknown>; message?: string } = {
|
||||||
args: { command: "git add file && git commit -m 'test'" },
|
args: { command: "git add file && git commit -m 'test'" },
|
||||||
@ -316,7 +313,7 @@ describe("non-interactive-env hook", () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const cmd = output.args.command as string
|
const cmd = output.args.command as string
|
||||||
expect(cmd).toContain("$env:")
|
expect(cmd).toStartWith("export ")
|
||||||
expect(cmd).toContain("; git add file && git commit")
|
expect(cmd).toContain("; git add file && git commit")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
|
import type { ShellType } from "../../shared"
|
||||||
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
|
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
|
||||||
import { isNonInteractive } from "./detector"
|
import { isNonInteractive } from "./detector"
|
||||||
import { log, detectShellType, buildEnvPrefix } from "../../shared"
|
import { log, buildEnvPrefix } from "../../shared"
|
||||||
|
|
||||||
export * from "./constants"
|
export * from "./constants"
|
||||||
export * from "./detector"
|
export * from "./detector"
|
||||||
@ -50,7 +51,10 @@ export function createNonInteractiveEnvHook(_ctx: PluginInput) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const shellType = detectShellType()
|
// The bash tool always runs in a Unix-like shell (bash/sh), even on Windows
|
||||||
|
// (via Git Bash, WSL, etc.), so we always use unix export syntax.
|
||||||
|
// This fixes GitHub issues #983 and #889.
|
||||||
|
const shellType: ShellType = "unix"
|
||||||
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType)
|
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType)
|
||||||
output.args.command = `${envPrefix} ${command}`
|
output.args.command = `${envPrefix} ${command}`
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user