@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 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-NOR7UT66.js";
41
+ } from "./chunk-PNA74AA5.js";
42
42
  import {
43
43
  CLI_VERSION
44
- } from "./chunk-RVZOUNMP.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-NOR7UT66.js";
7
- import "./chunk-RVZOUNMP.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-RVZOUNMP.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.4";
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-RVZOUNMP.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;
@@ -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
- if (options.output) {
4474
- 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) {
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-AN3ELGIY.js")
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-AN3ELGIY.js")
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-XD5GCTUQ.js")).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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganakailabs/cloudeval-cli",
3
- "version": "0.30.4",
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.4",
17
+ "versionInfo": "0.31.0",
18
18
  "downloadLocation": "https://github.com/ganakailabs/cloudeval-cli",
19
19
  "filesAnalyzed": false,
20
20
  "licenseConcluded": "LicenseRef-CloudEval-CLI",