import fs from 'fs'; import os from 'os'; import path from 'path'; const getEnvValue = (env, keys) => { for (const key of keys) { const value = env?.[key]; if (typeof value === 'string' && value.trim().length > 0) { return value; } } return ''; }; const normalizeSearchDirectoryKey = (directory, platform) => { const trimmed = typeof directory === 'string' ? directory.trim() : ''; return platform === 'win32' ? trimmed.toLowerCase() : trimmed; }; const getWindowsAppsDirectory = (env) => { const localAppData = getEnvValue(env, ['LOCALAPPDATA', 'LocalAppData', 'localappdata']); if (localAppData) { return path.win32.join(localAppData, 'Microsoft', 'WindowsApps'); } const userProfile = getEnvValue(env, ['USERPROFILE', 'UserProfile', 'userprofile']); if (userProfile) { return path.win32.join(userProfile, 'AppData', 'Local', 'Microsoft', 'WindowsApps'); } return path.win32.join(os.homedir(), 'AppData', 'Local', 'Microsoft', 'WindowsApps'); }; export function getExecutableSearchDirectories({ env = process.env, platform = process.platform } = {}) { const delimiter = platform === 'win32' ? ';' : ':'; const pathValue = getEnvValue(env, ['PATH', 'Path', 'path']); const directories = pathValue.split(delimiter).map((entry) => entry.trim()).filter(Boolean); if (platform === 'win32') { directories.push(getWindowsAppsDirectory(env)); } const seen = new Set(); const unique = []; for (const directory of directories) { const key = normalizeSearchDirectoryKey(directory, platform); if (!key || seen.has(key)) { continue; } seen.add(key); unique.push(directory); } return unique; } export function createExecutableSearchEnv({ env = process.env, platform = process.platform } = {}) { const delimiter = platform === 'win32' ? ';' : ':'; const pathValue = getExecutableSearchDirectories({ env, platform }).join(delimiter); const nextEnv = { ...env }; if (platform === 'win32') { nextEnv.PATH = pathValue; nextEnv.Path = pathValue; nextEnv.path = pathValue; } else { nextEnv.PATH = pathValue; } return nextEnv; } const getExecutableExtensions = ({ env = process.env, platform = process.platform } = {}) => { if (platform !== 'win32') { return ['']; } return (env.PATHEXT || env.PathExt || env.pathext || '.EXE;.CMD;.BAT;.COM') .split(';') .map((ext) => ext.trim().toLowerCase()) .filter(Boolean) .map((ext) => (ext.startsWith('.') ? ext : `.${ext}`)); }; export function findExecutableOnPath(command, { env = process.env, platform = process.platform, fsLike = fs, } = {}) { if (typeof command !== 'string' || command.trim().length === 0) { return null; } const pathApi = platform === 'win32' ? path.win32 : path; const directories = getExecutableSearchDirectories({ env, platform }); const extensions = getExecutableExtensions({ env, platform }); const commandName = command.trim(); for (const directory of directories) { for (const extension of extensions) { const fileName = platform === 'win32' ? `${commandName}${extension}` : commandName; const candidate = pathApi.join(directory, fileName); try { const stats = fsLike.statSync(candidate); if (!stats.isFile()) { continue; } if (platform !== 'win32') { try { fsLike.accessSync(candidate, fs.constants.X_OK); } catch { continue; } } return candidate; } catch { continue; } } } return null; } export function resolveExecutableLaunchTarget(command, options = {}) { const platform = options.platform || process.platform; const resolvedPath = findExecutableOnPath(command, { ...options, platform }); const env = createExecutableSearchEnv({ env: options.env || process.env, platform }); if (resolvedPath) { return { command: resolvedPath, env }; } // Windows Store app execution aliases are launchable through CreateProcess // but can reject fs.stat/fs.access with EACCES. Let the version probe decide. if (platform === 'win32' && typeof command === 'string' && command.trim().length > 0) { return { command: command.trim(), env }; } return null; }