diff --git a/README.md b/README.md index e66ebbd95..6cca1391c 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Update either installation with `rea update`. - macOS 12 or newer - Ubuntu 24.04+, Fedora 41+, or 64-bit Arch Linux -- Node.js 22.19+ or 24.11+ (including newer releases) +- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ - npm; REA does not require or install a particular npm version Native binary analysis requires [Hopper](https://www.hopperapp.com/) or [Ghidra](#ghidra-read-only-analysis-provider). Hopper is separate software with its own license; its demo supports analysis with vendor-defined limits. REA can use Ghidra that you have already installed. diff --git a/README_ar.md b/README_ar.md index 43e3c43c5..9c07329f6 100644 --- a/README_ar.md +++ b/README_ar.md @@ -126,7 +126,7 @@ rea setup - macOS 12 أو أحدث - Ubuntu 24.04+ أو Fedora 41+ أو Arch Linux بنواة 64 بت -- Node.js 22.19+ أو 24.11+ وnpm +- Node.js 22.x (>=22.19) أو 24.x (>=24.11) أو 26+ وnpm يتطلب تحليل الملفات التنفيذية الأصلية Hopper أو Ghidra. Hopper برنامج منفصل، وللوضع التجريبي قيود يحددها المورّد، لكن الترخيص المدفوع ليس إلزاميًا. diff --git a/README_ja.md b/README_ja.md index 382b178f9..edc1763f0 100644 --- a/README_ja.md +++ b/README_ja.md @@ -126,7 +126,7 @@ rea setup - macOS 12 以降 - Ubuntu 24.04+、Fedora 41+、または 64 ビット Arch Linux -- Node.js 22.19+ または 24.11+ と npm +- Node.js 22.x (>=22.19)、24.x (>=24.11)、または 26+ と npm ネイティブバイナリ解析には Hopper または Ghidra が必要です。Hopper は別製品です。デモにはベンダー所定の制限がありますが、有料ライセンスは必須ではありません。 diff --git a/README_ko.md b/README_ko.md index ae1c1bbed..c0f7096e1 100644 --- a/README_ko.md +++ b/README_ko.md @@ -127,7 +127,7 @@ rea setup - macOS 12 이상 - Ubuntu 24.04+, Fedora 41+ 또는 64비트 Arch Linux -- Node.js 22.19+ 또는 24.11+와 npm +- Node.js 22.x (>=22.19), 24.x (>=24.11) 또는 26+와 npm 네이티브 바이너리 분석에는 Hopper 또는 Ghidra가 필요합니다. Hopper는 별도 소프트웨어입니다. 데모에는 공급업체의 제한이 있지만 유료 라이선스가 필수는 아닙니다. diff --git a/README_zh.md b/README_zh.md index c465662a0..671e85754 100644 --- a/README_zh.md +++ b/README_zh.md @@ -126,7 +126,7 @@ rea setup - macOS 12 或更高版本 - Ubuntu 24.04+、Fedora 41+ 或 64 位 Arch Linux -- Node.js 22.19+ 或 24.11+,以及 npm +- Node.js 22.x (>=22.19)、24.x (>=24.11) 或 26+,以及 npm 原生二进制分析需要 Hopper 或 Ghidra。Hopper 是独立软件;演示模式有厂商规定的限制,不要求购买许可证。 diff --git a/docs/installation.md b/docs/installation.md index eadb07e00..65983bc6c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -36,7 +36,7 @@ Setup continues to pin persistent MCP registrations to the exact version that performed setup. Running current setup later migrates unversioned or older managed registrations through the normal reviewed setup transaction. -REA supports Node.js 22.19+ and 24.11+ (including newer releases). It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew. +REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew. Running `npm install rea-agents` without `--global` installs the executable only in the current project's `node_modules/.bin`; it does not make `rea` available diff --git a/docs/user-visible-errors.csv b/docs/user-visible-errors.csv index 89ea16f33..b4c48f58d 100644 --- a/docs/user-visible-errors.csv +++ b/docs/user-visible-errors.csv @@ -55,7 +55,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test 46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed 47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test -48,src/application/Doctor.ts:node,Runtime version is outside supported ranges,Install Node.js 22.19+ or 24.11+.,Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test +48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test 49,src/application/Doctor.ts:host,Host OS/distribution is unsupported,"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; clearly states supported choices,Keep current copy,unchanged,verified by doctor unit tests 50,src/application/Doctor.ts:analysis engine,Optional deep-analysis executable cannot be found,"Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.",Low risk in setup-specific diagnostic; exact recovery paths,Keep current copy,unchanged,verified by doctor unit tests 51,src/application/Doctor.ts:target,Optional target path fails shared validation,Supply a readable local app or program path.,Low risk; tells caller valid target class,Keep current copy,unchanged,verified by doctor unit test @@ -65,7 +65,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 55,src/application/Setup.ts:optional engine,Setup completes without optional deep-analysis engine,Hopper is optional for non-Hopper providers. Rerun with --yes --install-hopper for deep native analysis.,Exposes provider terminology but this is an explanatory setup status rather than an error; recovery command is exact,Keep current copy,unchanged,verified by setup unit test 56,src/application/Setup.ts:doctor fallback,Post-setup diagnostics remain unhealthy,Run rea doctor and apply each reported remediation.,Low risk; delegates to structured per-check recovery,Keep current copy,unchanged,verified by exact setup unit test 57,src/application/Setup.ts:unsupported platform,Platform is neither macOS nor Linux,REA supports Hopper on macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by setup unit test -58,src/application/Setup.ts:unsupported Node,Runtime version cannot run setup,Install Node.js 22.19+ or 24.11+ and rerun setup.,Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests +58,src/application/SetupHost.ts:unsupported Node,Runtime version cannot run setup,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.",Low risk; exact dependency and retry,Keep current copy,unchanged,verified by setup host tests 59,src/application/Setup.ts:unsupported macOS,macOS version is missing or older than 12,Upgrade to macOS 12 or newer.,Low risk; exact support floor,Keep current copy,unchanged,verified by setup host tests 60,src/application/Setup.ts:unsupported Linux,Linux distribution is outside supported set,"REA supports Hopper on Ubuntu 24.04+, Fedora 41+, and 64-bit Arch Linux.",Low risk; exact supported alternatives,Keep current copy,unchanged,verified by setup unit test 61,src/application/ProcessCli.ts:readJson read,Process scenario or evidence file cannot be read,Raw filesystem exception through CLI framework,Can expose local paths/codes and no recovery,"Process input file could not be read. Check that the path exists and is readable.",implemented,verified by focused CLI test @@ -83,11 +83,11 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement 74,install.sh:unknown option,Installer receives an unsupported flag,unknown option with caller-provided flag,Low risk; identifies offending input,Keep current copy,unchanged,verified by shell branch inspection 75,install.sh:platform,Installer runs outside macOS or supported Linux,REA supports macOS and selected 64-bit Linux distributions.,Low risk; states support boundary,Keep current copy,unchanged,verified by shell branch inspection 76,install.sh:curl missing,curl is absent,curl is required.,No recovery instruction,"curl is required. Install curl, then rerun this installer.",implemented,verified by exact missing-curl installer test -77,install.sh:node missing,node is absent,Node.js 22.19+ or 24.11+ is required; install it from nodejs.org and retry.,Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection +77,install.sh:node missing,node is absent,"Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from nodejs.org and retry.",Low risk; exact versions and recovery,Keep current copy,unchanged,verified by shell branch inspection 78,install.sh:npm missing,npm is absent,npm is required; install it with Node.js and retry.,Low risk; exact recovery,Keep current copy,unchanged,verified by shell branch inspection 79,install.sh:node unreadable,node exists but does not report a parseable version,could not determine the active Node.js version.,No cause-local recovery,"The active Node.js version could not be read. Check that node works and is on PATH, then retry.",implemented,verified by exact unreadable-Node-version installer test -80,install.sh:Node 22 floor,Node 22 minor version is below 19,Node.js VERSION is unsupported; use Node.js 22.19+ or 24.11+.,Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection -81,install.sh:Node 24 floor,Node 24 minor version is below 11,Node.js VERSION is unsupported; use Node.js 24.11 or newer.,Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection +80,install.sh:Node 22 floor,Node 22 minor version is below 19,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and supported alternatives,Keep current copy,unchanged,verified by exact installer test using Node 20 and branch inspection +81,install.sh:Node 24 floor,Node 24 minor version is below 11,"Node.js VERSION is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact detected version and floor,Keep current copy,unchanged,verified by shell branch inspection 82,install.sh:version syntax,Explicit version is not exact semantic version,version must be an exact semantic version.,Low risk; exact correction,Keep current copy,unchanged,verified by shell branch inspection 83,install.sh:release lookup,Latest GitHub release request fails,could not resolve the latest GitHub release.,No fallback despite supporting pinned versions,"The latest REA release could not be resolved. Check network access or pass --version VERSION, then retry.",implemented,verified by exact release-lookup installer test 84,install.sh:release response,Latest release response is not valid expected JSON,Node JSON parser stack followed by the safe release-response message,Leaks a parser stack and buries recovery guidance,The release response was invalid. Retry later or pass --version VERSION.,implemented,verified by exact malformed-release test with parser stderr suppressed diff --git a/docs/windows-ghidra-p0.md b/docs/windows-ghidra-p0.md index 25a056ba7..64ed8f5ae 100644 --- a/docs/windows-ghidra-p0.md +++ b/docs/windows-ghidra-p0.md @@ -15,7 +15,7 @@ mutable targets require separate acceptance. Once the required native controls are implemented, the intended P0 is limited to: - a Windows x64 host; -- Node.js 22.19+ or 24.11+; +- Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+; - an operator-installed official Ghidra 12.1.4 distribution; - a 64-bit full JDK 21; - an explicit native, non-managed, non-DLL x86-64 PE application; and diff --git a/install.sh b/install.sh index 34a5b280c..2df6548e2 100755 --- a/install.sh +++ b/install.sh @@ -55,17 +55,15 @@ fi platform="$(uname -s)" [[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions." command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer." -command -v node >/dev/null 2>&1 || fail "Node.js 22.19+ or 24.11+ is required; install it from https://nodejs.org and retry." +command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry." command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry." node_version="$(node -p 'process.versions.node' 2>/dev/null || true)" IFS=. read -r node_major node_minor _node_patch <<<"$node_version" [[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry." -if ! ((node_major == 22 && node_minor >= 19 || node_major >= 24)); then - fail "Node.js $node_version is unsupported; use Node.js 22.19+ or 24.11+." -fi -if ((node_major == 24 && node_minor < 11)); then - fail "Node.js $node_version is unsupported; use Node.js 24.11 or newer." +if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] || + ! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then + fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+." fi if [[ -n "$version" ]]; then diff --git a/src/application/Doctor.test.ts b/src/application/Doctor.test.ts index f686fd922..bd6628a75 100644 --- a/src/application/Doctor.test.ts +++ b/src/application/Doctor.test.ts @@ -19,13 +19,27 @@ describe("doctor", () => { result.checks.map(({ name, remediation }) => [name, remediation]), ), ).toEqual({ - node: "Install Node.js 22.19+ or 24.11+.", + node: "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.", host: "REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.", hopper: "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH.", target: "Supply a readable local app or program path.", }); }); + it("reports Node 25 as unsupported", async () => { + const result = await runDoctor(undefined, host({ nodeVersion: "25.1.0" })); + + expect(result.healthy).toBe(false); + expect(result.checks).toContainEqual( + expect.objectContaining({ + name: "node", + ok: false, + detail: "25.1.0", + classification: "missing_dependency", + }), + ); + }); + it.each(["11.7", undefined])( "rejects unsupported macOS version %s", async (version) => { diff --git a/src/application/DoctorDiagnostics.ts b/src/application/DoctorDiagnostics.ts index e9b4a697e..23d19fddf 100644 --- a/src/application/DoctorDiagnostics.ts +++ b/src/application/DoctorDiagnostics.ts @@ -92,7 +92,7 @@ export const doctorHealthy = ( const nodeCheck = (host: DoctorHost): DoctorCheck => check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, { - remediation: "Install Node.js 22.19+ or 24.11+.", + remediation: "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.", classification: "missing_dependency", }); diff --git a/src/application/Setup.fixture.ts b/src/application/Setup.fixture.ts index f119ec581..61b8f1dc5 100644 --- a/src/application/Setup.fixture.ts +++ b/src/application/Setup.fixture.ts @@ -16,7 +16,7 @@ import type { ClientRegistrationStatus } from "./ClientRegistrationStatus.js"; /** Recording setup host for service-level planning and recovery tests. */ export class FakeSetupHost implements SetupHost { readonly platform: NodeJS.Platform; - nodeVersion = "25.1.0"; + nodeVersion = "24.18.0"; version: string | undefined = "14.5"; distribution: LinuxDistribution | undefined; hopper: string | undefined; diff --git a/src/application/Setup.recovery.test.ts b/src/application/Setup.recovery.test.ts index f228582af..13638a0d0 100644 --- a/src/application/Setup.recovery.test.ts +++ b/src/application/Setup.recovery.test.ts @@ -4,6 +4,23 @@ import { FakeSetupHost, options } from "./Setup.fixture.js"; import { runSetup } from "./Setup.js"; describe("setup workflow", () => { + it("rejects Node 25 before applying setup actions", async () => { + const host = new FakeSetupHost(); + host.nodeVersion = "25.1.0"; + host.clients = [{ name: "codex", configPath: "/codex.toml" }]; + + const result = await runSetup( + { ...options(true, true), installSkill: true, clientIds: ["codex"] }, + host, + ); + + expect(result.status).toBe("needs_human"); + expect(result.appliedActions).toEqual([]); + expect(host.hopperInstalls).toBe(0); + expect(host.configurations).toBe(0); + expect(host.skillInstalls).toBe(0); + }); + it("records every detected client outcome after an earlier failure", async () => { const host = new FakeSetupHost(); host.hopper = "/Applications/Hopper"; @@ -105,7 +122,7 @@ describe("setup scoped readiness", () => { const nodeHost = new FakeSetupHost(); nodeHost.nodeVersion = "20.0.0"; expect((await runSetup(options(true), nodeHost)).remediation).toBe( - "Install Node.js 22.19+ or 24.11+ and rerun setup.", + "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup.", ); const macHost = new FakeSetupHost(); diff --git a/src/application/SetupHost.ts b/src/application/SetupHost.ts index 471fc3f19..31075a7a7 100644 --- a/src/application/SetupHost.ts +++ b/src/application/SetupHost.ts @@ -63,7 +63,7 @@ export const hostRemediation = async ( installHopper: boolean, ): Promise => { if (!supportsNodeVersion(host.nodeVersion)) - return "Install Node.js 22.19+ or 24.11+ and rerun setup."; + return "Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ and rerun setup."; if (!installHopper) return undefined; if (host.platform !== "darwin" && host.platform !== "linux") return "REA supports Hopper on macOS and selected 64-bit Linux distributions."; diff --git a/src/domain/runtimeVersion.test.ts b/src/domain/runtimeVersion.test.ts new file mode 100644 index 000000000..307c6778d --- /dev/null +++ b/src/domain/runtimeVersion.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; + +import { supportsNodeVersion } from "./runtimeVersion.js"; + +describe("supported Node.js versions", () => { + it.each([ + ["20.0.0", false], + ["22.18.9", false], + ["22.19.0", true], + ["22.99.0", true], + ["23.0.0", false], + ["24.10.9", false], + ["24.11.0", true], + ["24.99.0", true], + ["25.1.0", false], + ["26.0.0", true], + ["27.0.0", true], + ["24.11.0-rc.1", false], + ["26.0.0-nightly.20261001", false], + ["24.11", false], + ["24.11.invalid", false], + ["invalid", false], + ])("classifies %s as supported=%s", (version, expected) => { + expect(supportsNodeVersion(version)).toBe(expected); + }); +}); diff --git a/src/domain/runtimeVersion.ts b/src/domain/runtimeVersion.ts index 0ab5647d7..dc6e08b64 100644 --- a/src/domain/runtimeVersion.ts +++ b/src/domain/runtimeVersion.ts @@ -1,10 +1,8 @@ +import { satisfies } from "semver"; + +/** Supported runtime range, kept in sync with the published package engines. */ +export const SUPPORTED_NODE_VERSION_RANGE = "^22.19.0 || ^24.11.0 || >=26.0.0"; + /** Return whether a Node.js version satisfies REA's tested runtime families. */ -export const supportsNodeVersion = (version: string): boolean => { - const [majorText = "", minorText = ""] = version.split("."); - const major = Number.parseInt(majorText, 10); - const minor = Number.parseInt(minorText, 10); - return ( - (major === 22 && minor >= 19) || - (major >= 24 && (major !== 24 || minor >= 11)) - ); -}; +export const supportsNodeVersion = (version: string): boolean => + satisfies(version, SUPPORTED_NODE_VERSION_RANGE); diff --git a/tests/boundary/filesystem/installScript.test.ts b/tests/boundary/filesystem/installScript.test.ts index 6891881a1..f6590e6cb 100644 --- a/tests/boundary/filesystem/installScript.test.ts +++ b/tests/boundary/filesystem/installScript.test.ts @@ -30,13 +30,33 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { expect(await readdir(fixture.temporary)).toEqual([]); }); - it("accepts Node 25 without installing or replacing it", async () => { - const fixture = await createFixture(); - const result = await runInstaller(fixture, ["--version", "0.3.0"], { - FAKE_NODE_VERSION: "25.1.0", - }); - expect(result.stdout).toContain("Runtime: Node.js 25.1.0"); - }); + it.each(["22.19.0", "24.11.0", "26.0.0", "27.0.0"])( + "accepts supported Node %s without installing or replacing it", + async (version) => { + const fixture = await createFixture(); + const result = await runInstaller(fixture, ["--version", "0.3.0"], { + FAKE_NODE_VERSION: version, + }); + expect(result.stdout).toContain(`Runtime: Node.js ${version}`); + }, + ); + + it.each(["22.18.9", "23.0.0", "24.10.9", "25.1.0", "26.0.0-rc.1"])( + "rejects unsupported Node %s before invoking npm", + async (version) => { + const fixture = await createFixture(); + await expect( + runInstaller(fixture, ["--version", "0.3.0"], { + FAKE_NODE_VERSION: version, + }), + ).rejects.toMatchObject({ + stderr: expect.stringContaining(`Node.js ${version} is unsupported`), + }); + await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({ + code: "ENOENT", + }); + }, + ); it("resolves and validates the latest REA release tag", async () => { const fixture = await createFixture(); @@ -71,7 +91,7 @@ describe("curl installer scenarios", { timeout: 20_000 }, () => { "unsupported Node", ["--version", "0.3.0"], { FAKE_NODE_VERSION: "20.0.0" }, - "REA installation failed: Node.js 20.0.0 is unsupported; use Node.js 22.19+ or 24.11+.\n", + "REA installation failed: Node.js 20.0.0 is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.\n", ], [ "unreadable Node version", diff --git a/tests/conformance/releaseConfiguration.test.ts b/tests/conformance/releaseConfiguration.test.ts index f64e1569d..dc6a3ef87 100644 --- a/tests/conformance/releaseConfiguration.test.ts +++ b/tests/conformance/releaseConfiguration.test.ts @@ -2,6 +2,8 @@ import { readFile } from "node:fs/promises"; import { describe, expect, it } from "vitest"; +import { SUPPORTED_NODE_VERSION_RANGE } from "../../src/domain/runtimeVersion.js"; + const readJson = async (path: string): Promise => JSON.parse(await readFile(path, "utf8")) as unknown; @@ -14,6 +16,12 @@ const readmes = [ ] as const; describe("release configuration", () => { + it("keeps doctor and setup runtime support aligned with package engines", async () => { + expect(await readJson("package.json")).toMatchObject({ + engines: { node: SUPPORTED_NODE_VERSION_RANGE }, + }); + }); + it("updates every package-version artifact in release PRs", async () => { const configuration = await readJson("release-please-config.json");