@tested/cli 0.1.7 → 0.1.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.
Files changed (4) hide show
  1. package/README.md +83 -2
  2. package/dist/td.js +1333 -180
  3. package/dist/tested.js +1333 -180
  4. package/package.json +10 -13
package/dist/tested.js CHANGED
@@ -46,6 +46,8 @@ function badge(kind) {
46
46
  return pc.cyan("[INFO]");
47
47
  case "skip":
48
48
  return pc.cyan("[SKIP]");
49
+ case "missing":
50
+ return pc.yellow("[MISSING]");
49
51
  }
50
52
  }
51
53
  function metricBar(pct3, width = 10) {
@@ -349,24 +351,576 @@ function registerInitCommand(program2) {
349
351
  }
350
352
 
351
353
  // src/commands/setup.ts
352
- import { existsSync as existsSync4 } from "fs";
353
- import { join as join5 } from "path";
354
+ import { existsSync as existsSync5 } from "fs";
355
+ import { join as join6 } from "path";
354
356
  import "commander";
355
357
 
356
358
  // src/commands/doctor.ts
357
- import { existsSync as existsSync3, accessSync, constants as fsConstants } from "fs";
358
- import { basename, isAbsolute as isAbsolute2, join as join4, resolve as resolve4 } from "path";
359
+ import { existsSync as existsSync4, accessSync, constants as fsConstants } from "fs";
360
+ import { basename as basename2, isAbsolute as isAbsolute2, join as join5, resolve as resolve5 } from "path";
359
361
  import "commander";
360
362
  import { simpleGit as simpleGit3 } from "simple-git";
361
363
 
362
364
  // src/config.ts
363
- import { readFile } from "fs/promises";
364
- import { join as join2 } from "path";
365
+ import { readFile as readFile4 } from "fs/promises";
366
+ import { join as join3 } from "path";
365
367
  import { parse as parseYaml } from "yaml";
366
368
 
367
369
  // src/schemas.ts
368
370
  import { z as z2 } from "zod";
369
371
 
372
+ // src/core/coverage.ts
373
+ import { readFile as readFile3, stat as stat2 } from "fs/promises";
374
+ import { basename } from "path";
375
+
376
+ // src/core/istanbul.ts
377
+ import { readFile } from "fs/promises";
378
+
379
+ // src/core/coverage-model.ts
380
+ import { isAbsolute, relative, resolve } from "path";
381
+ function resolveCoverageEntryPath(repoRoot, entryPath) {
382
+ const cleaned = entryPath.trim().replace(/\\/g, "/");
383
+ if (cleaned.startsWith("file://")) {
384
+ try {
385
+ return decodeURIComponent(new URL(cleaned).pathname);
386
+ } catch {
387
+ return resolve(repoRoot, cleaned.replace(/^file:\/\//, ""));
388
+ }
389
+ }
390
+ return isAbsolute(cleaned) ? resolve(cleaned) : resolve(repoRoot, cleaned);
391
+ }
392
+ function isCoveragePathInsideRoot(repoRoot, entryPath) {
393
+ const root = resolve(repoRoot);
394
+ let absPath;
395
+ try {
396
+ absPath = resolveCoverageEntryPath(root, entryPath);
397
+ } catch {
398
+ return false;
399
+ }
400
+ const relPath = relative(root, absPath).split("\\").join("/");
401
+ if (!relPath || relPath === "") return true;
402
+ if (isAbsolute(relPath)) return false;
403
+ if (relPath === ".." || relPath.startsWith("../")) return false;
404
+ return true;
405
+ }
406
+ function toFileCoverage(repoRoot, entryPath, statements) {
407
+ if (!isCoveragePathInsideRoot(repoRoot, entryPath)) return null;
408
+ const absPath = resolveCoverageEntryPath(repoRoot, entryPath);
409
+ const relPath = relative(resolve(repoRoot), absPath).split("\\").join("/");
410
+ return { path: relPath, absPath, statements };
411
+ }
412
+ function statementsFromLineHits(lineHits) {
413
+ return [...lineHits].filter(([line]) => Number.isInteger(line) && line > 0).sort((a, b) => a[0] - b[0]).map(([line, hits]) => ({
414
+ id: String(line),
415
+ startLine: line,
416
+ endLine: line,
417
+ hits: Number.isFinite(hits) && hits > 0 ? hits : 0
418
+ }));
419
+ }
420
+ function mergeLineHits(into, line, hits) {
421
+ if (!Number.isInteger(line) || line <= 0) return;
422
+ const n = Number.isFinite(hits) && hits > 0 ? hits : 0;
423
+ into.set(line, (into.get(line) ?? 0) + n);
424
+ }
425
+ function fileCoverageFromLineHits(repoRoot, entryPath, lineHits) {
426
+ return toFileCoverage(repoRoot, entryPath, statementsFromLineHits(lineHits));
427
+ }
428
+
429
+ // src/core/istanbul.ts
430
+ function parseIstanbulString(raw, repoRoot, pathForError) {
431
+ let data;
432
+ try {
433
+ data = JSON.parse(raw);
434
+ } catch {
435
+ throw new Error(
436
+ `Istanbul/V8 JSON is not valid JSON${pathForError ? ` (${pathForError})` : ""}`
437
+ );
438
+ }
439
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
440
+ throw new Error("Istanbul/V8 JSON must be an object of file entries");
441
+ }
442
+ const out = [];
443
+ for (const entry of Object.values(data)) {
444
+ if (!entry || typeof entry !== "object" || typeof entry.path !== "string") {
445
+ continue;
446
+ }
447
+ if (!entry.statementMap || typeof entry.statementMap !== "object") {
448
+ continue;
449
+ }
450
+ const statements = Object.entries(
451
+ entry.statementMap
452
+ ).map(([id, loc]) => ({
453
+ id,
454
+ startLine: loc.start.line,
455
+ endLine: loc.end.line,
456
+ hits: entry.s?.[id] ?? 0
457
+ }));
458
+ const file = toFileCoverage(repoRoot, entry.path, statements);
459
+ if (file) out.push(file);
460
+ }
461
+ return out;
462
+ }
463
+
464
+ // src/core/formats/lcov.ts
465
+ function parseLcov(raw, repoRoot) {
466
+ const byFile = /* @__PURE__ */ new Map();
467
+ let current = null;
468
+ for (const rawLine of raw.split(/\r?\n/)) {
469
+ const line = rawLine.trim();
470
+ if (!line) continue;
471
+ if (line.startsWith("SF:")) {
472
+ current = line.slice(3).trim();
473
+ if (current && !byFile.has(current)) byFile.set(current, /* @__PURE__ */ new Map());
474
+ continue;
475
+ }
476
+ if (line === "end_of_record") {
477
+ current = null;
478
+ continue;
479
+ }
480
+ if (!current || !line.startsWith("DA:")) continue;
481
+ const payload = line.slice(3);
482
+ const comma = payload.indexOf(",");
483
+ if (comma < 0) continue;
484
+ const lineNo = Number(payload.slice(0, comma));
485
+ const hitsRaw = payload.slice(comma + 1).split(",")[0] ?? "0";
486
+ const hits = Number(hitsRaw);
487
+ mergeLineHits(byFile.get(current), lineNo, hits);
488
+ }
489
+ const out = [];
490
+ for (const [path, hits] of byFile) {
491
+ const file = fileCoverageFromLineHits(repoRoot, path, hits);
492
+ if (file) out.push(file);
493
+ }
494
+ return out;
495
+ }
496
+
497
+ // src/core/formats/xml.ts
498
+ function decodeXmlEntities(s) {
499
+ return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
500
+ }
501
+ function xmlAttr(tag, name) {
502
+ const re = new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i");
503
+ const m = tag.match(re);
504
+ if (!m) return void 0;
505
+ return decodeXmlEntities(m[2] ?? m[3] ?? "");
506
+ }
507
+
508
+ // src/core/formats/cobertura.ts
509
+ function parseCobertura(raw, repoRoot) {
510
+ const byFile = /* @__PURE__ */ new Map();
511
+ const classRe = /<class\b([^>]*)>([\s\S]*?)<\/class>|<class\b([^>]*)\s*\/>/gi;
512
+ let classMatch;
513
+ while ((classMatch = classRe.exec(raw)) !== null) {
514
+ const attrs = (classMatch[1] ?? classMatch[3] ?? "").trim();
515
+ const body = classMatch[2] ?? "";
516
+ const filename = xmlAttr(attrs, "filename")?.trim();
517
+ const name = xmlAttr(attrs, "name")?.trim();
518
+ const path = classPath(filename, name);
519
+ if (!path) continue;
520
+ let hits = byFile.get(path);
521
+ if (!hits) {
522
+ hits = /* @__PURE__ */ new Map();
523
+ byFile.set(path, hits);
524
+ }
525
+ const lineRe = /<line\b([^>]*)\/?>/gi;
526
+ let lineMatch;
527
+ while ((lineMatch = lineRe.exec(body)) !== null) {
528
+ const lineAttrs = lineMatch[1] ?? "";
529
+ const number = Number(xmlAttr(lineAttrs, "number"));
530
+ const hitCount = Number(xmlAttr(lineAttrs, "hits") ?? "0");
531
+ mergeLineHits(hits, number, hitCount);
532
+ }
533
+ }
534
+ const out = [];
535
+ for (const [path, hits] of byFile) {
536
+ const file = fileCoverageFromLineHits(repoRoot, path, hits);
537
+ if (file) out.push(file);
538
+ }
539
+ return out;
540
+ }
541
+ function classPath(filename, name) {
542
+ if (filename) {
543
+ if (filename.includes("/") || filename.includes("\\")) return filename;
544
+ if (name && name.includes(".")) {
545
+ const dir = name.split(".").slice(0, -1).join("/");
546
+ return dir ? `${dir}/${filename}` : filename;
547
+ }
548
+ return filename;
549
+ }
550
+ if (name) return name.replace(/\./g, "/");
551
+ return null;
552
+ }
553
+
554
+ // src/core/formats/gcov.ts
555
+ import { readdir, readFile as readFile2, stat } from "fs/promises";
556
+ import { join as join2 } from "path";
557
+ function parseGcov(raw, repoRoot) {
558
+ const byFile = /* @__PURE__ */ new Map();
559
+ let current = null;
560
+ for (const rawLine of raw.split(/\r?\n/)) {
561
+ const parsed = parseGcovLine(rawLine);
562
+ if (!parsed) continue;
563
+ if (parsed.lineNo === 0) {
564
+ const source = sourceFromMeta(parsed.source);
565
+ if (source) {
566
+ current = source;
567
+ if (!byFile.has(current)) byFile.set(current, /* @__PURE__ */ new Map());
568
+ }
569
+ continue;
570
+ }
571
+ if (parsed.hits === null) continue;
572
+ if (!current) continue;
573
+ mergeLineHits(byFile.get(current), parsed.lineNo, parsed.hits);
574
+ }
575
+ const out = [];
576
+ for (const [path, hits] of byFile) {
577
+ const file = fileCoverageFromLineHits(repoRoot, path, hits);
578
+ if (file) out.push(file);
579
+ }
580
+ return out;
581
+ }
582
+ async function parseGcovPath(opts) {
583
+ const info = await stat(opts.path);
584
+ if (info.isDirectory()) {
585
+ const names = (await readdir(opts.path)).filter((n) => n.endsWith(".gcov")).sort();
586
+ if (names.length === 0) {
587
+ throw new Error(
588
+ `no .gcov files in ${opts.path}. Run \`gcov\` on your .gcda files (binary .gcno/.gcda notes are not parsed).`
589
+ );
590
+ }
591
+ const byPath = /* @__PURE__ */ new Map();
592
+ const absByPath = /* @__PURE__ */ new Map();
593
+ for (const name of names) {
594
+ const filePath = join2(opts.path, name);
595
+ const raw2 = await readFile2(filePath, "utf8");
596
+ for (const file of parseGcov(raw2, opts.repoRoot)) {
597
+ let hits = byPath.get(file.path);
598
+ if (!hits) {
599
+ hits = /* @__PURE__ */ new Map();
600
+ byPath.set(file.path, hits);
601
+ absByPath.set(file.path, file.absPath);
602
+ }
603
+ for (const s of file.statements) {
604
+ mergeLineHits(hits, s.startLine, s.hits);
605
+ }
606
+ }
607
+ }
608
+ return [...byPath.entries()].map(([path, hits]) => ({
609
+ path,
610
+ absPath: absByPath.get(path),
611
+ statements: statementsFromLineHits(hits)
612
+ }));
613
+ }
614
+ const raw = await readFile2(opts.path, "utf8");
615
+ return parseGcov(raw, opts.repoRoot);
616
+ }
617
+ function parseGcovLine(rawLine) {
618
+ const first = rawLine.indexOf(":");
619
+ if (first < 0) return null;
620
+ const second = rawLine.indexOf(":", first + 1);
621
+ if (second < 0) return null;
622
+ const countField = rawLine.slice(0, first).trim();
623
+ const lineNo = Number(rawLine.slice(first + 1, second).trim());
624
+ if (!Number.isInteger(lineNo)) return null;
625
+ const source = rawLine.slice(second + 1);
626
+ if (countField === "-" || countField === "") {
627
+ return { hits: null, lineNo, source };
628
+ }
629
+ if (countField.startsWith("#") || countField.startsWith("=")) {
630
+ return { hits: 0, lineNo, source };
631
+ }
632
+ const hits = Number.parseInt(countField.replace(/\*+$/, ""), 10);
633
+ if (!Number.isFinite(hits)) return { hits: 0, lineNo, source };
634
+ return { hits, lineNo, source };
635
+ }
636
+ function sourceFromMeta(source) {
637
+ const trimmed = source.trim();
638
+ const m = trimmed.match(/^Source:(.*)$/);
639
+ if (!m) return null;
640
+ const path = (m[1] ?? "").trim();
641
+ return path || null;
642
+ }
643
+
644
+ // src/core/formats/jacoco.ts
645
+ function parseJacoco(raw, repoRoot) {
646
+ const byFile = /* @__PURE__ */ new Map();
647
+ const pkgRe = /<package\b([^>]*)>([\s\S]*?)<\/package>/gi;
648
+ let pkgMatch;
649
+ while ((pkgMatch = pkgRe.exec(raw)) !== null) {
650
+ const pkgName = xmlAttr(pkgMatch[1] ?? "", "name")?.trim() ?? "";
651
+ const pkgPath = normalizePackagePath(pkgName);
652
+ const body = pkgMatch[2] ?? "";
653
+ const sfRe = /<sourcefile\b([^>]*)>([\s\S]*?)<\/sourcefile>/gi;
654
+ let sfMatch;
655
+ while ((sfMatch = sfRe.exec(body)) !== null) {
656
+ const sourceName = xmlAttr(sfMatch[1] ?? "", "name")?.trim();
657
+ if (!sourceName) continue;
658
+ const path = pkgPath ? `${pkgPath}/${sourceName}` : sourceName;
659
+ let hits = byFile.get(path);
660
+ if (!hits) {
661
+ hits = /* @__PURE__ */ new Map();
662
+ byFile.set(path, hits);
663
+ }
664
+ const lineRe = /<line\b([^>]*)\/?>/gi;
665
+ let lineMatch;
666
+ while ((lineMatch = lineRe.exec(sfMatch[2] ?? "")) !== null) {
667
+ const lineAttrs = lineMatch[1] ?? "";
668
+ const nr = Number(xmlAttr(lineAttrs, "nr"));
669
+ const ci = Number(xmlAttr(lineAttrs, "ci") ?? "0");
670
+ mergeLineHits(hits, nr, Number.isFinite(ci) ? ci : 0);
671
+ }
672
+ }
673
+ }
674
+ const out = [];
675
+ for (const [path, hits] of byFile) {
676
+ const file = fileCoverageFromLineHits(repoRoot, path, hits);
677
+ if (file) out.push(file);
678
+ }
679
+ return out;
680
+ }
681
+ function normalizePackagePath(name) {
682
+ if (!name) return "";
683
+ if (name.includes("/")) return name.replace(/\\/g, "/");
684
+ return name.replace(/\./g, "/");
685
+ }
686
+
687
+ // src/core/formats/simplecov.ts
688
+ function parseSimpleCov(raw, repoRoot) {
689
+ let data;
690
+ try {
691
+ data = JSON.parse(raw);
692
+ } catch {
693
+ throw new Error("SimpleCov coverage file is not valid JSON");
694
+ }
695
+ const byFile = /* @__PURE__ */ new Map();
696
+ if (isSimpleCovJsonGem(data)) {
697
+ for (const file of data.files) {
698
+ const filename = typeof file.filename === "string" ? file.filename : "";
699
+ if (!filename) continue;
700
+ addCoverageArray(byFile, filename, file.coverage);
701
+ }
702
+ } else if (isSimpleCovResultset(data)) {
703
+ for (const suite of Object.values(data)) {
704
+ if (!suite || typeof suite !== "object") continue;
705
+ const coverage = suite.coverage;
706
+ if (!coverage || typeof coverage !== "object") continue;
707
+ for (const [filename, entry] of Object.entries(
708
+ coverage
709
+ )) {
710
+ addCoverageArray(byFile, filename, linesFromResultsetEntry(entry));
711
+ }
712
+ }
713
+ } else {
714
+ throw new Error(
715
+ "Not a SimpleCov resultset or simplecov-json report. Expected coverage/.resultset.json"
716
+ );
717
+ }
718
+ const out = [];
719
+ for (const [path, hits] of byFile) {
720
+ const file = fileCoverageFromLineHits(repoRoot, path, hits);
721
+ if (file) out.push(file);
722
+ }
723
+ return out;
724
+ }
725
+ function isSimpleCovJsonGem(data) {
726
+ return typeof data === "object" && data !== null && Array.isArray(data.files) && data.files.some(
727
+ (f) => typeof f === "object" && f !== null && typeof f.filename === "string"
728
+ );
729
+ }
730
+ function isSimpleCovResultset(data) {
731
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
732
+ return false;
733
+ }
734
+ if (isSimpleCovJsonGem(data)) return false;
735
+ return Object.values(data).some((suite) => {
736
+ if (typeof suite !== "object" || suite === null) return false;
737
+ const coverage = suite.coverage;
738
+ return typeof coverage === "object" && coverage !== null;
739
+ });
740
+ }
741
+ function linesFromResultsetEntry(entry) {
742
+ if (Array.isArray(entry)) return entry;
743
+ if (entry && typeof entry === "object" && "lines" in entry) {
744
+ return entry.lines;
745
+ }
746
+ return void 0;
747
+ }
748
+ function addCoverageArray(byFile, filename, coverage) {
749
+ if (!Array.isArray(coverage)) return;
750
+ let hits = byFile.get(filename);
751
+ if (!hits) {
752
+ hits = /* @__PURE__ */ new Map();
753
+ byFile.set(filename, hits);
754
+ }
755
+ coverage.forEach((cell, idx) => {
756
+ if (cell === null || cell === void 0) return;
757
+ const n = typeof cell === "number" ? cell : Number(cell);
758
+ if (!Number.isFinite(n)) return;
759
+ mergeLineHits(hits, idx + 1, n);
760
+ });
761
+ }
762
+
763
+ // src/core/coverage.ts
764
+ var COVERAGE_FORMATS = [
765
+ "istanbul-json",
766
+ "v8-json",
767
+ "lcov",
768
+ "cobertura",
769
+ "jacoco",
770
+ "gcov",
771
+ "simplecov"
772
+ ];
773
+ function resolveCoverageFormat(format) {
774
+ return format === "v8-json" ? "istanbul-json" : format;
775
+ }
776
+ var MISSING_COVERAGE = "coverage file missing. Run `tested run` first, or set coverage.path in .tested.yaml.";
777
+ async function parseCoverage(opts) {
778
+ const explicit = opts.format ? resolveCoverageFormat(opts.format) : void 0;
779
+ let info;
780
+ try {
781
+ info = await stat2(opts.path);
782
+ } catch (err) {
783
+ if (err.code === "ENOENT") {
784
+ throw missingCoverageError(opts.path);
785
+ }
786
+ throw err;
787
+ }
788
+ if (info.isDirectory()) {
789
+ const format2 = explicit ?? detectFormatFromPath(opts.path) ?? "gcov";
790
+ if (format2 !== "gcov") {
791
+ throw new Error(
792
+ `coverage path ${opts.path} is a directory; only gcov accepts a directory of .gcov files`
793
+ );
794
+ }
795
+ return parseGcovPath({ path: opts.path, repoRoot: opts.repoRoot });
796
+ }
797
+ let raw;
798
+ try {
799
+ raw = await readFile3(opts.path, "utf8");
800
+ } catch (err) {
801
+ if (err.code === "ENOENT") {
802
+ throw missingCoverageError(opts.path);
803
+ }
804
+ throw err;
805
+ }
806
+ const format = explicit ?? detectFormatFromContents(raw) ?? detectFormatFromPath(opts.path) ?? defaultIstanbulIfCoverageFinal(opts.path);
807
+ if (!format) {
808
+ throw new Error(
809
+ `Unable to detect coverage format for ${opts.path}. Set coverage.format in .tested.yaml (${COVERAGE_FORMATS.filter((f) => f !== "v8-json").join(", ")}).`
810
+ );
811
+ }
812
+ return parseCoverageText({
813
+ raw,
814
+ path: opts.path,
815
+ repoRoot: opts.repoRoot,
816
+ format
817
+ });
818
+ }
819
+ function parseCoverageText(opts) {
820
+ switch (opts.format) {
821
+ case "istanbul-json":
822
+ return parseIstanbulString(opts.raw, opts.repoRoot, opts.path);
823
+ case "lcov":
824
+ return parseLcov(opts.raw, opts.repoRoot);
825
+ case "cobertura":
826
+ return parseCobertura(opts.raw, opts.repoRoot);
827
+ case "jacoco":
828
+ return parseJacoco(opts.raw, opts.repoRoot);
829
+ case "gcov":
830
+ return parseGcov(opts.raw, opts.repoRoot);
831
+ case "simplecov":
832
+ return parseSimpleCov(opts.raw, opts.repoRoot);
833
+ default: {
834
+ const _exhaustive = opts.format;
835
+ throw new Error(`Unsupported coverage format: ${String(_exhaustive)}`);
836
+ }
837
+ }
838
+ }
839
+ function detectFormatFromPath(filePath) {
840
+ const base = basename(filePath).toLowerCase();
841
+ if (base === "coverage-final.json") return "istanbul-json";
842
+ if (base === "lcov.info" || base.endsWith(".lcov")) return "lcov";
843
+ if (base.includes("cobertura")) return "cobertura";
844
+ if (base.includes("jacoco")) return "jacoco";
845
+ if (base.endsWith(".gcov")) return "gcov";
846
+ if (base === ".resultset.json" || base === "resultset.json") return "simplecov";
847
+ return void 0;
848
+ }
849
+ function detectFormatFromContents(raw) {
850
+ const text = raw.replace(/^\uFEFF/, "");
851
+ const trimmed = text.trimStart();
852
+ if (!trimmed) return void 0;
853
+ if (/^\s*-:\s*0:Source:/m.test(trimmed.slice(0, 4e3))) return "gcov";
854
+ const headLines = trimmed.slice(0, 2e3);
855
+ if (/^(TN:|SF:|DA:\d)/m.test(headLines)) return "lcov";
856
+ if (trimmed.startsWith("<?xml") || trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<coverage") || trimmed.startsWith("<report")) {
857
+ return detectXmlFormat(trimmed.slice(0, 8e3));
858
+ }
859
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
860
+ try {
861
+ const data = JSON.parse(text);
862
+ if (isCoveragePyJson(data)) {
863
+ throw new Error(
864
+ "coverage.py JSON is not supported. Emit lcov or Cobertura XML (pytest-cov `--cov-report=lcov` or `--cov-report=xml`) instead."
865
+ );
866
+ }
867
+ if (isIstanbulJson(data)) return "istanbul-json";
868
+ if (isSimpleCovJsonGem(data) || isSimpleCovResultset(data)) {
869
+ return "simplecov";
870
+ }
871
+ } catch (err) {
872
+ if (err instanceof Error && /coverage\.py JSON/.test(err.message)) {
873
+ throw err;
874
+ }
875
+ return void 0;
876
+ }
877
+ }
878
+ const firstData = trimmed.split(/\r?\n/).find((l) => l.includes(":"));
879
+ if (firstData && /^\s*(?:#####|=======|\d+)\s*:\s*\d+:/.test(firstData)) {
880
+ return "gcov";
881
+ }
882
+ return void 0;
883
+ }
884
+ function detectXmlFormat(head) {
885
+ const lower = head.toLowerCase();
886
+ if (lower.includes("cobertura")) return "cobertura";
887
+ if (lower.includes("jacoco")) return "jacoco";
888
+ if (lower.includes("<report") && /<line\b[^>]*\bci\s*=/.test(lower)) {
889
+ return "jacoco";
890
+ }
891
+ if (lower.includes("<coverage") && /<line\b[^>]*\bhits\s*=/.test(lower)) {
892
+ return "cobertura";
893
+ }
894
+ if (lower.includes("<report")) return "jacoco";
895
+ if (lower.includes("<coverage")) return "cobertura";
896
+ return void 0;
897
+ }
898
+ function defaultIstanbulIfCoverageFinal(filePath) {
899
+ const base = basename(filePath).toLowerCase();
900
+ if (base === "coverage-final.json") return "istanbul-json";
901
+ return void 0;
902
+ }
903
+ function isIstanbulJson(data) {
904
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
905
+ return false;
906
+ }
907
+ const values = Object.values(data);
908
+ if (values.length === 0) return true;
909
+ return values.some(
910
+ (v) => typeof v === "object" && v !== null && "statementMap" in v && "s" in v
911
+ );
912
+ }
913
+ function isCoveragePyJson(data) {
914
+ if (typeof data !== "object" || data === null || Array.isArray(data)) {
915
+ return false;
916
+ }
917
+ const rec = data;
918
+ return typeof rec.meta === "object" && rec.meta !== null && typeof rec.files === "object" && rec.files !== null && !Array.isArray(rec.files);
919
+ }
920
+ function missingCoverageError(path) {
921
+ return new Error(`${MISSING_COVERAGE} (${path})`);
922
+ }
923
+
370
924
  // src/core/junit.ts
