@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 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-QB3BBKVH.js";
41
+ } from "./chunk-PNA74AA5.js";
42
42
  import {
43
43
  CLI_VERSION
44
- } from "./chunk-FPZWMNAI.js";
44
+ } from "./chunk-YZ4GRXIK.js";
45
45
  import {
46
46
  raisedButtonStyle,
47
47
  terminalTheme
@@ -3,8 +3,8 @@ import {
3
3
  bannerMetaColor,
4
4
  bannerSegmentColor,
5
5
  splitBannerLineSegments
6
- } from "./chunk-QB3BBKVH.js";
7
- import "./chunk-FPZWMNAI.js";
6
+ } from "./chunk-PNA74AA5.js";
7
+ import "./chunk-YZ4GRXIK.js";
8
8
  import "./chunk-ZDKRIOMB.js";
9
9
  export {
10
10
  Banner,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  CLI_VERSION
3
- } from "./chunk-FPZWMNAI.js";
3
+ } from "./chunk-YZ4GRXIK.js";
4
4
  import {
5
5
  shouldUseColor,
6
6
  terminalTheme
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var CLI_VERSION = "0.30.3";
2
+ var CLI_VERSION = "0.31.0";
3
3
 
4
4
  export {
5
5
  CLI_VERSION
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-FPZWMNAI.js";
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((pillar) => `${pillar.id}["${mermaidLabel(pillar.label)}"]`).join(", ")}`,
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
- if (options.output) {
4441
- const outputDir = path2.resolve(options.output);
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-SKVX7NAF.js")
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-SKVX7NAF.js")
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-CRBHEOTC.js")).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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganakailabs/cloudeval-cli",
3
- "version": "0.30.3",
3
+ "version": "0.31.0",
4
4
  "license": "LicenseRef-CloudEval-CLI",
5
5
  "type": "module",
6
6
  "description": "Review Cloud infra-as-code and live environments from CLI, CI, and MCP agents.",
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.30.3",
17
+ "versionInfo": "0.31.0",
18
18
  "downloadLocation": "https://github.com/ganakailabs/cloudeval-cli",
19
19
  "filesAnalyzed": false,
20
20
  "licenseConcluded": "LicenseRef-CloudEval-CLI",