infra-cost 1.3.0 → 1.4.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/README.md +141 -23
- package/dist/cli/index.js +317 -20
- package/dist/index.js +317 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -309,6 +309,83 @@ infra-cost history --format json > cost-history.json
|
|
|
309
309
|
|
|
310
310
|
---
|
|
311
311
|
|
|
312
|
+
#### `infra-cost terraform` - Terraform Cost Preview (NEW in v1.4.0)
|
|
313
|
+
|
|
314
|
+
**Estimate infrastructure costs BEFORE deploying - shift-left cost management!**
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
# Generate terraform plan and estimate costs
|
|
318
|
+
terraform plan -out=tfplan
|
|
319
|
+
infra-cost terraform --plan tfplan
|
|
320
|
+
|
|
321
|
+
# Output:
|
|
322
|
+
# ╭─────────────────────────────────────────────────────────────╮
|
|
323
|
+
# │ Terraform Cost Estimate │
|
|
324
|
+
# ├─────────────────────────────────────────────────────────────┤
|
|
325
|
+
# │ Resources to CREATE: │
|
|
326
|
+
# │ ───────────────────────────────────────────────────────────│
|
|
327
|
+
# │ + aws_instance.web_server (t3.xlarge) │
|
|
328
|
+
# │ └── Monthly: $121.47 | Hourly: $0.1664 │
|
|
329
|
+
# │ + aws_db_instance.primary (db.r5.large, 100GB gp2) │
|
|
330
|
+
# │ └── Monthly: $182.80 | Hourly: $0.25 │
|
|
331
|
+
# ├─────────────────────────────────────────────────────────────┤
|
|
332
|
+
# │ Resources to MODIFY: │
|
|
333
|
+
# │ ───────────────────────────────────────────────────────────│
|
|
334
|
+
# │ ~ aws_instance.api_server │
|
|
335
|
+
# │ └── t3.medium → t3.large: +$30.37/month │
|
|
336
|
+
# ├─────────────────────────────────────────────────────────────┤
|
|
337
|
+
# │ Resources to DESTROY: │
|
|
338
|
+
# │ ───────────────────────────────────────────────────────────│
|
|
339
|
+
# │ - aws_instance.old_server │
|
|
340
|
+
# │ └── Savings: -$60.74/month │
|
|
341
|
+
# ├─────────────────────────────────────────────────────────────┤
|
|
342
|
+
# │ SUMMARY │
|
|
343
|
+
# │ ───────────────────────────────────────────────────────────│
|
|
344
|
+
# │ Current Monthly Cost: $1,240.50 │
|
|
345
|
+
# │ Estimated New Cost: $1,520.83 │
|
|
346
|
+
# │ Difference: +$280.33/month (+22.6%) │
|
|
347
|
+
# │ │
|
|
348
|
+
# │ ⚠️ Cost increase exceeds 20% threshold! │
|
|
349
|
+
# ╰─────────────────────────────────────────────────────────────╯
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**Usage Examples:**
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
# Basic cost estimate
|
|
356
|
+
terraform plan -out=tfplan
|
|
357
|
+
infra-cost terraform --plan tfplan
|
|
358
|
+
|
|
359
|
+
# With cost threshold (fail if > 20% change)
|
|
360
|
+
infra-cost terraform --plan tfplan --threshold 20
|
|
361
|
+
|
|
362
|
+
# JSON format for automation
|
|
363
|
+
infra-cost terraform --plan tfplan --output json
|
|
364
|
+
|
|
365
|
+
# From JSON plan
|
|
366
|
+
terraform show -json tfplan > tfplan.json
|
|
367
|
+
infra-cost terraform --plan tfplan.json
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Supported Resources:**
|
|
371
|
+
- ✅ EC2 instances (all types)
|
|
372
|
+
- ✅ RDS instances with storage
|
|
373
|
+
- ✅ EBS volumes (gp2, gp3, io1, io2, st1, sc1)
|
|
374
|
+
- ✅ Load Balancers (ALB, NLB, CLB)
|
|
375
|
+
- ✅ NAT Gateways
|
|
376
|
+
- ✅ ElastiCache clusters
|
|
377
|
+
- ✅ S3 buckets (estimated)
|
|
378
|
+
- ✅ Lambda functions (estimated)
|
|
379
|
+
|
|
380
|
+
**Perfect for:**
|
|
381
|
+
- CI/CD cost gates
|
|
382
|
+
- Preventing expensive deployments
|
|
383
|
+
- Cost-aware infrastructure changes
|
|
384
|
+
- Shift-left FinOps culture
|
|
385
|
+
- Pre-deployment cost reviews
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
312
389
|
### Command Migration Table
|
|
313
390
|
|
|
314
391
|
| Command Usage | Old Command (v0.x) | New Command (v1.0) |
|
|
@@ -719,48 +796,71 @@ jobs:
|
|
|
719
796
|
--slack-channel ${{ secrets.SLACK_CHANNEL }}
|
|
720
797
|
```
|
|
721
798
|
|
|
722
|
-
## 🤖 GitHub Actions Integration
|
|
799
|
+
## 🤖 GitHub Actions Integration (v1.4.0+)
|
|
723
800
|
|
|
724
|
-
**infra-cost** is available as a GitHub Action
|
|
801
|
+
**infra-cost** is available as a GitHub Action, making it easy to integrate cost analysis into your CI/CD workflows with cost gates and automated PR comments.
|
|
725
802
|
|
|
726
|
-
###
|
|
803
|
+
### Quick Cost Check
|
|
727
804
|
```yaml
|
|
728
805
|
name: Cost Analysis
|
|
729
|
-
on: [
|
|
806
|
+
on: [pull_request]
|
|
730
807
|
|
|
731
808
|
jobs:
|
|
732
|
-
|
|
809
|
+
cost-check:
|
|
733
810
|
runs-on: ubuntu-latest
|
|
734
811
|
steps:
|
|
735
|
-
- uses:
|
|
812
|
+
- uses: actions/checkout@v4
|
|
813
|
+
- uses: codecollab-co/infra-cost@v1.4.0
|
|
736
814
|
with:
|
|
737
815
|
provider: aws
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
816
|
+
command: now
|
|
817
|
+
comment-on-pr: true
|
|
818
|
+
env:
|
|
819
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
741
820
|
```
|
|
742
821
|
|
|
743
|
-
###
|
|
822
|
+
### Cost Gate - Fail on Increase
|
|
744
823
|
```yaml
|
|
745
|
-
name:
|
|
746
|
-
on:
|
|
747
|
-
pull_request:
|
|
748
|
-
branches: [main]
|
|
824
|
+
name: Cost Gate
|
|
825
|
+
on: pull_request
|
|
749
826
|
|
|
750
827
|
jobs:
|
|
751
|
-
cost-
|
|
828
|
+
cost-gate:
|
|
752
829
|
runs-on: ubuntu-latest
|
|
753
|
-
permissions:
|
|
754
|
-
pull-requests: write
|
|
755
830
|
steps:
|
|
756
|
-
- uses:
|
|
831
|
+
- uses: actions/checkout@v4
|
|
832
|
+
- uses: codecollab-co/infra-cost@v1.4.0
|
|
757
833
|
with:
|
|
758
834
|
provider: aws
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
835
|
+
command: now
|
|
836
|
+
fail-on-increase: true # Fail if ANY cost increase
|
|
837
|
+
cost-threshold: 1000 # Fail if monthly cost exceeds $1000
|
|
838
|
+
comment-on-pr: true
|
|
839
|
+
env:
|
|
840
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
### Terraform Cost Preview (Shift-Left)
|
|
844
|
+
```yaml
|
|
845
|
+
name: Terraform Cost Check
|
|
846
|
+
on: pull_request
|
|
847
|
+
|
|
848
|
+
jobs:
|
|
849
|
+
terraform-cost:
|
|
850
|
+
runs-on: ubuntu-latest
|
|
851
|
+
steps:
|
|
852
|
+
- uses: actions/checkout@v4
|
|
853
|
+
|
|
854
|
+
- name: Terraform Plan
|
|
855
|
+
run: terraform plan -out=tfplan
|
|
856
|
+
|
|
857
|
+
- name: Cost Estimate
|
|
858
|
+
uses: codecollab-co/infra-cost@v1.4.0
|
|
859
|
+
with:
|
|
860
|
+
command: terraform
|
|
861
|
+
additional-args: '--plan tfplan --threshold 20'
|
|
862
|
+
env:
|
|
863
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
764
864
|
```
|
|
765
865
|
|
|
766
866
|
### Daily Cost Report to Slack
|
|
@@ -1063,6 +1163,24 @@ src/
|
|
|
1063
1163
|
- **Note:** VS Code extension will be a separate repository
|
|
1064
1164
|
- Integration points with CLI for cost data
|
|
1065
1165
|
|
|
1166
|
+
### Q2 2026 (v1.4.0 - Phase 3: CI/CD & Shift-Left)
|
|
1167
|
+
|
|
1168
|
+
**Priority: CI/CD Integration & Shift-Left Cost Management**
|
|
1169
|
+
- ✅ **GitHub Actions Integration** (Issue #46)
|
|
1170
|
+
- Native GitHub Action for cost analysis in PRs
|
|
1171
|
+
- Cost gates with fail-on-increase and threshold checks
|
|
1172
|
+
- Enhanced PR comments with cost breakdown
|
|
1173
|
+
- Multiple output variables for downstream jobs
|
|
1174
|
+
- Slack notification support
|
|
1175
|
+
- ✅ **Terraform Cost Preview** (Issue #47)
|
|
1176
|
+
- Parse terraform plan files (binary and JSON)
|
|
1177
|
+
- Estimate costs before deployment
|
|
1178
|
+
- Show create/modify/destroy breakdown
|
|
1179
|
+
- Cost threshold gates for CI/CD
|
|
1180
|
+
- Support for EC2, RDS, EBS, Load Balancers, and more
|
|
1181
|
+
- Monthly and hourly cost estimates
|
|
1182
|
+
- Shift-left cost management
|
|
1183
|
+
|
|
1066
1184
|
### Q3 2026 (Planned)
|
|
1067
1185
|
|
|
1068
1186
|
**Priority: Enterprise Features**
|
package/dist/cli/index.js
CHANGED
|
@@ -9432,7 +9432,7 @@ var import_commander = require("commander");
|
|
|
9432
9432
|
// package.json
|
|
9433
9433
|
var package_default = {
|
|
9434
9434
|
name: "infra-cost",
|
|
9435
|
-
version: "1.
|
|
9435
|
+
version: "1.4.0",
|
|
9436
9436
|
description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
|
|
9437
9437
|
keywords: [
|
|
9438
9438
|
"aws",
|
|
@@ -11735,26 +11735,329 @@ function getPeriodLabel(period) {
|
|
|
11735
11735
|
}
|
|
11736
11736
|
__name(getPeriodLabel, "getPeriodLabel");
|
|
11737
11737
|
|
|
11738
|
+
// src/cli/commands/terraform/index.ts
|
|
11739
|
+
var import_fs4 = require("fs");
|
|
11740
|
+
var import_child_process2 = require("child_process");
|
|
11741
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
11742
|
+
var AWS_PRICING = {
|
|
11743
|
+
// EC2 instances (us-east-1 on-demand hourly rates)
|
|
11744
|
+
ec2: {
|
|
11745
|
+
"t2.micro": 0.0116,
|
|
11746
|
+
"t2.small": 0.0232,
|
|
11747
|
+
"t2.medium": 0.0464,
|
|
11748
|
+
"t2.large": 0.0928,
|
|
11749
|
+
"t2.xlarge": 0.1856,
|
|
11750
|
+
"t3.micro": 0.0104,
|
|
11751
|
+
"t3.small": 0.0208,
|
|
11752
|
+
"t3.medium": 0.0416,
|
|
11753
|
+
"t3.large": 0.0832,
|
|
11754
|
+
"t3.xlarge": 0.1664,
|
|
11755
|
+
"t3.2xlarge": 0.3328,
|
|
11756
|
+
"m5.large": 0.096,
|
|
11757
|
+
"m5.xlarge": 0.192,
|
|
11758
|
+
"m5.2xlarge": 0.384,
|
|
11759
|
+
"c5.large": 0.085,
|
|
11760
|
+
"c5.xlarge": 0.17,
|
|
11761
|
+
"r5.large": 0.126,
|
|
11762
|
+
"r5.xlarge": 0.252
|
|
11763
|
+
},
|
|
11764
|
+
// RDS instances (db.r5 family)
|
|
11765
|
+
rds: {
|
|
11766
|
+
"db.t3.micro": 0.017,
|
|
11767
|
+
"db.t3.small": 0.034,
|
|
11768
|
+
"db.t3.medium": 0.068,
|
|
11769
|
+
"db.t3.large": 0.136,
|
|
11770
|
+
"db.r5.large": 0.24,
|
|
11771
|
+
"db.r5.xlarge": 0.48,
|
|
11772
|
+
"db.r5.2xlarge": 0.96,
|
|
11773
|
+
"db.m5.large": 0.196,
|
|
11774
|
+
"db.m5.xlarge": 0.392
|
|
11775
|
+
},
|
|
11776
|
+
// EBS volumes (per GB-month)
|
|
11777
|
+
ebs: {
|
|
11778
|
+
gp2: 0.1,
|
|
11779
|
+
gp3: 0.08,
|
|
11780
|
+
io1: 0.125,
|
|
11781
|
+
io2: 0.125,
|
|
11782
|
+
st1: 0.045,
|
|
11783
|
+
sc1: 0.015
|
|
11784
|
+
},
|
|
11785
|
+
// Load Balancers (per hour)
|
|
11786
|
+
alb: 0.0225,
|
|
11787
|
+
nlb: 0.0225,
|
|
11788
|
+
clb: 0.025,
|
|
11789
|
+
// NAT Gateway (per hour + data transfer)
|
|
11790
|
+
natgateway: 0.045,
|
|
11791
|
+
// ElastiCache (per hour)
|
|
11792
|
+
elasticache: {
|
|
11793
|
+
"cache.t3.micro": 0.017,
|
|
11794
|
+
"cache.t3.small": 0.034,
|
|
11795
|
+
"cache.m5.large": 0.161
|
|
11796
|
+
}
|
|
11797
|
+
};
|
|
11798
|
+
function parseTerraformPlan(planPath) {
|
|
11799
|
+
try {
|
|
11800
|
+
let planJson;
|
|
11801
|
+
if (planPath.endsWith(".json")) {
|
|
11802
|
+
planJson = (0, import_fs4.readFileSync)(planPath, "utf-8");
|
|
11803
|
+
} else {
|
|
11804
|
+
planJson = (0, import_child_process2.execSync)(`terraform show -json "${planPath}"`, {
|
|
11805
|
+
encoding: "utf-8"
|
|
11806
|
+
});
|
|
11807
|
+
}
|
|
11808
|
+
return JSON.parse(planJson);
|
|
11809
|
+
} catch (error) {
|
|
11810
|
+
throw new Error(`Failed to parse Terraform plan: ${error.message}`);
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
__name(parseTerraformPlan, "parseTerraformPlan");
|
|
11814
|
+
function estimateResourceCost2(change) {
|
|
11815
|
+
const { type, name, address, change: changeDetails } = change;
|
|
11816
|
+
const action = changeDetails.actions[0];
|
|
11817
|
+
const actionMap = {
|
|
11818
|
+
create: "create",
|
|
11819
|
+
update: "modify",
|
|
11820
|
+
delete: "destroy"
|
|
11821
|
+
};
|
|
11822
|
+
const mappedAction = actionMap[action] || "create";
|
|
11823
|
+
const config = changeDetails.after || changeDetails.before || {};
|
|
11824
|
+
let monthlyCost = 0;
|
|
11825
|
+
let hourlyCost = 0;
|
|
11826
|
+
let details = "";
|
|
11827
|
+
if (type === "aws_instance") {
|
|
11828
|
+
const instanceType = config.instance_type || "t3.micro";
|
|
11829
|
+
hourlyCost = AWS_PRICING.ec2[instanceType] || 0.1;
|
|
11830
|
+
monthlyCost = hourlyCost * 730;
|
|
11831
|
+
details = instanceType;
|
|
11832
|
+
} else if (type === "aws_db_instance") {
|
|
11833
|
+
const instanceClass = config.instance_class || "db.t3.micro";
|
|
11834
|
+
hourlyCost = AWS_PRICING.rds[instanceClass] || 0.1;
|
|
11835
|
+
monthlyCost = hourlyCost * 730;
|
|
11836
|
+
const allocatedStorage = config.allocated_storage || 20;
|
|
11837
|
+
const storageType = config.storage_type || "gp2";
|
|
11838
|
+
const storageCostPerGB = AWS_PRICING.ebs[storageType] || 0.1;
|
|
11839
|
+
monthlyCost += allocatedStorage * storageCostPerGB;
|
|
11840
|
+
details = `${instanceClass}, ${allocatedStorage}GB ${storageType}`;
|
|
11841
|
+
} else if (type === "aws_ebs_volume") {
|
|
11842
|
+
const size = config.size || 8;
|
|
11843
|
+
const volumeType = config.type || "gp2";
|
|
11844
|
+
const costPerGB = AWS_PRICING.ebs[volumeType] || 0.1;
|
|
11845
|
+
monthlyCost = size * costPerGB;
|
|
11846
|
+
hourlyCost = monthlyCost / 730;
|
|
11847
|
+
details = `${size}GB ${volumeType}`;
|
|
11848
|
+
} else if (type === "aws_lb" || type === "aws_alb") {
|
|
11849
|
+
const lbType = config.load_balancer_type || "application";
|
|
11850
|
+
hourlyCost = lbType === "network" ? AWS_PRICING.nlb : AWS_PRICING.alb;
|
|
11851
|
+
monthlyCost = hourlyCost * 730;
|
|
11852
|
+
details = `${lbType} load balancer`;
|
|
11853
|
+
} else if (type === "aws_elb") {
|
|
11854
|
+
hourlyCost = AWS_PRICING.clb;
|
|
11855
|
+
monthlyCost = hourlyCost * 730;
|
|
11856
|
+
details = "classic load balancer";
|
|
11857
|
+
} else if (type === "aws_nat_gateway") {
|
|
11858
|
+
hourlyCost = AWS_PRICING.natgateway;
|
|
11859
|
+
monthlyCost = hourlyCost * 730;
|
|
11860
|
+
details = "NAT gateway (base cost)";
|
|
11861
|
+
} else if (type === "aws_elasticache_cluster") {
|
|
11862
|
+
const nodeType = config.node_type || "cache.t3.micro";
|
|
11863
|
+
const numNodes = config.num_cache_nodes || 1;
|
|
11864
|
+
hourlyCost = (AWS_PRICING.elasticache[nodeType] || 0.017) * numNodes;
|
|
11865
|
+
monthlyCost = hourlyCost * 730;
|
|
11866
|
+
details = `${numNodes}x ${nodeType}`;
|
|
11867
|
+
} else if (type === "aws_s3_bucket") {
|
|
11868
|
+
monthlyCost = 5;
|
|
11869
|
+
hourlyCost = monthlyCost / 730;
|
|
11870
|
+
details = "estimated storage cost";
|
|
11871
|
+
} else if (type === "aws_lambda_function") {
|
|
11872
|
+
monthlyCost = 1;
|
|
11873
|
+
hourlyCost = monthlyCost / 730;
|
|
11874
|
+
details = "estimated execution cost";
|
|
11875
|
+
} else {
|
|
11876
|
+
return null;
|
|
11877
|
+
}
|
|
11878
|
+
if (mappedAction === "destroy") {
|
|
11879
|
+
monthlyCost = -monthlyCost;
|
|
11880
|
+
hourlyCost = -hourlyCost;
|
|
11881
|
+
}
|
|
11882
|
+
return {
|
|
11883
|
+
resource: address,
|
|
11884
|
+
resourceType: type,
|
|
11885
|
+
action: mappedAction,
|
|
11886
|
+
monthlyCost,
|
|
11887
|
+
hourlyCost,
|
|
11888
|
+
details
|
|
11889
|
+
};
|
|
11890
|
+
}
|
|
11891
|
+
__name(estimateResourceCost2, "estimateResourceCost");
|
|
11892
|
+
function analyzePlan(plan) {
|
|
11893
|
+
const creates = [];
|
|
11894
|
+
const modifies = [];
|
|
11895
|
+
const destroys = [];
|
|
11896
|
+
const changes = plan.resource_changes || [];
|
|
11897
|
+
changes.forEach((change) => {
|
|
11898
|
+
const estimate = estimateResourceCost2(change);
|
|
11899
|
+
if (!estimate)
|
|
11900
|
+
return;
|
|
11901
|
+
if (estimate.action === "create") {
|
|
11902
|
+
creates.push(estimate);
|
|
11903
|
+
} else if (estimate.action === "modify") {
|
|
11904
|
+
modifies.push(estimate);
|
|
11905
|
+
} else if (estimate.action === "destroy") {
|
|
11906
|
+
destroys.push(estimate);
|
|
11907
|
+
}
|
|
11908
|
+
});
|
|
11909
|
+
const createCost = creates.reduce((sum, e) => sum + e.monthlyCost, 0);
|
|
11910
|
+
const destroyCost = Math.abs(
|
|
11911
|
+
destroys.reduce((sum, e) => sum + e.monthlyCost, 0)
|
|
11912
|
+
);
|
|
11913
|
+
const modifyCost = modifies.reduce((sum, e) => sum + e.monthlyCost, 0);
|
|
11914
|
+
const currentMonthlyCost = destroyCost;
|
|
11915
|
+
const newMonthlyCost = createCost + modifyCost;
|
|
11916
|
+
const difference = newMonthlyCost - currentMonthlyCost;
|
|
11917
|
+
const percentChange = currentMonthlyCost > 0 ? difference / currentMonthlyCost * 100 : difference > 0 ? 100 : 0;
|
|
11918
|
+
return {
|
|
11919
|
+
creates,
|
|
11920
|
+
modifies,
|
|
11921
|
+
destroys,
|
|
11922
|
+
currentMonthlyCost,
|
|
11923
|
+
newMonthlyCost,
|
|
11924
|
+
difference,
|
|
11925
|
+
percentChange
|
|
11926
|
+
};
|
|
11927
|
+
}
|
|
11928
|
+
__name(analyzePlan, "analyzePlan");
|
|
11929
|
+
function formatCostEstimate(summary, options) {
|
|
11930
|
+
const { output } = options;
|
|
11931
|
+
if (output === "json") {
|
|
11932
|
+
return JSON.stringify(summary, null, 2);
|
|
11933
|
+
}
|
|
11934
|
+
let result = "";
|
|
11935
|
+
result += import_chalk14.default.bold.blue(
|
|
11936
|
+
"\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E\n"
|
|
11937
|
+
);
|
|
11938
|
+
result += import_chalk14.default.bold.blue(
|
|
11939
|
+
"\u2502" + import_chalk14.default.white(" Terraform Cost Estimate ") + "\u2502\n"
|
|
11940
|
+
);
|
|
11941
|
+
result += import_chalk14.default.bold.blue(
|
|
11942
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11943
|
+
);
|
|
11944
|
+
if (summary.creates.length > 0) {
|
|
11945
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.green("Resources to CREATE:") + " ".repeat(38) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11946
|
+
result += import_chalk14.default.bold.blue(
|
|
11947
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11948
|
+
);
|
|
11949
|
+
summary.creates.forEach((est) => {
|
|
11950
|
+
const resourceLine = `+ ${est.resource} (${est.details})`;
|
|
11951
|
+
const costLine = ` \u2514\u2500\u2500 Monthly: $${est.monthlyCost.toFixed(2)} | Hourly: $${est.hourlyCost.toFixed(4)}`;
|
|
11952
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.green(resourceLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11953
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.gray(costLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11954
|
+
});
|
|
11955
|
+
result += import_chalk14.default.bold.blue(
|
|
11956
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11957
|
+
);
|
|
11958
|
+
}
|
|
11959
|
+
if (summary.modifies.length > 0) {
|
|
11960
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.yellow("Resources to MODIFY:") + " ".repeat(38) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11961
|
+
result += import_chalk14.default.bold.blue(
|
|
11962
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11963
|
+
);
|
|
11964
|
+
summary.modifies.forEach((est) => {
|
|
11965
|
+
const resourceLine = `~ ${est.resource}`;
|
|
11966
|
+
const costLine = ` \u2514\u2500\u2500 ${est.details}: ${est.monthlyCost >= 0 ? "+" : ""}$${est.monthlyCost.toFixed(2)}/month`;
|
|
11967
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.yellow(resourceLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11968
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.gray(costLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11969
|
+
});
|
|
11970
|
+
result += import_chalk14.default.bold.blue(
|
|
11971
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11972
|
+
);
|
|
11973
|
+
}
|
|
11974
|
+
if (summary.destroys.length > 0) {
|
|
11975
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.red("Resources to DESTROY:") + " ".repeat(37) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11976
|
+
result += import_chalk14.default.bold.blue(
|
|
11977
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11978
|
+
);
|
|
11979
|
+
summary.destroys.forEach((est) => {
|
|
11980
|
+
const resourceLine = `- ${est.resource}`;
|
|
11981
|
+
const costLine = ` \u2514\u2500\u2500 Savings: $${Math.abs(est.monthlyCost).toFixed(2)}/month`;
|
|
11982
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.red(resourceLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11983
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.gray(costLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11984
|
+
});
|
|
11985
|
+
result += import_chalk14.default.bold.blue(
|
|
11986
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11987
|
+
);
|
|
11988
|
+
}
|
|
11989
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.bold.white("SUMMARY") + " ".repeat(52) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11990
|
+
result += import_chalk14.default.bold.blue(
|
|
11991
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11992
|
+
);
|
|
11993
|
+
const currentLine = `Current Monthly Cost: $${summary.currentMonthlyCost.toFixed(2)}`;
|
|
11994
|
+
const newLine = `Estimated New Cost: $${summary.newMonthlyCost.toFixed(2)}`;
|
|
11995
|
+
const diffSymbol = summary.difference >= 0 ? "+" : "";
|
|
11996
|
+
const diffLine = `Difference: ${diffSymbol}$${summary.difference.toFixed(2)}/month (${diffSymbol}${summary.percentChange.toFixed(1)}%)`;
|
|
11997
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + currentLine.padEnd(59) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11998
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + newLine.padEnd(59) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11999
|
+
const diffColor = summary.difference > 0 ? import_chalk14.default.red : import_chalk14.default.green;
|
|
12000
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + diffColor(diffLine).padEnd(69) + import_chalk14.default.bold.blue("\u2502\n");
|
|
12001
|
+
const threshold = parseFloat(options.threshold || "20");
|
|
12002
|
+
if (Math.abs(summary.percentChange) > threshold) {
|
|
12003
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + " ".repeat(59) + import_chalk14.default.bold.blue("\u2502\n");
|
|
12004
|
+
const warning = `\u26A0\uFE0F Cost ${summary.difference > 0 ? "increase" : "decrease"} exceeds ${threshold}% threshold!`;
|
|
12005
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.yellow.bold(warning.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
12006
|
+
}
|
|
12007
|
+
result += import_chalk14.default.bold.blue(
|
|
12008
|
+
"\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F\n"
|
|
12009
|
+
);
|
|
12010
|
+
return result;
|
|
12011
|
+
}
|
|
12012
|
+
__name(formatCostEstimate, "formatCostEstimate");
|
|
12013
|
+
async function handleTerraform(options) {
|
|
12014
|
+
const { plan, threshold, output } = options;
|
|
12015
|
+
if (!plan) {
|
|
12016
|
+
console.error(import_chalk14.default.red("Error: --plan argument is required"));
|
|
12017
|
+
console.log("Usage: infra-cost terraform --plan <path-to-terraform-plan>");
|
|
12018
|
+
process.exit(1);
|
|
12019
|
+
}
|
|
12020
|
+
try {
|
|
12021
|
+
console.log(import_chalk14.default.blue("\u{1F4CA} Analyzing Terraform plan...\n"));
|
|
12022
|
+
const terraformPlan = parseTerraformPlan(plan);
|
|
12023
|
+
const summary = analyzePlan(terraformPlan);
|
|
12024
|
+
const formatted = formatCostEstimate(summary, options);
|
|
12025
|
+
console.log(formatted);
|
|
12026
|
+
const thresholdValue = parseFloat(threshold || "0");
|
|
12027
|
+
if (thresholdValue > 0 && Math.abs(summary.percentChange) > thresholdValue) {
|
|
12028
|
+
process.exit(1);
|
|
12029
|
+
}
|
|
12030
|
+
} catch (error) {
|
|
12031
|
+
console.error(import_chalk14.default.red(`Error: ${error.message}`));
|
|
12032
|
+
process.exit(1);
|
|
12033
|
+
}
|
|
12034
|
+
}
|
|
12035
|
+
__name(handleTerraform, "handleTerraform");
|
|
12036
|
+
function registerTerraformCommand(program) {
|
|
12037
|
+
program.command("terraform").description("Estimate costs from Terraform plan (shift-left cost management)").option(
|
|
12038
|
+
"--plan <path>",
|
|
12039
|
+
"Path to terraform plan file (binary or JSON)",
|
|
12040
|
+
""
|
|
12041
|
+
).option(
|
|
12042
|
+
"--threshold <percent>",
|
|
12043
|
+
"Fail if cost change exceeds threshold percentage",
|
|
12044
|
+
"0"
|
|
12045
|
+
).action(handleTerraform);
|
|
12046
|
+
}
|
|
12047
|
+
__name(registerTerraformCommand, "registerTerraformCommand");
|
|
12048
|
+
|
|
11738
12049
|
// src/cli/middleware/auth.ts
|
|
11739
|
-
init_logging();
|
|
11740
12050
|
async function authMiddleware(thisCommand, actionCommand) {
|
|
11741
|
-
const logger = getGlobalLogger27();
|
|
11742
|
-
const opts = thisCommand.opts();
|
|
11743
12051
|
const isConfigCommand = actionCommand.name() === "config" || actionCommand.parent?.name() === "config";
|
|
11744
12052
|
if (isConfigCommand) {
|
|
11745
12053
|
return;
|
|
11746
12054
|
}
|
|
11747
|
-
logger.debug("Running authentication middleware", { provider: opts.provider });
|
|
11748
|
-
logger.debug("Authentication check passed");
|
|
11749
12055
|
}
|
|
11750
12056
|
__name(authMiddleware, "authMiddleware");
|
|
11751
12057
|
|
|
11752
12058
|
// src/cli/middleware/validation.ts
|
|
11753
|
-
init_logging();
|
|
11754
12059
|
async function validationMiddleware(thisCommand, actionCommand) {
|
|
11755
|
-
const logger = getGlobalLogger28();
|
|
11756
12060
|
const opts = thisCommand.opts();
|
|
11757
|
-
logger.debug("Running validation middleware");
|
|
11758
12061
|
const validProviders = ["aws", "gcp", "azure", "alibaba", "oracle"];
|
|
11759
12062
|
if (opts.provider && !validProviders.includes(opts.provider)) {
|
|
11760
12063
|
throw new Error(`Invalid provider: ${opts.provider}. Must be one of: ${validProviders.join(", ")}`);
|
|
@@ -11767,34 +12070,27 @@ async function validationMiddleware(thisCommand, actionCommand) {
|
|
|
11767
12070
|
if (opts.logLevel && !validLogLevels.includes(opts.logLevel)) {
|
|
11768
12071
|
throw new Error(`Invalid log level: ${opts.logLevel}. Must be one of: ${validLogLevels.join(", ")}`);
|
|
11769
12072
|
}
|
|
11770
|
-
logger.debug("Validation check passed");
|
|
11771
12073
|
}
|
|
11772
12074
|
__name(validationMiddleware, "validationMiddleware");
|
|
11773
12075
|
|
|
11774
12076
|
// src/cli/middleware/error-handler.ts
|
|
11775
|
-
var
|
|
11776
|
-
init_logging();
|
|
12077
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
11777
12078
|
function errorHandler(error) {
|
|
11778
12079
|
const message = error?.message ?? String(error);
|
|
11779
12080
|
const stack = error?.stack;
|
|
11780
12081
|
if (message === "(outputHelp)" || message === "(outputVersion)") {
|
|
11781
12082
|
return;
|
|
11782
12083
|
}
|
|
11783
|
-
try {
|
|
11784
|
-
const logger = getGlobalLogger29();
|
|
11785
|
-
logger.error("Command failed", { error: message, stack });
|
|
11786
|
-
} catch (loggerError) {
|
|
11787
|
-
}
|
|
11788
12084
|
console.error("");
|
|
11789
|
-
console.error(
|
|
12085
|
+
console.error(import_chalk15.default.red("\u2716"), import_chalk15.default.bold("Error:"), message);
|
|
11790
12086
|
if (process.env.DEBUG || process.env.VERBOSE) {
|
|
11791
12087
|
console.error("");
|
|
11792
12088
|
if (stack) {
|
|
11793
|
-
console.error(
|
|
12089
|
+
console.error(import_chalk15.default.gray(stack));
|
|
11794
12090
|
}
|
|
11795
12091
|
} else {
|
|
11796
12092
|
console.error("");
|
|
11797
|
-
console.error(
|
|
12093
|
+
console.error(import_chalk15.default.gray("Run with --verbose for detailed error information"));
|
|
11798
12094
|
}
|
|
11799
12095
|
console.error("");
|
|
11800
12096
|
}
|
|
@@ -11818,6 +12114,7 @@ function createCLI() {
|
|
|
11818
12114
|
registerConfigCommands(program);
|
|
11819
12115
|
registerDashboardCommands(program);
|
|
11820
12116
|
registerGitCommands(program);
|
|
12117
|
+
registerTerraformCommand(program);
|
|
11821
12118
|
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
11822
12119
|
const opts = thisCommand.opts();
|
|
11823
12120
|
const logLevel = opts.verbose ? "debug" : opts.quiet ? "error" : opts.logLevel;
|
package/dist/index.js
CHANGED
|
@@ -9426,7 +9426,7 @@ var import_commander = require("commander");
|
|
|
9426
9426
|
// package.json
|
|
9427
9427
|
var package_default = {
|
|
9428
9428
|
name: "infra-cost",
|
|
9429
|
-
version: "1.
|
|
9429
|
+
version: "1.4.0",
|
|
9430
9430
|
description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
|
|
9431
9431
|
keywords: [
|
|
9432
9432
|
"aws",
|
|
@@ -11729,26 +11729,329 @@ function getPeriodLabel(period) {
|
|
|
11729
11729
|
}
|
|
11730
11730
|
__name(getPeriodLabel, "getPeriodLabel");
|
|
11731
11731
|
|
|
11732
|
+
// src/cli/commands/terraform/index.ts
|
|
11733
|
+
var import_fs4 = require("fs");
|
|
11734
|
+
var import_child_process2 = require("child_process");
|
|
11735
|
+
var import_chalk14 = __toESM(require("chalk"));
|
|
11736
|
+
var AWS_PRICING = {
|
|
11737
|
+
// EC2 instances (us-east-1 on-demand hourly rates)
|
|
11738
|
+
ec2: {
|
|
11739
|
+
"t2.micro": 0.0116,
|
|
11740
|
+
"t2.small": 0.0232,
|
|
11741
|
+
"t2.medium": 0.0464,
|
|
11742
|
+
"t2.large": 0.0928,
|
|
11743
|
+
"t2.xlarge": 0.1856,
|
|
11744
|
+
"t3.micro": 0.0104,
|
|
11745
|
+
"t3.small": 0.0208,
|
|
11746
|
+
"t3.medium": 0.0416,
|
|
11747
|
+
"t3.large": 0.0832,
|
|
11748
|
+
"t3.xlarge": 0.1664,
|
|
11749
|
+
"t3.2xlarge": 0.3328,
|
|
11750
|
+
"m5.large": 0.096,
|
|
11751
|
+
"m5.xlarge": 0.192,
|
|
11752
|
+
"m5.2xlarge": 0.384,
|
|
11753
|
+
"c5.large": 0.085,
|
|
11754
|
+
"c5.xlarge": 0.17,
|
|
11755
|
+
"r5.large": 0.126,
|
|
11756
|
+
"r5.xlarge": 0.252
|
|
11757
|
+
},
|
|
11758
|
+
// RDS instances (db.r5 family)
|
|
11759
|
+
rds: {
|
|
11760
|
+
"db.t3.micro": 0.017,
|
|
11761
|
+
"db.t3.small": 0.034,
|
|
11762
|
+
"db.t3.medium": 0.068,
|
|
11763
|
+
"db.t3.large": 0.136,
|
|
11764
|
+
"db.r5.large": 0.24,
|
|
11765
|
+
"db.r5.xlarge": 0.48,
|
|
11766
|
+
"db.r5.2xlarge": 0.96,
|
|
11767
|
+
"db.m5.large": 0.196,
|
|
11768
|
+
"db.m5.xlarge": 0.392
|
|
11769
|
+
},
|
|
11770
|
+
// EBS volumes (per GB-month)
|
|
11771
|
+
ebs: {
|
|
11772
|
+
gp2: 0.1,
|
|
11773
|
+
gp3: 0.08,
|
|
11774
|
+
io1: 0.125,
|
|
11775
|
+
io2: 0.125,
|
|
11776
|
+
st1: 0.045,
|
|
11777
|
+
sc1: 0.015
|
|
11778
|
+
},
|
|
11779
|
+
// Load Balancers (per hour)
|
|
11780
|
+
alb: 0.0225,
|
|
11781
|
+
nlb: 0.0225,
|
|
11782
|
+
clb: 0.025,
|
|
11783
|
+
// NAT Gateway (per hour + data transfer)
|
|
11784
|
+
natgateway: 0.045,
|
|
11785
|
+
// ElastiCache (per hour)
|
|
11786
|
+
elasticache: {
|
|
11787
|
+
"cache.t3.micro": 0.017,
|
|
11788
|
+
"cache.t3.small": 0.034,
|
|
11789
|
+
"cache.m5.large": 0.161
|
|
11790
|
+
}
|
|
11791
|
+
};
|
|
11792
|
+
function parseTerraformPlan(planPath) {
|
|
11793
|
+
try {
|
|
11794
|
+
let planJson;
|
|
11795
|
+
if (planPath.endsWith(".json")) {
|
|
11796
|
+
planJson = (0, import_fs4.readFileSync)(planPath, "utf-8");
|
|
11797
|
+
} else {
|
|
11798
|
+
planJson = (0, import_child_process2.execSync)(`terraform show -json "${planPath}"`, {
|
|
11799
|
+
encoding: "utf-8"
|
|
11800
|
+
});
|
|
11801
|
+
}
|
|
11802
|
+
return JSON.parse(planJson);
|
|
11803
|
+
} catch (error) {
|
|
11804
|
+
throw new Error(`Failed to parse Terraform plan: ${error.message}`);
|
|
11805
|
+
}
|
|
11806
|
+
}
|
|
11807
|
+
__name(parseTerraformPlan, "parseTerraformPlan");
|
|
11808
|
+
function estimateResourceCost2(change) {
|
|
11809
|
+
const { type, name, address, change: changeDetails } = change;
|
|
11810
|
+
const action = changeDetails.actions[0];
|
|
11811
|
+
const actionMap = {
|
|
11812
|
+
create: "create",
|
|
11813
|
+
update: "modify",
|
|
11814
|
+
delete: "destroy"
|
|
11815
|
+
};
|
|
11816
|
+
const mappedAction = actionMap[action] || "create";
|
|
11817
|
+
const config = changeDetails.after || changeDetails.before || {};
|
|
11818
|
+
let monthlyCost = 0;
|
|
11819
|
+
let hourlyCost = 0;
|
|
11820
|
+
let details = "";
|
|
11821
|
+
if (type === "aws_instance") {
|
|
11822
|
+
const instanceType = config.instance_type || "t3.micro";
|
|
11823
|
+
hourlyCost = AWS_PRICING.ec2[instanceType] || 0.1;
|
|
11824
|
+
monthlyCost = hourlyCost * 730;
|
|
11825
|
+
details = instanceType;
|
|
11826
|
+
} else if (type === "aws_db_instance") {
|
|
11827
|
+
const instanceClass = config.instance_class || "db.t3.micro";
|
|
11828
|
+
hourlyCost = AWS_PRICING.rds[instanceClass] || 0.1;
|
|
11829
|
+
monthlyCost = hourlyCost * 730;
|
|
11830
|
+
const allocatedStorage = config.allocated_storage || 20;
|
|
11831
|
+
const storageType = config.storage_type || "gp2";
|
|
11832
|
+
const storageCostPerGB = AWS_PRICING.ebs[storageType] || 0.1;
|
|
11833
|
+
monthlyCost += allocatedStorage * storageCostPerGB;
|
|
11834
|
+
details = `${instanceClass}, ${allocatedStorage}GB ${storageType}`;
|
|
11835
|
+
} else if (type === "aws_ebs_volume") {
|
|
11836
|
+
const size = config.size || 8;
|
|
11837
|
+
const volumeType = config.type || "gp2";
|
|
11838
|
+
const costPerGB = AWS_PRICING.ebs[volumeType] || 0.1;
|
|
11839
|
+
monthlyCost = size * costPerGB;
|
|
11840
|
+
hourlyCost = monthlyCost / 730;
|
|
11841
|
+
details = `${size}GB ${volumeType}`;
|
|
11842
|
+
} else if (type === "aws_lb" || type === "aws_alb") {
|
|
11843
|
+
const lbType = config.load_balancer_type || "application";
|
|
11844
|
+
hourlyCost = lbType === "network" ? AWS_PRICING.nlb : AWS_PRICING.alb;
|
|
11845
|
+
monthlyCost = hourlyCost * 730;
|
|
11846
|
+
details = `${lbType} load balancer`;
|
|
11847
|
+
} else if (type === "aws_elb") {
|
|
11848
|
+
hourlyCost = AWS_PRICING.clb;
|
|
11849
|
+
monthlyCost = hourlyCost * 730;
|
|
11850
|
+
details = "classic load balancer";
|
|
11851
|
+
} else if (type === "aws_nat_gateway") {
|
|
11852
|
+
hourlyCost = AWS_PRICING.natgateway;
|
|
11853
|
+
monthlyCost = hourlyCost * 730;
|
|
11854
|
+
details = "NAT gateway (base cost)";
|
|
11855
|
+
} else if (type === "aws_elasticache_cluster") {
|
|
11856
|
+
const nodeType = config.node_type || "cache.t3.micro";
|
|
11857
|
+
const numNodes = config.num_cache_nodes || 1;
|
|
11858
|
+
hourlyCost = (AWS_PRICING.elasticache[nodeType] || 0.017) * numNodes;
|
|
11859
|
+
monthlyCost = hourlyCost * 730;
|
|
11860
|
+
details = `${numNodes}x ${nodeType}`;
|
|
11861
|
+
} else if (type === "aws_s3_bucket") {
|
|
11862
|
+
monthlyCost = 5;
|
|
11863
|
+
hourlyCost = monthlyCost / 730;
|
|
11864
|
+
details = "estimated storage cost";
|
|
11865
|
+
} else if (type === "aws_lambda_function") {
|
|
11866
|
+
monthlyCost = 1;
|
|
11867
|
+
hourlyCost = monthlyCost / 730;
|
|
11868
|
+
details = "estimated execution cost";
|
|
11869
|
+
} else {
|
|
11870
|
+
return null;
|
|
11871
|
+
}
|
|
11872
|
+
if (mappedAction === "destroy") {
|
|
11873
|
+
monthlyCost = -monthlyCost;
|
|
11874
|
+
hourlyCost = -hourlyCost;
|
|
11875
|
+
}
|
|
11876
|
+
return {
|
|
11877
|
+
resource: address,
|
|
11878
|
+
resourceType: type,
|
|
11879
|
+
action: mappedAction,
|
|
11880
|
+
monthlyCost,
|
|
11881
|
+
hourlyCost,
|
|
11882
|
+
details
|
|
11883
|
+
};
|
|
11884
|
+
}
|
|
11885
|
+
__name(estimateResourceCost2, "estimateResourceCost");
|
|
11886
|
+
function analyzePlan(plan) {
|
|
11887
|
+
const creates = [];
|
|
11888
|
+
const modifies = [];
|
|
11889
|
+
const destroys = [];
|
|
11890
|
+
const changes = plan.resource_changes || [];
|
|
11891
|
+
changes.forEach((change) => {
|
|
11892
|
+
const estimate = estimateResourceCost2(change);
|
|
11893
|
+
if (!estimate)
|
|
11894
|
+
return;
|
|
11895
|
+
if (estimate.action === "create") {
|
|
11896
|
+
creates.push(estimate);
|
|
11897
|
+
} else if (estimate.action === "modify") {
|
|
11898
|
+
modifies.push(estimate);
|
|
11899
|
+
} else if (estimate.action === "destroy") {
|
|
11900
|
+
destroys.push(estimate);
|
|
11901
|
+
}
|
|
11902
|
+
});
|
|
11903
|
+
const createCost = creates.reduce((sum, e) => sum + e.monthlyCost, 0);
|
|
11904
|
+
const destroyCost = Math.abs(
|
|
11905
|
+
destroys.reduce((sum, e) => sum + e.monthlyCost, 0)
|
|
11906
|
+
);
|
|
11907
|
+
const modifyCost = modifies.reduce((sum, e) => sum + e.monthlyCost, 0);
|
|
11908
|
+
const currentMonthlyCost = destroyCost;
|
|
11909
|
+
const newMonthlyCost = createCost + modifyCost;
|
|
11910
|
+
const difference = newMonthlyCost - currentMonthlyCost;
|
|
11911
|
+
const percentChange = currentMonthlyCost > 0 ? difference / currentMonthlyCost * 100 : difference > 0 ? 100 : 0;
|
|
11912
|
+
return {
|
|
11913
|
+
creates,
|
|
11914
|
+
modifies,
|
|
11915
|
+
destroys,
|
|
11916
|
+
currentMonthlyCost,
|
|
11917
|
+
newMonthlyCost,
|
|
11918
|
+
difference,
|
|
11919
|
+
percentChange
|
|
11920
|
+
};
|
|
11921
|
+
}
|
|
11922
|
+
__name(analyzePlan, "analyzePlan");
|
|
11923
|
+
function formatCostEstimate(summary, options) {
|
|
11924
|
+
const { output } = options;
|
|
11925
|
+
if (output === "json") {
|
|
11926
|
+
return JSON.stringify(summary, null, 2);
|
|
11927
|
+
}
|
|
11928
|
+
let result = "";
|
|
11929
|
+
result += import_chalk14.default.bold.blue(
|
|
11930
|
+
"\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E\n"
|
|
11931
|
+
);
|
|
11932
|
+
result += import_chalk14.default.bold.blue(
|
|
11933
|
+
"\u2502" + import_chalk14.default.white(" Terraform Cost Estimate ") + "\u2502\n"
|
|
11934
|
+
);
|
|
11935
|
+
result += import_chalk14.default.bold.blue(
|
|
11936
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11937
|
+
);
|
|
11938
|
+
if (summary.creates.length > 0) {
|
|
11939
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.green("Resources to CREATE:") + " ".repeat(38) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11940
|
+
result += import_chalk14.default.bold.blue(
|
|
11941
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11942
|
+
);
|
|
11943
|
+
summary.creates.forEach((est) => {
|
|
11944
|
+
const resourceLine = `+ ${est.resource} (${est.details})`;
|
|
11945
|
+
const costLine = ` \u2514\u2500\u2500 Monthly: $${est.monthlyCost.toFixed(2)} | Hourly: $${est.hourlyCost.toFixed(4)}`;
|
|
11946
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.green(resourceLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11947
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.gray(costLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11948
|
+
});
|
|
11949
|
+
result += import_chalk14.default.bold.blue(
|
|
11950
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11951
|
+
);
|
|
11952
|
+
}
|
|
11953
|
+
if (summary.modifies.length > 0) {
|
|
11954
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.yellow("Resources to MODIFY:") + " ".repeat(38) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11955
|
+
result += import_chalk14.default.bold.blue(
|
|
11956
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11957
|
+
);
|
|
11958
|
+
summary.modifies.forEach((est) => {
|
|
11959
|
+
const resourceLine = `~ ${est.resource}`;
|
|
11960
|
+
const costLine = ` \u2514\u2500\u2500 ${est.details}: ${est.monthlyCost >= 0 ? "+" : ""}$${est.monthlyCost.toFixed(2)}/month`;
|
|
11961
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.yellow(resourceLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11962
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.gray(costLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11963
|
+
});
|
|
11964
|
+
result += import_chalk14.default.bold.blue(
|
|
11965
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11966
|
+
);
|
|
11967
|
+
}
|
|
11968
|
+
if (summary.destroys.length > 0) {
|
|
11969
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.red("Resources to DESTROY:") + " ".repeat(37) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11970
|
+
result += import_chalk14.default.bold.blue(
|
|
11971
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11972
|
+
);
|
|
11973
|
+
summary.destroys.forEach((est) => {
|
|
11974
|
+
const resourceLine = `- ${est.resource}`;
|
|
11975
|
+
const costLine = ` \u2514\u2500\u2500 Savings: $${Math.abs(est.monthlyCost).toFixed(2)}/month`;
|
|
11976
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.red(resourceLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11977
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.gray(costLine.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11978
|
+
});
|
|
11979
|
+
result += import_chalk14.default.bold.blue(
|
|
11980
|
+
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11981
|
+
);
|
|
11982
|
+
}
|
|
11983
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.bold.white("SUMMARY") + " ".repeat(52) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11984
|
+
result += import_chalk14.default.bold.blue(
|
|
11985
|
+
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11986
|
+
);
|
|
11987
|
+
const currentLine = `Current Monthly Cost: $${summary.currentMonthlyCost.toFixed(2)}`;
|
|
11988
|
+
const newLine = `Estimated New Cost: $${summary.newMonthlyCost.toFixed(2)}`;
|
|
11989
|
+
const diffSymbol = summary.difference >= 0 ? "+" : "";
|
|
11990
|
+
const diffLine = `Difference: ${diffSymbol}$${summary.difference.toFixed(2)}/month (${diffSymbol}${summary.percentChange.toFixed(1)}%)`;
|
|
11991
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + currentLine.padEnd(59) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11992
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + newLine.padEnd(59) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11993
|
+
const diffColor = summary.difference > 0 ? import_chalk14.default.red : import_chalk14.default.green;
|
|
11994
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + diffColor(diffLine).padEnd(69) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11995
|
+
const threshold = parseFloat(options.threshold || "20");
|
|
11996
|
+
if (Math.abs(summary.percentChange) > threshold) {
|
|
11997
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + " ".repeat(59) + import_chalk14.default.bold.blue("\u2502\n");
|
|
11998
|
+
const warning = `\u26A0\uFE0F Cost ${summary.difference > 0 ? "increase" : "decrease"} exceeds ${threshold}% threshold!`;
|
|
11999
|
+
result += import_chalk14.default.bold.blue("\u2502 ") + import_chalk14.default.yellow.bold(warning.padEnd(59)) + import_chalk14.default.bold.blue("\u2502\n");
|
|
12000
|
+
}
|
|
12001
|
+
result += import_chalk14.default.bold.blue(
|
|
12002
|
+
"\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F\n"
|
|
12003
|
+
);
|
|
12004
|
+
return result;
|
|
12005
|
+
}
|
|
12006
|
+
__name(formatCostEstimate, "formatCostEstimate");
|
|
12007
|
+
async function handleTerraform(options) {
|
|
12008
|
+
const { plan, threshold, output } = options;
|
|
12009
|
+
if (!plan) {
|
|
12010
|
+
console.error(import_chalk14.default.red("Error: --plan argument is required"));
|
|
12011
|
+
console.log("Usage: infra-cost terraform --plan <path-to-terraform-plan>");
|
|
12012
|
+
process.exit(1);
|
|
12013
|
+
}
|
|
12014
|
+
try {
|
|
12015
|
+
console.log(import_chalk14.default.blue("\u{1F4CA} Analyzing Terraform plan...\n"));
|
|
12016
|
+
const terraformPlan = parseTerraformPlan(plan);
|
|
12017
|
+
const summary = analyzePlan(terraformPlan);
|
|
12018
|
+
const formatted = formatCostEstimate(summary, options);
|
|
12019
|
+
console.log(formatted);
|
|
12020
|
+
const thresholdValue = parseFloat(threshold || "0");
|
|
12021
|
+
if (thresholdValue > 0 && Math.abs(summary.percentChange) > thresholdValue) {
|
|
12022
|
+
process.exit(1);
|
|
12023
|
+
}
|
|
12024
|
+
} catch (error) {
|
|
12025
|
+
console.error(import_chalk14.default.red(`Error: ${error.message}`));
|
|
12026
|
+
process.exit(1);
|
|
12027
|
+
}
|
|
12028
|
+
}
|
|
12029
|
+
__name(handleTerraform, "handleTerraform");
|
|
12030
|
+
function registerTerraformCommand(program) {
|
|
12031
|
+
program.command("terraform").description("Estimate costs from Terraform plan (shift-left cost management)").option(
|
|
12032
|
+
"--plan <path>",
|
|
12033
|
+
"Path to terraform plan file (binary or JSON)",
|
|
12034
|
+
""
|
|
12035
|
+
).option(
|
|
12036
|
+
"--threshold <percent>",
|
|
12037
|
+
"Fail if cost change exceeds threshold percentage",
|
|
12038
|
+
"0"
|
|
12039
|
+
).action(handleTerraform);
|
|
12040
|
+
}
|
|
12041
|
+
__name(registerTerraformCommand, "registerTerraformCommand");
|
|
12042
|
+
|
|
11732
12043
|
// src/cli/middleware/auth.ts
|
|
11733
|
-
init_logging();
|
|
11734
12044
|
async function authMiddleware(thisCommand, actionCommand) {
|
|
11735
|
-
const logger = getGlobalLogger27();
|
|
11736
|
-
const opts = thisCommand.opts();
|
|
11737
12045
|
const isConfigCommand = actionCommand.name() === "config" || actionCommand.parent?.name() === "config";
|
|
11738
12046
|
if (isConfigCommand) {
|
|
11739
12047
|
return;
|
|
11740
12048
|
}
|
|
11741
|
-
logger.debug("Running authentication middleware", { provider: opts.provider });
|
|
11742
|
-
logger.debug("Authentication check passed");
|
|
11743
12049
|
}
|
|
11744
12050
|
__name(authMiddleware, "authMiddleware");
|
|
11745
12051
|
|
|
11746
12052
|
// src/cli/middleware/validation.ts
|
|
11747
|
-
init_logging();
|
|
11748
12053
|
async function validationMiddleware(thisCommand, actionCommand) {
|
|
11749
|
-
const logger = getGlobalLogger28();
|
|
11750
12054
|
const opts = thisCommand.opts();
|
|
11751
|
-
logger.debug("Running validation middleware");
|
|
11752
12055
|
const validProviders = ["aws", "gcp", "azure", "alibaba", "oracle"];
|
|
11753
12056
|
if (opts.provider && !validProviders.includes(opts.provider)) {
|
|
11754
12057
|
throw new Error(`Invalid provider: ${opts.provider}. Must be one of: ${validProviders.join(", ")}`);
|
|
@@ -11761,34 +12064,27 @@ async function validationMiddleware(thisCommand, actionCommand) {
|
|
|
11761
12064
|
if (opts.logLevel && !validLogLevels.includes(opts.logLevel)) {
|
|
11762
12065
|
throw new Error(`Invalid log level: ${opts.logLevel}. Must be one of: ${validLogLevels.join(", ")}`);
|
|
11763
12066
|
}
|
|
11764
|
-
logger.debug("Validation check passed");
|
|
11765
12067
|
}
|
|
11766
12068
|
__name(validationMiddleware, "validationMiddleware");
|
|
11767
12069
|
|
|
11768
12070
|
// src/cli/middleware/error-handler.ts
|
|
11769
|
-
var
|
|
11770
|
-
init_logging();
|
|
12071
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
11771
12072
|
function errorHandler(error) {
|
|
11772
12073
|
const message = error?.message ?? String(error);
|
|
11773
12074
|
const stack = error?.stack;
|
|
11774
12075
|
if (message === "(outputHelp)" || message === "(outputVersion)") {
|
|
11775
12076
|
return;
|
|
11776
12077
|
}
|
|
11777
|
-
try {
|
|
11778
|
-
const logger = getGlobalLogger29();
|
|
11779
|
-
logger.error("Command failed", { error: message, stack });
|
|
11780
|
-
} catch (loggerError) {
|
|
11781
|
-
}
|
|
11782
12078
|
console.error("");
|
|
11783
|
-
console.error(
|
|
12079
|
+
console.error(import_chalk15.default.red("\u2716"), import_chalk15.default.bold("Error:"), message);
|
|
11784
12080
|
if (process.env.DEBUG || process.env.VERBOSE) {
|
|
11785
12081
|
console.error("");
|
|
11786
12082
|
if (stack) {
|
|
11787
|
-
console.error(
|
|
12083
|
+
console.error(import_chalk15.default.gray(stack));
|
|
11788
12084
|
}
|
|
11789
12085
|
} else {
|
|
11790
12086
|
console.error("");
|
|
11791
|
-
console.error(
|
|
12087
|
+
console.error(import_chalk15.default.gray("Run with --verbose for detailed error information"));
|
|
11792
12088
|
}
|
|
11793
12089
|
console.error("");
|
|
11794
12090
|
}
|
|
@@ -11812,6 +12108,7 @@ function createCLI() {
|
|
|
11812
12108
|
registerConfigCommands(program);
|
|
11813
12109
|
registerDashboardCommands(program);
|
|
11814
12110
|
registerGitCommands(program);
|
|
12111
|
+
registerTerraformCommand(program);
|
|
11815
12112
|
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
11816
12113
|
const opts = thisCommand.opts();
|
|
11817
12114
|
const logLevel = opts.verbose ? "debug" : opts.quiet ? "error" : opts.logLevel;
|
package/package.json
CHANGED