@cleartrip/frontguard 0.2.6 → 0.2.7

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,16 +5,16 @@ 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 from 'fs/promises';
9
- import { pathToFileURL, fileURLToPath } from 'url';
10
- import { tmpdir } from 'os';
8
+ import fs, { writeFile } from 'fs/promises';
9
+ import { fileURLToPath, pathToFileURL } from 'url';
11
10
  import fs2, { existsSync, statSync, readFileSync } from 'fs';
12
- import { spawn, execFileSync } from 'child_process';
11
+ import { execFileSync, spawn } from 'child_process';
13
12
  import { createRequire } from 'module';
14
13
  import { pipeline } from 'stream/promises';
15
14
  import { PassThrough } from 'stream';
16
15
  import fg from 'fast-glob';
17
16
  import * as ts from 'typescript';
17
+ import { Resvg } from '@resvg/resvg-js';
18
18
 
19
19
  var __create = Object.create;
20
20
  var __defProp = Object.defineProperty;
@@ -2497,26 +2497,52 @@ async function initFrontGuard(cwd) {
2497
2497
  await fs.writeFile(tplPr, PR_TEMPLATE, "utf8");
2498
2498
  }
2499
2499
  }
2500
- var MAX_PNG_BYTES_FOR_EMBED = 22e4;
2500
+ var BITBUCKET_CHECKS_IMAGE_MARKDOWN_ALT = "FrontGuard checks summary";
2501
+ var MAX_BITBUCKET_INLINE_IMAGE_MARKDOWN_LINE_LENGTH = 3e5;
2502
+ var MAX_PNG_BYTES_BITBUCKET_INLINE = 21e4;
2503
+ var MARKDOWN_LINE_PREFIX = `![${BITBUCKET_CHECKS_IMAGE_MARKDOWN_ALT}](data:image/png;base64,`;
2504
+ function isPngBuffer(buf) {
2505
+ return buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71 && buf[4] === 13 && buf[5] === 10 && buf[6] === 26 && buf[7] === 10;
2506
+ }
2507
+ function pngBufferToBitbucketImageMarkdownLine(png) {
2508
+ if (!isPngBuffer(png)) {
2509
+ throw new TypeError("Buffer is not a PNG (missing IHDR signature)");
2510
+ }
2511
+ if (png.length > MAX_PNG_BYTES_BITBUCKET_INLINE) {
2512
+ throw new RangeError(
2513
+ `PNG too large for Bitbucket inline markdown (${png.length} bytes; max ${MAX_PNG_BYTES_BITBUCKET_INLINE}). Host the image and use an HTTPS URL instead.`
2514
+ );
2515
+ }
2516
+ const b64 = png.toString("base64");
2517
+ const line = `${MARKDOWN_LINE_PREFIX}${b64})`;
2518
+ if (line.length > MAX_BITBUCKET_INLINE_IMAGE_MARKDOWN_LINE_LENGTH) {
2519
+ throw new RangeError(
2520
+ `Inline markdown line too long (${line.length} chars; max ${MAX_BITBUCKET_INLINE_IMAGE_MARKDOWN_LINE_LENGTH})`
2521
+ );
2522
+ }
2523
+ return line;
2524
+ }
2525
+ function tryPngFileToBitbucketImageMarkdownLine(filePath) {
2526
+ try {
2527
+ if (!existsSync(filePath)) return null;
2528
+ const st = statSync(filePath);
2529
+ if (!st.isFile() || st.size > MAX_PNG_BYTES_BITBUCKET_INLINE) return null;
2530
+ const buf = readFileSync(filePath);
2531
+ return pngBufferToBitbucketImageMarkdownLine(buf);
2532
+ } catch {
2533
+ return null;
2534
+ }
2535
+ }
2536
+
2537
+ // src/ci/bitbucket-pr-snippet.ts
2501
2538
  function checksImageMarkdown() {
2502
2539
  const embedPath = process.env.FRONTGUARD_EMBED_CHECKS_PNG_PATH?.trim();
2503
2540
  if (embedPath) {
2504
- try {
2505
- if (!existsSync(embedPath)) return null;
2506
- const st = statSync(embedPath);
2507
- if (!st.isFile() || st.size > MAX_PNG_BYTES_FOR_EMBED) return null;
2508
- const buf = readFileSync(embedPath);
2509
- const b64 = buf.toString("base64");
2510
- const md = `![FrontGuard \u2014 checks summary](data:image/png;base64,${b64})`;
2511
- if (md.length > 45e4) return null;
2512
- return md;
2513
- } catch {
2514
- return null;
2515
- }
2541
+ return tryPngFileToBitbucketImageMarkdownLine(embedPath);
2516
2542
  }
2517
2543
  const u4 = process.env.FRONTGUARD_CHECKS_IMAGE_URL?.trim();
2518
2544
  if (!u4) return null;
2519
- return `![FrontGuard \u2014 checks summary](${u4})`;
2545
+ return `![${BITBUCKET_CHECKS_IMAGE_MARKDOWN_ALT}](${u4})`;
2520
2546
  }
