@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/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  renderTerminal,
13
13
  runAudit,
14
14
  summarize
15
- } from "./chunk-UOKYUKHQ.js";
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/cli-args.ts
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
- switch (arg) {
403
- case "-h":
404
- case "--help":
405
- parsed.help = true;
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
- break;
815
+ } else {
816
+ spec.apply({ parsed, value: "", flag: arg });
458
817
  }
459
- case "--profile": {
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
- rigor: null,
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