@bruc3van/dsh-doctor 0.1.2 → 0.1.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.
- package/README.en.md +142 -0
- package/README.md +36 -4
- package/package.json +3 -2
- package/src/cli.mjs +1 -1
- package/src/doctor.mjs +591 -54
- package/src/i18n.mjs +66 -10
- package/src/repair.mjs +23 -7
package/README.en.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# DSH Doctor
|
|
2
|
+
|
|
3
|
+
[中文](README.md) | English
|
|
4
|
+
|
|
5
|
+
DSH Doctor helps DSH and plugin users quickly identify plugins that break startup or stop working after a DSH upgrade. It groups each plugin's problems, impact, and recommended actions, while also checking common profile configuration and version-drift issues. Diagnosis is read-only by default; repairs run only after you explicitly use `--fix`, review the exact plan, and confirm it. File edits are backed up first.
|
|
6
|
+
|
|
7
|
+
This is a community-maintained third-party tool and is not an official DeepSeek project. It does not load or execute code from the plugins it inspects.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Node.js `22.19+` or `24+` is required:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install --global @bruc3van/dsh-doctor
|
|
15
|
+
dsh-doctor
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
You can also run it without a global installation:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx @bruc3van/dsh-doctor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
By default, Doctor checks `$DSH_HOME/profiles/web`. If `DSH_HOME` is unset, it uses `~/.dsh`.
|
|
25
|
+
|
|
26
|
+
Doctor does not require `dsh` to be installed as a global command. It searches PATH, the current project, shared profile installations, links left by the npx cache, and built Harness source checkouts, in that order. For a bundled DSH Desktop runtime or another custom installation, pass `--dsh-command /path/to/dsh`; the official package's `lib/bin.js` is also accepted. If no CLI can be found, Doctor still completes its read-only checks but does not offer or run command-based repairs that it cannot verify.
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
A complete diagnosis and repair flow has four steps:
|
|
31
|
+
|
|
32
|
+
1. `dsh-doctor` inspects the active DSH Home, profile, plugins, and Harness versions without making changes.
|
|
33
|
+
2. Doctor reports evidence and recommendations by severity and plugin compatibility state.
|
|
34
|
+
3. `dsh-doctor --fix` shows the exact file edits or DSH command plan and waits for confirmation.
|
|
35
|
+
4. After applying confirmed repairs, Doctor runs the full diagnosis again and determines the exit code from the final state.
|
|
36
|
+
|
|
37
|
+
Doctor never loads inspected plugins and does not modify configuration during a normal diagnosis. Operations without one deterministic answer—such as guessing credentials, rewriting damaged YAML, or removing a plugin—remain recommendations only.
|
|
38
|
+
|
|
39
|
+
## Output language
|
|
40
|
+
|
|
41
|
+
Text output supports English and Chinese. Doctor resolves the language in this order:
|
|
42
|
+
|
|
43
|
+
1. `--lang zh|en`
|
|
44
|
+
2. `DSH_DOCTOR_LANG`
|
|
45
|
+
3. `locale.preference` in the active DSH Home's `settings.yaml`
|
|
46
|
+
4. Terminal or system locale
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
dsh-doctor --lang zh
|
|
50
|
+
dsh-doctor --lang en
|
|
51
|
+
DSH_DOCTOR_LANG=zh dsh-doctor
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`--json` always keeps stable English messages and diagnostic codes so language changes do not break automation.
|
|
55
|
+
|
|
56
|
+
## Common commands
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
# Read-only diagnosis
|
|
60
|
+
dsh-doctor
|
|
61
|
+
dsh-doctor --profile web
|
|
62
|
+
dsh-doctor --home /path/to/.dsh
|
|
63
|
+
dsh-doctor --dsh-command /path/to/@deepseek-ai/dsh/lib/bin.js
|
|
64
|
+
|
|
65
|
+
# Machine-readable read-only report with no prompts
|
|
66
|
+
dsh-doctor --json
|
|
67
|
+
|
|
68
|
+
# Show a repair plan, apply it after confirmation, and diagnose again
|
|
69
|
+
dsh-doctor --fix
|
|
70
|
+
|
|
71
|
+
# Explicitly confirm the current plan in automation
|
|
72
|
+
dsh-doctor --fix --yes --json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`--repair` is an alias for `--fix`. `--yes` is valid only together with `--fix`.
|
|
76
|
+
|
|
77
|
+
## Plugin compatibility after a DSH upgrade
|
|
78
|
+
|
|
79
|
+
After DSH is updated, Doctor assigns every direct profile plugin one explicit state and summarizes the result in both text and JSON reports:
|
|
80
|
+
|
|
81
|
+
- `incompatible`: Doctor found an error that can prevent the plugin or Harness from loading, such as a missing plugin or an injection targeting a removed client runtime.
|
|
82
|
+
- `risk`: Doctor found a current-version risk, such as a Harness peer range that rejects the new version, a dependency on a removed DSH package, an unsupported Node.js version, or installation drift.
|
|
83
|
+
- `unknown`: The plugin does not declare a Harness compatibility range through `peerDependencies`. Doctor cannot prove it supports the upgraded DSH, but does not report uncertainty as a failure.
|
|
84
|
+
- `compatible`: The declared compatibility ranges accept the active Harness and no plugin-related errors or warnings were found.
|
|
85
|
+
|
|
86
|
+
Compatibility checks cover every direct profile plugin, not only frontend plugins with `dsh.client`. References to removed Harness APIs in bundle-only or server-side plugins are reported as well. After upgrading DSH, run `dsh-doctor` first, review the exact update recommendations, and then decide whether to continue with `dsh-doctor --fix`.
|
|
87
|
+
|
|
88
|
+
## Current checks
|
|
89
|
+
|
|
90
|
+
- JSON root structure, dependency maps, bundle lists, and reload lifecycle in the profile `package.json`
|
|
91
|
+
- Syntax and top-level structure of profile, home, and bundle `cordis.patch.yml` files, including `!!js` expressions
|
|
92
|
+
- Safe structural checks for `settings.yaml` and `.credentials.yaml`; credential diagnostics never expose secret values
|
|
93
|
+
- Presence of profile dependencies, bundle declarations, patch files, and client bundles
|
|
94
|
+
- Consistency among the profile `package.json`, the `pnpm-lock.yaml` importer, and installed versions
|
|
95
|
+
- Node.js `engines`, Harness peer ranges, and obsolete DSH dependencies for all direct plugins, including bundle-only and server-side plugins
|
|
96
|
+
- Version drift and stale top-level `@deepseek-ai/dsh-*` packages across the active DSH CLI, Harness workspace, and profile
|
|
97
|
+
- The `platform`, `immediately`, `inject`, `external`, and `./client` export contract for `dsh.client`
|
|
98
|
+
- Consistency between literal `require()` calls in client bundles and external or module suppliers
|
|
99
|
+
- References to removed Harness client packages
|
|
100
|
+
- Third-party plugin peer ranges against actual active Harness versions
|
|
101
|
+
- Real resolution precedence where the Harness installation wins over a profile-local bundle with the same name
|
|
102
|
+
- Static composition of bundle, profile, and home patches in official Harness order, including missing targets, invalid group inserts, and name assertions, without loading plugins
|
|
103
|
+
|
|
104
|
+
## Repair safety
|
|
105
|
+
|
|
106
|
+
Every executable repair has a stable ID, risk level, description, and exact target:
|
|
107
|
+
|
|
108
|
+
- File repairs show their paths before confirmation and verify the SHA-256 fingerprint again before writing.
|
|
109
|
+
- Doctor creates a `.dsh-doctor-<timestamp>.bak` backup before replacing a file atomically through a temporary file in the same directory.
|
|
110
|
+
- External commands use fixed argument arrays and never construct shell commands.
|
|
111
|
+
- `--json --fix --yes` captures subprocess output in the repair result so stdout remains exactly one valid JSON document.
|
|
112
|
+
- Command repairs bind the diagnosed `DSH_HOME` and show the resolved CLI path instead of assuming `dsh` exists on PATH.
|
|
113
|
+
- A failed repair stops later actions and preserves backups already created.
|
|
114
|
+
- Doctor runs every diagnostic again after repairs and uses the final state for its exit code.
|
|
115
|
+
|
|
116
|
+
The initial release automatically performs only deterministic operations, such as restoring an installed bundle to the manifest list or running an exact profile install or update command. Damaged JSON or YAML, credential values, and plugin removal remain recommendations because Doctor cannot safely guess the intended result.
|
|
117
|
+
|
|
118
|
+
## Exit codes
|
|
119
|
+
|
|
120
|
+
- `0`: No blocking errors were found; warnings may still be present
|
|
121
|
+
- `1`: Doctor found a problem that may prevent Harness from starting
|
|
122
|
+
- `2`: Invalid arguments, an operational failure, or a failed repair
|
|
123
|
+
|
|
124
|
+
## Current limitations
|
|
125
|
+
|
|
126
|
+
- Static scanning recognizes only literal `require("package")` calls. Dynamic dependencies require a future bundle metadata contract.
|
|
127
|
+
- Configuration checks cover syntax and structures that Doctor can align deterministically. Patch composition follows the current Harness algorithm, but Doctor does not evaluate `!!js` or load third-party plugins.
|
|
128
|
+
- Version compatibility is based on plugin `peerDependencies` and resolvable active Harness package versions. A plugin without a declared range can receive only structural checks and an `unknown` compatibility state.
|
|
129
|
+
- Lockfile checks deterministically cross-check the direct profile importer only; they do not recursively scan the complete npm dependency graph.
|
|
130
|
+
- A runtime startup probe is not enabled. Even a copied `DSH_HOME` would not make arbitrary third-party plugin code side-effect-free because it could access the network, absolute paths, or external processes.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
npm install
|
|
136
|
+
npm run check
|
|
137
|
+
node src/cli.mjs --help
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The first publication of a new package must be performed by the npm account that owns the `@bruc3van` scope with `npm publish --access public`. Then configure a GitHub Actions Trusted Publisher in the npm package settings with Organization or user `bruc3van`, Repository `dsh-doctor`, Workflow filename `release.yml`, no Environment, and only the `npm publish` allowed action.
|
|
141
|
+
|
|
142
|
+
Before each later release, add a Chinese `## vX.Y.Z` entry matching the version tag to `CHANGELOG.md`. Pushing a tag that matches `package.json` makes the workflow publish through OIDC with npm provenance and automatically create or update the GitHub Release from that Chinese entry. The release fails if the entry is missing or contains no Chinese text. No long-lived npm token is required.
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# DSH Doctor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
中文 | [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
DSH Doctor 面向 DSH 与插件使用者,帮助快速找出导致 DSH 启动异常或升级后不可用的插件,集中说明每个插件的问题、影响与处理方式,并检查常见的 profile 配置和版本漂移。诊断默认完全只读;只有显式使用 `--fix`、核对并确认精确的修复计划后才会执行,文件修改会先创建备份。
|
|
4
6
|
|
|
5
7
|
这是社区维护的第三方工具,不属于 DeepSeek 官方项目。它不会加载或执行待检查插件的代码。
|
|
6
8
|
|
|
@@ -23,6 +25,17 @@ npx @bruc3van/dsh-doctor
|
|
|
23
25
|
|
|
24
26
|
Doctor 不要求 `dsh` 必须是全局命令。它会按顺序查找 PATH、当前项目安装、profile 共享安装或 npx 缓存留下的链接、已构建的 Harness 源码工作区。DSH Desktop 内置运行时或其他特殊安装可以通过 `--dsh-command /path/to/dsh`(也接受官方包的 `lib/bin.js`)明确指定。找不到 CLI 时仍会完成只读诊断,但不会提供或执行无法验证的命令型修复。
|
|
25
27
|
|
|
28
|
+
## 工作方式
|
|
29
|
+
|
|
30
|
+
一次完整流程分为四步:
|
|
31
|
+
|
|
32
|
+
1. `dsh-doctor` 只读检查当前 DSH Home、profile、插件和 Harness 版本。
|
|
33
|
+
2. Doctor 按错误、警告和插件兼容状态展示证据与建议。
|
|
34
|
+
3. `dsh-doctor --fix` 先展示精确的文件修改或 DSH 命令计划,并等待用户确认。
|
|
35
|
+
4. 修复完成后自动重新诊断,以最终状态决定退出码。
|
|
36
|
+
|
|
37
|
+
Doctor 不会加载待检查插件,也不会在普通诊断时修改配置。无法确定正确结果的操作,例如猜测凭据、重写损坏 YAML 或直接移除插件,只会给出建议。
|
|
38
|
+
|
|
26
39
|
## 输出语言
|
|
27
40
|
|
|
28
41
|
文本输出支持中文和英文。默认依次读取:
|
|
@@ -40,6 +53,17 @@ DSH_DOCTOR_LANG=zh dsh-doctor
|
|
|
40
53
|
|
|
41
54
|
`--json` 始终保留稳定的英文消息与诊断 code,避免语言变化破坏脚本。
|
|
42
55
|
|
|
56
|
+
## DSH 升级后的插件兼容性
|
|
57
|
+
|
|
58
|
+
DSH 更新后,Doctor 会把每个 profile 插件归入一个明确状态,并在文本与 JSON 报告中汇总:
|
|
59
|
+
|
|
60
|
+
- `incompatible`:已经发现会阻断插件加载或 Harness 启动的错误,例如插件未安装,或注入了已删除的 client runtime。
|
|
61
|
+
- `risk`:发现当前版本风险,例如 Harness peer range 不接受新版本、仍依赖已删除的 DSH 包、Node.js 不兼容,或安装版本发生漂移。
|
|
62
|
+
- `unknown`:插件没有通过 `peerDependencies` 声明 Harness 兼容范围;Doctor 无法证明它支持升级后的 DSH,但不会把未知误报成故障。
|
|
63
|
+
- `compatible`:插件声明的兼容范围接受当前 Harness,且没有发现插件相关错误或警告。
|
|
64
|
+
|
|
65
|
+
兼容性检查覆盖所有 profile 直接插件,不再只检查带 `dsh.client` 的前端插件;纯 bundle 或服务端插件引用旧 Harness API 也会被报告。建议 DSH 升级后先运行一次 `dsh-doctor`,再根据精确的 update 建议决定是否执行 `dsh-doctor --fix`。
|
|
66
|
+
|
|
43
67
|
## 常用命令
|
|
44
68
|
|
|
45
69
|
```sh
|
|
@@ -67,11 +91,15 @@ dsh-doctor --fix --yes --json
|
|
|
67
91
|
- profile、home 和 bundle 的 `cordis.patch.yml` 语法与顶层结构,包括 `!!js` 表达式
|
|
68
92
|
- `settings.yaml` 和 `.credentials.yaml` 的安全结构检查;凭据诊断不输出秘密值
|
|
69
93
|
- profile 依赖、bundle 声明、patch 文件和 client bundle 是否存在
|
|
94
|
+
- profile `package.json`、`pnpm-lock.yaml` importer 与实际安装版本是否一致
|
|
95
|
+
- 所有直接插件(包括纯 bundle/服务端插件)的 Node.js `engines`、Harness peer range、旧 DSH 依赖与当前运行时是否兼容
|
|
96
|
+
- 当前 DSH CLI、Harness 工作区和 profile 顶层 `@deepseek-ai/dsh-*` 包是否发生版本漂移或残留
|
|
70
97
|
- `dsh.client` 的 `platform`、`immediately`、`inject`、`external` 和 `./client` export contract
|
|
71
98
|
- client bundle 中字面量 `require()` 与 external/module supplier 的一致性
|
|
72
99
|
- 已删除的 Harness client package 引用
|
|
73
100
|
- 第三方插件 peer range 与当前 Harness 实际版本的兼容性
|
|
74
101
|
- Harness installation 优先于 profile 同名 bundle 的真实解析顺序
|
|
102
|
+
- 按 Harness 官方层级顺序静态组合 bundle、profile 和 home patch,检查缺失 target、错误 group insert 与 name assertion;不会加载插件
|
|
75
103
|
|
|
76
104
|
## 修复安全边界
|
|
77
105
|
|
|
@@ -80,6 +108,7 @@ dsh-doctor --fix --yes --json
|
|
|
80
108
|
- 文件修复在确认前展示路径,确认后再次校验 SHA-256 指纹。
|
|
81
109
|
- 写入前创建 `.dsh-doctor-<timestamp>.bak` 备份,再通过同目录临时文件原子替换。
|
|
82
110
|
- 外部命令使用固定 argv 调用,不拼接 shell 命令。
|
|
111
|
+
- `--json --fix --yes` 会捕获子命令输出并放入修复结果,保证 stdout 始终只有一个合法 JSON 文档。
|
|
83
112
|
- 命令修复绑定当前诊断的 `DSH_HOME`,并展示解析出的真实 CLI 路径;不会假定 PATH 中存在 `dsh`。
|
|
84
113
|
- 任一步失败即停止后续修复,并保留已经创建的备份。
|
|
85
114
|
- 完成后重新运行全部诊断,以最终状态决定退出码。
|
|
@@ -95,9 +124,10 @@ dsh-doctor --fix --yes --json
|
|
|
95
124
|
## 当前限制
|
|
96
125
|
|
|
97
126
|
- 静态扫描只识别代码中的字面量 `require("package")`;动态依赖需要未来的 bundle 元数据协议。
|
|
98
|
-
- 配置检查覆盖语法和 Doctor
|
|
127
|
+
- 配置检查覆盖语法和 Doctor 能稳定对齐的结构,并按当前 Harness patch 算法做无执行组合检查;不会求值 `!!js`,也不会加载第三方插件。
|
|
99
128
|
- 版本兼容以插件 `peerDependencies` 和当前可解析 Harness package 版本为依据;未声明兼容范围的插件只能做结构检查。
|
|
100
|
-
-
|
|
129
|
+
- lockfile 检查只对 profile 的直接依赖 importer 做确定性交叉验证,不递归扫描整个 npm 依赖树。
|
|
130
|
+
- 真实启动探针尚未启用;即使复制 `DSH_HOME`,第三方插件仍可能访问网络、绝对路径或启动外部进程,不能宣称无副作用。
|
|
101
131
|
|
|
102
132
|
## 从源码开发
|
|
103
133
|
|
|
@@ -107,4 +137,6 @@ npm run check
|
|
|
107
137
|
node src/cli.mjs --help
|
|
108
138
|
```
|
|
109
139
|
|
|
110
|
-
新包需要先由 `@bruc3van` 对应的 npm 账号完成一次 `npm publish --access public`,创建公开包页面。然后在 npm 包设置中添加 GitHub Actions Trusted Publisher:Organization or user 为 `bruc3van`,Repository 为 `dsh-doctor`,Workflow filename 为 `release.yml`,Environment 留空,Allowed actions 只启用 `npm publish
|
|
140
|
+
新包需要先由 `@bruc3van` 对应的 npm 账号完成一次 `npm publish --access public`,创建公开包页面。然后在 npm 包设置中添加 GitHub Actions Trusted Publisher:Organization or user 为 `bruc3van`,Repository 为 `dsh-doctor`,Workflow filename 为 `release.yml`,Environment 留空,Allowed actions 只启用 `npm publish`。
|
|
141
|
+
|
|
142
|
+
后续发布前,需要在 `CHANGELOG.md` 中增加与版本 tag 同名的中文 `## vX.Y.Z` 条目。推送与 `package.json` 版本一致的 tag 后,workflow 会通过 OIDC 发布 npm 包、生成 provenance,并自动用该中文条目创建或更新 GitHub Release;缺少中文条目时发布流程会失败。不需要保存长期 npm token。
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bruc3van/dsh-doctor",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Diagnose DSH plugins broken by upgrades, startup blockers, and profile issues with explicit, confirmed repairs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"files": [
|
|
19
19
|
"src",
|
|
20
20
|
"README.md",
|
|
21
|
+
"README.en.md",
|
|
21
22
|
"LICENSE"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
package/src/cli.mjs
CHANGED
|
@@ -118,7 +118,7 @@ async function main() {
|
|
|
118
118
|
confirmed = /^(?:y(?:es)?|是|确认)$/i.test(answer.trim())
|
|
119
119
|
}
|
|
120
120
|
if (confirmed) {
|
|
121
|
-
repairs = applyRepairs(actions)
|
|
121
|
+
repairs = applyRepairs(actions, { captureOutput: options.json })
|
|
122
122
|
if (repairs.every(item => item.status === 'applied')) report = diagnose(options)
|
|
123
123
|
}
|
|
124
124
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -22,10 +22,11 @@ const BUILTIN_MODULES = new Set(builtinModules.map(name => name.replace(/^node:/
|
|
|
22
22
|
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 }
|
|
23
23
|
const JS_EXPRESSION = new yaml.Type('tag:yaml.org,2002:js', {
|
|
24
24
|
kind: 'scalar',
|
|
25
|
-
resolve: value => typeof value === 'string'
|
|
26
|
-
construct: value => value,
|
|
25
|
+
resolve: value => typeof value === 'string',
|
|
26
|
+
construct: value => ({ __jsExpr: value }),
|
|
27
27
|
})
|
|
28
|
-
|
|
28
|
+
// Keep this dialect aligned with Harness entryListSchema: JSON values plus !!js.
|
|
29
|
+
const PATCH_SCHEMA = yaml.JSON_SCHEMA.extend([JS_EXPRESSION])
|
|
29
30
|
|
|
30
31
|
function objectRecord(value) {
|
|
31
32
|
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined
|
|
@@ -38,6 +39,7 @@ function finding(severity, code, message, options = {}) {
|
|
|
38
39
|
message,
|
|
39
40
|
...options.package === undefined ? {} : { package: options.package },
|
|
40
41
|
...options.evidence === undefined ? {} : { evidence: options.evidence },
|
|
42
|
+
...options.details === undefined ? {} : { details: options.details },
|
|
41
43
|
...options.suggestion === undefined ? {} : { suggestion: options.suggestion },
|
|
42
44
|
...options.repair === undefined ? {} : { repair: options.repair },
|
|
43
45
|
}
|
|
@@ -292,7 +294,7 @@ function resolveHarnessContext(home, explicitRoot, findings) {
|
|
|
292
294
|
return { root: canonical, packages: new Map(), version: manifest?.version, authoritative: false }
|
|
293
295
|
}
|
|
294
296
|
|
|
295
|
-
findings.push(finding('warning', 'HARNESS_INSTALLATION_UNKNOWN', 'Could not locate the
|
|
297
|
+
findings.push(finding('warning', 'HARNESS_INSTALLATION_UNKNOWN', 'Could not locate the DSH installation currently used by this home.', {
|
|
296
298
|
evidence: sharedDsh,
|
|
297
299
|
suggestion: 'Pass --harness-root when diagnosing a source checkout.',
|
|
298
300
|
}))
|
|
@@ -351,11 +353,12 @@ function clientExport(manifest) {
|
|
|
351
353
|
return undefined
|
|
352
354
|
}
|
|
353
355
|
|
|
354
|
-
function dependencyEntries(value, field, file, findings) {
|
|
356
|
+
function dependencyEntries(value, field, file, findings, packageName) {
|
|
355
357
|
if (value === undefined) return []
|
|
356
358
|
const record = objectRecord(value)
|
|
357
359
|
if (record === undefined || Object.values(record).some(item => typeof item !== 'string')) {
|
|
358
360
|
findings.push(finding('error', 'INVALID_DEPENDENCY_MAP', `${field} must be an object of package names to string ranges.`, {
|
|
361
|
+
package: packageName,
|
|
359
362
|
evidence: file,
|
|
360
363
|
suggestion: `Repair ${field} before managing or starting this profile.`,
|
|
361
364
|
}))
|
|
@@ -554,7 +557,7 @@ function inspectClientPackage(record, context) {
|
|
|
554
557
|
? harnessPackages.get(supplier)
|
|
555
558
|
: harnessPackages.get(supplier) ?? resolvePackage(supplier)
|
|
556
559
|
if (supplied === undefined || supplied.manifest?.dsh?.client === undefined) {
|
|
557
|
-
findings.push(finding('error', 'CLIENT_EXTERNAL_WITHOUT_SUPPLIER', `${name} requests ${specifier}, but the active
|
|
560
|
+
findings.push(finding('error', 'CLIENT_EXTERNAL_WITHOUT_SUPPLIER', `${name} requests ${specifier}, but the active DSH has no client module supplier.`, {
|
|
558
561
|
package: name,
|
|
559
562
|
evidence: record.file,
|
|
560
563
|
suggestion: disableSuggestion,
|
|
@@ -567,7 +570,7 @@ function inspectClientPackage(record, context) {
|
|
|
567
570
|
for (const dependency of inject ?? []) {
|
|
568
571
|
if (!dependency.startsWith('@deepseek-ai/dsh-')) continue
|
|
569
572
|
if (!harnessPackages.has(stripClientSuffix(dependency))) {
|
|
570
|
-
findings.push(finding('error', 'REMOVED_CLIENT_INJECT', `${name} injects ${dependency}, which is absent from the active
|
|
573
|
+
findings.push(finding('error', 'REMOVED_CLIENT_INJECT', `${name} injects ${dependency}, which is absent from the active DSH.`, {
|
|
571
574
|
package: name,
|
|
572
575
|
evidence: record.file,
|
|
573
576
|
suggestion: disableSuggestion,
|
|
@@ -576,16 +579,6 @@ function inspectClientPackage(record, context) {
|
|
|
576
579
|
}
|
|
577
580
|
}
|
|
578
581
|
|
|
579
|
-
const legacyPeers = Object.keys(record.manifest.peerDependencies ?? {})
|
|
580
|
-
.filter(peer => peer.startsWith('@deepseek-ai/dsh-') && !harnessPackages.has(peer))
|
|
581
|
-
.sort()
|
|
582
|
-
if (legacyPeers.length > 0) {
|
|
583
|
-
findings.push(finding('warning', 'LEGACY_HARNESS_PEERS', `${name} still declares Harness packages that no longer exist in the active source tree.`, {
|
|
584
|
-
package: name,
|
|
585
|
-
evidence: legacyPeers.join(', '),
|
|
586
|
-
suggestion: 'Treat this plugin as compatibility-risky and update it before the next Harness upgrade.',
|
|
587
|
-
}))
|
|
588
|
-
}
|
|
589
582
|
}
|
|
590
583
|
}
|
|
591
584
|
|
|
@@ -595,7 +588,7 @@ function inspectBundle(name, record, findings) {
|
|
|
595
588
|
package: name,
|
|
596
589
|
suggestion: 'Install the profile dependencies with the active DSH installation, upgrade the bundle, or remove it from the profile.',
|
|
597
590
|
}))
|
|
598
|
-
return
|
|
591
|
+
return undefined
|
|
599
592
|
}
|
|
600
593
|
const patch = record.manifest?.dsh?.bundle?.patch
|
|
601
594
|
if (typeof patch !== 'string' || patch.length === 0) {
|
|
@@ -604,7 +597,7 @@ function inspectBundle(name, record, findings) {
|
|
|
604
597
|
evidence: record.file,
|
|
605
598
|
suggestion: 'Upgrade or remove this bundle from dsh.profile.bundles.',
|
|
606
599
|
}))
|
|
607
|
-
return
|
|
600
|
+
return undefined
|
|
608
601
|
}
|
|
609
602
|
const file = safePackageFile(record.directory, patch)
|
|
610
603
|
if (file === undefined || !regularFile(file)) {
|
|
@@ -613,25 +606,135 @@ function inspectBundle(name, record, findings) {
|
|
|
613
606
|
evidence: file ?? `${record.file}: dsh.bundle.patch = ${JSON.stringify(patch)}`,
|
|
614
607
|
suggestion: 'Reinstall or upgrade this bundle, or remove it from the profile.',
|
|
615
608
|
}))
|
|
616
|
-
|
|
609
|
+
return undefined
|
|
610
|
+
}
|
|
611
|
+
const patches = inspectPatchFile(file, `${name} bundle patch`, findings, name)
|
|
612
|
+
return patches === undefined ? undefined : { label: name, file, patches, package: name }
|
|
617
613
|
}
|
|
618
614
|
|
|
619
|
-
function inspectPatchFile(file, subject, findings) {
|
|
615
|
+
function inspectPatchFile(file, subject, findings, packageName) {
|
|
620
616
|
let parsed
|
|
621
617
|
try {
|
|
622
618
|
parsed = yaml.load(readFileSync(file, 'utf8'), { schema: PATCH_SCHEMA })
|
|
623
619
|
} catch (error) {
|
|
624
620
|
findings.push(finding('error', 'INVALID_PATCH_YAML', `${subject} cannot be parsed.`, {
|
|
621
|
+
package: packageName,
|
|
625
622
|
evidence: `${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
626
623
|
suggestion: 'Repair the YAML syntax before starting this profile.',
|
|
627
624
|
}))
|
|
628
|
-
return
|
|
625
|
+
return undefined
|
|
629
626
|
}
|
|
630
627
|
if (!Array.isArray(parsed) || parsed.some(item => objectRecord(item) === undefined)) {
|
|
631
628
|
findings.push(finding('error', 'INVALID_PATCH_LIST', `${subject} must be a top-level YAML array of mappings.`, {
|
|
629
|
+
package: packageName,
|
|
632
630
|
evidence: file,
|
|
633
631
|
suggestion: 'Repair the patch structure before starting this profile.',
|
|
634
632
|
}))
|
|
633
|
+
return undefined
|
|
634
|
+
}
|
|
635
|
+
let valid = true
|
|
636
|
+
parsed.forEach((patch, index) => {
|
|
637
|
+
if (patch.id !== undefined && typeof patch.id !== 'string') {
|
|
638
|
+
valid = false
|
|
639
|
+
findings.push(finding('error', 'INVALID_PATCH_ID', `${subject} entry ${String(index + 1)} has a non-string id.`, {
|
|
640
|
+
package: packageName,
|
|
641
|
+
evidence: file,
|
|
642
|
+
suggestion: 'Use a string row id or omit id for a root insert patch.',
|
|
643
|
+
}))
|
|
644
|
+
}
|
|
645
|
+
if (patch.name !== undefined && typeof patch.name !== 'string') {
|
|
646
|
+
valid = false
|
|
647
|
+
findings.push(finding('error', 'INVALID_PATCH_NAME', `${subject} entry ${String(index + 1)} has a non-string name assertion.`, {
|
|
648
|
+
package: packageName,
|
|
649
|
+
evidence: file,
|
|
650
|
+
suggestion: 'Use a string plugin name assertion or omit the name field.',
|
|
651
|
+
}))
|
|
652
|
+
}
|
|
653
|
+
if (patch.insert !== undefined
|
|
654
|
+
&& (!Array.isArray(patch.insert) || patch.insert.some(item => objectRecord(item) === undefined))) {
|
|
655
|
+
valid = false
|
|
656
|
+
findings.push(finding('error', 'INVALID_PATCH_INSERT', `${subject} entry ${String(index + 1)} insert must be an array of mappings.`, {
|
|
657
|
+
package: packageName,
|
|
658
|
+
evidence: file,
|
|
659
|
+
suggestion: 'Repair the insert list before starting this profile.',
|
|
660
|
+
}))
|
|
661
|
+
}
|
|
662
|
+
})
|
|
663
|
+
return valid ? parsed : undefined
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Mirrors the current Harness applyEntryPatches control flow without importing
|
|
667
|
+
// code from (or executing code inside) the installation being diagnosed.
|
|
668
|
+
function inspectPatchComposition(layers, findings) {
|
|
669
|
+
const entryMap = new Map()
|
|
670
|
+
const indexEntries = (values) => {
|
|
671
|
+
for (const entry of values) {
|
|
672
|
+
if (typeof entry.id === 'string' && entry.id.length > 0) entryMap.set(entry.id, entry)
|
|
673
|
+
if (entry.group && Array.isArray(entry.config)) indexEntries(entry.config)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const layer of layers) {
|
|
677
|
+
layer.patches.forEach((patch, index) => {
|
|
678
|
+
const evidence = `${layer.file}: entry ${String(index + 1)}`
|
|
679
|
+
const hasInsert = patch.insert !== undefined
|
|
680
|
+
if (hasInsert) {
|
|
681
|
+
if (!Array.isArray(patch.insert)) return
|
|
682
|
+
if (patch.id !== undefined) {
|
|
683
|
+
const target = entryMap.get(patch.id)
|
|
684
|
+
if (target === undefined) {
|
|
685
|
+
findings.push(finding('warning', 'PATCH_TARGET_NOT_FOUND', `${layer.label} insert targets missing row ${patch.id}.`, {
|
|
686
|
+
package: layer.package,
|
|
687
|
+
evidence,
|
|
688
|
+
suggestion: 'Check whether this overlay is intended for the selected profile and bundle order.',
|
|
689
|
+
}))
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
if (!target.group) {
|
|
693
|
+
findings.push(finding('warning', 'PATCH_TARGET_NOT_GROUP', `${layer.label} inserts into row ${patch.id}, which is not a group.`, {
|
|
694
|
+
package: layer.package,
|
|
695
|
+
evidence,
|
|
696
|
+
suggestion: 'Target a group row or use a root insert.',
|
|
697
|
+
}))
|
|
698
|
+
return
|
|
699
|
+
}
|
|
700
|
+
if (!Array.isArray(target.config)) target.config = []
|
|
701
|
+
target.config.push(...structuredClone(patch.insert))
|
|
702
|
+
indexEntries(target.config.slice(-patch.insert.length))
|
|
703
|
+
} else {
|
|
704
|
+
const inserted = structuredClone(patch.insert)
|
|
705
|
+
indexEntries(inserted)
|
|
706
|
+
}
|
|
707
|
+
return
|
|
708
|
+
}
|
|
709
|
+
if (patch.id === undefined) {
|
|
710
|
+
findings.push(finding('warning', 'PATCH_ID_REQUIRED', `${layer.label} has a non-insert patch without an id.`, {
|
|
711
|
+
package: layer.package,
|
|
712
|
+
evidence,
|
|
713
|
+
suggestion: 'Add the target row id or turn the entry into an insert patch.',
|
|
714
|
+
}))
|
|
715
|
+
return
|
|
716
|
+
}
|
|
717
|
+
const target = entryMap.get(patch.id)
|
|
718
|
+
if (target === undefined) {
|
|
719
|
+
findings.push(finding('warning', 'PATCH_TARGET_NOT_FOUND', `${layer.label} targets missing row ${patch.id}.`, {
|
|
720
|
+
package: layer.package,
|
|
721
|
+
evidence,
|
|
722
|
+
suggestion: 'Check whether this overlay is intended for the selected profile and bundle order.',
|
|
723
|
+
}))
|
|
724
|
+
return
|
|
725
|
+
}
|
|
726
|
+
if (patch.name !== undefined && patch.name !== target.name) {
|
|
727
|
+
findings.push(finding('warning', 'PATCH_NAME_MISMATCH', `${layer.label} name assertion does not match row ${patch.id}.`, {
|
|
728
|
+
package: layer.package,
|
|
729
|
+
evidence: `${evidence}: expected ${JSON.stringify(target.name)}, got ${JSON.stringify(patch.name)}`,
|
|
730
|
+
suggestion: 'Update the assertion or target the intended row.',
|
|
731
|
+
}))
|
|
732
|
+
return
|
|
733
|
+
}
|
|
734
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
735
|
+
if (key !== 'id' && key !== 'insert' && key !== 'name') target[key] = structuredClone(value)
|
|
736
|
+
}
|
|
737
|
+
})
|
|
635
738
|
}
|
|
636
739
|
}
|
|
637
740
|
|
|
@@ -702,33 +805,288 @@ function inspectCredentials(file, findings) {
|
|
|
702
805
|
}
|
|
703
806
|
|
|
704
807
|
function inspectPatchFileIfPresent(file, subject, findings) {
|
|
705
|
-
|
|
808
|
+
return existsSync(file) ? inspectPatchFile(file, subject, findings) : undefined
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function looksLikeSemverRange(value) {
|
|
812
|
+
return /^(?:\s*[v=~^<>*]|\s*\d)/.test(value)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function inspectPluginNodeEngine(record, nodeVersion, findings) {
|
|
816
|
+
const range = record.manifest?.engines?.node
|
|
817
|
+
if (range === undefined) return
|
|
818
|
+
const name = record.requestedName ?? record.manifest.name
|
|
819
|
+
if (typeof range !== 'string' || semver.validRange(range) === null) {
|
|
820
|
+
findings.push(finding('warning', 'INVALID_NODE_ENGINE_RANGE', `${name} declares an invalid Node.js engine range.`, {
|
|
821
|
+
package: name,
|
|
822
|
+
evidence: `${record.file}: engines.node = ${JSON.stringify(range)}`,
|
|
823
|
+
suggestion: 'The plugin author should publish a valid engines.node range.',
|
|
824
|
+
}))
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
if (nodeVersion === undefined) return
|
|
828
|
+
if (!semver.satisfies(nodeVersion, range, { includePrerelease: true })) {
|
|
829
|
+
findings.push(finding('warning', 'PLUGIN_NODE_VERSION_MISMATCH', `${name} does not support the Node.js version used by the active DSH CLI.`, {
|
|
830
|
+
package: name,
|
|
831
|
+
evidence: `engines.node ${range} (active ${nodeVersion})`,
|
|
832
|
+
suggestion: `Update ${name} or run DSH with a supported Node.js version.`,
|
|
833
|
+
}))
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function lockedRegistryVersion(value) {
|
|
838
|
+
if (typeof value !== 'string') return undefined
|
|
839
|
+
const matched = value.match(/^(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)(?:\(|$)/)?.[1]
|
|
840
|
+
return matched !== undefined && semver.valid(matched) !== null ? matched : undefined
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function inspectPnpmLock(profileDir, dependencyEntriesList, records, findings, commandRepair, profile) {
|
|
844
|
+
const file = join(profileDir, 'pnpm-lock.yaml')
|
|
845
|
+
const installRepair = () => commandRepair(
|
|
846
|
+
`install-profile:${profile}`,
|
|
847
|
+
`Install the declared dependencies for profile ${profile}.`,
|
|
848
|
+
['plugin', '--profile', profile, 'install'],
|
|
849
|
+
{ profile },
|
|
850
|
+
)
|
|
851
|
+
if (!existsSync(file)) return { file, present: false }
|
|
852
|
+
let root
|
|
853
|
+
try {
|
|
854
|
+
const document = parseDocument(readFileSync(file, 'utf8'), { prettyErrors: true, uniqueKeys: true })
|
|
855
|
+
if (document.errors.length > 0) throw new Error(document.errors.map(error => error.message).join('; '))
|
|
856
|
+
root = document.toJS()
|
|
857
|
+
} catch (error) {
|
|
858
|
+
findings.push(finding('error', 'INVALID_PNPM_LOCKFILE', 'The profile pnpm lockfile cannot be parsed.', {
|
|
859
|
+
evidence: `${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
860
|
+
suggestion: 'Run the exact profile install command after repairing or regenerating the lockfile.',
|
|
861
|
+
repair: commandRepair(
|
|
862
|
+
`install-profile:${profile}`,
|
|
863
|
+
`Install the declared dependencies for profile ${profile}.`,
|
|
864
|
+
['plugin', '--profile', profile, 'install'],
|
|
865
|
+
{ profile },
|
|
866
|
+
),
|
|
867
|
+
}))
|
|
868
|
+
return { file, present: true, valid: false }
|
|
869
|
+
}
|
|
870
|
+
const importer = objectRecord(objectRecord(objectRecord(root)?.importers)?.['.'])
|
|
871
|
+
const locked = importer?.dependencies
|
|
872
|
+
const lockedDependencies = objectRecord(locked)
|
|
873
|
+
if (lockedDependencies === undefined) {
|
|
874
|
+
if (dependencyEntriesList.length === 0 && locked === undefined) {
|
|
875
|
+
return { file, present: true, valid: true }
|
|
876
|
+
}
|
|
877
|
+
findings.push(finding('warning', 'PNPM_LOCKFILE_IMPORTER_MISSING', 'The profile pnpm lockfile has no usable root dependencies map.', {
|
|
878
|
+
evidence: file,
|
|
879
|
+
suggestion: 'Use the exact profile install command to reconcile the lockfile.',
|
|
880
|
+
repair: installRepair(),
|
|
881
|
+
}))
|
|
882
|
+
return { file, present: true, valid: false }
|
|
883
|
+
}
|
|
884
|
+
const declaredNames = new Set(dependencyEntriesList.map(([name]) => name))
|
|
885
|
+
for (const [name, declared] of dependencyEntriesList) {
|
|
886
|
+
const entry = lockedDependencies[name]
|
|
887
|
+
if (entry === undefined) {
|
|
888
|
+
findings.push(finding('warning', 'LOCKFILE_DEPENDENCY_MISSING', `Profile dependency ${name} is absent from the pnpm lockfile importer.`, {
|
|
889
|
+
package: name,
|
|
890
|
+
evidence: file,
|
|
891
|
+
suggestion: 'Use the exact profile install command to reconcile the manifest and lockfile.',
|
|
892
|
+
repair: installRepair(),
|
|
893
|
+
}))
|
|
894
|
+
continue
|
|
895
|
+
}
|
|
896
|
+
const lockedEntry = typeof entry === 'string' ? { version: entry } : objectRecord(entry)
|
|
897
|
+
const specifier = lockedEntry?.specifier
|
|
898
|
+
if (typeof specifier === 'string' && specifier !== declared) {
|
|
899
|
+
findings.push(finding('warning', 'LOCKFILE_SPECIFIER_MISMATCH', `Profile dependency ${name} has a different specifier in pnpm-lock.yaml.`, {
|
|
900
|
+
package: name,
|
|
901
|
+
evidence: `package.json ${declared} (lockfile ${specifier})`,
|
|
902
|
+
suggestion: 'Use the exact profile install command to reconcile the manifest and lockfile.',
|
|
903
|
+
repair: installRepair(),
|
|
904
|
+
}))
|
|
905
|
+
}
|
|
906
|
+
const lockedVersion = lockedRegistryVersion(lockedEntry?.version)
|
|
907
|
+
const installedVersion = records.get(name)?.manifest?.version
|
|
908
|
+
if (lockedVersion !== undefined && typeof installedVersion === 'string'
|
|
909
|
+
&& semver.valid(installedVersion) !== null && installedVersion !== lockedVersion) {
|
|
910
|
+
findings.push(finding('warning', 'LOCKFILE_INSTALLED_VERSION_MISMATCH', `Profile dependency ${name} does not match its locked version.`, {
|
|
911
|
+
package: name,
|
|
912
|
+
evidence: `locked ${lockedVersion} (installed ${installedVersion})`,
|
|
913
|
+
suggestion: 'Use the exact profile install command to restore the locked installation.',
|
|
914
|
+
repair: installRepair(),
|
|
915
|
+
}))
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
for (const name of Object.keys(lockedDependencies)) {
|
|
919
|
+
if (declaredNames.has(name)) continue
|
|
920
|
+
findings.push(finding('warning', 'LOCKFILE_DEPENDENCY_STALE', `pnpm-lock.yaml still lists undeclared profile dependency ${name}.`, {
|
|
921
|
+
package: name,
|
|
922
|
+
evidence: file,
|
|
923
|
+
suggestion: 'Use the exact profile install command to remove stale lockfile importer entries.',
|
|
924
|
+
repair: installRepair(),
|
|
925
|
+
}))
|
|
926
|
+
}
|
|
927
|
+
return { file, present: true, valid: true }
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function inspectRuntimeAlignment(harness, dshCli, findings) {
|
|
931
|
+
// Harness release tooling uses the root manifest as the DSH release-family
|
|
932
|
+
// baseline and bumps apps/cli plus the published members to the same version.
|
|
933
|
+
if (typeof harness.version !== 'string' || typeof dshCli?.version !== 'string') return
|
|
934
|
+
if (semver.valid(harness.version) === null || semver.valid(dshCli.version) === null) return
|
|
935
|
+
if (harness.version === dshCli.version) return
|
|
936
|
+
findings.push(finding('warning', 'DSH_CLI_HARNESS_VERSION_MISMATCH', 'The active DSH CLI and diagnosed Harness installation have different versions.', {
|
|
937
|
+
evidence: `DSH CLI ${dshCli.version} (Harness ${harness.version})`,
|
|
938
|
+
suggestion: 'Diagnose with the DSH CLI and Harness checkout used by the same installation.',
|
|
939
|
+
}))
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function inspectProfileHarnessPackages(profileDir, home, harness, findings) {
|
|
943
|
+
const profileScope = join(profileDir, 'node_modules', '@deepseek-ai')
|
|
944
|
+
if (!existsSync(profileScope)) return
|
|
945
|
+
let entries
|
|
946
|
+
try {
|
|
947
|
+
entries = readdirSync(profileScope, { withFileTypes: true })
|
|
948
|
+
} catch (error) {
|
|
949
|
+
findings.push(finding('warning', 'PROFILE_HARNESS_SCOPE_UNREADABLE', 'The profile-local @deepseek-ai package scope cannot be read as a directory.', {
|
|
950
|
+
evidence: `${profileScope}: ${error instanceof Error ? error.message : String(error)}`,
|
|
951
|
+
suggestion: 'Reinstall the profile with the active DSH CLI to repair its node_modules layout.',
|
|
952
|
+
}))
|
|
953
|
+
return
|
|
954
|
+
}
|
|
955
|
+
for (const entry of entries) {
|
|
956
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
957
|
+
const name = `@deepseek-ai/${entry.name}`
|
|
958
|
+
if (!name.startsWith('@deepseek-ai/dsh-') && name !== '@deepseek-ai/dsh') continue
|
|
959
|
+
const profilePackage = join(profileScope, entry.name)
|
|
960
|
+
const profileManifestFile = join(profilePackage, 'package.json')
|
|
961
|
+
if (!existsSync(profileManifestFile)) continue
|
|
962
|
+
let profileManifest
|
|
963
|
+
try {
|
|
964
|
+
profileManifest = JSON.parse(readFileSync(profileManifestFile, 'utf8'))
|
|
965
|
+
} catch {
|
|
966
|
+
continue
|
|
967
|
+
}
|
|
968
|
+
const sharedPackage = join(home, 'profiles', 'node_modules', '@deepseek-ai', entry.name)
|
|
969
|
+
if (existsSync(join(sharedPackage, 'package.json'))) {
|
|
970
|
+
try {
|
|
971
|
+
if (realpathSync(profilePackage) !== realpathSync(sharedPackage)) {
|
|
972
|
+
const sharedManifest = JSON.parse(readFileSync(join(sharedPackage, 'package.json'), 'utf8'))
|
|
973
|
+
if (typeof profileManifest.version === 'string' && typeof sharedManifest.version === 'string'
|
|
974
|
+
&& profileManifest.version !== sharedManifest.version) {
|
|
975
|
+
findings.push(finding('warning', 'DUPLICATE_HARNESS_PACKAGE_VERSION', `${name} exists in the profile and shared DSH installation at different versions.`, {
|
|
976
|
+
package: name,
|
|
977
|
+
evidence: `profile ${profileManifest.version} (${profilePackage}), shared ${sharedManifest.version} (${sharedPackage})`,
|
|
978
|
+
suggestion: 'Reinstall the profile with the active DSH CLI so its module resolution uses one compatible version.',
|
|
979
|
+
}))
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
} catch {
|
|
983
|
+
// Other manifest and filesystem checks report unreadable package state.
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (harness.authoritative && !harness.packages.has(name)) {
|
|
987
|
+
findings.push(finding('warning', 'STALE_PROFILE_HARNESS_PACKAGE', `${name} remains in the profile but is absent from the active DSH.`, {
|
|
988
|
+
package: name,
|
|
989
|
+
evidence: profileManifestFile,
|
|
990
|
+
suggestion: 'Reinstall the profile with the active DSH CLI and review plugins that still require this package.',
|
|
991
|
+
}))
|
|
992
|
+
}
|
|
993
|
+
}
|
|
706
994
|
}
|
|
707
995
|
|
|
708
996
|
function inspectCompatibility(record, context) {
|
|
709
|
-
const {
|
|
710
|
-
|
|
997
|
+
const {
|
|
998
|
+
commandRepair, findings, harnessPackages, harnessPackagesAuthoritative, profile, resolvePackage,
|
|
999
|
+
} = context
|
|
1000
|
+
const packageName = record.requestedName ?? record.manifest.name
|
|
1001
|
+
const peers = dependencyEntries(record.manifest.peerDependencies, 'peerDependencies', record.file, findings, packageName)
|
|
1002
|
+
const dependencies = dependencyEntries(record.manifest.dependencies, 'dependencies', record.file, findings, packageName)
|
|
1003
|
+
if (harnessPackagesAuthoritative) {
|
|
1004
|
+
const removedPeers = peers
|
|
1005
|
+
.map(([name]) => name)
|
|
1006
|
+
.filter(name => name.startsWith('@deepseek-ai/dsh-') && !harnessPackages.has(name))
|
|
1007
|
+
.sort()
|
|
1008
|
+
if (removedPeers.length > 0) {
|
|
1009
|
+
findings.push(finding('warning', 'LEGACY_HARNESS_PEERS', `${packageName} declares old interface packages that the active DSH has removed.`, {
|
|
1010
|
+
package: packageName,
|
|
1011
|
+
evidence: removedPeers.join(', '),
|
|
1012
|
+
suggestion: 'Update this plugin before relying on it with the current DSH release.',
|
|
1013
|
+
repair: updateRepair(profile, packageName, commandRepair),
|
|
1014
|
+
}))
|
|
1015
|
+
}
|
|
1016
|
+
const removedDependencies = dependencies
|
|
1017
|
+
.map(([name]) => name)
|
|
1018
|
+
.filter(name => name.startsWith('@deepseek-ai/dsh-') && !harnessPackages.has(name))
|
|
1019
|
+
.sort()
|
|
1020
|
+
if (removedDependencies.length > 0) {
|
|
1021
|
+
findings.push(finding('warning', 'LEGACY_HARNESS_DEPENDENCIES', `${packageName} depends on Harness packages that no longer exist in the active DSH.`, {
|
|
1022
|
+
package: packageName,
|
|
1023
|
+
evidence: removedDependencies.join(', '),
|
|
1024
|
+
suggestion: 'Update this plugin; its bundled DSH APIs may be incompatible with the current release.',
|
|
1025
|
+
repair: updateRepair(profile, packageName, commandRepair),
|
|
1026
|
+
}))
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
711
1029
|
const mismatches = []
|
|
712
1030
|
for (const [name, range] of peers) {
|
|
713
1031
|
if (!name.startsWith('@deepseek-ai/') && name !== 'cordis') continue
|
|
1032
|
+
if (semver.validRange(range) === null) {
|
|
1033
|
+
findings.push(finding('warning', 'INVALID_HARNESS_PEER_RANGE', `${packageName} declares an invalid Harness peer range for ${name}.`, {
|
|
1034
|
+
package: packageName,
|
|
1035
|
+
evidence: `${name}: ${range}`,
|
|
1036
|
+
suggestion: 'The plugin author should publish a valid peer dependency range.',
|
|
1037
|
+
}))
|
|
1038
|
+
continue
|
|
1039
|
+
}
|
|
714
1040
|
const supplier = harnessPackages.get(name) ?? resolvePackage(name)
|
|
715
1041
|
if (supplier === undefined) continue
|
|
716
1042
|
const version = supplier.manifest?.version
|
|
717
|
-
if (typeof version !== 'string' || semver.valid(version) === null
|
|
1043
|
+
if (typeof version !== 'string' || semver.valid(version) === null) continue
|
|
718
1044
|
if (semver.satisfies(version, range, { includePrerelease: true })) continue
|
|
719
|
-
mismatches.push(
|
|
1045
|
+
mismatches.push({ name, required: range, active: version })
|
|
720
1046
|
}
|
|
721
1047
|
if (mismatches.length > 0) {
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
1048
|
+
const grouped = new Map()
|
|
1049
|
+
for (const mismatch of mismatches) {
|
|
1050
|
+
const key = JSON.stringify([mismatch.required, mismatch.active])
|
|
1051
|
+
const group = grouped.get(key) ?? {
|
|
1052
|
+
required: mismatch.required, active: mismatch.active, packages: [],
|
|
1053
|
+
}
|
|
1054
|
+
group.packages.push(mismatch.name)
|
|
1055
|
+
grouped.set(key, group)
|
|
1056
|
+
}
|
|
1057
|
+
const groups = [...grouped.values()]
|
|
1058
|
+
.map(group => ({ ...group, packages: group.packages.sort() }))
|
|
1059
|
+
const evidence = groups.flatMap((group, index) => [
|
|
1060
|
+
...(groups.length > 1 ? [`Group ${String(index + 1)}:`] : []),
|
|
1061
|
+
`${groups.length > 1 ? ' ' : ''}Plugin requires: ${group.required}`,
|
|
1062
|
+
`${groups.length > 1 ? ' ' : ''}Active DSH: ${group.active}`,
|
|
1063
|
+
`${groups.length > 1 ? ' ' : ''}Affected ${String(group.packages.length)} package(s): ${group.packages.join(', ')}`,
|
|
1064
|
+
]).join('\n')
|
|
1065
|
+
findings.push(finding('warning', 'HARNESS_PEER_VERSION_MISMATCH', `${packageName} declares compatibility ranges that exclude the active DSH version.`, {
|
|
1066
|
+
package: packageName,
|
|
1067
|
+
evidence,
|
|
1068
|
+
details: { peerVersionGroups: groups },
|
|
1069
|
+
suggestion: `Update ${packageName} to a release compatible with the active DSH.`,
|
|
1070
|
+
repair: updateRepair(profile, packageName, commandRepair),
|
|
728
1071
|
}))
|
|
729
1072
|
}
|
|
730
1073
|
}
|
|
731
1074
|
|
|
1075
|
+
function hasHarnessCompatibilityDeclaration(record) {
|
|
1076
|
+
const peers = objectRecord(record.manifest.peerDependencies)
|
|
1077
|
+
return peers !== undefined && Object.keys(peers)
|
|
1078
|
+
.some(name => name.startsWith('@deepseek-ai/') || name === 'cordis')
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function pluginCompatibility(record, findings) {
|
|
1082
|
+
const name = record.requestedName ?? record.manifest.name
|
|
1083
|
+
const related = findings.filter(item => item.package === name)
|
|
1084
|
+
if (related.some(item => item.severity === 'error')) return 'incompatible'
|
|
1085
|
+
if (related.some(item => item.severity === 'warning')) return 'risk'
|
|
1086
|
+
if (!hasHarnessCompatibilityDeclaration(record)) return 'unknown'
|
|
1087
|
+
return 'compatible'
|
|
1088
|
+
}
|
|
1089
|
+
|
|
732
1090
|
export function defaultDshHome(env = process.env) {
|
|
733
1091
|
const configured = env.DSH_HOME?.trim()
|
|
734
1092
|
if (configured !== undefined && configured.length > 0) {
|
|
@@ -779,6 +1137,15 @@ export function diagnose(options = {}) {
|
|
|
779
1137
|
const resolvePackage = packageResolver(profileDir, home, harness.packages, findings)
|
|
780
1138
|
const resolveBundle = bundleResolver(profileDir, home, harness.packages, findings)
|
|
781
1139
|
const dependencyEntriesList = dependencyEntries(profileManifest.dependencies, 'dependencies', profileManifestFile, findings)
|
|
1140
|
+
for (const [name, range] of dependencyEntriesList) {
|
|
1141
|
+
if (looksLikeSemverRange(range) && semver.validRange(range) === null) {
|
|
1142
|
+
findings.push(finding('error', 'INVALID_PROFILE_DEPENDENCY_RANGE', `Profile dependency ${name} has an invalid semantic version range.`, {
|
|
1143
|
+
package: name,
|
|
1144
|
+
evidence: `${profileManifestFile}: ${range}`,
|
|
1145
|
+
suggestion: 'Repair the dependency range before installing or starting this profile.',
|
|
1146
|
+
}))
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
782
1149
|
const dependencyNames = dependencyEntriesList.map(([name]) => name)
|
|
783
1150
|
const dshConfig = profileManifest.dsh
|
|
784
1151
|
const dshConfigValid = dshConfig === undefined || objectRecord(dshConfig) !== undefined
|
|
@@ -818,6 +1185,11 @@ export function diagnose(options = {}) {
|
|
|
818
1185
|
const record = resolvePackage(name)
|
|
819
1186
|
if (record !== undefined) records.set(name, record)
|
|
820
1187
|
}
|
|
1188
|
+
const lockfile = inspectPnpmLock(
|
|
1189
|
+
profileDir, dependencyEntriesList, records, findings, commandRepair, profile,
|
|
1190
|
+
)
|
|
1191
|
+
inspectRuntimeAlignment(harness, dshCli, findings)
|
|
1192
|
+
inspectProfileHarnessPackages(profileDir, home, harness, findings)
|
|
821
1193
|
const bundleRecords = new Map()
|
|
822
1194
|
for (const name of bundleNames) {
|
|
823
1195
|
const record = resolveBundle(name)
|
|
@@ -873,10 +1245,23 @@ export function diagnose(options = {}) {
|
|
|
873
1245
|
}))
|
|
874
1246
|
}
|
|
875
1247
|
}
|
|
876
|
-
|
|
1248
|
+
const patchLayers = []
|
|
1249
|
+
let patchCompositionValid = true
|
|
1250
|
+
for (const name of bundleNames) {
|
|
1251
|
+
const layer = inspectBundle(name, bundleRecords.get(name), findings)
|
|
1252
|
+
if (layer === undefined) patchCompositionValid = false
|
|
1253
|
+
else patchLayers.push(layer)
|
|
1254
|
+
}
|
|
877
1255
|
|
|
878
|
-
|
|
879
|
-
|
|
1256
|
+
for (const [file, subject, label] of [
|
|
1257
|
+
[join(profileDir, 'cordis.patch.yml'), 'profile patch', 'profile patch'],
|
|
1258
|
+
[join(home, 'cordis.patch.yml'), 'home patch', 'home patch'],
|
|
1259
|
+
]) {
|
|
1260
|
+
const patches = inspectPatchFileIfPresent(file, subject, findings)
|
|
1261
|
+
if (patches !== undefined) patchLayers.push({ file, label, patches })
|
|
1262
|
+
else if (existsSync(file)) patchCompositionValid = false
|
|
1263
|
+
}
|
|
1264
|
+
if (patchCompositionValid) inspectPatchComposition(patchLayers, findings)
|
|
880
1265
|
inspectSettings(join(home, 'settings.yaml'), findings)
|
|
881
1266
|
inspectCredentials(join(home, '.credentials.yaml'), findings)
|
|
882
1267
|
|
|
@@ -898,7 +1283,13 @@ export function diagnose(options = {}) {
|
|
|
898
1283
|
profile,
|
|
899
1284
|
resolvePackage,
|
|
900
1285
|
harnessPackages: harness.packages,
|
|
1286
|
+
harnessPackagesAuthoritative: harness.authoritative,
|
|
901
1287
|
})
|
|
1288
|
+
inspectPluginNodeEngine(
|
|
1289
|
+
record,
|
|
1290
|
+
dshCli?.command?.[0] === process.execPath ? process.version : undefined,
|
|
1291
|
+
findings,
|
|
1292
|
+
)
|
|
902
1293
|
}
|
|
903
1294
|
|
|
904
1295
|
return finish({
|
|
@@ -906,16 +1297,23 @@ export function diagnose(options = {}) {
|
|
|
906
1297
|
profile,
|
|
907
1298
|
profileDir,
|
|
908
1299
|
harness: { root: harness.root, version: harness.version },
|
|
1300
|
+
lockfile,
|
|
909
1301
|
dshCli: dshCli === undefined
|
|
910
1302
|
? { available: false, commandRepairNeeded }
|
|
911
1303
|
: { available: true, commandRepairNeeded, ...dshCli },
|
|
912
|
-
packages:
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1304
|
+
packages: dependencyNames.map(name => {
|
|
1305
|
+
const record = records.get(name)
|
|
1306
|
+
if (record === undefined) return { name, installed: false, compatibility: 'incompatible' }
|
|
1307
|
+
return {
|
|
1308
|
+
name: record.requestedName ?? record.manifest.name,
|
|
1309
|
+
version: record.manifest.version,
|
|
1310
|
+
directory: record.directory,
|
|
1311
|
+
installed: true,
|
|
1312
|
+
client: record.manifest?.dsh?.client !== undefined,
|
|
1313
|
+
bundle: record.manifest?.dsh?.bundle !== undefined,
|
|
1314
|
+
compatibility: pluginCompatibility(record, findings),
|
|
1315
|
+
}
|
|
1316
|
+
}),
|
|
919
1317
|
}, findings)
|
|
920
1318
|
}
|
|
921
1319
|
|
|
@@ -929,9 +1327,30 @@ function finish(context, findings) {
|
|
|
929
1327
|
warnings: findings.filter(item => item.severity === 'warning').length,
|
|
930
1328
|
info: findings.filter(item => item.severity === 'info').length,
|
|
931
1329
|
}
|
|
1330
|
+
const compatibility = {
|
|
1331
|
+
incompatible: context.packages.filter(item => item.compatibility === 'incompatible').length,
|
|
1332
|
+
risk: context.packages.filter(item => item.compatibility === 'risk').length,
|
|
1333
|
+
unknown: context.packages.filter(item => item.compatibility === 'unknown').length,
|
|
1334
|
+
compatible: context.packages.filter(item => item.compatibility === 'compatible').length,
|
|
1335
|
+
}
|
|
1336
|
+
context = { ...context, compatibility }
|
|
932
1337
|
return { version: 1, ok: summary.errors === 0, context, summary, findings }
|
|
933
1338
|
}
|
|
934
1339
|
|
|
1340
|
+
function appendReportField(lines, label, value, indent = ' ') {
|
|
1341
|
+
const valueLines = String(value).split('\n')
|
|
1342
|
+
if (valueLines.length === 1) {
|
|
1343
|
+
lines.push(`${indent}${label}: ${valueLines[0]}`)
|
|
1344
|
+
return
|
|
1345
|
+
}
|
|
1346
|
+
lines.push(`${indent}${label}:`)
|
|
1347
|
+
lines.push(...valueLines.map(line => `${indent} ${line}`))
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function uniqueStrings(values) {
|
|
1351
|
+
return [...new Set(values.filter(value => typeof value === 'string' && value.length > 0))]
|
|
1352
|
+
}
|
|
1353
|
+
|
|
935
1354
|
export function formatReport(report, options = {}) {
|
|
936
1355
|
const color = options.color ?? false
|
|
937
1356
|
const language = options.language ?? 'en'
|
|
@@ -950,25 +1369,139 @@ export function formatReport(report, options = {}) {
|
|
|
950
1369
|
'DSH Doctor',
|
|
951
1370
|
`${zh ? 'Profile' : 'Profile'}: ${report.context.profile}`,
|
|
952
1371
|
`${zh ? 'DSH 主目录' : 'Home'}: ${report.context.home}`,
|
|
953
|
-
|
|
1372
|
+
`${zh ? '当前使用的 DSH' : 'Active DSH'}: ${report.context.harness.version ?? 'unknown'}${report.context.harness.root ? ` (${report.context.harness.root})` : ''}`,
|
|
954
1373
|
`${zh ? 'DSH CLI' : 'DSH CLI'}: ${cliText}`,
|
|
955
|
-
`${zh ? '
|
|
1374
|
+
`${zh ? 'Profile 插件' : 'Profile plugins'}: ${String(report.context.packages.length)}`,
|
|
1375
|
+
zh
|
|
1376
|
+
? `插件兼容性: ${String(report.context.compatibility.incompatible)} 个不兼容,${String(report.context.compatibility.risk)} 个风险,${String(report.context.compatibility.unknown)} 个未知,${String(report.context.compatibility.compatible)} 个兼容`
|
|
1377
|
+
: `Plugin compatibility: ${String(report.context.compatibility.incompatible)} incompatible, ${String(report.context.compatibility.risk)} risk, ${String(report.context.compatibility.unknown)} unknown, ${String(report.context.compatibility.compatible)} compatible`,
|
|
956
1378
|
`${zh ? '输出语言' : 'Output language'}: ${languageName(language)}`,
|
|
957
1379
|
'',
|
|
958
1380
|
]
|
|
959
|
-
|
|
1381
|
+
const packageNames = new Set(report.context.packages.map(item => item.name))
|
|
1382
|
+
const pluginFindings = new Map()
|
|
1383
|
+
const environmentFindings = []
|
|
1384
|
+
for (const item of report.findings) {
|
|
1385
|
+
if (item.package !== undefined && packageNames.has(item.package)) {
|
|
1386
|
+
const items = pluginFindings.get(item.package) ?? []
|
|
1387
|
+
items.push(item)
|
|
1388
|
+
pluginFindings.set(item.package, items)
|
|
1389
|
+
} else environmentFindings.push(item)
|
|
1390
|
+
}
|
|
1391
|
+
const problemPackages = report.context.packages
|
|
1392
|
+
.filter(item => (item.compatibility === 'incompatible' || item.compatibility === 'risk')
|
|
1393
|
+
&& pluginFindings.has(item.name))
|
|
1394
|
+
.sort((left, right) => {
|
|
1395
|
+
const rank = { incompatible: 0, risk: 1 }
|
|
1396
|
+
return rank[left.compatibility] - rank[right.compatibility] || left.name.localeCompare(right.name)
|
|
1397
|
+
})
|
|
1398
|
+
const unknownPackages = report.context.packages
|
|
1399
|
+
.filter(item => item.compatibility === 'unknown')
|
|
1400
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
1401
|
+
|
|
1402
|
+
if (report.findings.length === 0 && unknownPackages.length === 0) {
|
|
960
1403
|
lines.push(paint('info', zh ? '正常 当前检查范围内未发现问题。' : 'OK No problems found by the MVP checks.'))
|
|
961
1404
|
} else {
|
|
962
|
-
|
|
1405
|
+
if (problemPackages.length > 0) {
|
|
1406
|
+
lines.push(zh
|
|
1407
|
+
? `插件问题(${String(problemPackages.length)} 个)`
|
|
1408
|
+
: `Plugin problems (${String(problemPackages.length)})`)
|
|
1409
|
+
lines.push('')
|
|
1410
|
+
for (const plugin of problemPackages) {
|
|
1411
|
+
const originals = pluginFindings.get(plugin.name)
|
|
1412
|
+
const status = plugin.compatibility === 'incompatible'
|
|
1413
|
+
? zh ? '不兼容' : 'INCOMPATIBLE'
|
|
1414
|
+
: zh ? '有风险' : 'RISK'
|
|
1415
|
+
const severity = plugin.compatibility === 'incompatible' ? 'error' : 'warning'
|
|
1416
|
+
lines.push(paint(severity, `[${status}] ${plugin.name}`))
|
|
1417
|
+
appendReportField(lines, zh ? '版本' : 'Version', plugin.installed === false
|
|
1418
|
+
? zh ? '未安装' : 'not installed'
|
|
1419
|
+
: plugin.version ?? (zh ? '未知' : 'unknown'))
|
|
1420
|
+
lines.push(zh
|
|
1421
|
+
? ` 问题(${String(originals.length)}):`
|
|
1422
|
+
: ` Problems (${String(originals.length)}):`)
|
|
1423
|
+
originals.forEach((original, index) => {
|
|
1424
|
+
const item = localizedFinding(original, language)
|
|
1425
|
+
const prefix = `${plugin.name} `
|
|
1426
|
+
const message = item.message.startsWith(prefix) ? item.message.slice(prefix.length) : item.message
|
|
1427
|
+
lines.push(` ${String(index + 1)}. [${item.code}] ${message}`)
|
|
1428
|
+
if (item.evidence !== undefined) appendReportField(
|
|
1429
|
+
lines, zh ? '证据' : 'Evidence', item.evidence, ' ',
|
|
1430
|
+
)
|
|
1431
|
+
})
|
|
1432
|
+
const suggestionGroups = new Map()
|
|
1433
|
+
for (const original of originals) {
|
|
1434
|
+
const suggestion = localizedFinding(original, language).suggestion
|
|
1435
|
+
if (suggestion === undefined) continue
|
|
1436
|
+
const key = original.repair?.id ?? `suggestion:${suggestion}`
|
|
1437
|
+
if (!suggestionGroups.has(key)) suggestionGroups.set(key, suggestion)
|
|
1438
|
+
}
|
|
1439
|
+
const suggestions = [...suggestionGroups.values()]
|
|
1440
|
+
if (suggestions.length > 0) {
|
|
1441
|
+
lines.push(` ${zh ? '处理建议' : 'Recommended actions'}:`)
|
|
1442
|
+
suggestions.forEach(item => lines.push(` - ${item}`))
|
|
1443
|
+
}
|
|
1444
|
+
const commands = uniqueStrings(originals
|
|
1445
|
+
.filter(item => item.repair?.kind === 'command')
|
|
1446
|
+
.map(item => item.repair.command.map(quote).join(' ')))
|
|
1447
|
+
if (commands.length > 0) {
|
|
1448
|
+
lines.push(` ${zh ? '可执行命令' : 'Available commands'}:`)
|
|
1449
|
+
commands.forEach(command => lines.push(` $ ${command}`))
|
|
1450
|
+
}
|
|
1451
|
+
lines.push('')
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
if (unknownPackages.length > 0) {
|
|
1456
|
+
lines.push(zh
|
|
1457
|
+
? `兼容性未确认(${String(unknownPackages.length)} 个插件)`
|
|
1458
|
+
: `Compatibility unknown (${String(unknownPackages.length)} plugin(s))`)
|
|
1459
|
+
for (const plugin of unknownPackages) {
|
|
1460
|
+
lines.push(`[${zh ? '未知' : 'UNKNOWN'}] ${plugin.name}${plugin.version ? ` ${plugin.version}` : ''}`)
|
|
1461
|
+
appendReportField(lines, zh ? '原因' : 'Reason', zh
|
|
1462
|
+
? '插件没有声明当前 DSH 的兼容范围。'
|
|
1463
|
+
: 'The plugin does not declare a compatibility range for the active DSH.')
|
|
1464
|
+
appendReportField(lines, zh ? '建议' : 'Action', zh
|
|
1465
|
+
? '升级 DSH 后请关注该插件的发布说明或向插件作者确认。'
|
|
1466
|
+
: 'After a DSH upgrade, review the plugin release notes or ask its author to confirm compatibility.')
|
|
1467
|
+
}
|
|
1468
|
+
lines.push('')
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
const stale = environmentFindings.filter(item => item.code === 'STALE_PROFILE_HARNESS_PACKAGE')
|
|
1472
|
+
const otherEnvironment = environmentFindings.filter(item => item.code !== 'STALE_PROFILE_HARNESS_PACKAGE')
|
|
1473
|
+
if (stale.length > 0 || otherEnvironment.length > 0) {
|
|
1474
|
+
lines.push(zh ? 'DSH 环境问题' : 'DSH environment problems')
|
|
1475
|
+
lines.push('')
|
|
1476
|
+
}
|
|
1477
|
+
if (stale.length > 0) {
|
|
1478
|
+
lines.push(paint('warning', zh
|
|
1479
|
+
? `[警告] [STALE_PROFILE_HARNESS_PACKAGE ×${String(stale.length)}] 检测到当前 DSH 已不再包含的 profile 残留包。`
|
|
1480
|
+
: `[WARN] [STALE_PROFILE_HARNESS_PACKAGE ×${String(stale.length)}] Profile packages remain that the active DSH no longer includes.`))
|
|
1481
|
+
lines.push(` ${zh ? '残留包' : 'Stale packages'}:`)
|
|
1482
|
+
for (const original of stale) {
|
|
1483
|
+
const item = localizedFinding(original, language)
|
|
1484
|
+
lines.push(` - ${item.package ?? (zh ? '未知包' : 'unknown package')}`)
|
|
1485
|
+
if (item.evidence !== undefined) appendReportField(
|
|
1486
|
+
lines, zh ? '位置' : 'Location', item.evidence, ' ',
|
|
1487
|
+
)
|
|
1488
|
+
}
|
|
1489
|
+
const suggestions = uniqueStrings(stale.map(item => localizedFinding(item, language).suggestion))
|
|
1490
|
+
suggestions.forEach(item => appendReportField(lines, zh ? '处理建议' : 'Recommended action', item))
|
|
1491
|
+
lines.push('')
|
|
1492
|
+
}
|
|
1493
|
+
for (const original of otherEnvironment) {
|
|
963
1494
|
const item = localizedFinding(original, language)
|
|
964
1495
|
const label = zh
|
|
965
1496
|
? item.severity === 'error' ? '错误' : item.severity === 'warning' ? '警告' : '信息'
|
|
966
|
-
: item.severity === 'error' ? 'ERROR' : item.severity === 'warning' ? 'WARN
|
|
1497
|
+
: item.severity === 'error' ? 'ERROR' : item.severity === 'warning' ? 'WARN' : 'INFO'
|
|
967
1498
|
lines.push(paint(item.severity, `${label} [${item.code}] ${item.message}`))
|
|
968
|
-
if (item.package !== undefined) lines
|
|
969
|
-
if (item.evidence !== undefined) lines
|
|
970
|
-
if (item.suggestion !== undefined) lines
|
|
971
|
-
if (item.repair?.kind === 'command')
|
|
1499
|
+
if (item.package !== undefined) appendReportField(lines, zh ? '包' : 'Package', item.package)
|
|
1500
|
+
if (item.evidence !== undefined) appendReportField(lines, zh ? '证据' : 'Evidence', item.evidence)
|
|
1501
|
+
if (item.suggestion !== undefined) appendReportField(lines, zh ? '处理建议' : 'Recommended action', item.suggestion)
|
|
1502
|
+
if (item.repair?.kind === 'command') appendReportField(
|
|
1503
|
+
lines, zh ? '可执行命令' : 'Available command', `$ ${item.repair.command.map(quote).join(' ')}`,
|
|
1504
|
+
)
|
|
972
1505
|
lines.push('')
|
|
973
1506
|
}
|
|
974
1507
|
}
|
|
@@ -976,7 +1509,11 @@ export function formatReport(report, options = {}) {
|
|
|
976
1509
|
? `汇总:${String(report.summary.errors)} 个错误,${String(report.summary.warnings)} 个警告`
|
|
977
1510
|
: `Summary: ${String(report.summary.errors)} error(s), ${String(report.summary.warnings)} warning(s)`)
|
|
978
1511
|
if (report.summary.errors > 0) lines.push(zh
|
|
979
|
-
? '
|
|
980
|
-
: '
|
|
1512
|
+
? 'DSH 可能无法启动。请优先更新或停用产生错误的插件。'
|
|
1513
|
+
: 'DSH may fail to start. Upgrade or disable the error-producing plugin first.')
|
|
1514
|
+
lines.push('')
|
|
1515
|
+
lines.push(zh
|
|
1516
|
+
? '本诊断结果由 @bruc3van/dsh-doctor 生成,仅供参考。欢迎在 GitHub Star 或反馈问题:https://github.com/bruc3van/dsh-doctor'
|
|
1517
|
+
: 'This diagnostic report was generated by @bruc3van/dsh-doctor for reference only. Star the project or share feedback on GitHub: https://github.com/bruc3van/dsh-doctor')
|
|
981
1518
|
return `${lines.join('\n')}\n`
|
|
982
1519
|
}
|
package/src/i18n.mjs
CHANGED
|
@@ -59,7 +59,7 @@ const ZH_MESSAGES = {
|
|
|
59
59
|
PACKAGE_NAME_MISMATCH: item => `${item.package ?? '依赖'}解析到了名称不匹配的包。`,
|
|
60
60
|
INVALID_HARNESS_ROOT: () => '指定的 Harness 根目录不是有效的源码工作区。',
|
|
61
61
|
INVALID_WORKSPACE_MANIFEST: () => '已忽略一个无效的 Harness workspace 包清单。',
|
|
62
|
-
HARNESS_INSTALLATION_UNKNOWN: () => '无法定位这个 DSH Home
|
|
62
|
+
HARNESS_INSTALLATION_UNKNOWN: () => '无法定位这个 DSH Home 当前使用的 DSH 安装。',
|
|
63
63
|
INVALID_DEPENDENCY_MAP: item => `${captured(item.message, /^(\S+) must be/) ?? '依赖字段'}必须是“包名到版本范围”的对象。`,
|
|
64
64
|
INVALID_CLIENT_DECLARATION: item => `${item.package} 的 dsh.client 声明无效。`,
|
|
65
65
|
INVALID_CLIENT_PLATFORM: item => `${item.package} 的 dsh.client.platform 必须是字符串。`,
|
|
@@ -71,20 +71,28 @@ const ZH_MESSAGES = {
|
|
|
71
71
|
CLIENT_BUNDLE_UNREADABLE: item => `无法读取 ${item.package} 的客户端 bundle。`,
|
|
72
72
|
UNDECLARED_CLIENT_REQUIRE: item => `${item.package} 引用了 ${captured(item.message, / requires (.+) but does not/) ?? '未声明模块'},但没有在 dsh.client.external 中声明。`,
|
|
73
73
|
REDUNDANT_CLIENT_EXTERNAL: item => `${item.package} 把平台模块 ${captured(item.message, / module (.+) as an external/) ?? ''} 重复声明为 external。`,
|
|
74
|
-
CLIENT_EXTERNAL_WITHOUT_SUPPLIER: item => `${item.package} 请求了 ${captured(item.message, / requests (.+), but/) ?? '客户端模块'}
|
|
75
|
-
REMOVED_CLIENT_INJECT: item => `${item.package} 注入了 ${captured(item.message, / injects (.+), which/) ?? '已移除的模块'}
|
|
76
|
-
LEGACY_HARNESS_PEERS: item => `${item.package}
|
|
74
|
+
CLIENT_EXTERNAL_WITHOUT_SUPPLIER: item => `${item.package} 请求了 ${captured(item.message, / requests (.+), but/) ?? '客户端模块'},但当前使用的 DSH 没有对应的模块提供方。`,
|
|
75
|
+
REMOVED_CLIENT_INJECT: item => `${item.package} 注入了 ${captured(item.message, / injects (.+), which/) ?? '已移除的模块'},但当前使用的 DSH 中已不存在该模块。`,
|
|
76
|
+
LEGACY_HARNESS_PEERS: item => `${item.package} 声明依赖当前使用的 DSH 已移除的旧接口包。`,
|
|
77
|
+
LEGACY_HARNESS_DEPENDENCIES: item => `${item.package} 仍依赖当前使用的 DSH 中已不存在的旧包。`,
|
|
77
78
|
BUNDLE_NOT_INSTALLED: item => `配置中的 bundle ${item.package} 尚未安装。`,
|
|
78
79
|
BUNDLE_DECLARATION_MISSING: item => `${item.package} 被列为 profile bundle,但没有声明 dsh.bundle.patch。`,
|
|
79
80
|
BUNDLE_PATCH_MISSING: item => `${item.package} 的 bundle patch 文件缺失。`,
|
|
80
81
|
INVALID_PATCH_YAML: item => `${captured(item.message, /^(.+) cannot be parsed\.$/) ?? 'Patch 文件'}无法解析。`,
|
|
81
82
|
INVALID_PATCH_LIST: item => `${captured(item.message, /^(.+) must be/) ?? 'Patch 文件'}的顶层必须是由映射组成的 YAML 数组。`,
|
|
83
|
+
INVALID_PATCH_ID: () => 'Patch 条目的 id 必须是字符串。',
|
|
84
|
+
INVALID_PATCH_NAME: () => 'Patch 条目的 name 断言必须是字符串。',
|
|
85
|
+
INVALID_PATCH_INSERT: () => 'Patch 条目的 insert 必须是由映射组成的数组。',
|
|
86
|
+
PATCH_ID_REQUIRED: () => '非 insert patch 缺少目标 id。',
|
|
87
|
+
PATCH_TARGET_NOT_FOUND: item => `Patch 指向了不存在的配置行:${captured(item.message, / row ([^.]+)\.$/) ?? '未知'}。`,
|
|
88
|
+
PATCH_TARGET_NOT_GROUP: item => `Patch 尝试向非 group 配置行 ${captured(item.message, / row ([^,]+),/) ?? '未知'} 插入内容。`,
|
|
89
|
+
PATCH_NAME_MISMATCH: () => 'Patch 的 name 断言与目标配置行不一致。',
|
|
82
90
|
INVALID_SETTINGS_DOCUMENT: () => 'Harness 设置文件无法解析。',
|
|
83
91
|
INVALID_SETTINGS_ROOT: () => 'Harness 设置文件顶层必须是命名空间映射。',
|
|
84
92
|
INVALID_CREDENTIALS_DOCUMENT: () => 'Harness 凭据文件无法解析。',
|
|
85
93
|
INVALID_CREDENTIALS_ROOT: () => 'Harness 凭据文件顶层必须是映射。',
|
|
86
94
|
INVALID_CREDENTIALS_LAYOUT: () => 'Harness 凭据文件不是受支持的 version 1 结构。',
|
|
87
|
-
HARNESS_PEER_VERSION_MISMATCH: item => `${item.package}
|
|
95
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `${item.package} 声明的兼容范围不包含当前使用的 DSH 版本。`,
|
|
88
96
|
INVALID_PROFILE_NAME: item => `Profile 名称无效:${captured(item.message, /^Invalid profile name (.+)\.$/) ?? ''}`,
|
|
89
97
|
PROFILE_NOT_FOUND: item => `Profile ${captured(item.message, /^Profile (.+) does not exist\.$/) ?? ''} 不存在。`,
|
|
90
98
|
INVALID_DSH_CONFIGURATION: () => 'dsh 字段存在时必须是对象。',
|
|
@@ -94,6 +102,20 @@ const ZH_MESSAGES = {
|
|
|
94
102
|
DEPENDENCY_NOT_INSTALLED: item => `Profile 依赖 ${item.package} 尚未安装。`,
|
|
95
103
|
PROFILE_DEPENDENCY_VERSION_MISMATCH: item => `${item.package} 的声明版本范围与当前安装版本不兼容。`,
|
|
96
104
|
INSTALLED_BUNDLE_INACTIVE: item => `${item.package} 已作为 bundle 安装,但不在 dsh.profile.bundles 中。`,
|
|
105
|
+
INVALID_PROFILE_DEPENDENCY_RANGE: item => `Profile 依赖 ${item.package} 的语义版本范围无效。`,
|
|
106
|
+
INVALID_HARNESS_PEER_RANGE: item => `${item.package} 声明了无效的 Harness peer 版本范围。`,
|
|
107
|
+
INVALID_NODE_ENGINE_RANGE: item => `${item.package} 声明了无效的 Node.js engines 范围。`,
|
|
108
|
+
PLUGIN_NODE_VERSION_MISMATCH: item => `${item.package} 不支持当前 DSH CLI 使用的 Node.js 版本。`,
|
|
109
|
+
INVALID_PNPM_LOCKFILE: () => 'Profile 的 pnpm lockfile 无法解析。',
|
|
110
|
+
PNPM_LOCKFILE_IMPORTER_MISSING: () => 'Profile 的 pnpm lockfile 缺少可用的根 dependencies 映射。',
|
|
111
|
+
LOCKFILE_DEPENDENCY_MISSING: item => `Profile 依赖 ${item.package} 未出现在 pnpm lockfile importer 中。`,
|
|
112
|
+
LOCKFILE_SPECIFIER_MISMATCH: item => `${item.package} 在 package.json 与 pnpm lockfile 中的声明不一致。`,
|
|
113
|
+
LOCKFILE_INSTALLED_VERSION_MISMATCH: item => `${item.package} 的实际安装版本与 lockfile 不一致。`,
|
|
114
|
+
LOCKFILE_DEPENDENCY_STALE: item => `pnpm lockfile 仍包含未声明的依赖 ${item.package}。`,
|
|
115
|
+
DSH_CLI_HARNESS_VERSION_MISMATCH: () => '当前 DSH CLI 与诊断到的 Harness 版本不一致。',
|
|
116
|
+
DUPLICATE_HARNESS_PACKAGE_VERSION: item => `${item.package} 在 profile 与共享 DSH 安装中存在不同版本。`,
|
|
117
|
+
STALE_PROFILE_HARNESS_PACKAGE: item => `${item.package} 残留在 profile 中,但当前使用的 DSH 已不再包含它。`,
|
|
118
|
+
PROFILE_HARNESS_SCOPE_UNREADABLE: () => 'Profile 内的 @deepseek-ai 包作用域无法作为目录读取。',
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
const ZH_SUGGESTIONS = {
|
|
@@ -115,17 +137,25 @@ const ZH_SUGGESTIONS = {
|
|
|
115
137
|
CLIENT_EXTERNAL_WITHOUT_SUPPLIER: update,
|
|
116
138
|
REMOVED_CLIENT_INJECT: update,
|
|
117
139
|
REDUNDANT_CLIENT_EXTERNAL: () => '插件作者应删除重复的 dsh.client.external 条目。',
|
|
118
|
-
LEGACY_HARNESS_PEERS: () => '
|
|
140
|
+
LEGACY_HARNESS_PEERS: () => '该插件存在兼容风险,请更新到支持当前 DSH 的版本。',
|
|
141
|
+
LEGACY_HARNESS_DEPENDENCIES: () => '请更新该插件;它依赖的 DSH API 可能与当前版本不兼容。',
|
|
119
142
|
BUNDLE_NOT_INSTALLED: () => '使用当前 DSH 安装补齐 profile 依赖、升级 bundle,或从 profile 中移除它。',
|
|
120
143
|
BUNDLE_DECLARATION_MISSING: () => '升级该 bundle,或把它从 dsh.profile.bundles 中移除。',
|
|
121
144
|
BUNDLE_PATCH_MISSING: () => '重新安装或升级该 bundle,或者从 profile 中移除它。',
|
|
122
145
|
INVALID_PATCH_YAML: () => '启动该 profile 前,请修复 YAML 语法。',
|
|
123
146
|
INVALID_PATCH_LIST: () => '启动该 profile 前,请修复 patch 顶层结构。',
|
|
147
|
+
INVALID_PATCH_ID: () => '请使用字符串行 id;根级 insert 可以省略 id。',
|
|
148
|
+
INVALID_PATCH_NAME: () => '请使用字符串插件名称断言,或删除 name 字段。',
|
|
149
|
+
INVALID_PATCH_INSERT: () => '启动该 profile 前,请修复 insert 列表。',
|
|
150
|
+
PATCH_ID_REQUIRED: () => '请补充目标行 id,或把该条目改为 insert patch。',
|
|
151
|
+
PATCH_TARGET_NOT_FOUND: () => '确认该 overlay 是否适用于当前 profile,并检查 bundle 顺序。',
|
|
152
|
+
PATCH_TARGET_NOT_GROUP: () => '请选择 group 行,或使用根级 insert。',
|
|
153
|
+
PATCH_NAME_MISMATCH: () => '请更新 name 断言,或改为指向正确的配置行。',
|
|
124
154
|
INVALID_SETTINGS_DOCUMENT: () => '修复设置文件语法;Doctor 不会猜测凭据或模型配置值。',
|
|
125
155
|
INVALID_SETTINGS_ROOT: () => '把顶层标量或数组替换为映射。',
|
|
126
156
|
INVALID_CREDENTIALS_DOCUMENT: () => '只修复报告的结构;Doctor 永远不会输出或重写秘密值。',
|
|
127
157
|
INVALID_CREDENTIALS_LAYOUT: () => '迁移文档结构,不要暴露或修改秘密值。',
|
|
128
|
-
HARNESS_PEER_VERSION_MISMATCH: item => `把 ${item.package}
|
|
158
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `把 ${item.package} 更新到兼容当前使用的 DSH 的版本。`,
|
|
129
159
|
PROFILE_NOT_FOUND: () => '先启动一次该 profile,或用当前 DSH 安装初始化它。',
|
|
130
160
|
INVALID_DSH_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh 配置对象。',
|
|
131
161
|
INVALID_PROFILE_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh.profile 配置对象。',
|
|
@@ -134,6 +164,20 @@ const ZH_SUGGESTIONS = {
|
|
|
134
164
|
DEPENDENCY_NOT_INSTALLED: () => '使用下方精确命令安装该 profile 声明的依赖。',
|
|
135
165
|
PROFILE_DEPENDENCY_VERSION_MISMATCH: () => '使用下方精确命令重新同步该 profile 的安装。',
|
|
136
166
|
INSTALLED_BUNDLE_INACTIVE: () => '重新执行匹配的插件添加或更新操作,或者移除未使用的依赖。',
|
|
167
|
+
INVALID_PROFILE_DEPENDENCY_RANGE: () => '安装或启动 profile 前,请修复依赖版本范围。',
|
|
168
|
+
INVALID_HARNESS_PEER_RANGE: () => '插件作者应发布有效的 peer dependency 版本范围。',
|
|
169
|
+
INVALID_NODE_ENGINE_RANGE: () => '插件作者应发布有效的 engines.node 版本范围。',
|
|
170
|
+
PLUGIN_NODE_VERSION_MISMATCH: item => `更新 ${item.package},或使用该插件支持的 Node.js 版本运行 DSH。`,
|
|
171
|
+
INVALID_PNPM_LOCKFILE: () => '修复或重新生成 lockfile 后,再执行精确的 profile 安装命令。',
|
|
172
|
+
PNPM_LOCKFILE_IMPORTER_MISSING: () => '使用精确的 profile 安装命令重新同步 lockfile。',
|
|
173
|
+
LOCKFILE_DEPENDENCY_MISSING: () => '使用下方精确命令重新同步 manifest 与 lockfile。',
|
|
174
|
+
LOCKFILE_SPECIFIER_MISMATCH: () => '使用下方精确命令重新同步 manifest 与 lockfile。',
|
|
175
|
+
LOCKFILE_INSTALLED_VERSION_MISMATCH: () => '使用下方精确命令恢复 lockfile 锁定的安装。',
|
|
176
|
+
LOCKFILE_DEPENDENCY_STALE: () => '使用下方精确命令移除 lockfile importer 中的残留条目。',
|
|
177
|
+
DSH_CLI_HARNESS_VERSION_MISMATCH: () => '请使用属于同一安装的 DSH CLI 与 Harness 工作区进行诊断。',
|
|
178
|
+
DUPLICATE_HARNESS_PACKAGE_VERSION: () => '使用当前 DSH CLI 重新安装 profile,确保模块解析只使用一个兼容版本。',
|
|
179
|
+
STALE_PROFILE_HARNESS_PACKAGE: () => '使用当前 DSH CLI 重新安装 profile,并检查仍依赖该旧包的插件。',
|
|
180
|
+
PROFILE_HARNESS_SCOPE_UNREADABLE: () => '请使用当前 DSH CLI 重新安装 profile,以修复 node_modules 布局。',
|
|
137
181
|
}
|
|
138
182
|
|
|
139
183
|
function update(item) {
|
|
@@ -144,9 +188,21 @@ export function localizedFinding(item, language) {
|
|
|
144
188
|
if (language !== 'zh') return item
|
|
145
189
|
const message = ZH_MESSAGES[item.code]?.(item) ?? item.message
|
|
146
190
|
const suggestion = item.suggestion === undefined ? undefined : (ZH_SUGGESTIONS[item.code]?.(item) ?? item.suggestion)
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
191
|
+
const peerGroups = item.details?.peerVersionGroups
|
|
192
|
+
const evidence = Array.isArray(peerGroups)
|
|
193
|
+
? peerGroups.flatMap((group, index) => [
|
|
194
|
+
...(peerGroups.length > 1 ? [`第 ${String(index + 1)} 组:`] : []),
|
|
195
|
+
`${peerGroups.length > 1 ? ' ' : ''}插件要求:${group.required}`,
|
|
196
|
+
`${peerGroups.length > 1 ? ' ' : ''}当前 DSH:${group.active}`,
|
|
197
|
+
`${peerGroups.length > 1 ? ' ' : ''}涉及 ${String(group.packages.length)} 个包:${group.packages.join('、')}`,
|
|
198
|
+
]).join('\n')
|
|
199
|
+
: typeof item.evidence === 'string'
|
|
200
|
+
? (item.code === 'LEGACY_HARNESS_PEERS' || item.code === 'LEGACY_HARNESS_DEPENDENCIES'
|
|
201
|
+
? item.evidence.replaceAll(', ', '、')
|
|
202
|
+
: item.evidence)
|
|
203
|
+
.replaceAll('(active ', '(当前 ')
|
|
204
|
+
.replace(/ at line (\d+), column (\d+)/g, ',第 $1 行第 $2 列')
|
|
205
|
+
: item.evidence
|
|
150
206
|
return { ...item, message, suggestion, evidence }
|
|
151
207
|
}
|
|
152
208
|
|
package/src/repair.mjs
CHANGED
|
@@ -82,26 +82,42 @@ function applyJsonEdit(action) {
|
|
|
82
82
|
return { id: action.id, status: 'applied', backup }
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function
|
|
85
|
+
function limitedOutput(value, limit = 8192) {
|
|
86
|
+
if (typeof value !== 'string' || value.length === 0) return undefined
|
|
87
|
+
return value.length <= limit ? value : `${value.slice(0, limit)}\n... output truncated by DSH Doctor ...`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function applyCommand(action, options) {
|
|
86
91
|
const [command, ...args] = action.command
|
|
92
|
+
const captureOutput = options.captureOutput === true
|
|
87
93
|
const result = crossSpawn.sync(command, args, {
|
|
88
|
-
stdio: 'inherit',
|
|
94
|
+
stdio: captureOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
95
|
+
...captureOutput ? { encoding: 'utf8' } : {},
|
|
89
96
|
env: action.env === undefined ? process.env : { ...process.env, ...action.env },
|
|
90
97
|
})
|
|
91
98
|
if (result.error != null) throw result.error
|
|
92
99
|
if (result.status !== 0) {
|
|
93
|
-
|
|
100
|
+
const reason = result.signal === null
|
|
94
101
|
? `${command} exited with status ${String(result.status)}`
|
|
95
|
-
: `${command} was terminated by signal ${result.signal}`
|
|
102
|
+
: `${command} was terminated by signal ${result.signal}`
|
|
103
|
+
const details = captureOutput ? limitedOutput(result.stderr) ?? limitedOutput(result.stdout) : undefined
|
|
104
|
+
throw new Error(details === undefined ? reason : `${reason}: ${details.trimEnd()}`)
|
|
105
|
+
}
|
|
106
|
+
const stdout = captureOutput ? limitedOutput(result.stdout) : undefined
|
|
107
|
+
const stderr = captureOutput ? limitedOutput(result.stderr) : undefined
|
|
108
|
+
return {
|
|
109
|
+
id: action.id,
|
|
110
|
+
status: 'applied',
|
|
111
|
+
...(stdout === undefined ? {} : { stdout }),
|
|
112
|
+
...(stderr === undefined ? {} : { stderr }),
|
|
96
113
|
}
|
|
97
|
-
return { id: action.id, status: 'applied' }
|
|
98
114
|
}
|
|
99
115
|
|
|
100
|
-
export function applyRepairs(actions) {
|
|
116
|
+
export function applyRepairs(actions, options = {}) {
|
|
101
117
|
const results = []
|
|
102
118
|
for (const action of actions) {
|
|
103
119
|
try {
|
|
104
|
-
results.push(action.kind === 'json-edit' ? applyJsonEdit(action) : applyCommand(action))
|
|
120
|
+
results.push(action.kind === 'json-edit' ? applyJsonEdit(action) : applyCommand(action, options))
|
|
105
121
|
} catch (error) {
|
|
106
122
|
results.push({
|
|
107
123
|
id: action.id,
|