@cleartrip/frontguard 0.1.9 → 0.2.1
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 +767 -429
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +41 -8
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/freekit-ci-setup.md +7 -0
package/dist/cli.js
CHANGED
|
@@ -4,12 +4,12 @@ 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 path5, { sep, normalize, delimiter, resolve, dirname } from 'path';
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
10
|
+
import { execFileSync, spawn } from 'child_process';
|
|
9
11
|
import { createRequire } from 'module';
|
|
10
|
-
import
|
|
11
|
-
import { pathToFileURL } from 'url';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
12
|
+
import fs2 from 'fs';
|
|
13
13
|
import { pipeline } from 'stream/promises';
|
|
14
14
|
import { PassThrough } from 'stream';
|
|
15
15
|
import fg from 'fast-glob';
|
|
@@ -2398,50 +2398,9 @@ async function runMain(cmd, opts = {}) {
|
|
|
2398
2398
|
process.exit(1);
|
|
2399
2399
|
}
|
|
2400
2400
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
pull_request:
|
|
2405
|
-
types: [opened, synchronize, reopened]
|
|
2406
|
-
|
|
2407
|
-
permissions:
|
|
2408
|
-
contents: read
|
|
2409
|
-
pull-requests: write
|
|
2410
|
-
|
|
2411
|
-
concurrency:
|
|
2412
|
-
group: frontguard-\${{ github.workflow }}-\${{ github.event.pull_request.number || github.ref }}
|
|
2413
|
-
cancel-in-progress: true
|
|
2414
|
-
|
|
2415
|
-
jobs:
|
|
2416
|
-
review-brief:
|
|
2417
|
-
runs-on: ubuntu-latest
|
|
2418
|
-
steps:
|
|
2419
|
-
- uses: actions/checkout@v4
|
|
2420
|
-
with:
|
|
2421
|
-
fetch-depth: 0
|
|
2422
|
-
|
|
2423
|
-
- uses: actions/setup-node@v4
|
|
2424
|
-
with:
|
|
2425
|
-
node-version: 20
|
|
2426
|
-
|
|
2427
|
-
- name: Install dependencies
|
|
2428
|
-
run: |
|
|
2429
|
-
if [ -f pnpm-lock.yaml ]; then
|
|
2430
|
-
corepack enable
|
|
2431
|
-
pnpm install --frozen-lockfile || pnpm install
|
|
2432
|
-
elif [ -f yarn.lock ]; then
|
|
2433
|
-
yarn install --frozen-lockfile || yarn install
|
|
2434
|
-
elif [ -f package-lock.json ]; then
|
|
2435
|
-
npm ci || npm install
|
|
2436
|
-
else
|
|
2437
|
-
npm install
|
|
2438
|
-
fi
|
|
2439
|
-
|
|
2440
|
-
- name: FrontGuard (Phase 1 \u2014 warn-only)
|
|
2441
|
-
run: npx @cleartrip/frontguard run --ci
|
|
2442
|
-
env:
|
|
2443
|
-
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
2444
|
-
`;
|
|
2401
|
+
function packageRoot() {
|
|
2402
|
+
return path5.resolve(path5.dirname(fileURLToPath(import.meta.url)), "..");
|
|
2403
|
+
}
|
|
2445
2404
|
var CONFIG = `import { defineConfig } from '@cleartrip/frontguard'
|
|
2446
2405
|
|
|
2447
2406
|
export default defineConfig({
|
|
@@ -2458,7 +2417,14 @@ export default defineConfig({
|
|
|
2458
2417
|
// },
|
|
2459
2418
|
|
|
2460
2419
|
// checks: {
|
|
2461
|
-
// bundle: { baselineRef: 'main', maxDeltaBytes: 50_000 },
|
|
2420
|
+
// bundle: { baselineRef: 'main', maxDeltaBytes: 50_000 },
|
|
2421
|
+
// coreWebVitals: { scanGlobs: ['src/**/*.{tsx,jsx}'] },
|
|
2422
|
+
// prSize: {
|
|
2423
|
+
// tiers: [
|
|
2424
|
+
// { minLines: 1000, severity: 'warn', message: 'Very large PR (\${lines} lines; \u2265 \${min})' },
|
|
2425
|
+
// { minLines: 500, severity: 'info', message: 'Consider splitting (\${lines} lines)' },
|
|
2426
|
+
// ],
|
|
2427
|
+
// },
|
|
2462
2428
|
// cycles: { enabled: true },
|
|
2463
2429
|
// deadCode: { enabled: true, gate: 'info' },
|
|
2464
2430
|
// // LLM: cloud keys in CI, or local Ollama (no API key) on dev/self-hosted runners:
|
|
@@ -2496,27 +2462,35 @@ If **Yes**, list tools and what they touched (helps reviewers run a stricter fir
|
|
|
2496
2462
|
## AI assistance (optional detail)
|
|
2497
2463
|
- [ ] I have reviewed every AI-suggested line for security, auth, and product correctness
|
|
2498
2464
|
`;
|
|
2499
|
-
async function ensureDir(dir) {
|
|
2500
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2501
|
-
}
|
|
2502
2465
|
async function initFrontGuard(cwd) {
|
|
2503
|
-
const
|
|
2504
|
-
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2507
|
-
|
|
2466
|
+
const root = packageRoot();
|
|
2467
|
+
const tplPath = path5.join(root, "templates", "bitbucket-pipelines.yml");
|
|
2468
|
+
const outPipeline = path5.join(cwd, "bitbucket-pipelines.frontguard.example.yml");
|
|
2469
|
+
try {
|
|
2470
|
+
await fs.access(outPipeline);
|
|
2471
|
+
} catch {
|
|
2472
|
+
try {
|
|
2473
|
+
const yml = await fs.readFile(tplPath, "utf8");
|
|
2474
|
+
await fs.writeFile(outPipeline, yml, "utf8");
|
|
2475
|
+
} catch {
|
|
2476
|
+
await fs.writeFile(
|
|
2477
|
+
outPipeline,
|
|
2478
|
+
"# Copy bitbucket-pipelines.yml from @cleartrip/frontguard/templates in node_modules\n",
|
|
2479
|
+
"utf8"
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
const cfgPath = path5.join(cwd, "frontguard.config.js");
|
|
2508
2484
|
try {
|
|
2509
2485
|
await fs.access(cfgPath);
|
|
2510
2486
|
} catch {
|
|
2511
2487
|
await fs.writeFile(cfgPath, CONFIG, "utf8");
|
|
2512
2488
|
}
|
|
2513
|
-
const
|
|
2514
|
-
await ensureDir(tplRoot);
|
|
2515
|
-
const tplPath = path4.join(tplRoot, "pull_request_template.md");
|
|
2489
|
+
const tplPr = path5.join(cwd, "pull_request_template.md");
|
|
2516
2490
|
try {
|
|
2517
|
-
await fs.access(
|
|
2491
|
+
await fs.access(tplPr);
|
|
2518
2492
|
} catch {
|
|
2519
|
-
await fs.writeFile(
|
|
2493
|
+
await fs.writeFile(tplPr, PR_TEMPLATE, "utf8");
|
|
2520
2494
|
}
|
|
2521
2495
|
}
|
|
2522
2496
|
|
|
@@ -2654,110 +2628,86 @@ function parseAiDisclosure(body) {
|
|
|
2654
2628
|
return { assisted, explicitNo, ambiguous };
|
|
2655
2629
|
}
|
|
2656
2630
|
|
|
2657
|
-
// src/ci/
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
headRef: pr.head?.ref ?? "",
|
|
2675
|
-
additions: pr.additions ?? 0,
|
|
2676
|
-
deletions: pr.deletions ?? 0,
|
|
2677
|
-
changedFiles: pr.changed_files ?? files.length,
|
|
2678
|
-
files,
|
|
2679
|
-
aiAssisted: ai.assisted,
|
|
2680
|
-
aiExplicitNo: ai.explicitNo,
|
|
2681
|
-
aiDisclosureAmbiguous: ai.ambiguous
|
|
2682
|
-
};
|
|
2683
|
-
} catch {
|
|
2684
|
-
return null;
|
|
2685
|
-
}
|
|
2686
|
-
}
|
|
2687
|
-
|
|
2688
|
-
// src/lib/http-fetch.ts
|
|
2689
|
-
function getFetch() {
|
|
2690
|
-
const f4 = globalThis.fetch;
|
|
2691
|
-
if (typeof f4 !== "function") {
|
|
2692
|
-
throw new Error(
|
|
2693
|
-
"FrontGuard needs Node.js 18+ (global fetch). Use NODE_VERSION / image >= 18 in CI."
|
|
2694
|
-
);
|
|
2695
|
-
}
|
|
2696
|
-
return f4;
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
// src/ci/pr-comment.ts
|
|
2700
|
-
var MARKER = "<!-- frontguard:brief -->";
|
|
2701
|
-
async function resolvePrNumber() {
|
|
2702
|
-
const raw = process.env.FRONTGUARD_PR_NUMBER ?? process.env.PR_NUMBER;
|
|
2703
|
-
const n3 = Number(raw);
|
|
2704
|
-
if (Number.isFinite(n3) && n3 > 0) return n3;
|
|
2705
|
-
const path17 = process.env.GITHUB_EVENT_PATH;
|
|
2706
|
-
if (!path17) return null;
|
|
2707
|
-
try {
|
|
2708
|
-
const payload = JSON.parse(await fs.readFile(path17, "utf8"));
|
|
2709
|
-
const num = payload.pull_request?.number;
|
|
2710
|
-
return typeof num === "number" && num > 0 ? num : null;
|
|
2711
|
-
} catch {
|
|
2712
|
-
return null;
|
|
2631
|
+
// src/ci/pr-context.ts
|
|
2632
|
+
function gitTrimmed(cwd, args) {
|
|
2633
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
2634
|
+
encoding: "utf8",
|
|
2635
|
+
maxBuffer: 20 * 1024 * 1024
|
|
2636
|
+
}).trimEnd();
|
|
2637
|
+
}
|
|
2638
|
+
function resolveCompareRef(cwd, destBranch) {
|
|
2639
|
+
const safe = destBranch.trim();
|
|
2640
|
+
if (!safe || safe.startsWith("-") || safe.includes("..")) return null;
|
|
2641
|
+
const candidates = [safe, `origin/${safe}`];
|
|
2642
|
+
for (const c4 of candidates) {
|
|
2643
|
+
try {
|
|
2644
|
+
gitTrimmed(cwd, ["rev-parse", "--verify", `${c4}^{commit}`]);
|
|
2645
|
+
return c4;
|
|
2646
|
+
} catch {
|
|
2647
|
+
}
|
|
2713
2648
|
}
|
|
2649
|
+
return null;
|
|
2714
2650
|
}
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2651
|
+
function parseNumstat(output) {
|
|
2652
|
+
let additions = 0;
|
|
2653
|
+
let deletions = 0;
|
|
2654
|
+
const files = [];
|
|
2655
|
+
for (const line of output.split("\n")) {
|
|
2656
|
+
const t3 = line.trim();
|
|
2657
|
+
if (!t3) continue;
|
|
2658
|
+
const tab = t3.indexOf(" ");
|
|
2659
|
+
if (tab < 0) continue;
|
|
2660
|
+
const tab2 = t3.indexOf(" ", tab + 1);
|
|
2661
|
+
if (tab2 < 0) continue;
|
|
2662
|
+
const aStr = t3.slice(0, tab);
|
|
2663
|
+
const dStr = t3.slice(tab + 1, tab2);
|
|
2664
|
+
const path18 = t3.slice(tab2 + 1);
|
|
2665
|
+
const a3 = aStr === "-" ? 0 : Number(aStr);
|
|
2666
|
+
const d3 = dStr === "-" ? 0 : Number(dStr);
|
|
2667
|
+
if (!Number.isFinite(a3) || !Number.isFinite(d3)) continue;
|
|
2668
|
+
additions += a3;
|
|
2669
|
+
deletions += d3;
|
|
2670
|
+
if (path18) files.push(path18);
|
|
2671
|
+
}
|
|
2672
|
+
return { additions, deletions, files };
|
|
2673
|
+
}
|
|
2674
|
+
function buildBitbucketPrContext(cwd) {
|
|
2675
|
+
const prId = process.env.BITBUCKET_PR_ID?.trim();
|
|
2676
|
+
if (!prId) return null;
|
|
2677
|
+
const dest = process.env.BITBUCKET_PR_DESTINATION_BRANCH?.trim() || "main";
|
|
2678
|
+
const body = process.env.FRONTGUARD_PR_BODY?.trim() ?? "";
|
|
2679
|
+
const headRef = process.env.BITBUCKET_PR_SOURCE_BRANCH?.trim() ?? "";
|
|
2680
|
+
const title = process.env.BITBUCKET_PR_TITLE?.trim() ?? "";
|
|
2681
|
+
let additions = 0;
|
|
2682
|
+
let deletions = 0;
|
|
2683
|
+
let files = [];
|
|
2684
|
+
const destRef = resolveCompareRef(cwd, dest);
|
|
2685
|
+
if (destRef) {
|
|
2686
|
+
try {
|
|
2687
|
+
const raw = gitTrimmed(cwd, ["diff", `${destRef}...HEAD`, "--numstat"]);
|
|
2688
|
+
const stat = parseNumstat(raw);
|
|
2689
|
+
additions = stat.additions;
|
|
2690
|
+
deletions = stat.deletions;
|
|
2691
|
+
files = stat.files;
|
|
2692
|
+
} catch {
|
|
2693
|
+
}
|
|
2726
2694
|
}
|
|
2727
|
-
const
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2695
|
+
const ai = parseAiDisclosure(body);
|
|
2696
|
+
return {
|
|
2697
|
+
number: Number.parseInt(prId, 10) || 0,
|
|
2698
|
+
title,
|
|
2699
|
+
body,
|
|
2700
|
+
baseRef: dest,
|
|
2701
|
+
headRef,
|
|
2702
|
+
additions,
|
|
2703
|
+
deletions,
|
|
2704
|
+
changedFiles: files.length,
|
|
2705
|
+
files,
|
|
2706
|
+
...ai
|
|
2733
2707
|
};
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
const fetch = getFetch();
|
|
2738
|
-
const listRes = await fetch(listUrl, { headers });
|
|
2739
|
-
if (!listRes.ok) {
|
|
2740
|
-
return;
|
|
2741
|
-
}
|
|
2742
|
-
const comments = await listRes.json();
|
|
2743
|
-
const existing = comments.find(
|
|
2744
|
-
(c4) => typeof c4.body === "string" && c4.body.includes(MARKER)
|
|
2745
|
-
);
|
|
2746
|
-
if (existing) {
|
|
2747
|
-
const patchUrl = `${apiBase}/repos/${owner}/${name}/issues/comments/${existing.id}`;
|
|
2748
|
-
await fetch(patchUrl, {
|
|
2749
|
-
method: "PATCH",
|
|
2750
|
-
headers,
|
|
2751
|
-
body: JSON.stringify({ body: prefixed })
|
|
2752
|
-
});
|
|
2753
|
-
return;
|
|
2754
|
-
}
|
|
2755
|
-
const postUrl = `${apiBase}/repos/${owner}/${name}/issues/${prNumber}/comments`;
|
|
2756
|
-
await fetch(postUrl, {
|
|
2757
|
-
method: "POST",
|
|
2758
|
-
headers,
|
|
2759
|
-
body: JSON.stringify({ body: prefixed })
|
|
2760
|
-
});
|
|
2708
|
+
}
|
|
2709
|
+
function readPrContext(cwd) {
|
|
2710
|
+
return buildBitbucketPrContext(cwd);
|
|
2761
2711
|
}
|
|
2762
2712
|
|
|
2763
2713
|
// node_modules/defu/dist/defu.mjs
|
|
@@ -2874,7 +2824,7 @@ var defaultConfig = {
|
|
|
2874
2824
|
maxDeltaBytes: null,
|
|
2875
2825
|
maxTotalBytes: null
|
|
2876
2826
|
},
|
|
2877
|
-
|
|
2827
|
+
coreWebVitals: {
|
|
2878
2828
|
enabled: true,
|
|
2879
2829
|
gate: "warn",
|
|
2880
2830
|
scanGlobs: ["app/**/*.{tsx,jsx}", "pages/**/*.{tsx,jsx}", "src/**/*.{tsx,jsx}"],
|
|
@@ -2895,6 +2845,16 @@ var defaultConfig = {
|
|
|
2895
2845
|
}
|
|
2896
2846
|
};
|
|
2897
2847
|
|
|
2848
|
+
// src/config/migrate.ts
|
|
2849
|
+
function migrateLegacyConfigKeys(config) {
|
|
2850
|
+
const ch = config.checks;
|
|
2851
|
+
if (!ch) return;
|
|
2852
|
+
if ("cwv" in ch && !("coreWebVitals" in ch)) {
|
|
2853
|
+
ch.coreWebVitals = ch.cwv;
|
|
2854
|
+
delete ch.cwv;
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2898
2858
|
// src/config/load.ts
|
|
2899
2859
|
var CONFIG_NAMES = [
|
|
2900
2860
|
"frontguard.config.js",
|
|
@@ -2917,7 +2877,7 @@ function stripExtends(c4) {
|
|
|
2917
2877
|
}
|
|
2918
2878
|
async function loadExtendsLayer(cwd, spec) {
|
|
2919
2879
|
if (!spec) return {};
|
|
2920
|
-
const req = createRequire(
|
|
2880
|
+
const req = createRequire(path5.join(cwd, "package.json"));
|
|
2921
2881
|
const specs = Array.isArray(spec) ? spec : [spec];
|
|
2922
2882
|
let merged = {};
|
|
2923
2883
|
for (const s3 of specs) {
|
|
@@ -2936,8 +2896,8 @@ async function loadExtendsLayer(cwd, spec) {
|
|
|
2936
2896
|
async function loadConfig(cwd) {
|
|
2937
2897
|
let userFile = null;
|
|
2938
2898
|
for (const name of CONFIG_NAMES) {
|
|
2939
|
-
const full =
|
|
2940
|
-
if (!
|
|
2899
|
+
const full = path5.join(cwd, name);
|
|
2900
|
+
if (!fs2.existsSync(full)) continue;
|
|
2941
2901
|
try {
|
|
2942
2902
|
const mod = await importConfig(full);
|
|
2943
2903
|
userFile = normalizeExport(mod);
|
|
@@ -2948,6 +2908,8 @@ async function loadConfig(cwd) {
|
|
|
2948
2908
|
}
|
|
2949
2909
|
const extendsSpec = userFile?.extends;
|
|
2950
2910
|
const orgLayer = await loadExtendsLayer(cwd, extendsSpec);
|
|
2911
|
+
migrateLegacyConfigKeys(orgLayer);
|
|
2912
|
+
if (userFile) migrateLegacyConfigKeys(userFile);
|
|
2951
2913
|
const user = userFile ? stripExtends(userFile) : {};
|
|
2952
2914
|
const base = structuredClone(defaultConfig);
|
|
2953
2915
|
const withOrg = defu2(orgLayer, base);
|
|
@@ -2966,7 +2928,7 @@ function hasDep(deps, name) {
|
|
|
2966
2928
|
async function detectStack(cwd) {
|
|
2967
2929
|
let pkg = {};
|
|
2968
2930
|
try {
|
|
2969
|
-
const raw = await fs.readFile(
|
|
2931
|
+
const raw = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
2970
2932
|
pkg = JSON.parse(raw);
|
|
2971
2933
|
} catch {
|
|
2972
2934
|
return {
|
|
@@ -2986,7 +2948,7 @@ async function detectStack(cwd) {
|
|
|
2986
2948
|
const isMonorepo = Boolean(pkg.workspaces);
|
|
2987
2949
|
let tsStrict = null;
|
|
2988
2950
|
try {
|
|
2989
|
-
const tsconfigPath =
|
|
2951
|
+
const tsconfigPath = path5.join(cwd, "tsconfig.json");
|
|
2990
2952
|
const tsRaw = await fs.readFile(tsconfigPath, "utf8");
|
|
2991
2953
|
const ts = JSON.parse(tsRaw);
|
|
2992
2954
|
if (typeof ts.compilerOptions?.strict === "boolean") {
|
|
@@ -2996,15 +2958,15 @@ async function detectStack(cwd) {
|
|
|
2996
2958
|
}
|
|
2997
2959
|
let pm = "unknown";
|
|
2998
2960
|
try {
|
|
2999
|
-
await fs.access(
|
|
2961
|
+
await fs.access(path5.join(cwd, "pnpm-lock.yaml"));
|
|
3000
2962
|
pm = "pnpm";
|
|
3001
2963
|
} catch {
|
|
3002
2964
|
try {
|
|
3003
|
-
await fs.access(
|
|
2965
|
+
await fs.access(path5.join(cwd, "yarn.lock"));
|
|
3004
2966
|
pm = "yarn";
|
|
3005
2967
|
} catch {
|
|
3006
2968
|
try {
|
|
3007
|
-
await fs.access(
|
|
2969
|
+
await fs.access(path5.join(cwd, "package-lock.json"));
|
|
3008
2970
|
pm = "npm";
|
|
3009
2971
|
} catch {
|
|
3010
2972
|
pm = "npm";
|
|
@@ -3034,6 +2996,65 @@ function formatStackOneLiner(s3) {
|
|
|
3034
2996
|
bits.push(`pkg: ${s3.packageManager}`);
|
|
3035
2997
|
return bits.join(" \xB7 ") || "unknown";
|
|
3036
2998
|
}
|
|
2999
|
+
function normalizePrPath(p2) {
|
|
3000
|
+
return p2.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
3001
|
+
}
|
|
3002
|
+
function hasPrFileList(pr) {
|
|
3003
|
+
return Boolean(pr?.files?.length);
|
|
3004
|
+
}
|
|
3005
|
+
function prPathSet(pr) {
|
|
3006
|
+
if (!hasPrFileList(pr)) return null;
|
|
3007
|
+
return new Set(pr.files.map(normalizePrPath));
|
|
3008
|
+
}
|
|
3009
|
+
function isPathInPrScope(relOrAbs, cwd, prSet) {
|
|
3010
|
+
const raw = relOrAbs.replace(/^file:\/\//, "");
|
|
3011
|
+
let rel = normalizePrPath(raw);
|
|
3012
|
+
if (path5.isAbsolute(raw)) {
|
|
3013
|
+
rel = normalizePrPath(path5.relative(cwd, raw));
|
|
3014
|
+
}
|
|
3015
|
+
if (prSet.has(rel)) return true;
|
|
3016
|
+
const trimmed = rel.replace(/^\.\//, "");
|
|
3017
|
+
if (prSet.has(trimmed)) return true;
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
var ESLINT_EXT = /\.(js|cjs|mjs|jsx|ts|tsx)$/i;
|
|
3021
|
+
var PRETTIER_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|json|md|css|scss|sass|less|yml|yaml|vue|svelte)$/i;
|
|
3022
|
+
var CYCLES_EXT = /\.(tsx?|jsx?|mjs|cjs|js)$/i;
|
|
3023
|
+
function filterPrFilesForEslint(pr) {
|
|
3024
|
+
if (!hasPrFileList(pr)) return null;
|
|
3025
|
+
return pr.files.map(normalizePrPath).filter((f4) => ESLINT_EXT.test(f4));
|
|
3026
|
+
}
|
|
3027
|
+
function filterPrFilesForPrettier(pr) {
|
|
3028
|
+
if (!hasPrFileList(pr)) return null;
|
|
3029
|
+
return pr.files.map(normalizePrPath).filter((f4) => PRETTIER_EXT.test(f4));
|
|
3030
|
+
}
|
|
3031
|
+
function filterPrFilesForMadge(pr) {
|
|
3032
|
+
if (!hasPrFileList(pr)) return null;
|
|
3033
|
+
return pr.files.map(normalizePrPath).filter((f4) => CYCLES_EXT.test(f4));
|
|
3034
|
+
}
|
|
3035
|
+
async function existingRepoPaths(cwd, rels) {
|
|
3036
|
+
const out = [];
|
|
3037
|
+
for (const rel of rels) {
|
|
3038
|
+
try {
|
|
3039
|
+
await fs.access(path5.join(cwd, rel));
|
|
3040
|
+
out.push(rel);
|
|
3041
|
+
} catch {
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return out;
|
|
3045
|
+
}
|
|
3046
|
+
function filterTscOutputToPrFiles(output, cwd, prSet) {
|
|
3047
|
+
const lines = output.split("\n");
|
|
3048
|
+
const kept = [];
|
|
3049
|
+
for (const line of lines) {
|
|
3050
|
+
const m3 = /^(.+?)\(\d+,\d+\):\s/.exec(line);
|
|
3051
|
+
if (m3?.[1]) {
|
|
3052
|
+
if (isPathInPrScope(m3[1], cwd, prSet)) kept.push(line);
|
|
3053
|
+
continue;
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
return kept.join("\n").trim();
|
|
3057
|
+
}
|
|
3037
3058
|
function stripFileUrl(p2) {
|
|
3038
3059
|
let s3 = p2.trim();
|
|
3039
3060
|
if (!/^file:/i.test(s3)) return s3;
|
|
@@ -3045,30 +3066,30 @@ function stripFileUrl(p2) {
|
|
|
3045
3066
|
return s3;
|
|
3046
3067
|
}
|
|
3047
3068
|
function isUnderDir(parent, child) {
|
|
3048
|
-
const rel =
|
|
3049
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
3069
|
+
const rel = path5.relative(parent, child);
|
|
3070
|
+
return rel === "" || !rel.startsWith("..") && !path5.isAbsolute(rel);
|
|
3050
3071
|
}
|
|
3051
3072
|
function toRepoRelativePath(cwd, filePath) {
|
|
3052
3073
|
if (!filePath?.trim()) return void 0;
|
|
3053
3074
|
const raw = stripFileUrl(filePath);
|
|
3054
|
-
const resolvedCwd =
|
|
3055
|
-
const absFile =
|
|
3075
|
+
const resolvedCwd = path5.resolve(cwd);
|
|
3076
|
+
const absFile = path5.isAbsolute(raw) ? path5.resolve(raw) : path5.resolve(resolvedCwd, raw);
|
|
3056
3077
|
if (!isUnderDir(resolvedCwd, absFile)) {
|
|
3057
3078
|
return raw.split(/[/\\]/g).join("/");
|
|
3058
3079
|
}
|
|
3059
|
-
let rel =
|
|
3080
|
+
let rel = path5.relative(resolvedCwd, absFile);
|
|
3060
3081
|
if (!rel || rel === ".") {
|
|
3061
|
-
return
|
|
3082
|
+
return path5.basename(absFile);
|
|
3062
3083
|
}
|
|
3063
|
-
return rel.split(
|
|
3084
|
+
return rel.split(path5.sep).join("/");
|
|
3064
3085
|
}
|
|
3065
3086
|
function stripRepoAbsolutePaths(cwd, text) {
|
|
3066
3087
|
if (!text || !cwd.trim()) return text;
|
|
3067
|
-
const resolvedCwd =
|
|
3088
|
+
const resolvedCwd = path5.resolve(cwd);
|
|
3068
3089
|
const asPosix = (s3) => s3.replace(/\\/g, "/");
|
|
3069
3090
|
const cwdPosix = asPosix(resolvedCwd);
|
|
3070
3091
|
let out = asPosix(text);
|
|
3071
|
-
const prefixes = [.../* @__PURE__ */ new Set([cwdPosix + "/", resolvedCwd +
|
|
3092
|
+
const prefixes = [.../* @__PURE__ */ new Set([cwdPosix + "/", resolvedCwd + path5.sep])].filter(
|
|
3072
3093
|
(p2) => p2.length > 1
|
|
3073
3094
|
);
|
|
3074
3095
|
for (const prefix of prefixes) {
|
|
@@ -3694,7 +3715,7 @@ async function pathExists(file) {
|
|
|
3694
3715
|
}
|
|
3695
3716
|
}
|
|
3696
3717
|
async function resolveBin(cwd, name) {
|
|
3697
|
-
const local =
|
|
3718
|
+
const local = path5.join(cwd, "node_modules", ".bin", name);
|
|
3698
3719
|
if (await pathExists(local)) return local;
|
|
3699
3720
|
const win = local + ".cmd";
|
|
3700
3721
|
if (await pathExists(win)) return win;
|
|
@@ -3750,7 +3771,7 @@ async function runNpx(cwd, args) {
|
|
|
3750
3771
|
// src/checks/eslint.ts
|
|
3751
3772
|
async function hasEslintDependency(cwd) {
|
|
3752
3773
|
try {
|
|
3753
|
-
const raw = await fs.readFile(
|
|
3774
|
+
const raw = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
3754
3775
|
const p2 = JSON.parse(raw);
|
|
3755
3776
|
return Boolean(p2.devDependencies?.eslint || p2.dependencies?.eslint);
|
|
3756
3777
|
} catch {
|
|
@@ -3762,6 +3783,7 @@ async function hasEslintConfig(cwd) {
|
|
|
3762
3783
|
"eslint.config.js",
|
|
3763
3784
|
"eslint.config.mjs",
|
|
3764
3785
|
"eslint.config.cjs",
|
|
3786
|
+
"eslint.config.json",
|
|
3765
3787
|
".eslintrc",
|
|
3766
3788
|
".eslintrc.json",
|
|
3767
3789
|
".eslintrc.cjs",
|
|
@@ -3769,14 +3791,47 @@ async function hasEslintConfig(cwd) {
|
|
|
3769
3791
|
".eslintrc.yml"
|
|
3770
3792
|
];
|
|
3771
3793
|
for (const c4 of candidates) {
|
|
3772
|
-
if (await pathExists(
|
|
3794
|
+
if (await pathExists(path5.join(cwd, c4))) return true;
|
|
3773
3795
|
}
|
|
3774
3796
|
return false;
|
|
3775
3797
|
}
|
|
3776
3798
|
function meaningfulStderr(stderr) {
|
|
3777
3799
|
return stderr.split("\n").filter((l3) => l3.trim() && !/^npm warn\b/i.test(l3)).join("\n").trim();
|
|
3778
3800
|
}
|
|
3779
|
-
|
|
3801
|
+
var ESLINT_BATCH = 80;
|
|
3802
|
+
async function runEslintOnPaths(cwd, relPaths) {
|
|
3803
|
+
let worstExit = 0;
|
|
3804
|
+
const merged = [];
|
|
3805
|
+
let stderrAcc = "";
|
|
3806
|
+
for (let i3 = 0; i3 < relPaths.length; i3 += ESLINT_BATCH) {
|
|
3807
|
+
const batch = relPaths.slice(i3, i3 + ESLINT_BATCH);
|
|
3808
|
+
const args = [
|
|
3809
|
+
...batch,
|
|
3810
|
+
"--max-warnings",
|
|
3811
|
+
"0",
|
|
3812
|
+
"--no-error-on-unmatched-pattern",
|
|
3813
|
+
"-f",
|
|
3814
|
+
"json"
|
|
3815
|
+
];
|
|
3816
|
+
const { exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "eslint", args);
|
|
3817
|
+
worstExit = Math.max(worstExit, exitCode ?? 0);
|
|
3818
|
+
stderrAcc += stderr;
|
|
3819
|
+
const t3 = stdout2.trim();
|
|
3820
|
+
if (t3) {
|
|
3821
|
+
try {
|
|
3822
|
+
const rows = JSON.parse(t3);
|
|
3823
|
+
if (Array.isArray(rows)) merged.push(...rows);
|
|
3824
|
+
} catch {
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
return {
|
|
3829
|
+
exitCode: worstExit,
|
|
3830
|
+
stdout: JSON.stringify(merged),
|
|
3831
|
+
stderr: stderrAcc
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
async function runEslint(cwd, config, _stack, pr) {
|
|
3780
3835
|
const t0 = performance.now();
|
|
3781
3836
|
if (!config.checks.eslint.enabled) {
|
|
3782
3837
|
return {
|
|
@@ -3797,15 +3852,39 @@ async function runEslint(cwd, config, _stack) {
|
|
|
3797
3852
|
};
|
|
3798
3853
|
}
|
|
3799
3854
|
const glob = config.checks.eslint.glob ?? "**/*.{js,cjs,mjs,jsx,ts,tsx}";
|
|
3800
|
-
const
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3855
|
+
const prPaths = filterPrFilesForEslint(pr);
|
|
3856
|
+
let exitCode;
|
|
3857
|
+
let stdout2;
|
|
3858
|
+
let stderr;
|
|
3859
|
+
if (prPaths !== null) {
|
|
3860
|
+
if (prPaths.length === 0) {
|
|
3861
|
+
return {
|
|
3862
|
+
checkId: "eslint",
|
|
3863
|
+
findings: [],
|
|
3864
|
+
durationMs: Math.round(performance.now() - t0),
|
|
3865
|
+
skipped: "no lintable files in PR diff"
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
const existing = await existingRepoPaths(cwd, prPaths);
|
|
3869
|
+
if (existing.length === 0) {
|
|
3870
|
+
return {
|
|
3871
|
+
checkId: "eslint",
|
|
3872
|
+
findings: [],
|
|
3873
|
+
durationMs: Math.round(performance.now() - t0),
|
|
3874
|
+
skipped: hasPrFileList(pr) ? "no lintable files in PR diff (added/modified only)" : "no files"
|
|
3875
|
+
};
|
|
3876
|
+
}
|
|
3877
|
+
({ exitCode, stdout: stdout2, stderr } = await runEslintOnPaths(cwd, existing));
|
|
3878
|
+
} else {
|
|
3879
|
+
({ exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "eslint", [
|
|
3880
|
+
glob,
|
|
3881
|
+
"--max-warnings",
|
|
3882
|
+
"0",
|
|
3883
|
+
"--no-error-on-unmatched-pattern",
|
|
3884
|
+
"-f",
|
|
3885
|
+
"json"
|
|
3886
|
+
]));
|
|
3887
|
+
}
|
|
3809
3888
|
const errText = meaningfulStderr(stderr);
|
|
3810
3889
|
const findings = [];
|
|
3811
3890
|
if (exitCode === 0) {
|
|
@@ -3871,7 +3950,25 @@ function truncate(s3, max) {
|
|
|
3871
3950
|
}
|
|
3872
3951
|
|
|
3873
3952
|
// src/checks/prettier.ts
|
|
3874
|
-
|
|
3953
|
+
var PRETTIER_BATCH = 100;
|
|
3954
|
+
async function runPrettierOnPaths(cwd, relPaths) {
|
|
3955
|
+
let worstExit = 0;
|
|
3956
|
+
let stdoutAcc = "";
|
|
3957
|
+
let stderrAcc = "";
|
|
3958
|
+
for (let i3 = 0; i3 < relPaths.length; i3 += PRETTIER_BATCH) {
|
|
3959
|
+
const batch = relPaths.slice(i3, i3 + PRETTIER_BATCH);
|
|
3960
|
+
const r4 = await runNpmBinary(cwd, "prettier", [
|
|
3961
|
+
"--check",
|
|
3962
|
+
"--ignore-unknown",
|
|
3963
|
+
...batch
|
|
3964
|
+
]);
|
|
3965
|
+
worstExit = Math.max(worstExit, r4.exitCode ?? 0);
|
|
3966
|
+
stdoutAcc += r4.stdout;
|
|
3967
|
+
stderrAcc += r4.stderr;
|
|
3968
|
+
}
|
|
3969
|
+
return { exitCode: worstExit, stdout: stdoutAcc, stderr: stderrAcc };
|
|
3970
|
+
}
|
|
3971
|
+
async function runPrettier(cwd, config, pr) {
|
|
3875
3972
|
const t0 = performance.now();
|
|
3876
3973
|
if (!config.checks.prettier.enabled) {
|
|
3877
3974
|
return {
|
|
@@ -3882,11 +3979,36 @@ async function runPrettier(cwd, config) {
|
|
|
3882
3979
|
};
|
|
3883
3980
|
}
|
|
3884
3981
|
const glob = config.checks.prettier.glob ?? "**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}";
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3982
|
+
let exitCode;
|
|
3983
|
+
let stdout2;
|
|
3984
|
+
let stderr;
|
|
3985
|
+
const prPaths = filterPrFilesForPrettier(pr);
|
|
3986
|
+
if (prPaths !== null) {
|
|
3987
|
+
if (prPaths.length === 0) {
|
|
3988
|
+
return {
|
|
3989
|
+
checkId: "prettier",
|
|
3990
|
+
findings: [],
|
|
3991
|
+
durationMs: Math.round(performance.now() - t0),
|
|
3992
|
+
skipped: "no formattable files in PR diff"
|
|
3993
|
+
};
|
|
3994
|
+
}
|
|
3995
|
+
const existing = await existingRepoPaths(cwd, prPaths);
|
|
3996
|
+
if (existing.length === 0) {
|
|
3997
|
+
return {
|
|
3998
|
+
checkId: "prettier",
|
|
3999
|
+
findings: [],
|
|
4000
|
+
durationMs: Math.round(performance.now() - t0),
|
|
4001
|
+
skipped: hasPrFileList(pr) ? "no formattable files in PR diff" : "no files"
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
({ exitCode, stdout: stdout2, stderr } = await runPrettierOnPaths(cwd, existing));
|
|
4005
|
+
} else {
|
|
4006
|
+
({ exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "prettier", [
|
|
4007
|
+
"--check",
|
|
4008
|
+
glob,
|
|
4009
|
+
"--ignore-unknown"
|
|
4010
|
+
]));
|
|
4011
|
+
}
|
|
3890
4012
|
const findings = [];
|
|
3891
4013
|
if (exitCode === 127 || /command not found|not recognized|ENOENT/i.test(stderr)) {
|
|
3892
4014
|
return {
|
|
@@ -3915,7 +4037,7 @@ function truncate2(s3, max) {
|
|
|
3915
4037
|
if (s3.length <= max) return s3;
|
|
3916
4038
|
return s3.slice(0, max) + "\u2026";
|
|
3917
4039
|
}
|
|
3918
|
-
async function runTypeScript(cwd, config, stack) {
|
|
4040
|
+
async function runTypeScript(cwd, config, stack, pr) {
|
|
3919
4041
|
const t0 = performance.now();
|
|
3920
4042
|
if (!config.checks.typescript.enabled) {
|
|
3921
4043
|
return {
|
|
@@ -3925,7 +4047,7 @@ async function runTypeScript(cwd, config, stack) {
|
|
|
3925
4047
|
skipped: "disabled in config"
|
|
3926
4048
|
};
|
|
3927
4049
|
}
|
|
3928
|
-
const hasTs = stack.hasTypeScript || await pathExists(
|
|
4050
|
+
const hasTs = stack.hasTypeScript || await pathExists(path5.join(cwd, "tsconfig.json"));
|
|
3929
4051
|
if (!hasTs) {
|
|
3930
4052
|
return {
|
|
3931
4053
|
checkId: "typescript",
|
|
@@ -3934,7 +4056,14 @@ async function runTypeScript(cwd, config, stack) {
|
|
|
3934
4056
|
skipped: "no TypeScript project detected"
|
|
3935
4057
|
};
|
|
3936
4058
|
}
|
|
3937
|
-
const
|
|
4059
|
+
const extra = config.checks.typescript.tscArgs ?? [];
|
|
4060
|
+
const hasProject = extra.some((a3) => a3 === "-p" || a3 === "--project");
|
|
4061
|
+
const tsconfigPath = path5.join(cwd, "tsconfig.json");
|
|
4062
|
+
const args = [
|
|
4063
|
+
"--noEmit",
|
|
4064
|
+
...hasProject || !await pathExists(tsconfigPath) ? [] : ["-p", "tsconfig.json"],
|
|
4065
|
+
...extra
|
|
4066
|
+
];
|
|
3938
4067
|
const { exitCode, stdout: stdout2, stderr } = await runNpmBinary(cwd, "tsc", args);
|
|
3939
4068
|
const findings = [];
|
|
3940
4069
|
if (exitCode === 127 || /command not found|not recognized|ENOENT/i.test(stderr)) {
|
|
@@ -3947,12 +4076,16 @@ async function runTypeScript(cwd, config, stack) {
|
|
|
3947
4076
|
}
|
|
3948
4077
|
if (exitCode !== 0) {
|
|
3949
4078
|
const out = [stdout2, stderr].filter(Boolean).join("\n").trim();
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4079
|
+
const prS = prPathSet(pr);
|
|
4080
|
+
const scoped = prS && out ? filterTscOutputToPrFiles(out, cwd, prS) : out;
|
|
4081
|
+
if (scoped) {
|
|
4082
|
+
findings.push({
|
|
4083
|
+
id: "tsc",
|
|
4084
|
+
severity: "warn",
|
|
4085
|
+
message: prS ? "TypeScript: issues in PR-changed files (full project was typechecked)" : "TypeScript compiler reported diagnostics",
|
|
4086
|
+
detail: scoped ? truncate3(scoped, 8e3) : `exit ${exitCode}`
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
3956
4089
|
}
|
|
3957
4090
|
return {
|
|
3958
4091
|
checkId: "typescript",
|
|
@@ -4038,7 +4171,7 @@ async function runSecrets(cwd, config, pr) {
|
|
|
4038
4171
|
});
|
|
4039
4172
|
break;
|
|
4040
4173
|
}
|
|
4041
|
-
const full =
|
|
4174
|
+
const full = path5.join(cwd, rel);
|
|
4042
4175
|
let content;
|
|
4043
4176
|
try {
|
|
4044
4177
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4064,7 +4197,7 @@ async function runSecrets(cwd, config, pr) {
|
|
|
4064
4197
|
};
|
|
4065
4198
|
}
|
|
4066
4199
|
function isProbablyTextFile(rel) {
|
|
4067
|
-
const ext =
|
|
4200
|
+
const ext = path5.extname(rel).toLowerCase();
|
|
4068
4201
|
return TEXT_EXT.has(ext);
|
|
4069
4202
|
}
|
|
4070
4203
|
|
|
@@ -4084,7 +4217,7 @@ function runPrHygiene(config, pr) {
|
|
|
4084
4217
|
checkId: "pr-hygiene",
|
|
4085
4218
|
findings: [],
|
|
4086
4219
|
durationMs: Math.round(performance.now() - t0),
|
|
4087
|
-
skipped: "
|
|
4220
|
+
skipped: "no PR context (run in a Bitbucket PR pipeline with BITBUCKET_PR_ID, or set FRONTGUARD_PR_BODY for description checks)"
|
|
4088
4221
|
};
|
|
4089
4222
|
}
|
|
4090
4223
|
const findings = [];
|
|
@@ -4163,6 +4296,25 @@ function sectionMentioned(body, hint) {
|
|
|
4163
4296
|
}
|
|
4164
4297
|
|
|
4165
4298
|
// src/checks/pr-size.ts
|
|
4299
|
+
function expandMessage(template, lines, min) {
|
|
4300
|
+
return template.replaceAll("${lines}", String(lines)).replaceAll("${min}", String(min));
|
|
4301
|
+
}
|
|
4302
|
+
function defaultTiers(cfg) {
|
|
4303
|
+
return [
|
|
4304
|
+
{
|
|
4305
|
+
minLines: cfg.softBlockLines,
|
|
4306
|
+
severity: "warn",
|
|
4307
|
+
id: "pr-size-large",
|
|
4308
|
+
message: "PR is very large (${lines} lines changed; threshold ${min}). Consider splitting for review."
|
|
4309
|
+
},
|
|
4310
|
+
{
|
|
4311
|
+
minLines: cfg.warnLines,
|
|
4312
|
+
severity: "info",
|
|
4313
|
+
id: "pr-size-medium",
|
|
4314
|
+
message: "PR size is elevated (${lines} lines changed; threshold ${min})."
|
|
4315
|
+
}
|
|
4316
|
+
];
|
|
4317
|
+
}
|
|
4166
4318
|
function runPrSize(config, pr) {
|
|
4167
4319
|
const t0 = performance.now();
|
|
4168
4320
|
if (!config.checks.prSize.enabled) {
|
|
@@ -4178,24 +4330,29 @@ function runPrSize(config, pr) {
|
|
|
4178
4330
|
checkId: "pr-size",
|
|
4179
4331
|
findings: [],
|
|
4180
4332
|
durationMs: Math.round(performance.now() - t0),
|
|
4181
|
-
skipped: "
|
|
4333
|
+
skipped: "no PR context (run in a Bitbucket pull-request pipeline so BITBUCKET_PR_* and git diff are available)"
|
|
4182
4334
|
};
|
|
4183
4335
|
}
|
|
4184
|
-
const findings = [];
|
|
4185
4336
|
const lines = pr.additions + pr.deletions;
|
|
4186
|
-
const
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
}
|
|
4194
|
-
|
|
4337
|
+
const cfg = config.checks.prSize;
|
|
4338
|
+
const rawTiers = cfg.tiers?.length ? cfg.tiers : defaultTiers(cfg);
|
|
4339
|
+
const sorted = [...rawTiers].sort((a3, b3) => b3.minLines - a3.minLines);
|
|
4340
|
+
const findings = [];
|
|
4341
|
+
let i3 = 0;
|
|
4342
|
+
for (const tier of sorted) {
|
|
4343
|
+
if (lines < tier.minLines) continue;
|
|
4344
|
+
const id = tier.id ?? `pr-size-tier-${i3}`;
|
|
4345
|
+
i3 += 1;
|
|
4346
|
+
const message = tier.message ? expandMessage(tier.message, lines, tier.minLines) : expandMessage(
|
|
4347
|
+
"PR has ${lines} lines changed (\u2265 ${min}).",
|
|
4348
|
+
lines,
|
|
4349
|
+
tier.minLines
|
|
4350
|
+
);
|
|
4195
4351
|
findings.push({
|
|
4196
|
-
id
|
|
4197
|
-
severity:
|
|
4198
|
-
message
|
|
4352
|
+
id,
|
|
4353
|
+
severity: tier.severity,
|
|
4354
|
+
message,
|
|
4355
|
+
detail: "Total = additions + deletions from the PR diff."
|
|
4199
4356
|
});
|
|
4200
4357
|
}
|
|
4201
4358
|
return {
|
|
@@ -4248,13 +4405,18 @@ async function gitDiffForReview(cwd, baseRef, maxChars) {
|
|
|
4248
4405
|
}
|
|
4249
4406
|
}
|
|
4250
4407
|
async function resolveDiffBaseRef(cwd, fallback) {
|
|
4251
|
-
const
|
|
4252
|
-
if (
|
|
4253
|
-
const origin = `origin/${
|
|
4408
|
+
const bb = process.env.BITBUCKET_PR_DESTINATION_BRANCH?.trim();
|
|
4409
|
+
if (bb) {
|
|
4410
|
+
const origin = `origin/${bb}`;
|
|
4254
4411
|
try {
|
|
4255
4412
|
await W2("git", ["rev-parse", "--verify", origin], { nodeOptions: { cwd } });
|
|
4256
4413
|
return origin;
|
|
4257
4414
|
} catch {
|
|
4415
|
+
try {
|
|
4416
|
+
await W2("git", ["rev-parse", "--verify", bb], { nodeOptions: { cwd } });
|
|
4417
|
+
return bb;
|
|
4418
|
+
} catch {
|
|
4419
|
+
}
|
|
4258
4420
|
}
|
|
4259
4421
|
}
|
|
4260
4422
|
try {
|
|
@@ -4352,7 +4514,7 @@ async function runTsAnyDelta(cwd, config, stack) {
|
|
|
4352
4514
|
function gateSeverity2(g4) {
|
|
4353
4515
|
return g4 === "block" ? "block" : g4 === "info" ? "info" : "warn";
|
|
4354
4516
|
}
|
|
4355
|
-
async function runCycles(cwd, config, stack) {
|
|
4517
|
+
async function runCycles(cwd, config, stack, pr) {
|
|
4356
4518
|
const t0 = performance.now();
|
|
4357
4519
|
const cfg = config.checks.cycles;
|
|
4358
4520
|
if (!cfg.enabled || !stack.hasTypeScript) {
|
|
@@ -4365,12 +4527,12 @@ async function runCycles(cwd, config, stack) {
|
|
|
4365
4527
|
}
|
|
4366
4528
|
let entry = cfg.entries[0] ?? "src";
|
|
4367
4529
|
for (const e3 of cfg.entries) {
|
|
4368
|
-
if (await pathExists(
|
|
4530
|
+
if (await pathExists(path5.join(cwd, e3))) {
|
|
4369
4531
|
entry = e3;
|
|
4370
4532
|
break;
|
|
4371
4533
|
}
|
|
4372
4534
|
}
|
|
4373
|
-
if (!await pathExists(
|
|
4535
|
+
if (!await pathExists(path5.join(cwd, entry))) {
|
|
4374
4536
|
return {
|
|
4375
4537
|
checkId: "cycles",
|
|
4376
4538
|
findings: [],
|
|
@@ -4378,10 +4540,13 @@ async function runCycles(cwd, config, stack) {
|
|
|
4378
4540
|
skipped: `entry path not found (${entry})`
|
|
4379
4541
|
};
|
|
4380
4542
|
}
|
|
4543
|
+
const prMadge = filterPrFilesForMadge(pr);
|
|
4544
|
+
const prExisting = prMadge?.length ? await existingRepoPaths(cwd, prMadge) : [];
|
|
4545
|
+
const roots = prExisting.length > 0 ? prExisting : [entry];
|
|
4381
4546
|
const args = [
|
|
4382
4547
|
"-y",
|
|
4383
4548
|
"madge@6",
|
|
4384
|
-
|
|
4549
|
+
...roots,
|
|
4385
4550
|
"--extensions",
|
|
4386
4551
|
"ts,tsx,js,jsx",
|
|
4387
4552
|
"--circular",
|
|
@@ -4403,7 +4568,7 @@ async function runCycles(cwd, config, stack) {
|
|
|
4403
4568
|
findings.push({
|
|
4404
4569
|
id: "import-cycle",
|
|
4405
4570
|
severity: gateSeverity2(cfg.gate),
|
|
4406
|
-
message: "Circular dependencies detected (madge)",
|
|
4571
|
+
message: prExisting.length > 0 ? "Circular dependencies detected (madge, scoped to PR files)" : "Circular dependencies detected (madge)",
|
|
4407
4572
|
detail: truncate4(out || `exit ${exitCode}`, 12e3)
|
|
4408
4573
|
});
|
|
4409
4574
|
} else if (exitCode !== 0) {
|
|
@@ -4494,7 +4659,7 @@ async function sumGlobBytes(cwd, patterns) {
|
|
|
4494
4659
|
});
|
|
4495
4660
|
for (const rel of files) {
|
|
4496
4661
|
try {
|
|
4497
|
-
const st = await fs.stat(
|
|
4662
|
+
const st = await fs.stat(path5.join(cwd, rel));
|
|
4498
4663
|
total += st.size;
|
|
4499
4664
|
} catch {
|
|
4500
4665
|
}
|
|
@@ -4503,7 +4668,7 @@ async function sumGlobBytes(cwd, patterns) {
|
|
|
4503
4668
|
return total;
|
|
4504
4669
|
}
|
|
4505
4670
|
async function readBaseline(cwd, relPath, baseRef) {
|
|
4506
|
-
const disk =
|
|
4671
|
+
const disk = path5.join(cwd, relPath);
|
|
4507
4672
|
try {
|
|
4508
4673
|
const raw = await fs.readFile(disk, "utf8");
|
|
4509
4674
|
return JSON.parse(raw);
|
|
@@ -4533,6 +4698,39 @@ async function gitOkQuick(cwd) {
|
|
|
4533
4698
|
function tokenizeCommand(cmd) {
|
|
4534
4699
|
return cmd.trim().split(/\s+/).map((t3) => t3.trim()).filter(Boolean);
|
|
4535
4700
|
}
|
|
4701
|
+
function npmScriptFromBuildCommand(cmd) {
|
|
4702
|
+
const t3 = cmd.trim();
|
|
4703
|
+
if (/^yarn\s+build\b/i.test(t3)) return "build";
|
|
4704
|
+
const np = /^(?:npm|pnpm)\s+run\s+(\S+)/i.exec(t3);
|
|
4705
|
+
if (np?.[1]) return np[1];
|
|
4706
|
+
const yr = /^yarn\s+run\s+(\S+)/i.exec(t3);
|
|
4707
|
+
if (yr?.[1]) return yr[1];
|
|
4708
|
+
return null;
|
|
4709
|
+
}
|
|
4710
|
+
async function bundleBuildPrecheck(cwd, buildCommand) {
|
|
4711
|
+
const script = npmScriptFromBuildCommand(buildCommand);
|
|
4712
|
+
if (!script) return { run: true };
|
|
4713
|
+
let scripts;
|
|
4714
|
+
try {
|
|
4715
|
+
const raw = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
4716
|
+
const pkg = JSON.parse(raw);
|
|
4717
|
+
scripts = pkg.scripts;
|
|
4718
|
+
} catch {
|
|
4719
|
+
return {
|
|
4720
|
+
run: false,
|
|
4721
|
+
message: "Skipped bundle build \u2014 no readable package.json",
|
|
4722
|
+
detail: "Set checks.bundle.buildCommand to your real build (e.g. `vite build`), set checks.bundle.runBuild to false, or add a package.json with the matching script."
|
|
4723
|
+
};
|
|
4724
|
+
}
|
|
4725
|
+
if (!scripts?.[script]) {
|
|
4726
|
+
return {
|
|
4727
|
+
run: false,
|
|
4728
|
+
message: `Skipped bundle build \u2014 no scripts.${script} in package.json`,
|
|
4729
|
+
detail: "The bundle check runs a production build, then sums file sizes under checks.bundle.measureGlobs (dist/, build/static/, .next/static/, etc.). Libraries and non-web repos often have no `build` script \u2014 set checks.bundle.runBuild to false and optionally tune measureGlobs, or set checks.bundle.buildCommand to whatever produces your artifacts (e.g. `pnpm run compile`, `npx vite build`)."
|
|
4730
|
+
};
|
|
4731
|
+
}
|
|
4732
|
+
return { run: true };
|
|
4733
|
+
}
|
|
4536
4734
|
async function runBundle(cwd, config, stack) {
|
|
4537
4735
|
const t0 = performance.now();
|
|
4538
4736
|
const cfg = config.checks.bundle;
|
|
@@ -4552,6 +4750,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4552
4750
|
skipped: "skipped for React Native (configure web artifacts if needed)"
|
|
4553
4751
|
};
|
|
4554
4752
|
}
|
|
4753
|
+
const preFindings = [];
|
|
4555
4754
|
if (cfg.runBuild) {
|
|
4556
4755
|
const parts = tokenizeCommand(cfg.buildCommand);
|
|
4557
4756
|
if (parts.length === 0) {
|
|
@@ -4567,21 +4766,31 @@ async function runBundle(cwd, config, stack) {
|
|
|
4567
4766
|
durationMs: Math.round(performance.now() - t0)
|
|
4568
4767
|
};
|
|
4569
4768
|
}
|
|
4570
|
-
const
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4769
|
+
const pre = await bundleBuildPrecheck(cwd, cfg.buildCommand);
|
|
4770
|
+
if (!pre.run) {
|
|
4771
|
+
preFindings.push({
|
|
4772
|
+
id: "bundle-build-skipped",
|
|
4773
|
+
severity: "info",
|
|
4774
|
+
message: pre.message,
|
|
4775
|
+
detail: pre.detail
|
|
4776
|
+
});
|
|
4777
|
+
} else {
|
|
4778
|
+
const [bin, ...args] = parts;
|
|
4779
|
+
const res = await W2(bin, args, { nodeOptions: { cwd } });
|
|
4780
|
+
if ((res.exitCode ?? 0) !== 0) {
|
|
4781
|
+
return {
|
|
4782
|
+
checkId: "bundle",
|
|
4783
|
+
findings: [
|
|
4784
|
+
{
|
|
4785
|
+
id: "bundle-build",
|
|
4786
|
+
severity: gateSeverity4(cfg.gate),
|
|
4787
|
+
message: "Build command failed \u2014 cannot measure bundle",
|
|
4788
|
+
detail: [res.stdout, res.stderr].filter(Boolean).join("\n").slice(0, 8e3)
|
|
4789
|
+
}
|
|
4790
|
+
],
|
|
4791
|
+
durationMs: Math.round(performance.now() - t0)
|
|
4792
|
+
};
|
|
4793
|
+
}
|
|
4585
4794
|
}
|
|
4586
4795
|
}
|
|
4587
4796
|
const total = await sumGlobBytes(cwd, cfg.measureGlobs);
|
|
@@ -4589,6 +4798,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4589
4798
|
return {
|
|
4590
4799
|
checkId: "bundle",
|
|
4591
4800
|
findings: [
|
|
4801
|
+
...preFindings,
|
|
4592
4802
|
{
|
|
4593
4803
|
id: "bundle-empty",
|
|
4594
4804
|
severity: "info",
|
|
@@ -4600,7 +4810,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4600
4810
|
}
|
|
4601
4811
|
const baseRef = await gitOkQuick(cwd) ? await resolveDiffBaseRef(cwd, cfg.baselineRef) : null;
|
|
4602
4812
|
const baseline = await readBaseline(cwd, cfg.baselinePath, baseRef);
|
|
4603
|
-
const findings = [];
|
|
4813
|
+
const findings = [...preFindings];
|
|
4604
4814
|
const infoLines = [
|
|
4605
4815
|
`Measured ${total} bytes (${(total / 1024 / 1024).toFixed(2)} MiB)`,
|
|
4606
4816
|
baseline ? `Baseline from \`${cfg.baselinePath}\`: ${baseline.totalBytes} bytes` : `No baseline at \`${cfg.baselinePath}\` (commit a baseline JSON to compare)`
|
|
@@ -4649,12 +4859,12 @@ async function runBundle(cwd, config, stack) {
|
|
|
4649
4859
|
function gateSeverity5(g4) {
|
|
4650
4860
|
return g4 === "block" ? "block" : g4 === "info" ? "info" : "warn";
|
|
4651
4861
|
}
|
|
4652
|
-
async function
|
|
4862
|
+
async function runCoreWebVitals(cwd, config, stack, pr) {
|
|
4653
4863
|
const t0 = performance.now();
|
|
4654
|
-
const cfg = config.checks.
|
|
4864
|
+
const cfg = config.checks.coreWebVitals;
|
|
4655
4865
|
if (!cfg.enabled) {
|
|
4656
4866
|
return {
|
|
4657
|
-
checkId: "
|
|
4867
|
+
checkId: "core-web-vitals",
|
|
4658
4868
|
findings: [],
|
|
4659
4869
|
durationMs: 0,
|
|
4660
4870
|
skipped: "disabled in config"
|
|
@@ -4662,7 +4872,7 @@ async function runCwv(cwd, config, stack, pr) {
|
|
|
4662
4872
|
}
|
|
4663
4873
|
if (stack.hasReactNative && !stack.hasNext) {
|
|
4664
4874
|
return {
|
|
4665
|
-
checkId: "
|
|
4875
|
+
checkId: "core-web-vitals",
|
|
4666
4876
|
findings: [],
|
|
4667
4877
|
durationMs: Math.round(performance.now() - t0),
|
|
4668
4878
|
skipped: "skipped for React Native"
|
|
@@ -4678,7 +4888,7 @@ async function runCwv(cwd, config, stack, pr) {
|
|
|
4678
4888
|
const findings = [];
|
|
4679
4889
|
const sev2 = gateSeverity5(cfg.gate);
|
|
4680
4890
|
for (const rel of toScan.slice(0, 400)) {
|
|
4681
|
-
const full =
|
|
4891
|
+
const full = path5.join(cwd, rel);
|
|
4682
4892
|
let text;
|
|
4683
4893
|
try {
|
|
4684
4894
|
text = await fs.readFile(full, "utf8");
|
|
@@ -4688,23 +4898,23 @@ async function runCwv(cwd, config, stack, pr) {
|
|
|
4688
4898
|
if (text.length > cfg.maxFileBytes) continue;
|
|
4689
4899
|
if (stack.hasNext && /<img\b/i.test(text) && !/from\s+['"]next\/image['"]/.test(text)) {
|
|
4690
4900
|
findings.push({
|
|
4691
|
-
id: "
|
|
4901
|
+
id: "core-web-vitals-img-tag",
|
|
4692
4902
|
severity: sev2,
|
|
4693
|
-
message: "Raw `<img>`
|
|
4903
|
+
message: "Raw `<img>` \u2014 prefer `next/image` for LCP-friendly delivery",
|
|
4694
4904
|
file: rel
|
|
4695
4905
|
});
|
|
4696
4906
|
}
|
|
4697
4907
|
if (/dangerouslySetInnerHTML/i.test(text)) {
|
|
4698
4908
|
findings.push({
|
|
4699
|
-
id: "
|
|
4909
|
+
id: "core-web-vitals-dsh",
|
|
4700
4910
|
severity: "warn",
|
|
4701
|
-
message: "`dangerouslySetInnerHTML` can
|
|
4911
|
+
message: "`dangerouslySetInnerHTML` can add main-thread work \u2014 validate necessity",
|
|
4702
4912
|
file: rel
|
|
4703
4913
|
});
|
|
4704
4914
|
}
|
|
4705
4915
|
}
|
|
4706
4916
|
return {
|
|
4707
|
-
checkId: "
|
|
4917
|
+
checkId: "core-web-vitals",
|
|
4708
4918
|
findings: dedupeFindings(findings).slice(0, 40),
|
|
4709
4919
|
durationMs: Math.round(performance.now() - t0)
|
|
4710
4920
|
};
|
|
@@ -4763,7 +4973,7 @@ async function runCustomRules(cwd, config, restrictToFiles) {
|
|
|
4763
4973
|
});
|
|
4764
4974
|
break;
|
|
4765
4975
|
}
|
|
4766
|
-
const full =
|
|
4976
|
+
const full = path5.join(cwd, rel);
|
|
4767
4977
|
let content;
|
|
4768
4978
|
try {
|
|
4769
4979
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4879,7 +5089,7 @@ async function runAiAssistedStrict(cwd, config, pr) {
|
|
|
4879
5089
|
checkId: "ai-assisted-strict",
|
|
4880
5090
|
findings: [],
|
|
4881
5091
|
durationMs: Math.round(performance.now() - t0),
|
|
4882
|
-
skipped: "
|
|
5092
|
+
skipped: "no PR context (Bitbucket PR pipeline + BITBUCKET_PR_ID required)"
|
|
4883
5093
|
};
|
|
4884
5094
|
}
|
|
4885
5095
|
if (!pr.aiAssisted) {
|
|
@@ -4894,7 +5104,7 @@ async function runAiAssistedStrict(cwd, config, pr) {
|
|
|
4894
5104
|
const gate = cfg.gate;
|
|
4895
5105
|
const findings = [];
|
|
4896
5106
|
for (const rel of files) {
|
|
4897
|
-
const full =
|
|
5107
|
+
const full = path5.join(cwd, rel);
|
|
4898
5108
|
let content;
|
|
4899
5109
|
try {
|
|
4900
5110
|
content = await fs.readFile(full, "utf8");
|
|
@@ -4959,25 +5169,6 @@ function escapeHtml(s3) {
|
|
|
4959
5169
|
}
|
|
4960
5170
|
|
|
4961
5171
|
// src/report/html-report.ts
|
|
4962
|
-
function shieldUrl(label, message, color) {
|
|
4963
|
-
const q2 = new URLSearchParams({ label, message, color, style: "for-the-badge" });
|
|
4964
|
-
return `https://img.shields.io/static/v1?${q2}`;
|
|
4965
|
-
}
|
|
4966
|
-
function riskColor(risk) {
|
|
4967
|
-
if (risk === "LOW") return "brightgreen";
|
|
4968
|
-
if (risk === "MEDIUM") return "orange";
|
|
4969
|
-
return "red";
|
|
4970
|
-
}
|
|
4971
|
-
function modeColor(mode) {
|
|
4972
|
-
return mode === "enforce" ? "critical" : "blue";
|
|
4973
|
-
}
|
|
4974
|
-
function countColor(kind, n3) {
|
|
4975
|
-
if (kind === "block") return n3 === 0 ? "brightgreen" : "critical";
|
|
4976
|
-
if (kind === "info") return n3 === 0 ? "inactive" : "informational";
|
|
4977
|
-
if (n3 === 0) return "brightgreen";
|
|
4978
|
-
if (n3 <= 10) return "yellow";
|
|
4979
|
-
return "orange";
|
|
4980
|
-
}
|
|
4981
5172
|
function parseLineHint(detail) {
|
|
4982
5173
|
if (!detail) return 0;
|
|
4983
5174
|
const m3 = /^line\s+(\d+)/i.exec(detail.trim());
|
|
@@ -5009,13 +5200,20 @@ function formatDuration(ms) {
|
|
|
5009
5200
|
const r4 = s3 % 60;
|
|
5010
5201
|
return r4 ? `${m3}m ${r4}s` : `${m3}m`;
|
|
5011
5202
|
}
|
|
5203
|
+
function statusDot(r4) {
|
|
5204
|
+
if (r4.skipped) return '<span class="dot dot-skip" title="Skipped"></span>';
|
|
5205
|
+
if (r4.findings.length === 0) return '<span class="dot dot-ok" title="Clean"></span>';
|
|
5206
|
+
if (r4.findings.some((x3) => x3.severity === "block"))
|
|
5207
|
+
return '<span class="dot dot-block" title="Blocking"></span>';
|
|
5208
|
+
return '<span class="dot dot-warn" title="Issues"></span>';
|
|
5209
|
+
}
|
|
5012
5210
|
function renderFindingCard(cwd, r4, f4) {
|
|
5013
5211
|
const d3 = normalizeFinding(cwd, f4);
|
|
5014
5212
|
const sevClass = f4.severity === "block" ? "sev-block" : f4.severity === "warn" ? "sev-warn" : "sev-info";
|
|
5015
|
-
const fixBlock = f4.suggestedFix ? `<div class="suggested-fix"><
|
|
5213
|
+
const fixBlock = f4.suggestedFix ? `<div class="suggested-fix"><div class="fix-label">Suggested fix <span class="pill pill-llm">LLM</span></div><div class="fix-md">${escapeHtml(f4.suggestedFix.summary)}</div>${f4.suggestedFix.code ? `<pre class="code"><code>${escapeHtml(f4.suggestedFix.code)}</code></pre>` : ""}<p class="disclaimer">Suggestions are non-binding; review before applying.</p></div>` : "";
|
|
5016
5214
|
const hintRow = d3.detail && !d3.detail.includes("\n") && d3.detail.length <= 220 && !d3.detail.includes("|") ? `<tr><th>Hint</th><td>${escapeHtml(d3.detail)}</td></tr>` : "";
|
|
5017
5215
|
const detailFence = d3.detail && (d3.detail.includes("\n") || d3.detail.length > 220 || d3.detail.includes("|")) ? `<pre class="code"><code>${escapeHtml(d3.detail)}</code></pre>` : "";
|
|
5018
|
-
return `<article class="card ${sevClass}"><
|
|
5216
|
+
return `<article class="card ${sevClass}"><div class="card-title">${escapeHtml(d3.file ?? "\u2014")}</div><p class="card-msg">${escapeHtml(d3.message)}</p><table class="meta"><tr><th>Check</th><td><code>${escapeHtml(r4.checkId)}</code></td></tr><tr><th>Rule</th><td><code>${escapeHtml(f4.id)}</code></td></tr>${d3.file ? `<tr><th>File</th><td><code>${escapeHtml(d3.file)}</code></td></tr>` : ""}${hintRow}</table>${detailFence}${fixBlock}</article>`;
|
|
5019
5217
|
}
|
|
5020
5218
|
function buildHtmlReport(p2) {
|
|
5021
5219
|
const {
|
|
@@ -5031,21 +5229,11 @@ function buildHtmlReport(p2) {
|
|
|
5031
5229
|
lines,
|
|
5032
5230
|
llmAppendix
|
|
5033
5231
|
} = p2;
|
|
5034
|
-
const modeLabel = mode === "enforce" ? "
|
|
5035
|
-
const
|
|
5036
|
-
["risk", riskScore, riskColor(riskScore)],
|
|
5037
|
-
["mode", modeLabel, modeColor(mode)],
|
|
5038
|
-
["blocking", String(blocks), countColor("block", blocks)],
|
|
5039
|
-
["warnings", String(warns), countColor("warn", warns)],
|
|
5040
|
-
["info", String(infos), countColor("info", infos)]
|
|
5041
|
-
];
|
|
5042
|
-
const badgeImgs = badges.map(([l3, m3, c4]) => {
|
|
5043
|
-
const alt = `${l3}: ${m3}`;
|
|
5044
|
-
return `<img class="badge" src="${escapeHtml(shieldUrl(l3, m3, c4))}" alt="${escapeHtml(alt)}" loading="lazy" />`;
|
|
5045
|
-
}).join(" ");
|
|
5232
|
+
const modeLabel = mode === "enforce" ? "Enforce" : "Warn only";
|
|
5233
|
+
const riskClass = riskScore === "LOW" ? "risk-low" : riskScore === "MEDIUM" ? "risk-med" : "risk-high";
|
|
5046
5234
|
const checkRows = results.map((r4) => {
|
|
5047
|
-
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "
|
|
5048
|
-
return `<tr><td
|
|
5235
|
+
const status = r4.skipped ? `Skipped \u2014 ${escapeHtml(r4.skipped)}` : r4.findings.length === 0 ? "Pass" : `${r4.findings.length} issue(s)`;
|
|
5236
|
+
return `<tr><td class="td-icon">${statusDot(r4)}</td><td><strong class="check-name">${escapeHtml(r4.checkId)}</strong></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>`;
|
|
5049
5237
|
}).join("\n");
|
|
5050
5238
|
const blockItems = sortFindings(
|
|
5051
5239
|
cwd,
|
|
@@ -5072,138 +5260,286 @@ function buildHtmlReport(p2) {
|
|
|
5072
5260
|
byCheck.set(item.r.checkId, list);
|
|
5073
5261
|
}
|
|
5074
5262
|
const checkOrder = [...byCheck.keys()].sort((a3, b3) => a3.localeCompare(b3));
|
|
5075
|
-
const blockingHtml = blockItems.length === 0 ? '<p class="
|
|
5263
|
+
const blockingHtml = blockItems.length === 0 ? '<p class="empty-state">No blocking findings.</p>' : blockItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5076
5264
|
let warningsHtml = "";
|
|
5077
5265
|
if (warnItems.length === 0) {
|
|
5078
|
-
warningsHtml = '<p class="
|
|
5266
|
+
warningsHtml = '<p class="empty-state">No warnings.</p>';
|
|
5079
5267
|
} else {
|
|
5080
5268
|
for (const cid of checkOrder) {
|
|
5081
5269
|
const group = sortFindings(cwd, byCheck.get(cid));
|
|
5082
|
-
|
|
5083
|
-
warningsHtml +=
|
|
5270
|
+
const cards = group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5271
|
+
warningsHtml += `<details class="panel nested"><summary><span class="summary-title">${escapeHtml(cid)}</span><span class="summary-count">${group.length}</span></summary><div class="panel-body">${cards}</div></details>`;
|
|
5084
5272
|
}
|
|
5085
5273
|
}
|
|
5086
|
-
const infoHtml = infoItems.length === 0 ? '<p class="muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5087
|
-
const prBlock = pr && lines != null ? `<tr><th>
|
|
5088
|
-
const appendix = llmAppendix?.trim() ? `<section class="
|
|
5274
|
+
const infoHtml = infoItems.length === 0 ? '<p class="empty-state muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5275
|
+
const prBlock = pr && lines != null ? `<tr><th>Pull request</th><td>${lines} lines changed (+${pr.additions} / \u2212${pr.deletions}) \xB7 ${pr.changedFiles} files</td></tr>` : "";
|
|
5276
|
+
const appendix = llmAppendix?.trim() ? `<section class="section"><h2 class="h2">Appendix</h2><pre class="md-raw">${escapeHtml(llmAppendix.trim())}</pre></section>` : "";
|
|
5089
5277
|
return `<!DOCTYPE html>
|
|
5090
5278
|
<html lang="en">
|
|
5091
5279
|
<head>
|
|
5092
5280
|
<meta charset="utf-8" />
|
|
5093
5281
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
5094
|
-
<title>FrontGuard
|
|
5282
|
+
<title>FrontGuard \u2014 Report</title>
|
|
5095
5283
|
<style>
|
|
5096
5284
|
:root {
|
|
5097
|
-
--bg: #
|
|
5098
|
-
--
|
|
5099
|
-
--text: #
|
|
5100
|
-
--muted: #
|
|
5101
|
-
--border: #
|
|
5102
|
-
--
|
|
5103
|
-
--
|
|
5104
|
-
--
|
|
5105
|
-
--
|
|
5106
|
-
--
|
|
5285
|
+
--bg: #f8fafc;
|
|
5286
|
+
--surface: #ffffff;
|
|
5287
|
+
--text: #0f172a;
|
|
5288
|
+
--muted: #64748b;
|
|
5289
|
+
--border: #e2e8f0;
|
|
5290
|
+
--accent: #4f46e5;
|
|
5291
|
+
--accent-soft: #eef2ff;
|
|
5292
|
+
--block: #dc2626;
|
|
5293
|
+
--warn: #d97706;
|
|
5294
|
+
--info: #0284c7;
|
|
5295
|
+
--ok: #16a34a;
|
|
5296
|
+
--radius: 10px;
|
|
5297
|
+
--shadow: 0 1px 3px rgba(15, 23, 42, 0.06);
|
|
5107
5298
|
}
|
|
5108
5299
|
* { box-sizing: border-box; }
|
|
5109
5300
|
body {
|
|
5110
|
-
margin: 0;
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
.
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5301
|
+
margin: 0;
|
|
5302
|
+
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
5303
|
+
background: var(--bg);
|
|
5304
|
+
color: var(--text);
|
|
5305
|
+
line-height: 1.55;
|
|
5306
|
+
font-size: 15px;
|
|
5307
|
+
padding: 2rem clamp(1rem, 4vw, 3rem) 4rem;
|
|
5308
|
+
max-width: 920px;
|
|
5309
|
+
margin-left: auto;
|
|
5310
|
+
margin-right: auto;
|
|
5311
|
+
}
|
|
5312
|
+
.hero {
|
|
5313
|
+
margin-bottom: 2rem;
|
|
5314
|
+
}
|
|
5315
|
+
.brand {
|
|
5316
|
+
font-size: 0.75rem;
|
|
5317
|
+
font-weight: 600;
|
|
5318
|
+
letter-spacing: 0.12em;
|
|
5319
|
+
text-transform: uppercase;
|
|
5320
|
+
color: var(--muted);
|
|
5321
|
+
margin-bottom: 0.35rem;
|
|
5322
|
+
}
|
|
5323
|
+
h1 {
|
|
5324
|
+
font-size: 1.75rem;
|
|
5325
|
+
font-weight: 700;
|
|
5326
|
+
letter-spacing: -0.03em;
|
|
5327
|
+
margin: 0 0 1rem;
|
|
5328
|
+
color: var(--text);
|
|
5329
|
+
}
|
|
5330
|
+
.metrics {
|
|
5331
|
+
display: flex;
|
|
5332
|
+
flex-wrap: wrap;
|
|
5333
|
+
gap: 0.65rem;
|
|
5334
|
+
margin-bottom: 0.5rem;
|
|
5335
|
+
}
|
|
5336
|
+
.metric {
|
|
5337
|
+
background: var(--surface);
|
|
5338
|
+
border: 1px solid var(--border);
|
|
5339
|
+
border-radius: var(--radius);
|
|
5340
|
+
padding: 0.5rem 0.9rem;
|
|
5341
|
+
box-shadow: var(--shadow);
|
|
5342
|
+
display: flex;
|
|
5343
|
+
align-items: center;
|
|
5344
|
+
gap: 0.5rem;
|
|
5345
|
+
}
|
|
5346
|
+
.metric-label { font-size: 0.72rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
5347
|
+
.metric-value { font-weight: 600; font-size: 0.95rem; }
|
|
5348
|
+
.risk-low { color: var(--ok); }
|
|
5349
|
+
.risk-med { color: var(--warn); }
|
|
5350
|
+
.risk-high { color: var(--block); }
|
|
5351
|
+
.section { margin-top: 2.25rem; }
|
|
5352
|
+
.h2 {
|
|
5353
|
+
font-size: 1rem;
|
|
5354
|
+
font-weight: 600;
|
|
5355
|
+
margin: 0 0 0.85rem;
|
|
5356
|
+
color: var(--text);
|
|
5357
|
+
letter-spacing: -0.02em;
|
|
5358
|
+
}
|
|
5359
|
+
.snapshot {
|
|
5360
|
+
width: 100%;
|
|
5361
|
+
border-collapse: collapse;
|
|
5362
|
+
font-size: 0.9rem;
|
|
5363
|
+
background: var(--surface);
|
|
5364
|
+
border-radius: var(--radius);
|
|
5365
|
+
overflow: hidden;
|
|
5366
|
+
border: 1px solid var(--border);
|
|
5367
|
+
box-shadow: var(--shadow);
|
|
5368
|
+
}
|
|
5369
|
+
.snapshot th, .snapshot td {
|
|
5370
|
+
padding: 0.65rem 1rem;
|
|
5371
|
+
text-align: left;
|
|
5372
|
+
border-bottom: 1px solid var(--border);
|
|
5373
|
+
}
|
|
5374
|
+
.snapshot tr:last-child th, .snapshot tr:last-child td { border-bottom: none; }
|
|
5375
|
+
.snapshot th {
|
|
5376
|
+
width: 9rem;
|
|
5377
|
+
color: var(--muted);
|
|
5378
|
+
font-weight: 500;
|
|
5379
|
+
background: #f1f5f9;
|
|
5380
|
+
}
|
|
5381
|
+
table.results {
|
|
5382
|
+
width: 100%;
|
|
5383
|
+
border-collapse: collapse;
|
|
5384
|
+
font-size: 0.875rem;
|
|
5385
|
+
background: var(--surface);
|
|
5386
|
+
border-radius: var(--radius);
|
|
5387
|
+
overflow: hidden;
|
|
5388
|
+
border: 1px solid var(--border);
|
|
5389
|
+
box-shadow: var(--shadow);
|
|
5390
|
+
}
|
|
5391
|
+
table.results th, table.results td {
|
|
5392
|
+
padding: 0.55rem 0.85rem;
|
|
5393
|
+
text-align: left;
|
|
5394
|
+
border-bottom: 1px solid var(--border);
|
|
5395
|
+
}
|
|
5396
|
+
table.results tr:last-child td { border-bottom: none; }
|
|
5397
|
+
table.results thead th {
|
|
5398
|
+
background: #f1f5f9;
|
|
5399
|
+
color: var(--muted);
|
|
5400
|
+
font-weight: 600;
|
|
5401
|
+
font-size: 0.72rem;
|
|
5402
|
+
text-transform: uppercase;
|
|
5403
|
+
letter-spacing: 0.04em;
|
|
5404
|
+
}
|
|
5405
|
+
.td-icon { width: 2rem; vertical-align: middle; }
|
|
5406
|
+
.td-num, .td-time { color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
5407
|
+
.check-name { font-weight: 600; }
|
|
5408
|
+
.dot {
|
|
5409
|
+
display: inline-block;
|
|
5410
|
+
width: 8px;
|
|
5411
|
+
height: 8px;
|
|
5412
|
+
border-radius: 50%;
|
|
5413
|
+
}
|
|
5414
|
+
.dot-ok { background: var(--ok); }
|
|
5415
|
+
.dot-warn { background: var(--warn); }
|
|
5416
|
+
.dot-block { background: var(--block); }
|
|
5417
|
+
.dot-skip { background: #cbd5e1; }
|
|
5418
|
+
.panel {
|
|
5419
|
+
background: var(--surface);
|
|
5420
|
+
border: 1px solid var(--border);
|
|
5421
|
+
border-radius: var(--radius);
|
|
5422
|
+
margin-bottom: 0.65rem;
|
|
5423
|
+
box-shadow: var(--shadow);
|
|
5424
|
+
}
|
|
5425
|
+
.panel summary {
|
|
5426
|
+
cursor: pointer;
|
|
5427
|
+
padding: 0.85rem 1rem;
|
|
5428
|
+
list-style: none;
|
|
5429
|
+
display: flex;
|
|
5430
|
+
align-items: center;
|
|
5431
|
+
justify-content: space-between;
|
|
5432
|
+
font-weight: 600;
|
|
5433
|
+
font-size: 0.9rem;
|
|
5434
|
+
}
|
|
5435
|
+
.panel summary::-webkit-details-marker { display: none; }
|
|
5436
|
+
.panel[open] summary { border-bottom: 1px solid var(--border); }
|
|
5437
|
+
.panel-body { padding: 0.75rem 1rem 1rem; }
|
|
5438
|
+
.nested summary { font-weight: 500; color: var(--warn); }
|
|
5439
|
+
.summary-count {
|
|
5440
|
+
font-size: 0.8rem;
|
|
5441
|
+
font-weight: 500;
|
|
5442
|
+
color: var(--muted);
|
|
5443
|
+
background: #f1f5f9;
|
|
5444
|
+
padding: 0.15rem 0.5rem;
|
|
5445
|
+
border-radius: 999px;
|
|
5446
|
+
}
|
|
5137
5447
|
.card {
|
|
5138
|
-
border: 1px solid var(--border);
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5448
|
+
border: 1px solid var(--border);
|
|
5449
|
+
border-radius: 8px;
|
|
5450
|
+
padding: 1rem;
|
|
5451
|
+
margin-bottom: 0.65rem;
|
|
5452
|
+
background: #fafafa;
|
|
5453
|
+
}
|
|
5454
|
+
.card:last-child { margin-bottom: 0; }
|
|
5455
|
+
.card.sev-block { border-left: 3px solid var(--block); }
|
|
5456
|
+
.card.sev-warn { border-left: 3px solid var(--warn); }
|
|
5457
|
+
.card.sev-info { border-left: 3px solid var(--info); }
|
|
5458
|
+
.card-title { font-size: 0.8rem; font-weight: 600; color: var(--muted); margin-bottom: 0.35rem; }
|
|
5459
|
+
.card-msg { margin: 0 0 0.65rem; font-size: 0.9rem; }
|
|
5460
|
+
table.meta { width: 100%; font-size: 0.78rem; border-collapse: collapse; margin: 0.35rem 0 0; }
|
|
5461
|
+
table.meta th { text-align: left; color: var(--muted); width: 4.5rem; padding: 0.2rem 0.5rem 0.2rem 0; vertical-align: top; }
|
|
5146
5462
|
table.meta td { padding: 0.2rem 0; }
|
|
5463
|
+
table.meta code { font-size: 0.85em; background: #f1f5f9; padding: 0.1rem 0.35rem; border-radius: 4px; }
|
|
5147
5464
|
.muted { color: var(--muted); }
|
|
5148
|
-
.
|
|
5465
|
+
.empty-state { margin: 0; font-size: 0.9rem; color: var(--muted); }
|
|
5149
5466
|
pre.code {
|
|
5150
|
-
margin: 0.5rem 0 0;
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5467
|
+
margin: 0.5rem 0 0;
|
|
5468
|
+
padding: 0.75rem;
|
|
5469
|
+
background: #f1f5f9;
|
|
5470
|
+
border-radius: 6px;
|
|
5471
|
+
overflow: auto;
|
|
5472
|
+
font-size: 0.78rem;
|
|
5473
|
+
border: 1px solid var(--border);
|
|
5474
|
+
}
|
|
5475
|
+
pre.code code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
|
|
5476
|
+
.suggested-fix { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px dashed var(--border); }
|
|
5477
|
+
.fix-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); margin-bottom: 0.35rem; }
|
|
5478
|
+
.pill-llm { background: var(--accent-soft); color: var(--accent); padding: 0.1rem 0.4rem; border-radius: 4px; font-size: 0.65rem; }
|
|
5157
5479
|
.fix-md { font-size: 0.85rem; white-space: pre-wrap; margin: 0.25rem 0 0.5rem; }
|
|
5158
|
-
.tag {
|
|
5159
|
-
font-size: 0.65rem; background: var(--accent); color: var(--bg);
|
|
5160
|
-
padding: 0.1rem 0.35rem; border-radius: 4px; vertical-align: middle;
|
|
5161
|
-
}
|
|
5162
5480
|
.disclaimer { font-size: 0.72rem; color: var(--muted); margin: 0.5rem 0 0; }
|
|
5163
|
-
|
|
5164
|
-
white-space: pre-wrap;
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5481
|
+
pre.md-raw {
|
|
5482
|
+
white-space: pre-wrap;
|
|
5483
|
+
font-size: 0.85rem;
|
|
5484
|
+
background: var(--surface);
|
|
5485
|
+
padding: 1rem;
|
|
5486
|
+
border-radius: var(--radius);
|
|
5487
|
+
border: 1px solid var(--border);
|
|
5488
|
+
margin: 0;
|
|
5489
|
+
}
|
|
5490
|
+
footer {
|
|
5491
|
+
margin-top: 3rem;
|
|
5492
|
+
padding-top: 1.25rem;
|
|
5493
|
+
border-top: 1px solid var(--border);
|
|
5494
|
+
font-size: 0.8rem;
|
|
5495
|
+
color: var(--muted);
|
|
5496
|
+
}
|
|
5497
|
+
footer a { color: var(--accent); text-decoration: none; }
|
|
5498
|
+
footer a:hover { text-decoration: underline; }
|
|
5168
5499
|
</style>
|
|
5169
5500
|
</head>
|
|
5170
5501
|
<body>
|
|
5171
|
-
<
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5502
|
+
<header class="hero">
|
|
5503
|
+
<div class="brand">FrontGuard</div>
|
|
5504
|
+
<h1>Code review report</h1>
|
|
5505
|
+
<div class="metrics">
|
|
5506
|
+
<div class="metric"><span class="metric-label">Risk</span><span class="metric-value ${riskClass}">${riskScore}</span></div>
|
|
5507
|
+
<div class="metric"><span class="metric-label">Mode</span><span class="metric-value">${escapeHtml(modeLabel)}</span></div>
|
|
5508
|
+
<div class="metric"><span class="metric-label">Blocking</span><span class="metric-value">${blocks}</span></div>
|
|
5509
|
+
<div class="metric"><span class="metric-label">Warnings</span><span class="metric-value">${warns}</span></div>
|
|
5510
|
+
<div class="metric"><span class="metric-label">Info</span><span class="metric-value">${infos}</span></div>
|
|
5511
|
+
</div>
|
|
5512
|
+
</header>
|
|
5181
5513
|
|
|
5182
|
-
<
|
|
5183
|
-
|
|
5184
|
-
<
|
|
5185
|
-
|
|
5186
|
-
|
|
5514
|
+
<section class="section">
|
|
5515
|
+
<h2 class="h2">Overview</h2>
|
|
5516
|
+
<table class="snapshot">
|
|
5517
|
+
<tr><th>Risk score</th><td><strong>${riskScore}</strong> <span class="muted">\u2014 heuristic</span></td></tr>
|
|
5518
|
+
<tr><th>Mode</th><td>${escapeHtml(modeLabel)}</td></tr>
|
|
5519
|
+
<tr><th>Stack</th><td>${escapeHtml(formatStackOneLiner(stack))}</td></tr>
|
|
5520
|
+
${prBlock}
|
|
5521
|
+
</table>
|
|
5522
|
+
</section>
|
|
5187
5523
|
|
|
5188
|
-
<
|
|
5189
|
-
<
|
|
5190
|
-
<
|
|
5191
|
-
|
|
5524
|
+
<section class="section">
|
|
5525
|
+
<h2 class="h2">Checks</h2>
|
|
5526
|
+
<table class="results">
|
|
5527
|
+
<thead><tr><th></th><th>Check</th><th>Status</th><th>#</th><th>Time</th></tr></thead>
|
|
5528
|
+
<tbody>${checkRows}</tbody>
|
|
5529
|
+
</table>
|
|
5530
|
+
</section>
|
|
5192
5531
|
|
|
5193
|
-
<
|
|
5194
|
-
<
|
|
5195
|
-
<div class="
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
<summary>Info (${infos})</summary>
|
|
5200
|
-
<div class="details-body">${infoHtml}</div>
|
|
5201
|
-
</details>
|
|
5532
|
+
<section class="section">
|
|
5533
|
+
<h2 class="h2">Findings</h2>
|
|
5534
|
+
<details class="panel"><summary>Blocking <span class="summary-count">${blocks}</span></summary><div class="panel-body">${blockingHtml}</div></details>
|
|
5535
|
+
<details class="panel"><summary>Warnings <span class="summary-count">${warns}</span></summary><div class="panel-body">${warningsHtml}</div></details>
|
|
5536
|
+
<details class="panel"><summary>Info <span class="summary-count">${infos}</span></summary><div class="panel-body">${infoHtml}</div></details>
|
|
5537
|
+
</section>
|
|
5202
5538
|
|
|
5203
5539
|
${appendix}
|
|
5204
5540
|
|
|
5205
5541
|
<footer>
|
|
5206
|
-
<p>
|
|
5542
|
+
<p>Static report \u2014 open in any browser. Generated by <strong>FrontGuard</strong>.</p>
|
|
5207
5543
|
</footer>
|
|
5208
5544
|
</body>
|
|
5209
5545
|
</html>`;
|
|
@@ -5605,6 +5941,17 @@ function formatConsole(p2) {
|
|
|
5605
5941
|
return lines.join("\n");
|
|
5606
5942
|
}
|
|
5607
5943
|
|
|
5944
|
+
// src/lib/http-fetch.ts
|
|
5945
|
+
function getFetch() {
|
|
5946
|
+
const f4 = globalThis.fetch;
|
|
5947
|
+
if (typeof f4 !== "function") {
|
|
5948
|
+
throw new Error(
|
|
5949
|
+
"FrontGuard needs Node.js 18+ (global fetch). Use NODE_VERSION / image >= 18 in CI."
|
|
5950
|
+
);
|
|
5951
|
+
}
|
|
5952
|
+
return f4;
|
|
5953
|
+
}
|
|
5954
|
+
|
|
5608
5955
|
// src/llm/ollama.ts
|
|
5609
5956
|
async function callOllamaChat(opts) {
|
|
5610
5957
|
const fetch = getFetch();
|
|
@@ -5638,10 +5985,10 @@ async function callOllamaChat(opts) {
|
|
|
5638
5985
|
|
|
5639
5986
|
// src/llm/finding-fixes.ts
|
|
5640
5987
|
async function safeReadRepoFile(cwd, rel, maxChars) {
|
|
5641
|
-
const root =
|
|
5642
|
-
const abs =
|
|
5643
|
-
const relToRoot =
|
|
5644
|
-
if (relToRoot.startsWith("..") ||
|
|
5988
|
+
const root = path5.resolve(cwd);
|
|
5989
|
+
const abs = path5.resolve(root, rel);
|
|
5990
|
+
const relToRoot = path5.relative(root, abs);
|
|
5991
|
+
if (relToRoot.startsWith("..") || path5.isAbsolute(relToRoot)) return null;
|
|
5645
5992
|
try {
|
|
5646
5993
|
let t3 = await fs.readFile(abs, "utf8");
|
|
5647
5994
|
if (t3.length > maxChars) {
|
|
@@ -5670,7 +6017,7 @@ async function enrichFindingsWithOllamaFixes(opts) {
|
|
|
5670
6017
|
}
|
|
5671
6018
|
let pkgSnippet = "";
|
|
5672
6019
|
try {
|
|
5673
|
-
const pj = await fs.readFile(
|
|
6020
|
+
const pj = await fs.readFile(path5.join(cwd, "package.json"), "utf8");
|
|
5674
6021
|
pkgSnippet = pj.slice(0, 4e3);
|
|
5675
6022
|
} catch {
|
|
5676
6023
|
pkgSnippet = "";
|
|
@@ -5754,7 +6101,7 @@ async function loadManualAppendix(opts) {
|
|
|
5754
6101
|
const envFile = process.env.FRONTGUARD_MANUAL_APPENDIX_FILE?.trim();
|
|
5755
6102
|
const resolvedPath = filePath?.trim() || envFile;
|
|
5756
6103
|
if (resolvedPath) {
|
|
5757
|
-
const abs =
|
|
6104
|
+
const abs = path5.isAbsolute(resolvedPath) ? resolvedPath : path5.join(cwd, resolvedPath);
|
|
5758
6105
|
try {
|
|
5759
6106
|
let text = await fs.readFile(abs, "utf8");
|
|
5760
6107
|
if (text.length > MAX_CHARS) {
|
|
@@ -5942,28 +6289,28 @@ async function runFrontGuard(opts) {
|
|
|
5942
6289
|
const config = await loadConfig(opts.cwd);
|
|
5943
6290
|
const mode = opts.enforce ? "enforce" : config.mode;
|
|
5944
6291
|
const stack = await detectStack(opts.cwd);
|
|
5945
|
-
const pr =
|
|
6292
|
+
const pr = readPrContext(opts.cwd);
|
|
5946
6293
|
const restrictFiles = pr?.files?.length ? pr.files : null;
|
|
5947
6294
|
const [
|
|
5948
6295
|
eslint,
|
|
5949
6296
|
prettier,
|
|
5950
6297
|
typescript,
|
|
5951
6298
|
secrets,
|
|
5952
|
-
tsAnyDelta,
|
|
5953
6299
|
cycles,
|
|
5954
6300
|
deadCode,
|
|
5955
|
-
|
|
6301
|
+
coreWebVitals,
|
|
6302
|
+
tsAnyDelta,
|
|
5956
6303
|
customRules,
|
|
5957
6304
|
aiStrict
|
|
5958
6305
|
] = await Promise.all([
|
|
5959
|
-
runEslint(opts.cwd, config),
|
|
5960
|
-
runPrettier(opts.cwd, config),
|
|
5961
|
-
runTypeScript(opts.cwd, config, stack),
|
|
6306
|
+
runEslint(opts.cwd, config, stack, pr),
|
|
6307
|
+
runPrettier(opts.cwd, config, pr),
|
|
6308
|
+
runTypeScript(opts.cwd, config, stack, pr),
|
|
5962
6309
|
runSecrets(opts.cwd, config, pr),
|
|
5963
|
-
|
|
5964
|
-
runCycles(opts.cwd, config, stack),
|
|
6310
|
+
runCycles(opts.cwd, config, stack, pr),
|
|
5965
6311
|
runDeadCode(opts.cwd, config, stack, pr),
|
|
5966
|
-
|
|
6312
|
+
runCoreWebVitals(opts.cwd, config, stack, pr),
|
|
6313
|
+
runTsAnyDelta(opts.cwd, config, stack),
|
|
5967
6314
|
runCustomRules(opts.cwd, config, restrictFiles),
|
|
5968
6315
|
runAiAssistedStrict(opts.cwd, config, pr)
|
|
5969
6316
|
]);
|
|
@@ -5975,15 +6322,15 @@ async function runFrontGuard(opts) {
|
|
|
5975
6322
|
prettier,
|
|
5976
6323
|
typescript,
|
|
5977
6324
|
secrets,
|
|
5978
|
-
tsAnyDelta,
|
|
5979
6325
|
cycles,
|
|
5980
6326
|
deadCode,
|
|
5981
6327
|
bundle,
|
|
5982
|
-
|
|
5983
|
-
customRules,
|
|
6328
|
+
coreWebVitals,
|
|
5984
6329
|
aiStrict,
|
|
5985
6330
|
prHygiene,
|
|
5986
|
-
prSize
|
|
6331
|
+
prSize,
|
|
6332
|
+
tsAnyDelta,
|
|
6333
|
+
customRules
|
|
5987
6334
|
];
|
|
5988
6335
|
applyAiAssistedEscalation(results, pr, config);
|
|
5989
6336
|
results = await enrichFindingsWithOllamaFixes({
|
|
@@ -6014,7 +6361,7 @@ async function runFrontGuard(opts) {
|
|
|
6014
6361
|
}
|
|
6015
6362
|
if (opts.prCommentOut) {
|
|
6016
6363
|
const snippet = formatBitbucketPrSnippet(report);
|
|
6017
|
-
const abs =
|
|
6364
|
+
const abs = path5.isAbsolute(opts.prCommentOut) ? opts.prCommentOut : path5.join(opts.cwd, opts.prCommentOut);
|
|
6018
6365
|
await fs.writeFile(abs, snippet, "utf8");
|
|
6019
6366
|
g.stderr.write(
|
|
6020
6367
|
`
|
|
@@ -6031,9 +6378,6 @@ FrontGuard: wrote Bitbucket PR comment text to ${abs} (${snippet.length} bytes).
|
|
|
6031
6378
|
g.stdout.write(report.consoleText + "\n\n");
|
|
6032
6379
|
g.stdout.write(report.markdown + "\n");
|
|
6033
6380
|
}
|
|
6034
|
-
if (opts.ci && g.env.GITHUB_TOKEN) {
|
|
6035
|
-
await upsertBriefComment(report.markdown);
|
|
6036
|
-
}
|
|
6037
6381
|
const hasBlock = results.some((r4) => r4.findings.some((f4) => f4.severity === "block"));
|
|
6038
6382
|
g.exitCode = mode === "enforce" && hasBlock ? 1 : 0;
|
|
6039
6383
|
}
|
|
@@ -6042,13 +6386,13 @@ FrontGuard: wrote Bitbucket PR comment text to ${abs} (${snippet.length} bytes).
|
|
|
6042
6386
|
var init2 = defineCommand({
|
|
6043
6387
|
meta: {
|
|
6044
6388
|
name: "init",
|
|
6045
|
-
description: "Add
|
|
6389
|
+
description: "Add Bitbucket pipeline example, pull_request_template.md, and frontguard.config.js"
|
|
6046
6390
|
},
|
|
6047
6391
|
run: async () => {
|
|
6048
6392
|
const cwd = g.cwd();
|
|
6049
6393
|
await initFrontGuard(cwd);
|
|
6050
6394
|
g.stdout.write(
|
|
6051
|
-
"FrontGuard initialized.\n\
|
|
6395
|
+
"FrontGuard initialized for Bitbucket.\n\n \u2022 bitbucket-pipelines.frontguard.example.yml \u2014 merge into your pipeline\n \u2022 pull_request_template.md \u2014 PR description template (Bitbucket Cloud)\n \u2022 frontguard.config.js (if missing)\n\nAdd the package as a devDependency so CI matches local runs:\n npm install -D @cleartrip/frontguard\n yarn add -D @cleartrip/frontguard\n"
|
|
6052
6396
|
);
|
|
6053
6397
|
}
|
|
6054
6398
|
});
|
|
@@ -6058,11 +6402,6 @@ var run = defineCommand({
|
|
|
6058
6402
|
description: "Run checks and print the review brief"
|
|
6059
6403
|
},
|
|
6060
6404
|
args: {
|
|
6061
|
-
ci: {
|
|
6062
|
-
type: "boolean",
|
|
6063
|
-
description: "Upsert PR comment when GITHUB_TOKEN is available",
|
|
6064
|
-
default: false
|
|
6065
|
-
},
|
|
6066
6405
|
markdown: {
|
|
6067
6406
|
type: "boolean",
|
|
6068
6407
|
description: "Print markdown only",
|
|
@@ -6089,7 +6428,6 @@ var run = defineCommand({
|
|
|
6089
6428
|
run: async ({ args }) => {
|
|
6090
6429
|
await runFrontGuard({
|
|
6091
6430
|
cwd: g.cwd(),
|
|
6092
|
-
ci: Boolean(args.ci),
|
|
6093
6431
|
markdown: Boolean(args.markdown),
|
|
6094
6432
|
enforce: Boolean(args.enforce),
|
|
6095
6433
|
append: typeof args.append === "string" ? args.append : null,
|