cloudburn 0.5.1 → 0.6.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 (2) hide show
  1. package/dist/cli.js +285 -117
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -5,11 +5,8 @@ import { pathToFileURL } from "url";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/commands/discover.ts
8
- import {
9
- assertValidAwsRegion,
10
- CloudBurnClient
11
- } from "@cloudburn/sdk";
12
- import { InvalidArgumentError } from "commander";
8
+ import { assertValidAwsRegion, CloudBurnClient } from "@cloudburn/sdk";
9
+ import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
13
10
 
14
11
  // src/exit-codes.ts
15
12
  var EXIT_CODE_OK = 0;
@@ -59,8 +56,8 @@ var categorize = (err) => {
59
56
  };
60
57
  };
61
58
 
62
- // src/formatters/json.ts
63
- var formatJson = (result) => JSON.stringify(result, null, 2);
59
+ // src/formatters/output.ts
60
+ import { InvalidArgumentError } from "commander";
64
61
 
65
62
  // src/formatters/shared.ts
66
63
  var flattenScanResult = (result) => result.providers.flatMap(
@@ -77,82 +74,192 @@ var flattenScanResult = (result) => result.providers.flatMap(
77
74
  );
78
75
  var countScanResultFindings = (result) => flattenScanResult(result).length;
79
76
 
80
- // src/formatters/sarif.ts
81
- var toSarifLocation = (finding) => {
82
- if (!finding.location) {
83
- return void 0;
77
+ // src/formatters/output.ts
78
+ var supportedOutputFormats = ["text", "json", "table"];
79
+ var scanColumns = [
80
+ { key: "provider", header: "Provider" },
81
+ { key: "ruleId", header: "RuleId" },
82
+ { key: "source", header: "Source" },
83
+ { key: "service", header: "Service" },
84
+ { key: "resourceId", header: "ResourceId" },
85
+ { key: "accountId", header: "AccountId" },
86
+ { key: "region", header: "Region" },
87
+ { key: "path", header: "Path" },
88
+ { key: "startLine", header: "StartLine" },
89
+ { key: "startColumn", header: "StartColumn" },
90
+ { key: "message", header: "Message" }
91
+ ];
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.";
93
+ var OUTPUT_FORMAT_OPTION_DESCRIPTION = formatOptionDescription;
94
+ var parseOutputFormat = (value) => {
95
+ if (supportedOutputFormats.includes(value)) {
96
+ return value;
84
97
  }
85
- return [
86
- {
87
- physicalLocation: {
88
- artifactLocation: {
89
- uri: finding.location.path
90
- },
91
- region: {
92
- startLine: finding.location.startLine,
93
- startColumn: finding.location.startColumn,
94
- ...finding.location.endLine ? { endLine: finding.location.endLine } : {},
95
- ...finding.location.endColumn ? { endColumn: finding.location.endColumn } : {}
96
- }
97
- }
98
+ throw new InvalidArgumentError(`Invalid format "${value}". Allowed formats: ${supportedOutputFormats.join(", ")}.`);
99
+ };
100
+ var resolveOutputFormat = (command, localFormat, defaultFormat = "table") => {
101
+ if (localFormat) {
102
+ return localFormat;
103
+ }
104
+ const options = typeof command.optsWithGlobals === "function" ? command.optsWithGlobals() : command.opts();
105
+ return options.format ?? defaultFormat;
106
+ };
107
+ var renderResponse = (response, format) => {
108
+ switch (format) {
109
+ case "json":
110
+ return renderJson(response);
111
+ case "text":
112
+ return renderText(response);
113
+ case "table":
114
+ return renderTable(response);
115
+ }
116
+ };
117
+ var renderJson = (response) => {
118
+ switch (response.kind) {
119
+ case "document":
120
+ return JSON.stringify({ content: response.content, contentType: response.contentType }, null, 2);
121
+ case "record-list":
122
+ return JSON.stringify(response.rows, null, 2);
123
+ case "rule-list":
124
+ return JSON.stringify(response.rules, null, 2);
125
+ case "scan-result":
126
+ return JSON.stringify(response.result, null, 2);
127
+ case "status":
128
+ return JSON.stringify(response.data, null, 2);
129
+ case "string-list":
130
+ return JSON.stringify(response.values, null, 2);
131
+ }
132
+ };
133
+ var renderText = (response) => {
134
+ switch (response.kind) {
135
+ case "document":
136
+ return response.content;
137
+ case "record-list":
138
+ return renderTextRows(response.rows, response.columns, response.emptyMessage);
139
+ case "rule-list":
140
+ return renderRuleList(response.rules, response.emptyMessage);
141
+ case "scan-result":
142
+ return renderTextRows(projectScanRows(response.result), scanColumns, "No findings.");
143
+ case "status":
144
+ return response.text;
145
+ case "string-list":
146
+ return response.values.length === 0 ? response.emptyMessage : response.values.join("\n");
147
+ }
148
+ };
149
+ var renderTable = (response) => {
150
+ switch (response.kind) {
151
+ case "document":
152
+ return renderAsciiTable(
153
+ [
154
+ { Field: "ContentType", Value: response.contentType },
155
+ { Field: "Content", Value: response.content }
156
+ ],
157
+ [
158
+ { key: "Field", header: "Field" },
159
+ { key: "Value", header: "Value" }
160
+ ]
161
+ );
162
+ case "record-list":
163
+ return response.rows.length === 0 ? response.emptyMessage : renderAsciiTable(response.rows, response.columns ?? inferColumns(response.rows));
164
+ case "rule-list":
165
+ return renderRuleList(response.rules, response.emptyMessage);
166
+ case "scan-result": {
167
+ const rows = projectScanRows(response.result);
168
+ return rows.length === 0 ? "No findings." : renderAsciiTable(rows, scanColumns);
98
169
  }
99
- ];
170
+ case "status":
171
+ return renderAsciiTable(
172
+ Object.entries(response.data).map(([field, value]) => ({ Field: field, Value: value })),
173
+ [
174
+ { key: "Field", header: "Field" },
175
+ { key: "Value", header: "Value" }
176
+ ]
177
+ );
178
+ case "string-list":
179
+ return response.values.length === 0 ? response.emptyMessage : renderAsciiTable(
180
+ response.values.map((value) => ({ [response.columnHeader]: value })),
181
+ [{ key: response.columnHeader, header: response.columnHeader }]
182
+ );
183
+ }
100
184
  };
101
- var formatSarif = (result) => JSON.stringify(
102
- {
103
- version: "2.1.0",
104
- runs: [
105
- {
106
- tool: {
107
- driver: {
108
- name: "cloudburn"
109
- }
110
- },
111
- results: flattenScanResult(result).map(({ ruleId, message, finding }) => ({
112
- ruleId,
113
- // Severity was intentionally removed — all findings are warnings until a priority model is added.
114
- level: "warning",
115
- message: { text: message },
116
- ...finding.location ? { locations: toSarifLocation(finding) } : {}
117
- }))
118
- }
119
- ]
120
- },
121
- null,
122
- 2
123
- );
124
-
125
- // src/formatters/table.ts
126
- var formatTable = (result) => {
127
- const flattenedFindings = flattenScanResult(result);
128
- if (flattenedFindings.length === 0) {
129
- return "No findings.";
185
+ var projectScanRows = (result) => flattenScanResult(result).map(({ finding, message, provider, ruleId, service, source }) => ({
186
+ accountId: finding.accountId ?? "",
187
+ message,
188
+ path: finding.location?.path ?? "",
189
+ provider,
190
+ region: finding.region ?? "",
191
+ resourceId: finding.resourceId,
192
+ ruleId,
193
+ service,
194
+ source,
195
+ startColumn: finding.location?.startColumn ?? "",
196
+ startLine: finding.location?.startLine ?? ""
197
+ }));
198
+ var renderTextRows = (rows, columns, emptyMessage) => {
199
+ if (rows.length === 0) {
200
+ return emptyMessage;
130
201
  }
131
- return flattenedFindings.map(({ provider, ruleId, source, service, message, finding }) => {
132
- const location = finding.location ? ` ${finding.location.path}:${finding.location.startLine}:${finding.location.startColumn}` : "";
133
- return `${provider} ${ruleId} ${source} ${service} ${finding.resourceId}${location} ${message}`;
134
- }).join("\n");
202
+ const resolvedColumns = columns ?? inferColumns(rows);
203
+ return rows.map((row) => resolvedColumns.map((column) => toTextCell(row[column.key])).join(" ")).join("\n");
135
204
  };
136
-
137
- // src/commands/discover.ts
138
- var supportedDiscoverFormats = ["table", "json", "sarif"];
139
- var parseDiscoverFormat = (value) => {
140
- if (supportedDiscoverFormats.includes(value)) {
141
- return value;
205
+ var inferColumns = (rows) => {
206
+ const keys = Array.from(new Set(rows.flatMap((row) => Object.keys(row)))).sort(
207
+ (left, right) => left.localeCompare(right)
208
+ );
209
+ return keys.map((key) => ({ key, header: key }));
210
+ };
211
+ var renderRuleList = (rules, emptyMessage) => {
212
+ if (rules.length === 0) {
213
+ return emptyMessage;
142
214
  }
143
- throw new InvalidArgumentError(`Invalid format "${value}". Allowed formats: ${supportedDiscoverFormats.join(", ")}.`);
215
+ let currentProvider = "";
216
+ let currentService = "";
217
+ const lines = [];
218
+ for (const rule of rules) {
219
+ if (rule.provider !== currentProvider) {
220
+ currentProvider = rule.provider;
221
+ currentService = "";
222
+ lines.push(rule.provider);
223
+ }
224
+ if (rule.service !== currentService) {
225
+ currentService = rule.service;
226
+ lines.push(` ${rule.service}`);
227
+ }
228
+ lines.push(` ${rule.id}: ${rule.description}`);
229
+ }
230
+ return lines.join("\n");
144
231
  };
145
- var parseDiscoverListFormat = (value) => {
146
- if (value === "table" || value === "json") {
232
+ var toTextCell = (value) => {
233
+ if (value === null || value === void 0) {
234
+ return "";
235
+ }
236
+ if (typeof value === "string") {
147
237
  return value;
148
238
  }
149
- throw new InvalidArgumentError("Invalid format. Allowed formats: table, json.");
239
+ if (typeof value === "number" || typeof value === "boolean") {
240
+ return String(value);
241
+ }
242
+ return JSON.stringify(value);
243
+ };
244
+ var toTableCell = (value) => toTextCell(value).replace(/\r?\n/g, "\\n");
245
+ var renderAsciiTable = (rows, columns) => {
246
+ const widths = columns.map(
247
+ (column) => Math.max(column.header.length, ...rows.map((row) => toTableCell(row[column.key]).length))
248
+ );
249
+ const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
250
+ const header = `| ${columns.map((column, index) => column.header.padEnd(widths[index] ?? 0)).join(" | ")} |`;
251
+ const body = rows.map(
252
+ (row) => `| ${columns.map((column, index) => toTableCell(row[column.key]).padEnd(widths[index] ?? 0)).join(" | ")} |`
253
+ );
254
+ return [border, header, border, ...body, border].join("\n");
150
255
  };
256
+
257
+ // src/commands/discover.ts
151
258
  var parseAwsRegion = (value) => {
152
259
  try {
153
260
  return assertValidAwsRegion(value);
154
261
  } catch (err) {
155
- throw new InvalidArgumentError(err instanceof Error ? err.message : "Invalid AWS region.");
262
+ throw new InvalidArgumentError2(err instanceof Error ? err.message : "Invalid AWS region.");
156
263
  }
157
264
  };
158
265
  var parseDiscoverRegion = (value) => {
@@ -162,16 +269,6 @@ var parseDiscoverRegion = (value) => {
162
269
  return parseAwsRegion(value);
163
270
  };
164
271
  var resolveDiscoveryTarget = (region) => region === void 0 ? { mode: "current" } : region === "all" ? { mode: "all" } : { mode: "region", region };
165
- var scanFormatters = {
166
- json: formatJson,
167
- sarif: formatSarif,
168
- table: formatTable
169
- };
170
- var formatValue = (value) => JSON.stringify(value, null, 2);
171
- var formatEnabledRegions = (regions) => regions.length === 0 ? "No Resource Explorer indexes are enabled." : regions.map(({ region, type }) => `${region} ${type}`).join("\n");
172
- var formatSupportedResourceTypes = (resourceTypes) => resourceTypes.length === 0 ? "No supported resource types were returned." : resourceTypes.map(({ resourceType, service }) => `${resourceType} ${service ?? "unknown"}`).join("\n");
173
- var formatInitializationResult = (result) => result.status === "EXISTING" ? `Resource Explorer setup already exists in ${result.aggregatorRegion}.` : `Resource Explorer setup created in ${result.aggregatorRegion}.`;
174
- var resolveListFormat = (parentCommand, options) => parentCommand.opts().format === "json" ? "json" : options.format ?? "table";
175
272
  var runCommand = async (action) => {
176
273
  try {
177
274
  process.exitCode = await action() ?? EXIT_CODE_OK;
@@ -186,7 +283,7 @@ var registerDiscoverCommand = (program) => {
186
283
  "--region <region>",
187
284
  'Discovery region to use. Pass "all" to require an aggregator index.',
188
285
  parseDiscoverRegion
189
- ).option("--format <format>", "Output format: table|json|sarif", parseDiscoverFormat, "table").option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
286
+ ).option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
190
287
  "after",
191
288
  `
192
289
  Examples:
@@ -196,11 +293,12 @@ Examples:
196
293
  cloudburn discover list-enabled-regions
197
294
  cloudburn discover init
198
295
  `
199
- ).action(async (options) => {
296
+ ).action(async (options, command) => {
200
297
  await runCommand(async () => {
201
298
  const scanner = new CloudBurnClient();
202
299
  const result = await scanner.discover({ target: resolveDiscoveryTarget(options.region) });
203
- const output = scanFormatters[options.format ?? "table"](result);
300
+ const format = resolveOutputFormat(command, options.format);
301
+ const output = renderResponse({ kind: "scan-result", result }, format);
204
302
  process.stdout.write(`${output}
205
303
  `);
206
304
  if (options.exitCode && countScanResultFindings(result) > 0) {
@@ -209,34 +307,75 @@ Examples:
209
307
  return EXIT_CODE_OK;
210
308
  });
211
309
  });
212
- discoverCommand.command("list-enabled-regions").description("List AWS regions with a local or aggregator Resource Explorer index").option("--format <format>", "Output format: table|json", parseDiscoverListFormat, "table").action(async (options) => {
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) => {
213
311
  await runCommand(async () => {
214
312
  const scanner = new CloudBurnClient();
215
313
  const regions = await scanner.listEnabledDiscoveryRegions();
216
- const format = resolveListFormat(discoverCommand, options);
217
- const output = format === "json" ? formatValue(regions) : formatEnabledRegions(regions);
314
+ const format = resolveOutputFormat(command, options.format);
315
+ const output = renderResponse(
316
+ {
317
+ kind: "record-list",
318
+ columns: [
319
+ { key: "region", header: "Region" },
320
+ { key: "type", header: "Type" }
321
+ ],
322
+ emptyMessage: "No Resource Explorer indexes are enabled.",
323
+ rows: regions
324
+ },
325
+ format
326
+ );
218
327
  process.stdout.write(`${output}
219
328
  `);
220
329
  return EXIT_CODE_OK;
221
330
  });
222
331
  });
223
- 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) => {
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) => {
224
333
  await runCommand(async () => {
225
334
  const scanner = new CloudBurnClient();
226
335
  const parentRegion = discoverCommand.opts().region;
227
336
  const region = options.region ?? (parentRegion === "all" ? void 0 : parentRegion);
228
337
  const result = await scanner.initializeDiscovery({ region });
229
- process.stdout.write(`${formatInitializationResult(result)}
338
+ 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);
340
+ const output = renderResponse(
341
+ {
342
+ kind: "status",
343
+ data: {
344
+ aggregatorRegion: result.aggregatorRegion,
345
+ message,
346
+ regions: result.regions,
347
+ status: result.status,
348
+ taskId: result.taskId ?? ""
349
+ },
350
+ text: message
351
+ },
352
+ format
353
+ );
354
+ process.stdout.write(`${output}
230
355
  `);
231
356
  return EXIT_CODE_OK;
232
357
  });
233
358
  });
234
- discoverCommand.command("supported-resource-types").description("List Resource Explorer supported AWS resource types").option("--format <format>", "Output format: table|json", parseDiscoverListFormat, "table").action(async (options) => {
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) => {
235
360
  await runCommand(async () => {
236
361
  const scanner = new CloudBurnClient();
237
362
  const resourceTypes = await scanner.listSupportedDiscoveryResourceTypes();
238
- const format = resolveListFormat(discoverCommand, options);
239
- const output = format === "json" ? formatValue(resourceTypes) : formatSupportedResourceTypes(resourceTypes);
363
+ const format = resolveOutputFormat(command, options.format);
364
+ const output = renderResponse(
365
+ {
366
+ kind: "record-list",
367
+ columns: [
368
+ { key: "resourceType", header: "ResourceType" },
369
+ { key: "service", header: "Service" }
370
+ ],
371
+ emptyMessage: "No supported resource types were returned.",
372
+ rows: resourceTypes.map((resourceType) => ({
373
+ resourceType: resourceType.resourceType,
374
+ service: resourceType.service ?? "unknown"
375
+ }))
376
+ },
377
+ format
378
+ );
240
379
  process.stdout.write(`${output}
241
380
  `);
242
381
  return EXIT_CODE_OK;
@@ -246,12 +385,38 @@ Examples:
246
385
 
247
386
  // src/commands/estimate.ts
248
387
  var registerEstimateCommand = (program) => {
249
- program.command("estimate").description("Request optional pricing estimates from a self-hosted dashboard").option("--server <url>", "Dashboard API base URL").action((options) => {
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);
250
390
  if (!options.server) {
251
- process.stdout.write("No server configured. This command is optional and requires a dashboard URL.\n");
391
+ const output2 = renderResponse(
392
+ {
393
+ kind: "status",
394
+ data: {
395
+ message: "No server configured. This command is optional and requires a dashboard URL.",
396
+ server: "",
397
+ status: "NOT_CONFIGURED"
398
+ },
399
+ text: "No server configured. This command is optional and requires a dashboard URL."
400
+ },
401
+ format
402
+ );
403
+ process.stdout.write(`${output2}
404
+ `);
252
405
  return;
253
406
  }
254
- process.stdout.write(`Estimate request scaffold ready for server: ${options.server}
407
+ const output = renderResponse(
408
+ {
409
+ kind: "status",
410
+ data: {
411
+ message: `Estimate request scaffold ready for server: ${options.server}`,
412
+ server: options.server,
413
+ status: "READY"
414
+ },
415
+ text: `Estimate request scaffold ready for server: ${options.server}`
416
+ },
417
+ format
418
+ );
419
+ process.stdout.write(`${output}
255
420
  `);
256
421
  });
257
422
  };
@@ -266,39 +431,42 @@ rules:
266
431
  severity: error
267
432
  `;
268
433
  var registerInitCommand = (program) => {
269
- program.command("init").description("Print a starter .cloudburn.yml configuration").action(() => {
270
- process.stdout.write(`${starterConfig}
434
+ program.command("init").description("Print a starter .cloudburn.yml configuration").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat).action((options, command) => {
435
+ const output = renderResponse(
436
+ {
437
+ kind: "document",
438
+ content: starterConfig,
439
+ contentType: "application/yaml"
440
+ },
441
+ resolveOutputFormat(command, options.format, "text")
442
+ );
443
+ process.stdout.write(`${output}
271
444
  `);
272
445
  });
273
446
  };
274
447
 
275
448
  // src/commands/rules-list.ts
276
- import { awsCorePreset } from "@cloudburn/sdk";
449
+ import { builtInRuleMetadata } from "@cloudburn/sdk";
277
450
  var registerRulesListCommand = (program) => {
278
- const rulesCommand = program.command("rules").description("Inspect built-in and custom rules");
279
- rulesCommand.command("list").description("List built-in CloudBurn rule IDs").action(() => {
280
- process.stdout.write(`${awsCorePreset.ruleIds.join("\n")}
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) => {
453
+ const output = renderResponse(
454
+ {
455
+ kind: "rule-list",
456
+ emptyMessage: "No built-in rules are available.",
457
+ rules: builtInRuleMetadata
458
+ },
459
+ resolveOutputFormat(command, options.format, "text")
460
+ );
461
+ process.stdout.write(`${output}
281
462
  `);
282
463
  });
283
464
  };
284
465
 
285
466
  // src/commands/scan.ts
286
467
  import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
287
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
288
- var supportedScanFormats = ["table", "json", "sarif"];
289
- var parseScanFormat = (value) => {
290
- if (supportedScanFormats.includes(value)) {
291
- return value;
292
- }
293
- throw new InvalidArgumentError2(`Invalid format "${value}". Allowed formats: ${supportedScanFormats.join(", ")}.`);
294
- };
295
- var formatters = {
296
- json: formatJson,
297
- sarif: formatSarif,
298
- table: formatTable
299
- };
300
468
  var registerScanCommand = (program) => {
301
- 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: table|json|sarif", parseScanFormat, "table").option("--exit-code", "Exit with code 1 when findings exist").addHelpText(
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(
302
470
  "after",
303
471
  `
304
472
  Examples:
@@ -306,12 +474,12 @@ Examples:
306
474
  cloudburn scan ./template.yaml
307
475
  cloudburn scan ./iac
308
476
  `
309
- ).action(async (path, options) => {
477
+ ).action(async (path, options, command) => {
310
478
  try {
311
479
  const scanner = new CloudBurnClient2();
312
480
  const result = await scanner.scanStatic(path ?? process.cwd());
313
- const format = formatters[options.format ?? "table"];
314
- const output = format(result);
481
+ const format = resolveOutputFormat(command, options.format);
482
+ const output = renderResponse({ kind: "scan-result", result }, format);
315
483
  process.stdout.write(`${output}
316
484
  `);
317
485
  if (options.exitCode && countScanResultFindings(result) > 0) {
@@ -330,7 +498,7 @@ Examples:
330
498
  // src/cli.ts
331
499
  var createProgram = () => {
332
500
  const program = new Command();
333
- program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.5.1");
501
+ program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.6.0").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
334
502
  registerDiscoverCommand(program);
335
503
  registerScanCommand(program);
336
504
  registerInitCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.5.1",
3
+ "version": "0.6.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.8.1"
14
+ "@cloudburn/sdk": "0.8.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@biomejs/biome": "^2.4.6",