@1e0zj/dsh-plugin-mall 0.3.2 → 0.3.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **An open plugin marketplace for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (dsh): search every GitHub repo tagged `topic:dsh-plugin`, automatically verify which ones are real dsh plugins, install and update with one click.**
4
4
 
5
- [中文说明](#中文说明) · [Install](#install) · [Why another marketplace](#why-another-marketplace)
5
+ [中文说明](#中文说明) · [Install](#install) · [Why another marketplace](#why-another-marketplace) · [Roadmap](ROADMAP.md)
6
6
 
7
7
  Two surfaces: a **Settings → Plugins → Marketplace** tab in the dsh web UI, and five agent tools usable from any session.
8
8
 
@@ -73,9 +73,11 @@ node <profile>/node_modules/@1e0zj/dsh-plugin-mall/src/cli.js guard launch --pro
73
73
 
74
74
  **A plain `dsh web` already resolves pending installs.** The marketplace plugin runs
75
75
  recovery when it loads: reaching that point proves dsh booted far enough to compose
76
- the profile, so the pending marker is committed (or rolled back when the profile
77
- fails validation). Without this a single install would wedge the profile every
78
- later install and uninstall refuses while a marker is outstanding.
76
+ the profile, so the pending marker is committed or rolled back, either because the
77
+ profile fails validation or because the install was left paused at the build-script
78
+ approval gate (an unapproved install is never committed, however healthy it looks).
79
+ Without this a single install would wedge the profile — every later install and
80
+ uninstall refuses while a marker is outstanding.
79
81
 
80
82
  **`guard launch` is still strictly better**, because it also covers what a plain
81
83
  start cannot: a plugin that boots fine and then crashes seconds later. It checks the
@@ -83,13 +85,42 @@ profile's pending-install marker before starting the command after `--`:
83
85
 
84
86
  - **No pending install** — the command runs as-is, inheriting the terminal, and its exit code is preserved.
85
87
  - **Clearly broken on disk** — the profile is rolled back to its pre-install snapshot *before* launch, then the command starts on the restored state.
86
- - **Alive through the grace period** (default **10 seconds**; `--grace-ms <ms>` to change) — the pending snapshot is committed and the wrapper keeps waiting on the process.
88
+ - **Alive through the grace period** (default **10 seconds**; `--grace-ms <ms>` to change) — the pending snapshot is committed and the wrapper keeps waiting on the process. An install still paused at the approval gate is rolled back instead: surviving probation only proves the JS loads, not that you approved its build scripts.
87
89
  - **Exits 0 inside the grace period** (one-shot command) — the pending snapshot is committed.
88
90
  - **Crashes or exits nonzero inside the grace period** — the profile is rolled back and the *exact same command* is restarted once with the restored state (never in a loop); the restarted process's exit code is preserved. SIGINT/SIGTERM are forwarded to the child where the platform supports it; on Windows `.cmd`/`.bat` shims go through `%ComSpec%` with strict per-argument quoting.
89
91
 
90
92
  Limitations: the grace window is the probation period — a failure that only surfaces **after** it (a plugin that crashes minutes in, or on a specific interaction) cannot be rolled back automatically, because committing deletes the active snapshot and `guard recover` then has nothing to restore. `guard validate` still diagnoses the on-disk state, but a post-commit failure needs manual repair — uninstall and reinstall the plugin, or restore a backup you kept separately. Both commands do only **static on-disk validation**; neither proves the plugin actually loads. A corrupt pending marker fails closed: the command is not launched and no unvalidated path is deleted. Preserve the snapshot and repair or restore a trustworthy marker, then run `guard recover`; quarantine the marker only after you have independently verified the profile, or decided to abandon automatic recovery.
91
93
 
92
94
 
95
+ ## dsh won't start after an update? (affects 0.2.0 – 0.3.2, fixed in 0.3.3)
96
+
97
+ Symptom: `dsh web` exits with `cannot resolve profile bundle "<package>"`.
98
+
99
+ Cause: **updating an already-installed plugin** into a rollback (most often the
100
+ target carries build scripts and the flow paused at the approval card) could lose
101
+ the package — the rollback's reinstall of the old version was fooled by pnpm's
102
+ "Already up to date" short-circuit, so the plugin left node_modules while its
103
+ bundles declaration stayed. Fresh installs and removals are unaffected.
104
+
105
+ Recovery: add the package back exactly as the profile's `package.json` declares
106
+ it, then run `dsh web`.
107
+
108
+ ```bash
109
+ # <profile> = %USERPROFILE%\.dsh\profiles\web or ~/.dsh/profiles/web
110
+ # npm package — package.json says "dsh-better-sidebar": "^0.13.1"
111
+ pnpm --dir <profile> add "dsh-better-sidebar@^0.13.1" --ignore-scripts
112
+ # GitHub source — package.json says "dsh-at-file": "github:omdsh-dev/dsh-at-file"
113
+ pnpm --dir <profile> add "github:omdsh-dev/dsh-at-file" --ignore-scripts
114
+ ```
115
+
116
+ > Do not reach for 0.3.2's `dsh-plugin-guard recover` here. Its per-package
117
+ > fallback refuses `^` (a cmd metacharacter) and skips `github:` entirely, and
118
+ > pnpm writes nearly every dependency with one or the other — so the fallback
119
+ > never fires and recovery fails closed. Fixed on main: the fallback now pins the
120
+ > version (or commit) the lockfile resolved. Fixed in 0.3.3 — on that version
121
+ > `dsh-plugin-guard recover` does repair this.
122
+
123
+
93
124
  ## Agent tools
94
125
 
95
126
  | Tool | What it does |
@@ -106,6 +137,8 @@ Limitations: the grace window is the probation period — a failure that only su
106
137
 
107
138
  **dsh 插件市场** — 搜索 GitHub `dsh-plugin` 话题下的 DeepSeek Harness 插件仓库,自动验证哪些是真 dsh 插件,一键安装与更新。
108
139
 
140
+ [路线图](ROADMAP.md)(安全 > 便捷 > 精简,以及明确不做的)
141
+
109
142
  与策展列表不同:**任何打上 `topic:dsh-plugin` 的仓库推送后立即可被发现**——无需投稿、无需审批。为保证开放性可用,做了这些事:
110
143
 
111
144
  - **自动验证**:逐仓库拉取 `package.json`(jsDelivr/raw 双源 CDN,不占 API 配额),按官方 `dsh.bundle` / `dsh.client` 声明打徽章;默认"只看已验证"视图过滤约 73% 的话题噪音
@@ -191,22 +224,48 @@ node <profile>/node_modules/@1e0zj/dsh-plugin-mall/src/cli.js guard launch --pro
191
224
  ```
192
225
 
193
226
  **普通的 `dsh web` 就会了结 pending 安装。** 本插件加载时即执行恢复——能加载
194
- 本身就证明 dsh 已经组装好 profile、启动到了这一步,于是提交 pending 标记
195
- profile 校验不过则回滚)。没有这一步的话,装完一个插件就会把 profile 卡住:
196
- 只要标记还在,之后所有安装和卸载都会被拒绝。
227
+ 本身就证明 dsh 已经组装好 profile、启动到了这一步,于是提交 pending 标记;两种
228
+ 情况改为回滚:profile 校验不过,或者那次安装停在构建脚本批准闸而未获批准
229
+ (没批准的安装绝不提交,哪怕它看起来一切正常)。没有这一步的话,装完一个插件
230
+ 就会把 profile 卡住:只要标记还在,之后所有安装和卸载都会被拒绝。
197
231
 
198
232
  **`guard launch` 仍然更强**,因为它覆盖普通启动覆盖不了的情况:插件启动正常、
199
233
  几秒后才崩。它在启动 `--` 之后的命令前检查该 profile 的 pending 安装标记:
200
234
 
201
235
  - **无 pending 安装** —— 命令原样运行(继承终端),透传退出码;
202
236
  - **静态校验明显过不了** —— 启动*之前*先把 profile 回滚到安装前快照,再在恢复后的状态上启动;
203
- - **活过缓刑期**(默认 **10 秒**,`--grace-ms <ms>` 可调)—— 提交 pending 快照,包装器继续守候该进程;
237
+ - **活过缓刑期**(默认 **10 秒**,`--grace-ms <ms>` 可调)—— 提交 pending 快照,包装器继续守候该进程;但仍停在批准闸的安装改为回滚:活过缓刑期只证明 JS 能加载,不证明你批准了它的构建脚本;
204
238
  - **缓刑期内以 0 退出**(一次性命令)—— 同样提交 pending 快照;
205
239
  - **缓刑期内崩溃或非零退出** —— 回滚 profile,并用恢复后的状态**原样重启同一命令一次**(绝不循环),透传重启进程的退出码。支持的平台会把 SIGINT/SIGTERM 转发给子进程;Windows 上 `.cmd`/`.bat` 经 `%ComSpec%` 启动,逐参数严格加引号。
206
240
 
207
241
  限制:缓刑期就是观察期——**之后**才暴露的故障(跑了几分钟才崩、或某个特定操作才触发)无法自动回滚:提交会删掉当前快照,此时 `guard recover` 已无可恢复的东西。`guard validate` 仍能诊断落盘状态,但提交之后的故障只能手工修复——卸载并重装插件(或恢复你另行保留的备份)。两条命令都只做**静态落盘校验**,都不证明插件真的能加载。pending 标记损坏时关闭式失败:不启动命令、不删除任何未校验路径。要**保留快照**、修复或恢复一个可信的标记后再跑 `guard recover`;只有在你已经独立核实过 profile、或决定放弃自动恢复之后,才去隔离(删除/移走)标记。
208
242
 
209
243
 
244
+ ## 升级后 dsh 起不来?(0.2.0 – 0.3.2 受影响,0.3.3 已修复)
245
+
246
+ 症状:`dsh web` 报 `cannot resolve profile bundle "<包名>"` 直接退出。
247
+
248
+ 原因:**更新已装插件**时若走到回滚(最常见:目标插件带构建脚本、停在批准卡),
249
+ 回滚里「装回旧版本」的一步会被 pnpm 的 "Already up to date" 空转骗过——包从
250
+ node_modules 消失而 bundles 声明还在。新装、卸载不受影响。
251
+
252
+ 恢复:照 profile `package.json` 里原本的写法把包装回去,然后 `dsh web`。
253
+
254
+ ```bash
255
+ # <profile> = %USERPROFILE%\.dsh\profiles\web 或 ~/.dsh/profiles/web
256
+ # npm 包 —— package.json 里是 "dsh-better-sidebar": "^0.13.1"
257
+ pnpm --dir <profile> add "dsh-better-sidebar@^0.13.1" --ignore-scripts
258
+ # GitHub 源 —— package.json 里是 "dsh-at-file": "github:omdsh-dev/dsh-at-file"
259
+ pnpm --dir <profile> add "github:omdsh-dev/dsh-at-file" --ignore-scripts
260
+ ```
261
+
262
+ > 别指望 0.3.2 的 `dsh-plugin-guard recover` 修这个:它的 per-package 兜底会
263
+ > 拒掉 `^`(cmd 转义符),`github:` 更是整个跳过,而 pnpm 存依赖几乎不是前者
264
+ > 就是后者——兜底一次也不会触发,恢复只会 fail-closed。main 上已修:兜底改钉
265
+ > lockfile 解析出的版本(或 commit)。0.3.3 已修复——那个版本的
266
+ > `dsh-plugin-guard recover` 确实能修这个故障。
267
+
268
+
210
269
  ## 工作原理
211
270
 
212
271
  - 双面包(dual-face)插件:`dsh.bundle` 半边挂在 **host 平面**(profile bundle 层),
@@ -265,10 +324,14 @@ node <profile>/node_modules/@1e0zj/dsh-plugin-mall/src/cli.js guard launch --pro
265
324
  三处。`allowBuilds` 是持久化的安全配置,所以安装最终失败时这次放宽会被
266
325
  **撤销** —— 否则一个没装成的插件会让那个包名从此静默获得构建脚本执行权。
267
326
  - 配置(`cordis.patch.yml` 中可改):`defaultProfile`(默认装进哪个 profile,
268
- 默认 `web`)、`apiBase`(GitHub API 地址)、`npmRegistry`(npm 查询源,留空
269
- 则跟随 pnpm 实际安装源)、`rawSources`(验证用的 package.json 源模板列表,
270
- `{repo}` 会替换成 owner/name,留空用内置的 jsDelivr + raw 双源)、
271
- `perPageMax`(搜索单页上限)、`allowRestart`(是否允许一键重启,默认 `true`)。
327
+ **不配则跟随本进程实际启动的那个** —— loader 的配置树锚点 `ctx.baseUrl`
328
+ 推导,那正是 profile 目录;推不出来才退回 `web` 并打日志)、`apiBase`
329
+ (GitHub API 地址)、`npmRegistry`(npm 查询源,留空则跟随 pnpm 实际安装
330
+ 源)、`rawSources`(验证用的 package.json 源模板列表,`{repo}` 会替换成
331
+ owner/name,留空用内置的 jsDelivr + raw 双源)、`perPageMax`(搜索单页上限,
332
+ 1–30 的整数,默认 30)、`allowRestart`(是否允许一键重启,默认 `true`)。
333
+ 自动识别同时决定**启动恢复作用在哪个 profile 上**:能在此提交半装状态,
334
+ 凭据是「本次启动成功了」,而那只能证明启动的那个 profile 是好的。
272
335
 
273
336
  ## 发布
274
337
 
package/cordis.patch.yml CHANGED
@@ -5,8 +5,19 @@
5
5
  - id: dsh-plugin-mall
6
6
  name: '@1e0zj/dsh-plugin-mall'
7
7
  config:
8
- # The profile market_install targets when the caller names none.
9
- defaultProfile: web
8
+ # defaultProfile is deliberately NOT set here.
9
+ #
10
+ # This file ships inside the package and is read as a bundle layer by
11
+ # every profile that installs the marketplace, so anything written here
12
+ # is an explicit config value in ALL of them. Pinning `defaultProfile:
13
+ # web` (as this file used to) meant a marketplace running in any other
14
+ # profile still defaulted its installs at web — and the value looked
15
+ # like the user's own choice, so nothing could tell it apart from one.
16
+ #
17
+ # Left unset, the plugin follows the profile this process actually
18
+ # booted (derived from the loader's config-tree anchor). Set it only to
19
+ # override that:
20
+ # defaultProfile: web
10
21
  # GitHub REST API base (override for self-hosted mirrors/proxies).
11
22
  apiBase: https://api.github.com
12
23
  # npm registry for anti-squatting, update checks and the host-dependency
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1e0zj/dsh-plugin-mall",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "dsh 插件市场:搜索 GitHub dsh-plugin 话题下的插件仓库,一键安装到本地 dsh profile(agent 工具 + 设置页插件市场 tab)",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/cli.js CHANGED
@@ -40,13 +40,18 @@ import { tmpdir } from "node:os";
40
40
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
41
41
  import { fileURLToPath } from "node:url";
42
42
  import {
43
+ clearPendingApprovalPause,
43
44
  commitPendingSnapshot,
44
45
  createProfileSnapshot,
46
+ describeRollbackRebuild,
45
47
  listPendingSnapshots,
48
+ markPendingApprovalPause,
46
49
  markPendingSnapshot,
50
+ pendingApprovalPaused,
47
51
  pnpmGuardEnv,
48
52
  preflightInstall,
49
53
  readPendingSnapshot,
54
+ readValidatedPendingSnapshot,
50
55
  recoverAll,
51
56
  recoverProfile,
52
57
  resolveDshHome,
@@ -380,6 +385,8 @@ function cmdRecover({ home, profileDir }) {
380
385
  } else if (entry.action === "rolled-back") {
381
386
  console.log(`ROLLED BACK ${scope}: ${(entry.issues ?? []).map((issueEntry) => issueEntry.title).join("; ") || "profile would not load"}`);
382
387
  if (entry.removed?.length) console.log(` removed from node_modules: ${entry.removed.join(", ")}`);
388
+ const rebuild = describeRollbackRebuild(entry.rebuild);
389
+ if (rebuild !== undefined) console.log(` node_modules rebuild: ${rebuild}`);
383
390
  } else if (entry.action === "none") {
384
391
  console.log(`no pending ${scope}`);
385
392
  } else {
@@ -693,9 +700,25 @@ async function runPlain(command, args) {
693
700
  * Commit the pending snapshot once startup probation passes. A commit failure
694
701
  * is a warning, not a launch failure — the process is already running and
695
702
  * healthy, and the marker simply stays pending for the next launch.
703
+ *
704
+ * An approval-paused marker must never commit: the new version sits there with
705
+ * its build scripts never approved, so staying alive only proves the JS loads.
706
+ * Roll it back to the pre-install snapshot instead (a rollback failure keeps
707
+ * the marker for the next attempt, same as recoverProfile).
696
708
  */
697
709
  function commitLaunchSnapshot(profileDir) {
698
710
  try {
711
+ let pending;
712
+ try {
713
+ pending = readValidatedPendingSnapshot(profileDir);
714
+ } catch {
715
+ pending = undefined; // unreadable marker: leave it to guard recover
716
+ }
717
+ if (pending !== undefined && pendingApprovalPaused(pending) !== undefined) {
718
+ rollbackPendingSnapshot(profileDir);
719
+ console.log(`[guard] startup probation passed, but the install was abandoned at the approval gate — profile rolled back for ${profileDir}`);
720
+ return;
721
+ }
699
722
  commitPendingSnapshot(profileDir);
700
723
  console.log(`[guard] startup probation passed — pending snapshot committed for ${profileDir}`);
701
724
  } catch (error) {
@@ -1156,6 +1179,31 @@ async function selfTest() {
1156
1179
  const validated = validateInstalledProfile(profileDir);
1157
1180
  if (validated.ok !== true) throw new Error("healthy profile should validate clean (cli)");
1158
1181
 
1182
+ // commitLaunchSnapshot: a marker paused at the approval gate must roll back
1183
+ // instead of committing even after a healthy probation — the candidate sits
1184
+ // there with its build scripts never approved, and committing would delete
1185
+ // the only rollback snapshot. Layout keeps the candidate a NEW dependency
1186
+ // so the rollback prunes node_modules without spawning pnpm.
1187
+ {
1188
+ const pauseProfile = join(root, "pause-home", "profiles", "web");
1189
+ mkdirSync(join(pauseProfile, "node_modules"), { recursive: true });
1190
+ writeFileSync(join(pauseProfile, "package.json"), JSON.stringify({ dependencies: {} }));
1191
+ writeFileSync(join(pauseProfile, "cordis.patch.yml"), "[]\n");
1192
+ const snap = createProfileSnapshot(pauseProfile, { fixture: true });
1193
+ markPendingSnapshot(snap, { spec: "good@2.0.0", preflight: { candidate: { name: "good", version: "2.0.0", kind: "bundle" } } });
1194
+ // 暂停现场:候选已装、声明已写,静态校验过得去——正是不许提交的原因。
1195
+ writeFileSync(join(pauseProfile, "package.json"), JSON.stringify({ dependencies: { good: "^2.0.0" } }));
1196
+ mkdirSync(join(pauseProfile, "node_modules", "good"), { recursive: true });
1197
+ writeFileSync(join(pauseProfile, "node_modules", "good", "package.json"), JSON.stringify({ name: "good", version: "2.0.0" }));
1198
+ markPendingApprovalPause(pauseProfile);
1199
+ commitLaunchSnapshot(pauseProfile);
1200
+ if (readPendingSnapshot(pauseProfile) !== undefined) throw new Error("commitLaunchSnapshot must consume (roll back) an approval-paused marker");
1201
+ if (existsSync(snap.dir)) throw new Error("the paused rollback must delete the snapshot dir");
1202
+ if (JSON.parse(readFileSync(join(pauseProfile, "package.json"), "utf8")).dependencies?.good !== undefined) {
1203
+ throw new Error("the paused rollback must restore the pre-install manifest");
1204
+ }
1205
+ }
1206
+
1159
1207
  // guarded remove: exact official argv + shell:false runner seam, snapshot
1160
1208
  // before mutation, immediate commit after a statically safe removal.
1161
1209
  {
package/src/client.js CHANGED
@@ -188,6 +188,7 @@ window.__ModuleLoader__.load({
188
188
  status: snapshot.status,
189
189
  detail: snapshot.detail,
190
190
  needsApproval: snapshot.needsApproval,
191
+ staleOnRestart: snapshot.staleOnRestart,
191
192
  approvalToken: snapshot.approvalToken,
192
193
  kind: snapshot.kind,
193
194
  output: output,
@@ -249,6 +250,7 @@ window.__ModuleLoader__.load({
249
250
  spec: snap.spec,
250
251
  detail: snap.detail,
251
252
  needsApproval: snap.needsApproval,
253
+ staleOnRestart: snap.staleOnRestart,
252
254
  approvalToken: snap.approvalToken,
253
255
  kind: snap.kind,
254
256
  output: entry.output || "",
@@ -259,9 +261,13 @@ window.__ModuleLoader__.load({
259
261
  // 不在本次服务器列表里的条目属于上一次宿主会话(进程重启后
260
262
  // tracker 清空)。已兑现的直接翻篇撤掉:completed 的重启已经
261
263
  // 发生;needsApproval 暂停的批准卡片已随进程失效(事务由启动
262
- // 恢复处置),留着只会让人点一个必然失败的按钮。running 的标
263
- // 中断,别让轮询对着不存在的 id 空转。failed 保留——日志还有
264
- // 排障价值。这个判据不依赖 finishedAt(旧镜像里没有该字段)。
264
+ // 恢复处置),留着只会让人点一个必然失败的按钮;staleOnRestart
265
+ // 的失败是「被另一个未了结事务挡住」,而那个事务必然已被启动恢复
266
+ // 处置——它的报错是现在时写的(「还没做完」「现在无法安装」),
267
+ // 留到重启之后会被当成当前状态读,而它描述的情形已经不存在。
268
+ // running 的标中断,别让轮询对着不存在的 id 空转。其余 failed
269
+ // 保留——网络、预检阻断这类原因重启后可能仍然成立,日志有排障
270
+ // 价值。这个判据不依赖 finishedAt(旧镜像里没有该字段)。
265
271
  for (var key in next) {
266
272
  if (serverIds[key]) continue;
267
273
  var stale = next[key];
@@ -270,7 +276,9 @@ window.__ModuleLoader__.load({
270
276
  status: "killed",
271
277
  detail: "宿主进程已重启,该任务的记录随之丢失",
272
278
  });
273
- } else if (stale.status === "completed" || (Array.isArray(stale.needsApproval) && stale.needsApproval.length > 0)) {
279
+ } else if (stale.status === "completed"
280
+ || stale.staleOnRestart === true
281
+ || (Array.isArray(stale.needsApproval) && stale.needsApproval.length > 0)) {
274
282
  delete next[key];
275
283
  }
276
284
  }