fix node npm detection (#1163)

This commit is contained in:
ekko
2026-05-30 20:19:01 +08:00
committed by GitHub
parent dcbf601e35
commit fc35c74eb3
18 changed files with 406 additions and 53 deletions
+102 -3
View File
@@ -1,13 +1,16 @@
import { ChildProcess, spawn } from 'node:child_process'
import { mkdirSync, readFileSync, writeFileSync, chmodSync, existsSync } from 'node:fs'
import { ChildProcess, execFile, spawn } from 'node:child_process'
import { mkdirSync, readFileSync, writeFileSync, chmodSync, existsSync, readdirSync } from 'node:fs'
import { createServer } from 'node:net'
import { homedir } from 'node:os'
import { dirname, delimiter, join } from 'node:path'
import { randomBytes } from 'node:crypto'
import { promisify } from 'node:util'
import { app } from 'electron'
import { webuiServerEntry, webuiDir, hermesBin, webUiHome, hermesHome, tokenFile, pythonDir } from './paths'
const DEFAULT_PORT = 8748
const READY_TIMEOUT_MS = 30_000
const execFileAsync = promisify(execFile)
let serverProc: ChildProcess | null = null
let cachedToken: string | null = null
@@ -43,6 +46,93 @@ function ensureNativeModules() {
}
}
const COMMON_USER_BIN_DIRS = process.platform === 'win32'
? []
: [
'/opt/homebrew/bin',
'/usr/local/bin',
'/usr/bin',
'/bin',
'/usr/sbin',
'/sbin',
]
const PATH_MARKER_START = '__HERMES_DESKTOP_PATH_START__'
const PATH_MARKER_END = '__HERMES_DESKTOP_PATH_END__'
function mergePathEntries(...paths: Array<string | undefined | null>): string {
const seen = new Set<string>()
const entries: string[] = []
for (const rawPath of paths) {
if (!rawPath) continue
for (const entry of rawPath.split(delimiter)) {
const trimmed = entry.trim()
if (!trimmed) continue
const key = process.platform === 'win32' ? trimmed.toLowerCase() : trimmed
if (seen.has(key)) continue
seen.add(key)
entries.push(trimmed)
}
}
return entries.join(delimiter)
}
function extractMarkedPath(output: string): string | null {
const start = output.lastIndexOf(PATH_MARKER_START)
const end = output.lastIndexOf(PATH_MARKER_END)
if (start < 0 || end <= start) return null
const value = output.slice(start + PATH_MARKER_START.length, end).trim()
return value || null
}
function compareNodeVersionDesc(left: string, right: string): number {
const leftParts = left.replace(/^v/, '').split('.').map(part => Number.parseInt(part, 10) || 0)
const rightParts = right.replace(/^v/, '').split('.').map(part => Number.parseInt(part, 10) || 0)
for (let index = 0; index < Math.max(leftParts.length, rightParts.length); index += 1) {
const diff = (rightParts[index] || 0) - (leftParts[index] || 0)
if (diff !== 0) return diff
}
return right.localeCompare(left)
}
function getNvmNodeBinPaths(): string {
if (process.platform === 'win32') return ''
const nvmDir = process.env.NVM_DIR?.trim() || join(homedir(), '.nvm')
const versionsDir = join(nvmDir, 'versions', 'node')
if (!existsSync(versionsDir)) return ''
try {
return readdirSync(versionsDir, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
.sort(compareNodeVersionDesc)
.map(version => join(versionsDir, version, 'bin'))
.filter(binDir => existsSync(binDir))
.join(delimiter)
} catch {
return ''
}
}
async function getLoginShellPath(): Promise<string | null> {
if (process.platform === 'win32') return null
const shell = process.env.SHELL?.trim() || (process.platform === 'darwin' ? '/bin/zsh' : '/bin/sh')
if (!existsSync(shell)) return null
try {
const { stdout } = await execFileAsync(shell, ['-l', '-c', `printf '\\n${PATH_MARKER_START}%s${PATH_MARKER_END}\\n' "$PATH"`], {
encoding: 'utf-8',
timeout: 1500,
windowsHide: true,
env: process.env,
})
return extractMarkedPath(stdout) || stdout.trim() || null
} catch {
return null
}
}
export function getToken(): string {
return ensureToken()
}
@@ -111,6 +201,15 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
: join(pythonDir(), 'bin', 'python3')
const bridgePort = await getFreeTcpPort()
const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
const loginShellPath = await getLoginShellPath()
const nvmNodeBinPaths = getNvmNodeBinPaths()
const runtimePath = mergePathEntries(
dirname(hermesBin()),
loginShellPath,
nvmNodeBinPaths,
process.env.PATH,
COMMON_USER_BIN_DIRS.join(delimiter),
)
// Run via Electron's "run as Node" mode — Electron binary doubles as Node.
const env: NodeJS.ProcessEnv = {
@@ -151,7 +250,7 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
AUTH_TOKEN: token,
PORT: String(port),
// Prepend bundled Python's bin to PATH so any incidental `python` resolution lands on ours
PATH: [dirname(hermesBin()), process.env.PATH].filter(Boolean).join(delimiter),
PATH: runtimePath,
}
serverProc = spawn(process.execPath, [entry], {