dsh-m 0.2.1 → 0.2.2

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/docs/DESIGN.md CHANGED
@@ -66,7 +66,7 @@
66
66
  - **安装(npm 源)**:装最新版并以**精确版本锁定**(不用 `^` 范围;用户指定版本必须为精确 semver,经该精确版本 endpoint 查询)。安装前对 profile 的 `package.json` / `pnpm-lock.yaml` / `pnpm-workspace.yaml` 做字节快照;安装后核验 importer 依赖为该精确版本,并在 lockfile `packages` 条目中比对与 npm dist 一致的 `resolution.integrity`——缺失或不一致 **fail closed** 并执行 **best-effort dependency rollback**(原子恢复快照 + `pnpm install --frozen-lockfile`;恢复失败同时报告两类错误并提示人工修复)。不声称 node_modules 与间接依赖已字节级回滚。
67
67
  - **安装(GitHub 源)**:解析并**锁定 commit SHA**(`github:owner/repo#sha`),skillhub 同款。
68
68
  - **已装识别**:读 profile `package.json` dependencies,与 registry 匹配 → 标注「市场安装」;不匹配的也列出,标注「非市场安装 / 来源未知」。卸载/升级对两类都可用。
69
- - **卸载**:live-disable(先让 client bundle 下线,避免 404)→ `dsh plugin remove`。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
69
+ - **卸载**:live-disable(先让 client bundle 下线,避免 404)→ 摘除该包在 profile 的补丁条目(`pnpm-workspace.yaml` 顶层 `patchedDependencies` 与 `package.json#pnpm.patchedDependencies`;依赖移除后残留条目会令 pnpm 以 `ERR_PNPM_UNUSED_PATCH` 整单失败,只精确匹配 `pkg` / `pkg@ver`,补丁文件本体保留并计入残留报告)→ `dsh plugin remove`。**不清理插件产生的数据/配置**,但把检测到的疑似残留路径(如 `~/.dsh/<plugin>.json`)列出报告。
70
70
  - **升级**:**按需检查**(`dshm_outdated` / `dshm_list` 时实时比对本地版本 vs npm latest / GitHub main),半自动——展示升级计划,确认后执行。**不做后台定时器**。
71
71
  - **自更新**:dsh-m 对自己同样做版本比对 + 提示升级(设置页呈现)。
72
72
  - **重启**:内置**一键重启**,复用 skillhub 验证过的重启路径(本机 `dsh-web.service` 是转发 shim,不新建 systemd 单元、不监听 3080)。安装/卸载/升级完成后 GUI 弹「需重启生效 [一键重启]」横幅,工具返回重启提示。
