@bruc3van/dsh-doctor 0.1.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bruc3van/dsh-doctor",
3
- "version": "0.1.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",
package/src/cli.mjs CHANGED
@@ -107,11 +107,13 @@ async function main() {
107
107
  }
108
108
  if (actions.length > 0) {
109
109
  let confirmed = options.yes === true
110
- if (!confirmed) {
110
+ const plan = formatRepairPlan(actions, { language, prompt: !confirmed })
111
+ if (confirmed) {
112
+ process.stderr.write(`${plan}\n`)
113
+ } else {
111
114
  if (!process.stdin.isTTY) throw new Error('--fix needs an interactive terminal or explicit --yes')
112
- const prompt = formatRepairPlan(actions, { language })
113
115
  const reader = createInterface({ input: process.stdin, output: process.stderr })
114
- const answer = await reader.question(prompt)
116
+ const answer = await reader.question(plan)
115
117
  reader.close()
116
118
  confirmed = /^(?:y(?:es)?|是|确认)$/i.test(answer.trim())
117
119
  }
package/src/doctor.mjs CHANGED
@@ -1,4 +1,5 @@
1
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
4
  import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
4
5
  import yaml from 'js-yaml'
@@ -16,6 +17,7 @@ export const PLATFORM_MODULES = new Set([
16
17
  '@deepseek-ai/dsh-client-ui-slots',
17
18
  '@deepseek-ai/dsh-client-ui-primitives',
18
19
  ])
20
+ const BUILTIN_MODULES = new Set(builtinModules.map(name => name.replace(/^node:/, '')))
19
21
 
20
22
  const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 }
21
23
  const JS_EXPRESSION = new yaml.Type('tag:yaml.org,2002:js', {
@@ -71,6 +73,7 @@ function readJson(file, subject, findings) {
71
73
  }
72
74
 
73
75
  function packagePathParts(name) {
76
+ if (typeof name !== 'string') return undefined
74
77
  if (name.startsWith('@')) {
75
78
  const parts = name.split('/')
76
79
  return parts.length === 2
@@ -94,7 +97,7 @@ function packageManifestAt(directory, expectedName, findings, source) {
94
97
  suggestion: 'Reinstall the dependency so its directory and package name agree.',
95
98
  }))
96
99
  }
97
- return { directory, file, manifest }
100
+ return { directory, file, manifest, requestedName: expectedName }
98
101
  }
99
102
 
100
103
  function findHarnessRoot(start) {
@@ -118,6 +121,14 @@ function executable(file) {
118
121
  }
119
122
  }
120
123
 
124
+ function regularFile(file) {
125
+ try {
126
+ return statSync(file).isFile()
127
+ } catch {
128
+ return false
129
+ }
130
+ }
131
+
121
132
  function executableOnPath(name, env) {
122
133
  const path = env.PATH ?? env.Path ?? env.path ?? ''
123
134
  const extensions = process.platform === 'win32'
@@ -168,10 +179,10 @@ function commandFromValue(value, env, cwd) {
168
179
  : value
169
180
  const looksLikePath = isAbsolute(expanded) || expanded.includes('/') || expanded.includes('\\') || expanded.startsWith('.')
170
181
  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 }
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
175
186
  }
176
187
 
177
188
  function resolveDshCli(options, harness, home) {
@@ -242,7 +253,14 @@ function indexWorkspace(root, findings) {
242
253
  for (const directory of workspacePackageDirectories(root)) {
243
254
  const file = join(directory, 'package.json')
244
255
  if (!existsSync(file)) continue
245
- 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
+ }
246
264
  if (typeof manifest?.name === 'string') packages.set(manifest.name, { directory, file, manifest })
247
265
  }
248
266
  return packages
@@ -256,10 +274,10 @@ function resolveHarnessContext(home, explicitRoot, findings) {
256
274
  evidence: root,
257
275
  suggestion: 'Pass the DeepSeek Harness repository root to --harness-root.',
258
276
  }))
259
- return { root, packages: new Map(), version: undefined, authoritative: true }
277
+ return { root, packages: new Map(), version: undefined, authoritative: false }
260
278
  }
261
279
  const manifest = readJson(join(root, 'package.json'), 'Harness root manifest', findings)
262
- return { root, packages: indexWorkspace(root, findings), version: manifest?.version, authoritative: true }
280
+ return { root, packages: indexWorkspace(root, findings), version: manifest?.version, authoritative: manifest !== undefined }
263
281
  }
