dsh-agy-cli 0.0.0-stage → 0.12.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.
Files changed (91) hide show
  1. package/LICENSE +31 -0
  2. package/README.en.md +48 -0
  3. package/README.md +67 -2
  4. package/agents/dsh-agy-image-view.md +17 -0
  5. package/agents/dsh-agy-mcp-executor.md +32 -0
  6. package/agents/dsh-agy-read-only.md +20 -0
  7. package/agents/dsh-agy-tool-free.md +16 -0
  8. package/agents/dsh-agy-workspace-write.md +23 -0
  9. package/bin/dsh-agy-cli.js +9 -0
  10. package/bin/win32-x64/agy-launcher.exe +0 -0
  11. package/cordis.patch.yml +16 -0
  12. package/dist/client.js +4259 -0
  13. package/lib/agent-installer.d.ts +66 -0
  14. package/lib/agent-installer.js +147 -0
  15. package/lib/agent-presets.d.ts +42 -0
  16. package/lib/agent-presets.js +130 -0
  17. package/lib/agents-cli.d.ts +3 -0
  18. package/lib/agents-cli.js +110 -0
  19. package/lib/agy/diagnostics.d.ts +29 -0
  20. package/lib/agy/diagnostics.js +109 -0
  21. package/lib/agy/errors.d.ts +20 -0
  22. package/lib/agy/errors.js +77 -0
  23. package/lib/agy/experimental-transport.d.ts +79 -0
  24. package/lib/agy/experimental-transport.js +612 -0
  25. package/lib/agy/keepalive.d.ts +140 -0
  26. package/lib/agy/keepalive.js +445 -0
  27. package/lib/agy/limiter.d.ts +32 -0
  28. package/lib/agy/limiter.js +111 -0
  29. package/lib/agy/log.d.ts +214 -0
  30. package/lib/agy/log.js +503 -0
  31. package/lib/agy/models.d.ts +49 -0
  32. package/lib/agy/models.js +203 -0
  33. package/lib/agy/overrides.d.ts +17 -0
  34. package/lib/agy/overrides.js +73 -0
  35. package/lib/agy/parser.d.ts +102 -0
  36. package/lib/agy/parser.js +492 -0
  37. package/lib/agy/persistent-transport.d.ts +101 -0
  38. package/lib/agy/persistent-transport.js +753 -0
  39. package/lib/agy/process.d.ts +144 -0
  40. package/lib/agy/process.js +452 -0
  41. package/lib/agy/redact.d.ts +3 -0
  42. package/lib/agy/redact.js +24 -0
  43. package/lib/agy/stream-protocol.d.ts +50 -0
  44. package/lib/agy/stream-protocol.js +156 -0
  45. package/lib/agy/windows-launcher.d.ts +21 -0
  46. package/lib/agy/windows-launcher.js +76 -0
  47. package/lib/diagnostics.d.ts +78 -0
  48. package/lib/diagnostics.js +162 -0
  49. package/lib/doctor.d.ts +138 -0
  50. package/lib/doctor.js +940 -0
  51. package/lib/dsh/context.d.ts +133 -0
  52. package/lib/dsh/context.js +352 -0
  53. package/lib/dsh-home.d.ts +6 -0
  54. package/lib/dsh-home.js +11 -0
  55. package/lib/index.d.ts +54 -0
  56. package/lib/index.js +267 -0
  57. package/lib/mcp/agy-registration.d.ts +70 -0
  58. package/lib/mcp/agy-registration.js +245 -0
  59. package/lib/mcp/bridge.d.ts +73 -0
  60. package/lib/mcp/bridge.js +295 -0
  61. package/lib/mcp/endpoint.d.ts +41 -0
  62. package/lib/mcp/endpoint.js +142 -0
  63. package/lib/mcp/protocol.d.ts +107 -0
  64. package/lib/mcp/protocol.js +197 -0
  65. package/lib/mcp/tool-projection.d.ts +64 -0
  66. package/lib/mcp/tool-projection.js +168 -0
  67. package/lib/provider/agy.d.ts +153 -0
  68. package/lib/provider/agy.js +2159 -0
  69. package/lib/provider/config.d.ts +226 -0
  70. package/lib/provider/config.js +294 -0
  71. package/lib/provider/error-codes.d.ts +22 -0
  72. package/lib/provider/error-codes.js +29 -0
  73. package/lib/provider/image-bridge.d.ts +60 -0
  74. package/lib/provider/image-bridge.js +290 -0
  75. package/lib/provider/mock.d.ts +15 -0
  76. package/lib/provider/mock.js +78 -0
  77. package/lib/provider/prompt-budget.d.ts +45 -0
  78. package/lib/provider/prompt-budget.js +596 -0
  79. package/lib/provider/serialize.d.ts +15 -0
  80. package/lib/provider/serialize.js +82 -0
  81. package/lib/provider/tool-protocol.d.ts +80 -0
  82. package/lib/provider/tool-protocol.js +848 -0
  83. package/lib/provider/tool-schema-file.d.ts +11 -0
  84. package/lib/provider/tool-schema-file.js +27 -0
  85. package/lib/session/file-store.d.ts +56 -0
  86. package/lib/session/file-store.js +186 -0
  87. package/lib/session/store.d.ts +49 -0
  88. package/lib/session/store.js +62 -0
  89. package/lib/web/router.d.ts +19 -0
  90. package/lib/web/router.js +994 -0
  91. package/package.json +115 -4
