cloudburn 0.6.2 → 0.7.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.
Files changed (3) hide show
  1. package/README.md +33 -0
  2. package/dist/cli.js +517 -68
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -8,6 +8,36 @@ Command-line interface for Cloudburn cloud cost optimization.
8
8
  npm install -g cloudburn
9
9
  ```
10
10
 
11
+ ## Shell completion
12
+
13
+ Inspect the available completion subcommands:
14
+
15
+ ```sh
16
+ cloudburn completion
17
+ cloudburn completion zsh --help
18
+ ```
19
+
20
+ Generate a completion script for your shell and source it directly:
21
+
22
+ ```sh
23
+ source <(cloudburn completion zsh)
24
+ source <(cloudburn completion bash)
25
+ cloudburn completion fish | source
26
+ ```
27
+
28
+ To enable completion persistently, add one of the following lines to your shell config:
29
+
30
+ ```sh
31
+ # ~/.zshrc
32
+ source <(cloudburn completion zsh)
33
+
34
+ # ~/.bashrc
35
+ source <(cloudburn completion bash)
36
+
37
+ # ~/.config/fish/config.fish
38
+ cloudburn completion fish | source
39
+ ```
40
+
11
41
  ## Usage
12
42
 
13
43
  ```sh
@@ -23,6 +53,9 @@ cloudburn scan ./template.yaml
23
53
  cloudburn scan ./iac
24
54
  cloudburn discover
25
55
  cloudburn discover --region all
