dsh-plugin 2.3.2 → 2.3.4

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.
Files changed (84) hide show
  1. package/README.en.md +3 -9
  2. package/README.md +3 -9
  3. package/client/client.js +371 -364
  4. package/client/client.js.map +1 -1
  5. package/lib/http/catalog.js +4 -4
  6. package/lib/http/diagnostics.js +4 -4
  7. package/lib/http/install.js +13 -13
  8. package/lib/http/npm-release-age.js +3 -3
  9. package/lib/http/shared.js +1 -1
  10. package/lib/services/host-version.js +2 -2
  11. package/lib/services/install/dsh-compat.js +1 -1
  12. package/lib/services/install/npm-resolve.js +1 -1
  13. package/lib/services/install/operation-log.js +3 -3
  14. package/lib/services/install/preflight.js +1 -1
  15. package/lib/services/install/registry-fallback.js +3 -3
  16. package/lib/services/install/release-target.js +2 -2
  17. package/lib/services/install/task-queue.js +35 -20
  18. package/lib/services/loader.js +8 -8
  19. package/lib/services/npm-release-age.js +8 -8
  20. package/lib/services/probe.js +12 -12
  21. package/lib/services/profile/installed-versions.js +2 -2
  22. package/lib/services/profile/profile.js +2 -2
  23. package/lib/types/services/host-version.d.ts +1 -1
  24. package/lib/types/services/install/install-types.d.ts +5 -5
  25. package/lib/types/services/install/operation-log.d.ts +1 -1
  26. package/lib/types/services/install/preflight.d.ts +1 -1
  27. package/lib/types/services/install/registry-fallback.d.ts +1 -1
  28. package/lib/types/services/install/release-target.d.ts +1 -1
  29. package/lib/types/services/install/task-queue.d.ts +10 -7
  30. package/lib/types/services/loader.d.ts +5 -5
  31. package/lib/types/services/npm-release-age.d.ts +5 -5
  32. package/lib/types/services/probe.d.ts +4 -4
  33. package/package.json +1 -1
  34. package/src/client/components/PluginHubSection.tsx +29 -29
  35. package/src/client/components/catalog/CatalogList.tsx +1 -1
  36. package/src/client/components/catalog/PluginCard.tsx +3 -3
  37. package/src/client/components/modals/AboutModal.tsx +2 -2
  38. package/src/client/components/modals/ConfirmDialog.tsx +2 -2
  39. package/src/client/components/modals/HubUpdateModal.tsx +3 -3
  40. package/src/client/components/modals/InstalledDetailModal.tsx +1 -1
  41. package/src/client/components/modals/LogsPathModal.tsx +1 -1
  42. package/src/client/components/modals/NotificationsModal.tsx +2 -2
  43. package/src/client/components/modals/modals.tsx +10 -10
  44. package/src/client/components/views/CustomInstallView.tsx +2 -2
  45. package/src/client/components/views/InstalledView.tsx +2 -2
  46. package/src/client/components/views/SettingsView.tsx +3 -3
  47. package/src/client/data/catalog.ts +1 -1
  48. package/src/client/data/host.ts +2 -2
  49. package/src/client/data/hub.ts +3 -3
  50. package/src/client/hooks/useCatalog.ts +14 -14
  51. package/src/client/hooks/useSettings.ts +2 -2
  52. package/src/client/hooks/useTaskQueue.ts +30 -23
  53. package/src/client/locales.ts +15 -15
  54. package/src/client/logic/constants.ts +1 -1
  55. package/src/client/logic/ensure-plugin-css.ts +2 -2
  56. package/src/client/logic/failures.ts +9 -9
  57. package/src/client/logic/install-command.ts +1 -2
  58. package/src/client/logic/installed.ts +1 -1
  59. package/src/client/logic/release-age.ts +10 -10
  60. package/src/client/logic/urls.ts +6 -4
  61. package/src/client/styles/Header.module.css +1 -1
  62. package/src/client/styles/Modal.module.css +2 -2
  63. package/src/client/styles/SectionTabs.module.css +1 -1
  64. package/src/client/styles/tokens.module.css +1 -1
  65. package/src/client/types.ts +4 -4
  66. package/src/server/http/catalog.ts +4 -4
  67. package/src/server/http/diagnostics.ts +4 -4
  68. package/src/server/http/install.ts +13 -13
  69. package/src/server/http/npm-release-age.ts +3 -3
  70. package/src/server/http/shared.ts +1 -1
  71. package/src/server/services/host-version.ts +2 -2
  72. package/src/server/services/install/dsh-compat.ts +1 -1
  73. package/src/server/services/install/install-types.ts +5 -5
  74. package/src/server/services/install/npm-resolve.ts +1 -1
  75. package/src/server/services/install/operation-log.ts +3 -3
  76. package/src/server/services/install/preflight.ts +1 -1
  77. package/src/server/services/install/registry-fallback.ts +3 -3
  78. package/src/server/services/install/release-target.ts +2 -2
  79. package/src/server/services/install/task-queue.ts +40 -26
  80. package/src/server/services/loader.ts +10 -10
  81. package/src/server/services/npm-release-age.ts +10 -10
  82. package/src/server/services/probe.ts +12 -12
  83. package/src/server/services/profile/installed-versions.ts +2 -2
  84. package/src/server/services/profile/profile.ts +2 -2
