@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,1034 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terraform Commands
|
|
3
|
+
*
|
|
4
|
+
* CLI commands for Terraform operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { terraformClient } from '../../clients';
|
|
8
|
+
import { ui } from '../../wizard/ui';
|
|
9
|
+
import { confirmWithResourceName } from '../../wizard/approval';
|
|
10
|
+
import { showDestructionCostWarning } from '../../utils/cost-warning';
|
|
11
|
+
import { historyManager } from '../../history';
|
|
12
|
+
|
|
13
|
+
export interface TfCommandOptions {
|
|
14
|
+
directory?: string;
|
|
15
|
+
varFile?: string;
|
|
16
|
+
vars?: Record<string, string>;
|
|
17
|
+
autoApprove?: boolean;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
out?: string;
|
|
20
|
+
planFile?: string;
|
|
21
|
+
check?: boolean;
|
|
22
|
+
recursive?: boolean;
|
|
23
|
+
diff?: boolean;
|
|
24
|
+
type?: 'plan' | 'apply';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Initialize Terraform working directory
|
|
29
|
+
*/
|
|
30
|
+
export async function tfInitCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
31
|
+
const directory = options.directory || process.cwd();
|
|
32
|
+
|
|
33
|
+
ui.header('Terraform Init');
|
|
34
|
+
ui.info(`Directory: ${directory}`);
|
|
35
|
+
|
|
36
|
+
ui.startSpinner({ message: 'Initializing Terraform...' });
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const available = await terraformClient.isAvailable();
|
|
40
|
+
if (!available) {
|
|
41
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
42
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const result = await terraformClient.init(directory);
|
|
47
|
+
|
|
48
|
+
if (result.success) {
|
|
49
|
+
ui.stopSpinnerSuccess('Terraform initialized successfully');
|
|
50
|
+
if (result.output) {
|
|
51
|
+
ui.box({ title: 'Output', content: result.output });
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
ui.stopSpinnerFail('Terraform init failed');
|
|
55
|
+
if (result.error) {
|
|
56
|
+
ui.error(result.error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
ui.stopSpinnerFail('Error initializing Terraform');
|
|
61
|
+
ui.error(error.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generate Terraform execution plan
|
|
67
|
+
*/
|
|
68
|
+
export async function tfPlanCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
69
|
+
const directory = options.directory || process.cwd();
|
|
70
|
+
|
|
71
|
+
ui.header('Terraform Plan');
|
|
72
|
+
ui.info(`Directory: ${directory}`);
|
|
73
|
+
|
|
74
|
+
ui.startSpinner({ message: 'Generating Terraform plan...' });
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const available = await terraformClient.isAvailable();
|
|
78
|
+
if (!available) {
|
|
79
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
80
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await terraformClient.plan(directory, {
|
|
85
|
+
varFile: options.varFile,
|
|
86
|
+
vars: options.vars,
|
|
87
|
+
out: options.out,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (result.success) {
|
|
91
|
+
if (result.hasChanges) {
|
|
92
|
+
ui.stopSpinnerSuccess('Plan generated with changes');
|
|
93
|
+
} else {
|
|
94
|
+
ui.stopSpinnerSuccess('Plan generated - no changes');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (result.output) {
|
|
98
|
+
ui.box({ title: 'Plan Output', content: result.output });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (result.planFile) {
|
|
102
|
+
ui.info(`Plan saved to: ${result.planFile}`);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
ui.stopSpinnerFail('Terraform plan failed');
|
|
106
|
+
if (result.error) {
|
|
107
|
+
ui.error(result.error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
ui.stopSpinnerFail('Error generating Terraform plan');
|
|
112
|
+
ui.error(error.message);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Apply Terraform changes
|
|
118
|
+
*/
|
|
119
|
+
export async function tfApplyCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
120
|
+
const directory = options.directory || process.cwd();
|
|
121
|
+
|
|
122
|
+
ui.header('Terraform Apply');
|
|
123
|
+
ui.info(`Directory: ${directory}`);
|
|
124
|
+
|
|
125
|
+
if (!options.autoApprove && !options.planFile) {
|
|
126
|
+
ui.warning('Running with -auto-approve flag or specify a plan file for non-interactive mode');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
ui.startSpinner({ message: 'Applying Terraform changes...' });
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const available = await terraformClient.isAvailable();
|
|
133
|
+
if (!available) {
|
|
134
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
135
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const result = await terraformClient.apply(directory, {
|
|
140
|
+
planFile: options.planFile,
|
|
141
|
+
autoApprove: options.autoApprove,
|
|
142
|
+
varFile: options.varFile,
|
|
143
|
+
vars: options.vars,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (result.success) {
|
|
147
|
+
ui.stopSpinnerSuccess('Terraform apply completed successfully');
|
|
148
|
+
if (result.output) {
|
|
149
|
+
ui.box({ title: 'Apply Output', content: result.output });
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
ui.stopSpinnerFail('Terraform apply failed');
|
|
153
|
+
if (result.error) {
|
|
154
|
+
ui.error(result.error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (error: any) {
|
|
158
|
+
ui.stopSpinnerFail('Error applying Terraform changes');
|
|
159
|
+
ui.error(error.message);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Validate Terraform configuration
|
|
165
|
+
*/
|
|
166
|
+
export async function tfValidateCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
167
|
+
const directory = options.directory || process.cwd();
|
|
168
|
+
|
|
169
|
+
ui.header('Terraform Validate');
|
|
170
|
+
ui.info(`Directory: ${directory}`);
|
|
171
|
+
|
|
172
|
+
ui.startSpinner({ message: 'Validating Terraform configuration...' });
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const available = await terraformClient.isAvailable();
|
|
176
|
+
if (!available) {
|
|
177
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
178
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const result = await terraformClient.validate(directory);
|
|
183
|
+
|
|
184
|
+
if (result.valid) {
|
|
185
|
+
ui.stopSpinnerSuccess('Configuration is valid');
|
|
186
|
+
if (result.output) {
|
|
187
|
+
ui.box({ title: 'Validation Output', content: result.output });
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
ui.stopSpinnerFail('Configuration is invalid');
|
|
191
|
+
if (result.error) {
|
|
192
|
+
ui.error(result.error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
} catch (error: any) {
|
|
196
|
+
ui.stopSpinnerFail('Error validating Terraform configuration');
|
|
197
|
+
ui.error(error.message);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Destroy Terraform-managed infrastructure
|
|
203
|
+
*/
|
|
204
|
+
export async function tfDestroyCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
205
|
+
const directory = options.directory || process.cwd();
|
|
206
|
+
const path = await import('path');
|
|
207
|
+
const workspaceName = path.basename(path.resolve(directory));
|
|
208
|
+
|
|
209
|
+
ui.header('Terraform Destroy');
|
|
210
|
+
ui.info(`Directory: ${directory}`);
|
|
211
|
+
ui.warning('This will destroy all managed infrastructure!');
|
|
212
|
+
|
|
213
|
+
// Show cost warning before destructive operation
|
|
214
|
+
await showDestructionCostWarning(directory);
|
|
215
|
+
|
|
216
|
+
if (!options.autoApprove) {
|
|
217
|
+
// Require type-name-to-delete confirmation
|
|
218
|
+
const confirmed = await confirmWithResourceName(workspaceName, 'terraform workspace');
|
|
219
|
+
if (!confirmed) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
ui.startSpinner({ message: 'Destroying Terraform resources...' });
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const available = await terraformClient.isAvailable();
|
|
228
|
+
if (!available) {
|
|
229
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
230
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const result = await terraformClient.destroy(directory, {
|
|
235
|
+
autoApprove: options.autoApprove,
|
|
236
|
+
varFile: options.varFile,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (result.success) {
|
|
240
|
+
ui.stopSpinnerSuccess('Terraform destroy completed');
|
|
241
|
+
if (result.output) {
|
|
242
|
+
ui.box({ title: 'Destroy Output', content: result.output });
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
ui.stopSpinnerFail('Terraform destroy failed');
|
|
246
|
+
if (result.error) {
|
|
247
|
+
ui.error(result.error);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (error: any) {
|
|
251
|
+
ui.stopSpinnerFail('Error destroying Terraform resources');
|
|
252
|
+
ui.error(error.message);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Show Terraform state
|
|
258
|
+
*/
|
|
259
|
+
export async function tfShowCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
260
|
+
const directory = options.directory || process.cwd();
|
|
261
|
+
|
|
262
|
+
ui.header('Terraform Show');
|
|
263
|
+
ui.info(`Directory: ${directory}`);
|
|
264
|
+
|
|
265
|
+
ui.startSpinner({ message: 'Retrieving Terraform state...' });
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const available = await terraformClient.isAvailable();
|
|
269
|
+
if (!available) {
|
|
270
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
271
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const result = await terraformClient.show(directory);
|
|
276
|
+
|
|
277
|
+
if (result.success) {
|
|
278
|
+
ui.stopSpinnerSuccess('State retrieved');
|
|
279
|
+
if (result.output) {
|
|
280
|
+
ui.box({ title: 'State', content: result.output });
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
ui.stopSpinnerFail('Failed to retrieve state');
|
|
284
|
+
}
|
|
285
|
+
} catch (error: any) {
|
|
286
|
+
ui.stopSpinnerFail('Error retrieving Terraform state');
|
|
287
|
+
ui.error(error.message);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Format Terraform configuration files
|
|
293
|
+
*/
|
|
294
|
+
export async function tfFmtCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
295
|
+
const directory = options.directory || process.cwd();
|
|
296
|
+
|
|
297
|
+
ui.header('Terraform Fmt');
|
|
298
|
+
ui.info(`Directory: ${directory}`);
|
|
299
|
+
|
|
300
|
+
if (options.check) {
|
|
301
|
+
ui.info('Mode: check only (no changes will be made)');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
ui.startSpinner({ message: 'Formatting Terraform files...' });
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const available = await terraformClient.isAvailable();
|
|
308
|
+
if (!available) {
|
|
309
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
310
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const result = await terraformClient.fmt(directory, {
|
|
315
|
+
check: options.check,
|
|
316
|
+
recursive: options.recursive,
|
|
317
|
+
diff: options.diff,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (result.success) {
|
|
321
|
+
if (result.files && result.files.length > 0) {
|
|
322
|
+
ui.stopSpinnerSuccess(`Formatted ${result.files.length} file(s)`);
|
|
323
|
+
for (const file of result.files) {
|
|
324
|
+
ui.print(` ${ui.color('*', 'green')} ${file}`);
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
ui.stopSpinnerSuccess('All files already formatted');
|
|
328
|
+
}
|
|
329
|
+
if (result.output) {
|
|
330
|
+
ui.box({ title: 'Fmt Output', content: result.output });
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
ui.stopSpinnerFail('Terraform fmt failed');
|
|
334
|
+
if (result.error) {
|
|
335
|
+
ui.error(result.error);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch (error: any) {
|
|
339
|
+
ui.stopSpinnerFail('Error formatting Terraform files');
|
|
340
|
+
ui.error(error.message);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Manage Terraform workspaces
|
|
346
|
+
*/
|
|
347
|
+
export async function tfWorkspaceCommand(
|
|
348
|
+
subcommand: string,
|
|
349
|
+
name: string | undefined,
|
|
350
|
+
options: TfCommandOptions = {}
|
|
351
|
+
): Promise<void> {
|
|
352
|
+
const directory = options.directory || process.cwd();
|
|
353
|
+
|
|
354
|
+
ui.header('Terraform Workspace');
|
|
355
|
+
ui.info(`Directory: ${directory}`);
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const available = await terraformClient.isAvailable();
|
|
359
|
+
if (!available) {
|
|
360
|
+
ui.error('Terraform Tools Service not available');
|
|
361
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
switch (subcommand) {
|
|
366
|
+
case 'list': {
|
|
367
|
+
ui.startSpinner({ message: 'Listing workspaces...' });
|
|
368
|
+
const result = await terraformClient.workspace.list(directory);
|
|
369
|
+
if (result.success) {
|
|
370
|
+
ui.stopSpinnerSuccess('Workspaces retrieved');
|
|
371
|
+
if (result.workspaces && result.workspaces.length > 0) {
|
|
372
|
+
for (const ws of result.workspaces) {
|
|
373
|
+
const marker = ws === result.current ? '* ' : ' ';
|
|
374
|
+
ui.print(`${marker}${ws}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (result.output) {
|
|
378
|
+
ui.box({ title: 'Workspace List', content: result.output });
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
ui.stopSpinnerFail('Failed to list workspaces');
|
|
382
|
+
if (result.error) {
|
|
383
|
+
ui.error(result.error);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
case 'select': {
|
|
390
|
+
if (!name) {
|
|
391
|
+
ui.error('Usage: nimbus tf workspace select <name>');
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
ui.startSpinner({ message: `Selecting workspace "${name}"...` });
|
|
395
|
+
const result = await terraformClient.workspace.select(name, directory);
|
|
396
|
+
if (result.success) {
|
|
397
|
+
ui.stopSpinnerSuccess(`Switched to workspace "${name}"`);
|
|
398
|
+
if (result.output) {
|
|
399
|
+
ui.box({ title: 'Output', content: result.output });
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
ui.stopSpinnerFail(`Failed to select workspace "${name}"`);
|
|
403
|
+
if (result.error) {
|
|
404
|
+
ui.error(result.error);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
case 'new': {
|
|
411
|
+
if (!name) {
|
|
412
|
+
ui.error('Usage: nimbus tf workspace new <name>');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
ui.startSpinner({ message: `Creating workspace "${name}"...` });
|
|
416
|
+
const result = await terraformClient.workspace.new(name, directory);
|
|
417
|
+
if (result.success) {
|
|
418
|
+
ui.stopSpinnerSuccess(`Created and switched to workspace "${name}"`);
|
|
419
|
+
if (result.output) {
|
|
420
|
+
ui.box({ title: 'Output', content: result.output });
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
ui.stopSpinnerFail(`Failed to create workspace "${name}"`);
|
|
424
|
+
if (result.error) {
|
|
425
|
+
ui.error(result.error);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
case 'delete': {
|
|
432
|
+
if (!name) {
|
|
433
|
+
ui.error('Usage: nimbus tf workspace delete <name>');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
ui.startSpinner({ message: `Deleting workspace "${name}"...` });
|
|
437
|
+
const result = await terraformClient.workspace.delete(name, directory);
|
|
438
|
+
if (result.success) {
|
|
439
|
+
ui.stopSpinnerSuccess(`Deleted workspace "${name}"`);
|
|
440
|
+
if (result.output) {
|
|
441
|
+
ui.box({ title: 'Output', content: result.output });
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
ui.stopSpinnerFail(`Failed to delete workspace "${name}"`);
|
|
445
|
+
if (result.error) {
|
|
446
|
+
ui.error(result.error);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
default:
|
|
453
|
+
ui.error(`Unknown workspace subcommand: ${subcommand}`);
|
|
454
|
+
ui.info('Available subcommands: list, select, new, delete');
|
|
455
|
+
}
|
|
456
|
+
} catch (error: any) {
|
|
457
|
+
ui.stopSpinnerFail('Error managing Terraform workspace');
|
|
458
|
+
ui.error(error.message);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Import existing infrastructure into Terraform state
|
|
464
|
+
*/
|
|
465
|
+
export async function tfImportCommand(
|
|
466
|
+
address: string,
|
|
467
|
+
id: string,
|
|
468
|
+
options: TfCommandOptions = {}
|
|
469
|
+
): Promise<void> {
|
|
470
|
+
const directory = options.directory || process.cwd();
|
|
471
|
+
|
|
472
|
+
ui.header('Terraform Import');
|
|
473
|
+
ui.info(`Directory: ${directory}`);
|
|
474
|
+
ui.info(`Address: ${address}`);
|
|
475
|
+
ui.info(`ID: ${id}`);
|
|
476
|
+
|
|
477
|
+
ui.startSpinner({ message: `Importing ${address}...` });
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const available = await terraformClient.isAvailable();
|
|
481
|
+
if (!available) {
|
|
482
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
483
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const result = await terraformClient.import(directory, address, id);
|
|
488
|
+
|
|
489
|
+
if (result.success) {
|
|
490
|
+
ui.stopSpinnerSuccess(`Successfully imported ${address}`);
|
|
491
|
+
if (result.output) {
|
|
492
|
+
ui.box({ title: 'Import Output', content: result.output });
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
ui.stopSpinnerFail('Terraform import failed');
|
|
496
|
+
if (result.error) {
|
|
497
|
+
ui.error(result.error);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
} catch (error: any) {
|
|
501
|
+
ui.stopSpinnerFail('Error importing resource');
|
|
502
|
+
ui.error(error.message);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Show Terraform output values
|
|
508
|
+
*/
|
|
509
|
+
export async function tfOutputCommand(
|
|
510
|
+
options: TfCommandOptions = {},
|
|
511
|
+
name?: string
|
|
512
|
+
): Promise<void> {
|
|
513
|
+
const directory = options.directory || process.cwd();
|
|
514
|
+
|
|
515
|
+
ui.header('Terraform Output');
|
|
516
|
+
ui.info(`Directory: ${directory}`);
|
|
517
|
+
if (name) {
|
|
518
|
+
ui.info(`Output: ${name}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
ui.startSpinner({ message: 'Retrieving Terraform outputs...' });
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
const available = await terraformClient.isAvailable();
|
|
525
|
+
if (!available) {
|
|
526
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
527
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const result = await terraformClient.output(directory, name);
|
|
532
|
+
|
|
533
|
+
if (result.success) {
|
|
534
|
+
ui.stopSpinnerSuccess('Outputs retrieved');
|
|
535
|
+
if (result.outputs) {
|
|
536
|
+
for (const [key, val] of Object.entries(result.outputs)) {
|
|
537
|
+
const value = val.sensitive ? '<sensitive>' : JSON.stringify(val.value);
|
|
538
|
+
ui.print(` ${ui.color(key, 'cyan')} = ${value}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (result.output) {
|
|
542
|
+
ui.box({ title: 'Output', content: result.output });
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
ui.stopSpinnerFail('Failed to retrieve outputs');
|
|
546
|
+
if (result.error) {
|
|
547
|
+
ui.error(result.error);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch (error: any) {
|
|
551
|
+
ui.stopSpinnerFail('Error retrieving Terraform outputs');
|
|
552
|
+
ui.error(error.message);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Manage Terraform state
|
|
558
|
+
*/
|
|
559
|
+
export async function tfStateCommand(
|
|
560
|
+
args: string[],
|
|
561
|
+
options: TfCommandOptions = {}
|
|
562
|
+
): Promise<void> {
|
|
563
|
+
const directory = options.directory || process.cwd();
|
|
564
|
+
const subcommand = args[0];
|
|
565
|
+
|
|
566
|
+
ui.header('Terraform State');
|
|
567
|
+
ui.info(`Directory: ${directory}`);
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const available = await terraformClient.isAvailable();
|
|
571
|
+
if (!available) {
|
|
572
|
+
ui.error('Terraform Tools Service not available');
|
|
573
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
switch (subcommand) {
|
|
578
|
+
case 'list': {
|
|
579
|
+
ui.startSpinner({ message: 'Listing state resources...' });
|
|
580
|
+
const result = await terraformClient.state.list(directory);
|
|
581
|
+
if (result.success) {
|
|
582
|
+
ui.stopSpinnerSuccess('State resources retrieved');
|
|
583
|
+
if (result.resources && result.resources.length > 0) {
|
|
584
|
+
for (const resource of result.resources) {
|
|
585
|
+
ui.print(` ${resource}`);
|
|
586
|
+
}
|
|
587
|
+
} else if (result.output) {
|
|
588
|
+
ui.box({ title: 'State List', content: result.output });
|
|
589
|
+
} else {
|
|
590
|
+
ui.info('No resources found in state.');
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
ui.stopSpinnerFail('Failed to list state resources');
|
|
594
|
+
if (result.error) {
|
|
595
|
+
ui.error(result.error);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
case 'show': {
|
|
602
|
+
const address = args[1];
|
|
603
|
+
if (!address) {
|
|
604
|
+
ui.error('Usage: nimbus tf state show <address>');
|
|
605
|
+
ui.info('Example: nimbus tf state show aws_instance.web');
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
ui.startSpinner({ message: `Showing state for ${address}...` });
|
|
609
|
+
const result = await terraformClient.state.show(address, directory);
|
|
610
|
+
if (result.success) {
|
|
611
|
+
ui.stopSpinnerSuccess(`State retrieved for ${address}`);
|
|
612
|
+
if (result.output) {
|
|
613
|
+
ui.box({ title: `State: ${address}`, content: result.output });
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
ui.stopSpinnerFail(`Failed to show state for ${address}`);
|
|
617
|
+
if (result.error) {
|
|
618
|
+
ui.error(result.error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
case 'mv': {
|
|
625
|
+
const source = args[1];
|
|
626
|
+
const destination = args[2];
|
|
627
|
+
if (!source || !destination) {
|
|
628
|
+
ui.error('Usage: nimbus tf state mv <source> <destination>');
|
|
629
|
+
ui.info('Example: nimbus tf state mv aws_instance.old aws_instance.new');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
ui.startSpinner({ message: `Moving state from ${source} to ${destination}...` });
|
|
633
|
+
const result = await terraformClient.state.mv(directory, source, destination);
|
|
634
|
+
if (result.success) {
|
|
635
|
+
ui.stopSpinnerSuccess(`Moved state: ${source} -> ${destination}`);
|
|
636
|
+
if (result.output) {
|
|
637
|
+
ui.box({ title: 'State Move Output', content: result.output });
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
ui.stopSpinnerFail(`Failed to move state from ${source} to ${destination}`);
|
|
641
|
+
if (result.error) {
|
|
642
|
+
ui.error(result.error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
case 'pull': {
|
|
649
|
+
ui.startSpinner({ message: 'Pulling remote state...' });
|
|
650
|
+
const result = await terraformClient.state.pull(directory);
|
|
651
|
+
if (result.success) {
|
|
652
|
+
ui.stopSpinnerSuccess('Remote state pulled successfully');
|
|
653
|
+
if (result.output) {
|
|
654
|
+
ui.box({ title: 'State Pull Output', content: result.output });
|
|
655
|
+
}
|
|
656
|
+
} else {
|
|
657
|
+
ui.stopSpinnerFail('Failed to pull remote state');
|
|
658
|
+
if (result.error) {
|
|
659
|
+
ui.error(result.error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
case 'push': {
|
|
666
|
+
ui.startSpinner({ message: 'Pushing local state...' });
|
|
667
|
+
const result = await terraformClient.state.push(directory);
|
|
668
|
+
if (result.success) {
|
|
669
|
+
ui.stopSpinnerSuccess('Local state pushed successfully');
|
|
670
|
+
if (result.output) {
|
|
671
|
+
ui.box({ title: 'State Push Output', content: result.output });
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
ui.stopSpinnerFail('Failed to push local state');
|
|
675
|
+
if (result.error) {
|
|
676
|
+
ui.error(result.error);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
default:
|
|
683
|
+
ui.error(`Unknown state subcommand: ${subcommand || '(none)'}`);
|
|
684
|
+
ui.info('Available subcommands: list, show <address>, mv <src> <dst>, pull, push');
|
|
685
|
+
}
|
|
686
|
+
} catch (error: any) {
|
|
687
|
+
ui.stopSpinnerFail('Error managing Terraform state');
|
|
688
|
+
ui.error(error.message);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Taint a resource, marking it for recreation on next apply
|
|
694
|
+
*/
|
|
695
|
+
export async function tfTaintCommand(
|
|
696
|
+
address: string,
|
|
697
|
+
options: TfCommandOptions = {}
|
|
698
|
+
): Promise<void> {
|
|
699
|
+
const directory = options.directory || process.cwd();
|
|
700
|
+
|
|
701
|
+
ui.header('Terraform Taint');
|
|
702
|
+
ui.info(`Directory: ${directory}`);
|
|
703
|
+
ui.info(`Address: ${address}`);
|
|
704
|
+
ui.warning('This resource will be destroyed and recreated on the next apply.');
|
|
705
|
+
|
|
706
|
+
ui.startSpinner({ message: `Tainting ${address}...` });
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
const available = await terraformClient.isAvailable();
|
|
710
|
+
if (!available) {
|
|
711
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
712
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const result = await terraformClient.taint(directory, address);
|
|
717
|
+
|
|
718
|
+
if (result.success) {
|
|
719
|
+
ui.stopSpinnerSuccess(`Resource ${address} tainted successfully`);
|
|
720
|
+
if (result.output) {
|
|
721
|
+
ui.box({ title: 'Taint Output', content: result.output });
|
|
722
|
+
}
|
|
723
|
+
} else {
|
|
724
|
+
ui.stopSpinnerFail(`Failed to taint resource ${address}`);
|
|
725
|
+
if (result.error) {
|
|
726
|
+
ui.error(result.error);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch (error: any) {
|
|
730
|
+
ui.stopSpinnerFail('Error tainting resource');
|
|
731
|
+
ui.error(error.message);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Untaint a resource, removing the taint mark
|
|
737
|
+
*/
|
|
738
|
+
export async function tfUntaintCommand(
|
|
739
|
+
address: string,
|
|
740
|
+
options: TfCommandOptions = {}
|
|
741
|
+
): Promise<void> {
|
|
742
|
+
const directory = options.directory || process.cwd();
|
|
743
|
+
|
|
744
|
+
ui.header('Terraform Untaint');
|
|
745
|
+
ui.info(`Directory: ${directory}`);
|
|
746
|
+
ui.info(`Address: ${address}`);
|
|
747
|
+
|
|
748
|
+
ui.startSpinner({ message: `Untainting ${address}...` });
|
|
749
|
+
|
|
750
|
+
try {
|
|
751
|
+
const available = await terraformClient.isAvailable();
|
|
752
|
+
if (!available) {
|
|
753
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
754
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const result = await terraformClient.untaint(directory, address);
|
|
759
|
+
|
|
760
|
+
if (result.success) {
|
|
761
|
+
ui.stopSpinnerSuccess(`Resource ${address} untainted successfully`);
|
|
762
|
+
if (result.output) {
|
|
763
|
+
ui.box({ title: 'Untaint Output', content: result.output });
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
ui.stopSpinnerFail(`Failed to untaint resource ${address}`);
|
|
767
|
+
if (result.error) {
|
|
768
|
+
ui.error(result.error);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
} catch (error: any) {
|
|
772
|
+
ui.stopSpinnerFail('Error untainting resource');
|
|
773
|
+
ui.error(error.message);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Generate a resource dependency graph in DOT format
|
|
779
|
+
*/
|
|
780
|
+
export async function tfGraphCommand(
|
|
781
|
+
options: TfCommandOptions & { type?: 'plan' | 'apply' } = {}
|
|
782
|
+
): Promise<void> {
|
|
783
|
+
const directory = options.directory || process.cwd();
|
|
784
|
+
|
|
785
|
+
ui.header('Terraform Graph');
|
|
786
|
+
ui.info(`Directory: ${directory}`);
|
|
787
|
+
if (options.type) {
|
|
788
|
+
ui.info(`Graph type: ${options.type}`);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
ui.startSpinner({ message: 'Generating resource dependency graph...' });
|
|
792
|
+
|
|
793
|
+
try {
|
|
794
|
+
const available = await terraformClient.isAvailable();
|
|
795
|
+
if (!available) {
|
|
796
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
797
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const result = await terraformClient.graph(directory, { type: options.type });
|
|
802
|
+
|
|
803
|
+
if (result.success) {
|
|
804
|
+
ui.stopSpinnerSuccess('Dependency graph generated');
|
|
805
|
+
if (result.output) {
|
|
806
|
+
ui.box({ title: 'Dependency Graph (DOT format)', content: result.output });
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
ui.stopSpinnerFail('Failed to generate dependency graph');
|
|
810
|
+
if (result.error) {
|
|
811
|
+
ui.error(result.error);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
} catch (error: any) {
|
|
815
|
+
ui.stopSpinnerFail('Error generating Terraform graph');
|
|
816
|
+
ui.error(error.message);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Force unlock a locked Terraform state
|
|
822
|
+
*/
|
|
823
|
+
export async function tfForceUnlockCommand(
|
|
824
|
+
lockId: string,
|
|
825
|
+
options: TfCommandOptions = {}
|
|
826
|
+
): Promise<void> {
|
|
827
|
+
const directory = options.directory || process.cwd();
|
|
828
|
+
|
|
829
|
+
ui.header('Terraform Force-Unlock');
|
|
830
|
+
ui.info(`Directory: ${directory}`);
|
|
831
|
+
ui.info(`Lock ID: ${lockId}`);
|
|
832
|
+
ui.warning('Force-unlocking state should only be done when a legitimate lock is stuck.');
|
|
833
|
+
|
|
834
|
+
ui.startSpinner({ message: `Force-unlocking state with lock ID ${lockId}...` });
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
const available = await terraformClient.isAvailable();
|
|
838
|
+
if (!available) {
|
|
839
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
840
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const result = await terraformClient.forceUnlock(directory, lockId);
|
|
845
|
+
|
|
846
|
+
if (result.success) {
|
|
847
|
+
ui.stopSpinnerSuccess('State lock released successfully');
|
|
848
|
+
if (result.output) {
|
|
849
|
+
ui.box({ title: 'Force-Unlock Output', content: result.output });
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
ui.stopSpinnerFail('Failed to force-unlock state');
|
|
853
|
+
if (result.error) {
|
|
854
|
+
ui.error(result.error);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
} catch (error: any) {
|
|
858
|
+
ui.stopSpinnerFail('Error force-unlocking Terraform state');
|
|
859
|
+
ui.error(error.message);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Refresh Terraform state against real infrastructure
|
|
865
|
+
*/
|
|
866
|
+
export async function tfRefreshCommand(options: TfCommandOptions = {}): Promise<void> {
|
|
867
|
+
const directory = options.directory || process.cwd();
|
|
868
|
+
|
|
869
|
+
ui.header('Terraform Refresh');
|
|
870
|
+
ui.info(`Directory: ${directory}`);
|
|
871
|
+
|
|
872
|
+
ui.startSpinner({ message: 'Refreshing Terraform state...' });
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
const available = await terraformClient.isAvailable();
|
|
876
|
+
if (!available) {
|
|
877
|
+
ui.stopSpinnerFail('Terraform Tools Service not available');
|
|
878
|
+
ui.error('Please ensure the Terraform Tools Service is running.');
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const result = await terraformClient.refresh(directory, { varFile: options.varFile });
|
|
883
|
+
|
|
884
|
+
if (result.success) {
|
|
885
|
+
ui.stopSpinnerSuccess('Terraform state refreshed successfully');
|
|
886
|
+
if (result.output) {
|
|
887
|
+
ui.box({ title: 'Refresh Output', content: result.output });
|
|
888
|
+
}
|
|
889
|
+
} else {
|
|
890
|
+
ui.stopSpinnerFail('Terraform refresh failed');
|
|
891
|
+
if (result.error) {
|
|
892
|
+
ui.error(result.error);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
} catch (error: any) {
|
|
896
|
+
ui.stopSpinnerFail('Error refreshing Terraform state');
|
|
897
|
+
ui.error(error.message);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Main terraform command router
|
|
903
|
+
*/
|
|
904
|
+
export async function tfCommand(subcommand: string, args: string[]): Promise<void> {
|
|
905
|
+
const options: TfCommandOptions = {
|
|
906
|
+
directory: process.cwd(),
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
// Collect positional args (non-flag args)
|
|
910
|
+
const positionalArgs: string[] = [];
|
|
911
|
+
|
|
912
|
+
// Parse args
|
|
913
|
+
for (let i = 0; i < args.length; i++) {
|
|
914
|
+
const arg = args[i];
|
|
915
|
+
if (arg === '-d' || arg === '--directory') {
|
|
916
|
+
options.directory = args[++i];
|
|
917
|
+
} else if (arg === '--var-file') {
|
|
918
|
+
options.varFile = args[++i];
|
|
919
|
+
} else if (arg === '--auto-approve' || arg === '--yes' || arg === '-y') {
|
|
920
|
+
options.autoApprove = true;
|
|
921
|
+
} else if (arg === '--dry-run') {
|
|
922
|
+
options.dryRun = true;
|
|
923
|
+
} else if (arg === '-o' || arg === '--out') {
|
|
924
|
+
options.out = args[++i];
|
|
925
|
+
} else if (arg === '-p' || arg === '--plan') {
|
|
926
|
+
options.planFile = args[++i];
|
|
927
|
+
} else if (arg.startsWith('--var=')) {
|
|
928
|
+
const [key, value] = arg.slice(6).split('=');
|
|
929
|
+
options.vars = options.vars || {};
|
|
930
|
+
options.vars[key] = value;
|
|
931
|
+
} else if (arg === '--check') {
|
|
932
|
+
options.check = true;
|
|
933
|
+
} else if (arg === '-r' || arg === '--recursive') {
|
|
934
|
+
options.recursive = true;
|
|
935
|
+
} else if (arg === '--diff') {
|
|
936
|
+
options.diff = true;
|
|
937
|
+
} else if (arg === '--type') {
|
|
938
|
+
const typeVal = args[++i];
|
|
939
|
+
if (typeVal === 'plan' || typeVal === 'apply') {
|
|
940
|
+
options.type = typeVal;
|
|
941
|
+
}
|
|
942
|
+
} else if (!arg.startsWith('-')) {
|
|
943
|
+
positionalArgs.push(arg);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const startTime = Date.now();
|
|
948
|
+
const entry = historyManager.addEntry('tf', [subcommand, ...args]);
|
|
949
|
+
|
|
950
|
+
try {
|
|
951
|
+
switch (subcommand) {
|
|
952
|
+
case 'init':
|
|
953
|
+
await tfInitCommand(options);
|
|
954
|
+
break;
|
|
955
|
+
case 'plan':
|
|
956
|
+
await tfPlanCommand(options);
|
|
957
|
+
break;
|
|
958
|
+
case 'apply':
|
|
959
|
+
await tfApplyCommand(options);
|
|
960
|
+
break;
|
|
961
|
+
case 'validate':
|
|
962
|
+
await tfValidateCommand(options);
|
|
963
|
+
break;
|
|
964
|
+
case 'destroy':
|
|
965
|
+
await tfDestroyCommand(options);
|
|
966
|
+
break;
|
|
967
|
+
case 'show':
|
|
968
|
+
await tfShowCommand(options);
|
|
969
|
+
break;
|
|
970
|
+
case 'fmt':
|
|
971
|
+
await tfFmtCommand(options);
|
|
972
|
+
break;
|
|
973
|
+
case 'workspace':
|
|
974
|
+
await tfWorkspaceCommand(positionalArgs[0] || 'list', positionalArgs[1], options);
|
|
975
|
+
break;
|
|
976
|
+
case 'import':
|
|
977
|
+
if (positionalArgs.length < 2) {
|
|
978
|
+
ui.error('Usage: nimbus tf import <address> <id>');
|
|
979
|
+
ui.info('Example: nimbus tf import aws_instance.web i-1234567890abcdef0');
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
await tfImportCommand(positionalArgs[0], positionalArgs[1], options);
|
|
983
|
+
break;
|
|
984
|
+
case 'output':
|
|
985
|
+
await tfOutputCommand(options, positionalArgs[0]);
|
|
986
|
+
break;
|
|
987
|
+
case 'state':
|
|
988
|
+
await tfStateCommand(positionalArgs, options);
|
|
989
|
+
break;
|
|
990
|
+
case 'taint':
|
|
991
|
+
if (positionalArgs.length < 1) {
|
|
992
|
+
ui.error('Usage: nimbus tf taint <address>');
|
|
993
|
+
ui.info('Example: nimbus tf taint aws_instance.web');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
await tfTaintCommand(positionalArgs[0], options);
|
|
997
|
+
break;
|
|
998
|
+
case 'untaint':
|
|
999
|
+
if (positionalArgs.length < 1) {
|
|
1000
|
+
ui.error('Usage: nimbus tf untaint <address>');
|
|
1001
|
+
ui.info('Example: nimbus tf untaint aws_instance.web');
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
await tfUntaintCommand(positionalArgs[0], options);
|
|
1005
|
+
break;
|
|
1006
|
+
case 'graph':
|
|
1007
|
+
await tfGraphCommand(options);
|
|
1008
|
+
break;
|
|
1009
|
+
case 'force-unlock':
|
|
1010
|
+
if (positionalArgs.length < 1) {
|
|
1011
|
+
ui.error('Usage: nimbus tf force-unlock <lock-id>');
|
|
1012
|
+
ui.info('Example: nimbus tf force-unlock 5b3ab8f0-e74b-5d85-4b2f-b6a9d4b3f3e2');
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
await tfForceUnlockCommand(positionalArgs[0], options);
|
|
1016
|
+
break;
|
|
1017
|
+
case 'refresh':
|
|
1018
|
+
await tfRefreshCommand(options);
|
|
1019
|
+
break;
|
|
1020
|
+
default:
|
|
1021
|
+
ui.error(`Unknown terraform subcommand: ${subcommand}`);
|
|
1022
|
+
ui.info(
|
|
1023
|
+
'Available commands: init, plan, apply, validate, destroy, show, fmt, workspace, import, output, state, taint, untaint, graph, force-unlock, refresh'
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
historyManager.completeEntry(entry.id, 'success', Date.now() - startTime);
|
|
1028
|
+
} catch (error: any) {
|
|
1029
|
+
historyManager.completeEntry(entry.id, 'failure', Date.now() - startTime, {
|
|
1030
|
+
error: error.message,
|
|
1031
|
+
});
|
|
1032
|
+
throw error;
|
|
1033
|
+
}
|
|
1034
|
+
}
|