@piwitests/reporter 0.61.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",
@@ -1007,6 +1013,82 @@ var ENV_FALLBACK_SPECS = [
1007
1013
  { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
1008
1014
  { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
1009
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
+ }
1010
1092
 
1011
1093
  // src/cli/bug.ts
1012
1094
  var EXIT_OK2 = 0;
@@ -1029,8 +1111,8 @@ Options:
1029
1111
  --no-run With --write or --out: write it without running it
1030
1112
  --force Replace an existing file
1031
1113
  --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)
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)
1034
1116
 
1035
1117
  Exit codes: 0 printed or written (and, when run, the test behaved as its
1036
1118
  test.fail() expects: the bug reproduces), 1 the run did not (the bug may be
@@ -1044,7 +1126,7 @@ function readOption2(argv, name) {
1044
1126
  const value = argv[index + 1];
1045
1127
  return value !== void 0 && !value.startsWith("--") ? value : void 0;
1046
1128
  }
1047
- function parseBugArgs(argv, env) {
1129
+ function parseBugArgs(argv, env, dirs = []) {
1048
1130
  let idText;
1049
1131
  for (let i = 0; i < argv.length; i++) {
1050
1132
  const token = argv[i];
@@ -1060,8 +1142,13 @@ function parseBugArgs(argv, env) {
1060
1142
  for (const flag of VALUE_FLAGS) {
1061
1143
  if (argv.includes(flag) && readOption2(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
1062
1144
  }
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");
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)");
1065
1152
  const out = readOption2(argv, "--out") ?? null;
1066
1153
  const write = argv.includes("--write") || out !== null;
1067
1154
  return {
@@ -1071,8 +1158,8 @@ function parseBugArgs(argv, env) {
1071
1158
  run: write && !argv.includes("--no-run"),
1072
1159
  force: argv.includes("--force"),
1073
1160
  mode: argv.includes("--run-mode") ? "run" : "commit",
1074
- serverUrl,
1075
- apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null
1161
+ serverUrl: connection.serverUrl,
1162
+ apiKey: connection.apiKey
1076
1163
  };
1077
1164
  }
1078
1165
  async function fetchBugSpec(args, specDir) {
@@ -1094,35 +1181,23 @@ async function fetchBugSpec(args, specDir) {
1094
1181
  if (typeof body.code !== "string" || typeof body.path !== "string") throw new Error("the dashboard sent no spec");
1095
1182
  return { code: body.code, path: body.path, warnings: Array.isArray(body.warnings) ? body.warnings : [] };
1096
1183
  }
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
1184
  function realFolder(dir) {
1110
1185
  const rest = [];
1111
1186
  let current = dir;
1112
- while (!fs6.existsSync(current) && path9.dirname(current) !== current) {
1113
- rest.unshift(path9.basename(current));
1114
- current = path9.dirname(current);
1187
+ while (!fs7.existsSync(current) && path10.dirname(current) !== current) {
1188
+ rest.unshift(path10.basename(current));
1189
+ current = path10.dirname(current);
1115
1190
  }
1116
- return path9.join(fs6.realpathSync(current), ...rest);
1191
+ return path10.join(fs7.realpathSync(current), ...rest);
1117
1192
  }
1118
1193
  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("/");
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("/");
1122
1197
  }
1123
1198
  function fileFilter(file) {
1124
1199
  const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1125
- return `${escape(path9.basename(path9.dirname(file)))}[\\\\/]${escape(path9.basename(file))}$`;
1200
+ return `${escape(path10.basename(path10.dirname(file)))}[\\\\/]${escape(path10.basename(file))}$`;
1126
1201
  }
1127
1202
  function runPlaywright(file, cwd, id, env) {
1128
1203
  return spawnPlaywright([fileFilter(file)], { ...env, ...originEnv("bug", id) }, { cwd, command: "piwi bug" });
@@ -1134,18 +1209,18 @@ async function runBug(argv, env = process.env, cwd = process.cwd()) {
1134
1209
  }
1135
1210
  let args;
1136
1211
  try {
1137
- args = parseBugArgs(argv, env);
1212
+ args = parseBugArgs(argv, env, readOption2(argv, "--server-url") ? [] : connectionFolders(cwd));
1138
1213
  } catch (e) {
1139
1214
  console.error(`piwi bug: ${e.message}
1140
1215
 
1141
1216
  ${USAGE2}`);
1142
1217
  return EXIT_ERROR2;
1143
1218
  }
1144
- const root = args.write ? repositoryRoot(cwd) : null;
1145
- 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;
1146
1221
  let spec;
1147
1222
  try {
1148
- 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);
1149
1224
  } catch (e) {
1150
1225
  console.error(`piwi bug: ${e.message}`);
1151
1226
  return EXIT_ERROR2;
@@ -1160,16 +1235,16 @@ ${USAGE2}`);
1160
1235
  if (!outFile && !root) {
1161
1236
  console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
1162
1237
  }
1163
- const file = outFile ?? path9.resolve(root ?? cwd, spec.path);
1238
+ const file = outFile ?? path10.resolve(root ?? cwd, spec.path);
1164
1239
  try {
1165
- fs6.mkdirSync(path9.dirname(file), { recursive: true });
1166
- 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" });
1167
1242
  } catch (e) {
1168
1243
  const exists = e.code === "EEXIST";
1169
1244
  console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
1170
1245
  return EXIT_ERROR2;
1171
1246
  }
1172
- const shown = path9.relative(cwd, file) || file;
1247
+ const shown = path10.relative(cwd, file) || file;
1173
1248
  console.error(`piwi bug: wrote ${shown}`);
1174
1249
  if (!args.run) return EXIT_OK2;
1175
1250
  const code = await runPlaywright(file, cwd, args.id, env);
@@ -1187,8 +1262,8 @@ ${USAGE2}`);
1187
1262
  }
1188
1263
 
1189
1264
  // src/cli/codegen.ts
1190
- var fs7 = __toESM(require("fs"));
1191
- var path10 = __toESM(require("path"));
1265
+ var fs8 = __toESM(require("fs"));
1266
+ var path11 = __toESM(require("path"));
1192
1267
 
1193
1268
  // ../core/src/recording.ts
1194
1269
  var ASSERTION_MATCHERS = [
@@ -1879,6 +1954,44 @@ function locatorForStep(step, index, ctx) {
1879
1954
  }
1880
1955
  return `${ctx.page}.${chosen.text}`;
1881
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
+ }
1882
1995
  function renderAssertStep(step, index, ctx) {
1883
1996
  const assertion = step.action === "assertVisible" ? null : step.assertion;
1884
1997
  if (step.action === "assert" && !assertion) {
@@ -1889,6 +2002,7 @@ function renderAssertStep(step, index, ctx) {
1889
2002
  const not = assertion?.negated ? ".not" : "";
1890
2003
  const lines = [];
1891
2004
  if (assertion?.note) lines.push(` // ${commentText(assertion.note)}`);
2005
+ if (matcher === "toMatchAriaSnapshot") return [...lines, ...renderAriaSnapshotStep(step, index, ctx, not)];
1892
2006
  let arg = "";
1893
2007
  if (VALUE_MATCHERS.has(matcher)) {
1894
2008
  if (assertion?.expected == null) {
@@ -2779,9 +2893,9 @@ Code:
2779
2893
  --tag <tag> Add a tag; repeat for more
2780
2894
 
2781
2895
  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)
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)
2785
2899
  --offline Do not contact the dashboard
2786
2900
 
2787
2901
  Exit codes: 0 ok, 2 the steps could not be read or the spec could not be written.
@@ -2821,12 +2935,22 @@ function findFile(argv) {
2821
2935
  }
2822
2936
  return void 0;
2823
2937
  }
2824
- function parseCodegenArgs(argv, env) {
2938
+ function parseCodegenArgs(argv, env, dirs = []) {
2825
2939
  const file = findFile(argv);
2826
2940
  if (!file) throw new Error("No steps file \u2014 usage: piwi codegen <steps.json>");
2827
2941
  for (const flag of VALUE_FLAGS2) {
2828
2942
  if (argv.includes(flag) && readOption3(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
2829
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
+ );
2830
2954
  return {
2831
2955
  file,
2832
2956
  out: readOption3(argv, "--out") ?? null,
@@ -2840,9 +2964,9 @@ function parseCodegenArgs(argv, env) {
2840
2964
  fail: argv.includes("--fail") || argv.includes("--fail-reason"),
2841
2965
  failReason: readOption3(argv, "--fail-reason") ?? null,
2842
2966
  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,
2967
+ project: connection?.project ?? readOption3(argv, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
2968
+ serverUrl: connection?.serverUrl ?? "",
2969
+ apiKey: connection?.apiKey ?? null,
2846
2970
  offline: argv.includes("--offline")
2847
2971
  };
2848
2972
  }
@@ -2901,14 +3025,15 @@ async function loadProjectContext(args) {
2901
3025
  );
2902
3026
  return { catalog, suiteLocators };
2903
3027
  }
2904
- async function runCodegen(argv, env = process.env) {
3028
+ async function runCodegen(argv, env = process.env, cwd = process.cwd()) {
2905
3029
  if (argv.includes("-h") || argv.includes("--help")) {
2906
3030
  console.log(USAGE3);
2907
3031
  return EXIT_OK3;
2908
3032
  }
2909
3033
  let args;
2910
3034
  try {
2911
- 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));
2912
3037
  } catch (e) {
2913
3038
  console.error(`piwi codegen: ${e.message}
2914
3039
 
@@ -2920,7 +3045,7 @@ ${USAGE3}`);
2920
3045
  if (bugId) {
2921
3046
  if (!args.serverUrl) {
2922
3047
  console.error(
2923
- "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)"
2924
3049
  );
2925
3050
  return EXIT_ERROR3;
2926
3051
  }
@@ -2933,8 +3058,8 @@ ${USAGE3}`);
2933
3058
  }
2934
3059
  } else {
2935
3060
  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");
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");
2938
3063
  } catch (e) {
2939
3064
  const reason = e.code === "ENOENT" ? "does not exist" : e.message;
2940
3065
  console.error(`piwi codegen: ${args.file} ${reason}`);
@@ -2966,8 +3091,8 @@ ${USAGE3}`);
2966
3091
  return EXIT_OK3;
2967
3092
  }
2968
3093
  try {
2969
- fs7.mkdirSync(path10.dirname(args.out), { recursive: true });
2970
- 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" });
2971
3096
  } catch (e) {
2972
3097
  const exists = e.code === "EEXIST";
2973
3098
  console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
@@ -2981,7 +3106,7 @@ ${USAGE3}`);
2981
3106
  }
2982
3107
 
2983
3108
  // src/cli/coverage.ts
2984
- var path15 = __toESM(require("path"));
3109
+ var path16 = __toESM(require("path"));
2985
3110
 
2986
3111
  // ../core/src/codeowners.ts
2987
3112
  function patternParts(pattern) {
@@ -3105,7 +3230,7 @@ function errorMessage(error) {
3105
3230
  }
3106
3231
 
3107
3232
  // src/internal/support/test-paths.ts
3108
- var path11 = __toESM(require("path"));
3233
+ var path12 = __toESM(require("path"));
3109
3234
 
3110
3235
  // src/internal/collect/metadata-collector.ts
3111
3236
  var DETACHED_HEAD = "HEAD";
@@ -3138,23 +3263,23 @@ function normalizeRef(ref) {
3138
3263
 
3139
3264
  // src/internal/coverage-upload/git-inventory.ts
3140
3265
  var import_node_child_process5 = require("child_process");
3141
- var fs9 = __toESM(require("fs"));
3142
- var path13 = __toESM(require("path"));
3266
+ var fs10 = __toESM(require("fs"));
3267
+ var path14 = __toESM(require("path"));
3143
3268
 
3144
3269
  // src/internal/collect/blob-ids.ts
3145
3270
  var import_node_child_process4 = require("child_process");
3146
3271
  var crypto2 = __toESM(require("crypto"));
3147
- var fs8 = __toESM(require("fs"));
3148
- var path12 = __toESM(require("path"));
3272
+ var fs9 = __toESM(require("fs"));
3273
+ var path13 = __toESM(require("path"));
3149
3274
  function gitHashObjects(root, paths) {
3150
- return new Promise((resolve10) => {
3275
+ return new Promise((resolve11) => {
3151
3276
  const child = (0, import_node_child_process4.execFile)(
3152
3277
  "git",
3153
3278
  ["hash-object", "--stdin-paths"],
3154
3279
  { cwd: root, encoding: "utf8", timeout: 3e4, windowsHide: true },
3155
3280
  (error, stdout) => {
3156
3281
  const ids = error ? [] : stdout.split("\n").filter(Boolean);
3157
- resolve10(ids.length === paths.length ? ids : null);
3282
+ resolve11(ids.length === paths.length ? ids : null);
3158
3283
  }
3159
3284
  );
3160
3285
  child.stdin?.on("error", () => {
@@ -3168,12 +3293,12 @@ function gitHashObjects(root, paths) {
3168
3293
  var MAX_GIT_OUTPUT = 512 * 1024 * 1024;
3169
3294
  var GIT_BATCH = 500;
3170
3295
  function git(cwd, args) {
3171
- return new Promise((resolve10, reject) => {
3296
+ return new Promise((resolve11, reject) => {
3172
3297
  (0, import_node_child_process5.execFile)(
3173
3298
  "git",
3174
3299
  args,
3175
3300
  { cwd, encoding: "utf8", maxBuffer: MAX_GIT_OUTPUT, timeout: 12e4, windowsHide: true },
3176
- (error, stdout) => error ? reject(error) : resolve10(stdout)
3301
+ (error, stdout) => error ? reject(error) : resolve11(stdout)
3177
3302
  );
3178
3303
  });
3179
3304
  }
@@ -3212,7 +3337,7 @@ async function readInventory(root, warn = () => {
3212
3337
  const changed = [];
3213
3338
  for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
3214
3339
  if (!file || !inventory.has(file)) continue;
3215
- if (fs9.existsSync(path13.join(root, file))) changed.push(file);
3340
+ if (fs10.existsSync(path14.join(root, file))) changed.push(file);
3216
3341
  else inventory.delete(file);
3217
3342
  }
3218
3343
  const ids = await hashPaths(root, changed);
@@ -3231,8 +3356,8 @@ async function readInventory(root, warn = () => {
3231
3356
  }
3232
3357
 
3233
3358
  // src/internal/coverage-upload/report-files.ts
3234
- var fs10 = __toESM(require("fs"));
3235
- var path14 = __toESM(require("path"));
3359
+ var fs11 = __toESM(require("fs"));
3360
+ var path15 = __toESM(require("path"));
3236
3361
 
3237
3362
  // ../core/src/coverage.ts
3238
3363
  var MAX_LINE = 1e6;
@@ -3744,14 +3869,14 @@ function globSegmentRegExp(segment) {
3744
3869
  function expandPattern(pattern, cwd) {
3745
3870
  const normalized = pattern.replace(/\\/g, "/");
3746
3871
  if (!/[*?]/.test(normalized)) {
3747
- const file = path14.resolve(cwd, normalized);
3748
- 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] : [];
3749
3874
  }
3750
- const absolute = path14.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3875
+ const absolute = path15.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3751
3876
  const segments = normalized.split("/");
3752
3877
  const firstWild = segments.findIndex((s) => /[*?]/.test(s));
3753
3878
  const baseParts = segments.slice(0, firstWild);
3754
- const base = absolute ? baseParts.join("/") || "/" : path14.resolve(cwd, ...baseParts);
3879
+ const base = absolute ? baseParts.join("/") || "/" : path15.resolve(cwd, ...baseParts);
3755
3880
  const rest = segments.slice(firstWild).filter((s) => s !== "");
3756
3881
  const out = [];
3757
3882
  const walk = (dir, at) => {
@@ -3759,14 +3884,14 @@ function expandPattern(pattern, cwd) {
3759
3884
  const segment = rest[at];
3760
3885
  let entries;
3761
3886
  try {
3762
- entries = fs10.readdirSync(dir, { withFileTypes: true });
3887
+ entries = fs11.readdirSync(dir, { withFileTypes: true });
3763
3888
  } catch {
3764
3889
  return;
3765
3890
  }
3766
3891
  if (segment === "**") {
3767
3892
  walk(dir, at + 1);
3768
3893
  for (const entry of entries) {
3769
- 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);
3770
3895
  }
3771
3896
  return;
3772
3897
  }
@@ -3774,7 +3899,7 @@ function expandPattern(pattern, cwd) {
3774
3899
  const last = at === rest.length - 1;
3775
3900
  for (const entry of entries) {
3776
3901
  if (!match.test(entry.name)) continue;
3777
- const full = path14.join(dir, entry.name);
3902
+ const full = path15.join(dir, entry.name);
3778
3903
  if (last && entry.isFile()) out.push(full);
3779
3904
  else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
3780
3905
  walk(full, at + 1);
@@ -3787,7 +3912,7 @@ function expandPattern(pattern, cwd) {
3787
3912
  async function readReport(file, format) {
3788
3913
  let reader = format ? new CoverageReportReader(format) : null;
3789
3914
  let head = "";
3790
- 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 })) {
3791
3916
  if (!reader) {
3792
3917
  head += chunk;
3793
3918
  if (head.length < 4096) continue;
@@ -3809,18 +3934,18 @@ async function readReport(file, format) {
3809
3934
  return reader.end();
3810
3935
  }
3811
3936
  function reportBases(report, reportFile, options) {
3812
- if (options.root) return [path14.resolve(options.cwd, options.root)];
3813
- 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));
3814
3939
  const sources = report.sources.map(
3815
- (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)
3816
3941
  );
3817
- return [...sources, options.cwd, dir, path14.dirname(dir), options.repoRoot];
3942
+ return [...sources, options.cwd, dir, path15.dirname(dir), options.repoRoot];
3818
3943
  }
3819
3944
  function resolveReport(report, reportFile, options, into) {
3820
3945
  const bases = reportBases(report, reportFile, options);
3821
3946
  for (const file of report.files) {
3822
3947
  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)));
3948
+ const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs11.existsSync(path15.join(options.repoRoot, candidate)));
3824
3949
  if (!match) {
3825
3950
  into.unmatched.push(file.path);
3826
3951
  continue;
@@ -3860,9 +3985,9 @@ Suite:
3860
3985
  --kind <kind> unit (the default) or e2e
3861
3986
 
3862
3987
  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)
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)
3866
3991
  --branch <name> The branch (env PIWI_BRANCH, the CI's, else git's)
3867
3992
 
3868
3993
  Other:
@@ -3882,7 +4007,7 @@ var VALUE_OPTIONS = /* @__PURE__ */ new Set([
3882
4007
  "--api-key",
3883
4008
  "--branch"
3884
4009
  ]);
3885
- function parseCoverageUploadArgs(argv, env) {
4010
+ function parseCoverageUploadArgs(argv, env, dirs = []) {
3886
4011
  const values = /* @__PURE__ */ new Map();
3887
4012
  const patterns2 = [];
3888
4013
  const flags = /* @__PURE__ */ new Set();
@@ -3913,11 +4038,19 @@ function parseCoverageUploadArgs(argv, env) {
3913
4038
  if (format !== void 0 && format !== "cobertura" && format !== "lcov" && format !== "piwi") {
3914
4039
  throw new Error("--format is cobertura, lcov or piwi");
3915
4040
  }
3916
- 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 ?? "";
3917
4048
  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");
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)");
3921
4054
  return {
3922
4055
  patterns: patterns2,
3923
4056
  suite,
@@ -3926,7 +4059,7 @@ function parseCoverageUploadArgs(argv, env) {
3926
4059
  ...values.get("--root") ? { root: values.get("--root") } : {},
3927
4060
  project,
3928
4061
  serverUrl,
3929
- apiKey: values.get("--api-key") ?? env.PIWI_API_KEY ?? null,
4062
+ apiKey: connection?.apiKey ?? null,
3930
4063
  ...values.get("--branch") ? { branch: values.get("--branch") } : {},
3931
4064
  dryRun,
3932
4065
  json: flags.has("--json")
@@ -4045,7 +4178,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
4045
4178
  commit: checkout.commit,
4046
4179
  branch,
4047
4180
  suite: args.suite,
4048
- reports: reports.map((report) => path15.relative(cwd, report) || report),
4181
+ reports: reports.map((report) => path16.relative(cwd, report) || report),
4049
4182
  files: files.length,
4050
4183
  unmatched: resolved.unmatched.length,
4051
4184
  outOfScope: resolved.outOfScope,
@@ -4092,7 +4225,7 @@ ${USAGE4}`);
4092
4225
  }
4093
4226
  let args;
4094
4227
  try {
4095
- args = parseCoverageUploadArgs(rest, env);
4228
+ args = parseCoverageUploadArgs(rest, env, [cwd]);
4096
4229
  } catch (error) {
4097
4230
  console.error(`piwi coverage upload: ${errorMessage(error)}`);
4098
4231
  return EXIT_ERROR4;
@@ -4115,7 +4248,7 @@ ${USAGE4}`);
4115
4248
 
4116
4249
  // src/cli/flake.ts
4117
4250
  var import_node_child_process6 = require("child_process");
4118
- var fs11 = __toESM(require("fs"));
4251
+ var fs12 = __toESM(require("fs"));
4119
4252
  var os2 = __toESM(require("os"));
4120
4253
  var path17 = __toESM(require("path"));
4121
4254
 
@@ -4318,19 +4451,6 @@ function withinBudget(startedAt, now, budgetMs) {
4318
4451
  return now - startedAt < budgetMs;
4319
4452
  }
4320
4453
 
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
4454
  // src/cli/flake.ts
4335
4455
  var USAGE5 = `
4336
4456
  piwi flake \u2014 make a flaky test fail on demand, then prove the fix
@@ -4373,7 +4493,7 @@ Options:
4373
4493
  --json Print the results as JSON instead of text
4374
4494
  --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL, .env, the desktop app)
4375
4495
  --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)
4496
+ --project <name|id> The Piwi project, for a file:line test (env PIWI_PROJECT_NAME, .env)
4377
4497
 
4378
4498
  Exit codes: 0 an arm reproduced the failure (verify: the fix held),
4379
4499
  1 nothing reproduced (verify: it still fails, or too few runs),
@@ -4521,8 +4641,8 @@ function playwrightRunner(dir, log) {
4521
4641
  now: () => Date.now(),
4522
4642
  async runBatch(plan, arm, repeat, batch) {
4523
4643
  const planFile = path17.join(dir, `${arm.id}.plan.json`);
4524
- if (!fs11.existsSync(planFile)) {
4525
- fs11.writeFileSync(
4644
+ if (!fs12.existsSync(planFile)) {
4645
+ fs12.writeFileSync(
4526
4646
  planFile,
4527
4647
  JSON.stringify({
4528
4648
  version: FLAKE_PLAN_VERSION,
@@ -4534,14 +4654,14 @@ function playwrightRunner(dir, log) {
4534
4654
  );
4535
4655
  }
4536
4656
  const resultsFile = path17.join(dir, `${arm.id}.${batch}.jsonl`);
4537
- fs11.writeFileSync(resultsFile, "");
4657
+ fs12.writeFileSync(resultsFile, "");
4538
4658
  const env = {
4539
4659
  ...process.env,
4540
4660
  [PIWI_FLAKE_ENV.plan]: planFile,
4541
4661
  [PIWI_FLAKE_ENV.results]: resultsFile
4542
4662
  };
4543
4663
  delete env[PIWI_PROBE_ENV.flag];
4544
- fs11.appendFileSync(log, `
4664
+ fs12.appendFileSync(log, `
4545
4665
  \u2500\u2500 ${arm.id} batch ${batch} \u2500\u2500
4546
4666
  `);
4547
4667
  await spawnPlaywright(playwrightArgs(plan.test, arm, repeat), env, {
@@ -4554,7 +4674,7 @@ function playwrightRunner(dir, log) {
4554
4674
  }
4555
4675
  function readResults(file) {
4556
4676
  const lines = [];
4557
- for (const line of fs11.readFileSync(file, "utf8").split("\n")) {
4677
+ for (const line of fs12.readFileSync(file, "utf8").split("\n")) {
4558
4678
  if (!line.trim()) continue;
4559
4679
  try {
4560
4680
  lines.push(JSON.parse(line));
@@ -4718,7 +4838,7 @@ function printHeader(plan, commit, estimate, budgetMs) {
4718
4838
  async function loadPlan(args, commit) {
4719
4839
  if (args.planFile) {
4720
4840
  try {
4721
- const raw = JSON.parse(fs11.readFileSync(args.planFile, "utf8"));
4841
+ const raw = JSON.parse(fs12.readFileSync(args.planFile, "utf8"));
4722
4842
  const plan = "plan" in raw && raw.plan ? raw.plan : raw;
4723
4843
  if (plan.version !== 1 || !plan.control || !Array.isArray(plan.arms)) throw new Error("not a flake-lab plan");
4724
4844
  if (args.verify !== (plan.kind === "verify")) {
@@ -4733,7 +4853,8 @@ async function loadPlan(args, commit) {
4733
4853
  const connection = resolveCliConnection(
4734
4854
  { serverUrl: args.serverUrl, apiKey: args.apiKey, project: args.project },
4735
4855
  process.env,
4736
- [process.cwd()]
4856
+ [process.cwd()],
4857
+ args.verify ? "piwi flake verify" : "piwi flake"
4737
4858
  );
4738
4859
  if (!connection) {
4739
4860
  throw new FlakeError(
@@ -4812,7 +4933,7 @@ async function runFlake(argv, runner) {
4812
4933
  if (!args.json) {
4813
4934
  printHeader(plan, commit, estimate, args.budgetMs);
4814
4935
  }
4815
- const dir = fs11.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
4936
+ const dir = fs12.mkdtempSync(path17.join(os2.tmpdir(), "piwi-flake-"));
4816
4937
  const log = path17.join(dir, "playwright.log");
4817
4938
  let session;
4818
4939
  try {
@@ -4821,8 +4942,8 @@ async function runFlake(argv, runner) {
4821
4942
  });
4822
4943
  } catch (error) {
4823
4944
  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");
4945
+ if (fs12.existsSync(log)) {
4946
+ const tail = fs12.readFileSync(log, "utf8").trimEnd().split("\n").slice(-LOG_TAIL_LINES).join("\n");
4826
4947
  console.error(`
4827
4948
  Playwright's output (${log}):
4828
4949
  ${tail}`);
@@ -4895,7 +5016,7 @@ ${tail}`);
4895
5016
  verifyCommand: plan.kind === "reproduce" && verdict === "reproduced" ? verifyCommand : null,
4896
5017
  exitCode: code
4897
5018
  };
4898
- fs11.rmSync(dir, { recursive: true, force: true });
5019
+ fs12.rmSync(dir, { recursive: true, force: true });
4899
5020
  if (args.json) {
4900
5021
  console.log(JSON.stringify(report, null, 2));
4901
5022
  return code;
@@ -4910,7 +5031,7 @@ ${tail}`);
4910
5031
  }
4911
5032
 
4912
5033
  // src/cli/gate.ts
4913
- var fs12 = __toESM(require("fs"));
5034
+ var fs13 = __toESM(require("fs"));
4914
5035
 
4915
5036
  // ../core/src/gate.ts
4916
5037
  function formatGateResult(result) {
@@ -4956,8 +5077,8 @@ Where the run comes from (first match wins):
4956
5077
  (default) ./piwi-run.json, if it exists
4957
5078
 
4958
5079
  Connection:
4959
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
4960
- --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)
4961
5082
 
4962
5083
  Policy (at least one rule or warn-only flag is required):
4963
5084
  --require-tag <tags> Comma-separated; every test carrying the tag must pass
@@ -5006,16 +5127,22 @@ function readCount(argv, name) {
5006
5127
  }
5007
5128
  function readRunIdFromFile(path25) {
5008
5129
  try {
5009
- const parsed = JSON.parse(fs12.readFileSync(path25, "utf-8"));
5130
+ const parsed = JSON.parse(fs13.readFileSync(path25, "utf-8"));
5010
5131
  const runId = Number(parsed.runId);
5011
5132
  return Number.isFinite(runId) && runId > 0 ? runId : null;
5012
5133
  } catch {
5013
5134
  return null;
5014
5135
  }
5015
5136
  }
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");
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)");
5019
5146
  let runId = Number(readOption4(argv, "--run-id") ?? NaN);
5020
5147
  if (!Number.isFinite(runId) || runId <= 0) {
5021
5148
  const candidate = readOption4(argv, "--from-file") ?? env.PIWI_OUTPUT_FILE ?? "piwi-run.json";
@@ -5040,8 +5167,8 @@ function parseGateArgs(argv, env) {
5040
5167
  maxNewLeaks: readCount(argv, "--max-new-leaks")
5041
5168
  };
5042
5169
  return {
5043
- serverUrl,
5044
- apiKey: readOption4(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
5170
+ serverUrl: connection.serverUrl,
5171
+ apiKey: connection.apiKey,
5045
5172
  runId,
5046
5173
  policy,
5047
5174
  maxUncoveredChanges: readCount(argv, "--max-uncovered-changes"),
@@ -5066,14 +5193,14 @@ async function requestGate(args) {
5066
5193
  }
5067
5194
  return await res.json();
5068
5195
  }
5069
- async function runGate(argv, env = process.env) {
5196
+ async function runGate(argv, env = process.env, cwd = process.cwd()) {
5070
5197
  if (argv.includes("-h") || argv.includes("--help")) {
5071
5198
  console.log(USAGE6);
5072
5199
  return EXIT_OK5;
5073
5200
  }
5074
5201
  let args;
5075
5202
  try {
5076
- args = parseGateArgs(argv, env);
5203
+ args = parseGateArgs(argv, env, [cwd]);
5077
5204
  } catch (e) {
5078
5205
  console.error(`piwi gate: ${e.message}
5079
5206
  `);
@@ -5097,7 +5224,7 @@ async function runGate(argv, env = process.env) {
5097
5224
  }
5098
5225
 
5099
5226
  // src/cli/init.ts
5100
- var fs15 = __toESM(require("fs"));
5227
+ var fs16 = __toESM(require("fs"));
5101
5228
  var path20 = __toESM(require("path"));
5102
5229
 
5103
5230
  // src/cli/report.ts
@@ -5115,9 +5242,9 @@ function formatStep(result) {
5115
5242
  }
5116
5243
 
5117
5244
  // src/cli/detect.ts
5118
- var fs13 = __toESM(require("fs"));
5245
+ var fs14 = __toESM(require("fs"));
5119
5246
  var path18 = __toESM(require("path"));
5120
- var CONFIG_NAMES = [
5247
+ var CONFIG_NAMES2 = [
5121
5248
  "playwright.config.ts",
5122
5249
  "playwright.config.mts",
5123
5250
  "playwright.config.cts",
@@ -5136,7 +5263,7 @@ var REPORTER_PACKAGE = "@piwitests/reporter";
5136
5263
  function readPackageJson(root) {
5137
5264
  const pkgPath = path18.join(root, "package.json");
5138
5265
  try {
5139
- return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
5266
+ return { pkg: JSON.parse(fs14.readFileSync(pkgPath, "utf-8")), pkgPath };
5140
5267
  } catch {
5141
5268
  return { pkg: null, pkgPath: null };
5142
5269
  }
@@ -5145,14 +5272,14 @@ function detectPackageManager(root, pkg) {
5145
5272
  const declared = pkg?.packageManager?.split("@")[0];
5146
5273
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
5147
5274
  for (const [file, manager] of LOCKFILES) {
5148
- if (fs13.existsSync(path18.join(root, file))) return manager;
5275
+ if (fs14.existsSync(path18.join(root, file))) return manager;
5149
5276
  }
5150
5277
  return "npm";
5151
5278
  }
5152
5279
  function findConfig(root) {
5153
- for (const name of CONFIG_NAMES) {
5280
+ for (const name of CONFIG_NAMES2) {
5154
5281
  const candidate = path18.join(root, name);
5155
- if (fs13.existsSync(candidate)) return candidate;
5282
+ if (fs14.existsSync(candidate)) return candidate;
5156
5283
  }
5157
5284
  return null;
5158
5285
  }
@@ -5313,7 +5440,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
5313
5440
  }
5314
5441
 
5315
5442
  // src/cli/skills.ts
5316
- var fs14 = __toESM(require("fs"));
5443
+ var fs15 = __toESM(require("fs"));
5317
5444
  var path19 = __toESM(require("path"));
5318
5445
 
5319
5446
  // ../core/src/skill-stamp.ts
@@ -5381,7 +5508,7 @@ var DEFAULT_SKILLS_DIR = path19.join(".claude", "skills");
5381
5508
  function findTemplatesDir(fromDir) {
5382
5509
  let dir = fromDir;
5383
5510
  for (let i = 0; i < 6; i++) {
5384
- 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");
5385
5512
  const parent = path19.dirname(dir);
5386
5513
  if (parent === dir) break;
5387
5514
  dir = parent;
@@ -5400,7 +5527,7 @@ function readFrontMatter(markdown) {
5400
5527
  }
5401
5528
  function readTemplatesVersion(templatesDir) {
5402
5529
  try {
5403
- 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"));
5404
5531
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
5405
5532
  } catch {
5406
5533
  return "0.0.0";
@@ -5413,8 +5540,8 @@ function listSkills(templatesDir, slugs = ALL_SKILLS) {
5413
5540
  const infos = [];
5414
5541
  for (const slug of slugs) {
5415
5542
  const file = templatePath(templatesDir, slug);
5416
- if (!fs14.existsSync(file)) continue;
5417
- const front = readFrontMatter(fs14.readFileSync(file, "utf-8"));
5543
+ if (!fs15.existsSync(file)) continue;
5544
+ const front = readFrontMatter(fs15.readFileSync(file, "utf-8"));
5418
5545
  infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
5419
5546
  }
5420
5547
  return infos;
@@ -5426,16 +5553,16 @@ function installSkills(opts) {
5426
5553
  const source = templatePath(opts.templatesDir, slug);
5427
5554
  const relDest = path19.join(opts.skillsDir, slug, "SKILL.md");
5428
5555
  const dest = path19.join(opts.root, relDest);
5429
- if (!fs14.existsSync(source)) {
5556
+ if (!fs15.existsSync(source)) {
5430
5557
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
5431
5558
  continue;
5432
5559
  }
5433
5560
  const version = opts.version ?? readTemplatesVersion(opts.templatesDir);
5434
- const contents = stampSkill(fs14.readFileSync(source, "utf-8"), version);
5435
- const exists = fs14.existsSync(dest);
5561
+ const contents = stampSkill(fs15.readFileSync(source, "utf-8"), version);
5562
+ const exists = fs15.existsSync(dest);
5436
5563
  let detail = "installed skill";
5437
5564
  if (exists) {
5438
- const installed = fs14.readFileSync(dest, "utf-8");
5565
+ const installed = fs15.readFileSync(dest, "utf-8");
5439
5566
  const state = classifyInstalledSkill(installed, contents);
5440
5567
  const from = readSkillStamp(installed).version;
5441
5568
  if (state === "current") {
@@ -5457,8 +5584,8 @@ function installSkills(opts) {
5457
5584
  }
5458
5585
  }
5459
5586
  if (!opts.dryRun) {
5460
- fs14.mkdirSync(path19.dirname(dest), { recursive: true });
5461
- fs14.writeFileSync(dest, contents);
5587
+ fs15.mkdirSync(path19.dirname(dest), { recursive: true });
5588
+ fs15.writeFileSync(dest, contents);
5462
5589
  }
5463
5590
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
5464
5591
  }
@@ -5552,7 +5679,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
5552
5679
 
5553
5680
  // src/cli/init.ts
5554
5681
  var DEFAULT_SERVER_URL = "http://localhost:3000";
5555
- var DESKTOP_APP_MODES = ["prefer", "fallback", "off"];
5682
+ var DESKTOP_APP_MODES2 = ["prefer", "fallback", "off"];
5556
5683
  var USAGE8 = `
5557
5684
  piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
5558
5685
 
@@ -5628,7 +5755,7 @@ function parseInitArgs(argv, env, cwd) {
5628
5755
  }
5629
5756
  function readFileOr(file, fallback) {
5630
5757
  try {
5631
- return fs15.readFileSync(file, "utf-8");
5758
+ return fs16.readFileSync(file, "utf-8");
5632
5759
  } catch {
5633
5760
  return fallback;
5634
5761
  }
@@ -5682,7 +5809,7 @@ function stepConfig(project, opts) {
5682
5809
  projectName: opts.projectName,
5683
5810
  desktopApp
5684
5811
  });
5685
- 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);
5686
5813
  if (edit.status === "already" && desktopApp && !new RegExp(`\\bdesktopApp\\s*:\\s*['"\`]${desktopApp}['"\`]`).test(source)) {
5687
5814
  return {
5688
5815
  step,
@@ -5697,7 +5824,7 @@ function stepFixtures(project, opts) {
5697
5824
  const step = "fixtures";
5698
5825
  const rel = path20.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5699
5826
  const dest = path20.join(project.root, rel);
5700
- if (fs15.existsSync(dest)) {
5827
+ if (fs16.existsSync(dest)) {
5701
5828
  const current = readFileOr(dest, "");
5702
5829
  if (/piwiFixtures/.test(current))
5703
5830
  return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
@@ -5709,8 +5836,8 @@ function stepFixtures(project, opts) {
5709
5836
  };
5710
5837
  }
5711
5838
  if (!opts.dryRun) {
5712
- fs15.mkdirSync(path20.dirname(dest), { recursive: true });
5713
- fs15.writeFileSync(dest, fixturesContents());
5839
+ fs16.mkdirSync(path20.dirname(dest), { recursive: true });
5840
+ fs16.writeFileSync(dest, fixturesContents());
5714
5841
  }
5715
5842
  return {
5716
5843
  step,
@@ -5726,7 +5853,7 @@ function stepEnv(project, opts) {
5726
5853
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
5727
5854
  ["PIWI_API_KEY", ""]
5728
5855
  ]);
5729
- if (example.added.length && !opts.dryRun) fs15.writeFileSync(examplePath, example.text);
5856
+ if (example.added.length && !opts.dryRun) fs16.writeFileSync(examplePath, example.text);
5730
5857
  results.push({
5731
5858
  step: "env",
5732
5859
  file: ".env.example",
@@ -5738,7 +5865,7 @@ function stepEnv(project, opts) {
5738
5865
  const entries = [["PIWI_API_KEY", opts.apiKey]];
5739
5866
  if (opts.serverUrl) entries.unshift(["PIWI_DASHBOARD_URL", opts.serverUrl]);
5740
5867
  const env = upsertEnvKeys(readFileOr(envPath, ""), entries);
5741
- if (env.added.length && !opts.dryRun) fs15.writeFileSync(envPath, env.text);
5868
+ if (env.added.length && !opts.dryRun) fs16.writeFileSync(envPath, env.text);
5742
5869
  results.push({
5743
5870
  step: "env",
5744
5871
  file: ".env",
@@ -5747,7 +5874,7 @@ function stepEnv(project, opts) {
5747
5874
  });
5748
5875
  const gitignorePath = path20.join(project.root, ".gitignore");
5749
5876
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
5750
- if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
5877
+ if (gitignore.added && !opts.dryRun) fs16.writeFileSync(gitignorePath, gitignore.text);
5751
5878
  results.push({
5752
5879
  step: "gitignore",
5753
5880
  file: ".gitignore",
@@ -5790,8 +5917,8 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
5790
5917
  return 0;
5791
5918
  }
5792
5919
  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}")`);
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}")`);
5795
5922
  return 1;
5796
5923
  }
5797
5924
  const project = detectProject(opts.root);
@@ -5848,7 +5975,7 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
5848
5975
  }
5849
5976
 
5850
5977
  // src/cli/select.ts
5851
- var fs17 = __toESM(require("fs"));
5978
+ var fs18 = __toESM(require("fs"));
5852
5979
  var path22 = __toESM(require("path"));
5853
5980
  var import_node_child_process7 = require("child_process");
5854
5981
 
@@ -6694,7 +6821,7 @@ function diffImpactInput(files) {
6694
6821
  }
6695
6822
 
6696
6823
  // src/cli/add-reporter.ts
6697
- var fs16 = __toESM(require("fs"));
6824
+ var fs17 = __toESM(require("fs"));
6698
6825
  var path21 = __toESM(require("path"));
6699
6826
  var import_node_module2 = require("module");
6700
6827
  var REPORTER_PACKAGE2 = "@piwitests/reporter";
@@ -6738,7 +6865,7 @@ function resolveConfigPath(cwd, playwrightArgs2) {
6738
6865
  function readConfigSource(configPath) {
6739
6866
  if (!configPath) return null;
6740
6867
  try {
6741
- return fs16.readFileSync(configPath, "utf-8");
6868
+ return fs17.readFileSync(configPath, "utf-8");
6742
6869
  } catch {
6743
6870
  return null;
6744
6871
  }
@@ -6773,9 +6900,9 @@ Usage:
6773
6900
  npx @piwitests/reporter run impact --base <ref> run the tests your diff impacts
6774
6901
 
6775
6902
  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)
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)
6779
6906
 
6780
6907
  Selection:
6781
6908
  --format <fmt> args (file:line, default) | grep | files | json
@@ -6832,12 +6959,22 @@ function parseDuration2(raw) {
6832
6959
  const ms = Math.round(value * factor);
6833
6960
  return ms > 0 ? ms : null;
6834
6961
  }
6835
- function parseSelectArgs(argv, env) {
6962
+ function parseSelectArgs(argv, env, dirs = [], command = "piwi select") {
6836
6963
  const dashIndex = argv.indexOf("--");
6837
6964
  const own = dashIndex === -1 ? argv : argv.slice(0, dashIndex);
6838
6965
  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");
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)");
6841
6978
  const key = findKey(own);
6842
6979
  if (!key) throw new Error("No selection key \u2014 usage: piwi select <key>");
6843
6980
  const budgetRaw = readOption7(own, "--budget");
@@ -6851,9 +6988,9 @@ function parseSelectArgs(argv, env) {
6851
6988
  throw new Error(`--shard expects an "i/n" spec like 2/4, got "${shard}"`);
6852
6989
  }
6853
6990
  return {
6854
- serverUrl,
6855
- apiKey: readOption7(own, "--api-key") ?? env.PIWI_API_KEY ?? null,
6856
- project: readOption7(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "",
6991
+ serverUrl: connection.serverUrl,
6992
+ apiKey: connection.apiKey,
6993
+ project: connection.project,
6857
6994
  key,
6858
6995
  format: readOption7(own, "--format") ?? "args",
6859
6996
  budgetMs,
@@ -6872,7 +7009,7 @@ function cacheKey(args) {
6872
7009
  }
6873
7010
  function readCache(args) {
6874
7011
  try {
6875
- const store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
7012
+ const store = JSON.parse(fs18.readFileSync(CACHE_FILE, "utf-8"));
6876
7013
  return store[cacheKey(args)] ?? null;
6877
7014
  } catch {
6878
7015
  return null;
@@ -6882,12 +7019,12 @@ function writeCache(args, resolution) {
6882
7019
  try {
6883
7020
  let store = {};
6884
7021
  try {
6885
- store = JSON.parse(fs17.readFileSync(CACHE_FILE, "utf-8"));
7022
+ store = JSON.parse(fs18.readFileSync(CACHE_FILE, "utf-8"));
6886
7023
  } catch {
6887
7024
  }
6888
7025
  store[cacheKey(args)] = resolution;
6889
- fs17.mkdirSync(path22.dirname(CACHE_FILE), { recursive: true });
6890
- 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));
6891
7028
  } catch {
6892
7029
  }
6893
7030
  }
@@ -6979,7 +7116,7 @@ async function runSelect(argv, env = process.env) {
6979
7116
  }
6980
7117
  let args;
6981
7118
  try {
6982
- args = parseSelectArgs(argv, env);
7119
+ args = parseSelectArgs(argv, env, [process.cwd()]);
6983
7120
  } catch (e) {
6984
7121
  console.error(`piwi select: ${e.message}
6985
7122
  `);
@@ -7070,7 +7207,7 @@ async function runRun(argv, env = process.env) {
7070
7207
  }
7071
7208
  let args;
7072
7209
  try {
7073
- args = parseSelectArgs(argv, env);
7210
+ args = parseSelectArgs(argv, env, [process.cwd()], "piwi run");
7074
7211
  } catch (e) {
7075
7212
  console.error(`piwi run: ${e.message}
7076
7213
  `);
@@ -7120,7 +7257,7 @@ async function runRun(argv, env = process.env) {
7120
7257
  }
7121
7258
 
7122
7259
  // src/cli/preflight.ts
7123
- var fs18 = __toESM(require("fs"));
7260
+ var fs19 = __toESM(require("fs"));
7124
7261
  var path23 = __toESM(require("path"));
7125
7262
  var import_node_child_process8 = require("child_process");
7126
7263
 
@@ -7186,8 +7323,8 @@ Actions:
7186
7323
 
7187
7324
  Connection:
7188
7325
  --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)
7326
+ --api-key <key> API key (env PIWI_API_KEY, .env)
7327
+ --project <name|id> Project (env PIWI_PROJECT_NAME, .env)
7191
7328
 
7192
7329
  Output:
7193
7330
  --json Print the breaks, the edits and the impact as JSON
@@ -7244,16 +7381,6 @@ function parsePreflightArgs(argv) {
7244
7381
  extra: dashIndex === -1 ? [] : argv.slice(dashIndex + 1)
7245
7382
  };
7246
7383
  }
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
7384
  function git3(cwd, args) {
7258
7385
  return (0, import_node_child_process8.execFileSync)("git", args, {
7259
7386
  cwd,
@@ -7271,7 +7398,12 @@ function tryGit(cwd, args) {
7271
7398
  }
7272
7399
  var TEST_FILE = /(?:^|\/)[^/]+\.(?:spec|test)\.[cm]?[jt]sx?$/;
7273
7400
  function resolveConnection(args, env, dirs) {
7274
- 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
+ );
7275
7407
  }
7276
7408
  function translationLookup(repoRoot, base, locale) {
7277
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));
@@ -7298,7 +7430,7 @@ function prefersLocale(file, locale) {
7298
7430
  }
7299
7431
  function readText(file) {
7300
7432
  try {
7301
- return fs18.readFileSync(file, "utf-8");
7433
+ return fs19.readFileSync(file, "utf-8");
7302
7434
  } catch {
7303
7435
  return void 0;
7304
7436
  }
@@ -7309,7 +7441,7 @@ function cacheKey2(connection, branch) {
7309
7441
  }
7310
7442
  function readCache2(root, key) {
7311
7443
  try {
7312
- 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"));
7313
7445
  return store[key] ?? null;
7314
7446
  } catch {
7315
7447
  return null;
@@ -7320,12 +7452,12 @@ function writeCache2(root, key, index) {
7320
7452
  try {
7321
7453
  let store = {};
7322
7454
  try {
7323
- store = JSON.parse(fs18.readFileSync(file, "utf-8"));
7455
+ store = JSON.parse(fs19.readFileSync(file, "utf-8"));
7324
7456
  } catch {
7325
7457
  }
7326
7458
  store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
7327
- fs18.mkdirSync(path23.dirname(file), { recursive: true });
7328
- fs18.writeFileSync(file, JSON.stringify(store));
7459
+ fs19.mkdirSync(path23.dirname(file), { recursive: true });
7460
+ fs19.writeFileSync(file, JSON.stringify(store));
7329
7461
  } catch {
7330
7462
  }
7331
7463
  }
@@ -7384,7 +7516,7 @@ function applyEdits(breaks, replacementsOf) {
7384
7516
  }
7385
7517
  const edited = [];
7386
7518
  for (const [file, edits] of byFile) {
7387
- const text = fs18.readFileSync(file, "utf-8");
7519
+ const text = fs19.readFileSync(file, "utf-8");
7388
7520
  const eol = text.includes("\r\n") ? "\r\n" : "\n";
7389
7521
  const lines = text.split(/\r?\n/);
7390
7522
  for (const e of edits) {
@@ -7394,7 +7526,7 @@ function applyEdits(breaks, replacementsOf) {
7394
7526
  }
7395
7527
  const next = lines.join(eol);
7396
7528
  if (next !== text) {
7397
- fs18.writeFileSync(file, next);
7529
+ fs19.writeFileSync(file, next);
7398
7530
  edited.push(file);
7399
7531
  }
7400
7532
  }
@@ -7505,7 +7637,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7505
7637
  console.error("piwi preflight: not inside a git repository");
7506
7638
  return EXIT_ERROR8;
7507
7639
  }
7508
- const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7640
+ const testRoot = path23.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd, repoRoot) ?? cwd);
7509
7641
  let files;
7510
7642
  try {
7511
7643
  files = parseUnifiedDiff(
@@ -7631,7 +7763,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7631
7763
  }
7632
7764
 
7633
7765
  // src/cli/probe.ts
7634
- var fs19 = __toESM(require("fs"));
7766
+ var fs20 = __toESM(require("fs"));
7635
7767
  var os3 = __toESM(require("os"));
7636
7768
  var path24 = __toESM(require("path"));
7637
7769
  var EXIT_OK8 = 0;
@@ -7643,9 +7775,9 @@ Usage:
7643
7775
  npx @piwitests/reporter probe [options] [-- <playwright args>]
7644
7776
 
7645
7777
  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)
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)
7649
7781
  --budget <n> Max pairs to probe this run (default the server's)
7650
7782
 
7651
7783
  Everything after \`--\` is passed to \`playwright test\`.
@@ -7666,13 +7798,21 @@ async function runProbe(argv) {
7666
7798
  return EXIT_OK8;
7667
7799
  }
7668
7800
  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");
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)");
7672
7813
  return EXIT_ERROR9;
7673
7814
  }
7674
- const apiKey = readOption9(own, "--api-key") ?? env.PIWI_API_KEY ?? null;
7675
- const project = readOption9(own, "--project") ?? env.PIWI_PROJECT_NAME ?? "";
7815
+ const { serverUrl, apiKey, project } = connection;
7676
7816
  const budget = readOption9(own, "--budget");
7677
7817
  let projectId;
7678
7818
  try {
@@ -7697,12 +7837,12 @@ async function runProbe(argv) {
7697
7837
  return EXIT_OK8;
7698
7838
  }
7699
7839
  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-"));
7840
+ const dir = fs20.mkdtempSync(path24.join(os3.tmpdir(), "piwi-probe-"));
7701
7841
  const planFile = path24.join(dir, "plan.json");
7702
7842
  const resultsFile = path24.join(dir, "results.jsonl");
7703
7843
  const runOutputFile = path24.join(dir, "run.json");
7704
- fs19.writeFileSync(planFile, JSON.stringify(plan));
7705
- fs19.writeFileSync(resultsFile, "");
7844
+ fs20.writeFileSync(planFile, JSON.stringify(plan));
7845
+ fs20.writeFileSync(resultsFile, "");
7706
7846
  const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");
7707
7847
  const pwArgs = [...playwrightArgs2];
7708
7848
  if (grep && !playwrightArgs2.includes("--grep") && !playwrightArgs2.includes("-g")) pwArgs.push("--grep", grep);
@@ -7716,7 +7856,7 @@ async function runProbe(argv) {
7716
7856
  [PIWI_ENV_KEYS.outputFile]: runOutputFile
7717
7857
  };
7718
7858
  await spawnPlaywright(pwArgs, childEnv, { command: "piwi probe" });
7719
- const lines = fs19.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
7859
+ const lines = fs20.readFileSync(resultsFile, "utf8").split("\n").filter(Boolean);
7720
7860
  const results = [];
7721
7861
  for (const line of lines) {
7722
7862
  try {
@@ -7730,7 +7870,7 @@ async function runProbe(argv) {
7730
7870
  }
7731
7871
  let runId = null;
7732
7872
  try {
7733
- const out = JSON.parse(fs19.readFileSync(runOutputFile, "utf8"));
7873
+ const out = JSON.parse(fs20.readFileSync(runOutputFile, "utf8"));
7734
7874
  if (typeof out.runId === "number") runId = out.runId;
7735
7875
  } catch {
7736
7876
  }
@@ -7755,7 +7895,7 @@ async function runProbe(argv) {
7755
7895
  }
7756
7896
 
7757
7897
  // src/cli/quality-report.ts
7758
- var fs20 = __toESM(require("fs"));
7898
+ var fs21 = __toESM(require("fs"));
7759
7899
  var EXIT_OK9 = 0;
7760
7900
  var EXIT_VERDICT = 1;
7761
7901
  var EXIT_ERROR10 = 2;
@@ -7768,8 +7908,8 @@ Usage:
7768
7908
  npx @piwitests/reporter report [options]
7769
7909
 
7770
7910
  Connection:
7771
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
7772
- --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)
7773
7913
 
7774
7914
  What to report:
7775
7915
  --project <names|ids> Comma-separated projects (default: every project you can see)
@@ -7801,9 +7941,15 @@ function normalizePeriod(raw) {
7801
7941
  const days = raw.trim().match(/^(\d+)d$/);
7802
7942
  return days ? `last-${days[1]}d` : raw.trim();
7803
7943
  }
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");
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)");
7807
7953
  const format = (readOption10(argv, "--format") ?? "md").toLowerCase();
7808
7954
  if (!FORMATS.includes(format)) {
7809
7955
  throw new Error(`--format must be one of ${FORMATS.join(", ")}, got "${format}"`);
@@ -7823,8 +7969,8 @@ function parseReportArgs(argv, env) {
7823
7969
  const output = readOption10(argv, "--output") ?? null;
7824
7970
  if (format === "pdf" && !output) throw new Error("A PDF needs --output <file>");
7825
7971
  return {
7826
- serverUrl,
7827
- apiKey: readOption10(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
7972
+ serverUrl: connection.serverUrl,
7973
+ apiKey: connection.apiKey,
7828
7974
  projects: (readOption10(argv, "--project") ?? "").split(",").map((p) => p.trim()).filter(Boolean),
7829
7975
  period: normalizePeriod(readOption10(argv, "--period") ?? "30d"),
7830
7976
  compare: readOption10(argv, "--compare") ?? null,
@@ -7868,14 +8014,14 @@ async function runQualityReport(argv) {
7868
8014
  return EXIT_OK9;
7869
8015
  }
7870
8016
  try {
7871
- const args = parseReportArgs(argv, process.env);
8017
+ const args = parseReportArgs(argv, process.env, [process.cwd()]);
7872
8018
  const projectIds = [];
7873
8019
  for (const project of args.projects) {
7874
8020
  projectIds.push(await resolveProjectId({ serverUrl: args.serverUrl, apiKey: args.apiKey, project, key: "" }));
7875
8021
  }
7876
8022
  const res = await fetchReport(args, reportQuery(args, projectIds, args.format));
7877
8023
  const bytes = new Uint8Array(await res.arrayBuffer());
7878
- if (args.output) fs20.writeFileSync(args.output, bytes);
8024
+ if (args.output) fs21.writeFileSync(args.output, bytes);
7879
8025
  else process.stdout.write(bytes);
7880
8026
  if (!args.failOn) return EXIT_OK9;
7881
8027
  const bundle = args.format === "json" ? JSON.parse(new TextDecoder().decode(bytes)) : await (await fetchReport(args, reportQuery(args, projectIds, "json"))).json();