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.
- package/client/client.js +386 -358
- package/client/client.js.map +1 -1
- package/lib/services/install/registry-check.js +3 -14
- package/lib/services/install/task-queue.js +34 -9
- package/lib/types/services/install/task-queue.d.ts +7 -1
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +6 -5
- package/src/client/components/modals/NotificationsModal.tsx +18 -9
- package/src/client/components/modals/modals.tsx +24 -16
- package/src/client/locales.ts +26 -14
- package/src/client/logic/failures.ts +25 -10
- package/src/client/logic/urls.ts +36 -31
- package/src/server/services/install/registry-check.ts +3 -14
- package/src/server/services/install/task-queue.ts +32 -9
package/src/client/logic/urls.ts
CHANGED
|
@@ -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
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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
|
|
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
|
|
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 '
|
|
67
|
-
case '
|
|
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
|
|
71
|
-
default: return
|
|
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
|
-
// 来源说明:标题(链官网)+ 一句来源(链仓库)+
|
|
127
|
-
`## [DSH Plugin 插件市场](${SITE_URL}) ·
|
|
128
|
-
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL})
|
|
129
|
-
`-
|
|
130
|
-
`-
|
|
131
|
-
//
|
|
132
|
-
//
|
|
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
|
|
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}) ·
|
|
240
|
-
`本错误信息由 [dsh-plugin-hub](${GITHUB_URL})
|
|
241
|
-
`-
|
|
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
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
//
|
|
166
|
-
cwd:
|
|
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] ${
|
|
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:
|
|
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
|
|
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
|
-
/**
|
|
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
|