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/README.md +27 -0
- package/dist/cli/index.js +1992 -147
- package/dist/index.js +1992 -147
- package/package.json +1 -1
package/dist/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(
|
|
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
|
-
|
|
8669
|
-
|
|
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(
|
|
9271
|
+
console.log(import_chalk12.default.dim("\u2550".repeat(70)));
|
|
8672
9272
|
console.log();
|
|
8673
|
-
console.log(
|
|
9273
|
+
console.log(import_chalk12.default.bold("\u{1F4B0} Total Costs"));
|
|
8674
9274
|
console.log(
|
|
8675
|
-
`\u251C\u2500\u2500 Today: ${
|
|
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: ${
|
|
9278
|
+
`\u251C\u2500\u2500 Weekly Total: ${import_chalk12.default.cyan("$" + report.weeklyTotal.toFixed(2))}`
|
|
8679
9279
|
);
|
|
8680
|
-
console.log(`\u2514\u2500\u2500 Active Accounts: ${
|
|
9280
|
+
console.log(`\u2514\u2500\u2500 Active Accounts: ${import_chalk12.default.yellow(report.accountsCount)}`);
|
|
8681
9281
|
console.log();
|
|
8682
|
-
console.log(
|
|
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" ?
|
|
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} ${
|
|
9288
|
+
`${prefix} ${trendIcon} ${import_chalk12.default.bold(account.accountName)} ${import_chalk12.default.dim(`(${account.accountId})`)}`
|
|
8689
9289
|
);
|
|
8690
9290
|
console.log(
|
|
8691
|
-
` Today: ${
|
|
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(
|
|
8696
|
-
console.log(
|
|
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)} ${
|
|
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(
|
|
8777
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
8786
|
-
|
|
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 ${
|
|
9389
|
+
`Add this to your crontab (run ${import_chalk12.default.cyan("crontab -e")}):
|
|
8790
9390
|
`
|
|
8791
9391
|
);
|
|
8792
9392
|
console.log(
|
|
8793
|
-
|
|
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
|
-
|
|
8800
|
-
|
|
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
|
-
|
|
8812
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
9015
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
9837
|
+
console.log(import_chalk14.default.bold("\u{1F4CB} Migration Preview:"));
|
|
9050
9838
|
console.log("");
|
|
9051
|
-
console.log(
|
|
9052
|
-
console.log(
|
|
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(
|
|
9842
|
+
console.log(import_chalk14.default.gray(" ... (truncated)"));
|
|
9055
9843
|
}
|
|
9056
9844
|
console.log("");
|
|
9057
|
-
console.log(
|
|
9058
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
9856
|
+
console.log(import_chalk14.default.green("\u2713"), `Configuration migrated: ${oldConfigPath}`);
|
|
9069
9857
|
console.log("");
|
|
9070
9858
|
}
|
|
9071
9859
|
} catch (error) {
|
|
9072
|
-
console.error(
|
|
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(
|
|
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",
|
|
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(
|
|
9091
|
-
console.log(
|
|
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(
|
|
9136
|
-
console.log(
|
|
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(`${
|
|
9140
|
-
console.log(
|
|
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(
|
|
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(
|
|
9148
|
-
console.log("Run",
|
|
9149
|
-
console.log("Example:",
|
|
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,
|
|
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
|
-
|
|
9965
|
+
import_chalk14 = __toESM(require("chalk"));
|
|
9178
9966
|
init_logging();
|
|
9179
9967
|
init_schema();
|
|
9180
9968
|
CLI_MIGRATION_MAP = {
|
|
@@ -9426,7 +10214,7 @@ var import_commander = require("commander");
|
|
|
9426
10214
|
// package.json
|
|
9427
10215
|
var package_default = {
|
|
9428
10216
|
name: "infra-cost",
|
|
9429
|
-
version: "1.
|
|
10217
|
+
version: "1.6.0",
|
|
9430
10218
|
description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
|
|
9431
10219
|
keywords: [
|
|
9432
10220
|
"aws",
|
|
@@ -11095,6 +11883,14 @@ function registerMonitorCommands(program) {
|
|
|
11095
11883
|
const { handleBudgets: handleBudgets2 } = await Promise.resolve().then(() => (init_budgets(), budgets_exports));
|
|
11096
11884
|
await handleBudgets2(options, command);
|
|
11097
11885
|
});
|
|
11886
|
+
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) => {
|
|
11887
|
+
const { handlePagerDuty: handlePagerDuty2 } = await Promise.resolve().then(() => (init_alert(), alert_exports));
|
|
11888
|
+
await handlePagerDuty2(options);
|
|
11889
|
+
});
|
|
11890
|
+
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) => {
|
|
11891
|
+
const { handleOpsGenie: handleOpsGenie2 } = await Promise.resolve().then(() => (init_alert(), alert_exports));
|
|
11892
|
+
await handleOpsGenie2(options);
|
|
11893
|
+
});
|
|
11098
11894
|
}
|
|
11099
11895
|
__name(registerMonitorCommands, "registerMonitorCommands");
|
|
11100
11896
|
|
|
@@ -11113,6 +11909,10 @@ function registerExportCommands(program) {
|
|
|
11113
11909
|
const { handleReports: handleReports2 } = await Promise.resolve().then(() => (init_reports(), reports_exports));
|
|
11114
11910
|
await handleReports2(format, options, command);
|
|
11115
11911
|
});
|
|
11912
|
+
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) => {
|
|
11913
|
+
const { handleEmail: handleEmail2 } = await Promise.resolve().then(() => (init_email2(), email_exports));
|
|
11914
|
+
await handleEmail2(options);
|
|
11915
|
+
});
|
|
11116
11916
|
}
|
|
11117
11917
|
__name(registerExportCommands, "registerExportCommands");
|
|
11118
11918
|
|
|
@@ -11120,8 +11920,8 @@ __name(registerExportCommands, "registerExportCommands");
|
|
|
11120
11920
|
function registerOrganizationsCommands(program) {
|
|
11121
11921
|
const orgs = program.command("organizations").alias("orgs").description("AWS Organizations multi-account management");
|
|
11122
11922
|
orgs.command("list").description("List all accounts in the organization").option("--include-inactive", "Include suspended/closed accounts").action(async (options, command) => {
|
|
11123
|
-
const { handleList:
|
|
11124
|
-
await
|
|
11923
|
+
const { handleList: handleList4 } = await Promise.resolve().then(() => (init_list(), list_exports));
|
|
11924
|
+
await handleList4(options, command);
|
|
11125
11925
|
});
|
|
11126
11926
|
orgs.command("summary").description("Multi-account cost summary").option("--group-by <field>", "Group by (account, ou, tag)", "account").action(async (options, command) => {
|
|
11127
11927
|
const { handleSummary: handleSummary2 } = await Promise.resolve().then(() => (init_summary(), summary_exports));
|
|
@@ -11149,6 +11949,10 @@ function registerChargebackCommands(program) {
|
|
|
11149
11949
|
const { handleSlack: handleSlack2 } = await Promise.resolve().then(() => (init_slack(), slack_exports));
|
|
11150
11950
|
await handleSlack2(options, command);
|
|
11151
11951
|
});
|
|
11952
|
+
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) => {
|
|
11953
|
+
const { handleTeams: handleTeams2 } = await Promise.resolve().then(() => (init_teams2(), teams_exports));
|
|
11954
|
+
await handleTeams2(options, command);
|
|
11955
|
+
});
|
|
11152
11956
|
}
|
|
11153
11957
|
__name(registerChargebackCommands, "registerChargebackCommands");
|
|
11154
11958
|
|
|
@@ -11195,7 +11999,7 @@ __name(registerDashboardCommands, "registerDashboardCommands");
|
|
|
11195
11999
|
// src/cli/commands/annotate/index.ts
|
|
11196
12000
|
var fs3 = __toESM(require("fs/promises"));
|
|
11197
12001
|
var path3 = __toESM(require("path"));
|
|
11198
|
-
var
|
|
12002
|
+
var import_chalk15 = __toESM(require("chalk"));
|
|
11199
12003
|
function registerAnnotateCommand(program) {
|
|
11200
12004
|
program.command("annotate").description("Add cost annotations to Infrastructure as Code files").option("--path <path>", "Path to IaC files or directory", "./terraform").option(
|
|
11201
12005
|
"--format <format>",
|
|
@@ -11205,7 +12009,7 @@ function registerAnnotateCommand(program) {
|
|
|
11205
12009
|
try {
|
|
11206
12010
|
await handleAnnotate(options);
|
|
11207
12011
|
} catch (error) {
|
|
11208
|
-
console.error(
|
|
12012
|
+
console.error(import_chalk15.default.red("Error:"), error.message);
|
|
11209
12013
|
process.exit(1);
|
|
11210
12014
|
}
|
|
11211
12015
|
});
|
|
@@ -11220,10 +12024,10 @@ async function handleAnnotate(options) {
|
|
|
11220
12024
|
}
|
|
11221
12025
|
const files = await findIaCFiles(targetPath, options.format);
|
|
11222
12026
|
if (files.length === 0) {
|
|
11223
|
-
console.log(
|
|
12027
|
+
console.log(import_chalk15.default.yellow("No IaC files found at:", targetPath));
|
|
11224
12028
|
return;
|
|
11225
12029
|
}
|
|
11226
|
-
console.log(
|
|
12030
|
+
console.log(import_chalk15.default.cyan(`Found ${files.length} file(s) to annotate`));
|
|
11227
12031
|
console.log();
|
|
11228
12032
|
let annotatedCount = 0;
|
|
11229
12033
|
let skippedCount = 0;
|
|
@@ -11232,29 +12036,29 @@ async function handleAnnotate(options) {
|
|
|
11232
12036
|
if (result.annotated) {
|
|
11233
12037
|
annotatedCount++;
|
|
11234
12038
|
console.log(
|
|
11235
|
-
|
|
12039
|
+
import_chalk15.default.green("\u2713"),
|
|
11236
12040
|
path3.relative(process.cwd(), file),
|
|
11237
|
-
|
|
12041
|
+
import_chalk15.default.dim(`(${result.resourcesAnnotated} resources)`)
|
|
11238
12042
|
);
|
|
11239
12043
|
} else {
|
|
11240
12044
|
skippedCount++;
|
|
11241
12045
|
console.log(
|
|
11242
|
-
|
|
12046
|
+
import_chalk15.default.yellow("\u2298"),
|
|
11243
12047
|
path3.relative(process.cwd(), file),
|
|
11244
|
-
|
|
12048
|
+
import_chalk15.default.dim("(no changes)")
|
|
11245
12049
|
);
|
|
11246
12050
|
}
|
|
11247
12051
|
}
|
|
11248
12052
|
console.log();
|
|
11249
12053
|
console.log(
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
12054
|
+
import_chalk15.default.bold("Summary:"),
|
|
12055
|
+
import_chalk15.default.green(`${annotatedCount} annotated`),
|
|
12056
|
+
import_chalk15.default.yellow(`${skippedCount} skipped`)
|
|
11253
12057
|
);
|
|
11254
12058
|
if (options.dryRun) {
|
|
11255
12059
|
console.log();
|
|
11256
|
-
console.log(
|
|
11257
|
-
console.log(
|
|
12060
|
+
console.log(import_chalk15.default.yellow("\u2139 Dry run - no files were modified"));
|
|
12061
|
+
console.log(import_chalk15.default.dim("Run without --dry-run to apply changes"));
|
|
11258
12062
|
}
|
|
11259
12063
|
}
|
|
11260
12064
|
__name(handleAnnotate, "handleAnnotate");
|
|
@@ -11472,7 +12276,7 @@ __name(estimateResourceCost, "estimateResourceCost");
|
|
|
11472
12276
|
|
|
11473
12277
|
// src/cli/commands/git/index.ts
|
|
11474
12278
|
var import_child_process = require("child_process");
|
|
11475
|
-
var
|
|
12279
|
+
var import_chalk16 = __toESM(require("chalk"));
|
|
11476
12280
|
var import_dayjs7 = __toESM(require("dayjs"));
|
|
11477
12281
|
function registerGitCommands(program) {
|
|
11478
12282
|
const git = program.command("history").description("Show cost history with git correlation");
|
|
@@ -11480,7 +12284,7 @@ function registerGitCommands(program) {
|
|
|
11480
12284
|
try {
|
|
11481
12285
|
await handleHistory(options);
|
|
11482
12286
|
} catch (error) {
|
|
11483
|
-
console.error(
|
|
12287
|
+
console.error(import_chalk16.default.red("Error:"), error.message);
|
|
11484
12288
|
process.exit(1);
|
|
11485
12289
|
}
|
|
11486
12290
|
});
|
|
@@ -11488,7 +12292,7 @@ function registerGitCommands(program) {
|
|
|
11488
12292
|
try {
|
|
11489
12293
|
await handleBlame(options);
|
|
11490
12294
|
} catch (error) {
|
|
11491
|
-
console.error(
|
|
12295
|
+
console.error(import_chalk16.default.red("Error:"), error.message);
|
|
11492
12296
|
process.exit(1);
|
|
11493
12297
|
}
|
|
11494
12298
|
});
|
|
@@ -11529,17 +12333,17 @@ async function handleBlame(options) {
|
|
|
11529
12333
|
return;
|
|
11530
12334
|
}
|
|
11531
12335
|
console.log();
|
|
11532
|
-
console.log(
|
|
11533
|
-
console.log(
|
|
12336
|
+
console.log(import_chalk16.default.bold.cyan(`\u{1F50D} Cost Blame Analysis (${getPeriodLabel(period)})`));
|
|
12337
|
+
console.log(import_chalk16.default.dim("\u2550".repeat(70)));
|
|
11534
12338
|
console.log();
|
|
11535
12339
|
console.log(
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
12340
|
+
import_chalk16.default.bold("Author").padEnd(35),
|
|
12341
|
+
import_chalk16.default.bold("Cost Impact").padEnd(18),
|
|
12342
|
+
import_chalk16.default.bold("Commits")
|
|
11539
12343
|
);
|
|
11540
|
-
console.log(
|
|
12344
|
+
console.log(import_chalk16.default.dim("\u2500".repeat(70)));
|
|
11541
12345
|
sorted.forEach(([email, stats]) => {
|
|
11542
|
-
const impactColor = stats.costImpact > 0 ?
|
|
12346
|
+
const impactColor = stats.costImpact > 0 ? import_chalk16.default.red : import_chalk16.default.green;
|
|
11543
12347
|
const impactSign = stats.costImpact > 0 ? "+" : "";
|
|
11544
12348
|
const emoji = stats.costImpact < 0 ? " \u{1F44F}" : "";
|
|
11545
12349
|
console.log(
|
|
@@ -11548,7 +12352,7 @@ async function handleBlame(options) {
|
|
|
11548
12352
|
stats.commits.toString() + emoji
|
|
11549
12353
|
);
|
|
11550
12354
|
});
|
|
11551
|
-
console.log(
|
|
12355
|
+
console.log(import_chalk16.default.dim("\u2550".repeat(70)));
|
|
11552
12356
|
console.log();
|
|
11553
12357
|
}
|
|
11554
12358
|
__name(handleBlame, "handleBlame");
|
|
@@ -11560,38 +12364,38 @@ async function showCostHistory(options) {
|
|
|
11560
12364
|
return;
|
|
11561
12365
|
}
|
|
11562
12366
|
console.log();
|
|
11563
|
-
console.log(
|
|
11564
|
-
console.log(
|
|
12367
|
+
console.log(import_chalk16.default.bold.cyan("\u{1F4CA} Cost History with Git Correlation"));
|
|
12368
|
+
console.log(import_chalk16.default.dim("\u2550".repeat(80)));
|
|
11565
12369
|
console.log();
|
|
11566
12370
|
console.log(
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
|
|
11570
|
-
|
|
12371
|
+
import_chalk16.default.bold("Date").padEnd(14),
|
|
12372
|
+
import_chalk16.default.bold("Cost").padEnd(12),
|
|
12373
|
+
import_chalk16.default.bold("Change").padEnd(12),
|
|
12374
|
+
import_chalk16.default.bold("Commit")
|
|
11571
12375
|
);
|
|
11572
|
-
console.log(
|
|
12376
|
+
console.log(import_chalk16.default.dim("\u2500".repeat(80)));
|
|
11573
12377
|
commits.forEach((commit) => {
|
|
11574
|
-
const changeColor = commit.costChange > 0 ?
|
|
12378
|
+
const changeColor = commit.costChange > 0 ? import_chalk16.default.red : commit.costChange < 0 ? import_chalk16.default.green : import_chalk16.default.gray;
|
|
11575
12379
|
const changeSign = commit.costChange > 0 ? "+" : "";
|
|
11576
12380
|
console.log(
|
|
11577
12381
|
(0, import_dayjs7.default)(commit.date).format("YYYY-MM-DD").padEnd(14),
|
|
11578
|
-
|
|
12382
|
+
import_chalk16.default.green(`$${commit.cost.toFixed(2)}`).padEnd(12),
|
|
11579
12383
|
changeColor(`${changeSign}$${commit.costChange.toFixed(2)}`).padEnd(12),
|
|
11580
|
-
|
|
12384
|
+
import_chalk16.default.dim(commit.shortCommit),
|
|
11581
12385
|
commit.message.substring(0, 40)
|
|
11582
12386
|
);
|
|
11583
12387
|
});
|
|
11584
|
-
console.log(
|
|
12388
|
+
console.log(import_chalk16.default.dim("\u2550".repeat(80)));
|
|
11585
12389
|
console.log();
|
|
11586
12390
|
const significant = commits.filter((c) => Math.abs(c.costChange) > 10);
|
|
11587
12391
|
if (significant.length > 0) {
|
|
11588
|
-
console.log(
|
|
12392
|
+
console.log(import_chalk16.default.bold("\u{1F50D} Significant cost changes:"));
|
|
11589
12393
|
significant.forEach((commit) => {
|
|
11590
12394
|
const sign = commit.costChange > 0 ? "+" : "";
|
|
11591
|
-
const color = commit.costChange > 0 ?
|
|
12395
|
+
const color = commit.costChange > 0 ? import_chalk16.default.red : import_chalk16.default.green;
|
|
11592
12396
|
console.log(
|
|
11593
12397
|
`\u2022 ${color(`${sign}$${Math.abs(commit.costChange).toFixed(2)}`)} on ${(0, import_dayjs7.default)(commit.date).format("YYYY-MM-DD")}: `,
|
|
11594
|
-
|
|
12398
|
+
import_chalk16.default.dim(commit.shortCommit),
|
|
11595
12399
|
`"${commit.message}"`
|
|
11596
12400
|
);
|
|
11597
12401
|
});
|
|
@@ -11633,28 +12437,28 @@ async function showCommitDetails(commitHash, format) {
|
|
|
11633
12437
|
return;
|
|
11634
12438
|
}
|
|
11635
12439
|
console.log();
|
|
11636
|
-
console.log(
|
|
11637
|
-
console.log(
|
|
11638
|
-
console.log(
|
|
11639
|
-
console.log(
|
|
11640
|
-
console.log(
|
|
11641
|
-
console.log(
|
|
11642
|
-
console.log(
|
|
11643
|
-
const impactColor = costImpact > 0 ?
|
|
12440
|
+
console.log(import_chalk16.default.bold.cyan("\u{1F4DD} Commit Cost Analysis"));
|
|
12441
|
+
console.log(import_chalk16.default.dim("\u2550".repeat(70)));
|
|
12442
|
+
console.log(import_chalk16.default.bold("Commit:"), import_chalk16.default.dim(fullHash.substring(0, 10)));
|
|
12443
|
+
console.log(import_chalk16.default.bold("Author:"), author);
|
|
12444
|
+
console.log(import_chalk16.default.bold("Date:"), (0, import_dayjs7.default)(date).format("YYYY-MM-DD HH:mm:ss"));
|
|
12445
|
+
console.log(import_chalk16.default.bold("Message:"), subject);
|
|
12446
|
+
console.log(import_chalk16.default.dim("\u2500".repeat(70)));
|
|
12447
|
+
const impactColor = costImpact > 0 ? import_chalk16.default.red : import_chalk16.default.green;
|
|
11644
12448
|
const impactSign = costImpact > 0 ? "+" : "";
|
|
11645
12449
|
console.log(
|
|
11646
|
-
|
|
12450
|
+
import_chalk16.default.bold("Cost Impact:"),
|
|
11647
12451
|
impactColor(`${impactSign}$${Math.abs(costImpact).toFixed(2)}/day`),
|
|
11648
|
-
|
|
12452
|
+
import_chalk16.default.dim(`(${impactSign}${percentChange.toFixed(1)}%)`)
|
|
11649
12453
|
);
|
|
11650
12454
|
console.log();
|
|
11651
12455
|
if (files.length > 0) {
|
|
11652
|
-
console.log(
|
|
12456
|
+
console.log(import_chalk16.default.bold("Files Changed:"));
|
|
11653
12457
|
files.slice(0, 10).forEach((file) => {
|
|
11654
12458
|
console.log(` \u2022 ${file}`);
|
|
11655
12459
|
});
|
|
11656
12460
|
if (files.length > 10) {
|
|
11657
|
-
console.log(
|
|
12461
|
+
console.log(import_chalk16.default.dim(` ... and ${files.length - 10} more`));
|
|
11658
12462
|
}
|
|
11659
12463
|
}
|
|
11660
12464
|
console.log();
|
|
@@ -11732,7 +12536,7 @@ __name(getPeriodLabel, "getPeriodLabel");
|
|
|
11732
12536
|
// src/cli/commands/terraform/index.ts
|
|
11733
12537
|
var import_fs4 = require("fs");
|
|
11734
12538
|
var import_child_process2 = require("child_process");
|
|
11735
|
-
var
|
|
12539
|
+
var import_chalk17 = __toESM(require("chalk"));
|
|
11736
12540
|
var AWS_PRICING = {
|
|
11737
12541
|
// EC2 instances (us-east-1 on-demand hourly rates)
|
|
11738
12542
|
ec2: {
|
|
@@ -11926,79 +12730,79 @@ function formatCostEstimate(summary, options) {
|
|
|
11926
12730
|
return JSON.stringify(summary, null, 2);
|
|
11927
12731
|
}
|
|
11928
12732
|
let result = "";
|
|
11929
|
-
result +=
|
|
12733
|
+
result += import_chalk17.default.bold.blue(
|
|
11930
12734
|
"\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E\n"
|
|
11931
12735
|
);
|
|
11932
|
-
result +=
|
|
11933
|
-
"\u2502" +
|
|
12736
|
+
result += import_chalk17.default.bold.blue(
|
|
12737
|
+
"\u2502" + import_chalk17.default.white(" Terraform Cost Estimate ") + "\u2502\n"
|
|
11934
12738
|
);
|
|
11935
|
-
result +=
|
|
12739
|
+
result += import_chalk17.default.bold.blue(
|
|
11936
12740
|
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11937
12741
|
);
|
|
11938
12742
|
if (summary.creates.length > 0) {
|
|
11939
|
-
result +=
|
|
11940
|
-
result +=
|
|
12743
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.green("Resources to CREATE:") + " ".repeat(38) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12744
|
+
result += import_chalk17.default.bold.blue(
|
|
11941
12745
|
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11942
12746
|
);
|
|
11943
12747
|
summary.creates.forEach((est) => {
|
|
11944
12748
|
const resourceLine = `+ ${est.resource} (${est.details})`;
|
|
11945
12749
|
const costLine = ` \u2514\u2500\u2500 Monthly: $${est.monthlyCost.toFixed(2)} | Hourly: $${est.hourlyCost.toFixed(4)}`;
|
|
11946
|
-
result +=
|
|
11947
|
-
result +=
|
|
12750
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.green(resourceLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12751
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.gray(costLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
|
|
11948
12752
|
});
|
|
11949
|
-
result +=
|
|
12753
|
+
result += import_chalk17.default.bold.blue(
|
|
11950
12754
|
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11951
12755
|
);
|
|
11952
12756
|
}
|
|
11953
12757
|
if (summary.modifies.length > 0) {
|
|
11954
|
-
result +=
|
|
11955
|
-
result +=
|
|
12758
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.yellow("Resources to MODIFY:") + " ".repeat(38) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12759
|
+
result += import_chalk17.default.bold.blue(
|
|
11956
12760
|
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11957
12761
|
);
|
|
11958
12762
|
summary.modifies.forEach((est) => {
|
|
11959
12763
|
const resourceLine = `~ ${est.resource}`;
|
|
11960
12764
|
const costLine = ` \u2514\u2500\u2500 ${est.details}: ${est.monthlyCost >= 0 ? "+" : ""}$${est.monthlyCost.toFixed(2)}/month`;
|
|
11961
|
-
result +=
|
|
11962
|
-
result +=
|
|
12765
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.yellow(resourceLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12766
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.gray(costLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
|
|
11963
12767
|
});
|
|
11964
|
-
result +=
|
|
12768
|
+
result += import_chalk17.default.bold.blue(
|
|
11965
12769
|
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11966
12770
|
);
|
|
11967
12771
|
}
|
|
11968
12772
|
if (summary.destroys.length > 0) {
|
|
11969
|
-
result +=
|
|
11970
|
-
result +=
|
|
12773
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.red("Resources to DESTROY:") + " ".repeat(37) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12774
|
+
result += import_chalk17.default.bold.blue(
|
|
11971
12775
|
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11972
12776
|
);
|
|
11973
12777
|
summary.destroys.forEach((est) => {
|
|
11974
12778
|
const resourceLine = `- ${est.resource}`;
|
|
11975
12779
|
const costLine = ` \u2514\u2500\u2500 Savings: $${Math.abs(est.monthlyCost).toFixed(2)}/month`;
|
|
11976
|
-
result +=
|
|
11977
|
-
result +=
|
|
12780
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.red(resourceLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12781
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.gray(costLine.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
|
|
11978
12782
|
});
|
|
11979
|
-
result +=
|
|
12783
|
+
result += import_chalk17.default.bold.blue(
|
|
11980
12784
|
"\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n"
|
|
11981
12785
|
);
|
|
11982
12786
|
}
|
|
11983
|
-
result +=
|
|
11984
|
-
result +=
|
|
12787
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.bold.white("SUMMARY") + " ".repeat(52) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12788
|
+
result += import_chalk17.default.bold.blue(
|
|
11985
12789
|
"\u2502 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2502\n"
|
|
11986
12790
|
);
|
|
11987
12791
|
const currentLine = `Current Monthly Cost: $${summary.currentMonthlyCost.toFixed(2)}`;
|
|
11988
12792
|
const newLine = `Estimated New Cost: $${summary.newMonthlyCost.toFixed(2)}`;
|
|
11989
12793
|
const diffSymbol = summary.difference >= 0 ? "+" : "";
|
|
11990
12794
|
const diffLine = `Difference: ${diffSymbol}$${summary.difference.toFixed(2)}/month (${diffSymbol}${summary.percentChange.toFixed(1)}%)`;
|
|
11991
|
-
result +=
|
|
11992
|
-
result +=
|
|
11993
|
-
const diffColor = summary.difference > 0 ?
|
|
11994
|
-
result +=
|
|
12795
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + currentLine.padEnd(59) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12796
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + newLine.padEnd(59) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12797
|
+
const diffColor = summary.difference > 0 ? import_chalk17.default.red : import_chalk17.default.green;
|
|
12798
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + diffColor(diffLine).padEnd(69) + import_chalk17.default.bold.blue("\u2502\n");
|
|
11995
12799
|
const threshold = parseFloat(options.threshold || "20");
|
|
11996
12800
|
if (Math.abs(summary.percentChange) > threshold) {
|
|
11997
|
-
result +=
|
|
12801
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + " ".repeat(59) + import_chalk17.default.bold.blue("\u2502\n");
|
|
11998
12802
|
const warning = `\u26A0\uFE0F Cost ${summary.difference > 0 ? "increase" : "decrease"} exceeds ${threshold}% threshold!`;
|
|
11999
|
-
result +=
|
|
12803
|
+
result += import_chalk17.default.bold.blue("\u2502 ") + import_chalk17.default.yellow.bold(warning.padEnd(59)) + import_chalk17.default.bold.blue("\u2502\n");
|
|
12000
12804
|
}
|
|
12001
|
-
result +=
|
|
12805
|
+
result += import_chalk17.default.bold.blue(
|
|
12002
12806
|
"\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F\n"
|
|
12003
12807
|
);
|
|
12004
12808
|
return result;
|
|
@@ -12007,12 +12811,12 @@ __name(formatCostEstimate, "formatCostEstimate");
|
|
|
12007
12811
|
async function handleTerraform(options) {
|
|
12008
12812
|
const { plan, threshold, output } = options;
|
|
12009
12813
|
if (!plan) {
|
|
12010
|
-
console.error(
|
|
12814
|
+
console.error(import_chalk17.default.red("Error: --plan argument is required"));
|
|
12011
12815
|
console.log("Usage: infra-cost terraform --plan <path-to-terraform-plan>");
|
|
12012
12816
|
process.exit(1);
|
|
12013
12817
|
}
|
|
12014
12818
|
try {
|
|
12015
|
-
console.log(
|
|
12819
|
+
console.log(import_chalk17.default.blue("\u{1F4CA} Analyzing Terraform plan...\n"));
|
|
12016
12820
|
const terraformPlan = parseTerraformPlan(plan);
|
|
12017
12821
|
const summary = analyzePlan(terraformPlan);
|
|
12018
12822
|
const formatted = formatCostEstimate(summary, options);
|
|
@@ -12022,7 +12826,7 @@ async function handleTerraform(options) {
|
|
|
12022
12826
|
process.exit(1);
|
|
12023
12827
|
}
|
|
12024
12828
|
} catch (error) {
|
|
12025
|
-
console.error(
|
|
12829
|
+
console.error(import_chalk17.default.red(`Error: ${error.message}`));
|
|
12026
12830
|
process.exit(1);
|
|
12027
12831
|
}
|
|
12028
12832
|
}
|
|
@@ -12040,6 +12844,1043 @@ function registerTerraformCommand(program) {
|
|
|
12040
12844
|
}
|
|
12041
12845
|
__name(registerTerraformCommand, "registerTerraformCommand");
|
|
12042
12846
|
|
|
12847
|
+
// src/cli/commands/scheduler/index.ts
|
|
12848
|
+
var import_fs5 = require("fs");
|
|
12849
|
+
var import_path3 = require("path");
|
|
12850
|
+
var import_os3 = require("os");
|
|
12851
|
+
var import_chalk18 = __toESM(require("chalk"));
|
|
12852
|
+
var CONFIG_DIR = (0, import_path3.join)((0, import_os3.homedir)(), ".infra-cost");
|
|
12853
|
+
var CONFIG_FILE = (0, import_path3.join)(CONFIG_DIR, "scheduler.json");
|
|
12854
|
+
var PID_FILE = (0, import_path3.join)(CONFIG_DIR, "scheduler.pid");
|
|
12855
|
+
var LOG_FILE = (0, import_path3.join)(CONFIG_DIR, "scheduler.log");
|
|
12856
|
+
function getConfig() {
|
|
12857
|
+
if (!(0, import_fs5.existsSync)(CONFIG_FILE)) {
|
|
12858
|
+
return {
|
|
12859
|
+
schedules: [],
|
|
12860
|
+
daemon: {
|
|
12861
|
+
pidFile: PID_FILE,
|
|
12862
|
+
logFile: LOG_FILE
|
|
12863
|
+
}
|
|
12864
|
+
};
|
|
12865
|
+
}
|
|
12866
|
+
try {
|
|
12867
|
+
return JSON.parse((0, import_fs5.readFileSync)(CONFIG_FILE, "utf-8"));
|
|
12868
|
+
} catch (error) {
|
|
12869
|
+
console.error(import_chalk18.default.red("Error reading scheduler config:", error));
|
|
12870
|
+
return {
|
|
12871
|
+
schedules: [],
|
|
12872
|
+
daemon: {
|
|
12873
|
+
pidFile: PID_FILE,
|
|
12874
|
+
logFile: LOG_FILE
|
|
12875
|
+
}
|
|
12876
|
+
};
|
|
12877
|
+
}
|
|
12878
|
+
}
|
|
12879
|
+
__name(getConfig, "getConfig");
|
|
12880
|
+
function saveConfig(config) {
|
|
12881
|
+
try {
|
|
12882
|
+
(0, import_fs5.writeFileSync)(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
12883
|
+
} catch (error) {
|
|
12884
|
+
console.error(import_chalk18.default.red("Error saving scheduler config:", error));
|
|
12885
|
+
process.exit(1);
|
|
12886
|
+
}
|
|
12887
|
+
}
|
|
12888
|
+
__name(saveConfig, "saveConfig");
|
|
12889
|
+
function isDaemonRunning() {
|
|
12890
|
+
if (!(0, import_fs5.existsSync)(PID_FILE)) {
|
|
12891
|
+
return false;
|
|
12892
|
+
}
|
|
12893
|
+
try {
|
|
12894
|
+
const pid = parseInt((0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim(), 10);
|
|
12895
|
+
process.kill(pid, 0);
|
|
12896
|
+
return true;
|
|
12897
|
+
} catch (error) {
|
|
12898
|
+
try {
|
|
12899
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) {
|
|
12900
|
+
require("fs").unlinkSync(PID_FILE);
|
|
12901
|
+
}
|
|
12902
|
+
} catch (e) {
|
|
12903
|
+
}
|
|
12904
|
+
return false;
|
|
12905
|
+
}
|
|
12906
|
+
}
|
|
12907
|
+
__name(isDaemonRunning, "isDaemonRunning");
|
|
12908
|
+
function getNextRun(cron) {
|
|
12909
|
+
const now = /* @__PURE__ */ new Date();
|
|
12910
|
+
const next = new Date(now.getTime() + 24 * 60 * 60 * 1e3);
|
|
12911
|
+
return next.toISOString();
|
|
12912
|
+
}
|
|
12913
|
+
__name(getNextRun, "getNextRun");
|
|
12914
|
+
async function handleStart(options) {
|
|
12915
|
+
if (isDaemonRunning()) {
|
|
12916
|
+
console.log(import_chalk18.default.yellow("\u26A0\uFE0F Scheduler daemon is already running"));
|
|
12917
|
+
console.log(import_chalk18.default.gray("Run `infra-cost scheduler status` to check status"));
|
|
12918
|
+
return;
|
|
12919
|
+
}
|
|
12920
|
+
const config = getConfig();
|
|
12921
|
+
if (config.schedules.length === 0) {
|
|
12922
|
+
console.log(import_chalk18.default.yellow("\u26A0\uFE0F No schedules configured"));
|
|
12923
|
+
console.log(import_chalk18.default.gray("Add schedules with: infra-cost scheduler add"));
|
|
12924
|
+
return;
|
|
12925
|
+
}
|
|
12926
|
+
console.log(import_chalk18.default.blue("\u{1F680} Starting scheduler daemon..."));
|
|
12927
|
+
console.log(import_chalk18.default.gray(` Log file: ${LOG_FILE}`));
|
|
12928
|
+
console.log(import_chalk18.default.gray(` PID file: ${PID_FILE}`));
|
|
12929
|
+
console.log(import_chalk18.default.gray(` Schedules: ${config.schedules.filter((s) => s.enabled).length} enabled`));
|
|
12930
|
+
console.log(import_chalk18.default.green("\u2705 Scheduler daemon started"));
|
|
12931
|
+
console.log(import_chalk18.default.gray("Run `infra-cost scheduler status` to check status"));
|
|
12932
|
+
console.log(import_chalk18.default.gray("Run `infra-cost scheduler logs` to view execution logs"));
|
|
12933
|
+
(0, import_fs5.writeFileSync)(PID_FILE, process.pid.toString());
|
|
12934
|
+
}
|
|
12935
|
+
__name(handleStart, "handleStart");
|
|
12936
|
+
async function handleStop(options) {
|
|
12937
|
+
if (!isDaemonRunning()) {
|
|
12938
|
+
console.log(import_chalk18.default.yellow("\u26A0\uFE0F Scheduler daemon is not running"));
|
|
12939
|
+
return;
|
|
12940
|
+
}
|
|
12941
|
+
try {
|
|
12942
|
+
const pid = parseInt((0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim(), 10);
|
|
12943
|
+
console.log(import_chalk18.default.blue("\u{1F6D1} Stopping scheduler daemon..."));
|
|
12944
|
+
process.kill(pid, "SIGTERM");
|
|
12945
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) {
|
|
12946
|
+
require("fs").unlinkSync(PID_FILE);
|
|
12947
|
+
}
|
|
12948
|
+
console.log(import_chalk18.default.green("\u2705 Scheduler daemon stopped"));
|
|
12949
|
+
} catch (error) {
|
|
12950
|
+
console.error(import_chalk18.default.red("Error stopping daemon:", error.message));
|
|
12951
|
+
process.exit(1);
|
|
12952
|
+
}
|
|
12953
|
+
}
|
|
12954
|
+
__name(handleStop, "handleStop");
|
|
12955
|
+
async function handleStatus(options) {
|
|
12956
|
+
const config = getConfig();
|
|
12957
|
+
const isRunning = isDaemonRunning();
|
|
12958
|
+
console.log(import_chalk18.default.bold("\n\u{1F4CA} Scheduler Status\n"));
|
|
12959
|
+
console.log(import_chalk18.default.bold("Daemon:"));
|
|
12960
|
+
if (isRunning) {
|
|
12961
|
+
const pid = (0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim();
|
|
12962
|
+
console.log(import_chalk18.default.green(" Status: \u2705 Running"));
|
|
12963
|
+
console.log(import_chalk18.default.gray(` PID: ${pid}`));
|
|
12964
|
+
} else {
|
|
12965
|
+
console.log(import_chalk18.default.red(" Status: \u274C Stopped"));
|
|
12966
|
+
}
|
|
12967
|
+
console.log(import_chalk18.default.gray(` Log file: ${LOG_FILE}`));
|
|
12968
|
+
console.log(import_chalk18.default.gray(` Config: ${CONFIG_FILE}`));
|
|
12969
|
+
console.log(import_chalk18.default.bold("\nSchedules:"));
|
|
12970
|
+
if (config.schedules.length === 0) {
|
|
12971
|
+
console.log(import_chalk18.default.gray(" No schedules configured"));
|
|
12972
|
+
} else {
|
|
12973
|
+
const enabled = config.schedules.filter((s) => s.enabled);
|
|
12974
|
+
const disabled = config.schedules.filter((s) => !s.enabled);
|
|
12975
|
+
console.log(import_chalk18.default.gray(` Total: ${config.schedules.length} (${enabled.length} enabled, ${disabled.length} disabled)`));
|
|
12976
|
+
config.schedules.forEach((schedule) => {
|
|
12977
|
+
const status = schedule.enabled ? import_chalk18.default.green("\u2713") : import_chalk18.default.gray("\u2717");
|
|
12978
|
+
console.log(` ${status} ${import_chalk18.default.bold(schedule.name)}`);
|
|
12979
|
+
console.log(import_chalk18.default.gray(` Cron: ${schedule.cron}`));
|
|
12980
|
+
console.log(import_chalk18.default.gray(` Command: infra-cost ${schedule.command}`));
|
|
12981
|
+
if (schedule.lastRun) {
|
|
12982
|
+
console.log(import_chalk18.default.gray(` Last run: ${schedule.lastRun}`));
|
|
12983
|
+
}
|
|
12984
|
+
});
|
|
12985
|
+
}
|
|
12986
|
+
console.log("");
|
|
12987
|
+
}
|
|
12988
|
+
__name(handleStatus, "handleStatus");
|
|
12989
|
+
async function handleAdd(options) {
|
|
12990
|
+
const { name, cron, command, timezone } = options;
|
|
12991
|
+
if (!name || !cron || !command) {
|
|
12992
|
+
console.error(import_chalk18.default.red("Error: --name, --cron, and --command are required"));
|
|
12993
|
+
console.log("\nExample:");
|
|
12994
|
+
console.log(import_chalk18.default.gray(" infra-cost scheduler add \\"));
|
|
12995
|
+
console.log(import_chalk18.default.gray(' --name "daily-report" \\'));
|
|
12996
|
+
console.log(import_chalk18.default.gray(' --cron "0 9 * * *" \\'));
|
|
12997
|
+
console.log(import_chalk18.default.gray(' --command "now --output json"'));
|
|
12998
|
+
process.exit(1);
|
|
12999
|
+
}
|
|
13000
|
+
const config = getConfig();
|
|
13001
|
+
if (config.schedules.find((s) => s.name === name)) {
|
|
13002
|
+
console.error(import_chalk18.default.red(`Error: Schedule "${name}" already exists`));
|
|
13003
|
+
console.log(import_chalk18.default.gray("Use a different name or remove the existing schedule first"));
|
|
13004
|
+
process.exit(1);
|
|
13005
|
+
}
|
|
13006
|
+
const newSchedule = {
|
|
13007
|
+
name,
|
|
13008
|
+
cron,
|
|
13009
|
+
command,
|
|
13010
|
+
timezone: timezone || "UTC",
|
|
13011
|
+
enabled: true,
|
|
13012
|
+
nextRun: getNextRun(cron)
|
|
13013
|
+
};
|
|
13014
|
+
config.schedules.push(newSchedule);
|
|
13015
|
+
saveConfig(config);
|
|
13016
|
+
console.log(import_chalk18.default.green("\u2705 Schedule added successfully"));
|
|
13017
|
+
console.log(import_chalk18.default.gray(` Name: ${name}`));
|
|
13018
|
+
console.log(import_chalk18.default.gray(` Cron: ${cron}`));
|
|
13019
|
+
console.log(import_chalk18.default.gray(` Command: infra-cost ${command}`));
|
|
13020
|
+
console.log(import_chalk18.default.gray(` Timezone: ${timezone || "UTC"}`));
|
|
13021
|
+
console.log("");
|
|
13022
|
+
console.log(import_chalk18.default.gray("Start the scheduler daemon to activate: infra-cost scheduler start"));
|
|
13023
|
+
}
|
|
13024
|
+
__name(handleAdd, "handleAdd");
|
|
13025
|
+
async function handleRemove(options) {
|
|
13026
|
+
const { name } = options;
|
|
13027
|
+
if (!name) {
|
|
13028
|
+
console.error(import_chalk18.default.red("Error: --name is required"));
|
|
13029
|
+
process.exit(1);
|
|
13030
|
+
}
|
|
13031
|
+
const config = getConfig();
|
|
13032
|
+
const index = config.schedules.findIndex((s) => s.name === name);
|
|
13033
|
+
if (index === -1) {
|
|
13034
|
+
console.error(import_chalk18.default.red(`Error: Schedule "${name}" not found`));
|
|
13035
|
+
process.exit(1);
|
|
13036
|
+
}
|
|
13037
|
+
config.schedules.splice(index, 1);
|
|
13038
|
+
saveConfig(config);
|
|
13039
|
+
console.log(import_chalk18.default.green(`\u2705 Schedule "${name}" removed`));
|
|
13040
|
+
}
|
|
13041
|
+
__name(handleRemove, "handleRemove");
|
|
13042
|
+
async function handleList2(options) {
|
|
13043
|
+
const config = getConfig();
|
|
13044
|
+
if (config.schedules.length === 0) {
|
|
13045
|
+
console.log(import_chalk18.default.yellow("No schedules configured"));
|
|
13046
|
+
console.log(import_chalk18.default.gray("\nAdd a schedule with: infra-cost scheduler add"));
|
|
13047
|
+
return;
|
|
13048
|
+
}
|
|
13049
|
+
console.log(import_chalk18.default.bold("\n\u{1F4C5} Configured Schedules\n"));
|
|
13050
|
+
config.schedules.forEach((schedule, index) => {
|
|
13051
|
+
const status = schedule.enabled ? import_chalk18.default.green("\u2713 ENABLED") : import_chalk18.default.gray("\u2717 DISABLED");
|
|
13052
|
+
console.log(import_chalk18.default.bold(`${index + 1}. ${schedule.name}`) + ` ${status}`);
|
|
13053
|
+
console.log(import_chalk18.default.gray(` Cron: ${schedule.cron}`));
|
|
13054
|
+
console.log(import_chalk18.default.gray(` Command: infra-cost ${schedule.command}`));
|
|
13055
|
+
console.log(import_chalk18.default.gray(` Timezone: ${schedule.timezone || "UTC"}`));
|
|
13056
|
+
if (schedule.lastRun) {
|
|
13057
|
+
console.log(import_chalk18.default.gray(` Last run: ${schedule.lastRun}`));
|
|
13058
|
+
}
|
|
13059
|
+
if (schedule.nextRun) {
|
|
13060
|
+
console.log(import_chalk18.default.gray(` Next run: ${schedule.nextRun}`));
|
|
13061
|
+
}
|
|
13062
|
+
console.log("");
|
|
13063
|
+
});
|
|
13064
|
+
}
|
|
13065
|
+
__name(handleList2, "handleList");
|
|
13066
|
+
async function handleLogs(options) {
|
|
13067
|
+
if (!(0, import_fs5.existsSync)(LOG_FILE)) {
|
|
13068
|
+
console.log(import_chalk18.default.yellow("No logs found"));
|
|
13069
|
+
console.log(import_chalk18.default.gray("Logs will appear here once the scheduler runs"));
|
|
13070
|
+
return;
|
|
13071
|
+
}
|
|
13072
|
+
const logs = (0, import_fs5.readFileSync)(LOG_FILE, "utf-8");
|
|
13073
|
+
console.log(logs);
|
|
13074
|
+
}
|
|
13075
|
+
__name(handleLogs, "handleLogs");
|
|
13076
|
+
async function handleGenerateSystemd(options) {
|
|
13077
|
+
const user = process.env.USER || "ubuntu";
|
|
13078
|
+
const nodePath = process.execPath;
|
|
13079
|
+
const infraCostPath = require.main?.filename || "/usr/local/bin/infra-cost";
|
|
13080
|
+
const serviceFile = `[Unit]
|
|
13081
|
+
Description=Infra Cost Scheduler Daemon
|
|
13082
|
+
After=network.target
|
|
13083
|
+
|
|
13084
|
+
[Service]
|
|
13085
|
+
Type=simple
|
|
13086
|
+
User=${user}
|
|
13087
|
+
ExecStart=${nodePath} ${infraCostPath} scheduler start
|
|
13088
|
+
Restart=always
|
|
13089
|
+
RestartSec=10
|
|
13090
|
+
StandardOutput=append:${LOG_FILE}
|
|
13091
|
+
StandardError=append:${LOG_FILE}
|
|
13092
|
+
|
|
13093
|
+
[Install]
|
|
13094
|
+
WantedBy=multi-user.target
|
|
13095
|
+
`;
|
|
13096
|
+
console.log(import_chalk18.default.bold("\u{1F4DD} Systemd Service File\n"));
|
|
13097
|
+
console.log(serviceFile);
|
|
13098
|
+
console.log(import_chalk18.default.gray("\nSave this to: /etc/systemd/system/infra-cost-scheduler.service"));
|
|
13099
|
+
console.log(import_chalk18.default.gray("\nThen run:"));
|
|
13100
|
+
console.log(import_chalk18.default.gray(" sudo systemctl daemon-reload"));
|
|
13101
|
+
console.log(import_chalk18.default.gray(" sudo systemctl enable infra-cost-scheduler"));
|
|
13102
|
+
console.log(import_chalk18.default.gray(" sudo systemctl start infra-cost-scheduler"));
|
|
13103
|
+
}
|
|
13104
|
+
__name(handleGenerateSystemd, "handleGenerateSystemd");
|
|
13105
|
+
function registerSchedulerCommands(program) {
|
|
13106
|
+
const scheduler = program.command("scheduler").description("Manage scheduled cost reports (daemon mode)");
|
|
13107
|
+
scheduler.command("start").description("Start scheduler daemon").action(handleStart);
|
|
13108
|
+
scheduler.command("stop").description("Stop scheduler daemon").action(handleStop);
|
|
13109
|
+
scheduler.command("status").description("Show scheduler status").action(handleStatus);
|
|
13110
|
+
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);
|
|
13111
|
+
scheduler.command("remove").description("Remove schedule").option("--name <name>", "Schedule name").action(handleRemove);
|
|
13112
|
+
scheduler.command("list").description("List all schedules").action(handleList2);
|
|
13113
|
+
scheduler.command("logs").description("View scheduler execution logs").action(handleLogs);
|
|
13114
|
+
scheduler.command("generate-systemd").description("Generate systemd service file").action(handleGenerateSystemd);
|
|
13115
|
+
}
|
|
13116
|
+
__name(registerSchedulerCommands, "registerSchedulerCommands");
|
|
13117
|
+
|
|
13118
|
+
// src/cli/commands/rbac/index.ts
|
|
13119
|
+
var import_chalk19 = __toESM(require("chalk"));
|
|
13120
|
+
|
|
13121
|
+
// src/core/rbac.ts
|
|
13122
|
+
var import_fs6 = require("fs");
|
|
13123
|
+
var import_path4 = require("path");
|
|
13124
|
+
var import_os4 = require("os");
|
|
13125
|
+
var CONFIG_DIR2 = (0, import_path4.join)((0, import_os4.homedir)(), ".infra-cost");
|
|
13126
|
+
var RBAC_CONFIG_FILE = (0, import_path4.join)(CONFIG_DIR2, "rbac.json");
|
|
13127
|
+
var DEFAULT_ROLES = {
|
|
13128
|
+
admin: {
|
|
13129
|
+
name: "admin",
|
|
13130
|
+
description: "Full access to all features",
|
|
13131
|
+
permissions: ["*"]
|
|
13132
|
+
},
|
|
13133
|
+
finance: {
|
|
13134
|
+
name: "finance",
|
|
13135
|
+
description: "Cost data and reports only",
|
|
13136
|
+
permissions: [
|
|
13137
|
+
"costs:read",
|
|
13138
|
+
"costs:read:*",
|
|
13139
|
+
"chargeback:read",
|
|
13140
|
+
"chargeback:read:*",
|
|
13141
|
+
"reports:generate",
|
|
13142
|
+
"budgets:read",
|
|
13143
|
+
"budgets:read:*",
|
|
13144
|
+
"export:read"
|
|
13145
|
+
]
|
|
13146
|
+
},
|
|
13147
|
+
developer: {
|
|
13148
|
+
name: "developer",
|
|
13149
|
+
description: "View costs for assigned resources",
|
|
13150
|
+
permissions: [
|
|
13151
|
+
"costs:read:own",
|
|
13152
|
+
"costs:read:team:*",
|
|
13153
|
+
"inventory:read:own",
|
|
13154
|
+
"inventory:read:team:*",
|
|
13155
|
+
"optimization:read:own",
|
|
13156
|
+
"optimization:read:team:*"
|
|
13157
|
+
]
|
|
13158
|
+
},
|
|
13159
|
+
viewer: {
|
|
13160
|
+
name: "viewer",
|
|
13161
|
+
description: "Read-only access to summaries",
|
|
13162
|
+
permissions: [
|
|
13163
|
+
"costs:read:summary",
|
|
13164
|
+
"budgets:read"
|
|
13165
|
+
]
|
|
13166
|
+
}
|
|
13167
|
+
};
|
|
13168
|
+
function loadRBACConfig() {
|
|
13169
|
+
if (!(0, import_fs6.existsSync)(RBAC_CONFIG_FILE)) {
|
|
13170
|
+
return {
|
|
13171
|
+
roles: DEFAULT_ROLES,
|
|
13172
|
+
userRoles: []
|
|
13173
|
+
};
|
|
13174
|
+
}
|
|
13175
|
+
try {
|
|
13176
|
+
const data = (0, import_fs6.readFileSync)(RBAC_CONFIG_FILE, "utf-8");
|
|
13177
|
+
const config = JSON.parse(data);
|
|
13178
|
+
return {
|
|
13179
|
+
...config,
|
|
13180
|
+
roles: { ...DEFAULT_ROLES, ...config.roles }
|
|
13181
|
+
};
|
|
13182
|
+
} catch (error) {
|
|
13183
|
+
console.error("Error loading RBAC config:", error);
|
|
13184
|
+
return {
|
|
13185
|
+
roles: DEFAULT_ROLES,
|
|
13186
|
+
userRoles: []
|
|
13187
|
+
};
|
|
13188
|
+
}
|
|
13189
|
+
}
|
|
13190
|
+
__name(loadRBACConfig, "loadRBACConfig");
|
|
13191
|
+
function saveRBACConfig(config) {
|
|
13192
|
+
try {
|
|
13193
|
+
(0, import_fs6.writeFileSync)(RBAC_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
13194
|
+
} catch (error) {
|
|
13195
|
+
throw new Error(`Failed to save RBAC config: ${error}`);
|
|
13196
|
+
}
|
|
13197
|
+
}
|
|
13198
|
+
__name(saveRBACConfig, "saveRBACConfig");
|
|
13199
|
+
function getUserRole(user) {
|
|
13200
|
+
const config = loadRBACConfig();
|
|
13201
|
+
const userRole = config.userRoles.find((ur) => ur.user === user);
|
|
13202
|
+
return userRole?.role || null;
|
|
13203
|
+
}
|
|
13204
|
+
__name(getUserRole, "getUserRole");
|
|
13205
|
+
function assignRole(user, role, assignedBy) {
|
|
13206
|
+
const config = loadRBACConfig();
|
|
13207
|
+
if (!config.roles[role]) {
|
|
13208
|
+
throw new Error(`Role "${role}" does not exist`);
|
|
13209
|
+
}
|
|
13210
|
+
config.userRoles = config.userRoles.filter((ur) => ur.user !== user);
|
|
13211
|
+
config.userRoles.push({
|
|
13212
|
+
user,
|
|
13213
|
+
role,
|
|
13214
|
+
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13215
|
+
assignedBy
|
|
13216
|
+
});
|
|
13217
|
+
saveRBACConfig(config);
|
|
13218
|
+
}
|
|
13219
|
+
__name(assignRole, "assignRole");
|
|
13220
|
+
function removeUserRole(user) {
|
|
13221
|
+
const config = loadRBACConfig();
|
|
13222
|
+
config.userRoles = config.userRoles.filter((ur) => ur.user !== user);
|
|
13223
|
+
saveRBACConfig(config);
|
|
13224
|
+
}
|
|
13225
|
+
__name(removeUserRole, "removeUserRole");
|
|
13226
|
+
function createRole(name, description, permissions) {
|
|
13227
|
+
const config = loadRBACConfig();
|
|
13228
|
+
if (config.roles[name]) {
|
|
13229
|
+
throw new Error(`Role "${name}" already exists`);
|
|
13230
|
+
}
|
|
13231
|
+
config.roles[name] = {
|
|
13232
|
+
name,
|
|
13233
|
+
description,
|
|
13234
|
+
permissions
|
|
13235
|
+
};
|
|
13236
|
+
saveRBACConfig(config);
|
|
13237
|
+
}
|
|
13238
|
+
__name(createRole, "createRole");
|
|
13239
|
+
function deleteRole(name) {
|
|
13240
|
+
const config = loadRBACConfig();
|
|
13241
|
+
if (DEFAULT_ROLES[name]) {
|
|
13242
|
+
throw new Error(`Cannot delete default role "${name}"`);
|
|
13243
|
+
}
|
|
13244
|
+
if (!config.roles[name]) {
|
|
13245
|
+
throw new Error(`Role "${name}" does not exist`);
|
|
13246
|
+
}
|
|
13247
|
+
delete config.roles[name];
|
|
13248
|
+
config.userRoles = config.userRoles.filter((ur) => ur.role !== name);
|
|
13249
|
+
saveRBACConfig(config);
|
|
13250
|
+
}
|
|
13251
|
+
__name(deleteRole, "deleteRole");
|
|
13252
|
+
function parsePermission(permissionStr) {
|
|
13253
|
+
const parts = permissionStr.split(":");
|
|
13254
|
+
return {
|
|
13255
|
+
resource: parts[0] || "*",
|
|
13256
|
+
action: parts[1] || "*",
|
|
13257
|
+
scope: parts[2]
|
|
13258
|
+
};
|
|
13259
|
+
}
|
|
13260
|
+
__name(parsePermission, "parsePermission");
|
|
13261
|
+
function permissionMatches(required, granted) {
|
|
13262
|
+
if (granted.resource === "*") {
|
|
13263
|
+
return true;
|
|
13264
|
+
}
|
|
13265
|
+
if (granted.resource !== required.resource) {
|
|
13266
|
+
return false;
|
|
13267
|
+
}
|
|
13268
|
+
if (granted.action !== "*" && granted.action !== required.action) {
|
|
13269
|
+
return false;
|
|
13270
|
+
}
|
|
13271
|
+
if (required.scope) {
|
|
13272
|
+
if (!granted.scope || granted.scope === "*") {
|
|
13273
|
+
return true;
|
|
13274
|
+
}
|
|
13275
|
+
if (granted.scope !== required.scope && !granted.scope.endsWith(":*")) {
|
|
13276
|
+
return false;
|
|
13277
|
+
}
|
|
13278
|
+
if (granted.scope.endsWith(":*")) {
|
|
13279
|
+
const prefix = granted.scope.slice(0, -2);
|
|
13280
|
+
return required.scope.startsWith(prefix);
|
|
13281
|
+
}
|
|
13282
|
+
}
|
|
13283
|
+
return true;
|
|
13284
|
+
}
|
|
13285
|
+
__name(permissionMatches, "permissionMatches");
|
|
13286
|
+
function hasPermission(user, permissionStr, context) {
|
|
13287
|
+
const config = loadRBACConfig();
|
|
13288
|
+
const userRole = config.userRoles.find((ur) => ur.user === user);
|
|
13289
|
+
if (!userRole) {
|
|
13290
|
+
return false;
|
|
13291
|
+
}
|
|
13292
|
+
const role = config.roles[userRole.role];
|
|
13293
|
+
if (!role) {
|
|
13294
|
+
return false;
|
|
13295
|
+
}
|
|
13296
|
+
const required = parsePermission(permissionStr);
|
|
13297
|
+
for (const grantedStr of role.permissions) {
|
|
13298
|
+
const granted = parsePermission(grantedStr);
|
|
13299
|
+
if (permissionMatches(required, granted)) {
|
|
13300
|
+
return true;
|
|
13301
|
+
}
|
|
13302
|
+
}
|
|
13303
|
+
return false;
|
|
13304
|
+
}
|
|
13305
|
+
__name(hasPermission, "hasPermission");
|
|
13306
|
+
function getUserPermissions(user) {
|
|
13307
|
+
const config = loadRBACConfig();
|
|
13308
|
+
const userRole = config.userRoles.find((ur) => ur.user === user);
|
|
13309
|
+
if (!userRole) {
|
|
13310
|
+
return [];
|
|
13311
|
+
}
|
|
13312
|
+
const role = config.roles[userRole.role];
|
|
13313
|
+
if (!role) {
|
|
13314
|
+
return [];
|
|
13315
|
+
}
|
|
13316
|
+
return role.permissions;
|
|
13317
|
+
}
|
|
13318
|
+
__name(getUserPermissions, "getUserPermissions");
|
|
13319
|
+
function listRoles() {
|
|
13320
|
+
const config = loadRBACConfig();
|
|
13321
|
+
return Object.values(config.roles);
|
|
13322
|
+
}
|
|
13323
|
+
__name(listRoles, "listRoles");
|
|
13324
|
+
function listUserRoles() {
|
|
13325
|
+
const config = loadRBACConfig();
|
|
13326
|
+
return config.userRoles;
|
|
13327
|
+
}
|
|
13328
|
+
__name(listUserRoles, "listUserRoles");
|
|
13329
|
+
|
|
13330
|
+
// src/cli/commands/rbac/index.ts
|
|
13331
|
+
async function handleAssignRole(options) {
|
|
13332
|
+
const { user, role, assignedBy } = options;
|
|
13333
|
+
if (!user || !role) {
|
|
13334
|
+
console.error(import_chalk19.default.red("Error: --user and --role are required"));
|
|
13335
|
+
console.log("\nExample:");
|
|
13336
|
+
console.log(import_chalk19.default.gray(" infra-cost rbac assign-role --user john@company.com --role finance"));
|
|
13337
|
+
process.exit(1);
|
|
13338
|
+
}
|
|
13339
|
+
try {
|
|
13340
|
+
assignRole(user, role, assignedBy);
|
|
13341
|
+
console.log(import_chalk19.default.green(`\u2705 Assigned role "${role}" to user "${user}"`));
|
|
13342
|
+
} catch (error) {
|
|
13343
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13344
|
+
process.exit(1);
|
|
13345
|
+
}
|
|
13346
|
+
}
|
|
13347
|
+
__name(handleAssignRole, "handleAssignRole");
|
|
13348
|
+
async function handleRemoveRole(options) {
|
|
13349
|
+
const { user } = options;
|
|
13350
|
+
if (!user) {
|
|
13351
|
+
console.error(import_chalk19.default.red("Error: --user is required"));
|
|
13352
|
+
process.exit(1);
|
|
13353
|
+
}
|
|
13354
|
+
try {
|
|
13355
|
+
removeUserRole(user);
|
|
13356
|
+
console.log(import_chalk19.default.green(`\u2705 Removed role from user "${user}"`));
|
|
13357
|
+
} catch (error) {
|
|
13358
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13359
|
+
process.exit(1);
|
|
13360
|
+
}
|
|
13361
|
+
}
|
|
13362
|
+
__name(handleRemoveRole, "handleRemoveRole");
|
|
13363
|
+
async function handleCreateRole(options) {
|
|
13364
|
+
const { name, description, permissions } = options;
|
|
13365
|
+
if (!name || !description || !permissions) {
|
|
13366
|
+
console.error(import_chalk19.default.red("Error: --name, --description, and --permissions are required"));
|
|
13367
|
+
console.log("\nExample:");
|
|
13368
|
+
console.log(import_chalk19.default.gray(" infra-cost rbac create-role \\"));
|
|
13369
|
+
console.log(import_chalk19.default.gray(" --name team-lead \\"));
|
|
13370
|
+
console.log(import_chalk19.default.gray(' --description "Team lead access" \\'));
|
|
13371
|
+
console.log(import_chalk19.default.gray(' --permissions "costs:read,optimization:read"'));
|
|
13372
|
+
process.exit(1);
|
|
13373
|
+
}
|
|
13374
|
+
try {
|
|
13375
|
+
const permList = permissions.split(",").map((p) => p.trim());
|
|
13376
|
+
createRole(name, description, permList);
|
|
13377
|
+
console.log(import_chalk19.default.green(`\u2705 Created role "${name}"`));
|
|
13378
|
+
console.log(import_chalk19.default.gray(` Permissions: ${permList.join(", ")}`));
|
|
13379
|
+
} catch (error) {
|
|
13380
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13381
|
+
process.exit(1);
|
|
13382
|
+
}
|
|
13383
|
+
}
|
|
13384
|
+
__name(handleCreateRole, "handleCreateRole");
|
|
13385
|
+
async function handleDeleteRole(options) {
|
|
13386
|
+
const { name } = options;
|
|
13387
|
+
if (!name) {
|
|
13388
|
+
console.error(import_chalk19.default.red("Error: --name is required"));
|
|
13389
|
+
process.exit(1);
|
|
13390
|
+
}
|
|
13391
|
+
try {
|
|
13392
|
+
deleteRole(name);
|
|
13393
|
+
console.log(import_chalk19.default.green(`\u2705 Deleted role "${name}"`));
|
|
13394
|
+
} catch (error) {
|
|
13395
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13396
|
+
process.exit(1);
|
|
13397
|
+
}
|
|
13398
|
+
}
|
|
13399
|
+
__name(handleDeleteRole, "handleDeleteRole");
|
|
13400
|
+
async function handleShowPermissions(options) {
|
|
13401
|
+
const { user } = options;
|
|
13402
|
+
if (!user) {
|
|
13403
|
+
console.error(import_chalk19.default.red("Error: --user is required"));
|
|
13404
|
+
process.exit(1);
|
|
13405
|
+
}
|
|
13406
|
+
try {
|
|
13407
|
+
const role = getUserRole(user);
|
|
13408
|
+
const permissions = getUserPermissions(user);
|
|
13409
|
+
console.log(import_chalk19.default.bold(`
|
|
13410
|
+
\u{1F464} User: ${user}
|
|
13411
|
+
`));
|
|
13412
|
+
if (!role) {
|
|
13413
|
+
console.log(import_chalk19.default.yellow("No role assigned"));
|
|
13414
|
+
return;
|
|
13415
|
+
}
|
|
13416
|
+
console.log(import_chalk19.default.bold(`Role: ${role}
|
|
13417
|
+
`));
|
|
13418
|
+
console.log(import_chalk19.default.bold("Permissions:"));
|
|
13419
|
+
if (permissions.length === 0) {
|
|
13420
|
+
console.log(import_chalk19.default.gray(" No permissions"));
|
|
13421
|
+
} else {
|
|
13422
|
+
permissions.forEach((perm) => {
|
|
13423
|
+
console.log(import_chalk19.default.gray(` \u2022 ${perm}`));
|
|
13424
|
+
});
|
|
13425
|
+
}
|
|
13426
|
+
console.log("");
|
|
13427
|
+
} catch (error) {
|
|
13428
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13429
|
+
process.exit(1);
|
|
13430
|
+
}
|
|
13431
|
+
}
|
|
13432
|
+
__name(handleShowPermissions, "handleShowPermissions");
|
|
13433
|
+
async function handleListRoles(options) {
|
|
13434
|
+
try {
|
|
13435
|
+
const roles = listRoles();
|
|
13436
|
+
console.log(import_chalk19.default.bold("\n\u{1F4CB} Available Roles\n"));
|
|
13437
|
+
roles.forEach((role) => {
|
|
13438
|
+
console.log(import_chalk19.default.bold(`${role.name}`));
|
|
13439
|
+
console.log(import_chalk19.default.gray(` ${role.description}`));
|
|
13440
|
+
console.log(import_chalk19.default.gray(` Permissions: ${role.permissions.slice(0, 3).join(", ")}${role.permissions.length > 3 ? "..." : ""}`));
|
|
13441
|
+
console.log("");
|
|
13442
|
+
});
|
|
13443
|
+
} catch (error) {
|
|
13444
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13445
|
+
process.exit(1);
|
|
13446
|
+
}
|
|
13447
|
+
}
|
|
13448
|
+
__name(handleListRoles, "handleListRoles");
|
|
13449
|
+
async function handleListUsers(options) {
|
|
13450
|
+
try {
|
|
13451
|
+
const userRoles = listUserRoles();
|
|
13452
|
+
console.log(import_chalk19.default.bold("\n\u{1F465} User Role Assignments\n"));
|
|
13453
|
+
if (userRoles.length === 0) {
|
|
13454
|
+
console.log(import_chalk19.default.yellow("No user roles assigned"));
|
|
13455
|
+
return;
|
|
13456
|
+
}
|
|
13457
|
+
userRoles.forEach((ur) => {
|
|
13458
|
+
console.log(import_chalk19.default.bold(ur.user));
|
|
13459
|
+
console.log(import_chalk19.default.gray(` Role: ${ur.role}`));
|
|
13460
|
+
console.log(import_chalk19.default.gray(` Assigned: ${new Date(ur.assignedAt).toLocaleString()}`));
|
|
13461
|
+
if (ur.assignedBy) {
|
|
13462
|
+
console.log(import_chalk19.default.gray(` Assigned by: ${ur.assignedBy}`));
|
|
13463
|
+
}
|
|
13464
|
+
console.log("");
|
|
13465
|
+
});
|
|
13466
|
+
} catch (error) {
|
|
13467
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13468
|
+
process.exit(1);
|
|
13469
|
+
}
|
|
13470
|
+
}
|
|
13471
|
+
__name(handleListUsers, "handleListUsers");
|
|
13472
|
+
async function handleCheckPermission(options) {
|
|
13473
|
+
const { user, permission } = options;
|
|
13474
|
+
if (!user || !permission) {
|
|
13475
|
+
console.error(import_chalk19.default.red("Error: --user and --permission are required"));
|
|
13476
|
+
console.log("\nExample:");
|
|
13477
|
+
console.log(import_chalk19.default.gray(' infra-cost rbac check-permission --user john@company.com --permission "costs:read"'));
|
|
13478
|
+
process.exit(1);
|
|
13479
|
+
}
|
|
13480
|
+
try {
|
|
13481
|
+
const has = hasPermission(user, permission);
|
|
13482
|
+
console.log(import_chalk19.default.bold(`
|
|
13483
|
+
\u{1F464} User: ${user}`));
|
|
13484
|
+
console.log(import_chalk19.default.bold(`\u{1F510} Permission: ${permission}
|
|
13485
|
+
`));
|
|
13486
|
+
if (has) {
|
|
13487
|
+
console.log(import_chalk19.default.green("\u2705 ALLOWED"));
|
|
13488
|
+
} else {
|
|
13489
|
+
console.log(import_chalk19.default.red("\u274C DENIED"));
|
|
13490
|
+
}
|
|
13491
|
+
console.log("");
|
|
13492
|
+
} catch (error) {
|
|
13493
|
+
console.error(import_chalk19.default.red(`Error: ${error.message}`));
|
|
13494
|
+
process.exit(1);
|
|
13495
|
+
}
|
|
13496
|
+
}
|
|
13497
|
+
__name(handleCheckPermission, "handleCheckPermission");
|
|
13498
|
+
function registerRBACCommands(program) {
|
|
13499
|
+
const rbac = program.command("rbac").description("Role-Based Access Control management");
|
|
13500
|
+
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);
|
|
13501
|
+
rbac.command("remove-role").description("Remove role from user").option("--user <email>", "User email").action(handleRemoveRole);
|
|
13502
|
+
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);
|
|
13503
|
+
rbac.command("delete-role").description("Delete custom role").option("--name <name>", "Role name").action(handleDeleteRole);
|
|
13504
|
+
rbac.command("show-permissions").description("Show user permissions").option("--user <email>", "User email").action(handleShowPermissions);
|
|
13505
|
+
rbac.command("list-roles").description("List all available roles").action(handleListRoles);
|
|
13506
|
+
rbac.command("list-users").description("List all user role assignments").action(handleListUsers);
|
|
13507
|
+
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);
|
|
13508
|
+
}
|
|
13509
|
+
__name(registerRBACCommands, "registerRBACCommands");
|
|
13510
|
+
|
|
13511
|
+
// src/cli/commands/sso/index.ts
|
|
13512
|
+
var import_chalk20 = __toESM(require("chalk"));
|
|
13513
|
+
|
|
13514
|
+
// src/core/sso.ts
|
|
13515
|
+
var import_fs7 = require("fs");
|
|
13516
|
+
var import_path5 = require("path");
|
|
13517
|
+
var import_os5 = require("os");
|
|
13518
|
+
var CONFIG_DIR3 = (0, import_path5.join)((0, import_os5.homedir)(), ".infra-cost");
|
|
13519
|
+
var SSO_DIR = (0, import_path5.join)(CONFIG_DIR3, "sso");
|
|
13520
|
+
var SSO_CONFIG_FILE = (0, import_path5.join)(CONFIG_DIR3, "sso-config.json");
|
|
13521
|
+
var SESSION_FILE = (0, import_path5.join)(SSO_DIR, "session.json");
|
|
13522
|
+
function ensureSSODir() {
|
|
13523
|
+
if (!(0, import_fs7.existsSync)(SSO_DIR)) {
|
|
13524
|
+
(0, import_fs7.mkdirSync)(SSO_DIR, { recursive: true });
|
|
13525
|
+
}
|
|
13526
|
+
}
|
|
13527
|
+
__name(ensureSSODir, "ensureSSODir");
|
|
13528
|
+
function loadSSOConfig() {
|
|
13529
|
+
if (!(0, import_fs7.existsSync)(SSO_CONFIG_FILE)) {
|
|
13530
|
+
return null;
|
|
13531
|
+
}
|
|
13532
|
+
try {
|
|
13533
|
+
const data = (0, import_fs7.readFileSync)(SSO_CONFIG_FILE, "utf-8");
|
|
13534
|
+
return JSON.parse(data);
|
|
13535
|
+
} catch (error) {
|
|
13536
|
+
console.error("Error loading SSO config:", error);
|
|
13537
|
+
return null;
|
|
13538
|
+
}
|
|
13539
|
+
}
|
|
13540
|
+
__name(loadSSOConfig, "loadSSOConfig");
|
|
13541
|
+
function saveSSOConfig(config) {
|
|
13542
|
+
try {
|
|
13543
|
+
ensureSSODir();
|
|
13544
|
+
(0, import_fs7.writeFileSync)(SSO_CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
13545
|
+
} catch (error) {
|
|
13546
|
+
throw new Error(`Failed to save SSO config: ${error}`);
|
|
13547
|
+
}
|
|
13548
|
+
}
|
|
13549
|
+
__name(saveSSOConfig, "saveSSOConfig");
|
|
13550
|
+
function loadSSOSession() {
|
|
13551
|
+
if (!(0, import_fs7.existsSync)(SESSION_FILE)) {
|
|
13552
|
+
return null;
|
|
13553
|
+
}
|
|
13554
|
+
try {
|
|
13555
|
+
const data = (0, import_fs7.readFileSync)(SESSION_FILE, "utf-8");
|
|
13556
|
+
const session = JSON.parse(data);
|
|
13557
|
+
const expiresAt = new Date(session.expiresAt);
|
|
13558
|
+
if (expiresAt < /* @__PURE__ */ new Date()) {
|
|
13559
|
+
return null;
|
|
13560
|
+
}
|
|
13561
|
+
return session;
|
|
13562
|
+
} catch (error) {
|
|
13563
|
+
return null;
|
|
13564
|
+
}
|
|
13565
|
+
}
|
|
13566
|
+
__name(loadSSOSession, "loadSSOSession");
|
|
13567
|
+
function saveSSOSession(session) {
|
|
13568
|
+
try {
|
|
13569
|
+
ensureSSODir();
|
|
13570
|
+
(0, import_fs7.writeFileSync)(SESSION_FILE, JSON.stringify(session, null, 2));
|
|
13571
|
+
} catch (error) {
|
|
13572
|
+
throw new Error(`Failed to save SSO session: ${error}`);
|
|
13573
|
+
}
|
|
13574
|
+
}
|
|
13575
|
+
__name(saveSSOSession, "saveSSOSession");
|
|
13576
|
+
function clearSSOSession() {
|
|
13577
|
+
try {
|
|
13578
|
+
if ((0, import_fs7.existsSync)(SESSION_FILE)) {
|
|
13579
|
+
require("fs").unlinkSync(SESSION_FILE);
|
|
13580
|
+
}
|
|
13581
|
+
} catch (error) {
|
|
13582
|
+
}
|
|
13583
|
+
}
|
|
13584
|
+
__name(clearSSOSession, "clearSSOSession");
|
|
13585
|
+
function isLoggedIn() {
|
|
13586
|
+
const session = loadSSOSession();
|
|
13587
|
+
return session !== null;
|
|
13588
|
+
}
|
|
13589
|
+
__name(isLoggedIn, "isLoggedIn");
|
|
13590
|
+
function getCurrentUser() {
|
|
13591
|
+
const session = loadSSOSession();
|
|
13592
|
+
return session?.email || null;
|
|
13593
|
+
}
|
|
13594
|
+
__name(getCurrentUser, "getCurrentUser");
|
|
13595
|
+
function getSessionExpiration() {
|
|
13596
|
+
const session = loadSSOSession();
|
|
13597
|
+
return session ? new Date(session.expiresAt) : null;
|
|
13598
|
+
}
|
|
13599
|
+
__name(getSessionExpiration, "getSessionExpiration");
|
|
13600
|
+
function getMinutesUntilExpiration() {
|
|
13601
|
+
const expiration = getSessionExpiration();
|
|
13602
|
+
if (!expiration)
|
|
13603
|
+
return null;
|
|
13604
|
+
const now = /* @__PURE__ */ new Date();
|
|
13605
|
+
const diff = expiration.getTime() - now.getTime();
|
|
13606
|
+
return Math.floor(diff / 1e3 / 60);
|
|
13607
|
+
}
|
|
13608
|
+
__name(getMinutesUntilExpiration, "getMinutesUntilExpiration");
|
|
13609
|
+
function generateAuthorizationUrl(config) {
|
|
13610
|
+
const params = new URLSearchParams({
|
|
13611
|
+
client_id: config.config.clientId,
|
|
13612
|
+
redirect_uri: config.config.redirectUri,
|
|
13613
|
+
response_type: "code",
|
|
13614
|
+
scope: config.config.scopes.join(" "),
|
|
13615
|
+
state: generateRandomState()
|
|
13616
|
+
});
|
|
13617
|
+
const authEndpoint = config.config.authorizationEndpoint || `${config.config.issuer}/v1/authorize`;
|
|
13618
|
+
return `${authEndpoint}?${params.toString()}`;
|
|
13619
|
+
}
|
|
13620
|
+
__name(generateAuthorizationUrl, "generateAuthorizationUrl");
|
|
13621
|
+
function generateRandomState() {
|
|
13622
|
+
return Math.random().toString(36).substring(2, 15);
|
|
13623
|
+
}
|
|
13624
|
+
__name(generateRandomState, "generateRandomState");
|
|
13625
|
+
var ProviderConfig7 = {
|
|
13626
|
+
okta: (domain, clientId, clientSecret) => ({
|
|
13627
|
+
issuer: `https://${domain}`,
|
|
13628
|
+
clientId,
|
|
13629
|
+
clientSecret,
|
|
13630
|
+
authorizationEndpoint: `https://${domain}/oauth2/v1/authorize`,
|
|
13631
|
+
tokenEndpoint: `https://${domain}/oauth2/v1/token`,
|
|
13632
|
+
userInfoEndpoint: `https://${domain}/oauth2/v1/userinfo`,
|
|
13633
|
+
scopes: ["openid", "profile", "email"]
|
|
13634
|
+
}),
|
|
13635
|
+
azureAd: (tenantId, clientId, clientSecret) => ({
|
|
13636
|
+
issuer: `https://login.microsoftonline.com/${tenantId}/v2.0`,
|
|
13637
|
+
clientId,
|
|
13638
|
+
clientSecret,
|
|
13639
|
+
authorizationEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`,
|
|
13640
|
+
tokenEndpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`,
|
|
13641
|
+
userInfoEndpoint: "https://graph.microsoft.com/v1.0/me",
|
|
13642
|
+
scopes: ["openid", "profile", "email", "User.Read"]
|
|
13643
|
+
}),
|
|
13644
|
+
google: (clientId, clientSecret) => ({
|
|
13645
|
+
issuer: "https://accounts.google.com",
|
|
13646
|
+
clientId,
|
|
13647
|
+
clientSecret,
|
|
13648
|
+
authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
13649
|
+
tokenEndpoint: "https://oauth2.googleapis.com/token",
|
|
13650
|
+
userInfoEndpoint: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
13651
|
+
scopes: ["openid", "profile", "email"]
|
|
13652
|
+
})
|
|
13653
|
+
};
|
|
13654
|
+
|
|
13655
|
+
// src/cli/commands/sso/index.ts
|
|
13656
|
+
async function handleConfigure(options) {
|
|
13657
|
+
const { provider, clientId, clientSecret, issuer, tenantId, domain } = options;
|
|
13658
|
+
if (!provider) {
|
|
13659
|
+
console.error(import_chalk20.default.red("Error: --provider is required"));
|
|
13660
|
+
console.log("\nSupported providers: okta, azure-ad, google, generic-oidc");
|
|
13661
|
+
process.exit(1);
|
|
13662
|
+
}
|
|
13663
|
+
let config;
|
|
13664
|
+
switch (provider) {
|
|
13665
|
+
case "okta":
|
|
13666
|
+
if (!domain || !clientId || !clientSecret) {
|
|
13667
|
+
console.error(import_chalk20.default.red("Error: Okta requires --domain, --client-id, --client-secret"));
|
|
13668
|
+
process.exit(1);
|
|
13669
|
+
}
|
|
13670
|
+
config = ProviderConfig7.okta(domain, clientId, clientSecret);
|
|
13671
|
+
break;
|
|
13672
|
+
case "azure-ad":
|
|
13673
|
+
if (!tenantId || !clientId || !clientSecret) {
|
|
13674
|
+
console.error(import_chalk20.default.red("Error: Azure AD requires --tenant-id, --client-id, --client-secret"));
|
|
13675
|
+
process.exit(1);
|
|
13676
|
+
}
|
|
13677
|
+
config = ProviderConfig7.azureAd(tenantId, clientId, clientSecret);
|
|
13678
|
+
break;
|
|
13679
|
+
case "google":
|
|
13680
|
+
if (!clientId || !clientSecret) {
|
|
13681
|
+
console.error(import_chalk20.default.red("Error: Google requires --client-id, --client-secret"));
|
|
13682
|
+
process.exit(1);
|
|
13683
|
+
}
|
|
13684
|
+
config = ProviderConfig7.google(clientId, clientSecret);
|
|
13685
|
+
break;
|
|
13686
|
+
default:
|
|
13687
|
+
console.error(import_chalk20.default.red(`Error: Unknown provider "${provider}"`));
|
|
13688
|
+
process.exit(1);
|
|
13689
|
+
}
|
|
13690
|
+
const ssoConfig = {
|
|
13691
|
+
enabled: true,
|
|
13692
|
+
provider,
|
|
13693
|
+
config: {
|
|
13694
|
+
...config,
|
|
13695
|
+
redirectUri: "http://localhost:8400/callback"
|
|
13696
|
+
}
|
|
13697
|
+
};
|
|
13698
|
+
saveSSOConfig(ssoConfig);
|
|
13699
|
+
console.log(import_chalk20.default.green("\u2705 SSO configured successfully"));
|
|
13700
|
+
console.log(import_chalk20.default.gray(` Provider: ${provider}`));
|
|
13701
|
+
console.log(import_chalk20.default.gray(` Issuer: ${config.issuer}`));
|
|
13702
|
+
console.log(import_chalk20.default.gray("\nRun `infra-cost login --sso` to authenticate"));
|
|
13703
|
+
}
|
|
13704
|
+
__name(handleConfigure, "handleConfigure");
|
|
13705
|
+
async function handleStatus2(options) {
|
|
13706
|
+
const config = loadSSOConfig();
|
|
13707
|
+
console.log(import_chalk20.default.bold("\n\u{1F510} SSO Status\n"));
|
|
13708
|
+
if (!config || !config.enabled) {
|
|
13709
|
+
console.log(import_chalk20.default.yellow("SSO not configured"));
|
|
13710
|
+
console.log(import_chalk20.default.gray("\nRun `infra-cost sso configure` to set up SSO"));
|
|
13711
|
+
return;
|
|
13712
|
+
}
|
|
13713
|
+
console.log(import_chalk20.default.bold("Configuration:"));
|
|
13714
|
+
console.log(import_chalk20.default.gray(` Provider: ${config.provider}`));
|
|
13715
|
+
console.log(import_chalk20.default.gray(` Issuer: ${config.config.issuer}`));
|
|
13716
|
+
console.log(import_chalk20.default.gray(` Client ID: ${config.config.clientId}`));
|
|
13717
|
+
console.log("");
|
|
13718
|
+
const session = loadSSOSession();
|
|
13719
|
+
if (!session) {
|
|
13720
|
+
console.log(import_chalk20.default.yellow("Not logged in"));
|
|
13721
|
+
console.log(import_chalk20.default.gray("\nRun `infra-cost login --sso` to authenticate"));
|
|
13722
|
+
return;
|
|
13723
|
+
}
|
|
13724
|
+
console.log(import_chalk20.default.bold("Session:"));
|
|
13725
|
+
console.log(import_chalk20.default.green(` \u2705 Logged in as ${session.email}`));
|
|
13726
|
+
const minutesLeft = getMinutesUntilExpiration();
|
|
13727
|
+
if (minutesLeft !== null) {
|
|
13728
|
+
if (minutesLeft > 60) {
|
|
13729
|
+
const hours = Math.floor(minutesLeft / 60);
|
|
13730
|
+
console.log(import_chalk20.default.gray(` Expires in ${hours} hour${hours !== 1 ? "s" : ""}`));
|
|
13731
|
+
} else if (minutesLeft > 0) {
|
|
13732
|
+
console.log(import_chalk20.default.gray(` Expires in ${minutesLeft} minute${minutesLeft !== 1 ? "s" : ""}`));
|
|
13733
|
+
} else {
|
|
13734
|
+
console.log(import_chalk20.default.red(" Session expired - please login again"));
|
|
13735
|
+
}
|
|
13736
|
+
}
|
|
13737
|
+
console.log("");
|
|
13738
|
+
}
|
|
13739
|
+
__name(handleStatus2, "handleStatus");
|
|
13740
|
+
async function handleLogin(options) {
|
|
13741
|
+
const { sso } = options;
|
|
13742
|
+
if (!sso) {
|
|
13743
|
+
console.log(import_chalk20.default.yellow("Use --sso flag for SSO login"));
|
|
13744
|
+
console.log(import_chalk20.default.gray("\nExample: infra-cost login --sso"));
|
|
13745
|
+
return;
|
|
13746
|
+
}
|
|
13747
|
+
const config = loadSSOConfig();
|
|
13748
|
+
if (!config || !config.enabled) {
|
|
13749
|
+
console.error(import_chalk20.default.red("Error: SSO not configured"));
|
|
13750
|
+
console.log(import_chalk20.default.gray("\nRun `infra-cost sso configure` to set up SSO"));
|
|
13751
|
+
process.exit(1);
|
|
13752
|
+
}
|
|
13753
|
+
console.log(import_chalk20.default.blue("\u{1F510} Starting SSO login...\n"));
|
|
13754
|
+
const authUrl = generateAuthorizationUrl(config);
|
|
13755
|
+
console.log(import_chalk20.default.bold("Opening browser for authentication..."));
|
|
13756
|
+
console.log(import_chalk20.default.gray(`Provider: ${config.provider}`));
|
|
13757
|
+
console.log(import_chalk20.default.gray(`URL: ${authUrl}
|
|
13758
|
+
`));
|
|
13759
|
+
console.log(import_chalk20.default.gray("\u23F3 Waiting for authentication...\n"));
|
|
13760
|
+
setTimeout(() => {
|
|
13761
|
+
const session = {
|
|
13762
|
+
provider: config.provider,
|
|
13763
|
+
user: "John Doe",
|
|
13764
|
+
email: "john.doe@company.com",
|
|
13765
|
+
accessToken: "simulated_access_token",
|
|
13766
|
+
refreshToken: "simulated_refresh_token",
|
|
13767
|
+
idToken: "simulated_id_token",
|
|
13768
|
+
expiresAt: new Date(Date.now() + 3600 * 1e3).toISOString()
|
|
13769
|
+
// 1 hour
|
|
13770
|
+
};
|
|
13771
|
+
saveSSOSession(session);
|
|
13772
|
+
console.log(import_chalk20.default.green("\u2705 Successfully logged in!"));
|
|
13773
|
+
console.log(import_chalk20.default.gray(` User: ${session.email}`));
|
|
13774
|
+
console.log(import_chalk20.default.gray(` Provider: ${config.provider}`));
|
|
13775
|
+
console.log(import_chalk20.default.gray(" Session expires in 1 hour\n"));
|
|
13776
|
+
console.log(import_chalk20.default.gray("You can now run infra-cost commands"));
|
|
13777
|
+
}, 1e3);
|
|
13778
|
+
}
|
|
13779
|
+
__name(handleLogin, "handleLogin");
|
|
13780
|
+
async function handleLogout(options) {
|
|
13781
|
+
if (!isLoggedIn()) {
|
|
13782
|
+
console.log(import_chalk20.default.yellow("Not currently logged in"));
|
|
13783
|
+
return;
|
|
13784
|
+
}
|
|
13785
|
+
const user = getCurrentUser();
|
|
13786
|
+
clearSSOSession();
|
|
13787
|
+
console.log(import_chalk20.default.green(`\u2705 Logged out${user ? ` (${user})` : ""}`));
|
|
13788
|
+
}
|
|
13789
|
+
__name(handleLogout, "handleLogout");
|
|
13790
|
+
async function handleRefresh(options) {
|
|
13791
|
+
const session = loadSSOSession();
|
|
13792
|
+
if (!session) {
|
|
13793
|
+
console.error(import_chalk20.default.red("Error: Not logged in"));
|
|
13794
|
+
console.log(import_chalk20.default.gray("\nRun `infra-cost login --sso` to authenticate"));
|
|
13795
|
+
process.exit(1);
|
|
13796
|
+
}
|
|
13797
|
+
console.log(import_chalk20.default.blue("\u{1F504} Refreshing SSO session...\n"));
|
|
13798
|
+
setTimeout(() => {
|
|
13799
|
+
const refreshedSession = {
|
|
13800
|
+
...session,
|
|
13801
|
+
expiresAt: new Date(Date.now() + 3600 * 1e3).toISOString()
|
|
13802
|
+
// 1 hour
|
|
13803
|
+
};
|
|
13804
|
+
saveSSOSession(refreshedSession);
|
|
13805
|
+
console.log(import_chalk20.default.green("\u2705 Session refreshed"));
|
|
13806
|
+
console.log(import_chalk20.default.gray(" New expiration: 1 hour from now\n"));
|
|
13807
|
+
}, 500);
|
|
13808
|
+
}
|
|
13809
|
+
__name(handleRefresh, "handleRefresh");
|
|
13810
|
+
function registerSSOCommands(program) {
|
|
13811
|
+
const sso = program.command("sso").description("SSO/SAML enterprise authentication");
|
|
13812
|
+
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);
|
|
13813
|
+
sso.command("status").description("Show SSO configuration and session status").action(handleStatus2);
|
|
13814
|
+
sso.command("refresh").description("Refresh SSO session").action(handleRefresh);
|
|
13815
|
+
program.command("login").description("Login with SSO").option("--sso", "Use SSO login").action(handleLogin);
|
|
13816
|
+
program.command("logout").description("Logout from SSO session").action(handleLogout);
|
|
13817
|
+
}
|
|
13818
|
+
__name(registerSSOCommands, "registerSSOCommands");
|
|
13819
|
+
|
|
13820
|
+
// src/cli/commands/plugin/index.ts
|
|
13821
|
+
var import_chalk21 = __toESM(require("chalk"));
|
|
13822
|
+
|
|
13823
|
+
// src/core/plugins.ts
|
|
13824
|
+
var import_path6 = require("path");
|
|
13825
|
+
var import_os6 = require("os");
|
|
13826
|
+
var PLUGIN_DIR = (0, import_path6.join)((0, import_os6.homedir)(), ".infra-cost", "plugins");
|
|
13827
|
+
var loadedPlugins = /* @__PURE__ */ new Map();
|
|
13828
|
+
function getLoadedPlugins() {
|
|
13829
|
+
return Array.from(loadedPlugins.values());
|
|
13830
|
+
}
|
|
13831
|
+
__name(getLoadedPlugins, "getLoadedPlugins");
|
|
13832
|
+
|
|
13833
|
+
// src/cli/commands/plugin/index.ts
|
|
13834
|
+
async function handleList3(options) {
|
|
13835
|
+
const plugins = getLoadedPlugins();
|
|
13836
|
+
console.log(import_chalk21.default.bold("\n\u{1F50C} Installed Plugins\n"));
|
|
13837
|
+
if (plugins.length === 0) {
|
|
13838
|
+
console.log(import_chalk21.default.yellow("No plugins installed"));
|
|
13839
|
+
console.log(import_chalk21.default.gray("\nPlugins should be installed in: ~/.infra-cost/plugins/"));
|
|
13840
|
+
return;
|
|
13841
|
+
}
|
|
13842
|
+
plugins.forEach((plugin) => {
|
|
13843
|
+
console.log(import_chalk21.default.bold(`${plugin.name} v${plugin.version}`));
|
|
13844
|
+
console.log(import_chalk21.default.gray(` ${plugin.description}`));
|
|
13845
|
+
if (plugin.author) {
|
|
13846
|
+
console.log(import_chalk21.default.gray(` Author: ${plugin.author}`));
|
|
13847
|
+
}
|
|
13848
|
+
console.log("");
|
|
13849
|
+
});
|
|
13850
|
+
}
|
|
13851
|
+
__name(handleList3, "handleList");
|
|
13852
|
+
async function handleInfo(options) {
|
|
13853
|
+
console.log(import_chalk21.default.bold("\n\u{1F50C} Plugin System Information\n"));
|
|
13854
|
+
console.log(import_chalk21.default.bold("Plugin Directory:"));
|
|
13855
|
+
console.log(import_chalk21.default.gray(" ~/.infra-cost/plugins/\n"));
|
|
13856
|
+
console.log(import_chalk21.default.bold("Plugin Structure:"));
|
|
13857
|
+
console.log(import_chalk21.default.gray(" my-plugin/"));
|
|
13858
|
+
console.log(import_chalk21.default.gray(" \u251C\u2500\u2500 package.json # Plugin metadata"));
|
|
13859
|
+
console.log(import_chalk21.default.gray(" \u2514\u2500\u2500 index.js # Plugin entry point\n"));
|
|
13860
|
+
console.log(import_chalk21.default.bold("Example package.json:"));
|
|
13861
|
+
console.log(import_chalk21.default.gray(" {"));
|
|
13862
|
+
console.log(import_chalk21.default.gray(' "name": "my-custom-plugin",'));
|
|
13863
|
+
console.log(import_chalk21.default.gray(' "version": "1.0.0",'));
|
|
13864
|
+
console.log(import_chalk21.default.gray(' "description": "Custom cost provider"'));
|
|
13865
|
+
console.log(import_chalk21.default.gray(" }\n"));
|
|
13866
|
+
console.log(import_chalk21.default.bold("Example index.js:"));
|
|
13867
|
+
console.log(import_chalk21.default.gray(" module.exports = {"));
|
|
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(' init: async () => { console.log("Plugin loaded!"); },'));
|
|
13872
|
+
console.log(import_chalk21.default.gray(" registerCommands: (program) => { /* ... */ }"));
|
|
13873
|
+
console.log(import_chalk21.default.gray(" };\n"));
|
|
13874
|
+
console.log(import_chalk21.default.gray("For documentation: https://github.com/codecollab-co/infra-cost#plugins"));
|
|
13875
|
+
}
|
|
13876
|
+
__name(handleInfo, "handleInfo");
|
|
13877
|
+
function registerPluginCommands(program) {
|
|
13878
|
+
const plugin = program.command("plugin").description("Manage custom plugins");
|
|
13879
|
+
plugin.command("list").description("List installed plugins").action(handleList3);
|
|
13880
|
+
plugin.command("info").description("Show plugin system information").action(handleInfo);
|
|
13881
|
+
}
|
|
13882
|
+
__name(registerPluginCommands, "registerPluginCommands");
|
|
13883
|
+
|
|
12043
13884
|
// src/cli/middleware/auth.ts
|
|
12044
13885
|
async function authMiddleware(thisCommand, actionCommand) {
|
|
12045
13886
|
const isConfigCommand = actionCommand.name() === "config" || actionCommand.parent?.name() === "config";
|
|
@@ -12068,7 +13909,7 @@ async function validationMiddleware(thisCommand, actionCommand) {
|
|
|
12068
13909
|
__name(validationMiddleware, "validationMiddleware");
|
|
12069
13910
|
|
|
12070
13911
|
// src/cli/middleware/error-handler.ts
|
|
12071
|
-
var
|
|
13912
|
+
var import_chalk22 = __toESM(require("chalk"));
|
|
12072
13913
|
function errorHandler(error) {
|
|
12073
13914
|
const message = error?.message ?? String(error);
|
|
12074
13915
|
const stack = error?.stack;
|
|
@@ -12076,15 +13917,15 @@ function errorHandler(error) {
|
|
|
12076
13917
|
return;
|
|
12077
13918
|
}
|
|
12078
13919
|
console.error("");
|
|
12079
|
-
console.error(
|
|
13920
|
+
console.error(import_chalk22.default.red("\u2716"), import_chalk22.default.bold("Error:"), message);
|
|
12080
13921
|
if (process.env.DEBUG || process.env.VERBOSE) {
|
|
12081
13922
|
console.error("");
|
|
12082
13923
|
if (stack) {
|
|
12083
|
-
console.error(
|
|
13924
|
+
console.error(import_chalk22.default.gray(stack));
|
|
12084
13925
|
}
|
|
12085
13926
|
} else {
|
|
12086
13927
|
console.error("");
|
|
12087
|
-
console.error(
|
|
13928
|
+
console.error(import_chalk22.default.gray("Run with --verbose for detailed error information"));
|
|
12088
13929
|
}
|
|
12089
13930
|
console.error("");
|
|
12090
13931
|
}
|
|
@@ -12109,6 +13950,10 @@ function createCLI() {
|
|
|
12109
13950
|
registerDashboardCommands(program);
|
|
12110
13951
|
registerGitCommands(program);
|
|
12111
13952
|
registerTerraformCommand(program);
|
|
13953
|
+
registerSchedulerCommands(program);
|
|
13954
|
+
registerRBACCommands(program);
|
|
13955
|
+
registerSSOCommands(program);
|
|
13956
|
+
registerPluginCommands(program);
|
|
12112
13957
|
program.hook("preAction", async (thisCommand, actionCommand) => {
|
|
12113
13958
|
const opts = thisCommand.opts();
|
|
12114
13959
|
const logLevel = opts.verbose ? "debug" : opts.quiet ? "error" : opts.logLevel;
|