@piwitests/reporter 0.61.0 → 0.63.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
@@ -36,6 +36,7 @@ var path2 = __toESM(require("path"));
36
36
 
37
37
  // src/internal/capture/locator-healing.ts
38
38
  var path = __toESM(require("path"));
39
+ var import_node_url = require("url");
39
40
 
40
41
  // ../core/src/locator-chain.ts
41
42
  var LOCATING_METHODS = /* @__PURE__ */ new Set([
@@ -647,11 +648,11 @@ function spawnPlaywright(playwrightArgs2, env, options = {}) {
647
648
  let fd = output === "inherit" ? null : fs3.openSync(output.file, "a");
648
649
  const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
649
650
  const child = (0, import_node_child_process.spawn)(invocation.file, invocation.args, { stdio, env, cwd });
650
- return new Promise((resolve10) => {
651
+ return new Promise((resolve11) => {
651
652
  const done = (code) => {
652
653
  if (fd !== null) fs3.closeSync(fd);
653
654
  fd = null;
654
- resolve10(code);
655
+ resolve11(code);
655
656
  };
656
657
  child.on("error", (err) => {
657
658
  console.error(`${label}: could not start Playwright \u2014 ${err.message}`);
@@ -788,9 +789,8 @@ async function runAi(argv, env = process.env) {
788
789
  }
789
790
 
790
791
  // 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");
792
+ var fs7 = __toESM(require("fs"));
793
+ var path10 = __toESM(require("path"));
794
794
 
795
795
  // src/internal/config/env.ts
796
796
  var path8 = __toESM(require("path"));
@@ -870,6 +870,13 @@ function readDotEnv(filePath2) {
870
870
  }
871
871
  }
872
872
 
873
+ // src/internal/support/ci.ts
874
+ function detectCi(env) {
875
+ return Boolean(
876
+ 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"
877
+ );
878
+ }
879
+
873
880
  // src/internal/config/env.ts
874
881
  var DEFAULTS = {
875
882
  projectName: "default-project",
@@ -1007,6 +1014,82 @@ var ENV_FALLBACK_SPECS = [
1007
1014
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
1008
1015
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
1009
1016
  ];
1017
+ var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
1018
+ function desktopAppMode(option, envValue) {
1019
+ const fromEnv = envValue?.trim().toLowerCase();
1020
+ if (DESKTOP_APP_MODES.includes(fromEnv)) return fromEnv;
1021
+ if (DESKTOP_APP_MODES.includes(option)) return option;
1022
+ return "fallback";
1023
+ }
1024
+
1025
+ // src/internal/support/connection.ts
1026
+ var fs6 = __toESM(require("fs"));
1027
+ var path9 = __toESM(require("path"));
1028
+ var import_node_child_process2 = require("child_process");
1029
+ var CONFIG_NAMES = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
1030
+ function findPlaywrightRoot(from, root) {
1031
+ const top = root ? path9.resolve(root) : null;
1032
+ let dir = path9.resolve(from);
1033
+ for (; ; ) {
1034
+ if (CONFIG_NAMES.some((name) => fs6.existsSync(path9.join(dir, name)))) return dir;
1035
+ const parent = path9.dirname(dir);
1036
+ if (dir === top || parent === dir) return null;
1037
+ dir = parent;
1038
+ }
1039
+ }
1040
+ function gitRoot(cwd) {
1041
+ try {
1042
+ const out = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
1043
+ cwd,
1044
+ encoding: "utf-8",
1045
+ stdio: ["ignore", "pipe", "ignore"]
1046
+ }).trim();
1047
+ return out ? path9.resolve(out) : null;
1048
+ } catch {
1049
+ return null;
1050
+ }
1051
+ }
1052
+ function connectionFolders(cwd) {
1053
+ const root = gitRoot(cwd);
1054
+ const dirs = [findPlaywrightRoot(cwd, root) ?? cwd, cwd, root];
1055
+ return [...new Set(dirs.filter((dir) => dir !== null).map((dir) => path9.resolve(dir)))];
1056
+ }
1057
+ function projectOf(source) {
1058
+ const project = source[PIWI_ENV_KEYS.projectName];
1059
+ return project ? { [PIWI_ENV_KEYS.projectName]: project } : {};
1060
+ }
1061
+ function readDotEnvConnection(dirs) {
1062
+ const files = dirs.map((dir) => readDotEnv(path9.join(dir, ".env")));
1063
+ const withUrl = files.find((file) => file[PIWI_ENV_KEYS.serverUrl]);
1064
+ const vars = {};
1065
+ if (withUrl) {
1066
+ vars[PIWI_ENV_KEYS.serverUrl] = withUrl[PIWI_ENV_KEYS.serverUrl];
1067
+ if (withUrl[PIWI_ENV_KEYS.apiKey]) vars[PIWI_ENV_KEYS.apiKey] = withUrl[PIWI_ENV_KEYS.apiKey];
1068
+ }
1069
+ const project = files.find((file) => file[PIWI_ENV_KEYS.projectName])?.[PIWI_ENV_KEYS.projectName];
1070
+ if (project) vars[PIWI_ENV_KEYS.projectName] = project;
1071
+ return { vars, anyKey: files.some((file) => file[PIWI_ENV_KEYS.apiKey]) };
1072
+ }
1073
+ function resolveCliConnection(flags, env, dirs, command = "piwi") {
1074
+ const { vars: dotEnv, anyKey } = readDotEnvConnection(dirs);
1075
+ const mode = desktopAppMode(void 0, env[PIWI_ENV_KEYS.desktopApp]);
1076
+ const preferLocally = mode === "prefer" && !flags.serverUrl && !detectCi(env);
1077
+ const urlSet = Boolean(flags.serverUrl || env[PIWI_ENV_KEYS.serverUrl] || dotEnv[PIWI_ENV_KEYS.serverUrl]);
1078
+ const keySet = Boolean(flags.apiKey || env[PIWI_ENV_KEYS.apiKey] || anyKey);
1079
+ const readDesktop = mode !== "off" && (preferLocally || urlSet || !keySet);
1080
+ const desktop = readDesktop ? readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath()) : null;
1081
+ const preferDesktop = desktop !== null && preferLocally;
1082
+ const connection = resolvePiwiConnection(
1083
+ preferDesktop ? { flags, env: projectOf(env), dotEnv: projectOf(dotEnv), desktop } : { flags, env, dotEnv, desktop }
1084
+ );
1085
+ if (connection && flags.apiKey && connection.apiKey !== flags.apiKey) {
1086
+ const from = !preferDesktop && dotEnv[PIWI_ENV_KEYS.serverUrl] ? "a .env file" : "the desktop app";
1087
+ console.error(
1088
+ `${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.`
1089
+ );
1090
+ }
1091
+ return connection;
1092
+ }
1010
1093
 
1011
1094
  // src/cli/bug.ts
1012
1095
  var EXIT_OK2 = 0;
@@ -1029,8 +1112,8 @@ Options:
1029
1112
  --no-run With --write or --out: write it without running it
1030
1113
  --force Replace an existing file
1031
1114
  --run-mode The spec without test.fail(), as a reproduction runs it
1032
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
1033
- --api-key <key> API key (env PIWI_API_KEY)
1115
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
1116
+ --api-key <key> API key (env PIWI_API_KEY, .env)
1034
1117
 
1035
1118
  Exit codes: 0 printed or written (and, when run, the test behaved as its
1036
1119
  test.fail() expects: the bug reproduces), 1 the run did not (the bug may be
@@ -1044,7 +1127,7 @@ function readOption2(argv, name) {
1044
1127
  const value = argv[index + 1];
1045
1128
  return value !== void 0 && !value.startsWith("--") ? value : void 0;
1046
1129
  }
1047
- function parseBugArgs(argv, env) {
1130
+ function parseBugArgs(argv, env, dirs = []) {
1048
1131
  let idText;
1049
1132
  for (let i = 0; i < argv.length; i++) {
1050
1133
  const token = argv[i];
@@ -1060,8 +1143,13 @@ function parseBugArgs(argv, env) {
1060
1143
  for (const flag of VALUE_FLAGS) {
1061
1144
  if (argv.includes(flag) && readOption2(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
1062
1145
  }
1063
- const serverUrl = (readOption2(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
1064
- if (!serverUrl) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
1146
+ const connection = resolveCliConnection(
1147
+ { serverUrl: readOption2(argv, "--server-url"), apiKey: readOption2(argv, "--api-key") },
1148
+ env,
1149
+ dirs,
1150
+ "piwi bug"
1151
+ );
1152
+ if (!connection) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
1065
1153
  const out = readOption2(argv, "--out") ?? null;
1066
1154
  const write = argv.includes("--write") || out !== null;
1067
1155
  return {
@@ -1071,8 +1159,8 @@ function parseBugArgs(argv, env) {
1071
1159
  run: write && !argv.includes("--no-run"),
1072
1160
  force: argv.includes("--force"),
1073
1161
  mode: argv.includes("--run-mode") ? "run" : "commit",
1074
- serverUrl,
1075
- apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null
1162
+ serverUrl: connection.serverUrl,
1163
+ apiKey: connection.apiKey
1076
1164
  };
1077
1165
  }
1078
1166
  async function fetchBugSpec(args, specDir) {
@@ -1094,35 +1182,23 @@ async function fetchBugSpec(args, specDir) {
1094
1182
  if (typeof body.code !== "string" || typeof body.path !== "string") throw new Error("the dashboard sent no spec");
1095
1183
  return { code: body.code, path: body.path, warnings: Array.isArray(body.warnings) ? body.warnings : [] };
1096
1184
  }
1097
- function repositoryRoot(cwd) {
1098
- try {
1099
- const out = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
1100
- cwd,
1101
- encoding: "utf-8",
1102
- stdio: ["ignore", "pipe", "ignore"]
1103
- }).trim();
1104
- return out ? path9.resolve(out) : null;
1105
- } catch {
1106
- return null;
1107
- }
1108
- }
1109
1185
  function realFolder(dir) {
1110
1186
  const rest = [];
1111
1187
  let current = dir;
1112
- while (!fs6.existsSync(current) && path9.dirname(current) !== current) {
1113
- rest.unshift(path9.basename(current));
1114
- current = path9.dirname(current);
1188
+ while (!fs7.existsSync(current) && path10.dirname(current) !== current) {
1189
+ rest.unshift(path10.basename(current));
1190
+ current = path10.dirname(current);
1115
1191
  }
1116
- return path9.join(fs6.realpathSync(current), ...rest);
1192
+ return path10.join(fs7.realpathSync(current), ...rest);
1117
1193
  }
1118
1194
  function folderInRepository(root, dir) {
1119
- const relative9 = path9.relative(realFolder(root), realFolder(dir));
1120
- if (relative9.startsWith("..") || path9.isAbsolute(relative9)) return void 0;
1121
- return relative9.split(path9.sep).join("/");
1195
+ const relative8 = path10.relative(realFolder(root), realFolder(dir));
1196
+ if (relative8.startsWith("..") || path10.isAbsolute(relative8)) return void 0;
1197
+ return relative8.split(path10.sep).join("/");
1122
1198
  }
1123
1199
  function fileFilter(file) {
1124
1200
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1125
- return `${escape(path9.basename(path9.dirname(file)))}[\\\\/]${escape(path9.basename(file))}$`;
1201
+ return `${escape(path10.basename(path10.dirname(file)))}[\\\\/]${escape(path10.basename(file))}$`;
1126
1202
  }
1127
1203
  function runPlaywright(file, cwd, id, env) {
1128
1204
  return spawnPlaywright([fileFilter(file)], { ...env, ...originEnv("bug", id) }, { cwd, command: "piwi bug" });
@@ -1134,18 +1210,18 @@ async function runBug(argv, env = process.env, cwd = process.cwd()) {
1134
1210
  }
1135
1211
  let args;
1136
1212
  try {
1137
- args = parseBugArgs(argv, env);
1213
+ args = parseBugArgs(argv, env, readOption2(argv, "--server-url") ? [] : connectionFolders(cwd));
1138
1214
  } catch (e) {
1139
1215
  console.error(`piwi bug: ${e.message}
1140
1216
 
1141
1217
  ${USAGE2}`);
1142
1218
  return EXIT_ERROR2;
1143
1219
  }
1144
- const root = args.write ? repositoryRoot(cwd) : null;
1145
- const outFile = args.out !== null ? path9.resolve(cwd, args.out) : null;
1220
+ const root = args.write ? gitRoot(cwd) : null;
1221
+ const outFile = args.out !== null ? path10.resolve(cwd, args.out) : null;
1146
1222
  let spec;
1147
1223
  try {
1148
- spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path9.dirname(outFile)) : void 0);
1224
+ spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path10.dirname(outFile)) : void 0);
1149
1225
  } catch (e) {
1150
1226
  console.error(`piwi bug: ${e.message}`);
1151
1227
  return EXIT_ERROR2;
@@ -1160,16 +1236,16 @@ ${USAGE2}`);
1160
1236
  if (!outFile && !root) {
1161
1237
  console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
1162
1238
  }
1163
- const file = outFile ?? path9.resolve(root ?? cwd, spec.path);
1239
+ const file = outFile ?? path10.resolve(root ?? cwd, spec.path);
1164
1240
  try {
1165
- fs6.mkdirSync(path9.dirname(file), { recursive: true });
1166
- fs6.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
1241
+ fs7.mkdirSync(path10.dirname(file), { recursive: true });
1242
+ fs7.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
1167
1243
  } catch (e) {
1168
1244
  const exists = e.code === "EEXIST";
1169
1245
  console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
1170
1246
  return EXIT_ERROR2;
1171
1247
  }
1172
- const shown = path9.relative(cwd, file) || file;
1248
+ const shown = path10.relative(cwd, file) || file;
1173
1249
  console.error(`piwi bug: wrote ${shown}`);
1174
1250
  if (!args.run) return EXIT_OK2;
1175
1251
  const code = await runPlaywright(file, cwd, args.id, env);
@@ -1187,8 +1263,8 @@ ${USAGE2}`);
1187
1263
  }
1188
1264
 
1189
1265
  // src/cli/codegen.ts
1190
- var fs7 = __toESM(require("fs"));
1191
- var path10 = __toESM(require("path"));
1266
+ var fs8 = __toESM(require("fs"));
1267
+ var path11 = __toESM(require("path"));
1192
1268
 
1193
1269
  // ../core/src/recording.ts
1194
1270
  var ASSERTION_MATCHERS = [
@@ -1879,6 +1955,44 @@ function locatorForStep(step, index, ctx) {
1879
1955
  }
1880
1956
  return `${ctx.page}.${chosen.text}`;
1881
1957
  }
1958
+ function escapedInTemplate(code) {
1959
+ return code < 32 && code !== 9 || code === 127 || code === 8232 || code === 8233;
1960
+ }
1961
+ function templateText(text) {
1962
+ let out = "";
1963
+ for (let i = 0; i < text.length; i++) {
1964
+ const c = text[i];
1965
+ const code = c.charCodeAt(0);
1966
+ if (c === "\\" || c === "`" || c === "$" && text[i + 1] === "{") out += `\\${c}`;
1967
+ else if (escapedInTemplate(code)) out += `\\u${code.toString(16).padStart(4, "0")}`;
1968
+ else out += c;
1969
+ }
1970
+ return out;
1971
+ }
1972
+ function ariaSnapshotLiteral(snapshot, indent) {
1973
+ const lines = snapshot.split(/\r\n?|\n/).filter((line) => line.trim());
1974
+ const common = Math.min(...lines.map((line) => line.length - line.replace(/^ +/, "").length));
1975
+ return ["`", ...lines.map((line) => `${indent} ${templateText(line.slice(common).trimEnd())}`), `${indent}\``];
1976
+ }
1977
+ function renderAriaSnapshotStep(step, index, ctx, not) {
1978
+ const snapshot = step.assertion?.expected ?? "";
1979
+ if (!snapshot.trim()) {
1980
+ ctx.warnings.push({
1981
+ step: index,
1982
+ code: "incomplete-assertion",
1983
+ detail: "toMatchAriaSnapshot",
1984
+ message: "toMatchAriaSnapshot needs the ARIA snapshot to match."
1985
+ });
1986
+ return [` // Step ${index + 1}: toMatchAriaSnapshot without a snapshot.`];
1987
+ }
1988
+ const [open, ...rest] = ariaSnapshotLiteral(snapshot, " ");
1989
+ const close = rest.pop();
1990
+ return [
1991
+ ` await expect(${locatorForStep(step, index, ctx)})${not}.toMatchAriaSnapshot(${open}`,
1992
+ ...rest,
1993
+ `${close});`
1994
+ ];
1995
+ }
1882
1996
  function renderAssertStep(step, index, ctx) {
1883
1997
  const assertion = step.action === "assertVisible" ? null : step.assertion;
1884
1998
  if (step.action === "assert" && !assertion) {
@@ -1889,6 +2003,7 @@ function renderAssertStep(step, index, ctx) {
1889
2003
  const not = assertion?.negated ? ".not" : "";
1890
2004
  const lines = [];
1891
2005
  if (assertion?.note) lines.push(` // ${commentText(assertion.note)}`);
2006
+ if (matcher === "toMatchAriaSnapshot") return [...lines, ...renderAriaSnapshotStep(step, index, ctx, not)];
1892
2007
  let arg = "";
1893
2008
  if (VALUE_MATCHERS.has(matcher)) {
1894
2009
  if (assertion?.expected == null) {
@@ -2779,9 +2894,9 @@ Code:
2779
2894
  --tag <tag> Add a tag; repeat for more
2780
2895
 
2781
2896
  Project (optional: your function catalog, and the locators your tests already use):
2782
- --project <name|id> Project (env PIWI_PROJECT_NAME)
2783
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
2784
- --api-key <key> API key (env PIWI_API_KEY)
2897
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
2898
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
2899
+ --api-key <key> API key (env PIWI_API_KEY, .env)
2785
2900
  --offline Do not contact the dashboard
2786
2901
 
2787
2902
  Exit codes: 0 ok, 2 the steps could not be read or the spec could not be written.
@@ -2821,12 +2936,22 @@ function findFile(argv) {
2821
2936
  }
2822
2937
  return void 0;
2823
2938
  }
2824
- function parseCodegenArgs(argv, env) {
2939
+ function parseCodegenArgs(argv, env, dirs = []) {
2825
2940
  const file = findFile(argv);
2826
2941
  if (!file) throw new Error("No steps file \u2014 usage: piwi codegen <steps.json>");
2827
2942
  for (const flag of VALUE_FLAGS2) {
2828
2943
  if (argv.includes(flag) && readOption3(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
2829
2944
  }
2945
+ const connection = resolveCliConnection(
2946
+ {
2947
+ serverUrl: readOption3(argv, "--server-url"),
2948
+ apiKey: readOption3(argv, "--api-key"),
2949
+ project: readOption3(argv, "--project")
2950
+ },
2951
+ env,
2952
+ dirs,
2953
+ "piwi codegen"
2954
+ );
2830
2955
  return {
2831
2956
  file,
2832
2957
  out: readOption3(argv, "--out") ?? null,
@@ -2840,9 +2965,9 @@ function parseCodegenArgs(argv, env) {
2840
2965
  fail: argv.includes("--fail") || argv.includes("--fail-reason"),
2841
2966
  failReason: readOption3(argv, "--fail-reason") ?? null,
2842
2967
  tags: readAll2(argv, "--tag"),
2843
- project: readOption3(argv, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
2844
- serverUrl: (readOption3(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, ""),
2845
- apiKey: readOption3(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
2968
+ project: connection?.project ?? readOption3(argv, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
2969
+ serverUrl: connection?.serverUrl ?? "",
2970
+ apiKey: connection?.apiKey ?? null,
2846
2971
  offline: argv.includes("--offline")
2847
2972
  };
2848
2973
  }
@@ -2901,14 +3026,15 @@ async function loadProjectContext(args) {
2901
3026
  );
2902
3027
  return { catalog, suiteLocators };
2903
3028
  }
2904
- async function runCodegen(argv, env = process.env) {
3029
+ async function runCodegen(argv, env = process.env, cwd = process.cwd()) {
2905
3030
  if (argv.includes("-h") || argv.includes("--help")) {
2906
3031
  console.log(USAGE3);
2907
3032
  return EXIT_OK3;
2908
3033
  }
2909
3034
  let args;
2910
3035
  try {
2911
- args = parseCodegenArgs(argv, env);
3036
+ const given = readOption3(argv, "--server-url") && (readOption3(argv, "--project") || env.PIWI_PROJECT_NAME);
3037
+ args = parseCodegenArgs(argv, env, given ? [] : connectionFolders(cwd));
2912
3038
  } catch (e) {
2913
3039
  console.error(`piwi codegen: ${e.message}
2914
3040
 
@@ -2920,7 +3046,7 @@ ${USAGE3}`);
2920
3046
  if (bugId) {
2921
3047
  if (!args.serverUrl) {
2922
3048
  console.error(
2923
- "piwi codegen: bug:<id> reads the report from the dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL"
3049
+ "piwi codegen: bug:<id> reads the report from the dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)"
2924
3050
  );
2925
3051
  return EXIT_ERROR3;
2926
3052
  }
@@ -2933,8 +3059,8 @@ ${USAGE3}`);
2933
3059
  }
2934
3060
  } else {
2935
3061
  try {
2936
- if (fs7.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
2937
- text = fs7.readFileSync(args.file, "utf-8");
3062
+ if (fs8.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
3063
+ text = fs8.readFileSync(args.file, "utf-8");
2938
3064
  } catch (e) {
2939
3065
  const reason = e.code === "ENOENT" ? "does not exist" : e.message;
2940
3066
  console.error(`piwi codegen: ${args.file} ${reason}`);
@@ -2966,8 +3092,8 @@ ${USAGE3}`);
2966
3092
  return EXIT_OK3;
2967
3093
  }
2968
3094
  try {
2969
- fs7.mkdirSync(path10.dirname(args.out), { recursive: true });
2970
- fs7.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
3095
+ fs8.mkdirSync(path11.dirname(args.out), { recursive: true });
3096
+ fs8.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
2971
3097
  } catch (e) {
2972
3098
  const exists = e.code === "EEXIST";
2973
3099
  console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
@@ -2981,7 +3107,7 @@ ${USAGE3}`);
2981
3107
  }
2982
3108
 
2983
3109
  // src/cli/coverage.ts
2984
- var path15 = __toESM(require("path"));
3110
+ var path16 = __toESM(require("path"));
2985
3111
 
2986
3112
  // ../core/src/codeowners.ts
2987
3113
  function patternParts(pattern) {
@@ -3105,7 +3231,7 @@ function errorMessage(error) {
3105
3231
  }
3106
3232
 
3107
3233
  // src/internal/support/test-paths.ts
3108
- var path11 = __toESM(require("path"));
3234
+ var path12 = __toESM(require("path"));
3109
3235
 
3110
3236
  // src/internal/collect/metadata-collector.ts
3111
3237
  var DETACHED_HEAD = "HEAD";
@@ -3138,23 +3264,23 @@ function normalizeRef(ref) {
3138
3264
 
3139
3265
  // src/internal/coverage-upload/git-inventory.ts
3140
3266
  var import_node_child_process5 = require("child_process");
3141
- var fs9 = __toESM(require("fs"));
3142
- var path13 = __toESM(require("path"));
3267
+ var fs10 = __toESM(require("fs"));
3268
+ var path14 = __toESM(require("path"));
3143
3269
 
3144
3270
  // src/internal/collect/blob-ids.ts
3145
3271
  var import_node_child_process4 = require("child_process");
3146
3272
  var crypto2 = __toESM(require("crypto"));
3147
- var fs8 = __toESM(require("fs"));
3148
- var path12 = __toESM(require("path"));
3273
+ var fs9 = __toESM(require("fs"));
3274
+ var path13 = __toESM(require("path"));
3149
3275
  function gitHashObjects(root, paths) {
3150
- return new Promise((resolve10) => {
3276
+ return new Promise((resolve11) => {
3151
3277
  const child = (0, import_node_child_process4.execFile)(
3152
3278
  "git",
3153
3279
  ["hash-object", "--stdin-paths"],
3154
3280
  { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
3155
3281
  (error, stdout) => {
3156
3282
  const ids = error ? [] : stdout.split("\n").filter(Boolean);
3157
- resolve10(ids.length === paths.length ? ids : null);
3283
+ resolve11(ids.length === paths.length ? ids : null);
3158
3284
  }
3159
3285
  );
3160
3286
  child.stdin?.on("error", () => {
@@ -3168,12 +3294,12 @@ function gitHashObjects(root, paths) {
3168
3294
  var MAX_GIT_OUTPUT = 512 * 1024 * 1024;
3169
3295
  var GIT_BATCH = 500;
3170
3296
  function git(cwd, args) {
3171
- return new Promise((resolve10, reject) => {
3297
+ return new Promise((resolve11, reject) => {
3172
3298
  (0, import_node_child_process5.execFile)(
3173
3299
  "git",
3174
3300
  args,
3175
3301
  { cwd, encoding: "utf8", maxBuffer: MAX_GIT_OUTPUT, timeout: 12e4, windowsHide: true },
3176
- (error, stdout) => error ? reject(error) : resolve10(stdout)
3302
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
3177
3303
  );
3178
3304
  });
3179
3305
  }
@@ -3212,7 +3338,7 @@ async function readInventory(root, warn = () => {
3212
3338
  const changed = [];
3213
3339
  for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
3214
3340
  if (!file || !inventory.has(file)) continue;
3215
- if (fs9.existsSync(path13.join(root, file))) changed.push(file);
3341
+ if (fs10.existsSync(path14.join(root, file))) changed.push(file);
3216
3342
  else inventory.delete(file);
3217
3343
  }
3218
3344
  const ids = await hashPaths(root, changed);
@@ -3231,8 +3357,8 @@ async function readInventory(root, warn = () => {
3231
3357
  }
3232
3358
 
3233
3359
  // src/internal/coverage-upload/report-files.ts
3234
- var fs10 = __toESM(require("fs"));
3235
- var path14 = __toESM(require("path"));
3360
+ var fs11 = __toESM(require("fs"));
3361
+ var path15 = __toESM(require("path"));
3236
3362
 
3237
3363
  // ../core/src/coverage.ts
3238
3364
  var MAX_LINE = 1e6;
@@ -3700,8 +3826,8 @@ function relativeTo(path25, root) {
3700
3826
  const head = path25.slice(0, base.length);
3701
3827
  const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
3702
3828
  if (!inside) return null;
3703
- const relative9 = path25.slice(base.length);
3704
- return relative9 || null;
3829
+ const relative8 = path25.slice(base.length);
3830
+ return relative8 || null;
3705
3831
  }
3706
3832
  function reportPathCandidates(raw, { repoRoot, bases }) {
3707
3833
  const slash = (value) => value.replace(/\\/g, "/");
@@ -3712,8 +3838,8 @@ function reportPathCandidates(raw, { repoRoot, bases }) {
3712
3838
  const add = (absolute) => {
3713
3839
  const normalized = normalizeSlashPath(absolute);
3714
3840
  if (!normalized) return;
3715
- const relative9 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
3716
- if (relative9 && !out.includes(relative9)) out.push(relative9);
3841
+ const relative8 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
3842
+ if (relative8 && !out.includes(relative8)) out.push(relative8);
3717
3843
  };
3718
3844
  if (isAbsolutePath(path25)) {
3719
3845
  add(path25);
@@ -3744,14 +3870,14 @@ function globSegmentRegExp(segment) {
3744
3870
  function expandPattern(pattern, cwd) {
3745
3871
  const normalized = pattern.replace(/\\/g, "/");
3746
3872
  if (!/[*?]/.test(normalized)) {
3747
- const file = path14.resolve(cwd, normalized);
3748
- return fs10.existsSync(file) && fs10.statSync(file).isFile() ? [file] : [];
3873
+ const file = path15.resolve(cwd, normalized);
3874
+ return fs11.existsSync(file) && fs11.statSync(file).isFile() ? [file] : [];
3749
3875
  }
3750
- const absolute = path14.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3876
+ const absolute = path15.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3751
3877
  const segments = normalized.split("/");
3752
3878
  const firstWild = segments.findIndex((s) => /[*?]/.test(s));
3753
3879
  const baseParts = segments.slice(0, firstWild);
3754
- const base = absolute ? baseParts.join("/") || "/" : path14.resolve(cwd, ...baseParts);
3880
+ const base = absolute ? baseParts.join("/") || "/" : path15.resolve(cwd, ...baseParts);
3755
3881
  const rest = segments.slice(firstWild).filter((s) => s !== "");
3756
3882
  const out = [];
3757
3883
  const walk = (dir, at) => {
@@ -3759,14 +3885,14 @@ function expandPattern(pattern, cwd) {
3759
3885
  const segment = rest[at];
3760
3886
  let entries;
3761
3887
  try {
3762
- entries = fs10.readdirSync(dir, { withFileTypes: true });
3888
+ entries = fs11.readdirSync(dir, { withFileTypes: true });
3763
3889
  } catch {
3764
3890
  return;
3765
3891
  }
3766
3892
  if (segment === "**") {
3767
3893
  walk(dir, at + 1);
3768
3894
  for (const entry of entries) {
3769
- if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path14.join(dir, entry.name), at);
3895
+ if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path15.join(dir, entry.name), at);
3770
3896
  }
3771
3897
  return;
3772
3898
  }
@@ -3774,7 +3900,7 @@ function expandPattern(pattern, cwd) {
3774
3900
  const last = at === rest.length - 1;
3775
3901
  for (const entry of entries) {
3776
3902
  if (!match.test(entry.name)) continue;
3777
- const full = path14.join(dir, entry.name);
3903
+ const full = path15.join(dir, entry.name);
3778
3904
  if (last && entry.isFile()) out.push(full);
3779
3905
  else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
3780
3906
  walk(full, at + 1);
@@ -3787,7 +3913,7 @@ function expandPattern(pattern, cwd) {
3787
3913
  async function readReport(file, format) {
3788
3914
  let reader = format ? new CoverageReportReader(format) : null;
3789
3915
  let head = "";
3790
- for await (const chunk of fs10.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3916
+ for await (const chunk of fs11.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3791
3917
  if (!reader) {
3792
3918
  head += chunk;
3793
3919
  if (head.length < 4096) continue;
@@ -3809,18 +3935,18 @@ async function readReport(file, format) {
3809
3935
  return reader.end();
3810
3936
  }
3811
3937
  function reportBases(report, reportFile, options) {
3812
- if (options.root) return [path14.resolve(options.cwd, options.root)];
3813
- const dir = path14.dirname(path14.resolve(reportFile));
3938
+ if (options.root) return [path15.resolve(options.cwd, options.root)];
3939
+ const dir = path15.dirname(path15.resolve(reportFile));
3814
3940
  const sources = report.sources.map(
3815
- (source) => path14.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path14.resolve(dir, source)
3941
+ (source) => path15.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path15.resolve(dir, source)
3816
3942
  );
3817
- return [...sources, options.cwd, dir, path14.dirname(dir), options.repoRoot];
3943
+ return [...sources, options.cwd, dir, path15.dirname(dir), options.repoRoot];
3818
3944
  }
3819
3945
  function resolveReport(report, reportFile, options, into) {
3820
3946
  const bases = reportBases(report, reportFile, options);
3821
3947
  for (const file of report.files) {
3822
3948
  const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
3823
- const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs10.existsSync(path14.join(options.repoRoot, candidate)));
3949
+ const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs11.existsSync(path15.join(options.repoRoot, candidate)));
3824
3950
  if (!match) {
3825
3951
  into.unmatched.push(file.path);
3826
3952
  continue;
@@ -3860,9 +3986,9 @@ Suite:
3860
3986
  --kind <kind> unit (the default) or e2e
3861
3987
 
3862
3988
  Connection:
3863
- --project <name|id> Project (env PIWI_PROJECT_NAME)
3864
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
3865
- --api-key <key> API key (env PIWI_API_KEY)
3989
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
3990
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
3991
+ --api-key <key> API key (env PIWI_API_KEY, .env)
3866
3992
  --branch <name> The branch (env PIWI_BRANCH, the CI's, else git's)
3867
3993
 
3868
3994
  Other:
@@ -3882,7 +4008,7 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
3882
4008
  "--api-key",
3883
4009
  "--branch"
3884
4010
  ]);
3885
- function parseCoverageUploadArgs(argv, env) {
4011
+ function parseCoverageUploadArgs(argv, env, dirs = []) {
3886
4012
  const values = /* @__PURE__ */ new Map();
3887
4013
  const patterns2 = [];
3888
4014
  const flags = /* @__PURE__ */ new Set();
@@ -3913,11 +4039,19 @@ function parseCoverageUploadArgs(argv, env) {
3913
4039
  if (format !== void 0 && format !== "cobertura" && format !== "lcov" && format !== "piwi") {
3914
4040
  throw new Error("--format is cobertura, lcov or piwi");
3915
4041
  }
3916
- const serverUrl = (values.get("--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
4042
+ const connection = resolveCliConnection(
4043
+ { serverUrl: values.get("--server-url"), apiKey: values.get("--api-key"), project: values.get("--project") },
4044
+ env,
4045
+ dirs,
4046
+ "piwi coverage upload"
4047
+ );
4048
+ const serverUrl = connection?.serverUrl ?? "";
3917
4049
  const dryRun = flags.has("--dry-run");
3918
- if (!serverUrl && !dryRun) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
3919
- const project = values.get("--project") ?? env.PIWI_PROJECT_NAME ?? "";
3920
- if (!project && !dryRun) throw new Error("No project \u2014 pass --project or set PIWI_PROJECT_NAME");
4050
+ if (!serverUrl && !dryRun)
4051
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
4052
+ const project = connection?.project ?? values.get("--project") ?? env.PIWI_PROJECT_NAME ?? "";
4053
+ if (!project && !dryRun)
4054
+ throw new Error("No project \u2014 pass --project or set PIWI_PROJECT_NAME (environment or .env)");
3921
4055
  return {
3922
4056
  patterns: patterns2,
3923
4057
  suite,
@@ -3926,7 +4060,7 @@ function parseCoverageUploadArgs(argv, env) {
3926
4060
  ...values.get("--root") ? { root: values.get("--root") } : {},
3927
4061
  project,
3928
4062
  serverUrl,
3929
- apiKey: values.get("--api-key") ?? env.PIWI_API_KEY ?? null,
4063
+ apiKey: connection?.apiKey ?? null,
3930
4064
  ...values.get("--branch") ? { branch: values.get("--branch") } : {},
3931
4065
  dryRun,
3932
4066
  json: flags.has("--json")
@@ -4045,7 +4179,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
4045
4179
  commit: checkout.commit,
4046
4180
  branch,
4047
4181
  suite: args.suite,
4048
- reports: reports.map((report) => path15.relative(cwd, report) || report),
4182
+ reports: reports.map((report) => path16.relative(cwd, report) || report),
4049
4183
  files: files.length,
4050
4184
  unmatched: resolved.unmatched.length,
4051
4185
  outOfScope: resolved.outOfScope,
@@ -4092,7 +4226,7 @@ ${USAGE4}`);
4092
4226
  }
4093
4227
  let args;
4094
4228
  try {
4095
- args = parseCoverageUploadArgs(rest, env);
4229
+ args = parseCoverageUploadArgs(rest, env, [cwd]);
4096
4230
  } catch (error) {
4097
4231
  console.error(`piwi coverage upload: ${errorMessage(error)}`);
4098
4232
  return EXIT_ERROR4;
@@ -4115,7 +4249,7 @@ ${USAGE4}`);
4115
4249
 
4116
4250
  // src/cli/flake.ts
4117
4251
  var import_node_child_process6 = require("child_process");
4118
- var fs11 = __toESM(require("fs"));
4252
+ var fs12 = __toESM(require("fs"));
4119
4253
  var os2 = __toESM(require("os"));
4120
4254
  var path17 = __toESM(require("path"));
4121
4255
 
@@ -4318,19 +4452,6 @@ function withinBudget(startedAt, now, budgetMs) {
4318
4452
  return now - startedAt < budgetMs;
4319
4453
  }
4320
4454
 
4321
- // src/internal/support/connection.ts
4322
- var path16 = __toESM(require("path"));
4323
- function resolveCliConnection(flags, env, dirs) {
4324
- let dotEnv = {};
4325
- for (const dir of dirs) dotEnv = { ...readDotEnv(path16.join(dir, ".env")), ...dotEnv };
4326
- return resolvePiwiConnection({
4327
- flags,
4328
- env,
4329
- dotEnv,
4330
- desktop: readDesktopConfig(env[PIWI_DESKTOP_CONFIG_ENV] || defaultDesktopConfigPath())
4331
- });
4332
- }
4333
-
4334
4455
  // src/cli/flake.ts
4335
4456
  var USAGE5 = `
4336
4457
  piwi flake \u2014 make a flaky test fail on demand, then prove the fix
@@ -4373,7 +4494,7 @@ Options:
4373
4494
  --json Print the results as JSON instead of text
4374
4495
  --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
4375
4496
  --api-key <key> Reporter API key (env PIWI_API_KEY, .env)
4376
- --project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME)
4497
+ --project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME, .env)
4377
4498
 
4378
4499
  Exit codes: 0 an arm reproduced the failure (verify: the fix held),
4379
4500
  1 nothing reproduced (verify: it still fails, or too few runs),
@@ -4521,8 +4642,8 @@ function playwrightRunner(dir, log) {
4521
4642
  now: () => Date.now(),
4522
4643
  async runBatch(plan, arm, repeat, batch) {
4523
4644
  const planFile = path17.join(dir, `${arm.id}.plan.json`);
4524
- if (!fs11.existsSync(planFile)) {
4525
- fs11.writeFileSync(
4645
+ if (!fs12.existsSync(planFile)) {
4646
+ fs12.writeFileSync(
4526
4647
  planFile,
4527
4648
  JSON.stringify({
4528
4649
  version: FLAKE_PLAN_VERSION,
@@ -4534,14 +4655,14 @@ function playwrightRunner(dir, log) {
4534
4655
  );
4535
4656
  }
4536
4657
  const resultsFile = path17.join(dir, `${arm.id}.${batch}.jsonl`);
4537
- fs11.writeFileSync(resultsFile, "");
4658
+ fs12.writeFileSync(resultsFile, "");
4538
4659
  const env = {
4539
4660
  ...process.env,
4540
4661
  [PIWI_FLAKE_ENV.plan]: planFile,
4541
4662
  [PIWI_FLAKE_ENV.results]: resultsFile
4542
4663
  };
4543
4664
  delete env[PIWI_PROBE_ENV.flag];
4544
- fs11.appendFileSync(log, `
4665
+ fs12.appendFileSync(log, `
4545
4666
  \u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
4546
4667
  `);
4547
4668
  await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
@@ -4554,7 +4675,7 @@ function playwrightRunner(dir, log) {
4554
4675
  }
4555
4676
  function readResults(file) {
4556
4677
  const lines = [];
4557
- for (const line of fs11.readFileSync(file, "utf8").split("\n")) {
4678
+ for (const line of fs12.readFileSync(file, "utf8").split("\n")) {
4558
4679
  if (!line.trim()) continue;
4559
4680
  try {
4560
4681
  lines.push(JSON.parse(line));
@@ -4718,7 +4839,7 @@ function printHeader(plan, commit, estimate, budgetMs) {
4718
4839
  async function loadPlan(args, commit) {
4719
4840
  if (args.planFile) {
4720
4841
  try {
4721
- const raw = JSON.parse(fs11.readFileSync(args.planFile, "utf8"));
4842
+ const raw = JSON.parse(fs12.readFileSync(args.planFile, "utf8"));
4722
4843
  const plan = "plan" in raw && raw.plan ? raw.plan : raw;
4723
4844
  if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
4724
4845
  if (args.verify !== (plan.kind === "verify")) {
@@ -4733,7 +4854,8 @@ async function loadPlan(args, commit) {
4733
4854
  const connection = resolveCliConnection(
4734
4855
  { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
4735
4856
  process.env,
4736
- [process.cwd()]
4857
+ [process.cwd()],
4858
+ args.verify ? "piwi flake verify" : "piwi flake"
4737
4859
  );
4738
4860
  if (!connection) {
4739
4861
  throw new FlakeError(
@@ -4812,7 +4934,7 @@ async function runFlake(argv, runner) {
4812
4934
  if (!args.json) {
4813
4935
  printHeader(plan, commit, estimate, args.budgetMs);
4814
4936
  }
4815
- const dir = fs11.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
4937
+ const dir = fs12.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
4816
4938
  const log = path17.join(dir, "playwright.log");
4817
4939
  let session;
4818
4940
  try {
@@ -4821,8 +4943,8 @@ async function runFlake(argv, runner) {
4821
4943
  });
4822
4944
  } catch (error) {
4823
4945
  console.error(`piwi flake: ${error.message}`);
4824
- if (fs11.existsSync(log)) {
4825
- const tail = fs11.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
4946
+ if (fs12.existsSync(log)) {
4947
+ const tail = fs12.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
4826
4948
  console.error(`
4827
4949
  Playwright's output (${log}):
4828
4950
  ${tail}`);
@@ -4895,7 +5017,7 @@ ${tail}`);
4895
5017
  verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
4896
5018
  exitCode: code
4897
5019
  };
4898
- fs11.rmSync(dir, { recursive: true, force: true });
5020
+ fs12.rmSync(dir, { recursive: true, force: true });
4899
5021
  if (args.json) {
4900
5022
  console.log(JSON.stringify(report, null, 2));
4901
5023
  return code;
@@ -4910,7 +5032,7 @@ ${tail}`);
4910
5032
  }
4911
5033
 
4912
5034
  // src/cli/gate.ts
4913
- var fs12 = __toESM(require("fs"));
5035
+ var fs13 = __toESM(require("fs"));
4914
5036
 
4915
5037
  // ../core/src/gate.ts
4916
5038
  function formatGateResult(result) {
@@ -4956,8 +5078,8 @@ Where the run comes from (first match wins):
4956
5078
  (default) ./piwi-run.json, if it exists
4957
5079
 
4958
5080
  Connection:
4959
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
4960
- --api-key <key> API key (env PIWI_API_KEY)
5081
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
5082
+ --api-key <key> API key (env PIWI_API_KEY, .env)
4961
5083
 
4962
5084
  Policy (at least one rule or warn-only flag is required):
4963
5085
  --require-tag <tags> Comma-separated; every test carrying the tag must pass
@@ -5006,16 +5128,22 @@ function readCount(argv, name) {
5006
5128
  }
5007
5129
  function readRunIdFromFile(path25) {
5008
5130
  try {
5009
- const parsed = JSON.parse(fs12.readFileSync(path25, "utf-8"));
5131
+ const parsed = JSON.parse(fs13.readFileSync(path25, "utf-8"));
5010
5132
  const runId = Number(parsed.runId);
5011
5133
  return Number.isFinite(runId) && runId > 0 ? runId : null;
5012
5134
  } catch {
5013
5135
  return null;
5014
5136
  }
5015
5137
  }
5016
- function parseGateArgs(argv, env) {
5017
- const serverUrl = (readOption4(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
5018
- if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
5138
+ function parseGateArgs(argv, env, dirs = []) {
5139
+ const connection = resolveCliConnection(
5140
+ { serverUrl: readOption4(argv, "--server-url"), apiKey: readOption4(argv, "--api-key") },
5141
+ env,
5142
+ dirs,
5143
+ "piwi gate"
5144
+ );
5145
+ if (!connection)
5146
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
5019
5147
  let runId = Number(readOption4(argv, "--run-id") ?? NaN);
5020
5148
  if (!Number.isFinite(runId) || runId <= 0) {
5021
5149
  const candidate = readOption4(argv, "--from-file") ?? env.PIWI_OUTPUT_FILE ?? "piwi-run.json";
@@ -5040,8 +5168,8 @@ function parseGateArgs(argv, env) {
5040
5168
  maxNewLeaks: readCount(argv, "--max-new-leaks")
5041
5169
  };
5042
5170
  return {
5043
- serverUrl,
5044
- apiKey: readOption4(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
5171
+ serverUrl: connection.serverUrl,
5172
+ apiKey: connection.apiKey,
5045
5173
  runId,
5046
5174
  policy,
5047
5175
  maxUncoveredChanges: readCount(argv, "--max-uncovered-changes"),
@@ -5066,14 +5194,14 @@ async function requestGate(args) {
5066
5194
  }
5067
5195
  return await res.json();
5068
5196
  }
5069
- async function runGate(argv, env = process.env) {
5197
+ async function runGate(argv, env = process.env, cwd = process.cwd()) {
5070
5198
  if (argv.includes("-h") || argv.includes("--help")) {
5071
5199
  console.log(USAGE6);
5072
5200
  return EXIT_OK5;
5073
5201
  }
5074
5202
  let args;
5075
5203
  try {
5076
- args = parseGateArgs(argv, env);
5204
+ args = parseGateArgs(argv, env, [cwd]);
5077
5205
  } catch (e) {
5078
5206
  console.error(`piwi gate: ${e.message}
5079
5207
  `);
@@ -5097,7 +5225,7 @@ async function runGate(argv, env = process.env) {
5097
5225
  }
5098
5226
 
5099
5227
  // src/cli/init.ts
5100
- var fs15 = __toESM(require("fs"));
5228
+ var fs16 = __toESM(require("fs"));
5101
5229
  var path20 = __toESM(require("path"));
5102
5230
 
5103
5231
  // src/cli/report.ts
@@ -5115,9 +5243,9 @@ function formatStep(result) {
5115
5243
  }
5116
5244
 
5117
5245
  // src/cli/detect.ts
5118
- var fs13 = __toESM(require("fs"));
5246
+ var fs14 = __toESM(require("fs"));
5119
5247
  var path18 = __toESM(require("path"));
5120
- var CONFIG_NAMES = [
5248
+ var CONFIG_NAMES2 = [
5121
5249
  "playwright.config.ts",
5122
5250
  "playwright.config.mts",
5123
5251
  "playwright.config.cts",
@@ -5136,7 +5264,7 @@ var REPORTER_PACKAGE = "@piwitests/reporter";
5136
5264
  function readPackageJson(root) {
5137
5265
  const pkgPath = path18.join(root, "package.json");
5138
5266
  try {
5139
- return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
5267
+ return { pkg: JSON.parse(fs14.readFileSync(pkgPath, "utf-8")), pkgPath };
5140
5268
  } catch {
5141
5269
  return { pkg: null, pkgPath: null };
5142
5270
  }
@@ -5145,14 +5273,14 @@ function detectPackageManager(root, pkg) {
5145
5273
  const declared = pkg?.packageManager?.split("@")[0];
5146
5274
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
5147
5275
  for (const [file, manager] of LOCKFILES) {
5148
- if (fs13.existsSync(path18.join(root, file))) return manager;
5276
+ if (fs14.existsSync(path18.join(root, file))) return manager;
5149
5277
  }
5150
5278
  return "npm";
5151
5279
  }
5152
5280
  function findConfig(root) {
5153
- for (const name of CONFIG_NAMES) {
5281
+ for (const name of CONFIG_NAMES2) {
5154
5282
  const candidate = path18.join(root, name);
5155
- if (fs13.existsSync(candidate)) return candidate;
5283
+ if (fs14.existsSync(candidate)) return candidate;
5156
5284
  }
5157
5285
  return null;
5158
5286
  }
@@ -5313,7 +5441,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
5313
5441
  }
5314
5442
 
5315
5443
  // src/cli/skills.ts
5316
- var fs14 = __toESM(require("fs"));
5444
+ var fs15 = __toESM(require("fs"));
5317
5445
  var path19 = __toESM(require("path"));
5318
5446
 
5319
5447
  // ../core/src/skill-stamp.ts
@@ -5381,7 +5509,7 @@ var DEFAULT_SKILLS_DIR = path19.join(".claude", "skills");
5381
5509
  function findTemplatesDir(fromDir) {
5382
5510
  let dir = fromDir;
5383
5511
  for (let i = 0; i < 6; i++) {
5384
- if (fs14.existsSync(path19.join(dir, "templates", "skills"))) return path19.join(dir, "templates");
5512
+ if (fs15.existsSync(path19.join(dir, "templates", "skills"))) return path19.join(dir, "templates");
5385
5513
  const parent = path19.dirname(dir);
5386
5514
  if (parent === dir) break;
5387
5515
  dir = parent;
@@ -5400,7 +5528,7 @@ function readFrontMatter(markdown) {
5400
5528
  }
5401
5529
  function readTemplatesVersion(templatesDir) {
5402
5530
  try {
5403
- const pkg = JSON.parse(fs14.readFileSync(path19.join(templatesDir, "..", "package.json"), "utf-8"));
5531
+ const pkg = JSON.parse(fs15.readFileSync(path19.join(templatesDir, "..", "package.json"), "utf-8"));
5404
5532
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
5405
5533
  } catch {
5406
5534
  return "0.0.0";
@@ -5413,8 +5541,8 @@ function listSkills(templatesDir, slugs = ALL_SKILLS) {
5413
5541
  const infos = [];
5414
5542
  for (const slug of slugs) {
5415
5543
  const file = templatePath(templatesDir, slug);
5416
- if (!fs14.existsSync(file)) continue;
5417
- const front = readFrontMatter(fs14.readFileSync(file, "utf-8"));
5544
+ if (!fs15.existsSync(file)) continue;
5545
+ const front = readFrontMatter(fs15.readFileSync(file, "utf-8"));
5418
5546
  infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
5419
5547
  }
5420
5548
  return infos;
@@ -5426,16 +5554,16 @@ function installSkills(opts) {
5426
5554
  const source = templatePath(opts.templatesDir, slug);
5427
5555
  const relDest = path19.join(opts.skillsDir, slug, "SKILL.md");
5428
5556
  const dest = path19.join(opts.root, relDest);
5429
- if (!fs14.existsSync(source)) {
5557
+ if (!fs15.existsSync(source)) {
5430
5558
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
5431
5559
  continue;
5432
5560
  }
5433
5561
  const version = opts.version ?? readTemplatesVersion(opts.templatesDir);
5434
- const contents = stampSkill(fs14.readFileSync(source, "utf-8"), version);
5435
- const exists = fs14.existsSync(dest);
5562
+ const contents = stampSkill(fs15.readFileSync(source, "utf-8"), version);
5563
+ const exists = fs15.existsSync(dest);
5436
5564
  let detail = "installed skill";
5437
5565
  if (exists) {
5438
- const installed = fs14.readFileSync(dest, "utf-8");
5566
+ const installed = fs15.readFileSync(dest, "utf-8");
5439
5567
  const state = classifyInstalledSkill(installed, contents);
5440
5568
  const from = readSkillStamp(installed).version;
5441
5569
  if (state === "current") {
@@ -5457,8 +5585,8 @@ function installSkills(opts) {
5457
5585
  }
5458
5586
  }
5459
5587
  if (!opts.dryRun) {
5460
- fs14.mkdirSync(path19.dirname(dest), { recursive: true });
5461
- fs14.writeFileSync(dest, contents);
5588
+ fs15.mkdirSync(path19.dirname(dest), { recursive: true });
5589
+ fs15.writeFileSync(dest, contents);
5462
5590
  }
5463
5591
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
5464
5592
  }
@@ -5552,7 +5680,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
5552
5680
 
5553
5681
  // src/cli/init.ts
5554
5682
  var DEFAULT_SERVER_URL = "http://localhost:3000";
5555
- var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
5683
+ var DESKTOP_APP_MODES2 = ["prefer", "fallback", "off"];
5556
5684
  var USAGE8 = `
5557
5685
  piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
5558
5686
 
@@ -5628,7 +5756,7 @@ function parseInitArgs(argv, env, cwd) {
5628
5756
  }
5629
5757
  function readFileOr(file, fallback) {
5630
5758
  try {
5631
- return fs15.readFileSync(file, "utf-8");
5759
+ return fs16.readFileSync(file, "utf-8");
5632
5760
  } catch {
5633
5761
  return fallback;
5634
5762
  }
@@ -5682,7 +5810,7 @@ function stepConfig(project, opts) {
5682
5810
  projectName: opts.projectName,
5683
5811
  desktopApp
5684
5812
  });
5685
- if (edit.status === "updated" && !opts.dryRun) fs15.writeFileSync(project.configPath, edit.text);
5813
+ if (edit.status === "updated" && !opts.dryRun) fs16.writeFileSync(project.configPath, edit.text);
5686
5814
  if (edit.status === "already" && desktopApp && !new RegExp(`\\bdesktopApp\\s*:\\s*['"\`]${desktopApp}['"\`]`).test(source)) {
5687
5815
  return {
5688
5816
  step,
@@ -5697,7 +5825,7 @@ function stepFixtures(project, opts) {
5697
5825
  const step = "fixtures";
5698
5826
  const rel = path20.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5699
5827
  const dest = path20.join(project.root, rel);
5700
- if (fs15.existsSync(dest)) {
5828
+ if (fs16.existsSync(dest)) {
5701
5829
  const current = readFileOr(dest, "");
5702
5830
  if (/piwiFixtures/.test(current))
5703
5831
  return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
@@ -5709,8 +5837,8 @@ function stepFixtures(project, opts) {
5709
5837
  };
5710
5838
  }
5711
5839
  if (!opts.dryRun) {
5712
- fs15.mkdirSync(path20.dirname(dest), { recursive: true });
5713
- fs15.writeFileSync(dest, fixturesContents());
5840
+ fs16.mkdirSync(path20.dirname(dest), { recursive: true });
5841
+ fs16.writeFileSync(dest, fixturesContents());
5714
5842
  }
5715
5843
  return {
5716
5844
  step,
@@ -5726,7 +5854,7 @@ function stepEnv(project, opts) {
5726
5854
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
5727
5855
  ["PIWI_API_KEY", ""]
5728
5856
  ]);
5729
- if (example.added.length && !opts.dryRun) fs15.writeFileSync(examplePath, example.text);
5857
+ if (example.added.length && !opts.dryRun) fs16.writeFileSync(examplePath, example.text);
5730
5858
  results.push({
5731
5859
  step: "env",
5732
5860
  file: ".env.example",
@@ -5738,7 +5866,7 @@ function stepEnv(project, opts) {
5738
5866
  const entries = [["PIWI_API_KEY", opts.apiKey]];
5739
5867
  if (opts.serverUrl) entries.unshift(["PIWI_DASHBOARD_URL", opts.serverUrl]);
5740
5868
  const env = upsertEnvKeys(readFileOr(envPath, ""), entries);
5741
- if (env.added.length && !opts.dryRun) fs15.writeFileSync(envPath, env.text);
5869
+ if (env.added.length && !opts.dryRun) fs16.writeFileSync(envPath, env.text);
5742
5870
  results.push({
5743
5871
  step: "env",
5744
5872
  file: ".env",
@@ -5747,7 +5875,7 @@ function stepEnv(project, opts) {
5747
5875
  });
5748
5876
  const gitignorePath = path20.join(project.root, ".gitignore");
5749
5877
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
5750
- if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
5878
+ if (gitignore.added && !opts.dryRun) fs16.writeFileSync(gitignorePath, gitignore.text);
5751
5879
  results.push({
5752
5880
  step: "gitignore",
5753
5881
  file: ".gitignore",
@@ -5790,8 +5918,8 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
5790
5918
  return 0;
5791
5919
  }
5792
5920
  const opts = parseInitArgs(argv, env, cwd);
5793
- if (opts.desktopApp !== null && !DESKTOP_APP_MODES.includes(opts.desktopApp)) {
5794
- console.error(`--desktop-app must be one of ${DESKTOP_APP_MODES.join(", ")} (got "${opts.desktopApp}")`);
5921
+ if (opts.desktopApp !== null && !DESKTOP_APP_MODES2.includes(opts.desktopApp)) {
5922
+ console.error(`--desktop-app must be one of ${DESKTOP_APP_MODES2.join(", ")} (got "${opts.desktopApp}")`);
5795
5923
  return 1;
5796
5924
  }
5797
5925
  const project = detectProject(opts.root);
@@ -5848,7 +5976,7 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
5848
5976
  }
5849
5977
 
5850
5978
  // src/cli/select.ts
5851
- var fs17 = __toESM(require("fs"));
5979
+ var fs18 = __toESM(require("fs"));
5852
5980
  var path22 = __toESM(require("path"));
5853
5981
  var import_node_child_process7 = require("child_process");
5854
5982
 
@@ -5979,9 +6107,9 @@ function pathGlobToRegExp(glob) {
5979
6107
  return new RegExp(`^(?:${parts.join("|")})$`);
5980
6108
  }
5981
6109
  function isTranslationFile(path25, globs = DEFAULT_TRANSLATION_GLOBS) {
5982
- const posix = path25.replace(/\\/g, "/");
5983
- if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
5984
- return globs.some((g) => pathGlobToRegExp(g).test(posix));
6110
+ const posix2 = path25.replace(/\\/g, "/");
6111
+ if (!TRANSLATION_EXTENSIONS.test(posix2)) return false;
6112
+ return globs.some((g) => pathGlobToRegExp(g).test(posix2));
5985
6113
  }
5986
6114
  function keepable(value) {
5987
6115
  const v = value.trim();
@@ -6694,7 +6822,7 @@ function diffImpactInput(files) {
6694
6822
  }
6695
6823
 
6696
6824
  // src/cli/add-reporter.ts
6697
- var fs16 = __toESM(require("fs"));
6825
+ var fs17 = __toESM(require("fs"));
6698
6826
  var path21 = __toESM(require("path"));
6699
6827
  var import_node_module2 = require("module");
6700
6828
  var REPORTER_PACKAGE2 = "@piwitests/reporter";
@@ -6738,7 +6866,7 @@ function resolveConfigPath(cwd, playwrightArgs2) {
6738
6866
  function readConfigSource(configPath) {
6739
6867
  if (!configPath) return null;
6740
6868
  try {
6741
- return fs16.readFileSync(configPath, "utf-8");
6869
+ return fs17.readFileSync(configPath, "utf-8");
6742
6870
  } catch {
6743
6871
  return null;
6744
6872
  }
@@ -6773,9 +6901,9 @@ Usage:
6773
6901
  npx @piwitests/reporter run impact --base <ref> run the tests your diff impacts
6774
6902
 
6775
6903
  Connection:
6776
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
6777
- --api-key <key> API key (env PIWI_API_KEY)
6778
- --project <name|id> Project (env PIWI_PROJECT_NAME)
6904
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
6905
+ --api-key <key> API key (env PIWI_API_KEY, .env)
6906
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
6779
6907
 
6780
6908
  Selection:
6781
6909
  --format <fmt> args (file:line, default) | grep | files | json
@@ -6832,12 +6960,22 @@ function parseDuration2(raw) {
6832
6960
  const ms = Math.round(value * factor);
6833
6961
  return ms > 0 ? ms : null;
6834
6962
  }
6835
- function parseSelectArgs(argv, env) {
6963
+ function parseSelectArgs(argv, env, dirs = [], command = "piwi select") {
6836
6964
  const dashIndex = argv.indexOf("--");
6837
6965
  const own = dashIndex === -1 ? argv : argv.slice(0, dashIndex);
6838
6966
  const extra = dashIndex === -1 ? [] : argv.slice(dashIndex + 1);
6839
- const serverUrl = (readOption7(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
6840
- if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
6967
+ const connection = resolveCliConnection(
6968
+ {
6969
+ serverUrl: readOption7(own, "--server-url"),
6970
+ apiKey: readOption7(own, "--api-key"),
6971
+ project: readOption7(own, "--project")
6972
+ },
6973
+ env,
6974
+ dirs,
6975
+ command
6976
+ );
6977
+ if (!connection)
6978
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
6841
6979
  const key = findKey(own);
6842
6980
  if (!key) throw new Error("No selection key \u2014 usage: piwi select <key>");
6843
6981
  const budgetRaw = readOption7(own, "--budget");
@@ -6851,9 +6989,9 @@ function parseSelectArgs(argv, env) {
6851
6989
  throw new Error(`--shard expects an "i/n" spec like 2/4, got "${shard}"`);
6852
6990
  }
6853
6991
  return {
6854
- serverUrl,
6855
- apiKey: readOption7(own, "--api-key") ?? env.PIWI_API_KEY ?? null,
6856
- project: readOption7(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
6992
+ serverUrl: connection.serverUrl,
6993
+ apiKey: connection.apiKey,
6994
+ project: connection.project,
6857
6995
  key,
6858
6996
  format: readOption7(own, "--format") ?? "args",
6859
6997
  budgetMs,
@@ -6872,7 +7010,7 @@ function cacheKey(args) {
6872
7010
  }
6873
7011
  function readCache(args) {
6874
7012
  try {
6875
- const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
7013
+ const store = JSON.parse(fs18.readFileSync(CACHE_FILE, "utf-8"));
6876
7014
  return store[cacheKey(args)] ?? null;
6877
7015
  } catch {
6878
7016
  return null;
@@ -6882,12 +7020,12 @@ function writeCache(args, resolution) {
6882
7020
  try {
6883
7021
  let store = {};
6884
7022
  try {
6885
- store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
7023
+ store = JSON.parse(fs18.readFileSync(CACHE_FILE, "utf-8"));
6886
7024
  } catch {
6887
7025
  }
6888
7026
  store[cacheKey(args)] = resolution;
6889
- fs17.mkdirSync(path22.dirname(CACHE_FILE), { recursive: true });
6890
- fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
7027
+ fs18.mkdirSync(path22.dirname(CACHE_FILE), { recursive: true });
7028
+ fs18.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
6891
7029
  } catch {
6892
7030
  }
6893
7031
  }
@@ -6979,7 +7117,7 @@ async function runSelect(argv, env = process.env) {
6979
7117
  }
6980
7118
  let args;
6981
7119
  try {
6982
- args = parseSelectArgs(argv, env);
7120
+ args = parseSelectArgs(argv, env, [process.cwd()]);
6983
7121
  } catch (e) {
6984
7122
  console.error(`piwi select: ${e.message}
6985
7123
  `);
@@ -7070,7 +7208,7 @@ async function runRun(argv, env = process.env) {
7070
7208
  }
7071
7209
  let args;
7072
7210
  try {
7073
- args = parseSelectArgs(argv, env);
7211
+ args = parseSelectArgs(argv, env, [process.cwd()], "piwi run");
7074
7212
  } catch (e) {
7075
7213
  console.error(`piwi run: ${e.message}
7076
7214
  `);
@@ -7120,7 +7258,7 @@ async function runRun(argv, env = process.env) {
7120
7258
  }
7121
7259
 
7122
7260
  // src/cli/preflight.ts
7123
- var fs18 = __toESM(require("fs"));
7261
+ var fs19 = __toESM(require("fs"));
7124
7262
  var path23 = __toESM(require("path"));
7125
7263
  var import_node_child_process8 = require("child_process");
7126
7264
 
@@ -7186,8 +7324,8 @@ Actions:
7186
7324
 
7187
7325
  Connection:
7188
7326
  --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
7189
- --api-key <key> API key (env PIWI_API_KEY)
7190
- --project <name|id> Project (env PIWI_PROJECT_NAME)
7327
+ --api-key <key> API key (env PIWI_API_KEY, .env)
7328
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
7191
7329
 
7192
7330
  Output:
7193
7331
  --json Print the breaks, the edits and the impact as JSON
@@ -7244,16 +7382,6 @@ function parsePreflightArgs(argv) {
7244
7382
  extra: dashIndex === -1 ? [] : argv.slice(dashIndex + 1)
7245
7383
  };
7246
7384
  }
7247
- var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
7248
- function findPlaywrightRoot(from) {
7249
- let dir = path23.resolve(from);
7250
- for (; ; ) {
7251
- if (CONFIG_NAMES2.some((name) => fs18.existsSync(path23.join(dir, name)))) return dir;
7252
- const parent = path23.dirname(dir);
7253
- if (parent === dir) return null;
7254
- dir = parent;
7255
- }
7256
- }
7257
7385
  function git3(cwd, args) {
7258
7386
  return (0, import_node_child_process8.execFileSync)("git", args, {
7259
7387
  cwd,
@@ -7271,7 +7399,12 @@ function tryGit(cwd, args) {
7271
7399
  }
7272
7400
  var TEST_FILE = /(?:^|\/)[^/]+\.(?:spec|test)\.[cm]?[jt]sx?$/;
7273
7401
  function resolveConnection(args, env, dirs) {
7274
- return resolveCliConnection({ serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project }, env, dirs);
7402
+ return resolveCliConnection(
7403
+ { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
7404
+ env,
7405
+ dirs,
7406
+ "piwi preflight"
7407
+ );
7275
7408
  }
7276
7409
  function translationLookup(repoRoot, base, locale) {
7277
7410
  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));
@@ -7298,7 +7431,7 @@ function prefersLocale(file, locale) {
7298
7431
  }
7299
7432
  function readText(file) {
7300
7433
  try {
7301
- return fs18.readFileSync(file, "utf-8");
7434
+ return fs19.readFileSync(file, "utf-8");
7302
7435
  } catch {
7303
7436
  return void 0;
7304
7437
  }
@@ -7309,7 +7442,7 @@ function cacheKey2(connection, branch) {
7309
7442
  }
7310
7443
  function readCache2(root, key) {
7311
7444
  try {
7312
- const store = JSON.parse(fs18.readFileSync(path23.join(root, CACHE_FILE2), "utf-8"));
7445
+ const store = JSON.parse(fs19.readFileSync(path23.join(root, CACHE_FILE2), "utf-8"));
7313
7446
  return store[key] ?? null;
7314
7447
  } catch {
7315
7448
  return null;
@@ -7320,12 +7453,12 @@ function writeCache2(root, key, index) {
7320
7453
  try {
7321
7454
  let store = {};
7322
7455
  try {
7323
- store = JSON.parse(fs18.readFileSync(file, "utf-8"));
7456
+ store = JSON.parse(fs19.readFileSync(file, "utf-8"));
7324
7457
  } catch {
7325
7458
  }
7326
7459
  store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
7327
- fs18.mkdirSync(path23.dirname(file), { recursive: true });
7328
- fs18.writeFileSync(file, JSON.stringify(store));
7460
+ fs19.mkdirSync(path23.dirname(file), { recursive: true });
7461
+ fs19.writeFileSync(file, JSON.stringify(store));
7329
7462
  } catch {
7330
7463
  }
7331
7464
  }
@@ -7384,7 +7517,7 @@ function applyEdits(breaks, replacementsOf) {
7384
7517
  }
7385
7518
  const edited = [];
7386
7519
  for (const [file, edits] of byFile) {
7387
- const text = fs18.readFileSync(file, "utf-8");
7520
+ const text = fs19.readFileSync(file, "utf-8");
7388
7521
  const eol = text.includes("\r\n") ? "\r\n" : "\n";
7389
7522
  const lines = text.split(/\r?\n/);
7390
7523
  for (const e of edits) {
@@ -7394,7 +7527,7 @@ function applyEdits(breaks, replacementsOf) {
7394
7527
  }
7395
7528
  const next = lines.join(eol);
7396
7529
  if (next !== text) {
7397
- fs18.writeFileSync(file, next);
7530
+ fs19.writeFileSync(file, next);
7398
7531
  edited.push(file);
7399
7532
  }
7400
7533
  }
@@ -7505,7 +7638,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7505
7638
  console.error("piwi preflight: not inside a git repository");
7506
7639
  return EXIT_ERROR8;
7507
7640
  }
7508
- const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7641
+ const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd, repoRoot) ?? cwd);
7509
7642
  let files;
7510
7643
  try {
7511
7644
  files = parseUnifiedDiff(
@@ -7631,7 +7764,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7631
7764
  }
7632
7765
 
7633
7766
  // src/cli/probe.ts
7634
- var fs19 = __toESM(require("fs"));
7767
+ var fs20 = __toESM(require("fs"));
7635
7768
  var os3 = __toESM(require("os"));
7636
7769
  var path24 = __toESM(require("path"));
7637
7770
  var EXIT_OK8 = 0;
@@ -7643,9 +7776,9 @@ Usage:
7643
7776
  npx @piwitests/reporter probe [options] [-- <playwright args>]
7644
7777
 
7645
7778
  Options:
7646
- --server-url <url> Dashboard URL (or PIWI_DASHBOARD_URL)
7647
- --api-key <key> Reporter API key (or PIWI_API_KEY)
7648
- --project <name|id> Project name or id (or PIWI_PROJECT_NAME)
7779
+ --server-url <url> Dashboard URL (or PIWI_DASHBOARD_URL, .env, the desktop app)
7780
+ --api-key <key> Reporter API key (or PIWI_API_KEY, .env)
7781
+ --project <name|id> Project name or id (or PIWI_PROJECT_NAME, .env)
7649
7782
  --budget <n> Max pairs to probe this run (default the server's)
7650
7783
 
7651
7784
  Everything after \`--\` is passed to \`playwright test\`.
@@ -7658,21 +7791,29 @@ function escapeRegex2(s) {
7658
7791
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7659
7792
  }
7660
7793
  async function runProbe(argv) {
7661
- const sep7 = argv.indexOf("--");
7662
- const own = sep7 < 0 ? argv : argv.slice(0, sep7);
7663
- const playwrightArgs2 = sep7 < 0 ? [] : argv.slice(sep7 + 1);
7794
+ const sep6 = argv.indexOf("--");
7795
+ const own = sep6 < 0 ? argv : argv.slice(0, sep6);
7796
+ const playwrightArgs2 = sep6 < 0 ? [] : argv.slice(sep6 + 1);
7664
7797
  if (own.includes("-h") || own.includes("--help")) {
7665
7798
  console.log(USAGE11);
7666
7799
  return EXIT_OK8;
7667
7800
  }
7668
7801
  const env = process.env;
7669
- const serverUrl = (readOption9(own, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
7670
- if (!serverUrl) {
7671
- console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
7802
+ const connection = resolveCliConnection(
7803
+ {
7804
+ serverUrl: readOption9(own, "--server-url"),
7805
+ apiKey: readOption9(own, "--api-key"),
7806
+ project: readOption9(own, "--project")
7807
+ },
7808
+ env,
7809
+ [process.cwd()],
7810
+ "piwi probe"
7811
+ );
7812
+ if (!connection) {
7813
+ console.error("piwi probe: no dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
7672
7814
  return EXIT_ERROR9;
7673
7815
  }
7674
- const apiKey = readOption9(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
7675
- const project = readOption9(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
7816
+ const { serverUrl, apiKey, project } = connection;
7676
7817
  const budget = readOption9(own, "--budget");
7677
7818
  let projectId;
7678
7819
  try {
@@ -7697,12 +7838,12 @@ async function runProbe(argv) {
7697
7838
  return EXIT_OK8;
7698
7839
  }
7699
7840
  console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
7700
- const dir = fs19.mkdtempSync(path24.join(os3.tmpdir(), "piwi-probe-"));
7841
+ const dir = fs20.mkdtempSync(path24.join(os3.tmpdir(), "piwi-probe-"));
7701
7842
  const planFile = path24.join(dir, "plan.json");
7702
7843
  const resultsFile = path24.join(dir, "results.jsonl");
7703
7844
  const runOutputFile = path24.join(dir, "run.json");
7704
- fs19.writeFileSync(planFile, JSON.stringify(plan));
7705
- fs19.writeFileSync(resultsFile, "");
7845
+ fs20.writeFileSync(planFile, JSON.stringify(plan));
7846
+ fs20.writeFileSync(resultsFile, "");
7706
7847
  const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
7707
7848
  const pwArgs = [...playwrightArgs2];
7708
7849
  if (grep && !playwrightArgs2.includes("--grep") && !playwrightArgs2.includes("-g")) pwArgs.push("--grep", grep);
@@ -7716,7 +7857,7 @@ async function runProbe(argv) {
7716
7857
  [PIWI_ENV_KEYS.outputFile]: runOutputFile
7717
7858
  };
7718
7859
  await spawnPlaywright(pwArgs, childEnv, { command: "piwi probe" });
7719
- const lines = fs19.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
7860
+ const lines = fs20.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
7720
7861
  const results = [];
7721
7862
  for (const line of lines) {
7722
7863
  try {
@@ -7730,7 +7871,7 @@ async function runProbe(argv) {
7730
7871
  }
7731
7872
  let runId = null;
7732
7873
  try {
7733
- const out = JSON.parse(fs19.readFileSync(runOutputFile, "utf8"));
7874
+ const out = JSON.parse(fs20.readFileSync(runOutputFile, "utf8"));
7734
7875
  if (typeof out.runId === "number") runId = out.runId;
7735
7876
  } catch {
7736
7877
  }
@@ -7755,7 +7896,7 @@ async function runProbe(argv) {
7755
7896
  }
7756
7897
 
7757
7898
  // src/cli/quality-report.ts
7758
- var fs20 = __toESM(require("fs"));
7899
+ var fs21 = __toESM(require("fs"));
7759
7900
  var EXIT_OK9 = 0;
7760
7901
  var EXIT_VERDICT = 1;
7761
7902
  var EXIT_ERROR10 = 2;
@@ -7768,8 +7909,8 @@ Usage:
7768
7909
  npx @piwitests/reporter report [options]
7769
7910
 
7770
7911
  Connection:
7771
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
7772
- --api-key <key> API key (env PIWI_API_KEY)
7912
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
7913
+ --api-key <key> API key (env PIWI_API_KEY, .env)
7773
7914
 
7774
7915
  What to report:
7775
7916
  --project <names|ids> Comma-separated projects (default: every project you can see)
@@ -7801,9 +7942,15 @@ function normalizePeriod(raw) {
7801
7942
  const days = raw.trim().match(/^(\d+)d$/);
7802
7943
  return days ? `last-${days[1]}d` : raw.trim();
7803
7944
  }
7804
- function parseReportArgs(argv, env) {
7805
- const serverUrl = (readOption10(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
7806
- if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
7945
+ function parseReportArgs(argv, env, dirs = []) {
7946
+ const connection = resolveCliConnection(
7947
+ { serverUrl: readOption10(argv, "--server-url"), apiKey: readOption10(argv, "--api-key") },
7948
+ env,
7949
+ dirs,
7950
+ "piwi report"
7951
+ );
7952
+ if (!connection)
7953
+ throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL (environment or .env)");
7807
7954
  const format = (readOption10(argv, "--format") ?? "md").toLowerCase();
7808
7955
  if (!FORMATS.includes(format)) {
7809
7956
  throw new Error(`--format must be one of ${FORMATS.join(", ")}, got "${format}"`);
@@ -7823,8 +7970,8 @@ function parseReportArgs(argv, env) {
7823
7970
  const output = readOption10(argv, "--output") ?? null;
7824
7971
  if (format === "pdf" && !output) throw new Error("A PDF needs --output <file>");
7825
7972
  return {
7826
- serverUrl,
7827
- apiKey: readOption10(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
7973
+ serverUrl: connection.serverUrl,
7974
+ apiKey: connection.apiKey,
7828
7975
  projects: (readOption10(argv, "--project") ?? "").split(",").map((p) => p.trim()).filter(Boolean),
7829
7976
  period: normalizePeriod(readOption10(argv, "--period") ?? "30d"),
7830
7977
  compare: readOption10(argv, "--compare") ?? null,
@@ -7868,14 +8015,14 @@ async function runQualityReport(argv) {
7868
8015
  return EXIT_OK9;
7869
8016
  }
7870
8017
  try {
7871
- const args = parseReportArgs(argv, process.env);
8018
+ const args = parseReportArgs(argv, process.env, [process.cwd()]);
7872
8019
  const projectIds = [];
7873
8020
  for (const project of args.projects) {
7874
8021
  projectIds.push(await resolveProjectId({ serverUrl: args.serverUrl, apiKey: args.apiKey, project, key: "" }));
7875
8022
  }
7876
8023
  const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
7877
8024
  const bytes = new Uint8Array(await res.arrayBuffer());
7878
- if (args.output) fs20.writeFileSync(args.output, bytes);
8025
+ if (args.output) fs21.writeFileSync(args.output, bytes);
7879
8026
  else process.stdout.write(bytes);
7880
8027
  if (!args.failOn) return EXIT_OK9;
7881
8028
  const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();