diff --git a/CHANGELOG.md b/CHANGELOG.md index 0feb6c9..a3f9bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ - 点「稍后」或「我已运行」后按**插件版本**记入 localStorage:同一版本不再弹,升级换了版本后重新提示一次; - 该 route 只在宿主有 `webServer` 时挂载(`ctx.inject(['webServer'])`),headless/ACP 主机行为不变; - 它只是提示:不 spawn、不下载、不改 `bundle.json`,也不跑 setup。 +- **`setup.mjs` 末尾新增 Ghidra 冒烟(`smoke` 步,`--skip-smoke` 可关)**:`rea doctor` 只校验路径、版本与 + 会用哪个 Java,**从不导入一个二进制**。首次真实导入要付一次性成本(本机实测:小 PE 约 36 s,6 MB DLL 约 64 s), + 一旦桥接或 native addon 有问题,操作者只在**第一次 MCP 工具调用超时**时才撞见 —— 那读起来就是「升级把 REA 搞坏了」。 + 现在供给结束会用 JDK 自带的 `bin/java` 跑一次 `rea analyze --json`,把「能不能分析」变成有测量结果的结论; + 失败即 `FAIL` 并计入退出码。`--skip-smoke` 跳过(CI 或低内存机器)。 ### 修复 @@ -38,6 +43,23 @@ - **`foreignReaRegistration` 不剥行尾注释**:`serverName: rea # upstream` 会被解析成 `rea # upstream` ≠ `rea`, 重复注册漏报 —— 而这正是人手编辑那个文件时最常见的形态。现在按 YAML 规则取值:引号内原样,引号外 ` #` 之后是 注释(`rea#1` 仍然是值)。 +- **`npm run check-updates` 在非 Windows 上是坏的**:这个 script 直接写死了 + `powershell -NoProfile -File ./check-updates.ps1`,而脚本本身读 `%APPDATA%`、优先 `rea.cmd` + (并刻意避开 `rea.ps1` 垫片),所以在 macOS/Linux 上即使装了 `pwsh` 也只会从一个不存在的布局里 + 给出一个自信的错答案。现在改为 `node scripts/check-updates.mjs`:Windows 上转发给 `powershell` + (缺失时退 `pwsh`),其它平台说明原因、列出 `node bin/rea-shim.mjs --shim-status` 与 + `node scripts/setup.mjs --dry-run`,并以**非零码**退出 —— 不把「不适用」伪装成「全绿」。 +- **供给期的 `rea doctor` 检查的不是本次供给的引擎**:那一步没传 `GHIDRA_INSTALL_DIR` / `JAVA_HOME`, + REA 于是回落到探测本机,于是报告里出现的是系统那份 Ghidra 与 JDK(本机实测:报的是 `D:\ghidra\...` + 系统 + Java 25,而 vendor 里那份从未被检查)。现在 doctor 与冒烟共用与镜像 shim 完全相同的一组变量。 +- **复用 Node 时不记录候选的真实版本**:`reusableNodeChecked` 只回 `{ok}`,于是 `bundle.json` 的 + `versions.node` 落到 `process.versions.node` —— 那是**跑 setup 的解释器**,不是被复用的那个。两者不同时 + (`REA_BUNDLE_NODE`,或 PATH 首个 `node` 是另一个安装)记录与实际就静默不一致。现在探针把 `-v` 的第一行 + 带回,复用路径照实记录。 +- **manifest 把 Node 钉在 24.21.0(含已知漏洞)**:24.22.1 是 2026-10-21 的安全发布(Brotli 解码器越界写 RCE、 + 两处 TLS 主机名绕过、移除 eMudhra 中间 CA)。复用路径在装有新 Node 的机器上没问题,但**干净环境走下载路径 + 会装回带漏洞的 24.21.0**。六个平台的 `version` / `url` / `size` / `sha256` 全部升到 24.22.1 + (校验和取自官方 `SHASUMS256.txt`;win32-x64 在本机下载、比对、解压并跑通 `node -v` / `npm -v` 后写入)。 ### 升级 @@ -56,11 +78,13 @@ ### 验证 -- 单测 **78 条**(新增「UTF-16 的 `bundle.json` 被 setup 读到」「UTF-16 的 `bundle.json` 被 shim 读到」两条; - 重复注册用例扩成「带行尾注释」;「setup 指南」5 条:钉子精确/家族比对、无记录=首装、旧 REA=stale、UTF-16 记录、 - 坏记录/无 REA 字段都要求 setup)。 +- 单测 **84 条**(新增「冒烟在没有引擎时跳过」「`--skip-smoke` 直接关闭」「冒烟必须真调 `analyze` 且失败即 + `FAIL`、探针与冒烟共用 vendor 变量」三条;此前 81 条:UTF-16 的 `bundle.json` 被 setup / shim 读到、「setup 指南」 + 5 条、「check-updates 包装器」3 条)。 +- `manifest` 审计:**24 条 fetch 全部 hash-pinned,0 TOFU**(Node 六平台换到 24.22.1 后重跑)。 - `verify-branch.ps1` 新增 §9:指南链路四端(`bin/rea-guide.mjs` 的 route、`index.mjs` 的挂载、`client.js` 的 - fetch、`cordis.patch.yml` 的行)逐一比对,防止改名后指南静默不出现。 + fetch、`cordis.patch.yml` 的行)逐一比对,防止改名后指南静默不出现;§10:`npm run check-updates` 必须走 + 包装器,且包装器必须保留「Windows 才跑」与两条替代命令。 - 未发版:无 tag / 无 Release / npm 仍 **2.0.10**。 ## 2.0.10 — 2026-10-19 diff --git a/README.md b/README.md index 6d1d0c1..b6cb9a4 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,10 @@ Three details in that row are load-bearing, and all three were bugs first: - **The `!!js` scalar is quoted.** A bare scalar containing `? ` and `: ` is not one scalar: YAML reads it as an explicit key/value pair, so the loader receives a *mapping* where an expression belongs. Measured against the harness's own dialect (`JSON_SCHEMA` plus the `!!js` type from `app.asar`), the unquoted form made `args[0]` into `{"[object Object]": "..."}` — an object the row cannot launch with. Quoting removes the ambiguity, and the JS string literals inside then use single quotes. - **Forward slashes, never backslashes.** A single-quoted YAML scalar keeps `\` literal, and JavaScript then consumes it: `'\plugins'` is not a path but the BEL control character, `'\rea-bundle'` becomes a carriage return and `'\bin'` a backspace — the path is destroyed silently. Forward slashes work on Windows and on POSIX. - **The `DSH_HOME` fallback is `/.dsh`, not ``.** The shim, `setup.mjs` and `bootstrap.ps1` all resolve an unset `DSH_HOME` to `/.dsh/...`; a fallback that stopped one level higher made the row point at a file provisioning never writes — invisible on any machine where `DSH_HOME` happens to be set. +- **With `DSH_HOME`, `USERPROFILE` and `HOME` all empty, the expression resolves to `/.dsh/...`** — the one + combination the concatenation cannot improve on. That is a service or container account with no home + directory; the row then cannot start, and because `failOnStartupError: false` keeps that quiet, the only + symptom is "no `mcp__rea__*` tools". Set `DSH_HOME` explicitly on such an account. - **No `path`/`require` in a `!!js` expression**: `ctx` is the loader scope, so the DSH_HOME fallback is written as plain concatenation. `npm run bundle:audit` enforces the first three: it evaluates this exact expression with `DSH_HOME` unset, empty and set, and rejects a bare scalar that YAML would read as a mapping. @@ -268,12 +272,20 @@ Ask for a `mcp__rea__*` call — `binary_session` reports the engine checks (`configuration` / `installation` / `headless` / `native_decompiler` / `java`), all of which should be `ok: true`. Then `open_binary` a PE, `binary_overview`, and `procedure_pseudo_code`. -Provisioning itself ends with two probes: `rea doctor --json` (reported, with unmet checks named) and the -exact shim launch the MCP row will use. +Provisioning itself ends with three checks: `rea doctor --json` (reported, with unmet checks named), a +**Ghidra smoke import** that runs `rea analyze` on the JDK's own `bin/java` and fails the run when it +cannot import (`--skip-smoke` omits it), and the exact shim launch the MCP row will use. The smoke step +exists because `doctor` validates paths, versions and which Java would be chosen — it never imports a +binary, so a broken bridge or a mismatched native addon used to surface only as an MCP timeout. + +**The first analysis of each new target is slow.** Measured here: 36 s for a small PE, 64 s for a 6 MB +DLL — every import starts a JVM and runs Ghidra's default auto-analysis. If the first +`open_binary` → `list_segments` on a large file times out, that is this cost and not a broken engine: +call it again and the open target answers immediately, because it keeps its ephemeral project. ## Verified against -Author's machine, 2026-10-13 / 14 / 16 / 19: +Author's machine, 2026-10-13 / 14 / 16 / 19 / 23: | Component | Version | |---|---| @@ -281,8 +293,8 @@ Author's machine, 2026-10-13 / 14 / 16 / 19: | DeepSeek Harness | desktop runtime **0.2.0-rc.2**, with its bundled dsh CLI 0.1.7-rc.2 | | `@deepseek-ai/dsh-mcp-client` (row provider) | bundled by the harness | | Ghidra | 12.1.4 | -| JDK | Temurin 21 (provisioned) / Microsoft OpenJDK 25.0.4 (adopted) | -| Node.js | 24.21.0 (REA engines: `^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`) | +| JDK | Temurin 21 (provisioned) / Amazon Corretto 25.0.5 (adopted by the host) | +| Node.js | **24.22.1** adopted (REA engines: `^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`; ABI 137) | | Host | Windows 11 x64 (build 26200) | Exercised live: a clean `setup.mjs --reuse` run reached `setup complete`; an MCP `initialize` handshake @@ -295,6 +307,13 @@ Not exercised end to end here, and therefore not claimed: the ~775 MB JDK + Ghid by `--dry-run`, but this machine adopted its existing installs with `--reuse`), and first runs on macOS/Linux. +On 2026-10-23 the same chain was re-verified after the host Node moved to **24.22.1**: the vendored +`rea-windows-x64.node` and both `conpty` addons loaded under ABI 137; `binary_overview` on `conpty.node` +reported 10 segments / 1178 procedures / 618 strings; `procedure_pseudo_code` returned real decompiled +output; and `java -version` from the vendored JDK answered `21.0.12.1`. The one thing that did **not** +work on the first try was the first `list_segments` — it timed out at 64 s of cold Ghidra analysis, which +is what the smoke step and the note above are for. + ## Scope and caveats - REA exposes 207 tools at 6.7.0, so every session's tool list grows. @@ -410,7 +429,10 @@ back on the next start. - **Upstream REA / Ghidra**: update the pins in the manifest, then re-run setup. REA version-gates Ghidra (12.1.x today), so update REA first. -`check-updates.ps1` (Windows PowerShell) reports the state of all of these at once. +`check-updates.ps1` (Windows PowerShell) reports the state of all of these at once. `npm run check-updates` +forwards to it on Windows (PowerShell 5.1, then `pwsh`); on macOS/Linux it prints why the script cannot +answer there and names the two portable commands instead — `node bin/rea-shim.mjs --shim-status` and +`node scripts/setup.mjs --dry-run` — exiting non-zero rather than reporting a green it did not earn. ## Still prefer the configuration-only 1.x line? diff --git a/README.zh-CN.md b/README.zh-CN.md index 70013e2..3e1e78c 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -188,6 +188,9 @@ node scripts/setup.mjs --reuse # 或先 --dry-run 看计划 - **`!!js` 标量必须加引号。** 裸标量里同时出现 `? ` 和 `: ` 时,它就不是一个标量了:YAML 会把它读成显式键值对,于是 loader 拿到的是一个 **mapping** 而不是表达式节点。用 harness 自己的方言实测(`JSON_SCHEMA` + `app.asar` 里的 `!!js` 类型),不加引号时 `args[0]` 变成 `{"[object Object]": "..."}` —— 一个行根本无法用来启动的对象。加引号消除歧义,内部的 JS 字符串改用单引号。 - **必须用正斜杠,不能用反斜杠。** YAML 单引号标量会保留 `\` 字面量,接着 JavaScript 会把它吃掉:`'\plugins'` 不是路径而是 BEL 控制字符,`'\rea-bundle'` 变成回车、`'\bin'` 变成退格 —— 路径被静默摧毁。正斜杠在 Windows 与 POSIX 上都成立。 - **`DSH_HOME` 的兜底是 `/.dsh`,不是 ``。** shim、`setup.mjs`、`bootstrap.ps1` 三者都把未设置的 `DSH_HOME` 解析到 `/.dsh/...`;兜底少这一层,行就会指向供给永远不会写的位置 —— 而在 `DSH_HOME` 恰好被设置的机器上,这个错误完全不可见。 +- **`DSH_HOME`、`USERPROFILE`、`HOME` 三者都为空时,表达式会得到 `/.dsh/...`** —— 这是纯拼接唯一无法兜住 + 的组合,出现在没有主目录的服务账户/容器里。此时那一行起不来,而 `failOnStartupError: false` 会让它保持安静, + 症状只有「没有 `mcp__rea__*` 工具」。这类账户请显式设置 `DSH_HOME`。 - **`!!js` 表达式里没有 `path`/`require`**:`ctx` 是 loader 作用域,所以 DSH_HOME 的兜底写成纯字符串拼接。 `npm run bundle:audit` 会把前三条钉住:它用「`DSH_HOME` 未设 / 为空 / 已设」三种情形求值这个表达式,并拒绝会被 YAML 读成 mapping 的裸标量。 @@ -246,12 +249,18 @@ shim 会在启动时提示冲突 —— `setup.mjs` 会把运行时的 DSH home (`configuration` / `installation` / `headless` / `native_decompiler` / `java`),应当全为 `ok: true`。 然后 `open_binary` 一个 PE、`binary_overview`、`procedure_pseudo_code`。 -供给本身结束时也会跑两道探针:`rea doctor --json`(如实报告,并点名未满足项),以及**与 MCP 行完全相同的 -shim 启动**。 +供给本身结束时跑三道检查:`rea doctor --json`(如实报告,并点名未满足项)、一次 **Ghidra 冒烟导入** +(用 JDK 自带的 `bin/java` 跑 `rea analyze`,导不进就判本次供给失败;`--skip-smoke` 可关),以及 +**与 MCP 行完全相同的 shim 启动**。之所以要冒烟:`doctor` 只校验路径、版本与会用哪个 Java,**从不导入 +二进制**,桥接或 native addon 坏了,过去只能在第一次 MCP 调用超时时才撞见。 + +**每个新目标的第一次分析都很慢。** 本机实测:小 PE 36 s,6 MB 的 DLL 64 s —— 每次导入都要起 JVM 并跑 +Ghidra 的默认自动分析。如果大文件的第一次 `open_binary` → `list_segments` 超时,那是这个成本、不是引擎坏了: +再调一次,已打开的目标会立刻返回(它保留着自己的临时项目)。 ## 实测环境 -作者机器,2026-10-13 / 14 / 16 / 19: +作者机器,2026-10-13 / 14 / 16 / 19 / 23: | 组件 | 版本 | |---|---| @@ -259,8 +268,8 @@ shim 启动**。 | DeepSeek Harness | desktop runtime **0.2.0-rc.2**,内含 dsh CLI 0.1.7-rc.2 | | `@deepseek-ai/dsh-mcp-client`(行 provider) | 随 harness 打包 | | Ghidra | 12.1.4 | -| JDK | Temurin 21(供给)/ Microsoft OpenJDK 25.0.4(复用) | -| Node.js | 24.21.0(REA engines:`^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`) | +| JDK | Temurin 21(供给)/ Amazon Corretto 25.0.5(宿主复用) | +| Node.js | **24.22.1**(复用;REA engines:`^22.19.0 \|\| ^24.11.0 \|\| >=26.0.0`,ABI 137) | | 宿主 | Windows 11 x64(build 26200) | 实测跑过:一次干净的 `setup.mjs --reuse` 走到 `setup complete`;一次**不经 PATH 上的 `rea`、完全经由 @@ -271,6 +280,11 @@ shim** 的 MCP `initialize` 握手返回 `serverInfo {name: rea, version: 6.7.0} 本机**没有**端到端跑过、因此不做承诺的:约 775 MB 的 JDK + Ghidra 下载分支(`--dry-run` 有计划,本机用 `--reuse` 复用了已有安装),以及 macOS/Linux 的首次运行。 +2026-10-23 宿主 Node 换到 **24.22.1** 后,同一条链路重验过:vendor 里的 `rea-windows-x64.node` 与两个 +`conpty` addon 在 ABI 137 下加载成功;`conpty.node` 的 `binary_overview` 报出 10 个段 / 1178 个函数 / +618 个字符串;`procedure_pseudo_code` 返回了真实反编译结果;vendor 里的 JDK `java -version` 回 `21.0.12.1`。 +唯一**第一次没成**的是 `list_segments`:它撞上 Ghidra 冷分析的 64 s 而超时 —— 这正是上面冒烟步与那条说明的由来。 + ## 边界与注意事项 - REA 在 6.7.0 有 207 个工具,每个会话的工具列表都会变长。 @@ -373,7 +387,10 @@ node scripts/setup.mjs # 或:powershell -File scripts\rea-bundle-boot - **上游 REA / Ghidra**:改清单里的钉子再重跑 setup。REA 对 Ghidra 有版本闸门(目前 12.1.x), 所以先更新 REA。 -`check-updates.ps1`(Windows PowerShell)会一次性报告以上各项的状态。 +`check-updates.ps1`(Windows PowerShell)会一次性报告以上各项的状态。`npm run check-updates` 在 Windows +上转发给它(优先 PowerShell 5.1,其次 `pwsh`);在 macOS/Linux 上它会说明这个脚本为何在那里给不出答案, +并列出两条可移植命令 —— `node bin/rea-shim.mjs --shim-status` 与 `node scripts/setup.mjs --dry-run` —— +且以非零码退出,不会报一个自己没查出来的「全绿」。 ## 还想用纯配置的 1.x 线? diff --git a/manifest/bundle.manifest.json b/manifest/bundle.manifest.json index c404acf..c228c22 100644 --- a/manifest/bundle.manifest.json +++ b/manifest/bundle.manifest.json @@ -24,7 +24,7 @@ "jdk": "21", "apktool": "3.0.3", "jadxHeadlessMcp": "0.7.1", - "node": "24.21.0", + "node": "24.22.1", "nodeEngines": "^22.19.0 || ^24.11.0 || >=26.0.0" }, "components": { @@ -42,60 +42,60 @@ }, "sources": { "win32-x64": { - "version": "24.21.0", - "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip", + "version": "24.22.1", + "url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip", "format": "zip", - "size": 37618919, - "sha256": "158f7685b44de51f6c0df1d153526cbcd3e1bc739a8dfc607721cef75de9e541", - "sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-win-x64.zip)", - "sha256VerifiedOn": "2026-10-13", - "note": "Checksum and unpack verified on this machine: the extracted node.exe runs standalone (v24.21.0), no installer and no registry writes." + "size": 37748064, + "sha256": "d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95fa5", + "sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-win-x64.zip)", + "sha256VerifiedOn": "2026-10-23", + "note": "Checksum and unpack verified on this machine: the extracted node.exe runs standalone (v24.22.1), no installer and no registry writes. 24.22.1 is Node's 2026-10-21 security release (Brotli decoder out-of-bounds write RCE, two TLS hostname bypasses, eMudhra intermediate removal); 24.21.0 must not be redistributed." }, "win32-arm64": { - "version": "24.21.0", - "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-arm64.zip", + "version": "24.22.1", + "url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-arm64.zip", "format": "zip", - "size": 33679608, - "sha256": "8779b1bde1d39f8d420e3b57aa657b39891af434d3de44a919044cec06785921", - "sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-win-arm64.zip), size confirmed by HEAD", - "sha256VerifiedOn": "2026-10-13" + "size": 33786263, + "sha256": "a0c807319fede24c4a0d8f84564c82a619bd313b16b10c007c383c2609e99171", + "sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-win-arm64.zip), size confirmed by HEAD", + "sha256VerifiedOn": "2026-10-23" }, "darwin-x64": { - "version": "24.21.0", - "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-x64.tar.gz", + "version": "24.22.1", + "url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-x64.tar.gz", "format": "tar.gz", - "size": 54203979, - "sha256": "1462cb3b3046b815cf8ea436d3da450ec1a9f11dac7e5a46b0ada5305d7e8097", - "sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-darwin-x64.tar.gz)", - "sha256VerifiedOn": "2026-10-13" + "size": 54382566, + "sha256": "d132557e7ee2b85af08b0c8c34338e65c6430f2238d01f727728ef42dc83b035", + "sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-darwin-x64.tar.gz)", + "sha256VerifiedOn": "2026-10-23" }, "darwin-arm64": { - "version": "24.21.0", - "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-darwin-arm64.tar.gz", + "version": "24.22.1", + "url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-darwin-arm64.tar.gz", "format": "tar.gz", - "size": 52909993, - "sha256": "bed7eea5325e1108f32ce5228ddd6a5f0f08a499ee42aa7442aea583702f6057", - "sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-darwin-arm64.tar.gz)", - "sha256VerifiedOn": "2026-10-13" + "size": 53062778, + "sha256": "89c682593d76cd44233fa53b1563264e7de9cbb189cad169135c5b4fadfbc0a7", + "sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-darwin-arm64.tar.gz)", + "sha256VerifiedOn": "2026-10-23" }, "linux-x64": { - "version": "24.21.0", - "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.xz", + "version": "24.22.1", + "url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-x64.tar.xz", "format": "tar.xz", - "size": 31890184, - "sha256": "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6", - "sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-linux-x64.tar.xz)", - "sha256VerifiedOn": "2026-10-13", + "size": 32026916, + "sha256": "6c172a9ab6718f7b801fb5a7bd486af2ee5e32758086a04b84fcce2aa77095e6", + "sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-linux-x64.tar.xz)", + "sha256VerifiedOn": "2026-10-23", "note": ".tar.xz is not unpackable by Windows Expand-Archive; bsdtar handles it. A .tar.gz twin exists for every platform if xz support is ever in doubt." }, "linux-arm64": { - "version": "24.21.0", - "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-arm64.tar.xz", + "version": "24.22.1", + "url": "https://nodejs.org/dist/v24.22.1/node-v24.22.1-linux-arm64.tar.xz", "format": "tar.xz", - "size": 30859684, - "sha256": "6ad1325edbdb5649c379b75a237147a666c95d4f9ae8d340fef2d1575d289ad2", - "sha256Source": "https://nodejs.org/dist/v24.21.0/SHASUMS256.txt (line: node-v24.21.0-linux-arm64.tar.xz)", - "sha256VerifiedOn": "2026-10-13" + "size": 30978868, + "sha256": "dbabf7a1cbf947e6059ffad2c27b80041db97dea1e553b62afdee7f13fca336e", + "sha256Source": "https://nodejs.org/dist/v24.22.1/SHASUMS256.txt (line: node-v24.22.1-linux-arm64.tar.xz)", + "sha256VerifiedOn": "2026-10-23" } } }, @@ -114,7 +114,7 @@ "size": 205073461, "sha256": "f9d6e191ab098c0d416e7d588a24420a8621cd2f4720dab2459b8b7b2d2d8b4e", "sha256Source": "Adoptium API /v3/assets/latest/21/hotspot (binary.package.checksum), cross-checked against the per-asset .zip.sha256.txt sidecar", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=x64&image_type=jdk&os=windows&vendor=eclipse", "note": "Unpacked and exercised on this machine: bin/javac.exe and jmods are present, java --list-modules reports jdk.compiler, and REA's Ghidra provider accepted it as a 64-bit full JDK 21+." }, @@ -125,7 +125,7 @@ "size": 194316575, "sha256": "44db0f08196daf19a47f90d13388b0c943b67663cb537f998fe29e836fa842ce", "sha256Source": "Adoptium API (binary.package.checksum)", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=x64&image_type=jdk&os=mac&vendor=eclipse" }, "darwin-arm64": { @@ -135,7 +135,7 @@ "size": 200073404, "sha256": "3623232f33a9c3baadf304480b2535f9a3cba8a58d42ecbb438ba267315d9998", "sha256Source": "Adoptium API (binary.package.checksum)", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=aarch64&image_type=jdk&os=mac&vendor=eclipse" }, "linux-x64": { @@ -145,7 +145,7 @@ "size": 207473347, "sha256": "ce79869e1307ed8ee1e2baa86a412b1eb5b75d10a01006d788a6f968bcfaee94", "sha256Source": "Adoptium API (binary.package.checksum)", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=x64&image_type=jdk&os=linux&vendor=eclipse" }, "linux-arm64": { @@ -155,7 +155,7 @@ "size": 205641175, "sha256": "23e37e026f12f3e706f18938ff611db3032d075b09d0879a25d06718c773e223", "sha256Source": "Adoptium API (binary.package.checksum)", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "metaApi": "https://api.adoptium.net/v3/assets/latest/21/hotspot?architecture=aarch64&image_type=jdk&os=linux&vendor=eclipse" } } @@ -176,7 +176,7 @@ "sha256": "ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db", "sha256Source": "GitHub release body of the pinned tag: api.github.com/repos/NationalSecurityAgency/ghidra/releases/tags/Ghidra_12.1.4_build -> .body, line '* SHA-256: `ddac49f9...`'. No checksum asset exists (six candidate names 404). Always read the pinned tag, never releases/latest, because the body changes.", "releaseApi": "https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/tags/Ghidra_12.1.4_build", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "note": "No platform split for the public release: one zip serves Windows/macOS/Linux. Unpacked and sniffed here (support/analyzeHeadless.bat and the win_x86_64 native decompiler are present, and REA reported provider version 12.1.4). Windows traps: a path containing '!' or '^' breaks Ghidra, and macOS native components are not shipped (they must be built with support/gradle/gradlew buildNatives). Ghidra requires JDK 21+; application.java.max is empty at this tag, so a newer JDK is permitted." } } @@ -196,7 +196,7 @@ "size": 15478013, "sha256": "dbf930b076c6b9be08d57c449cacefc3bdd6b71ebd59b3066fc0e1f5b14f9423", "sha256Source": "Vendor-published in the 3.0.3 release post (apktool.org/blog/apktool-3.0.3/), md5 50d98c920b005d2fcdc18d3d52e3f23b. Independently reproduced here: this is the hash of the 15,478,013-byte apktool_3.0.3.jar.", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "moveTo": "apktool", "saveAs": "apktool.jar" } @@ -301,7 +301,7 @@ "size": 42619858, "sha256": "6e5eacf500b64292bfb73c49797c1958f6ee44646e43e868039ae7feb573ff75", "sha256Source": "Vendor-published sidecar asset releases/download/v0.7.1/jadx-headless-mcp.sha256 (98 bytes, one line); the value was independently reproduced here by hashing the downloaded 42,619,858-byte jar. 0.7.1 is the newest release and REA requires exactly this bridge build: the project's own build.gradle.kts documents that the fat jar must be shaded with duplicatesStrategy INCLUDE, otherwise only KotlinMetadataPlugin registers and every APK resolves to a single R class. Versions 0.6.0 and earlier predate that fix.", - "sha256VerifiedOn": "2026-10-13", + "sha256VerifiedOn": "2026-10-23", "moveTo": "jadx", "saveAs": "jadx-headless-mcp-0.7.1-all.jar" } @@ -323,8 +323,8 @@ "format": "zip", "size": 8044989, "sha256": "45f4d63113e895ebde0c90f194099a4676b6ac653bd28d54314a9e022bbc1a99", - "sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-13 and the archive verified readable (15 entries incl. adb.exe). The archive's own platform-tools/source.properties reports Pkg.Revision=37.0.1, so the real pin is 'platform-tools 37.0.1'. Google's repository2-3.xml carries size+checksum for versioned archives if a vendor hash is ever required.", - "sha256VerifiedOn": "2026-10-13", + "sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-23 and the archive verified readable (15 entries incl. adb.exe). The archive's own platform-tools/source.properties reports Pkg.Revision=37.0.1, so the real pin is 'platform-tools 37.0.1'. Google's repository2-3.xml carries size+checksum for versioned archives if a vendor hash is ever required.", + "sha256VerifiedOn": "2026-10-23", "note": "Rolling 'latest' URL pinned by observation; setup re-verifies against this value and reports drift. Android SDK components are not OSI-licensed and Google restricts redistribution, so this is fetched from Google at install time and never re-hosted. Also contains fastboot, sqlite3, mke2fs and the USB API DLLs." }, "darwin-x64": { @@ -333,8 +333,8 @@ "format": "zip", "size": 16110554, "sha256": "ee39ad5967e95c2a07f04dbcbde96b1a0c916ba376096db5d2f498b7727a5d1d", - "sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-13 by downloading the archive and hashing it locally (16,110,554 bytes)", - "sha256VerifiedOn": "2026-10-13", + "sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-23 by downloading the archive and hashing it locally (16,110,554 bytes)", + "sha256VerifiedOn": "2026-10-23", "note": "Rolling 'latest' URL pinned by observation; setup re-verifies against this value and reports drift." }, "darwin-arm64": { @@ -343,8 +343,8 @@ "format": "zip", "size": 16110554, "sha256": "ee39ad5967e95c2a07f04dbcbde96b1a0c916ba376096db5d2f498b7727a5d1d", - "sha256Source": "Same darwin archive as x64 (measured 2026-10-13); Google publishes one darwin zip for both architectures.", - "sha256VerifiedOn": "2026-10-13", + "sha256Source": "Same darwin archive as x64 (measured 2026-10-23); Google publishes one darwin zip for both architectures.", + "sha256VerifiedOn": "2026-10-23", "note": "The darwin archive contains x86_64 binaries. On Apple silicon they run only through Rosetta 2; without it adb cannot start and this optional component is reported unavailable." }, "linux-x64": { @@ -353,8 +353,8 @@ "format": "zip", "size": 9054187, "sha256": "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1", - "sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-13 by downloading the archive and hashing it locally (9,054,187 bytes)", - "sha256VerifiedOn": "2026-10-13", + "sha256Source": "Google publishes no checksum for this rolling asset; measured on 2026-10-23 by downloading the archive and hashing it locally (9,054,187 bytes)", + "sha256VerifiedOn": "2026-10-23", "note": "The linux archive contains x86_64 binaries. Google publishes no arm64 platform-tools and this bundle does not emulate one, so on linux-arm64 the entry below is offered but its probe is expected to fail — reported as an optional component being unavailable, never as a broken install." }, "linux-arm64": { @@ -363,8 +363,8 @@ "format": "zip", "size": 9054187, "sha256": "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1", - "sha256Source": "Google publishes no arm64 platform-tools; this is the same x86_64 linux archive as x64 (measured 2026-10-13), recorded so the mismatch is explicit instead of implied.", - "sha256VerifiedOn": "2026-10-13", + "sha256Source": "Google publishes no arm64 platform-tools; this is the same x86_64 linux archive as x64 (measured 2026-10-23), recorded so the mismatch is explicit instead of implied.", + "sha256VerifiedOn": "2026-10-23", "note": "Kept so an arm64 host reports a precise failure instead of \"no source\". adb here is x86_64: it runs under qemu/binfmt where the host provides it, and otherwise the probe fails and the report names it." } } diff --git a/package.json b/package.json index 3294c68..2849c09 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ } }, "scripts": { - "check-updates": "powershell -NoProfile -File ./check-updates.ps1", + "check-updates": "node ./scripts/check-updates.mjs", "bundle:setup": "node ./scripts/setup.mjs", "bundle:status": "node ./bin/rea-shim.mjs --shim-status", "bundle:dry-run": "node ./scripts/setup.mjs --reuse --dry-run", diff --git a/scripts/check-updates.mjs b/scripts/check-updates.mjs new file mode 100644 index 0000000..b199582 --- /dev/null +++ b/scripts/check-updates.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node +/** + * `npm run check-updates` — a portable entry for a Windows-only script. + * + * `check-updates.ps1` is a Windows PowerShell tool on purpose: it reads + * `%APPDATA%\npm`, prefers a `rea.cmd` shim over `rea.ps1`, and walks the + * profile the way the desktop app lays it out. On macOS/Linux even a working + * `pwsh` would run those branches against paths that do not exist there, so + * "make the .ps1 cross-platform" is not the fix — the honest answer is to say + * so, and to name the two commands that DO work everywhere. + * + * This wrapper therefore: + * - on Windows: runs `powershell` (the 5.1 the script is written for), and + * falls back to `pwsh` when 5.1 is absent; + * - elsewhere: prints why, plus the portable equivalents, and exits non-zero + * so a CI step cannot mistake "not applicable" for "all green". + * + * Its pure parts are exported so a unit test can pin both branches without + * spawning anything. + */ + +import { spawnSync } from 'node:child_process' +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +/** Repository root: this file lives in /scripts/. */ +export const ROOT = fileURLToPath(new URL('..', import.meta.url)) + +/** + * PowerShell executables to try, in order, for a platform. + * + * Empty on POSIX: the script's own probes are Windows-specific, so running it + * there would produce a confident wrong answer instead of a clear "not here". + */ +export function powerShellCandidates(platform = process.platform) { + return platform === 'win32' ? ['powershell', 'pwsh'] : [] +} + +/** The message used when this platform (or this machine) cannot run the script. */ +export function unsupportedMessage(platform = process.platform) { + const where = platform === 'win32' + ? 'No PowerShell (powershell or pwsh) was found on this machine.' + : `check-updates.ps1 is a Windows PowerShell tool; this host is ${platform}.` + return [ + where, + '', + 'The same questions, answered with commands that work on every platform:', + ' node bin/rea-shim.mjs --shim-status # provisioned bundle, launch choice, versions', + ' node scripts/setup.mjs --dry-run # what a setup run would still fetch', + '', + 'On Windows, `npm run check-updates` also compares the installed versions', + 'against the upstream registries and the profile registration.', + ].join('\n') +} + +function main(argv) { + const script = join(ROOT, 'check-updates.ps1') + if (!existsSync(script)) { + process.stderr.write(`check-updates: ${script} is missing from this package.\n`) + return 1 + } + + const candidates = powerShellCandidates() + if (candidates.length === 0) { + process.stderr.write(`${unsupportedMessage()}\n`) + return 1 + } + + const env = process.env + const attempts = [] + for (const command of candidates) { + const result = spawnSync(command, ['-NoProfile', '-File', script, ...argv], { + stdio: 'inherit', + cwd: ROOT, + env, + windowsHide: true, + }) + if (result.error && result.error.code === 'ENOENT') { + attempts.push(command) + continue + } + if (result.error) { + process.stderr.write(`check-updates: ${command} failed to start: ${result.error.message}\n`) + return 1 + } + return typeof result.status === 'number' ? result.status : 1 + } + + process.stderr.write( + `check-updates: tried ${attempts.join(', ')} — none is on PATH.\n\n${unsupportedMessage()}\n`, + ) + return 1 +} + +if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) { + process.exitCode = main(process.argv.slice(2)) +} diff --git a/scripts/setup.mjs b/scripts/setup.mjs index ee475b6..7cebb90 100644 --- a/scripts/setup.mjs +++ b/scripts/setup.mjs @@ -94,6 +94,7 @@ function parseArgs(argv) { json: false, only: null, offline: false, + skipSmoke: false, // Two different clocks, because one number cannot serve both: a route must // be declared dead quickly when the socket never opens, but a slow-but-alive // transfer must not be killed for taking long overall (measured here: @@ -110,6 +111,7 @@ function parseArgs(argv) { else if (arg === "--dry-run") options.dryRun = true; else if (arg === "--json") options.json = true; else if (arg === "--offline") options.offline = true; + else if (arg === "--skip-smoke") options.skipSmoke = true; else if (arg === "--yes") continue; else if (arg === "--only") options.only = (argv[++i] ?? "").split(",").map((s) => s.trim()).filter(Boolean); else if (arg === "--help" || arg === "-h") options.help = true; @@ -889,7 +891,44 @@ function reusableNodeChecked(candidate) { if (typeof regex === "string" && regex !== "" && !new RegExp(regex, "u").test(stdout)) { return { ok: false, detail: `it reports "${firstLine}", which REA's engines reject` }; } - return { ok: true }; + // Report the version the candidate actually printed, not the interpreter that + // happens to be running setup. They differ whenever the adopted Node is not + // the one executing this script — `REA_BUNDLE_NODE`, or a PATH whose first hit + // is a different install — and recording the running interpreter silently + // wrote a bundle.json that disagreed with the runtime the row launches. + return { ok: true, version: firstLine.replace(/^v/, "").trim() || null }; +} + +/** + * The version a reused JDK or Ghidra reports about itself. + * + * Both reuse branches used to return only `{path, how}`, so `bundle.json` fell + * back to `merged.?.version ?? null` and recorded `""` for a JDK or Ghidra + * the machine had adopted — a record that names a runtime but cannot say which + * one. `check-updates.ps1` compares against exactly that field, so the drift it + * exists to report became invisible for precisely the components a reuse run + * touches. Both files are the vendor's own declaration, not a guess: + * `release` carries `JAVA_VERSION="…"`, `Ghidra/application.properties` carries + * `application.version=…`. + */ +function readJdkVersion(root) { + try { + const release = readFileSync(path.join(root, "release"), "utf8"); + const match = /^JAVA_VERSION="?([^"\r\n]+)"?/m.exec(release); + return match ? match[1].trim() : null; + } catch { + return null; + } +} + +function readGhidraVersion(root) { + try { + const properties = readFileSync(path.join(root, "Ghidra", "application.properties"), "utf8"); + const match = /^application\.version=(.+)$/m.exec(properties); + return match ? match[1].trim() : null; + } catch { + return null; + } } /** Adopt an existing installation instead of downloading a duplicate. */ @@ -912,7 +951,7 @@ function reuseExisting(name, root) { if (seen.has(key)) continue; seen.add(key); const checked = reusableNodeChecked(candidate.path); - if (checked.ok) return candidate; + if (checked.ok) return { ...candidate, version: checked.version ?? null }; step("node", "plan", `not reusing ${candidate.how} node ${candidate.path}: ${checked.detail}`); } return null; @@ -920,14 +959,16 @@ function reuseExisting(name, root) { if (name === "jdk") { for (const [variable, how] of [["JAVA_HOME", "JAVA_HOME"]]) { const home = process.env[variable]; - if (home && existsSync(path.join(home, "bin", `javac${EXE}`))) return { path: home, how }; + if (home && existsSync(path.join(home, "bin", `javac${EXE}`))) { + return { path: path.normalize(home), how, version: readJdkVersion(home) }; + } } return null; } if (name === "ghidra") { const install = process.env.GHIDRA_INSTALL_DIR; if (install && existsSync(path.join(install, "support", IS_WINDOWS ? "analyzeHeadless.bat" : "analyzeHeadless"))) { - return { path: install, how: "GHIDRA_INSTALL_DIR" }; + return { path: path.normalize(install), how: "GHIDRA_INSTALL_DIR", version: readGhidraVersion(install) }; } return null; } @@ -1068,7 +1109,11 @@ async function provisionComponent(name, component, context) { } if (probe.ok) { step(name, "skip", `reusing existing ${name} from ${reused.how}: ${reused.path}`); - return { path: reused.path, reused: true }; + // Carry the adopted installation's own version. Dropping it here made + // `bundle.json` record the manifest's pin instead of what was adopted — a + // record that names a path but describes a different build, which is the + // one thing `check-updates.ps1` exists to catch. + return { ...reused, reused: true }; } step(name, "plan", `not reusing ${reused.how} ${name} at ${reused.path}: ${probe.detail}`); } @@ -1550,8 +1595,18 @@ if (!options.dryRun) { // what matters is that the CLI runs, emits parseable JSON, and reports the // tool catalog. Unmet checks are reported, not treated as errors. const reaEntry = paths.reaEntry; + // The same two variables the mirrored shim exports. Without them REA falls + // back to probing the machine, so `doctor` here reported whichever Ghidra and + // JDK happened to be installed globally — on a machine with an older global + // Ghidra this run's own engine was never the thing being checked, and a broken + // vendor copy could still come back healthy. + const reaEnv = { + ...process.env, + ...(paths.ghidra ? { GHIDRA_INSTALL_DIR: paths.ghidra } : {}), + ...(paths.jdk ? { JAVA_HOME: paths.jdk } : {}), + }; if (existsSync(reaEntry)) { - const doctor = spawnSync(nodePath, [reaEntry, "doctor", "--json"], { encoding: "utf8", timeout: 300000 }); + const doctor = spawnSync(nodePath, [reaEntry, "doctor", "--json"], { encoding: "utf8", timeout: 300000, env: reaEnv }); const stdout = doctor.stdout ?? ""; let parsed = null; try { @@ -1575,6 +1630,59 @@ if (!options.dryRun) { step("probe", "FAIL", "vendored REA CLI is missing; the MCP row will report unprovisioned"); } + // Ghidra smoke: prove the engine runs, not merely that its files exist. + // + // `doctor` validates paths, versions and which Java would be chosen; it never + // imports a binary. The first real import pays a one-time cost (measured here: + // ~35 s for a small PE, ~64 s for a 6 MB DLL) for the JVM start plus default + // auto-analysis. On a broken bridge or a mismatched native addon the operator + // met that only as an MCP tool timeout on the first real analysis — which reads + // exactly like "the runtime upgrade broke REA". Importing one small file here + // ends provisioning with a measured verdict instead of an inference, and makes + // the failure visible while the operator is still in the setup output. + // + // The input is the JDK's own launcher: it exists on every platform this bundle + // provisions, it is small enough to keep the step short, and it is never the + // user's file. + if (options.skipSmoke) { + step("smoke", "skip", "disabled by --skip-smoke"); + } else if (!existsSync(reaEntry)) { + step("smoke", "skip", "no vendored REA CLI to drive"); + } else if (!paths.ghidra || !paths.jdk) { + step("smoke", "skip", "this run resolved no ghidra + jdk pair"); + } else { + const smokeInput = path.join(paths.jdk, "bin", `java${EXE}`); + if (!existsSync(smokeInput)) { + step("smoke", "skip", `no small input at ${smokeInput}`); + } else { + const startedAt = Date.now(); + const smoke = spawnSync(nodePath, [reaEntry, "analyze", smokeInput, "--json"], { + encoding: "utf8", + timeout: 600000, + env: reaEnv, + }); + const elapsed = Math.round((Date.now() - startedAt) / 1000); + let evidence = null; + try { + const text = String(smoke.stdout ?? ""); + const open = text.indexOf("{"); + const close = text.lastIndexOf("}"); + evidence = open >= 0 && close > open ? JSON.parse(text.slice(open, close + 1)) : null; + } catch { + evidence = null; + } + if (smoke.status === 0 && evidence && typeof evidence.evidence_id === "string") { + step("smoke", "ok", `imported ${path.basename(smokeInput)} in ${elapsed}s via ghidra`); + } else { + step( + "smoke", + "FAIL", + `ghidra import failed (exit ${smoke.status}, ${elapsed}s): ${`${smoke.stdout ?? ""}${smoke.stderr ?? ""}`.trim().slice(0, 300)}`, + ); + } + } + } + // The real end-to-end check: launch the exact shim the MCP row will use // (the mirrored copy, not the source tree) and confirm it relays the CLI. const mirroredShim = path.join(options.root, "bin", "rea-shim.mjs"); diff --git a/scripts/setup.test.mjs b/scripts/setup.test.mjs index 8b427bc..7492d08 100644 --- a/scripts/setup.test.mjs +++ b/scripts/setup.test.mjs @@ -21,6 +21,7 @@ import test from "node:test"; import { fileURLToPath, pathToFileURL } from "node:url"; import { HOST_RELEASES, OUTSIDE_HOSTS, checkHostCoverage, declaredHostRange, compareVersions, satisfiesDeclaredRange } from "./host-compat.mjs"; import { computeGuideState, reaPinMatches } from "../bin/rea-guide.mjs"; +import { powerShellCandidates, unsupportedMessage } from "./check-updates.mjs"; /* ------------------------------------------------- probe token substitution */ @@ -434,6 +435,81 @@ test("an unavailable optional component is reported, not fatal", () => { } }); +/* ------------------------------ the Ghidra smoke is measured, and it can be off */ + +// `doctor` validates paths, versions and which Java would be chosen; it never +// imports a binary. The first real import pays a one-time ~35 s for the JVM start +// plus default auto-analysis, and when the bridge or the native addon is broken +// the operator met that only as an MCP tool timeout on the first real analysis — +// which reads exactly like "the runtime upgrade broke REA". The smoke step turns +// provisioning into a measured verdict. These tests keep it bounded in both +// directions: it must not run when this run resolved no engine, --skip-smoke must +// win, and an "ok" that only checked that files exist is the bug it replaced. +function runSmokeSetup(root, extra = []) { + const stub = path.join(root, "vendor", "node_modules", "rea-agents", "scripts"); + mkdirSync(stub, { recursive: true }); + writeFileSync( + path.join(stub, "rea.mjs"), + 'if (process.argv.includes("--version")) { console.log("6.5.0"); } else { console.log(JSON.stringify({ checks: [], identity: {} })); }\n', + ); + return spawnSync(process.execPath, [SETUP_SOURCE, "--reuse", "--only", "node", "--root", root, ...extra], { + encoding: "utf8", + timeout: 180000, + env: { ...process.env, REA_BUNDLE_NODE: process.execPath }, + }); +} + +test("the smoke step is skipped when this run resolved no engine", () => { + const root = mkdtempSync(path.join(tmpdir(), "rea-smoke-skip-")); + try { + const run = runSmokeSetup(root); + assert.match(run.stdout, /\[skip\] smoke — this run resolved no ghidra \+ jdk pair/); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("--skip-smoke turns the smoke step off outright", () => { + const root = mkdtempSync(path.join(tmpdir(), "rea-smoke-off-")); + try { + const run = runSmokeSetup(root, ["--skip-smoke"]); + assert.match(run.stdout, /\[skip\] smoke — disabled by --skip-smoke/); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test("the smoke step drives a real import and fails the run when it fails", () => { + // Contract assertions rather than a live Ghidra run: CI has no 570 MB engine. + // What must not regress is (a) the step really invokes REA's analyze on a file + // this run owns, and (b) a non-zero verdict is a FAIL, not a silent skip. + const source = readFileSync(SETUP_SOURCE, "utf8"); + assert.match(source, /\[reaEntry, "analyze", smokeInput, "--json"\]/); + assert.match(source, /step\(\s*"smoke",\s*"FAIL",/); + assert.match(source, /path\.join\(paths\.jdk, "bin", `java\$\{EXE\}`\)/); + // The two variables the mirrored shim exports must reach the probe and the + // smoke alike; without them both fell back to probing the machine. + assert.match(source, /GHIDRA_INSTALL_DIR: paths\.ghidra/); + assert.match(source, /JAVA_HOME: paths\.jdk/); +}); + +test("a reused component carries its own version into the record", () => { + // Both reuse branches returned only `{path, how}`, so bundle.json recorded the + // interpreter running setup for Node and the manifest's pin for a JDK/Ghidra — + // a record that names a runtime but describes a different build, which is the + // one thing `check-updates.ps1` exists to catch. A live fixture cannot cover + // this: adoption runs the component's own probe, so a stub tree would have to + // be a working compiler or analysis engine. Contract assertions instead. + const source = readFileSync(SETUP_SOURCE, "utf8"); + // The adopted record is spread, not rebuilt from its path alone. + assert.match(source, /return \{ \.\.\.reused, reused: true \};/); + // ...and each installation is asked what it is, from its own declaration. + assert.match(source, /version: readJdkVersion\(home\)/); + assert.match(source, /version: readGhidraVersion\(install\)/); + assert.match(source, /function readJdkVersion\(root\)/); + assert.match(source, /function readGhidraVersion\(root\)/); +}); + /* =========================== this round's regressions ========================= * * Each test below pins a defect that was verified in the code before it was @@ -2049,3 +2125,28 @@ test("the guide asks for setup when the record cannot be parsed or names no REA" rmSync(root, { recursive: true, force: true }); } }); + +/* ------------------------------------------------- npm run check-updates */ + +test("check-updates runs PowerShell on Windows, 5.1 first", () => { + assert.deepEqual(powerShellCandidates("win32"), ["powershell", "pwsh"]); +}); + +test("check-updates refuses to fake a POSIX run", () => { + // The .ps1 probes %APPDATA% and prefers rea.cmd; a pwsh on Linux would run + // those branches against paths that do not exist and print a confident wrong + // answer. No candidates is the point, not an oversight. + assert.deepEqual(powerShellCandidates("linux"), []); + assert.deepEqual(powerShellCandidates("darwin"), []); +}); + +test("check-updates names the portable commands it falls back to", () => { + const posix = unsupportedMessage("linux"); + assert.match(posix, /Windows PowerShell tool/); + assert.match(posix, /this host is linux/); + assert.match(posix, /--shim-status/); + assert.match(posix, /setup\.mjs --dry-run/); + + const win = unsupportedMessage("win32"); + assert.match(win, /No PowerShell/); +}); diff --git a/scripts/verify-branch.ps1 b/scripts/verify-branch.ps1 index 285eeba..a8edab0 100644 --- a/scripts/verify-branch.ps1 +++ b/scripts/verify-branch.ps1 @@ -12,7 +12,7 @@ function Check($name, [bool]$ok, $detail) { } Write-Host "`n=== 1. JS syntax ===" -ForegroundColor Cyan -foreach ($f in @('index.mjs','bin\rea-shim.mjs','bin\rea-guide.mjs','client.js','scripts\setup.mjs','scripts\audit-manifest.mjs','scripts\setup.test.mjs','scripts\host-compat.mjs')) { +foreach ($f in @('index.mjs','bin\rea-shim.mjs','bin\rea-guide.mjs','client.js','scripts\setup.mjs','scripts\audit-manifest.mjs','scripts\setup.test.mjs','scripts\host-compat.mjs','scripts\check-updates.mjs')) { $out = & node --check $f 2>&1 Check $f ($LASTEXITCODE -eq 0) ($out -join ' ') } @@ -229,6 +229,21 @@ try { $guideParsed = ($guideRaw -join "`n") | ConvertFrom-Json } catch { $guideP Check 'bin/rea-guide.mjs answers with JSON' ($null -ne $guideParsed -and $null -ne $guideParsed.needsSetup) (($guideRaw | Select-Object -Last 2) -join ' ') Check 'the guide command points at this package scripts' ($null -ne $guideParsed -and [string]$guideParsed.setupCommand -match 'scripts[\\/]setup\.mjs') '' +Write-Host "`n=== 10. npm run check-updates is portable ===" -ForegroundColor Cyan +# check-updates.ps1 is Windows-only on purpose (it reads %APPDATA% and prefers a +# rea.cmd shim over the rea.ps1 one). The npm script used to BE the bare +# `powershell -File` command, so on macOS/Linux `npm run check-updates` was an +# exec error, and a pwsh there would have answered from paths that do not exist. +# The wrapper is the contract: Windows forwards, everything else explains and +# exits non-zero. +$wrapper = Join-Path $root 'scripts\check-updates.mjs' +Check 'scripts/check-updates.mjs exists' (Test-Path $wrapper) '' +$checkScript = [string]$pkg.scripts.'check-updates' +Check 'scripts.check-updates goes through the wrapper' ($checkScript -match 'check-updates\.mjs') $checkScript +$wrapperText = if (Test-Path $wrapper) { Get-Content $wrapper -Raw -Encoding UTF8 } else { '' } +Check 'the wrapper runs PowerShell on Windows only' ($wrapperText.Contains("platform === 'win32' ? ['powershell', 'pwsh'] : []")) '' +Check 'the wrapper names both portable fallbacks' ($wrapperText.Contains('--shim-status') -and $wrapperText.Contains('--dry-run')) '' + Write-Host '' if ($fail -eq 0) { Write-Host 'ALL CHECKS PASSED' -ForegroundColor Green } else { Write-Host "$fail CHECK(S) FAILED" -ForegroundColor Red }