mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix: reject incompatible Node 23 runtimes (#99832)
* fix: reject incompatible Node 23 runtimes * fix: repair installer CI coverage * docs: clarify supported Node ranges * fix: fail closed on unreadable runtime versions --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
a845c0e93e
commit
cccc856b82
21 changed files with 395 additions and 174 deletions
|
|
@ -316,7 +316,7 @@ OpenClaw's web interface (Gateway Control UI + HTTP endpoints) is intended for *
|
|||
|
||||
### Node.js Version
|
||||
|
||||
OpenClaw requires **Node.js 22.19.0 or later** (LTS). Node 24 is the recommended default runtime for new installs. The minimum version includes important security patches:
|
||||
OpenClaw requires **Node.js 22.19+, Node.js 23.11+, or Node.js 24+**. Node 24 is the recommended default runtime for new installs. The minimum supported Node 22 version includes important security patches:
|
||||
|
||||
- CVE-2025-59466: async_hooks DoS vulnerability
|
||||
- CVE-2026-21636: Permission model bypass vulnerability
|
||||
|
|
@ -324,7 +324,7 @@ OpenClaw requires **Node.js 22.19.0 or later** (LTS). Node 24 is the recommended
|
|||
Verify your Node.js version:
|
||||
|
||||
```bash
|
||||
node --version # Should be v22.19.0 or later
|
||||
node --version # Should be v22.19+, v23.11+, or v24+
|
||||
```
|
||||
|
||||
### Docker Security
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ enum RuntimeResolutionError: Error {
|
|||
case unsupported(
|
||||
kind: RuntimeKind,
|
||||
found: RuntimeVersion,
|
||||
required: RuntimeVersion,
|
||||
path: String,
|
||||
searchPaths: [String])
|
||||
case versionParse(kind: RuntimeKind, raw: String, path: String, searchPaths: [String])
|
||||
|
|
@ -54,7 +53,19 @@ enum RuntimeResolutionError: Error {
|
|||
|
||||
enum RuntimeLocator {
|
||||
private static let logger = Logger(subsystem: "ai.openclaw", category: "runtime")
|
||||
private static let minNode = RuntimeVersion(major: 22, minor: 19, patch: 0)
|
||||
private static let minNode22 = RuntimeVersion(major: 22, minor: 19, patch: 0)
|
||||
private static let minNode23 = RuntimeVersion(major: 23, minor: 11, patch: 0)
|
||||
private static let supportedNodeRange = ">=22.19.0 <23 or >=23.11.0"
|
||||
|
||||
static func isSupportedNodeVersion(_ version: RuntimeVersion) -> Bool {
|
||||
if version.major == self.minNode22.major {
|
||||
return version >= self.minNode22
|
||||
}
|
||||
if version.major == self.minNode23.major {
|
||||
return version >= self.minNode23
|
||||
}
|
||||
return version.major > self.minNode23.major
|
||||
}
|
||||
|
||||
static func resolve(
|
||||
searchPaths: [String] = CommandResolver.preferredPaths()) -> Result<RuntimeResolution, RuntimeResolutionError>
|
||||
|
|
@ -75,11 +86,10 @@ enum RuntimeLocator {
|
|||
guard let parsed = RuntimeVersion.from(string: rawVersion) else {
|
||||
return .failure(.versionParse(kind: runtime, raw: rawVersion, path: binary, searchPaths: searchPaths))
|
||||
}
|
||||
guard parsed >= self.minNode else {
|
||||
guard self.isSupportedNodeVersion(parsed) else {
|
||||
return .failure(.unsupported(
|
||||
kind: runtime,
|
||||
found: parsed,
|
||||
required: self.minNode,
|
||||
path: binary,
|
||||
searchPaths: searchPaths))
|
||||
}
|
||||
|
|
@ -91,13 +101,13 @@ enum RuntimeLocator {
|
|||
switch error {
|
||||
case let .notFound(searchPaths):
|
||||
[
|
||||
"openclaw needs Node >=22.19.0 but found no runtime.",
|
||||
"openclaw needs Node \(self.supportedNodeRange) but found no runtime.",
|
||||
"PATH searched: \(searchPaths.joined(separator: ":"))",
|
||||
"Install Node: https://nodejs.org/en/download",
|
||||
].joined(separator: "\n")
|
||||
case let .unsupported(kind, found, required, path, searchPaths):
|
||||
case let .unsupported(kind, found, path, searchPaths):
|
||||
[
|
||||
"Found \(kind.rawValue) \(found) at \(path) but need >= \(required).",
|
||||
"Found \(kind.rawValue) \(found) at \(path) but need \(self.supportedNodeRange).",
|
||||
"PATH searched: \(searchPaths.joined(separator: ":"))",
|
||||
"Upgrade Node and rerun openclaw.",
|
||||
].joined(separator: "\n")
|
||||
|
|
@ -105,7 +115,7 @@ enum RuntimeLocator {
|
|||
[
|
||||
"Could not parse \(kind.rawValue) version output \"\(raw)\" from \(path).",
|
||||
"PATH searched: \(searchPaths.joined(separator: ":"))",
|
||||
"Try reinstalling or pinning a supported version (Node >=22.19.0).",
|
||||
"Try reinstalling or pinning a supported version (Node \(self.supportedNodeRange)).",
|
||||
].joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,15 +35,44 @@ struct RuntimeLocatorTests {
|
|||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
guard case let .failure(.unsupported(_, found, required, path, _)) = result else {
|
||||
guard case let .failure(.unsupported(_, found, path, _)) = result else {
|
||||
Issue.record("Expected unsupported error, got \(result)")
|
||||
return
|
||||
}
|
||||
#expect(found == RuntimeVersion(major: 22, minor: 18, patch: 9))
|
||||
#expect(required == RuntimeVersion(major: 22, minor: 19, patch: 0))
|
||||
#expect(path == node.path)
|
||||
}
|
||||
|
||||
@Test func `resolve rejects early node 23`() throws {
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
echo v23.7.0
|
||||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
guard case let .failure(.unsupported(_, found, path, _)) = result else {
|
||||
Issue.record("Expected unsupported error, got \(result)")
|
||||
return
|
||||
}
|
||||
#expect(found == RuntimeVersion(major: 23, minor: 7, patch: 0))
|
||||
#expect(path == node.path)
|
||||
}
|
||||
|
||||
@Test func `resolve accepts node 23 with statement columns`() throws {
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
echo v23.11.0
|
||||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
guard case let .success(res) = result else {
|
||||
Issue.record("Expected success, got \(result)")
|
||||
return
|
||||
}
|
||||
#expect(res.path == node.path)
|
||||
#expect(res.version == RuntimeVersion(major: 23, minor: 11, patch: 0))
|
||||
}
|
||||
|
||||
@Test func `resolve fails when too old`() throws {
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
|
|
@ -51,7 +80,7 @@ struct RuntimeLocatorTests {
|
|||
"""
|
||||
let node = try self.makeTempExecutable(contents: script)
|
||||
let result = RuntimeLocator.resolve(searchPaths: [node.deletingLastPathComponent().path])
|
||||
guard case let .failure(.unsupported(_, found, _, path, _)) = result else {
|
||||
guard case let .failure(.unsupported(_, found, path, _)) = result else {
|
||||
Issue.record("Expected unsupported error, got \(result)")
|
||||
return
|
||||
}
|
||||
|
|
@ -76,7 +105,7 @@ struct RuntimeLocatorTests {
|
|||
|
||||
@Test func `describe failure includes paths`() {
|
||||
let msg = RuntimeLocator.describeFailure(.notFound(searchPaths: ["/tmp/a", "/tmp/b"]))
|
||||
#expect(msg.contains("Node >=22.19.0"))
|
||||
#expect(msg.contains("Node >=22.19.0 <23 or >=23.11.0"))
|
||||
#expect(msg.contains("PATH searched: /tmp/a:/tmp/b"))
|
||||
|
||||
let parseMsg = RuntimeLocator.describeFailure(
|
||||
|
|
@ -85,7 +114,7 @@ struct RuntimeLocatorTests {
|
|||
raw: "garbage",
|
||||
path: "/usr/local/bin/node",
|
||||
searchPaths: ["/usr/local/bin"]))
|
||||
#expect(parseMsg.contains("Node >=22.19.0"))
|
||||
#expect(parseMsg.contains("Node >=22.19.0 <23 or >=23.11.0"))
|
||||
}
|
||||
|
||||
@Test func `runtime version parses with leading V and metadata`() {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ read_when:
|
|||
- "npm install -g fails with permissions or PATH issues"
|
||||
---
|
||||
|
||||
OpenClaw requires **Node 22.19 or newer**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically - this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
|
||||
OpenClaw requires **Node 22.19+, Node 23.11+, or Node 24+**. **Node 24 is the default and recommended runtime** for installs, CI, and release workflows. Node 22 remains supported via the active LTS line. The [installer script](/install#alternative-install-methods) will detect and install Node automatically - this page is for when you want to set up Node yourself and make sure everything is wired up correctly (versions, PATH, global installs).
|
||||
|
||||
## Check your version
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ OpenClaw requires **Node 22.19 or newer**. **Node 24 is the default and recommen
|
|||
node -v
|
||||
```
|
||||
|
||||
If this prints `v24.x.x` or higher, you're on the recommended default. If it prints `v22.19.x` or higher, you're on the supported Node 22 LTS path, but we still recommend upgrading to Node 24 when convenient. If Node isn't installed or the version is too old, pick an install method below.
|
||||
If this prints `v24.x.x` or higher, you're on the recommended default. If it prints `v22.19.x` or higher, you're on the supported Node 22 LTS path, but we still recommend upgrading to Node 24 when convenient. Node 23 versions before `v23.11.0` are unsupported. If Node isn't installed or the version is outside the supported range, pick an install method below.
|
||||
|
||||
## Install Node
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Bare package specs still install from npm during the launch cutover. Use the
|
|||
|
||||
## Requirements
|
||||
|
||||
- Use Node 22.19 or newer and a package manager such as `npm` or `pnpm`.
|
||||
- Use Node 22.19+, Node 23.11+, or Node 24+ and a package manager such as `npm` or `pnpm`.
|
||||
- Be familiar with TypeScript ESM modules.
|
||||
- For in-repo bundled plugin work, clone the repository and run `pnpm install`.
|
||||
Source-checkout plugin development is pnpm-only because OpenClaw loads bundled
|
||||
|
|
|
|||
2
npm-shrinkwrap.json
generated
2
npm-shrinkwrap.json
generated
|
|
@ -69,7 +69,7 @@
|
|||
"openclaw": "openclaw.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"node": ">=22.19.0 <23 || >=23.11.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"sqlite-vec": "0.1.9"
|
||||
|
|
|
|||
24
openclaw.mjs
24
openclaw.mjs
|
|
@ -10,7 +10,9 @@ import { fileURLToPath } from "node:url";
|
|||
|
||||
const MIN_NODE_MAJOR = 22;
|
||||
const MIN_NODE_MINOR = 19;
|
||||
const MIN_NODE_VERSION = `${MIN_NODE_MAJOR}.${MIN_NODE_MINOR}`;
|
||||
const MIN_NODE_23_MINOR = 11;
|
||||
const RECOMMENDED_NODE_MAJOR = 24;
|
||||
const SUPPORTED_NODE_RANGE = ">=22.19.0 <23 or >=23.11.0";
|
||||
const MIN_COMPILE_CACHE_NODE_24_MINOR = 15;
|
||||
const COMPILE_CACHE_DISABLED_RESPAWNED_ENV = "OPENCLAW_COMPILE_CACHE_DISABLED_RESPAWNED";
|
||||
|
||||
|
|
@ -22,9 +24,15 @@ const parseNodeVersion = (rawVersion) => {
|
|||
};
|
||||
};
|
||||
|
||||
const isSupportedNodeVersion = (version) =>
|
||||
version.major > MIN_NODE_MAJOR ||
|
||||
(version.major === MIN_NODE_MAJOR && version.minor >= MIN_NODE_MINOR);
|
||||
const isSupportedNodeVersion = (version) => {
|
||||
if (version.major === MIN_NODE_MAJOR) {
|
||||
return version.minor >= MIN_NODE_MINOR;
|
||||
}
|
||||
if (version.major === 23) {
|
||||
return version.minor >= MIN_NODE_23_MINOR;
|
||||
}
|
||||
return version.major > 23;
|
||||
};
|
||||
|
||||
const isNodeVersionAffectedByCompileCacheDeadlock = (rawVersion) => {
|
||||
const version = parseNodeVersion(rawVersion);
|
||||
|
|
@ -41,11 +49,11 @@ const ensureSupportedNodeVersion = () => {
|
|||
}
|
||||
|
||||
process.stderr.write(
|
||||
`openclaw: Node.js v${MIN_NODE_VERSION}+ is required (current: v${process.versions.node}).\n` +
|
||||
`openclaw: Node.js ${SUPPORTED_NODE_RANGE} is required (current: v${process.versions.node}).\n` +
|
||||
"If you use nvm, run:\n" +
|
||||
` nvm install ${MIN_NODE_MAJOR}\n` +
|
||||
` nvm use ${MIN_NODE_MAJOR}\n` +
|
||||
` nvm alias default ${MIN_NODE_MAJOR}\n`,
|
||||
` nvm install ${RECOMMENDED_NODE_MAJOR}\n` +
|
||||
` nvm use ${RECOMMENDED_NODE_MAJOR}\n` +
|
||||
` nvm alias default ${RECOMMENDED_NODE_MAJOR}\n`,
|
||||
);
|
||||
process.exit(1);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2060,7 +2060,7 @@
|
|||
"sqlite-vec": "0.1.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.19.0"
|
||||
"node": ">=22.19.0 <23 || >=23.11.0"
|
||||
},
|
||||
"packageManager": "pnpm@11.2.2+sha512.36e6621fad506178936455e70247b8808ef4ec25797a9f437a93281a020484e2607f6a469a22e982987c3dbb8866e3071514ab10a4a1749e06edcd1ec118436f"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ if [[ -n "${OPENCLAW_NODE_VERSION:-}" ]]; then
|
|||
NODE_VERSION_REQUESTED=1
|
||||
fi
|
||||
MIN_NODE_VERSION="22.19.0"
|
||||
MIN_NODE_23_VERSION="23.11.0"
|
||||
SUPPORTED_NODE_VERSION_LABEL="Node 22.19+, Node 23.11+, or Node 24+"
|
||||
APK_NODE_BIN_DIR="/usr/bin"
|
||||
NPM_LOGLEVEL="${OPENCLAW_NPM_LOGLEVEL:-error}"
|
||||
INSTALL_METHOD="${OPENCLAW_INSTALL_METHOD:-npm}"
|
||||
|
|
@ -423,11 +425,14 @@ linked_node_is_usable() {
|
|||
|
||||
current_version="$("$(node_bin)" -v 2>/dev/null || echo "")"
|
||||
required_version="$(required_node_version)"
|
||||
if ! node_version_is_supported "$current_version"; then
|
||||
return 1
|
||||
fi
|
||||
if ! semver_at_least "$current_version" "$required_version"; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
"$(node_bin)" -e "require('node:sqlite')" >/dev/null 2>&1
|
||||
"$(node_bin)" -e "const { DatabaseSync } = require('node:sqlite'); const db = new DatabaseSync(':memory:'); const statement = db.prepare('SELECT 1'); const supported = typeof statement.columns === 'function'; db.close(); if (!supported) process.exit(1);" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
semver_at_least() {
|
||||
|
|
@ -460,8 +465,32 @@ semver_at_least() {
|
|||
((version_patch >= required_patch))
|
||||
}
|
||||
|
||||
node_version_is_supported() {
|
||||
local version="${1#v}"
|
||||
local major minor patch
|
||||
|
||||
IFS=. read -r major minor patch <<<"$version"
|
||||
minor="${minor:-0}"
|
||||
patch="${patch:-0}"
|
||||
for part in "$major" "$minor" "$patch"; do
|
||||
if [[ ! "$part" =~ ^[0-9]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ((major == 22)); then
|
||||
semver_at_least "$version" "$MIN_NODE_VERSION"
|
||||
return
|
||||
fi
|
||||
if ((major == 23)); then
|
||||
semver_at_least "$version" "$MIN_NODE_23_VERSION"
|
||||
return
|
||||
fi
|
||||
((major > 23))
|
||||
}
|
||||
|
||||
required_node_version() {
|
||||
if [[ "$NODE_VERSION_REQUESTED" == "1" ]] && semver_at_least "$NODE_VERSION" "$MIN_NODE_VERSION"; then
|
||||
if [[ "$NODE_VERSION_REQUESTED" == "1" ]] && node_version_is_supported "$NODE_VERSION"; then
|
||||
printf '%s\n' "$NODE_VERSION"
|
||||
return
|
||||
fi
|
||||
|
|
@ -767,6 +796,10 @@ install_node() {
|
|||
local expected_sha
|
||||
local actual_sha
|
||||
|
||||
if ! node_version_is_supported "$NODE_VERSION"; then
|
||||
fail "Node ${NODE_VERSION} is unsupported; use ${SUPPORTED_NODE_VERSION_LABEL}."
|
||||
fi
|
||||
|
||||
os="$(os_detect)"
|
||||
arch="$(arch_detect)"
|
||||
dir="$(node_dir)"
|
||||
|
|
|
|||
|
|
@ -149,18 +149,33 @@ if ([string]::IsNullOrWhiteSpace($GitDir)) {
|
|||
}
|
||||
|
||||
# Check for Node.js
|
||||
function Test-NodeVersionSupported {
|
||||
param([string]$Version)
|
||||
|
||||
$versionMatch = [regex]::Match($Version, '^v?(?<major>\d+)\.(?<minor>\d+)\.')
|
||||
if (-not $versionMatch.Success) {
|
||||
return $false
|
||||
}
|
||||
$major = [int]$versionMatch.Groups["major"].Value
|
||||
$minor = [int]$versionMatch.Groups["minor"].Value
|
||||
if ($major -eq 22) {
|
||||
return ($minor -ge 19)
|
||||
}
|
||||
if ($major -eq 23) {
|
||||
return ($minor -ge 11)
|
||||
}
|
||||
return ($major -gt 23)
|
||||
}
|
||||
|
||||
function Check-Node {
|
||||
try {
|
||||
$nodeVersion = (node -v 2>$null)
|
||||
if ($nodeVersion) {
|
||||
$versionMatch = [regex]::Match($nodeVersion, '^v(?<major>\d+)\.(?<minor>\d+)\.')
|
||||
$major = if ($versionMatch.Success) { [int]$versionMatch.Groups["major"].Value } else { 0 }
|
||||
$minor = if ($versionMatch.Success) { [int]$versionMatch.Groups["minor"].Value } else { 0 }
|
||||
if (($major -gt 22) -or (($major -eq 22) -and ($minor -ge 19))) {
|
||||
if (Test-NodeVersionSupported -Version $nodeVersion) {
|
||||
Write-Host "[OK] Node.js $nodeVersion found" -ForegroundColor Green
|
||||
return $true
|
||||
} else {
|
||||
Write-Host "[!] Node.js $nodeVersion found, but v22.19+ required" -ForegroundColor Yellow
|
||||
Write-Host "[!] Node.js $nodeVersion found, but Node 22.19+, Node 23.11+, or Node 24+ is required" -ForegroundColor Yellow
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ DEFAULT_TAGLINE="All your chats, one OpenClaw."
|
|||
NODE_DEFAULT_MAJOR=24
|
||||
NODE_MIN_MAJOR=22
|
||||
NODE_MIN_MINOR=19
|
||||
NODE_MIN_VERSION="${NODE_MIN_MAJOR}.${NODE_MIN_MINOR}"
|
||||
NODE_23_MIN_MINOR=11
|
||||
NODE_SUPPORTED_VERSION_LABEL="22.19+, 23.11+, or 24+"
|
||||
|
||||
ORIGINAL_PATH="${PATH:-}"
|
||||
|
||||
|
|
@ -1479,23 +1480,32 @@ node_major_version() {
|
|||
return 1
|
||||
}
|
||||
|
||||
node_is_at_least_required() {
|
||||
node_version_components_are_supported() {
|
||||
local major="$1"
|
||||
local minor="$2"
|
||||
if [[ "$major" -eq "$NODE_MIN_MAJOR" && "$minor" -ge "$NODE_MIN_MINOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$major" -eq 23 && "$minor" -ge "$NODE_23_MIN_MINOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$major" -gt 23 ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
node_is_supported() {
|
||||
local version_components major minor
|
||||
version_components="$(parse_node_version_components || true)"
|
||||
read -r major minor <<< "$version_components"
|
||||
if [[ ! "$major" =~ ^[0-9]+$ || ! "$minor" =~ ^[0-9]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$major" -gt "$NODE_MIN_MAJOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$major" -eq "$NODE_MIN_MAJOR" && "$minor" -ge "$NODE_MIN_MINOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
node_version_components_are_supported "$major" "$minor"
|
||||
}
|
||||
|
||||
node_binary_is_at_least_required() {
|
||||
node_binary_is_supported() {
|
||||
local node_bin="$1"
|
||||
local version_components major minor
|
||||
version_components="$(parse_node_version_components_for_binary "$node_bin" || true)"
|
||||
|
|
@ -1503,13 +1513,7 @@ node_binary_is_at_least_required() {
|
|||
if [[ ! "$major" =~ ^[0-9]+$ || ! "$minor" =~ ^[0-9]+$ ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$major" -gt "$NODE_MIN_MAJOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
if [[ "$major" -eq "$NODE_MIN_MAJOR" && "$minor" -ge "$NODE_MIN_MINOR" ]]; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
node_version_components_are_supported "$major" "$minor"
|
||||
}
|
||||
|
||||
prepend_path_dir() {
|
||||
|
|
@ -1585,7 +1589,7 @@ promote_supported_node_binary() {
|
|||
continue
|
||||
fi
|
||||
seen_dirs="${seen_dirs}${dir}:"
|
||||
if node_binary_is_at_least_required "$candidate"; then
|
||||
if node_binary_is_supported "$candidate"; then
|
||||
prepend_path_dir "$dir" || continue
|
||||
if [[ "$OS" == "linux" ]]; then
|
||||
persist_shell_path_prepend "$dir" || true
|
||||
|
|
@ -1633,9 +1637,7 @@ ensure_macos_default_node_active() {
|
|||
fi
|
||||
fi
|
||||
|
||||
local major=""
|
||||
major="$(node_major_version || true)"
|
||||
if [[ -n "$major" && "$major" -ge 22 ]]; then
|
||||
if node_is_supported; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
|
@ -1658,7 +1660,7 @@ ensure_macos_default_node_active() {
|
|||
|
||||
ensure_default_node_active_shell() {
|
||||
promote_supported_node_binary || true
|
||||
if node_is_at_least_required; then
|
||||
if node_is_supported; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
|
|
@ -1666,7 +1668,7 @@ ensure_default_node_active_shell() {
|
|||
active_path="$(command -v node 2>/dev/null || echo "not found")"
|
||||
active_version="$(node -v 2>/dev/null || echo "missing")"
|
||||
|
||||
ui_error "Active Node.js must be v${NODE_MIN_VERSION}+ but this shell is using ${active_version} (${active_path})"
|
||||
ui_error "Active Node.js must be ${NODE_SUPPORTED_VERSION_LABEL} but this shell is using ${active_version} (${active_path})"
|
||||
print_active_node_paths || true
|
||||
|
||||
local nvm_detected=0
|
||||
|
|
@ -1686,7 +1688,7 @@ ensure_default_node_active_shell() {
|
|||
echo "Then open a new shell and rerun:"
|
||||
echo " curl -fsSL https://openclaw.ai/install.sh | bash"
|
||||
else
|
||||
echo "Install/select Node.js ${NODE_DEFAULT_MAJOR} (or Node ${NODE_MIN_VERSION}+ minimum) and ensure it is first on PATH, then rerun installer."
|
||||
echo "Install/select Node.js ${NODE_DEFAULT_MAJOR} and ensure it is first on PATH, then rerun installer."
|
||||
fi
|
||||
|
||||
return 1
|
||||
|
|
@ -1716,15 +1718,15 @@ load_nvm_for_node_detection() {
|
|||
check_node() {
|
||||
if command -v node &> /dev/null; then
|
||||
NODE_VERSION="$(node_major_version || true)"
|
||||
if node_is_at_least_required; then
|
||||
if node_is_supported; then
|
||||
ui_success "Node.js v$(node -v | cut -d'v' -f2) found"
|
||||
print_active_node_paths || true
|
||||
return 0
|
||||
else
|
||||
if [[ -n "$NODE_VERSION" ]]; then
|
||||
ui_info "Node.js $(node -v) found, upgrading to v${NODE_MIN_VERSION}+"
|
||||
ui_info "Node.js $(node -v) found, upgrading to a supported version"
|
||||
else
|
||||
ui_info "Node.js found but version could not be parsed; reinstalling v${NODE_MIN_VERSION}+"
|
||||
ui_info "Node.js found but version could not be parsed; reinstalling a supported version"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
|
@ -1736,11 +1738,11 @@ check_node() {
|
|||
|
||||
finish_linux_node_install() {
|
||||
activate_supported_node_on_path || true
|
||||
if ! node_is_at_least_required; then
|
||||
if ! node_is_supported; then
|
||||
local active_path active_version
|
||||
active_path="$(command -v node 2>/dev/null || echo "not found")"
|
||||
active_version="$(node -v 2>/dev/null || echo "missing")"
|
||||
ui_error "Installed Node.js must be v${NODE_MIN_VERSION}+ but this shell is using ${active_version} (${active_path})"
|
||||
ui_error "Installed Node.js must be ${NODE_SUPPORTED_VERSION_LABEL} but this shell is using ${active_version} (${active_path})"
|
||||
echo "Upgrade the system Node.js package or install Node.js ${NODE_DEFAULT_MAJOR} manually, then rerun the installer."
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1758,14 +1760,14 @@ install_node_with_apk() {
|
|||
fi
|
||||
|
||||
activate_supported_node_on_path || true
|
||||
if node_is_at_least_required; then
|
||||
if node_is_supported; then
|
||||
finish_linux_node_install
|
||||
return 0
|
||||
fi
|
||||
|
||||
local apk_node_version
|
||||
apk_node_version="$(node -v 2>/dev/null || echo "missing")"
|
||||
ui_warn "Alpine nodejs package installed ${apk_node_version}, below required v${NODE_MIN_VERSION}+"
|
||||
ui_warn "Alpine nodejs package installed ${apk_node_version}, outside the supported range (${NODE_SUPPORTED_VERSION_LABEL})"
|
||||
ui_info "Trying Alpine nodejs-current package"
|
||||
if is_root; then
|
||||
run_required_step "Installing nodejs-current" apk add --no-cache nodejs-current npm
|
||||
|
|
@ -1774,7 +1776,7 @@ install_node_with_apk() {
|
|||
fi
|
||||
|
||||
activate_supported_node_on_path || true
|
||||
if node_is_at_least_required; then
|
||||
if node_is_supported; then
|
||||
finish_linux_node_install
|
||||
return 0
|
||||
fi
|
||||
|
|
@ -1782,7 +1784,7 @@ install_node_with_apk() {
|
|||
local active_path active_version
|
||||
active_path="$(command -v node 2>/dev/null || echo "not found")"
|
||||
active_version="$(node -v 2>/dev/null || echo "missing")"
|
||||
ui_error "Alpine apk repositories did not provide Node.js v${NODE_MIN_VERSION}+; found ${active_version} (${active_path})"
|
||||
ui_error "Alpine apk repositories did not provide Node.js ${NODE_SUPPORTED_VERSION_LABEL}; found ${active_version} (${active_path})"
|
||||
echo "Use Alpine 3.21+ or install Node.js ${NODE_DEFAULT_MAJOR} manually, then rerun the installer."
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -1864,7 +1866,7 @@ install_node() {
|
|||
fi
|
||||
else
|
||||
ui_error "Could not detect package manager"
|
||||
echo "Please install Node.js ${NODE_DEFAULT_MAJOR} manually (or Node ${NODE_MIN_VERSION}+ minimum): https://nodejs.org"
|
||||
echo "Please install Node.js ${NODE_DEFAULT_MAJOR} manually: https://nodejs.org"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -1186,7 +1186,7 @@ async function resolvePackageRuntimePreflightError(params: {
|
|||
`The requested package requires ${status.nodeEngine}.`,
|
||||
runtime.nodeRunner
|
||||
? "Upgrade the Node runtime that owns the managed Gateway service, then rerun `openclaw update`."
|
||||
: "Upgrade Node to 22.19+ or Node 24, then rerun `openclaw update`.",
|
||||
: "Upgrade to Node 22.19 or newer 22.x, Node 23.11+, or Node 24+, then rerun `openclaw update`.",
|
||||
"Bare `npm i -g openclaw` can silently install an older compatible release.",
|
||||
"After upgrading Node, use `npm i -g openclaw@latest`.",
|
||||
].join("\n");
|
||||
|
|
|
|||
|
|
@ -411,7 +411,7 @@ describe("maybeRepairGatewayServiceConfig", () => {
|
|||
});
|
||||
mocks.buildGatewayInstallPlan.mockImplementation(async ({ warn }) => {
|
||||
warn?.(
|
||||
"System Node 20.20.2 at /usr/bin/node is below the required Node 22.19+. Using /home/orin/.nvm/versions/node/v22.22.2/bin/node for the daemon.",
|
||||
"System Node 20.20.2 at /usr/bin/node is outside the supported range. Using /home/orin/.nvm/versions/node/v22.22.2/bin/node for the daemon.",
|
||||
"Gateway runtime",
|
||||
);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -442,7 +442,7 @@ describe("resolveSystemNodeInfo", () => {
|
|||
"/Users/me/.fnm/node-22/bin/node",
|
||||
);
|
||||
|
||||
expect(warning).toContain("below the required Node 22.19+");
|
||||
expect(warning).toContain("outside the supported range");
|
||||
expect(warning).toContain(darwinNode);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ export async function resolveSystemNodeInfo(params: {
|
|||
return firstAvailable;
|
||||
}
|
||||
|
||||
/** Renders a warning when the system Node exists but is below the supported floor. */
|
||||
/** Renders a warning when the system Node exists but is outside the supported range. */
|
||||
export function renderSystemNodeWarning(
|
||||
systemNode: SystemNodeInfo | null,
|
||||
selectedNodePath?: string,
|
||||
|
|
@ -196,7 +196,7 @@ export function renderSystemNodeWarning(
|
|||
}
|
||||
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.19+.${selectedLabel} Install Node 24 (recommended) or Node 22 LTS from nodejs.org or Homebrew.`;
|
||||
return `System Node ${versionLabel} at ${systemNode.path} is outside the supported range.${selectedLabel} Install Node 24 (recommended) or Node 22 LTS from nodejs.org or Homebrew.`;
|
||||
}
|
||||
/** Resolves the Node binary the daemon should use for a node runtime. */
|
||||
export async function resolvePreferredNodePath(params: {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ describe("runtime-guard", () => {
|
|||
expect(runtimeSatisfies(unknown)).toBe(false);
|
||||
expect(isSupportedNodeVersion("22.19.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion("22.18.9")).toBe(false);
|
||||
expect(isSupportedNodeVersion("23.7.0")).toBe(false);
|
||||
expect(isSupportedNodeVersion("23.10.9")).toBe(false);
|
||||
expect(isSupportedNodeVersion("23.11.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion("24.0.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion(null)).toBe(false);
|
||||
});
|
||||
|
||||
|
|
@ -72,6 +76,18 @@ describe("runtime-guard", () => {
|
|||
expect(nodeVersionSatisfiesEngine("22.19.0", "^22.19.0")).toBeNull();
|
||||
});
|
||||
|
||||
it("checks node versions against the supported engine range", () => {
|
||||
const engine = ">=22.19.0 <23 || >=23.11.0";
|
||||
expect(nodeVersionSatisfiesEngine("22.19.0", engine)).toBe(true);
|
||||
expect(nodeVersionSatisfiesEngine("22.18.9", engine)).toBe(false);
|
||||
expect(nodeVersionSatisfiesEngine("23.7.0", engine)).toBe(false);
|
||||
expect(nodeVersionSatisfiesEngine("23.10.9", engine)).toBe(false);
|
||||
expect(nodeVersionSatisfiesEngine("23.11.0", engine)).toBe(true);
|
||||
expect(nodeVersionSatisfiesEngine("24.0.0", engine)).toBe(true);
|
||||
expect(nodeVersionSatisfiesEngine(null, engine)).toBe(false);
|
||||
expect(nodeVersionSatisfiesEngine("unknown", engine)).toBe(false);
|
||||
});
|
||||
|
||||
it("throws via exit when runtime is too old", () => {
|
||||
const runtime = {
|
||||
log: vi.fn(),
|
||||
|
|
@ -90,7 +106,7 @@ describe("runtime-guard", () => {
|
|||
expect(runtime.error).toHaveBeenCalledOnce();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
[
|
||||
"openclaw requires Node >=22.19.0.",
|
||||
"openclaw requires Node >=22.19.0 <23 or >=23.11.0.",
|
||||
"Detected: node 20.0.0 (exec: /usr/bin/node).",
|
||||
"PATH searched: /usr/bin",
|
||||
"Install Node: https://nodejs.org/en/download",
|
||||
|
|
@ -135,7 +151,7 @@ describe("runtime-guard", () => {
|
|||
expect(runtime.error).toHaveBeenCalledOnce();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
[
|
||||
"openclaw requires Node >=22.19.0.",
|
||||
"openclaw requires Node >=22.19.0 <23 or >=23.11.0.",
|
||||
"Detected: unknown runtime (exec: unknown).",
|
||||
"PATH searched: (not set)",
|
||||
"Install Node: https://nodejs.org/en/download",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,11 @@ type Semver = {
|
|||
patch: number;
|
||||
};
|
||||
|
||||
const MIN_NODE: Semver = { major: 22, minor: 19, patch: 0 };
|
||||
const MIN_NODE_22: Semver = { major: 22, minor: 19, patch: 0 };
|
||||
const MIN_NODE_23: Semver = { major: 23, minor: 11, patch: 0 };
|
||||
const MINIMUM_ENGINE_RE = /^\s*>=\s*v?(\d+\.\d+\.\d+)\s*$/i;
|
||||
const DISJUNCTIVE_ENGINE_RE =
|
||||
/^\s*>=\s*v?(\d+\.\d+\.\d+)\s+<\s*v?(\d+)(?:\.(\d+)\.(\d+))?\s*\|\|\s*>=\s*v?(\d+\.\d+\.\d+)\s*$/i;
|
||||
|
||||
/** Runtime facts included in startup/runtime-version diagnostics. */
|
||||
export type RuntimeDetails = {
|
||||
|
|
@ -69,16 +72,25 @@ export function detectRuntime(): RuntimeDetails {
|
|||
|
||||
/** Returns whether a detected runtime meets OpenClaw's minimum runtime contract. */
|
||||
export function runtimeSatisfies(details: RuntimeDetails): boolean {
|
||||
const parsed = parseSemver(details.version);
|
||||
if (details.kind === "node") {
|
||||
return isAtLeast(parsed, MIN_NODE);
|
||||
return isSupportedNodeVersion(details.version);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Checks a Node version label against OpenClaw's current minimum Node version. */
|
||||
/** Checks a Node version label against OpenClaw's supported Node version range. */
|
||||
export function isSupportedNodeVersion(version: string | null): boolean {
|
||||
return isAtLeast(parseSemver(version), MIN_NODE);
|
||||
const parsed = parseSemver(version);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
if (parsed.major === MIN_NODE_22.major) {
|
||||
return isAtLeast(parsed, MIN_NODE_22);
|
||||
}
|
||||
if (parsed.major === MIN_NODE_23.major) {
|
||||
return isAtLeast(parsed, MIN_NODE_23);
|
||||
}
|
||||
return parsed.major > MIN_NODE_23.major;
|
||||
}
|
||||
|
||||
/** Parses simple package `engines.node` ranges of the form `>=x.y.z`. */
|
||||
|
|
@ -93,16 +105,40 @@ export function parseMinimumNodeEngine(engine: string | null): Semver | null {
|
|||
return parseSemver(match[1] ?? null);
|
||||
}
|
||||
|
||||
/** Returns whether a Node version satisfies a simple minimum engine range, or null if unsupported. */
|
||||
/** Returns whether a Node version satisfies a supported engine range, or null if unsupported. */
|
||||
export function nodeVersionSatisfiesEngine(
|
||||
version: string | null,
|
||||
engine: string | null,
|
||||
): boolean | null {
|
||||
const minimum = parseMinimumNodeEngine(engine);
|
||||
if (!minimum) {
|
||||
if (minimum) {
|
||||
return isAtLeast(parseSemver(version), minimum);
|
||||
}
|
||||
|
||||
const rangeMatch = engine?.match(DISJUNCTIVE_ENGINE_RE);
|
||||
if (!rangeMatch) {
|
||||
return null;
|
||||
}
|
||||
return isAtLeast(parseSemver(version), minimum);
|
||||
const parsed = parseSemver(version);
|
||||
if (!parsed) {
|
||||
return false;
|
||||
}
|
||||
const [, firstMinimumRaw, upperMajorRaw, upperMinorRaw, upperPatchRaw, secondMinimumRaw] =
|
||||
rangeMatch;
|
||||
const firstMinimum = parseSemver(firstMinimumRaw ?? null);
|
||||
const secondMinimum = parseSemver(secondMinimumRaw ?? null);
|
||||
const upperBound: Semver = {
|
||||
major: Number.parseInt(upperMajorRaw ?? "", 10),
|
||||
minor: Number.parseInt(upperMinorRaw ?? "0", 10),
|
||||
patch: Number.parseInt(upperPatchRaw ?? "0", 10),
|
||||
};
|
||||
if (!firstMinimum || !secondMinimum || !Number.isFinite(upperBound.major)) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
(isAtLeast(parsed, firstMinimum) && !isAtLeast(parsed, upperBound)) ||
|
||||
isAtLeast(parsed, secondMinimum)
|
||||
);
|
||||
}
|
||||
|
||||
/** Exits through the provided runtime when the current Node runtime is unsupported. */
|
||||
|
|
@ -121,7 +157,7 @@ export function assertSupportedRuntime(
|
|||
|
||||
runtime.error(
|
||||
[
|
||||
"openclaw requires Node >=22.19.0.",
|
||||
"openclaw requires Node >=22.19.0 <23 or >=23.11.0.",
|
||||
`Detected: ${runtimeLabel} (exec: ${execLabel}).`,
|
||||
`PATH searched: ${details.pathEnv}`,
|
||||
"Install Node: https://nodejs.org/en/download",
|
||||
|
|
|
|||
|
|
@ -163,75 +163,67 @@ describe("openclaw launcher", () => {
|
|||
cleanupTempDirs(fixtureRoots);
|
||||
});
|
||||
|
||||
it("keeps the bootstrap Node floor aligned with package and runtime guards", async () => {
|
||||
const [launcher, runtimeGuard, packageJsonRaw] = await Promise.all([
|
||||
fs.readFile(path.resolve(process.cwd(), "openclaw.mjs"), "utf8"),
|
||||
fs.readFile(path.resolve(process.cwd(), "src/infra/runtime-guard.ts"), "utf8"),
|
||||
fs.readFile(path.resolve(process.cwd(), "package.json"), "utf8"),
|
||||
]);
|
||||
it("keeps the bootstrap Node range aligned with the package engine", async () => {
|
||||
const packageJsonRaw = await fs.readFile(path.resolve(process.cwd(), "package.json"), "utf8");
|
||||
const packageJson = JSON.parse(packageJsonRaw) as { engines?: { node?: string } };
|
||||
const launcherMatch = launcher.match(
|
||||
/const MIN_NODE_MAJOR = (\d+);\s+const MIN_NODE_MINOR = (\d+);/u,
|
||||
);
|
||||
const runtimeMatch = runtimeGuard.match(
|
||||
/const MIN_NODE: Semver = \{ major: (\d+), minor: (\d+), patch: (\d+) \};/u,
|
||||
);
|
||||
const engineMatch = packageJson.engines?.node?.match(/^>=(\d+)\.(\d+)\.(\d+)$/u);
|
||||
|
||||
if (!launcherMatch) {
|
||||
throw new Error("openclaw.mjs MIN_NODE_* constants were not found");
|
||||
}
|
||||
if (!runtimeMatch) {
|
||||
throw new Error("src/infra/runtime-guard.ts MIN_NODE constant was not found");
|
||||
}
|
||||
if (!engineMatch) {
|
||||
throw new Error("package.json engines.node must use >=<major>.<minor>.<patch>");
|
||||
}
|
||||
const [engineMajor, engineMinor, enginePatch] = engineMatch.slice(1, 4).map(Number);
|
||||
const launcherMinimumLabel = `${engineMajor}.${engineMinor}`;
|
||||
|
||||
expect(
|
||||
[Number(launcherMatch[1]), Number(launcherMatch[2]), 0],
|
||||
"openclaw.mjs MIN_NODE_* must match package.json engines.node",
|
||||
).toEqual([engineMajor, engineMinor, enginePatch]);
|
||||
expect(
|
||||
runtimeMatch.slice(1, 4).map(Number),
|
||||
"src/infra/runtime-guard.ts MIN_NODE must match package.json engines.node",
|
||||
).toEqual([engineMajor, engineMinor, enginePatch]);
|
||||
expect(packageJson.engines?.node).toBe(">=22.19.0 <23 || >=23.11.0");
|
||||
|
||||
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
|
||||
const mockedNodeVersion =
|
||||
engineMinor > 0 ? `${engineMajor}.${engineMinor - 1}.0` : `${engineMajor - 1}.999.0`;
|
||||
const mockNodeVersionPath = path.join(fixtureRoot, "mock-node-version.mjs");
|
||||
await fs.writeFile(
|
||||
mockNodeVersionPath,
|
||||
[
|
||||
"Object.defineProperty(process.versions, 'node', {",
|
||||
` value: ${JSON.stringify(mockedNodeVersion)},`,
|
||||
"});",
|
||||
].join("\n"),
|
||||
path.join(fixtureRoot, "dist", "entry.js"),
|
||||
'process.stdout.write("runtime-loaded\\n");\n',
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(mockNodeVersionPath).href,
|
||||
path.join(fixtureRoot, "openclaw.mjs"),
|
||||
"--help",
|
||||
],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
const cases = [
|
||||
{ version: "22.18.9", supported: false },
|
||||
{ version: "22.19.0", supported: true },
|
||||
{ version: "23.7.0", supported: false },
|
||||
{ version: "23.10.9", supported: false },
|
||||
{ version: "23.11.0", supported: true },
|
||||
{ version: "24.0.0", supported: true },
|
||||
] as const;
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stderr).toContain(
|
||||
`openclaw: Node.js v${launcherMinimumLabel}+ is required (current: v${mockedNodeVersion}).`,
|
||||
);
|
||||
for (const testCase of cases) {
|
||||
const mockNodeVersionPath = path.join(
|
||||
fixtureRoot,
|
||||
`mock-node-version-${testCase.version}.mjs`,
|
||||
);
|
||||
await fs.writeFile(
|
||||
mockNodeVersionPath,
|
||||
[
|
||||
"Object.defineProperty(process.versions, 'node', {",
|
||||
` value: ${JSON.stringify(testCase.version)},`,
|
||||
"});",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"--import",
|
||||
pathToFileURL(mockNodeVersionPath).href,
|
||||
path.join(fixtureRoot, "openclaw.mjs"),
|
||||
"--help",
|
||||
],
|
||||
{
|
||||
cwd: fixtureRoot,
|
||||
env: launcherEnv(),
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
|
||||
if (testCase.supported) {
|
||||
expect(result.status, testCase.version).toBe(0);
|
||||
expect(result.stdout, testCase.version).toContain("runtime-loaded");
|
||||
} else {
|
||||
expect(result.status, testCase.version).toBe(1);
|
||||
expect(result.stderr, testCase.version).toContain(
|
||||
`openclaw: Node.js >=22.19.0 <23 or >=23.11.0 is required (current: v${testCase.version}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("surfaces transitive entry import failures instead of masking them as missing dist", async () => {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,42 @@ describe("install-cli.sh", () => {
|
|||
expect(script).toContain('cleanup_legacy_submodules "$repo_dir"');
|
||||
});
|
||||
|
||||
it("accepts only Node versions with the required SQLite statement API", () => {
|
||||
const result = runInstallCliShell(`
|
||||
set -euo pipefail
|
||||
source "${SCRIPT_PATH}"
|
||||
set +e
|
||||
for version in 22.18.9 22.19.0 23.7.0 23.10.9 23.11.0 24.0.0; do
|
||||
node_version_is_supported "$version"
|
||||
printf '%s=%s\n' "$version" "$?"
|
||||
done
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toContain("22.18.9=1");
|
||||
expect(result.stdout).toContain("22.19.0=0");
|
||||
expect(result.stdout).toContain("23.7.0=1");
|
||||
expect(result.stdout).toContain("23.10.9=1");
|
||||
expect(result.stdout).toContain("23.11.0=0");
|
||||
expect(result.stdout).toContain("24.0.0=0");
|
||||
});
|
||||
|
||||
it("rejects an explicitly requested incompatible Node 23 release", () => {
|
||||
const result = runInstallCliShell(`
|
||||
set -euo pipefail
|
||||
source "${SCRIPT_PATH}"
|
||||
NODE_VERSION=23.7.0
|
||||
NODE_VERSION_REQUESTED=1
|
||||
install_node
|
||||
`);
|
||||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"Node 23.7.0 is unsupported; use Node 22.19+, Node 23.11+, or Node 24+.",
|
||||
);
|
||||
expect(result.stdout).not.toContain("Installing Node 23.7.0");
|
||||
});
|
||||
|
||||
it("rejects installer options with missing values", () => {
|
||||
const result = runInstallCliShell(`
|
||||
set -euo pipefail
|
||||
|
|
@ -686,7 +722,7 @@ describe("install-cli.sh", () => {
|
|||
"require_bin() { :; }",
|
||||
"download_file() {",
|
||||
' case "$1" in',
|
||||
" */SHASUMS256.txt) printf 'fixture-sha node-v22.18.0-linux-x64.tar.gz\\n' > \"$2\" ;;",
|
||||
" */SHASUMS256.txt) printf 'fixture-sha node-v22.19.0-linux-x64.tar.gz\\n' > \"$2\" ;;",
|
||||
" *) printf 'node tarball fixture\\n' > \"$2\" ;;",
|
||||
" esac",
|
||||
"}",
|
||||
|
|
@ -701,7 +737,7 @@ describe("install-cli.sh", () => {
|
|||
' cp "$NEW_NPM" "$dest/bin/npm"',
|
||||
"}",
|
||||
`PREFIX=${JSON.stringify(prefix)}`,
|
||||
"NODE_VERSION=22.18.0",
|
||||
"NODE_VERSION=22.19.0",
|
||||
"NODE_VERSION_REQUESTED=1",
|
||||
"install_node",
|
||||
].join("\n"),
|
||||
|
|
@ -713,7 +749,7 @@ describe("install-cli.sh", () => {
|
|||
|
||||
expect(result.status).toBe(1);
|
||||
expect(result.stdout).toContain(
|
||||
"Installed Node 22.18.0 must provide Node >= 22.19.0 with node:sqlite",
|
||||
"Installed Node 22.19.0 must provide Node >= 22.19.0 with node:sqlite",
|
||||
);
|
||||
expect(result.stdout).toContain("found v22.18.0");
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,56 @@ describe("install.ps1 failure handling", () => {
|
|||
expect(source).toContain("$installSucceeded = Test-BooleanSuccessResult -Results $mainResults");
|
||||
});
|
||||
|
||||
it("checks the full supported Node version range", () => {
|
||||
const versionBody = extractFunctionBody(source, "Test-NodeVersionSupported");
|
||||
const checkNodeBody = extractFunctionBody(source, "Check-Node");
|
||||
expect(versionBody).toContain("$major -eq 22");
|
||||
expect(versionBody).toContain("$minor -ge 19");
|
||||
expect(versionBody).toContain("$major -eq 23");
|
||||
expect(versionBody).toContain("$minor -ge 11");
|
||||
expect(versionBody).toContain("$major -gt 23");
|
||||
expect(checkNodeBody).toContain("Test-NodeVersionSupported -Version $nodeVersion");
|
||||
});
|
||||
|
||||
runIfPowerShell("accepts only supported Node versions", () => {
|
||||
const tempDir = harness.createTempDir("openclaw-install-ps1-");
|
||||
const scriptPath = join(tempDir, "install.ps1");
|
||||
const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, "");
|
||||
writeFileSync(
|
||||
scriptPath,
|
||||
[
|
||||
scriptWithoutEntryPoint,
|
||||
"",
|
||||
"$cases = @{",
|
||||
" '22.18.9' = $false",
|
||||
" '22.19.0' = $true",
|
||||
" '23.7.0' = $false",
|
||||
" '23.10.9' = $false",
|
||||
" '23.11.0' = $true",
|
||||
" '24.0.0' = $true",
|
||||
"}",
|
||||
"foreach ($entry in $cases.GetEnumerator()) {",
|
||||
" $actual = Test-NodeVersionSupported -Version $entry.Key",
|
||||
' if ($actual -ne $entry.Value) { throw "Version=$($entry.Key) Actual=$actual" }',
|
||||
"}",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
chmodSync(scriptPath, 0o755);
|
||||
|
||||
const result = runPowerShell([
|
||||
"-NoLogo",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
scriptPath,
|
||||
]);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stderr).toBe("");
|
||||
});
|
||||
|
||||
it("runs npm install through the resolved command with quiet CI defaults", () => {
|
||||
const npmInstallBody = extractFunctionBody(source, "Install-OpenClaw");
|
||||
expect(npmInstallBody).toContain("$npmOutput = Invoke-NpmCommand -Arguments");
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ NODE
|
|||
expect(script).toContain(
|
||||
'run_required_step "Installing nodejs-current" apk add --no-cache nodejs-current npm',
|
||||
);
|
||||
expect(script).toContain("if ! node_is_at_least_required; then");
|
||||
expect(script).toContain("if ! node_is_supported; then");
|
||||
|
||||
const apkIndex = script.indexOf("if command -v apk &> /dev/null && is_alpine_linux; then");
|
||||
const nodeSourceIndex = script.indexOf('ui_info "Installing Node.js via NodeSource"');
|
||||
|
|
@ -182,7 +182,7 @@ NODE
|
|||
ui_success() { printf 'success:%s\\n' "$*"; }
|
||||
run_quiet_step() { printf 'step:%s|%s\\n' "$1" "\${*:2}"; }
|
||||
apk() { :; }
|
||||
node_is_at_least_required() { return 0; }
|
||||
node_is_supported() { return 0; }
|
||||
finish_linux_node_install() { printf 'finish-linux-node\\n'; }
|
||||
install_node
|
||||
`);
|
||||
|
|
@ -275,7 +275,7 @@ NODE
|
|||
"step:Installing nodejs-current|apk add --no-cache nodejs-current npm",
|
||||
);
|
||||
expect(result.stdout).toContain(
|
||||
"error:Alpine apk repositories did not provide Node.js v22.19+",
|
||||
"error:Alpine apk repositories did not provide Node.js 22.19+, 23.11+, or 24+",
|
||||
);
|
||||
expect(result.stdout).toContain("Use Alpine 3.21+ or install Node.js 24 manually");
|
||||
});
|
||||
|
|
@ -1078,15 +1078,11 @@ NODE
|
|||
expect(output).toContain("version=v22.22.0");
|
||||
});
|
||||
|
||||
it("uses the package engine floor when accepting existing Node runtimes", () => {
|
||||
it("uses the package engine range when accepting existing Node runtimes", () => {
|
||||
const pkg = JSON.parse(readFileSync("package.json", "utf8")) as {
|
||||
engines?: { node?: string };
|
||||
};
|
||||
const engineMatch = /^>=22\.(\d+)\.0$/.exec(pkg.engines?.node ?? "");
|
||||
expect(engineMatch).not.toBeNull();
|
||||
|
||||
const minMinor = Number(engineMatch?.[1]);
|
||||
expect(script).toContain(`NODE_MIN_MINOR=${minMinor}`);
|
||||
expect(pkg.engines?.node).toBe(">=22.19.0 <23 || >=23.11.0");
|
||||
|
||||
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-node-floor-"));
|
||||
const bin = join(tmp, "bin");
|
||||
|
|
@ -1111,15 +1107,12 @@ NODE
|
|||
"unset -f node 2>/dev/null || true",
|
||||
"unalias node 2>/dev/null || true",
|
||||
'node() { printf "%s\\n" "${FAKE_NODE_VERSION:-v0.0.0}"; }',
|
||||
`FAKE_NODE_VERSION="v22.${minMinor - 1}.0"`,
|
||||
"export FAKE_NODE_VERSION",
|
||||
"node_is_at_least_required",
|
||||
"node_below_floor=$?",
|
||||
`FAKE_NODE_VERSION="v22.${minMinor}.0"`,
|
||||
"export FAKE_NODE_VERSION",
|
||||
"node_is_at_least_required",
|
||||
"node_at_floor=$?",
|
||||
'printf "node_below_floor=%s\\nnode_at_floor=%s\\n" "$node_below_floor" "$node_at_floor"',
|
||||
"for version in 22.18.9 22.19.0 23.7.0 23.10.9 23.11.0 24.0.0; do",
|
||||
' FAKE_NODE_VERSION="v${version}"',
|
||||
" export FAKE_NODE_VERSION",
|
||||
" node_is_supported",
|
||||
' printf "%s=%s\\n" "$version" "$?"',
|
||||
"done",
|
||||
"exit 0",
|
||||
].join("\n"),
|
||||
{
|
||||
|
|
@ -1132,8 +1125,12 @@ NODE
|
|||
}
|
||||
|
||||
expect(result?.status).toBe(0);
|
||||
expect(result?.stdout).toContain("node_below_floor=1");
|
||||
expect(result?.stdout).toContain("node_at_floor=0");
|
||||
expect(result?.stdout).toContain("22.18.9=1");
|
||||
expect(result?.stdout).toContain("22.19.0=0");
|
||||
expect(result?.stdout).toContain("23.7.0=1");
|
||||
expect(result?.stdout).toContain("23.10.9=1");
|
||||
expect(result?.stdout).toContain("23.11.0=0");
|
||||
expect(result?.stdout).toContain("24.0.0=0");
|
||||
});
|
||||
|
||||
it("persists a supported Linux Node path before noninteractive shell guards", () => {
|
||||
|
|
@ -1414,10 +1411,7 @@ NODE
|
|||
mkdirSync(bin, { recursive: true });
|
||||
mkdirSync(outer, { recursive: true });
|
||||
mkdirSync(repo, { recursive: true });
|
||||
writeFileSync(
|
||||
join(outer, "package.json"),
|
||||
'{\n "packageManager": "yarn@4.5.0"\n}\n',
|
||||
);
|
||||
writeFileSync(join(outer, "package.json"), '{\n "packageManager": "yarn@4.5.0"\n}\n');
|
||||
writeFileSync(
|
||||
join(repo, "package.json"),
|
||||
'{\n "packageManager": "pnpm@11.2.2+sha512.test"\n}\n',
|
||||
|
|
@ -1545,7 +1539,7 @@ describe("install.sh macOS Homebrew Node behavior", () => {
|
|||
fi
|
||||
return 0
|
||||
}
|
||||
node_major_version() { echo 16; }
|
||||
node_is_supported() { return 1; }
|
||||
if ensure_macos_default_node_active; then
|
||||
echo "ensure returned success"
|
||||
else
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue