@cleartrip/frontguard 0.2.3 → 0.2.5

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/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import * as tty from 'tty';
6
6
  import { WriteStream } from 'tty';
7
7
  import path5, { sep, normalize, delimiter, resolve, dirname } from 'path';
8
8
  import fs from 'fs/promises';
9
- import { fileURLToPath, pathToFileURL } from 'url';
9
+ import { pathToFileURL, fileURLToPath } from 'url';
10
10
  import { execFileSync, spawn } from 'child_process';
11
11
  import { createRequire } from 'module';
12
12
  import fs2 from 'fs';
@@ -2498,6 +2498,11 @@ async function initFrontGuard(cwd) {
2498
2498
  }
2499
2499
 
2500
2500
  // src/ci/bitbucket-pr-snippet.ts
2501
+ function checksImageMarkdown() {
2502
+ const u4 = process.env.FRONTGUARD_CHECKS_IMAGE_URL?.trim();
2503
+ if (!u4) return null;
2504
+ return `![FrontGuard \u2014 checks summary](${u4})`;
2505
+ }
2501
2506
  function bitbucketPipelineResultsUrl() {
2502
2507
  const full = process.env.BITBUCKET_REPO_FULL_NAME?.trim();
2503
2508
  const bn = process.env.BITBUCKET_BUILD_NUMBER?.trim();
@@ -2519,11 +2524,19 @@ function bitbucketDownloadsPageUrl() {
2519
2524
  if (ws && slug) return `https://bitbucket.org/${ws}/${slug}/downloads/`;
2520
2525
  return null;
2521
2526
  }
2527
+ var DETAILED_REPORT_LINE = "For detailed check analysis, please open the full interactive report:";
2522
2528
  function formatBitbucketPrSnippet(report) {
2523
2529
  const publicReport = process.env.FRONTGUARD_PUBLIC_REPORT_URL?.trim();
2524
2530
  const linkOnly = process.env.FRONTGUARD_BITBUCKET_COMMENT_LINK_ONLY === "1";
2531
+ const imgMd = checksImageMarkdown();
2525
2532
  if (linkOnly && publicReport) {
2526
- return publicReport.endsWith("\n") ? publicReport : `${publicReport}
2533
+ const parts = [];
2534
+ if (imgMd) {
2535
+ parts.push(imgMd, "");
2536
+ parts.push(DETAILED_REPORT_LINE);
2537
+ }
2538
+ parts.push(publicReport.endsWith("\n") ? publicReport.slice(0, -1) : publicReport);
2539
+ return `${parts.join("\n")}
2527
2540
  `;
2528
2541
  }
2529
2542
  const downloadsName = process.env.FRONTGUARD_REPORT_DOWNLOAD_NAME?.trim();
@@ -2538,14 +2551,23 @@ function formatBitbucketPrSnippet(report) {
2538
2551
  (n3, r4) => n3 + r4.findings.filter((f4) => f4.severity === "warn").length,
2539
2552
  0
2540
2553
  );
2541
- const out = [
2554
+ const out = [];
2555
+ if (imgMd) {
2556
+ out.push(imgMd);
2557
+ out.push("");
2558
+ }
2559
+ out.push(
2542
2560
  "FrontGuard report (short summary)",
2543
2561
  "",
2544
2562
  `Risk: ${riskScore} | Blocking: ${blocks} | Warnings: ${warns}`,
2545
2563
  ""
2546
- ];
2564
+ );
2547
2565
  if (publicReport) {
2548
- out.push("Full interactive report (open in browser):");
2566
+ if (imgMd) {
2567
+ out.push(DETAILED_REPORT_LINE);
2568
+ } else {
2569
+ out.push("Full interactive report (open in browser):");
2570
+ }
2549
2571
  out.push(publicReport);
2550
2572
  out.push("");
2551
2573
  } else if (downloadsName && downloadsPage) {
@@ -5578,6 +5600,205 @@ function statusDot(r4) {
5578
5600
  return '<span class="dot dot-block" title="Blocking"></span>';
5579
5601
  return '<span class="dot dot-warn" title="Issues"></span>';
5580
5602
  }
5603
+ var CHECKS_TABLE_STYLES = `
5604
+ table.results {
5605
+ width: 100%;
5606
+ border-collapse: collapse;
5607
+ font-size: 0.875rem;
5608
+ background: var(--surface);
5609
+ border-radius: var(--radius);
5610
+ overflow: hidden;
5611
+ border: 1px solid var(--border);
5612
+ box-shadow: var(--shadow);
5613
+ }
5614
+ table.results th, table.results td {
5615
+ padding: 0.55rem 0.85rem;
5616
+ text-align: left;
5617
+ border-bottom: 1px solid var(--border);
5618
+ }
5619
+ table.results tr:last-child td { border-bottom: none; }
5620
+ table.results thead th {
5621
+ background: #f1f5f9;
5622
+ color: var(--muted);
5623
+ font-weight: 600;
5624
+ font-size: 0.72rem;
5625
+ text-transform: uppercase;
5626
+ letter-spacing: 0.04em;
5627
+ }
5628
+ .td-icon { width: 2rem; vertical-align: middle; }
5629
+ .td-check { vertical-align: middle; }
5630
+ .td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
5631
+ .check-title-cell {
5632
+ display: inline-flex;
5633
+ align-items: center;
5634
+ gap: 0.35rem;
5635
+ flex-wrap: nowrap;
5636
+ }
5637
+ .check-name { font-weight: 600; }
5638
+ .check-info-wrap {
5639
+ position: relative;
5640
+ display: inline-flex;
5641
+ align-items: center;
5642
+ flex-shrink: 0;
5643
+ }
5644
+ .check-info {
5645
+ display: inline-flex;
5646
+ align-items: center;
5647
+ justify-content: center;
5648
+ width: 1.125rem;
5649
+ height: 1.125rem;
5650
+ padding: 0;
5651
+ margin: 0;
5652
+ border: 1px solid var(--border);
5653
+ border-radius: 50%;
5654
+ background: #f1f5f9;
5655
+ color: var(--muted);
5656
+ font-size: 0.62rem;
5657
+ font-weight: 700;
5658
+ font-style: normal;
5659
+ line-height: 1;
5660
+ cursor: help;
5661
+ flex-shrink: 0;
5662
+ }
5663
+ .check-info:hover,
5664
+ .check-info:focus-visible {
5665
+ border-color: var(--accent);
5666
+ color: var(--accent);
5667
+ background: var(--accent-soft);
5668
+ outline: none;
5669
+ }
5670
+ .check-tooltip {
5671
+ position: absolute;
5672
+ left: 50%;
5673
+ bottom: calc(100% + 8px);
5674
+ transform: translateX(-50%);
5675
+ min-width: 12rem;
5676
+ max-width: min(22rem, 86vw);
5677
+ padding: 0.55rem 0.65rem;
5678
+ background: var(--text);
5679
+ color: #f8fafc;
5680
+ font-size: 0.78rem;
5681
+ font-weight: 400;
5682
+ line-height: 1.45;
5683
+ border-radius: 6px;
5684
+ box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
5685
+ z-index: 50;
5686
+ opacity: 0;
5687
+ visibility: hidden;
5688
+ pointer-events: none;
5689
+ transition: opacity 0.12s ease, visibility 0.12s ease;
5690
+ text-align: left;
5691
+ }
5692
+ .check-info-wrap:hover .check-tooltip,
5693
+ .check-info-wrap:focus-within .check-tooltip {
5694
+ opacity: 1;
5695
+ visibility: visible;
5696
+ }
5697
+ .check-tooltip::after {
5698
+ content: '';
5699
+ position: absolute;
5700
+ top: 100%;
5701
+ left: 50%;
5702
+ margin-left: -6px;
5703
+ border: 6px solid transparent;
5704
+ border-top-color: var(--text);
5705
+ }
5706
+ .dot {
5707
+ display: inline-block;
5708
+ width: 8px;
5709
+ height: 8px;
5710
+ border-radius: 50%;
5711
+ }
5712
+ .dot-ok { background: var(--ok); }
5713
+ .dot-warn { background: var(--warn); }
5714
+ .dot-block { background: var(--block); }
5715
+ .dot-skip { background: #cbd5e1; }
5716
+ `;
5717
+ function renderCheckTableRows(results) {
5718
+ return results.map((r4) => {
5719
+ const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
5720
+ const help = escapeHtml(getCheckDescription(r4.checkId));
5721
+ const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
5722
+ const checkTitle = `<span class="check-title-cell"><strong class="check-name">${escapeHtml(r4.checkId)}</strong><span class="check-info-wrap"><button type="button" class="check-info" title="${help}" aria-label="${ariaWhat}">i</button><span class="check-tooltip" role="tooltip">${help}</span></span></span>`;
5723
+ return `<tr><td class="td-icon">${statusDot(r4)}</td><td class="td-check">${checkTitle}</td><td class="td-status">${status}</td><td class="td-num">${r4.skipped ? "\u2014" : r4.findings.length}</td><td class="td-time">${formatDuration(r4.durationMs)}</td></tr>`;
5724
+ }).join("\n");
5725
+ }
5726
+ function buildChecksSnapshotHtml(p2) {
5727
+ const { riskScore, mode, results, warns, infos, blocks } = p2;
5728
+ const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
5729
+ const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
5730
+ const checkRows = renderCheckTableRows(results);
5731
+ return `<!DOCTYPE html>
5732
+ <html lang="en">
5733
+ <head>
5734
+ <meta charset="utf-8" />
5735
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
5736
+ <title>FrontGuard \u2014 Checks</title>
5737
+ <style>
5738
+ :root {
5739
+ --bg: #f8fafc;
5740
+ --surface: #ffffff;
5741
+ --text: #0f172a;
5742
+ --muted: #64748b;
5743
+ --border: #e2e8f0;
5744
+ --accent: #4f46e5;
5745
+ --accent-soft: #eef2ff;
5746
+ --block: #dc2626;
5747
+ --warn: #d97706;
5748
+ --info: #0284c7;
5749
+ --ok: #16a34a;
5750
+ --radius: 10px;
5751
+ --shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
5752
+ }
5753
+ * { box-sizing: border-box; }
5754
+ body {
5755
+ margin: 0;
5756
+ padding: 1.25rem 1.5rem 1.5rem;
5757
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
5758
+ background: var(--bg);
5759
+ color: var(--text);
5760
+ line-height: 1.55;
5761
+ font-size: 15px;
5762
+ max-width: 920px;
5763
+ }
5764
+ .brand {
5765
+ font-size: 0.75rem;
5766
+ font-weight: 600;
5767
+ letter-spacing: 0.12em;
5768
+ text-transform: uppercase;
5769
+ color: var(--muted);
5770
+ margin-bottom: 0.35rem;
5771
+ }
5772
+ .h2 {
5773
+ font-size: 1rem;
5774
+ font-weight: 600;
5775
+ margin: 0 0 0.5rem;
5776
+ color: var(--text);
5777
+ letter-spacing: -0.02em;
5778
+ }
5779
+ .snap-meta {
5780
+ font-size: 0.8rem;
5781
+ color: var(--muted);
5782
+ margin: 0 0 0.85rem;
5783
+ }
5784
+ .snap-meta strong { color: var(--text); font-weight: 600; }
5785
+ .risk-low { color: var(--ok); }
5786
+ .risk-med { color: var(--warn); }
5787
+ .risk-high { color: var(--block); }
5788
+ ${CHECKS_TABLE_STYLES}
5789
+ </style>
5790
+ </head>
5791
+ <body>
5792
+ <div class="brand">FrontGuard</div>
5793
+ <h2 class="h2">Checks</h2>
5794
+ <p class="snap-meta">Risk <strong class="${riskClass}">${riskScore}</strong> \xB7 ${escapeHtml(modeLabel)} \xB7 Blocking <strong>${blocks}</strong> \xB7 Warnings <strong>${warns}</strong> \xB7 Info <strong>${infos}</strong></p>
5795
+ <table class="results">
5796
+ <thead><tr><th></th><th>Check</th><th>Status</th><th>#</th><th>Time</th></tr></thead>
5797
+ <tbody>${checkRows}</tbody>
5798
+ </table>
5799
+ </body>
5800
+ </html>`;
5801
+ }
5581
5802
  function renderFindingCard(cwd, r4, f4) {
5582
5803
  const d3 = normalizeFinding(cwd, f4);
5583
5804
  const sevClass = f4.severity === "block" ? "sev-block" : f4.severity === "warn" ? "sev-warn" : "sev-info";
@@ -5602,13 +5823,7 @@ function buildHtmlReport(p2) {
5602
5823
  } = p2;
5603
5824
  const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
5604
5825
  const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
5605
- const checkRows = results.map((r4) => {
5606
- const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
5607
- const help = escapeHtml(getCheckDescription(r4.checkId));
5608
- const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
5609
- const checkTitle = `<span class="check-title-cell"><strong class="check-name">${escapeHtml(r4.checkId)}</strong><span class="check-info-wrap"><button type="button" class="check-info" title="${help}" aria-label="${ariaWhat}">i</button><span class="check-tooltip" role="tooltip">${help}</span></span></span>`;
5610
- return `<tr><td class="td-icon">${statusDot(r4)}</td><td class="td-check">${checkTitle}</td><td class="td-status">${status}</td><td class="td-num">${r4.skipped ? "\u2014" : r4.findings.length}</td><td class="td-time">${formatDuration(r4.durationMs)}</td></tr>`;
5611
- }).join("\n");
5826
+ const checkRows = renderCheckTableRows(results);
5612
5827
  const blockItems = sortFindings(
5613
5828
  cwd,
5614
5829
  results.flatMap(
@@ -5752,118 +5967,7 @@ function buildHtmlReport(p2) {
5752
5967
  font-weight: 500;
5753
5968
  background: #f1f5f9;
5754
5969
  }
5755
- table.results {
5756
- width: 100%;
5757
- border-collapse: collapse;
5758
- font-size: 0.875rem;
5759
- background: var(--surface);
5760
- border-radius: var(--radius);
5761
- overflow: hidden;
5762
- border: 1px solid var(--border);
5763
- box-shadow: var(--shadow);
5764
- }
5765
- table.results th, table.results td {
5766
- padding: 0.55rem 0.85rem;
5767
- text-align: left;
5768
- border-bottom: 1px solid var(--border);
5769
- }
5770
- table.results tr:last-child td { border-bottom: none; }
5771
- table.results thead th {
5772
- background: #f1f5f9;
5773
- color: var(--muted);
5774
- font-weight: 600;
5775
- font-size: 0.72rem;
5776
- text-transform: uppercase;
5777
- letter-spacing: 0.04em;
5778
- }
5779
- .td-icon { width: 2rem; vertical-align: middle; }
5780
- .td-check { vertical-align: middle; }
5781
- .td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
5782
- .check-title-cell {
5783
- display: inline-flex;
5784
- align-items: center;
5785
- gap: 0.35rem;
5786
- flex-wrap: nowrap;
5787
- }
5788
- .check-name { font-weight: 600; }
5789
- .check-info-wrap {
5790
- position: relative;
5791
- display: inline-flex;
5792
- align-items: center;
5793
- flex-shrink: 0;
5794
- }
5795
- .check-info {
5796
- display: inline-flex;
5797
- align-items: center;
5798
- justify-content: center;
5799
- width: 1.125rem;
5800
- height: 1.125rem;
5801
- padding: 0;
5802
- margin: 0;
5803
- border: 1px solid var(--border);
5804
- border-radius: 50%;
5805
- background: #f1f5f9;
5806
- color: var(--muted);
5807
- font-size: 0.62rem;
5808
- font-weight: 700;
5809
- font-style: normal;
5810
- line-height: 1;
5811
- cursor: help;
5812
- flex-shrink: 0;
5813
- }
5814
- .check-info:hover,
5815
- .check-info:focus-visible {
5816
- border-color: var(--accent);
5817
- color: var(--accent);
5818
- background: var(--accent-soft);
5819
- outline: none;
5820
- }
5821
- .check-tooltip {
5822
- position: absolute;
5823
- left: 50%;
5824
- bottom: calc(100% + 8px);
5825
- transform: translateX(-50%);
5826
- min-width: 12rem;
5827
- max-width: min(22rem, 86vw);
5828
- padding: 0.55rem 0.65rem;
5829
- background: var(--text);
5830
- color: #f8fafc;
5831
- font-size: 0.78rem;
5832
- font-weight: 400;
5833
- line-height: 1.45;
5834
- border-radius: 6px;
5835
- box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
5836
- z-index: 50;
5837
- opacity: 0;
5838
- visibility: hidden;
5839
- pointer-events: none;
5840
- transition: opacity 0.12s ease, visibility 0.12s ease;
5841
- text-align: left;
5842
- }
5843
- .check-info-wrap:hover .check-tooltip,
5844
- .check-info-wrap:focus-within .check-tooltip {
5845
- opacity: 1;
5846
- visibility: visible;
5847
- }
5848
- .check-tooltip::after {
5849
- content: '';
5850
- position: absolute;
5851
- top: 100%;
5852
- left: 50%;
5853
- margin-left: -6px;
5854
- border: 6px solid transparent;
5855
- border-top-color: var(--text);
5856
- }
5857
- .dot {
5858
- display: inline-block;
5859
- width: 8px;
5860
- height: 8px;
5861
- border-radius: 50%;
5862
- }
5863
- .dot-ok { background: var(--ok); }
5864
- .dot-warn { background: var(--warn); }
5865
- .dot-block { background: var(--block); }
5866
- .dot-skip { background: #cbd5e1; }
5970
+ ${CHECKS_TABLE_STYLES}
5867
5971
  .panel {
5868
5972
  background: var(--surface);
5869
5973
  border: 1px solid var(--border);
@@ -6042,7 +6146,14 @@ function buildReport(stack, pr, results, options) {
6042
6146
  lines,
6043
6147
  llmAppendix: options?.llmAppendix ?? null
6044
6148
  }) : null;
6045
- return { riskScore, stack, pr, results, markdown, consoleText, html };
6149
+ const checksSnapshotHtml = options?.emitChecksSnapshot === true ? buildChecksSnapshotHtml({
6150
+ riskScore,
6151
+ mode,
6152
+ results,
6153
+ warns,
6154
+ infos,
6155
+ blocks}) : null;
6156
+ return { riskScore, stack, pr, results, markdown, consoleText, html, checksSnapshotHtml };
6046
6157
  }
6047
6158
  function scoreRisk(blocks, warns, lines, files) {
6048
6159
  let score = 0;
@@ -6803,11 +6914,25 @@ async function runFrontGuard(opts) {
6803
6914
  mode,
6804
6915
  llmAppendix,
6805
6916
  cwd: opts.cwd,
6806
- emitHtml: Boolean(opts.htmlOut)
6917
+ emitHtml: Boolean(opts.htmlOut),
6918
+ emitChecksSnapshot: Boolean(opts.checksSnapshotOut)
6807
6919
  });
6808
6920
  if (opts.htmlOut && report.html) {
6809
6921
  await fs.writeFile(opts.htmlOut, report.html, "utf8");
6810
6922
  }
6923
+ if (opts.checksSnapshotOut && report.checksSnapshotHtml) {
6924
+ const snapPath = path5.isAbsolute(opts.checksSnapshotOut) ? opts.checksSnapshotOut : path5.join(opts.cwd, opts.checksSnapshotOut);
6925
+ await fs.writeFile(snapPath, report.checksSnapshotHtml, "utf8");
6926
+ const fileUrl = pathToFileURL(snapPath).href;
6927
+ g.stderr.write(
6928
+ `
6929
+ FrontGuard: wrote checks snapshot HTML to ${snapPath} (screenshot this file for PR comments).
6930
+ Example: npx playwright screenshot "${fileUrl}" frontguard-checks.png
6931
+ Host the PNG at an HTTPS URL, then set FRONTGUARD_CHECKS_IMAGE_URL before generating the PR comment.
6932
+
6933
+ `
6934
+ );
6935
+ }
6811
6936
  if (opts.prCommentOut) {
6812
6937
  const snippet = formatBitbucketPrSnippet(report);
6813
6938
  const abs = path5.isAbsolute(opts.prCommentOut) ? opts.prCommentOut : path5.join(opts.cwd, opts.prCommentOut);
@@ -6817,6 +6942,7 @@ async function runFrontGuard(opts) {
6817
6942
  FrontGuard: wrote Bitbucket PR comment text to ${abs} (${snippet.length} bytes).
6818
6943
  Use ONLY this file in your POST \u2026/pullrequests/{id}/comments payload (content.raw).
6819
6944
  Do not post frontguard-report.md or captured stdout \u2014 that is the long markdown log.
6945
+ Optional: set FRONTGUARD_CHECKS_IMAGE_URL so the comment includes a checks summary image.
6820
6946
 
6821
6947
  `
6822
6948
  );
@@ -6869,6 +6995,10 @@ var run = defineCommand({
6869
6995
  type: "string",
6870
6996
  description: "Write interactive HTML report (use with CI artifacts; PR comment links to download)"
6871
6997
  },
6998
+ checksSnapshotOut: {
6999
+ type: "string",
7000
+ description: "Write HTML with only the Checks table (screenshot \u2192 PNG \u2192 FRONTGUARD_CHECKS_IMAGE_URL in PR comment)"
7001
+ },
6872
7002
  prCommentOut: {
6873
7003
  type: "string",
6874
7004
  description: "Write short Markdown for Bitbucket PR comment (summary + pipeline link for HTML artifact)"
@@ -6881,6 +7011,7 @@ var run = defineCommand({
6881
7011
  enforce: Boolean(args.enforce),
6882
7012
  append: typeof args.append === "string" ? args.append : null,
6883
7013
  htmlOut: typeof args.htmlOut === "string" ? args.htmlOut : null,
7014
+ checksSnapshotOut: typeof args.checksSnapshotOut === "string" ? args.checksSnapshotOut : null,
6884
7015
  prCommentOut: typeof args.prCommentOut === "string" ? args.prCommentOut : null
6885
7016
  });
6886
7017
  }