infra-cost 1.4.0 → 1.6.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/dist/cli/index.js CHANGED
@@ -8434,6 +8434,214 @@ var init_budgets = __esm({
8434
8434
  }
8435
8435
  });
8436
8436
 
8437
+ // src/integrations/pagerduty.ts
8438
+ async function sendPagerDutyAlert(alert, config) {
8439
+ const { routingKey, severity = "warning", dedupKey, component = "cost-monitoring" } = config;
8440
+ const payload = {
8441
+ routing_key: routingKey,
8442
+ event_action: "trigger",
8443
+ dedup_key: dedupKey || `infra-cost-${alert.accountId}-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
8444
+ payload: {
8445
+ summary: `Cloud Cost Spike: +$${alert.increaseAmount.toFixed(2)} (+${alert.increasePercent.toFixed(1)}%) in ${alert.accountName}`,
8446
+ severity,
8447
+ source: "infra-cost",
8448
+ component,
8449
+ group: alert.accountName,
8450
+ class: "cost-anomaly",
8451
+ custom_details: {
8452
+ account_id: alert.accountId,
8453
+ account_name: alert.accountName,
8454
+ today_cost: `$${alert.todayCost.toFixed(2)}`,
8455
+ yesterday_cost: `$${alert.yesterdayCost.toFixed(2)}`,
8456
+ increase_amount: `$${alert.increaseAmount.toFixed(2)}`,
8457
+ increase_percent: `${alert.increasePercent.toFixed(1)}%`,
8458
+ top_service: alert.topService,
8459
+ top_service_increase: `+$${alert.topServiceIncrease.toFixed(2)}`,
8460
+ threshold: alert.threshold ? `${alert.threshold}%` : "N/A"
8461
+ }
8462
+ },
8463
+ links: [
8464
+ {
8465
+ href: "https://console.aws.amazon.com/cost-management",
8466
+ text: "View in AWS Cost Explorer"
8467
+ }
8468
+ ]
8469
+ };
8470
+ try {
8471
+ const response = await (0, import_node_fetch.default)("https://events.pagerduty.com/v2/enqueue", {
8472
+ method: "POST",
8473
+ headers: {
8474
+ "Content-Type": "application/json"
8475
+ },
8476
+ body: JSON.stringify(payload)
8477
+ });
8478
+ if (!response.ok) {
8479
+ const errorText = await response.text();
8480
+ throw new Error(`PagerDuty API error: ${response.status} ${errorText}`);
8481
+ }
8482
+ const result = await response.json();
8483
+ return result;
8484
+ } catch (error) {
8485
+ throw new Error(`Failed to send PagerDuty alert: ${error.message}`);
8486
+ }
8487
+ }
8488
+ var import_node_fetch;
8489
+ var init_pagerduty = __esm({
8490
+ "src/integrations/pagerduty.ts"() {
8491
+ import_node_fetch = __toESM(require("node-fetch"));
8492
+ __name(sendPagerDutyAlert, "sendPagerDutyAlert");
8493
+ }
8494
+ });
8495
+
8496
+ // src/integrations/opsgenie.ts
8497
+ async function sendOpsGenieAlert(alert, config) {
8498
+ const { apiKey, priority = "P3", tags = ["cost-alert", "finops"], responders, alias } = config;
8499
+ const payload = {
8500
+ message: `Cloud Cost Spike: +$${alert.increaseAmount.toFixed(2)} (+${alert.increasePercent.toFixed(1)}%)`,
8501
+ alias: alias || `infra-cost-${alert.accountId}-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
8502
+ description: `Cost increase detected in ${alert.accountName} account.
8503
+
8504
+ Today: $${alert.todayCost.toFixed(2)}
8505
+ Yesterday: $${alert.yesterdayCost.toFixed(2)}
8506
+ Increase: +$${alert.increaseAmount.toFixed(2)} (+${alert.increasePercent.toFixed(1)}%)
8507
+
8508
+ Top service: ${alert.topService} (+$${alert.topServiceIncrease.toFixed(2)})`,
8509
+ priority,
8510
+ tags,
8511
+ details: {
8512
+ account_id: alert.accountId,
8513
+ account_name: alert.accountName,
8514
+ today_cost: alert.todayCost.toFixed(2),
8515
+ yesterday_cost: alert.yesterdayCost.toFixed(2),
8516
+ increase_amount: alert.increaseAmount.toFixed(2),
8517
+ increase_percent: alert.increasePercent.toFixed(1),
8518
+ top_service: alert.topService,
8519
+ top_service_increase: alert.topServiceIncrease.toFixed(2),
8520
+ threshold: alert.threshold?.toString() || "N/A"
8521
+ },
8522
+ responders
8523
+ };
8524
+ try {
8525
+ const response = await (0, import_node_fetch2.default)("https://api.opsgenie.com/v2/alerts", {
8526
+ method: "POST",
8527
+ headers: {
8528
+ "Content-Type": "application/json",
8529
+ Authorization: `GenieKey ${apiKey}`
8530
+ },
8531
+ body: JSON.stringify(payload)
8532
+ });
8533
+ if (!response.ok) {
8534
+ const errorText = await response.text();
8535
+ throw new Error(`OpsGenie API error: ${response.status} ${errorText}`);
8536
+ }
8537
+ const result = await response.json();
8538
+ return result;
8539
+ } catch (error) {
8540
+ throw new Error(`Failed to send OpsGenie alert: ${error.message}`);
8541
+ }
8542
+ }
8543
+ var import_node_fetch2;
8544
+ var init_opsgenie = __esm({
8545
+ "src/integrations/opsgenie.ts"() {
8546
+ import_node_fetch2 = __toESM(require("node-fetch"));
8547
+ __name(sendOpsGenieAlert, "sendOpsGenieAlert");
8548
+ }
8549
+ });
8550
+
8551
+ // src/cli/commands/monitor/alert.ts
8552
+ var alert_exports = {};
8553
+ __export(alert_exports, {
8554
+ handleOpsGenie: () => handleOpsGenie,
8555
+ handlePagerDuty: () => handlePagerDuty
8556
+ });
8557
+ async function handlePagerDuty(options) {
8558
+ const { pagerdutyKey, severity, threshold } = options;
8559
+ if (!pagerdutyKey) {
8560
+ console.error(import_chalk10.default.red("Error: --pagerduty-key is required"));
8561
+ console.log("\nExample:");
8562
+ console.log(import_chalk10.default.gray(" infra-cost monitor alert-pagerduty --pagerduty-key YOUR_ROUTING_KEY"));
8563
+ process.exit(1);
8564
+ }
8565
+ console.log(import_chalk10.default.blue("\u{1F6A8} Checking for cost anomalies...\n"));
8566
+ const alert = {
8567
+ accountId: "123456789012",
8568
+ accountName: "Production",
8569
+ todayCost: 1850.23,
8570
+ yesterdayCost: 1400,
8571
+ increaseAmount: 450.23,
8572
+ increasePercent: 32,
8573
+ topService: "EC2",
8574
+ topServiceIncrease: 320,
8575
+ threshold: threshold ? parseFloat(threshold) : void 0
8576
+ };
8577
+ if (threshold && alert.increasePercent < parseFloat(threshold)) {
8578
+ console.log(import_chalk10.default.green(`\u2705 No alert needed (${alert.increasePercent.toFixed(1)}% < ${threshold}% threshold)`));
8579
+ return;
8580
+ }
8581
+ try {
8582
+ await sendPagerDutyAlert(alert, {
8583
+ routingKey: pagerdutyKey,
8584
+ severity: severity || "warning",
8585
+ component: "aws-cost-monitoring"
8586
+ });
8587
+ console.log(import_chalk10.default.green("\u2705 Alert sent to PagerDuty"));
8588
+ console.log(import_chalk10.default.gray(` Severity: ${severity || "warning"}`));
8589
+ console.log(import_chalk10.default.gray(` Increase: +$${alert.increaseAmount.toFixed(2)} (+${alert.increasePercent.toFixed(1)}%)`));
8590
+ } catch (error) {
8591
+ console.error(import_chalk10.default.red("Error sending PagerDuty alert:", error.message));
8592
+ process.exit(1);
8593
+ }
8594
+ }
8595
+ async function handleOpsGenie(options) {
8596
+ const { opsgenieKey, priority, threshold } = options;
8597
+ if (!opsgenieKey) {
8598
+ console.error(import_chalk10.default.red("Error: --opsgenie-key is required"));
8599
+ console.log("\nExample:");
8600
+ console.log(import_chalk10.default.gray(" infra-cost monitor alert-opsgenie --opsgenie-key YOUR_API_KEY"));
8601
+ process.exit(1);
8602
+ }
8603
+ console.log(import_chalk10.default.blue("\u{1F6A8} Checking for cost anomalies...\n"));
8604
+ const alert = {
8605
+ accountId: "123456789012",
8606
+ accountName: "Production",
8607
+ todayCost: 1850.23,
8608
+ yesterdayCost: 1400,
8609
+ increaseAmount: 450.23,
8610
+ increasePercent: 32,
8611
+ topService: "EC2",
8612
+ topServiceIncrease: 320,
8613
+ threshold: threshold ? parseFloat(threshold) : void 0
8614
+ };
8615
+ if (threshold && alert.increasePercent < parseFloat(threshold)) {
8616
+ console.log(import_chalk10.default.green(`\u2705 No alert needed (${alert.increasePercent.toFixed(1)}% < ${threshold}% threshold)`));
8617
+ return;
8618
+ }
8619
+ try {
8620
+ await sendOpsGenieAlert(alert, {
8621
+ apiKey: opsgenieKey,
8622
+ priority: priority || "P3",
8623
+ tags: ["cost-alert", "finops", "aws"],
8624
+ responders: options.team ? [{ type: "team", name: options.team }] : void 0
8625
+ });
8626
+ console.log(import_chalk10.default.green("\u2705 Alert sent to OpsGenie"));
8627
+ console.log(import_chalk10.default.gray(` Priority: ${priority || "P3"}`));
8628
+ console.log(import_chalk10.default.gray(` Increase: +$${alert.increaseAmount.toFixed(2)} (+${alert.increasePercent.toFixed(1)}%)`));
8629
+ } catch (error) {
8630
+ console.error(import_chalk10.default.red("Error sending OpsGenie alert:", error.message));
8631
+ process.exit(1);
8632
+ }
8633
+ }
8634
+ var import_chalk10;
8635
+ var init_alert = __esm({
8636
+ "src/cli/commands/monitor/alert.ts"() {
8637
+ init_pagerduty();
8638
+ init_opsgenie();
8639
+ import_chalk10 = __toESM(require("chalk"));
8640
+ __name(handlePagerDuty, "handlePagerDuty");
8641
+ __name(handleOpsGenie, "handleOpsGenie");
8642
+ }
8643
+ });
8644
+
8437
8645
  // src/cli/commands/export/inventory.ts
8438
8646
  var inventory_exports = {};
