@cleartrip/frontguard 0.1.9 → 0.2.0
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 +195 -197
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/package.json +1 -1
- package/templates/freekit-ci-setup.md +7 -0
package/dist/cli.js
CHANGED
|
@@ -6,10 +6,10 @@ import * as tty from 'tty';
|
|
|
6
6
|
import { WriteStream } from 'tty';
|
|
7
7
|
import path4, { 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 path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "..");
|
|
2403
|
+
}
|
|
2445
2404
|
var CONFIG = `import { defineConfig } from '@cleartrip/frontguard'
|
|
2446
2405
|
|
|
2447
2406
|
export default defineConfig({
|
|
@@ -2496,27 +2455,35 @@ If **Yes**, list tools and what they touched (helps reviewers run a stricter fir
|
|
|
2496
2455
|
## AI assistance (optional detail)
|
|
2497
2456
|
- [ ] I have reviewed every AI-suggested line for security, auth, and product correctness
|
|
2498
2457
|
`;
|
|
2499
|
-
async function ensureDir(dir) {
|
|
2500
|
-
await fs.mkdir(dir, { recursive: true });
|
|
2501
|
-
}
|
|
2502
2458
|
async function initFrontGuard(cwd) {
|
|
2503
|
-
const
|
|
2504
|
-
|
|
2505
|
-
const
|
|
2506
|
-
|
|
2459
|
+
const root = packageRoot();
|
|
2460
|
+
const tplPath = path4.join(root, "templates", "bitbucket-pipelines.yml");
|
|
2461
|
+
const outPipeline = path4.join(cwd, "bitbucket-pipelines.frontguard.example.yml");
|
|
2462
|
+
try {
|
|
2463
|
+
await fs.access(outPipeline);
|
|
2464
|
+
} catch {
|
|
2465
|
+
try {
|
|
2466
|
+
const yml = await fs.readFile(tplPath, "utf8");
|
|
2467
|
+
await fs.writeFile(outPipeline, yml, "utf8");
|
|
2468
|
+
} catch {
|
|
2469
|
+
await fs.writeFile(
|
|
2470
|
+
outPipeline,
|
|
2471
|
+
"# Copy bitbucket-pipelines.yml from @cleartrip/frontguard/templates in node_modules\n",
|
|
2472
|
+
"utf8"
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2507
2476
|
const cfgPath = path4.join(cwd, "frontguard.config.js");
|
|
2508
2477
|
try {
|
|
2509
2478
|
await fs.access(cfgPath);
|
|
2510
2479
|
} catch {
|
|
2511
2480
|
await fs.writeFile(cfgPath, CONFIG, "utf8");
|
|
2512
2481
|
}
|
|
2513
|
-
const
|
|
2514
|
-
await ensureDir(tplRoot);
|
|
2515
|
-
const tplPath = path4.join(tplRoot, "pull_request_template.md");
|
|
2482
|
+
const tplPr = path4.join(cwd, "pull_request_template.md");
|
|
2516
2483
|
try {
|
|
2517
|
-
await fs.access(
|
|
2484
|
+
await fs.access(tplPr);
|
|
2518
2485
|
} catch {
|
|
2519
|
-
await fs.writeFile(
|
|
2486
|
+
await fs.writeFile(tplPr, PR_TEMPLATE, "utf8");
|
|
2520
2487
|
}
|
|
2521
2488
|
}
|
|
2522
2489
|
|
|
@@ -2654,110 +2621,86 @@ function parseAiDisclosure(body) {
|
|
|
2654
2621
|
return { assisted, explicitNo, ambiguous };
|
|
2655
2622
|
}
|
|
2656
2623
|
|
|
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;
|
|
2624
|
+
// src/ci/pr-context.ts
|
|
2625
|
+
function gitTrimmed(cwd, args) {
|
|
2626
|
+
return execFileSync("git", ["-C", cwd, ...args], {
|
|
2627
|
+
encoding: "utf8",
|
|
2628
|
+
maxBuffer: 20 * 1024 * 1024
|
|
2629
|
+
}).trimEnd();
|
|
2630
|
+
}
|
|
2631
|
+
function resolveCompareRef(cwd, destBranch) {
|
|
2632
|
+
const safe = destBranch.trim();
|
|
2633
|
+
if (!safe || safe.startsWith("-") || safe.includes("..")) return null;
|
|
2634
|
+
const candidates = [safe, `origin/${safe}`];
|
|
2635
|
+
for (const c4 of candidates) {
|
|
2636
|
+
try {
|
|
2637
|
+
gitTrimmed(cwd, ["rev-parse", "--verify", `${c4}^{commit}`]);
|
|
2638
|
+
return c4;
|
|
2639
|
+
} catch {
|
|
2640
|
+
}
|
|
2713
2641
|
}
|
|
2642
|
+
return null;
|
|
2714
2643
|
}
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2644
|
+
function parseNumstat(output) {
|
|
2645
|
+
let additions = 0;
|
|
2646
|
+
let deletions = 0;
|
|
2647
|
+
const files = [];
|
|
2648
|
+
for (const line of output.split("\n")) {
|
|
2649
|
+
const t3 = line.trim();
|
|
2650
|
+
if (!t3) continue;
|
|
2651
|
+
const tab = t3.indexOf(" ");
|
|
2652
|
+
if (tab < 0) continue;
|
|
2653
|
+
const tab2 = t3.indexOf(" ", tab + 1);
|
|
2654
|
+
if (tab2 < 0) continue;
|
|
2655
|
+
const aStr = t3.slice(0, tab);
|
|
2656
|
+
const dStr = t3.slice(tab + 1, tab2);
|
|
2657
|
+
const path17 = t3.slice(tab2 + 1);
|
|
2658
|
+
const a3 = aStr === "-" ? 0 : Number(aStr);
|
|
2659
|
+
const d3 = dStr === "-" ? 0 : Number(dStr);
|
|
2660
|
+
if (!Number.isFinite(a3) || !Number.isFinite(d3)) continue;
|
|
2661
|
+
additions += a3;
|
|
2662
|
+
deletions += d3;
|
|
2663
|
+
if (path17) files.push(path17);
|
|
2664
|
+
}
|
|
2665
|
+
return { additions, deletions, files };
|
|
2666
|
+
}
|
|
2667
|
+
function buildBitbucketPrContext(cwd) {
|
|
2668
|
+
const prId = process.env.BITBUCKET_PR_ID?.trim();
|
|
2669
|
+
if (!prId) return null;
|
|
2670
|
+
const dest = process.env.BITBUCKET_PR_DESTINATION_BRANCH?.trim() || "main";
|
|
2671
|
+
const body = process.env.FRONTGUARD_PR_BODY?.trim() ?? "";
|
|
2672
|
+
const headRef = process.env.BITBUCKET_PR_SOURCE_BRANCH?.trim() ?? "";
|
|
2673
|
+
const title = process.env.BITBUCKET_PR_TITLE?.trim() ?? "";
|
|
2674
|
+
let additions = 0;
|
|
2675
|
+
let deletions = 0;
|
|
2676
|
+
let files = [];
|
|
2677
|
+
const destRef = resolveCompareRef(cwd, dest);
|
|
2678
|
+
if (destRef) {
|
|
2679
|
+
try {
|
|
2680
|
+
const raw = gitTrimmed(cwd, ["diff", `${destRef}...HEAD`, "--numstat"]);
|
|
2681
|
+
const stat = parseNumstat(raw);
|
|
2682
|
+
additions = stat.additions;
|
|
2683
|
+
deletions = stat.deletions;
|
|
2684
|
+
files = stat.files;
|
|
2685
|
+
} catch {
|
|
2686
|
+
}
|
|
2726
2687
|
}
|
|
2727
|
-
const
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2688
|
+
const ai = parseAiDisclosure(body);
|
|
2689
|
+
return {
|
|
2690
|
+
number: Number.parseInt(prId, 10) || 0,
|
|
2691
|
+
title,
|
|
2692
|
+
body,
|
|
2693
|
+
baseRef: dest,
|
|
2694
|
+
headRef,
|
|
2695
|
+
additions,
|
|
2696
|
+
deletions,
|
|
2697
|
+
changedFiles: files.length,
|
|
2698
|
+
files,
|
|
2699
|
+
...ai
|
|
2733
2700
|
};
|
|
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
|
-
});
|
|
2701
|
+
}
|
|
2702
|
+
function readPrContext(cwd) {
|
|
2703
|
+
return buildBitbucketPrContext(cwd);
|
|
2761
2704
|
}
|
|
2762
2705
|
|
|
2763
2706
|
// node_modules/defu/dist/defu.mjs
|
|
@@ -2937,7 +2880,7 @@ async function loadConfig(cwd) {
|
|
|
2937
2880
|
let userFile = null;
|
|
2938
2881
|
for (const name of CONFIG_NAMES) {
|
|
2939
2882
|
const full = path4.join(cwd, name);
|
|
2940
|
-
if (!
|
|
2883
|
+
if (!fs2.existsSync(full)) continue;
|
|
2941
2884
|
try {
|
|
2942
2885
|
const mod = await importConfig(full);
|
|
2943
2886
|
userFile = normalizeExport(mod);
|
|
@@ -4084,7 +4027,7 @@ function runPrHygiene(config, pr) {
|
|
|
4084
4027
|
checkId: "pr-hygiene",
|
|
4085
4028
|
findings: [],
|
|
4086
4029
|
durationMs: Math.round(performance.now() - t0),
|
|
4087
|
-
skipped: "
|
|
4030
|
+
skipped: "no PR context (run in a Bitbucket PR pipeline with BITBUCKET_PR_ID, or set FRONTGUARD_PR_BODY for description checks)"
|
|
4088
4031
|
};
|
|
4089
4032
|
}
|
|
4090
4033
|
const findings = [];
|
|
@@ -4178,7 +4121,7 @@ function runPrSize(config, pr) {
|
|
|
4178
4121
|
checkId: "pr-size",
|
|
4179
4122
|
findings: [],
|
|
4180
4123
|
durationMs: Math.round(performance.now() - t0),
|
|
4181
|
-
skipped: "
|
|
4124
|
+
skipped: "no PR context (run in a Bitbucket pull-request pipeline so BITBUCKET_PR_* and git diff are available)"
|
|
4182
4125
|
};
|
|
4183
4126
|
}
|
|
4184
4127
|
const findings = [];
|
|
@@ -4248,13 +4191,18 @@ async function gitDiffForReview(cwd, baseRef, maxChars) {
|
|
|
4248
4191
|
}
|
|
4249
4192
|
}
|
|
4250
4193
|
async function resolveDiffBaseRef(cwd, fallback) {
|
|
4251
|
-
const
|
|
4252
|
-
if (
|
|
4253
|
-
const origin = `origin/${
|
|
4194
|
+
const bb = process.env.BITBUCKET_PR_DESTINATION_BRANCH?.trim();
|
|
4195
|
+
if (bb) {
|
|
4196
|
+
const origin = `origin/${bb}`;
|
|
4254
4197
|
try {
|
|
4255
4198
|
await W2("git", ["rev-parse", "--verify", origin], { nodeOptions: { cwd } });
|
|
4256
4199
|
return origin;
|
|
4257
4200
|
} catch {
|
|
4201
|
+
try {
|
|
4202
|
+
await W2("git", ["rev-parse", "--verify", bb], { nodeOptions: { cwd } });
|
|
4203
|
+
return bb;
|
|
4204
|
+
} catch {
|
|
4205
|
+
}
|
|
4258
4206
|
}
|
|
4259
4207
|
}
|
|
4260
4208
|
try {
|
|
@@ -4533,6 +4481,39 @@ async function gitOkQuick(cwd) {
|
|
|
4533
4481
|
function tokenizeCommand(cmd) {
|
|
4534
4482
|
return cmd.trim().split(/\s+/).map((t3) => t3.trim()).filter(Boolean);
|
|
4535
4483
|
}
|
|
4484
|
+
function npmScriptFromBuildCommand(cmd) {
|
|
4485
|
+
const t3 = cmd.trim();
|
|
4486
|
+
if (/^yarn\s+build\b/i.test(t3)) return "build";
|
|
4487
|
+
const np = /^(?:npm|pnpm)\s+run\s+(\S+)/i.exec(t3);
|
|
4488
|
+
if (np?.[1]) return np[1];
|
|
4489
|
+
const yr = /^yarn\s+run\s+(\S+)/i.exec(t3);
|
|
4490
|
+
if (yr?.[1]) return yr[1];
|
|
4491
|
+
return null;
|
|
4492
|
+
}
|
|
4493
|
+
async function bundleBuildPrecheck(cwd, buildCommand) {
|
|
4494
|
+
const script = npmScriptFromBuildCommand(buildCommand);
|
|
4495
|
+
if (!script) return { run: true };
|
|
4496
|
+
let scripts;
|
|
4497
|
+
try {
|
|
4498
|
+
const raw = await fs.readFile(path4.join(cwd, "package.json"), "utf8");
|
|
4499
|
+
const pkg = JSON.parse(raw);
|
|
4500
|
+
scripts = pkg.scripts;
|
|
4501
|
+
} catch {
|
|
4502
|
+
return {
|
|
4503
|
+
run: false,
|
|
4504
|
+
message: "Skipped bundle build \u2014 no readable package.json",
|
|
4505
|
+
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."
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
if (!scripts?.[script]) {
|
|
4509
|
+
return {
|
|
4510
|
+
run: false,
|
|
4511
|
+
message: `Skipped bundle build \u2014 no scripts.${script} in package.json`,
|
|
4512
|
+
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`)."
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
return { run: true };
|
|
4516
|
+
}
|
|
4536
4517
|
async function runBundle(cwd, config, stack) {
|
|
4537
4518
|
const t0 = performance.now();
|
|
4538
4519
|
const cfg = config.checks.bundle;
|
|
@@ -4552,6 +4533,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4552
4533
|
skipped: "skipped for React Native (configure web artifacts if needed)"
|
|
4553
4534
|
};
|
|
4554
4535
|
}
|
|
4536
|
+
const preFindings = [];
|
|
4555
4537
|
if (cfg.runBuild) {
|
|
4556
4538
|
const parts = tokenizeCommand(cfg.buildCommand);
|
|
4557
4539
|
if (parts.length === 0) {
|
|
@@ -4567,21 +4549,31 @@ async function runBundle(cwd, config, stack) {
|
|
|
4567
4549
|
durationMs: Math.round(performance.now() - t0)
|
|
4568
4550
|
};
|
|
4569
4551
|
}
|
|
4570
|
-
const
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4552
|
+
const pre = await bundleBuildPrecheck(cwd, cfg.buildCommand);
|
|
4553
|
+
if (!pre.run) {
|
|
4554
|
+
preFindings.push({
|
|
4555
|
+
id: "bundle-build-skipped",
|
|
4556
|
+
severity: "info",
|
|
4557
|
+
message: pre.message,
|
|
4558
|
+
detail: pre.detail
|
|
4559
|
+
});
|
|
4560
|
+
} else {
|
|
4561
|
+
const [bin, ...args] = parts;
|
|
4562
|
+
const res = await W2(bin, args, { nodeOptions: { cwd } });
|
|
4563
|
+
if ((res.exitCode ?? 0) !== 0) {
|
|
4564
|
+
return {
|
|
4565
|
+
checkId: "bundle",
|
|
4566
|
+
findings: [
|
|
4567
|
+
{
|
|
4568
|
+
id: "bundle-build",
|
|
4569
|
+
severity: gateSeverity4(cfg.gate),
|
|
4570
|
+
message: "Build command failed \u2014 cannot measure bundle",
|
|
4571
|
+
detail: [res.stdout, res.stderr].filter(Boolean).join("\n").slice(0, 8e3)
|
|
4572
|
+
}
|
|
4573
|
+
],
|
|
4574
|
+
durationMs: Math.round(performance.now() - t0)
|
|
4575
|
+
};
|
|
4576
|
+
}
|
|
4585
4577
|
}
|
|
4586
4578
|
}
|
|
4587
4579
|
const total = await sumGlobBytes(cwd, cfg.measureGlobs);
|
|
@@ -4589,6 +4581,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4589
4581
|
return {
|
|
4590
4582
|
checkId: "bundle",
|
|
4591
4583
|
findings: [
|
|
4584
|
+
...preFindings,
|
|
4592
4585
|
{
|
|
4593
4586
|
id: "bundle-empty",
|
|
4594
4587
|
severity: "info",
|
|
@@ -4600,7 +4593,7 @@ async function runBundle(cwd, config, stack) {
|
|
|
4600
4593
|
}
|
|
4601
4594
|
const baseRef = await gitOkQuick(cwd) ? await resolveDiffBaseRef(cwd, cfg.baselineRef) : null;
|
|
4602
4595
|
const baseline = await readBaseline(cwd, cfg.baselinePath, baseRef);
|
|
4603
|
-
const findings = [];
|
|
4596
|
+
const findings = [...preFindings];
|
|
4604
4597
|
const infoLines = [
|
|
4605
4598
|
`Measured ${total} bytes (${(total / 1024 / 1024).toFixed(2)} MiB)`,
|
|
4606
4599
|
baseline ? `Baseline from \`${cfg.baselinePath}\`: ${baseline.totalBytes} bytes` : `No baseline at \`${cfg.baselinePath}\` (commit a baseline JSON to compare)`
|
|
@@ -5079,8 +5072,8 @@ function buildHtmlReport(p2) {
|
|
|
5079
5072
|
} else {
|
|
5080
5073
|
for (const cid of checkOrder) {
|
|
5081
5074
|
const group = sortFindings(cwd, byCheck.get(cid));
|
|
5082
|
-
|
|
5083
|
-
warningsHtml +=
|
|
5075
|
+
const cards = group.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
5076
|
+
warningsHtml += `<details class="warn-check"><summary>${escapeHtml(cid)} <span class="count">(${group.length})</span></summary><div class="details-body warn-check-body">${cards}</div></details>`;
|
|
5084
5077
|
}
|
|
5085
5078
|
}
|
|
5086
5079
|
const infoHtml = infoItems.length === 0 ? '<p class="muted">No info notes.</p>' : infoItems.map(({ r: r4, f: f4 }) => renderFindingCard(cwd, r4, f4)).join("\n");
|
|
@@ -5114,8 +5107,11 @@ function buildHtmlReport(p2) {
|
|
|
5114
5107
|
}
|
|
5115
5108
|
h1 { font-size: 1.5rem; margin: 0 0 1rem; letter-spacing: -0.02em; }
|
|
5116
5109
|
h2 { font-size: 1.15rem; margin: 2rem 0 0.75rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.35rem; }
|
|
5117
|
-
|
|
5118
|
-
|
|
5110
|
+
details.warn-check { margin-bottom: 0.45rem; }
|
|
5111
|
+
details.warn-check:last-child { margin-bottom: 0; }
|
|
5112
|
+
details.warn-check > summary { color: var(--warn); font-size: 0.95rem; }
|
|
5113
|
+
details.warn-check .count { color: var(--muted); font-weight: normal; }
|
|
5114
|
+
.warn-check-body { padding-top: 0.35rem; }
|
|
5119
5115
|
h4 { font-size: 0.95rem; margin: 0 0 0.5rem; font-weight: 600; }
|
|
5120
5116
|
h5 { font-size: 0.8rem; margin: 0 0 0.35rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--accent); }
|
|
5121
5117
|
.badges { margin-bottom: 1.25rem; display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
|
|
@@ -5605,6 +5601,17 @@ function formatConsole(p2) {
|
|
|
5605
5601
|
return lines.join("\n");
|
|
5606
5602
|
}
|
|
5607
5603
|
|
|
5604
|
+
// src/lib/http-fetch.ts
|
|
5605
|
+
function getFetch() {
|
|
5606
|
+
const f4 = globalThis.fetch;
|
|
5607
|
+
if (typeof f4 !== "function") {
|
|
5608
|
+
throw new Error(
|
|
5609
|
+
"FrontGuard needs Node.js 18+ (global fetch). Use NODE_VERSION / image >= 18 in CI."
|
|
5610
|
+
);
|
|
5611
|
+
}
|
|
5612
|
+
return f4;
|
|
5613
|
+
}
|
|
5614
|
+
|
|
5608
5615
|
// src/llm/ollama.ts
|
|
5609
5616
|
async function callOllamaChat(opts) {
|
|
5610
5617
|
const fetch = getFetch();
|
|
@@ -5942,7 +5949,7 @@ async function runFrontGuard(opts) {
|
|
|
5942
5949
|
const config = await loadConfig(opts.cwd);
|
|
5943
5950
|
const mode = opts.enforce ? "enforce" : config.mode;
|
|
5944
5951
|
const stack = await detectStack(opts.cwd);
|
|
5945
|
-
const pr =
|
|
5952
|
+
const pr = readPrContext(opts.cwd);
|
|
5946
5953
|
const restrictFiles = pr?.files?.length ? pr.files : null;
|
|
5947
5954
|
const [
|
|
5948
5955
|
eslint,
|
|
@@ -6031,9 +6038,6 @@ FrontGuard: wrote Bitbucket PR comment text to ${abs} (${snippet.length} bytes).
|
|
|
6031
6038
|
g.stdout.write(report.consoleText + "\n\n");
|
|
6032
6039
|
g.stdout.write(report.markdown + "\n");
|
|
6033
6040
|
}
|
|
6034
|
-
if (opts.ci && g.env.GITHUB_TOKEN) {
|
|
6035
|
-
await upsertBriefComment(report.markdown);
|
|
6036
|
-
}
|
|
6037
6041
|
const hasBlock = results.some((r4) => r4.findings.some((f4) => f4.severity === "block"));
|
|
6038
6042
|
g.exitCode = mode === "enforce" && hasBlock ? 1 : 0;
|
|
6039
6043
|
}
|
|
@@ -6042,13 +6046,13 @@ FrontGuard: wrote Bitbucket PR comment text to ${abs} (${snippet.length} bytes).
|
|
|
6042
6046
|
var init2 = defineCommand({
|
|
6043
6047
|
meta: {
|
|
6044
6048
|
name: "init",
|
|
6045
|
-
description: "Add
|
|
6049
|
+
description: "Add Bitbucket pipeline example, pull_request_template.md, and frontguard.config.js"
|
|
6046
6050
|
},
|
|
6047
6051
|
run: async () => {
|
|
6048
6052
|
const cwd = g.cwd();
|
|
6049
6053
|
await initFrontGuard(cwd);
|
|
6050
6054
|
g.stdout.write(
|
|
6051
|
-
"FrontGuard initialized.\n\
|
|
6055
|
+
"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
6056
|
);
|
|
6053
6057
|
}
|
|
6054
6058
|
});
|
|
@@ -6058,11 +6062,6 @@ var run = defineCommand({
|
|
|
6058
6062
|
description: "Run checks and print the review brief"
|
|
6059
6063
|
},
|
|
6060
6064
|
args: {
|
|
6061
|
-
ci: {
|
|
6062
|
-
type: "boolean",
|
|
6063
|
-
description: "Upsert PR comment when GITHUB_TOKEN is available",
|
|
6064
|
-
default: false
|
|
6065
|
-
},
|
|
6066
6065
|
markdown: {
|
|
6067
6066
|
type: "boolean",
|
|
6068
6067
|
description: "Print markdown only",
|
|
@@ -6089,7 +6088,6 @@ var run = defineCommand({
|
|
|
6089
6088
|
run: async ({ args }) => {
|
|
6090
6089
|
await runFrontGuard({
|
|
6091
6090
|
cwd: g.cwd(),
|
|
6092
|
-
ci: Boolean(args.ci),
|
|
6093
6091
|
markdown: Boolean(args.markdown),
|
|
6094
6092
|
enforce: Boolean(args.enforce),
|
|
6095
6093
|
append: typeof args.append === "string" ? args.append : null,
|