@cleartrip/frontguard 0.1.4 → 0.1.6
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 +350 -95
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import g, { stdin, stdout, cwd } from 'process';
|
|
|
4
4
|
import f from 'readline';
|
|
5
5
|
import * as tty from 'tty';
|
|
6
6
|
import { WriteStream } from 'tty';
|
|
7
|
-
import
|
|
7
|
+
import path4, { sep, normalize, delimiter, resolve, dirname } from 'path';
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import { createRequire } from 'module';
|
|
10
10
|
import fs4 from 'fs';
|
|
@@ -2497,19 +2497,19 @@ async function ensureDir(dir) {
|
|
|
2497
2497
|
await fs.mkdir(dir, { recursive: true });
|
|
2498
2498
|
}
|
|
2499
2499
|
async function initFrontGuard(cwd) {
|
|
2500
|
-
const gh =
|
|
2500
|
+
const gh = path4.join(cwd, ".github", "workflows");
|
|
2501
2501
|
await ensureDir(gh);
|
|
2502
|
-
const wfPath =
|
|
2502
|
+
const wfPath = path4.join(gh, "frontguard.yml");
|
|
2503
2503
|
await fs.writeFile(wfPath, WORKFLOW, "utf8");
|
|
2504
|
-
const cfgPath =
|
|
2504
|
+
const cfgPath = path4.join(cwd, "frontguard.config.js");
|
|
2505
2505
|
try {
|
|
2506
2506
|
await fs.access(cfgPath);
|
|
2507
2507
|
} catch {
|
|
2508
2508
|
await fs.writeFile(cfgPath, CONFIG, "utf8");
|
|
2509
2509
|
}
|
|
2510
|
-
const tplRoot =
|
|
2510
|
+
const tplRoot = path4.join(cwd, ".github");
|
|
2511
2511
|
await ensureDir(tplRoot);
|
|
2512
|
-
const tplPath =
|
|
2512
|
+
const tplPath = path4.join(tplRoot, "pull_request_template.md");
|
|
2513
2513
|
try {
|
|
2514
2514
|
await fs.access(tplPath);
|
|
2515
2515
|
} catch {
|
|
@@ -2614,10 +2614,10 @@ async function resolvePrNumber() {
|
|
|
2614
2614
|
const raw = process.env.FRONTGUARD_PR_NUMBER ?? process.env.PR_NUMBER;
|
|
2615
2615
|
const n3 = Number(raw);
|
|
2616
2616
|
if (Number.isFinite(n3) && n3 > 0) return n3;
|
|
2617
|
-
const
|
|
2618
|
-
if (!
|
|
2617
|
+
const path15 = process.env.GITHUB_EVENT_PATH;
|
|
2618
|
+
if (!path15) return null;
|
|
2619
2619
|
try {
|
|
2620
|
-
const payload = JSON.parse(await fs.readFile(
|
|
2620
|
+
const payload = JSON.parse(await fs.readFile(path15, "utf8"));
|
|
2621
2621
|
const num = payload.pull_request?.number;
|
|
2622
2622
|
return typeof num === "number" && num > 0 ? num : null;
|
|
2623
2623
|
} catch {
|
|
@@ -2824,7 +2824,7 @@ function stripExtends(c4) {
|
|
|
2824
2824
|
}
|
|
2825
2825
|
async function loadExtendsLayer(cwd, spec) {
|
|
2826
2826
|
if (!spec) return {};
|
|
2827
|
-
const req = createRequire(
|
|
2827
|
+
const req = createRequire(path4.join(cwd, "package.json"));
|
|
2828
2828
|
const specs = Array.isArray(spec) ? spec : [spec];
|
|
2829
2829
|
let merged = {};
|
|
2830
2830
|
for (const s3 of specs) {
|
|
@@ -2843,7 +2843,7 @@ async function loadExtendsLayer(cwd, spec) {
|
|
|
2843
2843
|
async function loadConfig(cwd) {
|
|
2844
2844
|
let userFile = null;
|
|
2845
2845
|
for (const name of CONFIG_NAMES) {
|
|
2846
|
-
const full =
|
|
2846
|
+
const full = path4.join(cwd, name);
|
|
2847
2847
|
if (!fs4.existsSync(full)) continue;
|
|
2848
2848
|
try {
|
|
2849
2849
|
const mod = await importConfig(full);
|
|
@@ -2873,7 +2873,7 @@ function hasDep(deps, name) {
|
|
|
2873
2873
|
async function detectStack(cwd) {
|
|
2874
2874
|
let pkg = {};
|
|
2875
2875
|
try {
|
|
2876
|
-
const raw = await fs.readFile(
|
|
2876
|
+
const raw = await fs.readFile(path4.join(cwd, "package.json"), "utf8");
|
|
2877
2877
|
pkg = JSON.parse(raw);
|
|
2878
2878
|
} catch {
|
|
2879
2879
|
return {
|
|
@@ -2893,7 +2893,7 @@ async function detectStack(cwd) {
|
|
|
2893
2893
|
const isMonorepo = Boolean(pkg.workspaces);
|
|
2894
2894
|
let tsStrict = null;
|
|
2895
2895
|
try {
|
|
2896
|
-
const tsconfigPath =
|
|
2896
|
+
const tsconfigPath = path4.join(cwd, "tsconfig.json");
|
|
2897
2897
|
const tsRaw = await fs.readFile(tsconfigPath, "utf8");
|
|
2898
2898
|
const ts = JSON.parse(tsRaw);
|
|
2899
2899
|
if (typeof ts.compilerOptions?.strict === "boolean") {
|
|
@@ -2903,15 +2903,15 @@ async function detectStack(cwd) {
|
|
|
2903
2903
|
}
|
|
2904
2904
|
let pm = "unknown";
|
|
2905
2905
|
try {
|
|
2906
|
-
await fs.access(
|
|
2906
|
+
await fs.access(path4.join(cwd, "pnpm-lock.yaml"));
|
|
2907
2907
|
pm = "pnpm";
|
|
2908
2908
|
} catch {
|
|
2909
2909
|
try {
|
|
2910
|
-
await fs.access(
|
|
2910
|
+
await fs.access(path4.join(cwd, "yarn.lock"));
|
|
2911
2911
|
pm = "yarn";
|
|
2912
2912
|
} catch {
|
|
2913
2913
|
try {
|
|
2914
|
-
await fs.access(
|
|
2914
|
+
await fs.access(path4.join(cwd, "package-lock.json"));
|
|
2915
2915
|
pm = "npm";
|
|
2916
2916
|
} catch {
|
|
2917
2917
|
pm = "npm";
|
|
@@ -2931,6 +2931,51 @@ async function detectStack(cwd) {
|
|
|
2931
2931
|
tsStrict
|
|
2932
2932
|
};
|
|
2933
2933
|
}
|
|
2934
|
+
function stripFileUrl(p2) {
|
|
2935
|
+
let s3 = p2.trim();
|
|
2936
|
+
if (!/^file:/i.test(s3)) return s3;
|
|
2937
|
+
s3 = s3.replace(/^file:\/\//i, "");
|
|
2938
|
+
if (process.platform === "win32" && s3.startsWith("/")) {
|
|
2939
|
+
const m3 = s3.match(/^\/([A-Za-z]:\/.*)$/);
|
|
2940
|
+
if (m3) return m3[1];
|
|
2941
|
+
}
|
|
2942
|
+
return s3;
|
|
2943
|
+
}
|
|
2944
|
+
function isUnderDir(parent, child) {
|
|
2945
|
+
const rel = path4.relative(parent, child);
|
|
2946
|
+
return rel === "" || !rel.startsWith("..") && !path4.isAbsolute(rel);
|
|
2947
|
+
}
|
|
2948
|
+
function toRepoRelativePath(cwd, filePath) {
|
|
2949
|
+
if (!filePath?.trim()) return void 0;
|
|
2950
|
+
const raw = stripFileUrl(filePath);
|
|
2951
|
+
const resolvedCwd = path4.resolve(cwd);
|
|
2952
|
+
const absFile = path4.isAbsolute(raw) ? path4.resolve(raw) : path4.resolve(resolvedCwd, raw);
|
|
2953
|
+
if (!isUnderDir(resolvedCwd, absFile)) {
|
|
2954
|
+
return raw.split(/[/\\]/g).join("/");
|
|
2955
|
+
}
|
|
2956
|
+
let rel = path4.relative(resolvedCwd, absFile);
|
|
2957
|
+
if (!rel || rel === ".") {
|
|
2958
|
+
return path4.basename(absFile);
|
|
2959
|
+
}
|
|
2960
|
+
return rel.split(path4.sep).join("/");
|
|
2961
|
+
}
|
|
2962
|
+
function stripRepoAbsolutePaths(cwd, text) {
|
|
2963
|
+
if (!text || !cwd.trim()) return text;
|
|
2964
|
+
const resolvedCwd = path4.resolve(cwd);
|
|
2965
|
+
const asPosix = (s3) => s3.replace(/\\/g, "/");
|
|
2966
|
+
const cwdPosix = asPosix(resolvedCwd);
|
|
2967
|
+
let out = asPosix(text);
|
|
2968
|
+
const prefixes = [.../* @__PURE__ */ new Set([cwdPosix + "/", resolvedCwd + path4.sep])].filter(
|
|
2969
|
+
(p2) => p2.length > 1
|
|
2970
|
+
);
|
|
2971
|
+
for (const prefix of prefixes) {
|
|
2972
|
+
const norm = prefix.replace(/\\/g, "/");
|
|
2973
|
+
if (out.includes(norm)) {
|
|
2974
|
+
out = out.split(norm).join("");
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return out;
|
|
2978
|
+
}
|
|
2934
2979
|
var d2 = (e3, t3) => () => (t3 || e3((t3 = { exports: {} }).exports, t3), t3.exports);
|
|
2935
2980
|
var f3 = /* @__PURE__ */ createRequire(import.meta.url);
|
|
2936
2981
|
var p = /^path$/i;
|
|
@@ -3546,7 +3591,7 @@ async function pathExists(file) {
|
|
|
3546
3591
|
}
|
|
3547
3592
|
}
|
|
3548
3593
|
async function resolveBin(cwd, name) {
|
|
3549
|
-
const local =
|
|
3594
|
+
const local = path4.join(cwd, "node_modules", ".bin", name);
|
|
3550
3595
|
if (await pathExists(local)) return local;
|
|
3551
3596
|
const win = local + ".cmd";
|
|
3552
3597
|
if (await pathExists(win)) return win;
|
|
@@ -3602,7 +3647,7 @@ async function runNpx(cwd, args) {
|
|
|
3602
3647
|
// src/checks/eslint.ts
|
|
3603
3648
|
async function hasEslintDependency(cwd) {
|
|
3604
3649
|
try {
|
|
3605
|
-
const raw = await fs.readFile(
|
|
3650
|
+
const raw = await fs.readFile(path4.join(cwd, "package.json"), "utf8");
|
|
3606
3651
|
const p2 = JSON.parse(raw);
|
|
3607
3652
|
return Boolean(p2.devDependencies?.eslint || p2.dependencies?.eslint);
|
|
3608
3653
|
} catch {
|
|
@@ -3621,7 +3666,7 @@ async function hasEslintConfig(cwd) {
|
|
|
3621
3666
|
".eslintrc.yml"
|
|
3622
3667
|
];
|
|
3623
3668
|
for (const c4 of candidates) {
|
|
3624
|
-
if (await pathExists(
|
|
3669
|
+
if (await pathExists(path4.join(cwd, c4))) return true;
|
|
3625
3670
|
}
|
|
3626
3671
|
return false;
|
|
3627
3672
|
}
|
|
@@ -3677,20 +3722,19 @@ async function runEslint(cwd, config, _stack) {
|
|
|
3677
3722
|
}
|
|
3678
3723
|
try {
|
|
3679
3724
|
const rows = JSON.parse(stdout2);
|
|
3680
|
-
let n3 = 0;
|
|
3681
3725
|
for (const row of rows) {
|
|
3726
|
+
const relFile = toRepoRelativePath(cwd, row.filePath);
|
|
3682
3727
|
for (const m3 of row.messages) {
|
|
3683
|
-
if (n3++ >= 40) break;
|
|
3684
3728
|
findings.push({
|
|
3685
3729
|
id: `eslint-${m3.ruleId ?? "unknown"}`,
|
|
3686
3730
|
severity: "warn",
|
|
3687
3731
|
message: m3.message,
|
|
3688
|
-
file:
|
|
3732
|
+
file: relFile,
|
|
3689
3733
|
detail: m3.line ? `line ${m3.line}` : void 0
|
|
3690
3734
|
});
|
|
3691
3735
|
}
|
|
3692
3736
|
}
|
|
3693
|
-
if (
|
|
3737
|
+
if (findings.length === 0) {
|
|
3694
3738
|
findings.push({
|
|
3695
3739
|
id: "eslint-failed",
|
|
3696
3740
|
severity: "warn",
|
|
@@ -3778,7 +3822,7 @@ async function runTypeScript(cwd, config, stack) {
|
|
|
3778
3822
|
skipped: "disabled in config"
|
|
3779
3823
|
};
|
|
3780
3824
|
}
|
|
3781
|
-
const hasTs = stack.hasTypeScript || await pathExists(
|
|
3825
|
+
const hasTs = stack.hasTypeScript || await pathExists(path4.join(cwd, "tsconfig.json"));
|
|
3782
3826
|
if (!hasTs) {
|
|
3783
3827
|
return {
|
|
3784
3828
|
checkId: "typescript",
|
|
@@ -3891,7 +3935,7 @@ async function runSecrets(cwd, config, pr) {
|
|
|
3891
3935
|
});
|
|
3892
3936
|
break;
|
|
3893
3937
|
}
|
|
3894
|
-
const full =
|
|
3938
|
+
const full = path4.join(cwd, rel);
|
|
3895
3939
|
let content;
|
|
3896
3940
|
try {
|
|
3897
3941
|
content = await fs.readFile(full, "utf8");
|
|
@@ -3917,7 +3961,7 @@ async function runSecrets(cwd, config, pr) {
|
|
|
3917
3961
|
};
|
|
3918
3962
|
}
|
|
3919
3963
|
function isProbablyTextFile(rel) {
|
|
3920
|
-
const ext =
|
|
3964
|
+
const ext = path4.extname(rel).toLowerCase();
|
|
3921
3965
|
return TEXT_EXT.has(ext);
|
|
3922
3966
|
}
|
|
3923
3967
|
|
|
@@ -4210,12 +4254,12 @@ async function runCycles(cwd, config, stack) {
|
|
|
4210
4254
|
}
|
|
4211
4255
|
let entry = cfg.entries[0] ?? "src";
|
|
4212
4256
|
for (const e3 of cfg.entries) {
|
|
4213
|
-
if (await pathExists(
|
|
4257
|
+
if (await pathExists(path4.join(cwd, e3))) {
|
|
4214
4258
|
entry = e3;
|
|
4215
4259
|
break;
|
|
4216
4260
|
}
|
|
4217
4261
|
}
|
|
4218
|
-
if (!await pathExists(
|
|
4262
|
+
if (!await pathExists(path4.join(cwd, entry))) {
|
|
4219
4263
|
return {
|
|
4220
4264
|
checkId: "cycles",
|
|
4221
4265
|
findings: [],
|
|
@@ -4339,7 +4383,7 @@ async function sumGlobBytes(cwd, patterns) {
|
|
|
4339
4383
|
});
|
|
4340
4384
|
for (const rel of files) {
|
|
4341
4385
|
try {
|
|
4342
|
-
const st = await fs.stat(
|
|
4386
|
+
const st = await fs.stat(path4.join(cwd, rel));
|
|
4343
4387
|
total += st.size;
|
|
4344
4388
|
} catch {
|
|
4345
4389
|
}
|
|
@@ -4348,7 +4392,7 @@ async function sumGlobBytes(cwd, patterns) {
|
|
|
4348
4392
|
return total;
|
|
4349
4393
|
}
|
|
4350
4394
|
async function readBaseline(cwd, relPath, baseRef) {
|
|
4351
|
-
const disk =
|
|
4395
|
+
const disk = path4.join(cwd, relPath);
|
|
4352
4396
|
try {
|
|
4353
4397
|
const raw = await fs.readFile(disk, "utf8");
|
|
4354
4398
|
return JSON.parse(raw);
|
|
@@ -4523,7 +4567,7 @@ async function runCwv(cwd, config, stack, pr) {
|
|
|
4523
4567
|
const findings = [];
|
|
4524
4568
|
const sev2 = gateSeverity5(cfg.gate);
|
|
4525
4569
|
for (const rel of toScan.slice(0, 400)) {
|
|
4526
|
-
const full =
|
|
4570
|
+
const full = path4.join(cwd, rel);
|
|
4527
4571
|
let text;
|
|
4528
4572
|
try {
|
|
4529
4573
|
text = await fs.readFile(full, "utf8");
|
|
@@ -4608,7 +4652,7 @@ async function runCustomRules(cwd, config, restrictToFiles) {
|
|
|
4608
4652
|
});
|
|
4609
4653
|
break;
|
|
4610
4654
|
}
|
|
4611
|
-
const full =
|
|
4655
|
+
const full = path4.join(cwd, rel);
|
|
4612
4656
|
let content;
|
|
4613
4657
|
try {
|
|
4614
4658
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4739,7 +4783,7 @@ async function runAiAssistedStrict(cwd, config, pr) {
|
|
|
4739
4783
|
const gate = cfg.gate;
|
|
4740
4784
|
const findings = [];
|
|
4741
4785
|
for (const rel of files) {
|
|
4742
|
-
const full =
|
|
4786
|
+
const full = path4.join(cwd, rel);
|
|
4743
4787
|
let content;
|
|
4744
4788
|
try {
|
|
4745
4789
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4799,6 +4843,7 @@ function applyAiAssistedEscalation(results, pr, config) {
|
|
|
4799
4843
|
var import_picocolors = __toESM(require_picocolors());
|
|
4800
4844
|
function buildReport(stack, pr, results, options) {
|
|
4801
4845
|
const mode = options?.mode ?? "warn";
|
|
4846
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
4802
4847
|
const allFindings = results.flatMap(
|
|
4803
4848
|
(r4) => r4.findings.map((f4) => ({ ...f4, checkId: r4.checkId }))
|
|
4804
4849
|
);
|
|
@@ -4808,6 +4853,7 @@ function buildReport(stack, pr, results, options) {
|
|
|
4808
4853
|
const lines = pr != null ? pr.additions + pr.deletions : null;
|
|
4809
4854
|
const riskScore = scoreRisk(blocks, warns, lines, pr?.changedFiles ?? 0);
|
|
4810
4855
|
const markdown = formatMarkdown({
|
|
4856
|
+
cwd,
|
|
4811
4857
|
riskScore,
|
|
4812
4858
|
mode,
|
|
4813
4859
|
stack,
|
|
@@ -4855,8 +4901,107 @@ function scoreRisk(blocks, warns, lines, files) {
|
|
|
4855
4901
|
if (score >= 2) return "MEDIUM";
|
|
4856
4902
|
return "LOW";
|
|
4857
4903
|
}
|
|
4904
|
+
function shieldImageUrl(label, message, color) {
|
|
4905
|
+
const q2 = new URLSearchParams({
|
|
4906
|
+
label,
|
|
4907
|
+
message,
|
|
4908
|
+
color,
|
|
4909
|
+
style: "for-the-badge"
|
|
4910
|
+
});
|
|
4911
|
+
return `https://img.shields.io/static/v1?${q2}`;
|
|
4912
|
+
}
|
|
4913
|
+
function mdShield(alt, label, message, color) {
|
|
4914
|
+
return `})`;
|
|
4915
|
+
}
|
|
4916
|
+
function riskEmoji(risk) {
|
|
4917
|
+
if (risk === "LOW") return "\u{1F7E2}";
|
|
4918
|
+
if (risk === "MEDIUM") return "\u{1F7E0}";
|
|
4919
|
+
return "\u{1F534}";
|
|
4920
|
+
}
|
|
4921
|
+
function riskShieldColor(risk) {
|
|
4922
|
+
if (risk === "LOW") return "brightgreen";
|
|
4923
|
+
if (risk === "MEDIUM") return "orange";
|
|
4924
|
+
return "red";
|
|
4925
|
+
}
|
|
4926
|
+
function modeShieldColor(mode) {
|
|
4927
|
+
return mode === "enforce" ? "critical" : "blue";
|
|
4928
|
+
}
|
|
4929
|
+
function countShieldColor(kind, n3) {
|
|
4930
|
+
if (kind === "block") return n3 === 0 ? "brightgreen" : "critical";
|
|
4931
|
+
if (kind === "info") return n3 === 0 ? "inactive" : "informational";
|
|
4932
|
+
if (n3 === 0) return "brightgreen";
|
|
4933
|
+
if (n3 <= 10) return "yellow";
|
|
4934
|
+
return "orange";
|
|
4935
|
+
}
|
|
4936
|
+
function formatDuration(ms) {
|
|
4937
|
+
if (ms < 1e3) return `${ms} ms`;
|
|
4938
|
+
const s3 = Math.round(ms / 1e3);
|
|
4939
|
+
if (s3 < 60) return `${s3}s`;
|
|
4940
|
+
const m3 = Math.floor(s3 / 60);
|
|
4941
|
+
const r4 = s3 % 60;
|
|
4942
|
+
return r4 ? `${m3}m ${r4}s` : `${m3}m`;
|
|
4943
|
+
}
|
|
4944
|
+
function healthEmojiForCheck(r4) {
|
|
4945
|
+
if (r4.skipped) return "\u23ED\uFE0F";
|
|
4946
|
+
if (r4.findings.length === 0) return "\u{1F7E2}";
|
|
4947
|
+
const hasBlock = r4.findings.some((f4) => f4.severity === "block");
|
|
4948
|
+
if (hasBlock) return "\u{1F534}";
|
|
4949
|
+
return "\u{1F7E1}";
|
|
4950
|
+
}
|
|
4951
|
+
function escapeHtml(s3) {
|
|
4952
|
+
return s3.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
4953
|
+
}
|
|
4954
|
+
function normalizeFindingDisplay(cwd, f4) {
|
|
4955
|
+
const file = toRepoRelativePath(cwd, f4.file);
|
|
4956
|
+
const message = stripRepoAbsolutePaths(cwd, f4.message);
|
|
4957
|
+
const detail = f4.detail ? stripRepoAbsolutePaths(cwd, f4.detail) : void 0;
|
|
4958
|
+
return { file, message, detail };
|
|
4959
|
+
}
|
|
4960
|
+
function sanitizeFence(cwd, s3) {
|
|
4961
|
+
return stripRepoAbsolutePaths(cwd, s3).replace(/\r\n/g, "\n").replace(/```/g, "`\u200B``");
|
|
4962
|
+
}
|
|
4963
|
+
function accordionSummaryHtml(file, message) {
|
|
4964
|
+
const msg = message.length > 160 ? `${message.slice(0, 157).trimEnd()}\u2026` : message;
|
|
4965
|
+
const filePart = file ? `<code>${escapeHtml(file)}</code> \xB7 ` : "";
|
|
4966
|
+
return filePart + escapeHtml(msg);
|
|
4967
|
+
}
|
|
4968
|
+
function parseLineHint(detail) {
|
|
4969
|
+
if (!detail) return 0;
|
|
4970
|
+
const m3 = /^line\s+(\d+)/i.exec(detail.trim());
|
|
4971
|
+
return m3 ? Number(m3[1]) : 0;
|
|
4972
|
+
}
|
|
4973
|
+
function appendDetailAfterTable(sb, cwd, detail) {
|
|
4974
|
+
if (!detail?.trim()) return;
|
|
4975
|
+
const d3 = detail.trim();
|
|
4976
|
+
const oneLine = !d3.includes("\n");
|
|
4977
|
+
const safeForTable = oneLine && d3.length <= 200 && !d3.includes("|") && !d3.includes("`");
|
|
4978
|
+
if (safeForTable) {
|
|
4979
|
+
sb.push(`| **Hint** | ${d3} |`);
|
|
4980
|
+
return;
|
|
4981
|
+
}
|
|
4982
|
+
sb.push("");
|
|
4983
|
+
sb.push("*Detail*");
|
|
4984
|
+
sb.push("");
|
|
4985
|
+
sb.push("```text");
|
|
4986
|
+
sb.push(sanitizeFence(cwd, d3));
|
|
4987
|
+
sb.push("```");
|
|
4988
|
+
}
|
|
4989
|
+
function appendDetailFree(sb, cwd, detail) {
|
|
4990
|
+
if (!detail?.trim()) return;
|
|
4991
|
+
const d3 = detail.trim();
|
|
4992
|
+
if (!d3.includes("\n") && d3.length <= 300) {
|
|
4993
|
+
sb.push("");
|
|
4994
|
+
sb.push(`_${d3}_`);
|
|
4995
|
+
return;
|
|
4996
|
+
}
|
|
4997
|
+
sb.push("");
|
|
4998
|
+
sb.push("```text");
|
|
4999
|
+
sb.push(sanitizeFence(cwd, d3));
|
|
5000
|
+
sb.push("```");
|
|
5001
|
+
}
|
|
4858
5002
|
function formatMarkdown(p2) {
|
|
4859
5003
|
const {
|
|
5004
|
+
cwd,
|
|
4860
5005
|
riskScore,
|
|
4861
5006
|
mode,
|
|
4862
5007
|
stack,
|
|
@@ -4869,99 +5014,205 @@ function formatMarkdown(p2) {
|
|
|
4869
5014
|
llmAppendix
|
|
4870
5015
|
} = p2;
|
|
4871
5016
|
const sb = [];
|
|
4872
|
-
|
|
5017
|
+
const sortWithCwd = (items) => [...items].sort((a3, b3) => {
|
|
5018
|
+
const af = toRepoRelativePath(cwd, a3.f.file) ?? "";
|
|
5019
|
+
const bf = toRepoRelativePath(cwd, b3.f.file) ?? "";
|
|
5020
|
+
if (af !== bf) return af.localeCompare(bf);
|
|
5021
|
+
const lineA = parseLineHint(a3.f.detail);
|
|
5022
|
+
const lineB = parseLineHint(b3.f.detail);
|
|
5023
|
+
if (lineA !== lineB) return lineA - lineB;
|
|
5024
|
+
return a3.f.message.localeCompare(b3.f.message);
|
|
5025
|
+
});
|
|
5026
|
+
sb.push("## \u2728 FrontGuard review brief");
|
|
5027
|
+
sb.push("");
|
|
5028
|
+
const modeLabel = mode === "enforce" ? "enforce" : "warn only";
|
|
5029
|
+
const badgeLine = [
|
|
5030
|
+
mdShield("Risk level", "risk", riskScore, riskShieldColor(riskScore)),
|
|
5031
|
+
mdShield("CI mode", "mode", modeLabel, modeShieldColor(mode)),
|
|
5032
|
+
mdShield("Blocking", "blocking", String(blocks), countShieldColor("block", blocks)),
|
|
5033
|
+
mdShield("Warnings", "warnings", String(warns), countShieldColor("warn", warns)),
|
|
5034
|
+
mdShield("Info", "info notes", String(infos), countShieldColor("info", infos))
|
|
5035
|
+
].join(" ");
|
|
5036
|
+
sb.push(badgeLine);
|
|
5037
|
+
sb.push("");
|
|
5038
|
+
sb.push("---");
|
|
4873
5039
|
sb.push("");
|
|
5040
|
+
sb.push("### \u{1F4CC} Snapshot");
|
|
5041
|
+
sb.push("");
|
|
5042
|
+
sb.push("| | |");
|
|
5043
|
+
sb.push("|:--|:--|");
|
|
5044
|
+
sb.push(`| **Composite risk** | ${riskEmoji(riskScore)} **${riskScore}** \u2014 heuristic from blocks, warnings, and PR size. |`);
|
|
4874
5045
|
sb.push(
|
|
4875
|
-
|
|
5046
|
+
`| **Gate mode** | ${mode === "enforce" ? "\u{1F512} **Enforce** \u2014 CI fails when a `block` finding is present." : "\u{1F6C8} **Warn only** \u2014 findings are advisory unless you use `--enforce`."} |`
|
|
4876
5047
|
);
|
|
4877
|
-
|
|
4878
|
-
|
|
5048
|
+
sb.push(`| **Stack detected** | ${formatStackOneLiner(stack)} |`);
|
|
5049
|
+
if (pr && lines != null) {
|
|
4879
5050
|
sb.push(
|
|
4880
|
-
|
|
5051
|
+
`| **PR size** | \u{1F4CF} **${lines}** LOC ( +${pr.additions} / \u2212${pr.deletions} ) \xB7 **${pr.changedFiles}** files |`
|
|
4881
5052
|
);
|
|
4882
5053
|
}
|
|
4883
5054
|
sb.push("");
|
|
4884
|
-
|
|
4885
|
-
if (pr && lines != null) {
|
|
5055
|
+
if (pr?.aiAssisted) {
|
|
4886
5056
|
sb.push(
|
|
4887
|
-
|
|
5057
|
+
"> **\u{1F916} AI-assisted PR** \u2014 Stricter static checks run on changed files (security / footguns; secrets & `any` deltas may escalate). This does not replace human review for behavior or product rules."
|
|
4888
5058
|
);
|
|
5059
|
+
sb.push("");
|
|
4889
5060
|
}
|
|
4890
|
-
sb.push("");
|
|
5061
|
+
sb.push("> **How to read this report**");
|
|
5062
|
+
sb.push(">");
|
|
5063
|
+
sb.push("> | Symbol | Meaning |");
|
|
5064
|
+
sb.push("> |:--|:--|");
|
|
5065
|
+
sb.push("> | \u{1F7E2} | Check passed or skipped cleanly |");
|
|
5066
|
+
sb.push("> | \u{1F7E1} | Warnings only \u2014 review recommended |");
|
|
5067
|
+
sb.push("> | \u{1F534} | Blocking (`block`) severity present |");
|
|
5068
|
+
sb.push("> | \u23ED\uFE0F | Check skipped (see reason in table) |");
|
|
5069
|
+
sb.push(">");
|
|
4891
5070
|
sb.push(
|
|
4892
|
-
|
|
5071
|
+
"> Paths in findings are **relative to the repo root**. Expand nested sections for rule id, file, and tool output."
|
|
4893
5072
|
);
|
|
4894
|
-
sb.push(`**Warnings:** ${warns} \xB7 **Info:** ${infos}`);
|
|
4895
5073
|
sb.push("");
|
|
4896
|
-
sb.push("
|
|
5074
|
+
sb.push("---");
|
|
5075
|
+
sb.push("");
|
|
5076
|
+
sb.push("### \u{1F4CB} Check results");
|
|
5077
|
+
sb.push("");
|
|
5078
|
+
sb.push("| | Check | Status | Findings | Duration |");
|
|
5079
|
+
sb.push("|:--:|:--|:--|:-:|--:|");
|
|
4897
5080
|
for (const r4 of results) {
|
|
4898
|
-
const
|
|
4899
|
-
|
|
5081
|
+
const he2 = healthEmojiForCheck(r4);
|
|
5082
|
+
const status = r4.skipped ? "\u23ED\uFE0F **Skipped**" : r4.findings.length === 0 ? "\u2705 **Clean**" : "\u26A0\uFE0F **Issues**";
|
|
5083
|
+
const nFind = r4.skipped ? "\u2014" : String(r4.findings.length);
|
|
5084
|
+
const note = r4.skipped ? `<br><sub>\u{1F4AC} ${escapeHtml(r4.skipped)}</sub>` : "";
|
|
5085
|
+
sb.push(
|
|
5086
|
+
`| ${he2} | **${r4.checkId}** | ${status}${note} | **${nFind}** | ${formatDuration(r4.durationMs)} |`
|
|
5087
|
+
);
|
|
4900
5088
|
}
|
|
4901
5089
|
sb.push("");
|
|
4902
|
-
const blockFindings =
|
|
4903
|
-
|
|
5090
|
+
const blockFindings = sortWithCwd(
|
|
5091
|
+
results.flatMap(
|
|
5092
|
+
(r4) => r4.findings.filter((f4) => f4.severity === "block").map((f4) => ({ r: r4, f: f4 }))
|
|
5093
|
+
)
|
|
4904
5094
|
);
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
\`\`\`text
|
|
4915
|
-
${f4.detail}
|
|
4916
|
-
\`\`\`
|
|
4917
|
-
|
|
4918
|
-
</details>`
|
|
4919
|
-
);
|
|
4920
|
-
}
|
|
4921
|
-
}
|
|
5095
|
+
sb.push("<details open>");
|
|
5096
|
+
sb.push(
|
|
5097
|
+
`<summary><strong>\u{1F6D1} Blocking \u2014 ${blocks} issue${blocks === 1 ? "" : "s"}</strong></summary>`
|
|
5098
|
+
);
|
|
5099
|
+
sb.push("");
|
|
5100
|
+
if (blockFindings.length === 0) {
|
|
5101
|
+
sb.push("*\u2705 No blocking findings \u2014 nothing mapped to severity `block`.*");
|
|
5102
|
+
} else {
|
|
4922
5103
|
sb.push("");
|
|
5104
|
+
for (const { r: r4, f: f4 } of blockFindings) {
|
|
5105
|
+
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
5106
|
+
sb.push("<details>");
|
|
5107
|
+
sb.push(
|
|
5108
|
+
`<summary>${accordionSummaryHtml(d3.file, d3.message)}</summary>`
|
|
5109
|
+
);
|
|
5110
|
+
sb.push("");
|
|
5111
|
+
sb.push(`| Field | Value |`);
|
|
5112
|
+
sb.push(`|:--|:--|`);
|
|
5113
|
+
sb.push(`| **Check** | \`${r4.checkId}\` |`);
|
|
5114
|
+
sb.push(`| **Rule / id** | \`${f4.id}\` |`);
|
|
5115
|
+
if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
|
|
5116
|
+
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
5117
|
+
sb.push("");
|
|
5118
|
+
sb.push("</details>");
|
|
5119
|
+
sb.push("");
|
|
5120
|
+
}
|
|
4923
5121
|
}
|
|
4924
|
-
|
|
4925
|
-
|
|
5122
|
+
sb.push("");
|
|
5123
|
+
sb.push("</details>");
|
|
5124
|
+
sb.push("");
|
|
5125
|
+
const warnFindings = sortWithCwd(
|
|
5126
|
+
results.flatMap(
|
|
5127
|
+
(r4) => r4.findings.filter((f4) => f4.severity === "warn").map((f4) => ({ r: r4, f: f4 }))
|
|
5128
|
+
)
|
|
4926
5129
|
);
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
5130
|
+
sb.push("<details open>");
|
|
5131
|
+
sb.push(
|
|
5132
|
+
`<summary><strong>\u26A0\uFE0F Warnings \u2014 ${warns} issue${warns === 1 ? "" : "s"}</strong> \xB7 grouped by check</summary>`
|
|
5133
|
+
);
|
|
5134
|
+
sb.push("");
|
|
5135
|
+
if (warnFindings.length === 0) {
|
|
5136
|
+
sb.push("*\u{1F389} No warnings \u2014 nice work.*");
|
|
5137
|
+
} else {
|
|
5138
|
+
const byCheck = /* @__PURE__ */ new Map();
|
|
5139
|
+
for (const item of warnFindings) {
|
|
5140
|
+
const list = byCheck.get(item.r.checkId) ?? [];
|
|
5141
|
+
list.push(item);
|
|
5142
|
+
byCheck.set(item.r.checkId, list);
|
|
5143
|
+
}
|
|
5144
|
+
const checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
|
|
5145
|
+
for (const checkId of checkOrder) {
|
|
5146
|
+
const group = sortWithCwd(byCheck.get(checkId));
|
|
5147
|
+
sb.push(`#### \u{1F4C2} \`${checkId}\` \xB7 ${group.length} finding${group.length === 1 ? "" : "s"}`);
|
|
5148
|
+
sb.push("");
|
|
5149
|
+
for (const { r: r4, f: f4 } of group) {
|
|
5150
|
+
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
5151
|
+
sb.push("<details>");
|
|
4933
5152
|
sb.push(
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
\`\`\`text
|
|
4937
|
-
${f4.detail}
|
|
4938
|
-
\`\`\`
|
|
4939
|
-
|
|
4940
|
-
</details>`
|
|
5153
|
+
`<summary>${accordionSummaryHtml(d3.file, d3.message)}</summary>`
|
|
4941
5154
|
);
|
|
5155
|
+
sb.push("");
|
|
5156
|
+
sb.push(`| Field | Value |`);
|
|
5157
|
+
sb.push(`|:--|:--|`);
|
|
5158
|
+
sb.push(`| **Check** | \`${r4.checkId}\` |`);
|
|
5159
|
+
sb.push(`| **Rule / id** | \`${f4.id}\` |`);
|
|
5160
|
+
if (d3.file) sb.push(`| **File** | \`${d3.file}\` |`);
|
|
5161
|
+
appendDetailAfterTable(sb, cwd, d3.detail);
|
|
5162
|
+
sb.push("");
|
|
5163
|
+
sb.push("</details>");
|
|
5164
|
+
sb.push("");
|
|
4942
5165
|
}
|
|
4943
5166
|
}
|
|
4944
|
-
if (warnFindings.length > 30) {
|
|
4945
|
-
sb.push(`- _\u2026and ${warnFindings.length - 30} more_`);
|
|
4946
|
-
}
|
|
4947
|
-
sb.push("");
|
|
4948
5167
|
}
|
|
4949
|
-
|
|
4950
|
-
|
|
5168
|
+
sb.push("</details>");
|
|
5169
|
+
sb.push("");
|
|
5170
|
+
const infoFindings = sortWithCwd(
|
|
5171
|
+
results.flatMap(
|
|
5172
|
+
(r4) => r4.findings.filter((f4) => f4.severity === "info").map((f4) => ({ r: r4, f: f4 }))
|
|
5173
|
+
)
|
|
4951
5174
|
);
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
5175
|
+
sb.push("<details>");
|
|
5176
|
+
sb.push(
|
|
5177
|
+
`<summary><strong>\u2139\uFE0F Info & notes \u2014 ${infos} item${infos === 1 ? "" : "s"}</strong></summary>`
|
|
5178
|
+
);
|
|
5179
|
+
sb.push("");
|
|
5180
|
+
if (infoFindings.length === 0) {
|
|
5181
|
+
sb.push("*No info-level notes.*");
|
|
5182
|
+
} else {
|
|
5183
|
+
for (const { r: r4, f: f4 } of infoFindings) {
|
|
5184
|
+
const d3 = normalizeFindingDisplay(cwd, f4);
|
|
5185
|
+
sb.push("<details>");
|
|
5186
|
+
sb.push(`<summary>${accordionSummaryHtml(d3.file, d3.message)}</summary>`);
|
|
5187
|
+
sb.push("");
|
|
5188
|
+
sb.push(`- **Check:** \`${r4.checkId}\` \xB7 **id:** \`${f4.id}\``);
|
|
5189
|
+
appendDetailFree(sb, cwd, d3.detail);
|
|
5190
|
+
sb.push("");
|
|
5191
|
+
sb.push("</details>");
|
|
5192
|
+
sb.push("");
|
|
4956
5193
|
}
|
|
4957
|
-
sb.push("");
|
|
4958
5194
|
}
|
|
5195
|
+
sb.push("");
|
|
5196
|
+
sb.push("</details>");
|
|
5197
|
+
sb.push("");
|
|
4959
5198
|
if (llmAppendix?.trim()) {
|
|
5199
|
+
sb.push("### \u{1F916} AI / manual appendix");
|
|
5200
|
+
sb.push("");
|
|
4960
5201
|
sb.push(llmAppendix.trim());
|
|
4961
5202
|
sb.push("");
|
|
4962
5203
|
}
|
|
4963
5204
|
sb.push("---");
|
|
4964
|
-
sb.push("
|
|
5205
|
+
sb.push("");
|
|
5206
|
+
sb.push(
|
|
5207
|
+
mdShield(
|
|
5208
|
+
"Generated by",
|
|
5209
|
+
"report",
|
|
5210
|
+
"FrontGuard",
|
|
5211
|
+
"blueviolet"
|
|
5212
|
+
)
|
|
5213
|
+
);
|
|
5214
|
+
sb.push("");
|
|
5215
|
+
sb.push("_Configure checks in `frontguard.config.js` \xB7 [Shields.io](https://shields.io) badges load at view time._");
|
|
4965
5216
|
return sb.join("\n");
|
|
4966
5217
|
}
|
|
4967
5218
|
function formatConsole(p2) {
|
|
@@ -5127,7 +5378,7 @@ async function loadManualAppendix(opts) {
|
|
|
5127
5378
|
const envFile = process.env.FRONTGUARD_MANUAL_APPENDIX_FILE?.trim();
|
|
5128
5379
|
const resolvedPath = filePath?.trim() || envFile;
|
|
5129
5380
|
if (resolvedPath) {
|
|
5130
|
-
const abs =
|
|
5381
|
+
const abs = path4.isAbsolute(resolvedPath) ? resolvedPath : path4.join(cwd, resolvedPath);
|
|
5131
5382
|
try {
|
|
5132
5383
|
let text = await fs.readFile(abs, "utf8");
|
|
5133
5384
|
if (text.length > MAX_CHARS) {
|
|
@@ -5217,7 +5468,11 @@ async function runFrontGuard(opts) {
|
|
|
5217
5468
|
results
|
|
5218
5469
|
});
|
|
5219
5470
|
const llmAppendix = [manualAppendix, automatedAppendix].filter(Boolean).join("\n\n") || null;
|
|
5220
|
-
const report = buildReport(stack, pr, results, {
|
|
5471
|
+
const report = buildReport(stack, pr, results, {
|
|
5472
|
+
mode,
|
|
5473
|
+
llmAppendix,
|
|
5474
|
+
cwd: opts.cwd
|
|
5475
|
+
});
|
|
5221
5476
|
if (opts.markdown) {
|
|
5222
5477
|
g.stdout.write(report.markdown + "\n");
|
|
5223
5478
|
} else {
|