package/lib/doctor.js ADDED
@@ -0,0 +1,940 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { createRequire } from 'node:module';
6
+ import { redactText } from './agy/redact.js';
7
+ import { getAgentPreset, listAgentPresets, readAgentPresetTemplate, } from './agent-presets.js';
8
+ import { diagnoseProvider, } from './diagnostics.js';
9
+ const require = createRequire(import.meta.url);
10
+ function loadPackageMetadata() {
11
+ try {
12
+ return require('../package.json');
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ const packageMetadata = loadPackageMetadata();
19
+ const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
20
+ const AGENT_WRITE_TOOLS = new Set([
21
+ 'multi_replace_file_content',
22
+ 'replace_file_content',
23
+ 'write_to_file',
24
+ ]);
25
+ function isSafeProfileName(value) {
26
+ return PROFILE_NAME_PATTERN.test(value);
27
+ }
28
+ function resolveDshInvocation(value) {
29
+ if (process.platform !== 'win32' || !/\.cmd$/i.test(value)) {
30
+ if (/\.(?:c|m)?js$/i.test(value))
31
+ return { executable: process.execPath, prefixArgs: [value] };
32
+ return { executable: value, prefixArgs: [] };
33
+ }
34
+ if (/[\\/]/.test(value)) {
35
+ const entry = join(dirname(value), '..', '@deepseek-ai', 'dsh', 'lib', 'bin.js');
36
+ if (existsSync(entry))
37
+ return { executable: process.execPath, prefixArgs: [entry] };
38
+ }
39
+ // npm's Windows .cmd shims cannot be spawned directly with shell=false.
40
+ // Keep shell=false and invoke the shim through the Windows command
41
+ // interpreter; profile names and DSH_HOME are passed as regular argv/env.
42
+ return {
43
+ executable: process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe',
44
+ prefixArgs: ['/d', '/s', '/c', 'call', value],
45
+ };
46
+ }
47
+ function killProcessTree(child) {
48
+ if (child.pid === undefined)
49
+ return;
50
+ if (process.platform === 'win32') {
51
+ const killer = spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
52
+ shell: false,
53
+ windowsHide: true,
54
+ stdio: 'ignore',
55
+ });
56
+ killer.once('error', () => child.kill());
57
+ return;
58
+ }
59
+ try {
60
+ process.kill(-child.pid, 'SIGKILL');
61
+ }
62
+ catch {
63
+ child.kill('SIGKILL');
64
+ }
65
+ }
66
+ function defaultRunCommand(executable, args, options = {}) {
67
+ const timeoutMs = options.timeoutMs ?? 10_000;
68
+ const maxOutputBytes = 1024 * 1024;
69
+ return new Promise(resolve => {
70
+ let stdout = '';
71
+ let stderr = '';
72
+ let totalBytes = 0;
73
+ let timedOut = false;
74
+ let settled = false;
75
+ const child = spawn(executable, args, {
76
+ cwd: options.cwd,
77
+ env: options.env ?? process.env,
78
+ shell: false,
79
+ windowsHide: true,
80
+ stdio: ['ignore', 'pipe', 'pipe'],
81
+ });
82
+ const timer = setTimeout(() => {
83
+ timedOut = true;
84
+ killProcessTree(child);
85
+ }, timeoutMs);
86
+ const finish = (exitCode) => {
87
+ if (settled)
88
+ return;
89
+ settled = true;
90
+ clearTimeout(timer);
91
+ resolve({ exitCode, stdout, stderr, timedOut });
92
+ };
93
+ child.stdout?.setEncoding('utf8');
94
+ child.stderr?.setEncoding('utf8');
95
+ let stderrBytes = 0;
96
+ child.stdout?.on('data', chunk => {
97
+ totalBytes += Buffer.byteLength(chunk, 'utf8');
98
+ if (totalBytes <= maxOutputBytes)
99
+ stdout += chunk;
100
+ });
101
+ child.stderr?.on('data', chunk => {
102
+ // Counted incrementally like stdout: re-measuring the whole accumulated
103
+ // string per chunk is quadratic on a chatty child.
104
+ stderrBytes += Buffer.byteLength(chunk, 'utf8');
105
+ if (stderrBytes <= maxOutputBytes)
106
+ stderr += chunk;
107
+ });
108
+ child.once('error', () => finish(null));
109
+ child.once('close', exitCode => finish(exitCode));
110
+ });
111
+ }
112
+ function resolveDshHome(customHome) {
113
+ const envHome = process.env.DSH_HOME?.trim();
114
+ if (customHome !== undefined && customHome.trim().length > 0)
115
+ return customHome.trim();
116
+ if (envHome !== undefined && envHome.length > 0)
117
+ return envHome;
118
+ return join(homedir(), '.dsh');
119
+ }
120
+ function stripDumpValue(value) {
121
+ const withoutComment = value.replace(/\s+#.*$/, '').trim();
122
+ if (withoutComment.length >= 2) {
123
+ const first = withoutComment[0];
124
+ const last = withoutComment.at(-1);
125
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
126
+ return withoutComment.slice(1, -1);
127
+ }
128
+ }
129
+ return withoutComment;
130
+ }
131
+ /** Parse only the provider bundle section; this is intentionally not a general YAML parser. */
132
+ function parseDumpConfig(stdout) {
133
+ const values = {};
134
+ let recognized = false;
135
+ let malformed = false;
136
+ let inBundleSection = false;
137
+ const stack = [];
138
+ for (const line of stdout.split(/\r?\n/)) {
139
+ if (/^\s*#\s*==\s*dsh-agy-cli\b/i.test(line)
140
+ || /^\s*-\s*id:\s*dsh-agy-cli\b/i.test(line)
141
+ || /^\s*id:\s*dsh-agy-cli\b/i.test(line)) {
142
+ recognized = true;
143
+ inBundleSection = true;
144
+ stack.length = 0;
145
+ continue;
146
+ }
147
+ if (inBundleSection && /^\s*#\s*==\s+/.test(line)) {
148
+ inBundleSection = false;
149
+ stack.length = 0;
150
+ continue;
151
+ }
152
+ if (!inBundleSection || line.trim().length === 0 || line.trim().startsWith('#'))
153
+ continue;
154
+ const match = /^(\s*)([A-Za-z][A-Za-z0-9_-]*):(?:\s*(.*))?$/.exec(line);
155
+ if (/^\s*-\s+/.test(line))
156
+ continue;
157
+ if (match === null || match[1] === undefined || match[2] === undefined) {
158
+ malformed = true;
159
+ continue;
160
+ }
161
+ const indent = match[1].replace(/\t/g, ' ').length;
162
+ while (stack.length > 0 && (stack.at(-1)?.indent ?? -1) >= indent)
163
+ stack.pop();
164
+ const path = [...stack.map(entry => entry.key), match[2]].join('.');
165
+ const rawValue = match[3]?.trim() ?? '';
166
+ values[path] = stripDumpValue(rawValue);
167
+ if (rawValue.length === 0)
168
+ stack.push({ indent, key: match[2] });
169
+ }
170
+ return { recognized, malformed, values };
171
+ }
172
+ function normalizeConfigValues(values) {
173
+ const normalized = { ...values };
174
+ for (const [key, value] of Object.entries(values)) {
175
+ if (key.startsWith('config.')) {
176
+ const shortKey = key.slice('config.'.length);
177
+ if (normalized[shortKey] === undefined)
178
+ normalized[shortKey] = value;
179
+ }
180
+ }
181
+ return normalized;
182
+ }
183
+ function valueOf(values, key) {
184
+ const value = values[key];
185
+ return value === undefined || value.length === 0 ? null : value;
186
+ }
187
+ function booleanValue(value) {
188
+ if (value === null)
189
+ return null;
190
+ if (value.toLowerCase() === 'true')
191
+ return true;
192
+ if (value.toLowerCase() === 'false')
193
+ return false;
194
+ return null;
195
+ }
196
+ function numberValue(value) {
197
+ if (value === null || !/^\d+$/.test(value))
198
+ return null;
199
+ const parsed = Number(value);
200
+ return Number.isSafeInteger(parsed) ? parsed : null;
201
+ }
202
+ function listValue(value) {
203
+ if (value === null)
204
+ return [];
205
+ const normalized = value.trim().replace(/^\[/, '').replace(/\]$/, '').trim();
206
+ if (normalized.length === 0)
207
+ return [];
208
+ return normalized.split(',').map(item => stripDumpValue(item.trim())).filter(Boolean);
209
+ }
210
+ function workspaceStatus(value) {
211
+ if (value === null || value.trim().length === 0)
212
+ return value === null ? 'unknown' : 'missing';
213
+ try {
214
+ return statSync(value).isDirectory() ? 'configured' : 'not-directory';
215
+ }
216
+ catch {
217
+ return 'missing';
218
+ }
219
+ }
220
+ function routeFromDump(values, purpose) {
221
+ const prefix = `purposeRoutes.${purpose}.`;
222
+ const configured = Object.keys(values).some(key => key.startsWith(prefix));
223
+ if (!configured)
224
+ return null;
225
+ const model = valueOf(values, `${prefix}model`);
226
+ const agent = valueOf(values, `${prefix}agent`);
227
+ const reasoningEffort = valueOf(values, `${prefix}reasoningEffort`);
228
+ return {
229
+ configured: true,
230
+ complete: model !== null && agent !== null && reasoningEffort !== null,
231
+ model,
232
+ agent,
233
+ reasoningEffort,
234
+ };
235
+ }
236
+ function emptyEffective(dumpStatus) {
237
+ return {
238
+ dumpStatus,
239
+ provider: null,
240
+ model: null,
241
+ agent: null,
242
+ agentPreset: null,
243
+ sessionMode: null,
244
+ retryPolicy: null,
245
+ purposeRoutes: { compaction: null, sessionTitle: null },
246
+ workspaceRootStatus: 'unknown',
247
+ workspaceSource: 'unknown',
248
+ visibleModels: { raw: null, count: null, filtered: false },
249
+ modelEffortSplit: { baseModel: null, suffixDetected: false, normalized: false },
250
+ imageInput: null,
251
+ modelCapability: { inputModalities: ['text'], imageBridge: 'unknown' },
252
+ agentCapability: {
253
+ knownPreset: null,
254
+ frontmatterValid: null,
255
+ tools: [],
256
+ viewFile: null,
257
+ writeTools: null,
258
+ commandExecutionPolicy: null,
259
+ mcpServersEmpty: null,
260
+ subagent: null,
261
+ },
262
+ dshContext: {
263
+ source: 'profile-dump',
264
+ session: { state: 'not-probed', required: false },
265
+ workspace: { state: 'not-probed', configured: false },
266
+ permission: { state: 'not-probed', preset: null },
267
+ approval: { state: 'not-probed', policy: null },
268
+ services: {
269
+ sessions: 'not-probed',
270
+ workspaceRegistry: 'not-probed',
271
+ sandboxPolicy: 'not-probed',
272
+ permissionPresets: 'not-probed',
273
+ approval: 'not-probed',
274
+ },
275
+ },
276
+ bridgeCapability: {
277
+ owner: 'unknown',
278
+ enabled: null,
279
+ agent: null,
280
+ internalTools: 'unknown',
281
+ schemaCapability: 'unknown',
282
+ legacyConfig: { agyOwned: false, workspaceRootConfigured: false },
283
+ },
284
+ };
285
+ }
286
+ function bridgeCapabilityFromConfig(toolPolicy, bundleEnabled, configuredAgent, agentCapability, workspaceRootStatus) {
287
+ const workspaceRootConfigured = workspaceRootStatus === 'configured';
288
+ if (toolPolicy === 'dsh-owned') {
289
+ return {
290
+ owner: 'dsh',
291
+ enabled: bundleEnabled,
292
+ agent: 'dsh-agy-tool-free',
293
+ internalTools: 'empty',
294
+ schemaCapability: 'prompt-contract-v1',
295
+ legacyConfig: { agyOwned: false, workspaceRootConfigured },
296
+ };
297
+ }
298
+ if (toolPolicy === 'agy-owned') {
299
+ const internalTools = agentCapability.frontmatterValid === true
300
+ && agentCapability.commandExecutionPolicy === 'off'
301
+ && agentCapability.mcpServersEmpty === true
302
+ && agentCapability.subagent === false
303
+ ? 'empty'
304
+ : agentCapability.frontmatterValid === false ? 'present' : 'unknown';
305
+ return {
306
+ owner: 'agy',
307
+ enabled: bundleEnabled,
308
+ agent: configuredAgent,
309
+ internalTools,
310
+ schemaCapability: 'agy-owned',
311
+ legacyConfig: { agyOwned: true, workspaceRootConfigured },
312
+ };
313
+ }
314
+ if (toolPolicy === 'reject') {
315
+ return {
316
+ owner: 'none',
317
+ enabled: bundleEnabled,
318
+ agent: configuredAgent,
319
+ internalTools: 'unknown',
320
+ schemaCapability: 'unsupported',
321
+ legacyConfig: { agyOwned: false, workspaceRootConfigured },
322
+ };
323
+ }
324
+ return {
325
+ owner: 'unknown',
326
+ enabled: bundleEnabled,
327
+ agent: configuredAgent,
328
+ internalTools: 'unknown',
329
+ schemaCapability: 'unknown',
330
+ legacyConfig: { agyOwned: false, workspaceRootConfigured },
331
+ };
332
+ }
333
+ function effectiveFromDump(parsed, dumpStatus) {
334
+ const values = normalizeConfigValues(parsed.values);
335
+ const imageInput = valueOf(values, 'imageInput');
336
+ const retryMax = valueOf(values, 'retryPolicy.maxRetries');
337
+ const retryCodes = valueOf(values, 'retryPolicy.retryableCodes');
338
+ const toolPolicy = valueOf(values, 'toolPolicy');
339
+ const bundleEnabled = booleanValue(valueOf(values, 'enabled'));
340
+ const workspaceRootStatus = workspaceStatus(valueOf(values, 'workspaceRoot'));
341
+ const workspaceSource = toolPolicy === 'dsh-owned' ? 'dsh-session-cwd' : workspaceRootStatus === 'configured' ? 'config-legacy' : 'unknown';
342
+ const rawVisible = valueOf(values, 'visibleModels');
343
+ const visibleCount = rawVisible === null ? null : (() => { try {
344
+ const v = JSON.parse(rawVisible);
345
+ return Array.isArray(v) ? v.length : null;
346
+ }
347
+ catch {
348
+ return rawVisible.length > 0 ? 1 : 0;
349
+ } })();
350
+ const rawModel = valueOf(values, 'model');
351
+ const suffixDetected = rawModel !== null && /-(?:low|medium|high)$/i.test(rawModel);
352
+ const baseModel = rawModel !== null ? rawModel.replace(/-(?:low|medium|high)$/i, '') : null;
353
+ const configuredAgent = valueOf(values, 'agent');
354
+ return {
355
+ dumpStatus,
356
+ provider: valueOf(values, 'provider'),
357
+ model: valueOf(values, 'model'),
358
+ agent: valueOf(values, 'agent'),
359
+ agentPreset: valueOf(values, 'agentPreset'),
360
+ sessionMode: valueOf(values, 'sessionMode'),
361
+ retryPolicy: retryMax === null && retryCodes === null
362
+ ? null
363
+ : { maxRetries: numberValue(retryMax), retryableCodes: listValue(retryCodes) },
364
+ purposeRoutes: {
365
+ compaction: routeFromDump(values, 'compaction'),
366
+ sessionTitle: routeFromDump(values, 'sessionTitle'),
367
+ },
368
+ workspaceRootStatus,
369
+ workspaceSource,
370
+ visibleModels: { raw: rawVisible, count: visibleCount, filtered: visibleCount !== null && visibleCount > 0 },
371
+ modelEffortSplit: { baseModel, suffixDetected, normalized: suffixDetected ? baseModel !== rawModel : false },
372
+ imageInput,
373
+ modelCapability: {
374
+ inputModalities: imageInput === 'experimental' ? ['text', 'image'] : ['text'],
375
+ imageBridge: imageInput === 'off' ? 'off' : imageInput === 'experimental' ? 'experimental' : 'unknown',
376
+ },
377
+ agentCapability: emptyEffective(dumpStatus).agentCapability,
378
+ dshContext: {
379
+ source: 'profile-dump',
380
+ session: { state: 'not-probed', required: toolPolicy === 'dsh-owned' },
381
+ workspace: { state: 'not-probed', configured: workspaceRootStatus === 'configured' },
382
+ permission: { state: 'not-probed', preset: null },
383
+ approval: { state: 'not-probed', policy: null },
384
+ services: {
385
+ sessions: 'not-probed',
386
+ workspaceRegistry: 'not-probed',
387
+ sandboxPolicy: 'not-probed',
388
+ permissionPresets: 'not-probed',
389
+ approval: 'not-probed',
390
+ },
391
+ },
392
+ bridgeCapability: bridgeCapabilityFromConfig(toolPolicy, bundleEnabled, configuredAgent, emptyEffective(dumpStatus).agentCapability, workspaceRootStatus),
393
+ };
394
+ }
395
+ function parseAgentFrontmatter(source) {
396
+ const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source);
397
+ if (match === null || match[1] === undefined) {
398
+ return { name: null, tools: null, commandExecutionPolicy: null, mcpServersEmpty: null, inheritMcp: null, subagent: null };
399
+ }
400
+ let name = null;
401
+ let tools = null;
402
+ let commandExecutionPolicy = null;
403
+ let mcpServersEmpty = null;
404
+ let inheritMcp = null;
405
+ let subagent = null;
406
+ let collectingTools = false;
407
+ for (const line of match[1].split(/\r?\n/)) {
408
+ const listItem = /^\s*-\s*(.+?)\s*$/.exec(line);
409
+ if (collectingTools && listItem?.[1] !== undefined) {
410
+ tools ??= [];
411
+ tools.push(stripDumpValue(listItem[1]));
412
+ continue;
413
+ }
414
+ const field = /^([A-Za-z][A-Za-z0-9_-]*):(?:\s*(.*))?$/.exec(line);
415
+ if (field === null || field[1] === undefined)
416
+ continue;
417
+ collectingTools = field[1] === 'tools' && (field[2] ?? '').trim().length === 0;
418
+ const rawValue = stripDumpValue(field[2]?.trim() ?? '');
419
+ if (field[1] === 'name')
420
+ name = rawValue || null;
421
+ if (field[1] === 'tools' && !collectingTools)
422
+ tools = [...listValue(rawValue)];
423
+ if (field[1] === 'commandExecutionPolicy')
424
+ commandExecutionPolicy = rawValue || null;
425
+ if (field[1] === 'mcpServers')
426
+ mcpServersEmpty = rawValue === '[]';
427
+ if (field[1] === 'inheritMcp')
428
+ inheritMcp = booleanValue(rawValue);
429
+ if (field[1] === 'subagent')
430
+ subagent = booleanValue(rawValue);
431
+ }
432
+ return { name, tools, commandExecutionPolicy, mcpServersEmpty, inheritMcp, subagent };
433
+ }
434
+ function presetForAgent(agent, agentPreset) {
435
+ return getAgentPreset(agentPreset) ?? listAgentPresets().find(preset => preset.agentName === agent);
436
+ }
437
+ async function inspectAgent(agent, agentPreset) {
438
+ const preset = presetForAgent(agent, agentPreset);
439
+ if (preset === undefined) {
440
+ return {
441
+ capability: {
442
+ knownPreset: agent === null ? null : false,
443
+ frontmatterValid: agent === null ? null : null,
444
+ tools: [],
445
+ viewFile: agent === null ? null : false,
446
+ writeTools: agent === null ? null : false,
447
+ commandExecutionPolicy: null,
448
+ mcpServersEmpty: null,
449
+ subagent: null,
450
+ },
451
+ ...(agent === null ? {} : {
452
+ suggestion: `Install or inspect the selected Agent explicitly; bundled options: ${listAgentPresets().map(item => item.id).join(', ')}`,
453
+ }),
454
+ };
455
+ }
456
+ try {
457
+ const frontmatter = parseAgentFrontmatter(await readAgentPresetTemplate(preset));
458
+ const actualTools = frontmatter.tools ?? [];
459
+ const toolsMatch = actualTools.length === preset.tools.length
460
+ && actualTools.every((tool, index) => tool === preset.tools[index]);
461
+ // The MCP half of the contract differs per preset: the four closed
462
+ // presets declare `mcpServers: []`, while the DSH bridge preset
463
+ // (`mcp-executor`) must inherit the host's `dsh` MCP server
464
+ // (`inheritMcp: true`, since 2b7988a) and carries no explicit list.
465
+ // Requiring `mcpServers: []` there made doctor flag its own default
466
+ // agent as invalid on every clean install.
467
+ const mcpContractSatisfied = preset.id === 'mcp-executor'
468
+ ? frontmatter.inheritMcp === true
469
+ : frontmatter.mcpServersEmpty === true;
470
+ const valid = frontmatter.name === preset.agentName
471
+ && toolsMatch
472
+ && frontmatter.commandExecutionPolicy === 'off'
473
+ && mcpContractSatisfied
474
+ && frontmatter.subagent === false;
475
+ const capability = {
476
+ knownPreset: true,
477
+ frontmatterValid: valid,
478
+ tools: actualTools,
479
+ viewFile: actualTools.includes('view_file'),
480
+ writeTools: actualTools.some(tool => AGENT_WRITE_TOOLS.has(tool)),
481
+ commandExecutionPolicy: frontmatter.commandExecutionPolicy,
482
+ mcpServersEmpty: frontmatter.mcpServersEmpty,
483
+ subagent: frontmatter.subagent,
484
+ };
485
+ return valid
486
+ ? { capability }
487
+ : {
488
+ capability,
489
+ issue: {
490
+ component: 'profile',
491
+ code: 'PROFILE_AGENT_FRONTMATTER_INVALID',
492
+ message: `Bundled Agent ${preset.agentName} does not match its declared tool and execution policy`,
493
+ },
494
+ suggestion: `Reinstall the bundled Agent with: npx dsh-agy-cli agents install ${preset.id} --apply`,
495
+ };
496
+ }
497
+ catch {
498
+ return {
499
+ capability: {
500
+ knownPreset: true,
501
+ frontmatterValid: false,
502
+ tools: [],
503
+ viewFile: preset.tools.includes('view_file'),
504
+ writeTools: preset.tools.some(tool => AGENT_WRITE_TOOLS.has(tool)),
505
+ commandExecutionPolicy: null,
506
+ mcpServersEmpty: null,
507
+ subagent: null,
508
+ },
509
+ issue: {
510
+ component: 'profile',
511
+ code: 'PROFILE_AGENT_FRONTMATTER_UNREADABLE',
512
+ message: `Could not read the bundled Agent template for ${preset.agentName}`,
513
+ },
514
+ suggestion: `Reinstall the bundled Agent with: npx dsh-agy-cli agents install ${preset.id} --apply`,
515
+ };
516
+ }
517
+ }
518
+ export async function diagnoseProfile(options) {
519
+ const profileName = options.profileName.trim();
520
+ const dshHome = resolveDshHome(options.dshHome);
521
+ const dshHomePresent = existsSync(dshHome);
522
+ const validProfileName = isSafeProfileName(profileName);
523
+ const profileDir = validProfileName ? join(dshHome, 'profiles', profileName) : '';
524
+ const profilePresent = dshHomePresent && existsSync(profileDir);
525
+ const issues = [];
526
+ if (!validProfileName) {
527
+ return {
528
+ profileSchemaVersion: 4,
529
+ name: '<invalid>',
530
+ dshHomePresent,
531
+ profilePresent: false,
532
+ packageInstalled: false,
533
+ bundleDeclared: false,
534
+ bundleEnabled: null,
535
+ toolPolicy: null,
536
+ effectiveProvider: null,
537
+ effectiveModel: null,
538
+ effective: emptyEffective('not-run'),
539
+ repairSuggestions: [],
540
+ issues: [{
541
+ component: 'profile',
542
+ code: 'PROFILE_NAME_INVALID',
543
+ message: 'Profile name must contain 1-64 letters, numbers, dots, underscores, or hyphens and must not contain path separators',
544
+ }],
545
+ };
546
+ }
547
+ let packageInstalled = false;
548
+ let bundleDeclared = false;
549
+ let bundleEnabled = null;
550
+ let toolPolicy = null;
551
+ let effectiveProvider = null;
552
+ let effectiveModel = null;
553
+ let effective = emptyEffective('not-run');
554
+ const repairSuggestions = [];
555
+ const addSuggestion = (suggestion) => {
556
+ if (!repairSuggestions.includes(suggestion))
557
+ repairSuggestions.push(suggestion);
558
+ };
559
+ if (!dshHomePresent) {
560
+ issues.push({
561
+ component: 'profile',
562
+ code: 'DSH_HOME_NOT_FOUND',
563
+ message: `DSH home directory not found: run DSH once or set DSH_HOME`,
564
+ });
565
+ }
566
+ else if (!profilePresent) {
567
+ issues.push({
568
+ component: 'profile',
569
+ code: 'PROFILE_NOT_FOUND',
570
+ message: `Profile "${profileName}" not found. Create with: npx @deepseek-ai/dsh plugin --profile ${profileName} add dsh-agy-cli`,
571
+ });
572
+ }
573
+ else {
574
+ const profilePackageJsonPath = join(profileDir, 'package.json');
575
+ if (existsSync(profilePackageJsonPath)) {
576
+ try {
577
+ const pkg = JSON.parse(readFileSync(profilePackageJsonPath, 'utf8'));
578
+ const deps = pkg.dependencies ?? {};
579
+ const devDeps = pkg.devDependencies ?? {};
580
+ packageInstalled = Boolean(deps['dsh-agy-cli'] || devDeps['dsh-agy-cli']);
581
+ const bundles = pkg.dsh?.profile?.bundles;
582
+ bundleDeclared = Array.isArray(bundles) && bundles.includes('dsh-agy-cli');
583
+ }
584
+ catch {
585
+ issues.push({
586
+ component: 'profile',
587
+ code: 'PROFILE_PACKAGE_JSON_INVALID',
588
+ message: `Could not parse package.json for profile "${profileName}"`,
589
+ });
590
+ }
591
+ }
592
+ if (!packageInstalled) {
593
+ issues.push({
594
+ component: 'profile',
595
+ code: 'PROFILE_PACKAGE_MISSING',
596
+ message: `Package dsh-agy-cli is not installed in profile "${profileName}". Run: npx @deepseek-ai/dsh plugin --profile ${profileName} add dsh-agy-cli`,
597
+ });
598
+ }
599
+ else if (!bundleDeclared) {
600
+ issues.push({
601
+ component: 'profile',
602
+ code: 'PROFILE_BUNDLE_MISSING',
603
+ message: `Bundle dsh-agy-cli is not declared in dsh.profile.bundles for profile "${profileName}". Re-run: npx @deepseek-ai/dsh plugin --profile ${profileName} add dsh-agy-cli`,
604
+ });
605
+ }
606
+ // Inspect effective dump-config. Failure is part of the diagnostic result;
607
+ // it must not silently turn into a false "unknown but healthy" profile.
608
+ const runCmd = options.runCommand ?? defaultRunCommand;
609
+ const customBin = options.dshBin?.trim() || process.env.DSH_BIN?.trim();
610
+ const dshCandidates = [
611
+ ...(customBin ? [customBin] : []),
612
+ join(profileDir, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'),
613
+ join(dirname(profileDir), '..', 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js'),
614
+ ];
615
+ const pathDshCommand = process.platform === 'win32' ? 'dsh.cmd' : 'dsh';
616
+ const dshEntry = options.runCommand !== undefined
617
+ ? (customBin ?? pathDshCommand)
618
+ : dshCandidates.find(p => existsSync(p))
619
+ ?? pathDshCommand;
620
+ // dshEntry always resolves (the PATH fallback is unconditional), so there
621
+ // is no "missing" branch to guard: a dump is always attempted.
622
+ let dumpStatus = 'not-run';
623
+ {
624
+ try {
625
+ const invocation = resolveDshInvocation(dshEntry);
626
+ const args = [...invocation.prefixArgs, '--profile', profileName, '--dump-config'];
627
+ const dumpResult = await runCmd(invocation.executable, args, {
628
+ timeoutMs: options.timeoutMs ?? 10_000,
629
+ cwd: profileDir,
630
+ env: { ...process.env, DSH_HOME: dshHome },
631
+ });
632
+ if (dumpResult.timedOut) {
633
+ dumpStatus = 'timeout';
634
+ issues.push({
635
+ component: 'profile',
636
+ code: 'PROFILE_DUMP_TIMEOUT',
637
+ message: `DSH --dump-config timed out for profile "${profileName}"`,
638
+ });
639
+ addSuggestion(`Retry the read-only check: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
640
+ }
641
+ else if (dumpResult.exitCode !== 0) {
642
+ dumpStatus = 'nonzero';
643
+ issues.push({
644
+ component: 'profile',
645
+ code: 'PROFILE_DUMP_NONZERO',
646
+ message: `DSH --dump-config exited unsuccessfully for profile "${profileName}"`,
647
+ });
648
+ addSuggestion(`Run the read-only check directly: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
649
+ }
650
+ else {
651
+ const parsed = parseDumpConfig(dumpResult.stdout);
652
+ if (!parsed.recognized || parsed.malformed) {
653
+ dumpStatus = 'parse-error';
654
+ issues.push({
655
+ component: 'profile',
656
+ code: 'PROFILE_DUMP_PARSE_FAILED',
657
+ message: `DSH --dump-config did not contain a valid dsh-agy-cli section for profile "${profileName}"`,
658
+ });
659
+ addSuggestion(`Inspect the read-only dump format: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
660
+ }
661
+ else {
662
+ dumpStatus = 'ok';
663
+ effective = effectiveFromDump(parsed, dumpStatus);
664
+ const configValues = normalizeConfigValues(parsed.values);
665
+ bundleEnabled = booleanValue(valueOf(configValues, 'enabled'));
666
+ toolPolicy = valueOf(configValues, 'toolPolicy');
667
+ effectiveProvider = effective.provider;
668
+ effectiveModel = effective.model;
669
+ const agentInspection = await inspectAgent(effective.agent, effective.agentPreset);
670
+ effective = {
671
+ ...effective,
672
+ agentCapability: agentInspection.capability,
673
+ bridgeCapability: bridgeCapabilityFromConfig(toolPolicy, bundleEnabled, effective.agent, agentInspection.capability, effective.workspaceRootStatus),
674
+ };
675
+ if (agentInspection.issue !== undefined)
676
+ issues.push(agentInspection.issue);
677
+ if (agentInspection.suggestion !== undefined)
678
+ addSuggestion(agentInspection.suggestion);
679
+ if (toolPolicy === 'agy-owned') {
680
+ issues.push({
681
+ component: 'profile',
682
+ code: 'PROFILE_TOOL_POLICY_DEPRECATED',
683
+ message: 'toolPolicy "agy-owned" is a legacy compatibility mode; DSH-owned execution is recommended and the profile was not changed',
684
+ });
685
+ addSuggestion('Set toolPolicy: dsh-owned after confirming the DSH permission and approval services are available');
686
+ }
687
+ if (toolPolicy === 'dsh-owned' && effective.bridgeCapability.internalTools === 'present') {
688
+ issues.push({
689
+ component: 'profile',
690
+ code: 'DSH_BRIDGE_AGENT_INTERNAL_TOOLS',
691
+ message: 'The DSH-owned bridge Agent exposes internal AGY tools; the bridge requires the bundled tool-free Agent',
692
+ });
693
+ addSuggestion('Use the bundled dsh-agy-tool-free Agent for toolPolicy: dsh-owned');
694
+ }
695
+ if (toolPolicy === 'reject' && bundleEnabled === true) {
696
+ issues.push({
697
+ component: 'profile',
698
+ code: 'DSH_SCHEMA_UNSUPPORTED',
699
+ message: 'The active profile rejects DSH tool schemas; tool bridge capability is unavailable until toolPolicy is explicitly selected',
700
+ });
701
+ }
702
+ // 0.9.0: dsh-owned uses DSH session cwd, no workspaceRoot required; only warn for legacy agy-owned
703
+ if (toolPolicy === 'dsh-owned' && effective.workspaceRootStatus === 'configured') {
704
+ issues.push({
705
+ component: 'profile',
706
+ code: 'DEPRECATED_WORKSPACE_ROOT',
707
+ message: 'workspaceRoot is deprecated for toolPolicy dsh-owned; DSH session workspace is used automatically',
708
+ });
709
+ addSuggestion('Remove workspaceRoot from profile; open the project folder in DSH and rely on session workspace');
710
+ }
711
+ if (effective.modelEffortSplit.suffixDetected) {
712
+ issues.push({
713
+ component: 'profile',
714
+ code: 'DEPRECATED_MODEL_EFFORT_SUFFIX',
715
+ message: `Model id "${effective.model}" contains -low/-medium/-high suffix; use base model "${effective.modelEffortSplit.baseModel}" and select reasoningEffort separately`,
716
+ });
717
+ addSuggestion('Set model to ' + effective.modelEffortSplit.baseModel + ' and choose reasoningEffort low/medium/high via DSH');
718
+ }
719
+ if (effective.agentCapability.writeTools === true
720
+ && effective.workspaceRootStatus !== 'configured'
721
+ && toolPolicy !== 'dsh-owned') {
722
+ issues.push({
723
+ component: 'profile',
724
+ code: 'PROFILE_WORKSPACE_REQUIRED',
725
+ message: 'The selected Agent can write files but no existing workspace directory is configured',
726
+ });
727
+ addSuggestion('Set workspaceRoot to an existing project directory before enabling workspace-write');
728
+ }
729
+ for (const purpose of ['compaction', 'sessionTitle']) {
730
+ const route = effective.purposeRoutes[purpose];
731
+ if (route?.complete === false) {
732
+ issues.push({
733
+ component: 'profile',
734
+ code: 'PROFILE_PURPOSE_ROUTE_INCOMPLETE',
735
+ message: `Purpose route "${purpose}" is missing model, agent, or reasoningEffort`,
736
+ });
737
+ addSuggestion(`Complete or remove purposeRoutes.${purpose} in the profile configuration`);
738
+ }
739
+ }
740
+ if (effective.retryPolicy !== null
741
+ && (effective.retryPolicy.maxRetries === null || effective.retryPolicy.maxRetries > 5)) {
742
+ issues.push({
743
+ component: 'profile',
744
+ code: 'PROFILE_RETRY_POLICY_INVALID',
745
+ message: 'retryPolicy.maxRetries must be an integer from 0 through 5',
746
+ });
747
+ addSuggestion('Set retryPolicy.maxRetries to an integer from 0 through 5 in the profile configuration');
748
+ }
749
+ }
750
+ }
751
+ }
752
+ catch {
753
+ dumpStatus = 'unavailable';
754
+ issues.push({
755
+ component: 'profile',
756
+ code: 'PROFILE_DUMP_UNAVAILABLE',
757
+ message: `Could not execute DSH --dump-config for profile "${profileName}"`,
758
+ });
759
+ addSuggestion(`Verify the DSH executable and retry: npx @deepseek-ai/dsh --profile ${profileName} --dump-config`);
760
+ }
761
+ }
762
+ if (dumpStatus !== 'ok')
763
+ effective = emptyEffective(dumpStatus);
764
+ // Check bundleEnabled and toolPolicy warnings
765
+ if (bundleEnabled === false) {
766
+ issues.push({
767
+ component: 'profile',
768
+ code: 'PROFILE_BUNDLE_DISABLED',
769
+ message: `Bundle dsh-agy-cli is disabled in profile "${profileName}". Enable in cordis.patch.yml or profile config`,
770
+ });
771
+ }
772
+ if (toolPolicy === 'reject' && profileName === 'web') {
773
+ issues.push({
774
+ component: 'profile',
775
+ code: 'PROFILE_TOOL_POLICY_REJECT',
776
+ message: `Profile "${profileName}" carries tool schemas by default, but toolPolicy is "reject". Set toolPolicy: dsh-owned to let DSH execute tools, or agy-owned for legacy AGY-owned execution`,
777
+ });
778
+ }
779
+ }
780
+ return {
781
+ profileSchemaVersion: 4,
782
+ name: profileName,
783
+ dshHomePresent,
784
+ profilePresent,
785
+ packageInstalled,
786
+ bundleDeclared,
787
+ bundleEnabled,
788
+ toolPolicy,
789
+ effectiveProvider,
790
+ effectiveModel,
791
+ effective,
792
+ repairSuggestions,
793
+ issues,
794
+ };
795
+ }
796
+ export async function runDoctor(options = {}) {
797
+ const providerResult = await diagnoseProvider(options);
798
+ let profileResult;
799
+ const profileName = options.profile?.trim();
800
+ if (profileName !== undefined && profileName.length > 0) {
801
+ profileResult = await diagnoseProfile({
802
+ profileName,
803
+ ...(options.dshHome === undefined ? {} : { dshHome: options.dshHome }),
804
+ ...(options.dshBin === undefined ? {} : { dshBin: options.dshBin }),
805
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
806
+ ...(options.runProfileCommand === undefined ? {} : { runCommand: options.runProfileCommand }),
807
+ });
808
+ }
809
+ const allErrors = [...providerResult.errors];
810
+ if (profileResult !== undefined) {
811
+ allErrors.push(...profileResult.issues);
812
+ }
813
+ return {
814
+ ...providerResult,
815
+ ok: allErrors.length === 0,
816
+ ...(profileResult === undefined ? {} : { profile: profileResult }),
817
+ errors: allErrors,
818
+ };
819
+ }
820
+ export function formatDoctorHuman(result) {
821
+ const lines = [];
822
+ lines.push(`dsh-agy-cli doctor (schema v${result.schemaVersion})`);
823
+ lines.push(`status: ${result.ok ? 'PASS' : 'FAIL'}; quotaUsed: ${result.quotaUsed}`);
824
+ lines.push(`plugin: ${result.plugin.name}@${result.plugin.version ?? 'unknown'}; enabled: ${result.plugin.enabled}`);
825
+ lines.push(`node: ${result.node.version} (${result.node.supported ? 'supported' : 'unsupported'})`);
826
+ lines.push(`dsh: cli=${result.dsh.cliVersion ?? 'unknown'}; llm=${result.dsh.llmContractVersion ?? 'unknown'}; bundle=${result.dsh.bundlePatchPresent ? 'present' : 'missing'}`);
827
+ if (result.profile !== undefined) {
828
+ const p = result.profile;
829
+ lines.push(`profile [${p.name}]: exists=${p.profilePresent}; packageInstalled=${p.packageInstalled}; bundleDeclared=${p.bundleDeclared}; enabled=${p.bundleEnabled ?? 'unknown'}; toolPolicy=${p.toolPolicy ?? 'unknown'}`);
830
+ lines.push(`effective: dump=${p.effective.dumpStatus}; provider=${p.effective.provider ?? 'unknown'}; model=${p.effective.model ?? 'unknown'}; agent=${p.effective.agent ?? 'unknown'}; sessionMode=${p.effective.sessionMode ?? 'unknown'}; imageInput=${p.effective.imageInput ?? 'unknown'}; inputModalities=${p.effective.modelCapability.inputModalities.join(',')}`);
831
+ lines.push(`agentCapability: preset=${p.effective.agentCapability.knownPreset ?? 'unknown'}; frontmatter=${p.effective.agentCapability.frontmatterValid ?? 'unknown'}; view_file=${p.effective.agentCapability.viewFile ?? 'unknown'}; writeTools=${p.effective.agentCapability.writeTools ?? 'unknown'}; workspace=${p.effective.workspaceRootStatus}`);
832
+ lines.push(`dshContext: session=${p.effective.dshContext.session.state}; workspace=${p.effective.dshContext.workspace.configured ? 'configured' : 'not-configured'}; permission=${p.effective.dshContext.permission.state}; approval=${p.effective.dshContext.approval.state}`);
833
+ lines.push(`bridge: owner=${p.effective.bridgeCapability.owner}; agent=${p.effective.bridgeCapability.agent ?? 'unknown'}; schema=${p.effective.bridgeCapability.schemaCapability}; internalTools=${p.effective.bridgeCapability.internalTools}`);
834
+ if (p.repairSuggestions.length > 0) {
835
+ lines.push('repair suggestions:');
836
+ for (const suggestion of p.repairSuggestions)
837
+ lines.push(`- ${suggestion}`);
838
+ }
839
+ }
840
+ lines.push(`provider: ${result.configuration.provider}; agent=${result.configuration.agent}; defaultModel=${result.configuration.defaultModel}; toolPolicy=${result.configuration.toolPolicy}; transport=${result.configuration.transport ?? 'one-shot'}`);
841
+ lines.push(`transport: persistentIdleTtlMs=${result.configuration.persistentIdleTtlMs ?? 30000}; readyTimeout=${result.configuration.persistentReadyTimeoutMs ?? 10000}; fallback=${result.configuration.persistentFallback ?? 'before-accept'}`);
842
+ lines.push(`models: ${result.models.map(m => `${m.id} (${m.name})`).join(', ') || 'none'}`);
843
+ lines.push(`modelCatalog: source=${result.modelCatalog.source}; stale=${result.modelCatalog.stale}; warningCode=${result.modelCatalog.warningCode ?? 'none'}`);
844
+ lines.push(`agy: version=${result.agy.version ?? 'unknown'}; agents=${result.agy.agents.join(', ') || 'none'}; executableSource=${result.agy.executableSource}`);
845
+ if (result.errors.length > 0) {
846
+ lines.push('issues:');
847
+ for (const issue of result.errors) {
848
+ lines.push(`- [${issue.component}/${issue.code}] ${issue.message}`);
849
+ }
850
+ }
851
+ return lines.join('\n');
852
+ }
853
+ export function formatDoctorHelp() {
854
+ return `Usage: dsh-agy-cli [doctor] [options]
855
+
856
+ Quota-free diagnostic CLI v3 for dsh-agy-cli and DSH profiles.
857
+
858
+ Commands:
859
+ doctor Run diagnostic checks (default)
860
+
861
+ Options:
862
+ --profile <name> DSH profile to inspect (e.g. web, headless)
863
+ --dsh-home <path> Custom DSH home directory path
864
+ --dsh-bin <path> Custom DSH CLI executable / entry path
865
+ --json Output structured JSON
866
+ -v, --version Print version and exit
867
+ -h, --help Show help information
868
+
869
+ Examples:
870
+ npx dsh-agy-cli doctor
871
+ npx dsh-agy-cli doctor --profile web
872
+ npx dsh-agy-cli doctor --profile web --json
873
+ `;
874
+ }
875
+ export async function runDoctorCli(argv, stdout = text => process.stdout.write(text), stderr = text => process.stderr.write(text)) {
876
+ const args = [...argv];
877
+ if (args[0] === 'doctor')
878
+ args.shift();
879
+ if (args.includes('-h') || args.includes('--help')) {
880
+ stdout(`${formatDoctorHelp()}\n`);
881
+ return 0;
882
+ }
883
+ if (args.includes('-v') || args.includes('--version')) {
884
+ stdout(`${packageMetadata.version ?? 'unknown'}\n`);
885
+ return 0;
886
+ }
887
+ const jsonOutput = args.includes('--json');
888
+ let profile;
889
+ const profileIdx = args.indexOf('--profile');
890
+ if (profileIdx !== -1 && profileIdx + 1 < args.length) {
891
+ profile = args[profileIdx + 1];
892
+ }
893
+ let dshHome;
894
+ const dshHomeIdx = args.indexOf('--dsh-home');
895
+ if (dshHomeIdx !== -1 && dshHomeIdx + 1 < args.length) {
896
+ dshHome = args[dshHomeIdx + 1];
897
+ }
898
+ let dshBin;
899
+ const dshBinIdx = args.indexOf('--dsh-bin');
900
+ if (dshBinIdx !== -1 && dshBinIdx + 1 < args.length) {
901
+ dshBin = args[dshBinIdx + 1];
902
+ }
903
+ try {
904
+ // Do NOT pre-resolve the config here. `enabled` is a `.volatile()` field,
905
+ // so one pass through the schema replaces it with a host-style reference
906
+ // object (`{ get() }`); feeding that back through the same schema (which
907
+ // diagnoseProvider does for its own defaults) throws
908
+ // `$.enabled expected boolean but got [object Object]`, and the whole
909
+ // doctor command dies as DOCTOR_EXECUTION_FAILED. diagnoseProvider already
910
+ // parses `options.config ?? {}` exactly once on its own.
911
+ const result = await runDoctor({
912
+ ...(profile === undefined ? {} : { profile }),
913
+ ...(dshHome === undefined ? {} : { dshHome }),
914
+ ...(dshBin === undefined ? {} : { dshBin }),
915
+ });
916
+ if (jsonOutput) {
917
+ stdout(`${JSON.stringify(result, null, 2)}\n`);
918
+ }
919
+ else {
920
+ stdout(`${formatDoctorHuman(result)}\n`);
921
+ }
922
+ return result.ok ? 0 : 1;
923
+ }
924
+ catch (error) {
925
+ const message = redactText(error instanceof Error ? error.message : String(error), 512);
926
+ if (jsonOutput) {
927
+ stdout(JSON.stringify({
928
+ schemaVersion: 1,
929
+ ok: false,
930
+ quotaUsed: false,
931
+ errors: [{ component: 'config', code: 'DOCTOR_EXECUTION_FAILED', message }],
932
+ }, null, 2) + '\n');
933
+ }
934
+ else {
935
+ stderr(`Error running doctor: ${message}\n`);
936
+ }
937
+ return 1;
938
+ }
939
+ }
940
+ //# sourceMappingURL=doctor.js.map