cloudburn 0.16.1 → 0.18.2

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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/dist/cli.js +47 -9
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -117,6 +117,25 @@ The discovery config equivalent is `discovery.fail-on`.
117
117
 
118
118
  Discovery has a five-minute deadline. Use `--timeout <seconds>` to change it, for example `cloudburn discover --timeout 600`. If the deadline expires, the command exits with code 2 and reports the timeout.
119
119
 
120
+ Discovery reuses fresh, complete evidence from a per-user cache at `$XDG_CACHE_HOME/cloudburn/evidence`, or
121
+ `~/.cache/cloudburn/evidence` when `XDG_CACHE_HOME` is unset. Rules and configuration are evaluated on every run.
122
+
123
+ ```bash
124
+ cloudburn discover --cache refresh
125
+ cloudburn discover --cache off
126
+ cloudburn discover --cache-dir /path/to/evidence --cache-context policy-revision-2
127
+ ```
128
+
129
+ `--cache refresh` collects current evidence and never silently falls back when collection fails. `--cache off` bypasses
130
+ persistent reads and writes. Customer evidence reuse requires a safe temporary AWS credential session scope or an
131
+ explicit `--cache-context` identifying the effective authorization and session-policy revision. An account ID or role
132
+ ARN alone is insufficient. Change the context when permissions change. Public pricing is cached independently of
133
+ customer evidence, and credentials are never stored.
134
+
135
+ Table output includes a freshness summary; `--format json` includes full evidence provenance and coverage. TTLs are
136
+ tunable starting policies rather than measured optimal defaults. See the [cache options](../../docs/reference/commands.md#discovery-evidence-cache)
137
+ and [SDK README](../sdk/README.md) for modes, defaults, and freshness tradeoffs.
138
+
120
139
  The CLI targets one region per run. Multi-region discovery remains available through the SDK and still needs an AWS Resource Explorer aggregator plus an unfiltered default view in the aggregator region.
121
140
  `CLDBRN-AWS-TAGGING-1` is opt-in and requires an accessible aggregator; a local-only setup cannot run account-wide tagging discovery.
122
141
  `CLDBRN-AWS-LAMBDA-4` is opt-in and requires AWS Compute Optimizer enrollment.
@@ -139,6 +158,10 @@ access, or incomplete configurations produce diagnostics. The SDK exposes both t
139
158
  `includeEvaluationResources: true`; the CLI prints finding identities and diagnostics.
140
159
  `CLDBRN-AWS-SAGEMAKER-3` uses Cost Explorer coverage data and remains available when Cost Optimization Hub is unavailable.
141
160
  Use `--debug` to print SDK and provider execution tracing to `stderr` without changing the normal `stdout` format.
161
+ On interactive terminals, discovery prints catalog and dataset progress plus provisional rule results to `stderr` as
162
+ rules finish. Each rule line includes its ID, status, and finding count. Provisional results describe work completed so
163
+ far; the final report and exit code are authoritative. Progress is silent when `stderr` is not a terminal or `--debug`
164
+ is enabled. `--format json` writes one final JSON result to `stdout`.
142
165
 
143
166
  ## Shell Completion
144
167
 
package/dist/cli.js CHANGED
@@ -654,20 +654,34 @@ ${regionsTable}`;
654
654
  const findingRows = projectFindingRows(response.result);
655
655
  const diagnosticRows = projectDiagnosticRows(response.result);
656
656
  const suppressedCount = response.result.suppressed?.length ?? 0;
657
- const withSuppressedCount = (content) => suppressedCount > 0 ? `${content}
658
-
659
- Suppressed: ${suppressedCount}` : content;
657
+ const withScanSummary = (content) => {
658
+ const sections = [content];
659
+ if (suppressedCount > 0) {
660
+ sections.push(`Suppressed: ${suppressedCount}`);
661
+ }
662
+ const evidence = response.result.evidence ?? [];
663
+ if (evidence.length > 0) {
664
+ const cachedCount = evidence.filter((entry) => entry.source === "cache").length;
665
+ const incompleteCount = evidence.filter((entry) => !entry.complete).length;
666
+ const oldestObservation = evidence.map((entry) => entry.observedAt).sort()[0];
667
+ sections.push(
668
+ `Evidence: ${cachedCount} cached, ${evidence.length - cachedCount} collected; ${incompleteCount} incomplete.
669
+ Oldest observation: ${oldestObservation}`
670
+ );
671
+ }
672
+ return sections.join("\n\n");
673
+ };
660
674
  if (findingRows.length === 0 && diagnosticRows.length === 0) {
661
- return withSuppressedCount(suppressedCount > 0 ? "No active findings." : "No findings.");
675
+ return withScanSummary(suppressedCount > 0 ? "No active findings." : "No findings.");
662
676
  }
663
677
  if (findingRows.length === 0) {
664
- return withSuppressedCount(`Diagnostics
678
+ return withScanSummary(`Diagnostics
665
679
  ${renderAsciiTable(diagnosticRows, diagnosticColumns)}`);
666
680
  }
