@piwitests/reporter 0.41.0 → 0.42.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,1016 @@ ${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
+ failOnFlakyTests: false,
2465
+ username: null,
2466
+ password: null,
2467
+ apiKey: null,
2468
+ verbose: false
2469
+ };
2470
+ var PIWI_ENV_KEYS = {
2471
+ serverUrl: "PIWI_DASHBOARD_URL",
2472
+ projectName: "PIWI_PROJECT_NAME",
2473
+ verbose: "PIWI_VERBOSE",
2474
+ apiKey: "PIWI_API_KEY",
2475
+ username: "PIWI_USERNAME",
2476
+ password: "PIWI_PASSWORD",
2477
+ environment: "PIWI_ENVIRONMENT",
2478
+ label: "PIWI_LABEL",
2479
+ runLabel: "PIWI_RUN_LABEL",
2480
+ keep: "PIWI_KEEP",
2481
+ streaming: "PIWI_STREAMING",
2482
+ streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
2483
+ streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
2484
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
2485
+ liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
2486
+ failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
2487
+ uploadTraces: "PIWI_UPLOAD_TRACES",
2488
+ uploadReport: "PIWI_UPLOAD_REPORT",
2489
+ captureLocators: "PIWI_CAPTURE_LOCATORS",
2490
+ capturePageState: "PIWI_CAPTURE_PAGE_STATE",
2491
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
2492
+ captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
2493
+ codeReachRoots: "PIWI_CODE_REACH_ROOTS",
2494
+ captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
2495
+ sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
2496
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
2497
+ defaultCapture: "PIWI_DEFAULT_CAPTURE",
2498
+ inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
2499
+ pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
2500
+ outputFile: "PIWI_OUTPUT_FILE",
2501
+ aiMode: "PIWI_AI",
2502
+ aiDir: "PIWI_AI_DIR",
2503
+ aiOnMiss: "PIWI_AI_ON_MISS",
2504
+ aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
2505
+ aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
2506
+ aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
2507
+ aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
2508
+ aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
2509
+ };
2510
+ var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
2511
+ var PIWI_SHARD_ENV = "PIWI_SHARD";
2512
+ var PIWI_PROBE_ENV = {
2513
+ flag: "PIWI_PROBE",
2514
+ plan: "PIWI_PROBE_PLAN",
2515
+ results: "PIWI_PROBE_RESULTS",
2516
+ secret: "PIWI_PROBE_SECRET"
2517
+ };
2518
+ var PIWI_FLAKE_ENV = {
2519
+ plan: "PIWI_FLAKE_PLAN",
2520
+ results: "PIWI_FLAKE_RESULTS"
2521
+ };
2522
+ var ENV_FALLBACK_SPECS = [
2523
+ { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
2524
+ { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
2525
+ { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
2526
+ { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
2527
+ { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
2528
+ { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
2529
+ { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
2530
+ { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
2531
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
2532
+ { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
2533
+ { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
2534
+ { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
2535
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
2536
+ { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
2537
+ { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
2538
+ { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
2539
+ { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
2540
+ { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
2541
+ { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
2542
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
2543
+ { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
2544
+ { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
2545
+ { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
2546
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
2547
+ { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
2548
+ { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
2549
+ { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
2550
+ { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
2551
+ ];
2552
+
2553
+ // ../core/src/flake-verdict.ts
2554
+ var FLAKE_SIGNIFICANCE = 0.05;
2555
+ var FLAKE_REPRODUCED_RATE = 0.5;
2556
+ var FLAKE_VERIFY_MIN_RUNS = 5;
2557
+ function logFactorial(n) {
2558
+ let sum = 0;
2559
+ for (let i = 2; i <= n; i++) sum += Math.log(i);
2560
+ return sum;
2561
+ }
2562
+ function logChoose(n, k) {
2563
+ return logFactorial(n) - logFactorial(k) - logFactorial(n - k);
2564
+ }
2565
+ function assertCount(value, name) {
2566
+ if (!Number.isInteger(value) || value < 0) throw new RangeError(`${name} must be a whole number from 0`);
2567
+ }
2568
+ function fisherExactGreater(armFailures, armRuns, controlFailures, controlRuns) {
2569
+ assertCount(armFailures, "armFailures");
2570
+ assertCount(armRuns, "armRuns");
2571
+ assertCount(controlFailures, "controlFailures");
2572
+ assertCount(controlRuns, "controlRuns");
2573
+ if (armFailures > armRuns || controlFailures > controlRuns) throw new RangeError("failures cannot exceed runs");
2574
+ const failures = armFailures + controlFailures;
2575
+ const total = armRuns + controlRuns;
2576
+ const denominator = logChoose(total, failures);
2577
+ const highest = Math.min(armRuns, failures);
2578
+ let p = 0;
2579
+ for (let k = armFailures; k <= highest; k++) {
2580
+ if (failures - k > controlRuns) continue;
2581
+ p += Math.exp(logChoose(armRuns, k) + logChoose(controlRuns, failures - k) - denominator);
2582
+ }
2583
+ return Math.min(1, p);
2584
+ }
2585
+ function flakeArmVerdict(arm, control) {
2586
+ const pValue = fisherExactGreater(arm.matchingFailures, arm.runs, control.matchingFailures, control.runs);
2587
+ const rate = arm.runs > 0 ? arm.matchingFailures / arm.runs : 0;
2588
+ const significant = pValue < FLAKE_SIGNIFICANCE;
2589
+ const verdict = significant && rate >= FLAKE_REPRODUCED_RATE ? "reproduced" : significant ? "amplified" : "not-reproduced";
2590
+ return { verdict, rate, pValue };
2591
+ }
2592
+ function flakeVerifyRuns(rate) {
2593
+ if (!Number.isFinite(rate) || rate <= 0 || rate > 1) throw new RangeError("rate must be above 0 and at most 1");
2594
+ if (rate === 1) return FLAKE_VERIFY_MIN_RUNS;
2595
+ const exact = Math.log(FLAKE_SIGNIFICANCE) / Math.log(1 - rate);
2596
+ return Math.max(FLAKE_VERIFY_MIN_RUNS, Math.ceil(Number(exact.toFixed(9))));
2597
+ }
2598
+ function flakeFixVerified(verify, reproducedRate) {
2599
+ return verify.matchingFailures === 0 && verify.runs >= flakeVerifyRuns(reproducedRate);
2600
+ }
2601
+
2602
+ // src/internal/flake/lab.ts
2603
+ var FAILED = /* @__PURE__ */ new Set(["failed", "timedOut"]);
2604
+ function overlaps(a, b) {
2605
+ return a.startedAt < b.startedAt + b.duration && b.startedAt < a.startedAt + a.duration;
2606
+ }
2607
+ function workerCondition(conditions) {
2608
+ for (const c of conditions) if (c.kind === "alongside" || c.kind === "after") return c;
2609
+ return null;
2610
+ }
2611
+ function roundKept(line, all, conditions) {
2612
+ const condition = workerCondition(conditions);
2613
+ if (!condition) return true;
2614
+ if (condition.kind === "alongside") return all.some((l) => l.role === "companion" && overlaps(l, line));
2615
+ const before = all.filter((l) => l !== line && l.startedAt < line.startedAt).sort((a, b) => b.startedAt - a.startedAt)[0];
2616
+ return before?.role === "companion";
2617
+ }
2618
+ function countArm(batches, arm) {
2619
+ let seen = 0;
2620
+ const kept = [];
2621
+ for (const lines of batches) {
2622
+ 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);
2623
+ seen += targets.length;
2624
+ kept.push(...targets.filter((l) => roundKept(l, lines, arm.conditions)));
2625
+ }
2626
+ const count = {
2627
+ runs: 0,
2628
+ matchingFailures: 0,
2629
+ otherFailures: 0,
2630
+ discardedRounds: seen - kept.length,
2631
+ stoppedEarly: false,
2632
+ otherSignatures: []
2633
+ };
2634
+ const others = /* @__PURE__ */ new Map();
2635
+ for (const [i, line] of kept.entries()) {
2636
+ count.runs++;
2637
+ if (line.matchesHistory) count.matchingFailures++;
2638
+ else if (FAILED.has(line.status)) {
2639
+ count.otherFailures++;
2640
+ const signature = line.errorSignature ?? line.status;
2641
+ others.set(signature, (others.get(signature) ?? 0) + 1);
2642
+ }
2643
+ if (arm.stopAt != null && count.matchingFailures >= arm.stopAt) {
2644
+ count.stoppedEarly = i < kept.length - 1;
2645
+ break;
2646
+ }
2647
+ }
2648
+ count.otherSignatures = [...others.entries()].sort((a, b) => b[1] - a[1]).map(([s]) => s);
2649
+ return count;
2650
+ }
2651
+ function armDone(count, arm) {
2652
+ return arm.stopAt != null && count.matchingFailures >= arm.stopAt || count.runs >= arm.runs;
2653
+ }
2654
+ function nextBatch(count, arm) {
2655
+ const left = Math.max(0, arm.runs - count.runs);
2656
+ return arm.stopAt == null ? left : Math.min(left, FLAKE_BATCH_RUNS);
2657
+ }
2658
+ function escapeRegex(s) {
2659
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2660
+ }
2661
+ function grepFor(tests) {
2662
+ return tests.map((t) => `(^| )${escapeRegex([...t.suite, t.title].join(" "))}( @\\S+)*$`).join("|");
2663
+ }
2664
+ function playwrightArgs(test, arm, repeat) {
2665
+ const companion = workerCondition(arm.conditions);
2666
+ const tests = companion ? [test, companion.test] : [test];
2667
+ const files = [...new Set(tests.map((t) => t.file))];
2668
+ const args = [...files, "--grep", grepFor(tests), `--repeat-each=${repeat}`, "--retries=0"];
2669
+ const alongside = companion?.kind === "alongside";
2670
+ args.push(`--workers=${alongside ? 2 : 1}`);
2671
+ if (alongside && files.length === 1) args.push("--fully-parallel");
2672
+ const project = arm.conditions.find((c) => c.kind === "project");
2673
+ const name = project?.kind === "project" ? project.name : test.project;
2674
+ if (name) args.push(`--project=${name}`);
2675
+ return args;
2676
+ }
2677
+ function armVerdict(arm, control) {
2678
+ return flakeArmVerdict(arm, control);
2679
+ }
2680
+ function verifyVerdict(arm, reproducedRate) {
2681
+ if (arm.matchingFailures > 0) return "still-fails";
2682
+ return flakeFixVerified(arm, reproducedRate) ? "verified" : "inconclusive";
2683
+ }
2684
+ var EXIT_REPRODUCED = 0;
2685
+ var EXIT_NOT_REPRODUCED = 1;
2686
+ var EXIT_ERROR4 = 2;
2687
+ var EXIT_BISECT_SKIP = 125;
2688
+ function exitCodeFor(verdict) {
2689
+ return verdict === "reproduced" || verdict === "verified" ? EXIT_REPRODUCED : EXIT_NOT_REPRODUCED;
2690
+ }
2691
+ function bisectExitCode(verdict) {
2692
+ if (verdict === "verified") return EXIT_REPRODUCED;
2693
+ if (verdict === "still-fails") return EXIT_NOT_REPRODUCED;
2694
+ return EXIT_BISECT_SKIP;
2695
+ }
2696
+ function parseDuration(text) {
2697
+ const trimmed = text.trim().toLowerCase();
2698
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 6e4);
2699
+ const re = /(\d+(?:\.\d+)?)\s*(h|m|s)/g;
2700
+ let total = 0;
2701
+ let consumed = "";
2702
+ for (const m of trimmed.matchAll(re)) {
2703
+ total += Number(m[1]) * (m[2] === "h" ? 36e5 : m[2] === "m" ? 6e4 : 1e3);
2704
+ consumed += m[0];
2705
+ }
2706
+ return consumed.replace(/\s/g, "") === trimmed.replace(/\s/g, "") && total > 0 ? Math.round(total) : null;
2707
+ }
2708
+ function formatDuration(ms) {
2709
+ if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))} s`;
2710
+ const minutes = Math.round(ms / 6e4);
2711
+ if (minutes < 60) return `${minutes} min`;
2712
+ return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
2713
+ }
2714
+ function withinBudget(startedAt, now, budgetMs) {
2715
+ return now - startedAt < budgetMs;
2716
+ }
2717
+
2718
+ // src/internal/support/connection.ts
2719
+ var fs6 = __toESM(require("fs"));
2720
+ var path9 = __toESM(require("path"));
2721
+
2722
+ // ../core/src/dotenv.ts
2723
+ function parseDotEnv(text) {
2724
+ const out = {};
2725
+ for (const raw of text.split(/\r?\n/)) {
2726
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
2727
+ if (!m) continue;
2728
+ let value = m[2];
2729
+ const quote4 = value[0];
2730
+ if ((quote4 === '"' || quote4 === "'") && value.length > 1 && value.endsWith(quote4)) {
2731
+ value = value.slice(1, -1);
2732
+ if (quote4 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
2733
+ } else {
2734
+ value = value.replace(/\s+#.*$/, "");
2735
+ }
2736
+ out[m[1]] = value;
2737
+ }
2738
+ return out;
2739
+ }
2740
+ function resolvePiwiConnection(sources) {
2741
+ const own = {
2742
+ serverUrl: sources.flags?.serverUrl || sources.env?.PIWI_DASHBOARD_URL,
2743
+ apiKey: sources.flags?.apiKey || sources.env?.PIWI_API_KEY
2744
+ };
2745
+ const workspace = { serverUrl: sources.dotEnv?.PIWI_DASHBOARD_URL, apiKey: sources.dotEnv?.PIWI_API_KEY };
2746
+ const project = sources.flags?.project || sources.env?.PIWI_PROJECT_NAME || sources.dotEnv?.PIWI_PROJECT_NAME || "";
2747
+ const desktop = sources.desktop ?? null;
2748
+ const trim = (url) => url.replace(/\/+$/, "");
2749
+ const layer = own.serverUrl ? own : workspace.serverUrl ? workspace : null;
2750
+ const serverUrl = layer ? trim(layer.serverUrl) : desktop ? trim(desktop.url) : "";
2751
+ if (!serverUrl) return null;
2752
+ const desktopKey = desktop && trim(desktop.url) === serverUrl ? desktop.token : null;
2753
+ return { serverUrl, apiKey: layer?.apiKey || desktopKey || null, project };
2754
+ }
2755
+
2756
+ // src/internal/support/connection.ts
2757
+ function resolveCliConnection(flags, env, dirs) {
2758
+ let dotEnv = {};
2759
+ for (const dir of dirs) {
2760
+ try {
2761
+ dotEnv = { ...parseDotEnv(fs6.readFileSync(path9.join(dir, ".env"), "utf-8")), ...dotEnv };
2762
+ } catch {
2763
+ }
2764
+ }
2765
+ return resolvePiwiConnection({
2766
+ flags,
2767
+ env,
2768
+ dotEnv,
2769
+ desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
2770
+ });
2771
+ }
2772
+
2773
+ // src/internal/support/playwright-spawn.ts
2774
+ var import_node_child_process3 = require("child_process");
2775
+ var fs7 = __toESM(require("fs"));
2776
+ var import_node_module2 = require("module");
2777
+ var path10 = __toESM(require("path"));
2778
+ function resolvePlaywrightCli2(cwd = process.cwd()) {
2779
+ const require2 = (0, import_node_module2.createRequire)(path10.join(cwd, "noop.js"));
2780
+ for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
2781
+ try {
2782
+ return require2.resolve(id);
2783
+ } catch {
2784
+ }
2785
+ }
2786
+ return null;
2787
+ }
2788
+ function spawnPlaywright(playwrightArgs2, env, options = {}) {
2789
+ const output = options.output ?? "inherit";
2790
+ let fd = output === "inherit" ? null : fs7.openSync(output.file, "a");
2791
+ const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
2792
+ const cli = resolvePlaywrightCli2();
2793
+ 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], {
2794
+ stdio,
2795
+ env
2796
+ });
2797
+ return new Promise((resolve8) => {
2798
+ const done = (code) => {
2799
+ if (fd !== null) fs7.closeSync(fd);
2800
+ fd = null;
2801
+ resolve8(code);
2802
+ };
2803
+ child.on("error", (err) => {
2804
+ console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
2805
+ done(2);
2806
+ });
2807
+ child.on("exit", (code) => done(code ?? 2));
2808
+ });
2809
+ }
2810
+
2811
+ // src/cli/flake.ts
2812
+ var USAGE4 = `
2813
+ piwi flake \u2014 make a flaky test fail on demand, then prove the fix
2814
+
2815
+ Usage:
2816
+ npx @piwitests/reporter flake <test> [options]
2817
+ npx @piwitests/reporter flake verify <test> [--runs <n>] [--bisect] [--json]
2818
+
2819
+ <test> is a test case id (the number in its dashboard URL) or a spec file and
2820
+ line, tests/checkout.spec.ts:42, looked up in the project on the dashboard.
2821
+
2822
+ It fetches the test's plan from the dashboard: a control arm and one arm per
2823
+ suspect of its flake profile, most likely first. It runs the control (10 runs),
2824
+ then each arm (up to 10 runs, stopping at 3 failures with the same error as in
2825
+ CI), one test at a time with retries off, in the checkout it runs in. It prints
2826
+ each arm against the control with its verdict and saves the experiment on the
2827
+ test's Flakiness tab. The capture fixtures must be in use (piwiFixtures).
2828
+
2829
+ verify reruns the arm that last reproduced the test, and its control, until a
2830
+ matching failure appears or enough runs pass to say the fix holds.
2831
+
2832
+ verify --bisect runs that arm alone at this checkout and saves nothing, for
2833
+ git bisect run: exit 0 (good) when enough runs pass, 1 (bad) on a failure with
2834
+ the same error as in CI, 125 (skip) when this commit cannot tell.
2835
+
2836
+ Options:
2837
+ --suspect <n> Run only the arm of suspect n (its rank on the Flakiness tab)
2838
+ --all Also run every condition at once when none reproduces alone
2839
+ --runs <n> Runs of the control and of each arm (default 10; verify:
2840
+ the number that proves the fix at the rate it reproduced)
2841
+ --budget <duration> Start no new arm after this long: 15m (default), 90s, 1h
2842
+ --no-upload Keep the results off the dashboard; it still needs the plan
2843
+ from the dashboard, or from --plan when it cannot be reached
2844
+ --plan <file> Read the plan from a file (a saved flake-plan response, or
2845
+ the plan the plan_flake_experiment MCP tool returns) and run
2846
+ it without the dashboard; implies --no-upload
2847
+ --bisect verify only: one bisect step, as above; implies --no-upload
2848
+ --source <where> Where the lab runs, recorded on the experiment: cli, ci or
2849
+ desktop (default: ci when CI is set, else cli)
2850
+ --json Print the results as JSON instead of text
2851
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
2852
+ --api-key <key> Reporter API key (env PIWI_API_KEY, .env)
2853
+ --project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME)
2854
+
2855
+ Exit codes: 0 an arm reproduced the failure (verify: the fix held),
2856
+ 1 nothing reproduced (verify: it still fails, or too few runs),
2857
+ 2 error; with --bisect, 0 good, 1 bad, 125 skip, 2 error.
2858
+ `.trim();
2859
+ var DEFAULT_BUDGET_MS = 15 * 6e4;
2860
+ var SOURCES = ["cli", "ci", "desktop"];
2861
+ var MAX_BATCHES = 12;
2862
+ var LOG_TAIL_LINES = 40;
2863
+ function parseFlakeArgs(argv) {
2864
+ const args = {
2865
+ verify: argv[0] === "verify",
2866
+ test: null,
2867
+ suspect: null,
2868
+ all: false,
2869
+ runs: null,
2870
+ budgetMs: DEFAULT_BUDGET_MS,
2871
+ upload: true,
2872
+ planFile: null,
2873
+ bisect: false,
2874
+ source: null,
2875
+ json: false,
2876
+ help: false
2877
+ };
2878
+ const rest = args.verify ? argv.slice(1) : argv;
2879
+ const value = (i, name) => {
2880
+ const v = rest[i + 1];
2881
+ if (v === void 0 || v.startsWith("--")) throw new Error(`${name} needs a value`);
2882
+ return v;
2883
+ };
2884
+ const whole = (text, name, min, max) => {
2885
+ const n = Number(text);
2886
+ if (!Number.isInteger(n) || n < min || n > max)
2887
+ throw new Error(`${name} must be a whole number from ${min} to ${max}`);
2888
+ return n;
2889
+ };
2890
+ for (let i = 0; i < rest.length; i++) {
2891
+ const arg = rest[i];
2892
+ switch (arg) {
2893
+ case "-h":
2894
+ case "--help":
2895
+ args.help = true;
2896
+ break;
2897
+ case "--suspect":
2898
+ args.suspect = whole(value(i++, arg), arg, 1, 50);
2899
+ break;
2900
+ case "--all":
2901
+ args.all = true;
2902
+ break;
2903
+ case "--runs":
2904
+ args.runs = whole(value(i++, arg), arg, 1, 100);
2905
+ break;
2906
+ case "--budget": {
2907
+ const ms = parseDuration(value(i++, arg));
2908
+ if (ms == null) throw new Error("--budget must be a duration like 15m, 90s or 1h");
2909
+ args.budgetMs = ms;
2910
+ break;
2911
+ }
2912
+ case "--no-upload":
2913
+ args.upload = false;
2914
+ break;
2915
+ case "--plan":
2916
+ args.planFile = value(i++, arg);
2917
+ args.upload = false;
2918
+ break;
2919
+ case "--bisect":
2920
+ args.bisect = true;
2921
+ args.upload = false;
2922
+ break;
2923
+ case "--source": {
2924
+ const where = value(i++, arg);
2925
+ if (!SOURCES.includes(where))
2926
+ throw new Error(`--source must be one of ${SOURCES.join(", ")}`);
2927
+ args.source = where;
2928
+ break;
2929
+ }
2930
+ case "--json":
2931
+ args.json = true;
2932
+ break;
2933
+ case "--server-url":
2934
+ args.serverUrl = value(i++, arg);
2935
+ break;
2936
+ case "--api-key":
2937
+ args.apiKey = value(i++, arg);
2938
+ break;
2939
+ case "--project":
2940
+ args.project = value(i++, arg);
2941
+ break;
2942
+ default:
2943
+ if (arg.startsWith("-")) throw new Error(`unknown option ${arg}`);
2944
+ if (args.test !== null) throw new Error(`one test at a time (got "${args.test}" and "${arg}")`);
2945
+ args.test = arg;
2946
+ }
2947
+ }
2948
+ if (args.suspect !== null && args.all) throw new Error("--suspect and --all cannot be used together");
2949
+ if (args.verify && (args.suspect !== null || args.all))
2950
+ throw new Error("verify reruns one arm: drop --suspect and --all");
2951
+ if (args.bisect && !args.verify) throw new Error("--bisect is a verify option: flake verify <test> --bisect");
2952
+ return args;
2953
+ }
2954
+ function gitHead() {
2955
+ try {
2956
+ return (0, import_node_child_process4.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
2957
+ } catch {
2958
+ return null;
2959
+ }
2960
+ }
2961
+ function commitsDiffer(a, b) {
2962
+ if (!a || !b) return false;
2963
+ const [x, y] = [a.toLowerCase(), b.toLowerCase()];
2964
+ return !x.startsWith(y) && !y.startsWith(x);
2965
+ }
2966
+ function short(sha) {
2967
+ return sha ? sha.slice(0, 7) : "unknown";
2968
+ }
2969
+ function fraction(count) {
2970
+ return `${count.matchingFailures}/${count.runs}`;
2971
+ }
2972
+ function formatP(p) {
2973
+ return p < 1e-3 ? "< 0.001" : `= ${p.toFixed(3)}`;
2974
+ }
2975
+ var FlakeError = class extends Error {
2976
+ };
2977
+ async function getJson(http, url) {
2978
+ const res = await fetch(`${http.serverUrl}${url}`, { headers: http.headers });
2979
+ if (!res.ok) {
2980
+ const body = await res.json().catch(() => ({}));
2981
+ throw new FlakeError(body.message || `the dashboard returned ${res.status} for ${url.split("?")[0]}`);
2982
+ }
2983
+ return await res.json();
2984
+ }
2985
+ async function resolveTestCase(http, test, project, apiKey) {
2986
+ if (/^\d+$/.test(test)) return Number(test);
2987
+ const m = /^(.+):(\d+)$/.exec(test);
2988
+ if (!m) throw new FlakeError(`"${test}" is neither a test case id nor file:line`);
2989
+ const projectId = await resolveProjectId({ serverUrl: http.serverUrl, apiKey, project, key: "" });
2990
+ const found = await getJson(
2991
+ http,
2992
+ `/api/projects/${projectId}/flake-lab/test?location=${encodeURIComponent(`${m[1].replace(/\\/g, "/")}:${m[2]}`)}`
2993
+ );
2994
+ return found.testCaseId;
2995
+ }
2996
+ function playwrightRunner(dir, log) {
2997
+ return {
2998
+ now: () => Date.now(),
2999
+ async runBatch(plan, arm, repeat, batch) {
3000
+ const planFile = path11.join(dir, `${arm.id}.plan.json`);
3001
+ if (!fs8.existsSync(planFile)) {
3002
+ fs8.writeFileSync(
3003
+ planFile,
3004
+ JSON.stringify({
3005
+ version: FLAKE_PLAN_VERSION,
3006
+ experimentId: plan.experimentId ?? `local-${process.pid}`,
3007
+ test: plan.test,
3008
+ arm: { id: arm.id, conditions: arm.conditions },
3009
+ errorSignatures: plan.errorSignatures
3010
+ })
3011
+ );
3012
+ }
3013
+ const resultsFile = path11.join(dir, `${arm.id}.${batch}.jsonl`);
3014
+ fs8.writeFileSync(resultsFile, "");
3015
+ const env = {
3016
+ ...process.env,
3017
+ [PIWI_FLAKE_ENV.plan]: planFile,
3018
+ [PIWI_FLAKE_ENV.results]: resultsFile
3019
+ };
3020
+ delete env[PIWI_PROBE_ENV.flag];
3021
+ fs8.appendFileSync(log, `
3022
+ \u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
3023
+ `);
3024
+ await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
3025
+ output: { file: log },
3026
+ command: "piwi flake"
3027
+ });
3028
+ return readResults(resultsFile);
3029
+ }
3030
+ };
3031
+ }
3032
+ function readResults(file) {
3033
+ const lines = [];
3034
+ for (const line of fs8.readFileSync(file, "utf8").split("\n")) {
3035
+ if (!line.trim()) continue;
3036
+ try {
3037
+ lines.push(JSON.parse(line));
3038
+ } catch {
3039
+ }
3040
+ }
3041
+ return lines;
3042
+ }
3043
+ async function runArm(runner, plan, arm) {
3044
+ const batches = [];
3045
+ let count = countArm(batches, arm);
3046
+ for (let batch = 1; batch <= MAX_BATCHES && !armDone(count, arm); batch++) {
3047
+ const repeat = nextBatch(count, arm);
3048
+ if (repeat <= 0) break;
3049
+ const lines = await runner.runBatch(plan, arm, repeat, batch);
3050
+ if (!lines.some((l) => l.role === "target")) {
3051
+ throw new FlakeError(
3052
+ `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?`
3053
+ );
3054
+ }
3055
+ batches.push(lines);
3056
+ count = countArm(batches, arm);
3057
+ }
3058
+ return count;
3059
+ }
3060
+ async function runSession(runner, plan, args, onArm = () => {
3061
+ }) {
3062
+ const started = runner.now();
3063
+ const control = args.bisect ? { arm: plan.control, count: countArm([], plan.control), verdict: null, pValue: null, skipped: "bisect step" } : {
3064
+ arm: plan.control,
3065
+ count: await runArm(runner, plan, plan.control),
3066
+ verdict: null,
3067
+ pValue: null,
3068
+ skipped: null
3069
+ };
3070
+ onArm(control, null);
3071
+ const arms = args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms;
3072
+ const results = [];
3073
+ const run = async (arm) => {
3074
+ if (!withinBudget(started, runner.now(), args.budgetMs)) {
3075
+ const result2 = { arm, count: countArm([], arm), verdict: null, pValue: null, skipped: "budget spent" };
3076
+ results.push(result2);
3077
+ onArm(result2, control.count);
3078
+ return;
3079
+ }
3080
+ const count = await runArm(runner, plan, arm);
3081
+ let verdict;
3082
+ let pValue = null;
3083
+ if (plan.kind === "verify") {
3084
+ verdict = verifyVerdict(count, plan.verifies?.rate ?? 1);
3085
+ } else {
3086
+ const v = armVerdict(count, control.count);
3087
+ verdict = v.verdict;
3088
+ pValue = v.pValue;
3089
+ }
3090
+ const result = { arm, count, verdict, pValue, skipped: null };
3091
+ results.push(result);
3092
+ onArm(result, control.count);
3093
+ };
3094
+ for (const arm of arms) await run(arm);
3095
+ if (args.all && plan.combined && !results.some((r) => r.verdict === "reproduced")) await run(plan.combined);
3096
+ return { control, arms: results };
3097
+ }
3098
+ function sessionVerdict(kind, arms) {
3099
+ if (kind === "verify") {
3100
+ const v = arms.find((a) => a.arm.id === "verify");
3101
+ return {
3102
+ verdict: v?.verdict ?? "inconclusive",
3103
+ reproducingArm: v ? "verify" : null
3104
+ };
3105
+ }
3106
+ const reproduced = arms.find((a) => a.verdict === "reproduced");
3107
+ if (reproduced) return { verdict: "reproduced", reproducingArm: reproduced.arm.id };
3108
+ return {
3109
+ verdict: arms.some((a) => a.verdict === "amplified") ? "amplified" : "not-reproduced",
3110
+ reproducingArm: null
3111
+ };
3112
+ }
3113
+ function pad(text, width) {
3114
+ return text.length >= width ? `${text} ` : text + " ".repeat(width - text.length);
3115
+ }
3116
+ function clip(text, width) {
3117
+ return text.length > width ? `${text.slice(0, width - 1)}\u2026` : text;
3118
+ }
3119
+ function armLine(result, stopAt) {
3120
+ const { arm, count } = result;
3121
+ const name = arm.id === "control" ? "control" : `${arm.rank != null ? `${arm.rank} ` : " "}${arm.label}`;
3122
+ if (result.skipped) return ` ${pad(clip(name, 34), 35)}${result.skipped}`;
3123
+ const notes = [];
3124
+ if (count.stoppedEarly && stopAt != null) notes.push(`stopped at ${stopAt}`);
3125
+ if (count.matchingFailures > 0) notes.push("same error as in CI");
3126
+ if (count.otherFailures > 0)
3127
+ notes.push(`${count.otherFailures} different error${count.otherFailures === 1 ? "" : "s"} (counted apart)`);
3128
+ if (count.discardedRounds > 0) {
3129
+ notes.push(`${count.discardedRounds} round${count.discardedRounds === 1 ? "" : "s"} left out`);
3130
+ }
3131
+ const verdict = result.verdict ? result.verdict.replace("-", " ") : "";
3132
+ return ` ${pad(clip(name, 34), 35)}${pad(fraction(count), 7)}${pad(notes.join(" \xB7 "), 44)}${verdict}`.trimEnd();
3133
+ }
3134
+ function verdictSentence(report, arms, control) {
3135
+ const c = fraction(control.count);
3136
+ const against = control.skipped ? "" : ` (control ${c})`;
3137
+ if (report.kind === "verify") {
3138
+ const v = arms.find((a) => a.arm.id === "verify");
3139
+ if (!v) return "Verdict: the verify arm did not run.";
3140
+ const label = v.arm.label;
3141
+ if (report.verdict === "verified") {
3142
+ return `Verdict: fix verified: 0 matching failures in ${v.count.runs} runs under ${label}${against}`;
3143
+ }
3144
+ if (report.verdict === "still-fails") {
3145
+ return `Verdict: still fails: ${fraction(v.count)} under ${label} failed as in CI${against}`;
3146
+ }
3147
+ return `Verdict: inconclusive: ${v.count.runs} clean runs under ${label}, too few to verify the fix`;
3148
+ }
3149
+ const r = arms.find((a) => a.arm.id === report.reproducingArm);
3150
+ if (r) {
3151
+ return `Verdict: reproduced by ${r.arm.label} (${fraction(r.count)} against ${c}, p ${formatP(r.pValue ?? 1)})`;
3152
+ }
3153
+ const amplified = arms.find((a) => a.verdict === "amplified");
3154
+ if (amplified) {
3155
+ return `Verdict: not reproduced; ${amplified.arm.label} made it fail more often (${fraction(amplified.count)} against ${c}, p ${formatP(amplified.pValue ?? 1)})`;
3156
+ }
3157
+ return `Verdict: not reproduced (control ${c})`;
3158
+ }
3159
+ function bisectStepWord(code) {
3160
+ if (code === 0) return "good (the arm held at this commit)";
3161
+ if (code === 1) return "bad (a failure with the same error as in CI)";
3162
+ return "skip (too few clean runs to tell)";
3163
+ }
3164
+ function printHeader(plan, commit, estimate, budgetMs) {
3165
+ const out = [];
3166
+ out.push(
3167
+ `piwi flake${plan.kind === "verify" ? " verify" : ""} \xB7 ${plan.displayTitle} \xB7 ${plan.failures} failures in ${plan.windowDays} days`
3168
+ );
3169
+ out.push("");
3170
+ if (plan.kind === "verify" && plan.verifies) {
3171
+ out.push(
3172
+ `Rerunning ${plan.verifies.label}: it reproduced ${Math.round(plan.verifies.rate * 100)}% of runs on ${short(plan.verifies.commit)}`
3173
+ );
3174
+ } else if (plan.suspects.length === 0) {
3175
+ out.push("Suspects from history: none. Nothing in its history separates its failures from its passes.");
3176
+ } else {
3177
+ out.push(`${pad("Suspects from history", 57)}condition`);
3178
+ for (const s of plan.suspects) {
3179
+ const counts = `${s.counts.failuresWith}/${s.counts.failures} failures \xB7 ${s.counts.passesWith}/${s.counts.passes} passes`;
3180
+ out.push(
3181
+ ` ${s.rank} ${pad(clip(s.label, 40), 41)}${pad(counts, 30)}${s.skipped ? `none: ${s.skipped}` : s.conditionLabel}`
3182
+ );
3183
+ if (s.sharedRoutes?.length) out.push(` both write ${s.sharedRoutes.join(", ")}`);
3184
+ }
3185
+ }
3186
+ out.push("");
3187
+ const estimateText = estimate != null ? `up to ${formatDuration(estimate)}` : "unknown (no passing run on record)";
3188
+ out.push(`Estimate: ${estimateText} \xB7 budget ${formatDuration(budgetMs)}`);
3189
+ out.push(`Lab \xB7 ${os2.hostname()} \xB7 HEAD ${short(commit)} \xB7 ${plan.test.project ?? "default project"} \xB7 retries off`);
3190
+ if (commitsDiffer(commit, plan.failureCommit)) {
3191
+ out.push(`warning: this checkout is ${short(commit)}; the latest failure ran on ${short(plan.failureCommit)}.`);
3192
+ }
3193
+ console.log(out.join("\n"));
3194
+ }
3195
+ async function loadPlan(args, commit) {
3196
+ if (args.planFile) {
3197
+ try {
3198
+ const raw = JSON.parse(fs8.readFileSync(args.planFile, "utf8"));
3199
+ const plan = "plan" in raw && raw.plan ? raw.plan : raw;
3200
+ if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
3201
+ if (args.verify !== (plan.kind === "verify")) {
3202
+ throw new Error(`it is a ${plan.kind} plan; run ${plan.kind === "verify" ? "flake verify" : "flake"} with it`);
3203
+ }
3204
+ return { plan: args.runs ? withRuns(plan, args.runs) : plan, http: null };
3205
+ } catch (error) {
3206
+ throw new FlakeError(`cannot read the plan ${args.planFile}: ${error.message}`);
3207
+ }
3208
+ }
3209
+ if (!args.test) throw new FlakeError("name the test: a test case id or file:line");
3210
+ const connection = resolveCliConnection(
3211
+ { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
3212
+ process.env,
3213
+ [process.cwd()]
3214
+ );
3215
+ if (!connection) {
3216
+ throw new FlakeError(
3217
+ "no dashboard to read the plan from: pass --server-url or set PIWI_DASHBOARD_URL (or run it offline with --plan <file>)"
3218
+ );
3219
+ }
3220
+ const http = {
3221
+ serverUrl: connection.serverUrl,
3222
+ headers: connection.apiKey ? { "X-API-Key": connection.apiKey } : {}
3223
+ };
3224
+ try {
3225
+ const testCaseId = await resolveTestCase(http, args.test, connection.project, connection.apiKey);
3226
+ const params = new URLSearchParams({
3227
+ kind: args.verify ? "verify" : "reproduce",
3228
+ source: args.source ?? (process.env.CI ? "ci" : "cli"),
3229
+ machine: os2.hostname(),
3230
+ record: String(args.upload)
3231
+ });
3232
+ if (commit) params.set("commit", commit);
3233
+ if (args.runs) params.set("runs", String(args.runs));
3234
+ return { plan: await getJson(http, `/api/test-cases/${testCaseId}/flake-plan?${params}`), http };
3235
+ } catch (error) {
3236
+ if (error instanceof FlakeError) throw error;
3237
+ throw new FlakeError(
3238
+ `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.`
3239
+ );
3240
+ }
3241
+ }
3242
+ function withRuns(plan, runs) {
3243
+ const set = (arm) => ({ ...arm, runs });
3244
+ return {
3245
+ ...plan,
3246
+ control: set(plan.control),
3247
+ arms: plan.arms.map(set),
3248
+ combined: plan.combined && set(plan.combined)
3249
+ };
3250
+ }
3251
+ async function runFlake(argv, runner) {
3252
+ let args;
3253
+ try {
3254
+ args = parseFlakeArgs(argv);
3255
+ } catch (error) {
3256
+ console.error(`piwi flake: ${error.message}
3257
+ `);
3258
+ console.error(USAGE4);
3259
+ return EXIT_ERROR4;
3260
+ }
3261
+ if (args.help) {
3262
+ console.log(USAGE4);
3263
+ return 0;
3264
+ }
3265
+ const commit = gitHead();
3266
+ let plan;
3267
+ let http;
3268
+ try {
3269
+ ({ plan, http } = await loadPlan(args, commit));
3270
+ } catch (error) {
3271
+ console.error(`piwi flake: ${error.message}`);
3272
+ return EXIT_ERROR4;
3273
+ }
3274
+ if (args.suspect != null && !plan.arms.some((a) => a.rank === args.suspect)) {
3275
+ console.error(`piwi flake: suspect ${args.suspect} has no arm (${plan.arms.length} arm(s) in the plan).`);
3276
+ return EXIT_ERROR4;
3277
+ }
3278
+ if (plan.kind === "reproduce" && plan.arms.length === 0) {
3279
+ if (!args.json) printHeader(plan, commit, null, args.budgetMs);
3280
+ console.error("piwi flake: no suspect with a condition to test, so there is nothing to run.");
3281
+ return 1;
3282
+ }
3283
+ const planned = [
3284
+ ...args.bisect ? [] : [plan.control],
3285
+ ...args.suspect != null ? plan.arms.filter((a) => a.rank === args.suspect) : plan.arms
3286
+ ];
3287
+ if (args.all && plan.combined) planned.push(plan.combined);
3288
+ const estimate = estimateFlakeSessionMs(planned, plan.medianDurationMs);
3289
+ if (!args.json) {
3290
+ printHeader(plan, commit, estimate, args.budgetMs);
3291
+ }
3292
+ const dir = fs8.mkdtempSync(path11.join(os2.tmpdir(), "piwi-flake-"));
3293
+ const log = path11.join(dir, "playwright.log");
3294
+ let session;
3295
+ try {
3296
+ session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
3297
+ if (!args.json && !result.skipped?.startsWith("bisect")) console.log(armLine(result, result.arm.stopAt));
3298
+ });
3299
+ } catch (error) {
3300
+ console.error(`piwi flake: ${error.message}`);
3301
+ if (fs8.existsSync(log)) {
3302
+ const tail = fs8.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
3303
+ console.error(`
3304
+ Playwright's output (${log}):
3305
+ ${tail}`);
3306
+ }
3307
+ if (args.bisect) {
3308
+ console.log("Bisect step: skip (this commit could not run the arm)");
3309
+ return EXIT_BISECT_SKIP;
3310
+ }
3311
+ return EXIT_ERROR4;
3312
+ }
3313
+ const { verdict, reproducingArm } = sessionVerdict(plan.kind, session.arms);
3314
+ const ran = [session.control, ...session.arms.filter((a) => !a.skipped)];
3315
+ let uploaded = false;
3316
+ if (args.upload && http && plan.experimentId) {
3317
+ try {
3318
+ const res = await fetch(`${http.serverUrl}/api/projects/${plan.projectId}/flake-lab/results`, {
3319
+ method: "POST",
3320
+ headers: { "Content-Type": "application/json", ...http.headers },
3321
+ body: JSON.stringify({
3322
+ experimentId: plan.experimentId,
3323
+ commit,
3324
+ playwrightProject: plan.test.project,
3325
+ arms: ran.map((r) => ({
3326
+ id: r.arm.id,
3327
+ label: r.arm.label,
3328
+ suspectId: r.arm.suspectId,
3329
+ conditions: r.arm.conditions,
3330
+ runs: r.count.runs,
3331
+ matchingFailures: r.count.matchingFailures,
3332
+ otherFailures: r.count.otherFailures,
3333
+ discardedRounds: r.count.discardedRounds,
3334
+ stoppedEarly: r.count.stoppedEarly
3335
+ }))
3336
+ })
3337
+ });
3338
+ if (!res.ok) {
3339
+ const body = await res.json().catch(() => ({}));
3340
+ throw new Error(body.message || `the dashboard returned ${res.status}`);
3341
+ }
3342
+ uploaded = true;
3343
+ } catch (error) {
3344
+ console.error(`piwi flake: could not save the results \u2014 ${error.message}`);
3345
+ }
3346
+ }
3347
+ const code = args.bisect ? bisectExitCode(verdict) : exitCodeFor(verdict);
3348
+ const verifyCommand = `npx @piwitests/reporter flake verify ${plan.testCaseId}`;
3349
+ const report = {
3350
+ kind: plan.kind,
3351
+ testCaseId: plan.testCaseId,
3352
+ test: plan.displayTitle,
3353
+ experimentId: plan.experimentId,
3354
+ commit,
3355
+ failureCommit: plan.failureCommit,
3356
+ commitsDiffer: commitsDiffer(commit, plan.failureCommit),
3357
+ playwrightProject: plan.test.project,
3358
+ estimateMs: estimate,
3359
+ arms: [session.control, ...session.arms].map((r) => ({
3360
+ id: r.arm.id,
3361
+ label: r.arm.label,
3362
+ suspectId: r.arm.suspectId,
3363
+ rank: r.arm.rank,
3364
+ ...r.count,
3365
+ verdict: r.verdict,
3366
+ pValue: r.pValue,
3367
+ skipped: r.skipped
3368
+ })),
3369
+ verdict,
3370
+ reproducingArm,
3371
+ uploaded,
3372
+ verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
3373
+ exitCode: code
3374
+ };
3375
+ fs8.rmSync(dir, { recursive: true, force: true });
3376
+ if (args.json) {
3377
+ console.log(JSON.stringify(report, null, 2));
3378
+ return code;
3379
+ }
3380
+ const lines = [verdictSentence(report, session.arms, session.control)];
3381
+ if (args.bisect) lines.push(`Bisect step: ${bisectStepWord(code)}`);
3382
+ if (uploaded)
3383
+ lines.push(`Saved to the test's Flakiness tab.${report.verifyCommand ? ` After your fix: ${verifyCommand}` : ""}`);
3384
+ else if (report.verifyCommand) lines.push(`Not saved (--no-upload). After your fix: ${verifyCommand}`);
3385
+ console.log(lines.join("\n"));
3386
+ return code;
3387
+ }
3388
+
3389
+ // src/cli/gate.ts
3390
+ var fs9 = __toESM(require("fs"));
2383
3391
 
2384
3392
  // ../core/src/gate.ts
2385
3393
  function formatGateResult(result) {
@@ -2402,8 +3410,8 @@ function formatGateResult(result) {
2402
3410
  // src/cli/gate.ts
2403
3411
  var EXIT_OK4 = 0;
2404
3412
  var EXIT_VIOLATED = 1;
2405
- var EXIT_ERROR4 = 2;
2406
- var USAGE4 = `
3413
+ var EXIT_ERROR5 = 2;
3414
+ var USAGE5 = `
2407
3415
  piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
2408
3416
 
2409
3417
  Usage:
@@ -2453,9 +3461,9 @@ function readCount(argv, name) {
2453
3461
  if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
2454
3462
  return Math.floor(n);
2455
3463
  }
2456
- function readRunIdFromFile(path16) {
3464
+ function readRunIdFromFile(path19) {
2457
3465
  try {
2458
- const parsed = JSON.parse(fs5.readFileSync(path16, "utf-8"));
3466
+ const parsed = JSON.parse(fs9.readFileSync(path19, "utf-8"));
2459
3467
  const runId = Number(parsed.runId);
2460
3468
  return Number.isFinite(runId) && runId > 0 ? runId : null;
2461
3469
  } catch {
@@ -2510,7 +3518,7 @@ async function requestGate(args) {
2510
3518
  }
2511
3519
  async function runGate(argv, env = process.env) {
2512
3520
  if (argv.includes("-h") || argv.includes("--help")) {
2513
- console.log(USAGE4);
3521
+ console.log(USAGE5);
2514
3522
  return EXIT_OK4;
2515
3523
  }
2516
3524
  let args;
@@ -2519,15 +3527,15 @@ async function runGate(argv, env = process.env) {
2519
3527
  } catch (e) {
2520
3528
  console.error(`piwi gate: ${e.message}
2521
3529
  `);
2522
- console.error(USAGE4);
2523
- return EXIT_ERROR4;
3530
+ console.error(USAGE5);
3531
+ return EXIT_ERROR5;
2524
3532
  }
2525
3533
  let result;
2526
3534
  try {
2527
3535
  result = await requestGate(args);
2528
3536
  } catch (e) {
2529
3537
  console.error(`piwi gate: ${e.message}`);
2530
- return EXIT_ERROR4;
3538
+ return EXIT_ERROR5;
2531
3539
  }
2532
3540
  if (argv.includes("--json")) {
2533
3541
  console.log(JSON.stringify(result, null, 2));
@@ -2538,9 +3546,9 @@ async function runGate(argv, env = process.env) {
2538
3546
  }
2539
3547
 
2540
3548
  // 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");
3549
+ var fs12 = __toESM(require("fs"));
3550
+ var path14 = __toESM(require("path"));
3551
+ var import_node_child_process5 = require("child_process");
2544
3552
 
2545
3553
  // src/cli/report.ts
2546
3554
  var STATUS_MARK = {
@@ -2557,8 +3565,8 @@ function formatStep(result) {
2557
3565
  }
2558
3566
 
2559
3567
  // src/cli/detect.ts
2560
- var fs6 = __toESM(require("fs"));
2561
- var path8 = __toESM(require("path"));
3568
+ var fs10 = __toESM(require("fs"));
3569
+ var path12 = __toESM(require("path"));
2562
3570
  var CONFIG_NAMES = [
2563
3571
  "playwright.config.ts",
2564
3572
  "playwright.config.mts",
@@ -2576,9 +3584,9 @@ var LOCKFILES = [
2576
3584
  ];
2577
3585
  var REPORTER_PACKAGE = "@piwitests/reporter";
2578
3586
  function readPackageJson(root) {
2579
- const pkgPath = path8.join(root, "package.json");
3587
+ const pkgPath = path12.join(root, "package.json");
2580
3588
  try {
2581
- return { pkg: JSON.parse(fs6.readFileSync(pkgPath, "utf-8")), pkgPath };
3589
+ return { pkg: JSON.parse(fs10.readFileSync(pkgPath, "utf-8")), pkgPath };
2582
3590
  } catch {
2583
3591
  return { pkg: null, pkgPath: null };
2584
3592
  }
@@ -2587,14 +3595,14 @@ function detectPackageManager(root, pkg) {
2587
3595
  const declared = pkg?.packageManager?.split("@")[0];
2588
3596
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
2589
3597
  for (const [file, manager] of LOCKFILES) {
2590
- if (fs6.existsSync(path8.join(root, file))) return manager;
3598
+ if (fs10.existsSync(path12.join(root, file))) return manager;
2591
3599
  }
2592
3600
  return "npm";
2593
3601
  }
2594
3602
  function findConfig(root) {
2595
3603
  for (const name of CONFIG_NAMES) {
2596
- const candidate = path8.join(root, name);
2597
- if (fs6.existsSync(candidate)) return candidate;
3604
+ const candidate = path12.join(root, name);
3605
+ if (fs10.existsSync(candidate)) return candidate;
2598
3606
  }
2599
3607
  return null;
2600
3608
  }
@@ -2604,14 +3612,14 @@ function langOf(configPath) {
2604
3612
  }
2605
3613
  function suggestProjectName(root, pkg) {
2606
3614
  const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
2607
- return fromPkg || path8.basename(root) || "default-project";
3615
+ return fromPkg || path12.basename(root) || "default-project";
2608
3616
  }
2609
3617
  function hasReporter(pkg) {
2610
3618
  if (!pkg) return false;
2611
3619
  return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
2612
3620
  }
2613
3621
  function detectProject(root) {
2614
- const absRoot = path8.resolve(root);
3622
+ const absRoot = path12.resolve(root);
2615
3623
  const { pkg, pkgPath } = readPackageJson(absRoot);
2616
3624
  const configPath = findConfig(absRoot);
2617
3625
  return {
@@ -2753,8 +3761,8 @@ function ensureGitignoreEntry(existing, entry = ".env") {
2753
3761
  }
2754
3762
 
2755
3763
  // src/cli/skills.ts
2756
- var fs7 = __toESM(require("fs"));
2757
- var path9 = __toESM(require("path"));
3764
+ var fs11 = __toESM(require("fs"));
3765
+ var path13 = __toESM(require("path"));
2758
3766
  var SETUP_SKILL = "setup-piwi";
2759
3767
  var WORKFLOW_SKILLS = [
2760
3768
  "investigate-failure",
@@ -2765,16 +3773,16 @@ var WORKFLOW_SKILLS = [
2765
3773
  "fix-a-reported-bug"
2766
3774
  ];
2767
3775
  var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
2768
- var DEFAULT_SKILLS_DIR = path9.join(".claude", "skills");
3776
+ var DEFAULT_SKILLS_DIR = path13.join(".claude", "skills");
2769
3777
  function findTemplatesDir(fromDir) {
2770
3778
  let dir = fromDir;
2771
3779
  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);
3780
+ if (fs11.existsSync(path13.join(dir, "templates", "skills"))) return path13.join(dir, "templates");
3781
+ const parent = path13.dirname(dir);
2774
3782
  if (parent === dir) break;
2775
3783
  dir = parent;
2776
3784
  }
2777
- return path9.resolve(fromDir, "..", "..", "templates");
3785
+ return path13.resolve(fromDir, "..", "..", "templates");
2778
3786
  }
2779
3787
  function readFrontMatter(markdown) {
2780
3788
  const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
@@ -2787,14 +3795,14 @@ function readFrontMatter(markdown) {
2787
3795
  return out;
2788
3796
  }
2789
3797
  function templatePath(templatesDir, slug) {
2790
- return path9.join(templatesDir, "skills", slug, "SKILL.md");
3798
+ return path13.join(templatesDir, "skills", slug, "SKILL.md");
2791
3799
  }
2792
3800
  function listSkills(templatesDir, slugs = ALL_SKILLS) {
2793
3801
  const infos = [];
2794
3802
  for (const slug of slugs) {
2795
3803
  const file = templatePath(templatesDir, slug);
2796
- if (!fs7.existsSync(file)) continue;
2797
- const front = readFrontMatter(fs7.readFileSync(file, "utf-8"));
3804
+ if (!fs11.existsSync(file)) continue;
3805
+ const front = readFrontMatter(fs11.readFileSync(file, "utf-8"));
2798
3806
  infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
2799
3807
  }
2800
3808
  return infos;
@@ -2804,16 +3812,16 @@ function installSkills(opts) {
2804
3812
  for (const slug of opts.slugs) {
2805
3813
  const step = `skill:${slug}`;
2806
3814
  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)) {
3815
+ const relDest = path13.join(opts.skillsDir, slug, "SKILL.md");
3816
+ const dest = path13.join(opts.root, relDest);
3817
+ if (!fs11.existsSync(source)) {
2810
3818
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
2811
3819
  continue;
2812
3820
  }
2813
- const contents = fs7.readFileSync(source, "utf-8");
2814
- const exists = fs7.existsSync(dest);
3821
+ const contents = fs11.readFileSync(source, "utf-8");
3822
+ const exists = fs11.existsSync(dest);
2815
3823
  if (exists && !opts.force) {
2816
- const identical = fs7.readFileSync(dest, "utf-8") === contents;
3824
+ const identical = fs11.readFileSync(dest, "utf-8") === contents;
2817
3825
  results.push({
2818
3826
  step,
2819
3827
  file: relDest,
@@ -2823,14 +3831,14 @@ function installSkills(opts) {
2823
3831
  continue;
2824
3832
  }
2825
3833
  if (!opts.dryRun) {
2826
- fs7.mkdirSync(path9.dirname(dest), { recursive: true });
2827
- fs7.writeFileSync(dest, contents);
3834
+ fs11.mkdirSync(path13.dirname(dest), { recursive: true });
3835
+ fs11.writeFileSync(dest, contents);
2828
3836
  }
2829
3837
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail: "installed skill" });
2830
3838
  }
2831
3839
  return results;
2832
3840
  }
2833
- var USAGE5 = `
3841
+ var USAGE6 = `
2834
3842
  piwi skills \u2014 install the Piwi agent skills into this project
2835
3843
 
2836
3844
  Usage:
@@ -2869,7 +3877,7 @@ function positionalSlugs(argv) {
2869
3877
  function runSkills(argv, templatesDir, cwd = process.cwd()) {
2870
3878
  const [sub, ...rest] = argv;
2871
3879
  if (sub === void 0 || sub === "-h" || sub === "--help") {
2872
- console.log(USAGE5);
3880
+ console.log(USAGE6);
2873
3881
  return 0;
2874
3882
  }
2875
3883
  if (sub === "list") {
@@ -2890,12 +3898,12 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
2890
3898
  if (unknown.length) {
2891
3899
  console.error(`piwi skills: unknown skill(s): ${unknown.join(", ")}
2892
3900
  `);
2893
- console.error(USAGE5);
3901
+ console.error(USAGE6);
2894
3902
  return 2;
2895
3903
  }
2896
3904
  const results = installSkills({
2897
3905
  templatesDir,
2898
- root: path9.resolve(readOption5(rest, "--cwd") ?? cwd),
3906
+ root: path13.resolve(readOption5(rest, "--cwd") ?? cwd),
2899
3907
  skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
2900
3908
  slugs: requested.length ? requested : ALL_SKILLS,
2901
3909
  force: rest.includes("--force"),
@@ -2911,13 +3919,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
2911
3919
  }
2912
3920
  console.error(`piwi skills: unknown command "${sub}"
2913
3921
  `);
2914
- console.error(USAGE5);
3922
+ console.error(USAGE6);
2915
3923
  return 2;
2916
3924
  }
2917
3925
 
2918
3926
  // src/cli/init.ts
2919
3927
  var DEFAULT_SERVER_URL = "http://localhost:3000";
2920
- var USAGE6 = `
3928
+ var USAGE7 = `
2921
3929
  piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
2922
3930
 
2923
3931
  Usage:
@@ -2966,7 +3974,7 @@ function resolveSkillSlugs(raw) {
2966
3974
  return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
2967
3975
  }
2968
3976
  function parseInitArgs(argv, env, cwd) {
2969
- const root = path10.resolve(readOption6(argv, "--cwd") ?? cwd);
3977
+ const root = path14.resolve(readOption6(argv, "--cwd") ?? cwd);
2970
3978
  const detected = detectProject(root);
2971
3979
  return {
2972
3980
  root,
@@ -2985,7 +3993,7 @@ function parseInitArgs(argv, env, cwd) {
2985
3993
  }
2986
3994
  function readFileOr(file, fallback) {
2987
3995
  try {
2988
- return fs8.readFileSync(file, "utf-8");
3996
+ return fs12.readFileSync(file, "utf-8");
2989
3997
  } catch {
2990
3998
  return fallback;
2991
3999
  }
@@ -3015,7 +4023,7 @@ function stepInstallReporter(project, opts) {
3015
4023
  if (opts.dryRun || !opts.install)
3016
4024
  return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
3017
4025
  const [command, args] = installArgv(project.packageManager);
3018
- const result = (0, import_node_child_process3.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
4026
+ const result = (0, import_node_child_process5.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
3019
4027
  if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
3020
4028
  return {
3021
4029
  step,
@@ -3031,19 +4039,19 @@ function stepConfig(project, opts) {
3031
4039
  status: "manual",
3032
4040
  detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
3033
4041
  };
3034
- const rel = path10.relative(project.root, project.configPath) || path10.basename(project.configPath);
4042
+ const rel = path14.relative(project.root, project.configPath) || path14.basename(project.configPath);
3035
4043
  const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
3036
4044
  serverUrl: opts.serverUrl,
3037
4045
  projectName: opts.projectName
3038
4046
  });
3039
- if (edit.status === "updated" && !opts.dryRun) fs8.writeFileSync(project.configPath, edit.text);
4047
+ if (edit.status === "updated" && !opts.dryRun) fs12.writeFileSync(project.configPath, edit.text);
3040
4048
  return { step, file: rel, status: edit.status, detail: edit.detail };
3041
4049
  }
3042
4050
  function stepFixtures(project, opts) {
3043
4051
  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)) {
4052
+ const rel = path14.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
4053
+ const dest = path14.join(project.root, rel);
4054
+ if (fs12.existsSync(dest)) {
3047
4055
  const current = readFileOr(dest, "");
3048
4056
  if (/piwiFixtures/.test(current))
3049
4057
  return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
@@ -3055,8 +4063,8 @@ function stepFixtures(project, opts) {
3055
4063
  };
3056
4064
  }
3057
4065
  if (!opts.dryRun) {
3058
- fs8.mkdirSync(path10.dirname(dest), { recursive: true });
3059
- fs8.writeFileSync(dest, fixturesContents());
4066
+ fs12.mkdirSync(path14.dirname(dest), { recursive: true });
4067
+ fs12.writeFileSync(dest, fixturesContents());
3060
4068
  }
3061
4069
  return {
3062
4070
  step,
@@ -3067,12 +4075,12 @@ function stepFixtures(project, opts) {
3067
4075
  }
3068
4076
  function stepEnv(project, opts) {
3069
4077
  const results = [];
3070
- const examplePath = path10.join(project.root, ".env.example");
4078
+ const examplePath = path14.join(project.root, ".env.example");
3071
4079
  const example = upsertEnvKeys(readFileOr(examplePath, ""), [
3072
4080
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
3073
4081
  ["PIWI_API_KEY", ""]
3074
4082
  ]);
3075
- if (example.added.length && !opts.dryRun) fs8.writeFileSync(examplePath, example.text);
4083
+ if (example.added.length && !opts.dryRun) fs12.writeFileSync(examplePath, example.text);
3076
4084
  results.push({
3077
4085
  step: "env",
3078
4086
  file: ".env.example",
@@ -3080,21 +4088,21 @@ function stepEnv(project, opts) {
3080
4088
  detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
3081
4089
  });
3082
4090
  if (opts.apiKey) {
3083
- const envPath = path10.join(project.root, ".env");
4091
+ const envPath = path14.join(project.root, ".env");
3084
4092
  const env = upsertEnvKeys(readFileOr(envPath, ""), [
3085
4093
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
3086
4094
  ["PIWI_API_KEY", opts.apiKey]
3087
4095
  ]);
3088
- if (env.added.length && !opts.dryRun) fs8.writeFileSync(envPath, env.text);
4096
+ if (env.added.length && !opts.dryRun) fs12.writeFileSync(envPath, env.text);
3089
4097
  results.push({
3090
4098
  step: "env",
3091
4099
  file: ".env",
3092
4100
  status: env.added.length ? "updated" : "already",
3093
4101
  detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
3094
4102
  });
3095
- const gitignorePath = path10.join(project.root, ".gitignore");
4103
+ const gitignorePath = path14.join(project.root, ".gitignore");
3096
4104
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
3097
- if (gitignore.added && !opts.dryRun) fs8.writeFileSync(gitignorePath, gitignore.text);
4105
+ if (gitignore.added && !opts.dryRun) fs12.writeFileSync(gitignorePath, gitignore.text);
3098
4106
  results.push({
3099
4107
  step: "gitignore",
3100
4108
  file: ".gitignore",
@@ -3108,7 +4116,7 @@ function nextSteps(opts, steps) {
3108
4116
  const out = [];
3109
4117
  if (!opts.apiKey) {
3110
4118
  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."
4119
+ "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
4120
  );
3113
4121
  }
3114
4122
  if (steps.some((s) => s.status === "manual")) {
@@ -3123,7 +4131,7 @@ function nextSteps(opts, steps) {
3123
4131
  }
3124
4132
  async function runInit(argv, env = process.env, cwd = process.cwd()) {
3125
4133
  if (argv.includes("-h") || argv.includes("--help")) {
3126
- console.log(USAGE6);
4134
+ console.log(USAGE7);
3127
4135
  return 0;
3128
4136
  }
3129
4137
  const opts = parseInitArgs(argv, env, cwd);
@@ -3156,7 +4164,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
3156
4164
  project: {
3157
4165
  root: project.root,
3158
4166
  packageManager: project.packageManager,
3159
- configPath: project.configPath ? path10.relative(project.root, project.configPath) : null,
4167
+ configPath: project.configPath ? path14.relative(project.root, project.configPath) : null,
3160
4168
  projectName: opts.projectName,
3161
4169
  serverUrl: opts.serverUrl
3162
4170
  },
@@ -3180,15 +4188,15 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
3180
4188
  }
3181
4189
 
3182
4190
  // 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");
4191
+ var fs14 = __toESM(require("fs"));
4192
+ var path16 = __toESM(require("path"));
4193
+ var import_node_child_process6 = require("child_process");
4194
+ var import_node_module4 = require("module");
3187
4195
 
3188
4196
  // src/cli/add-reporter.ts
3189
- var fs9 = __toESM(require("fs"));
3190
- var path11 = __toESM(require("path"));
3191
- var import_node_module2 = require("module");
4197
+ var fs13 = __toESM(require("fs"));
4198
+ var path15 = __toESM(require("path"));
4199
+ var import_node_module3 = require("module");
3192
4200
  var REPORTER_PACKAGE2 = "@piwitests/reporter";
3193
4201
  var ADD_REPORTER_FLAG = "--add-reporter";
3194
4202
  var ADD_REPORTER_MIN = { major: 1, minor: 63 };
@@ -3215,14 +4223,14 @@ function decideAddReporter(configSource, playwrightVersion) {
3215
4223
  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
4224
  };
3217
4225
  }
3218
- function resolveConfigPath(cwd, playwrightArgs) {
3219
- for (let i = 0; i < playwrightArgs.length; i++) {
3220
- const arg = playwrightArgs[i];
4226
+ function resolveConfigPath(cwd, playwrightArgs2) {
4227
+ for (let i = 0; i < playwrightArgs2.length; i++) {
4228
+ const arg = playwrightArgs2[i];
3221
4229
  if (arg === "--config" || arg === "-c") {
3222
- const next = playwrightArgs[i + 1];
3223
- if (next) return path11.resolve(cwd, next);
4230
+ const next = playwrightArgs2[i + 1];
4231
+ if (next) return path15.resolve(cwd, next);
3224
4232
  } else if (arg.startsWith("--config=")) {
3225
- return path11.resolve(cwd, arg.slice("--config=".length));
4233
+ return path15.resolve(cwd, arg.slice("--config=".length));
3226
4234
  }
3227
4235
  }
3228
4236
  return detectProject(cwd).configPath;
@@ -3230,14 +4238,14 @@ function resolveConfigPath(cwd, playwrightArgs) {
3230
4238
  function readConfigSource(configPath) {
3231
4239
  if (!configPath) return null;
3232
4240
  try {
3233
- return fs9.readFileSync(configPath, "utf-8");
4241
+ return fs13.readFileSync(configPath, "utf-8");
3234
4242
  } catch {
3235
4243
  return null;
3236
4244
  }
3237
4245
  }
3238
4246
  function readInstalledPlaywrightVersion(cwd) {
3239
4247
  try {
3240
- const require2 = (0, import_node_module2.createRequire)(path11.join(cwd, "noop.js"));
4248
+ const require2 = (0, import_node_module3.createRequire)(path15.join(cwd, "noop.js"));
3241
4249
  for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
3242
4250
  try {
3243
4251
  return require2(id).version ?? null;
@@ -3248,15 +4256,15 @@ function readInstalledPlaywrightVersion(cwd) {
3248
4256
  }
3249
4257
  return null;
3250
4258
  }
3251
- function computeAddReporterArgs(cwd, playwrightArgs) {
3252
- const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs));
4259
+ function computeAddReporterArgs(cwd, playwrightArgs2) {
4260
+ const source = readConfigSource(resolveConfigPath(cwd, playwrightArgs2));
3253
4261
  return decideAddReporter(source, readInstalledPlaywrightVersion(cwd));
3254
4262
  }
3255
4263
 
3256
4264
  // src/cli/select.ts
3257
4265
  var EXIT_OK5 = 0;
3258
- var EXIT_ERROR5 = 2;
3259
- var USAGE7 = `
4266
+ var EXIT_ERROR6 = 2;
4267
+ var USAGE8 = `
3260
4268
  piwi select / piwi run \u2014 run a saved selection of tests
3261
4269
 
3262
4270
  Usage:
@@ -3314,7 +4322,7 @@ function findKey(argv) {
3314
4322
  }
3315
4323
  return void 0;
3316
4324
  }
3317
- function parseDuration(raw) {
4325
+ function parseDuration2(raw) {
3318
4326
  const match = raw.trim().match(/^(\d+(?:\.\d+)?)(ms|s|m|h)?$/);
3319
4327
  if (!match) return null;
3320
4328
  const value = Number(match[1]);
@@ -3334,7 +4342,7 @@ function parseSelectArgs(argv, env) {
3334
4342
  const budgetRaw = readOption7(own, "--budget");
3335
4343
  let budgetMs = null;
3336
4344
  if (budgetRaw !== void 0) {
3337
- budgetMs = parseDuration(budgetRaw);
4345
+ budgetMs = parseDuration2(budgetRaw);
3338
4346
  if (budgetMs === null) throw new Error(`--budget expects a duration like 5m or 90s, got "${budgetRaw}"`);
3339
4347
  }
3340
4348
  const shard = readOption7(own, "--shard") ?? null;
@@ -3357,51 +4365,52 @@ function parseSelectArgs(argv, env) {
3357
4365
  extra
3358
4366
  };
3359
4367
  }
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 ?? ""}`;
4368
+ var CACHE_FILE = path16.join(".piwi", "selection-cache.json");
4369
+ function cacheKey(args) {
4370
+ return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
3363
4371
  }
3364
- function readCache(projectId, args) {
4372
+ function readCache(args) {
3365
4373
  try {
3366
- const store = JSON.parse(fs10.readFileSync(CACHE_FILE, "utf-8"));
3367
- return store[cacheKey(projectId, args)] ?? null;
4374
+ const store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
4375
+ return store[cacheKey(args)] ?? null;
3368
4376
  } catch {
3369
4377
  return null;
3370
4378
  }
3371
4379
  }
3372
- function writeCache(projectId, args, resolution) {
4380
+ function writeCache(args, resolution) {
3373
4381
  try {
3374
4382
  let store = {};
3375
4383
  try {
3376
- store = JSON.parse(fs10.readFileSync(CACHE_FILE, "utf-8"));
4384
+ store = JSON.parse(fs14.readFileSync(CACHE_FILE, "utf-8"));
3377
4385
  } catch {
3378
4386
  }
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));
4387
+ store[cacheKey(args)] = resolution;
4388
+ fs14.mkdirSync(path16.dirname(CACHE_FILE), { recursive: true });
4389
+ fs14.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
3382
4390
  } catch {
3383
4391
  }
3384
4392
  }
3385
- async function resolveWithCache(args, projectId) {
4393
+ async function resolveWithCache(args) {
3386
4394
  try {
4395
+ const projectId = await resolveProjectId(args);
3387
4396
  const resolution = await fetchResolution(args, projectId);
3388
- writeCache(projectId, args, resolution);
4397
+ writeCache(args, resolution);
3389
4398
  return { resolution, fromCache: false };
3390
4399
  } catch (e) {
3391
4400
  if (args.strict) throw e;
3392
- const cached = readCache(projectId, args);
4401
+ const cached = readCache(args);
3393
4402
  if (cached) {
3394
4403
  console.error(`piwi: dashboard unreachable, using cached resolution \u2014 ${e.message}`);
3395
4404
  return { resolution: cached, fromCache: true };
3396
4405
  }
3397
- return null;
4406
+ return { error: e };
3398
4407
  }
3399
4408
  }
3400
4409
  function printWarnings(resolution) {
3401
4410
  for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
3402
4411
  }
3403
4412
  function gitChangedFiles(base) {
3404
- const out = (0, import_node_child_process4.execFileSync)("git", ["diff", "--name-only", base], {
4413
+ const out = (0, import_node_child_process6.execFileSync)("git", ["diff", "--name-only", base], {
3405
4414
  encoding: "utf-8",
3406
4415
  stdio: ["ignore", "pipe", "ignore"]
3407
4416
  });
@@ -3422,7 +4431,7 @@ async function runSelectImpact(args) {
3422
4431
  impact = await loadImpact(args, projectId);
3423
4432
  } catch (e) {
3424
4433
  console.error(`piwi select: ${e.message}`);
3425
- return EXIT_ERROR5;
4434
+ return EXIT_ERROR6;
3426
4435
  }
3427
4436
  printWarnings(impact);
3428
4437
  console.error(
@@ -3437,7 +4446,7 @@ async function runSelectImpact(args) {
3437
4446
  }
3438
4447
  async function runSelect(argv, env = process.env) {
3439
4448
  if (argv.includes("-h") || argv.includes("--help")) {
3440
- console.log(USAGE7);
4449
+ console.log(USAGE8);
3441
4450
  return EXIT_OK5;
3442
4451
  }
3443
4452
  let args;
@@ -3446,13 +4455,13 @@ async function runSelect(argv, env = process.env) {
3446
4455
  } catch (e) {
3447
4456
  console.error(`piwi select: ${e.message}
3448
4457
  `);
3449
- console.error(USAGE7);
3450
- return EXIT_ERROR5;
4458
+ console.error(USAGE8);
4459
+ return EXIT_ERROR6;
3451
4460
  }
3452
4461
  if (args.key === IMPACT_KEY) {
3453
4462
  if (!args.base) {
3454
4463
  console.error("piwi select: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
3455
- return EXIT_ERROR5;
4464
+ return EXIT_ERROR6;
3456
4465
  }
3457
4466
  return runSelectImpact(args);
3458
4467
  }
@@ -3462,19 +4471,19 @@ async function runSelect(argv, env = process.env) {
3462
4471
  resolution = await fetchResolution(args, projectId);
3463
4472
  } catch (e) {
3464
4473
  console.error(`piwi select: ${e.message}`);
3465
- return EXIT_ERROR5;
4474
+ return EXIT_ERROR6;
3466
4475
  }
3467
4476
  printWarnings(resolution);
3468
4477
  if (resolution.estimate.count === 0) {
3469
4478
  console.error(`piwi select: "${args.key}" resolved to 0 tests`);
3470
- return EXIT_ERROR5;
4479
+ return EXIT_ERROR6;
3471
4480
  }
3472
4481
  if (args.json) console.log(JSON.stringify(resolution, null, 2));
3473
4482
  else console.log(resolution.materialization.args.join(" "));
3474
4483
  return EXIT_OK5;
3475
4484
  }
3476
- function resolvePlaywrightCli2() {
3477
- const require2 = (0, import_node_module3.createRequire)(path12.join(process.cwd(), "noop.js"));
4485
+ function resolvePlaywrightCli3() {
4486
+ const require2 = (0, import_node_module4.createRequire)(path16.join(process.cwd(), "noop.js"));
3478
4487
  for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
3479
4488
  try {
3480
4489
  return require2.resolve(id);
@@ -3483,24 +4492,27 @@ function resolvePlaywrightCli2() {
3483
4492
  }
3484
4493
  return null;
3485
4494
  }
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], {
4495
+ function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
4496
+ const cli = resolvePlaywrightCli3();
4497
+ 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
4498
  stdio: "inherit",
3490
4499
  env
3491
4500
  });
3492
4501
  return new Promise((resolve8) => {
3493
4502
  child.on("error", (err) => {
3494
4503
  console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
3495
- resolve8(EXIT_ERROR5);
4504
+ resolve8(EXIT_ERROR6);
3496
4505
  });
3497
- child.on("exit", (code) => resolve8(code ?? EXIT_ERROR5));
4506
+ child.on("exit", (code) => resolve8(code ?? EXIT_ERROR6));
3498
4507
  });
3499
4508
  }
3500
- function spawnPlaywrightForRun(pkgRunner, playwrightArgs, env) {
3501
- const decision = computeAddReporterArgs(process.cwd(), playwrightArgs);
4509
+ function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
4510
+ const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
3502
4511
  if (decision.log) console.error(decision.log);
3503
- return spawnPlaywright(pkgRunner, [...decision.args, ...playwrightArgs], env);
4512
+ return spawnPlaywright2(pkgRunner, [...decision.args, ...playwrightArgs2], env);
4513
+ }
4514
+ function shardEnv(args) {
4515
+ return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
3504
4516
  }
3505
4517
  async function runRunImpact(args, env) {
3506
4518
  let projectId;
@@ -3509,7 +4521,7 @@ async function runRunImpact(args, env) {
3509
4521
  } catch (e) {
3510
4522
  if (args.strict) {
3511
4523
  console.error(`piwi run: ${e.message}`);
3512
- return EXIT_ERROR5;
4524
+ return EXIT_ERROR6;
3513
4525
  }
3514
4526
  console.error(`piwi run: ${e.message} \u2014 running the full suite`);
3515
4527
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
@@ -3520,7 +4532,7 @@ async function runRunImpact(args, env) {
3520
4532
  } catch (e) {
3521
4533
  if (args.strict) {
3522
4534
  console.error(`piwi run: ${e.message}`);
3523
- return EXIT_ERROR5;
4535
+ return EXIT_ERROR6;
3524
4536
  }
3525
4537
  console.error(`piwi run: ${e.message} \u2014 running the full suite`);
3526
4538
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
@@ -3541,14 +4553,15 @@ async function runRunImpact(args, env) {
3541
4553
  PIWI_SELECTION: IMPACT_KEY,
3542
4554
  PIWI_SELECTION_VERSION: "0",
3543
4555
  PIWI_SELECTION_HASH: impact.resolvedHash,
3544
- PIWI_SELECTION_COUNT: String(impact.estimate.count)
4556
+ PIWI_SELECTION_COUNT: String(impact.estimate.count),
4557
+ ...shardEnv(args)
3545
4558
  };
3546
4559
  console.error(`piwi run: impact \u2192 ${impact.estimate.count} test(s)`);
3547
4560
  return spawnPlaywrightForRun(args.pkgRunner, [...impact.materialization.args, ...args.extra], runEnv);
3548
4561
  }
3549
4562
  async function runRun(argv, env = process.env) {
3550
4563
  if (argv.includes("-h") || argv.includes("--help")) {
3551
- console.log(USAGE7);
4564
+ console.log(USAGE8);
3552
4565
  return EXIT_OK5;
3553
4566
  }
3554
4567
  let args;
@@ -3557,54 +4570,44 @@ async function runRun(argv, env = process.env) {
3557
4570
  } catch (e) {
3558
4571
  console.error(`piwi run: ${e.message}
3559
4572
  `);
3560
- console.error(USAGE7);
3561
- return EXIT_ERROR5;
4573
+ console.error(USAGE8);
4574
+ return EXIT_ERROR6;
3562
4575
  }
3563
4576
  if (args.format === "json") {
3564
4577
  console.error("piwi run: --format json cannot be run; use args, grep or files");
3565
- return EXIT_ERROR5;
4578
+ return EXIT_ERROR6;
3566
4579
  }
3567
4580
  if (args.key === IMPACT_KEY) {
3568
4581
  if (!args.base) {
3569
4582
  console.error("piwi run: impact needs a base ref \u2014 pass --base <ref> (e.g. --base origin/main)");
3570
- return EXIT_ERROR5;
4583
+ return EXIT_ERROR6;
3571
4584
  }
3572
4585
  return runRunImpact(args, env);
3573
4586
  }
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
4587
  let outcome;
3586
4588
  try {
3587
- outcome = await resolveWithCache(args, projectId);
4589
+ outcome = await resolveWithCache(args);
3588
4590
  } catch (e) {
3589
4591
  console.error(`piwi run: ${e.message}`);
3590
- return EXIT_ERROR5;
4592
+ return EXIT_ERROR6;
3591
4593
  }
3592
- if (!outcome) {
3593
- console.error("piwi run: dashboard unreachable and no cached resolution \u2014 running the full suite");
4594
+ if ("error" in outcome) {
4595
+ console.error(`piwi run: ${outcome.error.message} \u2014 no cached resolution, running the full suite`);
3594
4596
  return spawnPlaywrightForRun(args.pkgRunner, args.extra, env);
3595
4597
  }
3596
4598
  const { resolution } = outcome;
3597
4599
  printWarnings(resolution);
3598
4600
  if (resolution.estimate.count === 0) {
3599
4601
  console.error(`piwi run: "${args.key}" resolved to 0 tests`);
3600
- return EXIT_ERROR5;
4602
+ return EXIT_ERROR6;
3601
4603
  }
3602
4604
  const runEnv = {
3603
4605
  ...env,
3604
4606
  PIWI_SELECTION: resolution.key ?? args.key,
3605
4607
  PIWI_SELECTION_VERSION: String(resolution.version ?? 0),
3606
4608
  PIWI_SELECTION_HASH: resolution.resolvedHash,
3607
- PIWI_SELECTION_COUNT: String(resolution.estimate.count)
4609
+ PIWI_SELECTION_COUNT: String(resolution.estimate.count),
4610
+ ...shardEnv(args)
3608
4611
  };
3609
4612
  console.error(
3610
4613
  `piwi run: ${args.key} \u2192 ${resolution.estimate.count} tests${resolution.materialization.format !== args.format ? ` (materialized as ${resolution.materialization.format})` : ""}`
@@ -3613,16 +4616,16 @@ async function runRun(argv, env = process.env) {
3613
4616
  }
3614
4617
 
3615
4618
  // 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");
4619
+ var fs15 = __toESM(require("fs"));
4620
+ var path17 = __toESM(require("path"));
4621
+ var import_node_child_process7 = require("child_process");
3619
4622
 
3620
4623
  // ../core/src/diff-anchors.ts
3621
4624
  var RENAME_FROM = `rename from `;
3622
4625
  var RENAME_TO = "rename to ";
3623
4626
  var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
3624
- function stripPrefix(path16) {
3625
- const unquoted = path16.startsWith('"') && path16.endsWith('"') ? path16.slice(1, -1) : path16;
4627
+ function stripPrefix(path19) {
4628
+ const unquoted = path19.startsWith('"') && path19.endsWith('"') ? path19.slice(1, -1) : path19;
3626
4629
  return unquoted.replace(/^[ab]\//, "");
3627
4630
  }
3628
4631
  function parseUnifiedDiff(text) {
@@ -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
  }