@goflag/cli 0.2.7 → 0.2.9
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 +34 -8
- package/dist/{chunk-UOKYUKHQ.js → chunk-TAZEAIFI.js} +977 -37
- package/dist/chunk-TAZEAIFI.js.map +1 -0
- package/dist/cli.js +547 -235
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +93 -3
- package/dist/index.js +1 -1
- package/flags.json +302 -0
- package/package.json +5 -2
- package/rules.json +366 -2
- package/dist/chunk-UOKYUKHQ.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
renderTerminal,
|
|
13
13
|
runAudit,
|
|
14
14
|
summarize
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-TAZEAIFI.js";
|
|
16
16
|
|
|
17
17
|
// src/report/diff.ts
|
|
18
18
|
function collectFindings(report) {
|
|
@@ -287,97 +287,505 @@ Raise --start-timeout, check the command listens on that URL, or point --start-c
|
|
|
287
287
|
}
|
|
288
288
|
}
|
|
289
289
|
|
|
290
|
-
// src/
|
|
291
|
-
var PROFILE_HELP = PROFILE_NAMES.map(
|
|
292
|
-
(name) => ` ${name} \u2014 ${PROFILES[name].description}`
|
|
293
|
-
).join("\n");
|
|
294
|
-
var HELP = `goflag \u2014 audit a site for broken links, missing translations, and SEO metadata
|
|
295
|
-
|
|
296
|
-
Usage:
|
|
297
|
-
goflag <url> [options]
|
|
298
|
-
goflag rules Print the rule catalogue as JSON and exit. No crawl
|
|
299
|
-
and no network: every rule with its severity, its
|
|
300
|
-
rigor and the documents it cites.
|
|
301
|
-
|
|
302
|
-
Options:
|
|
303
|
-
--json Print the JSON report to stdout (nothing else).
|
|
304
|
-
--summary, -s Roll findings up (dedup by link/rule/code). Pairs
|
|
305
|
-
with --json for a compact, agent-friendly payload.
|
|
306
|
-
--report <file> Write the (full) JSON report to <file>.
|
|
307
|
-
--depth <n> How far to follow links out of each page (0 = follow
|
|
308
|
-
none). Sitemap URLs are seeded regardless, so --depth 0
|
|
309
|
-
alone is not "entry page only" \u2014 add --no-sitemap for
|
|
310
|
-
that. Default: 2.
|
|
311
|
-
--max-pages <n> Page budget for the crawl. A hard cap under
|
|
312
|
-
--coverage all; under structural coverage the selection
|
|
313
|
-
wins and the budget is max(<n>, selected + 5).
|
|
314
|
-
Default: 200.
|
|
315
|
-
--coverage <mode> How to choose which pages to audit.
|
|
316
|
-
"structural" (default when a sitemap is found) keeps
|
|
317
|
-
every standalone page and samples families of pages
|
|
318
|
-
built from one template. "all" audits everything the
|
|
319
|
-
sitemap lists, up to --max-pages.
|
|
320
|
-
--include <glob> Only crawl paths matching <glob> (repeatable).
|
|
321
|
-
--exclude <glob> Skip paths matching <glob> (repeatable).
|
|
322
|
-
--locales <list> Comma-separated locales the site serves, e.g.
|
|
323
|
-
"fr,en,pt-br". Unioned with what the sitemap shows,
|
|
324
|
-
never substituted for it, and the only way to make a
|
|
325
|
-
locale the site does not serve yet show up as missing.
|
|
326
|
-
Also folds /en/\u2026 and /fr/\u2026 into one route family for
|
|
327
|
-
structural coverage, so pass it on any locale-prefixed
|
|
328
|
-
site.
|
|
329
|
-
--ignore-holes <glob> Route (locale-free) that is deliberately not
|
|
330
|
-
translated everywhere, so its gaps are not reported
|
|
331
|
-
as missing translations (repeatable).
|
|
332
|
-
--no-sitemap Do not discover the sitemap; crawl from <url> only.
|
|
333
|
-
Discovery is on by default because link-only crawling
|
|
334
|
-
cannot find locales a site never links to.
|
|
335
|
-
--profile <name> Policy overlay on the rule set \u2014 it changes how
|
|
336
|
-
loudly a rule fires, never what it observes:
|
|
337
|
-
${PROFILE_HELP}
|
|
338
|
-
--conformance Report EVERY rule's status on every page (pass /
|
|
339
|
-
fail / warn / na), not just the violations. Answers
|
|
340
|
-
"where do we stand against the catalog?"; pairs with
|
|
341
|
-
--json.
|
|
342
|
-
--advisories Attach the prose rules \u2014 the judgment calls goflag
|
|
343
|
-
refuses to fake \u2014 each with the observed facts an
|
|
344
|
-
agent needs to judge it. Never affects the gate.
|
|
345
|
-
--fail-on <level> Exit 1 at or above this severity: warning (default),
|
|
346
|
-
error, or never.
|
|
347
|
-
--regressions-only Weaken the gate: fail only on findings that are NEW
|
|
348
|
-
relative to --baseline. Known findings stop blocking
|
|
349
|
-
the build \u2014 a green run no longer means a clean site.
|
|
350
|
-
Requires --baseline.
|
|
351
|
-
--baseline <file> Stored report to compare against. Use with
|
|
352
|
-
--regressions-only.
|
|
353
|
-
--update-baseline Write this run to --baseline and exit 0, instead of
|
|
354
|
-
judging against it. Use it to capture a baseline, or
|
|
355
|
-
to accept findings you have decided to live with \u2014
|
|
356
|
-
it says what it accepted.
|
|
357
|
-
--max-debt <n> Fail when the site carries more than <n> findings in
|
|
358
|
-
total, whether new or known. Lower it as you fix, to
|
|
359
|
-
stop a baseline from fossilising.
|
|
360
|
-
--start <cmd> Boot <cmd>, wait for <url> to answer, audit, then
|
|
361
|
-
stop it. Use to gate a merge on the built app before
|
|
362
|
-
it ships.
|
|
363
|
-
--start-cwd <dir> Directory to run --start in. Defaults to the current
|
|
364
|
-
one; set it when auditing a monorepo package from the
|
|
365
|
-
repository root.
|
|
366
|
-
--start-timeout <ms> How long to wait for --start to answer. Default: 60000.
|
|
367
|
-
--no-external Do not probe off-origin (external) links.
|
|
368
|
-
--static Static HTML only; never launch headless Chromium.
|
|
369
|
-
--allow-insecure-tls Accept self-signed / invalid TLS (localhost, tunnels).
|
|
370
|
-
--timeout <ms> Per-request timeout in ms, for page fetches and link
|
|
371
|
-
probes alike. Unset, the defaults differ: 8000 for link
|
|
372
|
-
probes, 15000 for page fetches.
|
|
373
|
-
--verbose, -V Log every page as it is analyzed.
|
|
374
|
-
--quiet, -q Suppress the live progress output.
|
|
375
|
-
--no-color Disable coloured output.
|
|
376
|
-
-h, --help Show this help.
|
|
377
|
-
-v, --version Show the version.
|
|
378
|
-
|
|
379
|
-
Exit codes: 0 clean, 1 findings found, 2 fatal error.`;
|
|
290
|
+
// src/lib/flags/registry.ts
|
|
380
291
|
var FAIL_ON_LEVELS = ["warning", "error", "never"];
|
|
292
|
+
function toInt(value, flag) {
|
|
293
|
+
const n = Number(value);
|
|
294
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`${flag} expects a non-negative integer`);
|
|
295
|
+
return n;
|
|
296
|
+
}
|
|
297
|
+
var FLAGS = [
|
|
298
|
+
{
|
|
299
|
+
name: "--json",
|
|
300
|
+
label: "--json",
|
|
301
|
+
group: "output",
|
|
302
|
+
help: ["Print the JSON report to stdout (nothing else)."],
|
|
303
|
+
takesValue: false,
|
|
304
|
+
apply: ({ parsed }) => {
|
|
305
|
+
parsed.json = true;
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "--summary",
|
|
310
|
+
short: "-s",
|
|
311
|
+
label: "--summary, -s",
|
|
312
|
+
group: "output",
|
|
313
|
+
help: [
|
|
314
|
+
"Roll findings up (dedup by link/rule/code). Pairs",
|
|
315
|
+
"with --json for a compact, agent-friendly payload."
|
|
316
|
+
],
|
|
317
|
+
takesValue: false,
|
|
318
|
+
apply: ({ parsed }) => {
|
|
319
|
+
parsed.summary = true;
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
name: "--report",
|
|
324
|
+
arg: "<file>",
|
|
325
|
+
label: "--report <file>",
|
|
326
|
+
group: "output",
|
|
327
|
+
help: ["Write the (full) JSON report to <file>."],
|
|
328
|
+
takesValue: true,
|
|
329
|
+
apply: ({ parsed, value }) => {
|
|
330
|
+
parsed.report = value;
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "--depth",
|
|
335
|
+
arg: "<n>",
|
|
336
|
+
label: "--depth <n>",
|
|
337
|
+
group: "crawl",
|
|
338
|
+
default: "2",
|
|
339
|
+
help: [
|
|
340
|
+
"How far to follow links out of each page (0 = follow",
|
|
341
|
+
"none). Sitemap URLs are seeded regardless, so --depth 0",
|
|
342
|
+
'alone is not "entry page only" \u2014 add --no-sitemap for',
|
|
343
|
+
"that. Default: 2."
|
|
344
|
+
],
|
|
345
|
+
takesValue: true,
|
|
346
|
+
apply: ({ parsed, value, flag }) => {
|
|
347
|
+
parsed.options.depth = toInt(value, flag);
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "--max-pages",
|
|
352
|
+
arg: "<n>",
|
|
353
|
+
label: "--max-pages <n>",
|
|
354
|
+
group: "crawl",
|
|
355
|
+
default: "200",
|
|
356
|
+
help: [
|
|
357
|
+
"Page budget for the crawl. A hard cap under",
|
|
358
|
+
"--coverage all; under structural coverage the selection",
|
|
359
|
+
"wins and the budget is max(<n>, selected + 5).",
|
|
360
|
+
"Default: 200."
|
|
361
|
+
],
|
|
362
|
+
takesValue: true,
|
|
363
|
+
apply: ({ parsed, value, flag }) => {
|
|
364
|
+
parsed.options.maxPages = toInt(value, flag);
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: "--coverage",
|
|
369
|
+
arg: "<mode>",
|
|
370
|
+
label: "--coverage <mode>",
|
|
371
|
+
group: "crawl",
|
|
372
|
+
default: "structural when a sitemap is found, all otherwise",
|
|
373
|
+
help: [
|
|
374
|
+
"How to choose which pages to audit.",
|
|
375
|
+
'"structural" (default when a sitemap is found) keeps',
|
|
376
|
+
"every standalone page and samples families of pages",
|
|
377
|
+
'built from one template. "all" audits everything the',
|
|
378
|
+
"sitemap lists, up to --max-pages."
|
|
379
|
+
],
|
|
380
|
+
takesValue: true,
|
|
381
|
+
apply: ({ parsed, value, flag }) => {
|
|
382
|
+
if (value !== "structural" && value !== "all") {
|
|
383
|
+
throw new Error(`${flag} takes "structural" or "all", not ${JSON.stringify(value)}`);
|
|
384
|
+
}
|
|
385
|
+
parsed.options.coverage = value;
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: "--include",
|
|
390
|
+
arg: "<glob>",
|
|
391
|
+
label: "--include <glob>",
|
|
392
|
+
group: "crawl",
|
|
393
|
+
help: ["Only crawl paths matching <glob> (repeatable)."],
|
|
394
|
+
takesValue: true,
|
|
395
|
+
apply: ({ parsed, value }) => {
|
|
396
|
+
parsed.options.include.push(value);
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "--exclude",
|
|
401
|
+
arg: "<glob>",
|
|
402
|
+
label: "--exclude <glob>",
|
|
403
|
+
group: "crawl",
|
|
404
|
+
help: ["Skip paths matching <glob> (repeatable)."],
|
|
405
|
+
takesValue: true,
|
|
406
|
+
apply: ({ parsed, value }) => {
|
|
407
|
+
parsed.options.exclude.push(value);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
name: "--locales",
|
|
412
|
+
arg: "<list>",
|
|
413
|
+
label: "--locales <list>",
|
|
414
|
+
group: "i18n",
|
|
415
|
+
help: [
|
|
416
|
+
"Comma-separated locales the site serves, e.g.",
|
|
417
|
+
'"fr,en,pt-br". Unioned with what the sitemap shows,',
|
|
418
|
+
"never substituted for it, and the only way to make a",
|
|
419
|
+
"locale the site does not serve yet show up as missing.",
|
|
420
|
+
"Also folds /en/\u2026 and /fr/\u2026 into one route family for",
|
|
421
|
+
"structural coverage, so pass it on any locale-prefixed",
|
|
422
|
+
"site."
|
|
423
|
+
],
|
|
424
|
+
takesValue: true,
|
|
425
|
+
apply: ({ parsed, value, flag }) => {
|
|
426
|
+
const tags = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
427
|
+
if (tags.length === 0) throw new Error(`${flag} expects at least one locale`);
|
|
428
|
+
parsed.options.locales = [...parsed.options.locales ?? [], ...tags];
|
|
429
|
+
}
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "--ignore-holes",
|
|
433
|
+
arg: "<glob>",
|
|
434
|
+
label: "--ignore-holes <glob>",
|
|
435
|
+
group: "i18n",
|
|
436
|
+
help: [
|
|
437
|
+
"Route (locale-free) that is deliberately not",
|
|
438
|
+
"translated everywhere, so its gaps are not reported",
|
|
439
|
+
"as missing translations (repeatable)."
|
|
440
|
+
],
|
|
441
|
+
takesValue: true,
|
|
442
|
+
apply: ({ parsed, value }) => {
|
|
443
|
+
parsed.options.ignoreHoles = [...parsed.options.ignoreHoles ?? [], value];
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: "--no-sitemap",
|
|
448
|
+
label: "--no-sitemap",
|
|
449
|
+
group: "crawl",
|
|
450
|
+
help: [
|
|
451
|
+
"Do not discover the sitemap; crawl from <url> only.",
|
|
452
|
+
"Discovery is on by default because link-only crawling",
|
|
453
|
+
"cannot find locales a site never links to."
|
|
454
|
+
],
|
|
455
|
+
takesValue: false,
|
|
456
|
+
apply: ({ parsed }) => {
|
|
457
|
+
parsed.options.noSitemap = true;
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
name: "--profile",
|
|
462
|
+
arg: "<name>",
|
|
463
|
+
label: "--profile <name>",
|
|
464
|
+
group: "gate",
|
|
465
|
+
default: "default",
|
|
466
|
+
help: [
|
|
467
|
+
"Policy overlay on the rule set \u2014 it changes how",
|
|
468
|
+
"loudly a rule fires, never what it observes:"
|
|
469
|
+
],
|
|
470
|
+
dynamicTail: () => PROFILE_NAMES.map((n) => `${n} \u2014 ${PROFILES[n].description}`),
|
|
471
|
+
takesValue: true,
|
|
472
|
+
apply: ({ parsed, value, flag }) => {
|
|
473
|
+
if (!PROFILE_NAMES.includes(value)) {
|
|
474
|
+
throw new Error(`${flag} expects one of: ${PROFILE_NAMES.join(", ")}`);
|
|
475
|
+
}
|
|
476
|
+
parsed.options.profile = value;
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "--conformance",
|
|
481
|
+
label: "--conformance",
|
|
482
|
+
group: "output",
|
|
483
|
+
help: [
|
|
484
|
+
"Report EVERY rule's status on every page (pass /",
|
|
485
|
+
"fail / warn / na), not just the violations. Answers",
|
|
486
|
+
'"where do we stand against the catalog?"; pairs with',
|
|
487
|
+
"--json."
|
|
488
|
+
],
|
|
489
|
+
takesValue: false,
|
|
490
|
+
apply: ({ parsed }) => {
|
|
491
|
+
parsed.options.conformance = true;
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
name: "--advisories",
|
|
496
|
+
label: "--advisories",
|
|
497
|
+
group: "output",
|
|
498
|
+
help: [
|
|
499
|
+
"Attach the prose rules \u2014 the judgment calls goflag",
|
|
500
|
+
"refuses to fake \u2014 each with the observed facts an",
|
|
501
|
+
"agent needs to judge it. Never affects the gate."
|
|
502
|
+
],
|
|
503
|
+
takesValue: false,
|
|
504
|
+
apply: ({ parsed }) => {
|
|
505
|
+
parsed.options.advisories = true;
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: "--fail-on",
|
|
510
|
+
arg: "<level>",
|
|
511
|
+
label: "--fail-on <level>",
|
|
512
|
+
group: "gate",
|
|
513
|
+
default: "warning",
|
|
514
|
+
help: ["Exit 1 at or above this severity: warning (default),", "error, or never."],
|
|
515
|
+
takesValue: true,
|
|
516
|
+
apply: ({ parsed, value, flag }) => {
|
|
517
|
+
const level = value;
|
|
518
|
+
if (!FAIL_ON_LEVELS.includes(level)) {
|
|
519
|
+
throw new Error(`${flag} expects one of: ${FAIL_ON_LEVELS.join(", ")}`);
|
|
520
|
+
}
|
|
521
|
+
parsed.failOn = level;
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
name: "--regressions-only",
|
|
526
|
+
label: "--regressions-only",
|
|
527
|
+
group: "gate",
|
|
528
|
+
requires: "--baseline",
|
|
529
|
+
help: [
|
|
530
|
+
"Weaken the gate: fail only on findings that are NEW",
|
|
531
|
+
"relative to --baseline. Known findings stop blocking",
|
|
532
|
+
"the build \u2014 a green run no longer means a clean site.",
|
|
533
|
+
"Requires --baseline."
|
|
534
|
+
],
|
|
535
|
+
takesValue: false,
|
|
536
|
+
apply: ({ parsed }) => {
|
|
537
|
+
parsed.regressionsOnly = true;
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: "--baseline",
|
|
542
|
+
arg: "<file>",
|
|
543
|
+
label: "--baseline <file>",
|
|
544
|
+
group: "gate",
|
|
545
|
+
help: ["Stored report to compare against. Use with", "--regressions-only."],
|
|
546
|
+
takesValue: true,
|
|
547
|
+
apply: ({ parsed, value }) => {
|
|
548
|
+
parsed.baseline = value;
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
name: "--update-baseline",
|
|
553
|
+
label: "--update-baseline",
|
|
554
|
+
group: "gate",
|
|
555
|
+
requires: "--baseline",
|
|
556
|
+
help: [
|
|
557
|
+
"Write this run to --baseline and exit 0, instead of",
|
|
558
|
+
"judging against it. Use it to capture a baseline, or",
|
|
559
|
+
"to accept findings you have decided to live with \u2014",
|
|
560
|
+
"it says what it accepted."
|
|
561
|
+
],
|
|
562
|
+
takesValue: false,
|
|
563
|
+
apply: ({ parsed }) => {
|
|
564
|
+
parsed.updateBaseline = true;
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
name: "--max-debt",
|
|
569
|
+
arg: "<n>",
|
|
570
|
+
label: "--max-debt <n>",
|
|
571
|
+
group: "gate",
|
|
572
|
+
help: [
|
|
573
|
+
"Fail when the site carries more than <n> findings in",
|
|
574
|
+
"total, whether new or known. Lower it as you fix, to",
|
|
575
|
+
"stop a baseline from fossilising."
|
|
576
|
+
],
|
|
577
|
+
takesValue: true,
|
|
578
|
+
apply: ({ parsed, value, flag }) => {
|
|
579
|
+
parsed.maxDebt = toInt(value, flag);
|
|
580
|
+
}
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "--start",
|
|
584
|
+
arg: "<cmd>",
|
|
585
|
+
label: "--start <cmd>",
|
|
586
|
+
group: "boot",
|
|
587
|
+
help: [
|
|
588
|
+
"Boot <cmd>, wait for <url> to answer, audit, then",
|
|
589
|
+
"stop it. Use to gate a merge on the built app before",
|
|
590
|
+
"it ships."
|
|
591
|
+
],
|
|
592
|
+
takesValue: true,
|
|
593
|
+
apply: ({ parsed, value }) => {
|
|
594
|
+
parsed.start = value;
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "--start-cwd",
|
|
599
|
+
arg: "<dir>",
|
|
600
|
+
label: "--start-cwd <dir>",
|
|
601
|
+
group: "boot",
|
|
602
|
+
default: "the current directory",
|
|
603
|
+
help: [
|
|
604
|
+
"Directory to run --start in. Defaults to the current",
|
|
605
|
+
"one; set it when auditing a monorepo package from the",
|
|
606
|
+
"repository root."
|
|
607
|
+
],
|
|
608
|
+
takesValue: true,
|
|
609
|
+
apply: ({ parsed, value }) => {
|
|
610
|
+
parsed.startCwd = value;
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: "--start-timeout",
|
|
615
|
+
arg: "<ms>",
|
|
616
|
+
label: "--start-timeout <ms>",
|
|
617
|
+
group: "boot",
|
|
618
|
+
default: "60000",
|
|
619
|
+
help: ["How long to wait for --start to answer. Default: 60000."],
|
|
620
|
+
takesValue: true,
|
|
621
|
+
apply: ({ parsed, value, flag }) => {
|
|
622
|
+
parsed.startTimeoutMs = toInt(value, flag);
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: "--no-external",
|
|
627
|
+
label: "--no-external",
|
|
628
|
+
group: "fetch",
|
|
629
|
+
help: ["Do not probe off-origin (external) links."],
|
|
630
|
+
takesValue: false,
|
|
631
|
+
apply: ({ parsed }) => {
|
|
632
|
+
parsed.options.checkExternal = false;
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "--static",
|
|
637
|
+
label: "--static",
|
|
638
|
+
group: "fetch",
|
|
639
|
+
help: ["Static HTML only; never launch headless Chromium."],
|
|
640
|
+
takesValue: false,
|
|
641
|
+
apply: ({ parsed }) => {
|
|
642
|
+
parsed.options.static = true;
|
|
643
|
+
}
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "--allow-insecure-tls",
|
|
647
|
+
label: "--allow-insecure-tls",
|
|
648
|
+
group: "fetch",
|
|
649
|
+
help: ["Accept self-signed / invalid TLS (localhost, tunnels)."],
|
|
650
|
+
takesValue: false,
|
|
651
|
+
apply: ({ parsed }) => {
|
|
652
|
+
parsed.options.allowInsecureTls = true;
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "--timeout",
|
|
657
|
+
arg: "<ms>",
|
|
658
|
+
label: "--timeout <ms>",
|
|
659
|
+
group: "fetch",
|
|
660
|
+
default: "8000 for link probes, 15000 for page fetches",
|
|
661
|
+
help: [
|
|
662
|
+
"Per-request timeout in ms, for page fetches and link",
|
|
663
|
+
"probes alike. Unset, the defaults differ: 8000 for link",
|
|
664
|
+
"probes, 15000 for page fetches."
|
|
665
|
+
],
|
|
666
|
+
takesValue: true,
|
|
667
|
+
apply: ({ parsed, value, flag }) => {
|
|
668
|
+
parsed.options.timeoutMs = toInt(value, flag);
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: "--verbose",
|
|
673
|
+
short: "-V",
|
|
674
|
+
label: "--verbose, -V",
|
|
675
|
+
group: "terminal",
|
|
676
|
+
help: ["Log every page as it is analyzed."],
|
|
677
|
+
takesValue: false,
|
|
678
|
+
apply: ({ parsed }) => {
|
|
679
|
+
parsed.logMode = "verbose";
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: "--quiet",
|
|
684
|
+
short: "-q",
|
|
685
|
+
label: "--quiet, -q",
|
|
686
|
+
group: "terminal",
|
|
687
|
+
help: ["Suppress the live progress output."],
|
|
688
|
+
takesValue: false,
|
|
689
|
+
apply: ({ parsed }) => {
|
|
690
|
+
parsed.logMode = "quiet";
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: "--no-color",
|
|
695
|
+
label: "--no-color",
|
|
696
|
+
group: "terminal",
|
|
697
|
+
default: "colour when stdout is a TTY and NO_COLOR is unset",
|
|
698
|
+
help: ["Disable coloured output."],
|
|
699
|
+
takesValue: false,
|
|
700
|
+
apply: ({ parsed }) => {
|
|
701
|
+
parsed.color = false;
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: "--help",
|
|
706
|
+
short: "-h",
|
|
707
|
+
label: "-h, --help",
|
|
708
|
+
group: "terminal",
|
|
709
|
+
help: ["Show this help."],
|
|
710
|
+
takesValue: false,
|
|
711
|
+
apply: ({ parsed }) => {
|
|
712
|
+
parsed.help = true;
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
{
|
|
716
|
+
name: "--version",
|
|
717
|
+
short: "-v",
|
|
718
|
+
label: "-v, --version",
|
|
719
|
+
group: "terminal",
|
|
720
|
+
help: ["Show the version."],
|
|
721
|
+
takesValue: false,
|
|
722
|
+
apply: ({ parsed }) => {
|
|
723
|
+
parsed.version = true;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
];
|
|
727
|
+
var FLAGS_BY_TOKEN = new Map(
|
|
728
|
+
FLAGS.flatMap(
|
|
729
|
+
(spec) => spec.short ? [
|
|
730
|
+
[spec.name, spec],
|
|
731
|
+
[spec.short, spec]
|
|
732
|
+
] : [[spec.name, spec]]
|
|
733
|
+
)
|
|
734
|
+
);
|
|
735
|
+
var EXIT_CODES = [
|
|
736
|
+
{ code: 0, label: "clean" },
|
|
737
|
+
{ code: 1, label: "findings found" },
|
|
738
|
+
{ code: 2, label: "fatal error" }
|
|
739
|
+
];
|
|
740
|
+
var COMMANDS = [
|
|
741
|
+
{
|
|
742
|
+
name: "rules",
|
|
743
|
+
help: [
|
|
744
|
+
"Print the rule catalogue as JSON and exit. No crawl",
|
|
745
|
+
"and no network: every rule with its severity, its",
|
|
746
|
+
"rigor and the documents it cites."
|
|
747
|
+
]
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
name: "flags",
|
|
751
|
+
help: [
|
|
752
|
+
"Print this flag reference as JSON and exit. The same",
|
|
753
|
+
"table this help is rendered from, so the two cannot",
|
|
754
|
+
"disagree."
|
|
755
|
+
]
|
|
756
|
+
}
|
|
757
|
+
];
|
|
758
|
+
var DESCRIPTION_COLUMN = 25;
|
|
759
|
+
function block(label, lines) {
|
|
760
|
+
const pad = " ".repeat(DESCRIPTION_COLUMN);
|
|
761
|
+
return lines.map(
|
|
762
|
+
(line, i) => i === 0 ? ` ${label.padEnd(DESCRIPTION_COLUMN - 2)}${line}` : `${pad}${line}`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
function renderHelp() {
|
|
766
|
+
const lines = [
|
|
767
|
+
"goflag \u2014 audit a site for broken links, missing translations, and SEO metadata",
|
|
768
|
+
"",
|
|
769
|
+
"Usage:",
|
|
770
|
+
" goflag <url> [options]"
|
|
771
|
+
];
|
|
772
|
+
for (const command of COMMANDS) {
|
|
773
|
+
const pad = " ".repeat(28);
|
|
774
|
+
command.help.forEach((line, i) => {
|
|
775
|
+
lines.push(i === 0 ? ` goflag ${command.name.padEnd(19)}${line}` : `${pad}${line}`);
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
lines.push("", "Options:");
|
|
779
|
+
for (const spec of FLAGS) {
|
|
780
|
+
lines.push(...block(spec.label, [...spec.help, ...spec.dynamicTail?.() ?? []]));
|
|
781
|
+
}
|
|
782
|
+
lines.push("", `Exit codes: ${EXIT_CODES.map((e) => `${e.code} ${e.label}`).join(", ")}.`);
|
|
783
|
+
return lines.join("\n");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/cli-args.ts
|
|
787
|
+
var HELP = renderHelp();
|
|
788
|
+
var COMMAND_NAMES = COMMANDS.map((c) => c.name);
|
|
381
789
|
function parseArgs(argv) {
|
|
382
790
|
const parsed = {
|
|
383
791
|
json: false,
|
|
@@ -399,146 +807,20 @@ function parseArgs(argv) {
|
|
|
399
807
|
};
|
|
400
808
|
for (let i = 0; i < argv.length; i++) {
|
|
401
809
|
const arg = argv[i];
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
break;
|
|
407
|
-
case "-v":
|
|
408
|
-
case "--version":
|
|
409
|
-
parsed.version = true;
|
|
410
|
-
break;
|
|
411
|
-
case "--json":
|
|
412
|
-
parsed.json = true;
|
|
413
|
-
break;
|
|
414
|
-
case "-s":
|
|
415
|
-
case "--summary":
|
|
416
|
-
parsed.summary = true;
|
|
417
|
-
break;
|
|
418
|
-
case "--no-color":
|
|
419
|
-
parsed.color = false;
|
|
420
|
-
break;
|
|
421
|
-
case "-V":
|
|
422
|
-
case "--verbose":
|
|
423
|
-
parsed.logMode = "verbose";
|
|
424
|
-
break;
|
|
425
|
-
case "-q":
|
|
426
|
-
case "--quiet":
|
|
427
|
-
parsed.logMode = "quiet";
|
|
428
|
-
break;
|
|
429
|
-
case "--static":
|
|
430
|
-
parsed.options.static = true;
|
|
431
|
-
break;
|
|
432
|
-
case "--allow-insecure-tls":
|
|
433
|
-
parsed.options.allowInsecureTls = true;
|
|
434
|
-
break;
|
|
435
|
-
case "--no-external":
|
|
436
|
-
parsed.options.checkExternal = false;
|
|
437
|
-
break;
|
|
438
|
-
case "--no-sitemap":
|
|
439
|
-
parsed.options.noSitemap = true;
|
|
440
|
-
break;
|
|
441
|
-
case "--conformance":
|
|
442
|
-
parsed.options.conformance = true;
|
|
443
|
-
break;
|
|
444
|
-
case "--advisories":
|
|
445
|
-
parsed.options.advisories = true;
|
|
446
|
-
break;
|
|
447
|
-
case "--ignore-holes":
|
|
448
|
-
parsed.options.ignoreHoles = [...parsed.options.ignoreHoles ?? [], next(i, arg)];
|
|
449
|
-
i++;
|
|
450
|
-
break;
|
|
451
|
-
case "--locales": {
|
|
452
|
-
const value = next(i, arg);
|
|
453
|
-
const tags = value.split(",").map((t) => t.trim()).filter(Boolean);
|
|
454
|
-
if (tags.length === 0) throw new Error(`${arg} expects at least one locale`);
|
|
455
|
-
parsed.options.locales = [...parsed.options.locales ?? [], ...tags];
|
|
810
|
+
const spec = arg === void 0 ? void 0 : FLAGS_BY_TOKEN.get(arg);
|
|
811
|
+
if (spec && arg !== void 0) {
|
|
812
|
+
if (spec.takesValue) {
|
|
813
|
+
spec.apply({ parsed, value: next(i, arg), flag: arg });
|
|
456
814
|
i++;
|
|
457
|
-
|
|
815
|
+
} else {
|
|
816
|
+
spec.apply({ parsed, value: "", flag: arg });
|
|
458
817
|
}
|
|
459
|
-
|
|
460
|
-
const value = next(i, arg);
|
|
461
|
-
if (!PROFILE_NAMES.includes(value)) {
|
|
462
|
-
throw new Error(`${arg} expects one of: ${PROFILE_NAMES.join(", ")}`);
|
|
463
|
-
}
|
|
464
|
-
parsed.options.profile = value;
|
|
465
|
-
i++;
|
|
466
|
-
break;
|
|
467
|
-
}
|
|
468
|
-
case "--fail-on": {
|
|
469
|
-
const value = next(i, arg);
|
|
470
|
-
if (!FAIL_ON_LEVELS.includes(value)) {
|
|
471
|
-
throw new Error(`${arg} expects one of: ${FAIL_ON_LEVELS.join(", ")}`);
|
|
472
|
-
}
|
|
473
|
-
parsed.failOn = value;
|
|
474
|
-
i++;
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
case "--start":
|
|
478
|
-
parsed.start = next(i, arg);
|
|
479
|
-
i++;
|
|
480
|
-
break;
|
|
481
|
-
case "--start-timeout":
|
|
482
|
-
parsed.startTimeoutMs = toInt(next(i, arg), arg);
|
|
483
|
-
i++;
|
|
484
|
-
break;
|
|
485
|
-
case "--start-cwd":
|
|
486
|
-
parsed.startCwd = next(i, arg);
|
|
487
|
-
i++;
|
|
488
|
-
break;
|
|
489
|
-
case "--report":
|
|
490
|
-
parsed.report = next(i, arg);
|
|
491
|
-
i++;
|
|
492
|
-
break;
|
|
493
|
-
case "--baseline":
|
|
494
|
-
parsed.baseline = next(i, arg);
|
|
495
|
-
i++;
|
|
496
|
-
break;
|
|
497
|
-
case "--update-baseline":
|
|
498
|
-
parsed.updateBaseline = true;
|
|
499
|
-
break;
|
|
500
|
-
case "--regressions-only":
|
|
501
|
-
parsed.regressionsOnly = true;
|
|
502
|
-
break;
|
|
503
|
-
case "--max-debt":
|
|
504
|
-
parsed.maxDebt = toInt(next(i, arg), arg);
|
|
505
|
-
i++;
|
|
506
|
-
break;
|
|
507
|
-
case "--depth":
|
|
508
|
-
parsed.options.depth = toInt(next(i, arg), arg);
|
|
509
|
-
i++;
|
|
510
|
-
break;
|
|
511
|
-
case "--coverage": {
|
|
512
|
-
const mode = next(i, arg);
|
|
513
|
-
if (mode !== "structural" && mode !== "all") {
|
|
514
|
-
throw new Error(`--coverage takes "structural" or "all", not ${JSON.stringify(mode)}`);
|
|
515
|
-
}
|
|
516
|
-
parsed.options.coverage = mode;
|
|
517
|
-
i += 1;
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
case "--max-pages":
|
|
521
|
-
parsed.options.maxPages = toInt(next(i, arg), arg);
|
|
522
|
-
i++;
|
|
523
|
-
break;
|
|
524
|
-
case "--timeout":
|
|
525
|
-
parsed.options.timeoutMs = toInt(next(i, arg), arg);
|
|
526
|
-
i++;
|
|
527
|
-
break;
|
|
528
|
-
case "--include":
|
|
529
|
-
parsed.options.include.push(next(i, arg));
|
|
530
|
-
i++;
|
|
531
|
-
break;
|
|
532
|
-
case "--exclude":
|
|
533
|
-
parsed.options.exclude.push(next(i, arg));
|
|
534
|
-
i++;
|
|
535
|
-
break;
|
|
536
|
-
default:
|
|
537
|
-
if (arg && arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
538
|
-
if (arg === "rules" && i === 0) parsed.command = "rules";
|
|
539
|
-
else if (arg && !parsed.url) parsed.url = arg;
|
|
540
|
-
else if (arg) throw new Error(`unexpected argument: ${arg}`);
|
|
818
|
+
continue;
|
|
541
819
|
}
|
|
820
|
+
if (arg && arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
821
|
+
if (arg && COMMAND_NAMES.includes(arg) && i === 0) parsed.command = arg;
|
|
822
|
+
else if (arg && !parsed.url) parsed.url = arg;
|
|
823
|
+
else if (arg) throw new Error(`unexpected argument: ${arg}`);
|
|
542
824
|
}
|
|
543
825
|
if (parsed.updateBaseline && !parsed.baseline) {
|
|
544
826
|
throw new Error("--update-baseline needs a --baseline <file> to write to");
|
|
@@ -553,11 +835,6 @@ function parseArgs(argv) {
|
|
|
553
835
|
}
|
|
554
836
|
return parsed;
|
|
555
837
|
}
|
|
556
|
-
function toInt(value, flag) {
|
|
557
|
-
const n = Number(value);
|
|
558
|
-
if (!Number.isInteger(n) || n < 0) throw new Error(`${flag} expects a non-negative integer`);
|
|
559
|
-
return n;
|
|
560
|
-
}
|
|
561
838
|
|
|
562
839
|
// src/lib/rules/sources/index.ts
|
|
563
840
|
var NORMATIVE = [
|
|
@@ -979,8 +1256,11 @@ function buildRuleCatalog(version) {
|
|
|
979
1256
|
severity: rule.severity,
|
|
980
1257
|
kind: "boolean",
|
|
981
1258
|
summary: rule.summary,
|
|
982
|
-
|
|
983
|
-
sources
|
|
1259
|
+
// `null` where a cross-page rule has not declared one yet — the three
|
|
1260
|
+
// that predate the field still need their sources chosen rather than
|
|
1261
|
+
// guessed, and emitting the gap is how it stays visible.
|
|
1262
|
+
rigor: rule.rigor ?? null,
|
|
1263
|
+
sources: rule.sources ?? []
|
|
984
1264
|
});
|
|
985
1265
|
}
|
|
986
1266
|
for (const rule of PROSE_RULES) {
|
|
@@ -1029,6 +1309,34 @@ function serialiseCatalog(catalog) {
|
|
|
1029
1309
|
`;
|
|
1030
1310
|
}
|
|
1031
1311
|
|
|
1312
|
+
// src/lib/flags/catalog.ts
|
|
1313
|
+
function buildFlagCatalog(version) {
|
|
1314
|
+
const flags = FLAGS.map((spec) => ({
|
|
1315
|
+
name: spec.name,
|
|
1316
|
+
short: spec.short ?? null,
|
|
1317
|
+
arg: spec.arg ?? null,
|
|
1318
|
+
default: spec.default ?? null,
|
|
1319
|
+
requires: spec.requires ?? null,
|
|
1320
|
+
group: spec.group,
|
|
1321
|
+
description: [...spec.help, ...spec.dynamicTail?.() ?? []].join(" ")
|
|
1322
|
+
}));
|
|
1323
|
+
flags.sort((a, b) => a.name.localeCompare(b.name));
|
|
1324
|
+
return {
|
|
1325
|
+
...version ? { version } : {},
|
|
1326
|
+
flags,
|
|
1327
|
+
exitCodes: EXIT_CODES.map((e) => ({ code: e.code, label: e.label })),
|
|
1328
|
+
counts: {
|
|
1329
|
+
flags: flags.length,
|
|
1330
|
+
withShortForm: flags.filter((f) => f.short !== null).length,
|
|
1331
|
+
takingValue: flags.filter((f) => f.arg !== null).length
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
function serialiseFlags(catalog) {
|
|
1336
|
+
return `${JSON.stringify(catalog, null, 2)}
|
|
1337
|
+
`;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1032
1340
|
// src/cli.ts
|
|
1033
1341
|
async function readVersion() {
|
|
1034
1342
|
try {
|
|
@@ -1074,6 +1382,10 @@ ${HELP}
|
|
|
1074
1382
|
process.stdout.write(serialiseCatalog(buildRuleCatalog(await readVersion())));
|
|
1075
1383
|
return 0;
|
|
1076
1384
|
}
|
|
1385
|
+
if (args.command === "flags") {
|
|
1386
|
+
process.stdout.write(serialiseFlags(buildFlagCatalog(await readVersion())));
|
|
1387
|
+
return 0;
|
|
1388
|
+
}
|
|
1077
1389
|
if (!args.url) {
|
|
1078
1390
|
process.stderr.write(`goflag: missing <url>
|
|
1079
1391
|
|