Merge pull request #3993 from code-yeongyu/fix/non-interactive-env-windows-shell
fix(non-interactive-env): respect Windows command shell
This commit is contained in:
@@ -13,6 +13,7 @@ describe("non-interactive-env hook", () => {
|
|||||||
SHELL: process.env.SHELL,
|
SHELL: process.env.SHELL,
|
||||||
PSModulePath: process.env.PSModulePath,
|
PSModulePath: process.env.PSModulePath,
|
||||||
MSYSTEM: process.env.MSYSTEM,
|
MSYSTEM: process.env.MSYSTEM,
|
||||||
|
ComSpec: process.env.ComSpec,
|
||||||
CI: process.env.CI,
|
CI: process.env.CI,
|
||||||
OPENCODE_NON_INTERACTIVE: process.env.OPENCODE_NON_INTERACTIVE,
|
OPENCODE_NON_INTERACTIVE: process.env.OPENCODE_NON_INTERACTIVE,
|
||||||
}
|
}
|
||||||
@@ -251,7 +252,7 @@ describe("non-interactive-env hook", () => {
|
|||||||
expect(cmd).toContain("; git commit")
|
expect(cmd).toContain("; git commit")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("#given Windows with PowerShell env #when bash tool git command executes #then uses powershell syntax", async () => {
|
test("#given Windows cmd environment with PSModulePath #when bash tool git command executes #then uses cmd syntax", async () => {
|
||||||
delete process.env.SHELL
|
delete process.env.SHELL
|
||||||
delete process.env.MSYSTEM
|
delete process.env.MSYSTEM
|
||||||
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
||||||
@@ -267,10 +268,80 @@ describe("non-interactive-env hook", () => {
|
|||||||
output
|
output
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const cmd = output.args.command as string
|
||||||
|
expect(cmd).toStartWith("set ")
|
||||||
|
expect(cmd).toContain(" && git status")
|
||||||
|
expect(cmd).toContain('GIT_EDITOR=":"')
|
||||||
|
expect(cmd).not.toContain("$env:")
|
||||||
|
expect(cmd).not.toContain("export ")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given Windows SHELL=cmd.exe #when bash tool git command executes #then uses cmd syntax", async () => {
|
||||||
|
process.env.SHELL = "C:\\Windows\\System32\\cmd.exe"
|
||||||
|
delete process.env.MSYSTEM
|
||||||
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
||||||
|
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).toStartWith("set ")
|
||||||
|
expect(cmd).toContain(" && git status")
|
||||||
|
expect(cmd).not.toContain("$env:")
|
||||||
|
expect(cmd).not.toContain("export ")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given Windows ComSpec=pwsh.exe without SHELL #when bash tool git command executes #then uses powershell syntax", async () => {
|
||||||
|
delete process.env.SHELL
|
||||||
|
delete process.env.MSYSTEM
|
||||||
|
process.env.ComSpec = "C:\\Program Files\\PowerShell\\7\\pwsh.exe"
|
||||||
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
||||||
|
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).toStartWith("$env:")
|
||||||
|
expect(cmd).toContain("; git status")
|
||||||
|
expect(cmd).not.toContain("set ")
|
||||||
|
expect(cmd).not.toContain("export ")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given Windows SHELL=pwsh.exe #when bash tool git command executes #then uses powershell syntax", async () => {
|
||||||
|
process.env.SHELL = "C:\\Program Files\\PowerShell\\7\\pwsh.exe"
|
||||||
|
delete process.env.MSYSTEM
|
||||||
|
process.env.PSModulePath = "C:\\Program Files\\PowerShell\\Modules"
|
||||||
|
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
|
const cmd = output.args.command as string
|
||||||
expect(cmd).toStartWith("$env:")
|
expect(cmd).toStartWith("$env:")
|
||||||
expect(cmd).toContain("; git status")
|
expect(cmd).toContain("; git status")
|
||||||
expect(cmd).toContain("$env:GIT_EDITOR=':'")
|
|
||||||
expect(cmd).not.toContain("set ")
|
expect(cmd).not.toContain("set ")
|
||||||
expect(cmd).not.toContain("export ")
|
expect(cmd).not.toContain("export ")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
|
import { HOOK_NAME, NON_INTERACTIVE_ENV, SHELL_COMMAND_PATTERNS } from "./constants"
|
||||||
import { log, buildEnvPrefix } from "../../shared"
|
import { log, buildEnvPrefix } from "../../shared"
|
||||||
import { detectShellType } from "../../shared/shell-env"
|
import { detectShellType, type ShellType } from "../../shared/shell-env"
|
||||||
|
|
||||||
export * from "./constants"
|
export * from "./constants"
|
||||||
export * from "./detector"
|
export * from "./detector"
|
||||||
@@ -20,6 +20,41 @@ function detectBannedCommand(command: string): string | undefined {
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectWindowsShellType(shellPath: string | undefined): ShellType | undefined {
|
||||||
|
if (!shellPath) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const shellName = shellPath.replace(/\\/g, "/").split("/").pop()?.toLowerCase()
|
||||||
|
if (shellName === "cmd" || shellName === "cmd.exe") {
|
||||||
|
return "cmd"
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
shellName === "powershell" ||
|
||||||
|
shellName === "powershell.exe" ||
|
||||||
|
shellName === "pwsh" ||
|
||||||
|
shellName === "pwsh.exe"
|
||||||
|
) {
|
||||||
|
return "powershell"
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectCommandShellType(): ShellType {
|
||||||
|
if (process.platform === "win32" && process.env.SHELL) {
|
||||||
|
const shellType = detectWindowsShellType(process.env.SHELL)
|
||||||
|
if (shellType) {
|
||||||
|
return shellType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === "win32" && !process.env.SHELL && !process.env.MSYSTEM) {
|
||||||
|
return detectWindowsShellType(process.env.ComSpec) ?? "cmd"
|
||||||
|
}
|
||||||
|
|
||||||
|
return detectShellType()
|
||||||
|
}
|
||||||
|
|
||||||
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
|
export function createNonInteractiveEnvHook(_ctx: PluginInput) {
|
||||||
return {
|
return {
|
||||||
"tool.execute.before": async (
|
"tool.execute.before": async (
|
||||||
@@ -53,7 +88,7 @@ export function createNonInteractiveEnvHook(_ctx: PluginInput) {
|
|||||||
// The env vars (GIT_EDITOR=:, EDITOR=:, etc.) must ALWAYS be injected
|
// The env vars (GIT_EDITOR=:, EDITOR=:, etc.) must ALWAYS be injected
|
||||||
// for git commands to prevent interactive prompts.
|
// for git commands to prevent interactive prompts.
|
||||||
|
|
||||||
const shellType = detectShellType()
|
const shellType = detectCommandShellType()
|
||||||
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType)
|
const envPrefix = buildEnvPrefix(NON_INTERACTIVE_ENV, shellType)
|
||||||
|
|
||||||
// Check if the command already starts with the prefix to avoid stacking.
|
// Check if the command already starts with the prefix to avoid stacking.
|
||||||
|
|||||||
Reference in New Issue
Block a user