@cleartrip/frontguard 0.2.7 → 0.2.8

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
@@ -5,7 +5,7 @@ import f from 'readline';
5
5
  import * as tty from 'tty';
6
6
  import { WriteStream } from 'tty';
7
7
  import path5, { sep, normalize, delimiter, resolve, dirname } from 'path';
8
- import fs, { writeFile } from 'fs/promises';
8
+ import fs, { writeFile, readFile } from 'fs/promises';
9
9
  import { fileURLToPath, pathToFileURL } from 'url';
10
10
  import fs2, { existsSync, statSync, readFileSync } from 'fs';
11
11
  import { execFileSync, spawn } from 'child_process';
@@ -5579,10 +5579,18 @@ var TABLE_X = 20;
5579
5579
  var TABLE_W = 880;
5580
5580
  var HEADER_H = 34;
5581
5581
  var ROW_H = 34;
5582
- var COL_CHECK = 56;
5583
- var COL_STATUS = 508;
5584
- var COL_NUM = 748;
5585
- var COL_TIME = 888;
5582
+ var COL_CHECK = 52;
5583
+ var ICON_X = 268;
5584
+ var COL_STATUS = 400;
5585
+ var COL_NUM = 700;
5586
+ var COL_TIME = 820;
5587
+ var OVER_LABEL_W = 132;
5588
+ var OVER_ROW_H = 36;
5589
+ var clipSeq = 0;
5590
+ function nextClipId() {
5591
+ clipSeq += 1;
5592
+ return `fgc${clipSeq}_${Math.random().toString(36).slice(2, 8)}`;
5593
+ }
5586
5594
  function escapeXml(s3) {
5587
5595
  return s3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5588
5596
  }
@@ -5600,7 +5608,7 @@ function formatDuration(ms) {
5600
5608
  return r4 ? `${m3}m ${r4}s` : `${m3}m`;
5601
5609
  }
5602
5610
  function statusText(r4) {
5603
- if (r4.skipped) return `Skipped \u2014 ${truncate5(r4.skipped, 36)}`;
5611
+ if (r4.skipped) return `Skipped \u2014 ${truncate5(r4.skipped, 42)}`;
5604
5612
  if (r4.findings.length === 0) return "Pass";
5605
5613
  return `${r4.findings.length} issue(s)`;
5606
5614
  }
@@ -5615,16 +5623,38 @@ function riskFill(risk) {
5615
5623
  if (risk === "MEDIUM") return "#d97706";
5616
5624
  return "#dc2626";
5617
5625
  }
