dsh-plugin 2.3.6 → 2.3.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.
- package/client/client.js +309 -306
- package/client/client.js.map +1 -1
- package/lib/http/diagnostics.js +5 -0
- package/lib/services/install/operation-log.js +18 -1
- package/lib/services/install/task-queue.js +31 -9
- package/lib/types/services/install/install-types.d.ts +7 -0
- package/lib/types/services/install/operation-log.d.ts +7 -0
- package/lib/types/services/install/task-queue.d.ts +10 -0
- package/package.json +1 -1
- package/src/client/components/views/DiagnosticsView.tsx +1 -0
- package/src/client/logic/urls.ts +4 -0
- package/src/client/types.ts +2 -0
- package/src/server/http/diagnostics.ts +5 -0
- package/src/server/services/install/install-types.ts +2 -0
- package/src/server/services/install/operation-log.ts +20 -1
- package/src/server/services/install/task-queue.ts +35 -8
package/lib/http/diagnostics.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from 'node:path';
|
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
11
|
import { loadSettings } from '../services/settings.js';
|
|
12
12
|
import { hostDshVersion, toolVersion } from '../services/host-version.js';
|
|
13
|
+
import { normalizeRegistry } from '../services/install/registry-fallback.js';
|
|
13
14
|
import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.js';
|
|
14
15
|
import { appendLog } from '../services/log.js';
|
|
15
16
|
import { readJsonBody, requireTrustedPost, sendJson } from './shared.js';
|
|
@@ -39,12 +40,16 @@ function configuredNpmRegistry(profile) {
|
|
|
39
40
|
}
|
|
40
41
|
/** 宿主环境快照:提交 bug 时随 issue 附上,便于作者复现(前端 /env 端点返回)。 */
|
|
41
42
|
export function hostEnv(profile) {
|
|
43
|
+
const settings = loadSettings(profile);
|
|
42
44
|
return {
|
|
43
45
|
dshVersion: hostDshVersion(),
|
|
44
46
|
nodeVersion: process.version,
|
|
45
47
|
pnpmVersion: toolVersion('pnpm', ['--version']),
|
|
46
48
|
npmVersion: toolVersion('npm', ['--version']),
|
|
47
49
|
gitVersion: toolVersion('git', ['--version']),
|
|
50
|
+
// 安装实际会用的 npm 源(设置里的镜像 → 本机 npm 配置 → 官方):装不上时第一嫌疑就是它,
|
|
51
|
+
// 上报正文带上它,作者不必再猜「用户到底连的哪个源」(#6118 就卡在这里)
|
|
52
|
+
npmRegistry: normalizeRegistry(settings.npmRegistry) || configuredNpmRegistry(profile),
|
|
48
53
|
platform: process.platform,
|
|
49
54
|
arch: process.arch,
|
|
50
55
|
release: release(),
|
|
@@ -87,6 +87,11 @@ export function backfillOperationLog(output, profileDir) {
|
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
/** 本队列自己打的标记行(退出、渠道、超时、回捞…):不含任何可排查信息。 */
|
|
91
|
+
const SELF_MARKER_RE = /^\[(?:exit\b|timed out\]|install-channel\]|stale pipe\]|error\]|pnpm log\]|registry-fallback\]|allow build\])/;
|
|
92
|
+
/** 外层包装行:`Command failed with exit code 1: pnpm add x` 只说明「子进程非零退出」,不含 pnpm 原因。
|
|
93
|
+
* 宿主摘要(`dsh: plugin command failed; diagnostics: <path>`)另算:带路径的那行是回捞证据,不是噪音。 */
|
|
94
|
+
const WRAPPER_LINE_RE = /^Command failed with exit code \d+/i;
|
|
90
95
|
/**
|
|
91
96
|
* 输出里是否有 dsh 自己的内容 —— 只有 `[exit N]`、`[install-channel]`、`[timed out]`
|
|
92
97
|
* 这类本队列自己打的标记时返回 false。用于把「宿主什么都没回显」的失败显式留痕,
|
|
@@ -95,7 +100,19 @@ export function backfillOperationLog(output, profileDir) {
|
|
|
95
100
|
export function hasSubstantiveOutput(output) {
|
|
96
101
|
return output.split(/\r?\n/).some((raw) => {
|
|
97
102
|
const line = raw.trim();
|
|
98
|
-
return line !== '' &&
|
|
103
|
+
return line !== '' && !SELF_MARKER_RE.test(line);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 失败输出里除包装噪声外没有任何可排查内容:只剩本队列标记,和
|
|
108
|
+
* `Command failed with exit code N: pnpm add …` 这类外层包装(#6118 的形态 ——
|
|
109
|
+
* pnpm 以 1 退出、operation 日志里只有这一行)。此时上报正文只会写出「安装失败」,
|
|
110
|
+
* 作者与维护者都无从判断,调用方据此补一条带启动参数的诊断标记。
|
|
111
|
+
*/
|
|
112
|
+
export function lacksDiagnosticContent(output) {
|
|
113
|
+
return output.split(/\r?\n/).every((raw) => {
|
|
114
|
+
const line = raw.trim();
|
|
115
|
+
return line === '' || SELF_MARKER_RE.test(line) || WRAPPER_LINE_RE.test(line);
|
|
99
116
|
});
|
|
100
117
|
}
|
|
101
118
|
/**
|
|
@@ -14,7 +14,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
14
14
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.js';
|
|
15
15
|
import { npmTooLowMarker } from './npm-check.js';
|
|
16
16
|
import { registryMissingMarker } from './registry-check.js';
|
|
17
|
-
import { backfillOperationLog, createOperationLogTap,
|
|
17
|
+
import { backfillOperationLog, createOperationLogTap, lacksDiagnosticContent, operationLogPathOf } from './operation-log.js';
|
|
18
18
|
import { cleanLine, estimateProgress } from '../profile/progress.js';
|
|
19
19
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.js';
|
|
20
20
|
import { addPendingRestart, clearPendingRestart } from '../profile/pending-restart.js';
|
|
@@ -479,6 +479,7 @@ function spawnMutation(options) {
|
|
|
479
479
|
error: marker === null ? detail : `${detail}\n${marker}`,
|
|
480
480
|
stdout,
|
|
481
481
|
stderr,
|
|
482
|
+
spawn: { file: invocation.file, args: attemptArgs, cwd: invocation.cwd, shell: invocation.useShell },
|
|
482
483
|
});
|
|
483
484
|
return;
|
|
484
485
|
}
|
|
@@ -504,7 +505,8 @@ function spawnMutation(options) {
|
|
|
504
505
|
clearTimeout(forceSettle);
|
|
505
506
|
if (task)
|
|
506
507
|
runningChildren.delete(task.id);
|
|
507
|
-
|
|
508
|
+
// 附带本次实际启动参数:输出为空时,上报里至少能说明「跑了什么、在哪个目录」
|
|
509
|
+
resolveOnce({ ...result, spawn: { file: invocation.file, args: attemptArgs, cwd: invocation.cwd, shell: invocation.useShell } });
|
|
508
510
|
};
|
|
509
511
|
timer = setTimeout(() => {
|
|
510
512
|
timedOut = true;
|
|
@@ -850,6 +852,24 @@ export function hostSpawnMarker(invocation, error) {
|
|
|
850
852
|
return null;
|
|
851
853
|
return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`;
|
|
852
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* 失败输出只剩外层包装时的诊断标记(导出供单测覆盖字段)。内容先后即上报正文里的优先级:
|
|
857
|
+
* 退出方式 → 实际生效的 registry → 实际 spawn 的命令与目录 → profile 位置。
|
|
858
|
+
* registry 用主机名展示,避免把带凭据的私有源地址写进公开 Issue。
|
|
859
|
+
*/
|
|
860
|
+
export function sparseFailureMarker(result, options) {
|
|
861
|
+
const verb = options.action === 'remove' ? 'uninstall' : options.action === 'update' ? 'update' : 'install';
|
|
862
|
+
const outcome = result.timedOut ? 'timed out' : `exited with code ${result.exitCode ?? '?'}`;
|
|
863
|
+
const registry = result.registry ?? options.registries?.[0] ?? 'unknown';
|
|
864
|
+
const spawn = result.spawn;
|
|
865
|
+
return [
|
|
866
|
+
`[no-output] the ${verb} command ${outcome} and printed nothing but its wrapper line`,
|
|
867
|
+
`registry: ${registryHost(registry)}`,
|
|
868
|
+
spawn === undefined ? '' : `spawn: ${spawn.file} ${spawn.args.join(' ')} (cwd ${spawn.cwd}, shell ${spawn.shell})`,
|
|
869
|
+
`profile: ${profileDirectory(options.profile)}`,
|
|
870
|
+
'no pnpm log under .plugin-manager/logs — attach hub.log when reporting',
|
|
871
|
+
].filter((part) => part !== '').join(' | ');
|
|
872
|
+
}
|
|
853
873
|
/**
|
|
854
874
|
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 限制拦下构建脚本而失败 ——
|
|
855
875
|
* 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
|
|
@@ -941,15 +961,17 @@ export async function runPluginMutation(options) {
|
|
|
941
961
|
// 诊断路径在、日志却取不到:留一行证据,否则下一次同类上报又只剩宿主那句摘要、看不出根因
|
|
942
962
|
pushLine(options.task, '[pnpm log] the host-reported diagnostics log could not be read from disk (missing, empty, or unreadable)');
|
|
943
963
|
}
|
|
944
|
-
else if (!hasSubstantiveOutput(output)) {
|
|
945
|
-
// 宿主连 diagnostics 路径都没回显:显式留痕,避免上报正文只剩一个退出码
|
|
946
|
-
const what = result.timedOut
|
|
947
|
-
? 'printed nothing before the timeout'
|
|
948
|
-
: `exited with code ${result.exitCode ?? '?'} without printing anything`;
|
|
949
|
-
pushLine(options.task, `[no-output] the dsh CLI ${what}, and no pnpm log was written under .plugin-manager/logs — attach ~/.dsh/profiles/${options.profile}/hub.log when reporting this`);
|
|
950
|
-
}
|
|
951
964
|
}
|
|
952
965
|
}
|
|
966
|
+
// 失败但输出里只剩外层包装(`Command failed with exit code 1: pnpm add …`)时,上报正文会退化成
|
|
967
|
+
// 一句「安装失败」:既看不出跑了什么,也看不出连的是哪个源(#6118 的形态)。补一条诊断标记,
|
|
968
|
+
// 把实际启动参数、profile 目录与生效的 registry 带进日志和上报正文。
|
|
969
|
+
if ((result.exitCode !== 0 || result.timedOut) && lacksDiagnosticContent(`${result.stderr}\n${result.stdout}`)) {
|
|
970
|
+
const line = sparseFailureMarker(result, options);
|
|
971
|
+
if (options.task && options.task.status !== 'cancelled')
|
|
972
|
+
pushLine(options.task, line);
|
|
973
|
+
result = { ...result, stderr: `${result.stderr}\n${line}` };
|
|
974
|
+
}
|
|
953
975
|
// profile 里的 YAML 配置(pnpm-workspace.yaml / pnpm-lock.yaml)解析失败 → 追加 [profile-yaml]
|
|
954
976
|
// 标记,前端给「修本机文件」的步骤,不引导向插件仓库提 Issue
|
|
955
977
|
result = annotateProfileYaml(result, options);
|
|
@@ -14,6 +14,13 @@ export interface InstallResult {
|
|
|
14
14
|
stderr: string;
|
|
15
15
|
/** 实际执行安装用的 npm 源(换源重试后是最终那一个):后续读元数据的注解要跟它同源 */
|
|
16
16
|
registry?: string;
|
|
17
|
+
/** 最后一次 spawn 的实际启动参数:失败且无任何输出时,上报里要靠它说明「到底跑了什么、在哪个目录」 */
|
|
18
|
+
spawn?: {
|
|
19
|
+
file: string;
|
|
20
|
+
args: string[];
|
|
21
|
+
cwd: string;
|
|
22
|
+
shell: boolean;
|
|
23
|
+
};
|
|
17
24
|
}
|
|
18
25
|
export interface InstallTask {
|
|
19
26
|
id: number;
|
|
@@ -15,6 +15,13 @@ export declare function backfillOperationLog(output: string, profileDir?: string
|
|
|
15
15
|
* 否则上报正文只剩退出码,无从排查。
|
|
16
16
|
*/
|
|
17
17
|
export declare function hasSubstantiveOutput(output: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* 失败输出里除包装噪声外没有任何可排查内容:只剩本队列标记,和
|
|
20
|
+
* `Command failed with exit code N: pnpm add …` 这类外层包装(#6118 的形态 ——
|
|
21
|
+
* pnpm 以 1 退出、operation 日志里只有这一行)。此时上报正文只会写出「安装失败」,
|
|
22
|
+
* 作者与维护者都无从判断,调用方据此补一条带启动参数的诊断标记。
|
|
23
|
+
*/
|
|
24
|
+
export declare function lacksDiagnosticContent(output: string): boolean;
|
|
18
25
|
/**
|
|
19
26
|
* 运行期抓取:宿主的 operation 日志只在命令运行期间存在,结束后可能被清理,
|
|
20
27
|
* 退出后再按路径读往往已经拿不到。这里在安装过程中轮询 profile 下最新的
|
|
@@ -104,6 +104,16 @@ export declare function verifyInstalledEntry(profile: string, target: string): {
|
|
|
104
104
|
* 导出供单测覆盖判据。
|
|
105
105
|
*/
|
|
106
106
|
export declare function hostSpawnMarker(invocation: Invocation, error: unknown): string | null;
|
|
107
|
+
/**
|
|
108
|
+
* 失败输出只剩外层包装时的诊断标记(导出供单测覆盖字段)。内容先后即上报正文里的优先级:
|
|
109
|
+
* 退出方式 → 实际生效的 registry → 实际 spawn 的命令与目录 → profile 位置。
|
|
110
|
+
* registry 用主机名展示,避免把带凭据的私有源地址写进公开 Issue。
|
|
111
|
+
*/
|
|
112
|
+
export declare function sparseFailureMarker(result: InstallResult, options: {
|
|
113
|
+
action: 'add' | 'remove' | 'update';
|
|
114
|
+
profile: string;
|
|
115
|
+
registries?: string[];
|
|
116
|
+
}): string;
|
|
107
117
|
/**
|
|
108
118
|
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 限制拦下构建脚本而失败 ——
|
|
109
119
|
* 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.7",
|
|
4
4
|
"description": "A community plugin marketplace for DeepSeek Harness, built to the official plugin spec — browse, search and install 20000+ human-curated community plugins without leaving the app. · DeepSeek Harness 社区插件市场(遵循官方开发规范):20000+ 人工精选社区插件,每日更新。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -21,6 +21,7 @@ function formatEnv(env: EnvInfo): string {
|
|
|
21
21
|
`DSH ${env.dshVersion ?? 'unknown'}`,
|
|
22
22
|
`Node ${env.nodeVersion}`,
|
|
23
23
|
`${env.platform} ${env.arch} (${env.release})`,
|
|
24
|
+
env.npmRegistry ? `npm registry ${env.npmRegistry}` : '',
|
|
24
25
|
`Profile ${env.profile}`,
|
|
25
26
|
env.dshHome ? `Home ${env.dshHome}` : '',
|
|
26
27
|
].filter(Boolean).join('\n')
|
package/src/client/logic/urls.ts
CHANGED
|
@@ -188,6 +188,8 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
|
|
|
188
188
|
// 这类环境问题,作者一眼就能对号入座;探测不到如实写 unknown
|
|
189
189
|
`- pnpm: ${env.pnpmVersion ? `v${env.pnpmVersion}` : 'unknown'}`,
|
|
190
190
|
`- npm: ${env.npmVersion ? `v${env.npmVersion}` : 'unknown'}`,
|
|
191
|
+
// 安装实际使用的源:网络/私有镜像类失败的第一嫌疑,作者据此判断是否「源不对/源不通」
|
|
192
|
+
`- npm registry: ${env.npmRegistry ? `\`${env.npmRegistry}\`` : 'unknown'}`,
|
|
191
193
|
`- git: ${env.gitVersion ? `v${env.gitVersion}` : 'unknown'}`,
|
|
192
194
|
`- OS: ${env.platform} ${env.arch} (${env.release})`,
|
|
193
195
|
`- Profile: \`${env.profile}\``,
|
|
@@ -290,6 +292,8 @@ export function hubIssueUrl(
|
|
|
290
292
|
`- Node: ${env.nodeVersion}`,
|
|
291
293
|
`- pnpm: ${env.pnpmVersion ? `v${env.pnpmVersion}` : 'unknown'}`,
|
|
292
294
|
`- npm: ${env.npmVersion ? `v${env.npmVersion}` : 'unknown'}`,
|
|
295
|
+
// 安装实际使用的源:网络/私有镜像类失败的第一嫌疑,作者据此判断是否「源不对/源不通」
|
|
296
|
+
`- npm registry: ${env.npmRegistry ? `\`${env.npmRegistry}\`` : 'unknown'}`,
|
|
293
297
|
`- git: ${env.gitVersion ? `v${env.gitVersion}` : 'unknown'}`,
|
|
294
298
|
`- OS: ${env.platform} ${env.arch} (${env.release})`,
|
|
295
299
|
`- Profile: \`${env.profile}\``,
|
package/src/client/types.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface EnvInfo {
|
|
|
95
95
|
npmVersion: string | null
|
|
96
96
|
/** 本机 git 版本(git --version 首行;缺失返回 null) */
|
|
97
97
|
gitVersion: string | null
|
|
98
|
+
/** 安装实际会用的 npm 源(设置镜像 → 本机 npm 配置 → 官方):失败上报要带上它 */
|
|
99
|
+
npmRegistry?: string | null
|
|
98
100
|
platform: string
|
|
99
101
|
arch: string
|
|
100
102
|
release: string
|
|
@@ -10,6 +10,7 @@ import { join } from 'node:path'
|
|
|
10
10
|
import { spawnSync } from 'node:child_process'
|
|
11
11
|
import { loadSettings } from '../services/settings.ts'
|
|
12
12
|
import { hostDshVersion, toolVersion } from '../services/host-version.ts'
|
|
13
|
+
import { normalizeRegistry } from '../services/install/registry-fallback.ts'
|
|
13
14
|
import { gitLsRemote, probeUrl, systemProxy } from '../services/probe.ts'
|
|
14
15
|
import { appendLog } from '../services/log.ts'
|
|
15
16
|
import { readJsonBody, requireTrustedPost, sendJson, type WebRoute, type WebServerService } from './shared.ts'
|
|
@@ -39,12 +40,16 @@ function configuredNpmRegistry(profile: string): string {
|
|
|
39
40
|
|
|
40
41
|
/** 宿主环境快照:提交 bug 时随 issue 附上,便于作者复现(前端 /env 端点返回)。 */
|
|
41
42
|
export function hostEnv(profile: string): Record<string, string | null> {
|
|
43
|
+
const settings = loadSettings(profile)
|
|
42
44
|
return {
|
|
43
45
|
dshVersion: hostDshVersion(),
|
|
44
46
|
nodeVersion: process.version,
|
|
45
47
|
pnpmVersion: toolVersion('pnpm', ['--version']),
|
|
46
48
|
npmVersion: toolVersion('npm', ['--version']),
|
|
47
49
|
gitVersion: toolVersion('git', ['--version']),
|
|
50
|
+
// 安装实际会用的 npm 源(设置里的镜像 → 本机 npm 配置 → 官方):装不上时第一嫌疑就是它,
|
|
51
|
+
// 上报正文带上它,作者不必再猜「用户到底连的哪个源」(#6118 就卡在这里)
|
|
52
|
+
npmRegistry: normalizeRegistry(settings.npmRegistry) || configuredNpmRegistry(profile),
|
|
48
53
|
platform: process.platform,
|
|
49
54
|
arch: process.arch,
|
|
50
55
|
release: release(),
|
|
@@ -15,6 +15,8 @@ export interface InstallResult {
|
|
|
15
15
|
stderr: string
|
|
16
16
|
/** 实际执行安装用的 npm 源(换源重试后是最终那一个):后续读元数据的注解要跟它同源 */
|
|
17
17
|
registry?: string
|
|
18
|
+
/** 最后一次 spawn 的实际启动参数:失败且无任何输出时,上报里要靠它说明「到底跑了什么、在哪个目录」 */
|
|
19
|
+
spawn?: { file: string; args: string[]; cwd: string; shell: boolean }
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export interface InstallTask {
|
|
@@ -81,6 +81,12 @@ export function backfillOperationLog(output: string, profileDir?: string): strin
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** 本队列自己打的标记行(退出、渠道、超时、回捞…):不含任何可排查信息。 */
|
|
85
|
+
const SELF_MARKER_RE = /^\[(?:exit\b|timed out\]|install-channel\]|stale pipe\]|error\]|pnpm log\]|registry-fallback\]|allow build\])/
|
|
86
|
+
/** 外层包装行:`Command failed with exit code 1: pnpm add x` 只说明「子进程非零退出」,不含 pnpm 原因。
|
|
87
|
+
* 宿主摘要(`dsh: plugin command failed; diagnostics: <path>`)另算:带路径的那行是回捞证据,不是噪音。 */
|
|
88
|
+
const WRAPPER_LINE_RE = /^Command failed with exit code \d+/i
|
|
89
|
+
|
|
84
90
|
/**
|
|
85
91
|
* 输出里是否有 dsh 自己的内容 —— 只有 `[exit N]`、`[install-channel]`、`[timed out]`
|
|
86
92
|
* 这类本队列自己打的标记时返回 false。用于把「宿主什么都没回显」的失败显式留痕,
|
|
@@ -89,7 +95,20 @@ export function backfillOperationLog(output: string, profileDir?: string): strin
|
|
|
89
95
|
export function hasSubstantiveOutput(output: string): boolean {
|
|
90
96
|
return output.split(/\r?\n/).some((raw) => {
|
|
91
97
|
const line = raw.trim()
|
|
92
|
-
return line !== '' &&
|
|
98
|
+
return line !== '' && !SELF_MARKER_RE.test(line)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 失败输出里除包装噪声外没有任何可排查内容:只剩本队列标记,和
|
|
104
|
+
* `Command failed with exit code N: pnpm add …` 这类外层包装(#6118 的形态 ——
|
|
105
|
+
* pnpm 以 1 退出、operation 日志里只有这一行)。此时上报正文只会写出「安装失败」,
|
|
106
|
+
* 作者与维护者都无从判断,调用方据此补一条带启动参数的诊断标记。
|
|
107
|
+
*/
|
|
108
|
+
export function lacksDiagnosticContent(output: string): boolean {
|
|
109
|
+
return output.split(/\r?\n/).every((raw) => {
|
|
110
|
+
const line = raw.trim()
|
|
111
|
+
return line === '' || SELF_MARKER_RE.test(line) || WRAPPER_LINE_RE.test(line)
|
|
93
112
|
})
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -15,7 +15,7 @@ import type { InstallResult, InstallTask, Invocation, QueueItem } from './instal
|
|
|
15
15
|
import { CAPTURE_LIMIT_BYTES, MAX_TASKS, MAX_TASK_LINES } from './install-types.ts'
|
|
16
16
|
import { npmTooLowMarker } from './npm-check.ts'
|
|
17
17
|
import { registryMissingMarker } from './registry-check.ts'
|
|
18
|
-
import { backfillOperationLog, createOperationLogTap,
|
|
18
|
+
import { backfillOperationLog, createOperationLogTap, lacksDiagnosticContent, operationLogPathOf } from './operation-log.ts'
|
|
19
19
|
import { cleanLine, estimateProgress } from '../profile/progress.ts'
|
|
20
20
|
import type { LoaderHandle } from '../loader.ts'
|
|
21
21
|
import { hasClientUi, isDshPlugin, mountLoadedEntry, removeLoadedEntry } from '../loader.ts'
|
|
@@ -536,6 +536,7 @@ function spawnMutation(options: {
|
|
|
536
536
|
error: marker === null ? detail : `${detail}\n${marker}`,
|
|
537
537
|
stdout,
|
|
538
538
|
stderr,
|
|
539
|
+
spawn: { file: invocation.file, args: attemptArgs, cwd: invocation.cwd, shell: invocation.useShell },
|
|
539
540
|
})
|
|
540
541
|
return
|
|
541
542
|
}
|
|
@@ -556,7 +557,8 @@ function spawnMutation(options: {
|
|
|
556
557
|
if (exitWatch !== undefined) clearTimeout(exitWatch)
|
|
557
558
|
if (forceSettle !== undefined) clearTimeout(forceSettle)
|
|
558
559
|
if (task) runningChildren.delete(task.id)
|
|
559
|
-
|
|
560
|
+
// 附带本次实际启动参数:输出为空时,上报里至少能说明「跑了什么、在哪个目录」
|
|
561
|
+
resolveOnce({ ...result, spawn: { file: invocation.file, args: attemptArgs, cwd: invocation.cwd, shell: invocation.useShell } })
|
|
560
562
|
}
|
|
561
563
|
|
|
562
564
|
timer = setTimeout(() => {
|
|
@@ -886,6 +888,29 @@ export function hostSpawnMarker(invocation: Invocation, error: unknown): string
|
|
|
886
888
|
return `[host-spawn] the DSH host could not be launched (${invocation.file}: ENOENT)`
|
|
887
889
|
}
|
|
888
890
|
|
|
891
|
+
/**
|
|
892
|
+
* 失败输出只剩外层包装时的诊断标记(导出供单测覆盖字段)。内容先后即上报正文里的优先级:
|
|
893
|
+
* 退出方式 → 实际生效的 registry → 实际 spawn 的命令与目录 → profile 位置。
|
|
894
|
+
* registry 用主机名展示,避免把带凭据的私有源地址写进公开 Issue。
|
|
895
|
+
*/
|
|
896
|
+
export function sparseFailureMarker(result: InstallResult, options: {
|
|
897
|
+
action: 'add' | 'remove' | 'update'
|
|
898
|
+
profile: string
|
|
899
|
+
registries?: string[]
|
|
900
|
+
}): string {
|
|
901
|
+
const verb = options.action === 'remove' ? 'uninstall' : options.action === 'update' ? 'update' : 'install'
|
|
902
|
+
const outcome = result.timedOut ? 'timed out' : `exited with code ${result.exitCode ?? '?'}`
|
|
903
|
+
const registry = result.registry ?? options.registries?.[0] ?? 'unknown'
|
|
904
|
+
const spawn = result.spawn
|
|
905
|
+
return [
|
|
906
|
+
`[no-output] the ${verb} command ${outcome} and printed nothing but its wrapper line`,
|
|
907
|
+
`registry: ${registryHost(registry)}`,
|
|
908
|
+
spawn === undefined ? '' : `spawn: ${spawn.file} ${spawn.args.join(' ')} (cwd ${spawn.cwd}, shell ${spawn.shell})`,
|
|
909
|
+
`profile: ${profileDirectory(options.profile)}`,
|
|
910
|
+
'no pnpm log under .plugin-manager/logs — attach hub.log when reporting',
|
|
911
|
+
].filter((part) => part !== '').join(' | ')
|
|
912
|
+
}
|
|
913
|
+
|
|
889
914
|
/**
|
|
890
915
|
* 执行插件变更,带一条恢复路径:当 `add` 因 pnpm 的 allowBuilds 限制拦下构建脚本而失败 ——
|
|
891
916
|
* 或是 git 源包自己的 `prepare`(`ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,key 取自打印的
|
|
@@ -988,15 +1013,17 @@ export async function runPluginMutation(options: {
|
|
|
988
1013
|
if (operationLogPathOf(output) !== null) {
|
|
989
1014
|
// 诊断路径在、日志却取不到:留一行证据,否则下一次同类上报又只剩宿主那句摘要、看不出根因
|
|
990
1015
|
pushLine(options.task, '[pnpm log] the host-reported diagnostics log could not be read from disk (missing, empty, or unreadable)')
|
|
991
|
-
} else if (!hasSubstantiveOutput(output)) {
|
|
992
|
-
// 宿主连 diagnostics 路径都没回显:显式留痕,避免上报正文只剩一个退出码
|
|
993
|
-
const what = result.timedOut
|
|
994
|
-
? 'printed nothing before the timeout'
|
|
995
|
-
: `exited with code ${result.exitCode ?? '?'} without printing anything`
|
|
996
|
-
pushLine(options.task, `[no-output] the dsh CLI ${what}, and no pnpm log was written under .plugin-manager/logs — attach ~/.dsh/profiles/${options.profile}/hub.log when reporting this`)
|
|
997
1016
|
}
|
|
998
1017
|
}
|
|
999
1018
|
}
|
|
1019
|
+
// 失败但输出里只剩外层包装(`Command failed with exit code 1: pnpm add …`)时,上报正文会退化成
|
|
1020
|
+
// 一句「安装失败」:既看不出跑了什么,也看不出连的是哪个源(#6118 的形态)。补一条诊断标记,
|
|
1021
|
+
// 把实际启动参数、profile 目录与生效的 registry 带进日志和上报正文。
|
|
1022
|
+
if ((result.exitCode !== 0 || result.timedOut) && lacksDiagnosticContent(`${result.stderr}\n${result.stdout}`)) {
|
|
1023
|
+
const line = sparseFailureMarker(result, options)
|
|
1024
|
+
if (options.task && options.task.status !== 'cancelled') pushLine(options.task, line)
|
|
1025
|
+
result = { ...result, stderr: `${result.stderr}\n${line}` }
|
|
1026
|
+
}
|
|
1000
1027
|
// profile 里的 YAML 配置(pnpm-workspace.yaml / pnpm-lock.yaml)解析失败 → 追加 [profile-yaml]
|
|
1001
1028
|
// 标记,前端给「修本机文件」的步骤,不引导向插件仓库提 Issue
|
|
1002
1029
|
result = annotateProfileYaml(result, options)
|