@jadenrazo/cloudcost-mcp 0.1.9 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4294 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- getAwsInstances,
4
- getAzureVmSizes,
5
- getGcpComputePricing,
6
- getGcpDiskPricing,
7
- getGcpMachineTypes,
8
- getGcpSqlPricing,
9
- getGcpStoragePricing,
10
- getInstanceMap,
11
- getRegionMappings,
12
- getResourceEquivalents,
13
- getStorageMap
14
- } from "./chunk-KZJSZMWM.js";
15
-
16
- // src/server.ts
17
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
-
20
- // src/config.ts
21
- import { readFileSync, existsSync } from "fs";
22
- import { join } from "path";
23
- import { homedir } from "os";
24
-
25
- // src/types/config.ts
26
- var DEFAULT_CONFIG = {
27
- cache: {
28
- ttl_seconds: 86400,
29
- db_path: ""
30
- },
31
- pricing: {
32
- monthly_hours: 730,
33
- default_currency: "USD",
34
- aws_pricing_region: "us-east-1"
35
- },
36
- logging: {
37
- level: "info"
38
- }
39
- };
40
-
41
- // src/config.ts
42
- function getConfigDir() {
43
- return join(homedir(), ".cloudcost");
44
- }
45
- function loadFileConfig() {
46
- const configPath = join(getConfigDir(), "config.json");
47
- if (!existsSync(configPath)) return {};
48
- try {
49
- return JSON.parse(readFileSync(configPath, "utf-8"));
50
- } catch {
51
- return {};
52
- }
53
- }
54
- function loadEnvConfig() {
55
- const config = {};
56
- const env = process.env;
57
- if (env.CLOUDCOST_CACHE_TTL || env.CLOUDCOST_CACHE_PATH) {
58
- config.cache = {
59
- ttl_seconds: env.CLOUDCOST_CACHE_TTL ? parseInt(env.CLOUDCOST_CACHE_TTL, 10) : DEFAULT_CONFIG.cache.ttl_seconds,
60
- db_path: env.CLOUDCOST_CACHE_PATH ?? ""
61
- };
62
- }
63
- if (env.CLOUDCOST_LOG_LEVEL) {
64
- config.logging = {
65
- level: env.CLOUDCOST_LOG_LEVEL
66
- };
67
- }
68
- if (env.CLOUDCOST_MONTHLY_HOURS) {
69
- config.pricing = {
70
- ...DEFAULT_CONFIG.pricing,
71
- monthly_hours: parseInt(env.CLOUDCOST_MONTHLY_HOURS, 10)
72
- };
73
- }
74
- return config;
75
- }
76
- function loadConfig() {
77
- const fileConfig = loadFileConfig();
78
- const envConfig = loadEnvConfig();
79
- const defaultDbPath = join(getConfigDir(), "cache.db");
80
- return {
81
- cache: {
82
- ...DEFAULT_CONFIG.cache,
83
- db_path: defaultDbPath,
84
- ...fileConfig.cache,
85
- ...envConfig.cache
86
- },
87
- pricing: {
88
- ...DEFAULT_CONFIG.pricing,
89
- ...fileConfig.pricing,
90
- ...envConfig.pricing
91
- },
92
- logging: {
93
- ...DEFAULT_CONFIG.logging,
94
- ...fileConfig.logging,
95
- ...envConfig.logging
96
- }
97
- };
98
- }
99
-
100
- // src/logger.ts
101
- var LEVEL_ORDER = {
102
- debug: 0,
103
- info: 1,
104
- warn: 2,
105
- error: 3
106
- };
107
- var currentLevel = "info";
108
- function setLogLevel(level) {
109
- currentLevel = level;
110
- }
111
- function shouldLog(level) {
112
- return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
113
- }
114
- function log(level, message, data) {
115
- if (!shouldLog(level)) return;
116
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
117
- const entry = data ? `[${timestamp}] ${level.toUpperCase()}: ${message} ${JSON.stringify(data)}` : `[${timestamp}] ${level.toUpperCase()}: ${message}`;
118
- process.stderr.write(entry + "\n");
119
- }
120
- var logger = {
121
- debug: (msg, data) => log("debug", msg, data),
122
- info: (msg, data) => log("info", msg, data),
123
- warn: (msg, data) => log("warn", msg, data),
124
- error: (msg, data) => log("error", msg, data)
125
- };
126
-
127
- // src/pricing/cache.ts
128
- import Database from "better-sqlite3";
129
- import { mkdirSync } from "fs";
130
- import { dirname } from "path";
131
- var SCHEMA = `
132
- CREATE TABLE IF NOT EXISTS pricing_cache (
133
- key TEXT PRIMARY KEY,
134
- data TEXT NOT NULL,
135
- provider TEXT NOT NULL,
136
- service TEXT NOT NULL,
137
- region TEXT NOT NULL,
138
- created_at TEXT NOT NULL,
139
- expires_at TEXT NOT NULL
140
- );
141
- CREATE INDEX IF NOT EXISTS idx_pricing_cache_provider ON pricing_cache (provider);
142
- CREATE INDEX IF NOT EXISTS idx_pricing_cache_expires_at ON pricing_cache (expires_at);
143
- `;
144
- var PricingCache = class {
145
- db;
146
- constructor(dbPath) {
147
- mkdirSync(dirname(dbPath), { recursive: true });
148
- this.db = new Database(dbPath);
149
- this.db.pragma("journal_mode = WAL");
150
- this.db.pragma("foreign_keys = ON");
151
- this.db.exec(SCHEMA);
152
- logger.debug("PricingCache initialised", { dbPath });
153
- }
154
- /**
155
- * Retrieve a cached value by key. Returns null when the entry does not exist
156
- * or has already expired; expired rows are deleted as a side-effect so the
157
- * table self-trims on read.
158
- */
159
- get(key) {
160
- const row = this.db.prepare(
161
- "SELECT * FROM pricing_cache WHERE key = ?"
162
- ).get(key);
163
- if (!row) return null;
164
- const now = (/* @__PURE__ */ new Date()).toISOString();
165
- if (row.expires_at <= now) {
166
- this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
167
- logger.debug("Cache miss (expired)", { key });
168
- return null;
169
- }
170
- logger.debug("Cache hit", { key });
171
- return JSON.parse(row.data);
172
- }
173
- /**
174
- * Store a value in the cache.
175
- *
176
- * The composite key convention for pricing lookups is:
177
- * `{provider}/{service}/{region}/{resource_type}`
178
- *
179
- * Callers are responsible for constructing the key before calling this
180
- * method; the method itself treats the key as an opaque string so it can
181
- * be reused for other cache domains in the future.
182
- */
183
- set(key, data, provider, service, region, ttlSeconds) {
184
- const now = /* @__PURE__ */ new Date();
185
- const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
186
- this.db.prepare(
187
- `INSERT INTO pricing_cache (key, data, provider, service, region, created_at, expires_at)
188
- VALUES (?, ?, ?, ?, ?, ?, ?)
189
- ON CONFLICT (key) DO UPDATE SET
190
- data = excluded.data,
191
- provider = excluded.provider,
192
- service = excluded.service,
193
- region = excluded.region,
194
- created_at = excluded.created_at,
195
- expires_at = excluded.expires_at`
196
- ).run(
197
- key,
198
- JSON.stringify(data),
199
- provider,
200
- service,
201
- region,
202
- now.toISOString(),
203
- expiresAt.toISOString()
204
- );
205
- logger.debug("Cache set", { key, ttlSeconds });
206
- }
207
- /** Remove a single cache entry. */
208
- invalidate(key) {
209
- const result = this.db.prepare("DELETE FROM pricing_cache WHERE key = ?").run(key);
210
- logger.debug("Cache invalidated", { key, removed: result.changes });
211
- }
212
- /** Remove every cache entry belonging to a provider. */
213
- invalidateByProvider(provider) {
214
- const result = this.db.prepare("DELETE FROM pricing_cache WHERE provider = ?").run(provider);
215
- logger.debug("Cache invalidated by provider", {
216
- provider,
217
- removed: result.changes
218
- });
219
- }
220
- /**
221
- * Delete all entries whose expiry timestamp is in the past.
222
- * Returns the number of rows removed.
223
- */
224
- cleanup() {
225
- const now = (/* @__PURE__ */ new Date()).toISOString();
226
- const result = this.db.prepare("DELETE FROM pricing_cache WHERE expires_at <= ?").run(now);
227
- const removed = result.changes;
228
- logger.debug("Cache cleanup complete", { removed });
229
- return removed;
230
- }
231
- /** Return aggregate statistics about the current cache state. */
232
- getStats() {
233
- const now = (/* @__PURE__ */ new Date()).toISOString();
234
- const totals = this.db.prepare(
235
- `SELECT
236
- COUNT(*) AS total_entries,
237
- COALESCE(SUM(LENGTH(data)), 0) AS size_bytes
238
- FROM pricing_cache`
239
- ).get();
240
- const expired = this.db.prepare(
241
- "SELECT COUNT(*) AS expired_entries FROM pricing_cache WHERE expires_at <= ?"
242
- ).get(now);
243
- return {
244
- total_entries: totals.total_entries,
245
- expired_entries: expired.expired_entries,
246
- size_bytes: totals.size_bytes
247
- };
248
- }
249
- /** Close the underlying database connection. */
250
- close() {
251
- this.db.close();
252
- logger.debug("PricingCache closed");
253
- }
254
- };
255
-
256
- // src/pricing/aws/aws-normalizer.ts
257
- function extractUsdPrice(priceDimensions) {
258
- for (const dim of Object.values(priceDimensions)) {
259
- const usd = dim?.pricePerUnit?.USD;
260
- if (usd !== void 0) {
261
- const val = parseFloat(usd);
262
- if (!isNaN(val)) return val;
263
- }
264
- }
265
- return 0;
266
- }
267
- function extractUnit(priceDimensions) {
268
- for (const dim of Object.values(priceDimensions)) {
269
- if (dim?.unit) return String(dim.unit);
270
- }
271
- return "Hrs";
272
- }
273
- function extractFromTerms(onDemandTerms, defaultUnit) {
274
- for (const skuTerms of Object.values(onDemandTerms)) {
275
- if (skuTerms?.priceDimensions) {
276
- return {
277
- price: extractUsdPrice(skuTerms.priceDimensions),
278
- unit: extractUnit(skuTerms.priceDimensions)
279
- };
280
- }
281
- for (const offerTerm of Object.values(skuTerms ?? {})) {
282
- const dims = offerTerm?.priceDimensions;
283
- if (dims) {
284
- return {
285
- price: extractUsdPrice(dims),
286
- unit: extractUnit(dims)
287
- };
288
- }
289
- }
290
- }
291
- return { price: 0, unit: defaultUnit };
292
- }
293
- function normalizeAwsCompute(rawProduct, rawPrice, region) {
294
- const attrs = rawProduct?.attributes ?? {};
295
- const terms = rawPrice?.terms?.OnDemand ?? {};
296
- const { price, unit } = extractFromTerms(terms, "Hrs");
297
- return {
298
- provider: "aws",
299
- service: "ec2",
300
- resource_type: attrs.instanceType ?? "unknown",
301
- region,
302
- unit,
303
- price_per_unit: price,
304
- currency: "USD",
305
- description: attrs.instanceType ? `AWS EC2 ${attrs.instanceType} (${attrs.operatingSystem ?? "Linux"})` : void 0,
306
- attributes: {
307
- instance_type: attrs.instanceType ?? "",
308
- vcpu: attrs.vcpu ?? "",
309
- memory: attrs.memory ?? "",
310
- operating_system: attrs.operatingSystem ?? "Linux",
311
- tenancy: attrs.tenancy ?? "Shared",
312
- pricing_source: "live"
313
- },
314
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
315
- };
316
- }
317
- function normalizeAwsDatabase(rawProduct, rawPrice, region) {
318
- const attrs = rawProduct?.attributes ?? {};
319
- const terms = rawPrice?.terms?.OnDemand ?? {};
320
- const { price, unit } = extractFromTerms(terms, "Hrs");
321
- return {
322
- provider: "aws",
323
- service: "rds",
324
- resource_type: attrs.instanceType ?? "unknown",
325
- region,
326
- unit,
327
- price_per_unit: price,
328
- currency: "USD",
329
- description: attrs.instanceType ? `AWS RDS ${attrs.instanceType} (${attrs.databaseEngine ?? "MySQL"})` : void 0,
330
- attributes: {
331
- instance_type: attrs.instanceType ?? "",
332
- database_engine: attrs.databaseEngine ?? "",
333
- deployment_option: attrs.deploymentOption ?? "Single-AZ",
334
- vcpu: attrs.vcpu ?? "",
335
- memory: attrs.memory ?? "",
336
- pricing_source: "live"
337
- },
338
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
339
- };
340
- }
341
- function normalizeAwsStorage(rawProduct, rawPrice, region) {
342
- const attrs = rawProduct?.attributes ?? {};
343
- const terms = rawPrice?.terms?.OnDemand ?? {};
344
- const { price, unit } = extractFromTerms(terms, "GB-Mo");
345
- const volumeType = attrs.volumeApiName ?? attrs.volumeType ?? "gp3";
346
- return {
347
- provider: "aws",
348
- service: "ebs",
349
- resource_type: volumeType,
350
- region,
351
- unit,
352
- price_per_unit: price,
353
- currency: "USD",
354
- description: `AWS EBS ${volumeType} volume`,
355
- attributes: {
356
- volume_type: volumeType,
357
- max_iops: attrs.maxIopsvolume ?? "",
358
- max_throughput: attrs.maxThroughputvolume ?? "",
359
- pricing_source: "live"
360
- },
361
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
362
- };
363
- }
364
-
365
- // src/pricing/aws/bulk-loader.ts
366
- function parseCsvLine(line) {
367
- const fields = [];
368
- let i = 0;
369
- const len = line.length;
370
- while (i <= len) {
371
- if (i === len) {
372
- if (fields.length > 0 && line[len - 1] === ",") {
373
- fields.push("");
374
- }
375
- break;
376
- }
377
- if (line[i] === '"') {
378
- i++;
379
- let field = "";
380
- while (i < len) {
381
- if (line[i] === '"') {
382
- if (i + 1 < len && line[i + 1] === '"') {
383
- field += '"';
384
- i += 2;
385
- } else {
386
- i++;
387
- break;
388
- }
389
- } else {
390
- field += line[i];
391
- i++;
392
- }
393
- }
394
- fields.push(field);
395
- if (i < len && line[i] === ",") i++;
396
- } else {
397
- const start = i;
398
- while (i < len && line[i] !== ",") i++;
399
- fields.push(line.slice(start, i));
400
- if (i < len) i++;
401
- }
402
- }
403
- return fields;
404
- }
405
- var EC2_BASE_PRICES = {
406
- "t2.micro": 0.0116,
407
- "t2.small": 0.023,
408
- "t2.medium": 0.0464,
409
- "t2.large": 0.0928,
410
- "t3.micro": 0.0104,
411
- "t3.small": 0.0208,
412
- "t3.medium": 0.0416,
413
- "t3.large": 0.0832,
414
- "t3.xlarge": 0.1664,
415
- "t3.2xlarge": 0.3328,
416
- "t3a.micro": 94e-4,
417
- "t3a.small": 0.0188,
418
- "t3a.medium": 0.0376,
419
- "t3a.large": 0.0752,
420
- "t3a.xlarge": 0.1504,
421
- "t4g.micro": 84e-4,
422
- "t4g.small": 0.0168,
423
- "t4g.medium": 0.0336,
424
- "t4g.large": 0.0672,
425
- "t4g.xlarge": 0.1344,
426
- "m5.large": 0.096,
427
- "m5.xlarge": 0.192,
428
- "m5.2xlarge": 0.384,
429
- "m5.4xlarge": 0.768,
430
- "m5a.large": 0.086,
431
- "m5a.xlarge": 0.172,
432
- "m5a.2xlarge": 0.344,
433
- "m5a.4xlarge": 0.688,
434
- "m6i.large": 0.096,
435
- "m6i.xlarge": 0.192,
436
- "m6i.2xlarge": 0.384,
437
- "m6i.4xlarge": 0.768,
438
- "m6i.8xlarge": 1.536,
439
- "m6g.large": 0.077,
440
- "m6g.xlarge": 0.154,
441
- "m6g.2xlarge": 0.308,
442
- "m6g.4xlarge": 0.616,
443
- "m7i.large": 0.1008,
444
- "m7i.xlarge": 0.2016,
445
- "m7i.2xlarge": 0.4032,
446
- "m7g.large": 0.0816,
447
- "m7g.xlarge": 0.1632,
448
- "m7g.2xlarge": 0.3264,
449
- "m7g.4xlarge": 0.6528,
450
- "c5.large": 0.085,
451
- "c5.xlarge": 0.17,
452
- "c5.2xlarge": 0.34,
453
- "c5a.large": 0.077,
454
- "c5a.xlarge": 0.154,
455
- "c5a.2xlarge": 0.308,
456
- "c6i.large": 0.085,
457
- "c6i.xlarge": 0.17,
458
- "c6i.2xlarge": 0.34,
459
- "c6g.large": 0.068,
460
- "c6g.xlarge": 0.136,
461
- "c6g.2xlarge": 0.272,
462
- "c6g.4xlarge": 0.544,
463
- "c7i.large": 0.089,
464
- "c7i.xlarge": 0.178,
465
- "c7i.2xlarge": 0.357,
466
- "c7g.large": 0.072,
467
- "c7g.xlarge": 0.145,
468
- "c7g.2xlarge": 0.29,
469
- "c7g.4xlarge": 0.58,
470
- "r5.large": 0.126,
471
- "r5.xlarge": 0.252,
472
- "r5.2xlarge": 0.504,
473
- "r5a.large": 0.113,
474
- "r5a.xlarge": 0.226,
475
- "r5a.2xlarge": 0.452,
476
- "r6i.large": 0.126,
477
- "r6i.xlarge": 0.252,
478
- "r6i.2xlarge": 0.504,
479
- "r6g.large": 0.101,
480
- "r6g.xlarge": 0.202,
481
- "r6g.2xlarge": 0.403,
482
- "r6g.4xlarge": 0.806,
483
- "r7i.large": 0.133,
484
- "r7i.xlarge": 0.266,
485
- "r7i.2xlarge": 0.532,
486
- "r7g.large": 0.107,
487
- "r7g.xlarge": 0.214,
488
- "r7g.2xlarge": 0.428,
489
- "r7g.4xlarge": 0.856
490
- };
491
- var RDS_BASE_PRICES = {
492
- "db.t3.micro": 0.017,
493
- "db.t3.small": 0.034,
494
- "db.t3.medium": 0.068,
495
- "db.t3.large": 0.136,
496
- "db.t4g.micro": 0.016,
497
- "db.t4g.small": 0.032,
498
- "db.t4g.medium": 0.065,
499
- "db.t4g.large": 0.129,
500
- "db.t4g.xlarge": 0.258,
501
- "db.m5.large": 0.171,
502
- "db.m5.xlarge": 0.342,
503
- "db.m6g.large": 0.154,
504
- "db.m6g.xlarge": 0.308,
505
- "db.m6g.2xlarge": 0.616,
506
- "db.m6i.large": 0.171,
507
- "db.m6i.xlarge": 0.342,
508
- "db.m6i.2xlarge": 0.684,
509
- "db.m7g.large": 0.168,
510
- "db.m7g.xlarge": 0.336,
511
- "db.m7g.2xlarge": 0.672,
512
- "db.r5.large": 0.25,
513
- "db.r5.xlarge": 0.5,
514
- "db.r5.2xlarge": 1,
515
- "db.r6g.large": 0.218,
516
- "db.r6g.xlarge": 0.437,
517
- "db.r6g.2xlarge": 0.874,
518
- "db.r6i.large": 0.25,
519
- "db.r6i.xlarge": 0.5,
520
- "db.r6i.2xlarge": 1
521
- };
522
- var EBS_BASE_PRICES = {
523
- "gp3": 0.08,
524
- "gp2": 0.1,
525
- "io2": 0.125,
526
- "io1": 0.125,
527
- "st1": 0.045,
528
- "sc1": 0.015
529
- };
530
- var ALB_HOURLY = 0.0225;
531
- var ALB_LCU_HOURLY = 8e-3;
532
- var NAT_HOURLY = 0.045;
533
- var NAT_PER_GB = 0.045;
534
- var EKS_HOURLY = 0.1;
535
- var REGION_MULTIPLIERS = {
536
- "us-east-1": 1,
537
- "us-east-2": 1,
538
- "us-west-1": 1.08,
539
- "us-west-2": 1,
540
- "eu-west-1": 1.1,
541
- "eu-west-2": 1.12,
542
- "eu-central-1": 1.12,
543
- "ap-southeast-1": 1.15,
544
- "ap-southeast-2": 1.15,
545
- "ap-northeast-1": 1.12,
546
- "ap-northeast-2": 1.1,
547
- "ap-south-1": 1.08,
548
- "sa-east-1": 1.2,
549
- "ca-central-1": 1.08,
550
- "eu-north-1": 1.1,
551
- "eu-south-1": 1.12,
552
- "me-south-1": 1.15,
553
- "af-south-1": 1.22
554
- };
555
- function regionMultiplier(region) {
556
- return REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
557
- }
558
- var CACHE_TTL = 86400;
559
- var BULK_PRICING_BASE = "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws";
560
- var AwsBulkLoader = class _AwsBulkLoader {
561
- cache;
562
- /**
563
- * Deduplicates concurrent streaming CSV fetches for the same region.
564
- * Keyed by region; value is the in-flight Promise<boolean>.
565
- */
566
- ec2CsvFetchInProgress = /* @__PURE__ */ new Map();
567
- constructor(cache) {
568
- this.cache = cache;
569
- }
570
- // -------------------------------------------------------------------------
571
- // Public API
572
- // -------------------------------------------------------------------------
573
- async getComputePrice(instanceType, region, os = "Linux") {
574
- const cacheKey = this.buildCacheKey("ec2", region, instanceType, os);
575
- const cached = this.cache.get(cacheKey);
576
- if (cached) return cached;
577
- try {
578
- const csvOk = await this.fetchAndCacheEc2CsvPrices(region);
579
- if (csvOk) {
580
- const afterCsv = this.cache.get(cacheKey);
581
- if (afterCsv) return afterCsv;
582
- }
583
- } catch (err) {
584
- logger.warn("AWS EC2 CSV streaming failed", {
585
- region,
586
- instanceType,
587
- err: err instanceof Error ? err.message : String(err)
588
- });
589
- }
590
- try {
591
- const bulk = await this.fetchBulkPricing("AmazonEC2", region);
592
- if (bulk) {
593
- const result = this.extractEc2Price(bulk, instanceType, region, os);
594
- if (result) {
595
- this.cache.set(cacheKey, result, "aws", "ec2", region, CACHE_TTL);
596
- return result;
597
- }
598
- }
599
- } catch (err) {
600
- logger.warn("AWS bulk EC2 JSON fetch failed, using fallback", {
601
- region,
602
- instanceType,
603
- err: err instanceof Error ? err.message : String(err)
604
- });
605
- }
606
- return this.fallbackComputePrice(instanceType, region, os, cacheKey);
607
- }
608
- async getDatabasePrice(instanceClass, region, engine = "MySQL") {
609
- const cacheKey = this.buildCacheKey("rds", region, instanceClass, engine);
610
- const cached = this.cache.get(cacheKey);
611
- if (cached) return cached;
612
- try {
613
- const bulk = await this.fetchBulkPricing("AmazonRDS", region);
614
- if (bulk) {
615
- const result = this.extractRdsPrice(bulk, instanceClass, region, engine);
616
- if (result) {
617
- this.cache.set(cacheKey, result, "aws", "rds", region, CACHE_TTL);
618
- return result;
619
- }
620
- }
621
- } catch (err) {
622
- logger.warn("AWS bulk RDS fetch failed, using fallback", {
623
- region,
624
- instanceClass,
625
- err: err instanceof Error ? err.message : String(err)
626
- });
627
- }
628
- return this.fallbackDatabasePrice(instanceClass, region, engine, cacheKey);
629
- }
630
- async getStoragePrice(volumeType, region) {
631
- const cacheKey = this.buildCacheKey("ebs", region, volumeType);
632
- const cached = this.cache.get(cacheKey);
633
- if (cached) return cached;
634
- try {
635
- const bulk = await this.fetchBulkPricing("AmazonEC2", region);
636
- if (bulk) {
637
- const result = this.extractEbsPrice(bulk, volumeType, region);
638
- if (result) {
639
- this.cache.set(cacheKey, result, "aws", "ebs", region, CACHE_TTL);
640
- return result;
641
- }
642
- }
643
- } catch (err) {
644
- logger.warn("AWS bulk EBS fetch failed, using fallback", {
645
- region,
646
- volumeType,
647
- err: err instanceof Error ? err.message : String(err)
648
- });
649
- }
650
- return this.fallbackStoragePrice(volumeType, region, cacheKey);
651
- }
652
- async getLoadBalancerPrice(region) {
653
- const multiplier = regionMultiplier(region);
654
- return {
655
- provider: "aws",
656
- service: "elb",
657
- resource_type: "alb",
658
- region,
659
- unit: "Hrs",
660
- price_per_unit: ALB_HOURLY * multiplier,
661
- currency: "USD",
662
- description: "AWS Application Load Balancer (hourly fixed charge)",
663
- attributes: {
664
- lcu_hourly_price: String(ALB_LCU_HOURLY * multiplier),
665
- pricing_model: "fixed_plus_lcu"
666
- },
667
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
668
- };
669
- }
670
- async getNatGatewayPrice(region) {
671
- const multiplier = regionMultiplier(region);
672
- return {
673
- provider: "aws",
674
- service: "vpc",
675
- resource_type: "nat-gateway",
676
- region,
677
- unit: "Hrs",
678
- price_per_unit: NAT_HOURLY * multiplier,
679
- currency: "USD",
680
- description: "AWS NAT Gateway (hourly charge + data processing)",
681
- attributes: {
682
- per_gb_price: String(NAT_PER_GB * multiplier)
683
- },
684
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
685
- };
686
- }
687
- async getKubernetesPrice(region) {
688
- const multiplier = regionMultiplier(region);
689
- return {
690
- provider: "aws",
691
- service: "eks",
692
- resource_type: "cluster",
693
- region,
694
- unit: "Hrs",
695
- price_per_unit: EKS_HOURLY * multiplier,
696
- currency: "USD",
697
- description: "AWS EKS control plane (per cluster/hour)",
698
- attributes: {},
699
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
700
- };
701
- }
702
- // -------------------------------------------------------------------------
703
- // Internal helpers – EC2 CSV streaming
704
- // -------------------------------------------------------------------------
705
- /**
706
- * Fetch the per-region EC2 pricing CSV from the AWS Bulk Pricing endpoint,
707
- * stream it line-by-line, parse every "Compute Instance / OnDemand / Shared"
708
- * row for ALL operating systems, and write each extracted price into the
709
- * SQLite cache.
710
- *
711
- * The CSV URL pattern is:
712
- * https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/{region}/index.csv
713
- *
714
- * The first ~5 rows of the file are metadata (FormatVersion, Disclaimer,
715
- * Publication Date, etc.). The actual header row is the first row whose
716
- * first field is "SKU".
717
- *
718
- * Returns true when at least one price was extracted and cached.
719
- */
720
- fetchAndCacheEc2CsvPrices(region) {
721
- const inflight = this.ec2CsvFetchInProgress.get(region);
722
- if (inflight) return inflight;
723
- const promise = this._doFetchAndCacheEc2CsvPrices(region).finally(() => {
724
- this.ec2CsvFetchInProgress.delete(region);
725
- });
726
- this.ec2CsvFetchInProgress.set(region, promise);
727
- return promise;
728
- }
729
- async _doFetchAndCacheEc2CsvPrices(region) {
730
- const url = `${BULK_PRICING_BASE}/AmazonEC2/current/${region}/index.csv`;
731
- logger.debug("Streaming AWS EC2 CSV pricing", { url, region });
732
- const controller = new AbortController();
733
- const timeoutId = setTimeout(() => controller.abort(), 12e4);
734
- try {
735
- const res = await fetch(url, { signal: controller.signal });
736
- if (!res.ok) {
737
- logger.debug("AWS EC2 CSV fetch returned non-OK status", {
738
- region,
739
- status: res.status
740
- });
741
- return false;
742
- }
743
- if (!res.body) {
744
- logger.debug("AWS EC2 CSV response has no body", { region });
745
- return false;
746
- }
747
- const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
748
- let colInstanceType = -1;
749
- let colOS = -1;
750
- let colTenancy = -1;
751
- let colTermType = -1;
752
- let colCapacityStatus = -1;
753
- let colProductFamily = -1;
754
- let colPrice = -1;
755
- let colUnit = -1;
756
- let headerFound = false;
757
- let leftover = "";
758
- let cachedCount = 0;
759
- const effectiveDate = (/* @__PURE__ */ new Date()).toISOString();
760
- while (true) {
761
- const { value, done } = await reader.read();
762
- if (done) break;
763
- const chunk = leftover + value;
764
- const lines = chunk.split("\n");
765
- leftover = lines.pop() ?? "";
766
- for (const rawLine of lines) {
767
- const line = rawLine.trimEnd();
768
- if (!line) continue;
769
- if (!headerFound) {
770
- if (line.startsWith('"SKU"') || line.startsWith("SKU")) {
771
- const headers = parseCsvLine(line);
772
- for (let h = 0; h < headers.length; h++) {
773
- const name = headers[h].trim();
774
- if (name === "Instance Type") colInstanceType = h;
775
- else if (name === "Operating System") colOS = h;
776
- else if (name === "Tenancy") colTenancy = h;
777
- else if (name === "TermType") colTermType = h;
778
- else if (name === "Capacity Status") colCapacityStatus = h;
779
- else if (name === "Product Family") colProductFamily = h;
780
- else if (name === "PricePerUnit" || name === "Price Per Unit") colPrice = h;
781
- else if (name === "Unit") colUnit = h;
782
- }
783
- if (colInstanceType === -1 || colOS === -1 || colPrice === -1) {
784
- logger.debug("AWS EC2 CSV header missing required columns", {
785
- region,
786
- colInstanceType,
787
- colOS,
788
- colPrice
789
- });
790
- return false;
791
- }
792
- headerFound = true;
793
- }
794
- continue;
795
- }
796
- if (!line.includes("OnDemand") || !line.includes("Compute Instance")) {
797
- continue;
798
- }
799
- const fields = parseCsvLine(line);
800
- if (colProductFamily !== -1) {
801
- const pf = fields[colProductFamily] ?? "";
802
- if (pf !== "Compute Instance") continue;
803
- }
804
- if (colTenancy !== -1) {
805
- const tenancy = fields[colTenancy] ?? "";
806
- if (tenancy !== "Shared") continue;
807
- }
808
- if (colTermType !== -1) {
809
- const termType = fields[colTermType] ?? "";
810
- if (termType !== "OnDemand") continue;
811
- }
812
- if (colCapacityStatus !== -1) {
813
- const cap = fields[colCapacityStatus] ?? "";
814
- if (cap !== "Used") continue;
815
- }
816
- const instanceType = fields[colInstanceType] ?? "";
817
- const os = fields[colOS] ?? "";
818
- const rawPrice = fields[colPrice] ?? "";
819
- const unit = colUnit !== -1 ? fields[colUnit] ?? "Hrs" : "Hrs";
820
- if (!instanceType || !os || !rawPrice) continue;
821
- const priceValue = parseFloat(rawPrice);
822
- if (!isFinite(priceValue) || priceValue <= 0) continue;
823
- const csvCacheKey = `aws/ec2/${region}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
824
- const normalized = {
825
- provider: "aws",
826
- service: "ec2",
827
- resource_type: instanceType,
828
- region,
829
- unit,
830
- price_per_unit: priceValue,
831
- currency: "USD",
832
- description: `AWS EC2 ${instanceType} (${os}) on-demand`,
833
- attributes: {
834
- instance_type: instanceType,
835
- operating_system: os,
836
- tenancy: "Shared",
837
- pricing_source: "live"
838
- },
839
- effective_date: effectiveDate
840
- };
841
- this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, CACHE_TTL);
842
- cachedCount++;
843
- }
844
- }
845
- if (leftover.trim() && headerFound) {
846
- const line = leftover.trimEnd();
847
- if (line.includes("OnDemand") && line.includes("Compute Instance")) {
848
- const fields = parseCsvLine(line);
849
- const instanceType = fields[colInstanceType] ?? "";
850
- const os = fields[colOS] ?? "";
851
- const rawPrice = fields[colPrice] ?? "";
852
- const unit = colUnit !== -1 ? fields[colUnit] ?? "Hrs" : "Hrs";
853
- const priceValue = parseFloat(rawPrice);
854
- if (instanceType && os && isFinite(priceValue) && priceValue > 0) {
855
- const csvCacheKey = `aws/ec2/${region}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
856
- this.cache.set(
857
- csvCacheKey,
858
- {
859
- provider: "aws",
860
- service: "ec2",
861
- resource_type: instanceType,
862
- region,
863
- unit,
864
- price_per_unit: priceValue,
865
- currency: "USD",
866
- description: `AWS EC2 ${instanceType} (${os}) on-demand`,
867
- attributes: {
868
- instance_type: instanceType,
869
- operating_system: os,
870
- tenancy: "Shared",
871
- pricing_source: "live"
872
- },
873
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
874
- },
875
- "aws",
876
- "ec2",
877
- region,
878
- CACHE_TTL
879
- );
880
- cachedCount++;
881
- }
882
- }
883
- }
884
- logger.debug("AWS EC2 CSV streaming complete", { region, cachedCount });
885
- return cachedCount > 0;
886
- } catch (err) {
887
- if (err?.name === "AbortError") {
888
- logger.debug("AWS EC2 CSV streaming timed out", { region });
889
- } else {
890
- logger.debug("AWS EC2 CSV streaming error", {
891
- region,
892
- err: err instanceof Error ? err.message : String(err)
893
- });
894
- }
895
- return false;
896
- } finally {
897
- clearTimeout(timeoutId);
898
- }
899
- }
900
- // -------------------------------------------------------------------------
901
- // Internal helpers – live API fetch
902
- // -------------------------------------------------------------------------
903
- async fetchBulkPricing(service, region) {
904
- const url = `${BULK_PRICING_BASE}/${service}/current/${region}/index.json`;
905
- logger.debug("Fetching AWS bulk pricing", { url });
906
- const res = await fetch(url, {
907
- signal: AbortSignal.timeout(3e4)
908
- });
909
- if (!res.ok) {
910
- throw new Error(`HTTP ${res.status} from ${url}`);
911
- }
912
- return res.json();
913
- }
914
- buildCacheKey(service, region, ...parts) {
915
- return ["aws", service, region, ...parts].map((p) => p.toLowerCase()).join("/");
916
- }
917
- // -------------------------------------------------------------------------
918
- // Internal helpers – extract prices from bulk JSON
919
- // -------------------------------------------------------------------------
920
- extractEc2Price(bulk, instanceType, region, os) {
921
- const products = bulk?.products ?? {};
922
- const onDemand = bulk?.terms?.OnDemand ?? {};
923
- const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
924
- for (const [sku, product] of sortedEntries) {
925
- const attrs = product?.attributes ?? {};
926
- if (attrs.instanceType === instanceType && (attrs.operatingSystem ?? "").toLowerCase() === os.toLowerCase() && attrs.tenancy === "Shared" && attrs.capacitystatus === "Used") {
927
- const priceTerms = onDemand[sku];
928
- if (priceTerms) {
929
- return normalizeAwsCompute(product, { terms: { OnDemand: { [sku]: priceTerms } } }, region);
930
- }
931
- }
932
- }
933
- return null;
934
- }
935
- extractRdsPrice(bulk, instanceClass, region, engine) {
936
- const products = bulk?.products ?? {};
937
- const onDemand = bulk?.terms?.OnDemand ?? {};
938
- const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
939
- for (const [sku, product] of sortedEntries) {
940
- const attrs = product?.attributes ?? {};
941
- if (attrs.instanceType === instanceClass && (attrs.databaseEngine ?? "").toLowerCase().includes(engine.toLowerCase()) && attrs.deploymentOption === "Single-AZ") {
942
- const priceTerms = onDemand[sku];
943
- if (priceTerms) {
944
- return normalizeAwsDatabase(product, { terms: { OnDemand: { [sku]: priceTerms } } }, region);
945
- }
946
- }
947
- }
948
- return null;
949
- }
950
- extractEbsPrice(bulk, volumeType, region) {
951
- const products = bulk?.products ?? {};
952
- const onDemand = bulk?.terms?.OnDemand ?? {};
953
- const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
954
- for (const [sku, product] of sortedEntries) {
955
- const attrs = product?.attributes ?? {};
956
- const apiName = (attrs.volumeApiName ?? "").toLowerCase();
957
- if (product?.productFamily === "Storage" && (apiName === volumeType.toLowerCase() || (attrs.volumeType ?? "").toLowerCase() === volumeType.toLowerCase())) {
958
- const priceTerms = onDemand[sku];
959
- if (priceTerms) {
960
- return normalizeAwsStorage(product, { terms: { OnDemand: { [sku]: priceTerms } } }, region);
961
- }
962
- }
963
- }
964
- return null;
965
- }
966
- // -------------------------------------------------------------------------
967
- // Internal helpers – size interpolation
968
- // -------------------------------------------------------------------------
969
- /**
970
- * AWS instance sizes follow a predictable doubling pattern:
971
- * nano → micro → small → medium → large → xlarge → 2xlarge → 4xlarge → 8xlarge → ...
972
- * Each step roughly doubles the price. Given a known price at one size,
973
- * we can estimate the price for a missing size in the same family.
974
- */
975
- static SIZE_ORDER = [
976
- "nano",
977
- "micro",
978
- "small",
979
- "medium",
980
- "large",
981
- "xlarge",
982
- "2xlarge",
983
- "4xlarge",
984
- "8xlarge",
985
- "12xlarge",
986
- "16xlarge",
987
- "24xlarge",
988
- "32xlarge",
989
- "48xlarge"
990
- ];
991
- interpolatePrice(requestedType, table) {
992
- const lower = requestedType.toLowerCase();
993
- const lastDot = lower.lastIndexOf(".");
994
- if (lastDot === -1) return void 0;
995
- const family = lower.slice(0, lastDot);
996
- const targetSize = lower.slice(lastDot + 1);
997
- const targetIdx = _AwsBulkLoader.SIZE_ORDER.indexOf(targetSize);
998
- if (targetIdx === -1) return void 0;
999
- let bestKey;
1000
- let bestIdx = -1;
1001
- let bestDistance = Infinity;
1002
- for (const key of Object.keys(table)) {
1003
- const keyLastDot = key.lastIndexOf(".");
1004
- if (keyLastDot === -1) continue;
1005
- const keyFamily = key.slice(0, keyLastDot);
1006
- const keySize = key.slice(keyLastDot + 1);
1007
- if (keyFamily !== family) continue;
1008
- const keyIdx = _AwsBulkLoader.SIZE_ORDER.indexOf(keySize);
1009
- if (keyIdx === -1) continue;
1010
- const distance = Math.abs(keyIdx - targetIdx);
1011
- if (distance < bestDistance) {
1012
- bestDistance = distance;
1013
- bestIdx = keyIdx;
1014
- bestKey = key;
1015
- }
1016
- }
1017
- if (bestKey === void 0 || bestIdx === -1) return void 0;
1018
- const knownPrice = table[bestKey];
1019
- const steps = targetIdx - bestIdx;
1020
- return knownPrice * Math.pow(2, steps);
1021
- }
1022
- // -------------------------------------------------------------------------
1023
- // Internal helpers – fallback hardcoded prices
1024
- // -------------------------------------------------------------------------
1025
- fallbackComputePrice(instanceType, region, os, cacheKey) {
1026
- let basePrice = EC2_BASE_PRICES[instanceType.toLowerCase()];
1027
- if (basePrice === void 0) {
1028
- basePrice = this.interpolatePrice(instanceType, EC2_BASE_PRICES) ?? void 0;
1029
- }
1030
- if (basePrice === void 0) {
1031
- logger.warn("No fallback price found for EC2 instance type", { instanceType });
1032
- return null;
1033
- }
1034
- const multiplier = regionMultiplier(region);
1035
- const result = {
1036
- provider: "aws",
1037
- service: "ec2",
1038
- resource_type: instanceType,
1039
- region,
1040
- unit: "Hrs",
1041
- price_per_unit: basePrice * multiplier,
1042
- currency: "USD",
1043
- description: `AWS EC2 ${instanceType} (${os}) \u2013 fallback pricing`,
1044
- attributes: {
1045
- instance_type: instanceType,
1046
- operating_system: os,
1047
- tenancy: "Shared",
1048
- pricing_source: "fallback"
1049
- },
1050
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1051
- };
1052
- this.cache.set(cacheKey, result, "aws", "ec2", region, CACHE_TTL);
1053
- return result;
1054
- }
1055
- fallbackDatabasePrice(instanceClass, region, engine, cacheKey) {
1056
- let basePrice = RDS_BASE_PRICES[instanceClass.toLowerCase()];
1057
- if (basePrice === void 0) {
1058
- basePrice = this.interpolatePrice(instanceClass, RDS_BASE_PRICES) ?? void 0;
1059
- }
1060
- if (basePrice === void 0) {
1061
- logger.warn("No fallback price found for RDS instance class", { instanceClass });
1062
- return null;
1063
- }
1064
- const multiplier = regionMultiplier(region);
1065
- const result = {
1066
- provider: "aws",
1067
- service: "rds",
1068
- resource_type: instanceClass,
1069
- region,
1070
- unit: "Hrs",
1071
- price_per_unit: basePrice * multiplier,
1072
- currency: "USD",
1073
- description: `AWS RDS ${instanceClass} (${engine}) \u2013 fallback pricing`,
1074
- attributes: {
1075
- instance_type: instanceClass,
1076
- database_engine: engine,
1077
- deployment_option: "Single-AZ",
1078
- pricing_source: "fallback"
1079
- },
1080
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1081
- };
1082
- this.cache.set(cacheKey, result, "aws", "rds", region, CACHE_TTL);
1083
- return result;
1084
- }
1085
- fallbackStoragePrice(volumeType, region, cacheKey) {
1086
- const basePrice = EBS_BASE_PRICES[volumeType.toLowerCase()];
1087
- if (basePrice === void 0) {
1088
- logger.warn("No fallback price found for EBS volume type", { volumeType });
1089
- return null;
1090
- }
1091
- const multiplier = regionMultiplier(region);
1092
- const result = {
1093
- provider: "aws",
1094
- service: "ebs",
1095
- resource_type: volumeType,
1096
- region,
1097
- unit: "GB-Mo",
1098
- price_per_unit: basePrice * multiplier,
1099
- currency: "USD",
1100
- description: `AWS EBS ${volumeType} volume \u2013 fallback pricing`,
1101
- attributes: {
1102
- volume_type: volumeType,
1103
- pricing_source: "fallback"
1104
- },
1105
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1106
- };
1107
- this.cache.set(cacheKey, result, "aws", "ebs", region, CACHE_TTL);
1108
- return result;
1109
- }
1110
- };
1111
-
1112
- // src/pricing/azure/azure-normalizer.ts
1113
- function normalizeAzureCompute(item) {
1114
- return {
1115
- provider: "azure",
1116
- service: "virtual-machines",
1117
- resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1118
- region: item.armRegionName ?? item.location ?? "",
1119
- unit: item.unitOfMeasure ?? "1 Hour",
1120
- price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1121
- currency: item.currencyCode ?? "USD",
1122
- tier: item.tierMinimumUnits !== void 0 ? String(item.tierMinimumUnits) : void 0,
1123
- description: item.productName ?? void 0,
1124
- attributes: {
1125
- sku_name: item.skuName ?? "",
1126
- arm_sku_name: item.armSkuName ?? "",
1127
- service_name: item.serviceName ?? "Virtual Machines",
1128
- product_name: item.productName ?? "",
1129
- meter_name: item.meterName ?? "",
1130
- pricing_source: "azure-retail-api"
1131
- },
1132
- effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1133
- };
1134
- }
1135
- function normalizeAzureDatabase(item) {
1136
- return {
1137
- provider: "azure",
1138
- service: "azure-database",
1139
- resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1140
- region: item.armRegionName ?? item.location ?? "",
1141
- unit: item.unitOfMeasure ?? "1 Hour",
1142
- price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1143
- currency: item.currencyCode ?? "USD",
1144
- description: item.productName ?? void 0,
1145
- attributes: {
1146
- sku_name: item.skuName ?? "",
1147
- service_name: item.serviceName ?? "",
1148
- product_name: item.productName ?? "",
1149
- meter_name: item.meterName ?? "",
1150
- pricing_source: "azure-retail-api"
1151
- },
1152
- effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1153
- };
1154
- }
1155
- function normalizeAzureStorage(item) {
1156
- return {
1157
- provider: "azure",
1158
- service: "managed-disks",
1159
- resource_type: item.skuName ?? item.armSkuName ?? "unknown",
1160
- region: item.armRegionName ?? item.location ?? "",
1161
- unit: item.unitOfMeasure ?? "1 GiB/Month",
1162
- price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
1163
- currency: item.currencyCode ?? "USD",
1164
- description: item.productName ?? void 0,
1165
- attributes: {
1166
- sku_name: item.skuName ?? "",
1167
- service_name: item.serviceName ?? "",
1168
- product_name: item.productName ?? "",
1169
- meter_name: item.meterName ?? "",
1170
- pricing_source: "azure-retail-api"
1171
- },
1172
- effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
1173
- };
1174
- }
1175
-
1176
- // src/pricing/azure/retail-client.ts
1177
- var VM_BASE_PRICES = {
1178
- "standard_b1s": 0.0104,
1179
- "standard_b1ms": 0.0207,
1180
- "standard_b2s": 0.0416,
1181
- "standard_b2ms": 0.0832,
1182
- "standard_b4ms": 0.166,
1183
- "standard_b8ms": 0.333,
1184
- "standard_b12ms": 0.499,
1185
- "standard_b16ms": 0.666,
1186
- "standard_d2s_v4": 0.096,
1187
- "standard_d4s_v4": 0.192,
1188
- "standard_d8s_v4": 0.384,
1189
- "standard_d2s_v5": 0.096,
1190
- "standard_d4s_v5": 0.192,
1191
- "standard_d8s_v5": 0.384,
1192
- "standard_d16s_v5": 0.768,
1193
- "standard_d32s_v5": 1.536,
1194
- "standard_d2ds_v5": 0.113,
1195
- "standard_d4ds_v5": 0.226,
1196
- "standard_d8ds_v5": 0.452,
1197
- "standard_d2ps_v5": 0.077,
1198
- "standard_d4ps_v5": 0.154,
1199
- "standard_e2s_v5": 0.126,
1200
- "standard_e4s_v5": 0.252,
1201
- "standard_e8s_v5": 0.504,
1202
- "standard_e16s_v5": 1.008,
1203
- "standard_e32s_v5": 2.016,
1204
- "standard_e2ds_v5": 0.144,
1205
- "standard_e4ds_v5": 0.288,
1206
- "standard_f2s_v2": 0.085,
1207
- "standard_f4s_v2": 0.17,
1208
- "standard_f8s_v2": 0.34,
1209
- "standard_f16s_v2": 0.68,
1210
- "standard_f32s_v2": 1.36,
1211
- "standard_l8s_v3": 0.624,
1212
- "standard_l16s_v3": 1.248,
1213
- "standard_nc4as_t4_v3": 0.526,
1214
- "standard_nc8as_t4_v3": 0.752,
1215
- "standard_e2ps_v5": 0.101,
1216
- "standard_e4ps_v5": 0.202,
1217
- "standard_e8ps_v5": 0.403
1218
- };
1219
- var DISK_BASE_PRICES = {
1220
- "premium_lrs": 0.132,
1221
- "standard_lrs": 0.04,
1222
- "standardssd_lrs": 0.075,
1223
- "ultrassd_lrs": 0.12,
1224
- "premium_lrs_p10": 52e-4,
1225
- "premium_lrs_p20": 0.01,
1226
- "premium_lrs_p30": 0.0192,
1227
- "premium_lrs_p40": 0.0373
1228
- };
1229
- var DB_BASE_PRICES = {
1230
- "burstable_standard_b1ms": 0.044,
1231
- "burstable_standard_b2s": 0.088,
1232
- "burstable_standard_b4ms": 0.176,
1233
- "gp_standard_d2s_v3": 0.124,
1234
- "gp_standard_d4s_v3": 0.248,
1235
- "gp_standard_d8s_v3": 0.496,
1236
- "gp_standard_d16s_v3": 0.992,
1237
- "gp_standard_d2ds_v4": 0.124,
1238
- "gp_standard_d4ds_v4": 0.248,
1239
- "gp_standard_d8ds_v4": 0.496,
1240
- "gp_standard_d16ds_v4": 0.992,
1241
- "mo_standard_e2ds_v4": 0.17,
1242
- "mo_standard_e4ds_v4": 0.341,
1243
- "mo_standard_e8ds_v4": 0.682
1244
- };
1245
- var ALB_HOURLY2 = 0.025;
1246
- var ALB_RULE_HOURLY = 5e-3;
1247
- var NAT_HOURLY2 = 0.045;
1248
- var NAT_PER_GB2 = 0.045;
1249
- var AKS_HOURLY = 0.1;
1250
- var REGION_MULTIPLIERS2 = {
1251
- eastus: 1,
1252
- eastus2: 1,
1253
- westus: 1.05,
1254
- westus2: 1,
1255
- westus3: 1.02,
1256
- centralus: 1,
1257
- northeurope: 1.08,
1258
- westeurope: 1.08,
1259
- uksouth: 1.08,
1260
- ukwest: 1.12,
1261
- southeastasia: 1.12,
1262
- eastasia: 1.12,
1263
- japaneast: 1.1,
1264
- japanwest: 1.12,
1265
- australiaeast: 1.15,
1266
- brazilsouth: 1.25,
1267
- canadacentral: 1.05,
1268
- southafricanorth: 1.15,
1269
- germanywestcentral: 1.1,
1270
- centralindia: 1.08,
1271
- koreacentral: 1.1,
1272
- swedencentral: 1.08,
1273
- italynorth: 1.1,
1274
- uaenorth: 1.12
1275
- };
1276
- function regionMultiplier2(region) {
1277
- return REGION_MULTIPLIERS2[region.toLowerCase()] ?? 1;
1278
- }
1279
- var CACHE_TTL2 = 86400;
1280
- var RETAIL_API_BASE = "https://prices.azure.com/api/retail/prices";
1281
- var AzureRetailClient = class {
1282
- cache;
1283
- constructor(cache) {
1284
- this.cache = cache;
1285
- }
1286
- // -------------------------------------------------------------------------
1287
- // Public API
1288
- // -------------------------------------------------------------------------
1289
- async getComputePrice(vmSize, region, os = "linux") {
1290
- const cacheKey = this.buildCacheKey("vm", region, vmSize, os);
1291
- const cached = this.cache.get(cacheKey);
1292
- if (cached) return cached;
1293
- try {
1294
- const armRegion = region.toLowerCase().replace(/\s+/g, "");
1295
- const filter = this.buildODataFilter("Virtual Machines", armRegion, vmSize, true);
1296
- const items = await this.queryPricing(filter);
1297
- const match = this.pickVmItem(items, vmSize, os);
1298
- if (match) {
1299
- const result = normalizeAzureCompute(match);
1300
- this.cache.set(cacheKey, result, "azure", "vm", region, CACHE_TTL2);
1301
- return result;
1302
- }
1303
- } catch (err) {
1304
- logger.warn("Azure Retail API VM fetch failed, using fallback", {
1305
- region,
1306
- vmSize,
1307
- err: err instanceof Error ? err.message : String(err)
1308
- });
1309
- }
1310
- return this.fallbackComputePrice(vmSize, region, os, cacheKey);
1311
- }
1312
- async getDatabasePrice(tier, region, engine = "PostgreSQL") {
1313
- const cacheKey = this.buildCacheKey("db", region, tier, engine);
1314
- const cached = this.cache.get(cacheKey);
1315
- if (cached) return cached;
1316
- try {
1317
- const armRegion = region.toLowerCase().replace(/\s+/g, "");
1318
- const serviceName = `Azure Database for ${engine}`;
1319
- const result = await this.queryDatabasePrice(tier, armRegion, serviceName);
1320
- if (result) {
1321
- this.cache.set(cacheKey, result, "azure", "db", region, CACHE_TTL2);
1322
- return result;
1323
- }
1324
- logger.warn("Azure Retail API returned no matching DB pricing", {
1325
- region,
1326
- tier,
1327
- engine
1328
- });
1329
- } catch (err) {
1330
- logger.warn("Azure Retail API DB fetch failed, using fallback", {
1331
- region,
1332
- tier,
1333
- err: err instanceof Error ? err.message : String(err)
1334
- });
1335
- }
1336
- return this.fallbackDatabasePrice(tier, region, engine, cacheKey);
1337
- }
1338
- /**
1339
- * Azure DB flexible server tier names follow the pattern:
1340
- * GP_Standard_D2s_v3 → General Purpose, D-series, 2 vCPUs, v3
1341
- * MO_Standard_E4ds_v5 → Memory Optimized, E-series, 4 vCPUs, v5
1342
- *
1343
- * The Retail API prices DB compute in two ways:
1344
- * 1. Per-instance entries with armSkuName like "Standard_D2ds_v5" (total price)
1345
- * 2. Per-vCore entries for a series (e.g., Dsv3Series_Compute) × vCPU count
1346
- *
1347
- * This method tries approach 1 first, then falls back to approach 2.
1348
- */
1349
- async queryDatabasePrice(tier, armRegion, serviceName) {
1350
- const vmName = tier.replace(/^(GP|MO|BC|GEN)_/i, "");
1351
- const exactFilter = `serviceName eq '${serviceName}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption' and armSkuName eq '${vmName}'`;
1352
- const exactItems = await this.queryPricing(exactFilter);
1353
- if (exactItems.length > 0) {
1354
- exactItems.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
1355
- const match = exactItems.find(
1356
- (i) => (i.meterName ?? "").toLowerCase().includes("vcore")
1357
- ) ?? exactItems[0];
1358
- return normalizeAzureDatabase(match);
1359
- }
1360
- const parsed = this.parseVmSeries(vmName);
1361
- if (!parsed) return null;
1362
- const tierPrefix = tier.match(/^(GP|MO|BC|GEN)_/i)?.[1]?.toUpperCase();
1363
- const seriesFamily = tierPrefix === "MO" ? "Memory_Optimized" : "General_Purpose";
1364
- const seriesFilter = `serviceName eq '${serviceName}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption' and contains(armSkuName, '${seriesFamily}') and contains(armSkuName, '${parsed.series}')`;
1365
- const seriesItems = await this.queryPricing(seriesFilter);
1366
- if (seriesItems.length > 0) {
1367
- seriesItems.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
1368
- const perVcore = seriesItems[0].retailPrice ?? 0;
1369
- const totalPrice = perVcore * parsed.vcpus;
1370
- const adjusted = { ...seriesItems[0], retailPrice: totalPrice };
1371
- return normalizeAzureDatabase(adjusted);
1372
- }
1373
- return null;
1374
- }
1375
- /**
1376
- * Parse a VM name like "Standard_D2s_v3" into series info.
1377
- * Returns { series: "Dsv3", vcpus: 2 } or null if unparseable.
1378
- */
1379
- parseVmSeries(vmName) {
1380
- const match = vmName.match(
1381
- /Standard_([A-Z])(\d+)([a-z]*)_v(\d+)/i
1382
- );
1383
- if (!match) return null;
1384
- const [, family, vcpuStr, variant, version] = match;
1385
- const vcpus = parseInt(vcpuStr, 10);
1386
- if (isNaN(vcpus) || vcpus === 0) return null;
1387
- const series = `${family}${variant}v${version}`;
1388
- return { series, vcpus };
1389
- }
1390
- async getStoragePrice(diskType, region) {
1391
- const cacheKey = this.buildCacheKey("disk", region, diskType);
1392
- const cached = this.cache.get(cacheKey);
1393
- if (cached) return cached;
1394
- try {
1395
- const armRegion = region.toLowerCase().replace(/\s+/g, "");
1396
- const filter = this.buildODataFilter("Storage", armRegion, diskType);
1397
- const items = await this.queryPricing(filter);
1398
- if (items.length > 0) {
1399
- items.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
1400
- const result = normalizeAzureStorage(items[0]);
1401
- this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
1402
- return result;
1403
- }
1404
- logger.warn("Azure Retail API returned no matching storage pricing", {
1405
- region,
1406
- diskType
1407
- });
1408
- } catch (err) {
1409
- logger.warn("Azure Retail API storage fetch failed, using fallback", {
1410
- region,
1411
- diskType,
1412
- err: err instanceof Error ? err.message : String(err)
1413
- });
1414
- }
1415
- return this.fallbackStoragePrice(diskType, region, cacheKey);
1416
- }
1417
- async getLoadBalancerPrice(region) {
1418
- const multiplier = regionMultiplier2(region);
1419
- return {
1420
- provider: "azure",
1421
- service: "load-balancer",
1422
- resource_type: "standard",
1423
- region,
1424
- unit: "1 Hour",
1425
- price_per_unit: ALB_HOURLY2 * multiplier,
1426
- currency: "USD",
1427
- description: "Azure Load Balancer Standard (hourly + per rule)",
1428
- attributes: {
1429
- rule_hourly_price: String(ALB_RULE_HOURLY * multiplier),
1430
- pricing_model: "fixed_plus_rules"
1431
- },
1432
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1433
- };
1434
- }
1435
- async getNatGatewayPrice(region) {
1436
- const multiplier = regionMultiplier2(region);
1437
- return {
1438
- provider: "azure",
1439
- service: "nat-gateway",
1440
- resource_type: "nat-gateway",
1441
- region,
1442
- unit: "1 Hour",
1443
- price_per_unit: NAT_HOURLY2 * multiplier,
1444
- currency: "USD",
1445
- description: "Azure NAT Gateway (hourly + data processed)",
1446
- attributes: {
1447
- per_gb_price: String(NAT_PER_GB2 * multiplier)
1448
- },
1449
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1450
- };
1451
- }
1452
- async getKubernetesPrice(region) {
1453
- const multiplier = regionMultiplier2(region);
1454
- return {
1455
- provider: "azure",
1456
- service: "aks",
1457
- resource_type: "cluster",
1458
- region,
1459
- unit: "1 Hour",
1460
- price_per_unit: AKS_HOURLY * multiplier,
1461
- currency: "USD",
1462
- description: "Azure Kubernetes Service (uptime SLA tier, per cluster/hour)",
1463
- attributes: {},
1464
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1465
- };
1466
- }
1467
- // -------------------------------------------------------------------------
1468
- // Internal helpers – live API
1469
- // -------------------------------------------------------------------------
1470
- async queryPricing(filter) {
1471
- const allItems = [];
1472
- let url = `${RETAIL_API_BASE}?api-version=2023-01-01-preview&$filter=${encodeURIComponent(filter)}`;
1473
- logger.debug("Querying Azure Retail Prices API", { filter });
1474
- let pages = 0;
1475
- while (url && pages < 10) {
1476
- const res = await fetch(url, {
1477
- signal: AbortSignal.timeout(3e4)
1478
- });
1479
- if (!res.ok) {
1480
- throw new Error(`HTTP ${res.status} from Azure Retail API`);
1481
- }
1482
- const json = await res.json();
1483
- allItems.push(...json?.Items ?? []);
1484
- url = json?.NextPageLink ?? null;
1485
- pages++;
1486
- }
1487
- return allItems;
1488
- }
1489
- buildODataFilter(service, armRegion, skuName, exactArmSku) {
1490
- let filter = `serviceName eq '${service}' and armRegionName eq '${armRegion}' and priceType eq 'Consumption'`;
1491
- if (skuName) {
1492
- if (exactArmSku) {
1493
- filter += ` and armSkuName eq '${skuName}'`;
1494
- } else {
1495
- filter += ` and contains(skuName, '${skuName}')`;
1496
- }
1497
- }
1498
- return filter;
1499
- }
1500
- buildCacheKey(service, region, ...parts) {
1501
- return ["azure", service, region, ...parts].map((p) => p.toLowerCase()).join("/");
1502
- }
1503
- /**
1504
- * From the API response items, pick the item that best matches the desired
1505
- * VM size and OS. Prefers exact skuName matches and the correct OS suffix
1506
- * ("Windows" vs everything else being Linux).
1507
- */
1508
- pickVmItem(items, vmSize, os) {
1509
- if (items.length === 0) return null;
1510
- const sorted = [...items].sort(
1511
- (a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? "")
1512
- );
1513
- const vmLower = vmSize.toLowerCase();
1514
- const isWindows = os.toLowerCase().includes("windows");
1515
- for (const item of sorted) {
1516
- const skuLower = (item.skuName ?? "").toLowerCase();
1517
- const hasWindows = skuLower.includes("windows");
1518
- if (skuLower.includes(vmLower) && (isWindows && hasWindows || !isWindows && !hasWindows)) {
1519
- return item;
1520
- }
1521
- }
1522
- return sorted[0];
1523
- }
1524
- // -------------------------------------------------------------------------
1525
- // Internal helpers – size interpolation
1526
- // -------------------------------------------------------------------------
1527
- /**
1528
- * Azure VM sizes embed vCPU count as a number: Standard_D2s_v5 → 2 vCPUs.
1529
- * Doubling the number roughly doubles the price. Find the nearest known
1530
- * size in the same family/suffix and scale proportionally.
1531
- */
1532
- interpolateVmPrice(vmSize, table) {
1533
- const key = vmSize.toLowerCase().replace(/\s+/g, "_");
1534
- const match = key.match(/^(.+?)(\d+)(.*?)$/);
1535
- if (!match) return void 0;
1536
- const [, prefix, numStr, suffix] = match;
1537
- const targetNum = parseInt(numStr, 10);
1538
- if (isNaN(targetNum) || targetNum === 0) return void 0;
1539
- let bestKey;
1540
- let bestNum = 0;
1541
- let bestDistance = Infinity;
1542
- for (const candidate of Object.keys(table)) {
1543
- const candMatch = candidate.match(/^(.+?)(\d+)(.*?)$/);
1544
- if (!candMatch) continue;
1545
- const [, cPrefix, cNumStr, cSuffix] = candMatch;
1546
- if (cPrefix !== prefix || cSuffix !== suffix) continue;
1547
- const cNum = parseInt(cNumStr, 10);
1548
- if (isNaN(cNum) || cNum === 0) continue;
1549
- const distance = Math.abs(cNum - targetNum);
1550
- if (distance < bestDistance) {
1551
- bestDistance = distance;
1552
- bestNum = cNum;
1553
- bestKey = candidate;
1554
- }
1555
- }
1556
- if (bestKey === void 0 || bestNum === 0) return void 0;
1557
- return table[bestKey] * (targetNum / bestNum);
1558
- }
1559
- /**
1560
- * Azure DB tier names embed a vCPU number similarly to VM sizes.
1561
- * e.g. "gp_standard_d2s_v3" → "gp_standard_d4s_v3" doubles.
1562
- */
1563
- interpolateDbPrice(tier, table) {
1564
- const key = tier.toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_");
1565
- const match = key.match(/^(.+?)(\d+)(.*?)$/);
1566
- if (!match) return void 0;
1567
- const [, prefix, numStr, suffix] = match;
1568
- const targetNum = parseInt(numStr, 10);
1569
- if (isNaN(targetNum) || targetNum === 0) return void 0;
1570
- let bestKey;
1571
- let bestNum = 0;
1572
- let bestDistance = Infinity;
1573
- for (const candidate of Object.keys(table)) {
1574
- const candMatch = candidate.match(/^(.+?)(\d+)(.*?)$/);
1575
- if (!candMatch) continue;
1576
- const [, cPrefix, cNumStr, cSuffix] = candMatch;
1577
- if (cPrefix !== prefix || cSuffix !== suffix) continue;
1578
- const cNum = parseInt(cNumStr, 10);
1579
- if (isNaN(cNum) || cNum === 0) continue;
1580
- const distance = Math.abs(cNum - targetNum);
1581
- if (distance < bestDistance) {
1582
- bestDistance = distance;
1583
- bestNum = cNum;
1584
- bestKey = candidate;
1585
- }
1586
- }
1587
- if (bestKey === void 0 || bestNum === 0) return void 0;
1588
- return table[bestKey] * (targetNum / bestNum);
1589
- }
1590
- // -------------------------------------------------------------------------
1591
- // Internal helpers – fallback hardcoded prices
1592
- // -------------------------------------------------------------------------
1593
- fallbackComputePrice(vmSize, region, os, cacheKey) {
1594
- const key = vmSize.toLowerCase().replace(/\s+/g, "_");
1595
- let basePrice = VM_BASE_PRICES[key];
1596
- if (basePrice === void 0) {
1597
- basePrice = this.interpolateVmPrice(vmSize, VM_BASE_PRICES) ?? void 0;
1598
- }
1599
- if (basePrice === void 0) {
1600
- logger.warn("No fallback price found for Azure VM size", { vmSize });
1601
- return null;
1602
- }
1603
- const multiplier = regionMultiplier2(region);
1604
- const result = {
1605
- provider: "azure",
1606
- service: "virtual-machines",
1607
- resource_type: vmSize,
1608
- region,
1609
- unit: "1 Hour",
1610
- price_per_unit: basePrice * multiplier,
1611
- currency: "USD",
1612
- description: `Azure VM ${vmSize} (${os}) \u2013 fallback pricing`,
1613
- attributes: {
1614
- sku_name: vmSize,
1615
- operating_system: os,
1616
- pricing_source: "fallback"
1617
- },
1618
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1619
- };
1620
- this.cache.set(cacheKey, result, "azure", "vm", region, CACHE_TTL2);
1621
- return result;
1622
- }
1623
- fallbackDatabasePrice(tier, region, engine, cacheKey) {
1624
- const key = tier.toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_");
1625
- let basePrice = DB_BASE_PRICES[key];
1626
- if (basePrice === void 0) {
1627
- basePrice = this.interpolateDbPrice(tier, DB_BASE_PRICES) ?? void 0;
1628
- }
1629
- if (basePrice === void 0) {
1630
- logger.warn("No fallback price found for Azure DB tier", { tier });
1631
- return null;
1632
- }
1633
- const multiplier = regionMultiplier2(region);
1634
- const result = {
1635
- provider: "azure",
1636
- service: "azure-database",
1637
- resource_type: tier,
1638
- region,
1639
- unit: "1 Hour",
1640
- price_per_unit: basePrice * multiplier,
1641
- currency: "USD",
1642
- description: `Azure Database for ${engine} ${tier} \u2013 fallback pricing`,
1643
- attributes: {
1644
- tier,
1645
- engine,
1646
- pricing_source: "fallback"
1647
- },
1648
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1649
- };
1650
- this.cache.set(cacheKey, result, "azure", "db", region, CACHE_TTL2);
1651
- return result;
1652
- }
1653
- fallbackStoragePrice(diskType, region, cacheKey) {
1654
- const key = diskType.toLowerCase().replace(/\s+/g, "_").replace(/-/g, "_");
1655
- const basePrice = DISK_BASE_PRICES[key];
1656
- if (basePrice === void 0) {
1657
- logger.warn("No fallback price found for Azure disk type", { diskType });
1658
- return null;
1659
- }
1660
- const multiplier = regionMultiplier2(region);
1661
- const result = {
1662
- provider: "azure",
1663
- service: "managed-disks",
1664
- resource_type: diskType,
1665
- region,
1666
- unit: "1 GiB/Month",
1667
- price_per_unit: basePrice * multiplier,
1668
- currency: "USD",
1669
- description: `Azure Managed Disk ${diskType} \u2013 fallback pricing`,
1670
- attributes: {
1671
- disk_type: diskType,
1672
- pricing_source: "fallback"
1673
- },
1674
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1675
- };
1676
- this.cache.set(cacheKey, result, "azure", "storage", region, CACHE_TTL2);
1677
- return result;
1678
- }
1679
- };
1680
-
1681
- // src/pricing/gcp/gcp-normalizer.ts
1682
- function normalizeGcpCompute(machineType, hourlyPrice, region) {
1683
- return {
1684
- provider: "gcp",
1685
- service: "compute-engine",
1686
- resource_type: machineType,
1687
- region,
1688
- unit: "h",
1689
- price_per_unit: hourlyPrice,
1690
- currency: "USD",
1691
- description: `GCP Compute Engine ${machineType}`,
1692
- attributes: {
1693
- machine_type: machineType,
1694
- pricing_source: "bundled"
1695
- },
1696
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1697
- };
1698
- }
1699
- function normalizeGcpDatabase(tier, hourlyPrice, region) {
1700
- return {
1701
- provider: "gcp",
1702
- service: "cloud-sql",
1703
- resource_type: tier,
1704
- region,
1705
- unit: "h",
1706
- price_per_unit: hourlyPrice,
1707
- currency: "USD",
1708
- description: `GCP Cloud SQL ${tier}`,
1709
- attributes: {
1710
- tier,
1711
- pricing_source: "bundled"
1712
- },
1713
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1714
- };
1715
- }
1716
- function normalizeGcpStorage(storageClass, pricePerGb, region) {
1717
- return {
1718
- provider: "gcp",
1719
- service: "cloud-storage",
1720
- resource_type: storageClass,
1721
- region,
1722
- unit: "GiBy.mo",
1723
- price_per_unit: pricePerGb,
1724
- currency: "USD",
1725
- description: `GCP Cloud Storage ${storageClass}`,
1726
- attributes: {
1727
- storage_class: storageClass,
1728
- pricing_source: "bundled"
1729
- },
1730
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1731
- };
1732
- }
1733
- function normalizeGcpDisk(diskType, pricePerGb, region) {
1734
- return {
1735
- provider: "gcp",
1736
- service: "persistent-disk",
1737
- resource_type: diskType,
1738
- region,
1739
- unit: "GiBy.mo",
1740
- price_per_unit: pricePerGb,
1741
- currency: "USD",
1742
- description: `GCP Persistent Disk ${diskType}`,
1743
- attributes: {
1744
- disk_type: diskType,
1745
- pricing_source: "bundled"
1746
- },
1747
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1748
- };
1749
- }
1750
-
1751
- // src/pricing/gcp/bundled-loader.ts
1752
- var LB_FORWARDING_RULE_HOURLY = 0.025;
1753
- var LB_PER_GB = 8e-3;
1754
- var NAT_HOURLY3 = 0.044;
1755
- var NAT_PER_GB3 = 0.045;
1756
- var GKE_STANDARD_HOURLY = 0.1;
1757
- var GKE_AUTOPILOT_VCPU_HOURLY = 0.0445;
1758
- var GcpBundledLoader = class {
1759
- // -------------------------------------------------------------------------
1760
- // Public API
1761
- // -------------------------------------------------------------------------
1762
- async getComputePrice(machineType, region) {
1763
- try {
1764
- const pricing = getGcpComputePricing();
1765
- const regionPrices = pricing[region];
1766
- if (!regionPrices) {
1767
- logger.debug("GCP compute: region not found in bundled data", {
1768
- region,
1769
- machineType
1770
- });
1771
- return null;
1772
- }
1773
- const price = regionPrices[machineType];
1774
- if (price === void 0) {
1775
- logger.debug("GCP compute: machine type not found in bundled data", {
1776
- region,
1777
- machineType
1778
- });
1779
- return null;
1780
- }
1781
- return normalizeGcpCompute(machineType, price, region);
1782
- } catch (err) {
1783
- logger.error("GCP compute pricing load error", {
1784
- machineType,
1785
- region,
1786
- err: err instanceof Error ? err.message : String(err)
1787
- });
1788
- return null;
1789
- }
1790
- }
1791
- async getDatabasePrice(tier, region) {
1792
- try {
1793
- const pricing = getGcpSqlPricing();
1794
- const regionPrices = pricing[region];
1795
- if (!regionPrices) {
1796
- logger.debug("GCP SQL: region not found in bundled data", {
1797
- region,
1798
- tier
1799
- });
1800
- return null;
1801
- }
1802
- const price = regionPrices[tier];
1803
- if (price === void 0 || tier === "storage_per_gb" || tier === "ha_multiplier") {
1804
- logger.debug("GCP SQL: tier not found in bundled data", {
1805
- region,
1806
- tier
1807
- });
1808
- return null;
1809
- }
1810
- return normalizeGcpDatabase(tier, price, region);
1811
- } catch (err) {
1812
- logger.error("GCP SQL pricing load error", {
1813
- tier,
1814
- region,
1815
- err: err instanceof Error ? err.message : String(err)
1816
- });
1817
- return null;
1818
- }
1819
- }
1820
- async getStoragePrice(storageClass, region) {
1821
- try {
1822
- const pricing = getGcpStoragePricing();
1823
- const regionPrices = pricing[region];
1824
- if (!regionPrices) {
1825
- logger.debug("GCP Storage: region not found in bundled data", {
1826
- region,
1827
- storageClass
1828
- });
1829
- return null;
1830
- }
1831
- const classKey = storageClass.toUpperCase();
1832
- const price = regionPrices[classKey];
1833
- if (price === void 0) {
1834
- logger.debug("GCP Storage: storage class not found in bundled data", {
1835
- region,
1836
- storageClass
1837
- });
1838
- return null;
1839
- }
1840
- return normalizeGcpStorage(storageClass.toUpperCase(), price, region);
1841
- } catch (err) {
1842
- logger.error("GCP Storage pricing load error", {
1843
- storageClass,
1844
- region,
1845
- err: err instanceof Error ? err.message : String(err)
1846
- });
1847
- return null;
1848
- }
1849
- }
1850
- async getDiskPrice(diskType, region) {
1851
- try {
1852
- const pricing = getGcpDiskPricing();
1853
- const regionPrices = pricing[region];
1854
- if (!regionPrices) {
1855
- logger.debug("GCP Disk: region not found in bundled data", {
1856
- region,
1857
- diskType
1858
- });
1859
- return null;
1860
- }
1861
- const diskKey = diskType;
1862
- const price = regionPrices[diskKey];
1863
- if (price === void 0) {
1864
- logger.debug("GCP Disk: disk type not found in bundled data", {
1865
- region,
1866
- diskType
1867
- });
1868
- return null;
1869
- }
1870
- return normalizeGcpDisk(diskType, price, region);
1871
- } catch (err) {
1872
- logger.error("GCP Disk pricing load error", {
1873
- diskType,
1874
- region,
1875
- err: err instanceof Error ? err.message : String(err)
1876
- });
1877
- return null;
1878
- }
1879
- }
1880
- async getLoadBalancerPrice(region) {
1881
- return {
1882
- provider: "gcp",
1883
- service: "cloud-load-balancing",
1884
- resource_type: "forwarding-rule",
1885
- region,
1886
- unit: "h",
1887
- price_per_unit: LB_FORWARDING_RULE_HOURLY,
1888
- currency: "USD",
1889
- description: "GCP Cloud Load Balancing forwarding rule (per hour + data processed)",
1890
- attributes: {
1891
- per_gb_price: String(LB_PER_GB)
1892
- },
1893
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1894
- };
1895
- }
1896
- async getNatGatewayPrice(region) {
1897
- return {
1898
- provider: "gcp",
1899
- service: "cloud-nat",
1900
- resource_type: "nat-gateway",
1901
- region,
1902
- unit: "h",
1903
- price_per_unit: NAT_HOURLY3,
1904
- currency: "USD",
1905
- description: "GCP Cloud NAT (per gateway/hour + data processed)",
1906
- attributes: {
1907
- per_gb_price: String(NAT_PER_GB3)
1908
- },
1909
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1910
- };
1911
- }
1912
- async getKubernetesPrice(region, mode = "standard") {
1913
- const hourlyPrice = mode === "autopilot" ? GKE_AUTOPILOT_VCPU_HOURLY : GKE_STANDARD_HOURLY;
1914
- return {
1915
- provider: "gcp",
1916
- service: "gke",
1917
- resource_type: "cluster",
1918
- region,
1919
- unit: "h",
1920
- price_per_unit: hourlyPrice,
1921
- currency: "USD",
1922
- description: mode === "autopilot" ? "GCP GKE Autopilot cluster (estimated per-vCPU/hr; actual billing is per-pod resources)" : "GCP GKE Standard cluster (per cluster/hour)",
1923
- attributes: {
1924
- mode,
1925
- ...mode === "autopilot" && {
1926
- pricing_model: "per_vcpu_hour",
1927
- pricing_note: "Autopilot charges per pod vCPU and memory. This price is a per-vCPU/hr approximation for cluster-level cost estimation."
1928
- }
1929
- },
1930
- effective_date: (/* @__PURE__ */ new Date()).toISOString()
1931
- };
1932
- }
1933
- };
1934
-
1935
- // src/pricing/pricing-engine.ts
1936
- var AwsProvider = class {
1937
- loader;
1938
- constructor(cache) {
1939
- this.loader = new AwsBulkLoader(cache);
1940
- }
1941
- getComputePrice(instanceType, region, os) {
1942
- return this.loader.getComputePrice(instanceType, region, os);
1943
- }
1944
- getDatabasePrice(instanceType, region, engine) {
1945
- return this.loader.getDatabasePrice(instanceType, region, engine);
1946
- }
1947
- getStoragePrice(storageType, region, _sizeGb) {
1948
- return this.loader.getStoragePrice(storageType, region);
1949
- }
1950
- getLoadBalancerPrice(_type, region) {
1951
- return this.loader.getLoadBalancerPrice(region);
1952
- }
1953
- getNatGatewayPrice(region) {
1954
- return this.loader.getNatGatewayPrice(region);
1955
- }
1956
- getKubernetesPrice(region) {
1957
- return this.loader.getKubernetesPrice(region);
1958
- }
1959
- };
1960
- var AzureProvider = class {
1961
- client;
1962
- constructor(cache) {
1963
- this.client = new AzureRetailClient(cache);
1964
- }
1965
- getComputePrice(instanceType, region, os) {
1966
- return this.client.getComputePrice(instanceType, region, os);
1967
- }
1968
- getDatabasePrice(instanceType, region, engine) {
1969
- return this.client.getDatabasePrice(instanceType, region, engine);
1970
- }
1971
- getStoragePrice(storageType, region, _sizeGb) {
1972
- return this.client.getStoragePrice(storageType, region);
1973
- }
1974
- getLoadBalancerPrice(_type, region) {
1975
- return this.client.getLoadBalancerPrice(region);
1976
- }
1977
- getNatGatewayPrice(region) {
1978
- return this.client.getNatGatewayPrice(region);
1979
- }
1980
- getKubernetesPrice(region) {
1981
- return this.client.getKubernetesPrice(region);
1982
- }
1983
- };
1984
- var GcpProvider = class {
1985
- loader;
1986
- constructor() {
1987
- this.loader = new GcpBundledLoader();
1988
- }
1989
- getComputePrice(instanceType, region, _os) {
1990
- return this.loader.getComputePrice(instanceType, region);
1991
- }
1992
- getDatabasePrice(instanceType, region, _engine) {
1993
- return this.loader.getDatabasePrice(instanceType, region);
1994
- }
1995
- getStoragePrice(storageType, region, _sizeGb) {
1996
- if (storageType.startsWith("pd-")) {
1997
- return this.loader.getDiskPrice(storageType, region);
1998
- }
1999
- return this.loader.getStoragePrice(storageType, region);
2000
- }
2001
- getLoadBalancerPrice(_type, region) {
2002
- return this.loader.getLoadBalancerPrice(region);
2003
- }
2004
- getNatGatewayPrice(region) {
2005
- return this.loader.getNatGatewayPrice(region);
2006
- }
2007
- getKubernetesPrice(region) {
2008
- return this.loader.getKubernetesPrice(region);
2009
- }
2010
- };
2011
- var PricingEngine = class {
2012
- providers = /* @__PURE__ */ new Map();
2013
- constructor(cache, _config) {
2014
- this.providers.set("aws", new AwsProvider(cache));
2015
- this.providers.set("azure", new AzureProvider(cache));
2016
- this.providers.set("gcp", new GcpProvider());
2017
- }
2018
- /**
2019
- * Return the PricingProvider for a specific cloud provider.
2020
- * Throws if the provider is not registered (should never happen for the
2021
- * three known providers).
2022
- */
2023
- getProvider(provider) {
2024
- const p = this.providers.get(provider);
2025
- if (!p) {
2026
- throw new Error(`Unknown cloud provider: ${provider}`);
2027
- }
2028
- return p;
2029
- }
2030
- /**
2031
- * Generic price lookup that maps service/resourceType strings to the
2032
- * appropriate method on the underlying PricingProvider.
2033
- *
2034
- * Supported service values (case-insensitive):
2035
- * compute, ec2, vm, instance → getComputePrice
2036
- * database, rds, sql, db → getDatabasePrice
2037
- * storage, ebs, disk, gcs → getStoragePrice
2038
- * lb, load-balancer, alb, nlb → getLoadBalancerPrice
2039
- * nat, nat-gateway → getNatGatewayPrice
2040
- * k8s, kubernetes, eks, aks, gke → getKubernetesPrice
2041
- */
2042
- async getPrice(provider, service, resourceType, region, attributes = {}) {
2043
- const p = this.getProvider(provider);
2044
- const svc = service.toLowerCase();
2045
- logger.debug("PricingEngine.getPrice", {
2046
- provider,
2047
- service,
2048
- resourceType,
2049
- region
2050
- });
2051
- if (svc === "compute" || svc === "ec2" || svc === "vm" || svc === "instance" || svc === "virtual-machines") {
2052
- return p.getComputePrice(resourceType, region, attributes.os);
2053
- }
2054
- if (svc === "database" || svc === "rds" || svc === "sql" || svc === "db" || svc === "cloud-sql" || svc === "azure-database") {
2055
- return p.getDatabasePrice(resourceType, region, attributes.engine);
2056
- }
2057
- if (svc === "storage" || svc === "ebs" || svc === "disk" || svc === "gcs" || svc === "managed-disks" || svc === "persistent-disk" || svc === "cloud-storage") {
2058
- const sizeGb = attributes.size_gb ? parseFloat(attributes.size_gb) : void 0;
2059
- return p.getStoragePrice(resourceType, region, sizeGb);
2060
- }
2061
- if (svc === "lb" || svc === "load-balancer" || svc === "alb" || svc === "nlb" || svc === "elb" || svc === "cloud-load-balancing") {
2062
- return p.getLoadBalancerPrice(resourceType, region);
2063
- }
2064
- if (svc === "nat" || svc === "nat-gateway" || svc === "cloud-nat") {
2065
- return p.getNatGatewayPrice(region);
2066
- }
2067
- if (svc === "k8s" || svc === "kubernetes" || svc === "eks" || svc === "aks" || svc === "gke") {
2068
- return p.getKubernetesPrice(region);
2069
- }
2070
- logger.warn("PricingEngine: unrecognised service", { service, provider });
2071
- return null;
2072
- }
2073
- };
2074
-
2075
- // src/tools/analyze-terraform.ts
2076
- import { z } from "zod";
2077
-
2078
- // src/parsers/hcl-parser.ts
2079
- import { parse } from "@cdktf/hcl2json";
2080
- async function parseHclToJson(hclContent, filename = "main.tf") {
2081
- if (!hclContent.trim()) {
2082
- logger.debug("Received empty HCL content, returning empty object", {
2083
- filename
2084
- });
2085
- return {};
2086
- }
2087
- try {
2088
- const result = await parse(filename, hclContent);
2089
- logger.debug("Parsed HCL successfully", { filename });
2090
- return result;
2091
- } catch (err) {
2092
- const message = err instanceof Error ? err.message : String(err);
2093
- logger.error("Failed to parse HCL", { filename, error: message });
2094
- throw new Error(
2095
- `HCL parse error in "${filename}": ${message}`
2096
- );
2097
- }
2098
- }
2099
-
2100
- // src/parsers/variable-resolver.ts
2101
- function resolveVariables(hclJson, tfvarsContent) {
2102
- const defaults = extractVariableDefaults(hclJson);
2103
- const overrides = tfvarsContent ? parseTfvars(tfvarsContent) : {};
2104
- const resolved = { ...defaults, ...overrides };
2105
- logger.debug("Resolved variables", {
2106
- defaultCount: Object.keys(defaults).length,
2107
- overrideCount: Object.keys(overrides).length
2108
- });
2109
- return resolved;
2110
- }
2111
- function extractVariableDefaults(hclJson) {
2112
- const defaults = {};
2113
- const variableBlock = hclJson["variable"];
2114
- if (!variableBlock || typeof variableBlock !== "object") {
2115
- return defaults;
2116
- }
2117
- for (const [varName, declarations] of Object.entries(
2118
- variableBlock
2119
- )) {
2120
- if (!Array.isArray(declarations) || declarations.length === 0) continue;
2121
- const declaration = declarations[0];
2122
- if ("default" in declaration) {
2123
- defaults[varName] = declaration["default"];
2124
- }
2125
- }
2126
- return defaults;
2127
- }
2128
- function parseTfvars(content) {
2129
- const result = {};
2130
- for (const rawLine of content.split("\n")) {
2131
- const line = rawLine.trim();
2132
- if (!line || line.startsWith("#")) continue;
2133
- const eqIndex = line.indexOf("=");
2134
- if (eqIndex === -1) continue;
2135
- const key = line.slice(0, eqIndex).trim();
2136
- const rawValue = line.slice(eqIndex + 1).trim();
2137
- if (!key) continue;
2138
- result[key] = parseTfvarsValue(rawValue);
2139
- }
2140
- return result;
2141
- }
2142
- function parseTfvarsValue(raw) {
2143
- if (raw.startsWith('"') && raw.endsWith('"') || raw.startsWith("'") && raw.endsWith("'")) {
2144
- return raw.slice(1, -1);
2145
- }
2146
- if (raw === "true") return true;
2147
- if (raw === "false") return false;
2148
- if (raw === "null") return null;
2149
- if (/^-?\d+$/.test(raw)) return parseInt(raw, 10);
2150
- if (/^-?\d+\.\d+$/.test(raw)) return parseFloat(raw);
2151
- return raw;
2152
- }
2153
- function substituteVariables(value, variables) {
2154
- if (typeof value === "string") {
2155
- return substituteInString(value, variables);
2156
- }
2157
- if (Array.isArray(value)) {
2158
- return value.map((item) => substituteVariables(item, variables));
2159
- }
2160
- if (value !== null && typeof value === "object") {
2161
- const result = {};
2162
- for (const [k, v] of Object.entries(value)) {
2163
- result[k] = substituteVariables(v, variables);
2164
- }
2165
- return result;
2166
- }
2167
- return value;
2168
- }
2169
- var VAR_ONLY_RE = /^\$\{var\.([^}]+)\}$/;
2170
- var VAR_INLINE_RE = /\$\{var\.([^}]+)\}/g;
2171
- function substituteInString(str2, variables) {
2172
- const wholeMatch = VAR_ONLY_RE.exec(str2);
2173
- if (wholeMatch) {
2174
- const varName = wholeMatch[1];
2175
- return varName in variables ? variables[varName] : str2;
2176
- }
2177
- return str2.replace(VAR_INLINE_RE, (_match, varName) => {
2178
- if (varName in variables) {
2179
- const v = variables[varName];
2180
- return v === null ? "" : String(v);
2181
- }
2182
- return _match;
2183
- });
2184
- }
2185
-
2186
- // src/parsers/provider-detector.ts
2187
- var PROVIDER_PREFIXES = [
2188
- ["aws_", "aws"],
2189
- ["azurerm_", "azure"],
2190
- ["google_", "gcp"]
2191
- ];
2192
- function detectProvider(resourceType) {
2193
- for (const [prefix, provider] of PROVIDER_PREFIXES) {
2194
- if (resourceType.startsWith(prefix)) {
2195
- return provider;
2196
- }
2197
- }
2198
- throw new Error(
2199
- `Cannot determine cloud provider for resource type "${resourceType}". Supported prefixes: aws_, azurerm_, google_`
2200
- );
2201
- }
2202
-
2203
- // src/parsers/resource-extractor.ts
2204
- function detectRegionFromProviders(hclJson, provider, variables) {
2205
- const defaults = {
2206
- aws: "us-east-1",
2207
- azure: "eastus",
2208
- gcp: "us-central1"
2209
- };
2210
- const providerBlock = hclJson["provider"];
2211
- if (!providerBlock || typeof providerBlock !== "object") {
2212
- return defaults[provider];
2213
- }
2214
- const providerKeyMap = {
2215
- aws: "aws",
2216
- azure: "azurerm",
2217
- gcp: "google"
2218
- };
2219
- const key = providerKeyMap[provider];
2220
- const declarations = providerBlock[key];
2221
- if (!Array.isArray(declarations) || declarations.length === 0) {
2222
- return defaults[provider];
2223
- }
2224
- const declaration = declarations[0];
2225
- const regionAttr = provider === "azure" ? "location" : "region";
2226
- const rawRegion = declaration[regionAttr];
2227
- if (!rawRegion) return defaults[provider];
2228
- const resolved = substituteVariables(rawRegion, variables);
2229
- if (typeof resolved === "string" && resolved && !resolved.startsWith("${")) {
2230
- return resolved;
2231
- }
2232
- return defaults[provider];
2233
- }
2234
- function firstBlock(block, key) {
2235
- const nested = block[key];
2236
- if (Array.isArray(nested) && nested.length > 0) {
2237
- return nested[0];
2238
- }
2239
- return {};
2240
- }
2241
- function str(v) {
2242
- return typeof v === "string" && v ? v : void 0;
2243
- }
2244
- function num(v) {
2245
- if (typeof v === "number") return v;
2246
- if (typeof v === "string") {
2247
- const n = Number(v);
2248
- return isNaN(n) ? void 0 : n;
2249
- }
2250
- return void 0;
2251
- }
2252
- function bool(v) {
2253
- if (typeof v === "boolean") return v;
2254
- if (v === "true") return true;
2255
- if (v === "false") return false;
2256
- return void 0;
2257
- }
2258
- var extractors = {
2259
- aws_instance(block) {
2260
- const root = firstBlock(block, "root_block_device");
2261
- const attrs = {};
2262
- if (str(block["instance_type"])) attrs.instance_type = str(block["instance_type"]);
2263
- if (str(block["ami"])) attrs.ami = str(block["ami"]);
2264
- if (str(block["availability_zone"])) attrs.availability_zone = str(block["availability_zone"]);
2265
- if (root["volume_type"]) attrs.storage_type = str(root["volume_type"]);
2266
- if (root["volume_size"] !== void 0) attrs.storage_size_gb = num(root["volume_size"]);
2267
- if (root["iops"] !== void 0) attrs.iops = num(root["iops"]);
2268
- if (root["throughput"] !== void 0) attrs.throughput_mbps = num(root["throughput"]);
2269
- return attrs;
2270
- },
2271
- aws_db_instance(block) {
2272
- const attrs = {};
2273
- if (str(block["instance_class"])) attrs.instance_type = str(block["instance_class"]);
2274
- if (str(block["engine"])) attrs.engine = str(block["engine"]);
2275
- if (str(block["engine_version"])) attrs.engine_version = str(block["engine_version"]);
2276
- if (str(block["storage_type"])) attrs.storage_type = str(block["storage_type"]);
2277
- if (block["allocated_storage"] !== void 0) attrs.storage_size_gb = num(block["allocated_storage"]);
2278
- if (block["iops"] !== void 0) attrs.iops = num(block["iops"]);
2279
- if (block["multi_az"] !== void 0) attrs.multi_az = bool(block["multi_az"]);
2280
- if (block["replicas"] !== void 0) attrs.replicas = num(block["replicas"]);
2281
- return attrs;
2282
- },
2283
- aws_rds_cluster(block) {
2284
- const attrs = {};
2285
- if (str(block["engine"])) attrs.engine = str(block["engine"]);
2286
- if (str(block["engine_version"])) attrs.engine_version = str(block["engine_version"]);
2287
- if (str(block["db_cluster_instance_class"])) attrs.instance_type = str(block["db_cluster_instance_class"]);
2288
- if (block["master_username"]) attrs.master_username = str(block["master_username"]);
2289
- return attrs;
2290
- },
2291
- aws_s3_bucket(block) {
2292
- const attrs = {};
2293
- const lifecycle = firstBlock(block, "lifecycle_rule");
2294
- const transition = firstBlock(lifecycle, "transition");
2295
- if (str(transition["storage_class"])) attrs.storage_type = str(transition["storage_class"]);
2296
- if (str(block["bucket"])) attrs.bucket = str(block["bucket"]);
2297
- return attrs;
2298
- },
2299
- aws_lb(block) {
2300
- const attrs = {};
2301
- if (str(block["load_balancer_type"])) attrs.load_balancer_type = str(block["load_balancer_type"]);
2302
- if (block["internal"] !== void 0) attrs.internal = bool(block["internal"]);
2303
- return attrs;
2304
- },
2305
- aws_alb(block) {
2306
- return extractors["aws_lb"](block);
2307
- },
2308
- aws_nat_gateway(block) {
2309
- const attrs = {};
2310
- if (str(block["allocation_id"])) attrs.allocation_id = str(block["allocation_id"]);
2311
- return attrs;
2312
- },
2313
- aws_eks_cluster(block) {
2314
- const attrs = {};
2315
- if (str(block["name"])) attrs.cluster_name = str(block["name"]);
2316
- return attrs;
2317
- },
2318
- aws_eks_node_group(block) {
2319
- const scaling = firstBlock(block, "scaling_config");
2320
- const attrs = {};
2321
- const instanceTypes = block["instance_types"];
2322
- if (Array.isArray(instanceTypes) && instanceTypes.length > 0) {
2323
- attrs.instance_type = str(instanceTypes[0]);
2324
- }
2325
- if (scaling["desired_size"] !== void 0) attrs.node_count = num(scaling["desired_size"]);
2326
- if (scaling["min_size"] !== void 0) attrs.min_node_count = num(scaling["min_size"]);
2327
- if (scaling["max_size"] !== void 0) attrs.max_node_count = num(scaling["max_size"]);
2328
- return attrs;
2329
- },
2330
- aws_elasticache_cluster(block) {
2331
- const attrs = {};
2332
- if (str(block["node_type"])) attrs.instance_type = str(block["node_type"]);
2333
- if (str(block["engine"])) attrs.engine = str(block["engine"]);
2334
- if (block["num_cache_nodes"] !== void 0) attrs.node_count = num(block["num_cache_nodes"]);
2335
- return attrs;
2336
- },
2337
- aws_elasticache_replication_group(block) {
2338
- const attrs = {};
2339
- if (str(block["node_type"])) attrs.instance_type = str(block["node_type"]);
2340
- if (block["num_cache_clusters"] !== void 0) attrs.node_count = num(block["num_cache_clusters"]);
2341
- return attrs;
2342
- },
2343
- aws_ebs_volume(block) {
2344
- const attrs = {};
2345
- if (str(block["type"])) attrs.storage_type = str(block["type"]);
2346
- if (block["size"] !== void 0) attrs.storage_size_gb = num(block["size"]);
2347
- if (block["iops"] !== void 0) attrs.iops = num(block["iops"]);
2348
- if (block["throughput"] !== void 0) attrs.throughput_mbps = num(block["throughput"]);
2349
- return attrs;
2350
- },
2351
- aws_lambda_function(block) {
2352
- const attrs = {};
2353
- if (block["memory_size"] !== void 0) attrs.memory_size = num(block["memory_size"]);
2354
- if (block["timeout"] !== void 0) attrs.timeout = num(block["timeout"]);
2355
- if (str(block["architecture"])) {
2356
- attrs.architecture = str(block["architecture"]);
2357
- } else {
2358
- const arch = block["architectures"];
2359
- if (Array.isArray(arch) && arch.length > 0) {
2360
- attrs.architecture = str(arch[0]);
2361
- }
2362
- }
2363
- if (str(block["runtime"])) attrs.runtime = str(block["runtime"]);
2364
- return attrs;
2365
- },
2366
- aws_dynamodb_table(block) {
2367
- const attrs = {};
2368
- if (str(block["billing_mode"])) attrs.billing_mode = str(block["billing_mode"]);
2369
- if (block["read_capacity"] !== void 0) attrs.read_capacity = num(block["read_capacity"]);
2370
- if (block["write_capacity"] !== void 0) attrs.write_capacity = num(block["write_capacity"]);
2371
- return attrs;
2372
- },
2373
- aws_efs_file_system(block) {
2374
- const attrs = {};
2375
- if (str(block["performance_mode"])) attrs.performance_mode = str(block["performance_mode"]);
2376
- if (str(block["throughput_mode"])) attrs.throughput_mode = str(block["throughput_mode"]);
2377
- return attrs;
2378
- },
2379
- aws_cloudfront_distribution(block) {
2380
- const attrs = {};
2381
- if (str(block["price_class"])) attrs.price_class = str(block["price_class"]);
2382
- return attrs;
2383
- },
2384
- aws_sqs_queue(block) {
2385
- const attrs = {};
2386
- if (block["fifo_queue"] !== void 0) attrs.fifo_queue = bool(block["fifo_queue"]);
2387
- return attrs;
2388
- },
2389
- // ---------------------------------------------------------------------------
2390
- // Azure
2391
- // ---------------------------------------------------------------------------
2392
- azurerm_virtual_machine(block) {
2393
- const attrs = {};
2394
- if (str(block["vm_size"])) attrs.vm_size = str(block["vm_size"]);
2395
- if (str(block["location"])) attrs.location = str(block["location"]);
2396
- return attrs;
2397
- },
2398
- azurerm_linux_virtual_machine(block) {
2399
- const attrs = {};
2400
- if (str(block["size"])) attrs.vm_size = str(block["size"]);
2401
- if (str(block["location"])) attrs.location = str(block["location"]);
2402
- return attrs;
2403
- },
2404
- azurerm_windows_virtual_machine(block) {
2405
- const attrs = {};
2406
- if (str(block["size"])) attrs.vm_size = str(block["size"]);
2407
- if (str(block["location"])) attrs.location = str(block["location"]);
2408
- attrs.os = "Windows";
2409
- return attrs;
2410
- },
2411
- azurerm_managed_disk(block) {
2412
- const attrs = {};
2413
- if (str(block["storage_account_type"])) attrs.storage_type = str(block["storage_account_type"]);
2414
- if (block["disk_size_gb"] !== void 0) attrs.storage_size_gb = num(block["disk_size_gb"]);
2415
- if (block["disk_iops_read_write"] !== void 0) attrs.iops = num(block["disk_iops_read_write"]);
2416
- if (str(block["location"])) attrs.location = str(block["location"]);
2417
- return attrs;
2418
- },
2419
- azurerm_sql_server(block) {
2420
- const attrs = {};
2421
- if (str(block["version"])) attrs.engine_version = str(block["version"]);
2422
- if (str(block["location"])) attrs.location = str(block["location"]);
2423
- return attrs;
2424
- },
2425
- azurerm_mssql_database(block) {
2426
- const attrs = {};
2427
- if (str(block["sku_name"])) attrs.sku = str(block["sku_name"]);
2428
- if (block["max_size_gb"] !== void 0) attrs.storage_size_gb = num(block["max_size_gb"]);
2429
- return attrs;
2430
- },
2431
- azurerm_kubernetes_cluster(block) {
2432
- const defaultPool = firstBlock(block, "default_node_pool");
2433
- const attrs = {};
2434
- if (str(defaultPool["vm_size"])) attrs.vm_size = str(defaultPool["vm_size"]);
2435
- if (defaultPool["node_count"] !== void 0) attrs.node_count = num(defaultPool["node_count"]);
2436
- if (defaultPool["min_count"] !== void 0) attrs.min_node_count = num(defaultPool["min_count"]);
2437
- if (defaultPool["max_count"] !== void 0) attrs.max_node_count = num(defaultPool["max_count"]);
2438
- if (str(block["location"])) attrs.location = str(block["location"]);
2439
- return attrs;
2440
- },
2441
- azurerm_cosmosdb_account(block) {
2442
- const attrs = {};
2443
- if (str(block["offer_type"])) attrs.offer_type = str(block["offer_type"]);
2444
- if (str(block["kind"])) attrs.kind = str(block["kind"]);
2445
- const capabilities = block["capabilities"];
2446
- if (Array.isArray(capabilities)) {
2447
- const capNames = capabilities.map((c) => typeof c === "object" && c !== null ? str(c["name"]) : void 0).filter((n) => n !== void 0);
2448
- if (capNames.length > 0) attrs.capabilities = capNames;
2449
- }
2450
- return attrs;
2451
- },
2452
- azurerm_app_service_plan(block) {
2453
- const attrs = {};
2454
- const skuBlock = firstBlock(block, "sku");
2455
- if (str(skuBlock["tier"])) attrs.sku_tier = str(skuBlock["tier"]);
2456
- if (str(skuBlock["size"])) attrs.sku_size = str(skuBlock["size"]);
2457
- if (str(block["sku_name"])) attrs.sku = str(block["sku_name"]);
2458
- return attrs;
2459
- },
2460
- azurerm_function_app(block) {
2461
- const attrs = {};
2462
- if (str(block["app_service_plan_id"])) attrs.app_service_plan_id = str(block["app_service_plan_id"]);
2463
- return attrs;
2464
- },
2465
- azurerm_redis_cache(block) {
2466
- const attrs = {};
2467
- if (block["capacity"] !== void 0) attrs.capacity = num(block["capacity"]);
2468
- if (str(block["family"])) attrs.family = str(block["family"]);
2469
- if (str(block["sku_name"])) attrs.sku = str(block["sku_name"]);
2470
- return attrs;
2471
- },
2472
- azurerm_cosmosdb_sql_database(block) {
2473
- const attrs = {};
2474
- if (block["throughput"] !== void 0) attrs.throughput = num(block["throughput"]);
2475
- return attrs;
2476
- },
2477
- azurerm_storage_account(block) {
2478
- const attrs = {};
2479
- if (str(block["account_tier"])) attrs.account_tier = str(block["account_tier"]);
2480
- if (str(block["account_replication_type"])) attrs.account_replication_type = str(block["account_replication_type"]);
2481
- return attrs;
2482
- },
2483
- // ---------------------------------------------------------------------------
2484
- // GCP
2485
- // ---------------------------------------------------------------------------
2486
- google_compute_instance(block) {
2487
- const attrs = {};
2488
- if (str(block["machine_type"])) attrs.machine_type = str(block["machine_type"]);
2489
- if (str(block["zone"])) attrs.zone = str(block["zone"]);
2490
- const boot = firstBlock(block, "boot_disk");
2491
- const initParams = firstBlock(boot, "initialize_params");
2492
- if (str(initParams["type"])) attrs.storage_type = str(initParams["type"]);
2493
- if (initParams["size"] !== void 0) attrs.storage_size_gb = num(initParams["size"]);
2494
- return attrs;
2495
- },
2496
- google_compute_disk(block) {
2497
- const attrs = {};
2498
- if (str(block["type"])) attrs.storage_type = str(block["type"]);
2499
- if (block["size"] !== void 0) attrs.storage_size_gb = num(block["size"]);
2500
- if (str(block["zone"])) attrs.zone = str(block["zone"]);
2501
- return attrs;
2502
- },
2503
- google_sql_database_instance(block) {
2504
- const settings = firstBlock(block, "settings");
2505
- const attrs = {};
2506
- if (str(settings["tier"])) attrs.tier = str(settings["tier"]);
2507
- if (str(block["database_version"])) attrs.engine = str(block["database_version"]);
2508
- if (str(block["region"])) attrs.region_attr = str(block["region"]);
2509
- const dataCacheConfig = firstBlock(settings, "data_cache_config");
2510
- if (dataCacheConfig["data_cache_enabled"] !== void 0) {
2511
- attrs.data_cache_enabled = bool(dataCacheConfig["data_cache_enabled"]);
2512
- }
2513
- return attrs;
2514
- },
2515
- google_container_cluster(block) {
2516
- const nodeConfig = firstBlock(block, "node_config");
2517
- const attrs = {};
2518
- if (str(nodeConfig["machine_type"])) attrs.machine_type = str(nodeConfig["machine_type"]);
2519
- if (block["initial_node_count"] !== void 0) attrs.node_count = num(block["initial_node_count"]);
2520
- if (str(block["location"])) attrs.zone = str(block["location"]);
2521
- return attrs;
2522
- },
2523
- google_container_node_pool(block) {
2524
- const nodeConfig = firstBlock(block, "node_config");
2525
- const autoscaling = firstBlock(block, "autoscaling");
2526
- const attrs = {};
2527
- if (str(nodeConfig["machine_type"])) attrs.machine_type = str(nodeConfig["machine_type"]);
2528
- if (block["node_count"] !== void 0) attrs.node_count = num(block["node_count"]);
2529
- if (autoscaling["min_node_count"] !== void 0) attrs.min_node_count = num(autoscaling["min_node_count"]);
2530
- if (autoscaling["max_node_count"] !== void 0) attrs.max_node_count = num(autoscaling["max_node_count"]);
2531
- return attrs;
2532
- },
2533
- google_cloud_run_service(block) {
2534
- const attrs = {};
2535
- const template = firstBlock(block, "template");
2536
- const spec = firstBlock(template, "spec");
2537
- const container = firstBlock(spec, "containers");
2538
- const resources = firstBlock(container, "resources");
2539
- const limits = firstBlock(resources, "limits");
2540
- if (str(limits["cpu"])) attrs.cpu = str(limits["cpu"]);
2541
- if (str(limits["memory"])) attrs.memory = str(limits["memory"]);
2542
- return attrs;
2543
- },
2544
- google_cloudfunctions_function(block) {
2545
- const attrs = {};
2546
- if (str(block["runtime"])) attrs.runtime = str(block["runtime"]);
2547
- if (block["available_memory_mb"] !== void 0) attrs.memory_size = num(block["available_memory_mb"]);
2548
- return attrs;
2549
- },
2550
- google_bigquery_dataset(block) {
2551
- const attrs = {};
2552
- if (str(block["location"])) attrs.location = str(block["location"]);
2553
- return attrs;
2554
- },
2555
- google_redis_instance(block) {
2556
- const attrs = {};
2557
- if (str(block["tier"])) attrs.tier = str(block["tier"]);
2558
- if (block["memory_size_gb"] !== void 0) attrs.memory_size = num(block["memory_size_gb"]);
2559
- return attrs;
2560
- },
2561
- google_artifact_registry_repository(block) {
2562
- const attrs = {};
2563
- if (str(block["format"])) attrs.format = str(block["format"]);
2564
- return attrs;
2565
- }
2566
- };
2567
- function regionFromResourceBlock(resourceType, block, provider) {
2568
- switch (provider) {
2569
- case "azure": {
2570
- const loc = str(block["location"]);
2571
- return loc && !loc.startsWith("${") ? loc : void 0;
2572
- }
2573
- case "gcp": {
2574
- const zone = str(block["zone"]);
2575
- if (zone && !zone.startsWith("${")) {
2576
- const parts = zone.split("-");
2577
- return parts.length > 2 ? parts.slice(0, -1).join("-") : zone;
2578
- }
2579
- return str(block["region"]);
2580
- }
2581
- case "aws": {
2582
- const az = str(block["availability_zone"]);
2583
- if (az && !az.startsWith("${")) {
2584
- const parts = az.split("-");
2585
- return parts.length > 2 ? parts.slice(0, -1).join("-") : az;
2586
- }
2587
- return void 0;
2588
- }
2589
- }
2590
- }
2591
- function extractTags(block) {
2592
- const tags = block["tags"];
2593
- if (!tags || typeof tags !== "object" || Array.isArray(tags)) return {};
2594
- const result = {};
2595
- for (const [k, v] of Object.entries(tags)) {
2596
- if (typeof v === "string") result[k] = v;
2597
- else if (v !== null && v !== void 0) result[k] = String(v);
2598
- }
2599
- return result;
2600
- }
2601
- function resolveCount(rawCount, resourceId, warnings) {
2602
- const n = num(rawCount);
2603
- if (n !== void 0 && Number.isInteger(n) && n >= 0) return n;
2604
- warnings.push(
2605
- `Resource "${resourceId}" has a count that could not be resolved to a number. Defaulting to 1.`
2606
- );
2607
- return 1;
2608
- }
2609
- function resolveForEachKeys(rawForEach, resourceId, warnings) {
2610
- if (rawForEach && typeof rawForEach === "object" && !Array.isArray(rawForEach)) {
2611
- const keys = Object.keys(rawForEach);
2612
- if (keys.length > 0) return keys;
2613
- }
2614
- if (Array.isArray(rawForEach) && rawForEach.length > 0) {
2615
- return rawForEach.map(
2616
- (v, i) => typeof v === "string" ? v : typeof v === "number" ? String(v) : String(i)
2617
- );
2618
- }
2619
- warnings.push(
2620
- `Resource "${resourceId}" has a for_each that could not be resolved. Defaulting to 1 instance.`
2621
- );
2622
- return ["0"];
2623
- }
2624
- function extractResources(hclJson, variables, sourceFile, defaultRegions = {}, warnings = []) {
2625
- const resources = [];
2626
- const moduleBlock = hclJson["module"];
2627
- if (moduleBlock && typeof moduleBlock === "object" && !Array.isArray(moduleBlock)) {
2628
- for (const moduleName of Object.keys(moduleBlock)) {
2629
- warnings.push(
2630
- `Module "${moduleName}" detected but not expanded. Resources inside modules are not included in the estimate.`
2631
- );
2632
- logger.debug("Module detected, skipping expansion", { moduleName });
2633
- }
2634
- }
2635
- const resourceBlock = hclJson["resource"];
2636
- if (!resourceBlock || typeof resourceBlock !== "object") {
2637
- return resources;
2638
- }
2639
- for (const [resourceType, instances] of Object.entries(
2640
- resourceBlock
2641
- )) {
2642
- let provider;
2643
- try {
2644
- provider = detectProvider(resourceType);
2645
- } catch {
2646
- logger.warn("Skipping resource with unknown provider prefix", {
2647
- resourceType
2648
- });
2649
- continue;
2650
- }
2651
- if (!instances || typeof instances !== "object" || Array.isArray(instances)) {
2652
- continue;
2653
- }
2654
- for (const [resourceName, blockList] of Object.entries(
2655
- instances
2656
- )) {
2657
- if (!Array.isArray(blockList) || blockList.length === 0) continue;
2658
- const rawBlock = blockList[0];
2659
- const block = substituteVariables(rawBlock, variables);
2660
- const resourceRegion = regionFromResourceBlock(resourceType, block, provider);
2661
- const region = resourceRegion ?? defaultRegions[provider] ?? (provider === "aws" ? "us-east-1" : provider === "azure" ? "eastus" : "us-central1");
2662
- const extractor = extractors[resourceType];
2663
- const attributes = extractor ? extractor(block) : {};
2664
- const tags = extractTags(block);
2665
- const baseId = `${resourceType}.${resourceName}`;
2666
- const rawCount = block["count"];
2667
- const rawForEach = block["for_each"];
2668
- if (rawCount !== void 0) {
2669
- const count = resolveCount(rawCount, baseId, warnings);
2670
- for (let i = 0; i < count; i++) {
2671
- resources.push({
2672
- id: `${baseId}[${i}]`,
2673
- type: resourceType,
2674
- name: resourceName,
2675
- provider,
2676
- region,
2677
- attributes,
2678
- tags,
2679
- source_file: sourceFile
2680
- });
2681
- }
2682
- } else if (rawForEach !== void 0) {
2683
- const keys = resolveForEachKeys(rawForEach, baseId, warnings);
2684
- for (const key of keys) {
2685
- resources.push({
2686
- id: `${baseId}["${key}"]`,
2687
- type: resourceType,
2688
- name: resourceName,
2689
- provider,
2690
- region,
2691
- attributes,
2692
- tags,
2693
- source_file: sourceFile
2694
- });
2695
- }
2696
- } else {
2697
- resources.push({
2698
- id: baseId,
2699
- type: resourceType,
2700
- name: resourceName,
2701
- provider,
2702
- region,
2703
- attributes,
2704
- tags,
2705
- source_file: sourceFile
2706
- });
2707
- }
2708
- logger.debug("Extracted resource", {
2709
- id: baseId,
2710
- region,
2711
- provider
2712
- });
2713
- }
2714
- }
2715
- return resources;
2716
- }
2717
-
2718
- // src/parsers/index.ts
2719
- var PROVIDER_DEFAULTS = {
2720
- aws: "us-east-1",
2721
- azure: "eastus",
2722
- gcp: "us-central1"
2723
- };
2724
- function inferDominantProvider(resources) {
2725
- if (resources.length === 0) return "aws";
2726
- const counts = {};
2727
- for (const r of resources) {
2728
- counts[r.provider] = (counts[r.provider] ?? 0) + 1;
2729
- }
2730
- let dominant = "aws";
2731
- let max = 0;
2732
- for (const [p, c] of Object.entries(counts)) {
2733
- if (c > max) {
2734
- max = c;
2735
- dominant = p;
2736
- }
2737
- }
2738
- return dominant;
2739
- }
2740
- async function parseTerraform(files, tfvarsContent) {
2741
- const warnings = [];
2742
- const parsedJsons = [];
2743
- for (const file of files) {
2744
- try {
2745
- const json = await parseHclToJson(file.content, file.path);
2746
- parsedJsons.push({ path: file.path, json });
2747
- } catch (err) {
2748
- const msg = err instanceof Error ? err.message : String(err);
2749
- warnings.push(`Parse error in ${file.path}: ${msg}`);
2750
- logger.warn("Skipping file due to parse error", {
2751
- path: file.path,
2752
- error: msg
2753
- });
2754
- }
2755
- }
2756
- if (parsedJsons.length === 0) {
2757
- logger.warn("No files were successfully parsed");
2758
- return {
2759
- provider: "aws",
2760
- region: PROVIDER_DEFAULTS["aws"],
2761
- resources: [],
2762
- total_count: 0,
2763
- by_type: {},
2764
- parse_warnings: warnings
2765
- };
2766
- }
2767
- const combined = mergeHclJsons(parsedJsons.map((p) => p.json));
2768
- const variables = resolveVariables(combined, tfvarsContent);
2769
- const providers = ["aws", "azure", "gcp"];
2770
- const defaultRegions = {};
2771
- for (const p of providers) {
2772
- defaultRegions[p] = detectRegionFromProviders(combined, p, variables);
2773
- }
2774
- const allResources = [];
2775
- for (const { path, json } of parsedJsons) {
2776
- try {
2777
- const resources = extractResources(json, variables, path, defaultRegions, warnings);
2778
- allResources.push(...resources);
2779
- } catch (err) {
2780
- const msg = err instanceof Error ? err.message : String(err);
2781
- warnings.push(`Extraction error in ${path}: ${msg}`);
2782
- logger.warn("Resource extraction failed", { path, error: msg });
2783
- }
2784
- }
2785
- const dominantProvider = inferDominantProvider(allResources);
2786
- const dominantRegion = defaultRegions[dominantProvider] ?? PROVIDER_DEFAULTS[dominantProvider];
2787
- const byType = {};
2788
- for (const r of allResources) {
2789
- byType[r.type] = (byType[r.type] ?? 0) + 1;
2790
- }
2791
- const inventory = {
2792
- provider: dominantProvider,
2793
- region: dominantRegion,
2794
- resources: allResources,
2795
- total_count: allResources.length,
2796
- by_type: byType,
2797
- parse_warnings: warnings
2798
- };
2799
- logger.info("Terraform parse complete", {
2800
- fileCount: files.length,
2801
- resourceCount: allResources.length,
2802
- provider: dominantProvider,
2803
- region: dominantRegion,
2804
- warningCount: warnings.length
2805
- });
2806
- return inventory;
2807
- }
2808
- function mergeHclJsons(jsons) {
2809
- const merged = {};
2810
- for (const json of jsons) {
2811
- for (const [key, value] of Object.entries(json)) {
2812
- if (!(key in merged)) {
2813
- merged[key] = deepClone(value);
2814
- continue;
2815
- }
2816
- const existing = merged[key];
2817
- if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
2818
- merged[key] = mergeObjects(
2819
- existing,
2820
- value
2821
- );
2822
- } else if (Array.isArray(existing) && Array.isArray(value)) {
2823
- merged[key] = [...existing, ...value];
2824
- } else {
2825
- merged[key] = deepClone(value);
2826
- }
2827
- }
2828
- }
2829
- return merged;
2830
- }
2831
- function mergeObjects(a, b) {
2832
- const result = { ...a };
2833
- for (const [key, bVal] of Object.entries(b)) {
2834
- const aVal = result[key];
2835
- if (aVal !== null && typeof aVal === "object" && !Array.isArray(aVal) && bVal !== null && typeof bVal === "object" && !Array.isArray(bVal)) {
2836
- result[key] = mergeObjects(
2837
- aVal,
2838
- bVal
2839
- );
2840
- } else if (Array.isArray(aVal) && Array.isArray(bVal)) {
2841
- result[key] = [...aVal, ...bVal];
2842
- } else {
2843
- result[key] = deepClone(bVal);
2844
- }
2845
- }
2846
- return result;
2847
- }
2848
- function deepClone(value) {
2849
- if (value === null || typeof value !== "object") return value;
2850
- return JSON.parse(JSON.stringify(value));
2851
- }
2852
-
2853
- // src/tools/analyze-terraform.ts
2854
- var analyzeTerraformSchema = z.object({
2855
- files: z.array(
2856
- z.object({
2857
- path: z.string().describe("File path"),
2858
- content: z.string().describe("File content (HCL)")
2859
- })
2860
- ).describe("Terraform files to analyze"),
2861
- tfvars: z.string().optional().describe("Contents of terraform.tfvars file")
2862
- });
2863
- async function analyzeTerraform(params) {
2864
- const inventory = await parseTerraform(params.files, params.tfvars);
2865
- const { parse_warnings, ...rest } = inventory;
2866
- return {
2867
- parse_warnings,
2868
- has_warnings: parse_warnings.length > 0,
2869
- ...rest
2870
- };
2871
- }
2872
-
2873
- // src/tools/estimate-cost.ts
2874
- import { z as z2 } from "zod";
2875
-
2876
- // src/mapping/region-mapper.ts
2877
- var DEFAULT_REGIONS = {
2878
- aws: "us-east-1",
2879
- azure: "eastus",
2880
- gcp: "us-central1"
2881
- };
2882
- function mapRegion(region, sourceProvider, targetProvider) {
2883
- if (sourceProvider === targetProvider) return region;
2884
- const mappings = getRegionMappings();
2885
- const row = mappings.find(
2886
- (entry) => entry[sourceProvider] === region
2887
- );
2888
- if (!row) {
2889
- const fallback = DEFAULT_REGIONS[targetProvider];
2890
- logger.debug("region-mapper: no mapping found, using default", {
2891
- region,
2892
- sourceProvider,
2893
- targetProvider,
2894
- fallback
2895
- });
2896
- return fallback;
2897
- }
2898
- const mapped = row[targetProvider];
2899
- if (!mapped) {
2900
- return DEFAULT_REGIONS[targetProvider];
2901
- }
2902
- return mapped;
2903
- }
2904
-
2905
- // src/mapping/instance-mapper.ts
2906
- function directionKey(source, target) {
2907
- if (source === target) return null;
2908
- return `${source}_to_${target}`;
2909
- }
2910
- function getSpecsForProvider(provider) {
2911
- switch (provider) {
2912
- case "aws":
2913
- return getAwsInstances();
2914
- case "azure":
2915
- return getAzureVmSizes();
2916
- case "gcp":
2917
- return getGcpMachineTypes();
2918
- }
2919
- }
2920
- var SCORE_EXACT_VCPU = 40;
2921
- var SCORE_APPROX_VCPU = 20;
2922
- var SCORE_EXACT_MEMORY = 40;
2923
- var SCORE_APPROX_MEMORY = 20;
2924
- var SCORE_CATEGORY = 20;
2925
- function mapInstance(instanceType, sourceProvider, targetProvider) {
2926
- if (sourceProvider === targetProvider) return instanceType;
2927
- const key = directionKey(sourceProvider, targetProvider);
2928
- if (!key) return instanceType;
2929
- const instanceMap = getInstanceMap();
2930
- const directionalMap = instanceMap[key];
2931
- if (directionalMap) {
2932
- const exact = directionalMap[instanceType];
2933
- if (exact) {
2934
- logger.debug("instance-mapper: exact match", {
2935
- instanceType,
2936
- sourceProvider,
2937
- targetProvider,
2938
- result: exact
2939
- });
2940
- return exact;
2941
- }
2942
- }
2943
- const sourceSpecs = getSpecsForProvider(sourceProvider);
2944
- const sourceSpec = sourceSpecs.find(
2945
- (s) => s.instance_type === instanceType
2946
- );
2947
- if (!sourceSpec) {
2948
- logger.debug("instance-mapper: source spec not found, cannot map", {
2949
- instanceType,
2950
- sourceProvider
2951
- });
2952
- return null;
2953
- }
2954
- return findNearestInstance(
2955
- {
2956
- vcpus: sourceSpec.vcpus,
2957
- memory_gb: sourceSpec.memory_gb,
2958
- category: sourceSpec.category
2959
- },
2960
- targetProvider
2961
- );
2962
- }
2963
- function findNearestInstance(spec, targetProvider) {
2964
- const candidates = getSpecsForProvider(targetProvider);
2965
- if (candidates.length === 0) return null;
2966
- let bestScore = -1;
2967
- let bestType = null;
2968
- for (const candidate of candidates) {
2969
- let score = 0;
2970
- if (candidate.vcpus === spec.vcpus) {
2971
- score += SCORE_EXACT_VCPU;
2972
- } else {
2973
- const larger = Math.max(candidate.vcpus, spec.vcpus);
2974
- const smaller = Math.min(candidate.vcpus, spec.vcpus);
2975
- if (smaller > 0) {
2976
- score += SCORE_APPROX_VCPU * (smaller / larger);
2977
- }
2978
- }
2979
- if (candidate.memory_gb === spec.memory_gb) {
2980
- score += SCORE_EXACT_MEMORY;
2981
- } else {
2982
- const larger = Math.max(candidate.memory_gb, spec.memory_gb);
2983
- const smaller = Math.min(candidate.memory_gb, spec.memory_gb);
2984
- if (smaller > 0) {
2985
- score += SCORE_APPROX_MEMORY * (smaller / larger);
2986
- }
2987
- }
2988
- if (candidate.category === spec.category) {
2989
- score += SCORE_CATEGORY;
2990
- }
2991
- if (score > bestScore || score === bestScore && bestType !== null && candidate.instance_type < bestType) {
2992
- bestScore = score;
2993
- bestType = candidate.instance_type;
2994
- }
2995
- }
2996
- if (bestType) {
2997
- logger.debug("instance-mapper: nearest match found", {
2998
- spec,
2999
- targetProvider,
3000
- result: bestType,
3001
- score: bestScore
3002
- });
3003
- }
3004
- return bestType;
3005
- }
3006
-
3007
- // src/mapping/storage-mapper.ts
3008
- function mapStorageType(storageType, sourceProvider, targetProvider) {
3009
- if (sourceProvider === targetProvider) return storageType;
3010
- const { block_storage, object_storage } = getStorageMap();
3011
- const allEntries = [...block_storage, ...object_storage];
3012
- const row = allEntries.find(
3013
- (entry) => entry[sourceProvider] === storageType
3014
- );
3015
- if (!row) {
3016
- logger.debug("storage-mapper: no mapping found", {
3017
- storageType,
3018
- sourceProvider,
3019
- targetProvider
3020
- });
3021
- return null;
3022
- }
3023
- const mapped = row[targetProvider];
3024
- if (!mapped) {
3025
- logger.debug("storage-mapper: target column missing in row", {
3026
- storageType,
3027
- sourceProvider,
3028
- targetProvider
3029
- });
3030
- return null;
3031
- }
3032
- return mapped;
3033
- }
3034
-
3035
- // src/calculator/compute.ts
3036
- async function calculateComputeCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3037
- const notes = [];
3038
- const breakdown = [];
3039
- const sourceInstanceType = resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.machine_type ?? "";
3040
- const mappedInstance = sourceInstanceType ? mapInstance(sourceInstanceType, resource.provider, targetProvider) : null;
3041
- let confidence = "low";
3042
- let specBasedMapping = false;
3043
- if (!mappedInstance) {
3044
- notes.push(
3045
- `No instance mapping found for ${sourceInstanceType} (${resource.provider} -> ${targetProvider})`
3046
- );
3047
- } else {
3048
- const { getInstanceMap: getInstanceMap2 } = await import("./loader-WIX54B7L.js");
3049
- const im = getInstanceMap2();
3050
- const dirKey = `${resource.provider}_to_${targetProvider}`;
3051
- const directMap = im[dirKey];
3052
- const isExact = directMap?.[sourceInstanceType] === mappedInstance;
3053
- confidence = isExact ? "high" : "medium";
3054
- if (!isExact) {
3055
- specBasedMapping = true;
3056
- notes.push(
3057
- `Instance mapping for ${sourceInstanceType} -> ${mappedInstance} is an approximate spec-based match`
3058
- );
3059
- }
3060
- }
3061
- const effectiveInstance = mappedInstance ?? sourceInstanceType;
3062
- const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(effectiveInstance, targetRegion, resource.attributes.os) : null;
3063
- let computeMonthlyCost = 0;
3064
- let pricingSource = "fallback";
3065
- if (computePrice) {
3066
- const rawSource = computePrice.attributes?.pricing_source;
3067
- if (rawSource === "bundled") {
3068
- pricingSource = "bundled";
3069
- } else if (rawSource === "fallback") {
3070
- pricingSource = "fallback";
3071
- notes.push(`Pricing for ${effectiveInstance} uses bundled fallback data (live API unavailable)`);
3072
- } else {
3073
- pricingSource = "live";
3074
- }
3075
- if (specBasedMapping) {
3076
- notes.push(`Spec-based instance mapping used: ${sourceInstanceType} -> ${mappedInstance}`);
3077
- }
3078
- const hourlyPrice = computePrice.price_per_unit;
3079
- computeMonthlyCost = hourlyPrice * monthlyHours;
3080
- breakdown.push({
3081
- description: `${effectiveInstance} compute (${monthlyHours}h/month)`,
3082
- unit: "Hrs",
3083
- quantity: monthlyHours,
3084
- unit_price: hourlyPrice,
3085
- monthly_cost: computeMonthlyCost
3086
- });
3087
- } else {
3088
- notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3089
- confidence = "low";
3090
- pricingSource = "fallback";
3091
- }
3092
- let storageMonthlyCost = 0;
3093
- const rootDisk = resource.attributes.root_block_device;
3094
- const storageType = rootDisk?.volume_type ?? resource.attributes.storage_type;
3095
- const storageSizeGb = rootDisk?.volume_size ?? resource.attributes.storage_size_gb;
3096
- if (storageType && storageSizeGb && storageSizeGb > 0) {
3097
- const mappedStorageType = mapStorageType(
3098
- storageType,
3099
- resource.provider,
3100
- targetProvider
3101
- ) ?? storageType;
3102
- const storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(mappedStorageType, targetRegion, storageSizeGb);
3103
- if (storagePrice) {
3104
- storageMonthlyCost = storagePrice.price_per_unit * storageSizeGb;
3105
- breakdown.push({
3106
- description: `Root disk ${mappedStorageType} (${storageSizeGb} GB)`,
3107
- unit: "GB-Mo",
3108
- quantity: storageSizeGb,
3109
- unit_price: storagePrice.price_per_unit,
3110
- monthly_cost: storageMonthlyCost
3111
- });
3112
- } else {
3113
- logger.debug("calculateComputeCost: no storage price found", {
3114
- mappedStorageType,
3115
- targetRegion
3116
- });
3117
- }
3118
- }
3119
- const totalMonthly = computeMonthlyCost + storageMonthlyCost;
3120
- return {
3121
- resource_id: resource.id,
3122
- resource_type: resource.type,
3123
- resource_name: resource.name,
3124
- provider: targetProvider,
3125
- region: targetRegion,
3126
- monthly_cost: totalMonthly,
3127
- yearly_cost: totalMonthly * 12,
3128
- currency: "USD",
3129
- breakdown,
3130
- confidence,
3131
- notes,
3132
- pricing_source: pricingSource
3133
- };
3134
- }
3135
-
3136
- // src/calculator/database.ts
3137
- function normalizeDbInstanceClass(instanceClass) {
3138
- return instanceClass.startsWith("db.") ? instanceClass.slice(3) : instanceClass;
3139
- }
3140
- async function calculateDatabaseCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3141
- const notes = [];
3142
- const breakdown = [];
3143
- const sourceInstanceClass = resource.attributes.instance_class ?? resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.tier ?? "";
3144
- const isMultiAz = Boolean(resource.attributes.multi_az);
3145
- const allocatedStorageGb = resource.attributes.allocated_storage ?? resource.attributes.storage_size_gb ?? 0;
3146
- const engine = resource.attributes.engine ?? "MySQL";
3147
- const normalised = normalizeDbInstanceClass(sourceInstanceClass);
3148
- let mappedInstance = null;
3149
- if (targetProvider === "gcp") {
3150
- mappedInstance = mapInstance(sourceInstanceClass, resource.provider, "gcp");
3151
- if (!mappedInstance) {
3152
- mappedInstance = mapInstance(normalised, resource.provider, "gcp");
3153
- }
3154
- if (mappedInstance && !mappedInstance.startsWith("db-")) {
3155
- const dbMapped = mapInstance(sourceInstanceClass, resource.provider, "gcp");
3156
- if (dbMapped) mappedInstance = dbMapped;
3157
- }
3158
- } else if (targetProvider === "aws") {
3159
- mappedInstance = mapInstance(sourceInstanceClass, resource.provider, "aws");
3160
- if (!mappedInstance && normalised !== sourceInstanceClass) {
3161
- mappedInstance = mapInstance(normalised, resource.provider, "aws");
3162
- }
3163
- if (mappedInstance && !mappedInstance.startsWith("db.")) {
3164
- mappedInstance = `db.${mappedInstance}`;
3165
- }
3166
- } else {
3167
- mappedInstance = mapInstance(sourceInstanceClass, resource.provider, targetProvider);
3168
- if (!mappedInstance) {
3169
- mappedInstance = mapInstance(normalised, resource.provider, targetProvider);
3170
- }
3171
- }
3172
- const effectiveInstance = mappedInstance ?? sourceInstanceClass;
3173
- if (!mappedInstance) {
3174
- notes.push(
3175
- `No DB instance mapping found for ${sourceInstanceClass} (${resource.provider} -> ${targetProvider}); using source class`
3176
- );
3177
- }
3178
- const dbPrice = await pricingEngine.getProvider(targetProvider).getDatabasePrice(effectiveInstance, targetRegion, engine);
3179
- let instanceMonthlyCost = 0;
3180
- let pricingSource = "fallback";
3181
- if (dbPrice) {
3182
- const rawSource = dbPrice.attributes?.pricing_source;
3183
- if (rawSource === "bundled") {
3184
- pricingSource = "bundled";
3185
- } else if (rawSource === "fallback") {
3186
- pricingSource = "fallback";
3187
- notes.push(`Pricing for ${effectiveInstance} uses bundled fallback data (live API unavailable)`);
3188
- } else {
3189
- pricingSource = "live";
3190
- }
3191
- instanceMonthlyCost = dbPrice.price_per_unit * monthlyHours;
3192
- let haMultiplier = 1;
3193
- if (isMultiAz) {
3194
- if (targetProvider === "gcp") {
3195
- const gcpMult = parseFloat(
3196
- dbPrice.attributes?.ha_multiplier ?? "2"
3197
- );
3198
- haMultiplier = isNaN(gcpMult) ? 2 : gcpMult;
3199
- } else {
3200
- haMultiplier = 2;
3201
- }
3202
- notes.push(`Multi-AZ enabled: instance cost multiplied by ${haMultiplier}`);
3203
- }
3204
- const adjustedCost = instanceMonthlyCost * haMultiplier;
3205
- breakdown.push({
3206
- description: `${effectiveInstance} DB instance (${monthlyHours}h/month)${isMultiAz ? " x" + haMultiplier + " (Multi-AZ)" : ""}`,
3207
- unit: "Hrs",
3208
- quantity: monthlyHours * haMultiplier,
3209
- unit_price: dbPrice.price_per_unit,
3210
- monthly_cost: adjustedCost
3211
- });
3212
- instanceMonthlyCost = adjustedCost;
3213
- } else {
3214
- notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3215
- logger.debug("calculateDatabaseCost: no price", {
3216
- effectiveInstance,
3217
- targetRegion
3218
- });
3219
- }
3220
- let storageMonthlyCost = 0;
3221
- if (allocatedStorageGb > 0) {
3222
- const storageType = resource.attributes.storage_type ?? "gp3";
3223
- const storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(storageType, targetRegion, allocatedStorageGb);
3224
- if (storagePrice) {
3225
- storageMonthlyCost = storagePrice.price_per_unit * allocatedStorageGb;
3226
- breakdown.push({
3227
- description: `DB storage ${storageType} (${allocatedStorageGb} GB)`,
3228
- unit: "GB-Mo",
3229
- quantity: allocatedStorageGb,
3230
- unit_price: storagePrice.price_per_unit,
3231
- monthly_cost: storageMonthlyCost
3232
- });
3233
- }
3234
- }
3235
- const totalMonthly = instanceMonthlyCost + storageMonthlyCost;
3236
- return {
3237
- resource_id: resource.id,
3238
- resource_type: resource.type,
3239
- resource_name: resource.name,
3240
- provider: targetProvider,
3241
- region: targetRegion,
3242
- monthly_cost: totalMonthly,
3243
- yearly_cost: totalMonthly * 12,
3244
- currency: "USD",
3245
- breakdown,
3246
- confidence: mappedInstance ? "high" : "low",
3247
- notes,
3248
- pricing_source: pricingSource
3249
- };
3250
- }
3251
-
3252
- // src/calculator/storage.ts
3253
- var DEFAULT_OBJECT_STORAGE_GB = 100;
3254
- var BLOCK_STORAGE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3255
- "aws_ebs_volume",
3256
- "azurerm_managed_disk",
3257
- "google_compute_disk"
3258
- ]);
3259
- var BLOCK_STORAGE_FALLBACK = {
3260
- aws: "gp3",
3261
- azure: "Premium_LRS",
3262
- gcp: "pd-ssd"
3263
- };
3264
- function isBlockStorage(resource) {
3265
- return BLOCK_STORAGE_RESOURCE_TYPES.has(resource.type);
3266
- }
3267
- async function calculateStorageCost(resource, targetProvider, targetRegion, pricingEngine) {
3268
- const notes = [];
3269
- const breakdown = [];
3270
- const block = isBlockStorage(resource);
3271
- const sourceStorageType = resource.attributes.storage_type ?? resource.attributes.type ?? resource.attributes.storage_class ?? (block ? "gp3" : "STANDARD");
3272
- const mappedStorageType = mapStorageType(sourceStorageType, resource.provider, targetProvider) ?? sourceStorageType;
3273
- let sizeGb = resource.attributes.storage_size_gb ?? resource.attributes.disk_size_gb ?? resource.attributes.size;
3274
- if (!sizeGb || sizeGb <= 0) {
3275
- if (block) {
3276
- sizeGb = 8;
3277
- notes.push("No storage size specified; defaulting to 8 GB for block volume");
3278
- } else {
3279
- sizeGb = DEFAULT_OBJECT_STORAGE_GB;
3280
- notes.push(
3281
- `No storage size specified; assuming ${DEFAULT_OBJECT_STORAGE_GB} GB for object storage cost estimate`
3282
- );
3283
- }
3284
- }
3285
- let resolvedStorageType = mappedStorageType;
3286
- let storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(resolvedStorageType, targetRegion, sizeGb);
3287
- if (!storagePrice && !block) {
3288
- const fallbackType = BLOCK_STORAGE_FALLBACK[targetProvider];
3289
- if (fallbackType) {
3290
- storagePrice = await pricingEngine.getProvider(targetProvider).getStoragePrice(fallbackType, targetRegion, sizeGb);
3291
- if (storagePrice) {
3292
- resolvedStorageType = fallbackType;
3293
- notes.push(
3294
- `Object storage class "${mappedStorageType}" not directly priced; using ${fallbackType} rate as an approximation`
3295
- );
3296
- }
3297
- }
3298
- }
3299
- let totalMonthly = 0;
3300
- let pricingSource = "fallback";
3301
- if (storagePrice) {
3302
- const rawSource = storagePrice.attributes?.pricing_source;
3303
- if (rawSource === "bundled") {
3304
- pricingSource = "bundled";
3305
- } else if (rawSource === "fallback") {
3306
- pricingSource = "fallback";
3307
- notes.push(`Pricing for ${resolvedStorageType} uses bundled fallback data (live API unavailable)`);
3308
- } else {
3309
- pricingSource = "live";
3310
- }
3311
- totalMonthly = storagePrice.price_per_unit * sizeGb;
3312
- breakdown.push({
3313
- description: `${block ? "Block" : "Object"} storage ${resolvedStorageType} (${sizeGb} GB)`,
3314
- unit: "GB-Mo",
3315
- quantity: sizeGb,
3316
- unit_price: storagePrice.price_per_unit,
3317
- monthly_cost: totalMonthly
3318
- });
3319
- } else {
3320
- notes.push(
3321
- `No pricing data found for ${resource.type} in ${targetRegion}`
3322
- );
3323
- logger.debug("calculateStorageCost: no price", {
3324
- mappedStorageType,
3325
- targetRegion
3326
- });
3327
- }
3328
- return {
3329
- resource_id: resource.id,
3330
- resource_type: resource.type,
3331
- resource_name: resource.name,
3332
- provider: targetProvider,
3333
- region: targetRegion,
3334
- monthly_cost: totalMonthly,
3335
- yearly_cost: totalMonthly * 12,
3336
- currency: "USD",
3337
- breakdown,
3338
- confidence: storagePrice ? "high" : "low",
3339
- notes,
3340
- pricing_source: pricingSource
3341
- };
3342
- }
3343
-
3344
- // src/calculator/network.ts
3345
- var DEFAULT_NAT_DATA_GB = 100;
3346
- async function calculateNatGatewayCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3347
- const notes = [];
3348
- const breakdown = [];
3349
- const natPrice = await pricingEngine.getProvider(targetProvider).getNatGatewayPrice(targetRegion);
3350
- let totalMonthly = 0;
3351
- let natPricingSource = "fallback";
3352
- if (natPrice) {
3353
- const rawSource = natPrice.attributes?.pricing_source;
3354
- if (rawSource === "bundled") {
3355
- natPricingSource = "bundled";
3356
- } else if (rawSource === "fallback") {
3357
- natPricingSource = "fallback";
3358
- } else {
3359
- natPricingSource = "live";
3360
- }
3361
- const hourlyCharge = natPrice.price_per_unit * monthlyHours;
3362
- breakdown.push({
3363
- description: `NAT Gateway hourly charge (${monthlyHours}h/month)`,
3364
- unit: "Hrs",
3365
- quantity: monthlyHours,
3366
- unit_price: natPrice.price_per_unit,
3367
- monthly_cost: hourlyCharge
3368
- });
3369
- totalMonthly += hourlyCharge;
3370
- const perGbPrice = parseFloat(
3371
- natPrice.attributes?.per_gb_price ?? "0"
3372
- );
3373
- if (perGbPrice > 0) {
3374
- const dataGb = resource.attributes.data_processed_gb ?? DEFAULT_NAT_DATA_GB;
3375
- if (!resource.attributes.data_processed_gb || resource.attributes.data_processed_gb <= 0) {
3376
- notes.push(
3377
- `Data processing cost estimated at ${dataGb} GB/month; provide data_processed_gb attribute for accuracy`
3378
- );
3379
- }
3380
- const dataCharge = perGbPrice * dataGb;
3381
- breakdown.push({
3382
- description: `NAT Gateway data processing (${dataGb} GB/month)`,
3383
- unit: "GB",
3384
- quantity: dataGb,
3385
- unit_price: perGbPrice,
3386
- monthly_cost: dataCharge
3387
- });
3388
- totalMonthly += dataCharge;
3389
- }
3390
- } else {
3391
- notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3392
- }
3393
- return {
3394
- resource_id: resource.id,
3395
- resource_type: resource.type,
3396
- resource_name: resource.name,
3397
- provider: targetProvider,
3398
- region: targetRegion,
3399
- monthly_cost: totalMonthly,
3400
- yearly_cost: totalMonthly * 12,
3401
- currency: "USD",
3402
- breakdown,
3403
- confidence: natPrice ? "medium" : "low",
3404
- notes,
3405
- pricing_source: natPricingSource
3406
- };
3407
- }
3408
- async function calculateLoadBalancerCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3409
- const notes = [];
3410
- const breakdown = [];
3411
- const lbType = resource.attributes.load_balancer_type ?? "application";
3412
- const lbPrice = await pricingEngine.getProvider(targetProvider).getLoadBalancerPrice(lbType, targetRegion);
3413
- let totalMonthly = 0;
3414
- let lbPricingSource = "fallback";
3415
- if (lbPrice) {
3416
- const rawSource = lbPrice.attributes?.pricing_source;
3417
- if (rawSource === "bundled") {
3418
- lbPricingSource = "bundled";
3419
- } else if (rawSource === "fallback") {
3420
- lbPricingSource = "fallback";
3421
- } else {
3422
- lbPricingSource = "live";
3423
- }
3424
- totalMonthly = lbPrice.price_per_unit * monthlyHours;
3425
- breakdown.push({
3426
- description: `Load Balancer hourly charge (${monthlyHours}h/month)`,
3427
- unit: "Hrs",
3428
- quantity: monthlyHours,
3429
- unit_price: lbPrice.price_per_unit,
3430
- monthly_cost: totalMonthly
3431
- });
3432
- notes.push(
3433
- "Traffic-based charges (LCU/data processing) are excluded; this is the fixed hourly component only"
3434
- );
3435
- } else {
3436
- notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3437
- }
3438
- return {
3439
- resource_id: resource.id,
3440
- resource_type: resource.type,
3441
- resource_name: resource.name,
3442
- provider: targetProvider,
3443
- region: targetRegion,
3444
- monthly_cost: totalMonthly,
3445
- yearly_cost: totalMonthly * 12,
3446
- currency: "USD",
3447
- breakdown,
3448
- confidence: lbPrice ? "medium" : "low",
3449
- notes,
3450
- pricing_source: lbPricingSource
3451
- };
3452
- }
3453
-
3454
- // src/calculator/kubernetes.ts
3455
- async function calculateKubernetesCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
3456
- const notes = [];
3457
- const breakdown = [];
3458
- const k8sPrice = await pricingEngine.getProvider(targetProvider).getKubernetesPrice(targetRegion);
3459
- let totalMonthly = 0;
3460
- let pricingSource = "fallback";
3461
- if (k8sPrice) {
3462
- const rawSource = k8sPrice.attributes?.pricing_source;
3463
- if (rawSource === "bundled") {
3464
- pricingSource = "bundled";
3465
- } else if (rawSource === "fallback") {
3466
- pricingSource = "fallback";
3467
- } else {
3468
- pricingSource = "live";
3469
- }
3470
- const controlPlaneCost = k8sPrice.price_per_unit * monthlyHours;
3471
- breakdown.push({
3472
- description: `Kubernetes control plane (${monthlyHours}h/month)`,
3473
- unit: "Hrs",
3474
- quantity: monthlyHours,
3475
- unit_price: k8sPrice.price_per_unit,
3476
- monthly_cost: controlPlaneCost
3477
- });
3478
- totalMonthly += controlPlaneCost;
3479
- } else {
3480
- notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
3481
- }
3482
- const nodeCount = resource.attributes.node_count ?? resource.attributes.min_node_count;
3483
- if (nodeCount && nodeCount > 0) {
3484
- const nodeInstanceType = resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.machine_type;
3485
- if (nodeInstanceType) {
3486
- const nodeResource = {
3487
- ...resource,
3488
- id: `${resource.id}/node`,
3489
- type: "compute_node",
3490
- name: `${resource.name}-node`,
3491
- attributes: {
3492
- instance_type: nodeInstanceType,
3493
- vm_size: nodeInstanceType,
3494
- machine_type: nodeInstanceType
3495
- }
3496
- };
3497
- const nodeCostEstimate = await calculateComputeCost(
3498
- nodeResource,
3499
- targetProvider,
3500
- targetRegion,
3501
- pricingEngine,
3502
- monthlyHours
3503
- );
3504
- if (nodeCostEstimate.monthly_cost > 0) {
3505
- const nodeGroupCost = nodeCostEstimate.monthly_cost * nodeCount;
3506
- breakdown.push({
3507
- description: `${nodeCount}x node (${nodeInstanceType}) compute`,
3508
- unit: "nodes",
3509
- quantity: nodeCount,
3510
- unit_price: nodeCostEstimate.monthly_cost,
3511
- monthly_cost: nodeGroupCost
3512
- });
3513
- totalMonthly += nodeGroupCost;
3514
- notes.push(
3515
- `Node cost estimated for ${nodeCount} nodes of type ${nodeInstanceType}`
3516
- );
3517
- }
3518
- } else {
3519
- notes.push(
3520
- `node_count=${nodeCount} specified but no instance_type found; node compute cost excluded`
3521
- );
3522
- }
3523
- }
3524
- return {
3525
- resource_id: resource.id,
3526
- resource_type: resource.type,
3527
- resource_name: resource.name,
3528
- provider: targetProvider,
3529
- region: targetRegion,
3530
- monthly_cost: totalMonthly,
3531
- yearly_cost: totalMonthly * 12,
3532
- currency: "USD",
3533
- breakdown,
3534
- confidence: k8sPrice ? "medium" : "low",
3535
- notes,
3536
- pricing_source: pricingSource
3537
- };
3538
- }
3539
-
3540
- // src/calculator/cost-engine.ts
3541
- var COMPUTE_TYPES = /* @__PURE__ */ new Set([
3542
- "aws_instance",
3543
- "azurerm_linux_virtual_machine",
3544
- "azurerm_windows_virtual_machine",
3545
- "google_compute_instance",
3546
- // Node groups – cost their compute like VMs.
3547
- "aws_eks_node_group",
3548
- "azurerm_kubernetes_cluster_node_pool",
3549
- "google_container_node_pool",
3550
- "compute_node"
3551
- ]);
3552
- var DATABASE_TYPES = /* @__PURE__ */ new Set([
3553
- "aws_db_instance",
3554
- "azurerm_postgresql_flexible_server",
3555
- "azurerm_mysql_flexible_server",
3556
- "azurerm_mssql_server",
3557
- "google_sql_database_instance"
3558
- ]);
3559
- var BLOCK_STORAGE_TYPES = /* @__PURE__ */ new Set([
3560
- "aws_ebs_volume",
3561
- "azurerm_managed_disk",
3562
- "google_compute_disk"
3563
- ]);
3564
- var OBJECT_STORAGE_TYPES = /* @__PURE__ */ new Set([
3565
- "aws_s3_bucket",
3566
- "azurerm_storage_account",
3567
- "google_storage_bucket"
3568
- ]);
3569
- var NAT_GATEWAY_TYPES = /* @__PURE__ */ new Set([
3570
- "aws_nat_gateway",
3571
- "azurerm_nat_gateway",
3572
- "google_compute_router_nat"
3573
- ]);
3574
- var LOAD_BALANCER_TYPES = /* @__PURE__ */ new Set([
3575
- "aws_lb",
3576
- "aws_alb",
3577
- "azurerm_lb",
3578
- "azurerm_application_gateway",
3579
- "google_compute_forwarding_rule"
3580
- ]);
3581
- var KUBERNETES_TYPES = /* @__PURE__ */ new Set([
3582
- "aws_eks_cluster",
3583
- "azurerm_kubernetes_cluster",
3584
- "google_container_cluster"
3585
- ]);
3586
- function serviceLabel(resourceType) {
3587
- if (COMPUTE_TYPES.has(resourceType)) return "compute";
3588
- if (DATABASE_TYPES.has(resourceType)) return "database";
3589
- if (BLOCK_STORAGE_TYPES.has(resourceType)) return "block_storage";
3590
- if (OBJECT_STORAGE_TYPES.has(resourceType)) return "object_storage";
3591
- if (NAT_GATEWAY_TYPES.has(resourceType)) return "network";
3592
- if (LOAD_BALANCER_TYPES.has(resourceType)) return "network";
3593
- if (KUBERNETES_TYPES.has(resourceType)) return "kubernetes";
3594
- return "other";
3595
- }
3596
- var CostEngine = class {
3597
- pricingEngine;
3598
- monthlyHours;
3599
- constructor(pricingEngine, config) {
3600
- this.pricingEngine = pricingEngine;
3601
- this.monthlyHours = config.pricing.monthly_hours;
3602
- }
3603
- /**
3604
- * Calculates the cost of a single resource on a target provider.
3605
- */
3606
- async calculateCost(resource, targetProvider, targetRegion) {
3607
- const type = resource.type;
3608
- logger.debug("CostEngine.calculateCost", {
3609
- resourceId: resource.id,
3610
- type,
3611
- targetProvider,
3612
- targetRegion
3613
- });
3614
- if (COMPUTE_TYPES.has(type)) {
3615
- return calculateComputeCost(
3616
- resource,
3617
- targetProvider,
3618
- targetRegion,
3619
- this.pricingEngine,
3620
- this.monthlyHours
3621
- );
3622
- }
3623
- if (DATABASE_TYPES.has(type)) {
3624
- return calculateDatabaseCost(
3625
- resource,
3626
- targetProvider,
3627
- targetRegion,
3628
- this.pricingEngine,
3629
- this.monthlyHours
3630
- );
3631
- }
3632
- if (BLOCK_STORAGE_TYPES.has(type) || OBJECT_STORAGE_TYPES.has(type)) {
3633
- return calculateStorageCost(
3634
- resource,
3635
- targetProvider,
3636
- targetRegion,
3637
- this.pricingEngine
3638
- );
3639
- }
3640
- if (NAT_GATEWAY_TYPES.has(type)) {
3641
- return calculateNatGatewayCost(
3642
- resource,
3643
- targetProvider,
3644
- targetRegion,
3645
- this.pricingEngine,
3646
- this.monthlyHours
3647
- );
3648
- }
3649
- if (LOAD_BALANCER_TYPES.has(type)) {
3650
- return calculateLoadBalancerCost(
3651
- resource,
3652
- targetProvider,
3653
- targetRegion,
3654
- this.pricingEngine,
3655
- this.monthlyHours
3656
- );
3657
- }
3658
- if (KUBERNETES_TYPES.has(type)) {
3659
- return calculateKubernetesCost(
3660
- resource,
3661
- targetProvider,
3662
- targetRegion,
3663
- this.pricingEngine,
3664
- this.monthlyHours
3665
- );
3666
- }
3667
- logger.warn("CostEngine: unsupported resource type", { type, targetProvider });
3668
- return {
3669
- resource_id: resource.id,
3670
- resource_type: type,
3671
- resource_name: resource.name,
3672
- provider: targetProvider,
3673
- region: targetRegion,
3674
- monthly_cost: 0,
3675
- yearly_cost: 0,
3676
- currency: "USD",
3677
- breakdown: [],
3678
- confidence: "low",
3679
- notes: [`Resource type "${type}" is not yet supported by the cost engine`],
3680
- pricing_source: "fallback"
3681
- };
3682
- }
3683
- /**
3684
- * Calculates costs for all resources and aggregates them into a CostBreakdown.
3685
- *
3686
- * Resources that fail to produce a cost estimate are logged and skipped rather
3687
- * than letting a single failure abort the entire calculation.
3688
- */
3689
- async calculateBreakdown(resources, targetProvider, targetRegion) {
3690
- const estimates = [];
3691
- const byService = {};
3692
- const warnings = [];
3693
- for (const resource of resources) {
3694
- try {
3695
- const estimate = await this.calculateCost(
3696
- resource,
3697
- targetProvider,
3698
- targetRegion
3699
- );
3700
- estimates.push(estimate);
3701
- const svc = serviceLabel(resource.type);
3702
- byService[svc] = (byService[svc] ?? 0) + estimate.monthly_cost;
3703
- if (estimate.pricing_source === "fallback") {
3704
- warnings.push(
3705
- `${estimate.resource_name} (${estimate.resource_type}): using fallback/bundled pricing data`
3706
- );
3707
- }
3708
- if (estimate.monthly_cost === 0 && estimate.confidence === "low") {
3709
- warnings.push(
3710
- `No pricing data found for ${estimate.resource_type} in ${targetRegion} \u2014 cost reported as $0`
3711
- );
3712
- }
3713
- } catch (err) {
3714
- logger.error("CostEngine: failed to calculate cost for resource", {
3715
- resourceId: resource.id,
3716
- type: resource.type,
3717
- error: err instanceof Error ? err.message : String(err)
3718
- });
3719
- }
3720
- }
3721
- const totalMonthly = estimates.reduce((sum, e) => sum + e.monthly_cost, 0);
3722
- return {
3723
- provider: targetProvider,
3724
- region: targetRegion,
3725
- total_monthly: Math.round(totalMonthly * 100) / 100,
3726
- total_yearly: Math.round(totalMonthly * 12 * 100) / 100,
3727
- currency: "USD",
3728
- by_service: Object.fromEntries(
3729
- Object.entries(byService).map(([k, v]) => [k, Math.round(v * 100) / 100])
3730
- ),
3731
- by_resource: estimates,
3732
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
3733
- warnings
3734
- };
3735
- }
3736
- };
3737
-
3738
- // src/tools/estimate-cost.ts
3739
- var estimateCostSchema = z2.object({
3740
- files: z2.array(
3741
- z2.object({
3742
- path: z2.string().describe("File path"),
3743
- content: z2.string().describe("File content (HCL)")
3744
- })
3745
- ),
3746
- tfvars: z2.string().optional().describe("Contents of terraform.tfvars file"),
3747
- provider: z2.enum(["aws", "azure", "gcp"]).describe("Target cloud provider to estimate costs for"),
3748
- region: z2.string().optional().describe(
3749
- "Target region for pricing lookup. Defaults to the source region mapped to the target provider."
3750
- )
3751
- });
3752
- async function estimateCost(params, pricingEngine, config) {
3753
- const inventory = await parseTerraform(params.files, params.tfvars);
3754
- const targetProvider = params.provider;
3755
- const targetRegion = params.region ?? mapRegion(inventory.region, inventory.provider, targetProvider);
3756
- const costEngine = new CostEngine(pricingEngine, config);
3757
- const breakdown = await costEngine.calculateBreakdown(
3758
- inventory.resources,
3759
- targetProvider,
3760
- targetRegion
3761
- );
3762
- const parseWarnings = inventory.parse_warnings ?? [];
3763
- const allWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...breakdown.warnings ?? []])];
3764
- return {
3765
- ...breakdown,
3766
- parse_warnings: parseWarnings,
3767
- warnings: allWarnings
3768
- };
3769
- }
3770
-
3771
- // src/tools/compare-providers.ts
3772
- import { z as z3 } from "zod";
3773
-
3774
- // src/calculator/reserved.ts
3775
- var DISCOUNT_RATES = {
3776
- aws: [
3777
- { term: "1yr", payment: "no_upfront", rate: 0.36 },
3778
- { term: "1yr", payment: "partial_upfront", rate: 0.4 },
3779
- { term: "1yr", payment: "all_upfront", rate: 0.42 },
3780
- { term: "3yr", payment: "no_upfront", rate: 0.52 },
3781
- { term: "3yr", payment: "partial_upfront", rate: 0.57 },
3782
- { term: "3yr", payment: "all_upfront", rate: 0.6 }
3783
- ],
3784
- azure: [
3785
- { term: "1yr", payment: "all_upfront", rate: 0.35 },
3786
- { term: "3yr", payment: "all_upfront", rate: 0.55 },
3787
- // Azure does not offer no-upfront RI in the same way; partial_upfront maps
3788
- // to hybrid benefit combined, approximated here.
3789
- { term: "1yr", payment: "partial_upfront", rate: 0.3 },
3790
- { term: "3yr", payment: "partial_upfront", rate: 0.5 }
3791
- ],
3792
- gcp: [
3793
- // GCP Committed Use Discounts (CUDs) – resource-based.
3794
- { term: "1yr", payment: "all_upfront", rate: 0.28 },
3795
- { term: "3yr", payment: "all_upfront", rate: 0.55 },
3796
- { term: "1yr", payment: "partial_upfront", rate: 0.2 },
3797
- { term: "3yr", payment: "partial_upfront", rate: 0.45 }
3798
- ]
3799
- };
3800
- function calculateReservedPricing(onDemandMonthly, provider) {
3801
- const rates = DISCOUNT_RATES[provider];
3802
- const options = rates.map(({ term, payment, rate }) => {
3803
- const monthly_cost = onDemandMonthly * (1 - rate);
3804
- const monthly_savings = onDemandMonthly - monthly_cost;
3805
- return {
3806
- term,
3807
- payment,
3808
- monthly_cost: Math.round(monthly_cost * 100) / 100,
3809
- monthly_savings: Math.round(monthly_savings * 100) / 100,
3810
- percentage_savings: Math.round(rate * 1e3) / 10
3811
- // e.g. 36.0
3812
- };
3813
- });
3814
- const best_option = options.reduce(
3815
- (best, opt) => opt.monthly_savings > best.monthly_savings ? opt : best,
3816
- options[0]
3817
- );
3818
- return {
3819
- on_demand_monthly: Math.round(onDemandMonthly * 100) / 100,
3820
- options,
3821
- best_option
3822
- };
3823
- }
3824
-
3825
- // src/calculator/optimizer.ts
3826
- var LARGE_INSTANCE_SUFFIXES = [
3827
- ".xlarge",
3828
- ".2xlarge",
3829
- ".4xlarge",
3830
- ".8xlarge",
3831
- ".16xlarge",
3832
- "Standard_B4ms",
3833
- "Standard_B8ms",
3834
- "Standard_D4",
3835
- "Standard_D8",
3836
- "Standard_D16",
3837
- "Standard_E4",
3838
- "Standard_E8",
3839
- "n2-standard-4",
3840
- "n2-standard-8",
3841
- "n2-standard-16",
3842
- "e2-standard-4",
3843
- "e2-standard-8"
3844
- ];
3845
- var HIGH_UTILISATION_INDICATORS = [
3846
- "high-cpu",
3847
- "high-memory",
3848
- "production",
3849
- "prod",
3850
- "perf",
3851
- "performance"
3852
- ];
3853
- function isLikelyOversized(resource) {
3854
- const instanceType = resource.attributes.instance_type ?? resource.attributes.vm_size ?? resource.attributes.machine_type ?? "";
3855
- const isLarge = LARGE_INSTANCE_SUFFIXES.some(
3856
- (suffix) => instanceType.toLowerCase().includes(suffix.toLowerCase())
3857
- );
3858
- if (!isLarge) return false;
3859
- const tagValues = Object.values(resource.tags).map((v) => v.toLowerCase());
3860
- const tagKeys = Object.keys(resource.tags).map((k) => k.toLowerCase());
3861
- const allTagText = [...tagKeys, ...tagValues].join(" ");
3862
- const hasHighUtilisationSignal = HIGH_UTILISATION_INDICATORS.some(
3863
- (signal) => allTagText.includes(signal)
3864
- );
3865
- return !hasHighUtilisationSignal;
3866
- }
3867
- var COMPUTE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3868
- "aws_instance",
3869
- "azurerm_linux_virtual_machine",
3870
- "azurerm_windows_virtual_machine",
3871
- "google_compute_instance",
3872
- "aws_eks_cluster",
3873
- "aws_eks_node_group",
3874
- "azurerm_kubernetes_cluster",
3875
- "google_container_cluster",
3876
- "google_container_node_pool"
3877
- ]);
3878
- var DATABASE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3879
- "aws_db_instance",
3880
- "azurerm_postgresql_flexible_server",
3881
- "azurerm_mysql_flexible_server",
3882
- "google_sql_database_instance"
3883
- ]);
3884
- var OBJECT_STORAGE_RESOURCE_TYPES = /* @__PURE__ */ new Set([
3885
- "aws_s3_bucket",
3886
- "azurerm_storage_account",
3887
- "google_storage_bucket"
3888
- ]);
3889
- function generateOptimizations(estimates, resources) {
3890
- const recommendations = [];
3891
- const resourceById = new Map(
3892
- resources.map((r) => [r.id, r])
3893
- );
3894
- const estimatesByResourceId = /* @__PURE__ */ new Map();
3895
- for (const estimate of estimates) {
3896
- const existing = estimatesByResourceId.get(estimate.resource_id) ?? [];
3897
- existing.push(estimate);
3898
- estimatesByResourceId.set(estimate.resource_id, existing);
3899
- }
3900
- for (const [resourceId, resourceEstimates] of estimatesByResourceId) {
3901
- const resource = resourceById.get(resourceId);
3902
- if (!resourceEstimates.length) continue;
3903
- const current = resourceEstimates[0];
3904
- if (resource && COMPUTE_RESOURCE_TYPES.has(resource.type)) {
3905
- if (isLikelyOversized(resource) && current.monthly_cost > 0) {
3906
- const estimatedSavings = current.monthly_cost * 0.3;
3907
- recommendations.push({
3908
- resource_id: resourceId,
3909
- resource_name: current.resource_name,
3910
- type: "right_size",
3911
- description: `${resource.attributes.instance_type ?? resource.attributes.vm_size ?? "Instance"} appears over-provisioned. Consider downsizing to the next smaller instance type.`,
3912
- current_monthly_cost: current.monthly_cost,
3913
- estimated_monthly_cost: current.monthly_cost - estimatedSavings,
3914
- monthly_savings: estimatedSavings,
3915
- percentage_savings: 30,
3916
- confidence: "medium",
3917
- provider: current.provider
3918
- });
3919
- }
3920
- }
3921
- if (resource && (COMPUTE_RESOURCE_TYPES.has(resource.type) || DATABASE_RESOURCE_TYPES.has(resource.type)) && current.monthly_cost > 0) {
3922
- const reserved = calculateReservedPricing(
3923
- current.monthly_cost,
3924
- current.provider
3925
- );
3926
- const best = reserved.best_option;
3927
- if (best.percentage_savings >= 20) {
3928
- recommendations.push({
3929
- resource_id: resourceId,
3930
- resource_name: current.resource_name,
3931
- type: "reserved",
3932
- description: `Switching to a ${best.term} ${best.payment.replace(/_/g, " ")} commitment could save approximately ${best.percentage_savings}% per month.`,
3933
- current_monthly_cost: current.monthly_cost,
3934
- estimated_monthly_cost: best.monthly_cost,
3935
- monthly_savings: best.monthly_savings,
3936
- percentage_savings: best.percentage_savings,
3937
- confidence: "high",
3938
- provider: current.provider
3939
- });
3940
- }
3941
- }
3942
- if (resourceEstimates.length > 1 && current.monthly_cost > 0) {
3943
- const cheapest = resourceEstimates.reduce(
3944
- (min, e) => e.monthly_cost < min.monthly_cost ? e : min,
3945
- current
3946
- );
3947
- if (cheapest.provider !== current.provider) {
3948
- const savings = current.monthly_cost - cheapest.monthly_cost;
3949
- const pct = savings / current.monthly_cost * 100;
3950
- if (pct >= 20) {
3951
- recommendations.push({
3952
- resource_id: resourceId,
3953
- resource_name: current.resource_name,
3954
- type: "switch_provider",
3955
- description: `${cheapest.provider.toUpperCase()} offers this resource at $${cheapest.monthly_cost.toFixed(2)}/month vs $${current.monthly_cost.toFixed(2)}/month on ${current.provider.toUpperCase()}, saving approximately ${pct.toFixed(0)}%.`,
3956
- current_monthly_cost: current.monthly_cost,
3957
- estimated_monthly_cost: cheapest.monthly_cost,
3958
- monthly_savings: savings,
3959
- percentage_savings: Math.round(pct * 10) / 10,
3960
- confidence: "medium",
3961
- provider: cheapest.provider
3962
- });
3963
- }
3964
- }
3965
- }
3966
- if (resource && OBJECT_STORAGE_RESOURCE_TYPES.has(resource.type)) {
3967
- const storageClass = resource.attributes.storage_class ?? "";
3968
- const isHotTier = storageClass === "" || storageClass.toLowerCase() === "standard" || storageClass.toLowerCase() === "hot";
3969
- if (isHotTier && current.monthly_cost > 0) {
3970
- const estimatedSavings = current.monthly_cost * 0.4;
3971
- recommendations.push({
3972
- resource_id: resourceId,
3973
- resource_name: current.resource_name,
3974
- type: "storage_tier",
3975
- description: "Consider using an infrequent-access or cool storage tier if data is not accessed daily. This can reduce storage costs by ~40\u201360%.",
3976
- current_monthly_cost: current.monthly_cost,
3977
- estimated_monthly_cost: current.monthly_cost - estimatedSavings,
3978
- monthly_savings: estimatedSavings,
3979
- percentage_savings: 40,
3980
- confidence: "low",
3981
- provider: current.provider
3982
- });
3983
- }
3984
- }
3985
- }
3986
- return recommendations;
3987
- }
3988
-
3989
- // src/reporting/markdown-report.ts
3990
- function formatUsd(amount) {
3991
- return `$${amount.toFixed(2)}`;
3992
- }
3993
- function providerLabel(provider) {
3994
- switch (provider) {
3995
- case "aws":
3996
- return "AWS";
3997
- case "azure":
3998
- return "Azure";
3999
- case "gcp":
4000
- return "GCP";
4001
- }
4002
- }
4003
- function generateMarkdownReport(comparison, resources, options = {}, parseWarnings = []) {
4004
- const includeBreakdown = options.include_breakdown !== false;
4005
- const includeRecommendations = options.include_recommendations !== false;
4006
- const monthlyHours = options.monthly_hours ?? 730;
4007
- const lines = [];
4008
- const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4009
- lines.push(
4010
- `# Cloud Cost Comparison Report`,
4011
- ``,
4012
- `**Generated:** ${now} `,
4013
- `**Source provider:** ${providerLabel(comparison.source_provider)} `,
4014
- `**Resources analysed:** ${resources.length}`,
4015
- ``
4016
- );
4017
- lines.push(`## Summary`, ``);
4018
- const sourceBreakdown = comparison.comparisons.find(
4019
- (b) => b.provider === comparison.source_provider
4020
- );
4021
- const sourceMonthly = sourceBreakdown?.total_monthly ?? 0;
4022
- lines.push(
4023
- `| Provider | Monthly Cost | Yearly Cost | vs Source |`,
4024
- `|----------|-------------|------------|-----------|`
4025
- );
4026
- for (const breakdown of comparison.comparisons) {
4027
- const diff = breakdown.total_monthly - sourceMonthly;
4028
- const diffLabel = breakdown.provider === comparison.source_provider ? "\u2014" : diff < 0 ? `${formatUsd(Math.abs(diff))} cheaper` : diff > 0 ? `${formatUsd(diff)} more expensive` : "same cost";
4029
- lines.push(
4030
- `| ${providerLabel(breakdown.provider)} | ${formatUsd(breakdown.total_monthly)} | ${formatUsd(breakdown.total_yearly)} | ${diffLabel} |`
4031
- );
4032
- }
4033
- lines.push(``);
4034
- if (comparison.savings_summary.length > 0) {
4035
- lines.push(`## Savings Potential`, ``);
4036
- lines.push(
4037
- `| Provider | Monthly Cost | Savings from Source | Savings % |`,
4038
- `|----------|-------------|--------------------|-----------| `
4039
- );
4040
- for (const s of comparison.savings_summary) {
4041
- const savings = s.difference_from_source;
4042
- const savingsLabel = savings < 0 ? formatUsd(Math.abs(savings)) : `-${formatUsd(savings)}`;
4043
- const pct = s.percentage_difference < 0 ? `${Math.abs(s.percentage_difference).toFixed(1)}% cheaper` : `${s.percentage_difference.toFixed(1)}% more`;
4044
- lines.push(
4045
- `| ${providerLabel(s.provider)} | ${formatUsd(s.total_monthly)} | ${savingsLabel} | ${pct} |`
4046
- );
4047
- }
4048
- lines.push(``);
4049
- }
4050
- if (includeBreakdown && resources.length > 0) {
4051
- lines.push(`## Resource Breakdown`, ``);
4052
- const costMatrix = /* @__PURE__ */ new Map();
4053
- for (const breakdown of comparison.comparisons) {
4054
- for (const estimate of breakdown.by_resource) {
4055
- const row = costMatrix.get(estimate.resource_id) ?? {};
4056
- row[breakdown.provider] = estimate.monthly_cost;
4057
- costMatrix.set(estimate.resource_id, row);
4058
- }
4059
- }
4060
- const presentProviders = comparison.comparisons.map((b) => b.provider);
4061
- const headerCols = presentProviders.map((p) => providerLabel(p)).join(" | ");
4062
- lines.push(
4063
- `| Resource | Type | ${headerCols} |`,
4064
- `|----------|------|${presentProviders.map(() => "------").join("|")}|`
4065
- );
4066
- for (const resource of resources) {
4067
- const costs = costMatrix.get(resource.id) ?? {};
4068
- const costCols = presentProviders.map((p) => formatUsd(costs[p] ?? 0)).join(" | ");
4069
- lines.push(
4070
- `| ${resource.name} | ${resource.type} | ${costCols} |`
4071
- );
4072
- }
4073
- lines.push(``);
4074
- }
4075
- if (includeRecommendations) {
4076
- const allEstimates = comparison.comparisons.flatMap((b) => b.by_resource);
4077
- const recommendations = generateOptimizations(allEstimates, resources);
4078
- if (recommendations.length > 0) {
4079
- lines.push(`## Savings Recommendations`, ``);
4080
- for (const rec of recommendations) {
4081
- const savingsLabel = `${formatUsd(rec.monthly_savings)}/month (${rec.percentage_savings}%)`;
4082
- lines.push(
4083
- `### ${rec.resource_name} \u2013 ${rec.type.replace(/_/g, " ")}`,
4084
- ``,
4085
- `${rec.description}`,
4086
- ``,
4087
- `- **Current cost:** ${formatUsd(rec.current_monthly_cost)}/month`,
4088
- `- **Estimated after:** ${formatUsd(rec.estimated_monthly_cost)}/month`,
4089
- `- **Potential savings:** ${savingsLabel}`,
4090
- `- **Confidence:** ${rec.confidence}`,
4091
- ``
4092
- );
4093
- }
4094
- } else {
4095
- lines.push(`## Savings Recommendations`, ``, `No recommendations at this time.`, ``);
4096
- }
4097
- }
4098
- lines.push(`## Assumptions`, ``);
4099
- lines.push(
4100
- `| Parameter | Value |`,
4101
- `|-----------|-------|`,
4102
- `| Pricing model | On-demand (no reserved or spot instances) |`,
4103
- `| Operating system | Linux (unless specified in resource attributes) |`,
4104
- `| Monthly hours | ${monthlyHours} |`,
4105
- `| Currency | USD |`,
4106
- `| Pricing source | Mix of live API and fallback/bundled data |`,
4107
- `| Data transfer costs | Not included |`,
4108
- `| Tax | Not included |`,
4109
- ``
4110
- );
4111
- const allDataWarnings = comparison.comparisons.flatMap((b) => b.warnings ?? []);
4112
- const allWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...allDataWarnings])];
4113
- if (allWarnings.length > 0) {
4114
- lines.push(`## Limitations`, ``);
4115
- lines.push(
4116
- `> The following issues were detected during analysis that may affect estimate accuracy:`,
4117
- ``
4118
- );
4119
- for (const w of allWarnings) {
4120
- lines.push(`- ${w}`);
4121
- }
4122
- lines.push(``);
4123
- }
4124
- return lines.join("\n");
4125
- }
4126
-
4127
- // src/reporting/json-report.ts
4128
- function generateJsonReport(comparison, resources, monthlyHours = 730, parseWarnings = []) {
4129
- const sourcesSet = /* @__PURE__ */ new Set();
4130
- for (const breakdown of comparison.comparisons) {
4131
- for (const estimate of breakdown.by_resource) {
4132
- const src = estimate.pricing_source;
4133
- if (src) {
4134
- sourcesSet.add(`${breakdown.provider}_${src}`);
4135
- }
4136
- }
4137
- }
4138
- const allDataWarnings = comparison.comparisons.flatMap((b) => b.warnings ?? []);
4139
- const allWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...allDataWarnings])];
4140
- const output = {
4141
- metadata: {
4142
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
4143
- pricing_sources_used: Array.from(sourcesSet).sort(),
4144
- assumptions: {
4145
- pricing_model: "on-demand",
4146
- operating_system: "Linux (unless specified)",
4147
- monthly_hours: monthlyHours,
4148
- currency: "USD",
4149
- data_transfer_costs: "not included",
4150
- tax: "not included"
4151
- },
4152
- warnings: allWarnings
4153
- },
4154
- ...comparison,
4155
- resource_summary: {
4156
- total: resources.length,
4157
- by_type: resources.reduce((acc, r) => {
4158
- acc[r.type] = (acc[r.type] ?? 0) + 1;
4159
- return acc;
4160
- }, {})
4161
- }
4162
- };
4163
- return JSON.stringify(output, null, 2);
4164
- }
4165
-
4166
- // src/reporting/csv-report.ts
4167
- var PROVIDERS = ["aws", "azure", "gcp"];
4168
- function csvEscape(value) {
4169
- const str2 = String(value);
4170
- if (str2.includes(",") || str2.includes("\n") || str2.includes('"')) {
4171
- return `"${str2.replace(/"/g, '""')}"`;
4172
- }
4173
- return str2;
4174
- }
4175
- function generateCsvReport(comparison, resources) {
4176
- const lines = [];
4177
- lines.push("# Prices are estimates based on on-demand pricing. See assumptions in JSON/Markdown reports.");
4178
- lines.push("Resource,Type,AWS Monthly,Azure Monthly,GCP Monthly,Cheapest");
4179
- const costByProviderAndResource = /* @__PURE__ */ new Map();
4180
- for (const breakdown of comparison.comparisons) {
4181
- const byResource = /* @__PURE__ */ new Map();
4182
- for (const estimate of breakdown.by_resource) {
4183
- byResource.set(estimate.resource_id, estimate.monthly_cost);
4184
- }
4185
- costByProviderAndResource.set(breakdown.provider, byResource);
4186
- }
4187
- for (const resource of resources) {
4188
- const costs = {};
4189
- for (const provider of PROVIDERS) {
4190
- const providerMap = costByProviderAndResource.get(provider);
4191
- if (providerMap) {
4192
- costs[provider] = providerMap.get(resource.id) ?? 0;
4193
- }
4194
- }
4195
- let cheapest = "N/A";
4196
- let lowestCost = Infinity;
4197
- for (const provider of PROVIDERS) {
4198
- const cost = costs[provider] ?? 0;
4199
- if (cost > 0 && cost < lowestCost) {
4200
- lowestCost = cost;
4201
- cheapest = provider.toUpperCase();
4202
- }
4203
- }
4204
- const row = [
4205
- csvEscape(resource.name),
4206
- csvEscape(resource.type),
4207
- csvEscape((costs.aws ?? 0).toFixed(2)),
4208
- csvEscape((costs.azure ?? 0).toFixed(2)),
4209
- csvEscape((costs.gcp ?? 0).toFixed(2)),
4210
- csvEscape(cheapest)
4211
- ];
4212
- lines.push(row.join(","));
4213
- }
4214
- return lines.join("\n");
4215
- }
3
+ PricingCache,
4
+ PricingEngine,
5
+ analyzeTerraform,
6
+ analyzeTerraformSchema,
7
+ compareProviders,
8
+ compareProvidersSchema,
9
+ estimateCost,
10
+ estimateCostSchema,
11
+ loadConfig,
12
+ logger,
13
+ mapInstance,
14
+ optimizeCost,
15
+ optimizeCostSchema,
16
+ setLogLevel,
17
+ whatIf,
18
+ whatIfSchema
19
+ } from "./chunk-EI63JCAA.js";
20
+ import {
21
+ getResourceEquivalents
22
+ } from "./chunk-XDBTEI42.js";
4216
23
 
