dsh-plugin 1.6.11 → 1.6.12

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.
@@ -9,9 +9,9 @@
9
9
  */
10
10
  import type { EnvInfo, HubPlugin, LocaleId } from '../types.ts'
11
11
  import { GITHUB_URL, HUB_REPO, PLUGIN_VERSION, SITE_URL } from './constants.ts'
12
- import { MAX_CORE_CHARS, classifyFailure, coreErrorCode, pnpmPolicyHintOf, summarizeError } from './failures.ts'
12
+ import { MAX_CORE_CHARS, actionWordEn, actionWordZh, classifyFailure, coreErrorCode, pnpmPolicyHintOf, summarizeError } from './failures.ts'
13
13
  import { executedCommandOf } from './install-command.ts'
14
- import type { FailureKind } from './failures.ts'
14
+ import type { FailureAction, FailureKind } from './failures.ts'
15
15
 
16
16
  /** dsh-plugin.org 中文页挂在 /zh/ 前缀下,英文在根路径。 */
17
17
  export function langPathOf(lang: LocaleId): string {
@@ -41,18 +41,18 @@ export function pluginSiteUrl(repo: string): string {
41
41
  return siteUrl(`plugins/${repo}`)
42
42
  }
43
43
 
44
- /** 按失败类型给出简洁的错误原因标题(对外用英文):
45
- * 标题直接点明问题出在哪一侧(构建白名单/分发物/本机 npm/网络/插件侧),
46
- * 让作者与用户扫一眼列表就能分流,而不是笼统的 Install/Remove 动作词。 */
47
- function reasonTitleOf(kind: FailureKind): string {
44
+ /** 错误原因标题(对外英文):点明问题出在哪一侧,便于 issue 列表分流;
45
+ * 动作级原因(未分类/超时…)带动作词,卸载失败不写成 install failed。 */
46
+ function reasonTitleOf(kind: FailureKind, action: FailureAction): string {
47
+ const word = actionWordEn(action)
48
48
  switch (kind) {
49
- case 'npmTooOld': return 'npm too old to install'
49
+ case 'npmTooOld': return `npm too old for this ${word}`
50
50
  case 'dshMissing': return 'dsh command not found on the user machine'
51
51
  case 'pnpmMissing': return 'pnpm command not found on the user machine'
52
52
  case 'pnpmStore': return 'pnpm store version mismatch on the user machine'
53
- case 'pnpmWorkspace': return 'pnpm workspace-root check blocked the install on the user machine'
53
+ case 'pnpmWorkspace': return `pnpm workspace-root check blocked the ${word} on the user machine`
54
54
  case 'originRejected': return 'request origin rejected by the local service on the user machine'
55
- case 'pnpmPolicy': return 'pnpm supply-chain policy blocked the install on the user machine'
55
+ case 'pnpmPolicy': return `pnpm supply-chain policy blocked the ${word} on the user machine`
56
56
  case 'pnpmUnusedPatch': return 'stale pnpm patch entry on the user machine'
57
57
  case 'pnpmMissingDep': return 'dependency not found on the npm registry on the user machine'
58
58
  case 'registryVersionStale': return 'requested version not available on the npm registry in use on the user machine'
@@ -63,12 +63,13 @@ function reasonTitleOf(kind: FailureKind): string {
63
63
  case 'pnpmIgnoredBuild': return 'build scripts blocked by pnpm allowlist'
64
64
  case 'pluginPrepare': return 'plugin distribution incomplete'
65
65
  case 'network': return 'network failure on the user side'
66
- case 'hubState': return 'stale install state in the plugin hub'
67
- case 'timeout': return 'install timed out'
66
+ case 'hostCliSpawn': return 'the DSH host could not be launched on the user machine'
67
+ case 'hubState': return `stale ${word} state in the plugin hub`
68
+ case 'timeout': return `${word} timed out`
68
69
  case 'profileLock': return 'DSH profile is locked by another process'
69
70
  case 'pluginMissingDep': return 'a dependency of the plugin is not published on npm'
70
- case 'hub': return 'install failed (cause unclassified)'
71
- default: return 'plugin install failed'
71
+ case 'hub': return `${word} failed (cause unclassified)`
72
+ default: return `plugin ${word} failed`
72
73
  }
73
74
  }
74
75
 
@@ -81,14 +82,16 @@ function reasonTitleOf(kind: FailureKind): string {
81
82
  * attempts(尝试过的安装方式,npm 反查 + 实际执行命令):作者看到我们查过/试过的命令,
82
83
  * 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
83
84
  */
84
- export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
85
+ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[], action: FailureAction = 'install'): string {
85
86
  // 原因判定:按失败类型归类,便于作者判断问题是否出在插件侧。
86
87
  // [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
87
88
  // (package.json 声明的文件在发布物里缺失),作者照此补齐即可。
88
89
  const kind = classifyFailure(message)
89
90
  const executed = executedCommandOf(command, attempts)
91
+ // 动作词:标题与正文的「安装/更新/卸载」统一取自这里
92
+ const zhWord = actionWordZh(action)
90
93
  // 标题 = 错误原因(非动作词),让 issue 列表一眼可分流
91
- const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
94
+ const title = `[dsh-plugin.org | dsh-plugin-hub] ${reasonTitleOf(kind, action)}: ${repo}`
92
95
  const reason = /\[packaging\]/i.test(message)
93
96
  ? 'plugin distribution is incomplete — a file the package needs (its entry from main / exports, an entry a bundled patch mounts, or the patch declared by dsh.bundle.patch) is missing from the published package (github tarball or npm package); please commit build output or publish a complete package'
94
97
  : /\[registry-missing\]/i.test(message)
@@ -123,16 +126,15 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
123
126
  `- Cause: ${reason}`,
124
127
  ...(code ? [`- Key error: \`${code}\``] : []),
125
128
  '',
126
- // 来源说明:标题(链官网)+ 一句来源(链仓库)+ 实际执行的安装命令(含 --profile)与执行结果,链接由常量动态拼接
127
- `## [DSH Plugin 插件市场](${SITE_URL}) · 安装 Plugin 失败错误信息`,
128
- `本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件市场的安装程序自动生成,随本次安装失败一并提交。`,
129
- `- 实际执行的安装命令:${executed ? `\`${executed}\`` : 'unknown (no executed command recorded)'}`,
130
- `- 执行结果:安装失败,未能安装该插件。`,
131
- // 尝试过的安装方式(npm 反查 + 实际执行命令,按先后顺序):作者据此反推正确的
132
- // npm 包名 —— 组织 scope 与 GitHub 用户名不一致时仅凭仓库名猜不到,作者看到我们
133
- // 查过/试过的命令就能直接指认
129
+ // 来源说明:标题(链官网)+ 一句来源(链仓库)+ 实际执行的命令(含 --profile)与执行结果,链接由常量动态拼接
130
+ `## [DSH Plugin 插件市场](${SITE_URL}) · ${zhWord} Plugin 失败错误信息`,
131
+ `本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件市场的安装程序自动生成,随本次${zhWord}失败一并提交。`,
132
+ `- 实际执行的${zhWord}命令:${executed ? `\`${executed}\`` : 'unknown (no executed command recorded)'}`,
133
+ `- 执行结果:${zhWord}失败,未能${zhWord}该插件。`,
134
+ // 尝试过的通道(npm 反查 + 实际执行命令,按先后顺序):作者据此指认正确的 npm 包名。
135
+ // 不写「安装」:卸载失败也走这条正文。
134
136
  ...(attempts && attempts.length > 0
135
- ? ['', '## Attempted install channels(已尝试的安装方式)', ...attempts.map((a) => `- ${a}`)]
137
+ ? ['', '## Attempted channels(已尝试的方式)', ...attempts.map((a) => `- ${a}`)]
136
138
  : []),
137
139
  // [packaging] 场景:说明三种官方安装通道(npm 包 / git 直装 / 预构建 release .tgz),当前插件在对应通道下分发不完整
138
140
  ...(/\[packaging\]/i.test(message)
@@ -210,19 +212,22 @@ export function hubIssueUrl(
210
212
  attempts?: string[],
211
213
  /** 见 HubIssueVariant;缺省 'triage',既有正文保持不变 */
212
214
  variant: HubIssueVariant = 'triage',
215
+ /** 上报动作:标题与正文按它取词 */
216
+ action: FailureAction = 'install',
213
217
  ): string {
214
218
  const kind = classifyFailure(message)
215
219
  const executed = executedCommandOf(command, attempts)
220
+ const zhWord = actionWordZh(action)
216
221
  const repo = pluginRepo && pluginRepo.trim() !== '' ? pluginRepo.trim() : 'unknown'
217
222
  const title = variant === 'misjudge'
218
223
  // 标题就点明「可能是误判」,维护者一眼能区分它和未分类兜底
219
224
  ? `[dsh-plugin-hub] possible misjudgment: ${repo}`
220
- : `[dsh-plugin-hub] ${reasonTitleOf(kind)}: ${repo}`
225
+ : `[dsh-plugin-hub] ${reasonTitleOf(kind, action)}: ${repo}`
221
226
  const code = coreErrorCode(message)
222
227
  const build = (coreChars: number): string => {
223
228
  const body = [
224
229
  '## Summary',
225
- `- Auto-classified cause: ${reasonTitleOf(kind)}`,
230
+ `- Auto-classified cause: ${reasonTitleOf(kind, action)}`,
226
231
  `- Target plugin: \`${repo}\``,
227
232
  ...(code ? [`- Key error: \`${code}\``] : []),
228
233
  // 为什么投递到这里而非插件仓库:只有确有证据是插件自身构建失败(pluginPrepare)才投插件仓库
@@ -236,12 +241,12 @@ export function hubIssueUrl(
236
241
  ]
237
242
  : ['- Why filed here: the installer only routes a failure to the plugin repo when there is positive evidence the plugin build itself failed (pluginPrepare). This failure does not match that, so it is routed here for triage.']),
238
243
  '',
239
- `## [DSH Plugin 插件市场](${SITE_URL}) · 安装 Plugin 失败错误信息`,
240
- `本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件市场的安装程序自动生成,随本次安装失败一并提交。`,
241
- `- 实际执行的安装命令:${executed ? `\`${executed}\`` : 'unknown (no executed command recorded)'}`,
242
- `- 执行结果:安装失败,未能安装该插件。`,
244
+ `## [DSH Plugin 插件市场](${SITE_URL}) · ${zhWord} Plugin 失败错误信息`,
245
+ `本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件市场的安装程序自动生成,随本次${zhWord}失败一并提交。`,
246
+ `- 实际执行的${zhWord}命令:${executed ? `\`${executed}\`` : 'unknown (no executed command recorded)'}`,
247
+ `- 执行结果:${zhWord}失败,未能${zhWord}该插件。`,
243
248
  ...(attempts && attempts.length > 0
244
- ? ['', '## Attempted install channels(已尝试的安装方式)', ...attempts.map((a) => `- ${a}`)]
249
+ ? ['', '## Attempted channels(已尝试的方式)', ...attempts.map((a) => `- ${a}`)]
245
250
  : []),
246
251
  '',
247
252
  '## Environment',
@@ -3,20 +3,9 @@
3
3
  * Website: https://dsh-plugin.org
4
4
  * GitHub: https://github.com/dshplugin/dsh-plugin-hub
5
5
  *
6
- * 官方 registry 存在性复核:把「包在 registry 上不存在」这类失败的责任方分开。
7
- *
8
- * 背景:安装失败日志出现 `ERR_PNPM_FETCH_404` / `Not Found - 404` 时,光看日志分不清三种情形:
9
- * ① 缺的包是 **profile 自己声明的依赖**(历史残留条目,指向一个从未发布的包)—— 用户环境问题:
10
- * pnpm 每次解析依赖都会先卡在它上面,任何插件都装不进来(issue #73),提示删掉该条目即可;
11
- * ② 缺的包来自**插件的依赖树**,且该包在官方 registry 上也不存在 —— 作者侧问题(漏声明依赖、
12
- * 依赖被删除或从未发布),应引导用户投插件作者仓库;
13
- * ③ 缺的包来自插件依赖树,但官方 registry 上有 —— 用户本机 registry 指向的镜像/私有源未同步
14
- * 该包,属用户环境问题,提示换源即可。
15
- * 故本模块先排除 ①(读 profile 的 package.json),再用**官方** registry(registry.npmjs.org,
16
- * 绕开本机可能配置的镜像)复查存在性,产出 ② / ③ 的标记行。
17
- *
18
- * 与 npm-check.ts 同构:本模块只产出「标记行」,由 task-queue 追加到任务输出,前端按标记分类。
19
- * fetch 与 profile 目录都可注入,便于单测覆盖全部结论而不依赖网络。
6
+ * 官方 registry 存在性复核:用官方源(绕开本机镜像)复查 404 的包,产出标记行区分
7
+ * 「profile 残留依赖」「插件依赖在官方源也不存在(作者侧)」「本机镜像未同步」三种情形;
8
+ * 标记由 task-queue 追加到任务输出,前端按标记归类。fetch 与 profile 目录可注入,便于单测。
20
9
  */
21
10
  import { readFileSync } from 'node:fs'
22
11
  import { join } from 'node:path'
@@ -149,6 +149,13 @@ function pushLine(task: InstallTask, line: string): void {
149
149
  }
150
150
  }
151
151
 
152
+ /** spawn 的 cwd:profile 目录(asar 路径不能作 cwd);目录不存在时回退进程工作目录,
153
+ * 否则 Node 报的是 `spawn <file> ENOENT`,与「可执行文件不存在」无法区分。 */
154
+ function spawnCwdFor(profile: string): string {
155
+ const dir = profileDirectory(profile)
156
+ return existsSync(dir) ? dir : process.cwd()
157
+ }
158
+
152
159
  function cliInvocation(profile: string): Invocation {
153
160
  const entry = process.argv[1]
154
161
  if (entry !== undefined) {
@@ -162,8 +169,8 @@ function cliInvocation(profile: string): Invocation {
162
169
  return {
163
170
  file: process.execPath,
164
171
  prefixArgs: ['--expose-internals', bootstrap],
165
- // 工作目录用 profile 目录(真实存在的目录):asar 内的路径不能作为 spawn 的 cwd
166
- cwd: profileDirectory(profile),
172
+ // cwd 用 profile 目录(asar 路径不能作 cwd),不存在时由 spawnCwdFor 回退
173
+ cwd: spawnCwdFor(profile),
167
174
  useShell: false,
168
175
  // Electron 可执行文件只有带这个变量才按 Node 运行引导脚本
169
176
  env: { ELECTRON_RUN_AS_NODE: '1' },
@@ -475,15 +482,19 @@ function spawnMutation(options: {
475
482
  stdio: ['ignore', 'pipe', 'pipe'],
476
483
  })
477
484
  } catch (error) {
485
+ const detail = error instanceof Error ? error.message : String(error)
486
+ // 宿主自身入口起不来 → 补标记,前端据此归因
487
+ const marker = hostSpawnMarker(invocation, error)
478
488
  if (task) {
479
489
  task.status = 'failed'
480
490
  task.exitCode = null
481
- pushLine(task, `[error] ${error instanceof Error ? error.message : String(error)}`)
491
+ pushLine(task, `[error] ${detail}`)
492
+ if (marker !== null) pushLine(task, marker)
482
493
  }
483
494
  resolveOnce({
484
495
  exitCode: null,
485
496
  timedOut: false,
486
- error: error instanceof Error ? error.message : String(error),
497
+ error: marker === null ? detail : `${detail}\n${marker}`,
487
498
  stdout,
488
499
  stderr,
489
500
  })
@@ -554,12 +565,15 @@ function spawnMutation(options: {
554
565
  })
555
566
 
556
567
  child.once('error', (error) => {
568
+ // 同同步抛出那条路径:宿主自身入口起不来 → 补标记
569
+ const marker = hostSpawnMarker(invocation, error)
557
570
  if (task && task.status !== 'cancelled') {
558
571
  task.status = 'failed'
559
572
  task.exitCode = null
560
573
  pushLine(task, `[error] ${error.message}`)
574
+ if (marker !== null) pushLine(task, marker)
561
575
  }
562
- settleOnce({ exitCode: null, timedOut: false, error: error.message, stdout, stderr })
576
+ settleOnce({ exitCode: null, timedOut: false, error: marker === null ? error.message : `${error.message}\n${marker}`, stdout, stderr })
563
577
  })
564
578
 
565
579
  child.once('close', (code) => {
@@ -712,10 +726,8 @@ function annotateNpmTooLow(result: InstallResult, options: {
712
726
  return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
713
727
  }
714
728
 
715
- /** 包在 registry 不存在(404)→ 用官方源复查一次,把「作者依赖缺失/包被删」与「本机镜像源滞后」
716
- * 分开(两者的提示与去向不同:前者引导投作者、后者只提示换源)。检测逻辑在独立的
717
- * registry-check.ts;这里之所以是异步,是因为要联网查官方 registry —— 只在安装失败且日志含 404
718
- * 时发生,代价可忽略。 */
729
+ /** 安装失败且日志含 404 时,用官方源复查一次并注入标记(逻辑见 registry-check.ts)。
730
+ * 异步是因为要联网;只在失败路径上发生。 */
719
731
  async function annotateRegistryMissing(result: InstallResult, options: {
720
732
  action: 'add' | 'remove' | 'update'
721
733
  profile: string
@@ -730,6 +742,17 @@ async function annotateRegistryMissing(result: InstallResult, options: {
730
742
  return { ...result, stderr: `${result.stderr}\n${line}`, error: result.error ? `${result.error}\n${line}` : line }
731
743
  }
732
744
 
745
+ /**
746
+ * 宿主自身 CLI 调不起来时的 ASCII 标记行:spawn 我们解析出的宿主入口报 ENOENT 时返回
747
+ * `[host-spawn]`,供前端单独归因、不引导提 Issue。PATH 上的 dsh/npm 各有自己的标记,返回 null。
748
+ * 导出供单测覆盖判据。
749
+ */
750
+ export function hostSpawnMarker(invocation: Invocation, error: unknown): string | null {
751
+ if ((error as NodeJS.ErrnoException | null)?.code !== 'ENOENT') return null
752
+ if (invocation.file === 'dsh' || invocation.file === 'npm') return null
753
+ return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`
754
+ }
755
+
733
756
  /**
734
757
  * Run a plugin mutation with one recovery path: when `add` fails because
735
758
  * pnpm's allowBuilds gate blocks a build script — either the git-hosted