@bruc3van/dsh-doctor 0.1.0 → 0.1.2
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 +46 -10
- package/src/doctor.mjs +253 -74
- package/src/i18n.mjs +155 -0
- package/src/repair.mjs +43 -10
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.2",
|
|
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,22 @@ 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
|
+
const plan = formatRepairPlan(actions, { language, prompt: !confirmed })
|
|
111
|
+
if (confirmed) {
|
|
112
|
+
process.stderr.write(`${plan}\n`)
|
|
113
|
+
} else {
|
|
77
114
|
if (!process.stdin.isTTY) throw new Error('--fix needs an interactive terminal or explicit --yes')
|
|
78
|
-
const prompt = formatRepairPlan(actions)
|
|
79
115
|
const reader = createInterface({ input: process.stdin, output: process.stderr })
|
|
80
|
-
const answer = await reader.question(
|
|
116
|
+
const answer = await reader.question(plan)
|
|
81
117
|
reader.close()
|
|
82
|
-
confirmed = /^y(?:es)
|
|
118
|
+
confirmed = /^(?:y(?:es)?|是|确认)$/i.test(answer.trim())
|
|
83
119
|
}
|
|
84
120
|
if (confirmed) {
|
|
85
121
|
repairs = applyRepairs(actions)
|
|
@@ -90,7 +126,7 @@ async function main() {
|
|
|
90
126
|
const output = options.fix ? { ...report, repairs } : report
|
|
91
127
|
process.stdout.write(options.json
|
|
92
128
|
? `${JSON.stringify(output, null, 2)}\n`
|
|
93
|
-
: `${formatReport(report, { color: process.stdout.isTTY })}${repairs.length > 0 ?
|
|
129
|
+
: `${formatReport(report, { color: process.stdout.isTTY, language })}${repairs.length > 0 ? `${language === 'zh' ? '修复结果' : 'Repairs'}: ${JSON.stringify(repairs, null, 2)}\n` : ''}`)
|
|
94
130
|
process.exitCode = repairs.some(item => item.status === 'failed') ? 2 : report.summary.errors > 0 ? 1 : 0
|
|
95
131
|
}
|
|
96
132
|
}
|
package/src/doctor.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
|
2
|
+
import { builtinModules } from 'node:module'
|
|
2
3
|
import { homedir } from 'node:os'
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
4
|
+
import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
4
5
|
import yaml from 'js-yaml'
|
|
5
6
|
import semver from 'semver'
|
|
6
7
|
import { parseDocument } from 'yaml'
|
|
8
|
+
import { languageName, localizedFinding } from './i18n.mjs'
|
|
7
9
|
|
|
8
10
|
export const PLATFORM_MODULES = new Set([
|
|
9
11
|
'react',
|
|
@@ -15,6 +17,7 @@ export const PLATFORM_MODULES = new Set([
|
|
|
15
17
|
'@deepseek-ai/dsh-client-ui-slots',
|
|
16
18
|
'@deepseek-ai/dsh-client-ui-primitives',
|
|
17
19
|
])
|
|
20
|
+
const BUILTIN_MODULES = new Set(builtinModules.map(name => name.replace(/^node:/, '')))
|
|
18
21
|
|
|
19
22
|
const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 }
|
|
20
23
|
const JS_EXPRESSION = new yaml.Type('tag:yaml.org,2002:js', {
|
|
@@ -70,6 +73,7 @@ function readJson(file, subject, findings) {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
function packagePathParts(name) {
|
|
76
|
+
if (typeof name !== 'string') return undefined
|
|
73
77
|
if (name.startsWith('@')) {
|
|
74
78
|
const parts = name.split('/')
|
|
75
79
|
return parts.length === 2
|
|
@@ -93,7 +97,7 @@ function packageManifestAt(directory, expectedName, findings, source) {
|
|
|
93
97
|
suggestion: 'Reinstall the dependency so its directory and package name agree.',
|
|
94
98
|
}))
|
|
95
99
|
}
|
|
96
|
-
return { directory, file, manifest }
|
|
100
|
+
return { directory, file, manifest, requestedName: expectedName }
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
function findHarnessRoot(start) {
|
|
@@ -108,6 +112,120 @@ function findHarnessRoot(start) {
|
|
|
108
112
|
}
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
function executable(file) {
|
|
116
|
+
try {
|
|
117
|
+
accessSync(file, process.platform === 'win32' ? constants.F_OK : constants.X_OK)
|
|
118
|
+
return true
|
|
119
|
+
} catch {
|
|
120
|
+
return false
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function regularFile(file) {
|
|
125
|
+
try {
|
|
126
|
+
return statSync(file).isFile()
|
|
127
|
+
} catch {
|
|
128
|
+
return false
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function executableOnPath(name, env) {
|
|
133
|
+
const path = env.PATH ?? env.Path ?? env.path ?? ''
|
|
134
|
+
const extensions = process.platform === 'win32'
|
|
135
|
+
? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';')
|
|
136
|
+
: ['']
|
|
137
|
+
for (const directory of path.split(delimiter).filter(Boolean)) {
|
|
138
|
+
for (const extension of extensions) {
|
|
139
|
+
const candidate = join(directory, process.platform === 'win32' ? `${name}${extension}` : name)
|
|
140
|
+
if (executable(candidate)) return candidate
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function manifestBin(packageDir) {
|
|
147
|
+
const file = join(packageDir, 'package.json')
|
|
148
|
+
if (!existsSync(file)) return undefined
|
|
149
|
+
try {
|
|
150
|
+
const manifest = JSON.parse(readFileSync(file, 'utf8'))
|
|
151
|
+
if (manifest?.name !== '@deepseek-ai/dsh') return undefined
|
|
152
|
+
const declared = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.dsh
|
|
153
|
+
if (typeof declared !== 'string') return undefined
|
|
154
|
+
const bin = resolve(packageDir, declared)
|
|
155
|
+
const rel = relative(packageDir, bin)
|
|
156
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel) || !existsSync(bin) || !statSync(bin).isFile()) return undefined
|
|
157
|
+
return { bin, version: manifest.version }
|
|
158
|
+
} catch {
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function localDshPackage(start) {
|
|
164
|
+
let current = resolve(start)
|
|
165
|
+
while (true) {
|
|
166
|
+
const packageDir = join(current, 'node_modules', '@deepseek-ai', 'dsh')
|
|
167
|
+
if (manifestBin(packageDir) !== undefined) return packageDir
|
|
168
|
+
const parent = dirname(current)
|
|
169
|
+
if (parent === current) return undefined
|
|
170
|
+
current = parent
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function commandFromValue(value, env, cwd) {
|
|
175
|
+
const expanded = value === '~'
|
|
176
|
+
? homedir()
|
|
177
|
+
: value.startsWith('~/') || value.startsWith('~\\')
|
|
178
|
+
? resolve(homedir(), value.slice(2))
|
|
179
|
+
: value
|
|
180
|
+
const looksLikePath = isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\') || expanded.startsWith('.')
|
|
181
|
+
const file = looksLikePath ? resolve(cwd, expanded) : executableOnPath(expanded, env)
|
|
182
|
+
if (file === undefined) return undefined
|
|
183
|
+
if (!regularFile(file)) return undefined
|
|
184
|
+
if (/\.(?:c?js|mjs)$/i.test(file)) return { command: [process.execPath, file], path: file }
|
|
185
|
+
return executable(file) ? { command: [file], path: file } : undefined
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveDshCli(options, harness, home) {
|
|
189
|
+
const env = options.env ?? process.env
|
|
190
|
+
const cwd = options.cwd ?? process.cwd()
|
|
191
|
+
const explicit = options.dshCommand ?? env.DSH_DOCTOR_DSH_COMMAND
|
|
192
|
+
if (explicit !== undefined) {
|
|
193
|
+
const resolved = commandFromValue(explicit, env, cwd)
|
|
194
|
+
if (resolved === undefined) throw new Error(`DSH command ${JSON.stringify(explicit)} does not exist or is not executable`)
|
|
195
|
+
return { ...resolved, source: 'explicit' }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (options.harnessRoot !== undefined && harness.root !== undefined) {
|
|
199
|
+
for (const packageDir of [harness.root, join(harness.root, 'apps', 'cli')]) {
|
|
200
|
+
const bin = manifestBin(packageDir)
|
|
201
|
+
if (bin !== undefined) return { command: [process.execPath, bin.bin], path: bin.bin, source: 'checkout', version: bin.version }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const sharedPackage = join(home, 'profiles', 'node_modules', '@deepseek-ai', 'dsh')
|
|
206
|
+
if (existsSync(sharedPackage)) {
|
|
207
|
+
const bin = manifestBin(realpathSync(sharedPackage))
|
|
208
|
+
if (bin !== undefined) return { command: [process.execPath, bin.bin], path: bin.bin, source: 'profile', version: bin.version }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const localPackage = localDshPackage(cwd)
|
|
212
|
+
if (localPackage !== undefined) {
|
|
213
|
+
const bin = manifestBin(localPackage)
|
|
214
|
+
return { command: [process.execPath, bin.bin], path: bin.bin, source: 'project', version: bin.version }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const pathCommand = executableOnPath('dsh', env)
|
|
218
|
+
if (pathCommand !== undefined) return { command: [pathCommand], path: pathCommand, source: 'path' }
|
|
219
|
+
|
|
220
|
+
if (harness.root !== undefined) {
|
|
221
|
+
for (const packageDir of [harness.root, join(harness.root, 'apps', 'cli')]) {
|
|
222
|
+
const bin = manifestBin(packageDir)
|
|
223
|
+
if (bin !== undefined) return { command: [process.execPath, bin.bin], path: bin.bin, source: 'checkout', version: bin.version }
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return undefined
|
|
227
|
+
}
|
|
228
|
+
|
|
111
229
|
function workspacePackageDirectories(root) {
|
|
112
230
|
const directories = []
|
|
113
231
|
const addChildren = (parent) => {
|
|
@@ -135,7 +253,14 @@ function indexWorkspace(root, findings) {
|
|
|
135
253
|
for (const directory of workspacePackageDirectories(root)) {
|
|
136
254
|
const file = join(directory, 'package.json')
|
|
137
255
|
if (!existsSync(file)) continue
|
|
138
|
-
const
|
|
256
|
+
const manifestFindings = []
|
|
257
|
+
const manifest = readJson(file, 'Harness workspace package manifest', manifestFindings)
|
|
258
|
+
for (const item of manifestFindings) {
|
|
259
|
+
findings.push(finding('warning', 'INVALID_WORKSPACE_MANIFEST', 'A Harness workspace package manifest was ignored because it is invalid.', {
|
|
260
|
+
evidence: item.evidence ?? file,
|
|
261
|
+
suggestion: 'Repair this workspace package manifest to include it in compatibility checks.',
|
|
262
|
+
}))
|
|
263
|
+
}
|
|
139
264
|
if (typeof manifest?.name === 'string') packages.set(manifest.name, { directory, file, manifest })
|
|
140
265
|
}
|
|
141
266
|
return packages
|
|
@@ -149,10 +274,10 @@ function resolveHarnessContext(home, explicitRoot, findings) {
|
|
|
149
274
|
evidence: root,
|
|
150
275
|
suggestion: 'Pass the DeepSeek Harness repository root to --harness-root.',
|
|
151
276
|
}))
|
|
152
|
-
return { root, packages: new Map(), version: undefined, authoritative:
|
|
277
|
+
return { root, packages: new Map(), version: undefined, authoritative: false }
|
|
153
278
|
}
|
|
154
279
|
const manifest = readJson(join(root, 'package.json'), 'Harness root manifest', findings)
|
|
155
|
-
return { root, packages: indexWorkspace(root, findings), version: manifest?.version, authoritative:
|
|
280
|
+
return { root, packages: indexWorkspace(root, findings), version: manifest?.version, authoritative: manifest !== undefined }
|
|
156
281
|
}
|
|
157
282
|
|
|
158
283
|
const sharedDsh = join(home, 'profiles', 'node_modules', '@deepseek-ai', 'dsh')
|
|
@@ -194,8 +319,9 @@ function packageResolver(profileDir, home, harnessPackages, findings) {
|
|
|
194
319
|
return resolved
|
|
195
320
|
}
|
|
196
321
|
const workspace = harnessPackages.get(name)
|
|
197
|
-
|
|
198
|
-
|
|
322
|
+
const resolved = workspace === undefined ? undefined : { ...workspace, requestedName: name }
|
|
323
|
+
cache.set(name, resolved)
|
|
324
|
+
return resolved
|
|
199
325
|
}
|
|
200
326
|
}
|
|
201
327
|
|
|
@@ -238,14 +364,14 @@ function dependencyEntries(value, field, file, findings) {
|
|
|
238
364
|
return Object.entries(record)
|
|
239
365
|
}
|
|
240
366
|
|
|
241
|
-
function updateRepair(profile, name) {
|
|
242
|
-
return
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
367
|
+
function updateRepair(profile, name, commandRepair) {
|
|
368
|
+
if (packagePathParts(name) === undefined) return undefined
|
|
369
|
+
return commandRepair(
|
|
370
|
+
`update-package:${name}`,
|
|
371
|
+
`Update ${name} in profile ${profile}.`,
|
|
372
|
+
['plugin', '--profile', profile, 'update', name],
|
|
373
|
+
{ package: name, profile },
|
|
374
|
+
)
|
|
249
375
|
}
|
|
250
376
|
|
|
251
377
|
function safePackageFile(packageDir, exported) {
|
|
@@ -259,11 +385,11 @@ function safePackageFile(packageDir, exported) {
|
|
|
259
385
|
export function extractStaticRequires(source) {
|
|
260
386
|
const values = new Set()
|
|
261
387
|
const code = codePositions(source)
|
|
262
|
-
const pattern =
|
|
388
|
+
const pattern = /(?<![\w$.])require\s*\(\s*(['"])([^'"\\\r\n]+)\1\s*\)/g
|
|
263
389
|
for (const match of source.matchAll(pattern)) {
|
|
264
390
|
if (!code[match.index]) continue
|
|
265
391
|
const specifier = match[2]
|
|
266
|
-
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('node:')) continue
|
|
392
|
+
if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('node:') || BUILTIN_MODULES.has(specifier)) continue
|
|
267
393
|
if (packagePathParts(specifier) !== undefined || specifier.startsWith('@')) values.add(specifier)
|
|
268
394
|
}
|
|
269
395
|
return [...values].sort()
|
|
@@ -316,10 +442,10 @@ function stringArray(value) {
|
|
|
316
442
|
|
|
317
443
|
function inspectClientPackage(record, context) {
|
|
318
444
|
const {
|
|
319
|
-
findings, harnessPackages, harnessPackagesAuthoritative, profile, resolvePackage,
|
|
445
|
+
commandRepair, findings, harnessPackages, harnessPackagesAuthoritative, profile, resolvePackage,
|
|
320
446
|
} = context
|
|
321
|
-
const name = record.manifest.name
|
|
322
|
-
const disableSuggestion = `Upgrade ${name}; if no compatible release exists,
|
|
447
|
+
const name = record.requestedName ?? record.manifest.name
|
|
448
|
+
const disableSuggestion = `Upgrade ${name}; if no compatible release exists, remove it through the same DSH installation.`
|
|
323
449
|
const declaration = record.manifest?.dsh?.client
|
|
324
450
|
if (declaration === undefined) return
|
|
325
451
|
if (declaration === null || typeof declaration !== 'object') {
|
|
@@ -327,7 +453,7 @@ function inspectClientPackage(record, context) {
|
|
|
327
453
|
package: name,
|
|
328
454
|
evidence: record.file,
|
|
329
455
|
suggestion: disableSuggestion,
|
|
330
|
-
repair: updateRepair(profile, name),
|
|
456
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
331
457
|
}))
|
|
332
458
|
return
|
|
333
459
|
}
|
|
@@ -337,7 +463,7 @@ function inspectClientPackage(record, context) {
|
|
|
337
463
|
package: name,
|
|
338
464
|
evidence: record.file,
|
|
339
465
|
suggestion: disableSuggestion,
|
|
340
|
-
repair: updateRepair(profile, name),
|
|
466
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
341
467
|
}))
|
|
342
468
|
return
|
|
343
469
|
}
|
|
@@ -346,7 +472,7 @@ function inspectClientPackage(record, context) {
|
|
|
346
472
|
package: name,
|
|
347
473
|
evidence: record.file,
|
|
348
474
|
suggestion: disableSuggestion,
|
|
349
|
-
repair: updateRepair(profile, name),
|
|
475
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
350
476
|
}))
|
|
351
477
|
}
|
|
352
478
|
if (declaration.platform !== 'web') return
|
|
@@ -358,7 +484,7 @@ function inspectClientPackage(record, context) {
|
|
|
358
484
|
package: name,
|
|
359
485
|
evidence: record.file,
|
|
360
486
|
suggestion: disableSuggestion,
|
|
361
|
-
repair: updateRepair(profile, name),
|
|
487
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
362
488
|
}))
|
|
363
489
|
}
|
|
364
490
|
if (declaration.inject !== undefined && inject === undefined) {
|
|
@@ -366,7 +492,7 @@ function inspectClientPackage(record, context) {
|
|
|
366
492
|
package: name,
|
|
367
493
|
evidence: record.file,
|
|
368
494
|
suggestion: disableSuggestion,
|
|
369
|
-
repair: updateRepair(profile, name),
|
|
495
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
370
496
|
}))
|
|
371
497
|
}
|
|
372
498
|
|
|
@@ -380,12 +506,12 @@ function inspectClientPackage(record, context) {
|
|
|
380
506
|
return
|
|
381
507
|
}
|
|
382
508
|
const file = safePackageFile(record.directory, exported)
|
|
383
|
-
if (file === undefined || !
|
|
509
|
+
if (file === undefined || !regularFile(file)) {
|
|
384
510
|
findings.push(finding('error', 'CLIENT_BUNDLE_MISSING', `${name} client bundle is missing.`, {
|
|
385
511
|
package: name,
|
|
386
512
|
evidence: file ?? `${record.file}: exports["./client"] = ${JSON.stringify(exported)}`,
|
|
387
|
-
suggestion: `Reinstall or rebuild ${name}; if it remains broken,
|
|
388
|
-
repair: updateRepair(profile, name),
|
|
513
|
+
suggestion: `Reinstall or rebuild ${name}; if it remains broken, remove it through the same DSH installation.`,
|
|
514
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
389
515
|
}))
|
|
390
516
|
return
|
|
391
517
|
}
|
|
@@ -409,7 +535,7 @@ function inspectClientPackage(record, context) {
|
|
|
409
535
|
package: name,
|
|
410
536
|
evidence: file,
|
|
411
537
|
suggestion: disableSuggestion,
|
|
412
|
-
repair: updateRepair(profile, name),
|
|
538
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
413
539
|
}))
|
|
414
540
|
}
|
|
415
541
|
}
|
|
@@ -432,7 +558,7 @@ function inspectClientPackage(record, context) {
|
|
|
432
558
|
package: name,
|
|
433
559
|
evidence: record.file,
|
|
434
560
|
suggestion: disableSuggestion,
|
|
435
|
-
repair: updateRepair(profile, name),
|
|
561
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
436
562
|
}))
|
|
437
563
|
}
|
|
438
564
|
}
|
|
@@ -445,7 +571,7 @@ function inspectClientPackage(record, context) {
|
|
|
445
571
|
package: name,
|
|
446
572
|
evidence: record.file,
|
|
447
573
|
suggestion: disableSuggestion,
|
|
448
|
-
repair: updateRepair(profile, name),
|
|
574
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
449
575
|
}))
|
|
450
576
|
}
|
|
451
577
|
}
|
|
@@ -467,7 +593,7 @@ function inspectBundle(name, record, findings) {
|
|
|
467
593
|
if (record === undefined) {
|
|
468
594
|
findings.push(finding('error', 'BUNDLE_NOT_INSTALLED', `Profile bundle ${name} is not installed.`, {
|
|
469
595
|
package: name,
|
|
470
|
-
suggestion:
|
|
596
|
+
suggestion: 'Install the profile dependencies with the active DSH installation, upgrade the bundle, or remove it from the profile.',
|
|
471
597
|
}))
|
|
472
598
|
return
|
|
473
599
|
}
|
|
@@ -481,7 +607,7 @@ function inspectBundle(name, record, findings) {
|
|
|
481
607
|
return
|
|
482
608
|
}
|
|
483
609
|
const file = safePackageFile(record.directory, patch)
|
|
484
|
-
if (file === undefined || !
|
|
610
|
+
if (file === undefined || !regularFile(file)) {
|
|
485
611
|
findings.push(finding('error', 'BUNDLE_PATCH_MISSING', `${name} bundle patch is missing.`, {
|
|
486
612
|
package: name,
|
|
487
613
|
evidence: file ?? `${record.file}: dsh.bundle.patch = ${JSON.stringify(patch)}`,
|
|
@@ -580,7 +706,7 @@ function inspectPatchFileIfPresent(file, subject, findings) {
|
|
|
580
706
|
}
|
|
581
707
|
|
|
582
708
|
function inspectCompatibility(record, context) {
|
|
583
|
-
const { findings, harnessPackages, profile, resolvePackage } = context
|
|
709
|
+
const { commandRepair, findings, harnessPackages, profile, resolvePackage } = context
|
|
584
710
|
const peers = dependencyEntries(record.manifest.peerDependencies, 'peerDependencies', record.file, findings)
|
|
585
711
|
const mismatches = []
|
|
586
712
|
for (const [name, range] of peers) {
|
|
@@ -593,18 +719,19 @@ function inspectCompatibility(record, context) {
|
|
|
593
719
|
mismatches.push(`${name} ${range} (active ${version})`)
|
|
594
720
|
}
|
|
595
721
|
if (mismatches.length > 0) {
|
|
596
|
-
|
|
597
|
-
|
|
722
|
+
const name = record.requestedName ?? record.manifest.name
|
|
723
|
+
findings.push(finding('warning', 'HARNESS_PEER_VERSION_MISMATCH', `${name} has Harness peer ranges that do not accept the active versions.`, {
|
|
724
|
+
package: name,
|
|
598
725
|
evidence: mismatches.join(', '),
|
|
599
|
-
suggestion: `Update ${
|
|
600
|
-
repair: updateRepair(profile,
|
|
726
|
+
suggestion: `Update ${name} to a release compatible with the active Harness.`,
|
|
727
|
+
repair: updateRepair(profile, name, commandRepair),
|
|
601
728
|
}))
|
|
602
729
|
}
|
|
603
730
|
}
|
|
604
731
|
|
|
605
732
|
export function defaultDshHome(env = process.env) {
|
|
606
|
-
const configured = env.DSH_HOME
|
|
607
|
-
if (configured !== undefined && configured.
|
|
733
|
+
const configured = env.DSH_HOME?.trim()
|
|
734
|
+
if (configured !== undefined && configured.length > 0) {
|
|
608
735
|
if (configured === '~') return homedir()
|
|
609
736
|
if (configured.startsWith('~/') || configured.startsWith('~\\')) return resolve(homedir(), configured.slice(2))
|
|
610
737
|
return resolve(configured)
|
|
@@ -625,7 +752,7 @@ export function diagnose(options = {}) {
|
|
|
625
752
|
if (!existsSync(profileManifestFile)) {
|
|
626
753
|
findings.push(finding('error', 'PROFILE_NOT_FOUND', `Profile ${profile} does not exist.`, {
|
|
627
754
|
evidence: profileDir,
|
|
628
|
-
suggestion:
|
|
755
|
+
suggestion: 'Start the profile once or initialize it with the plugin install command from the same DSH installation.',
|
|
629
756
|
}))
|
|
630
757
|
return finish({ home, profile, profileDir, harness: {}, packages: [] }, findings)
|
|
631
758
|
}
|
|
@@ -635,19 +762,50 @@ export function diagnose(options = {}) {
|
|
|
635
762
|
}
|
|
636
763
|
|
|
637
764
|
const harness = resolveHarnessContext(home, options.harnessRoot, findings)
|
|
765
|
+
const dshCli = resolveDshCli(options, harness, home)
|
|
766
|
+
let commandRepairNeeded = false
|
|
767
|
+
const commandRepair = (id, description, args, metadata = {}) => {
|
|
768
|
+
commandRepairNeeded = true
|
|
769
|
+
return dshCli === undefined ? undefined : {
|
|
770
|
+
id,
|
|
771
|
+
kind: 'command',
|
|
772
|
+
risk: 'medium',
|
|
773
|
+
description,
|
|
774
|
+
command: [...dshCli.command, ...args],
|
|
775
|
+
env: { DSH_HOME: home },
|
|
776
|
+
...metadata,
|
|
777
|
+
}
|
|
778
|
+
}
|
|
638
779
|
const resolvePackage = packageResolver(profileDir, home, harness.packages, findings)
|
|
639
780
|
const resolveBundle = bundleResolver(profileDir, home, harness.packages, findings)
|
|
640
781
|
const dependencyEntriesList = dependencyEntries(profileManifest.dependencies, 'dependencies', profileManifestFile, findings)
|
|
641
782
|
const dependencyNames = dependencyEntriesList.map(([name]) => name)
|
|
642
|
-
const
|
|
783
|
+
const dshConfig = profileManifest.dsh
|
|
784
|
+
const dshConfigValid = dshConfig === undefined || objectRecord(dshConfig) !== undefined
|
|
785
|
+
if (!dshConfigValid) {
|
|
786
|
+
findings.push(finding('error', 'INVALID_DSH_CONFIGURATION', 'dsh must be an object when present.', {
|
|
787
|
+
evidence: profileManifestFile,
|
|
788
|
+
suggestion: 'Repair the dsh configuration object before starting Harness.',
|
|
789
|
+
}))
|
|
790
|
+
}
|
|
791
|
+
const profileConfigValue = objectRecord(dshConfig)?.profile
|
|
792
|
+
const profileConfigValid = profileConfigValue === undefined || objectRecord(profileConfigValue) !== undefined
|
|
793
|
+
if (dshConfigValid && !profileConfigValid) {
|
|
794
|
+
findings.push(finding('error', 'INVALID_PROFILE_CONFIGURATION', 'dsh.profile must be an object when present.', {
|
|
795
|
+
evidence: profileManifestFile,
|
|
796
|
+
suggestion: 'Repair the dsh.profile configuration object before starting Harness.',
|
|
797
|
+
}))
|
|
798
|
+
}
|
|
799
|
+
const profileConfig = objectRecord(profileConfigValue)
|
|
800
|
+
const bundles = profileConfig?.bundles
|
|
643
801
|
if (bundles !== undefined && (!Array.isArray(bundles) || !bundles.every(item => typeof item === 'string'))) {
|
|
644
802
|
findings.push(finding('error', 'INVALID_BUNDLE_LIST', 'dsh.profile.bundles must be a string array.', {
|
|
645
803
|
evidence: profileManifestFile,
|
|
646
804
|
suggestion: 'Repair the profile manifest before starting Harness.',
|
|
647
805
|
}))
|
|
648
806
|
}
|
|
649
|
-
const bundleNames = Array.isArray(bundles) ? bundles.filter(item => typeof item === 'string') : []
|
|
650
|
-
const patchReload =
|
|
807
|
+
const bundleNames = Array.isArray(bundles) ? [...new Set(bundles.filter(item => typeof item === 'string'))] : []
|
|
808
|
+
const patchReload = profileConfig?.patchReload
|
|
651
809
|
if (patchReload !== undefined && patchReload !== 'live' && patchReload !== 'startup') {
|
|
652
810
|
findings.push(finding('error', 'INVALID_PATCH_RELOAD', 'dsh.profile.patchReload must be "live" or "startup".', {
|
|
653
811
|
evidence: profileManifestFile,
|
|
@@ -671,14 +829,13 @@ export function diagnose(options = {}) {
|
|
|
671
829
|
findings.push(finding('error', 'DEPENDENCY_NOT_INSTALLED', `Profile dependency ${name} is not installed.`, {
|
|
672
830
|
package: name,
|
|
673
831
|
evidence: profileManifestFile,
|
|
674
|
-
suggestion:
|
|
675
|
-
repair:
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
},
|
|
832
|
+
suggestion: 'Use the exact command shown below to install the declared profile dependencies.',
|
|
833
|
+
repair: commandRepair(
|
|
834
|
+
`install-profile:${profile}`,
|
|
835
|
+
`Install the declared dependencies for profile ${profile}.`,
|
|
836
|
+
['plugin', '--profile', profile, 'install'],
|
|
837
|
+
{ profile },
|
|
838
|
+
),
|
|
682
839
|
}))
|
|
683
840
|
continue
|
|
684
841
|
}
|
|
@@ -690,17 +847,17 @@ export function diagnose(options = {}) {
|
|
|
690
847
|
findings.push(finding('warning', 'PROFILE_DEPENDENCY_VERSION_MISMATCH', `Profile requests ${name} ${declared}, but ${installed} is installed.`, {
|
|
691
848
|
package: name,
|
|
692
849
|
evidence: record.file,
|
|
693
|
-
suggestion:
|
|
694
|
-
repair:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
},
|
|
850
|
+
suggestion: 'Use the exact command shown below to reconcile the profile installation.',
|
|
851
|
+
repair: commandRepair(
|
|
852
|
+
`install-profile:${profile}`,
|
|
853
|
+
`Install the declared dependencies for profile ${profile}.`,
|
|
854
|
+
['plugin', '--profile', profile, 'install'],
|
|
855
|
+
{ profile },
|
|
856
|
+
),
|
|
701
857
|
}))
|
|
702
858
|
}
|
|
703
|
-
if (
|
|
859
|
+
if (dshConfigValid && profileConfigValid
|
|
860
|
+
&& record.manifest?.dsh?.bundle?.patch !== undefined && !bundleNames.includes(name)) {
|
|
704
861
|
findings.push(finding('warning', 'INSTALLED_BUNDLE_INACTIVE', `${name} is installed as a bundle but is absent from dsh.profile.bundles.`, {
|
|
705
862
|
package: name,
|
|
706
863
|
evidence: profileManifestFile,
|
|
@@ -729,6 +886,7 @@ export function diagnose(options = {}) {
|
|
|
729
886
|
for (const record of thirdPartyRecords) {
|
|
730
887
|
inspectClientPackage(record, {
|
|
731
888
|
findings,
|
|
889
|
+
commandRepair,
|
|
732
890
|
harnessPackages: harness.packages,
|
|
733
891
|
harnessPackagesAuthoritative: harness.authoritative,
|
|
734
892
|
profile,
|
|
@@ -736,6 +894,7 @@ export function diagnose(options = {}) {
|
|
|
736
894
|
})
|
|
737
895
|
inspectCompatibility(record, {
|
|
738
896
|
findings,
|
|
897
|
+
commandRepair,
|
|
739
898
|
profile,
|
|
740
899
|
resolvePackage,
|
|
741
900
|
harnessPackages: harness.packages,
|
|
@@ -747,8 +906,11 @@ export function diagnose(options = {}) {
|
|
|
747
906
|
profile,
|
|
748
907
|
profileDir,
|
|
749
908
|
harness: { root: harness.root, version: harness.version },
|
|
909
|
+
dshCli: dshCli === undefined
|
|
910
|
+
? { available: false, commandRepairNeeded }
|
|
911
|
+
: { available: true, commandRepairNeeded, ...dshCli },
|
|
750
912
|
packages: thirdPartyRecords.map(record => ({
|
|
751
|
-
name: record.manifest.name,
|
|
913
|
+
name: record.requestedName ?? record.manifest.name,
|
|
752
914
|
version: record.manifest.version,
|
|
753
915
|
directory: record.directory,
|
|
754
916
|
client: record.manifest?.dsh?.client !== undefined,
|
|
@@ -772,32 +934,49 @@ function finish(context, findings) {
|
|
|
772
934
|
|
|
773
935
|
export function formatReport(report, options = {}) {
|
|
774
936
|
const color = options.color ?? false
|
|
937
|
+
const language = options.language ?? 'en'
|
|
938
|
+
const zh = language === 'zh'
|
|
775
939
|
const paint = (severity, value) => {
|
|
776
940
|
if (!color) return value
|
|
777
941
|
const code = severity === 'error' ? 31 : severity === 'warning' ? 33 : 36
|
|
778
942
|
return `\u001B[${code}m${value}\u001B[0m`
|
|
779
943
|
}
|
|
944
|
+
const quote = value => /^[a-zA-Z0-9_@./:\\-]+$/.test(value) ? value : JSON.stringify(value)
|
|
945
|
+
const cli = report.context.dshCli
|
|
946
|
+
const cliText = cli?.available
|
|
947
|
+
? `${cli.source}${cli.version ? ` ${cli.version}` : ''} (${cli.command.map(quote).join(' ')})`
|
|
948
|
+
: zh ? '未找到(命令型修复已禁用)' : 'not found (command-based repairs disabled)'
|
|
780
949
|
const lines = [
|
|
781
950
|
'DSH Doctor',
|
|
782
|
-
|
|
783
|
-
|
|
951
|
+
`${zh ? 'Profile' : 'Profile'}: ${report.context.profile}`,
|
|
952
|
+
`${zh ? 'DSH 主目录' : 'Home'}: ${report.context.home}`,
|
|
784
953
|
`Harness: ${report.context.harness.version ?? 'unknown'}${report.context.harness.root ? ` (${report.context.harness.root})` : ''}`,
|
|
785
|
-
|
|
954
|
+
`${zh ? 'DSH CLI' : 'DSH CLI'}: ${cliText}`,
|
|
955
|
+
`${zh ? '已检查第三方包' : 'Checked third-party packages'}: ${String(report.context.packages.length)}`,
|
|
956
|
+
`${zh ? '输出语言' : 'Output language'}: ${languageName(language)}`,
|
|
786
957
|
'',
|
|
787
958
|
]
|
|
788
959
|
if (report.findings.length === 0) {
|
|
789
|
-
lines.push(paint('info', 'OK No problems found by the MVP checks.'))
|
|
960
|
+
lines.push(paint('info', zh ? '正常 当前检查范围内未发现问题。' : 'OK No problems found by the MVP checks.'))
|
|
790
961
|
} else {
|
|
791
|
-
for (const
|
|
792
|
-
const
|
|
962
|
+
for (const original of report.findings) {
|
|
963
|
+
const item = localizedFinding(original, language)
|
|
964
|
+
const label = zh
|
|
965
|
+
? item.severity === 'error' ? '错误' : item.severity === 'warning' ? '警告' : '信息'
|
|
966
|
+
: item.severity === 'error' ? 'ERROR' : item.severity === 'warning' ? 'WARN ' : 'INFO '
|
|
793
967
|
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}`)
|
|
968
|
+
if (item.package !== undefined) lines.push(` ${zh ? '包' : 'Package'}: ${item.package}`)
|
|
969
|
+
if (item.evidence !== undefined) lines.push(` ${zh ? '证据' : 'Evidence'}: ${item.evidence}`)
|
|
970
|
+
if (item.suggestion !== undefined) lines.push(` ${zh ? '建议' : 'Action'}: ${item.suggestion}`)
|
|
971
|
+
if (item.repair?.kind === 'command') lines.push(` ${zh ? '更新命令' : 'Update command'}: ${item.repair.command.map(quote).join(' ')}`)
|
|
797
972
|
lines.push('')
|
|
798
973
|
}
|
|
799
974
|
}
|
|
800
|
-
lines.push(
|
|
801
|
-
|
|
975
|
+
lines.push(zh
|
|
976
|
+
? `汇总:${String(report.summary.errors)} 个错误,${String(report.summary.warnings)} 个警告`
|
|
977
|
+
: `Summary: ${String(report.summary.errors)} error(s), ${String(report.summary.warnings)} warning(s)`)
|
|
978
|
+
if (report.summary.errors > 0) lines.push(zh
|
|
979
|
+
? 'Harness 可能无法启动。请优先更新或停用产生错误的插件。'
|
|
980
|
+
: 'Harness may fail to start. Upgrade or disable the error-producing plugin first.')
|
|
802
981
|
return `${lines.join('\n')}\n`
|
|
803
982
|
}
|
package/src/i18n.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
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
|
+
INVALID_WORKSPACE_MANIFEST: () => '已忽略一个无效的 Harness workspace 包清单。',
|
|
62
|
+
HARNESS_INSTALLATION_UNKNOWN: () => '无法定位这个 DSH Home 实际使用的 Harness。',
|
|
63
|
+
INVALID_DEPENDENCY_MAP: item => `${captured(item.message, /^(\S+) must be/) ?? '依赖字段'}必须是“包名到版本范围”的对象。`,
|
|
64
|
+
INVALID_CLIENT_DECLARATION: item => `${item.package} 的 dsh.client 声明无效。`,
|
|
65
|
+
INVALID_CLIENT_PLATFORM: item => `${item.package} 的 dsh.client.platform 必须是字符串。`,
|
|
66
|
+
INVALID_CLIENT_IMMEDIATELY: item => `${item.package} 的 dsh.client.immediately 必须是布尔值。`,
|
|
67
|
+
INVALID_CLIENT_EXTERNAL: item => `${item.package} 的 dsh.client.external 必须是字符串数组。`,
|
|
68
|
+
INVALID_CLIENT_INJECT: item => `${item.package} 的 dsh.client.inject 必须是字符串数组。`,
|
|
69
|
+
CLIENT_EXPORT_MISSING: item => `${item.package} 声明了 dsh.client,但没有导出 ./client 入口。`,
|
|
70
|
+
CLIENT_BUNDLE_MISSING: item => `${item.package} 的客户端 bundle 缺失。`,
|
|
71
|
+
CLIENT_BUNDLE_UNREADABLE: item => `无法读取 ${item.package} 的客户端 bundle。`,
|
|
72
|
+
UNDECLARED_CLIENT_REQUIRE: item => `${item.package} 引用了 ${captured(item.message, / requires (.+) but does not/) ?? '未声明模块'},但没有在 dsh.client.external 中声明。`,
|
|
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/) ?? '客户端模块'},但当前 Harness 没有对应的模块提供方。`,
|
|
75
|
+
REMOVED_CLIENT_INJECT: item => `${item.package} 注入了 ${captured(item.message, / injects (.+), which/) ?? '已移除的模块'},但当前 Harness 源码中已不存在该模块。`,
|
|
76
|
+
LEGACY_HARNESS_PEERS: item => `${item.package} 仍声明了当前 Harness 源码中已不存在的旧包。`,
|
|
77
|
+
BUNDLE_NOT_INSTALLED: item => `配置中的 bundle ${item.package} 尚未安装。`,
|
|
78
|
+
BUNDLE_DECLARATION_MISSING: item => `${item.package} 被列为 profile bundle,但没有声明 dsh.bundle.patch。`,
|
|
79
|
+
BUNDLE_PATCH_MISSING: item => `${item.package} 的 bundle patch 文件缺失。`,
|
|
80
|
+
INVALID_PATCH_YAML: item => `${captured(item.message, /^(.+) cannot be parsed\.$/) ?? 'Patch 文件'}无法解析。`,
|
|
81
|
+
INVALID_PATCH_LIST: item => `${captured(item.message, /^(.+) must be/) ?? 'Patch 文件'}的顶层必须是由映射组成的 YAML 数组。`,
|
|
82
|
+
INVALID_SETTINGS_DOCUMENT: () => 'Harness 设置文件无法解析。',
|
|
83
|
+
INVALID_SETTINGS_ROOT: () => 'Harness 设置文件顶层必须是命名空间映射。',
|
|
84
|
+
INVALID_CREDENTIALS_DOCUMENT: () => 'Harness 凭据文件无法解析。',
|
|
85
|
+
INVALID_CREDENTIALS_ROOT: () => 'Harness 凭据文件顶层必须是映射。',
|
|
86
|
+
INVALID_CREDENTIALS_LAYOUT: () => 'Harness 凭据文件不是受支持的 version 1 结构。',
|
|
87
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `${item.package} 声明的 Harness peer 版本范围不接受当前已安装版本。`,
|
|
88
|
+
INVALID_PROFILE_NAME: item => `Profile 名称无效:${captured(item.message, /^Invalid profile name (.+)\.$/) ?? ''}`,
|
|
89
|
+
PROFILE_NOT_FOUND: item => `Profile ${captured(item.message, /^Profile (.+) does not exist\.$/) ?? ''} 不存在。`,
|
|
90
|
+
INVALID_DSH_CONFIGURATION: () => 'dsh 字段存在时必须是对象。',
|
|
91
|
+
INVALID_PROFILE_CONFIGURATION: () => 'dsh.profile 字段存在时必须是对象。',
|
|
92
|
+
INVALID_BUNDLE_LIST: () => 'dsh.profile.bundles 必须是字符串数组。',
|
|
93
|
+
INVALID_PATCH_RELOAD: () => 'dsh.profile.patchReload 必须是 “live” 或 “startup”。',
|
|
94
|
+
DEPENDENCY_NOT_INSTALLED: item => `Profile 依赖 ${item.package} 尚未安装。`,
|
|
95
|
+
PROFILE_DEPENDENCY_VERSION_MISMATCH: item => `${item.package} 的声明版本范围与当前安装版本不兼容。`,
|
|
96
|
+
INSTALLED_BUNDLE_INACTIVE: item => `${item.package} 已作为 bundle 安装,但不在 dsh.profile.bundles 中。`,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const ZH_SUGGESTIONS = {
|
|
100
|
+
INVALID_JSON_OBJECT: () => '启动 Harness 前,请恢复有效的 package manifest 对象。',
|
|
101
|
+
INVALID_JSON: () => '启动该 profile 前,请先修复 JSON。',
|
|
102
|
+
PACKAGE_NAME_MISMATCH: () => '重新安装依赖,使目录位置与包名一致。',
|
|
103
|
+
INVALID_HARNESS_ROOT: () => '诊断源码工作区时,请把 DeepSeek Harness 仓库根目录传给 --harness-root。',
|
|
104
|
+
INVALID_WORKSPACE_MANIFEST: () => '修复该 workspace 包清单后,Doctor 才能把它纳入兼容性检查。',
|
|
105
|
+
HARNESS_INSTALLATION_UNKNOWN: () => '若使用源码工作区,请通过 --harness-root 明确指定;若使用独立 CLI,请通过 --dsh-command 指定。',
|
|
106
|
+
INVALID_DEPENDENCY_MAP: () => '管理或启动 profile 前,请先修复这个依赖字段。',
|
|
107
|
+
INVALID_CLIENT_DECLARATION: update,
|
|
108
|
+
INVALID_CLIENT_PLATFORM: update,
|
|
109
|
+
INVALID_CLIENT_IMMEDIATELY: update,
|
|
110
|
+
INVALID_CLIENT_EXTERNAL: update,
|
|
111
|
+
INVALID_CLIENT_INJECT: update,
|
|
112
|
+
CLIENT_EXPORT_MISSING: update,
|
|
113
|
+
CLIENT_BUNDLE_MISSING: update,
|
|
114
|
+
UNDECLARED_CLIENT_REQUIRE: update,
|
|
115
|
+
CLIENT_EXTERNAL_WITHOUT_SUPPLIER: update,
|
|
116
|
+
REMOVED_CLIENT_INJECT: update,
|
|
117
|
+
REDUNDANT_CLIENT_EXTERNAL: () => '插件作者应删除重复的 dsh.client.external 条目。',
|
|
118
|
+
LEGACY_HARNESS_PEERS: () => '该插件存在兼容风险,请在下次升级 Harness 前更新。',
|
|
119
|
+
BUNDLE_NOT_INSTALLED: () => '使用当前 DSH 安装补齐 profile 依赖、升级 bundle,或从 profile 中移除它。',
|
|
120
|
+
BUNDLE_DECLARATION_MISSING: () => '升级该 bundle,或把它从 dsh.profile.bundles 中移除。',
|
|
121
|
+
BUNDLE_PATCH_MISSING: () => '重新安装或升级该 bundle,或者从 profile 中移除它。',
|
|
122
|
+
INVALID_PATCH_YAML: () => '启动该 profile 前,请修复 YAML 语法。',
|
|
123
|
+
INVALID_PATCH_LIST: () => '启动该 profile 前,请修复 patch 顶层结构。',
|
|
124
|
+
INVALID_SETTINGS_DOCUMENT: () => '修复设置文件语法;Doctor 不会猜测凭据或模型配置值。',
|
|
125
|
+
INVALID_SETTINGS_ROOT: () => '把顶层标量或数组替换为映射。',
|
|
126
|
+
INVALID_CREDENTIALS_DOCUMENT: () => '只修复报告的结构;Doctor 永远不会输出或重写秘密值。',
|
|
127
|
+
INVALID_CREDENTIALS_LAYOUT: () => '迁移文档结构,不要暴露或修改秘密值。',
|
|
128
|
+
HARNESS_PEER_VERSION_MISMATCH: item => `把 ${item.package} 更新到兼容当前 Harness 的版本。`,
|
|
129
|
+
PROFILE_NOT_FOUND: () => '先启动一次该 profile,或用当前 DSH 安装初始化它。',
|
|
130
|
+
INVALID_DSH_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh 配置对象。',
|
|
131
|
+
INVALID_PROFILE_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh.profile 配置对象。',
|
|
132
|
+
INVALID_BUNDLE_LIST: () => '启动 Harness 前,请修复 profile manifest。',
|
|
133
|
+
INVALID_PATCH_RELOAD: () => '选择该 profile 需要的 reload 生命周期。',
|
|
134
|
+
DEPENDENCY_NOT_INSTALLED: () => '使用下方精确命令安装该 profile 声明的依赖。',
|
|
135
|
+
PROFILE_DEPENDENCY_VERSION_MISMATCH: () => '使用下方精确命令重新同步该 profile 的安装。',
|
|
136
|
+
INSTALLED_BUNDLE_INACTIVE: () => '重新执行匹配的插件添加或更新操作,或者移除未使用的依赖。',
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function update(item) {
|
|
140
|
+
return `更新 ${item.package};如果没有兼容版本,再通过同一个 DSH 安装移除该插件。`
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function localizedFinding(item, language) {
|
|
144
|
+
if (language !== 'zh') return item
|
|
145
|
+
const message = ZH_MESSAGES[item.code]?.(item) ?? item.message
|
|
146
|
+
const suggestion = item.suggestion === undefined ? undefined : (ZH_SUGGESTIONS[item.code]?.(item) ?? item.suggestion)
|
|
147
|
+
const evidence = typeof item.evidence === 'string'
|
|
148
|
+
? item.evidence.replaceAll('(active ', '(当前 ').replace(/ at line (\d+), column (\d+)/g, ',第 $1 行第 $2 列')
|
|
149
|
+
: item.evidence
|
|
150
|
+
return { ...item, message, suggestion, evidence }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function languageName(language) {
|
|
154
|
+
return language === 'zh' ? '中文' : 'English'
|
|
155
|
+
}
|
package/src/repair.mjs
CHANGED
|
@@ -22,15 +22,30 @@ 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
|
-
|
|
37
|
+
const plan = lines.join('\n')
|
|
38
|
+
return options.prompt === false
|
|
39
|
+
? plan.trimEnd()
|
|
40
|
+
: `${plan}${zh ? '执行这些修复吗?[y/N] ' : 'Apply these repairs? [y/N] '}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function localizedDescription(action, zh) {
|
|
44
|
+
if (!zh) return action.description
|
|
45
|
+
if (action.id.startsWith('update-package:')) return `更新 profile ${action.profile} 中的 ${action.package}。`
|
|
46
|
+
if (action.id.startsWith('install-profile:')) return `安装 profile ${action.profile} 声明的依赖。`
|
|
47
|
+
if (action.id.startsWith('activate-bundle:')) return `把 ${action.operation.name} 加入 dsh.profile.bundles。`
|
|
48
|
+
return action.description
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
function quoteArgument(value) {
|
|
@@ -44,8 +59,19 @@ function applyJsonEdit(action) {
|
|
|
44
59
|
}
|
|
45
60
|
const manifest = JSON.parse(current)
|
|
46
61
|
if (action.operation.type !== 'add-bundle') throw new Error(`unsupported JSON repair ${action.operation.type}`)
|
|
47
|
-
|
|
48
|
-
if (
|
|
62
|
+
if (manifest.dsh === undefined) manifest.dsh = {}
|
|
63
|
+
if (manifest.dsh === null || typeof manifest.dsh !== 'object' || Array.isArray(manifest.dsh)) {
|
|
64
|
+
throw new Error(`${action.file} no longer has a valid dsh object`)
|
|
65
|
+
}
|
|
66
|
+
if (manifest.dsh.profile === undefined) manifest.dsh.profile = {}
|
|
67
|
+
if (manifest.dsh.profile === null || typeof manifest.dsh.profile !== 'object' || Array.isArray(manifest.dsh.profile)) {
|
|
68
|
+
throw new Error(`${action.file} no longer has a valid dsh.profile object`)
|
|
69
|
+
}
|
|
70
|
+
if (manifest.dsh.profile.bundles === undefined) manifest.dsh.profile.bundles = []
|
|
71
|
+
const bundles = manifest.dsh.profile.bundles
|
|
72
|
+
if (!Array.isArray(bundles) || !bundles.every(item => typeof item === 'string')) {
|
|
73
|
+
throw new Error(`${action.file} no longer has a valid dsh.profile.bundles array`)
|
|
74
|
+
}
|
|
49
75
|
if (!bundles.includes(action.operation.name)) bundles.push(action.operation.name)
|
|
50
76
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-')
|
|
51
77
|
const backup = `${action.file}.dsh-doctor-${stamp}.bak`
|
|
@@ -58,9 +84,16 @@ function applyJsonEdit(action) {
|
|
|
58
84
|
|
|
59
85
|
function applyCommand(action) {
|
|
60
86
|
const [command, ...args] = action.command
|
|
61
|
-
const result = crossSpawn.sync(command, args, {
|
|
87
|
+
const result = crossSpawn.sync(command, args, {
|
|
88
|
+
stdio: 'inherit',
|
|
89
|
+
env: action.env === undefined ? process.env : { ...process.env, ...action.env },
|
|
90
|
+
})
|
|
62
91
|
if (result.error != null) throw result.error
|
|
63
|
-
if (result.status !== 0)
|
|
92
|
+
if (result.status !== 0) {
|
|
93
|
+
throw new Error(result.signal === null
|
|
94
|
+
? `${command} exited with status ${String(result.status)}`
|
|
95
|
+
: `${command} was terminated by signal ${result.signal}`)
|
|
96
|
+
}
|
|
64
97
|
return { id: action.id, status: 'applied' }
|
|
65
98
|
}
|
|
66
99
|
|