371
925
  import { z } from "zod";
372
926
  function testCaseKey(classname, name) {
@@ -404,14 +958,14 @@ var TestReportSchema = z.object({
404
958
  ).max(50),
405
959
  slowest: z.array(TestCaseRefSchema).max(15)
406
960
  });
407
- function decodeXmlEntities(s) {
961
+ function decodeXmlEntities2(s) {
408
962
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
409
963
  }
410
964
  function attr(tag, name) {
411
965
  const re = new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, "i");
412
966
  const m = tag.match(re);
413
967
  if (!m) return void 0;
414
- return decodeXmlEntities(m[2] ?? m[3] ?? "");
968
+ return decodeXmlEntities2(m[2] ?? m[3] ?? "");
415
969
  }
416
970
  function parseJunitXml(xml) {
417
971
  const cases = [];
@@ -438,7 +992,7 @@ function parseJunitXml(xml) {
438
992
  message = fm ? attr(fm[1] ?? "", "message") : void 0;
439
993
  if (!message) {
440
994
  const inner = body.match(/<failure\b[^>]*>([\s\S]*?)<\/failure>/i);
441
- if (inner?.[1]?.trim()) message = decodeXmlEntities(inner[1].trim()).slice(0, 500);
995
+ if (inner?.[1]?.trim()) message = decodeXmlEntities2(inner[1].trim()).slice(0, 500);
442
996
  }
443
997
  } else if (/<error\b/i.test(body)) {
444
998
  status = "error";
@@ -541,22 +1095,43 @@ function parseJunitToTestReport(xml) {
541
1095
  }
542
1096
 
543
1097
  // src/schemas.ts
1098
+ var CoverageFormatSchema = z2.enum(COVERAGE_FORMATS);
1099
+ var FLAG_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
1100
+ var FlagNameSchema = z2.string().regex(FLAG_NAME_PATTERN, "flag names must be alphanumeric plus _ . -");
1101
+ var FlagThresholdsSchema = z2.object({
1102
+ patch: z2.number().min(0).max(100).optional(),
1103
+ project: z2.number().min(0).max(100).optional()
1104
+ });
1105
+ var FlagConfigSchema = z2.object({
1106
+ paths: z2.array(z2.string().min(1)).min(1),
1107
+ thresholds: FlagThresholdsSchema.optional()
1108
+ });
544
1109
  var TestedConfigSchema = z2.object({
545
1110
  ignores: z2.array(z2.string()).default([]),
546
1111
  coverage: z2.object({
547
- format: z2.literal("istanbul-json").default("istanbul-json"),
548
- path: z2.string().default("coverage/coverage-final.json")
1112
+ /** Omit to auto-detect from path and file contents. */
1113
+ format: CoverageFormatSchema.optional(),
1114
+ /**
1115
+ * One file or a list of files to merge (union of paths, max hits).
1116
+ * A CI matrix that uploads many files in one job should list them here
1117
+ * or pass `--file` / Action `files`.
1118
+ */
1119
+ path: z2.union([z2.string().min(1), z2.array(z2.string().min(1)).min(1)]).default("coverage/coverage-final.json")
549
1120
  }).prefault({}),
550
1121
  base: z2.string().default("origin/main"),
551
1122
  testRunner: z2.enum(["vitest", "jest", "pytest"]).nullable().default(null),
552
1123
  // Patch / project coverage gates. `tested init` writes these so users can
553
1124
  // tune what counts as "passing" — schema MUST accept them so loadConfig
554
- // doesn't silently drop the field. Enforcement in `diff` lands in a
555
- // follow-up; today we just round-trip the values cleanly.
1125
+ // doesn't silently drop the field.
556
1126
  thresholds: z2.object({
557
1127
  patch: z2.number().min(0).max(100),
558
1128
  project: z2.number().min(0).max(100)
559
- }).optional()
1129
+ }).optional(),
1130
+ /**
1131
+ * Per-package gates. Each flag is graded from this run's coverage files
1132
+ * only — a missing path is skipped (not 0%), never another flag's totals.
1133
+ */
1134
+ flags: z2.record(FlagNameSchema, FlagConfigSchema).optional()
560
1135
  });
