@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 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.0",
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 HELP = `Usage: dsh-doctor [options]
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
- function fail(message) {
26
- process.stderr.write(`dsh-doctor: ${message}\n\n${HELP}`)
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(HELP)
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
- if (!confirmed) {
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(prompt)
116
+ const answer = await reader.question(plan)
81
117
  reader.close()
82
- confirmed = /^y(?:es)?$/i.test(answer.trim())
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 ? `Repairs: ${JSON.stringify(repairs, null, 2)}\n` : ''}`)
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 manifest = readJson(file, 'Harness workspace package manifest', findings)
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: true }
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: true }
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
- cache.set(name, workspace)
198
- return workspace
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
- id: `update-package:${name}`,
244
- kind: 'command',
245
- risk: 'medium',
246
- description: `Update ${name} in profile ${profile}.`,
247
- command: ['dsh', 'plugin', '--profile', profile, 'update', name],
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 = /\brequire\s*\(\s*(['"])([^'"\\\r\n]+)\1\s*\)/g
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, run dsh plugin --profile ${profile} remove ${name}.`
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 || !existsSync(file) || !statSync(file).isFile()) {
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, run dsh plugin --profile ${profile} remove ${name}.`,
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: `Run dsh plugin --profile <profile> install, upgrade the bundle, or remove it from the profile.`,
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 || !existsSync(file) || !statSync(file).isFile()) {
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
- findings.push(finding('warning', 'HARNESS_PEER_VERSION_MISMATCH', `${record.manifest.name} has Harness peer ranges that do not accept the active versions.`, {
597
- package: record.manifest.name,
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 ${record.manifest.name} to a release compatible with the active Harness.`,
600
- repair: updateRepair(profile, record.manifest.name),
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.trim().length > 0) {
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: `Start the profile once or initialize it with dsh plugin --profile ${profile} install.`,
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 bundles = profileManifest?.dsh?.profile?.bundles
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 = profileManifest?.dsh?.profile?.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: `Run dsh plugin --profile ${profile} install.`,
675
- repair: {
676
- id: `install-profile:${profile}`,
677
- kind: 'command',
678
- risk: 'medium',
679
- description: `Install the declared dependencies for profile ${profile}.`,
680
- command: ['dsh', 'plugin', '--profile', profile, 'install'],
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: `Reconcile the profile installation with dsh plugin --profile ${profile} install.`,
694
- repair: {
695
- id: `install-profile:${profile}`,
696
- kind: 'command',
697
- risk: 'medium',
698
- description: `Install the declared dependencies for profile ${profile}.`,
699
- command: ['dsh', 'plugin', '--profile', profile, 'install'],
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 (record.manifest?.dsh?.bundle?.patch !== undefined && !bundleNames.includes(name)) {
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
- `Profile: ${report.context.profile}`,
783
- `Home: ${report.context.home}`,
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
- `Checked third-party packages: ${String(report.context.packages.length)}`,
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 item of report.findings) {
792
- const label = item.severity === 'error' ? 'ERROR' : item.severity === 'warning' ? 'WARN ' : 'INFO '
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(`Summary: ${String(report.summary.errors)} error(s), ${String(report.summary.warnings)} warning(s)`)
801
- if (report.summary.errors > 0) lines.push('Harness may fail to start. Upgrade or disable the error-producing plugin first.')
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 lines = ['Proposed repairs:', '']
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
- lines.push(`${String(index + 1)}. [${action.risk.toUpperCase()}] ${action.description}`)
29
- if (action.kind === 'command') lines.push(` Command: ${action.command.map(quoteArgument).join(' ')}`)
30
- else lines.push(` File: ${action.file}`, ` Backup: ${action.file}.dsh-doctor-<timestamp>.bak`)
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
+ 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
- const bundles = manifest?.dsh?.profile?.bundles
48
- if (!Array.isArray(bundles)) throw new Error(`${action.file} no longer has a valid dsh.profile.bundles array`)
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, { stdio: 'inherit' })
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) throw new Error(`${command} exited with status ${String(result.status)}`)
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