cloud-cost-cli 0.1.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +235 -0
  3. package/dist/bin/cloud-cost-cli.d.ts +2 -0
  4. package/dist/bin/cloud-cost-cli.js +23 -0
  5. package/dist/src/analyzers/cost-estimator.d.ts +21 -0
  6. package/dist/src/analyzers/cost-estimator.js +86 -0
  7. package/dist/src/commands/scan.d.ts +12 -0
  8. package/dist/src/commands/scan.js +111 -0
  9. package/dist/src/providers/aws/client.d.ts +27 -0
  10. package/dist/src/providers/aws/client.js +53 -0
  11. package/dist/src/providers/aws/ebs.d.ts +3 -0
  12. package/dist/src/providers/aws/ebs.js +41 -0
  13. package/dist/src/providers/aws/ec2.d.ts +3 -0
  14. package/dist/src/providers/aws/ec2.js +75 -0
  15. package/dist/src/providers/aws/eip.d.ts +3 -0
  16. package/dist/src/providers/aws/eip.js +38 -0
  17. package/dist/src/providers/aws/elb.d.ts +3 -0
  18. package/dist/src/providers/aws/elb.js +66 -0
  19. package/dist/src/providers/aws/rds.d.ts +3 -0
  20. package/dist/src/providers/aws/rds.js +92 -0
  21. package/dist/src/providers/aws/s3.d.ts +3 -0
  22. package/dist/src/providers/aws/s3.js +54 -0
  23. package/dist/src/reporters/json.d.ts +2 -0
  24. package/dist/src/reporters/json.js +6 -0
  25. package/dist/src/reporters/table.d.ts +2 -0
  26. package/dist/src/reporters/table.js +41 -0
  27. package/dist/src/types/index.d.ts +2 -0
  28. package/dist/src/types/index.js +18 -0
  29. package/dist/src/types/opportunity.d.ts +31 -0
  30. package/dist/src/types/opportunity.js +2 -0
  31. package/dist/src/types/provider.d.ts +15 -0
  32. package/dist/src/types/provider.js +2 -0
  33. package/dist/src/utils/formatter.d.ts +3 -0
  34. package/dist/src/utils/formatter.js +16 -0
  35. package/dist/src/utils/index.d.ts +2 -0
  36. package/dist/src/utils/index.js +18 -0
  37. package/dist/src/utils/logger.d.ts +6 -0
  38. package/dist/src/utils/logger.js +30 -0
  39. package/docs/RELEASE.md +143 -0
  40. package/docs/contributing.md +201 -0
  41. package/docs/iam-policy.json +25 -0
  42. package/docs/installation.md +208 -0
  43. package/package.json +69 -0
