@ganakailabs/cloudeval-cli 0.27.2 → 0.28.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.
@@ -235,7 +235,7 @@ This notice is not a substitute for legal review before public or enterprise dis
235
235
  | safer-buffer | 2.1.2 | MIT | Nikita Skovoroda | https://github.com/ChALkeR/safer-buffer#readme |
236
236
  | scheduler | 0.23.2 | MIT | NOASSERTION | https://reactjs.org/ |
237
237
  | semver | 7.7.3 | ISC | GitHub Inc. | https://github.com/npm/node-semver#readme |
238
- | shell-quote | 1.8.3 | MIT | James Halliday | https://github.com/ljharb/shell-quote |
238
+ | shell-quote | 1.8.4 | MIT | James Halliday | https://github.com/ljharb/shell-quote |
239
239
  | signal-exit | 3.0.7 | ISC | Ben Coe | https://github.com/tapjs/signal-exit |
240
240
  | skin-tone | 2.0.0 | MIT | Sindre Sorhus | https://github.com/sindresorhus/skin-tone#readme |
241
241
  | slice-ansi | 5.0.0 | MIT | NOASSERTION | https://github.com/chalk/slice-ansi#readme |
@@ -38,10 +38,10 @@ import {
38
38
  } from "./chunk-USSCB2ZU.js";
39
39
  import {
40
40
  Banner
41
- } from "./chunk-3DBCT463.js";
41
+ } from "./chunk-GGHX5LSI.js";
42
42
  import {
43
43
  CLI_VERSION
44
- } from "./chunk-SLRYBS3P.js";
44
+ } from "./chunk-JFJQZGZH.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-3DBCT463.js";
7
- import "./chunk-SLRYBS3P.js";
6
+ } from "./chunk-GGHX5LSI.js";
7
+ import "./chunk-JFJQZGZH.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-SLRYBS3P.js";
3
+ } from "./chunk-JFJQZGZH.js";
4
4
  import {
5
5
  shouldUseColor,
6
6
  terminalTheme
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var CLI_VERSION = "0.27.2";
2
+ var CLI_VERSION = "0.28.0";
3
3
 
4
4
  export {
5
5
  CLI_VERSION
package/dist/cli.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  } from "./chunk-USSCB2ZU.js";
40
40
  import {
41
41
  CLI_VERSION
42
- } from "./chunk-SLRYBS3P.js";
42
+ } from "./chunk-JFJQZGZH.js";
43
43
 
44
44
  // src/runtime/prepareInk.ts
45
45
  import fs from "fs";
@@ -2842,10 +2842,14 @@ var namedAmount = (record) => numberFrom(
2842
2842
  record.amount,
2843
2843
  record.monthly_cost,
2844
2844
  record.monthlyCost,
2845
+ record.monthly_cost_estimate,
2846
+ record.monthlyCostEstimate,
2845
2847
  record.cost,
2846
2848
  record.value
2847
2849
  );
