| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { execSync } from 'child_process'; |
| import path from 'path'; |
| import os from 'os'; |
| import { |
| systemPathExists, |
| systemPathIsExecutable, |
| systemPathReaddirSync, |
| systemPathReadFileSync, |
| getNvmPaths, |
| getFnmPaths, |
| getNodeSystemPaths, |
| getScoopNodePath, |
| getChocolateyNodePath, |
| getWslVersionPath, |
| } from './system-paths.js'; |
|
|
| |
| const VERSION_DIR_PATTERN = /^v?\d+/; |
|
|
| |
| const PRE_RELEASE_PATTERN = /-(beta|rc|alpha|nightly|canary|dev|pre)/i; |
|
|
| |
| export interface NodeFinderResult { |
| |
| nodePath: string; |
| |
| source: |
| | 'homebrew' |
| | 'system' |
| | 'nvm' |
| | 'fnm' |
| | 'nvm-windows' |
| | 'program-files' |
| | 'scoop' |
| | 'chocolatey' |
| | 'which' |
| | 'where' |
| | 'fallback'; |
| } |
|
|
| |
| export interface NodeFinderOptions { |
| |
| skipSearch?: boolean; |
| |
| logger?: (message: string) => void; |
| } |
|
|
| |
| |
| |
| |
| function isExecutable(filePath: string): boolean { |
| try { |
| return systemPathIsExecutable(filePath); |
| } catch { |
| return false; |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function findNodeFromVersionManager( |
| basePath: string, |
| binSubpath: string = 'bin/node' |
| ): string | null { |
| try { |
| if (!systemPathExists(basePath)) return null; |
| } catch { |
| return null; |
| } |
|
|
| try { |
| const allVersions = systemPathReaddirSync(basePath) |
| .filter((v) => VERSION_DIR_PATTERN.test(v)) |
| |
| .sort((a, b) => b.localeCompare(a, undefined, { numeric: true, sensitivity: 'base' })); |
|
|
| |
| const stableVersions = allVersions.filter((v) => !PRE_RELEASE_PATTERN.test(v)); |
| const preReleaseVersions = allVersions.filter((v) => PRE_RELEASE_PATTERN.test(v)); |
|
|
| |
| for (const version of [...stableVersions, ...preReleaseVersions]) { |
| const nodePath = path.join(basePath, version, binSubpath); |
| if (isExecutable(nodePath)) { |
| return nodePath; |
| } |
| } |
| } catch { |
| |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| function findNodeMacOS(_homeDir: string): NodeFinderResult | null { |
| |
| const systemPaths = getNodeSystemPaths(); |
| for (const nodePath of systemPaths) { |
| if (isExecutable(nodePath)) { |
| |
| if (nodePath.includes('homebrew') || nodePath === '/usr/local/bin/node') { |
| return { nodePath, source: 'homebrew' }; |
| } |
| return { nodePath, source: 'system' }; |
| } |
| } |
|
|
| |
| const nvmPaths = getNvmPaths(); |
| for (const nvmPath of nvmPaths) { |
| const nvmNode = findNodeFromVersionManager(nvmPath); |
| if (nvmNode) { |
| return { nodePath: nvmNode, source: 'nvm' }; |
| } |
| } |
|
|
| |
| const fnmPaths = getFnmPaths(); |
| for (const fnmBasePath of fnmPaths) { |
| const fnmNode = findNodeFromVersionManager(fnmBasePath); |
| if (fnmNode) { |
| return { nodePath: fnmNode, source: 'fnm' }; |
| } |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| function findNodeLinux(_homeDir: string): NodeFinderResult | null { |
| |
| const systemPaths = getNodeSystemPaths(); |
| for (const nodePath of systemPaths) { |
| if (isExecutable(nodePath)) { |
| return { nodePath, source: 'system' }; |
| } |
| } |
|
|
| |
| const nvmPaths = getNvmPaths(); |
| for (const nvmPath of nvmPaths) { |
| const nvmNode = findNodeFromVersionManager(nvmPath); |
| if (nvmNode) { |
| return { nodePath: nvmNode, source: 'nvm' }; |
| } |
| } |
|
|
| |
| const fnmPaths = getFnmPaths(); |
| for (const fnmBasePath of fnmPaths) { |
| const fnmNode = findNodeFromVersionManager(fnmBasePath); |
| if (fnmNode) { |
| return { nodePath: fnmNode, source: 'fnm' }; |
| } |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| function findNodeWindows(_homeDir: string): NodeFinderResult | null { |
| |
| const systemPaths = getNodeSystemPaths(); |
| for (const nodePath of systemPaths) { |
| if (isExecutable(nodePath)) { |
| return { nodePath, source: 'program-files' }; |
| } |
| } |
|
|
| |
| const nvmPaths = getNvmPaths(); |
| for (const nvmPath of nvmPaths) { |
| const nvmNode = findNodeFromVersionManager(nvmPath, 'node.exe'); |
| if (nvmNode) { |
| return { nodePath: nvmNode, source: 'nvm-windows' }; |
| } |
| } |
|
|
| |
| const fnmPaths = getFnmPaths(); |
| for (const fnmBasePath of fnmPaths) { |
| const fnmNode = findNodeFromVersionManager(fnmBasePath, 'node.exe'); |
| if (fnmNode) { |
| return { nodePath: fnmNode, source: 'fnm' }; |
| } |
| } |
|
|
| |
| const scoopPath = getScoopNodePath(); |
| if (isExecutable(scoopPath)) { |
| return { nodePath: scoopPath, source: 'scoop' }; |
| } |
|
|
| |
| const chocoPath = getChocolateyNodePath(); |
| if (isExecutable(chocoPath)) { |
| return { nodePath: chocoPath, source: 'chocolatey' }; |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| function findNodeViaShell( |
| platform: NodeJS.Platform, |
| logger: (message: string) => void = () => {} |
| ): NodeFinderResult | null { |
| try { |
| const command = platform === 'win32' ? 'where node' : 'which node'; |
| const result = execSync(command, { |
| encoding: 'utf8', |
| stdio: ['pipe', 'pipe', 'pipe'], |
| }).trim(); |
|
|
| |
| const nodePath = result.split(/\r?\n/)[0]; |
|
|
| |
| if (nodePath && !nodePath.includes('\x00') && isExecutable(nodePath)) { |
| return { |
| nodePath, |
| source: platform === 'win32' ? 'where' : 'which', |
| }; |
| } |
| } catch { |
| |
| logger('Shell command failed to find Node.js (expected when launched from desktop)'); |
| } |
|
|
| return null; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function findNodeExecutable(options: NodeFinderOptions = {}): NodeFinderResult { |
| const { skipSearch = false, logger = () => {} } = options; |
|
|
| |
| if (skipSearch) { |
| return { nodePath: 'node', source: 'fallback' }; |
| } |
|
|
| const platform = process.platform; |
| const homeDir = os.homedir(); |
|
|
| |
| let result: NodeFinderResult | null = null; |
|
|
| switch (platform) { |
| case 'darwin': |
| result = findNodeMacOS(homeDir); |
| break; |
| case 'linux': |
| result = findNodeLinux(homeDir); |
| break; |
| case 'win32': |
| result = findNodeWindows(homeDir); |
| break; |
| } |
|
|
| if (result) { |
| logger(`Found Node.js via ${result.source} at: ${result.nodePath}`); |
| return result; |
| } |
|
|
| |
| result = findNodeViaShell(platform, logger); |
| if (result) { |
| logger(`Found Node.js via ${result.source} at: ${result.nodePath}`); |
| return result; |
| } |
|
|
| |
| logger('Could not find Node.js, falling back to "node"'); |
| return { nodePath: 'node', source: 'fallback' }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function buildEnhancedPath(nodePath: string, currentPath: string = ''): string { |
| |
| if (nodePath === 'node') { |
| return currentPath; |
| } |
|
|
| const nodeDir = path.dirname(nodePath); |
|
|
| |
| |
| |
| const normalizedNodeDir = path.normalize(nodeDir); |
| const pathSegments = currentPath.split(path.delimiter).map((s) => path.normalize(s)); |
| if (normalizedNodeDir === '.' || pathSegments.includes(normalizedNodeDir)) { |
| return currentPath; |
| } |
|
|
| |
| |
| if (!currentPath) { |
| return nodeDir; |
| } |
| return `${nodeDir}${path.delimiter}${currentPath}`; |
| } |
|
|