dsh-plugin 1.1.2 → 1.2.0
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/client.js +4469 -1445
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +438 -22
- package/lib/index.js +1 -1
- package/lib/services/install/install.js +4 -0
- package/lib/services/install/npm-check.js +55 -0
- package/lib/services/{npm-resolve.js → install/npm-resolve.js} +7 -5
- package/lib/services/{preflight.js → install/preflight.js} +15 -4
- package/lib/services/{task-queue.js → install/task-queue.js} +155 -66
- package/lib/services/loader.js +56 -1
- package/lib/services/log.js +138 -0
- package/lib/services/probe.js +157 -0
- package/lib/services/{profile.js → profile/profile.js} +29 -1
- package/lib/services/settings.js +60 -0
- package/lib/types/http/routes.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +3 -0
- package/lib/types/services/{install.d.ts → install/install.d.ts} +5 -5
- package/lib/types/services/install/npm-check.d.ts +12 -0
- package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
- package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
- package/lib/types/services/loader.d.ts +14 -8
- package/lib/types/services/log.d.ts +49 -0
- package/lib/types/services/probe.d.ts +30 -0
- package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +1 -1
- package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +2 -0
- package/lib/types/services/settings.d.ts +24 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +341 -120
- package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +11 -7
- package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +5 -4
- package/src/client/components/layout/CatalogControls.tsx +80 -0
- package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +4 -4
- package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +3 -3
- package/src/client/components/layout/SectionTabs.tsx +60 -0
- package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +7 -7
- package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +5 -5
- package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
- package/src/client/components/modals/LogsModal.tsx +301 -0
- package/src/client/components/modals/LogsPathModal.tsx +161 -0
- package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +47 -20
- package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +2 -2
- package/src/client/components/{modals.tsx → modals/modals.tsx} +117 -40
- package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +14 -6
- package/src/client/components/ui/Toggle.tsx +28 -0
- package/src/client/components/ui/icons.tsx +343 -0
- package/src/client/components/views/CustomInstallView.tsx +250 -0
- package/src/client/components/views/DiagnosticsView.tsx +172 -0
- package/src/client/components/views/InstalledView.tsx +328 -0
- package/src/client/components/views/LogsView.tsx +150 -0
- package/src/client/components/views/MarketView.tsx +66 -0
- package/src/client/components/views/SettingsView.tsx +273 -0
- package/src/client/data/catalog.ts +38 -0
- package/src/client/data/host.ts +107 -0
- package/src/client/data/hub.ts +41 -0
- package/src/client/hooks/useCatalog.ts +71 -115
- package/src/client/hooks/useIncrementalList.ts +46 -0
- package/src/client/hooks/useLanguage.ts +40 -0
- package/src/client/hooks/useSettings.ts +90 -0
- package/src/client/hooks/useTaskQueue.ts +156 -39
- package/src/client/locales.ts +358 -38
- package/src/client/logic/constants.ts +87 -0
- package/src/client/{lib → logic}/failures.ts +95 -74
- package/src/client/logic/install-command.ts +56 -0
- package/src/client/logic/installed.ts +207 -0
- package/src/client/logic/normalize.ts +52 -0
- package/src/client/logic/urls.ts +125 -0
- package/src/client/styles/CustomInstallView.module.css +191 -0
- package/src/client/styles/DiagnosticsView.module.css +231 -0
- package/src/client/styles/Dropdown.module.css +42 -41
- package/src/client/styles/Header.module.css +100 -132
- package/src/client/styles/InstalledView.module.css +447 -0
- package/src/client/styles/List.module.css +6 -0
- package/src/client/styles/LogsView.module.css +503 -0
- package/src/client/styles/Modal.module.css +352 -70
- package/src/client/styles/SectionTabs.module.css +142 -0
- package/src/client/styles/SettingsView.module.css +232 -0
- package/src/client/styles/Toggle.module.css +60 -0
- package/src/client/styles/tokens.module.css +1 -0
- package/src/client/types.ts +23 -3
- package/src/server/http/routes.ts +414 -22
- package/src/server/index.ts +1 -1
- package/src/server/services/{install-types.ts → install/install-types.ts} +3 -1
- package/src/server/services/{install.ts → install/install.ts} +5 -5
- package/src/server/services/install/npm-check.ts +53 -0
- package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +7 -5
- package/src/server/services/{preflight.ts → install/preflight.ts} +16 -4
- package/src/server/services/{task-queue.ts → install/task-queue.ts} +157 -63
- package/src/server/services/loader.ts +55 -1
- package/src/server/services/log.ts +167 -0
- package/src/server/services/probe.ts +157 -0
- package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +1 -1
- package/src/server/services/{profile.ts → profile/profile.ts} +31 -1
- package/src/server/services/settings.ts +79 -0
- package/lib/services/install.js +0 -4
- package/src/client/components/CatalogControls.tsx +0 -108
- package/src/client/components/icons.tsx +0 -129
- package/src/client/lib/catalog.ts +0 -293
- package/src/client/lib/env.ts +0 -20
- /package/lib/services/{install-types.js → install/install-types.js} +0 -0
- /package/lib/services/{installed-versions.js → profile/installed-versions.js} +0 -0
- /package/lib/services/{pending-restart.js → profile/pending-restart.js} +0 -0
- /package/lib/services/{progress.js → profile/progress.js} +0 -0
- /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
- /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
- /package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +0 -0
- /package/src/client/{lib → logic}/format.ts +0 -0
- /package/src/client/{lib → logic}/renderMarkdown.ts +0 -0
- /package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +0 -0
- /package/src/server/services/{progress.ts → profile/progress.ts} +0 -0
package/lib/http/routes.js
CHANGED
|
@@ -6,16 +6,20 @@
|
|
|
6
6
|
* Local HTTP routes exposing real installs to the in-app Plugin Hub UI.
|
|
7
7
|
* The client fetches the same-origin `/dsh-plugin-hub/*` endpoints; the
|
|
8
8
|
* install handler validates the target, then spawns the official dsh CLI
|
|
9
|
-
* (see services/install.ts) and reports the captured result back.
|
|
9
|
+
* (see services/install/install.ts) and reports the captured result back.
|
|
10
10
|
*/
|
|
11
|
-
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
12
12
|
import { homedir, release } from 'node:os';
|
|
13
13
|
import { dirname, join, resolve } from 'node:path';
|
|
14
14
|
import { spawn } from 'node:child_process';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
15
|
+
import { fetchViaCurl, probeUrl, systemProxy } from '../services/probe.js';
|
|
16
|
+
import { activeTask, cancelTask, dumpLoaderEntries, getTask, githubRepoOf, githubTarget, globalNpmPackagesOf, hasQueuedTarget, installTargetOf, listPendingRestarts, readProfileArg, startPluginMutation, validPackageName } from '../services/install/install.js';
|
|
17
|
+
import { recordInstalledVersion, recordResolvedNpmPackage, readInstalledVersions, removeInstalledVersion } from '../services/profile/installed-versions.js';
|
|
18
|
+
import { resolveNpmPackage } from '../services/install/npm-resolve.js';
|
|
19
|
+
import { preflightTarget } from '../services/install/preflight.js';
|
|
20
|
+
import { isDshPlugin, isEntryLoaded } from '../services/loader.js';
|
|
21
|
+
import { loadSettings, saveSettings, resetSettings } from '../services/settings.js';
|
|
22
|
+
import { appendLog, readLog, logFilePath, defaultLogFilePath, customLogFile } from '../services/log.js';
|
|
19
23
|
const PROFILE_RE = /^[A-Za-z0-9_-]+$/;
|
|
20
24
|
const BODY_LIMIT_BYTES = 4 * 1024;
|
|
21
25
|
const COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -68,6 +72,22 @@ function hostEnv(profile) {
|
|
|
68
72
|
dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
|
|
69
73
|
};
|
|
70
74
|
}
|
|
75
|
+
/** 安装/卸载 CLI 的子进程环境:透传宿主环境,并按设置注入 npm 镜像源与 HTTP(S) 代理。
|
|
76
|
+
* 代理优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 宿主 env 原有值。
|
|
77
|
+
* 使安装通道与诊断使用相同的代理来源。 */
|
|
78
|
+
function mutationEnv(settings) {
|
|
79
|
+
const env = { ...process.env, CI: 'true' };
|
|
80
|
+
if (settings.npmRegistry !== '')
|
|
81
|
+
env.npm_config_registry = settings.npmRegistry;
|
|
82
|
+
const proxy = settings.proxy !== '' ? settings.proxy : (systemProxy() ?? '');
|
|
83
|
+
if (proxy !== '') {
|
|
84
|
+
env.HTTP_PROXY = proxy;
|
|
85
|
+
env.HTTPS_PROXY = proxy;
|
|
86
|
+
env.http_proxy = proxy;
|
|
87
|
+
env.https_proxy = proxy;
|
|
88
|
+
}
|
|
89
|
+
return env;
|
|
90
|
+
}
|
|
71
91
|
/** Match a GitHub install target against the installed table; returns the package name when already installed. */
|
|
72
92
|
function installedByRepo(installed, target) {
|
|
73
93
|
const repo = githubRepoOf(target);
|
|
@@ -140,6 +160,14 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
140
160
|
if (!PROFILE_RE.test(profile))
|
|
141
161
|
throw new Error(`invalid profile name: ${profile}`);
|
|
142
162
|
console.log(`[hub] routes mounted (profile=${profile}, loader=${loader === undefined ? 'undefined' : 'provided'})`);
|
|
163
|
+
// 系统日志:插件随宿主启动即记录一条 —— 保证「打开就有运行日志」,日志页永远有内容
|
|
164
|
+
appendLog(profile, {
|
|
165
|
+
at: Date.now(),
|
|
166
|
+
level: 'info',
|
|
167
|
+
category: 'system',
|
|
168
|
+
event: 'system.start',
|
|
169
|
+
message: `Plugin Hub 已启动(profile=${profile})`,
|
|
170
|
+
});
|
|
143
171
|
const disposers = [
|
|
144
172
|
webServer.register({
|
|
145
173
|
kind: 'exact',
|
|
@@ -150,6 +178,283 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
150
178
|
sendJson(response, 200, { entries: dumpLoaderEntries(loader) });
|
|
151
179
|
},
|
|
152
180
|
}),
|
|
181
|
+
webServer.register({
|
|
182
|
+
kind: 'exact',
|
|
183
|
+
path: '/dsh-plugin-hub/settings',
|
|
184
|
+
// GET 读 / POST 写:webServer.register 按 (kind, path) 唯一,多方法必须合并到同一个 handler 按 method 分派
|
|
185
|
+
handler: async (request, response) => {
|
|
186
|
+
if (request.method === 'GET') {
|
|
187
|
+
sendJson(response, 200, loadSettings(profile));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (!requireTrustedPost(request, response))
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
const body = await readJsonBody(request);
|
|
194
|
+
// 白名单字段:只接受已知设置项,防客户端把任意键写进文件
|
|
195
|
+
const patch = {};
|
|
196
|
+
const source = typeof body === 'object' && body !== null ? body : {};
|
|
197
|
+
for (const key of ['checkUpdatesOnStart', 'enableNpmInstall', 'enableGitInstall', 'enableDshInstall', 'npmRegistry', 'proxy', 'logPath']) {
|
|
198
|
+
if (source[key] !== undefined)
|
|
199
|
+
patch[key] = source[key];
|
|
200
|
+
}
|
|
201
|
+
// 日志位置:非空时预建目录验证可写,失败直接 400 拦截(默认位置永远合法,空串 = 回默认)
|
|
202
|
+
if (typeof patch.logPath === 'string' && patch.logPath.trim() !== '') {
|
|
203
|
+
try {
|
|
204
|
+
mkdirSync(dirname(customLogFile(patch.logPath)), { recursive: true });
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
sendJson(response, 400, { error: 'log path is not writable' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
sendJson(response, 200, saveSettings(profile, patch));
|
|
212
|
+
// 系统日志:设置项变更
|
|
213
|
+
appendLog(profile, {
|
|
214
|
+
at: Date.now(),
|
|
215
|
+
level: 'info',
|
|
216
|
+
category: 'settings',
|
|
217
|
+
event: 'settings.update',
|
|
218
|
+
message: `更新设置:${Object.keys(patch).join(', ')}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
}),
|
|
226
|
+
webServer.register({
|
|
227
|
+
kind: 'exact',
|
|
228
|
+
path: '/dsh-plugin-hub/settings/reset',
|
|
229
|
+
handler: async (request, response) => {
|
|
230
|
+
if (!requireTrustedPost(request, response))
|
|
231
|
+
return;
|
|
232
|
+
sendJson(response, 200, resetSettings(profile));
|
|
233
|
+
appendLog(profile, {
|
|
234
|
+
at: Date.now(),
|
|
235
|
+
level: 'warn',
|
|
236
|
+
category: 'settings',
|
|
237
|
+
event: 'settings.reset',
|
|
238
|
+
message: '恢复全部默认设置',
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
webServer.register({
|
|
243
|
+
kind: 'exact',
|
|
244
|
+
path: '/dsh-plugin-hub/logs',
|
|
245
|
+
handler: (request, response) => {
|
|
246
|
+
if (!requireMethod(request, response, 'GET'))
|
|
247
|
+
return;
|
|
248
|
+
// 日志查看器分页接口:?offset=&limit=&category=&level=&query=,返回过滤后的倒序分页
|
|
249
|
+
try {
|
|
250
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
251
|
+
const num = (name) => {
|
|
252
|
+
const v = url.searchParams.get(name);
|
|
253
|
+
if (v === null || v === '')
|
|
254
|
+
return undefined;
|
|
255
|
+
const n = Number(v);
|
|
256
|
+
return Number.isFinite(n) && n >= 0 ? Math.floor(n) : undefined;
|
|
257
|
+
};
|
|
258
|
+
const limit = Math.min(num('limit') ?? 200, 500);
|
|
259
|
+
const category = url.searchParams.get('category') ?? 'all';
|
|
260
|
+
const level = url.searchParams.get('level') ?? 'all';
|
|
261
|
+
const query = url.searchParams.get('query') ?? '';
|
|
262
|
+
const result = readLog(profile, {
|
|
263
|
+
offset: num('offset') ?? 0,
|
|
264
|
+
limit,
|
|
265
|
+
category: category,
|
|
266
|
+
level: level,
|
|
267
|
+
query,
|
|
268
|
+
});
|
|
269
|
+
sendJson(response, 200, {
|
|
270
|
+
entries: result.entries,
|
|
271
|
+
total: result.total,
|
|
272
|
+
offset: num('offset') ?? 0,
|
|
273
|
+
limit,
|
|
274
|
+
hasMore: result.total > (num('offset') ?? 0) + result.entries.length,
|
|
275
|
+
path: logFilePath(profile),
|
|
276
|
+
defaultPath: defaultLogFilePath(profile),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
sendJson(response, 400, { error: 'invalid log query' });
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
}),
|
|
284
|
+
webServer.register({
|
|
285
|
+
kind: 'exact',
|
|
286
|
+
path: '/dsh-plugin-hub/open-log',
|
|
287
|
+
handler: (request, response) => {
|
|
288
|
+
if (!requireTrustedPost(request, response))
|
|
289
|
+
return;
|
|
290
|
+
// 在系统文件管理器里定位日志文件(macOS Finder -R / Windows explorer /select, / Linux xdg-open)
|
|
291
|
+
try {
|
|
292
|
+
const file = logFilePath(profile);
|
|
293
|
+
const bin = process.platform === 'darwin' ? 'open'
|
|
294
|
+
: process.platform === 'win32' ? 'explorer'
|
|
295
|
+
: 'xdg-open';
|
|
296
|
+
const args = process.platform === 'darwin' ? ['-R', file]
|
|
297
|
+
: process.platform === 'win32' ? ['/select,', file]
|
|
298
|
+
: [file];
|
|
299
|
+
const child = spawn(bin, args, { stdio: 'ignore', detached: true });
|
|
300
|
+
child.once('error', () => sendJson(response, 500, { error: 'failed to open log file' }));
|
|
301
|
+
child.once('spawn', () => sendJson(response, 200, { ok: true }));
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
sendJson(response, 500, { error: 'failed to open log file' });
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
// 系统目录选择器:弹原生文件夹对话框让用户挑日志存放目录(macOS osascript /
|
|
309
|
+
// Windows FolderBrowserDialog / Linux zenity),选中返回绝对路径,取消返回 cancelled。
|
|
310
|
+
// 与 open-log 不同,这里必须等对话框关闭才能拿结果,所以阻塞收集 stdout 再应答。
|
|
311
|
+
// 对话框默认定位到当前日志文件所在目录 —— 每台机器/平台的默认位置都不同,
|
|
312
|
+
// 这里动态取 logFilePath() 的目录,用户打开就在「它原来的位置」附近。
|
|
313
|
+
webServer.register({
|
|
314
|
+
kind: 'exact',
|
|
315
|
+
path: '/dsh-plugin-hub/choose-log-dir',
|
|
316
|
+
handler: (request, response) => {
|
|
317
|
+
if (!requireTrustedPost(request, response))
|
|
318
|
+
return;
|
|
319
|
+
try {
|
|
320
|
+
const title = 'DSH Plugin Hub — choose log folder';
|
|
321
|
+
const startDir = dirname(logFilePath(profile));
|
|
322
|
+
let bin;
|
|
323
|
+
let args;
|
|
324
|
+
if (process.platform === 'darwin') {
|
|
325
|
+
bin = 'osascript';
|
|
326
|
+
args = ['-e', `POSIX path of (choose folder with prompt "${title}" default location (POSIX file "${startDir}"))`];
|
|
327
|
+
}
|
|
328
|
+
else if (process.platform === 'win32') {
|
|
329
|
+
bin = 'powershell.exe';
|
|
330
|
+
args = ['-NoProfile', '-STA', '-Command', [
|
|
331
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
332
|
+
'$f = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
333
|
+
`$f.Description = '${title}'`,
|
|
334
|
+
// 初始定位到当前日志目录(单引号字符串内反斜杠不转义,Windows 路径原样传入)
|
|
335
|
+
`$f.SelectedPath = '${startDir}'`,
|
|
336
|
+
'if ($f.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $f.SelectedPath } else { "" }',
|
|
337
|
+
].join('; ')];
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
bin = 'zenity';
|
|
341
|
+
args = ['--file-selection', '--directory', `--title=${title}`, `--filename=${startDir}`];
|
|
342
|
+
}
|
|
343
|
+
const child = spawn(bin, args);
|
|
344
|
+
let out = '';
|
|
345
|
+
child.stdout.on('data', (d) => { out += String(d); });
|
|
346
|
+
child.on('error', () => sendJson(response, 500, { error: 'directory picker unavailable' }));
|
|
347
|
+
child.on('close', (code) => {
|
|
348
|
+
// macOS osascript 输出带引号(如 "/Users/x"),统一剥掉首尾空白与引号
|
|
349
|
+
const picked = out.trim().replace(/^"|"$/g, '');
|
|
350
|
+
if (code === 0 && picked !== '')
|
|
351
|
+
sendJson(response, 200, { path: picked });
|
|
352
|
+
else
|
|
353
|
+
sendJson(response, 200, { cancelled: true });
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
sendJson(response, 500, { error: 'directory picker unavailable' });
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
}),
|
|
361
|
+
webServer.register({
|
|
362
|
+
kind: 'exact',
|
|
363
|
+
path: '/dsh-plugin-hub/diagnostics',
|
|
364
|
+
handler: async (request, response) => {
|
|
365
|
+
if (!requireTrustedPost(request, response))
|
|
366
|
+
return;
|
|
367
|
+
// 连通性自检:npm registry(镜像/官方)/ GitHub API / 目录站点,逐路探测并测速。
|
|
368
|
+
// 与安装通道一一对应 —— npm 反查与 npm 安装走 registry、git 源走 github、目录数据走 dsh-plugin.org。
|
|
369
|
+
// 流式(NDJSON)逐条上报:每个探测先发命令行,完成后发结果行,客户端以黑窗口终端实时展示。
|
|
370
|
+
// 可选 body { key }:只重测指定通道(页面列表「重测」单项时用),缺省全量串行探测。
|
|
371
|
+
let onlyKey = '';
|
|
372
|
+
try {
|
|
373
|
+
const body = await readJsonBody(request);
|
|
374
|
+
if (body !== null && typeof body === 'object' && typeof body.key === 'string') {
|
|
375
|
+
onlyKey = body.key;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
// 空 body / 非 JSON:视为全量探测
|
|
380
|
+
}
|
|
381
|
+
const settings = loadSettings(profile);
|
|
382
|
+
const registry = settings.npmRegistry.replace(/\/+$/, '') || 'https://registry.npmjs.org';
|
|
383
|
+
// 连通性自检直接打安装通道真实访问的轻量资源:npm 源拉包元数据、git 通道探
|
|
384
|
+
// 仓库主页、目录站探 badge 小文件,避免拉取目录全量 JSON。
|
|
385
|
+
const allChecks = [
|
|
386
|
+
{ key: 'npm', url: `${registry}/dsh-plugin`, display: settings.npmRegistry !== '' ? settings.npmRegistry : 'registry.npmjs.org', cmd: `npm view dsh-plugin version --registry ${registry}` },
|
|
387
|
+
{ key: 'github', url: 'https://github.com/dshplugin/dsh-plugin-hub', display: 'github.com', cmd: 'git ls-remote https://github.com/dshplugin/dsh-plugin-hub' },
|
|
388
|
+
{ key: 'catalog', url: 'https://api.dsh-plugin.org/stats.json', display: 'api.dsh-plugin.org', cmd: 'curl -s https://api.dsh-plugin.org/stats.json' },
|
|
389
|
+
];
|
|
390
|
+
const checks = onlyKey !== '' ? allChecks.filter((c) => c.key === onlyKey) : allChecks;
|
|
391
|
+
response.writeHead(200, {
|
|
392
|
+
'content-type': 'application/x-ndjson; charset=utf-8',
|
|
393
|
+
'cache-control': 'no-store',
|
|
394
|
+
});
|
|
395
|
+
const send = (line) => { response.write(`${JSON.stringify(line)}\n`); };
|
|
396
|
+
// 探测走代理的优先级:设置里的代理 → 系统代理(macOS scutil / Windows 注册表)→ 环境变量 HTTPS_PROXY → 直连。
|
|
397
|
+
// 与安装通道一致(npm/git 靠 HTTP_PROXY 环境变量走代理),且自动跟随系统代理 ——
|
|
398
|
+
// 浏览器挂着代理能开、诊断就能测通,不用用户手动把代理填进设置。
|
|
399
|
+
const effectiveProxy = settings.proxy !== ''
|
|
400
|
+
? settings.proxy
|
|
401
|
+
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
402
|
+
const probe = (url) => probeUrl(url, effectiveProxy, 6000);
|
|
403
|
+
// 逐路串行:一条命令一条结果地推送给终端,保持「程序在跑」的真实感
|
|
404
|
+
const results = [];
|
|
405
|
+
for (const c of checks) {
|
|
406
|
+
send({ type: 'probe', key: c.key, cmd: c.cmd, display: c.display });
|
|
407
|
+
const r = await probe(c.url);
|
|
408
|
+
results.push({ key: c.key, ok: r.ok });
|
|
409
|
+
send({ type: r.ok ? 'ok' : 'fail', key: c.key, display: c.display, ms: r.ms, status: r.status });
|
|
410
|
+
}
|
|
411
|
+
send({ type: 'end', at: Date.now() });
|
|
412
|
+
response.end();
|
|
413
|
+
// 系统日志:诊断汇总(全部通过 / 存在不可达)
|
|
414
|
+
const failed = results.filter((r) => !r.ok);
|
|
415
|
+
appendLog(profile, {
|
|
416
|
+
at: Date.now(),
|
|
417
|
+
level: failed.length === 0 ? 'success' : 'error',
|
|
418
|
+
category: 'diagnostics',
|
|
419
|
+
event: 'diagnostics.done',
|
|
420
|
+
message: failed.length === 0
|
|
421
|
+
? `连通性检测通过(${results.length} 个通道)`
|
|
422
|
+
: `连通性检测:${failed.length}/${results.length} 个通道不可达`,
|
|
423
|
+
});
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
webServer.register({
|
|
427
|
+
kind: 'exact',
|
|
428
|
+
path: '/dsh-plugin-hub/catalog',
|
|
429
|
+
handler: async (request, response) => {
|
|
430
|
+
if (!requireMethod(request, response, 'GET'))
|
|
431
|
+
return;
|
|
432
|
+
// 目录/统计数据服务端代理:浏览器不再直连 dsh-plugin.org,改经此路由
|
|
433
|
+
// 转发(curl 子进程注入代理 env),与 npm / git 安装通道走同一代理口径,
|
|
434
|
+
// 「npm / git / 目录数据请求统一走该代理」的设置文案因此真实生效。
|
|
435
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
436
|
+
const settings = loadSettings(profile);
|
|
437
|
+
// 代理优先级与安装/诊断一致:设置里的代理 → 系统代理 → 环境变量 → 直连
|
|
438
|
+
const proxy = settings.proxy !== ''
|
|
439
|
+
? settings.proxy
|
|
440
|
+
: (systemProxy() ?? process.env.HTTPS_PROXY ?? process.env.https_proxy ?? '');
|
|
441
|
+
const isStats = url.searchParams.get('stats') === '1';
|
|
442
|
+
const target = isStats
|
|
443
|
+
? 'https://api.dsh-plugin.org/stats.json'
|
|
444
|
+
: `https://api.dsh-plugin.org/plugins.${url.searchParams.get('lang') === 'en' ? 'en' : 'zh'}.json`;
|
|
445
|
+
const r = await fetchViaCurl(target, proxy, 20000);
|
|
446
|
+
if (!r.ok || r.body === '') {
|
|
447
|
+
sendJson(response, 502, { error: 'catalog fetch failed' });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
sendJson(response, 200, JSON.parse(r.body));
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
sendJson(response, 502, { error: 'catalog fetch returned invalid JSON' });
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
}),
|
|
153
458
|
webServer.register({
|
|
154
459
|
kind: 'exact',
|
|
155
460
|
path: '/dsh-plugin-hub/install',
|
|
@@ -163,40 +468,103 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
163
468
|
const mode = body !== null && typeof body === 'object' && body.mode === 'update'
|
|
164
469
|
? 'update'
|
|
165
470
|
: 'install';
|
|
471
|
+
// installTargetOf 先把完整 DSH 命令(`dsh plugin [--profile <p>] add <target>`)剥成裸目标,
|
|
472
|
+
// 兼容用户直接粘贴命令行(防输错),真正语法判定交给下面 githubRepoOf / validPackageName。
|
|
166
473
|
const rawRepo = typeof body === 'object' && body !== null && typeof body.repo === 'string'
|
|
167
|
-
? body.repo
|
|
474
|
+
? installTargetOf(body.repo)
|
|
168
475
|
: '';
|
|
169
476
|
// 前端决策的展示用仓库名(owner/repo):npm 安装时 body.repo 是包名,display 用于
|
|
170
477
|
// 待重启行 / 任务恢复时展示仓库名,保证用户无感知(安装通道变化不改变所见目标)
|
|
171
478
|
const displayRepo = typeof body === 'object' && body !== null && typeof body.display === 'string'
|
|
172
479
|
? body.display.trim()
|
|
173
480
|
: rawRepo;
|
|
174
|
-
//
|
|
175
|
-
const
|
|
176
|
-
|
|
481
|
+
// 请求来源:目录插件安装(catalog)走目录白名单;命令行安装(custom)按通道受安全开关控制
|
|
482
|
+
const source = typeof body === 'object' && body !== null && typeof body.source === 'string'
|
|
483
|
+
? body.source
|
|
484
|
+
: '';
|
|
485
|
+
const settings = loadSettings(profile);
|
|
486
|
+
// 全局 npm 安装(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令):
|
|
487
|
+
// 直接从命令框输入格式执行全局安装,不进任何 profile、无需宿主重启。
|
|
488
|
+
// 包列表优先取 body.globalNpm(客户端命令框解析后显式携带);兜底再从 rawRepo 解析,
|
|
489
|
+
// 保证直接调 API 传完整命令也能识别。
|
|
490
|
+
const bodyGlobalNpm = Array.isArray(body.globalNpm)
|
|
491
|
+
? body.globalNpm.filter((x) => typeof x === 'string' && x !== '')
|
|
492
|
+
: [];
|
|
493
|
+
let globalNpm = bodyGlobalNpm.length > 0 ? bodyGlobalNpm : (globalNpmPackagesOf(rawRepo) ?? []);
|
|
494
|
+
if (globalNpm.length > 0) {
|
|
495
|
+
// 包名逐个按 npm 包名语法校验 + 拒绝以 - 开头的 token(npm 会把 --xxx 当参数而非包名),数量封顶防滥用
|
|
496
|
+
if (globalNpm.length > 20 || globalNpm.some((p) => !validPackageName(p) || p.startsWith('-'))) {
|
|
497
|
+
sendJson(response, 400, { error: 'unsupported install target' });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// 通道门禁与安装通道同构:全局安装本质是 npm 操作,吃「启用 NPM 安装」开关
|
|
501
|
+
if (source !== 'catalog' && !settings.enableNpmInstall) {
|
|
502
|
+
sendJson(response, 403, { error: 'npm installs are disabled by the security settings' });
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
// 队列级去重:同一包列表已在排队/执行中时拒绝
|
|
506
|
+
const identity = globalNpm.join(' ');
|
|
507
|
+
if (hasQueuedTarget(identity)) {
|
|
508
|
+
sendJson(response, 409, { error: `already queued: ${identity}` });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const task = startPluginMutation({
|
|
512
|
+
action: 'add',
|
|
513
|
+
profile,
|
|
514
|
+
target: identity,
|
|
515
|
+
displayTarget: identity,
|
|
516
|
+
globalNpm,
|
|
517
|
+
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
518
|
+
env: mutationEnv(settings),
|
|
519
|
+
});
|
|
520
|
+
sendJson(response, 200, { ok: true, task: task.id });
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
// 目标语法两态:GitHub 地址(owner/repo、github:、https/ssh 链接等任意写法)→ 显式
|
|
524
|
+
// HTTPS Git 源(走装前预检);npm 包名(@scope/name 或 name)→ 信任 registry 直接安装。
|
|
525
|
+
// githubRepoOf 先把各种 GitHub 地址归一成 owner/repo,让「输入地址即装」兼容粘贴完整链接。
|
|
526
|
+
const gitRepo = githubRepoOf(rawRepo);
|
|
527
|
+
const repoTarget = gitRepo !== null ? githubTarget(gitRepo) : null;
|
|
528
|
+
let target = repoTarget ?? (validPackageName(rawRepo) ? rawRepo : null);
|
|
177
529
|
if (target === null) {
|
|
178
530
|
sendJson(response, 400, { error: 'unsupported install target' });
|
|
179
531
|
return;
|
|
180
532
|
}
|
|
181
533
|
// npm 优先:git 目标先反查该仓库的官方 npm 包,命中则改走 npm 通道。
|
|
182
|
-
// git
|
|
183
|
-
//
|
|
184
|
-
//
|
|
534
|
+
// git 分发常缺构建产物/子模块,npm 包是作者发布的完整产物;未命中或
|
|
535
|
+
// 查询失败保留 github 直装(不阻塞安装,预检仍会拦截缺入口文件的情况)。
|
|
536
|
+
// 通用机制,不针对具体插件。
|
|
185
537
|
// 反查本身计入「已尝试的安装方式」:组织 scope 与 GitHub 用户名不一致时仅凭
|
|
186
538
|
// 仓库名猜不到包名,失败提 Issue 时作者看到我们查过的命令就能直接指认正确包名。
|
|
187
539
|
const attempts = [];
|
|
188
540
|
if (repoTarget !== null) {
|
|
189
|
-
|
|
541
|
+
// 反查/记录统一用归一化后的 owner/repo(gitRepo),保证「输入完整链接」也走同一套 npm 反查
|
|
542
|
+
const repoIdentity = gitRepo ?? rawRepo;
|
|
543
|
+
const npmName = await resolveNpmPackage(repoIdentity, settings.npmRegistry);
|
|
190
544
|
if (npmName !== null) {
|
|
191
545
|
target = npmName;
|
|
192
|
-
attempts.push(`npm registry search: \`npm search repository:${
|
|
546
|
+
attempts.push(`npm registry search: \`npm search repository:${repoIdentity}\` → found \`${npmName}\``);
|
|
193
547
|
// 持久化 repo → npm 包名映射:目录数据未下发 npmPackage(组织 scope 与 GitHub
|
|
194
548
|
// 用户名不一致)时,客户端仍能通过 /installed 的 versions 把依赖 key 匹配回仓库,
|
|
195
549
|
// 列表立即显示「已安装」,避免安装成功后仍显示可安装
|
|
196
|
-
recordResolvedNpmPackage(profile,
|
|
550
|
+
recordResolvedNpmPackage(profile, repoIdentity, npmName);
|
|
197
551
|
}
|
|
198
552
|
else {
|
|
199
|
-
attempts.push(`npm registry search: \`npm search repository:${
|
|
553
|
+
attempts.push(`npm registry search: \`npm search repository:${repoIdentity}\` → no matching package found (falling back to git install)`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// 命令行安装(目录外)按通道门禁:GitHub 源码通道受「启用 GitHub 源码安装」控制,
|
|
557
|
+
// npm 通道受「启用 NPM 安装」控制 —— 目录插件安装走目录白名单,不受开关影响。
|
|
558
|
+
// 通道判定看反查后的最终 target:仍是 git 目标 = 走 GitHub 源码,否则走 npm。
|
|
559
|
+
if (source !== 'catalog') {
|
|
560
|
+
const isGitChannel = repoTarget !== null && target === repoTarget;
|
|
561
|
+
if (isGitChannel && !settings.enableGitInstall) {
|
|
562
|
+
sendJson(response, 403, { error: 'git installs are disabled by the security settings' });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (!isGitChannel && !settings.enableNpmInstall) {
|
|
566
|
+
sendJson(response, 403, { error: 'npm installs are disabled by the security settings' });
|
|
567
|
+
return;
|
|
200
568
|
}
|
|
201
569
|
}
|
|
202
570
|
// 重复安装防护:非更新请求命中已安装目标时直接拒绝,不重复跑 CLI
|
|
@@ -247,7 +615,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
247
615
|
// 已尝试的安装方式(npm 反查):实际执行命令由 spawn 时追加,失败 issue 一并展示
|
|
248
616
|
attempts,
|
|
249
617
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
250
|
-
env:
|
|
618
|
+
env: mutationEnv(settings),
|
|
251
619
|
});
|
|
252
620
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
253
621
|
}
|
|
@@ -294,7 +662,7 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
294
662
|
// 运行中 loader:卸载成功后主动移除条目,立即生效、无需重启
|
|
295
663
|
uninstallLoader: loader,
|
|
296
664
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
297
|
-
env:
|
|
665
|
+
env: mutationEnv(loadSettings(profile)),
|
|
298
666
|
});
|
|
299
667
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
300
668
|
}
|
|
@@ -418,15 +786,63 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
418
786
|
}
|
|
419
787
|
},
|
|
420
788
|
}),
|
|
789
|
+
webServer.register({
|
|
790
|
+
kind: 'exact',
|
|
791
|
+
path: '/dsh-plugin-hub/open-path',
|
|
792
|
+
handler: async (request, response) => {
|
|
793
|
+
if (!requireTrustedPost(request, response))
|
|
794
|
+
return;
|
|
795
|
+
// 在系统文件管理器里定位并打开已安装插件目录(详情视图「在文件夹中显示」)。
|
|
796
|
+
// 只接受已安装依赖的包名、服务端自行拼接路径——杜绝任意路径注入
|
|
797
|
+
try {
|
|
798
|
+
const body = await readJsonBody(request);
|
|
799
|
+
const name = typeof body === 'object' && body !== null && typeof body.name === 'string'
|
|
800
|
+
? body.name
|
|
801
|
+
: '';
|
|
802
|
+
if (!validPackageName(name) || readInstalled(profile)[name] === undefined) {
|
|
803
|
+
sendJson(response, 400, { error: 'plugin is not installed' });
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const dir = join(profileDirectory(profile), 'node_modules', name);
|
|
807
|
+
// macOS Finder 用 -R 定位选中;Windows explorer 用 /select,;Linux 打开目录(xdg-open)
|
|
808
|
+
const bin = process.platform === 'darwin' ? 'open'
|
|
809
|
+
: process.platform === 'win32' ? 'explorer'
|
|
810
|
+
: 'xdg-open';
|
|
811
|
+
const args = process.platform === 'darwin' ? ['-R', dir]
|
|
812
|
+
: process.platform === 'win32' ? ['/select,', dir]
|
|
813
|
+
: [dir];
|
|
814
|
+
spawn(bin, args, { detached: true, stdio: 'ignore' }).unref();
|
|
815
|
+
sendJson(response, 200, { ok: true });
|
|
816
|
+
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
}),
|
|
421
822
|
webServer.register({
|
|
422
823
|
kind: 'exact',
|
|
423
824
|
path: '/dsh-plugin-hub/installed',
|
|
424
825
|
handler: (request, response) => {
|
|
425
826
|
if (!requireMethod(request, response, 'GET'))
|
|
426
827
|
return;
|
|
427
|
-
// installed:依赖表(包名 → spec);versions:安装时记录的目录版本(repo →
|
|
428
|
-
//
|
|
429
|
-
|
|
828
|
+
// installed:依赖表(包名 → spec);versions:安装时记录的目录版本(repo → 版本);
|
|
829
|
+
// paths:每个依赖在系统上的安装目录(profile/node_modules/<包名>),详情视图展示用;
|
|
830
|
+
// loaded:已加载进运行中 loader 的包名(官方 ctx.loader.entries(),未重启装的新插件不在其中);
|
|
831
|
+
// dshCapable:真正的 dsh 插件(包内声明 dsh 配置 / 在 profile bundles 清单)——
|
|
832
|
+
// 非 dsh 插件(如 GitHub 官方示例仓库)装上也不会被宿主加载,客户端据此不再提示「待重启」。
|
|
833
|
+
// 客户端合并这些判断「是否有更新」「运行状态」并展示安装路径/时间等运行时信息。
|
|
834
|
+
const installed = readInstalled(profile);
|
|
835
|
+
const paths = {};
|
|
836
|
+
const loaded = [];
|
|
837
|
+
const dshCapable = [];
|
|
838
|
+
for (const name of Object.keys(installed)) {
|
|
839
|
+
paths[name] = join(profileDirectory(profile), 'node_modules', name);
|
|
840
|
+
if (isEntryLoaded(loader, name))
|
|
841
|
+
loaded.push(name);
|
|
842
|
+
if (isDshPlugin(profile, name))
|
|
843
|
+
dshCapable.push(name);
|
|
844
|
+
}
|
|
845
|
+
sendJson(response, 200, { profile, installed, versions: readInstalledVersions(profile), paths, loaded, dshCapable });
|
|
430
846
|
},
|
|
431
847
|
}),
|
|
432
848
|
];
|
package/lib/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* web server service is available. The browser bundle (src/client) talks to
|
|
8
8
|
* these same-origin routes to perform real installs from inside the app.
|
|
9
9
|
*/
|
|
10
|
-
import { readProfileArg } from './services/install.js';
|
|
10
|
+
import { readProfileArg } from './services/install/install.js';
|
|
11
11
|
import { mountPluginHubRoutes } from './http/routes.js';
|
|
12
12
|
export const name = 'dsh-plugin';
|
|
13
13
|
/**
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { addPendingRestart, clearPendingRestart, listPendingRestarts } from '../profile/pending-restart.js';
|
|
2
|
+
export { dumpLoaderEntries, removeLoadedEntry } from '../loader.js';
|
|
3
|
+
export { addAllowBuildsKey, githubRepoOf, githubTarget, globalNpmPackagesOf, installTargetOf, parseAllowBuildsKey, readProfileArg, validPackageName } from '../profile/profile.js';
|
|
4
|
+
export { activeTask, cancelTask, getTask, hasQueuedTarget, hasRunningTask, runPluginMutation, startPluginMutation } from './task-queue.js';
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* 本机 npm 环境检测:判断一次安装失败是否源于「本机 npm 版本过低」。
|
|
7
|
+
*
|
|
8
|
+
* 背景:pnpm 在 git 源插件的 prepare 阶段会调 npm 装依赖,npm arborist 在解 peer 依赖时
|
|
9
|
+
* 有已知内部崩溃 —— `Cannot read properties of null (reading 'edgesOut')`(build-ideal-tree.js
|
|
10
|
+
* 的 #loadPeerSet),官方标记 Cannot Reproduce(npm/cli#8261、#9787),npm 11.6.0 起不再复现。
|
|
11
|
+
* 该报错会被 pnpm 包进 ERR_PNPM_PREPARE_PACKAGE 输出,若按插件打包问题引导提 Issue 就误导了用户。
|
|
12
|
+
*
|
|
13
|
+
* 这里的 `npmTooLowMarker` 是纯函数(输出文本 + 环境 → 标记行或 null),不触碰任务/队列,
|
|
14
|
+
* 由调用方(task-queue.ts)负责把标记追加到任务输出;spawn 读 npm 版本是唯一副作用,独立可测。
|
|
15
|
+
*/
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
/** npm arborist 内部崩溃特征:`Cannot read properties of null (reading 'edgesOut')`。 */
|
|
18
|
+
export const NPM_CRASH_EDGES_OUT_RE = /Cannot read properties of null \(reading 'edgesOut'\)|edgesOut/i;
|
|
19
|
+
/** 该缺陷在 npm 11.6.0 起不再复现;低于此版本遇到 edgesOut 崩溃即判「本机 npm 版本过低」。 */
|
|
20
|
+
export const NPM_MIN_VERSION = [11, 6, 0];
|
|
21
|
+
/** 读本机 npm 版本(major.minor.patch);npm 不在 PATH 或执行失败/超时时返回 null(不妄下结论)。 */
|
|
22
|
+
export function npmVersionOf(env) {
|
|
23
|
+
try {
|
|
24
|
+
const res = spawnSync('npm', ['-v'], { env, encoding: 'utf8', timeout: 10_000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
25
|
+
if (res.error !== undefined || res.status !== 0)
|
|
26
|
+
return null;
|
|
27
|
+
const m = /(\d+)\.(\d+)\.(\d+)/.exec(res.stdout ?? '');
|
|
28
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function belowMin(v) {
|
|
35
|
+
for (let i = 0; i < NPM_MIN_VERSION.length; i++) {
|
|
36
|
+
if (v[i] < NPM_MIN_VERSION[i])
|
|
37
|
+
return true;
|
|
38
|
+
if (v[i] > NPM_MIN_VERSION[i])
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 安装失败输出含 npm arborist edgesOut 崩溃特征,且本机 npm 版本低于阈值时,返回
|
|
45
|
+
* `[npm-too-low] npm@X.Y.Z …` 标记行;否则返回 null。前端据此展示「本机 npm 版本过低」
|
|
46
|
+
* 的准确原因,避免该报错被误判成插件打包/分发问题。
|
|
47
|
+
*/
|
|
48
|
+
export function npmTooLowMarker(output, env) {
|
|
49
|
+
if (!NPM_CRASH_EDGES_OUT_RE.test(output))
|
|
50
|
+
return null;
|
|
51
|
+
const v = npmVersionOf(env);
|
|
52
|
+
if (v === null || !belowMin(v))
|
|
53
|
+
return null;
|
|
54
|
+
return `[npm-too-low] npm@${v.join('.')} — npm arborist crashed while resolving peer deps ('edgesOut'); local npm is below ${NPM_MIN_VERSION.join('.')}; upgrade npm (npm install -g npm@latest) and retry`;
|
|
55
|
+
}
|