dsh-plugin-pack 1.0.18 → 1.0.19

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
@@ -18,6 +18,7 @@ import { readJson, readText, writeJson, writeText, ensureDir, isDir, isFile, rea
18
18
  import { existingIds, rowId, appendInsertBlock, splitTopLevel, yamlScalar } from './patch-text.js';
19
19
  import { isAbsoluteSpec, clearScanCache, resolvePackageDir } from './scan.js';
20
20
  import { repairProfile } from './repair.js';
21
+ import { syncLockfile } from './lockfile.js';
21
22
 
22
23
  export const PACK_FORMAT = 'dsh-plugin-pack@2';
23
24
  /** 兼容旧的散文件布局。 */
@@ -543,6 +544,25 @@ export function importBundle(options = {}) {
543
544
 
544
545
  record.links = links;
545
546
  writeJson(path.join(backupRoot, 'record.json'), record);
547
+
548
+ /* 最后一步:让 pnpm-lock.yaml 跟上 package.json。
549
+ *
550
+ * 我们改了 dependencies,但锁文件不会自己更新。dsh 安装/卸载插件时会跑
551
+ * `pnpm install --frozen-lockfile`,两边对不上就抛
552
+ * ERR_PNPM_OUTDATED_LOCKFILE —— 用户看到的就是"卸载点了没反应"。
553
+ * 这一步只更新锁文件,不碰 node_modules;失败不影响导入本身。 */
554
+ let lockfile = { ok: false, skipped: true, reason: '没有修改依赖' };
555
+ if (Object.keys(record.pkgDepsBefore ?? {}).length) {
556
+ onProgress('同步 pnpm 锁文件', 98);
557
+ lockfile = syncLockfile(profile);
558
+ if (!lockfile.ok && !lockfile.skipped) {
559
+ warnings.push(
560
+ `锁文件同步失败:${lockfile.error ?? '未知原因'}。` +
561
+ `这会让 dsh 的插件卸载/安装报 ERR_PNPM_OUTDATED_LOCKFILE,` +
562
+ `可在 profile 目录手动跑 pnpm install --lockfile-only 修好。`,
563
+ );
564
+ }
565
+ }
546
566
  // 导入改了 profile 配置和插件目录,扫描缓存必须失效
547
567
  clearScanCache();
548
568
  onProgress(`完成:${applied.length} 个插件已处理`, 100);
@@ -564,6 +584,8 @@ export function importBundle(options = {}) {
564
584
  disabledNames: enable ? [] : applied.filter((item) => item.action !== 'skipped').map((item) => item.name),
565
585
  patch: patchPlan.summary,
566
586
  dataRestored: record.dataDirs.length,
587
+ // 锁文件同步结果:ok=false 且没 skipped 时要提醒手动跑 pnpm install
588
+ lockfile,
567
589
  warnings,
568
590
  restartRequired: true,
569
591
  };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * 同步 pnpm 锁文件。
3
+ *
4
+ * 为什么必须有这一步:我们改的是 `package.json` 的 dependencies,
5
+ * 但 `pnpm-lock.yaml` 不会自己更新。而 dsh 的插件卸载/安装会跑
6
+ *
7
+ * pnpm install --frozen-lockfile
8
+ *
9
+ * 锁文件一旦和 package.json 对不上,pnpm 直接抛
10
+ *
11
+ * ERR_PNPM_OUTDATED_LOCKFILE
12
+ * specifiers in the lockfile don't match specifiers in package.json
13
+ *
14
+ * 表现就是:卸载按钮点了没反应、装新插件也失败。
15
+ *
16
+ * 所以导入/删除依赖之后,必须补一次 `pnpm install --lockfile-only`:
17
+ * 只更新锁文件,不碰 node_modules,几秒就完。
18
+ *
19
+ * @module dsh-plugin-pack/lib/lockfile
20
+ */
21
+ import { spawnSync } from 'node:child_process';
22
+ import fs from 'node:fs';
23
+ import path from 'node:path';
24
+
25
+ /** 找 pnpm 可执行文件;找不到返回 null。 */
26
+ function findPnpm() {
27
+ const candidates =
28
+ process.platform === 'win32'
29
+ ? ['pnpm.cmd', 'pnpm.exe', 'pnpm']
30
+ : ['pnpm'];
31
+
32
+ for (const name of candidates) {
33
+ const result = spawnSync(name, ['--version'], {
34
+ encoding: 'utf8',
35
+ shell: process.platform === 'win32',
36
+ timeout: 15000,
37
+ windowsHide: true,
38
+ });
39
+ if (!result.error && result.status === 0) return name;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * 让 `pnpm-lock.yaml` 与 `package.json` 对齐。
46
+ *
47
+ * 没有 package.json / 没有 pnpm / 没有 lockfile 时都不算错,直接跳过 ——
48
+ * 这一步是"顺手修好别再踩坑",不该阻断导入本身。
49
+ *
50
+ * @param {string} profileDir
51
+ * @param {object} [options]
52
+ * @param {number} [options.timeoutMs]
53
+ * @param {boolean} [options.force] 即使没有 lockfile 也生成一份
54
+ * @returns {{ ok: boolean, skipped?: boolean, reason?: string, error?: string }}
55
+ */
56
+ export function syncLockfile(profileDir, options = {}) {
57
+ const profile = path.resolve(profileDir);
58
+ const pkgFile = path.join(profile, 'package.json');
59
+ const lockFile = path.join(profile, 'pnpm-lock.yaml');
60
+
61
+ try {
62
+ if (!fs.existsSync(pkgFile)) return { ok: false, skipped: true, reason: '没有 package.json' };
63
+ // 本来就没有锁文件就别硬造一个(可能是 npm/yarn 项目)
64
+ if (!fs.existsSync(lockFile) && !options.force) {
65
+ return { ok: false, skipped: true, reason: '没有 pnpm-lock.yaml,跳过' };
66
+ }
67
+
68
+ const pnpm = findPnpm();
69
+ if (!pnpm) return { ok: false, skipped: true, reason: '没找到 pnpm' };
70
+
71
+ const result = spawnSync(pnpm, ['install', '--lockfile-only', '--reporter=silent'], {
72
+ cwd: profile,
73
+ encoding: 'utf8',
74
+ shell: process.platform === 'win32',
75
+ timeout: options.timeoutMs ?? 120000,
76
+ windowsHide: true,
77
+ env: { ...process.env, CI: '' },
78
+ });
79
+
80
+ if (result.error) {
81
+ return { ok: false, error: String(result.error.message ?? result.error) };
82
+ }
83
+ if (result.status !== 0) {
84
+ const detail = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
85
+ return { ok: false, error: detail.slice(0, 400) || `pnpm 退出码 ${result.status}` };
86
+ }
87
+ return { ok: true };
88
+ } catch (error) {
89
+ return { ok: false, error: String(error?.message ?? error) };
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 检查锁文件与 package.json 是否一致(不写盘)。
95
+ *
96
+ * @param {string} profileDir
97
+ * @returns {{ ok: boolean, skipped?: boolean, outdated?: boolean, error?: string }}
98
+ */
99
+ export function checkLockfile(profileDir) {
100
+ const profile = path.resolve(profileDir);
101
+ const pkgFile = path.join(profile, 'package.json');
102
+ const lockFile = path.join(profile, 'pnpm-lock.yaml');
103
+
104
+ try {
105
+ if (!fs.existsSync(pkgFile) || !fs.existsSync(lockFile)) {
106
+ return { ok: false, skipped: true };
107
+ }
108
+ const pnpm = findPnpm();
109
+ if (!pnpm) return { ok: false, skipped: true, reason: '没找到 pnpm' };
110
+
111
+ const result = spawnSync(pnpm, ['install', '--frozen-lockfile', '--lockfile-only'], {
112
+ cwd: profile,
113
+ encoding: 'utf8',
114
+ shell: process.platform === 'win32',
115
+ timeout: 120000,
116
+ windowsHide: true,
117
+ });
118
+ // 注意:这里不能用 --reporter=silent —— 它会把 ERR_PNPM_OUTDATED_LOCKFILE
119
+ // 这段错误文本一起吞掉,导致分不清"锁文件不一致"和"其它失败"。
120
+ const detail = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
121
+ if (/ERR_PNPM_OUTDATED_LOCKFILE/.test(detail)) return { ok: true, outdated: true };
122
+ if (result.status === 0) return { ok: true, outdated: false };
123
+ return { ok: false, error: detail.trim().slice(0, 400) || `pnpm 退出码 ${result.status}` };
124
+ } catch (error) {
125
+ return { ok: false, error: String(error?.message ?? error) };
126
+ }
127
+ }
package/lib/repair.js CHANGED
@@ -21,6 +21,7 @@ import path from 'node:path';
21
21
 
22
22
  import { readJson, writeJson, ensureDir, isDir } from './util.js';
23
23
  import { clearScanCache } from './scan.js';
24
+ import { syncLockfile } from './lockfile.js';
24
25
 
25
26
  /** 这个包是不是宿主自带(从 dsh 安装目录解析)。 */
26
27
  function isOfficial(name) {
@@ -142,5 +143,12 @@ export function repairProfile({ profileDir, dryRun = false }) {
142
143
  clearScanCache();
143
144
  }
144
145
 
145
- return { ok: true, linked, removed, unlinked: unlinked, warnings };
146
+ /* 删掉依赖之后锁文件也要跟上,否则 dsh 卸载/安装插件时会因为
147
+ * ERR_PNPM_OUTDATED_LOCKFILE 失败。 */
148
+ let lockfile = null;
149
+ if (!dryRun && removed.length) {
150
+ lockfile = syncLockfile(profile);
151
+ }
152
+
153
+ return { ok: true, linked, removed, unlinked: unlinked, warnings, lockfile };
146
154
  }
package/lib/server.js CHANGED
@@ -41,6 +41,7 @@ import { runJob, listJobs, getJob, runningJobs, clearFinished, openWaiting, getW
41
41
  import { fetchBundleFrom, parseSource } from './fetch-bundle.js';
42
42
  import { setPluginsEnabled, getEnabledState } from './enable.js';
43
43
  import { repairProfile } from './repair.js';
44
+ import { syncLockfile, checkLockfile } from './lockfile.js';
44
45
  import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, humanBytes } from './home.js';
45
46
  import { readJson, writeJson, isDir, isFile } from './util.js';
46
47
 
@@ -767,6 +768,43 @@ export function mountRoutes(ctx, config = {}) {
767
768
  'plugin-pack: repair',
768
769
  );
769
770
 
771
+ /* pnpm 锁文件:dsh 卸载/安装插件时会用 --frozen-lockfile 校验,
772
+ * 对不上就报 ERR_PNPM_OUTDATED_LOCKFILE(表现是"卸载点了没反应")。
773
+ * GET /plugin-pack/lockfile 查是否一致
774
+ * POST /plugin-pack/lockfile/sync 同步(= pnpm install --lockfile-only)
775
+ */
776
+ register(
777
+ {
778
+ kind: 'exact',
779
+ path: `${PREFIX}/lockfile`,
780
+ handler: async (request, response) => {
781
+ if (request.method !== 'GET') {
782
+ sendJson(response, 405, { ok: false, error: '只支持 GET' });
783
+ return;
784
+ }
785
+ const local = contextOf(config);
786
+ sendJson(response, 200, checkLockfile(local.profileDir));
787
+ },
788
+ },
789
+ 'plugin-pack: lockfile',
790
+ );
791
+
792
+ register(
793
+ {
794
+ kind: 'exact',
795
+ path: `${PREFIX}/lockfile/sync`,
796
+ handler: async (request, response) => {
797
+ if (request.method !== 'POST') {
798
+ sendJson(response, 405, { ok: false, error: '只支持 POST' });
799
+ return;
800
+ }
801
+ const local = contextOf(config);
802
+ sendJson(response, 200, syncLockfile(local.profileDir));
803
+ },
804
+ },
805
+ 'plugin-pack: lockfile-sync',
806
+ );
807
+
770
808
  /* 待决策任务:导入遇到重名时登记,界面切走再回来靠它捡回弹窗。
771
809
  * GET /plugin-pack/waiting 列出待决策任务(含冲突列表)
772
810
  * 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.18",
3
+ "version": "1.0.19",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",