@bruc3van/dsh-doctor 0.1.3 → 0.1.5
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 +8 -5
- package/README.md +8 -5
- package/package.json +2 -2
- package/src/cli.mjs +32 -24
- package/src/doctor.mjs +243 -37
- package/src/i18n.mjs +24 -11
- package/src/repair.mjs +23 -2
package/README.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[中文](README.md) | English
|
|
4
4
|
|
|
5
|
-
DSH Doctor
|
|
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
6
|
|
|
7
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
8
|
|
|
@@ -23,7 +23,7 @@ npx @bruc3van/dsh-doctor
|
|
|
23
23
|
|
|
24
24
|
By default, Doctor checks `$DSH_HOME/profiles/web`. If `DSH_HOME` is unset, it uses `~/.dsh`.
|
|
25
25
|
|
|
26
|
-
Doctor does not require `dsh` to be installed as a global command. It searches
|
|
26
|
+
Doctor does not require `dsh` to be installed as a global command. It searches, in order, an explicit `--dsh-command` or `DSH_DOCTOR_DSH_COMMAND`, the CLI under an explicit `--harness-root`, the shared profile installation or links left by the npx cache, the current project, PATH, and finally an automatically detected Harness source checkout. 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
27
|
|
|
28
28
|
## How it works
|
|
29
29
|
|
|
@@ -80,7 +80,7 @@ After DSH is updated, Doctor assigns every direct profile plugin one explicit st
|
|
|
80
80
|
|
|
81
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
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
|
|
83
|
+
- `unknown`: The plugin does not declare a Harness compatibility range through `peerDependencies`, or the active package version for a declared peer cannot be resolved. Doctor cannot prove it supports the upgraded DSH, but does not report uncertainty as a failure.
|
|
84
84
|
- `compatible`: The declared compatibility ranges accept the active Harness and no plugin-related errors or warnings were found.
|
|
85
85
|
|
|
86
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`.
|
|
@@ -110,6 +110,7 @@ Every executable repair has a stable ID, risk level, description, and exact targ
|
|
|
110
110
|
- External commands use fixed argument arrays and never construct shell commands.
|
|
111
111
|
- `--json --fix --yes` captures subprocess output in the repair result so stdout remains exactly one valid JSON document.
|
|
112
112
|
- Command repairs bind the diagnosed `DSH_HOME` and show the resolved CLI path instead of assuming `dsh` exists on PATH.
|
|
113
|
+
- Each command repair has a 10-minute limit; a timeout terminates that action and marks subsequent actions as skipped.
|
|
113
114
|
- A failed repair stops later actions and preserves backups already created.
|
|
114
115
|
- Doctor runs every diagnostic again after repairs and uses the final state for its exit code.
|
|
115
116
|
|
|
@@ -125,7 +126,7 @@ The initial release automatically performs only deterministic operations, such a
|
|
|
125
126
|
|
|
126
127
|
- Static scanning recognizes only literal `require("package")` calls. Dynamic dependencies require a future bundle metadata contract.
|
|
127
128
|
- 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
|
+
- Version compatibility is based on plugin `peerDependencies` and resolvable active Harness package versions. A plugin without a declared range, or whose corresponding active version cannot be resolved, can receive only structural checks and an `unknown` compatibility state.
|
|
129
130
|
- Lockfile checks deterministically cross-check the direct profile importer only; they do not recursively scan the complete npm dependency graph.
|
|
130
131
|
- 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
|
|
|
@@ -137,4 +138,6 @@ npm run check
|
|
|
137
138
|
node src/cli.mjs --help
|
|
138
139
|
```
|
|
139
140
|
|
|
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
|
+
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.
|
|
142
|
+
|
|
143
|
+
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
中文 | [English](README.en.md)
|
|
4
4
|
|
|
5
|
-
DSH Doctor
|
|
5
|
+
DSH Doctor 面向 DSH 与插件使用者,帮助快速找出导致 DSH 启动异常或升级后不可用的插件,集中说明每个插件的问题、影响与处理方式,并检查常见的 profile 配置和版本漂移。诊断默认完全只读;只有显式使用 `--fix`、核对并确认精确的修复计划后才会执行,文件修改会先创建备份。
|
|
6
6
|
|
|
7
7
|
这是社区维护的第三方工具,不属于 DeepSeek 官方项目。它不会加载或执行待检查插件的代码。
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ npx @bruc3van/dsh-doctor
|
|
|
23
23
|
|
|
24
24
|
默认检查 `$DSH_HOME/profiles/web`;未设置 `DSH_HOME` 时使用 `~/.dsh`。
|
|
25
25
|
|
|
26
|
-
Doctor 不要求 `dsh` 必须是全局命令。它会按顺序查找
|
|
26
|
+
Doctor 不要求 `dsh` 必须是全局命令。它会按顺序查找 `--dsh-command` 或 `DSH_DOCTOR_DSH_COMMAND` 指定的命令、显式 `--harness-root` 中的 CLI、profile 共享安装或 npx 缓存留下的链接、当前项目安装、PATH,最后是自动识别出的 Harness 源码工作区。DSH Desktop 内置运行时或其他特殊安装可以通过 `--dsh-command /path/to/dsh`(也接受官方包的 `lib/bin.js`)明确指定。找不到 CLI 时仍会完成只读诊断,但不会提供或执行无法验证的命令型修复。
|
|
27
27
|
|
|
28
28
|
## 工作方式
|
|
29
29
|
|
|
@@ -59,7 +59,7 @@ DSH 更新后,Doctor 会把每个 profile 插件归入一个明确状态,并
|
|
|
59
59
|
|
|
60
60
|
- `incompatible`:已经发现会阻断插件加载或 Harness 启动的错误,例如插件未安装,或注入了已删除的 client runtime。
|
|
61
61
|
- `risk`:发现当前版本风险,例如 Harness peer range 不接受新版本、仍依赖已删除的 DSH 包、Node.js 不兼容,或安装版本发生漂移。
|
|
62
|
-
- `unknown`:插件没有通过 `peerDependencies` 声明 Harness
|
|
62
|
+
- `unknown`:插件没有通过 `peerDependencies` 声明 Harness 兼容范围,或声明对应的当前 package 版本无法解析;Doctor 无法证明它支持升级后的 DSH,但不会把未知误报成故障。
|
|
63
63
|
- `compatible`:插件声明的兼容范围接受当前 Harness,且没有发现插件相关错误或警告。
|
|
64
64
|
|
|
65
65
|
兼容性检查覆盖所有 profile 直接插件,不再只检查带 `dsh.client` 的前端插件;纯 bundle 或服务端插件引用旧 Harness API 也会被报告。建议 DSH 升级后先运行一次 `dsh-doctor`,再根据精确的 update 建议决定是否执行 `dsh-doctor --fix`。
|
|
@@ -110,6 +110,7 @@ dsh-doctor --fix --yes --json
|
|
|
110
110
|
- 外部命令使用固定 argv 调用,不拼接 shell 命令。
|
|
111
111
|
- `--json --fix --yes` 会捕获子命令输出并放入修复结果,保证 stdout 始终只有一个合法 JSON 文档。
|
|
112
112
|
- 命令修复绑定当前诊断的 `DSH_HOME`,并展示解析出的真实 CLI 路径;不会假定 PATH 中存在 `dsh`。
|
|
113
|
+
- 单个命令修复最长运行 10 分钟,超时会终止该动作并把后续动作标记为跳过。
|
|
113
114
|
- 任一步失败即停止后续修复,并保留已经创建的备份。
|
|
114
115
|
- 完成后重新运行全部诊断,以最终状态决定退出码。
|
|
115
116
|
|
|
@@ -125,7 +126,7 @@ dsh-doctor --fix --yes --json
|
|
|
125
126
|
|
|
126
127
|
- 静态扫描只识别代码中的字面量 `require("package")`;动态依赖需要未来的 bundle 元数据协议。
|
|
127
128
|
- 配置检查覆盖语法和 Doctor 能稳定对齐的结构,并按当前 Harness patch 算法做无执行组合检查;不会求值 `!!js`,也不会加载第三方插件。
|
|
128
|
-
- 版本兼容以插件 `peerDependencies` 和当前可解析 Harness package
|
|
129
|
+
- 版本兼容以插件 `peerDependencies` 和当前可解析 Harness package 版本为依据;未声明兼容范围或无法解析对应当前版本的插件只能做结构检查。
|
|
129
130
|
- lockfile 检查只对 profile 的直接依赖 importer 做确定性交叉验证,不递归扫描整个 npm 依赖树。
|
|
130
131
|
- 真实启动探针尚未启用;即使复制 `DSH_HOME`,第三方插件仍可能访问网络、绝对路径或启动外部进程,不能宣称无副作用。
|
|
131
132
|
|
|
@@ -137,4 +138,6 @@ npm run check
|
|
|
137
138
|
node src/cli.mjs --help
|
|
138
139
|
```
|
|
139
140
|
|
|
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
|
+
新包需要先由 `@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`。
|
|
142
|
+
|
|
143
|
+
后续发布前,需要在 `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.5",
|
|
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": {
|
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { applyRepairs, formatRepairPlan, repairsFromReport } from './repair.mjs'
|
|
3
|
+
import { formatReport, diagnose, resolveDshHome } from './doctor.mjs'
|
|
4
|
+
import { applyRepairs, formatRepairOutcome, formatRepairPlan, repairsFromReport } from './repair.mjs'
|
|
5
5
|
import { resolveLanguage } from './i18n.mjs'
|
|
6
6
|
import { readFileSync } from 'node:fs'
|
|
7
7
|
import { dirname, resolve } from 'node:path'
|
|
@@ -44,11 +44,6 @@ const HELP_ZH = `用法:dsh-doctor [选项]
|
|
|
44
44
|
|
|
45
45
|
const help = language => language === 'zh' ? HELP_ZH : HELP_EN
|
|
46
46
|
|
|
47
|
-
function fail(message, language = 'en') {
|
|
48
|
-
process.stderr.write(`dsh-doctor: ${message}\n\n${help(language)}`)
|
|
49
|
-
process.exitCode = 2
|
|
50
|
-
}
|
|
51
|
-
|
|
52
47
|
function valueAfter(args, index, name) {
|
|
53
48
|
const value = args[index + 1]
|
|
54
49
|
if (value === undefined || value.startsWith('-')) throw new Error(`${name} needs a value`)
|
|
@@ -75,31 +70,37 @@ function parse(args) {
|
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
async function main() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
const options = parse(process.argv.slice(2))
|
|
74
|
+
if (options.version) {
|
|
75
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
76
|
+
const manifest = JSON.parse(readFileSync(resolve(here, '..', 'package.json'), 'utf8'))
|
|
77
|
+
process.stdout.write(`${manifest.version}\n`)
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
if (options.help) {
|
|
81
|
+
const requested = ['auto', 'zh', 'en'].includes(options.lang) ? options.lang : undefined
|
|
82
|
+
const language = resolveLanguage({
|
|
83
|
+
requested,
|
|
84
|
+
home: resolveDshHome(options.home),
|
|
85
|
+
systemLocale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
86
|
+
})
|
|
87
|
+
process.stdout.write(help(language))
|
|
83
88
|
return
|
|
84
89
|
}
|
|
85
90
|
const language = resolveLanguage({
|
|
86
91
|
requested: options.lang,
|
|
87
|
-
home:
|
|
92
|
+
home: resolveDshHome(options.home),
|
|
88
93
|
systemLocale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
89
94
|
})
|
|
90
95
|
if (options.yes && !options.fix) throw new Error('--yes requires --fix')
|
|
91
96
|
if (options.json && options.fix && !options.yes) throw new Error('--json --fix requires --yes because a prompt would corrupt JSON output')
|
|
92
|
-
|
|
93
|
-
process.stdout.write(help(language))
|
|
94
|
-
} else if (options.version) {
|
|
95
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
96
|
-
const manifest = JSON.parse(readFileSync(resolve(here, '..', 'package.json'), 'utf8'))
|
|
97
|
-
process.stdout.write(`${manifest.version}\n`)
|
|
98
|
-
} else {
|
|
97
|
+
{
|
|
99
98
|
let report = diagnose(options)
|
|
100
99
|
let repairs = []
|
|
100
|
+
let actions = []
|
|
101
|
+
let repairDeclined = false
|
|
101
102
|
if (options.fix) {
|
|
102
|
-
|
|
103
|
+
actions = repairsFromReport(report)
|
|
103
104
|
if (actions.length === 0 && report.context.dshCli?.commandRepairNeeded && !report.context.dshCli.available) {
|
|
104
105
|
throw new Error(language === 'zh'
|
|
105
106
|
? '未找到可用的 DSH CLI,无法执行命令型修复;请使用 --dsh-command 指定当前安装的 dsh 或 lib/bin.js'
|
|
@@ -120,13 +121,20 @@ async function main() {
|
|
|
120
121
|
if (confirmed) {
|
|
121
122
|
repairs = applyRepairs(actions, { captureOutput: options.json })
|
|
122
123
|
if (repairs.every(item => item.status === 'applied')) report = diagnose(options)
|
|
123
|
-
}
|
|
124
|
+
} else repairDeclined = true
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
const output = options.fix ? { ...report, repairs } : report
|
|
128
|
+
const noRepairMessage = options.fix && repairs.length === 0 && !options.json
|
|
129
|
+
? formatRepairOutcome(actions, repairs, {
|
|
130
|
+
declined: repairDeclined,
|
|
131
|
+
findingCount: report.findings.length,
|
|
132
|
+
language,
|
|
133
|
+
})
|
|
134
|
+
: ''
|
|
127
135
|
process.stdout.write(options.json
|
|
128
136
|
? `${JSON.stringify(output, null, 2)}\n`
|
|
129
|
-
: `${formatReport(report, { color: process.stdout.isTTY, language })}${repairs.length > 0 ? `${language === 'zh' ? '修复结果' : 'Repairs'}: ${JSON.stringify(repairs, null, 2)}\n` :
|
|
137
|
+
: `${formatReport(report, { color: process.stdout.isTTY, language })}${repairs.length > 0 ? `${language === 'zh' ? '修复结果' : 'Repairs'}: ${JSON.stringify(repairs, null, 2)}\n` : noRepairMessage}`)
|
|
130
138
|
process.exitCode = repairs.some(item => item.status === 'failed') ? 2 : report.summary.errors > 0 ? 1 : 0
|
|
131
139
|
}
|
|
132
140
|
}
|
|
@@ -138,7 +146,7 @@ try {
|
|
|
138
146
|
if (process.argv.includes('--json')) {
|
|
139
147
|
process.stdout.write(`${JSON.stringify({ version: 1, operationalError: message }, null, 2)}\n`)
|
|
140
148
|
} else {
|
|
141
|
-
process.stderr.write(`dsh-doctor: ${message}\n`)
|
|
149
|
+
process.stderr.write(`dsh-doctor: ${message}\n\n${help('en')}`)
|
|
142
150
|
}
|
|
143
151
|
process.exitCode = 2
|
|
144
152
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -39,6 +39,7 @@ function finding(severity, code, message, options = {}) {
|
|
|
39
39
|
message,
|
|
40
40
|
...options.package === undefined ? {} : { package: options.package },
|
|
41
41
|
...options.evidence === undefined ? {} : { evidence: options.evidence },
|
|
42
|
+
...options.details === undefined ? {} : { details: options.details },
|
|
42
43
|
...options.suggestion === undefined ? {} : { suggestion: options.suggestion },
|
|
43
44
|
...options.repair === undefined ? {} : { repair: options.repair },
|
|
44
45
|
}
|
|
@@ -86,6 +87,12 @@ function packagePathParts(name) {
|
|
|
86
87
|
return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name) ? [name] : undefined
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
function expandUserPath(value) {
|
|
91
|
+
if (value === '~') return homedir()
|
|
92
|
+
if (value.startsWith('~/') || value.startsWith('~\\')) return resolve(homedir(), value.slice(2))
|
|
93
|
+
return value
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
function packageManifestAt(directory, expectedName, findings, source) {
|
|
90
97
|
const file = join(directory, 'package.json')
|
|
91
98
|
if (!existsSync(file)) return undefined
|
|
@@ -173,11 +180,7 @@ function localDshPackage(start) {
|
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
function commandFromValue(value, env, cwd) {
|
|
176
|
-
const expanded = value
|
|
177
|
-
? homedir()
|
|
178
|
-
: value.startsWith('~/') || value.startsWith('~\\')
|
|
179
|
-
? resolve(homedir(), value.slice(2))
|
|
180
|
-
: value
|
|
183
|
+
const expanded = expandUserPath(value)
|
|
181
184
|
const looksLikePath = isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\') || expanded.startsWith('.')
|
|
182
185
|
const file = looksLikePath ? resolve(cwd, expanded) : executableOnPath(expanded, env)
|
|
183
186
|
if (file === undefined) return undefined
|
|
@@ -205,7 +208,13 @@ function resolveDshCli(options, harness, home) {
|
|
|
205
208
|
|
|
206
209
|
const sharedPackage = join(home, 'profiles', 'node_modules', '@deepseek-ai', 'dsh')
|
|
207
210
|
if (existsSync(sharedPackage)) {
|
|
208
|
-
|
|
211
|
+
let canonical
|
|
212
|
+
try {
|
|
213
|
+
canonical = realpathSync(sharedPackage)
|
|
214
|
+
} catch {
|
|
215
|
+
canonical = undefined
|
|
216
|
+
}
|
|
217
|
+
const bin = canonical === undefined ? undefined : manifestBin(canonical)
|
|
209
218
|
if (bin !== undefined) return { command: [process.execPath, bin.bin], path: bin.bin, source: 'profile', version: bin.version }
|
|
210
219
|
}
|
|
211
220
|
|
|
@@ -269,7 +278,7 @@ function indexWorkspace(root, findings) {
|
|
|
269
278
|
|
|
270
279
|
function resolveHarnessContext(home, explicitRoot, findings) {
|
|
271
280
|
if (explicitRoot !== undefined) {
|
|
272
|
-
const root = resolve(explicitRoot)
|
|
281
|
+
const root = resolve(expandUserPath(explicitRoot))
|
|
273
282
|
if (!existsSync(join(root, 'package.json')) || !existsSync(join(root, 'packages'))) {
|
|
274
283
|
findings.push(finding('error', 'INVALID_HARNESS_ROOT', 'The configured Harness root is not a source checkout.', {
|
|
275
284
|
evidence: root,
|
|
@@ -283,7 +292,16 @@ function resolveHarnessContext(home, explicitRoot, findings) {
|
|
|
283
292
|
|
|
284
293
|
const sharedDsh = join(home, 'profiles', 'node_modules', '@deepseek-ai', 'dsh')
|
|
285
294
|
if (existsSync(sharedDsh)) {
|
|
286
|
-
|
|
295
|
+
let canonical
|
|
296
|
+
try {
|
|
297
|
+
canonical = realpathSync(sharedDsh)
|
|
298
|
+
} catch (error) {
|
|
299
|
+
findings.push(finding('warning', 'HARNESS_INSTALLATION_UNKNOWN', 'Could not resolve the DSH installation currently used by this home.', {
|
|
300
|
+
evidence: `${sharedDsh}: ${error instanceof Error ? error.message : String(error)}`,
|
|
301
|
+
suggestion: 'Repair the shared DSH installation or pass --harness-root for a source checkout.',
|
|
302
|
+
}))
|
|
303
|
+
return { root: undefined, packages: new Map(), version: undefined, authoritative: false }
|
|
304
|
+
}
|
|
287
305
|
const root = findHarnessRoot(canonical)
|
|
288
306
|
if (root !== undefined) {
|
|
289
307
|
const manifest = readJson(join(root, 'package.json'), 'Harness root manifest', findings)
|
|
@@ -293,7 +311,7 @@ function resolveHarnessContext(home, explicitRoot, findings) {
|
|
|
293
311
|
return { root: canonical, packages: new Map(), version: manifest?.version, authoritative: false }
|
|
294
312
|
}
|
|
295
313
|
|
|
296
|
-
findings.push(finding('warning', 'HARNESS_INSTALLATION_UNKNOWN', 'Could not locate the
|
|
314
|
+
findings.push(finding('warning', 'HARNESS_INSTALLATION_UNKNOWN', 'Could not locate the DSH installation currently used by this home.', {
|
|
297
315
|
evidence: sharedDsh,
|
|
298
316
|
suggestion: 'Pass --harness-root when diagnosing a source checkout.',
|
|
299
317
|
}))
|
|
@@ -392,7 +410,10 @@ export function extractStaticRequires(source) {
|
|
|
392
410
|
if (!code[match.index]) continue
|
|
393
411
|
const specifier = match[2]
|
|
394
412
|
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('node:') || BUILTIN_MODULES.has(specifier)) continue
|
|
395
|
-
|
|
413
|
+
const packageName = specifier.startsWith('@')
|
|
414
|
+
? specifier.split('/').slice(0, 2).join('/')
|
|
415
|
+
: specifier.split('/')[0]
|
|
416
|
+
if (packagePathParts(packageName) !== undefined) values.add(specifier)
|
|
396
417
|
}
|
|
397
418
|
return [...values].sort()
|
|
398
419
|
}
|
|
@@ -400,6 +421,7 @@ export function extractStaticRequires(source) {
|
|
|
400
421
|
function codePositions(source) {
|
|
401
422
|
const code = new Uint8Array(source.length)
|
|
402
423
|
let state = 'code'
|
|
424
|
+
let regexClass = false
|
|
403
425
|
for (let index = 0; index < source.length; index += 1) {
|
|
404
426
|
const char = source[index]
|
|
405
427
|
const next = source[index + 1]
|
|
@@ -412,6 +434,9 @@ function codePositions(source) {
|
|
|
412
434
|
index += 1
|
|
413
435
|
} else if (char === "'" || char === '"' || char === '`') {
|
|
414
436
|
state = char
|
|
437
|
+
} else if (char === '/' && regexCanStart(source, index)) {
|
|
438
|
+
state = 'regex'
|
|
439
|
+
regexClass = false
|
|
415
440
|
} else {
|
|
416
441
|
code[index] = 1
|
|
417
442
|
}
|
|
@@ -425,6 +450,12 @@ function codePositions(source) {
|
|
|
425
450
|
state = 'code'
|
|
426
451
|
index += 1
|
|
427
452
|
}
|
|
453
|
+
} else if (state === 'regex') {
|
|
454
|
+
if (char === '\\') index += 1
|
|
455
|
+
else if (char === '[') regexClass = true
|
|
456
|
+
else if (char === ']') regexClass = false
|
|
457
|
+
else if (char === '/' && !regexClass) state = 'code'
|
|
458
|
+
else if (char === '\n' || char === '\r') state = 'code'
|
|
428
459
|
} else if (char === '\\') {
|
|
429
460
|
index += 1
|
|
430
461
|
} else if (char === state) {
|
|
@@ -434,6 +465,17 @@ function codePositions(source) {
|
|
|
434
465
|
return code
|
|
435
466
|
}
|
|
436
467
|
|
|
468
|
+
function regexCanStart(source, slashIndex) {
|
|
469
|
+
let index = slashIndex - 1
|
|
470
|
+
while (index >= 0 && /\s/.test(source[index])) index -= 1
|
|
471
|
+
if (index < 0) return true
|
|
472
|
+
if ('([{=,:;!&|?~+-*%^<>'.includes(source[index])) return true
|
|
473
|
+
if (!/[a-zA-Z0-9_$]/.test(source[index])) return false
|
|
474
|
+
const end = index + 1
|
|
475
|
+
while (index >= 0 && /[a-zA-Z0-9_$]/.test(source[index])) index -= 1
|
|
476
|
+
return /^(?:await|case|delete|do|else|in|instanceof|new|of|return|throw|typeof|void|yield)$/.test(source.slice(index + 1, end))
|
|
477
|
+
}
|
|
478
|
+
|
|
437
479
|
function stripClientSuffix(specifier) {
|
|
438
480
|
return specifier.endsWith('/client') ? specifier.slice(0, -'/client'.length) : specifier
|
|
439
481
|
}
|
|
@@ -504,6 +546,7 @@ function inspectClientPackage(record, context) {
|
|
|
504
546
|
package: name,
|
|
505
547
|
evidence: record.file,
|
|
506
548
|
suggestion: disableSuggestion,
|
|
549
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
507
550
|
}))
|
|
508
551
|
return
|
|
509
552
|
}
|
|
@@ -525,6 +568,8 @@ function inspectClientPackage(record, context) {
|
|
|
525
568
|
findings.push(finding('error', 'CLIENT_BUNDLE_UNREADABLE', `${name} client bundle cannot be read.`, {
|
|
526
569
|
package: name,
|
|
527
570
|
evidence: `${file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
571
|
+
suggestion: disableSuggestion,
|
|
572
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
528
573
|
}))
|
|
529
574
|
return
|
|
530
575
|
}
|
|
@@ -556,7 +601,7 @@ function inspectClientPackage(record, context) {
|
|
|
556
601
|
? harnessPackages.get(supplier)
|
|
557
602
|
: harnessPackages.get(supplier) ?? resolvePackage(supplier)
|
|
558
603
|
if (supplied === undefined || supplied.manifest?.dsh?.client === undefined) {
|
|
559
|
-
findings.push(finding('error', 'CLIENT_EXTERNAL_WITHOUT_SUPPLIER', `${name} requests ${specifier}, but the active
|
|
604
|
+
findings.push(finding('error', 'CLIENT_EXTERNAL_WITHOUT_SUPPLIER', `${name} requests ${specifier}, but the active DSH has no client module supplier.`, {
|
|
560
605
|
package: name,
|
|
561
606
|
evidence: record.file,
|
|
562
607
|
suggestion: disableSuggestion,
|
|
@@ -569,7 +614,7 @@ function inspectClientPackage(record, context) {
|
|
|
569
614
|
for (const dependency of inject ?? []) {
|
|
570
615
|
if (!dependency.startsWith('@deepseek-ai/dsh-')) continue
|
|
571
616
|
if (!harnessPackages.has(stripClientSuffix(dependency))) {
|
|
572
|
-
findings.push(finding('error', 'REMOVED_CLIENT_INJECT', `${name} injects ${dependency}, which is absent from the active
|
|
617
|
+
findings.push(finding('error', 'REMOVED_CLIENT_INJECT', `${name} injects ${dependency}, which is absent from the active DSH.`, {
|
|
573
618
|
package: name,
|
|
574
619
|
evidence: record.file,
|
|
575
620
|
suggestion: disableSuggestion,
|
|
@@ -983,7 +1028,7 @@ function inspectProfileHarnessPackages(profileDir, home, harness, findings) {
|
|
|
983
1028
|
}
|
|
984
1029
|
}
|
|
985
1030
|
if (harness.authoritative && !harness.packages.has(name)) {
|
|
986
|
-
findings.push(finding('warning', 'STALE_PROFILE_HARNESS_PACKAGE', `${name} remains in the profile but is absent from the active
|
|
1031
|
+
findings.push(finding('warning', 'STALE_PROFILE_HARNESS_PACKAGE', `${name} remains in the profile but is absent from the active DSH.`, {
|
|
987
1032
|
package: name,
|
|
988
1033
|
evidence: profileManifestFile,
|
|
989
1034
|
suggestion: 'Reinstall the profile with the active DSH CLI and review plugins that still require this package.',
|
|
@@ -1005,7 +1050,7 @@ function inspectCompatibility(record, context) {
|
|
|
1005
1050
|
.filter(name => name.startsWith('@deepseek-ai/dsh-') && !harnessPackages.has(name))
|
|
1006
1051
|
.sort()
|
|
1007
1052
|
if (removedPeers.length > 0) {
|
|
1008
|
-
findings.push(finding('warning', 'LEGACY_HARNESS_PEERS', `${packageName}
|
|
1053
|
+
findings.push(finding('warning', 'LEGACY_HARNESS_PEERS', `${packageName} declares old interface packages that the active DSH has removed.`, {
|
|
1009
1054
|
package: packageName,
|
|
1010
1055
|
evidence: removedPeers.join(', '),
|
|
1011
1056
|
suggestion: 'Update this plugin before relying on it with the current DSH release.',
|
|
@@ -1017,7 +1062,7 @@ function inspectCompatibility(record, context) {
|
|
|
1017
1062
|
.filter(name => name.startsWith('@deepseek-ai/dsh-') && !harnessPackages.has(name))
|
|
1018
1063
|
.sort()
|
|
1019
1064
|
if (removedDependencies.length > 0) {
|
|
1020
|
-
findings.push(finding('warning', 'LEGACY_HARNESS_DEPENDENCIES', `${packageName} depends on Harness packages that no longer exist in the active
|
|
1065
|
+
findings.push(finding('warning', 'LEGACY_HARNESS_DEPENDENCIES', `${packageName} depends on Harness packages that no longer exist in the active DSH.`, {
|
|
1021
1066
|
package: packageName,
|
|
1022
1067
|
evidence: removedDependencies.join(', '),
|
|
1023
1068
|
suggestion: 'Update this plugin; its bundled DSH APIs may be incompatible with the current release.',
|
|
@@ -1026,8 +1071,11 @@ function inspectCompatibility(record, context) {
|
|
|
1026
1071
|
}
|
|
1027
1072
|
}
|
|
1028
1073
|
const mismatches = []
|
|
1074
|
+
let declaredPeers = 0
|
|
1075
|
+
let resolvedPeers = 0
|
|
1029
1076
|
for (const [name, range] of peers) {
|
|
1030
1077
|
if (!name.startsWith('@deepseek-ai/') && name !== 'cordis') continue
|
|
1078
|
+
declaredPeers += 1
|
|
1031
1079
|
if (semver.validRange(range) === null) {
|
|
1032
1080
|
findings.push(finding('warning', 'INVALID_HARNESS_PEER_RANGE', `${packageName} declares an invalid Harness peer range for ${name}.`, {
|
|
1033
1081
|
package: packageName,
|
|
@@ -1040,17 +1088,37 @@ function inspectCompatibility(record, context) {
|
|
|
1040
1088
|
if (supplier === undefined) continue
|
|
1041
1089
|
const version = supplier.manifest?.version
|
|
1042
1090
|
if (typeof version !== 'string' || semver.valid(version) === null) continue
|
|
1091
|
+
resolvedPeers += 1
|
|
1043
1092
|
if (semver.satisfies(version, range, { includePrerelease: true })) continue
|
|
1044
|
-
mismatches.push(
|
|
1093
|
+
mismatches.push({ name, required: range, active: version })
|
|
1045
1094
|
}
|
|
1046
1095
|
if (mismatches.length > 0) {
|
|
1047
|
-
|
|
1096
|
+
const grouped = new Map()
|
|
1097
|
+
for (const mismatch of mismatches) {
|
|
1098
|
+
const key = JSON.stringify([mismatch.required, mismatch.active])
|
|
1099
|
+
const group = grouped.get(key) ?? {
|
|
1100
|
+
required: mismatch.required, active: mismatch.active, packages: [],
|
|
1101
|
+
}
|
|
1102
|
+
group.packages.push(mismatch.name)
|
|
1103
|
+
grouped.set(key, group)
|
|
1104
|
+
}
|
|
1105
|
+
const groups = [...grouped.values()]
|
|
1106
|
+
.map(group => ({ ...group, packages: group.packages.sort() }))
|
|
1107
|
+
const evidence = groups.flatMap((group, index) => [
|
|
1108
|
+
...(groups.length > 1 ? [`Group ${String(index + 1)}:`] : []),
|
|
1109
|
+
`${groups.length > 1 ? ' ' : ''}Plugin requires: ${group.required}`,
|
|
1110
|
+
`${groups.length > 1 ? ' ' : ''}Active DSH: ${group.active}`,
|
|
1111
|
+
`${groups.length > 1 ? ' ' : ''}Affected ${String(group.packages.length)} package(s): ${group.packages.join(', ')}`,
|
|
1112
|
+
]).join('\n')
|
|
1113
|
+
findings.push(finding('warning', 'HARNESS_PEER_VERSION_MISMATCH', `${packageName} declares compatibility ranges that exclude the active DSH version.`, {
|
|
1048
1114
|
package: packageName,
|
|
1049
|
-
evidence
|
|
1050
|
-
|
|
1115
|
+
evidence,
|
|
1116
|
+
details: { peerVersionGroups: groups },
|
|
1117
|
+
suggestion: `Update ${packageName} to a release compatible with the active DSH.`,
|
|
1051
1118
|
repair: updateRepair(profile, packageName, commandRepair),
|
|
1052
1119
|
}))
|
|
1053
1120
|
}
|
|
1121
|
+
return { declaredPeers, resolvedPeers }
|
|
1054
1122
|
}
|
|
1055
1123
|
|
|
1056
1124
|
function hasHarnessCompatibilityDeclaration(record) {
|
|
@@ -1059,28 +1127,31 @@ function hasHarnessCompatibilityDeclaration(record) {
|
|
|
1059
1127
|
.some(name => name.startsWith('@deepseek-ai/') || name === 'cordis')
|
|
1060
1128
|
}
|
|
1061
1129
|
|
|
1062
|
-
function pluginCompatibility(record, findings) {
|
|
1130
|
+
function pluginCompatibility(record, findings, compatibilityCheck) {
|
|
1063
1131
|
const name = record.requestedName ?? record.manifest.name
|
|
1064
1132
|
const related = findings.filter(item => item.package === name)
|
|
1065
1133
|
if (related.some(item => item.severity === 'error')) return 'incompatible'
|
|
1066
1134
|
if (related.some(item => item.severity === 'warning')) return 'risk'
|
|
1067
1135
|
if (!hasHarnessCompatibilityDeclaration(record)) return 'unknown'
|
|
1136
|
+
if (compatibilityCheck.resolvedPeers < compatibilityCheck.declaredPeers) return 'unknown'
|
|
1068
1137
|
return 'compatible'
|
|
1069
1138
|
}
|
|
1070
1139
|
|
|
1071
1140
|
export function defaultDshHome(env = process.env) {
|
|
1072
1141
|
const configured = env.DSH_HOME?.trim()
|
|
1073
1142
|
if (configured !== undefined && configured.length > 0) {
|
|
1074
|
-
|
|
1075
|
-
if (configured.startsWith('~/') || configured.startsWith('~\\')) return resolve(homedir(), configured.slice(2))
|
|
1076
|
-
return resolve(configured)
|
|
1143
|
+
return resolve(expandUserPath(configured))
|
|
1077
1144
|
}
|
|
1078
1145
|
return join(homedir(), '.dsh')
|
|
1079
1146
|
}
|
|
1080
1147
|
|
|
1148
|
+
export function resolveDshHome(value, env = process.env) {
|
|
1149
|
+
return resolve(expandUserPath(value ?? defaultDshHome(env)))
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1081
1152
|
export function diagnose(options = {}) {
|
|
1082
1153
|
const findings = []
|
|
1083
|
-
const home =
|
|
1154
|
+
const home = resolveDshHome(options.home, options.env)
|
|
1084
1155
|
const profile = options.profile ?? 'web'
|
|
1085
1156
|
if (profile === '' || profile === '.' || profile === '..' || profile.includes('/') || profile.includes('\\')) {
|
|
1086
1157
|
findings.push(finding('error', 'INVALID_PROFILE_NAME', `Invalid profile name ${JSON.stringify(profile)}.`))
|
|
@@ -1249,6 +1320,7 @@ export function diagnose(options = {}) {
|
|
|
1249
1320
|
const thirdPartyRecords = dependencyNames
|
|
1250
1321
|
.map(name => records.get(name))
|
|
1251
1322
|
.filter(record => record !== undefined)
|
|
1323
|
+
const compatibilityChecks = new Map()
|
|
1252
1324
|
for (const record of thirdPartyRecords) {
|
|
1253
1325
|
inspectClientPackage(record, {
|
|
1254
1326
|
findings,
|
|
@@ -1258,7 +1330,7 @@ export function diagnose(options = {}) {
|
|
|
1258
1330
|
profile,
|
|
1259
1331
|
resolvePackage,
|
|
1260
1332
|
})
|
|
1261
|
-
inspectCompatibility(record, {
|
|
1333
|
+
const check = inspectCompatibility(record, {
|
|
1262
1334
|
findings,
|
|
1263
1335
|
commandRepair,
|
|
1264
1336
|
profile,
|
|
@@ -1266,6 +1338,7 @@ export function diagnose(options = {}) {
|
|
|
1266
1338
|
harnessPackages: harness.packages,
|
|
1267
1339
|
harnessPackagesAuthoritative: harness.authoritative,
|
|
1268
1340
|
})
|
|
1341
|
+
compatibilityChecks.set(record.requestedName ?? record.manifest.name, check)
|
|
1269
1342
|
inspectPluginNodeEngine(
|
|
1270
1343
|
record,
|
|
1271
1344
|
dshCli?.command?.[0] === process.execPath ? process.version : undefined,
|
|
@@ -1292,7 +1365,11 @@ export function diagnose(options = {}) {
|
|
|
1292
1365
|
installed: true,
|
|
1293
1366
|
client: record.manifest?.dsh?.client !== undefined,
|
|
1294
1367
|
bundle: record.manifest?.dsh?.bundle !== undefined,
|
|
1295
|
-
compatibility: pluginCompatibility(
|
|
1368
|
+
compatibility: pluginCompatibility(
|
|
1369
|
+
record,
|
|
1370
|
+
findings,
|
|
1371
|
+
compatibilityChecks.get(record.requestedName ?? record.manifest.name),
|
|
1372
|
+
),
|
|
1296
1373
|
}
|
|
1297
1374
|
}),
|
|
1298
1375
|
}, findings)
|
|
@@ -1318,6 +1395,20 @@ function finish(context, findings) {
|
|
|
1318
1395
|
return { version: 1, ok: summary.errors === 0, context, summary, findings }
|
|
1319
1396
|
}
|
|
1320
1397
|
|
|
1398
|
+
function appendReportField(lines, label, value, indent = ' ') {
|
|
1399
|
+
const valueLines = String(value).split('\n')
|
|
1400
|
+
if (valueLines.length === 1) {
|
|
1401
|
+
lines.push(`${indent}${label}: ${valueLines[0]}`)
|
|
1402
|
+
return
|
|
1403
|
+
}
|
|
1404
|
+
lines.push(`${indent}${label}:`)
|
|
1405
|
+
lines.push(...valueLines.map(line => `${indent} ${line}`))
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function uniqueStrings(values) {
|
|
1409
|
+
return [...new Set(values.filter(value => typeof value === 'string' && value.length > 0))]
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1321
1412
|
export function formatReport(report, options = {}) {
|
|
1322
1413
|
const color = options.color ?? false
|
|
1323
1414
|
const language = options.language ?? 'en'
|
|
@@ -1336,7 +1427,7 @@ export function formatReport(report, options = {}) {
|
|
|
1336
1427
|
'DSH Doctor',
|
|
1337
1428
|
`${zh ? 'Profile' : 'Profile'}: ${report.context.profile}`,
|
|
1338
1429
|
`${zh ? 'DSH 主目录' : 'Home'}: ${report.context.home}`,
|
|
1339
|
-
|
|
1430
|
+
`${zh ? '当前使用的 DSH' : 'Active DSH'}: ${report.context.harness.version ?? 'unknown'}${report.context.harness.root ? ` (${report.context.harness.root})` : ''}`,
|
|
1340
1431
|
`${zh ? 'DSH CLI' : 'DSH CLI'}: ${cliText}`,
|
|
1341
1432
|
`${zh ? 'Profile 插件' : 'Profile plugins'}: ${String(report.context.packages.length)}`,
|
|
1342
1433
|
zh
|
|
@@ -1345,19 +1436,130 @@ export function formatReport(report, options = {}) {
|
|
|
1345
1436
|
`${zh ? '输出语言' : 'Output language'}: ${languageName(language)}`,
|
|
1346
1437
|
'',
|
|
1347
1438
|
]
|
|
1348
|
-
|
|
1349
|
-
|
|
1439
|
+
const packageNames = new Set(report.context.packages.map(item => item.name))
|
|
1440
|
+
const pluginFindings = new Map()
|
|
1441
|
+
const environmentFindings = []
|
|
1442
|
+
for (const item of report.findings) {
|
|
1443
|
+
if (item.package !== undefined && packageNames.has(item.package)) {
|
|
1444
|
+
const items = pluginFindings.get(item.package) ?? []
|
|
1445
|
+
items.push(item)
|
|
1446
|
+
pluginFindings.set(item.package, items)
|
|
1447
|
+
} else environmentFindings.push(item)
|
|
1448
|
+
}
|
|
1449
|
+
const problemPackages = report.context.packages
|
|
1450
|
+
.filter(item => (item.compatibility === 'incompatible' || item.compatibility === 'risk')
|
|
1451
|
+
&& pluginFindings.has(item.name))
|
|
1452
|
+
.sort((left, right) => {
|
|
1453
|
+
const rank = { incompatible: 0, risk: 1 }
|
|
1454
|
+
return rank[left.compatibility] - rank[right.compatibility] || left.name.localeCompare(right.name)
|
|
1455
|
+
})
|
|
1456
|
+
const unknownPackages = report.context.packages
|
|
1457
|
+
.filter(item => item.compatibility === 'unknown')
|
|
1458
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
1459
|
+
|
|
1460
|
+
if (report.findings.length === 0 && unknownPackages.length === 0) {
|
|
1461
|
+
lines.push(paint('info', zh ? '正常 当前检查范围内未发现问题。' : 'OK No problems found by the current checks.'))
|
|
1350
1462
|
} else {
|
|
1351
|
-
|
|
1463
|
+
if (problemPackages.length > 0) {
|
|
1464
|
+
lines.push(zh
|
|
1465
|
+
? `插件问题(${String(problemPackages.length)} 个)`
|
|
1466
|
+
: `Plugin problems (${String(problemPackages.length)})`)
|
|
1467
|
+
lines.push('')
|
|
1468
|
+
for (const plugin of problemPackages) {
|
|
1469
|
+
const originals = pluginFindings.get(plugin.name)
|
|
1470
|
+
const status = plugin.compatibility === 'incompatible'
|
|
1471
|
+
? zh ? '不兼容' : 'INCOMPATIBLE'
|
|
1472
|
+
: zh ? '有风险' : 'RISK'
|
|
1473
|
+
const severity = plugin.compatibility === 'incompatible' ? 'error' : 'warning'
|
|
1474
|
+
lines.push(paint(severity, `[${status}] ${plugin.name}`))
|
|
1475
|
+
appendReportField(lines, zh ? '版本' : 'Version', plugin.installed === false
|
|
1476
|
+
? zh ? '未安装' : 'not installed'
|
|
1477
|
+
: plugin.version ?? (zh ? '未知' : 'unknown'))
|
|
1478
|
+
lines.push(zh
|
|
1479
|
+
? ` 问题(${String(originals.length)}):`
|
|
1480
|
+
: ` Problems (${String(originals.length)}):`)
|
|
1481
|
+
originals.forEach((original, index) => {
|
|
1482
|
+
const item = localizedFinding(original, language)
|
|
1483
|
+
const prefix = `${plugin.name} `
|
|
1484
|
+
const message = item.message.startsWith(prefix) ? item.message.slice(prefix.length) : item.message
|
|
1485
|
+
lines.push(` ${String(index + 1)}. [${item.code}] ${message}`)
|
|
1486
|
+
if (item.evidence !== undefined) appendReportField(
|
|
1487
|
+
lines, zh ? '证据' : 'Evidence', item.evidence, ' ',
|
|
1488
|
+
)
|
|
1489
|
+
})
|
|
1490
|
+
const suggestionGroups = new Map()
|
|
1491
|
+
for (const original of originals) {
|
|
1492
|
+
const suggestion = localizedFinding(original, language).suggestion
|
|
1493
|
+
if (suggestion === undefined) continue
|
|
1494
|
+
const key = original.repair?.id ?? `suggestion:${suggestion}`
|
|
1495
|
+
if (!suggestionGroups.has(key)) suggestionGroups.set(key, suggestion)
|
|
1496
|
+
}
|
|
1497
|
+
const suggestions = [...suggestionGroups.values()]
|
|
1498
|
+
if (suggestions.length > 0) {
|
|
1499
|
+
lines.push(` ${zh ? '处理建议' : 'Recommended actions'}:`)
|
|
1500
|
+
suggestions.forEach(item => lines.push(` - ${item}`))
|
|
1501
|
+
}
|
|
1502
|
+
const commands = uniqueStrings(originals
|
|
1503
|
+
.filter(item => item.repair?.kind === 'command')
|
|
1504
|
+
.map(item => item.repair.command.map(quote).join(' ')))
|
|
1505
|
+
if (commands.length > 0) {
|
|
1506
|
+
lines.push(` ${zh ? '可执行命令' : 'Available commands'}:`)
|
|
1507
|
+
commands.forEach(command => lines.push(` $ ${command}`))
|
|
1508
|
+
}
|
|
1509
|
+
lines.push('')
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (unknownPackages.length > 0) {
|
|
1514
|
+
lines.push(zh
|
|
1515
|
+
? `兼容性未确认(${String(unknownPackages.length)} 个插件)`
|
|
1516
|
+
: `Compatibility unknown (${String(unknownPackages.length)} plugin(s))`)
|
|
1517
|
+
for (const plugin of unknownPackages) {
|
|
1518
|
+
lines.push(`[${zh ? '未知' : 'UNKNOWN'}] ${plugin.name}${plugin.version ? ` ${plugin.version}` : ''}`)
|
|
1519
|
+
appendReportField(lines, zh ? '原因' : 'Reason', zh
|
|
1520
|
+
? '插件没有声明当前 DSH 的兼容范围,或 Doctor 无法解析声明所对应的当前版本。'
|
|
1521
|
+
: 'The plugin declares no range for the active DSH, or Doctor could not resolve the active version for a declared peer.')
|
|
1522
|
+
appendReportField(lines, zh ? '建议' : 'Action', zh
|
|
1523
|
+
? '升级 DSH 后请关注该插件的发布说明或向插件作者确认。'
|
|
1524
|
+
: 'After a DSH upgrade, review the plugin release notes or ask its author to confirm compatibility.')
|
|
1525
|
+
}
|
|
1526
|
+
lines.push('')
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
const stale = environmentFindings.filter(item => item.code === 'STALE_PROFILE_HARNESS_PACKAGE')
|
|
1530
|
+
const otherEnvironment = environmentFindings.filter(item => item.code !== 'STALE_PROFILE_HARNESS_PACKAGE')
|
|
1531
|
+
if (stale.length > 0 || otherEnvironment.length > 0) {
|
|
1532
|
+
lines.push(zh ? 'DSH 环境问题' : 'DSH environment problems')
|
|
1533
|
+
lines.push('')
|
|
1534
|
+
}
|
|
1535
|
+
if (stale.length > 0) {
|
|
1536
|
+
lines.push(paint('warning', zh
|
|
1537
|
+
? `[警告] [STALE_PROFILE_HARNESS_PACKAGE ×${String(stale.length)}] 检测到当前 DSH 已不再包含的 profile 残留包。`
|
|
1538
|
+
: `[WARN] [STALE_PROFILE_HARNESS_PACKAGE ×${String(stale.length)}] Profile packages remain that the active DSH no longer includes.`))
|
|
1539
|
+
lines.push(` ${zh ? '残留包' : 'Stale packages'}:`)
|
|
1540
|
+
for (const original of stale) {
|
|
1541
|
+
const item = localizedFinding(original, language)
|
|
1542
|
+
lines.push(` - ${item.package ?? (zh ? '未知包' : 'unknown package')}`)
|
|
1543
|
+
if (item.evidence !== undefined) appendReportField(
|
|
1544
|
+
lines, zh ? '位置' : 'Location', item.evidence, ' ',
|
|
1545
|
+
)
|
|
1546
|
+
}
|
|
1547
|
+
const suggestions = uniqueStrings(stale.map(item => localizedFinding(item, language).suggestion))
|
|
1548
|
+
suggestions.forEach(item => appendReportField(lines, zh ? '处理建议' : 'Recommended action', item))
|
|
1549
|
+
lines.push('')
|
|
1550
|
+
}
|
|
1551
|
+
for (const original of otherEnvironment) {
|
|
1352
1552
|
const item = localizedFinding(original, language)
|
|
1353
1553
|
const label = zh
|
|
1354
1554
|
? item.severity === 'error' ? '错误' : item.severity === 'warning' ? '警告' : '信息'
|
|
1355
|
-
: item.severity === 'error' ? 'ERROR' : item.severity === 'warning' ? 'WARN
|
|
1555
|
+
: item.severity === 'error' ? 'ERROR' : item.severity === 'warning' ? 'WARN' : 'INFO'
|
|
1356
1556
|
lines.push(paint(item.severity, `${label} [${item.code}] ${item.message}`))
|
|
1357
|
-
if (item.package !== undefined) lines
|
|
1358
|
-
if (item.evidence !== undefined) lines
|
|
1359
|
-
if (item.suggestion !== undefined) lines
|
|
1360
|
-
if (item.repair?.kind === 'command')
|
|
1557
|
+
if (item.package !== undefined) appendReportField(lines, zh ? '包' : 'Package', item.package)
|
|
1558
|
+
if (item.evidence !== undefined) appendReportField(lines, zh ? '证据' : 'Evidence', item.evidence)
|
|
1559
|
+
if (item.suggestion !== undefined) appendReportField(lines, zh ? '处理建议' : 'Recommended action', item.suggestion)
|
|
1560
|
+
if (item.repair?.kind === 'command') appendReportField(
|
|
1561
|
+
lines, zh ? '可执行命令' : 'Available command', `$ ${item.repair.command.map(quote).join(' ')}`,
|
|
1562
|
+
)
|
|
1361
1563
|
lines.push('')
|
|
1362
1564
|
}
|
|
1363
1565
|
}
|
|
@@ -1365,7 +1567,11 @@ export function formatReport(report, options = {}) {
|
|
|
1365
1567
|
? `汇总:${String(report.summary.errors)} 个错误,${String(report.summary.warnings)} 个警告`
|
|
1366
1568
|
: `Summary: ${String(report.summary.errors)} error(s), ${String(report.summary.warnings)} warning(s)`)
|
|
1367
1569
|
if (report.summary.errors > 0) lines.push(zh
|
|
1368
|
-
? '
|
|
1369
|
-
: '
|
|
1570
|
+
? 'DSH 可能无法启动。请优先更新或停用产生错误的插件。'
|
|
1571
|
+
: 'DSH may fail to start. Upgrade or disable the error-producing plugin first.')
|
|
1572
|
+
lines.push('')
|
|
1573
|
+
lines.push(zh
|
|
1574
|
+
? '本诊断结果由 @bruc3van/dsh-doctor 生成,仅供参考。欢迎在 GitHub Star 或反馈问题:https://github.com/bruc3van/dsh-doctor'
|
|
1575
|
+
: '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')
|
|
1370
1576
|
return `${lines.join('\n')}\n`
|
|
1371
1577
|
}
|
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,10 +71,10 @@ 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}
|
|
77
|
-
LEGACY_HARNESS_DEPENDENCIES: 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 中已不存在的旧包。`,
|
|
78
78
|
BUNDLE_NOT_INSTALLED: item => `配置中的 bundle ${item.package} 尚未安装。`,
|
|
79
79
|
BUNDLE_DECLARATION_MISSING: item => `${item.package} 被列为 profile bundle,但没有声明 dsh.bundle.patch。`,
|
|
80
80
|
BUNDLE_PATCH_MISSING: item => `${item.package} 的 bundle patch 文件缺失。`,
|
|
@@ -92,7 +92,7 @@ const ZH_MESSAGES = {
|
|
|
92
92
|
INVALID_CREDENTIALS_DOCUMENT: () => 'Harness 凭据文件无法解析。',
|
|
93
93
|
INVALID_CREDENTIALS_ROOT: () => 'Harness 凭据文件顶层必须是映射。',
|
|
94
94
|
INVALID_CREDENTIALS_LAYOUT: () => 'Harness 凭据文件不是受支持的 version 1 结构。',
|
|
95
|
-
HARNESS_PEER_VERSION_MISMATCH: item => `${item.package}
|
|
95
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `${item.package} 声明的兼容范围不包含当前使用的 DSH 版本。`,
|
|
96
96
|
INVALID_PROFILE_NAME: item => `Profile 名称无效:${captured(item.message, /^Invalid profile name (.+)\.$/) ?? ''}`,
|
|
97
97
|
PROFILE_NOT_FOUND: item => `Profile ${captured(item.message, /^Profile (.+) does not exist\.$/) ?? ''} 不存在。`,
|
|
98
98
|
INVALID_DSH_CONFIGURATION: () => 'dsh 字段存在时必须是对象。',
|
|
@@ -114,7 +114,7 @@ const ZH_MESSAGES = {
|
|
|
114
114
|
LOCKFILE_DEPENDENCY_STALE: item => `pnpm lockfile 仍包含未声明的依赖 ${item.package}。`,
|
|
115
115
|
DSH_CLI_HARNESS_VERSION_MISMATCH: () => '当前 DSH CLI 与诊断到的 Harness 版本不一致。',
|
|
116
116
|
DUPLICATE_HARNESS_PACKAGE_VERSION: item => `${item.package} 在 profile 与共享 DSH 安装中存在不同版本。`,
|
|
117
|
-
STALE_PROFILE_HARNESS_PACKAGE: item => `${item.package} 残留在 profile
|
|
117
|
+
STALE_PROFILE_HARNESS_PACKAGE: item => `${item.package} 残留在 profile 中,但当前使用的 DSH 已不再包含它。`,
|
|
118
118
|
PROFILE_HARNESS_SCOPE_UNREADABLE: () => 'Profile 内的 @deepseek-ai 包作用域无法作为目录读取。',
|
|
119
119
|
}
|
|
120
120
|
|
|
@@ -133,6 +133,7 @@ const ZH_SUGGESTIONS = {
|
|
|
133
133
|
INVALID_CLIENT_INJECT: update,
|
|
134
134
|
CLIENT_EXPORT_MISSING: update,
|
|
135
135
|
CLIENT_BUNDLE_MISSING: update,
|
|
136
|
+
CLIENT_BUNDLE_UNREADABLE: update,
|
|
136
137
|
UNDECLARED_CLIENT_REQUIRE: update,
|
|
137
138
|
CLIENT_EXTERNAL_WITHOUT_SUPPLIER: update,
|
|
138
139
|
REMOVED_CLIENT_INJECT: update,
|
|
@@ -155,7 +156,7 @@ const ZH_SUGGESTIONS = {
|
|
|
155
156
|
INVALID_SETTINGS_ROOT: () => '把顶层标量或数组替换为映射。',
|
|
156
157
|
INVALID_CREDENTIALS_DOCUMENT: () => '只修复报告的结构;Doctor 永远不会输出或重写秘密值。',
|
|
157
158
|
INVALID_CREDENTIALS_LAYOUT: () => '迁移文档结构,不要暴露或修改秘密值。',
|
|
158
|
-
HARNESS_PEER_VERSION_MISMATCH: item => `把 ${item.package}
|
|
159
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `把 ${item.package} 更新到兼容当前使用的 DSH 的版本。`,
|
|
159
160
|
PROFILE_NOT_FOUND: () => '先启动一次该 profile,或用当前 DSH 安装初始化它。',
|
|
160
161
|
INVALID_DSH_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh 配置对象。',
|
|
161
162
|
INVALID_PROFILE_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh.profile 配置对象。',
|
|
@@ -188,9 +189,21 @@ export function localizedFinding(item, language) {
|
|
|
188
189
|
if (language !== 'zh') return item
|
|
189
190
|
const message = ZH_MESSAGES[item.code]?.(item) ?? item.message
|
|
190
191
|
const suggestion = item.suggestion === undefined ? undefined : (ZH_SUGGESTIONS[item.code]?.(item) ?? item.suggestion)
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
const peerGroups = item.details?.peerVersionGroups
|
|
193
|
+
const evidence = Array.isArray(peerGroups)
|
|
194
|
+
? peerGroups.flatMap((group, index) => [
|
|
195
|
+
...(peerGroups.length > 1 ? [`第 ${String(index + 1)} 组:`] : []),
|
|
196
|
+
`${peerGroups.length > 1 ? ' ' : ''}插件要求:${group.required}`,
|
|
197
|
+
`${peerGroups.length > 1 ? ' ' : ''}当前 DSH:${group.active}`,
|
|
198
|
+
`${peerGroups.length > 1 ? ' ' : ''}涉及 ${String(group.packages.length)} 个包:${group.packages.join('、')}`,
|
|
199
|
+
]).join('\n')
|
|
200
|
+
: typeof item.evidence === 'string'
|
|
201
|
+
? (item.code === 'LEGACY_HARNESS_PEERS' || item.code === 'LEGACY_HARNESS_DEPENDENCIES'
|
|
202
|
+
? item.evidence.replaceAll(', ', '、')
|
|
203
|
+
: item.evidence)
|
|
204
|
+
.replaceAll('(active ', '(当前 ')
|
|
205
|
+
.replace(/ at line (\d+), column (\d+)/g, ',第 $1 行第 $2 列')
|
|
206
|
+
: item.evidence
|
|
194
207
|
return { ...item, message, suggestion, evidence }
|
|
195
208
|
}
|
|
196
209
|
|
package/src/repair.mjs
CHANGED
|
@@ -3,6 +3,9 @@ import { copyFileSync, readFileSync, renameSync, statSync, writeFileSync } from
|
|
|
3
3
|
import { dirname, join } from 'node:path'
|
|
4
4
|
import crossSpawn from 'cross-spawn'
|
|
5
5
|
|
|
6
|
+
export const COMMAND_TIMEOUT_MS = 10 * 60 * 1000
|
|
7
|
+
const COMMAND_MAX_BUFFER = 16 * 1024 * 1024
|
|
8
|
+
|
|
6
9
|
function hash(value) {
|
|
7
10
|
return createHash('sha256').update(value).digest('hex')
|
|
8
11
|
}
|
|
@@ -40,6 +43,16 @@ export function formatRepairPlan(actions, options = {}) {
|
|
|
40
43
|
: `${plan}${zh ? '执行这些修复吗?[y/N] ' : 'Apply these repairs? [y/N] '}`
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
export function formatRepairOutcome(actions, results, options = {}) {
|
|
47
|
+
if (results.length > 0) return ''
|
|
48
|
+
const zh = options.language === 'zh'
|
|
49
|
+
if (options.declined === true) return zh ? '修复结果:用户已取消。\n' : 'Repairs: cancelled by the user.\n'
|
|
50
|
+
if (actions.length > 0) return ''
|
|
51
|
+
return options.findingCount === 0
|
|
52
|
+
? zh ? '修复结果:当前无需修复。\n' : 'Repairs: no repairs are needed.\n'
|
|
53
|
+
: zh ? '修复结果:没有可安全自动执行的修复,请按诊断建议手动处理。\n' : 'Repairs: no safe automatic repairs are available; follow the diagnostic suggestions manually.\n'
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
function localizedDescription(action, zh) {
|
|
44
57
|
if (!zh) return action.description
|
|
45
58
|
if (action.id.startsWith('update-package:')) return `更新 profile ${action.profile} 中的 ${action.package}。`
|
|
@@ -90,11 +103,15 @@ function limitedOutput(value, limit = 8192) {
|
|
|
90
103
|
function applyCommand(action, options) {
|
|
91
104
|
const [command, ...args] = action.command
|
|
92
105
|
const captureOutput = options.captureOutput === true
|
|
106
|
+
const timeout = options.commandTimeoutMs ?? COMMAND_TIMEOUT_MS
|
|
107
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error('command repair timeout must be a positive number')
|
|
93
108
|
const result = crossSpawn.sync(command, args, {
|
|
94
109
|
stdio: captureOutput ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
95
|
-
...captureOutput ? { encoding: 'utf8' } : {},
|
|
110
|
+
...captureOutput ? { encoding: 'utf8', maxBuffer: COMMAND_MAX_BUFFER } : {},
|
|
96
111
|
env: action.env === undefined ? process.env : { ...process.env, ...action.env },
|
|
112
|
+
timeout,
|
|
97
113
|
})
|
|
114
|
+
if (result.error?.code === 'ETIMEDOUT') throw new Error(`${command} timed out after ${String(timeout)} ms`)
|
|
98
115
|
if (result.error != null) throw result.error
|
|
99
116
|
if (result.status !== 0) {
|
|
100
117
|
const reason = result.signal === null
|
|
@@ -115,7 +132,8 @@ function applyCommand(action, options) {
|
|
|
115
132
|
|
|
116
133
|
export function applyRepairs(actions, options = {}) {
|
|
117
134
|
const results = []
|
|
118
|
-
for (
|
|
135
|
+
for (let index = 0; index < actions.length; index += 1) {
|
|
136
|
+
const action = actions[index]
|
|
119
137
|
try {
|
|
120
138
|
results.push(action.kind === 'json-edit' ? applyJsonEdit(action) : applyCommand(action, options))
|
|
121
139
|
} catch (error) {
|
|
@@ -124,6 +142,9 @@ export function applyRepairs(actions, options = {}) {
|
|
|
124
142
|
status: 'failed',
|
|
125
143
|
error: error instanceof Error ? error.message : String(error),
|
|
126
144
|
})
|
|
145
|
+
for (const skipped of actions.slice(index + 1)) {
|
|
146
|
+
results.push({ id: skipped.id, status: 'skipped' })
|
|
147
|
+
}
|
|
127
148
|
break
|
|
128
149
|
}
|
|
129
150
|
}
|