import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { promisify } from "node:util"; import { isSupportedNodeVersion } from "../infra/runtime-guard.js"; const VERSION_MANAGER_MARKERS = [ "/.nvm/", "/.fnm/", "/.volta/", "/.asdf/", "/.n/", "/.nodenv/", "/.nodebrew/", "/nvs/", ]; function getPathModule(platform: NodeJS.Platform) { return platform === "win32" ? path.win32 : path.posix; } function normalizeForCompare(input: string, platform: NodeJS.Platform): string { const pathModule = getPathModule(platform); const normalized = pathModule.normalize(input).replaceAll("\\", "/"); if (platform === "win32") { return normalized.toLowerCase(); } return normalized; } function buildSystemNodeCandidates( env: Record, platform: NodeJS.Platform, ): string[] { if (platform === "darwin") { return ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"]; } if (platform === "linux") { return ["/usr/local/bin/node", "/usr/bin/node"]; } if (platform === "win32") { const pathModule = getPathModule(platform); const programFiles = env.ProgramFiles ?? "C:\\Program Files"; const programFilesX86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)"; return [ pathModule.join(programFiles, "nodejs", "node.exe"), pathModule.join(programFilesX86, "nodejs", "node.exe"), ]; } return []; } type ExecFileAsync = ( file: string, args: readonly string[], options: { encoding: "utf8" }, ) => Promise<{ stdout: string; stderr: string }>; const execFileAsync = promisify(execFile) as unknown as ExecFileAsync; async function resolveNodeVersion( nodePath: string, execFileImpl: ExecFileAsync, ): Promise { try { const { stdout } = await execFileImpl(nodePath, ["-p", "process.versions.node"], { encoding: "utf8", }); const value = stdout.trim(); return value ? value : null; } catch { return null; } } export type SystemNodeInfo = { path: string; version: string | null; supported: boolean; }; export function isVersionManagedNodePath( nodePath: string, platform: NodeJS.Platform = process.platform, ): boolean { const normalized = normalizeForCompare(nodePath, platform); return VERSION_MANAGER_MARKERS.some((marker) => normalized.includes(marker)); } export function isSystemNodePath( nodePath: string, env: Record = process.env, platform: NodeJS.Platform = process.platform, ): boolean { const normalized = normalizeForCompare(nodePath, platform); return buildSystemNodeCandidates(env, platform).some((candidate) => { const normalizedCandidate = normalizeForCompare(candidate, platform); return normalized === normalizedCandidate; }); } export async function resolveSystemNodePath( env: Record = process.env, platform: NodeJS.Platform = process.platform, ): Promise { const candidates = buildSystemNodeCandidates(env, platform); for (const candidate of candidates) { try { await fs.access(candidate); return candidate; } catch { // keep going } } return null; } export async function resolveSystemNodeInfo(params: { env?: Record; platform?: NodeJS.Platform; execFile?: ExecFileAsync; }): Promise { const env = params.env ?? process.env; const platform = params.platform ?? process.platform; const systemNode = await resolveSystemNodePath(env, platform); if (!systemNode) return null; const version = await resolveNodeVersion(systemNode, params.execFile ?? execFileAsync); return { path: systemNode, version, supported: isSupportedNodeVersion(version), }; } export function renderSystemNodeWarning( systemNode: SystemNodeInfo | null, selectedNodePath?: string, ): string | null { if (!systemNode || systemNode.supported) return null; const versionLabel = systemNode.version ?? "unknown"; const selectedLabel = selectedNodePath ? ` Using ${selectedNodePath} for the daemon.` : ""; return `System Node ${versionLabel} at ${systemNode.path} is below the required Node 22+.${selectedLabel} Install Node 22+ from nodejs.org or Homebrew.`; } export async function resolvePreferredNodePath(params: { env?: Record; runtime?: string; platform?: NodeJS.Platform; execFile?: ExecFileAsync; }): Promise { if (params.runtime !== "node") return undefined; const systemNode = await resolveSystemNodeInfo(params); if (!systemNode?.supported) return undefined; return systemNode.path; }