56
+ cloudburn rules
57
+ cloudburn completion
58
+ cloudburn completion zsh
26
59
  ```
27
60
 
28
61
  `cloudburn scan --format json` emits the lean canonical grouped result:
package/dist/cli.js CHANGED
@@ -2,7 +2,460 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { pathToFileURL } from "url";
5
+
6
+ // src/completion/engine.ts
7
+ var buildCompletionTree = (command) => {
8
+ const help = command.createHelp();
9
+ return {
10
+ commands: help.visibleCommands(command).map((childCommand) => buildCompletionTree(childCommand)),
11
+ name: command.name(),
12
+ options: collectVisibleOptions([...help.visibleOptions(command), ...help.visibleGlobalOptions(command)])
13
+ };
14
+ };
15
+ var collectVisibleOptions = (options) => options.map((option) => ({
16
+ expectsValue: option.required || option.optional,
17
+ tokens: [option.short, option.long].filter((token) => token !== void 0)
18
+ }));
19
+ var flattenOptionTokens = (options) => {
20
+ const seen = /* @__PURE__ */ new Set();
21
+ const tokens = [];
22
+ for (const option of options) {
23
+ for (const token of option.tokens) {
24
+ if (seen.has(token)) {
25
+ continue;
26
+ }
27
+ seen.add(token);
28
+ tokens.push(token);
29
+ }
30
+ }
31
+ return tokens;
32
+ };
33
+ var findOption = (options, token) => options.find((option) => option.tokens.includes(token));
34
+ var parseOptionToken = (token) => {
35
+ const separatorIndex = token.indexOf("=");
36
+ if (separatorIndex === -1) {
37
+ return { hasInlineValue: false, name: token };
38
+ }
39
+ return {
40
+ hasInlineValue: true,
41
+ name: token.slice(0, separatorIndex)
42
+ };
43
+ };
44
+ var createCompletionTree = (command) => buildCompletionTree(command);
45
+ var resolveCompletionSuggestions = (root, words) => {
46
+ const committedWords = words.length === 0 ? [] : words.slice(0, -1);
47
+ const partialWord = words.at(-1) ?? "";
48
+ let currentNode = root;
49
+ let expectsOptionValue = false;
50
+ for (const word of committedWords) {
51
+ if (expectsOptionValue) {
52
+ expectsOptionValue = false;
53
+ continue;
54
+ }
55
+ if (word.startsWith("-")) {
56
+ const parsedOption = parseOptionToken(word);
57
+ const option = findOption(currentNode.options, parsedOption.name);
58
+ if (option?.expectsValue && !parsedOption.hasInlineValue) {
59
+ expectsOptionValue = true;
60
+ }
61
+ continue;
62
+ }
63
+ const nextNode = currentNode.commands.find((command) => command.name === word);
64
+ if (nextNode !== void 0) {
65
+ currentNode = nextNode;
66
+ }
67
+ }
68
+ if (expectsOptionValue) {
69
+ return [];
70
+ }
71
+ if (partialWord.startsWith("-")) {
72
+ const parsedOption = parseOptionToken(partialWord);
73
+ if (parsedOption.hasInlineValue && findOption(currentNode.options, parsedOption.name) !== void 0) {
74
+ return [];
75
+ }
76
+ return flattenOptionTokens(currentNode.options).filter((token) => token.startsWith(partialWord));
77
+ }
78
+ const commandSuggestions = currentNode.commands.map((command) => command.name).filter((commandName) => commandName.startsWith(partialWord));
79
+ if (partialWord.length > 0) {
80
+ return commandSuggestions;
81
+ }
82
+ return [...commandSuggestions, ...flattenOptionTokens(currentNode.options)];
83
+ };
84
+
85
+ // src/completion/shell.ts
86
+ var renderZshCompletion = (commandName) => `#compdef ${commandName}
87
+
88
+ _${commandName}() {
89
+ local -a suggestions
90
+ suggestions=("\${(@f)$("\${words[1]}" __complete -- "\${words[@]:1:$((CURRENT - 1))}")}")
91
+ _describe 'values' suggestions
92
+ }
93
+
94
+ compdef _${commandName} ${commandName}
95
+ `;
96
+ var renderBashCompletion = (commandName) => `_${commandName}() {
97
+ local -a args=()
98
+ local line
99
+ local -a suggestions=()
100
+
101
+ args=("\${COMP_WORDS[@]:1:$COMP_CWORD}")
102
+ if [ "$COMP_CWORD" -ge "\${#COMP_WORDS[@]}" ]; then
103
+ args+=("")
104
+ fi
105
+
106
+ while IFS= read -r line; do
107
+ suggestions+=("$line")
108
+ done < <("\${COMP_WORDS[0]}" __complete -- "\${args[@]}")
109
+
110
+ COMPREPLY=("\${suggestions[@]}")
111
+ }
112
+
113
+ complete -F _${commandName} ${commandName}
114
+ `;
115
+ var renderFishCompletion = (commandName) => `function __fish_${commandName}_complete
116
+ set -l words (commandline -opc)
117
+ if test (count $words) -gt 0
118
+ set -e words[1]
119
+ end
120
+ set -a words (commandline -ct)
121
+ ${commandName} __complete -- $words
122
+ end
123
+
124
+ complete -c ${commandName} -f -a "(__fish_${commandName}_complete)"
125
+ `;
126
+ var generateCompletionScript = (shell, program) => {
127
+ const commandName = program.name();
128
+ switch (shell) {
129
+ case "zsh":
130
+ return renderZshCompletion(commandName);
131
+ case "bash":
132
+ return renderBashCompletion(commandName);
133
+ case "fish":
134
+ return renderFishCompletion(commandName);
135
+ }
136
+ };
137
+
138
+ // src/help.ts
5
139
  import { Command } from "commander";
140
+ var ITEM_INDENT = 2;
141
+ var SPACER_WIDTH = 2;
142
+ var INCOMPLETE_HELP_ERROR_CODES = /* @__PURE__ */ new Set([
143
+ "commander.missingArgument",
144
+ "commander.missingMandatoryOptionValue",
145
+ "commander.optionMissingArgument"
146
+ ]);
147
+ var buildCommandPath = (command) => {
148
+ const names = [];
149
+ let current = command;
150
+ while (current !== null) {
151
+ names.unshift(current.name());
152
+ current = current.parent;
153
+ }
154
+ return names.join(" ");
155
+ };
156
+ var visibleCommands = (command) => command.commands.filter((subcommand) => !subcommand._hidden);
157
+ var getCommandExamples = (command) => {
158
+ const { _cloudburnExamples } = command;
159
+ return _cloudburnExamples ?? [];
160
+ };
161
+ var getCommandUsageGuidance = (command) => {
162
+ const { _cloudburnUsageGuidance } = command;
163
+ return _cloudburnUsageGuidance ?? null;
164
+ };
165
+ var getHelpScenario = (command) => command._cloudburnRenderingHelpScenario ?? "explicit";
166
+ var inferHelpScenario = (errorCode) => errorCode !== void 0 && INCOMPLETE_HELP_ERROR_CODES.has(errorCode) ? "incomplete" : "error";
167
+ var buildSubcommandGuidance = (command) => {
168
+ const firstSubcommand = visibleCommands(command).at(0);
169
+ if (firstSubcommand === void 0) {
170
+ return null;
171
+ }
172
+ return `Specify one of the available subcommands to continue.
173
+ Try: ${buildCommandPath(command)} ${firstSubcommand.name()}`;
174
+ };
175
+ var buildExampleGuidance = (command) => {
176
+ const [firstExample] = getCommandExamples(command);
177
+ return firstExample === void 0 ? null : `Try: ${firstExample}`;
178
+ };
179
+ var buildRecoveryGuidance = (command, scenario) => {
180
+ if (scenario !== "incomplete") {
181
+ return null;
182
+ }
183
+ return buildSubcommandGuidance(command) ?? buildExampleGuidance(command);
184
+ };
185
+ var formatDescriptionItem = (term, description, termWidth, helper) => {
186
+ const itemIndent = " ".repeat(ITEM_INDENT);
187
+ if (!description.includes("\n")) {
188
+ return helper.formatItem(term, termWidth, description, helper);
189
+ }
190
+ const paddedTerm = term.padEnd(termWidth + term.length - helper.displayWidth(term));
191
+ const continuationIndent = `${itemIndent}${" ".repeat(termWidth)}${" ".repeat(SPACER_WIDTH)}`;
192
+ const [firstLine, ...remainingLines] = description.split("\n");
193
+ return [
194
+ `${itemIndent}${paddedTerm}${" ".repeat(SPACER_WIDTH)}${firstLine}`,
195
+ ...remainingLines.map((line) => `${continuationIndent}${line}`)
196
+ ].join("\n");
197
+ };
198
+ var formatSection = (title, items, termWidth, helper) => {
199
+ if (items.length === 0) {
200
+ return [];
201
+ }
202
+ return [
203
+ helper.styleTitle(title),
204
+ ...items.map(({ description, term }) => formatDescriptionItem(term, description, termWidth, helper)),
205
+ ""
206
+ ];
207
+ };
208
+ var formatExamplesSection = (examples) => {
209
+ if (examples.length === 0) {
210
+ return [];
211
+ }
212
+ return ["Examples:", ...examples.map((example) => ` ${example}`), ""];
213
+ };
214
+ var formatGuidanceBlock = (guidance) => {
215
+ if (guidance === null) {
216
+ return [];
217
+ }
218
+ return [...guidance.trim().split("\n"), ""];
219
+ };
220
+ var formatCloudBurnHelp = (command, helper) => {
221
+ const cloudBurnHelp = helper;
222
+ const helpWidth = helper.helpWidth ?? 80;
223
+ const scenario = getHelpScenario(command);
224
+ const localOptions = helper.visibleOptions(command);
225
+ const globalOptions = helper.visibleGlobalOptions(command);
226
+ const commands = helper.visibleCommands(command);
227
+ const examples = getCommandExamples(command);
228
+ const usageGuidance = getCommandUsageGuidance(command);
229
+ const recoveryGuidance = buildRecoveryGuidance(command, scenario);
230
+ const termWidth = Math.max(
231
+ helper.longestArgumentTermLength(command, helper),
232
+ helper.longestOptionTermLength(command, helper),
233
+ helper.longestGlobalOptionTermLength(command, helper),
234
+ helper.longestSubcommandTermLength(command, helper)
235
+ );
236
+ const output = [];
237
+ const description = helper.commandDescription(command);
238
+ if (!cloudBurnHelp._cloudburnIsErrorOutput && description.length > 0) {
239
+ output.push(helper.boxWrap(helper.styleCommandDescription(description), helpWidth), "");
240
+ }
241
+ output.push(`${helper.styleTitle("Usage:")} ${helper.styleUsage(helper.commandUsage(command))}`, "");
242
+ output.push(...formatExamplesSection(examples));
243
+ output.push(...formatGuidanceBlock(usageGuidance));
244
+ output.push(
245
+ ...formatSection(
246
+ "Available Commands:",
247
+ commands.map((subcommand) => ({
248
+ description: helper.styleSubcommandDescription(helper.subcommandDescription(subcommand)),
249
+ term: helper.styleSubcommandTerm(helper.subcommandTerm(subcommand))
250
+ })),
251
+ termWidth,
252
+ helper
253
+ )
254
+ );
255
+ output.push(
256
+ ...formatSection(
257
+ "Arguments:",
258
+ helper.visibleArguments(command).map((argument) => ({
259
+ description: helper.styleArgumentDescription(helper.argumentDescription(argument)),
260
+ term: helper.styleArgumentTerm(helper.argumentTerm(argument))
261
+ })),
262
+ termWidth,
263
+ helper
264
+ )
265
+ );
266
+ output.push(
267
+ ...formatSection(
268
+ command.parent === null ? "Global Flags:" : "Flags:",
269
+ localOptions.map((option) => ({
270
+ description: helper.styleOptionDescription(helper.optionDescription(option)),
271
+ term: helper.styleOptionTerm(helper.optionTerm(option))
272
+ })),
273
+ termWidth,
274
+ helper
275
+ )
276
+ );
277
+ if (command.parent !== null) {
278
+ output.push(
279
+ ...formatSection(
280
+ "Global Flags:",
281
+ globalOptions.map((option) => ({
282
+ description: helper.styleOptionDescription(helper.optionDescription(option)),
283
+ term: helper.styleOptionTerm(helper.optionTerm(option))
284
+ })),
285
+ termWidth,
286
+ helper
287
+ )
288
+ );
289
+ }
290
+ output.push(...formatGuidanceBlock(recoveryGuidance));
291
+ if (scenario === "error" && commands.length > 0) {
292
+ output.push(`Use "${buildCommandPath(command)} [command] --help" for more information about a command.`, "");
293
+ }
294
+ while (output.at(-1) === "") {
295
+ output.pop();
296
+ }
297
+ return `${output.join("\n")}
298
+ `;
299
+ };
300
+ var prepareCloudBurnHelpContext = function(contextOptions) {
301
+ this._cloudburnIsErrorOutput = !!contextOptions.error;
302
+ this.helpWidth = this.helpWidth ?? contextOptions.helpWidth ?? 80;
303
+ };
304
+ var CloudBurnCommand = class _CloudBurnCommand extends Command {
305
+ _cloudburnExamples;
306
+ _cloudburnPendingHelpScenario;
307
+ _cloudburnRenderingHelpScenario;
308
+ _cloudburnUsageGuidance;
309
+ /**
310
+ * Creates CloudBurn subcommands that inherit the scenario-aware help behavior.
311
+ *
312
+ * @param name - Optional subcommand name supplied by Commander.
313
+ * @returns A new CloudBurn-aware command instance.
314
+ */
315
+ createCommand(name) {
316
+ return new _CloudBurnCommand(name);
317
+ }
318
+ /**
319
+ * Captures the error scenario so Commander help-after-error renders the correct layout.
320
+ *
321
+ * @param message - Error text emitted by Commander.
322
+ * @param errorOptions - Commander exit metadata including the error code.
323
+ * @returns Never returns because Commander exits or throws via exitOverride.
324
+ */
325
+ error(message, errorOptions) {
326
+ this._cloudburnPendingHelpScenario = inferHelpScenario(errorOptions?.code);
327
+ return super.error(message, errorOptions);
328
+ }
329
+ /**
330
+ * Returns help text using the shared CloudBurn scenario rules.
331
+ *
332
+ * @param context - Commander help context.
333
+ * @returns The formatted help text.
334
+ */
335
+ helpInformation(context) {
336
+ return this.renderWithHelpScenario(context, () => super.helpInformation(context));
337
+ }
338
+ renderWithHelpScenario(context, render) {
339
+ const previousScenario = this._cloudburnRenderingHelpScenario;
340
+ const pendingScenario = this._cloudburnPendingHelpScenario;
341
+ this._cloudburnRenderingHelpScenario = context?.error ? pendingScenario ?? "error" : pendingScenario ?? "explicit";
342
+ this._cloudburnPendingHelpScenario = void 0;
343
+ try {
344
+ return render();
345
+ } finally {
346
+ this._cloudburnRenderingHelpScenario = previousScenario;
347
+ }
348
+ }
349
+ };
350
+ var outputScenarioHelp = (command, scenario) => {
351
+ const cloudBurnCommand = command;
352
+ const previousScenario = cloudBurnCommand._cloudburnPendingHelpScenario;
353
+ cloudBurnCommand._cloudburnPendingHelpScenario = scenario;
354
+ try {
355
+ command.outputHelp();
356
+ } finally {
357
+ cloudBurnCommand._cloudburnPendingHelpScenario = previousScenario;
358
+ }
359
+ };
360
+ var createCliCommand = () => new CloudBurnCommand();
361
+ var createHelpConfiguration = () => ({
362
+ formatHelp: formatCloudBurnHelp,
363
+ prepareContext: prepareCloudBurnHelpContext,
364
+ showGlobalOptions: true,
365
+ visibleCommands
366
+ });
367
+ var configureCliHelp = (program) => {
368
+ program.configureHelp(createHelpConfiguration());
369
+ program.showHelpAfterError();
370
+ program.showSuggestionAfterError();
371
+ };
372
+ var registerParentCommand = (parent, name, description) => parent.command(name).description(description).usage("[command]").allowExcessArguments(true).action(function() {
373
+ if (this.args.length > 0) {
374
+ this.unknownCommand();
375
+ return;
376
+ }
377
+ outputScenarioHelp(this, "incomplete");
378
+ });
379
+ var setCommandExamples = (command, examples) => {
380
+ command._cloudburnExamples = examples;
381
+ return command;
382
+ };
383
+ var setCommandUsageGuidance = (command, guidance) => {
384
+ command._cloudburnUsageGuidance = guidance.trim();
385
+ return command;
386
+ };
387
+
388
+ // src/commands/completion.ts
389
+ var getRootCommand = (command) => {
390
+ let currentCommand = command;
391
+ while (currentCommand.parent !== null) {
392
+ currentCommand = currentCommand.parent;
393
+ }
394
+ return currentCommand;
395
+ };
396
+ var getCompletionHelpText = (shell) => {
397
+ switch (shell) {
398
+ case "bash":
399
+ return `
400
+ To load completions in your current shell session:
401
+
402
+ source <(cloudburn completion bash)
403
+
404
+ To load completions for every new session, add this to your shell config:
405
+
406
+ source <(cloudburn completion bash)
407
+ `;
408
+ case "fish":
409
+ return `
410
+ To load completions in your current shell session:
411
+
412
+ cloudburn completion fish | source
413
+
414
+ To load completions for every new session, add this to your shell config:
415
+
416
+ cloudburn completion fish | source
417
+ `;
418
+ case "zsh":
419
+ return `
420
+ If shell completion is not already enabled in your environment you will need
421
+ to enable it. You can execute the following once:
422
+
423
+ echo "autoload -U compinit; compinit" >> ~/.zshrc
424
+
425
+ To load completions in your current shell session:
426
+
427
+ source <(cloudburn completion zsh)
428
+
429
+ To load completions for every new session, add this to your shell config:
430
+
431
+ source <(cloudburn completion zsh)
432
+ `;
433
+ }
434
+ };
435
+ var registerCompletionCommand = (program) => {
436
+ const completionCommand = registerParentCommand(
437
+ program,
438
+ "completion",
439
+ "Generate shell completion scripts for CloudBurn."
440
+ );
441
+ for (const shell of ["bash", "fish", "zsh"]) {
442
+ setCommandUsageGuidance(
443
+ 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));
445
+ process.stdout.write(output);
446
+ }),
447
+ getCompletionHelpText(shell)
448
+ );
449
+ }
450
+ program.command("__complete", { hidden: true }).argument("[words...]").allowUnknownOption().action(function(words) {
451
+ const suggestions = resolveCompletionSuggestions(createCompletionTree(getRootCommand(this)), words ?? []);
452
+ if (suggestions.length === 0) {
453
+ return;
454
+ }
455
+ process.stdout.write(`${suggestions.join("\n")}
456
+ `);
457
+ });
458
+ };
6
459
 
7
460
  // src/commands/discover.ts
8
461
  import { assertValidAwsRegion, CloudBurnClient } from "@cloudburn/sdk";
@@ -89,7 +542,7 @@ var scanColumns = [
89
542
  { key: "startColumn", header: "StartColumn" },
90
543
  { key: "message", header: "Message" }
91
544
  ];
92
- var formatOptionDescription = "Output format. table: human-readable terminal output. text: tab-delimited output for grep, sed, and awk. json: machine-readable output for automation and downstream systems.";
545
+ 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.";
93
546
  var OUTPUT_FORMAT_OPTION_DESCRIPTION = formatOptionDescription;
94
547
  var parseOutputFormat = (value) => {
95
548
  if (supportedOutputFormats.includes(value)) {
@@ -279,39 +732,38 @@ var runCommand = async (action) => {
279
732
  }
280
733
  };
281
734
  var registerDiscoverCommand = (program) => {
282
- const discoverCommand = program.command("discover").description("Run a live AWS discovery").enablePositionalOptions().option(
283
- "--region <region>",
284
- 'Discovery region to use. Pass "all" to require an aggregator index.',
285
- parseDiscoverRegion
286
- ).option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
287
- "after",
288
- `
289
- Examples:
290
- cloudburn discover
291
- cloudburn discover --region eu-central-1
292
- cloudburn discover --region all
293
- cloudburn discover list-enabled-regions
294
- cloudburn discover init
295
- `
296
- ).action(async (options, command) => {
297
- await runCommand(async () => {
298
- const scanner = new CloudBurnClient();
299
- const result = await scanner.discover({ target: resolveDiscoveryTarget(options.region) });
300
- const format = resolveOutputFormat(command, options.format);
301
- const output = renderResponse({ kind: "scan-result", result }, format);
302
- process.stdout.write(`${output}
735
+ const discoverCommand = setCommandExamples(
736
+ program.command("discover").description("Run a live AWS discovery").enablePositionalOptions().option(
737
+ "--region <region>",
738
+ 'Discovery region to use. Pass "all" to require an aggregator index.',
739
+ parseDiscoverRegion
740
+ ).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
741
+ await runCommand(async () => {
742
+ const scanner = new CloudBurnClient();
743
+ const result = await scanner.discover({ target: resolveDiscoveryTarget(options.region) });
744
+ const format = resolveOutputFormat(command);
745
+ const output = renderResponse({ kind: "scan-result", result }, format);
746
+ process.stdout.write(`${output}
303
747
  `);
304
- if (options.exitCode && countScanResultFindings(result) > 0) {
305
- return EXIT_CODE_POLICY_VIOLATION;
306
- }
307
- return EXIT_CODE_OK;
308
- });
309
- });
310
- discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action(async (options, command) => {
748
+ if (options.exitCode && countScanResultFindings(result) > 0) {
749
+ return EXIT_CODE_POLICY_VIOLATION;
750
+ }
751
+ return EXIT_CODE_OK;
752
+ });
753
+ }),
754
+ [
755
+ "cloudburn discover",
756
+ "cloudburn discover --region eu-central-1",
757
+ "cloudburn discover --region all",
758
+ "cloudburn discover list-enabled-regions",
759
+ "cloudburn discover init"
760
+ ]
761
+ );
762
+ discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").action(async (_options, command) => {
311
763
  await runCommand(async () => {
312
764
  const scanner = new CloudBurnClient();
313
765
  const regions = await scanner.listEnabledDiscoveryRegions();
314
- const format = resolveOutputFormat(command, options.format);
766
+ const format = resolveOutputFormat(command);
315
767
  const output = renderResponse(
316
768
  {
317
769
  kind: "record-list",
@@ -329,14 +781,14 @@ Examples:
329
781
  return EXIT_CODE_OK;
330
782
  });
331
783
  });
