@piwitests/reporter 0.60.0 → 0.62.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
@@ -647,11 +647,11 @@ function spawnPlaywright(playwrightArgs2, env, options = {}) {
647
647
  let fd = output === "inherit" ? null : fs3.openSync(output.file, "a");
648
648
  const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
649
649
  const child = (0, import_node_child_process.spawn)(invocation.file, invocation.args, { stdio, env, cwd });
650
- return new Promise((resolve10) => {
650
+ return new Promise((resolve11) => {
651
651
  const done = (code) => {
652
652
  if (fd !== null) fs3.closeSync(fd);
653
653
  fd = null;
654
- resolve10(code);
654
+ resolve11(code);
655
655
  };
656
656
  child.on("error", (err) => {
657
657
  console.error(`${label}: could not start Playwright \u2014 ${err.message}`);
@@ -788,9 +788,8 @@ async function runAi(argv, env = process.env) {
788
788
  }
789
789
 
790
790
  // src/cli/bug.ts
791
- var fs6 = __toESM(require("fs"));
792
- var path9 = __toESM(require("path"));
793
- var import_node_child_process2 = require("child_process");
791
+ var fs7 = __toESM(require("fs"));
792
+ var path10 = __toESM(require("path"));
794
793
 
795
794
  // src/internal/config/env.ts
796
795
  var path8 = __toESM(require("path"));
@@ -870,6 +869,13 @@ function readDotEnv(filePath2) {
870
869
  }
871
870
  }
872
871
 
872
+ // src/internal/support/ci.ts
873
+ function detectCi(env) {
874
+ return Boolean(
875
+ env.JENKINS_URL || env.GITHUB_ACTIONS || env.GITLAB_CI || env.CIRCLECI || env.TRAVIS || env.TF_BUILD || env.BITBUCKET_BUILD_NUMBER || env.TEAMCITY_VERSION || env.CODEBUILD_BUILD_ID || env.bamboo_buildKey || env.CI && env.CI.toLowerCase() !== "false" && env.CI !== "0"
876
+ );
877
+ }
878
+
873
879
  // src/internal/config/env.ts
874
880
  var DEFAULTS = {
875
881
  projectName: "default-project",
@@ -932,6 +938,7 @@ var PIWI_ENV_KEYS = {
932
938
  codeReachRoots: "PIWI_CODE_REACH_ROOTS",
933
939
  coverageBrowser: "PIWI_COVERAGE_BROWSER",
934
940
  coverageServer: "PIWI_COVERAGE_SERVER",
941
+ coverageCss: "PIWI_COVERAGE_CSS",
935
942
  externalIdPattern: "PIWI_EXTERNAL_ID_PATTERN",
936
943
  externalIdOnDuplicate: "PIWI_EXTERNAL_ID_ON_DUPLICATE",
937
944
  captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
@@ -1006,6 +1013,82 @@ var ENV_FALLBACK_SPECS = [
1006
1013
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
1007
1014
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
1008
1015
  ];
1016
+ var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
1017
+ function desktopAppMode(option, envValue) {
1018
+ const fromEnv = envValue?.trim().toLowerCase();
1019
+ if (DESKTOP_APP_MODES.includes(fromEnv)) return fromEnv;
1020
+ if (DESKTOP_APP_MODES.includes(option)) return option;
1021
+ return "fallback";
1022
+ }
1023
+
1024
+ // src/internal/support/connection.ts
1025
+ var fs6 = __toESM(require("fs"));
1026
+ var path9 = __toESM(require("path"));
1027
+ var import_node_child_process2 = require("child_process");
1028
+ var CONFIG_NAMES = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
1029
+ function findPlaywrightRoot(from, root) {
1030
+ const top = root ? path9.resolve(root) : null;
1031
+ let dir = path9.resolve(from);
1032
+ for (; ; ) {
1033
+ if (CONFIG_NAMES.some((name) => fs6.existsSync(path9.join(dir, name)))) return dir;
1034
+ const parent = path9.dirname(dir);
1035
+ if (dir === top || parent === dir) return null;
1036
+ dir = parent;
1037
+ }
1038
+ }
1039
+ function gitRoot(cwd) {
1040
+ try {
1041
+ const out = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
1042
+ cwd,
1043
+ encoding: "utf-8",
1044
+ stdio: ["ignore", "pipe", "ignore"]
1045
+ }).trim();
1046
+ return out ? path9.resolve(out) : null;
1047
+ } catch {
1048
+ return null;
1049
+ }
1050
+ }
1051
+ function connectionFolders(cwd) {
1052
+ const root = gitRoot(cwd);
1053
+ const dirs = [findPlaywrightRoot(cwd, root) ?? cwd, cwd, root];
1054
+ return [...new Set(dirs.filter((dir) => dir !== null).map((dir) => path9.resolve(dir)))];
1055
+ }
1056
+ function projectOf(source) {
1057
+ const project = source[PIWI_ENV_KEYS.projectName];
1058
+ return project ? { [PIWI_ENV_KEYS.projectName]: project } : {};
1059
+ }
1060
+ function readDotEnvConnection(dirs) {
1061
+ const files = dirs.map((dir) => readDotEnv(path9.join(dir, ".env")));
1062
+ const withUrl = files.find((file) => file[PIWI_ENV_KEYS.serverUrl]);
1063
+ const vars = {};
1064
+ if (withUrl) {
1065
+ vars[PIWI_ENV_KEYS.serverUrl] = withUrl[PIWI_ENV_KEYS.serverUrl];
1066
+ if (withUrl[PIWI_ENV_KEYS.apiKey]) vars[PIWI_ENV_KEYS.apiKey] = withUrl[PIWI_ENV_KEYS.apiKey];
1067
+ }
1068
+ const project = files.find((file) => file[PIWI_ENV_KEYS.projectName])?.[PIWI_ENV_KEYS.projectName];
1069
+ if (project) vars[PIWI_ENV_KEYS.projectName] = project;
1070
+ return { vars, anyKey: files.some((file) => file[PIWI_ENV_KEYS.apiKey]) };
1071
+ }
1072
+ function resolveCliConnection(flags, env, dirs, command = "piwi") {
1073
+ const { vars: dotEnv, anyKey } = readDotEnvConnection(dirs);
1074
+ const mode = desktopAppMode(void 0, env[PIWI_ENV_KEYS.desktopApp]);
1075
+ const preferLocally = mode === "prefer" && !flags.serverUrl && !detectCi(env);
1076
+ const urlSet = Boolean(flags.serverUrl || env[PIWI_ENV_KEYS.serverUrl] || dotEnv[PIWI_ENV_KEYS.serverUrl]);
1077
+ const keySet = Boolean(flags.apiKey || env[PIWI_ENV_KEYS.apiKey] || anyKey);
1078
+ const readDesktop = mode !== "off" && (preferLocally || urlSet || !keySet);
1079
+ const desktop = readDesktop ? readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath()) : null;
1080
+ const preferDesktop = desktop !== null && preferLocally;
1081
+ const connection = resolvePiwiConnection(
1082
+ preferDesktop ? { flags, env: projectOf(env), dotEnv: projectOf(dotEnv), desktop } : { flags, env, dotEnv, desktop }
1083
+ );
1084
+ if (connection && flags.apiKey && connection.apiKey !== flags.apiKey) {
1085
+ const from = !preferDesktop && dotEnv[PIWI_ENV_KEYS.serverUrl] ? "a .env file" : "the desktop app";
1086
+ console.error(
1087
+ `${command}: --api-key is not sent to ${connection.serverUrl}, which comes from ${from}: a key goes only to the URL given with it. Pass --server-url to name the instance the key is for.`
1088
+ );
1089
+ }
1090
+ return connection;
1091
+ }
1009
1092
 
1010
1093
  // src/cli/bug.ts
1011
1094
  var EXIT_OK2 = 0;
@@ -1028,8 +1111,8 @@ Options:
1028
1111
  --no-run With --write or --out: write it without running it
1029
1112
  --force Replace an existing file
1030
1113
  --run-mode The spec without test.fail(), as a reproduction runs it
1031
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
1032
- --api-key <key> API key (env PIWI_API_KEY)
1114
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
1115
+ --api-key <key> API key (env PIWI_API_KEY, .env)
1033
1116
 
1034
1117
  Exit codes: 0 printed or written (and, when run, the test behaved as its
1035
1118
  test.fail() expects: the bug reproduces), 1 the run did not (the bug may be
@@ -1043,7 +1126,7 @@ function readOption2(argv, name) {
1043
1126
  const value = argv[index + 1];
1044
1127
  return value !== void 0 && !value.startsWith("--") ? value : void 0;
1045
1128
  }
1046
- function parseBugArgs(argv, env) {
1129
+ function parseBugArgs(argv, env, dirs = []) {
1047
1130
  let idText;
1048
1131
  for (let i = 0; i < argv.length; i++) {
1049
1132
  const token = argv[i];
@@ -1059,8 +1142,13 @@ function parseBugArgs(argv, env) {
1059
1142
  for (const flag of VALUE_FLAGS) {
1060
1143
  if (argv.includes(flag) && readOption2(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
1061
1144
  }
1062
- const serverUrl = (readOption2(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
1063
- if (!serverUrl) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
1145
+ const connection = resolveCliConnection(
1146
+ { serverUrl: readOption2(argv, "--server-url"), apiKey: readOption2(argv, "--api-key") },
1147
+ env,
1148
+ dirs,
1149
+ "piwi bug"
1150
+ );
1151
+ if (!connection) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
1064
1152
  const out = readOption2(argv, "--out") ?? null;
1065
1153
  const write = argv.includes("--write") || out !== null;
1066
1154
  return {
@@ -1070,8 +1158,8 @@ function parseBugArgs(argv, env) {
1070
1158
  run: write && !argv.includes("--no-run"),
1071
1159
  force: argv.includes("--force"),
1072
1160
  mode: argv.includes("--run-mode") ? "run" : "commit",
1073
- serverUrl,
1074
- apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null
1161
+ serverUrl: connection.serverUrl,
1162
+ apiKey: connection.apiKey
1075
1163
  };
1076
1164
  }
1077
1165
  async function fetchBugSpec(args, specDir) {
@@ -1093,35 +1181,23 @@ async function fetchBugSpec(args, specDir) {
1093
1181
  if (typeof body.code !== "string" || typeof body.path !== "string") throw new Error("the dashboard sent no spec");
1094
1182
  return { code: body.code, path: body.path, warnings: Array.isArray(body.warnings) ? body.warnings : [] };
1095
1183
  }
1096
- function repositoryRoot(cwd) {
1097
- try {
1098
- const out = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
1099
- cwd,
1100
- encoding: "utf-8",
1101
- stdio: ["ignore", "pipe", "ignore"]
1102
- }).trim();
1103
- return out ? path9.resolve(out) : null;
1104
- } catch {
1105
- return null;
1106
- }
1107
- }
1108
1184
  function realFolder(dir) {
1109
1185
  const rest = [];
1110
1186
  let current = dir;
1111
- while (!fs6.existsSync(current) && path9.dirname(current) !== current) {
1112
- rest.unshift(path9.basename(current));
1113
- current = path9.dirname(current);
1187
+ while (!fs7.existsSync(current) && path10.dirname(current) !== current) {
1188
+ rest.unshift(path10.basename(current));
1189
+ current = path10.dirname(current);
1114
1190
  }
1115
- return path9.join(fs6.realpathSync(current), ...rest);
1191
+ return path10.join(fs7.realpathSync(current), ...rest);
1116
1192
  }
1117
1193
  function folderInRepository(root, dir) {
1118
- const relative9 = path9.relative(realFolder(root), realFolder(dir));
1119
- if (relative9.startsWith("..") || path9.isAbsolute(relative9)) return void 0;
1120
- return relative9.split(path9.sep).join("/");
1194
+ const relative9 = path10.relative(realFolder(root), realFolder(dir));
1195
+ if (relative9.startsWith("..") || path10.isAbsolute(relative9)) return void 0;
1196
+ return relative9.split(path10.sep).join("/");
1121
1197
  }
1122
1198
  function fileFilter(file) {
1123
1199
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1124
- return `${escape(path9.basename(path9.dirname(file)))}[\\\\/]${escape(path9.basename(file))}$`;
1200
+ return `${escape(path10.basename(path10.dirname(file)))}[\\\\/]${escape(path10.basename(file))}$`;
1125
1201
  }
1126
1202
  function runPlaywright(file, cwd, id, env) {
1127
1203
  return spawnPlaywright([fileFilter(file)], { ...env, ...originEnv("bug", id) }, { cwd, command: "piwi bug" });
@@ -1133,18 +1209,18 @@ async function runBug(argv, env = process.env, cwd = process.cwd()) {
1133
1209
  }
1134
1210
  let args;
1135
1211
  try {
1136
- args = parseBugArgs(argv, env);
1212
+ args = parseBugArgs(argv, env, readOption2(argv, "--server-url") ? [] : connectionFolders(cwd));
1137
1213
  } catch (e) {
1138
1214
  console.error(`piwi bug: ${e.message}
1139
1215
 
1140
1216
  ${USAGE2}`);
1141
1217
  return EXIT_ERROR2;
1142
1218
  }
1143
- const root = args.write ? repositoryRoot(cwd) : null;
1144
- const outFile = args.out !== null ? path9.resolve(cwd, args.out) : null;
1219
+ const root = args.write ? gitRoot(cwd) : null;
1220
+ const outFile = args.out !== null ? path10.resolve(cwd, args.out) : null;
1145
1221
  let spec;
1146
1222
  try {
1147
- spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path9.dirname(outFile)) : void 0);
1223
+ spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path10.dirname(outFile)) : void 0);
1148
1224
  } catch (e) {
1149
1225
  console.error(`piwi bug: ${e.message}`);
1150
1226
  return EXIT_ERROR2;
@@ -1159,16 +1235,16 @@ ${USAGE2}`);
1159
1235
  if (!outFile && !root) {
1160
1236
  console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
1161
1237
  }
1162
- const file = outFile ?? path9.resolve(root ?? cwd, spec.path);
1238
+ const file = outFile ?? path10.resolve(root ?? cwd, spec.path);
1163
1239
  try {
1164
- fs6.mkdirSync(path9.dirname(file), { recursive: true });
1165
- fs6.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
1240
+ fs7.mkdirSync(path10.dirname(file), { recursive: true });
1241
+ fs7.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
1166
1242
  } catch (e) {
1167
1243
  const exists = e.code === "EEXIST";
1168
1244
  console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
1169
1245
  return EXIT_ERROR2;
1170
1246
  }
1171
- const shown = path9.relative(cwd, file) || file;
1247
+ const shown = path10.relative(cwd, file) || file;
1172
1248
  console.error(`piwi bug: wrote ${shown}`);
1173
1249
  if (!args.run) return EXIT_OK2;
1174
1250
  const code = await runPlaywright(file, cwd, args.id, env);
@@ -1186,8 +1262,8 @@ ${USAGE2}`);
1186
1262
  }
1187
1263
 
1188
1264
  // src/cli/codegen.ts
1189
- var fs7 = __toESM(require("fs"));
1190
- var path10 = __toESM(require("path"));
1265
+ var fs8 = __toESM(require("fs"));
1266
+ var path11 = __toESM(require("path"));
1191
1267
 
1192
1268
  // ../core/src/recording.ts
1193
1269
  var ASSERTION_MATCHERS = [
@@ -1878,6 +1954,44 @@ function locatorForStep(step, index, ctx) {
1878
1954
  }
1879
1955
  return `${ctx.page}.${chosen.text}`;
1880
1956
  }
1957
+ function escapedInTemplate(code) {
1958
+ return code < 32 && code !== 9 || code === 127 || code === 8232 || code === 8233;
1959
+ }
1960
+ function templateText(text) {
1961
+ let out = "";
1962
+ for (let i = 0; i < text.length; i++) {
1963
+ const c = text[i];
1964
+ const code = c.charCodeAt(0);
1965
+ if (c === "\\" || c === "`" || c === "$" && text[i + 1] === "{") out += `\\${c}`;
1966
+ else if (escapedInTemplate(code)) out += `\\u${code.toString(16).padStart(4, "0")}`;
1967
+ else out += c;
1968
+ }
1969
+ return out;
1970
+ }
1971
+ function ariaSnapshotLiteral(snapshot, indent) {
1972
+ const lines = snapshot.split(/\r\n?|\n/).filter((line) => line.trim());
1973
+ const common = Math.min(...lines.map((line) => line.length - line.replace(/^ +/, "").length));
1974
+ return ["`", ...lines.map((line) => `${indent} ${templateText(line.slice(common).trimEnd())}`), `${indent}\``];
1975
+ }
1976
+ function renderAriaSnapshotStep(step, index, ctx, not) {
1977
+ const snapshot = step.assertion?.expected ?? "";
1978
+ if (!snapshot.trim()) {
1979
+ ctx.warnings.push({
1980
+ step: index,
1981
+ code: "incomplete-assertion",
1982
+ detail: "toMatchAriaSnapshot",
1983
+ message: "toMatchAriaSnapshot needs the ARIA snapshot to match."
1984
+ });
1985
+ return [` // Step ${index + 1}: toMatchAriaSnapshot without a snapshot.`];
1986
+ }
1987
+ const [open, ...rest] = ariaSnapshotLiteral(snapshot, " ");
1988
+ const close = rest.pop();
1989
+ return [
1990
+ ` await expect(${locatorForStep(step, index, ctx)})${not}.toMatchAriaSnapshot(${open}`,
1991
+ ...rest,
1992
+ `${close});`
1993
+ ];
1994
+ }
1881
1995
  function renderAssertStep(step, index, ctx) {
1882
1996
  const assertion = step.action === "assertVisible" ? null : step.assertion;
1883
1997
  if (step.action === "assert" && !assertion) {
@@ -1888,6 +2002,7 @@ function renderAssertStep(step, index, ctx) {
1888
2002
  const not = assertion?.negated ? ".not" : "";
1889
2003
  const lines = [];
1890
2004
  if (assertion?.note) lines.push(` // ${commentText(assertion.note)}`);
2005
+ if (matcher === "toMatchAriaSnapshot") return [...lines, ...renderAriaSnapshotStep(step, index, ctx, not)];
1891
2006
  let arg = "";
1892
2007
  if (VALUE_MATCHERS.has(matcher)) {
1893
2008
  if (assertion?.expected == null) {
@@ -2320,6 +2435,8 @@ var STEPS_LIMITS = {
2320
2435
  /** Titles, names, texts, locators. */
2321
2436
  textLength: 500,
2322
2437
  viewports: 50,
2438
+ /** The options a manual step's instruction names. */
2439
+ options: 50,
2323
2440
  /** The widest and tallest viewport, in CSS pixels. */
2324
2441
  viewportSize: 1e4,
2325
2442
  /** The browser's zoom factors a viewport may name, as browsers allow them. */
@@ -2491,12 +2608,71 @@ function checkManual(c, v, path25) {
2491
2608
  }
2492
2609
  const description = c.text(v.description, `${path25}.description`, STEPS_LIMITS.valueLength, false);
2493
2610
  if (description != null && !description.trim()) c.fail(`${path25}.description`, "must not be empty");
2611
+ const instruction = readInstruction(v.instruction);
2494
2612
  return {
2495
2613
  description: description ?? "",
2496
2614
  location: c.text(v.location, `${path25}.location`, STEPS_LIMITS.textLength, true),
2497
- call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true)
2615
+ call: c.text(v.call, `${path25}.call`, STEPS_LIMITS.textLength, true),
2616
+ ...instruction ? { instruction } : {}
2498
2617
  };
2499
2618
  }
2619
+ var isText = (v) => typeof v === "string" && v.length <= STEPS_LIMITS.valueLength;
2620
+ var isTextOrNull = (v) => v == null || isText(v);
2621
+ var isCount = (v) => Number.isInteger(v) && v >= 0;
2622
+ var INSTRUCTION_FIELDS = {
2623
+ locator: isText,
2624
+ call: isText,
2625
+ matcher: isText,
2626
+ what: isText,
2627
+ expected: isTextOrNull,
2628
+ received: isTextOrNull,
2629
+ x: (v) => typeof v === "number" && Number.isFinite(v),
2630
+ y: (v) => typeof v === "number" && Number.isFinite(v),
2631
+ count: isCount,
2632
+ limit: isCount,
2633
+ options: (v) => Array.isArray(v) && v.length > 0 && v.length <= STEPS_LIMITS.options && v.every((o) => isText(o) || isCount(o))
2634
+ };
2635
+ var CHECK_VALUES = ["expected", "received"];
2636
+ var INSTRUCTION_KINDS = {
2637
+ goBack: [],
2638
+ goForward: [],
2639
+ reload: [],
2640
+ gotoUnknown: [],
2641
+ setContent: [],
2642
+ keyboardType: [],
2643
+ mouseAt: ["x", "y", "call"],
2644
+ mouseOrKeyboard: ["call"],
2645
+ failedCall: ["call"],
2646
+ locatorCall: ["locator", "call"],
2647
+ fillUnknown: ["locator"],
2648
+ fillPartial: ["locator"],
2649
+ pressUnknown: ["locator"],
2650
+ selectOptionUnknown: ["locator"],
2651
+ setInputFiles: ["locator"],
2652
+ dragTo: ["locator"],
2653
+ drop: ["locator"],
2654
+ focus: ["locator"],
2655
+ blur: ["locator"],
2656
+ dispatchEvent: ["locator"],
2657
+ scrollIntoViewIfNeeded: ["locator"],
2658
+ selectOptions: ["locator", "options"],
2659
+ expectPoll: ["matcher", ...CHECK_VALUES],
2660
+ expectValue: ["what", ...CHECK_VALUES],
2661
+ expectLocator: ["locator", "matcher", ...CHECK_VALUES],
2662
+ expectURL: CHECK_VALUES,
2663
+ leadingSteps: ["count", "limit"]
2664
+ };
2665
+ function readInstruction(v) {
2666
+ if (!isObject(v) || typeof v.kind !== "string" || !Object.prototype.hasOwnProperty.call(INSTRUCTION_KINDS, v.kind))
2667
+ return null;
2668
+ const kind = v.kind;
2669
+ const out = { kind };
2670
+ for (const field of INSTRUCTION_KINDS[kind]) {
2671
+ if (!INSTRUCTION_FIELDS[field](v[field])) return null;
2672
+ out[field] = v[field] ?? null;
2673
+ }
2674
+ return out;
2675
+ }
2500
2676
  function checkViewports(c, v, stepCount) {
2501
2677
  if (v == null) return [];
2502
2678
  if (!Array.isArray(v)) {
@@ -2717,9 +2893,9 @@ Code:
2717
2893
  --tag <tag> Add a tag; repeat for more
2718
2894
 
2719
2895
  Project (optional: your function catalog, and the locators your tests already use):
2720
- --project <name|id> Project (env PIWI_PROJECT_NAME)
2721
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
2722
- --api-key <key> API key (env PIWI_API_KEY)
2896
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
2897
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
2898
+ --api-key <key> API key (env PIWI_API_KEY, .env)
2723
2899
  --offline Do not contact the dashboard
2724
2900
 
2725
2901
  Exit codes: 0 ok, 2 the steps could not be read or the spec could not be written.
@@ -2759,12 +2935,22 @@ function findFile(argv) {
2759
2935
  }
2760
2936
  return void 0;
2761
2937
  }
2762
- function parseCodegenArgs(argv, env) {
2938
+ function parseCodegenArgs(argv, env, dirs = []) {
2763
2939
  const file = findFile(argv);
2764
2940
  if (!file) throw new Error("No steps file \u2014 usage: piwi codegen <steps.json>");
2765
2941
  for (const flag of VALUE_FLAGS2) {
2766
2942
  if (argv.includes(flag) && readOption3(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
2767
2943
  }
2944
+ const connection = resolveCliConnection(
2945
+ {
2946
+ serverUrl: readOption3(argv, "--server-url"),
2947
+ apiKey: readOption3(argv, "--api-key"),
2948
+ project: readOption3(argv, "--project")
2949
+ },
2950
+ env,
2951
+ dirs,
2952
+ "piwi codegen"
2953
+ );
2768
2954
  return {
2769
2955
  file,
2770
2956
  out: readOption3(argv, "--out") ?? null,
@@ -2778,9 +2964,9 @@ function parseCodegenArgs(argv, env) {
2778
2964
  fail: argv.includes("--fail") || argv.includes("--fail-reason"),
2779
2965
  failReason: readOption3(argv, "--fail-reason") ?? null,
2780
2966
  tags: readAll2(argv, "--tag"),
2781
- project: readOption3(argv, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
2782
- serverUrl: (readOption3(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, ""),
2783
- apiKey: readOption3(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
2967
+ project: connection?.project ?? readOption3(argv, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
2968
+ serverUrl: connection?.serverUrl ?? "",
2969
+ apiKey: connection?.apiKey ?? null,
2784
2970
  offline: argv.includes("--offline")
2785
2971
  };
2786
2972
  }
@@ -2839,14 +3025,15 @@ async function loadProjectContext(args) {
2839
3025
  );
2840
3026
  return { catalog, suiteLocators };
2841
3027
  }
2842
- async function runCodegen(argv, env = process.env) {
3028
+ async function runCodegen(argv, env = process.env, cwd = process.cwd()) {
2843
3029
  if (argv.includes("-h") || argv.includes("--help")) {
2844
3030
  console.log(USAGE3);
2845
3031
  return EXIT_OK3;
2846
3032
  }
2847
3033
  let args;
2848
3034
  try {
2849
- args = parseCodegenArgs(argv, env);
3035
+ const given = readOption3(argv, "--server-url") && (readOption3(argv, "--project") || env.PIWI_PROJECT_NAME);
3036
+ args = parseCodegenArgs(argv, env, given ? [] : connectionFolders(cwd));
2850
3037
  } catch (e) {
2851
3038
  console.error(`piwi codegen: ${e.message}
2852
3039
 
@@ -2858,7 +3045,7 @@ ${USAGE3}`);
2858
3045
  if (bugId) {
2859
3046
  if (!args.serverUrl) {
2860
3047
  console.error(
2861
- "piwi codegen: bug:<id> reads the report from the dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL"
3048
+ "piwi codegen: bug:<id> reads the report from the dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)"
2862
3049
  );
2863
3050
  return EXIT_ERROR3;
2864
3051
  }
@@ -2871,8 +3058,8 @@ ${USAGE3}`);
2871
3058
  }
2872
3059
  } else {
2873
3060
  try {
2874
- if (fs7.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
2875
- text = fs7.readFileSync(args.file, "utf-8");
3061
+ if (fs8.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
3062
+ text = fs8.readFileSync(args.file, "utf-8");
2876
3063
  } catch (e) {
2877
3064
  const reason = e.code === "ENOENT" ? "does not exist" : e.message;
2878
3065
  console.error(`piwi codegen: ${args.file} ${reason}`);
@@ -2904,8 +3091,8 @@ ${USAGE3}`);
2904
3091
  return EXIT_OK3;
2905
3092
  }
2906
3093
  try {
2907
- fs7.mkdirSync(path10.dirname(args.out), { recursive: true });
2908
- fs7.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
3094
+ fs8.mkdirSync(path11.dirname(args.out), { recursive: true });
3095
+ fs8.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
2909
3096
  } catch (e) {
2910
3097
  const exists = e.code === "EEXIST";
2911
3098
  console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
@@ -2919,7 +3106,7 @@ ${USAGE3}`);
2919
3106
  }
2920
3107
 
2921
3108
  // src/cli/coverage.ts
2922
- var path15 = __toESM(require("path"));
3109
+ var path16 = __toESM(require("path"));
2923
3110
 
2924
3111
  // ../core/src/codeowners.ts
2925
3112
  function patternParts(pattern) {
@@ -3043,7 +3230,7 @@ function errorMessage(error) {
3043
3230
  }
3044
3231
 
3045
3232
  // src/internal/support/test-paths.ts
3046
- var path11 = __toESM(require("path"));
3233
+ var path12 = __toESM(require("path"));
3047
3234
 
3048
3235
  // src/internal/collect/metadata-collector.ts
3049
3236
  var DETACHED_HEAD = "HEAD";
@@ -3076,23 +3263,23 @@ function normalizeRef(ref) {
3076
3263
 
3077
3264
  // src/internal/coverage-upload/git-inventory.ts
3078
3265
  var import_node_child_process5 = require("child_process");
3079
- var fs9 = __toESM(require("fs"));
3080
- var path13 = __toESM(require("path"));
3266
+ var fs10 = __toESM(require("fs"));
3267
+ var path14 = __toESM(require("path"));
3081
3268
 
3082
3269
  // src/internal/collect/blob-ids.ts
3083
3270
  var import_node_child_process4 = require("child_process");
3084
3271
  var crypto2 = __toESM(require("crypto"));
3085
- var fs8 = __toESM(require("fs"));
3086
- var path12 = __toESM(require("path"));
3272
+ var fs9 = __toESM(require("fs"));
3273
+ var path13 = __toESM(require("path"));
3087
3274
  function gitHashObjects(root, paths) {
3088
- return new Promise((resolve10) => {
3275
+ return new Promise((resolve11) => {
3089
3276
  const child = (0, import_node_child_process4.execFile)(
3090
3277
  "git",
3091
3278
  ["hash-object", "--stdin-paths"],
3092
3279
  { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
3093
3280
  (error, stdout) => {
3094
3281
  const ids = error ? [] : stdout.split("\n").filter(Boolean);
3095
- resolve10(ids.length === paths.length ? ids : null);
3282
+ resolve11(ids.length === paths.length ? ids : null);
3096
3283
  }
3097
3284
  );
3098
3285
  child.stdin?.on("error", () => {
@@ -3106,12 +3293,12 @@ function gitHashObjects(root, paths) {
3106
3293
  var MAX_GIT_OUTPUT = 512 * 1024 * 1024;
3107
3294
  var GIT_BATCH = 500;
3108
3295
  function git(cwd, args) {
3109
- return new Promise((resolve10, reject) => {
3296
+ return new Promise((resolve11, reject) => {
3110
3297
  (0, import_node_child_process5.execFile)(
3111
3298
  "git",
3112
3299
  args,
3113
3300
  { cwd, encoding: "utf8", maxBuffer: MAX_GIT_OUTPUT, timeout: 12e4, windowsHide: true },
3114
- (error, stdout) => error ? reject(error) : resolve10(stdout)
3301
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
3115
3302
  );
3116
3303
  });
3117
3304
  }
@@ -3150,7 +3337,7 @@ async function readInventory(root, warn = () => {
3150
3337
  const changed = [];
3151
3338
  for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
3152
3339
  if (!file || !inventory.has(file)) continue;
3153
- if (fs9.existsSync(path13.join(root, file))) changed.push(file);
3340
+ if (fs10.existsSync(path14.join(root, file))) changed.push(file);
3154
3341
  else inventory.delete(file);
3155
3342
  }
3156
3343
  const ids = await hashPaths(root, changed);
@@ -3169,8 +3356,8 @@ async function readInventory(root, warn = () => {
3169
3356
  }
3170
3357
 
3171
3358
  // src/internal/coverage-upload/report-files.ts
3172
- var fs10 = __toESM(require("fs"));
3173
- var path14 = __toESM(require("path"));
3359
+ var fs11 = __toESM(require("fs"));
3360
+ var path15 = __toESM(require("path"));
3174
3361
 
3175
3362
  // ../core/src/coverage.ts
3176
3363
  var MAX_LINE = 1e6;
@@ -3682,14 +3869,14 @@ function globSegmentRegExp(segment) {
3682
3869
  function expandPattern(pattern, cwd) {
3683
3870
  const normalized = pattern.replace(/\\/g, "/");
3684
3871
  if (!/[*?]/.test(normalized)) {
3685
- const file = path14.resolve(cwd, normalized);
3686
- return fs10.existsSync(file) && fs10.statSync(file).isFile() ? [file] : [];
3872
+ const file = path15.resolve(cwd, normalized);
3873
+ return fs11.existsSync(file) && fs11.statSync(file).isFile() ? [file] : [];
3687
3874
  }
3688
- const absolute = path14.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3875
+ const absolute = path15.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3689
3876
  const segments = normalized.split("/");
3690
3877
  const firstWild = segments.findIndex((s) => /[*?]/.test(s));
3691
3878
  const baseParts = segments.slice(0, firstWild);
3692
- const base = absolute ? baseParts.join("/") || "/" : path14.resolve(cwd, ...baseParts);
3879
+ const base = absolute ? baseParts.join("/") || "/" : path15.resolve(cwd, ...baseParts);
3693
3880
  const rest = segments.slice(firstWild).filter((s) => s !== "");
3694
3881
  const out = [];
3695
3882
  const walk = (dir, at) => {
@@ -3697,14 +3884,14 @@ function expandPattern(pattern, cwd) {
3697
3884
  const segment = rest[at];
3698
3885
  let entries;
3699
3886
  try {
3700
- entries = fs10.readdirSync(dir, { withFileTypes: true });
3887
+ entries = fs11.readdirSync(dir, { withFileTypes: true });
3701
3888
  } catch {
3702
3889
  return;
3703
3890
  }
3704
3891
  if (segment === "**") {
3705
3892
  walk(dir, at + 1);
3706
3893
  for (const entry of entries) {
3707
- if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path14.join(dir, entry.name), at);
3894
+ if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path15.join(dir, entry.name), at);
3708
3895
  }
3709
3896
  return;
3710
3897
  }
@@ -3712,7 +3899,7 @@ function expandPattern(pattern, cwd) {
3712
3899
  const last = at === rest.length - 1;
3713
3900
  for (const entry of entries) {
3714
3901
  if (!match.test(entry.name)) continue;
3715
- const full = path14.join(dir, entry.name);
3902
+ const full = path15.join(dir, entry.name);
3716
3903
  if (last && entry.isFile()) out.push(full);
3717
3904
  else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
3718
3905
  walk(full, at + 1);
@@ -3725,7 +3912,7 @@ function expandPattern(pattern, cwd) {
3725
3912
  async function readReport(file, format) {
3726
3913
  let reader = format ? new CoverageReportReader(format) : null;
3727
3914
  let head = "";
3728
- for await (const chunk of fs10.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3915
+ for await (const chunk of fs11.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3729
3916
  if (!reader) {
3730
3917
  head += chunk;
3731
3918
  if (head.length < 4096) continue;
@@ -3747,18 +3934,18 @@ async function readReport(file, format) {
3747
3934
  return reader.end();
3748
3935
  }
3749
3936
  function reportBases(report, reportFile, options) {
3750
- if (options.root) return [path14.resolve(options.cwd, options.root)];
3751
- const dir = path14.dirname(path14.resolve(reportFile));
3937
+ if (options.root) return [path15.resolve(options.cwd, options.root)];
3938
+ const dir = path15.dirname(path15.resolve(reportFile));
3752
3939
  const sources = report.sources.map(
3753
- (source) => path14.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path14.resolve(dir, source)
3940
+ (source) => path15.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path15.resolve(dir, source)
3754
3941
  );
3755
- return [...sources, options.cwd, dir, path14.dirname(dir), options.repoRoot];
3942
+ return [...sources, options.cwd, dir, path15.dirname(dir), options.repoRoot];
3756
3943
  }
3757
3944
  function resolveReport(report, reportFile, options, into) {
3758
3945
  const bases = reportBases(report, reportFile, options);
3759
3946
  for (const file of report.files) {
3760
3947
  const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
3761
- const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs10.existsSync(path14.join(options.repoRoot, candidate)));
3948
+ const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs11.existsSync(path15.join(options.repoRoot, candidate)));
3762
3949
  if (!match) {
3763
3950
  into.unmatched.push(file.path);
3764
3951
  continue;
@@ -3798,9 +3985,9 @@ Suite:
3798
3985
  --kind <kind> unit (the default) or e2e
3799
3986
 
3800
3987
  Connection:
3801
- --project <name|id> Project (env PIWI_PROJECT_NAME)
3802
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
3803
- --api-key <key> API key (env PIWI_API_KEY)
3988
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
3989
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
3990
+ --api-key <key> API key (env PIWI_API_KEY, .env)
3804
3991
  --branch <name> The branch (env PIWI_BRANCH, the CI's, else git's)
3805
3992
 
3806
3993
  Other:
@@ -3820,7 +4007,7 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
3820
4007
  "--api-key",
3821
4008
  "--branch"
3822
4009
  ]);
3823
- function parseCoverageUploadArgs(argv, env) {
4010
+ function parseCoverageUploadArgs(argv, env, dirs = []) {
3824
4011
  const values = /* @__PURE__ */ new Map();
3825
4012
  const patterns2 = [];
3826
4013
  const flags = /* @__PURE__ */ new Set();
@@ -3851,11 +4038,19 @@ function parseCoverageUploadArgs(argv, env) {
3851
4038
  if (format !== void 0 && format !== "cobertura" && format !== "lcov" && format !== "piwi") {
3852
4039
  throw new Error("--format is cobertura, lcov or piwi");
3853
4040
  }
3854
- const serverUrl = (values.get("--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
4041
+ const connection = resolveCliConnection(
4042
+ { serverUrl: values.get("--server-url"), apiKey: values.get("--api-key"), project: values.get("--project") },
4043
+ env,
4044
+ dirs,
4045
+ "piwi coverage upload"
4046
+ );
4047
+ const serverUrl = connection?.serverUrl ?? "";
3855
4048
  const dryRun = flags.has("--dry-run");
3856
- if (!serverUrl && !dryRun) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
3857
- const project = values.get("--project") ?? env.PIWI_PROJECT_NAME ?? "";
3858
- if (!project && !dryRun) throw new Error("No project \u2014 pass --project or set PIWI_PROJECT_NAME");
4049
+ if (!serverUrl && !dryRun)
4050
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
4051
+ const project = connection?.project ?? values.get("--project") ?? env.PIWI_PROJECT_NAME ?? "";
4052
+ if (!project && !dryRun)
4053
+ throw new Error("No project \u2014 pass --project or set PIWI_PROJECT_NAME (environment or .env)");
3859
4054
  return {
3860
4055
  patterns: patterns2,
3861
4056
  suite,
@@ -3864,7 +4059,7 @@ function parseCoverageUploadArgs(argv, env) {
3864
4059
  ...values.get("--root") ? { root: values.get("--root") } : {},
3865
4060
  project,
3866
4061
  serverUrl,
3867
- apiKey: values.get("--api-key") ?? env.PIWI_API_KEY ?? null,
4062
+ apiKey: connection?.apiKey ?? null,
3868
4063
  ...values.get("--branch") ? { branch: values.get("--branch") } : {},
3869
4064
  dryRun,
3870
4065
  json: flags.has("--json")
@@ -3983,7 +4178,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
3983
4178
  commit: checkout.commit,
3984
4179
  branch,
3985
4180
  suite: args.suite,
3986
- reports: reports.map((report) => path15.relative(cwd, report) || report),
4181
+ reports: reports.map((report) => path16.relative(cwd, report) || report),
3987
4182
  files: files.length,
3988
4183
  unmatched: resolved.unmatched.length,
3989
4184
  outOfScope: resolved.outOfScope,
@@ -4030,7 +4225,7 @@ ${USAGE4}`);
4030
4225
  }
4031
4226
  let args;
4032
4227
  try {
4033
- args = parseCoverageUploadArgs(rest, env);
4228
+ args = parseCoverageUploadArgs(rest, env, [cwd]);
4034
4229
  } catch (error) {
4035
4230
  console.error(`piwi coverage upload: ${errorMessage(error)}`);
4036
4231
  return EXIT_ERROR4;
@@ -4053,7 +4248,7 @@ ${USAGE4}`);
4053
4248
 
4054
4249
  // src/cli/flake.ts
4055
4250
  var import_node_child_process6 = require("child_process");
4056
- var fs11 = __toESM(require("fs"));
4251
+ var fs12 = __toESM(require("fs"));
4057
4252
  var os2 = __toESM(require("os"));
4058
4253
  var path17 = __toESM(require("path"));
4059
4254
 
@@ -4256,19 +4451,6 @@ function withinBudget(startedAt, now, budgetMs) {
4256
4451
  return now - startedAt < budgetMs;
4257
4452
  }
4258
4453
 
4259
- // src/internal/support/connection.ts
4260
- var path16 = __toESM(require("path"));
4261
- function resolveCliConnection(flags, env, dirs) {
4262
- let dotEnv = {};
4263
- for (const dir of dirs) dotEnv = { ...readDotEnv(path16.join(dir, ".env")), ...dotEnv };
4264
- return resolvePiwiConnection({
4265
- flags,
4266
- env,
4267
- dotEnv,
4268
- desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
4269
- });
4270
- }
4271
-
4272
4454
  // src/cli/flake.ts
4273
4455
  var USAGE5 = `
4274
4456
  piwi flake \u2014 make a flaky test fail on demand, then prove the fix
@@ -4311,7 +4493,7 @@ Options:
4311
4493
  --json Print the results as JSON instead of text
4312
4494
  --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
4313
4495
  --api-key <key> Reporter API key (env PIWI_API_KEY, .env)
4314
- --project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME)
4496
+ --project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME, .env)
4315
4497
 
4316
4498
  Exit codes: 0 an arm reproduced the failure (verify: the fix held),
4317
4499
  1 nothing reproduced (verify: it still fails, or too few runs),
@@ -4459,8 +4641,8 @@ function playwrightRunner(dir, log) {
4459
4641
  now: () => Date.now(),
4460
4642
  async runBatch(plan, arm, repeat, batch) {
4461
4643
  const planFile = path17.join(dir, `${arm.id}.plan.json`);
4462
- if (!fs11.existsSync(planFile)) {
4463
- fs11.writeFileSync(
4644
+ if (!fs12.existsSync(planFile)) {
4645
+ fs12.writeFileSync(
4464
4646
  planFile,
4465
4647
  JSON.stringify({
4466
4648
  version: FLAKE_PLAN_VERSION,
@@ -4472,14 +4654,14 @@ function playwrightRunner(dir, log) {
4472
4654
  );
4473
4655
  }
4474
4656
  const resultsFile = path17.join(dir, `${arm.id}.${batch}.jsonl`);
4475
- fs11.writeFileSync(resultsFile, "");
4657
+ fs12.writeFileSync(resultsFile, "");
4476
4658
  const env = {
4477
4659
  ...process.env,
4478
4660
  [PIWI_FLAKE_ENV.plan]: planFile,
4479
4661
  [PIWI_FLAKE_ENV.results]: resultsFile
4480
4662
  };
4481
4663
  delete env[PIWI_PROBE_ENV.flag];
4482
- fs11.appendFileSync(log, `
4664
+ fs12.appendFileSync(log, `
4483
4665
  \u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
4484
4666
  `);
4485
4667
  await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
@@ -4492,7 +4674,7 @@ function playwrightRunner(dir, log) {
4492
4674
  }
4493
4675
  function readResults(file) {
4494
4676
  const lines = [];
4495
- for (const line of fs11.readFileSync(file, "utf8").split("\n")) {
4677
+ for (const line of fs12.readFileSync(file, "utf8").split("\n")) {
4496
4678
  if (!line.trim()) continue;
4497
4679
  try {
4498
4680
  lines.push(JSON.parse(line));
@@ -4656,7 +4838,7 @@ function printHeader(plan, commit, estimate, budgetMs) {
4656
4838
  async function loadPlan(args, commit) {
4657
4839
  if (args.planFile) {
4658
4840
  try {
4659
- const raw = JSON.parse(fs11.readFileSync(args.planFile, "utf8"));
4841
+ const raw = JSON.parse(fs12.readFileSync(args.planFile, "utf8"));
4660
4842
  const plan = "plan" in raw && raw.plan ? raw.plan : raw;
4661
4843
  if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
4662
4844
  if (args.verify !== (plan.kind === "verify")) {
@@ -4671,7 +4853,8 @@ async function loadPlan(args, commit) {
4671
4853
  const connection = resolveCliConnection(
4672
4854
  { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
4673
4855
  process.env,
4674
- [process.cwd()]
4856
+ [process.cwd()],
4857
+ args.verify ? "piwi flake verify" : "piwi flake"
4675
4858
  );
4676
4859
  if (!connection) {
4677
4860
  throw new FlakeError(
@@ -4750,7 +4933,7 @@ async function runFlake(argv, runner) {
4750
4933
  if (!args.json) {
4751
4934
  printHeader(plan, commit, estimate, args.budgetMs);
4752
4935
  }
4753
- const dir = fs11.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
4936
+ const dir = fs12.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
4754
4937
  const log = path17.join(dir, "playwright.log");
4755
4938
  let session;
4756
4939
  try {
@@ -4759,8 +4942,8 @@ async function runFlake(argv, runner) {
4759
4942
  });
4760
4943
  } catch (error) {
4761
4944
  console.error(`piwi flake: ${error.message}`);
4762
- if (fs11.existsSync(log)) {
4763
- const tail = fs11.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
4945
+ if (fs12.existsSync(log)) {
4946
+ const tail = fs12.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
4764
4947
  console.error(`
4765
4948
  Playwright's output (${log}):
4766
4949
  ${tail}`);
@@ -4833,7 +5016,7 @@ ${tail}`);
4833
5016
  verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
4834
5017
  exitCode: code
4835
5018
  };
4836
- fs11.rmSync(dir, { recursive: true, force: true });
5019
+ fs12.rmSync(dir, { recursive: true, force: true });
4837
5020
  if (args.json) {
4838
5021
  console.log(JSON.stringify(report, null, 2));
4839
5022
  return code;
@@ -4848,7 +5031,7 @@ ${tail}`);
4848
5031
  }
4849
5032
 
4850
5033
  // src/cli/gate.ts
4851
- var fs12 = __toESM(require("fs"));
5034
+ var fs13 = __toESM(require("fs"));
4852
5035
 
4853
5036
  // ../core/src/gate.ts
4854
5037
  function formatGateResult(result) {
@@ -4894,8 +5077,8 @@ Where the run comes from (first match wins):
4894
5077
  (default) ./piwi-run.json, if it exists
4895
5078
 
4896
5079
  Connection:
4897
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
4898
- --api-key <key> API key (env PIWI_API_KEY)
5080
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
5081
+ --api-key <key> API key (env PIWI_API_KEY, .env)
4899
5082
 
4900
5083
  Policy (at least one rule or warn-only flag is required):
4901
5084
  --require-tag <tags> Comma-separated; every test carrying the tag must pass
@@ -4944,16 +5127,22 @@ function readCount(argv, name) {
4944
5127
  }
4945
5128
  function readRunIdFromFile(path25) {
4946
5129
  try {
4947
- const parsed = JSON.parse(fs12.readFileSync(path25, "utf-8"));
5130
+ const parsed = JSON.parse(fs13.readFileSync(path25, "utf-8"));
4948
5131
  const runId = Number(parsed.runId);
4949
5132
  return Number.isFinite(runId) && runId > 0 ? runId : null;
4950
5133
  } catch {
4951
5134
  return null;
4952
5135
  }
4953
5136
  }
4954
- function parseGateArgs(argv, env) {
4955
- const serverUrl = (readOption4(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
4956
- if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
5137
+ function parseGateArgs(argv, env, dirs = []) {
5138
+ const connection = resolveCliConnection(
5139
+ { serverUrl: readOption4(argv, "--server-url"), apiKey: readOption4(argv, "--api-key") },
5140
+ env,
5141
+ dirs,
5142
+ "piwi gate"
5143
+ );
5144
+ if (!connection)
5145
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
4957
5146
  let runId = Number(readOption4(argv, "--run-id") ?? NaN);
4958
5147
  if (!Number.isFinite(runId) || runId <= 0) {
4959
5148
  const candidate = readOption4(argv, "--from-file") ?? env.PIWI_OUTPUT_FILE ?? "piwi-run.json";
@@ -4978,8 +5167,8 @@ function parseGateArgs(argv, env) {
4978
5167
  maxNewLeaks: readCount(argv, "--max-new-leaks")
4979
5168
  };
4980
5169
  return {
4981
- serverUrl,
4982
- apiKey: readOption4(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
5170
+ serverUrl: connection.serverUrl,
5171
+ apiKey: connection.apiKey,
4983
5172
  runId,
4984
5173
  policy,
4985
5174
  maxUncoveredChanges: readCount(argv, "--max-uncovered-changes"),
@@ -5004,14 +5193,14 @@ async function requestGate(args) {
5004
5193
  }
5005
5194
  return await res.json();
5006
5195
  }
5007
- async function runGate(argv, env = process.env) {
5196
+ async function runGate(argv, env = process.env, cwd = process.cwd()) {
5008
5197
  if (argv.includes("-h") || argv.includes("--help")) {
5009
5198
  console.log(USAGE6);
5010
5199
  return EXIT_OK5;
5011
5200
  }
5012
5201
  let args;
5013
5202
  try {
5014
- args = parseGateArgs(argv, env);
5203
+ args = parseGateArgs(argv, env, [cwd]);
5015
5204
  } catch (e) {
5016
5205
  console.error(`piwi gate: ${e.message}
5017
5206
  `);
@@ -5035,7 +5224,7 @@ async function runGate(argv, env = process.env) {
5035
5224
  }
5036
5225
 
5037
5226
  // src/cli/init.ts
5038
- var fs15 = __toESM(require("fs"));
5227
+ var fs16 = __toESM(require("fs"));
5039
5228
  var path20 = __toESM(require("path"));
5040
5229
 
5041
5230
  // src/cli/report.ts
@@ -5053,9 +5242,9 @@ function formatStep(result) {
5053
5242
  }
5054
5243
 
5055
5244
  // src/cli/detect.ts
5056
- var fs13 = __toESM(require("fs"));
5245
+ var fs14 = __toESM(require("fs"));
5057
5246
  var path18 = __toESM(require("path"));
5058
- var CONFIG_NAMES = [
5247
+ var CONFIG_NAMES2 = [
5059
5248
  "playwright.config.ts",
5060
5249
  "playwright.config.mts",
5061
5250
  "playwright.config.cts",
@@ -5074,7 +5263,7 @@ var REPORTER_PACKAGE = "@piwitests/reporter";
5074
5263
  function readPackageJson(root) {
5075
5264
  const pkgPath = path18.join(root, "package.json");
5076
5265
  try {
5077
- return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
5266
+ return { pkg: JSON.parse(fs14.readFileSync(pkgPath, "utf-8")), pkgPath };
5078
5267
  } catch {
5079
5268
  return { pkg: null, pkgPath: null };
5080
5269
  }
@@ -5083,14 +5272,14 @@ function detectPackageManager(root, pkg) {
5083
5272
  const declared = pkg?.packageManager?.split("@")[0];
5084
5273
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
5085
5274
  for (const [file, manager] of LOCKFILES) {
5086
- if (fs13.existsSync(path18.join(root, file))) return manager;
5275
+ if (fs14.existsSync(path18.join(root, file))) return manager;
5087
5276
  }
5088
5277
  return "npm";
5089
5278
  }
5090
5279
  function findConfig(root) {
5091
- for (const name of CONFIG_NAMES) {
5280
+ for (const name of CONFIG_NAMES2) {
5092
5281
  const candidate = path18.join(root, name);
5093
- if (fs13.existsSync(candidate)) return candidate;
5282
+ if (fs14.existsSync(candidate)) return candidate;
5094
5283
  }
5095
5284
  return null;
5096
5285
  }
@@ -5251,7 +5440,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
5251
5440
  }
5252
5441
 
5253
5442
  // src/cli/skills.ts
5254
- var fs14 = __toESM(require("fs"));
5443
+ var fs15 = __toESM(require("fs"));
5255
5444
  var path19 = __toESM(require("path"));
5256
5445
 
5257
5446
  // ../core/src/skill-stamp.ts
@@ -5319,7 +5508,7 @@ var DEFAULT_SKILLS_DIR = path19.join(".claude", "skills");
5319
5508
  function findTemplatesDir(fromDir) {
5320
5509
  let dir = fromDir;
5321
5510
  for (let i = 0; i < 6; i++) {
5322
- if (fs14.existsSync(path19.join(dir, "templates", "skills"))) return path19.join(dir, "templates");
5511
+ if (fs15.existsSync(path19.join(dir, "templates", "skills"))) return path19.join(dir, "templates");
5323
5512
  const parent = path19.dirname(dir);
5324
5513
  if (parent === dir) break;
5325
5514
  dir = parent;
@@ -5338,7 +5527,7 @@ function readFrontMatter(markdown) {
5338
5527
  }
5339
5528
  function readTemplatesVersion(templatesDir) {
5340
5529
  try {
5341
- const pkg = JSON.parse(fs14.readFileSync(path19.join(templatesDir, "..", "package.json"), "utf-8"));
5530
+ const pkg = JSON.parse(fs15.readFileSync(path19.join(templatesDir, "..", "package.json"), "utf-8"));
5342
5531
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
5343
5532
  } catch {
5344
5533
  return "0.0.0";
@@ -5351,8 +5540,8 @@ function listSkills(templatesDir, slugs = ALL_SKILLS) {
5351
5540
  const infos = [];
5352
5541
  for (const slug of slugs) {
5353
5542
  const file = templatePath(templatesDir, slug);
5354
- if (!fs14.existsSync(file)) continue;
5355
- const front = readFrontMatter(fs14.readFileSync(file, "utf-8"));
5543
+ if (!fs15.existsSync(file)) continue;
5544
+ const front = readFrontMatter(fs15.readFileSync(file, "utf-8"));
5356
5545
  infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
5357
5546
  }
5358
5547
  return infos;
@@ -5364,16 +5553,16 @@ function installSkills(opts) {
5364
5553
  const source = templatePath(opts.templatesDir, slug);
5365
5554
  const relDest = path19.join(opts.skillsDir, slug, "SKILL.md");
5366
5555
  const dest = path19.join(opts.root, relDest);
5367
- if (!fs14.existsSync(source)) {
5556
+ if (!fs15.existsSync(source)) {
5368
5557
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
5369
5558
  continue;
5370
5559
  }
5371
5560
  const version = opts.version ?? readTemplatesVersion(opts.templatesDir);
5372
- const contents = stampSkill(fs14.readFileSync(source, "utf-8"), version);
5373
- const exists = fs14.existsSync(dest);
5561
+ const contents = stampSkill(fs15.readFileSync(source, "utf-8"), version);
5562
+ const exists = fs15.existsSync(dest);
5374
5563
  let detail = "installed skill";
5375
5564
  if (exists) {
5376
- const installed = fs14.readFileSync(dest, "utf-8");
5565
+ const installed = fs15.readFileSync(dest, "utf-8");
5377
5566
  const state = classifyInstalledSkill(installed, contents);
5378
5567
  const from = readSkillStamp(installed).version;
5379
5568
  if (state === "current") {
@@ -5395,8 +5584,8 @@ function installSkills(opts) {
5395
5584
  }
5396
5585
  }
5397
5586
  if (!opts.dryRun) {
5398
- fs14.mkdirSync(path19.dirname(dest), { recursive: true });
5399
- fs14.writeFileSync(dest, contents);
5587
+ fs15.mkdirSync(path19.dirname(dest), { recursive: true });
5588
+ fs15.writeFileSync(dest, contents);
5400
5589
  }
5401
5590
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
5402
5591
  }
@@ -5490,7 +5679,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
5490
5679
 
5491
5680
  // src/cli/init.ts
5492
5681
  var DEFAULT_SERVER_URL = "http://localhost:3000";
5493
- var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
5682
+ var DESKTOP_APP_MODES2 = ["prefer", "fallback", "off"];
5494
5683
  var USAGE8 = `
5495
5684
  piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
5496
5685
 
@@ -5566,7 +5755,7 @@ function parseInitArgs(argv, env, cwd) {
5566
5755
  }
5567
5756
  function readFileOr(file, fallback) {
5568
5757
  try {
5569
- return fs15.readFileSync(file, "utf-8");
5758
+ return fs16.readFileSync(file, "utf-8");
5570
5759
  } catch {
5571
5760
  return fallback;
5572
5761
  }
@@ -5620,7 +5809,7 @@ function stepConfig(project, opts) {
5620
5809
  projectName: opts.projectName,
5621
5810
  desktopApp
5622
5811
  });
5623
- if (edit.status === "updated" && !opts.dryRun) fs15.writeFileSync(project.configPath, edit.text);
5812
+ if (edit.status === "updated" && !opts.dryRun) fs16.writeFileSync(project.configPath, edit.text);
5624
5813
  if (edit.status === "already" && desktopApp && !new RegExp(`\\bdesktopApp\\s*:\\s*['"\`]${desktopApp}['"\`]`).test(source)) {
5625
5814
  return {
5626
5815
  step,
@@ -5635,7 +5824,7 @@ function stepFixtures(project, opts) {
5635
5824
  const step = "fixtures";
5636
5825
  const rel = path20.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5637
5826
  const dest = path20.join(project.root, rel);
5638
- if (fs15.existsSync(dest)) {
5827
+ if (fs16.existsSync(dest)) {
5639
5828
  const current = readFileOr(dest, "");
5640
5829
  if (/piwiFixtures/.test(current))
5641
5830
  return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
@@ -5647,8 +5836,8 @@ function stepFixtures(project, opts) {
5647
5836
  };
5648
5837
  }
5649
5838
  if (!opts.dryRun) {
5650
- fs15.mkdirSync(path20.dirname(dest), { recursive: true });
5651
- fs15.writeFileSync(dest, fixturesContents());
5839
+ fs16.mkdirSync(path20.dirname(dest), { recursive: true });
5840
+ fs16.writeFileSync(dest, fixturesContents());
5652
5841
  }
5653
5842
  return {
5654
5843
  step,
@@ -5664,7 +5853,7 @@ function stepEnv(project, opts) {
5664
5853
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
5665
5854
  ["PIWI_API_KEY", ""]
5666
5855
  ]);
5667
- if (example.added.length && !opts.dryRun) fs15.writeFileSync(examplePath, example.text);
5856
+ if (example.added.length && !opts.dryRun) fs16.writeFileSync(examplePath, example.text);
5668
5857
  results.push({
5669
5858
  step: "env",
5670
5859
  file: ".env.example",
@@ -5676,7 +5865,7 @@ function stepEnv(project, opts) {
5676
5865
  const entries = [["PIWI_API_KEY", opts.apiKey]];
5677
5866
  if (opts.serverUrl) entries.unshift(["PIWI_DASHBOARD_URL", opts.serverUrl]);
5678
5867
  const env = upsertEnvKeys(readFileOr(envPath, ""), entries);
5679
- if (env.added.length && !opts.dryRun) fs15.writeFileSync(envPath, env.text);
5868
+ if (env.added.length && !opts.dryRun) fs16.writeFileSync(envPath, env.text);
5680
5869
  results.push({
5681
5870
  step: "env",
5682
5871
  file: ".env",
@@ -5685,7 +5874,7 @@ function stepEnv(project, opts) {
5685
5874
  });
5686
5875
  const gitignorePath = path20.join(project.root, ".gitignore");
5687
5876
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
5688
- if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
5877
+ if (gitignore.added && !opts.dryRun) fs16.writeFileSync(gitignorePath, gitignore.text);
5689
5878
  results.push({
5690
5879
  step: "gitignore",
5691
5880
  file: ".gitignore",
@@ -5728,8 +5917,8 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
5728
5917
  return 0;
5729
5918
  }
5730
5919
  const opts = parseInitArgs(argv, env, cwd);
5731
- if (opts.desktopApp !== null && !DESKTOP_APP_MODES.includes(opts.desktopApp)) {
5732
- console.error(`--desktop-app must be one of ${DESKTOP_APP_MODES.join(", ")} (got "${opts.desktopApp}")`);
5920
+ if (opts.desktopApp !== null && !DESKTOP_APP_MODES2.includes(opts.desktopApp)) {
5921
+ console.error(`--desktop-app must be one of ${DESKTOP_APP_MODES2.join(", ")} (got "${opts.desktopApp}")`);
5733
5922
  return 1;
5734
5923
  }
5735
5924
  const project = detectProject(opts.root);
@@ -5786,7 +5975,7 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
5786
5975
  }
5787
5976
 
5788
5977
  // src/cli/select.ts
5789
- var fs17 = __toESM(require("fs"));
5978
+ var fs18 = __toESM(require("fs"));
5790
5979
  var path22 = __toESM(require("path"));
5791
5980
  var import_node_child_process7 = require("child_process");
5792
5981
 
@@ -6308,7 +6497,7 @@ function attributeMatches(value, exact, actual) {
6308
6497
 
6309
6498
  // ../core/src/locator-break.ts
6310
6499
  var TEXT_KINDS = /* @__PURE__ */ new Set(["text", "translation", "literal"]);
6311
- function isText(anchor) {
6500
+ function isText2(anchor) {
6312
6501
  return TEXT_KINDS.has(anchor.kind);
6313
6502
  }
6314
6503
  function hasAttribute(anchor, names) {
@@ -6407,7 +6596,7 @@ function directSlots(call, testIds) {
6407
6596
  const hasTextSlot = (index) => {
6408
6597
  const hasText = optionsOf(call.args[index]).get("hasText");
6409
6598
  if (!hasText) return null;
6410
- return stringSlot(hasText, false, "text", isText, (next) => withOption(call, index, "hasText", next));
6599
+ return stringSlot(hasText, false, "text", isText2, (next) => withOption(call, index, "hasText", next));
6411
6600
  };
6412
6601
  const textOr = (attribute) => (a) => a.kind === "translation" || a.kind === "literal" || hasAttribute(a, [attribute]);
6413
6602
  switch (call.method) {
@@ -6420,16 +6609,16 @@ function directSlots(call, testIds) {
6420
6609
  opts.get("name"),
6421
6610
  exact,
6422
6611
  "name",
6423
- (a) => isText(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
6612
+ (a) => isText2(a) || hasAttribute(a, ["aria-label", "title", "alt", "value"]),
6424
6613
  (next) => withOption(call, 1, "name", next)
6425
6614
  )
6426
6615
  );
6427
6616
  break;
6428
6617
  case "getByText":
6429
- slots.push(argSlot("text", isText));
6618
+ slots.push(argSlot("text", isText2));
6430
6619
  break;
6431
6620
  case "getByLabel":
6432
- slots.push(argSlot("text", (a) => isText(a) || hasAttribute(a, ["aria-label"])));
6621
+ slots.push(argSlot("text", (a) => isText2(a) || hasAttribute(a, ["aria-label"])));
6433
6622
  break;
6434
6623
  case "getByPlaceholder":
6435
6624
  slots.push(argSlot("text", textOr("placeholder")));
@@ -6632,7 +6821,7 @@ function diffImpactInput(files) {
6632
6821
  }
6633
6822
 
6634
6823
  // src/cli/add-reporter.ts
6635
- var fs16 = __toESM(require("fs"));
6824
+ var fs17 = __toESM(require("fs"));
6636
6825
  var path21 = __toESM(require("path"));
6637
6826
  var import_node_module2 = require("module");
6638
6827
  var REPORTER_PACKAGE2 = "@piwitests/reporter";
@@ -6676,7 +6865,7 @@ function resolveConfigPath(cwd, playwrightArgs2) {
6676
6865
  function readConfigSource(configPath) {
6677
6866
  if (!configPath) return null;
6678
6867
  try {
6679
- return fs16.readFileSync(configPath, "utf-8");
6868
+ return fs17.readFileSync(configPath, "utf-8");
6680
6869
  } catch {
6681
6870
  return null;
6682
6871
  }
@@ -6711,9 +6900,9 @@ Usage:
6711
6900
  npx @piwitests/reporter run impact --base <ref> run the tests your diff impacts
6712
6901
 
6713
6902
  Connection:
6714
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
6715
- --api-key <key> API key (env PIWI_API_KEY)
6716
- --project <name|id> Project (env PIWI_PROJECT_NAME)
6903
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
6904
+ --api-key <key> API key (env PIWI_API_KEY, .env)
6905
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
6717
6906
 
6718
6907
  Selection:
6719
6908
  --format <fmt> args (file:line, default) | grep | files | json
@@ -6770,12 +6959,22 @@ function parseDuration2(raw) {
6770
6959
  const ms = Math.round(value * factor);
6771
6960
  return ms > 0 ? ms : null;
6772
6961
  }
6773
- function parseSelectArgs(argv, env) {
6962
+ function parseSelectArgs(argv, env, dirs = [], command = "piwi select") {
6774
6963
  const dashIndex = argv.indexOf("--");
6775
6964
  const own = dashIndex === -1 ? argv : argv.slice(0, dashIndex);
6776
6965
  const extra = dashIndex === -1 ? [] : argv.slice(dashIndex + 1);
6777
- const serverUrl = (readOption7(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
6778
- if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
6966
+ const connection = resolveCliConnection(
6967
+ {
6968
+ serverUrl: readOption7(own, "--server-url"),
6969
+ apiKey: readOption7(own, "--api-key"),
6970
+ project: readOption7(own, "--project")
6971
+ },
6972
+ env,
6973
+ dirs,
6974
+ command
6975
+ );
6976
+ if (!connection)
6977
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
6779
6978
  const key = findKey(own);
6780
6979
  if (!key) throw new Error("No selection key \u2014 usage: piwi select <key>");
6781
6980
  const budgetRaw = readOption7(own, "--budget");
@@ -6789,9 +6988,9 @@ function parseSelectArgs(argv, env) {
6789
6988
  throw new Error(`--shard expects an "i/n" spec like 2/4, got "${shard}"`);
6790
6989
  }
6791
6990
  return {
6792
- serverUrl,
6793
- apiKey: readOption7(own, "--api-key") ?? env.PIWI_API_KEY ?? null,
6794
- project: readOption7(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
6991
+ serverUrl: connection.serverUrl,
6992
+ apiKey: connection.apiKey,
6993
+ project: connection.project,
6795
6994
  key,
6796
6995
  format: readOption7(own, "--format") ?? "args",
6797
6996
  budgetMs,
@@ -6810,7 +7009,7 @@ function cacheKey(args) {
6810
7009
  }
6811
7010
  function readCache(args) {
6812
7011
  try {
6813
- const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
7012
+ const store = JSON.parse(fs18.readFileSync(CACHE_FILE, "utf-8"));
6814
7013
  return store[cacheKey(args)] ?? null;
6815
7014
  } catch {
6816
7015
  return null;
@@ -6820,12 +7019,12 @@ function writeCache(args, resolution) {
6820
7019
  try {
6821
7020
  let store = {};
6822
7021
  try {
6823
- store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
7022
+ store = JSON.parse(fs18.readFileSync(CACHE_FILE, "utf-8"));
6824
7023
  } catch {
6825
7024
  }
6826
7025
  store[cacheKey(args)] = resolution;
6827
- fs17.mkdirSync(path22.dirname(CACHE_FILE), { recursive: true });
6828
- fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
7026
+ fs18.mkdirSync(path22.dirname(CACHE_FILE), { recursive: true });
7027
+ fs18.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
6829
7028
  } catch {
6830
7029
  }
6831
7030
  }
@@ -6917,7 +7116,7 @@ async function runSelect(argv, env = process.env) {
6917
7116
  }
6918
7117
  let args;
6919
7118
  try {
6920
- args = parseSelectArgs(argv, env);
7119
+ args = parseSelectArgs(argv, env, [process.cwd()]);
6921
7120
  } catch (e) {
6922
7121
  console.error(`piwi select: ${e.message}
6923
7122
  `);
@@ -7008,7 +7207,7 @@ async function runRun(argv, env = process.env) {
7008
7207
  }
7009
7208
  let args;
7010
7209
  try {
7011
- args = parseSelectArgs(argv, env);
7210
+ args = parseSelectArgs(argv, env, [process.cwd()], "piwi run");
7012
7211
  } catch (e) {
7013
7212
  console.error(`piwi run: ${e.message}
7014
7213
  `);
@@ -7058,7 +7257,7 @@ async function runRun(argv, env = process.env) {
7058
7257
  }
7059
7258
 
7060
7259
  // src/cli/preflight.ts
7061
- var fs18 = __toESM(require("fs"));
7260
+ var fs19 = __toESM(require("fs"));
7062
7261
  var path23 = __toESM(require("path"));
7063
7262
  var import_node_child_process8 = require("child_process");
7064
7263
 
@@ -7124,8 +7323,8 @@ Actions:
7124
7323
 
7125
7324
  Connection:
7126
7325
  --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
7127
- --api-key <key> API key (env PIWI_API_KEY)
7128
- --project <name|id> Project (env PIWI_PROJECT_NAME)
7326
+ --api-key <key> API key (env PIWI_API_KEY, .env)
7327
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
7129
7328
 
7130
7329
  Output:
7131
7330
  --json Print the breaks, the edits and the impact as JSON
@@ -7182,16 +7381,6 @@ function parsePreflightArgs(argv) {
7182
7381
  extra: dashIndex === -1 ? [] : argv.slice(dashIndex + 1)
7183
7382
  };
7184
7383
  }
7185
- var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
7186
- function findPlaywrightRoot(from) {
7187
- let dir = path23.resolve(from);
7188
- for (; ; ) {
7189
- if (CONFIG_NAMES2.some((name) => fs18.existsSync(path23.join(dir, name)))) return dir;
7190
- const parent = path23.dirname(dir);
7191
- if (parent === dir) return null;
7192
- dir = parent;
7193
- }
7194
- }
7195
7384
  function git3(cwd, args) {
7196
7385
  return (0, import_node_child_process8.execFileSync)("git", args, {
7197
7386
  cwd,
@@ -7209,7 +7398,12 @@ function tryGit(cwd, args) {
7209
7398
  }
7210
7399
  var TEST_FILE = /(?:^|\/)[^/]+\.(?:spec|test)\.[cm]?[jt]sx?$/;
7211
7400
  function resolveConnection(args, env, dirs) {
7212
- return resolveCliConnection({ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project }, env, dirs);
7401
+ return resolveCliConnection(
7402
+ { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
7403
+ env,
7404
+ dirs,
7405
+ "piwi preflight"
7406
+ );
7213
7407
  }
7214
7408
  function translationLookup(repoRoot, base, locale) {
7215
7409
  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));
@@ -7236,7 +7430,7 @@ function prefersLocale(file, locale) {
7236
7430
  }
7237
7431
  function readText(file) {
7238
7432
  try {
7239
- return fs18.readFileSync(file, "utf-8");
7433
+ return fs19.readFileSync(file, "utf-8");
7240
7434
  } catch {
7241
7435
  return void 0;
7242
7436
  }
@@ -7247,7 +7441,7 @@ function cacheKey2(connection, branch) {
7247
7441
  }
7248
7442
  function readCache2(root, key) {
7249
7443
  try {
7250
- const store = JSON.parse(fs18.readFileSync(path23.join(root, CACHE_FILE2), "utf-8"));
7444
+ const store = JSON.parse(fs19.readFileSync(path23.join(root, CACHE_FILE2), "utf-8"));
7251
7445
  return store[key] ?? null;
7252
7446
  } catch {
7253
7447
  return null;
@@ -7258,12 +7452,12 @@ function writeCache2(root, key, index) {
7258
7452
  try {
7259
7453
  let store = {};
7260
7454
  try {
7261
- store = JSON.parse(fs18.readFileSync(file, "utf-8"));
7455
+ store = JSON.parse(fs19.readFileSync(file, "utf-8"));
7262
7456
  } catch {
7263
7457
  }
7264
7458
  store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
7265
- fs18.mkdirSync(path23.dirname(file), { recursive: true });
7266
- fs18.writeFileSync(file, JSON.stringify(store));
7459
+ fs19.mkdirSync(path23.dirname(file), { recursive: true });
7460
+ fs19.writeFileSync(file, JSON.stringify(store));
7267
7461
  } catch {
7268
7462
  }
7269
7463
  }
@@ -7322,7 +7516,7 @@ function applyEdits(breaks, replacementsOf) {
7322
7516
  }
7323
7517
  const edited = [];
7324
7518
  for (const [file, edits] of byFile) {
7325
- const text = fs18.readFileSync(file, "utf-8");
7519
+ const text = fs19.readFileSync(file, "utf-8");
7326
7520
  const eol = text.includes("\r\n") ? "\r\n" : "\n";
7327
7521
  const lines = text.split(/\r?\n/);
7328
7522
  for (const e of edits) {
@@ -7332,7 +7526,7 @@ function applyEdits(breaks, replacementsOf) {
7332
7526
  }
7333
7527
  const next = lines.join(eol);
7334
7528
  if (next !== text) {
7335
- fs18.writeFileSync(file, next);
7529
+ fs19.writeFileSync(file, next);
7336
7530
  edited.push(file);
7337
7531
  }
7338
7532
  }
@@ -7443,7 +7637,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7443
7637
  console.error("piwi preflight: not inside a git repository");
7444
7638
  return EXIT_ERROR8;
7445
7639
  }
7446
- const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7640
+ const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd, repoRoot) ?? cwd);
7447
7641
  let files;
7448
7642
  try {
7449
7643
  files = parseUnifiedDiff(
@@ -7569,7 +7763,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7569
7763
  }
7570
7764
 
7571
7765
  // src/cli/probe.ts
7572
- var fs19 = __toESM(require("fs"));
7766
+ var fs20 = __toESM(require("fs"));
7573
7767
  var os3 = __toESM(require("os"));
7574
7768
  var path24 = __toESM(require("path"));
7575
7769
  var EXIT_OK8 = 0;
@@ -7581,9 +7775,9 @@ Usage:
7581
7775
  npx @piwitests/reporter probe [options] [-- <playwright args>]
7582
7776
 
7583
7777
  Options:
7584
- --server-url <url> Dashboard URL (or PIWI_DASHBOARD_URL)
7585
- --api-key <key> Reporter API key (or PIWI_API_KEY)
7586
- --project <name|id> Project name or id (or PIWI_PROJECT_NAME)
7778
+ --server-url <url> Dashboard URL (or PIWI_DASHBOARD_URL, .env, the desktop app)
7779
+ --api-key <key> Reporter API key (or PIWI_API_KEY, .env)
7780
+ --project <name|id> Project name or id (or PIWI_PROJECT_NAME, .env)
7587
7781
  --budget <n> Max pairs to probe this run (default the server's)
7588
7782
 
7589
7783
  Everything after \`--\` is passed to \`playwright test\`.
@@ -7604,13 +7798,21 @@ async function runProbe(argv) {
7604
7798
  return EXIT_OK8;
7605
7799
  }
7606
7800
  const env = process.env;
7607
- const serverUrl = (readOption9(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
7608
- if (!serverUrl) {
7609
- console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
7801
+ const connection = resolveCliConnection(
7802
+ {
7803
+ serverUrl: readOption9(own, "--server-url"),
7804
+ apiKey: readOption9(own, "--api-key"),
7805
+ project: readOption9(own, "--project")
7806
+ },
7807
+ env,
7808
+ [process.cwd()],
7809
+ "piwi probe"
7810
+ );
7811
+ if (!connection) {
7812
+ console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
7610
7813
  return EXIT_ERROR9;
7611
7814
  }
7612
- const apiKey = readOption9(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
7613
- const project = readOption9(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
7815
+ const { serverUrl, apiKey, project } = connection;
7614
7816
  const budget = readOption9(own, "--budget");
7615
7817
  let projectId;
7616
7818
  try {
@@ -7635,12 +7837,12 @@ async function runProbe(argv) {
7635
7837
  return EXIT_OK8;
7636
7838
  }
7637
7839
  console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
7638
- const dir = fs19.mkdtempSync(path24.join(os3.tmpdir(), "piwi-probe-"));
7840
+ const dir = fs20.mkdtempSync(path24.join(os3.tmpdir(), "piwi-probe-"));
7639
7841
  const planFile = path24.join(dir, "plan.json");
7640
7842
  const resultsFile = path24.join(dir, "results.jsonl");
7641
7843
  const runOutputFile = path24.join(dir, "run.json");
7642
- fs19.writeFileSync(planFile, JSON.stringify(plan));
7643
- fs19.writeFileSync(resultsFile, "");
7844
+ fs20.writeFileSync(planFile, JSON.stringify(plan));
7845
+ fs20.writeFileSync(resultsFile, "");
7644
7846
  const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
7645
7847
  const pwArgs = [...playwrightArgs2];
7646
7848
  if (grep && !playwrightArgs2.includes("--grep") && !playwrightArgs2.includes("-g")) pwArgs.push("--grep", grep);
@@ -7654,7 +7856,7 @@ async function runProbe(argv) {
7654
7856
  [PIWI_ENV_KEYS.outputFile]: runOutputFile
7655
7857
  };
7656
7858
  await spawnPlaywright(pwArgs, childEnv, { command: "piwi probe" });
7657
- const lines = fs19.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
7859
+ const lines = fs20.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
7658
7860
  const results = [];
7659
7861
  for (const line of lines) {
7660
7862
  try {
@@ -7668,7 +7870,7 @@ async function runProbe(argv) {
7668
7870
  }
7669
7871
  let runId = null;
7670
7872
  try {
7671
- const out = JSON.parse(fs19.readFileSync(runOutputFile, "utf8"));
7873
+ const out = JSON.parse(fs20.readFileSync(runOutputFile, "utf8"));
7672
7874
  if (typeof out.runId === "number") runId = out.runId;
7673
7875
  } catch {
7674
7876
  }
@@ -7693,7 +7895,7 @@ async function runProbe(argv) {
7693
7895
  }
7694
7896
 
7695
7897
  // src/cli/quality-report.ts
7696
- var fs20 = __toESM(require("fs"));
7898
+ var fs21 = __toESM(require("fs"));
7697
7899
  var EXIT_OK9 = 0;
7698
7900
  var EXIT_VERDICT = 1;
7699
7901
  var EXIT_ERROR10 = 2;
@@ -7706,8 +7908,8 @@ Usage:
7706
7908
  npx @piwitests/reporter report [options]
7707
7909
 
7708
7910
  Connection:
7709
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
7710
- --api-key <key> API key (env PIWI_API_KEY)
7911
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
7912
+ --api-key <key> API key (env PIWI_API_KEY, .env)
7711
7913
 
7712
7914
  What to report:
7713
7915
  --project <names|ids> Comma-separated projects (default: every project you can see)
@@ -7739,9 +7941,15 @@ function normalizePeriod(raw) {
7739
7941
  const days = raw.trim().match(/^(\d+)d$/);
7740
7942
  return days ? `last-${days[1]}d` : raw.trim();
7741
7943
  }
7742
- function parseReportArgs(argv, env) {
7743
- const serverUrl = (readOption10(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
7744
- if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
7944
+ function parseReportArgs(argv, env, dirs = []) {
7945
+ const connection = resolveCliConnection(
7946
+ { serverUrl: readOption10(argv, "--server-url"), apiKey: readOption10(argv, "--api-key") },
7947
+ env,
7948
+ dirs,
7949
+ "piwi report"
7950
+ );
7951
+ if (!connection)
7952
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
7745
7953
  const format = (readOption10(argv, "--format") ?? "md").toLowerCase();
7746
7954
  if (!FORMATS.includes(format)) {
7747
7955
  throw new Error(`--format must be one of ${FORMATS.join(", ")}, got "${format}"`);
@@ -7761,8 +7969,8 @@ function parseReportArgs(argv, env) {
7761
7969
  const output = readOption10(argv, "--output") ?? null;
7762
7970
  if (format === "pdf" && !output) throw new Error("A PDF needs --output <file>");
7763
7971
  return {
7764
- serverUrl,
7765
- apiKey: readOption10(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
7972
+ serverUrl: connection.serverUrl,
7973
+ apiKey: connection.apiKey,
7766
7974
  projects: (readOption10(argv, "--project") ?? "").split(",").map((p) => p.trim()).filter(Boolean),
7767
7975
  period: normalizePeriod(readOption10(argv, "--period") ?? "30d"),
7768
7976
  compare: readOption10(argv, "--compare") ?? null,
@@ -7806,14 +8014,14 @@ async function runQualityReport(argv) {
7806
8014
  return EXIT_OK9;
7807
8015
  }
7808
8016
  try {
7809
- const args = parseReportArgs(argv, process.env);
8017
+ const args = parseReportArgs(argv, process.env, [process.cwd()]);
7810
8018
  const projectIds = [];
7811
8019
  for (const project of args.projects) {
7812
8020
  projectIds.push(await resolveProjectId({ serverUrl: args.serverUrl, apiKey: args.apiKey, project, key: "" }));
7813
8021
  }
7814
8022
  const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
7815
8023
  const bytes = new Uint8Array(await res.arrayBuffer());
7816
- if (args.output) fs20.writeFileSync(args.output, bytes);
8024
+ if (args.output) fs21.writeFileSync(args.output, bytes);
7817
8025
  else process.stdout.write(bytes);
7818
8026
  if (!args.failOn) return EXIT_OK9;
7819
8027
  const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();