From 86385f72e98daa3c7c9cbf52069333eb0ca59496 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 22:19:17 +0100 Subject: [PATCH] fix(update): use absolute npm script shell --- src/infra/update-global.test.ts | 41 +++++++++++++++++++++++++++++++++ src/infra/update-global.ts | 34 ++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/infra/update-global.test.ts b/src/infra/update-global.test.ts index fdd3b25f53d..20c401162f9 100644 --- a/src/infra/update-global.test.ts +++ b/src/infra/update-global.test.ts @@ -150,6 +150,47 @@ describe("update global helpers", () => { }); }); + it("uses an absolute POSIX script shell for npm lifecycle scripts during global installs", async () => { + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("linux"); + try { + await expect( + createGlobalInstallEnv({ + COREPACK_ENABLE_DOWNLOAD_PROMPT: "1", + PATH: "/home/peter/.npm-global/bin", + }), + ).resolves.toMatchObject({ + COREPACK_ENABLE_DOWNLOAD_PROMPT: "1", + NPM_CONFIG_SCRIPT_SHELL: "/bin/sh", + }); + } finally { + platformSpy.mockRestore(); + } + }); + + it("preserves explicit npm script shell config for global installs", async () => { + const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("linux"); + try { + await expect( + createGlobalInstallEnv({ + COREPACK_ENABLE_DOWNLOAD_PROMPT: "1", + NPM_CONFIG_SCRIPT_SHELL: "/custom/sh", + }), + ).resolves.toMatchObject({ + NPM_CONFIG_SCRIPT_SHELL: "/custom/sh", + }); + await expect( + createGlobalInstallEnv({ + COREPACK_ENABLE_DOWNLOAD_PROMPT: "1", + npm_config_script_shell: "/custom/lower-sh", + }), + ).resolves.toMatchObject({ + npm_config_script_shell: "/custom/lower-sh", + }); + } finally { + platformSpy.mockRestore(); + } + }); + it("resolves portable Git paths from process-local app data only", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); try { diff --git a/src/infra/update-global.ts b/src/infra/update-global.ts index a80017f062e..e1628e64380 100644 --- a/src/infra/update-global.ts +++ b/src/infra/update-global.ts @@ -41,6 +41,7 @@ const NPM_GLOBAL_INSTALL_OMIT_OPTIONAL_FLAGS = [ "--omit=optional", ...NPM_GLOBAL_INSTALL_QUIET_FLAGS, ] as const; +const NPM_CONFIG_SCRIPT_SHELL_KEYS = ["NPM_CONFIG_SCRIPT_SHELL", "npm_config_script_shell"]; const FIRST_PACKAGED_DIST_INVENTORY_VERSION = { major: 2026, minor: 4, patch: 15 }; const OMITTED_PRIVATE_QA_BUNDLED_PLUGIN_ROOTS = new Set([ "dist/extensions/qa-channel", @@ -315,6 +316,31 @@ function applyCorepackDownloadPromptEnv(env: Record) { } } +function hasNpmScriptShellSetting(env: NodeJS.ProcessEnv): boolean { + return NPM_CONFIG_SCRIPT_SHELL_KEYS.some((key) => Boolean(env[key]?.trim())); +} + +function resolvePosixNpmScriptShell(env: NodeJS.ProcessEnv): string | null { + if (process.platform === "win32") { + return null; + } + if (fsSync.existsSync("/bin/sh")) { + return "/bin/sh"; + } + const shell = env.SHELL?.trim(); + return shell && path.isAbsolute(shell) && fsSync.existsSync(shell) ? shell : null; +} + +function applyPosixNpmScriptShellEnv(env: Record) { + if (hasNpmScriptShellSetting(env)) { + return; + } + const scriptShell = resolvePosixNpmScriptShell(env); + if (scriptShell) { + env.NPM_CONFIG_SCRIPT_SHELL = scriptShell; + } +} + export function resolveGlobalInstallSpec(params: { packageName: string; tag: string; @@ -344,8 +370,13 @@ export async function createGlobalInstallEnv( const hasCorepackDownloadPromptSetting = Boolean( sourceEnv.COREPACK_ENABLE_DOWNLOAD_PROMPT?.trim(), ); + const missingPosixScriptShell = + Boolean(resolvePosixNpmScriptShell(sourceEnv)) && !hasNpmScriptShellSetting(sourceEnv); const requiresMergedEnv = - pathPrepend.length > 0 || process.platform === "win32" || !hasCorepackDownloadPromptSetting; + pathPrepend.length > 0 || + process.platform === "win32" || + !hasCorepackDownloadPromptSetting || + missingPosixScriptShell; if (!requiresMergedEnv) { return env; } @@ -357,6 +388,7 @@ export async function createGlobalInstallEnv( applyPathPrepend(merged, pathPrepend); applyWindowsPackageInstallEnv(merged); applyCorepackDownloadPromptEnv(merged); + applyPosixNpmScriptShellEnv(merged); return merged; }