@cleartrip/frontguard 0.2.6 → 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 +309 -75
- package/dist/cli.js.map +1 -1
- package/package.json +5 -2
- package/templates/bitbucket-pipelines.yml +19 -6
- package/templates/freekit-ci-setup.md +2 -2
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 {
|
|
10
|
-
import { tmpdir } from 'os';
|
|
8
|
+
import fs, { writeFile, readFile } from 'fs/promises';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
11
10
|
import fs2, { existsSync, statSync, readFileSync } from 'fs';
|
|
12
|
-
import {
|
|
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
|
|
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 = ` {
|
|
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
|
-
|
|
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 = ``;
|
|
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 ``;
|
|
2520
2546
|
}
|
|
2521
2547
|
function bitbucketPipelineResultsUrl() {
|
|
2522
2548
|
const full = process.env.BITBUCKET_REPO_FULL_NAME?.trim();
|
|
@@ -5547,29 +5573,203 @@ function applyAiAssistedEscalation(results, pr, config) {
|
|
|
5547
5573
|
}
|
|
5548
5574
|
}
|
|
5549
5575
|
}
|
|
5550
|
-
var
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
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 = 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
|
+
}
|
|
5594
|
+
function escapeXml(s3) {
|
|
5595
|
+
return s3.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
5560
5596
|
}
|
|
5561
|
-
|
|
5562
|
-
const
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5597
|
+
function truncate5(s3, max) {
|
|
5598
|
+
const t3 = s3.replace(/\s+/g, " ").trim();
|
|
5599
|
+
if (t3.length <= max) return t3;
|
|
5600
|
+
return `${t3.slice(0, max - 1)}\u2026`;
|
|
5601
|
+
}
|
|
5602
|
+
function formatDuration(ms) {
|
|
5603
|
+
if (ms < 1e3) return `${ms} ms`;
|
|
5604
|
+
const s3 = Math.round(ms / 1e3);
|
|
5605
|
+
if (s3 < 60) return `${s3}s`;
|
|
5606
|
+
const m3 = Math.floor(s3 / 60);
|
|
5607
|
+
const r4 = s3 % 60;
|
|
5608
|
+
return r4 ? `${m3}m ${r4}s` : `${m3}m`;
|
|
5609
|
+
}
|
|
5610
|
+
function statusText(r4) {
|
|
5611
|
+
if (r4.skipped) return `Skipped \u2014 ${truncate5(r4.skipped, 42)}`;
|
|
5612
|
+
if (r4.findings.length === 0) return "Pass";
|
|
5613
|
+
return `${r4.findings.length} issue(s)`;
|
|
5614
|
+
}
|
|
5615
|
+
function dotFill(r4) {
|
|
5616
|
+
if (r4.skipped) return "#cbd5e1";
|
|
5617
|
+
if (r4.findings.length === 0) return "#16a34a";
|
|
5618
|
+
if (r4.findings.some((x3) => x3.severity === "block")) return "#dc2626";
|
|
5619
|
+
return "#d97706";
|
|
5620
|
+
}
|
|
5621
|
+
function riskFill(risk) {
|
|
5622
|
+
if (risk === "LOW") return "#16a34a";
|
|
5623
|
+
if (risk === "MEDIUM") return "#d97706";
|
|
5624
|
+
return "#dc2626";
|
|
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
|
+
}
|
|
5641
|
+
function buildChecksSnapshotSvg(p2) {
|
|
5642
|
+
const { riskScore, mode, stack, pr, results, lines } = p2;
|
|
5643
|
+
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
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;
|
|
5655
|
+
const bodyRows = results.length;
|
|
5656
|
+
const cardH = HEADER_H + bodyRows * ROW_H;
|
|
5657
|
+
const totalH = tableTop + cardH + 36;
|
|
5658
|
+
const headerMid = tableTop + HEADER_H / 2 + 5;
|
|
5659
|
+
const rowTextY = (i3) => tableTop + HEADER_H + i3 * ROW_H + ROW_H / 2 + 5;
|
|
5660
|
+
const headerCells = [
|
|
5661
|
+
{ x: COL_CHECK, label: "CHECK", anchor: "start" },
|
|
5662
|
+
{ x: COL_STATUS, label: "STATUS", anchor: "start" },
|
|
5663
|
+
{ x: COL_NUM, label: "#", anchor: "end" },
|
|
5664
|
+
{ x: COL_TIME, label: "TIME", anchor: "end" }
|
|
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
|
+
}
|
|
5693
|
+
const rowLines = [];
|
|
5694
|
+
for (let i3 = 0; i3 < bodyRows; i3++) {
|
|
5695
|
+
const y4 = tableTop + HEADER_H + i3 * ROW_H;
|
|
5696
|
+
const fill = i3 % 2 === 1 ? "#f8fafc" : "#ffffff";
|
|
5697
|
+
rowLines.push(
|
|
5698
|
+
`<rect x="${TABLE_X}" y="${y4}" width="${TABLE_W}" height="${ROW_H}" fill="${fill}" stroke="none"/>`
|
|
5699
|
+
);
|
|
5700
|
+
}
|
|
5701
|
+
const bodyCells = [];
|
|
5702
|
+
for (let i3 = 0; i3 < bodyRows; i3++) {
|
|
5703
|
+
const r4 = results[i3];
|
|
5704
|
+
const cy = rowTextY(i3);
|
|
5705
|
+
bodyCells.push(
|
|
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>`
|
|
5713
|
+
);
|
|
5714
|
+
}
|
|
5715
|
+
const gridLines = [];
|
|
5716
|
+
for (let i3 = 0; i3 <= bodyRows; i3++) {
|
|
5717
|
+
const y4 = tableTop + HEADER_H + i3 * ROW_H;
|
|
5718
|
+
gridLines.push(
|
|
5719
|
+
`<line x1="${TABLE_X}" y1="${y4}" x2="${TABLE_X + TABLE_W}" y2="${y4}" stroke="#e2e8f0" stroke-width="1"/>`
|
|
5720
|
+
);
|
|
5721
|
+
}
|
|
5722
|
+
const checksClip = nextClipId();
|
|
5723
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5724
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${W3}" height="${totalH}" viewBox="0 0 ${W3} ${totalH}">
|
|
5725
|
+
<rect width="${W3}" height="${totalH}" fill="#f8fafc"/>
|
|
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>
|
|
5728
|
+
<defs>
|
|
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}">
|
|
5733
|
+
<rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${cardH}" rx="10" ry="10"/>
|
|
5734
|
+
</clipPath>
|
|
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>
|
|
5741
|
+
<rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${cardH}" rx="10" ry="10" fill="#ffffff" stroke="#e2e8f0" stroke-width="1"/>
|
|
5742
|
+
<g clip-path="url(#${checksClip})">
|
|
5743
|
+
<rect x="${TABLE_X}" y="${tableTop}" width="${TABLE_W}" height="${HEADER_H}" fill="#f1f5f9"/>
|
|
5744
|
+
${headerCells.map(
|
|
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>`
|
|
5746
|
+
).join("\n ")}
|
|
5747
|
+
${rowLines.join("\n ")}
|
|
5748
|
+
${gridLines.join("\n ")}
|
|
5749
|
+
${bodyCells.join("\n ")}
|
|
5750
|
+
</g>
|
|
5751
|
+
</svg>`;
|
|
5752
|
+
}
|
|
5753
|
+
async function renderChecksSnapshotPng(pngPath, input) {
|
|
5754
|
+
const svg = buildChecksSnapshotSvg(input);
|
|
5755
|
+
const resvg = new Resvg(svg, {
|
|
5756
|
+
background: "#f8fafc",
|
|
5757
|
+
fitTo: { mode: "width", value: W3 },
|
|
5758
|
+
font: resvgFontOptions()
|
|
5759
|
+
});
|
|
5760
|
+
const img = resvg.render();
|
|
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
|
+
}
|
|
5573
5773
|
}
|
|
5574
5774
|
|
|
5575
5775
|
// src/report/builder.ts
|
|
@@ -5624,7 +5824,7 @@ function sortFindings(cwd, items) {
|
|
|
5624
5824
|
return a3.f.message.localeCompare(b3.f.message);
|
|
5625
5825
|
});
|
|
5626
5826
|
}
|
|
5627
|
-
function
|
|
5827
|
+
function formatDuration2(ms) {
|
|
5628
5828
|
if (ms < 1e3) return `${ms} ms`;
|
|
5629
5829
|
const s3 = Math.round(ms / 1e3);
|
|
5630
5830
|
if (s3 < 60) return `${s3}s`;
|
|
@@ -5753,26 +5953,53 @@ var CHECKS_TABLE_STYLES = `
|
|
|
5753
5953
|
.dot-block { background: var(--block); }
|
|
5754
5954
|
.dot-skip { background: #cbd5e1; }
|
|
5755
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
|
+
`;
|
|
5756
5982
|
function renderCheckTableRows(results) {
|
|
5757
5983
|
return results.map((r4) => {
|
|
5758
5984
|
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
|
|
5759
5985
|
const help = escapeHtml(getCheckDescription(r4.checkId));
|
|
5760
5986
|
const ariaWhat = escapeHtml(`What does the ${r4.checkId} check do?`);
|
|
5761
5987
|
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">${
|
|
5988
|
+
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
5989
|
}).join("\n");
|
|
5764
5990
|
}
|
|
5765
5991
|
function buildChecksSnapshotHtml(p2) {
|
|
5766
|
-
const { riskScore, mode,
|
|
5992
|
+
const { riskScore, mode, stack, pr, results, lines } = p2;
|
|
5767
5993
|
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
5768
5994
|
const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
|
|
5769
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>` : "";
|
|
5770
5997
|
return `<!DOCTYPE html>
|
|
5771
5998
|
<html lang="en">
|
|
5772
5999
|
<head>
|
|
5773
6000
|
<meta charset="utf-8" />
|
|
5774
6001
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5775
|
-
<title>FrontGuard \u2014
|
|
6002
|
+
<title>FrontGuard \u2014 Snapshot</title>
|
|
5776
6003
|
<style>
|
|
5777
6004
|
:root {
|
|
5778
6005
|
--bg: #f8fafc;
|
|
@@ -5811,26 +6038,27 @@ function buildChecksSnapshotHtml(p2) {
|
|
|
5811
6038
|
.h2 {
|
|
5812
6039
|
font-size: 1rem;
|
|
5813
6040
|
font-weight: 600;
|
|
5814
|
-
margin: 0 0 0.
|
|
6041
|
+
margin: 0 0 0.85rem;
|
|
5815
6042
|
color: var(--text);
|
|
5816
6043
|
letter-spacing: -0.02em;
|
|
5817
6044
|
}
|
|
5818
|
-
.snap-meta {
|
|
5819
|
-
font-size: 0.8rem;
|
|
5820
|
-
color: var(--muted);
|
|
5821
|
-
margin: 0 0 0.85rem;
|
|
5822
|
-
}
|
|
5823
|
-
.snap-meta strong { color: var(--text); font-weight: 600; }
|
|
5824
6045
|
.risk-low { color: var(--ok); }
|
|
5825
6046
|
.risk-med { color: var(--warn); }
|
|
5826
6047
|
.risk-high { color: var(--block); }
|
|
6048
|
+
${SNAPSHOT_OVERVIEW_STYLES}
|
|
5827
6049
|
${CHECKS_TABLE_STYLES}
|
|
5828
6050
|
</style>
|
|
5829
6051
|
</head>
|
|
5830
6052
|
<body>
|
|
5831
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>
|
|
5832
6061
|
<h2 class="h2">Checks</h2>
|
|
5833
|
-
<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>
|
|
5834
6062
|
<table class="results">
|
|
5835
6063
|
<thead><tr><th></th><th>Check</th><th>Status</th><th>#</th><th>Time</th></tr></thead>
|
|
5836
6064
|
<tbody>${checkRows}</tbody>
|
|
@@ -6185,14 +6413,32 @@ function buildReport(stack, pr, results, options) {
|
|
|
6185
6413
|
lines,
|
|
6186
6414
|
llmAppendix: options?.llmAppendix ?? null
|
|
6187
6415
|
}) : null;
|
|
6188
|
-
const
|
|
6416
|
+
const llmAppendix = options?.llmAppendix ?? null;
|
|
6417
|
+
const checksSnapshotInput = options?.emitChecksSnapshot === true ? {
|
|
6418
|
+
cwd,
|
|
6189
6419
|
riskScore,
|
|
6190
6420
|
mode,
|
|
6421
|
+
stack,
|
|
6422
|
+
pr,
|
|
6191
6423
|
results,
|
|
6192
6424
|
warns,
|
|
6193
6425
|
infos,
|
|
6194
|
-
blocks
|
|
6195
|
-
|
|
6426
|
+
blocks,
|
|
6427
|
+
lines,
|
|
6428
|
+
llmAppendix
|
|
6429
|
+
} : null;
|
|
6430
|
+
const checksSnapshotHtml = checksSnapshotInput != null ? buildChecksSnapshotHtml(checksSnapshotInput) : null;
|
|
6431
|
+
return {
|
|
6432
|
+
riskScore,
|
|
6433
|
+
stack,
|
|
6434
|
+
pr,
|
|
6435
|
+
results,
|
|
6436
|
+
markdown,
|
|
6437
|
+
consoleText,
|
|
6438
|
+
html,
|
|
6439
|
+
checksSnapshotHtml,
|
|
6440
|
+
checksSnapshotInput
|
|
6441
|
+
};
|
|
6196
6442
|
}
|
|
6197
6443
|
function scoreRisk(blocks, warns, lines, files) {
|
|
6198
6444
|
let score = 0;
|
|
@@ -6240,7 +6486,7 @@ function countShieldColor(kind, n3) {
|
|
|
6240
6486
|
if (n3 <= 10) return "yellow";
|
|
6241
6487
|
return "orange";
|
|
6242
6488
|
}
|
|
6243
|
-
function
|
|
6489
|
+
function formatDuration3(ms) {
|
|
6244
6490
|
if (ms < 1e3) return `${ms} ms`;
|
|
6245
6491
|
const s3 = Math.round(ms / 1e3);
|
|
6246
6492
|
if (s3 < 60) return `${s3}s`;
|
|
@@ -6395,7 +6641,7 @@ function formatMarkdown(p2) {
|
|
|
6395
6641
|
}
|
|
6396
6642
|
const nFind = r4.skipped ? "\u2014" : String(r4.findings.length);
|
|
6397
6643
|
sb.push(
|
|
6398
|
-
`| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${
|
|
6644
|
+
`| ${he2} | **${r4.checkId}** | ${status} | **${nFind}** | ${formatDuration3(r4.durationMs)} |`
|
|
6399
6645
|
);
|
|
6400
6646
|
}
|
|
6401
6647
|
sb.push("");
|
|
@@ -6960,36 +7206,24 @@ async function runFrontGuard(opts) {
|
|
|
6960
7206
|
await fs.writeFile(opts.htmlOut, report.html, "utf8");
|
|
6961
7207
|
}
|
|
6962
7208
|
let embedPngPath = null;
|
|
6963
|
-
|
|
6964
|
-
let htmlForPng;
|
|
6965
|
-
if ((opts.checksSnapshotOut || opts.checksPngOut) && report.checksSnapshotHtml) {
|
|
7209
|
+
if ((opts.checksSnapshotOut || opts.checksPngOut) && report.checksSnapshotHtml && report.checksSnapshotInput) {
|
|
6966
7210
|
if (opts.checksSnapshotOut) {
|
|
6967
7211
|
const snapPath = path5.isAbsolute(opts.checksSnapshotOut) ? opts.checksSnapshotOut : path5.join(opts.cwd, opts.checksSnapshotOut);
|
|
6968
7212
|
await fs.writeFile(snapPath, report.checksSnapshotHtml, "utf8");
|
|
6969
|
-
htmlForPng = snapPath;
|
|
6970
7213
|
g.stderr.write(`
|
|
6971
7214
|
FrontGuard: wrote checks snapshot HTML to ${snapPath}
|
|
6972
7215
|
`);
|
|
6973
7216
|
}
|
|
6974
7217
|
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
7218
|
const pngAbs = path5.isAbsolute(opts.checksPngOut) ? opts.checksPngOut : path5.join(opts.cwd, opts.checksPngOut);
|
|
6981
|
-
await
|
|
7219
|
+
await renderChecksSnapshotPng(pngAbs, report.checksSnapshotInput);
|
|
6982
7220
|
embedPngPath = pngAbs;
|
|
6983
|
-
g.stderr.write(`FrontGuard: wrote checks PNG to ${pngAbs}
|
|
7221
|
+
g.stderr.write(`FrontGuard: wrote checks PNG to ${pngAbs}.
|
|
6984
7222
|
|
|
6985
7223
|
`);
|
|
6986
|
-
|
|
6987
|
-
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
6988
|
-
tmpDir = null;
|
|
6989
|
-
}
|
|
6990
|
-
} else if (opts.checksSnapshotOut && htmlForPng) {
|
|
7224
|
+
} else if (opts.checksSnapshotOut) {
|
|
6991
7225
|
g.stderr.write(
|
|
6992
|
-
` Tip: add --checksPngOut checks.png to render
|
|
7226
|
+
` Tip: add --checksPngOut checks.png to render the checks table to a PNG (no browser).
|
|
6993
7227
|
|
|
6994
7228
|
`
|
|
6995
7229
|
);
|
|
@@ -7067,7 +7301,7 @@ var run = defineCommand({
|
|
|
7067
7301
|
},
|
|
7068
7302
|
checksPngOut: {
|
|
7069
7303
|
type: "string",
|
|
7070
|
-
description: "Write PNG of the checks table
|
|
7304
|
+
description: "Write PNG of the checks table (SVG raster; @resvg/resvg-js, no browser). Pair with --checksSnapshotOut or use alone"
|
|
7071
7305
|
},
|
|
7072
7306
|
prCommentOut: {
|
|
7073
7307
|
type: "string",
|