Spaces:
Configuration error
Configuration error
| 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<string, string | undefined>, | |
| 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<string | null> { | |
| 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<string, string | undefined> = 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<string, string | undefined> = process.env, | |
| platform: NodeJS.Platform = process.platform, | |
| ): Promise<string | null> { | |
| 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<string, string | undefined>; | |
| platform?: NodeJS.Platform; | |
| execFile?: ExecFileAsync; | |
| }): Promise<SystemNodeInfo | null> { | |
| 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<string, string | undefined>; | |
| runtime?: string; | |
| platform?: NodeJS.Platform; | |
| execFile?: ExecFileAsync; | |
| }): Promise<string | undefined> { | |
| if (params.runtime !== "node") return undefined; | |
| const systemNode = await resolveSystemNodeInfo(params); | |
| if (!systemNode?.supported) return undefined; | |
| return systemNode.path; | |
| } | |