@ganakailabs/cloudeval-cli 0.30.3 → 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 +25 -1
- package/dist/{App-SKVX7NAF.js → App-OHSI5XYX.js} +2 -2
- package/dist/{Banner-CRBHEOTC.js → Banner-NKBFXQJ2.js} +2 -2
- package/dist/{chunk-QB3BBKVH.js → chunk-PNA74AA5.js} +1 -1
- package/dist/{chunk-FPZWMNAI.js → chunk-YZ4GRXIK.js} +1 -1
- package/dist/cli.js +221 -10
- package/package.json +1 -1
- package/sbom.spdx.json +1 -1
package/README.md
CHANGED
|
@@ -117,7 +117,31 @@ jobs:
|
|
|
117
117
|
--non-interactive
|
|
118
118
|
```
|
|
119
119
|
|
|
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 Well-Architected radar/table drilldown, and cost Mermaid charts.
|
|
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
|
+
|
|
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` |
|
|
121
145
|
|
|
122
146
|
### MCP For Codex, Cursor, Claude, VS Code
|
|
123
147
|
|
|
@@ -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;
|
|
@@ -3397,6 +3485,28 @@ var mermaidAxisId = (label) => {
|
|
|
3397
3485
|
const normalized = normalizeKey(label).replace(/[^a-z0-9_]/g, "_");
|
|
3398
3486
|
return normalized || "pillar";
|
|
3399
3487
|
};
|
|
3488
|
+
var compactMermaidAxisLabel = (label) => {
|
|
3489
|
+
const compactLabels = {
|
|
3490
|
+
security: "Security",
|
|
3491
|
+
reliability: "Reliability",
|
|
3492
|
+
cost_optimization: "Cost",
|
|
3493
|
+
operational_excellence: "Ops",
|
|
3494
|
+
performance_efficiency: "Performance"
|
|
3495
|
+
};
|
|
3496
|
+
const normalized = normalizeKey(label);
|
|
3497
|
+
if (compactLabels[normalized]) {
|
|
3498
|
+
return compactLabels[normalized];
|
|
3499
|
+
}
|
|
3500
|
+
const cleaned = label.trim().replace(/\s+/g, " ");
|
|
3501
|
+
if (cleaned.length <= 16) {
|
|
3502
|
+
return cleaned;
|
|
3503
|
+
}
|
|
3504
|
+
const words = cleaned.split(" ");
|
|
3505
|
+
if (words.length > 1) {
|
|
3506
|
+
return words.slice(0, 2).map((word) => word.length > 8 ? word.slice(0, 8) : word).join(" ");
|
|
3507
|
+
}
|
|
3508
|
+
return cleaned.slice(0, 16);
|
|
3509
|
+
};
|
|
3400
3510
|
var wellArchitectedRadarLines = (pillars) => {
|
|
3401
3511
|
const scored = pillars.map((pillar) => {
|
|
3402
3512
|
const label = String(pillar.label ?? pillar.id ?? "Pillar");
|
|
@@ -3416,10 +3526,15 @@ var wellArchitectedRadarLines = (pillars) => {
|
|
|
3416
3526
|
"```mermaid",
|
|
3417
3527
|
"radar-beta",
|
|
3418
3528
|
" title Well-Architected posture",
|
|
3419
|
-
` axis ${scored.map(
|
|
3529
|
+
` axis ${scored.map(
|
|
3530
|
+
(pillar) => `${pillar.id}["${mermaidLabel(compactMermaidAxisLabel(pillar.label))}"]`
|
|
3531
|
+
).join(", ")}`,
|
|
3420
3532
|
` curve current["Current"]{${scored.map((pillar) => trimNumber(pillar.score, 3)).join(", ")}}`,
|
|
3533
|
+
" showLegend false",
|
|
3421
3534
|
" max 100",
|
|
3422
3535
|
" min 0",
|
|
3536
|
+
" graticule polygon",
|
|
3537
|
+
" ticks 4",
|
|
3423
3538
|
"```",
|
|
3424
3539
|
"",
|
|
3425
3540
|
"_If GitHub does not render Mermaid radar charts yet, use the table below as the fallback._"
|
|
@@ -3750,6 +3865,68 @@ var safeFetch = async (input) => {
|
|
|
3750
3865
|
return void 0;
|
|
3751
3866
|
}
|
|
3752
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
|
+
};
|
|
3753
3930
|
var parsePositiveInteger = (value, flagName, fallback) => {
|
|
3754
3931
|
if (value === void 0 || value === "") {
|
|
3755
3932
|
return fallback;
|
|
@@ -4186,7 +4363,9 @@ var buildMarkdownSummary = (data) => {
|
|
|
4186
4363
|
"",
|
|
4187
4364
|
...riskLines,
|
|
4188
4365
|
"",
|
|
4189
|
-
...radarLines.length ? [...radarLines, ""] : [],
|
|
4366
|
+
...radarLines.length ? ["**Radar (compact labels)**", "", ...radarLines, ""] : [],
|
|
4367
|
+
"**Scores**",
|
|
4368
|
+
"",
|
|
4190
4369
|
"| Pillar | Score | Rating |",
|
|
4191
4370
|
"| --- | ---: | --- |",
|
|
4192
4371
|
...pillarLines,
|
|
@@ -4202,14 +4381,18 @@ var buildMarkdownSummary = (data) => {
|
|
|
4202
4381
|
cost?.currency ?? data.gate?.cost?.estimatedSavings?.currency
|
|
4203
4382
|
);
|
|
4204
4383
|
if (impactLines.length) {
|
|
4205
|
-
costLines.push(...impactLines);
|
|
4384
|
+
costLines.push("**Cost impact**", "", ...impactLines);
|
|
4206
4385
|
} else if (data.gate?.cost?.estimatedSavings?.amount !== void 0) {
|
|
4207
4386
|
costLines.push(
|
|
4387
|
+
"**Cost impact**",
|
|
4388
|
+
"",
|
|
4208
4389
|
`- Estimated savings: **${formatMonthlyMoney(data.gate.cost.estimatedSavings.amount, data.gate.cost.estimatedSavings.currency)}**`
|
|
4209
4390
|
);
|
|
4210
4391
|
}
|
|
4211
4392
|
if (costPieRows.length) {
|
|
4212
4393
|
costLines.push(
|
|
4394
|
+
"",
|
|
4395
|
+
"**Cost split**",
|
|
4213
4396
|
"",
|
|
4214
4397
|
"```mermaid",
|
|
4215
4398
|
`pie title ${costPieTitle}`,
|
|
@@ -4435,10 +4618,27 @@ var registerReviewCommand = (program2, deps) => {
|
|
|
4435
4618
|
} else {
|
|
4436
4619
|
data.aiSummary = { enabled: false };
|
|
4437
4620
|
}
|
|
4438
|
-
const summaryMarkdown = buildMarkdownSummary(data);
|
|
4439
4621
|
const filesWritten = [];
|
|
4440
|
-
|
|
4441
|
-
|
|
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) {
|
|
4442
4642
|
await fs2.mkdir(outputDir, { recursive: true });
|
|
4443
4643
|
const jsonPath = path2.join(outputDir, "review.json");
|
|
4444
4644
|
const markdownPath = path2.join(outputDir, "review.md");
|
|
@@ -4452,6 +4652,10 @@ var registerReviewCommand = (program2, deps) => {
|
|
|
4452
4652
|
format: options.format,
|
|
4453
4653
|
filesWritten
|
|
4454
4654
|
});
|
|
4655
|
+
if (pdfOutput?.status === "failed" && pdfOutput.failOnError === true) {
|
|
4656
|
+
process.exitCode = 1;
|
|
4657
|
+
return;
|
|
4658
|
+
}
|
|
4455
4659
|
if (data.gate.status === "fail") {
|
|
4456
4660
|
process.exitCode = 1;
|
|
4457
4661
|
return;
|
|
@@ -6953,6 +7157,13 @@ var generateWorkspaceConfig = (entry, parameters, sourceEntry) => {
|
|
|
6953
7157
|
"# fail_when_high_risk_findings_exist: true",
|
|
6954
7158
|
"# fail_when_validation_fails: true",
|
|
6955
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",
|
|
6956
7167
|
""
|
|
6957
7168
|
].filter((line) => line.length > 0).join("\n");
|
|
6958
7169
|
};
|
|
@@ -17181,7 +17392,7 @@ program.command("tui").description("Open the CloudEval Terminal UI").option("--b
|
|
|
17181
17392
|
const { assertSecureBaseUrl } = await import("./dist-6LEMVXIY.js");
|
|
17182
17393
|
const [{ render }, { App }] = await Promise.all([
|
|
17183
17394
|
import("ink"),
|
|
17184
|
-
import("./App-
|
|
17395
|
+
import("./App-OHSI5XYX.js")
|
|
17185
17396
|
]);
|
|
17186
17397
|
const baseUrl = await resolveBaseUrl(options, command);
|
|
17187
17398
|
assertSecureBaseUrl(baseUrl);
|
|
@@ -17242,7 +17453,7 @@ program.command("chat").description("Start an interactive chat session").option(
|
|
|
17242
17453
|
const { assertSecureBaseUrl } = await import("./dist-6LEMVXIY.js");
|
|
17243
17454
|
const [{ render }, { App }] = await Promise.all([
|
|
17244
17455
|
import("ink"),
|
|
17245
|
-
import("./App-
|
|
17456
|
+
import("./App-OHSI5XYX.js")
|
|
17246
17457
|
]);
|
|
17247
17458
|
const baseUrl = await resolveBaseUrl(options, command);
|
|
17248
17459
|
assertSecureBaseUrl(baseUrl);
|
|
@@ -18034,7 +18245,7 @@ Error: ${errorMsg}
|
|
|
18034
18245
|
program.command("banner").description("Preview the startup banner and terminal capabilities").action(async () => {
|
|
18035
18246
|
const { render } = await import("ink");
|
|
18036
18247
|
const BannerPreview = React.lazy(async () => ({
|
|
18037
|
-
default: (await import("./Banner-
|
|
18248
|
+
default: (await import("./Banner-NKBFXQJ2.js")).Banner
|
|
18038
18249
|
}));
|
|
18039
18250
|
render(
|
|
18040
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",
|