@piwitests/reporter 0.55.0 → 0.56.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
@@ -24,8 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/cli/ai.ts
27
- var path5 = __toESM(require("path"));
28
- var import_node_child_process = require("child_process");
27
+ var path6 = __toESM(require("path"));
29
28
 
30
29
  // src/internal/ai/check.ts
31
30
  var fs2 = __toESM(require("fs"));
@@ -38,8 +37,8 @@ var path2 = __toESM(require("path"));
38
37
  // src/internal/capture/locator-healing.ts
39
38
  var path = __toESM(require("path"));
40
39
 
41
- // ../core/src/locator-methods.ts
42
- var LOCATOR_BUILDER_METHODS = [
40
+ // ../core/src/locator-chain.ts
41
+ var LOCATING_METHODS = /* @__PURE__ */ new Set([
43
42
  "getByRole",
44
43
  "getByTestId",
45
44
  "getByText",
@@ -47,677 +46,565 @@ var LOCATOR_BUILDER_METHODS = [
47
46
  "getByPlaceholder",
48
47
  "getByAltText",
49
48
  "getByTitle",
50
- "locator"
51
- ];
52
-
53
- // ../core/src/locator-generation.ts
54
- function classifyCssStability(className) {
55
- if (/[a-f0-9]{8,}/i.test(className)) return 10;
56
- if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
57
- if (/^(bg|text|border|shadow|opacity|font|w-|h-|m[tblrxy]?-|p[tblrxy]?-|flex|grid|gap|rounded|absolute|relative|fixed|sticky|block|inline|hidden|overflow|z-|top-|right-|bottom-|left-|inset-|justify-|items-|self-|content-|order-|col-|row-)/.test(
58
- className
59
- ))
60
- return 25;
61
- if (/__/.test(className) || /--/.test(className)) return 35;
62
- if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
63
- if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
64
- if (className.includes("_")) return 15;
65
- if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
66
- return 15;
67
- }
68
- function isAutoGenerated(value) {
69
- if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
70
- if (/^[a-f0-9]{8,}$/i.test(value)) return true;
71
- if (/^[a-z]+-\d{4,}$/.test(value)) return true;
72
- if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
73
- value
74
- )) {
75
- return true;
76
- }
77
- if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
78
- if (value.startsWith("ng-")) return true;
79
- if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
80
- if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
81
- if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
82
- return false;
83
- }
84
-
85
- // src/internal/capture/locator-healing.ts
86
- var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
87
- var ACTION_METHODS = [
88
- "click",
89
- "fill",
90
- "check",
91
- "uncheck",
92
- "selectOption",
93
- "dblclick",
94
- "tap",
95
- "hover",
96
- "press",
97
- "type",
98
- "pressSequentially",
99
- "clear",
100
- "setInputFiles",
101
- "dragTo",
102
- "focus",
103
- "blur",
104
- "scrollIntoViewIfNeeded",
105
- "dispatchEvent",
106
- "selectText",
107
- // Not an action, but a successful waitFor proves the element resolved — the
108
- // closest capture hook available for assertion-style usage of a locator.
109
- "waitFor"
110
- ];
111
- var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
112
-
113
- // src/internal/ai/artifact.ts
114
- var ARTIFACT_VERSION = 1;
115
- var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
116
- var ACTION_METHOD_SET = new Set(ACTION_METHODS);
117
- var POSTCONDITION_ASSERTS = /* @__PURE__ */ new Set([
49
+ "locator",
50
+ "frameLocator"
51
+ ]);
52
+ var NARROWING_METHODS = /* @__PURE__ */ new Set([
53
+ "filter",
54
+ "first",
55
+ "last",
56
+ "nth",
57
+ "and",
58
+ "or",
118
59
  "visible",
119
- "hidden",
120
- "attached",
121
- "url"
60
+ "contentFrame",
61
+ "owner"
122
62
  ]);
123
- function sortDeep(value) {
124
- if (Array.isArray(value)) return value.map(sortDeep);
125
- if (value !== null && typeof value === "object") {
126
- const out = {};
127
- for (const key of Object.keys(value).sort()) {
128
- const v = value[key];
129
- if (v !== void 0) out[key] = sortDeep(v);
130
- }
131
- return out;
63
+ var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
64
+ var LocatorParseError = class extends Error {
65
+ constructor(message) {
66
+ super(message);
67
+ this.name = "LocatorParseError";
132
68
  }
133
- return value;
134
- }
135
- function serializeEntry(entry) {
136
- return `${JSON.stringify(sortDeep(entry), null, 2)}
137
- `;
138
- }
139
- var ArtifactError = class extends Error {
140
69
  };
141
- function assert(condition, message) {
142
- if (!condition) throw new ArtifactError(message);
143
- }
144
- function validateStructuredLocator(value, where) {
145
- assert(value !== null && typeof value === "object", `${where}: locator must be an object`);
146
- const loc = value;
147
- assert(typeof loc.method === "string", `${where}: locator.method must be a string`);
148
- assert(LOCATOR_METHOD_SET.has(loc.method), `${where}: locator method "${String(loc.method)}" is not allowlisted`);
149
- assert(Array.isArray(loc.args), `${where}: locator.args must be an array`);
150
- if (loc.chain !== void 0) {
151
- assert(Array.isArray(loc.chain), `${where}: locator.chain must be an array`);
152
- loc.chain.forEach((child, i) => validateStructuredLocator(child, `${where}.chain[${i}]`));
70
+ var SIMPLE_ESCAPES = {
71
+ n: "\n",
72
+ t: " ",
73
+ r: "\r",
74
+ b: "\b",
75
+ f: "\f",
76
+ v: "\v",
77
+ "0": "\0"
78
+ };
79
+ var Parser = class {
80
+ constructor(src) {
81
+ this.src = src;
82
+ this.i = 0;
153
83
  }
154
- }
155
- function validateRunStep(value, where) {
156
- assert(value !== null && typeof value === "object", `${where}: step must be an object`);
157
- const step = value;
158
- validateStructuredLocator(step.locator, `${where}.locator`);
159
- assert(typeof step.action === "string", `${where}: action must be a string`);
160
- assert(ACTION_METHOD_SET.has(step.action), `${where}: action "${String(step.action)}" is not allowlisted`);
161
- if (step.value !== void 0) assert(typeof step.value === "string", `${where}: value must be a string`);
162
- if (step.optional !== void 0) assert(typeof step.optional === "boolean", `${where}: optional must be a boolean`);
163
- if (step.waitForResponse !== void 0) {
164
- assert(typeof step.waitForResponse === "string", `${where}: waitForResponse must be a string`);
84
+ parseTopLevel() {
85
+ const chain = this.parseChain();
86
+ this.skipSpace();
87
+ if (this.i < this.src.length) this.fail("unexpected text");
88
+ return chain;
165
89
  }
166
- }
167
- function validatePostcondition(value) {
168
- assert(value !== null && typeof value === "object", "postcondition must be an object");
169
- const post = value;
170
- assert(
171
- POSTCONDITION_ASSERTS.has(post.assert),
172
- `postcondition.assert "${String(post.assert)}" is not supported`
173
- );
174
- if (post.assert === "url") {
175
- assert(typeof post.url === "string", "postcondition.url must be a string for a url assert");
176
- } else {
177
- validateStructuredLocator(post.locator, "postcondition.locator");
90
+ fail(message) {
91
+ throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
178
92
  }
179
- }
180
- function parseEntry(text) {
181
- let raw;
182
- try {
183
- raw = JSON.parse(text);
184
- } catch (error) {
185
- throw new ArtifactError(`entry is not valid JSON: ${error.message}`);
93
+ skipSpace() {
94
+ while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
186
95
  }
187
- assert(raw !== null && typeof raw === "object", "entry must be an object");
188
- const obj = raw;
189
- assert(obj.version === ARTIFACT_VERSION, `unsupported entry version ${String(obj.version)}`);
190
- assert(typeof obj.template === "string", "entry.template must be a string");
191
- if (obj.kind === "locator") {
192
- validateStructuredLocator(obj.locator, "entry.locator");
193
- return obj;
96
+ peek() {
97
+ return this.src[this.i];
194
98
  }
195
- if (obj.kind === "run") {
196
- assert(Array.isArray(obj.steps), "entry.steps must be an array");
197
- obj.steps.forEach((step, i) => validateRunStep(step, `entry.steps[${i}]`));
198
- validatePostcondition(obj.postcondition);
199
- return obj;
99
+ expect(ch) {
100
+ this.skipSpace();
101
+ if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
102
+ this.i++;
200
103
  }
201
- throw new ArtifactError(`unsupported entry kind "${String(obj.kind)}"`);
202
- }
203
-
204
- // src/internal/ai/keys.ts
205
- var crypto = __toESM(require("crypto"));
206
- var path3 = __toESM(require("path"));
207
- var DEFAULT_AI_DIR = "__piwi__";
208
- function normalizeTemplate(template) {
209
- return template.trim().replace(/\s+/g, " ").toLowerCase();
210
- }
211
-
212
- // src/internal/ai/check.ts
213
- function findEntryFiles(root, dir) {
214
- const out = [];
215
- const walk = (current) => {
216
- let items;
217
- try {
218
- items = fs2.readdirSync(current, { withFileTypes: true });
219
- } catch {
220
- return;
221
- }
222
- for (const item of items) {
223
- const full = path4.join(current, item.name);
224
- if (item.isDirectory()) {
225
- walk(full);
226
- } else if (item.isFile() && item.name.endsWith(".json")) {
227
- const specFileDir = path4.dirname(full);
228
- const dirName = path4.basename(path4.dirname(specFileDir));
229
- if (dirName !== dir) continue;
230
- const specDir = path4.dirname(path4.dirname(specFileDir));
231
- const specFile = path4.join(specDir, path4.basename(specFileDir));
232
- out.push({ file: full, specFile, testSlug: item.name.split(".")[0] });
233
- }
234
- }
235
- };
236
- walk(root);
237
- return out;
238
- }
239
- function readTextOr(file, fallback) {
240
- try {
241
- return fs2.readFileSync(file, "utf8");
242
- } catch {
243
- return fallback;
104
+ identifier() {
105
+ const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
106
+ if (!m) this.fail("expected a name");
107
+ this.i += m[0].length;
108
+ return m[0];
244
109
  }
245
- }
246
- function checkAiTree(root, opts = {}) {
247
- const dir = opts.dir ?? DEFAULT_AI_DIR;
248
- const findings = [];
249
- const rel = (file) => path4.relative(root, file).split(path4.sep).join("/");
250
- const sourceCache = /* @__PURE__ */ new Map();
251
- const duplicates = /* @__PURE__ */ new Map();
252
- for (const found of findEntryFiles(root, dir)) {
253
- const text = readTextOr(found.file, null);
254
- if (text === null) continue;
255
- let templateNormalized = null;
256
- try {
257
- const entry = parseEntry(text);
258
- templateNormalized = normalizeTemplate(entry.template);
259
- if (serializeEntry(entry) !== text) {
260
- findings.push({
261
- severity: "error",
262
- kind: "non-canonical",
263
- file: rel(found.file),
264
- message: "file is not in canonical form \u2014 re-author it with `piwi ai resolve --update-ai`"
265
- });
110
+ /**
111
+ * A chain of calls joined by `.`. With `lenient`, parsing stops before the
112
+ * first link that doesn't parse instead of failing, so a chain followed by
113
+ * other text (an error message) is read up to where it ends. `spans` receives
114
+ * where each top-level call starts and ends.
115
+ */
116
+ parseChain(opts = {}) {
117
+ this.skipSpace();
118
+ const calls = [];
119
+ for (; ; ) {
120
+ const before = this.i;
121
+ if (calls.length > 0) this.i++;
122
+ this.skipSpace();
123
+ const start = this.i;
124
+ try {
125
+ calls.push(this.parseCall(calls.length === 0));
126
+ } catch (error) {
127
+ if (!opts.lenient || calls.length === 0) throw error;
128
+ this.i = before;
129
+ break;
266
130
  }
267
- if (!sourceCache.has(found.specFile)) sourceCache.set(found.specFile, readTextOr(found.specFile, null));
268
- const source = sourceCache.get(found.specFile) ?? null;
269
- if (source === null) {
270
- findings.push({
271
- severity: "error",
272
- kind: "orphan",
273
- file: rel(found.file),
274
- message: `spec file is gone (${rel(found.specFile)}) \u2014 the entry is orphaned`
275
- });
276
- } else if (!source.includes(entry.template.trim())) {
277
- findings.push({
278
- severity: "error",
279
- kind: "orphan",
280
- file: rel(found.file),
281
- message: `template "${entry.template}" no longer appears in ${rel(found.specFile)} \u2014 the entry is orphaned`
282
- });
131
+ opts.spans?.push({ start, end: this.i });
132
+ const end = this.i;
133
+ this.skipSpace();
134
+ if (this.peek() !== ".") {
135
+ this.i = end;
136
+ break;
283
137
  }
284
- } catch (error) {
285
- findings.push({
286
- severity: "error",
287
- kind: "invalid",
288
- file: rel(found.file),
289
- message: `not a valid entry: ${error.message}`
290
- });
291
138
  }
292
- if (templateNormalized !== null) {
293
- const key = `${found.specFile}::${found.testSlug}::${templateNormalized}`;
294
- const list = duplicates.get(key) ?? [];
295
- list.push(rel(found.file));
296
- duplicates.set(key, list);
139
+ return { calls };
140
+ }
141
+ /** Where the parser stopped. */
142
+ get position() {
143
+ return this.i;
144
+ }
145
+ parseCall(first) {
146
+ this.skipSpace();
147
+ const method = this.identifier();
148
+ if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
149
+ if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
150
+ this.expect("(");
151
+ const args = [];
152
+ this.skipSpace();
153
+ if (this.peek() === ")") {
154
+ this.i++;
155
+ return { method, args };
156
+ }
157
+ for (; ; ) {
158
+ args.push(this.parseValue());
159
+ this.skipSpace();
160
+ if (this.peek() === ",") {
161
+ this.i++;
162
+ this.skipSpace();
163
+ if (this.peek() === ")") {
164
+ this.i++;
165
+ break;
166
+ }
167
+ continue;
168
+ }
169
+ this.expect(")");
170
+ break;
297
171
  }
172
+ return { method, args };
298
173
  }
299
- for (const files of duplicates.values()) {
300
- if (files.length < 2) continue;
301
- for (const file of files) {
302
- findings.push({
303
- severity: "warning",
304
- kind: "duplicate-template",
305
- file,
306
- message: `duplicate template within the same test \u2014 consider distinct phrasings (${files.join(", ")})`
307
- });
174
+ parseValue() {
175
+ this.skipSpace();
176
+ const ch = this.peek();
177
+ if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
178
+ if (ch === "/") return this.parseRegex();
179
+ if (ch === "{") return this.parseObject();
180
+ if (ch !== void 0 && /[-\d]/.test(ch)) {
181
+ const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
182
+ if (!m) this.fail("expected a number");
183
+ this.i += m[0].length;
184
+ return { type: "number", value: Number(m[0]) };
185
+ }
186
+ const rest = this.src.slice(this.i);
187
+ if (/^true\b/.test(rest)) {
188
+ this.i += 4;
189
+ return { type: "boolean", value: true };
190
+ }
191
+ if (/^false\b/.test(rest)) {
192
+ this.i += 5;
193
+ return { type: "boolean", value: false };
308
194
  }
195
+ if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
196
+ return this.fail("expected a value");
309
197
  }
310
- return findings.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
198
+ /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
199
+ parseString() {
200
+ const quote3 = this.src[this.i];
201
+ let out = "";
202
+ this.i++;
203
+ while (this.i < this.src.length) {
204
+ const c = this.src[this.i];
205
+ if (c === quote3) {
206
+ this.i++;
207
+ return out;
208
+ }
209
+ if (c !== "\\") {
210
+ out += c;
211
+ this.i++;
212
+ continue;
213
+ }
214
+ const next = this.src[this.i + 1];
215
+ if (next === void 0) break;
216
+ const simple = SIMPLE_ESCAPES[next];
217
+ if (simple !== void 0) {
218
+ out += simple;
219
+ this.i += 2;
220
+ continue;
221
+ }
222
+ const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
223
+ if (hex) {
224
+ out += String.fromCodePoint(parseInt(hex[1], 16));
225
+ this.i += 1 + hex[0].length;
226
+ continue;
227
+ }
228
+ if (next === "u" || next === "x") this.fail("malformed escape");
229
+ out += next;
230
+ this.i += 2;
231
+ }
232
+ return this.fail("unterminated string");
233
+ }
234
+ parseRegex() {
235
+ let j = this.i + 1;
236
+ let inClass = false;
237
+ while (j < this.src.length) {
238
+ const c = this.src[j];
239
+ if (c === "\\") {
240
+ j += 2;
241
+ continue;
242
+ }
243
+ if (c === "[") inClass = true;
244
+ else if (c === "]") inClass = false;
245
+ else if (c === "/" && !inClass) break;
246
+ j++;
247
+ }
248
+ if (j >= this.src.length) this.fail("unterminated regex");
249
+ const source = this.src.slice(this.i + 1, j);
250
+ const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
251
+ this.i = j + 1 + flags.length;
252
+ return { type: "regex", source, flags };
253
+ }
254
+ parseObject() {
255
+ this.expect("{");
256
+ const entries = [];
257
+ for (; ; ) {
258
+ this.skipSpace();
259
+ if (this.peek() === "}") {
260
+ this.i++;
261
+ break;
262
+ }
263
+ const ch = this.peek();
264
+ const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
265
+ this.expect(":");
266
+ entries.push([key, this.parseValue()]);
267
+ this.skipSpace();
268
+ if (this.peek() === ",") {
269
+ this.i++;
270
+ continue;
271
+ }
272
+ this.expect("}");
273
+ break;
274
+ }
275
+ return { type: "object", entries };
276
+ }
277
+ };
278
+ function parseLocatorChain(expr) {
279
+ const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
280
+ if (!trimmed) throw new LocatorParseError("empty expression");
281
+ return new Parser(trimmed).parseTopLevel();
311
282
  }
312
- function hasBlockingFindings(findings) {
313
- return findings.some((f) => f.severity === "error");
283
+ function tryParseLocatorChain(expr) {
284
+ try {
285
+ return parseLocatorChain(expr);
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+ function quoteJsString(value) {
291
+ const json = JSON.stringify(value).slice(1, -1);
292
+ return `'${json.replace(/\\"/g, '"').replace(/'/g, "\\'").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")}'`;
293
+ }
294
+ function renderArg(arg) {
295
+ switch (arg.type) {
296
+ case "string":
297
+ return quoteJsString(arg.value);
298
+ case "regex":
299
+ return `/${arg.source}/${arg.flags}`;
300
+ case "number":
301
+ case "boolean":
302
+ return String(arg.value);
303
+ case "object":
304
+ return arg.entries.length === 0 ? "{}" : `{ ${arg.entries.map(([k, v]) => `${/^[A-Za-z_$][\w$]*$/.test(k) ? k : quoteJsString(k)}: ${renderArg(v)}`).join(", ")} }`;
305
+ case "chain":
306
+ return renderLocatorChain(arg.chain);
307
+ }
308
+ }
309
+ function renderLocatorCall(call) {
310
+ return `${call.method}(${call.args.map(renderArg).join(", ")})`;
311
+ }
312
+ function renderLocatorChain(chain) {
313
+ return chain.calls.map(renderLocatorCall).join(".");
314
+ }
315
+ function canonicalLocator(expr) {
316
+ const chain = tryParseLocatorChain(expr);
317
+ return chain ? renderLocatorChain(chain) : null;
314
318
  }
315
319
 
316
- // src/cli/ai.ts
317
- var EXIT_OK = 0;
318
- var EXIT_ISSUES = 1;
319
- var EXIT_ERROR = 2;
320
- var USAGE = `
321
- Usage: piwi ai <command> [options]
322
-
323
- Commands:
324
- check Scan committed AI-step entries for orphans, non-canonical files and
325
- duplicate templates. Read-only; exits 1 when issues are found.
326
- resolve Author missing entries by running the suite in resolve mode against
327
- the configured authoring server (forces --workers=1).
328
- prune Delete orphaned/dormant entries.
320
+ // ../core/src/locator-fingerprint.ts
321
+ var FUNCTION_WORDS = new Set(
322
+ "the and for with from your you our this that les des une pour avec par sur aux dans vos votre nos notre".split(" ")
323
+ );
329
324
 
330
- Options (check):
331
- --dir <name> Entry directory name per spec (env PIWI_AI_DIR, default ${DEFAULT_AI_DIR})
332
- --cwd <path> Root to scan (default: current directory)
333
- --json Emit findings as JSON
334
-
335
- Options (resolve):
336
- --grep <re> Only author entries for matching tests
337
- --project <name> Author under one Playwright project (a resolve profile)
338
- --env K=V Extra env for the run (repeatable \u2014 flags/viewport profiles)
339
- --update-ai Re-author entries that already exist
340
- (needs PIWI_DASHBOARD_URL / PIWI_API_KEY for the server)
325
+ // ../core/src/locator-methods.ts
326
+ var LOCATOR_BUILDER_METHODS = [
327
+ "getByRole",
328
+ "getByTestId",
329
+ "getByText",
330
+ "getByLabel",
331
+ "getByPlaceholder",
332
+ "getByAltText",
333
+ "getByTitle",
334
+ "locator"
335
+ ];
341
336
 
342
- Exit codes:
343
- 0 clean (or --help)
344
- 1 hygiene issues found
345
- 2 bad arguments / command unavailable
346
- `.trim();
347
- function readOption(argv, name) {
348
- const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
349
- if (withEquals) return withEquals.slice(name.length + 1);
350
- const index = argv.indexOf(name);
351
- if (index === -1) return void 0;
352
- const value = argv[index + 1];
353
- return value && !value.startsWith("--") ? value : void 0;
337
+ // ../core/src/locator-generation.ts
338
+ function classifyCssStability(className) {
339
+ if (/[a-f0-9]{8,}/i.test(className)) return 10;
340
+ if (/^(?:css|sc|emotion|styled)-/i.test(className)) return 15;
341
+ if (/^(bg|text|border|shadow|opacity|font|w-|h-|m[tblrxy]?-|p[tblrxy]?-|flex|grid|gap|rounded|absolute|relative|fixed|sticky|block|inline|hidden|overflow|z-|top-|right-|bottom-|left-|inset-|justify-|items-|self-|content-|order-|col-|row-)/.test(
342
+ className
343
+ ))
344
+ return 25;
345
+ if (/__/.test(className) || /--/.test(className)) return 35;
346
+ if (/^[a-z]+(-[a-z]+)+$/.test(className)) return 40;
347
+ if (/^[a-z]+[A-Z][a-zA-Z]+$/.test(className)) return 40;
348
+ if (className.includes("_")) return 15;
349
+ if (/^[a-z]+-[a-z0-9]{5,}$/.test(className) && /[0-9]/.test(className)) return 15;
350
+ return 15;
354
351
  }
355
- function readAll(argv, name) {
356
- const values = [];
357
- for (let i = 0; i < argv.length; i++) {
358
- if (argv[i].startsWith(`${name}=`)) values.push(argv[i].slice(name.length + 1));
359
- else if (argv[i] === name && argv[i + 1] && !argv[i + 1].startsWith("--")) values.push(argv[++i]);
352
+ function isAutoGenerated(value) {
353
+ if (/^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/i.test(value)) return true;
354
+ if (/^[a-f0-9]{8,}$/i.test(value)) return true;
355
+ if (/^[a-z]+-\d{4,}$/.test(value)) return true;
356
+ if (/^(tab|panel|input|select|option|dialog|modal|popup|tooltip|menu|listbox|combobox|checkbox|radio|textarea|button|field|label|accordion|collapse|step|slider|toggle|switch|dropdown|overlay|portal|layer)-\d+$/i.test(
357
+ value
358
+ )) {
359
+ return true;
360
360
  }
361
- return values;
362
- }
363
- function formatFinding(finding) {
364
- const mark = finding.severity === "error" ? "\u2716" : "\u26A0";
365
- return `${mark} ${finding.file}
366
- ${finding.kind}: ${finding.message}`;
361
+ if (/^(emotion-|styled-|css-|sc-)/.test(value)) return true;
362
+ if (value.startsWith("ng-")) return true;
363
+ if (/^(radix-|headlessui-|mui-|mantine-|chakra-|reka-|bits-|downshift-|react-aria|el-id-)/i.test(value)) return true;
364
+ if (/^:r[0-9a-z]+:$/i.test(value) || /^«r[0-9a-z]+»$/i.test(value)) return true;
365
+ if (/(^|[-_:])v-\d+(-\d+)+$/.test(value) || /(^|[-_:])v-\d+(-\d+)+[-_:]/.test(value)) return true;
366
+ return false;
367
367
  }
368
- function runCheck(argv, env) {
369
- const root = path5.resolve(readOption(argv, "--cwd") ?? process.cwd());
370
- const dir = readOption(argv, "--dir") ?? env.PIWI_AI_DIR ?? DEFAULT_AI_DIR;
371
- let findings;
372
- try {
373
- findings = checkAiTree(root, { dir });
374
- } catch (error) {
375
- console.error(`piwi ai check: ${error.message}`);
376
- return EXIT_ERROR;
377
- }
378
- if (argv.includes("--json")) {
379
- console.log(JSON.stringify({ findings }, null, 2));
380
- } else if (findings.length === 0) {
381
- console.log("piwi ai check: no issues found.");
382
- } else {
383
- for (const finding of findings) console.log(formatFinding(finding));
384
- const errors = findings.filter((f) => f.severity === "error").length;
385
- const warnings = findings.length - errors;
386
- console.log(`
387
- ${errors} error(s), ${warnings} warning(s).`);
368
+
369
+ // src/internal/capture/locator-healing.ts
370
+ var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
371
+ var ACTION_METHODS = [
372
+ "click",
373
+ "fill",
374
+ "check",
375
+ "uncheck",
376
+ "selectOption",
377
+ "dblclick",
378
+ "tap",
379
+ "hover",
380
+ "press",
381
+ "type",
382
+ "pressSequentially",
383
+ "clear",
384
+ "setInputFiles",
385
+ "dragTo",
386
+ "focus",
387
+ "blur",
388
+ "scrollIntoViewIfNeeded",
389
+ "dispatchEvent",
390
+ "selectText",
391
+ // Not an action, but a successful waitFor proves the element resolved — the
392
+ // closest capture hook available for assertion-style usage of a locator.
393
+ "waitFor"
394
+ ];
395
+ var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
396
+
397
+ // src/internal/ai/artifact.ts
398
+ var ARTIFACT_VERSION = 1;
399
+ var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
400
+ var ACTION_METHOD_SET = new Set(ACTION_METHODS);
401
+ var POSTCONDITION_ASSERTS = /* @__PURE__ */ new Set([
402
+ "visible",
403
+ "hidden",
404
+ "attached",
405
+ "url"
406
+ ]);
407
+ function sortDeep(value) {
408
+ if (Array.isArray(value)) return value.map(sortDeep);
409
+ if (value !== null && typeof value === "object") {
410
+ const out = {};
411
+ for (const key of Object.keys(value).sort()) {
412
+ const v = value[key];
413
+ if (v !== void 0) out[key] = sortDeep(v);
414
+ }
415
+ return out;
388
416
  }
389
- return hasBlockingFindings(findings) ? EXIT_ISSUES : EXIT_OK;
417
+ return value;
390
418
  }
391
- function buildResolveInvocation(argv, env) {
392
- const args = ["playwright", "test"];
393
- const grep = readOption(argv, "--grep");
394
- if (grep) args.push("--grep", grep);
395
- const project = readOption(argv, "--project");
396
- if (project) args.push("--project", project);
397
- args.push("--workers=1");
398
- const childEnv = { ...env, PIWI_AI: "resolve" };
399
- if (argv.includes("--update-ai")) childEnv.PIWI_AI_UPDATE = "true";
400
- for (const pair of readAll(argv, "--env")) {
401
- const eq = pair.indexOf("=");
402
- if (eq > 0) childEnv[pair.slice(0, eq)] = pair.slice(eq + 1);
419
+ function serializeEntry(entry) {
420
+ return `${JSON.stringify(sortDeep(entry), null, 2)}
421
+ `;
422
+ }
423
+ var ArtifactError = class extends Error {
424
+ };
425
+ function assert(condition, message) {
426
+ if (!condition) throw new ArtifactError(message);
427
+ }
428
+ function validateStructuredLocator(value, where) {
429
+ assert(value !== null && typeof value === "object", `${where}: locator must be an object`);
430
+ const loc = value;
431
+ assert(typeof loc.method === "string", `${where}: locator.method must be a string`);
432
+ assert(LOCATOR_METHOD_SET.has(loc.method), `${where}: locator method "${String(loc.method)}" is not allowlisted`);
433
+ assert(Array.isArray(loc.args), `${where}: locator.args must be an array`);
434
+ if (loc.chain !== void 0) {
435
+ assert(Array.isArray(loc.chain), `${where}: locator.chain must be an array`);
436
+ loc.chain.forEach((child, i) => validateStructuredLocator(child, `${where}.chain[${i}]`));
403
437
  }
404
- return { command: "npx", args, env: childEnv };
405
438
  }
406
- function runResolve(argv, env) {
407
- if (!env.PIWI_DASHBOARD_URL) {
408
- console.error("piwi ai resolve: set PIWI_DASHBOARD_URL (the authoring server) before resolving.");
409
- return EXIT_ERROR;
439
+ function validateRunStep(value, where) {
440
+ assert(value !== null && typeof value === "object", `${where}: step must be an object`);
441
+ const step = value;
442
+ validateStructuredLocator(step.locator, `${where}.locator`);
443
+ assert(typeof step.action === "string", `${where}: action must be a string`);
444
+ assert(ACTION_METHOD_SET.has(step.action), `${where}: action "${String(step.action)}" is not allowlisted`);
445
+ if (step.value !== void 0) assert(typeof step.value === "string", `${where}: value must be a string`);
446
+ if (step.optional !== void 0) assert(typeof step.optional === "boolean", `${where}: optional must be a boolean`);
447
+ if (step.waitForResponse !== void 0) {
448
+ assert(typeof step.waitForResponse === "string", `${where}: waitForResponse must be a string`);
410
449
  }
411
- const invocation = buildResolveInvocation(argv, env);
412
- const result = (0, import_node_child_process.spawnSync)(invocation.command, invocation.args, { stdio: "inherit", env: invocation.env });
413
- if (result.error) {
414
- console.error(`piwi ai resolve: ${result.error.message}`);
415
- return EXIT_ERROR;
450
+ }
451
+ function validatePostcondition(value) {
452
+ assert(value !== null && typeof value === "object", "postcondition must be an object");
453
+ const post = value;
454
+ assert(
455
+ POSTCONDITION_ASSERTS.has(post.assert),
456
+ `postcondition.assert "${String(post.assert)}" is not supported`
457
+ );
458
+ if (post.assert === "url") {
459
+ assert(typeof post.url === "string", "postcondition.url must be a string for a url assert");
460
+ } else {
461
+ validateStructuredLocator(post.locator, "postcondition.locator");
416
462
  }
417
- return result.status ?? EXIT_ERROR;
418
463
  }
419
- async function runAi(argv, env = process.env) {
420
- const [sub, ...rest] = argv;
421
- if (sub === void 0 || sub === "-h" || sub === "--help") {
422
- console.log(USAGE);
423
- return EXIT_OK;
464
+ function parseEntry(text) {
465
+ let raw;
466
+ try {
467
+ raw = JSON.parse(text);
468
+ } catch (error) {
469
+ throw new ArtifactError(`entry is not valid JSON: ${error.message}`);
424
470
  }
425
- switch (sub) {
426
- case "check":
427
- return runCheck(rest, env);
428
- case "resolve":
429
- return runResolve(rest, env);
430
- case "prune":
431
- console.error(
432
- "piwi ai prune: not available in this build yet. `piwi ai check` lists orphaned entries: delete their files by hand, and re-author a non-canonical one with `piwi ai resolve --update-ai`."
433
- );
434
- return EXIT_ERROR;
435
- default:
436
- console.error(`piwi ai: unknown command "${sub}"
437
- `);
438
- console.error(USAGE);
439
- return EXIT_ERROR;
471
+ assert(raw !== null && typeof raw === "object", "entry must be an object");
472
+ const obj = raw;
473
+ assert(obj.version === ARTIFACT_VERSION, `unsupported entry version ${String(obj.version)}`);
474
+ assert(typeof obj.template === "string", "entry.template must be a string");
475
+ if (obj.kind === "locator") {
476
+ validateStructuredLocator(obj.locator, "entry.locator");
477
+ return obj;
478
+ }
479
+ if (obj.kind === "run") {
480
+ assert(Array.isArray(obj.steps), "entry.steps must be an array");
481
+ obj.steps.forEach((step, i) => validateRunStep(step, `entry.steps[${i}]`));
482
+ validatePostcondition(obj.postcondition);
483
+ return obj;
440
484
  }
485
+ throw new ArtifactError(`unsupported entry kind "${String(obj.kind)}"`);
441
486
  }
442
487
 
443
- // src/cli/bug.ts
444
- var fs4 = __toESM(require("fs"));
445
- var path7 = __toESM(require("path"));
446
- var import_node_child_process2 = require("child_process");
447
- var import_node_module = require("module");
448
-
449
- // src/internal/config/desktop.ts
450
- var fs3 = __toESM(require("fs"));
451
- var os = __toESM(require("os"));
452
- var path6 = __toESM(require("path"));
453
- function defaultDesktopConfigPath() {
454
- return path6.join(os.homedir(), ".piwi", "desktop.json");
488
+ // src/internal/ai/keys.ts
489
+ var crypto = __toESM(require("crypto"));
490
+ var path3 = __toESM(require("path"));
491
+ var DEFAULT_AI_DIR = "__piwi__";
492
+ function normalizeTemplate(template) {
493
+ return template.trim().replace(/\s+/g, " ").toLowerCase();
455
494
  }
456
- function readDesktopConfig(filePath2) {
457
- try {
458
- const parsed = JSON.parse(fs3.readFileSync(filePath2, "utf8"));
459
- const url = parsed?.url;
460
- const token = parsed?.token;
461
- if (typeof url !== "string" || !url) return null;
462
- if (typeof token !== "string" || !token) return null;
463
- return { url, token };
464
- } catch {
465
- return null;
466
- }
467
- }
468
-
469
- // src/internal/config/env.ts
470
- var DEFAULTS = {
471
- projectName: "default-project",
472
- uploadTraces: true,
473
- uploadReport: true,
474
- liveFileUploads: true,
475
- collectScmInfo: true,
476
- collectCiInfo: true,
477
- collectPerformanceMetrics: true,
478
- captureLocators: true,
479
- capturePageState: true,
480
- // Off by default: the page inventory reads control and link names from every
481
- // visited page, so it stays opt-in until a project turns it on.
482
- capturePageInventory: false,
483
- // Off by default: JavaScript coverage adds time to every test, so a scheduled job opts in.
484
- captureCodeReach: false,
485
- captureServerTraces: true,
486
- captureResources: true,
487
- leakCheck: "report",
488
- sampleAriaOnPass: true,
489
- uploadManifest: true,
490
- defaultCapture: true,
491
- checkBaseUrl: false,
492
- streaming: true,
493
- streamingBatchSize: 20,
494
- streamingBatchDelay: 2e3,
495
- maxStreamBufferBytes: 100 * 1024 * 1024,
496
- submitTimeout: 15 * 60 * 1e3,
497
- failOnFlakyTests: false,
498
- username: null,
499
- password: null,
500
- apiKey: null,
501
- verbose: false
502
- };
503
- var PIWI_ENV_KEYS = {
504
- serverUrl: "PIWI_DASHBOARD_URL",
505
- projectName: "PIWI_PROJECT_NAME",
506
- verbose: "PIWI_VERBOSE",
507
- apiKey: "PIWI_API_KEY",
508
- username: "PIWI_USERNAME",
509
- password: "PIWI_PASSWORD",
510
- environment: "PIWI_ENVIRONMENT",
511
- label: "PIWI_LABEL",
512
- runLabel: "PIWI_RUN_LABEL",
513
- keep: "PIWI_KEEP",
514
- streaming: "PIWI_STREAMING",
515
- streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
516
- streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
517
- maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
518
- submitTimeout: "PIWI_SUBMIT_TIMEOUT",
519
- liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
520
- failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
521
- uploadTraces: "PIWI_UPLOAD_TRACES",
522
- uploadReport: "PIWI_UPLOAD_REPORT",
523
- captureLocators: "PIWI_CAPTURE_LOCATORS",
524
- capturePageState: "PIWI_CAPTURE_PAGE_STATE",
525
- capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
526
- captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
527
- codeReachRoots: "PIWI_CODE_REACH_ROOTS",
528
- coverageBrowser: "PIWI_COVERAGE_BROWSER",
529
- coverageServer: "PIWI_COVERAGE_SERVER",
530
- captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
531
- captureResources: "PIWI_CAPTURE_RESOURCES",
532
- leakCheck: "PIWI_LEAK_CHECK",
533
- sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
534
- uploadManifest: "PIWI_UPLOAD_MANIFEST",
535
- defaultCapture: "PIWI_DEFAULT_CAPTURE",
536
- checkBaseUrl: "PIWI_CHECK_BASE_URL",
537
- inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
538
- pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
539
- outputFile: "PIWI_OUTPUT_FILE",
540
- aiMode: "PIWI_AI",
541
- aiDir: "PIWI_AI_DIR",
542
- aiOnMiss: "PIWI_AI_ON_MISS",
543
- aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
544
- aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
545
- aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
546
- aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
547
- aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
548
- };
549
- var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
550
- var PIWI_ORIGIN_ENV = {
551
- kind: "PIWI_ORIGIN",
552
- ref: "PIWI_ORIGIN_REF"
553
- };
554
- function originEnv(kind, ref) {
555
- return ref === void 0 ? { [PIWI_ORIGIN_ENV.kind]: kind } : { [PIWI_ORIGIN_ENV.kind]: kind, [PIWI_ORIGIN_ENV.ref]: String(ref) };
556
- }
557
- var PIWI_SHARD_ENV = "PIWI_SHARD";
558
- var PIWI_PROBE_ENV = {
559
- flag: "PIWI_PROBE",
560
- plan: "PIWI_PROBE_PLAN",
561
- results: "PIWI_PROBE_RESULTS",
562
- secret: "PIWI_PROBE_SECRET"
563
- };
564
- var PIWI_FLAKE_ENV = {
565
- plan: "PIWI_FLAKE_PLAN",
566
- results: "PIWI_FLAKE_RESULTS"
567
- };
568
- var ENV_FALLBACK_SPECS = [
569
- { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
570
- { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
571
- { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
572
- { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
573
- { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
574
- { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
575
- { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
576
- { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
577
- { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
578
- { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
579
- { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
580
- { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
581
- { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
582
- { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
583
- { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
584
- { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
585
- { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
586
- { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
587
- { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
588
- { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
589
- { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
590
- { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
591
- { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
592
- { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
593
- { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
594
- { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
595
- { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
596
- { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
597
- { option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
598
- { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
599
- { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
600
- { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
601
- ];
602
-
603
- // src/cli/bug.ts
604
- var EXIT_OK2 = 0;
605
- var EXIT_RUN_FAILED = 1;
606
- var EXIT_ERROR2 = 2;
607
- var REQUEST_TIMEOUT_MS = 3e4;
608
- var USAGE2 = `
609
- piwi bug \u2014 a bug report's failing test, from the dashboard
610
-
611
- Usage:
612
- npx @piwitests/reporter bug <id> [options]
613
-
614
- Prints the spec to commit: the report's steps, test.fail() while the bug
615
- exists, @bug and piwi:bug <id>, written with the project's settings.
616
-
617
- Options:
618
- --write Write it to the project's bugs folder (tests/bugs by default, from the
619
- repository root), then run it once
620
- --out <file> Write it to this file instead (then run it once)
621
- --no-run With --write or --out: write it without running it
622
- --force Replace an existing file
623
- --run-mode The spec without test.fail(), as a reproduction runs it
624
- --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
625
- --api-key <key> API key (env PIWI_API_KEY)
626
495
 
627
- Exit codes: 0 printed or written (and, when run, the test behaved as its
628
- test.fail() expects: the bug reproduces), 1 the run did not (the bug may be
629
- fixed, or a step no longer matches the page), 2 the report could not be read
630
- or the file could not be written.
631
- `.trim();
632
- var VALUE_FLAGS = /* @__PURE__ */ new Set(["--out", "--server-url", "--api-key"]);
633
- function readOption2(argv, name) {
634
- const index = argv.indexOf(name);
635
- if (index === -1) return void 0;
636
- const value = argv[index + 1];
637
- return value !== void 0 && !value.startsWith("--") ? value : void 0;
638
- }
639
- function parseBugArgs(argv, env) {
640
- let idText;
641
- for (let i = 0; i < argv.length; i++) {
642
- const token = argv[i];
643
- if (token.startsWith("--")) {
644
- if (VALUE_FLAGS.has(token)) i++;
645
- continue;
496
+ // src/internal/ai/check.ts
497
+ function findEntryFiles(root, dir) {
498
+ const out = [];
499
+ const walk = (current) => {
500
+ let items;
501
+ try {
502
+ items = fs2.readdirSync(current, { withFileTypes: true });
503
+ } catch {
504
+ return;
505
+ }
506
+ for (const item of items) {
507
+ const full = path4.join(current, item.name);
508
+ if (item.isDirectory()) {
509
+ walk(full);
510
+ } else if (item.isFile() && item.name.endsWith(".json")) {
511
+ const specFileDir = path4.dirname(full);
512
+ const dirName = path4.basename(path4.dirname(specFileDir));
513
+ if (dirName !== dir) continue;
514
+ const specDir = path4.dirname(path4.dirname(specFileDir));
515
+ const specFile = path4.join(specDir, path4.basename(specFileDir));
516
+ out.push({ file: full, specFile, testSlug: item.name.split(".")[0] });
517
+ }
646
518
  }
647
- idText = token;
648
- break;
649
- }
650
- const id = Number((idText ?? "").replace(/^#/, ""));
651
- if (!idText || !Number.isInteger(id) || id <= 0) throw new Error("No bug report id \u2014 usage: piwi bug <id>");
652
- for (const flag of VALUE_FLAGS) {
653
- if (argv.includes(flag) && readOption2(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
654
- }
655
- const serverUrl = (readOption2(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
656
- if (!serverUrl) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
657
- const out = readOption2(argv, "--out") ?? null;
658
- const write = argv.includes("--write") || out !== null;
659
- return {
660
- id,
661
- write,
662
- out,
663
- run: write && !argv.includes("--no-run"),
664
- force: argv.includes("--force"),
665
- mode: argv.includes("--run-mode") ? "run" : "commit",
666
- serverUrl,
667
- apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null
668
519
  };
520
+ walk(root);
521
+ return out;
669
522
  }
670
- async function fetchBugSpec(args, specDir) {
671
- const dir = specDir !== void 0 ? `&specDir=${encodeURIComponent(specDir)}` : "";
672
- const url = `${args.serverUrl}/api/bug-reports/${args.id}/spec?mode=${args.mode}${dir}`;
673
- let res;
674
- try {
675
- res = await fetch(url, {
676
- headers: args.apiKey ? { "X-API-Key": args.apiKey } : {},
677
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
678
- });
679
- } catch (e) {
680
- throw new Error(`could not reach ${args.serverUrl} (${e.message})`);
681
- }
682
- if (res.status === 404) throw new Error(`no bug report #${args.id} on ${args.serverUrl}`);
683
- if (res.status === 401 || res.status === 403) throw new Error(`the dashboard refused the API key (${res.status})`);
684
- if (!res.ok) throw new Error(`the dashboard answered ${res.status}`);
685
- const body = await res.json();
686
- if (typeof body.code !== "string" || typeof body.path !== "string") throw new Error("the dashboard sent no spec");
687
- return { code: body.code, path: body.path, warnings: Array.isArray(body.warnings) ? body.warnings : [] };
688
- }
689
- function repositoryRoot(cwd) {
523
+ function readTextOr(file, fallback) {
690
524
  try {
691
- const out = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
692
- cwd,
693
- encoding: "utf-8",
694
- stdio: ["ignore", "pipe", "ignore"]
695
- }).trim();
696
- return out ? path7.resolve(out) : null;
525
+ return fs2.readFileSync(file, "utf8");
697
526
  } catch {
698
- return null;
527
+ return fallback;
699
528
  }
700
529
  }
701
- function realFolder(dir) {
702
- const rest = [];
703
- let current = dir;
704
- while (!fs4.existsSync(current) && path7.dirname(current) !== current) {
705
- rest.unshift(path7.basename(current));
706
- current = path7.dirname(current);
530
+ function checkAiTree(root, opts = {}) {
531
+ const dir = opts.dir ?? DEFAULT_AI_DIR;
532
+ const findings = [];
533
+ const rel = (file) => path4.relative(root, file).split(path4.sep).join("/");
534
+ const sourceCache = /* @__PURE__ */ new Map();
535
+ const duplicates = /* @__PURE__ */ new Map();
536
+ for (const found of findEntryFiles(root, dir)) {
537
+ const text = readTextOr(found.file, null);
538
+ if (text === null) continue;
539
+ let templateNormalized = null;
540
+ try {
541
+ const entry = parseEntry(text);
542
+ templateNormalized = normalizeTemplate(entry.template);
543
+ if (serializeEntry(entry) !== text) {
544
+ findings.push({
545
+ severity: "error",
546
+ kind: "non-canonical",
547
+ file: rel(found.file),
548
+ message: "file is not in canonical form \u2014 re-author it with `piwi ai resolve --update-ai`"
549
+ });
550
+ }
551
+ if (!sourceCache.has(found.specFile)) sourceCache.set(found.specFile, readTextOr(found.specFile, null));
552
+ const source = sourceCache.get(found.specFile) ?? null;
553
+ if (source === null) {
554
+ findings.push({
555
+ severity: "error",
556
+ kind: "orphan",
557
+ file: rel(found.file),
558
+ message: `spec file is gone (${rel(found.specFile)}) \u2014 the entry is orphaned`
559
+ });
560
+ } else if (!source.includes(entry.template.trim())) {
561
+ findings.push({
562
+ severity: "error",
563
+ kind: "orphan",
564
+ file: rel(found.file),
565
+ message: `template "${entry.template}" no longer appears in ${rel(found.specFile)} \u2014 the entry is orphaned`
566
+ });
567
+ }
568
+ } catch (error) {
569
+ findings.push({
570
+ severity: "error",
571
+ kind: "invalid",
572
+ file: rel(found.file),
573
+ message: `not a valid entry: ${error.message}`
574
+ });
575
+ }
576
+ if (templateNormalized !== null) {
577
+ const key = `${found.specFile}::${found.testSlug}::${templateNormalized}`;
578
+ const list = duplicates.get(key) ?? [];
579
+ list.push(rel(found.file));
580
+ duplicates.set(key, list);
581
+ }
707
582
  }
708
- return path7.join(fs4.realpathSync(current), ...rest);
709
- }
710
- function folderInRepository(root, dir) {
711
- const relative8 = path7.relative(realFolder(root), realFolder(dir));
712
- if (relative8.startsWith("..") || path7.isAbsolute(relative8)) return void 0;
713
- return relative8.split(path7.sep).join("/");
714
- }
715
- function fileFilter(file) {
716
- const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
717
- return `${escape(path7.basename(path7.dirname(file)))}[\\\\/]${escape(path7.basename(file))}$`;
583
+ for (const files of duplicates.values()) {
584
+ if (files.length < 2) continue;
585
+ for (const file of files) {
586
+ findings.push({
587
+ severity: "warning",
588
+ kind: "duplicate-template",
589
+ file,
590
+ message: `duplicate template within the same test \u2014 consider distinct phrasings (${files.join(", ")})`
591
+ });
592
+ }
593
+ }
594
+ return findings.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
718
595
  }
719
- function resolvePlaywrightCli(cwd) {
720
- const require2 = (0, import_node_module.createRequire)(path7.join(cwd, "noop.js"));
596
+ function hasBlockingFindings(findings) {
597
+ return findings.some((f) => f.severity === "error");
598
+ }
599
+
600
+ // src/internal/support/playwright-spawn.ts
601
+ var import_node_child_process = require("child_process");
602
+ var fs3 = __toESM(require("fs"));
603
+ var import_node_module = require("module");
604
+ var path5 = __toESM(require("path"));
605
+ var COULD_NOT_START = 2;
606
+ function resolvePlaywrightCli(cwd = process.cwd()) {
607
+ const require2 = (0, import_node_module.createRequire)(path5.join(cwd, "noop.js"));
721
608
  for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
722
609
  try {
723
610
  return require2.resolve(id);
@@ -726,479 +613,634 @@ function resolvePlaywrightCli(cwd) {
726
613
  }
727
614
  return null;
728
615
  }
729
- function runPlaywright(file, cwd, id, env) {
730
- const cli = resolvePlaywrightCli(cwd);
731
- const filter = fileFilter(file);
732
- const childEnv = { ...env, ...originEnv("bug", id) };
733
- const child = cli ? (0, import_node_child_process2.spawn)(process.execPath, [cli, "test", filter], { stdio: "inherit", cwd, env: childEnv }) : (0, import_node_child_process2.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", filter], {
734
- stdio: "inherit",
735
- cwd,
736
- env: childEnv
737
- });
738
- return new Promise((resolve10) => {
739
- child.on("error", (err) => {
740
- console.error(`piwi bug: could not start Playwright \u2014 ${err.message}`);
741
- resolve10(EXIT_ERROR2);
742
- });
743
- child.on("exit", (code) => resolve10(code ?? EXIT_ERROR2));
744
- });
745
- }
746
- async function runBug(argv, env = process.env, cwd = process.cwd()) {
747
- if (argv.includes("-h") || argv.includes("--help")) {
748
- console.log(USAGE2);
749
- return EXIT_OK2;
750
- }
751
- let args;
752
- try {
753
- args = parseBugArgs(argv, env);
754
- } catch (e) {
755
- console.error(`piwi bug: ${e.message}
756
-
757
- ${USAGE2}`);
758
- return EXIT_ERROR2;
759
- }
760
- const root = args.write ? repositoryRoot(cwd) : null;
761
- const outFile = args.out !== null ? path7.resolve(cwd, args.out) : null;
762
- let spec;
763
- try {
764
- spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path7.dirname(outFile)) : void 0);
765
- } catch (e) {
766
- console.error(`piwi bug: ${e.message}`);
767
- return EXIT_ERROR2;
768
- }
769
- for (const warning of spec.warnings) {
770
- console.error(`piwi bug: warning: step ${warning.step + 1}: ${warning.message}`);
616
+ function resolveNpmEntry(name, execPath = process.execPath) {
617
+ const dir = path5.dirname(execPath);
618
+ for (const candidate of [
619
+ path5.join(dir, "node_modules", "npm", "bin", `${name}-cli.js`),
620
+ path5.join(dir, "..", "lib", "node_modules", "npm", "bin", `${name}-cli.js`)
621
+ ]) {
622
+ if (fs3.existsSync(candidate)) return candidate;
771
623
  }
772
- if (!args.write) {
773
- process.stdout.write(spec.code);
774
- return EXIT_OK2;
775
- }
776
- if (!outFile && !root) {
777
- console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
778
- }
779
- const file = outFile ?? path7.resolve(root ?? cwd, spec.path);
780
- try {
781
- fs4.mkdirSync(path7.dirname(file), { recursive: true });
782
- fs4.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
783
- } catch (e) {
784
- const exists = e.code === "EEXIST";
785
- console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
786
- return EXIT_ERROR2;
624
+ return null;
625
+ }
626
+ function playwrightInvocation(playwrightArgs2, options = {}) {
627
+ const cli = resolvePlaywrightCli(options.cwd);
628
+ if (cli) return { file: process.execPath, args: [cli, "test", ...playwrightArgs2] };
629
+ const runner = options.runner ?? "npx";
630
+ if ((options.platform ?? process.platform) !== "win32") {
631
+ return { file: runner, args: ["playwright", "test", ...playwrightArgs2] };
787
632
  }
788
- const shown = path7.relative(cwd, file) || file;
789
- console.error(`piwi bug: wrote ${shown}`);
790
- if (!args.run) return EXIT_OK2;
791
- const code = await runPlaywright(file, cwd, args.id, env);
792
- if (code === EXIT_ERROR2 && !resolvePlaywrightCli(cwd)) return EXIT_ERROR2;
793
- if (code === 0) {
633
+ const npx = runner === "npx" ? resolveNpmEntry("npx") : null;
634
+ return npx ? { file: process.execPath, args: [npx, "playwright", "test", ...playwrightArgs2] } : null;
635
+ }
636
+ function spawnPlaywright(playwrightArgs2, env, options = {}) {
637
+ const label = options.command ?? "piwi";
638
+ const cwd = options.cwd ?? process.cwd();
639
+ const invocation = playwrightInvocation(playwrightArgs2, { cwd, runner: options.runner });
640
+ if (!invocation) {
794
641
  console.error(
795
- args.mode === "commit" ? `piwi bug: the bug reproduces \u2014 the spec fails on it, as test.fail() expects. Commit ${shown}; remove test.fail() with the fix.` : "piwi bug: the spec passed \u2014 the bug does not show here."
642
+ `${label}: could not find Playwright in ${cwd} \u2014 install it there (npm install --save-dev @playwright/test).`
796
643
  );
797
- return EXIT_OK2;
644
+ return Promise.resolve(COULD_NOT_START);
798
645
  }
799
- console.error(
800
- args.mode === "commit" ? "piwi bug: the spec did not fail as test.fail() expects \u2014 the bug may be fixed here, or a step no longer matches the page (see the output above)." : "piwi bug: the spec failed \u2014 the bug reproduces here, or a step no longer matches the page (see the output above)."
801
- );
802
- return EXIT_RUN_FAILED;
646
+ const output = options.output ?? "inherit";
647
+ let fd = output === "inherit" ? null : fs3.openSync(output.file, "a");
648
+ const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
649
+ const child = (0, import_node_child_process.spawn)(invocation.file, invocation.args, { stdio, env, cwd });
650
+ return new Promise((resolve10) => {
651
+ const done = (code) => {
652
+ if (fd !== null) fs3.closeSync(fd);
653
+ fd = null;
654
+ resolve10(code);
655
+ };
656
+ child.on("error", (err) => {
657
+ console.error(`${label}: could not start Playwright \u2014 ${err.message}`);
658
+ done(COULD_NOT_START);
659
+ });
660
+ child.on("exit", (code) => done(code ?? COULD_NOT_START));
661
+ });
662
+ }
663
+ function runPackageManager(command, args, cwd) {
664
+ const result = process.platform === "win32" ? (0, import_node_child_process.spawnSync)([command, ...args].join(" "), { cwd, stdio: "inherit", shell: true }) : (0, import_node_child_process.spawnSync)(command, args, { cwd, stdio: "inherit" });
665
+ return result.status;
803
666
  }
804
667
 
805
- // src/cli/codegen.ts
806
- var fs5 = __toESM(require("fs"));
807
- var path8 = __toESM(require("path"));
668
+ // src/cli/ai.ts
669
+ var EXIT_OK = 0;
670
+ var EXIT_ISSUES = 1;
671
+ var EXIT_ERROR = 2;
672
+ var USAGE = `
673
+ Usage: piwi ai <command> [options]
808
674
 
809
- // ../core/src/recording.ts
810
- var ASSERTION_MATCHERS = [
811
- "toHaveText",
812
- "toHaveValue",
813
- "toHaveAccessibleName",
814
- "toBeVisible",
815
- "toBeHidden",
816
- "toBeEnabled",
817
- "toBeDisabled",
818
- "toHaveURL"
819
- ];
820
- var VALUE_MATCHERS = /* @__PURE__ */ new Set([
821
- "toHaveText",
822
- "toHaveValue",
823
- "toHaveAccessibleName",
824
- "toHaveURL"
825
- ]);
675
+ Commands:
676
+ check Scan committed AI-step entries for orphans, non-canonical files and
677
+ duplicate templates. Read-only; exits 1 when issues are found.
678
+ resolve Author missing entries by running the suite in resolve mode against
679
+ the configured authoring server (forces --workers=1).
680
+ prune Delete orphaned/dormant entries.
826
681
 
827
- // ../core/src/function-match.ts
828
- function paramArgKey(source) {
829
- return source.path ? `${source.param}.${source.path}` : source.param;
830
- }
831
- function globToRegExp(glob) {
832
- const escaped = glob.split("**").map((part) => part.replace(/[.?+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*");
833
- return new RegExp(`^${escaped}$`);
682
+ Options (check):
683
+ --dir <name> Entry directory name per spec (env PIWI_AI_DIR, default ${DEFAULT_AI_DIR})
684
+ --cwd <path> Root to scan (default: current directory)
685
+ --json Emit findings as JSON
686
+
687
+ Options (resolve):
688
+ --grep <re> Only author entries for matching tests
689
+ --project <name> Author under one Playwright project (a resolve profile)
690
+ --env K=V Extra env for the run (repeatable \u2014 flags/viewport profiles)
691
+ --update-ai Re-author entries that already exist
692
+ (needs PIWI_DASHBOARD_URL / PIWI_API_KEY for the server)
693
+
694
+ Exit codes:
695
+ 0 clean (or --help)
696
+ 1 hygiene issues found
697
+ 2 bad arguments / command unavailable
698
+ `.trim();
699
+ function readOption(argv, name) {
700
+ const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
701
+ if (withEquals) return withEquals.slice(name.length + 1);
702
+ const index = argv.indexOf(name);
703
+ if (index === -1) return void 0;
704
+ const value = argv[index + 1];
705
+ return value && !value.startsWith("--") ? value : void 0;
834
706
  }
835
- function urlMatches(pattern, url) {
836
- if (!pattern) return true;
837
- try {
838
- return globToRegExp(pattern).test(url);
839
- } catch {
840
- return true;
707
+ function readAll(argv, name) {
708
+ const values = [];
709
+ for (let i = 0; i < argv.length; i++) {
710
+ if (argv[i].startsWith(`${name}=`)) values.push(argv[i].slice(name.length + 1));
711
+ else if (argv[i] === name && argv[i + 1] && !argv[i + 1].startsWith("--")) values.push(argv[++i]);
841
712
  }
713
+ return values;
842
714
  }
843
- function scoreTargetMatch(pattern, candidate) {
844
- if (!candidate) return pattern.role || pattern.testId || pattern.name ? 0 : 0.3;
845
- if (pattern.testId) {
846
- return candidate.testId && candidate.testId.toLowerCase() === pattern.testId.toLowerCase() ? 1 : 0;
715
+ function formatFinding(finding) {
716
+ const mark = finding.severity === "error" ? "\u2716" : "\u26A0";
717
+ return `${mark} ${finding.file}
718
+ ${finding.kind}: ${finding.message}`;
719
+ }
720
+ function runCheck(argv, env) {
721
+ const root = path6.resolve(readOption(argv, "--cwd") ?? process.cwd());
722
+ const dir = readOption(argv, "--dir") ?? env.PIWI_AI_DIR ?? DEFAULT_AI_DIR;
723
+ let findings;
724
+ try {
725
+ findings = checkAiTree(root, { dir });
726
+ } catch (error) {
727
+ console.error(`piwi ai check: ${error.message}`);
728
+ return EXIT_ERROR;
847
729
  }
848
- const roleMatches = !pattern.role || candidate.role != null && candidate.role.toLowerCase() === pattern.role.toLowerCase();
849
- if (!roleMatches) return 0;
850
- if (pattern.name) {
851
- const needle = pattern.name.toLowerCase();
852
- const haystack = `${candidate.accessibleName ?? ""} ${candidate.text ?? ""}`.toLowerCase().trim();
853
- const nameMatches2 = haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
854
- if (!nameMatches2) return pattern.role ? 0.35 : 0;
855
- return pattern.role ? 0.95 : 0.6;
730
+ if (argv.includes("--json")) {
731
+ console.log(JSON.stringify({ findings }, null, 2));
732
+ } else if (findings.length === 0) {
733
+ console.log("piwi ai check: no issues found.");
734
+ } else {
735
+ for (const finding of findings) console.log(formatFinding(finding));
736
+ const errors = findings.filter((f) => f.severity === "error").length;
737
+ const warnings = findings.length - errors;
738
+ console.log(`
739
+ ${errors} error(s), ${warnings} warning(s).`);
856
740
  }
857
- return pattern.role ? 0.7 : 0.3;
858
- }
859
- function actionCompatible(patternAction, recordedAction) {
860
- if (patternAction === recordedAction) return true;
861
- return patternAction === "click" && recordedAction === "press" || patternAction === "press" && recordedAction === "click";
862
- }
863
- function stepPairScore(pattern, recorded) {
864
- if (!actionCompatible(pattern.action, recorded.action)) return 0;
865
- const base = scoreTargetMatch(pattern.target, recorded.target);
866
- return pattern.action === recorded.action ? base : base * 0.6;
741
+ return hasBlockingFindings(findings) ? EXIT_ISSUES : EXIT_OK;
867
742
  }
868
- function extractArgs(entry, window, pairs) {
869
- const args = {};
870
- const byPatternIndex = new Map(pairs.map(([windowIdx, patternIdx]) => [patternIdx, windowIdx]));
871
- for (const source of entry.paramSources) {
872
- const windowIdx = byPatternIndex.get(source.stepIndex);
873
- if (windowIdx == null) continue;
874
- const step = window[windowIdx];
875
- const value = source.from === "value" ? step.value : source.from === "testId" ? step.target?.testId ?? null : step.target?.text ?? null;
876
- if (value != null) args[paramArgKey(source)] = value;
743
+ function buildResolveInvocation(argv, env) {
744
+ const args = [];
745
+ const grep = readOption(argv, "--grep");
746
+ if (grep) args.push("--grep", grep);
747
+ const project = readOption(argv, "--project");
748
+ if (project) args.push("--project", project);
749
+ args.push("--workers=1");
750
+ const childEnv = { ...env, PIWI_AI: "resolve" };
751
+ if (argv.includes("--update-ai")) childEnv.PIWI_AI_UPDATE = "true";
752
+ for (const pair of readAll(argv, "--env")) {
753
+ const eq = pair.indexOf("=");
754
+ if (eq > 0) childEnv[pair.slice(0, eq)] = pair.slice(eq + 1);
877
755
  }
878
- return args;
756
+ return { args, env: childEnv };
879
757
  }
880
- function sameElement(pattern, recorded) {
881
- if (pattern.testId || !pattern.name) return true;
882
- const haystack = `${recorded?.accessibleName ?? ""} ${recorded?.text ?? ""}`.toLowerCase().trim();
883
- const needle = pattern.name.toLowerCase();
884
- return haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
758
+ function runResolve(argv, env) {
759
+ if (!env.PIWI_DASHBOARD_URL) {
760
+ console.error("piwi ai resolve: set PIWI_DASHBOARD_URL (the authoring server) before resolving.");
761
+ return Promise.resolve(EXIT_ERROR);
762
+ }
763
+ const invocation = buildResolveInvocation(argv, env);
764
+ return spawnPlaywright(invocation.args, invocation.env, { command: "piwi ai resolve" });
885
765
  }
886
- function contiguousAlignment(steps, startIndex, pattern) {
887
- if (startIndex + pattern.length > steps.length) return null;
888
- const pairs = [];
889
- let total = 0;
890
- for (let j = 0; j < pattern.length; j++) {
891
- const recorded = steps[startIndex + j];
892
- const score = stepPairScore(pattern[j], recorded);
893
- if (score <= 0 || !sameElement(pattern[j].target, recorded.target)) return null;
894
- total += score;
895
- pairs.push([j, j]);
766
+ async function runAi(argv, env = process.env) {
767
+ const [sub, ...rest] = argv;
768
+ if (sub === void 0 || sub === "-h" || sub === "--help") {
769
+ console.log(USAGE);
770
+ return EXIT_OK;
771
+ }
772
+ switch (sub) {
773
+ case "check":
774
+ return runCheck(rest, env);
775
+ case "resolve":
776
+ return runResolve(rest, env);
777
+ case "prune":
778
+ console.error(
779
+ "piwi ai prune: not available in this build yet. `piwi ai check` lists orphaned entries: delete their files by hand, and re-author a non-canonical one with `piwi ai resolve --update-ai`."
780
+ );
781
+ return EXIT_ERROR;
782
+ default:
783
+ console.error(`piwi ai: unknown command "${sub}"
784
+ `);
785
+ console.error(USAGE);
786
+ return EXIT_ERROR;
896
787
  }
897
- return { total, pairs };
898
788
  }
899
- function matchFunctionAt(steps, startIndex, catalog) {
900
- const anchor = steps[startIndex];
901
- if (!anchor) return null;
902
- let best = null;
903
- for (const entry of catalog) {
904
- if (entry.steps.length === 0) continue;
905
- if (!urlMatches(entry.urlPattern, anchor.pageUrl)) continue;
906
- const aligned = contiguousAlignment(steps, startIndex, entry.steps);
907
- if (!aligned) continue;
908
- const { total, pairs } = aligned;
909
- const window = steps.slice(startIndex, startIndex + entry.steps.length);
910
- const score = total / entry.steps.length;
911
- const match = {
912
- entry,
913
- score,
914
- matchedIndices: pairs.map(([windowIdx]) => windowIdx + startIndex),
915
- complete: true,
916
- args: extractArgs(entry, window, pairs)
917
- };
918
- if (!best || match.score > best.score || match.score === best.score && entry.steps.length > best.entry.steps.length)
919
- best = match;
789
+
790
+ // src/cli/bug.ts
791
+ var fs5 = __toESM(require("fs"));
792
+ var path8 = __toESM(require("path"));
793
+ var import_node_child_process2 = require("child_process");
794
+
795
+ // src/internal/config/desktop.ts
796
+ var fs4 = __toESM(require("fs"));
797
+ var os = __toESM(require("os"));
798
+ var path7 = __toESM(require("path"));
799
+ function defaultDesktopConfigPath() {
800
+ return path7.join(os.homedir(), ".piwi", "desktop.json");
801
+ }
802
+ function readDesktopConfig(filePath2) {
803
+ try {
804
+ const parsed = JSON.parse(fs4.readFileSync(filePath2, "utf8"));
805
+ const url = parsed?.url;
806
+ const token = parsed?.token;
807
+ if (typeof url !== "string" || !url) return null;
808
+ if (typeof token !== "string" || !token) return null;
809
+ return { url, token };
810
+ } catch {
811
+ return null;
920
812
  }
921
- return best;
922
813
  }
923
814
 
924
- // ../core/src/locator-chain.ts
925
- var LOCATING_METHODS = /* @__PURE__ */ new Set([
926
- "getByRole",
927
- "getByTestId",
928
- "getByText",
929
- "getByLabel",
930
- "getByPlaceholder",
931
- "getByAltText",
932
- "getByTitle",
933
- "locator",
934
- "frameLocator"
935
- ]);
936
- var NARROWING_METHODS = /* @__PURE__ */ new Set([
937
- "filter",
938
- "first",
939
- "last",
940
- "nth",
941
- "and",
942
- "or",
943
- "visible",
944
- "contentFrame",
945
- "owner"
946
- ]);
947
- var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([...LOCATING_METHODS, ...NARROWING_METHODS]);
948
- var LocatorParseError = class extends Error {
949
- constructor(message) {
950
- super(message);
951
- this.name = "LocatorParseError";
952
- }
815
+ // src/internal/config/env.ts
816
+ var DEFAULTS = {
817
+ projectName: "default-project",
818
+ uploadTraces: true,
819
+ uploadReport: true,
820
+ liveFileUploads: true,
821
+ collectScmInfo: true,
822
+ collectCiInfo: true,
823
+ collectPerformanceMetrics: true,
824
+ captureLocators: true,
825
+ capturePageState: true,
826
+ // Off by default: the page inventory reads control and link names from every
827
+ // visited page, so it stays opt-in until a project turns it on.
828
+ capturePageInventory: false,
829
+ // Off by default: JavaScript coverage adds time to every test, so a scheduled job opts in.
830
+ captureCodeReach: false,
831
+ captureServerTraces: true,
832
+ captureResources: true,
833
+ leakCheck: "report",
834
+ sampleAriaOnPass: true,
835
+ uploadManifest: true,
836
+ defaultCapture: true,
837
+ checkBaseUrl: false,
838
+ streaming: true,
839
+ streamingBatchSize: 20,
840
+ streamingBatchDelay: 2e3,
841
+ maxStreamBufferBytes: 100 * 1024 * 1024,
842
+ submitTimeout: 15 * 60 * 1e3,
843
+ failOnFlakyTests: false,
844
+ username: null,
845
+ password: null,
846
+ apiKey: null,
847
+ verbose: false
953
848
  };
954
- var SIMPLE_ESCAPES = {
955
- n: "\n",
956
- t: " ",
957
- r: "\r",
958
- b: "\b",
959
- f: "\f",
960
- v: "\v",
961
- "0": "\0"
849
+ var PIWI_ENV_KEYS = {
850
+ serverUrl: "PIWI_DASHBOARD_URL",
851
+ projectName: "PIWI_PROJECT_NAME",
852
+ verbose: "PIWI_VERBOSE",
853
+ apiKey: "PIWI_API_KEY",
854
+ username: "PIWI_USERNAME",
855
+ password: "PIWI_PASSWORD",
856
+ environment: "PIWI_ENVIRONMENT",
857
+ label: "PIWI_LABEL",
858
+ runLabel: "PIWI_RUN_LABEL",
859
+ keep: "PIWI_KEEP",
860
+ streaming: "PIWI_STREAMING",
861
+ streamingBatchSize: "PIWI_STREAMING_BATCH_SIZE",
862
+ streamingBatchDelay: "PIWI_STREAMING_BATCH_DELAY",
863
+ maxStreamBufferBytes: "PIWI_MAX_STREAM_BUFFER_BYTES",
864
+ submitTimeout: "PIWI_SUBMIT_TIMEOUT",
865
+ liveFileUploads: "PIWI_LIVE_FILE_UPLOADS",
866
+ failOnFlakyTests: "PIWI_FAIL_ON_FLAKY_TESTS",
867
+ uploadTraces: "PIWI_UPLOAD_TRACES",
868
+ uploadReport: "PIWI_UPLOAD_REPORT",
869
+ captureLocators: "PIWI_CAPTURE_LOCATORS",
870
+ capturePageState: "PIWI_CAPTURE_PAGE_STATE",
871
+ capturePageInventory: "PIWI_CAPTURE_PAGE_INVENTORY",
872
+ captureCodeReach: "PIWI_CAPTURE_CODE_REACH",
873
+ codeReachRoots: "PIWI_CODE_REACH_ROOTS",
874
+ coverageBrowser: "PIWI_COVERAGE_BROWSER",
875
+ coverageServer: "PIWI_COVERAGE_SERVER",
876
+ captureServerTraces: "PIWI_CAPTURE_SERVER_TRACES",
877
+ captureResources: "PIWI_CAPTURE_RESOURCES",
878
+ leakCheck: "PIWI_LEAK_CHECK",
879
+ sampleAriaOnPass: "PIWI_SAMPLE_ARIA_ON_PASS",
880
+ uploadManifest: "PIWI_UPLOAD_MANIFEST",
881
+ defaultCapture: "PIWI_DEFAULT_CAPTURE",
882
+ checkBaseUrl: "PIWI_CHECK_BASE_URL",
883
+ inspectOnFailure: "PIWI_INSPECT_ON_FAIL",
884
+ pickLocatorOnFailure: "PIWI_PICK_LOCATOR_ON_FAIL",
885
+ outputFile: "PIWI_OUTPUT_FILE",
886
+ aiMode: "PIWI_AI",
887
+ aiDir: "PIWI_AI_DIR",
888
+ aiOnMiss: "PIWI_AI_ON_MISS",
889
+ aiMaxSteps: "PIWI_AI_MAX_FLOW_STEPS",
890
+ aiMaxSnapshotChars: "PIWI_AI_MAX_SNAPSHOT_CHARS",
891
+ aiOptionalProbeTimeout: "PIWI_AI_OPTIONAL_PROBE_TIMEOUT",
892
+ aiResponseWaitTimeout: "PIWI_AI_RESPONSE_WAIT_TIMEOUT",
893
+ aiScreenshotFallback: "PIWI_AI_SCREENSHOT_FALLBACK"
962
894
  };
963
- var Parser = class {
964
- constructor(src) {
965
- this.src = src;
966
- this.i = 0;
967
- }
968
- parseTopLevel() {
969
- const chain = this.parseChain();
970
- this.skipSpace();
971
- if (this.i < this.src.length) this.fail("unexpected text");
972
- return chain;
895
+ var PIWI_DESKTOP_CONFIG_ENV = "PIWI_DESKTOP_CONFIG";
896
+ var PIWI_ORIGIN_ENV = {
897
+ kind: "PIWI_ORIGIN",
898
+ ref: "PIWI_ORIGIN_REF"
899
+ };
900
+ function originEnv(kind, ref) {
901
+ return ref === void 0 ? { [PIWI_ORIGIN_ENV.kind]: kind } : { [PIWI_ORIGIN_ENV.kind]: kind, [PIWI_ORIGIN_ENV.ref]: String(ref) };
902
+ }
903
+ var PIWI_SHARD_ENV = "PIWI_SHARD";
904
+ var PIWI_PROBE_ENV = {
905
+ flag: "PIWI_PROBE",
906
+ plan: "PIWI_PROBE_PLAN",
907
+ results: "PIWI_PROBE_RESULTS",
908
+ secret: "PIWI_PROBE_SECRET"
909
+ };
910
+ var PIWI_FLAKE_ENV = {
911
+ plan: "PIWI_FLAKE_PLAN",
912
+ results: "PIWI_FLAKE_RESULTS"
913
+ };
914
+ var ENV_FALLBACK_SPECS = [
915
+ { option: "serverUrl", env: PIWI_ENV_KEYS.serverUrl, kind: "string" },
916
+ { option: "projectName", env: PIWI_ENV_KEYS.projectName, kind: "string" },
917
+ { option: "apiKey", env: PIWI_ENV_KEYS.apiKey, kind: "string" },
918
+ { option: "username", env: PIWI_ENV_KEYS.username, kind: "string" },
919
+ { option: "password", env: PIWI_ENV_KEYS.password, kind: "string" },
920
+ { option: "environment", env: PIWI_ENV_KEYS.environment, kind: "string" },
921
+ { option: "label", env: PIWI_ENV_KEYS.label, kind: "string" },
922
+ { option: "runLabel", env: PIWI_ENV_KEYS.runLabel, kind: "string" },
923
+ { option: "keep", env: PIWI_ENV_KEYS.keep, kind: "bool" },
924
+ { option: "streaming", env: PIWI_ENV_KEYS.streaming, kind: "bool" },
925
+ { option: "streamingBatchSize", env: PIWI_ENV_KEYS.streamingBatchSize, kind: "number" },
926
+ { option: "streamingBatchDelay", env: PIWI_ENV_KEYS.streamingBatchDelay, kind: "number" },
927
+ { option: "maxStreamBufferBytes", env: PIWI_ENV_KEYS.maxStreamBufferBytes, kind: "number" },
928
+ { option: "submitTimeout", env: PIWI_ENV_KEYS.submitTimeout, kind: "number" },
929
+ { option: "liveFileUploads", env: PIWI_ENV_KEYS.liveFileUploads, kind: "bool" },
930
+ { option: "failOnFlakyTests", env: PIWI_ENV_KEYS.failOnFlakyTests, kind: "bool" },
931
+ { option: "uploadTraces", env: PIWI_ENV_KEYS.uploadTraces, kind: "bool" },
932
+ { option: "uploadReport", env: PIWI_ENV_KEYS.uploadReport, kind: "bool" },
933
+ { option: "captureLocators", env: PIWI_ENV_KEYS.captureLocators, kind: "bool" },
934
+ { option: "capturePageState", env: PIWI_ENV_KEYS.capturePageState, kind: "bool" },
935
+ { option: "capturePageInventory", env: PIWI_ENV_KEYS.capturePageInventory, kind: "bool" },
936
+ { option: "captureCodeReach", env: PIWI_ENV_KEYS.captureCodeReach, kind: "bool" },
937
+ { option: "captureServerTraces", env: PIWI_ENV_KEYS.captureServerTraces, kind: "bool" },
938
+ { option: "captureResources", env: PIWI_ENV_KEYS.captureResources, kind: "bool" },
939
+ { option: "leakCheck", env: PIWI_ENV_KEYS.leakCheck, kind: "string" },
940
+ { option: "sampleAriaOnPass", env: PIWI_ENV_KEYS.sampleAriaOnPass, kind: "bool" },
941
+ { option: "uploadManifest", env: PIWI_ENV_KEYS.uploadManifest, kind: "bool" },
942
+ { option: "defaultCapture", env: PIWI_ENV_KEYS.defaultCapture, kind: "bool" },
943
+ { option: "checkBaseUrl", env: PIWI_ENV_KEYS.checkBaseUrl, kind: "bool" },
944
+ { option: "inspectOnFailure", env: PIWI_ENV_KEYS.inspectOnFailure, kind: "bool" },
945
+ { option: "pickLocatorOnFailure", env: PIWI_ENV_KEYS.pickLocatorOnFailure, kind: "bool" },
946
+ { option: "outputFile", env: PIWI_ENV_KEYS.outputFile, kind: "string" }
947
+ ];
948
+
949
+ // src/cli/bug.ts
950
+ var EXIT_OK2 = 0;
951
+ var EXIT_RUN_FAILED = 1;
952
+ var EXIT_ERROR2 = 2;
953
+ var REQUEST_TIMEOUT_MS = 3e4;
954
+ var USAGE2 = `
955
+ piwi bug \u2014 a bug report's failing test, from the dashboard
956
+
957
+ Usage:
958
+ npx @piwitests/reporter bug <id> [options]
959
+
960
+ Prints the spec to commit: the report's steps, test.fail() while the bug
961
+ exists, @bug and piwi:bug <id>, written with the project's settings.
962
+
963
+ Options:
964
+ --write Write it to the project's bugs folder (tests/bugs by default, from the
965
+ repository root), then run it once
966
+ --out <file> Write it to this file instead (then run it once)
967
+ --no-run With --write or --out: write it without running it
968
+ --force Replace an existing file
969
+ --run-mode The spec without test.fail(), as a reproduction runs it
970
+ --server-url <url> Dashboard URL (env PIWI_DASHBOARD_URL)
971
+ --api-key <key> API key (env PIWI_API_KEY)
972
+
973
+ Exit codes: 0 printed or written (and, when run, the test behaved as its
974
+ test.fail() expects: the bug reproduces), 1 the run did not (the bug may be
975
+ fixed, or a step no longer matches the page), 2 the report could not be read
976
+ or the file could not be written.
977
+ `.trim();
978
+ var VALUE_FLAGS = /* @__PURE__ */ new Set(["--out", "--server-url", "--api-key"]);
979
+ function readOption2(argv, name) {
980
+ const index = argv.indexOf(name);
981
+ if (index === -1) return void 0;
982
+ const value = argv[index + 1];
983
+ return value !== void 0 && !value.startsWith("--") ? value : void 0;
984
+ }
985
+ function parseBugArgs(argv, env) {
986
+ let idText;
987
+ for (let i = 0; i < argv.length; i++) {
988
+ const token = argv[i];
989
+ if (token.startsWith("--")) {
990
+ if (VALUE_FLAGS.has(token)) i++;
991
+ continue;
992
+ }
993
+ idText = token;
994
+ break;
973
995
  }
974
- fail(message) {
975
- throw new LocatorParseError(`${message} at "${this.src.slice(this.i, this.i + 20)}"`);
996
+ const id = Number((idText ?? "").replace(/^#/, ""));
997
+ if (!idText || !Number.isInteger(id) || id <= 0) throw new Error("No bug report id \u2014 usage: piwi bug <id>");
998
+ for (const flag of VALUE_FLAGS) {
999
+ if (argv.includes(flag) && readOption2(argv, flag) === void 0) throw new Error(`${flag} expects a value`);
976
1000
  }
977
- skipSpace() {
978
- while (this.i < this.src.length && /\s/.test(this.src[this.i])) this.i++;
1001
+ const serverUrl = (readOption2(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/+$/, "");
1002
+ if (!serverUrl) throw new Error("No dashboard \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
1003
+ const out = readOption2(argv, "--out") ?? null;
1004
+ const write = argv.includes("--write") || out !== null;
1005
+ return {
1006
+ id,
1007
+ write,
1008
+ out,
1009
+ run: write && !argv.includes("--no-run"),
1010
+ force: argv.includes("--force"),
1011
+ mode: argv.includes("--run-mode") ? "run" : "commit",
1012
+ serverUrl,
1013
+ apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null
1014
+ };
1015
+ }
1016
+ async function fetchBugSpec(args, specDir) {
1017
+ const dir = specDir !== void 0 ? `&specDir=${encodeURIComponent(specDir)}` : "";
1018
+ const url = `${args.serverUrl}/api/bug-reports/${args.id}/spec?mode=${args.mode}${dir}`;
1019
+ let res;
1020
+ try {
1021
+ res = await fetch(url, {
1022
+ headers: args.apiKey ? { "X-API-Key": args.apiKey } : {},
1023
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
1024
+ });
1025
+ } catch (e) {
1026
+ throw new Error(`could not reach ${args.serverUrl} (${e.message})`);
979
1027
  }
980
- peek() {
981
- return this.src[this.i];
1028
+ if (res.status === 404) throw new Error(`no bug report #${args.id} on ${args.serverUrl}`);
1029
+ if (res.status === 401 || res.status === 403) throw new Error(`the dashboard refused the API key (${res.status})`);
1030
+ if (!res.ok) throw new Error(`the dashboard answered ${res.status}`);
1031
+ const body = await res.json();
1032
+ if (typeof body.code !== "string" || typeof body.path !== "string") throw new Error("the dashboard sent no spec");
1033
+ return { code: body.code, path: body.path, warnings: Array.isArray(body.warnings) ? body.warnings : [] };
1034
+ }
1035
+ function repositoryRoot(cwd) {
1036
+ try {
1037
+ const out = (0, import_node_child_process2.execFileSync)("git", ["rev-parse", "--show-toplevel"], {
1038
+ cwd,
1039
+ encoding: "utf-8",
1040
+ stdio: ["ignore", "pipe", "ignore"]
1041
+ }).trim();
1042
+ return out ? path8.resolve(out) : null;
1043
+ } catch {
1044
+ return null;
982
1045
  }
983
- expect(ch) {
984
- this.skipSpace();
985
- if (this.src[this.i] !== ch) this.fail(`expected "${ch}"`);
986
- this.i++;
1046
+ }
1047
+ function realFolder(dir) {
1048
+ const rest = [];
1049
+ let current = dir;
1050
+ while (!fs5.existsSync(current) && path8.dirname(current) !== current) {
1051
+ rest.unshift(path8.basename(current));
1052
+ current = path8.dirname(current);
987
1053
  }
988
- identifier() {
989
- const m = /^[A-Za-z_$][\w$]*/.exec(this.src.slice(this.i));
990
- if (!m) this.fail("expected a name");
991
- this.i += m[0].length;
992
- return m[0];
1054
+ return path8.join(fs5.realpathSync(current), ...rest);
1055
+ }
1056
+ function folderInRepository(root, dir) {
1057
+ const relative9 = path8.relative(realFolder(root), realFolder(dir));
1058
+ if (relative9.startsWith("..") || path8.isAbsolute(relative9)) return void 0;
1059
+ return relative9.split(path8.sep).join("/");
1060
+ }
1061
+ function fileFilter(file) {
1062
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1063
+ return `${escape(path8.basename(path8.dirname(file)))}[\\\\/]${escape(path8.basename(file))}$`;
1064
+ }
1065
+ function runPlaywright(file, cwd, id, env) {
1066
+ return spawnPlaywright([fileFilter(file)], { ...env, ...originEnv("bug", id) }, { cwd, command: "piwi bug" });
1067
+ }
1068
+ async function runBug(argv, env = process.env, cwd = process.cwd()) {
1069
+ if (argv.includes("-h") || argv.includes("--help")) {
1070
+ console.log(USAGE2);
1071
+ return EXIT_OK2;
993
1072
  }
994
- /**
995
- * A chain of calls joined by `.`. With `lenient`, parsing stops before the
996
- * first link that doesn't parse instead of failing, so a chain followed by
997
- * other text (an error message) is read up to where it ends. `spans` receives
998
- * where each top-level call starts and ends.
999
- */
1000
- parseChain(opts = {}) {
1001
- this.skipSpace();
1002
- const calls = [];
1003
- for (; ; ) {
1004
- const before = this.i;
1005
- if (calls.length > 0) this.i++;
1006
- this.skipSpace();
1007
- const start = this.i;
1008
- try {
1009
- calls.push(this.parseCall(calls.length === 0));
1010
- } catch (error) {
1011
- if (!opts.lenient || calls.length === 0) throw error;
1012
- this.i = before;
1013
- break;
1014
- }
1015
- opts.spans?.push({ start, end: this.i });
1016
- const end = this.i;
1017
- this.skipSpace();
1018
- if (this.peek() !== ".") {
1019
- this.i = end;
1020
- break;
1021
- }
1022
- }
1023
- return { calls };
1073
+ let args;
1074
+ try {
1075
+ args = parseBugArgs(argv, env);
1076
+ } catch (e) {
1077
+ console.error(`piwi bug: ${e.message}
1078
+
1079
+ ${USAGE2}`);
1080
+ return EXIT_ERROR2;
1024
1081
  }
1025
- /** Where the parser stopped. */
1026
- get position() {
1027
- return this.i;
1082
+ const root = args.write ? repositoryRoot(cwd) : null;
1083
+ const outFile = args.out !== null ? path8.resolve(cwd, args.out) : null;
1084
+ let spec;
1085
+ try {
1086
+ spec = await fetchBugSpec(args, outFile && root ? folderInRepository(root, path8.dirname(outFile)) : void 0);
1087
+ } catch (e) {
1088
+ console.error(`piwi bug: ${e.message}`);
1089
+ return EXIT_ERROR2;
1028
1090
  }
1029
- parseCall(first) {
1030
- this.skipSpace();
1031
- const method = this.identifier();
1032
- if (!LOCATOR_CHAIN_METHODS.has(method)) this.fail(`unsupported method ${method}()`);
1033
- if (first && !LOCATING_METHODS.has(method)) this.fail(`${method}() needs a locator before it`);
1034
- this.expect("(");
1035
- const args = [];
1036
- this.skipSpace();
1037
- if (this.peek() === ")") {
1038
- this.i++;
1039
- return { method, args };
1040
- }
1041
- for (; ; ) {
1042
- args.push(this.parseValue());
1043
- this.skipSpace();
1044
- if (this.peek() === ",") {
1045
- this.i++;
1046
- this.skipSpace();
1047
- if (this.peek() === ")") {
1048
- this.i++;
1049
- break;
1050
- }
1051
- continue;
1052
- }
1053
- this.expect(")");
1054
- break;
1055
- }
1056
- return { method, args };
1091
+ for (const warning of spec.warnings) {
1092
+ console.error(`piwi bug: warning: step ${warning.step + 1}: ${warning.message}`);
1057
1093
  }
1058
- parseValue() {
1059
- this.skipSpace();
1060
- const ch = this.peek();
1061
- if (ch === "'" || ch === '"' || ch === "`") return { type: "string", value: this.parseString() };
1062
- if (ch === "/") return this.parseRegex();
1063
- if (ch === "{") return this.parseObject();
1064
- if (ch !== void 0 && /[-\d]/.test(ch)) {
1065
- const m = /^-?\d+(?:\.\d+)?/.exec(this.src.slice(this.i));
1066
- if (!m) this.fail("expected a number");
1067
- this.i += m[0].length;
1068
- return { type: "number", value: Number(m[0]) };
1069
- }
1070
- const rest = this.src.slice(this.i);
1071
- if (/^true\b/.test(rest)) {
1072
- this.i += 4;
1073
- return { type: "boolean", value: true };
1074
- }
1075
- if (/^false\b/.test(rest)) {
1076
- this.i += 5;
1077
- return { type: "boolean", value: false };
1078
- }
1079
- if (/^[A-Za-z_$]/.test(rest)) return { type: "chain", chain: this.parseChain() };
1080
- return this.fail("expected a value");
1094
+ if (!args.write) {
1095
+ process.stdout.write(spec.code);
1096
+ return EXIT_OK2;
1081
1097
  }
1082
- /** A quoted string, decoding the escapes `JSON.stringify` and JavaScript produce. */
1083
- parseString() {
1084
- const quote4 = this.src[this.i];
1085
- let out = "";
1086
- this.i++;
1087
- while (this.i < this.src.length) {
1088
- const c = this.src[this.i];
1089
- if (c === quote4) {
1090
- this.i++;
1091
- return out;
1092
- }
1093
- if (c !== "\\") {
1094
- out += c;
1095
- this.i++;
1096
- continue;
1097
- }
1098
- const next = this.src[this.i + 1];
1099
- if (next === void 0) break;
1100
- const simple = SIMPLE_ESCAPES[next];
1101
- if (simple !== void 0) {
1102
- out += simple;
1103
- this.i += 2;
1104
- continue;
1105
- }
1106
- const hex = next === "u" && this.src[this.i + 2] === "{" ? /^u\{([0-9a-fA-F]{1,6})\}/.exec(this.src.slice(this.i + 1)) : next === "u" ? /^u([0-9a-fA-F]{4})/.exec(this.src.slice(this.i + 1)) : next === "x" ? /^x([0-9a-fA-F]{2})/.exec(this.src.slice(this.i + 1)) : null;
1107
- if (hex) {
1108
- out += String.fromCodePoint(parseInt(hex[1], 16));
1109
- this.i += 1 + hex[0].length;
1110
- continue;
1111
- }
1112
- if (next === "u" || next === "x") this.fail("malformed escape");
1113
- out += next;
1114
- this.i += 2;
1115
- }
1116
- return this.fail("unterminated string");
1098
+ if (!outFile && !root) {
1099
+ console.error("piwi bug: not in a git repository \u2014 the bugs folder is read from the working directory");
1117
1100
  }
1118
- parseRegex() {
1119
- let j = this.i + 1;
1120
- let inClass = false;
1121
- while (j < this.src.length) {
1122
- const c = this.src[j];
1123
- if (c === "\\") {
1124
- j += 2;
1125
- continue;
1126
- }
1127
- if (c === "[") inClass = true;
1128
- else if (c === "]") inClass = false;
1129
- else if (c === "/" && !inClass) break;
1130
- j++;
1131
- }
1132
- if (j >= this.src.length) this.fail("unterminated regex");
1133
- const source = this.src.slice(this.i + 1, j);
1134
- const flags = /^[a-z]*/.exec(this.src.slice(j + 1))[0];
1135
- this.i = j + 1 + flags.length;
1136
- return { type: "regex", source, flags };
1101
+ const file = outFile ?? path8.resolve(root ?? cwd, spec.path);
1102
+ try {
1103
+ fs5.mkdirSync(path8.dirname(file), { recursive: true });
1104
+ fs5.writeFileSync(file, spec.code, { flag: args.force ? "w" : "wx" });
1105
+ } catch (e) {
1106
+ const exists = e.code === "EEXIST";
1107
+ console.error(`piwi bug: ${exists ? `${file} exists \u2014 pass --force to replace it` : e.message}`);
1108
+ return EXIT_ERROR2;
1137
1109
  }
1138
- parseObject() {
1139
- this.expect("{");
1140
- const entries = [];
1141
- for (; ; ) {
1142
- this.skipSpace();
1143
- if (this.peek() === "}") {
1144
- this.i++;
1145
- break;
1146
- }
1147
- const ch = this.peek();
1148
- const key = ch === "'" || ch === '"' ? this.parseString() : this.identifier();
1149
- this.expect(":");
1150
- entries.push([key, this.parseValue()]);
1151
- this.skipSpace();
1152
- if (this.peek() === ",") {
1153
- this.i++;
1154
- continue;
1155
- }
1156
- this.expect("}");
1157
- break;
1158
- }
1159
- return { type: "object", entries };
1110
+ const shown = path8.relative(cwd, file) || file;
1111
+ console.error(`piwi bug: wrote ${shown}`);
1112
+ if (!args.run) return EXIT_OK2;
1113
+ const code = await runPlaywright(file, cwd, args.id, env);
1114
+ if (code === EXIT_ERROR2 && !resolvePlaywrightCli(cwd)) return EXIT_ERROR2;
1115
+ if (code === 0) {
1116
+ console.error(
1117
+ args.mode === "commit" ? `piwi bug: the bug reproduces \u2014 the spec fails on it, as test.fail() expects. Commit ${shown}; remove test.fail() with the fix.` : "piwi bug: the spec passed \u2014 the bug does not show here."
1118
+ );
1119
+ return EXIT_OK2;
1160
1120
  }
1161
- };
1162
- function parseLocatorChain(expr) {
1163
- const trimmed = expr.trim().replace(/^(?:await\s+)?(?:this\.)?page\./, "");
1164
- if (!trimmed) throw new LocatorParseError("empty expression");
1165
- return new Parser(trimmed).parseTopLevel();
1121
+ console.error(
1122
+ args.mode === "commit" ? "piwi bug: the spec did not fail as test.fail() expects \u2014 the bug may be fixed here, or a step no longer matches the page (see the output above)." : "piwi bug: the spec failed \u2014 the bug reproduces here, or a step no longer matches the page (see the output above)."
1123
+ );
1124
+ return EXIT_RUN_FAILED;
1166
1125
  }
1167
- function tryParseLocatorChain(expr) {
1126
+
1127
+ // src/cli/codegen.ts
1128
+ var fs6 = __toESM(require("fs"));
1129
+ var path9 = __toESM(require("path"));
1130
+
1131
+ // ../core/src/recording.ts
1132
+ var ASSERTION_MATCHERS = [
1133
+ "toHaveText",
1134
+ "toHaveValue",
1135
+ "toHaveAccessibleName",
1136
+ "toBeVisible",
1137
+ "toBeHidden",
1138
+ "toBeEnabled",
1139
+ "toBeDisabled",
1140
+ "toHaveURL"
1141
+ ];
1142
+ var VALUE_MATCHERS = /* @__PURE__ */ new Set([
1143
+ "toHaveText",
1144
+ "toHaveValue",
1145
+ "toHaveAccessibleName",
1146
+ "toHaveURL"
1147
+ ]);
1148
+
1149
+ // ../core/src/function-match.ts
1150
+ function paramArgKey(source) {
1151
+ return source.path ? `${source.param}.${source.path}` : source.param;
1152
+ }
1153
+ function globToRegExp(glob) {
1154
+ const escaped = glob.split("**").map((part) => part.replace(/[.?+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*")).join(".*");
1155
+ return new RegExp(`^${escaped}$`);
1156
+ }
1157
+ function urlMatches(pattern, url) {
1158
+ if (!pattern) return true;
1168
1159
  try {
1169
- return parseLocatorChain(expr);
1160
+ return globToRegExp(pattern).test(url);
1170
1161
  } catch {
1171
- return null;
1162
+ return true;
1172
1163
  }
1173
1164
  }
1174
- function quote(value) {
1175
- const json = JSON.stringify(value);
1176
- return `'${json.slice(1, -1).replace(/\\"/g, '"').replace(/'/g, "\\'")}'`;
1165
+ function scoreTargetMatch(pattern, candidate) {
1166
+ if (!candidate) return pattern.role || pattern.testId || pattern.name ? 0 : 0.3;
1167
+ if (pattern.testId) {
1168
+ return candidate.testId && candidate.testId.toLowerCase() === pattern.testId.toLowerCase() ? 1 : 0;
1169
+ }
1170
+ const roleMatches = !pattern.role || candidate.role != null && candidate.role.toLowerCase() === pattern.role.toLowerCase();
1171
+ if (!roleMatches) return 0;
1172
+ if (pattern.name) {
1173
+ const needle = pattern.name.toLowerCase();
1174
+ const haystack = `${candidate.accessibleName ?? ""} ${candidate.text ?? ""}`.toLowerCase().trim();
1175
+ const nameMatches2 = haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
1176
+ if (!nameMatches2) return pattern.role ? 0.35 : 0;
1177
+ return pattern.role ? 0.95 : 0.6;
1178
+ }
1179
+ return pattern.role ? 0.7 : 0.3;
1177
1180
  }
1178
- function renderArg(arg) {
1179
- switch (arg.type) {
1180
- case "string":
1181
- return quote(arg.value);
1182
- case "regex":
1183
- return `/${arg.source}/${arg.flags}`;
1184
- case "number":
1185
- case "boolean":
1186
- return String(arg.value);
1187
- case "object":
1188
- return arg.entries.length === 0 ? "{}" : `{ ${arg.entries.map(([k, v]) => `${/^[A-Za-z_$][\w$]*$/.test(k) ? k : quote(k)}: ${renderArg(v)}`).join(", ")} }`;
1189
- case "chain":
1190
- return renderLocatorChain(arg.chain);
1181
+ function actionCompatible(patternAction, recordedAction) {
1182
+ if (patternAction === recordedAction) return true;
1183
+ return patternAction === "click" && recordedAction === "press" || patternAction === "press" && recordedAction === "click";
1184
+ }
1185
+ function stepPairScore(pattern, recorded) {
1186
+ if (!actionCompatible(pattern.action, recorded.action)) return 0;
1187
+ const base = scoreTargetMatch(pattern.target, recorded.target);
1188
+ return pattern.action === recorded.action ? base : base * 0.6;
1189
+ }
1190
+ function extractArgs(entry, window, pairs) {
1191
+ const args = {};
1192
+ const byPatternIndex = new Map(pairs.map(([windowIdx, patternIdx]) => [patternIdx, windowIdx]));
1193
+ for (const source of entry.paramSources) {
1194
+ const windowIdx = byPatternIndex.get(source.stepIndex);
1195
+ if (windowIdx == null) continue;
1196
+ const step = window[windowIdx];
1197
+ const value = source.from === "value" ? step.value : source.from === "testId" ? step.target?.testId ?? null : step.target?.text ?? null;
1198
+ if (value != null) args[paramArgKey(source)] = value;
1191
1199
  }
1200
+ return args;
1192
1201
  }
1193
- function renderLocatorCall(call) {
1194
- return `${call.method}(${call.args.map(renderArg).join(", ")})`;
1202
+ function sameElement(pattern, recorded) {
1203
+ if (pattern.testId || !pattern.name) return true;
1204
+ const haystack = `${recorded?.accessibleName ?? ""} ${recorded?.text ?? ""}`.toLowerCase().trim();
1205
+ const needle = pattern.name.toLowerCase();
1206
+ return haystack.length > 0 && (haystack.includes(needle) || needle.includes(haystack));
1195
1207
  }
1196
- function renderLocatorChain(chain) {
1197
- return chain.calls.map(renderLocatorCall).join(".");
1208
+ function contiguousAlignment(steps, startIndex, pattern) {
1209
+ if (startIndex + pattern.length > steps.length) return null;
1210
+ const pairs = [];
1211
+ let total = 0;
1212
+ for (let j = 0; j < pattern.length; j++) {
1213
+ const recorded = steps[startIndex + j];
1214
+ const score = stepPairScore(pattern[j], recorded);
1215
+ if (score <= 0 || !sameElement(pattern[j].target, recorded.target)) return null;
1216
+ total += score;
1217
+ pairs.push([j, j]);
1218
+ }
1219
+ return { total, pairs };
1198
1220
  }
1199
- function canonicalLocator(expr) {
1200
- const chain = tryParseLocatorChain(expr);
1201
- return chain ? renderLocatorChain(chain) : null;
1221
+ function matchFunctionAt(steps, startIndex, catalog) {
1222
+ const anchor = steps[startIndex];
1223
+ if (!anchor) return null;
1224
+ let best = null;
1225
+ for (const entry of catalog) {
1226
+ if (entry.steps.length === 0) continue;
1227
+ if (!urlMatches(entry.urlPattern, anchor.pageUrl)) continue;
1228
+ const aligned = contiguousAlignment(steps, startIndex, entry.steps);
1229
+ if (!aligned) continue;
1230
+ const { total, pairs } = aligned;
1231
+ const window = steps.slice(startIndex, startIndex + entry.steps.length);
1232
+ const score = total / entry.steps.length;
1233
+ const match = {
1234
+ entry,
1235
+ score,
1236
+ matchedIndices: pairs.map(([windowIdx]) => windowIdx + startIndex),
1237
+ complete: true,
1238
+ args: extractArgs(entry, window, pairs)
1239
+ };
1240
+ if (!best || match.score > best.score || match.score === best.score && entry.steps.length > best.entry.steps.length)
1241
+ best = match;
1242
+ }
1243
+ return best;
1202
1244
  }
1203
1245
 
1204
1246
  // ../core/src/locator-stability.ts
@@ -1332,16 +1374,16 @@ function checkFilterOptions(arg, out) {
1332
1374
  function splitTopLevel(text, separator) {
1333
1375
  const parts = [];
1334
1376
  let depth = 0;
1335
- let quote4 = null;
1377
+ let quote3 = null;
1336
1378
  let start = 0;
1337
1379
  for (let i = 0; i < text.length; i++) {
1338
1380
  const c = text[i];
1339
- if (quote4) {
1381
+ if (quote3) {
1340
1382
  if (c === "\\") i++;
1341
- else if (c === quote4) quote4 = null;
1383
+ else if (c === quote3) quote3 = null;
1342
1384
  continue;
1343
1385
  }
1344
- if (c === '"' || c === "'") quote4 = c;
1386
+ if (c === '"' || c === "'") quote3 = c;
1345
1387
  else if (c === "[" || c === "(") depth++;
1346
1388
  else if (c === "]" || c === ")") depth = Math.max(0, depth - 1);
1347
1389
  else if (depth === 0 && text.startsWith(separator, i)) {
@@ -1421,7 +1463,7 @@ function tokenizeComplex(selector) {
1421
1463
  const combinators = [];
1422
1464
  let current = "";
1423
1465
  let depth = 0;
1424
- let quote4 = null;
1466
+ let quote3 = null;
1425
1467
  let pending = null;
1426
1468
  const flush = () => {
1427
1469
  if (!current) return;
@@ -1433,14 +1475,14 @@ function tokenizeComplex(selector) {
1433
1475
  };
1434
1476
  for (let i = 0; i < selector.length; i++) {
1435
1477
  const c = selector[i];
1436
- if (quote4) {
1478
+ if (quote3) {
1437
1479
  current += c;
1438
1480
  if (c === "\\" && i + 1 < selector.length) current += selector[++i];
1439
- else if (c === quote4) quote4 = null;
1481
+ else if (c === quote3) quote3 = null;
1440
1482
  continue;
1441
1483
  }
1442
1484
  if (c === '"' || c === "'") {
1443
- quote4 = c;
1485
+ quote3 = c;
1444
1486
  current += c;
1445
1487
  } else if (c === "[" || c === "(") {
1446
1488
  depth++;
@@ -1482,16 +1524,16 @@ function checkCompound(compound, out) {
1482
1524
  };
1483
1525
  const readBalanced = (open, close) => {
1484
1526
  let depth = 0;
1485
- let quote4 = null;
1527
+ let quote3 = null;
1486
1528
  const start = i + 1;
1487
1529
  for (; i < compound.length; i++) {
1488
1530
  const c = compound[i];
1489
- if (quote4) {
1531
+ if (quote3) {
1490
1532
  if (c === "\\") i++;
1491
- else if (c === quote4) quote4 = null;
1533
+ else if (c === quote3) quote3 = null;
1492
1534
  continue;
1493
1535
  }
1494
- if (c === '"' || c === "'") quote4 = c;
1536
+ if (c === '"' || c === "'") quote3 = c;
1495
1537
  else if (c === open) depth++;
1496
1538
  else if (c === close && --depth === 0) {
1497
1539
  i++;
@@ -1631,17 +1673,7 @@ function pageKey(url) {
1631
1673
  }
1632
1674
 
1633
1675
  // ../core/src/codegen.ts
1634
- var QUOTE_ESCAPES = {
1635
- "\\": "\\\\",
1636
- "'": "\\'",
1637
- "\n": "\\n",
1638
- "\r": "\\r",
1639
- "\u2028": "\\u2028",
1640
- "\u2029": "\\u2029"
1641
- };
1642
- function quote2(s) {
1643
- return `'${s.replace(/[\\'\n\r\u2028\u2029]/g, (c) => QUOTE_ESCAPES[c])}'`;
1644
- }
1676
+ var quote = quoteJsString;
1645
1677
  var LINE_TERMINATORS = /[\n\r\u2028\u2029]/;
1646
1678
  function argIsSafe(arg) {
1647
1679
  switch (arg.type) {
@@ -1768,7 +1800,7 @@ function locatorForStep(step, index, ctx) {
1768
1800
  const chosen = chooseLocator(step.target, ctx.options);
1769
1801
  if (!chosen) {
1770
1802
  ctx.warnings.push({ step: index, code: "no-locator", message: "No locator was captured for this element." });
1771
- return `${ctx.page}.locator(${quote2("/* no locator captured */")})`;
1803
+ return `${ctx.page}.locator(${quote("/* no locator captured */")})`;
1772
1804
  }
1773
1805
  if (chosen.level === "brittle") {
1774
1806
  ctx.warnings.push({
@@ -1801,12 +1833,12 @@ function renderAssertStep(step, index, ctx) {
1801
1833
  });
1802
1834
  return [` // Step ${index + 1}: ${matcher} without an expected value.`];
1803
1835
  }
1804
- arg = quote2(matcher === "toHaveURL" ? urlForCode(assertion.expected, ctx) : assertion.expected);
1836
+ arg = quote(matcher === "toHaveURL" ? urlForCode(assertion.expected, ctx) : assertion.expected);
1805
1837
  }
1806
1838
  const subject = matcher === "toHaveURL" ? ctx.page : locatorForStep(step, index, ctx);
1807
1839
  let line = ` await expect(${subject})${not}.${matcher}(${arg});`;
1808
1840
  if (assertion?.actual != null && assertion.actual !== assertion.expected) {
1809
- line += ` // recorded: ${quote2(assertion.actual)}`;
1841
+ line += ` // recorded: ${quote(assertion.actual)}`;
1810
1842
  }
1811
1843
  lines.push(line);
1812
1844
  return lines;
@@ -1816,10 +1848,10 @@ function fileNames(value) {
1816
1848
  }
1817
1849
  function renderRawStep(step, index, ctx) {
1818
1850
  const { page } = ctx;
1819
- if (step.action === "goto") return [` await ${page}.goto(${quote2(urlForCode(step.value ?? step.pageUrl, ctx))});`];
1851
+ if (step.action === "goto") return [` await ${page}.goto(${quote(urlForCode(step.value ?? step.pageUrl, ctx))});`];
1820
1852
  if (step.action === "assert" || step.action === "assertVisible") return renderAssertStep(step, index, ctx);
1821
1853
  if (step.action === "press" && !step.target) {
1822
- return [` await ${page}.keyboard.press(${quote2(step.value ?? "Enter")});`];
1854
+ return [` await ${page}.keyboard.press(${quote(step.value ?? "Enter")});`];
1823
1855
  }
1824
1856
  const loc = locatorForStep(step, index, ctx);
1825
1857
  switch (step.action) {
@@ -1840,23 +1872,23 @@ function renderRawStep(step, index, ctx) {
1840
1872
  detail: names.join(", "),
1841
1873
  message: `A file was chosen here (${names.join(", ")}); the spec reads it from the directory Playwright runs in.`
1842
1874
  });
1843
- const files = names.length === 1 ? quote2(names[0]) : `[${names.map(quote2).join(", ")}]`;
1875
+ const files = names.length === 1 ? quote(names[0]) : `[${names.map(quote).join(", ")}]`;
1844
1876
  return [` await ${loc}.setInputFiles(${files});`];
1845
1877
  }
1846
1878
  case "hover":
1847
1879
  return [` await ${loc}.hover();`];
1848
1880
  case "fill": {
1849
1881
  if (readsEnv(step, ctx)) return [` await ${loc}.fill(process.env.${envVarFor(step, index, ctx)} ?? '');`];
1850
- return [` await ${loc}.fill(${quote2(step.value ?? "")});`];
1882
+ return [` await ${loc}.fill(${quote(step.value ?? "")});`];
1851
1883
  }
1852
1884
  case "check":
1853
1885
  return [` await ${loc}.check();`];
1854
1886
  case "uncheck":
1855
1887
  return [` await ${loc}.uncheck();`];
1856
1888
  case "selectOption":
1857
- return [` await ${loc}.selectOption(${quote2(step.value ?? "")});`];
1889
+ return [` await ${loc}.selectOption(${quote(step.value ?? "")});`];
1858
1890
  case "press":
1859
- return [` await ${loc}.press(${quote2(step.value ?? "Enter")});`];
1891
+ return [` await ${loc}.press(${quote(step.value ?? "Enter")});`];
1860
1892
  }
1861
1893
  }
1862
1894
  var IDENTIFIER_RE = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
@@ -1939,7 +1971,7 @@ function objectArgExpr(param, match, envArgs) {
1939
1971
  const env = envArgs.get(key);
1940
1972
  const raw = match.args[key];
1941
1973
  if (env == null && raw == null) return null;
1942
- return `${IDENTIFIER_RE.test(field) ? field : quote2(field)}: ${env ?? quote2(raw)}`;
1974
+ return `${IDENTIFIER_RE.test(field) ? field : quote(field)}: ${env ?? quote(raw)}`;
1943
1975
  }).filter((pair) => pair != null);
1944
1976
  return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
1945
1977
  }
@@ -1967,7 +1999,7 @@ function argExpr(entry, match, envArgs) {
1967
1999
  if (raw == null) return p.type === "number" ? "0" : p.type === "boolean" ? "false" : "''";
1968
2000
  if (p.type === "number") return String(Number(raw) || 0);
1969
2001
  if (p.type === "boolean") return String(raw === "true" || raw === "1");
1970
- return quote2(raw);
2002
+ return quote(raw);
1971
2003
  }).join(", ");
1972
2004
  }
1973
2005
  function renderFunctionCall(match, page, envArgs) {
@@ -1987,11 +2019,11 @@ function renderImports(usedEntries, given) {
1987
2019
  const moduleKey = `${entry.module}#${entry.importName}`;
1988
2020
  if (!seenModules.has(moduleKey)) {
1989
2021
  seenModules.add(moduleKey);
1990
- lines.push(`import { ${entry.importName} } from ${quote2(entry.module)};`);
2022
+ lines.push(`import { ${entry.importName} } from ${quote(entry.module)};`);
1991
2023
  }
1992
2024
  } else if (!seenModules.has(entry.module + entry.name)) {
1993
2025
  seenModules.add(entry.module + entry.name);
1994
- lines.push(`import { ${entry.name} } from ${quote2(entry.module)};`);
2026
+ lines.push(`import { ${entry.name} } from ${quote(entry.module)};`);
1995
2027
  }
1996
2028
  }
1997
2029
  return lines;
@@ -2011,12 +2043,12 @@ function renderDetails(options) {
2011
2043
  const tags = (options.tags ?? []).map((t) => t.trim()).filter(Boolean);
2012
2044
  const annotations = (options.annotations ?? []).filter((a) => a.type.trim());
2013
2045
  const lines = [];
2014
- if (tags.length > 0) lines.push(` tag: [${tags.map((t) => quote2(t.startsWith("@") ? t : `@${t}`)).join(", ")}],`);
2046
+ if (tags.length > 0) lines.push(` tag: [${tags.map((t) => quote(t.startsWith("@") ? t : `@${t}`)).join(", ")}],`);
2015
2047
  if (annotations.length > 0) {
2016
2048
  lines.push(" annotation: [");
2017
2049
  for (const a of annotations) {
2018
- const description = a.description != null ? `, description: ${quote2(a.description)}` : "";
2019
- lines.push(` { type: ${quote2(a.type)}${description} },`);
2050
+ const description = a.description != null ? `, description: ${quote(a.description)}` : "";
2051
+ lines.push(` { type: ${quote(a.type)}${description} },`);
2020
2052
  }
2021
2053
  lines.push(" ],");
2022
2054
  }
@@ -2049,7 +2081,7 @@ function wrapInPageSteps(lines, starts, origin) {
2049
2081
  const out = lines.slice(0, starts[0].line);
2050
2082
  starts.forEach((start, n) => {
2051
2083
  const end = starts[n + 1]?.line ?? lines.length;
2052
- out.push(` await test.step(${quote2(pageStepTitle(start.url, origin))}, async () => {`);
2084
+ out.push(` await test.step(${quote(pageStepTitle(start.url, origin))}, async () => {`);
2053
2085
  out.push(...lines.slice(start.line, end).map((line) => ` ${line}`));
2054
2086
  out.push(" });");
2055
2087
  });
@@ -2135,7 +2167,7 @@ function renderSpec(session, options = {}) {
2135
2167
  const start = viewportAt.get(0);
2136
2168
  const opening = [
2137
2169
  ...start ? [viewportLine(start, page)] : [],
2138
- ...!sawGoto && session.startUrl ? [` await ${page}.goto(${quote2(urlForCode(session.startUrl, ctx))});`] : []
2170
+ ...!sawGoto && session.startUrl ? [` await ${page}.goto(${quote(urlForCode(session.startUrl, ctx))});`] : []
2139
2171
  ];
2140
2172
  bodyLines.unshift(...opening);
2141
2173
  const bodyOffset = opening.length;
@@ -2172,7 +2204,7 @@ function renderSpec(session, options = {}) {
2172
2204
  }
2173
2205
  const details = renderDetails(options);
2174
2206
  const fixture = [.../* @__PURE__ */ new Set([page.split(".")[0], ...fixtureReceivers])].join(", ");
2175
- const testOpening = details.length > 0 ? [`test(${quote2(title)}, {`, ...details, `}, async ({ ${fixture} }) => {`] : [`test(${quote2(title)}, async ({ ${fixture} }) => {`];
2207
+ const testOpening = details.length > 0 ? [`test(${quote(title)}, {`, ...details, `}, async ({ ${fixture} }) => {`] : [`test(${quote(title)}, async ({ ${fixture} }) => {`];
2176
2208
  if (options.format === "test") {
2177
2209
  const needs = needsLines("");
2178
2210
  return result([...needs, ...testOpening, ...testBody, `});`, ``], needs.length + testOpening.length);
@@ -2181,7 +2213,7 @@ function renderSpec(session, options = {}) {
2181
2213
  // Package name interpolated rather than adjoining the preceding keyword
2182
2214
  // directly, so this generated-code line does not itself look like a real
2183
2215
  // import to the boundary test in tests/boundary.test.ts.
2184
- `import { test, expect } from ${quote2(options.testImport?.trim() || "@playwright/test")};`,
2216
+ `import { test, expect } from ${quote(options.testImport?.trim() || "@playwright/test")};`,
2185
2217
  ...importLines,
2186
2218
  ``,
2187
2219
  ...testOpening,
@@ -2285,43 +2317,43 @@ var Checker = class {
2285
2317
  constructor() {
2286
2318
  this.errors = [];
2287
2319
  }
2288
- fail(path23, message) {
2289
- if (this.errors.length < 20) this.errors.push(`${path23}: ${message}`);
2320
+ fail(path24, message) {
2321
+ if (this.errors.length < 20) this.errors.push(`${path24}: ${message}`);
2290
2322
  }
2291
- text(v, path23, max, nullable) {
2323
+ text(v, path24, max, nullable) {
2292
2324
  if (v == null) {
2293
- if (!nullable) this.fail(path23, "is required");
2325
+ if (!nullable) this.fail(path24, "is required");
2294
2326
  return null;
2295
2327
  }
2296
2328
  if (typeof v !== "string") {
2297
- this.fail(path23, "must be a string");
2329
+ this.fail(path24, "must be a string");
2298
2330
  return null;
2299
2331
  }
2300
- if (v.length > max) this.fail(path23, `is longer than ${max} characters`);
2332
+ if (v.length > max) this.fail(path24, `is longer than ${max} characters`);
2301
2333
  return v;
2302
2334
  }
2303
- number(v, path23) {
2335
+ number(v, path24) {
2304
2336
  if (typeof v !== "number" || !Number.isFinite(v)) {
2305
- this.fail(path23, "must be a number");
2337
+ this.fail(path24, "must be a number");
2306
2338
  return 0;
2307
2339
  }
2308
2340
  return v;
2309
2341
  }
2310
2342
  };
2311
- function checkTarget(c, v, path23) {
2343
+ function checkTarget(c, v, path24) {
2312
2344
  if (v == null) return null;
2313
2345
  if (!isObject(v)) {
2314
- c.fail(path23, "must be an object");
2346
+ c.fail(path24, "must be an object");
2315
2347
  return null;
2316
2348
  }
2317
2349
  const alternativesIn = v.alternatives ?? [];
2318
2350
  const alternatives = [];
2319
- if (!Array.isArray(alternativesIn)) c.fail(`${path23}.alternatives`, "must be a list");
2351
+ if (!Array.isArray(alternativesIn)) c.fail(`${path24}.alternatives`, "must be a list");
2320
2352
  else if (alternativesIn.length > STEPS_LIMITS.alternatives)
2321
- c.fail(`${path23}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
2353
+ c.fail(`${path24}.alternatives`, `holds more than ${STEPS_LIMITS.alternatives}`);
2322
2354
  else {
2323
2355
  alternativesIn.forEach((alt, i) => {
2324
- const at = `${path23}.alternatives[${i}]`;
2356
+ const at = `${path24}.alternatives[${i}]`;
2325
2357
  if (!isObject(alt)) return c.fail(at, "must be an object");
2326
2358
  const locator = c.text(alt.locator, `${at}.locator`, STEPS_LIMITS.textLength, false);
2327
2359
  const safe = locator ? safeLocator(locator) : null;
@@ -2335,31 +2367,31 @@ function checkTarget(c, v, path23) {
2335
2367
  });
2336
2368
  }
2337
2369
  return {
2338
- tagName: c.text(v.tagName, `${path23}.tagName`, 64, false) ?? "",
2339
- role: c.text(v.role, `${path23}.role`, 64, true),
2340
- accessibleName: c.text(v.accessibleName, `${path23}.accessibleName`, STEPS_LIMITS.textLength, true),
2341
- testId: c.text(v.testId, `${path23}.testId`, STEPS_LIMITS.textLength, true),
2342
- text: c.text(v.text, `${path23}.text`, STEPS_LIMITS.textLength, true),
2370
+ tagName: c.text(v.tagName, `${path24}.tagName`, 64, false) ?? "",
2371
+ role: c.text(v.role, `${path24}.role`, 64, true),
2372
+ accessibleName: c.text(v.accessibleName, `${path24}.accessibleName`, STEPS_LIMITS.textLength, true),
2373
+ testId: c.text(v.testId, `${path24}.testId`, STEPS_LIMITS.textLength, true),
2374
+ text: c.text(v.text, `${path24}.text`, STEPS_LIMITS.textLength, true),
2343
2375
  alternatives
2344
2376
  };
2345
2377
  }
2346
- function checkAssertion(c, v, path23) {
2378
+ function checkAssertion(c, v, path24) {
2347
2379
  if (!isObject(v)) {
2348
- c.fail(path23, "is required on an assert step");
2380
+ c.fail(path24, "is required on an assert step");
2349
2381
  return void 0;
2350
2382
  }
2351
2383
  const matcher = v.matcher;
2352
2384
  if (!ASSERTION_MATCHERS.includes(matcher)) {
2353
- c.fail(`${path23}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
2385
+ c.fail(`${path24}.matcher`, `must be one of ${ASSERTION_MATCHERS.join(", ")}`);
2354
2386
  return void 0;
2355
2387
  }
2356
- const expected = c.text(v.expected, `${path23}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
2388
+ const expected = c.text(v.expected, `${path24}.expected`, STEPS_LIMITS.valueLength, !VALUE_MATCHERS.has(matcher));
2357
2389
  return {
2358
2390
  matcher,
2359
2391
  expected: VALUE_MATCHERS.has(matcher) ? expected : null,
2360
- actual: c.text(v.actual, `${path23}.actual`, STEPS_LIMITS.valueLength, true),
2392
+ actual: c.text(v.actual, `${path24}.actual`, STEPS_LIMITS.valueLength, true),
2361
2393
  negated: v.negated === true,
2362
- note: c.text(v.note, `${path23}.note`, STEPS_LIMITS.valueLength, true)
2394
+ note: c.text(v.note, `${path24}.note`, STEPS_LIMITS.valueLength, true)
2363
2395
  };
2364
2396
  }
2365
2397
  function checkViewports(c, v, stepCount) {
@@ -2400,40 +2432,40 @@ function checkViewports(c, v, stepCount) {
2400
2432
  });
2401
2433
  return out;
2402
2434
  }
2403
- function checkStep(c, v, path23) {
2435
+ function checkStep(c, v, path24) {
2404
2436
  if (!isObject(v)) {
2405
- c.fail(path23, "must be an object");
2437
+ c.fail(path24, "must be an object");
2406
2438
  return null;
2407
2439
  }
2408
2440
  const action = v.action;
2409
2441
  if (!ACTIONS.has(action)) {
2410
- c.fail(`${path23}.action`, "is not a known action");
2442
+ c.fail(`${path24}.action`, "is not a known action");
2411
2443
  return null;
2412
2444
  }
2413
2445
  const step = {
2414
2446
  action: action === "assertVisible" ? "assert" : action,
2415
- target: checkTarget(c, v.target, `${path23}.target`),
2416
- value: c.text(v.value, `${path23}.value`, STEPS_LIMITS.valueLength, true),
2447
+ target: checkTarget(c, v.target, `${path24}.target`),
2448
+ value: c.text(v.value, `${path24}.value`, STEPS_LIMITS.valueLength, true),
2417
2449
  redacted: v.redacted === true,
2418
- pageUrl: c.text(v.pageUrl, `${path23}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2419
- timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path23}.timestamp`)
2450
+ pageUrl: c.text(v.pageUrl, `${path24}.pageUrl`, STEPS_LIMITS.valueLength, false) ?? "",
2451
+ timestamp: v.timestamp == null ? 0 : c.number(v.timestamp, `${path24}.timestamp`)
2420
2452
  };
2421
2453
  if (step.redacted) step.value = null;
2422
2454
  if (action === "hover" || action === "dblclick") {
2423
2455
  step.value = null;
2424
- if (!step.target) c.fail(`${path23}.target`, `is required on a ${action} step`);
2456
+ if (!step.target) c.fail(`${path24}.target`, `is required on a ${action} step`);
2425
2457
  }
2426
- if (action === "setInputFiles" && !step.target) c.fail(`${path23}.target`, "is required on a setInputFiles step");
2458
+ if (action === "setInputFiles" && !step.target) c.fail(`${path24}.target`, "is required on a setInputFiles step");
2427
2459
  if (action === "dragTo") {
2428
2460
  step.value = null;
2429
- if (!step.target) c.fail(`${path23}.target`, "is required on a dragTo step");
2430
- step.dropTarget = checkTarget(c, v.dropTarget, `${path23}.dropTarget`);
2431
- if (!step.dropTarget) c.fail(`${path23}.dropTarget`, "is required on a dragTo step");
2461
+ if (!step.target) c.fail(`${path24}.target`, "is required on a dragTo step");
2462
+ step.dropTarget = checkTarget(c, v.dropTarget, `${path24}.dropTarget`);
2463
+ if (!step.dropTarget) c.fail(`${path24}.dropTarget`, "is required on a dragTo step");
2432
2464
  }
2433
2465
  if (action === "assertVisible") {
2434
2466
  step.assertion = { matcher: "toBeVisible", expected: null, actual: null, negated: false, note: null };
2435
2467
  } else if (action === "assert") {
2436
- step.assertion = checkAssertion(c, v.assertion, `${path23}.assertion`);
2468
+ step.assertion = checkAssertion(c, v.assertion, `${path24}.assertion`);
2437
2469
  }
2438
2470
  return step;
2439
2471
  }
@@ -2732,8 +2764,8 @@ ${USAGE3}`);
2732
2764
  }
2733
2765
  } else {
2734
2766
  try {
2735
- if (fs5.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
2736
- text = fs5.readFileSync(args.file, "utf-8");
2767
+ if (fs6.statSync(args.file).size > MAX_STEPS_FILE_BYTES) throw new Error("is too large to be a steps file");
2768
+ text = fs6.readFileSync(args.file, "utf-8");
2737
2769
  } catch (e) {
2738
2770
  const reason = e.code === "ENOENT" ? "does not exist" : e.message;
2739
2771
  console.error(`piwi codegen: ${args.file} ${reason}`);
@@ -2765,8 +2797,8 @@ ${USAGE3}`);
2765
2797
  return EXIT_OK3;
2766
2798
  }
2767
2799
  try {
2768
- fs5.mkdirSync(path8.dirname(args.out), { recursive: true });
2769
- fs5.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
2800
+ fs6.mkdirSync(path9.dirname(args.out), { recursive: true });
2801
+ fs6.writeFileSync(args.out, result.code, { flag: args.force ? "w" : "wx" });
2770
2802
  } catch (e) {
2771
2803
  const exists = e.code === "EEXIST";
2772
2804
  console.error(`piwi codegen: ${exists ? `${args.out} exists \u2014 pass --force to replace it` : e.message}`);
@@ -2780,7 +2812,7 @@ ${USAGE3}`);
2780
2812
  }
2781
2813
 
2782
2814
  // src/cli/coverage.ts
2783
- var path12 = __toESM(require("path"));
2815
+ var path14 = __toESM(require("path"));
2784
2816
 
2785
2817
  // ../core/src/codeowners.ts
2786
2818
  function patternParts(pattern) {
@@ -2818,10 +2850,10 @@ function pathPatternMatcher(pattern) {
2818
2850
  const { segments, floating } = patternParts(pattern);
2819
2851
  if (floating) {
2820
2852
  const only = segments[0];
2821
- return (path23) => path23.split("/").some((part) => segmentMatches(only, part));
2853
+ return (path24) => path24.split("/").some((part) => segmentMatches(only, part));
2822
2854
  }
2823
- return (path23) => {
2824
- const parts = path23.split("/");
2855
+ return (path24) => {
2856
+ const parts = path24.split("/");
2825
2857
  let at = new Uint8Array(parts.length);
2826
2858
  at[0] = 1;
2827
2859
  for (let i = 0; i < segments.length; i++) {
@@ -2887,8 +2919,8 @@ function parseCoverageScope(raw) {
2887
2919
  function compileCoverageScope(scope) {
2888
2920
  const include = scope.include.map(pathPatternMatcher);
2889
2921
  const exclude = scope.exclude.map(pathPatternMatcher);
2890
- return (path23) => {
2891
- const file = path23.replace(/\\/g, "/").replace(/^\.?\//, "");
2922
+ return (path24) => {
2923
+ const file = path24.replace(/\\/g, "/").replace(/^\.?\//, "");
2892
2924
  if (include.length > 0 && !include.some((matches) => matches(file))) return false;
2893
2925
  return !exclude.some((matches) => matches(file));
2894
2926
  };
@@ -2903,6 +2935,9 @@ function errorMessage(error) {
2903
2935
  return String(error);
2904
2936
  }
2905
2937
 
2938
+ // src/internal/support/test-paths.ts
2939
+ var path10 = __toESM(require("path"));
2940
+
2906
2941
  // src/internal/collect/metadata-collector.ts
2907
2942
  var DETACHED_HEAD = "HEAD";
2908
2943
  function resolveScmBranch(env, gitBranch) {
@@ -2934,14 +2969,14 @@ function normalizeRef(ref) {
2934
2969
 
2935
2970
  // src/internal/coverage-upload/git-inventory.ts
2936
2971
  var import_node_child_process5 = require("child_process");
2937
- var fs7 = __toESM(require("fs"));
2938
- var path10 = __toESM(require("path"));
2972
+ var fs8 = __toESM(require("fs"));
2973
+ var path12 = __toESM(require("path"));
2939
2974
 
2940
2975
  // src/internal/collect/blob-ids.ts
2941
2976
  var import_node_child_process4 = require("child_process");
2942
2977
  var crypto2 = __toESM(require("crypto"));
2943
- var fs6 = __toESM(require("fs"));
2944
- var path9 = __toESM(require("path"));
2978
+ var fs7 = __toESM(require("fs"));
2979
+ var path11 = __toESM(require("path"));
2945
2980
  function gitHashObjects(root, paths) {
2946
2981
  return new Promise((resolve10) => {
2947
2982
  const child = (0, import_node_child_process4.execFile)(
@@ -3008,7 +3043,7 @@ async function readInventory(root, warn = () => {
3008
3043
  const changed = [];
3009
3044
  for (const file of (await git(root, ["diff", "--name-only", "-z", "HEAD"]).catch(() => "")).split("\0")) {
3010
3045
  if (!file || !inventory.has(file)) continue;
3011
- if (fs7.existsSync(path10.join(root, file))) changed.push(file);
3046
+ if (fs8.existsSync(path12.join(root, file))) changed.push(file);
3012
3047
  else inventory.delete(file);
3013
3048
  }
3014
3049
  const ids = await hashPaths(root, changed);
@@ -3027,8 +3062,8 @@ async function readInventory(root, warn = () => {
3027
3062
  }
3028
3063
 
3029
3064
  // src/internal/coverage-upload/report-files.ts
3030
- var fs8 = __toESM(require("fs"));
3031
- var path11 = __toESM(require("path"));
3065
+ var fs9 = __toESM(require("fs"));
3066
+ var path13 = __toESM(require("path"));
3032
3067
 
3033
3068
  // ../core/src/coverage.ts
3034
3069
  var MAX_LINE = 1e6;
@@ -3107,7 +3142,7 @@ var FileLines = class {
3107
3142
  this.functions.set(key, { start, end: last, ran });
3108
3143
  }
3109
3144
  }
3110
- toFile(path23) {
3145
+ toFile(path24) {
3111
3146
  const executable = [];
3112
3147
  const lines = [];
3113
3148
  const push = (ranges, line) => {
@@ -3127,7 +3162,7 @@ var FileLines = class {
3127
3162
  if (didRun) ran.push([start, end]);
3128
3163
  }
3129
3164
  return {
3130
- path: path23,
3165
+ path: path24,
3131
3166
  executable,
3132
3167
  lines,
3133
3168
  functions: normalizeFunctionRanges(ran),
@@ -3139,16 +3174,16 @@ var ReportFiles = class {
3139
3174
  constructor() {
3140
3175
  this.byPath = /* @__PURE__ */ new Map();
3141
3176
  }
3142
- of(path23) {
3143
- let file = this.byPath.get(path23);
3177
+ of(path24) {
3178
+ let file = this.byPath.get(path24);
3144
3179
  if (!file) {
3145
3180
  file = new FileLines();
3146
- this.byPath.set(path23, file);
3181
+ this.byPath.set(path24, file);
3147
3182
  }
3148
3183
  return file;
3149
3184
  }
3150
3185
  toFiles() {
3151
- return [...this.byPath].map(([path23, file]) => file.toFile(path23));
3186
+ return [...this.byPath].map(([path24, file]) => file.toFile(path24));
3152
3187
  }
3153
3188
  };
3154
3189
  var XML_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
@@ -3231,21 +3266,21 @@ var CoberturaReader = class {
3231
3266
  if (head.length < 9 && ("<!--".startsWith(head) || "<![CDATA[".startsWith(head))) return -1;
3232
3267
  if (head.startsWith("<!--")) return this.through(text, open, "-->", 4);
3233
3268
  if (head.startsWith("<![CDATA[")) return this.through(text, open, "]]>", 9);
3234
- let quote4 = this.quote;
3269
+ let quote3 = this.quote;
3235
3270
  for (let at = open + Math.max(1, this.scanned); at < text.length; at++) {
3236
3271
  const char = text.charCodeAt(at);
3237
- if (quote4 !== 0) {
3238
- if (char === quote4) quote4 = 0;
3272
+ if (quote3 !== 0) {
3273
+ if (char === quote3) quote3 = 0;
3239
3274
  } else if (char === GREATER_THAN) {
3240
3275
  this.scanned = 0;
3241
3276
  this.quote = 0;
3242
3277
  return at;
3243
3278
  } else if (char === DOUBLE_QUOTE || char === SINGLE_QUOTE) {
3244
- quote4 = char;
3279
+ quote3 = char;
3245
3280
  }
3246
3281
  }
3247
3282
  this.scanned = text.length - open;
3248
- this.quote = quote4;
3283
+ this.quote = quote3;
3249
3284
  return -1;
3250
3285
  }
3251
3286
  /** Where a comment or CDATA section starting at `open` ends: the last character of `end`; -1 if not yet read. */
@@ -3487,36 +3522,36 @@ function normalizeSlashPath(raw) {
3487
3522
  }
3488
3523
  return (drive || (absolute ? "/" : "")) + out.join("/");
3489
3524
  }
3490
- function isAbsolutePath(path23) {
3491
- return path23.startsWith("/") || /^[A-Za-z]:\//.test(path23);
3525
+ function isAbsolutePath(path24) {
3526
+ return path24.startsWith("/") || /^[A-Za-z]:\//.test(path24);
3492
3527
  }
3493
- function relativeTo(path23, root) {
3528
+ function relativeTo(path24, root) {
3494
3529
  const drive = /^[A-Za-z]:/.test(root);
3495
3530
  const base = root.endsWith("/") ? root : `${root}/`;
3496
- const head = path23.slice(0, base.length);
3531
+ const head = path24.slice(0, base.length);
3497
3532
  const inside = drive ? head.toLowerCase() === base.toLowerCase() : head === base;
3498
3533
  if (!inside) return null;
3499
- const relative8 = path23.slice(base.length);
3500
- return relative8 || null;
3534
+ const relative9 = path24.slice(base.length);
3535
+ return relative9 || null;
3501
3536
  }
3502
3537
  function reportPathCandidates(raw, { repoRoot, bases }) {
3503
3538
  const slash = (value) => value.replace(/\\/g, "/");
3504
3539
  const root = normalizeSlashPath(slash(repoRoot));
3505
- const path23 = slash(raw.trim());
3506
- if (!root || !path23 || path23.includes("\0")) return [];
3540
+ const path24 = slash(raw.trim());
3541
+ if (!root || !path24 || path24.includes("\0")) return [];
3507
3542
  const out = [];
3508
3543
  const add = (absolute) => {
3509
3544
  const normalized = normalizeSlashPath(absolute);
3510
3545
  if (!normalized) return;
3511
- const relative8 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
3512
- if (relative8 && !out.includes(relative8)) out.push(relative8);
3546
+ const relative9 = normalized.startsWith("/_/") ? normalized.slice(3) : relativeTo(normalized, root);
3547
+ if (relative9 && !out.includes(relative9)) out.push(relative9);
3513
3548
  };
3514
- if (isAbsolutePath(path23)) {
3515
- add(path23);
3549
+ if (isAbsolutePath(path24)) {
3550
+ add(path24);
3516
3551
  } else {
3517
3552
  for (const base of bases) {
3518
3553
  const dir = slash(base);
3519
- if (isAbsolutePath(dir)) add(`${dir}/${path23}`);
3554
+ if (isAbsolutePath(dir)) add(`${dir}/${path24}`);
3520
3555
  }
3521
3556
  }
3522
3557
  return out;
@@ -3540,14 +3575,14 @@ function globSegmentRegExp(segment) {
3540
3575
  function expandPattern(pattern, cwd) {
3541
3576
  const normalized = pattern.replace(/\\/g, "/");
3542
3577
  if (!/[*?]/.test(normalized)) {
3543
- const file = path11.resolve(cwd, normalized);
3544
- return fs8.existsSync(file) && fs8.statSync(file).isFile() ? [file] : [];
3578
+ const file = path13.resolve(cwd, normalized);
3579
+ return fs9.existsSync(file) && fs9.statSync(file).isFile() ? [file] : [];
3545
3580
  }
3546
- const absolute = path11.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3581
+ const absolute = path13.isAbsolute(normalized) || /^[A-Za-z]:\//.test(normalized);
3547
3582
  const segments = normalized.split("/");
3548
3583
  const firstWild = segments.findIndex((s) => /[*?]/.test(s));
3549
3584
  const baseParts = segments.slice(0, firstWild);
3550
- const base = absolute ? baseParts.join("/") || "/" : path11.resolve(cwd, ...baseParts);
3585
+ const base = absolute ? baseParts.join("/") || "/" : path13.resolve(cwd, ...baseParts);
3551
3586
  const rest = segments.slice(firstWild).filter((s) => s !== "");
3552
3587
  const out = [];
3553
3588
  const walk = (dir, at) => {
@@ -3555,14 +3590,14 @@ function expandPattern(pattern, cwd) {
3555
3590
  const segment = rest[at];
3556
3591
  let entries;
3557
3592
  try {
3558
- entries = fs8.readdirSync(dir, { withFileTypes: true });
3593
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
3559
3594
  } catch {
3560
3595
  return;
3561
3596
  }
3562
3597
  if (segment === "**") {
3563
3598
  walk(dir, at + 1);
3564
3599
  for (const entry of entries) {
3565
- if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path11.join(dir, entry.name), at);
3600
+ if (entry.isDirectory() && !SKIPPED_DIRS.has(entry.name)) walk(path13.join(dir, entry.name), at);
3566
3601
  }
3567
3602
  return;
3568
3603
  }
@@ -3570,7 +3605,7 @@ function expandPattern(pattern, cwd) {
3570
3605
  const last = at === rest.length - 1;
3571
3606
  for (const entry of entries) {
3572
3607
  if (!match.test(entry.name)) continue;
3573
- const full = path11.join(dir, entry.name);
3608
+ const full = path13.join(dir, entry.name);
3574
3609
  if (last && entry.isFile()) out.push(full);
3575
3610
  else if (!last && entry.isDirectory() && (!SKIPPED_DIRS.has(entry.name) || segment === entry.name)) {
3576
3611
  walk(full, at + 1);
@@ -3583,7 +3618,7 @@ function expandPattern(pattern, cwd) {
3583
3618
  async function readReport(file, format) {
3584
3619
  let reader = format ? new CoverageReportReader(format) : null;
3585
3620
  let head = "";
3586
- for await (const chunk of fs8.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3621
+ for await (const chunk of fs9.createReadStream(file, { encoding: "utf8", highWaterMark: 1024 * 1024 })) {
3587
3622
  if (!reader) {
3588
3623
  head += chunk;
3589
3624
  if (head.length < 4096) continue;
@@ -3605,18 +3640,18 @@ async function readReport(file, format) {
3605
3640
  return reader.end();
3606
3641
  }
3607
3642
  function reportBases(report, reportFile, options) {
3608
- if (options.root) return [path11.resolve(options.cwd, options.root)];
3609
- const dir = path11.dirname(path11.resolve(reportFile));
3643
+ if (options.root) return [path13.resolve(options.cwd, options.root)];
3644
+ const dir = path13.dirname(path13.resolve(reportFile));
3610
3645
  const sources = report.sources.map(
3611
- (source) => path11.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path11.resolve(dir, source)
3646
+ (source) => path13.isAbsolute(source) || /^[A-Za-z]:[\\/]/.test(source) ? source : path13.resolve(dir, source)
3612
3647
  );
3613
- return [...sources, options.cwd, dir, path11.dirname(dir), options.repoRoot];
3648
+ return [...sources, options.cwd, dir, path13.dirname(dir), options.repoRoot];
3614
3649
  }
3615
3650
  function resolveReport(report, reportFile, options, into) {
3616
3651
  const bases = reportBases(report, reportFile, options);
3617
3652
  for (const file of report.files) {
3618
3653
  const candidates = reportPathCandidates(file.path, { repoRoot: options.repoRoot, bases });
3619
- const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs8.existsSync(path11.join(options.repoRoot, candidate)));
3654
+ const match = candidates.find((candidate) => options.inventory.has(candidate)) ?? candidates.find((candidate) => fs9.existsSync(path13.join(options.repoRoot, candidate)));
3620
3655
  if (!match) {
3621
3656
  into.unmatched.push(file.path);
3622
3657
  continue;
@@ -3841,7 +3876,7 @@ async function uploadCoverage(args, env, cwd, warn = warnOnStderr) {
3841
3876
  commit: checkout.commit,
3842
3877
  branch,
3843
3878
  suite: args.suite,
3844
- reports: reports.map((report) => path12.relative(cwd, report) || report),
3879
+ reports: reports.map((report) => path14.relative(cwd, report) || report),
3845
3880
  files: files.length,
3846
3881
  unmatched: resolved.unmatched.length,
3847
3882
  outOfScope: resolved.outOfScope,
@@ -3910,10 +3945,10 @@ ${USAGE4}`);
3910
3945
  }
3911
3946
 
3912
3947
  // src/cli/flake.ts
3913
- var import_node_child_process7 = require("child_process");
3948
+ var import_node_child_process6 = require("child_process");
3914
3949
  var fs11 = __toESM(require("fs"));
3915
3950
  var os2 = __toESM(require("os"));
3916
- var path15 = __toESM(require("path"));
3951
+ var path16 = __toESM(require("path"));
3917
3952
 
3918
3953
  // ../core/src/error-parse.ts
3919
3954
  var ANSI_RE = new RegExp("\\u001B\\[[0-9;]*m", "g");
@@ -3932,6 +3967,7 @@ var CHAIN_LINK_METHODS = /* @__PURE__ */ new Set([
3932
3967
  var LEAF_METHODS = new Set(LOCATOR_BUILDER_METHODS);
3933
3968
  var SUBJECTS = "locator|page|frame|frameLocator|elementHandle|mouse|keyboard|touchscreen|browserContext|browser|apiRequestContext|request|response|route|download|dialog|fileChooser|expect|worker|jsHandle|clock|tracing|video";
3934
3969
  var CALL_RE = new RegExp(`\\b(${SUBJECTS})\\.(\\w+): `);
3970
+ var MAX_PARSED_ERROR_CHARS = 64 * 1024;
3935
3971
 
3936
3972
  // ../core/src/flake-plan.ts
3937
3973
  var FLAKE_PLAN_VERSION = 1;
@@ -4114,8 +4150,8 @@ function withinBudget(startedAt, now, budgetMs) {
4114
4150
  }
4115
4151
 
4116
4152
  // src/internal/support/connection.ts
4117
- var fs9 = __toESM(require("fs"));
4118
- var path13 = __toESM(require("path"));
4153
+ var fs10 = __toESM(require("fs"));
4154
+ var path15 = __toESM(require("path"));
4119
4155
 
4120
4156
  // ../core/src/dotenv.ts
4121
4157
  function parseDotEnv(text) {
@@ -4124,10 +4160,10 @@ function parseDotEnv(text) {
4124
4160
  const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/.exec(raw);
4125
4161
  if (!m) continue;
4126
4162
  let value = m[2];
4127
- const quote4 = value[0];
4128
- if ((quote4 === '"' || quote4 === "'") && value.length > 1 && value.endsWith(quote4)) {
4163
+ const quote3 = value[0];
4164
+ if ((quote3 === '"' || quote3 === "'") && value.length > 1 && value.endsWith(quote3)) {
4129
4165
  value = value.slice(1, -1);
4130
- if (quote4 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
4166
+ if (quote3 === '"') value = value.replace(/\\n/g, "\n").replace(/\\"/g, '"');
4131
4167
  } else {
4132
4168
  value = value.replace(/\s+#.*$/, "");
4133
4169
  }
@@ -4156,7 +4192,7 @@ function resolveCliConnection(flags, env, dirs) {
4156
4192
  let dotEnv = {};
4157
4193
  for (const dir of dirs) {
4158
4194
  try {
4159
- dotEnv = { ...parseDotEnv(fs9.readFileSync(path13.join(dir, ".env"), "utf-8")), ...dotEnv };
4195
+ dotEnv = { ...parseDotEnv(fs10.readFileSync(path15.join(dir, ".env"), "utf-8")), ...dotEnv };
4160
4196
  } catch {
4161
4197
  }
4162
4198
  }
@@ -4168,44 +4204,6 @@ function resolveCliConnection(flags, env, dirs) {
4168
4204
  });
4169
4205
  }
4170
4206
 
4171
- // src/internal/support/playwright-spawn.ts
4172
- var import_node_child_process6 = require("child_process");
4173
- var fs10 = __toESM(require("fs"));
4174
- var import_node_module2 = require("module");
4175
- var path14 = __toESM(require("path"));
4176
- function resolvePlaywrightCli2(cwd = process.cwd()) {
4177
- const require2 = (0, import_node_module2.createRequire)(path14.join(cwd, "noop.js"));
4178
- for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
4179
- try {
4180
- return require2.resolve(id);
4181
- } catch {
4182
- }
4183
- }
4184
- return null;
4185
- }
4186
- function spawnPlaywright(playwrightArgs2, env, options = {}) {
4187
- const output = options.output ?? "inherit";
4188
- let fd = output === "inherit" ? null : fs10.openSync(output.file, "a");
4189
- const stdio = fd === null ? "inherit" : ["ignore", fd, fd];
4190
- const cli = resolvePlaywrightCli2();
4191
- const child = cli ? (0, import_node_child_process6.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio, env }) : (0, import_node_child_process6.spawn)(process.platform === "win32" ? "npx.cmd" : "npx", ["playwright", "test", ...playwrightArgs2], {
4192
- stdio,
4193
- env
4194
- });
4195
- return new Promise((resolve10) => {
4196
- const done = (code) => {
4197
- if (fd !== null) fs10.closeSync(fd);
4198
- fd = null;
4199
- resolve10(code);
4200
- };
4201
- child.on("error", (err) => {
4202
- console.error(`${options.command ?? "piwi"}: could not start Playwright \u2014 ${err.message}`);
4203
- done(2);
4204
- });
4205
- child.on("exit", (code) => done(code ?? 2));
4206
- });
4207
- }
4208
-
4209
4207
  // src/cli/flake.ts
4210
4208
  var USAGE5 = `
4211
4209
  piwi flake \u2014 make a flaky test fail on demand, then prove the fix
@@ -4351,7 +4349,7 @@ function parseFlakeArgs(argv) {
4351
4349
  }
4352
4350
  function gitHead() {
4353
4351
  try {
4354
- return (0, import_node_child_process7.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4352
+ return (0, import_node_child_process6.execFileSync)("git", ["rev-parse", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
4355
4353
  } catch {
4356
4354
  return null;
4357
4355
  }
@@ -4395,7 +4393,7 @@ function playwrightRunner(dir, log) {
4395
4393
  return {
4396
4394
  now: () => Date.now(),
4397
4395
  async runBatch(plan, arm, repeat, batch) {
4398
- const planFile = path15.join(dir, `${arm.id}.plan.json`);
4396
+ const planFile = path16.join(dir, `${arm.id}.plan.json`);
4399
4397
  if (!fs11.existsSync(planFile)) {
4400
4398
  fs11.writeFileSync(
4401
4399
  planFile,
@@ -4408,7 +4406,7 @@ function playwrightRunner(dir, log) {
4408
4406
  })
4409
4407
  );
4410
4408
  }
4411
- const resultsFile = path15.join(dir, `${arm.id}.${batch}.jsonl`);
4409
+ const resultsFile = path16.join(dir, `${arm.id}.${batch}.jsonl`);
4412
4410
  fs11.writeFileSync(resultsFile, "");
4413
4411
  const env = {
4414
4412
  ...process.env,
@@ -4687,8 +4685,8 @@ async function runFlake(argv, runner) {
4687
4685
  if (!args.json) {
4688
4686
  printHeader(plan, commit, estimate, args.budgetMs);
4689
4687
  }
4690
- const dir = fs11.mkdtempSync(path15.join(os2.tmpdir(), "piwi-flake-"));
4691
- const log = path15.join(dir, "playwright.log");
4688
+ const dir = fs11.mkdtempSync(path16.join(os2.tmpdir(), "piwi-flake-"));
4689
+ const log = path16.join(dir, "playwright.log");
4692
4690
  let session;
4693
4691
  try {
4694
4692
  session = await runSession(runner ?? playwrightRunner(dir, log), plan, args, (result) => {
@@ -4879,9 +4877,9 @@ function readCount(argv, name) {
4879
4877
  if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
4880
4878
  return Math.floor(n);
4881
4879
  }
4882
- function readRunIdFromFile(path23) {
4880
+ function readRunIdFromFile(path24) {
4883
4881
  try {
4884
- const parsed = JSON.parse(fs12.readFileSync(path23, "utf-8"));
4882
+ const parsed = JSON.parse(fs12.readFileSync(path24, "utf-8"));
4885
4883
  const runId = Number(parsed.runId);
4886
4884
  return Number.isFinite(runId) && runId > 0 ? runId : null;
4887
4885
  } catch {
@@ -4973,8 +4971,7 @@ async function runGate(argv, env = process.env) {
4973
4971
 
4974
4972
  // src/cli/init.ts
4975
4973
  var fs15 = __toESM(require("fs"));
4976
- var path18 = __toESM(require("path"));
4977
- var import_node_child_process8 = require("child_process");
4974
+ var path19 = __toESM(require("path"));
4978
4975
 
4979
4976
  // src/cli/report.ts
4980
4977
  var STATUS_MARK = {
@@ -4992,7 +4989,7 @@ function formatStep(result) {
4992
4989
 
4993
4990
  // src/cli/detect.ts
4994
4991
  var fs13 = __toESM(require("fs"));
4995
- var path16 = __toESM(require("path"));
4992
+ var path17 = __toESM(require("path"));
4996
4993
  var CONFIG_NAMES = [
4997
4994
  "playwright.config.ts",
4998
4995
  "playwright.config.mts",
@@ -5010,7 +5007,7 @@ var LOCKFILES = [
5010
5007
  ];
5011
5008
  var REPORTER_PACKAGE = "@piwitests/reporter";
5012
5009
  function readPackageJson(root) {
5013
- const pkgPath = path16.join(root, "package.json");
5010
+ const pkgPath = path17.join(root, "package.json");
5014
5011
  try {
5015
5012
  return { pkg: JSON.parse(fs13.readFileSync(pkgPath, "utf-8")), pkgPath };
5016
5013
  } catch {
@@ -5021,13 +5018,13 @@ function detectPackageManager(root, pkg) {
5021
5018
  const declared = pkg?.packageManager?.split("@")[0];
5022
5019
  if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
5023
5020
  for (const [file, manager] of LOCKFILES) {
5024
- if (fs13.existsSync(path16.join(root, file))) return manager;
5021
+ if (fs13.existsSync(path17.join(root, file))) return manager;
5025
5022
  }
5026
5023
  return "npm";
5027
5024
  }
5028
5025
  function findConfig(root) {
5029
5026
  for (const name of CONFIG_NAMES) {
5030
- const candidate = path16.join(root, name);
5027
+ const candidate = path17.join(root, name);
5031
5028
  if (fs13.existsSync(candidate)) return candidate;
5032
5029
  }
5033
5030
  return null;
@@ -5038,14 +5035,14 @@ function langOf(configPath) {
5038
5035
  }
5039
5036
  function suggestProjectName(root, pkg) {
5040
5037
  const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
5041
- return fromPkg || path16.basename(root) || "default-project";
5038
+ return fromPkg || path17.basename(root) || "default-project";
5042
5039
  }
5043
5040
  function hasReporter(pkg) {
5044
5041
  if (!pkg) return false;
5045
5042
  return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
5046
5043
  }
5047
5044
  function detectProject(root) {
5048
- const absRoot = path16.resolve(root);
5045
+ const absRoot = path17.resolve(root);
5049
5046
  const { pkg, pkgPath } = readPackageJson(absRoot);
5050
5047
  const configPath = findConfig(absRoot);
5051
5048
  return {
@@ -5075,9 +5072,7 @@ function installCommand(manager) {
5075
5072
  // src/cli/configure.ts
5076
5073
  var REPORTER_IMPORT = "import { wrapConfig } from '@piwitests/reporter'";
5077
5074
  var MANUAL_HINT = "Could not find `export default defineConfig(...)`. Wrap your exported config by hand: import { wrapConfig } from '@piwitests/reporter', then export default wrapConfig(defineConfig({ ... }), { serverUrl, projectName }).";
5078
- function quote3(value) {
5079
- return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
5080
- }
5075
+ var quote2 = quoteJsString;
5081
5076
  function matchClosingParen(src, openIndex) {
5082
5077
  let depth = 0;
5083
5078
  let quoteChar = null;
@@ -5113,8 +5108,8 @@ function matchClosingParen(src, openIndex) {
5113
5108
  }
5114
5109
  function optionsLiteral(opts) {
5115
5110
  const lines = [];
5116
- if (opts.serverUrl) lines.push(` serverUrl: ${quote3(opts.serverUrl)},`);
5117
- lines.push(` projectName: ${quote3(opts.projectName)},`);
5111
+ if (opts.serverUrl) lines.push(` serverUrl: ${quote2(opts.serverUrl)},`);
5112
+ lines.push(` projectName: ${quote2(opts.projectName)},`);
5118
5113
  return `{
5119
5114
  ${lines.join("\n")}
5120
5115
  }`;
@@ -5188,7 +5183,7 @@ function ensureGitignoreEntry(existing, entry = ".env") {
5188
5183
 
5189
5184
  // src/cli/skills.ts
5190
5185
  var fs14 = __toESM(require("fs"));
5191
- var path17 = __toESM(require("path"));
5186
+ var path18 = __toESM(require("path"));
5192
5187
 
5193
5188
  // ../core/src/skill-stamp.ts
5194
5189
  var SKILL_VERSION_KEY = "piwi-version";
@@ -5251,16 +5246,16 @@ var WORKFLOW_SKILLS = [
5251
5246
  "fix-a-reported-bug"
5252
5247
  ];
5253
5248
  var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
5254
- var DEFAULT_SKILLS_DIR = path17.join(".claude", "skills");
5249
+ var DEFAULT_SKILLS_DIR = path18.join(".claude", "skills");
5255
5250
  function findTemplatesDir(fromDir) {
5256
5251
  let dir = fromDir;
5257
5252
  for (let i = 0; i < 6; i++) {
5258
- if (fs14.existsSync(path17.join(dir, "templates", "skills"))) return path17.join(dir, "templates");
5259
- const parent = path17.dirname(dir);
5253
+ if (fs14.existsSync(path18.join(dir, "templates", "skills"))) return path18.join(dir, "templates");
5254
+ const parent = path18.dirname(dir);
5260
5255
  if (parent === dir) break;
5261
5256
  dir = parent;
5262
5257
  }
5263
- return path17.resolve(fromDir, "..", "..", "templates");
5258
+ return path18.resolve(fromDir, "..", "..", "templates");
5264
5259
  }
5265
5260
  function readFrontMatter(markdown) {
5266
5261
  const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
@@ -5274,14 +5269,14 @@ function readFrontMatter(markdown) {
5274
5269
  }
5275
5270
  function readTemplatesVersion(templatesDir) {
5276
5271
  try {
5277
- const pkg = JSON.parse(fs14.readFileSync(path17.join(templatesDir, "..", "package.json"), "utf-8"));
5272
+ const pkg = JSON.parse(fs14.readFileSync(path18.join(templatesDir, "..", "package.json"), "utf-8"));
5278
5273
  return typeof pkg.version === "string" ? pkg.version : "0.0.0";
5279
5274
  } catch {
5280
5275
  return "0.0.0";
5281
5276
  }
5282
5277
  }
5283
5278
  function templatePath(templatesDir, slug) {
5284
- return path17.join(templatesDir, "skills", slug, "SKILL.md");
5279
+ return path18.join(templatesDir, "skills", slug, "SKILL.md");
5285
5280
  }
5286
5281
  function listSkills(templatesDir, slugs = ALL_SKILLS) {
5287
5282
  const infos = [];
@@ -5298,8 +5293,8 @@ function installSkills(opts) {
5298
5293
  for (const slug of opts.slugs) {
5299
5294
  const step = `skill:${slug}`;
5300
5295
  const source = templatePath(opts.templatesDir, slug);
5301
- const relDest = path17.join(opts.skillsDir, slug, "SKILL.md");
5302
- const dest = path17.join(opts.root, relDest);
5296
+ const relDest = path18.join(opts.skillsDir, slug, "SKILL.md");
5297
+ const dest = path18.join(opts.root, relDest);
5303
5298
  if (!fs14.existsSync(source)) {
5304
5299
  results.push({ step, status: "error", detail: `no template found for "${slug}"` });
5305
5300
  continue;
@@ -5331,7 +5326,7 @@ function installSkills(opts) {
5331
5326
  }
5332
5327
  }
5333
5328
  if (!opts.dryRun) {
5334
- fs14.mkdirSync(path17.dirname(dest), { recursive: true });
5329
+ fs14.mkdirSync(path18.dirname(dest), { recursive: true });
5335
5330
  fs14.writeFileSync(dest, contents);
5336
5331
  }
5337
5332
  results.push({ step, file: relDest, status: exists ? "updated" : "created", detail });
@@ -5404,7 +5399,7 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
5404
5399
  }
5405
5400
  const results = installSkills({
5406
5401
  templatesDir,
5407
- root: path17.resolve(readOption5(rest, "--cwd") ?? cwd),
5402
+ root: path18.resolve(readOption5(rest, "--cwd") ?? cwd),
5408
5403
  skillsDir: readOption5(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
5409
5404
  slugs: requested.length ? requested : ALL_SKILLS,
5410
5405
  force: rest.includes("--force"),
@@ -5475,7 +5470,7 @@ function resolveSkillSlugs(raw) {
5475
5470
  return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
5476
5471
  }
5477
5472
  function parseInitArgs(argv, env, cwd) {
5478
- const root = path18.resolve(readOption6(argv, "--cwd") ?? cwd);
5473
+ const root = path19.resolve(readOption6(argv, "--cwd") ?? cwd);
5479
5474
  const detected = detectProject(root);
5480
5475
  return {
5481
5476
  root,
@@ -5524,12 +5519,12 @@ function stepInstallReporter(project, opts) {
5524
5519
  if (opts.dryRun || !opts.install)
5525
5520
  return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
5526
5521
  const [command, args] = installArgv(project.packageManager);
5527
- const result = (0, import_node_child_process8.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
5528
- if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
5522
+ const status = runPackageManager(command, args, opts.root);
5523
+ if (status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
5529
5524
  return {
5530
5525
  step,
5531
5526
  status: "error",
5532
- detail: `\`${installCommand(project.packageManager)}\` failed (exit ${result.status ?? "unknown"}) \u2014 install it by hand`
5527
+ detail: `\`${installCommand(project.packageManager)}\` failed (exit ${status ?? "unknown"}) \u2014 install it by hand`
5533
5528
  };
5534
5529
  }
5535
5530
  function stepConfig(project, opts) {
@@ -5540,7 +5535,7 @@ function stepConfig(project, opts) {
5540
5535
  status: "manual",
5541
5536
  detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
5542
5537
  };
5543
- const rel = path18.relative(project.root, project.configPath) || path18.basename(project.configPath);
5538
+ const rel = path19.relative(project.root, project.configPath) || path19.basename(project.configPath);
5544
5539
  const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
5545
5540
  serverUrl: opts.serverUrl,
5546
5541
  projectName: opts.projectName
@@ -5550,8 +5545,8 @@ function stepConfig(project, opts) {
5550
5545
  }
5551
5546
  function stepFixtures(project, opts) {
5552
5547
  const step = "fixtures";
5553
- const rel = path18.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5554
- const dest = path18.join(project.root, rel);
5548
+ const rel = path19.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
5549
+ const dest = path19.join(project.root, rel);
5555
5550
  if (fs15.existsSync(dest)) {
5556
5551
  const current = readFileOr(dest, "");
5557
5552
  if (/piwiFixtures/.test(current))
@@ -5564,7 +5559,7 @@ function stepFixtures(project, opts) {
5564
5559
  };
5565
5560
  }
5566
5561
  if (!opts.dryRun) {
5567
- fs15.mkdirSync(path18.dirname(dest), { recursive: true });
5562
+ fs15.mkdirSync(path19.dirname(dest), { recursive: true });
5568
5563
  fs15.writeFileSync(dest, fixturesContents());
5569
5564
  }
5570
5565
  return {
@@ -5576,7 +5571,7 @@ function stepFixtures(project, opts) {
5576
5571
  }
5577
5572
  function stepEnv(project, opts) {
5578
5573
  const results = [];
5579
- const examplePath = path18.join(project.root, ".env.example");
5574
+ const examplePath = path19.join(project.root, ".env.example");
5580
5575
  const example = upsertEnvKeys(readFileOr(examplePath, ""), [
5581
5576
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
5582
5577
  ["PIWI_API_KEY", ""]
@@ -5589,7 +5584,7 @@ function stepEnv(project, opts) {
5589
5584
  detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
5590
5585
  });
5591
5586
  if (opts.apiKey) {
5592
- const envPath = path18.join(project.root, ".env");
5587
+ const envPath = path19.join(project.root, ".env");
5593
5588
  const env = upsertEnvKeys(readFileOr(envPath, ""), [
5594
5589
  ["PIWI_DASHBOARD_URL", opts.serverUrl],
5595
5590
  ["PIWI_API_KEY", opts.apiKey]
@@ -5601,7 +5596,7 @@ function stepEnv(project, opts) {
5601
5596
  status: env.added.length ? "updated" : "already",
5602
5597
  detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
5603
5598
  });
5604
- const gitignorePath = path18.join(project.root, ".gitignore");
5599
+ const gitignorePath = path19.join(project.root, ".gitignore");
5605
5600
  const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
5606
5601
  if (gitignore.added && !opts.dryRun) fs15.writeFileSync(gitignorePath, gitignore.text);
5607
5602
  results.push({
@@ -5665,7 +5660,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
5665
5660
  project: {
5666
5661
  root: project.root,
5667
5662
  packageManager: project.packageManager,
5668
- configPath: project.configPath ? path18.relative(project.root, project.configPath) : null,
5663
+ configPath: project.configPath ? path19.relative(project.root, project.configPath) : null,
5669
5664
  projectName: opts.projectName,
5670
5665
  serverUrl: opts.serverUrl
5671
5666
  },
@@ -5690,14 +5685,13 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
5690
5685
 
5691
5686
  // src/cli/select.ts
5692
5687
  var fs17 = __toESM(require("fs"));
5693
- var path20 = __toESM(require("path"));
5694
- var import_node_child_process9 = require("child_process");
5695
- var import_node_module4 = require("module");
5688
+ var path21 = __toESM(require("path"));
5689
+ var import_node_child_process7 = require("child_process");
5696
5690
 
5697
5691
  // src/cli/add-reporter.ts
5698
5692
  var fs16 = __toESM(require("fs"));
5699
- var path19 = __toESM(require("path"));
5700
- var import_node_module3 = require("module");
5693
+ var path20 = __toESM(require("path"));
5694
+ var import_node_module2 = require("module");
5701
5695
  var REPORTER_PACKAGE2 = "@piwitests/reporter";
5702
5696
  var ADD_REPORTER_FLAG = "--add-reporter";
5703
5697
  var ADD_REPORTER_MIN = { major: 1, minor: 63 };
@@ -5729,9 +5723,9 @@ function resolveConfigPath(cwd, playwrightArgs2) {
5729
5723
  const arg = playwrightArgs2[i];
5730
5724
  if (arg === "--config" || arg === "-c") {
5731
5725
  const next = playwrightArgs2[i + 1];
5732
- if (next) return path19.resolve(cwd, next);
5726
+ if (next) return path20.resolve(cwd, next);
5733
5727
  } else if (arg.startsWith("--config=")) {
5734
- return path19.resolve(cwd, arg.slice("--config=".length));
5728
+ return path20.resolve(cwd, arg.slice("--config=".length));
5735
5729
  }
5736
5730
  }
5737
5731
  return detectProject(cwd).configPath;
@@ -5746,7 +5740,7 @@ function readConfigSource(configPath) {
5746
5740
  }
5747
5741
  function readInstalledPlaywrightVersion(cwd) {
5748
5742
  try {
5749
- const require2 = (0, import_node_module3.createRequire)(path19.join(cwd, "noop.js"));
5743
+ const require2 = (0, import_node_module2.createRequire)(path20.join(cwd, "noop.js"));
5750
5744
  for (const id of ["@playwright/test/package.json", "playwright/package.json"]) {
5751
5745
  try {
5752
5746
  return require2(id).version ?? null;
@@ -5866,7 +5860,7 @@ function parseSelectArgs(argv, env) {
5866
5860
  extra
5867
5861
  };
5868
5862
  }
5869
- var CACHE_FILE = path20.join(".piwi", "selection-cache.json");
5863
+ var CACHE_FILE = path21.join(".piwi", "selection-cache.json");
5870
5864
  function cacheKey(args) {
5871
5865
  return `${args.project.toLowerCase()}:${args.key}:${args.format}:${args.budgetMs ?? 0}:${args.shard ?? ""}:${args.order ?? ""}`;
5872
5866
  }
@@ -5886,7 +5880,7 @@ function writeCache(args, resolution) {
5886
5880
  } catch {
5887
5881
  }
5888
5882
  store[cacheKey(args)] = resolution;
5889
- fs17.mkdirSync(path20.dirname(CACHE_FILE), { recursive: true });
5883
+ fs17.mkdirSync(path21.dirname(CACHE_FILE), { recursive: true });
5890
5884
  fs17.writeFileSync(CACHE_FILE, JSON.stringify(store, null, 2));
5891
5885
  } catch {
5892
5886
  }
@@ -5911,7 +5905,7 @@ function printWarnings(resolution) {
5911
5905
  for (const w of resolution.warnings) console.error(`piwi: warning [${w.code}] ${w.message}`);
5912
5906
  }
5913
5907
  function gitChangedFiles(base) {
5914
- const out = (0, import_node_child_process9.execFileSync)("git", ["diff", "--name-only", base], {
5908
+ const out = (0, import_node_child_process7.execFileSync)("git", ["diff", "--name-only", base], {
5915
5909
  encoding: "utf-8",
5916
5910
  stdio: ["ignore", "pipe", "ignore"]
5917
5911
  });
@@ -5983,34 +5977,10 @@ async function runSelect(argv, env = process.env) {
5983
5977
  else console.log(resolution.materialization.args.join(" "));
5984
5978
  return EXIT_OK6;
5985
5979
  }
5986
- function resolvePlaywrightCli3() {
5987
- const require2 = (0, import_node_module4.createRequire)(path20.join(process.cwd(), "noop.js"));
5988
- for (const id of ["playwright/cli", "@playwright/test/cli", "playwright/lib/cli/cli"]) {
5989
- try {
5990
- return require2.resolve(id);
5991
- } catch {
5992
- }
5993
- }
5994
- return null;
5995
- }
5996
- function spawnPlaywright2(pkgRunner, playwrightArgs2, env) {
5997
- const cli = resolvePlaywrightCli3();
5998
- const child = cli ? (0, import_node_child_process9.spawn)(process.execPath, [cli, "test", ...playwrightArgs2], { stdio: "inherit", env }) : (0, import_node_child_process9.spawn)(process.platform === "win32" ? `${pkgRunner}.cmd` : pkgRunner, ["playwright", "test", ...playwrightArgs2], {
5999
- stdio: "inherit",
6000
- env
6001
- });
6002
- return new Promise((resolve10) => {
6003
- child.on("error", (err) => {
6004
- console.error(`piwi run: could not start Playwright \u2014 ${err.message}`);
6005
- resolve10(EXIT_ERROR7);
6006
- });
6007
- child.on("exit", (code) => resolve10(code ?? EXIT_ERROR7));
6008
- });
6009
- }
6010
5980
  function spawnPlaywrightForRun(pkgRunner, playwrightArgs2, env) {
6011
5981
  const decision = computeAddReporterArgs(process.cwd(), playwrightArgs2);
6012
5982
  if (decision.log) console.error(decision.log);
6013
- return spawnPlaywright2(pkgRunner, [...decision.args, ...playwrightArgs2], env);
5983
+ return spawnPlaywright([...decision.args, ...playwrightArgs2], env, { runner: pkgRunner, command: "piwi run" });
6014
5984
  }
6015
5985
  function shardEnv(args) {
6016
5986
  return args.shard ? { [PIWI_SHARD_ENV]: args.shard } : {};
@@ -6118,15 +6088,15 @@ async function runRun(argv, env = process.env) {
6118
6088
 
6119
6089
  // src/cli/preflight.ts
6120
6090
  var fs18 = __toESM(require("fs"));
6121
- var path21 = __toESM(require("path"));
6122
- var import_node_child_process10 = require("child_process");
6091
+ var path22 = __toESM(require("path"));
6092
+ var import_node_child_process8 = require("child_process");
6123
6093
 
6124
6094
  // ../core/src/diff-anchors.ts
6125
6095
  var RENAME_FROM = `rename from `;
6126
6096
  var RENAME_TO = "rename to ";
6127
6097
  var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
6128
- function stripPrefix(path23) {
6129
- const unquoted = path23.startsWith('"') && path23.endsWith('"') ? path23.slice(1, -1) : path23;
6098
+ function stripPrefix(path24) {
6099
+ const unquoted = path24.startsWith('"') && path24.endsWith('"') ? path24.slice(1, -1) : path24;
6130
6100
  return unquoted.replace(/^[ab]\//, "");
6131
6101
  }
6132
6102
  function parseUnifiedDiff(text) {
@@ -6236,8 +6206,8 @@ function pathGlobToRegExp(glob) {
6236
6206
  });
6237
6207
  return new RegExp(`^(?:${parts.join("|")})$`);
6238
6208
  }
6239
- function isTranslationFile(path23, globs = DEFAULT_TRANSLATION_GLOBS) {
6240
- const posix = path23.replace(/\\/g, "/");
6209
+ function isTranslationFile(path24, globs = DEFAULT_TRANSLATION_GLOBS) {
6210
+ const posix = path24.replace(/\\/g, "/");
6241
6211
  if (!TRANSLATION_EXTENSIONS.test(posix)) return false;
6242
6212
  return globs.some((g) => pathGlobToRegExp(g).test(posix));
6243
6213
  }
@@ -6248,7 +6218,7 @@ function keepable(value) {
6248
6218
  return new RegExp("\\p{L}", "u").test(v);
6249
6219
  }
6250
6220
  function readQuoted(text, start) {
6251
- const quote4 = text[start];
6221
+ const quote3 = text[start];
6252
6222
  let value = "";
6253
6223
  for (let i = start + 1; i < text.length; i++) {
6254
6224
  const ch = text[i];
@@ -6258,8 +6228,8 @@ function readQuoted(text, start) {
6258
6228
  i++;
6259
6229
  continue;
6260
6230
  }
6261
- if (quote4 === "`" && ch === "$" && text[i + 1] === "{") return null;
6262
- if (ch === quote4) return { value, end: i + 1 };
6231
+ if (quote3 === "`" && ch === "$" && text[i + 1] === "{") return null;
6232
+ if (ch === quote3) return { value, end: i + 1 };
6263
6233
  value += ch;
6264
6234
  }
6265
6235
  return null;
@@ -6377,13 +6347,13 @@ function unquoteYaml(raw) {
6377
6347
  function decodeXml2(text) {
6378
6348
  return text.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
6379
6349
  }
6380
- function parseTranslationFile(path23, content) {
6350
+ function parseTranslationFile(path24, content) {
6381
6351
  const lines = content.split(/\r?\n/);
6382
6352
  const out = [];
6383
- const ext = /\.([a-z]+)$/i.exec(path23)?.[1]?.toLowerCase();
6353
+ const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
6384
6354
  if (ext === "json") {
6385
6355
  const stack = [];
6386
- const path24 = (key) => [...stack.map((f) => f.key), key].join(".");
6356
+ const path25 = (key) => [...stack.map((f) => f.key), key].join(".");
6387
6357
  lines.forEach((text, i) => {
6388
6358
  const t = text.trim();
6389
6359
  const top = stack[stack.length - 1];
@@ -6392,14 +6362,14 @@ function parseTranslationFile(path23, content) {
6392
6362
  const key = JSON.parse(`"${pair[1]}"`);
6393
6363
  const rest = pair[2];
6394
6364
  const str = /^"((?:[^"\\]|\\.)*)"/.exec(rest);
6395
- if (str) out.push({ key: path24(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6365
+ if (str) out.push({ key: path25(key), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6396
6366
  else if (/^[[{]\s*$/.test(rest)) stack.push({ key, array: rest.startsWith("["), next: 0 });
6397
6367
  return;
6398
6368
  }
6399
6369
  if (top?.array) {
6400
6370
  const str = /^"((?:[^"\\]|\\.)*)"\s*,?$/.exec(t);
6401
6371
  if (str) {
6402
- out.push({ key: path24(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6372
+ out.push({ key: path25(top.next++), value: JSON.parse(`"${str[1]}"`), line: i + 1 });
6403
6373
  return;
6404
6374
  }
6405
6375
  if (/^[[{]\s*$/.test(t)) {
@@ -6463,8 +6433,8 @@ function parseTranslationFile(path23, content) {
6463
6433
  }
6464
6434
  return out;
6465
6435
  }
6466
- function translationLineToken(path23, text, line) {
6467
- const ext = /\.([a-z]+)$/i.exec(path23)?.[1]?.toLowerCase();
6436
+ function translationLineToken(path24, text, line) {
6437
+ const ext = /\.([a-z]+)$/i.exec(path24)?.[1]?.toLowerCase();
6468
6438
  let key;
6469
6439
  let value;
6470
6440
  if (ext === "json") {
@@ -6501,9 +6471,9 @@ function translationLineToken(path23, text, line) {
6501
6471
  if (value === void 0 || !keepable(value)) return null;
6502
6472
  return { kind: "translation", group: `translation:${key ?? ""}`, value, key, line };
6503
6473
  }
6504
- function lineTokens(path23, text, line, side, attributes, options) {
6474
+ function lineTokens(path24, text, line, side, attributes, options) {
6505
6475
  const spans = [];
6506
- const markup = MARKUP_FILE.test(path23);
6476
+ const markup = MARKUP_FILE.test(path24);
6507
6477
  return [
6508
6478
  ...attributeTokens(text, attributes, line, spans),
6509
6479
  ...translationRefTokens(text, line, side, options.translations, spans),
@@ -6511,10 +6481,10 @@ function lineTokens(path23, text, line, side, attributes, options) {
6511
6481
  ...textTokens(text, line, markup)
6512
6482
  ];
6513
6483
  }
6514
- function keysByLine(path23, side, options) {
6515
- const content = options.readFile?.(path23, side);
6484
+ function keysByLine(path24, side, options) {
6485
+ const content = options.readFile?.(path24, side);
6516
6486
  if (content === void 0) return null;
6517
- return new Map(parseTranslationFile(path23, content).map((e) => [e.line, e.key]));
6487
+ return new Map(parseTranslationFile(path24, content).map((e) => [e.line, e.key]));
6518
6488
  }
6519
6489
  function tokenId(t) {
6520
6490
  return `${t.group}\0${t.value}`;
@@ -6712,9 +6682,9 @@ function cssSlots(selector, testIds, replace) {
6712
6682
  const attr = m[1];
6713
6683
  const names = testIds.includes(attr) ? testIds : attr === "id" || attr === "name" ? [attr] : null;
6714
6684
  if (!names) continue;
6715
- const quote4 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
6685
+ const quote3 = m[2] !== void 0 ? '"' : m[3] !== void 0 ? "'" : "";
6716
6686
  const value = m[2] ?? m[3] ?? m[4];
6717
- slot(value, [attr], (v) => `[${attr}=${quote4}${v}${quote4}]`, m.index, m[0].length);
6687
+ slot(value, [attr], (v) => `[${attr}=${quote3}${v}${quote3}]`, m.index, m[0].length);
6718
6688
  }
6719
6689
  return slots;
6720
6690
  }
@@ -6814,7 +6784,7 @@ function slotMatches(slot, text) {
6814
6784
  return textMatches(slot.value, slot.exact, text);
6815
6785
  }
6816
6786
  }
6817
- var filePath = (path23) => path23.replace(/\\/g, "/").replace(/^\.\//, "");
6787
+ var filePath = (path24) => path24.replace(/\\/g, "/").replace(/^\.\//, "");
6818
6788
  function sameFilePath(a, b) {
6819
6789
  const x = filePath(a);
6820
6790
  const y = filePath(b);
@@ -6908,16 +6878,16 @@ function confidenceOf(anchor, tests, reach) {
6908
6878
  }
6909
6879
 
6910
6880
  // ../core/src/locator-edit.ts
6911
- function escapeForQuote(value, quote4) {
6912
- let out = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
6913
- out = out.split(quote4).join(`\\${quote4}`);
6914
- return quote4 === "`" ? out.replace(/\$\{/g, "\\${") : out;
6881
+ function escapeForQuote(value, quote3) {
6882
+ let out = value.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
6883
+ out = out.split(quote3).join(`\\${quote3}`);
6884
+ return quote3 === "`" ? out.replace(/\$\{/g, "\\${") : out;
6915
6885
  }
6916
6886
  function buildLiteralEdit(sourceLine, oldValue, newValue) {
6917
6887
  if (!sourceLine || oldValue === newValue) return null;
6918
6888
  for (let i = 0; i < sourceLine.length; i++) {
6919
- const quote4 = sourceLine[i];
6920
- if (quote4 !== "'" && quote4 !== '"' && quote4 !== "`") continue;
6889
+ const quote3 = sourceLine[i];
6890
+ if (quote3 !== "'" && quote3 !== '"' && quote3 !== "`") continue;
6921
6891
  let value = "";
6922
6892
  let end = -1;
6923
6893
  for (let j = i + 1; j < sourceLine.length; j++) {
@@ -6928,8 +6898,8 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
6928
6898
  j++;
6929
6899
  continue;
6930
6900
  }
6931
- if (quote4 === "`" && ch === "$" && sourceLine[j + 1] === "{") break;
6932
- if (ch === quote4) {
6901
+ if (quote3 === "`" && ch === "$" && sourceLine[j + 1] === "{") break;
6902
+ if (ch === quote3) {
6933
6903
  end = j;
6934
6904
  break;
6935
6905
  }
@@ -6937,7 +6907,7 @@ function buildLiteralEdit(sourceLine, oldValue, newValue) {
6937
6907
  }
6938
6908
  if (end === -1) continue;
6939
6909
  if (value === oldValue) {
6940
- const next = sourceLine.slice(0, i + 1) + escapeForQuote(newValue, quote4) + sourceLine.slice(end);
6910
+ const next = sourceLine.slice(0, i + 1) + escapeForQuote(newValue, quote3) + sourceLine.slice(end);
6941
6911
  return { old: sourceLine, new: next };
6942
6912
  }
6943
6913
  i = end;
@@ -7029,16 +6999,16 @@ function parsePreflightArgs(argv) {
7029
6999
  }
7030
7000
  var CONFIG_NAMES2 = ["playwright.config.ts", "playwright.config.js", "playwright.config.mjs", "playwright.config.cjs"];
7031
7001
  function findPlaywrightRoot(from) {
7032
- let dir = path21.resolve(from);
7002
+ let dir = path22.resolve(from);
7033
7003
  for (; ; ) {
7034
- if (CONFIG_NAMES2.some((name) => fs18.existsSync(path21.join(dir, name)))) return dir;
7035
- const parent = path21.dirname(dir);
7004
+ if (CONFIG_NAMES2.some((name) => fs18.existsSync(path22.join(dir, name)))) return dir;
7005
+ const parent = path22.dirname(dir);
7036
7006
  if (parent === dir) return null;
7037
7007
  dir = parent;
7038
7008
  }
7039
7009
  }
7040
7010
  function git2(cwd, args) {
7041
- return (0, import_node_child_process10.execFileSync)("git", args, {
7011
+ return (0, import_node_child_process8.execFileSync)("git", args, {
7042
7012
  cwd,
7043
7013
  encoding: "utf-8",
7044
7014
  stdio: ["ignore", "pipe", "pipe"],
@@ -7062,7 +7032,7 @@ function translationLookup(repoRoot, base, locale) {
7062
7032
  const build = (side) => {
7063
7033
  const map = /* @__PURE__ */ new Map();
7064
7034
  for (const file of files) {
7065
- const content = side === "new" ? readText(path21.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
7035
+ const content = side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${base}:${file}`]);
7066
7036
  if (content === void 0) continue;
7067
7037
  for (const entry of parseTranslationFile(file, content)) {
7068
7038
  if (!map.has(entry.key)) map.set(entry.key, entry.value);
@@ -7086,20 +7056,20 @@ function readText(file) {
7086
7056
  return void 0;
7087
7057
  }
7088
7058
  }
7089
- var CACHE_FILE2 = path21.join(".piwi", "locator-index.json");
7059
+ var CACHE_FILE2 = path22.join(".piwi", "locator-index.json");
7090
7060
  function cacheKey2(connection, branch) {
7091
7061
  return `${connection.serverUrl}|${connection.project}|${branch ?? ""}`;
7092
7062
  }
7093
7063
  function readCache2(root, key) {
7094
7064
  try {
7095
- const store = JSON.parse(fs18.readFileSync(path21.join(root, CACHE_FILE2), "utf-8"));
7065
+ const store = JSON.parse(fs18.readFileSync(path22.join(root, CACHE_FILE2), "utf-8"));
7096
7066
  return store[key] ?? null;
7097
7067
  } catch {
7098
7068
  return null;
7099
7069
  }
7100
7070
  }
7101
7071
  function writeCache2(root, key, index) {
7102
- const file = path21.join(root, CACHE_FILE2);
7072
+ const file = path22.join(root, CACHE_FILE2);
7103
7073
  try {
7104
7074
  let store = {};
7105
7075
  try {
@@ -7107,7 +7077,7 @@ function writeCache2(root, key, index) {
7107
7077
  } catch {
7108
7078
  }
7109
7079
  store[key] = { savedAt: (/* @__PURE__ */ new Date()).toISOString(), index };
7110
- fs18.mkdirSync(path21.dirname(file), { recursive: true });
7080
+ fs18.mkdirSync(path22.dirname(file), { recursive: true });
7111
7081
  fs18.writeFileSync(file, JSON.stringify(store));
7112
7082
  } catch {
7113
7083
  }
@@ -7124,7 +7094,7 @@ function planSites(b, testRoot) {
7124
7094
  const sites = [...new Set(b.uses.flatMap((u) => u.callSites))];
7125
7095
  return sites.map((callSite) => {
7126
7096
  if (!b.rewrite || !b.replacements?.length) return { callSite, action: "none" };
7127
- const file = path21.resolve(testRoot, callSiteFile(callSite));
7097
+ const file = path22.resolve(testRoot, callSiteFile(callSite));
7128
7098
  const lineNo = callSiteLine(callSite);
7129
7099
  const text = readText(file);
7130
7100
  if (text === void 0 || lineNo === null) return { callSite, action: "elsewhere" };
@@ -7140,8 +7110,8 @@ function planSites(b, testRoot) {
7140
7110
  }
7141
7111
  const holders = /* @__PURE__ */ new Set();
7142
7112
  for (const [before] of b.replacements) {
7143
- for (const quote4 of ["'", '"', "`"]) {
7144
- const out = tryGit(testRoot, ["grep", "-n", "--fixed-strings", `${quote4}${before}${quote4}`]);
7113
+ for (const quote3 of ["'", '"', "`"]) {
7114
+ const out = tryGit(testRoot, ["grep", "-n", "--fixed-strings", `${quote3}${before}${quote3}`]);
7145
7115
  for (const row of (out ?? "").split("\n")) {
7146
7116
  const m = /^(.+?):(\d+):/.exec(row);
7147
7117
  if (m) holders.add(`${m[1]}:${m[2]}`);
@@ -7288,7 +7258,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7288
7258
  console.error("piwi preflight: not inside a git repository");
7289
7259
  return EXIT_ERROR8;
7290
7260
  }
7291
- const testRoot = path21.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7261
+ const testRoot = path22.resolve(cwd, args.testRoot ?? findPlaywrightRoot(cwd) ?? cwd);
7292
7262
  let files;
7293
7263
  try {
7294
7264
  files = parseUnifiedDiff(git2(repoRoot, ["diff", "--unified=0", "--no-color", "--no-ext-diff", args.base, "--"]));
@@ -7325,7 +7295,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7325
7295
  testIdAttributes: index.testIdAttributes ?? void 0,
7326
7296
  isTestFile: (file) => TEST_FILE.test(file),
7327
7297
  translations: translationLookup(repoRoot, args.base, args.locale),
7328
- readFile: (file, side) => side === "new" ? readText(path21.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
7298
+ readFile: (file, side) => side === "new" ? readText(path22.join(repoRoot, file)) : tryGit(repoRoot, ["show", `${args.base}:${file}`])
7329
7299
  });
7330
7300
  const found = predictLocatorBreaks(anchors, index, { reach: await loadReach(connection, projectId, args.branch) });
7331
7301
  const replacementsOf = /* @__PURE__ */ new Map();
@@ -7370,8 +7340,8 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7370
7340
  if (args.json) console.log(JSON.stringify(result, null, 2));
7371
7341
  else {
7372
7342
  const rel = (file) => {
7373
- const abs = path21.isAbsolute(file) ? file : path21.resolve(testRoot, file);
7374
- const r = path21.relative(cwd, abs).split(path21.sep).join("/");
7343
+ const abs = path22.isAbsolute(file) ? file : path22.resolve(testRoot, file);
7344
+ const r = path22.relative(cwd, abs).split(path22.sep).join("/");
7375
7345
  return r.startsWith("..") ? file : r || file;
7376
7346
  };
7377
7347
  console.log(renderPreflight(result, rel));
@@ -7403,7 +7373,7 @@ async function runPreflight(argv, env = process.env, cwd = process.cwd()) {
7403
7373
  // src/cli/probe.ts
7404
7374
  var fs19 = __toESM(require("fs"));
7405
7375
  var os3 = __toESM(require("os"));
7406
- var path22 = __toESM(require("path"));
7376
+ var path23 = __toESM(require("path"));
7407
7377
  var EXIT_OK8 = 0;
7408
7378
  var EXIT_ERROR9 = 2;
7409
7379
  var USAGE11 = `
@@ -7428,9 +7398,9 @@ function escapeRegex2(s) {
7428
7398
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
7429
7399
  }
7430
7400
  async function runProbe(argv) {
7431
- const sep6 = argv.indexOf("--");
7432
- const own = sep6 < 0 ? argv : argv.slice(0, sep6);
7433
- const playwrightArgs2 = sep6 < 0 ? [] : argv.slice(sep6 + 1);
7401
+ const sep7 = argv.indexOf("--");
7402
+ const own = sep7 < 0 ? argv : argv.slice(0, sep7);
7403
+ const playwrightArgs2 = sep7 < 0 ? [] : argv.slice(sep7 + 1);
7434
7404
  if (own.includes("-h") || own.includes("--help")) {
7435
7405
  console.log(USAGE11);
7436
7406
  return EXIT_OK8;
@@ -7467,10 +7437,10 @@ async function runProbe(argv) {
7467
7437
  return EXIT_OK8;
7468
7438
  }
7469
7439
  console.error(`piwi probe: ${plan.items.length} pair(s) to probe (budget ${plan.budget}).`);
7470
- const dir = fs19.mkdtempSync(path22.join(os3.tmpdir(), "piwi-probe-"));
7471
- const planFile = path22.join(dir, "plan.json");
7472
- const resultsFile = path22.join(dir, "results.jsonl");
7473
- const runOutputFile = path22.join(dir, "run.json");
7440
+ const dir = fs19.mkdtempSync(path23.join(os3.tmpdir(), "piwi-probe-"));
7441
+ const planFile = path23.join(dir, "plan.json");
7442
+ const resultsFile = path23.join(dir, "results.jsonl");
7443
+ const runOutputFile = path23.join(dir, "run.json");
7474
7444
  fs19.writeFileSync(planFile, JSON.stringify(plan));
7475
7445
  fs19.writeFileSync(resultsFile, "");
7476
7446
  const grep = plan.items.map((i) => i.testTitle).filter(Boolean).map(escapeRegex2).join("|");