8439
8647
  __export(inventory_exports, {
@@ -8483,6 +8691,398 @@ var init_reports = __esm({
8483
8691
  }
8484
8692
  });
8485
8693
 
8694
+ // src/integrations/email.ts
8695
+ function generateEmailHTML(data) {
8696
+ const {
8697
+ date,
8698
+ todayCost,
8699
+ mtdCost,
8700
+ budget: budget2,
8701
+ budgetPercent,
8702
+ projectedMonthEnd,
8703
+ topServices,
8704
+ alerts,
8705
+ accountName,
8706
+ provider
8707
+ } = data;
8708
+ return `
8709
+ <!DOCTYPE html>
8710
+ <html>
8711
+ <head>
8712
+ <meta charset="utf-8">
8713
+ <style>
8714
+ body {
8715
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
8716
+ line-height: 1.6;
8717
+ color: #333;
8718
+ max-width: 600px;
8719
+ margin: 0 auto;
8720
+ padding: 20px;
8721
+ }
8722
+ .header {
8723
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
8724
+ color: white;
8725
+ padding: 30px;
8726
+ border-radius: 8px 8px 0 0;
8727
+ text-align: center;
8728
+ }
8729
+ .header h1 {
8730
+ margin: 0;
8731
+ font-size: 24px;
8732
+ }
8733
+ .content {
8734
+ background: #f8f9fa;
8735
+ padding: 30px;
8736
+ border-radius: 0 0 8px 8px;
8737
+ }
8738
+ .section {
8739
+ background: white;
8740
+ padding: 20px;
8741
+ margin-bottom: 20px;
8742
+ border-radius: 8px;
8743
+ border-left: 4px solid #667eea;
8744
+ }
8745
+ .section-title {
8746
+ font-size: 18px;
8747
+ font-weight: bold;
8748
+ margin-bottom: 15px;
8749
+ color: #667eea;
8750
+ }
8751
+ .metric-row {
8752
+ display: flex;
8753
+ justify-content: space-between;
8754
+ padding: 10px 0;
8755
+ border-bottom: 1px solid #e9ecef;
8756
+ }
8757
+ .metric-row:last-child {
8758
+ border-bottom: none;
8759
+ }
8760
+ .metric-label {
8761
+ font-weight: 500;
8762
+ color: #6c757d;
8763
+ }
8764
+ .metric-value {
8765
+ font-weight: bold;
8766
+ color: #333;
8767
+ }
8768
+ .service-item {
8769
+ padding: 8px 0;
8770
+ display: flex;
8771
+ justify-content: space-between;
8772
+ align-items: center;
8773
+ }
8774
+ .change-up {
8775
+ color: #dc3545;
8776
+ }
8777
+ .change-down {
8778
+ color: #28a745;
8779
+ }
8780
+ .change-neutral {
8781
+ color: #6c757d;
8782
+ }
8783
+ .alert {
8784
+ background: #fff3cd;
8785
+ border-left: 4px solid #ffc107;
8786
+ padding: 15px;
8787
+ margin-bottom: 10px;
8788
+ border-radius: 4px;
8789
+ }
8790
+ .footer {
8791
+ text-align: center;
8792
+ padding: 20px;
8793
+ color: #6c757d;
8794
+ font-size: 12px;
8795
+ }
8796
+ </style>
8797
+ </head>
8798
+ <body>
8799
+ <div class="header">
8800
+ <h1>\u{1F4B0} Daily Cost Report</h1>
8801
+ <p>${accountName ? `${provider || "Cloud"} Account: ${accountName}` : ""}</p>
8802
+ <p>${date}</p>
8803
+ </div>
8804
+
8805
+ <div class="content">
8806
+ <div class="section">
8807
+ <div class="section-title">\u{1F4CA} Today's Summary</div>
8808
+ <div class="metric-row">
8809
+ <span class="metric-label">Today's Cost</span>
8810
+ <span class="metric-value">$${todayCost.toFixed(2)}</span>
8811
+ </div>
8812
+ <div class="metric-row">
8813
+ <span class="metric-label">Month-to-Date</span>
8814
+ <span class="metric-value">$${mtdCost.toFixed(2)}${budget2 ? ` / $${budget2.toFixed(2)}` : ""}</span>
8815
+ </div>
8816
+ ${budget2 ? `
8817
+ <div class="metric-row">
8818
+ <span class="metric-label">Budget Usage</span>
8819
+ <span class="metric-value">${budgetPercent?.toFixed(0)}%</span>
8820
+ </div>
8821
+ ` : ""}
8822
+ ${projectedMonthEnd ? `
8823
+ <div class="metric-row">
8824
+ <span class="metric-label">Projected Month-End</span>
8825
+ <span class="metric-value">$${projectedMonthEnd.toFixed(2)}</span>
8826
+ </div>
8827
+ ` : ""}
8828
+ </div>
8829
+
8830
+ <div class="section">
8831
+ <div class="section-title">\u{1F4C8} Top Services</div>
8832
+ ${topServices.map((service) => {
8833
+ const changeClass = service.change > 0 ? "change-up" : service.change < 0 ? "change-down" : "change-neutral";
8834
+ const changeSymbol = service.change > 0 ? "\u2191" : service.change < 0 ? "\u2193" : "\u2192";
8835
+ const changeText = service.change !== 0 ? `${changeSymbol} ${service.change > 0 ? "+" : ""}${service.change}%` : "\u2192 0%";
8836
+ return `
8837
+ <div class="service-item">
8838
+ <span>${service.name}: $${service.cost.toFixed(2)}</span>
8839
+ <span class="${changeClass}">${changeText}</span>
8840
+ </div>
8841
+ `;
8842
+ }).join("")}
8843
+ </div>
8844
+
8845
+ ${alerts && alerts.length > 0 ? `
8846
+ <div class="section">
8847
+ <div class="section-title">\u26A0\uFE0F Alerts</div>
8848
+ ${alerts.map((alert) => `<div class="alert">${alert}</div>`).join("")}
8849
+ </div>
8850
+ ` : ""}
8851
+ </div>
8852
+
8853
+ <div class="footer">
8854
+ <p>Generated by infra-cost</p>
8855
+ <p>This is an automated report</p>
8856
+ </div>
8857
+ </body>
8858
+ </html>
8859
+ `.trim();
8860
+ }
8861
+ function generateEmailText(data) {
8862
+ const { date, todayCost, mtdCost, budget: budget2, budgetPercent, topServices, alerts } = data;
8863
+ let text = `Daily Cost Report - ${date}
8864
+
8865
+ `;
8866
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
8867
+ `;
8868
+ text += `\u{1F4CA} Today's Summary
8869
+ `;
8870
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
8871
+ `;
8872
+ text += `\u2022 Today: $${todayCost.toFixed(2)}
8873
+ `;
8874
+ text += `\u2022 Month-to-Date: $${mtdCost.toFixed(2)}${budget2 ? ` / $${budget2.toFixed(2)}` : ""}
8875
+ `;
8876
+ if (budget2) {
8877
+ text += `\u2022 Budget Usage: ${budgetPercent?.toFixed(0)}%
8878
+ `;
8879
+ }
8880
+ text += `
8881
+ `;
8882
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
8883
+ `;
8884
+ text += `\u{1F4C8} Top Services
8885
+ `;
8886
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
8887
+ `;
8888
+ topServices.forEach((service, index) => {
8889
+ const changeSymbol = service.change > 0 ? "\u2191" : service.change < 0 ? "\u2193" : "\u2192";
8890
+ text += `${index + 1}. ${service.name}: $${service.cost.toFixed(2)} (${changeSymbol} ${service.change > 0 ? "+" : ""}${service.change}%)
8891
+ `;
8892
+ });
8893
+ text += `
8894
+ `;
8895
+ if (alerts && alerts.length > 0) {
8896
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
8897
+ `;
8898
+ text += `\u26A0\uFE0F Alerts
8899
+ `;
8900
+ text += `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
8901
+ `;
8902
+ alerts.forEach((alert) => {
8903
+ text += `\u2022 ${alert}
8904
+ `;
8905
+ });
8906
+ text += `
8907
+ `;
8908
+ }
8909
+ text += `--
8910
+ Generated by infra-cost`;
8911
+ return text;
8912
+ }
8913
+ async function sendViaSendGrid(config, data) {
8914
+ if (!config.sendgrid?.apiKey) {
8915
+ throw new Error("SendGrid API key is required");
8916
+ }
8917
+ const recipients = Array.isArray(config.to) ? config.to : [config.to];
8918
+ const payload = {
8919
+ personalizations: [
8920
+ {
8921
+ to: recipients.map((email) => ({ email })),
8922
+ subject: config.subject || `\u{1F4B0} Daily Cost Report - ${data.date}`
8923
+ }
8924
+ ],
8925
+ from: {
8926
+ email: config.from
8927
+ },
8928
+ content: [
8929
+ {
8930
+ type: "text/plain",
8931
+ value: generateEmailText(data)
8932
+ },
8933
+ {
8934
+ type: "text/html",
8935
+ value: generateEmailHTML(data)
8936
+ }
8937
+ ]
8938
+ };
8939
+ const response = await (0, import_node_fetch3.default)("https://api.sendgrid.com/v3/mail/send", {
8940
+ method: "POST",
8941
+ headers: {
8942
+ "Content-Type": "application/json",
8943
+ Authorization: `Bearer ${config.sendgrid.apiKey}`
8944
+ },
8945
+ body: JSON.stringify(payload)
8946
+ });
8947
+ if (!response.ok) {
8948
+ const errorText = await response.text();
8949
+ throw new Error(`SendGrid API error: ${response.status} ${errorText}`);
8950
+ }
8951
+ }
8952
+ async function sendViaMailgun(config, data) {
8953
+ if (!config.mailgun?.apiKey || !config.mailgun?.domain) {
8954
+ throw new Error("Mailgun API key and domain are required");
8955
+ }
8956
+ const recipients = Array.isArray(config.to) ? config.to.join(",") : config.to;
8957
+ const formData = new URLSearchParams();
8958
+ formData.append("from", config.from);
8959
+ formData.append("to", recipients);
8960
+ formData.append("subject", config.subject || `\u{1F4B0} Daily Cost Report - ${data.date}`);
8961
+ formData.append("text", generateEmailText(data));
8962
+ formData.append("html", generateEmailHTML(data));
8963
+ const response = await (0, import_node_fetch3.default)(
8964
+ `https://api.mailgun.net/v3/${config.mailgun.domain}/messages`,
8965
+ {
8966
+ method: "POST",
8967
+ headers: {
8968
+ Authorization: `Basic ${Buffer.from(`api:${config.mailgun.apiKey}`).toString("base64")}`
8969
+ },
8970
+ body: formData
8971
+ }
8972
+ );
8973
+ if (!response.ok) {
8974
+ const errorText = await response.text();
8975
+ throw new Error(`Mailgun API error: ${response.status} ${errorText}`);
8976
+ }
8977
+ }
8978
+ async function sendEmailReport(data, config) {
8979
+ try {
8980
+ switch (config.provider) {
8981
+ case "sendgrid":
8982
+ await sendViaSendGrid(config, data);
8983
+ break;
8984
+ case "mailgun":
8985
+ await sendViaMailgun(config, data);
8986
+ break;
8987
+ case "smtp":
8988
+ throw new Error("SMTP provider not yet implemented - use SendGrid or Mailgun");
8989
+ case "ses":
8990
+ throw new Error("AWS SES provider not yet implemented - use SendGrid or Mailgun");
8991
+ default:
8992
+ throw new Error(`Unknown email provider: ${config.provider}`);
8993
+ }
8994
+ } catch (error) {
8995
+ throw new Error(`Failed to send email report: ${error.message}`);
8996
+ }
8997
+ }
8998
+ var import_node_fetch3;
8999
+ var init_email = __esm({
9000
+ "src/integrations/email.ts"() {
9001
+ import_node_fetch3 = __toESM(require("node-fetch"));
9002
+ __name(generateEmailHTML, "generateEmailHTML");
9003
+ __name(generateEmailText, "generateEmailText");
9004
+ __name(sendViaSendGrid, "sendViaSendGrid");
9005
+ __name(sendViaMailgun, "sendViaMailgun");
9006
+ __name(sendEmailReport, "sendEmailReport");
9007
+ }
9008
+ });
9009
+
9010
+ // src/cli/commands/export/email.ts
9011
+ var email_exports = {};
9012
+ __export(email_exports, {
9013
+ handleEmail: () => handleEmail
9014
+ });
9015
+ async function handleEmail(options) {
9016
+ const { emailTo, emailFrom, emailProvider, sendgridKey, mailgunKey, mailgunDomain } = options;
9017
+ if (!emailTo) {
9018
+ console.error(import_chalk11.default.red("Error: --email-to is required"));
9019
+ console.log("\nExample:");
9020
+ console.log(import_chalk11.default.gray(" infra-cost export email --email-to finance@company.com --email-provider sendgrid --sendgrid-key YOUR_KEY"));
9021
+ process.exit(1);
9022
+ }
9023
+ if (!emailFrom) {
9024
+ console.error(import_chalk11.default.red("Error: --email-from is required"));
9025
+ process.exit(1);
9026
+ }
9027
+ const provider = emailProvider || "sendgrid";
9028
+ if (provider === "sendgrid" && !sendgridKey) {
9029
+ console.error(import_chalk11.default.red("Error: --sendgrid-key is required for SendGrid"));
9030
+ process.exit(1);
9031
+ }
9032
+ if (provider === "mailgun" && (!mailgunKey || !mailgunDomain)) {
9033
+ console.error(import_chalk11.default.red("Error: --mailgun-key and --mailgun-domain are required for Mailgun"));
9034
+ process.exit(1);
9035
+ }
9036
+ console.log(import_chalk11.default.blue("\u{1F4E7} Generating and sending email report...\n"));
9037
+ try {
9038
+ const costData = {
9039
+ date: (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
9040
+ weekday: "long",
9041
+ year: "numeric",
9042
+ month: "long",
9043
+ day: "numeric"
9044
+ }),
9045
+ todayCost: 142.3,
9046
+ mtdCost: 2847.5,
9047
+ budget: 5e3,
9048
+ budgetPercent: 57,
9049
+ projectedMonthEnd: 4520,
9050
+ topServices: [
9051
+ { name: "EC2", cost: 52.3, change: 8 },
9052
+ { name: "RDS", cost: 38.5, change: 1 },
9053
+ { name: "S3", cost: 22.1, change: -3 },
9054
+ { name: "Lambda", cost: 15.2, change: 12 }
9055
+ ],
9056
+ alerts: ["Lambda costs increased 12% - investigate function usage"],
9057
+ accountName: options.account || "Production",
9058
+ provider: options.provider?.toUpperCase() || "AWS"
9059
+ };
9060
+ const emailConfig = {
9061
+ provider,
9062
+ from: emailFrom,
9063
+ to: emailTo.split(",").map((e) => e.trim()),
9064
+ subject: options.subject,
9065
+ sendgrid: sendgridKey ? { apiKey: sendgridKey } : void 0,
9066
+ mailgun: mailgunKey && mailgunDomain ? { apiKey: mailgunKey, domain: mailgunDomain } : void 0
9067
+ };
9068
+ await sendEmailReport(costData, emailConfig);
9069
+ console.log(import_chalk11.default.green("\u2705 Email report sent successfully"));
9070
+ console.log(import_chalk11.default.gray(` To: ${emailTo}`));
9071
+ console.log(import_chalk11.default.gray(` Provider: ${provider}`));
9072
+ } catch (error) {
9073
+ console.error(import_chalk11.default.red("Error sending email report:", error.message));
9074
+ process.exit(1);
9075
+ }
9076
+ }
9077
+ var import_chalk11;
9078
+ var init_email2 = __esm({
9079
+ "src/cli/commands/export/email.ts"() {
9080
+ init_email();
9081
+ import_chalk11 = __toESM(require("chalk"));
9082
+ __name(handleEmail, "handleEmail");
9083
+ }
9084
+ });
9085
+
8486
9086
  // src/cli/commands/organizations/list.ts
8487
9087
  var list_exports = {};