2521
2547
  function bitbucketPipelineResultsUrl() {
2522
2548
  const full = process.env.BITBUCKET_REPO_FULL_NAME?.trim();
@@ -5547,29 +5573,131 @@ function applyAiAssistedEscalation(results, pr, config) {
5547
5573
  }
5548
5574
  }
5549
5575
  }
5550
- var PLAYWRIGHT = "playwright@1.49.1";
5551
- function runCmd(command, args) {
5552
- return new Promise((resolve, reject) => {
5553
- const p2 = spawn(command, args, { stdio: "inherit", env: process.env });
5554
- p2.on("error", reject);
5555
- p2.on("close", (code) => {
5556
- if (code === 0) resolve();
5557
- else reject(new Error(`${command} exited with code ${code}`));
5558
- });
5559
- });
5576
+ var W3 = 920;
5577
+ var PAD_X = 24;
5578
+ var TABLE_X = 20;
5579
+ var TABLE_W = 880;
5580
+ var HEADER_H = 34;
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;
5586
+ function escapeXml(s3) {
5587
+ return s3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5560
5588
  }
5561
- async function runChecksHtmlToPng(htmlPath, pngPath) {
5562
- const fileUrl = pathToFileURL(htmlPath).href;
5563
- const npx = process.platform === "win32" ? "npx.cmd" : "npx";
5564
- await runCmd(npx, ["-y", PLAYWRIGHT, "install", "chromium"]);
5565
- await runCmd(npx, [
5566
- "-y",
5567
- PLAYWRIGHT,
5568
- "screenshot",
5569
- fileUrl,
5570
- pngPath,
5571
- "--viewport-size=920,1000"
5572
- ]);
5589
+ function truncate5(s3, max) {
5590
+ const t3 = s3.replace(/\s+/g, " ").trim();
5591
+ if (t3.length <= max) return t3;
5592
+ return `${t3.slice(0, max - 1)}\u2026`;
5593
+ }
5594
+ function formatDuration(ms) {
5595
+ if (ms < 1e3) return `${ms} ms`;
5596
+ const s3 = Math.round(ms / 1e3);
5597
+ if (s3 < 60) return `${s3}s`;
5598
+ const m3 = Math.floor(s3 / 60);
5599
+ const r4 = s3 % 60;
5600
+ return r4 ? `${m3}m ${r4}s` : `${m3}m`;
5601
+ }
5602
+ function statusText(r4) {
5603
+ if (r4.skipped) return `Skipped \u2014 ${truncate5(r4.skipped, 36)}`;
5604
+ if (r4.findings.length === 0) return "Pass";
5605
+ return `${r4.findings.length} issue(s)`;
5606
+ }
5607
+ function dotFill(r4) {
5608
+ if (r4.skipped) return "#cbd5e1";
5609
+ if (r4.findings.length === 0) return "#16a34a";
5610
+ if (r4.findings.some((x3) => x3.severity === "block")) return "#dc2626";
5611
+ return "#d97706";
5612
+ }
5613
+ function riskFill(risk) {
5614
+ if (risk === "LOW") return "#16a34a";
5615
+ if (risk === "MEDIUM") return "#d97706";
5616
+ return "#dc2626";
5617
+ }
5618
+ function buildChecksSnapshotSvg(p2) {
5619
+ const { riskScore, mode, results, warns, infos, blocks } = p2;
5620
+ const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
5621
+ const brandY = 20;
5622
+ const titleY = 42;
5623
+ const metaY = 66;
5624
+ const tableTop = 88;
5625
+ const bodyRows = results.length;
5626
+ const cardH = HEADER_H + bodyRows * ROW_H;
5627
+ const totalH = tableTop + cardH + 28;
5628
+ const headerMid = tableTop + HEADER_H / 2 + 5;
5629
+ const rowTextY = (i3) => tableTop + HEADER_H + i3 * ROW_H + ROW_H / 2 + 5;
5630
+ const headerCells = [
5631
+ { x: COL_CHECK, label: "CHECK", anchor: "start" },
5632
+ { x: COL_STATUS, label: "STATUS", anchor: "start" },
5633
+ { x: COL_NUM, label: "#", anchor: "end" },
5634
+ { x: COL_TIME, label: "TIME", anchor: "end" }
5635
+ ];
5636
+ const rowLines = [];
5637
+ for (let i3 = 0; i3 < bodyRows; i3++) {
5638
+ const y4 = tableTop + HEADER_H + i3 * ROW_H;
5639
+ const fill = i3 % 2 === 1 ? "#f8fafc" : "#ffffff";
5640
+ rowLines.push(
5641
+ `<rect x="${TABLE_X}" y="${y4}" width="${TABLE_W}" height="${ROW_H}" fill="${fill}" stroke="none"/>`
5642
+ );
5643
+ }
5644
+ const bodyCells = [];
5645
+ for (let i3 = 0; i3 < bodyRows; i3++) {
5646
+ const r4 = results[i3];
5647
+ const cy = rowTextY(i3);
5648
+ const cx = 36;
5649
+ 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>`
5655
+ );
5656
+ }
5657
+ const gridLines = [];
5658
+ for (let i3 = 0; i3 <= bodyRows; i3++) {
5659
+ const y4 = tableTop + HEADER_H + i3 * ROW_H;
5660
+ gridLines.push(
5661
+ `<line x1="${TABLE_X}" y1="${y4}" x2="${TABLE_X + TABLE_W}" y2="${y4}" stroke="#e2e8f0" stroke-width="1"/>`
5662
+ );
5663
+ }
5664
+ return `<?xml version="1.0" encoding="UTF-8"?>
5665
+ <svg xmlns="http://www.w3.org/2000/svg" width="${W3}" height="${totalH}" viewBox="0 0 ${W3} ${totalH}">
5666
+ <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>
5675
+ <defs>
5676
+ <clipPath id="fg-card">
5677
+ <rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${cardH}" rx="10" ry="10"/>
5678
+ </clipPath>
5679
+ </defs>
5680
+ <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)">
5682
+ <rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${HEADER_H}" fill="#f1f5f9"/>
5683
+ ${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>`
5685
+ ).join("\n ")}
5686
+ ${rowLines.join("\n ")}
5687
+ ${gridLines.join("\n ")}
5688
+ ${bodyCells.join("\n ")}
5689
+ </g>
5690
+ </svg>`;
5691
+ }
5692
+ async function renderChecksSnapshotPng(pngPath, input) {
5693
+ const svg = buildChecksSnapshotSvg(input);
5694
+ const resvg = new Resvg(svg, {
5695
+ background: "#f8fafc",
5696
+ fitTo: { mode: "width", value: W3 },
5697
+ font: { loadSystemFonts: true }
5698
+ });
5699
+ const img = resvg.render();
5700
+ await writeFile(pngPath, img.asPng());
5573
5701
  }
5574
5702
 
5575
5703
  // src/report/builder.ts
@@ -5624,7 +5752,7 @@ function sortFindings(cwd, items) {
5624
5752
  return a3.f.message.localeCompare(b3.f.message);
5625
5753
  });
5626
5754
  }
5627
- function formatDuration(ms) {
5755
+ function formatDuration2(ms) {
5628
5756
  if (ms < 1e3) return `${ms} ms`;
5629
5757
  const s3 = Math.round(ms / 1e3);
5630
5758
  if (s3 < 60) return `${s3}s`;
@@ -5759,7 +5887,7 @@ function renderCheckTableRows(results) {
5759
5887
  const help = escapeHtml(getCheckDescription(r4.checkId));
5760
5888
  const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
5761
5889
  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>`;
5762
- 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>`;
5890
+ 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">${formatDuration2(r4.durationMs)}</td></tr>`;
5763
5891
  }).join("\n");
5764
5892
  }
5765
5893
  function buildChecksSnapshotHtml(p2) {
@@ -6185,14 +6313,32 @@ function buildReport(stack, pr, results, options) {
6185
6313
  lines,
6186
6314
  llmAppendix: options?.llmAppendix ?? null
6187
6315
  }) : null;
6188
- const checksSnapshotHtml = options?.emitChecksSnapshot === true ? buildChecksSnapshotHtml({
6316
+ const llmAppendix = options?.llmAppendix ?? null;
6317
+ const checksSnapshotInput = options?.emitChecksSnapshot === true ? {
6318
+ cwd,
6189
6319
  riskScore,
6190
6320
  mode,
6321
+ stack,
6322
+ pr,
6191
6323
  results,
6192
6324
  warns,
6193
6325
  infos,
6194
- blocks}) : null;
6195
- return { riskScore, stack, pr, results, markdown, consoleText, html, checksSnapshotHtml };
6326
+ blocks,
6327
+ lines,
6328
+ llmAppendix
6329
+ } : null;
6330
+ const checksSnapshotHtml = checksSnapshotInput != null ? buildChecksSnapshotHtml(checksSnapshotInput) : null;
6331
+ return {
6332
+ riskScore,
6333
+ stack,
6334
+ pr,
6335
+ results,
6336
+ markdown,
6337
+ consoleText,
6338
+ html,
6339
+ checksSnapshotHtml,
6340
+ checksSnapshotInput
6341
+ };
6196
6342
  }
