dsh-plugin 1.1.1 → 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/README.en.md +5 -5
- package/README.md +5 -5
- package/client/client.js +4772 -1659
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +451 -28
- package/lib/index.js +5 -1
- package/lib/services/{install-types.js → install/install-types.js} +8 -2
- 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} +11 -5
- package/lib/services/{preflight.js → install/preflight.js} +23 -7
- package/lib/services/{task-queue.js → install/task-queue.js} +166 -71
- package/lib/services/loader.js +60 -1
- package/lib/services/log.js +138 -0
- package/lib/services/probe.js +157 -0
- package/lib/services/{installed-versions.js → profile/installed-versions.js} +5 -1
- package/lib/services/{pending-restart.js → profile/pending-restart.js} +1 -1
- package/lib/services/{profile.js → profile/profile.js} +59 -4
- package/lib/services/{progress.js → profile/progress.js} +4 -0
- package/lib/services/settings.js +60 -0
- package/lib/types/http/routes.d.ts +1 -1
- package/lib/types/index.d.ts +5 -1
- package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +13 -4
- package/lib/types/services/{install.d.ts → install/install.d.ts} +9 -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 -4
- 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} +9 -3
- package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +6 -2
- package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +4 -0
- package/lib/types/services/settings.d.ts +24 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +346 -121
- package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +15 -7
- package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +10 -5
- package/src/client/components/layout/CatalogControls.tsx +80 -0
- package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +8 -4
- package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +7 -3
- package/src/client/components/layout/SectionTabs.tsx +60 -0
- package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +11 -7
- package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +9 -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} +51 -20
- package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +6 -2
- package/src/client/components/{modals.tsx → modals/modals.tsx} +121 -40
- package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +18 -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 +77 -117
- 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 +168 -47
- package/src/client/index.tsx +4 -0
- package/src/client/locales.ts +365 -39
- package/src/client/logic/constants.ts +87 -0
- package/src/client/{lib → logic}/failures.ts +99 -74
- package/src/client/{lib → logic}/format.ts +4 -0
- 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/{lib → logic}/renderMarkdown.ts +4 -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 +46 -41
- package/src/client/styles/Header.module.css +105 -133
- package/src/client/styles/InstalledView.module.css +447 -0
- package/src/client/styles/List.module.css +11 -1
- package/src/client/styles/LogsView.module.css +503 -0
- package/src/client/styles/Modal.module.css +357 -71
- 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 +6 -1
- package/src/client/types.ts +28 -4
- package/src/server/http/routes.ts +426 -28
- package/src/server/index.ts +5 -1
- package/src/server/services/{install-types.ts → install/install-types.ts} +13 -5
- package/src/server/services/{install.ts → install/install.ts} +9 -5
- package/src/server/services/install/npm-check.ts +53 -0
- package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +11 -5
- package/src/server/services/{preflight.ts → install/preflight.ts} +24 -7
- package/src/server/services/{task-queue.ts → install/task-queue.ts} +168 -68
- package/src/server/services/loader.ts +59 -1
- package/src/server/services/log.ts +167 -0
- package/src/server/services/probe.ts +157 -0
- package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +5 -1
- package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +9 -3
- package/src/server/services/{profile.ts → profile/profile.ts} +58 -4
- package/src/server/services/{progress.ts → profile/progress.ts} +4 -0
- package/src/server/services/settings.ts +79 -0
- package/lib/services/install.js +0 -4
- package/src/client/components/CatalogControls.tsx +0 -104
- package/src/client/components/icons.tsx +0 -125
- package/src/client/lib/catalog.ts +0 -272
- package/src/client/lib/env.ts +0 -16
- /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/http/routes.js
CHANGED
|
@@ -1,17 +1,25 @@
|
|
|
1
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
|
+
*
|
|
2
6
|
* Local HTTP routes exposing real installs to the in-app Plugin Hub UI.
|
|
3
7
|
* The client fetches the same-origin `/dsh-plugin-hub/*` endpoints; the
|
|
4
8
|
* install handler validates the target, then spawns the official dsh CLI
|
|
5
|
-
* (see services/install.ts) and reports the captured result back.
|
|
9
|
+
* (see services/install/install.ts) and reports the captured result back.
|
|
6
10
|
*/
|
|
7
|
-
import { readFileSync } from 'node:fs';
|
|
11
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
8
12
|
import { homedir, release } from 'node:os';
|
|
9
13
|
import { dirname, join, resolve } from 'node:path';
|
|
10
14
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
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';
|
|
15
23
|
const PROFILE_RE = /^[A-Za-z0-9_-]+$/;
|
|
16
24
|
const BODY_LIMIT_BYTES = 4 * 1024;
|
|
17
25
|
const COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
|
|
@@ -64,11 +72,30 @@ function hostEnv(profile) {
|
|
|
64
72
|
dshHome: process.env.DSH_HOME ?? join(homedir(), '.dsh'),
|
|
65
73
|
};
|
|
66
74
|
}
|
|
67
|
-
/**
|
|
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
|
+
}
|
|
91
|
+
/** Match a GitHub install target against the installed table; returns the package name when already installed. */
|
|
68
92
|
function installedByRepo(installed, target) {
|
|
69
|
-
const
|
|
93
|
+
const repo = githubRepoOf(target);
|
|
94
|
+
if (repo === null)
|
|
95
|
+
return null;
|
|
96
|
+
const needle = repo.toLowerCase();
|
|
70
97
|
for (const [name, spec] of Object.entries(installed)) {
|
|
71
|
-
if (spec
|
|
98
|
+
if (githubRepoOf(spec)?.toLowerCase() === needle)
|
|
72
99
|
return name;
|
|
73
100
|
}
|
|
74
101
|
return null;
|
|
@@ -133,6 +160,14 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
133
160
|
if (!PROFILE_RE.test(profile))
|
|
134
161
|
throw new Error(`invalid profile name: ${profile}`);
|
|
135
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
|
+
});
|
|
136
171
|
const disposers = [
|
|
137
172
|
webServer.register({
|
|
138
173
|
kind: 'exact',
|
|
@@ -143,6 +178,283 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
143
178
|
sendJson(response, 200, { entries: dumpLoaderEntries(loader) });
|
|
144
179
|
},
|
|
145
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
|
+
}),
|
|
146
458
|
webServer.register({
|
|
147
459
|
kind: 'exact',
|
|
148
460
|
path: '/dsh-plugin-hub/install',
|
|
@@ -156,46 +468,109 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
156
468
|
const mode = body !== null && typeof body === 'object' && body.mode === 'update'
|
|
157
469
|
? 'update'
|
|
158
470
|
: 'install';
|
|
471
|
+
// installTargetOf 先把完整 DSH 命令(`dsh plugin [--profile <p>] add <target>`)剥成裸目标,
|
|
472
|
+
// 兼容用户直接粘贴命令行(防输错),真正语法判定交给下面 githubRepoOf / validPackageName。
|
|
159
473
|
const rawRepo = typeof body === 'object' && body !== null && typeof body.repo === 'string'
|
|
160
|
-
? body.repo
|
|
474
|
+
? installTargetOf(body.repo)
|
|
161
475
|
: '';
|
|
162
476
|
// 前端决策的展示用仓库名(owner/repo):npm 安装时 body.repo 是包名,display 用于
|
|
163
477
|
// 待重启行 / 任务恢复时展示仓库名,保证用户无感知(安装通道变化不改变所见目标)
|
|
164
478
|
const displayRepo = typeof body === 'object' && body !== null && typeof body.display === 'string'
|
|
165
479
|
? body.display.trim()
|
|
166
480
|
: rawRepo;
|
|
167
|
-
//
|
|
168
|
-
const
|
|
169
|
-
|
|
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);
|
|
170
529
|
if (target === null) {
|
|
171
530
|
sendJson(response, 400, { error: 'unsupported install target' });
|
|
172
531
|
return;
|
|
173
532
|
}
|
|
174
533
|
// npm 优先:git 目标先反查该仓库的官方 npm 包,命中则改走 npm 通道。
|
|
175
|
-
// git
|
|
176
|
-
//
|
|
177
|
-
//
|
|
534
|
+
// git 分发常缺构建产物/子模块,npm 包是作者发布的完整产物;未命中或
|
|
535
|
+
// 查询失败保留 github 直装(不阻塞安装,预检仍会拦截缺入口文件的情况)。
|
|
536
|
+
// 通用机制,不针对具体插件。
|
|
178
537
|
// 反查本身计入「已尝试的安装方式」:组织 scope 与 GitHub 用户名不一致时仅凭
|
|
179
538
|
// 仓库名猜不到包名,失败提 Issue 时作者看到我们查过的命令就能直接指认正确包名。
|
|
180
539
|
const attempts = [];
|
|
181
540
|
if (repoTarget !== null) {
|
|
182
|
-
|
|
541
|
+
// 反查/记录统一用归一化后的 owner/repo(gitRepo),保证「输入完整链接」也走同一套 npm 反查
|
|
542
|
+
const repoIdentity = gitRepo ?? rawRepo;
|
|
543
|
+
const npmName = await resolveNpmPackage(repoIdentity, settings.npmRegistry);
|
|
183
544
|
if (npmName !== null) {
|
|
184
545
|
target = npmName;
|
|
185
|
-
attempts.push(`npm registry search: \`npm search repository:${
|
|
546
|
+
attempts.push(`npm registry search: \`npm search repository:${repoIdentity}\` → found \`${npmName}\``);
|
|
186
547
|
// 持久化 repo → npm 包名映射:目录数据未下发 npmPackage(组织 scope 与 GitHub
|
|
187
548
|
// 用户名不一致)时,客户端仍能通过 /installed 的 versions 把依赖 key 匹配回仓库,
|
|
188
549
|
// 列表立即显示「已安装」,避免安装成功后仍显示可安装
|
|
189
|
-
recordResolvedNpmPackage(profile,
|
|
550
|
+
recordResolvedNpmPackage(profile, repoIdentity, npmName);
|
|
190
551
|
}
|
|
191
552
|
else {
|
|
192
|
-
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;
|
|
193
568
|
}
|
|
194
569
|
}
|
|
195
570
|
// 重复安装防护:非更新请求命中已安装目标时直接拒绝,不重复跑 CLI
|
|
196
571
|
// (否则 CLI 会因「已存在」失败,且用户看到的是莫名其妙的报错);
|
|
197
572
|
// 更新请求(mode: 'update')放行,CLI 的 add 对已存在依赖是 pnpm 原位覆盖重装。
|
|
198
|
-
// 命中判定双通道:npm 包名按 dependencies 键直接命中,
|
|
573
|
+
// 命中判定双通道:npm 包名按 dependencies 键直接命中,GitHub 目标按仓库身份匹配。
|
|
199
574
|
const installed = readInstalled(profile);
|
|
200
575
|
const already = installedByRepo(installed, target) ?? (installed[target] !== undefined ? target : null);
|
|
201
576
|
if (already !== null && mode !== 'update') {
|
|
@@ -236,11 +611,11 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
236
611
|
profile,
|
|
237
612
|
target,
|
|
238
613
|
// npm 安装时 target 是包名:displayTarget 固定展示仓库名,待重启行/前端恢复不受通道变化影响
|
|
239
|
-
displayTarget:
|
|
614
|
+
displayTarget: githubRepoOf(displayRepo) ?? undefined,
|
|
240
615
|
// 已尝试的安装方式(npm 反查):实际执行命令由 spawn 时追加,失败 issue 一并展示
|
|
241
616
|
attempts,
|
|
242
617
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
243
|
-
env:
|
|
618
|
+
env: mutationEnv(settings),
|
|
244
619
|
});
|
|
245
620
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
246
621
|
}
|
|
@@ -283,11 +658,11 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
283
658
|
profile,
|
|
284
659
|
target: name,
|
|
285
660
|
// 卸载的 mutation 目标是 npm 包名;待重启行要显示 owner/repo(与安装行一致),非法/缺失时回退包名
|
|
286
|
-
displayTarget:
|
|
661
|
+
displayTarget: githubRepoOf(repo) ?? undefined,
|
|
287
662
|
// 运行中 loader:卸载成功后主动移除条目,立即生效、无需重启
|
|
288
663
|
uninstallLoader: loader,
|
|
289
664
|
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
290
|
-
env:
|
|
665
|
+
env: mutationEnv(loadSettings(profile)),
|
|
291
666
|
});
|
|
292
667
|
sendJson(response, 200, { ok: true, task: task.id });
|
|
293
668
|
}
|
|
@@ -411,15 +786,63 @@ export function mountPluginHubRoutes(webServer, profile, loader) {
|
|
|
411
786
|
}
|
|
412
787
|
},
|
|
413
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
|
+
}),
|
|
414
822
|
webServer.register({
|
|
415
823
|
kind: 'exact',
|
|
416
824
|
path: '/dsh-plugin-hub/installed',
|
|
417
825
|
handler: (request, response) => {
|
|
418
826
|
if (!requireMethod(request, response, 'GET'))
|
|
419
827
|
return;
|
|
420
|
-
// installed:依赖表(包名 → spec);versions:安装时记录的目录版本(repo →
|
|
421
|
-
//
|
|
422
|
-
|
|
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 });
|
|
423
846
|
},
|
|
424
847
|
}),
|
|
425
848
|
];
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
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
|
+
*
|
|
2
6
|
* dsh-plugin host entry: mounts the Plugin Hub HTTP routes once the host
|
|
3
7
|
* web server service is available. The browser bundle (src/client) talks to
|
|
4
8
|
* these same-origin routes to perform real installs from inside the app.
|
|
5
9
|
*/
|
|
6
|
-
import { readProfileArg } from './services/install.js';
|
|
10
|
+
import { readProfileArg } from './services/install/install.js';
|
|
7
11
|
import { mountPluginHubRoutes } from './http/routes.js';
|
|
8
12
|
export const name = 'dsh-plugin';
|
|
9
13
|
/**
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
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
|
+
* 安装/卸载运行时的公共类型与常量(无依赖,供各服务模块复用)。
|
|
7
|
+
*/
|
|
8
|
+
/** Grammar of a catalog GitHub repository identity (`owner/repo`). */
|
|
3
9
|
export const REPO_RE = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
4
10
|
/** Grammar of an npm package name (used for uninstall targets). */
|
|
5
11
|
export const PACKAGE_RE = /^(?:@[a-z0-9._-]+\/)?[A-Za-z0-9._-]+$/;
|
|
@@ -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';
|