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 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 on the [GitHub Marketplace](https://github.com/marketplace/actions/infra-cost-multi-cloud-finops-analysis), making it easy to integrate cost analysis into your CI/CD workflows.
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
- ### Basic Usage
803
+ ### Quick Cost Check
727
804
  ```yaml
728
805
  name: Cost Analysis
729
- on: [push, pull_request]
806
+ on: [pull_request]
730
807
 
731
808
  jobs:
732
- analyze:
809
+ cost-check:
733
810
  runs-on: ubuntu-latest
734
811
  steps:
735
- - uses: codecollab-co/infra-cost@v0.3.0
812
+ - uses: actions/checkout@v4
813
+ - uses: codecollab-co/infra-cost@v1.4.0
736
814
  with:
737
815
  provider: aws
738
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
739
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
740
- analysis-type: summary
816
+ command: now
817
+ comment-on-pr: true
818
+ env:
819
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
741
820
  ```
742
821
 
743
- ### PR Cost Check with Comments
822
+ ### Cost Gate - Fail on Increase
744
823
  ```yaml
745
- name: PR Cost Check
746
- on:
747
- pull_request:
748
- branches: [main]
824
+ name: Cost Gate
825
+ on: pull_request
749
826
 
750
827
  jobs:
751
- cost-check:
828
+ cost-gate:
752
829
  runs-on: ubuntu-latest
753
- permissions:
754
- pull-requests: write
755
830
  steps:
756
- - uses: codecollab-co/infra-cost@v0.3.0
831
+ - uses: actions/checkout@v4
832
+ - uses: codecollab-co/infra-cost@v1.4.0
757
833
  with:
758
834
  provider: aws
759
- aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
760
- aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
761
- analysis-type: delta
762
- delta-threshold: '10'
763
- comment-on-pr: 'true'
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.3.0",
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 import_chalk14 = __toESM(require("chalk"));
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(import_chalk14.default.red("\u2716"), import_chalk14.default.bold("Error:"), message);
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(import_chalk14.default.gray(stack));
12089
+ console.error(import_chalk15.default.gray(stack));
11794
12090
  }
11795
12091
  } else {
11796
12092
  console.error("");
11797
- console.error(import_chalk14.default.gray("Run with --verbose for detailed error information"));
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.3.0",
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 import_chalk14 = __toESM(require("chalk"));
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(import_chalk14.default.red("\u2716"), import_chalk14.default.bold("Error:"), message);
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(import_chalk14.default.gray(stack));
12083
+ console.error(import_chalk15.default.gray(stack));
11788
12084
  }
11789
12085
  } else {
11790
12086
  console.error("");
11791
- console.error(import_chalk14.default.gray("Run with --verbose for detailed error information"));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infra-cost",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
5
5
  "keywords": [
6
6
  "aws",