cloudburn 0.7.0 → 0.8.2
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 +2 -2
- package/dist/cli.js +265 -36
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/cli.js
CHANGED
|
@@ -459,7 +459,7 @@ var registerCompletionCommand = (program) => {
|
|
|
459
459
|
|
|
460
460
|
// src/commands/discover.ts
|
|
461
461
|
import { assertValidAwsRegion, CloudBurnClient } from "@cloudburn/sdk";
|
|
462
|
-
import { InvalidArgumentError as
|
|
462
|
+
import { InvalidArgumentError as InvalidArgumentError3 } from "commander";
|
|
463
463
|
|
|
464
464
|
// src/exit-codes.ts
|
|
465
465
|
var EXIT_CODE_OK = 0;
|
|
@@ -528,6 +528,9 @@ var flattenScanResult = (result) => result.providers.flatMap(
|
|
|
528
528
|
var countScanResultFindings = (result) => flattenScanResult(result).length;
|
|
529
529
|
|
|
530
530
|
// src/formatters/output.ts
|
|
531
|
+
var DEFAULT_TABLE_WIDTH = 200;
|
|
532
|
+
var MIN_COLUMN_WIDTH = 4;
|
|
533
|
+
var PREFERRED_MIN_COLUMN_WIDTH = 8;
|
|
531
534
|
var supportedOutputFormats = ["text", "json", "table"];
|
|
532
535
|
var scanColumns = [
|
|
533
536
|
{ key: "provider", header: "Provider" },
|
|
@@ -538,8 +541,8 @@ var scanColumns = [
|
|
|
538
541
|
{ key: "accountId", header: "AccountId" },
|
|
539
542
|
{ key: "region", header: "Region" },
|
|
540
543
|
{ key: "path", header: "Path" },
|
|
541
|
-
{ key: "
|
|
542
|
-
{ key: "
|
|
544
|
+
{ key: "line", header: "Line" },
|
|
545
|
+
{ key: "column", header: "Column" },
|
|
543
546
|
{ key: "message", header: "Message" }
|
|
544
547
|
];
|
|
545
548
|
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.";
|
|
@@ -645,8 +648,8 @@ var projectScanRows = (result) => flattenScanResult(result).map(({ finding, mess
|
|
|
645
648
|
ruleId,
|
|
646
649
|
service,
|
|
647
650
|
source,
|
|
648
|
-
|
|
649
|
-
|
|
651
|
+
column: finding.location?.column ?? "",
|
|
652
|
+
line: finding.location?.line ?? ""
|
|
650
653
|
}));
|
|
651
654
|
var renderTextRows = (rows, columns, emptyMessage) => {
|
|
652
655
|
if (rows.length === 0) {
|
|
@@ -694,17 +697,128 @@ var toTextCell = (value) => {
|
|
|
694
697
|
}
|
|
695
698
|
return JSON.stringify(value);
|
|
696
699
|
};
|
|
697
|
-
var toTableCell = (value) =>
|
|
698
|
-
|
|
699
|
-
|
|
700
|
+
var toTableCell = (value) => {
|
|
701
|
+
if (Array.isArray(value)) {
|
|
702
|
+
return value.map((item) => toTextCell(item)).join(", ").replace(/\r?\n/g, "\\n");
|
|
703
|
+
}
|
|
704
|
+
return toTextCell(value).replace(/\r?\n/g, "\\n");
|
|
705
|
+
};
|
|
706
|
+
var resolveTableColumns = (rows, columns) => {
|
|
707
|
+
const visibleColumns = columns.filter((column) => rows.some((row) => toTableCell(row[column.key]).length > 0));
|
|
708
|
+
return visibleColumns.length === 0 ? columns : visibleColumns;
|
|
709
|
+
};
|
|
710
|
+
var getTargetTableWidth = () => {
|
|
711
|
+
const terminalWidth = process.stdout.columns;
|
|
712
|
+
return typeof terminalWidth === "number" && Number.isFinite(terminalWidth) && terminalWidth > 0 ? terminalWidth : DEFAULT_TABLE_WIDTH;
|
|
713
|
+
};
|
|
714
|
+
var measureTableWidth = (widths) => widths.reduce((total, width) => total + width, 0) + widths.length * 3 + 1;
|
|
715
|
+
var fitColumnWidths = (columns, rows) => {
|
|
716
|
+
const maxWidths = columns.map(
|
|
700
717
|
(column) => Math.max(column.header.length, ...rows.map((row) => toTableCell(row[column.key]).length))
|
|
701
718
|
);
|
|
719
|
+
const minWidths = columns.map(
|
|
720
|
+
(column, index) => Math.min(
|
|
721
|
+
maxWidths[index] ?? MIN_COLUMN_WIDTH,
|
|
722
|
+
Math.max(MIN_COLUMN_WIDTH, Math.min(column.header.length, PREFERRED_MIN_COLUMN_WIDTH))
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
const widths = [...maxWidths];
|
|
726
|
+
const targetWidth = getTargetTableWidth();
|
|
727
|
+
while (measureTableWidth(widths) > targetWidth) {
|
|
728
|
+
let widestColumnIndex = -1;
|
|
729
|
+
for (let index = 0; index < widths.length; index += 1) {
|
|
730
|
+
if ((widths[index] ?? 0) <= (minWidths[index] ?? MIN_COLUMN_WIDTH)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (widestColumnIndex === -1 || (widths[index] ?? 0) > (widths[widestColumnIndex] ?? 0)) {
|
|
734
|
+
widestColumnIndex = index;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (widestColumnIndex === -1) {
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
widths[widestColumnIndex] = Math.max(MIN_COLUMN_WIDTH, (widths[widestColumnIndex] ?? MIN_COLUMN_WIDTH) - 1);
|
|
741
|
+
}
|
|
742
|
+
return widths;
|
|
743
|
+
};
|
|
744
|
+
var wrapToken = (token, width) => {
|
|
745
|
+
if (token.length <= width) {
|
|
746
|
+
return [token];
|
|
747
|
+
}
|
|
748
|
+
const segments = [];
|
|
749
|
+
for (let start = 0; start < token.length; start += width) {
|
|
750
|
+
segments.push(token.slice(start, start + width));
|
|
751
|
+
}
|
|
752
|
+
return segments;
|
|
753
|
+
};
|
|
754
|
+
var wrapCell = (value, width) => {
|
|
755
|
+
if (value.length <= width) {
|
|
756
|
+
return [value];
|
|
757
|
+
}
|
|
758
|
+
const words = value.split(/\s+/).filter((word) => word.length > 0);
|
|
759
|
+
if (words.length === 0) {
|
|
760
|
+
return [""];
|
|
761
|
+
}
|
|
762
|
+
const lines = [];
|
|
763
|
+
let currentLine = "";
|
|
764
|
+
for (const word of words) {
|
|
765
|
+
if (word.length > width) {
|
|
766
|
+
if (currentLine.length > 0) {
|
|
767
|
+
lines.push(currentLine);
|
|
768
|
+
currentLine = "";
|
|
769
|
+
}
|
|
770
|
+
lines.push(...wrapToken(word, width));
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
if (currentLine.length === 0) {
|
|
774
|
+
currentLine = word;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (currentLine.length + 1 + word.length <= width) {
|
|
778
|
+
currentLine = `${currentLine} ${word}`;
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
lines.push(currentLine);
|
|
782
|
+
currentLine = word;
|
|
783
|
+
}
|
|
784
|
+
if (currentLine.length > 0) {
|
|
785
|
+
lines.push(currentLine);
|
|
786
|
+
}
|
|
787
|
+
return lines;
|
|
788
|
+
};
|
|
789
|
+
var renderTableLines = (cells, widths) => {
|
|
790
|
+
const wrappedCells = cells.map((cell, index) => wrapCell(cell, widths[index] ?? MIN_COLUMN_WIDTH));
|
|
791
|
+
const height = Math.max(...wrappedCells.map((lines) => lines.length));
|
|
792
|
+
return Array.from({ length: height }, (_, lineIndex) => {
|
|
793
|
+
const line = wrappedCells.map((lines, index) => (lines[lineIndex] ?? "").padEnd(widths[index] ?? MIN_COLUMN_WIDTH)).join(" | ");
|
|
794
|
+
return `| ${line} |`;
|
|
795
|
+
});
|
|
796
|
+
};
|
|
797
|
+
var renderAsciiTable = (rows, columns) => {
|
|
798
|
+
const visibleColumns = resolveTableColumns(rows, columns);
|
|
799
|
+
const widths = fitColumnWidths(visibleColumns, rows);
|
|
702
800
|
const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
703
|
-
const header =
|
|
704
|
-
|
|
705
|
-
|
|
801
|
+
const header = renderTableLines(
|
|
802
|
+
visibleColumns.map((column) => column.header),
|
|
803
|
+
widths
|
|
706
804
|
);
|
|
707
|
-
|
|
805
|
+
const body = rows.flatMap(
|
|
806
|
+
(row) => renderTableLines(
|
|
807
|
+
visibleColumns.map((column) => toTableCell(row[column.key])),
|
|
808
|
+
widths
|
|
809
|
+
)
|
|
810
|
+
);
|
|
811
|
+
return [border, ...header, border, ...body, border].join("\n");
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// src/commands/config-options.ts
|
|
815
|
+
import { InvalidArgumentError as InvalidArgumentError2 } from "commander";
|
|
816
|
+
var parseRuleIdList = (value) => {
|
|
817
|
+
const ruleIds = value.split(",").map((ruleId) => ruleId.trim()).filter((ruleId) => ruleId.length > 0);
|
|
818
|
+
if (ruleIds.length === 0) {
|
|
819
|
+
throw new InvalidArgumentError2("Provide at least one rule ID.");
|
|
820
|
+
}
|
|
821
|
+
return ruleIds;
|
|
708
822
|
};
|
|
709
823
|
|
|
710
824
|
// src/commands/discover.ts
|
|
@@ -712,7 +826,7 @@ var parseAwsRegion = (value) => {
|
|
|
712
826
|
try {
|
|
713
827
|
return assertValidAwsRegion(value);
|
|
714
828
|
} catch (err) {
|
|
715
|
-
throw new
|
|
829
|
+
throw new InvalidArgumentError3(err instanceof Error ? err.message : "Invalid AWS region.");
|
|
716
830
|
}
|
|
717
831
|
};
|
|
718
832
|
var parseDiscoverRegion = (value) => {
|
|
@@ -722,6 +836,17 @@ var parseDiscoverRegion = (value) => {
|
|
|
722
836
|
return parseAwsRegion(value);
|
|
723
837
|
};
|
|
724
838
|
var resolveDiscoveryTarget = (region) => region === void 0 ? { mode: "current" } : region === "all" ? { mode: "all" } : { mode: "region", region };
|
|
839
|
+
var toDiscoveryConfigOverride = (options) => {
|
|
840
|
+
if (options.enabledRules === void 0 && options.disabledRules === void 0) {
|
|
841
|
+
return void 0;
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
discovery: {
|
|
845
|
+
disabledRules: options.disabledRules,
|
|
846
|
+
enabledRules: options.enabledRules
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
};
|
|
725
850
|
var runCommand = async (action) => {
|
|
726
851
|
try {
|
|
727
852
|
process.exitCode = await action() ?? EXIT_CODE_OK;
|
|
@@ -737,11 +862,22 @@ var registerDiscoverCommand = (program) => {
|
|
|
737
862
|
"--region <region>",
|
|
738
863
|
'Discovery region to use. Pass "all" to require an aggregator index.',
|
|
739
864
|
parseDiscoverRegion
|
|
740
|
-
).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
|
|
865
|
+
).option("--config <path>", "Explicit CloudBurn config file to load").option("--enabled-rules <ruleIds>", "Comma-separated rule IDs to enable", parseRuleIdList).option("--disabled-rules <ruleIds>", "Comma-separated rule IDs to disable", parseRuleIdList).option("--exit-code", "Exit with code 1 when findings exist").action(async (options, command) => {
|
|
741
866
|
await runCommand(async () => {
|
|
742
867
|
const scanner = new CloudBurnClient();
|
|
743
|
-
const
|
|
744
|
-
const
|
|
868
|
+
const loadedConfig = await scanner.loadConfig(options.config);
|
|
869
|
+
const discoveryOptions = {
|
|
870
|
+
target: resolveDiscoveryTarget(options.region)
|
|
871
|
+
};
|
|
872
|
+
const configOverride = toDiscoveryConfigOverride(options);
|
|
873
|
+
if (configOverride !== void 0) {
|
|
874
|
+
discoveryOptions.config = configOverride;
|
|
875
|
+
}
|
|
876
|
+
if (options.config !== void 0) {
|
|
877
|
+
discoveryOptions.configPath = options.config;
|
|
878
|
+
}
|
|
879
|
+
const result = await scanner.discover(discoveryOptions);
|
|
880
|
+
const format = resolveOutputFormat(command, void 0, loadedConfig.discovery.format ?? "table");
|
|
745
881
|
const output = renderResponse({ kind: "scan-result", result }, format);
|
|
746
882
|
process.stdout.write(`${output}
|
|
747
883
|
`);
|
|
@@ -874,26 +1010,105 @@ var registerEstimateCommand = (program) => {
|
|
|
874
1010
|
};
|
|
875
1011
|
|
|
876
1012
|
// src/commands/init.ts
|
|
877
|
-
|
|
878
|
-
|
|
1013
|
+
import { access, writeFile } from "fs/promises";
|
|
1014
|
+
import { dirname, join, resolve } from "path";
|
|
1015
|
+
var CONFIG_FILENAMES = [".cloudburn.yml", ".cloudburn.yaml"];
|
|
1016
|
+
var starterConfig = `# Static IaC scan configuration.
|
|
1017
|
+
# enabled-rules restricts scans to only the listed rule IDs.
|
|
1018
|
+
# disabled-rules removes specific rule IDs from the active set.
|
|
1019
|
+
# format sets the default output format when --format is not passed.
|
|
1020
|
+
iac:
|
|
1021
|
+
enabled-rules:
|
|
1022
|
+
- CLDBRN-AWS-EBS-1
|
|
1023
|
+
disabled-rules:
|
|
1024
|
+
- CLDBRN-AWS-EC2-2
|
|
1025
|
+
format: table
|
|
879
1026
|
|
|
880
|
-
#
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1027
|
+
# Live AWS discovery configuration.
|
|
1028
|
+
# Use the same rule controls here to tune discover runs separately from IaC scans.
|
|
1029
|
+
discovery:
|
|
1030
|
+
enabled-rules:
|
|
1031
|
+
- CLDBRN-AWS-EBS-1
|
|
1032
|
+
disabled-rules:
|
|
1033
|
+
- CLDBRN-AWS-S3-1
|
|
1034
|
+
format: json
|
|
884
1035
|
`;
|
|
1036
|
+
var renderStarterConfig = (command) => renderResponse(
|
|
1037
|
+
{
|
|
1038
|
+
kind: "document",
|
|
1039
|
+
content: starterConfig,
|
|
1040
|
+
contentType: "application/yaml"
|
|
1041
|
+
},
|
|
1042
|
+
resolveOutputFormat(command, void 0, "text")
|
|
1043
|
+
);
|
|
1044
|
+
var fileExists = async (path) => {
|
|
1045
|
+
try {
|
|
1046
|
+
await access(path);
|
|
1047
|
+
return true;
|
|
1048
|
+
} catch {
|
|
1049
|
+
return false;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
var findProjectRoot = async (startDirectory) => {
|
|
1053
|
+
let currentDirectory = resolve(startDirectory);
|
|
1054
|
+
while (true) {
|
|
1055
|
+
if (await fileExists(join(currentDirectory, ".git"))) {
|
|
1056
|
+
return currentDirectory;
|
|
1057
|
+
}
|
|
1058
|
+
const parentDirectory = dirname(currentDirectory);
|
|
1059
|
+
if (parentDirectory === currentDirectory) {
|
|
1060
|
+
return resolve(startDirectory);
|
|
1061
|
+
}
|
|
1062
|
+
currentDirectory = parentDirectory;
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
885
1065
|
var registerInitCommand = (program) => {
|
|
886
|
-
program.command("init").description("
|
|
887
|
-
|
|
888
|
-
{
|
|
889
|
-
kind: "document",
|
|
890
|
-
content: starterConfig,
|
|
891
|
-
contentType: "application/yaml"
|
|
892
|
-
},
|
|
893
|
-
resolveOutputFormat(this, void 0, "text")
|
|
894
|
-
);
|
|
895
|
-
process.stdout.write(`${output}
|
|
1066
|
+
const initCommand = program.command("init").description("Initialize CloudBurn scaffolding").usage("[command]").action(function() {
|
|
1067
|
+
process.stdout.write(`${renderStarterConfig(this)}
|
|
896
1068
|
`);
|
|
1069
|
+
process.exitCode = EXIT_CODE_OK;
|
|
1070
|
+
});
|
|
1071
|
+
initCommand.command("config").description("Create a starter .cloudburn.yml configuration").option("--print", "Print the starter config instead of writing the file").action(async function(options) {
|
|
1072
|
+
try {
|
|
1073
|
+
if (options.print) {
|
|
1074
|
+
process.stdout.write(`${renderStarterConfig(this)}
|
|
1075
|
+
`);
|
|
1076
|
+
process.exitCode = EXIT_CODE_OK;
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const rootDirectory = await findProjectRoot(process.cwd());
|
|
1080
|
+
const existingConfigPath = (await Promise.all(
|
|
1081
|
+
CONFIG_FILENAMES.map(async (filename) => {
|
|
1082
|
+
const path = join(rootDirectory, filename);
|
|
1083
|
+
return await fileExists(path) ? path : void 0;
|
|
1084
|
+
})
|
|
1085
|
+
)).find((path) => path !== void 0);
|
|
1086
|
+
if (existingConfigPath) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`CloudBurn config already exists at ${existingConfigPath}. Use --print to inspect the template.`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
const configPath = join(rootDirectory, ".cloudburn.yml");
|
|
1092
|
+
await writeFile(configPath, starterConfig, { encoding: "utf8", flag: "wx" });
|
|
1093
|
+
const output = renderResponse(
|
|
1094
|
+
{
|
|
1095
|
+
kind: "status",
|
|
1096
|
+
data: {
|
|
1097
|
+
message: "Created CloudBurn config.",
|
|
1098
|
+
path: configPath
|
|
1099
|
+
},
|
|
1100
|
+
text: `Created ${configPath}.`
|
|
1101
|
+
},
|
|
1102
|
+
resolveOutputFormat(this)
|
|
1103
|
+
);
|
|
1104
|
+
process.stdout.write(`${output}
|
|
1105
|
+
`);
|
|
1106
|
+
process.exitCode = EXIT_CODE_OK;
|
|
1107
|
+
} catch (err) {
|
|
1108
|
+
process.stderr.write(`${formatError(err)}
|
|
1109
|
+
`);
|
|
1110
|
+
process.exitCode = EXIT_CODE_RUNTIME_ERROR;
|
|
1111
|
+
}
|
|
897
1112
|
});
|
|
898
1113
|
};
|
|
899
1114
|
|
|
@@ -917,13 +1132,27 @@ var registerRulesListCommand = (program) => {
|
|
|
917
1132
|
|
|
918
1133
|
// src/commands/scan.ts
|
|
919
1134
|
import { CloudBurnClient as CloudBurnClient2 } from "@cloudburn/sdk";
|
|
1135
|
+
var toScanConfigOverride = (options) => {
|
|
1136
|
+
if (options.enabledRules === void 0 && options.disabledRules === void 0) {
|
|
1137
|
+
return void 0;
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
iac: {
|
|
1141
|
+
disabledRules: options.disabledRules,
|
|
1142
|
+
enabledRules: options.enabledRules
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
};
|
|
920
1146
|
var registerScanCommand = (program) => {
|
|
921
1147
|
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) => {
|
|
1148
|
+
program.command("scan").description("Run an autodetected static IaC scan").argument("[path]", "Terraform file, CloudFormation template, or directory to scan").option("--config <path>", "Explicit CloudBurn config file to load").option("--enabled-rules <ruleIds>", "Comma-separated rule IDs to enable", parseRuleIdList).option("--disabled-rules <ruleIds>", "Comma-separated rule IDs to disable", parseRuleIdList).option("--exit-code", "Exit with code 1 when findings exist").action(async (path, options, command) => {
|
|
923
1149
|
try {
|
|
924
1150
|
const scanner = new CloudBurnClient2();
|
|
925
|
-
const
|
|
926
|
-
const
|
|
1151
|
+
const loadedConfig = await scanner.loadConfig(options.config);
|
|
1152
|
+
const configOverride = toScanConfigOverride(options);
|
|
1153
|
+
const scanPath = path ?? process.cwd();
|
|
1154
|
+
const result = configOverride === void 0 && options.config === void 0 ? await scanner.scanStatic(scanPath) : options.config === void 0 ? await scanner.scanStatic(scanPath, configOverride) : await scanner.scanStatic(scanPath, configOverride, { configPath: options.config });
|
|
1155
|
+
const format = resolveOutputFormat(command, void 0, loadedConfig.iac.format ?? "table");
|
|
927
1156
|
const output = renderResponse({ kind: "scan-result", result }, format);
|
|
928
1157
|
process.stdout.write(`${output}
|
|
929
1158
|
`);
|
|
@@ -945,7 +1174,7 @@ var registerScanCommand = (program) => {
|
|
|
945
1174
|
// src/cli.ts
|
|
946
1175
|
var createProgram = () => {
|
|
947
1176
|
const program = createCliCommand();
|
|
948
|
-
program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.
|
|
1177
|
+
program.name("cloudburn").usage("[command]").description("Know what you spend. Fix what you waste.").version("0.8.2").option("--format <format>", OUTPUT_FORMAT_OPTION_DESCRIPTION, parseOutputFormat);
|
|
949
1178
|
configureCliHelp(program);
|
|
950
1179
|
registerCompletionCommand(program);
|
|
951
1180
|
registerDiscoverCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudburn",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.2",
|
|
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.13.1"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@biomejs/biome": "^2.4.6",
|