UNPKG

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
#!/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 = []