diff --git a/src/infra/exec-wrapper-resolution.test.ts b/src/infra/exec-wrapper-resolution.test.ts new file mode 100644 index 00000000000..b271c97ee8d --- /dev/null +++ b/src/infra/exec-wrapper-resolution.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, test } from "vitest"; +import { normalizeExecutableToken } from "./exec-wrapper-resolution.js"; + +describe("normalizeExecutableToken", () => { + test("strips common windows executable suffixes", () => { + expect(normalizeExecutableToken("bun.cmd")).toBe("bun"); + expect(normalizeExecutableToken("deno.bat")).toBe("deno"); + expect(normalizeExecutableToken("pwsh.com")).toBe("pwsh"); + expect(normalizeExecutableToken("cmd.exe")).toBe("cmd"); + }); + + test("normalizes path-qualified windows shims", () => { + expect(normalizeExecutableToken("C:\\tools\\bun.cmd")).toBe("bun"); + expect(normalizeExecutableToken("/tmp/deno.exe")).toBe("deno"); + }); +}); diff --git a/src/infra/exec-wrapper-resolution.ts b/src/infra/exec-wrapper-resolution.ts index 006a0a65612..0cb423a11b3 100644 --- a/src/infra/exec-wrapper-resolution.ts +++ b/src/infra/exec-wrapper-resolution.ts @@ -7,7 +7,7 @@ import { export const MAX_DISPATCH_WRAPPER_DEPTH = 4; -const WINDOWS_EXE_SUFFIX = ".exe"; +const WINDOWS_EXECUTABLE_SUFFIXES = [".exe", ".cmd", ".bat", ".com"] as const; const POSIX_SHELL_WRAPPER_NAMES = ["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"] as const; const WINDOWS_CMD_WRAPPER_NAMES = ["cmd"] as const; @@ -31,13 +31,18 @@ function withWindowsExeAliases(names: readonly string[]): string[] { const expanded = new Set(); for (const name of names) { expanded.add(name); - expanded.add(`${name}${WINDOWS_EXE_SUFFIX}`); + expanded.add(`${name}.exe`); } return Array.from(expanded); } -function stripWindowsExeSuffix(value: string): string { - return value.endsWith(WINDOWS_EXE_SUFFIX) ? value.slice(0, -WINDOWS_EXE_SUFFIX.length) : value; +function stripWindowsExecutableSuffix(value: string): string { + for (const suffix of WINDOWS_EXECUTABLE_SUFFIXES) { + if (value.endsWith(suffix)) { + return value.slice(0, -suffix.length); + } + } + return value; } export const POSIX_SHELL_WRAPPERS = new Set(POSIX_SHELL_WRAPPER_NAMES); @@ -115,7 +120,7 @@ export function basenameLower(token: string): string { } export function normalizeExecutableToken(token: string): string { - return stripWindowsExeSuffix(basenameLower(token)); + return stripWindowsExecutableSuffix(basenameLower(token)); } export function isDispatchWrapperExecutable(token: string): boolean { @@ -132,7 +137,7 @@ function normalizeRawCommand(rawCommand?: string | null): string | null { } function findShellWrapperSpec(baseExecutable: string): ShellWrapperSpec | null { - const canonicalBase = stripWindowsExeSuffix(baseExecutable); + const canonicalBase = stripWindowsExecutableSuffix(baseExecutable); for (const spec of SHELL_WRAPPER_SPECS) { if (spec.names.has(canonicalBase)) { return spec;