dsh-plugin-toolkit 0.9.4 → 0.9.5

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 CHANGED
@@ -1,3 +1,21 @@
1
+ ## 0.9.5
2
+
3
+ - The panel reports its own failures. When a row fails to activate the app shows
4
+ one red dot and no text, so the bundle now posts what happened to
5
+ `POST /toolkit/report`; the host appends it to `~/.dsh/toolkit-client-report.ndjson`.
6
+ Every stage is reported — `apply`, `no-inject`, `no-sidebar`, `tab-registered`,
7
+ `tab-failed` — so "did it run at all" is answerable without a debugger.
8
+ - The failure that motivated it: `ctx.inject` callbacks run *after* `apply()`
9
+ returns, so the guards added in 0.9.4 could not see a throwing registration and
10
+ the row still ended up failed. The callback body is now guarded and reported.
11
+ - The client module no longer sets `name` on its face; it is the package's name
12
+ either way, and the face now matches the shape that works.
13
+ - `mountClient` accepts a `host`, so a test can hand the panel a service that
14
+ throws on purpose. Without it "the service threw" — the failure mode that costs
15
+ the most to debug in the app — was untestable.
16
+ - `README.zh.md`: the whole tour in Chinese, linked from the top of the README.
17
+ The install examples now say `@latest` instead of a version that goes stale.
18
+
1
19
  ## 0.9.4
2
20
 
3
21
  - The client module now declares `inject: ['slots', 'locale']` and guards every
package/README.md CHANGED
@@ -2,12 +2,14 @@
2
2
 
3
3
  ![dsh-plugin-toolkit](docs/card.png)
4
4
 
