dsh-plugin-pack 1.0.15 → 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.
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,8 @@ 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
+ import { repairProfile } from './repair.js';
20
21
 
21
22
  export const PACK_FORMAT = 'dsh-plugin-pack@2';
22
23
  /** 兼容旧的散文件布局。 */
@@ -285,7 +286,34 @@ export function importBundle(options = {}) {
285
286
  for (const plugin of chosen) {
286
287
  const dest = path.join(localPluginsDir, plugin.segment);
287
288
  const nodeLink = path.join(profile, 'node_modules', ...String(plugin.name).split('/'));
288
- 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
+ }
289
317
  const linkState = describeLink(nodeLink, dest);
290
318
  // 路径插件:只在 cordis.patch.yml 里用绝对路径 insert、没进 bundles 的(如 pwsh7-preference)。
291
319
  // 导入后 name 重写成新位置的绝对文件路径,不进 bundles / dependencies / 链接。
@@ -302,6 +330,11 @@ export function importBundle(options = {}) {
302
330
  bytes: plugin.bytes,
303
331
  dest,
304
332
  exists,
333
+ installedDir,
334
+ installedVersion,
335
+ installedSpec,
336
+ installSource,
337
+ older,
305
338
  action: exists ? decisionFor(plugin.name) : 'create',
306
339
  nodeLink,
307
340
  linkState,
@@ -324,18 +357,20 @@ export function importBundle(options = {}) {
324
357
  * dryRun 也返回它 —— 这正是界面判断"要不要弹窗"的依据。 */
325
358
  const conflicts = plan
326
359
  .filter((item) => item.exists)
327
- .map((item) => {
328
- const installed = readJson(path.join(item.dest, 'package.json'), null);
329
- return {
330
- name: item.name,
331
- version: item.version,
332
- installedVersion: installed?.version ?? null,
333
- dest: item.dest,
334
- bytes: item.bytes,
335
- // 同一个版本还是不同版本,界面可以据此给更明确的提示
336
- sameVersion: installed?.version === item.version,
337
- };
338
- });
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
+ }));
339
374
 
340
375
  if (dryRun) {
341
376
  return {
@@ -395,7 +430,12 @@ export function importBundle(options = {}) {
395
430
  continue;
396
431
  }
397
432
  onProgress(`解包 ${item.name}`, basePercent);
398
- 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) {
399
439
  const backupDir = path.join(backupRoot, 'local-plugins', item.segment);
400
440
  copyTree(item.dest, backupDir);
401
441
  record.replacedDirs.push({ dest: item.dest, backup: backupDir });
@@ -419,7 +459,7 @@ export function importBundle(options = {}) {
419
459
  }
420
460
  }
421
461
 
422
- if (!item.exists) record.addedDirs.push(item.dest);
462
+ if (!localDestExists) record.addedDirs.push(item.dest);
423
463
  applied.push({ name: item.name, action: item.action, files: written, dest: item.dest });
424
464
  }
425
465
 
@@ -668,6 +708,39 @@ function ensurePackageLink(profile, name, dest, { backupRoot, record }) {
668
708
  }
669
709
  }
670
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
+
671
744
  /** 备份目录撞名(同一秒内多次导入)时追加序号,不覆盖旧备份。 */
672
745
  function uniqueBackupDir(base) {
673
746
  if (!isDir(base) && !isFile(base)) return base;
@@ -836,6 +909,18 @@ export function rollbackImport(options = {}) {
836
909
  // (早先这行写在 return 之后,成功路径根本执行不到)
837
910
  clearScanCache();
838
911
 
912
+ /* 回滚后对账:恢复回来的 package.json 可能声明了某个插件,
913
+ * 而它的 node_modules 链接/目录已经被更早的回滚删了。
914
+ * 不修的话 dsh 启动会报
915
+ * cannot resolve profile bundle "xxx" ... run 'dsh plugin install'
916
+ * 而点「卸载」又会触发 pnpm(可能 store 版本不匹配直接失败)。 */
917
+ let repair = null;
918
+ try {
919
+ repair = repairProfile({ profileDir: profile });
920
+ } catch (error) {
921
+ repair = { ok: false, error: String(error?.message ?? error) };
922
+ }
923
+
839
924
  /* 标记该备份已消费:重命名加 .done,之后「最新备份」指向更早的、还没回滚过的那次。
840
925
  * 注意:pre-rollback 快照在备份目录里,重命名后它跟着一起搬到 .done 里,
841
926
  * 仍然可以取出来反悔。 */
@@ -859,6 +944,8 @@ export function rollbackImport(options = {}) {
859
944
  // 回滚前把当时的配置另存了一份,路径给出来方便反悔
860
945
  preRollbackDir: preRollbackFiles.length ? preRollbackDir : null,
861
946
  preRollbackFiles,
947
+ // 回滚后自动对账结果(补了哪些链接、清了哪些僵尸声明)
948
+ repair,
862
949
  restartRequired: true,
863
950
  };
864
951
  }
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.17",
4
4
  "description": "DeepSeek Harness 插件整合包:一键把当前 profile 的第三方插件连依赖和配置导出成 zip(内部每个插件是标准 npm tgz),换机/换 profile 拖进去就能还原。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",