@jadenrazo/cloudcost-mcp 0.2.0 → 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.
@@ -40,6 +40,7 @@ var _gcpComputePricing = null;
40
40
  var _gcpSqlPricing = null;
41
41
  var _gcpStoragePricing = null;
42
42
  var _gcpDiskPricing = null;
43
+ var _regionPriceMultipliers = null;
43
44
  function getResourceEquivalents() {
44
45
  if (_resourceEquivalents === null) {
45
46
  _resourceEquivalents = loadJsonFile(
@@ -122,6 +123,14 @@ function getGcpDiskPricing() {
122
123
  }
123
124
  return _gcpDiskPricing;
124
125
  }
126
+ function getRegionPriceMultipliers() {
127
+ if (_regionPriceMultipliers === null) {
128
+ _regionPriceMultipliers = loadJsonFile(
129
+ "region-price-multipliers.json"
130
+ );
131
+ }
132
+ return _regionPriceMultipliers;
133
+ }
125
134
  function _resetLoaderCache() {
126
135
  _resourceEquivalents = null;
127
136
  _instanceMap = null;
@@ -134,6 +143,7 @@ function _resetLoaderCache() {
134
143
  _gcpSqlPricing = null;
135
144
  _gcpStoragePricing = null;
136
145
  _gcpDiskPricing = null;
146
+ _regionPriceMultipliers = null;
137
147
  }
138
148
 
139
149
  export {
@@ -148,6 +158,7 @@ export {
148
158
  getGcpSqlPricing,
149
159
  getGcpStoragePricing,
150
160
  getGcpDiskPricing,
161
+ getRegionPriceMultipliers,
151
162
  _resetLoaderCache
152
163
  };
153
- //# sourceMappingURL=chunk-KZJSZMWM.js.map
164
+ //# sourceMappingURL=chunk-XDBTEI42.js.map
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,345 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+ import {
4
+ PricingCache,
5
+ PricingEngine,
6
+ analyzeTerraform,
7
+ compareProviders,
8
+ estimateCost,
9
+ loadConfig,
10
+ optimizeCost,
11
+ whatIf
12
+ } from "./chunk-EI63JCAA.js";
13
+ import "./chunk-XDBTEI42.js";
14
+
15
+ // src/cli.ts
16
+ import { readFileSync, existsSync, statSync } from "fs";
17
+ import { resolve, extname, join } from "path";
18
+ import { readdirSync } from "fs";
19
+ function parseArgs(argv) {
20
+ const args = argv.slice(2);
21
+ const parsed = {
22
+ command: void 0,
23
+ path: void 0,
24
+ provider: "aws",
25
+ region: void 0,
26
+ format: "markdown",
27
+ providers: ["aws", "azure", "gcp"],
28
+ changes: void 0,
29
+ json: false,
30
+ help: false
31
+ };
32
+ let i = 0;
33
+ while (i < args.length) {
34
+ const arg = args[i];
35
+ if (arg === "--help" || arg === "-h") {
36
+ parsed.help = true;
37
+ } else if (arg === "--json") {
38
+ parsed.json = true;
39
+ } else if (arg === "--provider" && args[i + 1]) {
40
+ parsed.provider = args[++i];
41
+ } else if (arg === "--region" && args[i + 1]) {
42
+ parsed.region = args[++i];
43
+ } else if (arg === "--format" && args[i + 1]) {
44
+ parsed.format = args[++i];
45
+ } else if (arg === "--providers" && args[i + 1]) {
46
+ parsed.providers = args[++i].split(",").map((p) => p.trim());
47
+ } else if (arg === "--changes" && args[i + 1]) {
48
+ parsed.changes = args[++i];
49
+ } else if (!arg.startsWith("--")) {
50
+ if (!parsed.command) {
51
+ parsed.command = arg;
52
+ } else if (!parsed.path) {
53
+ parsed.path = arg;
54
+ }
55
+ }
56
+ i++;
57
+ }
58
+ return parsed;
59
+ }
60
+ function loadTerraformFiles(inputPath) {
61
+ const resolved = resolve(inputPath);
62
+ if (!existsSync(resolved)) {
63
+ throw new Error(`Path not found: ${resolved}`);
64
+ }
65
+ const stat = statSync(resolved);
66
+ if (stat.isFile()) {
67
+ return [
68
+ {
69
+ path: resolved,
70
+ content: readFileSync(resolved, "utf-8")
71
+ }
72
+ ];
73
+ }
74
+ if (stat.isDirectory()) {
75
+ const entries = readdirSync(resolved);
76
+ const tfFiles = entries.filter((e) => extname(e) === ".tf" || extname(e) === ".tofu").map((e) => join(resolved, e));
77
+ if (tfFiles.length === 0) {
78
+ throw new Error(`No .tf or .tofu files found in directory: ${resolved}`);
79
+ }
80
+ return tfFiles.map((f) => ({
81
+ path: f,
82
+ content: readFileSync(f, "utf-8")
83
+ }));
84
+ }
85
+ throw new Error(`Path is neither a file nor directory: ${resolved}`);
86
+ }
87
+ function loadTfvars(inputPath) {
88
+ const dir = statSync(resolve(inputPath)).isDirectory() ? resolve(inputPath) : resolve(inputPath, "..");
89
+ const tfvarsPath = join(dir, "terraform.tfvars");
90
+ if (existsSync(tfvarsPath)) {
91
+ return readFileSync(tfvarsPath, "utf-8");
92
+ }
93
+ return void 0;
94
+ }
95
+ function printJson(data) {
96
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
97
+ }
98
+ function printText(data) {
99
+ if (typeof data === "string") {
100
+ process.stdout.write(data + "\n");
101
+ return;
102
+ }
103
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
104
+ }
105
+ function printUsage() {
106
+ process.stdout.write(`
107
+ CloudCost \u2014 multi-cloud Terraform cost estimator
108
+
109
+ Usage:
110
+ cloudcost analyze <path> Parse Terraform and show resource inventory
111
+ cloudcost estimate <path> [options] Estimate costs on a specific provider
112
+ cloudcost compare <path> [options] Compare costs across all providers
113
+ cloudcost optimize <path> [options] Show cost optimization recommendations
114
+ cloudcost what-if <path> --changes <file> Simulate attribute changes and show cost impact
115
+
116
+ Options:
117
+ --provider <aws|azure|gcp> Target provider (default: aws)
118
+ --region <region> Target region (default: auto-detect)
119
+ --format <markdown|json|csv> Report format for compare (default: markdown)
120
+ --providers <aws,azure,gcp> Comma-separated providers for compare/optimize
121
+ --changes <path> JSON file with changes array for what-if
122
+ --json Output raw JSON
123
+ --help, -h Show this help
124
+
125
+ Examples:
126
+ cloudcost analyze ./terraform
127
+ cloudcost estimate ./terraform --provider aws --region us-east-1
128
+ cloudcost compare ./terraform --format markdown
129
+ cloudcost optimize ./terraform --providers aws,gcp
130
+ cloudcost estimate main.tf --provider gcp --json
131
+ cloudcost what-if ./terraform --changes changes.json --provider aws
132
+ `.trimStart());
133
+ }
134
+ async function runAnalyze(args, files, tfvars) {
135
+ const result = await analyzeTerraform({ files, tfvars, include_dependencies: false });
136
+ if (args.json) {
137
+ printJson(result);
138
+ } else {
139
+ printText(result);
140
+ }
141
+ }
142
+ async function runEstimate(args, files, tfvars, pricingEngine, config) {
143
+ const validProviders = ["aws", "azure", "gcp"];
144
+ if (!validProviders.includes(args.provider)) {
145
+ throw new Error(`Invalid provider "${args.provider}". Must be one of: aws, azure, gcp`);
146
+ }
147
+ const result = await estimateCost(
148
+ {
149
+ files,
150
+ tfvars,
151
+ provider: args.provider,
152
+ region: args.region,
153
+ currency: "USD"
154
+ },
155
+ pricingEngine,
156
+ config
157
+ );
158
+ if (args.json) {
159
+ printJson(result);
160
+ } else {
161
+ printText(result);
162
+ }
163
+ }
164
+ async function runCompare(args, files, tfvars, pricingEngine, config) {
165
+ const validFormats = ["markdown", "json", "csv"];
166
+ if (!validFormats.includes(args.format)) {
167
+ throw new Error(`Invalid format "${args.format}". Must be one of: markdown, json, csv`);
168
+ }
169
+ const validatedProviders = args.providers.filter(
170
+ (p) => p === "aws" || p === "azure" || p === "gcp"
171
+ );
172
+ const result = await compareProviders(
173
+ {
174
+ files,
175
+ tfvars,
176
+ format: args.format,
177
+ currency: "USD",
178
+ providers: validatedProviders.length > 0 ? validatedProviders : ["aws", "azure", "gcp"]
179
+ },
180
+ pricingEngine,
181
+ config
182
+ );
183
+ if (args.json) {
184
+ printJson(result);
185
+ } else {
186
+ const obj = result;
187
+ if (typeof obj["report"] === "string") {
188
+ printText(obj["report"]);
189
+ } else {
190
+ printText(result);
191
+ }
192
+ }
193
+ }
194
+ async function runOptimize(args, files, tfvars, pricingEngine, config) {
195
+ const validatedProviders = args.providers.filter(
196
+ (p) => p === "aws" || p === "azure" || p === "gcp"
197
+ );
198
+ const result = await optimizeCost(
199
+ {
200
+ files,
201
+ tfvars,
202
+ providers: validatedProviders.length > 0 ? validatedProviders : ["aws", "azure", "gcp"]
203
+ },
204
+ pricingEngine,
205
+ config
206
+ );
207
+ if (args.json) {
208
+ printJson(result);
209
+ } else {
210
+ printText(result);
211
+ }
212
+ }
213
+ async function runWhatIf(args, files, tfvars, pricingEngine, config) {
214
+ if (!args.changes) {
215
+ throw new Error(
216
+ 'The what-if command requires --changes <path> pointing to a JSON file with a "changes" array'
217
+ );
218
+ }
219
+ const changesPath = resolve(args.changes);
220
+ if (!existsSync(changesPath)) {
221
+ throw new Error(`Changes file not found: ${changesPath}`);
222
+ }
223
+ let parsedChanges;
224
+ try {
225
+ parsedChanges = JSON.parse(readFileSync(changesPath, "utf-8"));
226
+ } catch (err) {
227
+ throw new Error(
228
+ `Failed to parse changes file "${changesPath}": ${err instanceof Error ? err.message : String(err)}`
229
+ );
230
+ }
231
+ let changesArray;
232
+ if (Array.isArray(parsedChanges)) {
233
+ changesArray = parsedChanges;
234
+ } else if (parsedChanges !== null && typeof parsedChanges === "object" && "changes" in parsedChanges && Array.isArray(parsedChanges["changes"])) {
235
+ changesArray = parsedChanges["changes"];
236
+ } else {
237
+ throw new Error(
238
+ `Changes file must contain a JSON array or an object with a "changes" array`
239
+ );
240
+ }
241
+ const changes = changesArray.map((item, index) => {
242
+ if (item === null || typeof item !== "object") {
243
+ throw new Error(`Change at index ${index} must be an object`);
244
+ }
245
+ const c = item;
246
+ if (typeof c["resource_id"] !== "string" || !c["resource_id"]) {
247
+ throw new Error(`Change at index ${index} is missing a valid "resource_id"`);
248
+ }
249
+ if (typeof c["attribute"] !== "string" || !c["attribute"]) {
250
+ throw new Error(`Change at index ${index} is missing a valid "attribute"`);
251
+ }
252
+ if (typeof c["new_value"] !== "string" && typeof c["new_value"] !== "number") {
253
+ throw new Error(
254
+ `Change at index ${index} "new_value" must be a string or number`
255
+ );
256
+ }
257
+ return {
258
+ resource_id: c["resource_id"],
259
+ attribute: c["attribute"],
260
+ new_value: c["new_value"]
261
+ };
262
+ });
263
+ const validProviders = ["aws", "azure", "gcp"];
264
+ const provider = validProviders.includes(args.provider) ? args.provider : void 0;
265
+ const result = await whatIf(
266
+ {
267
+ files,
268
+ tfvars,
269
+ changes,
270
+ provider,
271
+ region: args.region
272
+ },
273
+ pricingEngine,
274
+ config
275
+ );
276
+ if (args.json) {
277
+ printJson(result);
278
+ } else {
279
+ printText(result);
280
+ }
281
+ }
282
+ async function main() {
283
+ const args = parseArgs(process.argv);
284
+ if (args.help || !args.command) {
285
+ printUsage();
286
+ process.exit(args.help ? 0 : 1);
287
+ }
288
+ const validCommands = ["analyze", "estimate", "compare", "optimize", "what-if"];
289
+ if (!validCommands.includes(args.command)) {
290
+ process.stderr.write(`Unknown command: ${args.command}
291
+ `);
292
+ printUsage();
293
+ process.exit(1);
294
+ }
295
+ if (!args.path) {
296
+ process.stderr.write(`Error: path argument is required
297
+ `);
298
+ printUsage();
299
+ process.exit(1);
300
+ }
301
+ let files;
302
+ let tfvars;
303
+ try {
304
+ files = loadTerraformFiles(args.path);
305
+ tfvars = loadTfvars(args.path);
306
+ } catch (err) {
307
+ process.stderr.write(`Error loading Terraform files: ${err instanceof Error ? err.message : String(err)}
308
+ `);
309
+ process.exit(1);
310
+ }
311
+ const config = loadConfig();
312
+ const cache = new PricingCache(config.cache.db_path);
313
+ const pricingEngine = new PricingEngine(cache, config);
314
+ try {
315
+ switch (args.command) {
316
+ case "analyze":
317
+ await runAnalyze(args, files, tfvars);
318
+ break;
319
+ case "estimate":
320
+ await runEstimate(args, files, tfvars, pricingEngine, config);
321
+ break;
322
+ case "compare":
323
+ await runCompare(args, files, tfvars, pricingEngine, config);
324
+ break;
325
+ case "optimize":
326
+ await runOptimize(args, files, tfvars, pricingEngine, config);
327
+ break;
328
+ case "what-if":
329
+ await runWhatIf(args, files, tfvars, pricingEngine, config);
330
+ break;
331
+ }
332
+ } catch (err) {
333
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
334
+ `);
335
+ process.exit(1);
336
+ } finally {
337
+ cache.close();
338
+ }
339
+ }
340
+ main().catch((err) => {
341
+ process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}
342
+ `);
343
+ process.exit(1);
344
+ });
345
+ //# sourceMappingURL=cli.js.map