5
+ English · [中文](./README.zh.md)
6
+
5
7
  Everything a [DeepSeek Harness](https://github.com/deepseek-ai/dsh) plugin needs between `init` and a version the market index accepts: scaffolding, a test harness for the client bundle, a way to see a change without restarting the app, and a release that does not depend on remembering fourteen steps.
6
8
 
7
9
  ```sh
8
- npx dsh-plugin-toolkit@0.7.0 init ./my-panel --name my-panel # a plugin that already passes the checks
9
- npx dsh-plugin-toolkit@0.7.0 verify . # pack it, install the tarball, mount the panel
10
- npx dsh-plugin-toolkit@0.7.0 release --pr --merge # tag, wait for npm, update the index, open and merge the PR
10
+ npx dsh-plugin-toolkit@latest init ./my-panel --name my-panel # a plugin that already passes the checks
11
+ npx dsh-plugin-toolkit@latest verify . # pack it, install the tarball, mount the panel
12
+ npx dsh-plugin-toolkit@latest release --pr --merge # tag, wait for npm, update the index, open and merge the PR
11
13
  ```
12
14
 
13
15
  ## Three paths
@@ -32,8 +34,8 @@ $ dsh plugin add dsh-plugin-toolkit
32
34
  [`init`](#start-from-a-plugin-that-already-passes) writes the files whose absence shows up as a failed first publish — `engines.dsh`, the patch file, a `files` list that ships the code, a client bundle, a test for it, and the CI that runs both. `doctor` then checks the same things without writing anything, and [`verify`](#prove-the-tarball-works-not-just-the-repository) checks the tarball instead of the working copy.
33
35
 
34
36
  ```sh
35
- npx dsh-plugin-toolkit@0.7.0 init ./my-panel --name my-panel
36
- cd my-panel && npm install && npm test && npx dsh-plugin-toolkit@0.7.0 doctor .
37
+ npx dsh-plugin-toolkit@latest init ./my-panel --name my-panel
38
+ cd my-panel && npm install && npm test && npx dsh-plugin-toolkit@latest doctor .
37
39
  ```
38
40
 
39
41
  ### 2. Develop a panel
@@ -41,8 +43,8 @@ cd my-panel && npm install && npm test && npx dsh-plugin-toolkit@0.7.0 doctor .
41
43
  A plugin's client code is one file that calls `window.__ModuleLoader__.load({factory})` — there is no module to import, so the usual React test setup does not apply. [`testkit`](#test-a-panel-without-a-browser) supplies the missing half, and [`dev`](#edit-a-plugin-that-is-already-installed) pushes a rebuilt bundle into the running app.
42
44
 
43
45
  ```sh
44
- npx dsh-plugin-toolkit@0.7.0 mount ./client.js # what a user would see, without the app
45
- npx dsh-plugin-toolkit@0.7.0 dev . --host http://127.0.0.1:54133
46
+ npx dsh-plugin-toolkit@latest mount ./client.js # what a user would see, without the app
47
+ npx dsh-plugin-toolkit@latest dev . --host http://127.0.0.1:54133
46
48
  ```
47
49
 
48
50
  ### 3. Ship a version
@@ -50,8 +52,8 @@ npx dsh-plugin-toolkit@0.7.0 dev . --host http://127.0.0.1:54133
50
52
  `verify` first (it is the only check that reads the tarball), then `release`, which tags, waits for the registry, writes the index entry with the publish time the registry reports, and — with `--pr --merge` — opens and merges the pull request.
51
53
 
52
54
  ```sh
53
- npx dsh-plugin-toolkit@0.7.0 verify .
54
- npx dsh-plugin-toolkit@0.7.0 release . --pr --merge
55
+ npx dsh-plugin-toolkit@latest verify .
56
+ npx dsh-plugin-toolkit@latest release . --pr --merge
55
57
  ```
56
58
 
57
59
  ## Why this exists
@@ -72,7 +74,7 @@ Releasing one plugin fifteen times taught me three things the ecosystem's toolin
72
74
  ## Start from a plugin that already passes
73
75
 
74
76
  ```sh
75
- $ npx dsh-plugin-toolkit@0.7.0 init ./my-panel --name my-panel --title "My panel"
77
+ $ npx dsh-plugin-toolkit@latest init ./my-panel --name my-panel --title "My panel"
76
78
  created 8 file(s) in /Users/you/my-panel
77
79
  .github/workflows/ci.yml
78
80
  .gitignore
package/README.zh.md ADDED
@@ -0,0 +1,158 @@
1
+ # dsh-plugin-toolkit
2
+
3
+ ![dsh-plugin-toolkit](docs/card.png)
4
+
5
+ [English](./README.md) · 中文
6
+
7
+ 一个 [DeepSeek Harness](https://github.com/deepseek-ai/dsh) 插件从 `init` 到被市场索引接受之间需要的一切:脚手架、客户端 bundle 的测试台、不重启应用就能看到改动的方法,以及一条不用背十四个步骤的发布流程。
8
+
9
+ ```sh
10
+ npx dsh-plugin-toolkit@latest init ./my-panel --name my-panel # 生成一个已经能通过检查的插件
11
+ npx dsh-plugin-toolkit@latest verify . # 打包、安装 tarball、真的把面板挂起来
12
+ npx dsh-plugin-toolkit@latest release --pr --merge # 打 tag、等 npm、更新索引、开 PR 并合并
13
+ ```
14
+
15
+ ## 三条路径
16
+
17
+ | 你是谁 | 从这里开始 | 然后 |
18
+ | --- | --- | --- |
19
+ | 第一次写插件 | `init` | `doctor`、`verify` |
20
+ | 改一个已经装上的面板 | `dev` | `mount`、testkit、`audit` |
21
+ | 要发一个版本 | `verify` | `release`、`index` |
22
+
23
+ ## 为什么会有这个工具
24
+
25
+ 我发布自己第一个面板插件的那天,同一个错误在四个地方各出现一次,而且每次的报错都指向别处:
26
+
27
+ 1. `package.json` 里 `exports["."]` 指向 CLI,插件装上了,加载器那行**什么都 apply 不到**;
28
+ 2. `files` 忘了 `client.js`,`npm pack` 出来的包里没有面板代码,`doctor` 读工作副本看不出来;
29
+ 3. 声明了 `dsh.client` 却没有 `exports["./client"]`,宿主直接把这一行丢掉,界面上只留一个红点「异常」,没有任何文字;
30
+ 4. 客户端 apply 里没保护的 `ctx.locale.register(...)` 在服务就绪前抛错,**整个 web boot 失败**,应用把这判成「第三方插件不支持」并关掉所有插件。
31
+
32
+ 每一条都能在发布前被机器拦住,所以每一条现在都是一项检查。这些检查对着**宿主的解析代码**写,而不是对着文件约定写——差别就是 `verify` 报 `mounted the panel` 而真实宿主什么都不加载。
33
+
34
+ ## 安装要求
35
+
36
+ - Node.js 18 或更新(CI 在 18/20/22 × ubuntu/macos/windows 上跑)。
37
+ - 要发布时需要一个能写 `market/index.json` 的仓库和 `gh`(已登录)。
38
+
39
+ ## 从一个已经能通过检查的插件开始
40
+
41
+ ```sh
42
+ npx dsh-plugin-toolkit@latest init ./my-panel --name my-panel
43
+ cd my-panel && npm install && npm test
44
+ ```
45
+
46
+ 生成的项目里已经有了这些容易被忘掉的东西:
47
+
48
+ - `main` / `exports[".""]` 指向插件本体(`index.js`,导出 `apply`);
49
+ - `exports["./client"]` 指向 `client.js`(**没有它宿主不服务这个 bundle**);
50
+ - `dsh.client = { platform: "web", immediately: true }` 与 `dsh.bundle.patch`;
51
+ - `files` 覆盖上面每一个路径;
52
+ - 一份 `node --test` 用例和一个钉住本工具版本的 CI 工作流。
53
+
54
+ 版本号取自当前运行的 CLI 自身,所以脚手架不会落后于你正在读的文档。
55
+
56
+ ## 不用浏览器测面板
57
+
58
+ ```js
59
+ import { createFetch, expectSnapshot, mountClient } from 'dsh-plugin-toolkit/testkit';
60
+
61
+ const panel = await mountClient({
62
+ file: 'client.js',
63
+ copy: { en: {} },
64
+ fetch: createFetch({ '/toolkit/check': { body: { ok: true, checks: [] } } }),
65
+ });
66
+
67
+ await panel.type('.tk-input', '/tmp/plugin');
68
+ await panel.click('.tk-button');
69
+ console.log(panel.text());
70
+ await panel.unmount();
71
+ ```
72
+
73
+ `mountClient` 用 jsdom 加载真实的单文件 bundle、注入宿主服务、把组件真的渲染出来。它提供 `type` / `click` / `press`(都返回 Promise)、`text()`、`query` / `queryAll` / `must`,以及 `aria(selector)` / `expectAria(selector, expected)`——后者让「读屏器看到的是什么」变成断言,而不是靠选择器猜。
74
+
75
+ ### 留一份视觉基线
76
+
77
+ ```js
78
+ await expectSnapshot(panel, { name: 'empty' });
79
+ ```
80
+
81
+ 第一次运行写入 `test/__snapshots__/empty.html` 与 `.aria.txt`;之后每次比较,差异会打印出来。
82
+
83
+ ## 看看装了什么,以及应用到底加载了没有
84
+
85
+ ```sh
86
+ npx dsh-plugin-toolkit@latest audit --host http://127.0.0.1:54133
87
+ ```
88
+
89
+ 它扫描 `~/.dsh/profiles/*/node_modules` 里所有声明了 `dsh` 的包,对每个跑 `doctor`,再从宿主的 `/plugins/events` 读取「应用真的加载了它吗」。输出把两类问题分开:
90
+
91
+ - **hard**:`root export` 没有 `apply`、`exports["./client"]` 缺失、patch 形状不对——这些会让插件不工作;
92
+ - **advisory**:缺 `engines.dsh`、README 没有 Requirements 段——市场索引要这些,加载器不读。
93
+
94
+ `7 installed plugin(s) are not in the running app` 和 `13 … not ready for the market index` 是两件事,混着数会让人去修根本不影响使用的东西。
95
+
96
+ ## 改一个已经装上的插件
97
+
98
+ ```sh
99
+ npx dsh-plugin-toolkit@latest dev ./my-plugin --host http://127.0.0.1:54133
100
+ ```
101
+
102
+ 它监听源码,把改动复制进 profile 的 `node_modules`,然后在宿主的 `/plugins/events` SSE 上等一帧 `rebuilt` 确认——只有真的重载了才算成功。如果预览显示 `the running app has not loaded <id>`,那是插件没被加载,重启一次应用即可,不是 dev 的问题。
103
+
104
+ ## 在 CI 里用
105
+
106
+ ```yaml
107
+ - uses: WJDdidi-1/dsh-plugin-toolkit@v1
108
+ with:
109
+ dir: .
110
+ version: 0.9.5
111
+ ```
112
+
113
+ 默认跑 `verify`:打包、把 tarball 装进临时目录、import 根导出断言它有 `apply`、用裸 ctx 调一次 `apply({})` 确认服务缺失时降级而不是抛错、最后把面板挂起来。`doctor` 在读工作副本,`verify` 在读**用户真正会装到的东西**。
114
+
115
+ ## `doctor` 检查什么
116
+
117
+ | 检查 | 拦住的是 |
118
+ | --- | --- |
119
+ | manifest | 没有 `name` / `version` / `engines.dsh` |
120
+ | root export | `exports["."]` 指向 CLI,加载器那行没有插件可 apply |
121
+ | client entry | 找不到 bundle,或 `dsh.client` 与 `exports["./client"]` 不一致 |
122
+ | client declaration | `platform` 不是 `web`、缺 `exports["./client"]`、指向的文件不存在 |
123
+ | files list | `npm pack` 不会带上 manifest 指到的路径 |
124
+ | patch | `cordis.patch.yml` 不是唯一一条 `- insert:` |
125
+ | readme | 没有 Requirements 段(advisory,市场索引要) |
126
+
127
+ `dsh-plugin-toolkit doctor .` 的退出码只由 hard 项决定。
128
+
129
+ ## 面板不出现的时候
130
+
131
+ 按顺序查这五件事:
132
+
133
+ 1. **重启应用**——客户端 bundle 只在启动时加载;
134
+ 2. 根导出是不是插件:`node -e "import('dsh-plugin-toolkit').then(m => console.log(typeof m.apply))"` 应为 `function`;
135
+ 3. tarball 里有没有它:`npx dsh-plugin-toolkit verify .`;
136
+ 4. `cordis.patch.yml` 是不是恰好一条 `- insert:`,`name` 与包名一致;
137
+ 5. 面板自己有没有报错:`~/.dsh/toolkit-client-report.ndjson`(本插件的面板把激活失败写在这里,因为界面只有一个红点)。
138
+
139
+ ## `release` 做什么
140
+
141
+ 1. 预检版本、`engines.dsh`、README Requirements;
142
+ 2. `git tag plugin-vX.Y.Z && git push origin plugin-vX.Y.Z`(`--publish` 时接着 `npm publish`);
143
+ 3. 轮询 npm registry 直到这个版本真的可下载(传播通常 2–8 分钟,`npm view` 有缓存,直接读 `versions`);
144
+ 4. 把 `market/index.json` 里该插件改成新版本 + 真实 `publishTimestamp`;
145
+ 5. 在索引所在的仓库开 PR,`--merge` 时等 CI 全绿后 squash 合并;
146
+ 6. 从 raw URL 复核线上索引确实变了。
147
+
148
+ 索引在别人的仓库里时用 `--market <dir>`,它会对着那个目录做第 4–6 步。
149
+
150
+ ## 还没有的
151
+
152
+ - 不做插件市场本身,也不代替 `dsh plugin add`;
153
+ - 不校验你的面板长得好不好看——视觉基线只负责「和上次一样」;
154
+ - Windows 上的路径与 `npm.cmd` 有专门的测试,但没有 Windows 实机手工验证。
155
+
156
+ ## 许可
157
+
158
+ Apache-2.0。
package/client.js CHANGED
@@ -136,44 +136,99 @@ window.__ModuleLoader__.load({
136
136
  : null);
137
137
  }
138
138
 
139
+ function describe(error) {
140
+ if (error === undefined || error === null) return 'unknown';
141
+ const text = error.stack !== undefined ? String(error.stack) : String(error.message !== undefined ? error.message : error);
142
+ return text.split('\n').slice(0, 3).join(' | ');
143
+ }
144
+
145
+ /**
146
+ * The panel's own crash reporter. When the row fails to activate the app
147
+ * shows one red dot and no text, and the bundle has no console anyone can
148
+ * read, so it posts what happened and the host appends it to
149
+ * `~/.dsh/toolkit-client-report.ndjson`.
150
+ */
151
+ function report(stage, detail) {
152
+ try {
153
+ if (typeof fetch !== 'function') return;
154
+ const body = JSON.stringify({
155
+ stage,
156
+ message: describe(detail),
157
+ services: ['slots', 'locale', 'sidebarRightTabs', 'sidebarRight'],
158
+ });
159
+ fetch('/toolkit/report', {
160
+ method: 'POST',
161
+ headers: { 'Content-Type': 'application/json' },
162
+ body,
163
+ }).catch(() => {});
164
+ } catch {
165
+ // Reporting is best effort; it never changes the outcome.
166
+ }
167
+ }
168
+
139
169
  function apply(ctx) {
140
170
  // A panel that cannot find its services must degrade, not fail the boot:
141
171
  // an exception here fails the whole web boot and takes every other plugin
142
172
  // down with it. Every service below is optional for that reason.
173
+ const has = (name) => {
174
+ try {
175
+ return ctx[name] !== undefined && ctx[name] !== null;
176
+ } catch {
177
+ return false;
178
+ }
179
+ };
180
+ report('apply', `services slots=${String(has('slots'))} locale=${String(has('locale'))} inject=${typeof ctx.inject}`);
143
181
  try {
144
182
  injectStyles(typeof document === 'undefined' ? undefined : document);
145
- } catch {
183
+ } catch (error) {
146
184
  // No document: the styles are cosmetic.
185
+ report('styles', error);
147
186
  }
148
187
  try {
149
188
  if (ctx.locale !== undefined && typeof ctx.locale.register === 'function') ctx.locale.register(NS, COPY);
150
- } catch {
189
+ } catch (error) {
151
190
  // The panel keeps its English strings.
191
+ report('locale', error);
152
192
  }
153
193
  try {
154
- if (typeof ctx.inject !== 'function') return;
194
+ if (typeof ctx.inject !== 'function') {
195
+ report('no-inject', 'ctx.inject is not a function');
196
+ return;
197
+ }
155
198
  ctx.inject(['sidebarRightTabs', 'sidebarRight'], (tabCtx) => {
156
- if (tabCtx === undefined || tabCtx.slots === undefined || tabCtx.sidebarRightTabs === undefined) return;
157
- tabCtx.effect(() => tabCtx.slots.inject('sidebar.right.pane.tab', () => tabCtx.slots.register({
158
- name: 'sidebar.right.pane.tab',
159
- key: NS,
160
- locale: NS,
161
- }, Panel)));
162
- tabCtx.effect(() => tabCtx.sidebarRightTabs.register({
163
- id: NS,
164
- kind: KIND,
165
- priority: 'extension',
166
- title: () => 'Plugin doctor',
167
- }));
199
+ // Failures inside this callback happen after apply() returned, so the
200
+ // guards above do not cover them — this try/catch is the one that
201
+ // decides whether the row lands in "failed".
202
+ try {
203
+ if (tabCtx === undefined || tabCtx.slots === undefined || tabCtx.sidebarRightTabs === undefined) {
204
+ report('no-sidebar', `tabCtx slots=${String(tabCtx !== undefined && tabCtx.slots !== undefined)} sidebarRightTabs=${String(tabCtx !== undefined && tabCtx.sidebarRightTabs !== undefined)}`);
205
+ return;
206
+ }
207
+ tabCtx.effect(() => tabCtx.slots.inject('sidebar.right.pane.tab', () => tabCtx.slots.register({
208
+ name: 'sidebar.right.pane.tab',
209
+ key: NS,
210
+ locale: NS,
211
+ }, Panel)));
212
+ tabCtx.effect(() => tabCtx.sidebarRightTabs.register({
213
+ id: NS,
214
+ kind: KIND,
215
+ priority: 'extension',
216
+ title: () => 'Plugin doctor',
217
+ }));
218
+ report('tab-registered', `kind=${KIND}`);
219
+ } catch (error) {
220
+ report('tab-failed', error);
221
+ }
168
222
  });
169
- } catch {
223
+ } catch (error) {
170
224
  // No sidebar: the plugin still loads, it just has no place to show.
225
+ report('inject', error);
171
226
  }
172
227
  }
173
228
 
174
229
  // The services the tab body reads. Declaring them lets the loader wait for
175
230
  // them instead of calling apply() before they exist.
176
- module.exports = { inject: ['slots', 'locale'], apply, name: 'dsh-plugin-toolkit' };
231
+ module.exports = { inject: ['slots', 'locale'], apply };
177
232
  return module.exports;
178
233
  },
179
234
  });
