@piwitests/reporter 0.22.1 → 0.24.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/README.md +20 -0
- package/dist/cli/index.js +469 -83
- package/dist/global-setup-module.js +9 -1
- package/dist/index.d.ts +110 -2
- package/dist/index.js +885 -6
- package/dist/internal/capture/attachments.d.ts +2 -0
- package/dist/internal/capture/attachments.js +3 -1
- package/dist/internal/capture/capture-fixtures.js +3 -1
- package/package.json +3 -1
package/dist/cli/index.js
CHANGED
|
@@ -23,8 +23,391 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
23
23
|
mod
|
|
24
24
|
));
|
|
25
25
|
|
|
26
|
-
// src/cli/
|
|
26
|
+
// src/cli/ai.ts
|
|
27
|
+
var path5 = __toESM(require("path"));
|
|
28
|
+
var import_node_child_process = require("child_process");
|
|
29
|
+
|
|
30
|
+
// src/internal/ai/check.ts
|
|
31
|
+
var fs2 = __toESM(require("fs"));
|
|
32
|
+
var path4 = __toESM(require("path"));
|
|
33
|
+
|
|
34
|
+
// src/internal/ai/artifact.ts
|
|
27
35
|
var fs = __toESM(require("fs"));
|
|
36
|
+
var path2 = __toESM(require("path"));
|
|
37
|
+
|
|
38
|
+
// src/internal/capture/locator-healing.ts
|
|
39
|
+
var path = __toESM(require("path"));
|
|
40
|
+
|
|
41
|
+
// ../core/src/locator-methods.ts
|
|
42
|
+
var LOCATOR_BUILDER_METHODS = [
|
|
43
|
+
"getByRole",
|
|
44
|
+
"getByTestId",
|
|
45
|
+
"getByText",
|
|
46
|
+
"getByLabel",
|
|
47
|
+
"getByPlaceholder",
|
|
48
|
+
"getByAltText",
|
|
49
|
+
"getByTitle",
|
|
50
|
+
"locator"
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// src/internal/capture/locator-healing.ts
|
|
54
|
+
var LOCATOR_METHODS = [...LOCATOR_BUILDER_METHODS];
|
|
55
|
+
var ACTION_METHODS = [
|
|
56
|
+
"click",
|
|
57
|
+
"fill",
|
|
58
|
+
"check",
|
|
59
|
+
"uncheck",
|
|
60
|
+
"selectOption",
|
|
61
|
+
"dblclick",
|
|
62
|
+
"tap",
|
|
63
|
+
"hover",
|
|
64
|
+
"press",
|
|
65
|
+
"type",
|
|
66
|
+
"pressSequentially",
|
|
67
|
+
"clear",
|
|
68
|
+
"setInputFiles",
|
|
69
|
+
"dragTo",
|
|
70
|
+
"focus",
|
|
71
|
+
"blur",
|
|
72
|
+
"scrollIntoViewIfNeeded",
|
|
73
|
+
"dispatchEvent",
|
|
74
|
+
"selectText",
|
|
75
|
+
// Not an action, but a successful waitFor proves the element resolved — the
|
|
76
|
+
// closest capture hook available for assertion-style usage of a locator.
|
|
77
|
+
"waitFor"
|
|
78
|
+
];
|
|
79
|
+
var LOCATOR_CREATING_CHAINS = new Set(LOCATOR_METHODS);
|
|
80
|
+
|
|
81
|
+
// src/internal/ai/artifact.ts
|
|
82
|
+
var ARTIFACT_VERSION = 1;
|
|
83
|
+
var LOCATOR_METHOD_SET = new Set(LOCATOR_METHODS);
|
|
84
|
+
var ACTION_METHOD_SET = new Set(ACTION_METHODS);
|
|
85
|
+
var POSTCONDITION_ASSERTS = /* @__PURE__ */ new Set([
|
|
86
|
+
"visible",
|
|
87
|
+
"hidden",
|
|
88
|
+
"attached",
|
|
89
|
+
"url"
|
|
90
|
+
]);
|
|
91
|
+
function sortDeep(value) {
|
|
92
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
93
|
+
if (value !== null && typeof value === "object") {
|
|
94
|
+
const out = {};
|
|
95
|
+
for (const key of Object.keys(value).sort()) {
|
|
96
|
+
const v = value[key];
|
|
97
|
+
if (v !== void 0) out[key] = sortDeep(v);
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
function serializeEntry(entry) {
|
|
104
|
+
return `${JSON.stringify(sortDeep(entry), null, 2)}
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
107
|
+
var ArtifactError = class extends Error {
|
|
108
|
+
};
|
|
109
|
+
function assert(condition, message) {
|
|
110
|
+
if (!condition) throw new ArtifactError(message);
|
|
111
|
+
}
|
|
112
|
+
function validateStructuredLocator(value, where) {
|
|
113
|
+
assert(value !== null && typeof value === "object", `${where}: locator must be an object`);
|
|
114
|
+
const loc = value;
|
|
115
|
+
assert(typeof loc.method === "string", `${where}: locator.method must be a string`);
|
|
116
|
+
assert(LOCATOR_METHOD_SET.has(loc.method), `${where}: locator method "${String(loc.method)}" is not allowlisted`);
|
|
117
|
+
assert(Array.isArray(loc.args), `${where}: locator.args must be an array`);
|
|
118
|
+
if (loc.chain !== void 0) {
|
|
119
|
+
assert(Array.isArray(loc.chain), `${where}: locator.chain must be an array`);
|
|
120
|
+
loc.chain.forEach((child, i) => validateStructuredLocator(child, `${where}.chain[${i}]`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function validateRunStep(value, where) {
|
|
124
|
+
assert(value !== null && typeof value === "object", `${where}: step must be an object`);
|
|
125
|
+
const step = value;
|
|
126
|
+
validateStructuredLocator(step.locator, `${where}.locator`);
|
|
127
|
+
assert(typeof step.action === "string", `${where}: action must be a string`);
|
|
128
|
+
assert(ACTION_METHOD_SET.has(step.action), `${where}: action "${String(step.action)}" is not allowlisted`);
|
|
129
|
+
if (step.value !== void 0) assert(typeof step.value === "string", `${where}: value must be a string`);
|
|
130
|
+
if (step.optional !== void 0) assert(typeof step.optional === "boolean", `${where}: optional must be a boolean`);
|
|
131
|
+
if (step.waitForResponse !== void 0) {
|
|
132
|
+
assert(typeof step.waitForResponse === "string", `${where}: waitForResponse must be a string`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function validatePostcondition(value) {
|
|
136
|
+
assert(value !== null && typeof value === "object", "postcondition must be an object");
|
|
137
|
+
const post = value;
|
|
138
|
+
assert(
|
|
139
|
+
POSTCONDITION_ASSERTS.has(post.assert),
|
|
140
|
+
`postcondition.assert "${String(post.assert)}" is not supported`
|
|
141
|
+
);
|
|
142
|
+
if (post.assert === "url") {
|
|
143
|
+
assert(typeof post.url === "string", "postcondition.url must be a string for a url assert");
|
|
144
|
+
} else {
|
|
145
|
+
validateStructuredLocator(post.locator, "postcondition.locator");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function parseEntry(text) {
|
|
149
|
+
let raw;
|
|
150
|
+
try {
|
|
151
|
+
raw = JSON.parse(text);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new ArtifactError(`entry is not valid JSON: ${error.message}`);
|
|
154
|
+
}
|
|
155
|
+
assert(raw !== null && typeof raw === "object", "entry must be an object");
|
|
156
|
+
const obj = raw;
|
|
157
|
+
assert(obj.version === ARTIFACT_VERSION, `unsupported entry version ${String(obj.version)}`);
|
|
158
|
+
assert(typeof obj.template === "string", "entry.template must be a string");
|
|
159
|
+
if (obj.kind === "locator") {
|
|
160
|
+
validateStructuredLocator(obj.locator, "entry.locator");
|
|
161
|
+
return obj;
|
|
162
|
+
}
|
|
163
|
+
if (obj.kind === "run") {
|
|
164
|
+
assert(Array.isArray(obj.steps), "entry.steps must be an array");
|
|
165
|
+
obj.steps.forEach((step, i) => validateRunStep(step, `entry.steps[${i}]`));
|
|
166
|
+
validatePostcondition(obj.postcondition);
|
|
167
|
+
return obj;
|
|
168
|
+
}
|
|
169
|
+
throw new ArtifactError(`unsupported entry kind "${String(obj.kind)}"`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/internal/ai/keys.ts
|
|
173
|
+
var crypto = __toESM(require("crypto"));
|
|
174
|
+
var path3 = __toESM(require("path"));
|
|
175
|
+
var DEFAULT_AI_DIR = "__piwi__";
|
|
176
|
+
function normalizeTemplate(template) {
|
|
177
|
+
return template.trim().replace(/\s+/g, " ").toLowerCase();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/internal/ai/check.ts
|
|
181
|
+
function findEntryFiles(root, dir) {
|
|
182
|
+
const out = [];
|
|
183
|
+
const walk = (current) => {
|
|
184
|
+
let items;
|
|
185
|
+
try {
|
|
186
|
+
items = fs2.readdirSync(current, { withFileTypes: true });
|
|
187
|
+
} catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const item of items) {
|
|
191
|
+
const full = path4.join(current, item.name);
|
|
192
|
+
if (item.isDirectory()) {
|
|
193
|
+
walk(full);
|
|
194
|
+
} else if (item.isFile() && item.name.endsWith(".json")) {
|
|
195
|
+
const specFileDir = path4.dirname(full);
|
|
196
|
+
const dirName = path4.basename(path4.dirname(specFileDir));
|
|
197
|
+
if (dirName !== dir) continue;
|
|
198
|
+
const specDir = path4.dirname(path4.dirname(specFileDir));
|
|
199
|
+
const specFile = path4.join(specDir, path4.basename(specFileDir));
|
|
200
|
+
out.push({ file: full, specFile, testSlug: item.name.split(".")[0] });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
walk(root);
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
function readTextOr(file, fallback) {
|
|
208
|
+
try {
|
|
209
|
+
return fs2.readFileSync(file, "utf8");
|
|
210
|
+
} catch {
|
|
211
|
+
return fallback;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function checkAiTree(root, opts = {}) {
|
|
215
|
+
const dir = opts.dir ?? DEFAULT_AI_DIR;
|
|
216
|
+
const findings = [];
|
|
217
|
+
const rel = (file) => path4.relative(root, file).split(path4.sep).join("/");
|
|
218
|
+
const sourceCache = /* @__PURE__ */ new Map();
|
|
219
|
+
const duplicates = /* @__PURE__ */ new Map();
|
|
220
|
+
for (const found of findEntryFiles(root, dir)) {
|
|
221
|
+
const text = readTextOr(found.file, null);
|
|
222
|
+
if (text === null) continue;
|
|
223
|
+
let templateNormalized = null;
|
|
224
|
+
try {
|
|
225
|
+
const entry = parseEntry(text);
|
|
226
|
+
templateNormalized = normalizeTemplate(entry.template);
|
|
227
|
+
if (serializeEntry(entry) !== text) {
|
|
228
|
+
findings.push({
|
|
229
|
+
severity: "error",
|
|
230
|
+
kind: "non-canonical",
|
|
231
|
+
file: rel(found.file),
|
|
232
|
+
message: "file is not in canonical form \u2014 run `piwi ai prune --apply` or re-resolve to rewrite it"
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
if (!sourceCache.has(found.specFile)) sourceCache.set(found.specFile, readTextOr(found.specFile, null));
|
|
236
|
+
const source = sourceCache.get(found.specFile) ?? null;
|
|
237
|
+
if (source === null) {
|
|
238
|
+
findings.push({
|
|
239
|
+
severity: "error",
|
|
240
|
+
kind: "orphan",
|
|
241
|
+
file: rel(found.file),
|
|
242
|
+
message: `spec file is gone (${rel(found.specFile)}) \u2014 the entry is orphaned`
|
|
243
|
+
});
|
|
244
|
+
} else if (!source.includes(entry.template.trim())) {
|
|
245
|
+
findings.push({
|
|
246
|
+
severity: "error",
|
|
247
|
+
kind: "orphan",
|
|
248
|
+
file: rel(found.file),
|
|
249
|
+
message: `template "${entry.template}" no longer appears in ${rel(found.specFile)} \u2014 the entry is orphaned`
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
findings.push({
|
|
254
|
+
severity: "error",
|
|
255
|
+
kind: "invalid",
|
|
256
|
+
file: rel(found.file),
|
|
257
|
+
message: `not a valid entry: ${error.message}`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (templateNormalized !== null) {
|
|
261
|
+
const key = `${found.specFile}::${found.testSlug}::${templateNormalized}`;
|
|
262
|
+
const list = duplicates.get(key) ?? [];
|
|
263
|
+
list.push(rel(found.file));
|
|
264
|
+
duplicates.set(key, list);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const files of duplicates.values()) {
|
|
268
|
+
if (files.length < 2) continue;
|
|
269
|
+
for (const file of files) {
|
|
270
|
+
findings.push({
|
|
271
|
+
severity: "warning",
|
|
272
|
+
kind: "duplicate-template",
|
|
273
|
+
file,
|
|
274
|
+
message: `duplicate template within the same test \u2014 consider distinct phrasings (${files.join(", ")})`
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return findings.sort((a, b) => a.file < b.file ? -1 : a.file > b.file ? 1 : 0);
|
|
279
|
+
}
|
|
280
|
+
function hasBlockingFindings(findings) {
|
|
281
|
+
return findings.some((f) => f.severity === "error");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/cli/ai.ts
|
|
285
|
+
var EXIT_OK = 0;
|
|
286
|
+
var EXIT_ISSUES = 1;
|
|
287
|
+
var EXIT_ERROR = 2;
|
|
288
|
+
var USAGE = `
|
|
289
|
+
Usage: piwi ai <command> [options]
|
|
290
|
+
|
|
291
|
+
Commands:
|
|
292
|
+
check Scan committed AI-step entries for orphans, non-canonical files and
|
|
293
|
+
duplicate templates. Read-only; exits 1 when issues are found.
|
|
294
|
+
resolve Author missing entries by running the suite in resolve mode against
|
|
295
|
+
the configured authoring server (forces --workers=1).
|
|
296
|
+
prune Delete orphaned/dormant entries.
|
|
297
|
+
|
|
298
|
+
Options (check):
|
|
299
|
+
--dir <name> Entry directory name per spec (env PIWI_AI_DIR, default ${DEFAULT_AI_DIR})
|
|
300
|
+
--cwd <path> Root to scan (default: current directory)
|
|
301
|
+
--json Emit findings as JSON
|
|
302
|
+
|
|
303
|
+
Options (resolve):
|
|
304
|
+
--grep <re> Only author entries for matching tests
|
|
305
|
+
--project <name> Author under one Playwright project (a resolve profile)
|
|
306
|
+
--env K=V Extra env for the run (repeatable \u2014 flags/viewport profiles)
|
|
307
|
+
--update-ai Re-author entries that already exist
|
|
308
|
+
(needs PIWI_DASHBOARD_URL / PIWI_API_KEY for the server)
|
|
309
|
+
|
|
310
|
+
Exit codes:
|
|
311
|
+
0 clean (or --help)
|
|
312
|
+
1 hygiene issues found
|
|
313
|
+
2 bad arguments / command unavailable
|
|
314
|
+
`.trim();
|
|
315
|
+
function readOption(argv, name) {
|
|
316
|
+
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
317
|
+
if (withEquals) return withEquals.slice(name.length + 1);
|
|
318
|
+
const index = argv.indexOf(name);
|
|
319
|
+
if (index === -1) return void 0;
|
|
320
|
+
const value = argv[index + 1];
|
|
321
|
+
return value && !value.startsWith("--") ? value : void 0;
|
|
322
|
+
}
|
|
323
|
+
function readAll(argv, name) {
|
|
324
|
+
const values = [];
|
|
325
|
+
for (let i = 0; i < argv.length; i++) {
|
|
326
|
+
if (argv[i].startsWith(`${name}=`)) values.push(argv[i].slice(name.length + 1));
|
|
327
|
+
else if (argv[i] === name && argv[i + 1] && !argv[i + 1].startsWith("--")) values.push(argv[++i]);
|
|
328
|
+
}
|
|
329
|
+
return values;
|
|
330
|
+
}
|
|
331
|
+
function formatFinding(finding) {
|
|
332
|
+
const mark = finding.severity === "error" ? "\u2716" : "\u26A0";
|
|
333
|
+
return `${mark} ${finding.file}
|
|
334
|
+
${finding.kind}: ${finding.message}`;
|
|
335
|
+
}
|
|
336
|
+
function runCheck(argv, env) {
|
|
337
|
+
const root = path5.resolve(readOption(argv, "--cwd") ?? process.cwd());
|
|
338
|
+
const dir = readOption(argv, "--dir") ?? env.PIWI_AI_DIR ?? DEFAULT_AI_DIR;
|
|
339
|
+
let findings;
|
|
340
|
+
try {
|
|
341
|
+
findings = checkAiTree(root, { dir });
|
|
342
|
+
} catch (error) {
|
|
343
|
+
console.error(`piwi ai check: ${error.message}`);
|
|
344
|
+
return EXIT_ERROR;
|
|
345
|
+
}
|
|
346
|
+
if (argv.includes("--json")) {
|
|
347
|
+
console.log(JSON.stringify({ findings }, null, 2));
|
|
348
|
+
} else if (findings.length === 0) {
|
|
349
|
+
console.log("piwi ai check: no issues found.");
|
|
350
|
+
} else {
|
|
351
|
+
for (const finding of findings) console.log(formatFinding(finding));
|
|
352
|
+
const errors = findings.filter((f) => f.severity === "error").length;
|
|
353
|
+
const warnings = findings.length - errors;
|
|
354
|
+
console.log(`
|
|
355
|
+
${errors} error(s), ${warnings} warning(s).`);
|
|
356
|
+
}
|
|
357
|
+
return hasBlockingFindings(findings) ? EXIT_ISSUES : EXIT_OK;
|
|
358
|
+
}
|
|
359
|
+
function buildResolveInvocation(argv, env) {
|
|
360
|
+
const args = ["playwright", "test"];
|
|
361
|
+
const grep = readOption(argv, "--grep");
|
|
362
|
+
if (grep) args.push("--grep", grep);
|
|
363
|
+
const project = readOption(argv, "--project");
|
|
364
|
+
if (project) args.push("--project", project);
|
|
365
|
+
args.push("--workers=1");
|
|
366
|
+
const childEnv = { ...env, PIWI_AI: "resolve" };
|
|
367
|
+
if (argv.includes("--update-ai")) childEnv.PIWI_AI_UPDATE = "true";
|
|
368
|
+
for (const pair of readAll(argv, "--env")) {
|
|
369
|
+
const eq = pair.indexOf("=");
|
|
370
|
+
if (eq > 0) childEnv[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
371
|
+
}
|
|
372
|
+
return { command: "npx", args, env: childEnv };
|
|
373
|
+
}
|
|
374
|
+
function runResolve(argv, env) {
|
|
375
|
+
if (!env.PIWI_DASHBOARD_URL) {
|
|
376
|
+
console.error("piwi ai resolve: set PIWI_DASHBOARD_URL (the authoring server) before resolving.");
|
|
377
|
+
return EXIT_ERROR;
|
|
378
|
+
}
|
|
379
|
+
const invocation = buildResolveInvocation(argv, env);
|
|
380
|
+
const result = (0, import_node_child_process.spawnSync)(invocation.command, invocation.args, { stdio: "inherit", env: invocation.env });
|
|
381
|
+
if (result.error) {
|
|
382
|
+
console.error(`piwi ai resolve: ${result.error.message}`);
|
|
383
|
+
return EXIT_ERROR;
|
|
384
|
+
}
|
|
385
|
+
return result.status ?? EXIT_ERROR;
|
|
386
|
+
}
|
|
387
|
+
async function runAi(argv, env = process.env) {
|
|
388
|
+
const [sub, ...rest] = argv;
|
|
389
|
+
if (sub === void 0 || sub === "-h" || sub === "--help") {
|
|
390
|
+
console.log(USAGE);
|
|
391
|
+
return EXIT_OK;
|
|
392
|
+
}
|
|
393
|
+
switch (sub) {
|
|
394
|
+
case "check":
|
|
395
|
+
return runCheck(rest, env);
|
|
396
|
+
case "resolve":
|
|
397
|
+
return runResolve(rest, env);
|
|
398
|
+
case "prune":
|
|
399
|
+
console.error("piwi ai prune: three-tier cleanup is not available in this build yet.");
|
|
400
|
+
return EXIT_ERROR;
|
|
401
|
+
default:
|
|
402
|
+
console.error(`piwi ai: unknown command "${sub}"
|
|
403
|
+
`);
|
|
404
|
+
console.error(USAGE);
|
|
405
|
+
return EXIT_ERROR;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/cli/gate.ts
|
|
410
|
+
var fs3 = __toESM(require("fs"));
|
|
28
411
|
|
|
29
412
|
// ../core/src/gate.ts
|
|
30
413
|
function formatGateResult(result) {
|
|
@@ -44,10 +427,10 @@ function formatGateResult(result) {
|
|
|
44
427
|
}
|
|
45
428
|
|
|
46
429
|
// src/cli/gate.ts
|
|
47
|
-
var
|
|
430
|
+
var EXIT_OK2 = 0;
|
|
48
431
|
var EXIT_VIOLATED = 1;
|
|
49
|
-
var
|
|
50
|
-
var
|
|
432
|
+
var EXIT_ERROR2 = 2;
|
|
433
|
+
var USAGE2 = `
|
|
51
434
|
piwi gate \u2014 fail a CI job on the dashboard's analysis of a run
|
|
52
435
|
|
|
53
436
|
Usage:
|
|
@@ -77,7 +460,7 @@ Other:
|
|
|
77
460
|
|
|
78
461
|
Exit codes: 0 satisfied, 1 violated, 2 could not evaluate.
|
|
79
462
|
`.trim();
|
|
80
|
-
function
|
|
463
|
+
function readOption2(argv, name) {
|
|
81
464
|
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
82
465
|
if (withEquals) return withEquals.slice(name.length + 1);
|
|
83
466
|
const index = argv.indexOf(name);
|
|
@@ -86,15 +469,15 @@ function readOption(argv, name) {
|
|
|
86
469
|
return value && !value.startsWith("--") ? value : void 0;
|
|
87
470
|
}
|
|
88
471
|
function readCount(argv, name) {
|
|
89
|
-
const raw =
|
|
472
|
+
const raw = readOption2(argv, name);
|
|
90
473
|
if (raw === void 0) return void 0;
|
|
91
474
|
const n = Number(raw);
|
|
92
475
|
if (!Number.isFinite(n) || n < 0) throw new Error(`${name} expects a non-negative number, got "${raw}"`);
|
|
93
476
|
return Math.floor(n);
|
|
94
477
|
}
|
|
95
|
-
function readRunIdFromFile(
|
|
478
|
+
function readRunIdFromFile(path9) {
|
|
96
479
|
try {
|
|
97
|
-
const parsed = JSON.parse(
|
|
480
|
+
const parsed = JSON.parse(fs3.readFileSync(path9, "utf-8"));
|
|
98
481
|
const runId = Number(parsed.runId);
|
|
99
482
|
return Number.isFinite(runId) && runId > 0 ? runId : null;
|
|
100
483
|
} catch {
|
|
@@ -102,11 +485,11 @@ function readRunIdFromFile(path4) {
|
|
|
102
485
|
}
|
|
103
486
|
}
|
|
104
487
|
function parseGateArgs(argv, env) {
|
|
105
|
-
const serverUrl = (
|
|
488
|
+
const serverUrl = (readOption2(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? "").replace(/\/$/, "");
|
|
106
489
|
if (!serverUrl) throw new Error("No dashboard URL \u2014 pass --server-url or set PIWI_DASHBOARD_URL");
|
|
107
|
-
let runId = Number(
|
|
490
|
+
let runId = Number(readOption2(argv, "--run-id") ?? NaN);
|
|
108
491
|
if (!Number.isFinite(runId) || runId <= 0) {
|
|
109
|
-
const candidate =
|
|
492
|
+
const candidate = readOption2(argv, "--from-file") ?? env.PIWI_OUTPUT_FILE ?? "piwi-run.json";
|
|
110
493
|
runId = readRunIdFromFile(candidate) ?? NaN;
|
|
111
494
|
}
|
|
112
495
|
if (!Number.isFinite(runId) || runId <= 0) {
|
|
@@ -114,7 +497,7 @@ function parseGateArgs(argv, env) {
|
|
|
114
497
|
"No run to evaluate \u2014 pass --run-id, or set PIWI_OUTPUT_FILE so the reporter records the run it submitted"
|
|
115
498
|
);
|
|
116
499
|
}
|
|
117
|
-
const requireTags = (
|
|
500
|
+
const requireTags = (readOption2(argv, "--require-tag") ?? "").split(",").map((tag) => tag.trim().replace(/^@+/, "")).filter(Boolean);
|
|
118
501
|
const policy = {
|
|
119
502
|
requireTags,
|
|
120
503
|
maxFailed: readCount(argv, "--max-failed"),
|
|
@@ -123,7 +506,7 @@ function parseGateArgs(argv, env) {
|
|
|
123
506
|
maxQuarantined: readCount(argv, "--max-quarantined"),
|
|
124
507
|
failOnNewCluster: argv.includes("--fail-on-new-cluster")
|
|
125
508
|
};
|
|
126
|
-
return { serverUrl, apiKey:
|
|
509
|
+
return { serverUrl, apiKey: readOption2(argv, "--api-key") ?? env.PIWI_API_KEY ?? null, runId, policy };
|
|
127
510
|
}
|
|
128
511
|
async function requestGate(args) {
|
|
129
512
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -141,8 +524,8 @@ async function requestGate(args) {
|
|
|
141
524
|
}
|
|
142
525
|
async function runGate(argv, env = process.env) {
|
|
143
526
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
144
|
-
console.log(
|
|
145
|
-
return
|
|
527
|
+
console.log(USAGE2);
|
|
528
|
+
return EXIT_OK2;
|
|
146
529
|
}
|
|
147
530
|
let args;
|
|
148
531
|
try {
|
|
@@ -150,28 +533,28 @@ async function runGate(argv, env = process.env) {
|
|
|
150
533
|
} catch (e) {
|
|
151
534
|
console.error(`piwi gate: ${e.message}
|
|
152
535
|
`);
|
|
153
|
-
console.error(
|
|
154
|
-
return
|
|
536
|
+
console.error(USAGE2);
|
|
537
|
+
return EXIT_ERROR2;
|
|
155
538
|
}
|
|
156
539
|
let result;
|
|
157
540
|
try {
|
|
158
541
|
result = await requestGate(args);
|
|
159
542
|
} catch (e) {
|
|
160
543
|
console.error(`piwi gate: ${e.message}`);
|
|
161
|
-
return
|
|
544
|
+
return EXIT_ERROR2;
|
|
162
545
|
}
|
|
163
546
|
if (argv.includes("--json")) {
|
|
164
547
|
console.log(JSON.stringify(result, null, 2));
|
|
165
548
|
} else {
|
|
166
549
|
console.log(formatGateResult(result));
|
|
167
550
|
}
|
|
168
|
-
return result.passed ?
|
|
551
|
+
return result.passed ? EXIT_OK2 : EXIT_VIOLATED;
|
|
169
552
|
}
|
|
170
553
|
|
|
171
554
|
// src/cli/init.ts
|
|
172
|
-
var
|
|
173
|
-
var
|
|
174
|
-
var
|
|
555
|
+
var fs6 = __toESM(require("fs"));
|
|
556
|
+
var path8 = __toESM(require("path"));
|
|
557
|
+
var import_node_child_process2 = require("child_process");
|
|
175
558
|
|
|
176
559
|
// src/cli/report.ts
|
|
177
560
|
var STATUS_MARK = {
|
|
@@ -188,8 +571,8 @@ function formatStep(result) {
|
|
|
188
571
|
}
|
|
189
572
|
|
|
190
573
|
// src/cli/detect.ts
|
|
191
|
-
var
|
|
192
|
-
var
|
|
574
|
+
var fs4 = __toESM(require("fs"));
|
|
575
|
+
var path6 = __toESM(require("path"));
|
|
193
576
|
var CONFIG_NAMES = [
|
|
194
577
|
"playwright.config.ts",
|
|
195
578
|
"playwright.config.mts",
|
|
@@ -207,9 +590,9 @@ var LOCKFILES = [
|
|
|
207
590
|
];
|
|
208
591
|
var REPORTER_PACKAGE = "@piwitests/reporter";
|
|
209
592
|
function readPackageJson(root) {
|
|
210
|
-
const pkgPath =
|
|
593
|
+
const pkgPath = path6.join(root, "package.json");
|
|
211
594
|
try {
|
|
212
|
-
return { pkg: JSON.parse(
|
|
595
|
+
return { pkg: JSON.parse(fs4.readFileSync(pkgPath, "utf-8")), pkgPath };
|
|
213
596
|
} catch {
|
|
214
597
|
return { pkg: null, pkgPath: null };
|
|
215
598
|
}
|
|
@@ -218,14 +601,14 @@ function detectPackageManager(root, pkg) {
|
|
|
218
601
|
const declared = pkg?.packageManager?.split("@")[0];
|
|
219
602
|
if (declared === "pnpm" || declared === "yarn" || declared === "bun" || declared === "npm") return declared;
|
|
220
603
|
for (const [file, manager] of LOCKFILES) {
|
|
221
|
-
if (
|
|
604
|
+
if (fs4.existsSync(path6.join(root, file))) return manager;
|
|
222
605
|
}
|
|
223
606
|
return "npm";
|
|
224
607
|
}
|
|
225
608
|
function findConfig(root) {
|
|
226
609
|
for (const name of CONFIG_NAMES) {
|
|
227
|
-
const candidate =
|
|
228
|
-
if (
|
|
610
|
+
const candidate = path6.join(root, name);
|
|
611
|
+
if (fs4.existsSync(candidate)) return candidate;
|
|
229
612
|
}
|
|
230
613
|
return null;
|
|
231
614
|
}
|
|
@@ -235,14 +618,14 @@ function langOf(configPath) {
|
|
|
235
618
|
}
|
|
236
619
|
function suggestProjectName(root, pkg) {
|
|
237
620
|
const fromPkg = pkg?.name?.replace(/^@[^/]+\//, "").trim();
|
|
238
|
-
return fromPkg ||
|
|
621
|
+
return fromPkg || path6.basename(root) || "default-project";
|
|
239
622
|
}
|
|
240
623
|
function hasReporter(pkg) {
|
|
241
624
|
if (!pkg) return false;
|
|
242
625
|
return Boolean(pkg.dependencies?.[REPORTER_PACKAGE] || pkg.devDependencies?.[REPORTER_PACKAGE]);
|
|
243
626
|
}
|
|
244
627
|
function detectProject(root) {
|
|
245
|
-
const absRoot =
|
|
628
|
+
const absRoot = path6.resolve(root);
|
|
246
629
|
const { pkg, pkgPath } = readPackageJson(absRoot);
|
|
247
630
|
const configPath = findConfig(absRoot);
|
|
248
631
|
return {
|
|
@@ -384,21 +767,21 @@ function ensureGitignoreEntry(existing, entry = ".env") {
|
|
|
384
767
|
}
|
|
385
768
|
|
|
386
769
|
// src/cli/skills.ts
|
|
387
|
-
var
|
|
388
|
-
var
|
|
770
|
+
var fs5 = __toESM(require("fs"));
|
|
771
|
+
var path7 = __toESM(require("path"));
|
|
389
772
|
var SETUP_SKILL = "setup-piwi";
|
|
390
773
|
var WORKFLOW_SKILLS = ["investigate-failure", "apply-locator-healing", "stabilize-flaky-tests"];
|
|
391
774
|
var ALL_SKILLS = [SETUP_SKILL, ...WORKFLOW_SKILLS];
|
|
392
|
-
var DEFAULT_SKILLS_DIR =
|
|
775
|
+
var DEFAULT_SKILLS_DIR = path7.join(".claude", "skills");
|
|
393
776
|
function findTemplatesDir(fromDir) {
|
|
394
777
|
let dir = fromDir;
|
|
395
778
|
for (let i = 0; i < 6; i++) {
|
|
396
|
-
if (
|
|
397
|
-
const parent =
|
|
779
|
+
if (fs5.existsSync(path7.join(dir, "templates", "skills"))) return path7.join(dir, "templates");
|
|
780
|
+
const parent = path7.dirname(dir);
|
|
398
781
|
if (parent === dir) break;
|
|
399
782
|
dir = parent;
|
|
400
783
|
}
|
|
401
|
-
return
|
|
784
|
+
return path7.resolve(fromDir, "..", "..", "templates");
|
|
402
785
|
}
|
|
403
786
|
function readFrontMatter(markdown) {
|
|
404
787
|
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(markdown);
|
|
@@ -411,14 +794,14 @@ function readFrontMatter(markdown) {
|
|
|
411
794
|
return out;
|
|
412
795
|
}
|
|
413
796
|
function templatePath(templatesDir, slug) {
|
|
414
|
-
return
|
|
797
|
+
return path7.join(templatesDir, "skills", slug, "SKILL.md");
|
|
415
798
|
}
|
|
416
799
|
function listSkills(templatesDir, slugs = ALL_SKILLS) {
|
|
417
800
|
const infos = [];
|
|
418
801
|
for (const slug of slugs) {
|
|
419
802
|
const file = templatePath(templatesDir, slug);
|
|
420
|
-
if (!
|
|
421
|
-
const front = readFrontMatter(
|
|
803
|
+
if (!fs5.existsSync(file)) continue;
|
|
804
|
+
const front = readFrontMatter(fs5.readFileSync(file, "utf-8"));
|
|
422
805
|
infos.push({ slug, name: front.name ?? slug, description: front.description ?? "" });
|
|
423
806
|
}
|
|
424
807
|
return infos;
|
|
@@ -428,16 +811,16 @@ function installSkills(opts) {
|
|
|
428
811
|
for (const slug of opts.slugs) {
|
|
429
812
|
const step = `skill:${slug}`;
|
|
430
813
|
const source = templatePath(opts.templatesDir, slug);
|
|
431
|
-
const relDest =
|
|
432
|
-
const dest =
|
|
433
|
-
if (!
|
|
814
|
+
const relDest = path7.join(opts.skillsDir, slug, "SKILL.md");
|
|
815
|
+
const dest = path7.join(opts.root, relDest);
|
|
816
|
+
if (!fs5.existsSync(source)) {
|
|
434
817
|
results.push({ step, status: "error", detail: `no template found for "${slug}"` });
|
|
435
818
|
continue;
|
|
436
819
|
}
|
|
437
|
-
const contents =
|
|
438
|
-
const exists =
|
|
820
|
+
const contents = fs5.readFileSync(source, "utf-8");
|
|
821
|
+
const exists = fs5.existsSync(dest);
|
|
439
822
|
if (exists && !opts.force) {
|
|
440
|
-
const identical =
|
|
823
|
+
const identical = fs5.readFileSync(dest, "utf-8") === contents;
|
|
441
824
|
results.push({
|
|
442
825
|
step,
|
|
443
826
|
file: relDest,
|
|
@@ -447,14 +830,14 @@ function installSkills(opts) {
|
|
|
447
830
|
continue;
|
|
448
831
|
}
|
|
449
832
|
if (!opts.dryRun) {
|
|
450
|
-
|
|
451
|
-
|
|
833
|
+
fs5.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
834
|
+
fs5.writeFileSync(dest, contents);
|
|
452
835
|
}
|
|
453
836
|
results.push({ step, file: relDest, status: exists ? "updated" : "created", detail: "installed skill" });
|
|
454
837
|
}
|
|
455
838
|
return results;
|
|
456
839
|
}
|
|
457
|
-
var
|
|
840
|
+
var USAGE3 = `
|
|
458
841
|
piwi skills \u2014 install the Piwi agent skills into this project
|
|
459
842
|
|
|
460
843
|
Usage:
|
|
@@ -474,7 +857,7 @@ Options for "add":
|
|
|
474
857
|
|
|
475
858
|
Skills: ${ALL_SKILLS.join(", ")}
|
|
476
859
|
`.trim();
|
|
477
|
-
function
|
|
860
|
+
function readOption3(argv, name) {
|
|
478
861
|
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
479
862
|
if (withEquals) return withEquals.slice(name.length + 1);
|
|
480
863
|
const index = argv.indexOf(name);
|
|
@@ -493,7 +876,7 @@ function positionalSlugs(argv) {
|
|
|
493
876
|
function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
494
877
|
const [sub, ...rest] = argv;
|
|
495
878
|
if (sub === void 0 || sub === "-h" || sub === "--help") {
|
|
496
|
-
console.log(
|
|
879
|
+
console.log(USAGE3);
|
|
497
880
|
return 0;
|
|
498
881
|
}
|
|
499
882
|
if (sub === "list") {
|
|
@@ -514,13 +897,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
514
897
|
if (unknown.length) {
|
|
515
898
|
console.error(`piwi skills: unknown skill(s): ${unknown.join(", ")}
|
|
516
899
|
`);
|
|
517
|
-
console.error(
|
|
900
|
+
console.error(USAGE3);
|
|
518
901
|
return 2;
|
|
519
902
|
}
|
|
520
903
|
const results = installSkills({
|
|
521
904
|
templatesDir,
|
|
522
|
-
root:
|
|
523
|
-
skillsDir:
|
|
905
|
+
root: path7.resolve(readOption3(rest, "--cwd") ?? cwd),
|
|
906
|
+
skillsDir: readOption3(rest, "--dir") ?? DEFAULT_SKILLS_DIR,
|
|
524
907
|
slugs: requested.length ? requested : ALL_SKILLS,
|
|
525
908
|
force: rest.includes("--force"),
|
|
526
909
|
dryRun: rest.includes("--dry-run")
|
|
@@ -535,13 +918,13 @@ function runSkills(argv, templatesDir, cwd = process.cwd()) {
|
|
|
535
918
|
}
|
|
536
919
|
console.error(`piwi skills: unknown command "${sub}"
|
|
537
920
|
`);
|
|
538
|
-
console.error(
|
|
921
|
+
console.error(USAGE3);
|
|
539
922
|
return 2;
|
|
540
923
|
}
|
|
541
924
|
|
|
542
925
|
// src/cli/init.ts
|
|
543
926
|
var DEFAULT_SERVER_URL = "http://localhost:3000";
|
|
544
|
-
var
|
|
927
|
+
var USAGE4 = `
|
|
545
928
|
piwi init \u2014 wire a Playwright project up to a Piwi Dashboard
|
|
546
929
|
|
|
547
930
|
Usage:
|
|
@@ -574,7 +957,7 @@ Options:
|
|
|
574
957
|
|
|
575
958
|
Skills: ${ALL_SKILLS.join(", ")}
|
|
576
959
|
`.trim();
|
|
577
|
-
function
|
|
960
|
+
function readOption4(argv, name) {
|
|
578
961
|
const withEquals = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
579
962
|
if (withEquals) return withEquals.slice(name.length + 1);
|
|
580
963
|
const index = argv.indexOf(name);
|
|
@@ -590,15 +973,15 @@ function resolveSkillSlugs(raw) {
|
|
|
590
973
|
return raw.split(",").map((slug) => slug.trim()).filter(Boolean);
|
|
591
974
|
}
|
|
592
975
|
function parseInitArgs(argv, env, cwd) {
|
|
593
|
-
const root =
|
|
976
|
+
const root = path8.resolve(readOption4(argv, "--cwd") ?? cwd);
|
|
594
977
|
const detected = detectProject(root);
|
|
595
978
|
return {
|
|
596
979
|
root,
|
|
597
|
-
serverUrl: (
|
|
598
|
-
projectName:
|
|
599
|
-
apiKey:
|
|
600
|
-
skillsDir:
|
|
601
|
-
skillSlugs: resolveSkillSlugs(
|
|
980
|
+
serverUrl: (readOption4(argv, "--server-url") ?? env.PIWI_DASHBOARD_URL ?? DEFAULT_SERVER_URL).replace(/\/$/, ""),
|
|
981
|
+
projectName: readOption4(argv, "--project") ?? detected.suggestedProjectName,
|
|
982
|
+
apiKey: readOption4(argv, "--api-key") ?? env.PIWI_API_KEY ?? null,
|
|
983
|
+
skillsDir: readOption4(argv, "--skills-dir") ?? DEFAULT_SKILLS_DIR,
|
|
984
|
+
skillSlugs: resolveSkillSlugs(readOption4(argv, "--skills")),
|
|
602
985
|
install: !argv.includes("--no-install"),
|
|
603
986
|
force: argv.includes("--force"),
|
|
604
987
|
dryRun: argv.includes("--dry-run"),
|
|
@@ -609,7 +992,7 @@ function parseInitArgs(argv, env, cwd) {
|
|
|
609
992
|
}
|
|
610
993
|
function readFileOr(file, fallback) {
|
|
611
994
|
try {
|
|
612
|
-
return
|
|
995
|
+
return fs6.readFileSync(file, "utf-8");
|
|
613
996
|
} catch {
|
|
614
997
|
return fallback;
|
|
615
998
|
}
|
|
@@ -639,7 +1022,7 @@ function stepInstallReporter(project, opts) {
|
|
|
639
1022
|
if (opts.dryRun || !opts.install)
|
|
640
1023
|
return { step, status: "manual", detail: `Run \`${installCommand(project.packageManager)}\`` };
|
|
641
1024
|
const [command, args] = installArgv(project.packageManager);
|
|
642
|
-
const result = (0,
|
|
1025
|
+
const result = (0, import_node_child_process2.spawnSync)(command, args, { cwd: opts.root, stdio: "inherit" });
|
|
643
1026
|
if (result.status === 0) return { step, status: "updated", detail: `Installed ${REPORTER_PACKAGE}` };
|
|
644
1027
|
return {
|
|
645
1028
|
step,
|
|
@@ -655,19 +1038,19 @@ function stepConfig(project, opts) {
|
|
|
655
1038
|
status: "manual",
|
|
656
1039
|
detail: "No playwright.config found \u2014 create one, then re-run `npx @piwitests/reporter init`"
|
|
657
1040
|
};
|
|
658
|
-
const rel =
|
|
1041
|
+
const rel = path8.relative(project.root, project.configPath) || path8.basename(project.configPath);
|
|
659
1042
|
const edit = wrapPlaywrightConfig(readFileOr(project.configPath, ""), {
|
|
660
1043
|
serverUrl: opts.serverUrl,
|
|
661
1044
|
projectName: opts.projectName
|
|
662
1045
|
});
|
|
663
|
-
if (edit.status === "updated" && !opts.dryRun)
|
|
1046
|
+
if (edit.status === "updated" && !opts.dryRun) fs6.writeFileSync(project.configPath, edit.text);
|
|
664
1047
|
return { step, file: rel, status: edit.status, detail: edit.detail };
|
|
665
1048
|
}
|
|
666
1049
|
function stepFixtures(project, opts) {
|
|
667
1050
|
const step = "fixtures";
|
|
668
|
-
const rel =
|
|
669
|
-
const dest =
|
|
670
|
-
if (
|
|
1051
|
+
const rel = path8.join("tests", project.configLang === "js" ? "fixtures.js" : "fixtures.ts");
|
|
1052
|
+
const dest = path8.join(project.root, rel);
|
|
1053
|
+
if (fs6.existsSync(dest)) {
|
|
671
1054
|
const current = readFileOr(dest, "");
|
|
672
1055
|
if (/piwiFixtures/.test(current))
|
|
673
1056
|
return { step, file: rel, status: "already", detail: "capture fixtures already set up" };
|
|
@@ -679,8 +1062,8 @@ function stepFixtures(project, opts) {
|
|
|
679
1062
|
};
|
|
680
1063
|
}
|
|
681
1064
|
if (!opts.dryRun) {
|
|
682
|
-
|
|
683
|
-
|
|
1065
|
+
fs6.mkdirSync(path8.dirname(dest), { recursive: true });
|
|
1066
|
+
fs6.writeFileSync(dest, fixturesContents());
|
|
684
1067
|
}
|
|
685
1068
|
return {
|
|
686
1069
|
step,
|
|
@@ -691,12 +1074,12 @@ function stepFixtures(project, opts) {
|
|
|
691
1074
|
}
|
|
692
1075
|
function stepEnv(project, opts) {
|
|
693
1076
|
const results = [];
|
|
694
|
-
const examplePath =
|
|
1077
|
+
const examplePath = path8.join(project.root, ".env.example");
|
|
695
1078
|
const example = upsertEnvKeys(readFileOr(examplePath, ""), [
|
|
696
1079
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
697
1080
|
["PIWI_API_KEY", ""]
|
|
698
1081
|
]);
|
|
699
|
-
if (example.added.length && !opts.dryRun)
|
|
1082
|
+
if (example.added.length && !opts.dryRun) fs6.writeFileSync(examplePath, example.text);
|
|
700
1083
|
results.push({
|
|
701
1084
|
step: "env",
|
|
702
1085
|
file: ".env.example",
|
|
@@ -704,21 +1087,21 @@ function stepEnv(project, opts) {
|
|
|
704
1087
|
detail: example.added.length ? `Recorded ${example.added.join(", ")}` : "connection template already present"
|
|
705
1088
|
});
|
|
706
1089
|
if (opts.apiKey) {
|
|
707
|
-
const envPath =
|
|
1090
|
+
const envPath = path8.join(project.root, ".env");
|
|
708
1091
|
const env = upsertEnvKeys(readFileOr(envPath, ""), [
|
|
709
1092
|
["PIWI_DASHBOARD_URL", opts.serverUrl],
|
|
710
1093
|
["PIWI_API_KEY", opts.apiKey]
|
|
711
1094
|
]);
|
|
712
|
-
if (env.added.length && !opts.dryRun)
|
|
1095
|
+
if (env.added.length && !opts.dryRun) fs6.writeFileSync(envPath, env.text);
|
|
713
1096
|
results.push({
|
|
714
1097
|
step: "env",
|
|
715
1098
|
file: ".env",
|
|
716
1099
|
status: env.added.length ? "updated" : "already",
|
|
717
1100
|
detail: env.added.length ? `Wrote ${env.added.join(", ")} (keep .env out of git)` : "already set"
|
|
718
1101
|
});
|
|
719
|
-
const gitignorePath =
|
|
1102
|
+
const gitignorePath = path8.join(project.root, ".gitignore");
|
|
720
1103
|
const gitignore = ensureGitignoreEntry(readFileOr(gitignorePath, ""));
|
|
721
|
-
if (gitignore.added && !opts.dryRun)
|
|
1104
|
+
if (gitignore.added && !opts.dryRun) fs6.writeFileSync(gitignorePath, gitignore.text);
|
|
722
1105
|
results.push({
|
|
723
1106
|
step: "gitignore",
|
|
724
1107
|
file: ".gitignore",
|
|
@@ -747,7 +1130,7 @@ function nextSteps(opts, steps) {
|
|
|
747
1130
|
}
|
|
748
1131
|
async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
749
1132
|
if (argv.includes("-h") || argv.includes("--help")) {
|
|
750
|
-
console.log(
|
|
1133
|
+
console.log(USAGE4);
|
|
751
1134
|
return 0;
|
|
752
1135
|
}
|
|
753
1136
|
const opts = parseInitArgs(argv, env, cwd);
|
|
@@ -780,7 +1163,7 @@ async function runInit(argv, env = process.env, cwd = process.cwd()) {
|
|
|
780
1163
|
project: {
|
|
781
1164
|
root: project.root,
|
|
782
1165
|
packageManager: project.packageManager,
|
|
783
|
-
configPath: project.configPath ?
|
|
1166
|
+
configPath: project.configPath ? path8.relative(project.root, project.configPath) : null,
|
|
784
1167
|
projectName: opts.projectName,
|
|
785
1168
|
serverUrl: opts.serverUrl
|
|
786
1169
|
},
|
|
@@ -804,7 +1187,7 @@ Piwi setup${opts.dryRun ? " (dry run \u2014 nothing written)" : ""} for "${opts.
|
|
|
804
1187
|
}
|
|
805
1188
|
|
|
806
1189
|
// src/cli/index.ts
|
|
807
|
-
var
|
|
1190
|
+
var USAGE5 = `
|
|
808
1191
|
piwi \u2014 companion commands for the Piwi Dashboard reporter
|
|
809
1192
|
|
|
810
1193
|
Usage:
|
|
@@ -814,6 +1197,7 @@ Commands:
|
|
|
814
1197
|
init Wire a Playwright project up to a Piwi Dashboard
|
|
815
1198
|
skills Install the Piwi agent skills into this project
|
|
816
1199
|
gate Fail a CI job on the dashboard's analysis of a run
|
|
1200
|
+
ai Manage committed natural-language AI-step artifacts
|
|
817
1201
|
|
|
818
1202
|
Run \`npx @piwitests/reporter <command> --help\` for a command's options.
|
|
819
1203
|
(The published package is @piwitests/reporter; its command is piwi. Invoke it
|
|
@@ -828,15 +1212,17 @@ async function main() {
|
|
|
828
1212
|
return runSkills(rest, findTemplatesDir(__dirname));
|
|
829
1213
|
case "gate":
|
|
830
1214
|
return runGate(rest);
|
|
1215
|
+
case "ai":
|
|
1216
|
+
return runAi(rest);
|
|
831
1217
|
case void 0:
|
|
832
1218
|
case "-h":
|
|
833
1219
|
case "--help":
|
|
834
|
-
console.log(
|
|
1220
|
+
console.log(USAGE5);
|
|
835
1221
|
return 0;
|
|
836
1222
|
default:
|
|
837
1223
|
console.error(`piwi: unknown command "${command}"
|
|
838
1224
|
`);
|
|
839
|
-
console.error(
|
|
1225
|
+
console.error(USAGE5);
|
|
840
1226
|
return 2;
|
|
841
1227
|
}
|
|
842
1228
|
}
|