dsh-plugin-pack 1.0.19 → 1.0.21
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 +71 -11
- package/lib/fetch-bundle.js +96 -22
- package/lib/lockfile.js +71 -31
- package/lib/server.js +2 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -580,7 +580,12 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
580
580
|
api('/plugin-pack/browse', { method: 'POST', body: JSON.stringify({ dir, create: Boolean(options.create) }) });
|
|
581
581
|
const doHotload = (pkg) => api('/plugin-pack/hotload', { method: 'POST', body: JSON.stringify({ package: pkg }) });
|
|
582
582
|
const getClientStatus = (pkg) => api(`/plugin-pack/client?package=${encodeURIComponent(pkg ?? 'dsh-plugin-pack')}`);
|
|
583
|
-
|
|
583
|
+
/** 导入一个本地插件目录(dryRun=true 时只预览冲突,不写盘)。 */
|
|
584
|
+
const doImportPlugin = (dir, dryRun = false) =>
|
|
585
|
+
api('/plugin-pack/import-plugin', {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
body: JSON.stringify({ dir, dryRun, enable: installPrefs.enable }),
|
|
588
|
+
});
|
|
584
589
|
/** 后台任务列表(进行中 + 已完成)。 */
|
|
585
590
|
const getJobs = () => api('/plugin-pack/jobs?limit=20');
|
|
586
591
|
const clearJobs = () => api('/plugin-pack/jobs/clear', { method: 'POST', body: '{}' });
|
|
@@ -2395,20 +2400,55 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2395
2400
|
}
|
|
2396
2401
|
setPhase(t('import.checking'));
|
|
2397
2402
|
|
|
2398
|
-
/*
|
|
2399
|
-
* kind='plugin' → npm 包 / 单插件 tgz
|
|
2400
|
-
* kind='
|
|
2403
|
+
/* 下载到的可能是三种:
|
|
2404
|
+
* kind='plugin' + file → npm 包 / 单插件 tgz
|
|
2405
|
+
* kind='plugin' + dir → 单个插件的原仓库(GitHub 归档解出来的目录)
|
|
2406
|
+
* kind='bundle' → 整合包 zip,可能要先问冲突 */
|
|
2401
2407
|
if (fetched.kind === 'plugin') {
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
+
// 仓库来的单插件:先 dryRun 看有没有同名已装,有就弹确认框
|
|
2409
|
+
if (fetched.dir) {
|
|
2410
|
+
const preview = await doImportPlugin(fetched.dir, true);
|
|
2411
|
+
const existingRow = preview?.conflicts?.[0] ?? null;
|
|
2412
|
+
if (existingRow || preview?.plan) {
|
|
2413
|
+
const row = {
|
|
2414
|
+
name: fetched.plugins?.[0]?.name ?? preview?.name ?? '(未知插件)',
|
|
2415
|
+
version: fetched.plugins?.[0]?.version ?? preview?.version ?? null,
|
|
2416
|
+
installedVersion: existingRow?.installedVersion ?? null,
|
|
2417
|
+
dest: existingRow?.dest ?? null,
|
|
2418
|
+
sameVersion: Boolean(existingRow?.sameVersion),
|
|
2419
|
+
older: Boolean(existingRow?.older),
|
|
2420
|
+
installSource: existingRow?.installSource ?? null,
|
|
2421
|
+
exists: Boolean(existingRow),
|
|
2422
|
+
};
|
|
2423
|
+
setPendingImport({
|
|
2424
|
+
kind: 'plugin-dir',
|
|
2425
|
+
pendingDir: fetched.dir,
|
|
2426
|
+
file: null,
|
|
2427
|
+
conflicts: row.exists ? [row] : [],
|
|
2428
|
+
rows: [row],
|
|
2429
|
+
plugins: fetched.plugins ?? [],
|
|
2430
|
+
from: fetched.from,
|
|
2431
|
+
});
|
|
2432
|
+
setNetOpen(false);
|
|
2433
|
+
setBusy(false);
|
|
2434
|
+
setPhase('');
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
const result = fetched.dir
|
|
2440
|
+
? await doImportPlugin(fetched.dir)
|
|
2441
|
+
: await doImport({
|
|
2442
|
+
file: fetched.file,
|
|
2443
|
+
dryRun: false,
|
|
2444
|
+
mode: 'merge',
|
|
2445
|
+
linkNodeModules: true,
|
|
2446
|
+
});
|
|
2408
2447
|
setNetResult({
|
|
2409
2448
|
ok: true,
|
|
2410
2449
|
kind: 'plugin',
|
|
2411
|
-
file: fetched.file,
|
|
2450
|
+
file: fetched.file ?? null,
|
|
2451
|
+
dir: fetched.dir ?? null,
|
|
2412
2452
|
from: fetched.from,
|
|
2413
2453
|
name: fetched.name,
|
|
2414
2454
|
bytes: fetched.bytes,
|
|
@@ -2493,6 +2533,26 @@ window.__ModuleLoader__.load({ id: 'dsh-plugin-pack', factory: (require) => {
|
|
|
2493
2533
|
setBusy(true);
|
|
2494
2534
|
setImportResult(null);
|
|
2495
2535
|
try {
|
|
2536
|
+
/* 单插件仓库(GitHub 归档解出来的目录):直接按目录导入。
|
|
2537
|
+
* 目录导入没有 decisions 的概念(一个插件一个决定),
|
|
2538
|
+
* 用户点了「开始导入」就等于选覆盖。 */
|
|
2539
|
+
if (pending.kind === 'plugin-dir' && pending.pendingDir) {
|
|
2540
|
+
setPhase(t('import.checking'));
|
|
2541
|
+
const dirResult = await doImportPlugin(pending.pendingDir);
|
|
2542
|
+
setNetResult({
|
|
2543
|
+
ok: true,
|
|
2544
|
+
kind: 'plugin',
|
|
2545
|
+
dir: pending.pendingDir,
|
|
2546
|
+
from: pending.from ?? null,
|
|
2547
|
+
plugins: pending.plugins ?? [],
|
|
2548
|
+
});
|
|
2549
|
+
setImportResult(dirResult);
|
|
2550
|
+
rememberEnableState(dirResult);
|
|
2551
|
+
await load();
|
|
2552
|
+
await refreshJobs();
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
|
|
2496
2556
|
/* quick 模式:拖进来时只读了 zip 尾部就弹了冲突窗,文件还没上传。
|
|
2497
2557
|
* 现在用户确认了,才真正把文件传上去。 */
|
|
2498
2558
|
let file = pending.file;
|
package/lib/fetch-bundle.js
CHANGED
|
@@ -20,6 +20,7 @@ import path from 'node:path';
|
|
|
20
20
|
import { inspectBundle } from './import.js';
|
|
21
21
|
import { saveIncomingZip } from './inbox.js';
|
|
22
22
|
import { ensureDir } from './util.js';
|
|
23
|
+
import { readZip } from './zip.js';
|
|
23
24
|
|
|
24
25
|
/** 单次下载的上限(与上传一致,1 GiB)。 */
|
|
25
26
|
const MAX_BYTES = 1024 * 1024 * 1024;
|
|
@@ -422,11 +423,14 @@ export async function fetchBundleFrom({ source, home, onProgress = () => {} }) {
|
|
|
422
423
|
const saved = saveIncomingZip({ home, filename: name, data, origin: 'network' });
|
|
423
424
|
report(`已保存到 ${saved.file}`);
|
|
424
425
|
|
|
425
|
-
/*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* .
|
|
429
|
-
*
|
|
426
|
+
/* 关键:校验它到底是什么。
|
|
427
|
+
*
|
|
428
|
+
* 三种可能,靠内容判断而不是靠地址猜:
|
|
429
|
+
* .tgz → 单个插件的 npm tarball
|
|
430
|
+
* .zip 带 manifest.json → dsh-plugin-pack 整合包
|
|
431
|
+
* .zip 带 package.json → 单个插件的原仓库(GitHub 归档常见)
|
|
432
|
+
* 都不是 → 报「不是整合包也不是插件」
|
|
433
|
+
*/
|
|
430
434
|
const isTgz = /\.(tgz|tar\.gz)$/i.test(saved.file);
|
|
431
435
|
|
|
432
436
|
if (isTgz) {
|
|
@@ -462,23 +466,93 @@ export async function fetchBundleFrom({ source, home, onProgress = () => {} }) {
|
|
|
462
466
|
};
|
|
463
467
|
}
|
|
464
468
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
}
|
|
481
|
-
}
|
|
469
|
+
// .zip:先按整合包认;不是整合包再按"单插件仓库"认
|
|
470
|
+
try {
|
|
471
|
+
const info = inspectBundle(saved.file);
|
|
472
|
+
report(`校验通过:整合包内含 ${info.plugins.length} 个插件`);
|
|
473
|
+
return {
|
|
474
|
+
file: saved.file,
|
|
475
|
+
bytes: saved.bytes,
|
|
476
|
+
name,
|
|
477
|
+
from,
|
|
478
|
+
kind: 'bundle',
|
|
479
|
+
manifest: info.manifest,
|
|
480
|
+
plugins: info.plugins.map((plugin) => ({
|
|
481
|
+
name: plugin.name,
|
|
482
|
+
version: plugin.version,
|
|
483
|
+
})),
|
|
484
|
+
};
|
|
485
|
+
} catch (bundleError) {
|
|
486
|
+
/* 不是整合包 → 看看是不是某个插件的原仓库。
|
|
487
|
+
* 判断依据是解出来的目录里有合法 package.json(有 name),
|
|
488
|
+
* 而不是"地址长得像 github"。 */
|
|
489
|
+
report('不是整合包,检查是否为单个插件的原仓库…');
|
|
490
|
+
const detected = await detectPluginInZip(data, path.dirname(saved.file));
|
|
491
|
+
if (detected) {
|
|
492
|
+
report(`校验通过:单个插件 ${detected.info.name}@${detected.info.version ?? '?'}(来自仓库)`);
|
|
493
|
+
return {
|
|
494
|
+
file: saved.file,
|
|
495
|
+
bytes: saved.bytes,
|
|
496
|
+
name,
|
|
497
|
+
from,
|
|
498
|
+
kind: 'plugin',
|
|
499
|
+
dir: detected.dir,
|
|
500
|
+
manifest: null,
|
|
501
|
+
plugins: [{ name: detected.info.name, version: detected.info.version ?? null }],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
throw bundleError;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 在一个 zip 里找"单个插件"的根目录。
|
|
510
|
+
*
|
|
511
|
+
* GitHub 的仓库归档会多包一层 `<repo>-<ref>/`,所以要判断:
|
|
512
|
+
* - 根目录就有 package.json → 就是它
|
|
513
|
+
* - 只有一个顶层目录,且该目录里有 package.json → 用它
|
|
514
|
+
* 其它情况(散落多目录、没有 package.json)返回 null。
|
|
515
|
+
*
|
|
516
|
+
* @param {Buffer} buffer zip 内容
|
|
517
|
+
* @param {string} parentDir 解压到哪儿(会在其下建 extracted/)
|
|
518
|
+
* @returns {Promise<{ dir: string, info: object } | null>}
|
|
519
|
+
*/
|
|
520
|
+
async function detectPluginInZip(buffer, parentDir) {
|
|
521
|
+
try {
|
|
522
|
+
const entries = readZip(buffer);
|
|
523
|
+
if (!entries.length) return null;
|
|
524
|
+
|
|
525
|
+
// 先看有没有 package.json,以及它的层级
|
|
526
|
+
const pkgEntry = entries.find((entry) => entry.name === 'package.json' || /^[^/]+\/package\.json$/.test(entry.name));
|
|
527
|
+
if (!pkgEntry) return null;
|
|
528
|
+
|
|
529
|
+
const dest = path.join(parentDir, 'extracted');
|
|
530
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
531
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
532
|
+
|
|
533
|
+
for (const entry of entries) {
|
|
534
|
+
if (entry.name.endsWith('/')) continue;
|
|
535
|
+
const target = path.join(dest, entry.name);
|
|
536
|
+
// 防路径穿越
|
|
537
|
+
if (!path.resolve(target).startsWith(path.resolve(dest))) continue;
|
|
538
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
539
|
+
fs.writeFileSync(target, entry.data);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// package.json 在根还是在唯一子目录里
|
|
543
|
+
const rootPkg = path.join(dest, 'package.json');
|
|
544
|
+
let pluginDir = dest;
|
|
545
|
+
if (!fs.existsSync(rootPkg)) {
|
|
546
|
+
pluginDir = path.join(dest, path.dirname(pkgEntry.name));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const { describeLocalPlugin } = await import('./import-plugin.js');
|
|
550
|
+
const info = describeLocalPlugin(pluginDir);
|
|
551
|
+
if (!info?.name) return null;
|
|
552
|
+
return { dir: pluginDir, info };
|
|
553
|
+
} catch {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
482
556
|
}
|
|
483
557
|
|
|
484
558
|
/** 归档分支名里的斜杠要转义,否则 codeload 会 404。 */
|
package/lib/lockfile.js
CHANGED
|
@@ -22,25 +22,69 @@ import { spawnSync } from 'node:child_process';
|
|
|
22
22
|
import fs from 'node:fs';
|
|
23
23
|
import path from 'node:path';
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* 找 pnpm —— 优先用 dsh 自带的那个。
|
|
27
|
+
*
|
|
28
|
+
* 为什么不能随便用 PATH 上的:dsh 内置 pnpm 11(store/v11),
|
|
29
|
+
* 用户命令行可能是 pnpm 10(store/v10)。store 目录不同、锁文件格式也可能不同,
|
|
30
|
+
* 用错版本去同步等于又把 dsh 的安装/卸载搞坏。
|
|
31
|
+
*
|
|
32
|
+
* 探测顺序:DSH_PNPM 环境变量 → dsh 运行时里的 pnpm.cjs → PATH 上的 pnpm。
|
|
33
|
+
*
|
|
34
|
+
* @returns {{ command: string, args: string[], version: string|null, bundled: boolean } | null}
|
|
35
|
+
*/
|
|
26
36
|
function findPnpm() {
|
|
27
|
-
const candidates =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
const candidates = [];
|
|
38
|
+
|
|
39
|
+
if (process.env.DSH_PNPM && fs.existsSync(process.env.DSH_PNPM)) {
|
|
40
|
+
candidates.push({ command: process.execPath, args: [process.env.DSH_PNPM], bundled: true });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// dsh 的 node 通常在 <runtime>/node.exe,pnpm 在 <runtime>/dependencies/pnpm/bin/pnpm.cjs
|
|
44
|
+
const exeDir = path.dirname(process.execPath);
|
|
45
|
+
for (const rel of [
|
|
46
|
+
['dependencies', 'pnpm', 'bin', 'pnpm.cjs'],
|
|
47
|
+
['..', 'dependencies', 'pnpm', 'bin', 'pnpm.cjs'],
|
|
48
|
+
['runtime', 'primary-runtime', 'dependencies', 'pnpm', 'bin', 'pnpm.cjs'],
|
|
49
|
+
]) {
|
|
50
|
+
const candidate = path.join(exeDir, ...rel);
|
|
51
|
+
if (fs.existsSync(candidate)) {
|
|
52
|
+
candidates.push({ command: process.execPath, args: [candidate], bundled: true });
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 兜底:PATH 上的 pnpm
|
|
58
|
+
const names = process.platform === 'win32' ? ['pnpm.cmd', 'pnpm.exe', 'pnpm'] : ['pnpm'];
|
|
59
|
+
for (const name of names) candidates.push({ command: name, args: [], bundled: false });
|
|
31
60
|
|
|
32
|
-
for (const
|
|
33
|
-
const result = spawnSync(
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
const result = spawnSync(candidate.command, [...candidate.args, '--version'], {
|
|
34
63
|
encoding: 'utf8',
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
// 自带 pnpm 是 node 直接跑 .cjs,不需要 shell;PATH 上的需要
|
|
65
|
+
shell: process.platform === 'win32' && !candidate.bundled,
|
|
66
|
+
timeout: 20000,
|
|
37
67
|
windowsHide: true,
|
|
38
68
|
});
|
|
39
|
-
if (!result.error && result.status === 0)
|
|
69
|
+
if (!result.error && result.status === 0) {
|
|
70
|
+
const version = String(result.stdout ?? '').trim().split('\n')[0] || null;
|
|
71
|
+
return { ...candidate, version };
|
|
72
|
+
}
|
|
40
73
|
}
|
|
41
74
|
return null;
|
|
42
75
|
}
|
|
43
76
|
|
|
77
|
+
/** 用找到的 pnpm 跑一条命令。 */
|
|
78
|
+
function runPnpm(pnpm, args, cwd, timeoutMs) {
|
|
79
|
+
return spawnSync(pnpm.command, [...pnpm.args, ...args], {
|
|
80
|
+
cwd,
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
shell: process.platform === 'win32' && !pnpm.bundled,
|
|
83
|
+
timeout: timeoutMs,
|
|
84
|
+
windowsHide: true,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
44
88
|
/**
|
|
45
89
|
* 让 `pnpm-lock.yaml` 与 `package.json` 对齐。
|
|
46
90
|
*
|
|
@@ -68,23 +112,21 @@ export function syncLockfile(profileDir, options = {}) {
|
|
|
68
112
|
const pnpm = findPnpm();
|
|
69
113
|
if (!pnpm) return { ok: false, skipped: true, reason: '没找到 pnpm' };
|
|
70
114
|
|
|
71
|
-
const result =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
env: { ...process.env, CI: '' },
|
|
78
|
-
});
|
|
115
|
+
const result = runPnpm(
|
|
116
|
+
pnpm,
|
|
117
|
+
['install', '--lockfile-only', '--config.confirmModulesPurge=false'],
|
|
118
|
+
profile,
|
|
119
|
+
options.timeoutMs ?? 180000,
|
|
120
|
+
);
|
|
79
121
|
|
|
80
122
|
if (result.error) {
|
|
81
|
-
return { ok: false, error: String(result.error.message ?? result.error) };
|
|
123
|
+
return { ok: false, error: String(result.error.message ?? result.error), pnpm: pnpm.version };
|
|
82
124
|
}
|
|
83
125
|
if (result.status !== 0) {
|
|
84
126
|
const detail = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
|
|
85
|
-
return { ok: false, error: detail.slice(0, 400) || `pnpm 退出码 ${result.status}
|
|
127
|
+
return { ok: false, error: detail.slice(0, 400) || `pnpm 退出码 ${result.status}`, pnpm: pnpm.version };
|
|
86
128
|
}
|
|
87
|
-
return { ok: true };
|
|
129
|
+
return { ok: true, pnpm: pnpm.version };
|
|
88
130
|
} catch (error) {
|
|
89
131
|
return { ok: false, error: String(error?.message ?? error) };
|
|
90
132
|
}
|
|
@@ -108,19 +150,17 @@ export function checkLockfile(profileDir) {
|
|
|
108
150
|
const pnpm = findPnpm();
|
|
109
151
|
if (!pnpm) return { ok: false, skipped: true, reason: '没找到 pnpm' };
|
|
110
152
|
|
|
111
|
-
const result =
|
|
112
|
-
cwd: profile,
|
|
113
|
-
encoding: 'utf8',
|
|
114
|
-
shell: process.platform === 'win32',
|
|
115
|
-
timeout: 120000,
|
|
116
|
-
windowsHide: true,
|
|
117
|
-
});
|
|
153
|
+
const result = runPnpm(pnpm, ['install', '--frozen-lockfile', '--lockfile-only'], profile, 180000);
|
|
118
154
|
// 注意:这里不能用 --reporter=silent —— 它会把 ERR_PNPM_OUTDATED_LOCKFILE
|
|
119
155
|
// 这段错误文本一起吞掉,导致分不清"锁文件不一致"和"其它失败"。
|
|
120
156
|
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
|
-
|
|
157
|
+
if (/ERR_PNPM_OUTDATED_LOCKFILE/.test(detail)) return { ok: true, outdated: true, pnpm: pnpm.version };
|
|
158
|
+
if (result.status === 0) return { ok: true, outdated: false, pnpm: pnpm.version };
|
|
159
|
+
// 其它错误里也可能带 store 不一致
|
|
160
|
+
if (/ERR_PNPM_UNEXPECTED_STORE/.test(detail)) {
|
|
161
|
+
return { ok: false, storeMismatch: true, pnpm: pnpm.version, error: detail.trim().slice(0, 400) };
|
|
162
|
+
}
|
|
163
|
+
return { ok: false, error: detail.trim().slice(0, 400) || `pnpm 退出码 ${result.status}`, pnpm: pnpm.version };
|
|
124
164
|
} catch (error) {
|
|
125
165
|
return { ok: false, error: String(error?.message ?? error) };
|
|
126
166
|
}
|
package/lib/server.js
CHANGED
|
@@ -1150,6 +1150,8 @@ export function mountRoutes(ctx, config = {}) {
|
|
|
1150
1150
|
mode: body.mode === 'skip' ? 'skip' : 'merge',
|
|
1151
1151
|
linkNodeModules: body.linkNodeModules !== false,
|
|
1152
1152
|
decisions: body.decisions && typeof body.decisions === 'object' ? body.decisions : {},
|
|
1153
|
+
// enable=false → 只装不启用(界面上「安装后立即启用」的勾选)
|
|
1154
|
+
enable: body.enable !== false,
|
|
1153
1155
|
onProgress: report,
|
|
1154
1156
|
});
|
|
1155
1157
|
if (dryRun) {
|
package/package.json
CHANGED