@cleartrip/frontguard 0.2.3 → 0.2.4

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