@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,599 @@
1
+ /**
2
+ * Plan Command
3
+ *
4
+ * Preview infrastructure changes for Terraform, Kubernetes, and Helm
5
+ *
6
+ * Usage: nimbus plan [options]
7
+ */
8
+
9
+ import { logger } from '../../utils';
10
+ import { ui } from '../../wizard';
11
+ import { terraformClient, k8sClient } from '../../clients';
12
+ import { displayPlan, type PlanResult } from './display';
13
+
14
+ // Re-export display utilities
15
+ export { displayPlan, type PlanResult } from './display';
16
+
17
+ /**
18
+ * Plan type
19
+ */
20
+ export type PlanType = 'terraform' | 'k8s' | 'helm' | 'auto';
21
+
22
+ /**
23
+ * Command options
24
+ */
25
+ export interface PlanOptions {
26
+ type?: PlanType;
27
+ target?: string;
28
+ out?: string;
29
+ detailed?: boolean;
30
+ json?: boolean;
31
+ namespace?: string;
32
+ var?: Record<string, string>;
33
+ varFile?: string;
34
+ }
35
+
36
+ /**
37
+ * Parse plan options from args
38
+ */
39
+ export function parsePlanOptions(args: string[]): PlanOptions {
40
+ const options: PlanOptions = {};
41
+
42
+ for (let i = 0; i < args.length; i++) {
43
+ const arg = args[i];
44
+
45
+ if (arg === '--type' && args[i + 1]) {
46
+ options.type = args[++i] as PlanType;
47
+ } else if ((arg === '--target' || arg === '-t') && args[i + 1]) {
48
+ options.target = args[++i];
49
+ } else if (arg === '--out' && args[i + 1]) {
50
+ options.out = args[++i];
51
+ } else if (arg === '--detailed' || arg === '-d') {
52
+ options.detailed = true;
53
+ } else if (arg === '--json') {
54
+ options.json = true;
55
+ } else if ((arg === '--namespace' || arg === '-n') && args[i + 1]) {
56
+ options.namespace = args[++i];
57
+ } else if (arg === '--var' && args[i + 1]) {
58
+ const varArg = args[++i];
59
+ const [key, ...valueParts] = varArg.split('=');
60
+ options.var = options.var || {};
61
+ options.var[key] = valueParts.join('=');
62
+ } else if (arg === '--var-file' && args[i + 1]) {
63
+ options.varFile = args[++i];
64
+ } else if (!arg.startsWith('-') && !options.target) {
65
+ options.target = arg;
66
+ }
67
+ }
68
+
69
+ return options;
70
+ }
71
+
72
+ /**
73
+ * Detect infrastructure type from current directory
74
+ */
75
+ async function detectInfraType(targetPath?: string): Promise<PlanType | null> {
76
+ const fs = await import('fs/promises');
77
+ const path = await import('path');
78
+
79
+ const basePath = targetPath || '.';
80
+
81
+ // Check for Terraform files
82
+ try {
83
+ const files = await fs.readdir(basePath);
84
+ if (files.some(f => f.endsWith('.tf'))) {
85
+ return 'terraform';
86
+ }
87
+ } catch {
88
+ // Directory doesn't exist or can't be read
89
+ }
90
+
91
+ // Check for Kubernetes manifests
92
+ try {
93
+ const files = await fs.readdir(basePath);
94
+ const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
95
+ for (const file of yamlFiles.slice(0, 5)) {
96
+ // Check first 5 files
97
+ try {
98
+ const content = await fs.readFile(path.join(basePath, file), 'utf-8');
99
+ if (content.includes('apiVersion:') && content.includes('kind:')) {
100
+ return 'k8s';
101
+ }
102
+ } catch {
103
+ continue;
104
+ }
105
+ }
106
+ } catch {
107
+ // Ignore
108
+ }
109
+
110
+ // Check for Helm chart
111
+ try {
112
+ await fs.access(path.join(basePath, 'Chart.yaml'));
113
+ return 'helm';
114
+ } catch {
115
+ // No Chart.yaml
116
+ }
117
+
118
+ // Check if target is a specific file
119
+ if (targetPath) {
120
+ if (targetPath.endsWith('.tf')) {
121
+ return 'terraform';
122
+ }
123
+ if (targetPath.endsWith('.yaml') || targetPath.endsWith('.yml')) {
124
+ return 'k8s';
125
+ }
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ /**
132
+ * Run Terraform plan
133
+ */
134
+ async function runTerraformPlan(options: PlanOptions): Promise<PlanResult> {
135
+ const directory = options.target || '.';
136
+
137
+ // Check if terraform client is available
138
+ const clientAvailable = await terraformClient.isAvailable();
139
+
140
+ if (clientAvailable) {
141
+ const result = await terraformClient.plan(directory, {
142
+ vars: options.var,
143
+ varFile: options.varFile,
144
+ out: options.out,
145
+ });
146
+
147
+ // Parse changes from output
148
+ const addMatch = result.output.match(/(\d+) to add/);
149
+ const changeMatch = result.output.match(/(\d+) to change/);
150
+ const destroyMatch = result.output.match(/(\d+) to destroy/);
151
+
152
+ return {
153
+ type: 'terraform',
154
+ success: result.success,
155
+ error: result.error,
156
+ changes: result.hasChanges
157
+ ? {
158
+ add: parseInt(addMatch?.[1] || '0', 10),
159
+ change: parseInt(changeMatch?.[1] || '0', 10),
160
+ destroy: parseInt(destroyMatch?.[1] || '0', 10),
161
+ }
162
+ : { add: 0, change: 0, destroy: 0 },
163
+ raw: options.detailed ? result.output : undefined,
164
+ };
165
+ } else {
166
+ // Fall back to local terraform CLI
167
+ return runLocalTerraformPlan(options);
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Run local Terraform plan
173
+ */
174
+ async function runLocalTerraformPlan(options: PlanOptions): Promise<PlanResult> {
175
+ const { execFileSync } = await import('child_process');
176
+ const directory = options.target || '.';
177
+
178
+ // Build command args (using execFileSync to prevent shell injection)
179
+ const args = ['plan', '-no-color'];
180
+
181
+ if (options.var) {
182
+ for (const [key, value] of Object.entries(options.var)) {
183
+ args.push('-var', `${key}=${value}`);
184
+ }
185
+ }
186
+
187
+ if (options.varFile) {
188
+ args.push('-var-file', options.varFile);
189
+ }
190
+
191
+ if (options.out) {
192
+ args.push('-out', options.out);
193
+ }
194
+
195
+ try {
196
+ const output = execFileSync('terraform', args, {
197
+ cwd: directory,
198
+ encoding: 'utf-8',
199
+ timeout: 300000, // 5 minutes
200
+ });
201
+
202
+ // Parse output for changes
203
+ const addMatch = output.match(/(\d+) to add/);
204
+ const changeMatch = output.match(/(\d+) to change/);
205
+ const destroyMatch = output.match(/(\d+) to destroy/);
206
+
207
+ const changes = {
208
+ add: parseInt(addMatch?.[1] || '0', 10),
209
+ change: parseInt(changeMatch?.[1] || '0', 10),
210
+ destroy: parseInt(destroyMatch?.[1] || '0', 10),
211
+ };
212
+
213
+ // Parse resource changes
214
+ const resources: PlanResult['resources'] = [];
215
+ const resourceMatches = output.matchAll(
216
+ /# ([\w.-]+\.[\w.-]+) will be (created|updated|destroyed|read)/g
217
+ );
218
+ for (const match of resourceMatches) {
219
+ const actionMap: Record<string, string> = {
220
+ created: 'create',
221
+ updated: 'update',
222
+ destroyed: 'delete',
223
+ read: 'read',
224
+ };
225
+ resources.push({
226
+ action: actionMap[match[2]] || match[2],
227
+ resource: match[1],
228
+ address: match[1],
229
+ });
230
+ }
231
+
232
+ return {
233
+ type: 'terraform',
234
+ success: true,
235
+ changes,
236
+ resources,
237
+ raw: options.detailed ? output : undefined,
238
+ };
239
+ } catch (error: any) {
240
+ return {
241
+ type: 'terraform',
242
+ success: false,
243
+ error: error.message || 'Terraform plan failed',
244
+ raw: error.stdout || error.stderr,
245
+ };
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Run Kubernetes dry-run plan
251
+ */
252
+ async function runK8sPlan(options: PlanOptions): Promise<PlanResult> {
253
+ const manifests = options.target || '.';
254
+ const fs = await import('fs/promises');
255
+ const path = await import('path');
256
+
257
+ // Read manifest files
258
+ let manifestContent: string;
259
+ const resources: PlanResult['resources'] = [];
260
+
261
+ try {
262
+ const stat = await fs.stat(manifests);
263
+
264
+ if (stat.isDirectory()) {
265
+ const files = await fs.readdir(manifests);
266
+ const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
267
+
268
+ if (yamlFiles.length === 0) {
269
+ return {
270
+ type: 'k8s',
271
+ success: false,
272
+ error: 'No YAML manifests found in directory',
273
+ };
274
+ }
275
+
276
+ const contents = await Promise.all(
277
+ yamlFiles.map(f => fs.readFile(path.join(manifests, f), 'utf-8'))
278
+ );
279
+ manifestContent = contents.join('\n---\n');
280
+ } else {
281
+ manifestContent = await fs.readFile(manifests, 'utf-8');
282
+ }
283
+ } catch (error: any) {
284
+ return {
285
+ type: 'k8s',
286
+ success: false,
287
+ error: `Failed to read manifests: ${error.message}`,
288
+ };
289
+ }
290
+
291
+ // Parse manifests to list resources
292
+ const documents = manifestContent.split(/^---$/m);
293
+ for (const doc of documents) {
294
+ const trimmed = doc.trim();
295
+ if (!trimmed) {
296
+ continue;
297
+ }
298
+
299
+ const kindMatch = trimmed.match(/^kind:\s*(.+)$/m);
300
+ const nameMatch = trimmed.match(/^\s+name:\s*(.+)$/m);
301
+ const namespaceMatch = trimmed.match(/^\s+namespace:\s*(.+)$/m);
302
+
303
+ if (kindMatch && nameMatch) {
304
+ resources.push({
305
+ action: 'apply', // K8s apply is idempotent
306
+ resource: `${kindMatch[1].trim()}/${nameMatch[1].trim()}`,
307
+ address: namespaceMatch
308
+ ? `${namespaceMatch[1].trim()}/${kindMatch[1].trim()}/${nameMatch[1].trim()}`
309
+ : `default/${kindMatch[1].trim()}/${nameMatch[1].trim()}`,
310
+ });
311
+ }
312
+ }
313
+
314
+ // Check if k8s client is available for dry-run
315
+ const clientAvailable = await k8sClient.isAvailable();
316
+
317
+ if (clientAvailable) {
318
+ const result = await k8sClient.apply(manifestContent, {
319
+ namespace: options.namespace,
320
+ dryRun: true,
321
+ });
322
+
323
+ return {
324
+ type: 'k8s',
325
+ success: result.success,
326
+ error: result.error,
327
+ changes: {
328
+ add: result.created?.length || 0,
329
+ change: result.configured?.length || 0,
330
+ destroy: 0,
331
+ },
332
+ resources,
333
+ raw: options.detailed ? result.output : undefined,
334
+ };
335
+ }
336
+
337
+ // Return parsed resources without dry-run validation
338
+ return {
339
+ type: 'k8s',
340
+ success: true,
341
+ changes: {
342
+ add: resources.length,
343
+ change: 0,
344
+ destroy: 0,
345
+ },
346
+ resources,
347
+ raw: options.detailed ? manifestContent : undefined,
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Run Helm diff plan
353
+ */
354
+ async function runHelmPlan(options: PlanOptions): Promise<PlanResult> {
355
+ const target = options.target || '.';
356
+ const { execFileSync } = await import('child_process');
357
+
358
+ // Check if helm-diff plugin is available
359
+ try {
360
+ const pluginOutput = execFileSync('helm', ['plugin', 'list'], {
361
+ encoding: 'utf-8',
362
+ stdio: 'pipe',
363
+ });
364
+ if (!pluginOutput.includes('diff')) {
365
+ // helm-diff not installed, use template comparison
366
+ return runHelmTemplatePlan(options);
367
+ }
368
+ } catch {
369
+ // helm-diff not installed, use template comparison
370
+ return runHelmTemplatePlan(options);
371
+ }
372
+
373
+ // Use helm diff for existing releases
374
+ // First, need to determine release name
375
+ let releaseName = target;
376
+ let chartPath = '.';
377
+
378
+ // If target is a path, try to extract release name from values
379
+ if (target.includes('/') || target === '.') {
380
+ chartPath = target;
381
+ const fs = await import('fs/promises');
382
+ const path = await import('path');
383
+
384
+ try {
385
+ // Look for release name in values file
386
+ const valuesFiles = ['values.yaml', 'values.yml'];
387
+ for (const vf of valuesFiles) {
388
+ try {
389
+ const content = await fs.readFile(path.join(chartPath, vf), 'utf-8');
390
+ const nameMatch = content.match(/release[Nn]ame:\s*(.+)/);
391
+ if (nameMatch) {
392
+ releaseName = nameMatch[1].trim();
393
+ break;
394
+ }
395
+ } catch {
396
+ continue;
397
+ }
398
+ }
399
+ } catch {
400
+ // Use directory name as release name
401
+ releaseName = path.basename(path.resolve(chartPath));
402
+ }
403
+ }
404
+
405
+ try {
406
+ // Use execFileSync with args array to prevent shell injection
407
+ const diffArgs = ['diff', 'upgrade', releaseName, chartPath];
408
+ if (options.namespace) {
409
+ diffArgs.push('-n', options.namespace);
410
+ }
411
+ const output = execFileSync('helm', diffArgs, {
412
+ encoding: 'utf-8',
413
+ timeout: 60000,
414
+ });
415
+
416
+ // Parse diff output
417
+ const addMatch = output.match(/^\+[^+]/gm);
418
+ const removeMatch = output.match(/^-[^-]/gm);
419
+
420
+ return {
421
+ type: 'helm',
422
+ success: true,
423
+ changes: {
424
+ add: addMatch?.length || 0,
425
+ change: 0,
426
+ destroy: removeMatch?.length || 0,
427
+ },
428
+ raw: options.detailed ? output : undefined,
429
+ };
430
+ } catch {
431
+ // Release might not exist
432
+ return runHelmTemplatePlan(options);
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Run Helm template plan (for new releases)
438
+ */
439
+ async function runHelmTemplatePlan(options: PlanOptions): Promise<PlanResult> {
440
+ const chartPath = options.target || '.';
441
+ const { execFileSync } = await import('child_process');
442
+
443
+ try {
444
+ // Use execFileSync with args array to prevent shell injection
445
+ const templateArgs = ['template', chartPath];
446
+ if (options.namespace) {
447
+ templateArgs.push('-n', options.namespace);
448
+ }
449
+ const output = execFileSync('helm', templateArgs, {
450
+ encoding: 'utf-8',
451
+ timeout: 60000,
452
+ });
453
+
454
+ // Parse rendered manifests
455
+ const resources: PlanResult['resources'] = [];
456
+ const documents = output.split(/^---$/m);
457
+
458
+ for (const doc of documents) {
459
+ const trimmed = doc.trim();
460
+ if (!trimmed) {
461
+ continue;
462
+ }
463
+
464
+ const kindMatch = trimmed.match(/^kind:\s*(.+)$/m);
465
+ const nameMatch = trimmed.match(/^\s+name:\s*(.+)$/m);
466
+
467
+ if (kindMatch && nameMatch) {
468
+ resources.push({
469
+ action: 'create',
470
+ resource: `${kindMatch[1].trim()}/${nameMatch[1].trim()}`,
471
+ address: `${kindMatch[1].trim()}/${nameMatch[1].trim()}`,
472
+ });
473
+ }
474
+ }
475
+
476
+ return {
477
+ type: 'helm',
478
+ success: true,
479
+ changes: {
480
+ add: resources.length,
481
+ change: 0,
482
+ destroy: 0,
483
+ },
484
+ resources,
485
+ raw: options.detailed ? output : undefined,
486
+ };
487
+ } catch (error: any) {
488
+ return {
489
+ type: 'helm',
490
+ success: false,
491
+ error: error.message || 'Helm template failed',
492
+ raw: error.stdout || error.stderr,
493
+ };
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Capitalize first letter
499
+ */
500
+ function capitalizeFirst(str: string): string {
501
+ return str.charAt(0).toUpperCase() + str.slice(1);
502
+ }
503
+
504
+ /**
505
+ * Run the plan command
506
+ */
507
+ export async function planCommand(options: PlanOptions = {}): Promise<void> {
508
+ // Redact sensitive variables from logs
509
+ const { var: _vars, ...safeOptions } = options;
510
+ logger.info('Running plan command', {
511
+ ...safeOptions,
512
+ var: options.var ? '[REDACTED]' : undefined,
513
+ });
514
+
515
+ // Detect or use specified type
516
+ let type = options.type;
517
+
518
+ if (!type || type === 'auto') {
519
+ ui.startSpinner({ message: 'Detecting infrastructure type...' });
520
+ const detectedType = await detectInfraType(options.target);
521
+ ui.stopSpinnerSuccess('');
522
+
523
+ if (!detectedType) {
524
+ ui.error('Could not detect infrastructure type');
525
+ ui.newLine();
526
+ ui.info('Usage: nimbus plan [options]');
527
+ ui.info('');
528
+ ui.info('Options:');
529
+ ui.info(' --type <type> Infrastructure type: terraform, k8s, helm');
530
+ ui.info(' --target <path> Target directory or file');
531
+ ui.info(' --detailed Show detailed plan output');
532
+ ui.info('');
533
+ ui.info('Examples:');
534
+ ui.info(' nimbus plan');
535
+ ui.info(' nimbus plan --type terraform');
536
+ ui.info(' nimbus plan --target ./manifests --type k8s');
537
+ process.exit(1);
538
+ }
539
+
540
+ type = detectedType;
541
+ ui.info(`Detected infrastructure type: ${type}`);
542
+ ui.newLine();
543
+ }
544
+
545
+ ui.header(`${capitalizeFirst(type)} Plan`);
546
+ ui.info(`Target: ${options.target || '.'}`);
547
+ ui.newLine();
548
+
549
+ ui.startSpinner({ message: 'Creating execution plan...' });
550
+
551
+ let plan: PlanResult;
552
+
553
+ switch (type) {
554
+ case 'terraform':
555
+ plan = await runTerraformPlan(options);
556
+ break;
557
+ case 'k8s':
558
+ plan = await runK8sPlan(options);
559
+ break;
560
+ case 'helm':
561
+ plan = await runHelmPlan(options);
562
+ break;
563
+ default:
564
+ ui.stopSpinnerFail(`Unknown type: ${type}`);
565
+ process.exit(1);
566
+ }
567
+
568
+ if (!plan.success) {
569
+ ui.stopSpinnerFail('Plan failed');
570
+ ui.error(plan.error || 'Unknown error');
571
+
572
+ if (plan.raw) {
573
+ ui.newLine();
574
+ ui.print(plan.raw);
575
+ }
576
+
577
+ process.exit(1);
578
+ }
579
+
580
+ ui.stopSpinnerSuccess('Plan created');
581
+ ui.newLine();
582
+
583
+ // Display the plan
584
+ if (options.json) {
585
+ console.log(JSON.stringify(plan, null, 2));
586
+ } else {
587
+ displayPlan(plan, options.detailed);
588
+ }
589
+
590
+ // Save plan output if requested (Terraform only)
591
+ if (options.out && type === 'terraform') {
592
+ ui.newLine();
593
+ ui.info(`Plan saved to: ${options.out}`);
594
+ ui.info('Apply with: nimbus apply terraform');
595
+ }
596
+ }
597
+
598
+ // Export as default
599
+ export default planCommand;