@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,649 @@
1
+ /**
2
+ * AWS Discover Command
3
+ *
4
+ * Interactive and non-interactive AWS infrastructure discovery
5
+ *
6
+ * Usage: nimbus aws discover [options]
7
+ */
8
+
9
+ import { writeFile } from 'node:fs/promises';
10
+ import { logger } from '../utils';
11
+ import { RestClient } from '../clients';
12
+ import { createWizard, ui, select, multiSelect, type WizardStep, type StepResult } from '../wizard';
13
+
14
+ // AWS Tools Service client
15
+ const awsToolsUrl = process.env.AWS_TOOLS_SERVICE_URL || 'http://localhost:3009';
16
+ const awsClient = new RestClient(awsToolsUrl);
17
+
18
+ /**
19
+ * Discovery context for wizard
20
+ */
21
+ export interface AwsDiscoverContext {
22
+ // AWS configuration
23
+ awsProfile?: string;
24
+ awsRegions?: string[];
25
+ awsAccountId?: string;
26
+ awsAccountAlias?: string;
27
+
28
+ // Discovery options
29
+ servicesToScan?: string[];
30
+ excludeServices?: string[];
31
+
32
+ // State
33
+ discoverySessionId?: string;
34
+ inventory?: DiscoveryInventory;
35
+ }
36
+
37
+ /**
38
+ * Discovery inventory from AWS
39
+ */
40
+ interface DiscoveryInventory {
41
+ resources: DiscoveredResource[];
42
+ byType: Record<string, number>;
43
+ byRegion: Record<string, number>;
44
+ byService: Record<string, number>;
45
+ summary?: {
46
+ totalResources: number;
47
+ resourcesByService: Record<string, number>;
48
+ resourcesByRegion: Record<string, number>;
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Discovered resource
54
+ */
55
+ interface DiscoveredResource {
56
+ id: string;
57
+ type: string;
58
+ region: string;
59
+ name?: string;
60
+ tags?: Record<string, string>;
61
+ properties: Record<string, unknown>;
62
+ }
63
+
64
+ /**
65
+ * Command options from CLI arguments
66
+ */
67
+ export interface AwsDiscoverOptions {
68
+ profile?: string;
69
+ regions?: string[];
70
+ services?: string[];
71
+ excludeServices?: string[];
72
+ outputFormat?: 'json' | 'table' | 'summary';
73
+ outputFile?: string;
74
+ nonInteractive?: boolean;
75
+ }
76
+
77
+ /**
78
+ * Run the AWS discover command
79
+ */
80
+ export async function awsDiscoverCommand(
81
+ options: AwsDiscoverOptions = {}
82
+ ): Promise<DiscoveryInventory | null> {
83
+ logger.info('Starting AWS infrastructure discovery');
84
+
85
+ // Non-interactive mode
86
+ if (options.nonInteractive) {
87
+ return await runNonInteractive(options);
88
+ }
89
+
90
+ // Interactive wizard mode
91
+ const wizard = createWizard<AwsDiscoverContext>({
92
+ title: 'nimbus aws discover',
93
+ description: 'Discover AWS infrastructure resources',
94
+ initialContext: {
95
+ awsProfile: options.profile,
96
+ awsRegions: options.regions,
97
+ servicesToScan: options.services,
98
+ excludeServices: options.excludeServices,
99
+ },
100
+ steps: createWizardSteps(),
101
+ onEvent: event => {
102
+ logger.debug('Wizard event', { type: event.type });
103
+ },
104
+ });
105
+
106
+ const result = await wizard.run();
107
+
108
+ if (result.success && result.context.inventory) {
109
+ ui.newLine();
110
+ displayInventorySummary(result.context.inventory);
111
+
112
+ // Handle output options
113
+ if (options.outputFile) {
114
+ await saveInventory(
115
+ result.context.inventory,
116
+ options.outputFile,
117
+ options.outputFormat || 'json'
118
+ );
119
+ }
120
+
121
+ return result.context.inventory;
122
+ } else {
123
+ ui.error(`Discovery failed: ${result.error?.message || 'Unknown error'}`);
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Create wizard steps
130
+ */
131
+ function createWizardSteps(): WizardStep<AwsDiscoverContext>[] {
132
+ return [
133
+ // Step 1: AWS Configuration
134
+ {
135
+ id: 'aws-config',
136
+ title: 'AWS Configuration',
137
+ description: 'Configure AWS profile and regions to scan',
138
+ execute: awsConfigStep,
139
+ },
140
+
141
+ // Step 2: Service Selection
142
+ {
143
+ id: 'services',
144
+ title: 'Service Selection',
145
+ description: 'Select which AWS services to scan',
146
+ execute: serviceSelectionStep,
147
+ },
148
+
149
+ // Step 3: Discovery
150
+ {
151
+ id: 'discovery',
152
+ title: 'Infrastructure Discovery',
153
+ description: 'Scanning your AWS infrastructure...',
154
+ execute: discoveryStep,
155
+ },
156
+ ];
157
+ }
158
+
159
+ /**
160
+ * Step 1: AWS Configuration
161
+ */
162
+ async function awsConfigStep(ctx: AwsDiscoverContext): Promise<StepResult> {
163
+ // Fetch available profiles
164
+ ui.startSpinner({ message: 'Fetching AWS profiles...' });
165
+
166
+ let profiles: Array<{ name: string; source: string; region?: string; isSSO: boolean }> = [];
167
+
168
+ try {
169
+ const profilesResponse = await awsClient.get<{
170
+ profiles: Array<{ name: string; source: string; region?: string; isSSO: boolean }>;
171
+ }>('/api/aws/profiles');
172
+
173
+ if (profilesResponse.success && profilesResponse.data?.profiles) {
174
+ profiles = profilesResponse.data.profiles;
175
+ }
176
+
177
+ ui.stopSpinnerSuccess(`Found ${profiles.length} AWS profiles`);
178
+ } catch (error) {
179
+ ui.stopSpinnerFail('Could not fetch AWS profiles');
180
+ profiles = [{ name: 'default', source: 'credentials', isSSO: false }];
181
+ }
182
+
183
+ // Profile selection
184
+ let selectedProfile = ctx.awsProfile;
185
+
186
+ if (!selectedProfile) {
187
+ const profileOptions = profiles.map(p => ({
188
+ value: p.name,
189
+ label: p.name + (p.isSSO ? ' (SSO)' : ''),
190
+ description: `Source: ${p.source}${p.region ? `, Region: ${p.region}` : ''}`,
191
+ }));
192
+
193
+ selectedProfile = await select({
194
+ message: 'Select AWS profile:',
195
+ options: profileOptions,
196
+ defaultValue: 'default',
197
+ });
198
+
199
+ if (!selectedProfile) {
200
+ return { success: false, error: 'No profile selected' };
201
+ }
202
+ }
203
+
204
+ // Validate credentials
205
+ ui.startSpinner({ message: `Validating credentials for profile "${selectedProfile}"...` });
206
+
207
+ try {
208
+ const validateResponse = await awsClient.post<{
209
+ valid: boolean;
210
+ accountId?: string;
211
+ accountAlias?: string;
212
+ error?: string;
213
+ }>('/api/aws/profiles/validate', { profile: selectedProfile });
214
+
215
+ if (!validateResponse.success || !validateResponse.data?.valid) {
216
+ ui.stopSpinnerFail(`Invalid credentials: ${validateResponse.data?.error || 'Unknown error'}`);
217
+ return { success: false, error: 'Invalid AWS credentials' };
218
+ }
219
+
220
+ ui.stopSpinnerSuccess(
221
+ `Authenticated to account ${validateResponse.data.accountId}${
222
+ validateResponse.data.accountAlias ? ` (${validateResponse.data.accountAlias})` : ''
223
+ }`
224
+ );
225
+
226
+ ctx.awsAccountId = validateResponse.data.accountId;
227
+ ctx.awsAccountAlias = validateResponse.data.accountAlias;
228
+ } catch (error: any) {
229
+ ui.stopSpinnerFail(`Failed to validate credentials: ${error.message}`);
230
+ return { success: false, error: 'Credential validation failed' };
231
+ }
232
+
233
+ // Region selection
234
+ ui.newLine();
235
+
236
+ const regionChoice = await select<'all' | 'specific'>({
237
+ message: 'Select regions to scan:',
238
+ options: [
239
+ {
240
+ value: 'all',
241
+ label: 'All enabled regions',
242
+ description: 'Scan all regions enabled for your account',
243
+ },
244
+ {
245
+ value: 'specific',
246
+ label: 'Specific regions',
247
+ description: 'Select specific regions to scan',
248
+ },
249
+ ],
250
+ defaultValue: ctx.awsRegions?.length ? 'specific' : 'all',
251
+ });
252
+
253
+ let selectedRegions: string[] = ctx.awsRegions || [];
254
+
255
+ if (regionChoice === 'specific' && selectedRegions.length === 0) {
256
+ // Fetch available regions
257
+ ui.startSpinner({ message: 'Fetching available regions...' });
258
+
259
+ try {
260
+ const regionsResponse = await awsClient.get<{
261
+ regions: Array<{ name: string; displayName: string }>;
262
+ }>(`/api/aws/regions?profile=${selectedProfile}`);
263
+
264
+ ui.stopSpinnerSuccess(`Found ${regionsResponse.data?.regions?.length || 0} regions`);
265
+
266
+ if (regionsResponse.success && regionsResponse.data?.regions) {
267
+ const regionOptions = regionsResponse.data.regions.map(r => ({
268
+ value: r.name,
269
+ label: `${r.name} - ${r.displayName}`,
270
+ }));
271
+
272
+ selectedRegions = (await multiSelect({
273
+ message: 'Select regions to scan:',
274
+ options: regionOptions,
275
+ required: true,
276
+ })) as string[];
277
+ }
278
+ } catch (error) {
279
+ ui.stopSpinnerFail('Could not fetch regions');
280
+ selectedRegions = ['us-east-1'];
281
+ }
282
+ }
283
+
284
+ return {
285
+ success: true,
286
+ data: {
287
+ awsProfile: selectedProfile,
288
+ awsRegions: regionChoice === 'all' ? undefined : selectedRegions,
289
+ },
290
+ };
291
+ }
292
+
293
+ /**
294
+ * Step 2: Service Selection
295
+ */
296
+ async function serviceSelectionStep(ctx: AwsDiscoverContext): Promise<StepResult> {
297
+ if (ctx.servicesToScan && ctx.servicesToScan.length > 0) {
298
+ // Services already specified
299
+ ui.info(`Services to scan: ${ctx.servicesToScan.join(', ')}`);
300
+ return { success: true, data: { servicesToScan: ctx.servicesToScan } };
301
+ }
302
+
303
+ const serviceChoice = await select<'all' | 'specific'>({
304
+ message: 'Select services to scan:',
305
+ options: [
306
+ {
307
+ value: 'all',
308
+ label: 'All supported services',
309
+ description: 'EC2, S3, RDS, Lambda, VPC, IAM, ECS, EKS, DynamoDB, CloudFront',
310
+ },
311
+ {
312
+ value: 'specific',
313
+ label: 'Specific services',
314
+ description: 'Select specific services to scan',
315
+ },
316
+ ],
317
+ defaultValue: 'all',
318
+ });
319
+
320
+ if (serviceChoice === 'all') {
321
+ return { success: true, data: { servicesToScan: undefined } };
322
+ }
323
+
324
+ const serviceOptions = [
325
+ { value: 'EC2', label: 'EC2', description: 'Instances, volumes, security groups, AMIs' },
326
+ { value: 'S3', label: 'S3', description: 'Buckets and bucket policies' },
327
+ { value: 'RDS', label: 'RDS', description: 'Database instances and clusters' },
328
+ { value: 'Lambda', label: 'Lambda', description: 'Functions and layers' },
329
+ { value: 'VPC', label: 'VPC', description: 'VPCs, subnets, route tables, NAT gateways' },
330
+ { value: 'IAM', label: 'IAM', description: 'Roles, policies, users, groups' },
331
+ { value: 'ECS', label: 'ECS', description: 'Clusters, services, task definitions' },
332
+ { value: 'EKS', label: 'EKS', description: 'Clusters and node groups' },
333
+ { value: 'DynamoDB', label: 'DynamoDB', description: 'Tables' },
334
+ { value: 'CloudFront', label: 'CloudFront', description: 'Distributions' },
335
+ ];
336
+
337
+ const selectedServices = await multiSelect({
338
+ message: 'Select services to scan:',
339
+ options: serviceOptions,
340
+ required: true,
341
+ });
342
+
343
+ return {
344
+ success: true,
345
+ data: { servicesToScan: selectedServices as string[] },
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Step 3: Discovery
351
+ */
352
+ async function discoveryStep(ctx: AwsDiscoverContext): Promise<StepResult> {
353
+ ui.print(' Starting infrastructure discovery...');
354
+ ui.newLine();
355
+
356
+ try {
357
+ // Start discovery
358
+ const startResponse = await awsClient.post<{
359
+ sessionId: string;
360
+ status: string;
361
+ }>('/api/aws/discover', {
362
+ profile: ctx.awsProfile,
363
+ regions: ctx.awsRegions || 'all',
364
+ services: ctx.servicesToScan,
365
+ excludeServices: ctx.excludeServices,
366
+ });
367
+
368
+ if (!startResponse.success || !startResponse.data?.sessionId) {
369
+ return { success: false, error: 'Failed to start discovery' };
370
+ }
371
+
372
+ const sessionId = startResponse.data.sessionId;
373
+ ctx.discoverySessionId = sessionId;
374
+
375
+ // Poll for progress with visual feedback
376
+ let completed = false;
377
+ let lastUpdate = '';
378
+
379
+ while (!completed) {
380
+ await new Promise(resolve => setTimeout(resolve, 1000));
381
+
382
+ const statusResponse = await awsClient.get<{
383
+ status: string;
384
+ progress: {
385
+ regionsScanned: number;
386
+ totalRegions: number;
387
+ servicesScanned: number;
388
+ totalServices: number;
389
+ resourcesFound: number;
390
+ currentRegion?: string;
391
+ currentService?: string;
392
+ errors: string[];
393
+ };
394
+ inventory?: DiscoveryInventory;
395
+ }>(`/api/aws/discover/${sessionId}`);
396
+
397
+ if (!statusResponse.success) {
398
+ continue;
399
+ }
400
+
401
+ const { status, progress, inventory } = statusResponse.data!;
402
+
403
+ // Build progress message
404
+ const progressMsg = buildProgressMessage(progress);
405
+ if (progressMsg !== lastUpdate) {
406
+ ui.clearLine();
407
+ ui.write(progressMsg);
408
+ lastUpdate = progressMsg;
409
+ }
410
+
411
+ if (status === 'completed') {
412
+ completed = true;
413
+ ctx.inventory = inventory;
414
+
415
+ ui.newLine();
416
+ ui.newLine();
417
+ ui.success(`Discovery complete! Found ${progress.resourcesFound} resources`);
418
+
419
+ if (progress.errors.length > 0) {
420
+ ui.newLine();
421
+ ui.warning(`${progress.errors.length} errors occurred during discovery:`);
422
+ for (const err of progress.errors.slice(0, 5)) {
423
+ ui.print(` ${ui.dim(err)}`);
424
+ }
425
+ if (progress.errors.length > 5) {
426
+ ui.print(` ${ui.dim(`... and ${progress.errors.length - 5} more`)}`);
427
+ }
428
+ }
429
+ } else if (status === 'failed') {
430
+ ui.newLine();
431
+ ui.error('Discovery failed');
432
+ return { success: false, error: 'Discovery failed' };
433
+ } else if (status === 'cancelled') {
434
+ ui.newLine();
435
+ ui.warning('Discovery was cancelled');
436
+ return { success: false, error: 'Discovery cancelled' };
437
+ }
438
+ }
439
+
440
+ return {
441
+ success: true,
442
+ data: {
443
+ discoverySessionId: sessionId,
444
+ inventory: ctx.inventory,
445
+ },
446
+ };
447
+ } catch (error: any) {
448
+ return { success: false, error: error.message };
449
+ }
450
+ }
451
+
452
+ /**
453
+ * Build progress message
454
+ */
455
+ function buildProgressMessage(progress: {
456
+ regionsScanned: number;
457
+ totalRegions: number;
458
+ servicesScanned: number;
459
+ totalServices: number;
460
+ resourcesFound: number;
461
+ currentRegion?: string;
462
+ currentService?: string;
463
+ }): string {
464
+ const parts = [
465
+ ` Regions: ${progress.regionsScanned}/${progress.totalRegions}`,
466
+ `Services: ${progress.servicesScanned}/${progress.totalServices}`,
467
+ `Resources: ${progress.resourcesFound}`,
468
+ ];
469
+
470
+ if (progress.currentRegion && progress.currentService) {
471
+ parts.push(`Current: ${progress.currentRegion}/${progress.currentService}`);
472
+ } else if (progress.currentRegion) {
473
+ parts.push(`Current: ${progress.currentRegion}`);
474
+ }
475
+
476
+ return parts.join(' | ');
477
+ }
478
+
479
+ /**
480
+ * Display inventory summary
481
+ */
482
+ function displayInventorySummary(inventory: DiscoveryInventory): void {
483
+ ui.box({
484
+ title: 'Discovery Summary',
485
+ content: [
486
+ `Total Resources: ${inventory.resources.length}`,
487
+ '',
488
+ 'By Service:',
489
+ ...Object.entries(inventory.byService || {}).map(
490
+ ([service, count]) => ` ${service}: ${count}`
491
+ ),
492
+ '',
493
+ 'By Region:',
494
+ ...Object.entries(inventory.byRegion || {}).map(([region, count]) => ` ${region}: ${count}`),
495
+ ],
496
+ style: 'rounded',
497
+ borderColor: 'cyan',
498
+ padding: 1,
499
+ });
500
+ }
501
+
502
+ /**
503
+ * Save inventory to file
504
+ */
505
+ async function saveInventory(
506
+ inventory: DiscoveryInventory,
507
+ outputFile: string,
508
+ format: 'json' | 'table' | 'summary'
509
+ ): Promise<void> {
510
+ let content: string;
511
+
512
+ if (format === 'json') {
513
+ content = JSON.stringify(inventory, null, 2);
514
+ } else if (format === 'summary') {
515
+ content = [
516
+ `# AWS Infrastructure Discovery Summary`,
517
+ ``,
518
+ `Total Resources: ${inventory.resources.length}`,
519
+ ``,
520
+ `## By Service`,
521
+ ...Object.entries(inventory.byService || {}).map(
522
+ ([service, count]) => `- ${service}: ${count}`
523
+ ),
524
+ ``,
525
+ `## By Region`,
526
+ ...Object.entries(inventory.byRegion || {}).map(([region, count]) => `- ${region}: ${count}`),
527
+ ].join('\n');
528
+ } else {
529
+ // Table format - CSV-like
530
+ const headers = ['ID', 'Type', 'Region', 'Name'];
531
+ const rows = inventory.resources.map(r => [r.id, r.type, r.region, r.name || '']);
532
+ content = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
533
+ }
534
+
535
+ await writeFile(outputFile, content, 'utf-8');
536
+ ui.success(`Inventory saved to ${outputFile}`);
537
+ }
538
+
539
+ /**
540
+ * Run in non-interactive mode
541
+ */
542
+ async function runNonInteractive(options: AwsDiscoverOptions): Promise<DiscoveryInventory | null> {
543
+ ui.header('nimbus aws discover', 'Non-interactive mode');
544
+
545
+ // Validate required options
546
+ if (!options.profile) {
547
+ ui.error('Profile is required in non-interactive mode (--profile)');
548
+ process.exit(1);
549
+ }
550
+
551
+ ui.info(`Using profile: ${options.profile}`);
552
+ ui.info(`Regions: ${options.regions?.join(', ') || 'all'}`);
553
+ ui.info(`Services: ${options.services?.join(', ') || 'all'}`);
554
+
555
+ // Validate credentials
556
+ ui.startSpinner({ message: 'Validating credentials...' });
557
+
558
+ try {
559
+ const validateResponse = await awsClient.post<{
560
+ valid: boolean;
561
+ accountId?: string;
562
+ error?: string;
563
+ }>('/api/aws/profiles/validate', { profile: options.profile });
564
+
565
+ if (!validateResponse.success || !validateResponse.data?.valid) {
566
+ ui.stopSpinnerFail(`Invalid credentials: ${validateResponse.data?.error || 'Unknown error'}`);
567
+ return null;
568
+ }
569
+
570
+ ui.stopSpinnerSuccess(`Authenticated to account ${validateResponse.data.accountId}`);
571
+ } catch (error: any) {
572
+ ui.stopSpinnerFail(`Credential validation failed: ${error.message}`);
573
+ return null;
574
+ }
575
+
576
+ // Start discovery
577
+ ui.startSpinner({ message: 'Starting discovery...' });
578
+
579
+ try {
580
+ const startResponse = await awsClient.post<{
581
+ sessionId: string;
582
+ }>('/api/aws/discover', {
583
+ profile: options.profile,
584
+ regions: options.regions || 'all',
585
+ services: options.services,
586
+ excludeServices: options.excludeServices,
587
+ });
588
+
589
+ if (!startResponse.success || !startResponse.data?.sessionId) {
590
+ ui.stopSpinnerFail('Failed to start discovery');
591
+ return null;
592
+ }
593
+
594
+ const sessionId = startResponse.data.sessionId;
595
+ ui.stopSpinnerSuccess(`Discovery started (session: ${sessionId})`);
596
+
597
+ // Poll for completion
598
+ ui.startSpinner({ message: 'Scanning infrastructure...' });
599
+
600
+ let completed = false;
601
+ let inventory: DiscoveryInventory | undefined;
602
+
603
+ while (!completed) {
604
+ await new Promise(resolve => setTimeout(resolve, 2000));
605
+
606
+ const statusResponse = await awsClient.get<{
607
+ status: string;
608
+ progress: { resourcesFound: number };
609
+ inventory?: DiscoveryInventory;
610
+ }>(`/api/aws/discover/${sessionId}`);
611
+
612
+ if (!statusResponse.success) {
613
+ continue;
614
+ }
615
+
616
+ const { status, progress } = statusResponse.data!;
617
+
618
+ ui.updateSpinner(`Scanning... ${progress.resourcesFound} resources found`);
619
+
620
+ if (status === 'completed') {
621
+ completed = true;
622
+ inventory = statusResponse.data!.inventory;
623
+ ui.stopSpinnerSuccess(`Discovery complete! Found ${progress.resourcesFound} resources`);
624
+ } else if (status === 'failed' || status === 'cancelled') {
625
+ ui.stopSpinnerFail(`Discovery ${status}`);
626
+ return null;
627
+ }
628
+ }
629
+
630
+ if (inventory) {
631
+ // Display summary
632
+ displayInventorySummary(inventory);
633
+
634
+ // Save to file if requested
635
+ if (options.outputFile) {
636
+ await saveInventory(inventory, options.outputFile, options.outputFormat || 'json');
637
+ }
638
+
639
+ return inventory;
640
+ }
641
+
642
+ return null;
643
+ } catch (error: any) {
644
+ ui.stopSpinnerFail(`Discovery failed: ${error.message}`);
645
+ return null;
646
+ }
647
+ }
648
+
649
+ export default awsDiscoverCommand;