dsh-plugin-rea 2.0.6 → 2.0.8

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
@@ -73,9 +73,13 @@ function Test-NodeEngines([string]$node, [string]$engines) {
73
73
  # bundle.json:把全局路径当默认是 1.x 的答案,在 2.x 机器上会让这一节指向错误的
74
74
  # 对象("全局包丢了"),并给出错误的升级动作(npm i -g)。
75
75
  $bundleRootEarly = $env:REA_BUNDLE_ROOT
76
+ # Resolve the home unconditionally: the profile section below needs it whether or
77
+ # not REA_BUNDLE_ROOT was set, and computing it only inside the fallback branch
78
+ # left it $null — `Join-Path $null 'profiles'` throws, so the section degraded to
79
+ # "no profile" on exactly the machines that use the override.
80
+ $homeEarly = $env:DSH_HOME
81
+ if (-not $homeEarly) { $homeEarly = Join-Path $HOME '.dsh' }
76
82
  if (-not $bundleRootEarly) {
77
- $homeEarly = $env:DSH_HOME
78
- if (-not $homeEarly) { $homeEarly = Join-Path $HOME '.dsh' }
79
83
  $bundleRootEarly = Join-Path $homeEarly 'plugins\rea-bundle'
80
84
  }
81
85
  $bundleState = $null
@@ -106,8 +110,12 @@ $reaFromBundle = [bool]($bundleReaPackageDir -and $ReaPackageDir -eq $bundleReaP
106
110
  if (-not $ProfileDir) {
107
111
  if ($env:DSH_PROFILE_DIR) { $ProfileDir = $env:DSH_PROFILE_DIR }
108
112
  else {
109
- $profiles = @(Get-ChildItem (Join-Path $env:USERPROFILE '.dsh\profiles') -Directory -ErrorAction SilentlyContinue)
110
- $ProfileDir = if ($profiles.Count -gt 0) { $profiles[0].FullName } else { Join-Path $env:USERPROFILE '.dsh\profiles\desktop' }
113
+ # Same home rule the bundle uses everywhere else: DSH_HOME, then ~/.dsh.
114
+ # Hardcoding $env:USERPROFILE here was a second algorithm in the same script,
115
+ # so on a custom DSH_HOME the profile section reported "no profile found"
116
+ # while the rest of the run had already resolved that home correctly.
117
+ $profiles = @(Get-ChildItem (Join-Path $homeEarly 'profiles') -Directory -ErrorAction SilentlyContinue)
118
+ $ProfileDir = if ($profiles.Count -gt 0) { $profiles[0].FullName } else { Join-Path $homeEarly 'profiles\desktop' }
111
119
  }
112
120
  }
113
121
 
@@ -340,7 +348,27 @@ if (-not (Test-Path $manifestPath)) {
340
348
  Warn ("TOFU(无厂商校验和,取首次实测值): " + (@($state.unverifiedChecksums) -join ', '))
341
349
  }
342
350
  $mirrored = Join-Path $bundleRoot 'bin\rea-shim.mjs'
343
- if (Test-Path $mirrored) { Ok 'MCP 行指向的 shim 已就位' }
351
+ if (Test-Path $mirrored) {
352
+ Ok 'MCP 行指向的 shim 已就位'
353
+ # The row launches this COPY, so a plugin upgrade that does not re-run
354
+ # setup leaves the old launcher in place — visible only as "the new
355
+ # version behaves like the old one". Compare it with the checkout the
356
+ # mirror record names.
357
+ $mirrorRecord = Join-Path $bundleRoot 'mirror.json'
358
+ if (Test-Path $mirrorRecord) {
359
+ try {
360
+ $record = Get-Content $mirrorRecord -Raw -Encoding UTF8 | ConvertFrom-Json
361
+ $pluginShim = if ($record.pluginRoot) { Join-Path $record.pluginRoot 'bin\rea-shim.mjs' } else { $null }
362
+ if ($pluginShim -and (Test-Path $pluginShim)) {
363
+ $same = (Get-FileHash $mirrored -Algorithm SHA256).Hash -eq (Get-FileHash $pluginShim -Algorithm SHA256).Hash
364
+ if ($same) { Ok '镜像 shim 与插件一致(行跑的就是这个版本)' }
365
+ else { Warn "镜像 shim 落后于插件($($record.pluginRoot))—— 行仍在跑旧副本;重跑 node scripts/setup.mjs 让它换成新版本" }
366
+ } else {
367
+ Warn "mirror.json 指向的插件目录已不存在($($record.pluginRoot))—— 无法比对镜像 shim 是否最新"
368
+ }
369
+ } catch { Warn "mirror.json 解析失败: $($_.Exception.Message)" }
370
+ }
371
+ }
344
372
  else { Bad "缺少 MCP 行指向的 shim: $mirrored —— 重跑 setup --reuse" }
345
373
  # 镜像副本的 setup 用它自己所在的位置推 plugin root,所以 manifest 必须
346
374
  # 一起镜像。旧版镜像缺这个文件:auto-provision 会以 ENOENT 崩掉且不可见,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-rea",
3
- "version": "2.0.6",
3
+ "version": "2.0.8",
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
  "main": "./index.mjs",
@@ -63,7 +63,7 @@
63
63
  },
64
64
  "engines": {
65
65
  "node": "^22.19.0 || ^24.11.0 || >=26.0.0",
66
- "dsh": ">=0.1.7-rc.2 <0.3.0"
66
+ "dsh": ">=0.1.7-rc.2 <0.3.0-0"
67
67
  },
68
68
  "publishConfig": {
69
69
  "registry": "https://registry.npmjs.org/"
@@ -24,6 +24,15 @@
24
24
  * shape for a gate. When a release is added upstream, add it here too — that
25
25
  * edit is the reminder to re-check the row against the new host.
26
26
  *
27
+ * SEMANTICS: this file evaluates the declaration the way `includePrerelease:
28
+ * true` does, and that choice is not stylistic. Both consumers of `engines.dsh`
29
+ * in this ecosystem pass that option — dshmarket's discovery
30
+ * (`satisfiesRange(host, declared, { includePrerelease: true })` in
31
+ * `deriveHostCompatibility`) and the host's own peer gate — so a plain semver
32
+ * reading, where `0.2.0-rc.2` does NOT satisfy `>=0.1.7-rc.2 <0.3.0-0` (a
33
+ * prerelease must be named by a comparator on its own tuple), would disagree
34
+ * with the verdict users actually see. The tests pin the option's effect.
35
+ *
27
36
  * Usage: `node scripts/host-compat.mjs` (exit 0 = every listed release is
28
37
  * covered by the declared range; exit 1 = at least one is not).
29
38
  */
@@ -59,6 +68,12 @@ export const HOST_RELEASES = [
59
68
  /** Hosts that must fall OUTSIDE the declaration, for the other direction. */
60
69
  export const OUTSIDE_HOSTS = [
61
70
  '0.3.0',
71
+ // Prereleases of the NEXT line are the case a plain `<0.3.0` ceiling lets
72
+ // through: with `includePrerelease` they sort below `0.3.0` itself. The
73
+ // declared ceiling is `<0.3.0-0`, semver's idiom for "nothing from 0.3.0",
74
+ // and these entries are what proves it does what it says.
75
+ '0.3.0-rc.1',
76
+ '0.3.0-alpha.1',
62
77
  '0.1.7-rc.1',
63
78
  '0.1.7-alpha.2',
64
79
  ]
@@ -67,15 +82,25 @@ const SEMVER = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/
67
82
 
68
83
  /**
69
84
  * Split a version into comparable parts.
85
+ *
86
+ * Build metadata (`+sha`) is dropped, as semver says it must be for precedence
87
+ * and as node-semver does for comparison: a host that reports
88
+ * `0.2.0-rc.2+build.7` is running 0.2.0-rc.2. Everything else is deliberately
89
+ * strict — a leading `v` or a zero-padded identifier is rejected rather than
90
+ * guessed at, because this gate exists to catch a declaration nobody checked and
91
+ * a guess would put the guess in charge.
70
92
  * @param {string} version - a plain semver string, no range operators.
71
93
  * @returns {{ parts: number[], pre: (string|number)[] } | null} null when unparseable.
72
94
  */
73
95
  export function parseVersion(version) {
74
- const match = SEMVER.exec(version.trim())
96
+ const cleaned = String(version ?? '').trim().replace(/\+.*$/, '')
97
+ const match = SEMVER.exec(cleaned)
75
98
  if (match === null) return null
76
- const pre = match[4] === undefined
77
- ? []
78
- : match[4].split('.').map(part => (/^\d+$/.test(part) ? Number(part) : part))
99
+ const raw = match[4] === undefined ? [] : match[4].split('.')
100
+ // semver forbids a zero-padded numeric prerelease identifier (`0.2.1-01`);
101
+ // accepting one would make this gate read a typo as a version.
102
+ if (raw.some(part => /^\d+$/.test(part) && part.length > 1 && part.startsWith('0'))) return null
103
+ const pre = raw.map(part => (/^\d+$/.test(part) ? Number(part) : part))
79
104
  return { parts: [Number(match[1]), Number(match[2]), Number(match[3])], pre }
80
105
  }
81
106
 
@@ -30,6 +30,11 @@ function Write-Step([string]$Message) { Write-Host "[bootstrap] $Message" }
30
30
 
31
31
  # --- where things go -------------------------------------------------------
32
32
 
33
+ # Same precedence as setup.mjs, the shim and check-updates.ps1: explicit override,
34
+ # then DSH_HOME, then ~/.dsh. An override this script alone ignored would put
35
+ # ~800 MB into <DSH_HOME>/plugins/rea-bundle while every reader looked in
36
+ # REA_BUNDLE_ROOT, saw an empty bundle, and started a second run.
37
+ if (-not $Root -or $Root.Trim() -eq '') { $Root = $env:REA_BUNDLE_ROOT }
33
38
  if (-not $Root -or $Root.Trim() -eq '') {
34
39
  $home2 = $env:DSH_HOME
35
40
  if (-not $home2 -or $home2.Trim() -eq '') { $home2 = Join-Path $HOME '.dsh' }
package/scripts/setup.mjs CHANGED
@@ -1553,7 +1553,11 @@ if (!options.dryRun) {
1553
1553
  const shim = spawnSync(process.execPath, [mirroredShim, "--version"], {
1554
1554
  encoding: "utf8",
1555
1555
  timeout: 120000,
1556
- env: { ...process.env, REA_BUNDLE_ROOT: options.root },
1556
+ // Pin the bundle path: this step exists to confirm the shim relays the REA
1557
+ // THIS run provisioned. Under the default `auto` mode a machine that also
1558
+ // has a newer global REA answers with its own version, and the step would
1559
+ // report success for something this run never verified.
1560
+ env: { ...process.env, REA_BUNDLE_ROOT: options.root, REA_BUNDLE_LAUNCH: "bundle" },
1557
1561
  });
1558
1562
  const output = `${shim.stdout ?? ""}${shim.stderr ?? ""}`.trim();
1559
1563
  const ok = shim.status === 0 && /\d+\.\d+\.\d+/.test(output);
@@ -1632,6 +1636,31 @@ function mirrorLauncher(context) {
1632
1636
  */
1633
1637
  function writeMirrorRecord(context, source) {
1634
1638
  const record = { schema: 1, pluginRoot: source, mirroredAt: new Date().toISOString() };
1639
+ // Provenance for the self-healing check: the mirrored shim compares itself
1640
+ // with `<pluginRoot>/bin/rea-shim.mjs` and re-execs that one when the plugin
1641
+ // has moved on. These two fields are what `--shim-status` shows when the
1642
+ // comparison cannot be made (missing checkout, unreadable file).
1643
+ try {
1644
+ record.shimSha256 = createHash("sha256")
1645
+ .update(readFileSync(path.join(context.root, "bin", "rea-shim.mjs")))
1646
+ .digest("hex");
1647
+ } catch {
1648
+ /* the copy is missing or unreadable; the shim reports the comparison itself */
1649
+ }
1650
+ try {
1651
+ record.pluginVersion = JSON.parse(readFileSync(path.join(source, "package.json"), "utf8")).version ?? null;
1652
+ } catch {
1653
+ record.pluginVersion = null;
1654
+ }
1655
+ // The MCP child the harness starts has every DSH_* variable stripped, so the
1656
+ // mirrored shim cannot re-derive a CUSTOM home from its environment — it would
1657
+ // read `~/.dsh` and miss a duplicate `serverName: rea` living in the real one.
1658
+ // Record the home setup ran under; the shim prefers the environment, then this.
1659
+ try {
1660
+ record.dshHome = dshHome();
1661
+ } catch {
1662
+ record.dshHome = null;
1663
+ }
1635
1664
  try {
1636
1665
  mkdirSync(context.root, { recursive: true });
1637
1666
  writeFileSync(path.join(context.root, "mirror.json"), JSON.stringify(record, null, 2) + "\n");