@jadenrazo/cloudcost-mcp 1.0.1 → 1.2.1

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.
@@ -132,6 +132,23 @@ function getFallbackMetadata(provider) {
132
132
  }
133
133
  return _fallbackMeta[provider];
134
134
  }
135
+ var FRESH_MAX_AGE_DAYS = 14;
136
+ var AGING_MAX_AGE_DAYS = 45;
137
+ var MS_PER_DAY = 864e5;
138
+ function getPricingMetadata(provider, now = /* @__PURE__ */ new Date()) {
139
+ const meta = getFallbackMetadata(provider);
140
+ const source = meta?.source ?? "unknown";
141
+ if (!meta?.last_updated) {
142
+ return { source, as_of: null, age_days: null, staleness: "stale" };
143
+ }
144
+ const asOf = new Date(meta.last_updated);
145
+ if (Number.isNaN(asOf.getTime())) {
146
+ return { source, as_of: meta.last_updated, age_days: null, staleness: "stale" };
147
+ }
148
+ const ageDays = Math.max(0, Math.floor((now.getTime() - asOf.getTime()) / MS_PER_DAY));
149
+ const staleness = ageDays < FRESH_MAX_AGE_DAYS ? "fresh" : ageDays < AGING_MAX_AGE_DAYS ? "aging" : "stale";
150
+ return { source, as_of: meta.last_updated, age_days: ageDays, staleness };
151
+ }
135
152
  function _resetLoaderCache() {
136
153
  _resourceEquivalents = null;
137
154
  _instanceMap = null;
@@ -161,6 +178,7 @@ export {
161
178
  getGcpDiskPricing,
162
179
  getRegionPriceMultipliers,
163
180
  getFallbackMetadata,
181
+ getPricingMetadata,
164
182
  _resetLoaderCache
165
183
  };
166
- //# sourceMappingURL=chunk-MNFT5YKN.js.map
184
+ //# sourceMappingURL=chunk-LWTVE7UY.js.map
package/dist/cli.d.ts CHANGED
@@ -1 +1,46 @@
1
- #!/usr/bin/env node
1
+ /**
2
+ * CloudCost MCP CLI
3
+ *
4
+ * NOTE: do not add a `#!/usr/bin/env node` line here. tsup injects one into
5
+ * every bundle via `banner.js` in tsup.config.ts; a second copy in this source
6
+ * file lands on line 2 of dist/cli.js, where Node does not strip it, and the
7
+ * binary dies with `SyntaxError: Invalid or unexpected token`.
8
+ *
9
+ * Standalone command-line interface for the CloudCost MCP server tools.
10
+ *
11
+ * Usage:
12
+ * cloudcost analyze <path>
13
+ * cloudcost estimate <path> [--provider aws|azure|gcp] [--region <region>] [--currency <code>]
14
+ * cloudcost compare <path> [--format markdown|json|csv|focus] [--currency <code>]
15
+ * cloudcost optimize <path> [--providers aws,azure,gcp]
16
+ * cloudcost what-if <path> --changes <changes.json> [--provider aws|azure|gcp] [--region <region>]
17
+ *
18
+ * Flags:
19
+ * --json Output raw JSON instead of formatted text
20
+ * --help, -h Show usage
21
+ */
22
+ interface ParsedArgs {
23
+ command: string | undefined;
24
+ path: string | undefined;
25
+ provider: string;
26
+ region: string | undefined;
27
+ format: string;
28
+ providers: string[];
29
+ currency: string;
30
+ changes: string | undefined;
31
+ json: boolean;
32
+ help: boolean;
33
+ }
34
+ declare const VALID_PROVIDERS: readonly ["aws", "azure", "gcp"];
35
+ type ValidProvider = (typeof VALID_PROVIDERS)[number];
36
+ declare const VALID_FORMATS: readonly ["markdown", "json", "csv", "focus"];
37
+ type ValidFormat = (typeof VALID_FORMATS)[number];
38
+ declare function parseArgs(argv: string[]): ParsedArgs;
39
+ /**
40
+ * Validate option values after parsing. Returns a human-readable usage error
41
+ * for the first invalid value, or null when everything is valid. Kept
42
+ * separate from parseArgs so both are unit-testable without process.exit.
43
+ */
44
+ declare function validateArgs(args: ParsedArgs): string | null;
45
+
46
+ export { type ParsedArgs, VALID_FORMATS, VALID_PROVIDERS, type ValidFormat, type ValidProvider, parseArgs, validateArgs };
package/dist/cli.js CHANGED
@@ -1,21 +1,24 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
  import {
4
3
  PricingCache,
5
4
  PricingEngine,
5
+ SUPPORTED_CURRENCIES,
6
6
  analyzeTerraform,
7
7
  compareProviders,
8
8
  estimateCost,
9
9
  loadConfig,
10
10
  optimizeCost,
11
11
  whatIf
12
- } from "./chunk-6O2Y6MKU.js";
13
- import "./chunk-MNFT5YKN.js";
12
+ } from "./chunk-CGMU4ETN.js";
13
+ import "./chunk-LWTVE7UY.js";
14
14
 
