@piwitests/reporter 0.41.0 → 0.43.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
@@ -1924,43 +1924,43 @@ var Checker = class {
1924
1924
  constructor() {
1925
1925
  this.errors = [];
1926
1926
  }
1927
- fail(path16, message) {
1928
- if (this.errors.length < 20) this.errors.push(`${path16}: ${message}`);
1927
+ fail(path19, message) {
1928
+ if (this.errors.length < 20) this.errors.push(`${path19}: ${message}`);
1929
1929
  }
1930
- text(v, path16, max, nullable) {
1930
+ text(v, path19, max, nullable) {
1931
1931
  if (v == null) {
1932
- if (!nullable) this.fail(path16, "is required");
1932
+ if (!nullable) this.fail(path19, "is required");
1933
1933
  return null;
1934
1934
  }
1935
1935
  if (typeof v !== "string") {
1936
- this.fail(path16, "must be a string");
1936
+ this.fail(path19, "must be a string");
1937
1937
  return null;
1938
1938
  }
1939
- if (v.length > max) this.fail(path16, `is longer than ${max} characters`);
1939
+ if (v.length > max) this.fail(path19, `is longer than ${max} characters`);
1940
1940
  return v;
1941
1941
  }
1942
- number(v, path16) {
1942
+ number(v, path19) {
1943
1943
  if (typeof v !== "number" || !Number.isFinite(v)) {
1944
- this.fail(path16, "must be a number");
1944
+ this.fail(path19, "must be a number");
1945
1945
  return 0;
1946
1946
  }
1947
1947
  return v;
1948
1948
  }
1949
1949
  };
1950
- function checkTarget(c, v, path16) {
1950
+ function checkTarget(c, v, path19) {
1951
1951
  if (v == null) return null;
1952
1952
  if (!isObject(v)) {
1953
- c.fail(path16, "must be an object");
1953
+ c.fail(path19, "must be an object");
1954
1954
  return null;
1955
1955
  }
1956
1956
  const alternativesIn = v.alternatives ?? [];
1957
1957
  const alternatives = [];
1958
- if (!Array.isArray(alternativesIn)) c.fail(`${path16}.alternatives`, "must be a list");
1958
+ if (!Array.isArray(alternativesIn)) c.fail(`${path19}.alternatives`, "must be a list");
1959
1959
  else if (alternativesIn.length > STEPS_LIMITS.alternatives)
1960
- c.fail(`${path16}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
1960
+ c.fail(`${path19}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
1961
1961
  else {
1962
1962
  alternativesIn.forEach((alt, i) => {
1963
- const at = `${path16}.alternatives[${i}]`;
1963
+ const at = `${path19}.alternatives[${i}]`;
1964
1964
  if (!isObject(alt)) return c.fail(at, "must be an object");
1965
1965
  const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
1966
1966
  const safe = locator ? safeLocator(locator) : null;
@@ -1974,67 +1974,67 @@ function checkTarget(c, v, path16) {
1974
1974
  });
1975
1975
  }
1976
1976
  return {
1977
- tagName: c.text(v.tagName, `${path16}.tagName`, 64, false) ?? "",
1978
- role: c.text(v.role, `${path16}.role`, 64, true),
1979
- accessibleName: c.text(v.accessibleName, `${path16}.accessibleName`, STEPS_LIMITS.textLength, true),
1980
- testId: c.text(v.testId, `${path16}.testId`, STEPS_LIMITS.textLength, true),
1981
- text: c.text(v.text, `${path16}.text`, STEPS_LIMITS.textLength, true),
1977
+ tagName: c.text(v.tagName, `${path19}.tagName`, 64, false) ?? "",
1978
+ role: c.text(v.role, `${path19}.role`, 64, true),
1979
+ accessibleName: c.text(v.accessibleName, `${path19}.accessibleName`, STEPS_LIMITS.textLength, true),
1980
+ testId: c.text(v.testId, `${path19}.testId`, STEPS_LIMITS.textLength, true),
1981
+ text: c.text(v.text, `${path19}.text`, STEPS_LIMITS.textLength, true),
1982
1982
  alternatives
1983
1983
  };
1984
1984
  }
1985
- function checkAssertion(c, v, path16) {
1985
+ function checkAssertion(c, v, path19) {
1986
1986
  if (!isObject(v)) {
1987
- c.fail(path16, "is required on an assert step");
1987
+ c.fail(path19, "is required on an assert step");
1988
1988
  return void 0;
1989
1989
  }
1990
1990
  const matcher = v.matcher;
1991
1991
  if (!ASSERTION_MATCHERS.includes(matcher)) {
1992
- c.fail(`${path16}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
1992
+ c.fail(`${path19}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
1993
1993
  return void 0;
1994
1994
  }
1995
- const expected = c.text(v.expected, `${path16}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
1995
+ const expected = c.text(v.expected, `${path19}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
1996
1996
  return {
1997
1997
  matcher,
1998
1998
  expected: VALUE_MATCHERS.has(matcher) ? expected : null,
1999
- actual: c.text(v.actual, `${path16}.actual`, STEPS_LIMITS.valueLength, true),
1999
+ actual: c.text(v.actual, `${path19}.actual`, STEPS_LIMITS.valueLength, true),
2000
2000
  negated: v.negated === true,
2001
- note: c.text(v.note, `${path16}.note`, STEPS_LIMITS.valueLength, true)
2001
+ note: c.text(v.note, `${path19}.note`, STEPS_LIMITS.valueLength, true)
2002
2002
  };
2003
2003
  }
2004
- function checkStep(c, v, path16) {
2004
+ function checkStep(c, v, path19) {
2005
2005
  if (!isObject(v)) {
2006
- c.fail(path16, "must be an object");
2006
+ c.fail(path19, "must be an object");
2007
2007
  return null;
2008
2008
  }
2009
2009
  const action = v.action;
2010
2010
  if (!ACTIONS.has(action)) {
2011
- c.fail(`${path16}.action`, "is not a known action");
2011
+ c.fail(`${path19}.action`, "is not a known action");
2012
2012
  return null;
2013
2013
  }
2014
2014
  const step = {
2015
2015
  action: action === "assertVisible" ? "assert" : action,
2016
- target: checkTarget(c, v.target, `${path16}.target`),
2017
- value: c.text(v.value, `${path16}.value`, STEPS_LIMITS.valueLength, true),
2016
+ target: checkTarget(c, v.target, `${path19}.target`),
2017
+ value: c.text(v.value, `${path19}.value`, STEPS_LIMITS.valueLength, true),
2018
2018
  redacted: v.redacted === true,
2019
- pageUrl: c.text(v.pageUrl, `${path16}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2020
- timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path16}.timestamp`)
2019
+ pageUrl: c.text(v.pageUrl, `${path19}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2020
+ timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path19}.timestamp`)
2021
2021
  };
2022
2022
  if (step.redacted) step.value = null;
2023
2023
  if (action === "hover" || action === "dblclick") {
2024
2024
  step.value = null;
2025
- if (!step.target) c.fail(`${path16}.target`, `is required on a ${action} step`);
2025
+ if (!step.target) c.fail(`${path19}.target`, `is required on a ${action} step`);
2026
2026
  }
2027
- if (action === "setInputFiles" && !step.target) c.fail(`${path16}.target`, "is required on a setInputFiles step");
2027
+ if (action === "setInputFiles" && !step.target) c.fail(`${path19}.target`, "is required on a setInputFiles step");
2028
2028
  if (action === "dragTo") {
2029
2029
  step.value = null;
2030
- if (!step.target) c.fail(`${path16}.target`, "is required on a dragTo step");
2031
- step.dropTarget = checkTarget(c, v.dropTarget, `${path16}.dropTarget`);
2032
- if (!step.dropTarget) c.fail(`${path16}.dropTarget`, "is required on a dragTo step");
2030
+ if (!step.target) c.fail(`${path19}.target`, "is required on a dragTo step");
2031
+ step.dropTarget = checkTarget(c, v.dropTarget, `${path19}.dropTarget`);
2032
+ if (!step.dropTarget) c.fail(`${path19}.dropTarget`, "is required on a dragTo step");
2033
2033
  }
2034
2034
  if (action === "assertVisible") {
2035
2035
  step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
2036
2036
  } else if (action === "assert") {
2037
- step.assertion = checkAssertion(c, v.assertion, `${path16}.assertion`);
2037
+ step.assertion = checkAssertion(c, v.assertion, `${path19}.assertion`);
2038
2038
  }
2039
2039
  return step;
2040
2040
  }
@@ -2378,8 +2378,1019 @@ ${USAGE3}`);
2378
2378
  return EXIT_OK3;
2379
2379
  }
2380
2380
 
2381
- // src/cli/gate.ts
2381
+ // src/cli/flake.ts
2382
+ var import_node_child_process4 = require("child_process");
2383
+ var fs8 = __toESM(require("fs"));
2384
+ var os2 = __toESM(require("os"));
2385
+ var path11 = __toESM(require("path"));
2386
+
2387
+ // ../core/src/error-parse.ts
2388
+ var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
2389
+ var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
2390
+ ...LOCATOR_BUILDER_METHODS,
2391
+ "frameLocator",
2392
+ "contentFrame",
2393
+ "filter",
2394
+ "first",
2395
+ "last",
2396
+ "nth",
2397
+ "and",
2398
+ "or",
2399
+ "describe"
2400
+ ]);
2401
+ var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
2402
+ var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
2403
+ var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
2404
+
2405
+ // ../core/src/flake-plan.ts
2406
+ var FLAKE_PLAN_VERSION = 1;
2407
+ var FLAKE_BATCH_RUNS = 5;
2408
+ var FLAKE_SPAWN_OVERHEAD_MS = 3e3;
2409
+ function estimateFlakeSessionMs(arms, medianDurationMs) {
2410
+ if (medianDurationMs == null) return null;
2411
+ let total = 0;
2412
+ for (const arm of arms) {
2413
+ const twoTests = arm.conditions.some((c) => c.kind === "alongside" || c.kind === "after");
2414
+ const batches = arm.stopAt == null ? 1 : Math.ceil(arm.runs / FLAKE_BATCH_RUNS);
2415
+ total += arm.runs * medianDurationMs * (twoTests ? 2 : 1) + batches * FLAKE_SPAWN_OVERHEAD_MS;
2416
+ }
2417
+ return total;
2418
+ }
2419
+
2420
+ // src/internal/config/desktop.ts
2382
2421
  var fs5 = __toESM(require("fs"));
2422
+ var os = __toESM(require("os"));
2423
+ var path8 = __toESM(require("path"));
2424
+ function defaultDesktopConfigPath() {
2425
+ return path8.join(os.homedir(), ".piwi", "desktop.json");
2426
+ }
2427
+ function readDesktopConfig(filePath) {
2428
+ try {
2429
+ const parsed = JSON.parse(fs5.readFileSync(filePath, "utf8"));
2430
+ const url = parsed?.url;
2431
+ const token = parsed?.token;
2432
+ if (typeof url !== "string" || !url) return null;
2433
+ if (typeof token !== "string" || !token) return null;
2434
+ return { url, token };
2435
+ } catch {
2436
+ return null;
2437
+ }
2438
+ }
2439
+
2440
+ // src/internal/config/env.ts
2441
+ var DEFAULTS = {
2442
+ projectName: "default-project",
2443
+ uploadTraces: true,
2444
+ uploadReport: true,
2445
+ liveFileUploads: true,
2446
+ collectScmInfo: true,
2447
+ collectCiInfo: true,
2448
+ collectPerformanceMetrics: true,
2449
+ captureLocators: true,
2450
+ capturePageState: true,
2451
+ // Off by default: the page inventory reads control and link names from every
2452
+ // visited page, so it stays opt-in until a project turns it on.
2453
+ capturePageInventory: false,
2454
+ // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
2455
+ captureCodeReach: false,
2456
+ captureServerTraces: true,
2457
+ sampleAriaOnPass: true,
2458
+ uploadManifest: true,
2459
+ defaultCapture: true,
2460
+ streaming: true,
2461
+ streamingBatchSize: 5,
2462
+ streamingBatchDelay: 2e3,
2463
+ maxStreamBufferBytes: 100 * 1024 * 1024,
2464
+ submitTimeout: 15 * 60 * 1e3,
2465
+ failOnFlakyTests: false,
2466
+ username: null,
2467
+ password: null,
2468
+ apiKey: null,
2469
+ verbose: false
2470
+ };
2471
+ var PIWI_ENV_KEYS = {
2472
+ serverUrl: "PIWI_DASHBOARD_URL",
2473
+ projectName: "PIWI_PROJECT_NAME",
2474
+ verbose: "PIWI_VERBOSE",
2475
+ apiKey: "PIWI_API_KEY",
2476
+ username: "PIWI_USERNAME",
2477
+ password: "PIWI_PASSWORD",
2478
+ environment: "PIWI_ENVIRONMENT",
2479
+ label: "PIWI_LABEL",
2480
+ runLabel: "PIWI_RUN_LABEL",
2481
+ keep: "PIWI_KEEP",
2482
+ streaming: "PIWI_STREAMING",
2483
+ streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
2484
+ streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
2485
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
2486
+ submitTimeout: "PIWI_SUBMIT_TIMEOUT",
2487
+ liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
2488
+ failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
2489
+ uploadTraces: "PIWI_UPLOAD_TRACES",
2490
+ uploadReport: "PIWI_UPLOAD_REPORT",
2491
+ captureLocators: "PIWI_CAPTURE_LOCATORS",
2492
+ capturePageState: "PIWI_CAPTURE_PAGE_STATE",
2493
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
2494
+ captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
2495
+ codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2496
+ captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
2497
+ sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
2498
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
2499
+ defaultCapture: "PIWI_DEFAULT_CAPTURE",
2500
+ inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
2501
+ pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
2502
+ outputFile: "PIWI_OUTPUT_FILE",
2503
+ aiMode: "PIWI_AI",
2504
+ aiDir: "PIWI_AI_DIR",
2505
+ aiOnMiss: "PIWI_AI_ON_MISS",
2506
+ aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
2507
+ aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
2508
+ aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
2509
+ aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
2510
+ aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
2511
+ };
2512
+ var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
2513
+ var PIWI_SHARD_ENV = "PIWI_SHARD";
2514
+ var PIWI_PROBE_ENV = {
2515
+ flag: "PIWI_PROBE",
2516
+ plan: "PIWI_PROBE_PLAN",
2517
+ results: "PIWI_PROBE_RESULTS",
2518
+ secret: "PIWI_PROBE_SECRET"
2519
+ };
2520
+ var PIWI_FLAKE_ENV = {
2521
+ plan: "PIWI_FLAKE_PLAN",
2522
+ results: "PIWI_FLAKE_RESULTS"
2523
+ };
2524
+ var ENV_FALLBACK_SPECS = [
2525
+ { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
2526
+ { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
2527
+ { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
2528
+ { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
2529
+ { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
2530
+ { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
2531
+ { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
2532
+ { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
2533
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
2534
+ { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
2535
+ { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
2536
+ { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
2537
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
2538
+ { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
2539
+ { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
2540
+ { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
2541
+ { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
2542
+ { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
2543
+ { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
2544
+ { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
2545
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
2546
+ { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
2547
+ { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
2548
+ { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
2549
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
2550
+ { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
2551
+ { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
2552
+ { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
2553
+ { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
2554
+ ];
2555
+
2556
+ // ../core/src/flake-verdict.ts
2557
+ var FLAKE_SIGNIFICANCE = 0.05;
2558
+ var FLAKE_REPRODUCED_RATE = 0.5;
2559
+ var FLAKE_VERIFY_MIN_RUNS = 5;
2560
+ function logFactorial(n) {
2561
+ let sum = 0;
2562
+ for (let i = 2; i <= n; i++) sum += Math.log(i);
2563
+ return sum;
2564
+ }
2565
+ function logChoose(n, k) {
2566
+ return logFactorial(n) - logFactorial(k) - logFactorial(n - k);
2567
+ }
2568
+ function assertCount(value, name) {
2569
+ if (!Number.isInteger(value) || value < 0) throw new RangeError(`${name} must be a whole number from 0`);
2570
+ }
2571
+ function fisherExactGreater(armFailures, armRuns, controlFailures, controlRuns) {
2572
+ assertCount(armFailures, "armFailures");
2573
+ assertCount(armRuns, "armRuns");
2574
+ assertCount(controlFailures, "controlFailures");
2575
+ assertCount(controlRuns, "controlRuns");
2576
+ if (armFailures > armRuns || controlFailures > controlRuns) throw new RangeError("failures cannot exceed runs");
2577
+ const failures = armFailures + controlFailures;
2578
+ const total = armRuns + controlRuns;
2579
+ const denominator = logChoose(total, failures);
2580
+ const highest = Math.min(armRuns, failures);
2581
+ let p = 0;
2582
+ for (let k = armFailures; k <= highest; k++) {
2583
+ if (failures - k > controlRuns) continue;
2584
+ p += Math.exp(logChoose(armRuns, k) + logChoose(controlRuns, failures - k) - denominator);
2585
+ }
2586
+ return Math.min(1, p);
2587
+ }
2588
+ function flakeArmVerdict(arm, control) {
2589
+ const pValue = fisherExactGreater(arm.matchingFailures, arm.runs, control.matchingFailures, control.runs);
2590
+ const rate = arm.runs > 0 ? arm.matchingFailures / arm.runs : 0;
2591
+ const significant = pValue < FLAKE_SIGNIFICANCE;
2592
+ const verdict = significant && rate >= FLAKE_REPRODUCED_RATE ? "reproduced" : significant ? "amplified" : "not-reproduced";
2593
+ return { verdict, rate, pValue };
2594
+ }
2595
+ function flakeVerifyRuns(rate) {
2596
+ if (!Number.isFinite(rate) || rate <= 0 || rate > 1) throw new RangeError("rate must be above 0 and at most 1");
2597
+ if (rate === 1) return FLAKE_VERIFY_MIN_RUNS;
2598
+ const exact = Math.log(FLAKE_SIGNIFICANCE) / Math.log(1 - rate);
2599
+ return Math.max(FLAKE_VERIFY_MIN_RUNS, Math.ceil(Number(exact.toFixed(9))));
2600
+ }
2601
+ function flakeFixVerified(verify, reproducedRate) {
2602
+ return verify.matchingFailures === 0 && verify.runs >= flakeVerifyRuns(reproducedRate);
2603
+ }
2604
+
2605
+ // src/internal/flake/lab.ts
2606
+ var FAILED = /* @__PURE__ */ new Set(["failed", "timedOut"]);
2607
+ function overlaps(a, b) {
2608
+ return a.startedAt < b.startedAt + b.duration && b.startedAt < a.startedAt + a.duration;
2609
+ }
2610
+ function workerCondition(conditions) {
2611
+ for (const c of conditions) if (c.kind === "alongside" || c.kind === "after") return c;
2612
+ return null;
2613
+ }
2614
+ function roundKept(line, all, conditions) {
2615
+ const condition = workerCondition(conditions);
2616
+ if (!condition) return true;
2617
+ if (condition.kind === "alongside") return all.some((l) => l.role === "companion" && overlaps(l, line));
2618
+ const before = all.filter((l) => l !== line && l.startedAt < line.startedAt).sort((a, b) => b.startedAt - a.startedAt)[0];
2619
+ return before?.role === "companion";
2620
+ }
2621
+ function countArm(batches, arm) {
2622
+ let seen = 0;
2623
+ const kept = [];
2624
+ for (const lines of batches) {
2625
+ const targets = lines.filter((l) => l.role === "target" && l.status !== "skipped" && l.status !== "interrupted").sort((a, b) => a.startedAt - b.startedAt || a.repeatEachIndex - b.repeatEachIndex);
2626
+ seen += targets.length;
2627
+ kept.push(...targets.filter((l) => roundKept(l, lines, arm.conditions)));
2628
+ }
2629
+ const count = {
2630
+ runs: 0,
2631
+ matchingFailures: 0,
2632
+ otherFailures: 0,
2633
+ discardedRounds: seen - kept.length,
2634
+ stoppedEarly: false,
2635
+ otherSignatures: []
2636
+ };
2637
+ const others = /* @__PURE__ */ new Map();
2638
+ for (const [i, line] of kept.entries()) {
2639
+ count.runs++;
2640
+ if (line.matchesHistory) count.matchingFailures++;
2641
+ else if (FAILED.has(line.status)) {
2642
+ count.otherFailures++;
2643
+ const signature = line.errorSignature ?? line.status;
2644
+ others.set(signature, (others.get(signature) ?? 0) + 1);
2645
+ }
2646
+ if (arm.stopAt != null && count.matchingFailures >= arm.stopAt) {
2647
+ count.stoppedEarly = i < kept.length - 1;
2648
+ break;
2649
+ }
2650
+ }
2651
+ count.otherSignatures = [...others.entries()].sort((a, b) => b[1] - a[1]).map(([s]) => s);
2652
+ return count;
2653
+ }
2654
+ function armDone(count, arm) {
2655
+ return arm.stopAt != null && count.matchingFailures >= arm.stopAt || count.runs >= arm.runs;
2656
+ }
2657
+ function nextBatch(count, arm) {
2658
+ const left = Math.max(0, arm.runs - count.runs);
2659
+ return arm.stopAt == null ? left : Math.min(left, FLAKE_BATCH_RUNS);
2660
+ }
2661
+ function escapeRegex(s) {
2662
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2663
+ }
2664
+ function grepFor(tests) {
2665
+ return tests.map((t) => `(^| )${escapeRegex([...t.suite, t.title].join(" "))}( @\\S+)*$`).join("|");
2666
+ }
2667
+ function playwrightArgs(test, arm, repeat) {
2668
+ const companion = workerCondition(arm.conditions);
2669
+ const tests = companion ? [test, companion.test] : [test];
2670
+ const files = [...new Set(tests.map((t) => t.file))];
2671
+ const args = [...files, "--grep", grepFor(tests), `--repeat-each=${repeat}`, "--retries=0"];
2672
+ const alongside = companion?.kind === "alongside";
2673
+ args.push(`--workers=${alongside ? 2 : 1}`);
2674
+ if (alongside && files.length === 1) args.push("--fully-parallel");
2675
+ const project = arm.conditions.find((c) => c.kind === "project");
2676
+ const name = project?.kind === "project" ? project.name : test.project;
2677
+ if (name) args.push(`--project=${name}`);
2678
+ return args;
2679
+ }
2680
+ function armVerdict(arm, control) {
2681
+ return flakeArmVerdict(arm, control);
2682
+ }
2683
+ function verifyVerdict(arm, reproducedRate) {
2684
+ if (arm.matchingFailures > 0) return "still-fails";
2685
+ return flakeFixVerified(arm, reproducedRate) ? "verified" : "inconclusive";
2686
+ }
2687
+ var EXIT_REPRODUCED = 0;
2688
+ var EXIT_NOT_REPRODUCED = 1;
2689
+ var EXIT_ERROR4 = 2;
2690
+ var EXIT_BISECT_SKIP = 125;
2691
+ function exitCodeFor(verdict) {
2692
+ return verdict === "reproduced" || verdict === "verified" ? EXIT_REPRODUCED : EXIT_NOT_REPRODUCED;
2693
+ }
2694
+ function bisectExitCode(verdict) {
2695
+ if (verdict === "verified") return EXIT_REPRODUCED;
2696
+ if (verdict === "still-fails") return EXIT_NOT_REPRODUCED;
2697
+ return EXIT_BISECT_SKIP;
2698
+ }
2699
+ function parseDuration(text) {
2700
+ const trimmed = text.trim().toLowerCase();
2701
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 6e4);
2702
+ const re = /(\d+(?:\.\d+)?)\s*(h|m|s)/g;
2703
+ let total = 0;
2704
+ let consumed = "";
2705
+ for (const m of trimmed.matchAll(re)) {
2706
+ total += Number(m[1]) * (m[2] === "h" ? 36e5 : m[2] === "m" ? 6e4 : 1e3);
2707
+ consumed += m[0];
2708
+ }
2709
+ return consumed.replace(/\s/g, "") === trimmed.replace(/\s/g, "") && total > 0 ? Math.round(total) : null;
2710
+ }
2711
+ function formatDuration(ms) {
2712
+ if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))} s`;
2713
+ const minutes = Math.round(ms / 6e4);
2714
+ if (minutes < 60) return `${minutes} min`;
2715
+ return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
2716
+ }
2717
+ function withinBudget(startedAt, now, budgetMs) {
2718
+ return now - startedAt < budgetMs;
2719
+ }
2720
+
2721
+ // src/internal/support/connection.ts
2722
+ var fs6 = __toESM(require("fs"));
2723
+ var path9 = __toESM(require("path"));
2724
+
2725
+ // ../core/src/dotenv.ts
2726
+ function parseDotEnv(text) {
2727
+ const out = {};
2728
+ for (const raw of text.split(/\r?\n/)) {
2729
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
2730
+ if (!m) continue;
2731
+ let value = m[2];
2732
+ const quote4 = value[0];
2733
+ if ((quote4 === '"' || quote4 === "'") && value.length > 1 && value.endsWith(quote4)) {
2734
+ value = value.slice(1, -1);
2735
+ if (quote4 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
2736
+ } else {
2737
+ value = value.replace(/\s+#.*$/, "");
2738
+ }
2739
+ out[m[1]] = value;
2740
+ }
2741
+ return out;
2742
+ }
2743
+ function resolvePiwiConnection(sources) {
2744
+ const own = {
2745
+ serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
2746
+ apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
2747
+ };
2748
+ const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
2749
+ const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
2750
+ const desktop = sources.desktop ?? null;
2751
+ const trim = (url) => url.replace(/\/+$/, "");
2752
+ const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
2753
+ const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
2754
+ if (!serverUrl) return null;
2755
+ const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
2756
+ return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
2757
+ }
2758
+
2759
+ // src/internal/support/connection.ts
2760
+ function resolveCliConnection(flags, env, dirs) {
2761
+ let dotEnv = {};
2762
+ for (const dir of dirs) {
2763
+ try {
2764
+ dotEnv = { ...parseDotEnv(fs6.readFileSync(path9.join(dir, ".env"), "utf-8")), ...dotEnv };
2765
+ } catch {
2766
+ }
2767
+ }
2768
+ return resolvePiwiConnection({
2769
+ flags,
2770
+ env,
2771
+ dotEnv,
2772
+ desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
2773
+ });
2774
+ }
2775
+
2776
+ // src/internal/support/playwright-spawn.ts
2777
+ var import_node_child_process3 = require("child_process");
2778
+ var fs7 = __toESM(require("fs"));
2779
+ var import_node_module2 = require("module");
2780
+ var path10 = __toESM(require("path"));
2781
+ function resolvePlaywrightCli2(cwd = process.cwd()) {
2782
+ const require2 = (0, import_node_module2.createRequire)(path10.join(cwd, "noop.js"));
2783
+ for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
2784
+ try {
2785
+ return require2.resolve(id);
2786
+ } catch {
2787
+ }
2788
+ }
2789
+ return null;
2790
+ }
2791
+ function spawnPlaywright(playwrightArgs2, env, options = {}) {
2792
+ const output = options.output ?? "inherit";
2793
+ let fd = output === "inherit" ? null : fs7.openSync(output.file, "a");
2794
+ const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
2795
+ const cli = resolvePlaywrightCli2();
2796
+ const child = cli ? (0, import_node_child_process3.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio, env }) : (0, import_node_child_process3.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs2], {
2797
+ stdio,
2798
+ env
2799
+ });
2800
+ return new Promise((resolve8) => {
2801
+ const done = (code) => {
2802
+ if (fd !== null) fs7.closeSync(fd);
2803
+ fd = null;
2804
+ resolve8(code);
2805
+ };
2806
+ child.on("error", (err) => {
2807
+ console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
2808
+ done(2);
2809
+ });
2810
+ child.on("exit", (code) => done(code ?? 2));
2811
+ });
2812
+ }
2813
+
2814
+ // src/cli/flake.ts
2815
+ var USAGE4 = `
2816
+ piwi flake \u2014 make a flaky test fail on demand, then prove the fix
2817
+
2818
+ Usage:
2819
+ npx @piwitests/reporter flake <test> [options]
2820
+ npx @piwitests/reporter flake verify <test> [--runs <n>] [--bisect] [--json]
2821
+
2822
+ <test> is a test case id (the number in its dashboard URL) or a spec file and
2823
+ line, tests/checkout.spec.ts:42, looked up in the project on the dashboard.
2824
+
2825
+ It fetches the test's plan from the dashboard: a control arm and one arm per
2826
+ suspect of its flake profile, most likely first. It runs the control (10 runs),
2827
+ then each arm (up to 10 runs, stopping at 3 failures with the same error as in
2828
+ CI), one test at a time with retries off, in the checkout it runs in. It prints
2829
+ each arm against the control with its verdict and saves the experiment on the
2830
+ test's Flakiness tab. The capture fixtures must be in use (piwiFixtures).
2831
+
2832
+ verify reruns the arm that last reproduced the test, and its control, until a
2833
+ matching failure appears or enough runs pass to say the fix holds.
2834
+
2835
+ verify --bisect runs that arm alone at this checkout and saves nothing, for
2836
+ git bisect run: exit 0 (good) when enough runs pass, 1 (bad) on a failure with
2837
+ the same error as in CI, 125 (skip) when this commit cannot tell.
2838
+
2839
+ Options:
2840
+ --suspect <n> Run only the arm of suspect n (its rank on the Flakiness tab)
2841
+ --all Also run every condition at once when none reproduces alone
2842
+ --runs <n> Runs of the control and of each arm (default 10; verify:
2843
+ the number that proves the fix at the rate it reproduced)
2844
+ --budget <duration> Start no new arm after this long: 15m (default), 90s, 1h
2845
+ --no-upload Keep the results off the dashboard; it still needs the plan
2846
+ from the dashboard, or from --plan when it cannot be reached
2847
+ --plan <file> Read the plan from a file (a saved flake-plan response, or
2848
+ the plan the plan_flake_experiment MCP tool returns) and run
2849
+ it without the dashboard; implies --no-upload
2850
+ --bisect verify only: one bisect step, as above; implies --no-upload
2851
+ --source <where> Where the lab runs, recorded on the experiment: cli, ci or
2852
+ desktop (default: ci when CI is set, else cli)
2853
+ --json Print the results as JSON instead of text
2854
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
2855
+ --api-key <key> Reporter API key (env PIWI_API_KEY, .env)
2856
+ --project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME)
2857
+
2858
+ Exit codes: 0 an arm reproduced the failure (verify: the fix held),
2859
+ 1 nothing reproduced (verify: it still fails, or too few runs),
2860
+ 2 error; with --bisect, 0 good, 1 bad, 125 skip, 2 error.
2861
+ `.trim();
2862
+ var DEFAULT_BUDGET_MS = 15 * 6e4;
2863
+ var SOURCES = ["cli", "ci", "desktop"];
2864
+ var MAX_BATCHES = 12;
2865
+ var LOG_TAIL_LINES = 40;
2866
+ function parseFlakeArgs(argv) {
2867
+ const args = {
2868
+ verify: argv[0] === "verify",
2869
+ test: null,
2870
+ suspect: null,
2871
+ all: false,
2872
+ runs: null,
2873
+ budgetMs: DEFAULT_BUDGET_MS,
2874
+ upload: true,
2875
+ planFile: null,
2876
+ bisect: false,
2877
+ source: null,
2878
+ json: false,
2879
+ help: false
2880
+ };
2881
+ const rest = args.verify ? argv.slice(1) : argv;
2882
+ const value = (i, name) => {
2883
+ const v = rest[i + 1];
2884
+ if (v === void 0 || v.startsWith("--")) throw new Error(`${name} needs a value`);
2885
+ return v;
2886
+ };
2887
+ const whole = (text, name, min, max) => {
2888
+ const n = Number(text);
2889
+ if (!Number.isInteger(n) || n < min || n > max)
2890
+ throw new Error(`${name} must be a whole number from ${min} to ${max}`);
2891
+ return n;
2892
+ };
2893
+ for (let i = 0; i < rest.length; i++) {
2894
+ const arg = rest[i];
2895
+ switch (arg) {
2896
+ case "-h":
2897
+ case "--help":
2898
+ args.help = true;
2899
+ break;
2900
+ case "--suspect":
2901
+ args.suspect = whole(value(i++, arg), arg, 1, 50);
2902
+ break;
2903
+ case "--all":
2904
+ args.all = true;
2905
+ break;
2906
+ case "--runs":
2907
+ args.runs = whole(value(i++, arg), arg, 1, 100);
2908
+ break;
2909
+ case "--budget": {
2910
+ const ms = parseDuration(value(i++, arg));
2911
+ if (ms == null) throw new Error("--budget must be a duration like 15m, 90s or 1h");
2912
+ args.budgetMs = ms;
2913
+ break;
2914
+ }
2915
+ case "--no-upload":
2916
+ args.upload = false;
2917
+ break;
2918
+ case "--plan":
2919
+ args.planFile = value(i++, arg);
2920
+ args.upload = false;
2921
+ break;
2922
+ case "--bisect":
2923
+ args.bisect = true;
2924
+ args.upload = false;
2925
+ break;
2926
+ case "--source": {
2927
+ const where = value(i++, arg);
2928
+ if (!SOURCES.includes(where))
2929
+ throw new Error(`--source must be one of ${SOURCES.join(", ")}`);
2930
+ args.source = where;
2931
+ break;
2932
+ }
2933
+ case "--json":
2934
+ args.json = true;
2935
+ break;
2936
+ case "--server-url":
2937
+ args.serverUrl = value(i++, arg);
2938
+ break;
2939
+ case "--api-key":
2940
+ args.apiKey = value(i++, arg);
2941
+ break;
2942
+ case "--project":
2943
+ args.project = value(i++, arg);
2944
+ break;
2945
+ default:
2946
+ if (arg.startsWith("-")) throw new Error(`unknown option ${arg}`);
2947
+ if (args.test !== null) throw new Error(`one test at a time (got "${args.test}" and "${arg}")`);
2948
+ args.test = arg;
2949
+ }
2950
+ }
2951
+ if (args.suspect !== null && args.all) throw new Error("--suspect and --all cannot be used together");
2952
+ if (args.verify && (args.suspect !== null || args.all))
2953
+ throw new Error("verify reruns one arm: drop --suspect and --all");
2954
+ if (args.bisect && !args.verify) throw new Error("--bisect is a verify option: flake verify <test> --bisect");
2955
+ return args;
2956
+ }
2957
+ function gitHead() {
2958
+ try {
2959
+ return (0, import_node_child_process4.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2960
+ } catch {
2961
+ return null;
2962
+ }
2963
+ }
2964
+ function commitsDiffer(a, b) {
2965
+ if (!a || !b) return false;
2966
+ const [x, y] = [a.toLowerCase(), b.toLowerCase()];
2967
+ return !x.startsWith(y) && !y.startsWith(x);
2968
+ }
2969
+ function short(sha) {
2970
+ return sha ? sha.slice(0, 7) : "unknown";
2971
+ }
2972
+ function fraction(count) {
2973
+ return `${count.matchingFailures}/${count.runs}`;
2974
+ }
2975
+ function formatP(p) {
2976
+ return p < 1e-3 ? "< 0.001" : `= ${p.toFixed(3)}`;
2977
+ }
2978
+ var FlakeError = class extends Error {
2979
+ };
2980
+ async function getJson(http, url) {
2981
+ const res = await fetch(`${http.serverUrl}${url}`, { headers: http.headers });
2982
+ if (!res.ok) {
2983
+ const body = await res.json().catch(() => ({}));
2984
+ throw new FlakeError(body.message || `the dashboard returned ${res.status} for ${url.split("?")[0]}`);
2985
+ }
2986
+ return await res.json();
2987
+ }
2988
+ async function resolveTestCase(http, test, project, apiKey) {
2989
+ if (/^\d+$/.test(test)) return Number(test);
2990
+ const m = /^(.+):(\d+)$/.exec(test);
2991
+ if (!m) throw new FlakeError(`"${test}" is neither a test case id nor file:line`);
2992
+ const projectId = await resolveProjectId({ serverUrl: http.serverUrl, apiKey, project, key: "" });
2993
+ const found = await getJson(
2994
+ http,
2995
+ `/api/projects/${projectId}/flake-lab/test?location=${encodeURIComponent(`${m[1].replace(/\\/g, "/")}:${m[2]}`)}`
2996
+ );
2997
+ return found.testCaseId;
2998
+ }
2999
+ function playwrightRunner(dir, log) {
3000
+ return {
3001
+ now: () => Date.now(),
3002
+ async runBatch(plan, arm, repeat, batch) {
3003
+ const planFile = path11.join(dir, `${arm.id}.plan.json`);
3004
+ if (!fs8.existsSync(planFile)) {
3005
+ fs8.writeFileSync(
3006
+ planFile,
3007
+ JSON.stringify({
3008
+ version: FLAKE_PLAN_VERSION,
3009
+ experimentId: plan.experimentId ?? `local-${process.pid}`,
3010
+ test: plan.test,
3011
+ arm: { id: arm.id, conditions: arm.conditions },
3012
+ errorSignatures: plan.errorSignatures
3013
+ })
3014
+ );
3015
+ }
3016
+ const resultsFile = path11.join(dir, `${arm.id}.${batch}.jsonl`);
3017
+ fs8.writeFileSync(resultsFile, "");
3018
+ const env = {
3019
+ ...process.env,
3020
+ [PIWI_FLAKE_ENV.plan]: planFile,
3021
+ [PIWI_FLAKE_ENV.results]: resultsFile
3022
+ };
3023
+ delete env[PIWI_PROBE_ENV.flag];
3024
+ fs8.appendFileSync(log, `
3025
+ \u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
3026
+ `);
3027
+ await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
3028
+ output: { file: log },
3029
+ command: "piwi flake"
3030
+ });
3031
+ return readResults(resultsFile);
3032
+ }
3033
+ };
3034
+ }
3035
+ function readResults(file) {
3036
+ const lines = [];
3037
+ for (const line of fs8.readFileSync(file, "utf8").split("\n")) {
3038
+ if (!line.trim()) continue;
3039
+ try {
3040
+ lines.push(JSON.parse(line));
3041
+ } catch {
3042
+ }
3043
+ }
3044
+ return lines;
3045
+ }
3046
+ async function runArm(runner, plan, arm) {
3047
+ const batches = [];
3048
+ let count = countArm(batches, arm);
3049
+ for (let batch = 1; batch <= MAX_BATCHES && !armDone(count, arm); batch++) {
3050
+ const repeat = nextBatch(count, arm);
3051
+ if (repeat <= 0) break;
3052
+ const lines = await runner.runBatch(plan, arm, repeat, batch);
3053
+ if (!lines.some((l) => l.role === "target")) {
3054
+ throw new FlakeError(
3055
+ `the ${arm.id} arm recorded no attempt of "${plan.displayTitle}" in ${plan.test.file}. Is the test in this checkout, and does its spec use the Piwi capture fixtures?`
3056
+ );
3057
+ }
3058
+ batches.push(lines);
3059
+ count = countArm(batches, arm);
3060
+ }
3061
+ return count;
3062
+ }
3063
+ async function runSession(runner, plan, args, onArm = () => {
3064
+ }) {
3065
+ const started = runner.now();
3066
+ const control = args.bisect ? { arm: plan.control, count: countArm([], plan.control), verdict: null, pValue: null, skipped: "bisect step" } : {
3067
+ arm: plan.control,
3068
+ count: await runArm(runner, plan, plan.control),
3069
+ verdict: null,
3070
+ pValue: null,
3071
+ skipped: null
3072
+ };
3073
+ onArm(control, null);
3074
+ const arms = args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms;
3075
+ const results = [];
3076
+ const run = async (arm) => {
3077
+ if (!withinBudget(started, runner.now(), args.budgetMs)) {
3078
+ const result2 = { arm, count: countArm([], arm), verdict: null, pValue: null, skipped: "budget spent" };
3079
+ results.push(result2);
3080
+ onArm(result2, control.count);
3081
+ return;
3082
+ }
3083
+ const count = await runArm(runner, plan, arm);
3084
+ let verdict;
3085
+ let pValue = null;
3086
+ if (plan.kind === "verify") {
3087
+ verdict = verifyVerdict(count, plan.verifies?.rate ?? 1);
3088
+ } else {
3089
+ const v = armVerdict(count, control.count);
3090
+ verdict = v.verdict;
3091
+ pValue = v.pValue;
3092
+ }
3093
+ const result = { arm, count, verdict, pValue, skipped: null };
3094
+ results.push(result);
3095
+ onArm(result, control.count);
3096
+ };
3097
+ for (const arm of arms) await run(arm);
3098
+ if (args.all && plan.combined && !results.some((r) => r.verdict === "reproduced")) await run(plan.combined);
3099
+ return { control, arms: results };
3100
+ }
3101
+ function sessionVerdict(kind, arms) {
3102
+ if (kind === "verify") {
3103
+ const v = arms.find((a) => a.arm.id === "verify");
3104
+ return {
3105
+ verdict: v?.verdict ?? "inconclusive",
3106
+ reproducingArm: v ? "verify" : null
3107
+ };
3108
+ }
3109
+ const reproduced = arms.find((a) => a.verdict === "reproduced");
3110
+ if (reproduced) return { verdict: "reproduced", reproducingArm: reproduced.arm.id };
3111
+ return {
3112
+ verdict: arms.some((a) => a.verdict === "amplified") ? "amplified" : "not-reproduced",
3113
+ reproducingArm: null
3114
+ };
3115
+ }
3116
+ function pad(text, width) {
3117
+ return text.length >= width ? `${text} ` : text + " ".repeat(width - text.length);
3118
+ }
3119
+ function clip(text, width) {
3120
+ return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text;
3121
+ }
3122
+ function armLine(result, stopAt) {
3123
+ const { arm, count } = result;
3124
+ const name = arm.id === "control" ? "control" : `${arm.rank != null ? `${arm.rank} ` : " "}${arm.label}`;
3125
+ if (result.skipped) return ` ${pad(clip(name, 34), 35)}${result.skipped}`;
3126
+ const notes = [];
3127
+ if (count.stoppedEarly && stopAt != null) notes.push(`stopped at ${stopAt}`);
3128
+ if (count.matchingFailures > 0) notes.push("same error as in CI");
3129
+ if (count.otherFailures > 0)
3130
+ notes.push(`${count.otherFailures} different error${count.otherFailures === 1 ? "" : "s"} (counted apart)`);
3131
+ if (count.discardedRounds > 0) {
3132
+ notes.push(`${count.discardedRounds} round${count.discardedRounds === 1 ? "" : "s"} left out`);
3133
+ }
3134
+ const verdict = result.verdict ? result.verdict.replace("-", " ") : "";
3135
+ return ` ${pad(clip(name, 34), 35)}${pad(fraction(count), 7)}${pad(notes.join(" \xB7 "), 44)}${verdict}`.trimEnd();
3136
+ }
3137
+ function verdictSentence(report, arms, control) {
3138
+ const c = fraction(control.count);
3139
+ const against = control.skipped ? "" : ` (control ${c})`;
3140
+ if (report.kind === "verify") {
3141
+ const v = arms.find((a) => a.arm.id === "verify");
3142
+ if (!v) return "Verdict: the verify arm did not run.";
3143
+ const label = v.arm.label;
3144
+ if (report.verdict === "verified") {
3145
+ return `Verdict: fix verified: 0 matching failures in ${v.count.runs} runs under ${label}${against}`;
3146
+ }
3147
+ if (report.verdict === "still-fails") {
3148
+ return `Verdict: still fails: ${fraction(v.count)} under ${label} failed as in CI${against}`;
3149
+ }
3150
+ return `Verdict: inconclusive: ${v.count.runs} clean runs under ${label}, too few to verify the fix`;
3151
+ }
3152
+ const r = arms.find((a) => a.arm.id === report.reproducingArm);
3153
+ if (r) {
3154
+ return `Verdict: reproduced by ${r.arm.label} (${fraction(r.count)} against ${c}, p ${formatP(r.pValue ?? 1)})`;
3155
+ }
3156
+ const amplified = arms.find((a) => a.verdict === "amplified");
3157
+ if (amplified) {
3158
+ return `Verdict: not reproduced; ${amplified.arm.label} made it fail more often (${fraction(amplified.count)} against ${c}, p ${formatP(amplified.pValue ?? 1)})`;
3159
+ }
3160
+ return `Verdict: not reproduced (control ${c})`;
3161
+ }
3162
+ function bisectStepWord(code) {
3163
+ if (code === 0) return "good (the arm held at this commit)";
3164
+ if (code === 1) return "bad (a failure with the same error as in CI)";
3165
+ return "skip (too few clean runs to tell)";
3166
+ }
3167
+ function printHeader(plan, commit, estimate, budgetMs) {
3168
+ const out = [];
3169
+ out.push(
3170
+ `piwi flake${plan.kind === "verify" ? " verify" : ""} \xB7 ${plan.displayTitle} \xB7 ${plan.failures} failures in ${plan.windowDays} days`
3171
+ );
3172
+ out.push("");
3173
+ if (plan.kind === "verify" && plan.verifies) {
3174
+ out.push(
3175
+ `Rerunning ${plan.verifies.label}: it reproduced ${Math.round(plan.verifies.rate * 100)}% of runs on ${short(plan.verifies.commit)}`
3176
+ );
3177
+ } else if (plan.suspects.length === 0) {
3178
+ out.push("Suspects from history: none. Nothing in its history separates its failures from its passes.");
3179
+ } else {
3180
+ out.push(`${pad("Suspects from history", 57)}condition`);
3181
+ for (const s of plan.suspects) {
3182
+ const counts = `${s.counts.failuresWith}/${s.counts.failures} failures \xB7 ${s.counts.passesWith}/${s.counts.passes} passes`;
3183
+ out.push(
3184
+ ` ${s.rank} ${pad(clip(s.label, 40), 41)}${pad(counts, 30)}${s.skipped ? `none: ${s.skipped}` : s.conditionLabel}`
3185
+ );
3186
+ if (s.sharedRoutes?.length) out.push(` both write ${s.sharedRoutes.join(", ")}`);
3187
+ }
3188
+ }
3189
+ out.push("");
3190
+ const estimateText = estimate != null ? `up to ${formatDuration(estimate)}` : "unknown (no passing run on record)";
3191
+ out.push(`Estimate: ${estimateText} \xB7 budget ${formatDuration(budgetMs)}`);
3192
+ out.push(`Lab \xB7 ${os2.hostname()} \xB7 HEAD ${short(commit)} \xB7 ${plan.test.project ?? "default project"} \xB7 retries off`);
3193
+ if (commitsDiffer(commit, plan.failureCommit)) {
3194
+ out.push(`warning: this checkout is ${short(commit)}; the latest failure ran on ${short(plan.failureCommit)}.`);
3195
+ }
3196
+ console.log(out.join("\n"));
3197
+ }
3198
+ async function loadPlan(args, commit) {
3199
+ if (args.planFile) {
3200
+ try {
3201
+ const raw = JSON.parse(fs8.readFileSync(args.planFile, "utf8"));
3202
+ const plan = "plan" in raw && raw.plan ? raw.plan : raw;
3203
+ if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
3204
+ if (args.verify !== (plan.kind === "verify")) {
3205
+ throw new Error(`it is a ${plan.kind} plan; run ${plan.kind === "verify" ? "flake verify" : "flake"} with it`);
3206
+ }
3207
+ return { plan: args.runs ? withRuns(plan, args.runs) : plan, http: null };
3208
+ } catch (error) {
3209
+ throw new FlakeError(`cannot read the plan ${args.planFile}: ${error.message}`);
3210
+ }
3211
+ }
3212
+ if (!args.test) throw new FlakeError("name the test: a test case id or file:line");
3213
+ const connection = resolveCliConnection(
3214
+ { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
3215
+ process.env,
3216
+ [process.cwd()]
3217
+ );
3218
+ if (!connection) {
3219
+ throw new FlakeError(
3220
+ "no dashboard to read the plan from: pass --server-url or set PIWI_DASHBOARD_URL (or run it offline with --plan <file>)"
3221
+ );
3222
+ }
3223
+ const http = {
3224
+ serverUrl: connection.serverUrl,
3225
+ headers: connection.apiKey ? { "X-API-Key": connection.apiKey } : {}
3226
+ };
3227
+ try {
3228
+ const testCaseId = await resolveTestCase(http, args.test, connection.project, connection.apiKey);
3229
+ const params = new URLSearchParams({
3230
+ kind: args.verify ? "verify" : "reproduce",
3231
+ source: args.source ?? (process.env.CI ? "ci" : "cli"),
3232
+ machine: os2.hostname(),
3233
+ record: String(args.upload)
3234
+ });
3235
+ if (commit) params.set("commit", commit);
3236
+ if (args.runs) params.set("runs", String(args.runs));
3237
+ return { plan: await getJson(http, `/api/test-cases/${testCaseId}/flake-plan?${params}`), http };
3238
+ } catch (error) {
3239
+ if (error instanceof FlakeError) throw error;
3240
+ throw new FlakeError(
3241
+ `could not reach the dashboard at ${connection.serverUrl} (${error.message}). The plan comes from the dashboard; to run offline, pass --plan <file> with a plan saved earlier.`
3242
+ );
3243
+ }
3244
+ }
3245
+ function withRuns(plan, runs) {
3246
+ const set = (arm) => ({ ...arm, runs });
3247
+ return {
3248
+ ...plan,
3249
+ control: set(plan.control),
3250
+ arms: plan.arms.map(set),
3251
+ combined: plan.combined && set(plan.combined)
3252
+ };
3253
+ }
3254
+ async function runFlake(argv, runner) {
3255
+ let args;
3256
+ try {
3257
+ args = parseFlakeArgs(argv);
3258
+ } catch (error) {
3259
+ console.error(`piwi flake: ${error.message}
3260
+ `);
3261
+ console.error(USAGE4);
3262
+ return EXIT_ERROR4;
3263
+ }
3264
+ if (args.help) {
3265
+ console.log(USAGE4);
3266
+ return 0;
3267
+ }
3268
+ const commit = gitHead();
3269
+ let plan;
3270
+ let http;
3271
+ try {
3272
+ ({ plan, http } = await loadPlan(args, commit));
3273
+ } catch (error) {
3274
+ console.error(`piwi flake: ${error.message}`);
3275
+ return EXIT_ERROR4;
3276
+ }
3277
+ if (args.suspect != null && !plan.arms.some((a) => a.rank === args.suspect)) {
3278
+ console.error(`piwi flake: suspect ${args.suspect} has no arm (${plan.arms.length} arm(s) in the plan).`);
3279
+ return EXIT_ERROR4;
3280
+ }
3281
+ if (plan.kind === "reproduce" && plan.arms.length === 0) {
3282
+ if (!args.json) printHeader(plan, commit, null, args.budgetMs);
3283
+ console.error("piwi flake: no suspect with a condition to test, so there is nothing to run.");
3284
+ return 1;
3285
+ }
3286
+ const planned = [
3287
+ ...args.bisect ? [] : [plan.control],
3288
+ ...args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms
3289
+ ];
3290
+ if (args.all && plan.combined) planned.push(plan.combined);
3291
+ const estimate = estimateFlakeSessionMs(planned, plan.medianDurationMs);
3292
+ if (!args.json) {
3293
+ printHeader(plan, commit, estimate, args.budgetMs);
3294
+ }
3295
+ const dir = fs8.mkdtempSync(path11.join(os2.tmpdir(), "piwi-flake-"));
3296
+ const log = path11.join(dir, "playwright.log");
3297
+ let session;
3298
+ try {
3299
+ session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
3300
+ if (!args.json && !result.skipped?.startsWith("bisect")) console.log(armLine(result, result.arm.stopAt));
3301
+ });
3302
+ } catch (error) {
3303
+ console.error(`piwi flake: ${error.message}`);
3304
+ if (fs8.existsSync(log)) {
3305
+ const tail = fs8.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
3306
+ console.error(`
3307
+ Playwright's output (${log}):
3308
+ ${tail}`);
3309
+ }
3310
+ if (args.bisect) {
3311
+ console.log("Bisect step: skip (this commit could not run the arm)");
3312
+ return EXIT_BISECT_SKIP;
3313
+ }
3314
+ return EXIT_ERROR4;
3315
+ }
3316
+ const { verdict, reproducingArm } = sessionVerdict(plan.kind, session.arms);
3317
+ const ran = [session.control, ...session.arms.filter((a) => !a.skipped)];
3318
+ let uploaded = false;
3319
+ if (args.upload && http && plan.experimentId) {
3320
+ try {
3321
+ const res = await fetch(`${http.serverUrl}/api/projects/${plan.projectId}/flake-lab/results`, {
3322
+ method: "POST",
3323
+ headers: { "Content-Type": "application/json", ...http.headers },
3324
+ body: JSON.stringify({
3325
+ experimentId: plan.experimentId,
3326
+ commit,
3327
+ playwrightProject: plan.test.project,
3328
+ arms: ran.map((r) => ({
3329
+ id: r.arm.id,
3330
+ label: r.arm.label,
3331
+ suspectId: r.arm.suspectId,
3332
+ conditions: r.arm.conditions,
3333
+ runs: r.count.runs,
3334
+ matchingFailures: r.count.matchingFailures,
3335
+ otherFailures: r.count.otherFailures,
3336
+ discardedRounds: r.count.discardedRounds,
3337
+ stoppedEarly: r.count.stoppedEarly
3338
+ }))
3339
+ })
3340
+ });
3341
+ if (!res.ok) {
3342
+ const body = await res.json().catch(() => ({}));
3343
+ throw new Error(body.message || `the dashboard returned ${res.status}`);
3344
+ }
3345
+ uploaded = true;
3346
+ } catch (error) {
3347
+ console.error(`piwi flake: could not save the results \u2014 ${error.message}`);
3348
+ }
3349
+ }
3350
+ const code = args.bisect ? bisectExitCode(verdict) : exitCodeFor(verdict);
3351
+ const verifyCommand = `npx @piwitests/reporter flake verify ${plan.testCaseId}`;
3352
+ const report = {
3353
+ kind: plan.kind,
3354
+ testCaseId: plan.testCaseId,
3355
+ test: plan.displayTitle,
3356
+ experimentId: plan.experimentId,
3357
+ commit,
3358
+ failureCommit: plan.failureCommit,
3359
+ commitsDiffer: commitsDiffer(commit, plan.failureCommit),
3360
+ playwrightProject: plan.test.project,
3361
+ estimateMs: estimate,
3362
+ arms: [session.control, ...session.arms].map((r) => ({
3363
+ id: r.arm.id,
3364
+ label: r.arm.label,
3365
+ suspectId: r.arm.suspectId,
3366
+ rank: r.arm.rank,
3367
+ ...r.count,
3368
+ verdict: r.verdict,
3369
+ pValue: r.pValue,
3370
+ skipped: r.skipped
3371
+ })),
3372
+ verdict,
3373
+ reproducingArm,
3374
+ uploaded,
3375
+ verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
3376
+ exitCode: code
3377
+ };
3378
+ fs8.rmSync(dir, { recursive: true, force: true });
3379
+ if (args.json) {
3380
+ console.log(JSON.stringify(report, null, 2));
3381
+ return code;
3382
+ }
3383
+ const lines = [verdictSentence(report, session.arms, session.control)];
3384
+ if (args.bisect) lines.push(`Bisect step: ${bisectStepWord(code)}`);
3385
+ if (uploaded)
3386
+ lines.push(`Saved to the test's Flakiness tab.${report.verifyCommand ? ` After your fix: ${verifyCommand}` : ""}`);
3387
+ else if (report.verifyCommand) lines.push(`Not saved (--no-upload). After your fix: ${verifyCommand}`);
3388
+ console.log(lines.join("\n"));
3389
+ return code;
3390
+ }
3391
+
3392
+ // src/cli/gate.ts
3393
+ var fs9 = __toESM(require("fs"));
2383
3394
 
2384
3395
  // ../core/src/gate.ts
2385
3396
  function formatGateResult(result) {
@@ -2402,8 +3413,8 @@ function formatGateResult(result) {
2402
3413
  // src/cli/gate.ts
2403
3414
  var EXIT_OK4 = 0;
2404
3415
  var EXIT_VIOLATED = 1;
2405
- var EXIT_ERROR4 = 2;
2406
- var USAGE4 = `
3416
+ var EXIT_ERROR5 = 2;
3417
+ var USAGE5 = `
2407
3418
  piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
2408
3419
 
2409
3420
  Usage:
@@ -2453,9 +3464,9 @@ function readCount(argv, name) {
2453
3464
  if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
2454
3465
  return Math.floor(n);
2455
3466
  }
2456
- function readRunIdFromFile(path16) {
3467
+ function readRunIdFromFile(path19) {
2457
3468
  try {
2458
- const parsed = JSON.parse(fs5.readFileSync(path16, "utf-8"));
3469
+ const parsed = JSON.parse(fs9.readFileSync(path19, "utf-8"));
2459
3470
  const runId = Number(parsed.runId);
2460
3471
  return Number.isFinite(runId) && runId > 0 ? runId : null;
2461
3472
  } catch {
@@ -2510,7 +3521,7 @@ async function requestGate(args) {
2510
3521
  }
2511
3522
  async function runGate(argv, env = process.env) {
2512
3523
  if (argv.includes("-h") || argv.includes("--help")) {
2513
- console.log(USAGE4);
3524
+ console.log(USAGE5);
2514
3525
  return EXIT_OK4;
2515
3526
  }
2516
3527
  let args;
@@ -2519,15 +3530,15 @@ async function runGate(argv, env = process.env) {
2519
3530
  } catch (e) {
2520
3531
  console.error(`piwi gate: ${e.message}
2521
3532
  `);
2522
- console.error(USAGE4);
2523
- return EXIT_ERROR4;
3533
+ console.error(USAGE5);
3534
+ return EXIT_ERROR5;
2524
3535
  }
2525
3536
  let result;
2526
3537
  try {
2527
3538
  result = await requestGate(args);
2528
3539
  } catch (e) {
2529
3540
  console.error(`piwi gate: ${e.message}`);
2530
- return EXIT_ERROR4;
3541
+ return EXIT_ERROR5;
2531
3542
  }
2532
3543
  if (argv.includes("--json")) {
2533
3544
  console.log(JSON.stringify(result, null, 2));
@@ -2538,9 +3549,9 @@ async function runGate(argv, env = process.env) {
2538
3549
  }
2539
3550
 
2540
3551
  // src/cli/init.ts
2541
- var fs8 = __toESM(require("fs"));
2542
- var path10 = __toESM(require("path"));
2543
- var import_node_child_process3 = require("child_process");
3552
+ var fs12 = __toESM(require("fs"));
3553
+ var path14 = __toESM(require("path"));
3554
+ var import_node_child_process5 = require("child_process");
2544
3555
 
2545
3556
  // src/cli/report.ts
2546
3557
  var STATUS_MARK = {
@@ -2557,8 +3568,8 @@ function formatStep(result) {
2557
3568
  }
2558
3569
 
2559
3570
  // src/cli/detect.ts
2560
- var fs6 = __toESM(require("fs"));
2561
- var path8 = __toESM(require("path"));
3571
+ var fs10 = __toESM(require("fs"));
3572
+ var path12 = __toESM(require("path"));
2562
3573
  var CONFIG_NAMES = [
2563
3574
  "playwright.config.ts",
2564
3575
  "playwright.config.mts",
@@ -2576,9 +3587,9 @@ var LOCKFILES = [
2576
3587
  ];
2577
3588
  var REPORTER_PACKAGE = "@piwitests/reporter";
2578
3589
  function readPackageJson(root) {
2579
- const pkgPath = path8.join(root, "package.json");
3590
+ const pkgPath = path12.join(root, "package.json");
2580
3591
  try {
2581
- return { pkg: JSON.parse(fs6.readFileSync(pkgPath, "utf-8")), pkgPath };
3592
+ return { pkg: JSON.parse(fs10.readFileSync(pkgPath, "utf-8")), pkgPath };
2582
3593
  } catch {
2583
3594
  return { pkg: null, pkgPath: null };
2584
3595
  }
@@ -2587,14 +3598,14 @@ function detectPackageManager(root, pkg) {
2587
3598
  const declared = pkg?.packageManager?.split("@")[0];
2588
3599
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
2589
3600
  for (const [file, manager] of LOCKFILES) {
2590
- if (fs6.existsSync(path8.join(root, file))) return manager;
3601
+ if (fs10.existsSync(path12.join(root, file))) return manager;
2591
3602
  }
2592
3603
  return "npm";
2593
3604
  }
2594
3605
  function findConfig(root) {
2595
3606
  for (const name of CONFIG_NAMES) {
2596
- const candidate = path8.join(root, name);
2597
- if (fs6.existsSync(candidate)) return candidate;
3607
+ const candidate = path12.join(root, name);
3608
+ if (fs10.existsSync(candidate)) return candidate;
2598
3609
  }
2599
3610
  return null;
2600
3611
  }
@@ -2604,14 +3615,14 @@ function langOf(configPath) {
2604
3615
  }
2605
3616
  function suggestProjectName(root, pkg) {
2606
3617
  const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
2607
- return fromPkg || path8.basename(root) || "default-project";
3618
+ return fromPkg || path12.basename(root) || "default-project";
2608
3619
  }
2609
3620
  function hasReporter(pkg) {
2610
3621
  if (!pkg) return false;
2611
3622
  return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
2612
3623
  }
2613
3624
  function detectProject(root) {
2614
- const absRoot = path8.resolve(root);
3625
+ const absRoot = path12.resolve(root);
2615
3626
  const { pkg, pkgPath } = readPackageJson(absRoot);
2616
3627
  const configPath = findConfig(absRoot);
2617
3628
  return {
@@ -2753,8 +3764,8 @@ function ensureGitignoreEntry(existing, entry = ".env") {
2753
3764
  }
2754
3765
 
2755
3766
  // src/cli/skills.ts
2756
- var fs7 = __toESM(require("fs"));
2757
- var path9 = __toESM(require("path"));
3767
+ var fs11 = __toESM(require("fs"));
3768
+ var path13 = __toESM(require("path"));
2758
3769
  var SETUP_SKILL = "setup-piwi";
2759
3770
  var WORKFLOW_SKILLS = [
2760
3771
  "investigate-failure",
@@ -2765,16 +3776,16 @@ var WORKFLOW_SKILLS = [
2765
3776
  "fix-a-reported-bug"
2766
3777
  ];
2767
3778
  var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
2768
- var DEFAULT_SKILLS_DIR = path9.join(".claude", "skills");
3779
+ var DEFAULT_SKILLS_DIR = path13.join(".claude", "skills");
2769
3780
  function findTemplatesDir(fromDir) {
2770
3781
  let dir = fromDir;
2771
3782
  for (let i = 0; i < 6; i++) {
2772
- if (fs7.existsSync(path9.join(dir, "templates", "skills"))) return path9.join(dir, "templates");
2773
- const parent = path9.dirname(dir);
3783
+ if (fs11.existsSync(path13.join(dir, "templates", "skills"))) return path13.join(dir, "templates");
3784
+ const parent = path13.dirname(dir);
2774
3785
  if (parent === dir) break;
2775
3786
  dir = parent;
2776
3787
  }
2777
- return path9.resolve(fromDir, "..", "..", "templates");
3788
+ return path13.resolve(fromDir, "..", "..", "templates");
2778
3789
  }
2779
3790
  function readFrontMatter(markdown) {
2780
3791
  const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
@@ -2787,14 +3798,14 @@ function readFrontMatter(markdown) {
2787
3798
  return out;
2788
3799
  }
2789
3800
  function templatePath(templatesDir, slug) {
2790
- return path9.join(templatesDir, "skills", slug, "SKILL.md");
3801
+ return path13.join(templatesDir, "skills", slug, "SKILL.md");
2791
3802
  }
2792
3803
  function listSkills(templatesDir, slugs = ALL_SKILLS) {
2793
3804
  const infos = [];
2794
3805
  for (const slug of slugs) {
2795
3806
  const file = templatePath(templatesDir, slug);
2796
- if (!fs7.existsSync(file)) continue;
2797
- const front = readFrontMatter(fs7.readFileSync(file, "utf-8"));
3807
+ if (!fs11.existsSync(file)) continue;
3808
+ const front = readFrontMatter(fs11.readFileSync(file, "utf-8"));
2798
3809
  infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
2799
3810
  }
2800
3811
  return infos;
@@ -2804,16 +3815,16 @@ function installSkills(opts) {
2804
3815
  for (const slug of opts.slugs) {
2805
3816
  const step = `skill:${slug}`;
2806
3817
  const source = templatePath(opts.templatesDir, slug);
2807
- const relDest = path9.join(opts.skillsDir, slug, "SKILL.md");
2808
- const dest = path9.join(opts.root, relDest);
2809
- if (!fs7.existsSync(source)) {
3818
+ const relDest = path13.join(opts.skillsDir, slug, "SKILL.md");
3819
+ const dest = path13.join(opts.root, relDest);
3820
+ if (!fs11.existsSync(source)) {
2810
3821
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
2811
3822
  continue;
2812
3823
  }
2813
- const contents = fs7.readFileSync(source, "utf-8");
2814
- const exists = fs7.existsSync(dest);
3824
+ const contents = fs11.readFileSync(source, "utf-8");
3825
+ const exists = fs11.existsSync(dest);
2815
3826
  if (exists && !opts.force) {
2816
- const identical = fs7.readFileSync(dest, "utf-8") === contents;
3827
+ const identical = fs11.readFileSync(dest, "utf-8") === contents;
2817
3828
  results.push({
2818
3829
  step,
2819
3830
  file: relDest,
@@ -2823,14 +3834,14 @@ function installSkills(opts) {
2823
3834
  continue;
2824
3835
  }
2825
3836
  if (!opts.dryRun) {
2826
- fs7.mkdirSync(path9.dirname(dest), { recursive: true });
2827
- fs7.writeFileSync(dest, contents);
3837
+ fs11.mkdirSync(path13.dirname(dest), { recursive: true });
3838
+ fs11.writeFileSync(dest, contents);
2828
3839
  }
2829
3840
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail: "installed skill" });
2830
3841
  }
2831
3842
  return results;
2832
3843
  }
2833
- var USAGE5 = `
3844
+ var USAGE6 = `
2834
3845
  piwi skills \u2014 install the Piwi agent skills into this project
2835
3846
 
2836
3847
  Usage:
@@ -2869,7 +3880,7 @@ function positionalSlugs(argv) {
2869
3880
  function runSkills(argv, templatesDir, cwd = process.cwd()) {
2870
3881
  const [sub, ...rest] = argv;
2871
3882
  if (sub === void 0 || sub === "-h" || sub === "--help") {
2872
- console.log(USAGE5);
3883
+ console.log(USAGE6);
2873
3884
  return 0;
2874
3885
  }
2875
3886
  if (sub === "list") {
@@ -2890,12 +3901,12 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
2890
3901
  if (unknown.length) {
2891
3902
  console.error(`piwi skills: unknown skill(s): ${unknown.join(", ")}
2892
3903
  `);
2893
- console.error(USAGE5);
3904
+ console.error(USAGE6);
2894
3905
  return 2;
2895
3906
  }
2896
3907
  const results = installSkills({
2897
3908
  templatesDir,
2898
- root: path9.resolve(readOption5(rest, "--cwd") ?? cwd),
3909
+ root: path13.resolve(readOption5(rest, "--cwd") ?? cwd),
2899
3910
  skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
2900
3911
  slugs: requested.length ? requested : ALL_SKILLS,
2901
3912
  force: rest.includes("--force"),
@@ -2911,13 +3922,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
2911
3922
  }
2912
3923
  console.error(`piwi skills: unknown command "${sub}"
2913
3924
  `);
2914
- console.error(USAGE5);
3925
+ console.error(USAGE6);
2915
3926
  return 2;
2916
3927
  }
2917
3928
 
2918
3929
  // src/cli/init.ts
2919
3930
  var DEFAULT_SERVER_URL = "http://localhost:3000";
2920
- var USAGE6 = `
3931
+ var USAGE7 = `
2921
3932
  piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
2922
3933
 
2923
3934
  Usage:
@@ -2966,7 +3977,7 @@ function resolveSkillSlugs(raw) {
2966
3977
  return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
2967
3978
  }
2968
3979
  function parseInitArgs(argv, env, cwd) {
2969
- const root = path10.resolve(readOption6(argv, "--cwd") ?? cwd);
3980
+ const root = path14.resolve(readOption6(argv, "--cwd") ?? cwd);
2970
3981
  const detected = detectProject(root);
2971
3982
  return {
2972
3983
  root,
@@ -2985,7 +3996,7 @@ function parseInitArgs(argv, env, cwd) {
2985
3996
  }
2986
3997
  function readFileOr(file, fallback) {
2987
3998
  try {
2988
- return fs8.readFileSync(file, "utf-8");
3999
+ return fs12.readFileSync(file, "utf-8");
2989
4000
  } catch {
2990
4001
  return fallback;
2991
4002
  }
@@ -3015,7 +4026,7 @@ function stepInstallReporter(project, opts) {
3015
4026
  if (opts.dryRun || !opts.install)
3016
4027
  return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
3017
4028
  const [command, args] = installArgv(project.packageManager);
3018
- const result = (0, import_node_child_process3.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
4029
+ const result = (0, import_node_child_process5.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
3019
4030
  if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
3020
4031
  return {
3021
4032
  step,
@@ -3031,19 +4042,19 @@ function stepConfig(project, opts) {
3031
4042
  status: "manual",
3032
4043
  detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
3033
4044
  };
3034
- const rel = path10.relative(project.root, project.configPath) || path10.basename(project.configPath);
4045
+ const rel = path14.relative(project.root, project.configPath) || path14.basename(project.configPath);
3035
4046
  const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
3036
4047
  serverUrl: opts.serverUrl,
3037
4048
  projectName: opts.projectName
3038
4049
  });
3039
- if (edit.status === "updated" && !opts.dryRun) fs8.writeFileSync(project.configPath, edit.text);
4050
+ if (edit.status === "updated" && !opts.dryRun) fs12.writeFileSync(project.configPath, edit.text);
3040
4051
  return { step, file: rel, status: edit.status, detail: edit.detail };
3041
4052
  }
3042
4053
  function stepFixtures(project, opts) {
3043
4054
  const step = "fixtures";
3044
- const rel = path10.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
3045
- const dest = path10.join(project.root, rel);
3046
- if (fs8.existsSync(dest)) {
4055
+ const rel = path14.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
4056
+ const dest = path14.join(project.root, rel);
4057
+ if (fs12.existsSync(dest)) {
3047
4058
  const current = readFileOr(dest, "");
3048
4059
  if (/piwiFixtures/.test(current))
3049
4060
  return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
@@ -3055,8 +4066,8 @@ function stepFixtures(project, opts) {
3055
4066
  };
3056
4067
  }
3057
4068
  if (!opts.dryRun) {
3058
- fs8.mkdirSync(path10.dirname(dest), { recursive: true });
3059
- fs8.writeFileSync(dest, fixturesContents());
4069
+ fs12.mkdirSync(path14.dirname(dest), { recursive: true });
4070
+ fs12.writeFileSync(dest, fixturesContents());
3060
4071
  }
3061
4072
  return {
3062
4073
  step,
@@ -3067,12 +4078,12 @@ function stepFixtures(project, opts) {
3067
4078
  }
3068
4079
  function stepEnv(project, opts) {
3069
4080
  const results = [];
3070
- const examplePath = path10.join(project.root, ".env.example");
4081
+ const examplePath = path14.join(project.root, ".env.example");
3071
4082
  const example = upsertEnvKeys(readFileOr(examplePath, ""), [
3072
4083
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
3073
4084
  ["PIWI_API_KEY", ""]
3074
4085
  ]);
3075
- if (example.added.length && !opts.dryRun) fs8.writeFileSync(examplePath, example.text);
4086
+ if (example.added.length && !opts.dryRun) fs12.writeFileSync(examplePath, example.text);
3076
4087
  results.push({
3077
4088
  step: "env",
3078
4089
  file: ".env.example",
@@ -3080,21 +4091,21 @@ function stepEnv(project, opts) {
3080
4091
  detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
3081
4092
  });
3082
4093
  if (opts.apiKey) {
3083
- const envPath = path10.join(project.root, ".env");
4094
+ const envPath = path14.join(project.root, ".env");
3084
4095
  const env = upsertEnvKeys(readFileOr(envPath, ""), [
3085
4096
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
3086
4097
  ["PIWI_API_KEY", opts.apiKey]
3087
4098
  ]);
3088
- if (env.added.length && !opts.dryRun) fs8.writeFileSync(envPath, env.text);
4099
+ if (env.added.length && !opts.dryRun) fs12.writeFileSync(envPath, env.text);
3089
4100
  results.push({
3090
4101
  step: "env",
3091
4102
  file: ".env",
3092
4103
  status: env.added.length ? "updated" : "already",
3093
4104
  detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
3094
4105
  });
3095
- const gitignorePath = path10.join(project.root, ".gitignore");
4106
+ const gitignorePath = path14.join(project.root, ".gitignore");
3096
4107
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
3097
- if (gitignore.added && !opts.dryRun) fs8.writeFileSync(gitignorePath, gitignore.text);
4108
+ if (gitignore.added && !opts.dryRun) fs12.writeFileSync(gitignorePath, gitignore.text);
3098
4109
  results.push({
3099
4110
  step: "gitignore",
3100
4111
  file: ".gitignore",
@@ -3108,7 +4119,7 @@ function nextSteps(opts, steps) {
3108
4119
  const out = [];
3109
4120
  if (!opts.apiKey) {
3110
4121
  out.push(
3111
- "If your dashboard has authentication on, create an API key (Settings \u2192 Users \u2192 API keys), put it in .env as PIWI_API_KEY, and keep .env out of git."
4122
+ "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."
3112
4123
  );
3113
4124
  }
3114
4125
  if (steps.some((s) => s.status === "manual")) {
@@ -3123,7 +4134,7 @@ function nextSteps(opts, steps) {
3123
4134
  }
3124
4135
  async function runInit(argv, env = process.env, cwd = process.cwd()) {
3125
4136
  if (argv.includes("-h") || argv.includes("--help")) {
3126
- console.log(USAGE6);
4137
+ console.log(USAGE7);
3127
4138
  return 0;
3128
4139
  }
3129
4140
  const opts = parseInitArgs(argv, env, cwd);
@@ -3156,7 +4167,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
3156
4167
  project: {
3157
4168
  root: project.root,
3158
4169
  packageManager: project.packageManager,
3159
- configPath: project.configPath ? path10.relative(project.root, project.configPath) : null,
4170
+ configPath: project.configPath ? path14.relative(project.root, project.configPath) : null,
3160
4171
  projectName: opts.projectName,
3161
4172
  serverUrl: opts.serverUrl
3162
4173
  },
@@ -3180,15 +4191,15 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
3180
4191
  }
3181
4192
 
3182
4193
  // src/cli/select.ts
3183
- var fs10 = __toESM(require("fs"));
3184
- var path12 = __toESM(require("path"));
3185
- var import_node_child_process4 = require("child_process");
3186
- var import_node_module3 = require("module");
4194
+ var fs14 = __toESM(require("fs"));
4195
+ var path16 = __toESM(require("path"));
4196
+ var import_node_child_process6 = require("child_process");
4197
+ var import_node_module4 = require("module");
3187
4198
 
3188
4199
  // src/cli/add-reporter.ts
3189
- var fs9 = __toESM(require("fs"));
3190
- var path11 = __toESM(require("path"));
3191
- var import_node_module2 = require("module");
4200
+ var fs13 = __toESM(require("fs"));
4201
+ var path15 = __toESM(require("path"));
4202
+ var import_node_module3 = require("module");
3192
4203
  var REPORTER_PACKAGE2 = "@piwitests/reporter";
3193
4204
  var ADD_REPORTER_FLAG = "--add-reporter";
3194
4205
  var ADD_REPORTER_MIN = { major: 1, minor: 63 };
@@ -3215,14 +4226,14 @@ function decideAddReporter(configSource, playwrightVersion) {
3215
4226
  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`
3216
4227
  };
3217
4228
  }
3218
- function resolveConfigPath(cwd, playwrightArgs) {
3219
- for (let i = 0; i < playwrightArgs.length; i++) {
3220
- const arg = playwrightArgs[i];
4229
+ function resolveConfigPath(cwd, playwrightArgs2) {
4230
+ for (let i = 0; i < playwrightArgs2.length; i++) {
4231
+ const arg = playwrightArgs2[i];
3221
4232
  if (arg === "--config" || arg === "-c") {
3222
- const next = playwrightArgs[i + 1];
3223
- if (next) return path11.resolve(cwd, next);
4233
+ const next = playwrightArgs2[i + 1];
4234
+ if (next) return path15.resolve(cwd, next);
3224
4235
  } else if (arg.startsWith("--config=")) {
3225
- return path11.resolve(cwd, arg.slice("--config=".length));
4236
+ return path15.resolve(cwd, arg.slice("--config=".length));
3226
4237
  }
3227
4238
  }
3228
4239
  return detectProject(cwd).configPath;
@@ -3230,14 +4241,14 @@ function resolveConfigPath(cwd, playwrightArgs) {
3230
4241
  function readConfigSource(configPath) {
3231
4242
  if (!configPath) return null;
3232
4243
  try {
3233
- return fs9.readFileSync(configPath, "utf-8");
4244
+ return fs13.readFileSync(configPath, "utf-8");
3234
4245
  } catch {
3235
4246
  return null;
3236
4247
  }
3237
4248
  }
3238
4249
  function readInstalledPlaywrightVersion(cwd) {
3239
4250
  try {
3240
- const require2 = (0, import_node_module2.createRequire)(path11.join(cwd, "noop.js"));
4251
+ const require2 = (0, import_node_module3.createRequire)(path15.join(cwd, "noop.js"));
3241
4252
  for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
3242
4253
  try {
3243
4254
  return require2(id).version ?? null;
@@ -3248,15 +4259,15 @@ function readInstalledPlaywrightVersion(cwd) {
3248
4259
  }
3249
4260
  return null;
3250
4261
  }
3251
- function computeAddReporterArgs(cwd, playwrightArgs) {
3252
- const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs));
4262
+ function computeAddReporterArgs(cwd, playwrightArgs2) {
4263
+ const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs2));
3253
4264
  return decideAddReporter(source, readInstalledPlaywrightVersion(cwd));
3254
4265
  }
3255
4266
 
3256
4267
  // src/cli/select.ts
3257
4268
  var EXIT_OK5 = 0;
3258
- var EXIT_ERROR5 = 2;
3259
- var USAGE7 = `
4269
+ var EXIT_ERROR6 = 2;
4270
+ var USAGE8 = `
3260
4271
  piwi select / piwi run \u2014 run a saved selection of tests
3261
4272
 
3262
4273
  Usage:
@@ -3314,7 +4325,7 @@ function findKey(argv) {
3314
4325
  }
3315
4326
  return void 0;
3316
4327
  }
3317
- function parseDuration(raw) {
4328
+ function parseDuration2(raw) {
3318
4329
  const match = raw.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
3319
4330
  if (!match) return null;
3320
4331
  const value = Number(match[1]);
@@ -3334,7 +4345,7 @@ function parseSelectArgs(argv, env) {
3334
4345
  const budgetRaw = readOption7(own, "--budget");
3335
4346
  let budgetMs = null;
3336
4347
  if (budgetRaw !== void 0) {
3337
- budgetMs = parseDuration(budgetRaw);
4348
+ budgetMs = parseDuration2(budgetRaw);
3338
4349
  if (budgetMs === null) throw new Error(`--budget expects a duration like 5m or 90s, got "${budgetRaw}"`);
3339
4350
  }
3340
4351
  const shard = readOption7(own, "--shard") ?? null;
@@ -3357,51 +4368,52 @@ function parseSelectArgs(argv, env) {
3357
4368
  extra
3358
4369
  };
3359
4370
  }
3360
- var CACHE_FILE = path12.join(".piwi", "selection-cache.json");
3361
- function cacheKey(projectId, args) {
3362
- return `${projectId}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
4371
+ var CACHE_FILE = path16.join(".piwi", "selection-cache.json");
4372
+ function cacheKey(args) {
4373
+ return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
3363
4374
  }
3364
- function readCache(projectId, args) {
4375
+ function readCache(args) {
3365
4376
  try {
3366
- const store = JSON.parse(fs10.readFileSync(CACHE_FILE, "utf-8"));
3367
- return store[cacheKey(projectId, args)] ?? null;
4377
+ const store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
4378
+ return store[cacheKey(args)] ?? null;
3368
4379
  } catch {
3369
4380
  return null;
3370
4381
  }
3371
4382
  }
3372
- function writeCache(projectId, args, resolution) {
4383
+ function writeCache(args, resolution) {
3373
4384
  try {
3374
4385
  let store = {};
3375
4386
  try {
3376
- store = JSON.parse(fs10.readFileSync(CACHE_FILE, "utf-8"));
4387
+ store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
3377
4388
  } catch {
3378
4389
  }
3379
- store[cacheKey(projectId, args)] = resolution;
3380
- fs10.mkdirSync(path12.dirname(CACHE_FILE), { recursive: true });
3381
- fs10.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
4390
+ store[cacheKey(args)] = resolution;
4391
+ fs14.mkdirSync(path16.dirname(CACHE_FILE), { recursive: true });
4392
+ fs14.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
3382
4393
  } catch {
3383
4394
  }
3384
4395
  }
3385
- async function resolveWithCache(args, projectId) {
4396
+ async function resolveWithCache(args) {
3386
4397
  try {
4398
+ const projectId = await resolveProjectId(args);
3387
4399
  const resolution = await fetchResolution(args, projectId);
3388
- writeCache(projectId, args, resolution);
4400
+ writeCache(args, resolution);
3389
4401
  return { resolution, fromCache: false };
3390
4402
  } catch (e) {
3391
4403
  if (args.strict) throw e;
3392
- const cached = readCache(projectId, args);
4404
+ const cached = readCache(args);
3393
4405
  if (cached) {
3394
4406
  console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
3395
4407
  return { resolution: cached, fromCache: true };
3396
4408
  }
3397
- return null;
4409
+ return { error: e };
3398
4410
  }
3399
4411
  }
3400
4412
  function printWarnings(resolution) {
3401
4413
  for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
3402
4414
  }
3403
4415
  function gitChangedFiles(base) {
3404
- const out = (0, import_node_child_process4.execFileSync)("git", ["diff", "--name-only", base], {
4416
+ const out = (0, import_node_child_process6.execFileSync)("git", ["diff", "--name-only", base], {
3405
4417
  encoding: "utf-8",
3406
4418
  stdio: ["ignore", "pipe", "ignore"]
3407
4419
  });
@@ -3422,7 +4434,7 @@ async function runSelectImpact(args) {
3422
4434
  impact = await loadImpact(args, projectId);
3423
4435
  } catch (e) {
3424
4436
  console.error(`piwi select: ${e.message}`);
3425
- return EXIT_ERROR5;
4437
+ return EXIT_ERROR6;
3426
4438
  }
3427
4439
  printWarnings(impact);
3428
4440
  console.error(
@@ -3437,7 +4449,7 @@ async function runSelectImpact(args) {
3437
4449
  }
3438
4450
  async function runSelect(argv, env = process.env) {
3439
4451
  if (argv.includes("-h") || argv.includes("--help")) {
3440
- console.log(USAGE7);
4452
+ console.log(USAGE8);
3441
4453
  return EXIT_OK5;
3442
4454
  }
3443
4455
  let args;
@@ -3446,13 +4458,13 @@ async function runSelect(argv, env = process.env) {
3446
4458
  } catch (e) {
3447
4459
  console.error(`piwi select: ${e.message}
3448
4460
  `);
3449
- console.error(USAGE7);
3450
- return EXIT_ERROR5;
4461
+ console.error(USAGE8);
4462
+ return EXIT_ERROR6;
3451
4463
  }
3452
4464
  if (args.key === IMPACT_KEY) {
3453
4465
  if (!args.base) {
3454
4466
  console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
3455
- return EXIT_ERROR5;
4467
+ return EXIT_ERROR6;
3456
4468
  }
3457
4469
  return runSelectImpact(args);
3458
4470
  }
@@ -3462,19 +4474,19 @@ async function runSelect(argv, env = process.env) {
3462
4474
  resolution = await fetchResolution(args, projectId);
3463
4475
  } catch (e) {
3464
4476
  console.error(`piwi select: ${e.message}`);
3465
- return EXIT_ERROR5;
4477
+ return EXIT_ERROR6;
3466
4478
  }
3467
4479
  printWarnings(resolution);
3468
4480
  if (resolution.estimate.count === 0) {
3469
4481
  console.error(`piwi select: "${args.key}" resolved to 0 tests`);
3470
- return EXIT_ERROR5;
4482
+ return EXIT_ERROR6;
3471
4483
  }
3472
4484
  if (args.json) console.log(JSON.stringify(resolution, null, 2));
3473
4485
  else console.log(resolution.materialization.args.join(" "));
3474
4486
  return EXIT_OK5;
3475
4487
  }
3476
- function resolvePlaywrightCli2() {
3477
- const require2 = (0, import_node_module3.createRequire)(path12.join(process.cwd(), "noop.js"));
4488
+ function resolvePlaywrightCli3() {
4489
+ const require2 = (0, import_node_module4.createRequire)(path16.join(process.cwd(), "noop.js"));
3478
4490
  for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
3479
4491
  try {
3480
4492
  return require2.resolve(id);
@@ -3483,24 +4495,27 @@ function resolvePlaywrightCli2() {
3483
4495
  }
3484
4496
  return null;
3485
4497
  }
3486
- function spawnPlaywright(pkgRunner, playwrightArgs, env) {
3487
- const cli = resolvePlaywrightCli2();
3488
- const child = cli ? (0, import_node_child_process4.spawn)(process.execPath, [cli, "test", ...playwrightArgs], { stdio: "inherit", env }) : (0, import_node_child_process4.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs], {
4498
+ function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
4499
+ const cli = resolvePlaywrightCli3();
4500
+ const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio: "inherit", env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs2], {
3489
4501
  stdio: "inherit",
3490
4502
  env
3491
4503
  });
3492
4504
  return new Promise((resolve8) => {
3493
4505
  child.on("error", (err) => {
3494
4506
  console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
3495
- resolve8(EXIT_ERROR5);
4507
+ resolve8(EXIT_ERROR6);
3496
4508
  });
3497
- child.on("exit", (code) => resolve8(code ?? EXIT_ERROR5));
4509
+ child.on("exit", (code) => resolve8(code ?? EXIT_ERROR6));
3498
4510
  });
3499
4511
  }
3500
- function spawnPlaywrightForRun(pkgRunner, playwrightArgs, env) {
3501
- const decision = computeAddReporterArgs(process.cwd(), playwrightArgs);
4512
+ function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
4513
+ const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
3502
4514
  if (decision.log) console.error(decision.log);
3503
- return spawnPlaywright(pkgRunner, [...decision.args, ...playwrightArgs], env);
4515
+ return spawnPlaywright2(pkgRunner, [...decision.args, ...playwrightArgs2], env);
4516
+ }
4517
+ function shardEnv(args) {
4518
+ return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
3504
4519
  }
3505
4520
  async function runRunImpact(args, env) {
3506
4521
  let projectId;
@@ -3509,7 +4524,7 @@ async function runRunImpact(args, env) {
3509
4524
  } catch (e) {
3510
4525
  if (args.strict) {
3511
4526
  console.error(`piwi run: ${e.message}`);
3512
- return EXIT_ERROR5;
4527
+ return EXIT_ERROR6;
3513
4528
  }
3514
4529
  console.error(`piwi run: ${e.message} \u2014 running the full suite`);
3515
4530
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
@@ -3520,7 +4535,7 @@ async function runRunImpact(args, env) {
3520
4535
  } catch (e) {
3521
4536
  if (args.strict) {
3522
4537
  console.error(`piwi run: ${e.message}`);
3523
- return EXIT_ERROR5;
4538
+ return EXIT_ERROR6;
3524
4539
  }
3525
4540
  console.error(`piwi run: ${e.message} \u2014 running the full suite`);
3526
4541
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
@@ -3541,14 +4556,15 @@ async function runRunImpact(args, env) {
3541
4556
  PIWI_SELECTION: IMPACT_KEY,
3542
4557
  PIWI_SELECTION_VERSION: "0",
3543
4558
  PIWI_SELECTION_HASH: impact.resolvedHash,
3544
- PIWI_SELECTION_COUNT: String(impact.estimate.count)
4559
+ PIWI_SELECTION_COUNT: String(impact.estimate.count),
4560
+ ...shardEnv(args)
3545
4561
  };
3546
4562
  console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)`);
3547
4563
  return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
3548
4564
  }
3549
4565
  async function runRun(argv, env = process.env) {
3550
4566
  if (argv.includes("-h") || argv.includes("--help")) {
3551
- console.log(USAGE7);
4567
+ console.log(USAGE8);
3552
4568
  return EXIT_OK5;
3553
4569
  }
3554
4570
  let args;
@@ -3557,54 +4573,44 @@ async function runRun(argv, env = process.env) {
3557
4573
  } catch (e) {
3558
4574
  console.error(`piwi run: ${e.message}
3559
4575
  `);
3560
- console.error(USAGE7);
3561
- return EXIT_ERROR5;
4576
+ console.error(USAGE8);
4577
+ return EXIT_ERROR6;
3562
4578
  }
3563
4579
  if (args.format === "json") {
3564
4580
  console.error("piwi run: --format json cannot be run; use args, grep or files");
3565
- return EXIT_ERROR5;
4581
+ return EXIT_ERROR6;
3566
4582
  }
3567
4583
  if (args.key === IMPACT_KEY) {
3568
4584
  if (!args.base) {
3569
4585
  console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
3570
- return EXIT_ERROR5;
4586
+ return EXIT_ERROR6;
3571
4587
  }
3572
4588
  return runRunImpact(args, env);
3573
4589
  }
3574
- let projectId;
3575
- try {
3576
- projectId = await resolveProjectId(args);
3577
- } catch (e) {
3578
- if (args.strict) {
3579
- console.error(`piwi run: ${e.message}`);
3580
- return EXIT_ERROR5;
3581
- }
3582
- console.error(`piwi run: ${e.message} \u2014 running the full suite`);
3583
- return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
3584
- }
3585
4590
  let outcome;
3586
4591
  try {
3587
- outcome = await resolveWithCache(args, projectId);
4592
+ outcome = await resolveWithCache(args);
3588
4593
  } catch (e) {
3589
4594
  console.error(`piwi run: ${e.message}`);
3590
- return EXIT_ERROR5;
4595
+ return EXIT_ERROR6;
3591
4596
  }
3592
- if (!outcome) {
3593
- console.error("piwi run: dashboard unreachable and no cached resolution \u2014 running the full suite");
4597
+ if ("error" in outcome) {
4598
+ console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
3594
4599
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
3595
4600
  }
3596
4601
  const { resolution } = outcome;
3597
4602
  printWarnings(resolution);
3598
4603
  if (resolution.estimate.count === 0) {
3599
4604
  console.error(`piwi run: "${args.key}" resolved to 0 tests`);
3600
- return EXIT_ERROR5;
4605
+ return EXIT_ERROR6;
3601
4606
  }
3602
4607
  const runEnv = {
3603
4608
  ...env,
3604
4609
  PIWI_SELECTION: resolution.key ?? args.key,
3605
4610
  PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
3606
4611
  PIWI_SELECTION_HASH: resolution.resolvedHash,
3607
- PIWI_SELECTION_COUNT: String(resolution.estimate.count)
4612
+ PIWI_SELECTION_COUNT: String(resolution.estimate.count),
4613
+ ...shardEnv(args)
3608
4614
  };
3609
4615
  console.error(
3610
4616
  `piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
@@ -3613,16 +4619,16 @@ async function runRun(argv, env = process.env) {
3613
4619
  }
3614
4620
 
3615
4621
  // src/cli/preflight.ts
3616
- var fs12 = __toESM(require("fs"));
3617
- var path14 = __toESM(require("path"));
3618
- var import_node_child_process5 = require("child_process");
4622
+ var fs15 = __toESM(require("fs"));
4623
+ var path17 = __toESM(require("path"));
4624
+ var import_node_child_process7 = require("child_process");
3619
4625
 
3620
4626
  // ../core/src/diff-anchors.ts
3621
4627
  var RENAME_FROM = `rename from `;
3622
4628
  var RENAME_TO = "rename to ";
3623
4629
  var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
3624
- function stripPrefix(path16) {
3625
- const unquoted = path16.startsWith('"') && path16.endsWith('"') ? path16.slice(1, -1) : path16;
4630
+ function stripPrefix(path19) {
4631
+ const unquoted = path19.startsWith('"') && path19.endsWith('"') ? path19.slice(1, -1) : path19;
3626
4632
  return unquoted.replace(/^[ab]\//, "");
3627
4633
  }
3628
4634
  function parseUnifiedDiff(text) {
@@ -3690,10 +4696,7 @@ function parseUnifiedDiff(text) {
3690
4696
  }
3691
4697
  return files.filter((f) => f.path);
3692
4698
  }
3693
- var DEFAULT_TRANSLATION_GLOBS = [
3694
- "**/{locales,locale,i18n,lang,translations}/**",
3695
- "**/*.resx"
3696
- ];
4699
+ var DEFAULT_TRANSLATION_GLOBS = ["**/{locales,locale,i18n,lang,translations}/**", "**/*.resx"];
3697
4700
  var ANCHOR_ATTRIBUTES = [
3698
4701
  "id",
3699
4702
  "name",
@@ -3735,8 +4738,8 @@ function pathGlobToRegExp(glob) {
3735
4738
  });
3736
4739
  return new RegExp(`^(?:${parts.join("|")})$`);
3737
4740
  }
3738
- function isTranslationFile(path16, globs = DEFAULT_TRANSLATION_GLOBS) {
3739
- const posix = path16.replace(/\\/g, "/");
4741
+ function isTranslationFile(path19, globs = DEFAULT_TRANSLATION_GLOBS) {
4742
+ const posix = path19.replace(/\\/g, "/");
3740
4743
  if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
3741
4744
  return globs.some((g) => pathGlobToRegExp(g).test(posix));
3742
4745
  }
@@ -3876,13 +4879,13 @@ function unquoteYaml(raw) {
3876
4879
  function decodeXml(text) {
3877
4880
  return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
3878
4881
  }
3879
- function parseTranslationFile(path16, content) {
4882
+ function parseTranslationFile(path19, content) {
3880
4883
  const lines = content.split(/\r?\n/);
3881
4884
  const out = [];
3882
- const ext = /\.([a-z]+)$/i.exec(path16)?.[1]?.toLowerCase();
4885
+ const ext = /\.([a-z]+)$/i.exec(path19)?.[1]?.toLowerCase();
3883
4886
  if (ext === "json") {
3884
4887
  const stack = [];
3885
- const path17 = (key) => [...stack.map((f) => f.key), key].join(".");
4888
+ const path20 = (key) => [...stack.map((f) => f.key), key].join(".");
3886
4889
  lines.forEach((text, i) => {
3887
4890
  const t = text.trim();
3888
4891
  const top = stack[stack.length - 1];
@@ -3891,14 +4894,14 @@ function parseTranslationFile(path16, content) {
3891
4894
  const key = JSON.parse(`"${pair[1]}"`);
3892
4895
  const rest = pair[2];
3893
4896
  const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
3894
- if (str) out.push({ key: path17(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
4897
+ if (str) out.push({ key: path20(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
3895
4898
  else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
3896
4899
  return;
3897
4900
  }
3898
4901
  if (top?.array) {
3899
4902
  const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
3900
4903
  if (str) {
3901
- out.push({ key: path17(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
4904
+ out.push({ key: path20(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
3902
4905
  return;
3903
4906
  }
3904
4907
  if (/^[[{]\s*$/.test(t)) {
@@ -3962,8 +4965,8 @@ function parseTranslationFile(path16, content) {
3962
4965
  }
3963
4966
  return out;
3964
4967
  }
3965
- function translationLineToken(path16, text, line) {
3966
- const ext = /\.([a-z]+)$/i.exec(path16)?.[1]?.toLowerCase();
4968
+ function translationLineToken(path19, text, line) {
4969
+ const ext = /\.([a-z]+)$/i.exec(path19)?.[1]?.toLowerCase();
3967
4970
  let key;
3968
4971
  let value;
3969
4972
  if (ext === "json") {
@@ -4000,9 +5003,9 @@ function translationLineToken(path16, text, line) {
4000
5003
  if (value === void 0 || !keepable(value)) return null;
4001
5004
  return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
4002
5005
  }
4003
- function lineTokens(path16, text, line, side, attributes, options) {
5006
+ function lineTokens(path19, text, line, side, attributes, options) {
4004
5007
  const spans = [];
4005
- const markup = MARKUP_FILE.test(path16);
5008
+ const markup = MARKUP_FILE.test(path19);
4006
5009
  return [
4007
5010
  ...attributeTokens(text, attributes, line, spans),
4008
5011
  ...translationRefTokens(text, line, side, options.translations, spans),
@@ -4010,10 +5013,10 @@ function lineTokens(path16, text, line, side, attributes, options) {
4010
5013
  ...textTokens(text, line, markup)
4011
5014
  ];
4012
5015
  }
4013
- function keysByLine(path16, side, options) {
4014
- const content = options.readFile?.(path16, side);
5016
+ function keysByLine(path19, side, options) {
5017
+ const content = options.readFile?.(path19, side);
4015
5018
  if (content === void 0) return null;
4016
- return new Map(parseTranslationFile(path16, content).map((e) => [e.line, e.key]));
5019
+ return new Map(parseTranslationFile(path19, content).map((e) => [e.line, e.key]));
4017
5020
  }
4018
5021
  function tokenId(t) {
4019
5022
  return `${t.group}\0${t.value}`;
@@ -4088,40 +5091,6 @@ function extractDiffAnchors(files, options = {}) {
4088
5091
  return anchors;
4089
5092
  }
4090
5093
 
4091
- // ../core/src/dotenv.ts
4092
- function parseDotEnv(text) {
4093
- const out = {};
4094
- for (const raw of text.split(/\r?\n/)) {
4095
- const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
4096
- if (!m) continue;
4097
- let value = m[2];
4098
- const quote4 = value[0];
4099
- if ((quote4 === '"' || quote4 === "'") && value.length > 1 && value.endsWith(quote4)) {
4100
- value = value.slice(1, -1);
4101
- if (quote4 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
4102
- } else {
4103
- value = value.replace(/\s+#.*$/, "");
4104
- }
4105
- out[m[1]] = value;
4106
- }
4107
- return out;
4108
- }
4109
- function resolvePiwiConnection(sources) {
4110
- const own = {
4111
- serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
4112
- apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
4113
- };
4114
- const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
4115
- const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
4116
- const desktop = sources.desktop ?? null;
4117
- const trim = (url) => url.replace(/\/+$/, "");
4118
- const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
4119
- const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
4120
- if (!serverUrl) return null;
4121
- const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
4122
- return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
4123
- }
4124
-
4125
5094
  // ../core/src/locator-text-match.ts
4126
5095
  function normalizeWhiteSpace(text) {
4127
5096
  return text.replace(/[​­]/g, "").trim().replace(/\s+/g, " ");
@@ -4477,140 +5446,12 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
4477
5446
  return null;
4478
5447
  }
4479
5448
 
4480
- // src/internal/config/desktop.ts
4481
- var fs11 = __toESM(require("fs"));
4482
- var os = __toESM(require("os"));
4483
- var path13 = __toESM(require("path"));
4484
- function defaultDesktopConfigPath() {
4485
- return path13.join(os.homedir(), ".piwi", "desktop.json");
4486
- }
4487
- function readDesktopConfig(filePath) {
4488
- try {
4489
- const parsed = JSON.parse(fs11.readFileSync(filePath, "utf8"));
4490
- const url = parsed?.url;
4491
- const token = parsed?.token;
4492
- if (typeof url !== "string" || !url) return null;
4493
- if (typeof token !== "string" || !token) return null;
4494
- return { url, token };
4495
- } catch {
4496
- return null;
4497
- }
4498
- }
4499
-
4500
- // src/internal/config/env.ts
4501
- var DEFAULTS = {
4502
- projectName: "default-project",
4503
- uploadTraces: true,
4504
- uploadReport: true,
4505
- liveFileUploads: true,
4506
- collectScmInfo: true,
4507
- collectCiInfo: true,
4508
- collectPerformanceMetrics: true,
4509
- captureLocators: true,
4510
- capturePageState: true,
4511
- // Off by default: the page inventory reads control and link names from every
4512
- // visited page, so it stays opt-in until a project turns it on.
4513
- capturePageInventory: false,
4514
- // Off by default: JavaScript coverage slows the page, so a scheduled job opts in.
4515
- captureCodeReach: false,
4516
- captureServerTraces: true,
4517
- sampleAriaOnPass: true,
4518
- uploadManifest: true,
4519
- defaultCapture: true,
4520
- streaming: true,
4521
- streamingBatchSize: 5,
4522
- streamingBatchDelay: 2e3,
4523
- maxStreamBufferBytes: 100 * 1024 * 1024,
4524
- failOnFlakyTests: false,
4525
- username: null,
4526
- password: null,
4527
- apiKey: null,
4528
- verbose: false
4529
- };
4530
- var PIWI_ENV_KEYS = {
4531
- serverUrl: "PIWI_DASHBOARD_URL",
4532
- projectName: "PIWI_PROJECT_NAME",
4533
- verbose: "PIWI_VERBOSE",
4534
- apiKey: "PIWI_API_KEY",
4535
- username: "PIWI_USERNAME",
4536
- password: "PIWI_PASSWORD",
4537
- environment: "PIWI_ENVIRONMENT",
4538
- label: "PIWI_LABEL",
4539
- runLabel: "PIWI_RUN_LABEL",
4540
- keep: "PIWI_KEEP",
4541
- streaming: "PIWI_STREAMING",
4542
- streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
4543
- streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
4544
- maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
4545
- liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
4546
- failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
4547
- uploadTraces: "PIWI_UPLOAD_TRACES",
4548
- uploadReport: "PIWI_UPLOAD_REPORT",
4549
- captureLocators: "PIWI_CAPTURE_LOCATORS",
4550
- capturePageState: "PIWI_CAPTURE_PAGE_STATE",
4551
- capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
4552
- captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
4553
- codeReachRoots: "PIWI_CODE_REACH_ROOTS",
4554
- captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
4555
- sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
4556
- uploadManifest: "PIWI_UPLOAD_MANIFEST",
4557
- defaultCapture: "PIWI_DEFAULT_CAPTURE",
4558
- inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
4559
- pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
4560
- outputFile: "PIWI_OUTPUT_FILE",
4561
- aiMode: "PIWI_AI",
4562
- aiDir: "PIWI_AI_DIR",
4563
- aiOnMiss: "PIWI_AI_ON_MISS",
4564
- aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
4565
- aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
4566
- aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
4567
- aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
4568
- aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
4569
- };
4570
- var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
4571
- var PIWI_PROBE_ENV = {
4572
- flag: "PIWI_PROBE",
4573
- plan: "PIWI_PROBE_PLAN",
4574
- results: "PIWI_PROBE_RESULTS",
4575
- secret: "PIWI_PROBE_SECRET"
4576
- };
4577
- var ENV_FALLBACK_SPECS = [
4578
- { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
4579
- { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
4580
- { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
4581
- { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
4582
- { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
4583
- { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
4584
- { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
4585
- { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
4586
- { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
4587
- { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
4588
- { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
4589
- { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
4590
- { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
4591
- { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
4592
- { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
4593
- { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
4594
- { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
4595
- { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
4596
- { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
4597
- { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
4598
- { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
4599
- { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
4600
- { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
4601
- { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
4602
- { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
4603
- { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
4604
- { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
4605
- { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
4606
- ];
4607
-
4608
5449
  // src/cli/preflight.ts
4609
5450
  var EXIT_OK6 = 0;
4610
5451
  var EXIT_BREAKS = 1;
4611
- var EXIT_ERROR6 = 2;
5452
+ var EXIT_ERROR7 = 2;
4612
5453
  var MAX_LISTED = 20;
4613
- var USAGE8 = `
5454
+ var USAGE9 = `
4614
5455
  piwi preflight \u2014 the test locators a change breaks, before it runs
4615
5456
 
4616
5457
  Usage:
@@ -4689,16 +5530,16 @@ function parsePreflightArgs(argv) {
4689
5530
  }
4690
5531
  var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
4691
5532
  function findPlaywrightRoot(from) {
4692
- let dir = path14.resolve(from);
5533
+ let dir = path17.resolve(from);
4693
5534
  for (; ; ) {
4694
- if (CONFIG_NAMES2.some((name) => fs12.existsSync(path14.join(dir, name)))) return dir;
4695
- const parent = path14.dirname(dir);
5535
+ if (CONFIG_NAMES2.some((name) => fs15.existsSync(path17.join(dir, name)))) return dir;
5536
+ const parent = path17.dirname(dir);
4696
5537
  if (parent === dir) return null;
4697
5538
  dir = parent;
4698
5539
  }
4699
5540
  }
4700
5541
  function git(cwd, args) {
4701
- return (0, import_node_child_process5.execFileSync)("git", args, {
5542
+ return (0, import_node_child_process7.execFileSync)("git", args, {
4702
5543
  cwd,
4703
5544
  encoding: "utf-8",
4704
5545
  stdio: ["ignore", "pipe", "pipe"],
@@ -4714,19 +5555,7 @@ function tryGit(cwd, args) {
4714
5555
  }
4715
5556
  var TEST_FILE = /(?:^|\/)[^/]+\.(?:spec|test)\.[cm]?[jt]sx?$/;
4716
5557
  function resolveConnection(args, env, dirs) {
4717
- let dotEnv = {};
4718
- for (const dir of dirs) {
4719
- try {
4720
- dotEnv = { ...parseDotEnv(fs12.readFileSync(path14.join(dir, ".env"), "utf-8")), ...dotEnv };
4721
- } catch {
4722
- }
4723
- }
4724
- return resolvePiwiConnection({
4725
- flags: { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
4726
- env,
4727
- dotEnv,
4728
- desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
4729
- });
5558
+ return resolveCliConnection({ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project }, env, dirs);
4730
5559
  }
4731
5560
  function translationLookup(repoRoot, base, locale) {
4732
5561
  const files = (tryGit(repoRoot, ["ls-files"]) ?? "").split("\n").filter((f) => f && isTranslationFile(f)).sort((a, b) => Number(prefersLocale(b, locale)) - Number(prefersLocale(a, locale)) || a.localeCompare(b));
@@ -4734,7 +5563,7 @@ function translationLookup(repoRoot, base, locale) {
4734
5563
  const build = (side) => {
4735
5564
  const map = /* @__PURE__ */ new Map();
4736
5565
  for (const file of files) {
4737
- const content = side === "new" ? readText(path14.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
5566
+ const content = side === "new" ? readText(path17.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
4738
5567
  if (content === void 0) continue;
4739
5568
  for (const entry of parseTranslationFile(file, content)) {
4740
5569
  if (!map.has(entry.key)) map.set(entry.key, entry.value);
@@ -4753,34 +5582,34 @@ function prefersLocale(file, locale) {
4753
5582
  }
4754
5583
  function readText(file) {
4755
5584
  try {
4756
- return fs12.readFileSync(file, "utf-8");
5585
+ return fs15.readFileSync(file, "utf-8");
4757
5586
  } catch {
4758
5587
  return void 0;
4759
5588
  }
4760
5589
  }
4761
- var CACHE_FILE2 = path14.join(".piwi", "locator-index.json");
5590
+ var CACHE_FILE2 = path17.join(".piwi", "locator-index.json");
4762
5591
  function cacheKey2(connection, branch) {
4763
5592
  return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
4764
5593
  }
4765
5594
  function readCache2(root, key) {
4766
5595
  try {
4767
- const store = JSON.parse(fs12.readFileSync(path14.join(root, CACHE_FILE2), "utf-8"));
5596
+ const store = JSON.parse(fs15.readFileSync(path17.join(root, CACHE_FILE2), "utf-8"));
4768
5597
  return store[key] ?? null;
4769
5598
  } catch {
4770
5599
  return null;
4771
5600
  }
4772
5601
  }
4773
5602
  function writeCache2(root, key, index) {
4774
- const file = path14.join(root, CACHE_FILE2);
5603
+ const file = path17.join(root, CACHE_FILE2);
4775
5604
  try {
4776
5605
  let store = {};
4777
5606
  try {
4778
- store = JSON.parse(fs12.readFileSync(file, "utf-8"));
5607
+ store = JSON.parse(fs15.readFileSync(file, "utf-8"));
4779
5608
  } catch {
4780
5609
  }
4781
5610
  store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
4782
- fs12.mkdirSync(path14.dirname(file), { recursive: true });
4783
- fs12.writeFileSync(file, JSON.stringify(store));
5611
+ fs15.mkdirSync(path17.dirname(file), { recursive: true });
5612
+ fs15.writeFileSync(file, JSON.stringify(store));
4784
5613
  } catch {
4785
5614
  }
4786
5615
  }
@@ -4796,7 +5625,7 @@ function planSites(b, testRoot) {
4796
5625
  const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
4797
5626
  return sites.map((callSite) => {
4798
5627
  if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
4799
- const file = path14.resolve(testRoot, callSiteFile(callSite));
5628
+ const file = path17.resolve(testRoot, callSiteFile(callSite));
4800
5629
  const lineNo = callSiteLine(callSite);
4801
5630
  const text = readText(file);
4802
5631
  if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
@@ -4839,7 +5668,7 @@ function applyEdits(breaks, replacementsOf) {
4839
5668
  }
4840
5669
  const edited = [];
4841
5670
  for (const [file, edits] of byFile) {
4842
- const text = fs12.readFileSync(file, "utf-8");
5671
+ const text = fs15.readFileSync(file, "utf-8");
4843
5672
  const eol = text.includes("\r\n") ? "\r\n" : "\n";
4844
5673
  const lines = text.split(/\r?\n/);
4845
5674
  for (const e of edits) {
@@ -4849,7 +5678,7 @@ function applyEdits(breaks, replacementsOf) {
4849
5678
  }
4850
5679
  const next = lines.join(eol);
4851
5680
  if (next !== text) {
4852
- fs12.writeFileSync(file, next);
5681
+ fs15.writeFileSync(file, next);
4853
5682
  edited.push(file);
4854
5683
  }
4855
5684
  }
@@ -4863,7 +5692,7 @@ function describeAnchor(a) {
4863
5692
  const extra = a.key ? ` (key ${a.key})` : a.attribute ? ` (${a.attribute})` : "";
4864
5693
  return `${what}${extra}`;
4865
5694
  }
4866
- function pad(text, width) {
5695
+ function pad2(text, width) {
4867
5696
  return text.length >= width ? `${text} ` : text + " ".repeat(width - text.length);
4868
5697
  }
4869
5698
  function renderPreflight(result, cwdToRoot = (f) => f) {
@@ -4884,14 +5713,14 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
4884
5713
  let shown = 0;
4885
5714
  for (const [anchor, group] of byAnchor) {
4886
5715
  if (shown >= budget) break;
4887
- out.push(` ${pad(`${anchor.file}:${anchor.line}`, 38)} ${describeAnchor(anchor)}`);
5716
+ out.push(` ${pad2(`${anchor.file}:${anchor.line}`, 38)} ${describeAnchor(anchor)}`);
4888
5717
  for (const b of group) {
4889
5718
  if (shown >= budget) break;
4890
5719
  shown++;
4891
5720
  const meta = `${b.actions.slice(0, 3).join(", ") || "used"} \xB7 ${plural(b.tests.length, "test")} \xB7 ${b.confidence}`;
4892
- out.push(` ${pad(b.locator, 48)} ${meta}`);
5721
+ out.push(` ${pad2(b.locator, 48)} ${meta}`);
4893
5722
  for (const site of b.sites.slice(0, 5)) {
4894
- const where = pad(cwdToRoot(site.callSite.replace(/:\d+$/, "")), 34);
5723
+ const where = pad2(cwdToRoot(site.callSite.replace(/:\d+$/, "")), 34);
4895
5724
  if (site.action === "edit") out.push(` ${where} \u2192 ${b.rewrite}`);
4896
5725
  else if (site.action === "applied") out.push(` ${where} already rewritten`);
4897
5726
  else if (site.action === "none") {
@@ -4920,7 +5749,7 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
4920
5749
  out.push(`${plural(new Set(possible.map((b) => b.locator)).size, "locator")} it may break (a bare string matched)`);
4921
5750
  for (const b of possible.slice(0, Math.max(0, MAX_LISTED - likely.length))) {
4922
5751
  out.push(
4923
- ` ${pad(b.locator, 48)} ${plural(b.tests.length, "test")} \xB7 ${b.anchor.file}:${b.anchor.line} ${describeAnchor(b.anchor)}`
5752
+ ` ${pad2(b.locator, 48)} ${plural(b.tests.length, "test")} \xB7 ${b.anchor.file}:${b.anchor.line} ${describeAnchor(b.anchor)}`
4924
5753
  );
4925
5754
  if (b.rewrite) out.push(` if it does, edit by hand: ${b.rewrite}`);
4926
5755
  }
@@ -4943,7 +5772,7 @@ function renderPreflight(result, cwdToRoot = (f) => f) {
4943
5772
  }
4944
5773
  async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
4945
5774
  if (argv.includes("-h") || argv.includes("--help")) {
4946
- console.log(USAGE8);
5775
+ console.log(USAGE9);
4947
5776
  return EXIT_OK6;
4948
5777
  }
4949
5778
  let args;
@@ -4952,26 +5781,26 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
4952
5781
  } catch (e) {
4953
5782
  console.error(`piwi preflight: ${e.message}
4954
5783
  `);
4955
- console.error(USAGE8);
4956
- return EXIT_ERROR6;
5784
+ console.error(USAGE9);
5785
+ return EXIT_ERROR7;
4957
5786
  }
4958
5787
  const repoRoot = tryGit(cwd, ["rev-parse", "--show-toplevel"])?.trim();
4959
5788
  if (!repoRoot) {
4960
5789
  console.error("piwi preflight: not inside a git repository");
4961
- return EXIT_ERROR6;
5790
+ return EXIT_ERROR7;
4962
5791
  }
4963
- const testRoot = path14.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
5792
+ const testRoot = path17.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
4964
5793
  let files;
4965
5794
  try {
4966
5795
  files = parseUnifiedDiff(git(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
4967
5796
  } catch (e) {
4968
5797
  console.error(`piwi preflight: could not diff against ${args.base} \u2014 ${e.message.split("\n")[0]}`);
4969
- return EXIT_ERROR6;
5798
+ return EXIT_ERROR7;
4970
5799
  }
4971
5800
  const connection = resolveConnection(args, env, [.../* @__PURE__ */ new Set([testRoot, cwd, repoRoot])]);
4972
5801
  if (!connection) {
4973
5802
  console.error("piwi preflight: no dashboard URL \u2014 pass --server-url, set PIWI_DASHBOARD_URL, or run piwi init");
4974
- return EXIT_ERROR6;
5803
+ return EXIT_ERROR7;
4975
5804
  }
4976
5805
  const key = cacheKey2(connection, args.branch);
4977
5806
  let index;
@@ -4985,7 +5814,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
4985
5814
  const cached = readCache2(testRoot, key);
4986
5815
  if (!cached) {
4987
5816
  console.error(`piwi preflight: ${e.message}, and there is no cached index`);
4988
- return EXIT_ERROR6;
5817
+ return EXIT_ERROR7;
4989
5818
  }
4990
5819
  console.error(
4991
5820
  `piwi preflight: dashboard unreachable, using the index cached ${cached.savedAt} \u2014 ${e.message}`
@@ -4997,7 +5826,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
4997
5826
  testIdAttributes: index.testIdAttributes ?? void 0,
4998
5827
  isTestFile: (file) => TEST_FILE.test(file),
4999
5828
  translations: translationLookup(repoRoot, args.base, args.locale),
5000
- readFile: (file, side) => side === "new" ? readText(path14.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
5829
+ readFile: (file, side) => side === "new" ? readText(path17.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
5001
5830
  });
5002
5831
  const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
5003
5832
  const replacementsOf = /* @__PURE__ */ new Map();
@@ -5042,8 +5871,8 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
5042
5871
  if (args.json) console.log(JSON.stringify(result, null, 2));
5043
5872
  else {
5044
5873
  const rel = (file) => {
5045
- const abs = path14.isAbsolute(file) ? file : path14.resolve(testRoot, file);
5046
- const r = path14.relative(cwd, abs).split(path14.sep).join("/");
5874
+ const abs = path17.isAbsolute(file) ? file : path17.resolve(testRoot, file);
5875
+ const r = path17.relative(cwd, abs).split(path17.sep).join("/");
5047
5876
  return r.startsWith("..") ? file : r || file;
5048
5877
  };
5049
5878
  console.log(renderPreflight(result, rel));
@@ -5070,14 +5899,12 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
5070
5899
  }
5071
5900
 
5072
5901
  // src/cli/probe.ts
5073
- var import_node_child_process6 = require("child_process");
5074
- var import_node_module4 = require("module");
5075
- var fs13 = __toESM(require("fs"));
5076
- var os2 = __toESM(require("os"));
5077
- var path15 = __toESM(require("path"));
5902
+ var fs16 = __toESM(require("fs"));
5903
+ var os3 = __toESM(require("os"));
5904
+ var path18 = __toESM(require("path"));
5078
5905
  var EXIT_OK7 = 0;
5079
- var EXIT_ERROR7 = 2;
5080
- var USAGE9 = `
5906
+ var EXIT_ERROR8 = 2;
5907
+ var USAGE10 = `
5081
5908
  piwi probe \u2014 run the dashboard's probe plan and record what the suite noticed
5082
5909
 
5083
5910
  Usage:
@@ -5095,46 +5922,22 @@ function readOption9(argv, name) {
5095
5922
  const i = argv.indexOf(name);
5096
5923
  return i >= 0 && i + 1 < argv.length ? argv[i + 1] : void 0;
5097
5924
  }
5098
- function escapeRegex(s) {
5925
+ function escapeRegex2(s) {
5099
5926
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5100
5927
  }
5101
- function resolvePlaywrightCli3() {
5102
- const require2 = (0, import_node_module4.createRequire)(path15.join(process.cwd(), "noop.js"));
5103
- for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
5104
- try {
5105
- return require2.resolve(id);
5106
- } catch {
5107
- }
5108
- }
5109
- return null;
5110
- }
5111
- function spawnPlaywright2(playwrightArgs, env) {
5112
- const cli = resolvePlaywrightCli3();
5113
- const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs], { stdio: "inherit", env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs], {
5114
- stdio: "inherit",
5115
- env
5116
- });
5117
- return new Promise((resolve8) => {
5118
- child.on("error", (err) => {
5119
- console.error(`piwi probe: could not start Playwright \u2014 ${err.message}`);
5120
- resolve8(EXIT_ERROR7);
5121
- });
5122
- child.on("exit", (code) => resolve8(code ?? EXIT_ERROR7));
5123
- });
5124
- }
5125
5928
  async function runProbe(argv) {
5126
5929
  const sep5 = argv.indexOf("--");
5127
5930
  const own = sep5 < 0 ? argv : argv.slice(0, sep5);
5128
- const playwrightArgs = sep5 < 0 ? [] : argv.slice(sep5 + 1);
5931
+ const playwrightArgs2 = sep5 < 0 ? [] : argv.slice(sep5 + 1);
5129
5932
  if (own.includes("-h") || own.includes("--help")) {
5130
- console.log(USAGE9);
5933
+ console.log(USAGE10);
5131
5934
  return EXIT_OK7;
5132
5935
  }
5133
5936
  const env = process.env;
5134
5937
  const serverUrl = (readOption9(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
5135
5938
  if (!serverUrl) {
5136
5939
  console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
5137
- return EXIT_ERROR7;
5940
+ return EXIT_ERROR8;
5138
5941
  }
5139
5942
  const apiKey = readOption9(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
5140
5943
  const project = readOption9(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
@@ -5144,7 +5947,7 @@ async function runProbe(argv) {
5144
5947
  projectId = await resolveProjectId({ serverUrl, apiKey, project, key: "" });
5145
5948
  } catch (e) {
5146
5949
  console.error(`piwi probe: ${e.message}`);
5147
- return EXIT_ERROR7;
5950
+ return EXIT_ERROR8;
5148
5951
  }
5149
5952
  const authHeaders3 = apiKey ? { "X-API-Key": apiKey } : {};
5150
5953
  const planUrl = `${serverUrl}/api/projects/${projectId}/probes/plan${budget ? `?budget=${encodeURIComponent(budget)}` : ""}`;
@@ -5155,22 +5958,22 @@ async function runProbe(argv) {
5155
5958
  plan = await res.json();
5156
5959
  } catch (e) {
5157
5960
  console.error(`piwi probe: could not fetch the probe plan \u2014 ${e.message}`);
5158
- return EXIT_ERROR7;
5961
+ return EXIT_ERROR8;
5159
5962
  }
5160
5963
  if (!plan.items || plan.items.length === 0) {
5161
5964
  console.error("piwi probe: nothing to probe \u2014 the plan is empty.");
5162
5965
  return EXIT_OK7;
5163
5966
  }
5164
5967
  console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
5165
- const dir = fs13.mkdtempSync(path15.join(os2.tmpdir(), "piwi-probe-"));
5166
- const planFile = path15.join(dir, "plan.json");
5167
- const resultsFile = path15.join(dir, "results.jsonl");
5168
- const runOutputFile = path15.join(dir, "run.json");
5169
- fs13.writeFileSync(planFile, JSON.stringify(plan));
5170
- fs13.writeFileSync(resultsFile, "");
5171
- const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex).join("|");
5172
- const pwArgs = [...playwrightArgs];
5173
- if (grep && !playwrightArgs.includes("--grep") && !playwrightArgs.includes("-g")) pwArgs.push("--grep", grep);
5968
+ const dir = fs16.mkdtempSync(path18.join(os3.tmpdir(), "piwi-probe-"));
5969
+ const planFile = path18.join(dir, "plan.json");
5970
+ const resultsFile = path18.join(dir, "results.jsonl");
5971
+ const runOutputFile = path18.join(dir, "run.json");
5972
+ fs16.writeFileSync(planFile, JSON.stringify(plan));
5973
+ fs16.writeFileSync(resultsFile, "");
5974
+ const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
5975
+ const pwArgs = [...playwrightArgs2];
5976
+ if (grep && !playwrightArgs2.includes("--grep") && !playwrightArgs2.includes("-g")) pwArgs.push("--grep", grep);
5174
5977
  const childEnv = {
5175
5978
  ...env,
5176
5979
  [PIWI_PROBE_ENV.flag]: "1",
@@ -5180,8 +5983,8 @@ async function runProbe(argv) {
5180
5983
  // it write the run output here so the results POST can name the run.
5181
5984
  [PIWI_ENV_KEYS.outputFile]: runOutputFile
5182
5985
  };
5183
- await spawnPlaywright2(pwArgs, childEnv);
5184
- const lines = fs13.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
5986
+ await spawnPlaywright(pwArgs, childEnv, { command: "piwi probe" });
5987
+ const lines = fs16.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
5185
5988
  const results = [];
5186
5989
  for (const line of lines) {
5187
5990
  try {
@@ -5195,7 +5998,7 @@ async function runProbe(argv) {
5195
5998
  }
5196
5999
  let runId = null;
5197
6000
  try {
5198
- const out = JSON.parse(fs13.readFileSync(runOutputFile, "utf8"));
6001
+ const out = JSON.parse(fs16.readFileSync(runOutputFile, "utf8"));
5199
6002
  if (typeof out.runId === "number") runId = out.runId;
5200
6003
  } catch {
5201
6004
  }
@@ -5214,19 +6017,19 @@ async function runProbe(argv) {
5214
6017
  );
5215
6018
  } catch (e) {
5216
6019
  console.error(`piwi probe: could not post probe results \u2014 ${e.message}`);
5217
- return EXIT_ERROR7;
6020
+ return EXIT_ERROR8;
5218
6021
  }
5219
6022
  return EXIT_OK7;
5220
6023
  }
5221
6024
 
5222
6025
  // src/cli/quality-report.ts
5223
- var fs14 = __toESM(require("fs"));
6026
+ var fs17 = __toESM(require("fs"));
5224
6027
  var EXIT_OK8 = 0;
5225
6028
  var EXIT_VERDICT = 1;
5226
- var EXIT_ERROR8 = 2;
6029
+ var EXIT_ERROR9 = 2;
5227
6030
  var FORMATS = ["md", "json", "html", "pdf", "csv"];
5228
6031
  var DASHBOARDS = ["executive", "engineering", "overview"];
5229
- var USAGE10 = `
6032
+ var USAGE11 = `
5230
6033
  piwi report \u2014 print a quality report from the dashboard
5231
6034
 
5232
6035
  Usage:
@@ -5329,7 +6132,7 @@ async function fetchReport(args, query) {
5329
6132
  }
5330
6133
  async function runQualityReport(argv) {
5331
6134
  if (argv.includes("-h") || argv.includes("--help")) {
5332
- console.log(USAGE10);
6135
+ console.log(USAGE11);
5333
6136
  return EXIT_OK8;
5334
6137
  }
5335
6138
  try {
@@ -5340,7 +6143,7 @@ async function runQualityReport(argv) {
5340
6143
  }
5341
6144
  const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
5342
6145
  const bytes = new Uint8Array(await res.arrayBuffer());
5343
- if (args.output) fs14.writeFileSync(args.output, bytes);
6146
+ if (args.output) fs17.writeFileSync(args.output, bytes);
5344
6147
  else process.stdout.write(bytes);
5345
6148
  if (!args.failOn) return EXIT_OK8;
5346
6149
  const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();
@@ -5351,12 +6154,12 @@ async function runQualityReport(argv) {
5351
6154
  return EXIT_OK8;
5352
6155
  } catch (e) {
5353
6156
  console.error(`piwi report: ${e.message}`);
5354
- return EXIT_ERROR8;
6157
+ return EXIT_ERROR9;
5355
6158
  }
5356
6159
  }
5357
6160
 
5358
6161
  // src/cli/index.ts
5359
- var USAGE11 = `
6162
+ var USAGE12 = `
5360
6163
  piwi \u2014 companion commands for the Piwi Dashboard reporter
5361
6164
 
5362
6165
  Usage:
@@ -5370,6 +6173,7 @@ Commands:
5370
6173
  select Print the Playwright args for a saved test selection
5371
6174
  run Run a saved test selection with playwright test
5372
6175
  probe Run the dashboard's probe plan and record what the suite noticed
6176
+ flake Make a flaky test fail on demand under its suspects' conditions, then verify the fix
5373
6177
  ai Manage committed natural-language AI-step artifacts
5374
6178
  codegen Turn a steps file (a Piwi Picker recording) into a Playwright spec
5375
6179
  preflight List the test locators your uncommitted change breaks, and fix them
@@ -5396,6 +6200,8 @@ async function main() {
5396
6200
  return runRun(rest);
5397
6201
  case "probe":
5398
6202
  return runProbe(rest);
6203
+ case "flake":
6204
+ return runFlake(rest);
5399
6205
  case "ai":
5400
6206
  return runAi(rest);
5401
6207
  case "codegen":
@@ -5407,12 +6213,12 @@ async function main() {
5407
6213
  case void 0:
5408
6214
  case "-h":
5409
6215
  case "--help":
5410
- console.log(USAGE11);
6216
+ console.log(USAGE12);
5411
6217
  return 0;
5412
6218
  default:
5413
6219
  console.error(`piwi: unknown command "${command}"
5414
6220
  `);
5415
- console.error(USAGE11);
6221
+ console.error(USAGE12);
5416
6222
  return 2;
5417
6223
  }
5418
6224
  }