@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.
Files changed (313) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +628 -0
  3. package/bin/nimbus +38 -0
  4. package/package.json +80 -0
  5. package/src/__tests__/app.test.ts +76 -0
  6. package/src/__tests__/audit.test.ts +877 -0
  7. package/src/__tests__/circuit-breaker.test.ts +116 -0
  8. package/src/__tests__/cli-run.test.ts +115 -0
  9. package/src/__tests__/context-manager.test.ts +502 -0
  10. package/src/__tests__/context.test.ts +242 -0
  11. package/src/__tests__/enterprise.test.ts +401 -0
  12. package/src/__tests__/generator.test.ts +433 -0
  13. package/src/__tests__/hooks.test.ts +582 -0
  14. package/src/__tests__/init.test.ts +436 -0
  15. package/src/__tests__/intent-parser.test.ts +229 -0
  16. package/src/__tests__/llm-router.test.ts +209 -0
  17. package/src/__tests__/lsp.test.ts +293 -0
  18. package/src/__tests__/modes.test.ts +336 -0
  19. package/src/__tests__/permissions.test.ts +338 -0
  20. package/src/__tests__/serve.test.ts +275 -0
  21. package/src/__tests__/sessions.test.ts +227 -0
  22. package/src/__tests__/sharing.test.ts +288 -0
  23. package/src/__tests__/snapshots.test.ts +581 -0
  24. package/src/__tests__/state-db.test.ts +334 -0
  25. package/src/__tests__/stream-with-tools.test.ts +732 -0
  26. package/src/__tests__/subagents.test.ts +176 -0
  27. package/src/__tests__/system-prompt.test.ts +169 -0
  28. package/src/__tests__/tool-converter.test.ts +256 -0
  29. package/src/__tests__/tool-schemas.test.ts +397 -0
  30. package/src/__tests__/tools.test.ts +143 -0
  31. package/src/__tests__/version.test.ts +49 -0
  32. package/src/agent/compaction-agent.ts +227 -0
  33. package/src/agent/context-manager.ts +435 -0
  34. package/src/agent/context.ts +427 -0
  35. package/src/agent/deploy-preview.ts +426 -0
  36. package/src/agent/index.ts +68 -0
  37. package/src/agent/loop.ts +717 -0
  38. package/src/agent/modes.ts +429 -0
  39. package/src/agent/permissions.ts +466 -0
  40. package/src/agent/subagents/base.ts +116 -0
  41. package/src/agent/subagents/cost.ts +51 -0
  42. package/src/agent/subagents/explore.ts +42 -0
  43. package/src/agent/subagents/general.ts +54 -0
  44. package/src/agent/subagents/index.ts +102 -0
  45. package/src/agent/subagents/infra.ts +59 -0
  46. package/src/agent/subagents/security.ts +69 -0
  47. package/src/agent/system-prompt.ts +436 -0
  48. package/src/app.ts +122 -0
  49. package/src/audit/activity-log.ts +290 -0
  50. package/src/audit/compliance-checker.ts +540 -0
  51. package/src/audit/cost-tracker.ts +318 -0
  52. package/src/audit/index.ts +23 -0
  53. package/src/audit/security-scanner.ts +596 -0
  54. package/src/auth/guard.ts +75 -0
  55. package/src/auth/index.ts +56 -0
  56. package/src/auth/oauth.ts +455 -0
  57. package/src/auth/providers.ts +470 -0
  58. package/src/auth/sso.ts +113 -0
  59. package/src/auth/store.ts +505 -0
  60. package/src/auth/types.ts +187 -0
  61. package/src/build.ts +141 -0
  62. package/src/cli/index.ts +16 -0
  63. package/src/cli/init.ts +854 -0
  64. package/src/cli/openapi-spec.ts +356 -0
  65. package/src/cli/run.ts +237 -0
  66. package/src/cli/serve-auth.ts +80 -0
  67. package/src/cli/serve.ts +462 -0
  68. package/src/cli/web.ts +67 -0
  69. package/src/cli.ts +1417 -0
  70. package/src/clients/core-engine-client.ts +227 -0
  71. package/src/clients/enterprise-client.ts +334 -0
  72. package/src/clients/generator-client.ts +351 -0
  73. package/src/clients/git-client.ts +627 -0
  74. package/src/clients/github-client.ts +410 -0
  75. package/src/clients/helm-client.ts +504 -0
  76. package/src/clients/index.ts +80 -0
  77. package/src/clients/k8s-client.ts +497 -0
  78. package/src/clients/llm-client.ts +161 -0
  79. package/src/clients/rest-client.ts +130 -0
  80. package/src/clients/service-discovery.ts +33 -0
  81. package/src/clients/terraform-client.ts +482 -0
  82. package/src/clients/tools-client.ts +1843 -0
  83. package/src/clients/ws-client.ts +115 -0
  84. package/src/commands/analyze/index.ts +352 -0
  85. package/src/commands/apply/helm.ts +473 -0
  86. package/src/commands/apply/index.ts +213 -0
  87. package/src/commands/apply/k8s.ts +454 -0
  88. package/src/commands/apply/terraform.ts +582 -0
  89. package/src/commands/ask.ts +167 -0
  90. package/src/commands/audit/index.ts +238 -0
  91. package/src/commands/auth-cloud.ts +294 -0
  92. package/src/commands/auth-list.ts +134 -0
  93. package/src/commands/auth-profile.ts +121 -0
  94. package/src/commands/auth-status.ts +141 -0
  95. package/src/commands/aws/ec2.ts +501 -0
  96. package/src/commands/aws/iam.ts +397 -0
  97. package/src/commands/aws/index.ts +133 -0
  98. package/src/commands/aws/lambda.ts +396 -0
  99. package/src/commands/aws/rds.ts +439 -0
  100. package/src/commands/aws/s3.ts +439 -0
  101. package/src/commands/aws/vpc.ts +393 -0
  102. package/src/commands/aws-discover.ts +649 -0
  103. package/src/commands/aws-terraform.ts +805 -0
  104. package/src/commands/azure/aks.ts +376 -0
  105. package/src/commands/azure/functions.ts +253 -0
  106. package/src/commands/azure/index.ts +116 -0
  107. package/src/commands/azure/storage.ts +478 -0
  108. package/src/commands/azure/vm.ts +355 -0
  109. package/src/commands/billing/index.ts +256 -0
  110. package/src/commands/chat.ts +314 -0
  111. package/src/commands/config.ts +346 -0
  112. package/src/commands/cost/cloud-cost-estimator.ts +266 -0
  113. package/src/commands/cost/estimator.ts +79 -0
  114. package/src/commands/cost/index.ts +594 -0
  115. package/src/commands/cost/parsers/terraform.ts +273 -0
  116. package/src/commands/cost/parsers/types.ts +25 -0
  117. package/src/commands/cost/pricing/aws.ts +544 -0
  118. package/src/commands/cost/pricing/azure.ts +499 -0
  119. package/src/commands/cost/pricing/gcp.ts +396 -0
  120. package/src/commands/cost/pricing/index.ts +40 -0
  121. package/src/commands/demo.ts +250 -0
  122. package/src/commands/doctor.ts +794 -0
  123. package/src/commands/drift/index.ts +439 -0
  124. package/src/commands/explain.ts +277 -0
  125. package/src/commands/feedback.ts +389 -0
  126. package/src/commands/fix.ts +324 -0
  127. package/src/commands/fs/index.ts +402 -0
  128. package/src/commands/gcp/compute.ts +325 -0
  129. package/src/commands/gcp/functions.ts +271 -0
  130. package/src/commands/gcp/gke.ts +438 -0
  131. package/src/commands/gcp/iam.ts +344 -0
  132. package/src/commands/gcp/index.ts +129 -0
  133. package/src/commands/gcp/storage.ts +284 -0
  134. package/src/commands/generate-helm.ts +1249 -0
  135. package/src/commands/generate-k8s.ts +1560 -0
  136. package/src/commands/generate-terraform.ts +1460 -0
  137. package/src/commands/gh/index.ts +863 -0
  138. package/src/commands/git/index.ts +1343 -0
  139. package/src/commands/helm/index.ts +1126 -0
  140. package/src/commands/help.ts +539 -0
  141. package/src/commands/history.ts +142 -0
  142. package/src/commands/import.ts +868 -0
  143. package/src/commands/index.ts +367 -0
  144. package/src/commands/init.ts +1046 -0
  145. package/src/commands/k8s/index.ts +1137 -0
  146. package/src/commands/login.ts +631 -0
  147. package/src/commands/logout.ts +83 -0
  148. package/src/commands/onboarding.ts +228 -0
  149. package/src/commands/plan/display.ts +279 -0
  150. package/src/commands/plan/index.ts +599 -0
  151. package/src/commands/preview.ts +452 -0
  152. package/src/commands/questionnaire.ts +1270 -0
  153. package/src/commands/resume.ts +55 -0
  154. package/src/commands/team/index.ts +346 -0
  155. package/src/commands/template.ts +232 -0
  156. package/src/commands/tf/index.ts +1034 -0
  157. package/src/commands/upgrade.ts +550 -0
  158. package/src/commands/usage/index.ts +134 -0
  159. package/src/commands/version.ts +170 -0
  160. package/src/compat/index.ts +2 -0
  161. package/src/compat/runtime.ts +12 -0
  162. package/src/compat/sqlite.ts +107 -0
  163. package/src/config/index.ts +17 -0
  164. package/src/config/manager.ts +530 -0
  165. package/src/config/safety-policy.ts +358 -0
  166. package/src/config/schema.ts +125 -0
  167. package/src/config/types.ts +527 -0
  168. package/src/context/context-db.ts +199 -0
  169. package/src/demo/index.ts +349 -0
  170. package/src/demo/scenarios/full-journey.ts +229 -0
  171. package/src/demo/scenarios/getting-started.ts +127 -0
  172. package/src/demo/scenarios/helm-release.ts +341 -0
  173. package/src/demo/scenarios/k8s-deployment.ts +194 -0
  174. package/src/demo/scenarios/terraform-vpc.ts +170 -0
  175. package/src/demo/types.ts +92 -0
  176. package/src/engine/cost-estimator.ts +438 -0
  177. package/src/engine/diagram-generator.ts +256 -0
  178. package/src/engine/drift-detector.ts +902 -0
  179. package/src/engine/executor.ts +1035 -0
  180. package/src/engine/index.ts +76 -0
  181. package/src/engine/orchestrator.ts +636 -0
  182. package/src/engine/planner.ts +720 -0
  183. package/src/engine/safety.ts +743 -0
  184. package/src/engine/verifier.ts +770 -0
  185. package/src/enterprise/audit.ts +348 -0
  186. package/src/enterprise/auth.ts +270 -0
  187. package/src/enterprise/billing.ts +822 -0
  188. package/src/enterprise/index.ts +17 -0
  189. package/src/enterprise/teams.ts +443 -0
  190. package/src/generator/best-practices.ts +1608 -0
  191. package/src/generator/helm.ts +630 -0
  192. package/src/generator/index.ts +37 -0
  193. package/src/generator/intent-parser.ts +514 -0
  194. package/src/generator/kubernetes.ts +976 -0
  195. package/src/generator/terraform.ts +1867 -0
  196. package/src/history/index.ts +8 -0
  197. package/src/history/manager.ts +322 -0
  198. package/src/history/types.ts +34 -0
  199. package/src/hooks/config.ts +432 -0
  200. package/src/hooks/engine.ts +391 -0
  201. package/src/hooks/index.ts +4 -0
  202. package/src/llm/auth-bridge.ts +198 -0
  203. package/src/llm/circuit-breaker.ts +140 -0
  204. package/src/llm/config-loader.ts +201 -0
  205. package/src/llm/cost-calculator.ts +171 -0
  206. package/src/llm/index.ts +8 -0
  207. package/src/llm/model-aliases.ts +115 -0
  208. package/src/llm/provider-registry.ts +63 -0
  209. package/src/llm/providers/anthropic.ts +433 -0
  210. package/src/llm/providers/bedrock.ts +477 -0
  211. package/src/llm/providers/google.ts +405 -0
  212. package/src/llm/providers/ollama.ts +767 -0
  213. package/src/llm/providers/openai-compatible.ts +340 -0
  214. package/src/llm/providers/openai.ts +328 -0
  215. package/src/llm/providers/openrouter.ts +338 -0
  216. package/src/llm/router.ts +1035 -0
  217. package/src/llm/types.ts +232 -0
  218. package/src/lsp/client.ts +298 -0
  219. package/src/lsp/languages.ts +116 -0
  220. package/src/lsp/manager.ts +278 -0
  221. package/src/mcp/client.ts +402 -0
  222. package/src/mcp/index.ts +5 -0
  223. package/src/mcp/manager.ts +133 -0
  224. package/src/nimbus.ts +214 -0
  225. package/src/plugins/index.ts +27 -0
  226. package/src/plugins/loader.ts +334 -0
  227. package/src/plugins/manager.ts +376 -0
  228. package/src/plugins/types.ts +284 -0
  229. package/src/scanners/cicd-scanner.ts +258 -0
  230. package/src/scanners/cloud-scanner.ts +466 -0
  231. package/src/scanners/framework-scanner.ts +469 -0
  232. package/src/scanners/iac-scanner.ts +388 -0
  233. package/src/scanners/index.ts +539 -0
  234. package/src/scanners/language-scanner.ts +276 -0
  235. package/src/scanners/package-manager-scanner.ts +277 -0
  236. package/src/scanners/types.ts +172 -0
  237. package/src/sessions/manager.ts +365 -0
  238. package/src/sessions/types.ts +44 -0
  239. package/src/sharing/sync.ts +296 -0
  240. package/src/sharing/viewer.ts +97 -0
  241. package/src/snapshots/index.ts +2 -0
  242. package/src/snapshots/manager.ts +530 -0
  243. package/src/state/artifacts.ts +147 -0
  244. package/src/state/audit.ts +137 -0
  245. package/src/state/billing.ts +240 -0
  246. package/src/state/checkpoints.ts +117 -0
  247. package/src/state/config.ts +67 -0
  248. package/src/state/conversations.ts +14 -0
  249. package/src/state/credentials.ts +154 -0
  250. package/src/state/db.ts +58 -0
  251. package/src/state/index.ts +26 -0
  252. package/src/state/messages.ts +115 -0
  253. package/src/state/projects.ts +123 -0
  254. package/src/state/schema.ts +236 -0
  255. package/src/state/sessions.ts +147 -0
  256. package/src/state/teams.ts +200 -0
  257. package/src/telemetry.ts +108 -0
  258. package/src/tools/aws-ops.ts +952 -0
  259. package/src/tools/azure-ops.ts +579 -0
  260. package/src/tools/file-ops.ts +593 -0
  261. package/src/tools/gcp-ops.ts +625 -0
  262. package/src/tools/git-ops.ts +773 -0
  263. package/src/tools/github-ops.ts +799 -0
  264. package/src/tools/helm-ops.ts +943 -0
  265. package/src/tools/index.ts +17 -0
  266. package/src/tools/k8s-ops.ts +819 -0
  267. package/src/tools/schemas/converter.ts +184 -0
  268. package/src/tools/schemas/devops.ts +612 -0
  269. package/src/tools/schemas/index.ts +73 -0
  270. package/src/tools/schemas/standard.ts +1144 -0
  271. package/src/tools/schemas/types.ts +705 -0
  272. package/src/tools/terraform-ops.ts +862 -0
  273. package/src/types/ambient.d.ts +193 -0
  274. package/src/types/config.ts +83 -0
  275. package/src/types/drift.ts +116 -0
  276. package/src/types/enterprise.ts +335 -0
  277. package/src/types/index.ts +20 -0
  278. package/src/types/plan.ts +44 -0
  279. package/src/types/request.ts +65 -0
  280. package/src/types/response.ts +54 -0
  281. package/src/types/service.ts +51 -0
  282. package/src/ui/App.tsx +997 -0
  283. package/src/ui/DeployPreview.tsx +169 -0
  284. package/src/ui/Header.tsx +68 -0
  285. package/src/ui/InputBox.tsx +350 -0
  286. package/src/ui/MessageList.tsx +585 -0
  287. package/src/ui/PermissionPrompt.tsx +151 -0
  288. package/src/ui/StatusBar.tsx +158 -0
  289. package/src/ui/ToolCallDisplay.tsx +409 -0
  290. package/src/ui/chat-ui.ts +853 -0
  291. package/src/ui/index.ts +33 -0
  292. package/src/ui/ink/index.ts +711 -0
  293. package/src/ui/streaming.ts +176 -0
  294. package/src/ui/types.ts +57 -0
  295. package/src/utils/analytics.ts +72 -0
  296. package/src/utils/cost-warning.ts +27 -0
  297. package/src/utils/env.ts +46 -0
  298. package/src/utils/errors.ts +69 -0
  299. package/src/utils/event-bus.ts +38 -0
  300. package/src/utils/index.ts +24 -0
  301. package/src/utils/logger.ts +171 -0
  302. package/src/utils/rate-limiter.ts +121 -0
  303. package/src/utils/service-auth.ts +49 -0
  304. package/src/utils/validation.ts +53 -0
  305. package/src/version.ts +4 -0
  306. package/src/watcher/index.ts +163 -0
  307. package/src/wizard/approval.ts +383 -0
  308. package/src/wizard/index.ts +25 -0
  309. package/src/wizard/prompts.ts +338 -0
  310. package/src/wizard/types.ts +171 -0
  311. package/src/wizard/ui.ts +556 -0
  312. package/src/wizard/wizard.ts +304 -0
  313. 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;