mirror of
https://github.com/czcdr/dsh-plugin-rea.git
synced 2026-11-07 10:25:59 -10:00
bundle: give each shim copy the bundle root it should have
The previous round made the shim resolve its bundle from its own location, so a custom DSH_HOME could not send it looking in ~/.dsh. That is right for the mirrored copy the published row launches — but wrong for the checkout copy a human runs, whose own directory is the plugin, not a bundle: * `npm run bundle:status` reported "not provisioned" on a fully provisioned machine, and the status line setup prints pointed at that same copy; * a bare `node bin/rea-shim.mjs mcp` would have provisioned ~800 MB into the plugin directory. Only a checkout ships cordis.patch.yml, so that file now discriminates: the mirrored copy keeps using its own position (an MCP child has no DSH_HOME to consult), while the checkout resolves the same root setup.mjs writes — including a custom --root, which is why the status hint now names the mirrored copy. The audit guards the discriminator by rejecting a mirror list that would copy it along; mirroring it would silently flip a mirrored copy back to the DSH_HOME rule. The README's "install the plugin first" paragraph was also false: the row points at a file setup creates, so before the first provisioning run it exits with MODULE_NOT_FOUND and nothing starts a background provisioning. It now says so and names the one manual step. Verified: 16/16 unit tests on Windows and on Linux — three of them spawn the shim in both layouts, and reverting the fix turns the checkout one red; audit clean with 5/5 deliberate sabotages caught; 25/25 end-to-end path checks; and the row → shim → MCP handshake still returning rea 6.4.0 with 170 tools.
This commit is contained in:
1 parent
0bf5c3eee2
commit
9f818e1f05
7 files changed
+145
-18
No files matched your search
+16
-3
@@ -32,15 +32,28 @@
|
||||
`path.resolve`(相对 root 会让 `bundle.json` 记下相对路径,shim 随即误判组件"已消失"并重新供给)。
|
||||
- **`fail()` 的修复命令**不再指向"存在但必然崩溃"的镜像副本;没有可信副本时给出明确指引。
|
||||
- **`check-updates.ps1`** 增加「镜像 manifest 是否就位」检查,指向上面那条升级路径。
|
||||
- **shim 的两份副本各自解析出正确的 bundle root。** 上面那条"用自身位置"只在**镜像副本**上成立:
|
||||
插件 checkout 里的那份是插件而不是 bundle,于是 `npm run bundle:status` 会在已供给的机器上报
|
||||
"未供给",而裸跑 `node bin/rea-shim.mjs mcp` 会把约 800 MB 下载进**插件目录**。现在用
|
||||
`cordis.patch.yml`(只有 checkout 会带)区分两者:镜像副本用自身位置(MCP 子进程里读不到
|
||||
`DSH_HOME`),checkout 副本按与 setup 相同的规则解析。setup 结尾打印的 `status:` 命令也改为指向
|
||||
镜像副本,这样它对自定义 `--root` 同样成立。
|
||||
- **README 的「先装插件也可以」改为诚实描述。** 发布的行指向的镜像 shim **由 setup 创建**,所以在第一次
|
||||
供给之前该行以 `MODULE_NOT_FOUND` 退出 —— 没有任何东西能启动后台供给。首次必须手动跑一次
|
||||
`setup.mjs`,此后才由 shim 自动补供给。
|
||||
|
||||
### 回归防护
|
||||
|
||||
- `audit-manifest.mjs` 新增四项,全部做过反向验证(**故意写坏 4/4 都被抓住**):`!!js` 裸标量 mapping
|
||||
检测、`DSH_HOME` 三态(未设/空/已设)求值一致性、README 与 patch 表达式不得漂移、镜像清单必须含 manifest。
|
||||
- `audit-manifest.mjs` 新增五项,全部做过反向验证(**故意写坏 5/5 都被抓住**):`!!js` 裸标量 mapping
|
||||
检测、`DSH_HOME` 三态(未设/空/已设)求值一致性、README 与 patch 表达式不得漂移、镜像清单必须含
|
||||
manifest、镜像清单**不得**携带 `cordis.patch.yml`(它是 shim 区分两份副本的判据,被镜像进去会让镜像
|
||||
副本退回 `DSH_HOME` 规则)。
|
||||
- `validate.yml`:YAML 步骤注册 `!!js` 标量构造器(原先 `safe_load` 直接拒绝这个仓库要发布的行 ——
|
||||
**CI 一直是红的**);行契约步骤断言 `args[0]` 是标量、不是 mapping;新增 `bundle:test` 与 `bundle:audit` 两步。
|
||||
- 单元测试新增三条**真的去 spawn shim**的用例(镜像布局、checkout 布局、`REA_BUNDLE_ROOT` 覆盖),
|
||||
并做了反向验证:把 shim 改回"一律用自身位置"时,checkout 那条会红。
|
||||
- 单元测试里的 POSIX 退化断言加平台守卫(`EXE` 为空时 `endsWith("java")` 恒真,在 Linux 上必红;
|
||||
WSL 实测修前 12/13、修后 13/13)。
|
||||
WSL 实测修前 12/13、修后 16/16)。
|
||||
|
||||
### 升级说明
|
||||
|
||||
|
||||
@@ -86,9 +86,12 @@ If Node is already installed, you can skip the bootstrap and run provisioning di
|
||||
node scripts/setup.mjs --reuse # or --dry-run to see the plan first
|
||||
```
|
||||
|
||||
To install the plugin **first** instead, that also works: the MCP row starts, finds no bundle state, reports
|
||||
exactly what to run, and starts provisioning in the background. The harness is never blocked —
|
||||
`failOnStartupError: false` is deliberate.
|
||||
Installing the plugin **first** also works, but it takes two steps. The row resolves to
|
||||
`<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs`, and that file is created by setup — so until the first
|
||||
provisioning run the row exits with `MODULE_NOT_FOUND`, and nothing is there to start a background
|
||||
provisioning (the harness itself is unaffected: `failOnStartupError: false` is deliberate). Run
|
||||
`node scripts/setup.mjs` once; from then on the shim is in place, and every later missing state is picked up
|
||||
by the shim itself, which provisions in the background.
|
||||
|
||||
## Configure — nothing
|
||||
|
||||
@@ -130,6 +133,11 @@ node bin/rea-shim.mjs --shim-status # resolved paths, versions, verificatio
|
||||
node bin/rea-shim.mjs --shim-print-env # the environment the MCP child will see
|
||||
```
|
||||
|
||||
Both commands answer with the same bundle whether you run them from the plugin directory (the checkout copy)
|
||||
or from `<DSH_HOME>/plugins/rea-bundle` (the mirrored copy): the checkout resolves the location by the same
|
||||
rule setup uses, while the mirror uses its own position — it cannot rely on `DSH_HOME`, which the harness
|
||||
strips from an MCP child's environment.
|
||||
|
||||
Setting `REA_BUNDLE_AUTO_PROVISION=0` disables the first-run background provisioning.
|
||||
|
||||
To pin a different launch command on this machine (for example your own globally installed `rea`), override
|
||||
|
||||
+8
-2
@@ -80,8 +80,10 @@ dsh plugin --profile <profile> add github:czcdr/dsh-plugin-rea#feat/one-plugin-b
|
||||
node scripts/setup.mjs --reuse # 或先 --dry-run 看计划
|
||||
```
|
||||
|
||||
反过来**先装插件**也可以:那一行会启动、发现没有本机状态、把该跑的命令原样告诉你,并在后台开始供给。
|
||||
harness 永远不会被卡住 —— `failOnStartupError: false` 是刻意的。
|
||||
反过来**先装插件**也可以,但要分两步:发布的行指向 `<DSH_HOME>/plugins/rea-bundle/bin/rea-shim.mjs`,
|
||||
而那个文件**只有 setup 会创建**——所以在第一次供给之前,该行会以 `MODULE_NOT_FOUND` 退出,也就没有
|
||||
任何东西能去启动后台供给(harness 本身不受影响:`failOnStartupError: false` 是刻意的)。先手动跑一次
|
||||
`node scripts/setup.mjs`;此后 shim 就位,之后每一次「状态缺失」都由它自己发现并在后台补供给。
|
||||
|
||||
## 配置 —— 不需要
|
||||
|
||||
@@ -123,6 +125,10 @@ node bin/rea-shim.mjs --shim-status # 解析到的路径、版本、校验
|
||||
node bin/rea-shim.mjs --shim-print-env # MCP 子进程将看到的环境变量
|
||||
```
|
||||
|
||||
这两个命令在**插件目录**(checkout 副本)或 `<DSH_HOME>/plugins/rea-bundle`(镜像副本)里跑都给出同一
|
||||
答案:checkout 副本按与 setup 相同的规则解析供给位置,镜像副本用自身位置(因为 MCP 子进程的环境里
|
||||
`DSH_*` 会被剥掉,它不能依赖 `DSH_HOME`)。
|
||||
|
||||
设 `REA_BUNDLE_AUTO_PROVISION=0` 可关掉首次运行的后台自动供给。
|
||||
|
||||
想在本机把这一行改成别的启动命令(例如用你自己全局装的 `rea`),从 profile 自己的
|
||||
|
||||
+24
-8
@@ -16,6 +16,7 @@
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -24,24 +25,39 @@ const EX_CONFIG = 78;
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PLUGIN_ROOT = path.resolve(HERE, "..");
|
||||
|
||||
/** DSH home, by the same rule setup.mjs and the published patch use. */
|
||||
function dshHome() {
|
||||
const fromEnv = process.env.DSH_HOME;
|
||||
if (fromEnv && fromEnv.trim() !== "") return path.resolve(fromEnv);
|
||||
return path.join(homedir(), ".dsh");
|
||||
}
|
||||
|
||||
/**
|
||||
* Where setup writes the machine-local truth.
|
||||
*
|
||||
* The authoritative answer is this file's own location: the row launches
|
||||
* `<bundle>/bin/rea-shim.mjs`, so the bundle root is the directory above `bin/`.
|
||||
* Two copies of this file exist and they need different answers, so the copy has
|
||||
* to be identified before the root can be:
|
||||
*
|
||||
* DSH_HOME must NOT be consulted here. The harness strips every `DSH_*` name from
|
||||
* an MCP child's environment (case-insensitively, because Windows is), so this
|
||||
* process never sees a custom DSH_HOME: falling back to `~/.dsh` would look for
|
||||
* bundle.json in the wrong home, report "not provisioned" for a bundle sitting
|
||||
* right next to it, and then start a second ~800 MB provisioning run there.
|
||||
* * The **mirrored** copy at `<bundle>/bin/rea-shim.mjs` is what the published
|
||||
* row launches, and its own location is the bundle root. It must not consult
|
||||
* DSH_HOME: the harness strips every `DSH_*` name from an MCP child's
|
||||
* environment (case-insensitively, because Windows is), so a custom DSH_HOME
|
||||
* would send it to `~/.dsh`, where it would report "not provisioned" for a
|
||||
* bundle sitting right next to it — and start a second ~800 MB run there.
|
||||
* * The **checkout** copy (`<plugin>/bin/rea-shim.mjs`) is what a human runs, e.g.
|
||||
* `npm run bundle:status`, from a shell where DSH_HOME *is* visible. Its own
|
||||
* directory is the plugin, not the bundle, so resolving to itself would report
|
||||
* "not provisioned" on a fully provisioned machine — and a bare
|
||||
* `node bin/rea-shim.mjs mcp` would provision ~800 MB into the checkout.
|
||||
*
|
||||
* Only the checkout ships `cordis.patch.yml`, so that file is the discriminator.
|
||||
* REA_BUNDLE_ROOT stays as an explicit override for tests and unusual layouts.
|
||||
*/
|
||||
function bundleRoot() {
|
||||
const override = process.env.REA_BUNDLE_ROOT;
|
||||
if (override && override.trim() !== "") return path.resolve(override);
|
||||
return PLUGIN_ROOT;
|
||||
if (!existsSync(path.join(PLUGIN_ROOT, "cordis.patch.yml"))) return PLUGIN_ROOT;
|
||||
return path.join(dshHome(), "plugins", "rea-bundle");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -372,6 +372,19 @@ if (manifest) {
|
||||
add("error", "scripts/setup.mjs", `mirrorLauncher() does not mirror ${needed}, which the mirrored copy reads at run time`);
|
||||
}
|
||||
}
|
||||
// The shim tells its two copies apart by `cordis.patch.yml`, which only the
|
||||
// checkout ships: a mirrored copy resolves its bundle from its own location
|
||||
// (an MCP child has no DSH_HOME to consult), a checkout copy resolves the
|
||||
// same root setup writes. Mirroring the discriminator would silently flip a
|
||||
// mirrored copy back to the DSH_HOME rule — wrong wherever DSH_HOME is
|
||||
// custom, and wrong in every MCP child, where it is stripped.
|
||||
if (/cordis\.patch\.yml/.test(sourcesBody)) {
|
||||
add(
|
||||
"error",
|
||||
"scripts/setup.mjs",
|
||||
"the mirror list ships cordis.patch.yml, which bin/rea-shim.mjs uses to tell a checkout from a mirrored copy — a mirrored copy would then resolve its bundle through DSH_HOME, which an MCP child does not have",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -1363,7 +1363,10 @@ if (options.json) {
|
||||
// lock, but the shim treats a lock older than six hours as stale.
|
||||
rmSync(path.join(options.root, "setup.lock"), { force: true });
|
||||
log("restart DSH (or reload the profile) so the MCP row reconnects.");
|
||||
log(`status: node ${path.join(PLUGIN_ROOT, "bin", "rea-shim.mjs")} --shim-status`);
|
||||
// Point at the mirrored copy, not at this checkout's: the mirror resolves its
|
||||
// bundle from its own location, so this command is right for the root this run
|
||||
// actually provisioned — including a custom --root.
|
||||
log(`status: node ${path.join(options.root, "bin", "rea-shim.mjs")} --shim-status`);
|
||||
} else {
|
||||
log("some components are unmet; fix the cause and re-run this command.");
|
||||
}
|
||||
|
||||
+69
-1
@@ -11,10 +11,12 @@
|
||||
* Runs with `node --test`. No network, no filesystem writes outside a temp dir.
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/* ------------------------------------------------- probe token substitution */
|
||||
|
||||
@@ -191,3 +193,69 @@ test("byte sizes format without a floating-point surprise", () => {
|
||||
assert.equal(mib(569649598), "543.3");
|
||||
assert.equal(mib(8044989), "7.7");
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------ the shim's two homes */
|
||||
|
||||
// The shim runs from two places and must answer differently in each:
|
||||
// * the mirrored copy the published row launches sits at <bundle>/bin/ and must
|
||||
// use its own location — an MCP child has no DSH_HOME to consult, because the
|
||||
// harness strips every DSH_* name from it;
|
||||
// * the checkout copy a human runs (`npm run bundle:status`) is a plugin, not a
|
||||
// bundle, and must resolve the same root setup.mjs writes.
|
||||
// Both mistakes are silent: the checkout claims "not provisioned" on a machine
|
||||
// that is provisioned, and a bare `node bin/rea-shim.mjs mcp` provisions ~800 MB
|
||||
// into the plugin directory.
|
||||
const SHIM_SOURCE = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "bin", "rea-shim.mjs");
|
||||
|
||||
function shimBundleRoot(shimFile, env) {
|
||||
const run = spawnSync(process.execPath, [shimFile, "--shim-status"], {
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, REA_BUNDLE_AUTO_PROVISION: "0", ...env },
|
||||
});
|
||||
// --shim-status prints its report even when no bundle exists yet (exit 78).
|
||||
return JSON.parse(run.stdout).bundleRoot;
|
||||
}
|
||||
|
||||
test("a mirrored shim takes its bundle root from its own location, not DSH_HOME", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "rea-shim-mirror-"));
|
||||
try {
|
||||
const mirror = path.join(root, "plugins", "rea-bundle");
|
||||
mkdirSync(path.join(mirror, "bin"), { recursive: true });
|
||||
copyFileSync(SHIM_SOURCE, path.join(mirror, "bin", "rea-shim.mjs"));
|
||||
assert.equal(shimBundleRoot(path.join(mirror, "bin", "rea-shim.mjs"), { DSH_HOME: path.join(root, "elsewhere") }), mirror);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("a checkout shim resolves the bundle root setup.mjs would write", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "rea-shim-checkout-"));
|
||||
try {
|
||||
const checkout = path.join(root, "checkout");
|
||||
mkdirSync(path.join(checkout, "bin"), { recursive: true });
|
||||
copyFileSync(SHIM_SOURCE, path.join(checkout, "bin", "rea-shim.mjs"));
|
||||
// The discriminator: only a plugin checkout ships cordis.patch.yml.
|
||||
writeFileSync(path.join(checkout, "cordis.patch.yml"), "- insert: []\n");
|
||||
const home = path.join(root, "dsh-home");
|
||||
assert.equal(
|
||||
shimBundleRoot(path.join(checkout, "bin", "rea-shim.mjs"), { DSH_HOME: home }),
|
||||
path.join(home, "plugins", "rea-bundle"),
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("REA_BUNDLE_ROOT still overrides both layouts", () => {
|
||||
const root = mkdtempSync(path.join(tmpdir(), "rea-shim-override-"));
|
||||
try {
|
||||
const checkout = path.join(root, "checkout");
|
||||
mkdirSync(path.join(checkout, "bin"), { recursive: true });
|
||||
copyFileSync(SHIM_SOURCE, path.join(checkout, "bin", "rea-shim.mjs"));
|
||||
writeFileSync(path.join(checkout, "cordis.patch.yml"), "- insert: []\n");
|
||||
const forced = path.join(root, "forced");
|
||||
assert.equal(shimBundleRoot(path.join(checkout, "bin", "rea-shim.mjs"), { REA_BUNDLE_ROOT: forced }), forced);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user