264
282
 
265
283
  const sharedDsh = join(home, 'profiles', 'node_modules', '@deepseek-ai', 'dsh')
@@ -301,8 +319,9 @@ function packageResolver(profileDir, home, harnessPackages, findings) {
301
319
  return resolved
302
320
  }
303
321
  const workspace = harnessPackages.get(name)
304
- cache.set(name, workspace)
305
- return workspace
322
+ const resolved = workspace === undefined ? undefined : { ...workspace, requestedName: name }
323
+ cache.set(name, resolved)
324
+ return resolved
306
325
  }
307
326
  }
308
327
 
@@ -346,6 +365,7 @@ function dependencyEntries(value, field, file, findings) {
346
365
  }
347
366
 
348
367
  function updateRepair(profile, name, commandRepair) {
368
+ if (packagePathParts(name) === undefined) return undefined
349
369
  return commandRepair(
350
370
  `update-package:${name}`,
351
371
  `Update ${name} in profile ${profile}.`,
@@ -365,11 +385,11 @@ function safePackageFile(packageDir, exported) {
365
385
  export function extractStaticRequires(source) {
366
386
  const values = new Set()
367
387
  const code = codePositions(source)
368
- const pattern = /\brequire\s*\(\s*(['"])([^'"\\\r\n]+)\1\s*\)/g
388
+ const pattern = /(?<![\w$.])require\s*\(\s*(['"])([^'"\\\r\n]+)\1\s*\)/g
369
389
  for (const match of source.matchAll(pattern)) {
370
390
  if (!code[match.index]) continue
371
391
  const specifier = match[2]
372
- if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('node:')) continue
392
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('node:') || BUILTIN_MODULES.has(specifier)) continue
373
393
  if (packagePathParts(specifier) !== undefined || specifier.startsWith('@')) values.add(specifier)
374
394
  }
375
395
  return [...values].sort()
@@ -424,7 +444,7 @@ function inspectClientPackage(record, context) {
424
444
  const {
425
445
  commandRepair, findings, harnessPackages, harnessPackagesAuthoritative, profile, resolvePackage,
426
446
  } = context
427
- const name = record.manifest.name
447
+ const name = record.requestedName ?? record.manifest.name
428
448
  const disableSuggestion = `Upgrade ${name}; if no compatible release exists, remove it through the same DSH installation.`
429
449
  const declaration = record.manifest?.dsh?.client
430
450
  if (declaration === undefined) return
@@ -486,7 +506,7 @@ function inspectClientPackage(record, context) {
486
506
  return
487
507
  }
488
508
  const file = safePackageFile(record.directory, exported)
489
- if (file === undefined || !existsSync(file) || !statSync(file).isFile()) {
509
+ if (file === undefined || !regularFile(file)) {
490
510
  findings.push(finding('error', 'CLIENT_BUNDLE_MISSING', `${name} client bundle is missing.`, {
491
511
  package: name,
492
512
  evidence: file ?? `${record.file}: exports["./client"] = ${JSON.stringify(exported)}`,
@@ -587,7 +607,7 @@ function inspectBundle(name, record, findings) {
587
607
  return
588
608
  }
589
609
  const file = safePackageFile(record.directory, patch)
590
- if (file === undefined || !existsSync(file) || !statSync(file).isFile()) {
610
+ if (file === undefined || !regularFile(file)) {
591
611
  findings.push(finding('error', 'BUNDLE_PATCH_MISSING', `${name} bundle patch is missing.`, {
592
612
  package: name,
593
613
  evidence: file ?? `${record.file}: dsh.bundle.patch = ${JSON.stringify(patch)}`,
@@ -699,18 +719,19 @@ function inspectCompatibility(record, context) {
699
719
  mismatches.push(`${name} ${range} (active ${version})`)
700
720
  }
701
721
  if (mismatches.length > 0) {
702
- findings.push(finding('warning', 'HARNESS_PEER_VERSION_MISMATCH', `${record.manifest.name} has Harness peer ranges that do not accept the active versions.`, {
703
- 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,
704
725
  evidence: mismatches.join(', '),
705
- suggestion: `Update ${record.manifest.name} to a release compatible with the active Harness.`,
706
- repair: updateRepair(profile, record.manifest.name, commandRepair),
726
+ suggestion: `Update ${name} to a release compatible with the active Harness.`,
727
+ repair: updateRepair(profile, name, commandRepair),
707
728
  }))
708
729
  }
709
730
  }
710
731
 
711
732
  export function defaultDshHome(env = process.env) {
712
- const configured = env.DSH_HOME
713
- if (configured !== undefined && configured.trim().length > 0) {
733
+ const configured = env.DSH_HOME?.trim()
734
+ if (configured !== undefined && configured.length > 0) {
714
735
  if (configured === '~') return homedir()
715
736
  if (configured.startsWith('~/') || configured.startsWith('~\\')) return resolve(homedir(), configured.slice(2))
716
737
  return resolve(configured)
@@ -759,15 +780,32 @@ export function diagnose(options = {}) {
759
780
  const resolveBundle = bundleResolver(profileDir, home, harness.packages, findings)
760
781
  const dependencyEntriesList = dependencyEntries(profileManifest.dependencies, 'dependencies', profileManifestFile, findings)
761
782
  const dependencyNames = dependencyEntriesList.map(([name]) => name)
762
- 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
763
801
  if (bundles !== undefined && (!Array.isArray(bundles) || !bundles.every(item => typeof item === 'string'))) {
764
802
  findings.push(finding('error', 'INVALID_BUNDLE_LIST', 'dsh.profile.bundles must be a string array.', {
765
803
  evidence: profileManifestFile,
766
804
  suggestion: 'Repair the profile manifest before starting Harness.',
767
805
  }))
768
806
  }
769
- const bundleNames = Array.isArray(bundles) ? bundles.filter(item => typeof item === 'string') : []
770
- 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
771
809
  if (patchReload !== undefined && patchReload !== 'live' && patchReload !== 'startup') {
772
810
  findings.push(finding('error', 'INVALID_PATCH_RELOAD', 'dsh.profile.patchReload must be "live" or "startup".', {
773
811
  evidence: profileManifestFile,
@@ -818,7 +856,8 @@ export function diagnose(options = {}) {
818
856
  ),
819
857
  }))
820
858
  }
821
- if (record.manifest?.dsh?.bundle?.patch !== undefined && !bundleNames.includes(name)) {
859
+ if (dshConfigValid && profileConfigValid
860
+ && record.manifest?.dsh?.bundle?.patch !== undefined && !bundleNames.includes(name)) {
822
861
  findings.push(finding('warning', 'INSTALLED_BUNDLE_INACTIVE', `${name} is installed as a bundle but is absent from dsh.profile.bundles.`, {
823
862
  package: name,
824
863
  evidence: profileManifestFile,
@@ -871,7 +910,7 @@ export function diagnose(options = {}) {
871
910
  ? { available: false, commandRepairNeeded }
872
911
  : { available: true, commandRepairNeeded, ...dshCli },
873
912
  packages: thirdPartyRecords.map(record => ({
874
- name: record.manifest.name,
913
+ name: record.requestedName ?? record.manifest.name,
875
914
  version: record.manifest.version,
876
915
  directory: record.directory,
877
916
  client: record.manifest?.dsh?.client !== undefined,
package/src/i18n.mjs CHANGED
@@ -58,6 +58,7 @@ const ZH_MESSAGES = {
58
58
  INVALID_JSON: item => `${captured(item.message, /^(.+) is not valid JSON\.$/) ?? '该文件'}不是有效的 JSON。`,
59
59
  PACKAGE_NAME_MISMATCH: item => `${item.package ?? '依赖'}解析到了名称不匹配的包。`,
60
60
  INVALID_HARNESS_ROOT: () => '指定的 Harness 根目录不是有效的源码工作区。',
61
+ INVALID_WORKSPACE_MANIFEST: () => '已忽略一个无效的 Harness workspace 包清单。',
61
62
  HARNESS_INSTALLATION_UNKNOWN: () => '无法定位这个 DSH Home 实际使用的 Harness。',
62
63
  INVALID_DEPENDENCY_MAP: item => `${captured(item.message, /^(\S+) must be/) ?? '依赖字段'}必须是“包名到版本范围”的对象。`,
63
64
  INVALID_CLIENT_DECLARATION: item => `${item.package} 的 dsh.client 声明无效。`,
@@ -86,6 +87,8 @@ const ZH_MESSAGES = {
86
87
  HARNESS_PEER_VERSION_MISMATCH: item => `${item.package} 声明的 Harness peer 版本范围不接受当前已安装版本。`,
87
88
  INVALID_PROFILE_NAME: item => `Profile 名称无效:${captured(item.message, /^Invalid profile name (.+)\.$/) ?? ''}`,
88
89
  PROFILE_NOT_FOUND: item => `Profile ${captured(item.message, /^Profile (.+) does not exist\.$/) ?? ''} 不存在。`,
90
+ INVALID_DSH_CONFIGURATION: () => 'dsh 字段存在时必须是对象。',
91
+ INVALID_PROFILE_CONFIGURATION: () => 'dsh.profile 字段存在时必须是对象。',
89
92
  INVALID_BUNDLE_LIST: () => 'dsh.profile.bundles 必须是字符串数组。',
90
93
  INVALID_PATCH_RELOAD: () => 'dsh.profile.patchReload 必须是 “live” 或 “startup”。',
91
94
  DEPENDENCY_NOT_INSTALLED: item => `Profile 依赖 ${item.package} 尚未安装。`,
@@ -98,6 +101,7 @@ const ZH_SUGGESTIONS = {
98
101
  INVALID_JSON: () => '启动该 profile 前,请先修复 JSON。',
99
102
  PACKAGE_NAME_MISMATCH: () => '重新安装依赖,使目录位置与包名一致。',
100
103
  INVALID_HARNESS_ROOT: () => '诊断源码工作区时,请把 DeepSeek Harness 仓库根目录传给 --harness-root。',
104
+ INVALID_WORKSPACE_MANIFEST: () => '修复该 workspace 包清单后,Doctor 才能把它纳入兼容性检查。',
101
105
  HARNESS_INSTALLATION_UNKNOWN: () => '若使用源码工作区,请通过 --harness-root 明确指定;若使用独立 CLI,请通过 --dsh-command 指定。',
102
106
  INVALID_DEPENDENCY_MAP: () => '管理或启动 profile 前,请先修复这个依赖字段。',
103
107
  INVALID_CLIENT_DECLARATION: update,
@@ -123,6 +127,8 @@ const ZH_SUGGESTIONS = {
123
127
  INVALID_CREDENTIALS_LAYOUT: () => '迁移文档结构,不要暴露或修改秘密值。',
124
128
  HARNESS_PEER_VERSION_MISMATCH: item => `把 ${item.package} 更新到兼容当前 Harness 的版本。`,
125
129
  PROFILE_NOT_FOUND: () => '先启动一次该 profile,或用当前 DSH 安装初始化它。',
130
+ INVALID_DSH_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh 配置对象。',
131
+ INVALID_PROFILE_CONFIGURATION: () => '启动 Harness 前,请先修复 dsh.profile 配置对象。',
126
132
  INVALID_BUNDLE_LIST: () => '启动 Harness 前,请修复 profile manifest。',
127
133
  INVALID_PATCH_RELOAD: () => '选择该 profile 需要的 reload 生命周期。',
128
134
  DEPENDENCY_NOT_INSTALLED: () => '使用下方精确命令安装该 profile 声明的依赖。',
package/src/repair.mjs CHANGED
@@ -34,7 +34,10 @@ export function formatRepairPlan(actions, options = {}) {
34
34
  } else lines.push(` ${zh ? '文件' : 'File'}: ${action.file}`, ` ${zh ? '备份' : 'Backup'}: ${action.file}.dsh-doctor-<timestamp>.bak`)
35
35
  lines.push('')
36
36
  })
37
- return `${lines.join('\n')}${zh ? '执行这些修复吗?[y/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] '}`
38
41
  }
39
42
 
40
43
  function localizedDescription(action, zh) {
@@ -56,8 +59,19 @@ function applyJsonEdit(action) {
56
59
  }
57
60
  const manifest = JSON.parse(current)
58
61
  if (action.operation.type !== 'add-bundle') throw new Error(`unsupported JSON repair ${action.operation.type}`)
59
- const bundles = manifest?.dsh?.profile?.bundles
60
- 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
+ }
61
75
  if (!bundles.includes(action.operation.name)) bundles.push(action.operation.name)
62
76
  const stamp = new Date().toISOString().replace(/[:.]/g, '-')
63
77
  const backup = `${action.file}.dsh-doctor-${stamp}.bak`
@@ -75,7 +89,11 @@ function applyCommand(action) {
75
89
  env: action.env === undefined ? process.env : { ...process.env, ...action.env },
76
90
  })
77
91
  if (result.error != null) throw result.error
78
- 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
+ }
79
97
  return { id: action.id, status: 'applied' }
80
98
  }
81
99