1 Commits
Author SHA1 Message Date
czcdr d0e2ffd7a6 package: declare the entry file the market's update check requires (2.0.5)
dshmarket's update path asks hasLoadableEntry whether the new build has an entry file that exists (main/exports, else index.js) and rolls back when it does not. This package declared neither, and the package it mounts (@deepseek-ai/dsh-mcp-client) ships inside the harness build rather than in node_modules, so every location the check probes was empty: 2.0.1 -> 2.0.4 failed three times with 'updated build has no loadable entry' on a healthy profile, and the rollback was reported unverifiable for the same reason.

index.mjs is a real but deliberately empty cordis plugin: the working row is still inserted by cordis.patch.yml, so loading it must do nothing (a second serverName: rea would be refused). Two tests, verify-branch and the CI manifest/payload checks now pin both the pointer and its presence in files[].
2026-10-18 11:53:24 +08:00
8 changed files with 165 additions and 6 deletions

No files matched your search

+8 -2
View File
@@ -25,10 +25,16 @@ jobs:
const fail = (m) => { console.error('FAIL: ' + m); process.exit(1) };
if (!p.dsh || !p.dsh.bundle || !p.dsh.bundle.patch) fail('package.json is missing dsh.bundle.patch');
if (p.private) fail('package.json must not be private to be publishable');
for (const f of ['cordis.patch.yml', 'README.md', 'LICENSE', 'check-updates.ps1']) {
for (const f of ['cordis.patch.yml', 'index.mjs', 'README.md', 'LICENSE', 'check-updates.ps1']) {
if (!p.files.includes(f)) fail('files[] must ship ' + f);
}
if (!require('fs').existsSync(p.dsh.bundle.patch)) fail('dsh.bundle.patch target does not exist');
// The market's update path rolls a build back when package.json points
// at no existing entry (dshmarket's hasLoadableEntry); this package
// shipped no main at all until 2.0.5, so every update of a healthy
// profile failed with "updated build has no loadable entry".
if (typeof p.main !== 'string') fail('package.json must declare main');
if (!require('fs').existsSync(p.main)) fail('main points at no shipped file: ' + p.main);
console.log('manifest ok');
"
@@ -51,7 +57,7 @@ jobs:
const meta = JSON.parse(execSync('npm pack --dry-run --json', { encoding: 'utf8' }))[0];
const files = meta.files.map((f) => f.path);
const need = [
'cordis.patch.yml', 'README.md', 'README.zh-CN.md', 'LICENSE', 'CHANGELOG.md', 'check-updates.ps1',
'cordis.patch.yml', 'index.mjs', 'README.md', 'README.zh-CN.md', 'LICENSE', 'CHANGELOG.md', 'check-updates.ps1',
'screenshots.json', 'assets/screenshot-1.png', 'assets/screenshot-2.png',
];
const missing = need.filter((f) => !files.includes(f));
+30
View File
@@ -2,6 +2,36 @@
本插件遵循语义化版本。版本号变化说明「配置组合」这层的变化,与 REA 自身的版本无关。
## 2.0.5 — 2026-10-18
**市场更新失败已定位并修复:清单里没有 `main`,市场因此在每次更新时判「新构建没有可加载入口」并回滚。**
### 修复
- **应用内市场更新到 2.0.4 失败**:`dshmarket` 的更新路径用 `hasLoadableEntry` 判断新构建能不能加载
(实现是 `entryArtifactExists`:读 `main`/`exports`,两者都没有就回落 `index.js`)。本包是纯 bundle
(只有 `dsh.bundle.patch`),从未声明 `main`;而它挂载的 `@deepseek-ai/dsh-mcp-client` 属于 harness 自身
构建、不在 `node_modules` 里,于是该检查要看的 profile / 包内 / workspace 三个位置全空。结果:2.0.1 → 2.0.4
的更新连续三次报「更新后缺少入口文件……且未能验证恢复原版本文件」,而回滚其实成功了 —— 旧版同样没有入口,
所以回滚后的复检依然为 false。现已声明 `main: ./index.mjs`,并让该文件进入 `files[]`。
- **`index.mjs` 是真空插件,不是占位**:导出 `name` 与空 `apply()`,不注册行、不起进程、不写状态;真正起作用的
行仍由 `cordis.patch.yml` 插入。若 harness 某天真把它当插件加载,行为仍是「什么都不做」,因此不会造出第二条
`serverName: rea`。
### 文档
- 两份 README 新增「在市场里更新 / Updating from the market」小节:说明该报错来自市场侧的入口判定、与 tarball
完整性无关、profile 未受损,以及 2.0.5 起的做法。
### 验证
- 用 `dshmarket` 自身的 `hasLoadableEntry` 实测:把本包按 profile 布局模拟安装,修复前 `false`、修复后 `true`;
对仓库目录 `entryArtifactExists` 修复后为 `true`。
- 单测 42 条(新增 2 条:清单声明的入口必须存在且被 `files[]` 收录;入口可 `import`、导出的 `name` 与包名一致、
`apply()` 返回 `undefined`):Windows 39 过 + 3 平台跳过。
- `verify-branch.ps1` 新增 `main` 存在性与 `files[]` 收录检查;`validate.yml` 的清单检查与 npm 载荷检查加了同一条守卫。
- `npm pack --dry-run`:20 个文件,含 `index.mjs`。
## 2.0.4 — 2026-10-17
**两处已核实的缺陷:只读 bundle 目录下 shim 启动即崩;与上游 `rea setup --client dsh` 的同名注册冲突现在会被明确提示。**
+21
View File
@@ -262,6 +262,27 @@ macOS/Linux.
## Updating — three independent things
### Updating from the market
Update it where you installed it. Updating **to 2.0.5 or later** is routine; updating an installed 2.0.1–2.0.4
*through the in-app market* failed with this, three times on a healthy profile:
> dsh-plugin-rea 更新后缺少入口文件(package.json 的 main/exports 指向的文件不存在),且未能验证恢复原版本文件
That message is not about a broken tarball. On every update the market asks whether the new build's
`package.json` points at an entry file that exists (`hasLoadableEntry` in `dshmarket`), and rolls the build back
when the answer is false. A bundle may legitimately own no entry — but only if a package it *mounts* has one, and
the package this bundle mounts (`@deepseek-ai/dsh-mcp-client`) ships inside the harness build rather than in
`node_modules`, so every location that test probes comes back empty. The 1.x and 2.0.x manifests also declared
no `main` at all, so the answer was false even though the profile was working; the rollback was then reported as
unverifiable for the same reason. Nothing in the profile was actually damaged — the update simply did not apply.
2.0.5 declares `main: ./index.mjs`. That file is a real, deliberately empty Cordis plugin: it registers nothing,
spawns nothing, touches no state — the working row is still inserted by `cordis.patch.yml` — and it exists so the
question "can this package load?" has an honest answer, and so `import 'dsh-plugin-rea'` resolves. On an older
version, update to `>=2.0.5`; if the market refuses before that version exists, install `[email protected]`
(Desktop: through the plugin manager) instead of updating in place.
### Upgrading from 1.x
Install 2.x, then run provisioning **once** before restarting dsh:
+19
View File
@@ -246,6 +246,25 @@ shim** 的 MCP `initialize` 握手返回 `serverInfo {name: rea, version: 6.5.0}
## 更新 —— 三件互相独立的事
### 在市场里更新
在哪儿装的就在哪儿更新。更新到 **2.0.5 及以上**是常规操作;而把已装的 2.0.1–2.0.4 通过应用内市场**更新到
2.0.4**,在健康的 profile 上连续失败了三次:
> dsh-plugin-rea 更新后缺少入口文件(package.json 的 main/exports 指向的文件不存在),且未能验证恢复原版本文件
这条消息与 tarball 是否完整无关。市场在每次更新时都会问一句:新构建的 `package.json` 是否指向一个真实存在的
入口文件(`dshmarket` 里的 `hasLoadableEntry`);答案是「否」,就把这次更新判为坏构建并回滚。bundle 本来允许
自己没有入口 —— 前提是它**挂载**的某个包有入口;而本包挂载的 `@deepseek-ai/dsh-mcp-client` 属于 harness 自身
构建,不在 `node_modules` 里,于是这项检查要看的每个位置都是空的。1.x 与 2.0.x 的清单又根本没有声明 `main`,
所以答案必然是「否」,哪怕 profile 一直是好的;紧接着的回滚也因此被报成「无法验证」。实际上 profile 没有被
破坏,只是这次更新没有生效。
2.0.5 声明了 `main: ./index.mjs`。那是一个真实存在、刻意留空的 Cordis 插件:不注册任何行、不启动任何进程、
不碰任何状态(真正起作用的行仍由 `cordis.patch.yml` 插入),它的作用只是让「这个包能不能加载」有一个诚实的
答案,并让 `import 'dsh-plugin-rea'` 能解析。还停在旧版本时,更新到 `>=2.0.5` 即可;若市场在 2.0.5 之前就拒绝,
就直接安装 `[email protected]`(桌面端:在插件管理器里装),不要原地更新。
### 从 1.x 升级
装上 2.x 后,**先跑一次供给**,再重启 dsh:
+43
View File
@@ -0,0 +1,43 @@
/**
* Package entry — the smallest thing that makes this bundle a *loadable* package.
*
* Everything this plugin DOES lives in `cordis.patch.yml`: it inserts one row of
* `@deepseek-ai/dsh-mcp-client` that launches `bin/rea-shim.mjs`, and that row is
* what the harness loads. This file registers nothing, starts nothing and
* provisions nothing.
*
* It exists because a package that declares no `main`/`exports` is judged by its
* consumers as "has no entry file", whether or not the loader ever needs one:
*
* - The market's update path (`hasLoadableEntry` in dshmarket) asks that
* question on every update and, when the answer is false, treats the new
* build as broken and rolls back. A bundle with no entry of its own is
* *supposed* to survive the test through the packages it mounts — but the
* package this one mounts (`@deepseek-ai/dsh-mcp-client`) is part of the
* harness build, not of `node_modules`, so every location that test probes
* comes back empty. Measured on 2026-10-18: updating an installed, working
* 2.0.1 to 2.0.4 failed three times with "updated build has no loadable
* entry", and the rollback was then reported as unverifiable for the same
* reason.
* - `npm pack` metadata and `import 'dsh-plugin-rea'` both resolve `main`;
* absent, both fall back to `index.js`, which this repository never shipped.
*
* So this is a real module with a real Cordis plugin shape rather than a stub
* for a checker: if the harness ever does load this package as a plugin, it
* loads cleanly and does nothing.
*/
/** Cordis plugin name. Also the package name, which is what the loader expects. */
export const name = 'dsh-plugin-rea'
/**
* Intentionally empty.
*
* The row that does the work is inserted by the bundle patch, not by this
* function: registering the MCP client from here would create a second
* `serverName: rea` entry, which the harness refuses as a duplicate (the same
* collision `bin/rea-shim.mjs` warns about for upstream's `rea setup --client
* dsh`). Doing nothing is the correct behaviour, and the function present keeps
* this a valid Cordis plugin instead of an arbitrary module.
*/
export function apply() {}
+3 -1
View File
@@ -1,8 +1,9 @@
{
"name": "dsh-plugin-rea",
"version": "2.0.4",
"version": "2.0.5",
"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。",
"type": "module",
"main": "./index.mjs",
"license": "MIT",
"author": "czcdr",
"homepage": "https://github.com/czcdr/dsh-plugin-rea",
@@ -33,6 +34,7 @@
},
"files": [
"cordis.patch.yml",
"index.mjs",
"bin/",
"scripts/",
"manifest/",
+32 -1
View File
@@ -18,7 +18,7 @@ import { createServer } from "node:http";
import { tmpdir } from "node:os";
import path from "node:path";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";
/* ------------------------------------------------- probe token substitution */
@@ -1168,3 +1168,34 @@ test("--json releases the lock on success and marks it failed on failure", () =>
rmSync(root, { recursive: true, force: true });
}
});
/* --------------------------------------------------------- the package entry */
// The market judges an update by asking whether the new build's package.json
// points at an entry file that exists (`hasLoadableEntry` -> `entryArtifactExists`
// in dshmarket). This package declared neither main nor exports, and the package
// it mounts (`@deepseek-ai/dsh-mcp-client`) ships inside the harness build rather
// than in node_modules, so every location that test probes came back empty:
// updating a healthy 2.0.1 profile to 2.0.4 failed with "updated build has no
// loadable entry", and the rollback was then reported unverifiable for the same
// reason (measured 2026-10-18). These two tests are that regression.
test("package.json declares an entry file that is actually shipped", () => {
const pkg = JSON.parse(readFileSync(path.join(REPO_ROOT, "package.json"), "utf8"));
assert.equal(typeof pkg.main, "string", "package.json must declare main; without it consumers probe index.js, which does not exist");
assert.equal(existsSync(path.join(REPO_ROOT, pkg.main)), true, `${pkg.main} is declared but missing`);
// `main` alone is not enough: npm pack ships only what files[] lists, and a
// tarball without the entry reproduces the very failure this fixes.
const shipped = String(pkg.main).replace(/^\.\//, "");
assert.equal(pkg.files.includes(shipped), true, `files[] must ship ${shipped}`);
});
test("the entry loads as a cordis plugin and does nothing", async () => {
const pkg = JSON.parse(readFileSync(path.join(REPO_ROOT, "package.json"), "utf8"));
const mod = await import(pathToFileURL(path.join(REPO_ROOT, pkg.main)).href);
assert.equal(mod.name, pkg.name, "the loader resolves the plugin by this name");
assert.equal(typeof mod.apply, "function", "a cordis plugin needs apply()");
// Loading and applying must not register a row, spawn anything or touch state:
// registering the MCP client here would add a second `serverName: rea` entry,
// which the harness refuses. Returning undefined is the whole contract.
assert.equal(mod.apply(), undefined);
});
+9 -2
View File
@@ -12,7 +12,7 @@ function Check($name, [bool]$ok, $detail) {
}
Write-Host "`n=== 1. JS syntax ===" -ForegroundColor Cyan
foreach ($f in @('bin\rea-shim.mjs','scripts\setup.mjs','scripts\audit-manifest.mjs','scripts\setup.test.mjs')) {
foreach ($f in @('index.mjs','bin\rea-shim.mjs','scripts\setup.mjs','scripts\audit-manifest.mjs','scripts\setup.test.mjs')) {
$out = & node --check $f 2>&1
Check $f ($LASTEXITCODE -eq 0) ($out -join ' ')
}
@@ -73,9 +73,16 @@ Write-Host "`n=== 6. package payload lists what the bundle needs ===" -Foregroun
# -Encoding UTF8 is required: the package description contains CJK text, and
# PowerShell 5.1 reads a BOM-less file as ANSI, which breaks ConvertFrom-Json.
$pkg = Get-Content (Join-Path $root 'package.json') -Raw -Encoding UTF8 | ConvertFrom-Json
foreach ($entry in @('bin/', 'scripts/', 'manifest/', 'cordis.patch.yml')) {
foreach ($entry in @('index.mjs', 'bin/', 'scripts/', 'manifest/', 'cordis.patch.yml')) {
Check "files[] includes $entry" (@($pkg.files) -contains $entry) ''
}
# The market's update path asks whether package.json's main/exports points at a
# file that exists, and rolls the update back when it does not (dshmarket's
# hasLoadableEntry). This package declared neither until 2.0.5, so every update of
# a healthy profile failed with "updated build has no loadable entry". Asserting
# the pointer resolves keeps that from returning silently.
Check 'package.json declares main' (-not [string]::IsNullOrWhiteSpace($pkg.main)) ''
Check 'main resolves to a shipped file' ($pkg.main -and (Test-Path (Join-Path $root $pkg.main))) "main=$($pkg.main)"
foreach ($script in @('bundle:setup', 'bundle:status', 'bundle:audit', 'bundle:test')) {
Check "scripts.$script exists" ($pkg.scripts.PSObject.Properties.Name -contains $script) ''
}