cloudburn 0.8.0 → 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 (3) hide show
  1. package/README.md +2 -2
  2. package/dist/cli.js +323 -45
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -76,8 +76,8 @@ cloudburn completion zsh
76
76
  "resourceId": "aws_ebs_volume.gp2_data",
77
77
  "location": {
78
78
  "path": "main.tf",
79
- "startLine": 4,
80
- "startColumn": 3
79
+ "line": 4,
80
+ "column": 3
81
81
  }
82
82
  }
83
83
  ]
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,8 +535,12 @@ 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
541
+ var DEFAULT_TABLE_WIDTH = 200;
542
+ var MIN_COLUMN_WIDTH = 4;
543
+ var PREFERRED_MIN_COLUMN_WIDTH = 8;
531
544
  var supportedOutputFormats = ["text", "json", "table"];
532
545
  var scanColumns = [
533
546
  { key: "provider", header: "Provider" },
@@ -538,8 +551,8 @@ var scanColumns = [
538
551
  { key: "accountId", header: "AccountId" },
539
552
  { key: "region", header: "Region" },
540
553
  { key: "path", header: "Path" },
541
- { key: "startLine", header: "StartLine" },
542
- { key: "startColumn", header: "StartColumn" },
554
+ { key: "line", header: "Line" },
555
+ { key: "column", header: "Column" },
543
556
  { key: "message", header: "Message" }
544
557
  ];
545
558
  var formatOptionDescription = "Options: table: human-readable terminal output.\ntext: tab-delimited output for grep, sed, and awk.\njson: machine-readable output for automation and downstream systems.";
@@ -571,6 +584,8 @@ var renderJson = (response) => {
571
584
  switch (response.kind) {
572
585
  case "document":
573
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);
574
589
  case "record-list":
575
590
  return JSON.stringify(response.rows, null, 2);
576
591
  case "rule-list":
@@ -587,6 +602,9 @@ var renderText = (response) => {
587
602
  switch (response.kind) {
588
603
  case "document":
589
604
  return response.content;
605
+ case "discovery-status":
606
+ return `${response.summaryText}
607
+ ${renderTextRows(response.rows, response.columns, "No discovery status available.")}`;
590
608
  case "record-list":
591
609
  return renderTextRows(response.rows, response.columns, response.emptyMessage);
592
610
  case "rule-list":
@@ -612,6 +630,19 @@ var renderTable = (response) => {
612
630
  { key: "Value", header: "Value" }
613
631
  ]
614
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
+ }
615
646
  case "record-list":
616
647
  return response.rows.length === 0 ? response.emptyMessage : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
617
648
  case "rule-list":
@@ -635,19 +666,34 @@ var renderTable = (response) => {
635
666
  );
636
667
  }
637
668
  };
638
- var projectScanRows = (result) => flattenScanResult(result).map(({ finding, message, provider, ruleId, service, source }) => ({
639
- accountId: finding.accountId ?? "",
640
- message,
641
- path: finding.location?.path ?? "",
642
- provider,
643
- region: finding.region ?? "",
644
- resourceId: finding.resourceId,
645
- ruleId,
646
- service,
647
- source,
648
- startColumn: finding.location?.startColumn ?? "",
649
- startLine: finding.location?.startLine ?? ""
650
- }));
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
+ ];
651
697
  var renderTextRows = (rows, columns, emptyMessage) => {
652
698
  if (rows.length === 0) {
653
699
  return emptyMessage;
@@ -694,17 +740,118 @@ var toTextCell = (value) => {
694
740
  }
695
741
  return JSON.stringify(value);
696
742
  };
697
- var toTableCell = (value) => toTextCell(value).replace(/\r?\n/g, "\\n");
698
- var renderAsciiTable = (rows, columns) => {
699
- const widths = columns.map(
743
+ var toTableCell = (value) => {
744
+ if (Array.isArray(value)) {
745
+ return value.map((item) => toTextCell(item)).join(", ").replace(/\r?\n/g, "\\n");
746
+ }
747
+ return toTextCell(value).replace(/\r?\n/g, "\\n");
748
+ };
749
+ var resolveTableColumns = (rows, columns) => {
750
+ const visibleColumns = columns.filter((column) => rows.some((row) => toTableCell(row[column.key]).length > 0));
751
+ return visibleColumns.length === 0 ? columns : visibleColumns;
752
+ };
753
+ var getTargetTableWidth = () => {
754
+ const terminalWidth = process.stdout.columns;
755
+ return typeof terminalWidth === "number" && Number.isFinite(terminalWidth) && terminalWidth > 0 ? terminalWidth : DEFAULT_TABLE_WIDTH;
756
+ };
757
+ var measureTableWidth = (widths) => widths.reduce((total, width) => total + width, 0) + widths.length * 3 + 1;
758
+ var fitColumnWidths = (columns, rows) => {
759
+ const maxWidths = columns.map(
700
760
  (column) => Math.max(column.header.length, ...rows.map((row) => toTableCell(row[column.key]).length))
701
761
  );
762
+ const minWidths = columns.map(
763
+ (column, index) => Math.min(
764
+ maxWidths[index] ?? MIN_COLUMN_WIDTH,
765
+ Math.max(MIN_COLUMN_WIDTH, Math.min(column.header.length, PREFERRED_MIN_COLUMN_WIDTH))
766
+ )
767
+ );
768
+ const widths = [...maxWidths];
769
+ const targetWidth = getTargetTableWidth();
770
+ while (measureTableWidth(widths) > targetWidth) {
771
+ let widestColumnIndex = -1;
772
+ for (let index = 0; index < widths.length; index += 1) {
773
+ if ((widths[index] ?? 0) <= (minWidths[index] ?? MIN_COLUMN_WIDTH)) {
774
+ continue;
775
+ }
776
+ if (widestColumnIndex === -1 || (widths[index] ?? 0) > (widths[widestColumnIndex] ?? 0)) {
777
+ widestColumnIndex = index;
778
+ }
779
+ }
780
+ if (widestColumnIndex === -1) {
781
+ break;
782
+ }
783
+ widths[widestColumnIndex] = Math.max(MIN_COLUMN_WIDTH, (widths[widestColumnIndex] ?? MIN_COLUMN_WIDTH) - 1);
784
+ }
785
+ return widths;
786
+ };
787
+ var wrapToken = (token, width) => {
788
+ if (token.length <= width) {
789
+ return [token];
790
+ }
791
+ const segments = [];
792
+ for (let start = 0; start < token.length; start += width) {
793
+ segments.push(token.slice(start, start + width));
794
+ }
795
+ return segments;
796
+ };
797
+ var wrapCell = (value, width) => {
798
+ if (value.length <= width) {
799
+ return [value];
800
+ }
801
+ const words = value.split(/\s+/).filter((word) => word.length > 0);
802
+ if (words.length === 0) {
803
+ return [""];
804
+ }
805
+ const lines = [];
806
+ let currentLine = "";
807
+ for (const word of words) {
808
+ if (word.length > width) {
809
+ if (currentLine.length > 0) {
810
+ lines.push(currentLine);
811
+ currentLine = "";
812
+ }
813
+ lines.push(...wrapToken(word, width));
814
+ continue;
815
+ }
816
+ if (currentLine.length === 0) {
817
+ currentLine = word;
818
+ continue;
819
+ }
820
+ if (currentLine.length + 1 + word.length <= width) {
821
+ currentLine = `${currentLine} ${word}`;
822
+ continue;
823
+ }
824
+ lines.push(currentLine);
825
+ currentLine = word;
826
+ }
827
+ if (currentLine.length > 0) {
828
+ lines.push(currentLine);
829
+ }
830
+ return lines;
831
+ };
832
+ var renderTableLines = (cells, widths) => {
833
+ const wrappedCells = cells.map((cell, index) => wrapCell(cell, widths[index] ?? MIN_COLUMN_WIDTH));
834
+ const height = Math.max(...wrappedCells.map((lines) => lines.length));
835
+ return Array.from({ length: height }, (_, lineIndex) => {
836
+ const line = wrappedCells.map((lines, index) => (lines[lineIndex] ?? "").padEnd(widths[index] ?? MIN_COLUMN_WIDTH)).join(" | ");
837
+ return `| ${line} |`;
838
+ });
839
+ };
840
+ var renderAsciiTable = (rows, columns) => {
841
+ const visibleColumns = resolveTableColumns(rows, columns);
842
+ const widths = fitColumnWidths(visibleColumns, rows);
702
843
  const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
703
- const header = `| ${columns.map((column, index) => column.header.padEnd(widths[index] ?? 0)).join(" | ")} |`;
704
- const body = rows.map(
705
- (row) => `| ${columns.map((column, index) => toTableCell(row[column.key]).padEnd(widths[index] ?? 0)).join(" | ")} |`
844
+ const header = renderTableLines(
845
+ visibleColumns.map((column) => column.header),
846
+ widths
847
+ );
848
+ const body = rows.flatMap(
849
+ (row) => renderTableLines(
850
+ visibleColumns.map((column) => toTableCell(row[column.key])),
851
+ widths
852
+ )
706
853
  );
707
- return [border, header, border, ...body, border].join("\n");
854
+ return [border, ...header, border, ...body, border].join("\n");
708
855
  };
709
856
 
710
857
  // src/commands/config-options.ts
@@ -718,6 +865,69 @@ var parseRuleIdList = (value) => {
718
865
  };
719
866
 
720
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
+ };
721
931
  var parseAwsRegion = (value) => {
722
932
  try {
723
933
  return assertValidAwsRegion(value);
@@ -756,9 +966,17 @@ var registerDiscoverCommand = (program) => {
756
966
  const discoverCommand = setCommandExamples(
757
967
  program.command("discover").description("Run a live AWS discovery").enablePositionalOptions().option(
758
968
  "--region <region>",
759
- '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.',
760
970
  parseDiscoverRegion
761
- ).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) => {
762
980
  await runCommand(async () => {
763
981
  const scanner = new CloudBurnClient();
764
982
  const loadedConfig = await scanner.loadConfig(options.config);
@@ -787,10 +1005,64 @@ var registerDiscoverCommand = (program) => {
787
1005
  "cloudburn discover",
788
1006
  "cloudburn discover --region eu-central-1",
789
1007
  "cloudburn discover --region all",
1008
+ "cloudburn discover status",
790
1009
  "cloudburn discover list-enabled-regions",
791
1010
  "cloudburn discover init"
792
1011
  ]
793
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
+ });
794
1066
  discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").action(async (_options, command) => {
795
1067
  await runCommand(async () => {
796
1068
  const scanner = new CloudBurnClient();
@@ -813,24 +1085,22 @@ var registerDiscoverCommand = (program) => {
813
1085
  return EXIT_CODE_OK;
814
1086
  });
815
1087
  });
816
- 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) => {
817
1093
  await runCommand(async () => {
818
1094
  const scanner = new CloudBurnClient();
819
1095
  const parentRegion = discoverCommand.opts().region;
820
1096
  const region = options.region ?? (parentRegion === "all" ? void 0 : parentRegion);
821
1097
  const result = await scanner.initializeDiscovery({ region });
822
- 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);
823
1099
  const format = resolveOutputFormat(command);
824
1100
  const output = renderResponse(
825
1101
  {
826
1102
  kind: "status",
827
- data: {
828
- aggregatorRegion: result.aggregatorRegion,
829
- message,
830
- regions: result.regions,
831
- status: result.status,
832
- taskId: result.taskId ?? ""
833
- },
1103
+ data: buildInitializationStatusData(result, message, format),
834
1104
  text: message
835
1105
  },
836
1106
  format
@@ -1041,7 +1311,15 @@ var toScanConfigOverride = (options) => {
1041
1311
  };
1042
1312
  var registerScanCommand = (program) => {
1043
1313
  setCommandExamples(
1044
- 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) => {
1045
1323
  try {
1046
1324
  const scanner = new CloudBurnClient2();
1047
1325
  const loadedConfig = await scanner.loadConfig(options.config);
@@ -1070,7 +1348,7 @@ var registerScanCommand = (program) => {
1070
1348
  // src/cli.ts
1071
1349
  var createProgram = () => {
1072
1350
  const program = createCliCommand();
1073
- program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.8.0").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);
1074
1352
  configureCliHelp(program);
1075
1353
  registerCompletionCommand(program);
1076
1354
  registerDiscoverCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.8.0",
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.12.0"
14
+ "@cloudburn/sdk": "0.13.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@biomejs/biome": "^2.4.6",