667
681
  if (diagnosticRows.length === 0) {
668
- return withSuppressedCount(renderAsciiTable(findingRows, scanColumns));
682
+ return withScanSummary(renderAsciiTable(findingRows, scanColumns));
669
683
  }
670
- return withSuppressedCount(
684
+ return withScanSummary(
671
685
  `${renderAsciiTable(findingRows, scanColumns)}
672
686
 
673
687
  Diagnostics
@@ -1059,6 +1073,8 @@ var registerConfigCommand = (program) => {
1059
1073
  };
1060
1074
 
1061
1075
  // src/commands/discover.ts
1076
+ import { homedir } from "os";
1077
+ import { join as join2 } from "path";
1062
1078
  import {
1063
1079
  assertSupportedAwsRegion,
1064
1080
  CloudBurnClient
@@ -1095,6 +1111,13 @@ var resolveCliDiscoveryProgressLogger = (command) => {
1095
1111
  return void 0;
1096
1112
  }
1097
1113
  return (event) => {
1114
+ if (event.kind === "rule") {
1115
+ process.stderr.write(
1116
+ `discover: rules ${event.completedRules}/${event.totalRules} (${event.ruleId}: ${event.status}, findings: ${event.findingCount}, provisional)
1117
+ `
1118
+ );
1119
+ return;
1120
+ }
1098
1121
  process.stderr.write(
1099
1122
  event.kind === "catalog" ? `discover: catalog ready with ${event.resourceCount} resources from ${event.searchRegion}
1100
1123
  ` : `discover: datasets ${event.completedDatasets}/${event.totalDatasets} loaded (${event.datasetKey})
@@ -1149,6 +1172,12 @@ var parseDiscoveryTimeout = (value) => {
1149
1172
  }
1150
1173
  return seconds;
1151
1174
  };
1175
+ var parseCacheMode = (value) => {
1176
+ if (value === "normal" || value === "refresh" || value === "off") {
1177
+ return value;
1178
+ }
1179
+ throw new InvalidArgumentError3("Cache mode must be normal, refresh, or off.");
1180
+ };
1152
1181
  var parseDiscoveryServiceList = (value) => validateServiceList("discovery", parseServiceList(value)) ?? [];
1153
1182
  var describeInitializationMessage = (result) => {
1154
1183
  const baseMessage = result.indexType === "aggregator" ? result.aggregatorAction === "promoted" ? `Promoted the existing local Resource Explorer index in ${result.aggregatorRegion} to the aggregator.` : result.aggregatorAction === "created" ? `Configured ${result.aggregatorRegion} as the Resource Explorer aggregator.` : result.coverage === "full" ? `Resource Explorer aggregator already exists in ${result.aggregatorRegion}.` : `Resource Explorer aggregator already exists in ${result.aggregatorRegion}, but only ${result.observedStatus.indexedRegionCount} of ${result.observedStatus.totalRegionCount} regions are indexed.` : result.status === "EXISTING" ? `Local Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Local Resource Explorer setup created in ${result.aggregatorRegion}.`;
@@ -1256,7 +1285,10 @@ var registerDiscoverCommand = (program) => {
1256
1285
  "--region <region>",
1257
1286
  "AWS region to discover. Defaults to the current AWS region from AWS_REGION when omitted.",
1258
1287
  parseAwsRegion
1259
- ).option("--config <path>", "Explicit CloudBurn config file to load (required for config files in CI)").option("--timeout <seconds>", "Maximum discovery duration in seconds (default: 300).", parseDiscoveryTimeout).option(
1288
+ ).option("--config <path>", "Explicit CloudBurn config file to load (required for config files in CI)").option("--timeout <seconds>", "Maximum discovery duration in seconds (default: 300).", parseDiscoveryTimeout).option("--cache <mode>", "Evidence cache mode: normal, refresh, or off (default: normal).", parseCacheMode).option(
1289
+ "--cache-dir <path>",
1290
+ "Evidence cache directory (default: $XDG_CACHE_HOME/cloudburn/evidence or ~/.cache/cloudburn/evidence)."
1291
+ ).option("--cache-context <id>", "Effective authorization/session-policy revision for evidence reuse.").option(
1260
1292
  "--enabled-rules <ruleIds>",
1261
1293
  "Comma-separated rule IDs to enable. When set, CloudBurn checks only these rules. By default, AWS Core preset rules are enabled.",
1262
1294
  parseRuleIdList
@@ -1276,6 +1308,11 @@ var registerDiscoverCommand = (program) => {
1276
1308
  const configOverride = toDiscoveryConfigOverride(options);
1277
1309
  const loadedConfig = await scanner.loadConfig(options.config);
1278
1310
  const discoveryOptions = {
1311
+ cache: {
1312
+ mode: options.cache ?? "normal",
1313
+ directory: options.cacheDir ?? join2(process.env.XDG_CACHE_HOME ?? join2(homedir(), ".cache"), "cloudburn", "evidence"),
1314
+ ...options.cacheContext === void 0 ? {} : { authorizationContext: options.cacheContext }
1315
+ },
1279
1316
  target: resolveDiscoveryTarget(options.region)
1280
1317
  };
1281
1318
  if (configOverride !== void 0) {
@@ -1307,6 +1344,7 @@ var registerDiscoverCommand = (program) => {
1307
1344
  [
1308
1345
  "cloudburn discover",
1309
1346
  "cloudburn discover --region eu-central-1",
1347
+ "cloudburn discover --cache refresh",
1310
1348
  "cloudburn discover status",
1311
1349
  "cloudburn discover init"
1312
1350
  ]
@@ -1566,7 +1604,7 @@ var isCliEntrypoint = (moduleUrl, argvEntry = process.argv[1]) => {
1566
1604
  };
1567
1605
  var createProgram = () => {
1568
1606
  const program = createCliCommand();
1569
- program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.16.1").option("--debug", "Write execution trace messages to stderr").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1607
+ program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.18.2").option("--debug", "Write execution trace messages to stderr").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1570
1608
  configureCliHelp(program);
1571
1609
  program.exitOverride();
1572
1610
  registerCompletionCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.16.1",
3
+ "version": "0.18.2",
4
4
  "description": "Cloudburn CLI for cloud cost optimization",
5
5
  "homepage": "https://cloudburn.io/docs",
6
6
  "bugs": {
@@ -20,7 +20,7 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "commander": "^13.1.0",
23
- "@cloudburn/sdk": "0.32.1"
23
+ "@cloudburn/sdk": "0.35.2"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@biomejs/biome": "^2.5.3",