@goflag/cli 0.1.3 → 0.1.4

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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > **A lean CLI that flags the site problems humans can't catch at scale.**
4
4
  > Point it at a URL and get three things: broken links, missing translation pages, and missing/misconfigured SEO metadata. JSON-first.
5
5
 
6
- [![node](https://img.shields.io/badge/node-%3E%3D20.11-339933?logo=nodedotjs&logoColor=white)](./.nvmrc)
6
+ [![node](https://img.shields.io/badge/node-%3E%3D20.11-339933?logo=nodedotjs&logoColor=white)](./package.json)
7
7
  [![license: MIT](https://img.shields.io/badge/license-MIT-green)](#license)
8
8
 
9
9
  Goflag crawls a site once and runs three focused static checks over it. It is deliberately small: no dashboard, no config system, no social-preview gallery. Just the findings, as a machine-readable report you can pipe, diff, or gate CI on.
@@ -23,12 +23,17 @@ SPA support is built in: pages that look client-rendered are re-rendered in head
23
23
 
24
24
  ```
25
25
  packages/cli/ @goflag/cli — the CLI (installs the `goflag` command)
26
- packages/next/ @goflag/next — the Next.js library (not built yet)
27
- apps/website/ documentation and marketing site (not built yet)
26
+ tools/name-holder/ the bare `goflag` name on npm, a signpost to @goflag/cli
28
27
  ```
29
28
 
30
- The library and the apps must not import from the CLI: the two products stay
31
- independently useful, and a lint rule enforces it rather than trusting memory.
29
+ That is the whole tree. `@goflag/next` (the Next.js library) and the
30
+ documentation site are planned but deliberately absent: the workspace globs
31
+ (`packages/*`, `apps/*`) are ready for them, and empty stubs for things that do
32
+ not exist are how a repository accumulates code nobody calls.
33
+
34
+ When they land, they must not import from the CLI: the two products stay
35
+ independently useful, and an ESLint rule enforces it rather than trusting
36
+ memory.
32
37
 
33
38
  ## Quick start
34
39
 
@@ -38,8 +43,10 @@ Requires Node `>=20.11`.
38
43
  npx @goflag/cli https://example.com
39
44
  ```
40
45
 
41
- The package is `@goflag/cli`; the command it installs is `goflag`. The bare
42
- `goflag` name on npm is not claimed yet — see the note in
46
+ The package is `@goflag/cli`; the command it installs is `goflag`. Everything
47
+ under the brand lives in the `@goflag` scope. The bare `goflag` name on npm is
48
+ claimed as a deprecated signpost pointing here — it carries no code, so install
49
+ the scoped package. The reasoning is in
43
50
  [docs/spec-and-lib-plan.md](docs/spec-and-lib-plan.md#une-marque-des-outils-nommés).
44
51
 
45
52
  You get a coloured terminal report and an exit code:
@@ -75,6 +82,8 @@ The terminal view is just a render of that JSON.
75
82
  --regressions-only Weaken the gate: fail only on NEW findings relative
76
83
  to --baseline. Requires --baseline.
77
84
  --baseline <file> Stored report to compare against.
85
+ --update-baseline Write this run to --baseline and exit 0, instead of
86
+ judging against it.
78
87
  --max-debt <n> Fail when the site carries more than <n> findings in
79
88
  total, new or known.
80
89
  --start <cmd> Boot <cmd>, wait for <url>, audit, then stop it.
@@ -90,7 +99,7 @@ The terminal view is just a render of that JSON.
90
99
  -v, --version Show the version.
91
100
  ```
92
101
 
93
- Headless mode needs Chromium. It ships as an optional dependency; if it's missing, install it with `npx playwright install chromium` (or just run with `--static`).
102
+ Headless mode needs Chromium. `playwright` is an optional peer dependency; if it's missing, install it with `npx playwright install chromium` (or just run with `--static`).
94
103
 
95
104
  ### Gate on regressions, not on perfection
96
105
 
@@ -99,7 +108,7 @@ yet — so it gets switched off, or ignored. Capture a baseline once and gate on
99
108
  "did this change make it worse?" instead:
100
109
 
101
110
  ```sh
102
- goflag https://example.com --report baseline.json --json # once
111
+ goflag https://example.com --baseline baseline.json --update-baseline # once
103
112
  goflag https://example.com --regressions-only --baseline baseline.json
104
113
  ```
105
114
 
@@ -129,6 +138,28 @@ goflag https://example.com --regressions-only --baseline baseline.json --max-deb
129
138
  Keeping `baseline.json` in the repository helps for the same reason: adding a
130
139
  finding to it then shows up in a diff someone reviews.
131
140
 
141
+ Capture it with the run that will later judge it, minus one flag:
142
+
143
+ ```sh
144
+ goflag https://example.com --baseline baseline.json --update-baseline
145
+ ```
146
+
147
+ That matters more than it looks. A baseline captured with different options than
148
+ the gate — a forgotten `--static`, another `--max-pages` — does not compare
149
+ cleanly, and the mismatch surfaces as findings that appear from nowhere. Using
150
+ the same command for both makes that impossible.
151
+
152
+ The same flag accepts findings you have decided to live with, and it says what
153
+ it accepted:
154
+
155
+ ```
156
+ goflag: baseline updated — 3 newly accepted, 0 resolved, 44 findings now
157
+ grandfathered in baseline.json
158
+ ```
159
+
160
+ Refreshing a baseline is taking on debt. It prints the number so the commit that
161
+ does it can be read as what it is.
162
+
132
163
  ### Gate a merge before it ships
133
164
 
134
165
  `--start` boots your app, waits for it to answer, audits it, and stops it — so
@@ -178,7 +209,7 @@ never means "nothing was wrong" by accident.
178
209
  ## Programmatic API
179
210
 
180
211
  ```ts
181
- import { runAudit } from "goflag";
212
+ import { runAudit } from "@goflag/cli";
182
213
 
183
214
  const report = await runAudit("https://example.com", { depth: 2 });
184
215
  console.log(report.summary); // { brokenLinks, missingTranslations, seoIssues, verdict }
@@ -188,6 +219,9 @@ console.log(report.summary); // { brokenLinks, missingTranslations, seoIssues, v
188
219
 
189
220
  ## Develop locally
190
221
 
222
+ Requires pnpm `11.18.0` (pinned via `packageManager` — `corepack enable` picks
223
+ it up). Run these from the repository root:
224
+
191
225
  ```sh
192
226
  pnpm install
193
227
  pnpm dev https://example.com # run the CLI from source (tsx)
@@ -200,7 +234,11 @@ pnpm typecheck # tsc --noEmit
200
234
  pnpm format # prettier
201
235
  ```
202
236
 
203
- The engine is framework-agnostic and lives in [`src/lib/core/`](src/lib/core) (crawl, fetch/extract, link audit, i18n) and [`src/lib/rules/`](src/lib/rules) (SEO checks). The CLI shell — orchestration, the `GoflagReport` schema, and rendering — lives in [`src/report/`](src/report) and [`src/cli.ts`](src/cli.ts).
237
+ `lint`, `format` and `format:check` run once over the whole repository; the
238
+ rest fan out across workspace packages with `pnpm -r`. To work on one package,
239
+ filter: `pnpm --filter @goflag/cli test`.
240
+
241
+ The engine is framework-agnostic and lives in [`packages/cli/src/lib/core/`](packages/cli/src/lib/core) (crawl, fetch/extract, link audit, i18n) and [`packages/cli/src/lib/rules/`](packages/cli/src/lib/rules) (SEO checks). The CLI shell — orchestration, the `GoflagReport` schema, and rendering — lives in [`packages/cli/src/report/`](packages/cli/src/report) and [`packages/cli/src/cli.ts`](packages/cli/src/cli.ts).
204
242
 
205
243
  ## License
206
244
 
package/dist/cli.js CHANGED
@@ -303,6 +303,10 @@ Options:
303
303
  Requires --baseline.
304
304
  --baseline <file> Stored report to compare against. Use with
305
305
  --regressions-only.
306
+ --update-baseline Write this run to --baseline and exit 0, instead of
307
+ judging against it. Use it to capture a baseline, or
308
+ to accept findings you have decided to live with \u2014
309
+ it says what it accepted.
306
310
  --max-debt <n> Fail when the site carries more than <n> findings in
307
311
  total, whether new or known. Lower it as you fix, to
308
312
  stop a baseline from fossilising.
@@ -335,6 +339,7 @@ function parseArgs(argv) {
335
339
  logMode: "compact",
336
340
  failOn: "warning",
337
341
  regressionsOnly: false,
342
+ updateBaseline: false,
338
343
  startTimeoutMs: 6e4,
339
344
  options: { include: [], exclude: [] }
340
345
  };
@@ -425,6 +430,9 @@ function parseArgs(argv) {
425
430
  parsed.baseline = next(i, arg);
426
431
  i++;
427
432
  break;
433
+ case "--update-baseline":
434
+ parsed.updateBaseline = true;
435
+ break;
428
436
  case "--regressions-only":
429
437
  parsed.regressionsOnly = true;
430
438
  break;
@@ -458,7 +466,10 @@ function parseArgs(argv) {
458
466
  else if (arg) throw new Error(`unexpected argument: ${arg}`);
459
467
  }
460
468
  }
461
- if (parsed.baseline && !parsed.regressionsOnly) {
469
+ if (parsed.updateBaseline && !parsed.baseline) {
470
+ throw new Error("--update-baseline needs a --baseline <file> to write to");
471
+ }
472
+ if (parsed.baseline && !parsed.regressionsOnly && !parsed.updateBaseline) {
462
473
  throw new Error(
463
474
  "--baseline weakens the gate, so it must be requested explicitly: add --regressions-only"
464
475
  );
@@ -546,17 +557,46 @@ ${HELP}
546
557
  await server?.stop();
547
558
  }
548
559
  if (args.baseline) {
549
- try {
550
- const { readFileSync } = await import("fs");
551
- const baseline = JSON.parse(readFileSync(args.baseline, "utf8"));
552
- report.diff = diffReports(baseline, report);
553
- } catch (err) {
554
- process.stderr.write(
555
- `goflag: could not read baseline ${args.baseline}: ${err.message}
560
+ const { existsSync, readFileSync } = await import("fs");
561
+ if (existsSync(args.baseline) || !args.updateBaseline) {
562
+ try {
563
+ const baseline = JSON.parse(readFileSync(args.baseline, "utf8"));
564
+ report.diff = diffReports(baseline, report);
565
+ } catch (err) {
566
+ process.stderr.write(
567
+ `goflag: could not read baseline ${args.baseline}: ${err.message}
568
+ `
569
+ );
570
+ return 2;
571
+ }
572
+ }
573
+ }
574
+ if (args.updateBaseline && args.baseline) {
575
+ const { writeFileSync } = await import("fs");
576
+ writeFileSync(args.baseline, `${JSON.stringify(report, null, 2)}
577
+ `, "utf8");
578
+ const total2 = totalFindings(report);
579
+ if (report.diff) {
580
+ const { added, resolved } = report.diff;
581
+ process.stdout.write(
582
+ `goflag: baseline updated \u2014 ${added.length} newly accepted, ${resolved.length} resolved, ${total2} findings now grandfathered in ${args.baseline}
583
+ `
584
+ );
585
+ if (added.length > 0) {
586
+ process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}
587
+ `);
588
+ }
589
+ } else {
590
+ process.stdout.write(
591
+ `goflag: baseline captured \u2014 ${total2} findings grandfathered in ${args.baseline}
556
592
  `
557
593
  );
558
- return 2;
559
594
  }
595
+ process.stdout.write(
596
+ `goflag: set --max-debt ${total2} to stop that number growing, and lower it as you fix.
597
+ `
598
+ );
599
+ return 0;
560
600
  }
561
601
  if (args.report) {
562
602
  const { writeFileSync } = await import("fs");
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/report/diff.ts","../src/report/render-diff.ts","../src/lib/runner/dev-server.ts","../src/cli-args.ts","../src/cli.ts"],"sourcesContent":["/**\n * Baseline comparison.\n *\n * Every finding already carries a `fingerprint` id derived from the *identity*\n * of the problem rather than its transient details, and page URLs are\n * normalised to origin-independent routes — so the same defect fingerprints\n * identically on localhost, staging and production. That machinery was built\n * for exactly this and had no consumer.\n *\n * What it changes is what the tool *is*. Without a baseline the CI gate is\n * binary: any finding fails, which is unusable on a site that is not clean\n * yet, so teams either never turn it on or learn to ignore it. With one, the\n * gate becomes \"no *new* findings\" — a promise a real codebase can keep while\n * the backlog is worked down.\n *\n * Pure function of two reports. No I/O; the CLI reads the baseline file.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport type { GoflagReport } from \"./types\";\n\n/** Which part of the report a finding came from. */\nexport type FindingKind =\n \"brokenLink\" | \"unreachablePage\" | \"translationHole\" | \"reciprocity\" | \"seo\" | \"site\";\n\n/** One finding, flattened out of its report section for comparison. */\nexport interface DiffEntry {\n id: string;\n kind: FindingKind;\n /**\n * Severity used for the exit-code decision. Rule findings carry their own;\n * the rest are mapped to match the verdict logic in `build.ts` — a broken\n * link or an unreachable page is an error, a translation gap is a warning.\n */\n severity: Severity;\n /** One-line description, for the rendered diff. */\n summary: string;\n /** Page the finding sits on, when it has one. */\n pageUrl?: string;\n}\n\nexport interface ReportDiff {\n /** Where and when the baseline was taken, echoed for the report header. */\n baseline: { url: string; finishedAt: string };\n /** Findings present now and absent from the baseline — what should fail CI. */\n added: DiffEntry[];\n /** Findings in the baseline that are gone. Worth showing: progress is data too. */\n resolved: DiffEntry[];\n /** Findings present in both. */\n unchanged: number;\n}\n\n/** Flatten every finding collection into one comparable list. */\nexport function collectFindings(report: GoflagReport): DiffEntry[] {\n const out: DiffEntry[] = [];\n\n for (const link of report.brokenLinks) {\n out.push({\n id: link.id,\n kind: \"brokenLink\",\n severity: \"error\",\n summary: `${link.verdict} link → ${link.href}`,\n pageUrl: link.pageUrl,\n });\n }\n\n for (const page of report.unreachablePages) {\n out.push({\n id: page.id,\n kind: \"unreachablePage\",\n severity: \"error\",\n summary: `${page.status || \"network error\"} on ${page.url}`,\n pageUrl: page.url,\n });\n }\n\n for (const hole of report.missingTranslations.holes) {\n out.push({\n id: hole.id,\n kind: \"translationHole\",\n severity: \"warning\",\n summary: `${hole.route} missing ${hole.missingLocales.join(\", \")}`,\n });\n }\n\n for (const issue of report.missingTranslations.reciprocity) {\n out.push({\n id: issue.id,\n kind: \"reciprocity\",\n severity: \"warning\",\n summary: `${issue.code} on ${issue.url}`,\n pageUrl: issue.url,\n });\n }\n\n for (const issue of report.seoIssues) {\n out.push({\n id: issue.id,\n kind: \"seo\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n for (const issue of report.siteIssues) {\n out.push({\n id: issue.id,\n kind: \"site\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n return out;\n}\n\n/** Deterministic ordering so a rendered diff is stable and reviewable. */\nconst SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 };\n\nfunction sortEntries(entries: DiffEntry[]): DiffEntry[] {\n return [...entries].sort(\n (a, b) =>\n SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||\n a.kind.localeCompare(b.kind) ||\n a.summary.localeCompare(b.summary),\n );\n}\n\n/** Total findings the site currently carries, new and known alike. */\nexport function totalFindings(report: GoflagReport): number {\n return collectFindings(report).length;\n}\n\n/** Compare a fresh report against a stored baseline. */\nexport function diffReports(baseline: GoflagReport, current: GoflagReport): ReportDiff {\n const before = new Map(collectFindings(baseline).map((e) => [e.id, e]));\n const now = new Map(collectFindings(current).map((e) => [e.id, e]));\n\n const added: DiffEntry[] = [];\n let unchanged = 0;\n for (const [id, entry] of now) {\n if (before.has(id)) unchanged += 1;\n else added.push(entry);\n }\n\n const resolved: DiffEntry[] = [];\n for (const [id, entry] of before) {\n if (!now.has(id)) resolved.push(entry);\n }\n\n return {\n baseline: { url: baseline.url, finishedAt: baseline.finishedAt },\n added: sortEntries(added),\n resolved: sortEntries(resolved),\n unchanged,\n };\n}\n\n/**\n * Exit code under `--regressions-only`: fail on findings that are *new* at or\n * above the threshold, or on a debt budget being exceeded.\n *\n * Deliberately blind to the overall verdict, unlike `exitCode`. A site with a\n * hundred known problems and no new ones passes — that is what the mode is\n * for, and why it has to be asked for by name.\n *\n * `maxDebt` is the counterweight. Gating on regressions alone lets a backlog\n * sit untouched forever behind a passing build; a ceiling you lower as you fix\n * is the only part of this design that makes the debt actually go down.\n */\nexport function diffExitCode(\n diff: ReportDiff,\n failOn: \"warning\" | \"error\" | \"never\",\n debt?: { total: number; max?: number },\n): number {\n if (debt?.max !== undefined && debt.total > debt.max) return 1;\n if (failOn === \"never\") return 0;\n const threshold = failOn === \"error\" ? 0 : 1;\n const blocking = diff.added.filter((e) => SEVERITY_RANK[e.severity] <= threshold);\n return blocking.length > 0 ? 1 : 0;\n}\n","/**\n * Render a `ReportDiff` for a terminal.\n *\n * Deliberately leads with what is new, because that is what fails the build\n * and what the reader has to act on. Resolved findings come second and are\n * kept — a run that only ever shows problems gives no signal that the backlog\n * is moving, which is how a gate stops being read.\n *\n * Pure function of the diff; no I/O.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport type { DiffEntry, ReportDiff } from \"./diff\";\n\ninterface RenderOptions {\n color?: boolean;\n /** Injected clock, so the rendered age is testable. */\n now?: number;\n}\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n cyan: \"\\x1b[36m\",\n};\n\nfunction paint(enabled: boolean) {\n return (code: string, text: string) => (enabled ? `${code}${text}${ANSI.reset}` : text);\n}\n\nfunction severityTag(c: (code: string, t: string) => string, severity: Severity): string {\n if (severity === \"error\") return c(ANSI.red, \"error\");\n if (severity === \"warning\") return c(ANSI.yellow, \"warn \");\n return c(ANSI.dim, \"info \");\n}\n\n/** How many entries to list before collapsing into a \"+N more\" line. */\nconst LIST_LIMIT = 20;\n\nfunction listEntries(\n entries: DiffEntry[],\n marker: string,\n markerColor: string,\n c: (code: string, t: string) => string,\n): string[] {\n const lines: string[] = [];\n for (const entry of entries.slice(0, LIST_LIMIT)) {\n // A broken link's identity is (page, target): the same dead URL referenced\n // from four pages is four findings, and without the page they render as\n // four identical lines that look like a rendering bug.\n const where =\n entry.pageUrl && !entry.summary.includes(entry.pageUrl)\n ? c(ANSI.dim, ` on ${entry.pageUrl}`)\n : \"\";\n lines.push(\n ` ${c(markerColor, marker)} ${severityTag(c, entry.severity)} ${c(ANSI.dim, entry.kind)} ${entry.summary}${where}`,\n );\n }\n if (entries.length > LIST_LIMIT) {\n lines.push(` ${c(ANSI.dim, `… +${entries.length - LIST_LIMIT} more`)}`);\n }\n return lines;\n}\n\n/** Whole days between the baseline timestamp and now, or null if unparseable. */\nfunction baselineAgeDays(finishedAt: string, now: number): number | null {\n const taken = Date.parse(finishedAt);\n if (Number.isNaN(taken)) return null;\n return Math.max(0, Math.floor((now - taken) / 86_400_000));\n}\n\nexport function renderDiffTerminal(diff: ReportDiff, options: RenderOptions = {}): string {\n const c = paint(options.color ?? false);\n const lines: string[] = [];\n const debt = diff.unchanged;\n\n // Never green, and never the word \"clean\". This mode passes builds on sites\n // with known defects; a green flag would say the opposite of what happened,\n // and a reader who learns to see green here stops reading the number beside\n // it. The debt is the headline, not a footnote.\n const verdict =\n diff.added.length > 0\n ? c(ANSI.red, \"REGRESSION\")\n : debt > 0\n ? c(ANSI.yellow, \"REGRESSION GATE\")\n : c(ANSI.green, \"CLEAN\");\n\n const age = baselineAgeDays(diff.baseline.finishedAt, options.now ?? Date.now());\n\n lines.push(\"\");\n lines.push(`${c(ANSI.bold, \"goflag\")} ${c(ANSI.dim, \"--regressions-only\")}`);\n lines.push(\n `${verdict} ${diff.added.length} new · ` +\n (debt > 0\n ? c(ANSI.bold, `${debt} known findings NOT gating this build`)\n : c(ANSI.dim, \"no known findings\")) +\n (diff.resolved.length > 0 ? c(ANSI.dim, ` · ${diff.resolved.length} resolved`) : \"\"),\n );\n lines.push(\n c(\n ANSI.dim,\n `baseline ${diff.baseline.url} — taken ${diff.baseline.finishedAt}` +\n (age === null ? \"\" : age === 0 ? \" (today)\" : ` (${age} day${age === 1 ? \"\" : \"s\"} ago)`),\n ),\n );\n lines.push(\"\");\n\n if (diff.added.length > 0) {\n lines.push(c(ANSI.bold, \"New findings\"));\n lines.push(...listEntries(diff.added, \"+\", ANSI.red, c));\n lines.push(\"\");\n }\n\n if (diff.resolved.length > 0) {\n lines.push(c(ANSI.bold, \"Resolved\"));\n lines.push(...listEntries(diff.resolved, \"-\", ANSI.green, c));\n lines.push(\"\");\n }\n\n if (diff.added.length === 0 && diff.resolved.length === 0) {\n lines.push(\n c(\n ANSI.dim,\n debt > 0\n ? `Nothing moved since the baseline — ${debt} findings still open. Run without --regressions-only to see them.`\n : \"Nothing moved since the baseline.\",\n ),\n );\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Boot-and-audit support for `--start`.\n *\n * goflag was only ever able to audit a deployed site. That makes it a post-hoc\n * auditor, and post-hoc auditors do not get run: by the time a finding appears,\n * the regression already shipped. `--start` closes that gap — boot the app,\n * wait until it answers, audit `localhost`, tear it down — so the same checks\n * can gate a merge instead of documenting a mistake.\n *\n * Deliberately dumb about readiness: rather than parsing a framework's stdout\n * for \"ready in 1.2s\" (every framework words it differently, and the wording\n * changes between minor versions), it polls the URL goflag is about to audit.\n * If that URL answers, the audit can proceed — which is the only definition of\n * \"ready\" that actually matters here.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\n\nexport interface StartedServer {\n /** Terminate the process group and resolve once it is gone. */\n stop: () => Promise<void>;\n}\n\nexport interface StartServerOptions {\n /** Shell command to run, e.g. `pnpm start`. */\n command: string;\n /**\n * Directory to run it in. Defaults to the process's own, which is right for\n * the usual `cd my-site && npx goflag …` — but not for a monorepo audited\n * from its root, where the app lives a few directories down.\n */\n cwd?: string;\n /** URL polled until it answers; normally the audit entry point. */\n url: string;\n /** Give up after this long. Defaults to 60s. */\n timeoutMs?: number;\n /** Poll interval. Defaults to 250ms. */\n intervalMs?: number;\n /** Called with each stdout/stderr chunk, for `--verbose`. */\n onOutput?: (chunk: string) => void;\n /** Accept self-signed TLS while polling (a local https dev server). */\n allowInsecureTls?: boolean;\n}\n\n/** True once the URL returns any HTTP response at all. */\nasync function answers(url: string, allowInsecureTls: boolean): Promise<boolean> {\n const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n if (allowInsecureTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n try {\n // Any status counts: a 404 on the entry path still proves the server is\n // up, and the audit itself is what judges the response.\n await fetch(url, { redirect: \"manual\" });\n return true;\n } catch {\n return false;\n } finally {\n if (allowInsecureTls) {\n if (previous === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n else process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;\n }\n }\n}\n\nfunction terminate(child: ChildProcess): Promise<void> {\n return new Promise((resolve) => {\n if (child.exitCode !== null || child.signalCode !== null) {\n resolve();\n return;\n }\n\n // Kill the whole group: `pnpm start` spawns the real server as a child,\n // and signalling only the wrapper orphans a process holding the port.\n const killGroup = (signal: NodeJS.Signals) => {\n try {\n if (child.pid) process.kill(-child.pid, signal);\n else child.kill(signal);\n } catch {\n // Already dead, or the group is gone — nothing left to do.\n }\n };\n\n const force = setTimeout(() => killGroup(\"SIGKILL\"), 5_000);\n child.once(\"exit\", () => {\n clearTimeout(force);\n resolve();\n });\n killGroup(\"SIGTERM\");\n });\n}\n\n/**\n * Spawn `command` and resolve once `url` answers.\n *\n * Rejects — after stopping the process — when the command exits early or the\n * timeout expires. Both are hard failures: auditing a server that never came\n * up would report a site-wide outage as a hundred SEO findings.\n */\nexport async function startServer(options: StartServerOptions): Promise<StartedServer> {\n const timeoutMs = options.timeoutMs ?? 60_000;\n const intervalMs = options.intervalMs ?? 250;\n const allowInsecureTls = options.allowInsecureTls === true;\n\n const cwd = options.cwd ?? process.cwd();\n const child = spawn(options.command, {\n cwd,\n shell: true,\n // Own process group, so `terminate` can take the whole tree down.\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let exited: { code: number | null; signal: NodeJS.Signals | null } | undefined;\n child.once(\"exit\", (code, signal) => {\n exited = { code, signal };\n });\n\n const tail: string[] = [];\n const record = (chunk: Buffer) => {\n const text = chunk.toString();\n options.onOutput?.(text);\n // Keep a short tail so a boot failure can be reported with its own words\n // instead of a bare \"command exited\".\n tail.push(text);\n if (tail.length > 40) tail.shift();\n };\n child.stdout?.on(\"data\", record);\n child.stderr?.on(\"data\", record);\n\n const server: StartedServer = { stop: () => terminate(child) };\n const deadline = Date.now() + timeoutMs;\n\n for (;;) {\n if (exited) {\n await server.stop();\n const why = exited.signal ? `signal ${exited.signal}` : `exit code ${exited.code}`;\n // Naming the directory is most of the diagnosis: a command that runs\n // fine by hand and dies here has almost always been started somewhere\n // other than where its package.json lives.\n throw new Error(\n `--start command exited before ${options.url} answered (${why}).\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `${tail.join(\"\").trimEnd()}`,\n );\n }\n if (await answers(options.url, allowInsecureTls)) return server;\n if (Date.now() >= deadline) {\n await server.stop();\n throw new Error(\n `--start command did not serve ${options.url} within ${timeoutMs}ms.\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `Raise --start-timeout, check the command listens on that URL, or point ` +\n `--start-cwd at the directory it should run in.`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n}\n","/**\n * CLI argument parsing — split out from `cli.ts` so it can be unit-tested\n * without importing the module's top-level `main()` side effect.\n *\n * `parseArgs` is a pure function of `argv` (plus the ambient TTY / NO_COLOR\n * signals used only for the default color choice). It throws a plain `Error`\n * with a user-facing message on malformed input; the CLI turns that into an\n * exit-code-2 with the help text.\n */\n\nimport type { AuditOptions, FailOn } from \"./report/build\";\nimport type { LogMode } from \"./report/logger\";\n\nexport const HELP = `goflag — audit a site for broken links, missing translations, and SEO metadata\n\nUsage:\n goflag <url> [options]\n\nOptions:\n --json Print the JSON report to stdout (nothing else).\n --summary, -s Roll findings up (dedup by link/rule/code). Pairs\n with --json for a compact, agent-friendly payload.\n --report <file> Write the (full) JSON report to <file>.\n --depth <n> Crawl depth (0 = entry page only). Default: 2.\n --max-pages <n> Hard cap on pages crawled. Default: 200.\n --include <glob> Only crawl paths matching <glob> (repeatable).\n --exclude <glob> Skip paths matching <glob> (repeatable).\n --locales <list> Comma-separated locales the site serves, e.g.\n \"fr,en,pt-br\". Authoritative: overrides what the\n sitemap and crawl suggest, and makes a locale the\n site does not serve yet show up as missing.\n --ignore-holes <glob> Route (locale-free) that is deliberately not\n translated everywhere, so its gaps are not reported\n as missing translations (repeatable).\n --no-sitemap Do not discover the sitemap; crawl from <url> only.\n Discovery is on by default because link-only crawling\n cannot find locales a site never links to.\n --fail-on <level> Exit 1 at or above this severity: warning (default),\n error, or never.\n --regressions-only Weaken the gate: fail only on findings that are NEW\n relative to --baseline. Known findings stop blocking\n the build — a green run no longer means a clean site.\n Requires --baseline.\n --baseline <file> Stored report to compare against. Use with\n --regressions-only.\n --max-debt <n> Fail when the site carries more than <n> findings in\n total, whether new or known. Lower it as you fix, to\n stop a baseline from fossilising.\n --start <cmd> Boot <cmd>, wait for <url> to answer, audit, then\n stop it. Use to gate a merge on the built app before\n it ships.\n --start-cwd <dir> Directory to run --start in. Defaults to the current\n one; set it when auditing a monorepo package from the\n repository root.\n --start-timeout <ms> How long to wait for --start to answer. Default: 60000.\n --no-external Do not probe off-origin (external) links.\n --static Static HTML only; never launch headless Chromium.\n --allow-insecure-tls Accept self-signed / invalid TLS (localhost, tunnels).\n --timeout <ms> Per-request timeout in ms. Default: 8000.\n --verbose, -V Log every page as it is analyzed.\n --quiet, -q Suppress the live progress output.\n --no-color Disable coloured output.\n -h, --help Show this help.\n -v, --version Show the version.\n\nExit codes: 0 clean, 1 findings found, 2 fatal error.`;\n\nexport interface ParsedArgs {\n url?: string;\n json: boolean;\n summary: boolean;\n report?: string;\n color: boolean;\n help: boolean;\n version: boolean;\n /** Live-progress verbosity for the logger. */\n logMode: LogMode;\n /** Severity at or above which the process exits 1. */\n failOn: FailOn;\n /** Path to a stored report to compare against (`--baseline`). */\n baseline?: string;\n /** Gate on new findings only, letting known ones through. */\n regressionsOnly: boolean;\n /** Hard ceiling on total findings, new or known. */\n maxDebt?: number;\n /** Command to boot before auditing, and stop afterwards (`--start`). */\n start?: string;\n /** How long to wait for `--start` to answer, in ms. */\n startTimeoutMs: number;\n /** Directory to run the `--start` command in. */\n startCwd?: string;\n options: AuditOptions;\n}\n\nconst FAIL_ON_LEVELS: readonly FailOn[] = [\"warning\", \"error\", \"never\"];\n\nexport function parseArgs(argv: string[]): ParsedArgs {\n const parsed: ParsedArgs = {\n json: false,\n summary: false,\n color: process.stdout.isTTY === true && !process.env.NO_COLOR,\n help: false,\n version: false,\n logMode: \"compact\",\n failOn: \"warning\",\n regressionsOnly: false,\n startTimeoutMs: 60_000,\n options: { include: [], exclude: [] },\n };\n\n const next = (i: number, flag: string): string => {\n const value = argv[i + 1];\n if (value === undefined) throw new Error(`${flag} requires a value`);\n return value;\n };\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n switch (arg) {\n case \"-h\":\n case \"--help\":\n parsed.help = true;\n break;\n case \"-v\":\n case \"--version\":\n parsed.version = true;\n break;\n case \"--json\":\n parsed.json = true;\n break;\n case \"-s\":\n case \"--summary\":\n parsed.summary = true;\n break;\n case \"--no-color\":\n parsed.color = false;\n break;\n case \"-V\":\n case \"--verbose\":\n parsed.logMode = \"verbose\";\n break;\n case \"-q\":\n case \"--quiet\":\n parsed.logMode = \"quiet\";\n break;\n case \"--static\":\n parsed.options.static = true;\n break;\n case \"--allow-insecure-tls\":\n parsed.options.allowInsecureTls = true;\n break;\n case \"--no-external\":\n parsed.options.checkExternal = false;\n break;\n case \"--no-sitemap\":\n parsed.options.noSitemap = true;\n break;\n case \"--ignore-holes\":\n parsed.options.ignoreHoles = [...(parsed.options.ignoreHoles ?? []), next(i, arg)];\n i++;\n break;\n case \"--locales\": {\n // Split on commas so `--locales fr,en` and repeated flags both work.\n const value = next(i, arg);\n const tags = value\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean);\n if (tags.length === 0) throw new Error(`${arg} expects at least one locale`);\n parsed.options.locales = [...(parsed.options.locales ?? []), ...tags];\n i++;\n break;\n }\n case \"--fail-on\": {\n const value = next(i, arg) as FailOn;\n if (!FAIL_ON_LEVELS.includes(value)) {\n throw new Error(`${arg} expects one of: ${FAIL_ON_LEVELS.join(\", \")}`);\n }\n parsed.failOn = value;\n i++;\n break;\n }\n case \"--start\":\n parsed.start = next(i, arg);\n i++;\n break;\n case \"--start-timeout\":\n parsed.startTimeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--start-cwd\":\n parsed.startCwd = next(i, arg);\n i++;\n break;\n case \"--report\":\n parsed.report = next(i, arg);\n i++;\n break;\n case \"--baseline\":\n parsed.baseline = next(i, arg);\n i++;\n break;\n case \"--regressions-only\":\n parsed.regressionsOnly = true;\n break;\n case \"--max-debt\":\n parsed.maxDebt = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--depth\":\n parsed.options.depth = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--max-pages\":\n parsed.options.maxPages = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--timeout\":\n parsed.options.timeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--include\":\n parsed.options.include!.push(next(i, arg));\n i++;\n break;\n case \"--exclude\":\n parsed.options.exclude!.push(next(i, arg));\n i++;\n break;\n default:\n if (arg && arg.startsWith(\"-\")) throw new Error(`unknown option: ${arg}`);\n if (arg && !parsed.url) parsed.url = arg;\n else if (arg) throw new Error(`unexpected argument: ${arg}`);\n }\n }\n\n // Each flag is meaningless without the other, and guessing which one the\n // caller meant would silently change how strict the build is.\n if (parsed.baseline && !parsed.regressionsOnly) {\n throw new Error(\n \"--baseline weakens the gate, so it must be requested explicitly: add --regressions-only\",\n );\n }\n if (parsed.regressionsOnly && !parsed.baseline) {\n throw new Error(\"--regressions-only needs a --baseline <file> to compare against\");\n }\n\n return parsed;\n}\n\nfunction toInt(value: string, flag: string): number {\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0) throw new Error(`${flag} expects a non-negative integer`);\n return n;\n}\n","/**\n * goflag CLI.\n *\n * goflag <url> [options]\n *\n * Crawls a site and reports broken links, missing translation pages, and\n * missing/misconfigured SEO metadata. The JSON report is the source of\n * truth; the terminal output is rendered from it.\n *\n * Exit codes:\n * 0 clean (green flag)\n * 1 findings present (yellow/red flag) — useful as a CI gate\n * 2 fatal (bad URL, unexpected error)\n */\n\nimport { runAudit, exitCode } from \"./report/build\";\nimport { diffReports, diffExitCode, totalFindings } from \"./report/diff\";\nimport { renderDiffTerminal } from \"./report/render-diff\";\nimport { startServer, type StartedServer } from \"./lib/runner/dev-server\";\nimport { renderTerminal } from \"./report/render-terminal\";\nimport { renderSummaryTerminal } from \"./report/render-summary\";\nimport { summarize } from \"./report/summarize\";\nimport { Logger } from \"./report/logger\";\nimport { HELP, parseArgs, type ParsedArgs } from \"./cli-args\";\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const { fileURLToPath } = await import(\"node:url\");\n const { dirname, join } = await import(\"node:path\");\n const here = dirname(fileURLToPath(import.meta.url));\n const pkg = JSON.parse(readFileSync(join(here, \"..\", \"package.json\"), \"utf8\"));\n return typeof pkg.version === \"string\" ? pkg.version : \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nasync function main(): Promise<number> {\n let args: ParsedArgs;\n try {\n args = parseArgs(process.argv.slice(2));\n } catch (err) {\n process.stderr.write(`goflag: ${(err as Error).message}\\n\\n${HELP}\\n`);\n return 2;\n }\n\n if (args.help) {\n process.stdout.write(`${HELP}\\n`);\n return 0;\n }\n if (args.version) {\n process.stdout.write(`${await readVersion()}\\n`);\n return 0;\n }\n if (!args.url) {\n process.stderr.write(`goflag: missing <url>\\n\\n${HELP}\\n`);\n return 2;\n }\n\n // Progress always goes to stderr, so stdout (JSON / rendered report)\n // stays clean and pipeable. Colour follows the stderr TTY.\n const logger = new Logger({\n stream: process.stderr,\n mode: args.logMode,\n color: process.stderr.isTTY === true && !process.env.NO_COLOR,\n });\n\n // `--start` owns a child process for the whole audit; it must be torn down\n // on every exit path, including a failed audit, or CI leaks a held port.\n let server: StartedServer | undefined;\n let report;\n try {\n if (args.start) {\n logger.note(`goflag: starting \\`${args.start}\\` …`);\n server = await startServer({\n command: args.start,\n cwd: args.startCwd,\n url: args.url,\n timeoutMs: args.startTimeoutMs,\n allowInsecureTls: args.options.allowInsecureTls,\n onOutput: args.logMode === \"verbose\" ? (chunk) => process.stderr.write(chunk) : undefined,\n });\n }\n logger.note(`goflag: auditing ${args.url} …`);\n report = await runAudit(args.url, { ...args.options, onProgress: logger.onProgress });\n logger.stop();\n } catch (err) {\n logger.stop();\n process.stderr.write(`goflag: ${(err as Error).message}\\n`);\n return 2;\n } finally {\n await server?.stop();\n }\n\n // A baseline changes the question from \"is this site clean?\" to \"did this\n // change make it worse?\" — the only one a site with a backlog can answer.\n if (args.baseline) {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const baseline = JSON.parse(readFileSync(args.baseline, \"utf8\"));\n report.diff = diffReports(baseline, report);\n } catch (err) {\n // Silently continuing would turn a typo'd path into a green build.\n process.stderr.write(\n `goflag: could not read baseline ${args.baseline}: ${(err as Error).message}\\n`,\n );\n return 2;\n }\n }\n\n if (args.report) {\n // The report file is always the full report — the source of truth a\n // baseline/diff can rely on, regardless of the --summary view choice.\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(args.report, `${JSON.stringify(report, null, 2)}\\n`, \"utf8\");\n process.stderr.write(`goflag: report written to ${args.report}\\n`);\n }\n\n // In baseline mode the diff *is* the answer. Printing the full report first\n // buries \"nothing changed\" under a hundred known findings — the reader stops\n // looking, which is precisely the failure the baseline exists to prevent.\n // The complete report stays available via --json and --report.\n if (report.diff && !args.json) {\n process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}\\n`);\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n }\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n if (args.summary) {\n const summary = summarize(report);\n process.stdout.write(\n args.json\n ? `${JSON.stringify(summary, null, 2)}\\n`\n : `${renderSummaryTerminal(summary, { color: args.color })}\\n`,\n );\n } else {\n process.stdout.write(\n args.json\n ? `${JSON.stringify(report, null, 2)}\\n`\n : `${renderTerminal(report, { color: args.color })}\\n`,\n );\n }\n\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n return 1;\n }\n\n if (report.diff) {\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n return exitCode(report, args.failOn);\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(`goflag: unexpected error: ${(err as Error).stack ?? err}\\n`);\n process.exit(2);\n });\n"],"mappings":"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","names":["total"]}
1
+ {"version":3,"sources":["../src/report/diff.ts","../src/report/render-diff.ts","../src/lib/runner/dev-server.ts","../src/cli-args.ts","../src/cli.ts"],"sourcesContent":["/**\n * Baseline comparison.\n *\n * Every finding already carries a `fingerprint` id derived from the *identity*\n * of the problem rather than its transient details, and page URLs are\n * normalised to origin-independent routes — so the same defect fingerprints\n * identically on localhost, staging and production. That machinery was built\n * for exactly this and had no consumer.\n *\n * What it changes is what the tool *is*. Without a baseline the CI gate is\n * binary: any finding fails, which is unusable on a site that is not clean\n * yet, so teams either never turn it on or learn to ignore it. With one, the\n * gate becomes \"no *new* findings\" — a promise a real codebase can keep while\n * the backlog is worked down.\n *\n * Pure function of two reports. No I/O; the CLI reads the baseline file.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport type { GoflagReport } from \"./types\";\n\n/** Which part of the report a finding came from. */\nexport type FindingKind =\n \"brokenLink\" | \"unreachablePage\" | \"translationHole\" | \"reciprocity\" | \"seo\" | \"site\";\n\n/** One finding, flattened out of its report section for comparison. */\nexport interface DiffEntry {\n id: string;\n kind: FindingKind;\n /**\n * Severity used for the exit-code decision. Rule findings carry their own;\n * the rest are mapped to match the verdict logic in `build.ts` — a broken\n * link or an unreachable page is an error, a translation gap is a warning.\n */\n severity: Severity;\n /** One-line description, for the rendered diff. */\n summary: string;\n /** Page the finding sits on, when it has one. */\n pageUrl?: string;\n}\n\nexport interface ReportDiff {\n /** Where and when the baseline was taken, echoed for the report header. */\n baseline: { url: string; finishedAt: string };\n /** Findings present now and absent from the baseline — what should fail CI. */\n added: DiffEntry[];\n /** Findings in the baseline that are gone. Worth showing: progress is data too. */\n resolved: DiffEntry[];\n /** Findings present in both. */\n unchanged: number;\n}\n\n/** Flatten every finding collection into one comparable list. */\nexport function collectFindings(report: GoflagReport): DiffEntry[] {\n const out: DiffEntry[] = [];\n\n for (const link of report.brokenLinks) {\n out.push({\n id: link.id,\n kind: \"brokenLink\",\n severity: \"error\",\n summary: `${link.verdict} link → ${link.href}`,\n pageUrl: link.pageUrl,\n });\n }\n\n for (const page of report.unreachablePages) {\n out.push({\n id: page.id,\n kind: \"unreachablePage\",\n severity: \"error\",\n summary: `${page.status || \"network error\"} on ${page.url}`,\n pageUrl: page.url,\n });\n }\n\n for (const hole of report.missingTranslations.holes) {\n out.push({\n id: hole.id,\n kind: \"translationHole\",\n severity: \"warning\",\n summary: `${hole.route} missing ${hole.missingLocales.join(\", \")}`,\n });\n }\n\n for (const issue of report.missingTranslations.reciprocity) {\n out.push({\n id: issue.id,\n kind: \"reciprocity\",\n severity: \"warning\",\n summary: `${issue.code} on ${issue.url}`,\n pageUrl: issue.url,\n });\n }\n\n for (const issue of report.seoIssues) {\n out.push({\n id: issue.id,\n kind: \"seo\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n for (const issue of report.siteIssues) {\n out.push({\n id: issue.id,\n kind: \"site\",\n severity: issue.severity,\n summary: `${issue.ruleId} on ${issue.pageUrl}`,\n pageUrl: issue.pageUrl,\n });\n }\n\n return out;\n}\n\n/** Deterministic ordering so a rendered diff is stable and reviewable. */\nconst SEVERITY_RANK: Record<Severity, number> = { error: 0, warning: 1, info: 2 };\n\nfunction sortEntries(entries: DiffEntry[]): DiffEntry[] {\n return [...entries].sort(\n (a, b) =>\n SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] ||\n a.kind.localeCompare(b.kind) ||\n a.summary.localeCompare(b.summary),\n );\n}\n\n/** Total findings the site currently carries, new and known alike. */\nexport function totalFindings(report: GoflagReport): number {\n return collectFindings(report).length;\n}\n\n/** Compare a fresh report against a stored baseline. */\nexport function diffReports(baseline: GoflagReport, current: GoflagReport): ReportDiff {\n const before = new Map(collectFindings(baseline).map((e) => [e.id, e]));\n const now = new Map(collectFindings(current).map((e) => [e.id, e]));\n\n const added: DiffEntry[] = [];\n let unchanged = 0;\n for (const [id, entry] of now) {\n if (before.has(id)) unchanged += 1;\n else added.push(entry);\n }\n\n const resolved: DiffEntry[] = [];\n for (const [id, entry] of before) {\n if (!now.has(id)) resolved.push(entry);\n }\n\n return {\n baseline: { url: baseline.url, finishedAt: baseline.finishedAt },\n added: sortEntries(added),\n resolved: sortEntries(resolved),\n unchanged,\n };\n}\n\n/**\n * Exit code under `--regressions-only`: fail on findings that are *new* at or\n * above the threshold, or on a debt budget being exceeded.\n *\n * Deliberately blind to the overall verdict, unlike `exitCode`. A site with a\n * hundred known problems and no new ones passes — that is what the mode is\n * for, and why it has to be asked for by name.\n *\n * `maxDebt` is the counterweight. Gating on regressions alone lets a backlog\n * sit untouched forever behind a passing build; a ceiling you lower as you fix\n * is the only part of this design that makes the debt actually go down.\n */\nexport function diffExitCode(\n diff: ReportDiff,\n failOn: \"warning\" | \"error\" | \"never\",\n debt?: { total: number; max?: number },\n): number {\n if (debt?.max !== undefined && debt.total > debt.max) return 1;\n if (failOn === \"never\") return 0;\n const threshold = failOn === \"error\" ? 0 : 1;\n const blocking = diff.added.filter((e) => SEVERITY_RANK[e.severity] <= threshold);\n return blocking.length > 0 ? 1 : 0;\n}\n","/**\n * Render a `ReportDiff` for a terminal.\n *\n * Deliberately leads with what is new, because that is what fails the build\n * and what the reader has to act on. Resolved findings come second and are\n * kept — a run that only ever shows problems gives no signal that the backlog\n * is moving, which is how a gate stops being read.\n *\n * Pure function of the diff; no I/O.\n */\n\nimport type { Severity } from \"../lib/core/types\";\nimport type { DiffEntry, ReportDiff } from \"./diff\";\n\ninterface RenderOptions {\n color?: boolean;\n /** Injected clock, so the rendered age is testable. */\n now?: number;\n}\n\nconst ANSI = {\n reset: \"\\x1b[0m\",\n bold: \"\\x1b[1m\",\n dim: \"\\x1b[2m\",\n red: \"\\x1b[31m\",\n green: \"\\x1b[32m\",\n yellow: \"\\x1b[33m\",\n cyan: \"\\x1b[36m\",\n};\n\nfunction paint(enabled: boolean) {\n return (code: string, text: string) => (enabled ? `${code}${text}${ANSI.reset}` : text);\n}\n\nfunction severityTag(c: (code: string, t: string) => string, severity: Severity): string {\n if (severity === \"error\") return c(ANSI.red, \"error\");\n if (severity === \"warning\") return c(ANSI.yellow, \"warn \");\n return c(ANSI.dim, \"info \");\n}\n\n/** How many entries to list before collapsing into a \"+N more\" line. */\nconst LIST_LIMIT = 20;\n\nfunction listEntries(\n entries: DiffEntry[],\n marker: string,\n markerColor: string,\n c: (code: string, t: string) => string,\n): string[] {\n const lines: string[] = [];\n for (const entry of entries.slice(0, LIST_LIMIT)) {\n // A broken link's identity is (page, target): the same dead URL referenced\n // from four pages is four findings, and without the page they render as\n // four identical lines that look like a rendering bug.\n const where =\n entry.pageUrl && !entry.summary.includes(entry.pageUrl)\n ? c(ANSI.dim, ` on ${entry.pageUrl}`)\n : \"\";\n lines.push(\n ` ${c(markerColor, marker)} ${severityTag(c, entry.severity)} ${c(ANSI.dim, entry.kind)} ${entry.summary}${where}`,\n );\n }\n if (entries.length > LIST_LIMIT) {\n lines.push(` ${c(ANSI.dim, `… +${entries.length - LIST_LIMIT} more`)}`);\n }\n return lines;\n}\n\n/** Whole days between the baseline timestamp and now, or null if unparseable. */\nfunction baselineAgeDays(finishedAt: string, now: number): number | null {\n const taken = Date.parse(finishedAt);\n if (Number.isNaN(taken)) return null;\n return Math.max(0, Math.floor((now - taken) / 86_400_000));\n}\n\nexport function renderDiffTerminal(diff: ReportDiff, options: RenderOptions = {}): string {\n const c = paint(options.color ?? false);\n const lines: string[] = [];\n const debt = diff.unchanged;\n\n // Never green, and never the word \"clean\". This mode passes builds on sites\n // with known defects; a green flag would say the opposite of what happened,\n // and a reader who learns to see green here stops reading the number beside\n // it. The debt is the headline, not a footnote.\n const verdict =\n diff.added.length > 0\n ? c(ANSI.red, \"REGRESSION\")\n : debt > 0\n ? c(ANSI.yellow, \"REGRESSION GATE\")\n : c(ANSI.green, \"CLEAN\");\n\n const age = baselineAgeDays(diff.baseline.finishedAt, options.now ?? Date.now());\n\n lines.push(\"\");\n lines.push(`${c(ANSI.bold, \"goflag\")} ${c(ANSI.dim, \"--regressions-only\")}`);\n lines.push(\n `${verdict} ${diff.added.length} new · ` +\n (debt > 0\n ? c(ANSI.bold, `${debt} known findings NOT gating this build`)\n : c(ANSI.dim, \"no known findings\")) +\n (diff.resolved.length > 0 ? c(ANSI.dim, ` · ${diff.resolved.length} resolved`) : \"\"),\n );\n lines.push(\n c(\n ANSI.dim,\n `baseline ${diff.baseline.url} — taken ${diff.baseline.finishedAt}` +\n (age === null ? \"\" : age === 0 ? \" (today)\" : ` (${age} day${age === 1 ? \"\" : \"s\"} ago)`),\n ),\n );\n lines.push(\"\");\n\n if (diff.added.length > 0) {\n lines.push(c(ANSI.bold, \"New findings\"));\n lines.push(...listEntries(diff.added, \"+\", ANSI.red, c));\n lines.push(\"\");\n }\n\n if (diff.resolved.length > 0) {\n lines.push(c(ANSI.bold, \"Resolved\"));\n lines.push(...listEntries(diff.resolved, \"-\", ANSI.green, c));\n lines.push(\"\");\n }\n\n if (diff.added.length === 0 && diff.resolved.length === 0) {\n lines.push(\n c(\n ANSI.dim,\n debt > 0\n ? `Nothing moved since the baseline — ${debt} findings still open. Run without --regressions-only to see them.`\n : \"Nothing moved since the baseline.\",\n ),\n );\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * Boot-and-audit support for `--start`.\n *\n * goflag was only ever able to audit a deployed site. That makes it a post-hoc\n * auditor, and post-hoc auditors do not get run: by the time a finding appears,\n * the regression already shipped. `--start` closes that gap — boot the app,\n * wait until it answers, audit `localhost`, tear it down — so the same checks\n * can gate a merge instead of documenting a mistake.\n *\n * Deliberately dumb about readiness: rather than parsing a framework's stdout\n * for \"ready in 1.2s\" (every framework words it differently, and the wording\n * changes between minor versions), it polls the URL goflag is about to audit.\n * If that URL answers, the audit can proceed — which is the only definition of\n * \"ready\" that actually matters here.\n */\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\n\nexport interface StartedServer {\n /** Terminate the process group and resolve once it is gone. */\n stop: () => Promise<void>;\n}\n\nexport interface StartServerOptions {\n /** Shell command to run, e.g. `pnpm start`. */\n command: string;\n /**\n * Directory to run it in. Defaults to the process's own, which is right for\n * the usual `cd my-site && npx goflag …` — but not for a monorepo audited\n * from its root, where the app lives a few directories down.\n */\n cwd?: string;\n /** URL polled until it answers; normally the audit entry point. */\n url: string;\n /** Give up after this long. Defaults to 60s. */\n timeoutMs?: number;\n /** Poll interval. Defaults to 250ms. */\n intervalMs?: number;\n /** Called with each stdout/stderr chunk, for `--verbose`. */\n onOutput?: (chunk: string) => void;\n /** Accept self-signed TLS while polling (a local https dev server). */\n allowInsecureTls?: boolean;\n}\n\n/** True once the URL returns any HTTP response at all. */\nasync function answers(url: string, allowInsecureTls: boolean): Promise<boolean> {\n const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n if (allowInsecureTls) process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\";\n try {\n // Any status counts: a 404 on the entry path still proves the server is\n // up, and the audit itself is what judges the response.\n await fetch(url, { redirect: \"manual\" });\n return true;\n } catch {\n return false;\n } finally {\n if (allowInsecureTls) {\n if (previous === undefined) delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;\n else process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;\n }\n }\n}\n\nfunction terminate(child: ChildProcess): Promise<void> {\n return new Promise((resolve) => {\n if (child.exitCode !== null || child.signalCode !== null) {\n resolve();\n return;\n }\n\n // Kill the whole group: `pnpm start` spawns the real server as a child,\n // and signalling only the wrapper orphans a process holding the port.\n const killGroup = (signal: NodeJS.Signals) => {\n try {\n if (child.pid) process.kill(-child.pid, signal);\n else child.kill(signal);\n } catch {\n // Already dead, or the group is gone — nothing left to do.\n }\n };\n\n const force = setTimeout(() => killGroup(\"SIGKILL\"), 5_000);\n child.once(\"exit\", () => {\n clearTimeout(force);\n resolve();\n });\n killGroup(\"SIGTERM\");\n });\n}\n\n/**\n * Spawn `command` and resolve once `url` answers.\n *\n * Rejects — after stopping the process — when the command exits early or the\n * timeout expires. Both are hard failures: auditing a server that never came\n * up would report a site-wide outage as a hundred SEO findings.\n */\nexport async function startServer(options: StartServerOptions): Promise<StartedServer> {\n const timeoutMs = options.timeoutMs ?? 60_000;\n const intervalMs = options.intervalMs ?? 250;\n const allowInsecureTls = options.allowInsecureTls === true;\n\n const cwd = options.cwd ?? process.cwd();\n const child = spawn(options.command, {\n cwd,\n shell: true,\n // Own process group, so `terminate` can take the whole tree down.\n detached: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n\n let exited: { code: number | null; signal: NodeJS.Signals | null } | undefined;\n child.once(\"exit\", (code, signal) => {\n exited = { code, signal };\n });\n\n const tail: string[] = [];\n const record = (chunk: Buffer) => {\n const text = chunk.toString();\n options.onOutput?.(text);\n // Keep a short tail so a boot failure can be reported with its own words\n // instead of a bare \"command exited\".\n tail.push(text);\n if (tail.length > 40) tail.shift();\n };\n child.stdout?.on(\"data\", record);\n child.stderr?.on(\"data\", record);\n\n const server: StartedServer = { stop: () => terminate(child) };\n const deadline = Date.now() + timeoutMs;\n\n for (;;) {\n if (exited) {\n await server.stop();\n const why = exited.signal ? `signal ${exited.signal}` : `exit code ${exited.code}`;\n // Naming the directory is most of the diagnosis: a command that runs\n // fine by hand and dies here has almost always been started somewhere\n // other than where its package.json lives.\n throw new Error(\n `--start command exited before ${options.url} answered (${why}).\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `${tail.join(\"\").trimEnd()}`,\n );\n }\n if (await answers(options.url, allowInsecureTls)) return server;\n if (Date.now() >= deadline) {\n await server.stop();\n throw new Error(\n `--start command did not serve ${options.url} within ${timeoutMs}ms.\\n` +\n ` command: ${options.command}\\n` +\n ` in: ${cwd}\\n` +\n `Raise --start-timeout, check the command listens on that URL, or point ` +\n `--start-cwd at the directory it should run in.`,\n );\n }\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n}\n","/**\n * CLI argument parsing — split out from `cli.ts` so it can be unit-tested\n * without importing the module's top-level `main()` side effect.\n *\n * `parseArgs` is a pure function of `argv` (plus the ambient TTY / NO_COLOR\n * signals used only for the default color choice). It throws a plain `Error`\n * with a user-facing message on malformed input; the CLI turns that into an\n * exit-code-2 with the help text.\n */\n\nimport type { AuditOptions, FailOn } from \"./report/build\";\nimport type { LogMode } from \"./report/logger\";\n\nexport const HELP = `goflag — audit a site for broken links, missing translations, and SEO metadata\n\nUsage:\n goflag <url> [options]\n\nOptions:\n --json Print the JSON report to stdout (nothing else).\n --summary, -s Roll findings up (dedup by link/rule/code). Pairs\n with --json for a compact, agent-friendly payload.\n --report <file> Write the (full) JSON report to <file>.\n --depth <n> Crawl depth (0 = entry page only). Default: 2.\n --max-pages <n> Hard cap on pages crawled. Default: 200.\n --include <glob> Only crawl paths matching <glob> (repeatable).\n --exclude <glob> Skip paths matching <glob> (repeatable).\n --locales <list> Comma-separated locales the site serves, e.g.\n \"fr,en,pt-br\". Authoritative: overrides what the\n sitemap and crawl suggest, and makes a locale the\n site does not serve yet show up as missing.\n --ignore-holes <glob> Route (locale-free) that is deliberately not\n translated everywhere, so its gaps are not reported\n as missing translations (repeatable).\n --no-sitemap Do not discover the sitemap; crawl from <url> only.\n Discovery is on by default because link-only crawling\n cannot find locales a site never links to.\n --fail-on <level> Exit 1 at or above this severity: warning (default),\n error, or never.\n --regressions-only Weaken the gate: fail only on findings that are NEW\n relative to --baseline. Known findings stop blocking\n the build — a green run no longer means a clean site.\n Requires --baseline.\n --baseline <file> Stored report to compare against. Use with\n --regressions-only.\n --update-baseline Write this run to --baseline and exit 0, instead of\n judging against it. Use it to capture a baseline, or\n to accept findings you have decided to live with —\n it says what it accepted.\n --max-debt <n> Fail when the site carries more than <n> findings in\n total, whether new or known. Lower it as you fix, to\n stop a baseline from fossilising.\n --start <cmd> Boot <cmd>, wait for <url> to answer, audit, then\n stop it. Use to gate a merge on the built app before\n it ships.\n --start-cwd <dir> Directory to run --start in. Defaults to the current\n one; set it when auditing a monorepo package from the\n repository root.\n --start-timeout <ms> How long to wait for --start to answer. Default: 60000.\n --no-external Do not probe off-origin (external) links.\n --static Static HTML only; never launch headless Chromium.\n --allow-insecure-tls Accept self-signed / invalid TLS (localhost, tunnels).\n --timeout <ms> Per-request timeout in ms. Default: 8000.\n --verbose, -V Log every page as it is analyzed.\n --quiet, -q Suppress the live progress output.\n --no-color Disable coloured output.\n -h, --help Show this help.\n -v, --version Show the version.\n\nExit codes: 0 clean, 1 findings found, 2 fatal error.`;\n\nexport interface ParsedArgs {\n url?: string;\n json: boolean;\n summary: boolean;\n report?: string;\n color: boolean;\n help: boolean;\n version: boolean;\n /** Live-progress verbosity for the logger. */\n logMode: LogMode;\n /** Severity at or above which the process exits 1. */\n failOn: FailOn;\n /** Path to a stored report to compare against (`--baseline`). */\n baseline?: string;\n /** Gate on new findings only, letting known ones through. */\n regressionsOnly: boolean;\n /** Write the run to `--baseline` rather than judging against it. */\n updateBaseline: boolean;\n /** Hard ceiling on total findings, new or known. */\n maxDebt?: number;\n /** Command to boot before auditing, and stop afterwards (`--start`). */\n start?: string;\n /** How long to wait for `--start` to answer, in ms. */\n startTimeoutMs: number;\n /** Directory to run the `--start` command in. */\n startCwd?: string;\n options: AuditOptions;\n}\n\nconst FAIL_ON_LEVELS: readonly FailOn[] = [\"warning\", \"error\", \"never\"];\n\nexport function parseArgs(argv: string[]): ParsedArgs {\n const parsed: ParsedArgs = {\n json: false,\n summary: false,\n color: process.stdout.isTTY === true && !process.env.NO_COLOR,\n help: false,\n version: false,\n logMode: \"compact\",\n failOn: \"warning\",\n regressionsOnly: false,\n updateBaseline: false,\n startTimeoutMs: 60_000,\n options: { include: [], exclude: [] },\n };\n\n const next = (i: number, flag: string): string => {\n const value = argv[i + 1];\n if (value === undefined) throw new Error(`${flag} requires a value`);\n return value;\n };\n\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n switch (arg) {\n case \"-h\":\n case \"--help\":\n parsed.help = true;\n break;\n case \"-v\":\n case \"--version\":\n parsed.version = true;\n break;\n case \"--json\":\n parsed.json = true;\n break;\n case \"-s\":\n case \"--summary\":\n parsed.summary = true;\n break;\n case \"--no-color\":\n parsed.color = false;\n break;\n case \"-V\":\n case \"--verbose\":\n parsed.logMode = \"verbose\";\n break;\n case \"-q\":\n case \"--quiet\":\n parsed.logMode = \"quiet\";\n break;\n case \"--static\":\n parsed.options.static = true;\n break;\n case \"--allow-insecure-tls\":\n parsed.options.allowInsecureTls = true;\n break;\n case \"--no-external\":\n parsed.options.checkExternal = false;\n break;\n case \"--no-sitemap\":\n parsed.options.noSitemap = true;\n break;\n case \"--ignore-holes\":\n parsed.options.ignoreHoles = [...(parsed.options.ignoreHoles ?? []), next(i, arg)];\n i++;\n break;\n case \"--locales\": {\n // Split on commas so `--locales fr,en` and repeated flags both work.\n const value = next(i, arg);\n const tags = value\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean);\n if (tags.length === 0) throw new Error(`${arg} expects at least one locale`);\n parsed.options.locales = [...(parsed.options.locales ?? []), ...tags];\n i++;\n break;\n }\n case \"--fail-on\": {\n const value = next(i, arg) as FailOn;\n if (!FAIL_ON_LEVELS.includes(value)) {\n throw new Error(`${arg} expects one of: ${FAIL_ON_LEVELS.join(\", \")}`);\n }\n parsed.failOn = value;\n i++;\n break;\n }\n case \"--start\":\n parsed.start = next(i, arg);\n i++;\n break;\n case \"--start-timeout\":\n parsed.startTimeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--start-cwd\":\n parsed.startCwd = next(i, arg);\n i++;\n break;\n case \"--report\":\n parsed.report = next(i, arg);\n i++;\n break;\n case \"--baseline\":\n parsed.baseline = next(i, arg);\n i++;\n break;\n case \"--update-baseline\":\n parsed.updateBaseline = true;\n break;\n case \"--regressions-only\":\n parsed.regressionsOnly = true;\n break;\n case \"--max-debt\":\n parsed.maxDebt = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--depth\":\n parsed.options.depth = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--max-pages\":\n parsed.options.maxPages = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--timeout\":\n parsed.options.timeoutMs = toInt(next(i, arg), arg);\n i++;\n break;\n case \"--include\":\n parsed.options.include!.push(next(i, arg));\n i++;\n break;\n case \"--exclude\":\n parsed.options.exclude!.push(next(i, arg));\n i++;\n break;\n default:\n if (arg && arg.startsWith(\"-\")) throw new Error(`unknown option: ${arg}`);\n if (arg && !parsed.url) parsed.url = arg;\n else if (arg) throw new Error(`unexpected argument: ${arg}`);\n }\n }\n\n // Each flag is meaningless without the other, and guessing which one the\n // caller meant would silently change how strict the build is.\n if (parsed.updateBaseline && !parsed.baseline) {\n throw new Error(\"--update-baseline needs a --baseline <file> to write to\");\n }\n // Capturing a baseline is not gating against one, so the explicit opt-in that\n // --regressions-only exists to force does not apply.\n if (parsed.baseline && !parsed.regressionsOnly && !parsed.updateBaseline) {\n throw new Error(\n \"--baseline weakens the gate, so it must be requested explicitly: add --regressions-only\",\n );\n }\n if (parsed.regressionsOnly && !parsed.baseline) {\n throw new Error(\"--regressions-only needs a --baseline <file> to compare against\");\n }\n\n return parsed;\n}\n\nfunction toInt(value: string, flag: string): number {\n const n = Number(value);\n if (!Number.isInteger(n) || n < 0) throw new Error(`${flag} expects a non-negative integer`);\n return n;\n}\n","/**\n * goflag CLI.\n *\n * goflag <url> [options]\n *\n * Crawls a site and reports broken links, missing translation pages, and\n * missing/misconfigured SEO metadata. The JSON report is the source of\n * truth; the terminal output is rendered from it.\n *\n * Exit codes:\n * 0 clean (green flag)\n * 1 findings present (yellow/red flag) — useful as a CI gate\n * 2 fatal (bad URL, unexpected error)\n */\n\nimport { runAudit, exitCode } from \"./report/build\";\nimport { diffReports, diffExitCode, totalFindings } from \"./report/diff\";\nimport { renderDiffTerminal } from \"./report/render-diff\";\nimport { startServer, type StartedServer } from \"./lib/runner/dev-server\";\nimport { renderTerminal } from \"./report/render-terminal\";\nimport { renderSummaryTerminal } from \"./report/render-summary\";\nimport { summarize } from \"./report/summarize\";\nimport { Logger } from \"./report/logger\";\nimport { HELP, parseArgs, type ParsedArgs } from \"./cli-args\";\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const { fileURLToPath } = await import(\"node:url\");\n const { dirname, join } = await import(\"node:path\");\n const here = dirname(fileURLToPath(import.meta.url));\n const pkg = JSON.parse(readFileSync(join(here, \"..\", \"package.json\"), \"utf8\"));\n return typeof pkg.version === \"string\" ? pkg.version : \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nasync function main(): Promise<number> {\n let args: ParsedArgs;\n try {\n args = parseArgs(process.argv.slice(2));\n } catch (err) {\n process.stderr.write(`goflag: ${(err as Error).message}\\n\\n${HELP}\\n`);\n return 2;\n }\n\n if (args.help) {\n process.stdout.write(`${HELP}\\n`);\n return 0;\n }\n if (args.version) {\n process.stdout.write(`${await readVersion()}\\n`);\n return 0;\n }\n if (!args.url) {\n process.stderr.write(`goflag: missing <url>\\n\\n${HELP}\\n`);\n return 2;\n }\n\n // Progress always goes to stderr, so stdout (JSON / rendered report)\n // stays clean and pipeable. Colour follows the stderr TTY.\n const logger = new Logger({\n stream: process.stderr,\n mode: args.logMode,\n color: process.stderr.isTTY === true && !process.env.NO_COLOR,\n });\n\n // `--start` owns a child process for the whole audit; it must be torn down\n // on every exit path, including a failed audit, or CI leaks a held port.\n let server: StartedServer | undefined;\n let report;\n try {\n if (args.start) {\n logger.note(`goflag: starting \\`${args.start}\\` …`);\n server = await startServer({\n command: args.start,\n cwd: args.startCwd,\n url: args.url,\n timeoutMs: args.startTimeoutMs,\n allowInsecureTls: args.options.allowInsecureTls,\n onOutput: args.logMode === \"verbose\" ? (chunk) => process.stderr.write(chunk) : undefined,\n });\n }\n logger.note(`goflag: auditing ${args.url} …`);\n report = await runAudit(args.url, { ...args.options, onProgress: logger.onProgress });\n logger.stop();\n } catch (err) {\n logger.stop();\n process.stderr.write(`goflag: ${(err as Error).message}\\n`);\n return 2;\n } finally {\n await server?.stop();\n }\n\n // A baseline changes the question from \"is this site clean?\" to \"did this\n // change make it worse?\" — the only one a site with a backlog can answer.\n if (args.baseline) {\n const { existsSync, readFileSync } = await import(\"node:fs\");\n // Capturing a baseline for the first time is the one case where the file\n // is legitimately absent. Everywhere else a missing baseline is a typo,\n // and continuing would turn it into a green build.\n if (existsSync(args.baseline) || !args.updateBaseline) {\n try {\n const baseline = JSON.parse(readFileSync(args.baseline, \"utf8\"));\n report.diff = diffReports(baseline, report);\n } catch (err) {\n process.stderr.write(\n `goflag: could not read baseline ${args.baseline}: ${(err as Error).message}\\n`,\n );\n return 2;\n }\n }\n }\n\n // Writing the baseline is accepting everything in it. The one thing this\n // must not do is accept it quietly: a counter that drops without explanation\n // reads as \"the problem went away\".\n if (args.updateBaseline && args.baseline) {\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(args.baseline, `${JSON.stringify(report, null, 2)}\\n`, \"utf8\");\n const total = totalFindings(report);\n if (report.diff) {\n const { added, resolved } = report.diff;\n process.stdout.write(\n `goflag: baseline updated — ${added.length} newly accepted, ${resolved.length} resolved, ` +\n `${total} findings now grandfathered in ${args.baseline}\\n`,\n );\n if (added.length > 0) {\n process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}\\n`);\n }\n } else {\n process.stdout.write(\n `goflag: baseline captured — ${total} findings grandfathered in ${args.baseline}\\n`,\n );\n }\n process.stdout.write(\n `goflag: set --max-debt ${total} to stop that number growing, and lower it as you fix.\\n`,\n );\n return 0;\n }\n\n if (args.report) {\n // The report file is always the full report — the source of truth a\n // baseline/diff can rely on, regardless of the --summary view choice.\n const { writeFileSync } = await import(\"node:fs\");\n writeFileSync(args.report, `${JSON.stringify(report, null, 2)}\\n`, \"utf8\");\n process.stderr.write(`goflag: report written to ${args.report}\\n`);\n }\n\n // In baseline mode the diff *is* the answer. Printing the full report first\n // buries \"nothing changed\" under a hundred known findings — the reader stops\n // looking, which is precisely the failure the baseline exists to prevent.\n // The complete report stays available via --json and --report.\n if (report.diff && !args.json) {\n process.stdout.write(`${renderDiffTerminal(report.diff, { color: args.color })}\\n`);\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n }\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n if (args.summary) {\n const summary = summarize(report);\n process.stdout.write(\n args.json\n ? `${JSON.stringify(summary, null, 2)}\\n`\n : `${renderSummaryTerminal(summary, { color: args.color })}\\n`,\n );\n } else {\n process.stdout.write(\n args.json\n ? `${JSON.stringify(report, null, 2)}\\n`\n : `${renderTerminal(report, { color: args.color })}\\n`,\n );\n }\n\n const total = totalFindings(report);\n if (args.maxDebt !== undefined && total > args.maxDebt) {\n process.stderr.write(\n `goflag: ${total} findings exceeds the --max-debt budget of ${args.maxDebt}.\\n`,\n );\n return 1;\n }\n\n if (report.diff) {\n return diffExitCode(report.diff, args.failOn, { total, max: args.maxDebt });\n }\n\n return exitCode(report, args.failOn);\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((err) => {\n process.stderr.write(`goflag: unexpected error: ${(err as Error).stack ?? err}\\n`);\n process.exit(2);\n });\n"],"mappings":"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","names":["total"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goflag/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "A lean CLI that audits a site for broken links, missing translation pages, and missing/misconfigured SEO metadata. JSON-first.",
6
6
  "keywords": [