cloud-cost-cli 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AZURE_DISK_PRICING = void 0;
4
+ exports.analyzeAzureDisks = analyzeAzureDisks;
5
+ // Azure Managed Disk pricing (per GB/month, East US)
6
+ exports.AZURE_DISK_PRICING = {
7
+ 'Premium_LRS': 0.135, // Premium SSD
8
+ 'StandardSSD_LRS': 0.075, // Standard SSD
9
+ 'Standard_LRS': 0.045, // Standard HDD
10
+ };
11
+ function getDiskMonthlyCost(sizeGB, diskType) {
12
+ const pricePerGB = exports.AZURE_DISK_PRICING[diskType] || 0.075;
13
+ return sizeGB * pricePerGB;
14
+ }
15
+ async function analyzeAzureDisks(client) {
16
+ const computeClient = client.getComputeClient();
17
+ const opportunities = [];
18
+ try {
19
+ // List all managed disks
20
+ const disks = computeClient.disks.list();
21
+ for await (const disk of disks) {
22
+ if (!disk.id || !disk.name)
23
+ continue;
24
+ // Filter by location if specified
25
+ if (client.location && disk.location?.toLowerCase() !== client.location.toLowerCase()) {
26
+ continue;
27
+ }
28
+ const sizeGB = disk.diskSizeGB || 0;
29
+ const diskType = disk.sku?.name || 'Standard_LRS';
30
+ const currentCost = getDiskMonthlyCost(sizeGB, diskType);
31
+ // Opportunity 1: Unattached disk
32
+ if (disk.diskState === 'Unattached') {
33
+ opportunities.push({
34
+ id: `azure-disk-unattached-${disk.name}`,
35
+ provider: 'azure',
36
+ resourceType: 'disk',
37
+ resourceId: disk.id,
38
+ resourceName: disk.name,
39
+ category: 'unused',
40
+ currentCost,
41
+ estimatedSavings: currentCost,
42
+ confidence: 'high',
43
+ recommendation: `Unattached disk (${sizeGB} GB). Delete if no longer needed.`,
44
+ metadata: {
45
+ sizeGB,
46
+ diskType,
47
+ location: disk.location,
48
+ diskState: disk.diskState,
49
+ },
50
+ detectedAt: new Date(),
51
+ });
52
+ }
53
+ // Opportunity 2: Premium disk that could be Standard SSD
54
+ else if (diskType === 'Premium_LRS' && sizeGB < 256) {
55
+ const newType = 'StandardSSD_LRS';
56
+ const newCost = getDiskMonthlyCost(sizeGB, newType);
57
+ const savings = currentCost - newCost;
58
+ if (savings > 5) {
59
+ opportunities.push({
60
+ id: `azure-disk-premium-${disk.name}`,
61
+ provider: 'azure',
62
+ resourceType: 'disk',
63
+ resourceId: disk.id,
64
+ resourceName: disk.name,
65
+ category: 'oversized',
66
+ currentCost,
67
+ estimatedSavings: savings,
68
+ confidence: 'medium',
69
+ recommendation: `Consider switching from Premium SSD to Standard SSD for non-performance-critical workloads.`,
70
+ metadata: {
71
+ sizeGB,
72
+ currentType: diskType,
73
+ suggestedType: newType,
74
+ location: disk.location,
75
+ },
76
+ detectedAt: new Date(),
77
+ });
78
+ }
79
+ }
80
+ }
81
+ return opportunities;
82
+ }
83
+ catch (error) {
84
+ console.error('Error analyzing Azure disks:', error);
85
+ return opportunities;
86
+ }
87
+ }
@@ -0,0 +1,6 @@
1
+ export { AzureClient } from './client';
2
+ export { analyzeAzureVMs } from './vms';
3
+ export { analyzeAzureDisks } from './disks';
4
+ export { analyzeAzureStorage } from './storage';
5
+ export { analyzeAzureSQL } from './sql';
6
+ export { analyzeAzurePublicIPs } from './public-ips';
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeAzurePublicIPs = exports.analyzeAzureSQL = exports.analyzeAzureStorage = exports.analyzeAzureDisks = exports.analyzeAzureVMs = exports.AzureClient = void 0;
4
+ var client_1 = require("./client");
5
+ Object.defineProperty(exports, "AzureClient", { enumerable: true, get: function () { return client_1.AzureClient; } });
6
+ var vms_1 = require("./vms");
7
+ Object.defineProperty(exports, "analyzeAzureVMs", { enumerable: true, get: function () { return vms_1.analyzeAzureVMs; } });
8
+ var disks_1 = require("./disks");
9
+ Object.defineProperty(exports, "analyzeAzureDisks", { enumerable: true, get: function () { return disks_1.analyzeAzureDisks; } });
10
+ var storage_1 = require("./storage");
11
+ Object.defineProperty(exports, "analyzeAzureStorage", { enumerable: true, get: function () { return storage_1.analyzeAzureStorage; } });
12
+ var sql_1 = require("./sql");
13
+ Object.defineProperty(exports, "analyzeAzureSQL", { enumerable: true, get: function () { return sql_1.analyzeAzureSQL; } });
14
+ var public_ips_1 = require("./public-ips");
15
+ Object.defineProperty(exports, "analyzeAzurePublicIPs", { enumerable: true, get: function () { return public_ips_1.analyzeAzurePublicIPs; } });
@@ -0,0 +1,3 @@
1
+ import { AzureClient } from './client';
2
+ import { SavingsOpportunity } from '../../types/opportunity';
3
+ export declare function analyzeAzurePublicIPs(client: AzureClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeAzurePublicIPs = analyzeAzurePublicIPs;
4
+ // Azure Public IP pricing (per month, East US)
5
+ const PUBLIC_IP_MONTHLY_COST = 3.65; // Static IP address
6
+ async function analyzeAzurePublicIPs(client) {
7
+ const networkClient = client.getNetworkClient();
8
+ const opportunities = [];
9
+ try {
10
+ // List all public IP addresses
11
+ const publicIPs = networkClient.publicIPAddresses.listAll();
12
+ for await (const ip of publicIPs) {
13
+ if (!ip.id || !ip.name)
14
+ continue;
15
+ // Filter by location if specified
16
+ if (client.location && ip.location?.toLowerCase() !== client.location.toLowerCase()) {
17
+ continue;
18
+ }
19
+ // Opportunity: Unassociated public IP
20
+ if (!ip.ipConfiguration) {
21
+ opportunities.push({
22
+ id: `azure-ip-unassociated-${ip.name}`,
23
+ provider: 'azure',
24
+ resourceType: 'public-ip',
25
+ resourceId: ip.id,
26
+ resourceName: ip.name,
27
+ category: 'unused',
28
+ currentCost: PUBLIC_IP_MONTHLY_COST,
29
+ estimatedSavings: PUBLIC_IP_MONTHLY_COST,
30
+ confidence: 'high',
31
+ recommendation: 'Unassociated public IP address. Delete if not needed.',
32
+ metadata: {
33
+ ipAddress: ip.ipAddress,
34
+ allocationMethod: ip.publicIPAllocationMethod,
35
+ location: ip.location,
36
+ },
37
+ detectedAt: new Date(),
38
+ });
39
+ }
40
+ }
41
+ return opportunities;
42
+ }
43
+ catch (error) {
44
+ console.error('Error analyzing Azure public IPs:', error);
45
+ return opportunities;
46
+ }
47
+ }
@@ -0,0 +1,4 @@
1
+ import { AzureClient } from './client';
2
+ import { SavingsOpportunity } from '../../types/opportunity';
3
+ export declare const AZURE_SQL_PRICING: Record<string, number>;
4
+ export declare function analyzeAzureSQL(client: AzureClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AZURE_SQL_PRICING = void 0;
4
+ exports.analyzeAzureSQL = analyzeAzureSQL;
5
+ // Azure SQL Database pricing (per month, East US, vCore model)
6
+ exports.AZURE_SQL_PRICING = {
7
+ 'GP_Gen5_2': 438.29, // General Purpose, 2 vCores
8
+ 'GP_Gen5_4': 876.58, // General Purpose, 4 vCores
9
+ 'GP_Gen5_8': 1753.16, // General Purpose, 8 vCores
10
+ 'BC_Gen5_2': 876.58, // Business Critical, 2 vCores
11
+ 'BC_Gen5_4': 1753.16, // Business Critical, 4 vCores
12
+ };
13
+ function getSQLMonthlyCost(sku) {
14
+ return exports.AZURE_SQL_PRICING[sku] || 500; // Fallback estimate
15
+ }
16
+ async function analyzeAzureSQL(client) {
17
+ const sqlClient = client.getSqlClient();
18
+ const monitorClient = client.getMonitorClient();
19
+ const opportunities = [];
20
+ try {
21
+ // List all SQL servers
22
+ const servers = sqlClient.servers.list();
23
+ for await (const server of servers) {
24
+ if (!server.id || !server.name)
25
+ continue;
26
+ // Filter by location if specified
27
+ if (client.location && server.location?.toLowerCase() !== client.location.toLowerCase()) {
28
+ continue;
29
+ }
30
+ const resourceGroup = extractResourceGroup(server.id);
31
+ if (!resourceGroup)
32
+ continue;
33
+ try {
34
+ // List databases on this server
35
+ const databases = sqlClient.databases.listByServer(resourceGroup, server.name);
36
+ for await (const db of databases) {
37
+ if (!db.id || !db.name || db.name === 'master')
38
+ continue;
39
+ const sku = db.sku?.name || 'Unknown';
40
+ const tier = db.sku?.tier || 'Unknown';
41
+ const currentCost = getSQLMonthlyCost(sku);
42
+ // Get DTU/CPU usage metrics
43
+ const avgDtu = await getAverageDTU(monitorClient, db.id, 7);
44
+ // Opportunity 1: Low utilization database
45
+ if (avgDtu < 10) {
46
+ opportunities.push({
47
+ id: `azure-sql-underutilized-${db.name}`,
48
+ provider: 'azure',
49
+ resourceType: 'sql',
50
+ resourceId: db.id,
51
+ resourceName: db.name,
52
+ category: 'oversized',
53
+ currentCost,
54
+ estimatedSavings: currentCost * 0.5, // Could downsize by ~50%
55
+ confidence: 'medium',
56
+ recommendation: `Database is underutilized (${avgDtu.toFixed(1)}% avg DTU). Consider downsizing tier.`,
57
+ metadata: {
58
+ sku,
59
+ tier,
60
+ avgDtu,
61
+ server: server.name,
62
+ location: db.location,
63
+ },
64
+ detectedAt: new Date(),
65
+ });
66
+ }
67
+ // Opportunity 2: Business Critical that could be General Purpose
68
+ if (tier === 'BusinessCritical' && avgDtu < 30) {
69
+ opportunities.push({
70
+ id: `azure-sql-tier-${db.name}`,
71
+ provider: 'azure',
72
+ resourceType: 'sql',
73
+ resourceId: db.id,
74
+ resourceName: db.name,
75
+ category: 'oversized',
76
+ currentCost,
77
+ estimatedSavings: currentCost * 0.5, // BC is ~2x GP cost
78
+ confidence: 'low',
79
+ recommendation: 'Consider switching from Business Critical to General Purpose tier if high availability is not required.',
80
+ metadata: {
81
+ sku,
82
+ tier,
83
+ avgDtu,
84
+ server: server.name,
85
+ },
86
+ detectedAt: new Date(),
87
+ });
88
+ }
89
+ }
90
+ }
91
+ catch (error) {
92
+ // Skip this server if we can't fetch databases
93
+ continue;
94
+ }
95
+ }
96
+ return opportunities;
97
+ }
98
+ catch (error) {
99
+ console.error('Error analyzing Azure SQL:', error);
100
+ return opportunities;
101
+ }
102
+ }
103
+ async function getAverageDTU(monitorClient, resourceId, days) {
104
+ try {
105
+ const endTime = new Date();
106
+ const startTime = new Date();
107
+ startTime.setDate(startTime.getDate() - days);
108
+ const metrics = await monitorClient.metrics.list(resourceId, {
109
+ timespan: `${startTime.toISOString()}/${endTime.toISOString()}`,
110
+ interval: 'PT1H',
111
+ metricnames: 'dtu_consumption_percent',
112
+ aggregation: 'Average',
113
+ });
114
+ const timeseries = metrics.value?.[0]?.timeseries?.[0]?.data || [];
115
+ if (timeseries.length === 0) {
116
+ return 0;
117
+ }
118
+ const values = timeseries
119
+ .map((d) => d.average)
120
+ .filter((v) => v !== null && v !== undefined);
121
+ if (values.length === 0) {
122
+ return 0;
123
+ }
124
+ const sum = values.reduce((a, b) => a + b, 0);
125
+ return sum / values.length;
126
+ }
127
+ catch (error) {
128
+ return 0;
129
+ }
130
+ }
131
+ function extractResourceGroup(resourceId) {
132
+ const match = resourceId.match(/resourceGroups\/([^\/]+)/i);
133
+ return match ? match[1] : null;
134
+ }
@@ -0,0 +1,8 @@
1
+ import { AzureClient } from './client';
2
+ import { SavingsOpportunity } from '../../types/opportunity';
3
+ export declare const AZURE_STORAGE_PRICING: {
4
+ hot: number;
5
+ cool: number;
6
+ archive: number;
7
+ };
8
+ export declare function analyzeAzureStorage(client: AzureClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AZURE_STORAGE_PRICING = void 0;
4
+ exports.analyzeAzureStorage = analyzeAzureStorage;
5
+ // Azure Blob Storage pricing (per GB/month, East US)
6
+ exports.AZURE_STORAGE_PRICING = {
7
+ hot: 0.0184,
8
+ cool: 0.01,
9
+ archive: 0.002,
10
+ };
11
+ async function analyzeAzureStorage(client) {
12
+ const storageClient = client.getStorageClient();
13
+ const opportunities = [];
14
+ try {
15
+ // List all storage accounts
16
+ const accounts = storageClient.storageAccounts.list();
17
+ for await (const account of accounts) {
18
+ if (!account.id || !account.name)
19
+ continue;
20
+ // Filter by location if specified
21
+ if (client.location && account.location?.toLowerCase() !== client.location.toLowerCase()) {
22
+ continue;
23
+ }
24
+ const resourceGroup = extractResourceGroup(account.id);
25
+ if (!resourceGroup)
26
+ continue;
27
+ try {
28
+ // Get blob service properties
29
+ const blobServices = await storageClient.blobServices.getServiceProperties(resourceGroup, account.name);
30
+ // Check if lifecycle management is enabled
31
+ const hasLifecyclePolicy = await hasLifecycleManagement(storageClient, resourceGroup, account.name);
32
+ if (!hasLifecyclePolicy) {
33
+ // Estimate potential savings (assume 30% of data could move to cool/archive)
34
+ // This is a rough estimate - actual savings depend on usage patterns
35
+ const estimatedSavings = 50; // Conservative estimate
36
+ opportunities.push({
37
+ id: `azure-storage-lifecycle-${account.name}`,
38
+ provider: 'azure',
39
+ resourceType: 'storage',
40
+ resourceId: account.id,
41
+ resourceName: account.name,
42
+ category: 'misconfigured',
43
+ currentCost: 100, // Placeholder - hard to estimate without usage data
44
+ estimatedSavings,
45
+ confidence: 'low',
46
+ recommendation: 'Enable lifecycle management to automatically move old data to Cool or Archive tiers.',
47
+ metadata: {
48
+ location: account.location,
49
+ accountType: account.kind,
50
+ replication: account.sku?.name,
51
+ },
52
+ detectedAt: new Date(),
53
+ });
54
+ }
55
+ // Check for public access
56
+ if (account.allowBlobPublicAccess === true) {
57
+ opportunities.push({
58
+ id: `azure-storage-public-${account.name}`,
59
+ provider: 'azure',
60
+ resourceType: 'storage',
61
+ resourceId: account.id,
62
+ resourceName: account.name,
63
+ category: 'misconfigured',
64
+ currentCost: 0,
65
+ estimatedSavings: 0,
66
+ confidence: 'high',
67
+ recommendation: 'Public blob access is enabled. Review security settings.',
68
+ metadata: {
69
+ location: account.location,
70
+ issue: 'security',
71
+ },
72
+ detectedAt: new Date(),
73
+ });
74
+ }
75
+ }
76
+ catch (error) {
77
+ // Skip this account if we can't fetch details
78
+ continue;
79
+ }
80
+ }
81
+ return opportunities;
82
+ }
83
+ catch (error) {
84
+ console.error('Error analyzing Azure storage:', error);
85
+ return opportunities;
86
+ }
87
+ }
88
+ async function hasLifecycleManagement(storageClient, resourceGroup, accountName) {
89
+ try {
90
+ const policy = await storageClient.managementPolicies.get(resourceGroup, accountName, 'default');
91
+ return policy && policy.policy && policy.policy.rules && policy.policy.rules.length > 0;
92
+ }
93
+ catch (error) {
94
+ return false;
95
+ }
96
+ }
97
+ function extractResourceGroup(resourceId) {
98
+ const match = resourceId.match(/resourceGroups\/([^\/]+)/i);
99
+ return match ? match[1] : null;
100
+ }
@@ -0,0 +1,4 @@
1
+ import { AzureClient } from './client';
2
+ import { SavingsOpportunity } from '../../types/opportunity';
3
+ export declare const AZURE_VM_PRICING: Record<string, number>;
4
+ export declare function analyzeAzureVMs(client: AzureClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.AZURE_VM_PRICING = void 0;
7
+ exports.analyzeAzureVMs = analyzeAzureVMs;
8
+ const dayjs_1 = __importDefault(require("dayjs"));
9
+ // Azure VM pricing (East US, pay-as-you-go, monthly estimate)
10
+ // Source: Azure pricing as of 2026-01, monthly = hourly Ɨ 730
11
+ exports.AZURE_VM_PRICING = {
12
+ 'Standard_B1s': 7.59,
13
+ 'Standard_B1ms': 15.33,
14
+ 'Standard_B2s': 30.37,
15
+ 'Standard_B2ms': 60.74,
16
+ 'Standard_D2s_v3': 70.08,
17
+ 'Standard_D4s_v3': 140.16,
18
+ 'Standard_D8s_v3': 280.32,
19
+ 'Standard_E2s_v3': 109.50,
20
+ 'Standard_E4s_v3': 219.00,
21
+ 'Standard_F2s_v2': 62.05,
22
+ 'Standard_F4s_v2': 124.10,
23
+ };
24
+ function getVMMonthlyCost(vmSize) {
25
+ return exports.AZURE_VM_PRICING[vmSize] || 100; // Fallback estimate
26
+ }
27
+ async function analyzeAzureVMs(client) {
28
+ const computeClient = client.getComputeClient();
29
+ const monitorClient = client.getMonitorClient();
30
+ const opportunities = [];
31
+ try {
32
+ // List all VMs in the subscription
33
+ const vms = computeClient.virtualMachines.listAll();
34
+ for await (const vm of vms) {
35
+ if (!vm.id || !vm.name)
36
+ continue;
37
+ // Filter by location if specified
38
+ if (client.location && vm.location?.toLowerCase() !== client.location.toLowerCase()) {
39
+ continue;
40
+ }
41
+ const vmSize = vm.hardwareProfile?.vmSize || 'Unknown';
42
+ const powerState = await getVMPowerState(computeClient, vm);
43
+ // Skip stopped/deallocated VMs (no compute cost)
44
+ if (powerState === 'stopped' || powerState === 'deallocated') {
45
+ continue;
46
+ }
47
+ const currentCost = getVMMonthlyCost(vmSize);
48
+ // Get CPU metrics for the last 7 days
49
+ const avgCpu = await getAverageCPU(monitorClient, vm.id, 7);
50
+ // Opportunity 1: Idle VM (low CPU usage)
51
+ if (avgCpu < 5) {
52
+ opportunities.push({
53
+ id: `azure-vm-idle-${vm.name}`,
54
+ provider: 'azure',
55
+ resourceType: 'vm',
56
+ resourceId: vm.id,
57
+ resourceName: vm.name,
58
+ category: 'idle',
59
+ currentCost,
60
+ estimatedSavings: currentCost * 0.9, // Could stop or downsize
61
+ confidence: 'high',
62
+ recommendation: `VM is idle (${avgCpu.toFixed(1)}% avg CPU). Consider stopping or downsizing.`,
63
+ metadata: {
64
+ vmSize,
65
+ avgCpu,
66
+ location: vm.location,
67
+ powerState,
68
+ },
69
+ detectedAt: new Date(),
70
+ });
71
+ }
72
+ // Opportunity 2: Underutilized VM (medium CPU usage)
73
+ else if (avgCpu < 20) {
74
+ const smallerSize = getSmallerVMSize(vmSize);
75
+ if (smallerSize) {
76
+ const newCost = getVMMonthlyCost(smallerSize);
77
+ const savings = currentCost - newCost;
78
+ if (savings > 10) { // At least $10/month savings
79
+ opportunities.push({
80
+ id: `azure-vm-underutilized-${vm.name}`,
81
+ provider: 'azure',
82
+ resourceType: 'vm',
83
+ resourceId: vm.id,
84
+ resourceName: vm.name,
85
+ category: 'oversized',
86
+ currentCost,
87
+ estimatedSavings: savings,
88
+ confidence: 'medium',
89
+ recommendation: `Downsize from ${vmSize} to ${smallerSize} (${avgCpu.toFixed(1)}% avg CPU).`,
90
+ metadata: {
91
+ vmSize,
92
+ suggestedSize: smallerSize,
93
+ avgCpu,
94
+ location: vm.location,
95
+ },
96
+ detectedAt: new Date(),
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+ return opportunities;
103
+ }
104
+ catch (error) {
105
+ console.error('Error analyzing Azure VMs:', error);
106
+ return opportunities;
107
+ }
108
+ }
109
+ async function getVMPowerState(computeClient, vm) {
110
+ try {
111
+ const resourceGroup = extractResourceGroup(vm.id);
112
+ if (!resourceGroup || !vm.name)
113
+ return 'unknown';
114
+ const instanceView = await computeClient.virtualMachines.instanceView(resourceGroup, vm.name);
115
+ const powerState = instanceView.statuses?.find((s) => s.code?.startsWith('PowerState/'));
116
+ return powerState?.code?.replace('PowerState/', '').toLowerCase() || 'unknown';
117
+ }
118
+ catch (error) {
119
+ return 'unknown';
120
+ }
121
+ }
122
+ async function getAverageCPU(monitorClient, resourceId, days) {
123
+ try {
124
+ const endTime = new Date();
125
+ const startTime = (0, dayjs_1.default)().subtract(days, 'days').toDate();
126
+ const metrics = await monitorClient.metrics.list(resourceId, {
127
+ timespan: `${startTime.toISOString()}/${endTime.toISOString()}`,
128
+ interval: 'PT1H',
129
+ metricnames: 'Percentage CPU',
130
+ aggregation: 'Average',
131
+ });
132
+ const timeseries = metrics.value?.[0]?.timeseries?.[0]?.data || [];
133
+ if (timeseries.length === 0) {
134
+ return 0;
135
+ }
136
+ const values = timeseries
137
+ .map((d) => d.average)
138
+ .filter((v) => v !== null && v !== undefined);
139
+ if (values.length === 0) {
140
+ return 0;
141
+ }
142
+ const sum = values.reduce((a, b) => a + b, 0);
143
+ return sum / values.length;
144
+ }
145
+ catch (error) {
146
+ console.error('Error fetching CPU metrics:', error);
147
+ return 0;
148
+ }
149
+ }
150
+ function getSmallerVMSize(currentSize) {
151
+ const downsizeMap = {
152
+ 'Standard_B2ms': 'Standard_B1ms',
153
+ 'Standard_B2s': 'Standard_B1s',
154
+ 'Standard_D4s_v3': 'Standard_D2s_v3',
155
+ 'Standard_D8s_v3': 'Standard_D4s_v3',
156
+ 'Standard_E4s_v3': 'Standard_E2s_v3',
157
+ 'Standard_F4s_v2': 'Standard_F2s_v2',
158
+ };
159
+ return downsizeMap[currentSize] || null;
160
+ }
161
+ function extractResourceGroup(resourceId) {
162
+ const match = resourceId.match(/resourceGroups\/([^\/]+)/i);
163
+ return match ? match[1] : null;
164
+ }
@@ -37,5 +37,7 @@ function renderTable(report, topN = 5) {
37
37
  });
38
38
  console.log(table.toString());
39
39
  console.log(chalk_1.default.bold(`\nTotal potential savings: ${chalk_1.default.green((0, utils_1.formatCurrency)(report.totalPotentialSavings))}/month (${chalk_1.default.green((0, utils_1.formatCurrency)(report.totalPotentialSavings * 12))}/year)`));
40
- console.log(`\nSummary: ${report.summary.totalResources} resources analyzed | ${report.summary.idleResources} idle | ${report.summary.oversizedResources} oversized | ${report.summary.unusedResources} unused\n`);
40
+ console.log(`\nSummary: ${report.summary.totalResources} resources analyzed | ${report.summary.idleResources} idle | ${report.summary.oversizedResources} oversized | ${report.summary.unusedResources} unused`);
41
+ console.log(chalk_1.default.dim(`\nšŸ’” Note: Cost estimates based on us-east-1 pricing and may vary by region.`));
42
+ console.log(chalk_1.default.dim(` For more accurate estimates, actual costs depend on your usage and region.\n`));
41
43
  }
@@ -1,3 +1,5 @@
1
1
  export declare function formatCurrency(amount: number): string;
2
+ export declare function formatBytes(bytes: number, decimals?: number): string;
3
+ export declare function formatPercent(value: number, decimals?: number): string;
2
4
  export declare function formatDate(date: Date): string;
3
5
  export declare function daysSince(date: Date): number;
@@ -1,10 +1,38 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.formatCurrency = formatCurrency;
4
+ exports.formatBytes = formatBytes;
5
+ exports.formatPercent = formatPercent;
4
6
  exports.formatDate = formatDate;
5
7
  exports.daysSince = daysSince;
6
8
  function formatCurrency(amount) {
7
- return `$${amount.toFixed(2)}`;
9
+ const formatted = new Intl.NumberFormat('en-US', {
10
+ style: 'currency',
11
+ currency: 'USD',
12
+ minimumFractionDigits: 2,
13
+ maximumFractionDigits: 2,
14
+ }).format(amount);
15
+ return formatted;
16
+ }
17
+ function formatBytes(bytes, decimals = 2) {
18
+ if (bytes === 0)
19
+ return '0 B';
20
+ const k = 1024;
21
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
22
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
23
+ // Don't use decimals for bytes
24
+ if (i === 0) {
25
+ return `${bytes} B`;
26
+ }
27
+ const value = bytes / Math.pow(k, i);
28
+ const formatted = decimals === 0
29
+ ? Math.round(value).toString()
30
+ : value.toFixed(decimals);
31
+ return `${formatted} ${sizes[i]}`;
32
+ }
33
+ function formatPercent(value, decimals = 1) {
34
+ const percent = value * 100;
35
+ return `${percent.toFixed(decimals)}%`;
8
36
  }
9
37
  function formatDate(date) {
10
38
  return date.toISOString().split('T')[0];