@@ -0,0 +1,75 @@
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.analyzeEC2Instances = analyzeEC2Instances;
7
+ const client_ec2_1 = require("@aws-sdk/client-ec2");
8
+ const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
9
+ const cost_estimator_1 = require("../../analyzers/cost-estimator");
10
+ const dayjs_1 = __importDefault(require("dayjs"));
11
+ async function analyzeEC2Instances(client) {
12
+ const ec2Client = client.getEC2Client();
13
+ const cloudwatchClient = client.getCloudWatchClient();
14
+ const result = await ec2Client.send(new client_ec2_1.DescribeInstancesCommand({
15
+ Filters: [{ Name: 'instance-state-name', Values: ['running'] }],
16
+ }));
17
+ const instances = [];
18
+ for (const reservation of result.Reservations || []) {
19
+ instances.push(...(reservation.Instances || []));
20
+ }
21
+ const opportunities = [];
22
+ for (const instance of instances) {
23
+ if (!instance.InstanceId || !instance.InstanceType)
24
+ continue;
25
+ // Get average CPU over last 30 days
26
+ const avgCpu = await getAvgCPU(cloudwatchClient, instance.InstanceId, 30);
27
+ if (avgCpu < 5) {
28
+ const monthlyCost = (0, cost_estimator_1.getEC2MonthlyCost)(instance.InstanceType);
29
+ opportunities.push({
30
+ id: `ec2-idle-${instance.InstanceId}`,
31
+ provider: 'aws',
32
+ resourceType: 'ec2',
33
+ resourceId: instance.InstanceId,
34
+ resourceName: instance.Tags?.find((t) => t.Key === 'Name')?.Value,
35
+ category: 'idle',
36
+ currentCost: monthlyCost,
37
+ estimatedSavings: monthlyCost,
38
+ confidence: 'high',
39
+ recommendation: `Stop instance or downsize to t3.small (avg CPU: ${avgCpu.toFixed(1)}%)`,
40
+ metadata: {
41
+ instanceType: instance.InstanceType,
42
+ avgCpu,
43
+ state: instance.State?.Name,
44
+ },
45
+ detectedAt: new Date(),
46
+ });
47
+ }
48
+ }
49
+ return opportunities;
50
+ }
51
+ async function getAvgCPU(cloudwatchClient, instanceId, days) {
52
+ const endTime = new Date();
53
+ const startTime = (0, dayjs_1.default)(endTime).subtract(days, 'day').toDate();
54
+ try {
55
+ const result = await cloudwatchClient.send(new client_cloudwatch_1.GetMetricStatisticsCommand({
56
+ Namespace: 'AWS/EC2',
57
+ MetricName: 'CPUUtilization',
58
+ Dimensions: [{ Name: 'InstanceId', Value: instanceId }],
59
+ StartTime: startTime,
60
+ EndTime: endTime,
61
+ Period: 86400, // 1 day
62
+ Statistics: [client_cloudwatch_1.Statistic.Average],
63
+ }));
64
+ if (!result.Datapoints || result.Datapoints.length === 0) {
65
+ return 0;
66
+ }
67
+ const avg = result.Datapoints.reduce((sum, dp) => sum + (dp.Average || 0), 0) /
68
+ result.Datapoints.length;
69
+ return avg;
70
+ }
71
+ catch (error) {
72
+ // CloudWatch metrics may not be available for all instances
73
+ return 0;
74
+ }
75
+ }
@@ -0,0 +1,3 @@
1
+ import { AWSClient } from './client';
2
+ import { SavingsOpportunity } from '../../types';
3
+ export declare function analyzeElasticIPs(client: AWSClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeElasticIPs = analyzeElasticIPs;
4
+ const client_ec2_1 = require("@aws-sdk/client-ec2");
5
+ const cost_estimator_1 = require("../../analyzers/cost-estimator");
6
+ async function analyzeElasticIPs(client) {
7
+ const ec2Client = client.getEC2Client();
8
+ const result = await ec2Client.send(new client_ec2_1.DescribeAddressesCommand({}));
9
+ const addresses = result.Addresses || [];
10
+ const opportunities = [];
11
+ for (const address of addresses) {
12
+ if (!address.PublicIp)
13
+ continue;
14
+ // Check if EIP is not associated with a running instance
15
+ if (!address.InstanceId || !address.AssociationId) {
16
+ const monthlyCost = (0, cost_estimator_1.getEIPMonthlyCost)();
17
+ opportunities.push({
18
+ id: `eip-unattached-${address.AllocationId || address.PublicIp}`,
19
+ provider: 'aws',
20
+ resourceType: 'eip',
21
+ resourceId: address.AllocationId || address.PublicIp,
22
+ resourceName: address.PublicIp,
23
+ category: 'unused',
24
+ currentCost: monthlyCost,
25
+ estimatedSavings: monthlyCost,
26
+ confidence: 'high',
27
+ recommendation: `Release unattached Elastic IP`,
28
+ metadata: {
29
+ publicIp: address.PublicIp,
30
+ allocationId: address.AllocationId,
31
+ domain: address.Domain,
32
+ },
33
+ detectedAt: new Date(),
34
+ });
35
+ }
36
+ }
37
+ return opportunities;
38
+ }
@@ -0,0 +1,3 @@
1
+ import { AWSClient } from './client';
2
+ import { SavingsOpportunity } from '../../types';
3
+ export declare function analyzeELBs(client: AWSClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeELBs = analyzeELBs;
4
+ const client_elastic_load_balancing_v2_1 = require("@aws-sdk/client-elastic-load-balancing-v2");
5
+ const cost_estimator_1 = require("../../analyzers/cost-estimator");
6
+ async function analyzeELBs(client) {
7
+ const elbClient = client.getELBClient();
8
+ const result = await elbClient.send(new client_elastic_load_balancing_v2_1.DescribeLoadBalancersCommand({}));
9
+ const loadBalancers = result.LoadBalancers || [];
10
+ const opportunities = [];
11
+ for (const lb of loadBalancers) {
12
+ if (!lb.LoadBalancerArn || !lb.LoadBalancerName)
13
+ continue;
14
+ // Check if load balancer has active targets
15
+ const hasActiveTargets = await checkActiveTargets(elbClient, lb.LoadBalancerArn);
16
+ if (!hasActiveTargets) {
17
+ const lbType = lb.Type === 'application' ? 'alb' : lb.Type === 'network' ? 'nlb' : 'alb';
18
+ const monthlyCost = (0, cost_estimator_1.getELBMonthlyCost)(lbType);
19
+ opportunities.push({
20
+ id: `elb-unused-${lb.LoadBalancerName}`,
21
+ provider: 'aws',
22
+ resourceType: 'elb',
23
+ resourceId: lb.LoadBalancerArn,
24
+ resourceName: lb.LoadBalancerName,
25
+ category: 'unused',
26
+ currentCost: monthlyCost,
27
+ estimatedSavings: monthlyCost,
28
+ confidence: 'high',
29
+ recommendation: `Delete unused load balancer (no active targets)`,
30
+ metadata: {
31
+ loadBalancerName: lb.LoadBalancerName,
32
+ type: lb.Type,
33
+ scheme: lb.Scheme,
34
+ createdTime: lb.CreatedTime,
35
+ },
36
+ detectedAt: new Date(),
37
+ });
38
+ }
39
+ }
40
+ return opportunities;
41
+ }
42
+ async function checkActiveTargets(elbClient, loadBalancerArn) {
43
+ try {
44
+ // Get target groups for this load balancer
45
+ const tgResult = await elbClient.send(new client_elastic_load_balancing_v2_1.DescribeTargetGroupsCommand({ LoadBalancerArn: loadBalancerArn }));
46
+ const targetGroups = tgResult.TargetGroups || [];
47
+ if (targetGroups.length === 0) {
48
+ return false; // No target groups = unused
49
+ }
50
+ // Check if any target group has healthy targets
51
+ for (const tg of targetGroups) {
52
+ if (!tg.TargetGroupArn)
53
+ continue;
54
+ const healthResult = await elbClient.send(new client_elastic_load_balancing_v2_1.DescribeTargetHealthCommand({ TargetGroupArn: tg.TargetGroupArn }));
55
+ const healthyTargets = (healthResult.TargetHealthDescriptions || []).filter((t) => t.TargetHealth?.State === 'healthy');
56
+ if (healthyTargets.length > 0) {
57
+ return true; // Has at least one healthy target
58
+ }
59
+ }
60
+ return false; // No healthy targets found
61
+ }
62
+ catch (error) {
63
+ // If we can't determine, assume it's in use (conservative)
64
+ return true;
65
+ }
66
+ }
@@ -0,0 +1,3 @@
1
+ import { AWSClient } from './client';
2
+ import { SavingsOpportunity } from '../../types';
3
+ export declare function analyzeRDSInstances(client: AWSClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,92 @@
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.analyzeRDSInstances = analyzeRDSInstances;
7
+ const client_rds_1 = require("@aws-sdk/client-rds");
8
+ const client_cloudwatch_1 = require("@aws-sdk/client-cloudwatch");
9
+ const cost_estimator_1 = require("../../analyzers/cost-estimator");
10
+ const dayjs_1 = __importDefault(require("dayjs"));
11
+ async function analyzeRDSInstances(client) {
12
+ const rdsClient = client.getRDSClient();
13
+ const cloudwatchClient = client.getCloudWatchClient();
14
+ const result = await rdsClient.send(new client_rds_1.DescribeDBInstancesCommand({}));
15
+ const instances = result.DBInstances || [];
16
+ const opportunities = [];
17
+ for (const instance of instances) {
18
+ if (!instance.DBInstanceIdentifier || !instance.DBInstanceClass)
19
+ continue;
20
+ // Get average CPU and connections over last 30 days
21
+ const avgCpu = await getAvgMetric(cloudwatchClient, instance.DBInstanceIdentifier, 'CPUUtilization', 30);
22
+ const avgConnections = await getAvgMetric(cloudwatchClient, instance.DBInstanceIdentifier, 'DatabaseConnections', 30);
23
+ // Detect oversized: low CPU (<20%) or low connections
24
+ if (avgCpu < 20 && avgCpu > 0) {
25
+ const currentCost = (0, cost_estimator_1.getRDSMonthlyCost)(instance.DBInstanceClass);
26
+ // Estimate smaller instance class (rough heuristic)
27
+ const smallerClass = getSmallerInstanceClass(instance.DBInstanceClass);
28
+ const proposedCost = smallerClass ? (0, cost_estimator_1.getRDSMonthlyCost)(smallerClass) : 0;
29
+ const savings = currentCost - proposedCost;
30
+ if (savings > 0) {
31
+ opportunities.push({
32
+ id: `rds-oversized-${instance.DBInstanceIdentifier}`,
33
+ provider: 'aws',
34
+ resourceType: 'rds',
35
+ resourceId: instance.DBInstanceIdentifier,
36
+ resourceName: instance.DBInstanceIdentifier,
37
+ category: 'oversized',
38
+ currentCost,
39
+ estimatedSavings: savings,
40
+ confidence: 'medium',
41
+ recommendation: `Downsize to ${smallerClass || 'smaller instance'} (avg CPU: ${avgCpu.toFixed(1)}%, avg connections: ${avgConnections.toFixed(0)})`,
42
+ metadata: {
43
+ instanceClass: instance.DBInstanceClass,
44
+ avgCpu,
45
+ avgConnections,
46
+ engine: instance.Engine,
47
+ },
48
+ detectedAt: new Date(),
49
+ });
50
+ }
51
+ }
52
+ }
53
+ return opportunities;
54
+ }
55
+ async function getAvgMetric(cloudwatchClient, dbInstanceId, metricName, days) {
56
+ const endTime = new Date();
57
+ const startTime = (0, dayjs_1.default)(endTime).subtract(days, 'day').toDate();
58
+ try {
59
+ const result = await cloudwatchClient.send(new client_cloudwatch_1.GetMetricStatisticsCommand({
60
+ Namespace: 'AWS/RDS',
61
+ MetricName: metricName,
62
+ Dimensions: [{ Name: 'DBInstanceIdentifier', Value: dbInstanceId }],
63
+ StartTime: startTime,
64
+ EndTime: endTime,
65
+ Period: 86400, // 1 day
66
+ Statistics: [client_cloudwatch_1.Statistic.Average],
67
+ }));
68
+ if (!result.Datapoints || result.Datapoints.length === 0) {
69
+ return 0;
70
+ }
71
+ const avg = result.Datapoints.reduce((sum, dp) => sum + (dp.Average || 0), 0) /
72
+ result.Datapoints.length;
73
+ return avg;
74
+ }
75
+ catch (error) {
76
+ return 0;
77
+ }
78
+ }
79
+ function getSmallerInstanceClass(currentClass) {
80
+ // Simple downsize heuristic: xlarge -> large, large -> medium, etc.
81
+ const downsizeMap = {
82
+ 'db.t3.large': 'db.t3.medium',
83
+ 'db.t3.xlarge': 'db.t3.large',
84
+ 'db.m5.large': 'db.t3.large',
85
+ 'db.m5.xlarge': 'db.m5.large',
86
+ 'db.m5.2xlarge': 'db.m5.xlarge',
87
+ 'db.r5.large': 'db.t3.large',
88
+ 'db.r5.xlarge': 'db.r5.large',
89
+ 'db.r5.2xlarge': 'db.r5.xlarge',
90
+ };
91
+ return downsizeMap[currentClass] || null;
92
+ }
@@ -0,0 +1,3 @@
1
+ import { AWSClient } from './client';
2
+ import { SavingsOpportunity } from '../../types';
3
+ export declare function analyzeS3Buckets(client: AWSClient): Promise<SavingsOpportunity[]>;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.analyzeS3Buckets = analyzeS3Buckets;
4
+ const client_s3_1 = require("@aws-sdk/client-s3");
5
+ const cost_estimator_1 = require("../../analyzers/cost-estimator");
6
+ async function analyzeS3Buckets(client) {
7
+ const s3Client = client.getS3Client();
8
+ const result = await s3Client.send(new client_s3_1.ListBucketsCommand({}));
9
+ const buckets = result.Buckets || [];
10
+ const opportunities = [];
11
+ for (const bucket of buckets) {
12
+ if (!bucket.Name)
13
+ continue;
14
+ try {
15
+ // Check if bucket has lifecycle policy
16
+ await s3Client.send(new client_s3_1.GetBucketLifecycleConfigurationCommand({ Bucket: bucket.Name }));
17
+ // If we get here, lifecycle exists — skip this bucket
18
+ }
19
+ catch (error) {
20
+ // NoSuchLifecycleConfiguration means no lifecycle policy
21
+ if (error.name === 'NoSuchLifecycleConfiguration') {
22
+ // Estimate bucket size (we can't get exact size without CloudWatch or S3 Storage Lens)
23
+ // For MVP, assume buckets without lifecycle are worth flagging
24
+ // In production, integrate with CloudWatch GetMetricStatistics for BucketSizeBytes
25
+ const estimatedSizeGB = 100; // Placeholder: assume 100 GB for flagged buckets
26
+ const currentCost = (0, cost_estimator_1.getS3MonthlyCost)(estimatedSizeGB, 'standard');
27
+ const glacierCost = (0, cost_estimator_1.getS3MonthlyCost)(estimatedSizeGB * 0.5, 'glacier'); // Assume 50% can move to Glacier
28
+ const savings = currentCost - glacierCost - (estimatedSizeGB * 0.5 * 0.023); // Remaining in standard
29
+ if (savings > 5) {
30
+ // Only flag if savings > $5/month
31
+ opportunities.push({
32
+ id: `s3-no-lifecycle-${bucket.Name}`,
33
+ provider: 'aws',
34
+ resourceType: 's3',
35
+ resourceId: bucket.Name,
36
+ resourceName: bucket.Name,
37
+ category: 'misconfigured',
38
+ currentCost,
39
+ estimatedSavings: savings,
40
+ confidence: 'low',
41
+ recommendation: `Enable lifecycle policy (Intelligent-Tiering or Glacier transition)`,
42
+ metadata: {
43
+ bucketName: bucket.Name,
44
+ estimatedSizeGB,
45
+ creationDate: bucket.CreationDate,
46
+ },
47
+ detectedAt: new Date(),
48
+ });
49
+ }
50
+ }
51
+ }
52
+ }
53
+ return opportunities;
54
+ }
@@ -0,0 +1,2 @@
1
+ import { ScanReport } from '../types';
2
+ export declare function renderJSON(report: ScanReport): void;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderJSON = renderJSON;
4
+ function renderJSON(report) {
5
+ console.log(JSON.stringify(report, null, 2));
6
+ }
@@ -0,0 +1,2 @@
1
+ import { ScanReport } from '../types';
2
+ export declare function renderTable(report: ScanReport, topN?: number): void;
@@ -0,0 +1,41 @@
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.renderTable = renderTable;
7
+ const cli_table3_1 = __importDefault(require("cli-table3"));
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const utils_1 = require("../utils");
10
+ function renderTable(report, topN = 5) {
11
+ console.log(chalk_1.default.bold('\nCloud Cost Optimization Report'));
12
+ console.log(`Provider: ${report.provider} | Region: ${report.region} | Account: ${report.accountId}`);
13
+ console.log(`Analyzed: ${report.scanPeriod.start.toISOString().split('T')[0]} to ${report.scanPeriod.end.toISOString().split('T')[0]}\n`);
14
+ const opportunities = report.opportunities
15
+ .sort((a, b) => b.estimatedSavings - a.estimatedSavings)
16
+ .slice(0, topN);
17
+ if (opportunities.length === 0) {
18
+ console.log(chalk_1.default.green('✓ No cost optimization opportunities found!'));
19
+ return;
20
+ }
21
+ console.log(chalk_1.default.bold(`Top ${opportunities.length} Savings Opportunities (est. ${(0, utils_1.formatCurrency)(report.totalPotentialSavings)}/month):\n`));
22
+ const table = new cli_table3_1.default({
23
+ head: ['#', 'Type', 'Resource ID', 'Recommendation', 'Savings/mo'],
24
+ colWidths: [5, 10, 25, 50, 15],
25
+ style: {
26
+ head: ['cyan'],
27
+ },
28
+ });
29
+ opportunities.forEach((opp, index) => {
30
+ table.push([
31
+ (index + 1).toString(),
32
+ opp.resourceType.toUpperCase(),
33
+ opp.resourceId,
34
+ opp.recommendation,
35
+ chalk_1.default.green((0, utils_1.formatCurrency)(opp.estimatedSavings)),
36
+ ]);
37
+ });
38
+ console.log(table.toString());
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`);
41
+ }
@@ -0,0 +1,2 @@
1
+ export * from './opportunity';
2
+ export * from './provider';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./opportunity"), exports);
18
+ __exportStar(require("./provider"), exports);
@@ -0,0 +1,31 @@
1
+ export interface SavingsOpportunity {
2
+ id: string;
3
+ provider: 'aws' | 'gcp' | 'azure';
4
+ resourceType: string;
5
+ resourceId: string;
6
+ resourceName?: string;
7
+ category: 'idle' | 'oversized' | 'unused' | 'misconfigured';
8
+ currentCost: number;
9
+ estimatedSavings: number;
10
+ confidence: 'high' | 'medium' | 'low';
11
+ recommendation: string;
12
+ metadata: Record<string, any>;
13
+ detectedAt: Date;
14
+ }
15
+ export interface ScanReport {
16
+ provider: string;
17
+ accountId: string;
18
+ region: string;
19
+ scanPeriod: {
20
+ start: Date;
21
+ end: Date;
22
+ };
23
+ opportunities: SavingsOpportunity[];
24
+ totalPotentialSavings: number;
25
+ summary: {
26
+ totalResources: number;
27
+ idleResources: number;
28
+ oversizedResources: number;
29
+ unusedResources: number;
30
+ };
31
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,15 @@
1
+ import { ScanReport } from './opportunity';
2
+ export interface CloudProvider {
3
+ name: string;
4
+ scan(options: ScanOptions): Promise<ScanReport>;
5
+ }
6
+ export interface ScanOptions {
7
+ region?: string;
8
+ profile?: string;
9
+ startDate?: Date;
10
+ endDate?: Date;
11
+ thresholds?: {
12
+ idleCpuPercent?: number;
13
+ minAgeDays?: number;
14
+ };
15
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,3 @@
1
+ export declare function formatCurrency(amount: number): string;
2
+ export declare function formatDate(date: Date): string;
3
+ export declare function daysSince(date: Date): number;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCurrency = formatCurrency;
4
+ exports.formatDate = formatDate;
5
+ exports.daysSince = daysSince;
6
+ function formatCurrency(amount) {
7
+ return `$${amount.toFixed(2)}`;
8
+ }
9
+ function formatDate(date) {
10
+ return date.toISOString().split('T')[0];
11
+ }
12
+ function daysSince(date) {
13
+ const now = new Date();
14
+ const diff = now.getTime() - date.getTime();
15
+ return Math.floor(diff / (1000 * 60 * 60 * 24));
16
+ }
@@ -0,0 +1,2 @@
1
+ export * from './logger';
2
+ export * from './formatter';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./logger"), exports);
18
+ __exportStar(require("./formatter"), exports);
@@ -0,0 +1,6 @@
1
+ export declare function log(message: string): void;
2
+ export declare function success(message: string): void;
3
+ export declare function error(message: string): void;
4
+ export declare function warn(message: string): void;
5
+ export declare function info(message: string): void;
6
+ export declare function progress(message: string): void;
@@ -0,0 +1,30 @@
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.log = log;
7
+ exports.success = success;
8
+ exports.error = error;
9
+ exports.warn = warn;
10
+ exports.info = info;
11
+ exports.progress = progress;
12
+ const chalk_1 = __importDefault(require("chalk"));
13
+ function log(message) {
14
+ console.log(message);
15
+ }
16
+ function success(message) {
17
+ console.log(chalk_1.default.green('✓'), message);
18
+ }
19
+ function error(message) {
20
+ console.error(chalk_1.default.red('✗'), message);
21
+ }
22
+ function warn(message) {
23
+ console.warn(chalk_1.default.yellow('⚠'), message);
24
+ }
25
+ function info(message) {
26
+ console.log(chalk_1.default.blue('ℹ'), message);
27
+ }
28
+ function progress(message) {
29
+ process.stdout.write(chalk_1.default.gray(message));
30
+ }