dsh-plugin-pack 1.0.16 → 1.0.17

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.
Files changed (3) hide show
  1. package/client.js +56 -0
  2. package/lib/import.js +88 -16
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -190,6 +190,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
190
190
  'conflict.titleNew': '确认导入',
191
191
  'conflict.subtitleAll': '个插件待安装',
192
192
  'conflict.create': '新建',
193
+ 'conflict.older': '包内更旧 · 覆盖会降级',
194
+ 'conflict.installedFrom': '已装来源',
193
195
  'conflict.subtitle': '个插件与已安装的重名,请选择如何处理:',
194
196
  'conflict.overwrite': '覆盖',
195
197
  'conflict.skip': '跳过',
@@ -389,6 +391,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
389
391
  'conflict.titleNew': 'Confirm import',
390
392
  'conflict.subtitleAll': 'plugins to install',
391
393
  'conflict.create': 'New',
394
+ 'conflict.older': 'Older in bundle · overwrite downgrades',
395
+ 'conflict.installedFrom': 'installed via',
392
396
  'conflict.subtitle': 'plugins already exist. Choose what to do:',
393
397
  'conflict.overwrite': 'Overwrite',
394
398
  'conflict.skip': 'Skip',
@@ -1365,6 +1369,27 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1365
1369
  * @param {() => void} props.onCancel
1366
1370
  * @param {(msg: string) => string} props.t
1367
1371
  */
