@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,582 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply Terraform Command
|
|
3
|
+
*
|
|
4
|
+
* Apply Terraform configuration to create/update infrastructure
|
|
5
|
+
*
|
|
6
|
+
* Usage: nimbus apply terraform [directory] [options]
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { logger } from '../../utils';
|
|
10
|
+
import { ui, confirm } from '../../wizard';
|
|
11
|
+
import { terraformClient } from '../../clients';
|
|
12
|
+
import { CostEstimator } from '../cost/estimator';
|
|
13
|
+
import {
|
|
14
|
+
loadSafetyPolicy,
|
|
15
|
+
evaluateSafety,
|
|
16
|
+
type SafetyContext,
|
|
17
|
+
type SafetyCheckResult,
|
|
18
|
+
} from '../../config/safety-policy';
|
|
19
|
+
import {
|
|
20
|
+
promptForApproval,
|
|
21
|
+
displaySafetySummary,
|
|
22
|
+
confirmWithResourceName,
|
|
23
|
+
} from '../../wizard/approval';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Command options
|
|
27
|
+
*/
|
|
28
|
+
export interface ApplyTerraformOptions {
|
|
29
|
+
directory?: string;
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
autoApprove?: boolean;
|
|
32
|
+
target?: string;
|
|
33
|
+
var?: Record<string, string>;
|
|
34
|
+
varFile?: string;
|
|
35
|
+
parallelism?: number;
|
|
36
|
+
refresh?: boolean;
|
|
37
|
+
lock?: boolean;
|
|
38
|
+
/** Skip safety checks */
|
|
39
|
+
skipSafety?: boolean;
|
|
40
|
+
/** Environment name (for safety policy) */
|
|
41
|
+
environment?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Display inline cost estimate for a terraform directory
|
|
46
|
+
*/
|
|
47
|
+
async function displayCostEstimate(directory: string): Promise<void> {
|
|
48
|
+
try {
|
|
49
|
+
const estimate = await CostEstimator.estimateDirectory(directory);
|
|
50
|
+
if (estimate.totalMonthlyCost > 0) {
|
|
51
|
+
ui.newLine();
|
|
52
|
+
ui.print(
|
|
53
|
+
` ${ui.color('$', 'yellow')} Estimated monthly cost: ${ui.bold(`$${estimate.totalMonthlyCost.toFixed(2)}/mo`)}`
|
|
54
|
+
);
|
|
55
|
+
const projects = estimate.projects || [];
|
|
56
|
+
const costResources = projects.length > 0 ? projects[0].resources || [] : [];
|
|
57
|
+
if (costResources.length > 0) {
|
|
58
|
+
for (const resource of costResources.slice(0, 5)) {
|
|
59
|
+
ui.print(` ${resource.name}: $${resource.monthlyCost.toFixed(2)}/mo`);
|
|
60
|
+
}
|
|
61
|
+
if (costResources.length > 5) {
|
|
62
|
+
ui.print(ui.dim(` ... and ${costResources.length - 5} more resources`));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Silently skip if cost estimation fails — don't block the apply
|
|
68
|
+
ui.print(ui.dim(' Cost estimation available: run "nimbus cost estimate"'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run terraform apply command
|
|
74
|
+
*/
|
|
75
|
+
export async function applyTerraformCommand(options: ApplyTerraformOptions = {}): Promise<void> {
|
|
76
|
+
logger.info('Running terraform apply', { options });
|
|
77
|
+
|
|
78
|
+
const directory = options.directory || '.';
|
|
79
|
+
|
|
80
|
+
ui.header('Terraform Apply');
|
|
81
|
+
ui.info(`Directory: ${directory}`);
|
|
82
|
+
ui.newLine();
|
|
83
|
+
|
|
84
|
+
// Check if terraform client is available
|
|
85
|
+
const clientAvailable = await terraformClient.isAvailable();
|
|
86
|
+
|
|
87
|
+
if (clientAvailable) {
|
|
88
|
+
// Use terraform tools service
|
|
89
|
+
await applyWithService(options);
|
|
90
|
+
} else {
|
|
91
|
+
// Fall back to local terraform CLI
|
|
92
|
+
await applyWithLocalCLI(options);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Apply using Terraform Tools Service
|
|
98
|
+
*/
|
|
99
|
+
async function applyWithService(options: ApplyTerraformOptions): Promise<void> {
|
|
100
|
+
const directory = options.directory || '.';
|
|
101
|
+
|
|
102
|
+
// First, run plan if not auto-approved
|
|
103
|
+
if (!options.autoApprove) {
|
|
104
|
+
ui.startSpinner({ message: 'Creating execution plan...' });
|
|
105
|
+
|
|
106
|
+
const planResult = await terraformClient.plan(directory, {
|
|
107
|
+
vars: options.var,
|
|
108
|
+
varFile: options.varFile,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
ui.stopSpinnerSuccess('Plan created');
|
|
112
|
+
ui.newLine();
|
|
113
|
+
|
|
114
|
+
if (!planResult.success) {
|
|
115
|
+
ui.error(`Plan failed: ${planResult.error}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Display plan summary
|
|
120
|
+
displayPlanSummary(planResult);
|
|
121
|
+
|
|
122
|
+
// Check if there are changes
|
|
123
|
+
if (!planResult.hasChanges) {
|
|
124
|
+
ui.success('No changes. Infrastructure is up to date.');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Show inline cost estimate if resources are being created
|
|
129
|
+
if (planResult.hasChanges) {
|
|
130
|
+
await displayCostEstimate(directory);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Dry run - don't apply
|
|
134
|
+
if (options.dryRun) {
|
|
135
|
+
ui.newLine();
|
|
136
|
+
ui.info('Dry run mode - no changes applied');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Parse destroy count to determine confirmation type
|
|
141
|
+
const destroyCountMatch = planResult.output.match(/(\d+) to destroy/);
|
|
142
|
+
const destroyCount = parseInt(destroyCountMatch?.[1] || '0', 10);
|
|
143
|
+
|
|
144
|
+
// Run safety checks if not skipped
|
|
145
|
+
if (!options.skipSafety) {
|
|
146
|
+
const safetyResult = await runSafetyChecks('apply', planResult.output, options);
|
|
147
|
+
|
|
148
|
+
if (!safetyResult.passed) {
|
|
149
|
+
ui.newLine();
|
|
150
|
+
ui.error('Safety checks failed - operation blocked');
|
|
151
|
+
for (const blocker of safetyResult.blockers) {
|
|
152
|
+
ui.print(` ${ui.color('✗', 'red')} ${blocker.message}`);
|
|
153
|
+
}
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If safety requires approval, prompt for it
|
|
158
|
+
if (safetyResult.requiresApproval) {
|
|
159
|
+
// Destructive plans require type-name confirmation first
|
|
160
|
+
if (destroyCount > 0) {
|
|
161
|
+
const confirmed = await confirmWithResourceName(directory, 'terraform directory');
|
|
162
|
+
if (!confirmed) {
|
|
163
|
+
ui.newLine();
|
|
164
|
+
ui.info('Apply cancelled');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const approvalResult = await promptForApproval({
|
|
170
|
+
title: 'Terraform Apply',
|
|
171
|
+
operation: 'terraform apply',
|
|
172
|
+
risks: safetyResult.risks,
|
|
173
|
+
environment: options.environment,
|
|
174
|
+
affectedResources: safetyResult.affectedResources,
|
|
175
|
+
estimatedCost: safetyResult.estimatedCost,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (!approvalResult.approved) {
|
|
179
|
+
ui.newLine();
|
|
180
|
+
ui.info(`Apply cancelled: ${approvalResult.reason || 'User declined'}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Show safety summary and simple confirm (or type-name confirm for destroys)
|
|
185
|
+
displaySafetySummary({
|
|
186
|
+
operation: 'terraform apply',
|
|
187
|
+
risks: safetyResult.risks,
|
|
188
|
+
passed: safetyResult.passed,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
ui.newLine();
|
|
192
|
+
|
|
193
|
+
if (destroyCount > 0) {
|
|
194
|
+
const confirmed = await confirmWithResourceName(directory, 'terraform directory');
|
|
195
|
+
if (!confirmed) {
|
|
196
|
+
ui.newLine();
|
|
197
|
+
ui.info('Apply cancelled');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const proceed = await confirm({
|
|
202
|
+
message: 'Do you want to apply these changes?',
|
|
203
|
+
defaultValue: false,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (!proceed) {
|
|
207
|
+
ui.info('Apply cancelled');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
// Simple confirmation when safety is skipped, but still enforce type-name for destroys
|
|
214
|
+
ui.newLine();
|
|
215
|
+
|
|
216
|
+
if (destroyCount > 0) {
|
|
217
|
+
const confirmed = await confirmWithResourceName(directory, 'terraform directory');
|
|
218
|
+
if (!confirmed) {
|
|
219
|
+
ui.newLine();
|
|
220
|
+
ui.info('Apply cancelled');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
const proceed = await confirm({
|
|
225
|
+
message: 'Do you want to apply these changes?',
|
|
226
|
+
defaultValue: false,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!proceed) {
|
|
230
|
+
ui.info('Apply cancelled');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Run apply
|
|
238
|
+
ui.newLine();
|
|
239
|
+
ui.startSpinner({ message: 'Applying changes...' });
|
|
240
|
+
|
|
241
|
+
const applyResult = await terraformClient.apply(directory, {
|
|
242
|
+
autoApprove: true, // Already confirmed above
|
|
243
|
+
vars: options.var,
|
|
244
|
+
varFile: options.varFile,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (!applyResult.success) {
|
|
248
|
+
ui.stopSpinnerFail('Apply failed');
|
|
249
|
+
ui.error(applyResult.error || 'Unknown error');
|
|
250
|
+
|
|
251
|
+
if (applyResult.output) {
|
|
252
|
+
ui.newLine();
|
|
253
|
+
ui.print(applyResult.output);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
ui.stopSpinnerSuccess('Apply complete!');
|
|
260
|
+
|
|
261
|
+
// Track successful terraform apply
|
|
262
|
+
try {
|
|
263
|
+
const { trackGeneration } = await import('../../telemetry');
|
|
264
|
+
trackGeneration('terraform-apply', ['terraform']);
|
|
265
|
+
} catch {
|
|
266
|
+
/* telemetry failure is non-critical */
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Display output
|
|
270
|
+
if (applyResult.output) {
|
|
271
|
+
ui.newLine();
|
|
272
|
+
ui.print(applyResult.output);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Apply using local Terraform CLI
|
|
278
|
+
*/
|
|
279
|
+
async function applyWithLocalCLI(options: ApplyTerraformOptions): Promise<void> {
|
|
280
|
+
const { spawn } = await import('child_process');
|
|
281
|
+
|
|
282
|
+
const directory = options.directory || '.';
|
|
283
|
+
|
|
284
|
+
// First, run plan to get the output for safety checks (unless auto-approved)
|
|
285
|
+
if (!options.autoApprove && !options.skipSafety) {
|
|
286
|
+
ui.startSpinner({ message: 'Creating execution plan...' });
|
|
287
|
+
|
|
288
|
+
const planOutput = await runLocalTerraformPlan(directory, options);
|
|
289
|
+
|
|
290
|
+
ui.stopSpinnerSuccess('Plan created');
|
|
291
|
+
ui.newLine();
|
|
292
|
+
|
|
293
|
+
// Display plan summary
|
|
294
|
+
const hasChanges =
|
|
295
|
+
planOutput.includes('to add') ||
|
|
296
|
+
planOutput.includes('to change') ||
|
|
297
|
+
planOutput.includes('to destroy');
|
|
298
|
+
|
|
299
|
+
displayPlanSummary({
|
|
300
|
+
success: true,
|
|
301
|
+
hasChanges,
|
|
302
|
+
output: planOutput,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (!hasChanges) {
|
|
306
|
+
ui.success('No changes. Infrastructure is up to date.');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Dry run - don't apply
|
|
311
|
+
if (options.dryRun) {
|
|
312
|
+
ui.newLine();
|
|
313
|
+
ui.info('Dry run mode - no changes applied');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Parse destroy count to determine confirmation type
|
|
318
|
+
const destroyCountMatch = planOutput.match(/(\d+) to destroy/);
|
|
319
|
+
const destroyCount = parseInt(destroyCountMatch?.[1] || '0', 10);
|
|
320
|
+
|
|
321
|
+
// Run safety checks
|
|
322
|
+
const safetyResult = await runSafetyChecks('apply', planOutput, options);
|
|
323
|
+
|
|
324
|
+
if (!safetyResult.passed) {
|
|
325
|
+
ui.newLine();
|
|
326
|
+
ui.error('Safety checks failed - operation blocked');
|
|
327
|
+
for (const blocker of safetyResult.blockers) {
|
|
328
|
+
ui.print(` ${ui.color('✗', 'red')} ${blocker.message}`);
|
|
329
|
+
}
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// If safety requires approval, prompt for it
|
|
334
|
+
if (safetyResult.requiresApproval) {
|
|
335
|
+
// Destructive plans require type-name confirmation first
|
|
336
|
+
if (destroyCount > 0) {
|
|
337
|
+
const confirmed = await confirmWithResourceName(directory, 'terraform directory');
|
|
338
|
+
if (!confirmed) {
|
|
339
|
+
ui.newLine();
|
|
340
|
+
ui.info('Apply cancelled');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const approvalResult = await promptForApproval({
|
|
346
|
+
title: 'Terraform Apply',
|
|
347
|
+
operation: 'terraform apply',
|
|
348
|
+
risks: safetyResult.risks,
|
|
349
|
+
environment: options.environment,
|
|
350
|
+
affectedResources: safetyResult.affectedResources,
|
|
351
|
+
estimatedCost: safetyResult.estimatedCost,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
if (!approvalResult.approved) {
|
|
355
|
+
ui.newLine();
|
|
356
|
+
ui.info(`Apply cancelled: ${approvalResult.reason || 'User declined'}`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
// Show safety summary and simple confirm (or type-name confirm for destroys)
|
|
361
|
+
displaySafetySummary({
|
|
362
|
+
operation: 'terraform apply',
|
|
363
|
+
risks: safetyResult.risks,
|
|
364
|
+
passed: safetyResult.passed,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
ui.newLine();
|
|
368
|
+
|
|
369
|
+
if (destroyCount > 0) {
|
|
370
|
+
const confirmed = await confirmWithResourceName(directory, 'terraform directory');
|
|
371
|
+
if (!confirmed) {
|
|
372
|
+
ui.newLine();
|
|
373
|
+
ui.info('Apply cancelled');
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
} else {
|
|
377
|
+
const proceed = await confirm({
|
|
378
|
+
message: 'Do you want to apply these changes?',
|
|
379
|
+
defaultValue: false,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
if (!proceed) {
|
|
383
|
+
ui.info('Apply cancelled');
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Build terraform apply command
|
|
391
|
+
const args = ['apply', '-auto-approve']; // Auto-approve since we already confirmed
|
|
392
|
+
|
|
393
|
+
if (options.var) {
|
|
394
|
+
for (const [key, value] of Object.entries(options.var)) {
|
|
395
|
+
args.push('-var', `${key}=${value}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (options.varFile) {
|
|
400
|
+
args.push('-var-file', options.varFile);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (options.target) {
|
|
404
|
+
args.push('-target', options.target);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (options.parallelism !== undefined) {
|
|
408
|
+
args.push('-parallelism', String(options.parallelism));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (options.refresh === false) {
|
|
412
|
+
args.push('-refresh=false');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (options.lock === false) {
|
|
416
|
+
args.push('-lock=false');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
ui.newLine();
|
|
420
|
+
ui.info(`Running: terraform ${args.join(' ')}`);
|
|
421
|
+
ui.newLine();
|
|
422
|
+
|
|
423
|
+
// Run terraform
|
|
424
|
+
return new Promise(resolve => {
|
|
425
|
+
const proc = spawn('terraform', args, {
|
|
426
|
+
cwd: directory,
|
|
427
|
+
stdio: 'inherit',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
proc.on('error', error => {
|
|
431
|
+
ui.error(`Failed to run terraform: ${error.message}`);
|
|
432
|
+
ui.info('Make sure terraform is installed and in your PATH');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
proc.on('close', code => {
|
|
437
|
+
if (code === 0) {
|
|
438
|
+
ui.newLine();
|
|
439
|
+
ui.success('Terraform apply completed successfully');
|
|
440
|
+
|
|
441
|
+
// Track successful terraform apply
|
|
442
|
+
try {
|
|
443
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
444
|
+
const { trackGeneration } = require('../../telemetry');
|
|
445
|
+
trackGeneration('terraform-apply', ['terraform']);
|
|
446
|
+
} catch {
|
|
447
|
+
/* telemetry failure is non-critical */
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
resolve();
|
|
451
|
+
} else {
|
|
452
|
+
ui.newLine();
|
|
453
|
+
ui.error(`Terraform apply failed with exit code ${code}`);
|
|
454
|
+
process.exit(code || 1);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Run local terraform plan and capture output
|
|
462
|
+
*/
|
|
463
|
+
async function runLocalTerraformPlan(
|
|
464
|
+
directory: string,
|
|
465
|
+
options: ApplyTerraformOptions
|
|
466
|
+
): Promise<string> {
|
|
467
|
+
const { spawn } = await import('child_process');
|
|
468
|
+
|
|
469
|
+
const args = ['plan', '-no-color'];
|
|
470
|
+
|
|
471
|
+
if (options.var) {
|
|
472
|
+
for (const [key, value] of Object.entries(options.var)) {
|
|
473
|
+
args.push('-var', `${key}=${value}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (options.varFile) {
|
|
478
|
+
args.push('-var-file', options.varFile);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (options.target) {
|
|
482
|
+
args.push('-target', options.target);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return new Promise((resolve, reject) => {
|
|
486
|
+
let output = '';
|
|
487
|
+
|
|
488
|
+
const proc = spawn('terraform', args, {
|
|
489
|
+
cwd: directory,
|
|
490
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
proc.stdout?.on('data', data => {
|
|
494
|
+
output += data.toString();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
proc.stderr?.on('data', data => {
|
|
498
|
+
output += data.toString();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
proc.on('error', error => {
|
|
502
|
+
reject(new Error(`Failed to run terraform plan: ${error.message}`));
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
proc.on('close', code => {
|
|
506
|
+
if (code === 0) {
|
|
507
|
+
resolve(output);
|
|
508
|
+
} else {
|
|
509
|
+
reject(new Error(`Terraform plan failed with exit code ${code}`));
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Run safety checks for the operation
|
|
517
|
+
*/
|
|
518
|
+
async function runSafetyChecks(
|
|
519
|
+
operation: string,
|
|
520
|
+
planOutput: string,
|
|
521
|
+
options: ApplyTerraformOptions
|
|
522
|
+
): Promise<SafetyCheckResult> {
|
|
523
|
+
const policy = loadSafetyPolicy();
|
|
524
|
+
|
|
525
|
+
const context: SafetyContext = {
|
|
526
|
+
operation,
|
|
527
|
+
type: 'terraform',
|
|
528
|
+
environment: options.environment,
|
|
529
|
+
planOutput,
|
|
530
|
+
metadata: {
|
|
531
|
+
directory: options.directory,
|
|
532
|
+
target: options.target,
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
return evaluateSafety(context, policy);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Display plan summary
|
|
541
|
+
*/
|
|
542
|
+
function displayPlanSummary(planResult: {
|
|
543
|
+
success: boolean;
|
|
544
|
+
hasChanges: boolean;
|
|
545
|
+
output: string;
|
|
546
|
+
}): void {
|
|
547
|
+
if (!planResult.hasChanges) {
|
|
548
|
+
ui.print('Plan Summary:');
|
|
549
|
+
ui.newLine();
|
|
550
|
+
ui.print(' No changes');
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Parse changes from output
|
|
555
|
+
const addMatch = planResult.output.match(/(\d+) to add/);
|
|
556
|
+
const changeMatch = planResult.output.match(/(\d+) to change/);
|
|
557
|
+
const destroyMatch = planResult.output.match(/(\d+) to destroy/);
|
|
558
|
+
|
|
559
|
+
const add = parseInt(addMatch?.[1] || '0', 10);
|
|
560
|
+
const change = parseInt(changeMatch?.[1] || '0', 10);
|
|
561
|
+
const destroy = parseInt(destroyMatch?.[1] || '0', 10);
|
|
562
|
+
|
|
563
|
+
ui.print('Plan Summary:');
|
|
564
|
+
ui.newLine();
|
|
565
|
+
|
|
566
|
+
if (add > 0) {
|
|
567
|
+
ui.print(` ${ui.color(`+ ${add} to add`, 'green')}`);
|
|
568
|
+
}
|
|
569
|
+
if (change > 0) {
|
|
570
|
+
ui.print(` ${ui.color(`~ ${change} to change`, 'yellow')}`);
|
|
571
|
+
}
|
|
572
|
+
if (destroy > 0) {
|
|
573
|
+
ui.print(` ${ui.color(`- ${destroy} to destroy`, 'red')}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (add === 0 && change === 0 && destroy === 0) {
|
|
577
|
+
ui.print(' Changes detected (see output)');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Export as default
|
|
582
|
+
export default applyTerraformCommand;
|