cloudburn 0.8.2 → 0.8.3

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 (2) hide show
  1. package/dist/cli.js +210 -36
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -153,6 +153,13 @@ var buildCommandPath = (command) => {
153
153
  }
154
154
  return names.join(" ");
155
155
  };
156
+ var getRootCommand = (command) => {
157
+ let current = command;
158
+ while (current.parent !== null) {
159
+ current = current.parent;
160
+ }
161
+ return current;
162
+ };
156
163
  var visibleCommands = (command) => command.commands.filter((subcommand) => !subcommand._hidden);
157
164
  var getCommandExamples = (command) => {
158
165
  const { _cloudburnExamples } = command;
@@ -221,8 +228,9 @@ var formatCloudBurnHelp = (command, helper) => {
221
228
  const cloudBurnHelp = helper;
222
229
  const helpWidth = helper.helpWidth ?? 80;
223
230
  const scenario = getHelpScenario(command);
224
- const localOptions = helper.visibleOptions(command);
225
- const globalOptions = helper.visibleGlobalOptions(command);
231
+ const rootCommand = getRootCommand(command);
232
+ const localOptions = helper.visibleOptions(command).filter((option) => command.options.includes(option));
233
+ const globalOptions = helper.visibleGlobalOptions(command).filter((option) => rootCommand.options.includes(option));
226
234
  const commands = helper.visibleCommands(command);
227
235
  const examples = getCommandExamples(command);
228
236
  const usageGuidance = getCommandUsageGuidance(command);
@@ -386,7 +394,7 @@ var setCommandUsageGuidance = (command, guidance) => {
386
394
  };
387
395
 
388
396
  // src/commands/completion.ts
389
- var getRootCommand = (command) => {
397
+ var getRootCommand2 = (command) => {
390
398
  let currentCommand = command;
391
399
  while (currentCommand.parent !== null) {
392
400
  currentCommand = currentCommand.parent;
@@ -441,14 +449,14 @@ var registerCompletionCommand = (program) => {
441
449
  for (const shell of ["bash", "fish", "zsh"]) {
442
450
  setCommandUsageGuidance(
443
451
  completionCommand.command(shell).description(`Generate the autocompletion script for the ${shell} shell.`).option("--no-descriptions", "disable completion descriptions").action(function() {
444
- const output = generateCompletionScript(shell, getRootCommand(this));
452
+ const output = generateCompletionScript(shell, getRootCommand2(this));
445
453
  process.stdout.write(output);
446
454
  }),
447
455
  getCompletionHelpText(shell)
448
456
  );
449
457
  }
450
458
  program.command("__complete", { hidden: true }).argument("[words...]").allowUnknownOption().action(function(words) {
451
- const suggestions = resolveCompletionSuggestions(createCompletionTree(getRootCommand(this)), words ?? []);
459
+ const suggestions = resolveCompletionSuggestions(createCompletionTree(getRootCommand2(this)), words ?? []);
452
460
  if (suggestions.length === 0) {
453
461
  return;
454
462
  }
@@ -480,25 +488,26 @@ var categorize = (err) => {
480
488
  if (!(err instanceof Error)) {
481
489
  return { code: "RUNTIME_ERROR", message: "An unexpected error occurred." };
482
490
  }
483
- if (err.name === "CredentialsProviderError" || err.name === "ExpiredTokenException") {
491
+ const code = "code" in err && typeof err.code === "string" ? err.code : void 0;
492
+ if (err.name === "CredentialsProviderError" || err.name === "ExpiredTokenException" || code === "CredentialsProviderError" || code === "ExpiredTokenException") {
484
493
  return {
485
494
  code: "CREDENTIALS_ERROR",
486
495
  message: "AWS credentials not found or expired. Run 'aws sts get-caller-identity' to verify your session."
487
496
  };
488
497
  }
489
- if (err.name.includes("AccessDenied")) {
498
+ if (err.name.includes("AccessDenied") || code?.includes("AccessDenied") === true) {
490
499
  return {
491
500
  code: "ACCESS_DENIED",
492
- message: "Insufficient AWS permissions. Check your IAM role or policy."
501
+ message: sanitizeRuntimeErrorMessage(err.message).trim() || "Insufficient AWS permissions. Check your IAM role or policy."
493
502
  };
494
503
  }
495
504
  if (err.code === "ENOENT") {
496
505
  const path = err.path ?? "unknown";
497
506
  return { code: "PATH_NOT_FOUND", message: `Path not found: ${path}` };
498
507
  }
499
- if ("code" in err && typeof err.code === "string" && isAwsDiscoveryErrorCode(err.code)) {
508
+ if (code && isAwsDiscoveryErrorCode(code)) {
500
509
  return {
501
- code: err.code,
510
+ code,
502
511
  message: sanitizeRuntimeErrorMessage(err.message).trim() || "AWS Resource Explorer discovery failed."
503
512
  };
504
513
  }
@@ -526,6 +535,7 @@ var flattenScanResult = (result) => result.providers.flatMap(
526
535
  )
527
536
  );
528
537
  var countScanResultFindings = (result) => flattenScanResult(result).length;
538
+ var getScanDiagnostics = (result) => result.diagnostics ?? [];
529
539
 
530
540
  // src/formatters/output.ts
531
541
  var DEFAULT_TABLE_WIDTH = 200;
@@ -574,6 +584,8 @@ var renderJson = (response) => {
574
584
  switch (response.kind) {
575
585
  case "document":
576
586
  return JSON.stringify({ content: response.content, contentType: response.contentType }, null, 2);
587
+ case "discovery-status":
588
+ return JSON.stringify({ summary: response.summary, regions: response.rows }, null, 2);
577
589
  case "record-list":
578
590
  return JSON.stringify(response.rows, null, 2);
579
591
  case "rule-list":
@@ -590,6 +602,9 @@ var renderText = (response) => {
590
602
  switch (response.kind) {
591
603
  case "document":
592
604
  return response.content;
605
+ case "discovery-status":
606
+ return `${response.summaryText}
607
+ ${renderTextRows(response.rows, response.columns, "No discovery status available.")}`;
593
608
  case "record-list":
594
609
  return renderTextRows(response.rows, response.columns, response.emptyMessage);
595
610
  case "rule-list":
@@ -615,6 +630,19 @@ var renderTable = (response) => {
615
630
  { key: "Value", header: "Value" }
616
631
  ]
617
632
  );
633
+ case "discovery-status": {
634
+ const summaryTable = renderAsciiTable(
635
+ Object.entries(response.summary).map(([field, value]) => ({ Field: field, Value: value })),
636
+ [
637
+ { key: "Field", header: "Field" },
638
+ { key: "Value", header: "Value" }
639
+ ]
640
+ );
641
+ const regionsTable = response.rows.length === 0 ? "No discovery status available." : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
642
+ return `${summaryTable}
643
+
644
+ ${regionsTable}`;
645
+ }
618
646
  case "record-list":
619
647
  return response.rows.length === 0 ? response.emptyMessage : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
620
648
  case "rule-list":
@@ -638,19 +666,34 @@ var renderTable = (response) => {
638
666
  );
639
667
  }
640
668
  };
641
- var projectScanRows = (result) => flattenScanResult(result).map(({ finding, message, provider, ruleId, service, source }) => ({
642
- accountId: finding.accountId ?? "",
643
- message,
644
- path: finding.location?.path ?? "",
645
- provider,
646
- region: finding.region ?? "",
647
- resourceId: finding.resourceId,
648
- ruleId,
649
- service,
650
- source,
651
- column: finding.location?.column ?? "",
652
- line: finding.location?.line ?? ""
653
- }));
669
+ var projectScanRows = (result) => [
670
+ ...flattenScanResult(result).map(({ finding, message, provider, ruleId, service, source }) => ({
671
+ accountId: finding.accountId ?? "",
672
+ message,
673
+ path: finding.location?.path ?? "",
674
+ provider,
675
+ region: finding.region ?? "",
676
+ resourceId: finding.resourceId,
677
+ ruleId,
678
+ service,
679
+ source,
680
+ column: finding.location?.column ?? "",
681
+ line: finding.location?.line ?? ""
682
+ })),
683
+ ...getScanDiagnostics(result).map((diagnostic) => ({
684
+ accountId: "",
685
+ column: "",
686
+ line: "",
687
+ message: diagnostic.message,
688
+ path: "",
689
+ provider: diagnostic.provider,
690
+ region: diagnostic.region ?? "",
691
+ resourceId: "",
692
+ ruleId: "",
693
+ service: diagnostic.service,
694
+ source: diagnostic.source
695
+ }))
696
+ ];
654
697
  var renderTextRows = (rows, columns, emptyMessage) => {
655
698
  if (rows.length === 0) {
656
699
  return emptyMessage;
@@ -822,6 +865,69 @@ var parseRuleIdList = (value) => {
822
865
  };
823
866
 
824
867
  // src/commands/discover.ts
868
+ var describeDiscoverySummary = (status) => {
869
+ const aggregatorSummary = status.aggregatorRegion ? ` Aggregator region: ${status.aggregatorRegion}.` : "";
870
+ const warningSummary = status.warning ? ` ${status.warning}` : "";
871
+ return `Coverage: ${status.coverage}. Indexed ${status.indexedRegionCount} of ${status.totalRegionCount} enabled regions.${aggregatorSummary}${warningSummary}`.trim();
872
+ };
873
+ var describeInitializationMessage = (result) => {
874
+ 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}.`;
875
+ const indexSummaryParts = [];
876
+ if (result.createdIndexCount > 0) {
877
+ indexSummaryParts.push(
878
+ `Created ${result.createdIndexCount} ${result.createdIndexCount === 1 ? "index" : "indexes"}.`
879
+ );
880
+ }
881
+ if (result.reusedIndexCount > 0) {
882
+ indexSummaryParts.push(
883
+ `Reused ${result.reusedIndexCount} existing ${result.reusedIndexCount === 1 ? "index" : "indexes"}.`
884
+ );
885
+ }
886
+ const indexSummary = indexSummaryParts.length === 0 ? "" : ` ${indexSummaryParts.join(" ")}`;
887
+ const warnings = Array.from(new Set([result.warning, result.observedStatus.warning].filter(Boolean)));
888
+ const warning = warnings.length === 0 ? "" : ` ${warnings.join(" ")}`;
889
+ const convergenceNotice = result.verificationStatus === "timed_out" ? " Setup is still converging in AWS, so the observed coverage may still change." : "";
890
+ return `${baseMessage}${indexSummary}${warning}${convergenceNotice}`.trim();
891
+ };
892
+ var buildInitializationStatusData = (result, message, format) => {
893
+ const restrictedRegionCount = Math.max(
894
+ 0,
895
+ result.observedStatus.totalRegionCount - result.observedStatus.accessibleRegionCount
896
+ );
897
+ if (format === "json") {
898
+ return {
899
+ aggregatorAction: result.aggregatorAction,
900
+ aggregatorRegion: result.aggregatorRegion,
901
+ coverage: result.coverage,
902
+ createdIndexCount: result.createdIndexCount,
903
+ indexType: result.indexType,
904
+ message,
905
+ observedStatus: result.observedStatus,
906
+ regions: result.regions,
907
+ reusedIndexCount: result.reusedIndexCount,
908
+ status: result.status,
909
+ taskId: result.taskId ?? "",
910
+ verificationStatus: result.verificationStatus,
911
+ ...result.warning ? { warning: result.warning } : {}
912
+ };
913
+ }
914
+ return {
915
+ aggregatorAction: result.aggregatorAction,
916
+ aggregatorRegion: result.aggregatorRegion,
917
+ coverage: result.coverage,
918
+ createdIndexes: String(result.createdIndexCount),
919
+ details: "Run `cloudburn discover status` for per-region details.",
920
+ indexedRegions: result.regions.length === 0 ? "none" : result.regions.join(", "),
921
+ indexedSummary: `${result.observedStatus.indexedRegionCount} of ${result.observedStatus.totalRegionCount}`,
922
+ indexType: result.indexType,
923
+ message,
924
+ reusedIndexes: String(result.reusedIndexCount),
925
+ ...restrictedRegionCount > 0 ? { restrictedRegions: String(restrictedRegionCount) } : {},
926
+ status: result.status,
927
+ taskId: result.taskId ?? "",
928
+ verificationStatus: result.verificationStatus
929
+ };
930
+ };
825
931
  var parseAwsRegion = (value) => {
826
932
  try {
827
933
  return assertValidAwsRegion(value);
@@ -860,9 +966,17 @@ var registerDiscoverCommand = (program) => {
860
966
  const discoverCommand = setCommandExamples(
861
967
  program.command("discover").description("Run a live AWS discovery").enablePositionalOptions().option(
862
968
  "--region <region>",
863
- 'Discovery region to use. Pass "all" to require an aggregator index.',
969
+ 'Discovery region to use. Defaults to the current AWS region from AWS_REGION; use this flag to override it. Pass "all" to check resources in all regions that are indexed in AWS Resource Explorer.',
864
970
  parseDiscoverRegion
865
- ).option("--config <path>", "Explicit CloudBurn config file to load").option("--enabled-rules <ruleIds>", "Comma-separated rule IDs to enable", parseRuleIdList).option("--disabled-rules <ruleIds>", "Comma-separated rule IDs to disable", parseRuleIdList).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
971
+ ).option("--config <path>", "Explicit CloudBurn config file to load").option(
972
+ "--enabled-rules <ruleIds>",
973
+ "Comma-separated rule IDs to enable. When set, CloudBurn checks only these rules. By default, all rules are enabled.",
974
+ parseRuleIdList
975
+ ).option(
976
+ "--disabled-rules <ruleIds>",
977
+ "Comma-separated rule IDs to disable. By default, all rules are enabled; use this to exclude specific rules.",
978
+ parseRuleIdList
979
+ ).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
866
980
  await runCommand(async () => {
867
981
  const scanner = new CloudBurnClient();
868
982
  const loadedConfig = await scanner.loadConfig(options.config);
@@ -891,10 +1005,64 @@ var registerDiscoverCommand = (program) => {
891
1005
  "cloudburn discover",
892
1006
  "cloudburn discover --region eu-central-1",
893
1007
  "cloudburn discover --region all",
1008
+ "cloudburn discover status",
894
1009
  "cloudburn discover list-enabled-regions",
895
1010
  "cloudburn discover init"
896
1011
  ]
897
1012
  );
1013
+ discoverCommand.command("status").description("Show Resource Explorer status across all enabled AWS regions").action(async (_options, command) => {
1014
+ await runCommand(async () => {
1015
+ const scanner = new CloudBurnClient();
1016
+ const parentRegion = discoverCommand.opts().region;
1017
+ const region = parentRegion === "all" ? void 0 : parentRegion;
1018
+ const status = await scanner.getDiscoveryStatus({ region });
1019
+ const format = resolveOutputFormat(command);
1020
+ const rows = format === "json" ? status.regions.map((regionStatus) => ({
1021
+ ...regionStatus,
1022
+ notes: regionStatus.notes ?? ""
1023
+ })) : status.regions.map((regionStatus) => ({
1024
+ region: regionStatus.region,
1025
+ indexType: regionStatus.indexType === void 0 ? "" : regionStatus.isAggregator ? `${regionStatus.indexType} (active)` : regionStatus.indexType,
1026
+ notes: regionStatus.notes ?? "",
1027
+ status: regionStatus.status,
1028
+ viewStatus: regionStatus.viewStatus ?? ""
1029
+ }));
1030
+ const summary = format === "json" ? {
1031
+ accessibleRegionCount: status.accessibleRegionCount,
1032
+ coverage: status.coverage,
1033
+ indexedRegionCount: status.indexedRegionCount,
1034
+ totalRegionCount: status.totalRegionCount,
1035
+ ...status.aggregatorRegion ? { aggregatorRegion: status.aggregatorRegion } : {},
1036
+ ...status.warning ? { warning: status.warning } : {}
1037
+ } : {
1038
+ accessibleRegionCount: status.accessibleRegionCount,
1039
+ aggregatorRegion: status.aggregatorRegion ?? "",
1040
+ coverage: status.coverage,
1041
+ indexedRegionCount: status.indexedRegionCount,
1042
+ totalRegionCount: status.totalRegionCount,
1043
+ ...status.warning ? { warning: status.warning } : {}
1044
+ };
1045
+ const output = renderResponse(
1046
+ {
1047
+ kind: "discovery-status",
1048
+ columns: [
1049
+ { key: "region", header: "Region" },
1050
+ { key: "indexType", header: "IndexType" },
1051
+ { key: "status", header: "Status" },
1052
+ { key: "viewStatus", header: "ViewStatus" },
1053
+ { key: "notes", header: "Notes" }
1054
+ ],
1055
+ rows,
1056
+ summary,
1057
+ summaryText: describeDiscoverySummary(status)
1058
+ },
1059
+ format
1060
+ );
1061
+ process.stdout.write(`${output}
1062
+ `);
1063
+ return EXIT_CODE_OK;
1064
+ });
1065
+ });
898
1066
  discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").action(async (_options, command) => {
899
1067
  await runCommand(async () => {
900
1068
  const scanner = new CloudBurnClient();
@@ -917,24 +1085,22 @@ var registerDiscoverCommand = (program) => {
917
1085
  return EXIT_CODE_OK;
918
1086
  });
919
1087
  });
920
- discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option("--region <region>", "Aggregator region to create or reuse during setup", parseAwsRegion).action(async (options, command) => {
1088
+ discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option(
1089
+ "--region <region>",
1090
+ "Requested aggregator region to create or reuse during setup. This is the main Resource Explorer region that aggregates indexes from other regions when cross-region setup succeeds. Defaults to the current AWS region from AWS_REGION; use this flag to override it.",
1091
+ parseAwsRegion
1092
+ ).action(async (options, command) => {
921
1093
  await runCommand(async () => {
922
1094
  const scanner = new CloudBurnClient();
923
1095
  const parentRegion = discoverCommand.opts().region;
924
1096
  const region = options.region ?? (parentRegion === "all" ? void 0 : parentRegion);
925
1097
  const result = await scanner.initializeDiscovery({ region });
926
- const message = result.status === "EXISTING" ? `Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Resource Explorer setup created in ${result.aggregatorRegion}.`;
1098
+ const message = describeInitializationMessage(result);
927
1099
  const format = resolveOutputFormat(command);
928
1100
  const output = renderResponse(
929
1101
  {
930
1102
  kind: "status",
931
- data: {
932
- aggregatorRegion: result.aggregatorRegion,
933
- message,
934
- regions: result.regions,
935
- status: result.status,
936
- taskId: result.taskId ?? ""
937
- },
1103
+ data: buildInitializationStatusData(result, message, format),
938
1104
  text: message
939
1105
  },
940
1106
  format
@@ -1145,7 +1311,15 @@ var toScanConfigOverride = (options) => {
1145
1311
  };
1146
1312
  var registerScanCommand = (program) => {
1147
1313
  setCommandExamples(
1148
- program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--config <path>", "Explicit CloudBurn config file to load").option("--enabled-rules <ruleIds>", "Comma-separated rule IDs to enable", parseRuleIdList).option("--disabled-rules <ruleIds>", "Comma-separated rule IDs to disable", parseRuleIdList).option("--exit-code", "Exit with code 1 when findings exist").action(async (path, options, command) => {
1314
+ program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--config <path>", "Explicit CloudBurn config file to load").option(
1315
+ "--enabled-rules <ruleIds>",
1316
+ "Comma-separated rule IDs to enable. When set, CloudBurn checks only these rules. By default, all rules are enabled.",
1317
+ parseRuleIdList
1318
+ ).option(
1319
+ "--disabled-rules <ruleIds>",
1320
+ "Comma-separated rule IDs to disable. By default, all rules are enabled; use this to exclude specific rules.",
1321
+ parseRuleIdList
1322
+ ).option("--exit-code", "Exit with code 1 when findings exist").action(async (path, options, command) => {
1149
1323
  try {
1150
1324
  const scanner = new CloudBurnClient2();
1151
1325
  const loadedConfig = await scanner.loadConfig(options.config);
@@ -1174,7 +1348,7 @@ var registerScanCommand = (program) => {
1174
1348
  // src/cli.ts
1175
1349
  var createProgram = () => {
1176
1350
  const program = createCliCommand();
1177
- program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.8.2").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1351
+ program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.8.3").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
1178
1352
  configureCliHelp(program);
1179
1353
  registerCompletionCommand(program);
1180
1354
  registerDiscoverCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Cloudburn CLI for cloud cost optimization",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "dependencies": {
13
13
  "commander": "^13.1.0",
14
- "@cloudburn/sdk": "0.13.1"
14
+ "@cloudburn/sdk": "0.13.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@biomejs/biome": "^2.4.6",