1372
+ /** 比较两个版本号:a>b → 正数。只认三段数字,够判断"包里是否更旧"。 */
1373
+ function compareVersions(a, b) {
1374
+ const parse = (text) =>
1375
+ String(text ?? '')
1376
+ .trim()
1377
+ .replace(/^v/i, '')
1378
+ .split('-')[0]
1379
+ .split('.')
1380
+ .map((part) => {
1381
+ const n = Number.parseInt(part.replace(/[^0-9].*$/, ''), 10);
1382
+ return Number.isFinite(n) ? n : 0;
1383
+ });
1384
+ const left = parse(a);
1385
+ const right = parse(b);
1386
+ for (let i = 0; i < 3; i += 1) {
1387
+ const diff = (left[i] ?? 0) - (right[i] ?? 0);
1388
+ if (diff !== 0) return diff;
1389
+ }
1390
+ return 0;
1391
+ }
1392
+
1368
1393
  /**
1369
1394
  * 「添加整合包」弹窗。
1370
1395
  *
@@ -1706,6 +1731,31 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
1706
1731
  item.exists && item.sameVersion
1707
1732
  ? h('span', { className: 'pp-ver' }, t('conflict.sameVersion'))
1708
1733
  : null,
1734
+ // 包内版本更旧:覆盖等于降级,必须显眼
1735
+ item.exists && item.older
1736
+ ? h(
1737
+ 'span',
1738
+ {
1739
+ className: 'pp-ver',
1740
+ style: {
1741
+ color: 'var(--dsw-alias-state-warning-primary,#b45309)',
1742
+ borderColor: 'color-mix(in srgb,var(--dsw-alias-state-warning-primary,#d97706) 45%,transparent)',
1743
+ },
1744
+ },
1745
+ t('conflict.older'),
1746
+ )
1747
+ : null,
1748
+ // 已装来源不是 local-plugins 时说明一下(覆盖会改掉原来的安装方式)
1749
+ item.installSource && item.installSource !== 'local-plugins'
1750
+ ? h(
1751
+ 'span',
1752
+ { className: 'pp-ver' },
1753
+ `${t('conflict.installedFrom')} ${
1754
+ { npm: 'npm', link: 'link:', remote: 'git/url', unknown: '?' }[item.installSource] ??
1755
+ item.installSource
1756
+ }`,
1757
+ )
1758
+ : null,
1709
1759
  ),
1710
1760
  item.dest ? h('div', { className: 'pp-meta pp-mono', style: { fontSize: 11 } }, item.dest) : null,
1711
1761
  ),
@@ -2216,6 +2266,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2216
2266
  dest: hit?.dir ?? null,
2217
2267
  bytes: plugin.bytes ?? null,
2218
2268
  sameVersion: (hit?.version ?? null) === (plugin.version ?? null),
2269
+ // 包内版本比已装的更旧 → 覆盖等于降级,要显式标出来
2270
+ older: Boolean(hit?.version && plugin.version && compareVersions(hit.version, plugin.version) > 0),
2219
2271
  exists: Boolean(hit),
2220
2272
  };
2221
2273
  });
@@ -2252,6 +2304,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2252
2304
  installedVersion: hit?.installedVersion ?? null,
2253
2305
  dest: hit?.dest ?? null,
2254
2306
  sameVersion: Boolean(hit?.sameVersion),
2307
+ older: Boolean(hit?.older),
2308
+ installSource: hit?.installSource ?? null,
2255
2309
  exists: Boolean(hit),
2256
2310
  };
2257
2311
  });
@@ -2371,6 +2425,8 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
2371
2425
  installedVersion: hit?.installedVersion ?? null,
2372
2426
  dest: hit?.dest ?? null,
2373
2427
  sameVersion: Boolean(hit?.sameVersion),
2428
+ older: Boolean(hit?.older),
2429
+ installSource: hit?.installSource ?? null,
2374
2430
  exists: Boolean(hit),
2375
2431
  };
2376
2432
  });
package/lib/import.js CHANGED
@@ -16,7 +16,7 @@ import { readTgz } from './tar.js';
16
16
  import { dshHome as resolveDshHome, profileDir as resolveProfileDir, profileName as resolveProfileName, packSegment, stamp, humanBytes } from './home.js';
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
- import { isAbsoluteSpec, clearScanCache } from './scan.js';
19
+ import { isAbsoluteSpec, clearScanCache, resolvePackageDir } from './scan.js';
20
20
  import { repairProfile } from './repair.js';
21
21
 
22
22
  export const PACK_FORMAT = 'dsh-plugin-pack@2';
@@ -286,7 +286,34 @@ export function importBundle(options = {}) {
286
286
  for (const plugin of chosen) {
287
287
  const dest = path.join(localPluginsDir, plugin.segment);
288
288
  const nodeLink = path.join(profile, 'node_modules', ...String(plugin.name).split('/'));
289
- const exists = isDir(dest);
289
+ /* 判断"是否已装"不能只看 local-plugins 目录 ——
290
+ * 插件可能是 npm / github / 绝对路径 link 装的,文件在 node_modules 里。
291
+ * 只看本地目录会把它当成"新建",于是重复装入、还把原来的
292
+ * dependencies 声明覆盖成 link:,用户原来的版本控制就丢了。
293
+ * 所以用真实的模块解析结果来判定。 */
294
+ const resolvedDir = resolvePackageDir(plugin.name, profile, dependencies);
295
+ const localExists = isDir(dest);
296
+ const exists = localExists || Boolean(resolvedDir);
297
+ const installedDir = localExists ? dest : resolvedDir;
298
+ const installedSpec = dependencies[plugin.name] ?? null;
299
+ const installedMeta = installedDir ? readJson(path.join(installedDir, 'package.json'), null) : null;
300
+ const installedVersion = installedMeta?.version ?? null;
301
+ const installSource = localExists
302
+ ? 'local-plugins'
303
+ : !installedSpec
304
+ ? 'unknown'
305
+ : /^link:/i.test(installedSpec)
306
+ ? 'link'
307
+ : /^(github:|git\+|https?:)/i.test(installedSpec)
308
+ ? 'remote'
309
+ : 'npm';
310
+ // 包里的版本比已装的更旧时提醒一句,覆盖等于降级
311
+ const older = exists && installedVersion && plugin.version ? compareVersions(installedVersion, plugin.version) > 0 : false;
312
+ if (older) {
313
+ warnings.push(
314
+ `${plugin.name}: 整合包里是旧版本(已装 ${installedVersion},包内 ${plugin.version}),覆盖会把它降级`,
315
+ );
316
+ }
290
317
  const linkState = describeLink(nodeLink, dest);
291
318
  // 路径插件:只在 cordis.patch.yml 里用绝对路径 insert、没进 bundles 的(如 pwsh7-preference)。
292
319
  // 导入后 name 重写成新位置的绝对文件路径,不进 bundles / dependencies / 链接。
