2026-01-12 08:33:28 +00:00
import { execFile } from "node:child_process" ;
2026-01-08 22:15:46 +00:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
2026-01-12 08:33:28 +00:00
import { promisify } from "node:util" ;
import { isSupportedNodeVersion } from "../infra/runtime-guard.js" ;
2026-01-08 22:15:46 +00:00
const VERSION_MANAGER_MARKERS = [
"/.nvm/" ,
"/.fnm/" ,
"/.volta/" ,
"/.asdf/" ,
"/.n/" ,
"/.nodenv/" ,
"/.nodebrew/" ,
"/nvs/" ,
] ;
2026-01-08 22:28:48 +00:00
function getPathModule ( platform : NodeJS.Platform ) {
return platform === "win32" ? path.win32 : path.posix ;
}
2026-02-17 10:01:37 -05:00
function isNodeExecPath ( execPath : string , platform : NodeJS.Platform ) : boolean {
const pathModule = getPathModule ( platform ) ;
const base = pathModule . basename ( execPath ) . toLowerCase ( ) ;
return base === "node" || base === "node.exe" ;
}
2026-01-08 22:15:46 +00:00
function normalizeForCompare ( input : string , platform : NodeJS.Platform ) : string {
2026-01-08 22:28:48 +00:00
const pathModule = getPathModule ( platform ) ;
const normalized = pathModule . normalize ( input ) . replaceAll ( "\\" , "/" ) ;
2026-01-08 22:15:46 +00:00
if ( platform === "win32" ) {
2026-01-08 22:28:48 +00:00
return normalized . toLowerCase ( ) ;
2026-01-08 22:15:46 +00:00
}
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" ) {
2026-01-08 22:28:48 +00:00
const pathModule = getPathModule ( platform ) ;
2026-01-08 22:15:46 +00:00
const programFiles = env . ProgramFiles ? ? "C:\\Program Files" ;
2026-01-14 14:31:43 +00:00
const programFilesX86 = env [ "ProgramFiles(x86)" ] ? ? "C:\\Program Files (x86)" ;
2026-01-08 22:15:46 +00:00
return [
2026-01-08 22:28:48 +00:00
pathModule . join ( programFiles , "nodejs" , "node.exe" ) ,
pathModule . join ( programFilesX86 , "nodejs" , "node.exe" ) ,
2026-01-08 22:15:46 +00:00
] ;
}
return [ ] ;
}
2026-01-12 08:33:28 +00:00
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 {
2026-01-14 14:31:43 +00:00
const { stdout } = await execFileImpl ( nodePath , [ "-p" , "process.versions.node" ] , {
encoding : "utf8" ,
} ) ;
2026-01-12 08:33:28 +00:00
const value = stdout . trim ( ) ;
return value ? value : null ;
} catch {
return null ;
}
}
export type SystemNodeInfo = {
path : string ;
version : string | null ;
supported : boolean ;
} ;
2026-01-08 22:15:46 +00:00
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 ;
}
2026-01-12 08:33:28 +00:00
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 ) ;
2026-01-31 16:19:20 +09:00
if ( ! systemNode ) {
return null ;
}
2026-01-12 08:33:28 +00:00
2026-01-14 14:31:43 +00:00
const version = await resolveNodeVersion ( systemNode , params . execFile ? ? execFileAsync ) ;
2026-01-12 08:33:28 +00:00
return {
path : systemNode ,
version ,
supported : isSupportedNodeVersion ( version ) ,
} ;
}
export function renderSystemNodeWarning (
systemNode : SystemNodeInfo | null ,
selectedNodePath? : string ,
) : string | null {
2026-01-31 16:19:20 +09:00
if ( ! systemNode || systemNode . supported ) {
return null ;
}
2026-01-12 08:33:28 +00:00
const versionLabel = systemNode . version ? ? "unknown" ;
2026-01-14 14:31:43 +00:00
const selectedLabel = selectedNodePath ? ` Using ${ selectedNodePath } for the daemon. ` : "" ;
2026-01-12 08:33:28 +00:00
return ` System Node ${ versionLabel } at ${ systemNode . path } is below the required Node 22+. ${ selectedLabel } Install Node 22+ from nodejs.org or Homebrew. ` ;
}
2026-01-08 22:15:46 +00:00
export async function resolvePreferredNodePath ( params : {
env? : Record < string , string | undefined > ;
runtime? : string ;
2026-01-12 08:33:28 +00:00
platform? : NodeJS.Platform ;
execFile? : ExecFileAsync ;
2026-02-16 22:03:53 +08:00
execPath? : string ;
2026-01-08 22:15:46 +00:00
} ) : Promise < string | undefined > {
2026-01-31 16:19:20 +09:00
if ( params . runtime !== "node" ) {
return undefined ;
}
2026-02-16 22:03:53 +08:00
// Prefer the node that is currently running `openclaw gateway install`.
// This respects the user's active version manager (fnm/nvm/volta/etc.).
2026-02-17 10:01:37 -05:00
const platform = params . platform ? ? process . platform ;
2026-02-16 22:03:53 +08:00
const currentExecPath = params . execPath ? ? process . execPath ;
2026-02-17 10:01:37 -05:00
if ( currentExecPath && isNodeExecPath ( currentExecPath , platform ) ) {
2026-02-16 22:03:53 +08:00
const execFileImpl = params . execFile ? ? execFileAsync ;
const version = await resolveNodeVersion ( currentExecPath , execFileImpl ) ;
if ( isSupportedNodeVersion ( version ) ) {
return currentExecPath ;
}
}
// Fall back to system node.
2026-01-12 08:33:28 +00:00
const systemNode = await resolveSystemNodeInfo ( params ) ;
2026-01-31 16:19:20 +09:00
if ( ! systemNode ? . supported ) {
return undefined ;
}
2026-01-12 08:33:28 +00:00
return systemNode . path ;
2026-01-08 22:15:46 +00:00
}