package/lib/client.js CHANGED
@@ -514,7 +514,7 @@ var CSS = `
514
514
  .dshm-detail{margin-top:8px;border-top:1px dashed var(--dsw-alias-border-l2,#e5e7eb);padding-top:8px;display:flex;flex-direction:column;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary,#4b5563)}
515
515
  .dshm-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:4px}
516
516
  .dshm-banner{display:flex;align-items:center;gap:10px;padding:10px 14px;border-top:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-state-warn-tertiary,#fffbeb);color:var(--dsw-alias-state-warn-primary,#b45309);font-size:12px}
517
- .dshm-banner .dshm-banner-text{flex:1}
517
+ .dshm-banner .dshm-banner-text{flex:1;max-height:140px;overflow:auto;overscroll-behavior:contain;white-space:pre-wrap;word-break:break-word;line-height:18px}
518
518
  .dshm-row{display:flex;align-items:center;gap:8px}
519
519
  .dshm-kv{display:grid;grid-template-columns:96px 1fr;gap:6px 10px;font-size:12px;align-items:baseline}
520
520
  .dshm-kv .k{color:var(--dsw-alias-label-caption,#6b7280);font-size:11px}
@@ -10,6 +10,119 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
10
10
  import { dirname, isAbsolute, join, resolve } from 'node:path';
11
11
  import { installTimeoutMs, WEB_PROFILE, webProfileDir } from './env.js';
12
12
  import { createProgressTracker } from './progress.js';
13
+ /** pnpm patchedDependencies 条目键与目标包匹配:`pkg` 或 `pkg@任意版本/区间`。 */
14
+ function matchesPatchedKey(key, pkg) {
15
+ return key === pkg || key.startsWith(`${pkg}@`);
16
+ }
17
+ /**
18
+ * 摘除 profile 里目标包的 pnpm 补丁条目:pnpm-workspace.yaml 顶层
19
+ * `patchedDependencies` 与 package.json 的 `pnpm.patchedDependencies` 两处。
20
+ * 卸载场景下依赖被移除后,残留补丁条目会让 pnpm 以 ERR_PNPM_UNUSED_PATCH
21
+ * 拒绝整个 remove/install。只精确匹配目标包的键(`pkg` / `pkg@ver`),
22
+ * 其他包的补丁不动;补丁文件本体保留在磁盘(删包不删数据,DESIGN.md §3)。
23
+ */
24
+ export function removePatchedDependencyEntries(profileDirectory, pkg, deps = {}) {
25
+ const target = String(pkg || '').trim();
26
+ if (!target || !isSafePluginTarget(target))
27
+ return { changed: false, orphanedPatchFiles: [] };
28
+ const exists = deps.existsSync ?? existsSync;
29
+ const read = deps.readFileSync ?? readFileSync;
30
+ const write = deps.writeFileSync ?? writeFileSync;
31
+ const orphanedPatchFiles = [];
32
+ let changed = false;
33
+ // --- pnpm-workspace.yaml(pnpm≥10 补丁配置落点;行级手术,只摘匹配键) ---
34
+ const wsFile = join(profileDirectory, 'pnpm-workspace.yaml');
35
+ let ws = '';
36
+ try {
37
+ ws = read(wsFile, 'utf8');
38
+ }
39
+ catch {
40
+ /* 无文件则跳过 */
41
+ }
42
+ if (ws) {
43
+ const srcLines = ws.split('\n');
44
+ let start = -1;
45
+ for (let i = 0; i < srcLines.length; i++) {
46
+ if (/^patchedDependencies:\s*$/.test(srcLines[i])) {
47
+ start = i;
48
+ break;
49
+ }
50
+ }
51
+ if (start >= 0) {
52
+ // 块边界:下一个顶层键(无缩进行)或文件尾
53
+ let end = srcLines.length;
54
+ for (let i = start + 1; i < srcLines.length; i++) {
55
+ if (/^\S/.test(srcLines[i])) {
56
+ end = i;
57
+ break;
58
+ }
59
+ }
60
+ const keptBlock = [];
61
+ for (const line of srcLines.slice(start + 1, end)) {
62
+ // 条目行:缩进键 + `:` + 补丁文件路径(键可带引号)
63
+ const m = /^\s+(["']?)([^"':]+?)\1\s*:\s*(.+?)\s*$/.exec(line);
64
+ if (m && matchesPatchedKey(m[2].trim(), target)) {
65
+ changed = true;
66
+ const file = resolve(profileDirectory, m[3].trim());
67
+ if (exists(file))
68
+ orphanedPatchFiles.push(file);
69
+ }
70
+ else {
71
+ keptBlock.push(line);
72
+ }
73
+ }
74
+ if (changed) {
75
+ // 块内条目被摘空 → 连 `patchedDependencies:` 头一起移除,避免留下空映射
76
+ const stillHasEntry = keptBlock.some((l) => /^\s+\S/.test(l));
77
+ const next = stillHasEntry
78
+ ? [...srcLines.slice(0, start + 1), ...keptBlock, ...srcLines.slice(end)]
79
+ : [...srcLines.slice(0, start), ...srcLines.slice(end)];
80
+ write(wsFile, next.join('\n'));
81
+ }
82
+ }
83
+ }
84
+ // --- package.json#pnpm.patchedDependencies(pnpm<10 落点,兼容清理) ---
85
+ const pkgJsonFile = join(profileDirectory, 'package.json');
86
+ let raw = '';
87
+ try {
88
+ raw = read(pkgJsonFile, 'utf8');
89
+ }
90
+ catch {
91
+ /* 无文件则跳过 */
92
+ }
93
+ if (raw) {
94
+ try {
95
+ const doc = JSON.parse(raw);
96
+ const patched = doc?.pnpm?.patchedDependencies;
97
+ if (patched && typeof patched === 'object') {
98
+ let touched = false;
99
+ for (const [key, file] of Object.entries(patched)) {
100
+ if (!matchesPatchedKey(key, target))
101
+ continue;
102
+ delete patched[key];
103
+ touched = true;
104
+ changed = true;
105
+ if (typeof file === 'string') {
106
+ const abs = resolve(profileDirectory, file);
107
+ if (exists(abs))
108
+ orphanedPatchFiles.push(abs);
109
+ }
110
+ }
111
+ if (touched) {
112
+ if (Object.keys(patched).length === 0)
113
+ delete doc.pnpm.patchedDependencies;
114
+ if (Object.keys(doc.pnpm).length === 0)
115
+ delete doc.pnpm;
116
+ write(pkgJsonFile, `${JSON.stringify(doc, null, 2)}\n`);
117
+ }
118
+ }
119
+ }
120
+ catch {
121
+ /* package.json 不是合法 JSON:不动 */
122
+ }
123
+ }
124
+ return { changed, orphanedPatchFiles };
125
+ }
13
126
  const TARGET_RE = /^[A-Za-z0-9@:./_#+-]+$/;
14
127
  const NDJSON_COMMANDS = new Set(['add', 'remove', 'install']);
15
128
  export const BOOT_ID = `${String(process.pid)}-${String(Date.now())}`;
@@ -163,6 +276,9 @@ function writeDangerouslyAllowAllBuilds(profileDirectory) {
163
276
  }
164
277
  export function rewritePnpmError(err) {
165
278
  const text = err instanceof Error ? err.message : String(err);
279
+ if (/ERR_PNPM_UNUSED_PATCH/.test(text)) {
280
+ return new Error('profile 的补丁配置(patchedDependencies)里存在不再使用的条目,pnpm 拒绝执行。卸载时 dsh-m 会自动摘除目标包自己的补丁条目;仍报此错通常是其他包留有失效补丁,请手工清理 profile 的 pnpm-workspace.yaml。');
281
+ }
166
282
  if (isPrepareBlocked(text)) {
167
283
  return new Error('该插件需要执行构建脚本(prepare),pnpm 默认拦截。dsh-m 已写入 profile 的 dangerouslyAllowAllBuilds 并重试;若仍失败请检查 web profile 是否可写。');
168
284
  }
@@ -7,7 +7,7 @@
7
7
  import { existsSync } from 'node:fs';
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { join } from 'node:path';
10
- import { addDshPlugin, removeDshPlugin, runCommand } from './dsh-cli.js';
10
+ import { addDshPlugin, removeDshPlugin, removePatchedDependencyEntries, runCommand } from './dsh-cli.js';
11
11
  import { dshHome, installTimeoutMs, webProfileDir } from './env.js';
12
12
  import { assertNpmIntegrity, readPnpmLockIntegrity, restoreSnapshots, snapshotFiles } from './npm-integrity.js';
13
13
  import { listInstalledPlugins as defaultListInstalledPlugins, readProfileDeps, removeInstalledPlugin, } from './installed.js';
@@ -528,11 +528,14 @@ export async function installEntry(entry, cfg = {}, opts = {}, deps) {
528
528
  }
529
529
  throw new Error(`条目 ${entry.id} 缺少可安装来源`);
530
530
  }
531
- /** 卸载:live-disable → pnpm remove → 报告疑似残留(DESIGN.md §3:删包不删数据)。 */
532
- export async function uninstallPlugin(pkg, _cfg = {}, _opts = {}) {
531
+ /** 卸载:live-disable → 摘除该包补丁条目 → pnpm remove → 报告疑似残留(DESIGN.md §3:删包不删数据)。 */
532
+ export async function uninstallPlugin(pkg, _cfg = {}, _opts = {}, deps = {}) {
533
533
  const liveDisabled = await setLivePluginDisabled(pkg, true);
534
- await removeInstalledPlugin(pkg);
535
- return { pkg, liveDisabled, needsRestart: true, leftovers: leftoverCandidates(pkg) };
534
+ // 依赖移除后残留的 patchedDependencies 条目会让 pnpm 以 ERR_PNPM_UNUSED_PATCH 整单失败,先摘掉
535
+ const patchCleanup = (deps.removePatchedEntries ?? removePatchedDependencyEntries)(webProfileDir(), pkg);
536
+ await (deps.removeInstalled ?? removeInstalledPlugin)(pkg);
537
+ const leftovers = [...new Set([...leftoverCandidates(pkg), ...patchCleanup.orphanedPatchFiles])];
538
+ return { pkg, liveDisabled, needsRestart: true, leftovers };
536
539
  }
537
540
  /** 升级 = 按最新重新安装(npm 拉最新精确版;github 重新锁 HEAD)。 */
538
541
  export async function upgradePlugin(pkg, cfg = {}, opts = {}, deps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-m",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "DSH Marketplace — 个人自用的 DeepSeek Harness 插件市场:收录、安装、卸载、升级 DSH 插件",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/registry.json CHANGED
@@ -7,9 +7,10 @@
7
7
  "description": "DeepSeek Harness Web 界面主题/皮肤管理插件,可切换多种视觉风格。",
8
8
  "category": "ui",
9
9
  "tags": ["主题", "美化"],
10
- "source": "github",
11
- "github": "iasiv5/skins",
12
- "homepage": "https://github.com/iasiv5/skins"
10
+ "source": "npm",
11
+ "npm": "@iasiv5/dsh-skins",
12
+ "github": "iasiv5/dsh-skins",
13
+ "homepage": "https://github.com/iasiv5/dsh-skins"
13
14
  },
14
15
  {
15
16
  "id": "modsearch",