@@ -303,6 +330,11 @@ export function importBundle(options = {}) {
303
330
  bytes: plugin.bytes,
304
331
  dest,
305
332
  exists,
333
+ installedDir,
334
+ installedVersion,
335
+ installedSpec,
336
+ installSource,
337
+ older,
306
338
  action: exists ? decisionFor(plugin.name) : 'create',
307
339
  nodeLink,
308
340
  linkState,
@@ -325,18 +357,20 @@ export function importBundle(options = {}) {
325
357
  * dryRun 也返回它 —— 这正是界面判断"要不要弹窗"的依据。 */
326
358
  const conflicts = plan
327
359
  .filter((item) => item.exists)
328
- .map((item) => {
329
- const installed = readJson(path.join(item.dest, 'package.json'), null);
330
- return {
331
- name: item.name,
332
- version: item.version,
333
- installedVersion: installed?.version ?? null,
334
- dest: item.dest,
335
- bytes: item.bytes,
336
- // 同一个版本还是不同版本,界面可以据此给更明确的提示
337
- sameVersion: installed?.version === item.version,
338
- };
339
- });
360
+ .map((item) => ({
361
+ name: item.name,
362
+ version: item.version,
363
+ installedVersion: item.installedVersion,
364
+ // 已装位置不一定是 local-plugins:npm/link/github 装的要走不同的提示
365
+ dest: item.installedDir,
366
+ bytes: item.bytes,
367
+ // 同一个版本还是不同版本,界面可以据此给更明确的提示
368
+ sameVersion: item.installedVersion === item.version,
369
+ // 包内版本更旧 → 覆盖等于降级,界面要显式警告
370
+ older: Boolean(item.older),
371
+ installSource: item.installSource,
372
+ installedSpec: item.installedSpec,
373
+ }));
340
374
 
341
375
  if (dryRun) {
342
376
  return {
@@ -396,7 +430,12 @@ export function importBundle(options = {}) {
396
430
  continue;
397
431
  }
398
432
  onProgress(`解包 ${item.name}`, basePercent);
399
- if (item.exists) {
433
+ /* 只有 dest 真的存在(=本地插件目录)才需要"搬走备份"。
434
+ * 已装但装在 node_modules 的(npm/link/github),local-plugins 下没有这个目录,
435
+ * 直接往空目录里写就行 —— 早先按 item.exists 判断会去 copy 一个不存在的路径,
436
+ * 报 ENOENT: no such file or directory, stat '...\local-plugins\xxx'。 */
437
+ const localDestExists = isDir(item.dest);
438
+ if (localDestExists) {
400
439
  const backupDir = path.join(backupRoot, 'local-plugins', item.segment);
401
440
  copyTree(item.dest, backupDir);
402
441
  record.replacedDirs.push({ dest: item.dest, backup: backupDir });
@@ -420,7 +459,7 @@ export function importBundle(options = {}) {
420
459
  }
421
460
  }
422
461
 
423
- if (!item.exists) record.addedDirs.push(item.dest);
462
+ if (!localDestExists) record.addedDirs.push(item.dest);
424
463
  applied.push({ name: item.name, action: item.action, files: written, dest: item.dest });
425
464
  }
426
465
 
@@ -669,6 +708,39 @@ function ensurePackageLink(profile, name, dest, { backupRoot, record }) {
669
708
  }
670
709
  }
671
710
 
711
+ /**
712
+ * 比较两个 semver 版本号,返回 a>b → 正数,a<b → 负数,相等 → 0。
713
+ *
714
+ * 只做够用的三段数字比较:够判断"整合包里的版本是不是更旧"。
715
+ * 预发布后缀(-beta.1)按字符串兜底比较。
716
+ *
717
+ * @param {string} a
718
+ * @param {string} b
719
+ * @returns {number}
720
+ */
721
+ export function compareVersions(a, b) {
722
+ const parse = (text) => {
723
+ const [core, pre = ''] = String(text ?? '').trim().replace(/^v/i, '').split('-');
724
+ const nums = core.split('.').map((part) => {
725
+ const n = Number.parseInt(part.replace(/[^0-9].*$/, ''), 10);
726
+ return Number.isFinite(n) ? n : 0;
727
+ });
728
+ while (nums.length < 3) nums.push(0);
729
+ return { nums, pre };
730
+ };
731
+
732
+ const left = parse(a);
733
+ const right = parse(b);
734
+ for (let i = 0; i < 3; i += 1) {
735
+ if (left.nums[i] !== right.nums[i]) return left.nums[i] - right.nums[i];
736
+ }
737
+ // 主版本相同:有预发布后缀的视为更小
738
+ if (left.pre === right.pre) return 0;
739
+ if (!left.pre) return 1;
740
+ if (!right.pre) return -1;
741
+ return left.pre < right.pre ? -1 : 1;
742
+ }
743
+
672
744
  /** 备份目录撞名(同一秒内多次导入)时追加序号,不覆盖旧备份。 */
673
745
  function uniqueBackupDir(base) {
674
746
  if (!isDir(base) && !isFile(base)) return base;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-pack",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",