infra-cost
Version:
Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud
1,340 lines (1,329 loc) • 657 kB
JavaScript
#!/usr/bin/env node
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// package.json
var require_package = __commonJS({
"package.json"(exports, module2) {
module2.exports = {
name: "infra-cost",
version: "1.11.0",
description: "Multi-cloud FinOps CLI tool for comprehensive cost analysis and infrastructure optimization across AWS, GCP, Azure, Alibaba Cloud, and Oracle Cloud",
keywords: [
"aws",
"gcp",
"azure",
"cloud-cost",
"finops",
"cost-optimization",
"multi-cloud",
"cost-analysis",
"infrastructure",
"cloud-billing",
"cost-management",
"devops",
"cli-tool",
"cost-monitoring",
"budget-tracking"
],
author: {
name: "Code Collab",
email: "codecollab.co@gmail.com",
url: "https://github.com/codecollab-co/infra-cost"
},
files: [
"!tests/**/*",
"dist/**/*",
"!dist/**/*.js.map",
"bin/**/*"
],
bin: {
"infra-cost": "./bin/index.js",
"aws-cost": "./bin/index.js"
},
main: "./dist/index.js",
scripts: {
prebuild: "run-s clean",
build: "tsup",
clean: "rm -rf dist",
typecheck: "tsc --noEmit",
lint: "eslint src --ext .ts",
"lint:fix": "eslint src --ext .ts --fix",
test: "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
dev: "tsup --watch",
"version:check": `echo "Current version: $(npm pkg get version | tr -d '"')"`,
"version:next": "npm version patch --no-git-tag-version",
"version:bump:patch": "npm version patch",
"version:bump:minor": "npm version minor",
"version:bump:major": "npm version major",
"publish:dry": "npm publish --dry-run",
"publish:latest": "npm publish",
"publish:beta": "npm publish --tag beta",
"prepare-release": "npm run build && npm run test && npm run version:bump:patch",
postpublish: 'echo "\u{1F389} Published $(npm pkg get name)@$(npm pkg get version) to npm!"',
prepublishOnly: "npm run build"
},
repository: {
type: "git",
url: "https://github.com/codecollab-co/infra-cost.git"
},
bugs: {
url: "https://github.com/codecollab-co/infra-cost/issues"
},
homepage: "https://github.com/codecollab-co/infra-cost#readme",
license: "MIT",
engines: {
node: ">=20.0.0",
npm: ">=10.0.0"
},
dependencies: {
"@alicloud/bssopenapi20171214": "^2.0.1",
"@alicloud/cs20151215": "^4.0.1",
"@alicloud/ecs20140526": "^4.0.3",
"@alicloud/oss20190517": "^1.0.6",
"@alicloud/rds20140815": "^3.0.2",
"@alicloud/tea-util": "^1.4.7",
"@aws-sdk/client-budgets": "^3.975.0",
"@aws-sdk/client-cost-explorer": "^3.975.0",
"@aws-sdk/client-ec2": "^3.975.0",
"@aws-sdk/client-elastic-load-balancing-v2": "^3.975.0",
"@aws-sdk/client-iam": "^3.975.0",
"@aws-sdk/client-lambda": "^3.975.0",
"@aws-sdk/client-rds": "^3.975.0",
"@aws-sdk/client-s3": "^3.975.0",
"@aws-sdk/client-sts": "^3.975.0",
"@aws-sdk/credential-providers": "^3.975.0",
"@azure/arm-compute": "^23.3.0",
"@azure/arm-consumption": "^9.2.1",
"@azure/arm-containerservice": "^24.1.0",
"@azure/arm-costmanagement": "^1.0.0-beta.2",
"@azure/arm-network": "^35.0.0",
"@azure/arm-sql": "^10.0.0",
"@azure/arm-storage": "^19.1.0",
"@azure/arm-subscriptions": "^6.0.0",
"@azure/identity": "^4.13.0",
"@google-cloud/bigquery": "^8.1.1",
"@google-cloud/billing": "^5.1.1",
"@google-cloud/compute": "^6.7.0",
"@google-cloud/container": "^6.6.0",
"@google-cloud/monitoring": "^5.3.1",
"@google-cloud/resource-manager": "^6.2.1",
"@google-cloud/sql": "^0.24.0",
"@google-cloud/storage": "^7.18.0",
"@slack/web-api": "^7.5.0",
callsites: "^3.1.0",
chalk: "^4.1.2",
"cli-progress": "^3.12.0",
"cli-table3": "^0.6.5",
commander: "^12.1.0",
cors: "^2.8.6",
dayjs: "^1.11.19",
exceljs: "^4.4.0",
express: "^5.2.1",
"express-rate-limit": "^8.2.1",
"fd-slicer": "^1.1.0",
"google-auth-library": "^10.5.0",
googleapis: "^171.0.0",
helmet: "^8.1.0",
ini: "^6.0.0",
ink: "^6.6.0",
moment: "^2.30.1",
"node-fetch": "^2.7.0",
"oci-budget": "^2.88.0",
"oci-common": "^2.88.0",
"oci-containerengine": "^2.88.0",
"oci-core": "^2.88.0",
"oci-database": "^2.88.0",
"oci-identity": "^2.88.0",
"oci-objectstorage": "^2.88.0",
"oci-usageapi": "^2.88.0",
ora: "^9.1.0",
pako: "^2.1.0",
pend: "^1.2.0",
react: "^19.2.4",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1",
yauzl: "^3.0.0",
zod: "^3.23.8"
},
devDependencies: {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.6",
"@types/jest": "^29.5.12",
"@types/node": "^22.5.4",
"@types/yauzl": "^2.10.3",
"@typescript-eslint/eslint-plugin": "^8.5.0",
"@typescript-eslint/parser": "^8.5.0",
eslint: "^8.57.0",
jest: "^29.7.0",
"npm-run-all": "^4.1.5",
"ts-jest": "^29.2.5",
tsup: "^6.7.0",
typescript: "^5.6.2"
}
};
}
});
// src/core/logging/structured-logger.ts
function initializeLogger(config) {
globalLogger = new StructuredLogger(config);
return globalLogger;
}
var fs, path, import_events, DEFAULT_CONFIG, globalLogger, StructuredLogger;
var init_structured_logger = __esm({
"src/core/logging/structured-logger.ts"() {
fs = __toESM(require("fs"));
path = __toESM(require("path"));
import_events = require("events");
DEFAULT_CONFIG = {
level: 2 /* INFO */,
format: "pretty",
outputs: [{ type: "console" }],
enableAudit: false,
enablePerformance: false,
silent: false
};
globalLogger = null;
StructuredLogger = class extends import_events.EventEmitter {
constructor(config = {}) {
super();
this.performanceCounters = /* @__PURE__ */ new Map();
this.fileStreams = /* @__PURE__ */ new Map();
this.config = { ...DEFAULT_CONFIG, ...config };
this.correlationId = config.correlationId || this.generateId("req");
this.component = config.component || "infra-cost";
this.sessionId = this.generateId("session");
}
/**
* Generate unique ID
*/
generateId(prefix) {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substring(2, 8);
return `${prefix}_${timestamp}${random}`;
}
/**
* Get current timestamp in ISO format
*/
getTimestamp() {
return (/* @__PURE__ */ new Date()).toISOString();
}
/**
* Check if should log at given level
*/
shouldLog(level) {
return !this.config.silent && level <= this.config.level;
}
/**
* Get level name from number
*/
getLevelName(level) {
const names = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
return names[level] || "UNKNOWN";
}
/**
* Format message for console output (pretty format)
*/
formatPretty(entry) {
const levelColors = {
ERROR: "\x1B[31m",
// Red
WARN: "\x1B[33m",
// Yellow
INFO: "\x1B[36m",
// Cyan
DEBUG: "\x1B[90m",
// Gray
TRACE: "\x1B[90m"
// Gray
};
const reset = "\x1B[0m";
const color = levelColors[entry.level] || "";
const levelIcon = {
ERROR: "\u274C",
WARN: "\u26A0\uFE0F ",
INFO: "\u2139\uFE0F ",
DEBUG: "\u{1F50D}",
TRACE: "\u{1F52C}"
};
const time = entry.timestamp.split("T")[1].split(".")[0];
const icon = levelIcon[entry.level] || "";
const component = entry.component ? `[${entry.component}]` : "";
const operation = entry.operation ? `(${entry.operation})` : "";
const duration = entry.duration ? ` [${entry.duration}ms]` : "";
let output = `${color}${icon} ${time} ${entry.level.padEnd(5)} ${component}${operation}${duration}${reset} ${entry.message}`;
if (entry.metadata && Object.keys(entry.metadata).length > 0) {
const metaStr = JSON.stringify(entry.metadata, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
output += `
${color}metadata:${reset} ${metaStr}`;
}
return output;
}
/**
* Format message for compact output
*/
formatCompact(entry) {
const time = entry.timestamp.split("T")[1].split(".")[0];
const component = entry.component ? `[${entry.component}]` : "";
return `${time} ${entry.level} ${component} ${entry.message}`;
}
/**
* Format message for JSON output
*/
formatJson(entry) {
return JSON.stringify(entry);
}
/**
* Format log entry based on configuration
*/
formatEntry(entry) {
switch (this.config.format) {
case "json":
return this.formatJson(entry);
case "compact":
return this.formatCompact(entry);
case "pretty":
default:
return this.formatPretty(entry);
}
}
/**
* Write to file output
*/
writeToFile(filepath, content) {
try {
const dir = path.dirname(filepath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
let stream = this.fileStreams.get(filepath);
if (!stream) {
stream = fs.createWriteStream(filepath, { flags: "a" });
stream.on("error", (err) => {
console.error(`Log stream error for ${filepath}:`, err);
this.fileStreams.delete(filepath);
});
this.fileStreams.set(filepath, stream);
}
stream.write(content + "\n");
} catch (error) {
console.error(`Failed to write to log file ${filepath}:`, error);
}
}
/**
* Write to outputs
*/
writeToOutputs(entry) {
const formatted = this.formatEntry(entry);
for (const output of this.config.outputs) {
const outputLevel = output.level ?? this.config.level;
if (entry.levelNum > outputLevel) {
continue;
}
switch (output.type) {
case "console":
if (entry.levelNum === 0 /* ERROR */) {
console.error(formatted);
} else {
console.log(formatted);
}
break;
case "file":
if (output.destination) {
this.writeToFile(output.destination, this.formatJson(entry));
}
break;
case "http":
this.emit("httpLog", entry, output.destination);
break;
case "syslog":
this.emit("syslog", entry, output.destination);
break;
}
}
this.emit("log", entry);
}
/**
* Create log entry
*/
createEntry(level, message, metadata) {
const entry = {
timestamp: this.getTimestamp(),
level: this.getLevelName(level),
levelNum: level,
correlationId: metadata?.correlationId || this.correlationId,
component: metadata?.component || this.component,
operation: metadata?.operation,
message,
duration: metadata?.duration,
metadata: this.sanitizeMetadata(metadata)
};
if (this.config.enablePerformance) {
entry.performance = {
apiCalls: this.performanceCounters.get("apiCalls") || 0,
cacheHits: this.performanceCounters.get("cacheHits") || 0,
cacheMisses: this.performanceCounters.get("cacheMisses") || 0
};
}
return entry;
}
/**
* Sanitize metadata (remove internal fields)
*/
sanitizeMetadata(metadata) {
if (!metadata)
return void 0;
const { component, operation, duration, correlationId, ...rest } = metadata;
if (rest.error instanceof Error) {
rest.error = {
name: rest.error.name,
message: rest.error.message,
stack: rest.error.stack
};
}
return Object.keys(rest).length > 0 ? rest : void 0;
}
// Public logging methods
error(message, metadata) {
if (this.shouldLog(0 /* ERROR */)) {
this.writeToOutputs(this.createEntry(0 /* ERROR */, message, metadata));
}
}
warn(message, metadata) {
if (this.shouldLog(1 /* WARN */)) {
this.writeToOutputs(this.createEntry(1 /* WARN */, message, metadata));
}
}
info(message, metadata) {
if (this.shouldLog(2 /* INFO */)) {
this.writeToOutputs(this.createEntry(2 /* INFO */, message, metadata));
}
}
debug(message, metadata) {
if (this.shouldLog(3 /* DEBUG */)) {
this.writeToOutputs(this.createEntry(3 /* DEBUG */, message, metadata));
}
}
trace(message, metadata) {
if (this.shouldLog(4 /* TRACE */)) {
this.writeToOutputs(this.createEntry(4 /* TRACE */, message, metadata));
}
}
/**
* Audit logging
*/
audit(eventType, details) {
if (!this.config.enableAudit)
return;
const entry = {
auditId: this.generateId("audit"),
timestamp: this.getTimestamp(),
eventType,
correlationId: this.correlationId,
sessionId: this.sessionId,
...details
};
if (this.config.auditOutput) {
this.writeToFile(this.config.auditOutput, JSON.stringify(entry));
}
this.emit("audit", entry);
if (eventType === "authentication_failure" /* AUTHENTICATION_FAILURE */ || eventType === "permission_denied" /* PERMISSION_DENIED */ || eventType === "error_occurred" /* ERROR_OCCURRED */) {
this.warn(`Audit: ${eventType}`, { action: details.action, result: details.result });
}
}
// Performance profiling methods
/**
* Start a timer for performance profiling
*/
startTimer(operation) {
return {
operation,
startTime: Date.now(),
startMemory: process.memoryUsage?.()?.heapUsed
};
}
/**
* End a timer and log the result
*/
endTimer(timer, metadata) {
const duration = Date.now() - timer.startTime;
this.debug(`${timer.operation} completed`, {
...metadata,
operation: timer.operation,
duration
});
return duration;
}
/**
* Profile an operation
*/
async profile(operation, fn, metadata) {
const timer = this.startTimer(operation);
try {
const result = await fn();
this.endTimer(timer, { ...metadata, result: "success" });
return result;
} catch (error) {
this.endTimer(timer, { ...metadata, result: "failure", error });
throw error;
}
}
/**
* Increment performance counter
*/
incrementCounter(name, amount = 1) {
const current = this.performanceCounters.get(name) || 0;
this.performanceCounters.set(name, current + amount);
}
/**
* Get performance stats
*/
getPerformanceStats() {
return Object.fromEntries(this.performanceCounters);
}
/**
* Reset performance counters
*/
resetPerformanceCounters() {
this.performanceCounters.clear();
}
// Configuration methods
/**
* Get current correlation ID
*/
getCorrelationId() {
return this.correlationId;
}
/**
* Set correlation ID
*/
setCorrelationId(id) {
this.correlationId = id;
}
/**
* Create a child logger with a specific component
*/
child(component) {
const childLogger = new StructuredLogger({
...this.config,
component,
correlationId: this.correlationId
});
return childLogger;
}
/**
* Update configuration
*/
configure(config) {
this.config = { ...this.config, ...config };
}
/**
* Close file streams
*/
close() {
for (const stream of this.fileStreams.values()) {
stream.end();
}
this.fileStreams.clear();
}
};
__name(StructuredLogger, "StructuredLogger");
__name(initializeLogger, "initializeLogger");
}
});
// src/core/logging/audit.ts
var init_audit = __esm({
"src/core/logging/audit.ts"() {
}
});
// src/core/logging/formatters.ts
var import_chalk;
var init_formatters = __esm({
"src/core/logging/formatters.ts"() {
import_chalk = __toESM(require("chalk"));
}
});
// src/core/logging/index.ts
var init_logging = __esm({
"src/core/logging/index.ts"() {
init_structured_logger();
init_audit();
init_formatters();
init_formatters();
}
});
// src/core/config/schema.ts
function validateConfig(config) {
return ConfigSchema.parse(config);
}
var import_zod, ProviderConfigSchema, OutputConfigSchema, CacheConfigSchema, SlackConfigSchema, LogOutputSchema, LoggingConfigSchema, ChargebackConfigSchema, AlertThresholdSchema, NotificationChannelSchema, MonitoringConfigSchema, OrganizationsConfigSchema, ProfileConfigSchema, ConfigSchema;
var init_schema = __esm({
"src/core/config/schema.ts"() {
import_zod = require("zod");
ProviderConfigSchema = import_zod.z.object({
provider: import_zod.z.enum(["aws", "gcp", "azure", "alibaba", "oracle"]).default("aws"),
profile: import_zod.z.string().default("default"),
region: import_zod.z.string().default("us-east-1"),
accessKey: import_zod.z.string().optional(),
secretKey: import_zod.z.string().optional(),
sessionToken: import_zod.z.string().optional(),
// GCP specific
projectId: import_zod.z.string().optional(),
keyFile: import_zod.z.string().optional(),
// Azure specific
subscriptionId: import_zod.z.string().optional(),
tenantId: import_zod.z.string().optional(),
clientId: import_zod.z.string().optional(),
clientSecret: import_zod.z.string().optional(),
// Oracle specific
userId: import_zod.z.string().optional(),
tenancyId: import_zod.z.string().optional(),
fingerprint: import_zod.z.string().optional()
});
OutputConfigSchema = import_zod.z.object({
format: import_zod.z.enum(["json", "text", "fancy", "table"]).default("fancy"),
summary: import_zod.z.boolean().default(false),
showDelta: import_zod.z.boolean().default(true),
showQuickWins: import_zod.z.boolean().default(true),
deltaThreshold: import_zod.z.number().min(0).max(100).default(10),
quickWinsCount: import_zod.z.number().int().min(1).max(10).default(3),
colorOutput: import_zod.z.boolean().default(true)
});
CacheConfigSchema = import_zod.z.object({
enabled: import_zod.z.boolean().default(true),
ttl: import_zod.z.string().default("4h"),
type: import_zod.z.enum(["file", "memory"]).default("file"),
directory: import_zod.z.string().optional()
});
SlackConfigSchema = import_zod.z.object({
enabled: import_zod.z.boolean().default(false),
token: import_zod.z.string().optional(),
channel: import_zod.z.string().optional()
});
LogOutputSchema = import_zod.z.object({
type: import_zod.z.enum(["console", "file", "syslog", "http"]),
destination: import_zod.z.string().optional(),
level: import_zod.z.enum(["error", "warn", "info", "debug", "trace"]).optional()
});
LoggingConfigSchema = import_zod.z.object({
level: import_zod.z.enum(["error", "warn", "info", "debug", "trace"]).default("info"),
format: import_zod.z.enum(["json", "pretty", "compact"]).default("pretty"),
outputs: import_zod.z.array(LogOutputSchema).default([{ type: "console" }]),
enableAudit: import_zod.z.boolean().default(false),
auditOutput: import_zod.z.string().optional(),
correlationId: import_zod.z.string().optional(),
component: import_zod.z.string().optional(),
enablePerformance: import_zod.z.boolean().optional(),
silent: import_zod.z.boolean().optional()
});
ChargebackConfigSchema = import_zod.z.object({
dimensions: import_zod.z.array(import_zod.z.string()).default(["team", "project", "environment"]),
handleUntagged: import_zod.z.enum(["ignore", "shared", "proportional"]).default("shared"),
requiredTags: import_zod.z.array(import_zod.z.string()).default([])
});
AlertThresholdSchema = import_zod.z.object({
id: import_zod.z.string(),
name: import_zod.z.string(),
type: import_zod.z.enum(["ABSOLUTE", "PERCENTAGE", "ANOMALY", "TREND", "BUDGET_FORECAST"]),
condition: import_zod.z.enum(["GREATER_THAN", "LESS_THAN", "EQUALS", "DEVIATION"]),
value: import_zod.z.number(),
timeWindow: import_zod.z.number(),
provider: import_zod.z.string().optional(),
service: import_zod.z.string().optional(),
severity: import_zod.z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
enabled: import_zod.z.boolean(),
cooldownPeriod: import_zod.z.number(),
description: import_zod.z.string()
});
NotificationChannelSchema = import_zod.z.object({
id: import_zod.z.string(),
type: import_zod.z.enum(["EMAIL", "SLACK", "WEBHOOK", "SMS", "TEAMS", "DISCORD"]),
config: import_zod.z.record(import_zod.z.any()),
enabled: import_zod.z.boolean(),
filters: import_zod.z.object({
minSeverity: import_zod.z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]),
providers: import_zod.z.array(import_zod.z.string()).optional(),
services: import_zod.z.array(import_zod.z.string()).optional()
})
});
MonitoringConfigSchema = import_zod.z.object({
enabled: import_zod.z.boolean().default(false),
interval: import_zod.z.number().default(3e5),
// 5 minutes in milliseconds
providers: import_zod.z.array(import_zod.z.string()).default([]),
alertThresholds: import_zod.z.array(AlertThresholdSchema).default([]),
notificationChannels: import_zod.z.array(NotificationChannelSchema).default([]),
enablePredictiveAlerts: import_zod.z.boolean().default(false),
dataRetentionDays: import_zod.z.number().default(90),
// Legacy fields for backwards compatibility
anomalyDetection: import_zod.z.boolean().optional(),
webhookUrl: import_zod.z.string().url().optional()
});
OrganizationsConfigSchema = import_zod.z.object({
enabled: import_zod.z.boolean().default(false),
managementAccountId: import_zod.z.string().optional(),
accountFilters: import_zod.z.array(import_zod.z.string()).optional()
});
ProfileConfigSchema = import_zod.z.object({
name: import_zod.z.string(),
provider: ProviderConfigSchema.optional(),
output: OutputConfigSchema.optional(),
cache: CacheConfigSchema.optional(),
slack: SlackConfigSchema.optional(),
logging: LoggingConfigSchema.optional(),
chargeback: ChargebackConfigSchema.optional(),
monitoring: MonitoringConfigSchema.optional(),
organizations: OrganizationsConfigSchema.optional()
});
ConfigSchema = import_zod.z.object({
version: import_zod.z.string().default("1.0"),
// Default settings
provider: ProviderConfigSchema.optional(),
output: OutputConfigSchema.optional(),
cache: CacheConfigSchema.optional(),
slack: SlackConfigSchema.optional(),
logging: LoggingConfigSchema.optional(),
chargeback: ChargebackConfigSchema.optional(),
monitoring: MonitoringConfigSchema.optional(),
organizations: OrganizationsConfigSchema.optional(),
// Named profiles
profiles: import_zod.z.record(import_zod.z.string(), ProfileConfigSchema).optional(),
// Active profile
activeProfile: import_zod.z.string().optional()
});
__name(validateConfig, "validateConfig");
}
});
// src/core/config/loader.ts
function discoverConfigFile() {
for (const configPath of CONFIG_PATHS) {
if ((0, import_fs.existsSync)(configPath)) {
return configPath;
}
}
return null;
}
function loadConfigFile(configPath) {
try {
const content = (0, import_fs.readFileSync)(configPath, "utf8");
const config = JSON.parse(content);
if (config.profiles && config.defaults?.profile) {
const activeProfile = config.profiles[config.defaults.profile];
if (activeProfile) {
return mergeConfigs(config.defaults, activeProfile);
}
}
return config.defaults || config;
} catch (error) {
console.warn(`Warning: Could not load config from ${configPath}: ${error.message}`);
return {};
}
}
function resolveEnvVars(config) {
const resolved = JSON.parse(JSON.stringify(config));
if (process.env.AWS_ACCESS_KEY_ID) {
resolved.accessKey = process.env.AWS_ACCESS_KEY_ID;
}
if (process.env.AWS_SECRET_ACCESS_KEY) {
resolved.secretKey = process.env.AWS_SECRET_ACCESS_KEY;
}
if (process.env.AWS_SESSION_TOKEN) {
resolved.sessionToken = process.env.AWS_SESSION_TOKEN;
}
if (process.env.AWS_REGION) {
resolved.region = process.env.AWS_REGION;
}
if (process.env.AWS_PROFILE) {
resolved.profile = process.env.AWS_PROFILE;
}
if (process.env.SLACK_TOKEN || process.env.SLACK_CHANNEL) {
resolved.slack = {
...resolved.slack,
token: process.env.SLACK_TOKEN ?? resolved.slack?.token,
channel: process.env.SLACK_CHANNEL ?? resolved.slack?.channel,
enabled: true
};
}
return resolved;
}
function mergeConfigs(...configs) {
const result = {};
for (const config of configs) {
for (const [key, value] of Object.entries(config)) {
if (value === void 0 || value === null) {
continue;
}
if (typeof value === "object" && !Array.isArray(value)) {
result[key] = { ...result[key], ...value };
} else {
result[key] = value;
}
}
}
return result;
}
function autoLoadConfig(cliOptions = {}) {
let config = { ...DEFAULT_CONFIG2 };
const configPath = cliOptions.configFile || discoverConfigFile();
if (configPath) {
const fileConfig = loadConfigFile(configPath);
config = mergeConfigs(config, fileConfig);
}
config = mergeConfigs(config, resolveEnvVars(config));
const cliConfig = mapCliOptionsToConfig(cliOptions);
config = mergeConfigs(config, cliConfig);
return config;
}
function mapCliOptionsToConfig(options) {
const config = {};
if (options.provider)
config.provider = options.provider;
if (options.profile)
config.profile = options.profile;
if (options.region)
config.region = options.region;
if (options.accessKey)
config.accessKey = options.accessKey;
if (options.secretKey)
config.secretKey = options.secretKey;
if (options.sessionToken)
config.sessionToken = options.sessionToken;
if (options.json || options.text || options.summary) {
config.output = config.output || {};
if (options.json)
config.output.format = "json";
if (options.text)
config.output.format = "text";
if (options.summary)
config.output.summary = true;
}
if (options.delta !== void 0) {
config.output = config.output || {};
config.output.showDelta = options.delta;
}
if (options.deltaThreshold) {
config.output = config.output || {};
config.output.deltaThreshold = parseFloat(options.deltaThreshold);
}
if (options.quickWins !== void 0) {
config.output = config.output || {};
config.output.showQuickWins = options.quickWins;
}
if (options.quickWinsCount) {
config.output = config.output || {};
config.output.quickWinsCount = parseInt(options.quickWinsCount, 10);
}
if (options.cache !== void 0 || options.noCache !== void 0) {
config.cache = config.cache || {};
config.cache.enabled = options.cache === true || options.noCache !== true;
}
if (options.cacheTtl) {
config.cache = config.cache || {};
config.cache.ttl = options.cacheTtl;
}
if (options.cacheType) {
config.cache = config.cache || {};
config.cache.type = options.cacheType;
}
if (options.slackToken || options.slackChannel) {
config.slack = config.slack || {};
if (options.slackToken)
config.slack.token = options.slackToken;
if (options.slackChannel)
config.slack.channel = options.slackChannel;
config.slack.enabled = true;
}
if (options.logLevel || options.verbose || options.quiet) {
config.logging = config.logging || {};
if (options.logLevel)
config.logging.level = options.logLevel;
if (options.verbose)
config.logging.level = "debug";
if (options.quiet)
config.logging.level = "error";
}
return config;
}
var import_fs, import_path, import_os, CONFIG_PATHS, DEFAULT_CONFIG2;
var init_loader = __esm({
"src/core/config/loader.ts"() {
import_fs = require("fs");
import_path = require("path");
import_os = require("os");
CONFIG_PATHS = [
(0, import_path.join)(process.cwd(), "infra-cost.config.json"),
// Project-specific
(0, import_path.join)(process.cwd(), ".infra-cost.config.json"),
// Project-specific (hidden)
(0, import_path.join)(process.cwd(), ".infra-cost", "config.json"),
// Project directory
(0, import_path.join)((0, import_os.homedir)(), ".infra-cost", "config.json"),
// User global
(0, import_path.join)((0, import_os.homedir)(), ".config", "infra-cost", "config.json")
// XDG standard
];
DEFAULT_CONFIG2 = {
provider: "aws",
profile: "default",
region: "us-east-1",
output: {
format: "fancy",
summary: false,
showDelta: true,
// NEW: Show delta by default
showQuickWins: true,
// NEW: Show quick wins by default
deltaThreshold: 10,
quickWinsCount: 3
},
cache: {
enabled: true,
// NEW: Cache enabled by default
ttl: "4h",
type: "file"
},
logging: {
level: "info",
format: "pretty",
auditEnabled: false
}
};
__name(discoverConfigFile, "discoverConfigFile");
__name(loadConfigFile, "loadConfigFile");
__name(resolveEnvVars, "resolveEnvVars");
__name(mergeConfigs, "mergeConfigs");
__name(autoLoadConfig, "autoLoadConfig");
__name(mapCliOptionsToConfig, "mapCliOptionsToConfig");
}
});
// src/types/providers.ts
var CloudProviderAdapter, ResourceType;
var init_providers = __esm({
"src/types/providers.ts"() {
CloudProviderAdapter = class {
constructor(config) {
this.config = config;
}
calculateServiceTotals(rawCostData) {
return this.processRawCostData(rawCostData);
}
processRawCostData(rawCostData) {
const totals = {
lastMonth: 0,
thisMonth: 0,
last7Days: 0,
yesterday: 0
};
const totalsByService = {
lastMonth: {},
thisMonth: {},
last7Days: {},
yesterday: {}
};
const now = /* @__PURE__ */ new Date();
const startOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0);
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
const last7DaysStart = new Date(now);
last7DaysStart.setDate(last7DaysStart.getDate() - 7);
for (const [serviceName, serviceCosts] of Object.entries(rawCostData)) {
let lastMonthServiceTotal = 0;
let thisMonthServiceTotal = 0;
let last7DaysServiceTotal = 0;
let yesterdayServiceTotal = 0;
for (const [dateStr, cost] of Object.entries(serviceCosts)) {
const date = new Date(dateStr);
if (date >= startOfLastMonth && date <= endOfLastMonth) {
lastMonthServiceTotal += cost;
}
if (date >= startOfThisMonth) {
thisMonthServiceTotal += cost;
}
if (date >= last7DaysStart && date < yesterday) {
last7DaysServiceTotal += cost;
}
if (date.toDateString() === yesterday.toDateString()) {
yesterdayServiceTotal += cost;
}
}
totalsByService.lastMonth[serviceName] = lastMonthServiceTotal;
totalsByService.thisMonth[serviceName] = thisMonthServiceTotal;
totalsByService.last7Days[serviceName] = last7DaysServiceTotal;
totalsByService.yesterday[serviceName] = yesterdayServiceTotal;
totals.lastMonth += lastMonthServiceTotal;
totals.thisMonth += thisMonthServiceTotal;
totals.last7Days += last7DaysServiceTotal;
totals.yesterday += yesterdayServiceTotal;
}
return {
totals,
totalsByService
};
}
};
__name(CloudProviderAdapter, "CloudProviderAdapter");
ResourceType = /* @__PURE__ */ ((ResourceType2) => {
ResourceType2["COMPUTE"] = "compute";
ResourceType2["STORAGE"] = "storage";
ResourceType2["DATABASE"] = "database";
ResourceType2["NETWORK"] = "network";
ResourceType2["SECURITY"] = "security";
ResourceType2["SERVERLESS"] = "serverless";
ResourceType2["CONTAINER"] = "container";
ResourceType2["ANALYTICS"] = "analytics";
return ResourceType2;
})(ResourceType || {});
}
});
// src/utils/error-handling.ts
function getErrorMessage(error, fallback = "Unknown error occurred") {
if (error instanceof Error) {
return error.message;
}
if (typeof error === "string") {
return error;
}
if (error && typeof error === "object" && "message" in error) {
return String(error.message);
}
return fallback;
}
var init_error_handling = __esm({
"src/utils/error-handling.ts"() {
__name(getErrorMessage, "getErrorMessage");
}
});
// src/logger.ts
function printFatalError(error) {
console.error(`
${import_chalk3.default.bold.redBright.underline(`Error:`)}
${import_chalk3.default.redBright(`${error}`)}
`);
process.exit(1);
}
function showSpinner(text) {
if (!spinner) {
spinner = (0, import_ora.default)({ text: "" }).start();
}
spinner.text = text;
}
function hideSpinner() {
if (!spinner) {
return;
}
spinner.stop();
}
var import_chalk3, import_ora, spinner;
var init_logger = __esm({
"src/logger.ts"() {
import_chalk3 = __toESM(require("chalk"));
import_ora = __toESM(require("ora"));
__name(printFatalError, "printFatalError");
__name(showSpinner, "showSpinner");
__name(hideSpinner, "hideSpinner");
}
});
// src/core/analytics/anomaly/detector.ts
var AnomalyDetector, CostAnalyticsEngine;
var init_detector = __esm({
"src/core/analytics/anomaly/detector.ts"() {
AnomalyDetector = class {
constructor(config = { sensitivity: "MEDIUM", lookbackPeriods: 14 }) {
this.config = config;
}
/**
* Detects anomalies using multiple statistical methods
*/
detectAnomalies(dataPoints) {
if (dataPoints.length < this.config.lookbackPeriods) {
return [];
}
const anomalies = [];
anomalies.push(...this.detectStatisticalAnomalies(dataPoints));
anomalies.push(...this.detectTrendAnomalies(dataPoints));
anomalies.push(...this.detectSeasonalAnomalies(dataPoints));
return this.consolidateAnomalies(anomalies);
}
/**
* Statistical anomaly detection using modified Z-score
*/
detectStatisticalAnomalies(dataPoints) {
const anomalies = [];
const values = dataPoints.map((dp) => dp.value);
for (let i = this.config.lookbackPeriods; i < dataPoints.length; i++) {
const currentValue = values[i];
const historicalValues = values.slice(i - this.config.lookbackPeriods, i);
const median = this.calculateMedian(historicalValues);
const mad = this.calculateMAD(historicalValues, median);
const modifiedZScore = mad === 0 ? 0 : 0.6745 * (currentValue - median) / mad;
const threshold = this.getSensitivityThreshold();
if (Math.abs(modifiedZScore) > threshold) {
const deviation = currentValue - median;
const deviationPercentage = median === 0 ? 0 : Math.abs(deviation / median) * 100;
anomalies.push({
timestamp: dataPoints[i].timestamp,
actualValue: currentValue,
expectedValue: median,
deviation: Math.abs(deviation),
deviationPercentage,
severity: this.calculateSeverity(Math.abs(modifiedZScore), threshold),
confidence: Math.min(95, Math.abs(modifiedZScore) / threshold * 100),
type: deviation > 0 ? "SPIKE" : "DROP",
description: this.generateAnomalyDescription("STATISTICAL", deviation, deviationPercentage),
potentialCauses: this.generatePotentialCauses(deviation > 0 ? "SPIKE" : "DROP", deviationPercentage)
});
}
}
return anomalies;
}
/**
* Trend-based anomaly detection
*/
detectTrendAnomalies(dataPoints) {
const anomalies = [];
const values = dataPoints.map((dp) => dp.value);
const trendWindow = Math.min(7, Math.floor(this.config.lookbackPeriods / 2));
for (let i = trendWindow * 2; i < dataPoints.length; i++) {
const recentTrend = this.calculateLinearTrend(values.slice(i - trendWindow, i));
const historicalTrend = this.calculateLinearTrend(values.slice(i - trendWindow * 2, i - trendWindow));
const trendChange = Math.abs(recentTrend - historicalTrend);
const trendChangeThreshold = this.config.sensitivity === "HIGH" ? 0.1 : this.config.sensitivity === "MEDIUM" ? 0.2 : 0.3;
if (trendChange > trendChangeThreshold) {
const currentValue = values[i];
const expectedValue = values[i - 1] + historicalTrend;
const deviation = Math.abs(currentValue - expectedValue);
const deviationPercentage = expectedValue === 0 ? 0 : deviation / expectedValue * 100;
anomalies.push({
timestamp: dataPoints[i].timestamp,
actualValue: currentValue,
expectedValue,
deviation,
deviationPercentage,
severity: this.calculateSeverity(trendChange, trendChangeThreshold),
confidence: Math.min(90, trendChange / trendChangeThreshold * 100),
type: "TREND_CHANGE",
description: `Significant trend change detected: ${recentTrend > historicalTrend ? "acceleration" : "deceleration"} in cost growth`,
potentialCauses: this.generatePotentialCauses("TREND_CHANGE", deviationPercentage)
});
}
}
return anomalies;
}
/**
* Seasonal anomaly detection
*/
detectSeasonalAnomalies(dataPoints) {
if (!this.config.seasonalityPeriods || dataPoints.length < this.config.seasonalityPeriods * 2) {
return [];
}
const anomalies = [];
const values = dataPoints.map((dp) => dp.value);
const seasonalPeriod = this.config.seasonalityPeriods;
for (let i = seasonalPeriod; i < dataPoints.length; i++) {
const currentValue = values[i];
const seasonalBaseline = values[i - seasonalPeriod];
const seasonalDeviation = Math.abs(currentValue - seasonalBaseline);
const seasonalDeviationPercentage = seasonalBaseline === 0 ? 0 : seasonalDeviation / seasonalBaseline * 100;
const threshold = seasonalBaseline * 0.3;
if (seasonalDeviation > threshold && seasonalDeviationPercentage > 25) {
anomalies.push({
timestamp: dataPoints[i].timestamp,
actualValue: currentValue,
expectedValue: seasonalBaseline,
deviation: seasonalDeviation,
deviationPercentage: seasonalDeviationPercentage,
severity: this.calculateSeverity(seasonalDeviationPercentage, 25),
confidence: 80,
type: "SEASONAL_ANOMALY",
description: `Unusual seasonal pattern: ${seasonalDeviationPercentage.toFixed(1)}% deviation from same period last cycle`,
potentialCauses: this.generatePotentialCauses("SEASONAL_ANOMALY", seasonalDeviationPercentage)
});
}
}
return anomalies;
}
calculateMedian(values) {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}
calculateMAD(values, median) {
const deviations = values.map((v) => Math.abs(v - median));
return this.calculateMedian(deviations);
}
calculateLinearTrend(values) {
const n = values.length;
const x = Array.from({ length: n }, (_, i) => i);
const sumX = x.reduce((a, b) => a + b, 0);
const sumY = values.reduce((a, b) => a + b, 0);
const sumXY = x.reduce((sum, xi, i) => sum + xi * values[i], 0);
const sumXX = x.reduce((sum, xi) => sum + xi * xi, 0);
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
return slope;
}
getSensitivityThreshold() {
switch (this.config.sensitivity) {
case "HIGH":
return 2.5;
case "MEDIUM":
return 3.5;
case "LOW":
return 4.5;
default:
return 3.5;
}
}
calculateSeverity(score, threshold) {
const ratio = score / threshold;
if (ratio > 3)
return "CRITICAL";
if (ratio > 2)
return "HIGH";
if (ratio > 1.5)
return "MEDIUM";
return "LOW";
}
generateAnomalyDescription(type, deviation, deviationPercentage) {
const direction = deviation > 0 ? "increase" : "decrease";
const magnitude = deviationPercentage > 100 ? "massive" : deviationPercentage > 50 ? "significant" : deviationPercentage > 25 ? "notable" : "minor";
return `${type.toLowerCase()} anomaly detected: ${magnitude} ${direction} of ${deviationPercentage.toFixed(1)}%`;
}
generatePotentialCauses(type, deviationPercentage) {
const causes = [];
switch (type) {
case "SPIKE":
causes.push("Increased resource usage or traffic");
causes.push("New service deployments or scaling events");
causes.push("Data transfer spikes or storage usage increases");
if (deviationPercentage > 50) {
causes.push("Potential security incident or DDoS attack");
causes.push("Misconfigured auto-scaling rules");
}
break;
case "DROP":
causes.push("Reduced usage or traffic patterns");
causes.push("Service shutdowns or downscaling");
causes.push("Resource optimization implementations");
if (deviationPercentage > 50) {
causes.push("Service outages or failures");
causes.push("Billing or account issues");
}
break;
case "TREND_CHANGE":
causes.push("Business growth or contraction");
causes.push("Architectural changes or migrations");
causes.push("New feature rollouts or service changes");
causes.push("Seasonal business pattern shifts");
break;
case "SEASONAL_ANOMALY":
causes.push("Unusual business events or promotions");
causes.push("Holiday pattern deviations");
causes.push("Market or economic factors");
causes.push("Competitor actions or market changes");
break;
}
return causes;
}
consolidateAnomalies(anomalies) {
const groupedAnomalies = /* @__PURE__ */ new Map();
anomalies.forEach((anomaly) => {
const key = anomaly.timestamp;
if (!groupedAnomalies.has(key)) {
groupedAnomalies.set(key, []);
}
groupedAnomalies.get(key).push(anomaly);
});
const consolidated = [];
groupedAnomalies.forEach((group) => {
const sorted = group.sort((a, b) => {
const severityOrder = { "CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1 };
if (severityOrder[a.severity] !== severityOrder[b.severity]) {
return severityOrder[b.severity] - severityOrder[a.severity];
}
return b.confidence - a.confidence;
});
consolidated.push(sorted[0]);
});
return consolidated.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
}
};
__name(AnomalyDetector, "AnomalyDetector");
CostAnalyticsEngine = class {
constructor(config) {
this.anomalyDetector = new AnomalyDetector(config);
}
analyzeProvider(provider, costData, serviceData) {
const analytics = {
provider,
analysisDate: (/* @__PURE__ */ new Date()).toISOString(),
overallAnomalies: this.anomalyDetector.detectAnomalies(costData),
serviceAnomalies: {},
insights: this.generateInsights(costData),
recommendations: this.generateRecommendations(costData)
};
if (serviceData) {
Object.entries(serviceData).forEach(([service, data]) => {
analytics.serviceAnomalies[service] = this.anomalyDetector.detectAnomalies(data);
});
}
return analytics;
}
generateInsights(costData) {
const insights = [];
const values = costData.map((dp) => dp.value);
if (values.length < 7)
return insights;
const latest = values[values.length - 1];
const weekAgo = values[values.length - 7];
const monthAgo = values.length > 30 ? values[values.length - 30] : values[0];
const weekGrowth = weekAgo > 0 ? (latest - weekAgo) / weekAgo * 100 : 0;
const monthGrowth = monthAgo > 0 ? (latest - monthAgo) / monthAgo * 100 : 0;
if (Math.abs(weekGrowth) > 15) {
insights.push(`Significant week-over-week cost ${weekGrowth > 0 ? "increase" : "decrease"} of ${Math.abs(weekGrowth).toFixed(1)}%`);
}
if (Math.abs(monthGrowth) > 25) {
insights.push(`Notable month-over-month cost ${monthGrowth > 0 ? "growth" : "reduction"} of ${Math.abs(monthGrowth).toFixed(1)}%`);
}
const volatility = this.calculateVolatility(values);
if (volatility > 0.3) {
insights.push(`High cost volatility detected (${(volatility * 100).toFixed(1)}%) - consider investigating irregular spending patterns`);
}
return insights;
}
generateRecommendations(costData) {
const recommendations = []