diff --git a/.agents/skills/nemoclaw-get-started/SKILL.md b/.agents/skills/nemoclaw-get-started/SKILL.md index e1e8c1b32e4..7c8d8eba98c 100644 --- a/.agents/skills/nemoclaw-get-started/SKILL.md +++ b/.agents/skills/nemoclaw-get-started/SKILL.md @@ -35,7 +35,7 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc | Dependency | Version | |------------|----------------------------------| | Linux | Ubuntu 22.04 LTS or later | -| Node.js | 20 or later | +| Node.js | 22.16 or later | | npm | 10 or later | | Container runtime | Supported runtime installed and running | | [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed | diff --git a/.agents/skills/nemoclaw-reference/references/troubleshooting.md b/.agents/skills/nemoclaw-reference/references/troubleshooting.md index 12dd07c9e8f..8174ea589fe 100644 --- a/.agents/skills/nemoclaw-reference/references/troubleshooting.md +++ b/.agents/skills/nemoclaw-reference/references/troubleshooting.md @@ -21,19 +21,19 @@ If you see an unsupported platform error, verify that you are running on a suppo ### Node.js version is too old -NemoClaw requires Node.js 20 or later. +NemoClaw requires Node.js 22.16 or later. If the installer exits with a Node.js version error, check your current version: ```console $ node --version ``` -If the version is below 20, install a supported release. +If the version is below 22.16, install a supported release. If you use nvm, run: ```console -$ nvm install 20 -$ nvm use 20 +$ nvm install 22 +$ nvm use 22 ``` Then re-run the installer. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f0f6137ca1..d5bddf7fd75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,7 @@ Open an issue when you encounter one of the following situations. Install the following before you begin. -- Node.js 20+ and npm 10+ +- Node.js 22.16+ and npm 10+ - Python 3.11+ (for blueprint and documentation builds) - Docker (running) - [uv](https://docs.astral.sh/uv/) (for Python dependency management) diff --git a/README.md b/README.md index 77bcf0566e7..9097327f41b 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ The sandbox image is approximately 2.4 GB compressed. During image push, the Doc | Dependency | Version | |------------|----------------------------------| | Linux | Ubuntu 22.04 LTS or later | -| Node.js | 20 or later | +| Node.js | 22.16 or later | | npm | 10 or later | | Container runtime | Supported runtime installed and running | | [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed | diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md index 4795fb52291..1a0ba5d7e19 100644 --- a/docs/reference/troubleshooting.md +++ b/docs/reference/troubleshooting.md @@ -47,19 +47,19 @@ If you see an unsupported platform error, verify that you are running on a suppo ### Node.js version is too old -NemoClaw requires Node.js 20 or later. +NemoClaw requires Node.js 22.16 or later. If the installer exits with a Node.js version error, check your current version: ```console $ node --version ``` -If the version is below 20, install a supported release. +If the version is below 22.16, install a supported release. If you use nvm, run: ```console -$ nvm install 20 -$ nvm use 20 +$ nvm install 22 +$ nvm use 22 ``` Then re-run the installer. diff --git a/install.sh b/install.sh index 67253287d5a..0f67d076609 100755 --- a/install.sh +++ b/install.sh @@ -213,15 +213,17 @@ spin() { command_exists() { command -v "$1" &>/dev/null; } -MIN_NODE_MAJOR=20 +MIN_NODE_VERSION="22.16.0" MIN_NPM_MAJOR=10 -RECOMMENDED_NODE_MAJOR=22 -RUNTIME_REQUIREMENT_MSG="NemoClaw requires Node.js >=${MIN_NODE_MAJOR} and npm >=${MIN_NPM_MAJOR} (recommended Node.js ${RECOMMENDED_NODE_MAJOR})." +RUNTIME_REQUIREMENT_MSG="NemoClaw requires Node.js >=${MIN_NODE_VERSION} and npm >=${MIN_NPM_MAJOR}." NEMOCLAW_SHIM_DIR="${HOME}/.local/bin" ORIGINAL_PATH="${PATH:-}" # Compare two semver strings (major.minor.patch). Returns 0 if $1 >= $2. +# Rejects prerelease suffixes (e.g. "22.16.0-rc.1") to avoid arithmetic errors. version_gte() { + [[ "$1" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || return 1 + [[ "$2" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || return 1 local -a a b IFS=. read -ra a <<<"$1" IFS=. read -ra b <<<"$2" @@ -300,7 +302,7 @@ ensure_supported_runtime() { [[ "$node_major" =~ ^[0-9]+$ ]] || error "Could not determine Node.js version from '${node_version}'. ${RUNTIME_REQUIREMENT_MSG}" [[ "$npm_major" =~ ^[0-9]+$ ]] || error "Could not determine npm version from '${npm_version}'. ${RUNTIME_REQUIREMENT_MSG}" - if ((node_major < MIN_NODE_MAJOR || npm_major < MIN_NPM_MAJOR)); then + if ! version_gte "${node_version#v}" "$MIN_NODE_VERSION" || ((npm_major < MIN_NPM_MAJOR)); then error "Unsupported runtime detected: Node.js ${node_version:-unknown}, npm ${npm_version:-unknown}. ${RUNTIME_REQUIREMENT_MSG} Upgrade Node.js and rerun the installer." fi @@ -344,9 +346,9 @@ install_nodejs() { spin "Installing nvm..." bash "$nvm_tmp" rm -f "$nvm_tmp" ensure_nvm_loaded - spin "Installing Node.js ${RECOMMENDED_NODE_MAJOR}..." bash -c ". \"$NVM_DIR/nvm.sh\" && nvm install ${RECOMMENDED_NODE_MAJOR} --no-progress" + spin "Installing Node.js 22..." bash -c ". \"$NVM_DIR/nvm.sh\" && nvm install 22 --no-progress" ensure_nvm_loaded - nvm use "${RECOMMENDED_NODE_MAJOR}" --silent + nvm use 22 --silent info "Node.js installed: $(node --version)" } diff --git a/nemoclaw/package-lock.json b/nemoclaw/package-lock.json index 7a44e78cba9..2a8023b9c82 100644 --- a/nemoclaw/package-lock.json +++ b/nemoclaw/package-lock.json @@ -16,7 +16,7 @@ "yaml": "^2.4.0" }, "devDependencies": { - "@types/node": "^20.19.37", + "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.57.0", "@typescript-eslint/parser": "^8.57.0", "eslint": "^9.39.4", @@ -26,7 +26,7 @@ "vitest": "^4.1.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.16.0" } }, "node_modules/@emnapi/core": { @@ -691,9 +691,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/nemoclaw/package.json b/nemoclaw/package.json index 0493fe15210..d338ee78566 100644 --- a/nemoclaw/package.json +++ b/nemoclaw/package.json @@ -29,7 +29,7 @@ "yaml": "^2.4.0" }, "devDependencies": { - "@types/node": "^20.19.37", + "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.57.0", "@typescript-eslint/parser": "^8.57.0", "eslint": "^9.39.4", @@ -39,7 +39,7 @@ "vitest": "^4.1.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=22.16.0" }, "files": [ "dist/", diff --git a/package.json b/package.json index b58159f406a..3eec63a48f8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ ".dockerignore" ], "engines": { - "node": ">=22.0.0" + "node": ">=22.16.0" }, "repository": { "type": "git", diff --git a/scripts/install.sh b/scripts/install.sh index 20481380607..e94becca3c1 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -126,10 +126,9 @@ refresh_path() { fi } -MIN_NODE_MAJOR=20 +MIN_NODE_VERSION="22.16.0" MIN_NPM_MAJOR=10 -RECOMMENDED_NODE_MAJOR=22 -RUNTIME_REQUIREMENT_MSG="NemoClaw requires Node.js >=${MIN_NODE_MAJOR} and npm >=${MIN_NPM_MAJOR} (recommended Node.js ${RECOMMENDED_NODE_MAJOR})." +RUNTIME_REQUIREMENT_MSG="NemoClaw requires Node.js >=${MIN_NODE_VERSION} and npm >=${MIN_NPM_MAJOR}." OS="$(uname -s)" ARCH="$(uname -m)" @@ -170,6 +169,22 @@ fi info "Node.js manager: $NODE_MGR" +# Compare two semver strings (major.minor.patch). Returns 0 if $1 >= $2. +# Rejects prerelease suffixes (e.g. "22.16.0-rc.1") to avoid arithmetic errors. +version_gte() { + [[ "$1" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || return 1 + [[ "$2" =~ ^[0-9]+(\.[0-9]+){0,2}$ ]] || return 1 + local -a a b + IFS=. read -ra a <<<"$1" + IFS=. read -ra b <<<"$2" + for i in 0 1 2; do + local ai=${a[$i]:-0} bi=${b[$i]:-0} + if ((ai > bi)); then return 0; fi + if ((ai < bi)); then return 1; fi + done + return 0 +} + version_major() { printf '%s\n' "${1#v}" | cut -d. -f1 } @@ -187,7 +202,7 @@ ensure_supported_runtime() { [[ "$node_major" =~ ^[0-9]+$ ]] || fail "Could not determine Node.js version from '${node_version}'. ${RUNTIME_REQUIREMENT_MSG}" [[ "$npm_major" =~ ^[0-9]+$ ]] || fail "Could not determine npm version from '${npm_version}'. ${RUNTIME_REQUIREMENT_MSG}" - if ((node_major < MIN_NODE_MAJOR || npm_major < MIN_NPM_MAJOR)); then + if ! version_gte "${node_version#v}" "$MIN_NODE_VERSION" || ((npm_major < MIN_NPM_MAJOR)); then fail "Unsupported runtime detected: Node.js ${node_version:-unknown}, npm ${npm_version:-unknown}. ${RUNTIME_REQUIREMENT_MSG} Upgrade Node.js and rerun the installer." fi @@ -197,13 +212,13 @@ ensure_supported_runtime() { # ── Install Node.js 22 if needed ──────────────────────────────── install_node() { - local current_major="" + local current_version="" if command -v node >/dev/null 2>&1; then - current_major="$(node -v 2>/dev/null | sed 's/^v//' | cut -d. -f1)" + current_version="$(node -v 2>/dev/null | sed 's/^v//')" fi - if [ "$current_major" = "22" ]; then - info "Node.js 22 already installed: $(node -v)" + if [ -n "$current_version" ] && version_gte "$current_version" "$MIN_NODE_VERSION"; then + info "Node.js v${current_version} meets minimum requirement (>= v${MIN_NODE_VERSION})" return 0 fi @@ -239,7 +254,7 @@ install_node() { sudo apt-get install -y -qq nodejs >/dev/null 2>&1 ;; none) - fail "No Node.js version manager found. Install Node.js 22 manually, then re-run." + fail "No Node.js version manager found. Install Node.js >=${MIN_NODE_VERSION} manually, then re-run." ;; esac diff --git a/test/install-preflight.test.js b/test/install-preflight.test.js index daed033a798..2d5e115049b 100644 --- a/test/install-preflight.test.js +++ b/test/install-preflight.test.js @@ -20,12 +20,12 @@ function writeExecutable(target, contents) { // Helpers shared across suites // --------------------------------------------------------------------------- -/** Fake node that reports v22.14.0. */ +/** Fake node that reports v22.16.0. */ function writeNodeStub(fakeBin) { writeExecutable( path.join(fakeBin, "node"), `#!/usr/bin/env bash -if [ "$1" = "--version" ] || [ "$1" = "-v" ]; then echo "v22.14.0"; exit 0; fi +if [ "$1" = "--version" ] || [ "$1" = "-v" ]; then echo "v22.16.0"; exit 0; fi if [ "$1" = "-e" ]; then if [[ "$2" == *"dependencies.openclaw"* ]]; then echo "2026.3.11" @@ -100,7 +100,7 @@ exit 98 const output = `${result.stdout}${result.stderr}`; expect(result.status).not.toBe(0); expect(output).toMatch(/Unsupported runtime detected/); - expect(output).toMatch(/Node\.js >=20 and npm >=10/); + expect(output).toMatch(/Node\.js >=22\.16\.0 and npm >=10/); expect(output).toMatch(/v18\.19\.1/); expect(output).toMatch(/9\.8\.1/); }); @@ -117,7 +117,7 @@ exit 98 path.join(fakeBin, "node"), `#!/usr/bin/env bash if [ "$1" = "--version" ]; then - echo "v22.14.0" + echo "v22.16.0" exit 0 fi if [ "$1" = "-e" ]; then @@ -212,7 +212,7 @@ exit 98 path.join(fakeBin, "node"), `#!/usr/bin/env bash if [ "$1" = "--version" ]; then - echo "v22.14.0" + echo "v22.16.0" exit 0 fi if [ "$1" = "-e" ]; then @@ -295,7 +295,7 @@ exit 98 path.join(fakeBin, "node"), `#!/usr/bin/env bash if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then - echo "v22.14.0" + echo "v22.16.0" exit 0 fi exit 99 @@ -375,7 +375,7 @@ echo "Darwin" path.join(fakeBin, "node"), `#!/usr/bin/env bash if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then - echo "v22.14.0" + echo "v22.16.0" exit 0 fi if [ "$1" = "-e" ]; then @@ -641,7 +641,7 @@ fi`, path.join(fakeBin, "node"), `#!/usr/bin/env bash if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then - echo "v22.14.0" + echo "v22.16.0" exit 0 fi if [ "$1" = "-e" ]; then @@ -1277,7 +1277,7 @@ exit 0`, expect(`${result.stdout}${result.stderr}`).toMatch(/npm was not found on PATH/); }); - it("succeeds with acceptable Node.js 20 and npm 10", () => { + it("succeeds with acceptable Node.js 22.16 and npm 10", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-runtime-ok-")); const fakeBin = path.join(tmp, "bin"); fs.mkdirSync(fakeBin); @@ -1285,7 +1285,7 @@ exit 0`, writeExecutable( path.join(fakeBin, "node"), `#!/usr/bin/env bash -if [ "$1" = "--version" ]; then echo "v20.0.0"; exit 0; fi +if [ "$1" = "--version" ]; then echo "v22.16.0"; exit 0; fi exit 0`, ); writeExecutable( @@ -1301,6 +1301,32 @@ exit 0`, expect(`${result.stdout}${result.stderr}`).toMatch(/Runtime OK/); }); + it("rejects Node.js 20 which is below the 22.16 minimum", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-runtime-node20-")); + const fakeBin = path.join(tmp, "bin"); + fs.mkdirSync(fakeBin); + + writeExecutable( + path.join(fakeBin, "node"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then echo "v20.18.0"; exit 0; fi +exit 0`, + ); + writeExecutable( + path.join(fakeBin, "npm"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then echo "10.9.2"; exit 0; fi +exit 0`, + ); + + const result = callEnsureSupportedRuntime(fakeBin); + + expect(result.status).not.toBe(0); + const output = `${result.stdout}${result.stderr}`; + expect(output).toMatch(/Unsupported runtime detected/); + expect(output).toMatch(/v20\.18\.0/); + }); + it("rejects node that returns a non-numeric version", () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-runtime-badver-")); const fakeBin = path.join(tmp, "bin"); @@ -1346,7 +1372,7 @@ describe("curl-pipe installer release-tag resolution", () => { writeExecutable( path.join(fakeBin, "node"), `#!/usr/bin/env bash -if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then echo "v22.14.0"; exit 0; fi +if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then echo "v22.16.0"; exit 0; fi if [ "$1" = "-e" ]; then exit 1; fi exit 99`, );