@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,355 @@
1
+ /**
2
+ * Azure Virtual Machine CLI Commands
3
+ *
4
+ * Operations for Azure VMs
5
+ */
6
+
7
+ import { execFile } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { logger } from '../../utils';
10
+ import { ui } from '../../wizard/ui';
11
+ import {
12
+ loadSafetyPolicy,
13
+ evaluateSafety,
14
+ type SafetyContext,
15
+ type SafetyCheckResult,
16
+ } from '../../config/safety-policy';
17
+ import { promptForApproval, displaySafetySummary } from '../../wizard/approval';
18
+ import type { AzureCommandOptions } from './index';
19
+
20
+ const execFileAsync = promisify(execFile);
21
+
22
+ /**
23
+ * Run VM safety checks
24
+ */
25
+ async function runVmSafetyChecks(
26
+ action: string,
27
+ vmName: string,
28
+ options: AzureCommandOptions
29
+ ): Promise<SafetyCheckResult> {
30
+ const safetyPolicy = loadSafetyPolicy();
31
+
32
+ const context: SafetyContext = {
33
+ operation: action,
34
+ type: 'azure',
35
+ environment: options.subscription || 'default',
36
+ resources: [vmName],
37
+ metadata: {
38
+ resourceType: 'virtual-machine',
39
+ resourceGroup: options.resourceGroup,
40
+ },
41
+ };
42
+
43
+ return evaluateSafety(context, safetyPolicy);
44
+ }
45
+
46
+ /**
47
+ * Main VM command router
48
+ */
49
+ export async function vmCommand(
50
+ action: string,
51
+ args: string[],
52
+ options: AzureCommandOptions
53
+ ): Promise<void> {
54
+ logger.info('Running Azure VM command', { action, args, options });
55
+
56
+ switch (action) {
57
+ case 'list':
58
+ await listVms(options);
59
+ break;
60
+
61
+ case 'show':
62
+ if (!args[0]) {
63
+ ui.error('VM name required');
64
+ return;
65
+ }
66
+ await showVm(args[0], options);
67
+ break;
68
+
69
+ case 'start':
70
+ if (!args[0]) {
71
+ ui.error('VM name required');
72
+ return;
73
+ }
74
+ await startVm(args[0], options);
75
+ break;
76
+
77
+ case 'stop':
78
+ if (!args[0]) {
79
+ ui.error('VM name required');
80
+ return;
81
+ }
82
+ await stopVm(args[0], options);
83
+ break;
84
+
85
+ case 'delete':
86
+ if (!args[0]) {
87
+ ui.error('VM name required');
88
+ return;
89
+ }
90
+ await deleteVm(args[0], options);
91
+ break;
92
+
93
+ default:
94
+ showVmHelp();
95
+ break;
96
+ }
97
+ }
98
+
99
+ /**
100
+ * List VMs
101
+ */
102
+ async function listVms(options: AzureCommandOptions): Promise<void> {
103
+ ui.header('Azure Virtual Machines');
104
+ ui.newLine();
105
+
106
+ const azArgs = ['vm', 'list', '-o', 'json'];
107
+ if (options.subscription) {
108
+ azArgs.push('--subscription', options.subscription);
109
+ }
110
+ if (options.resourceGroup) {
111
+ azArgs.push('-g', options.resourceGroup);
112
+ }
113
+
114
+ try {
115
+ ui.startSpinner({ message: 'Fetching VMs...' });
116
+ const { stdout } = await execFileAsync('az', azArgs);
117
+ ui.stopSpinnerSuccess('VMs fetched');
118
+
119
+ const vms = JSON.parse(stdout || '[]');
120
+
121
+ if (vms.length === 0) {
122
+ ui.info('No VMs found');
123
+ return;
124
+ }
125
+
126
+ ui.print(`Found ${vms.length} VM(s)\n`);
127
+
128
+ // Display table
129
+ ui.print(
130
+ ui.color(
131
+ `${
132
+ 'Name'.padEnd(25) +
133
+ 'Resource Group'.padEnd(25) +
134
+ 'Location'.padEnd(15) +
135
+ 'VM Size'.padEnd(20)
136
+ }Status`,
137
+ 'cyan'
138
+ )
139
+ );
140
+ ui.print('─'.repeat(100));
141
+
142
+ for (const vm of vms) {
143
+ const name = vm.name?.substring(0, 24) || '';
144
+ const rg = vm.resourceGroup?.substring(0, 24) || '';
145
+ const location = vm.location?.substring(0, 14) || '';
146
+ const size = vm.hardwareProfile?.vmSize?.substring(0, 19) || '';
147
+ const status = vm.provisioningState || '';
148
+
149
+ const statusColor =
150
+ status === 'Succeeded'
151
+ ? 'green'
152
+ : status === 'Failed'
153
+ ? 'red'
154
+ : status === 'Updating'
155
+ ? 'yellow'
156
+ : 'white';
157
+
158
+ ui.print(
159
+ `${name.padEnd(25)}${rg.padEnd(25)}${location.padEnd(15)}${size.padEnd(20)}${ui.color(status, statusColor as 'green' | 'red' | 'yellow' | 'white')}`
160
+ );
161
+ }
162
+ } catch (error: unknown) {
163
+ ui.stopSpinnerFail('Failed to fetch VMs');
164
+ const message = error instanceof Error ? error.message : 'Unknown error';
165
+ logger.error('Failed to list VMs', { error: message });
166
+ ui.error(`Failed to list VMs: ${message}`);
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Show VM details
172
+ */
173
+ async function showVm(vmName: string, options: AzureCommandOptions): Promise<void> {
174
+ ui.header(`VM: ${vmName}`);
175
+ ui.newLine();
176
+
177
+ if (!options.resourceGroup) {
178
+ ui.error('Resource group required (use -g)');
179
+ return;
180
+ }
181
+
182
+ const azArgs = [
183
+ 'vm',
184
+ 'show',
185
+ '-n',
186
+ vmName,
187
+ '-g',
188
+ options.resourceGroup,
189
+ '-o',
190
+ 'json',
191
+ '--show-details',
192
+ ];
193
+ if (options.subscription) {
194
+ azArgs.push('--subscription', options.subscription);
195
+ }
196
+
197
+ try {
198
+ ui.startSpinner({ message: 'Fetching VM details...' });
199
+ const { stdout } = await execFileAsync('az', azArgs);
200
+ ui.stopSpinnerSuccess('Details fetched');
201
+
202
+ const vm = JSON.parse(stdout);
203
+
204
+ ui.print(ui.bold('Basic Information:'));
205
+ ui.print(` Name: ${vm.name}`);
206
+ ui.print(` Resource Group: ${vm.resourceGroup}`);
207
+ ui.print(` Location: ${vm.location}`);
208
+ ui.print(` VM Size: ${vm.hardwareProfile?.vmSize}`);
209
+ ui.print(` State: ${vm.powerState || 'Unknown'}`);
210
+ ui.newLine();
211
+
212
+ ui.print(ui.bold('OS:'));
213
+ ui.print(` Publisher: ${vm.storageProfile?.imageReference?.publisher || 'Custom'}`);
214
+ ui.print(` Offer: ${vm.storageProfile?.imageReference?.offer || 'N/A'}`);
215
+ ui.print(` SKU: ${vm.storageProfile?.imageReference?.sku || 'N/A'}`);
216
+ ui.newLine();
217
+
218
+ ui.print(ui.bold('Network:'));
219
+ if (vm.publicIps) {
220
+ ui.print(` Public IP: ${vm.publicIps}`);
221
+ }
222
+ if (vm.privateIps) {
223
+ ui.print(` Private IP: ${vm.privateIps}`);
224
+ }
225
+ if (vm.fqdns) {
226
+ ui.print(` FQDN: ${vm.fqdns}`);
227
+ }
228
+ ui.newLine();
229
+
230
+ ui.print(ui.bold('Disks:'));
231
+ ui.print(` OS Disk: ${vm.storageProfile?.osDisk?.name}`);
232
+ ui.print(` OS Disk Size: ${vm.storageProfile?.osDisk?.diskSizeGb} GB`);
233
+ const dataDisks = vm.storageProfile?.dataDisks || [];
234
+ if (dataDisks.length > 0) {
235
+ ui.print(` Data Disks: ${dataDisks.length}`);
236
+ }
237
+ } catch (error: unknown) {
238
+ ui.stopSpinnerFail('Failed to fetch details');
239
+ const message = error instanceof Error ? error.message : 'Unknown error';
240
+ logger.error('Failed to show VM', { error: message });
241
+ ui.error(`Failed to show VM: ${message}`);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Start a VM
247
+ */
248
+ async function startVm(vmName: string, options: AzureCommandOptions): Promise<void> {
249
+ if (!options.resourceGroup) {
250
+ ui.error('Resource group required (use -g)');
251
+ return;
252
+ }
253
+
254
+ const azArgs = ['vm', 'start', '-n', vmName, '-g', options.resourceGroup];
255
+ if (options.subscription) {
256
+ azArgs.push('--subscription', options.subscription);
257
+ }
258
+
259
+ try {
260
+ ui.startSpinner({ message: `Starting VM ${vmName}...` });
261
+ await execFileAsync('az', azArgs);
262
+ ui.stopSpinnerSuccess(`VM ${vmName} started`);
263
+ } catch (error: unknown) {
264
+ ui.stopSpinnerFail('Failed to start VM');
265
+ const message = error instanceof Error ? error.message : 'Unknown error';
266
+ logger.error('Failed to start VM', { error: message });
267
+ ui.error(`Failed to start VM: ${message}`);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Stop a VM
273
+ */
274
+ async function stopVm(vmName: string, options: AzureCommandOptions): Promise<void> {
275
+ if (!options.resourceGroup) {
276
+ ui.error('Resource group required (use -g)');
277
+ return;
278
+ }
279
+
280
+ const azArgs = ['vm', 'stop', '-n', vmName, '-g', options.resourceGroup];
281
+ if (options.subscription) {
282
+ azArgs.push('--subscription', options.subscription);
283
+ }
284
+
285
+ try {
286
+ ui.startSpinner({ message: `Stopping VM ${vmName}...` });
287
+ await execFileAsync('az', azArgs);
288
+ ui.stopSpinnerSuccess(`VM ${vmName} stopped`);
289
+ } catch (error: unknown) {
290
+ ui.stopSpinnerFail('Failed to stop VM');
291
+ const message = error instanceof Error ? error.message : 'Unknown error';
292
+ logger.error('Failed to stop VM', { error: message });
293
+ ui.error(`Failed to stop VM: ${message}`);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Delete a VM (requires safety approval)
299
+ */
300
+ async function deleteVm(vmName: string, options: AzureCommandOptions): Promise<void> {
301
+ if (!options.resourceGroup) {
302
+ ui.error('Resource group required (use -g)');
303
+ return;
304
+ }
305
+
306
+ // Run safety checks
307
+ const safetyResult = await runVmSafetyChecks('delete', vmName, options);
308
+
309
+ displaySafetySummary({
310
+ operation: `delete VM ${vmName}`,
311
+ risks: safetyResult.risks,
312
+ passed: safetyResult.passed,
313
+ });
314
+
315
+ if (safetyResult.requiresApproval) {
316
+ const result = await promptForApproval({
317
+ title: 'Delete Virtual Machine',
318
+ operation: `az vm delete -n ${vmName} -g ${options.resourceGroup}`,
319
+ risks: safetyResult.risks,
320
+ });
321
+
322
+ if (!result.approved) {
323
+ ui.warning('Operation cancelled');
324
+ return;
325
+ }
326
+ }
327
+
328
+ const azArgs = ['vm', 'delete', '-n', vmName, '-g', options.resourceGroup, '--yes'];
329
+ if (options.subscription) {
330
+ azArgs.push('--subscription', options.subscription);
331
+ }
332
+
333
+ try {
334
+ ui.startSpinner({ message: `Deleting VM ${vmName}...` });
335
+ await execFileAsync('az', azArgs);
336
+ ui.stopSpinnerSuccess(`VM ${vmName} deleted`);
337
+ } catch (error: unknown) {
338
+ ui.stopSpinnerFail('Failed to delete VM');
339
+ const message = error instanceof Error ? error.message : 'Unknown error';
340
+ logger.error('Failed to delete VM', { error: message });
341
+ ui.error(`Failed to delete VM: ${message}`);
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Show VM help
347
+ */
348
+ function showVmHelp(): void {
349
+ ui.print(ui.bold('VM Commands:'));
350
+ ui.print(' list List all VMs');
351
+ ui.print(' show <name> -g <rg> Show VM details');
352
+ ui.print(' start <name> -g <rg> Start a VM');
353
+ ui.print(' stop <name> -g <rg> Stop a VM');
354
+ ui.print(' delete <name> -g <rg> Delete a VM (requires approval)');
355
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Billing Commands
3
+ * Billing and subscription CLI commands
4
+ */
5
+
6
+ import { ui } from '../../wizard/ui';
7
+ import { billingClient } from '../../clients/enterprise-client';
8
+ import type {
9
+ BillingStatusOptions,
10
+ BillingUpgradeOptions,
11
+ BillingInvoicesOptions,
12
+ TeamPlan,
13
+ } from '../../types';
14
+
15
+ /**
16
+ * Get current team ID from config or environment
17
+ */
18
+ function getCurrentTeamId(): string | null {
19
+ return process.env.NIMBUS_TEAM_ID || null;
20
+ }
21
+
22
+ /**
23
+ * Parse billing status options
24
+ */
25
+ export function parseBillingStatusOptions(args: string[]): BillingStatusOptions {
26
+ const options: BillingStatusOptions = {};
27
+
28
+ for (let i = 0; i < args.length; i++) {
29
+ const arg = args[i];
30
+ if (arg === '--json') {
31
+ options.json = true;
32
+ } else if (arg === '--non-interactive') {
33
+ options.nonInteractive = true;
34
+ }
35
+ }
36
+
37
+ return options;
38
+ }
39
+
40
+ /**
41
+ * Parse billing upgrade options
42
+ */
43
+ export function parseBillingUpgradeOptions(args: string[]): BillingUpgradeOptions {
44
+ const options: BillingUpgradeOptions = {};
45
+
46
+ for (let i = 0; i < args.length; i++) {
47
+ const arg = args[i];
48
+ if (arg === '--plan' && args[i + 1]) {
49
+ options.plan = args[++i] as TeamPlan;
50
+ } else if (arg === '--non-interactive') {
51
+ options.nonInteractive = true;
52
+ } else if (!arg.startsWith('-') && !options.plan) {
53
+ options.plan = arg as TeamPlan;
54
+ }
55
+ }
56
+
57
+ return options;
58
+ }
59
+
60
+ /**
61
+ * Parse billing invoices options
62
+ */
63
+ export function parseBillingInvoicesOptions(args: string[]): BillingInvoicesOptions {
64
+ const options: BillingInvoicesOptions = {};
65
+
66
+ for (let i = 0; i < args.length; i++) {
67
+ const arg = args[i];
68
+ if (arg === '--limit' && args[i + 1]) {
69
+ options.limit = parseInt(args[++i], 10);
70
+ } else if (arg === '--json') {
71
+ options.json = true;
72
+ } else if (arg === '--non-interactive') {
73
+ options.nonInteractive = true;
74
+ }
75
+ }
76
+
77
+ return options;
78
+ }
79
+
80
+ /**
81
+ * Billing status command
82
+ */
83
+ export async function billingStatusCommand(options: BillingStatusOptions): Promise<void> {
84
+ try {
85
+ const teamId = getCurrentTeamId();
86
+ if (!teamId) {
87
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
88
+ return;
89
+ }
90
+
91
+ ui.startSpinner({ message: 'Fetching billing status...' });
92
+ const status = await billingClient.getStatus(teamId);
93
+ ui.stopSpinnerSuccess('Billing status retrieved');
94
+
95
+ if (options.json) {
96
+ console.log(JSON.stringify(status, null, 2));
97
+ return;
98
+ }
99
+
100
+ ui.newLine();
101
+ ui.header('Billing Status');
102
+
103
+ ui.print(` Plan: ${status.plan.toUpperCase()}`);
104
+ ui.print(` Status: ${status.status}`);
105
+ ui.print(
106
+ ` Period: ${new Date(status.currentPeriodStart).toLocaleDateString()} - ${new Date(status.currentPeriodEnd).toLocaleDateString()}`
107
+ );
108
+ ui.print(` Seats: ${status.seats.used} / ${status.seats.total} used`);
109
+
110
+ if (status.cancelAtPeriodEnd) {
111
+ ui.warning('Subscription will cancel at end of period');
112
+ }
113
+
114
+ ui.newLine();
115
+ if (status.plan === 'free') {
116
+ ui.info('Upgrade to Pro for more seats and features:');
117
+ ui.print(' nimbus billing upgrade pro');
118
+ }
119
+ } catch (error: any) {
120
+ ui.stopSpinnerFail('Failed to get billing status');
121
+ ui.error(error.message);
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Billing upgrade command
127
+ */
128
+ export async function billingUpgradeCommand(options: BillingUpgradeOptions): Promise<void> {
129
+ try {
130
+ const teamId = getCurrentTeamId();
131
+ if (!teamId) {
132
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
133
+ return;
134
+ }
135
+
136
+ const plan = options.plan;
137
+ if (!plan || !['pro', 'enterprise'].includes(plan)) {
138
+ ui.error('Plan is required');
139
+ ui.info('Usage: nimbus billing upgrade <pro|enterprise>');
140
+ return;
141
+ }
142
+
143
+ ui.startSpinner({ message: `Upgrading to ${plan}...` });
144
+ const status = await billingClient.subscribe(teamId, { plan });
145
+ ui.stopSpinnerSuccess(`Upgraded to ${plan.toUpperCase()}`);
146
+
147
+ ui.newLine();
148
+ ui.success(`Your team is now on the ${plan.toUpperCase()} plan!`);
149
+ ui.print(` Seats available: ${status.seats.total}`);
150
+ ui.print(` Next billing date: ${new Date(status.currentPeriodEnd).toLocaleDateString()}`);
151
+ } catch (error: any) {
152
+ ui.stopSpinnerFail('Failed to upgrade');
153
+ ui.error(error.message);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Billing invoices command
159
+ */
160
+ export async function billingInvoicesCommand(options: BillingInvoicesOptions): Promise<void> {
161
+ try {
162
+ const teamId = getCurrentTeamId();
163
+ if (!teamId) {
164
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
165
+ return;
166
+ }
167
+
168
+ ui.startSpinner({ message: 'Fetching invoices...' });
169
+ const invoices = await billingClient.getInvoices(teamId, options.limit || 10);
170
+ ui.stopSpinnerSuccess(`Found ${invoices.length} invoices`);
171
+
172
+ if (options.json) {
173
+ console.log(JSON.stringify(invoices, null, 2));
174
+ return;
175
+ }
176
+
177
+ if (invoices.length === 0) {
178
+ ui.info('No invoices found');
179
+ return;
180
+ }
181
+
182
+ ui.newLine();
183
+ ui.table({
184
+ columns: [
185
+ { key: 'number', header: 'Invoice #' },
186
+ { key: 'date', header: 'Date' },
187
+ { key: 'amount', header: 'Amount' },
188
+ { key: 'status', header: 'Status' },
189
+ ],
190
+ data: invoices.map(inv => ({
191
+ number: inv.number,
192
+ date: new Date(inv.createdAt).toLocaleDateString(),
193
+ amount: `$${(inv.amountDue / 100).toFixed(2)} ${inv.currency.toUpperCase()}`,
194
+ status: inv.status,
195
+ })),
196
+ });
197
+ } catch (error: any) {
198
+ ui.stopSpinnerFail('Failed to get invoices');
199
+ ui.error(error.message);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Billing cancel command
205
+ */
206
+ export async function billingCancelCommand(): Promise<void> {
207
+ try {
208
+ const teamId = getCurrentTeamId();
209
+ if (!teamId) {
210
+ ui.error('No team selected. Run `nimbus team switch <team-id>` first.');
211
+ return;
212
+ }
213
+
214
+ ui.startSpinner({ message: 'Canceling subscription...' });
215
+ const status = await billingClient.cancel(teamId);
216
+ ui.stopSpinnerSuccess('Subscription canceled');
217
+
218
+ ui.newLine();
219
+ ui.warning(
220
+ `Your subscription will end on ${new Date(status.currentPeriodEnd).toLocaleDateString()}`
221
+ );
222
+ ui.info('You will retain access until then.');
223
+ } catch (error: any) {
224
+ ui.stopSpinnerFail('Failed to cancel');
225
+ ui.error(error.message);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Main billing command dispatcher
231
+ */
232
+ export async function billingCommand(subcommand: string, args: string[]): Promise<void> {
233
+ switch (subcommand) {
234
+ case 'status':
235
+ case undefined:
236
+ await billingStatusCommand(parseBillingStatusOptions(args));
237
+ break;
238
+ case 'upgrade':
239
+ await billingUpgradeCommand(parseBillingUpgradeOptions(args));
240
+ break;
241
+ case 'invoices':
242
+ await billingInvoicesCommand(parseBillingInvoicesOptions(args));
243
+ break;
244
+ case 'cancel':
245
+ await billingCancelCommand();
246
+ break;
247
+ default:
248
+ ui.error(`Unknown billing command: ${subcommand}`);
249
+ ui.newLine();
250
+ ui.info('Available billing commands:');
251
+ ui.print(' nimbus billing status - Show billing status');
252
+ ui.print(' nimbus billing upgrade <plan> - Upgrade plan (pro|enterprise)');
253
+ ui.print(' nimbus billing invoices - List invoices');
254
+ ui.print(' nimbus billing cancel - Cancel subscription');
255
+ }
256
+ }