@@ -4,7 +4,7 @@
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
6
  * 宿主与命令行工具的版本探测:dsh 版本同时用于诊断快照和「插件要求更高 DSH」判定,
7
- * pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms 超时兜底,拿不到一律 null,绝不挂起。
7
+ * pnpm/npm/git 只用于诊断快照。同步探测 + 1500ms 超时保护,拿不到一律 null,绝不挂起。
8
8
  */
9
9
  import { readFileSync } from 'node:fs'
10
10
  import { dirname, join, resolve } from 'node:path'
@@ -39,7 +39,7 @@ export function toolVersion(cmd: string, args: string[]): string | null {
39
39
  let cachedDshVersion: string | null = null
40
40
 
41
41
  /** 宿主 dsh 版本:优先 `dsh --version`;拿不到再从入口 `process.argv[1]` 向上找 package.json
42
- * (dsh 不在 PATH 时兜底)。探测成功即缓存,失败不缓存以便下次重试。 */
42
+ * (dsh 不在 PATH 时使用)。探测成功即缓存,失败不缓存以便下次重试。 */
43
43
  export function hostDshVersion(): string | null {
44
44
  if (cachedDshVersion !== null) return cachedDshVersion
45
45
  const viaCli = toolVersion('dsh', ['--version'])
@@ -9,7 +9,7 @@
9
9
  * 不再把这类失败归成「未分类」丢给用户或插件作者。
10
10
  *
11
11
  * 只在失败路径调用:peer 不满足本身通常只是警告,成功安装时不该出现这个提示;
12
- * 任何网络/解析异常都静默返回 null,不改变原有失败处理。
12
+ * 任何网络/解析异常都悄悄返回 null,不改变原有失败处理。
13
13
  */
14
14
  import { hostDshVersion } from '../host-version.ts'
15
15
 
@@ -36,14 +36,14 @@ export interface InstallTask {
36
36
  status: 'pending' | 'running' | 'done' | 'failed' | 'cancelled'
37
37
  timedOut: boolean
38
38
  exitCode: number | null
39
- /** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出,阶段行兜底 */
39
+ /** 0-100 估算进度:解析 pnpm 的 `Progress: resolved…` 输出;没有进度行时按阶段行估算 */
40
40
  progress: number
41
41
  /** 输出行最新在前(消费方展示尾部)。 */
42
42
  lines: string[]
43
- /** 完成后是否仍需宿主重启才生效:仅「热挂载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
44
- * 无 loader」等刷新解决不了的兜底场景为 true(弹窗给「立即重启」);其余一律 false。 */
43
+ /** 完成后是否仍需宿主重启才生效:仅「热加载失败 / 非 dsh 插件 / 更新(ESM 缓存取不到新代码)/
44
+ * 无 loader」等刷新解决不了的例外场景为 true(弹窗给「立即重启」);其余一律 false。 */
45
45
  needsRestart: boolean
46
- /** 完成结果是否只需刷新页面即生效(插件已热挂进运行中 loader,仅剩客户端 UI 待页面重新加载,
46
+ /** 完成结果是否只需刷新页面即生效(插件已热加载进运行中 loader,仅剩客户端 UI 待页面重新加载,
47
47
  * 网页端与桌面端一致):true 时关闭结果弹窗即自动刷新页面,不给手动刷新按钮。 */
48
48
  needsReload?: boolean
49
49
  /** 自更新成功、但该 profile 的 dsh.profile.bundles 已不含 dsh-plugin:重启后市场不会出现 */
@@ -79,7 +79,7 @@ export interface QueueItem {
79
79
  installChannel?: 'npm'
80
80
  /** npm 源候选(首选在前):传输层失败时按顺序换源重试 */
81
81
  registries?: string[]
82
- /** 精确安装 spec(Hub 自我更新钉版本) */
82
+ /** 精确安装 spec(Hub 自我更新时锁定版本) */
83
83
  installSpec?: string
84
84
  }
85
85
  }
@@ -83,7 +83,7 @@ export function isDshNpmPackageForRepo(packageName: string, repo: string, regist
83
83
 
84
84
  /**
85
85
  * 解析仓库改名后的规范 `owner/repo`:GitHub 对已改名仓库返回 301,跟随跳转取最终地址。
86
- * 与其它 GitHub 访问同口径走 curl + 代理 env(Node 内置 https 读不到系统代理)。
86
+ * 与其它 GitHub 访问同一套规则走 curl + 代理 env(Node 内置 https 读不到系统代理)。
87
87
  * 未改名、仓库不存在、网络失败或 curl 不可用都返回 '',调用方按原名继续。
88
88
  */
89
89
  async function canonicalRepo(repo: string, proxy: string): Promise<string> {
@@ -7,7 +7,7 @@
7
7
  * 回显到控制台,真正的 pnpm 报错写进该路径的 operation 日志。这里从输出里解析出该路径
8
8
  * 并读取其尾部,补进失败信息 —— 否则前端拿不到任何错误特征,只能把失败归为「未知」。
9
9
  * 只允许读 profile 目录下的 pnpm.log(dsh 自己的 operation 日志),
10
- * 防止插件把任意路径打印进日志造成越权读取;任何异常都静默降级(返回 null),
10
+ * 防止插件把任意路径打印进日志造成越权读取;任何异常都降级为 null,
11
11
  * 不改变原有失败处理。
12
12
  */
13
13
  import { readFileSync, readdirSync, statSync } from 'node:fs'
@@ -53,7 +53,7 @@ export function operationLogPathOf(output: string): string | null {
53
53
  /**
54
54
  * 只允许读 profile 目录下的 pnpm.log(dsh 自己的操作日志):
55
55
  * 绝对路径必须落在该目录内,相对路径按该目录解析。
56
- * 未提供 profileDir 时退回旧口径(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
56
+ * 未提供 profileDir 时退回旧规则(路径须含 .plugin-manager),防止插件打印任意路径造成越权读取。
57
57
  */
58
58
  function allowedLogPath(raw: string, profileDir?: string): string | null {
59
59
  const path = resolve(profileDir ?? '', raw)
@@ -96,7 +96,7 @@ export function hasSubstantiveOutput(output: string): boolean {
96
96
  /**
97
97
  * 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
98
98
  * 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
99
- * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容兜底。
99
+ * `.plugin-manager/logs/<operation>/pnpm.log`,用抓到的内容补全。
100
100
  * 只认本次运行产生的新文件(mtime 晚于启动基线),避免复述上一次安装的日志。
101
101
  */
102
102
  export function createOperationLogTap(profileDir: string): { stop: () => string } {
@@ -10,7 +10,7 @@ import { spawnSync } from 'node:child_process'
10
10
  export const GIT_MISSING_DETAIL = '[git-missing] the git CLI was not found on this machine (install git and add it to the PATH, then retry)'
11
11
 
12
12
  /** 命令存在性探测:which/where 找 PATH 里的命令。
13
- * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验兜底。 */
13
+ * 探测自身异常(which/where 不可用等)返回 true,不拦截安装,交给装后校验处理。 */
14
14
  export function commandOnPath(cmd: string, env?: NodeJS.ProcessEnv): boolean {
15
15
  try {
16
16
  const probe = process.platform === 'win32' ? 'where' : 'which'
@@ -1,6 +1,6 @@
1
1
  /**
2
- * npm 源降级:当前源是公共镜像时,传输层失败后按「当前源 → 官方 → npmmirror」
3
- * 自动换源重试;自定义/内网源不降级,避免私有包名泄露给公共源。
2
+ * npm 换源:当前源是公共镜像时,传输层失败后按「当前源 → 官方 → npmmirror」
3
+ * 自动换源重试;自定义/内网源不换源,避免私有包名泄露给公共源。
4
4
  */
5
5
  import { spawnSync } from 'node:child_process'
6
6
 
@@ -37,7 +37,7 @@ export function isPublicRegistry(value: string): boolean {
37
37
 
38
38
  /**
39
39
  * 依次尝试的源:显式配置 → 本机 npm 配置 → 官方源。
40
- * 只有公共源才追加后备(最多 3 个);关闭降级或源不在白名单时只返回它自己。
40
+ * 只有公共源才追加后备(最多 3 个);关闭换源或源不在白名单时只返回它自己。
41
41
  */
42
42
  export function registryCandidates(configured: string, userRegistry: string, enabled: boolean): string[] {
43
43
  const base = normalizeRegistry(configured) || normalizeRegistry(userRegistry) || PUBLIC_REGISTRIES[0]!
@@ -6,7 +6,7 @@
6
6
  * 预构建 release 直链的仓库身份解析。部分插件(如 loopx)当初以 GitHub release 的 .tgz
7
7
  * 安装过,仓库 HEAD 反而是 monorepo 根、走 git 直装会因缺入口文件失败;老安装记录里留的
8
8
  * 就是这种 release 直链。这里把「历史安装目标」还原成所属仓库身份,供已装态读回、队列
9
- * 去重与展示复用(单一事实来源,避免两端口径漂移)。
9
+ * 去重与展示复用(单一事实来源,避免两端规则漂移)。
10
10
  *
11
11
  * 只接受固定形态的 HTTPS `github.com/<owner>/<repo>/releases/download/<tag>/<asset>.tgz`:
12
12
  * 输入是记录/展示数据、绝非 shell 代码,绝不执行其文本;跨仓跳转、外部主机、凭据、
@@ -21,7 +21,7 @@ export interface GithubReleaseTarget {
21
21
  repo: string
22
22
  }
23
23
 
24
- /** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同口径)。 */
24
+ /** 完整命令前缀:`dsh plugin --profile <p> add|update <target>`(与 profile.ts installTargetOf 同一套规则)。 */
25
25
  const DSH_PLUGIN_CMD_RE = /^dsh[ \t]+plugin[ \t]+--profile(?:[ \t]+|=)[A-Za-z0-9_-]+[ \t]+(?:add|update)[ \t]+(.+)$/i
26
26
 
27
27
  /** 固定形态的 release .tgz 直链:主机、路径段与扩展名全部锁死。 */
@@ -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
- // 兜底:行无解析格式也随输出量推进,保证进度条始终在动(不会卡在 0)
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 的安装目标:钉住版本优先(Hub 自我更新提示的是已过供应链门禁的版本),
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
- * 必须钉住安装,不能再用 @latest 被带回门禁内的最新版 */
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 动词透传 add,不与官方 update 动词分叉。
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
- // 钉住版本优先于 @latest:提示的是「已过门禁」的版本,装 latest 会被 pnpm 拒绝
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 并捕获结果:流式输出打日志 + 超时/取消兜底,close 后 resolve。
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 原样透传,有它时必须基于完整环境叠加,否则子进程会丢掉 PATH。
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
- // 兜底定时器句柄:整段超时 / exit 后 close 宽限 / 强制收尾
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
- // 时间兜底:即使 CLI 静默不吐进度行,进度条也单调推进,不会卡在 0
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
- // 原生透传 pnpm/npm 输出:stdout/stderr 原样入日志,不做格式化,保证报错信息完整可见
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
- // 超时被强杀 ≠ 没装上:命中的安装可能已经落盘(日志里 pnpm 已打印 Done,只是 dsh 进程迟迟不退出
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
- // 安装成功 → 尝试把新插件热挂进运行中 loader,让插件无需重启宿主即生效。
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 缓存下热挂取不到新代码)/ 无 loader 的安装或卸载 → 登记待重启:
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
- // 用错源会读不到包而静默漏报;私有源/镜像里同样能读到包声明的 engines
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 的读取、停用与热挂载:卸载成功后对匹配条目做 live-disable、
7
- * 安装成功后把新插件条目热挂进运行中的 loader,让两类操作都尽量立即生效
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 解析条目:热挂载后确认 fiber 真的起来了(import 失败时条目在、但 fiber 为空)。 */
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 超时兜底:fiber dispose 卡住时不能让卸载任务(乃至整个队列)被拖死
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
- * 安装成功后把新插件热挂进运行中 loader(等价 profile bundle patch 的
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 超时兜底:import 卡住时不能让装完收尾(乃至整个队列)被拖死
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,13 +17,13 @@ export interface NpmRelease {
17
17
  publishedAt: string
18
18
  }
19
19
 
20
- /** 最新版 + 可钉住的安装版本:安装时钉住后者,pnpm 才不会把「发布未满门禁」的最新版回退成很旧的版本。 */
20
+ /** 最新版 + 可锁定的安装版本:安装锁定后者,pnpm 才不会回退到旧版本。 */
21
21
  export interface NpmReleaseWithInstallable extends NpmRelease {
22
- /** 已过供应链门禁的最高稳定版;时间表缺失或没有满足项时为 null(调用方退回不钉版本)。 */
22
+ /** 已过发布冷却期的最高稳定版;时间表缺失或没有满足项时为 null(调用方退回不锁定版本)。 */
23
23
  installable: NpmRelease | null
24
24
  }
25
25
 
26
- /** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同口径。 */
26
+ /** pnpm profile 默认 minimumReleaseAge=1440 分钟;+30 分钟缓冲与客户端 release-age 判定同一套规则。 */
27
27
  export const MIN_RELEASE_AGE_MS = 24 * 60 * 60 * 1000 + 30 * 60 * 1000
28
28
 
29
29
  /** 请求体包名数量上限:只为已安装插件按需查询,几十个足矣,封顶防滥用。 */
@@ -36,7 +36,7 @@ export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
36
36
 
37
37
  /** 会话内缓存(源 + 包名 → 结果):只存成功结果,一次网络抖动不会把判断永久粘住。
38
38
  * 必须带过期时间 —— 插件中心可能连续开着几小时,作者发了新版本后不能一直沿用旧结果;
39
- * 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错门禁。 */
39
+ * 也必须把源算进键 —— 同一个包在官方源与镜像上的 latest / 发布时间不同,混用会算错冷却期。 */
40
40
  const CACHE_TTL_MS = 10 * 60 * 1000
41
41
  const releaseCache = new Map<string, { at: number; release: NpmReleaseWithInstallable }>()
42
42
 
@@ -54,7 +54,7 @@ export function latestReleaseOf(packument: unknown): NpmRelease | null {
54
54
  }
55
55
 
56
56
  /**
57
- * 已过供应链门禁的最高稳定版本:安装时钉住它,pnpm 就不会把最新版静默回退成很旧的版本。
57
+ * 已过发布冷却期的最高稳定版本:安装锁定它,pnpm 不会回退到旧版本。
58
58
  * 只看纯 x.y.z(预发布版不参与,避免把用户带到不兼容预览版);时间表缺失或无满足项返回 null。
59
59
  */
60
60
  export function installableReleaseOf(packument: unknown, now = Date.now()): NpmRelease | null {
@@ -119,7 +119,7 @@ function getJson(url: string, timeoutMs: number): Promise<string | null> {
119
119
  })
120
120
  }
121
121
 
122
- /** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理口径(curl 子进程注入 proxy env)。 */
122
+ /** 拉取一个包的完整 packument 正文;配了代理时复用与安装/目录同一套代理规则(curl 子进程注入 proxy env)。 */
123
123
  async function fetchPackument(url: string, proxy: string, timeoutMs: number): Promise<string | null> {
124
124
  if (proxy !== '') {
125
125
  const r = await fetchViaCurl(url, proxy, timeoutMs)
@@ -128,7 +128,7 @@ async function fetchPackument(url: string, proxy: string, timeoutMs: number): Pr
128
128
  return getJson(url, timeoutMs)
129
129
  }
130
130
 
131
- /** 查询一个包的最新版本、发布时间与可钉住的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
131
+ /** 查询一个包的最新版本、发布时间与可锁定的安装版本(成功结果按源缓存 10 分钟);registry 为空回退官方源,失败返回 null。 */
132
132
  export async function lookupReleaseAge(
133
133
  packageName: string,
134
134
  registry: string,
@@ -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
- /** 彻底关闭证书吊销检查的兜底参数(curl ≥ 7.19),比降级参数覆盖更老的 Windows curl。 */
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
- /** 探测/安装同一套代理口径:curl 与 git 子进程都读 HTTP(S)_PROXY 环境变量。 */
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
- // 超时兜底:curl --max-time 到点自行退出,此处再设一个进程级超时防止悬挂
159
+ // 超时保护:curl --max-time 到点自行退出,此处再设一个进程级超时防止悬挂
160
160
  const timer = setTimeout(() => { child.kill(); finish(null) }, timeoutMs + 1000)
161
161
  })
162
162
  }
163
163
 
164
- /** 带 TLS 兜底的 curl 调用:先按平台参数跑(Windows 带吊销检查降级),
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 的报错不透传给终端:结果以退出码 + HEAD ref 是否返回为准
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
- // 超时兜底:与 curlProbe 同口径,防止 git 挂起
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 访问同口径 —— curl 子进程注入代理 env(Node 内置
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,