cloudburn 0.5.0 → 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 +287 -123
  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
  };
@@ -260,49 +425,48 @@ var registerEstimateCommand = (program) => {
260
425
  var starterConfig = `version: 1
261
426
  profile: dev
262
427
 
263
- profiles:
264
- dev:
265
- ec2-allowed-instance-types:
266
- allow: [t3.micro, t3.small, t3.medium]
267
-
428
+ # Profiles are parsed but not applied yet, so configure the active rules block directly for now.
268
429
  rules:
269
- ec2-allowed-instance-types:
430
+ ec2-instance-type-preferred:
270
431
  severity: error
271
432
  `;
272
433
  var registerInitCommand = (program) => {
273
- program.command("init").description("Print a starter .cloudburn.yml configuration").action(() => {
274
- 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}
275
444
  `);
276
445
  });
277
446
  };
278
447
 
279
448
  // src/commands/rules-list.ts
280
- import { awsCorePreset } from "@cloudburn/sdk";
449
+ import { builtInRuleMetadata } from "@cloudburn/sdk";
281
450
  var registerRulesListCommand = (program) => {
282
- const rulesCommand = program.command("rules").description("Inspect built-in and custom rules");
283
- rulesCommand.command("list").description("List built-in CloudBurn rule IDs").action(() => {
284
- 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}
285
462
  `);
286
463
  });
287
464
  };
288
465
 
289
466
  // src/commands/scan.ts
290
467
  import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
291
- import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
292
- var supportedScanFormats = ["table", "json", "sarif"];
293
- var parseScanFormat = (value) => {
294
- if (supportedScanFormats.includes(value)) {
295
- return value;
296
- }
297
- throw new InvalidArgumentError2(`Invalid format "${value}". Allowed formats: ${supportedScanFormats.join(", ")}.`);
298
- };
299
- var formatters = {
300
- json: formatJson,
301
- sarif: formatSarif,
302
- table: formatTable
303
- };
304
468
  var registerScanCommand = (program) => {
305
- 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(
306
470
  "after",
307
471
  `
308
472
  Examples:
@@ -310,12 +474,12 @@ Examples:
310
474
  cloudburn scan ./template.yaml
311
475
  cloudburn scan ./iac
312
476
  `
313
- ).action(async (path, options) => {
477
+ ).action(async (path, options, command) => {
314
478
  try {
315
479
  const scanner = new CloudBurnClient2();
316
480
  const result = await scanner.scanStatic(path ?? process.cwd());
317
- const format = formatters[options.format ?? "table"];
318
- const output = format(result);
481
+ const format = resolveOutputFormat(command, options.format);
482
+ const output = renderResponse({ kind: "scan-result", result }, format);
319
483
  process.stdout.write(`${output}
320
484
  `);
321
485
  if (options.exitCode && countScanResultFindings(result) > 0) {
@@ -334,7 +498,7 @@ Examples:
334
498
  // src/cli.ts
335
499
  var createProgram = () => {
336
500
  const program = new Command();
337
- program.name("cloudburn").description("Know what you spend. Fix what you waste.").version("0.5.0");
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);
338
502
  registerDiscoverCommand(program);
339
503
  registerScanCommand(program);
340
504
  registerInitCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudburn",
3
- "version": "0.5.0",
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.0"
14
+ "@cloudburn/sdk": "0.8.2"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@biomejs/biome": "^2.4.6",