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/CHANGELOG.md +121 -0
- package/README.md +50 -7
- package/README.zh-CN.md +45 -6
- package/assets/make-screenshots.ps1 +2 -2
- package/assets/screenshot-1.png +0 -0
- package/bin/rea-shim.mjs +169 -17
- package/check-updates.ps1 +90 -15
- package/docs/bundle-design.md +6 -5
- package/manifest/bundle.manifest.json +48 -41
- package/package.json +2 -3
- package/scripts/audit-manifest.mjs +98 -0
- package/scripts/rea-bundle-bootstrap.ps1 +37 -10
- package/scripts/setup.mjs +352 -87
- package/scripts/setup.test.mjs +756 -4
- package/examples/cordis.patch.posix.yml +0 -18
- package/examples/cordis.patch.with-env.yml +0 -42
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
|
-
$
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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 {
|
|
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
|
-
$
|
|
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
|
-
|
|
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
|
-
#
|
|
166
|
-
#
|
|
167
|
-
$reaCmd =
|
|
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) {
|
|
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
|
}
|
package/docs/bundle-design.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
58
|
-
shim is responsible for the
|
|
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.
|
|
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": "
|
|
207
|
-
"url": "https://raw.githubusercontent.com/iBotPeaches/Apktool/
|
|
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":
|
|
210
|
-
"sha256": "
|
|
211
|
-
"sha256Source": "measured on 2026-10-
|
|
212
|
-
"sha256VerifiedOn": "2026-10-
|
|
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
|
-
"
|
|
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": "
|
|
226
|
-
"url": "https://raw.githubusercontent.com/iBotPeaches/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":
|
|
229
|
-
"sha256": "
|
|
230
|
-
"sha256Source": "measured on 2026-10-
|
|
231
|
-
"sha256VerifiedOn": "2026-10-
|
|
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": "
|
|
240
|
-
"url": "https://raw.githubusercontent.com/iBotPeaches/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":
|
|
243
|
-
"sha256": "
|
|
244
|
-
"sha256Source": "measured on 2026-10-
|
|
245
|
-
"sha256VerifiedOn": "2026-10-
|
|
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": "
|
|
254
|
-
"url": "https://raw.githubusercontent.com/iBotPeaches/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":
|
|
257
|
-
"sha256": "
|
|
258
|
-
"sha256Source": "measured on 2026-10-
|
|
259
|
-
"sha256VerifiedOn": "2026-10-
|
|
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": "
|
|
268
|
-
"url": "https://raw.githubusercontent.com/iBotPeaches/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":
|
|
271
|
-
"sha256": "
|
|
272
|
-
"sha256Source": "measured on 2026-10-
|
|
273
|
-
"sha256VerifiedOn": "2026-10-
|
|
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);
|
|
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
|
|
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
|
|
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": "
|
|
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\\.
|
|
380
|
+
"expect": { "regex": "^6\\.5\\.0(\\s|$)" }
|
|
374
381
|
},
|
|
375
382
|
"sources": {
|
|
376
383
|
"npm": {
|
|
377
|
-
"version": "6.
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
$
|
|
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
|
|