dsh-plugin-pack 1.0.15 → 1.0.16

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/lib/import.js CHANGED
@@ -17,6 +17,7 @@ import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName
17
17
  import { readJson, readText, writeJson, writeText, ensureDir, isDir, isFile, realOrSelf, copyTree, removeTree } from './util.js';
18
18
  import { existingIds, rowId, appendInsertBlock, splitTopLevel, yamlScalar } from './patch-text.js';
19
19
  import { isAbsoluteSpec, clearScanCache } from './scan.js';
20
+ import { repairProfile } from './repair.js';
20
21
 
21
22
  export const PACK_FORMAT = 'dsh-plugin-pack@2';
22
23
  /** 兼容旧的散文件布局。 */
@@ -836,6 +837,18 @@ export function rollbackImport(options = {}) {
836
837
  // (早先这行写在 return 之后,成功路径根本执行不到)
837
838
  clearScanCache();
838
839
 
840
+ /* 回滚后对账:恢复回来的 package.json 可能声明了某个插件,
841
+ * 而它的 node_modules 链接/目录已经被更早的回滚删了。
842
+ * 不修的话 dsh 启动会报
843
+ * cannot resolve profile bundle "xxx" ... run 'dsh plugin install'
844
+ * 而点「卸载」又会触发 pnpm(可能 store 版本不匹配直接失败)。 */
845
+ let repair = null;
846
+ try {
847
+ repair = repairProfile({ profileDir: profile });
848
+ } catch (error) {
849
+ repair = { ok: false, error: String(error?.message ?? error) };
850
+ }
851
+
839
852
  /* 标记该备份已消费:重命名加 .done,之后「最新备份」指向更早的、还没回滚过的那次。
840
853
  * 注意:pre-rollback 快照在备份目录里,重命名后它跟着一起搬到 .done 里,
841
854
  * 仍然可以取出来反悔。 */
@@ -859,6 +872,8 @@ export function rollbackImport(options = {}) {
859
872
  // 回滚前把当时的配置另存了一份,路径给出来方便反悔
860
873
  preRollbackDir: preRollbackFiles.length ? preRollbackDir : null,
861
874
  preRollbackFiles,
875
+ // 回滚后自动对账结果(补了哪些链接、清了哪些僵尸声明)
876
+ repair,
862
877
  restartRequired: true,
863
878
  };
864
879
  }
