@piwitests/reporter 0.53.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -453,9 +453,9 @@ var path6 = __toESM(require("path"));
453
453
  function defaultDesktopConfigPath() {
454
454
  return path6.join(os.homedir(), ".piwi", "desktop.json");
455
455
  }
456
- function readDesktopConfig(filePath) {
456
+ function readDesktopConfig(filePath2) {
457
457
  try {
458
- const parsed = JSON.parse(fs3.readFileSync(filePath, "utf8"));
458
+ const parsed = JSON.parse(fs3.readFileSync(filePath2, "utf8"));
459
459
  const url = parsed?.url;
460
460
  const token = parsed?.token;
461
461
  if (typeof url !== "string" || !url) return null;
@@ -708,9 +708,9 @@ function realFolder(dir) {
708
708
  return path7.join(fs4.realpathSync(current), ...rest);
709
709
  }
710
710
  function folderInRepository(root, dir) {
711
- const relative6 = path7.relative(realFolder(root), realFolder(dir));
712
- if (relative6.startsWith("..") || path7.isAbsolute(relative6)) return void 0;
713
- return relative6.split(path7.sep).join("/");
711
+ const relative8 = path7.relative(realFolder(root), realFolder(dir));
712
+ if (relative8.startsWith("..") || path7.isAbsolute(relative8)) return void 0;
713
+ return relative8.split(path7.sep).join("/");
714
714
  }
715
715
  function fileFilter(file) {
716
716
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -735,12 +735,12 @@ function runPlaywright(file, cwd, id, env) {
735
735
  cwd,
736
736
  env: childEnv
737
737
  });
738
- return new Promise((resolve8) => {
738
+ return new Promise((resolve10) => {
739
739
  child.on("error", (err) => {
740
740
  console.error(`piwi bug: could not start Playwright \u2014 ${err.message}`);
741
- resolve8(EXIT_ERROR2);
741
+ resolve10(EXIT_ERROR2);
742
742
  });
743
- child.on("exit", (code) => resolve8(code ?? EXIT_ERROR2));
743
+ child.on("exit", (code) => resolve10(code ?? EXIT_ERROR2));
744
744
  });
745
745
  }
746
746
  async function runBug(argv, env = process.env, cwd = process.cwd()) {
@@ -2285,43 +2285,43 @@ var Checker = class {
2285
2285
  constructor() {
2286
2286
  this.errors = [];
2287
2287
  }
2288
- fail(path19, message) {
2289
- if (this.errors.length < 20) this.errors.push(`${path19}: ${message}`);
2288
+ fail(path23, message) {
2289
+ if (this.errors.length < 20) this.errors.push(`${path23}: ${message}`);
2290
2290
  }
2291
- text(v, path19, max, nullable) {
2291
+ text(v, path23, max, nullable) {
2292
2292
  if (v == null) {
2293
- if (!nullable) this.fail(path19, "is required");
2293
+ if (!nullable) this.fail(path23, "is required");
2294
2294
  return null;
2295
2295
  }
2296
2296
  if (typeof v !== "string") {
2297
- this.fail(path19, "must be a string");
2297
+ this.fail(path23, "must be a string");
2298
2298
  return null;
2299
2299
  }
2300
- if (v.length > max) this.fail(path19, `is longer than ${max} characters`);
2300
+ if (v.length > max) this.fail(path23, `is longer than ${max} characters`);
2301
2301
  return v;
2302
2302
  }
2303
- number(v, path19) {
2303
+ number(v, path23) {
2304
2304
  if (typeof v !== "number" || !Number.isFinite(v)) {
2305
- this.fail(path19, "must be a number");
2305
+ this.fail(path23, "must be a number");
2306
2306
  return 0;
2307
2307
  }
2308
2308
  return v;
2309
2309
  }
2310
2310
  };
2311
- function checkTarget(c, v, path19) {
2311
+ function checkTarget(c, v, path23) {
2312
2312
  if (v == null) return null;
2313
2313
  if (!isObject(v)) {
2314
- c.fail(path19, "must be an object");
2314
+ c.fail(path23, "must be an object");
2315
2315
  return null;
2316
2316
  }
2317
2317
  const alternativesIn = v.alternatives ?? [];
2318
2318
  const alternatives = [];
2319
- if (!Array.isArray(alternativesIn)) c.fail(`${path19}.alternatives`, "must be a list");
2319
+ if (!Array.isArray(alternativesIn)) c.fail(`${path23}.alternatives`, "must be a list");
2320
2320
  else if (alternativesIn.length > STEPS_LIMITS.alternatives)
2321
- c.fail(`${path19}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
2321
+ c.fail(`${path23}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
2322
2322
  else {
2323
2323
  alternativesIn.forEach((alt, i) => {
2324
- const at = `${path19}.alternatives[${i}]`;
2324
+ const at = `${path23}.alternatives[${i}]`;
2325
2325
  if (!isObject(alt)) return c.fail(at, "must be an object");
2326
2326
  const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
2327
2327
  const safe = locator ? safeLocator(locator) : null;
@@ -2335,31 +2335,31 @@ function checkTarget(c, v, path19) {
2335
2335
  });
2336
2336
  }
2337
2337
  return {
2338
- tagName: c.text(v.tagName, `${path19}.tagName`, 64, false) ?? "",
2339
- role: c.text(v.role, `${path19}.role`, 64, true),
2340
- accessibleName: c.text(v.accessibleName, `${path19}.accessibleName`, STEPS_LIMITS.textLength, true),
2341
- testId: c.text(v.testId, `${path19}.testId`, STEPS_LIMITS.textLength, true),
2342
- text: c.text(v.text, `${path19}.text`, STEPS_LIMITS.textLength, true),
2338
+ tagName: c.text(v.tagName, `${path23}.tagName`, 64, false) ?? "",
2339
+ role: c.text(v.role, `${path23}.role`, 64, true),
2340
+ accessibleName: c.text(v.accessibleName, `${path23}.accessibleName`, STEPS_LIMITS.textLength, true),
2341
+ testId: c.text(v.testId, `${path23}.testId`, STEPS_LIMITS.textLength, true),
2342
+ text: c.text(v.text, `${path23}.text`, STEPS_LIMITS.textLength, true),
2343
2343
  alternatives
2344
2344
  };
2345
2345
  }
2346
- function checkAssertion(c, v, path19) {
2346
+ function checkAssertion(c, v, path23) {
2347
2347
  if (!isObject(v)) {
2348
- c.fail(path19, "is required on an assert step");
2348
+ c.fail(path23, "is required on an assert step");
2349
2349
  return void 0;
2350
2350
  }
2351
2351
  const matcher = v.matcher;
2352
2352
  if (!ASSERTION_MATCHERS.includes(matcher)) {
2353
- c.fail(`${path19}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
2353
+ c.fail(`${path23}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
2354
2354
  return void 0;
2355
2355
  }
2356
- const expected = c.text(v.expected, `${path19}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
2356
+ const expected = c.text(v.expected, `${path23}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
2357
2357
  return {
2358
2358
  matcher,
2359
2359
  expected: VALUE_MATCHERS.has(matcher) ? expected : null,
2360
- actual: c.text(v.actual, `${path19}.actual`, STEPS_LIMITS.valueLength, true),
2360
+ actual: c.text(v.actual, `${path23}.actual`, STEPS_LIMITS.valueLength, true),
2361
2361
  negated: v.negated === true,
2362
- note: c.text(v.note, `${path19}.note`, STEPS_LIMITS.valueLength, true)
2362
+ note: c.text(v.note, `${path23}.note`, STEPS_LIMITS.valueLength, true)
2363
2363
  };
2364
2364
  }
2365
2365
  function checkViewports(c, v, stepCount) {
@@ -2400,40 +2400,40 @@ function checkViewports(c, v, stepCount) {
2400
2400
  });
2401
2401
  return out;
2402
2402
  }
2403
- function checkStep(c, v, path19) {
2403
+ function checkStep(c, v, path23) {
2404
2404
  if (!isObject(v)) {
2405
- c.fail(path19, "must be an object");
2405
+ c.fail(path23, "must be an object");
2406
2406
  return null;
2407
2407
  }
2408
2408
  const action = v.action;
2409
2409
  if (!ACTIONS.has(action)) {
2410
- c.fail(`${path19}.action`, "is not a known action");
2410
+ c.fail(`${path23}.action`, "is not a known action");
2411
2411
  return null;
2412
2412
  }
2413
2413
  const step = {
2414
2414
  action: action === "assertVisible" ? "assert" : action,
2415
- target: checkTarget(c, v.target, `${path19}.target`),
2416
- value: c.text(v.value, `${path19}.value`, STEPS_LIMITS.valueLength, true),
2415
+ target: checkTarget(c, v.target, `${path23}.target`),
2416
+ value: c.text(v.value, `${path23}.value`, STEPS_LIMITS.valueLength, true),
2417
2417
  redacted: v.redacted === true,
2418
- pageUrl: c.text(v.pageUrl, `${path19}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2419
- timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path19}.timestamp`)
2418
+ pageUrl: c.text(v.pageUrl, `${path23}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2419
+ timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path23}.timestamp`)
2420
2420
  };
2421
2421
  if (step.redacted) step.value = null;
2422
2422
  if (action === "hover" || action === "dblclick") {
2423
2423
  step.value = null;
2424
- if (!step.target) c.fail(`${path19}.target`, `is required on a ${action} step`);
2424
+ if (!step.target) c.fail(`${path23}.target`, `is required on a ${action} step`);
2425
2425
  }
2426
- if (action === "setInputFiles" && !step.target) c.fail(`${path19}.target`, "is required on a setInputFiles step");
2426
+ if (action === "setInputFiles" && !step.target) c.fail(`${path23}.target`, "is required on a setInputFiles step");
2427
2427
  if (action === "dragTo") {
2428
2428
  step.value = null;
2429
- if (!step.target) c.fail(`${path19}.target`, "is required on a dragTo step");
2430
- step.dropTarget = checkTarget(c, v.dropTarget, `${path19}.dropTarget`);
2431
- if (!step.dropTarget) c.fail(`${path19}.dropTarget`, "is required on a dragTo step");
2429
+ if (!step.target) c.fail(`${path23}.target`, "is required on a dragTo step");
2430
+ step.dropTarget = checkTarget(c, v.dropTarget, `${path23}.dropTarget`);
2431
+ if (!step.dropTarget) c.fail(`${path23}.dropTarget`, "is required on a dragTo step");
2432
2432
  }
2433
2433
  if (action === "assertVisible") {
2434
2434
  step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
2435
2435
  } else if (action === "assert") {
2436
- step.assertion = checkAssertion(c, v.assertion, `${path19}.assertion`);
2436
+ step.assertion = checkAssertion(c, v.assertion, `${path23}.assertion`);
2437
2437
  }
2438
2438
  return step;
2439
2439
  }
@@ -2779,12 +2779,1142 @@ ${USAGE3}`);
2779
2779
  return EXIT_OK3;
2780
2780
  }
2781
2781
 
2782
- // src/cli/flake.ts
2782
+ // src/cli/coverage.ts
2783
+ var path12 = __toESM(require("path"));
2784
+
2785
+ // ../core/src/codeowners.ts
2786
+ function patternParts(pattern) {
2787
+ let source = pattern;
2788
+ const anchored = source.startsWith("/");
2789
+ if (anchored) source = source.slice(1);
2790
+ if (source.endsWith("/")) source = source.slice(0, -1);
2791
+ const segments = source.split("/").filter((segment, i, all) => segment !== "**" || all[i - 1] !== "**");
2792
+ return { segments, floating: !anchored && !source.includes("/") };
2793
+ }
2794
+ function segmentMatches(pattern, segment) {
2795
+ let p = 0;
2796
+ let s = 0;
2797
+ let afterStar = -1;
2798
+ let starTook = 0;
2799
+ while (s < segment.length) {
2800
+ const char = pattern[p];
2801
+ if (char === "*") {
2802
+ afterStar = ++p;
2803
+ starTook = s;
2804
+ } else if (char !== void 0 && (char === "?" || char === segment[s])) {
2805
+ p++;
2806
+ s++;
2807
+ } else if (afterStar >= 0) {
2808
+ p = afterStar;
2809
+ s = ++starTook;
2810
+ } else {
2811
+ return false;
2812
+ }
2813
+ }
2814
+ while (pattern[p] === "*") p++;
2815
+ return p === pattern.length;
2816
+ }
2817
+ function pathPatternMatcher(pattern) {
2818
+ const { segments, floating } = patternParts(pattern);
2819
+ if (floating) {
2820
+ const only = segments[0];
2821
+ return (path23) => path23.split("/").some((part) => segmentMatches(only, part));
2822
+ }
2823
+ return (path23) => {
2824
+ const parts = path23.split("/");
2825
+ let at = new Uint8Array(parts.length);
2826
+ at[0] = 1;
2827
+ for (let i = 0; i < segments.length; i++) {
2828
+ const segment = segments[i];
2829
+ const isLast = i === segments.length - 1;
2830
+ if (segment === "**") {
2831
+ if (isLast) return at.includes(1);
2832
+ for (let j = 1; j < parts.length; j++) if (at[j - 1] && parts[j - 1] !== "") at[j] = 1;
2833
+ continue;
2834
+ }
2835
+ const next = new Uint8Array(parts.length);
2836
+ for (let j = 0; j < parts.length; j++) {
2837
+ if (!at[j] || !segmentMatches(segment, parts[j])) continue;
2838
+ if (isLast) return true;
2839
+ if (j + 1 < parts.length) next[j + 1] = 1;
2840
+ }
2841
+ at = next;
2842
+ }
2843
+ return false;
2844
+ };
2845
+ }
2846
+
2847
+ // ../core/src/coverage-scope.ts
2848
+ var DEFAULT_COVERAGE_EXCLUDES = [
2849
+ "*.test.*",
2850
+ "*.spec.*",
2851
+ "tests/",
2852
+ "test/",
2853
+ "__tests__/",
2854
+ "*Tests/",
2855
+ "*.Tests/",
2856
+ "obj/",
2857
+ "bin/",
2858
+ "node_modules/",
2859
+ "*.g.cs",
2860
+ "*.Designer.cs",
2861
+ "*.generated.*"
2862
+ ];
2863
+ var DEFAULT_COVERAGE_SCOPE = { include: [], exclude: [...DEFAULT_COVERAGE_EXCLUDES] };
2864
+ var MAX_SCOPE_PATTERNS = 200;
2865
+ var MAX_PATTERN_LENGTH = 300;
2866
+ var MAX_PATTERN_WILDCARDS = 20;
2867
+ function patterns(raw) {
2868
+ if (!Array.isArray(raw)) return null;
2869
+ const out = [];
2870
+ for (const item of raw) {
2871
+ if (typeof item !== "string") return null;
2872
+ const pattern = item.trim();
2873
+ if (!pattern || pattern.startsWith("#")) continue;
2874
+ const tooLarge = pattern.length > MAX_PATTERN_LENGTH || pattern.split("*").length - 1 > MAX_PATTERN_WILDCARDS;
2875
+ if (tooLarge || pattern.startsWith("!")) return null;
2876
+ if (!out.includes(pattern)) out.push(pattern);
2877
+ }
2878
+ return out.length > MAX_SCOPE_PATTERNS ? null : out;
2879
+ }
2880
+ function parseCoverageScope(raw) {
2881
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
2882
+ const record = raw;
2883
+ const include = record.include === void 0 ? [] : patterns(record.include);
2884
+ const exclude = record.exclude === void 0 ? [] : patterns(record.exclude);
2885
+ return include && exclude ? { include, exclude } : null;
2886
+ }
2887
+ function compileCoverageScope(scope) {
2888
+ const include = scope.include.map(pathPatternMatcher);
2889
+ const exclude = scope.exclude.map(pathPatternMatcher);
2890
+ return (path23) => {
2891
+ const file = path23.replace(/\\/g, "/").replace(/^\.?\//, "");
2892
+ if (include.length > 0 && !include.some((matches) => matches(file))) return false;
2893
+ return !exclude.some((matches) => matches(file));
2894
+ };
2895
+ }
2896
+
2897
+ // src/internal/collect/metadata-collector.ts
2898
+ var import_node_child_process3 = require("child_process");
2899
+
2900
+ // src/internal/support/errors.ts
2901
+ function errorMessage(error) {
2902
+ if (error instanceof Error) return error.message;
2903
+ return String(error);
2904
+ }
2905
+
2906
+ // src/internal/collect/metadata-collector.ts
2907
+ var DETACHED_HEAD = "HEAD";
2908
+ function resolveScmBranch(env, gitBranch) {
2909
+ const pick = (...names) => {
2910
+ for (const name of names) {
2911
+ const value = env[name]?.trim();
2912
+ if (value) return value;
2913
+ }
2914
+ return void 0;
2915
+ };
2916
+ const override = env.PIWI_BRANCH?.trim();
2917
+ if (override) return override;
2918
+ let provider;
2919
+ if (env.JENKINS_URL) provider = pick("CHANGE_BRANCH", "BRANCH_NAME");
2920
+ else if (env.GITHUB_ACTIONS) provider = pick("GITHUB_HEAD_REF", "GITHUB_REF_NAME");
2921
+ else if (env.GITLAB_CI) provider = pick("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME", "CI_COMMIT_REF_NAME");
2922
+ else if (env.CIRCLECI) provider = pick("CIRCLE_BRANCH");
2923
+ else if (env.TRAVIS) provider = pick("TRAVIS_PULL_REQUEST_BRANCH", "TRAVIS_BRANCH");
2924
+ else if (env.TF_BUILD) provider = pick("SYSTEM_PULLREQUEST_SOURCEBRANCH", "BUILD_SOURCEBRANCHNAME");
2925
+ else if (env.BITBUCKET_BUILD_NUMBER) provider = pick("BITBUCKET_BRANCH");
2926
+ if (provider) return normalizeRef(provider);
2927
+ const trimmed = gitBranch?.trim();
2928
+ if (trimmed && trimmed !== DETACHED_HEAD) return trimmed;
2929
+ return void 0;
2930
+ }
2931
+ function normalizeRef(ref) {
2932
+ return ref.replace(/^refs\/heads\//, "");
2933
+ }
2934
+
2935
+ // src/internal/coverage-upload/git-inventory.ts
2936
+ var import_node_child_process5 = require("child_process");
2937
+ var fs7 = __toESM(require("fs"));
2938
+ var path10 = __toESM(require("path"));
2939
+
2940
+ // src/internal/collect/blob-ids.ts
2783
2941
  var import_node_child_process4 = require("child_process");
2942
+ var crypto2 = __toESM(require("crypto"));
2943
+ var fs6 = __toESM(require("fs"));
2944
+ var path9 = __toESM(require("path"));
2945
+ function gitHashObjects(root, paths) {
2946
+ return new Promise((resolve10) => {
2947
+ const child = (0, import_node_child_process4.execFile)(
2948
+ "git",
2949
+ ["hash-object", "--stdin-paths"],
2950
+ { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
2951
+ (error, stdout) => {
2952
+ const ids = error ? [] : stdout.split("\n").filter(Boolean);
2953
+ resolve10(ids.length === paths.length ? ids : null);
2954
+ }
2955
+ );
2956
+ child.stdin?.on("error", () => {
2957
+ });
2958
+ child.stdin?.end(`${paths.join("\n")}
2959
+ `);
2960
+ });
2961
+ }
2962
+
2963
+ // src/internal/coverage-upload/git-inventory.ts
2964
+ var MAX_GIT_OUTPUT = 512 * 1024 * 1024;
2965
+ var GIT_BATCH = 500;
2966
+ function git(cwd, args) {
2967
+ return new Promise((resolve10, reject) => {
2968
+ (0, import_node_child_process5.execFile)(
2969
+ "git",
2970
+ args,
2971
+ { cwd, encoding: "utf8", maxBuffer: MAX_GIT_OUTPUT, timeout: 12e4, windowsHide: true },
2972
+ (error, stdout) => error ? reject(error) : resolve10(stdout)
2973
+ );
2974
+ });
2975
+ }
2976
+ async function readCheckout(cwd) {
2977
+ let root;
2978
+ try {
2979
+ root = (await git(cwd, ["rev-parse", "--show-toplevel"])).trim();
2980
+ } catch {
2981
+ throw new Error(`${cwd} is not inside a git checkout: coverage is uploaded from the checkout the tests ran in`);
2982
+ }
2983
+ const commit = (await git(root, ["rev-parse", "HEAD"])).trim().toLowerCase();
2984
+ const branch = (await git(root, ["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "")).trim();
2985
+ return { root, commit, ...branch && branch !== "HEAD" ? { gitBranch: branch } : {} };
2986
+ }
2987
+ async function hashPaths(root, paths) {
2988
+ const ids = /* @__PURE__ */ new Map();
2989
+ for (let start = 0; start < paths.length; start += GIT_BATCH) {
2990
+ const batch = paths.slice(start, start + GIT_BATCH);
2991
+ const hashed = await gitHashObjects(root, batch);
2992
+ batch.forEach((file, i) => {
2993
+ const id = hashed?.[i];
2994
+ if (id) ids.set(file, id);
2995
+ });
2996
+ }
2997
+ return ids;
2998
+ }
2999
+ async function readInventory(root, warn = () => {
3000
+ }) {
3001
+ const inventory = /* @__PURE__ */ new Map();
3002
+ for (const entry of (await git(root, ["ls-tree", "-r", "-z", "--full-tree", "HEAD"])).split("\0")) {
3003
+ const tab = entry.indexOf(" ");
3004
+ if (tab < 0) continue;
3005
+ const [, type, id] = entry.slice(0, tab).split(" ");
3006
+ if (type === "blob" && id) inventory.set(entry.slice(tab + 1), id);
3007
+ }
3008
+ const changed = [];
3009
+ for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
3010
+ if (!file || !inventory.has(file)) continue;
3011
+ if (fs7.existsSync(path10.join(root, file))) changed.push(file);
3012
+ else inventory.delete(file);
3013
+ }
3014
+ const ids = await hashPaths(root, changed);
3015
+ for (const file of changed) {
3016
+ const id = ids.get(file);
3017
+ if (id) inventory.set(file, id);
3018
+ else inventory.delete(file);
3019
+ }
3020
+ const unhashed = changed.length - ids.size;
3021
+ if (unhashed > 0) {
3022
+ warn(
3023
+ `git hash-object failed on ${unhashed} changed file${unhashed === 1 ? "" : "s"}: left out of the commit's inventory`
3024
+ );
3025
+ }
3026
+ return inventory;
3027
+ }
3028
+
3029
+ // src/internal/coverage-upload/report-files.ts
2784
3030
  var fs8 = __toESM(require("fs"));
2785
- var os2 = __toESM(require("os"));
2786
3031
  var path11 = __toESM(require("path"));
2787
3032
 
3033
+ // ../core/src/coverage.ts
3034
+ var MAX_LINE = 1e6;
3035
+ function isLine(value) {
3036
+ return typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= MAX_LINE;
3037
+ }
3038
+ function validRanges(raw) {
3039
+ if (!Array.isArray(raw)) return [];
3040
+ const out = [];
3041
+ for (const item of raw) {
3042
+ if (!Array.isArray(item) || item.length !== 2) continue;
3043
+ const [start, end] = item;
3044
+ if (isLine(start) && isLine(end) && start <= end) out.push([start, end]);
3045
+ }
3046
+ return out.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
3047
+ }
3048
+ function normalizeLineRanges(ranges) {
3049
+ const out = [];
3050
+ for (const range of validRanges(ranges)) {
3051
+ const last = out[out.length - 1];
3052
+ if (last && range[0] <= last[1] + 1) last[1] = Math.max(last[1], range[1]);
3053
+ else out.push(range);
3054
+ }
3055
+ return out;
3056
+ }
3057
+ function normalizeFunctionRanges(ranges) {
3058
+ const out = [];
3059
+ for (const range of validRanges(ranges)) {
3060
+ const last = out[out.length - 1];
3061
+ if (!last || last[0] !== range[0] || last[1] !== range[1]) out.push(range);
3062
+ }
3063
+ return out;
3064
+ }
3065
+
3066
+ // ../core/src/coverage-formats.ts
3067
+ var MAX_LINE2 = 1e6;
3068
+ var MAX_PENDING = 1024 * 1024;
3069
+ var EXECUTABLE = 1;
3070
+ var RAN = 2;
3071
+ var GREATER_THAN = 62;
3072
+ var DOUBLE_QUOTE = 34;
3073
+ var SINGLE_QUOTE = 39;
3074
+ function detectCoverageFormat(head) {
3075
+ const text = head.replace(/^/, "").trimStart();
3076
+ if (text.startsWith("{")) return /"files"\s*:/.test(text) ? "piwi" : null;
3077
+ if (text.startsWith("<"))
3078
+ return /<coverage[\s>]/.test(text) || !/<\w/.test(text.replace(/<[?!][^>]*>/g, "")) ? "cobertura" : null;
3079
+ return /^(?:TN|SF):/m.test(text) ? "lcov" : null;
3080
+ }
3081
+ var FileLines = class {
3082
+ constructor() {
3083
+ this.flags = new Uint8Array(64);
3084
+ this.length = 0;
3085
+ this.functions = /* @__PURE__ */ new Map();
3086
+ }
3087
+ line(number, hits) {
3088
+ if (!Number.isInteger(number) || number < 1 || number > MAX_LINE2) return;
3089
+ if (number >= this.flags.length) {
3090
+ const grown = new Uint8Array(Math.min(MAX_LINE2 + 1, Math.max(number + 1, this.flags.length * 2)));
3091
+ grown.set(this.flags);
3092
+ this.flags = grown;
3093
+ }
3094
+ this.flags[number] |= hits > 0 ? EXECUTABLE | RAN : EXECUTABLE;
3095
+ if (number >= this.length) this.length = number + 1;
3096
+ }
3097
+ /** A function by a key unique in the file; a later entry for the same key widens it and keeps it run. */
3098
+ fn(key, start, end, ran) {
3099
+ if (!(start >= 1 && start <= MAX_LINE2)) return;
3100
+ const last = end >= start && end <= MAX_LINE2 ? end : start;
3101
+ const known = this.functions.get(key);
3102
+ if (known) {
3103
+ known.start = Math.min(known.start, start);
3104
+ known.end = Math.max(known.end, last);
3105
+ known.ran ||= ran;
3106
+ } else {
3107
+ this.functions.set(key, { start, end: last, ran });
3108
+ }
3109
+ }
3110
+ toFile(path23) {
3111
+ const executable = [];
3112
+ const lines = [];
3113
+ const push = (ranges, line) => {
3114
+ const last = ranges[ranges.length - 1];
3115
+ if (last && last[1] === line - 1) last[1] = line;
3116
+ else ranges.push([line, line]);
3117
+ };
3118
+ for (let line = 1; line < this.length; line++) {
3119
+ const flags = this.flags[line];
3120
+ if (flags & EXECUTABLE) push(executable, line);
3121
+ if (flags & RAN) push(lines, line);
3122
+ }
3123
+ const all = [];
3124
+ const ran = [];
3125
+ for (const { start, end, ran: didRun } of this.functions.values()) {
3126
+ all.push([start, end]);
3127
+ if (didRun) ran.push([start, end]);
3128
+ }
3129
+ return {
3130
+ path: path23,
3131
+ executable,
3132
+ lines,
3133
+ functions: normalizeFunctionRanges(ran),
3134
+ allFunctions: normalizeFunctionRanges(all)
3135
+ };
3136
+ }
3137
+ };
3138
+ var ReportFiles = class {
3139
+ constructor() {
3140
+ this.byPath = /* @__PURE__ */ new Map();
3141
+ }
3142
+ of(path23) {
3143
+ let file = this.byPath.get(path23);
3144
+ if (!file) {
3145
+ file = new FileLines();
3146
+ this.byPath.set(path23, file);
3147
+ }
3148
+ return file;
3149
+ }
3150
+ toFiles() {
3151
+ return [...this.byPath].map(([path23, file]) => file.toFile(path23));
3152
+ }
3153
+ };
3154
+ var XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
3155
+ function decodeXml(text) {
3156
+ return text.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (whole, name) => {
3157
+ if (name[0] !== "#") return XML_ENTITIES[name.toLowerCase()] ?? whole;
3158
+ const code = name[1] === "x" || name[1] === "X" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
3159
+ return Number.isInteger(code) && code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
3160
+ });
3161
+ }
3162
+ function attributesOf(text) {
3163
+ const out = {};
3164
+ const pattern = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
3165
+ for (let match = pattern.exec(text); match; match = pattern.exec(text)) {
3166
+ out[match[1]] = decodeXml(match[2] ?? match[3] ?? "");
3167
+ }
3168
+ return out;
3169
+ }
3170
+ var NUMBER_ATTRIBUTE = /\snumber\s*=\s*(?:"([^"]*)"|'([^']*)')/;
3171
+ var HITS_ATTRIBUTE = /\shits\s*=\s*(?:"([^"]*)"|'([^']*)')/;
3172
+ function attributeValue(markup, pattern) {
3173
+ const match = pattern.exec(markup);
3174
+ return match ? match[1] ?? match[2] : void 0;
3175
+ }
3176
+ function integer(value) {
3177
+ if (value === void 0) return Number.NaN;
3178
+ const number = Number(value.trim());
3179
+ return Number.isFinite(number) ? Math.trunc(number) : Number.NaN;
3180
+ }
3181
+ var CoberturaReader = class {
3182
+ constructor() {
3183
+ /** The text not read yet: empty, or the markup still open, from its `<`. */
3184
+ this.pending = "";
3185
+ /** How many characters of that markup are scanned, and the quote open after them (0 when none). */
3186
+ this.scanned = 0;
3187
+ this.quote = 0;
3188
+ this.files = new ReportFiles();
3189
+ this.sources = [];
3190
+ /** The text inside the `<source>` being read, or null outside one. */
3191
+ this.sourceText = null;
3192
+ this.file = null;
3193
+ this.methodCount = 0;
3194
+ /** The method being read: its lines' extent and whether one ran. */
3195
+ this.method = null;
3196
+ }
3197
+ write(chunk) {
3198
+ let text = this.pending + chunk;
3199
+ let at = 0;
3200
+ for (; ; ) {
3201
+ const open = text.indexOf("<", at);
3202
+ if (open < 0) {
3203
+ if (this.sourceText !== null) this.sourceText += text.slice(at);
3204
+ at = text.length;
3205
+ break;
3206
+ }
3207
+ if (this.sourceText !== null) this.sourceText += text.slice(at, open);
3208
+ const close = this.tagEnd(text, open);
3209
+ if (close < 0) {
3210
+ at = open;
3211
+ break;
3212
+ }
3213
+ this.tag(text.slice(open, close + 1));
3214
+ at = close + 1;
3215
+ }
3216
+ text = text.slice(at);
3217
+ if (text.length > MAX_PENDING) throw new Error("The Cobertura report has a tag longer than 1 MB.");
3218
+ this.pending = text;
3219
+ }
3220
+ end() {
3221
+ this.write("");
3222
+ return { format: "cobertura", sources: this.sources, files: this.files.toFiles() };
3223
+ }
3224
+ /**
3225
+ * Where the markup starting at `open` ends: its `>`, past comments, CDATA and quoted attribute values; -1 if not
3226
+ * yet read. The text is scanned once, forward: an unfinished markup keeps how far it is scanned and its open quote
3227
+ * for the next chunk.
3228
+ */
3229
+ tagEnd(text, open) {
3230
+ const head = text.slice(open, open + 9);
3231
+ if (head.length < 9 && ("<!--".startsWith(head) || "<![CDATA[".startsWith(head))) return -1;
3232
+ if (head.startsWith("<!--")) return this.through(text, open, "-->", 4);
3233
+ if (head.startsWith("<![CDATA[")) return this.through(text, open, "]]>", 9);
3234
+ let quote4 = this.quote;
3235
+ for (let at = open + Math.max(1, this.scanned); at < text.length; at++) {
3236
+ const char = text.charCodeAt(at);
3237
+ if (quote4 !== 0) {
3238
+ if (char === quote4) quote4 = 0;
3239
+ } else if (char === GREATER_THAN) {
3240
+ this.scanned = 0;
3241
+ this.quote = 0;
3242
+ return at;
3243
+ } else if (char === DOUBLE_QUOTE || char === SINGLE_QUOTE) {
3244
+ quote4 = char;
3245
+ }
3246
+ }
3247
+ this.scanned = text.length - open;
3248
+ this.quote = quote4;
3249
+ return -1;
3250
+ }
3251
+ /** Where a comment or CDATA section starting at `open` ends: the last character of `end`; -1 if not yet read. */
3252
+ through(text, open, end, start) {
3253
+ const at = text.indexOf(end, open + Math.max(start, this.scanned));
3254
+ if (at < 0) {
3255
+ this.scanned = Math.max(start, text.length - open - end.length + 1);
3256
+ return -1;
3257
+ }
3258
+ this.scanned = 0;
3259
+ return at + end.length - 1;
3260
+ }
3261
+ tag(markup) {
3262
+ if (markup.startsWith("<line ") || markup.startsWith("<line ") || markup.startsWith("<line\n")) {
3263
+ this.line(integer(attributeValue(markup, NUMBER_ATTRIBUTE)), integer(attributeValue(markup, HITS_ATTRIBUTE)));
3264
+ return;
3265
+ }
3266
+ if (markup.startsWith("<![CDATA[")) {
3267
+ if (this.sourceText !== null) this.sourceText += markup.slice(9, -3);
3268
+ return;
3269
+ }
3270
+ if (markup.startsWith("<!") || markup.startsWith("<?")) return;
3271
+ const closing = markup[1] === "/";
3272
+ const match = /^<\/?([\w:.-]+)/.exec(markup);
3273
+ if (!match) return;
3274
+ const name = match[1].toLowerCase();
3275
+ const selfClosing = markup.endsWith("/>");
3276
+ if (closing) {
3277
+ this.closeTag(name);
3278
+ return;
3279
+ }
3280
+ const attributes = attributesOf(markup.slice(match[0].length));
3281
+ switch (name) {
3282
+ case "source":
3283
+ if (!selfClosing) this.sourceText = "";
3284
+ break;
3285
+ case "class":
3286
+ this.file = attributes.filename ? this.files.of(attributes.filename) : null;
3287
+ this.method = null;
3288
+ if (selfClosing) this.file = null;
3289
+ break;
3290
+ case "method":
3291
+ if (this.file) {
3292
+ const line = integer(attributes.line);
3293
+ this.method = { start: line, end: line, ran: false, key: `m${this.methodCount++}` };
3294
+ if (selfClosing) this.closeTag("method");
3295
+ }
3296
+ break;
3297
+ }
3298
+ }
3299
+ line(number, hits) {
3300
+ if (!Number.isInteger(number)) return;
3301
+ const { method } = this;
3302
+ if (method) {
3303
+ method.start = Number.isInteger(method.start) ? Math.min(method.start, number) : number;
3304
+ method.end = Number.isInteger(method.end) ? Math.max(method.end, number) : number;
3305
+ if (hits > 0) method.ran = true;
3306
+ } else if (this.file) {
3307
+ this.file.line(number, Number.isInteger(hits) ? hits : 0);
3308
+ }
3309
+ }
3310
+ closeTag(name) {
3311
+ if (name === "source" && this.sourceText !== null) {
3312
+ const source = decodeXml(this.sourceText).trim();
3313
+ if (source) this.sources.push(source);
3314
+ this.sourceText = null;
3315
+ } else if (name === "method" && this.method) {
3316
+ const { start, end, ran, key } = this.method;
3317
+ if (this.file && Number.isInteger(start)) this.file.fn(key, start, end, ran);
3318
+ this.method = null;
3319
+ } else if (name === "class") {
3320
+ this.file = null;
3321
+ this.method = null;
3322
+ }
3323
+ }
3324
+ };
3325
+ var LcovReader = class {
3326
+ constructor() {
3327
+ this.pending = "";
3328
+ this.files = new ReportFiles();
3329
+ this.file = null;
3330
+ /**
3331
+ * The functions of the current record by name (`FN`/`FNDA`), each name's in the order its `FN`s come, with how
3332
+ * many `FNDA`s of the name are read; and by index (`FNL`/`FNA`).
3333
+ */
3334
+ this.byName = /* @__PURE__ */ new Map();
3335
+ this.namesRun = /* @__PURE__ */ new Map();
3336
+ this.byIndex = /* @__PURE__ */ new Map();
3337
+ this.record = 0;
3338
+ }
3339
+ write(chunk) {
3340
+ const text = this.pending + chunk;
3341
+ const lines = text.split(/\r?\n/);
3342
+ this.pending = lines.pop() ?? "";
3343
+ if (this.pending.length > MAX_PENDING) throw new Error("The LCOV report has a line longer than 1 MB.");
3344
+ for (const line of lines) this.line(line);
3345
+ }
3346
+ end() {
3347
+ if (this.pending) this.line(this.pending);
3348
+ this.pending = "";
3349
+ this.endRecord();
3350
+ return { format: "lcov", sources: [], files: this.files.toFiles() };
3351
+ }
3352
+ line(raw) {
3353
+ const line = raw.trim();
3354
+ const colon = line.indexOf(":");
3355
+ const key = colon < 0 ? line : line.slice(0, colon);
3356
+ const value = colon < 0 ? "" : line.slice(colon + 1);
3357
+ switch (key) {
3358
+ case "SF":
3359
+ this.endRecord();
3360
+ this.file = value ? this.files.of(value) : null;
3361
+ this.record++;
3362
+ break;
3363
+ case "DA": {
3364
+ const [number, hits] = value.split(",");
3365
+ this.file?.line(integer(number), Math.max(0, integer(hits) || 0));
3366
+ break;
3367
+ }
3368
+ case "FN": {
3369
+ const parts = value.split(",");
3370
+ const start = integer(parts[0]);
3371
+ const withEnd = parts.length >= 3 && /^\s*\d+\s*$/.test(parts[1]);
3372
+ const end = withEnd ? integer(parts[1]) : start;
3373
+ const name = parts.slice(withEnd ? 2 : 1).join(",");
3374
+ if (this.file && Number.isInteger(start)) {
3375
+ const named = this.byName.get(name) ?? [];
3376
+ this.byName.set(name, named);
3377
+ named.push({ start, end });
3378
+ this.file.fn(`${this.record}:n:${named.length - 1}:${name}`, start, end, false);
3379
+ }
3380
+ break;
3381
+ }
3382
+ case "FNDA": {
3383
+ const comma = value.indexOf(",");
3384
+ const hits = integer(value.slice(0, comma));
3385
+ const name = value.slice(comma + 1);
3386
+ const nth = this.namesRun.get(name) ?? 0;
3387
+ this.namesRun.set(name, nth + 1);
3388
+ const known = this.byName.get(name)?.[nth];
3389
+ if (this.file && known && hits > 0) {
3390
+ this.file.fn(`${this.record}:n:${nth}:${name}`, known.start, known.end, true);
3391
+ }
3392
+ break;
3393
+ }
3394
+ case "FNL": {
3395
+ const [index, start, end] = value.split(",");
3396
+ const first = integer(start);
3397
+ if (this.file && index !== void 0 && Number.isInteger(first)) {
3398
+ const last = end === void 0 ? first : integer(end);
3399
+ this.byIndex.set(index, { start: first, end: last });
3400
+ this.file.fn(`${this.record}:i:${index}`, first, last, false);
3401
+ }
3402
+ break;
3403
+ }
3404
+ case "FNA": {
3405
+ const [index, hits] = value.split(",");
3406
+ const known = index === void 0 ? void 0 : this.byIndex.get(index);
3407
+ if (this.file && known && integer(hits) > 0) {
3408
+ this.file.fn(`${this.record}:i:${index}`, known.start, known.end, true);
3409
+ }
3410
+ break;
3411
+ }
3412
+ case "end_of_record":
3413
+ this.endRecord();
3414
+ break;
3415
+ }
3416
+ }
3417
+ endRecord() {
3418
+ this.file = null;
3419
+ this.byName = /* @__PURE__ */ new Map();
3420
+ this.namesRun = /* @__PURE__ */ new Map();
3421
+ this.byIndex = /* @__PURE__ */ new Map();
3422
+ }
3423
+ };
3424
+ var MAX_PIWI_REPORT = 512 * 1024 * 1024;
3425
+ var PiwiReader = class {
3426
+ constructor() {
3427
+ this.chunks = [];
3428
+ this.length = 0;
3429
+ }
3430
+ write(chunk) {
3431
+ this.length += chunk.length;
3432
+ if (this.length > MAX_PIWI_REPORT) throw new Error("The Piwi coverage report is larger than 512 MB.");
3433
+ this.chunks.push(chunk);
3434
+ }
3435
+ end() {
3436
+ let parsed;
3437
+ try {
3438
+ parsed = JSON.parse(this.chunks.join("").replace(/^/, ""));
3439
+ } catch {
3440
+ throw new Error("The Piwi coverage report is not JSON.");
3441
+ }
3442
+ const files = parsed?.files;
3443
+ if (!Array.isArray(files)) throw new Error('The Piwi coverage report has no "files" array.');
3444
+ const byPath = /* @__PURE__ */ new Map();
3445
+ for (const item of files) {
3446
+ if (typeof item?.path !== "string" || !item.path) continue;
3447
+ const file = {
3448
+ path: item.path,
3449
+ executable: normalizeLineRanges(item.executable),
3450
+ lines: normalizeLineRanges(item.lines),
3451
+ functions: normalizeFunctionRanges(item.functions),
3452
+ allFunctions: normalizeFunctionRanges(item.allFunctions)
3453
+ };
3454
+ const known = byPath.get(file.path);
3455
+ byPath.set(file.path, known ? mergeReportedFiles(known, file) : file);
3456
+ }
3457
+ return { format: "piwi", sources: [], files: [...byPath.values()] };
3458
+ }
3459
+ };
3460
+ var CoverageReportReader = class {
3461
+ constructor(format) {
3462
+ this.format = format;
3463
+ this.reader = format === "cobertura" ? new CoberturaReader() : format === "lcov" ? new LcovReader() : new PiwiReader();
3464
+ }
3465
+ /** Adds the next chunk of the report's text. Throws on a tag or a line longer than 1 MB. */
3466
+ write(chunk) {
3467
+ this.reader.write(chunk);
3468
+ }
3469
+ /** The report read; its files merged by path and sorted in the order the report first names them. */
3470
+ end() {
3471
+ return this.reader.end();
3472
+ }
3473
+ };
3474
+ function normalizeSlashPath(raw) {
3475
+ const absolute = raw.startsWith("/");
3476
+ const drive = /^[A-Za-z]:\//.exec(raw)?.[0] ?? "";
3477
+ const rest = drive ? raw.slice(drive.length) : absolute ? raw.slice(1) : raw;
3478
+ const out = [];
3479
+ for (const part of rest.split("/")) {
3480
+ if (part === "" || part === ".") continue;
3481
+ if (part === "..") {
3482
+ if (out.length === 0) return null;
3483
+ out.pop();
3484
+ } else {
3485
+ out.push(part);
3486
+ }
3487
+ }
3488
+ return (drive || (absolute ? "/" : "")) + out.join("/");
3489
+ }
3490
+ function isAbsolutePath(path23) {
3491
+ return path23.startsWith("/") || /^[A-Za-z]:\//.test(path23);
3492
+ }
3493
+ function relativeTo(path23, root) {
3494
+ const drive = /^[A-Za-z]:/.test(root);
3495
+ const base = root.endsWith("/") ? root : `${root}/`;
3496
+ const head = path23.slice(0, base.length);
3497
+ const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
3498
+ if (!inside) return null;
3499
+ const relative8 = path23.slice(base.length);
3500
+ return relative8 || null;
3501
+ }
3502
+ function reportPathCandidates(raw, { repoRoot, bases }) {
3503
+ const slash = (value) => value.replace(/\\/g, "/");
3504
+ const root = normalizeSlashPath(slash(repoRoot));
3505
+ const path23 = slash(raw.trim());
3506
+ if (!root || !path23 || path23.includes("\0")) return [];
3507
+ const out = [];
3508
+ const add = (absolute) => {
3509
+ const normalized = normalizeSlashPath(absolute);
3510
+ if (!normalized) return;
3511
+ const relative8 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
3512
+ if (relative8 && !out.includes(relative8)) out.push(relative8);
3513
+ };
3514
+ if (isAbsolutePath(path23)) {
3515
+ add(path23);
3516
+ } else {
3517
+ for (const base of bases) {
3518
+ const dir = slash(base);
3519
+ if (isAbsolutePath(dir)) add(`${dir}/${path23}`);
3520
+ }
3521
+ }
3522
+ return out;
3523
+ }
3524
+ function mergeReportedFiles(a, b) {
3525
+ return {
3526
+ path: a.path,
3527
+ executable: normalizeLineRanges([...a.executable, ...b.executable]),
3528
+ lines: normalizeLineRanges([...a.lines, ...b.lines]),
3529
+ functions: normalizeFunctionRanges([...a.functions, ...b.functions]),
3530
+ allFunctions: normalizeFunctionRanges([...a.allFunctions, ...b.allFunctions])
3531
+ };
3532
+ }
3533
+
3534
+ // src/internal/coverage-upload/report-files.ts
3535
+ var SKIPPED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git"]);
3536
+ function globSegmentRegExp(segment) {
3537
+ const source = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
3538
+ return new RegExp(`^${source}$`);
3539
+ }
3540
+ function expandPattern(pattern, cwd) {
3541
+ const normalized = pattern.replace(/\\/g, "/");
3542
+ if (!/[*?]/.test(normalized)) {
3543
+ const file = path11.resolve(cwd, normalized);
3544
+ return fs8.existsSync(file) && fs8.statSync(file).isFile() ? [file] : [];
3545
+ }
3546
+ const absolute = path11.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3547
+ const segments = normalized.split("/");
3548
+ const firstWild = segments.findIndex((s) => /[*?]/.test(s));
3549
+ const baseParts = segments.slice(0, firstWild);
3550
+ const base = absolute ? baseParts.join("/") || "/" : path11.resolve(cwd, ...baseParts);
3551
+ const rest = segments.slice(firstWild).filter((s) => s !== "");
3552
+ const out = [];
3553
+ const walk = (dir, at) => {
3554
+ if (at >= rest.length) return;
3555
+ const segment = rest[at];
3556
+ let entries;
3557
+ try {
3558
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
3559
+ } catch {
3560
+ return;
3561
+ }
3562
+ if (segment === "**") {
3563
+ walk(dir, at + 1);
3564
+ for (const entry of entries) {
3565
+ if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path11.join(dir, entry.name), at);
3566
+ }
3567
+ return;
3568
+ }
3569
+ const match = globSegmentRegExp(segment);
3570
+ const last = at === rest.length - 1;
3571
+ for (const entry of entries) {
3572
+ if (!match.test(entry.name)) continue;
3573
+ const full = path11.join(dir, entry.name);
3574
+ if (last && entry.isFile()) out.push(full);
3575
+ else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
3576
+ walk(full, at + 1);
3577
+ }
3578
+ }
3579
+ };
3580
+ walk(base, 0);
3581
+ return [...new Set(out)].sort();
3582
+ }
3583
+ async function readReport(file, format) {
3584
+ let reader = format ? new CoverageReportReader(format) : null;
3585
+ let head = "";
3586
+ for await (const chunk of fs8.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3587
+ if (!reader) {
3588
+ head += chunk;
3589
+ if (head.length < 4096) continue;
3590
+ const detected = detectCoverageFormat(head);
3591
+ if (!detected) throw new Error(`${file} is not a Cobertura, LCOV or Piwi coverage report`);
3592
+ reader = new CoverageReportReader(detected);
3593
+ reader.write(head);
3594
+ head = "";
3595
+ continue;
3596
+ }
3597
+ reader.write(chunk);
3598
+ }
3599
+ if (!reader) {
3600
+ const detected = detectCoverageFormat(head);
3601
+ if (!detected) throw new Error(`${file} is not a Cobertura, LCOV or Piwi coverage report`);
3602
+ reader = new CoverageReportReader(detected);
3603
+ reader.write(head);
3604
+ }
3605
+ return reader.end();
3606
+ }
3607
+ function reportBases(report, reportFile, options) {
3608
+ if (options.root) return [path11.resolve(options.cwd, options.root)];
3609
+ const dir = path11.dirname(path11.resolve(reportFile));
3610
+ const sources = report.sources.map(
3611
+ (source) => path11.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path11.resolve(dir, source)
3612
+ );
3613
+ return [...sources, options.cwd, dir, path11.dirname(dir), options.repoRoot];
3614
+ }
3615
+ function resolveReport(report, reportFile, options, into) {
3616
+ const bases = reportBases(report, reportFile, options);
3617
+ for (const file of report.files) {
3618
+ const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
3619
+ const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs8.existsSync(path11.join(options.repoRoot, candidate)));
3620
+ if (!match) {
3621
+ into.unmatched.push(file.path);
3622
+ continue;
3623
+ }
3624
+ if (!options.inScope(match)) {
3625
+ into.outOfScope++;
3626
+ continue;
3627
+ }
3628
+ const known = into.files.get(match);
3629
+ into.files.set(match, known ? mergeReportedFiles(known, file) : { ...file, path: match });
3630
+ }
3631
+ }
3632
+
3633
+ // src/cli/coverage.ts
3634
+ var EXIT_OK4 = 0;
3635
+ var EXIT_NOTHING = 1;
3636
+ var EXIT_ERROR4 = 2;
3637
+ var FILES_PER_REQUEST = 5e3;
3638
+ var BYTES_PER_REQUEST = 8 * 1024 * 1024;
3639
+ var INVENTORY_PER_REQUEST = 5e4;
3640
+ var SUITE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
3641
+ var USAGE4 = `
3642
+ piwi coverage upload \u2014 send a coverage report of a suite to the dashboard
3643
+
3644
+ Usage:
3645
+ npx @piwitests/reporter coverage upload <file or pattern>... --suite <name> [options]
3646
+
3647
+ Reports:
3648
+ <file or pattern> Cobertura, LCOV or Piwi report files; quote patterns ("**/coverage.cobertura.xml")
3649
+ --format <format> cobertura, lcov or piwi; detected from the content when absent
3650
+ --root <dir> The directory the reports' relative paths start from; tried
3651
+ against each report's <source>, the current directory, the
3652
+ report's directory and its parent, and the repository root otherwise
3653
+
3654
+ Suite:
3655
+ --suite <name> The suite the report covers, e.g. unit-back or unit-front
3656
+ --kind <kind> unit (the default) or e2e
3657
+
3658
+ Connection:
3659
+ --project <name|id> Project (env PIWI_PROJECT_NAME)
3660
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
3661
+ --api-key <key> API key (env PIWI_API_KEY)
3662
+ --branch <name> The branch (env PIWI_BRANCH, the CI's, else git's)
3663
+
3664
+ Other:
3665
+ --dry-run Read and resolve the reports, print the summary, send nothing
3666
+ --json Print the summary as JSON
3667
+ -h, --help Show this help
3668
+
3669
+ Exit codes: 0 uploaded, 1 nothing to upload, 2 the upload could not be made.
3670
+ `.trim();
3671
+ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
3672
+ "--suite",
3673
+ "--kind",
3674
+ "--format",
3675
+ "--root",
3676
+ "--project",
3677
+ "--server-url",
3678
+ "--api-key",
3679
+ "--branch"
3680
+ ]);
3681
+ function parseCoverageUploadArgs(argv, env) {
3682
+ const values = /* @__PURE__ */ new Map();
3683
+ const patterns2 = [];
3684
+ const flags = /* @__PURE__ */ new Set();
3685
+ for (let i = 0; i < argv.length; i++) {
3686
+ const arg = argv[i];
3687
+ const eq = arg.indexOf("=");
3688
+ const name = arg.startsWith("--") && eq > 0 ? arg.slice(0, eq) : arg;
3689
+ if (VALUE_OPTIONS.has(name)) {
3690
+ const value = eq > 0 && arg.startsWith("--") ? arg.slice(eq + 1) : argv[++i];
3691
+ if (value === void 0 || value === "") throw new Error(`${name} expects a value`);
3692
+ values.set(name, value);
3693
+ } else if (arg.startsWith("--")) {
3694
+ flags.add(arg);
3695
+ } else {
3696
+ patterns2.push(arg);
3697
+ }
3698
+ }
3699
+ const unknown = [...flags].filter((flag) => flag !== "--dry-run" && flag !== "--json");
3700
+ if (unknown.length > 0) throw new Error(`Unknown option ${unknown[0]}`);
3701
+ if (patterns2.length === 0) throw new Error("No report \u2014 name the Cobertura, LCOV or Piwi report files to upload");
3702
+ const suite = values.get("--suite") ?? "";
3703
+ if (!SUITE.test(suite)) {
3704
+ throw new Error('--suite names the suite: 1 to 64 letters, digits, ".", "_" or "-", e.g. unit-back');
3705
+ }
3706
+ const kind = values.get("--kind") ?? "unit";
3707
+ if (kind !== "unit" && kind !== "e2e") throw new Error("--kind is unit or e2e");
3708
+ const format = values.get("--format");
3709
+ if (format !== void 0 && format !== "cobertura" && format !== "lcov" && format !== "piwi") {
3710
+ throw new Error("--format is cobertura, lcov or piwi");
3711
+ }
3712
+ const serverUrl = (values.get("--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
3713
+ const dryRun = flags.has("--dry-run");
3714
+ if (!serverUrl && !dryRun) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
3715
+ const project = values.get("--project") ?? env.PIWI_PROJECT_NAME ?? "";
3716
+ if (!project && !dryRun) throw new Error("No project \u2014 pass --project or set PIWI_PROJECT_NAME");
3717
+ return {
3718
+ patterns: patterns2,
3719
+ suite,
3720
+ kind,
3721
+ ...format ? { format } : {},
3722
+ ...values.get("--root") ? { root: values.get("--root") } : {},
3723
+ project,
3724
+ serverUrl,
3725
+ apiKey: values.get("--api-key") ?? env.PIWI_API_KEY ?? null,
3726
+ ...values.get("--branch") ? { branch: values.get("--branch") } : {},
3727
+ dryRun,
3728
+ json: flags.has("--json")
3729
+ };
3730
+ }
3731
+ function jsonBytes(item) {
3732
+ return Buffer.byteLength(JSON.stringify(item)) + 1;
3733
+ }
3734
+ function splitUpload(head, files, inventory) {
3735
+ const parts = [];
3736
+ let nextFile = 0;
3737
+ let nextEntry = 0;
3738
+ do {
3739
+ const partFiles = [];
3740
+ const partInventory = [];
3741
+ let bytes = 0;
3742
+ const fits = (item) => {
3743
+ const size = jsonBytes(item);
3744
+ if (bytes > 0 && bytes + size > BYTES_PER_REQUEST) return false;
3745
+ bytes += size;
3746
+ return true;
3747
+ };
3748
+ while (nextFile < files.length && partFiles.length < FILES_PER_REQUEST && fits(files[nextFile])) {
3749
+ partFiles.push(files[nextFile++]);
3750
+ }
3751
+ while (nextEntry < inventory.length && partInventory.length < INVENTORY_PER_REQUEST && fits(inventory[nextEntry])) {
3752
+ partInventory.push(inventory[nextEntry++]);
3753
+ }
3754
+ parts.push({ ...head, ...partInventory.length > 0 ? { inventory: partInventory } : {}, files: partFiles });
3755
+ } while (nextFile < files.length || nextEntry < inventory.length);
3756
+ return parts;
3757
+ }
3758
+ function formatSummary(summary, dryRun) {
3759
+ const lines = [
3760
+ `${dryRun ? "Would upload" : "Uploaded"} ${summary.suite} for ${summary.commit.slice(0, 7)}${summary.branch ? ` (${summary.branch})` : ""}: ${summary.files} file${summary.files === 1 ? "" : "s"} from ${summary.reports.length} report${summary.reports.length === 1 ? "" : "s"}, ${summary.inventoryFiles} tracked files in scope.`
3761
+ ];
3762
+ if (summary.reportFiles !== void 0 && summary.reportFiles !== summary.files) {
3763
+ lines.push(`The dashboard's report of this commit and suite now covers ${summary.reportFiles} files.`);
3764
+ }
3765
+ if (summary.unmatched > 0) {
3766
+ lines.push(
3767
+ `${summary.unmatched} path${summary.unmatched === 1 ? "" : "s"} matched no file in the checkout: check --root.`
3768
+ );
3769
+ }
3770
+ if (summary.outOfScope > 0) lines.push(`${summary.outOfScope} files are outside the project's coverage scope.`);
3771
+ if (summary.supersededBy !== void 0) {
3772
+ const by = summary.supersededBy ? ` ${summary.supersededBy.slice(0, 7)}` : " a newer commit";
3773
+ lines.push(
3774
+ `${summary.commit.slice(0, 7)} was superseded by${by} on its branch: the dashboard keeps its totals as they were and drops this upload.`
3775
+ );
3776
+ }
3777
+ return lines.join("\n");
3778
+ }
3779
+ function authHeaders3(apiKey) {
3780
+ return apiKey ? { "X-API-Key": apiKey } : {};
3781
+ }
3782
+ async function fetchScope(args, projectId) {
3783
+ const res = await fetch(`${args.serverUrl}/api/projects/${projectId}`, { headers: authHeaders3(args.apiKey) });
3784
+ if (!res.ok) throw new Error(`Could not read the project (dashboard returned ${res.status})`);
3785
+ const body = await res.json();
3786
+ const raw = body.coverageScope ?? body.project?.coverageScope;
3787
+ return (raw ? parseCoverageScope(raw) : null) ?? DEFAULT_COVERAGE_SCOPE;
3788
+ }
3789
+ function warnOnStderr(message) {
3790
+ console.error(`piwi coverage upload: warning: ${message}`);
3791
+ }
3792
+ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
3793
+ const checkout = await readCheckout(cwd);
3794
+ const projectId = args.dryRun ? null : await resolveProjectId({ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project, key: "" });
3795
+ const scope = projectId === null ? DEFAULT_COVERAGE_SCOPE : await fetchScope(args, projectId);
3796
+ const inScope = compileCoverageScope(scope);
3797
+ const branch = args.branch ?? resolveScmBranch(env, checkout.gitBranch) ?? null;
3798
+ const reports = [...new Set(args.patterns.flatMap((pattern) => expandPattern(pattern, cwd)))];
3799
+ const inventory = await readInventory(checkout.root, warn);
3800
+ const resolved = { files: /* @__PURE__ */ new Map(), unmatched: [], outOfScope: 0 };
3801
+ const formats = /* @__PURE__ */ new Set();
3802
+ for (const report of reports) {
3803
+ const read = await readReport(report, args.format);
3804
+ formats.add(read.format);
3805
+ resolveReport(read, report, { repoRoot: checkout.root, cwd, root: args.root, inventory, inScope }, resolved);
3806
+ }
3807
+ const untracked = [...resolved.files.keys()].filter((file) => !inventory.has(file));
3808
+ const hashed = await hashPaths(checkout.root, untracked);
3809
+ if (hashed.size < untracked.length) {
3810
+ const unhashed = untracked.length - hashed.size;
3811
+ warn(
3812
+ `git hash-object failed on ${unhashed} untracked file${unhashed === 1 ? "" : "s"} the reports name: sent without a blob id`
3813
+ );
3814
+ }
3815
+ const files = [...resolved.files.values()].sort((a, b) => a.path < b.path ? -1 : 1).map((file) => {
3816
+ const blob = inventory.get(file.path) ?? hashed.get(file.path);
3817
+ return {
3818
+ path: file.path,
3819
+ ...blob ? { blob } : {},
3820
+ executable: file.executable,
3821
+ lines: file.lines,
3822
+ ...file.functions.length > 0 ? { functions: file.functions } : {},
3823
+ ...file.allFunctions.length > 0 ? { allFunctions: file.allFunctions } : {}
3824
+ };
3825
+ });
3826
+ const inScopeInventory = [...inventory].filter(([file]) => inScope(file)).sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
3827
+ const parts = splitUpload(
3828
+ {
3829
+ v: 1,
3830
+ commit: checkout.commit,
3831
+ ...branch ? { branch } : {},
3832
+ suite: args.suite,
3833
+ kind: args.kind,
3834
+ tool: [...formats].sort().join("+") || "unknown",
3835
+ ...resolved.unmatched.length > 0 ? { incomplete: true } : {}
3836
+ },
3837
+ files,
3838
+ inScopeInventory
3839
+ );
3840
+ const summary = {
3841
+ commit: checkout.commit,
3842
+ branch,
3843
+ suite: args.suite,
3844
+ reports: reports.map((report) => path12.relative(cwd, report) || report),
3845
+ files: files.length,
3846
+ unmatched: resolved.unmatched.length,
3847
+ outOfScope: resolved.outOfScope,
3848
+ inventoryFiles: inScopeInventory.length,
3849
+ requests: parts.length
3850
+ };
3851
+ if (reports.length === 0 || files.length === 0 || args.dryRun || projectId === null) {
3852
+ return { summary, code: reports.length === 0 || files.length === 0 ? EXIT_NOTHING : EXIT_OK4 };
3853
+ }
3854
+ for (const part of parts) {
3855
+ const res = await fetch(`${args.serverUrl}/api/projects/${projectId}/coverage-reports`, {
3856
+ method: "POST",
3857
+ headers: { "Content-Type": "application/json", ...authHeaders3(args.apiKey) },
3858
+ body: JSON.stringify(part)
3859
+ });
3860
+ if (!res.ok) {
3861
+ const text = await res.text().catch(() => "");
3862
+ throw new Error(`The dashboard refused the upload (${res.status})${text ? `: ${text.slice(0, 400)}` : ""}`);
3863
+ }
3864
+ const answer = await res.json();
3865
+ if (typeof answer.report?.files === "number") summary.reportFiles = answer.report.files;
3866
+ if (answer.superseded) summary.supersededBy = answer.superseded.by ?? null;
3867
+ }
3868
+ return { summary, code: EXIT_OK4 };
3869
+ }
3870
+ function failureMessage(error, args) {
3871
+ const cause = error instanceof TypeError ? error.cause : void 0;
3872
+ if (cause) {
3873
+ return `Could not reach the dashboard at ${args.serverUrl}: ${cause.message || cause.code || errorMessage(error)}`;
3874
+ }
3875
+ return errorMessage(error);
3876
+ }
3877
+ async function runCoverage(argv, env = process.env, cwd = process.cwd()) {
3878
+ const [subcommand, ...rest] = argv;
3879
+ if (subcommand === void 0 || subcommand === "-h" || subcommand === "--help" || rest.includes("--help") || rest.includes("-h")) {
3880
+ console.log(USAGE4);
3881
+ return subcommand === void 0 ? EXIT_ERROR4 : EXIT_OK4;
3882
+ }
3883
+ if (subcommand !== "upload") {
3884
+ console.error(`piwi coverage: unknown subcommand "${subcommand}"
3885
+
3886
+ ${USAGE4}`);
3887
+ return EXIT_ERROR4;
3888
+ }
3889
+ let args;
3890
+ try {
3891
+ args = parseCoverageUploadArgs(rest, env);
3892
+ } catch (error) {
3893
+ console.error(`piwi coverage upload: ${errorMessage(error)}`);
3894
+ return EXIT_ERROR4;
3895
+ }
3896
+ try {
3897
+ const { summary, code } = await uploadCoverage(args, env, cwd);
3898
+ if (args.json) console.log(JSON.stringify(summary, null, 2));
3899
+ else if (summary.reports.length === 0) console.error("piwi coverage upload: no report matched the files named");
3900
+ else if (summary.files === 0) {
3901
+ console.error(
3902
+ `piwi coverage upload: none of the ${summary.unmatched + summary.outOfScope} files the reports name is a file of the checkout in the coverage scope: check --root`
3903
+ );
3904
+ } else console.log(formatSummary(summary, args.dryRun));
3905
+ return code;
3906
+ } catch (error) {
3907
+ console.error(`piwi coverage upload: ${failureMessage(error, args)}`);
3908
+ return EXIT_ERROR4;
3909
+ }
3910
+ }
3911
+
3912
+ // src/cli/flake.ts
3913
+ var import_node_child_process7 = require("child_process");
3914
+ var fs11 = __toESM(require("fs"));
3915
+ var os2 = __toESM(require("os"));
3916
+ var path15 = __toESM(require("path"));
3917
+
2788
3918
  // ../core/src/error-parse.ts
2789
3919
  var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
2790
3920
  var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
@@ -2951,7 +4081,7 @@ function verifyVerdict(arm, reproducedRate) {
2951
4081
  }
2952
4082
  var EXIT_REPRODUCED = 0;
2953
4083
  var EXIT_NOT_REPRODUCED = 1;
2954
- var EXIT_ERROR4 = 2;
4084
+ var EXIT_ERROR5 = 2;
2955
4085
  var EXIT_BISECT_SKIP = 125;
2956
4086
  function exitCodeFor(verdict) {
2957
4087
  return verdict === "reproduced" || verdict === "verified" ? EXIT_REPRODUCED : EXIT_NOT_REPRODUCED;
@@ -2984,8 +4114,8 @@ function withinBudget(startedAt, now, budgetMs) {
2984
4114
  }
2985
4115
 
2986
4116
  // src/internal/support/connection.ts
2987
- var fs6 = __toESM(require("fs"));
2988
- var path9 = __toESM(require("path"));
4117
+ var fs9 = __toESM(require("fs"));
4118
+ var path13 = __toESM(require("path"));
2989
4119
 
2990
4120
  // ../core/src/dotenv.ts
2991
4121
  function parseDotEnv(text) {
@@ -3026,7 +4156,7 @@ function resolveCliConnection(flags, env, dirs) {
3026
4156
  let dotEnv = {};
3027
4157
  for (const dir of dirs) {
3028
4158
  try {
3029
- dotEnv = { ...parseDotEnv(fs6.readFileSync(path9.join(dir, ".env"), "utf-8")), ...dotEnv };
4159
+ dotEnv = { ...parseDotEnv(fs9.readFileSync(path13.join(dir, ".env"), "utf-8")), ...dotEnv };
3030
4160
  } catch {
3031
4161
  }
3032
4162
  }
@@ -3039,12 +4169,12 @@ function resolveCliConnection(flags, env, dirs) {
3039
4169
  }
3040
4170
 
3041
4171
  // src/internal/support/playwright-spawn.ts
3042
- var import_node_child_process3 = require("child_process");
3043
- var fs7 = __toESM(require("fs"));
4172
+ var import_node_child_process6 = require("child_process");
4173
+ var fs10 = __toESM(require("fs"));
3044
4174
  var import_node_module2 = require("module");
3045
- var path10 = __toESM(require("path"));
4175
+ var path14 = __toESM(require("path"));
3046
4176
  function resolvePlaywrightCli2(cwd = process.cwd()) {
3047
- const require2 = (0, import_node_module2.createRequire)(path10.join(cwd, "noop.js"));
4177
+ const require2 = (0, import_node_module2.createRequire)(path14.join(cwd, "noop.js"));
3048
4178
  for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
3049
4179
  try {
3050
4180
  return require2.resolve(id);
@@ -3055,18 +4185,18 @@ function resolvePlaywrightCli2(cwd = process.cwd()) {
3055
4185
  }
3056
4186
  function spawnPlaywright(playwrightArgs2, env, options = {}) {
3057
4187
  const output = options.output ?? "inherit";
3058
- let fd = output === "inherit" ? null : fs7.openSync(output.file, "a");
4188
+ let fd = output === "inherit" ? null : fs10.openSync(output.file, "a");
3059
4189
  const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
3060
4190
  const cli = resolvePlaywrightCli2();
3061
- const child = cli ? (0, import_node_child_process3.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio, env }) : (0, import_node_child_process3.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs2], {
4191
+ const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio, env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs2], {
3062
4192
  stdio,
3063
4193
  env
3064
4194
  });
3065
- return new Promise((resolve8) => {
4195
+ return new Promise((resolve10) => {
3066
4196
  const done = (code) => {
3067
- if (fd !== null) fs7.closeSync(fd);
4197
+ if (fd !== null) fs10.closeSync(fd);
3068
4198
  fd = null;
3069
- resolve8(code);
4199
+ resolve10(code);
3070
4200
  };
3071
4201
  child.on("error", (err) => {
3072
4202
  console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
@@ -3077,7 +4207,7 @@ function spawnPlaywright(playwrightArgs2, env, options = {}) {
3077
4207
  }
3078
4208
 
3079
4209
  // src/cli/flake.ts
3080
- var USAGE4 = `
4210
+ var USAGE5 = `
3081
4211
  piwi flake \u2014 make a flaky test fail on demand, then prove the fix
3082
4212
 
3083
4213
  Usage:
@@ -3221,7 +4351,7 @@ function parseFlakeArgs(argv) {
3221
4351
  }
3222
4352
  function gitHead() {
3223
4353
  try {
3224
- return (0, import_node_child_process4.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4354
+ return (0, import_node_child_process7.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
3225
4355
  } catch {
3226
4356
  return null;
3227
4357
  }
@@ -3265,9 +4395,9 @@ function playwrightRunner(dir, log) {
3265
4395
  return {
3266
4396
  now: () => Date.now(),
3267
4397
  async runBatch(plan, arm, repeat, batch) {
3268
- const planFile = path11.join(dir, `${arm.id}.plan.json`);
3269
- if (!fs8.existsSync(planFile)) {
3270
- fs8.writeFileSync(
4398
+ const planFile = path15.join(dir, `${arm.id}.plan.json`);
4399
+ if (!fs11.existsSync(planFile)) {
4400
+ fs11.writeFileSync(
3271
4401
  planFile,
3272
4402
  JSON.stringify({
3273
4403
  version: FLAKE_PLAN_VERSION,
@@ -3278,15 +4408,15 @@ function playwrightRunner(dir, log) {
3278
4408
  })
3279
4409
  );
3280
4410
  }
3281
- const resultsFile = path11.join(dir, `${arm.id}.${batch}.jsonl`);
3282
- fs8.writeFileSync(resultsFile, "");
4411
+ const resultsFile = path15.join(dir, `${arm.id}.${batch}.jsonl`);
4412
+ fs11.writeFileSync(resultsFile, "");
3283
4413
  const env = {
3284
4414
  ...process.env,
3285
4415
  [PIWI_FLAKE_ENV.plan]: planFile,
3286
4416
  [PIWI_FLAKE_ENV.results]: resultsFile
3287
4417
  };
3288
4418
  delete env[PIWI_PROBE_ENV.flag];
3289
- fs8.appendFileSync(log, `
4419
+ fs11.appendFileSync(log, `
3290
4420
  \u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
3291
4421
  `);
3292
4422
  await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
@@ -3299,7 +4429,7 @@ function playwrightRunner(dir, log) {
3299
4429
  }
3300
4430
  function readResults(file) {
3301
4431
  const lines = [];
3302
- for (const line of fs8.readFileSync(file, "utf8").split("\n")) {
4432
+ for (const line of fs11.readFileSync(file, "utf8").split("\n")) {
3303
4433
  if (!line.trim()) continue;
3304
4434
  try {
3305
4435
  lines.push(JSON.parse(line));
@@ -3463,7 +4593,7 @@ function printHeader(plan, commit, estimate, budgetMs) {
3463
4593
  async function loadPlan(args, commit) {
3464
4594
  if (args.planFile) {
3465
4595
  try {
3466
- const raw = JSON.parse(fs8.readFileSync(args.planFile, "utf8"));
4596
+ const raw = JSON.parse(fs11.readFileSync(args.planFile, "utf8"));
3467
4597
  const plan = "plan" in raw && raw.plan ? raw.plan : raw;
3468
4598
  if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
3469
4599
  if (args.verify !== (plan.kind === "verify")) {
@@ -3523,11 +4653,11 @@ async function runFlake(argv, runner) {
3523
4653
  } catch (error) {
3524
4654
  console.error(`piwi flake: ${error.message}
3525
4655
  `);
3526
- console.error(USAGE4);
3527
- return EXIT_ERROR4;
4656
+ console.error(USAGE5);
4657
+ return EXIT_ERROR5;
3528
4658
  }
3529
4659
  if (args.help) {
3530
- console.log(USAGE4);
4660
+ console.log(USAGE5);
3531
4661
  return 0;
3532
4662
  }
3533
4663
  const commit = gitHead();
@@ -3537,11 +4667,11 @@ async function runFlake(argv, runner) {
3537
4667
  ({ plan, http } = await loadPlan(args, commit));
3538
4668
  } catch (error) {
3539
4669
  console.error(`piwi flake: ${error.message}`);
3540
- return EXIT_ERROR4;
4670
+ return EXIT_ERROR5;
3541
4671
  }
3542
4672
  if (args.suspect != null && !plan.arms.some((a) => a.rank === args.suspect)) {
3543
4673
  console.error(`piwi flake: suspect ${args.suspect} has no arm (${plan.arms.length} arm(s) in the plan).`);
3544
- return EXIT_ERROR4;
4674
+ return EXIT_ERROR5;
3545
4675
  }
3546
4676
  if (plan.kind === "reproduce" && plan.arms.length === 0) {
3547
4677
  if (!args.json) printHeader(plan, commit, null, args.budgetMs);
@@ -3557,8 +4687,8 @@ async function runFlake(argv, runner) {
3557
4687
  if (!args.json) {
3558
4688
  printHeader(plan, commit, estimate, args.budgetMs);
3559
4689
  }
3560
- const dir = fs8.mkdtempSync(path11.join(os2.tmpdir(), "piwi-flake-"));
3561
- const log = path11.join(dir, "playwright.log");
4690
+ const dir = fs11.mkdtempSync(path15.join(os2.tmpdir(), "piwi-flake-"));
4691
+ const log = path15.join(dir, "playwright.log");
3562
4692
  let session;
3563
4693
  try {
3564
4694
  session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
@@ -3566,8 +4696,8 @@ async function runFlake(argv, runner) {
3566
4696
  });
3567
4697
  } catch (error) {
3568
4698
  console.error(`piwi flake: ${error.message}`);
3569
- if (fs8.existsSync(log)) {
3570
- const tail = fs8.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
4699
+ if (fs11.existsSync(log)) {
4700
+ const tail = fs11.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
3571
4701
  console.error(`
3572
4702
  Playwright's output (${log}):
3573
4703
  ${tail}`);
@@ -3576,7 +4706,7 @@ ${tail}`);
3576
4706
  console.log("Bisect step: skip (this commit could not run the arm)");
3577
4707
  return EXIT_BISECT_SKIP;
3578
4708
  }
3579
- return EXIT_ERROR4;
4709
+ return EXIT_ERROR5;
3580
4710
  }
3581
4711
  const { verdict, reproducingArm } = sessionVerdict(plan.kind, session.arms);
3582
4712
  const ran = [session.control, ...session.arms.filter((a) => !a.skipped)];
@@ -3640,7 +4770,7 @@ ${tail}`);
3640
4770
  verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
3641
4771
  exitCode: code
3642
4772
  };
3643
- fs8.rmSync(dir, { recursive: true, force: true });
4773
+ fs11.rmSync(dir, { recursive: true, force: true });
3644
4774
  if (args.json) {
3645
4775
  console.log(JSON.stringify(report, null, 2));
3646
4776
  return code;
@@ -3655,7 +4785,7 @@ ${tail}`);
3655
4785
  }
3656
4786
 
3657
4787
  // src/cli/gate.ts
3658
- var fs9 = __toESM(require("fs"));
4788
+ var fs12 = __toESM(require("fs"));
3659
4789
 
3660
4790
  // ../core/src/gate.ts
3661
4791
  function formatGateResult(result) {
@@ -3684,11 +4814,11 @@ function formatGateResult(result) {
3684
4814
  }
3685
4815
 
3686
4816
  // src/cli/gate.ts
3687
- var EXIT_OK4 = 0;
4817
+ var EXIT_OK5 = 0;
3688
4818
  var EXIT_VIOLATED = 1;
3689
- var EXIT_ERROR5 = 2;
4819
+ var EXIT_ERROR6 = 2;
3690
4820
  var EXIT_INCONCLUSIVE = 3;
3691
- var USAGE5 = `
4821
+ var USAGE6 = `
3692
4822
  piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
3693
4823
 
3694
4824
  Usage:
@@ -3718,6 +4848,7 @@ Policy (at least one is required):
3718
4848
 
3719
4849
  Warn-only (reported, never fails the build):
3720
4850
  --max-uncovered-changes <n> Warn when more than n changed files have no observed test reach
4851
+ --min-patch-coverage <percent> Warn when fewer of the changed lines ran, in the commit's combined coverage
3721
4852
 
3722
4853
  Other:
3723
4854
  --json Print the raw result as JSON instead of a summary
@@ -3733,6 +4864,14 @@ function readOption4(argv, name) {
3733
4864
  const value = argv[index + 1];
3734
4865
  return value && !value.startsWith("--") ? value : void 0;
3735
4866
  }
4867
+ function readPercent(argv, name) {
4868
+ const raw = readOption4(argv, name)?.replace(/%$/, "");
4869
+ if (raw === void 0) return void 0;
4870
+ const n = Number(raw);
4871
+ if (!Number.isFinite(n) || n < 0 || n > 100)
4872
+ throw new Error(`${name} expects a percentage from 0 to 100, got "${raw}"`);
4873
+ return n;
4874
+ }
3736
4875
  function readCount(argv, name) {
3737
4876
  const raw = readOption4(argv, name);
3738
4877
  if (raw === void 0) return void 0;
@@ -3740,9 +4879,9 @@ function readCount(argv, name) {
3740
4879
  if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
3741
4880
  return Math.floor(n);
3742
4881
  }
3743
- function readRunIdFromFile(path19) {
4882
+ function readRunIdFromFile(path23) {
3744
4883
  try {
3745
- const parsed = JSON.parse(fs9.readFileSync(path19, "utf-8"));
4884
+ const parsed = JSON.parse(fs12.readFileSync(path23, "utf-8"));
3746
4885
  const runId = Number(parsed.runId);
3747
4886
  return Number.isFinite(runId) && runId > 0 ? runId : null;
3748
4887
  } catch {
@@ -3780,7 +4919,8 @@ function parseGateArgs(argv, env) {
3780
4919
  apiKey: readOption4(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
3781
4920
  runId,
3782
4921
  policy,
3783
- maxUncoveredChanges: readCount(argv, "--max-uncovered-changes")
4922
+ maxUncoveredChanges: readCount(argv, "--max-uncovered-changes"),
4923
+ minPatchCoverage: readPercent(argv, "--min-patch-coverage")
3784
4924
  };
3785
4925
  }
3786
4926
  async function requestGate(args) {
@@ -3789,7 +4929,11 @@ async function requestGate(args) {
3789
4929
  const res = await fetch(`${args.serverUrl}/api/test-runs/${args.runId}/gate`, {
3790
4930
  method: "POST",
3791
4931
  headers,
3792
- body: JSON.stringify({ ...args.policy, maxUncoveredChanges: args.maxUncoveredChanges })
4932
+ body: JSON.stringify({
4933
+ ...args.policy,
4934
+ maxUncoveredChanges: args.maxUncoveredChanges,
4935
+ minPatchCoverage: args.minPatchCoverage
4936
+ })
3793
4937
  });
3794
4938
  if (!res.ok) {
3795
4939
  const body = await res.json().catch(() => ({}));
@@ -3799,8 +4943,8 @@ async function requestGate(args) {
3799
4943
  }
3800
4944
  async function runGate(argv, env = process.env) {
3801
4945
  if (argv.includes("-h") || argv.includes("--help")) {
3802
- console.log(USAGE5);
3803
- return EXIT_OK4;
4946
+ console.log(USAGE6);
4947
+ return EXIT_OK5;
3804
4948
  }
3805
4949
  let args;
3806
4950
  try {
@@ -3808,15 +4952,15 @@ async function runGate(argv, env = process.env) {
3808
4952
  } catch (e) {
3809
4953
  console.error(`piwi gate: ${e.message}
3810
4954
  `);
3811
- console.error(USAGE5);
3812
- return EXIT_ERROR5;
4955
+ console.error(USAGE6);
4956
+ return EXIT_ERROR6;
3813
4957
  }
3814
4958
  let result;
3815
4959
  try {
3816
4960
  result = await requestGate(args);
3817
4961
  } catch (e) {
3818
4962
  console.error(`piwi gate: ${e.message}`);
3819
- return EXIT_ERROR5;
4963
+ return EXIT_ERROR6;
3820
4964
  }
3821
4965
  if (argv.includes("--json")) {
3822
4966
  console.log(JSON.stringify(result, null, 2));
@@ -3824,13 +4968,13 @@ async function runGate(argv, env = process.env) {
3824
4968
  console.log(formatGateResult(result));
3825
4969
  }
3826
4970
  if (result.verdict === "inconclusive") return EXIT_INCONCLUSIVE;
3827
- return result.passed ? EXIT_OK4 : EXIT_VIOLATED;
4971
+ return result.passed ? EXIT_OK5 : EXIT_VIOLATED;
3828
4972
  }
3829
4973
 
3830
4974
  // src/cli/init.ts
3831
- var fs12 = __toESM(require("fs"));
3832
- var path14 = __toESM(require("path"));
3833
- var import_node_child_process5 = require("child_process");
4975
+ var fs15 = __toESM(require("fs"));
4976
+ var path18 = __toESM(require("path"));
4977
+ var import_node_child_process8 = require("child_process");
3834
4978
 
3835
4979
  // src/cli/report.ts
3836
4980
  var STATUS_MARK = {
@@ -3847,8 +4991,8 @@ function formatStep(result) {
3847
4991
  }
3848
4992
 
3849
4993
  // src/cli/detect.ts
3850
- var fs10 = __toESM(require("fs"));
3851
- var path12 = __toESM(require("path"));
4994
+ var fs13 = __toESM(require("fs"));
4995
+ var path16 = __toESM(require("path"));
3852
4996
  var CONFIG_NAMES = [
3853
4997
  "playwright.config.ts",
3854
4998
  "playwright.config.mts",
@@ -3866,9 +5010,9 @@ var LOCKFILES = [
3866
5010
  ];
3867
5011
  var REPORTER_PACKAGE = "@piwitests/reporter";
3868
5012
  function readPackageJson(root) {
3869
- const pkgPath = path12.join(root, "package.json");
5013
+ const pkgPath = path16.join(root, "package.json");
3870
5014
  try {
3871
- return { pkg: JSON.parse(fs10.readFileSync(pkgPath, "utf-8")), pkgPath };
5015
+ return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
3872
5016
  } catch {
3873
5017
  return { pkg: null, pkgPath: null };
3874
5018
  }
@@ -3877,14 +5021,14 @@ function detectPackageManager(root, pkg) {
3877
5021
  const declared = pkg?.packageManager?.split("@")[0];
3878
5022
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
3879
5023
  for (const [file, manager] of LOCKFILES) {
3880
- if (fs10.existsSync(path12.join(root, file))) return manager;
5024
+ if (fs13.existsSync(path16.join(root, file))) return manager;
3881
5025
  }
3882
5026
  return "npm";
3883
5027
  }
3884
5028
  function findConfig(root) {
3885
5029
  for (const name of CONFIG_NAMES) {
3886
- const candidate = path12.join(root, name);
3887
- if (fs10.existsSync(candidate)) return candidate;
5030
+ const candidate = path16.join(root, name);
5031
+ if (fs13.existsSync(candidate)) return candidate;
3888
5032
  }
3889
5033
  return null;
3890
5034
  }
@@ -3894,14 +5038,14 @@ function langOf(configPath) {
3894
5038
  }
3895
5039
  function suggestProjectName(root, pkg) {
3896
5040
  const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
3897
- return fromPkg || path12.basename(root) || "default-project";
5041
+ return fromPkg || path16.basename(root) || "default-project";
3898
5042
  }
3899
5043
  function hasReporter(pkg) {
3900
5044
  if (!pkg) return false;
3901
5045
  return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
3902
5046
  }
3903
5047
  function detectProject(root) {
3904
- const absRoot = path12.resolve(root);
5048
+ const absRoot = path16.resolve(root);
3905
5049
  const { pkg, pkgPath } = readPackageJson(absRoot);
3906
5050
  const configPath = findConfig(absRoot);
3907
5051
  return {
@@ -4043,8 +5187,8 @@ function ensureGitignoreEntry(existing, entry = ".env") {
4043
5187
  }
4044
5188
 
4045
5189
  // src/cli/skills.ts
4046
- var fs11 = __toESM(require("fs"));
4047
- var path13 = __toESM(require("path"));
5190
+ var fs14 = __toESM(require("fs"));
5191
+ var path17 = __toESM(require("path"));
4048
5192
 
4049
5193
  // ../core/src/skill-stamp.ts
4050
5194
  var SKILL_VERSION_KEY = "piwi-version";
@@ -4107,16 +5251,16 @@ var WORKFLOW_SKILLS = [
4107
5251
  "fix-a-reported-bug"
4108
5252
  ];
4109
5253
  var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
4110
- var DEFAULT_SKILLS_DIR = path13.join(".claude", "skills");
5254
+ var DEFAULT_SKILLS_DIR = path17.join(".claude", "skills");
4111
5255
  function findTemplatesDir(fromDir) {
4112
5256
  let dir = fromDir;
4113
5257
  for (let i = 0; i < 6; i++) {
4114
- if (fs11.existsSync(path13.join(dir, "templates", "skills"))) return path13.join(dir, "templates");
4115
- const parent = path13.dirname(dir);
5258
+ if (fs14.existsSync(path17.join(dir, "templates", "skills"))) return path17.join(dir, "templates");
5259
+ const parent = path17.dirname(dir);
4116
5260
  if (parent === dir) break;
4117
5261
  dir = parent;
4118
5262
  }
4119
- return path13.resolve(fromDir, "..", "..", "templates");
5263
+ return path17.resolve(fromDir, "..", "..", "templates");
4120
5264
  }
4121
5265
  function readFrontMatter(markdown) {
4122
5266
  const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
@@ -4130,21 +5274,21 @@ function readFrontMatter(markdown) {
4130
5274
  }
4131
5275
  function readTemplatesVersion(templatesDir) {
4132
5276
  try {
4133
- const pkg = JSON.parse(fs11.readFileSync(path13.join(templatesDir, "..", "package.json"), "utf-8"));
5277
+ const pkg = JSON.parse(fs14.readFileSync(path17.join(templatesDir, "..", "package.json"), "utf-8"));
4134
5278
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
4135
5279
  } catch {
4136
5280
  return "0.0.0";
4137
5281
  }
4138
5282
  }
4139
5283
  function templatePath(templatesDir, slug) {
4140
- return path13.join(templatesDir, "skills", slug, "SKILL.md");
5284
+ return path17.join(templatesDir, "skills", slug, "SKILL.md");
4141
5285
  }
4142
5286
  function listSkills(templatesDir, slugs = ALL_SKILLS) {
4143
5287
  const infos = [];
4144
5288
  for (const slug of slugs) {
4145
5289
  const file = templatePath(templatesDir, slug);
4146
- if (!fs11.existsSync(file)) continue;
4147
- const front = readFrontMatter(fs11.readFileSync(file, "utf-8"));
5290
+ if (!fs14.existsSync(file)) continue;
5291
+ const front = readFrontMatter(fs14.readFileSync(file, "utf-8"));
4148
5292
  infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
4149
5293
  }
4150
5294
  return infos;
@@ -4154,18 +5298,18 @@ function installSkills(opts) {
4154
5298
  for (const slug of opts.slugs) {
4155
5299
  const step = `skill:${slug}`;
4156
5300
  const source = templatePath(opts.templatesDir, slug);
4157
- const relDest = path13.join(opts.skillsDir, slug, "SKILL.md");
4158
- const dest = path13.join(opts.root, relDest);
4159
- if (!fs11.existsSync(source)) {
5301
+ const relDest = path17.join(opts.skillsDir, slug, "SKILL.md");
5302
+ const dest = path17.join(opts.root, relDest);
5303
+ if (!fs14.existsSync(source)) {
4160
5304
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
4161
5305
  continue;
4162
5306
  }
4163
5307
  const version = opts.version ?? readTemplatesVersion(opts.templatesDir);
4164
- const contents = stampSkill(fs11.readFileSync(source, "utf-8"), version);
4165
- const exists = fs11.existsSync(dest);
5308
+ const contents = stampSkill(fs14.readFileSync(source, "utf-8"), version);
5309
+ const exists = fs14.existsSync(dest);
4166
5310
  let detail = "installed skill";
4167
5311
  if (exists) {
4168
- const installed = fs11.readFileSync(dest, "utf-8");
5312
+ const installed = fs14.readFileSync(dest, "utf-8");
4169
5313
  const state = classifyInstalledSkill(installed, contents);
4170
5314
  const from = readSkillStamp(installed).version;
4171
5315
  if (state === "current") {
@@ -4187,14 +5331,14 @@ function installSkills(opts) {
4187
5331
  }
4188
5332
  }
4189
5333
  if (!opts.dryRun) {
4190
- fs11.mkdirSync(path13.dirname(dest), { recursive: true });
4191
- fs11.writeFileSync(dest, contents);
5334
+ fs14.mkdirSync(path17.dirname(dest), { recursive: true });
5335
+ fs14.writeFileSync(dest, contents);
4192
5336
  }
4193
5337
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
4194
5338
  }
4195
5339
  return results;
4196
5340
  }
4197
- var USAGE6 = `
5341
+ var USAGE7 = `
4198
5342
  piwi skills \u2014 install the Piwi agent skills into this project
4199
5343
 
4200
5344
  Usage:
@@ -4234,7 +5378,7 @@ function positionalSlugs(argv) {
4234
5378
  function runSkills(argv, templatesDir, cwd = process.cwd()) {
4235
5379
  const [sub, ...rest] = argv;
4236
5380
  if (sub === void 0 || sub === "-h" || sub === "--help") {
4237
- console.log(USAGE6);
5381
+ console.log(USAGE7);
4238
5382
  return 0;
4239
5383
  }
4240
5384
  if (sub === "list") {
@@ -4255,12 +5399,12 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
4255
5399
  if (unknown.length) {
4256
5400
  console.error(`piwi skills: unknown skill(s): ${unknown.join(", ")}
4257
5401
  `);
4258
- console.error(USAGE6);
5402
+ console.error(USAGE7);
4259
5403
  return 2;
4260
5404
  }
4261
5405
  const results = installSkills({
4262
5406
  templatesDir,
4263
- root: path13.resolve(readOption5(rest, "--cwd") ?? cwd),
5407
+ root: path17.resolve(readOption5(rest, "--cwd") ?? cwd),
4264
5408
  skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
4265
5409
  slugs: requested.length ? requested : ALL_SKILLS,
4266
5410
  force: rest.includes("--force"),
@@ -4276,13 +5420,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
4276
5420
  }
4277
5421
  console.error(`piwi skills: unknown command "${sub}"
4278
5422
  `);
4279
- console.error(USAGE6);
5423
+ console.error(USAGE7);
4280
5424
  return 2;
4281
5425
  }
4282
5426
 
4283
5427
  // src/cli/init.ts
4284
5428
  var DEFAULT_SERVER_URL = "http://localhost:3000";
4285
- var USAGE7 = `
5429
+ var USAGE8 = `
4286
5430
  piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
4287
5431
 
4288
5432
  Usage:
@@ -4331,7 +5475,7 @@ function resolveSkillSlugs(raw) {
4331
5475
  return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
4332
5476
  }
4333
5477
  function parseInitArgs(argv, env, cwd) {
4334
- const root = path14.resolve(readOption6(argv, "--cwd") ?? cwd);
5478
+ const root = path18.resolve(readOption6(argv, "--cwd") ?? cwd);
4335
5479
  const detected = detectProject(root);
4336
5480
  return {
4337
5481
  root,
@@ -4350,7 +5494,7 @@ function parseInitArgs(argv, env, cwd) {
4350
5494
  }
4351
5495
  function readFileOr(file, fallback) {
4352
5496
  try {
4353
- return fs12.readFileSync(file, "utf-8");
5497
+ return fs15.readFileSync(file, "utf-8");
4354
5498
  } catch {
4355
5499
  return fallback;
4356
5500
  }
@@ -4380,7 +5524,7 @@ function stepInstallReporter(project, opts) {
4380
5524
  if (opts.dryRun || !opts.install)
4381
5525
  return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
4382
5526
  const [command, args] = installArgv(project.packageManager);
4383
- const result = (0, import_node_child_process5.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
5527
+ const result = (0, import_node_child_process8.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
4384
5528
  if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
4385
5529
  return {
4386
5530
  step,
@@ -4396,19 +5540,19 @@ function stepConfig(project, opts) {
4396
5540
  status: "manual",
4397
5541
  detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
4398
5542
  };
4399
- const rel = path14.relative(project.root, project.configPath) || path14.basename(project.configPath);
5543
+ const rel = path18.relative(project.root, project.configPath) || path18.basename(project.configPath);
4400
5544
  const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
4401
5545
  serverUrl: opts.serverUrl,
4402
5546
  projectName: opts.projectName
4403
5547
  });
4404
- if (edit.status === "updated" && !opts.dryRun) fs12.writeFileSync(project.configPath, edit.text);
5548
+ if (edit.status === "updated" && !opts.dryRun) fs15.writeFileSync(project.configPath, edit.text);
4405
5549
  return { step, file: rel, status: edit.status, detail: edit.detail };
4406
5550
  }
4407
5551
  function stepFixtures(project, opts) {
4408
5552
  const step = "fixtures";
4409
- const rel = path14.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
4410
- const dest = path14.join(project.root, rel);
4411
- if (fs12.existsSync(dest)) {
5553
+ const rel = path18.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5554
+ const dest = path18.join(project.root, rel);
5555
+ if (fs15.existsSync(dest)) {
4412
5556
  const current = readFileOr(dest, "");
4413
5557
  if (/piwiFixtures/.test(current))
4414
5558
  return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
@@ -4420,8 +5564,8 @@ function stepFixtures(project, opts) {
4420
5564
  };
4421
5565
  }
4422
5566
  if (!opts.dryRun) {
4423
- fs12.mkdirSync(path14.dirname(dest), { recursive: true });
4424
- fs12.writeFileSync(dest, fixturesContents());
5567
+ fs15.mkdirSync(path18.dirname(dest), { recursive: true });
5568
+ fs15.writeFileSync(dest, fixturesContents());
4425
5569
  }
4426
5570
  return {
4427
5571
  step,
@@ -4432,12 +5576,12 @@ function stepFixtures(project, opts) {
4432
5576
  }
4433
5577
  function stepEnv(project, opts) {
4434
5578
  const results = [];
4435
- const examplePath = path14.join(project.root, ".env.example");
5579
+ const examplePath = path18.join(project.root, ".env.example");
4436
5580
  const example = upsertEnvKeys(readFileOr(examplePath, ""), [
4437
5581
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
4438
5582
  ["PIWI_API_KEY", ""]
4439
5583
  ]);
4440
- if (example.added.length && !opts.dryRun) fs12.writeFileSync(examplePath, example.text);
5584
+ if (example.added.length && !opts.dryRun) fs15.writeFileSync(examplePath, example.text);
4441
5585
  results.push({
4442
5586
  step: "env",
4443
5587
  file: ".env.example",
@@ -4445,21 +5589,21 @@ function stepEnv(project, opts) {
4445
5589
  detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
4446
5590
  });
4447
5591
  if (opts.apiKey) {
4448
- const envPath = path14.join(project.root, ".env");
5592
+ const envPath = path18.join(project.root, ".env");
4449
5593
  const env = upsertEnvKeys(readFileOr(envPath, ""), [
4450
5594
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
4451
5595
  ["PIWI_API_KEY", opts.apiKey]
4452
5596
  ]);
4453
- if (env.added.length && !opts.dryRun) fs12.writeFileSync(envPath, env.text);
5597
+ if (env.added.length && !opts.dryRun) fs15.writeFileSync(envPath, env.text);
4454
5598
  results.push({
4455
5599
  step: "env",
4456
5600
  file: ".env",
4457
5601
  status: env.added.length ? "updated" : "already",
4458
5602
  detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
4459
5603
  });
4460
- const gitignorePath = path14.join(project.root, ".gitignore");
5604
+ const gitignorePath = path18.join(project.root, ".gitignore");
4461
5605
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
4462
- if (gitignore.added && !opts.dryRun) fs12.writeFileSync(gitignorePath, gitignore.text);
5606
+ if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
4463
5607
  results.push({
4464
5608
  step: "gitignore",
4465
5609
  file: ".gitignore",
@@ -4488,7 +5632,7 @@ function nextSteps(opts, steps) {
4488
5632
  }
4489
5633
  async function runInit(argv, env = process.env, cwd = process.cwd()) {
4490
5634
  if (argv.includes("-h") || argv.includes("--help")) {
4491
- console.log(USAGE7);
5635
+ console.log(USAGE8);
4492
5636
  return 0;
4493
5637
  }
4494
5638
  const opts = parseInitArgs(argv, env, cwd);
@@ -4521,7 +5665,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
4521
5665
  project: {
4522
5666
  root: project.root,
4523
5667
  packageManager: project.packageManager,
4524
- configPath: project.configPath ? path14.relative(project.root, project.configPath) : null,
5668
+ configPath: project.configPath ? path18.relative(project.root, project.configPath) : null,
4525
5669
  projectName: opts.projectName,
4526
5670
  serverUrl: opts.serverUrl
4527
5671
  },
@@ -4545,14 +5689,14 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
4545
5689
  }
4546
5690
 
4547
5691
  // src/cli/select.ts
4548
- var fs14 = __toESM(require("fs"));
4549
- var path16 = __toESM(require("path"));
4550
- var import_node_child_process6 = require("child_process");
5692
+ var fs17 = __toESM(require("fs"));
5693
+ var path20 = __toESM(require("path"));
5694
+ var import_node_child_process9 = require("child_process");
4551
5695
  var import_node_module4 = require("module");
4552
5696
 
4553
5697
  // src/cli/add-reporter.ts
4554
- var fs13 = __toESM(require("fs"));
4555
- var path15 = __toESM(require("path"));
5698
+ var fs16 = __toESM(require("fs"));
5699
+ var path19 = __toESM(require("path"));
4556
5700
  var import_node_module3 = require("module");
4557
5701
  var REPORTER_PACKAGE2 = "@piwitests/reporter";
4558
5702
  var ADD_REPORTER_FLAG = "--add-reporter";
@@ -4585,9 +5729,9 @@ function resolveConfigPath(cwd, playwrightArgs2) {
4585
5729
  const arg = playwrightArgs2[i];
4586
5730
  if (arg === "--config" || arg === "-c") {
4587
5731
  const next = playwrightArgs2[i + 1];
4588
- if (next) return path15.resolve(cwd, next);
5732
+ if (next) return path19.resolve(cwd, next);
4589
5733
  } else if (arg.startsWith("--config=")) {
4590
- return path15.resolve(cwd, arg.slice("--config=".length));
5734
+ return path19.resolve(cwd, arg.slice("--config=".length));
4591
5735
  }
4592
5736
  }
4593
5737
  return detectProject(cwd).configPath;
@@ -4595,14 +5739,14 @@ function resolveConfigPath(cwd, playwrightArgs2) {
4595
5739
  function readConfigSource(configPath) {
4596
5740
  if (!configPath) return null;
4597
5741
  try {
4598
- return fs13.readFileSync(configPath, "utf-8");
5742
+ return fs16.readFileSync(configPath, "utf-8");
4599
5743
  } catch {
4600
5744
  return null;
4601
5745
  }
4602
5746
  }
4603
5747
  function readInstalledPlaywrightVersion(cwd) {
4604
5748
  try {
4605
- const require2 = (0, import_node_module3.createRequire)(path15.join(cwd, "noop.js"));
5749
+ const require2 = (0, import_node_module3.createRequire)(path19.join(cwd, "noop.js"));
4606
5750
  for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
4607
5751
  try {
4608
5752
  return require2(id).version ?? null;
@@ -4619,9 +5763,9 @@ function computeAddReporterArgs(cwd, playwrightArgs2) {
4619
5763
  }
4620
5764
 
4621
5765
  // src/cli/select.ts
4622
- var EXIT_OK5 = 0;
4623
- var EXIT_ERROR6 = 2;
4624
- var USAGE8 = `
5766
+ var EXIT_OK6 = 0;
5767
+ var EXIT_ERROR7 = 2;
5768
+ var USAGE9 = `
4625
5769
  piwi select / piwi run \u2014 run a saved selection of tests
4626
5770
 
4627
5771
  Usage:
@@ -4722,13 +5866,13 @@ function parseSelectArgs(argv, env) {
4722
5866
  extra
4723
5867
  };
4724
5868
  }
4725
- var CACHE_FILE = path16.join(".piwi", "selection-cache.json");
5869
+ var CACHE_FILE = path20.join(".piwi", "selection-cache.json");
4726
5870
  function cacheKey(args) {
4727
5871
  return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
4728
5872
  }
4729
5873
  function readCache(args) {
4730
5874
  try {
4731
- const store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
5875
+ const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
4732
5876
  return store[cacheKey(args)] ?? null;
4733
5877
  } catch {
4734
5878
  return null;
@@ -4738,12 +5882,12 @@ function writeCache(args, resolution) {
4738
5882
  try {
4739
5883
  let store = {};
4740
5884
  try {
4741
- store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
5885
+ store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
4742
5886
  } catch {
4743
5887
  }
4744
5888
  store[cacheKey(args)] = resolution;
4745
- fs14.mkdirSync(path16.dirname(CACHE_FILE), { recursive: true });
4746
- fs14.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
5889
+ fs17.mkdirSync(path20.dirname(CACHE_FILE), { recursive: true });
5890
+ fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
4747
5891
  } catch {
4748
5892
  }
4749
5893
  }
@@ -4767,7 +5911,7 @@ function printWarnings(resolution) {
4767
5911
  for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
4768
5912
  }
4769
5913
  function gitChangedFiles(base) {
4770
- const out = (0, import_node_child_process6.execFileSync)("git", ["diff", "--name-only", base], {
5914
+ const out = (0, import_node_child_process9.execFileSync)("git", ["diff", "--name-only", base], {
4771
5915
  encoding: "utf-8",
4772
5916
  stdio: ["ignore", "pipe", "ignore"]
4773
5917
  });
@@ -4788,7 +5932,7 @@ async function runSelectImpact(args) {
4788
5932
  impact = await loadImpact(args, projectId);
4789
5933
  } catch (e) {
4790
5934
  console.error(`piwi select: ${e.message}`);
4791
- return EXIT_ERROR6;
5935
+ return EXIT_ERROR7;
4792
5936
  }
4793
5937
  printWarnings(impact);
4794
5938
  console.error(
@@ -4796,15 +5940,15 @@ async function runSelectImpact(args) {
4796
5940
  );
4797
5941
  if (args.json) {
4798
5942
  console.log(JSON.stringify(impact, null, 2));
4799
- return EXIT_OK5;
5943
+ return EXIT_OK6;
4800
5944
  }
4801
5945
  console.log(impact.impact.widened ? "" : impact.materialization.args.join(" "));
4802
- return EXIT_OK5;
5946
+ return EXIT_OK6;
4803
5947
  }
4804
5948
  async function runSelect(argv, env = process.env) {
4805
5949
  if (argv.includes("-h") || argv.includes("--help")) {
4806
- console.log(USAGE8);
4807
- return EXIT_OK5;
5950
+ console.log(USAGE9);
5951
+ return EXIT_OK6;
4808
5952
  }
4809
5953
  let args;
4810
5954
  try {
@@ -4812,13 +5956,13 @@ async function runSelect(argv, env = process.env) {
4812
5956
  } catch (e) {
4813
5957
  console.error(`piwi select: ${e.message}
4814
5958
  `);
4815
- console.error(USAGE8);
4816
- return EXIT_ERROR6;
5959
+ console.error(USAGE9);
5960
+ return EXIT_ERROR7;
4817
5961
  }
4818
5962
  if (args.key === IMPACT_KEY) {
4819
5963
  if (!args.base) {
4820
5964
  console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
4821
- return EXIT_ERROR6;
5965
+ return EXIT_ERROR7;
4822
5966
  }
4823
5967
  return runSelectImpact(args);
4824
5968
  }
@@ -4828,19 +5972,19 @@ async function runSelect(argv, env = process.env) {
4828
5972
  resolution = await fetchResolution(args, projectId);
4829
5973
  } catch (e) {
4830
5974
  console.error(`piwi select: ${e.message}`);
4831
- return EXIT_ERROR6;
5975
+ return EXIT_ERROR7;
4832
5976
  }
4833
5977
  printWarnings(resolution);
4834
5978
  if (resolution.estimate.count === 0) {
4835
5979
  console.error(`piwi select: "${args.key}" resolved to 0 tests`);
4836
- return EXIT_ERROR6;
5980
+ return EXIT_ERROR7;
4837
5981
  }
4838
5982
  if (args.json) console.log(JSON.stringify(resolution, null, 2));
4839
5983
  else console.log(resolution.materialization.args.join(" "));
4840
- return EXIT_OK5;
5984
+ return EXIT_OK6;
4841
5985
  }
4842
5986
  function resolvePlaywrightCli3() {
4843
- const require2 = (0, import_node_module4.createRequire)(path16.join(process.cwd(), "noop.js"));
5987
+ const require2 = (0, import_node_module4.createRequire)(path20.join(process.cwd(), "noop.js"));
4844
5988
  for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
4845
5989
  try {
4846
5990
  return require2.resolve(id);
@@ -4851,16 +5995,16 @@ function resolvePlaywrightCli3() {
4851
5995
  }
4852
5996
  function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
4853
5997
  const cli = resolvePlaywrightCli3();
4854
- const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio: "inherit", env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs2], {
5998
+ const child = cli ? (0, import_node_child_process9.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio: "inherit", env }) : (0, import_node_child_process9.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs2], {
4855
5999
  stdio: "inherit",
4856
6000
  env
4857
6001
  });
4858
- return new Promise((resolve8) => {
6002
+ return new Promise((resolve10) => {
4859
6003
  child.on("error", (err) => {
4860
6004
  console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
4861
- resolve8(EXIT_ERROR6);
6005
+ resolve10(EXIT_ERROR7);
4862
6006
  });
4863
- child.on("exit", (code) => resolve8(code ?? EXIT_ERROR6));
6007
+ child.on("exit", (code) => resolve10(code ?? EXIT_ERROR7));
4864
6008
  });
4865
6009
  }
4866
6010
  function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
@@ -4878,7 +6022,7 @@ async function runRunImpact(args, env) {
4878
6022
  } catch (e) {
4879
6023
  if (args.strict) {
4880
6024
  console.error(`piwi run: ${e.message}`);
4881
- return EXIT_ERROR6;
6025
+ return EXIT_ERROR7;
4882
6026
  }
4883
6027
  console.error(`piwi run: ${e.message} \u2014 running the full suite`);
4884
6028
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
@@ -4889,7 +6033,7 @@ async function runRunImpact(args, env) {
4889
6033
  } catch (e) {
4890
6034
  if (args.strict) {
4891
6035
  console.error(`piwi run: ${e.message}`);
4892
- return EXIT_ERROR6;
6036
+ return EXIT_ERROR7;
4893
6037
  }
4894
6038
  console.error(`piwi run: ${e.message} \u2014 running the full suite`);
4895
6039
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
@@ -4903,7 +6047,7 @@ async function runRunImpact(args, env) {
4903
6047
  }
4904
6048
  if (impact.estimate.count === 0) {
4905
6049
  console.error(`piwi run: no tests impacted by ${impact.impact.changedFiles} changed file(s) \u2014 nothing to run`);
4906
- return EXIT_OK5;
6050
+ return EXIT_OK6;
4907
6051
  }
4908
6052
  const runEnv = {
4909
6053
  ...env,
@@ -4918,8 +6062,8 @@ async function runRunImpact(args, env) {
4918
6062
  }
4919
6063
  async function runRun(argv, env = process.env) {
4920
6064
  if (argv.includes("-h") || argv.includes("--help")) {
4921
- console.log(USAGE8);
4922
- return EXIT_OK5;
6065
+ console.log(USAGE9);
6066
+ return EXIT_OK6;
4923
6067
  }
4924
6068
  let args;
4925
6069
  try {
@@ -4927,17 +6071,17 @@ async function runRun(argv, env = process.env) {
4927
6071
  } catch (e) {
4928
6072
  console.error(`piwi run: ${e.message}
4929
6073
  `);
4930
- console.error(USAGE8);
4931
- return EXIT_ERROR6;
6074
+ console.error(USAGE9);
6075
+ return EXIT_ERROR7;
4932
6076
  }
4933
6077
  if (args.format === "json") {
4934
6078
  console.error("piwi run: --format json cannot be run; use args, grep or files");
4935
- return EXIT_ERROR6;
6079
+ return EXIT_ERROR7;
4936
6080
  }
4937
6081
  if (args.key === IMPACT_KEY) {
4938
6082
  if (!args.base) {
4939
6083
  console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
4940
- return EXIT_ERROR6;
6084
+ return EXIT_ERROR7;
4941
6085
  }
4942
6086
  return runRunImpact(args, env);
4943
6087
  }
@@ -4946,7 +6090,7 @@ async function runRun(argv, env = process.env) {
4946
6090
  outcome = await resolveWithCache(args);
4947
6091
  } catch (e) {
4948
6092
  console.error(`piwi run: ${e.message}`);
4949
- return EXIT_ERROR6;
6093
+ return EXIT_ERROR7;
4950
6094
  }
4951
6095
  if ("error" in outcome) {
4952
6096
  console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
@@ -4956,7 +6100,7 @@ async function runRun(argv, env = process.env) {
4956
6100
  printWarnings(resolution);
4957
6101
  if (resolution.estimate.count === 0) {
4958
6102
  console.error(`piwi run: "${args.key}" resolved to 0 tests`);
4959
- return EXIT_ERROR6;
6103
+ return EXIT_ERROR7;
4960
6104
  }
4961
6105
  const runEnv = {
4962
6106
  ...env,
@@ -4973,16 +6117,16 @@ async function runRun(argv, env = process.env) {
4973
6117
  }
4974
6118
 
4975
6119
  // src/cli/preflight.ts
4976
- var fs15 = __toESM(require("fs"));
4977
- var path17 = __toESM(require("path"));
4978
- var import_node_child_process7 = require("child_process");
6120
+ var fs18 = __toESM(require("fs"));
6121
+ var path21 = __toESM(require("path"));
6122
+ var import_node_child_process10 = require("child_process");
4979
6123
 
4980
6124
  // ../core/src/diff-anchors.ts
4981
6125
  var RENAME_FROM = `rename from `;
4982
6126
  var RENAME_TO = "rename to ";
4983
6127
  var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
4984
- function stripPrefix(path19) {
4985
- const unquoted = path19.startsWith('"') && path19.endsWith('"') ? path19.slice(1, -1) : path19;
6128
+ function stripPrefix(path23) {
6129
+ const unquoted = path23.startsWith('"') && path23.endsWith('"') ? path23.slice(1, -1) : path23;
4986
6130
  return unquoted.replace(/^[ab]\//, "");
4987
6131
  }
4988
6132
  function parseUnifiedDiff(text) {
@@ -5092,8 +6236,8 @@ function pathGlobToRegExp(glob) {
5092
6236
  });
5093
6237
  return new RegExp(`^(?:${parts.join("|")})$`);
5094
6238
  }
5095
- function isTranslationFile(path19, globs = DEFAULT_TRANSLATION_GLOBS) {
5096
- const posix = path19.replace(/\\/g, "/");
6239
+ function isTranslationFile(path23, globs = DEFAULT_TRANSLATION_GLOBS) {
6240
+ const posix = path23.replace(/\\/g, "/");
5097
6241
  if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
5098
6242
  return globs.some((g) => pathGlobToRegExp(g).test(posix));
5099
6243
  }
@@ -5230,16 +6374,16 @@ function unquoteYaml(raw) {
5230
6374
  }
5231
6375
  return v.replace(/\s+#.*$/, "");
5232
6376
  }
5233
- function decodeXml(text) {
6377
+ function decodeXml2(text) {
5234
6378
  return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
5235
6379
  }
5236
- function parseTranslationFile(path19, content) {
6380
+ function parseTranslationFile(path23, content) {
5237
6381
  const lines = content.split(/\r?\n/);
5238
6382
  const out = [];
5239
- const ext = /\.([a-z]+)$/i.exec(path19)?.[1]?.toLowerCase();
6383
+ const ext = /\.([a-z]+)$/i.exec(path23)?.[1]?.toLowerCase();
5240
6384
  if (ext === "json") {
5241
6385
  const stack = [];
5242
- const path20 = (key) => [...stack.map((f) => f.key), key].join(".");
6386
+ const path24 = (key) => [...stack.map((f) => f.key), key].join(".");
5243
6387
  lines.forEach((text, i) => {
5244
6388
  const t = text.trim();
5245
6389
  const top = stack[stack.length - 1];
@@ -5248,14 +6392,14 @@ function parseTranslationFile(path19, content) {
5248
6392
  const key = JSON.parse(`"${pair[1]}"`);
5249
6393
  const rest = pair[2];
5250
6394
  const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
5251
- if (str) out.push({ key: path20(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6395
+ if (str) out.push({ key: path24(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
5252
6396
  else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
5253
6397
  return;
5254
6398
  }
5255
6399
  if (top?.array) {
5256
6400
  const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
5257
6401
  if (str) {
5258
- out.push({ key: path20(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6402
+ out.push({ key: path24(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
5259
6403
  return;
5260
6404
  }
5261
6405
  if (/^[[{]\s*$/.test(t)) {
@@ -5308,10 +6452,10 @@ function parseTranslationFile(path19, content) {
5308
6452
  let name = null;
5309
6453
  lines.forEach((text, i) => {
5310
6454
  const data = /<data\s+name="([^"]+)"/.exec(text);
5311
- if (data) name = decodeXml(data[1]);
6455
+ if (data) name = decodeXml2(data[1]);
5312
6456
  const value = /<value>(.*)<\/value>/.exec(text);
5313
6457
  if (value && name) {
5314
- out.push({ key: name, value: decodeXml(value[1]), line: i + 1 });
6458
+ out.push({ key: name, value: decodeXml2(value[1]), line: i + 1 });
5315
6459
  name = null;
5316
6460
  }
5317
6461
  });
@@ -5319,8 +6463,8 @@ function parseTranslationFile(path19, content) {
5319
6463
  }
5320
6464
  return out;
5321
6465
  }
5322
- function translationLineToken(path19, text, line) {
5323
- const ext = /\.([a-z]+)$/i.exec(path19)?.[1]?.toLowerCase();
6466
+ function translationLineToken(path23, text, line) {
6467
+ const ext = /\.([a-z]+)$/i.exec(path23)?.[1]?.toLowerCase();
5324
6468
  let key;
5325
6469
  let value;
5326
6470
  if (ext === "json") {
@@ -5352,14 +6496,14 @@ function translationLineToken(path19, text, line) {
5352
6496
  }
5353
6497
  } else if (ext === "resx") {
5354
6498
  const m = /<value>(.*)<\/value>/.exec(text);
5355
- if (m) value = decodeXml(m[1]);
6499
+ if (m) value = decodeXml2(m[1]);
5356
6500
  }
5357
6501
  if (value === void 0 || !keepable(value)) return null;
5358
6502
  return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
5359
6503
  }
5360
- function lineTokens(path19, text, line, side, attributes, options) {
6504
+ function lineTokens(path23, text, line, side, attributes, options) {
5361
6505
  const spans = [];
5362
- const markup = MARKUP_FILE.test(path19);
6506
+ const markup = MARKUP_FILE.test(path23);
5363
6507
  return [
5364
6508
  ...attributeTokens(text, attributes, line, spans),
5365
6509
  ...translationRefTokens(text, line, side, options.translations, spans),
@@ -5367,10 +6511,10 @@ function lineTokens(path19, text, line, side, attributes, options) {
5367
6511
  ...textTokens(text, line, markup)
5368
6512
  ];
5369
6513
  }
5370
- function keysByLine(path19, side, options) {
5371
- const content = options.readFile?.(path19, side);
6514
+ function keysByLine(path23, side, options) {
6515
+ const content = options.readFile?.(path23, side);
5372
6516
  if (content === void 0) return null;
5373
- return new Map(parseTranslationFile(path19, content).map((e) => [e.line, e.key]));
6517
+ return new Map(parseTranslationFile(path23, content).map((e) => [e.line, e.key]));
5374
6518
  }
5375
6519
  function tokenId(t) {
5376
6520
  return `${t.group}\0${t.value}`;
@@ -5670,9 +6814,10 @@ function slotMatches(slot, text) {
5670
6814
  return textMatches(slot.value, slot.exact, text);
5671
6815
  }
5672
6816
  }
6817
+ var filePath = (path23) => path23.replace(/\\/g, "/").replace(/^\.\//, "");
5673
6818
  function sameFilePath(a, b) {
5674
- const x = a.replace(/\\/g, "/").replace(/^\.\//, "");
5675
- const y = b.replace(/\\/g, "/").replace(/^\.\//, "");
6819
+ const x = filePath(a);
6820
+ const y = filePath(b);
5676
6821
  return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
5677
6822
  }
5678
6823
  function reachOfIndex(index) {
@@ -5801,11 +6946,11 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
5801
6946
  }
5802
6947
 
5803
6948
  // src/cli/preflight.ts
5804
- var EXIT_OK6 = 0;
6949
+ var EXIT_OK7 = 0;
5805
6950
  var EXIT_BREAKS = 1;
5806
- var EXIT_ERROR7 = 2;
6951
+ var EXIT_ERROR8 = 2;
5807
6952
  var MAX_LISTED = 20;
5808
- var USAGE9 = `
6953
+ var USAGE10 = `
5809
6954
  piwi preflight \u2014 the test locators a change breaks, before it runs
5810
6955
 
5811
6956
  Usage:
@@ -5884,16 +7029,16 @@ function parsePreflightArgs(argv) {
5884
7029
  }
5885
7030
  var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
5886
7031
  function findPlaywrightRoot(from) {
5887
- let dir = path17.resolve(from);
7032
+ let dir = path21.resolve(from);
5888
7033
  for (; ; ) {
5889
- if (CONFIG_NAMES2.some((name) => fs15.existsSync(path17.join(dir, name)))) return dir;
5890
- const parent = path17.dirname(dir);
7034
+ if (CONFIG_NAMES2.some((name) => fs18.existsSync(path21.join(dir, name)))) return dir;
7035
+ const parent = path21.dirname(dir);
5891
7036
  if (parent === dir) return null;
5892
7037
  dir = parent;
5893
7038
  }
5894
7039
  }
5895
- function git(cwd, args) {
5896
- return (0, import_node_child_process7.execFileSync)("git", args, {
7040
+ function git2(cwd, args) {
7041
+ return (0, import_node_child_process10.execFileSync)("git", args, {
5897
7042
  cwd,
5898
7043
  encoding: "utf-8",
5899
7044
  stdio: ["ignore", "pipe", "pipe"],
@@ -5902,7 +7047,7 @@ function git(cwd, args) {
5902
7047
  }
5903
7048
  function tryGit(cwd, args) {
5904
7049
  try {
5905
- return git(cwd, args);
7050
+ return git2(cwd, args);
5906
7051
  } catch {
5907
7052
  return void 0;
5908
7053
  }
@@ -5917,7 +7062,7 @@ function translationLookup(repoRoot, base, locale) {
5917
7062
  const build = (side) => {
5918
7063
  const map = /* @__PURE__ */ new Map();
5919
7064
  for (const file of files) {
5920
- const content = side === "new" ? readText(path17.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
7065
+ const content = side === "new" ? readText(path21.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
5921
7066
  if (content === void 0) continue;
5922
7067
  for (const entry of parseTranslationFile(file, content)) {
5923
7068
  if (!map.has(entry.key)) map.set(entry.key, entry.value);
@@ -5936,34 +7081,34 @@ function prefersLocale(file, locale) {
5936
7081
  }
5937
7082
  function readText(file) {
5938
7083
  try {
5939
- return fs15.readFileSync(file, "utf-8");
7084
+ return fs18.readFileSync(file, "utf-8");
5940
7085
  } catch {
5941
7086
  return void 0;
5942
7087
  }
5943
7088
  }
5944
- var CACHE_FILE2 = path17.join(".piwi", "locator-index.json");
7089
+ var CACHE_FILE2 = path21.join(".piwi", "locator-index.json");
5945
7090
  function cacheKey2(connection, branch) {
5946
7091
  return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
5947
7092
  }
5948
7093
  function readCache2(root, key) {
5949
7094
  try {
5950
- const store = JSON.parse(fs15.readFileSync(path17.join(root, CACHE_FILE2), "utf-8"));
7095
+ const store = JSON.parse(fs18.readFileSync(path21.join(root, CACHE_FILE2), "utf-8"));
5951
7096
  return store[key] ?? null;
5952
7097
  } catch {
5953
7098
  return null;
5954
7099
  }
5955
7100
  }
5956
7101
  function writeCache2(root, key, index) {
5957
- const file = path17.join(root, CACHE_FILE2);
7102
+ const file = path21.join(root, CACHE_FILE2);
5958
7103
  try {
5959
7104
  let store = {};
5960
7105
  try {
5961
- store = JSON.parse(fs15.readFileSync(file, "utf-8"));
7106
+ store = JSON.parse(fs18.readFileSync(file, "utf-8"));
5962
7107
  } catch {
5963
7108
  }
5964
7109
  store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
5965
- fs15.mkdirSync(path17.dirname(file), { recursive: true });
5966
- fs15.writeFileSync(file, JSON.stringify(store));
7110
+ fs18.mkdirSync(path21.dirname(file), { recursive: true });
7111
+ fs18.writeFileSync(file, JSON.stringify(store));
5967
7112
  } catch {
5968
7113
  }
5969
7114
  }
@@ -5979,7 +7124,7 @@ function planSites(b, testRoot) {
5979
7124
  const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
5980
7125
  return sites.map((callSite) => {
5981
7126
  if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
5982
- const file = path17.resolve(testRoot, callSiteFile(callSite));
7127
+ const file = path21.resolve(testRoot, callSiteFile(callSite));
5983
7128
  const lineNo = callSiteLine(callSite);
5984
7129
  const text = readText(file);
5985
7130
  if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
@@ -6022,7 +7167,7 @@ function applyEdits(breaks, replacementsOf) {
6022
7167
  }
6023
7168
  const edited = [];
6024
7169
  for (const [file, edits] of byFile) {
6025
- const text = fs15.readFileSync(file, "utf-8");
7170
+ const text = fs18.readFileSync(file, "utf-8");
6026
7171
  const eol = text.includes("\r\n") ? "\r\n" : "\n";
6027
7172
  const lines = text.split(/\r?\n/);
6028
7173
  for (const e of edits) {
@@ -6032,7 +7177,7 @@ function applyEdits(breaks, replacementsOf) {
6032
7177
  }
6033
7178
  const next = lines.join(eol);
6034
7179
  if (next !== text) {
6035
- fs15.writeFileSync(file, next);
7180
+ fs18.writeFileSync(file, next);
6036
7181
  edited.push(file);
6037
7182
  }
6038
7183
  }
@@ -6126,8 +7271,8 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
6126
7271
  }
6127
7272
  async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
6128
7273
  if (argv.includes("-h") || argv.includes("--help")) {
6129
- console.log(USAGE9);
6130
- return EXIT_OK6;
7274
+ console.log(USAGE10);
7275
+ return EXIT_OK7;
6131
7276
  }
6132
7277
  let args;
6133
7278
  try {
@@ -6135,26 +7280,26 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
6135
7280
  } catch (e) {
6136
7281
  console.error(`piwi preflight: ${e.message}
6137
7282
  `);
6138
- console.error(USAGE9);
6139
- return EXIT_ERROR7;
7283
+ console.error(USAGE10);
7284
+ return EXIT_ERROR8;
6140
7285
  }
6141
7286
  const repoRoot = tryGit(cwd, ["rev-parse", "--show-toplevel"])?.trim();
6142
7287
  if (!repoRoot) {
6143
7288
  console.error("piwi preflight: not inside a git repository");
6144
- return EXIT_ERROR7;
7289
+ return EXIT_ERROR8;
6145
7290
  }
6146
- const testRoot = path17.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7291
+ const testRoot = path21.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
6147
7292
  let files;
6148
7293
  try {
6149
- files = parseUnifiedDiff(git(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
7294
+ files = parseUnifiedDiff(git2(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
6150
7295
  } catch (e) {
6151
7296
  console.error(`piwi preflight: could not diff against ${args.base} \u2014 ${e.message.split("\n")[0]}`);
6152
- return EXIT_ERROR7;
7297
+ return EXIT_ERROR8;
6153
7298
  }
6154
7299
  const connection = resolveConnection(args, env, [.../* @__PURE__ */ new Set([testRoot, cwd, repoRoot])]);
6155
7300
  if (!connection) {
6156
7301
  console.error("piwi preflight: no dashboard URL \u2014 pass --server-url, set PIWI_DASHBOARD_URL, or run piwi init");
6157
- return EXIT_ERROR7;
7302
+ return EXIT_ERROR8;
6158
7303
  }
6159
7304
  const key = cacheKey2(connection, args.branch);
6160
7305
  let index;
@@ -6168,7 +7313,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
6168
7313
  const cached = readCache2(testRoot, key);
6169
7314
  if (!cached) {
6170
7315
  console.error(`piwi preflight: ${e.message}, and there is no cached index`);
6171
- return EXIT_ERROR7;
7316
+ return EXIT_ERROR8;
6172
7317
  }
6173
7318
  console.error(
6174
7319
  `piwi preflight: dashboard unreachable, using the index cached ${cached.savedAt} \u2014 ${e.message}`
@@ -6180,7 +7325,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
6180
7325
  testIdAttributes: index.testIdAttributes ?? void 0,
6181
7326
  isTestFile: (file) => TEST_FILE.test(file),
6182
7327
  translations: translationLookup(repoRoot, args.base, args.locale),
6183
- readFile: (file, side) => side === "new" ? readText(path17.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
7328
+ readFile: (file, side) => side === "new" ? readText(path21.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
6184
7329
  });
6185
7330
  const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
6186
7331
  const replacementsOf = /* @__PURE__ */ new Map();
@@ -6225,13 +7370,13 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
6225
7370
  if (args.json) console.log(JSON.stringify(result, null, 2));
6226
7371
  else {
6227
7372
  const rel = (file) => {
6228
- const abs = path17.isAbsolute(file) ? file : path17.resolve(testRoot, file);
6229
- const r = path17.relative(cwd, abs).split(path17.sep).join("/");
7373
+ const abs = path21.isAbsolute(file) ? file : path21.resolve(testRoot, file);
7374
+ const r = path21.relative(cwd, abs).split(path21.sep).join("/");
6230
7375
  return r.startsWith("..") ? file : r || file;
6231
7376
  };
6232
7377
  console.log(renderPreflight(result, rel));
6233
7378
  }
6234
- let code = EXIT_OK6;
7379
+ let code = EXIT_OK7;
6235
7380
  const editedSet = new Set(edited);
6236
7381
  const fixed = (s) => s.action === "applied" || s.action === "edit" && !!s.edit && editedSet.has(s.edit.file);
6237
7382
  const unfixed = breaks.some((b) => b.confidence === "likely" && !(b.sites.length && b.sites.every(fixed)));
@@ -6256,12 +7401,12 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
6256
7401
  }
6257
7402
 
6258
7403
  // src/cli/probe.ts
6259
- var fs16 = __toESM(require("fs"));
7404
+ var fs19 = __toESM(require("fs"));
6260
7405
  var os3 = __toESM(require("os"));
6261
- var path18 = __toESM(require("path"));
6262
- var EXIT_OK7 = 0;
6263
- var EXIT_ERROR8 = 2;
6264
- var USAGE10 = `
7406
+ var path22 = __toESM(require("path"));
7407
+ var EXIT_OK8 = 0;
7408
+ var EXIT_ERROR9 = 2;
7409
+ var USAGE11 = `
6265
7410
  piwi probe \u2014 run the dashboard's probe plan and record what the suite noticed
6266
7411
 
6267
7412
  Usage:
@@ -6283,18 +7428,18 @@ function escapeRegex2(s) {
6283
7428
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6284
7429
  }
6285
7430
  async function runProbe(argv) {
6286
- const sep5 = argv.indexOf("--");
6287
- const own = sep5 < 0 ? argv : argv.slice(0, sep5);
6288
- const playwrightArgs2 = sep5 < 0 ? [] : argv.slice(sep5 + 1);
7431
+ const sep6 = argv.indexOf("--");
7432
+ const own = sep6 < 0 ? argv : argv.slice(0, sep6);
7433
+ const playwrightArgs2 = sep6 < 0 ? [] : argv.slice(sep6 + 1);
6289
7434
  if (own.includes("-h") || own.includes("--help")) {
6290
- console.log(USAGE10);
6291
- return EXIT_OK7;
7435
+ console.log(USAGE11);
7436
+ return EXIT_OK8;
6292
7437
  }
6293
7438
  const env = process.env;
6294
7439
  const serverUrl = (readOption9(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
6295
7440
  if (!serverUrl) {
6296
7441
  console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
6297
- return EXIT_ERROR8;
7442
+ return EXIT_ERROR9;
6298
7443
  }
6299
7444
  const apiKey = readOption9(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
6300
7445
  const project = readOption9(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
@@ -6304,30 +7449,30 @@ async function runProbe(argv) {
6304
7449
  projectId = await resolveProjectId({ serverUrl, apiKey, project, key: "" });
6305
7450
  } catch (e) {
6306
7451
  console.error(`piwi probe: ${e.message}`);
6307
- return EXIT_ERROR8;
7452
+ return EXIT_ERROR9;
6308
7453
  }
6309
- const authHeaders3 = apiKey ? { "X-API-Key": apiKey } : {};
7454
+ const authHeaders4 = apiKey ? { "X-API-Key": apiKey } : {};
6310
7455
  const planUrl = `${serverUrl}/api/projects/${projectId}/probes/plan${budget ? `?budget=${encodeURIComponent(budget)}` : ""}`;
6311
7456
  let plan;
6312
7457
  try {
6313
- const res = await fetch(planUrl, { headers: authHeaders3 });
7458
+ const res = await fetch(planUrl, { headers: authHeaders4 });
6314
7459
  if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
6315
7460
  plan = await res.json();
6316
7461
  } catch (e) {
6317
7462
  console.error(`piwi probe: could not fetch the probe plan \u2014 ${e.message}`);
6318
- return EXIT_ERROR8;
7463
+ return EXIT_ERROR9;
6319
7464
  }
6320
7465
  if (!plan.items || plan.items.length === 0) {
6321
7466
  console.error("piwi probe: nothing to probe \u2014 the plan is empty.");
6322
- return EXIT_OK7;
7467
+ return EXIT_OK8;
6323
7468
  }
6324
7469
  console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
6325
- const dir = fs16.mkdtempSync(path18.join(os3.tmpdir(), "piwi-probe-"));
6326
- const planFile = path18.join(dir, "plan.json");
6327
- const resultsFile = path18.join(dir, "results.jsonl");
6328
- const runOutputFile = path18.join(dir, "run.json");
6329
- fs16.writeFileSync(planFile, JSON.stringify(plan));
6330
- fs16.writeFileSync(resultsFile, "");
7470
+ const dir = fs19.mkdtempSync(path22.join(os3.tmpdir(), "piwi-probe-"));
7471
+ const planFile = path22.join(dir, "plan.json");
7472
+ const resultsFile = path22.join(dir, "results.jsonl");
7473
+ const runOutputFile = path22.join(dir, "run.json");
7474
+ fs19.writeFileSync(planFile, JSON.stringify(plan));
7475
+ fs19.writeFileSync(resultsFile, "");
6331
7476
  const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
6332
7477
  const pwArgs = [...playwrightArgs2];
6333
7478
  if (grep && !playwrightArgs2.includes("--grep") && !playwrightArgs2.includes("-g")) pwArgs.push("--grep", grep);
@@ -6341,7 +7486,7 @@ async function runProbe(argv) {
6341
7486
  [PIWI_ENV_KEYS.outputFile]: runOutputFile
6342
7487
  };
6343
7488
  await spawnPlaywright(pwArgs, childEnv, { command: "piwi probe" });
6344
- const lines = fs16.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
7489
+ const lines = fs19.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
6345
7490
  const results = [];
6346
7491
  for (const line of lines) {
6347
7492
  try {
@@ -6351,18 +7496,18 @@ async function runProbe(argv) {
6351
7496
  }
6352
7497
  if (results.length === 0) {
6353
7498
  console.error("piwi probe: no probe outcomes were recorded (are the Piwi capture fixtures active?).");
6354
- return EXIT_OK7;
7499
+ return EXIT_OK8;
6355
7500
  }
6356
7501
  let runId = null;
6357
7502
  try {
6358
- const out = JSON.parse(fs16.readFileSync(runOutputFile, "utf8"));
7503
+ const out = JSON.parse(fs19.readFileSync(runOutputFile, "utf8"));
6359
7504
  if (typeof out.runId === "number") runId = out.runId;
6360
7505
  } catch {
6361
7506
  }
6362
7507
  try {
6363
7508
  const res = await fetch(`${serverUrl}/api/projects/${projectId}/probes/results`, {
6364
7509
  method: "POST",
6365
- headers: { "Content-Type": "application/json", ...authHeaders3 },
7510
+ headers: { "Content-Type": "application/json", ...authHeaders4 },
6366
7511
  body: JSON.stringify({ runId, results })
6367
7512
  });
6368
7513
  if (!res.ok) throw new Error(`dashboard returned ${res.status}`);
@@ -6374,19 +7519,19 @@ async function runProbe(argv) {
6374
7519
  );
6375
7520
  } catch (e) {
6376
7521
  console.error(`piwi probe: could not post probe results \u2014 ${e.message}`);
6377
- return EXIT_ERROR8;
7522
+ return EXIT_ERROR9;
6378
7523
  }
6379
- return EXIT_OK7;
7524
+ return EXIT_OK8;
6380
7525
  }
6381
7526
 
6382
7527
  // src/cli/quality-report.ts
6383
- var fs17 = __toESM(require("fs"));
6384
- var EXIT_OK8 = 0;
7528
+ var fs20 = __toESM(require("fs"));
7529
+ var EXIT_OK9 = 0;
6385
7530
  var EXIT_VERDICT = 1;
6386
- var EXIT_ERROR9 = 2;
7531
+ var EXIT_ERROR10 = 2;
6387
7532
  var FORMATS = ["md", "json", "html", "pdf", "csv"];
6388
7533
  var DASHBOARDS = ["executive", "engineering", "overview"];
6389
- var USAGE11 = `
7534
+ var USAGE12 = `
6390
7535
  piwi report \u2014 print a quality report from the dashboard
6391
7536
 
6392
7537
  Usage:
@@ -6489,8 +7634,8 @@ async function fetchReport(args, query) {
6489
7634
  }
6490
7635
  async function runQualityReport(argv) {
6491
7636
  if (argv.includes("-h") || argv.includes("--help")) {
6492
- console.log(USAGE11);
6493
- return EXIT_OK8;
7637
+ console.log(USAGE12);
7638
+ return EXIT_OK9;
6494
7639
  }
6495
7640
  try {
6496
7641
  const args = parseReportArgs(argv, process.env);
@@ -6500,23 +7645,23 @@ async function runQualityReport(argv) {
6500
7645
  }
6501
7646
  const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
6502
7647
  const bytes = new Uint8Array(await res.arrayBuffer());
6503
- if (args.output) fs17.writeFileSync(args.output, bytes);
7648
+ if (args.output) fs20.writeFileSync(args.output, bytes);
6504
7649
  else process.stdout.write(bytes);
6505
- if (!args.failOn) return EXIT_OK8;
7650
+ if (!args.failOn) return EXIT_OK9;
6506
7651
  const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();
6507
7652
  if (failsOn(bundle.verdict.tone, args.failOn)) {
6508
7653
  console.error(`piwi report: the verdict is ${bundle.verdict.tone}`);
6509
7654
  return EXIT_VERDICT;
6510
7655
  }
6511
- return EXIT_OK8;
7656
+ return EXIT_OK9;
6512
7657
  } catch (e) {
6513
7658
  console.error(`piwi report: ${e.message}`);
6514
- return EXIT_ERROR9;
7659
+ return EXIT_ERROR10;
6515
7660
  }
6516
7661
  }
6517
7662
 
6518
7663
  // src/cli/index.ts
6519
- var USAGE12 = `
7664
+ var USAGE13 = `
6520
7665
  piwi \u2014 companion commands for the Piwi Dashboard reporter
6521
7666
 
6522
7667
  Usage:
@@ -6527,6 +7672,7 @@ Commands:
6527
7672
  skills Install the Piwi agent skills into this project
6528
7673
  gate Fail a CI job on the dashboard's analysis of a run
6529
7674
  report Print a quality report (Markdown, PDF, HTML, CSV or JSON)
7675
+ coverage Upload a unit-test (or other) coverage report of a commit (coverage upload)
6530
7676
  select Print the Playwright args for a saved test selection
6531
7677
  run Run a saved test selection with playwright test
6532
7678
  probe Run the dashboard's probe plan and record what the suite noticed
@@ -6551,6 +7697,8 @@ async function main() {
6551
7697
  return runGate(rest);
6552
7698
  case "report":
6553
7699
  return runQualityReport(rest);
7700
+ case "coverage":
7701
+ return runCoverage(rest);
6554
7702
  case "select":
6555
7703
  return runSelect(rest);
6556
7704
  case "run":
@@ -6570,12 +7718,12 @@ async function main() {
6570
7718
  case void 0:
6571
7719
  case "-h":
6572
7720
  case "--help":
6573
- console.log(USAGE12);
7721
+ console.log(USAGE13);
6574
7722
  return 0;
6575
7723
  default:
6576
7724
  console.error(`piwi: unknown command "${command}"
6577
7725
  `);
6578
- console.error(USAGE12);
7726
+ console.error(USAGE13);
6579
7727
  return 2;
6580
7728
  }
6581
7729
  }