From ac0a7132877f24e357d5b05f70993cf0c94626c0 Mon Sep 17 00:00:00 2001 From: Kaguya-19 Date: Thu, 30 Jul 2026 16:50:18 +0800 Subject: [PATCH] fix: enforce Node 22 runtime before startup --- .nvmrc | 1 + scripts/check-node-runtime.mjs | 37 ++++++++++++++----- .../memory/edgeclaw-memory-core/package.json | 2 +- tests/cli/check-node-runtime.spec.ts | 35 ++++++++++++++++++ ui/package.json | 2 +- ui/server/index.js | 1 + 6 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 .nvmrc create mode 100644 tests/cli/check-node-runtime.spec.ts diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..53d1c14db --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22 diff --git a/scripts/check-node-runtime.mjs b/scripts/check-node-runtime.mjs index 6006d8623..7f8784169 100644 --- a/scripts/check-node-runtime.mjs +++ b/scripts/check-node-runtime.mjs @@ -1,5 +1,8 @@ const minimumNodeVersion = [22, 13, 0]; const minimumNodeVersionLabel = "22.13.0"; +const supportedNodeMajor = 22; +const maximumNodeVersionLabel = "23"; +const nodeVersionRequirementLabel = `>=${minimumNodeVersionLabel} and <${maximumNodeVersionLabel}`; function parseNodeVersion(version) { return version @@ -17,6 +20,14 @@ function isAtLeastMinimum(version) { return true; } +function isSupportedMajor(version) { + return (parseNodeVersion(version)[0] ?? 0) === supportedNodeMajor; +} + +function formatNodeVersion(version) { + return version.startsWith("v") ? version : `v${version}`; +} + function fail(message) { console.error(`[pilotdeck] ${message}`); process.exit(1); @@ -32,17 +43,25 @@ process.emitWarning = (warning, typeOrOptions, ...args) => { emitWarning(warning, typeOrOptions, ...args); }; -const nodeVersion = process.versions.node; -if (!isAtLeastMinimum(nodeVersion)) { +const testMode = process.env.PILOTDECK_RUNTIME_CHECK_TEST_MODE === "1"; +const nodeVersion = + testMode && process.env.PILOTDECK_TEST_NODE_VERSION + ? process.env.PILOTDECK_TEST_NODE_VERSION + : process.versions.node; +const skipSqliteCheck = testMode && process.env.PILOTDECK_TEST_SKIP_SQLITE === "1"; + +if (!isAtLeastMinimum(nodeVersion) || !isSupportedMajor(nodeVersion)) { fail( - `Node.js >=${minimumNodeVersionLabel} is required because PilotDeck uses node:sqlite. Current: v${nodeVersion}.`, + `Node.js ${nodeVersionRequirementLabel} is required because PilotDeck uses node:sqlite and native packages are built for Node.js ${supportedNodeMajor}. Current: ${formatNodeVersion(nodeVersion)}. Switch to Node.js ${supportedNodeMajor} and reinstall dependencies.`, ); } -try { - await import("node:sqlite"); -} catch { - fail( - `Current Node.js (v${nodeVersion}) does not provide node:sqlite. Switch to Node.js ${minimumNodeVersionLabel}+ and reinstall dependencies.`, - ); +if (!skipSqliteCheck) { + try { + await import("node:sqlite"); + } catch { + fail( + `Current Node.js (${formatNodeVersion(nodeVersion)}) does not provide node:sqlite. Switch to Node.js ${minimumNodeVersionLabel}+ and reinstall dependencies.`, + ); + } } diff --git a/src/context/memory/edgeclaw-memory-core/package.json b/src/context/memory/edgeclaw-memory-core/package.json index ad0952f56..e37e6ef84 100644 --- a/src/context/memory/edgeclaw-memory-core/package.json +++ b/src/context/memory/edgeclaw-memory-core/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": ">=22.13.0" + "node": ">=22.13.0 <23" }, "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/tests/cli/check-node-runtime.spec.ts b/tests/cli/check-node-runtime.spec.ts new file mode 100644 index 000000000..4a1a7b48f --- /dev/null +++ b/tests/cli/check-node-runtime.spec.ts @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { join } from "node:path"; +import test from "node:test"; + +const scriptPath = join(process.cwd(), "scripts", "check-node-runtime.mjs"); + +function runRuntimeCheck(nodeVersion: string) { + return spawnSync(process.execPath, [scriptPath], { + cwd: process.cwd(), + env: { + ...process.env, + PILOTDECK_RUNTIME_CHECK_TEST_MODE: "1", + PILOTDECK_TEST_NODE_VERSION: nodeVersion, + PILOTDECK_TEST_SKIP_SQLITE: "1", + }, + encoding: "utf8", + }); +} + +test("runtime check accepts the supported Node 22 range", () => { + assert.equal(runRuntimeCheck("22.13.0").status, 0); + assert.equal(runRuntimeCheck("22.22.0").status, 0); +}); + +test("runtime check rejects Node versions outside the supported range", () => { + const tooOld = runRuntimeCheck("22.12.0"); + assert.equal(tooOld.status, 1); + assert.match(tooOld.stderr, />=22\.13\.0 and <23/); + + const tooNew = runRuntimeCheck("25.5.0"); + assert.equal(tooNew.status, 1); + assert.match(tooNew.stderr, />=22\.13\.0 and <23/); + assert.match(tooNew.stderr, /native packages are built for Node\.js 22/); +}); diff --git a/ui/package.json b/ui/package.json index 53705578d..8a219b9c7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -6,7 +6,7 @@ "type": "module", "main": "server/index.js", "engines": { - "node": ">=22.13.0" + "node": ">=22.13.0 <23" }, "scripts": { "check:runtime": "node ../scripts/check-node-runtime.mjs", diff --git a/ui/server/index.js b/ui/server/index.js index 6733ad3cf..72928e5c0 100755 --- a/ui/server/index.js +++ b/ui/server/index.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +import '../../scripts/check-node-runtime.mjs'; // Load environment variables before other imports execute import { assertRequiredPilotDeckEnv } from './load-env.js'; // Install global fetch proxy (PILOTDECK_PROXY / HTTPS_PROXY) before any network calls