2848
- var namedLabel = (record, fallback) => String(record.name ?? record.service ?? record.label ?? record.category ?? fallback);
2850
+ var namedLabel = (record, fallback) => String(
2851
+ record.name ?? record.resource_name ?? record.resourceName ?? record.service ?? record.label ?? record.category ?? fallback
2852
+ );
2849
2853
  var mermaidLabel = (value) => value.replace(/"/g, "'");
2850
2854
  var joinReadableList = (values) => {
2851
2855
  if (values.length <= 1) {
@@ -2869,6 +2873,7 @@ var costServiceRows = (services, currency) => (Array.isArray(services) ? service
2869
2873
  ...rowCurrency ? { currency: rowCurrency } : {}
2870
2874
  };
2871
2875
  }).filter((service) => service !== void 0);
2876
+ var sortedPositiveCostRows = (rows) => rows.filter((row) => row.amount > 0).sort((left, right) => right.amount - left.amount);
2872
2877
  var reconcileCostServiceRows = (services, totalAmount, currency) => {
2873
2878
  const total = numberFrom(totalAmount);
2874
2879
  if (total === void 0 || services.length === 0) {
@@ -2897,7 +2902,80 @@ var reconcileCostServiceRows = (services, totalAmount, currency) => {
2897
2902
  }
2898
2903
  return services;
2899
2904
  };
2905
+ var compactCostRowsForChart = (rows, totalAmount, currency, options = {}) => {
2906
+ const maxRows = options.maxRows ?? 5;
2907
+ const remainderLabel = options.remainderLabel ?? "Unallocated";
2908
+ const total = numberFrom(totalAmount);
2909
+ const positiveRows = sortedPositiveCostRows(rows);
2910
+ const selected = positiveRows.slice(0, maxRows);
2911
+ const collapsed = positiveRows.slice(maxRows);
2912
+ const collapsedSum = Number(
2913
+ collapsed.reduce((sum, row) => sum + row.amount, 0).toFixed(3)
2914
+ );
2915
+ const result = [...selected];
2916
+ if (collapsedSum > 1e-3) {
2917
+ result.push({
2918
+ name: "Other",
2919
+ amount: collapsedSum,
2920
+ ...currency ? { currency } : {}
2921
+ });
2922
+ }
2923
+ const represented = result.reduce((sum, row) => sum + row.amount, 0);
2924
+ if (total !== void 0) {
2925
+ const delta = Number((total - represented).toFixed(3));
2926
+ if (delta > 1e-3) {
2927
+ result.push({
2928
+ name: remainderLabel,
2929
+ amount: delta,
2930
+ ...currency ? { currency } : {}
2931
+ });
2932
+ }
2933
+ }
2934
+ return result;
2935
+ };
2900
2936
  var markdownLink = (label, url) => url ? `[${label}](${url})` : label;
2937
+ var openInCloudEvalLines = (links) => {
2938
+ if (!links) {
2939
+ return [];
2940
+ }
2941
+ const reports = asRecord(links.reports);
2942
+ const downloads = asRecord(links.downloads);
2943
+ const entries = [
2944
+ ["Project preview", links.project],
2945
+ ["Architecture report", reports.architecture],
2946
+ ["Cost report", reports.cost],
2947
+ ["Validation details", reports.validation],
2948
+ ["Download PDF", downloads.pdf],
2949
+ ["Workflow run", links.workflowRun],
2950
+ ["Download review artifacts", downloads.reviewArtifacts]
2951
+ ].filter((entry) => typeof entry[1] === "string" && entry[1].length > 0);
2952
+ return entries.map(([label, url]) => `- ${markdownLink(label, url)}`);
2953
+ };
2954
+ var monthlyCostImpactLines = (currentAmount, savingsAmount, currency) => {
2955
+ const current = numberFrom(currentAmount);
2956
+ const savings = numberFrom(savingsAmount);
2957
+ if (current === void 0 || current <= 0 || savings === void 0 || savings <= 0) {
2958
+ return [];
2959
+ }
2960
+ const optimized = Math.max(current - savings, 0);
2961
+ const savingsPercent = current > 0 ? savings / current * 100 : void 0;
2962
+ const yMax = Math.max(current, optimized, savings);
2963
+ return [
2964
+ "| Metric | Amount |",
2965
+ "| --- | ---: |",
2966
+ `| Current monthly cost | **${formatMonthlyMoney(current, currency)}** |`,
2967
+ `| Potential savings | **${formatMonthlyMoney(savings, currency)}${savingsPercent !== void 0 ? ` (${trimNumber(savingsPercent, 1)}%)` : ""}** |`,
2968
+ `| Optimized monthly cost | **${formatMonthlyMoney(optimized, currency)}** |`,
2969
+ "",
2970
+ "```mermaid",
2971
+ "xychart-beta",
2972
+ ' title "Monthly cost impact"',
2973
+ ' x-axis ["Current", "Optimized"]',
2974
+ ` y-axis "${currency ? `${currency}/mo` : "monthly cost"}" 0 --> ${trimNumber(yMax, 3)}`,
2975
+ ` bar [${trimNumber(current, 3)}, ${trimNumber(optimized, 3)}]`,
2976
+ "```"
2977
+ ];
2978
+ };
2901
2979
  var extractPillars = (waf) => {
2902
2980
  const fromArray = waf?.parsed?.score?.pillars;
2903
2981
  if (Array.isArray(fromArray)) {
@@ -3125,6 +3203,16 @@ var evaluateGate = ({
3125
3203
  currency
3126
3204
  },
3127
3205
  topServices: (Array.isArray(cost?.parsed?.serviceGroups) ? cost.parsed.serviceGroups : entriesAsNamedRecords(cost?.report?.processed?.cost_by_service_family)).slice(0, 5),
3206
+ topResources: (firstArray(
3207
+ cost?.report?.raw?.resource_estimates,
3208
+ cost?.report?.raw?.resourceEstimates,
3209
+ cost?.raw?.resource_estimates,
3210
+ cost?.raw?.resourceEstimates,
3211
+ cost?.parsed?.resourceEstimates,
3212
+ cost?.parsed?.resource_estimates,
3213
+ preload?.latest_payloads?.cost?.raw?.resource_estimates,
3214
+ preload?.latest_payloads?.cost?.raw?.resourceEstimates
3215
+ ) ?? []).slice(0, 20),
3128
3216
  recommendations: (Array.isArray(cost?.parsed?.recommendations) ? cost.parsed.recommendations : Array.isArray(cost?.report?.processed?.optimization_recommendations) ? cost.report.processed.optimization_recommendations : []).slice(0, 5)
3129
3217
  };
3130
3218
  if (!gateConfig) {
@@ -3397,9 +3485,9 @@ var deterministicAiSummary = (data, error) => {
3397
3485
  const policyFailed = numberFrom(validation?.policyChecks?.failed) ?? numberFrom(validation?.policy_checks?.failed) ?? numberFrom(data.gate?.policy?.failed) ?? 0;
3398
3486
  const policyStatus = policyFailed > 0 ? "has failed checks" : "GOOD";
3399
3487
  const summary = [
3400
- `CloudEval review completed with ${String(data.gate?.status ?? "UNKNOWN").toUpperCase()}.`,
3401
- `Well-Architected posture is ${formatScore(score)} (${rating}), validation has ${displayNumber(failedTests)} failed unit tests, policy checks are ${policyStatus}, and monthly cost is ${formatMonthlyMoney(cost?.amount, cost?.currency)}.`,
3402
- "Prioritize failed validation checks and the weakest Well-Architected pillar first."
3488
+ `CloudEval review completed with **${String(data.gate?.status ?? "UNKNOWN").toUpperCase()}**.`,
3489
+ `Well-Architected posture is **${formatScore(score)} (${rating})**, validation has **${displayNumber(failedTests)} failed unit tests**, policy checks are **${policyStatus}**, and monthly cost is **${formatMonthlyMoney(cost?.amount, cost?.currency)}**.`,
3490
+ "Prioritize **failed validation checks** and the **weakest Well-Architected pillar** first."
3403
3491
  ].join(" ");
3404
3492
  return {
3405
3493
  enabled: true,
@@ -3409,17 +3497,17 @@ var deterministicAiSummary = (data, error) => {
3409
3497
  shortSummary: summary,
3410
3498
  detailsMarkdown: [
3411
3499
  "**Main risk**\nCloudEval could not produce an AI-written review summary, so use the deterministic gate evidence.",
3412
- "**Why it matters**\nFailed validation and weak architecture pillars are the highest-signal remediation inputs.",
3413
- "**Recommended actions**\nFix failed validation checks, address the weakest pillar, rerun CloudEval review, and compare the updated gate.",
3414
- "**Evidence used**\nGate status, Well-Architected score, validation totals, policy totals, and monthly cost."
3500
+ "**Why it matters**\n**Failed validation** and **weak architecture pillars** are the highest-signal remediation inputs.",
3501
+ "**Recommended actions**\nFix **failed validation checks**, address the **weakest pillar**, rerun CloudEval review, and compare the updated gate.",
3502
+ "**Evidence used**\n**Gate status**, **Well-Architected score**, **validation totals**, **policy totals**, and **monthly cost**."
3415
3503
  ].join("\n\n"),
3416
3504
  markdown: renderAiSummarySections(
3417
3505
  summary,
3418
3506
  [
3419
3507
  "**Main risk**\nCloudEval could not produce an AI-written review summary, so use the deterministic gate evidence.",
3420
- "**Why it matters**\nFailed validation and weak architecture pillars are the highest-signal remediation inputs.",
3421
- "**Recommended actions**\nFix failed validation checks, address the weakest pillar, rerun CloudEval review, and compare the updated gate.",
3422
- "**Evidence used**\nGate status, Well-Architected score, validation totals, policy totals, and monthly cost."
3508
+ "**Why it matters**\n**Failed validation** and **weak architecture pillars** are the highest-signal remediation inputs.",
3509
+ "**Recommended actions**\nFix **failed validation checks**, address the **weakest pillar**, rerun CloudEval review, and compare the updated gate.",
3510
+ "**Evidence used**\n**Gate status**, **Well-Architected score**, **validation totals**, **policy totals**, and **monthly cost**."
3423
3511
  ].join("\n\n")
3424
3512
  )
3425
3513
  };
@@ -3481,7 +3569,14 @@ var buildMarkdownSummary = (data) => {
3481
3569
  cost?.amount,
3482
3570
  cost?.currency
3483
3571
  );
3484
- const positiveCostServices = costServices.filter((service) => service.amount > 0);
3572
+ const resourceCostRows = compactCostRowsForChart(
3573
+ costServiceRows(data.gate?.cost?.topResources, cost?.currency),
3574
+ cost?.amount,
3575
+ cost?.currency,
3576
+ { maxRows: 5, remainderLabel: "Unallocated" }
3577
+ );
3578
+ const positiveResourceCosts = resourceCostRows.filter((resource) => resource.amount > 0);
3579
+ const openLinks = openInCloudEvalLines(data.links);
3485
3580
  const architectureLines = architectureSignalLines({
3486
3581
  architecture,
3487
3582
  costServices,
@@ -3505,6 +3600,9 @@ var buildMarkdownSummary = (data) => {
3505
3600
  `- **Ref**: \`${ref}\``,
3506
3601
  `- **Commit**: \`${commit}\``
3507
3602
  ];
3603
+ if (openLinks.length) {
3604
+ lines.push("", "#### Open in CloudEval", "", ...openLinks);
3605
+ }
3508
3606
  if (data.aiSummary?.markdown) {
3509
3607
  lines.push("", "#### AI summary", "", data.aiSummary.markdown);
3510
3608
  }
@@ -3528,11 +3626,29 @@ var buildMarkdownSummary = (data) => {
3528
3626
  }
3529
3627
  if (cost?.amount !== void 0 || cost?.threshold !== void 0) {
3530
3628
  const costLines = [];
3531
- if (data.gate?.cost?.estimatedSavings?.amount !== void 0) {
3629
+ const impactLines = monthlyCostImpactLines(
3630
+ cost?.amount,
3631
+ data.gate?.cost?.estimatedSavings?.amount,
3632
+ cost?.currency ?? data.gate?.cost?.estimatedSavings?.currency
3633
+ );
3634
+ if (impactLines.length) {
3635
+ costLines.push(...impactLines);
3636
+ } else if (data.gate?.cost?.estimatedSavings?.amount !== void 0) {
3532
3637
  costLines.push(
3533
3638
  `- Estimated savings: **${formatMonthlyMoney(data.gate.cost.estimatedSavings.amount, data.gate.cost.estimatedSavings.currency)}**`
3534
3639
  );
3535
3640
  }
3641
+ if (positiveResourceCosts.length) {
3642
+ costLines.push(
3643
+ "",
3644
+ "```mermaid",
3645
+ "pie title Monthly cost by resource",
3646
+ ...positiveResourceCosts.map(
3647
+ (resource) => ` "${mermaidLabel(resource.name)}" : ${trimNumber(resource.amount, 3)}`
3648
+ ),
3649
+ "```"
3650
+ );
3651
+ }
3536
3652
  if (costServices.length) {
3537
3653
  costLines.push(
3538
3654
  "",
@@ -3543,17 +3659,6 @@ var buildMarkdownSummary = (data) => {
3543
3659
  )
3544
3660
  );
3545
3661
  }
3546
- if (positiveCostServices.length) {
3547
- costLines.push(
3548
- "",
3549
- "```mermaid",
3550
- "pie title Monthly cost by service",
3551
- ...positiveCostServices.map(
3552
- (service) => ` "${mermaidLabel(service.name)}" : ${trimNumber(service.amount, 3)}`
3553
- ),
3554
- "```"
3555
- );
3556
- }
3557
3662
  if (costLines.length) {
3558
3663
  lines.push(
3559
3664
  "",
@@ -3673,18 +3778,58 @@ var registerReviewCommand = (program2, deps) => {
3673
3778
  }),
3674
3779
  readConfigText(cwd, options)
3675
3780
  ]);
3781
+ const frontendBaseUrl = resolveFrontendBaseUrl({ apiBaseUrl: context.baseUrl });
3782
+ const projectUrl = buildFrontendUrl({
3783
+ baseUrl: frontendBaseUrl,
3784
+ target: "project",
3785
+ projectId,
3786
+ view: "preview",
3787
+ layout: "architecture"
3788
+ });
3789
+ const workflowRunUrl = githubWorkflowRunUrl();
3676
3790
  const data = {
3677
3791
  projectId,
3678
3792
  project: {
3679
3793
  id: projectId,
3680
3794
  name: String(project?.name ?? projectId),
3681
- url: buildFrontendUrl({
3682
- baseUrl: resolveFrontendBaseUrl({ apiBaseUrl: context.baseUrl }),
3683
- target: "project",
3684
- projectId,
3685
- view: "preview",
3686
- layout: "architecture"
3687
- })
3795
+ url: projectUrl
3796
+ },
3797
+ links: {
3798
+ project: projectUrl,
3799
+ reports: {
3800
+ architecture: buildFrontendUrl({
3801
+ baseUrl: frontendBaseUrl,
3802
+ target: "reports",
3803
+ projectId,
3804
+ tab: "architecture",
3805
+ reportType: "architecture"
3806
+ }),
3807
+ cost: buildFrontendUrl({
3808
+ baseUrl: frontendBaseUrl,
3809
+ target: "reports",
3810
+ projectId,
3811
+ tab: "cost",
3812
+ reportType: "cost"
3813
+ }),
3814
+ validation: buildFrontendUrl({
3815
+ baseUrl: frontendBaseUrl,
3816
+ target: "reports",
3817
+ projectId,
3818
+ tab: "validation",
3819
+ reportType: "unit_tests"
3820
+ })
3821
+ },
3822
+ downloads: {
3823
+ pdf: buildFrontendUrl({
3824
+ baseUrl: frontendBaseUrl,
3825
+ target: "reports",
3826
+ projectId,
3827
+ downloadPdf: true,
3828
+ pdfVerbosity: "full"
3829
+ }),
3830
+ ...workflowRunUrl ? { reviewArtifacts: workflowRunUrl } : {}
3831
+ },
3832
+ ...workflowRunUrl ? { workflowRun: workflowRunUrl } : {}
3688
3833
  },
3689
3834
  repo,
3690
3835
  ref,
@@ -15626,7 +15771,7 @@ program.command("tui").description("Open the CloudEval Terminal UI").option(
15626
15771
  const { assertSecureBaseUrl } = await import("./dist-PEYJDO7A.js");
15627
15772
  const [{ render }, { App }] = await Promise.all([
15628
15773
  import("ink"),
15629
- import("./App-VSOTHLOD.js")
15774
+ import("./App-3RDW53BW.js")
15630
15775
  ]);
15631
15776
  const baseUrl = await resolveBaseUrl(options, command);
15632
15777
  assertSecureBaseUrl(baseUrl);
@@ -15684,7 +15829,7 @@ program.command("chat").description("Start an interactive chat session").option(
15684
15829
  const { assertSecureBaseUrl } = await import("./dist-PEYJDO7A.js");
15685
15830
  const [{ render }, { App }] = await Promise.all([
15686
15831
  import("ink"),
15687
- import("./App-VSOTHLOD.js")
15832
+ import("./App-3RDW53BW.js")
15688
15833
  ]);
15689
15834
  const baseUrl = await resolveBaseUrl(options, command);
15690
15835
  assertSecureBaseUrl(baseUrl);
@@ -16438,7 +16583,7 @@ Error: ${errorMsg}
16438
16583
  program.command("banner").description("Preview the startup banner and terminal capabilities").action(async () => {
16439
16584
  const { render } = await import("ink");
16440
16585
  const BannerPreview = React.lazy(async () => ({
16441
- default: (await import("./Banner-7XUOJYL5.js")).Banner
16586
+ default: (await import("./Banner-HGG5NHXF.js")).Banner
16442
16587
  }));
16443
16588
  render(
16444
16589
  /* @__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.27.2",
3
+ "version": "0.28.0",
4
4
  "license": "LicenseRef-CloudEval-CLI",
5
5
  "type": "module",
6
6
  "description": "CloudEval CLI for cloud architecture, cost, report, automation, and MCP workflows.",
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.27.2",
17
+ "versionInfo": "0.28.0",
18
18
  "downloadLocation": "https://github.com/ganakailabs/cloudeval-cli",
19
19
  "filesAnalyzed": false,
20
20
  "licenseConcluded": "LicenseRef-CloudEval-CLI",
@@ -2354,9 +2354,9 @@
2354
2354
  "summary": "The semantic version parser used by npm."
2355
2355
  },
2356
2356
  {
2357
- "SPDXID": "SPDXRef-Package-shell-quote-1.8.3",
2357
+ "SPDXID": "SPDXRef-Package-shell-quote-1.8.4",
2358
2358
  "name": "shell-quote",
2359
- "versionInfo": "1.8.3",
2359
+ "versionInfo": "1.8.4",
2360
2360
  "downloadLocation": "https://github.com/ljharb/shell-quote",
2361
2361
  "filesAnalyzed": false,
2362
2362
  "licenseConcluded": "MIT",
@@ -3847,7 +3847,7 @@
3847
3847
  {
3848
3848
  "spdxElementId": "SPDXRef-Package-CloudEval-CLI",
3849
3849
  "relationshipType": "DEPENDS_ON",
3850
- "relatedSpdxElement": "SPDXRef-Package-shell-quote-1.8.3"
3850
+ "relatedSpdxElement": "SPDXRef-Package-shell-quote-1.8.4"
3851
3851
  },
3852
3852
  {
3853
3853
  "spdxElementId": "SPDXRef-Package-CloudEval-CLI",