@piwitests/reporter 0.59.0 → 0.61.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
@@ -788,10 +788,13 @@ async function runAi(argv, env = process.env) {
788
788
  }
789
789
 
790
790
  // src/cli/bug.ts
791
- var fs5 = __toESM(require("fs"));
792
- var path8 = __toESM(require("path"));
791
+ var fs6 = __toESM(require("fs"));
792
+ var path9 = __toESM(require("path"));
793
793
  var import_node_child_process2 = require("child_process");
794
794
 
795
+ // src/internal/config/env.ts
796
+ var path8 = __toESM(require("path"));
797
+
795
798
  // src/internal/config/desktop.ts
796
799
  var fs4 = __toESM(require("fs"));
797
800
  var os = __toESM(require("os"));
@@ -799,6 +802,14 @@ var path7 = __toESM(require("path"));
799
802
  function defaultDesktopConfigPath() {
800
803
  return path7.join(os.homedir(), ".piwi", "desktop.json");
801
804
  }
805
+ function processAlive(pid) {
806
+ try {
807
+ process.kill(pid, 0);
808
+ return true;
809
+ } catch (error) {
810
+ return error.code === "EPERM";
811
+ }
812
+ }
802
813
  function readDesktopConfig(filePath2) {
803
814
  try {
804
815
  const parsed = JSON.parse(fs4.readFileSync(filePath2, "utf8"));
@@ -806,12 +817,59 @@ function readDesktopConfig(filePath2) {
806
817
  const token = parsed?.token;
807
818
  if (typeof url !== "string" || !url) return null;
808
819
  if (typeof token !== "string" || !token) return null;
820
+ if (Number.isInteger(parsed?.pid) && parsed.pid > 0 && !processAlive(parsed.pid)) return null;
809
821
  return { url, token };
810
822
  } catch {
811
823
  return null;
812
824
  }
813
825
  }
814
826
 
827
+ // src/internal/config/dotenv.ts
828
+ var fs5 = __toESM(require("fs"));
829
+
830
+ // ../core/src/dotenv.ts
831
+ function parseDotEnv(text) {
832
+ const out = {};
833
+ for (const raw of text.split(/\r?\n/)) {
834
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
835
+ if (!m) continue;
836
+ let value = m[2];
837
+ const quote3 = value[0];
838
+ if ((quote3 === '"' || quote3 === "'") && value.length > 1 && value.endsWith(quote3)) {
839
+ value = value.slice(1, -1);
840
+ if (quote3 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
841
+ } else {
842
+ value = value.replace(/\s+#.*$/, "");
843
+ }
844
+ out[m[1]] = value;
845
+ }
846
+ return out;
847
+ }
848
+ function resolvePiwiConnection(sources) {
849
+ const own = {
850
+ serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
851
+ apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
852
+ };
853
+ const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
854
+ const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
855
+ const desktop = sources.desktop ?? null;
856
+ const trim = (url) => url.replace(/\/+$/, "");
857
+ const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
858
+ const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
859
+ if (!serverUrl) return null;
860
+ const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
861
+ return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
862
+ }
863
+
864
+ // src/internal/config/dotenv.ts
865
+ function readDotEnv(filePath2) {
866
+ try {
867
+ return parseDotEnv(fs5.readFileSync(filePath2, "utf-8"));
868
+ } catch {
869
+ return {};
870
+ }
871
+ }
872
+
815
873
  // src/internal/config/env.ts
816
874
  var DEFAULTS = {
817
875
  projectName: "default-project",
@@ -848,6 +906,7 @@ var DEFAULTS = {
848
906
  };
849
907
  var PIWI_ENV_KEYS = {
850
908
  serverUrl: "PIWI_DASHBOARD_URL",
909
+ desktopApp: "PIWI_DESKTOP_APP",
851
910
  projectName: "PIWI_PROJECT_NAME",
852
911
  verbose: "PIWI_VERBOSE",
853
912
  apiKey: "PIWI_API_KEY",
@@ -873,6 +932,7 @@ var PIWI_ENV_KEYS = {
873
932
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
874
933
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
875
934
  coverageServer: "PIWI_COVERAGE_SERVER",
935
+ coverageCss: "PIWI_COVERAGE_CSS",
876
936
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
877
937
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
878
938
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -1041,7 +1101,7 @@ function repositoryRoot(cwd) {
1041
1101
  encoding: "utf-8",
1042
1102
  stdio: ["ignore", "pipe", "ignore"]
1043
1103
  }).trim();
1044
- return out ? path8.resolve(out) : null;
1104
+ return out ? path9.resolve(out) : null;
1045
1105
  } catch {
1046
1106
  return null;
1047
1107
  }
@@ -1049,20 +1109,20 @@ function repositoryRoot(cwd) {
1049
1109
  function realFolder(dir) {
1050
1110
  const rest = [];
1051
1111
  let current = dir;
1052
- while (!fs5.existsSync(current) && path8.dirname(current) !== current) {
1053
- rest.unshift(path8.basename(current));
1054
- current = path8.dirname(current);
1112
+ while (!fs6.existsSync(current) && path9.dirname(current) !== current) {
1113
+ rest.unshift(path9.basename(current));
1114
+ current = path9.dirname(current);
1055
1115
  }
1056
- return path8.join(fs5.realpathSync(current), ...rest);
1116
+ return path9.join(fs6.realpathSync(current), ...rest);
1057
1117
  }
1058
1118
  function folderInRepository(root, dir) {
1059
- const relative9 = path8.relative(realFolder(root), realFolder(dir));
1060
- if (relative9.startsWith("..") || path8.isAbsolute(relative9)) return void 0;
1061
- return relative9.split(path8.sep).join("/");
1119
+ const relative9 = path9.relative(realFolder(root), realFolder(dir));
1120
+ if (relative9.startsWith("..") || path9.isAbsolute(relative9)) return void 0;
1121
+ return relative9.split(path9.sep).join("/");
1062
1122
  }
1063
1123
  function fileFilter(file) {
1064
1124
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1065
- return `${escape(path8.basename(path8.dirname(file)))}[\\\\/]${escape(path8.basename(file))}$`;
1125
+ return `${escape(path9.basename(path9.dirname(file)))}[\\\\/]${escape(path9.basename(file))}$`;
1066
1126
  }
1067
1127
  function runPlaywright(file, cwd, id, env) {
1068
1128
  return spawnPlaywright([fileFilter(file)], { ...env, ...originEnv("bug", id) }, { cwd, command: "piwi bug" });
@@ -1082,10 +1142,10 @@ ${USAGE2}`);
1082
1142
  return EXIT_ERROR2;
1083
1143
  }
1084
1144
  const root = args.write ? repositoryRoot(cwd) : null;
1085
- const outFile = args.out !== null ? path8.resolve(cwd, args.out) : null;
1145
+ const outFile = args.out !== null ? path9.resolve(cwd, args.out) : null;
1086
1146
  let spec;
1087
1147
  try {
1088
- spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path8.dirname(outFile)) : void 0);
1148
+ spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path9.dirname(outFile)) : void 0);
1089
1149
  } catch (e) {
1090
1150
  console.error(`piwi bug: ${e.message}`);
1091
1151
  return EXIT_ERROR2;
@@ -1100,16 +1160,16 @@ ${USAGE2}`);
1100
1160
  if (!outFile && !root) {
1101
1161
  console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
1102
1162
  }
1103
- const file = outFile ?? path8.resolve(root ?? cwd, spec.path);
1163
+ const file = outFile ?? path9.resolve(root ?? cwd, spec.path);
1104
1164
  try {
1105
- fs5.mkdirSync(path8.dirname(file), { recursive: true });
1106
- fs5.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
1165
+ fs6.mkdirSync(path9.dirname(file), { recursive: true });
1166
+ fs6.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
1107
1167
  } catch (e) {
1108
1168
  const exists = e.code === "EEXIST";
1109
1169
  console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
1110
1170
  return EXIT_ERROR2;
1111
1171
  }
1112
- const shown = path8.relative(cwd, file) || file;
1172
+ const shown = path9.relative(cwd, file) || file;
1113
1173
  console.error(`piwi bug: wrote ${shown}`);
1114
1174
  if (!args.run) return EXIT_OK2;
1115
1175
  const code = await runPlaywright(file, cwd, args.id, env);
@@ -1127,8 +1187,8 @@ ${USAGE2}`);
1127
1187
  }
1128
1188
 
1129
1189
  // src/cli/codegen.ts
1130
- var fs6 = __toESM(require("fs"));
1131
- var path9 = __toESM(require("path"));
1190
+ var fs7 = __toESM(require("fs"));
1191
+ var path10 = __toESM(require("path"));
1132
1192
 
1133
1193
  // ../core/src/recording.ts
1134
1194
  var ASSERTION_MATCHERS = [
@@ -2168,7 +2228,7 @@ function renderSpec(session, options = {}) {
2168
2228
  if (viewport) bodyLines.push(viewportLine(viewport, page));
2169
2229
  stepStarts[pos] = bodyLines.length;
2170
2230
  if (step.action === "goto") {
2171
- bodyLines.push(...renderRawStep(step, pos, ctx));
2231
+ if (!(options.continuePage && pos === 0)) bodyLines.push(...renderRawStep(step, pos, ctx));
2172
2232
  sawGoto = true;
2173
2233
  pos++;
2174
2234
  continue;
@@ -2187,10 +2247,10 @@ function renderSpec(session, options = {}) {
2187
2247
  if (!callAt(pos + 1)) checkNextPage(pos);
2188
2248
  pos++;
2189
2249
  }
2190
- const start = viewportAt.get(0);
2250
+ const start = options.continuePage ? void 0 : viewportAt.get(0);
2191
2251
  const opening = [
2192
2252
  ...start ? [viewportLine(start, page)] : [],
2193
- ...!sawGoto && session.startUrl ? [` await ${page}.goto(${quote(urlForCode(session.startUrl, ctx))});`] : []
2253
+ ...!sawGoto && !options.continuePage && session.startUrl ? [` await ${page}.goto(${quote(urlForCode(session.startUrl, ctx))});`] : []
2194
2254
  ];
2195
2255
  bodyLines.unshift(...opening);
2196
2256
  const bodyOffset = opening.length;
@@ -2261,6 +2321,8 @@ var STEPS_LIMITS = {
2261
2321
  /** Titles, names, texts, locators. */
2262
2322
  textLength: 500,
2263
2323
  viewports: 50,
2324
+ /** The options a manual step's instruction names. */
2325
+ options: 50,
2264
2326
  /** The widest and tallest viewport, in CSS pixels. */
2265
2327
  viewportSize: 1e4,
2266
2328
  /** The browser's zoom factors a viewport may name, as browsers allow them. */
@@ -2345,43 +2407,43 @@ var Checker = class {
2345
2407
  constructor() {
2346
2408
  this.errors = [];
2347
2409
  }
2348
- fail(path24, message) {
2349
- if (this.errors.length < 20) this.errors.push(`${path24}: ${message}`);
2410
+ fail(path25, message) {
2411
+ if (this.errors.length < 20) this.errors.push(`${path25}: ${message}`);
2350
2412
  }
2351
- text(v, path24, max, nullable) {
2413
+ text(v, path25, max, nullable) {
2352
2414
  if (v == null) {
2353
- if (!nullable) this.fail(path24, "is required");
2415
+ if (!nullable) this.fail(path25, "is required");
2354
2416
  return null;
2355
2417
  }
2356
2418
  if (typeof v !== "string") {
2357
- this.fail(path24, "must be a string");
2419
+ this.fail(path25, "must be a string");
2358
2420
  return null;
2359
2421
  }
2360
- if (v.length > max) this.fail(path24, `is longer than ${max} characters`);
2422
+ if (v.length > max) this.fail(path25, `is longer than ${max} characters`);
2361
2423
  return v;
2362
2424
  }
2363
- number(v, path24) {
2425
+ number(v, path25) {
2364
2426
  if (typeof v !== "number" || !Number.isFinite(v)) {
2365
- this.fail(path24, "must be a number");
2427
+ this.fail(path25, "must be a number");
2366
2428
  return 0;
2367
2429
  }
2368
2430
  return v;
2369
2431
  }
2370
2432
  };
2371
- function checkTarget(c, v, path24) {
2433
+ function checkTarget(c, v, path25) {
2372
2434
  if (v == null) return null;
2373
2435
  if (!isObject(v)) {
2374
- c.fail(path24, "must be an object");
2436
+ c.fail(path25, "must be an object");
2375
2437
  return null;
2376
2438
  }
2377
2439
  const alternativesIn = v.alternatives ?? [];
2378
2440
  const alternatives = [];
2379
- if (!Array.isArray(alternativesIn)) c.fail(`${path24}.alternatives`, "must be a list");
2441
+ if (!Array.isArray(alternativesIn)) c.fail(`${path25}.alternatives`, "must be a list");
2380
2442
  else if (alternativesIn.length > STEPS_LIMITS.alternatives)
2381
- c.fail(`${path24}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
2443
+ c.fail(`${path25}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
2382
2444
  else {
2383
2445
  alternativesIn.forEach((alt, i) => {
2384
- const at = `${path24}.alternatives[${i}]`;
2446
+ const at = `${path25}.alternatives[${i}]`;
2385
2447
  if (!isObject(alt)) return c.fail(at, "must be an object");
2386
2448
  const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
2387
2449
  const safe = locator ? safeLocator(locator) : null;
@@ -2395,49 +2457,108 @@ function checkTarget(c, v, path24) {
2395
2457
  });
2396
2458
  }
2397
2459
  return {
2398
- tagName: c.text(v.tagName, `${path24}.tagName`, 64, false) ?? "",
2399
- role: c.text(v.role, `${path24}.role`, 64, true),
2400
- accessibleName: c.text(v.accessibleName, `${path24}.accessibleName`, STEPS_LIMITS.textLength, true),
2401
- testId: c.text(v.testId, `${path24}.testId`, STEPS_LIMITS.textLength, true),
2402
- text: c.text(v.text, `${path24}.text`, STEPS_LIMITS.textLength, true),
2460
+ tagName: c.text(v.tagName, `${path25}.tagName`, 64, false) ?? "",
2461
+ role: c.text(v.role, `${path25}.role`, 64, true),
2462
+ accessibleName: c.text(v.accessibleName, `${path25}.accessibleName`, STEPS_LIMITS.textLength, true),
2463
+ testId: c.text(v.testId, `${path25}.testId`, STEPS_LIMITS.textLength, true),
2464
+ text: c.text(v.text, `${path25}.text`, STEPS_LIMITS.textLength, true),
2403
2465
  alternatives
2404
2466
  };
2405
2467
  }
2406
- function checkAssertion(c, v, path24) {
2468
+ function checkAssertion(c, v, path25) {
2407
2469
  if (!isObject(v)) {
2408
- c.fail(path24, "is required on an assert step");
2470
+ c.fail(path25, "is required on an assert step");
2409
2471
  return void 0;
2410
2472
  }
2411
2473
  const matcher = v.matcher;
2412
2474
  if (!ASSERTION_MATCHERS.includes(matcher)) {
2413
- c.fail(`${path24}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
2475
+ c.fail(`${path25}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
2414
2476
  return void 0;
2415
2477
  }
2416
- const expected = c.text(v.expected, `${path24}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
2478
+ const expected = c.text(v.expected, `${path25}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
2417
2479
  if (matcher === "toHaveCount" && expected != null && !/^\d{1,6}$/.test(expected)) {
2418
- c.fail(`${path24}.expected`, "must be a whole number of elements on a toHaveCount assertion");
2480
+ c.fail(`${path25}.expected`, "must be a whole number of elements on a toHaveCount assertion");
2419
2481
  }
2420
2482
  return {
2421
2483
  matcher,
2422
2484
  expected: VALUE_MATCHERS.has(matcher) ? expected : null,
2423
- actual: c.text(v.actual, `${path24}.actual`, STEPS_LIMITS.valueLength, true),
2485
+ actual: c.text(v.actual, `${path25}.actual`, STEPS_LIMITS.valueLength, true),
2424
2486
  negated: v.negated === true,
2425
- note: c.text(v.note, `${path24}.note`, STEPS_LIMITS.valueLength, true)
2487
+ note: c.text(v.note, `${path25}.note`, STEPS_LIMITS.valueLength, true)
2426
2488
  };
2427
2489
  }
2428
- function checkManual(c, v, path24) {
2490
+ function checkManual(c, v, path25) {
2429
2491
  if (!isObject(v)) {
2430
- c.fail(path24, "is required on a manual step");
2492
+ c.fail(path25, "is required on a manual step");
2431
2493
  return void 0;
2432
2494
  }
2433
- const description = c.text(v.description, `${path24}.description`, STEPS_LIMITS.valueLength, false);
2434
- if (description != null && !description.trim()) c.fail(`${path24}.description`, "must not be empty");
2495
+ const description = c.text(v.description, `${path25}.description`, STEPS_LIMITS.valueLength, false);
2496
+ if (description != null && !description.trim()) c.fail(`${path25}.description`, "must not be empty");
2497
+ const instruction = readInstruction(v.instruction);
2435
2498
  return {
2436
2499
  description: description ?? "",
2437
- location: c.text(v.location, `${path24}.location`, STEPS_LIMITS.textLength, true),
2438
- call: c.text(v.call, `${path24}.call`, STEPS_LIMITS.textLength, true)
2500
+ location: c.text(v.location, `${path25}.location`, STEPS_LIMITS.textLength, true),
2501
+ call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true),
2502
+ ...instruction ? { instruction } : {}
2439
2503
  };
2440
2504
  }
2505
+ var isText = (v) => typeof v === "string" && v.length <= STEPS_LIMITS.valueLength;
2506
+ var isTextOrNull = (v) => v == null || isText(v);
2507
+ var isCount = (v) => Number.isInteger(v) && v >= 0;
2508
+ var INSTRUCTION_FIELDS = {
2509
+ locator: isText,
2510
+ call: isText,
2511
+ matcher: isText,
2512
+ what: isText,
2513
+ expected: isTextOrNull,
2514
+ received: isTextOrNull,
2515
+ x: (v) => typeof v === "number" && Number.isFinite(v),
2516
+ y: (v) => typeof v === "number" && Number.isFinite(v),
2517
+ count: isCount,
2518
+ limit: isCount,
2519
+ options: (v) => Array.isArray(v) && v.length > 0 && v.length <= STEPS_LIMITS.options && v.every((o) => isText(o) || isCount(o))
2520
+ };
2521
+ var CHECK_VALUES = ["expected", "received"];
2522
+ var INSTRUCTION_KINDS = {
2523
+ goBack: [],
2524
+ goForward: [],
2525
+ reload: [],
2526
+ gotoUnknown: [],
2527
+ setContent: [],
2528
+ keyboardType: [],
2529
+ mouseAt: ["x", "y", "call"],
2530
+ mouseOrKeyboard: ["call"],
2531
+ failedCall: ["call"],
2532
+ locatorCall: ["locator", "call"],
2533
+ fillUnknown: ["locator"],
2534
+ fillPartial: ["locator"],
2535
+ pressUnknown: ["locator"],
2536
+ selectOptionUnknown: ["locator"],
2537
+ setInputFiles: ["locator"],
2538
+ dragTo: ["locator"],
2539
+ drop: ["locator"],
2540
+ focus: ["locator"],
2541
+ blur: ["locator"],
2542
+ dispatchEvent: ["locator"],
2543
+ scrollIntoViewIfNeeded: ["locator"],
2544
+ selectOptions: ["locator", "options"],
2545
+ expectPoll: ["matcher", ...CHECK_VALUES],
2546
+ expectValue: ["what", ...CHECK_VALUES],
2547
+ expectLocator: ["locator", "matcher", ...CHECK_VALUES],
2548
+ expectURL: CHECK_VALUES,
2549
+ leadingSteps: ["count", "limit"]
2550
+ };
2551
+ function readInstruction(v) {
2552
+ if (!isObject(v) || typeof v.kind !== "string" || !Object.prototype.hasOwnProperty.call(INSTRUCTION_KINDS, v.kind))
2553
+ return null;
2554
+ const kind = v.kind;
2555
+ const out = { kind };
2556
+ for (const field of INSTRUCTION_KINDS[kind]) {
2557
+ if (!INSTRUCTION_FIELDS[field](v[field])) return null;
2558
+ out[field] = v[field] ?? null;
2559
+ }
2560
+ return out;
2561
+ }
2441
2562
  function checkViewports(c, v, stepCount) {
2442
2563
  if (v == null) return [];
2443
2564
  if (!Array.isArray(v)) {
@@ -2476,43 +2597,43 @@ function checkViewports(c, v, stepCount) {
2476
2597
  });
2477
2598
  return out;
2478
2599
  }
2479
- function checkStep(c, v, path24) {
2600
+ function checkStep(c, v, path25) {
2480
2601
  if (!isObject(v)) {
2481
- c.fail(path24, "must be an object");
2602
+ c.fail(path25, "must be an object");
2482
2603
  return null;
2483
2604
  }
2484
2605
  const action = v.action;
2485
2606
  if (!ACTIONS.has(action)) {
2486
- c.fail(`${path24}.action`, "is not a known action");
2607
+ c.fail(`${path25}.action`, "is not a known action");
2487
2608
  return null;
2488
2609
  }
2489
2610
  const step = {
2490
2611
  action: action === "assertVisible" ? "assert" : action,
2491
- target: checkTarget(c, v.target, `${path24}.target`),
2492
- value: c.text(v.value, `${path24}.value`, STEPS_LIMITS.valueLength, true),
2612
+ target: checkTarget(c, v.target, `${path25}.target`),
2613
+ value: c.text(v.value, `${path25}.value`, STEPS_LIMITS.valueLength, true),
2493
2614
  redacted: v.redacted === true,
2494
- pageUrl: c.text(v.pageUrl, `${path24}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2495
- timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path24}.timestamp`)
2615
+ pageUrl: c.text(v.pageUrl, `${path25}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2616
+ timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path25}.timestamp`)
2496
2617
  };
2497
2618
  if (step.redacted) step.value = null;
2498
2619
  if (action === "hover" || action === "dblclick") {
2499
2620
  step.value = null;
2500
- if (!step.target) c.fail(`${path24}.target`, `is required on a ${action} step`);
2621
+ if (!step.target) c.fail(`${path25}.target`, `is required on a ${action} step`);
2501
2622
  }
2502
- if (action === "setInputFiles" && !step.target) c.fail(`${path24}.target`, "is required on a setInputFiles step");
2623
+ if (action === "setInputFiles" && !step.target) c.fail(`${path25}.target`, "is required on a setInputFiles step");
2503
2624
  if (action === "dragTo") {
2504
2625
  step.value = null;
2505
- if (!step.target) c.fail(`${path24}.target`, "is required on a dragTo step");
2506
- step.dropTarget = checkTarget(c, v.dropTarget, `${path24}.dropTarget`);
2507
- if (!step.dropTarget) c.fail(`${path24}.dropTarget`, "is required on a dragTo step");
2626
+ if (!step.target) c.fail(`${path25}.target`, "is required on a dragTo step");
2627
+ step.dropTarget = checkTarget(c, v.dropTarget, `${path25}.dropTarget`);
2628
+ if (!step.dropTarget) c.fail(`${path25}.dropTarget`, "is required on a dragTo step");
2508
2629
  }
2509
2630
  if (action === "assertVisible") {
2510
2631
  step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
2511
2632
  } else if (action === "assert") {
2512
- step.assertion = checkAssertion(c, v.assertion, `${path24}.assertion`);
2633
+ step.assertion = checkAssertion(c, v.assertion, `${path25}.assertion`);
2513
2634
  } else if (action === "manual") {
2514
2635
  step.value = null;
2515
- step.manual = checkManual(c, v.manual, `${path24}.manual`);
2636
+ step.manual = checkManual(c, v.manual, `${path25}.manual`);
2516
2637
  }
2517
2638
  return step;
2518
2639
  }
@@ -2592,13 +2713,14 @@ async function fetchResolution(options, projectId) {
2592
2713
  }
2593
2714
  return await res.json();
2594
2715
  }
2595
- async function fetchImpact(options, projectId, changedFiles) {
2716
+ async function fetchImpact(options, projectId, changedFiles, changes = []) {
2596
2717
  const url = `${options.serverUrl}/api/projects/${projectId}/selections/impact`;
2597
2718
  const res = await fetch(url, {
2598
2719
  method: "POST",
2599
2720
  headers: { "Content-Type": "application/json", ...authHeaders(options.apiKey) },
2600
2721
  body: JSON.stringify({
2601
2722
  changedFiles,
2723
+ ...changes.length > 0 ? { changes } : {},
2602
2724
  format: options.format ?? "args",
2603
2725
  shard: options.shard ?? void 0,
2604
2726
  order: options.order ?? void 0
@@ -2811,8 +2933,8 @@ ${USAGE3}`);
2811
2933
  }
2812
2934
  } else {
2813
2935
  try {
2814
- if (fs6.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
2815
- text = fs6.readFileSync(args.file, "utf-8");
2936
+ if (fs7.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
2937
+ text = fs7.readFileSync(args.file, "utf-8");
2816
2938
  } catch (e) {
2817
2939
  const reason = e.code === "ENOENT" ? "does not exist" : e.message;
2818
2940
  console.error(`piwi codegen: ${args.file} ${reason}`);
@@ -2844,8 +2966,8 @@ ${USAGE3}`);
2844
2966
  return EXIT_OK3;
2845
2967
  }
2846
2968
  try {
2847
- fs6.mkdirSync(path9.dirname(args.out), { recursive: true });
2848
- fs6.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
2969
+ fs7.mkdirSync(path10.dirname(args.out), { recursive: true });
2970
+ fs7.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
2849
2971
  } catch (e) {
2850
2972
  const exists = e.code === "EEXIST";
2851
2973
  console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
@@ -2859,7 +2981,7 @@ ${USAGE3}`);
2859
2981
  }
2860
2982
 
2861
2983
  // src/cli/coverage.ts
2862
- var path14 = __toESM(require("path"));
2984
+ var path15 = __toESM(require("path"));
2863
2985
 
2864
2986
  // ../core/src/codeowners.ts
2865
2987
  function patternParts(pattern) {
@@ -2897,10 +3019,10 @@ function pathPatternMatcher(pattern) {
2897
3019
  const { segments, floating } = patternParts(pattern);
2898
3020
  if (floating) {
2899
3021
  const only = segments[0];
2900
- return (path24) => path24.split("/").some((part) => segmentMatches(only, part));
3022
+ return (path25) => path25.split("/").some((part) => segmentMatches(only, part));
2901
3023
  }
2902
- return (path24) => {
2903
- const parts = path24.split("/");
3024
+ return (path25) => {
3025
+ const parts = path25.split("/");
2904
3026
  let at = new Uint8Array(parts.length);
2905
3027
  at[0] = 1;
2906
3028
  for (let i = 0; i < segments.length; i++) {
@@ -2966,8 +3088,8 @@ function parseCoverageScope(raw) {
2966
3088
  function compileCoverageScope(scope) {
2967
3089
  const include = scope.include.map(pathPatternMatcher);
2968
3090
  const exclude = scope.exclude.map(pathPatternMatcher);
2969
- return (path24) => {
2970
- const file = path24.replace(/\\/g, "/").replace(/^\.?\//, "");
3091
+ return (path25) => {
3092
+ const file = path25.replace(/\\/g, "/").replace(/^\.?\//, "");
2971
3093
  if (include.length > 0 && !include.some((matches) => matches(file))) return false;
2972
3094
  return !exclude.some((matches) => matches(file));
2973
3095
  };
@@ -2983,7 +3105,7 @@ function errorMessage(error) {
2983
3105
  }
2984
3106
 
2985
3107
  // src/internal/support/test-paths.ts
2986
- var path10 = __toESM(require("path"));
3108
+ var path11 = __toESM(require("path"));
2987
3109
 
2988
3110
  // src/internal/collect/metadata-collector.ts
2989
3111
  var DETACHED_HEAD = "HEAD";
@@ -3016,14 +3138,14 @@ function normalizeRef(ref) {
3016
3138
 
3017
3139
  // src/internal/coverage-upload/git-inventory.ts
3018
3140
  var import_node_child_process5 = require("child_process");
3019
- var fs8 = __toESM(require("fs"));
3020
- var path12 = __toESM(require("path"));
3141
+ var fs9 = __toESM(require("fs"));
3142
+ var path13 = __toESM(require("path"));
3021
3143
 
3022
3144
  // src/internal/collect/blob-ids.ts
3023
3145
  var import_node_child_process4 = require("child_process");
3024
3146
  var crypto2 = __toESM(require("crypto"));
3025
- var fs7 = __toESM(require("fs"));
3026
- var path11 = __toESM(require("path"));
3147
+ var fs8 = __toESM(require("fs"));
3148
+ var path12 = __toESM(require("path"));
3027
3149
  function gitHashObjects(root, paths) {
3028
3150
  return new Promise((resolve10) => {
3029
3151
  const child = (0, import_node_child_process4.execFile)(
@@ -3090,7 +3212,7 @@ async function readInventory(root, warn = () => {
3090
3212
  const changed = [];
3091
3213
  for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
3092
3214
  if (!file || !inventory.has(file)) continue;
3093
- if (fs8.existsSync(path12.join(root, file))) changed.push(file);
3215
+ if (fs9.existsSync(path13.join(root, file))) changed.push(file);
3094
3216
  else inventory.delete(file);
3095
3217
  }
3096
3218
  const ids = await hashPaths(root, changed);
@@ -3109,8 +3231,8 @@ async function readInventory(root, warn = () => {
3109
3231
  }
3110
3232
 
3111
3233
  // src/internal/coverage-upload/report-files.ts
3112
- var fs9 = __toESM(require("fs"));
3113
- var path13 = __toESM(require("path"));
3234
+ var fs10 = __toESM(require("fs"));
3235
+ var path14 = __toESM(require("path"));
3114
3236
 
3115
3237
  // ../core/src/coverage.ts
3116
3238
  var MAX_LINE = 1e6;
@@ -3189,7 +3311,7 @@ var FileLines = class {
3189
3311
  this.functions.set(key, { start, end: last, ran });
3190
3312
  }
3191
3313
  }
3192
- toFile(path24) {
3314
+ toFile(path25) {
3193
3315
  const executable = [];
3194
3316
  const lines = [];
3195
3317
  const push = (ranges, line) => {
@@ -3209,7 +3331,7 @@ var FileLines = class {
3209
3331
  if (didRun) ran.push([start, end]);
3210
3332
  }
3211
3333
  return {
3212
- path: path24,
3334
+ path: path25,
3213
3335
  executable,
3214
3336
  lines,
3215
3337
  functions: normalizeFunctionRanges(ran),
@@ -3221,16 +3343,16 @@ var ReportFiles = class {
3221
3343
  constructor() {
3222
3344
  this.byPath = /* @__PURE__ */ new Map();
3223
3345
  }
3224
- of(path24) {
3225
- let file = this.byPath.get(path24);
3346
+ of(path25) {
3347
+ let file = this.byPath.get(path25);
3226
3348
  if (!file) {
3227
3349
  file = new FileLines();
3228
- this.byPath.set(path24, file);
3350
+ this.byPath.set(path25, file);
3229
3351
  }
3230
3352
  return file;
3231
3353
  }
3232
3354
  toFiles() {
3233
- return [...this.byPath].map(([path24, file]) => file.toFile(path24));
3355
+ return [...this.byPath].map(([path25, file]) => file.toFile(path25));
3234
3356
  }
3235
3357
  };
3236
3358
  var XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
@@ -3569,23 +3691,23 @@ function normalizeSlashPath(raw) {
3569
3691
  }
3570
3692
  return (drive || (absolute ? "/" : "")) + out.join("/");
3571
3693
  }
3572
- function isAbsolutePath(path24) {
3573
- return path24.startsWith("/") || /^[A-Za-z]:\//.test(path24);
3694
+ function isAbsolutePath(path25) {
3695
+ return path25.startsWith("/") || /^[A-Za-z]:\//.test(path25);
3574
3696
  }
3575
- function relativeTo(path24, root) {
3697
+ function relativeTo(path25, root) {
3576
3698
  const drive = /^[A-Za-z]:/.test(root);
3577
3699
  const base = root.endsWith("/") ? root : `${root}/`;
3578
- const head = path24.slice(0, base.length);
3700
+ const head = path25.slice(0, base.length);
3579
3701
  const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
3580
3702
  if (!inside) return null;
3581
- const relative9 = path24.slice(base.length);
3703
+ const relative9 = path25.slice(base.length);
3582
3704
  return relative9 || null;
3583
3705
  }
3584
3706
  function reportPathCandidates(raw, { repoRoot, bases }) {
3585
3707
  const slash = (value) => value.replace(/\\/g, "/");
3586
3708
  const root = normalizeSlashPath(slash(repoRoot));
3587
- const path24 = slash(raw.trim());
3588
- if (!root || !path24 || path24.includes("\0")) return [];
3709
+ const path25 = slash(raw.trim());
3710
+ if (!root || !path25 || path25.includes("\0")) return [];
3589
3711
  const out = [];
3590
3712
  const add = (absolute) => {
3591
3713
  const normalized = normalizeSlashPath(absolute);
@@ -3593,12 +3715,12 @@ function reportPathCandidates(raw, { repoRoot, bases }) {
3593
3715
  const relative9 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
3594
3716
  if (relative9 && !out.includes(relative9)) out.push(relative9);
3595
3717
  };
3596
- if (isAbsolutePath(path24)) {
3597
- add(path24);
3718
+ if (isAbsolutePath(path25)) {
3719
+ add(path25);
3598
3720
  } else {
3599
3721
  for (const base of bases) {
3600
3722
  const dir = slash(base);
3601
- if (isAbsolutePath(dir)) add(`${dir}/${path24}`);
3723
+ if (isAbsolutePath(dir)) add(`${dir}/${path25}`);
3602
3724
  }
3603
3725
  }
3604
3726
  return out;
@@ -3622,14 +3744,14 @@ function globSegmentRegExp(segment) {
3622
3744
  function expandPattern(pattern, cwd) {
3623
3745
  const normalized = pattern.replace(/\\/g, "/");
3624
3746
  if (!/[*?]/.test(normalized)) {
3625
- const file = path13.resolve(cwd, normalized);
3626
- return fs9.existsSync(file) && fs9.statSync(file).isFile() ? [file] : [];
3747
+ const file = path14.resolve(cwd, normalized);
3748
+ return fs10.existsSync(file) && fs10.statSync(file).isFile() ? [file] : [];
3627
3749
  }
3628
- const absolute = path13.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3750
+ const absolute = path14.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3629
3751
  const segments = normalized.split("/");
3630
3752
  const firstWild = segments.findIndex((s) => /[*?]/.test(s));
3631
3753
  const baseParts = segments.slice(0, firstWild);
3632
- const base = absolute ? baseParts.join("/") || "/" : path13.resolve(cwd, ...baseParts);
3754
+ const base = absolute ? baseParts.join("/") || "/" : path14.resolve(cwd, ...baseParts);
3633
3755
  const rest = segments.slice(firstWild).filter((s) => s !== "");
3634
3756
  const out = [];
3635
3757
  const walk = (dir, at) => {
@@ -3637,14 +3759,14 @@ function expandPattern(pattern, cwd) {
3637
3759
  const segment = rest[at];
3638
3760
  let entries;
3639
3761
  try {
3640
- entries = fs9.readdirSync(dir, { withFileTypes: true });
3762
+ entries = fs10.readdirSync(dir, { withFileTypes: true });
3641
3763
  } catch {
3642
3764
  return;
3643
3765
  }
3644
3766
  if (segment === "**") {
3645
3767
  walk(dir, at + 1);
3646
3768
  for (const entry of entries) {
3647
- if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path13.join(dir, entry.name), at);
3769
+ if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path14.join(dir, entry.name), at);
3648
3770
  }
3649
3771
  return;
3650
3772
  }
@@ -3652,7 +3774,7 @@ function expandPattern(pattern, cwd) {
3652
3774
  const last = at === rest.length - 1;
3653
3775
  for (const entry of entries) {
3654
3776
  if (!match.test(entry.name)) continue;
3655
- const full = path13.join(dir, entry.name);
3777
+ const full = path14.join(dir, entry.name);
3656
3778
  if (last && entry.isFile()) out.push(full);
3657
3779
  else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
3658
3780
  walk(full, at + 1);
@@ -3665,7 +3787,7 @@ function expandPattern(pattern, cwd) {
3665
3787
  async function readReport(file, format) {
3666
3788
  let reader = format ? new CoverageReportReader(format) : null;
3667
3789
  let head = "";
3668
- for await (const chunk of fs9.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3790
+ for await (const chunk of fs10.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3669
3791
  if (!reader) {
3670
3792
  head += chunk;
3671
3793
  if (head.length < 4096) continue;
@@ -3687,18 +3809,18 @@ async function readReport(file, format) {
3687
3809
  return reader.end();
3688
3810
  }
3689
3811
  function reportBases(report, reportFile, options) {
3690
- if (options.root) return [path13.resolve(options.cwd, options.root)];
3691
- const dir = path13.dirname(path13.resolve(reportFile));
3812
+ if (options.root) return [path14.resolve(options.cwd, options.root)];
3813
+ const dir = path14.dirname(path14.resolve(reportFile));
3692
3814
  const sources = report.sources.map(
3693
- (source) => path13.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path13.resolve(dir, source)
3815
+ (source) => path14.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path14.resolve(dir, source)
3694
3816
  );
3695
- return [...sources, options.cwd, dir, path13.dirname(dir), options.repoRoot];
3817
+ return [...sources, options.cwd, dir, path14.dirname(dir), options.repoRoot];
3696
3818
  }
3697
3819
  function resolveReport(report, reportFile, options, into) {
3698
3820
  const bases = reportBases(report, reportFile, options);
3699
3821
  for (const file of report.files) {
3700
3822
  const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
3701
- const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs9.existsSync(path13.join(options.repoRoot, candidate)));
3823
+ const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs10.existsSync(path14.join(options.repoRoot, candidate)));
3702
3824
  if (!match) {
3703
3825
  into.unmatched.push(file.path);
3704
3826
  continue;
@@ -3923,7 +4045,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
3923
4045
  commit: checkout.commit,
3924
4046
  branch,
3925
4047
  suite: args.suite,
3926
- reports: reports.map((report) => path14.relative(cwd, report) || report),
4048
+ reports: reports.map((report) => path15.relative(cwd, report) || report),
3927
4049
  files: files.length,
3928
4050
  unmatched: resolved.unmatched.length,
3929
4051
  outOfScope: resolved.outOfScope,
@@ -3995,7 +4117,7 @@ ${USAGE4}`);
3995
4117
  var import_node_child_process6 = require("child_process");
3996
4118
  var fs11 = __toESM(require("fs"));
3997
4119
  var os2 = __toESM(require("os"));
3998
- var path16 = __toESM(require("path"));
4120
+ var path17 = __toESM(require("path"));
3999
4121
 
4000
4122
  // ../core/src/error-parse.ts
4001
4123
  var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
@@ -4197,52 +4319,10 @@ function withinBudget(startedAt, now, budgetMs) {
4197
4319
  }
4198
4320
 
4199
4321
  // src/internal/support/connection.ts
4200
- var fs10 = __toESM(require("fs"));
4201
- var path15 = __toESM(require("path"));
4202
-
4203
- // ../core/src/dotenv.ts
4204
- function parseDotEnv(text) {
4205
- const out = {};
4206
- for (const raw of text.split(/\r?\n/)) {
4207
- const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
4208
- if (!m) continue;
4209
- let value = m[2];
4210
- const quote3 = value[0];
4211
- if ((quote3 === '"' || quote3 === "'") && value.length > 1 && value.endsWith(quote3)) {
4212
- value = value.slice(1, -1);
4213
- if (quote3 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
4214
- } else {
4215
- value = value.replace(/\s+#.*$/, "");
4216
- }
4217
- out[m[1]] = value;
4218
- }
4219
- return out;
4220
- }
4221
- function resolvePiwiConnection(sources) {
4222
- const own = {
4223
- serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
4224
- apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
4225
- };
4226
- const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
4227
- const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
4228
- const desktop = sources.desktop ?? null;
4229
- const trim = (url) => url.replace(/\/+$/, "");
4230
- const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
4231
- const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
4232
- if (!serverUrl) return null;
4233
- const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
4234
- return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
4235
- }
4236
-
4237
- // src/internal/support/connection.ts
4322
+ var path16 = __toESM(require("path"));
4238
4323
  function resolveCliConnection(flags, env, dirs) {
4239
4324
  let dotEnv = {};
4240
- for (const dir of dirs) {
4241
- try {
4242
- dotEnv = { ...parseDotEnv(fs10.readFileSync(path15.join(dir, ".env"), "utf-8")), ...dotEnv };
4243
- } catch {
4244
- }
4245
- }
4325
+ for (const dir of dirs) dotEnv = { ...readDotEnv(path16.join(dir, ".env")), ...dotEnv };
4246
4326
  return resolvePiwiConnection({
4247
4327
  flags,
4248
4328
  env,
@@ -4440,7 +4520,7 @@ function playwrightRunner(dir, log) {
4440
4520
  return {
4441
4521
  now: () => Date.now(),
4442
4522
  async runBatch(plan, arm, repeat, batch) {
4443
- const planFile = path16.join(dir, `${arm.id}.plan.json`);
4523
+ const planFile = path17.join(dir, `${arm.id}.plan.json`);
4444
4524
  if (!fs11.existsSync(planFile)) {
4445
4525
  fs11.writeFileSync(
4446
4526
  planFile,
@@ -4453,7 +4533,7 @@ function playwrightRunner(dir, log) {
4453
4533
  })
4454
4534
  );
4455
4535
  }
4456
- const resultsFile = path16.join(dir, `${arm.id}.${batch}.jsonl`);
4536
+ const resultsFile = path17.join(dir, `${arm.id}.${batch}.jsonl`);
4457
4537
  fs11.writeFileSync(resultsFile, "");
4458
4538
  const env = {
4459
4539
  ...process.env,
@@ -4732,8 +4812,8 @@ async function runFlake(argv, runner) {
4732
4812
  if (!args.json) {
4733
4813
  printHeader(plan, commit, estimate, args.budgetMs);
4734
4814
  }
4735
- const dir = fs11.mkdtempSync(path16.join(os2.tmpdir(), "piwi-flake-"));
4736
- const log = path16.join(dir, "playwright.log");
4815
+ const dir = fs11.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
4816
+ const log = path17.join(dir, "playwright.log");
4737
4817
  let session;
4738
4818
  try {
4739
4819
  session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
@@ -4924,9 +5004,9 @@ function readCount(argv, name) {
4924
5004
  if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
4925
5005
  return Math.floor(n);
4926
5006
  }
4927
- function readRunIdFromFile(path24) {
5007
+ function readRunIdFromFile(path25) {
4928
5008
  try {
4929
- const parsed = JSON.parse(fs12.readFileSync(path24, "utf-8"));
5009
+ const parsed = JSON.parse(fs12.readFileSync(path25, "utf-8"));
4930
5010
  const runId = Number(parsed.runId);
4931
5011
  return Number.isFinite(runId) && runId > 0 ? runId : null;
4932
5012
  } catch {
@@ -5018,7 +5098,7 @@ async function runGate(argv, env = process.env) {
5018
5098
 
5019
5099
  // src/cli/init.ts
5020
5100
  var fs15 = __toESM(require("fs"));
5021
- var path19 = __toESM(require("path"));
5101
+ var path20 = __toESM(require("path"));
5022
5102
 
5023
5103
  // src/cli/report.ts
5024
5104
  var STATUS_MARK = {
@@ -5036,7 +5116,7 @@ function formatStep(result) {
5036
5116
 
5037
5117
  // src/cli/detect.ts
5038
5118
  var fs13 = __toESM(require("fs"));
5039
- var path17 = __toESM(require("path"));
5119
+ var path18 = __toESM(require("path"));
5040
5120
  var CONFIG_NAMES = [
5041
5121
  "playwright.config.ts",
5042
5122
  "playwright.config.mts",
@@ -5054,7 +5134,7 @@ var LOCKFILES = [
5054
5134
  ];
5055
5135
  var REPORTER_PACKAGE = "@piwitests/reporter";
5056
5136
  function readPackageJson(root) {
5057
- const pkgPath = path17.join(root, "package.json");
5137
+ const pkgPath = path18.join(root, "package.json");
5058
5138
  try {
5059
5139
  return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
5060
5140
  } catch {
@@ -5065,13 +5145,13 @@ function detectPackageManager(root, pkg) {
5065
5145
  const declared = pkg?.packageManager?.split("@")[0];
5066
5146
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
5067
5147
  for (const [file, manager] of LOCKFILES) {
5068
- if (fs13.existsSync(path17.join(root, file))) return manager;
5148
+ if (fs13.existsSync(path18.join(root, file))) return manager;
5069
5149
  }
5070
5150
  return "npm";
5071
5151
  }
5072
5152
  function findConfig(root) {
5073
5153
  for (const name of CONFIG_NAMES) {
5074
- const candidate = path17.join(root, name);
5154
+ const candidate = path18.join(root, name);
5075
5155
  if (fs13.existsSync(candidate)) return candidate;
5076
5156
  }
5077
5157
  return null;
@@ -5082,14 +5162,14 @@ function langOf(configPath) {
5082
5162
  }
5083
5163
  function suggestProjectName(root, pkg) {
5084
5164
  const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
5085
- return fromPkg || path17.basename(root) || "default-project";
5165
+ return fromPkg || path18.basename(root) || "default-project";
5086
5166
  }
5087
5167
  function hasReporter(pkg) {
5088
5168
  if (!pkg) return false;
5089
5169
  return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
5090
5170
  }
5091
5171
  function detectProject(root) {
5092
- const absRoot = path17.resolve(root);
5172
+ const absRoot = path18.resolve(root);
5093
5173
  const { pkg, pkgPath } = readPackageJson(absRoot);
5094
5174
  const configPath = findConfig(absRoot);
5095
5175
  return {
@@ -5118,7 +5198,10 @@ function installCommand(manager) {
5118
5198
 
5119
5199
  // src/cli/configure.ts
5120
5200
  var REPORTER_IMPORT = "import { wrapConfig } from '@piwitests/reporter'";
5121
- var MANUAL_HINT = "Could not find `export default defineConfig(...)`. Wrap your exported config by hand: import { wrapConfig } from '@piwitests/reporter', then export default wrapConfig(defineConfig({ ... }), { serverUrl, projectName }).";
5201
+ function manualHint(opts) {
5202
+ const options = optionsLiteral(opts).replace(/\s+/g, " ");
5203
+ return `Could not find \`export default defineConfig(...)\`. Wrap your exported config by hand: import { wrapConfig } from '@piwitests/reporter', then export default wrapConfig(defineConfig({ ... }), ${options}).`;
5204
+ }
5122
5205
  var quote2 = quoteJsString;
5123
5206
  function matchClosingParen(src, openIndex) {
5124
5207
  let depth = 0;
@@ -5157,6 +5240,7 @@ function optionsLiteral(opts) {
5157
5240
  const lines = [];
5158
5241
  if (opts.serverUrl) lines.push(` serverUrl: ${quote2(opts.serverUrl)},`);
5159
5242
  lines.push(` projectName: ${quote2(opts.projectName)},`);
5243
+ if (opts.desktopApp) lines.push(` desktopApp: ${quote2(opts.desktopApp)},`);
5160
5244
  return `{
5161
5245
  ${lines.join("\n")}
5162
5246
  }`;
@@ -5180,11 +5264,11 @@ function wrapPlaywrightConfig(source, opts) {
5180
5264
  return { text: source, status: "already", detail: "Piwi reporter is already wired into the config" };
5181
5265
  }
5182
5266
  const match = /export\s+default\s+defineConfig\s*\(/.exec(source);
5183
- if (!match) return { text: source, status: "manual", detail: MANUAL_HINT };
5267
+ if (!match) return { text: source, status: "manual", detail: manualHint(opts) };
5184
5268
  const openParen = match.index + match[0].length - 1;
5185
5269
  const closeParen = matchClosingParen(source, openParen);
5186
5270
  const callStart = source.indexOf("defineConfig", match.index);
5187
- if (closeParen === -1 || callStart === -1) return { text: source, status: "manual", detail: MANUAL_HINT };
5271
+ if (closeParen === -1 || callStart === -1) return { text: source, status: "manual", detail: manualHint(opts) };
5188
5272
  const callExpr = source.slice(callStart, closeParen + 1);
5189
5273
  let before = withReporterImport(source.slice(0, match.index));
5190
5274
  const after = source.slice(closeParen + 1);
@@ -5230,7 +5314,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
5230
5314
 
5231
5315
  // src/cli/skills.ts
5232
5316
  var fs14 = __toESM(require("fs"));
5233
- var path18 = __toESM(require("path"));
5317
+ var path19 = __toESM(require("path"));
5234
5318
 
5235
5319
  // ../core/src/skill-stamp.ts
5236
5320
  var SKILL_VERSION_KEY = "piwi-version";
@@ -5293,16 +5377,16 @@ var WORKFLOW_SKILLS = [
5293
5377
  "fix-a-reported-bug"
5294
5378
  ];
5295
5379
  var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
5296
- var DEFAULT_SKILLS_DIR = path18.join(".claude", "skills");
5380
+ var DEFAULT_SKILLS_DIR = path19.join(".claude", "skills");
5297
5381
  function findTemplatesDir(fromDir) {
5298
5382
  let dir = fromDir;
5299
5383
  for (let i = 0; i < 6; i++) {
5300
- if (fs14.existsSync(path18.join(dir, "templates", "skills"))) return path18.join(dir, "templates");
5301
- const parent = path18.dirname(dir);
5384
+ if (fs14.existsSync(path19.join(dir, "templates", "skills"))) return path19.join(dir, "templates");
5385
+ const parent = path19.dirname(dir);
5302
5386
  if (parent === dir) break;
5303
5387
  dir = parent;
5304
5388
  }
5305
- return path18.resolve(fromDir, "..", "..", "templates");
5389
+ return path19.resolve(fromDir, "..", "..", "templates");
5306
5390
  }
5307
5391
  function readFrontMatter(markdown) {
5308
5392
  const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
@@ -5316,14 +5400,14 @@ function readFrontMatter(markdown) {
5316
5400
  }
5317
5401
  function readTemplatesVersion(templatesDir) {
5318
5402
  try {
5319
- const pkg = JSON.parse(fs14.readFileSync(path18.join(templatesDir, "..", "package.json"), "utf-8"));
5403
+ const pkg = JSON.parse(fs14.readFileSync(path19.join(templatesDir, "..", "package.json"), "utf-8"));
5320
5404
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
5321
5405
  } catch {
5322
5406
  return "0.0.0";
5323
5407
  }
5324
5408
  }
5325
5409
  function templatePath(templatesDir, slug) {
5326
- return path18.join(templatesDir, "skills", slug, "SKILL.md");
5410
+ return path19.join(templatesDir, "skills", slug, "SKILL.md");
5327
5411
  }
5328
5412
  function listSkills(templatesDir, slugs = ALL_SKILLS) {
5329
5413
  const infos = [];
@@ -5340,8 +5424,8 @@ function installSkills(opts) {
5340
5424
  for (const slug of opts.slugs) {
5341
5425
  const step = `skill:${slug}`;
5342
5426
  const source = templatePath(opts.templatesDir, slug);
5343
- const relDest = path18.join(opts.skillsDir, slug, "SKILL.md");
5344
- const dest = path18.join(opts.root, relDest);
5427
+ const relDest = path19.join(opts.skillsDir, slug, "SKILL.md");
5428
+ const dest = path19.join(opts.root, relDest);
5345
5429
  if (!fs14.existsSync(source)) {
5346
5430
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
5347
5431
  continue;
@@ -5373,7 +5457,7 @@ function installSkills(opts) {
5373
5457
  }
5374
5458
  }
5375
5459
  if (!opts.dryRun) {
5376
- fs14.mkdirSync(path18.dirname(dest), { recursive: true });
5460
+ fs14.mkdirSync(path19.dirname(dest), { recursive: true });
5377
5461
  fs14.writeFileSync(dest, contents);
5378
5462
  }
5379
5463
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
@@ -5446,7 +5530,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
5446
5530
  }
5447
5531
  const results = installSkills({
5448
5532
  templatesDir,
5449
- root: path18.resolve(readOption5(rest, "--cwd") ?? cwd),
5533
+ root: path19.resolve(readOption5(rest, "--cwd") ?? cwd),
5450
5534
  skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
5451
5535
  slugs: requested.length ? requested : ALL_SKILLS,
5452
5536
  force: rest.includes("--force"),
@@ -5468,6 +5552,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
5468
5552
 
5469
5553
  // src/cli/init.ts
5470
5554
  var DEFAULT_SERVER_URL = "http://localhost:3000";
5555
+ var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
5471
5556
  var USAGE8 = `
5472
5557
  piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
5473
5558
 
@@ -5484,9 +5569,14 @@ What it does (each step is idempotent and safe to re-run):
5484
5569
  Options:
5485
5570
  --server-url <url> Dashboard URL to write into the config (env PIWI_DASHBOARD_URL,
5486
5571
  default ${DEFAULT_SERVER_URL})
5572
+ --no-server-url Write no URL into the config: the reporter finds the Piwi desktop
5573
+ app running on this machine, or reads PIWI_DASHBOARD_URL
5487
5574
  --project <name> Project name to report under (default: your package/folder name)
5488
- --api-key <key> API key to write into .env (env PIWI_API_KEY). Omit to keep it a
5489
- .env.example placeholder you fill in yourself.
5575
+ --desktop-app <mode> Write desktopApp into the config: "prefer" sends local runs to the
5576
+ Piwi desktop app while it runs, and CI runs to --server-url
5577
+ --api-key <key> API key to write into .env (env PIWI_API_KEY), where the reporter
5578
+ reads it. Omit to keep it a .env.example placeholder you fill in
5579
+ yourself.
5490
5580
  --cwd <path> Project root to operate on (default: current directory)
5491
5581
  --skills <list> Comma-separated skills to install, or "all" / "none"
5492
5582
  (default: ${WORKFLOW_SKILLS.join(", ")})
@@ -5517,13 +5607,15 @@ function resolveSkillSlugs(raw) {
5517
5607
  return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
5518
5608
  }
5519
5609
  function parseInitArgs(argv, env, cwd) {
5520
- const root = path19.resolve(readOption6(argv, "--cwd") ?? cwd);
5610
+ const root = path20.resolve(readOption6(argv, "--cwd") ?? cwd);
5521
5611
  const detected = detectProject(root);
5522
5612
  return {
5523
5613
  root,
5524
- serverUrl: (readOption6(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? DEFAULT_SERVER_URL).replace(/\/$/, ""),
5614
+ serverUrl: argv.includes("--no-server-url") ? "" : (readOption6(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? DEFAULT_SERVER_URL).replace(/\/$/, ""),
5525
5615
  projectName: readOption6(argv, "--project") ?? detected.suggestedProjectName,
5526
- apiKey: readOption6(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
5616
+ desktopApp: argv.includes("--desktop-app") ? readOption6(argv, "--desktop-app") ?? "" : null,
5617
+ // The desktop app needs no key, and a key with no URL keeps the reporter from finding it.
5618
+ apiKey: argv.includes("--no-server-url") ? null : readOption6(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
5527
5619
  skillsDir: readOption6(argv, "--skills-dir") ?? DEFAULT_SKILLS_DIR,
5528
5620
  skillSlugs: resolveSkillSlugs(readOption6(argv, "--skills")),
5529
5621
  install: !argv.includes("--no-install"),
@@ -5582,18 +5674,29 @@ function stepConfig(project, opts) {
5582
5674
  status: "manual",
5583
5675
  detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
5584
5676
  };
5585
- const rel = path19.relative(project.root, project.configPath) || path19.basename(project.configPath);
5586
- const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
5677
+ const rel = path20.relative(project.root, project.configPath) || path20.basename(project.configPath);
5678
+ const source = readFileOr(project.configPath, "");
5679
+ const desktopApp = opts.desktopApp ?? void 0;
5680
+ const edit = wrapPlaywrightConfig(source, {
5587
5681
  serverUrl: opts.serverUrl,
5588
- projectName: opts.projectName
5682
+ projectName: opts.projectName,
5683
+ desktopApp
5589
5684
  });
5590
5685
  if (edit.status === "updated" && !opts.dryRun) fs15.writeFileSync(project.configPath, edit.text);
5686
+ if (edit.status === "already" && desktopApp && !new RegExp(`\\bdesktopApp\\s*:\\s*['"\`]${desktopApp}['"\`]`).test(source)) {
5687
+ return {
5688
+ step,
5689
+ file: rel,
5690
+ status: "manual",
5691
+ detail: `The Piwi reporter is already wired in \u2014 add \`desktopApp: '${desktopApp}'\` to its options`
5692
+ };
5693
+ }
5591
5694
  return { step, file: rel, status: edit.status, detail: edit.detail };
5592
5695
  }
5593
5696
  function stepFixtures(project, opts) {
5594
5697
  const step = "fixtures";
5595
- const rel = path19.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5596
- const dest = path19.join(project.root, rel);
5698
+ const rel = path20.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5699
+ const dest = path20.join(project.root, rel);
5597
5700
  if (fs15.existsSync(dest)) {
5598
5701
  const current = readFileOr(dest, "");
5599
5702
  if (/piwiFixtures/.test(current))
@@ -5606,7 +5709,7 @@ function stepFixtures(project, opts) {
5606
5709
  };
5607
5710
  }
5608
5711
  if (!opts.dryRun) {
5609
- fs15.mkdirSync(path19.dirname(dest), { recursive: true });
5712
+ fs15.mkdirSync(path20.dirname(dest), { recursive: true });
5610
5713
  fs15.writeFileSync(dest, fixturesContents());
5611
5714
  }
5612
5715
  return {
@@ -5618,7 +5721,7 @@ function stepFixtures(project, opts) {
5618
5721
  }
5619
5722
  function stepEnv(project, opts) {
5620
5723
  const results = [];
5621
- const examplePath = path19.join(project.root, ".env.example");
5724
+ const examplePath = path20.join(project.root, ".env.example");
5622
5725
  const example = upsertEnvKeys(readFileOr(examplePath, ""), [
5623
5726
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
5624
5727
  ["PIWI_API_KEY", ""]
@@ -5631,11 +5734,10 @@ function stepEnv(project, opts) {
5631
5734
  detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
5632
5735
  });
5633
5736
  if (opts.apiKey) {
5634
- const envPath = path19.join(project.root, ".env");
5635
- const env = upsertEnvKeys(readFileOr(envPath, ""), [
5636
- ["PIWI_DASHBOARD_URL", opts.serverUrl],
5637
- ["PIWI_API_KEY", opts.apiKey]
5638
- ]);
5737
+ const envPath = path20.join(project.root, ".env");
5738
+ const entries = [["PIWI_API_KEY", opts.apiKey]];
5739
+ if (opts.serverUrl) entries.unshift(["PIWI_DASHBOARD_URL", opts.serverUrl]);
5740
+ const env = upsertEnvKeys(readFileOr(envPath, ""), entries);
5639
5741
  if (env.added.length && !opts.dryRun) fs15.writeFileSync(envPath, env.text);
5640
5742
  results.push({
5641
5743
  step: "env",
@@ -5643,7 +5745,7 @@ function stepEnv(project, opts) {
5643
5745
  status: env.added.length ? "updated" : "already",
5644
5746
  detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
5645
5747
  });
5646
- const gitignorePath = path19.join(project.root, ".gitignore");
5748
+ const gitignorePath = path20.join(project.root, ".gitignore");
5647
5749
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
5648
5750
  if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
5649
5751
  results.push({
@@ -5657,16 +5759,26 @@ function stepEnv(project, opts) {
5657
5759
  }
5658
5760
  function nextSteps(opts, steps) {
5659
5761
  const out = [];
5660
- if (!opts.apiKey) {
5762
+ if (!opts.apiKey && opts.serverUrl) {
5661
5763
  out.push(
5662
- "If your dashboard has authentication on, create an API key (Settings \u2192 Account \u2192 API keys), put it in .env as PIWI_API_KEY, and keep .env out of git."
5764
+ "If your dashboard has authentication on, create an API key (Settings \u2192 Account \u2192 API keys) and put it in .env as PIWI_API_KEY: the reporter reads it from the .env next to your Playwright config. Keep .env out of git; in CI, set PIWI_API_KEY as a secret."
5663
5765
  );
5664
5766
  }
5665
5767
  if (steps.some((s) => s.status === "manual")) {
5666
5768
  out.push("Finish the steps marked [manual] above \u2014 each one carries the exact change to make.");
5667
5769
  }
5668
5770
  out.push("In your specs, import `test` (and `expect`) from your fixtures file instead of `@playwright/test`.");
5669
- out.push(`Run \`npx playwright test\` \u2014 the run should appear at ${opts.serverUrl}.`);
5771
+ if (!opts.serverUrl) {
5772
+ out.push(
5773
+ "Run `npx playwright test` \u2014 the run appears in the Piwi desktop app while it is running. Elsewhere, set PIWI_DASHBOARD_URL to the dashboard the runs go to."
5774
+ );
5775
+ } else if (opts.desktopApp === "prefer") {
5776
+ out.push(
5777
+ `Run \`npx playwright test\` \u2014 the run appears in the Piwi desktop app while it is running, and at ${opts.serverUrl} on CI or with the app closed.`
5778
+ );
5779
+ } else {
5780
+ out.push(`Run \`npx playwright test\` \u2014 the run should appear at ${opts.serverUrl}.`);
5781
+ }
5670
5782
  out.push(
5671
5783
  "For a machine-readable check, set PIWI_OUTPUT_FILE=piwi-run.json and read runUrl from that file after the run."
5672
5784
  );
@@ -5678,6 +5790,10 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
5678
5790
  return 0;
5679
5791
  }
5680
5792
  const opts = parseInitArgs(argv, env, cwd);
5793
+ if (opts.desktopApp !== null && !DESKTOP_APP_MODES.includes(opts.desktopApp)) {
5794
+ console.error(`--desktop-app must be one of ${DESKTOP_APP_MODES.join(", ")} (got "${opts.desktopApp}")`);
5795
+ return 1;
5796
+ }
5681
5797
  const project = detectProject(opts.root);
5682
5798
  const steps = [];
5683
5799
  if (!opts.skillsOnly) {
@@ -5707,9 +5823,10 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
5707
5823
  project: {
5708
5824
  root: project.root,
5709
5825
  packageManager: project.packageManager,
5710
- configPath: project.configPath ? path19.relative(project.root, project.configPath) : null,
5826
+ configPath: project.configPath ? path20.relative(project.root, project.configPath) : null,
5711
5827
  projectName: opts.projectName,
5712
- serverUrl: opts.serverUrl
5828
+ serverUrl: opts.serverUrl,
5829
+ desktopApp: opts.desktopApp
5713
5830
  },
5714
5831
  steps,
5715
5832
  nextSteps: guidance
@@ -5732,1197 +5849,1280 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
5732
5849
 
5733
5850
  // src/cli/select.ts
5734
5851
  var fs17 = __toESM(require("fs"));
5735
- var path21 = __toESM(require("path"));
5852
+ var path22 = __toESM(require("path"));
5736
5853
  var import_node_child_process7 = require("child_process");
5737
5854
 
5738
- // src/cli/add-reporter.ts
5739
- var fs16 = __toESM(require("fs"));
5740
- var path20 = __toESM(require("path"));
5741
- var import_node_module2 = require("module");
5742
- var REPORTER_PACKAGE2 = "@piwitests/reporter";
5743
- var ADD_REPORTER_FLAG = "--add-reporter";
5744
- var ADD_REPORTER_MIN = { major: 1, minor: 63 };
5745
- function playwrightSupportsAddReporter(version) {
5746
- const match = version ? /^(\d+)\.(\d+)/.exec(version) : null;
5747
- if (!match) return false;
5748
- const major = Number(match[1]);
5749
- const minor = Number(match[2]);
5750
- return major > ADD_REPORTER_MIN.major || major === ADD_REPORTER_MIN.major && minor >= ADD_REPORTER_MIN.minor;
5855
+ // ../core/src/diff-anchors.ts
5856
+ var RENAME_FROM = `rename from `;
5857
+ var RENAME_TO = "rename to ";
5858
+ var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
5859
+ var INDEX_LINE = /^index ([0-9a-f]+)\.\.([0-9a-f]+)(?: |$)/i;
5860
+ function blobOf(id) {
5861
+ return /^0+$/.test(id) ? void 0 : id.toLowerCase();
5751
5862
  }
5752
- function configHasPiwiReporter(source) {
5753
- return new RegExp(REPORTER_PACKAGE2.replace(/[/\\]/g, "\\$&")).test(source);
5863
+ function stripPrefix(path25) {
5864
+ const unquoted = path25.startsWith('"') && path25.endsWith('"') ? path25.slice(1, -1) : path25;
5865
+ return unquoted.replace(/^[ab]\//, "");
5754
5866
  }
5755
- function decideAddReporter(configSource, playwrightVersion) {
5756
- if (configSource === null || configHasPiwiReporter(configSource)) return { args: [], log: null };
5757
- if (playwrightSupportsAddReporter(playwrightVersion)) {
5758
- return {
5759
- args: [ADD_REPORTER_FLAG, REPORTER_PACKAGE2],
5760
- log: `piwi run: the Playwright config has no Piwi reporter \u2014 appending ${ADD_REPORTER_FLAG} ${REPORTER_PACKAGE2}`
5761
- };
5762
- }
5763
- return {
5764
- args: [],
5765
- log: `piwi run: the Playwright config has no Piwi reporter and Playwright ${playwrightVersion ?? "unknown"} predates ${ADD_REPORTER_FLAG} (1.63) \u2014 results will not reach the dashboard`
5867
+ function parseUnifiedDiff(text) {
5868
+ const files = [];
5869
+ let file = null;
5870
+ let hunk = null;
5871
+ let oldLine = 0;
5872
+ let newLine = 0;
5873
+ let oldLeft = 0;
5874
+ let newLeft = 0;
5875
+ const current = () => {
5876
+ if (!file) {
5877
+ file = { path: "", status: "modified", hunks: [] };
5878
+ files.push(file);
5879
+ }
5880
+ return file;
5766
5881
  };
5767
- }
5768
- function resolveConfigPath(cwd, playwrightArgs2) {
5769
- for (let i = 0; i < playwrightArgs2.length; i++) {
5770
- const arg = playwrightArgs2[i];
5771
- if (arg === "--config" || arg === "-c") {
5772
- const next = playwrightArgs2[i + 1];
5773
- if (next) return path20.resolve(cwd, next);
5774
- } else if (arg.startsWith("--config=")) {
5775
- return path20.resolve(cwd, arg.slice("--config=".length));
5882
+ for (const line of text.split(/\r?\n/)) {
5883
+ if (hunk) {
5884
+ if (line.startsWith("-") && oldLeft > 0) {
5885
+ hunk.removed.push({ line: oldLine++, text: line.slice(1) });
5886
+ oldLeft--;
5887
+ } else if (line.startsWith("+") && newLeft > 0) {
5888
+ hunk.added.push({ line: newLine++, text: line.slice(1) });
5889
+ newLeft--;
5890
+ } else if ((line.startsWith(" ") || line === "") && oldLeft > 0 && newLeft > 0) {
5891
+ oldLine++;
5892
+ newLine++;
5893
+ oldLeft--;
5894
+ newLeft--;
5895
+ } else if (!line.startsWith("\\")) hunk = null;
5896
+ if (hunk && oldLeft === 0 && newLeft === 0) hunk = null;
5897
+ if (hunk || !line.startsWith("@@")) continue;
5776
5898
  }
5899
+ const header = HUNK_HEADER.exec(line);
5900
+ if (header) {
5901
+ oldLine = Number(header[1]);
5902
+ newLine = Number(header[3]);
5903
+ oldLeft = header[2] === void 0 ? 1 : Number(header[2]);
5904
+ newLeft = header[4] === void 0 ? 1 : Number(header[4]);
5905
+ hunk = { oldStart: oldLine, newStart: newLine, removed: [], added: [] };
5906
+ current().hunks.push(hunk);
5907
+ if (oldLeft === 0 && newLeft === 0) hunk = null;
5908
+ continue;
5909
+ }
5910
+ if (line.startsWith("diff --git ")) {
5911
+ const m = /^diff --git (\S+|"[^"]+") (\S+|"[^"]+")$/.exec(line);
5912
+ file = { path: m ? stripPrefix(m[2]) : "", status: "modified", hunks: [] };
5913
+ files.push(file);
5914
+ } else if (line.startsWith("--- ")) {
5915
+ const f = file && !file.hunks.length ? file : (file = null, current());
5916
+ const old = line.slice(4).trim();
5917
+ if (old === "/dev/null") f.status = "added";
5918
+ else if (!f.path) f.path = stripPrefix(old);
5919
+ } else if (line.startsWith("+++ ")) {
5920
+ const next = line.slice(4).trim();
5921
+ if (next === "/dev/null") current().status = "deleted";
5922
+ else current().path = stripPrefix(next);
5923
+ } else if (line.startsWith(RENAME_FROM)) {
5924
+ current().oldPath = line.slice(RENAME_FROM.length);
5925
+ current().status = "renamed";
5926
+ } else if (line.startsWith(RENAME_TO)) current().path = line.slice(RENAME_TO.length);
5927
+ else if (INDEX_LINE.test(line)) {
5928
+ const [, oldId, newId] = INDEX_LINE.exec(line);
5929
+ const f = current();
5930
+ const oldBlob = blobOf(oldId);
5931
+ const newBlob = blobOf(newId);
5932
+ if (oldBlob) f.oldBlob = oldBlob;
5933
+ if (newBlob) f.newBlob = newBlob;
5934
+ } else if (line.startsWith("new file mode")) current().status = "added";
5935
+ else if (line.startsWith("deleted file mode")) current().status = "deleted";
5777
5936
  }
5778
- return detectProject(cwd).configPath;
5779
- }
5780
- function readConfigSource(configPath) {
5781
- if (!configPath) return null;
5782
- try {
5783
- return fs16.readFileSync(configPath, "utf-8");
5784
- } catch {
5785
- return null;
5786
- }
5937
+ return files.filter((f) => f.path);
5787
5938
  }
5788
- function readInstalledPlaywrightVersion(cwd) {
5789
- try {
5790
- const require2 = (0, import_node_module2.createRequire)(path20.join(cwd, "noop.js"));
5791
- for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
5792
- try {
5793
- return require2(id).version ?? null;
5794
- } catch {
5795
- }
5939
+ var DEFAULT_TRANSLATION_GLOBS = ["**/{locales,locale,i18n,lang,translations}/**", "**/*.resx"];
5940
+ var ANCHOR_ATTRIBUTES = [
5941
+ "id",
5942
+ "name",
5943
+ "aria-label",
5944
+ "placeholder",
5945
+ "alt",
5946
+ "title",
5947
+ "role",
5948
+ "value",
5949
+ "label"
5950
+ ];
5951
+ var TRANSLATION_EXTENSIONS = /\.(json|ya?ml|properties|po|resx)$/i;
5952
+ var MARKUP_FILE = /\.(vue|svelte|html?|cshtml|razor|hbs|handlebars|jsx|tsx|astro|njk|liquid|erb|php)$/i;
5953
+ var MIN_LENGTH = 2;
5954
+ var MAX_LENGTH = 200;
5955
+ function expandBraces(glob) {
5956
+ const m = /\{([^{}]*)\}/.exec(glob);
5957
+ if (!m) return [glob];
5958
+ return m[1].split(",").flatMap((alt) => expandBraces(glob.slice(0, m.index) + alt + glob.slice(m.index + m[0].length)));
5959
+ }
5960
+ function pathGlobToRegExp(glob) {
5961
+ const parts = expandBraces(glob).map((g) => {
5962
+ let re = "";
5963
+ for (let i = 0; i < g.length; i++) {
5964
+ const c = g[i];
5965
+ if (c === "*") {
5966
+ if (g[i + 1] === "*") {
5967
+ i++;
5968
+ if (g[i + 1] === "/") {
5969
+ i++;
5970
+ re += "(?:.*/)?";
5971
+ } else re += ".*";
5972
+ } else re += "[^/]*";
5973
+ } else if (c === "?") re += "[^/]";
5974
+ else if ("\\^$.|+()[]{}".includes(c)) re += "\\" + c;
5975
+ else re += c;
5796
5976
  }
5797
- } catch {
5798
- }
5799
- return null;
5977
+ return re;
5978
+ });
5979
+ return new RegExp(`^(?:${parts.join("|")})$`);
5800
5980
  }
5801
- function computeAddReporterArgs(cwd, playwrightArgs2) {
5802
- const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs2));
5803
- return decideAddReporter(source, readInstalledPlaywrightVersion(cwd));
5981
+ function isTranslationFile(path25, globs = DEFAULT_TRANSLATION_GLOBS) {
5982
+ const posix = path25.replace(/\\/g, "/");
5983
+ if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
5984
+ return globs.some((g) => pathGlobToRegExp(g).test(posix));
5804
5985
  }
5805
-
5806
- // src/cli/select.ts
5807
- var EXIT_OK6 = 0;
5808
- var EXIT_ERROR7 = 2;
5809
- var USAGE9 = `
5810
- piwi select / piwi run \u2014 run a saved selection of tests
5811
-
5812
- Usage:
5813
- npx @piwitests/reporter select <key> [options] print the Playwright args
5814
- npx @piwitests/reporter run <key> [options] [-- <playwright args>]
5815
- npx @piwitests/reporter run impact --base <ref> run the tests your diff impacts
5816
-
5817
- Connection:
5818
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
5819
- --api-key <key> API key (env PIWI_API_KEY)
5820
- --project <name|id> Project (env PIWI_PROJECT_NAME)
5821
-
5822
- Selection:
5823
- --format <fmt> args (file:line, default) | grep | files | json
5824
- --budget <duration> Cap total time, e.g. 5m, 90s, 300000 (ms)
5825
- --shard <i/n> Keep only shard i of n, balanced by duration, lock-aware
5826
- --fail-fast Order the least-reliable tests first (fail-fast)
5827
- --base <ref> For "impact": the ref to diff the working tree against
5828
-
5829
- Behavior:
5830
- --strict Fail (exit 2) instead of falling back when unreachable
5831
- --pkg-runner <cmd> Package runner for the printed command (default npx)
5832
- --json Print the full resolution as JSON (select only)
5833
- -h, --help Show this help
5834
-
5835
- Exit codes: 0 ok, 1 the test run failed (run only), 2 could not resolve.
5836
- `.trim();
5837
- var IMPACT_KEY = "impact";
5838
- var VALUE_FLAGS3 = /* @__PURE__ */ new Set([
5839
- "--server-url",
5840
- "--api-key",
5841
- "--project",
5842
- "--format",
5843
- "--budget",
5844
- "--shard",
5845
- "--base",
5846
- "--pkg-runner"
5847
- ]);
5848
- function readOption7(argv, name) {
5849
- const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
5850
- if (withEquals) return withEquals.slice(name.length + 1);
5851
- const index = argv.indexOf(name);
5852
- if (index === -1) return void 0;
5853
- const value = argv[index + 1];
5854
- return value && !value.startsWith("-") ? value : void 0;
5986
+ function keepable(value) {
5987
+ const v = value.trim();
5988
+ if (v.length < MIN_LENGTH || v.length > MAX_LENGTH) return false;
5989
+ if (/^[-+]?[\d.,]+$/.test(v)) return false;
5990
+ return new RegExp("\\p{L}", "u").test(v);
5855
5991
  }
5856
- function findKey(argv) {
5857
- for (let i = 0; i < argv.length; i++) {
5858
- const tok = argv[i];
5859
- if (tok.startsWith("-")) {
5860
- if (!tok.includes("=") && VALUE_FLAGS3.has(tok)) i++;
5992
+ function readQuoted(text, start) {
5993
+ const quote3 = text[start];
5994
+ let value = "";
5995
+ for (let i = start + 1; i < text.length; i++) {
5996
+ const ch = text[i];
5997
+ if (ch === "\\" && i + 1 < text.length) {
5998
+ const next = text[i + 1];
5999
+ value += next === "n" ? "\n" : next === "t" ? " " : next;
6000
+ i++;
5861
6001
  continue;
5862
6002
  }
5863
- return tok;
6003
+ if (quote3 === "`" && ch === "$" && text[i + 1] === "{") return null;
6004
+ if (ch === quote3) return { value, end: i + 1 };
6005
+ value += ch;
5864
6006
  }
5865
- return void 0;
6007
+ return null;
5866
6008
  }
5867
- function parseDuration2(raw) {
5868
- const match = raw.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
5869
- if (!match) return null;
5870
- const value = Number(match[1]);
5871
- const unit = match[2];
5872
- const factor = unit === "h" ? 36e5 : unit === "m" ? 6e4 : unit === "s" ? 1e3 : 1;
5873
- const ms = Math.round(value * factor);
5874
- return ms > 0 ? ms : null;
6009
+ function escapeRegExp(s) {
6010
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5875
6011
  }
5876
- function parseSelectArgs(argv, env) {
5877
- const dashIndex = argv.indexOf("--");
5878
- const own = dashIndex === -1 ? argv : argv.slice(0, dashIndex);
5879
- const extra = dashIndex === -1 ? [] : argv.slice(dashIndex + 1);
5880
- const serverUrl = (readOption7(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
5881
- if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
5882
- const key = findKey(own);
5883
- if (!key) throw new Error("No selection key \u2014 usage: piwi select <key>");
5884
- const budgetRaw = readOption7(own, "--budget");
5885
- let budgetMs = null;
5886
- if (budgetRaw !== void 0) {
5887
- budgetMs = parseDuration2(budgetRaw);
5888
- if (budgetMs === null) throw new Error(`--budget expects a duration like 5m or 90s, got "${budgetRaw}"`);
5889
- }
5890
- const shard = readOption7(own, "--shard") ?? null;
5891
- if (shard !== null && !/^\d+\s*\/\s*\d+$/.test(shard)) {
5892
- throw new Error(`--shard expects an "i/n" spec like 2/4, got "${shard}"`);
6012
+ function attributeTokens(text, attributes, line, spans) {
6013
+ const tokens = [];
6014
+ const names = attributes.map(escapeRegExp).join("|");
6015
+ const re = new RegExp(
6016
+ `(?<![\\w-])(?:v-bind:|:|\\[(?:attr\\.)?)?(["']?)(${names})\\1\\]?\\s*(=|:)\\s*(\\{\\s*)?(["'\`])`,
6017
+ "g"
6018
+ );
6019
+ let m;
6020
+ while ((m = re.exec(text)) !== null) {
6021
+ const bound = m[0].startsWith(":") || m[0].startsWith("v-bind:") || m[0].startsWith("[");
6022
+ const quoteAt = m.index + m[0].length - 1;
6023
+ const outer = readQuoted(text, quoteAt);
6024
+ if (!outer) continue;
6025
+ let value = outer.value;
6026
+ if (bound) {
6027
+ const inner = /^\s*(['"`])(.*)\1\s*$/.exec(value);
6028
+ if (!inner) continue;
6029
+ value = inner[2];
6030
+ }
6031
+ spans.push({ start: m.index, end: outer.end });
6032
+ re.lastIndex = outer.end;
6033
+ if (!keepable(value) || value.includes("${")) continue;
6034
+ const attribute = m[2];
6035
+ tokens.push({ kind: "attribute", group: `attribute:${attribute}`, value, attribute, line });
5893
6036
  }
5894
- return {
5895
- serverUrl,
5896
- apiKey: readOption7(own, "--api-key") ?? env.PIWI_API_KEY ?? null,
5897
- project: readOption7(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
5898
- key,
5899
- format: readOption7(own, "--format") ?? "args",
5900
- budgetMs,
5901
- shard,
5902
- order: own.includes("--fail-fast") ? "failureLikelihood" : null,
5903
- base: readOption7(own, "--base") ?? null,
5904
- strict: own.includes("--strict"),
5905
- pkgRunner: readOption7(own, "--pkg-runner") ?? "npx",
5906
- json: own.includes("--json"),
5907
- extra
5908
- };
5909
- }
5910
- var CACHE_FILE = path21.join(".piwi", "selection-cache.json");
5911
- function cacheKey(args) {
5912
- return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
6037
+ return tokens;
5913
6038
  }
5914
- function readCache(args) {
5915
- try {
5916
- const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
5917
- return store[cacheKey(args)] ?? null;
5918
- } catch {
5919
- return null;
6039
+ var TRANSLATION_CALL = /(?:(?<![\w$.])(?:\$?t|i18n\.t|translate|formatMessage)\s*\(\s*(?:\{\s*id\s*:\s*)?(['"`])([\w.:-]+)\1|\b\w*[Ll]ocalizer\s*\[\s*"([\w.:-]+)"\s*\])/g;
6040
+ function translationRefTokens(text, line, side, lookup, spans) {
6041
+ const tokens = [];
6042
+ for (const m of text.matchAll(TRANSLATION_CALL)) {
6043
+ spans.push({ start: m.index, end: m.index + m[0].length });
6044
+ const key = m[2] ?? m[3];
6045
+ const value = lookup?.(key, side);
6046
+ if (value === void 0 || !keepable(value)) continue;
6047
+ tokens.push({ kind: "translation", group: "translation-ref", value, key, line });
5920
6048
  }
6049
+ return tokens;
5921
6050
  }
5922
- function writeCache(args, resolution) {
5923
- try {
5924
- let store = {};
5925
- try {
5926
- store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
5927
- } catch {
6051
+ function inSpans(spans, at) {
6052
+ return spans.some((s) => at >= s.start && at < s.end);
6053
+ }
6054
+ var NON_TEXT_LITERAL = /^(?:\.{0,2}\/|[a-z][\w+.-]*:\/\/|#[\w-]+$|@\/|~\/)|\.(?:m?[jt]sx?|vue|svelte|css|scss|json|png|svg|jpe?g)$/i;
6055
+ function literalTokens(text, line, spans, markup) {
6056
+ const tokens = [];
6057
+ if (/^\s*(?:import\b|export\s+\*|\/\/|\*|\/\*|#include|using\s)/.test(text)) return tokens;
6058
+ for (let i = 0; i < text.length; i++) {
6059
+ const ch = text[i];
6060
+ if (ch !== '"' && ch !== "'" && ch !== "`") continue;
6061
+ const lit = readQuoted(text, i);
6062
+ if (!lit) {
6063
+ if (ch === "'") continue;
6064
+ break;
5928
6065
  }
5929
- store[cacheKey(args)] = resolution;
5930
- fs17.mkdirSync(path21.dirname(CACHE_FILE), { recursive: true });
5931
- fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
5932
- } catch {
6066
+ const start = i;
6067
+ i = lit.end - 1;
6068
+ if (inSpans(spans, start)) continue;
6069
+ const before = text.slice(0, start);
6070
+ if (markup && /[\w:@.[\]-]=\s*$/.test(before)) continue;
6071
+ if (/\b(?:from|require|import)\s*\(?\s*$/.test(before)) continue;
6072
+ const value = lit.value;
6073
+ if (!keepable(value) || NON_TEXT_LITERAL.test(value)) continue;
6074
+ if (/^[\w-]+(?:\.[\w-]+)+$/.test(value) && !/\s/.test(value)) continue;
6075
+ tokens.push({ kind: "literal", group: "literal", value, line });
5933
6076
  }
6077
+ return tokens;
5934
6078
  }
5935
- async function resolveWithCache(args) {
5936
- try {
5937
- const projectId = await resolveProjectId(args);
5938
- const resolution = await fetchResolution(args, projectId);
5939
- writeCache(args, resolution);
5940
- return { resolution, fromCache: false };
5941
- } catch (e) {
5942
- if (args.strict) throw e;
5943
- const cached = readCache(args);
5944
- if (cached) {
5945
- console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
5946
- return { resolution: cached, fromCache: true };
5947
- }
5948
- return { error: e };
5949
- }
6079
+ var CODE_LINE = /^(?:import|export|const|let|var|function|return|if|else|for|while|switch|case|default|class|interface|type|await|async|new|throw|try|catch|public|private|protected|static|using|namespace|@\w+\s*\()\b/;
6080
+ function textSegment(raw, line) {
6081
+ const value = raw.replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
6082
+ if (!keepable(value)) return null;
6083
+ if (/[{}]|@[\w(]|<%|%>/.test(value)) return null;
6084
+ return { kind: "text", group: "text", value, line };
5950
6085
  }
5951
- function printWarnings(resolution) {
5952
- for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
6086
+ function textTokens(text, line, markup) {
6087
+ const tokens = [];
6088
+ if (!markup) return tokens;
6089
+ const trimmed = text.trim();
6090
+ if (!trimmed) return tokens;
6091
+ if (!/[<>]/.test(trimmed)) {
6092
+ if (CODE_LINE.test(trimmed) || /[;=(){}[\]"'`]|^\/\/|^\*|^\/\*|^<!--|[,:]$/.test(trimmed)) return tokens;
6093
+ const token = textSegment(trimmed, line);
6094
+ return token ? [token] : tokens;
6095
+ }
6096
+ for (const m of text.matchAll(/>([^<>]+)</g)) {
6097
+ const token = textSegment(m[1], line);
6098
+ if (token) tokens.push(token);
6099
+ }
6100
+ const lead = /^\s*([^<>]+)<\//.exec(text);
6101
+ if (lead && !/[=;(){}]/.test(lead[1])) {
6102
+ const token = textSegment(lead[1], line);
6103
+ if (token) tokens.push(token);
6104
+ }
6105
+ const tail = />([^<>]+)$/.exec(text);
6106
+ if (tail && !/[=;(){}"']/.test(tail[1])) {
6107
+ const token = textSegment(tail[1], line);
6108
+ if (token) tokens.push(token);
6109
+ }
6110
+ return tokens;
5953
6111
  }
5954
- function gitChangedFiles(base) {
5955
- const out = (0, import_node_child_process7.execFileSync)("git", ["diff", "--name-only", base], {
5956
- encoding: "utf-8",
5957
- stdio: ["ignore", "pipe", "ignore"]
5958
- });
5959
- return [
5960
- ...new Set(
5961
- out.split("\n").map((line) => line.trim()).filter(Boolean)
5962
- )
5963
- ];
6112
+ function unquoteYaml(raw) {
6113
+ const v = raw.trim();
6114
+ if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
6115
+ return v.slice(1, -1).replace(/\\"/g, '"').replace(/''/g, "'");
6116
+ }
6117
+ return v.replace(/\s+#.*$/, "");
5964
6118
  }
5965
- async function loadImpact(args, projectId) {
5966
- const files = gitChangedFiles(args.base);
5967
- return fetchImpact(args, projectId, files);
6119
+ function decodeXml2(text) {
6120
+ return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
5968
6121
  }
5969
- async function runSelectImpact(args) {
5970
- let impact;
5971
- try {
5972
- const projectId = await resolveProjectId(args);
5973
- impact = await loadImpact(args, projectId);
5974
- } catch (e) {
5975
- console.error(`piwi select: ${e.message}`);
5976
- return EXIT_ERROR7;
5977
- }
5978
- printWarnings(impact);
5979
- console.error(
5980
- `piwi select: ${impact.impact.changedFiles} changed file(s) \u2192 ${impact.estimate.count} impacted test(s)${impact.impact.widened ? " (widened to full suite)" : ""}`
5981
- );
5982
- if (args.json) {
5983
- console.log(JSON.stringify(impact, null, 2));
5984
- return EXIT_OK6;
6122
+ function parseTranslationFile(path25, content) {
6123
+ const lines = content.split(/\r?\n/);
6124
+ const out = [];
6125
+ const ext = /\.([a-z]+)$/i.exec(path25)?.[1]?.toLowerCase();
6126
+ if (ext === "json") {
6127
+ const stack = [];
6128
+ const path26 = (key) => [...stack.map((f) => f.key), key].join(".");
6129
+ lines.forEach((text, i) => {
6130
+ const t = text.trim();
6131
+ const top = stack[stack.length - 1];
6132
+ const pair = /^"((?:[^"\\]|\\.)*)"\s*:\s*(.*)$/.exec(t);
6133
+ if (pair) {
6134
+ const key = JSON.parse(`"${pair[1]}"`);
6135
+ const rest = pair[2];
6136
+ const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
6137
+ if (str) out.push({ key: path26(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6138
+ else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
6139
+ return;
6140
+ }
6141
+ if (top?.array) {
6142
+ const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
6143
+ if (str) {
6144
+ out.push({ key: path26(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6145
+ return;
6146
+ }
6147
+ if (/^[[{]\s*$/.test(t)) {
6148
+ stack.push({ key: top.next++, array: t === "[", next: 0 });
6149
+ return;
6150
+ }
6151
+ }
6152
+ if (/^[\]}]/.test(t)) stack.pop();
6153
+ });
6154
+ return out;
5985
6155
  }
5986
- console.log(impact.impact.widened ? "" : impact.materialization.args.join(" "));
5987
- return EXIT_OK6;
5988
- }
5989
- async function runSelect(argv, env = process.env) {
5990
- if (argv.includes("-h") || argv.includes("--help")) {
5991
- console.log(USAGE9);
5992
- return EXIT_OK6;
6156
+ if (ext === "yaml" || ext === "yml") {
6157
+ const stack = [];
6158
+ lines.forEach((text, i) => {
6159
+ const m = /^(\s*)(?:"([^"]+)"|'([^']+)'|([^\s:#][^:#]*?))\s*:(?:\s+(.*))?$/.exec(text);
6160
+ if (!m) return;
6161
+ const indent = m[1].length;
6162
+ const key = (m[2] ?? m[3] ?? m[4]).trim();
6163
+ while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
6164
+ const raw = m[5]?.trim();
6165
+ if (!raw || raw.startsWith("#") || raw === "|" || raw === ">") {
6166
+ stack.push({ indent, key });
6167
+ return;
6168
+ }
6169
+ out.push({ key: [...stack.map((s) => s.key), key].join("."), value: unquoteYaml(raw), line: i + 1 });
6170
+ });
6171
+ return out;
5993
6172
  }
5994
- let args;
5995
- try {
5996
- args = parseSelectArgs(argv, env);
5997
- } catch (e) {
5998
- console.error(`piwi select: ${e.message}
5999
- `);
6000
- console.error(USAGE9);
6001
- return EXIT_ERROR7;
6173
+ if (ext === "properties") {
6174
+ lines.forEach((text, i) => {
6175
+ const m = /^\s*([^#!\s][^=:]*?)\s*[=:]\s*(.*)$/.exec(text);
6176
+ if (m && !m[2].endsWith("\\")) out.push({ key: m[1], value: m[2], line: i + 1 });
6177
+ });
6178
+ return out;
6002
6179
  }
6003
- if (args.key === IMPACT_KEY) {
6004
- if (!args.base) {
6005
- console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
6006
- return EXIT_ERROR7;
6007
- }
6008
- return runSelectImpact(args);
6180
+ if (ext === "po") {
6181
+ let msgid = null;
6182
+ lines.forEach((text, i) => {
6183
+ const id = /^msgid\s+"(.*)"\s*$/.exec(text);
6184
+ if (id) {
6185
+ msgid = id[1].replace(/\\"/g, '"');
6186
+ return;
6187
+ }
6188
+ const str = /^msgstr(?:\[\d+\])?\s+"(.*)"\s*$/.exec(text);
6189
+ if (str && msgid) out.push({ key: msgid, value: str[1].replace(/\\"/g, '"'), line: i + 1 });
6190
+ });
6191
+ return out;
6009
6192
  }
6010
- let resolution;
6011
- try {
6012
- const projectId = await resolveProjectId(args);
6013
- resolution = await fetchResolution(args, projectId);
6014
- } catch (e) {
6015
- console.error(`piwi select: ${e.message}`);
6016
- return EXIT_ERROR7;
6193
+ if (ext === "resx") {
6194
+ let name = null;
6195
+ lines.forEach((text, i) => {
6196
+ const data = /<data\s+name="([^"]+)"/.exec(text);
6197
+ if (data) name = decodeXml2(data[1]);
6198
+ const value = /<value>(.*)<\/value>/.exec(text);
6199
+ if (value && name) {
6200
+ out.push({ key: name, value: decodeXml2(value[1]), line: i + 1 });
6201
+ name = null;
6202
+ }
6203
+ });
6204
+ return out;
6017
6205
  }
6018
- printWarnings(resolution);
6019
- if (resolution.estimate.count === 0) {
6020
- console.error(`piwi select: "${args.key}" resolved to 0 tests`);
6021
- return EXIT_ERROR7;
6206
+ return out;
6207
+ }
6208
+ function translationLineToken(path25, text, line) {
6209
+ const ext = /\.([a-z]+)$/i.exec(path25)?.[1]?.toLowerCase();
6210
+ let key;
6211
+ let value;
6212
+ if (ext === "json") {
6213
+ const pair = /^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(text);
6214
+ if (pair) {
6215
+ key = JSON.parse(`"${pair[1]}"`);
6216
+ value = JSON.parse(`"${pair[2]}"`);
6217
+ } else {
6218
+ const item = /^\s*"((?:[^"\\]|\\.)*)"\s*,?\s*$/.exec(text);
6219
+ if (item) value = JSON.parse(`"${item[1]}"`);
6220
+ }
6221
+ } else if (ext === "yaml" || ext === "yml") {
6222
+ const m = /^\s*(?:-\s+)?(?:"([^"]+)"|'([^']+)'|([^\s:#][^:#]*?))\s*:\s+(.+)$/.exec(text);
6223
+ if (m) {
6224
+ key = (m[1] ?? m[2] ?? m[3]).trim();
6225
+ value = unquoteYaml(m[4]);
6226
+ }
6227
+ } else if (ext === "properties") {
6228
+ const m = /^\s*([^#!\s][^=:]*?)\s*[=:]\s*(.*)$/.exec(text);
6229
+ if (m) {
6230
+ key = m[1];
6231
+ value = m[2];
6232
+ }
6233
+ } else if (ext === "po") {
6234
+ const m = /^msg(id|str(?:\[\d+\])?)\s+"(.*)"\s*$/.exec(text);
6235
+ if (m) {
6236
+ value = m[2].replace(/\\"/g, '"');
6237
+ if (m[1] === "id") key = value;
6238
+ }
6239
+ } else if (ext === "resx") {
6240
+ const m = /<value>(.*)<\/value>/.exec(text);
6241
+ if (m) value = decodeXml2(m[1]);
6022
6242
  }
6023
- if (args.json) console.log(JSON.stringify(resolution, null, 2));
6024
- else console.log(resolution.materialization.args.join(" "));
6025
- return EXIT_OK6;
6243
+ if (value === void 0 || !keepable(value)) return null;
6244
+ return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
6026
6245
  }
6027
- function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
6028
- const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
6029
- if (decision.log) console.error(decision.log);
6030
- return spawnPlaywright([...decision.args, ...playwrightArgs2], env, { runner: pkgRunner, command: "piwi run" });
6246
+ function lineTokens(path25, text, line, side, attributes, options) {
6247
+ const spans = [];
6248
+ const markup = MARKUP_FILE.test(path25);
6249
+ return [
6250
+ ...attributeTokens(text, attributes, line, spans),
6251
+ ...translationRefTokens(text, line, side, options.translations, spans),
6252
+ ...literalTokens(text, line, spans, markup),
6253
+ ...textTokens(text, line, markup)
6254
+ ];
6031
6255
  }
6032
- function shardEnv(args) {
6033
- return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
6256
+ function keysByLine(path25, side, options) {
6257
+ const content = options.readFile?.(path25, side);
6258
+ if (content === void 0) return null;
6259
+ return new Map(parseTranslationFile(path25, content).map((e) => [e.line, e.key]));
6034
6260
  }
6035
- async function runRunImpact(args, env) {
6036
- let projectId;
6037
- try {
6038
- projectId = await resolveProjectId(args);
6039
- } catch (e) {
6040
- if (args.strict) {
6041
- console.error(`piwi run: ${e.message}`);
6042
- return EXIT_ERROR7;
6043
- }
6044
- console.error(`piwi run: ${e.message} \u2014 running the full suite`);
6045
- return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
6261
+ function tokenId(t) {
6262
+ return `${t.group}\0${t.value}`;
6263
+ }
6264
+ function hunkAnchors(file, hunk, removed, added) {
6265
+ const addedLeft = /* @__PURE__ */ new Map();
6266
+ for (const t of added) addedLeft.set(tokenId(t), [...addedLeft.get(tokenId(t)) ?? [], t]);
6267
+ const gone = [];
6268
+ for (const t of removed) {
6269
+ const same = addedLeft.get(tokenId(t));
6270
+ if (same?.length) same.shift();
6271
+ else gone.push(t);
6046
6272
  }
6047
- let impact;
6048
- try {
6049
- impact = await loadImpact(args, projectId);
6050
- } catch (e) {
6051
- if (args.strict) {
6052
- console.error(`piwi run: ${e.message}`);
6053
- return EXIT_ERROR7;
6054
- }
6055
- console.error(`piwi run: ${e.message} \u2014 running the full suite`);
6056
- return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
6057
- }
6058
- printWarnings(impact);
6059
- if (impact.impact.widened) {
6060
- console.error(
6061
- `piwi run: impact widened to the full suite (${impact.impact.unmappedSourceFiles.length} changed file(s) not fully mapped to tests)`
6062
- );
6063
- return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
6064
- }
6065
- if (impact.estimate.count === 0) {
6066
- console.error(`piwi run: no tests impacted by ${impact.impact.changedFiles} changed file(s) \u2014 nothing to run`);
6067
- return EXIT_OK6;
6273
+ const fresh = [...addedLeft.values()].flat();
6274
+ const anchors = [];
6275
+ const seen = /* @__PURE__ */ new Set();
6276
+ for (const t of gone) {
6277
+ const peers = gone.filter((g) => g.group === t.group);
6278
+ const replacements = fresh.filter((a) => a.group === t.group);
6279
+ const rename = peers.length === 1 && replacements.length === 1 ? replacements[0] : void 0;
6280
+ const anchor = {
6281
+ file,
6282
+ line: rename ? rename.line : Math.max(1, hunk.newStart),
6283
+ oldLine: t.line,
6284
+ kind: t.kind,
6285
+ before: t.value
6286
+ };
6287
+ if (t.attribute) anchor.attribute = t.attribute;
6288
+ if (t.key) anchor.key = t.key;
6289
+ if (rename) anchor.after = rename.value;
6290
+ const id = `${anchor.kind}\0${anchor.attribute ?? ""}\0${anchor.key ?? ""}\0${anchor.before}\0${anchor.after ?? ""}`;
6291
+ if (seen.has(id)) continue;
6292
+ seen.add(id);
6293
+ anchors.push(anchor);
6068
6294
  }
6069
- const runEnv = {
6070
- ...env,
6071
- PIWI_SELECTION: IMPACT_KEY,
6072
- PIWI_SELECTION_VERSION: "0",
6073
- PIWI_SELECTION_HASH: impact.resolvedHash,
6074
- PIWI_SELECTION_COUNT: String(impact.estimate.count),
6075
- ...shardEnv(args)
6076
- };
6077
- console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)`);
6078
- return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
6295
+ return anchors;
6079
6296
  }
6080
- async function runRun(argv, env = process.env) {
6081
- if (argv.includes("-h") || argv.includes("--help")) {
6082
- console.log(USAGE9);
6083
- return EXIT_OK6;
6084
- }
6085
- let args;
6086
- try {
6087
- args = parseSelectArgs(argv, env);
6088
- } catch (e) {
6089
- console.error(`piwi run: ${e.message}
6090
- `);
6091
- console.error(USAGE9);
6092
- return EXIT_ERROR7;
6093
- }
6094
- if (args.format === "json") {
6095
- console.error("piwi run: --format json cannot be run; use args, grep or files");
6096
- return EXIT_ERROR7;
6097
- }
6098
- if (args.key === IMPACT_KEY) {
6099
- if (!args.base) {
6100
- console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
6101
- return EXIT_ERROR7;
6297
+ function extractDiffAnchors(files, options = {}) {
6298
+ const attributes = [.../* @__PURE__ */ new Set([...options.testIdAttributes ?? ["data-testid"], ...ANCHOR_ATTRIBUTES])];
6299
+ const globs = options.translationGlobs ?? DEFAULT_TRANSLATION_GLOBS;
6300
+ const anchors = [];
6301
+ for (const file of files) {
6302
+ if (options.isTestFile?.(file.path)) continue;
6303
+ const translation = isTranslationFile(file.path, globs);
6304
+ let oldKeys;
6305
+ let newKeys;
6306
+ for (const hunk of file.hunks) {
6307
+ let removed;
6308
+ let added;
6309
+ if (translation) {
6310
+ oldKeys ??= keysByLine(file.oldPath ?? file.path, "old", options);
6311
+ newKeys ??= keysByLine(file.path, "new", options);
6312
+ const read = (lines, keys) => lines.flatMap((l) => {
6313
+ const token = translationLineToken(file.path, l.text, l.line);
6314
+ if (!token) return [];
6315
+ const full = keys?.get(l.line);
6316
+ if (full) {
6317
+ token.key = full;
6318
+ token.group = `translation:${full}`;
6319
+ }
6320
+ return [token];
6321
+ });
6322
+ removed = read(hunk.removed, oldKeys);
6323
+ added = read(hunk.added, newKeys);
6324
+ } else {
6325
+ removed = hunk.removed.flatMap((l) => lineTokens(file.path, l.text, l.line, "old", attributes, options));
6326
+ added = hunk.added.flatMap((l) => lineTokens(file.path, l.text, l.line, "new", attributes, options));
6327
+ }
6328
+ anchors.push(...hunkAnchors(file.path, hunk, removed, added));
6102
6329
  }
6103
- return runRunImpact(args, env);
6104
6330
  }
6105
- let outcome;
6106
- try {
6107
- outcome = await resolveWithCache(args);
6108
- } catch (e) {
6109
- console.error(`piwi run: ${e.message}`);
6110
- return EXIT_ERROR7;
6111
- }
6112
- if ("error" in outcome) {
6113
- console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
6114
- return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
6115
- }
6116
- const { resolution } = outcome;
6117
- printWarnings(resolution);
6118
- if (resolution.estimate.count === 0) {
6119
- console.error(`piwi run: "${args.key}" resolved to 0 tests`);
6120
- return EXIT_ERROR7;
6121
- }
6122
- const runEnv = {
6123
- ...env,
6124
- PIWI_SELECTION: resolution.key ?? args.key,
6125
- PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
6126
- PIWI_SELECTION_HASH: resolution.resolvedHash,
6127
- PIWI_SELECTION_COUNT: String(resolution.estimate.count),
6128
- ...shardEnv(args)
6129
- };
6130
- console.error(
6131
- `piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
6132
- );
6133
- return spawnPlaywrightForRun(args.pkgRunner, [...resolution.materialization.args, ...args.extra], runEnv);
6331
+ return anchors;
6134
6332
  }
6135
6333
 
6136
- // src/cli/preflight.ts
6137
- var fs18 = __toESM(require("fs"));
6138
- var path22 = __toESM(require("path"));
6139
- var import_node_child_process8 = require("child_process");
6334
+ // ../core/src/locator-text-match.ts
6335
+ function normalizeWhiteSpace(text) {
6336
+ return text.replace(/[​­]/g, "").trim().replace(/\s+/g, " ");
6337
+ }
6338
+ function textMatcher(value, exact) {
6339
+ if (typeof value !== "string") {
6340
+ const re = value;
6341
+ return { matcher: (text) => re.test(text.full), kind: "regex" };
6342
+ }
6343
+ const needle = normalizeWhiteSpace(value);
6344
+ if (exact) return { matcher: (text) => text.normalized === needle, kind: "strict" };
6345
+ const lower = needle.toLowerCase();
6346
+ return { matcher: (text) => (text.lower ??= text.normalized.toLowerCase()).includes(lower), kind: "lax" };
6347
+ }
6348
+ function attributeMatcher(value, exact) {
6349
+ if (typeof value !== "string") return (actual) => !!actual.match(value);
6350
+ if (exact) return (actual) => actual === value;
6351
+ const lower = value.toLowerCase();
6352
+ return (actual) => actual.toLowerCase().includes(lower);
6353
+ }
6354
+ function nameMatcher(expected, exact) {
6355
+ if (typeof expected !== "string") return (name) => !!name.match(expected);
6356
+ const wanted = normalizeWhiteSpace(expected);
6357
+ if (exact) return (name) => name === wanted;
6358
+ const upper = wanted.toUpperCase();
6359
+ return (name) => name.toUpperCase().includes(upper);
6360
+ }
6361
+ function textMatches(value, exact, text) {
6362
+ return textMatcher(value, exact).matcher({ full: text, normalized: normalizeWhiteSpace(text) });
6363
+ }
6364
+ function nameMatches(value, exact, name) {
6365
+ return nameMatcher(value, exact)(normalizeWhiteSpace(name));
6366
+ }
6367
+ function attributeMatches(value, exact, actual) {
6368
+ return attributeMatcher(value, exact)(actual);
6369
+ }
6140
6370
 
6141
- // ../core/src/diff-anchors.ts
6142
- var RENAME_FROM = `rename from `;
6143
- var RENAME_TO = "rename to ";
6144
- var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
6145
- function stripPrefix(path24) {
6146
- const unquoted = path24.startsWith('"') && path24.endsWith('"') ? path24.slice(1, -1) : path24;
6147
- return unquoted.replace(/^[ab]\//, "");
6371
+ // ../core/src/locator-break.ts
6372
+ var TEXT_KINDS = /* @__PURE__ */ new Set(["text", "translation", "literal"]);
6373
+ function isText2(anchor) {
6374
+ return TEXT_KINDS.has(anchor.kind);
6148
6375
  }
6149
- function parseUnifiedDiff(text) {
6150
- const files = [];
6151
- let file = null;
6152
- let hunk = null;
6153
- let oldLine = 0;
6154
- let newLine = 0;
6155
- let oldLeft = 0;
6156
- let newLeft = 0;
6157
- const current = () => {
6158
- if (!file) {
6159
- file = { path: "", status: "modified", hunks: [] };
6160
- files.push(file);
6161
- }
6162
- return file;
6163
- };
6164
- for (const line of text.split(/\r?\n/)) {
6165
- if (hunk) {
6166
- if (line.startsWith("-") && oldLeft > 0) {
6167
- hunk.removed.push({ line: oldLine++, text: line.slice(1) });
6168
- oldLeft--;
6169
- } else if (line.startsWith("+") && newLeft > 0) {
6170
- hunk.added.push({ line: newLine++, text: line.slice(1) });
6171
- newLeft--;
6172
- } else if ((line.startsWith(" ") || line === "") && oldLeft > 0 && newLeft > 0) {
6173
- oldLine++;
6174
- newLine++;
6175
- oldLeft--;
6176
- newLeft--;
6177
- } else if (!line.startsWith("\\")) hunk = null;
6178
- if (hunk && oldLeft === 0 && newLeft === 0) hunk = null;
6179
- if (hunk || !line.startsWith("@@")) continue;
6180
- }
6181
- const header = HUNK_HEADER.exec(line);
6182
- if (header) {
6183
- oldLine = Number(header[1]);
6184
- newLine = Number(header[3]);
6185
- oldLeft = header[2] === void 0 ? 1 : Number(header[2]);
6186
- newLeft = header[4] === void 0 ? 1 : Number(header[4]);
6187
- hunk = { oldStart: oldLine, newStart: newLine, removed: [], added: [] };
6188
- current().hunks.push(hunk);
6189
- if (oldLeft === 0 && newLeft === 0) hunk = null;
6190
- continue;
6191
- }
6192
- if (line.startsWith("diff --git ")) {
6193
- const m = /^diff --git (\S+|"[^"]+") (\S+|"[^"]+")$/.exec(line);
6194
- file = { path: m ? stripPrefix(m[2]) : "", status: "modified", hunks: [] };
6195
- files.push(file);
6196
- } else if (line.startsWith("--- ")) {
6197
- const f = file && !file.hunks.length ? file : (file = null, current());
6198
- const old = line.slice(4).trim();
6199
- if (old === "/dev/null") f.status = "added";
6200
- else if (!f.path) f.path = stripPrefix(old);
6201
- } else if (line.startsWith("+++ ")) {
6202
- const next = line.slice(4).trim();
6203
- if (next === "/dev/null") current().status = "deleted";
6204
- else current().path = stripPrefix(next);
6205
- } else if (line.startsWith(RENAME_FROM)) {
6206
- current().oldPath = line.slice(RENAME_FROM.length);
6207
- current().status = "renamed";
6208
- } else if (line.startsWith(RENAME_TO)) current().path = line.slice(RENAME_TO.length);
6209
- else if (line.startsWith("new file mode")) current().status = "added";
6210
- else if (line.startsWith("deleted file mode")) current().status = "deleted";
6376
+ function hasAttribute(anchor, names) {
6377
+ return anchor.kind === "attribute" && !!anchor.attribute && names.includes(anchor.attribute);
6378
+ }
6379
+ function toRegExp(arg) {
6380
+ try {
6381
+ return new RegExp(arg.source, arg.flags.replace(/[gy]/g, ""));
6382
+ } catch {
6383
+ return null;
6211
6384
  }
6212
- return files.filter((f) => f.path);
6213
6385
  }
6214
- var DEFAULT_TRANSLATION_GLOBS = ["**/{locales,locale,i18n,lang,translations}/**", "**/*.resx"];
6215
- var ANCHOR_ATTRIBUTES = [
6216
- "id",
6217
- "name",
6218
- "aria-label",
6219
- "placeholder",
6220
- "alt",
6221
- "title",
6222
- "role",
6223
- "value",
6224
- "label"
6225
- ];
6226
- var TRANSLATION_EXTENSIONS = /\.(json|ya?ml|properties|po|resx)$/i;
6227
- var MARKUP_FILE = /\.(vue|svelte|html?|cshtml|razor|hbs|handlebars|jsx|tsx|astro|njk|liquid|erb|php)$/i;
6228
- var MIN_LENGTH = 2;
6229
- var MAX_LENGTH = 200;
6230
- function expandBraces(glob) {
6231
- const m = /\{([^{}]*)\}/.exec(glob);
6232
- if (!m) return [glob];
6233
- return m[1].split(",").flatMap((alt) => expandBraces(glob.slice(0, m.index) + alt + glob.slice(m.index + m[0].length)));
6386
+ function optionsOf(arg) {
6387
+ return new Map(arg?.type === "object" ? arg.entries : []);
6234
6388
  }
6235
- function pathGlobToRegExp(glob) {
6236
- const parts = expandBraces(glob).map((g) => {
6237
- let re = "";
6238
- for (let i = 0; i < g.length; i++) {
6239
- const c = g[i];
6240
- if (c === "*") {
6241
- if (g[i + 1] === "*") {
6242
- i++;
6243
- if (g[i + 1] === "/") {
6244
- i++;
6245
- re += "(?:.*/)?";
6246
- } else re += ".*";
6247
- } else re += "[^/]*";
6248
- } else if (c === "?") re += "[^/]";
6249
- else if ("\\^$.|+()[]{}".includes(c)) re += "\\" + c;
6250
- else re += c;
6251
- }
6252
- return re;
6389
+ function isExact(opts) {
6390
+ const exact = opts.get("exact");
6391
+ return exact?.type === "boolean" && exact.value;
6392
+ }
6393
+ function escapeRegExp2(s) {
6394
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6395
+ }
6396
+ function renameValue(value, before, after, exact) {
6397
+ const v = normalizeWhiteSpace(value);
6398
+ const b = normalizeWhiteSpace(before);
6399
+ if (exact ? v === b : v.toLowerCase() === b.toLowerCase()) return after;
6400
+ const re = new RegExp(escapeRegExp2(b).replace(/ /g, "\\s+"), exact ? "" : "i");
6401
+ if (!re.test(value)) return null;
6402
+ return value.replace(re, () => after);
6403
+ }
6404
+ function withArg(call, index, arg) {
6405
+ return { method: call.method, args: call.args.map((a, i) => i === index ? arg : a) };
6406
+ }
6407
+ function withOption(call, index, key, arg) {
6408
+ const obj = call.args[index];
6409
+ if (obj?.type !== "object") return call;
6410
+ return withArg(call, index, {
6411
+ type: "object",
6412
+ entries: obj.entries.map(([k, v]) => [k, k === key ? arg : v])
6253
6413
  });
6254
- return new RegExp(`^(?:${parts.join("|")})$`);
6255
6414
  }
6256
- function isTranslationFile(path24, globs = DEFAULT_TRANSLATION_GLOBS) {
6257
- const posix = path24.replace(/\\/g, "/");
6258
- if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
6259
- return globs.some((g) => pathGlobToRegExp(g).test(posix));
6415
+ function stringSlot(arg, exact, rule, accepts, replace) {
6416
+ if (arg?.type === "regex") {
6417
+ const re = toRegExp(arg);
6418
+ return re ? { value: re, exact, rule, accepts } : null;
6419
+ }
6420
+ if (arg?.type !== "string") return null;
6421
+ const value = arg.value;
6422
+ return {
6423
+ value,
6424
+ exact,
6425
+ rule,
6426
+ accepts,
6427
+ rewrite: (after, before) => replace({ type: "string", value: renameValue(value, before, after, exact) ?? value }),
6428
+ literal: (after, before) => [value, renameValue(value, before, after, exact) ?? value]
6429
+ };
6260
6430
  }
6261
- function keepable(value) {
6262
- const v = value.trim();
6263
- if (v.length < MIN_LENGTH || v.length > MAX_LENGTH) return false;
6264
- if (/^[-+]?[\d.,]+$/.test(v)) return false;
6265
- return new RegExp("\\p{L}", "u").test(v);
6431
+ function cssSlots(selector, testIds, replace) {
6432
+ const slots = [];
6433
+ const slot = (value, names, render, at, len) => {
6434
+ const edited = (after) => selector.slice(0, at) + render(after) + selector.slice(at + len);
6435
+ slots.push({
6436
+ value,
6437
+ exact: true,
6438
+ rule: "exact",
6439
+ accepts: (a) => hasAttribute(a, names),
6440
+ rewrite: (after) => replace(edited(after)),
6441
+ literal: (after) => [selector, edited(after)]
6442
+ });
6443
+ };
6444
+ for (const m of selector.matchAll(/#((?:[\w-]|\\.)+)/g)) {
6445
+ slot(
6446
+ m[1].replace(/\\(.)/g, "$1"),
6447
+ ["id"],
6448
+ (v) => `#${v.replace(/[^\w-]/g, (c) => `\\${c}`)}`,
6449
+ m.index,
6450
+ m[0].length
6451
+ );
6452
+ }
6453
+ for (const m of selector.matchAll(/\[\s*([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))\s*\]/g)) {
6454
+ const attr = m[1];
6455
+ const names = testIds.includes(attr) ? testIds : attr === "id" || attr === "name" ? [attr] : null;
6456
+ if (!names) continue;
6457
+ const quote3 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
6458
+ const value = m[2] ?? m[3] ?? m[4];
6459
+ slot(value, [attr], (v) => `[${attr}=${quote3}${v}${quote3}]`, m.index, m[0].length);
6460
+ }
6461
+ return slots;
6266
6462
  }
6267
- function readQuoted(text, start) {
6268
- const quote3 = text[start];
6269
- let value = "";
6270
- for (let i = start + 1; i < text.length; i++) {
6271
- const ch = text[i];
6272
- if (ch === "\\" && i + 1 < text.length) {
6273
- const next = text[i + 1];
6274
- value += next === "n" ? "\n" : next === "t" ? " " : next;
6275
- i++;
6276
- continue;
6277
- }
6278
- if (quote3 === "`" && ch === "$" && text[i + 1] === "{") return null;
6279
- if (ch === quote3) return { value, end: i + 1 };
6280
- value += ch;
6463
+ function directSlots(call, testIds) {
6464
+ const first = call.args[0];
6465
+ const opts = optionsOf(call.args[1]);
6466
+ const exact = isExact(opts);
6467
+ const slots = [];
6468
+ const argSlot = (rule, accepts, ex = exact) => stringSlot(first, ex, rule, accepts, (next) => withArg(call, 0, next));
6469
+ const hasTextSlot = (index) => {
6470
+ const hasText = optionsOf(call.args[index]).get("hasText");
6471
+ if (!hasText) return null;
6472
+ return stringSlot(hasText, false, "text", isText2, (next) => withOption(call, index, "hasText", next));
6473
+ };
6474
+ const textOr = (attribute) => (a) => a.kind === "translation" || a.kind === "literal" || hasAttribute(a, [attribute]);
6475
+ switch (call.method) {
6476
+ case "getByTestId":
6477
+ slots.push(argSlot("exact", (a) => hasAttribute(a, testIds), true));
6478
+ break;
6479
+ case "getByRole":
6480
+ slots.push(
6481
+ stringSlot(
6482
+ opts.get("name"),
6483
+ exact,
6484
+ "name",
6485
+ (a) => isText2(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
6486
+ (next) => withOption(call, 1, "name", next)
6487
+ )
6488
+ );
6489
+ break;
6490
+ case "getByText":
6491
+ slots.push(argSlot("text", isText2));
6492
+ break;
6493
+ case "getByLabel":
6494
+ slots.push(argSlot("text", (a) => isText2(a) || hasAttribute(a, ["aria-label"])));
6495
+ break;
6496
+ case "getByPlaceholder":
6497
+ slots.push(argSlot("text", textOr("placeholder")));
6498
+ break;
6499
+ case "getByAltText":
6500
+ slots.push(argSlot("text", textOr("alt")));
6501
+ break;
6502
+ case "getByTitle":
6503
+ slots.push(argSlot("text", textOr("title")));
6504
+ break;
6505
+ case "locator":
6506
+ if (first?.type === "string") {
6507
+ slots.push(...cssSlots(first.value, testIds, (next) => withArg(call, 0, { type: "string", value: next })));
6508
+ }
6509
+ slots.push(hasTextSlot(1));
6510
+ break;
6511
+ case "filter":
6512
+ slots.push(hasTextSlot(0));
6513
+ break;
6281
6514
  }
6282
- return null;
6515
+ return slots.filter((slot) => !!slot);
6283
6516
  }
6284
- function escapeRegExp(s) {
6285
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6517
+ function nestedChains(call) {
6518
+ const out = [];
6519
+ call.args.forEach((arg, index) => {
6520
+ if (arg.type === "chain")
6521
+ out.push({ chain: arg.chain, put: (c) => withArg(call, index, { type: "chain", chain: c }) });
6522
+ if (arg.type === "object") {
6523
+ for (const [key, v] of arg.entries) {
6524
+ if (v.type === "chain") {
6525
+ out.push({ chain: v.chain, put: (c) => withOption(call, index, key, { type: "chain", chain: c }) });
6526
+ }
6527
+ }
6528
+ }
6529
+ });
6530
+ return out;
6286
6531
  }
6287
- function attributeTokens(text, attributes, line, spans) {
6288
- const tokens = [];
6289
- const names = attributes.map(escapeRegExp).join("|");
6290
- const re = new RegExp(
6291
- `(?<![\\w-])(?:v-bind:|:|\\[(?:attr\\.)?)?(["']?)(${names})\\1\\]?\\s*(=|:)\\s*(\\{\\s*)?(["'\`])`,
6292
- "g"
6293
- );
6294
- let m;
6295
- while ((m = re.exec(text)) !== null) {
6296
- const bound = m[0].startsWith(":") || m[0].startsWith("v-bind:") || m[0].startsWith("[");
6297
- const quoteAt = m.index + m[0].length - 1;
6298
- const outer = readQuoted(text, quoteAt);
6299
- if (!outer) continue;
6300
- let value = outer.value;
6301
- if (bound) {
6302
- const inner = /^\s*(['"`])(.*)\1\s*$/.exec(value);
6303
- if (!inner) continue;
6304
- value = inner[2];
6532
+ function chainSlots(chain, testIds) {
6533
+ const out = [];
6534
+ chain.calls.forEach((call, i) => {
6535
+ const replaceAt = (next) => ({ calls: chain.calls.map((c, j) => j === i ? next : c) });
6536
+ for (const slot of directSlots(call, testIds)) {
6537
+ const rewrite = slot.rewrite;
6538
+ out.push({ ...slot, rewrite: rewrite && ((after, before) => replaceAt(rewrite(after, before))) });
6305
6539
  }
6306
- spans.push({ start: m.index, end: outer.end });
6307
- re.lastIndex = outer.end;
6308
- if (!keepable(value) || value.includes("${")) continue;
6309
- const attribute = m[2];
6310
- tokens.push({ kind: "attribute", group: `attribute:${attribute}`, value, attribute, line });
6311
- }
6312
- return tokens;
6540
+ for (const n of nestedChains(call)) {
6541
+ for (const slot of chainSlots(n.chain, testIds)) {
6542
+ const rewrite = slot.rewrite;
6543
+ out.push({ ...slot, rewrite: rewrite && ((after, before) => replaceAt(n.put(rewrite(after, before)))) });
6544
+ }
6545
+ }
6546
+ });
6547
+ return out;
6313
6548
  }
6314
- var TRANSLATION_CALL = /(?:(?<![\w$.])(?:\$?t|i18n\.t|translate|formatMessage)\s*\(\s*(?:\{\s*id\s*:\s*)?(['"`])([\w.:-]+)\1|\b\w*[Ll]ocalizer\s*\[\s*"([\w.:-]+)"\s*\])/g;
6315
- function translationRefTokens(text, line, side, lookup, spans) {
6316
- const tokens = [];
6317
- for (const m of text.matchAll(TRANSLATION_CALL)) {
6318
- spans.push({ start: m.index, end: m.index + m[0].length });
6319
- const key = m[2] ?? m[3];
6320
- const value = lookup?.(key, side);
6321
- if (value === void 0 || !keepable(value)) continue;
6322
- tokens.push({ kind: "translation", group: "translation-ref", value, key, line });
6549
+ function slotMatches(slot, text) {
6550
+ switch (slot.rule) {
6551
+ case "exact":
6552
+ return attributeMatches(slot.value, true, text);
6553
+ case "name":
6554
+ return nameMatches(slot.value, slot.exact, text);
6555
+ case "text":
6556
+ return textMatches(slot.value, slot.exact, text);
6323
6557
  }
6324
- return tokens;
6325
6558
  }
6326
- function inSpans(spans, at) {
6327
- return spans.some((s) => at >= s.start && at < s.end);
6559
+ var filePath = (path25) => path25.replace(/\\/g, "/").replace(/^\.\//, "");
6560
+ function sameFilePath(a, b) {
6561
+ const x = filePath(a);
6562
+ const y = filePath(b);
6563
+ return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
6328
6564
  }
6329
- var NON_TEXT_LITERAL = /^(?:\.{0,2}\/|[a-z][\w+.-]*:\/\/|#[\w-]+$|@\/|~\/)|\.(?:m?[jt]sx?|vue|svelte|css|scss|json|png|svg|jpe?g)$/i;
6330
- function literalTokens(text, line, spans, markup) {
6331
- const tokens = [];
6332
- if (/^\s*(?:import\b|export\s+\*|\/\/|\*|\/\*|#include|using\s)/.test(text)) return tokens;
6333
- for (let i = 0; i < text.length; i++) {
6334
- const ch = text[i];
6335
- if (ch !== '"' && ch !== "'" && ch !== "`") continue;
6336
- const lit = readQuoted(text, i);
6337
- if (!lit) {
6338
- if (ch === "'") continue;
6339
- break;
6565
+ function reachOfIndex(index) {
6566
+ if (!index.reach.some((r) => r.origin === "client")) return void 0;
6567
+ const filesOf = /* @__PURE__ */ new Map();
6568
+ for (const r of index.reach) {
6569
+ const file = index.files[r.file];
6570
+ if (file === void 0) continue;
6571
+ for (const t of r.tests) {
6572
+ const id = index.tests[t]?.id;
6573
+ if (id === void 0) continue;
6574
+ const files = filesOf.get(id);
6575
+ if (files) files.push(file);
6576
+ else filesOf.set(id, [file]);
6340
6577
  }
6341
- const start = i;
6342
- i = lit.end - 1;
6343
- if (inSpans(spans, start)) continue;
6344
- const before = text.slice(0, start);
6345
- if (markup && /[\w:@.[\]-]=\s*$/.test(before)) continue;
6346
- if (/\b(?:from|require|import)\s*\(?\s*$/.test(before)) continue;
6347
- const value = lit.value;
6348
- if (!keepable(value) || NON_TEXT_LITERAL.test(value)) continue;
6349
- if (/^[\w-]+(?:\.[\w-]+)+$/.test(value) && !/\s/.test(value)) continue;
6350
- tokens.push({ kind: "literal", group: "literal", value, line });
6351
6578
  }
6352
- return tokens;
6579
+ return (testId, file) => (filesOf.get(testId) ?? []).some((reached) => sameFilePath(reached, file));
6353
6580
  }
6354
- var CODE_LINE = /^(?:import|export|const|let|var|function|return|if|else|for|while|switch|case|default|class|interface|type|await|async|new|throw|try|catch|public|private|protected|static|using|namespace|@\w+\s*\()\b/;
6355
- function textSegment(raw, line) {
6356
- const value = raw.replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
6357
- if (!keepable(value)) return null;
6358
- if (/[{}]|@[\w(]|<%|%>/.test(value)) return null;
6359
- return { kind: "text", group: "text", value, line };
6581
+ function callSiteFile(callSite) {
6582
+ return callSite.replace(/:\d+(?::\d+)?$/, "");
6360
6583
  }
6361
- function textTokens(text, line, markup) {
6362
- const tokens = [];
6363
- if (!markup) return tokens;
6364
- const trimmed = text.trim();
6365
- if (!trimmed) return tokens;
6366
- if (!/[<>]/.test(trimmed)) {
6367
- if (CODE_LINE.test(trimmed) || /[;=(){}[\]"'`]|^\/\/|^\*|^\/\*|^<!--|[,:]$/.test(trimmed)) return tokens;
6368
- const token = textSegment(trimmed, line);
6369
- return token ? [token] : tokens;
6370
- }
6371
- for (const m of text.matchAll(/>([^<>]+)</g)) {
6372
- const token = textSegment(m[1], line);
6373
- if (token) tokens.push(token);
6374
- }
6375
- const lead = /^\s*([^<>]+)<\//.exec(text);
6376
- if (lead && !/[=;(){}]/.test(lead[1])) {
6377
- const token = textSegment(lead[1], line);
6378
- if (token) tokens.push(token);
6379
- }
6380
- const tail = />([^<>]+)$/.exec(text);
6381
- if (tail && !/[=;(){}"']/.test(tail[1])) {
6382
- const token = textSegment(tail[1], line);
6383
- if (token) tokens.push(token);
6384
- }
6385
- return tokens;
6584
+ function callSiteLine(callSite) {
6585
+ const m = /:(\d+)(?::\d+)?$/.exec(callSite);
6586
+ return m ? Number(m[1]) : null;
6386
6587
  }
6387
- function unquoteYaml(raw) {
6388
- const v = raw.trim();
6389
- if (v.startsWith('"') && v.endsWith('"') || v.startsWith("'") && v.endsWith("'")) {
6390
- return v.slice(1, -1).replace(/\\"/g, '"').replace(/''/g, "'");
6588
+ function locatorCallSiteFiles(index) {
6589
+ const files = /* @__PURE__ */ new Set();
6590
+ for (const entry of index.locators) {
6591
+ for (const use of entry.uses) for (const site of use.callSites) files.add(callSiteFile(site));
6391
6592
  }
6392
- return v.replace(/\s+#.*$/, "");
6393
- }
6394
- function decodeXml2(text) {
6395
- return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
6593
+ return files;
6396
6594
  }
6397
- function parseTranslationFile(path24, content) {
6398
- const lines = content.split(/\r?\n/);
6399
- const out = [];
6400
- const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
6401
- if (ext === "json") {
6402
- const stack = [];
6403
- const path25 = (key) => [...stack.map((f) => f.key), key].join(".");
6404
- lines.forEach((text, i) => {
6405
- const t = text.trim();
6406
- const top = stack[stack.length - 1];
6407
- const pair = /^"((?:[^"\\]|\\.)*)"\s*:\s*(.*)$/.exec(t);
6408
- if (pair) {
6409
- const key = JSON.parse(`"${pair[1]}"`);
6410
- const rest = pair[2];
6411
- const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
6412
- if (str) out.push({ key: path25(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6413
- else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
6414
- return;
6415
- }
6416
- if (top?.array) {
6417
- const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
6418
- if (str) {
6419
- out.push({ key: path25(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6420
- return;
6421
- }
6422
- if (/^[[{]\s*$/.test(t)) {
6423
- stack.push({ key: top.next++, array: t === "[", next: 0 });
6424
- return;
6425
- }
6426
- }
6427
- if (/^[\]}]/.test(t)) stack.pop();
6428
- });
6429
- return out;
6430
- }
6431
- if (ext === "yaml" || ext === "yml") {
6432
- const stack = [];
6433
- lines.forEach((text, i) => {
6434
- const m = /^(\s*)(?:"([^"]+)"|'([^']+)'|([^\s:#][^:#]*?))\s*:(?:\s+(.*))?$/.exec(text);
6435
- if (!m) return;
6436
- const indent = m[1].length;
6437
- const key = (m[2] ?? m[3] ?? m[4]).trim();
6438
- while (stack.length && stack[stack.length - 1].indent >= indent) stack.pop();
6439
- const raw = m[5]?.trim();
6440
- if (!raw || raw.startsWith("#") || raw === "|" || raw === ">") {
6441
- stack.push({ indent, key });
6442
- return;
6443
- }
6444
- out.push({ key: [...stack.map((s) => s.key), key].join("."), value: unquoteYaml(raw), line: i + 1 });
6445
- });
6446
- return out;
6447
- }
6448
- if (ext === "properties") {
6449
- lines.forEach((text, i) => {
6450
- const m = /^\s*([^#!\s][^=:]*?)\s*[=:]\s*(.*)$/.exec(text);
6451
- if (m && !m[2].endsWith("\\")) out.push({ key: m[1], value: m[2], line: i + 1 });
6452
- });
6453
- return out;
6454
- }
6455
- if (ext === "po") {
6456
- let msgid = null;
6457
- lines.forEach((text, i) => {
6458
- const id = /^msgid\s+"(.*)"\s*$/.exec(text);
6459
- if (id) {
6460
- msgid = id[1].replace(/\\"/g, '"');
6461
- return;
6462
- }
6463
- const str = /^msgstr(?:\[\d+\])?\s+"(.*)"\s*$/.exec(text);
6464
- if (str && msgid) out.push({ key: msgid, value: str[1].replace(/\\"/g, '"'), line: i + 1 });
6465
- });
6466
- return out;
6467
- }
6468
- if (ext === "resx") {
6469
- let name = null;
6470
- lines.forEach((text, i) => {
6471
- const data = /<data\s+name="([^"]+)"/.exec(text);
6472
- if (data) name = decodeXml2(data[1]);
6473
- const value = /<value>(.*)<\/value>/.exec(text);
6474
- if (value && name) {
6475
- out.push({ key: name, value: decodeXml2(value[1]), line: i + 1 });
6476
- name = null;
6595
+ var CONFIDENCE_RANK = { likely: 0, possible: 1 };
6596
+ function predictLocatorBreaks(anchors, index, options = {}) {
6597
+ const testIds = options.testIdAttributes ?? index.testIdAttributes ?? ["data-testid"];
6598
+ const siteFiles = [...locatorCallSiteFiles(index)];
6599
+ const live = anchors.filter((a) => !siteFiles.some((f) => sameFilePath(f, a.file)));
6600
+ if (!live.length) return [];
6601
+ const breaks = [];
6602
+ for (const entry of index.locators) {
6603
+ const chain = tryParseLocatorChain(entry.locator);
6604
+ if (!chain) continue;
6605
+ const slots = chainSlots(chain, testIds);
6606
+ if (!slots.length) continue;
6607
+ for (const anchor of live) {
6608
+ const hit = slots.filter(
6609
+ (slot) => slot.accepts(anchor) && slotMatches(slot, anchor.before) && (anchor.after === void 0 || !slotMatches(slot, anchor.after))
6610
+ );
6611
+ if (!hit.length) continue;
6612
+ const tests = [...new Set(entry.uses.map((u) => u.test))].map((t) => index.tests[t]).filter((t) => !!t);
6613
+ const found = {
6614
+ anchor,
6615
+ locator: entry.locator,
6616
+ entry,
6617
+ uses: entry.uses,
6618
+ tests,
6619
+ confidence: confidenceOf(anchor, tests, options.reach)
6620
+ };
6621
+ if (anchor.after !== void 0 && hit.every((slot) => slot.rewrite)) {
6622
+ let rewritten = chain;
6623
+ const replacements = [];
6624
+ for (const slot of hit) {
6625
+ const again = chainSlots(rewritten, testIds).find(
6626
+ (s) => s.rule === slot.rule && String(s.value) === String(slot.value) && s.rewrite
6627
+ );
6628
+ if (!again?.rewrite) continue;
6629
+ rewritten = again.rewrite(anchor.after, anchor.before);
6630
+ if (slot.literal) replacements.push(slot.literal(anchor.after, anchor.before));
6631
+ }
6632
+ const rendered = renderLocatorChain(rewritten);
6633
+ if (rendered !== entry.locator) {
6634
+ found.rewrite = rendered;
6635
+ found.replacements = replacements.filter(([a, b]) => a !== b);
6636
+ }
6477
6637
  }
6478
- });
6479
- return out;
6480
- }
6481
- return out;
6482
- }
6483
- function translationLineToken(path24, text, line) {
6484
- const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
6485
- let key;
6486
- let value;
6487
- if (ext === "json") {
6488
- const pair = /^\s*"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(text);
6489
- if (pair) {
6490
- key = JSON.parse(`"${pair[1]}"`);
6491
- value = JSON.parse(`"${pair[2]}"`);
6492
- } else {
6493
- const item = /^\s*"((?:[^"\\]|\\.)*)"\s*,?\s*$/.exec(text);
6494
- if (item) value = JSON.parse(`"${item[1]}"`);
6495
- }
6496
- } else if (ext === "yaml" || ext === "yml") {
6497
- const m = /^\s*(?:-\s+)?(?:"([^"]+)"|'([^']+)'|([^\s:#][^:#]*?))\s*:\s+(.+)$/.exec(text);
6498
- if (m) {
6499
- key = (m[1] ?? m[2] ?? m[3]).trim();
6500
- value = unquoteYaml(m[4]);
6501
- }
6502
- } else if (ext === "properties") {
6503
- const m = /^\s*([^#!\s][^=:]*?)\s*[=:]\s*(.*)$/.exec(text);
6504
- if (m) {
6505
- key = m[1];
6506
- value = m[2];
6507
- }
6508
- } else if (ext === "po") {
6509
- const m = /^msg(id|str(?:\[\d+\])?)\s+"(.*)"\s*$/.exec(text);
6510
- if (m) {
6511
- value = m[2].replace(/\\"/g, '"');
6512
- if (m[1] === "id") key = value;
6638
+ breaks.push(found);
6513
6639
  }
6514
- } else if (ext === "resx") {
6515
- const m = /<value>(.*)<\/value>/.exec(text);
6516
- if (m) value = decodeXml2(m[1]);
6517
6640
  }
6518
- if (value === void 0 || !keepable(value)) return null;
6519
- return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
6641
+ return breaks.sort(
6642
+ (a, b) => CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence] || b.tests.length - a.tests.length
6643
+ );
6520
6644
  }
6521
- function lineTokens(path24, text, line, side, attributes, options) {
6522
- const spans = [];
6523
- const markup = MARKUP_FILE.test(path24);
6524
- return [
6525
- ...attributeTokens(text, attributes, line, spans),
6526
- ...translationRefTokens(text, line, side, options.translations, spans),
6527
- ...literalTokens(text, line, spans, markup),
6528
- ...textTokens(text, line, markup)
6529
- ];
6645
+ function confidenceOf(anchor, tests, reach) {
6646
+ if (anchor.kind === "literal") return "possible";
6647
+ if (!reach) return "likely";
6648
+ if (anchor.kind === "translation" && /\.(?:json|ya?ml|properties|po|resx)$/i.test(anchor.file)) return "likely";
6649
+ return tests.some((t) => reach(t.id, anchor.file)) ? "likely" : "possible";
6530
6650
  }
6531
- function keysByLine(path24, side, options) {
6532
- const content = options.readFile?.(path24, side);
6533
- if (content === void 0) return null;
6534
- return new Map(parseTranslationFile(path24, content).map((e) => [e.line, e.key]));
6651
+
6652
+ // ../core/src/suspect-lines.ts
6653
+ function deletionPoints(hunk) {
6654
+ const added = hunk.added.map(({ line }) => line).sort((a, b) => a - b);
6655
+ const addedLines = new Set(added);
6656
+ const points = [];
6657
+ let previous = -1;
6658
+ hunk.removed.forEach(({ line }, removedBefore) => {
6659
+ const opensRun = line !== previous + 1;
6660
+ previous = line;
6661
+ if (!opensRun) return;
6662
+ let before;
6663
+ if (line === hunk.oldStart && added.length === 0) before = line === 1 ? 0 : hunk.newStart;
6664
+ else {
6665
+ before = hunk.newStart + (line - 1 - hunk.oldStart) - removedBefore;
6666
+ for (const addedLine of added) {
6667
+ if (addedLine > before) break;
6668
+ before++;
6669
+ }
6670
+ }
6671
+ if (!addedLines.has(before + 1)) points.push(before);
6672
+ });
6673
+ return points;
6535
6674
  }
6536
- function tokenId(t) {
6537
- return `${t.group}\0${t.value}`;
6675
+ function hunkHeadLines(hunk) {
6676
+ const lines = hunk.added.map(({ line }) => [line, line]);
6677
+ for (const before of deletionPoints(hunk)) lines.push(before >= 1 ? [before, before + 1] : [1, 1]);
6678
+ return normalizeLineRanges(lines);
6538
6679
  }
6539
- function hunkAnchors(file, hunk, removed, added) {
6540
- const addedLeft = /* @__PURE__ */ new Map();
6541
- for (const t of added) addedLeft.set(tokenId(t), [...addedLeft.get(tokenId(t)) ?? [], t]);
6542
- const gone = [];
6543
- for (const t of removed) {
6544
- const same = addedLeft.get(tokenId(t));
6545
- if (same?.length) same.shift();
6546
- else gone.push(t);
6547
- }
6548
- const fresh = [...addedLeft.values()].flat();
6549
- const anchors = [];
6550
- const seen = /* @__PURE__ */ new Set();
6551
- for (const t of gone) {
6552
- const peers = gone.filter((g) => g.group === t.group);
6553
- const replacements = fresh.filter((a) => a.group === t.group);
6554
- const rename = peers.length === 1 && replacements.length === 1 ? replacements[0] : void 0;
6555
- const anchor = {
6556
- file,
6557
- line: rename ? rename.line : Math.max(1, hunk.newStart),
6558
- oldLine: t.line,
6559
- kind: t.kind,
6560
- before: t.value
6561
- };
6562
- if (t.attribute) anchor.attribute = t.attribute;
6563
- if (t.key) anchor.key = t.key;
6564
- if (rename) anchor.after = rename.value;
6565
- const id = `${anchor.kind}\0${anchor.attribute ?? ""}\0${anchor.key ?? ""}\0${anchor.before}\0${anchor.after ?? ""}`;
6566
- if (seen.has(id)) continue;
6567
- seen.add(id);
6568
- anchors.push(anchor);
6569
- }
6570
- return anchors;
6680
+ function hunkBaseLines(hunk) {
6681
+ return hunkHeadLines({ oldStart: hunk.newStart, newStart: hunk.oldStart, removed: hunk.added, added: hunk.removed });
6571
6682
  }
6572
- function extractDiffAnchors(files, options = {}) {
6573
- const attributes = [.../* @__PURE__ */ new Set([...options.testIdAttributes ?? ["data-testid"], ...ANCHOR_ATTRIBUTES])];
6574
- const globs = options.translationGlobs ?? DEFAULT_TRANSLATION_GLOBS;
6575
- const anchors = [];
6683
+ function diffImpactInput(files) {
6684
+ const changedFiles = /* @__PURE__ */ new Set();
6685
+ const changes = [];
6576
6686
  for (const file of files) {
6577
- if (options.isTestFile?.(file.path)) continue;
6578
- const translation = isTranslationFile(file.path, globs);
6579
- let oldKeys;
6580
- let newKeys;
6581
- for (const hunk of file.hunks) {
6582
- let removed;
6583
- let added;
6584
- if (translation) {
6585
- oldKeys ??= keysByLine(file.oldPath ?? file.path, "old", options);
6586
- newKeys ??= keysByLine(file.path, "new", options);
6587
- const read = (lines, keys) => lines.flatMap((l) => {
6588
- const token = translationLineToken(file.path, l.text, l.line);
6589
- if (!token) return [];
6590
- const full = keys?.get(l.line);
6591
- if (full) {
6592
- token.key = full;
6593
- token.group = `translation:${full}`;
6594
- }
6595
- return [token];
6596
- });
6597
- removed = read(hunk.removed, oldKeys);
6598
- added = read(hunk.added, newKeys);
6599
- } else {
6600
- removed = hunk.removed.flatMap((l) => lineTokens(file.path, l.text, l.line, "old", attributes, options));
6601
- added = hunk.added.flatMap((l) => lineTokens(file.path, l.text, l.line, "new", attributes, options));
6602
- }
6603
- anchors.push(...hunkAnchors(file.path, hunk, removed, added));
6604
- }
6687
+ changedFiles.add(file.path);
6688
+ if (file.oldPath) changedFiles.add(file.oldPath);
6689
+ if (file.status !== "modified" || !file.oldBlob) continue;
6690
+ const lines = normalizeLineRanges(file.hunks.flatMap(hunkBaseLines));
6691
+ if (lines.length > 0) changes.push({ filePath: file.path, blob: file.oldBlob, lines });
6605
6692
  }
6606
- return anchors;
6693
+ return { changedFiles: [...changedFiles], changes };
6607
6694
  }
6608
6695
 
6609
- // ../core/src/locator-text-match.ts
6610
- function normalizeWhiteSpace(text) {
6611
- return text.replace(/[​­]/g, "").trim().replace(/\s+/g, " ");
6696
+ // src/cli/add-reporter.ts
6697
+ var fs16 = __toESM(require("fs"));
6698
+ var path21 = __toESM(require("path"));
6699
+ var import_node_module2 = require("module");
6700
+ var REPORTER_PACKAGE2 = "@piwitests/reporter";
6701
+ var ADD_REPORTER_FLAG = "--add-reporter";
6702
+ var ADD_REPORTER_MIN = { major: 1, minor: 63 };
6703
+ function playwrightSupportsAddReporter(version) {
6704
+ const match = version ? /^(\d+)\.(\d+)/.exec(version) : null;
6705
+ if (!match) return false;
6706
+ const major = Number(match[1]);
6707
+ const minor = Number(match[2]);
6708
+ return major > ADD_REPORTER_MIN.major || major === ADD_REPORTER_MIN.major && minor >= ADD_REPORTER_MIN.minor;
6612
6709
  }
6613
- function textMatcher(value, exact) {
6614
- if (typeof value !== "string") {
6615
- const re = value;
6616
- return { matcher: (text) => re.test(text.full), kind: "regex" };
6617
- }
6618
- const needle = normalizeWhiteSpace(value);
6619
- if (exact) return { matcher: (text) => text.normalized === needle, kind: "strict" };
6620
- const lower = needle.toLowerCase();
6621
- return { matcher: (text) => (text.lower ??= text.normalized.toLowerCase()).includes(lower), kind: "lax" };
6710
+ function configHasPiwiReporter(source) {
6711
+ return new RegExp(REPORTER_PACKAGE2.replace(/[/\\]/g, "\\$&")).test(source);
6622
6712
  }
6623
- function attributeMatcher(value, exact) {
6624
- if (typeof value !== "string") return (actual) => !!actual.match(value);
6625
- if (exact) return (actual) => actual === value;
6626
- const lower = value.toLowerCase();
6627
- return (actual) => actual.toLowerCase().includes(lower);
6713
+ function decideAddReporter(configSource, playwrightVersion) {
6714
+ if (configSource === null || configHasPiwiReporter(configSource)) return { args: [], log: null };
6715
+ if (playwrightSupportsAddReporter(playwrightVersion)) {
6716
+ return {
6717
+ args: [ADD_REPORTER_FLAG, REPORTER_PACKAGE2],
6718
+ log: `piwi run: the Playwright config has no Piwi reporter \u2014 appending ${ADD_REPORTER_FLAG} ${REPORTER_PACKAGE2}`
6719
+ };
6720
+ }
6721
+ return {
6722
+ args: [],
6723
+ log: `piwi run: the Playwright config has no Piwi reporter and Playwright ${playwrightVersion ?? "unknown"} predates ${ADD_REPORTER_FLAG} (1.63) \u2014 results will not reach the dashboard`
6724
+ };
6628
6725
  }
6629
- function nameMatcher(expected, exact) {
6630
- if (typeof expected !== "string") return (name) => !!name.match(expected);
6631
- const wanted = normalizeWhiteSpace(expected);
6632
- if (exact) return (name) => name === wanted;
6633
- const upper = wanted.toUpperCase();
6634
- return (name) => name.toUpperCase().includes(upper);
6726
+ function resolveConfigPath(cwd, playwrightArgs2) {
6727
+ for (let i = 0; i < playwrightArgs2.length; i++) {
6728
+ const arg = playwrightArgs2[i];
6729
+ if (arg === "--config" || arg === "-c") {
6730
+ const next = playwrightArgs2[i + 1];
6731
+ if (next) return path21.resolve(cwd, next);
6732
+ } else if (arg.startsWith("--config=")) {
6733
+ return path21.resolve(cwd, arg.slice("--config=".length));
6734
+ }
6735
+ }
6736
+ return detectProject(cwd).configPath;
6635
6737
  }
6636
- function textMatches(value, exact, text) {
6637
- return textMatcher(value, exact).matcher({ full: text, normalized: normalizeWhiteSpace(text) });
6738
+ function readConfigSource(configPath) {
6739
+ if (!configPath) return null;
6740
+ try {
6741
+ return fs16.readFileSync(configPath, "utf-8");
6742
+ } catch {
6743
+ return null;
6744
+ }
6638
6745
  }
6639
- function nameMatches(value, exact, name) {
6640
- return nameMatcher(value, exact)(normalizeWhiteSpace(name));
6746
+ function readInstalledPlaywrightVersion(cwd) {
6747
+ try {
6748
+ const require2 = (0, import_node_module2.createRequire)(path21.join(cwd, "noop.js"));
6749
+ for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
6750
+ try {
6751
+ return require2(id).version ?? null;
6752
+ } catch {
6753
+ }
6754
+ }
6755
+ } catch {
6756
+ }
6757
+ return null;
6641
6758
  }
6642
- function attributeMatches(value, exact, actual) {
6643
- return attributeMatcher(value, exact)(actual);
6759
+ function computeAddReporterArgs(cwd, playwrightArgs2) {
6760
+ const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs2));
6761
+ return decideAddReporter(source, readInstalledPlaywrightVersion(cwd));
6644
6762
  }
6645
6763
 
6646
- // ../core/src/locator-break.ts
6647
- var TEXT_KINDS = /* @__PURE__ */ new Set(["text", "translation", "literal"]);
6648
- function isText(anchor) {
6649
- return TEXT_KINDS.has(anchor.kind);
6650
- }
6651
- function hasAttribute(anchor, names) {
6652
- return anchor.kind === "attribute" && !!anchor.attribute && names.includes(anchor.attribute);
6764
+ // src/cli/select.ts
6765
+ var EXIT_OK6 = 0;
6766
+ var EXIT_ERROR7 = 2;
6767
+ var USAGE9 = `
6768
+ piwi select / piwi run \u2014 run a saved selection of tests
6769
+
6770
+ Usage:
6771
+ npx @piwitests/reporter select <key> [options] print the Playwright args
6772
+ npx @piwitests/reporter run <key> [options] [-- <playwright args>]
6773
+ npx @piwitests/reporter run impact --base <ref> run the tests your diff impacts
6774
+
6775
+ Connection:
6776
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
6777
+ --api-key <key> API key (env PIWI_API_KEY)
6778
+ --project <name|id> Project (env PIWI_PROJECT_NAME)
6779
+
6780
+ Selection:
6781
+ --format <fmt> args (file:line, default) | grep | files | json
6782
+ --budget <duration> Cap total time, e.g. 5m, 90s, 300000 (ms)
6783
+ --shard <i/n> Keep only shard i of n, balanced by duration, lock-aware
6784
+ --fail-fast Order the least-reliable tests first (fail-fast)
6785
+ --base <ref> For "impact": the ref to diff the working tree against
6786
+
6787
+ Behavior:
6788
+ --strict Fail (exit 2) instead of falling back when unreachable
6789
+ --pkg-runner <cmd> Package runner for the printed command (default npx)
6790
+ --json Print the full resolution as JSON (select only)
6791
+ -h, --help Show this help
6792
+
6793
+ Exit codes: 0 ok, 1 the test run failed (run only), 2 could not resolve.
6794
+ `.trim();
6795
+ var IMPACT_KEY = "impact";
6796
+ var NOTHING_TO_RUN_ARGS = ["--pass-with-no-tests", "--grep-invert", "."];
6797
+ var VALUE_FLAGS3 = /* @__PURE__ */ new Set([
6798
+ "--server-url",
6799
+ "--api-key",
6800
+ "--project",
6801
+ "--format",
6802
+ "--budget",
6803
+ "--shard",
6804
+ "--base",
6805
+ "--pkg-runner"
6806
+ ]);
6807
+ function readOption7(argv, name) {
6808
+ const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
6809
+ if (withEquals) return withEquals.slice(name.length + 1);
6810
+ const index = argv.indexOf(name);
6811
+ if (index === -1) return void 0;
6812
+ const value = argv[index + 1];
6813
+ return value && !value.startsWith("-") ? value : void 0;
6653
6814
  }
6654
- function toRegExp(arg) {
6655
- try {
6656
- return new RegExp(arg.source, arg.flags.replace(/[gy]/g, ""));
6657
- } catch {
6658
- return null;
6815
+ function findKey(argv) {
6816
+ for (let i = 0; i < argv.length; i++) {
6817
+ const tok = argv[i];
6818
+ if (tok.startsWith("-")) {
6819
+ if (!tok.includes("=") && VALUE_FLAGS3.has(tok)) i++;
6820
+ continue;
6821
+ }
6822
+ return tok;
6659
6823
  }
6824
+ return void 0;
6660
6825
  }
6661
- function optionsOf(arg) {
6662
- return new Map(arg?.type === "object" ? arg.entries : []);
6663
- }
6664
- function isExact(opts) {
6665
- const exact = opts.get("exact");
6666
- return exact?.type === "boolean" && exact.value;
6667
- }
6668
- function escapeRegExp2(s) {
6669
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6670
- }
6671
- function renameValue(value, before, after, exact) {
6672
- const v = normalizeWhiteSpace(value);
6673
- const b = normalizeWhiteSpace(before);
6674
- if (exact ? v === b : v.toLowerCase() === b.toLowerCase()) return after;
6675
- const re = new RegExp(escapeRegExp2(b).replace(/ /g, "\\s+"), exact ? "" : "i");
6676
- if (!re.test(value)) return null;
6677
- return value.replace(re, () => after);
6678
- }
6679
- function withArg(call, index, arg) {
6680
- return { method: call.method, args: call.args.map((a, i) => i === index ? arg : a) };
6681
- }
6682
- function withOption(call, index, key, arg) {
6683
- const obj = call.args[index];
6684
- if (obj?.type !== "object") return call;
6685
- return withArg(call, index, {
6686
- type: "object",
6687
- entries: obj.entries.map(([k, v]) => [k, k === key ? arg : v])
6688
- });
6826
+ function parseDuration2(raw) {
6827
+ const match = raw.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
6828
+ if (!match) return null;
6829
+ const value = Number(match[1]);
6830
+ const unit = match[2];
6831
+ const factor = unit === "h" ? 36e5 : unit === "m" ? 6e4 : unit === "s" ? 1e3 : 1;
6832
+ const ms = Math.round(value * factor);
6833
+ return ms > 0 ? ms : null;
6689
6834
  }
6690
- function stringSlot(arg, exact, rule, accepts, replace) {
6691
- if (arg?.type === "regex") {
6692
- const re = toRegExp(arg);
6693
- return re ? { value: re, exact, rule, accepts } : null;
6835
+ function parseSelectArgs(argv, env) {
6836
+ const dashIndex = argv.indexOf("--");
6837
+ const own = dashIndex === -1 ? argv : argv.slice(0, dashIndex);
6838
+ const extra = dashIndex === -1 ? [] : argv.slice(dashIndex + 1);
6839
+ const serverUrl = (readOption7(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
6840
+ if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
6841
+ const key = findKey(own);
6842
+ if (!key) throw new Error("No selection key \u2014 usage: piwi select <key>");
6843
+ const budgetRaw = readOption7(own, "--budget");
6844
+ let budgetMs = null;
6845
+ if (budgetRaw !== void 0) {
6846
+ budgetMs = parseDuration2(budgetRaw);
6847
+ if (budgetMs === null) throw new Error(`--budget expects a duration like 5m or 90s, got "${budgetRaw}"`);
6848
+ }
6849
+ const shard = readOption7(own, "--shard") ?? null;
6850
+ if (shard !== null && !/^\d+\s*\/\s*\d+$/.test(shard)) {
6851
+ throw new Error(`--shard expects an "i/n" spec like 2/4, got "${shard}"`);
6694
6852
  }
6695
- if (arg?.type !== "string") return null;
6696
- const value = arg.value;
6697
6853
  return {
6698
- value,
6699
- exact,
6700
- rule,
6701
- accepts,
6702
- rewrite: (after, before) => replace({ type: "string", value: renameValue(value, before, after, exact) ?? value }),
6703
- literal: (after, before) => [value, renameValue(value, before, after, exact) ?? value]
6854
+ serverUrl,
6855
+ apiKey: readOption7(own, "--api-key") ?? env.PIWI_API_KEY ?? null,
6856
+ project: readOption7(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
6857
+ key,
6858
+ format: readOption7(own, "--format") ?? "args",
6859
+ budgetMs,
6860
+ shard,
6861
+ order: own.includes("--fail-fast") ? "failureLikelihood" : null,
6862
+ base: readOption7(own, "--base") ?? null,
6863
+ strict: own.includes("--strict"),
6864
+ pkgRunner: readOption7(own, "--pkg-runner") ?? "npx",
6865
+ json: own.includes("--json"),
6866
+ extra
6704
6867
  };
6705
6868
  }
6706
- function cssSlots(selector, testIds, replace) {
6707
- const slots = [];
6708
- const slot = (value, names, render, at, len) => {
6709
- const edited = (after) => selector.slice(0, at) + render(after) + selector.slice(at + len);
6710
- slots.push({
6711
- value,
6712
- exact: true,
6713
- rule: "exact",
6714
- accepts: (a) => hasAttribute(a, names),
6715
- rewrite: (after) => replace(edited(after)),
6716
- literal: (after) => [selector, edited(after)]
6717
- });
6718
- };
6719
- for (const m of selector.matchAll(/#((?:[\w-]|\\.)+)/g)) {
6720
- slot(
6721
- m[1].replace(/\\(.)/g, "$1"),
6722
- ["id"],
6723
- (v) => `#${v.replace(/[^\w-]/g, (c) => `\\${c}`)}`,
6724
- m.index,
6725
- m[0].length
6726
- );
6727
- }
6728
- for (const m of selector.matchAll(/\[\s*([\w-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))\s*\]/g)) {
6729
- const attr = m[1];
6730
- const names = testIds.includes(attr) ? testIds : attr === "id" || attr === "name" ? [attr] : null;
6731
- if (!names) continue;
6732
- const quote3 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
6733
- const value = m[2] ?? m[3] ?? m[4];
6734
- slot(value, [attr], (v) => `[${attr}=${quote3}${v}${quote3}]`, m.index, m[0].length);
6735
- }
6736
- return slots;
6869
+ var CACHE_FILE = path22.join(".piwi", "selection-cache.json");
6870
+ function cacheKey(args) {
6871
+ return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
6737
6872
  }
6738
- function directSlots(call, testIds) {
6739
- const first = call.args[0];
6740
- const opts = optionsOf(call.args[1]);
6741
- const exact = isExact(opts);
6742
- const slots = [];
6743
- const argSlot = (rule, accepts, ex = exact) => stringSlot(first, ex, rule, accepts, (next) => withArg(call, 0, next));
6744
- const hasTextSlot = (index) => {
6745
- const hasText = optionsOf(call.args[index]).get("hasText");
6746
- if (!hasText) return null;
6747
- return stringSlot(hasText, false, "text", isText, (next) => withOption(call, index, "hasText", next));
6748
- };
6749
- const textOr = (attribute) => (a) => a.kind === "translation" || a.kind === "literal" || hasAttribute(a, [attribute]);
6750
- switch (call.method) {
6751
- case "getByTestId":
6752
- slots.push(argSlot("exact", (a) => hasAttribute(a, testIds), true));
6753
- break;
6754
- case "getByRole":
6755
- slots.push(
6756
- stringSlot(
6757
- opts.get("name"),
6758
- exact,
6759
- "name",
6760
- (a) => isText(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
6761
- (next) => withOption(call, 1, "name", next)
6762
- )
6763
- );
6764
- break;
6765
- case "getByText":
6766
- slots.push(argSlot("text", isText));
6767
- break;
6768
- case "getByLabel":
6769
- slots.push(argSlot("text", (a) => isText(a) || hasAttribute(a, ["aria-label"])));
6770
- break;
6771
- case "getByPlaceholder":
6772
- slots.push(argSlot("text", textOr("placeholder")));
6773
- break;
6774
- case "getByAltText":
6775
- slots.push(argSlot("text", textOr("alt")));
6776
- break;
6777
- case "getByTitle":
6778
- slots.push(argSlot("text", textOr("title")));
6779
- break;
6780
- case "locator":
6781
- if (first?.type === "string") {
6782
- slots.push(...cssSlots(first.value, testIds, (next) => withArg(call, 0, { type: "string", value: next })));
6783
- }
6784
- slots.push(hasTextSlot(1));
6785
- break;
6786
- case "filter":
6787
- slots.push(hasTextSlot(0));
6788
- break;
6873
+ function readCache(args) {
6874
+ try {
6875
+ const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
6876
+ return store[cacheKey(args)] ?? null;
6877
+ } catch {
6878
+ return null;
6789
6879
  }
6790
- return slots.filter((slot) => !!slot);
6791
6880
  }
6792
- function nestedChains(call) {
6793
- const out = [];
6794
- call.args.forEach((arg, index) => {
6795
- if (arg.type === "chain")
6796
- out.push({ chain: arg.chain, put: (c) => withArg(call, index, { type: "chain", chain: c }) });
6797
- if (arg.type === "object") {
6798
- for (const [key, v] of arg.entries) {
6799
- if (v.type === "chain") {
6800
- out.push({ chain: v.chain, put: (c) => withOption(call, index, key, { type: "chain", chain: c }) });
6801
- }
6802
- }
6881
+ function writeCache(args, resolution) {
6882
+ try {
6883
+ let store = {};
6884
+ try {
6885
+ store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
6886
+ } catch {
6803
6887
  }
6804
- });
6805
- return out;
6888
+ store[cacheKey(args)] = resolution;
6889
+ fs17.mkdirSync(path22.dirname(CACHE_FILE), { recursive: true });
6890
+ fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
6891
+ } catch {
6892
+ }
6806
6893
  }
6807
- function chainSlots(chain, testIds) {
6808
- const out = [];
6809
- chain.calls.forEach((call, i) => {
6810
- const replaceAt = (next) => ({ calls: chain.calls.map((c, j) => j === i ? next : c) });
6811
- for (const slot of directSlots(call, testIds)) {
6812
- const rewrite = slot.rewrite;
6813
- out.push({ ...slot, rewrite: rewrite && ((after, before) => replaceAt(rewrite(after, before))) });
6814
- }
6815
- for (const n of nestedChains(call)) {
6816
- for (const slot of chainSlots(n.chain, testIds)) {
6817
- const rewrite = slot.rewrite;
6818
- out.push({ ...slot, rewrite: rewrite && ((after, before) => replaceAt(n.put(rewrite(after, before)))) });
6819
- }
6894
+ async function resolveWithCache(args) {
6895
+ try {
6896
+ const projectId = await resolveProjectId(args);
6897
+ const resolution = await fetchResolution(args, projectId);
6898
+ writeCache(args, resolution);
6899
+ return { resolution, fromCache: false };
6900
+ } catch (e) {
6901
+ if (args.strict) throw e;
6902
+ const cached = readCache(args);
6903
+ if (cached) {
6904
+ console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
6905
+ return { resolution: cached, fromCache: true };
6820
6906
  }
6907
+ return { error: e };
6908
+ }
6909
+ }
6910
+ function printWarnings(resolution) {
6911
+ for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
6912
+ }
6913
+ function git2(args) {
6914
+ return (0, import_node_child_process7.execFileSync)("git", ["-c", "core.quotePath=false", ...args], {
6915
+ encoding: "utf-8",
6916
+ stdio: ["ignore", "pipe", "ignore"],
6917
+ maxBuffer: 256 * 1024 * 1024
6821
6918
  });
6822
- return out;
6823
6919
  }
6824
- function slotMatches(slot, text) {
6825
- switch (slot.rule) {
6826
- case "exact":
6827
- return attributeMatches(slot.value, true, text);
6828
- case "name":
6829
- return nameMatches(slot.value, slot.exact, text);
6830
- case "text":
6831
- return textMatches(slot.value, slot.exact, text);
6920
+ function gitChange(base) {
6921
+ const names = git2(["diff", "--name-only", base]).split("\n").map((line) => line.trim()).filter(Boolean);
6922
+ let fromDiff = { changedFiles: [], changes: [] };
6923
+ try {
6924
+ fromDiff = diffImpactInput(
6925
+ parseUnifiedDiff(
6926
+ git2([
6927
+ "diff",
6928
+ "--full-index",
6929
+ "--unified=0",
6930
+ "--no-color",
6931
+ "--no-ext-diff",
6932
+ "--src-prefix=a/",
6933
+ "--dst-prefix=b/",
6934
+ base,
6935
+ "--"
6936
+ ])
6937
+ )
6938
+ );
6939
+ } catch {
6832
6940
  }
6941
+ return { changedFiles: [.../* @__PURE__ */ new Set([...names, ...fromDiff.changedFiles])], changes: fromDiff.changes };
6833
6942
  }
6834
- var filePath = (path24) => path24.replace(/\\/g, "/").replace(/^\.\//, "");
6835
- function sameFilePath(a, b) {
6836
- const x = filePath(a);
6837
- const y = filePath(b);
6838
- return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
6943
+ async function loadImpact(args, projectId) {
6944
+ const { changedFiles, changes } = gitChange(args.base);
6945
+ return fetchImpact(args, projectId, changedFiles, changes);
6839
6946
  }
6840
- function reachOfIndex(index) {
6841
- if (!index.reach.some((r) => r.origin === "client")) return void 0;
6842
- const filesOf = /* @__PURE__ */ new Map();
6843
- for (const r of index.reach) {
6844
- const file = index.files[r.file];
6845
- if (file === void 0) continue;
6846
- for (const t of r.tests) {
6847
- const id = index.tests[t]?.id;
6848
- if (id === void 0) continue;
6849
- const files = filesOf.get(id);
6850
- if (files) files.push(file);
6851
- else filesOf.set(id, [file]);
6947
+ function lineLevelNote(impact) {
6948
+ const files = impact.impact.lineLevelFiles ?? 0;
6949
+ return files > 0 ? `, ${files} file(s) narrowed by line coverage` : "";
6950
+ }
6951
+ async function runSelectImpact(args) {
6952
+ let impact;
6953
+ try {
6954
+ const projectId = await resolveProjectId(args);
6955
+ impact = await loadImpact(args, projectId);
6956
+ } catch (e) {
6957
+ console.error(`piwi select: ${e.message}`);
6958
+ return EXIT_ERROR7;
6959
+ }
6960
+ printWarnings(impact);
6961
+ console.error(
6962
+ `piwi select: ${impact.impact.changedFiles} changed file(s) \u2192 ${impact.estimate.count} impacted test(s)${impact.impact.widened ? " (widened to full suite)" : lineLevelNote(impact)}`
6963
+ );
6964
+ if (args.json) {
6965
+ console.log(JSON.stringify(impact, null, 2));
6966
+ return EXIT_OK6;
6967
+ }
6968
+ if (impact.impact.widened) console.log("");
6969
+ else if (impact.estimate.count === 0) {
6970
+ console.error(`piwi select: no tests impacted \u2014 printing arguments that run none`);
6971
+ console.log(NOTHING_TO_RUN_ARGS.join(" "));
6972
+ } else console.log(impact.materialization.args.join(" "));
6973
+ return EXIT_OK6;
6974
+ }
6975
+ async function runSelect(argv, env = process.env) {
6976
+ if (argv.includes("-h") || argv.includes("--help")) {
6977
+ console.log(USAGE9);
6978
+ return EXIT_OK6;
6979
+ }
6980
+ let args;
6981
+ try {
6982
+ args = parseSelectArgs(argv, env);
6983
+ } catch (e) {
6984
+ console.error(`piwi select: ${e.message}
6985
+ `);
6986
+ console.error(USAGE9);
6987
+ return EXIT_ERROR7;
6988
+ }
6989
+ if (args.key === IMPACT_KEY) {
6990
+ if (!args.base) {
6991
+ console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
6992
+ return EXIT_ERROR7;
6852
6993
  }
6994
+ return runSelectImpact(args);
6853
6995
  }
6854
- return (testId, file) => (filesOf.get(testId) ?? []).some((reached) => sameFilePath(reached, file));
6996
+ let resolution;
6997
+ try {
6998
+ const projectId = await resolveProjectId(args);
6999
+ resolution = await fetchResolution(args, projectId);
7000
+ } catch (e) {
7001
+ console.error(`piwi select: ${e.message}`);
7002
+ return EXIT_ERROR7;
7003
+ }
7004
+ printWarnings(resolution);
7005
+ if (resolution.estimate.count === 0) {
7006
+ console.error(`piwi select: "${args.key}" resolved to 0 tests`);
7007
+ return EXIT_ERROR7;
7008
+ }
7009
+ if (args.json) console.log(JSON.stringify(resolution, null, 2));
7010
+ else console.log(resolution.materialization.args.join(" "));
7011
+ return EXIT_OK6;
6855
7012
  }
6856
- function callSiteFile(callSite) {
6857
- return callSite.replace(/:\d+(?::\d+)?$/, "");
7013
+ function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
7014
+ const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
7015
+ if (decision.log) console.error(decision.log);
7016
+ return spawnPlaywright([...decision.args, ...playwrightArgs2], env, { runner: pkgRunner, command: "piwi run" });
6858
7017
  }
6859
- function callSiteLine(callSite) {
6860
- const m = /:(\d+)(?::\d+)?$/.exec(callSite);
6861
- return m ? Number(m[1]) : null;
7018
+ function shardEnv(args) {
7019
+ return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
6862
7020
  }
6863
- function locatorCallSiteFiles(index) {
6864
- const files = /* @__PURE__ */ new Set();
6865
- for (const entry of index.locators) {
6866
- for (const use of entry.uses) for (const site of use.callSites) files.add(callSiteFile(site));
7021
+ async function runRunImpact(args, env) {
7022
+ let projectId;
7023
+ try {
7024
+ projectId = await resolveProjectId(args);
7025
+ } catch (e) {
7026
+ if (args.strict) {
7027
+ console.error(`piwi run: ${e.message}`);
7028
+ return EXIT_ERROR7;
7029
+ }
7030
+ console.error(`piwi run: ${e.message} \u2014 running the full suite`);
7031
+ return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
6867
7032
  }
6868
- return files;
7033
+ let impact;
7034
+ try {
7035
+ impact = await loadImpact(args, projectId);
7036
+ } catch (e) {
7037
+ if (args.strict) {
7038
+ console.error(`piwi run: ${e.message}`);
7039
+ return EXIT_ERROR7;
7040
+ }
7041
+ console.error(`piwi run: ${e.message} \u2014 running the full suite`);
7042
+ return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
7043
+ }
7044
+ printWarnings(impact);
7045
+ if (impact.impact.widened) {
7046
+ console.error(
7047
+ `piwi run: impact widened to the full suite (${impact.impact.unmappedSourceFiles.length} changed file(s) not fully mapped to tests)`
7048
+ );
7049
+ return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
7050
+ }
7051
+ if (impact.estimate.count === 0) {
7052
+ console.error(`piwi run: no tests impacted by ${impact.impact.changedFiles} changed file(s) \u2014 nothing to run`);
7053
+ return EXIT_OK6;
7054
+ }
7055
+ const runEnv = {
7056
+ ...env,
7057
+ PIWI_SELECTION: IMPACT_KEY,
7058
+ PIWI_SELECTION_VERSION: "0",
7059
+ PIWI_SELECTION_HASH: impact.resolvedHash,
7060
+ PIWI_SELECTION_COUNT: String(impact.estimate.count),
7061
+ ...shardEnv(args)
7062
+ };
7063
+ console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)${lineLevelNote(impact)}`);
7064
+ return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
6869
7065
  }
6870
- var CONFIDENCE_RANK = { likely: 0, possible: 1 };
6871
- function predictLocatorBreaks(anchors, index, options = {}) {
6872
- const testIds = options.testIdAttributes ?? index.testIdAttributes ?? ["data-testid"];
6873
- const siteFiles = [...locatorCallSiteFiles(index)];
6874
- const live = anchors.filter((a) => !siteFiles.some((f) => sameFilePath(f, a.file)));
6875
- if (!live.length) return [];
6876
- const breaks = [];
6877
- for (const entry of index.locators) {
6878
- const chain = tryParseLocatorChain(entry.locator);
6879
- if (!chain) continue;
6880
- const slots = chainSlots(chain, testIds);
6881
- if (!slots.length) continue;
6882
- for (const anchor of live) {
6883
- const hit = slots.filter(
6884
- (slot) => slot.accepts(anchor) && slotMatches(slot, anchor.before) && (anchor.after === void 0 || !slotMatches(slot, anchor.after))
6885
- );
6886
- if (!hit.length) continue;
6887
- const tests = [...new Set(entry.uses.map((u) => u.test))].map((t) => index.tests[t]).filter((t) => !!t);
6888
- const found = {
6889
- anchor,
6890
- locator: entry.locator,
6891
- entry,
6892
- uses: entry.uses,
6893
- tests,
6894
- confidence: confidenceOf(anchor, tests, options.reach)
6895
- };
6896
- if (anchor.after !== void 0 && hit.every((slot) => slot.rewrite)) {
6897
- let rewritten = chain;
6898
- const replacements = [];
6899
- for (const slot of hit) {
6900
- const again = chainSlots(rewritten, testIds).find(
6901
- (s) => s.rule === slot.rule && String(s.value) === String(slot.value) && s.rewrite
6902
- );
6903
- if (!again?.rewrite) continue;
6904
- rewritten = again.rewrite(anchor.after, anchor.before);
6905
- if (slot.literal) replacements.push(slot.literal(anchor.after, anchor.before));
6906
- }
6907
- const rendered = renderLocatorChain(rewritten);
6908
- if (rendered !== entry.locator) {
6909
- found.rewrite = rendered;
6910
- found.replacements = replacements.filter(([a, b]) => a !== b);
6911
- }
6912
- }
6913
- breaks.push(found);
7066
+ async function runRun(argv, env = process.env) {
7067
+ if (argv.includes("-h") || argv.includes("--help")) {
7068
+ console.log(USAGE9);
7069
+ return EXIT_OK6;
7070
+ }
7071
+ let args;
7072
+ try {
7073
+ args = parseSelectArgs(argv, env);
7074
+ } catch (e) {
7075
+ console.error(`piwi run: ${e.message}
7076
+ `);
7077
+ console.error(USAGE9);
7078
+ return EXIT_ERROR7;
7079
+ }
7080
+ if (args.format === "json") {
7081
+ console.error("piwi run: --format json cannot be run; use args, grep or files");
7082
+ return EXIT_ERROR7;
7083
+ }
7084
+ if (args.key === IMPACT_KEY) {
7085
+ if (!args.base) {
7086
+ console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
7087
+ return EXIT_ERROR7;
6914
7088
  }
7089
+ return runRunImpact(args, env);
6915
7090
  }
6916
- return breaks.sort(
6917
- (a, b) => CONFIDENCE_RANK[a.confidence] - CONFIDENCE_RANK[b.confidence] || b.tests.length - a.tests.length
7091
+ let outcome;
7092
+ try {
7093
+ outcome = await resolveWithCache(args);
7094
+ } catch (e) {
7095
+ console.error(`piwi run: ${e.message}`);
7096
+ return EXIT_ERROR7;
7097
+ }
7098
+ if ("error" in outcome) {
7099
+ console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
7100
+ return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
7101
+ }
7102
+ const { resolution } = outcome;
7103
+ printWarnings(resolution);
7104
+ if (resolution.estimate.count === 0) {
7105
+ console.error(`piwi run: "${args.key}" resolved to 0 tests`);
7106
+ return EXIT_ERROR7;
7107
+ }
7108
+ const runEnv = {
7109
+ ...env,
7110
+ PIWI_SELECTION: resolution.key ?? args.key,
7111
+ PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
7112
+ PIWI_SELECTION_HASH: resolution.resolvedHash,
7113
+ PIWI_SELECTION_COUNT: String(resolution.estimate.count),
7114
+ ...shardEnv(args)
7115
+ };
7116
+ console.error(
7117
+ `piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
6918
7118
  );
7119
+ return spawnPlaywrightForRun(args.pkgRunner, [...resolution.materialization.args, ...args.extra], runEnv);
6919
7120
  }
6920
- function confidenceOf(anchor, tests, reach) {
6921
- if (anchor.kind === "literal") return "possible";
6922
- if (!reach) return "likely";
6923
- if (anchor.kind === "translation" && /\.(?:json|ya?ml|properties|po|resx)$/i.test(anchor.file)) return "likely";
6924
- return tests.some((t) => reach(t.id, anchor.file)) ? "likely" : "possible";
6925
- }
7121
+
7122
+ // src/cli/preflight.ts
7123
+ var fs18 = __toESM(require("fs"));
7124
+ var path23 = __toESM(require("path"));
7125
+ var import_node_child_process8 = require("child_process");
6926
7126
 
6927
7127
  // ../core/src/locator-edit.ts
6928
7128
  function escapeForQuote(value, quote3) {
@@ -7046,15 +7246,15 @@ function parsePreflightArgs(argv) {
7046
7246
  }
7047
7247
  var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
7048
7248
  function findPlaywrightRoot(from) {
7049
- let dir = path22.resolve(from);
7249
+ let dir = path23.resolve(from);
7050
7250
  for (; ; ) {
7051
- if (CONFIG_NAMES2.some((name) => fs18.existsSync(path22.join(dir, name)))) return dir;
7052
- const parent = path22.dirname(dir);
7251
+ if (CONFIG_NAMES2.some((name) => fs18.existsSync(path23.join(dir, name)))) return dir;
7252
+ const parent = path23.dirname(dir);
7053
7253
  if (parent === dir) return null;
7054
7254
  dir = parent;
7055
7255
  }
7056
7256
  }
7057
- function git2(cwd, args) {
7257
+ function git3(cwd, args) {
7058
7258
  return (0, import_node_child_process8.execFileSync)("git", args, {
7059
7259
  cwd,
7060
7260
  encoding: "utf-8",
@@ -7064,7 +7264,7 @@ function git2(cwd, args) {
7064
7264
  }
7065
7265
  function tryGit(cwd, args) {
7066
7266
  try {
7067
- return git2(cwd, args);
7267
+ return git3(cwd, args);
7068
7268
  } catch {
7069
7269
  return void 0;
7070
7270
  }
@@ -7079,7 +7279,7 @@ function translationLookup(repoRoot, base, locale) {
7079
7279
  const build = (side) => {
7080
7280
  const map = /* @__PURE__ */ new Map();
7081
7281
  for (const file of files) {
7082
- const content = side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
7282
+ const content = side === "new" ? readText(path23.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
7083
7283
  if (content === void 0) continue;
7084
7284
  for (const entry of parseTranslationFile(file, content)) {
7085
7285
  if (!map.has(entry.key)) map.set(entry.key, entry.value);
@@ -7103,20 +7303,20 @@ function readText(file) {
7103
7303
  return void 0;
7104
7304
  }
7105
7305
  }
7106
- var CACHE_FILE2 = path22.join(".piwi", "locator-index.json");
7306
+ var CACHE_FILE2 = path23.join(".piwi", "locator-index.json");
7107
7307
  function cacheKey2(connection, branch) {
7108
7308
  return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
7109
7309
  }
7110
7310
  function readCache2(root, key) {
7111
7311
  try {
7112
- const store = JSON.parse(fs18.readFileSync(path22.join(root, CACHE_FILE2), "utf-8"));
7312
+ const store = JSON.parse(fs18.readFileSync(path23.join(root, CACHE_FILE2), "utf-8"));
7113
7313
  return store[key] ?? null;
7114
7314
  } catch {
7115
7315
  return null;
7116
7316
  }
7117
7317
  }
7118
7318
  function writeCache2(root, key, index) {
7119
- const file = path22.join(root, CACHE_FILE2);
7319
+ const file = path23.join(root, CACHE_FILE2);
7120
7320
  try {
7121
7321
  let store = {};
7122
7322
  try {
@@ -7124,7 +7324,7 @@ function writeCache2(root, key, index) {
7124
7324
  } catch {
7125
7325
  }
7126
7326
  store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
7127
- fs18.mkdirSync(path22.dirname(file), { recursive: true });
7327
+ fs18.mkdirSync(path23.dirname(file), { recursive: true });
7128
7328
  fs18.writeFileSync(file, JSON.stringify(store));
7129
7329
  } catch {
7130
7330
  }
@@ -7141,7 +7341,7 @@ function planSites(b, testRoot) {
7141
7341
  const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
7142
7342
  return sites.map((callSite) => {
7143
7343
  if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
7144
- const file = path22.resolve(testRoot, callSiteFile(callSite));
7344
+ const file = path23.resolve(testRoot, callSiteFile(callSite));
7145
7345
  const lineNo = callSiteLine(callSite);
7146
7346
  const text = readText(file);
7147
7347
  if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
@@ -7305,10 +7505,22 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7305
7505
  console.error("piwi preflight: not inside a git repository");
7306
7506
  return EXIT_ERROR8;
7307
7507
  }
7308
- const testRoot = path22.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7508
+ const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7309
7509
  let files;
7310
7510
  try {
7311
- files = parseUnifiedDiff(git2(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
7511
+ files = parseUnifiedDiff(
7512
+ git3(repoRoot, [
7513
+ "diff",
7514
+ "--full-index",
7515
+ "--unified=0",
7516
+ "--no-color",
7517
+ "--no-ext-diff",
7518
+ "--src-prefix=a/",
7519
+ "--dst-prefix=b/",
7520
+ args.base,
7521
+ "--"
7522
+ ])
7523
+ );
7312
7524
  } catch (e) {
7313
7525
  console.error(`piwi preflight: could not diff against ${args.base} \u2014 ${e.message.split("\n")[0]}`);
7314
7526
  return EXIT_ERROR8;
@@ -7342,7 +7554,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7342
7554
  testIdAttributes: index.testIdAttributes ?? void 0,
7343
7555
  isTestFile: (file) => TEST_FILE.test(file),
7344
7556
  translations: translationLookup(repoRoot, args.base, args.locale),
7345
- readFile: (file, side) => side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
7557
+ readFile: (file, side) => side === "new" ? readText(path23.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
7346
7558
  });
7347
7559
  const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
7348
7560
  const replacementsOf = /* @__PURE__ */ new Map();
@@ -7360,14 +7572,15 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7360
7572
  return pb;
7361
7573
  });
7362
7574
  let impact = null;
7363
- const changed = files.map((f) => f.path);
7575
+ const { changedFiles: changed, changes } = diffImpactInput(files);
7364
7576
  const breakSpecs = [...new Set(breaks.flatMap((b) => b.tests.map((t) => t.file)))];
7365
7577
  if (projectId !== null && (changed.length || args.run && breakSpecs.length)) {
7366
7578
  try {
7367
7579
  impact = await fetchImpact(
7368
7580
  { ...connection, key: "impact", format: "args", pkgRunner: args.pkgRunner },
7369
7581
  projectId,
7370
- args.run ? [.../* @__PURE__ */ new Set([...changed, ...breakSpecs])] : changed
7582
+ args.run ? [.../* @__PURE__ */ new Set([...changed, ...breakSpecs])] : changed,
7583
+ changes
7371
7584
  );
7372
7585
  } catch (e) {
7373
7586
  console.error(`piwi preflight: could not ask which tests reach the changed files \u2014 ${e.message}`);
@@ -7387,8 +7600,8 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7387
7600
  if (args.json) console.log(JSON.stringify(result, null, 2));
7388
7601
  else {
7389
7602
  const rel = (file) => {
7390
- const abs = path22.isAbsolute(file) ? file : path22.resolve(testRoot, file);
7391
- const r = path22.relative(cwd, abs).split(path22.sep).join("/");
7603
+ const abs = path23.isAbsolute(file) ? file : path23.resolve(testRoot, file);
7604
+ const r = path23.relative(cwd, abs).split(path23.sep).join("/");
7392
7605
  return r.startsWith("..") ? file : r || file;
7393
7606
  };
7394
7607
  console.log(renderPreflight(result, rel));
@@ -7420,7 +7633,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7420
7633
  // src/cli/probe.ts
7421
7634
  var fs19 = __toESM(require("fs"));
7422
7635
  var os3 = __toESM(require("os"));
7423
- var path23 = __toESM(require("path"));
7636
+ var path24 = __toESM(require("path"));
7424
7637
  var EXIT_OK8 = 0;
7425
7638
  var EXIT_ERROR9 = 2;
7426
7639
  var USAGE11 = `
@@ -7484,10 +7697,10 @@ async function runProbe(argv) {
7484
7697
  return EXIT_OK8;
7485
7698
  }
7486
7699
  console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
7487
- const dir = fs19.mkdtempSync(path23.join(os3.tmpdir(), "piwi-probe-"));
7488
- const planFile = path23.join(dir, "plan.json");
7489
- const resultsFile = path23.join(dir, "results.jsonl");
7490
- const runOutputFile = path23.join(dir, "run.json");
7700
+ const dir = fs19.mkdtempSync(path24.join(os3.tmpdir(), "piwi-probe-"));
7701
+ const planFile = path24.join(dir, "plan.json");
7702
+ const resultsFile = path24.join(dir, "results.jsonl");
7703
+ const runOutputFile = path24.join(dir, "run.json");
7491
7704
  fs19.writeFileSync(planFile, JSON.stringify(plan));
7492
7705
  fs19.writeFileSync(resultsFile, "");
7493
7706
  const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");