package/lib/repair.js ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * 修复 profile 里「配置说有、磁盘上没有」的插件。
3
+ *
4
+ * 为什么需要:回滚会把 package.json 恢复成某份快照,那份快照可能声明了
5
+ * 某个插件,而它的 node_modules 链接(甚至目录)已经在更早的回滚里被删掉。
6
+ * 结果 dsh 启动时报:
7
+ *
8
+ * cannot resolve profile bundle "dsh-pet" ... run 'dsh plugin install'
9
+ *
10
+ * 而点「卸载」又会触发 pnpm 安装(可能因为 store 版本不匹配直接失败)。
11
+ *
12
+ * 这里做的是把两边对齐:
13
+ * 目录在、链接缺 → 补链接(插件能正常用)
14
+ * 目录和链接都缺 → 从 dependencies / bundles 里移除(不留僵尸条目)
15
+ * 官方包(@deepseek-ai/*)→ 从 dsh 安装目录解析,永远跳过
16
+ *
17
+ * @module dsh-plugin-pack/lib/repair
18
+ */
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+
22
+ import { readJson, writeJson, ensureDir, isDir } from './util.js';
23
+ import { clearScanCache } from './scan.js';
24
+
25
+ /** 这个包是不是宿主自带(从 dsh 安装目录解析)。 */
26
+ function isOfficial(name) {
27
+ return String(name).startsWith('@deepseek-ai/');
28
+ }
29
+
30
+ /** 把 `link:./local-plugins/xxx` 里的目录段取出来;不是这种写法返回 null。 */
31
+ function localSegment(spec) {
32
+ const text = String(spec ?? '');
33
+ if (!text.startsWith('link:')) return null;
34
+ const rel = text.slice('link:'.length).replace(/\\/g, '/');
35
+ const match = /(?:^|\/)local-plugins\/([^/]+)\/?$/.exec(rel);
36
+ return match ? match[1] : null;
37
+ }
38
+
39
+ /** node_modules 里这个包名的路径(scoped 要分两级)。 */
40
+ function linkPathOf(profileDir, name) {
41
+ return path.join(profileDir, 'node_modules', ...String(name).split('/'));
42
+ }
43
+
44
+ /** 指向目标目录建一个 junction / symlink。 */
45
+ function makeLink(linkPath, target) {
46
+ ensureDir(path.dirname(linkPath));
47
+ try {
48
+ fs.symlinkSync(target, linkPath, 'junction');
49
+ return true;
50
+ } catch {
51
+ try {
52
+ fs.symlinkSync(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 对齐 profile 的声明与实际磁盘。
62
+ *
63
+ * @param {object} options
64
+ * @param {string} options.profileDir
65
+ * @param {boolean} [options.dryRun] 只算不改
66
+ * @returns {{ ok: boolean, linked: string[], removed: string[], unlinked: string[], warnings: string[] }}
67
+ */
68
+ export function repairProfile({ profileDir, dryRun = false }) {
69
+ const profile = path.resolve(profileDir);
70
+ const pkgFile = path.join(profile, 'package.json');
71
+ const pkg = readJson(pkgFile, null);
72
+ if (!pkg || typeof pkg !== 'object') {
73
+ const error = new Error(`profile 里没有 package.json: ${pkgFile}`);
74
+ error.code = 'BAD_PROFILE';
75
+ throw error;
76
+ }
77
+
78
+ pkg.dependencies = pkg.dependencies && typeof pkg.dependencies === 'object' ? pkg.dependencies : {};
79
+ pkg.dsh = pkg.dsh && typeof pkg.dsh === 'object' ? pkg.dsh : {};
80
+ pkg.dsh.profile = pkg.dsh.profile && typeof pkg.dsh.profile === 'object' ? pkg.dsh.profile : {};
81
+ const bundles = Array.isArray(pkg.dsh.profile.bundles) ? pkg.dsh.profile.bundles : [];
82
+
83
+ const linked = [];
84
+ const removed = [];
85
+ const unlinked = [];
86
+ const warnings = [];
87
+
88
+ const dropNames = new Set();
89
+
90
+ for (const [name, spec] of Object.entries(pkg.dependencies)) {
91
+ if (isOfficial(name)) continue; // 宿主自带,不碰
92
+ const seg = localSegment(spec);
93
+ if (!seg) continue; // 只处理本工具自己写的那种 link
94
+
95
+ const dir = path.join(profile, 'local-plugins', seg);
96
+ const linkPath = linkPathOf(profile, name);
97
+ const dirOk = isDir(dir);
98
+ const linkOk = fs.existsSync(linkPath);
99
+
100
+ if (dirOk && !linkOk) {
101
+ // 目录在、链接缺 → 补上,插件恢复可用
102
+ if (!dryRun && makeLink(linkPath, dir)) linked.push(name);
103
+ else if (dryRun) linked.push(name);
104
+ else warnings.push(`补链接失败: ${name}`);
105
+ continue;
106
+ }
107
+
108
+ if (!dirOk) {
109
+ // 目录没了 → 不论链接在不在,这条声明都是僵尸
110
+ if (linkOk) {
111
+ if (!dryRun) {
112
+ try {
113
+ fs.unlinkSync(linkPath);
114
+ unlinked.push(name);
115
+ } catch {
116
+ try {
117
+ fs.rmSync(linkPath, { recursive: true, force: true });
118
+ unlinked.push(name);
119
+ } catch {
120
+ /* 删不掉就留着,至少别写进删名单 */
121
+ }
122
+ }
123
+ } else {
124
+ unlinked.push(name);
125
+ }
126
+ }
127
+ dropNames.add(name);
128
+ }
129
+ }
130
+
131
+ if (dropNames.size) {
132
+ for (const name of dropNames) {
133
+ delete pkg.dependencies[name];
134
+ removed.push(name);
135
+ }
136
+ pkg.dsh.profile.bundles = bundles.filter((name) => !dropNames.has(name));
137
+ }
138
+
139
+ if (!dryRun && (linked.length || removed.length)) {
140
+ writeJson(pkgFile, pkg);
141
+ // 配置和磁盘刚对齐,扫描缓存必须失效
142
+ clearScanCache();
143
+ }
144
+
145
+ return { ok: true, linked, removed, unlinked: unlinked, warnings };
146
+ }
package/lib/server.js CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  import { runJob, listJobs, getJob, runningJobs, clearFinished, openWaiting, getWaiting, listWaiting, cancelWaiting } from './jobs.js';
41
41
  import { fetchBundleFrom, parseSource } from './fetch-bundle.js';
42
42
  import { setPluginsEnabled, getEnabledState } from './enable.js';
43
+ import { repairProfile } from './repair.js';
43
44
  import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, humanBytes } from './home.js';
44
45
  import { readJson, writeJson, isDir, isFile } from './util.js';
45
46
 
@@ -737,6 +738,35 @@ export function mountRoutes(ctx, config = {}) {
737
738
  'plugin-pack: enable',
738
739
  );
739
740
 
741
+ /* 对账修复:把 package.json 与实际磁盘对齐。
742
+ * POST /plugin-pack/repair { dryRun }
743
+ *
744
+ * 解决"配置里有、磁盘上没有"导致 dsh 报
745
+ * cannot resolve profile bundle "xxx" 的情况:
746
+ * - 目录在、链接缺 → 补链接(插件恢复可用)
747
+ * - 目录和链接都缺 → 从 dependencies/bundles 移除(清僵尸条目)
748
+ */
749
+ register(
750
+ {
751
+ kind: 'exact',
752
+ path: `${PREFIX}/repair`,
753
+ handler: async (request, response) => {
754
+ if (request.method !== 'POST') {
755
+ sendJson(response, 405, { ok: false, error: '只支持 POST' });
756
+ return;
757
+ }
758
+ const body = await readJsonBody(request).catch(() => ({}));
759
+ const local = contextOf(config);
760
+ const result = repairProfile({
761
+ profileDir: local.profileDir,
762
+ dryRun: Boolean(body.dryRun),
763
+ });
764
+ sendJson(response, 200, result);
765
+ },
766
+ },
767
+ 'plugin-pack: repair',
768
+ );
769
+
740
770
  /* 待决策任务:导入遇到重名时登记,界面切走再回来靠它捡回弹窗。
741
771
  * GET /plugin-pack/waiting 列出待决策任务(含冲突列表)
742
772
  * POST /plugin-pack/waiting/cancel 取消(用户切走/关页面/点取消)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",