8488
9088
  __export(list_exports, {
@@ -8528,7 +9128,7 @@ async function handleDaily(options, command) {
8528
9128
  const report = await generateDailyReport(options);
8529
9129
  if (options.slackWebhook) {
8530
9130
  await sendToSlack(report, options.slackWebhook);
8531
- console.log(import_chalk10.default.green("\u2705 Daily report sent to Slack successfully!"));
9131
+ console.log(import_chalk12.default.green("\u2705 Daily report sent to Slack successfully!"));
8532
9132
  }
8533
9133
  if (options.json) {
8534
9134
  console.log(JSON.stringify(report, null, 2));
@@ -8665,39 +9265,39 @@ async function generateDailyReport(options) {
8665
9265
  function displayDailyReport(report) {
8666
9266
  console.log();
8667
9267
  console.log(
8668
- import_chalk10.default.bold.cyan("\u{1F4CA} AWS Organizations Daily Cost Report"),
8669
- import_chalk10.default.dim(`(${report.date})`)
9268
+ import_chalk12.default.bold.cyan("\u{1F4CA} AWS Organizations Daily Cost Report"),
9269
+ import_chalk12.default.dim(`(${report.date})`)
8670
9270
  );
8671
- console.log(import_chalk10.default.dim("\u2550".repeat(70)));
9271
+ console.log(import_chalk12.default.dim("\u2550".repeat(70)));
8672
9272
  console.log();
8673
- console.log(import_chalk10.default.bold("\u{1F4B0} Total Costs"));
9273
+ console.log(import_chalk12.default.bold("\u{1F4B0} Total Costs"));
8674
9274
  console.log(
8675
- `\u251C\u2500\u2500 Today: ${import_chalk10.default.green("$" + report.totalCost.toFixed(2))}`
9275
+ `\u251C\u2500\u2500 Today: ${import_chalk12.default.green("$" + report.totalCost.toFixed(2))}`
8676
9276
  );
8677
9277
  console.log(
8678
- `\u251C\u2500\u2500 Weekly Total: ${import_chalk10.default.cyan("$" + report.weeklyTotal.toFixed(2))}`
9278
+ `\u251C\u2500\u2500 Weekly Total: ${import_chalk12.default.cyan("$" + report.weeklyTotal.toFixed(2))}`
8679
9279
  );
8680
- console.log(`\u2514\u2500\u2500 Active Accounts: ${import_chalk10.default.yellow(report.accountsCount)}`);
9280
+ console.log(`\u2514\u2500\u2500 Active Accounts: ${import_chalk12.default.yellow(report.accountsCount)}`);
8681
9281
  console.log();
8682
- console.log(import_chalk10.default.bold("\u{1F525} Top 5 Spenders Today"));
9282
+ console.log(import_chalk12.default.bold("\u{1F525} Top 5 Spenders Today"));
8683
9283
  report.topSpenders.forEach((account, index) => {
8684
9284
  const trendIcon = account.trend === "up" ? "\u{1F4C8}" : account.trend === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
8685
- const deltaColor = account.trend === "up" ? import_chalk10.default.red : account.trend === "down" ? import_chalk10.default.green : import_chalk10.default.gray;
9285
+ const deltaColor = account.trend === "up" ? import_chalk12.default.red : account.trend === "down" ? import_chalk12.default.green : import_chalk12.default.gray;
8686
9286
  const prefix = index === report.topSpenders.length - 1 ? "\u2514\u2500\u2500" : "\u251C\u2500\u2500";
8687
9287
  console.log(
8688
- `${prefix} ${trendIcon} ${import_chalk10.default.bold(account.accountName)} ${import_chalk10.default.dim(`(${account.accountId})`)}`
9288
+ `${prefix} ${trendIcon} ${import_chalk12.default.bold(account.accountName)} ${import_chalk12.default.dim(`(${account.accountId})`)}`
8689
9289
  );
8690
9290
  console.log(
8691
- ` Today: ${import_chalk10.default.green("$" + account.todayCost.toFixed(2))} | Yesterday: $${account.yesterdayCost.toFixed(2)} | ${deltaColor(account.delta >= 0 ? "+" : "")}${deltaColor("$" + account.delta.toFixed(2))}`
9291
+ ` Today: ${import_chalk12.default.green("$" + account.todayCost.toFixed(2))} | Yesterday: $${account.yesterdayCost.toFixed(2)} | ${deltaColor(account.delta >= 0 ? "+" : "")}${deltaColor("$" + account.delta.toFixed(2))}`
8692
9292
  );
8693
9293
  });
8694
9294
  console.log();
8695
- console.log(import_chalk10.default.bold("\u{1F4CB} All Accounts"));
8696
- console.log(import_chalk10.default.dim("\u2500".repeat(70)));
9295
+ console.log(import_chalk12.default.bold("\u{1F4CB} All Accounts"));
9296
+ console.log(import_chalk12.default.dim("\u2500".repeat(70)));
8697
9297
  report.accounts.forEach((account) => {
8698
9298
  const trendIcon = account.trend === "up" ? "\u2B06\uFE0F" : account.trend === "down" ? "\u2B07\uFE0F" : "\u27A1\uFE0F";
8699
9299
  console.log(
8700
- `${trendIcon} ${account.accountName.padEnd(30)} $${account.todayCost.toFixed(2).padStart(8)} ${import_chalk10.default.dim("(7d avg: $" + account.weeklyAverage.toFixed(2) + ")")}`
9300
+ `${trendIcon} ${account.accountName.padEnd(30)} $${account.todayCost.toFixed(2).padStart(8)} ${import_chalk12.default.dim("(7d avg: $" + account.weeklyAverage.toFixed(2) + ")")}`
8701
9301
  );
8702
9302
  });
8703
9303
  console.log();
@@ -8773,31 +9373,31 @@ $${account.todayCost.toFixed(2)} today | Yesterday: $${account.yesterdayCost.toF
8773
9373
  function setupDailySchedule(options) {
8774
9374
  const scheduleTime = options.scheduleTime || "09:00";
8775
9375
  console.log();
8776
- console.log(import_chalk10.default.bold.yellow("\u2699\uFE0F Daily Scheduling Setup"));
8777
- console.log(import_chalk10.default.dim("\u2500".repeat(70)));
9376
+ console.log(import_chalk12.default.bold.yellow("\u2699\uFE0F Daily Scheduling Setup"));
9377
+ console.log(import_chalk12.default.dim("\u2500".repeat(70)));
8778
9378
  console.log(
8779
- import_chalk10.default.yellow(
9379
+ import_chalk12.default.yellow(
8780
9380
  "\u26A0\uFE0F Note: Automated scheduling requires an external scheduler (cron, AWS Lambda, GitHub Actions)"
8781
9381
  )
8782
9382
  );
8783
9383
  console.log();
8784
9384
  console.log(
8785
- import_chalk10.default.bold("Option 1: Cron (Linux/macOS)"),
8786
- import_chalk10.default.dim("- Best for servers")
9385
+ import_chalk12.default.bold("Option 1: Cron (Linux/macOS)"),
9386
+ import_chalk12.default.dim("- Best for servers")
8787
9387
  );
8788
9388
  console.log(
8789
- `Add this to your crontab (run ${import_chalk10.default.cyan("crontab -e")}):
9389
+ `Add this to your crontab (run ${import_chalk12.default.cyan("crontab -e")}):
8790
9390
  `
8791
9391
  );
8792
9392
  console.log(
8793
- import_chalk10.default.cyan(
9393
+ import_chalk12.default.cyan(
8794
9394
  ` 0 9 * * * cd $(pwd) && npx infra-cost organizations daily --slack-webhook="${options.slackWebhook}" > /dev/null 2>&1`
8795
9395
  )
8796
9396
  );
8797
9397
  console.log();
8798
9398
  console.log(
8799
- import_chalk10.default.bold("Option 2: AWS Lambda + EventBridge"),
8800
- import_chalk10.default.dim("- Best for AWS-native")
9399
+ import_chalk12.default.bold("Option 2: AWS Lambda + EventBridge"),
9400
+ import_chalk12.default.dim("- Best for AWS-native")
8801
9401
  );
8802
9402
  console.log(
8803
9403
  ` 1. Deploy this CLI as a Lambda function
@@ -8808,12 +9408,12 @@ function setupDailySchedule(options) {
8808
9408
  );
8809
9409
  console.log();
8810
9410
  console.log(
8811
- import_chalk10.default.bold("Option 3: GitHub Actions"),
8812
- import_chalk10.default.dim("- Best for CI/CD workflows")
9411
+ import_chalk12.default.bold("Option 3: GitHub Actions"),
9412
+ import_chalk12.default.dim("- Best for CI/CD workflows")
8813
9413
  );
8814
9414
  console.log(" Create .github/workflows/daily-cost-report.yml:\n");
8815
9415
  console.log(
8816
- import_chalk10.default.cyan(` name: Daily Cost Report
9416
+ import_chalk12.default.cyan(` name: Daily Cost Report
8817
9417
  on:
8818
9418
  schedule:
8819
9419
  - cron: '0 ${scheduleTime.split(":")[0]} * * *'
@@ -8826,16 +9426,16 @@ function setupDailySchedule(options) {
8826
9426
  );
8827
9427
  console.log();
8828
9428
  console.log(
8829
- import_chalk10.default.green(
9429
+ import_chalk12.default.green(
8830
9430
  `\u{1F4A1} Tip: Store your Slack webhook URL in environment variables or secrets manager`
8831
9431
  )
8832
9432
  );
8833
9433
  console.log();
8834
9434
  }
8835
- var import_chalk10, import_dayjs5, import_client_organizations, import_client_cost_explorer4;
9435
+ var import_chalk12, import_dayjs5, import_client_organizations, import_client_cost_explorer4;
8836
9436
  var init_daily = __esm({
8837
9437
  "src/cli/commands/organizations/daily.ts"() {
8838
- import_chalk10 = __toESM(require("chalk"));
9438
+ import_chalk12 = __toESM(require("chalk"));
8839
9439
  import_dayjs5 = __toESM(require("dayjs"));
8840
9440
  init_logging();
8841
9441
  import_client_organizations = require("@aws-sdk/client-organizations");
@@ -8896,6 +9496,194 @@ var init_slack = __esm({
8896
9496
  }
8897
9497
  });
8898
9498
 
9499
+ // src/integrations/teams.ts
9500
+ function createAdaptiveCard(data, style = "detailed") {
9501
+ const { todayCost, mtdCost, budget: budget2, budgetPercent, topServices, alerts, accountName, provider } = data;
9502
+ let color = "good";
9503
+ if (budgetPercent && budgetPercent > 90) {
9504
+ color = "attention";
9505
+ } else if (budgetPercent && budgetPercent > 75) {
9506
+ color = "warning";
9507
+ }
9508
+ const card = {
9509
+ type: "message",
9510
+ attachments: [
9511
+ {
9512
+ contentType: "application/vnd.microsoft.card.adaptive",
9513
+ content: {
9514
+ type: "AdaptiveCard",
9515
+ $schema: "http://adaptivecards.io/schemas/adaptive-card.json",
9516
+ version: "1.4",
9517
+ body: [],
9518
+ actions: []
9519
+ }
9520
+ }
9521
+ ]
9522
+ };
9523
+ const body = card.attachments[0].content.body;
9524
+ body.push({
9525
+ type: "TextBlock",
9526
+ text: "\u{1F4B0} Daily Cost Report",
9527
+ weight: "bolder",
9528
+ size: "large"
9529
+ });
9530
+ if (accountName) {
9531
+ body.push({
9532
+ type: "TextBlock",
9533
+ text: `${provider || "Cloud"} Account: ${accountName}`,
9534
+ isSubtle: true,
9535
+ wrap: true
9536
+ });
9537
+ }
9538
+ const facts = [
9539
+ {
9540
+ title: "Today's Cost",
9541
+ value: `$${todayCost.toFixed(2)}`
9542
+ },
9543
+ {
9544
+ title: "Month-to-Date",
9545
+ value: `$${mtdCost.toFixed(2)}`
9546
+ }
9547
+ ];
9548
+ if (budget2) {
9549
+ facts.push({
9550
+ title: "Budget",
9551
+ value: `$${mtdCost.toFixed(2)} / $${budget2.toFixed(2)} (${budgetPercent?.toFixed(0)}%)`
9552
+ });
9553
+ }
9554
+ body.push({
9555
+ type: "FactSet",
9556
+ facts
9557
+ });
9558
+ if (style === "detailed" && topServices.length > 0) {
9559
+ body.push({
9560
+ type: "TextBlock",
9561
+ text: "Top Services",
9562
+ weight: "bolder",
9563
+ wrap: true,
9564
+ spacing: "medium"
9565
+ });
9566
+ topServices.forEach((service) => {
9567
+ const changeEmoji = service.change > 0 ? "\u2197" : service.change < 0 ? "\u2198" : "\u2192";
9568
+ const changePercent = service.change > 0 ? `+${service.change}%` : `${service.change}%`;
9569
+ body.push({
9570
+ type: "TextBlock",
9571
+ text: `\u2022 ${service.name}: $${service.cost.toFixed(2)} (${changeEmoji} ${changePercent})`,
9572
+ wrap: true
9573
+ });
9574
+ });
9575
+ }
9576
+ if (alerts && alerts.length > 0) {
9577
+ body.push({
9578
+ type: "TextBlock",
9579
+ text: "\u26A0\uFE0F Alerts",
9580
+ weight: "bolder",
9581
+ wrap: true,
9582
+ spacing: "medium",
9583
+ color: "attention"
9584
+ });
9585
+ alerts.forEach((alert) => {
9586
+ body.push({
9587
+ type: "TextBlock",
9588
+ text: `\u2022 ${alert}`,
9589
+ wrap: true,
9590
+ color: "attention"
9591
+ });
9592
+ });
9593
+ }
9594
+ card.attachments[0].content.actions = [
9595
+ {
9596
+ type: "Action.OpenUrl",
9597
+ title: "View Details",
9598
+ url: "https://console.aws.amazon.com/cost-management"
9599
+ }
9600
+ ];
9601
+ return card;
9602
+ }
9603
+ async function sendTeamsReport(webhook, data, config = {}) {
9604
+ const { cardStyle = "detailed", mentions = [] } = config;
9605
+ try {
9606
+ const card = createAdaptiveCard(data, cardStyle);
9607
+ if (mentions.length > 0) {
9608
+ const mentionText = mentions.map((email) => `<at>${email}</at>`).join(" ");
9609
+ card.attachments[0].content.body.unshift({
9610
+ type: "TextBlock",
9611
+ text: mentionText,
9612
+ wrap: true
9613
+ });
9614
+ }
9615
+ const response = await (0, import_node_fetch4.default)(webhook, {
9616
+ method: "POST",
9617
+ headers: {
9618
+ "Content-Type": "application/json"
9619
+ },
9620
+ body: JSON.stringify(card)
9621
+ });
9622
+ if (!response.ok) {
9623
+ const errorText = await response.text();
9624
+ throw new Error(`Teams webhook failed: ${response.status} ${errorText}`);
9625
+ }
9626
+ } catch (error) {
9627
+ throw new Error(`Failed to send Teams report: ${error.message}`);
9628
+ }
9629
+ }
9630
+ var import_node_fetch4;
9631
+ var init_teams = __esm({
9632
+ "src/integrations/teams.ts"() {
9633
+ import_node_fetch4 = __toESM(require("node-fetch"));
9634
+ __name(createAdaptiveCard, "createAdaptiveCard");
9635
+ __name(sendTeamsReport, "sendTeamsReport");
9636
+ }
9637
+ });
9638
+
9639
+ // src/cli/commands/chargeback/teams.ts
9640
+ var teams_exports = {};
9641
+ __export(teams_exports, {
9642
+ handleTeams: () => handleTeams
9643
+ });
9644
+ async function handleTeams(options, command) {
9645
+ const { teamsWebhook, provider = "aws" } = options;
9646
+ if (!teamsWebhook) {
9647
+ console.error(import_chalk13.default.red("Error: --teams-webhook is required"));
9648
+ console.log("\nExample:");
9649
+ console.log(import_chalk13.default.gray(" infra-cost chargeback teams --teams-webhook https://outlook.office.com/webhook/..."));
9650
+ process.exit(1);
9651
+ }
9652
+ console.log(import_chalk13.default.blue("\u{1F4CA} Generating chargeback report for Teams...\n"));
9653
+ try {
9654
+ const costData = {
9655
+ todayCost: 142.3,
9656
+ mtdCost: 2847.5,
9657
+ budget: 5e3,
9658
+ budgetPercent: 57,
9659
+ topServices: [
9660
+ { name: "EC2", cost: 52.3, change: 8 },
9661
+ { name: "RDS", cost: 38.5, change: 1 },
9662
+ { name: "S3", cost: 22.1, change: -3 },
9663
+ { name: "Lambda", cost: 15.2, change: 12 }
9664
+ ],
9665
+ alerts: ["Lambda costs increased 12% - investigate function usage"],
9666
+ accountName: options.account || "Production",
9667
+ provider: provider.toUpperCase()
9668
+ };
9669
+ await sendTeamsReport(teamsWebhook, costData, {
9670
+ cardStyle: options.cardStyle || "detailed"
9671
+ });
9672
+ console.log(import_chalk13.default.green("\u2705 Chargeback report sent to Microsoft Teams"));
9673
+ } catch (error) {
9674
+ console.error(import_chalk13.default.red("Error sending to Teams:", error.message));
9675
+ process.exit(1);
9676
+ }
9677
+ }
9678
+ var import_chalk13;
9679
+ var init_teams2 = __esm({
9680
+ "src/cli/commands/chargeback/teams.ts"() {
9681
+ init_teams();
9682
+ import_chalk13 = __toESM(require("chalk"));
9683
+ __name(handleTeams, "handleTeams");
9684
+ }
9685
+ });
9686
+
8899
9687
  // src/cli/commands/config/init.ts
8900
9688
  var init_exports = {};
8901
9689
  __export(init_exports, {
@@ -9011,15 +9799,15 @@ async function handleMigrate(options, command) {
9011
9799
  const logger = getGlobalLogger26();
9012
9800
  logger.info("Starting configuration migration", { dryRun: options.dryRun });
9013
9801
  console.log("");
9014
- console.log(import_chalk11.default.bold.cyan("\u{1F504} Configuration Migration Tool"));
9015
- console.log(import_chalk11.default.gray("\u2500".repeat(60)));
9802
+ console.log(import_chalk14.default.bold.cyan("\u{1F504} Configuration Migration Tool"));
9803
+ console.log(import_chalk14.default.gray("\u2500".repeat(60)));
9016
9804
  console.log("");
9017
9805
  const oldConfigFiles = options.configFile ? [options.configFile] : discoverOldConfigFiles();
9018
9806
  if (options.configFile && !(0, import_fs3.existsSync)(options.configFile)) {
9019
9807
  throw new Error(`Config file not found: ${options.configFile}`);
9020
9808
  }
9021
9809
  if (oldConfigFiles.length === 0) {
9022
- console.log(import_chalk11.default.yellow("\u26A0\uFE0F No old configuration files found"));
9810
+ console.log(import_chalk14.default.yellow("\u26A0\uFE0F No old configuration files found"));
9023
9811
  console.log("");
9024
9812
  console.log("Looking for config files in:");
9025
9813
  console.log(" \u2022 ./infra-cost.config.json");
@@ -9032,7 +9820,7 @@ async function handleMigrate(options, command) {
9032
9820
  return;
9033
9821
  }
9034
9822
  for (const oldConfigPath of oldConfigFiles) {
9035
- console.log(import_chalk11.default.green("\u2713"), `Found configuration: ${import_chalk11.default.bold(oldConfigPath)}`);
9823
+ console.log(import_chalk14.default.green("\u2713"), `Found configuration: ${import_chalk14.default.bold(oldConfigPath)}`);
9036
9824
  console.log("");
9037
9825
  try {
9038
9826
  const oldConfigContent = (0, import_fs3.readFileSync)(oldConfigPath, "utf8");
@@ -9046,49 +9834,49 @@ async function handleMigrate(options, command) {
9046
9834
  const lowerKey = key.toLowerCase();
9047
9835
  return REDACT_KEYS.some((k) => lowerKey.includes(k.toLowerCase())) ? "***REDACTED***" : value;
9048
9836
  }, "redact");
9049
- console.log(import_chalk11.default.bold("\u{1F4CB} Migration Preview:"));
9837
+ console.log(import_chalk14.default.bold("\u{1F4CB} Migration Preview:"));
9050
9838
  console.log("");
9051
- console.log(import_chalk11.default.gray("Old format:"));
9052
- console.log(import_chalk11.default.gray(JSON.stringify(oldConfig, redact, 2).split("\n").slice(0, 10).join("\n")));
9839
+ console.log(import_chalk14.default.gray("Old format:"));
9840
+ console.log(import_chalk14.default.gray(JSON.stringify(oldConfig, redact, 2).split("\n").slice(0, 10).join("\n")));
9053
9841
  if (Object.keys(oldConfig).length > 10) {
9054
- console.log(import_chalk11.default.gray(" ... (truncated)"));
9842
+ console.log(import_chalk14.default.gray(" ... (truncated)"));
9055
9843
  }
9056
9844
  console.log("");
9057
- console.log(import_chalk11.default.gray("New format:"));
9058
- console.log(import_chalk11.default.cyan(JSON.stringify(newConfig, redact, 2)));
9845
+ console.log(import_chalk14.default.gray("New format:"));
9846
+ console.log(import_chalk14.default.cyan(JSON.stringify(newConfig, redact, 2)));
9059
9847
  console.log("");
9060
9848
  if (options.dryRun) {
9061
- console.log(import_chalk11.default.yellow("\u{1F50D} Dry run mode - no changes made"));
9849
+ console.log(import_chalk14.default.yellow("\u{1F50D} Dry run mode - no changes made"));
9062
9850
  console.log("");
9063
9851
  } else {
9064
9852
  const backupPath = `${oldConfigPath}.backup`;
9065
9853
  (0, import_fs3.writeFileSync)(backupPath, oldConfigContent);
9066
- console.log(import_chalk11.default.green("\u2713"), `Backup created: ${backupPath}`);
9854
+ console.log(import_chalk14.default.green("\u2713"), `Backup created: ${backupPath}`);
9067
9855
  (0, import_fs3.writeFileSync)(oldConfigPath, JSON.stringify(newConfig, null, 2));
9068
- console.log(import_chalk11.default.green("\u2713"), `Configuration migrated: ${oldConfigPath}`);
9856
+ console.log(import_chalk14.default.green("\u2713"), `Configuration migrated: ${oldConfigPath}`);
9069
9857
  console.log("");
9070
9858
  }
9071
9859
  } catch (error) {
9072
- console.error(import_chalk11.default.red("\u2716"), `Failed to migrate ${oldConfigPath}:`, error.message);
9860
+ console.error(import_chalk14.default.red("\u2716"), `Failed to migrate ${oldConfigPath}:`, error.message);
9073
9861
  console.log("");
9074
9862
  }
9075
9863
  }
9076
9864
  console.log("");
9077
9865
  printCLIMigrationGuide();
9078
9866
  console.log("");
9079
- console.log(import_chalk11.default.bold.green("\u2705 Migration Complete!"));
9867
+ console.log(import_chalk14.default.bold.green("\u2705 Migration Complete!"));
9080
9868
  console.log("");
9081
9869
  console.log("Next steps:");
9082
9870
  console.log(" 1. Review the migrated configuration file");
9083
9871
  console.log(" 2. Test the new CLI commands");
9084
9872
  console.log(" 3. Update any scripts or automation");
9085
9873
  console.log("");
9086
- console.log("Run", import_chalk11.default.cyan("infra-cost config validate"), "to verify your configuration");
9874
+ console.log("Run", import_chalk14.default.cyan("infra-cost config validate"), "to verify your configuration");
9087
9875
  console.log("");
9088
9876
  }
9089
9877
  function printCLIMigrationGuide() {
9090
- console.log(import_chalk11.default.bold("\u{1F4D6} CLI Command Migration Guide"));
9091
- console.log(import_chalk11.default.gray("\u2500".repeat(60)));
9878
+ console.log(import_chalk14.default.bold("\u{1F4D6} CLI Command Migration Guide"));
9879
+ console.log(import_chalk14.default.gray("\u2500".repeat(60)));
9092
9880
  console.log("");
9093
9881
  const examples = [
9094
9882
  {
@@ -9132,21 +9920,21 @@ function printCLIMigrationGuide() {
9132
9920
  desc: "Interactive dashboard"
9133
9921
  }
9134
9922
  ];
9135
- console.log(import_chalk11.default.gray("Old Command") + " ".repeat(35) + import_chalk11.default.cyan("New Command"));
9136
- console.log(import_chalk11.default.gray("\u2500".repeat(60)));
9923
+ console.log(import_chalk14.default.gray("Old Command") + " ".repeat(35) + import_chalk14.default.cyan("New Command"));
9924
+ console.log(import_chalk14.default.gray("\u2500".repeat(60)));
9137
9925
  for (const example of examples) {
9138
9926
  const padding = " ".repeat(Math.max(0, 38 - example.old.length));
9139
- console.log(`${import_chalk11.default.gray(example.old)}${padding}${import_chalk11.default.cyan(example.new)}`);
9140
- console.log(import_chalk11.default.dim(` ${example.desc}`));
9927
+ console.log(`${import_chalk14.default.gray(example.old)}${padding}${import_chalk14.default.cyan(example.new)}`);
9928
+ console.log(import_chalk14.default.dim(` ${example.desc}`));
9141
9929
  console.log("");
9142
9930
  }
9143
- console.log(import_chalk11.default.bold("\u{1F4A1} Global Options"));
9931
+ console.log(import_chalk14.default.bold("\u{1F4A1} Global Options"));
9144
9932
  console.log("All global options remain the same:");
9145
9933
  console.log(" --provider, --region, --profile, --output, --config-file, etc.");
9146
9934
  console.log("");
9147
- console.log(import_chalk11.default.bold("\u{1F4DA} More Information"));
9148
- console.log("Run", import_chalk11.default.cyan("infra-cost <command> --help"), "for detailed command help");
9149
- console.log("Example:", import_chalk11.default.cyan("infra-cost cost --help"));
9935
+ console.log(import_chalk14.default.bold("\u{1F4DA} More Information"));
9936
+ console.log("Run", import_chalk14.default.cyan("infra-cost <command> --help"), "for detailed command help");
9937
+ console.log("Example:", import_chalk14.default.cyan("infra-cost cost --help"));
9150
9938
  console.log("");
9151
9939
  }
9152
9940
  function generateMigrationReport() {
@@ -9168,13 +9956,13 @@ function generateMigrationReport() {
9168
9956
  report.push("3. Test all workflows with new commands");
9169
9957
  return report.join("\n");
9170
9958
  }
9171
- var import_fs3, import_path2, import_os2, import_chalk11, CLI_MIGRATION_MAP;
9959
+ var import_fs3, import_path2, import_os2, import_chalk14, CLI_MIGRATION_MAP;
9172
9960
  var init_migrate = __esm({
9173
9961
  "src/cli/commands/config/migrate.ts"() {
9174
9962
  import_fs3 = require("fs");
9175
9963
  import_path2 = require("path");
9176
9964
  import_os2 = require("os");
9177
- import_chalk11 = __toESM(require("chalk"));
9965
+ import_chalk14 = __toESM(require("chalk"));
9178
9966
  init_logging();
9179
9967
  init_schema();
9180
9968
  CLI_MIGRATION_MAP = {
@@ -9432,7 +10220,7 @@ var import_commander = require("commander");
9432
10220
  // package.json
9433
10221
  var package_default = {
9434
10222
  name: "infra-cost",
9435
- version: "1.4.0",
10223
+ version: "1.6.0",
9436
10224
  description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
9437
10225
  keywords: [
9438
10226
  "aws",
@@ -11101,6 +11889,14 @@ function registerMonitorCommands(program) {
11101
11889
  const { handleBudgets: handleBudgets2 } = await Promise.resolve().then(() => (init_budgets(), budgets_exports));
11102
11890
  await handleBudgets2(options, command);
11103
11891
  });
11892
+ monitor.command("alert-pagerduty").description("Send cost alert to PagerDuty").option("--pagerduty-key <key>", "PagerDuty routing key").option("--severity <level>", "Alert severity (critical, error, warning, info)", "warning").option("--threshold <percent>", "Alert threshold percentage").action(async (options) => {
11893
+ const { handlePagerDuty: handlePagerDuty2 } = await Promise.resolve().then(() => (init_alert(), alert_exports));
11894
+ await handlePagerDuty2(options);
11895
+ });
11896
+ monitor.command("alert-opsgenie").description("Send cost alert to OpsGenie").option("--opsgenie-key <key>", "OpsGenie API key").option("--priority <level>", "Alert priority (P1, P2, P3, P4, P5)", "P3").option("--threshold <percent>", "Alert threshold percentage").option("--team <name>", "Team to notify").action(async (options) => {
11897
+ const { handleOpsGenie: handleOpsGenie2 } = await Promise.resolve().then(() => (init_alert(), alert_exports));
11898
+ await handleOpsGenie2(options);
11899
+ });
11104
11900
  }
11105
11901
  __name(registerMonitorCommands, "registerMonitorCommands");
11106
11902
 
@@ -11119,6 +11915,10 @@ function registerExportCommands(program) {
11119
11915
  const { handleReports: handleReports2 } = await Promise.resolve().then(() => (init_reports(), reports_exports));
11120
11916
  await handleReports2(format, options, command);
11121
11917
  });
11918
+ exportCmd.command("email").description("Send cost report via email").option("--email-to <addresses>", "Recipient email addresses (comma-separated)").option("--email-from <address>", "Sender email address").option("--email-provider <provider>", "Email provider (sendgrid, mailgun, smtp, ses)", "sendgrid").option("--sendgrid-key <key>", "SendGrid API key").option("--mailgun-key <key>", "Mailgun API key").option("--mailgun-domain <domain>", "Mailgun domain").option("--subject <text>", "Email subject").option("--account <name>", "Account name for report").action(async (options) => {
11919
+ const { handleEmail: handleEmail2 } = await Promise.resolve().then(() => (init_email2(), email_exports));
11920
+ await handleEmail2(options);
11921
+ });
11122
11922
  }
11123
11923
  __name(registerExportCommands, "registerExportCommands");
11124
11924
 
@@ -11126,8 +11926,8 @@ __name(registerExportCommands, "registerExportCommands");
11126
11926
  function registerOrganizationsCommands(program) {
11127
11927
  const orgs = program.command("organizations").alias("orgs").description("AWS Organizations multi-account management");
11128
11928
  orgs.command("list").description("List all accounts in the organization").option("--include-inactive", "Include suspended/closed accounts").action(async (options, command) => {
11129
- const { handleList: handleList2 } = await Promise.resolve().then(() => (init_list(), list_exports));
11130
- await handleList2(options, command);
11929
+ const { handleList: handleList4 } = await Promise.resolve().then(() => (init_list(), list_exports));
11930
+ await handleList4(options, command);
11131
11931
  });
11132
11932
  orgs.command("summary").description("Multi-account cost summary").option("--group-by <field>", "Group by (account, ou, tag)", "account").action(async (options, command) => {
11133
11933
  const { handleSummary: handleSummary2 } = await Promise.resolve().then(() => (init_summary(), summary_exports));
@@ -11155,6 +11955,10 @@ function registerChargebackCommands(program) {
11155
11955
  const { handleSlack: handleSlack2 } = await Promise.resolve().then(() => (init_slack(), slack_exports));
11156
11956
  await handleSlack2(options, command);
11157
11957
  });
11958
+ chargeback.command("teams").description("Send chargeback report to Microsoft Teams").option("--teams-webhook <url>", "Teams incoming webhook URL").option("--card-style <style>", "Card style (compact, detailed, executive)", "detailed").option("--account <name>", "Account name for report").action(async (options, command) => {
11959
+ const { handleTeams: handleTeams2 } = await Promise.resolve().then(() => (init_teams2(), teams_exports));
11960
+ await handleTeams2(options, command);
11961
+ });
11158
11962
  }
11159
11963
  __name(registerChargebackCommands, "registerChargebackCommands");
11160
11964
 
@@ -11201,7 +12005,7 @@ __name(registerDashboardCommands, "registerDashboardCommands");
11201
12005
  // src/cli/commands/annotate/index.ts
11202
12006
  var fs3 = __toESM(require("fs/promises"));
11203
12007
  var path3 = __toESM(require("path"));
11204
- var import_chalk12 = __toESM(require("chalk"));
12008
+ var import_chalk15 = __toESM(require("chalk"));
11205
12009
  function registerAnnotateCommand(program) {
11206
12010
  program.command("annotate").description("Add cost annotations to Infrastructure as Code files").option("--path <path>", "Path to IaC files or directory", "./terraform").option(
11207
12011
  "--format <format>",
@@ -11211,7 +12015,7 @@ function registerAnnotateCommand(program) {
11211
12015
  try {
11212
12016
  await handleAnnotate(options);
11213
12017
  } catch (error) {
11214
- console.error(import_chalk12.default.red("Error:"), error.message);
12018
+ console.error(import_chalk15.default.red("Error:"), error.message);
11215
12019
  process.exit(1);
11216
12020
  }
11217
12021
  });
@@ -11226,10 +12030,10 @@ async function handleAnnotate(options) {
11226
12030
  }
11227
12031
  const files = await findIaCFiles(targetPath, options.format);
11228
12032
  if (files.length === 0) {
11229
- console.log(import_chalk12.default.yellow("No IaC files found at:", targetPath));
12033
+ console.log(import_chalk15.default.yellow("No IaC files found at:", targetPath));
11230
12034
  return;
11231
12035
  }
11232
- console.log(import_chalk12.default.cyan(`Found ${files.length} file(s) to annotate`));
12036
+ console.log(import_chalk15.default.cyan(`Found ${files.length} file(s) to annotate`));
11233
12037
  console.log();
11234
12038
  let annotatedCount = 0;
11235
12039
  let skippedCount = 0;
@@ -11238,29 +12042,29 @@ async function handleAnnotate(options) {
11238
12042
  if (result.annotated) {
11239
12043
  annotatedCount++;
11240
12044
  console.log(
11241
- import_chalk12.default.green("\u2713"),
12045
+ import_chalk15.default.green("\u2713"),
11242
12046
  path3.relative(process.cwd(), file),
11243
- import_chalk12.default.dim(`(${result.resourcesAnnotated} resources)`)
12047
+ import_chalk15.default.dim(`(${result.resourcesAnnotated} resources)`)
11244
12048
  );
11245
12049
  } else {
11246
12050
  skippedCount++;
11247
12051
  console.log(
11248
- import_chalk12.default.yellow("\u2298"),
12052
+ import_chalk15.default.yellow("\u2298"),
11249
12053
  path3.relative(process.cwd(), file),
11250
- import_chalk12.default.dim("(no changes)")
12054
+ import_chalk15.default.dim("(no changes)")
11251
12055
  );
11252
12056
  }
11253
12057
  }
11254
12058
  console.log();
11255
12059
  console.log(
11256
- import_chalk12.default.bold("Summary:"),
11257
- import_chalk12.default.green(`${annotatedCount} annotated`),
11258
- import_chalk12.default.yellow(`${skippedCount} skipped`)
12060
+ import_chalk15.default.bold("Summary:"),
12061
+ import_chalk15.default.green(`${annotatedCount} annotated`),
12062
+ import_chalk15.default.yellow(`${skippedCount} skipped`)
11259
12063
  );
11260
12064
  if (options.dryRun) {
11261
12065
  console.log();
11262
- console.log(import_chalk12.default.yellow("\u2139 Dry run - no files were modified"));
11263
- console.log(import_chalk12.default.dim("Run without --dry-run to apply changes"));
12066
+ console.log(import_chalk15.default.yellow("\u2139 Dry run - no files were modified"));
12067
+ console.log(import_chalk15.default.dim("Run without --dry-run to apply changes"));
11264
12068
  }
11265
12069
  }
11266
12070
  __name(handleAnnotate, "handleAnnotate");
@@ -11478,7 +12282,7 @@ __name(estimateResourceCost, "estimateResourceCost");
11478
12282
 
11479
12283
  // src/cli/commands/git/index.ts
11480
12284
  var import_child_process = require("child_process");
11481
- var import_chalk13 = __toESM(require("chalk"));
12285
+ var import_chalk16 = __toESM(require("chalk"));
11482
12286
  var import_dayjs7 = __toESM(require("dayjs"));
11483
12287
  function registerGitCommands(program) {
11484
12288
  const git = program.command("history").description("Show cost history with git correlation");
@@ -11486,7 +12290,7 @@ function registerGitCommands(program) {
11486
12290
  try {
11487
12291
  await handleHistory(options);
11488
12292
  } catch (error) {
11489
- console.error(import_chalk13.default.red("Error:"), error.message);
12293
+ console.error(import_chalk16.default.red("Error:"), error.message);
11490
12294
  process.exit(1);
11491
12295
  }
11492
12296
  });
@@ -11494,7 +12298,7 @@ function registerGitCommands(program) {
11494
12298
  try {
11495
12299
  await handleBlame(options);
11496
12300
  } catch (error) {
11497
- console.error(import_chalk13.default.red("Error:"), error.message);
12301
+ console.error(import_chalk16.default.red("Error:"), error.message);
11498
12302
  process.exit(1);
11499
12303
  }
11500
12304
  });
@@ -11535,17 +12339,17 @@ async function handleBlame(options) {
11535
12339
  return;
11536
12340
  }
11537
12341
  console.log();
11538
- console.log(import_chalk13.default.bold.cyan(`\u{1F50D} Cost Blame Analysis (${getPeriodLabel(period)})`));
11539
- console.log(import_chalk13.default.dim("\u2550".repeat(70)));
12342
+ console.log(import_chalk16.default.bold.cyan(`\u{1F50D} Cost Blame Analysis (${getPeriodLabel(period)})`));
12343
+ console.log(import_chalk16.default.dim("\u2550".repeat(70)));
11540
12344
  console.log();
11541
12345
  console.log(
11542
- import_chalk13.default.bold("Author").padEnd(35),
11543
- import_chalk13.default.bold("Cost Impact").padEnd(18),
11544
- import_chalk13.default.bold("Commits")
12346
+ import_chalk16.default.bold("Author").padEnd(35),
12347
+ import_chalk16.default.bold("Cost Impact").padEnd(18),
12348
+ import_chalk16.default.bold("Commits")
11545
12349
  );
11546
- console.log(import_chalk13.default.dim("\u2500".repeat(70)));
12350
+ console.log(import_chalk16.default.dim("\u2500".repeat(70)));
11547
12351
  sorted.forEach(([email, stats]) => {
11548
- const impactColor = stats.costImpact > 0 ? import_chalk13.default.red : import_chalk13.default.green;
12352
+ const impactColor = stats.costImpact > 0 ? import_chalk16.default.red : import_chalk16.default.green;
11549
12353
  const impactSign = stats.costImpact > 0 ? "+" : "";
11550
12354
  const emoji = stats.costImpact < 0 ? " \u{1F44F}" : "";
11551
12355
  console.log(
@@ -11554,7 +12358,7 @@ async function handleBlame(options) {
11554
12358
  stats.commits.toString() + emoji
11555
12359
  );
11556
12360
  });
11557
- console.log(import_chalk13.default.dim("\u2550".repeat(70)));
12361
+ console.log(import_chalk16.default.dim("\u2550".repeat(70)));
11558
12362
  console.log();
11559
12363
  }
11560
12364
  __name(handleBlame, "handleBlame");
@@ -11566,38 +12370,38 @@ async function showCostHistory(options) {
11566
12370
  return;
11567
12371
  }
11568
12372
  console.log();
11569
- console.log(import_chalk13.default.bold.cyan("\u{1F4CA} Cost History with Git Correlation"));
11570
- console.log(import_chalk13.default.dim("\u2550".repeat(80)));
12373
+ console.log(import_chalk16.default.bold.cyan("\u{1F4CA} Cost History with Git Correlation"));
12374
+ console.log(import_chalk16.default.dim("\u2550".repeat(80)));
11571
12375
  console.log();
11572
12376
  console.log(
11573
- import_chalk13.default.bold("Date").padEnd(14),
11574
- import_chalk13.default.bold("Cost").padEnd(12),
11575
- import_chalk13.default.bold("Change").padEnd(12),
11576
- import_chalk13.default.bold("Commit")
12377
+ import_chalk16.default.bold("Date").padEnd(14),
12378
+ import_chalk16.default.bold("Cost").padEnd(12),
12379
+ import_chalk16.default.bold("Change").padEnd(12),
12380
+ import_chalk16.default.bold("Commit")
11577
12381
  );
11578
- console.log(import_chalk13.default.dim("\u2500".repeat(80)));
12382
+ console.log(import_chalk16.default.dim("\u2500".repeat(80)));
11579
12383
  commits.forEach((commit) => {
11580
- const changeColor = commit.costChange > 0 ? import_chalk13.default.red : commit.costChange < 0 ? import_chalk13.default.green : import_chalk13.default.gray;
12384
+ const changeColor = commit.costChange > 0 ? import_chalk16.default.red : commit.costChange < 0 ? import_chalk16.default.green : import_chalk16.default.gray;
11581
12385
  const changeSign = commit.costChange > 0 ? "+" : "";
11582
12386
  console.log(
11583
12387
  (0, import_dayjs7.default)(commit.date).format("YYYY-MM-DD").padEnd(14),
11584
- import_chalk13.default.green(`$${commit.cost.toFixed(2)}`).padEnd(12),
12388
+ import_chalk16.default.green(`$${commit.cost.toFixed(2)}`).padEnd(12),
11585
12389
  changeColor(`${changeSign}$${commit.costChange.toFixed(2)}`).padEnd(12),
11586
- import_chalk13.default.dim(commit.shortCommit),
12390
+ import_chalk16.default.dim(commit.shortCommit),
11587
12391
  commit.message.substring(0, 40)
11588
12392
  );
11589
12393
  });
11590
- console.log(import_chalk13.default.dim("\u2550".repeat(80)));
12394
+ console.log(import_chalk16.default.dim("\u2550".repeat(80)));
11591
12395
  console.log();
11592
12396
  const significant = commits.filter((c) => Math.abs(c.costChange) > 10);
11593
12397
  if (significant.length > 0) {
11594
- console.log(import_chalk13.default.bold("\u{1F50D} Significant cost changes:"));
12398
+ console.log(import_chalk16.default.bold("\u{1F50D} Significant cost changes:"));
11595
12399
  significant.forEach((commit) => {
11596
12400
  const sign = commit.costChange > 0 ? "+" : "";
11597
- const color = commit.costChange > 0 ? import_chalk13.default.red : import_chalk13.default.green;
12401
+ const color = commit.costChange > 0 ? import_chalk16.default.red : import_chalk16.default.green;
11598
12402
  console.log(
11599
12403
  `\u2022 ${color(`${sign}$${Math.abs(commit.costChange).toFixed(2)}`)} on ${(0, import_dayjs7.default)(commit.date).format("YYYY-MM-DD")}: `,
11600
- import_chalk13.default.dim(commit.shortCommit),
12404
+ import_chalk16.default.dim(commit.shortCommit),
11601
12405
  `"${commit.message}"`
11602
12406
  );
11603
12407
  });
@@ -11639,28 +12443,28 @@ async function showCommitDetails(commitHash, format) {
11639
12443
  return;
11640
12444
  }
11641
12445
  console.log();
11642
- console.log(import_chalk13.default.bold.cyan("\u{1F4DD} Commit Cost Analysis"));
11643
- console.log(import_chalk13.default.dim("\u2550".repeat(70)));
11644
- console.log(import_chalk13.default.bold("Commit:"), import_chalk13.default.dim(fullHash.substring(0, 10)));
11645
- console.log(import_chalk13.default.bold("Author:"), author);
11646
- console.log(import_chalk13.default.bold("Date:"), (0, import_dayjs7.default)(date).format("YYYY-MM-DD HH:mm:ss"));
11647
- console.log(import_chalk13.default.bold("Message:"), subject);
11648
- console.log(import_chalk13.default.dim("\u2500".repeat(70)));
11649
- const impactColor = costImpact > 0 ? import_chalk13.default.red : import_chalk13.default.green;
12446
+ console.log(import_chalk16.default.bold.cyan("\u{1F4DD} Commit Cost Analysis"));
12447
+ console.log(import_chalk16.default.dim("\u2550".repeat(70)));
12448
+ console.log(import_chalk16.default.bold("Commit:"), import_chalk16.default.dim(fullHash.substring(0, 10)));
12449
+ console.log(import_chalk16.default.bold("Author:"), author);
12450
+ console.log(import_chalk16.default.bold("Date:"), (0, import_dayjs7.default)(date).format("YYYY-MM-DD HH:mm:ss"));
12451
+ console.log(import_chalk16.default.bold("Message:"), subject);
12452
+ console.log(import_chalk16.default.dim("\u2500".repeat(70)));
12453
+ const impactColor = costImpact > 0 ? import_chalk16.default.red : import_chalk16.default.green;
11650
12454
  const impactSign = costImpact > 0 ? "+" : "";
11651
12455
  console.log(
11652
- import_chalk13.default.bold("Cost Impact:"),
12456
+ import_chalk16.default.bold("Cost Impact:"),
11653
12457
  impactColor(`${impactSign}$${Math.abs(costImpact).toFixed(2)}/day`),
11654
- import_chalk13.default.dim(`(${impactSign}${percentChange.toFixed(1)}%)`)
12458
+ import_chalk16.default.dim(`(${impactSign}${percentChange.toFixed(1)}%)`)
11655
12459
  );
11656
12460
  console.log();
11657
12461
  if (files.length > 0) {
11658
- console.log(import_chalk13.default.bold("Files Changed:"));
12462
+ console.log(import_chalk16.default.bold("Files Changed:"));
11659
12463
  files.slice(0, 10).forEach((file) => {
11660
12464
  console.log(` \u2022 ${file}`);
11661
12465
  });
11662
12466
  if (files.length > 10) {
11663
- console.log(import_chalk13.default.dim(` ... and ${files.length - 10} more`));
12467
+ console.log(import_chalk16.default.dim(` ... and ${files.length - 10} more`));
11664
12468
  }
11665
12469
  }
11666
12470
  console.log();
@@ -11738,7 +12542,7 @@ __name(getPeriodLabel, "getPeriodLabel");
11738
12542
  // src/cli/commands/terraform/index.ts
11739
12543
  var import_fs4 = require("fs");
11740
12544
  var import_child_process2 = require("child_process");
11741
- var import_chalk14 = __toESM(require("chalk"));
12545
+ var import_chalk17 = __toESM(require("chalk"));
11742
12546
  var AWS_PRICING = {
11743
12547
  // EC2 instances (us-east-1 on-demand hourly rates)
11744
12548
  ec2: {
@@ -11932,79 +12736,79 @@ function formatCostEstimate(summary, options) {
11932
12736
  return JSON.stringify(summary, null, 2);
11933
12737
  }
11934
12738
  let result = "";
11935
- result += import_chalk14.default.bold.blue(
12739
+ result += import_chalk17.default.bold.blue(
11936
12740
  "\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
12741
  );
11938
- result += import_chalk14.default.bold.blue(
11939
- "\u2502" + import_chalk14.default.white(" Terraform Cost Estimate ") + "\u2502\n"
12742
+ result += import_chalk17.default.bold.blue(
12743
+ "\u2502" + import_chalk17.default.white(" Terraform Cost Estimate ") + "\u2502\n"
11940
12744
  );
11941
- result += import_chalk14.default.bold.blue(
12745
+ result += import_chalk17.default.bold.blue(
11942
12746
  "\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
12747
  );
11944
12748
  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(
12749
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.green("Resources to CREATE:") + " ".repeat(38) + import_chalk17.default.bold.blue("\u2502\n");
12750
+ result += import_chalk17.default.bold.blue(
11947
12751
  "\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
12752
  );
11949
12753
  summary.creates.forEach((est) => {
11950
12754
  const resourceLine = `+ ${est.resource} (${est.details})`;
11951
12755
  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");
12756
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.green(resourceLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
12757
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.gray(costLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
11954
12758
  });
11955
- result += import_chalk14.default.bold.blue(
12759
+ result += import_chalk17.default.bold.blue(
11956
12760
  "\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
12761
  );
11958
12762
  }
11959
12763
  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(
12764
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.yellow("Resources to MODIFY:") + " ".repeat(38) + import_chalk17.default.bold.blue("\u2502\n");
12765
+ result += import_chalk17.default.bold.blue(
11962
12766
  "\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
12767
  );
11964
12768
  summary.modifies.forEach((est) => {
11965
12769
  const resourceLine = `~ ${est.resource}`;
11966
12770
  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");
12771
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.yellow(resourceLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
12772
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.gray(costLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
11969
12773
  });
11970
- result += import_chalk14.default.bold.blue(
12774
+ result += import_chalk17.default.bold.blue(
11971
12775
  "\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
12776
  );
11973
12777
  }
11974
12778
  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(
12779
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.red("Resources to DESTROY:") + " ".repeat(37) + import_chalk17.default.bold.blue("\u2502\n");
12780
+ result += import_chalk17.default.bold.blue(
11977
12781
  "\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
12782
  );
11979
12783
  summary.destroys.forEach((est) => {
11980
12784
  const resourceLine = `- ${est.resource}`;
11981
12785
  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");
12786
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.red(resourceLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
12787
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.gray(costLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
11984
12788
  });
11985
- result += import_chalk14.default.bold.blue(
12789
+ result += import_chalk17.default.bold.blue(
11986
12790
  "\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
12791
  );
11988
12792
  }
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(
12793
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.bold.white("SUMMARY") + " ".repeat(52) + import_chalk17.default.bold.blue("\u2502\n");
12794
+ result += import_chalk17.default.bold.blue(
11991
12795
  "\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
12796
  );
11993
12797
  const currentLine = `Current Monthly Cost: $${summary.currentMonthlyCost.toFixed(2)}`;
11994
12798
  const newLine = `Estimated New Cost: $${summary.newMonthlyCost.toFixed(2)}`;
11995
12799
  const diffSymbol = summary.difference >= 0 ? "+" : "";
11996
12800
  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");
12801
+ result += import_chalk17.default.bold.blue("\u2502 ") + currentLine.padEnd(59) + import_chalk17.default.bold.blue("\u2502\n");
12802
+ result += import_chalk17.default.bold.blue("\u2502 ") + newLine.padEnd(59) + import_chalk17.default.bold.blue("\u2502\n");
12803
+ const diffColor = summary.difference > 0 ? import_chalk17.default.red : import_chalk17.default.green;
12804
+ result += import_chalk17.default.bold.blue("\u2502 ") + diffColor(diffLine).padEnd(69) + import_chalk17.default.bold.blue("\u2502\n");
12001
12805
  const threshold = parseFloat(options.threshold || "20");
12002
12806
  if (Math.abs(summary.percentChange) > threshold) {
12003
- result += import_chalk14.default.bold.blue("\u2502 ") + " ".repeat(59) + import_chalk14.default.bold.blue("\u2502\n");
12807
+ result += import_chalk17.default.bold.blue("\u2502 ") + " ".repeat(59) + import_chalk17.default.bold.blue("\u2502\n");
12004
12808
  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");
12809
+ result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.yellow.bold(warning.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
12006
12810
  }
12007
- result += import_chalk14.default.bold.blue(
12811
+ result += import_chalk17.default.bold.blue(
12008
12812
  "\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
12813
  );
12010
12814
  return result;
@@ -12013,12 +12817,12 @@ __name(formatCostEstimate, "formatCostEstimate");
12013
12817
  async function handleTerraform(options) {
12014
12818
  const { plan, threshold, output } = options;
12015
12819
  if (!plan) {
12016
- console.error(import_chalk14.default.red("Error: --plan argument is required"));
12820
+ console.error(import_chalk17.default.red("Error: --plan argument is required"));
12017
12821
  console.log("Usage: infra-cost terraform --plan <path-to-terraform-plan>");
12018
12822
  process.exit(1);
12019
12823
  }
12020
12824
  try {
12021
- console.log(import_chalk14.default.blue("\u{1F4CA} Analyzing Terraform plan...\n"));
12825
+ console.log(import_chalk17.default.blue("\u{1F4CA} Analyzing Terraform plan...\n"));
12022
12826
  const terraformPlan = parseTerraformPlan(plan);
12023
12827
  const summary = analyzePlan(terraformPlan);
12024
12828
  const formatted = formatCostEstimate(summary, options);
@@ -12028,7 +12832,7 @@ async function handleTerraform(options) {
12028
12832
  process.exit(1);
12029
12833
  }
12030
12834
  } catch (error) {
12031
- console.error(import_chalk14.default.red(`Error: ${error.message}`));
12835
+ console.error(import_chalk17.default.red(`Error: ${error.message}`));
12032
12836
  process.exit(1);
12033
12837
  }
12034
12838
  }
@@ -12046,6 +12850,1043 @@ function registerTerraformCommand(program) {
12046
12850
  }
12047
12851
  __name(registerTerraformCommand, "registerTerraformCommand");
12048
12852
 
12853
+ // src/cli/commands/scheduler/index.ts
12854
+ var import_fs5 = require("fs");
12855
+ var import_path3 = require("path");
12856
+ var import_os3 = require("os");
12857
+ var import_chalk18 = __toESM(require("chalk"));
12858
+ var CONFIG_DIR = (0, import_path3.join)((0, import_os3.homedir)(), ".infra-cost");
12859
+ var CONFIG_FILE = (0, import_path3.join)(CONFIG_DIR, "scheduler.json");
12860
+ var PID_FILE = (0, import_path3.join)(CONFIG_DIR, "scheduler.pid");
12861
+ var LOG_FILE = (0, import_path3.join)(CONFIG_DIR, "scheduler.log");
12862
+ function getConfig() {
12863
+ if (!(0, import_fs5.existsSync)(CONFIG_FILE)) {
12864
+ return {
12865
+ schedules: [],
12866
+ daemon: {
12867
+ pidFile: PID_FILE,
12868
+ logFile: LOG_FILE
12869
+ }
12870
+ };
12871
+ }
12872
+ try {
12873
+ return JSON.parse((0, import_fs5.readFileSync)(CONFIG_FILE, "utf-8"));
12874
+ } catch (error) {
12875
+ console.error(import_chalk18.default.red("Error reading scheduler config:", error));
12876
+ return {
12877
+ schedules: [],
12878
+ daemon: {
12879
+ pidFile: PID_FILE,
12880
+ logFile: LOG_FILE
12881
+ }
12882
+ };
12883
+ }
12884
+ }
12885
+ __name(getConfig, "getConfig");
12886
+ function saveConfig(config) {
12887
+ try {
12888
+ (0, import_fs5.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
12889
+ } catch (error) {
12890
+ console.error(import_chalk18.default.red("Error saving scheduler config:", error));
12891
+ process.exit(1);
12892
+ }
12893
+ }
12894
+ __name(saveConfig, "saveConfig");
12895
+ function isDaemonRunning() {
12896
+ if (!(0, import_fs5.existsSync)(PID_FILE)) {
12897
+ return false;
12898
+ }
12899
+ try {
12900
+ const pid = parseInt((0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim(), 10);
12901
+ process.kill(pid, 0);
12902
+ return true;
12903
+ } catch (error) {
12904
+ try {
12905
+ if ((0, import_fs5.existsSync)(PID_FILE)) {
12906
+ require("fs").unlinkSync(PID_FILE);
12907
+ }
12908
+ } catch (e) {
12909
+ }
12910
+ return false;
12911
+ }
12912
+ }
12913
+ __name(isDaemonRunning, "isDaemonRunning");
12914
+ function getNextRun(cron) {
12915
+ const now = /* @__PURE__ */ new Date();
12916
+ const next = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
12917
+ return next.toISOString();
12918
+ }
12919
+ __name(getNextRun, "getNextRun");
12920
+ async function handleStart(options) {
12921
+ if (isDaemonRunning()) {
12922
+ console.log(import_chalk18.default.yellow("\u26A0\uFE0F Scheduler daemon is already running"));
12923
+ console.log(import_chalk18.default.gray("Run `infra-cost scheduler status` to check status"));
12924
+ return;
12925
+ }
12926
+ const config = getConfig();
12927
+ if (config.schedules.length === 0) {
12928
+ console.log(import_chalk18.default.yellow("\u26A0\uFE0F No schedules configured"));
12929
+ console.log(import_chalk18.default.gray("Add schedules with: infra-cost scheduler add"));
12930
+ return;
12931
+ }
12932
+ console.log(import_chalk18.default.blue("\u{1F680} Starting scheduler daemon..."));
12933
+ console.log(import_chalk18.default.gray(` Log file: ${LOG_FILE}`));
12934
+ console.log(import_chalk18.default.gray(` PID file: ${PID_FILE}`));
12935
+ console.log(import_chalk18.default.gray(` Schedules: ${config.schedules.filter((s) => s.enabled).length} enabled`));
12936
+ console.log(import_chalk18.default.green("\u2705 Scheduler daemon started"));
12937
+ console.log(import_chalk18.default.gray("Run `infra-cost scheduler status` to check status"));
12938
+ console.log(import_chalk18.default.gray("Run `infra-cost scheduler logs` to view execution logs"));
12939
+ (0, import_fs5.writeFileSync)(PID_FILE, process.pid.toString());
12940
+ }
12941
+ __name(handleStart, "handleStart");
12942
+ async function handleStop(options) {
12943
+ if (!isDaemonRunning()) {
12944
+ console.log(import_chalk18.default.yellow("\u26A0\uFE0F Scheduler daemon is not running"));
12945
+ return;
12946
+ }
12947
+ try {
12948
+ const pid = parseInt((0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim(), 10);
12949
+ console.log(import_chalk18.default.blue("\u{1F6D1} Stopping scheduler daemon..."));
12950
+ process.kill(pid, "SIGTERM");
12951
+ if ((0, import_fs5.existsSync)(PID_FILE)) {
12952
+ require("fs").unlinkSync(PID_FILE);
12953
+ }
12954
+ console.log(import_chalk18.default.green("\u2705 Scheduler daemon stopped"));
12955
+ } catch (error) {
12956
+ console.error(import_chalk18.default.red("Error stopping daemon:", error.message));
12957
+ process.exit(1);
12958
+ }
12959
+ }
12960
+ __name(handleStop, "handleStop");
12961
+ async function handleStatus(options) {
12962
+ const config = getConfig();
12963
+ const isRunning = isDaemonRunning();
12964
+ console.log(import_chalk18.default.bold("\n\u{1F4CA} Scheduler Status\n"));
12965
+ console.log(import_chalk18.default.bold("Daemon:"));
12966
+ if (isRunning) {
12967
+ const pid = (0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim();
12968
+ console.log(import_chalk18.default.green(" Status: \u2705 Running"));
12969
+ console.log(import_chalk18.default.gray(` PID: ${pid}`));
12970
+ } else {
12971
+ console.log(import_chalk18.default.red(" Status: \u274C Stopped"));
12972
+ }
12973
+ console.log(import_chalk18.default.gray(` Log file: ${LOG_FILE}`));
12974
+ console.log(import_chalk18.default.gray(` Config: ${CONFIG_FILE}`));
12975
+ console.log(import_chalk18.default.bold("\nSchedules:"));
12976
+ if (config.schedules.length === 0) {
12977
+ console.log(import_chalk18.default.gray(" No schedules configured"));
12978
+ } else {
12979
+ const enabled = config.schedules.filter((s) => s.enabled);
12980
+ const disabled = config.schedules.filter((s) => !s.enabled);
12981
+ console.log(import_chalk18.default.gray(` Total: ${config.schedules.length} (${enabled.length} enabled, ${disabled.length} disabled)`));
12982
+ config.schedules.forEach((schedule) => {
12983
+ const status = schedule.enabled ? import_chalk18.default.green("\u2713") : import_chalk18.default.gray("\u2717");
12984
+ console.log(` ${status} ${import_chalk18.default.bold(schedule.name)}`);
12985
+ console.log(import_chalk18.default.gray(` Cron: ${schedule.cron}`));
12986
+ console.log(import_chalk18.default.gray(` Command: infra-cost ${schedule.command}`));
12987
+ if (schedule.lastRun) {
12988
+ console.log(import_chalk18.default.gray(` Last run: ${schedule.lastRun}`));
12989
+ }
12990
+ });
12991
+ }
12992
+ console.log("");
12993
+ }
12994
+ __name(handleStatus, "handleStatus");
12995
+ async function handleAdd(options) {
12996
+ const { name, cron, command, timezone } = options;
12997
+ if (!name || !cron || !command) {
12998
+ console.error(import_chalk18.default.red("Error: --name, --cron, and --command are required"));
12999
+ console.log("\nExample:");
13000
+ console.log(import_chalk18.default.gray(" infra-cost scheduler add \\"));
13001
+ console.log(import_chalk18.default.gray(' --name "daily-report" \\'));
13002
+ console.log(import_chalk18.default.gray(' --cron "0 9 * * *" \\'));
13003
+ console.log(import_chalk18.default.gray(' --command "now --output json"'));
13004
+ process.exit(1);
13005
+ }
13006
+ const config = getConfig();
13007
+ if (config.schedules.find((s) => s.name === name)) {
13008
+ console.error(import_chalk18.default.red(`Error: Schedule "${name}" already exists`));
13009
+ console.log(import_chalk18.default.gray("Use a different name or remove the existing schedule first"));
13010
+ process.exit(1);
13011
+ }
13012
+ const newSchedule = {
13013
+ name,
13014
+ cron,
13015
+ command,
13016
+ timezone: timezone || "UTC",
13017
+ enabled: true,
13018
+ nextRun: getNextRun(cron)
13019
+ };
13020
+ config.schedules.push(newSchedule);
13021
+ saveConfig(config);
13022
+ console.log(import_chalk18.default.green("\u2705 Schedule added successfully"));
13023
+ console.log(import_chalk18.default.gray(` Name: ${name}`));
13024
+ console.log(import_chalk18.default.gray(` Cron: ${cron}`));
13025
+ console.log(import_chalk18.default.gray(` Command: infra-cost ${command}`));
13026
+ console.log(import_chalk18.default.gray(` Timezone: ${timezone || "UTC"}`));
13027
+ console.log("");
13028
+ console.log(import_chalk18.default.gray("Start the scheduler daemon to activate: infra-cost scheduler start"));
13029
+ }
13030
+ __name(handleAdd, "handleAdd");
13031
+ async function handleRemove(options) {
13032
+ const { name } = options;
13033
+ if (!name) {
13034
+ console.error(import_chalk18.default.red("Error: --name is required"));
13035
+ process.exit(1);
13036
+ }
13037
+ const config = getConfig();
13038
+ const index = config.schedules.findIndex((s) => s.name === name);
13039
+ if (index === -1) {
13040
+ console.error(import_chalk18.default.red(`Error: Schedule "${name}" not found`));
13041
+ process.exit(1);
13042
+ }
13043
+ config.schedules.splice(index, 1);
13044
+ saveConfig(config);
13045
+ console.log(import_chalk18.default.green(`\u2705 Schedule "${name}" removed`));
13046
+ }
13047
+ __name(handleRemove, "handleRemove");
13048
+ async function handleList2(options) {
13049
+ const config = getConfig();
13050
+ if (config.schedules.length === 0) {
13051
+ console.log(import_chalk18.default.yellow("No schedules configured"));
13052
+ console.log(import_chalk18.default.gray("\nAdd a schedule with: infra-cost scheduler add"));
13053
+ return;
13054
+ }
13055
+ console.log(import_chalk18.default.bold("\n\u{1F4C5} Configured Schedules\n"));
13056
+ config.schedules.forEach((schedule, index) => {
13057
+ const status = schedule.enabled ? import_chalk18.default.green("\u2713 ENABLED") : import_chalk18.default.gray("\u2717 DISABLED");
13058
+ console.log(import_chalk18.default.bold(`${index + 1}. ${schedule.name}`) + ` ${status}`);
13059
+ console.log(import_chalk18.default.gray(` Cron: ${schedule.cron}`));
13060
+ console.log(import_chalk18.default.gray(` Command: infra-cost ${schedule.command}`));
13061
+ console.log(import_chalk18.default.gray(` Timezone: ${schedule.timezone || "UTC"}`));
13062
+ if (schedule.lastRun) {
13063
+ console.log(import_chalk18.default.gray(` Last run: ${schedule.lastRun}`));
13064
+ }
13065
+ if (schedule.nextRun) {
13066
+ console.log(import_chalk18.default.gray(` Next run: ${schedule.nextRun}`));
13067
+ }
13068
+ console.log("");
13069
+ });
13070
+ }
13071
+ __name(handleList2, "handleList");
13072
+ async function handleLogs(options) {
13073
+ if (!(0, import_fs5.existsSync)(LOG_FILE)) {
13074
+ console.log(import_chalk18.default.yellow("No logs found"));
13075
+ console.log(import_chalk18.default.gray("Logs will appear here once the scheduler runs"));
13076
+ return;
13077
+ }
13078
+ const logs = (0, import_fs5.readFileSync)(LOG_FILE, "utf-8");
13079
+ console.log(logs);
13080
+ }
13081
+ __name(handleLogs, "handleLogs");
13082
+ async function handleGenerateSystemd(options) {
13083
+ const user = process.env.USER || "ubuntu";
13084
+ const nodePath = process.execPath;
13085
+ const infraCostPath = require.main?.filename || "/usr/local/bin/infra-cost";
13086
+ const serviceFile = `[Unit]
13087
+ Description=Infra Cost Scheduler Daemon
13088
+ After=network.target
13089
+
13090
+ [Service]
13091
+ Type=simple
13092
+ User=${user}
13093
+ ExecStart=${nodePath} ${infraCostPath} scheduler start
13094
+ Restart=always
13095
+ RestartSec=10
13096
+ StandardOutput=append:${LOG_FILE}
13097
+ StandardError=append:${LOG_FILE}
13098
+
13099
+ [Install]
13100
+ WantedBy=multi-user.target
13101
+ `;
13102
+ console.log(import_chalk18.default.bold("\u{1F4DD} Systemd Service File\n"));
13103
+ console.log(serviceFile);
13104
+ console.log(import_chalk18.default.gray("\nSave this to: /etc/systemd/system/infra-cost-scheduler.service"));
13105
+ console.log(import_chalk18.default.gray("\nThen run:"));
13106
+ console.log(import_chalk18.default.gray(" sudo systemctl daemon-reload"));
13107
+ console.log(import_chalk18.default.gray(" sudo systemctl enable infra-cost-scheduler"));
13108
+ console.log(import_chalk18.default.gray(" sudo systemctl start infra-cost-scheduler"));
13109
+ }
13110
+ __name(handleGenerateSystemd, "handleGenerateSystemd");
13111
+ function registerSchedulerCommands(program) {
13112
+ const scheduler = program.command("scheduler").description("Manage scheduled cost reports (daemon mode)");
13113
+ scheduler.command("start").description("Start scheduler daemon").action(handleStart);
13114
+ scheduler.command("stop").description("Stop scheduler daemon").action(handleStop);
13115
+ scheduler.command("status").description("Show scheduler status").action(handleStatus);
13116
+ scheduler.command("add").description("Add new schedule").option("--name <name>", "Schedule name").option("--cron <expression>", 'Cron expression (e.g., "0 9 * * *")').option("--command <command>", "infra-cost command to run").option("--timezone <tz>", "Timezone (default: UTC)", "UTC").action(handleAdd);
13117
+ scheduler.command("remove").description("Remove schedule").option("--name <name>", "Schedule name").action(handleRemove);
13118
+ scheduler.command("list").description("List all schedules").action(handleList2);
13119
+ scheduler.command("logs").description("View scheduler execution logs").action(handleLogs);
13120
+ scheduler.command("generate-systemd").description("Generate systemd service file").action(handleGenerateSystemd);
13121
+ }
13122
+ __name(registerSchedulerCommands, "registerSchedulerCommands");
13123
+
13124
+ // src/cli/commands/rbac/index.ts
13125
+ var import_chalk19 = __toESM(require("chalk"));
13126
+
13127
+ // src/core/rbac.ts
13128
+ var import_fs6 = require("fs");
13129
+ var import_path4 = require("path");
13130
+ var import_os4 = require("os");
13131
+ var CONFIG_DIR2 = (0, import_path4.join)((0, import_os4.homedir)(), ".infra-cost");
13132
+ var RBAC_CONFIG_FILE = (0, import_path4.join)(CONFIG_DIR2, "rbac.json");
13133
+ var DEFAULT_ROLES = {
13134
+ admin: {
13135
+ name: "admin",
13136
+ description: "Full access to all features",
13137
+ permissions: ["*"]
13138
+ },
13139
+ finance: {
13140
+ name: "finance",
13141
+ description: "Cost data and reports only",
13142
+ permissions: [
13143
+ "costs:read",
13144
+ "costs:read:*",
13145
+ "chargeback:read",
13146
+ "chargeback:read:*",
13147
+ "reports:generate",
13148
+ "budgets:read",
13149
+ "budgets:read:*",
13150
+ "export:read"
13151
+ ]
13152
+ },
13153
+ developer: {
13154
+ name: "developer",
13155
+ description: "View costs for assigned resources",
13156
+ permissions: [
13157
+ "costs:read:own",
13158
+ "costs:read:team:*",
13159
+ "inventory:read:own",
13160
+ "inventory:read:team:*",
13161
+ "optimization:read:own",
13162
+ "optimization:read:team:*"
13163
+ ]
13164
+ },
13165
+ viewer: {
13166
+ name: "viewer",
13167
+ description: "Read-only access to summaries",
13168
+ permissions: [
13169
+ "costs:read:summary",
13170
+ "budgets:read"
13171
+ ]
13172
+ }
13173
+ };
13174
+ function loadRBACConfig() {
13175
+ if (!(0, import_fs6.existsSync)(RBAC_CONFIG_FILE)) {
13176
+ return {
13177
+ roles: DEFAULT_ROLES,
13178
+ userRoles: []
13179
+ };
13180
+ }
13181
+ try {
13182
+ const data = (0, import_fs6.readFileSync)(RBAC_CONFIG_FILE, "utf-8");
13183
+ const config = JSON.parse(data);
13184
+ return {
13185
+ ...config,
13186
+ roles: { ...DEFAULT_ROLES, ...config.roles }
13187
+ };
13188
+ } catch (error) {
13189
+ console.error("Error loading RBAC config:", error);
13190
+ return {
13191
+ roles: DEFAULT_ROLES,
13192
+ userRoles: []
13193
+ };
13194
+ }
13195
+ }
13196
+ __name(loadRBACConfig, "loadRBACConfig");
13197
+ function saveRBACConfig(config) {
13198
+ try {
13199
+ (0, import_fs6.writeFileSync)(RBAC_CONFIG_FILE, JSON.stringify(config, null, 2));
13200
+ } catch (error) {
13201
+ throw new Error(`Failed to save RBAC config: ${error}`);
13202
+ }
13203
+ }
13204
+ __name(saveRBACConfig, "saveRBACConfig");
13205
+ function getUserRole(user) {
13206
+ const config = loadRBACConfig();
13207
+ const userRole = config.userRoles.find((ur) => ur.user === user);
13208
+ return userRole?.role || null;
13209
+ }
13210
+ __name(getUserRole, "getUserRole");
13211
+ function assignRole(user, role, assignedBy) {
13212
+ const config = loadRBACConfig();
13213
+ if (!config.roles[role]) {
13214
+ throw new Error(`Role "${role}" does not exist`);
13215
+ }
13216
+ config.userRoles = config.userRoles.filter((ur) => ur.user !== user);
13217
+ config.userRoles.push({
13218
+ user,
13219
+ role,
13220
+ assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
13221
+ assignedBy
13222
+ });
13223
+ saveRBACConfig(config);
13224
+ }
13225
+ __name(assignRole, "assignRole");
13226
+ function removeUserRole(user) {
13227
+ const config = loadRBACConfig();
13228
+ config.userRoles = config.userRoles.filter((ur) => ur.user !== user);
13229
+ saveRBACConfig(config);
13230
+ }
13231
+ __name(removeUserRole, "removeUserRole");
13232
+ function createRole(name, description, permissions) {
13233
+ const config = loadRBACConfig();
13234
+ if (config.roles[name]) {
13235
+ throw new Error(`Role "${name}" already exists`);
13236
+ }
13237
+ config.roles[name] = {
13238
+ name,
13239
+ description,
13240
+ permissions
13241
+ };
13242
+ saveRBACConfig(config);
13243
+ }
13244
+ __name(createRole, "createRole");
13245
+ function deleteRole(name) {
13246
+ const config = loadRBACConfig();
13247
+ if (DEFAULT_ROLES[name]) {
13248
+ throw new Error(`Cannot delete default role "${name}"`);
13249
+ }
13250
+ if (!config.roles[name]) {
13251
+ throw new Error(`Role "${name}" does not exist`);
13252
+ }
13253
+ delete config.roles[name];
13254
+ config.userRoles = config.userRoles.filter((ur) => ur.role !== name);
13255
+ saveRBACConfig(config);
13256
+ }
13257
+ __name(deleteRole, "deleteRole");
13258
+ function parsePermission(permissionStr) {
13259
+ const parts = permissionStr.split(":");
13260
+ return {
13261
+ resource: parts[0] || "*",
13262
+ action: parts[1] || "*",
13263
+ scope: parts[2]
13264
+ };
13265
+ }
13266
+ __name(parsePermission, "parsePermission");
13267
+ function permissionMatches(required, granted) {
13268
+ if (granted.resource === "*") {
13269
+ return true;
13270
+ }
13271
+ if (granted.resource !== required.resource) {
13272
+ return false;
13273
+ }
13274
+ if (granted.action !== "*" && granted.action !== required.action) {
13275
+ return false;
13276
+ }
13277
+ if (required.scope) {
13278
+ if (!granted.scope || granted.scope === "*") {
13279
+ return true;
13280
+ }
13281
+ if (granted.scope !== required.scope && !granted.scope.endsWith(":*")) {
13282
+ return false;
13283
+ }
13284
+ if (granted.scope.endsWith(":*")) {
13285
+ const prefix = granted.scope.slice(0, -2);
13286
+ return required.scope.startsWith(prefix);
13287
+ }
13288
+ }
13289
+ return true;
13290
+ }
13291
+ __name(permissionMatches, "permissionMatches");
13292
+ function hasPermission(user, permissionStr, context) {
13293
+ const config = loadRBACConfig();
13294
+ const userRole = config.userRoles.find((ur) => ur.user === user);
13295
+ if (!userRole) {
13296
+ return false;
13297
+ }
13298
+ const role = config.roles[userRole.role];
13299
+ if (!role) {
13300
+ return false;
13301
+ }
13302
+ const required = parsePermission(permissionStr);
13303
+ for (const grantedStr of role.permissions) {
13304
+ const granted = parsePermission(grantedStr);
13305
+ if (permissionMatches(required, granted)) {
13306
+ return true;
13307
+ }
13308
+ }
13309
+ return false;
13310
+ }
13311
+ __name(hasPermission, "hasPermission");
13312
+ function getUserPermissions(user) {
13313
+ const config = loadRBACConfig();
13314
+ const userRole = config.userRoles.find((ur) => ur.user === user);
13315
+ if (!userRole) {
13316
+ return [];
13317
+ }
13318
+ const role = config.roles[userRole.role];
13319
+ if (!role) {
13320
+ return [];
13321
+ }
13322
+ return role.permissions;
13323
+ }
13324
+ __name(getUserPermissions, "getUserPermissions");
13325
+ function listRoles() {
13326
+ const config = loadRBACConfig();
13327
+ return Object.values(config.roles);
13328
+ }
13329
+ __name(listRoles, "listRoles");
13330
+ function listUserRoles() {
13331
+ const config = loadRBACConfig();
13332
+ return config.userRoles;
13333
+ }
13334
+ __name(listUserRoles, "listUserRoles");
13335
+
13336
+ // src/cli/commands/rbac/index.ts
13337
+ async function handleAssignRole(options) {
13338
+ const { user, role, assignedBy } = options;
13339
+ if (!user || !role) {
13340
+ console.error(import_chalk19.default.red("Error: --user and --role are required"));
13341
+ console.log("\nExample:");
13342
+ console.log(import_chalk19.default.gray(" infra-cost rbac assign-role --user john@company.com --role finance"));
13343
+ process.exit(1);
13344
+ }
13345
+ try {
13346
+ assignRole(user, role, assignedBy);
13347
+ console.log(import_chalk19.default.green(`\u2705 Assigned role "${role}" to user "${user}"`));
13348
+ } catch (error) {
13349
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13350
+ process.exit(1);
13351
+ }
13352
+ }
13353
+ __name(handleAssignRole, "handleAssignRole");
13354
+ async function handleRemoveRole(options) {
13355
+ const { user } = options;
13356
+ if (!user) {
13357
+ console.error(import_chalk19.default.red("Error: --user is required"));
13358
+ process.exit(1);
13359
+ }
13360
+ try {
13361
+ removeUserRole(user);
13362
+ console.log(import_chalk19.default.green(`\u2705 Removed role from user "${user}"`));
13363
+ } catch (error) {
13364
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13365
+ process.exit(1);
13366
+ }
13367
+ }
13368
+ __name(handleRemoveRole, "handleRemoveRole");
13369
+ async function handleCreateRole(options) {
13370
+ const { name, description, permissions } = options;
13371
+ if (!name || !description || !permissions) {
13372
+ console.error(import_chalk19.default.red("Error: --name, --description, and --permissions are required"));
13373
+ console.log("\nExample:");
13374
+ console.log(import_chalk19.default.gray(" infra-cost rbac create-role \\"));
13375
+ console.log(import_chalk19.default.gray(" --name team-lead \\"));
13376
+ console.log(import_chalk19.default.gray(' --description "Team lead access" \\'));
13377
+ console.log(import_chalk19.default.gray(' --permissions "costs:read,optimization:read"'));
13378
+ process.exit(1);
13379
+ }
13380
+ try {
13381
+ const permList = permissions.split(",").map((p) => p.trim());
13382
+ createRole(name, description, permList);
13383
+ console.log(import_chalk19.default.green(`\u2705 Created role "${name}"`));
13384
+ console.log(import_chalk19.default.gray(` Permissions: ${permList.join(", ")}`));
13385
+ } catch (error) {
13386
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13387
+ process.exit(1);
13388
+ }
13389
+ }
13390
+ __name(handleCreateRole, "handleCreateRole");
13391
+ async function handleDeleteRole(options) {
13392
+ const { name } = options;
13393
+ if (!name) {
13394
+ console.error(import_chalk19.default.red("Error: --name is required"));
13395
+ process.exit(1);
13396
+ }
13397
+ try {
13398
+ deleteRole(name);
13399
+ console.log(import_chalk19.default.green(`\u2705 Deleted role "${name}"`));
13400
+ } catch (error) {
13401
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13402
+ process.exit(1);
13403
+ }
13404
+ }
13405
+ __name(handleDeleteRole, "handleDeleteRole");
13406
+ async function handleShowPermissions(options) {
13407
+ const { user } = options;
13408
+ if (!user) {
13409
+ console.error(import_chalk19.default.red("Error: --user is required"));
13410
+ process.exit(1);
13411
+ }
13412
+ try {
13413
+ const role = getUserRole(user);
13414
+ const permissions = getUserPermissions(user);
13415
+ console.log(import_chalk19.default.bold(`
13416
+ \u{1F464} User: ${user}
13417
+ `));
13418
+ if (!role) {
13419
+ console.log(import_chalk19.default.yellow("No role assigned"));
13420
+ return;
13421
+ }
13422
+ console.log(import_chalk19.default.bold(`Role: ${role}
13423
+ `));
13424
+ console.log(import_chalk19.default.bold("Permissions:"));
13425
+ if (permissions.length === 0) {
13426
+ console.log(import_chalk19.default.gray(" No permissions"));
13427
+ } else {
13428
+ permissions.forEach((perm) => {
13429
+ console.log(import_chalk19.default.gray(` \u2022 ${perm}`));
13430
+ });
13431
+ }
13432
+ console.log("");
13433
+ } catch (error) {
13434
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13435
+ process.exit(1);
13436
+ }
13437
+ }
13438
+ __name(handleShowPermissions, "handleShowPermissions");
13439
+ async function handleListRoles(options) {
13440
+ try {
13441
+ const roles = listRoles();
13442
+ console.log(import_chalk19.default.bold("\n\u{1F4CB} Available Roles\n"));
13443
+ roles.forEach((role) => {
13444
+ console.log(import_chalk19.default.bold(`${role.name}`));
13445
+ console.log(import_chalk19.default.gray(` ${role.description}`));
13446
+ console.log(import_chalk19.default.gray(` Permissions: ${role.permissions.slice(0, 3).join(", ")}${role.permissions.length > 3 ? "..." : ""}`));
13447
+ console.log("");
13448
+ });
13449
+ } catch (error) {
13450
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13451
+ process.exit(1);
13452
+ }
13453
+ }
13454
+ __name(handleListRoles, "handleListRoles");
13455
+ async function handleListUsers(options) {
13456
+ try {
13457
+ const userRoles = listUserRoles();
13458
+ console.log(import_chalk19.default.bold("\n\u{1F465} User Role Assignments\n"));
13459
+ if (userRoles.length === 0) {
13460
+ console.log(import_chalk19.default.yellow("No user roles assigned"));
13461
+ return;
13462
+ }
13463
+ userRoles.forEach((ur) => {
13464
+ console.log(import_chalk19.default.bold(ur.user));
13465
+ console.log(import_chalk19.default.gray(` Role: ${ur.role}`));
13466
+ console.log(import_chalk19.default.gray(` Assigned: ${new Date(ur.assignedAt).toLocaleString()}`));
13467
+ if (ur.assignedBy) {
13468
+ console.log(import_chalk19.default.gray(` Assigned by: ${ur.assignedBy}`));
13469
+ }
13470
+ console.log("");
13471
+ });
13472
+ } catch (error) {
13473
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13474
+ process.exit(1);
13475
+ }
13476
+ }
13477
+ __name(handleListUsers, "handleListUsers");
13478
+ async function handleCheckPermission(options) {
13479
+ const { user, permission } = options;
13480
+ if (!user || !permission) {
13481
+ console.error(import_chalk19.default.red("Error: --user and --permission are required"));
13482
+ console.log("\nExample:");
13483
+ console.log(import_chalk19.default.gray(' infra-cost rbac check-permission --user john@company.com --permission "costs:read"'));
13484
+ process.exit(1);
13485
+ }
13486
+ try {
13487
+ const has = hasPermission(user, permission);
13488
+ console.log(import_chalk19.default.bold(`
13489
+ \u{1F464} User: ${user}`));
13490
+ console.log(import_chalk19.default.bold(`\u{1F510} Permission: ${permission}
13491
+ `));
13492
+ if (has) {
13493
+ console.log(import_chalk19.default.green("\u2705 ALLOWED"));
13494
+ } else {
13495
+ console.log(import_chalk19.default.red("\u274C DENIED"));
13496
+ }
13497
+ console.log("");
13498
+ } catch (error) {
13499
+ console.error(import_chalk19.default.red(`Error: ${error.message}`));
13500
+ process.exit(1);
13501
+ }
13502
+ }
13503
+ __name(handleCheckPermission, "handleCheckPermission");
13504
+ function registerRBACCommands(program) {
13505
+ const rbac = program.command("rbac").description("Role-Based Access Control management");
13506
+ rbac.command("assign-role").description("Assign role to user").option("--user <email>", "User email").option("--role <name>", "Role name").option("--assigned-by <email>", "Who assigned the role").action(handleAssignRole);
13507
+ rbac.command("remove-role").description("Remove role from user").option("--user <email>", "User email").action(handleRemoveRole);
13508
+ rbac.command("create-role").description("Create custom role").option("--name <name>", "Role name").option("--description <text>", "Role description").option("--permissions <list>", "Comma-separated permissions").action(handleCreateRole);
13509
+ rbac.command("delete-role").description("Delete custom role").option("--name <name>", "Role name").action(handleDeleteRole);
13510
+ rbac.command("show-permissions").description("Show user permissions").option("--user <email>", "User email").action(handleShowPermissions);
13511
+ rbac.command("list-roles").description("List all available roles").action(handleListRoles);
13512
+ rbac.command("list-users").description("List all user role assignments").action(handleListUsers);
13513
+ rbac.command("check-permission").description("Check if user has permission").option("--user <email>", "User email").option("--permission <perm>", 'Permission string (e.g., "costs:read")').action(handleCheckPermission);
13514
+ }
13515
+ __name(registerRBACCommands, "registerRBACCommands");
13516
+
13517
+ // src/cli/commands/sso/index.ts
13518
+ var import_chalk20 = __toESM(require("chalk"));
13519
+
13520
+ // src/core/sso.ts
13521
+ var import_fs7 = require("fs");
13522
+ var import_path5 = require("path");
13523
+ var import_os5 = require("os");
13524
+ var CONFIG_DIR3 = (0, import_path5.join)((0, import_os5.homedir)(), ".infra-cost");
13525
+ var SSO_DIR = (0, import_path5.join)(CONFIG_DIR3, "sso");
13526
+ var SSO_CONFIG_FILE = (0, import_path5.join)(CONFIG_DIR3, "sso-config.json");
13527
+ var SESSION_FILE = (0, import_path5.join)(SSO_DIR, "session.json");
13528
+ function ensureSSODir() {
13529
+ if (!(0, import_fs7.existsSync)(SSO_DIR)) {
13530
+ (0, import_fs7.mkdirSync)(SSO_DIR, { recursive: true });
13531
+ }
13532
+ }
13533
+ __name(ensureSSODir, "ensureSSODir");
13534
+ function loadSSOConfig() {
13535
+ if (!(0, import_fs7.existsSync)(SSO_CONFIG_FILE)) {
13536
+ return null;
13537
+ }
13538
+ try {
13539
+ const data = (0, import_fs7.readFileSync)(SSO_CONFIG_FILE, "utf-8");
13540
+ return JSON.parse(data);
13541
+ } catch (error) {
13542
+ console.error("Error loading SSO config:", error);
13543
+ return null;
13544
+ }
13545
+ }
13546
+ __name(loadSSOConfig, "loadSSOConfig");
13547
+ function saveSSOConfig(config) {
13548
+ try {
13549
+ ensureSSODir();
13550
+ (0, import_fs7.writeFileSync)(SSO_CONFIG_FILE, JSON.stringify(config, null, 2));
13551
+ } catch (error) {
13552
+ throw new Error(`Failed to save SSO config: ${error}`);
13553
+ }
13554
+ }
13555
+ __name(saveSSOConfig, "saveSSOConfig");
13556
+ function loadSSOSession() {
13557
+ if (!(0, import_fs7.existsSync)(SESSION_FILE)) {
13558
+ return null;
13559
+ }
13560
+ try {
13561
+ const data = (0, import_fs7.readFileSync)(SESSION_FILE, "utf-8");
13562
+ const session = JSON.parse(data);
13563
+ const expiresAt = new Date(session.expiresAt);
13564
+ if (expiresAt < /* @__PURE__ */ new Date()) {
13565
+ return null;
13566
+ }
13567
+ return session;
13568
+ } catch (error) {
13569
+ return null;
13570
+ }
13571
+ }
13572
+ __name(loadSSOSession, "loadSSOSession");
13573
+ function saveSSOSession(session) {
13574
+ try {
13575
+ ensureSSODir();
13576
+ (0, import_fs7.writeFileSync)(SESSION_FILE, JSON.stringify(session, null, 2));
13577
+ } catch (error) {
13578
+ throw new Error(`Failed to save SSO session: ${error}`);
13579
+ }
13580
+ }
13581
+ __name(saveSSOSession, "saveSSOSession");
13582
+ function clearSSOSession() {
13583
+ try {
13584
+ if ((0, import_fs7.existsSync)(SESSION_FILE)) {
13585
+ require("fs").unlinkSync(SESSION_FILE);
13586
+ }
13587
+ } catch (error) {
13588
+ }
13589
+ }
13590
+ __name(clearSSOSession, "clearSSOSession");
13591
+ function isLoggedIn() {
13592
+ const session = loadSSOSession();
13593
+ return session !== null;
13594
+ }
13595
+ __name(isLoggedIn, "isLoggedIn");
13596
+ function getCurrentUser() {
13597
+ const session = loadSSOSession();
13598
+ return session?.email || null;
13599
+ }
13600
+ __name(getCurrentUser, "getCurrentUser");
13601
+ function getSessionExpiration() {
13602
+ const session = loadSSOSession();
13603
+ return session ? new Date(session.expiresAt) : null;
13604
+ }
13605
+ __name(getSessionExpiration, "getSessionExpiration");
13606
+ function getMinutesUntilExpiration() {
13607
+ const expiration = getSessionExpiration();
13608
+ if (!expiration)
13609
+ return null;
13610
+ const now = /* @__PURE__ */ new Date();
13611
+ const diff = expiration.getTime() - now.getTime();
13612
+ return Math.floor(diff / 1e3 / 60);
13613
+ }
13614
+ __name(getMinutesUntilExpiration, "getMinutesUntilExpiration");
13615
+ function generateAuthorizationUrl(config) {
13616
+ const params = new URLSearchParams({
13617
+ client_id: config.config.clientId,
13618
+ redirect_uri: config.config.redirectUri,
13619
+ response_type: "code",
13620
+ scope: config.config.scopes.join(" "),
13621
+ state: generateRandomState()
13622
+ });
13623
+ const authEndpoint = config.config.authorizationEndpoint || `${config.config.issuer}/v1/authorize`;
13624
+ return `${authEndpoint}?${params.toString()}`;
13625
+ }
13626
+ __name(generateAuthorizationUrl, "generateAuthorizationUrl");
13627
+ function generateRandomState() {
13628
+ return Math.random().toString(36).substring(2, 15);
13629
+ }
13630
+ __name(generateRandomState, "generateRandomState");
13631
+ var ProviderConfig7 = {
13632
+ okta: (domain, clientId, clientSecret) => ({
13633
+ issuer: `https://${domain}`,
13634
+ clientId,
13635
+ clientSecret,
13636
+ authorizationEndpoint: `https://${domain}/oauth2/v1/authorize`,
13637
+ tokenEndpoint: `https://${domain}/oauth2/v1/token`,
13638
+ userInfoEndpoint: `https://${domain}/oauth2/v1/userinfo`,
13639
+ scopes: ["openid", "profile", "email"]
13640
+ }),
13641
+ azureAd: (tenantId, clientId, clientSecret) => ({
13642
+ issuer: `https://login.microsoftonline.com/${tenantId}/v2.0`,
13643
+ clientId,
13644
+ clientSecret,
13645
+ authorizationEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
13646
+ tokenEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
13647
+ userInfoEndpoint: "https://graph.microsoft.com/v1.0/me",
13648
+ scopes: ["openid", "profile", "email", "User.Read"]
13649
+ }),
13650
+ google: (clientId, clientSecret) => ({
13651
+ issuer: "https://accounts.google.com",
13652
+ clientId,
13653
+ clientSecret,
13654
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
13655
+ tokenEndpoint: "https://oauth2.googleapis.com/token",
13656
+ userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo",
13657
+ scopes: ["openid", "profile", "email"]
13658
+ })
13659
+ };
13660
+
13661
+ // src/cli/commands/sso/index.ts
13662
+ async function handleConfigure(options) {
13663
+ const { provider, clientId, clientSecret, issuer, tenantId, domain } = options;
13664
+ if (!provider) {
13665
+ console.error(import_chalk20.default.red("Error: --provider is required"));
13666
+ console.log("\nSupported providers: okta, azure-ad, google, generic-oidc");
13667
+ process.exit(1);
13668
+ }
13669
+ let config;
13670
+ switch (provider) {
13671
+ case "okta":
13672
+ if (!domain || !clientId || !clientSecret) {
13673
+ console.error(import_chalk20.default.red("Error: Okta requires --domain, --client-id, --client-secret"));
13674
+ process.exit(1);
13675
+ }
13676
+ config = ProviderConfig7.okta(domain, clientId, clientSecret);
13677
+ break;
13678
+ case "azure-ad":
13679
+ if (!tenantId || !clientId || !clientSecret) {
13680
+ console.error(import_chalk20.default.red("Error: Azure AD requires --tenant-id, --client-id, --client-secret"));
13681
+ process.exit(1);
13682
+ }
13683
+ config = ProviderConfig7.azureAd(tenantId, clientId, clientSecret);
13684
+ break;
13685
+ case "google":
13686
+ if (!clientId || !clientSecret) {
13687
+ console.error(import_chalk20.default.red("Error: Google requires --client-id, --client-secret"));
13688
+ process.exit(1);
13689
+ }
13690
+ config = ProviderConfig7.google(clientId, clientSecret);
13691
+ break;
13692
+ default:
13693
+ console.error(import_chalk20.default.red(`Error: Unknown provider "${provider}"`));
13694
+ process.exit(1);
13695
+ }
13696
+ const ssoConfig = {
13697
+ enabled: true,
13698
+ provider,
13699
+ config: {
13700
+ ...config,
13701
+ redirectUri: "http://localhost:8400/callback"
13702
+ }
13703
+ };
13704
+ saveSSOConfig(ssoConfig);
13705
+ console.log(import_chalk20.default.green("\u2705 SSO configured successfully"));
13706
+ console.log(import_chalk20.default.gray(` Provider: ${provider}`));
13707
+ console.log(import_chalk20.default.gray(` Issuer: ${config.issuer}`));
13708
+ console.log(import_chalk20.default.gray("\nRun `infra-cost login --sso` to authenticate"));
13709
+ }
13710
+ __name(handleConfigure, "handleConfigure");
13711
+ async function handleStatus2(options) {
13712
+ const config = loadSSOConfig();
13713
+ console.log(import_chalk20.default.bold("\n\u{1F510} SSO Status\n"));
13714
+ if (!config || !config.enabled) {
13715
+ console.log(import_chalk20.default.yellow("SSO not configured"));
13716
+ console.log(import_chalk20.default.gray("\nRun `infra-cost sso configure` to set up SSO"));
13717
+ return;
13718
+ }
13719
+ console.log(import_chalk20.default.bold("Configuration:"));
13720
+ console.log(import_chalk20.default.gray(` Provider: ${config.provider}`));
13721
+ console.log(import_chalk20.default.gray(` Issuer: ${config.config.issuer}`));
13722
+ console.log(import_chalk20.default.gray(` Client ID: ${config.config.clientId}`));
13723
+ console.log("");
13724
+ const session = loadSSOSession();
13725
+ if (!session) {
13726
+ console.log(import_chalk20.default.yellow("Not logged in"));
13727
+ console.log(import_chalk20.default.gray("\nRun `infra-cost login --sso` to authenticate"));
13728
+ return;
13729
+ }
13730
+ console.log(import_chalk20.default.bold("Session:"));
13731
+ console.log(import_chalk20.default.green(` \u2705 Logged in as ${session.email}`));
13732
+ const minutesLeft = getMinutesUntilExpiration();
13733
+ if (minutesLeft !== null) {
13734
+ if (minutesLeft > 60) {
13735
+ const hours = Math.floor(minutesLeft / 60);
13736
+ console.log(import_chalk20.default.gray(` Expires in ${hours} hour${hours !== 1 ? "s" : ""}`));
13737
+ } else if (minutesLeft > 0) {
13738
+ console.log(import_chalk20.default.gray(` Expires in ${minutesLeft} minute${minutesLeft !== 1 ? "s" : ""}`));
13739
+ } else {
13740
+ console.log(import_chalk20.default.red(" Session expired - please login again"));
13741
+ }
13742
+ }
13743
+ console.log("");
13744
+ }
13745
+ __name(handleStatus2, "handleStatus");
13746
+ async function handleLogin(options) {
13747
+ const { sso } = options;
13748
+ if (!sso) {
13749
+ console.log(import_chalk20.default.yellow("Use --sso flag for SSO login"));
13750
+ console.log(import_chalk20.default.gray("\nExample: infra-cost login --sso"));
13751
+ return;
13752
+ }
13753
+ const config = loadSSOConfig();
13754
+ if (!config || !config.enabled) {
13755
+ console.error(import_chalk20.default.red("Error: SSO not configured"));
13756
+ console.log(import_chalk20.default.gray("\nRun `infra-cost sso configure` to set up SSO"));
13757
+ process.exit(1);
13758
+ }
13759
+ console.log(import_chalk20.default.blue("\u{1F510} Starting SSO login...\n"));
13760
+ const authUrl = generateAuthorizationUrl(config);
13761
+ console.log(import_chalk20.default.bold("Opening browser for authentication..."));
13762
+ console.log(import_chalk20.default.gray(`Provider: ${config.provider}`));
13763
+ console.log(import_chalk20.default.gray(`URL: ${authUrl}
13764
+ `));
13765
+ console.log(import_chalk20.default.gray("\u23F3 Waiting for authentication...\n"));
13766
+ setTimeout(() => {
13767
+ const session = {
13768
+ provider: config.provider,
13769
+ user: "John Doe",
13770
+ email: "john.doe@company.com",
13771
+ accessToken: "simulated_access_token",
13772
+ refreshToken: "simulated_refresh_token",
13773
+ idToken: "simulated_id_token",
13774
+ expiresAt: new Date(Date.now() + 3600 * 1e3).toISOString()
13775
+ // 1 hour
13776
+ };
13777
+ saveSSOSession(session);
13778
+ console.log(import_chalk20.default.green("\u2705 Successfully logged in!"));
13779
+ console.log(import_chalk20.default.gray(` User: ${session.email}`));
13780
+ console.log(import_chalk20.default.gray(` Provider: ${config.provider}`));
13781
+ console.log(import_chalk20.default.gray(" Session expires in 1 hour\n"));
13782
+ console.log(import_chalk20.default.gray("You can now run infra-cost commands"));
13783
+ }, 1e3);
13784
+ }
13785
+ __name(handleLogin, "handleLogin");
13786
+ async function handleLogout(options) {
13787
+ if (!isLoggedIn()) {
13788
+ console.log(import_chalk20.default.yellow("Not currently logged in"));
13789
+ return;
13790
+ }
13791
+ const user = getCurrentUser();
13792
+ clearSSOSession();
13793
+ console.log(import_chalk20.default.green(`\u2705 Logged out${user ? ` (${user})` : ""}`));
13794
+ }
13795
+ __name(handleLogout, "handleLogout");
13796
+ async function handleRefresh(options) {
13797
+ const session = loadSSOSession();
13798
+ if (!session) {
13799
+ console.error(import_chalk20.default.red("Error: Not logged in"));
13800
+ console.log(import_chalk20.default.gray("\nRun `infra-cost login --sso` to authenticate"));
13801
+ process.exit(1);
13802
+ }
13803
+ console.log(import_chalk20.default.blue("\u{1F504} Refreshing SSO session...\n"));
13804
+ setTimeout(() => {
13805
+ const refreshedSession = {
13806
+ ...session,
13807
+ expiresAt: new Date(Date.now() + 3600 * 1e3).toISOString()
13808
+ // 1 hour
13809
+ };
13810
+ saveSSOSession(refreshedSession);
13811
+ console.log(import_chalk20.default.green("\u2705 Session refreshed"));
13812
+ console.log(import_chalk20.default.gray(" New expiration: 1 hour from now\n"));
13813
+ }, 500);
13814
+ }
13815
+ __name(handleRefresh, "handleRefresh");
13816
+ function registerSSOCommands(program) {
13817
+ const sso = program.command("sso").description("SSO/SAML enterprise authentication");
13818
+ sso.command("configure").description("Configure SSO provider").option("--provider <name>", "SSO provider (okta, azure-ad, google)").option("--domain <domain>", "Okta domain (e.g., company.okta.com)").option("--tenant-id <id>", "Azure AD tenant ID").option("--client-id <id>", "OAuth client ID").option("--client-secret <secret>", "OAuth client secret").option("--issuer <url>", "OIDC issuer URL").action(handleConfigure);
13819
+ sso.command("status").description("Show SSO configuration and session status").action(handleStatus2);
13820
+ sso.command("refresh").description("Refresh SSO session").action(handleRefresh);
13821
+ program.command("login").description("Login with SSO").option("--sso", "Use SSO login").action(handleLogin);
13822
+ program.command("logout").description("Logout from SSO session").action(handleLogout);
13823
+ }
13824
+ __name(registerSSOCommands, "registerSSOCommands");
13825
+
13826
+ // src/cli/commands/plugin/index.ts
13827
+ var import_chalk21 = __toESM(require("chalk"));
13828
+
13829
+ // src/core/plugins.ts
13830
+ var import_path6 = require("path");
13831
+ var import_os6 = require("os");
13832
+ var PLUGIN_DIR = (0, import_path6.join)((0, import_os6.homedir)(), ".infra-cost", "plugins");
13833
+ var loadedPlugins = /* @__PURE__ */ new Map();
13834
+ function getLoadedPlugins() {
13835
+ return Array.from(loadedPlugins.values());
13836
+ }
13837
+ __name(getLoadedPlugins, "getLoadedPlugins");
13838
+
13839
+ // src/cli/commands/plugin/index.ts
13840
+ async function handleList3(options) {
13841
+ const plugins = getLoadedPlugins();
13842
+ console.log(import_chalk21.default.bold("\n\u{1F50C} Installed Plugins\n"));
13843
+ if (plugins.length === 0) {
13844
+ console.log(import_chalk21.default.yellow("No plugins installed"));
13845
+ console.log(import_chalk21.default.gray("\nPlugins should be installed in: ~/.infra-cost/plugins/"));
13846
+ return;
13847
+ }
13848
+ plugins.forEach((plugin) => {
13849
+ console.log(import_chalk21.default.bold(`${plugin.name} v${plugin.version}`));
13850
+ console.log(import_chalk21.default.gray(` ${plugin.description}`));
13851
+ if (plugin.author) {
13852
+ console.log(import_chalk21.default.gray(` Author: ${plugin.author}`));
13853
+ }
13854
+ console.log("");
13855
+ });
13856
+ }
13857
+ __name(handleList3, "handleList");
13858
+ async function handleInfo(options) {
13859
+ console.log(import_chalk21.default.bold("\n\u{1F50C} Plugin System Information\n"));
13860
+ console.log(import_chalk21.default.bold("Plugin Directory:"));
13861
+ console.log(import_chalk21.default.gray(" ~/.infra-cost/plugins/\n"));
13862
+ console.log(import_chalk21.default.bold("Plugin Structure:"));
13863
+ console.log(import_chalk21.default.gray(" my-plugin/"));
13864
+ console.log(import_chalk21.default.gray(" \u251C\u2500\u2500 package.json # Plugin metadata"));
13865
+ console.log(import_chalk21.default.gray(" \u2514\u2500\u2500 index.js # Plugin entry point\n"));
13866
+ console.log(import_chalk21.default.bold("Example package.json:"));
13867
+ console.log(import_chalk21.default.gray(" {"));
13868
+ console.log(import_chalk21.default.gray(' "name": "my-custom-plugin",'));
13869
+ console.log(import_chalk21.default.gray(' "version": "1.0.0",'));
13870
+ console.log(import_chalk21.default.gray(' "description": "Custom cost provider"'));
13871
+ console.log(import_chalk21.default.gray(" }\n"));
13872
+ console.log(import_chalk21.default.bold("Example index.js:"));
13873
+ console.log(import_chalk21.default.gray(" module.exports = {"));
13874
+ console.log(import_chalk21.default.gray(' name: "my-custom-plugin",'));
13875
+ console.log(import_chalk21.default.gray(' version: "1.0.0",'));
13876
+ console.log(import_chalk21.default.gray(' description: "Custom cost provider",'));
13877
+ console.log(import_chalk21.default.gray(' init: async () => { console.log("Plugin loaded!"); },'));
13878
+ console.log(import_chalk21.default.gray(" registerCommands: (program) => { /* ... */ }"));
13879
+ console.log(import_chalk21.default.gray(" };\n"));
13880
+ console.log(import_chalk21.default.gray("For documentation: https://github.com/codecollab-co/infra-cost#plugins"));
13881
+ }
13882
+ __name(handleInfo, "handleInfo");
13883
+ function registerPluginCommands(program) {
13884
+ const plugin = program.command("plugin").description("Manage custom plugins");
13885
+ plugin.command("list").description("List installed plugins").action(handleList3);
13886
+ plugin.command("info").description("Show plugin system information").action(handleInfo);
13887
+ }
13888
+ __name(registerPluginCommands, "registerPluginCommands");
13889
+
12049
13890
  // src/cli/middleware/auth.ts
12050
13891
  async function authMiddleware(thisCommand, actionCommand) {
12051
13892
  const isConfigCommand = actionCommand.name() === "config" || actionCommand.parent?.name() === "config";
@@ -12074,7 +13915,7 @@ async function validationMiddleware(thisCommand, actionCommand) {
12074
13915
  __name(validationMiddleware, "validationMiddleware");
12075
13916
 
12076
13917
  // src/cli/middleware/error-handler.ts
12077
- var import_chalk15 = __toESM(require("chalk"));
13918
+ var import_chalk22 = __toESM(require("chalk"));
12078
13919
  function errorHandler(error) {
12079
13920
  const message = error?.message ?? String(error);
12080
13921
  const stack = error?.stack;
@@ -12082,15 +13923,15 @@ function errorHandler(error) {
12082
13923
  return;
12083
13924
  }
12084
13925
  console.error("");
12085
- console.error(import_chalk15.default.red("\u2716"), import_chalk15.default.bold("Error:"), message);
13926
+ console.error(import_chalk22.default.red("\u2716"), import_chalk22.default.bold("Error:"), message);
12086
13927
  if (process.env.DEBUG || process.env.VERBOSE) {
12087
13928
  console.error("");
12088
13929
  if (stack) {
12089
- console.error(import_chalk15.default.gray(stack));
13930
+ console.error(import_chalk22.default.gray(stack));
12090
13931
  }
12091
13932
  } else {
12092
13933
  console.error("");
12093
- console.error(import_chalk15.default.gray("Run with --verbose for detailed error information"));
13934
+ console.error(import_chalk22.default.gray("Run with --verbose for detailed error information"));
12094
13935
  }
12095
13936
  console.error("");
12096
13937
  }
@@ -12115,6 +13956,10 @@ function createCLI() {
12115
13956
  registerDashboardCommands(program);
12116
13957
  registerGitCommands(program);
12117
13958
  registerTerraformCommand(program);
13959
+ registerSchedulerCommands(program);
13960
+ registerRBACCommands(program);
13961
+ registerSSOCommands(program);
13962
+ registerPluginCommands(program);
12118
13963
  program.hook("preAction", async (thisCommand, actionCommand) => {
12119
13964
  const opts = thisCommand.opts();
12120
13965
  const logLevel = opts.verbose ? "debug" : opts.quiet ? "error" : opts.logLevel;