dsh-plugin 1.6.5 → 1.6.7

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 (53) hide show
  1. package/README.en.md +11 -11
  2. package/README.md +11 -11
  3. package/client/client.js +421 -390
  4. package/client/client.js.map +1 -1
  5. package/lib/http/catalog.js +132 -0
  6. package/lib/http/diagnostics.js +201 -0
  7. package/lib/http/install.js +399 -0
  8. package/lib/http/logs.js +164 -0
  9. package/lib/http/routes.js +19 -1263
  10. package/lib/http/settings.js +77 -0
  11. package/lib/http/shared.js +169 -0
  12. package/lib/http/task.js +208 -0
  13. package/lib/services/install/install.js +1 -1
  14. package/lib/services/install/package-entry.js +59 -8
  15. package/lib/services/install/preflight.js +47 -17
  16. package/lib/services/install/task-queue.js +75 -31
  17. package/lib/services/probe.js +1 -1
  18. package/lib/types/http/catalog.d.ts +10 -0
  19. package/lib/types/http/diagnostics.d.ts +4 -0
  20. package/lib/types/http/install.d.ts +11 -0
  21. package/lib/types/http/logs.d.ts +2 -0
  22. package/lib/types/http/routes.d.ts +15 -20
  23. package/lib/types/http/settings.d.ts +2 -0
  24. package/lib/types/http/shared.d.ts +44 -0
  25. package/lib/types/http/task.d.ts +3 -0
  26. package/lib/types/services/install/install.d.ts +1 -1
  27. package/lib/types/services/install/package-entry.d.ts +42 -10
  28. package/lib/types/services/install/preflight.d.ts +9 -1
  29. package/lib/types/services/install/task-queue.d.ts +6 -4
  30. package/package.json +1 -1
  31. package/src/client/components/PluginHubSection.tsx +3 -0
  32. package/src/client/components/modals/NotificationsModal.tsx +34 -18
  33. package/src/client/components/modals/modals.tsx +38 -28
  34. package/src/client/components/views/InstalledView.tsx +23 -11
  35. package/src/client/hooks/useCatalog.ts +1 -1
  36. package/src/client/locales.ts +28 -12
  37. package/src/client/logic/failures.ts +36 -157
  38. package/src/client/logic/urls.ts +75 -4
  39. package/src/client/types.ts +1 -1
  40. package/src/server/http/catalog.ts +143 -0
  41. package/src/server/http/diagnostics.ts +193 -0
  42. package/src/server/http/install.ts +391 -0
  43. package/src/server/http/logs.ts +149 -0
  44. package/src/server/http/routes.ts +22 -1222
  45. package/src/server/http/settings.ts +73 -0
  46. package/src/server/http/shared.ts +191 -0
  47. package/src/server/http/task.ts +194 -0
  48. package/src/server/services/install/install-types.ts +9 -1
  49. package/src/server/services/install/install.ts +11 -1
  50. package/src/server/services/install/package-entry.ts +72 -9
  51. package/src/server/services/install/preflight.ts +47 -18
  52. package/src/server/services/install/task-queue.ts +73 -34
  53. package/src/server/services/probe.ts +1 -1
@@ -3,10 +3,10 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball)里是否
7
- * 真的包含 package.json 声明的入口文件(main / exports["."].default)。
8
- * git 分发常不提交构建产物(lib/ 等),这类残缺包装完会让宿主重启加载插件树时
9
- * ERR_MODULE_NOT_FOUND 直接崩溃 —— 预检在动手安装前就把它们拦下,避免白装一次。
6
+ * 安装前预检:对 GitHub 源的插件,检查其分发(codeload tarball)里是否真的包含
7
+ * package.json 声明的文件(main / exports["."] / dsh.bundle.patch)。git 分发常不提交
8
+ * 构建产物(lib/ 等),这类残缺包装完会让宿主重启加载插件树时 ERR_MODULE_NOT_FOUND
9
+ * 直接崩溃 —— 预检在动手安装前就把它们拦下,避免白装一次。
10
10
  * npm 包信任 registry 的完整性,直接放行;任何无法预检的情况也放行,交给装后
11
11
  * 校验(verifyInstalledEntry)兜底,保证预检自身失败不会卡死安装流程。
12
12
  */
@@ -19,7 +19,7 @@ import { pipeline } from 'node:stream/promises'
19
19
  import { promisify } from 'node:util'
