@build-astron-co/nimbus 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +628 -0
- package/bin/nimbus +38 -0
- package/package.json +80 -0
- package/src/__tests__/app.test.ts +76 -0
- package/src/__tests__/audit.test.ts +877 -0
- package/src/__tests__/circuit-breaker.test.ts +116 -0
- package/src/__tests__/cli-run.test.ts +115 -0
- package/src/__tests__/context-manager.test.ts +502 -0
- package/src/__tests__/context.test.ts +242 -0
- package/src/__tests__/enterprise.test.ts +401 -0
- package/src/__tests__/generator.test.ts +433 -0
- package/src/__tests__/hooks.test.ts +582 -0
- package/src/__tests__/init.test.ts +436 -0
- package/src/__tests__/intent-parser.test.ts +229 -0
- package/src/__tests__/llm-router.test.ts +209 -0
- package/src/__tests__/lsp.test.ts +293 -0
- package/src/__tests__/modes.test.ts +336 -0
- package/src/__tests__/permissions.test.ts +338 -0
- package/src/__tests__/serve.test.ts +275 -0
- package/src/__tests__/sessions.test.ts +227 -0
- package/src/__tests__/sharing.test.ts +288 -0
- package/src/__tests__/snapshots.test.ts +581 -0
- package/src/__tests__/state-db.test.ts +334 -0
- package/src/__tests__/stream-with-tools.test.ts +732 -0
- package/src/__tests__/subagents.test.ts +176 -0
- package/src/__tests__/system-prompt.test.ts +169 -0
- package/src/__tests__/tool-converter.test.ts +256 -0
- package/src/__tests__/tool-schemas.test.ts +397 -0
- package/src/__tests__/tools.test.ts +143 -0
- package/src/__tests__/version.test.ts +49 -0
- package/src/agent/compaction-agent.ts +227 -0
- package/src/agent/context-manager.ts +435 -0
- package/src/agent/context.ts +427 -0
- package/src/agent/deploy-preview.ts +426 -0
- package/src/agent/index.ts +68 -0
- package/src/agent/loop.ts +717 -0
- package/src/agent/modes.ts +429 -0
- package/src/agent/permissions.ts +466 -0
- package/src/agent/subagents/base.ts +116 -0
- package/src/agent/subagents/cost.ts +51 -0
- package/src/agent/subagents/explore.ts +42 -0
- package/src/agent/subagents/general.ts +54 -0
- package/src/agent/subagents/index.ts +102 -0
- package/src/agent/subagents/infra.ts +59 -0
- package/src/agent/subagents/security.ts +69 -0
- package/src/agent/system-prompt.ts +436 -0
- package/src/app.ts +122 -0
- package/src/audit/activity-log.ts +290 -0
- package/src/audit/compliance-checker.ts +540 -0
- package/src/audit/cost-tracker.ts +318 -0
- package/src/audit/index.ts +23 -0
- package/src/audit/security-scanner.ts +596 -0
- package/src/auth/guard.ts +75 -0
- package/src/auth/index.ts +56 -0
- package/src/auth/oauth.ts +455 -0
- package/src/auth/providers.ts +470 -0
- package/src/auth/sso.ts +113 -0
- package/src/auth/store.ts +505 -0
- package/src/auth/types.ts +187 -0
- package/src/build.ts +141 -0
- package/src/cli/index.ts +16 -0
- package/src/cli/init.ts +854 -0
- package/src/cli/openapi-spec.ts +356 -0
- package/src/cli/run.ts +237 -0
- package/src/cli/serve-auth.ts +80 -0
- package/src/cli/serve.ts +462 -0
- package/src/cli/web.ts +67 -0
- package/src/cli.ts +1417 -0
- package/src/clients/core-engine-client.ts +227 -0
- package/src/clients/enterprise-client.ts +334 -0
- package/src/clients/generator-client.ts +351 -0
- package/src/clients/git-client.ts +627 -0
- package/src/clients/github-client.ts +410 -0
- package/src/clients/helm-client.ts +504 -0
- package/src/clients/index.ts +80 -0
- package/src/clients/k8s-client.ts +497 -0
- package/src/clients/llm-client.ts +161 -0
- package/src/clients/rest-client.ts +130 -0
- package/src/clients/service-discovery.ts +33 -0
- package/src/clients/terraform-client.ts +482 -0
- package/src/clients/tools-client.ts +1843 -0
- package/src/clients/ws-client.ts +115 -0
- package/src/commands/analyze/index.ts +352 -0
- package/src/commands/apply/helm.ts +473 -0
- package/src/commands/apply/index.ts +213 -0
- package/src/commands/apply/k8s.ts +454 -0
- package/src/commands/apply/terraform.ts +582 -0
- package/src/commands/ask.ts +167 -0
- package/src/commands/audit/index.ts +238 -0
- package/src/commands/auth-cloud.ts +294 -0
- package/src/commands/auth-list.ts +134 -0
- package/src/commands/auth-profile.ts +121 -0
- package/src/commands/auth-status.ts +141 -0
- package/src/commands/aws/ec2.ts +501 -0
- package/src/commands/aws/iam.ts +397 -0
- package/src/commands/aws/index.ts +133 -0
- package/src/commands/aws/lambda.ts +396 -0
- package/src/commands/aws/rds.ts +439 -0
- package/src/commands/aws/s3.ts +439 -0
- package/src/commands/aws/vpc.ts +393 -0
- package/src/commands/aws-discover.ts +649 -0
- package/src/commands/aws-terraform.ts +805 -0
- package/src/commands/azure/aks.ts +376 -0
- package/src/commands/azure/functions.ts +253 -0
- package/src/commands/azure/index.ts +116 -0
- package/src/commands/azure/storage.ts +478 -0
- package/src/commands/azure/vm.ts +355 -0
- package/src/commands/billing/index.ts +256 -0
- package/src/commands/chat.ts +314 -0
- package/src/commands/config.ts +346 -0
- package/src/commands/cost/cloud-cost-estimator.ts +266 -0
- package/src/commands/cost/estimator.ts +79 -0
- package/src/commands/cost/index.ts +594 -0
- package/src/commands/cost/parsers/terraform.ts +273 -0
- package/src/commands/cost/parsers/types.ts +25 -0
- package/src/commands/cost/pricing/aws.ts +544 -0
- package/src/commands/cost/pricing/azure.ts +499 -0
- package/src/commands/cost/pricing/gcp.ts +396 -0
- package/src/commands/cost/pricing/index.ts +40 -0
- package/src/commands/demo.ts +250 -0
- package/src/commands/doctor.ts +794 -0
- package/src/commands/drift/index.ts +439 -0
- package/src/commands/explain.ts +277 -0
- package/src/commands/feedback.ts +389 -0
- package/src/commands/fix.ts +324 -0
- package/src/commands/fs/index.ts +402 -0
- package/src/commands/gcp/compute.ts +325 -0
- package/src/commands/gcp/functions.ts +271 -0
- package/src/commands/gcp/gke.ts +438 -0
- package/src/commands/gcp/iam.ts +344 -0
- package/src/commands/gcp/index.ts +129 -0
- package/src/commands/gcp/storage.ts +284 -0
- package/src/commands/generate-helm.ts +1249 -0
- package/src/commands/generate-k8s.ts +1560 -0
- package/src/commands/generate-terraform.ts +1460 -0
- package/src/commands/gh/index.ts +863 -0
- package/src/commands/git/index.ts +1343 -0
- package/src/commands/helm/index.ts +1126 -0
- package/src/commands/help.ts +539 -0
- package/src/commands/history.ts +142 -0
- package/src/commands/import.ts +868 -0
- package/src/commands/index.ts +367 -0
- package/src/commands/init.ts +1046 -0
- package/src/commands/k8s/index.ts +1137 -0
- package/src/commands/login.ts +631 -0
- package/src/commands/logout.ts +83 -0
- package/src/commands/onboarding.ts +228 -0
- package/src/commands/plan/display.ts +279 -0
- package/src/commands/plan/index.ts +599 -0
- package/src/commands/preview.ts +452 -0
- package/src/commands/questionnaire.ts +1270 -0
- package/src/commands/resume.ts +55 -0
- package/src/commands/team/index.ts +346 -0
- package/src/commands/template.ts +232 -0
- package/src/commands/tf/index.ts +1034 -0
- package/src/commands/upgrade.ts +550 -0
- package/src/commands/usage/index.ts +134 -0
- package/src/commands/version.ts +170 -0
- package/src/compat/index.ts +2 -0
- package/src/compat/runtime.ts +12 -0
- package/src/compat/sqlite.ts +107 -0
- package/src/config/index.ts +17 -0
- package/src/config/manager.ts +530 -0
- package/src/config/safety-policy.ts +358 -0
- package/src/config/schema.ts +125 -0
- package/src/config/types.ts +527 -0
- package/src/context/context-db.ts +199 -0
- package/src/demo/index.ts +349 -0
- package/src/demo/scenarios/full-journey.ts +229 -0
- package/src/demo/scenarios/getting-started.ts +127 -0
- package/src/demo/scenarios/helm-release.ts +341 -0
- package/src/demo/scenarios/k8s-deployment.ts +194 -0
- package/src/demo/scenarios/terraform-vpc.ts +170 -0
- package/src/demo/types.ts +92 -0
- package/src/engine/cost-estimator.ts +438 -0
- package/src/engine/diagram-generator.ts +256 -0
- package/src/engine/drift-detector.ts +902 -0
- package/src/engine/executor.ts +1035 -0
- package/src/engine/index.ts +76 -0
- package/src/engine/orchestrator.ts +636 -0
- package/src/engine/planner.ts +720 -0
- package/src/engine/safety.ts +743 -0
- package/src/engine/verifier.ts +770 -0
- package/src/enterprise/audit.ts +348 -0
- package/src/enterprise/auth.ts +270 -0
- package/src/enterprise/billing.ts +822 -0
- package/src/enterprise/index.ts +17 -0
- package/src/enterprise/teams.ts +443 -0
- package/src/generator/best-practices.ts +1608 -0
- package/src/generator/helm.ts +630 -0
- package/src/generator/index.ts +37 -0
- package/src/generator/intent-parser.ts +514 -0
- package/src/generator/kubernetes.ts +976 -0
- package/src/generator/terraform.ts +1867 -0
- package/src/history/index.ts +8 -0
- package/src/history/manager.ts +322 -0
- package/src/history/types.ts +34 -0
- package/src/hooks/config.ts +432 -0
- package/src/hooks/engine.ts +391 -0
- package/src/hooks/index.ts +4 -0
- package/src/llm/auth-bridge.ts +198 -0
- package/src/llm/circuit-breaker.ts +140 -0
- package/src/llm/config-loader.ts +201 -0
- package/src/llm/cost-calculator.ts +171 -0
- package/src/llm/index.ts +8 -0
- package/src/llm/model-aliases.ts +115 -0
- package/src/llm/provider-registry.ts +63 -0
- package/src/llm/providers/anthropic.ts +433 -0
- package/src/llm/providers/bedrock.ts +477 -0
- package/src/llm/providers/google.ts +405 -0
- package/src/llm/providers/ollama.ts +767 -0
- package/src/llm/providers/openai-compatible.ts +340 -0
- package/src/llm/providers/openai.ts +328 -0
- package/src/llm/providers/openrouter.ts +338 -0
- package/src/llm/router.ts +1035 -0
- package/src/llm/types.ts +232 -0
- package/src/lsp/client.ts +298 -0
- package/src/lsp/languages.ts +116 -0
- package/src/lsp/manager.ts +278 -0
- package/src/mcp/client.ts +402 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/manager.ts +133 -0
- package/src/nimbus.ts +214 -0
- package/src/plugins/index.ts +27 -0
- package/src/plugins/loader.ts +334 -0
- package/src/plugins/manager.ts +376 -0
- package/src/plugins/types.ts +284 -0
- package/src/scanners/cicd-scanner.ts +258 -0
- package/src/scanners/cloud-scanner.ts +466 -0
- package/src/scanners/framework-scanner.ts +469 -0
- package/src/scanners/iac-scanner.ts +388 -0
- package/src/scanners/index.ts +539 -0
- package/src/scanners/language-scanner.ts +276 -0
- package/src/scanners/package-manager-scanner.ts +277 -0
- package/src/scanners/types.ts +172 -0
- package/src/sessions/manager.ts +365 -0
- package/src/sessions/types.ts +44 -0
- package/src/sharing/sync.ts +296 -0
- package/src/sharing/viewer.ts +97 -0
- package/src/snapshots/index.ts +2 -0
- package/src/snapshots/manager.ts +530 -0
- package/src/state/artifacts.ts +147 -0
- package/src/state/audit.ts +137 -0
- package/src/state/billing.ts +240 -0
- package/src/state/checkpoints.ts +117 -0
- package/src/state/config.ts +67 -0
- package/src/state/conversations.ts +14 -0
- package/src/state/credentials.ts +154 -0
- package/src/state/db.ts +58 -0
- package/src/state/index.ts +26 -0
- package/src/state/messages.ts +115 -0
- package/src/state/projects.ts +123 -0
- package/src/state/schema.ts +236 -0
- package/src/state/sessions.ts +147 -0
- package/src/state/teams.ts +200 -0
- package/src/telemetry.ts +108 -0
- package/src/tools/aws-ops.ts +952 -0
- package/src/tools/azure-ops.ts +579 -0
- package/src/tools/file-ops.ts +593 -0
- package/src/tools/gcp-ops.ts +625 -0
- package/src/tools/git-ops.ts +773 -0
- package/src/tools/github-ops.ts +799 -0
- package/src/tools/helm-ops.ts +943 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/k8s-ops.ts +819 -0
- package/src/tools/schemas/converter.ts +184 -0
- package/src/tools/schemas/devops.ts +612 -0
- package/src/tools/schemas/index.ts +73 -0
- package/src/tools/schemas/standard.ts +1144 -0
- package/src/tools/schemas/types.ts +705 -0
- package/src/tools/terraform-ops.ts +862 -0
- package/src/types/ambient.d.ts +193 -0
- package/src/types/config.ts +83 -0
- package/src/types/drift.ts +116 -0
- package/src/types/enterprise.ts +335 -0
- package/src/types/index.ts +20 -0
- package/src/types/plan.ts +44 -0
- package/src/types/request.ts +65 -0
- package/src/types/response.ts +54 -0
- package/src/types/service.ts +51 -0
- package/src/ui/App.tsx +997 -0
- package/src/ui/DeployPreview.tsx +169 -0
- package/src/ui/Header.tsx +68 -0
- package/src/ui/InputBox.tsx +350 -0
- package/src/ui/MessageList.tsx +585 -0
- package/src/ui/PermissionPrompt.tsx +151 -0
- package/src/ui/StatusBar.tsx +158 -0
- package/src/ui/ToolCallDisplay.tsx +409 -0
- package/src/ui/chat-ui.ts +853 -0
- package/src/ui/index.ts +33 -0
- package/src/ui/ink/index.ts +711 -0
- package/src/ui/streaming.ts +176 -0
- package/src/ui/types.ts +57 -0
- package/src/utils/analytics.ts +72 -0
- package/src/utils/cost-warning.ts +27 -0
- package/src/utils/env.ts +46 -0
- package/src/utils/errors.ts +69 -0
- package/src/utils/event-bus.ts +38 -0
- package/src/utils/index.ts +24 -0
- package/src/utils/logger.ts +171 -0
- package/src/utils/rate-limiter.ts +121 -0
- package/src/utils/service-auth.ts +49 -0
- package/src/utils/validation.ts +53 -0
- package/src/version.ts +4 -0
- package/src/watcher/index.ts +163 -0
- package/src/wizard/approval.ts +383 -0
- package/src/wizard/index.ts +25 -0
- package/src/wizard/prompts.ts +338 -0
- package/src/wizard/types.ts +171 -0
- package/src/wizard/ui.ts +556 -0
- package/src/wizard/wizard.ts +304 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,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;
|