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.
- package/README.md +33 -0
- package/dist/cli.js +517 -68
- 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 = "
|
|
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 =
|
|
283
|
-
"
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
|
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("--
|
|
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
|
|
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").
|
|
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
|
|
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("--
|
|
389
|
-
const format = resolveOutputFormat(command
|
|
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").
|
|
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(
|
|
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
|
|
452
|
-
rulesCommand.command("list").description("List built-in CloudBurn rules").
|
|
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(
|
|
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
|
-
|
|
470
|
-
"
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|
-
|
|
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 =
|
|
501
|
-
program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.
|
|
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.
|
|
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.
|
|
14
|
+
"@cloudburn/sdk": "0.11.0"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@biomejs/biome": "^2.4.6",
|