dsh-plugin 2.3.1 → 2.3.3
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 +15 -21
- package/README.md +15 -21
- package/client/client.js +412 -382
- package/client/client.js.map +1 -1
- package/lib/http/catalog.js +4 -4
- package/lib/http/diagnostics.js +4 -4
- package/lib/http/install.js +21 -10
- package/lib/http/npm-release-age.js +9 -3
- package/lib/http/shared.js +1 -1
- package/lib/services/host-version.js +2 -2
- package/lib/services/install/dsh-compat.js +1 -1
- package/lib/services/install/npm-resolve.js +1 -1
- package/lib/services/install/operation-log.js +3 -3
- package/lib/services/install/preflight.js +1 -1
- package/lib/services/install/registry-fallback.js +3 -3
- package/lib/services/install/release-target.js +2 -2
- package/lib/services/install/task-queue.js +35 -20
- package/lib/services/loader.js +8 -8
- package/lib/services/npm-release-age.js +46 -9
- package/lib/services/probe.js +12 -12
- package/lib/services/profile/installed-versions.js +2 -2
- package/lib/services/profile/profile.js +2 -2
- package/lib/types/services/host-version.d.ts +1 -1
- package/lib/types/services/install/install-types.d.ts +5 -5
- package/lib/types/services/install/operation-log.d.ts +1 -1
- package/lib/types/services/install/preflight.d.ts +1 -1
- package/lib/types/services/install/registry-fallback.d.ts +1 -1
- package/lib/types/services/install/release-target.d.ts +1 -1
- package/lib/types/services/install/task-queue.d.ts +10 -7
- package/lib/types/services/loader.d.ts +5 -5
- package/lib/types/services/npm-release-age.d.ts +14 -2
- package/lib/types/services/probe.d.ts +4 -4
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +59 -28
- package/src/client/components/catalog/CatalogList.tsx +1 -1
- package/src/client/components/catalog/PluginCard.tsx +3 -3
- package/src/client/components/modals/AboutModal.tsx +2 -2
- package/src/client/components/modals/ConfirmDialog.tsx +2 -2
- package/src/client/components/modals/HubUpdateModal.tsx +3 -3
- package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
- package/src/client/components/modals/LogsPathModal.tsx +1 -1
- package/src/client/components/modals/NotificationsModal.tsx +2 -2
- package/src/client/components/modals/modals.tsx +13 -11
- package/src/client/components/views/CustomInstallView.tsx +2 -2
- package/src/client/components/views/InstalledView.tsx +2 -2
- package/src/client/components/views/SettingsView.tsx +3 -3
- package/src/client/data/catalog.ts +1 -1
- package/src/client/data/host.ts +3 -1
- package/src/client/data/hub.ts +3 -3
- package/src/client/hooks/useCatalog.ts +14 -14
- package/src/client/hooks/useSettings.ts +2 -2
- package/src/client/hooks/useTaskQueue.ts +31 -24
- package/src/client/locales.ts +21 -21
- package/src/client/logic/constants.ts +1 -1
- package/src/client/logic/ensure-plugin-css.ts +2 -2
- package/src/client/logic/failures.ts +9 -9
- package/src/client/logic/install-command.ts +5 -3
- package/src/client/logic/installed.ts +1 -1
- package/src/client/logic/release-age.ts +10 -10
- package/src/client/logic/urls.ts +3 -3
- package/src/client/styles/Header.module.css +1 -1
- package/src/client/styles/Modal.module.css +2 -2
- package/src/client/styles/SectionTabs.module.css +1 -1
- package/src/client/styles/tokens.module.css +1 -1
- package/src/client/types.ts +4 -4
- package/src/server/http/catalog.ts +4 -4
- package/src/server/http/diagnostics.ts +4 -4
- package/src/server/http/install.ts +20 -10
- package/src/server/http/npm-release-age.ts +9 -3
- package/src/server/http/shared.ts +1 -1
- package/src/server/services/host-version.ts +2 -2
- package/src/server/services/install/dsh-compat.ts +1 -1
- package/src/server/services/install/install-types.ts +5 -5
- package/src/server/services/install/npm-resolve.ts +1 -1
- package/src/server/services/install/operation-log.ts +3 -3
- package/src/server/services/install/preflight.ts +1 -1
- package/src/server/services/install/registry-fallback.ts +3 -3
- package/src/server/services/install/release-target.ts +2 -2
- package/src/server/services/install/task-queue.ts +40 -26
- package/src/server/services/loader.ts +10 -10
- package/src/server/services/npm-release-age.ts +50 -10
- package/src/server/services/probe.ts +12 -12
- package/src/server/services/profile/installed-versions.ts +2 -2
- package/src/server/services/profile/profile.ts +2 -2
|
@@ -148,7 +148,7 @@ function pushLine(task: InstallTask, line: string): void {
|
|
|
148
148
|
if (estimate > 0) {
|
|
149
149
|
task.progress = Math.max(task.progress, estimate)
|
|
150
150
|
} else if (task.status === 'running' && task.progress < 85) {
|
|
151
|
-
//
|
|
151
|
+
// 补充:行无解析格式也随输出量推进,保证进度条始终在动(不会卡在 0)
|
|
152
152
|
task.progress = Math.max(task.progress, 6 + Math.min(79, task.lines.length * 2))
|
|
153
153
|
}
|
|
154
154
|
}
|
|
@@ -220,7 +220,7 @@ export function desktopCliBootstrap(entry: string): string | null {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
|
-
* 传给 CLI
|
|
223
|
+
* 传给 CLI 的安装目标:锁定版本优先(Hub 自我更新提示的是已过发布冷却期的版本),
|
|
224
224
|
* 其次 npm 的 add/update 都显式 `@latest`;git 源、全局安装、已带版本/tag 的 spec 保持原样。
|
|
225
225
|
*/
|
|
226
226
|
export function npmInstallTargetOf(target: string, opts: { update: boolean; global: boolean; pinned?: string; add?: boolean }): string {
|
|
@@ -231,6 +231,13 @@ export function npmInstallTargetOf(target: string, opts: { update: boolean; glob
|
|
|
231
231
|
return `${target}@latest`
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
/** 重复卸载:目标已不在 profile 依赖里,pnpm 报「没有这个依赖」。
|
|
235
|
+
* 结果与卸载成功等价 —— 按失败上报会弹出错误并自动提一条不存在的 Issue。 */
|
|
236
|
+
export function isAlreadyRemoved(action: 'add' | 'remove' | 'update', output: string): boolean {
|
|
237
|
+
if (action !== 'remove') return false
|
|
238
|
+
return /ERR_PNPM_CANNOT_REMOVE_MISSING_DEPS|no such dependency found/i.test(output)
|
|
239
|
+
}
|
|
240
|
+
|
|
234
241
|
function stopChild(child: ReturnType<typeof spawn>): void {
|
|
235
242
|
if (process.platform === 'win32' && child.pid !== undefined) {
|
|
236
243
|
const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' })
|
|
@@ -280,7 +287,7 @@ export function startPluginMutation(options: {
|
|
|
280
287
|
globalNpm?: string[]
|
|
281
288
|
/** 待重启行的展示目标(owner/repo):卸载时用于把 npm 包名映射回仓库名 */
|
|
282
289
|
displayTarget?: string
|
|
283
|
-
/** 运行中 loader
|
|
290
|
+
/** 运行中 loader:安装成功后热加载新条目、卸载成功后停用旧条目,两类操作都尽量立即生效
|
|
284
291
|
* (缺失时安装/卸载仍需重启) */
|
|
285
292
|
loader?: LoaderHandle
|
|
286
293
|
/** 入队前已尝试的安装方式:失败提 Issue 时如实展示;实际执行的命令由 spawn 时追加 */
|
|
@@ -290,10 +297,10 @@ export function startPluginMutation(options: {
|
|
|
290
297
|
/** 自定义安装入口渠道(NPM 包卡片):任务输出行与系统日志据此溯源「从哪个入口发起」;
|
|
291
298
|
* 目录插件安装不传 */
|
|
292
299
|
installChannel?: 'npm'
|
|
293
|
-
/** npm
|
|
300
|
+
/** npm 源候选(首选在前):传输层失败时按顺序换源重试,空/单个表示不换源 */
|
|
294
301
|
registries?: string[]
|
|
295
|
-
/** 精确安装 spec(如 `dsh-plugin@2.0.5`):Hub
|
|
296
|
-
*
|
|
302
|
+
/** 精确安装 spec(如 `dsh-plugin@2.0.5`):Hub 自我更新提示的是已过发布冷却期的版本,
|
|
303
|
+
* 必须锁定安装,不能再用 @latest 被带回冷却期内的最新版 */
|
|
297
304
|
installSpec?: string
|
|
298
305
|
}): InstallTask {
|
|
299
306
|
// 卸载入队时先快照「该包是否带客户端 UI」:卸载完成后包已从磁盘删除,读不到 package.json,
|
|
@@ -414,7 +421,7 @@ function spawnMutation(options: {
|
|
|
414
421
|
task?: InstallTask
|
|
415
422
|
/** 待重启行的展示目标(owner/repo):与 mutation 目标解耦——卸载的 mutation 目标是 npm 包名,展示要回退到仓库名 */
|
|
416
423
|
displayTarget?: string
|
|
417
|
-
/** 运行中 loader
|
|
424
|
+
/** 运行中 loader:安装成功后热加载新条目、卸载成功后停用旧条目(缺失时仍需重启) */
|
|
418
425
|
loader?: LoaderHandle
|
|
419
426
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
420
427
|
globalNpm?: string[]
|
|
@@ -439,10 +446,10 @@ function spawnMutation(options: {
|
|
|
439
446
|
}
|
|
440
447
|
// 安装/更新 npm 包都显式 @latest:pnpm add <pkg>(无版本)对已存在的精确 spec 是幂等的,
|
|
441
448
|
// 会原样留用旧版本(更新不生效,普通安装也可能装回不支持当前宿主的旧包)。
|
|
442
|
-
// update 动作同样执行 `add <target>@latest`,CLI
|
|
449
|
+
// update 动作同样执行 `add <target>@latest`,CLI 动词原样传递 add,不与官方 update 动词分叉。
|
|
443
450
|
const isUpdate = action === 'update' || updateNpm === true
|
|
444
451
|
const cliAction = action === 'update' ? 'add' : action
|
|
445
|
-
//
|
|
452
|
+
// 锁定版本优先于 @latest:提示的是「已过冷却期」的版本,装 latest 会被 pnpm 拒绝
|
|
446
453
|
const npmUpdateTarget = npmInstallTargetOf(target, { update: isUpdate, global: isGlobalNpm, pinned: installSpec, add: cliAction === 'add' })
|
|
447
454
|
const args = isGlobalNpm
|
|
448
455
|
? ['install', '-g', ...(globalNpm ?? [])]
|
|
@@ -454,7 +461,7 @@ function spawnMutation(options: {
|
|
|
454
461
|
if (task && !task.attempts.includes(executed)) {
|
|
455
462
|
task.attempts.push(executed)
|
|
456
463
|
}
|
|
457
|
-
// 单次 spawn 并捕获结果:流式输出打日志 +
|
|
464
|
+
// 单次 spawn 并捕获结果:流式输出打日志 + 超时、取消时收尾,close 后 resolve。
|
|
458
465
|
// 不做任何终态后处理(exitCode/status/needsRestart 由下方统一收尾),
|
|
459
466
|
// 这样全局 npm 首次失败后可以干净地换参数重试一次。
|
|
460
467
|
const spawnOnce = (attemptArgs: string[], registry?: string): Promise<InstallResult> => new Promise((resolveOnce) => {
|
|
@@ -497,7 +504,7 @@ function spawnMutation(options: {
|
|
|
497
504
|
}
|
|
498
505
|
}
|
|
499
506
|
// invocation.env 是「必须叠加」的变量(打包桌面端的 ELECTRON_RUN_AS_NODE):
|
|
500
|
-
// 没有它时保持 env
|
|
507
|
+
// 没有它时保持 env 原样传递,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
|
|
501
508
|
// registry 最后写入:换源重试只改这一层,pnpm/npm 都认 npm_config_registry。
|
|
502
509
|
const childEnv: NodeJS.ProcessEnv = { ...(env ?? process.env), ...(invocation.env ?? {}) }
|
|
503
510
|
if (registry !== undefined) {
|
|
@@ -534,13 +541,13 @@ function spawnMutation(options: {
|
|
|
534
541
|
}
|
|
535
542
|
if (task) runningChildren.set(task.id, child)
|
|
536
543
|
|
|
537
|
-
//
|
|
544
|
+
// 最终超时定时器句柄:整段超时 / exit 后 close 宽限 / 强制收尾
|
|
538
545
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
539
546
|
let progressTimer: ReturnType<typeof setInterval> | undefined
|
|
540
547
|
let exitWatch: ReturnType<typeof setTimeout> | undefined
|
|
541
548
|
let forceSettle: ReturnType<typeof setTimeout> | undefined
|
|
542
549
|
|
|
543
|
-
// 唯一收尾入口:close / error /
|
|
550
|
+
// 唯一收尾入口:close / error / 最终超时 三者只放行一次,确保任务持续推进
|
|
544
551
|
const settleOnce = (result: InstallResult): void => {
|
|
545
552
|
if (settled) return
|
|
546
553
|
settled = true
|
|
@@ -556,7 +563,7 @@ function spawnMutation(options: {
|
|
|
556
563
|
timedOut = true
|
|
557
564
|
stopChild(child)
|
|
558
565
|
}, timeoutMs)
|
|
559
|
-
//
|
|
566
|
+
// 时间补充:即使 CLI 悄悄不吐进度行,进度条也单调推进,不会卡在 0
|
|
560
567
|
progressTimer = setInterval(() => {
|
|
561
568
|
if (task && task.status === 'running' && task.progress < 85) task.progress += 1
|
|
562
569
|
}, 500)
|
|
@@ -565,7 +572,7 @@ function spawnMutation(options: {
|
|
|
565
572
|
if (kind === 'stdout') stdout = (stdout + text).slice(-CAPTURE_LIMIT_BYTES)
|
|
566
573
|
else stderr = (stderr + text).slice(-CAPTURE_LIMIT_BYTES)
|
|
567
574
|
if (task) {
|
|
568
|
-
//
|
|
575
|
+
// 原生原样传递 pnpm/npm 输出:stdout/stderr 原样入日志,不做格式化,保证报错信息完整可见
|
|
569
576
|
for (const line of text.split(/\r?\n|\r/)) {
|
|
570
577
|
if (line.trim() !== '') pushLine(task, line.trimEnd())
|
|
571
578
|
}
|
|
@@ -582,7 +589,7 @@ function spawnMutation(options: {
|
|
|
582
589
|
if (settled) return
|
|
583
590
|
if (task && task.status !== 'cancelled') pushLine(task, '[stale pipe] force closing child')
|
|
584
591
|
stopChild(child)
|
|
585
|
-
// 清理后极端情况下 close 仍未触发(kill
|
|
592
|
+
// 清理后极端情况下 close 仍未触发(kill 失败):再强制收尾一次
|
|
586
593
|
forceSettle = setTimeout(() => {
|
|
587
594
|
if (settled) return
|
|
588
595
|
if (task && task.status !== 'cancelled') {
|
|
@@ -642,7 +649,7 @@ function spawnMutation(options: {
|
|
|
642
649
|
if (task) pushLine(task, '[npm] peer-set resolution crashed npm (known npm bug) — retrying with --legacy-peer-deps')
|
|
643
650
|
result = await spawnOnce(retryArgs, activeRegistry)
|
|
644
651
|
}
|
|
645
|
-
// 超时被强杀 ≠
|
|
652
|
+
// 超时被强杀 ≠ 没装上:命中的安装可能已经写入磁盘(日志里 pnpm 已打印 Done,只是 dsh 进程迟迟不退出
|
|
646
653
|
// 而被我们超时掐掉)。判失败之前先复核 profile —— 目标已在依赖里、且声明分发的入口文件都在,
|
|
647
654
|
// 就按成功收尾,避免「其实装好了却报安装失败」并被自动归档成未知失败。
|
|
648
655
|
if (result.timedOut && !wasInstalled && action !== 'remove' && !isGlobalNpm && task?.status !== 'cancelled') {
|
|
@@ -652,6 +659,13 @@ function spawnMutation(options: {
|
|
|
652
659
|
if (task) pushLine(task, '[timeout] the install had already completed — treating it as a success')
|
|
653
660
|
}
|
|
654
661
|
}
|
|
662
|
+
// 重复卸载:依赖已经不在 profile 里,pnpm 拒绝移除并报「没有这个依赖」。
|
|
663
|
+
// 与卸载成功同义(用户要的结果已达成),按成功收尾,避免误报失败、误提 Issue。
|
|
664
|
+
if (!result.timedOut && task?.status !== 'cancelled'
|
|
665
|
+
&& isAlreadyRemoved(action, `${result.stderr}\n${result.stdout}`)) {
|
|
666
|
+
result = { ...result, exitCode: 0, error: null }
|
|
667
|
+
if (task) pushLine(task, '[already removed] the package is no longer in the profile — treating it as uninstalled')
|
|
668
|
+
}
|
|
655
669
|
// 统一终态后处理:exitCode/status/needsRestart 以最后一次尝试为准
|
|
656
670
|
if (task) {
|
|
657
671
|
task.exitCode = result.exitCode
|
|
@@ -676,17 +690,17 @@ function spawnMutation(options: {
|
|
|
676
690
|
} else if (action === 'remove' && loader) {
|
|
677
691
|
// 卸载成功 → 主动从运行中 loader 停用该包条目:
|
|
678
692
|
// - 停用成功:服务端即时卸载;带客户端 UI 的插件面板要页面刷新才摘除(无需重启宿主)
|
|
679
|
-
// - 停用失败 /
|
|
693
|
+
// - 停用失败 / 超时:回退「待重启」提醒(刷新解决不了,登记常驻提醒)
|
|
680
694
|
const removal = await removeLoadedEntry(loader, target)
|
|
681
695
|
task.needsRestart = !removal.ok
|
|
682
696
|
task.needsReload = removal.ok && removal.live && task.hadClientUi === true
|
|
683
697
|
if (task.needsRestart) addPendingRestart(displayTarget ?? target, 'uninstall')
|
|
684
698
|
else clearPendingRestart(displayTarget ?? target)
|
|
685
699
|
} else if (action !== 'remove' && !isUpdate && loader) {
|
|
686
|
-
// 安装成功 →
|
|
700
|
+
// 安装成功 → 尝试把新插件热加载进运行中 loader,让插件无需重启宿主即生效。
|
|
687
701
|
// 挂上了:服务端即时生效;带客户端 UI 的插件还需页面重新加载(宿主按请求现场
|
|
688
702
|
// 拼装 __DSH_BOOT__ 注入清单,网页端与桌面端一致,刷新即可)。
|
|
689
|
-
// 挂不上 / 非 dsh 插件 /
|
|
703
|
+
// 挂不上 / 非 dsh 插件 / 无法定位包名:回退「待重启」提醒(保证不会无声失效)。
|
|
690
704
|
const { name } = verifyInstalledEntry(profile, target)
|
|
691
705
|
if (name !== null && isDshPlugin(profile, name) && await mountLoadedEntry(loader, name)) {
|
|
692
706
|
task.needsReload = hasClientUi(profile, name)
|
|
@@ -696,7 +710,7 @@ function spawnMutation(options: {
|
|
|
696
710
|
addPendingRestart(displayTarget ?? target, 'install')
|
|
697
711
|
}
|
|
698
712
|
} else {
|
|
699
|
-
// 更新(ESM
|
|
713
|
+
// 更新(ESM 缓存下热加载取不到新代码)/ 无 loader 的安装或卸载 → 登记待重启:
|
|
700
714
|
// 插件要宿主重启才会挂载/卸载干净,重启前一直提醒;展示目标用 displayTarget(owner/repo)
|
|
701
715
|
task.needsRestart = true
|
|
702
716
|
addPendingRestart(displayTarget ?? target, action === 'remove' ? 'uninstall' : 'install')
|
|
@@ -708,7 +722,7 @@ function spawnMutation(options: {
|
|
|
708
722
|
}
|
|
709
723
|
}
|
|
710
724
|
// 带上最终生效的源:后续注解(如 [dsh-too-old])读元数据必须与安装同源,
|
|
711
|
-
//
|
|
725
|
+
// 否则换过源之后会去另一个源查,查不到就漏报
|
|
712
726
|
return activeRegistry === undefined ? result : { ...result, registry: activeRegistry }
|
|
713
727
|
})()
|
|
714
728
|
}
|
|
@@ -854,7 +868,7 @@ async function annotateDshTooOld(result: InstallResult, options: {
|
|
|
854
868
|
// 全局 npm 装的是本机 CLI 工具,不进 profile,没有宿主版本要求
|
|
855
869
|
if ((options.globalNpm ?? []).length > 0) return result
|
|
856
870
|
// 用安装**实际**使用的源读元数据:换源重试后候选表首位可能并不是真正生效的那个,
|
|
857
|
-
//
|
|
871
|
+
// 用错源会读不到包而漏报;私有源/镜像里同样能读到包声明的 engines
|
|
858
872
|
const line = await dshTooOldMarker(options.target, { registry: result.registry ?? options.registries?.[0] })
|
|
859
873
|
if (line === null) return result
|
|
860
874
|
if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
|
|
@@ -873,7 +887,7 @@ export function hostSpawnMarker(invocation: Invocation, error: unknown): string
|
|
|
873
887
|
}
|
|
874
888
|
|
|
875
889
|
/**
|
|
876
|
-
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds
|
|
890
|
+
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 限制拦下构建脚本而失败 ——
|
|
877
891
|
* 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
|
|
878
892
|
* key@url 提示),或是某依赖的原生构建(`ERR_PNPM_IGNORED_BUILDS`,key = 列出的 name@version)——
|
|
879
893
|
* 就把确切的 key 写进 profile 的 pnpm-workspace.yaml 并重试一次。
|
|
@@ -888,7 +902,7 @@ export async function runPluginMutation(options: {
|
|
|
888
902
|
task?: InstallTask
|
|
889
903
|
/** 待重启行的展示目标(owner/repo):卸载时用于把 npm 包名映射回仓库名 */
|
|
890
904
|
displayTarget?: string
|
|
891
|
-
/** 运行中 loader
|
|
905
|
+
/** 运行中 loader:安装成功后热加载新条目、卸载成功后停用旧条目(缺失时仍需重启) */
|
|
892
906
|
loader?: LoaderHandle
|
|
893
907
|
/** 全局 npm 安装(官方 `npm install -g <pkgs>`):非空时执行全局安装,不进任何 profile、无需宿主重启 */
|
|
894
908
|
globalNpm?: string[]
|
|
@@ -897,7 +911,7 @@ export async function runPluginMutation(options: {
|
|
|
897
911
|
/** 精确安装 spec(如 `dsh-plugin@2.0.5`):见 startPluginMutation 同名说明 */
|
|
898
912
|
installSpec?: string
|
|
899
913
|
}): Promise<InstallResult> {
|
|
900
|
-
// operation
|
|
914
|
+
// operation 日志只在运行期存在(宿主结束后可能清掉),先开抓取,退出后按路径读取作为后备
|
|
901
915
|
const profileDir = profileDirectory(options.profile)
|
|
902
916
|
const logTap = createOperationLogTap(profileDir)
|
|
903
917
|
let result = await spawnMutation(options)
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* 运行中 loader
|
|
7
|
-
*
|
|
6
|
+
* 运行中 loader 的读取、停用与热加载:卸载成功后对匹配条目做 live-disable、
|
|
7
|
+
* 安装成功后把新插件条目热加载进运行中的 loader,让两类操作都尽量立即生效
|
|
8
8
|
* (刷新页面即可,无需重启宿主)。
|
|
9
9
|
*/
|
|
10
10
|
import { readFileSync } from 'node:fs'
|
|
@@ -12,7 +12,7 @@ import { join } from 'node:path'
|
|
|
12
12
|
import { profileDirectory } from './profile/profile.ts'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* 运行中 loader 的最小接口:卸载即时停用 /
|
|
15
|
+
* 运行中 loader 的最小接口:卸载即时停用 / 安装即时热加载用。
|
|
16
16
|
* 对应官方 `ctx.loader`(cordis-plugin-loader 的 Loader 服务)的读取、新建与移除面。
|
|
17
17
|
* 移除方式:对匹配条目 live-disable(`entry.update({ disabled: true })`),
|
|
18
18
|
* 而不是 `loader.remove(id)` —— disable 走 Entry.update 的 disabled 分支,不触发
|
|
@@ -32,7 +32,7 @@ export interface LoaderHandle {
|
|
|
32
32
|
* `- insert: - name: <pkg>` 行;Loader.write() 为空实现,不会写盘(重启后由
|
|
33
33
|
* profile 的 bundles 清单重新挂载,不会重复挂载)。返回新条目的 id。 */
|
|
34
34
|
create(options: { id?: string; name: string }): Promise<string>
|
|
35
|
-
/** 按 id
|
|
35
|
+
/** 按 id 解析条目:热加载后确认 fiber 真的起来了(import 失败时条目在、但 fiber 为空)。 */
|
|
36
36
|
resolve(id: string): { id: string; options?: { name?: string; disabled?: boolean | null }; fiber?: unknown; subgroup?: unknown }
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -82,7 +82,7 @@ function nameMatches(entryName: string | undefined, target: string): boolean {
|
|
|
82
82
|
// 2) 条目是去 scope 的短名(bundle patch 手写短名最常见)
|
|
83
83
|
if (e === tShort) return true
|
|
84
84
|
// 3) 反向变体:目标短名可能就是条目的完整形式(条目 `scope-widget`,
|
|
85
|
-
// 目标短名 `widget`
|
|
85
|
+
// 目标短名 `widget` 是它的尾段)——以「条目名以短名结尾」作为匹配条件,避免误伤。
|
|
86
86
|
if (tShort.length >= 2 && e.endsWith(`-${tShort}`)) return true
|
|
87
87
|
// 4) 条目名是目标包内的模块路径(`aegis/extensions/dsh/index.js` 属于包 `aegis`,
|
|
88
88
|
// `@scope/pkg/dist/x.js` 属于 `@scope/pkg`):归一化会吞掉路径分隔符,
|
|
@@ -102,12 +102,12 @@ function nameMatches(entryName: string | undefined, target: string): boolean {
|
|
|
102
102
|
* - `{ ok: true, live: false }`:宿主从未加载过它(磁盘已干净),无需刷新/重启;
|
|
103
103
|
* - `{ ok: true, live: true }`:曾在 loader 中存活且已 live-disable —— 服务端即时卸载;
|
|
104
104
|
* 带客户端 UI 的插件面板要页面刷新才摘除(由调用方按 `hadClientUi` 决定 needsReload);
|
|
105
|
-
* - `{ ok: false, live: true }`:disable 失败 /
|
|
105
|
+
* - `{ ok: false, live: true }`:disable 失败 / 超时,需宿主重启后清理。
|
|
106
106
|
* 宿主关键包(@deepseek-ai/* 与本插件自身)一律跳过。
|
|
107
107
|
*/
|
|
108
108
|
export async function removeLoadedEntry(loader: LoaderHandle, name: string): Promise<{ ok: boolean; live: boolean }> {
|
|
109
109
|
if (name === 'dsh-plugin' || name.startsWith('@deepseek-ai/')) return { ok: false, live: true }
|
|
110
|
-
// 5s
|
|
110
|
+
// 5s 超时保护:fiber dispose 卡住时不能让卸载任务(乃至整个队列)被拖死
|
|
111
111
|
const removal = (async () => {
|
|
112
112
|
const entries = dumpLoaderEntries(loader)
|
|
113
113
|
const match = entries.find((e) => e.name === name)
|
|
@@ -151,7 +151,7 @@ export async function removeLoadedEntry(loader: LoaderHandle, name: string): Pro
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
|
-
*
|
|
154
|
+
* 安装成功后把新插件热加载进运行中 loader(等价 profile bundle patch 的
|
|
155
155
|
* `- insert: - name: <pkg>` 行),让插件无需重启宿主即可生效。
|
|
156
156
|
* 做法:在 Loader 根组新建条目并启动 —— Loader.write() 为空实现,不写盘;
|
|
157
157
|
* 重启后由 profile 的 bundles 清单重新挂载,不会重复。
|
|
@@ -171,7 +171,7 @@ export async function mountLoadedEntry(loader: LoaderHandle, name: string): Prom
|
|
|
171
171
|
console.log(`[hub-install] hot mount ${name}: entry ${id} created but not running (import failed?)`)
|
|
172
172
|
return false
|
|
173
173
|
})()
|
|
174
|
-
// 10s
|
|
174
|
+
// 10s 超时保护:import 卡住时不能让装完收尾(乃至整个队列)被拖死
|
|
175
175
|
return Promise.race([
|
|
176
176
|
mounting.catch((error) => {
|
|
177
177
|
console.log(`[hub-install] hot mount ${name} failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
@@ -183,7 +183,7 @@ export async function mountLoadedEntry(loader: LoaderHandle, name: string): Prom
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* 已安装包是否带客户端 UI(package.json 的 `dsh.client` 声明):client-modules 据它把
|
|
186
|
-
* 客户端 bundle 注入宿主页面。带 UI
|
|
186
|
+
* 客户端 bundle 注入宿主页面。带 UI 的插件热加载后需要页面重新加载才会渲染,
|
|
187
187
|
* 网页端与桌面端一致 —— `__DSH_BOOT__` 注入清单由宿主服务端按请求现场拼装,
|
|
188
188
|
* 壳不缓存,所以桌面端刷新页面同样能拿到新清单,无需重开 App。
|
|
189
189
|
*/
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Website: https://dsh-plugin.org
|
|
4
4
|
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
5
|
*
|
|
6
|
-
* npm
|
|
7
|
-
* pnpm 对发布不满 24h(dsh profile minimumReleaseAge=1440
|
|
8
|
-
*
|
|
6
|
+
* npm 包最新版本的发布时间查询:为「已安装插件是否有更新」提供发布冷却期依据。
|
|
7
|
+
* pnpm 对发布不满 24h(dsh profile minimumReleaseAge=1440)的版本会不提示地回退到旧版本,
|
|
8
|
+
* 因此发布还不满 24 小时的版本不该提示更新(否则用户点了也装不上)。
|
|
9
9
|
* 结果按包名内存缓存,同一次会话内不重复请求;失败/超时/响应过大一律返回 null,绝不抛错、绝不阻塞。
|
|
10
10
|
*/
|
|
11
11
|
import { get as httpsGet } from 'node:https'
|
|
@@ -17,6 +17,15 @@ export interface NpmRelease {
|
|
|
17
17
|
publishedAt: string
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** 最新版 + 可锁定的安装版本:安装锁定后者,pnpm 才不会回退到旧版本。 */
|
|
21
|
+
export interface NpmReleaseWithInstallable extends NpmRelease {
|
|
22
|
+
/** 已过发布冷却期的最高稳定版;时间表缺失或没有满足项时为 null(调用方退回不锁定版本)。 */
|
|
23
|
+
installable: NpmRelease | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同一套规则。 */
|
|
27
|
+
export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
|
|
28
|
+
|
|
20
29
|
/** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
|
|
21
30
|
export const MAX_RELEASE_AGE_PACKAGES = 50
|
|
22
31
|
/** 单包查询超时(ms):慢网络下失败即返回 null,不拖住整个响应。 */
|
|
@@ -27,9 +36,9 @@ export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
|
|
|
27
36
|
|
|
28
37
|
/** 会话内缓存(源 + 包名 → 结果):只存成功结果,一次网络抖动不会把判断永久粘住。
|
|
29
38
|
* 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
|
|
30
|
-
* 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest /
|
|
39
|
+
* 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错冷却期。 */
|
|
31
40
|
const CACHE_TTL_MS = 10 * 60 * 1000
|
|
32
|
-
const releaseCache = new Map<string, { at: number; release:
|
|
41
|
+
const releaseCache = new Map<string, { at: number; release: NpmReleaseWithInstallable }>()
|
|
33
42
|
|
|
34
43
|
/** 从完整 packument 取 dist-tags.latest 对应版本及其发布时间;缺任一字段返回 null。 */
|
|
35
44
|
export function latestReleaseOf(packument: unknown): NpmRelease | null {
|
|
@@ -44,6 +53,35 @@ export function latestReleaseOf(packument: unknown): NpmRelease | null {
|
|
|
44
53
|
return { version, publishedAt }
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 已过发布冷却期的最高稳定版本:安装锁定它,pnpm 不会回退到旧版本。
|
|
58
|
+
* 只看纯 x.y.z(预发布版不参与,避免把用户带到不兼容预览版);时间表缺失或无满足项返回 null。
|
|
59
|
+
*/
|
|
60
|
+
export function installableReleaseOf(packument: unknown, now = Date.now()): NpmRelease | null {
|
|
61
|
+
if (packument === null || typeof packument !== 'object') return null
|
|
62
|
+
const time = (packument as { time?: unknown }).time
|
|
63
|
+
if (time === null || typeof time !== 'object') return null
|
|
64
|
+
let best: NpmRelease | null = null
|
|
65
|
+
for (const [version, published] of Object.entries(time as Record<string, unknown>)) {
|
|
66
|
+
if (!/^\d+\.\d+\.\d+$/.test(version) || typeof published !== 'string') continue
|
|
67
|
+
const at = Date.parse(published)
|
|
68
|
+
if (Number.isNaN(at) || now - at < MIN_RELEASE_AGE_MS) continue
|
|
69
|
+
if (best === null || compareVersion(version, best.version) > 0) best = { version, publishedAt: published }
|
|
70
|
+
}
|
|
71
|
+
return best
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 纯数字段版本比较(installableReleaseOf 只在 x.y.z 里用)。 */
|
|
75
|
+
function compareVersion(a: string, b: string): number {
|
|
76
|
+
const pa = a.split('.')
|
|
77
|
+
const pb = b.split('.')
|
|
78
|
+
for (let i = 0; i < 3; i++) {
|
|
79
|
+
const d = Number.parseInt(pa[i] ?? '0', 10) - Number.parseInt(pb[i] ?? '0', 10)
|
|
80
|
+
if (d !== 0) return d
|
|
81
|
+
}
|
|
82
|
+
return 0
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
/** 请求体 → 去重后的合法包名列表;非数组 / 超量 / 含非法包名一律 null(拒绝非法输入)。 */
|
|
48
86
|
export function parseReleaseAgeRequest(body: unknown): string[] | null {
|
|
49
87
|
if (body === null || typeof body !== 'object') return null
|
|
@@ -81,7 +119,7 @@ function getJson(url: string, timeoutMs: number): Promise<string | null> {
|
|
|
81
119
|
})
|
|
82
120
|
}
|
|
83
121
|
|
|
84
|
-
/** 拉取一个包的完整 packument
|
|
122
|
+
/** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理规则(curl 子进程注入 proxy env)。 */
|
|
85
123
|
async function fetchPackument(url: string, proxy: string, timeoutMs: number): Promise<string | null> {
|
|
86
124
|
if (proxy !== '') {
|
|
87
125
|
const r = await fetchViaCurl(url, proxy, timeoutMs)
|
|
@@ -90,13 +128,13 @@ async function fetchPackument(url: string, proxy: string, timeoutMs: number): Pr
|
|
|
90
128
|
return getJson(url, timeoutMs)
|
|
91
129
|
}
|
|
92
130
|
|
|
93
|
-
/**
|
|
131
|
+
/** 查询一个包的最新版本、发布时间与可锁定的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
|
|
94
132
|
export async function lookupReleaseAge(
|
|
95
133
|
packageName: string,
|
|
96
134
|
registry: string,
|
|
97
135
|
proxy: string,
|
|
98
136
|
timeoutMs = RELEASE_AGE_TIMEOUT_MS,
|
|
99
|
-
): Promise<
|
|
137
|
+
): Promise<NpmReleaseWithInstallable | null> {
|
|
100
138
|
const base = registry.replace(/\/+$/, '') || DEFAULT_NPM_REGISTRY
|
|
101
139
|
const key = `${base}\n${packageName}`
|
|
102
140
|
const cached = releaseCache.get(key)
|
|
@@ -105,7 +143,9 @@ export async function lookupReleaseAge(
|
|
|
105
143
|
if (body === null) return null
|
|
106
144
|
let parsed: unknown
|
|
107
145
|
try { parsed = JSON.parse(body) } catch { return null }
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
146
|
+
const latest = latestReleaseOf(parsed)
|
|
147
|
+
if (latest === null) return null
|
|
148
|
+
const release: NpmReleaseWithInstallable = { ...latest, installable: installableReleaseOf(parsed) }
|
|
149
|
+
releaseCache.set(key, { at: Date.now(), release })
|
|
110
150
|
return release
|
|
111
151
|
}
|
|
@@ -51,7 +51,7 @@ export function curlRevokeFlagUnsupported(code: number | null, stderr: string):
|
|
|
51
51
|
return code === 2 && /ssl-revoke-best-effort/i.test(stderr) && /unknown|unrecognized/i.test(stderr)
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/** 彻底关闭证书吊销检查的保底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
|
|
55
55
|
const NO_REVOKE_ARG = '--ssl-no-revoke'
|
|
56
56
|
|
|
57
57
|
/** stderr 是否指向 Schannel 取不到证书吊销信息(据此归因为环境 TLS 问题,而非网络不通)。 */
|
|
@@ -59,7 +59,7 @@ export function tlsRevocationBlocked(stderr: string): boolean {
|
|
|
59
59
|
return /CRYPT_E_NO_REVOCATION_CHECK|CRYPT_E_REVOCATION_OFFLINE|0x80092012/i.test(stderr)
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
/**
|
|
62
|
+
/** 探测/安装同一套代理规则:curl 与 git 子进程都读 HTTP(S)_PROXY 环境变量。 */
|
|
63
63
|
function proxyEnv(proxy: string): NodeJS.ProcessEnv {
|
|
64
64
|
if (proxy === '') return { ...process.env }
|
|
65
65
|
return {
|
|
@@ -156,12 +156,12 @@ function runCurl(args: string[], env: NodeJS.ProcessEnv, timeoutMs: number): Pro
|
|
|
156
156
|
child.stderr.on('data', (c: Buffer) => { stderr += c.toString() })
|
|
157
157
|
child.on('error', () => finish(null))
|
|
158
158
|
child.on('close', (code) => finish(code))
|
|
159
|
-
//
|
|
159
|
+
// 超时保护:curl --max-time 到点自行退出,此处再设一个进程级超时防止悬挂
|
|
160
160
|
const timer = setTimeout(() => { child.kill(); finish(null) }, timeoutMs + 1000)
|
|
161
161
|
})
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
/** 带 TLS
|
|
164
|
+
/** 带 TLS 重试的 curl 调用:先按平台参数跑(Windows 带吊销检查降级),
|
|
165
165
|
* 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败;
|
|
166
166
|
* 降级后仍被吊销检查拦死(CRYPT_E_NO_REVOCATION_CHECK)时,再用 --ssl-no-revoke
|
|
167
167
|
* 彻底关掉吊销检查跑一次 —— 该参数 curl ≥ 7.19 即存在,覆盖面比降级参数更广,
|
|
@@ -175,7 +175,7 @@ async function curlWithTlsFallback(args: string[], env: NodeJS.ProcessEnv, timeo
|
|
|
175
175
|
}
|
|
176
176
|
if (tlsArgs.length > 0 && tlsRevocationBlocked(run.stderr)) {
|
|
177
177
|
const retry = await runCurl([NO_REVOKE_ARG, ...args], env, timeoutMs)
|
|
178
|
-
//
|
|
178
|
+
// 只在重试本身跑起来(非「不认识该参数」)时采用其结果,否则保留原始失败线索
|
|
179
179
|
if (retry.code !== null && !curlRevokeFlagUnsupported(retry.code, retry.stderr)) run = retry
|
|
180
180
|
}
|
|
181
181
|
return run
|
|
@@ -183,7 +183,7 @@ async function curlWithTlsFallback(args: string[], env: NodeJS.ProcessEnv, timeo
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* curl 子进程探测:spawn curl -s -w '%{http_code}' 并注入代理 env。
|
|
186
|
-
* curl 读 HTTP_PROXY/HTTPS_PROXY 是原生行为,与 pnpm/git
|
|
186
|
+
* curl 读 HTTP_PROXY/HTTPS_PROXY 是原生行为,与 pnpm/git 安装通道同一套代理规则;
|
|
187
187
|
* 失败(curl 不存在 / 连接失败 / 超时)都归为不可达,全程不抛错。
|
|
188
188
|
* 证书吊销检查受阻(Windows Schannel)另记 reason,供诊断给出对症提示。
|
|
189
189
|
*/
|
|
@@ -240,7 +240,7 @@ export function gitLsRemote(url: string, proxy: string, timeoutMs: number): Prom
|
|
|
240
240
|
const child = spawn('git', ['ls-remote', '--exit-code', url, 'HEAD'], { env })
|
|
241
241
|
let out = ''
|
|
242
242
|
child.stdout.on('data', (c: Buffer) => { out += c.toString() })
|
|
243
|
-
// git
|
|
243
|
+
// git 的报错不原样传递给终端:结果以退出码 + HEAD ref 是否返回为准
|
|
244
244
|
child.stderr.on('data', () => {})
|
|
245
245
|
let done = false
|
|
246
246
|
const finish = (r: ProbeResult) => {
|
|
@@ -255,7 +255,7 @@ export function gitLsRemote(url: string, proxy: string, timeoutMs: number): Prom
|
|
|
255
255
|
// status 复用为 git 退出码(0 = 克隆握手成功);客户端 git 行不把它显示成 HTTP 码
|
|
256
256
|
finish({ ok, ms: Date.now() - started, status: code })
|
|
257
257
|
})
|
|
258
|
-
//
|
|
258
|
+
// 超时保护:与 curlProbe 同一套规则,防止 git 挂起
|
|
259
259
|
const timer = setTimeout(() => { child.kill(); finish(probeFail()) }, timeoutMs + 1000)
|
|
260
260
|
})
|
|
261
261
|
}
|
|
@@ -263,7 +263,7 @@ export function gitLsRemote(url: string, proxy: string, timeoutMs: number): Prom
|
|
|
263
263
|
/**
|
|
264
264
|
* 请求一个 URL 并返回重定向后的最终地址(curl -I -L -w %{url_effective})。
|
|
265
265
|
* 供 GitHub 仓库改名解析使用:对已改名仓库 GitHub 回 301,跟随跳转后的最终地址
|
|
266
|
-
* 即规范地址。与其它 GitHub
|
|
266
|
+
* 即规范地址。与其它 GitHub 访问同一套规则 —— curl 子进程注入代理 env(Node 内置
|
|
267
267
|
* http(s) 读不到系统代理,直连在部分网络下不可达)。
|
|
268
268
|
* 未发生跳转(200/404)、网络失败或 curl 不可用都返回 null,调用方保留原始地址。
|
|
269
269
|
*/
|
|
@@ -279,11 +279,11 @@ export async function finalUrl(url: string, proxy: string, timeoutMs: number): P
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS
|
|
282
|
+
* curl 子进程抓取响应体(与 probeUrl 同一套代理 env 注入与 TLS 参数重试)。
|
|
283
283
|
* 供服务端 /catalog 代理路由使用:目录/统计数据经此拉到服务端再转给浏览器,
|
|
284
|
-
* 使「目录数据请求走设置里的代理」与 npm / git
|
|
284
|
+
* 使「目录数据请求走设置里的代理」与 npm / git 安装通道规则一致。
|
|
285
285
|
* 失败(curl 不存在 / 连接失败 / 超时 / 非零退出)时 ok=false,全程不抛错;
|
|
286
|
-
* reason 区分「证书吊销受阻」这类环境 TLS
|
|
286
|
+
* reason 区分「证书吊销受阻」这类环境 TLS 问题,供调用方决定备用策略。
|
|
287
287
|
*/
|
|
288
288
|
export function fetchViaCurl(
|
|
289
289
|
url: string, proxy: string, timeoutMs: number,
|
|
@@ -79,7 +79,7 @@ export function recordInstalledVersion(profile: string, repo: string, version: s
|
|
|
79
79
|
export function recordResolvedNpmPackage(profile: string, repo: string, npmPackage: string): void {
|
|
80
80
|
const table = readInstalledVersions(profile)
|
|
81
81
|
const key = repo.toLowerCase()
|
|
82
|
-
// 显式写全字段:这一步发生在安装开始时,版本还不知道 ——
|
|
82
|
+
// 显式写全字段:这一步发生在安装开始时,版本还不知道 —— 但写入磁盘记录必须是完整形状,
|
|
83
83
|
// 否则只带 npmPackage 的残缺记录会在客户端版本比较时崩掉整块界面
|
|
84
84
|
table[key] = {
|
|
85
85
|
version: table[key]?.version ?? '',
|
|
@@ -101,7 +101,7 @@ function writeVersions(profile: string, table: InstalledVersionTable): void {
|
|
|
101
101
|
const file = versionsFilePath(profile)
|
|
102
102
|
mkdirSync(dirname(file), { recursive: true })
|
|
103
103
|
// 先写临时文件再原子替换:直接覆盖时进程被杀/磁盘满会留下截断的 JSON,
|
|
104
|
-
//
|
|
104
|
+
// 下次读取整份账本都会失效(更新检测随之失效),而用户看不到任何报错
|
|
105
105
|
const tmp = `${file}.tmp`
|
|
106
106
|
writeFileSync(tmp, JSON.stringify(table, null, 2) + '\n', 'utf8')
|
|
107
107
|
renameSync(tmp, file)
|
|
@@ -77,7 +77,7 @@ export function githubTarget(repo: string, ref?: string | null): string | null {
|
|
|
77
77
|
* 非命令输入原样返回。兼容「直接粘贴完整命令」(防用户输错):--profile 必填且只有长选项
|
|
78
78
|
* (官方 CLI 无 -p 简写,省略即报错),支持 `--profile=<name>` 等号形式,命令大小写不敏感。
|
|
79
79
|
* update = 更新已安装目标到最新;remove 不在此列(卸载走 /uninstall 路由,按已装包名直卸)。
|
|
80
|
-
* 与客户端 CustomInstallView
|
|
80
|
+
* 与客户端 CustomInstallView 规则一致。
|
|
81
81
|
*/
|
|
82
82
|
const DSH_PLUGIN_CMD_RE = /^dsh\s+plugin\s+--profile(?:\s+|=)\S+\s+(add|update)\s+(.+)$/i
|
|
83
83
|
|
|
@@ -90,7 +90,7 @@ export function installTargetOf(value: string): string {
|
|
|
90
90
|
/**
|
|
91
91
|
* npm 全局安装命令(官方 README 的 `npm install -g <pkgs>`,如 dsh-tui 的安装命令)——
|
|
92
92
|
* 提取 -g 之后的包列表。支持 `npm install|i` 与 `-g|--global`;任一包名非法(或格式
|
|
93
|
-
* 不匹配)返回 null,由调用方按「不支持的目标」处理。与客户端 CustomInstallView
|
|
93
|
+
* 不匹配)返回 null,由调用方按「不支持的目标」处理。与客户端 CustomInstallView 同一套规则。
|
|
94
94
|
*/
|
|
95
95
|
const NPM_GLOBAL_CMD_RE = /^npm\s+(?:install|i)\s+(?:-g|--global)\s+(.+)$/i
|
|
96
96
|
|