@bruc3van/dsh-doctor 0.1.0 → 0.1.1

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