@build-astron-co/nimbus 0.2.0
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/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor Command
|
|
3
|
+
*
|
|
4
|
+
* Run diagnostic checks on Nimbus installation and configuration
|
|
5
|
+
*
|
|
6
|
+
* Usage: nimbus doctor [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../utils';
|
|
10
|
+
import { ui } from '../wizard';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Command options
|
|
14
|
+
*/
|
|
15
|
+
export interface DoctorOptions {
|
|
16
|
+
fix?: boolean;
|
|
17
|
+
verbose?: boolean;
|
|
18
|
+
json?: boolean;
|
|
19
|
+
metrics?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check result structure
|
|
24
|
+
*/
|
|
25
|
+
interface CheckResult {
|
|
26
|
+
name: string;
|
|
27
|
+
passed: boolean;
|
|
28
|
+
message?: string;
|
|
29
|
+
error?: string;
|
|
30
|
+
details?: Record<string, unknown>;
|
|
31
|
+
fix?: string;
|
|
32
|
+
runFix?: () => Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Diagnostic check function type
|
|
37
|
+
*/
|
|
38
|
+
type DiagnosticCheck = (options: DoctorOptions) => Promise<CheckResult>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check configuration files
|
|
42
|
+
*/
|
|
43
|
+
async function checkConfiguration(options: DoctorOptions): Promise<CheckResult> {
|
|
44
|
+
const fs = await import('fs/promises');
|
|
45
|
+
const path = await import('path');
|
|
46
|
+
const os = await import('os');
|
|
47
|
+
|
|
48
|
+
const configDir = path.join(os.homedir(), '.nimbus');
|
|
49
|
+
const configFile = path.join(configDir, 'config.json');
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(configDir);
|
|
53
|
+
} catch {
|
|
54
|
+
return {
|
|
55
|
+
name: 'Configuration',
|
|
56
|
+
passed: false,
|
|
57
|
+
error: 'Configuration directory not found',
|
|
58
|
+
fix: 'Run "nimbus init" to create configuration',
|
|
59
|
+
runFix: async () => {
|
|
60
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(configFile);
|
|
67
|
+
const content = await fs.readFile(configFile, 'utf-8');
|
|
68
|
+
JSON.parse(content); // Validate JSON
|
|
69
|
+
return {
|
|
70
|
+
name: 'Configuration',
|
|
71
|
+
passed: true,
|
|
72
|
+
message: 'Configuration file valid',
|
|
73
|
+
details: options.verbose ? { path: configFile } : undefined,
|
|
74
|
+
};
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
if (error.code === 'ENOENT') {
|
|
77
|
+
return {
|
|
78
|
+
name: 'Configuration',
|
|
79
|
+
passed: false,
|
|
80
|
+
error: 'Configuration file not found',
|
|
81
|
+
fix: 'Run "nimbus config init" to create configuration',
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
name: 'Configuration',
|
|
86
|
+
passed: false,
|
|
87
|
+
error: `Invalid configuration: ${error.message}`,
|
|
88
|
+
fix: 'Run "nimbus config reset" to reset configuration',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check LLM provider configuration
|
|
95
|
+
*/
|
|
96
|
+
async function checkLLMProvider(options: DoctorOptions): Promise<CheckResult> {
|
|
97
|
+
const fs = await import('fs/promises');
|
|
98
|
+
const path = await import('path');
|
|
99
|
+
const os = await import('os');
|
|
100
|
+
|
|
101
|
+
// Check for API keys
|
|
102
|
+
const envKeys = ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'AWS_ACCESS_KEY_ID'];
|
|
103
|
+
const foundKeys: string[] = [];
|
|
104
|
+
|
|
105
|
+
for (const key of envKeys) {
|
|
106
|
+
if (process.env[key]) {
|
|
107
|
+
foundKeys.push(key);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check credentials file
|
|
112
|
+
const credentialsFile = path.join(os.homedir(), '.nimbus', 'credentials.json');
|
|
113
|
+
let hasStoredCredentials = false;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await fs.access(credentialsFile);
|
|
117
|
+
const content = await fs.readFile(credentialsFile, 'utf-8');
|
|
118
|
+
const creds = JSON.parse(content);
|
|
119
|
+
hasStoredCredentials = Object.keys(creds.providers || {}).length > 0;
|
|
120
|
+
} catch {
|
|
121
|
+
// No stored credentials
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (foundKeys.length === 0 && !hasStoredCredentials) {
|
|
125
|
+
return {
|
|
126
|
+
name: 'LLM Provider',
|
|
127
|
+
passed: false,
|
|
128
|
+
error: 'No LLM provider configured',
|
|
129
|
+
fix: 'Run "nimbus login" to configure an LLM provider',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Try to verify LLM service is reachable
|
|
134
|
+
const llmUrl = process.env.LLM_SERVICE_URL || 'http://localhost:3002';
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(`${llmUrl}/health`, {
|
|
138
|
+
signal: AbortSignal.timeout(3000),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (response.ok) {
|
|
142
|
+
return {
|
|
143
|
+
name: 'LLM Provider',
|
|
144
|
+
passed: true,
|
|
145
|
+
message: 'LLM service connected',
|
|
146
|
+
details: options.verbose
|
|
147
|
+
? {
|
|
148
|
+
envKeys: foundKeys,
|
|
149
|
+
hasStoredCredentials,
|
|
150
|
+
serviceUrl: llmUrl,
|
|
151
|
+
}
|
|
152
|
+
: undefined,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// Service not available, but that's okay if we have credentials
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
name: 'LLM Provider',
|
|
161
|
+
passed: true,
|
|
162
|
+
message: hasStoredCredentials ? 'Credentials configured' : `Using ${foundKeys.join(', ')}`,
|
|
163
|
+
details: options.verbose
|
|
164
|
+
? {
|
|
165
|
+
envKeys: foundKeys,
|
|
166
|
+
hasStoredCredentials,
|
|
167
|
+
}
|
|
168
|
+
: undefined,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check cloud credentials (AWS, etc.)
|
|
174
|
+
*/
|
|
175
|
+
async function checkCloudCredentials(options: DoctorOptions): Promise<CheckResult> {
|
|
176
|
+
const fs = await import('fs/promises');
|
|
177
|
+
const path = await import('path');
|
|
178
|
+
const os = await import('os');
|
|
179
|
+
|
|
180
|
+
const checks: string[] = [];
|
|
181
|
+
|
|
182
|
+
// Check AWS credentials
|
|
183
|
+
const awsConfigDir = path.join(os.homedir(), '.aws');
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await fs.access(path.join(awsConfigDir, 'credentials'));
|
|
187
|
+
checks.push('AWS credentials');
|
|
188
|
+
} catch {
|
|
189
|
+
// Check environment variables
|
|
190
|
+
if (process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY) {
|
|
191
|
+
checks.push('AWS (env vars)');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Check GCP credentials
|
|
196
|
+
if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
|
|
197
|
+
try {
|
|
198
|
+
await fs.access(process.env.GOOGLE_APPLICATION_CREDENTIALS);
|
|
199
|
+
checks.push('GCP credentials');
|
|
200
|
+
} catch {
|
|
201
|
+
// Invalid path
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check Azure credentials
|
|
206
|
+
if (process.env.AZURE_CLIENT_ID || process.env.AZURE_SUBSCRIPTION_ID) {
|
|
207
|
+
checks.push('Azure (env vars)');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check kubeconfig
|
|
211
|
+
const kubeconfigPath = process.env.KUBECONFIG || path.join(os.homedir(), '.kube', 'config');
|
|
212
|
+
try {
|
|
213
|
+
await fs.access(kubeconfigPath);
|
|
214
|
+
checks.push('Kubernetes');
|
|
215
|
+
} catch {
|
|
216
|
+
// No kubeconfig
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (checks.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
name: 'Cloud Credentials',
|
|
222
|
+
passed: false,
|
|
223
|
+
error: 'No cloud credentials found',
|
|
224
|
+
fix: 'Configure AWS credentials (~/.aws/credentials) or set environment variables',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
name: 'Cloud Credentials',
|
|
230
|
+
passed: true,
|
|
231
|
+
message: `Found: ${checks.join(', ')}`,
|
|
232
|
+
details: options.verbose ? { providers: checks } : undefined,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check cloud connectivity (real API calls)
|
|
238
|
+
*/
|
|
239
|
+
async function checkCloudConnectivity(options: DoctorOptions): Promise<CheckResult> {
|
|
240
|
+
const { execFileSync } = await import('child_process');
|
|
241
|
+
|
|
242
|
+
const results: Array<{ provider: string; status: string; details?: string }> = [];
|
|
243
|
+
|
|
244
|
+
// AWS: try sts get-caller-identity
|
|
245
|
+
try {
|
|
246
|
+
const output = execFileSync('aws', ['sts', 'get-caller-identity', '--output', 'json'], {
|
|
247
|
+
encoding: 'utf-8',
|
|
248
|
+
timeout: 10000,
|
|
249
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
250
|
+
});
|
|
251
|
+
const identity = JSON.parse(output);
|
|
252
|
+
results.push({
|
|
253
|
+
provider: 'AWS',
|
|
254
|
+
status: 'connected',
|
|
255
|
+
details: `Account: ${identity.Account}, User: ${identity.UserId}`,
|
|
256
|
+
});
|
|
257
|
+
} catch (error: any) {
|
|
258
|
+
if (error.code === 'ENOENT') {
|
|
259
|
+
results.push({
|
|
260
|
+
provider: 'AWS',
|
|
261
|
+
status: 'not installed',
|
|
262
|
+
details: 'Install AWS CLI: https://aws.amazon.com/cli/',
|
|
263
|
+
});
|
|
264
|
+
} else {
|
|
265
|
+
results.push({
|
|
266
|
+
provider: 'AWS',
|
|
267
|
+
status: 'failed',
|
|
268
|
+
details: 'Run "aws configure" or check credentials',
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// GCP: try gcloud auth print-access-token
|
|
274
|
+
try {
|
|
275
|
+
const output = execFileSync('gcloud', ['auth', 'print-access-token'], {
|
|
276
|
+
encoding: 'utf-8',
|
|
277
|
+
timeout: 10000,
|
|
278
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
279
|
+
});
|
|
280
|
+
if (output.trim().length > 0) {
|
|
281
|
+
results.push({ provider: 'GCP', status: 'connected', details: 'Access token valid' });
|
|
282
|
+
} else {
|
|
283
|
+
results.push({ provider: 'GCP', status: 'failed', details: 'Run "gcloud auth login"' });
|
|
284
|
+
}
|
|
285
|
+
} catch (error: any) {
|
|
286
|
+
if (error.code === 'ENOENT') {
|
|
287
|
+
results.push({
|
|
288
|
+
provider: 'GCP',
|
|
289
|
+
status: 'not installed',
|
|
290
|
+
details: 'Install gcloud: https://cloud.google.com/sdk/docs/install',
|
|
291
|
+
});
|
|
292
|
+
} else {
|
|
293
|
+
results.push({ provider: 'GCP', status: 'failed', details: 'Run "gcloud auth login"' });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Azure: try az account show
|
|
298
|
+
try {
|
|
299
|
+
const output = execFileSync('az', ['account', 'show', '--output', 'json'], {
|
|
300
|
+
encoding: 'utf-8',
|
|
301
|
+
timeout: 10000,
|
|
302
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
303
|
+
});
|
|
304
|
+
const account = JSON.parse(output);
|
|
305
|
+
results.push({
|
|
306
|
+
provider: 'Azure',
|
|
307
|
+
status: 'connected',
|
|
308
|
+
details: `Subscription: ${account.name || account.id}`,
|
|
309
|
+
});
|
|
310
|
+
} catch (error: any) {
|
|
311
|
+
if (error.code === 'ENOENT') {
|
|
312
|
+
results.push({
|
|
313
|
+
provider: 'Azure',
|
|
314
|
+
status: 'not installed',
|
|
315
|
+
details: 'Install Azure CLI: https://learn.microsoft.com/en-us/cli/azure/install-azure-cli',
|
|
316
|
+
});
|
|
317
|
+
} else {
|
|
318
|
+
results.push({ provider: 'Azure', status: 'failed', details: 'Run "az login"' });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const connected = results.filter(r => r.status === 'connected');
|
|
323
|
+
|
|
324
|
+
if (connected.length === 0) {
|
|
325
|
+
const installed = results.filter(r => r.status !== 'not installed');
|
|
326
|
+
if (installed.length === 0) {
|
|
327
|
+
return {
|
|
328
|
+
name: 'Cloud Connectivity',
|
|
329
|
+
passed: true,
|
|
330
|
+
message: 'No cloud CLIs installed (optional)',
|
|
331
|
+
details: options.verbose ? { providers: results } : undefined,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
name: 'Cloud Connectivity',
|
|
336
|
+
passed: false,
|
|
337
|
+
error: 'No cloud provider connected',
|
|
338
|
+
fix: results
|
|
339
|
+
.map(r => r.details)
|
|
340
|
+
.filter(Boolean)
|
|
341
|
+
.join('; '),
|
|
342
|
+
details: options.verbose ? { providers: results } : undefined,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
name: 'Cloud Connectivity',
|
|
348
|
+
passed: true,
|
|
349
|
+
message: connected.map(r => `${r.provider}: ${r.details}`).join(', '),
|
|
350
|
+
details: options.verbose ? { providers: results } : undefined,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check core services
|
|
356
|
+
*/
|
|
357
|
+
async function checkCoreServices(options: DoctorOptions): Promise<CheckResult> {
|
|
358
|
+
const services = [
|
|
359
|
+
{ name: 'Core Engine', url: process.env.CORE_ENGINE_URL || 'http://localhost:3001' },
|
|
360
|
+
{ name: 'LLM Service', url: process.env.LLM_SERVICE_URL || 'http://localhost:3002' },
|
|
361
|
+
{ name: 'Generator', url: process.env.GENERATOR_SERVICE_URL || 'http://localhost:3003' },
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const results: Array<{ name: string; status: string; url?: string }> = [];
|
|
365
|
+
let anyAvailable = false;
|
|
366
|
+
|
|
367
|
+
for (const service of services) {
|
|
368
|
+
try {
|
|
369
|
+
const response = await fetch(`${service.url}/health`, {
|
|
370
|
+
signal: AbortSignal.timeout(2000),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (response.ok) {
|
|
374
|
+
results.push({
|
|
375
|
+
name: service.name,
|
|
376
|
+
status: 'running',
|
|
377
|
+
url: options.verbose ? service.url : undefined,
|
|
378
|
+
});
|
|
379
|
+
anyAvailable = true;
|
|
380
|
+
} else {
|
|
381
|
+
results.push({ name: service.name, status: 'unhealthy' });
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
results.push({ name: service.name, status: 'unavailable' });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// For CLI-only mode, it's okay if services aren't running
|
|
389
|
+
const cliOnlyMode = !anyAvailable;
|
|
390
|
+
|
|
391
|
+
if (cliOnlyMode) {
|
|
392
|
+
return {
|
|
393
|
+
name: 'Core Services',
|
|
394
|
+
passed: true,
|
|
395
|
+
message: 'Running in standalone mode (services optional)',
|
|
396
|
+
details: options.verbose ? { services: results } : undefined,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const runningCount = results.filter(r => r.status === 'running').length;
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
name: 'Core Services',
|
|
404
|
+
passed: runningCount > 0,
|
|
405
|
+
message: `${runningCount}/${services.length} services running`,
|
|
406
|
+
details: options.verbose ? { services: results } : undefined,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Check tool services
|
|
412
|
+
*/
|
|
413
|
+
async function checkToolServices(options: DoctorOptions): Promise<CheckResult> {
|
|
414
|
+
const services = [
|
|
415
|
+
{ name: 'Git Tools', url: process.env.GIT_TOOLS_URL || 'http://localhost:3004' },
|
|
416
|
+
{ name: 'FS Tools', url: process.env.FS_TOOLS_URL || 'http://localhost:3005' },
|
|
417
|
+
{ name: 'Terraform Tools', url: process.env.TERRAFORM_TOOLS_URL || 'http://localhost:3006' },
|
|
418
|
+
{ name: 'K8s Tools', url: process.env.K8S_TOOLS_URL || 'http://localhost:3007' },
|
|
419
|
+
{ name: 'Helm Tools', url: process.env.HELM_TOOLS_URL || 'http://localhost:3008' },
|
|
420
|
+
{ name: 'AWS Tools', url: process.env.AWS_TOOLS_URL || 'http://localhost:3009' },
|
|
421
|
+
{ name: 'GitHub Tools', url: process.env.GITHUB_TOOLS_URL || 'http://localhost:3010' },
|
|
422
|
+
{ name: 'State Service', url: process.env.STATE_SERVICE_URL || 'http://localhost:3011' },
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
const results: Array<{ name: string; status: string }> = [];
|
|
426
|
+
|
|
427
|
+
for (const service of services) {
|
|
428
|
+
try {
|
|
429
|
+
const response = await fetch(`${service.url}/health`, {
|
|
430
|
+
signal: AbortSignal.timeout(2000),
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (response.ok) {
|
|
434
|
+
results.push({ name: service.name, status: 'running' });
|
|
435
|
+
} else {
|
|
436
|
+
results.push({ name: service.name, status: 'unhealthy' });
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
results.push({ name: service.name, status: 'unavailable' });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const runningCount = results.filter(r => r.status === 'running').length;
|
|
444
|
+
|
|
445
|
+
// Tool services are optional - the CLI has local fallbacks
|
|
446
|
+
return {
|
|
447
|
+
name: 'Tool Services',
|
|
448
|
+
passed: true,
|
|
449
|
+
message:
|
|
450
|
+
runningCount > 0
|
|
451
|
+
? `${runningCount}/${services.length} services running`
|
|
452
|
+
: 'Using local tools (services unavailable)',
|
|
453
|
+
details: options.verbose ? { services: results } : undefined,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Check dependencies (CLI tools)
|
|
459
|
+
*/
|
|
460
|
+
async function checkDependencies(options: DoctorOptions): Promise<CheckResult> {
|
|
461
|
+
const { execFileSync } = await import('child_process');
|
|
462
|
+
|
|
463
|
+
// Use execFileSync with args arrays to prevent shell injection
|
|
464
|
+
const tools = [
|
|
465
|
+
{ name: 'git', cmd: 'git', args: ['--version'], required: true },
|
|
466
|
+
{ name: 'terraform', cmd: 'terraform', args: ['version'], required: false },
|
|
467
|
+
{ name: 'kubectl', cmd: 'kubectl', args: ['version', '--client'], required: false },
|
|
468
|
+
{ name: 'helm', cmd: 'helm', args: ['version', '--short'], required: false },
|
|
469
|
+
{ name: 'aws', cmd: 'aws', args: ['--version'], required: false },
|
|
470
|
+
{ name: 'gcloud', cmd: 'gcloud', args: ['version'], required: false },
|
|
471
|
+
{ name: 'az', cmd: 'az', args: ['version'], required: false },
|
|
472
|
+
];
|
|
473
|
+
|
|
474
|
+
const results: Array<{ name: string; version?: string; available: boolean }> = [];
|
|
475
|
+
const requiredMissing: string[] = [];
|
|
476
|
+
|
|
477
|
+
for (const tool of tools) {
|
|
478
|
+
try {
|
|
479
|
+
const output = execFileSync(tool.cmd, tool.args, {
|
|
480
|
+
encoding: 'utf-8',
|
|
481
|
+
timeout: 5000,
|
|
482
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Extract version from output
|
|
486
|
+
const versionMatch = output.match(/\d+\.\d+(\.\d+)?/);
|
|
487
|
+
results.push({
|
|
488
|
+
name: tool.name,
|
|
489
|
+
version: versionMatch ? versionMatch[0] : 'installed',
|
|
490
|
+
available: true,
|
|
491
|
+
});
|
|
492
|
+
} catch {
|
|
493
|
+
results.push({ name: tool.name, available: false });
|
|
494
|
+
if (tool.required) {
|
|
495
|
+
requiredMissing.push(tool.name);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (requiredMissing.length > 0) {
|
|
501
|
+
return {
|
|
502
|
+
name: 'Dependencies',
|
|
503
|
+
passed: false,
|
|
504
|
+
error: `Required tools not found: ${requiredMissing.join(', ')}`,
|
|
505
|
+
fix: `Install missing tools: ${requiredMissing.join(', ')}`,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const availableCount = results.filter(r => r.available).length;
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
name: 'Dependencies',
|
|
513
|
+
passed: true,
|
|
514
|
+
message: `${availableCount}/${tools.length} tools available`,
|
|
515
|
+
details: options.verbose ? { tools: results } : undefined,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Check disk space
|
|
521
|
+
*/
|
|
522
|
+
async function checkDiskSpace(_options: DoctorOptions): Promise<CheckResult> {
|
|
523
|
+
const os = await import('os');
|
|
524
|
+
const { execFileSync } = await import('child_process');
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
// Get disk space for home directory
|
|
528
|
+
const homeDir = os.homedir();
|
|
529
|
+
let available: number | undefined;
|
|
530
|
+
|
|
531
|
+
if (process.platform === 'win32') {
|
|
532
|
+
// Windows - use execFileSync with args array to prevent shell injection
|
|
533
|
+
const output = execFileSync('wmic', ['logicaldisk', 'get', 'size,freespace,caption'], {
|
|
534
|
+
encoding: 'utf-8',
|
|
535
|
+
});
|
|
536
|
+
const lines = output.trim().split('\n');
|
|
537
|
+
const drive = homeDir.charAt(0).toUpperCase();
|
|
538
|
+
for (const line of lines) {
|
|
539
|
+
if (line.startsWith(drive)) {
|
|
540
|
+
const parts = line.trim().split(/\s+/);
|
|
541
|
+
available = parseInt(parts[1], 10);
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
// Unix-like - use execFileSync with args array to prevent shell injection
|
|
547
|
+
const output = execFileSync('df', ['-k', homeDir], { encoding: 'utf-8' });
|
|
548
|
+
// Skip header line and parse the data line
|
|
549
|
+
const lines = output.trim().split('\n');
|
|
550
|
+
const dataLine = lines[lines.length - 1];
|
|
551
|
+
const parts = dataLine.trim().split(/\s+/);
|
|
552
|
+
available = parseInt(parts[3], 10) * 1024; // Convert KB to bytes
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Handle case where disk space could not be determined
|
|
556
|
+
if (available === undefined || isNaN(available)) {
|
|
557
|
+
return {
|
|
558
|
+
name: 'Disk Space',
|
|
559
|
+
passed: true,
|
|
560
|
+
message: 'Unable to determine disk space (assuming OK)',
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const availableGB = available / (1024 * 1024 * 1024);
|
|
565
|
+
const minRequired = 1; // 1 GB minimum
|
|
566
|
+
|
|
567
|
+
if (availableGB < minRequired) {
|
|
568
|
+
return {
|
|
569
|
+
name: 'Disk Space',
|
|
570
|
+
passed: false,
|
|
571
|
+
error: `Low disk space: ${availableGB.toFixed(1)} GB available`,
|
|
572
|
+
fix: 'Free up disk space (at least 1 GB recommended)',
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
name: 'Disk Space',
|
|
578
|
+
passed: true,
|
|
579
|
+
message: `${availableGB.toFixed(1)} GB available`,
|
|
580
|
+
};
|
|
581
|
+
} catch {
|
|
582
|
+
return {
|
|
583
|
+
name: 'Disk Space',
|
|
584
|
+
passed: true,
|
|
585
|
+
message: 'Unable to check (assuming OK)',
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Check network connectivity
|
|
592
|
+
*/
|
|
593
|
+
async function checkNetwork(options: DoctorOptions): Promise<CheckResult> {
|
|
594
|
+
const endpoints = [
|
|
595
|
+
{ name: 'api.anthropic.com', url: 'https://api.anthropic.com' },
|
|
596
|
+
{ name: 'api.openai.com', url: 'https://api.openai.com' },
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
const results: Array<{ name: string; reachable: boolean }> = [];
|
|
600
|
+
|
|
601
|
+
for (const endpoint of endpoints) {
|
|
602
|
+
try {
|
|
603
|
+
await fetch(endpoint.url, {
|
|
604
|
+
method: 'HEAD',
|
|
605
|
+
signal: AbortSignal.timeout(5000),
|
|
606
|
+
});
|
|
607
|
+
results.push({ name: endpoint.name, reachable: true });
|
|
608
|
+
} catch {
|
|
609
|
+
results.push({ name: endpoint.name, reachable: false });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const reachableCount = results.filter(r => r.reachable).length;
|
|
614
|
+
|
|
615
|
+
if (reachableCount === 0) {
|
|
616
|
+
return {
|
|
617
|
+
name: 'Network',
|
|
618
|
+
passed: false,
|
|
619
|
+
error: 'Cannot reach LLM APIs',
|
|
620
|
+
fix: 'Check network connection and firewall settings',
|
|
621
|
+
details: options.verbose ? { endpoints: results } : undefined,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
name: 'Network',
|
|
627
|
+
passed: true,
|
|
628
|
+
message: `${reachableCount}/${endpoints.length} API endpoints reachable`,
|
|
629
|
+
details: options.verbose ? { endpoints: results } : undefined,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* All diagnostic checks
|
|
635
|
+
*/
|
|
636
|
+
const DIAGNOSTIC_CHECKS: Array<{ name: string; check: DiagnosticCheck }> = [
|
|
637
|
+
{ name: 'Configuration', check: checkConfiguration },
|
|
638
|
+
{ name: 'LLM Provider', check: checkLLMProvider },
|
|
639
|
+
{ name: 'Cloud Credentials', check: checkCloudCredentials },
|
|
640
|
+
{ name: 'Cloud Connectivity', check: checkCloudConnectivity },
|
|
641
|
+
{ name: 'Core Services', check: checkCoreServices },
|
|
642
|
+
{ name: 'Tool Services', check: checkToolServices },
|
|
643
|
+
{ name: 'Dependencies', check: checkDependencies },
|
|
644
|
+
{ name: 'Disk Space', check: checkDiskSpace },
|
|
645
|
+
{ name: 'Network', check: checkNetwork },
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Run the doctor command
|
|
650
|
+
*/
|
|
651
|
+
export async function doctorCommand(options: DoctorOptions = {}): Promise<void> {
|
|
652
|
+
logger.debug('Running doctor command', { options });
|
|
653
|
+
|
|
654
|
+
ui.header('Nimbus Doctor');
|
|
655
|
+
ui.info('Running diagnostic checks...');
|
|
656
|
+
ui.newLine();
|
|
657
|
+
|
|
658
|
+
const results: CheckResult[] = [];
|
|
659
|
+
let allPassed = true;
|
|
660
|
+
|
|
661
|
+
for (const { name, check } of DIAGNOSTIC_CHECKS) {
|
|
662
|
+
ui.write(` ${name.padEnd(20)}`);
|
|
663
|
+
|
|
664
|
+
try {
|
|
665
|
+
const result = await check(options);
|
|
666
|
+
results.push(result);
|
|
667
|
+
|
|
668
|
+
if (result.passed) {
|
|
669
|
+
ui.print(`${ui.color('✓', 'green')} ${result.message || 'OK'}`);
|
|
670
|
+
} else {
|
|
671
|
+
ui.print(`${ui.color('✗', 'red')} ${result.error || 'Failed'}`);
|
|
672
|
+
allPassed = false;
|
|
673
|
+
|
|
674
|
+
if (options.fix && result.runFix) {
|
|
675
|
+
ui.print(` → Attempting fix...`);
|
|
676
|
+
try {
|
|
677
|
+
await result.runFix();
|
|
678
|
+
ui.print(` → ${ui.color('Fixed', 'green')}`);
|
|
679
|
+
} catch (fixError: any) {
|
|
680
|
+
ui.print(
|
|
681
|
+
` → ${ui.color(`Fix failed: ${fixError.message}`, 'red')}`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
} else if (result.fix) {
|
|
685
|
+
ui.print(` → ${ui.dim(result.fix)}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Show details in verbose mode
|
|
690
|
+
if (options.verbose && result.details) {
|
|
691
|
+
for (const [key, value] of Object.entries(result.details)) {
|
|
692
|
+
if (Array.isArray(value)) {
|
|
693
|
+
ui.print(` ${key}:`);
|
|
694
|
+
for (const item of value) {
|
|
695
|
+
if (typeof item === 'object') {
|
|
696
|
+
ui.print(` - ${JSON.stringify(item)}`);
|
|
697
|
+
} else {
|
|
698
|
+
ui.print(` - ${item}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} else {
|
|
702
|
+
ui.print(` ${key}: ${value}`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
} catch (error: any) {
|
|
707
|
+
ui.print(`${ui.color('✗', 'red')} Error: ${error.message}`);
|
|
708
|
+
results.push({
|
|
709
|
+
name,
|
|
710
|
+
passed: false,
|
|
711
|
+
error: error.message,
|
|
712
|
+
});
|
|
713
|
+
allPassed = false;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
ui.newLine();
|
|
718
|
+
|
|
719
|
+
// JSON output
|
|
720
|
+
if (options.json) {
|
|
721
|
+
console.log(
|
|
722
|
+
JSON.stringify(
|
|
723
|
+
{
|
|
724
|
+
passed: allPassed,
|
|
725
|
+
results: results.map(r => ({
|
|
726
|
+
name: r.name,
|
|
727
|
+
passed: r.passed,
|
|
728
|
+
message: r.message,
|
|
729
|
+
error: r.error,
|
|
730
|
+
details: r.details,
|
|
731
|
+
})),
|
|
732
|
+
},
|
|
733
|
+
null,
|
|
734
|
+
2
|
|
735
|
+
)
|
|
736
|
+
);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Summary
|
|
741
|
+
const passedCount = results.filter(r => r.passed).length;
|
|
742
|
+
const totalCount = results.length;
|
|
743
|
+
|
|
744
|
+
if (allPassed) {
|
|
745
|
+
ui.success(`All checks passed! (${passedCount}/${totalCount})`);
|
|
746
|
+
} else {
|
|
747
|
+
const failedCount = totalCount - passedCount;
|
|
748
|
+
ui.warning(`${failedCount} check(s) failed. ${passedCount}/${totalCount} passed.`);
|
|
749
|
+
ui.newLine();
|
|
750
|
+
ui.info('Run with --fix to attempt automatic fixes');
|
|
751
|
+
ui.info('Run with --verbose for more details');
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Quality Metrics
|
|
755
|
+
if (options.metrics) {
|
|
756
|
+
ui.newLine();
|
|
757
|
+
ui.header('Quality Metrics');
|
|
758
|
+
|
|
759
|
+
const stateUrl = process.env.STATE_SERVICE_URL || 'http://localhost:3011';
|
|
760
|
+
try {
|
|
761
|
+
const response = await fetch(`${stateUrl}/api/state/metrics`, {
|
|
762
|
+
signal: AbortSignal.timeout(5000),
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
if (response.ok) {
|
|
766
|
+
const { data } = (await response.json()) as any;
|
|
767
|
+
|
|
768
|
+
ui.newLine();
|
|
769
|
+
ui.print(` Response Time (P95) ${data.responseTime.p95}ms`);
|
|
770
|
+
ui.print(` Response Time (P50) ${data.responseTime.p50}ms`);
|
|
771
|
+
ui.print(` Response Time (Avg) ${data.responseTime.avg}ms`);
|
|
772
|
+
ui.print(` Error Rate ${data.errorRate}%`);
|
|
773
|
+
ui.print(` Total Operations ${data.totalOperations}`);
|
|
774
|
+
ui.print(` Total Tokens Used ${data.totalTokensUsed.toLocaleString()}`);
|
|
775
|
+
ui.print(` Total Cost $${data.totalCostUsd.toFixed(4)}`);
|
|
776
|
+
|
|
777
|
+
if (Object.keys(data.operationsByType).length > 0) {
|
|
778
|
+
ui.newLine();
|
|
779
|
+
ui.print(' Operations by type:');
|
|
780
|
+
for (const [type, count] of Object.entries(data.operationsByType)) {
|
|
781
|
+
ui.print(` ${type.padEnd(20)} ${count}`);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} else {
|
|
785
|
+
ui.warning('Could not fetch metrics (State service unavailable)');
|
|
786
|
+
}
|
|
787
|
+
} catch {
|
|
788
|
+
ui.warning('Could not fetch metrics (State service unavailable)');
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// Export as default command
|
|
794
|
+
export default doctorCommand;
|