5626
+ function resvgFontOptions() {
5627
+ const base = { loadSystemFonts: true };
5628
+ if (process.platform === "linux") {
5629
+ return {
5630
+ ...base,
5631
+ fontDirs: [
5632
+ "/usr/share/fonts/truetype/dejavu",
5633
+ "/usr/share/fonts/truetype/liberation",
5634
+ "/usr/share/fonts/opentype/noto",
5635
+ "/usr/share/fonts"
5636
+ ]
5637
+ };
5638
+ }
5639
+ return base;
5640
+ }
5618
5641
  function buildChecksSnapshotSvg(p2) {
5619
- const { riskScore, mode, results, warns, infos, blocks } = p2;
5642
+ const { riskScore, mode, stack, pr, results, lines } = p2;
5620
5643
  const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
5621
- const brandY = 20;
5622
- const titleY = 42;
5623
- const metaY = 66;
5624
- const tableTop = 88;
5644
+ const stackLine = truncate5(formatStackOneLiner(stack), 96);
5645
+ const prLine = pr && lines != null ? truncate5(
5646
+ `${lines} lines changed (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files`,
5647
+ 96
5648
+ ) : null;
5649
+ const nOverview = 3 + (prLine ? 1 : 0);
5650
+ const overviewH = nOverview * OVER_ROW_H;
5651
+ const overviewY0 = 44;
5652
+ const overviewClip = nextClipId();
5653
+ const checksTitleY = overviewY0 + overviewH + 22;
5654
+ const tableTop = checksTitleY + 22;
5625
5655
  const bodyRows = results.length;
5626
5656
  const cardH = HEADER_H + bodyRows * ROW_H;
5627
- const totalH = tableTop + cardH + 28;
5657
+ const totalH = tableTop + cardH + 36;
5628
5658
  const headerMid = tableTop + HEADER_H / 2 + 5;
5629
5659
  const rowTextY = (i3) => tableTop + HEADER_H + i3 * ROW_H + ROW_H / 2 + 5;
5630
5660
  const headerCells = [
@@ -5633,6 +5663,33 @@ function buildChecksSnapshotSvg(p2) {
5633
5663
  { x: COL_NUM, label: "#", anchor: "end" },
5634
5664
  { x: COL_TIME, label: "TIME", anchor: "end" }
5635
5665
  ];
5666
+ const overviewRows = [];
5667
+ const ov = [
5668
+ { label: "Risk score", kind: "risk" },
5669
+ { label: "Mode", kind: "text", text: modeLabel },
5670
+ { label: "Stack", kind: "text", text: stackLine }
5671
+ ];
5672
+ if (prLine) ov.push({ label: "Pull request", kind: "text", text: prLine });
5673
+ for (let i3 = 0; i3 < nOverview; i3++) {
5674
+ const row = ov[i3];
5675
+ const y4 = overviewY0 + i3 * OVER_ROW_H;
5676
+ const mid = y4 + OVER_ROW_H / 2 + 4;
5677
+ overviewRows.push(
5678
+ `<rect x="${TABLE_X}" y="${y4}" width="${OVER_LABEL_W}" height="${OVER_ROW_H}" fill="#f1f5f9" stroke="none"/>`,
5679
+ `<line x1="${TABLE_X}" y1="${y4 + OVER_ROW_H}" x2="${TABLE_X + TABLE_W}" y2="${y4 + OVER_ROW_H}" stroke="#e2e8f0" stroke-width="1"/>`,
5680
+ `<text x="${TABLE_X + 10}" y="${mid}" font-size="12" fill="#64748b" font-weight="500" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">${escapeXml(row.label)}</text>`
5681
+ );
5682
+ if (row.kind === "risk") {
5683
+ const rs = riskScore;
5684
+ overviewRows.push(
5685
+ `<text x="${TABLE_X + OVER_LABEL_W + 10}" y="${mid}" font-size="13" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif"><tspan fill="${riskFill(riskScore)}" font-weight="600">${escapeXml(rs)}</tspan><tspan fill="#64748b"> \u2014 heuristic</tspan></text>`
5686
+ );
5687
+ } else {
5688
+ overviewRows.push(
5689
+ `<text x="${TABLE_X + OVER_LABEL_W + 10}" y="${mid}" font-size="13" fill="#0f172a" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">${escapeXml(row.text)}</text>`
5690
+ );
5691
+ }
5692
+ }
5636
5693
  const rowLines = [];
5637
5694
  for (let i3 = 0; i3 < bodyRows; i3++) {
5638
5695
  const y4 = tableTop + HEADER_H + i3 * ROW_H;
@@ -5645,13 +5702,14 @@ function buildChecksSnapshotSvg(p2) {
5645
5702
  for (let i3 = 0; i3 < bodyRows; i3++) {
5646
5703
  const r4 = results[i3];
5647
5704
  const cy = rowTextY(i3);
5648
- const cx = 36;
5649
5705
  bodyCells.push(
5650
- `<circle cx="${cx}" cy="${cy - 4}" r="4.5" fill="${dotFill(r4)}"/>`,
5651
- `<text x="${COL_CHECK}" y="${cy}" font-size="13" font-weight="600" fill="#0f172a" font-family="system-ui, -apple-system, Segoe UI, sans-serif">${escapeXml(truncate5(r4.checkId, 46))}</text>`,
5652
- `<text x="${COL_STATUS}" y="${cy}" font-size="13" fill="#0f172a" font-family="system-ui, -apple-system, Segoe UI, sans-serif">${escapeXml(statusText(r4))}</text>`,
5653
- `<text x="${COL_NUM}" y="${cy}" font-size="13" fill="#64748b" font-family="ui-monospace, monospace" text-anchor="end">${escapeXml(r4.skipped ? "\u2014" : String(r4.findings.length))}</text>`,
5654
- `<text x="${COL_TIME}" y="${cy}" font-size="13" fill="#64748b" font-family="ui-monospace, monospace" text-anchor="end">${escapeXml(formatDuration(r4.durationMs))}</text>`
5706
+ `<circle cx="34" cy="${cy - 4}" r="4.5" fill="${dotFill(r4)}"/>`,
5707
+ `<text x="${COL_CHECK}" y="${cy}" font-size="13" font-weight="600" fill="#0f172a" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">${escapeXml(truncate5(r4.checkId, 22))}</text>`,
5708
+ `<circle cx="${ICON_X}" cy="${cy - 3}" r="8" fill="#f1f5f9" stroke="#e2e8f0" stroke-width="1"/>`,
5709
+ `<text x="${ICON_X}" y="${cy}" font-size="9" font-weight="700" fill="#64748b" text-anchor="middle" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">i</text>`,
5710
+ `<text x="${COL_STATUS}" y="${cy}" font-size="13" fill="#0f172a" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">${escapeXml(statusText(r4))}</text>`,
5711
+ `<text x="${COL_NUM}" y="${cy}" font-size="13" fill="#64748b" font-family="Liberation Mono, DejaVu Sans Mono, monospace" text-anchor="end">${escapeXml(r4.skipped ? "\u2014" : String(r4.findings.length))}</text>`,
5712
+ `<text x="${COL_TIME}" y="${cy}" font-size="13" fill="#64748b" font-family="Liberation Mono, DejaVu Sans Mono, monospace" text-anchor="end">${escapeXml(formatDuration(r4.durationMs))}</text>`
5655
5713
  );
5656
5714
  }
5657
5715
  const gridLines = [];
@@ -5661,27 +5719,30 @@ function buildChecksSnapshotSvg(p2) {
5661
5719
  `<line x1="${TABLE_X}" y1="${y4}" x2="${TABLE_X + TABLE_W}" y2="${y4}" stroke="#e2e8f0" stroke-width="1"/>`
5662
5720
  );
5663
5721
  }
5722
+ const checksClip = nextClipId();
5664
5723
  return `<?xml version="1.0" encoding="UTF-8"?>
5665
5724
  <svg xmlns="http://www.w3.org/2000/svg" width="${W3}" height="${totalH}" viewBox="0 0 ${W3} ${totalH}">
5666
5725
  <rect width="${W3}" height="${totalH}" fill="#f8fafc"/>
5667
- <text x="${PAD_X}" y="${brandY}" font-size="11" font-weight="600" fill="#64748b" letter-spacing="0.12em" font-family="system-ui, -apple-system, Segoe UI, sans-serif">FRONTGUARD</text>
5668
- <text x="${PAD_X}" y="${titleY}" font-size="17" font-weight="600" fill="#0f172a" font-family="system-ui, -apple-system, Segoe UI, sans-serif">Checks</text>
5669
- <text x="${PAD_X}" y="${metaY}" font-size="13" fill="#64748b" font-family="system-ui, -apple-system, Segoe UI, sans-serif">
5670
- <tspan>Risk </tspan><tspan fill="${riskFill(riskScore)}" font-weight="600">${escapeXml(riskScore)}</tspan>
5671
- <tspan> \xB7 ${escapeXml(modeLabel)} \xB7 Blocking </tspan><tspan fill="#0f172a" font-weight="600">${blocks}</tspan>
5672
- <tspan> \xB7 Warnings </tspan><tspan fill="#0f172a" font-weight="600">${warns}</tspan>
5673
- <tspan> \xB7 Info </tspan><tspan fill="#0f172a" font-weight="600">${infos}</tspan>
5674
- </text>
5726
+ <text x="${PAD_X}" y="18" font-size="11" font-weight="600" fill="#64748b" letter-spacing="0.12em" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">FRONTGUARD</text>
5727
+ <text x="${PAD_X}" y="36" font-size="16" font-weight="600" fill="#0f172a" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">Overview</text>
5675
5728
  <defs>
5676
- <clipPath id="fg-card">
5729
+ <clipPath id="${overviewClip}">
5730
+ <rect x="${TABLE_X}" y="${overviewY0}" width="${TABLE_W}" height="${overviewH}" rx="10" ry="10"/>
5731
+ </clipPath>
5732
+ <clipPath id="${checksClip}">
5677
5733
  <rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${cardH}" rx="10" ry="10"/>
5678
5734
  </clipPath>
5679
5735
  </defs>
5736
+ <rect x="${TABLE_X}" y="${overviewY0}" width="${TABLE_W}" height="${overviewH}" rx="10" ry="10" fill="#ffffff" stroke="#e2e8f0" stroke-width="1"/>
5737
+ <g clip-path="url(#${overviewClip})">
5738
+ ${overviewRows.join("\n ")}
5739
+ </g>
5740
+ <text x="${PAD_X}" y="${checksTitleY}" font-size="16" font-weight="600" fill="#0f172a" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif">Checks</text>
5680
5741
  <rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${cardH}" rx="10" ry="10" fill="#ffffff" stroke="#e2e8f0" stroke-width="1"/>
5681
- <g clip-path="url(#fg-card)">
5742
+ <g clip-path="url(#${checksClip})">
5682
5743
  <rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${HEADER_H}" fill="#f1f5f9"/>
5683
5744
  ${headerCells.map(
5684
- (c4) => `<text x="${c4.x}" y="${headerMid}" font-size="11" font-weight="600" fill="#64748b" letter-spacing="0.04em" font-family="system-ui, -apple-system, Segoe UI, sans-serif" text-anchor="${c4.anchor}">${c4.label}</text>`
5745
+ (c4) => `<text x="${c4.x}" y="${headerMid}" font-size="11" font-weight="600" fill="#64748b" letter-spacing="0.04em" font-family="DejaVu Sans, Liberation Sans, system-ui, sans-serif" text-anchor="${c4.anchor}">${c4.label}</text>`
5685
5746
  ).join("\n ")}
5686
5747
  ${rowLines.join("\n ")}
5687
5748
  ${gridLines.join("\n ")}
@@ -5694,10 +5755,21 @@ async function renderChecksSnapshotPng(pngPath, input) {
5694
5755
  const resvg = new Resvg(svg, {
5695
5756
  background: "#f8fafc",
5696
5757
  fitTo: { mode: "width", value: W3 },
5697
- font: { loadSystemFonts: true }
5758
+ font: resvgFontOptions()
5698
5759
  });
5699
5760
  const img = resvg.render();
5700
- await writeFile(pngPath, img.asPng());
5761
+ const png = img.asPng();
5762
+ await writeFile(pngPath, png);
5763
+ const written = await readFile(pngPath);
5764
+ if (!isPngBuffer(written) || written.length < 200) {
5765
+ throw new Error(
5766
+ "FrontGuard: checks PNG is missing or invalid after render. Install fonts on the runner (e.g. apt install fonts-dejavu-core fonts-liberation) and ensure @resvg/resvg-js native binary loads."
5767
+ );
5768
+ }
5769
+ const { width, height } = img;
5770
+ if (width < 16 || height < 16) {
5771
+ throw new Error(`FrontGuard: checks PNG has invalid dimensions ${width}x${height}.`);
5772
+ }
5701
5773
  }
5702
5774
 
5703
5775
  // src/report/builder.ts
@@ -5881,6 +5953,32 @@ var CHECKS_TABLE_STYLES = `
5881
5953
  .dot-block { background: var(--block); }
5882
5954
  .dot-skip { background: #cbd5e1; }
5883
5955
  `;
5956
+ var SNAPSHOT_OVERVIEW_STYLES = `
5957
+ .snapshot {
5958
+ width: 100%;
5959
+ border-collapse: collapse;
5960
+ font-size: 0.9rem;
5961
+ background: var(--surface);
5962
+ border-radius: var(--radius);
5963
+ overflow: hidden;
5964
+ border: 1px solid var(--border);
5965
+ box-shadow: var(--shadow);
5966
+ margin: 0 0 1.25rem;
5967
+ }
5968
+ .snapshot th, .snapshot td {
5969
+ padding: 0.65rem 1rem;
5970
+ text-align: left;
5971
+ border-bottom: 1px solid var(--border);
5972
+ }
5973
+ .snapshot tr:last-child th, .snapshot tr:last-child td { border-bottom: none; }
5974
+ .snapshot th {
5975
+ width: 9rem;
5976
+ color: var(--muted);
5977
+ font-weight: 500;
5978
+ background: #f1f5f9;
5979
+ }
5980
+ .muted { color: var(--muted); }
5981
+ `;
5884
5982
  function renderCheckTableRows(results) {
5885
5983
  return results.map((r4) => {
5886
5984
  const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
@@ -5891,16 +5989,17 @@ function renderCheckTableRows(results) {
5891
5989
  }).join("\n");
5892
5990
  }
5893
5991
  function buildChecksSnapshotHtml(p2) {
5894
- const { riskScore, mode, results, warns, infos, blocks } = p2;
5992
+ const { riskScore, mode, stack, pr, results, lines } = p2;
5895
5993
  const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
5896
5994
  const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
5897
5995
  const checkRows = renderCheckTableRows(results);
5996
+ const prRow = pr && lines != null ? `<tr><th>Pull request</th><td>${lines} lines changed (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files</td></tr>` : "";
5898
5997
  return `<!DOCTYPE html>
5899
5998
  <html lang="en">
5900
5999
  <head>
5901
6000
  <meta charset="utf-8" />
5902
6001
  <meta name="viewport" content="width=device-width, initial-scale=1" />
5903
- <title>FrontGuard \u2014 Checks</title>
6002
+ <title>FrontGuard \u2014 Snapshot</title>
5904
6003
  <style>
5905
6004
  :root {
5906
6005
  --bg: #f8fafc;
@@ -5939,26 +6038,27 @@ function buildChecksSnapshotHtml(p2) {
5939
6038
  .h2 {
5940
6039
  font-size: 1rem;
5941
6040
  font-weight: 600;
5942
- margin: 0 0 0.5rem;
6041
+ margin: 0 0 0.85rem;
5943
6042
  color: var(--text);
5944
6043
  letter-spacing: -0.02em;
5945
6044
  }
5946
- .snap-meta {
5947
- font-size: 0.8rem;
5948
- color: var(--muted);
5949
- margin: 0 0 0.85rem;
5950
- }
5951
- .snap-meta strong { color: var(--text); font-weight: 600; }
5952
6045
  .risk-low { color: var(--ok); }
5953
6046
  .risk-med { color: var(--warn); }
5954
6047
  .risk-high { color: var(--block); }
6048
+ ${SNAPSHOT_OVERVIEW_STYLES}
5955
6049
  ${CHECKS_TABLE_STYLES}
5956
6050
  </style>
5957
6051
  </head>
5958
6052
  <body>
5959
6053
  <div class="brand">FrontGuard</div>
6054
+ <h2 class="h2">Overview</h2>
6055
+ <table class="snapshot">
6056
+ <tr><th>Risk score</th><td><strong class="${riskClass}">${riskScore}</strong> <span class="muted">\u2014 heuristic</span></td></tr>
6057
+ <tr><th>Mode</th><td>${escapeHtml(modeLabel)}</td></tr>
6058
+ <tr><th>Stack</th><td>${escapeHtml(formatStackOneLiner(stack))}</td></tr>
6059
+ ${prRow}
6060
+ </table>
5960
6061
  <h2 class="h2">Checks</h2>
5961
- <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>
5962
6062
  <table class="results">
5963
6063
  <thead><tr><th></th><th>Check</th><th>Status</th><th>#</th><th>Time</th></tr></thead>
5964
6064
  <tbody>${checkRows}</tbody>