diff --git a/server/modules/websocket/services/shell-websocket.service.ts b/server/modules/websocket/services/shell-websocket.service.ts index d41b781f..55680b8c 100644 --- a/server/modules/websocket/services/shell-websocket.service.ts +++ b/server/modules/websocket/services/shell-websocket.service.ts @@ -171,6 +171,62 @@ function buildShellCommand( return command; } +function readEnvValue(env: NodeJS.ProcessEnv, key: string): string | undefined { + const resolvedKey = Object.keys(env).find((envKey) => envKey.toLowerCase() === key.toLowerCase()); + return resolvedKey ? env[resolvedKey] : undefined; +} + +function getPathEnvKey(env: NodeJS.ProcessEnv): string { + return Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH'; +} + +function prioritizeUserNpmGlobalBin(env: NodeJS.ProcessEnv): { key: string; value: string | undefined } { + const pathKey = getPathEnvKey(env); + const currentPath = env[pathKey]; + if (!currentPath) { + return { key: pathKey, value: currentPath }; + } + + const delimiter = path.delimiter; + const pathEntries = currentPath.split(delimiter).filter(Boolean); + const npmPrefix = readEnvValue(env, 'npm_config_prefix'); + const appData = readEnvValue(env, 'APPDATA'); + const candidates = [ + npmPrefix || '', + npmPrefix ? path.join(npmPrefix, 'bin') : '', + appData ? path.join(appData, 'npm') : '', + path.join(os.homedir(), 'AppData', 'Roaming', 'npm'), + path.join(os.homedir(), '.npm-global', 'bin'), + ].filter(Boolean); + + const normalizedPathEntries = pathEntries.map((entry) => os.platform() === 'win32' ? entry.toLowerCase() : entry); + const preferredEntries = candidates.filter((candidate, index) => { + const normalizedCandidate = os.platform() === 'win32' ? candidate.toLowerCase() : candidate; + return ( + candidates.indexOf(candidate) === index && + normalizedPathEntries.includes(normalizedCandidate) + ); + }); + + if (preferredEntries.length === 0) { + return { key: pathKey, value: currentPath }; + } + + const normalizedPreferredEntries = preferredEntries.map((entry) => + os.platform() === 'win32' ? entry.toLowerCase() : entry + ); + + const value = [ + ...preferredEntries, + ...pathEntries.filter((entry) => { + const normalizedEntry = os.platform() === 'win32' ? entry.toLowerCase() : entry; + return !normalizedPreferredEntries.includes(normalizedEntry); + }), + ].join(delimiter); + + return { key: pathKey, value }; +} + /** * Handles websocket connections used by the standalone shell terminal UI. */ @@ -284,6 +340,7 @@ export function handleShellConnection( os.platform() === 'win32' ? ['-Command', shellCommand] : ['-c', shellCommand]; const termCols = readNumber(data.cols, 80); const termRows = readNumber(data.rows, 24); + const prioritizedPath = prioritizeUserNpmGlobalBin(process.env); shellProcess = pty.spawn(shell, shellArgs, { name: 'xterm-256color', @@ -292,6 +349,7 @@ export function handleShellConnection( cwd: resolvedProjectPath, env: { ...process.env, + [prioritizedPath.key]: prioritizedPath.value, TERM: 'xterm-256color', COLORTERM: 'truecolor', FORCE_COLOR: '3',