package/index.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * the command that fixes it. Nothing here writes to the workspace.
8
8
  */
9
9
  import fs from 'node:fs';
10
+ import os from 'node:os';
10
11
  import path from 'node:path';
11
12
 
12
13
  import { doctor } from './lib/doctor.mjs';
@@ -78,7 +79,56 @@ export async function checkPlugin(dir) {
78
79
  return { ok: checks.every((entry) => entry.ok === true), dir: target, checks };
79
80
  }
80
81
 
82
+ /**
83
+ * The client half cannot see the host console, and a panel that fails to
84
+ * activate shows up as one red dot — no message, nowhere to look. This route is
85
+ * the plugin's own crash reporter: the bundle posts what happened, the host
86
+ * appends it to `~/.dsh/toolkit-client-report.ndjson`.
87
+ */
88
+ function appendReport(entry) {
89
+ try {
90
+ const file = path.join(os.homedir(), '.dsh', 'toolkit-client-report.ndjson');
91
+ fs.mkdirSync(path.dirname(file), { recursive: true });
92
+ fs.appendFileSync(file, `${JSON.stringify(entry)}\n`);
93
+ } catch {
94
+ // A report that fails to write must not become a second failure.
95
+ }
96
+ }
97
+
98
+ function clip(value, limit) {
99
+ if (typeof value !== 'string') return null;
100
+ return value.length > limit ? `${value.slice(0, limit)}…` : value;
101
+ }
102
+
103
+ async function readBody(req) {
104
+ const chunks = [];
105
+ let size = 0;
106
+ for await (const chunk of req) {
107
+ size += chunk.length;
108
+ if (size > 8192) return null;
109
+ chunks.push(chunk);
110
+ }
111
+ try {
112
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
81
118
  export function apply(ctx) {
119
+ const reportHandler = async (req, res) => {
120
+ if (req.method !== 'POST') return sendJson(res, 405, { error: 'method-not-allowed' });
121
+ const body = await readBody(req);
122
+ if (body === null || typeof body !== 'object') return sendJson(res, 400, { error: 'bad-report' });
123
+ appendReport({
124
+ at: new Date().toISOString(),
125
+ stage: clip(body.stage, 60),
126
+ message: clip(body.message, 500),
127
+ services: Array.isArray(body.services) ? body.services.slice(0, 20).map((name) => clip(String(name), 60)) : [],
128
+ });
129
+ return sendJson(res, 200, { ok: true });
130
+ };
131
+
82
132
  const checkHandler = async (req, res) => {
83
133
  if (req.method !== 'GET') return sendJson(res, 405, { error: 'method-not-allowed' });
84
134
  if (sameOrigin(req) === false) return sendJson(res, 403, { error: 'cross-origin' });
@@ -96,6 +146,7 @@ export function apply(ctx) {
96
146
  const server = serviceCtx !== null && serviceCtx !== undefined ? serviceCtx.webServer : undefined;
97
147
  if (server === null || server === undefined || typeof server.register !== 'function') return;
98
148
  server.register({ kind: 'exact', path: '/toolkit/check', handler: checkHandler });
149
+ server.register({ kind: 'exact', path: '/toolkit/report', handler: reportHandler });
99
150
  installed = true;
100
151
  };
101
152
 
@@ -65,7 +65,10 @@ export async function mountClient(options) {
65
65
  'react/jsx-dev-runtime': jsxDevRuntime,
66
66
  };
67
67
 
68
- const host = createHost({ locale: copy, fetch: fetchImpl, surface });
68
+ // A test that needs a host which fails on purpose passes its own: the default
69
+ // host never throws, and "the service threw" is the failure mode that costs
70
+ // the most to debug in the real app.
71
+ const host = options.host ?? createHost({ locale: copy, fetch: fetchImpl, surface });
69
72
  // The panel calls the browser's fetch, not a service, so the stub has to be
70
73
  // the global one for the duration of the mount.
71
74
  const previousFetch = globalThis.fetch;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-toolkit",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Preflight, release and test DeepSeek Harness plugins: checks what the host needs, waits for npm to actually serve the version, and keeps the market index honest.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "client.js",
20
20
  "cordis.patch.yml",
21
21
  "README.md",
22
+ "README.zh.md",
22
23
  "LICENSE",
23
24
  "CHANGELOG.md"
24
25
  ],