4217
- // src/tools/compare-providers.ts
4218
- var compareProvidersSchema = z3.object({
4219
- files: z3.array(
4220
- z3.object({
4221
- path: z3.string().describe("File path"),
4222
- content: z3.string().describe("File content (HCL)")
4223
- })
4224
- ),
4225
- tfvars: z3.string().optional().describe("Contents of terraform.tfvars file"),
4226
- format: z3.enum(["markdown", "json", "csv"]).default("markdown").describe("Output report format"),
4227
- providers: z3.array(z3.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Cloud providers to include in the comparison")
4228
- });
4229
- async function compareProviders(params, pricingEngine, config) {
4230
- const inventory = await parseTerraform(params.files, params.tfvars);
4231
- const sourceProvider = inventory.provider;
4232
- const costEngine = new CostEngine(pricingEngine, config);
4233
- const comparisons = [];
4234
- for (const provider of params.providers) {
4235
- const targetRegion = mapRegion(inventory.region, sourceProvider, provider);
4236
- const breakdown = await costEngine.calculateBreakdown(
4237
- inventory.resources,
4238
- provider,
4239
- targetRegion
4240
- );
4241
- comparisons.push(breakdown);
4242
- }
4243
- const sourceBreakdown = comparisons.find((b) => b.provider === sourceProvider) ?? comparisons[0];
4244
- const sourceMonthly = sourceBreakdown?.total_monthly ?? 0;
4245
- const savingsSummary = comparisons.map((b) => {
4246
- const diff = b.total_monthly - sourceMonthly;
4247
- const pct = sourceMonthly !== 0 ? diff / sourceMonthly * 100 : 0;
4248
- return {
4249
- provider: b.provider,
4250
- total_monthly: b.total_monthly,
4251
- difference_from_source: Math.round(diff * 100) / 100,
4252
- percentage_difference: Math.round(pct * 10) / 10
4253
- };
4254
- });
4255
- const comparison = {
4256
- source_provider: sourceProvider,
4257
- comparisons,
4258
- savings_summary: savingsSummary,
4259
- generated_at: (/* @__PURE__ */ new Date()).toISOString()
4260
- };
4261
- const allWarnings = comparisons.flatMap((b) => b.warnings ?? []);
4262
- const parseWarnings = inventory.parse_warnings ?? [];
4263
- const monthlyHours = config.pricing.monthly_hours;
4264
- let report;
4265
- switch (params.format) {
4266
- case "json":
4267
- report = generateJsonReport(comparison, inventory.resources, monthlyHours, parseWarnings);
4268
- break;
4269
- case "csv":
4270
- report = generateCsvReport(comparison, inventory.resources);
4271
- break;
4272
- case "markdown":
4273
- default:
4274
- report = generateMarkdownReport(
4275
- comparison,
4276
- inventory.resources,
4277
- {},
4278
- parseWarnings
4279
- );
4280
- break;
4281
- }
4282
- return {
4283
- report,
4284
- format: params.format,
4285
- comparison,
4286
- warnings: [.../* @__PURE__ */ new Set([...parseWarnings, ...allWarnings])]
4287
- };
4288
- }
24
+ // src/server.ts
25
+ import { createRequire } from "module";
26
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
27
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4289
28
 
4290
29
  // src/tools/get-equivalents.ts
4291
- import { z as z4 } from "zod";
30
+ import { z } from "zod";
4292
31
 
4293
32
  // src/mapping/resource-mapper.ts
4294
33
  function findEquivalent(resourceType, sourceProvider, targetProvider) {
@@ -4320,15 +59,15 @@ function findAllEquivalents(resourceType, sourceProvider) {
4320
59
  }
4321
60
 
4322
61
  // src/tools/get-equivalents.ts
4323
- var getEquivalentsSchema = z4.object({
4324
- resource_type: z4.string().describe(
62
+ var getEquivalentsSchema = z.object({
63
+ resource_type: z.string().describe(
4325
64
  "Terraform resource type to look up (e.g. aws_instance, google_compute_instance)"
4326
65
  ),
4327
- source_provider: z4.enum(["aws", "azure", "gcp"]).describe("Cloud provider the resource type belongs to"),
4328
- target_provider: z4.enum(["aws", "azure", "gcp"]).optional().describe(
66
+ source_provider: z.enum(["aws", "azure", "gcp"]).describe("Cloud provider the resource type belongs to"),
67
+ target_provider: z.enum(["aws", "azure", "gcp"]).optional().describe(
4329
68
  "Specific target provider. When omitted, equivalents for all providers are returned."
4330
69
  ),
4331
- instance_type: z4.string().optional().describe(
70
+ instance_type: z.string().optional().describe(
4332
71
  "Instance type / VM size to also map across providers (e.g. t3.large, Standard_D4s_v3)"
4333
72
  )
4334
73
  });
@@ -4371,14 +110,14 @@ async function getEquivalents(params) {
4371
110
  }
4372
111
 
4373
112
  // src/tools/get-pricing.ts
4374
- import { z as z5 } from "zod";
4375
- var getPricingSchema = z5.object({
4376
- provider: z5.enum(["aws", "azure", "gcp"]).describe("Cloud provider to look up pricing for"),
4377
- service: z5.enum(["compute", "database", "storage", "network", "kubernetes"]).describe("Service category"),
4378
- resource_type: z5.string().describe(
113
+ import { z as z2 } from "zod";
114
+ var getPricingSchema = z2.object({
115
+ provider: z2.enum(["aws", "azure", "gcp"]).describe("Cloud provider to look up pricing for"),
116
+ service: z2.enum(["compute", "database", "storage", "network", "load_balancer", "nat_gateway", "kubernetes"]).describe("Service category (network defaults to nat_gateway for backward compatibility)"),
117
+ resource_type: z2.string().describe(
4379
118
  "Instance type, storage type, or resource identifier (e.g. t3.large, gp3, Standard_D4s_v3)"
4380
119
  ),
4381
- region: z5.string().describe("Cloud region (e.g. us-east-1, eastus, us-central1)")
120
+ region: z2.string().describe("Cloud region (e.g. us-east-1, eastus, us-central1)")
4382
121
  });
4383
122
  async function getPricing(params, pricingEngine) {
4384
123
  const provider = params.provider;
@@ -4387,6 +126,8 @@ async function getPricing(params, pricingEngine) {
4387
126
  database: "database",
4388
127
  storage: "storage",
4389
128
  network: "nat-gateway",
129
+ load_balancer: "load-balancer",
130
+ nat_gateway: "nat-gateway",
4390
131
  kubernetes: "kubernetes"
4391
132
  };
4392
133
  const service = serviceMap[params.service] ?? params.service;
@@ -4405,53 +146,6 @@ async function getPricing(params, pricingEngine) {
4405
146
  };
4406
147
  }
4407
148
 
4408
- // src/tools/optimize-cost.ts
4409
- import { z as z6 } from "zod";
4410
- var optimizeCostSchema = z6.object({
4411
- files: z6.array(
4412
- z6.object({
4413
- path: z6.string().describe("File path"),
4414
- content: z6.string().describe("File content (HCL)")
4415
- })
4416
- ),
4417
- tfvars: z6.string().optional().describe("Contents of terraform.tfvars file"),
4418
- providers: z6.array(z6.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Providers to calculate costs against for cross-provider recommendations")
4419
- });
4420
- async function optimizeCost(params, pricingEngine, config) {
4421
- const inventory = await parseTerraform(params.files, params.tfvars);
4422
- const sourceProvider = inventory.provider;
4423
- const costEngine = new CostEngine(pricingEngine, config);
4424
- const allEstimates = [];
4425
- for (const provider of params.providers) {
4426
- const targetRegion = mapRegion(inventory.region, sourceProvider, provider);
4427
- const breakdown = await costEngine.calculateBreakdown(
4428
- inventory.resources,
4429
- provider,
4430
- targetRegion
4431
- );
4432
- allEstimates.push(...breakdown.by_resource);
4433
- }
4434
- const recommendations = generateOptimizations(allEstimates, inventory.resources);
4435
- const sourceEstimates = allEstimates.filter(
4436
- (e) => e.provider === sourceProvider && e.monthly_cost > 0
4437
- );
4438
- const reservedPricing = sourceEstimates.map((estimate) => ({
4439
- resource_id: estimate.resource_id,
4440
- resource_name: estimate.resource_name,
4441
- resource_type: estimate.resource_type,
4442
- provider: estimate.provider,
4443
- ...calculateReservedPricing(estimate.monthly_cost, estimate.provider)
4444
- }));
4445
- const totalPotentialSavings = recommendations.reduce((sum, r) => sum + r.monthly_savings, 0);
4446
- return {
4447
- recommendations,
4448
- reserved_pricing: reservedPricing,
4449
- total_potential_savings: Math.round(totalPotentialSavings * 100) / 100,
4450
- recommendation_count: recommendations.length,
4451
- resource_count: inventory.resources.length
4452
- };
4453
- }
4454
-
4455
149
  // src/tools/index.ts
4456
150
  function registerTools(server, config) {
4457
151
  const cache = new PricingCache(config.cache.db_path);
@@ -4522,15 +216,28 @@ function registerTools(server, config) {
4522
216
  };
4523
217
  }
4524
218
  );
219
+ server.tool(
220
+ "what_if",
221
+ "Estimate cost impact of infrastructure changes without modifying Terraform files. Applies attribute overrides to a cloned resource set and returns a per-resource and aggregate cost diff.",
222
+ whatIfSchema.shape,
223
+ async (params) => {
224
+ const result = await whatIf(params, pricingEngine, config);
225
+ return {
226
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
227
+ };
228
+ }
229
+ );
4525
230
  }
4526
231
 
4527
232
  // src/server.ts
233
+ var require2 = createRequire(import.meta.url);
234
+ var { version } = require2("../package.json");
4528
235
  async function startServer() {
4529
236
  const config = loadConfig();
4530
237
  setLogLevel(config.logging.level);
4531
238
  const server = new McpServer({
4532
239
  name: "cloudcost-mcp",
4533
- version: "0.1.0"
240
+ version
4534
241
  });
4535
242
  registerTools(server, config);
4536
243
  const transport = new StdioServerTransport();