dsh-plugin-rea 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/check-updates.ps1 CHANGED
@@ -69,13 +69,40 @@ function Test-NodeEngines([string]$node, [string]$engines) {
69
69
  }
70
70
 
71
71
  # ------------------------------------------------------- 定位 REA 与 profile
72
+ # 2.x 的 REA 在 <bundleRoot>\vendor\node_modules\rea-agents,不在 npm 全局。先读
73
+ # bundle.json:把全局路径当默认是 1.x 的答案,在 2.x 机器上会让这一节指向错误的
74
+ # 对象("全局包丢了"),并给出错误的升级动作(npm i -g)。
75
+ $bundleRootEarly = $env:REA_BUNDLE_ROOT
76
+ if (-not $bundleRootEarly) {
77
+ $homeEarly = $env:DSH_HOME
78
+ if (-not $homeEarly) { $homeEarly = Join-Path $HOME '.dsh' }
79
+ $bundleRootEarly = Join-Path $homeEarly 'plugins\rea-bundle'
80
+ }
81
+ $bundleState = $null
82
+ $bundleJsonEarly = Join-Path $bundleRootEarly 'bundle.json'
83
+ if (Test-Path $bundleJsonEarly) {
84
+ try { $bundleState = Get-Content $bundleJsonEarly -Raw -Encoding UTF8 | ConvertFrom-Json } catch { $bundleState = $null }
85
+ }
86
+ $bundleReaPackageDir = $null
87
+ $bundleReaEntry = $null
88
+ if ($bundleState -and $bundleState.paths.reaEntry) {
89
+ $bundleReaEntry = [string]$bundleState.paths.reaEntry
90
+ # <vendor>\node_modules\rea-agents\scripts\rea.mjs -> <vendor>\node_modules\rea-agents
91
+ $bundleReaPackageDir = Split-Path -Parent (Split-Path -Parent $bundleReaEntry)
92
+ }
93
+
72
94
  if (-not $ReaPackageDir) {
73
- $npmRoot = $null
74
- if (Get-Command npm -ErrorAction SilentlyContinue) {
75
- try { $npmRoot = (& npm root -g 2>$null | Select-Object -First 1).Trim() } catch { }
95
+ if ($bundleReaPackageDir -and (Test-Path (Join-Path $bundleReaPackageDir 'package.json'))) {
96
+ $ReaPackageDir = $bundleReaPackageDir
97
+ } else {
98
+ $npmRoot = $null
99
+ if (Get-Command npm -ErrorAction SilentlyContinue) {
100
+ try { $npmRoot = (& npm root -g 2>$null | Select-Object -First 1).Trim() } catch { }
101
+ }
102
+ $ReaPackageDir = if ($npmRoot) { Join-Path $npmRoot 'rea-agents' } else { Join-Path $env:APPDATA 'npm\node_modules\rea-agents' }
76
103
  }
77
- $ReaPackageDir = if ($npmRoot) { Join-Path $npmRoot 'rea-agents' } else { Join-Path $env:APPDATA 'npm\node_modules\rea-agents' }
78
104
  }
105
+ $reaFromBundle = [bool]($bundleReaPackageDir -and $ReaPackageDir -eq $bundleReaPackageDir)
79
106
  if (-not $ProfileDir) {
80
107
  if ($env:DSH_PROFILE_DIR) { $ProfileDir = $env:DSH_PROFILE_DIR }
81
108
  else {
@@ -94,11 +121,21 @@ if (Test-Path $pkgFile) {
94
121
  $installed = $pkg.version; $engines = $pkg.engines.node
95
122
  $binPath = ($pkg.bin.PSObject.Properties | Select-Object -First 1).Value
96
123
  if ($binPath) { $reaEntry = Join-Path $ReaPackageDir ($binPath -replace '/', '\') }
97
- Ok "本地已安装: $installed"
98
- Info "入口(bin): $binPath ← 插件通过 PATH 上的 rea 调用它,跨版本稳定"
124
+ if ($reaFromBundle) {
125
+ Ok "一体包内置(vendored): $installed"
126
+ Info "位置: $ReaPackageDir"
127
+ Info "入口(bin): $binPath"
128
+ } else {
129
+ Ok "本地已安装: $installed"
130
+ Info "入口(bin): $binPath ← 插件通过 PATH 上的 rea 调用它,跨版本稳定"
131
+ }
99
132
  if ($engines) { Info "Node 要求: $engines" }
100
133
  } catch { Bad "package.json 解析失败: $($_.Exception.Message)" }
101
- } else { Bad "找不到 $pkgFile —— REA 全局包丢了?重装: npm i -g rea-agents" }
134
+ } else {
135
+ if ($reaFromBundle) { Bad "找不到 $pkgFile —— 内置副本不完整?重跑 setup.mjs" }
136
+ elseif ($bundleReaPackageDir) { Bad "找不到 $pkgFile —— 一体包内置副本也缺($bundleReaPackageDir);记录里没有 reaEntry" }
137
+ else { Bad "找不到 $pkgFile —— REA 全局包丢了?重装: npm i -g rea-agents" }
138
+ }
102
139
 
103
140
  $latest = $null
104
141
  foreach ($r in @($Registry, 'https://registry.npmjs.org')) {
@@ -110,7 +147,11 @@ if ($installed -and $latest) {
110
147
  $vi = Get-NumericVersion $installed; $vl = Get-NumericVersion $latest
111
148
  if ($vi -and $vl -and $vl -gt $vi) {
112
149
  Warn "有新版本:$installed → $latest"
113
- $actions.Add("升级上游 REA: npm i -g rea-agents@$latest (只更新 REA;本插件不会自动更新——从市场装的在市场里更新,file: 装的覆盖目录后重装)")
150
+ if ($reaFromBundle) {
151
+ $actions.Add("升级一体包内置 REA:把 manifest 的 pins.reaAgents / components.reaAgents.sources.npm.version 改成 $latest,然后重跑 node scripts\setup.mjs(内置副本不会自己更新)")
152
+ } else {
153
+ $actions.Add("升级上游 REA: npm i -g rea-agents@$latest (只更新 REA;本插件不会自动更新——从市场装的在市场里更新,file: 装的覆盖目录后重装)")
154
+ }
114
155
  } elseif ($vi -and $vl -and $vl -eq $vi -and $installed -ne $latest) {
115
156
  Warn "同版本号但通道不同:$installed → $latest(预发布/正式)"
116
157
  } else { Ok '已是最新版本' }
@@ -139,7 +180,14 @@ if (Get-Command node -ErrorAction SilentlyContinue) {
139
180
 
140
181
  # ------------------------------------------------------------ Ghidra 引擎
141
182
  Head 'Ghidra 引擎'
142
- $gd = [Environment]::GetEnvironmentVariable('GHIDRA_INSTALL_DIR', 'User')
183
+ # bundle.json 是 2.x 的真源:shim 只在 MCP 子进程里设 GHIDRA_INSTALL_DIR,用户级
184
+ # 变量是 1.x 的写法,没手工设过的机器会被误报"未设置"。
185
+ $gd = $null
186
+ if ($bundleState -and $bundleState.paths.ghidra) {
187
+ $bundleGhidra = [string]$bundleState.paths.ghidra
188
+ if (Test-Path $bundleGhidra) { $gd = $bundleGhidra }
189
+ }
190
+ if (-not $gd) { $gd = [Environment]::GetEnvironmentVariable('GHIDRA_INSTALL_DIR', 'User') }
143
191
  if (-not $gd) { $gd = $env:GHIDRA_INSTALL_DIR }
144
192
  if ($gd -and (Test-Path $gd)) {
145
193
  Ok "GHIDRA_INSTALL_DIR = $gd"
@@ -162,11 +210,28 @@ try {
162
210
  # --------------------------------------------------------- REA 引擎自检
163
211
  Head 'REA 引擎自检(rea doctor)'
164
212
  $env:GHIDRA_INSTALL_DIR = $gd
165
- # 优先取 .cmd:发布出去的那一行是 `cmd.exe /c rea mcp`(走 cmd 的 PATHEXT,命中 rea.cmd),
166
- # 而 PowerShell 的 Get-Command rea 会先命中同目录的 rea.ps1 垫片,两者行为(尤其退出码)并不相同。
167
- $reaCmd = (Get-Command 'rea.cmd' -ErrorAction SilentlyContinue).Source
168
- if (-not $reaCmd) { $reaCmd = (Get-Command rea -ErrorAction SilentlyContinue).Source }
213
+ # 2.x:优先用 bundle 内置的 CLI 和它自己的 Node。PATH 上的 rea 可能是另一个,
214
+ # 甚至完全不在 PATH —— 那会让整段自检被静默跳过。这正是 1.x 口径的残留。
215
+ $reaCmd = $null
169
216
  $reaArgs = @()
217
+ if ($bundleReaEntry -and (Test-Path $bundleReaEntry)) {
218
+ $bundleNode = $null
219
+ if ($bundleState -and $bundleState.paths.node) {
220
+ $candidateNode = [string]$bundleState.paths.node
221
+ if (Test-Path $candidateNode) { $bundleNode = $candidateNode }
222
+ }
223
+ if (-not $bundleNode) { $bundleNode = (Get-Command node -ErrorAction SilentlyContinue).Source }
224
+ if ($bundleNode) {
225
+ $reaCmd = $bundleNode; $reaArgs = @($bundleReaEntry)
226
+ Info '使用一体包内置 REA(node <bundle 入口>),不依赖 PATH 上的 rea'
227
+ }
228
+ }
229
+ if (-not $reaCmd) {
230
+ # 优先取 .cmd:发布出去的那一行是 `cmd.exe /c rea mcp`(走 cmd 的 PATHEXT,命中 rea.cmd),
231
+ # 而 PowerShell 的 Get-Command rea 会先命中同目录的 rea.ps1 垫片,两者行为(尤其退出码)并不相同。
232
+ $reaCmd = (Get-Command 'rea.cmd' -ErrorAction SilentlyContinue).Source
233
+ if (-not $reaCmd) { $reaCmd = (Get-Command rea -ErrorAction SilentlyContinue).Source }
234
+ }
170
235
  if (-not $reaCmd) {
171
236
  if ($reaEntry -and (Test-Path $reaEntry) -and (Get-Command node -ErrorAction SilentlyContinue)) {
172
237
  $reaCmd = 'node'; $reaArgs = @($reaEntry)
@@ -247,6 +312,8 @@ if (-not (Test-Path $manifestPath)) {
247
312
  $state = Get-Content $bundleJson -Raw -Encoding UTF8 | ConvertFrom-Json
248
313
  Ok "已供给:$($state.provisionedAt)"
249
314
  $drift = 0
315
+ $compared = 0
316
+ $unrecorded = 0
250
317
  foreach ($name in $manifest.pins.PSObject.Properties.Name) {
251
318
  if ($name -eq 'nodeEngines') { continue }
252
319
  $pinned = [string]$manifest.pins.$name
@@ -254,13 +321,21 @@ if (-not (Test-Path $manifestPath)) {
254
321
  if ($state.versions -and ($state.versions.PSObject.Properties.Name -contains $name)) {
255
322
  $installed = [string]$state.versions.$name
256
323
  }
257
- if (-not $installed) { continue }
324
+ if (-not $installed) {
325
+ # 被 --reuse 采纳的组件没有 version 字段(bundle.json 里是 null)。静默
326
+ # continue 会让这一项从未被比对,却在下面打印"全部一致"——那是假绿。
327
+ $unrecorded++
328
+ Warn " $name 未记录版本(reuse 采纳的组件?)—— 无法与 pin $pinned 对账"
329
+ continue
330
+ }
331
+ $compared++
258
332
  # Node 的 pin 是引擎范围,不是版本号;其它组件比前缀即可
259
333
  $same = $installed.Equals($pinned) -or $installed.StartsWith(($pinned -split '\.')[0] + '.')
260
334
  if ($same) { Info " $name = $installed(pin $pinned)" }
261
335
  else { Warn "$name 盘上为 $installed,manifest 已钉 $pinned —— 重跑 setup 对齐"; $drift++ }
262
336
  }
263
- if ($drift -eq 0) { Ok '所有组件与 manifest 的 pin 一致' }
337
+ if ($drift -eq 0 -and $unrecorded -eq 0) { Ok '所有组件与 manifest 的 pin 一致' }
338
+ elseif ($drift -eq 0) { Warn "已比对的 $compared 项与 pin 一致,但 $unrecorded 项没有记录版本(见上)—— 不是全绿" }
264
339
  if ($state.unverifiedChecksums -and @($state.unverifiedChecksums).Count -gt 0) {
265
340
  Warn ("TOFU(无厂商校验和,取首次实测值): " + (@($state.unverifiedChecksums) -join ', '))
266
341
  }
@@ -31,11 +31,11 @@ Two upstream rules shape the runtime wiring:
31
31
 
32
32
  ## 2. Verified facts this design rests on
33
33
 
34
- Measured on Windows 11 x64, REA 6.4.0, Ghidra 12.1.4, Temurin JDK 25.0.4:
34
+ Measured on Windows 11 x64, REA 6.5.0, Ghidra 12.1.4, Temurin JDK 25.0.4:
35
35
 
36
36
  | Fact | Evidence |
37
37
  | --- | --- |
38
- | A row can launch REA with **no PATH `rea`** | `node <pkg>/scripts/rea.mjs mcp` answered the MCP `initialize` handshake (`serverInfo.name = rea`, version 6.4.0) and `rea doctor --json` reported 170 tools, `healthy: true` |
38
+ | A row can launch REA with **no PATH `rea`** | `node <pkg>/scripts/rea.mjs mcp` answered the MCP `initialize` handshake (`serverInfo.name = rea`, version 6.5.0) and `rea doctor --json` reported 207 tools. `healthy` is `false` on a bundle-only install: 6.5.0 also checks the host's own setup (Hopper, IDA registration, `rea setup` skill/registrations), which provisioning deliberately never touches — the row does not require `healthy`, only a CLI that runs and a catalog it can read |
39
39
  | `!!js` expressions are supported in this profile layer | the existing profile patch already used one; the loader evaluates them |
40
40
  | A row-level `env:` block reaches the MCP child | `binary_session` reported `registrationEnvironment` = `GHIDRA_INSTALL_DIR`, `JAVA_HOME` |
41
41
  | No fixed home/layout is promised | the disk layout differs per install; nothing may assume one |
@@ -54,8 +54,9 @@ Three responsibilities, kept strictly apart:
54
54
  checksums, unpacks, runs the readiness probe, and writes `bundle.json`.
55
55
  Idempotent and re-runnable; `--reuse` adopts pre-existing installs.
56
56
  3. **Patch (wiring, published).** `cordis.patch.yml` on `main`, carrying
57
- **no machine path**: the row points at `<plugin>/bin/rea-shim.mjs` and the
58
- shim is responsible for the rest.
57
+ **no machine path**: the row computes `<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs`
58
+ at load time from `DSH_HOME` alone, and that mirrored shim is responsible for the
59
+ rest. (The checkout's own copy is what a human runs; the row never points at it.)
59
60
 
60
61
  The published plugin therefore stays portable, while the machine-specific truth
61
62
  lives in one small JSON file under the DSH home.
@@ -161,7 +162,7 @@ Windows and is what this bundle targets to complete.
161
162
  | Component | Needed for | Provisioned as |
162
163
  | --- | --- | --- |
163
164
  | Node runtime | running REA at all | reuse a compatible existing node, else portable `node.exe` |
164
- | `rea-agents` | the MCP server itself | vendored copy under `vendor/rea-agents` |
165
+ | `rea-agents` | the MCP server itself | vendored copy under `vendor/node_modules/rea-agents` |
165
166
  | Temurin JDK 21/25 | Ghidra headless + JADX bridge | portable JDK zip (full JDK, not a JRE) |
166
167
  | Ghidra 12.1.x | native analysis | official release zip, extracted |
167
168
  | apktool | Android resource decode | `apktool.bat` + jar in one directory |
@@ -12,12 +12,14 @@
12
12
  " binaries - workspace-relative entries inside the unpacked directory that are",
13
13
  " directly spawnable; the first that exists becomes the component's",
14
14
  " recorded path so another component can depend on it.",
15
+ " executable - a placed file that must carry the POSIX exec bit (a wrapper REA",
16
+ " spawns without a shell); setup chmods exactly these to 0755.",
15
17
  "Nothing here is redistributed inside the npm package; setup downloads it."
16
18
  ],
17
19
  "manifestVersion": 1,
18
20
  "generatedFor": "dsh-plugin-rea one-plugin bundle",
19
21
  "pins": {
20
- "reaAgents": "6.4.0",
22
+ "reaAgents": "6.5.0",
21
23
  "ghidra": "12.1.4",
22
24
  "jdk": "21",
23
25
  "apktool": "3.0.3",
@@ -203,16 +205,16 @@
203
205
  "win32-x64": [
204
206
  {
205
207
  "id": "wrapper",
206
- "version": "master",
207
- "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/windows/apktool.bat",
208
+ "version": "3.0.3",
209
+ "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/v3.0.3/scripts/windows/apktool.bat",
208
210
  "format": "file",
209
- "size": 2348,
210
- "sha256": "9440ec9d98efdef14136f4cefd79ac200d72baa016b5991d128234a3a3ef3b43",
211
- "sha256Source": "measured on 2026-10-13 (raw.githubusercontent.com publishes no checksum for the wrapper); a single 2,348-byte file, so the archive-free case needs no TOFU",
212
- "sha256VerifiedOn": "2026-10-13",
211
+ "size": 2386,
212
+ "sha256": "d81a91bd694459a61a88e4a1a8184efc21130390ff7cb5c172f631551ad9842a",
213
+ "sha256Source": "measured on 2026-10-16 from the immutable v3.0.3 tag (raw.githubusercontent.com publishes no checksum for the wrapper). The tag, not master: pinning master meant any upstream commit to the wrapper broke every new install with a hash mismatch, and four retries could not fix a download whose content had legitimately changed.",
214
+ "sha256VerifiedOn": "2026-10-16",
213
215
  "notes": [
214
216
  "The wrapper pushd's to its own directory and resolves apktool.jar first, then globs apktool_*.jar, so the renamed jar next to it is found.",
215
- "Its final line is `for %%i in (%cmdcmdline%) do if /i \"%%~i\"==\"/c\" pause & exit /b`: measured here it returns exit 0 in 0.2 s, but it appends 'Press any key to continue . . .' to stdout. That trailing text is upstream's, not a provisioning defect; tools that parse apktool's stdout should tolerate it.",
217
+ "The previous entry measured master's copy (2,348 bytes) and recorded that it returns exit 0 in 0.2 s while appending 'Press any key to continue . . .' to stdout. This is the tag's copy (2,386 bytes) and that runtime behaviour has NOT been re-measured; treat the trailing-text claim as unverified for this file.",
216
218
  "REA is pointed at this .bat (REA_APKTOOL_COMMAND), not at the jar, exactly as the wrapper expects."
217
219
  ],
218
220
  "moveTo": "apktool",
@@ -222,13 +224,14 @@
222
224
  "darwin-x64": [
223
225
  {
224
226
  "id": "wrapper",
225
- "version": "master",
226
- "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/osx/apktool",
227
+ "version": "3.0.3",
228
+ "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/v3.0.3/scripts/osx/apktool",
227
229
  "format": "file",
228
- "size": 2672,
229
- "sha256": "d0c86f0475ed73bfffa6ac39faa69de7aa2dd40ff5e7e6540f82002be3a38f0b",
230
- "sha256Source": "measured on 2026-10-13 from the osx wrapper (the correct one for macOS; the linux script is a different file)",
231
- "sha256VerifiedOn": "2026-10-13",
230
+ "size": 2709,
231
+ "sha256": "3b9a95c41680c4a758b0b8d6e91e122b2ea3f1a189685a915613ca1ccba00fd9",
232
+ "sha256Source": "measured on 2026-10-16 from the v3.0.3 tag (the correct file for macOS; the linux script is a different file). The tag is immutable, so this pin cannot drift under a new install the way master's did.",
233
+ "sha256VerifiedOn": "2026-10-16",
234
+ "executable": true,
232
235
  "moveTo": "apktool",
233
236
  "saveAs": "apktool"
234
237
  }
@@ -236,13 +239,14 @@
236
239
  "darwin-arm64": [
237
240
  {
238
241
  "id": "wrapper",
239
- "version": "master",
240
- "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/osx/apktool",
242
+ "version": "3.0.3",
243
+ "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/v3.0.3/scripts/osx/apktool",
241
244
  "format": "file",
242
- "size": 2672,
243
- "sha256": "d0c86f0475ed73bfffa6ac39faa69de7aa2dd40ff5e7e6540f82002be3a38f0b",
244
- "sha256Source": "measured on 2026-10-13 from the osx wrapper",
245
- "sha256VerifiedOn": "2026-10-13",
245
+ "size": 2709,
246
+ "sha256": "3b9a95c41680c4a758b0b8d6e91e122b2ea3f1a189685a915613ca1ccba00fd9",
247
+ "sha256Source": "measured on 2026-10-16 from the v3.0.3 tag; the same osx wrapper serves arm64.",
248
+ "sha256VerifiedOn": "2026-10-16",
249
+ "executable": true,
246
250
  "moveTo": "apktool",
247
251
  "saveAs": "apktool"
248
252
  }
@@ -250,13 +254,14 @@
250
254
  "linux-x64": [
251
255
  {
252
256
  "id": "wrapper",
253
- "version": "master",
254
- "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool",
257
+ "version": "3.0.3",
258
+ "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/v3.0.3/scripts/linux/apktool",
255
259
  "format": "file",
256
- "size": 2648,
257
- "sha256": "a851371023c43ca9b9a30218f87f9fd536304283de7b85447183ae811dfd033c",
258
- "sha256Source": "measured on 2026-10-13 from the linux wrapper",
259
- "sha256VerifiedOn": "2026-10-13",
260
+ "size": 2685,
261
+ "sha256": "493a9c98df968a25c373ca9df871ca77730c82954be4959b74e90adaf91a0e77",
262
+ "sha256Source": "measured on 2026-10-16 from the v3.0.3 tag.",
263
+ "sha256VerifiedOn": "2026-10-16",
264
+ "executable": true,
260
265
  "moveTo": "apktool",
261
266
  "saveAs": "apktool"
262
267
  }
@@ -264,13 +269,14 @@
264
269
  "linux-arm64": [
265
270
  {
266
271
  "id": "wrapper",
267
- "version": "master",
268
- "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/master/scripts/linux/apktool",
272
+ "version": "3.0.3",
273
+ "url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/v3.0.3/scripts/linux/apktool",
269
274
  "format": "file",
270
- "size": 2648,
271
- "sha256": "a851371023c43ca9b9a30218f87f9fd536304283de7b85447183ae811dfd033c",
272
- "sha256Source": "measured on 2026-10-13 from the linux wrapper",
273
- "sha256VerifiedOn": "2026-10-13",
275
+ "size": 2685,
276
+ "sha256": "493a9c98df968a25c373ca9df871ca77730c82954be4959b74e90adaf91a0e77",
277
+ "sha256Source": "measured on 2026-10-16 from the v3.0.3 tag; the same linux wrapper serves arm64. It is only correct where the platform-tools archive is too — see the arch caveat on androidPlatformTools.",
278
+ "sha256VerifiedOn": "2026-10-16",
279
+ "executable": true,
274
280
  "moveTo": "apktool",
275
281
  "saveAs": "apktool"
276
282
  }
@@ -337,9 +343,9 @@
337
343
  "format": "zip",
338
344
  "size": 16110554,
339
345
  "sha256": "ee39ad5967e95c2a07f04dbcbde96b1a0c916ba376096db5d2f498b7727a5d1d",
340
- "sha256Source": "Same darwin archive as x64 (measured 2026-10-13); it serves Intel and Apple silicon.",
346
+ "sha256Source": "Same darwin archive as x64 (measured 2026-10-13); Google publishes one darwin zip for both architectures.",
341
347
  "sha256VerifiedOn": "2026-10-13",
342
- "note": "The same darwin archive serves Intel and Apple silicon."
348
+ "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."
343
349
  },
344
350
  "linux-x64": {
345
351
  "version": "latest",
@@ -349,7 +355,7 @@
349
355
  "sha256": "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1",
350
356
  "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)",
351
357
  "sha256VerifiedOn": "2026-10-13",
352
- "note": "The same linux archive serves x86-64 and arm64."
358
+ "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."
353
359
  },
354
360
  "linux-arm64": {
355
361
  "version": "latest",
@@ -357,8 +363,9 @@
357
363
  "format": "zip",
358
364
  "size": 9054187,
359
365
  "sha256": "d230f13842f60f782a8645f9c813f8f845bf36089ea7289f28c48f17979313f1",
360
- "sha256Source": "Same linux archive as x64 (measured 2026-10-13).",
361
- "sha256VerifiedOn": "2026-10-13"
366
+ "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.",
367
+ "sha256VerifiedOn": "2026-10-13",
368
+ "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."
362
369
  }
363
370
  }
364
371
  },
@@ -370,14 +377,14 @@
370
377
  "unpack": { "moveTo": "rea-agents" },
371
378
  "probe": {
372
379
  "argv": ["{node}", "{vendor}/node_modules/rea-agents/scripts/rea.mjs", "--version"],
373
- "expect": { "regex": "^6\\.4\\.0" }
380
+ "expect": { "regex": "^6\\.5\\.0(\\s|$)" }
374
381
  },
375
382
  "sources": {
376
383
  "npm": {
377
- "version": "6.4.0",
384
+ "version": "6.5.0",
378
385
  "registry": "https://registry.npmjs.org/rea-agents",
379
- "integrity": "npm's own dist.integrity is the authoritative check for this tarball: the registry tarball ships NO node_modules (verified: 1161 entries, 0 under node_modules), so setup resolves the dependency closure with `npm install --ignore-scripts` instead of unpacking a tarball.",
380
- "note": "`just unpacking the published tarball cannot work` is a verified fact, not a caution: the CLI would fail with ERR_MODULE_NOT_FOUND on zod."
386
+ "integrity": "npm's own dist.integrity is the authoritative check for this tarball: the registry tarball ships NO node_modules (verified: 1650 entries in 6.5.0, 0 under node_modules), so setup resolves the dependency closure with `npm install --ignore-scripts` instead of unpacking a tarball.",
387
+ "note": "`just unpacking the published tarball cannot work` is a verified fact, not a caution: the CLI would fail with ERR_MODULE_NOT_FOUND on zod. 6.5.0 is the same engines/bin contract as 6.4.0 (^22.19.0 || ^24.11.0 || >=26.0.0, scripts/rea.mjs) and adds 37 MCP tools (170 -> 207 by rea doctor --json on 2026-10-16; the upstream contract files count 206) plus an optional frida dependency; --ignore-scripts means its install script never runs."
381
388
  }
382
389
  }
383
390
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-rea",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "One-plugin bundle: provisions Node/REA/JDK/Ghidra for you, then registers the upstream REA MCP server as mcp__rea__* tools. · 一体包:自动准备 Node/REA/JDK/Ghidra,并把上游 REA 的 MCP 服务接入 DeepSeek Harness。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -43,8 +43,7 @@
43
43
  "CHANGELOG.md",
44
44
  "check-updates.ps1",
45
45
  "screenshots.json",
46
- "assets/",
47
- "examples/"
46
+ "assets/"
48
47
  ],
49
48
  "dsh": {
50
49
  "bundle": {
@@ -430,6 +430,104 @@ if (manifest) {
430
430
  }
431
431
  }
432
432
 
433
+ /* ------------------------- 11. setup and the manifest describe the same set */
434
+
435
+ // A component added to the manifest but not to setup.mjs's INSTALL_ORDER was
436
+ // silently never provisioned, and nothing here said so. The reverse — a name in
437
+ // INSTALL_ORDER with no manifest entry — is skipped by the loop's
438
+ // `if (!component) continue`, so it is equally silent.
439
+ const installOrderMatch = setupText.match(/const INSTALL_ORDER = \[([^\]]*)\]/);
440
+ if (!installOrderMatch) {
441
+ add("error", "scripts/setup.mjs", "INSTALL_ORDER was not found, so manifest/setup agreement cannot be checked");
442
+ } else {
443
+ const order = [...installOrderMatch[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
444
+ const manifestNames = Object.keys(manifest.components ?? {});
445
+ for (const name of manifestNames) {
446
+ if (!order.includes(name)) {
447
+ add("error", "manifest.components", `"${name}" is not in scripts/setup.mjs INSTALL_ORDER, so setup would silently never provision it`);
448
+ }
449
+ }
450
+ for (const name of order) {
451
+ if (!manifestNames.includes(name)) {
452
+ add("error", "scripts/setup.mjs", `INSTALL_ORDER names "${name}", which the manifest does not describe`);
453
+ }
454
+ }
455
+ }
456
+
457
+ /* ---------------------------- 12. the Node engines rule has one wording too */
458
+
459
+ // REA's engines are restated in the manifest pin, package.json and the node
460
+ // probe regex. Only the pin and package.json can be compared as text, but a
461
+ // drift between those two would advertise one range and enforce another.
462
+ const declaredEngines = manifest.pins?.nodeEngines;
463
+ let packageEngines = null;
464
+ try {
465
+ packageEngines = JSON.parse(readFileSync(path.join(HERE, "..", "package.json"), "utf8")).engines?.node ?? null;
466
+ } catch (error) {
467
+ add("error", "package.json", `could not be read: ${String(error && error.message)}`);
468
+ }
469
+ if (!declaredEngines) {
470
+ add("error", "manifest.pins.nodeEngines", "is missing, so nothing states REA's engines for the reuse path");
471
+ } else if (declaredEngines !== packageEngines) {
472
+ add(
473
+ "error",
474
+ "manifest.pins.nodeEngines",
475
+ `is ${JSON.stringify(declaredEngines)} but package.json engines.node is ${JSON.stringify(packageEngines)}`,
476
+ );
477
+ }
478
+
479
+ /* ------------------- 13. a pinned version's probe must not accept a prefix */
480
+
481
+ // The concern is a probe that accepts a *newer* release as the pin, so test the
482
+ // expression against the versions that would mean that. The audit is not a
483
+ // regex linter: `^6\.4\.0` legitimately rejects 6.4.1 and 6.5.0 — the failure
484
+ // mode is only real when the next patch or minor still matches.
485
+ const reaAgents = manifest.components?.reaAgents;
486
+ const pinnedRea = reaAgents?.sources?.npm?.version;
487
+ const reaProbe = reaAgents?.probe?.expect?.regex;
488
+ if (pinnedRea && reaProbe) {
489
+ let probe = null;
490
+ try {
491
+ probe = new RegExp(reaProbe, "m");
492
+ } catch (error) {
493
+ add("error", "reaAgents.probe.expect.regex", `is not a valid regular expression: ${String(error && error.message)}`);
494
+ }
495
+ if (probe) {
496
+ if (!probe.test(pinnedRea)) {
497
+ add("error", "reaAgents.probe.expect.regex", `does not match the pinned version ${pinnedRea}`);
498
+ }
499
+ const [major, minor, patch] = pinnedRea.split(".").map((part) => Number(part));
500
+ for (const [label, next] of [
501
+ ["the next patch", `${major}.${minor}.${patch + 1}`],
502
+ ["the next minor", `${major}.${minor + 1}.0`],
503
+ ]) {
504
+ if (Number.isFinite(next.split(".")[2]) && probe.test(next)) {
505
+ add("error", "reaAgents.probe.expect.regex", `accepts ${next} (${label}), so a newer release passes as the pin`);
506
+ }
507
+ }
508
+ }
509
+ }
510
+
511
+ /* --------------------- 14. a POSIX wrapper must be marked executable */
512
+
513
+ // copyFileSync carries the archive's 0644 and nothing else changes a mode, while
514
+ // REA spawns REA_APKTOOL_COMMAND without a shell: on Linux/macOS an unmarked
515
+ // wrapper fails with EACCES even though the component probe (`java -jar`) passes.
516
+ // Only the manifest can say which files need the bit, so require it here.
517
+ const apktoolWrappers = manifest.components?.apktool?.entries?.byPlatform ?? {};
518
+ for (const [platform, list] of Object.entries(apktoolWrappers)) {
519
+ if (platform.startsWith("win32")) continue;
520
+ for (const entry of list) {
521
+ if (entry.executable !== true) {
522
+ add(
523
+ "error",
524
+ `apktool.entries.byPlatform.${platform}`,
525
+ `the ${platform} wrapper is not marked "executable": true, so it lands 0644 and cannot be spawned without a shell`,
526
+ );
527
+ }
528
+ }
529
+ }
530
+
433
531
  /* ------------------------------------------------------------- summary */
434
532
 
435
533
  const pinned = fetchEntries.filter((entry) => entry.entry.sha256 && entry.entry.sha256 !== "unverified").length;
@@ -1,4 +1,4 @@
1
- # rea-bundle-bootstrap.ps1 — get from "no Node at all" to a provisioned bundle.
1
+ # rea-bundle-bootstrap.ps1 — get from "no Node at all" to a provisioned bundle.
2
2
  #
3
3
  # Why this exists: setup.mjs is written in Node, so a machine with no Node
4
4
  # cannot run it. This script uses only PowerShell + Expand-Archive (no
@@ -39,8 +39,12 @@ $Root = [System.IO.Path]::GetFullPath($Root)
39
39
  $stack = Join-Path $Root 'stack'
40
40
  $cache = Join-Path $Root 'cache'
41
41
  $logs = Join-Path $Root 'logs'
42
- foreach ($dir in @($Root, $stack, $cache, $logs)) {
43
- if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
42
+ # -DryRun promises that nothing was changed, so it must not create these either:
43
+ # it only reads them, and Test-Path / Get-ChildItem handle a missing path.
44
+ if (-not $DryRun) {
45
+ foreach ($dir in @($Root, $stack, $cache, $logs)) {
46
+ if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
47
+ }
44
48
  }
45
49
 
46
50
  $pluginRoot = Split-Path -Parent $PSScriptRoot
@@ -84,6 +88,32 @@ $nodeFolder = [System.IO.Path]::GetFileNameWithoutExtension($nodeZipName)
84
88
  if ($nodeSha256 -eq 'unverified') { throw "the manifest pins no checksum for $hostKey; refusing to download unverified bytes" }
85
89
  Write-Step "Node source: $hostKey v$nodeVersion"
86
90
 
91
+ $script:NodeEngines = [string]$manifest.pins.nodeEngines
92
+ if (-not $script:NodeEngines) { throw "the manifest pins no nodeEngines, so Node compatibility cannot be judged" }
93
+
94
+ # REA's engines come from the manifest pin, which the rest of provisioning already
95
+ # treats as the single source of truth. This used to be a second copy of the same
96
+ # numbers in a comment, and two copies drift: the audit now asserts that the pin
97
+ # keeps the shape this parser understands.
98
+ function Test-EnginesMatch([string]$Clean, [string]$Range) {
99
+ $versions = @()
100
+ foreach ($part in $Clean.Split('.')) {
101
+ $n = 0
102
+ if (-not [int]::TryParse($part, [ref]$n)) { return $false }
103
+ $versions += $n
104
+ }
105
+ if ($versions.Count -lt 2) { return $false }
106
+ foreach ($clause in ($Range -split '\|\|')) {
107
+ $c = $clause.Trim()
108
+ if ($c -match '^\^(\d+)\.(\d+)\.(\d+)$') {
109
+ if ($versions[0] -eq [int]$Matches[1] -and $versions[1] -ge [int]$Matches[2]) { return $true }
110
+ } elseif ($c -match '^>=(\d+)\.(\d+)\.(\d+)$') {
111
+ if ($versions[0] -gt [int]$Matches[1] -or ($versions[0] -eq [int]$Matches[1] -and $versions[1] -ge [int]$Matches[2])) { return $true }
112
+ }
113
+ }
114
+ return $false
115
+ }
116
+
87
117
  function Test-NodeUsable([string]$Exe) {
88
118
  if (-not $Exe -or -not (Test-Path $Exe)) { return $null }
89
119
  try {
@@ -91,13 +121,10 @@ function Test-NodeUsable([string]$Exe) {
91
121
  } catch { return $null }
92
122
  if (-not $version) { return $null }
93
123
  $clean = $version.TrimStart('v').Trim()
94
- $parts = $clean.Split('.')
95
- if ($parts.Count -lt 2) { return $null }
96
- $major = [int]$parts[0]
97
- $minor = [int]$parts[1]
98
- # REA's engines: ^22.19.0 || ^24.11.0 || >=26.0.0
99
- $ok = ($major -eq 22 -and $minor -ge 19) -or ($major -eq 24 -and $minor -ge 11) -or ($major -ge 26)
100
- if (-not $ok) { return $null }
124
+ # A version that is not three numbers (`vX.Y`) used to reach `[int]` and throw
125
+ # under $ErrorActionPreference='Stop', aborting the whole bootstrap instead of
126
+ # falling through to the portable download — the one path this script exists for.
127
+ if (-not (Test-EnginesMatch $clean $script:NodeEngines)) { return $null }
101
128
  return [pscustomobject]@{ Path = (Resolve-Path $Exe).Path; Version = $clean }
102
129
  }
103
130