561
1136
  var UncoveredRangeSchema = z2.object({
562
1137
  start: z2.number().int().positive(),
@@ -588,6 +1163,29 @@ var DiffOutputSchema = z2.object({
588
1163
  files: z2.array(FileCoverageSchema),
589
1164
  ignored: z2.array(z2.string())
590
1165
  });
1166
+ var FlagMetricJsonSchema = z2.object({
1167
+ /** Omitted when this flag had no files this run (`skipped: true`). */
1168
+ pct: z2.number().min(0).max(100).optional(),
1169
+ threshold: z2.number().min(0).max(100),
1170
+ /** Omitted when skipped — a missing flag is not a 0% fail. */
1171
+ pass: z2.boolean().optional(),
1172
+ executable: z2.number().int().nonnegative().optional(),
1173
+ covered: z2.number().int().nonnegative().optional(),
1174
+ skipped: z2.literal(true).optional(),
1175
+ reason: z2.string().optional()
1176
+ });
1177
+ var FlagResultJsonSchema = z2.object({
1178
+ status: z2.enum(["pass", "fail", "missing"]),
1179
+ present: z2.boolean(),
1180
+ /** True when this flag had no coverage files this run (not a 0% result). */
1181
+ skipped: z2.literal(true).optional(),
1182
+ reason: z2.string().optional(),
1183
+ patchCheck: z2.string(),
1184
+ projectCheck: z2.string(),
1185
+ patch: FlagMetricJsonSchema,
1186
+ project: FlagMetricJsonSchema
1187
+ });
1188
+ var FlagsJsonMapSchema = z2.record(FlagNameSchema, FlagResultJsonSchema);
591
1189
 
592
1190
  // src/config.ts
593
1191
  var DEFAULT_IGNORES = [
@@ -608,10 +1206,10 @@ var DEFAULT_IGNORES = [
608
1206
  "stubs/**"
609
1207
  ];
610
1208
  async function loadConfig(opts) {
611
- const file = join2(opts.cwd, ".tested.yaml");
1209
+ const file = join3(opts.cwd, ".tested.yaml");
612
1210
  let raw = {};
613
1211
  try {
614
- const text = await readFile(file, "utf8");
1212
+ const text = await readFile4(file, "utf8");
615
1213
  raw = parseYaml(text) ?? {};
616
1214
  } catch (err) {
617
1215
  if (err.code !== "ENOENT") throw err;
@@ -621,13 +1219,129 @@ async function loadConfig(opts) {
621
1219
  return { ...parsed, ignores: [...merged] };
622
1220
  }
623
1221
 
1222
+ // src/core/coverage-paths.ts
1223
+ import { existsSync as existsSync2 } from "fs";
1224
+ import { resolve as resolve3 } from "path";
1225
+
1226
+ // src/core/assert-within-root.ts
1227
+ import { resolve as resolve2, sep } from "path";
1228
+ function assertWithinRoot(root, resolvedPath) {
1229
+ const safeRoot = resolve2(root) + sep;
1230
+ const safePath = resolve2(resolvedPath);
1231
+ if (!safePath.startsWith(safeRoot)) {
1232
+ throw new Error(
1233
+ `Path traversal rejected: ${safePath} is outside repository root ${safeRoot}`
1234
+ );
1235
+ }
1236
+ }
1237
+
1238
+ // src/core/merge-coverage.ts
1239
+ function statementKey(stmt) {
1240
+ return `${stmt.startLine}:${stmt.endLine}:${stmt.id}`;
1241
+ }
1242
+ function sameRange(a, b) {
1243
+ return a.startLine === b.startLine && a.endLine === b.endLine;
1244
+ }
1245
+ function mergeStatements(left, right) {
1246
+ const out = /* @__PURE__ */ new Map();
1247
+ for (const stmt of left) {
1248
+ out.set(statementKey(stmt), { ...stmt });
1249
+ }
1250
+ for (const stmt of right) {
1251
+ const exact = statementKey(stmt);
1252
+ const existing = out.get(exact);
1253
+ if (existing) {
1254
+ existing.hits = Math.max(existing.hits, stmt.hits);
1255
+ continue;
1256
+ }
1257
+ const rangeMatch = [...out.values()].find((prev) => sameRange(prev, stmt));
1258
+ if (rangeMatch) {
1259
+ rangeMatch.hits = Math.max(rangeMatch.hits, stmt.hits);
1260
+ continue;
1261
+ }
1262
+ out.set(exact, { ...stmt });
1263
+ }
1264
+ return [...out.values()].sort(
1265
+ (a, b) => a.startLine - b.startLine || a.endLine - b.endLine || a.id.localeCompare(b.id)
1266
+ );
1267
+ }
1268
+ function mergeFileCoverage(shards) {
1269
+ const byPath = /* @__PURE__ */ new Map();
1270
+ for (const shard of shards) {
1271
+ for (const file of shard) {
1272
+ const existing = byPath.get(file.path);
1273
+ if (!existing) {
1274
+ byPath.set(file.path, {
1275
+ path: file.path,
1276
+ absPath: file.absPath,
1277
+ statements: file.statements.map((s) => ({ ...s }))
1278
+ });
1279
+ continue;
1280
+ }
1281
+ byPath.set(file.path, {
1282
+ path: existing.path,
1283
+ absPath: existing.absPath,
1284
+ statements: mergeStatements(existing.statements, file.statements)
1285
+ });
1286
+ }
1287
+ }
1288
+ return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path));
1289
+ }
1290
+
1291
+ // src/core/coverage-paths.ts
1292
+ function coveragePathList(path) {
1293
+ const list = Array.isArray(path) ? [...path] : [path];
1294
+ return list.map((p) => p.trim()).filter((p) => p.length > 0);
1295
+ }
1296
+ function parseCoverageFileList(raw) {
1297
+ if (raw === void 0 || raw === null) return [];
1298
+ return raw.split(/[\n,]+/).map((p) => p.trim()).filter((p) => p.length > 0);
1299
+ }
1300
+ function collectCoverageFile(value, prev) {
1301
+ const next = value.trim();
1302
+ return next ? [...prev, next] : prev;
1303
+ }
1304
+ function resolveCoveragePaths(opts) {
1305
+ const fromFlag = (opts.files ?? []).map((p) => p.trim()).filter((p) => p.length > 0);
1306
+ if (fromFlag.length > 0) return fromFlag;
1307
+ const fromEnv = parseCoverageFileList(opts.env?.TESTED_COVERAGE_FILES);
1308
+ if (fromEnv.length > 0) return fromEnv;
1309
+ return coveragePathList(opts.configPath);
1310
+ }
1311
+ function existingCoveragePaths(paths, cwd, existsFn = existsSync2) {
1312
+ return paths.filter((rel) => {
1313
+ const abs = resolve3(cwd, rel);
1314
+ return existsFn(abs);
1315
+ });
1316
+ }
1317
+ async function parseAndMergeCoverage(opts) {
1318
+ if (opts.paths.length === 0) {
1319
+ throw new Error(
1320
+ "coverage file missing. Run `tested run` first, or set coverage.path in .tested.yaml."
1321
+ );
1322
+ }
1323
+ const shards = [];
1324
+ for (const rel of opts.paths) {
1325
+ const coveragePath = resolve3(opts.cwd, rel);
1326
+ assertWithinRoot(opts.repoRoot, coveragePath);
1327
+ shards.push(
1328
+ await parseCoverage({
1329
+ path: coveragePath,
1330
+ repoRoot: opts.repoRoot,
1331
+ ...opts.format ? { format: opts.format } : {}
1332
+ })
1333
+ );
1334
+ }
1335
+ return mergeFileCoverage(shards);
1336
+ }
1337
+
624
1338
  // src/commands/push.ts
625
- import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
626
- import { join as join3 } from "path";
1339
+ import { existsSync as existsSync3, readFileSync as readFileSync2, statSync } from "fs";
1340
+ import { join as join4 } from "path";
627
1341
  import "commander";
628
1342
 
629
1343
  // src/core/computeDiff.ts
630
- import { resolve as resolve3 } from "path";
1344
+ import { resolve as resolve4 } from "path";
631
1345
 
632
1346
  // src/git.ts
633
1347
  import { simpleGit as simpleGit2 } from "simple-git";
@@ -740,50 +1454,6 @@ async function gitUserName(ctx) {
740
1454
  }
741
1455
  }
742
1456
 
743
- // src/core/istanbul.ts
744
- import { readFile as readFile2 } from "fs/promises";
745
- import { isAbsolute, relative, resolve } from "path";
746
- function isCoveragePathInsideRoot(repoRoot, entryPath) {
747
- const root = resolve(repoRoot);
748
- const absPath = resolve(entryPath);
749
- const relPath = relative(root, absPath).split("\\").join("/");
750
- if (!relPath || relPath === "") return true;
751
- if (isAbsolute(relPath)) return false;
752
- if (relPath === ".." || relPath.startsWith("../")) return false;
753
- return true;
754
- }
755
- async function parseIstanbul(opts) {
756
- let raw;
757
- try {
758
- raw = await readFile2(opts.path, "utf8");
759
- } catch (err) {
760
- if (err.code === "ENOENT") {
761
- throw new Error(
762
- `coverage-final.json not found at ${opts.path}. Run \`tested run\` first.`
763
- );
764
- }
765
- throw err;
766
- }
767
- const data = JSON.parse(raw);
768
- const root = resolve(opts.repoRoot);
769
- const out = [];
770
- for (const entry of Object.values(data)) {
771
- if (!isCoveragePathInsideRoot(root, entry.path)) {
772
- continue;
773
- }
774
- const absPath = resolve(entry.path);
775
- const relPath = relative(root, absPath).split("\\").join("/");
776
- const statements = Object.entries(entry.statementMap).map(([id, loc]) => ({
777
- id,
778
- startLine: loc.start.line,
779
- endLine: loc.end.line,
780
- hits: entry.s[id] ?? 0
781
- }));
782
- out.push({ path: relPath, absPath, statements });
783
- }
784
- return out;
785
- }
786
-
787
1457
  // src/core/diff.ts
788
1458
  var FILE_HEADER = /^diff --git a\/(.+?) b\/(.+?)$/;
789
1459
  var HUNK_HEADER = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
@@ -837,18 +1507,6 @@ function splitByIgnore(paths, patterns) {
837
1507
  return { kept, ignored };
838
1508
  }
839
1509
 
840
- // src/core/assert-within-root.ts
841
- import { resolve as resolve2, sep } from "path";
842
- function assertWithinRoot(root, resolvedPath) {
843
- const safeRoot = resolve2(root) + sep;
844
- const safePath = resolve2(resolvedPath);
845
- if (!safePath.startsWith(safeRoot)) {
846
- throw new Error(
847
- `Path traversal rejected: ${safePath} is outside repository root ${safeRoot}`
848
- );
849
- }
850
- }
851
-
852
1510
  // src/core/patch.ts
853
1511
  var EMPTY_PATCH_REASON = "no executable lines in the patch";
854
1512
  function isEmptyPatch(totals) {
@@ -981,7 +1639,7 @@ function buildDiffOutput(args) {
981
1639
  }
982
1640
 
983
1641
  // src/core/computeDiff.ts
984
- async function computeDiff(opts) {
1642
+ async function computeDiffContext(opts) {
985
1643
  const { cwd, config } = opts;
986
1644
  const ctx = opts.ctx ?? await openRepo(cwd);
987
1645
  const requested = assertSafeGitRef(opts.baseRef ?? config.base);
@@ -989,9 +1647,16 @@ async function computeDiff(opts) {
989
1647
  const head = await headSha(ctx);
990
1648
  const diffText = await unifiedDiff(ctx, base);
991
1649
  const addedByFile = parseUnifiedDiff(diffText);
992
- const coveragePath = resolve3(cwd, config.coverage.path);
993
- assertWithinRoot(ctx.repoRoot, coveragePath);
994
- const allFiles = await parseIstanbul({ path: coveragePath, repoRoot: ctx.repoRoot });
1650
+ const coveragePaths = resolveCoveragePaths({
1651
+ ...opts.coveragePaths ? { files: opts.coveragePaths } : {},
1652
+ configPath: config.coverage.path
1653
+ });
1654
+ const allFiles = await parseAndMergeCoverage({
1655
+ paths: coveragePaths,
1656
+ cwd,
1657
+ repoRoot: ctx.repoRoot,
1658
+ ...config.coverage.format ? { format: config.coverage.format } : {}
1659
+ });
995
1660
  const { kept, ignored } = splitByIgnore(
996
1661
  allFiles.map((f) => f.path),
997
1662
  config.ignores
@@ -1000,11 +1665,12 @@ async function computeDiff(opts) {
1000
1665
  const files = allFiles.filter((f) => keptSet.has(f.path));
1001
1666
  let projectDelta = null;
1002
1667
  if (opts.withBaseCoverage) {
1003
- const baseCoveragePath = resolve3(cwd, opts.withBaseCoverage);
1668
+ const baseCoveragePath = resolve4(cwd, opts.withBaseCoverage);
1004
1669
  assertWithinRoot(ctx.repoRoot, baseCoveragePath);
1005
- const baseFiles = await parseIstanbul({
1670
+ const baseFiles = await parseCoverage({
1006
1671
  path: baseCoveragePath,
1007
- repoRoot: ctx.repoRoot
1672
+ repoRoot: ctx.repoRoot,
1673
+ ...config.coverage.format ? { format: config.coverage.format } : {}
1008
1674
  });
1009
1675
  const baseKept = baseFiles.filter((f) => !ignored.includes(f.path));
1010
1676
  const baseExec = baseKept.reduce((n, f) => n + f.statements.length, 0);
@@ -1023,7 +1689,7 @@ async function computeDiff(opts) {
1023
1689
  })();
1024
1690
  projectDelta = Math.round((headPct - basePct) * 10) / 10;
1025
1691
  }
1026
- return buildDiffOutput({
1692
+ const diff = buildDiffOutput({
1027
1693
  base: baseRef,
1028
1694
  head,
1029
1695
  files,
@@ -1031,6 +1697,224 @@ async function computeDiff(opts) {
1031
1697
  ignored,
1032
1698
  projectDelta
1033
1699
  });
1700
+ return { diff, files, addedByFile };
1701
+ }
1702
+
1703
+ // src/core/flags.ts
1704
+ import { minimatch as minimatch2 } from "minimatch";
1705
+ var MISSING_FLAG_REASON = "no coverage files matched this flag in this run";
1706
+ var SCOPED_MISSING_FLAG_REASON = "no coverage files in this run for this flag";
1707
+ var MATCH_OPTS = { dot: true, matchBase: true };
1708
+ function pathMatchesFlag(filePath, patterns) {
1709
+ const normalized = filePath.replace(/\\/g, "/");
1710
+ return patterns.some(
1711
+ (p) => minimatch2(normalized, p, MATCH_OPTS) || minimatch2(normalized, `**/${p}`, MATCH_OPTS)
1712
+ );
1713
+ }
1714
+ function filterFilesByFlag(files, patterns) {
1715
+ return files.filter((f) => pathMatchesFlag(f.path, patterns));
1716
+ }
1717
+ function resolveFlagThresholds(flag, global) {
1718
+ return {
1719
+ patch: flag.thresholds?.patch ?? global.patch,
1720
+ project: flag.thresholds?.project ?? global.project
1721
+ };
1722
+ }
1723
+ function unknownFlagError(name, configured) {
1724
+ const list = configured.length > 0 ? configured.join(", ") : "(none)";
1725
+ return new Error(`unknown flag "${name}" \u2014 configured flags: ${list}`);
1726
+ }
1727
+ function totalsToMetric(totals, threshold, kind) {
1728
+ const empty = isEmptyPatch(totals);
1729
+ if (kind === "patch" && empty) {
1730
+ return {
1731
+ pct: totals.pct,
1732
+ threshold,
1733
+ pass: true,
1734
+ executable: totals.executable,
1735
+ covered: totals.covered,
1736
+ skipped: true,
1737
+ reason: EMPTY_PATCH_REASON
1738
+ };
1739
+ }
1740
+ const pass = totals.pct >= threshold;
1741
+ return {
1742
+ pct: totals.pct,
1743
+ threshold,
1744
+ pass,
1745
+ executable: totals.executable,
1746
+ covered: totals.covered
1747
+ };
1748
+ }
1749
+ function missingMetric(threshold, reason) {
1750
+ return {
1751
+ threshold,
1752
+ skipped: true,
1753
+ reason
1754
+ };
1755
+ }
1756
+ function checkSlug(kind, name) {
1757
+ return `tested.dev / ${kind} / ${name}`;
1758
+ }
1759
+ function evaluatePresentFlag(name, files, addedByFile, thresholds) {
1760
+ const patch = computePatchCoverage(files, addedByFile);
1761
+ const project = computeProjectCoverage(files);
1762
+ const patchMetric = totalsToMetric(patch.totals, thresholds.patch, "patch");
1763
+ const projectMetric = totalsToMetric(project.totals, thresholds.project, "project");
1764
+ const status = patchMetric.pass && projectMetric.pass ? "pass" : "fail";
1765
+ return {
1766
+ name,
1767
+ present: true,
1768
+ status,
1769
+ patchCheck: checkSlug("patch", name),
1770
+ projectCheck: checkSlug("project", name),
1771
+ patch: patchMetric,
1772
+ project: projectMetric
1773
+ };
1774
+ }
1775
+ function missingFlag(name, thresholds, reason) {
1776
+ return {
1777
+ name,
1778
+ present: false,
1779
+ status: "missing",
1780
+ reason,
1781
+ patchCheck: checkSlug("patch", name),
1782
+ projectCheck: checkSlug("project", name),
1783
+ patch: missingMetric(thresholds.patch, reason),
1784
+ project: missingMetric(thresholds.project, reason)
1785
+ };
1786
+ }
1787
+ function evaluateFlags(input) {
1788
+ const flags = input.config.flags;
1789
+ const global = input.config.thresholds;
1790
+ if (!flags || !global) return [];
1791
+ const names = Object.keys(flags);
1792
+ if (input.onlyFlag !== void 0 && input.onlyFlag !== "") {
1793
+ const def = flags[input.onlyFlag];
1794
+ if (!def) throw unknownFlagError(input.onlyFlag, names);
1795
+ const thresholds = resolveFlagThresholds(def, global);
1796
+ if (input.files.length === 0) {
1797
+ return [missingFlag(input.onlyFlag, thresholds, SCOPED_MISSING_FLAG_REASON)];
1798
+ }
1799
+ return [evaluatePresentFlag(input.onlyFlag, input.files, input.addedByFile, thresholds)];
1800
+ }
1801
+ const results = [];
1802
+ for (const name of names) {
1803
+ const def = flags[name];
1804
+ const thresholds = resolveFlagThresholds(def, global);
1805
+ const matched = filterFilesByFlag(input.files, def.paths);
1806
+ if (matched.length === 0) {
1807
+ results.push(missingFlag(name, thresholds, MISSING_FLAG_REASON));
1808
+ continue;
1809
+ }
1810
+ results.push(evaluatePresentFlag(name, matched, input.addedByFile, thresholds));
1811
+ }
1812
+ return results;
1813
+ }
1814
+ function flagsPass(results) {
1815
+ return results.every((f) => f.status !== "fail");
1816
+ }
1817
+ function flagsToJson(results) {
1818
+ const out = {};
1819
+ for (const flag of results) {
1820
+ out[flag.name] = {
1821
+ status: flag.status,
1822
+ present: flag.present,
1823
+ ...flag.status === "missing" ? { skipped: true } : {},
1824
+ ...flag.reason ? { reason: flag.reason } : {},
1825
+ patchCheck: flag.patchCheck,
1826
+ projectCheck: flag.projectCheck,
1827
+ patch: metricToJson(flag.patch),
1828
+ project: metricToJson(flag.project)
1829
+ };
1830
+ }
1831
+ return out;
1832
+ }
1833
+ function metricToJson(metric) {
1834
+ if (metric.skipped) {
1835
+ return {
1836
+ threshold: metric.threshold,
1837
+ skipped: true,
1838
+ ...metric.reason ? { reason: metric.reason } : {}
1839
+ };
1840
+ }
1841
+ return {
1842
+ pct: metric.pct ?? 0,
1843
+ threshold: metric.threshold,
1844
+ pass: metric.pass ?? false,
1845
+ executable: metric.executable ?? 0,
1846
+ covered: metric.covered ?? 0,
1847
+ ...metric.reason ? { reason: metric.reason } : {}
1848
+ };
1849
+ }
1850
+ function flagsToIngestJson(results) {
1851
+ const present = results.filter((f) => f.present);
1852
+ return present.length > 0 ? flagsToJson(present) : void 0;
1853
+ }
1854
+ function resolveFlagsJson(input) {
1855
+ const results = evaluateFlags(input);
1856
+ return results.length > 0 ? flagsToJson(results) : void 0;
1857
+ }
1858
+ function resolveIngestFlagsJson(input) {
1859
+ return flagsToIngestJson(evaluateFlags(input));
1860
+ }
1861
+
1862
+ // src/core/coverage-merge.ts
1863
+ function emptyToUndef(value) {
1864
+ const trimmed = value?.trim();
1865
+ return trimmed ? trimmed : void 0;
1866
+ }
1867
+ function parsePositiveInt(raw, label) {
1868
+ const trimmed = emptyToUndef(raw);
1869
+ if (trimmed === void 0) return void 0;
1870
+ const n = Number(trimmed);
1871
+ if (!Number.isInteger(n) || n <= 0) {
1872
+ throw new Error(`invalid ${label} "${raw}" \u2014 expected a positive integer`);
1873
+ }
1874
+ return n;
1875
+ }
1876
+ function resolveCoverageMerge(cli, env = process.env) {
1877
+ if (cli.complete && cli.incomplete) {
1878
+ throw new Error("cannot pass both --complete and --incomplete");
1879
+ }
1880
+ const totalParts = parsePositiveInt(cli.parts ?? env.TESTED_PARTS, "--parts");
1881
+ const part = parsePositiveInt(cli.part ?? env.TESTED_PART, "--part");
1882
+ const runId = emptyToUndef(cli.runId ?? env.TESTED_RUN_ID);
1883
+ const shard = emptyToUndef(cli.shard ?? env.TESTED_SHARD);
1884
+ if (part !== void 0 && totalParts !== void 0 && part > totalParts) {
1885
+ throw new Error(`--part ${part} is greater than --parts ${totalParts}`);
1886
+ }
1887
+ let complete;
1888
+ if (cli.complete) {
1889
+ complete = true;
1890
+ } else if (cli.incomplete) {
1891
+ complete = false;
1892
+ } else if (totalParts !== void 0) {
1893
+ complete = part !== void 0 && part === totalParts;
1894
+ } else {
1895
+ complete = true;
1896
+ }
1897
+ const state = { complete };
1898
+ if (totalParts !== void 0) state.totalParts = totalParts;
1899
+ if (part !== void 0) state.part = part;
1900
+ if (runId !== void 0) state.runId = runId;
1901
+ if (shard !== void 0) state.shard = shard;
1902
+ return state;
1903
+ }
1904
+ function toCoverageMergePayload(state) {
1905
+ const payload = { complete: state.complete };
1906
+ if (state.totalParts !== void 0) payload.totalParts = state.totalParts;
1907
+ if (state.part !== void 0) payload.part = state.part;
1908
+ if (state.runId !== void 0) payload.runId = state.runId;
1909
+ if (state.shard !== void 0) payload.shard = state.shard;
1910
+ return payload;
1911
+ }
1912
+ function hasShardMetadata(state) {
1913
+ return !state.complete || state.totalParts !== void 0 || state.part !== void 0 || state.runId !== void 0 || state.shard !== void 0;
1914
+ }
1915
+ function formatIncompleteGateMessage(state) {
1916
+ const part = state.part !== void 0 && state.totalParts !== void 0 ? `part ${state.part} of ${state.totalParts}` : state.totalParts !== void 0 ? `waiting for ${state.totalParts} parts` : "incomplete shard";
1917
+ return `coverage shard is incomplete (${part}). The patch/project gate runs only on --complete or the last part.`;
1034
1918
  }
1035
1919
 
1036
1920
  // src/commands/push.ts
@@ -1056,10 +1940,10 @@ function parseGitHubRepository(repo) {
1056
1940
  var tokenArgvWarned = false;
1057
1941
  function readTokenFile(filePath, opts) {
1058
1942
  const read = opts?.readFileSyncFn ?? readFileSync2;
1059
- const stat = opts?.statSyncFn ?? statSync;
1943
+ const stat3 = opts?.statSyncFn ?? statSync;
1060
1944
  let mode;
1061
1945
  try {
1062
- const st = stat(filePath);
1946
+ const st = stat3(filePath);
1063
1947
  mode = st.mode;
1064
1948
  } catch (err) {
1065
1949
  const message = err instanceof Error ? err.message : String(err);
@@ -1331,8 +2215,10 @@ function buildIngestBody(input) {
1331
2215
  state: "open"
1332
2216
  },
1333
2217
  runUrl: input.runUrl,
1334
- diff: input.diff,
1335
- ...input.testReport ? { testReport: input.testReport } : {}
2218
+ ...input.diff ? { diff: input.diff } : {},
2219
+ ...input.testReport ? { testReport: input.testReport } : {},
2220
+ ...input.coverageMerge ? { coverageMerge: input.coverageMerge } : {},
2221
+ ...input.flags ? { flags: input.flags } : {}
1336
2222
  };
1337
2223
  }
1338
2224
  function buildMainlineIngestBody(input) {
@@ -1343,11 +2229,13 @@ function buildMainlineIngestBody(input) {
1343
2229
  defaultBranch: input.defaultBranch
1344
2230
  },
1345
2231
  runUrl: input.runUrl,
1346
- diff: input.diff,
2232
+ ...input.diff ? { diff: input.diff } : {},
1347
2233
  ref: input.ref,
1348
2234
  isDefaultBranch: true,
1349
2235
  headSha: input.headSha,
1350
- ...input.testReport ? { testReport: input.testReport } : {}
2236
+ ...input.testReport ? { testReport: input.testReport } : {},
2237
+ ...input.coverageMerge ? { coverageMerge: input.coverageMerge } : {},
2238
+ ...input.flags ? { flags: input.flags } : {}
1351
2239
  };
1352
2240
  }
1353
2241
  var DEFAULT_JUNIT_CANDIDATES = [
@@ -1358,10 +2246,10 @@ var DEFAULT_JUNIT_CANDIDATES = [
1358
2246
  ];
1359
2247
  function resolveJunitPath(opts) {
1360
2248
  const env = opts.env ?? process.env;
1361
- const exists = opts.existsSyncFn ?? existsSync2;
2249
+ const exists = opts.existsSyncFn ?? existsSync3;
1362
2250
  if (opts.flag && opts.flag.trim()) {
1363
2251
  const p = opts.flag.trim();
1364
- const abs = p.startsWith("/") ? p : join3(opts.cwd, p);
2252
+ const abs = p.startsWith("/") ? p : join4(opts.cwd, p);
1365
2253
  if (!exists(abs)) {
1366
2254
  throw new Error(`JUnit file not found: ${p}`);
1367
2255
  }
@@ -1369,14 +2257,14 @@ function resolveJunitPath(opts) {
1369
2257
  }
1370
2258
  const fromEnv = env.TESTED_JUNIT?.trim();
1371
2259
  if (fromEnv) {
1372
- const abs = fromEnv.startsWith("/") ? fromEnv : join3(opts.cwd, fromEnv);
2260
+ const abs = fromEnv.startsWith("/") ? fromEnv : join4(opts.cwd, fromEnv);
1373
2261
  if (!exists(abs)) {
1374
2262
  throw new Error(`TESTED_JUNIT file not found: ${fromEnv}`);
1375
2263
  }
1376
2264
  return abs;
1377
2265
  }
1378
2266
  for (const rel of DEFAULT_JUNIT_CANDIDATES) {
1379
- const abs = join3(opts.cwd, rel);
2267
+ const abs = join4(opts.cwd, rel);
1380
2268
  if (exists(abs)) return abs;
1381
2269
  }
1382
2270
  return null;
@@ -1422,15 +2310,12 @@ async function postIngest(opts) {
1422
2310
  parsed = null;
1423
2311
  }
1424
2312
  }
1425
- if (res.status === 200) {
1426
- const data = parsed;
1427
- if (!data) {
1428
- return {
1429
- ok: false,
1430
- status: res.status,
1431
- message: "ingest succeeded but response was empty"
1432
- };
1433
- }
2313
+ const concludesGate = opts.body.coverageMerge?.complete !== false;
2314
+ const handshakeOnly = concludesGate && !opts.body.diff;
2315
+ const acceptIncomplete = opts.body.coverageMerge?.complete === false;
2316
+ const successStatus = res.status === 200 || acceptIncomplete && res.status === 202;
2317
+ if (successStatus) {
2318
+ const data = parsed ?? {};
1434
2319
  if (data.mainline === true) {
1435
2320
  return {
1436
2321
  ok: true,
@@ -1438,11 +2323,20 @@ async function postIngest(opts) {
1438
2323
  data: {
1439
2324
  mainline: true,
1440
2325
  ...typeof data.date === "string" ? { date: data.date } : {},
1441
- ...typeof data.projectPct === "number" ? { projectPct: data.projectPct } : {}
2326
+ ...typeof data.projectPct === "number" ? { projectPct: data.projectPct } : {},
2327
+ ...acceptIncomplete ? { complete: false } : {}
1442
2328
  }
1443
2329
  };
1444
2330
  }
1445
- if (typeof data.shareUrl !== "string" || !data.shareUrl) {
2331
+ const shareUrl2 = typeof data.shareUrl === "string" && data.shareUrl ? data.shareUrl : void 0;
2332
+ if (concludesGate && !handshakeOnly && !acceptIncomplete && !shareUrl2) {
2333
+ if (parsed === null) {
2334
+ return {
2335
+ ok: false,
2336
+ status: res.status,
2337
+ message: "ingest succeeded but response was empty"
2338
+ };
2339
+ }
1446
2340
  return {
1447
2341
  ok: false,
1448
2342
  status: res.status,
@@ -1453,8 +2347,9 @@ async function postIngest(opts) {
1453
2347
  ok: true,
1454
2348
  status: res.status,
1455
2349
  data: {
1456
- shareUrl: data.shareUrl,
1457
- ...typeof data.expiresAt === "string" ? { expiresAt: data.expiresAt } : {}
2350
+ ...shareUrl2 ? { shareUrl: shareUrl2 } : {},
2351
+ ...typeof data.expiresAt === "string" ? { expiresAt: data.expiresAt } : {},
2352
+ ...acceptIncomplete || handshakeOnly ? { complete: concludesGate } : {}
1458
2353
  }
1459
2354
  };
1460
2355
  }
@@ -1471,7 +2366,8 @@ async function postIngest(opts) {
1471
2366
  }
1472
2367
  return { ok: false, status: res.status, message, ...code ? { code } : {} };
1473
2368
  }
1474
- function formatPushSuccess(data, json) {
2369
+ function formatPushSuccess(data, json, merge) {
2370
+ const incomplete = merge ? !merge.complete : data.complete === false;
1475
2371
  if (json) {
1476
2372
  const payload = {};
1477
2373
  if (data.shareUrl) payload.shareUrl = data.shareUrl;
@@ -1479,9 +2375,23 @@ function formatPushSuccess(data, json) {
1479
2375
  if (data.mainline) payload.mainline = true;
1480
2376
  if (data.date) payload.date = data.date;
1481
2377
  if (typeof data.projectPct === "number") payload.projectPct = data.projectPct;
2378
+ if (merge && hasShardMetadata(merge)) {
2379
+ payload.complete = merge.complete;
2380
+ if (merge.part !== void 0) payload.part = merge.part;
2381
+ if (merge.totalParts !== void 0) payload.totalParts = merge.totalParts;
2382
+ } else if (incomplete) {
2383
+ payload.complete = false;
2384
+ }
1482
2385
  return { stdout: JSON.stringify(payload) + "\n", stderr: "" };
1483
2386
  }
1484
2387
  const lines = [];
2388
+ if (incomplete && merge) {
2389
+ lines.push(successLine(`uploaded shard (${formatIncompleteGateMessage(merge)})`));
2390
+ if (data.shareUrl) {
2391
+ lines.push(dim(` ${shareUrl(data.shareUrl)} (pending \u2014 not a gate result)`));
2392
+ }
2393
+ return { stdout: lines.join("\n") + "\n", stderr: "" };
2394
+ }
1485
2395
  if (data.mainline) {
1486
2396
  lines.push(
1487
2397
  successLine(
@@ -1537,7 +2447,8 @@ function formatPushError(status, message, code, identity) {
1537
2447
  }
1538
2448
  async function executePush(cli, deps) {
1539
2449
  const env = deps.env ?? process.env;
1540
- const computeDiffFn = deps.computeDiffFn ?? computeDiff;
2450
+ const computeDiffFn = deps.computeDiffFn;
2451
+ const computeDiffContextFn = deps.computeDiffContextFn ?? computeDiffContext;
1541
2452
  const fetchFn = deps.fetchFn ?? globalThis.fetch;
1542
2453
  const openRepoFn = deps.openRepoFn ?? openRepo;
1543
2454
  const loadConfigFn = deps.loadConfigFn ?? loadConfig;
@@ -1601,6 +2512,14 @@ async function executePush(cli, deps) {
1601
2512
  const message = err instanceof Error ? err.message : String(err);
1602
2513
  return { exitCode: 1, stdout: "", stderr: errorBlock(message) };
1603
2514
  }
2515
+ let merge;
2516
+ try {
2517
+ merge = resolveCoverageMerge(cli, env);
2518
+ } catch (err) {
2519
+ const message = err instanceof Error ? err.message : String(err);
2520
+ return { exitCode: 1, stdout: "", stderr: errorBlock(message) };
2521
+ }
2522
+ const coverageMerge = toCoverageMergePayload(merge);
1604
2523
  const config = await loadConfigFn({ cwd: deps.cwd });
1605
2524
  const ctx = await openRepoFn(deps.cwd);
1606
2525
  const peeked = await peekRepoIdentity({
@@ -1609,32 +2528,68 @@ async function executePush(cli, deps) {
1609
2528
  env,
1610
2529
  ctx
1611
2530
  });
2531
+ const coveragePaths = resolveCoveragePaths({
2532
+ ...cli.file && cli.file.length > 0 ? { files: cli.file } : {},
2533
+ env,
2534
+ configPath: config.coverage.path
2535
+ });
2536
+ const existingCoverage = existingCoveragePaths(coveragePaths, deps.cwd);
2537
+ const handshakeOnly = merge.complete && existingCoverage.length === 0 && (cli.complete === true || merge.totalParts !== void 0);
1612
2538
  let diff;
1613
- try {
1614
- let baseOverride = cli.base;
1615
- if (baseOverride === void 0 && prNumber !== null) {
1616
- const resolved = await resolvePrPushBase({
2539
+ let files = [];
2540
+ let addedByFile = /* @__PURE__ */ new Map();
2541
+ if (!handshakeOnly) {
2542
+ try {
2543
+ let baseOverride = cli.base;
2544
+ if (baseOverride === void 0 && prNumber !== null) {
2545
+ const resolved = await resolvePrPushBase({
2546
+ ctx,
2547
+ requested: config.base,
2548
+ prNumber,
2549
+ ...peeked.owner != null ? { owner: peeked.owner } : {},
2550
+ ...peeked.name != null ? { name: peeked.name } : {},
2551
+ fetchFn,
2552
+ env,
2553
+ onProgress
2554
+ });
2555
+ if (resolved !== void 0) baseOverride = resolved;
2556
+ }
2557
+ onProgress("computing diff\u2026");
2558
+ const diffOpts = {
2559
+ cwd: deps.cwd,
2560
+ config,
2561
+ ...baseOverride !== void 0 ? { baseRef: baseOverride } : {},
1617
2562
  ctx,
1618
- requested: config.base,
1619
- prNumber,
1620
- ...peeked.owner != null ? { owner: peeked.owner } : {},
1621
- ...peeked.name != null ? { name: peeked.name } : {},
1622
- fetchFn,
1623
- env,
1624
- onProgress
2563
+ ...coveragePaths.length > 0 ? { coveragePaths } : {}
2564
+ };
2565
+ if (computeDiffFn) {
2566
+ diff = await computeDiffFn(diffOpts);
2567
+ } else {
2568
+ const computed = await computeDiffContextFn(diffOpts);
2569
+ diff = computed.diff;
2570
+ files = computed.files;
2571
+ addedByFile = computed.addedByFile;
2572
+ }
2573
+ } catch (err) {
2574
+ const message = err instanceof Error ? err.message : String(err);
2575
+ return { exitCode: 1, stdout: "", stderr: errorBlock(message) };
2576
+ }
2577
+ } else {
2578
+ onProgress("complete handshake (no local coverage)\u2026");
2579
+ }
2580
+ let flags;
2581
+ if (!handshakeOnly && !computeDiffFn) {
2582
+ try {
2583
+ flags = resolveIngestFlagsJson({
2584
+ config,
2585
+ files,
2586
+ addedByFile,
2587
+ ...cli.flag && cli.flag.trim() ? { onlyFlag: cli.flag.trim() } : {}
1625
2588
  });
1626
- if (resolved !== void 0) baseOverride = resolved;
2589
+ } catch (err) {
2590
+ const message = err instanceof Error ? err.message : String(err);
2591
+ return { exitCode: 1, stdout: "", stderr: errorBlock(message) };
1627
2592
  }
1628
- onProgress("computing diff\u2026");
1629
- diff = await computeDiffFn({
1630
- cwd: deps.cwd,
1631
- config,
1632
- ...baseOverride !== void 0 ? { baseRef: baseOverride } : {},
1633
- ctx
1634
- });
1635
- } catch (err) {
1636
- const message = err instanceof Error ? err.message : String(err);
1637
- return { exitCode: 1, stdout: "", stderr: errorBlock(message) };
1638
2593
  }
1639
2594
  let owner = cli.owner ?? peeked.owner ?? void 0;
1640
2595
  let name = cli.name ?? peeked.name ?? void 0;
@@ -1690,13 +2645,19 @@ async function executePush(cli, deps) {
1690
2645
  env
1691
2646
  });
1692
2647
  if (junitPath) {
1693
- onProgress("parsing JUnit\u2026");
2648
+ onProgress(`parsing JUnit (${junitPath})\u2026`);
1694
2649
  testReport = loadTestReportFromJunit(junitPath);
1695
2650
  }
1696
2651
  } catch (err) {
1697
2652
  const message = err instanceof Error ? err.message : String(err);
1698
2653
  return { exitCode: 1, stdout: "", stderr: errorBlock(message) };
1699
2654
  }
2655
+ const mergeFields = {
2656
+ coverageMerge,
2657
+ ...diff ? { diff } : {},
2658
+ ...testReport ? { testReport } : {},
2659
+ ...flags ? { flags } : {}
2660
+ };
1700
2661
  const body = mainline ? buildMainlineIngestBody({
1701
2662
  owner,
1702
2663
  name,
@@ -1704,8 +2665,7 @@ async function executePush(cli, deps) {
1704
2665
  headSha: sha,
1705
2666
  ref: `refs/heads/${baseRef}`,
1706
2667
  runUrl: cli.runUrl ?? null,
1707
- diff,
1708
- ...testReport ? { testReport } : {}
2668
+ ...mergeFields
1709
2669
  }) : buildIngestBody({
1710
2670
  owner,
1711
2671
  name,
@@ -1716,10 +2676,11 @@ async function executePush(cli, deps) {
1716
2676
  headRef,
1717
2677
  headSha: sha,
1718
2678
  runUrl: cli.runUrl ?? null,
1719
- diff,
1720
- ...testReport ? { testReport } : {}
2679
+ ...mergeFields
1721
2680
  });
1722
- onProgress(mainline ? "uploading mainline coverage\u2026" : "uploading\u2026");
2681
+ onProgress(
2682
+ !merge.complete ? "uploading shard (incomplete)\u2026" : mainline ? "uploading mainline coverage\u2026" : handshakeOnly ? "sending complete handshake\u2026" : "uploading\u2026"
2683
+ );
1723
2684
  const result = await postIngest({ apiBase, token, body, fetchFn });
1724
2685
  if (!result.ok) {
1725
2686
  return {
@@ -1731,11 +2692,12 @@ async function executePush(cli, deps) {
1731
2692
  })
1732
2693
  };
1733
2694
  }
1734
- const formatted = formatPushSuccess(result.data, cli.json);
2695
+ const formatted = formatPushSuccess(result.data, cli.json, merge);
1735
2696
  return {
1736
2697
  exitCode: 0,
1737
2698
  stdout: formatted.stdout,
1738
2699
  stderr: formatted.stderr,
2700
+ complete: merge.complete,
1739
2701
  ...result.data.shareUrl !== void 0 ? { shareUrl: result.data.shareUrl } : {},
1740
2702
  ...result.data.expiresAt !== void 0 ? { expiresAt: result.data.expiresAt } : {}
1741
2703
  };
@@ -1759,7 +2721,15 @@ function registerPushCommand(program2) {
1759
2721
  "Base branch name sent to the API (default: .tested.yaml base or main)"
1760
2722
  ).option("--head-ref <ref>", "Head branch name (default: current branch)").option("--run-url <url>", "Optional CI run URL attached to the ingest").option("--base <ref>", "Git base ref to diff against (same as `tested diff --base`)").option(
1761
2723
  "--junit <path>",
1762
- "JUnit XML for test analytics (flakes / slowest). Also TESTED_JUNIT or junit.xml"
2724
+ "JUnit XML for test analytics (flakes / slowest). Also TESTED_JUNIT or auto-detect junit.xml / test-results/junit.xml / coverage/junit.xml"
2725
+ ).option(
2726
+ "--file <path>",
2727
+ "Coverage file to merge (repeatable). Overrides coverage.path.",
2728
+ collectCoverageFile,
2729
+ []
2730
+ ).option("--complete", "Conclude the gate (last shard / finish job)", false).option("--incomplete", "Upload a shard without concluding GitHub checks", false).option("--parts <n>", "Total shard count (Codecov after_n_builds / Qlty total-parts-count)").option("--part <n>", "1-based shard index").option("--run-id <id>", "CI run id grouping shards for one SHA (or TESTED_RUN_ID)").option("--shard <id>", "Optional shard label (or TESTED_SHARD)").option(
2731
+ "--flag <name>",
2732
+ "This coverage file is the named flag (job already scoped \u2014 omit other packages)"
1763
2733
  ).option("--json", "Emit machine-readable JSON instead of the share URL only", false).action(async (opts) => {
1764
2734
  try {
1765
2735
  const result = await executePush(opts, { cwd: process.cwd() });
@@ -1833,7 +2803,7 @@ function isReadableFile(path, exists) {
1833
2803
  async function runDoctor(deps) {
1834
2804
  const cwd = deps.cwd;
1835
2805
  const env = deps.env ?? process.env;
1836
- const exists = deps.existsSyncFn ?? existsSync3;
2806
+ const exists = deps.existsSyncFn ?? existsSync4;
1837
2807
  const gitFactory = deps.gitFactory ?? simpleGit3;
1838
2808
  const loadConfigFn = deps.loadConfigFn ?? loadConfig;
1839
2809
  const resolveTokenFn = deps.resolveTokenFn ?? resolveToken;
@@ -1880,7 +2850,7 @@ async function runDoctor(deps) {
1880
2850
  detail: "not a git repository \u2014 run from a repo root"
1881
2851
  });
1882
2852
  }
1883
- const configPath = join4(cwd, ".tested.yaml");
2853
+ const configPath = join5(cwd, ".tested.yaml");
1884
2854
  const hasConfig = exists(configPath);
1885
2855
  if (hasConfig) {
1886
2856
  checks.push({
@@ -1897,19 +2867,21 @@ async function runDoctor(deps) {
1897
2867
  detail: "missing \u2014 run: tested setup (or tested init)"
1898
2868
  });
1899
2869
  }
1900
- let coverageRel = "coverage/coverage-final.json";
2870
+ let coverageRels = ["coverage/coverage-final.json"];
1901
2871
  if (hasConfig) {
1902
2872
  try {
1903
2873
  const config = await loadConfigFn({ cwd });
1904
- coverageRel = config.coverage.path;
2874
+ const listed = coveragePathList(config.coverage.path);
2875
+ if (listed.length > 0) coverageRels = listed;
1905
2876
  } catch {
1906
2877
  }
1907
2878
  }
1908
- const coverageAbs = resolve4(cwd, coverageRel);
1909
- if (isReadableFile(coverageAbs, exists)) {
2879
+ const coverageRel = coverageRels.join(", ");
2880
+ const missing = coverageRels.filter((rel) => !isReadableFile(resolve5(cwd, rel), exists));
2881
+ if (missing.length === 0) {
1910
2882
  checks.push({
1911
2883
  id: "coverage",
1912
- label: "Coverage file",
2884
+ label: coverageRels.length > 1 ? "Coverage files" : "Coverage file",
1913
2885
  status: "pass",
1914
2886
  detail: coverageRel,
1915
2887
  optional: true
@@ -1917,9 +2889,9 @@ async function runDoctor(deps) {
1917
2889
  } else {
1918
2890
  checks.push({
1919
2891
  id: "coverage",
1920
- label: "Coverage file",
2892
+ label: coverageRels.length > 1 ? "Coverage files" : "Coverage file",
1921
2893
  status: "warn",
1922
- detail: `missing ${coverageRel} \u2014 run: tested run`,
2894
+ detail: `missing ${missing.join(", ")} \u2014 run: tested run`,
1923
2895
  optional: true
1924
2896
  });
1925
2897
  }
@@ -2042,7 +3014,7 @@ async function runDoctor(deps) {
2042
3014
  }
2043
3015
  const testedBin = env.TESTED_BIN;
2044
3016
  if (testedBin !== void 0 && testedBin !== "") {
2045
- const base = basename(testedBin);
3017
+ const base = basename2(testedBin);
2046
3018
  const okName = TESTED_BIN_BASENAME_RE.test(base);
2047
3019
  if (!okName) {
2048
3020
  checks.push({
@@ -2127,7 +3099,7 @@ import pc3 from "picocolors";
2127
3099
  // package.json
2128
3100
  var package_default = {
2129
3101
  name: "@tested/cli",
2130
- version: "0.1.7",
3102
+ version: "0.1.9",
2131
3103
  description: "Coverage your agent can use. CLI for patch + project coverage with agent-readable JSON output.",
2132
3104
  license: "MIT",
2133
3105
  homepage: "https://tested.dev",
@@ -2278,13 +3250,13 @@ function formatSetupHuman(opts) {
2278
3250
  async function runSetup(deps) {
2279
3251
  const cwd = deps.cwd;
2280
3252
  const env = deps.env ?? process.env;
2281
- const exists = deps.existsSyncFn ?? existsSync4;
3253
+ const exists = deps.existsSyncFn ?? existsSync5;
2282
3254
  const runInitFn = deps.runInitFn ?? runInit;
2283
3255
  const runDoctorFn = deps.runDoctorFn ?? runDoctor;
2284
3256
  const force = deps.force ?? false;
2285
3257
  const hooks = deps.hooks ?? false;
2286
3258
  const json = deps.json ?? false;
2287
- const configPath = join5(cwd, ".tested.yaml");
3259
+ const configPath = join6(cwd, ".tested.yaml");
2288
3260
  let initRan = false;
2289
3261
  let initResult = null;
2290
3262
  if (!exists(configPath) || force) {
@@ -2368,8 +3340,8 @@ function registerSetupCommand(program2) {
2368
3340
  }
2369
3341
 
2370
3342
  // src/commands/run.ts
2371
- import { existsSync as existsSync5 } from "fs";
2372
- import { isAbsolute as isAbsolute3, resolve as resolve5, sep as sep2 } from "path";
3343
+ import { existsSync as existsSync6 } from "fs";
3344
+ import { isAbsolute as isAbsolute3, resolve as resolve6, sep as sep2 } from "path";
2373
3345
  import { spawn } from "child_process";
2374
3346
  import "commander";
2375
3347
  function splitRunArgs(extraArgs) {
@@ -2439,8 +3411,8 @@ function shouldEnforceSafeRun(opts) {
2439
3411
  return false;
2440
3412
  }
2441
3413
  function configPathEscapesRoot(configPath, repoRoot) {
2442
- const root = resolve5(repoRoot);
2443
- const abs = isAbsolute3(configPath) ? resolve5(configPath) : resolve5(repoRoot, configPath);
3414
+ const root = resolve6(repoRoot);
3415
+ const abs = isAbsolute3(configPath) ? resolve6(configPath) : resolve6(repoRoot, configPath);
2444
3416
  const safeRoot = root.endsWith(sep2) ? root : root + sep2;
2445
3417
  return !(abs === root || abs.startsWith(safeRoot));
2446
3418
  }
@@ -2491,7 +3463,8 @@ function registerRunCommand(program2) {
2491
3463
  return;
2492
3464
  }
2493
3465
  const config = await loadConfig({ cwd });
2494
- const coveragePath = resolve5(cwd, config.coverage.path);
3466
+ const coverageRel = coveragePathList(config.coverage.path)[0] ?? "coverage/coverage-final.json";
3467
+ const coveragePath = resolve6(cwd, coverageRel);
2495
3468
  const { command, args } = resolveRunCommand({
2496
3469
  runner: config.testRunner,
2497
3470
  extraArgs: forwarded
@@ -2503,14 +3476,14 @@ function registerRunCommand(program2) {
2503
3476
  const child = spawn(command, args, { stdio: "inherit" });
2504
3477
  child.on("exit", (code) => {
2505
3478
  const exit = code ?? 1;
2506
- const coverageWritten = existsSync5(coveragePath);
3479
+ const coverageWritten = existsSync6(coveragePath);
2507
3480
  if (json) {
2508
3481
  const payload = buildRunJsonOutput({
2509
3482
  command,
2510
3483
  args,
2511
3484
  exitCode: exit,
2512
3485
  coverageWritten,
2513
- coveragePath: config.coverage.path
3486
+ coveragePath: coverageRel
2514
3487
  });
2515
3488
  process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
2516
3489
  process.exit(exit);
@@ -2524,7 +3497,7 @@ function registerRunCommand(program2) {
2524
3497
  process.stderr.write("\n");
2525
3498
  process.stderr.write(
2526
3499
  dim(
2527
- coverageWritten ? `tests failed (exit ${exit}); coverage still written to ${config.coverage.path}` : `tests failed (exit ${exit}); no coverage file at ${config.coverage.path}`
3500
+ coverageWritten ? `tests failed (exit ${exit}); coverage still written to ${coverageRel}` : `tests failed (exit ${exit}); no coverage file at ${coverageRel}`
2528
3501
  ) + "\n"
2529
3502
  );
2530
3503
  }
@@ -2665,21 +3638,29 @@ function formatHuman(out, opts = {}) {
2665
3638
 
2666
3639
  // src/commands/diff.ts
2667
3640
  function registerDiffCommand(program2) {
2668
- program2.command("diff").description("Compute patch + project coverage against a base ref").option("--base <ref>", "Git base ref to diff against", void 0).option("--with-base-coverage <path>", "Compare project coverage against a baseline JSON", void 0).option("--json", "Emit schema-v1 JSON instead of human text", false).action(async (opts) => {
3641
+ program2.command("diff").description("Compute patch + project coverage against a base ref").option("--base <ref>", "Git base ref to diff against", void 0).option("--with-base-coverage <path>", "Compare project coverage against a baseline JSON", void 0).option(
3642
+ "--file <path>",
3643
+ "Coverage file to merge (repeatable). Overrides coverage.path.",
3644
+ collectCoverageFile,
3645
+ []
3646
+ ).option("--json", "Emit schema-v1 JSON instead of human text", false).action(async (opts) => {
2669
3647
  try {
2670
3648
  const cwd = process.cwd();
2671
3649
  const config = await loadConfig({ cwd });
2672
- const output = await computeDiff({
3650
+ const { diff, files, addedByFile } = await computeDiffContext({
2673
3651
  cwd,
2674
3652
  config,
2675
3653
  ...opts.base !== void 0 ? { baseRef: opts.base } : {},
2676
- ...opts.withBaseCoverage !== void 0 ? { withBaseCoverage: opts.withBaseCoverage } : {}
3654
+ ...opts.withBaseCoverage !== void 0 ? { withBaseCoverage: opts.withBaseCoverage } : {},
3655
+ ...opts.file && opts.file.length > 0 ? { coveragePaths: opts.file } : {}
2677
3656
  });
2678
3657
  if (opts.json) {
2679
- process.stdout.write(JSON.stringify(output, null, 2) + "\n");
3658
+ const flags = resolveFlagsJson({ config, files, addedByFile });
3659
+ const payload = flags ? { ...diff, flags } : diff;
3660
+ process.stdout.write(JSON.stringify(payload, null, 2) + "\n");
2680
3661
  } else {
2681
3662
  process.stdout.write(
2682
- formatHuman(output, {
3663
+ formatHuman(diff, {
2683
3664
  ...config.thresholds ? { thresholds: config.thresholds } : {},
2684
3665
  tips: true
2685
3666
  }) + "\n"
@@ -2694,11 +3675,115 @@ function registerDiffCommand(program2) {
2694
3675
  }
2695
3676
 
2696
3677
  // src/commands/check.ts
3678
+ import { existsSync as existsSync7 } from "fs";
2697
3679
  import "commander";
2698
- function formatMetricLine(label, pct3, threshold, pass) {
3680
+ function formatIncompleteCheck(state, json) {
3681
+ const message = formatIncompleteGateMessage(state);
3682
+ if (json) {
3683
+ return {
3684
+ skipped: true,
3685
+ patchPass: true,
3686
+ projectPass: true,
3687
+ overall: "pass",
3688
+ flagResults: [],
3689
+ stdout: JSON.stringify({
3690
+ overall: "pending",
3691
+ complete: false,
3692
+ ...state.part !== void 0 ? { part: state.part } : {},
3693
+ ...state.totalParts !== void 0 ? { totalParts: state.totalParts } : {},
3694
+ note: message
3695
+ }) + "\n",
3696
+ stderr: "",
3697
+ exitCode: 0
3698
+ };
3699
+ }
3700
+ return {
3701
+ skipped: true,
3702
+ patchPass: true,
3703
+ projectPass: true,
3704
+ overall: "pass",
3705
+ flagResults: [],
3706
+ stdout: "",
3707
+ stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3708
+
3709
+ ${dim(` ${message}`)}
3710
+ ${tip("tested push --complete (or --parts N --part N)")}
3711
+ `,
3712
+ exitCode: 0
3713
+ };
3714
+ }
3715
+ function formatCompleteHandshakeCheck(json) {
3716
+ const note = "no local coverage files \u2014 complete handshake only. The app merges stored shards; this job does not evaluate the gate.";
3717
+ if (json) {
3718
+ return {
3719
+ skipped: true,
3720
+ patchPass: true,
3721
+ projectPass: true,
3722
+ overall: "pass",
3723
+ flagResults: [],
3724
+ stdout: JSON.stringify({ overall: "pending", complete: true, note }) + "\n",
3725
+ stderr: "",
3726
+ exitCode: 0
3727
+ };
3728
+ }
3729
+ return {
3730
+ skipped: true,
3731
+ patchPass: true,
3732
+ projectPass: true,
3733
+ overall: "pass",
3734
+ flagResults: [],
3735
+ stdout: "",
3736
+ stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
3737
+
3738
+ ${dim(` ${note}`)}
3739
+ `,
3740
+ exitCode: 0
3741
+ };
3742
+ }
3743
+ function formatMetricLine(label, pct3, threshold, pass, indent = " ") {
2699
3744
  const pctStr = pct3.toFixed(1);
2700
3745
  const status = pass ? badge("pass") : badge("fail");
2701
- return ` ${label.padEnd(8)} ${pctStr}% (threshold ${threshold}) ${status}`;
3746
+ return `${indent}${label.padEnd(8)} ${pctStr}% (threshold ${threshold}) ${status}`;
3747
+ }
3748
+ function presentPct(metric) {
3749
+ if (metric.pct === void 0 || metric.pass === void 0) {
3750
+ throw new Error("present flag metric missing pct/pass");
3751
+ }
3752
+ return { pct: metric.pct, pass: metric.pass };
3753
+ }
3754
+ function formatFlagLines(results) {
3755
+ if (results.length === 0) return [];
3756
+ const lines = [""];
3757
+ for (const flag of results) {
3758
+ if (flag.status === "missing") {
3759
+ lines.push(
3760
+ ` ${flag.name} ${dim(flag.reason ?? "missing this run")} ${badge("missing")}`
3761
+ );
3762
+ continue;
3763
+ }
3764
+ lines.push(` ${flag.name} ${flag.status === "pass" ? badge("pass") : badge("fail")}`);
3765
+ if (flag.patch.skipped) {
3766
+ lines.push(
3767
+ ` ${"Patch".padEnd(8)} ${dim("-")} ${EMPTY_PATCH_REASON} ${badge("skip")}`
3768
+ );
3769
+ } else {
3770
+ const patch = presentPct(flag.patch);
3771
+ lines.push(
3772
+ formatMetricLine("Patch", patch.pct, flag.patch.threshold, patch.pass, " ")
3773
+ );
3774
+ }
3775
+ const project = presentPct(flag.project);
3776
+ lines.push(
3777
+ formatMetricLine(
3778
+ "Project",
3779
+ project.pct,
3780
+ flag.project.threshold,
3781
+ project.pass,
3782
+ " "
3783
+ )
3784
+ );
3785
+ }
3786
+ return lines;
2702
3787
  }
2703
3788
  function runCheck(input) {
2704
3789
  const { config, diff, json } = input;
@@ -2708,6 +3793,7 @@ function runCheck(input) {
2708
3793
  patchPass: true,
2709
3794
  projectPass: true,
2710
3795
  overall: "pass",
3796
+ flagResults: [],
2711
3797
  stdout: "",
2712
3798
  stderr: `${dim("tested.dev \u2014 coverage gate")} ${badge("info")}
2713
3799
 
@@ -2724,7 +3810,14 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
2724
3810
  const patchSkipped = isEmptyPatch(diff.patch);
2725
3811
  const patchPass = patchSkipped ? true : patchPct >= patchThreshold;
2726
3812
  const projectPass = projectPct >= projectThreshold;
2727
- const overall = patchPass && projectPass ? "pass" : "fail";
3813
+ const flagResults = evaluateFlags({
3814
+ config,
3815
+ files: input.files ?? [],
3816
+ addedByFile: input.addedByFile ?? /* @__PURE__ */ new Map(),
3817
+ ...input.onlyFlag !== void 0 ? { onlyFlag: input.onlyFlag } : {}
3818
+ });
3819
+ const flagsOk = flagsPass(flagResults);
3820
+ const overall = patchPass && projectPass && flagsOk ? "pass" : "fail";
2728
3821
  const exitCode = overall === "pass" ? 0 : 1;
2729
3822
  if (json) {
2730
3823
  const payload = {
@@ -2735,6 +3828,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
2735
3828
  ...patchSkipped ? { skipped: true, reason: EMPTY_PATCH_REASON } : {}
2736
3829
  },
2737
3830
  project: { pct: projectPct, threshold: projectThreshold, pass: projectPass },
3831
+ ...flagResults.length > 0 ? { flags: flagsToJson(flagResults) } : {},
2738
3832
  overall,
2739
3833
  ...patchSkipped ? { note: EMPTY_PATCH_REASON } : {}
2740
3834
  };
@@ -2743,6 +3837,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
2743
3837
  patchPass,
2744
3838
  projectPass,
2745
3839
  overall,
3840
+ flagResults,
2746
3841
  stdout: JSON.stringify(payload) + "\n",
2747
3842
  stderr: "",
2748
3843
  exitCode
@@ -2761,11 +3856,20 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
2761
3856
  lines.push(formatMetricLine("Patch", patchPct, patchThreshold, patchPass));
2762
3857
  }
2763
3858
  lines.push(formatMetricLine("Project", projectPct, projectThreshold, projectPass));
3859
+ lines.push(...formatFlagLines(flagResults));
2764
3860
  if (overall === "fail") {
2765
3861
  lines.push("");
2766
3862
  if (patchSkipped) {
2767
3863
  lines.push(dim("No executable lines in the patch \u2014 patch gate skipped."));
2768
3864
  }
3865
+ const missing = flagResults.filter((f) => f.status === "missing");
3866
+ if (missing.length > 0) {
3867
+ lines.push(
3868
+ dim(
3869
+ "Flags with no files this run are skipped (not 0%). Scope the job with --flag when this coverage file is one package."
3870
+ )
3871
+ );
3872
+ }
2769
3873
  lines.push(tip("add tests for uncovered ranges: tested diff"));
2770
3874
  } else {
2771
3875
  lines.push("");
@@ -2781,6 +3885,7 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
2781
3885
  patchPass,
2782
3886
  projectPass,
2783
3887
  overall,
3888
+ flagResults,
2784
3889
  stdout: lines.join("\n"),
2785
3890
  stderr: "",
2786
3891
  exitCode
@@ -2789,10 +3894,47 @@ ${tip("add thresholds.patch / thresholds.project to enforce")}
2789
3894
  function registerCheckCommand(program2) {
2790
3895
  program2.command("check").description(
2791
3896
  "Exit non-zero if patch or project coverage falls below configured thresholds."
2792
- ).option("--json", "Emit machine-readable JSON to stdout (exit code unchanged).", false).option("--base <ref>", "Git base ref to diff against", void 0).action(async (opts) => {
3897
+ ).option("--json", "Emit machine-readable JSON to stdout (exit code unchanged).", false).option("--base <ref>", "Git base ref to diff against", void 0).option(
3898
+ "--file <path>",
3899
+ "Coverage file to merge (repeatable). Overrides coverage.path.",
3900
+ collectCoverageFile,
3901
+ []
3902
+ ).option("--complete", "Conclude the gate (last shard / finish job)", false).option("--incomplete", "Do not evaluate the gate (shard 1 of N)", false).option("--parts <n>", "Total shard count (gate waits until last part / --complete)").option("--part <n>", "1-based shard index").option(
3903
+ "--flag <name>",
3904
+ "Evaluate only this flag (job already scoped \u2014 the coverage file is the flag)"
3905
+ ).action(async (opts) => {
2793
3906
  try {
2794
3907
  const cwd = process.cwd();
2795
3908
  const config = await loadConfig({ cwd });
3909
+ let merge;
3910
+ try {
3911
+ merge = resolveCoverageMerge(opts);
3912
+ } catch (err) {
3913
+ const message = err instanceof Error ? err.message : String(err);
3914
+ process.stderr.write(formatCliError(message));
3915
+ process.exitCode = 1;
3916
+ return;
3917
+ }
3918
+ if (!merge.complete) {
3919
+ const result2 = formatIncompleteCheck(merge, opts.json);
3920
+ if (result2.stderr) process.stderr.write(result2.stderr);
3921
+ if (result2.stdout) process.stdout.write(result2.stdout);
3922
+ process.exitCode = result2.exitCode;
3923
+ return;
3924
+ }
3925
+ const coveragePaths = resolveCoveragePaths({
3926
+ ...opts.file && opts.file.length > 0 ? { files: opts.file } : {},
3927
+ configPath: config.coverage.path
3928
+ });
3929
+ const existing = existingCoveragePaths(coveragePaths, cwd, existsSync7);
3930
+ const handshakeOnly = merge.complete && existing.length === 0 && (opts.complete || merge.totalParts !== void 0);
3931
+ if (handshakeOnly) {
3932
+ const result2 = formatCompleteHandshakeCheck(opts.json);
3933
+ if (result2.stderr) process.stderr.write(result2.stderr);
3934
+ if (result2.stdout) process.stdout.write(result2.stdout);
3935
+ process.exitCode = result2.exitCode;
3936
+ return;
3937
+ }
2796
3938
  if (!config.thresholds) {
2797
3939
  const result2 = runCheck({
2798
3940
  config,
@@ -2813,12 +3955,20 @@ function registerCheckCommand(program2) {
2813
3955
  process.exitCode = result2.exitCode;
2814
3956
  return;
2815
3957
  }
2816
- const diff = await computeDiff({
3958
+ const { diff, files, addedByFile } = await computeDiffContext({
2817
3959
  cwd,
2818
3960
  config,
2819
- ...opts.base !== void 0 ? { baseRef: opts.base } : {}
3961
+ ...opts.base !== void 0 ? { baseRef: opts.base } : {},
3962
+ ...coveragePaths.length > 0 ? { coveragePaths } : {}
3963
+ });
3964
+ const result = runCheck({
3965
+ config,
3966
+ diff,
3967
+ json: opts.json,
3968
+ files,
3969
+ addedByFile,
3970
+ ...opts.flag && opts.flag.trim() ? { onlyFlag: opts.flag.trim() } : {}
2820
3971
  });
2821
- const result = runCheck({ config, diff, json: opts.json });
2822
3972
  if (result.stderr) process.stderr.write(result.stderr);
2823
3973
  if (result.stdout) process.stdout.write(result.stdout);
2824
3974
  process.exitCode = result.exitCode;
@@ -2831,8 +3981,8 @@ function registerCheckCommand(program2) {
2831
3981
  }
2832
3982
 
2833
3983
  // src/commands/explain.ts
2834
- import { readFile as readFile3 } from "fs/promises";
2835
- import { resolve as resolve6 } from "path";
3984
+ import { readFile as readFile5 } from "fs/promises";
3985
+ import { resolve as resolve7 } from "path";
2836
3986
  import "commander";
2837
3987
  function parseLocation(input) {
2838
3988
  const idx = input.lastIndexOf(":");
@@ -2893,9 +4043,12 @@ function registerExplainCommand(program2) {
2893
4043
  const { path: relPath, line } = parseLocation(location);
2894
4044
  const config = await loadConfig({ cwd });
2895
4045
  const ctx = await openRepo(cwd);
2896
- const coveragePath = resolve6(cwd, config.coverage.path);
2897
- assertWithinRoot(ctx.repoRoot, coveragePath);
2898
- const files = await parseIstanbul({ path: coveragePath, repoRoot: ctx.repoRoot });
4046
+ const files = await parseAndMergeCoverage({
4047
+ paths: resolveCoveragePaths({ configPath: config.coverage.path }),
4048
+ cwd,
4049
+ repoRoot: ctx.repoRoot,
4050
+ ...config.coverage.format ? { format: config.coverage.format } : {}
4051
+ });
2899
4052
  const file = files.find((f) => f.path === relPath);
2900
4053
  if (!file) {
2901
4054
  process.stderr.write(`error: no coverage data for ${relPath}
@@ -2903,9 +4056,9 @@ function registerExplainCommand(program2) {
2903
4056
  process.exitCode = 2;
2904
4057
  return;
2905
4058
  }
2906
- const resolvedSource = resolve6(ctx.repoRoot, relPath);
4059
+ const resolvedSource = resolve7(ctx.repoRoot, relPath);
2907
4060
  assertWithinRoot(ctx.repoRoot, resolvedSource);
2908
- const source = await readFile3(resolvedSource, "utf8");
4061
+ const source = await readFile5(resolvedSource, "utf8");
2909
4062
  const sourceLines = source.split("\n");
2910
4063
  const result = explainAt(file, line, sourceLines);
2911
4064
  if (opts.json) {
@@ -3017,11 +4170,11 @@ async function runToken(opts) {
3017
4170
  async function runWhoami(opts) {
3018
4171
  const env = opts.env ?? process.env;
3019
4172
  const identity = await resolveRepoIdentity(opts);
3020
- const resolve7 = opts.resolveTokenFn ?? resolveToken;
4173
+ const resolve8 = opts.resolveTokenFn ?? resolveToken;
3021
4174
  let tokenSet = false;
3022
4175
  let source = null;
3023
4176
  try {
3024
- const token = resolve7({ env, isTTY: false, warn: () => {
4177
+ const token = resolve8({ env, isTTY: false, warn: () => {
3025
4178
  } });
3026
4179
  tokenSet = Boolean(token);
3027
4180
  source = tokenSet ? tokenSourceFromEnv(env) : null;