332
- discoverCommand.command("init").description("Set up AWS Resource Explorer for CloudBurn").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--region <region>", "Aggregator region to create or reuse during setup", parseAwsRegion).action(async (options, command) => {
784
+ 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) => {
333
785
  await runCommand(async () => {
334
786
  const scanner = new CloudBurnClient();
335
787
  const parentRegion = discoverCommand.opts().region;
336
788
  const region = options.region ?? (parentRegion === "all" ? void 0 : parentRegion);
337
789
  const result = await scanner.initializeDiscovery({ region });
338
790
  const message = result.status === "EXISTING" ? `Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Resource Explorer setup created in ${result.aggregatorRegion}.`;
339
- const format = resolveOutputFormat(command, options.format);
791
+ const format = resolveOutputFormat(command);
340
792
  const output = renderResponse(
341
793
  {
342
794
  kind: "status",
@@ -356,11 +808,11 @@ Examples:
356
808
  return EXIT_CODE_OK;
357
809
  });
358
810
  });
359
- discoverCommand.command("supported-resource-types").description("List Resource Explorer supported AWS resource types").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action(async (options, command) => {
811
+ discoverCommand.command("supported-resource-types").description("List Resource Explorer supported AWS resource types").action(async (_options, command) => {
360
812
  await runCommand(async () => {
361
813
  const scanner = new CloudBurnClient();
362
814
  const resourceTypes = await scanner.listSupportedDiscoveryResourceTypes();
363
- const format = resolveOutputFormat(command, options.format);
815
+ const format = resolveOutputFormat(command);
364
816
  const output = renderResponse(
365
817
  {
366
818
  kind: "record-list",
@@ -385,8 +837,8 @@ Examples:
385
837
 
386
838
  // src/commands/estimate.ts
387
839
  var registerEstimateCommand = (program) => {
388
- program.command("estimate").description("Request optional pricing estimates from a self-hosted dashboard").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--server <url>", "Dashboard API base URL").action((options, command) => {
389
- const format = resolveOutputFormat(command, options.format);
840
+ program.command("estimate").description("Request optional pricing estimates from a self-hosted dashboard").option("--server <url>", "Dashboard API base URL").action((options, command) => {
841
+ const format = resolveOutputFormat(command);
390
842
  if (!options.server) {
391
843
  const output2 = renderResponse(
392
844
  {
@@ -431,14 +883,14 @@ rules:
431
883
  severity: error
432
884
  `;
433
885
  var registerInitCommand = (program) => {
434
- program.command("init").description("Print a starter .cloudburn.yml configuration").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action((options, command) => {
886
+ program.command("init").description("Print a starter .cloudburn.yml configuration").action(function() {
435
887
  const output = renderResponse(
436
888
  {
437
889
  kind: "document",
438
890
  content: starterConfig,
439
891
  contentType: "application/yaml"
440
892
  },
441
- resolveOutputFormat(command, options.format, "text")
893
+ resolveOutputFormat(this, void 0, "text")
442
894
  );
443
895
  process.stdout.write(`${output}
444
896
  `);
@@ -448,15 +900,15 @@ var registerInitCommand = (program) => {
448
900
  // src/commands/rules-list.ts
449
901
  import { builtInRuleMetadata } from "@cloudburn/sdk";
450
902
  var registerRulesListCommand = (program) => {
451
- const rulesCommand = program.command("rules").description("Inspect built-in CloudBurn rules");
452
- rulesCommand.command("list").description("List built-in CloudBurn rules").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action((options, command) => {
903
+ const rulesCommand = registerParentCommand(program, "rules", "Inspect built-in CloudBurn rules");
904
+ rulesCommand.command("list").description("List built-in CloudBurn rules").action(function() {
453
905
  const output = renderResponse(
454
906
  {
455
907
  kind: "rule-list",
456
908
  emptyMessage: "No built-in rules are available.",
457
909
  rules: builtInRuleMetadata
458
910
  },
459
- resolveOutputFormat(command, options.format, "text")
911
+ resolveOutputFormat(this, void 0, "text")
460
912
  );
461
913
  process.stdout.write(`${output}
462
914
  `);
@@ -466,39 +918,36 @@ var registerRulesListCommand = (program) => {
466
918
  // src/commands/scan.ts
467
919
  import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
468
920
  var registerScanCommand = (program) => {
469
- program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
470
- "after",
471
- `
472
- Examples:
473
- cloudburn scan ./main.tf
474
- cloudburn scan ./template.yaml
475
- cloudburn scan ./iac
476
- `
477
- ).action(async (path, options, command) => {
478
- try {
479
- const scanner = new CloudBurnClient2();
480
- const result = await scanner.scanStatic(path ?? process.cwd());
481
- const format = resolveOutputFormat(command, options.format);
482
- const output = renderResponse({ kind: "scan-result", result }, format);
483
- process.stdout.write(`${output}
921
+ setCommandExamples(
922
+ program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--exit-code", "Exit with code 1 when findings exist").action(async (path, options, command) => {
923
+ try {
924
+ const scanner = new CloudBurnClient2();
925
+ const result = await scanner.scanStatic(path ?? process.cwd());
926
+ const format = resolveOutputFormat(command);
927
+ const output = renderResponse({ kind: "scan-result", result }, format);
928
+ process.stdout.write(`${output}
484
929
  `);
485
- if (options.exitCode && countScanResultFindings(result) > 0) {
486
- process.exitCode = EXIT_CODE_POLICY_VIOLATION;
487
- return;
488
- }
489
- process.exitCode = EXIT_CODE_OK;
490
- } catch (err) {
491
- process.stderr.write(`${formatError(err)}
930
+ if (options.exitCode && countScanResultFindings(result) > 0) {
931
+ process.exitCode = EXIT_CODE_POLICY_VIOLATION;
932
+ return;
933
+ }
934
+ process.exitCode = EXIT_CODE_OK;
935
+ } catch (err) {
936
+ process.stderr.write(`${formatError(err)}
492
937
  `);
493
- process.exitCode = EXIT_CODE_RUNTIME_ERROR;
494
- }
495
- });
938
+ process.exitCode = EXIT_CODE_RUNTIME_ERROR;
939
+ }
940
+ }),
941
+ ["cloudburn scan ./main.tf", "cloudburn scan ./template.yaml", "cloudburn scan ./iac"]
942
+ );
496
943
  };
497
944
 
498
945
  // src/cli.ts
499
946
  var createProgram = () => {
500
- const program = new Command();
501
- program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.6.2").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
947
+ const program = createCliCommand();
948
+ program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.7.0").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
949
+ configureCliHelp(program);
950
+ registerCompletionCommand(program);
502
951
  registerDiscoverCommand(program);
503
952
  registerScanCommand(program);
504
953
  registerInitCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
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.10.0"
14
+ "@cloudburn/sdk": "0.11.0"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@biomejs/biome": "^2.4.6",