20
20
  import { githubRefOf, githubRepoOf } from '../profile/profile.ts'
21
21
  import { githubReleaseTarget } from './release-target.ts'
22
- import { resolvePackageEntry } from './package-entry.ts'
22
+ import { requiredDistributionFiles, resolveBundlePatchFiles } from './package-entry.ts'
23
23
 
24
24
  const execFileAsync = promisify(execFile)
25
25
 
@@ -27,7 +27,8 @@ const execFileAsync = promisify(execFile)
27
27
  * 否则 `/install` 请求永远不返回、任务不入队、前端进度卡 0%。超时后放行(交给装后校验)。 */
28
28
  const PREFLIGHT_TIMEOUT_MS = 15_000
29
29
 
30
- /** 预检结果:ok=false 表示分发改入口文件缺失,missing 为该文件在包内的相对路径;
30
+ /** 预检结果:ok=false 表示分发改 package.json 声明的文件(入口或 bundle 补丁),
31
+ * missing 为该文件在包内的相对路径;
31
32
  * name 为 git 分发包 package.json 声明的包名(非 git 目标/无法确定时为 null),
32
33
  * 供安装路由做「包名冲突」检测 —— 同名包名已被其他来源占用时,pnpm 装前必然撞车,
33
34
  * 需要把晦涩的 CLI 报错转成明确的拦截。 */
@@ -57,9 +58,17 @@ export async function preflightTarget(target: string): Promise<PreflightResult>
57
58
  const rootPrefix = rootPackage.slice(0, -'package.json'.length)
58
59
  const meta = await readTarJson(tar, rootPackage)
59
60
  if (meta === null) return { ok: true, missing: null, name: null }