15
15
  // src/cli.ts
16
- import { readFileSync, existsSync, statSync } from "fs";
16
+ import { readFileSync, existsSync, statSync, realpathSync } from "fs";
17
17
  import { resolve, extname, join } from "path";
18
18
  import { readdirSync } from "fs";
19
+ import { pathToFileURL } from "url";
20
+ var VALID_PROVIDERS = ["aws", "azure", "gcp"];
21
+ var VALID_FORMATS = ["markdown", "json", "csv", "focus"];
19
22
  function parseArgs(argv) {
20
23
  const args = argv.slice(2);
21
24
  const parsed = {
@@ -25,6 +28,7 @@ function parseArgs(argv) {
25
28
  region: void 0,
26
29
  format: "markdown",
27
30
  providers: ["aws", "azure", "gcp"],
31
+ currency: "USD",
28
32
  changes: void 0,
29
33
  json: false,
30
34
  help: false
@@ -44,6 +48,8 @@ function parseArgs(argv) {
44
48
  parsed.format = args[++i];
45
49
  } else if (arg === "--providers" && args[i + 1]) {
46
50
  parsed.providers = args[++i].split(",").map((p) => p.trim());
51
+ } else if (arg === "--currency" && args[i + 1]) {
52
+ parsed.currency = args[++i].toUpperCase();
47
53
  } else if (arg === "--changes" && args[i + 1]) {
48
54
  parsed.changes = args[++i];
49
55
  } else if (!arg.startsWith("--")) {
@@ -57,6 +63,23 @@ function parseArgs(argv) {
57
63
  }
58
64
  return parsed;
59
65
  }
66
+ function validateArgs(args) {
67
+ if (!VALID_PROVIDERS.includes(args.provider)) {
68
+ return `Invalid --provider "${args.provider}". Must be one of: ${VALID_PROVIDERS.join(", ")}`;
69
+ }
70
+ if (!VALID_FORMATS.includes(args.format)) {
71
+ return `Invalid --format "${args.format}". Must be one of: ${VALID_FORMATS.join(", ")}`;
72
+ }
73
+ if (!SUPPORTED_CURRENCIES.includes(args.currency)) {
74
+ return `Invalid --currency "${args.currency}". Must be one of: ${SUPPORTED_CURRENCIES.join(", ")}`;
75
+ }
76
+ for (const p of args.providers) {
77
+ if (!VALID_PROVIDERS.includes(p)) {
78
+ return `Invalid provider "${p}" in --providers. Must be a comma-separated list of: ${VALID_PROVIDERS.join(", ")}`;
79
+ }
80
+ }
81
+ return null;
82
+ }
60
83
  function loadTerraformFiles(inputPath) {
61
84
  const resolved = resolve(inputPath);
62
85
  if (!existsSync(resolved)) {
@@ -117,8 +140,9 @@ Usage:
117
140
  Options:
118
141
  --provider <aws|azure|gcp> Target provider (default: aws)
119
142
  --region <region> Target region (default: auto-detect)
120
- --format <markdown|json|csv> Report format for compare (default: markdown)
143
+ --format <markdown|json|csv|focus> Report format for compare (default: markdown)
121
144
  --providers <aws,azure,gcp> Comma-separated providers for compare/optimize
145
+ --currency <USD|EUR|GBP|JPY|CAD|AUD|INR|BRL> Output currency for estimate/compare (default: USD)
122
146
  --changes <path> JSON file with changes array for what-if
123
147
  --json Output raw JSON
124
148
  --help, -h Show this help
@@ -142,17 +166,13 @@ async function runAnalyze(args, files, tfvars) {
142
166
  }
143
167
  }
144
168
  async function runEstimate(args, files, tfvars, pricingEngine, config) {
145
- const validProviders = ["aws", "azure", "gcp"];
146
- if (!validProviders.includes(args.provider)) {
147
- throw new Error(`Invalid provider "${args.provider}". Must be one of: aws, azure, gcp`);
148
- }
149
169
  const result = await estimateCost(
150
170
  {
151
171
  files,
152
172
  tfvars,
153
173
  provider: args.provider,
154
174
  region: args.region,
155
- currency: "USD"
175
+ currency: args.currency
156
176
  },
157
177
  pricingEngine,
158
178
  config
@@ -164,19 +184,15 @@ async function runEstimate(args, files, tfvars, pricingEngine, config) {
164
184
  }
165
185
  }
166
186
  async function runCompare(args, files, tfvars, pricingEngine, config) {
167
- const validFormats = ["markdown", "json", "csv"];
168
- if (!validFormats.includes(args.format)) {
169
- throw new Error(`Invalid format "${args.format}". Must be one of: markdown, json, csv`);
170
- }
171
187
  const validatedProviders = args.providers.filter(
172
- (p) => p === "aws" || p === "azure" || p === "gcp"
188
+ (p) => VALID_PROVIDERS.includes(p)
173
189
  );
174
190
  const result = await compareProviders(
175
191
  {
176
192
  files,
177
193
  tfvars,
178
194
  format: args.format,
179
- currency: "USD",
195
+ currency: args.currency,
180
196
  providers: validatedProviders.length > 0 ? validatedProviders : ["aws", "azure", "gcp"]
181
197
  },
182
198
  pricingEngine,
@@ -195,7 +211,7 @@ async function runCompare(args, files, tfvars, pricingEngine, config) {
195
211
  }
196
212
  async function runOptimize(args, files, tfvars, pricingEngine, config) {
197
213
  const validatedProviders = args.providers.filter(
198
- (p) => p === "aws" || p === "azure" || p === "gcp"
214
+ (p) => VALID_PROVIDERS.includes(p)
199
215
  );
200
216
  const result = await optimizeCost(
201
217
  {
@@ -258,8 +274,7 @@ async function runWhatIf(args, files, tfvars, pricingEngine, config) {
258
274
  new_value: c["new_value"]
259
275
  };
260
276
  });
261
- const validProviders = ["aws", "azure", "gcp"];
262
- const provider = validProviders.includes(args.provider) ? args.provider : void 0;
277
+ const provider = VALID_PROVIDERS.includes(args.provider) ? args.provider : void 0;
263
278
  const result = await whatIf(
264
279
  {
265
280
  files,
@@ -292,6 +307,14 @@ async function main() {
292
307
  }
293
308
  if (!args.path) {
294
309
  process.stderr.write(`Error: path argument is required
310
+ `);
311
+ printUsage();
312
+ process.exit(1);
313
+ }
314
+ const argError = validateArgs(args);
315
+ if (argError) {
316
+ process.stderr.write(`Error: ${argError}
317
+
295
318
  `);
296
319
  printUsage();
297
320
  process.exit(1);
@@ -337,9 +360,26 @@ async function main() {
337
360
  cache.close();
338
361
  }
339
362
  }
340
- main().catch((err) => {
341
- process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}
363
+ function isMainModule() {
364
+ const entry = process.argv[1];
365
+ if (!entry) return false;
366
+ try {
367
+ return import.meta.url === pathToFileURL(realpathSync(entry)).href;
368
+ } catch {
369
+ return false;
370
+ }
371
+ }
372
+ if (isMainModule()) {
373
+ main().catch((err) => {
374
+ process.stderr.write(`Fatal error: ${err instanceof Error ? err.message : String(err)}
342
375
  `);
343
- process.exit(1);
344
- });
376
+ process.exit(1);
377
+ });
378
+ }
379
+ export {
380
+ VALID_FORMATS,
381
+ VALID_PROVIDERS,
382
+ parseArgs,
383
+ validateArgs
384
+ };
345
385
  //# sourceMappingURL=cli.js.map