/** * This fixes https://github.com/google/zx/issues/230 */ import { isWin } from '@/helpers/system'; import { execFileSync } from 'child_process'; import { userInfo } from 'os'; import process from 'process'; import stripAnsi from 'strip-ansi'; const defaultShell = (() => { const { env, platform } = process; if (platform === 'win32') { return env.COMSPEC ?? 'pwsh.exe'; // 'cmd.exe'; } try { const { shell } = userInfo(); if (shell) { return shell; } } catch (_error: unknown) { // userInfo may throw in some environments; ignore and fallback to defaults void _error; } if (platform === 'darwin') { return env.SHELL ?? '/bin/zsh'; } return env.SHELL ?? '/bin/sh'; })(); const arguments_ = ['-ilc', 'echo -n "_SHELL_ENV_DELIMITER_"; env; echo -n "_SHELL_ENV_DELIMITER_"; exit']; const environment = { // Disables Oh My Zsh auto-update thing that can block the process. DISABLE_AUTO_UPDATE: 'true', }; const parseEnvironment = (environment_: string): Record => { environment_ = environment_.split('_SHELL_ENV_DELIMITER_')[1]; const returnValue: Record = {}; for (const line of stripAnsi(environment_).split('\n').filter(Boolean)) { const [key, ...values] = line.split('='); returnValue[key] = values.join('='); } return returnValue; }; /** * Validates that a shell path is safe to execute * Only allows absolute paths without shell metacharacters */ function validateShellPath(shellPath: string): boolean { // Must be an absolute path if (!shellPath.startsWith('/')) { return false; } // Must not contain shell metacharacters that could enable injection const dangerousChars = /[;&|`$(){}[\]<>'"\\]/; if (dangerousChars.test(shellPath)) { return false; } return true; } export function shellEnvironmentSync(shell?: string): NodeJS.ProcessEnv { if (isWin) { return process.env; } const shellToUse = shell ?? defaultShell; // Validate shell path to prevent command injection if (!validateShellPath(shellToUse)) { console.warn(`[fixPath] Invalid shell path rejected: ${shellToUse}`); return process.env; } try { // Use execFileSync to prevent argument injection - arguments are passed as array, not shell-interpolated string // This prevents shell metacharacters in arguments from being interpreted const stdout = execFileSync(shellToUse, arguments_, { env: environment, }); return parseEnvironment(String(stdout)); } catch (error) { if (shell === undefined) { return process.env; } else { throw error; } } } export function shellPathSync(): string | undefined { const { PATH } = shellEnvironmentSync(); return PATH; } export function fixPath(): void { if (isWin) { return; } process.env.PATH = shellPathSync() ?? ['./node_modules/.bin', '/.nodebrew/current/bin', '/usr/local/bin', process.env.PATH].join(':'); }