60
- const entry = resolvePackageEntry(meta)
61
- const inTar = await hasTarEntry(tar, `${rootPrefix}${entry}`)
62
- return { ok: inTar, missing: inTar ? null : entry, name: typeof meta.name === 'string' ? meta.name : null }
61
+ // 只校验作者声明过的文件:没声明入口的包(只挂技能的 bundle)没有可缺失的入口,
62
+ // 它的分发是否完整由 dsh.bundle.patch 是否在包里决定;补丁若按本包名 mount 自己,
63
+ // 入口又变成必须的(宿主会 import 本包)—— 所以判入口前先把补丁读出来。
64
+ const patchTexts: string[] = []
65
+ for (const file of resolveBundlePatchFiles(meta)) {
66
+ const text = await readTarText(tar, `${rootPrefix}${file}`)
67
+ if (text !== null) patchTexts.push(text)
68
+ }
69
+ const required = requiredDistributionFiles(meta, patchTexts).map((file) => `${rootPrefix}${file}`)
70
+ const missing = missingDistributionFileOfTarList(await tarListing(tar), required)
71
+ return { ok: missing === null, missing, name: typeof meta.name === 'string' ? meta.name : null }
63
72
  } catch {
64
73
  return { ok: true, missing: null, name: null }
65
74
  } finally {
@@ -125,31 +134,51 @@ export function rootPackageEntryOfTarList(listing: string): string | null {
125
134
  return candidates.length === 1 ? candidates[0] : null
126
135
  }
127
136
 
137
+ /**
138
+ * 清单里第一个缺失的必需文件(成员名带包根前缀,由调用方拼好),全部都在则为 null。
139
+ * 清单不可读(tar 缺失/解压失败)时按第一个必需文件缺失处理:与逐文件查询时
140
+ * 「查不到就是缺」的判定一致,宁可拦下一次可疑安装,也不放一个空包过去。
141
+ * Exported for a small parser regression test.
142
+ */
143
+ export function missingDistributionFileOfTarList(listing: string | null, required: string[]): string | null {
144
+ if (required.length === 0) return null
145
+ if (listing === null) return required[0]
146
+ const present = new Set(listing.split(/\r?\n/))
147
+ return required.find((file) => !present.has(file)) ?? null
148
+ }
149
+
128
150
  async function rootPackageEntry(tar: string): Promise<string | null> {
151
+ const listing = await tarListing(tar)
152
+ return listing === null ? null : rootPackageEntryOfTarList(listing)
153
+ }
154
+
155
+ /** tar 成员清单;`tar` 执行失败时返回 null(调用方按无法判定处理)。 */
156
+ async function tarListing(tar: string): Promise<string | null> {
129
157
  try {
130
158
  const { stdout } = await execFileAsync('tar', ['-tzf', tar], { maxBuffer: 16 * 1024 * 1024 })
131
- return rootPackageEntryOfTarList(stdout)
159
+ return stdout
132
160
  } catch {
133
161
  return null
134
162
  }
135
163
  }
136
164
 
137
- /** 从 tarball 提取并解析一个 JSON 文件;文件不存在或解析失败返回 null。 */
138
- async function readTarJson(tar: string, entry: string): Promise<Record<string, unknown> | null> {
165
+ /** 从 tarball 提取一个文件的内容;文件不存在或提取失败返回 null。 */
166
+ async function readTarText(tar: string, entry: string): Promise<string | null> {
139
167
  try {
140
168
  const { stdout } = await execFileAsync('tar', ['-xzOf', tar, entry], { maxBuffer: 4 * 1024 * 1024 })
141
- return JSON.parse(stdout) as Record<string, unknown>
169
+ return stdout
142
170
  } catch {
143
171
  return null
144
172
  }
145
173
  }
146
174
 
147
- /** 检查 tarball 内是否存在指定 entry。 */
148
- async function hasTarEntry(tar: string, entry: string): Promise<boolean> {
175
+ /** 从 tarball 提取并解析一个 JSON 文件;文件不存在或解析失败返回 null。 */
176
+ async function readTarJson(tar: string, entry: string): Promise<Record<string, unknown> | null> {
177
+ const text = await readTarText(tar, entry)
178
+ if (text === null) return null
149
179
  try {
150
- const { stdout } = await execFileAsync('tar', ['-tzf', tar, entry], { maxBuffer: 1024 * 1024 })
151
- return stdout.split(/\r?\n/).some((line) => line === entry)
180
+ return JSON.parse(text) as Record<string, unknown>
152
181
  } catch {
153
- return false
182
+ return null
154
183
  }
155
184
  }
@@ -8,7 +8,7 @@
8
8
  * 输出流式收集到任务上;卸载成功时尝试从运行中 loader 即时停用(见 loader.ts)。
9
9
  */
10
10
  import { spawn, spawnSync } from 'node:child_process'
11
- import { resolvePackageEntry } from './package-entry.ts'
11
+ import { requiredDistributionFiles, resolveBundlePatchFiles, type PackageManifest } from './package-entry.ts'
12
12
  import { existsSync, readFileSync, statSync } from 'node:fs'
13
13
  import { dirname, join, resolve } from 'node:path'
14
14
  import type { InstallResult, InstallTask, Invocation, QueueItem } from './install-types.ts'
@@ -58,12 +58,30 @@ export function activeTask(): ActiveTaskInfo[] {
58
58
  for (const item of queue) {
59
59
  const task = item.task
60
60
  if (task.status === 'pending') {
61
- found.push({ id: task.id, target: show(task), action: task.action, status: 'pending', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel })
61
+ found.push({
62
+ id: task.id,
63
+ target: show(task),
64
+ action: task.action,
65
+ status: 'pending',
66
+ progress: task.progress,
67
+ lines: task.lines.slice(0, MAX_TASK_LINES),
68
+ attempts: task.attempts,
69
+ installChannel: task.installChannel,
70
+ })
62
71
  }
63
72
  }
64
73
  tasks.forEach((task) => {
65
74
  if (task.status === 'running') {
66
- found.unshift({ id: task.id, target: show(task), action: task.action, status: 'running', progress: task.progress, lines: task.lines.slice(0, MAX_TASK_LINES), attempts: task.attempts, installChannel: task.installChannel })
75
+ found.unshift({
76
+ id: task.id,
77
+ target: show(task),
78
+ action: task.action,
79
+ status: 'running',
80
+ progress: task.progress,
81
+ lines: task.lines.slice(0, MAX_TASK_LINES),
82
+ attempts: task.attempts,
83
+ installChannel: task.installChannel,
84
+ })
67
85
  }
68
86
  })
69
87
  return found
@@ -134,7 +152,7 @@ function cliInvocation(profile: string): Invocation {
134
152
  if (entry !== undefined) {
135
153
  // 打包桌面端(Electron):宿主入口在 app.asar 内,机器上根本没有 `dsh` 可执行文件
136
154
  //(Windows 那条 dsh.cmd shim 只存在于宿主进程自己的 PATH 里),直接回退 PATH 必然
137
- // 报 [dsh-missing](见 issue #71)。这里改用应用自己的 CLI 引导脚本调起同一份打包
155
+ // 报 [dsh-missing]。这里改用应用自己的 CLI 引导脚本调起同一份打包
138
156
  // CLI:Electron 可执行文件 + --expose-internals + desktop-cli.js,与内置终端、
139
157
  // 官方插件页安装完全同一条路径 —— desktop profile 由应用独占管理的授权也在其中。
140
158
  const bootstrap = desktopCliBootstrap(entry)
@@ -189,9 +207,8 @@ export function desktopCliBootstrap(entry: string): string | null {
189
207
 
190
208
  /**
191
209
  * dsh 命令存在性探测:which/where 找 PATH 里的 dsh。
192
- * Windows shell 模式命令缺失时 cmd 只输出 GBK 乱码「不是内部或外部命令」(UTF-8 解码后
193
- * 无法匹配),POSIX 非 shell 模式才会抛 spawn dsh ENOENT —— 两种形态都不可靠,统一在
194
- * spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
210
+ * 命令缺失时各平台报错形态不同(Windows shell 只回 GBK 乱码、POSIX 才抛 spawn ENOENT),
211
+ * 统一在 spawn 前用 which/where 探测,命令不存在则直接失败并打 ASCII 标记 [dsh-missing](乱码免疫)。
195
212
  * 探测自身异常(which/where 不可用等)不拦截安装,交给装后校验兜底。
196
213
  */
197
214
  function dshOnPath(env?: NodeJS.ProcessEnv): boolean {
@@ -270,7 +287,22 @@ export function startPluginMutation(options: {
270
287
  // 卸载入队时先快照「该包是否带客户端 UI」:卸载完成后包已从磁盘删除,读不到 package.json,
271
288
  // 只能在此刻取;卸载成功且曾存活时据此决定是否要刷新页面摘除插件面板
272
289
  const hadClientUi = options.action === 'remove' ? hasClientUi(options.profile, options.target) : undefined
273
- const task: InstallTask = { id: nextTaskId, target: options.target, displayTarget: options.displayTarget, globalNpm: options.globalNpm, action: options.action, status: 'pending', timedOut: false, exitCode: null, progress: 0, lines: [], attempts: options.attempts ?? [], needsRestart: false, hadClientUi, installChannel: options.installChannel }
290
+ const task: InstallTask = {
291
+ id: nextTaskId,
292
+ target: options.target,
293
+ displayTarget: options.displayTarget,
294
+ globalNpm: options.globalNpm,
295
+ action: options.action,
296
+ status: 'pending',
297
+ timedOut: false,
298
+ exitCode: null,
299
+ progress: 0,
300
+ lines: [],
301
+ attempts: options.attempts ?? [],
302
+ needsRestart: false,
303
+ hadClientUi,
304
+ installChannel: options.installChannel,
305
+ }
274
306
  nextTaskId = nextTaskId >= Number.MAX_SAFE_INTEGER ? 1 : nextTaskId + 1
275
307
  tasks.set(task.id, task)
276
308
  if (tasks.size > MAX_TASKS) {
@@ -416,8 +448,8 @@ function spawnMutation(options: {
416
448
  let child: ReturnType<typeof spawn>
417
449
  try {
418
450
  // 独立 CLI 场景(file: 'dsh',宿主加载器走 node bin 不经过这里):先探测 dsh 是否在 PATH。
419
- // 命令不存在时 Windows shell 模式只回传 GBK 乱码、POSIX 才抛 spawn dsh ENOENT —— 统一前置
420
- // 拦截并打 ASCII 标记 [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
451
+ // 命令缺失时各平台报错形态不同(GBK 乱码 / spawn ENOENT),统一前置拦截并打 ASCII 标记
452
+ // [dsh-missing],前端据此精准提示「dsh 未加入 PATH」而不是笼统的插件失败。
421
453
  if (invocation.file === 'dsh' && !dshOnPath(env)) {
422
454
  const detail = '[dsh-missing] the dsh CLI was not found on this machine (DeepSeek Harness not installed, or dsh not on the PATH)'
423
455
  if (task) {
@@ -535,12 +567,9 @@ function spawnMutation(options: {
535
567
 
536
568
  return (async () => {
537
569
  let result = await spawnOnce(args)
538
- // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`):
539
- // 某些包的 peerDependencies 用「双段 prerelease 范围」(如 dsh-tui@0.9.0 的 `^0.1.0-rc.6 || ^0.1.1-rc.1`,
540
- // 26 个必需 peer 全是这种形状)会让 `npm install -g` 在解析阶段直接崩溃 —— 退出码 1、什么都没装,
541
- // 与是不是从我们界面发起无关(终端里同样复现)。首次失败后自动带 `--legacy-peer-deps` 重试一次:
542
- // 跳过 peer 自动解析即可绕过该 bug(peer 通常由同装的 @deepseek-ai/dsh 依赖树天然提供)。
543
- // 二次尝试仍失败则保留其错误信息,不掩盖真正的原因。
570
+ // npm 11 的 arborist peer-set bug(`Cannot read properties of null (reading 'children')`)
571
+ // 会让 `npm install -g` 在解析阶段直接崩溃。首次失败后自动带 `--legacy-peer-deps` 重试一次:
572
+ // 跳过 peer 自动解析即可绕过该 bug;二次尝试仍失败则保留其错误信息,不掩盖真正的原因。
544
573
  if (isGlobalNpm && result.exitCode !== 0 && !result.timedOut && task?.status !== 'cancelled') {
545
574
  const retryArgs = ['install', '-g', '--legacy-peer-deps', ...(globalNpm ?? [])]
546
575
  const retryExecuted = `npm install -g --legacy-peer-deps ${(globalNpm ?? []).join(' ')}`
@@ -596,10 +625,12 @@ function spawnMutation(options: {
596
625
  }
597
626
 
598
627
  /**
599
- * 安装后校验已装包的入口文件(package.json 的 main / exports["."].default)是否真实存在。
600
- * git: 分发常不提交构建产物(lib/ 等),pnpm 装完没有报错,但宿主重启加载插件树时会
601
- * ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在登记「待重启」前就把这类残缺包拦下。
602
- * 返回 { name, missing }:name 为空表示无法定位目标包(跳过校验);missing 为缺失的入口路径。
628
+ * 安装后校验已装包里 package.json 声明的文件(main / exports["."].default 与
629
+ * dsh.bundle.patch)是否真实存在。git: 分发常不提交构建产物(lib/ 等),pnpm 装完没有
630
+ * 报错,但宿主重启加载插件树时会 ERR_MODULE_NOT_FOUND 直接崩溃、网页打不开 —— 这里在
631
+ * 登记「待重启」前就把这类残缺包拦下。没有声明过的文件不在这里追究:没声明入口的
632
+ * bundle 包(只挂技能)本来就不需要入口,宿主读的是它的 package.json 与声明的补丁。
633
+ * 返回 { name, missing }:name 为空表示无法定位目标包(跳过校验);missing 为缺失文件的路径。
603
634
  */
604
635
  export function verifyInstalledEntry(profile: string, target: string): { name: string | null; missing: string | null } {
605
636
  let profilePkg: Record<string, unknown> | null = null
@@ -616,21 +647,29 @@ export function verifyInstalledEntry(profile: string, target: string): { name: s
616
647
  : deps[target] !== undefined ? target : Object.keys(deps).find((k) => deps[k] === target)
617
648
  if (!name) return { name: null, missing: null }
618
649
  const pkgDir = join(profileDirectory(profile), 'node_modules', name)
619
- let entry: string | null = null
650
+ let required: string[] = []
620
651
  try {
621
- const meta = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')) as {
622
- main?: unknown
623
- exports?: unknown
624
- }
625
- entry = resolvePackageEntry(meta)
652
+ const meta = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')) as PackageManifest
653
+ // 补丁按本包名 mount 自己时入口也是必须的,所以先读已装包里的补丁(读不到就当没有)。
654
+ const patchTexts = resolveBundlePatchFiles(meta).flatMap((file) => {
655
+ try {
656
+ return [readFileSync(join(pkgDir, file), 'utf8')]
657
+ } catch {
658
+ return []
659
+ }
660
+ })
661
+ required = requiredDistributionFiles(meta, patchTexts)
626
662
  } catch {
627
663
  return { name, missing: null }
628
664
  }
629
- try {
630
- return { name, missing: statSync(join(pkgDir, entry)).isFile() ? null : entry }
631
- } catch {
632
- return { name, missing: entry }
665
+ for (const file of required) {
666
+ try {
667
+ if (!statSync(join(pkgDir, file)).isFile()) return { name, missing: file }
668
+ } catch {
669
+ return { name, missing: file }
670
+ }
633
671
  }
672
+ return { name, missing: null }
634
673
  }
635
674
 
636
675
  /** 从 pnpm `Ignored build scripts:` 提示行解析被拦截的构建脚本 key(name@version,逗号分隔)。 */
@@ -645,7 +684,7 @@ function ignoredBuildKeys(output: string): string[] {
645
684
 
646
685
  /** npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 把 [npm-too-low] 标记追加到任务输出,
647
686
  * 前端据此展示「本机 npm 版本过低」的准确原因 —— 避免该报错被外层 ERR_PNPM_PREPARE_PACKAGE
648
- * 误判成插件打包/分发问题、误导用户提 Issue。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
687
+ * 误判成插件打包/分发问题。检测逻辑在独立的 npm-check.ts(本机 npm 环境职责)。 */
649
688
  function annotateNpmTooLow(result: InstallResult, options: {
650
689
  action: 'add' | 'remove' | 'update'
651
690
  task?: InstallTask
@@ -720,21 +759,21 @@ export async function runPluginMutation(options: {
720
759
  }
721
760
  }
722
761
  }
723
- // pnpm 安装本身成功,但目标包入口文件缺失(git 分发缺构建产物)→ 判为失败:
762
+ // pnpm 安装本身成功,但目标包声明分发的文件缺失(git 分发缺构建产物 / 缺声明的补丁)→ 判为失败:
724
763
  // 撤销刚登记的待重启,避免用户重启时宿主加载残缺包直接崩溃。
725
764
  // 全局 npm 安装不进 profile(profile deps 里没有这些包),跳过入口校验
726
765
  if (options.action !== 'remove' && result.exitCode === 0 && !result.timedOut && (options.globalNpm ?? []).length === 0) {
727
766
  const verified = verifyInstalledEntry(options.profile, options.target)
728
767
  if (verified.name !== null && verified.missing !== null) {
729
768
  clearPendingRestart(options.displayTarget ?? options.target)
730
- const detail = `[packaging] ${verified.name}: entry file missing: ${verified.missing} (git distribution lacks build output — install the npm version or report to the author)`
769
+ const detail = `[packaging] ${verified.name}: declared file missing: ${verified.missing} (git distribution lacks build output — install the npm version or report to the author)`
731
770
  if (options.task && options.task.status !== 'cancelled') {
732
771
  options.task.status = 'failed'
733
772
  options.task.exitCode = null
734
773
  options.task.progress = 0
735
774
  pushLine(options.task, detail)
736
775
  }
737
- return { exitCode: 1, timedOut: false, error: `entry file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` }
776
+ return { exitCode: 1, timedOut: false, error: `declared file missing: ${verified.missing}`, stdout: result.stdout, stderr: `${result.stderr}\n${detail}` }
738
777
  }
739
778
  }
740
779
  // npm arborist edgesOut 崩溃 + 本机 npm 版本过低 → 追加 [npm-too-low] 标记,前端据此给准确原因
@@ -165,7 +165,7 @@ function runCurl(args: string[], env: NodeJS.ProcessEnv, timeoutMs: number): Pro
165
165
  * 若本机 curl 不认这个参数(curl < 7.70)则去掉参数重跑一次,保证老版本不因此直接失败;
166
166
  * 降级后仍被吊销检查拦死(CRYPT_E_NO_REVOCATION_CHECK)时,再用 --ssl-no-revoke
167
167
  * 彻底关掉吊销检查跑一次 —— 该参数 curl ≥ 7.19 即存在,覆盖面比降级参数更广,
168
- * 否则「能上网但取不到 CRL」的 Windows 机器会把所有通道误报成不可达(见 issue #71)。
168
+ * 否则「能上网但取不到 CRL」的 Windows 机器会把所有通道误报成不可达。
169
169
  * 吊销检查只影响本次探测自身的信任判断,不改变系统 TLS 配置。 */
170
170
  async function curlWithTlsFallback(args: string[], env: NodeJS.ProcessEnv, timeoutMs: number): Promise<CurlRun> {
171
171
  const tlsArgs = curlTlsArgs()