diff --git a/src/shared/shell-env.ts b/src/shared/shell-env.ts index d34ee3254d..c56ae46d0a 100644 --- a/src/shared/shell-env.ts +++ b/src/shared/shell-env.ts @@ -5,12 +5,14 @@ export type ShellType = "unix" | "powershell" | "cmd" | "csh" * * Detection priority: * 1. SHELL env var → Unix shell (explicit user choice takes precedence) - * 2. PSModulePath → PowerShell - * 3. Platform fallback → win32: cmd, others: unix + * 2. Unix shell indicators on Windows → Git Bash, WSL, MSYS2 + * 3. PSModulePath → PowerShell + * 4. Platform fallback → win32: cmd, others: unix * - * Note: SHELL is checked before PSModulePath because on Windows, PSModulePath - * is always set by the system even when the active shell is Git Bash or WSL. - * An explicit SHELL variable indicates the user's chosen shell overrides that. + * Note: Step 2 is scoped to Windows only because PSModulePath is always set + * on Windows regardless of the active shell. Indicators are deliberately + * specific (BASH_VERSION, MSYSTEM, WSL_DISTRO_NAME) — TERM is excluded + * because some PowerShell users set it manually. */ export function detectShellType(): ShellType { if (process.env.SHELL) { @@ -21,6 +23,22 @@ export function detectShellType(): ShellType { return "unix" } + // On Windows, detect Unix-compatible shells (Git Bash, WSL, MSYS2). + // PSModulePath is always set on Windows, so we must check these BEFORE it. + // Indicators are shell-specific — no broad signals like TERM. + if ( + process.platform === "win32" && + (process.env.BASH_VERSION || + process.env.MSYSTEM || + process.env.WSL_DISTRO_NAME) + ) { + return "unix" + } + + if (process.platform === "win32" && detectUnixPathInPATH()) { + return "unix" + } + if (process.env.PSModulePath) { return "powershell" } @@ -28,6 +46,15 @@ export function detectShellType(): ShellType { return process.platform === "win32" ? "cmd" : "unix" } +function detectUnixPathInPATH(): boolean { + const path = (process.env.PATH || process.env.Path || "").toLowerCase() + if (path.includes("\\usr\\bin") || path.includes("/usr/bin")) return true + if (path.includes("\\msys64\\bin") || path.includes("/msys64/bin")) return true + if (path.includes("\\cygwin\\bin") || path.includes("/cygwin/bin")) return true + if (path.includes("\\git\\usr\\bin") || path.includes("/git/usr/bin")) return true + return false +} + /** * Shell-escape a value for use in environment variable assignment. * @@ -161,8 +188,8 @@ export function shellEscapeForDoubleQuotedCommand(value: string): string { .replace(/"/g, "\\\"") // escape double quotes .replace(/;/g, "\\;") // escape semicolon (command separator) .replace(/\|/g, "\\|") // escape pipe (command separator) - .replace(/&/g, "\\&") // escape ampersand (command separator) - .replace(/#/g, "\\#") // escape hash (comment) + .replace(/&/g, "\\&") // escape ampersand + .replace(/#/g, "\\#") // escape hash .replace(/\(/g, "\\(") // escape parentheses .replace(/\)/g, "\\)") // escape parentheses -} +} \ No newline at end of file