6197
6343
  function scoreRisk(blocks, warns, lines, files) {
6198
6344
  let score = 0;
@@ -6240,7 +6386,7 @@ function countShieldColor(kind, n3) {
6240
6386
  if (n3 <= 10) return "yellow";
6241
6387
  return "orange";
6242
6388
  }
6243
- function formatDuration2(ms) {
6389
+ function formatDuration3(ms) {
6244
6390
  if (ms < 1e3) return `${ms} ms`;
6245
6391
  const s3 = Math.round(ms / 1e3);
6246
6392
  if (s3 < 60) return `${s3}s`;
@@ -6395,7 +6541,7 @@ function formatMarkdown(p2) {
6395
6541
  }
6396
6542
  const nFind = r4.skipped ? "\u2014" : String(r4.findings.length);
6397
6543
  sb.push(
6398
- `| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${formatDuration2(r4.durationMs)} |`
6544
+ `| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${formatDuration3(r4.durationMs)} |`
6399
6545
  );
6400
6546
  }
6401
6547
  sb.push("");
@@ -6960,36 +7106,24 @@ async function runFrontGuard(opts) {
6960
7106
  await fs.writeFile(opts.htmlOut, report.html, "utf8");
6961
7107
  }
6962
7108
  let embedPngPath = null;
6963
- let tmpDir = null;
6964
- let htmlForPng;
6965
- if ((opts.checksSnapshotOut || opts.checksPngOut) && report.checksSnapshotHtml) {
7109
+ if ((opts.checksSnapshotOut || opts.checksPngOut) && report.checksSnapshotHtml && report.checksSnapshotInput) {
6966
7110
  if (opts.checksSnapshotOut) {
6967
7111
  const snapPath = path5.isAbsolute(opts.checksSnapshotOut) ? opts.checksSnapshotOut : path5.join(opts.cwd, opts.checksSnapshotOut);
6968
7112
  await fs.writeFile(snapPath, report.checksSnapshotHtml, "utf8");
6969
- htmlForPng = snapPath;
6970
7113
  g.stderr.write(`
6971
7114
  FrontGuard: wrote checks snapshot HTML to ${snapPath}
6972
7115
  `);
6973
7116
  }
6974
7117
  if (opts.checksPngOut) {
6975
- if (!htmlForPng) {
6976
- tmpDir = await fs.mkdtemp(path5.join(tmpdir(), "fg-checks-"));
6977
- htmlForPng = path5.join(tmpDir, "checks.html");
6978
- await fs.writeFile(htmlForPng, report.checksSnapshotHtml, "utf8");
6979
- }
6980
7118
  const pngAbs = path5.isAbsolute(opts.checksPngOut) ? opts.checksPngOut : path5.join(opts.cwd, opts.checksPngOut);
6981
- await runChecksHtmlToPng(htmlForPng, pngAbs);
7119
+ await renderChecksSnapshotPng(pngAbs, report.checksSnapshotInput);
6982
7120
  embedPngPath = pngAbs;
6983
- g.stderr.write(`FrontGuard: wrote checks PNG to ${pngAbs} (Playwright via npx).
7121
+ g.stderr.write(`FrontGuard: wrote checks PNG to ${pngAbs}.
6984
7122
 
6985
7123
  `);
6986
- if (tmpDir) {
6987
- await fs.rm(tmpDir, { recursive: true, force: true });
6988
- tmpDir = null;
6989
- }
6990
- } else if (opts.checksSnapshotOut && htmlForPng) {
7124
+ } else if (opts.checksSnapshotOut) {
6991
7125
  g.stderr.write(
6992
- ` Tip: add --checksPngOut checks.png to render this HTML to PNG with Playwright (npx).
7126
+ ` Tip: add --checksPngOut checks.png to render the checks table to a PNG (no browser).
6993
7127
 
6994
7128
  `
6995
7129
  );
@@ -7067,7 +7201,7 @@ var run = defineCommand({
7067
7201
  },
7068
7202
  checksPngOut: {
7069
7203
  type: "string",
7070
- description: "Write PNG of the checks table via Playwright (npx). Pair with --checksSnapshotOut or use alone"
7204
+ description: "Write PNG of the checks table (SVG raster; @resvg/resvg-js, no browser). Pair with --checksSnapshotOut or use alone"
7071
7205
  },
7072
7206
  prCommentOut: {
7073
7207
  type: "string",