@ganakailabs/cloudeval-cli 0.30.4 → 0.31.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 +24 -0
- package/dist/{App-AN3ELGIY.js → App-OHSI5XYX.js} +2 -2
- package/dist/{Banner-XD5GCTUQ.js → Banner-NKBFXQJ2.js} +2 -2
- package/dist/{chunk-NOR7UT66.js → chunk-PNA74AA5.js} +1 -1
- package/dist/{chunk-RVZOUNMP.js → chunk-YZ4GRXIK.js} +1 -1
- package/dist/cli.js +185 -7
- package/package.json +1 -1
- package/sbom.spdx.json +1 -1
package/README.md
CHANGED
|
@@ -119,6 +119,30 @@ jobs:
|
|
|
119
119
|
|
|
120
120
|
Public example: [passing baseline PR #6](https://github.com/ganakailabs/cloudeval-azure-arm-review-example/pull/6) in [`ganakailabs/cloudeval-azure-arm-review-example`](https://github.com/ganakailabs/cloudeval-azure-arm-review-example). Review comments show a merge-gate table, CloudEval report badges, a visible AI summary, a folded detailed AI reviewer note, a compact Well-Architected radar/table drilldown, and cost Mermaid charts grouped for quick scanning.
|
|
121
121
|
|
|
122
|
+
To include a PDF in each review artifact bundle, opt in from `.cloudeval/config.yaml`:
|
|
123
|
+
|
|
124
|
+
```yaml
|
|
125
|
+
ci:
|
|
126
|
+
review:
|
|
127
|
+
outputs:
|
|
128
|
+
pdf:
|
|
129
|
+
enabled: true
|
|
130
|
+
report_type: all
|
|
131
|
+
verbosity: evidence
|
|
132
|
+
fail_on_error: false
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When `cloudeval review --output <dir>` runs, the CLI writes `<dir>/review.pdf` alongside `review.json` and `review.md`. In GitHub Actions, `ganakailabs/cloudeval-action` uploads that file when `upload_artifacts: true`; PR comments keep both the CloudEval-hosted `PDF` badge and the GitHub `Artifacts` badge.
|
|
136
|
+
|
|
137
|
+
Supported PDF output keys:
|
|
138
|
+
|
|
139
|
+
| Key | Supported values | Default |
|
|
140
|
+
| --- | --- | --- |
|
|
141
|
+
| `enabled` | `true`, `false` | `false` |
|
|
142
|
+
| `report_type` | `all`, `architecture`, `cost`, `unit_tests` | `all` |
|
|
143
|
+
| `verbosity` | `brief`, `detailed`, `evidence` | `evidence` |
|
|
144
|
+
| `fail_on_error` | `true`, `false` | `false` |
|
|
145
|
+
|
|
122
146
|
### MCP For Codex, Cursor, Claude, VS Code
|
|
123
147
|
|
|
124
148
|
Start with read-only agent integration:
|
|
@@ -38,10 +38,10 @@ import {
|
|
|
38
38
|
} from "./chunk-NXM4JEOB.js";
|
|
39
39
|
import {
|
|
40
40
|
Banner
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-PNA74AA5.js";
|
|
42
42
|
import {
|
|
43
43
|
CLI_VERSION
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-YZ4GRXIK.js";
|
|
45
45
|
import {
|
|
46
46
|
raisedButtonStyle,
|
|
47
47
|
terminalTheme
|
package/dist/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
} from "./chunk-NXM4JEOB.js";
|
|
40
40
|
import {
|
|
41
41
|
CLI_VERSION
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-YZ4GRXIK.js";
|
|
43
43
|
|
|
44
44
|
// src/runtime/prepareInk.ts
|
|
45
45
|
import fs from "fs";
|
|
@@ -2717,6 +2717,94 @@ var readConfigText = async (cwd, options) => {
|
|
|
2717
2717
|
return void 0;
|
|
2718
2718
|
}
|
|
2719
2719
|
};
|
|
2720
|
+
var findYamlBlock = (configText, pathKeys) => {
|
|
2721
|
+
if (!configText) return void 0;
|
|
2722
|
+
const lines = configText.split(/\r?\n/).map((raw) => ({
|
|
2723
|
+
raw,
|
|
2724
|
+
indent: raw.match(/^\s*/)?.[0].length ?? 0,
|
|
2725
|
+
trimmed: raw.trim()
|
|
2726
|
+
}));
|
|
2727
|
+
let searchStart = 0;
|
|
2728
|
+
let searchEnd = lines.length;
|
|
2729
|
+
let parentIndent = -1;
|
|
2730
|
+
for (const key of pathKeys) {
|
|
2731
|
+
let found = -1;
|
|
2732
|
+
for (let index = searchStart; index < searchEnd; index += 1) {
|
|
2733
|
+
const line = lines[index];
|
|
2734
|
+
if (line.trimmed === `${key}:` && line.indent > parentIndent) {
|
|
2735
|
+
found = index;
|
|
2736
|
+
break;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
if (found === -1) {
|
|
2740
|
+
return void 0;
|
|
2741
|
+
}
|
|
2742
|
+
const blockIndent = lines[found].indent;
|
|
2743
|
+
let blockEnd = searchEnd;
|
|
2744
|
+
for (let index = found + 1; index < searchEnd; index += 1) {
|
|
2745
|
+
const line = lines[index];
|
|
2746
|
+
if (line.trimmed && line.indent <= blockIndent) {
|
|
2747
|
+
blockEnd = index;
|
|
2748
|
+
break;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
searchStart = found + 1;
|
|
2752
|
+
searchEnd = blockEnd;
|
|
2753
|
+
parentIndent = blockIndent;
|
|
2754
|
+
}
|
|
2755
|
+
return lines.slice(searchStart, searchEnd).map((line) => line.raw).join("\n");
|
|
2756
|
+
};
|
|
2757
|
+
var yamlScalarValue = (block, ...keys) => {
|
|
2758
|
+
if (!block) return void 0;
|
|
2759
|
+
for (const key of keys) {
|
|
2760
|
+
const match = block.match(
|
|
2761
|
+
new RegExp(`^\\s*${key}\\s*:\\s*["']?([^"'\\n#]+)["']?\\s*(?:#.*)?$`, "m")
|
|
2762
|
+
);
|
|
2763
|
+
if (match?.[1]?.trim()) {
|
|
2764
|
+
return match[1].trim();
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
return void 0;
|
|
2768
|
+
};
|
|
2769
|
+
var yamlBooleanValue = (block, ...keys) => {
|
|
2770
|
+
const value = yamlScalarValue(block, ...keys);
|
|
2771
|
+
if (value === void 0) return void 0;
|
|
2772
|
+
if (/^(true|yes|on|1)$/i.test(value)) return true;
|
|
2773
|
+
if (/^(false|no|off|0)$/i.test(value)) return false;
|
|
2774
|
+
return void 0;
|
|
2775
|
+
};
|
|
2776
|
+
var normalizeReviewPdfReportType = (value) => {
|
|
2777
|
+
const normalized = String(value ?? "all").trim().toLowerCase().replace(/-/g, "_");
|
|
2778
|
+
if (normalized === "cost") return "cost";
|
|
2779
|
+
if (normalized === "waf" || normalized === "architecture") return "architecture";
|
|
2780
|
+
if (normalized === "unit_tests" || normalized === "validation") return "unit_tests";
|
|
2781
|
+
return "all";
|
|
2782
|
+
};
|
|
2783
|
+
var normalizeReviewPdfVerbosity = (value) => {
|
|
2784
|
+
const normalized = String(value ?? "evidence").trim().toLowerCase();
|
|
2785
|
+
if (normalized === "brief" || normalized === "short") return "brief";
|
|
2786
|
+
if (normalized === "detailed") return "detailed";
|
|
2787
|
+
if (normalized === "evidence" || normalized === "full" || normalized === "extended") {
|
|
2788
|
+
return "evidence";
|
|
2789
|
+
}
|
|
2790
|
+
return "evidence";
|
|
2791
|
+
};
|
|
2792
|
+
var parseReviewPdfOutputConfig = (configText) => {
|
|
2793
|
+
const block = findYamlBlock(configText, ["ci", "review", "outputs", "pdf"]);
|
|
2794
|
+
if (!block) {
|
|
2795
|
+
return void 0;
|
|
2796
|
+
}
|
|
2797
|
+
return {
|
|
2798
|
+
enabled: yamlBooleanValue(block, "enabled") ?? false,
|
|
2799
|
+
reportType: normalizeReviewPdfReportType(
|
|
2800
|
+
yamlScalarValue(block, "report_type", "reportType", "type")
|
|
2801
|
+
),
|
|
2802
|
+
verbosity: normalizeReviewPdfVerbosity(
|
|
2803
|
+
yamlScalarValue(block, "verbosity", "pdf_verbosity", "pdfVerbosity")
|
|
2804
|
+
),
|
|
2805
|
+
failOnError: yamlBooleanValue(block, "fail_on_error", "failOnError") ?? false
|
|
2806
|
+
};
|
|
2807
|
+
};
|
|
2720
2808
|
var parseGateConfig = (configText) => {
|
|
2721
2809
|
if (!configText || !/^\s*ci\s*:/m.test(configText) || !/^\s*gates\s*:/m.test(configText)) {
|
|
2722
2810
|
return void 0;
|
|
@@ -3777,6 +3865,68 @@ var safeFetch = async (input) => {
|
|
|
3777
3865
|
return void 0;
|
|
3778
3866
|
}
|
|
3779
3867
|
};
|
|
3868
|
+
var writeReviewPdfOutput = async ({
|
|
3869
|
+
config,
|
|
3870
|
+
outputDir,
|
|
3871
|
+
baseUrl,
|
|
3872
|
+
token,
|
|
3873
|
+
projectId,
|
|
3874
|
+
userId
|
|
3875
|
+
}) => {
|
|
3876
|
+
if (!config) {
|
|
3877
|
+
return void 0;
|
|
3878
|
+
}
|
|
3879
|
+
const base2 = {
|
|
3880
|
+
enabled: config.enabled,
|
|
3881
|
+
reportType: config.reportType,
|
|
3882
|
+
verbosity: config.verbosity,
|
|
3883
|
+
failOnError: config.failOnError
|
|
3884
|
+
};
|
|
3885
|
+
if (!config.enabled) {
|
|
3886
|
+
return { ...base2, status: "skipped" };
|
|
3887
|
+
}
|
|
3888
|
+
if (!outputDir) {
|
|
3889
|
+
return {
|
|
3890
|
+
...base2,
|
|
3891
|
+
status: "skipped",
|
|
3892
|
+
reason: "PDF output requires --output so the file can be attached as an artifact."
|
|
3893
|
+
};
|
|
3894
|
+
}
|
|
3895
|
+
try {
|
|
3896
|
+
const core = await import("./dist-6LEMVXIY.js");
|
|
3897
|
+
const pdf = await core.downloadReportPdf({
|
|
3898
|
+
baseUrl,
|
|
3899
|
+
authToken: token,
|
|
3900
|
+
projectId,
|
|
3901
|
+
userId,
|
|
3902
|
+
verbosity: config.verbosity,
|
|
3903
|
+
reportType: config.reportType,
|
|
3904
|
+
includeVisuals: true
|
|
3905
|
+
});
|
|
3906
|
+
if (!pdf.bytes.length) {
|
|
3907
|
+
throw new Error("Backend returned an empty PDF.");
|
|
3908
|
+
}
|
|
3909
|
+
await fs2.mkdir(outputDir, { recursive: true });
|
|
3910
|
+
const file = path2.join(outputDir, "review.pdf");
|
|
3911
|
+
await fs2.writeFile(file, pdf.bytes);
|
|
3912
|
+
return {
|
|
3913
|
+
...base2,
|
|
3914
|
+
status: "written",
|
|
3915
|
+
file,
|
|
3916
|
+
bytes: pdf.bytes.length,
|
|
3917
|
+
contentType: pdf.contentType,
|
|
3918
|
+
backendFilename: pdf.filename,
|
|
3919
|
+
reportStatus: pdf.status,
|
|
3920
|
+
warningsCount: pdf.warningsCount ?? 0
|
|
3921
|
+
};
|
|
3922
|
+
} catch (error) {
|
|
3923
|
+
return {
|
|
3924
|
+
...base2,
|
|
3925
|
+
status: "failed",
|
|
3926
|
+
error: error?.message ?? "PDF download failed."
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
};
|
|
3780
3930
|
var parsePositiveInteger = (value, flagName, fallback) => {
|
|
3781
3931
|
if (value === void 0 || value === "") {
|
|
3782
3932
|
return fallback;
|
|
@@ -4468,10 +4618,27 @@ var registerReviewCommand = (program2, deps) => {
|
|
|
4468
4618
|
} else {
|
|
4469
4619
|
data.aiSummary = { enabled: false };
|
|
4470
4620
|
}
|
|
4471
|
-
const summaryMarkdown = buildMarkdownSummary(data);
|
|
4472
4621
|
const filesWritten = [];
|
|
4473
|
-
|
|
4474
|
-
|
|
4622
|
+
const outputDir = options.output ? path2.resolve(options.output) : void 0;
|
|
4623
|
+
const pdfOutput = await writeReviewPdfOutput({
|
|
4624
|
+
config: parseReviewPdfOutputConfig(configText),
|
|
4625
|
+
outputDir,
|
|
4626
|
+
baseUrl: context.baseUrl,
|
|
4627
|
+
token: context.token,
|
|
4628
|
+
projectId,
|
|
4629
|
+
userId: scopedUserId
|
|
4630
|
+
});
|
|
4631
|
+
if (pdfOutput) {
|
|
4632
|
+
data.outputs = {
|
|
4633
|
+
...asRecord(data.outputs),
|
|
4634
|
+
pdf: pdfOutput
|
|
4635
|
+
};
|
|
4636
|
+
if (pdfOutput.status === "written" && typeof pdfOutput.file === "string") {
|
|
4637
|
+
filesWritten.push(pdfOutput.file);
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
const summaryMarkdown = buildMarkdownSummary(data);
|
|
4641
|
+
if (outputDir) {
|
|
4475
4642
|
await fs2.mkdir(outputDir, { recursive: true });
|
|
4476
4643
|
const jsonPath = path2.join(outputDir, "review.json");
|
|
4477
4644
|
const markdownPath = path2.join(outputDir, "review.md");
|
|
@@ -4485,6 +4652,10 @@ var registerReviewCommand = (program2, deps) => {
|
|
|
4485
4652
|
format: options.format,
|
|
4486
4653
|
filesWritten
|
|
4487
4654
|
});
|
|
4655
|
+
if (pdfOutput?.status === "failed" && pdfOutput.failOnError === true) {
|
|
4656
|
+
process.exitCode = 1;
|
|
4657
|
+
return;
|
|
4658
|
+
}
|
|
4488
4659
|
if (data.gate.status === "fail") {
|
|
4489
4660
|
process.exitCode = 1;
|
|
4490
4661
|
return;
|
|
@@ -6986,6 +7157,13 @@ var generateWorkspaceConfig = (entry, parameters, sourceEntry) => {
|
|
|
6986
7157
|
"# fail_when_high_risk_findings_exist: true",
|
|
6987
7158
|
"# fail_when_validation_fails: true",
|
|
6988
7159
|
"# max_monthly_cost_usd: 500",
|
|
7160
|
+
"# review:",
|
|
7161
|
+
"# outputs:",
|
|
7162
|
+
"# pdf:",
|
|
7163
|
+
"# enabled: true",
|
|
7164
|
+
"# report_type: all",
|
|
7165
|
+
"# verbosity: evidence",
|
|
7166
|
+
"# fail_on_error: false",
|
|
6989
7167
|
""
|
|
6990
7168
|
].filter((line) => line.length > 0).join("\n");
|
|
6991
7169
|
};
|
|
@@ -17214,7 +17392,7 @@ program.command("tui").description("Open the CloudEval Terminal UI").option("--b
|
|
|
17214
17392
|
const { assertSecureBaseUrl } = await import("./dist-6LEMVXIY.js");
|
|
17215
17393
|
const [{ render }, { App }] = await Promise.all([
|
|
17216
17394
|
import("ink"),
|
|
17217
|
-
import("./App-
|
|
17395
|
+
import("./App-OHSI5XYX.js")
|
|
17218
17396
|
]);
|
|
17219
17397
|
const baseUrl = await resolveBaseUrl(options, command);
|
|
17220
17398
|
assertSecureBaseUrl(baseUrl);
|
|
@@ -17275,7 +17453,7 @@ program.command("chat").description("Start an interactive chat session").option(
|
|
|
17275
17453
|
const { assertSecureBaseUrl } = await import("./dist-6LEMVXIY.js");
|
|
17276
17454
|
const [{ render }, { App }] = await Promise.all([
|
|
17277
17455
|
import("ink"),
|
|
17278
|
-
import("./App-
|
|
17456
|
+
import("./App-OHSI5XYX.js")
|
|
17279
17457
|
]);
|
|
17280
17458
|
const baseUrl = await resolveBaseUrl(options, command);
|
|
17281
17459
|
assertSecureBaseUrl(baseUrl);
|
|
@@ -18067,7 +18245,7 @@ Error: ${errorMsg}
|
|
|
18067
18245
|
program.command("banner").description("Preview the startup banner and terminal capabilities").action(async () => {
|
|
18068
18246
|
const { render } = await import("ink");
|
|
18069
18247
|
const BannerPreview = React.lazy(async () => ({
|
|
18070
|
-
default: (await import("./Banner-
|
|
18248
|
+
default: (await import("./Banner-NKBFXQJ2.js")).Banner
|
|
18071
18249
|
}));
|
|
18072
18250
|
render(
|
|
18073
18251
|
/* @__PURE__ */ jsx(React.Suspense, { fallback: null, children: /* @__PURE__ */ jsx(BannerPreview, { disable: false }) })
|
package/package.json
CHANGED
package/sbom.spdx.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
{
|
|
15
15
|
"SPDXID": "SPDXRef-Package-CloudEval-CLI",
|
|
16
16
|
"name": "CloudEval CLI",
|
|
17
|
-
"versionInfo": "0.
|
|
17
|
+
"versionInfo": "0.31.0",
|
|
18
18
|
"downloadLocation": "https://github.com/ganakailabs/cloudeval-cli",
|
|
19
19
|
"filesAnalyzed": false,
|
|
20
20
|
"licenseConcluded": "LicenseRef-CloudEval-CLI",
|