@bruc3van/dsh-doctor 0.1.0 → 0.1.1
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.md +21 -0
- package/package.json +2 -2
- package/src/cli.mjs +42 -8
- package/src/doctor.mjs +193 -53
- package/src/i18n.mjs +149 -0
- package/src/repair.mjs +22 -7
package/README.md
CHANGED
|
@@ -21,6 +21,25 @@ npx @bruc3van/dsh-doctor
|
|
|
21
21
|
|
|
22
22
|
默认检查 `$DSH_HOME/profiles/web`;未设置 `DSH_HOME` 时使用 `~/.dsh`。
|
|
23
23
|
|
|
24
|
+
Doctor 不要求 `dsh` 必须是全局命令。它会按顺序查找 PATH、当前项目安装、profile 共享安装或 npx 缓存留下的链接、已构建的 Harness 源码工作区。DSH Desktop 内置运行时或其他特殊安装可以通过 `--dsh-command /path/to/dsh`(也接受官方包的 `lib/bin.js`)明确指定。找不到 CLI 时仍会完成只读诊断,但不会提供或执行无法验证的命令型修复。
|
|
25
|
+
|
|
26
|
+
## 输出语言
|
|
27
|
+
|
|
28
|
+
文本输出支持中文和英文。默认依次读取:
|
|
29
|
+
|
|
30
|
+
1. `--lang zh|en`
|
|
31
|
+
2. `DSH_DOCTOR_LANG`
|
|
32
|
+
3. 当前 DSH Home 中 `settings.yaml` 的 `locale.preference`
|
|
33
|
+
4. 终端或系统 locale
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
dsh-doctor --lang zh
|
|
37
|
+
dsh-doctor --lang en
|
|
38
|
+
DSH_DOCTOR_LANG=zh dsh-doctor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`--json` 始终保留稳定的英文消息与诊断 code,避免语言变化破坏脚本。
|
|
42
|
+
|
|
24
43
|
## 常用命令
|
|
25
44
|
|
|
26
45
|
```sh
|
|
@@ -28,6 +47,7 @@ npx @bruc3van/dsh-doctor
|
|
|
28
47
|
dsh-doctor
|
|
29
48
|
dsh-doctor --profile web
|
|
30
49
|
dsh-doctor --home /path/to/.dsh
|
|
50
|
+
dsh-doctor --dsh-command /path/to/@deepseek-ai/dsh/lib/bin.js
|
|
31
51
|
|
|
32
52
|
# 机器可读的只读报告,不显示提示
|
|
33
53
|
dsh-doctor --json
|
|
@@ -60,6 +80,7 @@ dsh-doctor --fix --yes --json
|
|
|
60
80
|
- 文件修复在确认前展示路径,确认后再次校验 SHA-256 指纹。
|
|
61
81
|
- 写入前创建 `.dsh-doctor-<timestamp>.bak` 备份,再通过同目录临时文件原子替换。
|
|
62
82
|
- 外部命令使用固定 argv 调用,不拼接 shell 命令。
|
|
83
|
+
- 命令修复绑定当前诊断的 `DSH_HOME`,并展示解析出的真实 CLI 路径;不会假定 PATH 中存在 `dsh`。
|
|
63
84
|
- 任一步失败即停止后续修复,并保留已经创建的备份。
|
|
64
85
|
- 完成后重新运行全部诊断,以最终状态决定退出码。
|
|
65
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bruc3van/dsh-doctor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Diagnostics and confirmed recovery for DeepSeek Harness profiles and third-party plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
24
|
"test": "node --test",
|
|
25
|
-
"check": "node --check src/cli.mjs && node --check src/doctor.mjs && node --check src/repair.mjs && npm test",
|
|
25
|
+
"check": "node --check src/cli.mjs && node --check src/doctor.mjs && node --check src/i18n.mjs && node --check src/repair.mjs && npm test",
|
|
26
26
|
"prepublishOnly": "npm run check"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
package/src/cli.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { formatReport, diagnose } from './doctor.mjs'
|
|
3
|
+
import { defaultDshHome, formatReport, diagnose } from './doctor.mjs'
|
|
4
4
|
import { applyRepairs, formatRepairPlan, repairsFromReport } from './repair.mjs'
|
|
5
|
+
import { resolveLanguage } from './i18n.mjs'
|
|
5
6
|
import { readFileSync } from 'node:fs'
|
|
6
7
|
import { dirname, resolve } from 'node:path'
|
|
7
8
|
import { fileURLToPath } from 'node:url'
|
|
8
9
|
import { createInterface } from 'node:readline/promises'
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const HELP_EN = `Usage: dsh-doctor [options]
|
|
11
12
|
|
|
12
13
|
Diagnostics and confirmed recovery for a DeepSeek Harness profile.
|
|
13
14
|
|
|
@@ -15,6 +16,8 @@ Options:
|
|
|
15
16
|
--profile <name> profile to inspect (default: web)
|
|
16
17
|
--home <path> Harness home (default: $DSH_HOME or ~/.dsh)
|
|
17
18
|
--harness-root <path> active DeepSeek Harness source checkout
|
|
19
|
+
--dsh-command <path> DSH executable or lib/bin.js used for command repairs
|
|
20
|
+
--lang <auto|zh|en> output language (default: DSH setting, then system)
|
|
18
21
|
--json print machine-readable JSON
|
|
19
22
|
--fix, --repair preview and apply available repairs after confirmation
|
|
20
23
|
--yes confirm the displayed repair plan non-interactively
|
|
@@ -22,8 +25,27 @@ Options:
|
|
|
22
25
|
-v, --version show version
|
|
23
26
|
`
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
const HELP_ZH = `用法:dsh-doctor [选项]
|
|
29
|
+
|
|
30
|
+
诊断 DeepSeek Harness profile,并在用户确认后实施修复。
|
|
31
|
+
|
|
32
|
+
选项:
|
|
33
|
+
--profile <名称> 要检查的 profile(默认:web)
|
|
34
|
+
--home <路径> Harness 主目录(默认:$DSH_HOME 或 ~/.dsh)
|
|
35
|
+
--harness-root <路径> 当前 DeepSeek Harness 源码工作区
|
|
36
|
+
--dsh-command <路径> 命令修复使用的 DSH 可执行文件或 lib/bin.js
|
|
37
|
+
--lang <auto|zh|en> 输出语言(默认:DSH 设置,其次系统语言)
|
|
38
|
+
--json 输出稳定的机器可读 JSON
|
|
39
|
+
--fix, --repair 展示修复计划,确认后实施
|
|
40
|
+
--yes 在非交互环境中确认当前修复计划
|
|
41
|
+
-h, --help 显示帮助
|
|
42
|
+
-v, --version 显示版本
|
|
43
|
+
`
|
|
44
|
+
|
|
45
|
+
const help = language => language === 'zh' ? HELP_ZH : HELP_EN
|
|
46
|
+
|
|
47
|
+
function fail(message, language = 'en') {
|
|
48
|
+
process.stderr.write(`dsh-doctor: ${message}\n\n${help(language)}`)
|
|
27
49
|
process.exitCode = 2
|
|
28
50
|
}
|
|
29
51
|
|
|
@@ -40,6 +62,8 @@ function parse(args) {
|
|
|
40
62
|
if (arg === '--profile') options.profile = valueAfter(args, index++, arg)
|
|
41
63
|
else if (arg === '--home') options.home = valueAfter(args, index++, arg)
|
|
42
64
|
else if (arg === '--harness-root') options.harnessRoot = valueAfter(args, index++, arg)
|
|
65
|
+
else if (arg === '--dsh-command') options.dshCommand = valueAfter(args, index++, arg)
|
|
66
|
+
else if (arg === '--lang') options.lang = valueAfter(args, index++, arg)
|
|
43
67
|
else if (arg === '--json') options.json = true
|
|
44
68
|
else if (arg === '--fix' || arg === '--repair') options.fix = true
|
|
45
69
|
else if (arg === '--yes') options.yes = true
|
|
@@ -58,10 +82,15 @@ async function main() {
|
|
|
58
82
|
fail(error instanceof Error ? error.message : String(error))
|
|
59
83
|
return
|
|
60
84
|
}
|
|
85
|
+
const language = resolveLanguage({
|
|
86
|
+
requested: options.lang,
|
|
87
|
+
home: resolve(options.home ?? defaultDshHome()),
|
|
88
|
+
systemLocale: Intl.DateTimeFormat().resolvedOptions().locale,
|
|
89
|
+
})
|
|
61
90
|
if (options.yes && !options.fix) throw new Error('--yes requires --fix')
|
|
62
91
|
if (options.json && options.fix && !options.yes) throw new Error('--json --fix requires --yes because a prompt would corrupt JSON output')
|
|
63
92
|
if (options.help) {
|
|
64
|
-
process.stdout.write(
|
|
93
|
+
process.stdout.write(help(language))
|
|
65
94
|
} else if (options.version) {
|
|
66
95
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
67
96
|
const manifest = JSON.parse(readFileSync(resolve(here, '..', 'package.json'), 'utf8'))
|
|
@@ -71,15 +100,20 @@ async function main() {
|
|
|
71
100
|
let repairs = []
|
|
72
101
|
if (options.fix) {
|
|
73
102
|
const actions = repairsFromReport(report)
|
|
103
|
+
if (actions.length === 0 && report.context.dshCli?.commandRepairNeeded && !report.context.dshCli.available) {
|
|
104
|
+
throw new Error(language === 'zh'
|
|
105
|
+
? '未找到可用的 DSH CLI,无法执行命令型修复;请使用 --dsh-command 指定当前安装的 dsh 或 lib/bin.js'
|
|
106
|
+
: 'no working DSH CLI was found; pass the active dsh executable or lib/bin.js with --dsh-command')
|
|
107
|
+
}
|
|
74
108
|
if (actions.length > 0) {
|
|
75
109
|
let confirmed = options.yes === true
|
|
76
110
|
if (!confirmed) {
|
|
77
111
|
if (!process.stdin.isTTY) throw new Error('--fix needs an interactive terminal or explicit --yes')
|
|
78
|
-
const prompt = formatRepairPlan(actions)
|
|
112
|
+
const prompt = formatRepairPlan(actions, { language })
|
|
79
113
|
const reader = createInterface({ input: process.stdin, output: process.stderr })
|
|
80
114
|
const answer = await reader.question(prompt)
|
|
81
115
|
reader.close()
|
|
82
|
-
confirmed = /^y(?:es)
|
|
116
|
+
confirmed = /^(?:y(?:es)?|是|确认)$/i.test(answer.trim())
|
|
83
117
|
}
|
|
84
118
|
if (confirmed) {
|
|
85
119
|
repairs = applyRepairs(actions)
|
|
@@ -90,7 +124,7 @@ async function main() {
|
|
|
90
124
|
const output = options.fix ? { ...report, repairs } : report
|
|
91
125
|
process.stdout.write(options.json
|
|
92
126
|
? `${JSON.stringify(output, null, 2)}\n`
|
|
93
|
-
: `${formatReport(report, { color: process.stdout.isTTY })}${repairs.length > 0 ?
|
|
127
|
+
: `${formatReport(report, { color: process.stdout.isTTY, language })}${repairs.length > 0 ? `${language === 'zh' ? '修复结果' : 'Repairs'}: ${JSON.stringify(repairs, null, 2)}\n` : ''}`)
|
|
94
128
|
process.exitCode = repairs.some(item => item.status === 'failed') ? 2 : report.summary.errors > 0 ? 1 : 0
|
|
95
129
|
}
|
|
96
130
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
2
2
|
import { homedir } from 'node:os'
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
4
4
|
import yaml from 'js-yaml'
|
|
5
5
|
import semver from 'semver'
|
|
6
6
|
import { parseDocument } from 'yaml'
|
|
7
|
+
import { languageName, localizedFinding } from './i18n.mjs'
|
|
7
8
|
|
|
8
9
|
export const PLATFORM_MODULES = new Set([
|
|
9
10
|
'react',
|
|
@@ -108,6 +109,112 @@ function findHarnessRoot(start) {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
function executable(file) {
|
|
113
|
+
try {
|
|
114
|
+
accessSync(file, process.platform === 'win32' ? constants.F_OK : constants.X_OK)
|
|
115
|
+
return true
|
|
116
|
+
} catch {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function executableOnPath(name, env) {
|
|
122
|
+
const path = env.PATH ?? env.Path ?? env.path ?? ''
|
|
123
|
+
const extensions = process.platform === 'win32'
|
|
124
|
+
? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';')
|
|
125
|
+
: ['']
|
|
126
|
+
for (const directory of path.split(delimiter).filter(Boolean)) {
|
|
127
|
+
for (const extension of extensions) {
|
|
128
|
+
const candidate = join(directory, process.platform === 'win32' ? `${name}${extension}` : name)
|
|
129
|
+
if (executable(candidate)) return candidate
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function manifestBin(packageDir) {
|
|
136
|
+
const file = join(packageDir, 'package.json')
|
|
137
|
+
if (!existsSync(file)) return undefined
|
|
138
|
+
try {
|
|
139
|
+
const manifest = JSON.parse(readFileSync(file, 'utf8'))
|
|
140
|
+
if (manifest?.name !== '@deepseek-ai/dsh') return undefined
|
|
141
|
+
const declared = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.dsh
|
|
142
|
+
if (typeof declared !== 'string') return undefined
|
|
143
|
+
const bin = resolve(packageDir, declared)
|
|
144
|
+
const rel = relative(packageDir, bin)
|
|
145
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel) || !existsSync(bin) || !statSync(bin).isFile()) return undefined
|
|
146
|
+
return { bin, version: manifest.version }
|
|
147
|
+
} catch {
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function localDshPackage(start) {
|
|
153
|
+
let current = resolve(start)
|
|
154
|
+
while (true) {
|
|
155
|
+
const packageDir = join(current, 'node_modules', '@deepseek-ai', 'dsh')
|
|
156
|
+
if (manifestBin(packageDir) !== undefined) return packageDir
|
|
157
|
+
const parent = dirname(current)
|
|
158
|
+
if (parent === current) return undefined
|
|
159
|
+
current = parent
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function commandFromValue(value, env, cwd) {
|
|
164
|
+
const expanded = value === '~'
|
|
165
|
+
? homedir()
|
|
166
|
+
: value.startsWith('~/') || value.startsWith('~\\')
|
|
167
|
+
? resolve(homedir(), value.slice(2))
|
|
168
|
+
: value
|
|
169
|
+
const looksLikePath = isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\') || expanded.startsWith('.')
|
|
170
|
+
const file = looksLikePath ? resolve(cwd, expanded) : executableOnPath(expanded, env)
|
|
171
|
+
if (file === undefined || !existsSync(file)) return undefined
|
|
172
|
+
return /\.(?:c?js|mjs)$/i.test(file)
|
|
173
|
+
? { command: [process.execPath, file], path: file }
|
|
174
|
+
: { command: [file], path: file }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolveDshCli(options, harness, home) {
|
|
178
|
+
const env = options.env ?? process.env
|
|
179
|
+
const cwd = options.cwd ?? process.cwd()
|
|
180
|
+
const explicit = options.dshCommand ?? env.DSH_DOCTOR_DSH_COMMAND
|
|
181
|
+
if (explicit !== undefined) {
|
|
182
|
+
const resolved = commandFromValue(explicit, env, cwd)
|
|
183
|
+
if (resolved === undefined) throw new Error(`DSH command ${JSON.stringify(explicit)} does not exist or is not executable`)
|
|
184
|
+
return { ...resolved, source: 'explicit' }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (options.harnessRoot !== undefined && harness.root !== undefined) {
|
|
188
|
+
for (const packageDir of [harness.root, join(harness.root, 'apps', 'cli')]) {
|
|
189
|
+
const bin = manifestBin(packageDir)
|
|
190
|
+
if (bin !== undefined) return { command: [process.execPath, bin.bin], path: bin.bin, source: 'checkout', version: bin.version }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const sharedPackage = join(home, 'profiles', 'node_modules', '@deepseek-ai', 'dsh')
|
|
195
|
+
if (existsSync(sharedPackage)) {
|
|
196
|
+
const bin = manifestBin(realpathSync(sharedPackage))
|
|
197
|
+
if (bin !== undefined) return { command: [process.execPath, bin.bin], path: bin.bin, source: 'profile', version: bin.version }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const localPackage = localDshPackage(cwd)
|
|
201
|
+
if (localPackage !== undefined) {
|
|
202
|
+
const bin = manifestBin(localPackage)
|
|
203
|
+
return { command: [process.execPath, bin.bin], path: bin.bin, source: 'project', version: bin.version }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const pathCommand = executableOnPath('dsh', env)
|
|
207
|
+
if (pathCommand !== undefined) return { command: [pathCommand], path: pathCommand, source: 'path' }
|
|
208
|
+
|
|
209
|
+
if (harness.root !== undefined) {
|
|
210
|
+
for (const packageDir of [harness.root, join(harness.root, 'apps', 'cli')]) {
|
|
211
|
+
const bin = manifestBin(packageDir)
|
|
212
|
+
if (bin !== undefined) return { command: [process.execPath, bin.bin], path: bin.bin, source: 'checkout', version: bin.version }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
111
218
|
function workspacePackageDirectories(root) {
|
|
112
219
|
const directories = []
|
|
113
220
|
const addChildren = (parent) => {
|
|
@@ -238,14 +345,13 @@ function dependencyEntries(value, field, file, findings) {
|
|
|
238
345
|
return Object.entries(record)
|
|
239
346
|
}
|
|
240
347
|
|
|
241
|
-
function updateRepair(profile, name) {
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
348
|
+
function updateRepair(profile, name, commandRepair) {
|
|
349
|
+
return commandRepair(
|
|
350
|
+
`update-package:${name}`,
|
|
351
|
+
`Update ${name} in profile ${profile}.`,
|
|
352
|
+
['plugin', '--profile', profile, 'update', name],
|
|
353
|
+
{ package: name, profile },
|
|
354
|
+
)
|
|
249
355
|
}
|
|
250
356
|
|
|
251
357
|
function safePackageFile(packageDir, exported) {
|
|
@@ -316,10 +422,10 @@ function stringArray(value) {
|
|
|
316
422
|
|
|
317
423
|
function inspectClientPackage(record, context) {
|
|
318
424
|
const {
|
|
319
|
-
findings, harnessPackages, harnessPackagesAuthoritative, profile, resolvePackage,
|
|
425
|
+
commandRepair, findings, harnessPackages, harnessPackagesAuthoritative, profile, resolvePackage,
|
|
320
426
|
} = context
|
|
321
427
|
const name = record.manifest.name
|
|
322
|
-
const disableSuggestion = `Upgrade ${name}; if no compatible release exists,
|
|
428
|
+
const disableSuggestion = `Upgrade ${name}; if no compatible release exists, remove it through the same DSH installation.`
|
|
323
429
|
const declaration = record.manifest?.dsh?.client
|
|
324
430
|
if (declaration === undefined) return
|
|
325
431
|
if (declaration === null || typeof declaration !== 'object') {
|
|
@@ -327,7 +433,7 @@ function inspectClientPackage(record, context) {
|
|
|
327
433
|
package: name,
|
|
328
434
|
evidence: record.file,
|
|
329
435
|
suggestion: disableSuggestion,
|
|
330
|
-
repair: updateRepair(profile, name),
|
|
436
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
331
437
|
}))
|
|
332
438
|
return
|
|
333
439
|
}
|
|
@@ -337,7 +443,7 @@ function inspectClientPackage(record, context) {
|
|
|
337
443
|
package: name,
|
|
338
444
|
evidence: record.file,
|
|
339
445
|
suggestion: disableSuggestion,
|
|
340
|
-
repair: updateRepair(profile, name),
|
|
446
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
341
447
|
}))
|
|
342
448
|
return
|
|
343
449
|
}
|
|
@@ -346,7 +452,7 @@ function inspectClientPackage(record, context) {
|
|
|
346
452
|
package: name,
|
|
347
453
|
evidence: record.file,
|
|
348
454
|
suggestion: disableSuggestion,
|
|
349
|
-
repair: updateRepair(profile, name),
|
|
455
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
350
456
|
}))
|
|
351
457
|
}
|
|
352
458
|
if (declaration.platform !== 'web') return
|
|
@@ -358,7 +464,7 @@ function inspectClientPackage(record, context) {
|
|
|
358
464
|
package: name,
|
|
359
465
|
evidence: record.file,
|
|
360
466
|
suggestion: disableSuggestion,
|
|
361
|
-
repair: updateRepair(profile, name),
|
|
467
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
362
468
|
}))
|
|
363
469
|
}
|
|
364
470
|
if (declaration.inject !== undefined && inject === undefined) {
|
|
@@ -366,7 +472,7 @@ function inspectClientPackage(record, context) {
|
|
|
366
472
|
package: name,
|
|
367
473
|
evidence: record.file,
|
|
368
474
|
suggestion: disableSuggestion,
|
|
369
|
-
repair: updateRepair(profile, name),
|
|
475
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
370
476
|
}))
|
|
371
477
|
}
|
|
372
478
|
|
|
@@ -384,8 +490,8 @@ function inspectClientPackage(record, context) {
|
|
|
384
490
|
findings.push(finding('error', 'CLIENT_BUNDLE_MISSING', `${name} client bundle is missing.`, {
|
|
385
491
|
package: name,
|
|
386
492
|
evidence: file ?? `${record.file}: exports["./client"] = ${JSON.stringify(exported)}`,
|
|
387
|
-
suggestion: `Reinstall or rebuild ${name}; if it remains broken,
|
|
388
|
-
repair: updateRepair(profile, name),
|
|
493
|
+
suggestion: `Reinstall or rebuild ${name}; if it remains broken, remove it through the same DSH installation.`,
|
|
494
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
389
495
|
}))
|
|
390
496
|
return
|
|
391
497
|
}
|
|
@@ -409,7 +515,7 @@ function inspectClientPackage(record, context) {
|
|
|
409
515
|
package: name,
|
|
410
516
|
evidence: file,
|
|
411
517
|
suggestion: disableSuggestion,
|
|
412
|
-
repair: updateRepair(profile, name),
|
|
518
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
413
519
|
}))
|
|
414
520
|
}
|
|
415
521
|
}
|
|
@@ -432,7 +538,7 @@ function inspectClientPackage(record, context) {
|
|
|
432
538
|
package: name,
|
|
433
539
|
evidence: record.file,
|
|
434
540
|
suggestion: disableSuggestion,
|
|
435
|
-
repair: updateRepair(profile, name),
|
|
541
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
436
542
|
}))
|
|
437
543
|
}
|
|
438
544
|
}
|
|
@@ -445,7 +551,7 @@ function inspectClientPackage(record, context) {
|
|
|
445
551
|
package: name,
|
|
446
552
|
evidence: record.file,
|
|
447
553
|
suggestion: disableSuggestion,
|
|
448
|
-
repair: updateRepair(profile, name),
|
|
554
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
449
555
|
}))
|
|
450
556
|
}
|
|
451
557
|
}
|
|
@@ -467,7 +573,7 @@ function inspectBundle(name, record, findings) {
|
|
|
467
573
|
if (record === undefined) {
|
|
468
574
|
findings.push(finding('error', 'BUNDLE_NOT_INSTALLED', `Profile bundle ${name} is not installed.`, {
|
|
469
575
|
package: name,
|
|
470
|
-
suggestion:
|
|
576
|
+
suggestion: 'Install the profile dependencies with the active DSH installation, upgrade the bundle, or remove it from the profile.',
|
|
471
577
|
}))
|
|
472
578
|
return
|
|
473
579
|
}
|
|
@@ -580,7 +686,7 @@ function inspectPatchFileIfPresent(file, subject, findings) {
|
|
|
580
686
|
}
|
|
581
687
|
|
|
582
688
|
function inspectCompatibility(record, context) {
|
|
583
|
-
const { findings, harnessPackages, profile, resolvePackage } = context
|
|
689
|
+
const { commandRepair, findings, harnessPackages, profile, resolvePackage } = context
|
|
584
690
|
const peers = dependencyEntries(record.manifest.peerDependencies, 'peerDependencies', record.file, findings)
|
|
585
691
|
const mismatches = []
|
|
586
692
|
for (const [name, range] of peers) {
|
|
@@ -597,7 +703,7 @@ function inspectCompatibility(record, context) {
|
|
|
597
703
|
package: record.manifest.name,
|
|
598
704
|
evidence: mismatches.join(', '),
|
|
599
705
|
suggestion: `Update ${record.manifest.name} to a release compatible with the active Harness.`,
|
|
600
|
-
repair: updateRepair(profile, record.manifest.name),
|
|
706
|
+
repair: updateRepair(profile, record.manifest.name, commandRepair),
|
|
601
707
|
}))
|
|
602
708
|
}
|
|
603
709
|
}
|
|
@@ -625,7 +731,7 @@ export function diagnose(options = {}) {
|
|
|
625
731
|
if (!existsSync(profileManifestFile)) {
|
|
626
732
|
findings.push(finding('error', 'PROFILE_NOT_FOUND', `Profile ${profile} does not exist.`, {
|
|
627
733
|
evidence: profileDir,
|
|
628
|
-
suggestion:
|
|
734
|
+
suggestion: 'Start the profile once or initialize it with the plugin install command from the same DSH installation.',
|
|
629
735
|
}))
|
|
630
736
|
return finish({ home, profile, profileDir, harness: {}, packages: [] }, findings)
|
|
631
737
|
}
|
|
@@ -635,6 +741,20 @@ export function diagnose(options = {}) {
|
|
|
635
741
|
}
|
|
636
742
|
|
|
637
743
|
const harness = resolveHarnessContext(home, options.harnessRoot, findings)
|
|
744
|
+
const dshCli = resolveDshCli(options, harness, home)
|
|
745
|
+
let commandRepairNeeded = false
|
|
746
|
+
const commandRepair = (id, description, args, metadata = {}) => {
|
|
747
|
+
commandRepairNeeded = true
|
|
748
|
+
return dshCli === undefined ? undefined : {
|
|
749
|
+
id,
|
|
750
|
+
kind: 'command',
|
|
751
|
+
risk: 'medium',
|
|
752
|
+
description,
|
|
753
|
+
command: [...dshCli.command, ...args],
|
|
754
|
+
env: { DSH_HOME: home },
|
|
755
|
+
...metadata,
|
|
756
|
+
}
|
|
757
|
+
}
|
|
638
758
|
const resolvePackage = packageResolver(profileDir, home, harness.packages, findings)
|
|
639
759
|
const resolveBundle = bundleResolver(profileDir, home, harness.packages, findings)
|
|
640
760
|
const dependencyEntriesList = dependencyEntries(profileManifest.dependencies, 'dependencies', profileManifestFile, findings)
|
|
@@ -671,14 +791,13 @@ export function diagnose(options = {}) {
|
|
|
671
791
|
findings.push(finding('error', 'DEPENDENCY_NOT_INSTALLED', `Profile dependency ${name} is not installed.`, {
|
|
672
792
|
package: name,
|
|
673
793
|
evidence: profileManifestFile,
|
|
674
|
-
suggestion:
|
|
675
|
-
repair:
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
},
|
|
794
|
+
suggestion: 'Use the exact command shown below to install the declared profile dependencies.',
|
|
795
|
+
repair: commandRepair(
|
|
796
|
+
`install-profile:${profile}`,
|
|
797
|
+
`Install the declared dependencies for profile ${profile}.`,
|
|
798
|
+
['plugin', '--profile', profile, 'install'],
|
|
799
|
+
{ profile },
|
|
800
|
+
),
|
|
682
801
|
}))
|
|
683
802
|
continue
|
|
684
803
|
}
|
|
@@ -690,14 +809,13 @@ export function diagnose(options = {}) {
|
|
|
690
809
|
findings.push(finding('warning', 'PROFILE_DEPENDENCY_VERSION_MISMATCH', `Profile requests ${name} ${declared}, but ${installed} is installed.`, {
|
|
691
810
|
package: name,
|
|
692
811
|
evidence: record.file,
|
|
693
|
-
suggestion:
|
|
694
|
-
repair:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
},
|
|
812
|
+
suggestion: 'Use the exact command shown below to reconcile the profile installation.',
|
|
813
|
+
repair: commandRepair(
|
|
814
|
+
`install-profile:${profile}`,
|
|
815
|
+
`Install the declared dependencies for profile ${profile}.`,
|
|
816
|
+
['plugin', '--profile', profile, 'install'],
|
|
817
|
+
{ profile },
|
|
818
|
+
),
|
|
701
819
|
}))
|
|
702
820
|
}
|
|
703
821
|
if (record.manifest?.dsh?.bundle?.patch !== undefined && !bundleNames.includes(name)) {
|
|
@@ -729,6 +847,7 @@ export function diagnose(options = {}) {
|
|
|
729
847
|
for (const record of thirdPartyRecords) {
|
|
730
848
|
inspectClientPackage(record, {
|
|
731
849
|
findings,
|
|
850
|
+
commandRepair,
|
|
732
851
|
harnessPackages: harness.packages,
|
|
733
852
|
harnessPackagesAuthoritative: harness.authoritative,
|
|
734
853
|
profile,
|
|
@@ -736,6 +855,7 @@ export function diagnose(options = {}) {
|
|
|
736
855
|
})
|
|
737
856
|
inspectCompatibility(record, {
|
|
738
857
|
findings,
|
|
858
|
+
commandRepair,
|
|
739
859
|
profile,
|
|
740
860
|
resolvePackage,
|
|
741
861
|
harnessPackages: harness.packages,
|
|
@@ -747,6 +867,9 @@ export function diagnose(options = {}) {
|
|
|
747
867
|
profile,
|
|
748
868
|
profileDir,
|
|
749
869
|
harness: { root: harness.root, version: harness.version },
|
|
870
|
+
dshCli: dshCli === undefined
|
|
871
|
+
? { available: false, commandRepairNeeded }
|
|
872
|
+
: { available: true, commandRepairNeeded, ...dshCli },
|
|
750
873
|
packages: thirdPartyRecords.map(record => ({
|
|
751
874
|
name: record.manifest.name,
|
|
752
875
|
version: record.manifest.version,
|
|
@@ -772,32 +895,49 @@ function finish(context, findings) {
|
|
|
772
895
|
|
|
773
896
|
export function formatReport(report, options = {}) {
|
|
774
897
|
const color = options.color ?? false
|
|
898
|
+
const language = options.language ?? 'en'
|
|
899
|
+
const zh = language === 'zh'
|
|
775
900
|
const paint = (severity, value) => {
|
|
776
901
|
if (!color) return value
|
|
777
902
|
const code = severity === 'error' ? 31 : severity === 'warning' ? 33 : 36
|
|
778
903
|
return `\u001B[${code}m${value}\u001B[0m`
|
|
779
904
|
}
|
|
905
|
+
const quote = value => /^[a-zA-Z0-9_@./:\\-]+$/.test(value) ? value : JSON.stringify(value)
|
|
906
|
+
const cli = report.context.dshCli
|
|
907
|
+
const cliText = cli?.available
|
|
908
|
+
? `${cli.source}${cli.version ? ` ${cli.version}` : ''} (${cli.command.map(quote).join(' ')})`
|
|
909
|
+
: zh ? '未找到(命令型修复已禁用)' : 'not found (command-based repairs disabled)'
|
|
780
910
|
const lines = [
|
|
781
911
|
'DSH Doctor',
|
|
782
|
-
|
|
783
|
-
|
|
912
|
+
`${zh ? 'Profile' : 'Profile'}: ${report.context.profile}`,
|
|
913
|
+
`${zh ? 'DSH 主目录' : 'Home'}: ${report.context.home}`,
|
|
784
914
|
`Harness: ${report.context.harness.version ?? 'unknown'}${report.context.harness.root ? ` (${report.context.harness.root})` : ''}`,
|
|
785
|
-
|
|
915
|
+
`${zh ? 'DSH CLI' : 'DSH CLI'}: ${cliText}`,
|
|
916
|
+
`${zh ? '已检查第三方包' : 'Checked third-party packages'}: ${String(report.context.packages.length)}`,
|
|
917
|
+
`${zh ? '输出语言' : 'Output language'}: ${languageName(language)}`,
|
|
786
918
|
'',
|
|
787
919
|
]
|
|
788
920
|
if (report.findings.length === 0) {
|
|
789
|
-
lines.push(paint('info', 'OK No problems found by the MVP checks.'))
|
|
921
|
+
lines.push(paint('info', zh ? '正常 当前检查范围内未发现问题。' : 'OK No problems found by the MVP checks.'))
|
|
790
922
|
} else {
|
|
791
|
-
for (const
|
|
792
|
-
const
|
|
923
|
+
for (const original of report.findings) {
|
|
924
|
+
const item = localizedFinding(original, language)
|
|
925
|
+
const label = zh
|
|
926
|
+
? item.severity === 'error' ? '错误' : item.severity === 'warning' ? '警告' : '信息'
|
|
927
|
+
: item.severity === 'error' ? 'ERROR' : item.severity === 'warning' ? 'WARN ' : 'INFO '
|
|
793
928
|
lines.push(paint(item.severity, `${label} [${item.code}] ${item.message}`))
|
|
794
|
-
if (item.package !== undefined) lines.push(` Package: ${item.package}`)
|
|
795
|
-
if (item.evidence !== undefined) lines.push(` Evidence: ${item.evidence}`)
|
|
796
|
-
if (item.suggestion !== undefined) lines.push(` Action: ${item.suggestion}`)
|
|
929
|
+
if (item.package !== undefined) lines.push(` ${zh ? '包' : 'Package'}: ${item.package}`)
|
|
930
|
+
if (item.evidence !== undefined) lines.push(` ${zh ? '证据' : 'Evidence'}: ${item.evidence}`)
|
|
931
|
+
if (item.suggestion !== undefined) lines.push(` ${zh ? '建议' : 'Action'}: ${item.suggestion}`)
|
|
932
|
+
if (item.repair?.kind === 'command') lines.push(` ${zh ? '更新命令' : 'Update command'}: ${item.repair.command.map(quote).join(' ')}`)
|
|
797
933
|
lines.push('')
|
|
798
934
|
}
|
|
799
935
|
}
|
|
800
|
-
lines.push(
|
|
801
|
-
|
|
936
|
+
lines.push(zh
|
|
937
|
+
? `汇总:${String(report.summary.errors)} 个错误,${String(report.summary.warnings)} 个警告`
|
|
938
|
+
: `Summary: ${String(report.summary.errors)} error(s), ${String(report.summary.warnings)} warning(s)`)
|
|
939
|
+
if (report.summary.errors > 0) lines.push(zh
|
|
940
|
+
? 'Harness 可能无法启动。请优先更新或停用产生错误的插件。'
|
|
941
|
+
: 'Harness may fail to start. Upgrade or disable the error-producing plugin first.')
|
|
802
942
|
return `${lines.join('\n')}\n`
|
|
803
943
|
}
|
package/src/i18n.mjs
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { parseDocument } from 'yaml'
|
|
4
|
+
|
|
5
|
+
const SUPPORTED = new Map([
|
|
6
|
+
['en', 'en'],
|
|
7
|
+
['en-us', 'en'],
|
|
8
|
+
['en_us', 'en'],
|
|
9
|
+
['zh', 'zh'],
|
|
10
|
+
['zh-cn', 'zh'],
|
|
11
|
+
['zh_cn', 'zh'],
|
|
12
|
+
['zh-hans', 'zh'],
|
|
13
|
+
])
|
|
14
|
+
|
|
15
|
+
function normalizedLanguage(value) {
|
|
16
|
+
if (typeof value !== 'string') return undefined
|
|
17
|
+
const cleaned = value.trim().split('.')[0].toLowerCase()
|
|
18
|
+
if (cleaned === '' || cleaned === 'auto' || cleaned === 'c' || cleaned === 'posix') return undefined
|
|
19
|
+
return SUPPORTED.get(cleaned) ?? SUPPORTED.get(cleaned.split(/[-_]/)[0])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function dshPreference(home) {
|
|
23
|
+
const file = join(home, 'settings.yaml')
|
|
24
|
+
if (!existsSync(file)) return undefined
|
|
25
|
+
try {
|
|
26
|
+
const document = parseDocument(readFileSync(file, 'utf8'), { prettyErrors: false })
|
|
27
|
+
if (document.errors.length > 0) return undefined
|
|
28
|
+
return document.toJS()?.locale?.preference
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveLanguage({ requested, env = process.env, home, systemLocale } = {}) {
|
|
35
|
+
if (requested !== undefined && requested !== 'auto') {
|
|
36
|
+
const language = normalizedLanguage(requested)
|
|
37
|
+
if (language === undefined) throw new Error(`unsupported language ${JSON.stringify(requested)}; expected auto, zh, or en`)
|
|
38
|
+
return language
|
|
39
|
+
}
|
|
40
|
+
const configured = normalizedLanguage(env.DSH_DOCTOR_LANG)
|
|
41
|
+
if (configured !== undefined) return configured
|
|
42
|
+
const preference = normalizedLanguage(home === undefined ? undefined : dshPreference(home))
|
|
43
|
+
if (preference !== undefined) return preference
|
|
44
|
+
for (const value of [env.LC_ALL, env.LC_MESSAGES, env.LANG, env.LANGUAGE, systemLocale]) {
|
|
45
|
+
const language = normalizedLanguage(value)
|
|
46
|
+
if (language !== undefined) return language
|
|
47
|
+
}
|
|
48
|
+
return 'en'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function captured(message, pattern, index = 1) {
|
|
52
|
+
return message.match(pattern)?.[index]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ZH_MESSAGES = {
|
|
56
|
+
FILE_READ_FAILED: item => `无法读取${captured(item.message, /^Cannot read (.+)\.$/) ?? '文件'}。`,
|
|
57
|
+
INVALID_JSON_OBJECT: item => `${captured(item.message, /^(.+) must contain a JSON object\.$/) ?? '该文件'}必须包含 JSON 对象。`,
|
|
58
|
+
INVALID_JSON: item => `${captured(item.message, /^(.+) is not valid JSON\.$/) ?? '该文件'}不是有效的 JSON。`,
|
|
59
|
+
PACKAGE_NAME_MISMATCH: item => `${item.package ?? '依赖'}解析到了名称不匹配的包。`,
|
|
60
|
+
INVALID_HARNESS_ROOT: () => '指定的 Harness 根目录不是有效的源码工作区。',
|
|
61
|
+
HARNESS_INSTALLATION_UNKNOWN: () => '无法定位这个 DSH Home 实际使用的 Harness。',
|
|
62
|
+
INVALID_DEPENDENCY_MAP: item => `${captured(item.message, /^(\S+) must be/) ?? '依赖字段'}必须是“包名到版本范围”的对象。`,
|
|
63
|
+
INVALID_CLIENT_DECLARATION: item => `${item.package} 的 dsh.client 声明无效。`,
|
|
64
|
+
INVALID_CLIENT_PLATFORM: item => `${item.package} 的 dsh.client.platform 必须是字符串。`,
|
|
65
|
+
INVALID_CLIENT_IMMEDIATELY: item => `${item.package} 的 dsh.client.immediately 必须是布尔值。`,
|
|
66
|
+
INVALID_CLIENT_EXTERNAL: item => `${item.package} 的 dsh.client.external 必须是字符串数组。`,
|
|
67
|
+
INVALID_CLIENT_INJECT: item => `${item.package} 的 dsh.client.inject 必须是字符串数组。`,
|
|
68
|
+
CLIENT_EXPORT_MISSING: item => `${item.package} 声明了 dsh.client,但没有导出 ./client 入口。`,
|
|
69
|
+
CLIENT_BUNDLE_MISSING: item => `${item.package} 的客户端 bundle 缺失。`,
|
|
70
|
+
CLIENT_BUNDLE_UNREADABLE: item => `无法读取 ${item.package} 的客户端 bundle。`,
|
|
71
|
+
UNDECLARED_CLIENT_REQUIRE: item => `${item.package} 引用了 ${captured(item.message, / requires (.+) but does not/) ?? '未声明模块'},但没有在 dsh.client.external 中声明。`,
|
|
72
|
+
REDUNDANT_CLIENT_EXTERNAL: item => `${item.package} 把平台模块 ${captured(item.message, / module (.+) as an external/) ?? ''} 重复声明为 external。`,
|
|
73
|
+
CLIENT_EXTERNAL_WITHOUT_SUPPLIER: item => `${item.package} 请求了 ${captured(item.message, / requests (.+), but/) ?? '客户端模块'},但当前 Harness 没有对应的模块提供方。`,
|
|
74
|
+
REMOVED_CLIENT_INJECT: item => `${item.package} 注入了 ${captured(item.message, / injects (.+), which/) ?? '已移除的模块'},但当前 Harness 源码中已不存在该模块。`,
|
|
75
|
+
LEGACY_HARNESS_PEERS: item => `${item.package} 仍声明了当前 Harness 源码中已不存在的旧包。`,
|
|
76
|
+
BUNDLE_NOT_INSTALLED: item => `配置中的 bundle ${item.package} 尚未安装。`,
|
|
77
|
+
BUNDLE_DECLARATION_MISSING: item => `${item.package} 被列为 profile bundle,但没有声明 dsh.bundle.patch。`,
|
|
78
|
+
BUNDLE_PATCH_MISSING: item => `${item.package} 的 bundle patch 文件缺失。`,
|
|
79
|
+
INVALID_PATCH_YAML: item => `${captured(item.message, /^(.+) cannot be parsed\.$/) ?? 'Patch 文件'}无法解析。`,
|
|
80
|
+
INVALID_PATCH_LIST: item => `${captured(item.message, /^(.+) must be/) ?? 'Patch 文件'}的顶层必须是由映射组成的 YAML 数组。`,
|
|
81
|
+
INVALID_SETTINGS_DOCUMENT: () => 'Harness 设置文件无法解析。',
|
|
82
|
+
INVALID_SETTINGS_ROOT: () => 'Harness 设置文件顶层必须是命名空间映射。',
|
|
83
|
+
INVALID_CREDENTIALS_DOCUMENT: () => 'Harness 凭据文件无法解析。',
|
|
84
|
+
INVALID_CREDENTIALS_ROOT: () => 'Harness 凭据文件顶层必须是映射。',
|
|
85
|
+
INVALID_CREDENTIALS_LAYOUT: () => 'Harness 凭据文件不是受支持的 version 1 结构。',
|
|
86
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `${item.package} 声明的 Harness peer 版本范围不接受当前已安装版本。`,
|
|
87
|
+
INVALID_PROFILE_NAME: item => `Profile 名称无效:${captured(item.message, /^Invalid profile name (.+)\.$/) ?? ''}`,
|
|
88
|
+
PROFILE_NOT_FOUND: item => `Profile ${captured(item.message, /^Profile (.+) does not exist\.$/) ?? ''} 不存在。`,
|
|
89
|
+
INVALID_BUNDLE_LIST: () => 'dsh.profile.bundles 必须是字符串数组。',
|
|
90
|
+
INVALID_PATCH_RELOAD: () => 'dsh.profile.patchReload 必须是 “live” 或 “startup”。',
|
|
91
|
+
DEPENDENCY_NOT_INSTALLED: item => `Profile 依赖 ${item.package} 尚未安装。`,
|
|
92
|
+
PROFILE_DEPENDENCY_VERSION_MISMATCH: item => `${item.package} 的声明版本范围与当前安装版本不兼容。`,
|
|
93
|
+
INSTALLED_BUNDLE_INACTIVE: item => `${item.package} 已作为 bundle 安装,但不在 dsh.profile.bundles 中。`,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ZH_SUGGESTIONS = {
|
|
97
|
+
INVALID_JSON_OBJECT: () => '启动 Harness 前,请恢复有效的 package manifest 对象。',
|
|
98
|
+
INVALID_JSON: () => '启动该 profile 前,请先修复 JSON。',
|
|
99
|
+
PACKAGE_NAME_MISMATCH: () => '重新安装依赖,使目录位置与包名一致。',
|
|
100
|
+
INVALID_HARNESS_ROOT: () => '诊断源码工作区时,请把 DeepSeek Harness 仓库根目录传给 --harness-root。',
|
|
101
|
+
HARNESS_INSTALLATION_UNKNOWN: () => '若使用源码工作区,请通过 --harness-root 明确指定;若使用独立 CLI,请通过 --dsh-command 指定。',
|
|
102
|
+
INVALID_DEPENDENCY_MAP: () => '管理或启动 profile 前,请先修复这个依赖字段。',
|
|
103
|
+
INVALID_CLIENT_DECLARATION: update,
|
|
104
|
+
INVALID_CLIENT_PLATFORM: update,
|
|
105
|
+
INVALID_CLIENT_IMMEDIATELY: update,
|
|
106
|
+
INVALID_CLIENT_EXTERNAL: update,
|
|
107
|
+
INVALID_CLIENT_INJECT: update,
|
|
108
|
+
CLIENT_EXPORT_MISSING: update,
|
|
109
|
+
CLIENT_BUNDLE_MISSING: update,
|
|
110
|
+
UNDECLARED_CLIENT_REQUIRE: update,
|
|
111
|
+
CLIENT_EXTERNAL_WITHOUT_SUPPLIER: update,
|
|
112
|
+
REMOVED_CLIENT_INJECT: update,
|
|
113
|
+
REDUNDANT_CLIENT_EXTERNAL: () => '插件作者应删除重复的 dsh.client.external 条目。',
|
|
114
|
+
LEGACY_HARNESS_PEERS: () => '该插件存在兼容风险,请在下次升级 Harness 前更新。',
|
|
115
|
+
BUNDLE_NOT_INSTALLED: () => '使用当前 DSH 安装补齐 profile 依赖、升级 bundle,或从 profile 中移除它。',
|
|
116
|
+
BUNDLE_DECLARATION_MISSING: () => '升级该 bundle,或把它从 dsh.profile.bundles 中移除。',
|
|
117
|
+
BUNDLE_PATCH_MISSING: () => '重新安装或升级该 bundle,或者从 profile 中移除它。',
|
|
118
|
+
INVALID_PATCH_YAML: () => '启动该 profile 前,请修复 YAML 语法。',
|
|
119
|
+
INVALID_PATCH_LIST: () => '启动该 profile 前,请修复 patch 顶层结构。',
|
|
120
|
+
INVALID_SETTINGS_DOCUMENT: () => '修复设置文件语法;Doctor 不会猜测凭据或模型配置值。',
|
|
121
|
+
INVALID_SETTINGS_ROOT: () => '把顶层标量或数组替换为映射。',
|
|
122
|
+
INVALID_CREDENTIALS_DOCUMENT: () => '只修复报告的结构;Doctor 永远不会输出或重写秘密值。',
|
|
123
|
+
INVALID_CREDENTIALS_LAYOUT: () => '迁移文档结构,不要暴露或修改秘密值。',
|
|
124
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `把 ${item.package} 更新到兼容当前 Harness 的版本。`,
|
|
125
|
+
PROFILE_NOT_FOUND: () => '先启动一次该 profile,或用当前 DSH 安装初始化它。',
|
|
126
|
+
INVALID_BUNDLE_LIST: () => '启动 Harness 前,请修复 profile manifest。',
|
|
127
|
+
INVALID_PATCH_RELOAD: () => '选择该 profile 需要的 reload 生命周期。',
|
|
128
|
+
DEPENDENCY_NOT_INSTALLED: () => '使用下方精确命令安装该 profile 声明的依赖。',
|
|
129
|
+
PROFILE_DEPENDENCY_VERSION_MISMATCH: () => '使用下方精确命令重新同步该 profile 的安装。',
|
|
130
|
+
INSTALLED_BUNDLE_INACTIVE: () => '重新执行匹配的插件添加或更新操作,或者移除未使用的依赖。',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function update(item) {
|
|
134
|
+
return `更新 ${item.package};如果没有兼容版本,再通过同一个 DSH 安装移除该插件。`
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function localizedFinding(item, language) {
|
|
138
|
+
if (language !== 'zh') return item
|
|
139
|
+
const message = ZH_MESSAGES[item.code]?.(item) ?? item.message
|
|
140
|
+
const suggestion = item.suggestion === undefined ? undefined : (ZH_SUGGESTIONS[item.code]?.(item) ?? item.suggestion)
|
|
141
|
+
const evidence = typeof item.evidence === 'string'
|
|
142
|
+
? item.evidence.replaceAll('(active ', '(当前 ').replace(/ at line (\d+), column (\d+)/g, ',第 $1 行第 $2 列')
|
|
143
|
+
: item.evidence
|
|
144
|
+
return { ...item, message, suggestion, evidence }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function languageName(language) {
|
|
148
|
+
return language === 'zh' ? '中文' : 'English'
|
|
149
|
+
}
|
package/src/repair.mjs
CHANGED
|
@@ -22,15 +22,27 @@ export function repairsFromReport(report) {
|
|
|
22
22
|
})
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function formatRepairPlan(actions) {
|
|
26
|
-
const
|
|
25
|
+
export function formatRepairPlan(actions, options = {}) {
|
|
26
|
+
const zh = options.language === 'zh'
|
|
27
|
+
const lines = [zh ? '建议执行以下修复:' : 'Proposed repairs:', '']
|
|
27
28
|
actions.forEach((action, index) => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const risk = zh ? action.risk === 'low' ? '低风险' : action.risk === 'medium' ? '中风险' : '高风险' : action.risk.toUpperCase()
|
|
30
|
+
lines.push(`${String(index + 1)}. [${risk}] ${localizedDescription(action, zh)}`)
|
|
31
|
+
if (action.kind === 'command') {
|
|
32
|
+
lines.push(` ${zh ? '命令' : 'Command'}: ${action.command.map(quoteArgument).join(' ')}`)
|
|
33
|
+
if (action.env !== undefined) lines.push(` ${zh ? '环境' : 'Environment'}: ${Object.entries(action.env).map(([key, value]) => `${key}=${quoteArgument(value)}`).join(' ')}`)
|
|
34
|
+
} else lines.push(` ${zh ? '文件' : 'File'}: ${action.file}`, ` ${zh ? '备份' : 'Backup'}: ${action.file}.dsh-doctor-<timestamp>.bak`)
|
|
31
35
|
lines.push('')
|
|
32
36
|
})
|
|
33
|
-
return `${lines.join('\n')}Apply these repairs? [y/N] `
|
|
37
|
+
return `${lines.join('\n')}${zh ? '执行这些修复吗?[y/N] ' : 'Apply these repairs? [y/N] '}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function localizedDescription(action, zh) {
|
|
41
|
+
if (!zh) return action.description
|
|
42
|
+
if (action.id.startsWith('update-package:')) return `更新 profile ${action.profile} 中的 ${action.package}。`
|
|
43
|
+
if (action.id.startsWith('install-profile:')) return `安装 profile ${action.profile} 声明的依赖。`
|
|
44
|
+
if (action.id.startsWith('activate-bundle:')) return `把 ${action.operation.name} 加入 dsh.profile.bundles。`
|
|
45
|
+
return action.description
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
function quoteArgument(value) {
|
|
@@ -58,7 +70,10 @@ function applyJsonEdit(action) {
|
|
|
58
70
|
|
|
59
71
|
function applyCommand(action) {
|
|
60
72
|
const [command, ...args] = action.command
|
|
61
|
-
const result = crossSpawn.sync(command, args, {
|
|
73
|
+
const result = crossSpawn.sync(command, args, {
|
|
74
|
+
stdio: 'inherit',
|
|
75
|
+
env: action.env === undefined ? process.env : { ...process.env, ...action.env },
|
|
76
|
+
})
|
|
62
77
|
if (result.error != null) throw result.error
|
|
63
78
|
if (result.status !== 0) throw new Error(`${command} exited with status ${String(result.status)}`)
|
|
64
79
|
return { id: action.id, status: 'applied' }
|