ptywright 0.3.0 → 0.5.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 +76 -31
- package/dist/agent.mjs +2 -2
- package/dist/bin/ptywright.mjs +1 -1
- package/dist/{cli-CfvlbRoZ.mjs → cli-PnG6UR43.mjs} +2390 -2309
- package/dist/cli.mjs +1 -1
- package/dist/config-bGg636EW.mjs +52 -0
- package/dist/config.mjs +2 -0
- package/dist/env-DPYHo-zH.mjs +36 -0
- package/dist/index.mjs +1 -1
- package/dist/manifest_files-DW80c1H7.mjs +77 -0
- package/dist/mcp.mjs +1 -1
- package/dist/pty-cassette.mjs +1 -1
- package/dist/{runner-zi0nItvB.mjs → runner-C1gPRyCM.mjs} +2002 -1038
- package/dist/{runner-zApMYWZx.mjs → runner-wW_DCBX7.mjs} +1576 -1422
- package/dist/script.mjs +1 -1
- package/dist/{server-BC3yo-dq.mjs → server-DMnnXjWv.mjs} +2643 -2527
- package/dist/session.mjs +1 -1
- package/dist/{terminal_session-DopC7Xg6.mjs → terminal_session-DJKr-O3X.mjs} +349 -328
- package/package.json +3 -1
- package/skills/ptywright-testing/SKILL.md +113 -79
- package/skills/ptywright-testing/agents/openai.yaml +4 -0
- package/skills/ptywright-testing/references/agent-regression.md +132 -0
- package/skills/ptywright-testing/references/ci-and-debugging.md +95 -0
- package/skills/ptywright-testing/references/mcp-tools.md +91 -0
- package/skills/ptywright-testing/references/raw-pty-cassettes.md +82 -0
- package/skills/ptywright-testing/references/script-runner.md +80 -0
- package/dist/{pty_like-Cpkh_O9B.mjs → pty_like-BjeBibSL.mjs} +2 -2
|
@@ -1,223 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as sameArgv, n as validateManifestFiles, r as formatZodIssues, t as collectManifestFiles } from "./manifest_files-DW80c1H7.mjs";
|
|
2
|
+
import { t as envTruthy } from "./env-DPYHo-zH.mjs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
5
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
6
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { chromium } from "playwright";
|
|
7
|
+
import { z } from "zod";
|
|
7
8
|
import { createServer } from "node:http";
|
|
9
|
+
import { chromium } from "playwright";
|
|
8
10
|
import { spawn } from "node:child_process";
|
|
9
|
-
//#region src/agent/manifest.ts
|
|
10
|
-
const AGENT_MANIFEST_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json";
|
|
11
|
-
const AGENT_MANIFEST_FILE_NAME = "ptywright-agent.manifest.json";
|
|
12
|
-
const agentManifestKindSchema = z.enum([
|
|
13
|
-
"run",
|
|
14
|
-
"replay-suite",
|
|
15
|
-
"check",
|
|
16
|
-
"promote"
|
|
17
|
-
]);
|
|
18
|
-
const agentManifestFileKindSchema = z.enum([
|
|
19
|
-
"flow",
|
|
20
|
-
"cassette",
|
|
21
|
-
"run-record",
|
|
22
|
-
"replay-summary",
|
|
23
|
-
"check-summary",
|
|
24
|
-
"promote-summary",
|
|
25
|
-
"report",
|
|
26
|
-
"terminal",
|
|
27
|
-
"dom",
|
|
28
|
-
"screenshot",
|
|
29
|
-
"diff"
|
|
30
|
-
]);
|
|
31
|
-
const agentManifestCommandSchema = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
|
|
32
|
-
const agentManifestValidationStageSchema = z.object({
|
|
33
|
-
name: z.string().min(1),
|
|
34
|
-
ok: z.boolean(),
|
|
35
|
-
totalCount: z.number().int().nonnegative(),
|
|
36
|
-
failureCount: z.number().int().nonnegative()
|
|
37
|
-
}).strict();
|
|
38
|
-
const agentManifestValidationSchema = z.object({
|
|
39
|
-
ok: z.boolean(),
|
|
40
|
-
stages: z.array(agentManifestValidationStageSchema)
|
|
41
|
-
}).strict().superRefine((validation, ctx) => {
|
|
42
|
-
const ok = validation.stages.every((stage) => stage.ok && stage.failureCount === 0);
|
|
43
|
-
if (validation.ok !== ok) ctx.addIssue({
|
|
44
|
-
code: z.ZodIssueCode.custom,
|
|
45
|
-
path: ["ok"],
|
|
46
|
-
message: "validation ok must match validation stages"
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
const agentManifestFileSchema = z.object({
|
|
50
|
-
path: z.string().min(1),
|
|
51
|
-
kind: agentManifestFileKindSchema,
|
|
52
|
-
role: z.string().min(1).optional(),
|
|
53
|
-
ok: z.boolean().optional(),
|
|
54
|
-
bytes: z.number().int().nonnegative(),
|
|
55
|
-
sha256: z.string().regex(/^[a-f0-9]{64}$/)
|
|
56
|
-
}).strict();
|
|
57
|
-
const agentManifestSchema = z.object({
|
|
58
|
-
$schema: z.string().optional(),
|
|
59
|
-
version: z.literal(1),
|
|
60
|
-
kind: agentManifestKindSchema,
|
|
61
|
-
ok: z.boolean(),
|
|
62
|
-
generatedAt: z.string().min(1),
|
|
63
|
-
rootDir: z.string().min(1),
|
|
64
|
-
primaryPath: z.string().min(1),
|
|
65
|
-
commands: z.record(agentManifestCommandSchema),
|
|
66
|
-
validation: agentManifestValidationSchema.optional(),
|
|
67
|
-
files: z.array(agentManifestFileSchema)
|
|
68
|
-
}).strict().superRefine((manifest, ctx) => {
|
|
69
|
-
const seen = /* @__PURE__ */ new Set();
|
|
70
|
-
for (const file of manifest.files) {
|
|
71
|
-
if (seen.has(file.path)) ctx.addIssue({
|
|
72
|
-
code: z.ZodIssueCode.custom,
|
|
73
|
-
path: ["files"],
|
|
74
|
-
message: `duplicate manifest file path: ${file.path}`
|
|
75
|
-
});
|
|
76
|
-
seen.add(file.path);
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
function agentManifestPath(rootDir) {
|
|
80
|
-
return join(rootDir, AGENT_MANIFEST_FILE_NAME);
|
|
81
|
-
}
|
|
82
|
-
function createAgentManifest(options) {
|
|
83
|
-
return normalizeAgentManifest({
|
|
84
|
-
$schema: AGENT_MANIFEST_SCHEMA_URL,
|
|
85
|
-
version: 1,
|
|
86
|
-
kind: options.kind,
|
|
87
|
-
ok: options.ok,
|
|
88
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
89
|
-
rootDir: options.rootDir,
|
|
90
|
-
primaryPath: options.primaryPath,
|
|
91
|
-
commands: options.commands,
|
|
92
|
-
validation: options.validation,
|
|
93
|
-
files: collectManifestFiles(options.files, options.rootDir)
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function readAgentManifestPath(path) {
|
|
97
|
-
return normalizeAgentManifest(JSON.parse(readFileSync(path, "utf8")));
|
|
98
|
-
}
|
|
99
|
-
function writeAgentManifestPath(path, options) {
|
|
100
|
-
const manifest = createAgentManifest(options);
|
|
101
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
102
|
-
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
103
|
-
return manifest;
|
|
104
|
-
}
|
|
105
|
-
function normalizeAgentManifest(input) {
|
|
106
|
-
try {
|
|
107
|
-
const parsed = agentManifestSchema.parse(input);
|
|
108
|
-
return {
|
|
109
|
-
...parsed,
|
|
110
|
-
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json"
|
|
111
|
-
};
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if (error instanceof z.ZodError) throw new Error(`invalid agent manifest: ${formatZodIssues$1(error)}`);
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function validateAgentManifestFiles(manifest, manifestPath) {
|
|
118
|
-
const failures = [];
|
|
119
|
-
const baseDir = manifestPath ? dirname(resolve(process.cwd(), manifestPath)) : resolve(process.cwd(), manifest.rootDir);
|
|
120
|
-
for (const file of manifest.files) {
|
|
121
|
-
let current = null;
|
|
122
|
-
try {
|
|
123
|
-
current = readManifestFile(file, baseDir);
|
|
124
|
-
} catch (error) {
|
|
125
|
-
failures.push(`${file.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (current.bytes !== file.bytes) failures.push(`${file.path}: bytes ${current.bytes} !== ${file.bytes}`);
|
|
129
|
-
if (current.sha256 !== file.sha256) failures.push(`${file.path}: sha256 ${current.sha256} !== ${file.sha256}`);
|
|
130
|
-
}
|
|
131
|
-
if (failures.length > 0) throw new Error(`invalid agent manifest files: ${failures.join("; ")}`);
|
|
132
|
-
}
|
|
133
|
-
function isAgentManifestLike(input) {
|
|
134
|
-
return typeof input === "object" && input !== null && input.version === 1 && typeof input.kind === "string" && Array.isArray(input.files) && typeof input.commands === "object";
|
|
135
|
-
}
|
|
136
|
-
function collectManifestFiles(files, rootDir) {
|
|
137
|
-
const out = [];
|
|
138
|
-
const seen = /* @__PURE__ */ new Set();
|
|
139
|
-
const rootAbs = resolve(process.cwd(), rootDir);
|
|
140
|
-
for (const file of files) {
|
|
141
|
-
if (!file.path || seen.has(file.path)) continue;
|
|
142
|
-
seen.add(file.path);
|
|
143
|
-
try {
|
|
144
|
-
out.push(readManifestFile(file, rootAbs, { portableRoot: true }));
|
|
145
|
-
} catch {}
|
|
146
|
-
}
|
|
147
|
-
return out.sort((a, b) => a.path.localeCompare(b.path));
|
|
148
|
-
}
|
|
149
|
-
function readManifestFile(file, baseDir, options = {}) {
|
|
150
|
-
if (!file.path) throw new Error("missing file path");
|
|
151
|
-
const absPath = isAbsolute(file.path) ? file.path : resolve(options.portableRoot ? process.cwd() : baseDir, file.path);
|
|
152
|
-
if (!statSync(absPath).isFile()) throw new Error("not a file");
|
|
153
|
-
const bytes = readFileSync(absPath);
|
|
154
|
-
return {
|
|
155
|
-
path: options.portableRoot ? portableManifestPath(absPath, baseDir) : file.path,
|
|
156
|
-
kind: file.kind,
|
|
157
|
-
role: file.role,
|
|
158
|
-
ok: file.ok,
|
|
159
|
-
bytes: bytes.byteLength,
|
|
160
|
-
sha256: createHash("sha256").update(bytes).digest("hex")
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
function portableManifestPath(absPath, rootAbs) {
|
|
164
|
-
const rel = relative(rootAbs, absPath);
|
|
165
|
-
if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return rel;
|
|
166
|
-
return absPath;
|
|
167
|
-
}
|
|
168
|
-
function formatZodIssues$1(error) {
|
|
169
|
-
return error.issues.map((issue) => {
|
|
170
|
-
return `${issue.path.length ? issue.path.join(".") : "<root>"}: ${issue.message}`;
|
|
171
|
-
}).join("; ");
|
|
172
|
-
}
|
|
173
|
-
//#endregion
|
|
174
|
-
//#region src/agent/browser.ts
|
|
175
|
-
async function launchAgentBrowser(args) {
|
|
176
|
-
let lastError;
|
|
177
|
-
for (let attempt = 0; attempt < 5; attempt += 1) try {
|
|
178
|
-
const browser = await chromium.launch({ headless: args.headless });
|
|
179
|
-
await verifyBrowserLaunch(browser);
|
|
180
|
-
return browser;
|
|
181
|
-
} catch (error) {
|
|
182
|
-
lastError = error;
|
|
183
|
-
if (!isTransientBrowserLaunchError(error) || attempt === 4) throw error;
|
|
184
|
-
await new Promise((resolveRetry) => setTimeout(resolveRetry, 250 * (attempt + 1)));
|
|
185
|
-
}
|
|
186
|
-
throw lastError;
|
|
187
|
-
}
|
|
188
|
-
async function verifyBrowserLaunch(browser) {
|
|
189
|
-
const context = await browser.newContext();
|
|
190
|
-
try {
|
|
191
|
-
const page = await context.newPage();
|
|
192
|
-
await page.goto("data:text/html,<title>ptywright</title>", { waitUntil: "load" });
|
|
193
|
-
await page.close();
|
|
194
|
-
} catch (error) {
|
|
195
|
-
await browser.close();
|
|
196
|
-
throw error;
|
|
197
|
-
} finally {
|
|
198
|
-
await context.close().catch(() => void 0);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
function isTransientBrowserLaunchError(error) {
|
|
202
|
-
const fields = errorFields(error);
|
|
203
|
-
return fields.message.includes("Failed to connect") || fields.message.includes("Target page, context or browser has been closed") || fields.message.includes("browserType.launch") || fields.code === "ENOENT" || fields.code === "ECONNREFUSED" || fields.syscall === "connect";
|
|
204
|
-
}
|
|
205
|
-
function errorFields(error) {
|
|
206
|
-
const record = typeof error === "object" && error !== null ? error : {};
|
|
207
|
-
return {
|
|
208
|
-
message: error instanceof Error ? error.message : String(error),
|
|
209
|
-
code: typeof record.code === "string" ? record.code : "",
|
|
210
|
-
syscall: typeof record.syscall === "string" ? record.syscall : ""
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
//#endregion
|
|
214
11
|
//#region src/agent/normalize.ts
|
|
215
|
-
function applyAgentMasks(input, rules = []) {
|
|
12
|
+
function applyAgentMasks(input, rules = [], options = {}) {
|
|
216
13
|
let out = input;
|
|
217
14
|
for (const rule of rules) {
|
|
218
15
|
const flags = rule.flags ?? "g";
|
|
219
16
|
const regex = new RegExp(rule.regex, flags.includes("g") ? flags : `${flags}g`);
|
|
220
|
-
const replacement = rule.replacement ?? "<masked>";
|
|
17
|
+
const replacement = options.replacement?.(rule.replacement ?? "<masked>") ?? rule.replacement ?? "<masked>";
|
|
221
18
|
out = out.replace(regex, (match) => {
|
|
222
19
|
if (!rule.preserveLength) return replacement;
|
|
223
20
|
if (replacement.length === match.length) return replacement;
|
|
@@ -234,7 +31,7 @@ function normalizeTerminalText(input, rules = []) {
|
|
|
234
31
|
return lines.join("\n");
|
|
235
32
|
}
|
|
236
33
|
function normalizeDomSnapshot(input, rules = []) {
|
|
237
|
-
return applyAgentMasks(input.replace(/\sdata-v-[a-z0-9-]+="[^"]*"/g, "").replace(/\sstyle="[^"]*--term-(?:cell-width|row-height):[^"]*"/g, "").replace(/\s+/g, " ").replace(/>\s+</g, "><").replace(/<div class="[^"]*\bterm-row\b[^"]*\bterm-scrollback-row\b[^"]*"[^>]*><span[^>]*><\/span><\/div>/g, "").trim(), rules);
|
|
34
|
+
return applyAgentMasks(input.replace(/\sdata-v-[a-z0-9-]+="[^"]*"/g, "").replace(/\sstyle="[^"]*--term-(?:cell-width|row-height):[^"]*"/g, "").replace(/\s+/g, " ").replace(/>\s+</g, "><").replace(/<div class="[^"]*\bterm-row\b[^"]*\bterm-scrollback-row\b[^"]*"[^>]*><span[^>]*><\/span><\/div>/g, "").trim(), rules, { replacement: escapeHtmlText });
|
|
238
35
|
}
|
|
239
36
|
function sanitizeArtifactName(input) {
|
|
240
37
|
return input.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "artifact";
|
|
@@ -247,6 +44,9 @@ function shortHash(input) {
|
|
|
247
44
|
}
|
|
248
45
|
return hash.toString(16).padStart(8, "0");
|
|
249
46
|
}
|
|
47
|
+
function escapeHtmlText(input) {
|
|
48
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
49
|
+
}
|
|
250
50
|
//#endregion
|
|
251
51
|
//#region src/agent/schema.ts
|
|
252
52
|
const agentTextMaskRuleSchema = z.object({
|
|
@@ -394,73 +194,15 @@ function resolveAgentLaunchMode(launch) {
|
|
|
394
194
|
return inferAgentLaunchMode(launch);
|
|
395
195
|
}
|
|
396
196
|
//#endregion
|
|
397
|
-
//#region src/agent/
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
viewport: agentViewportSchema,
|
|
401
|
-
phase: z.number().int().nonnegative(),
|
|
402
|
-
stepIndex: z.number().int().nonnegative().nullable(),
|
|
403
|
-
stepType: z.string().min(1),
|
|
404
|
-
terminalText: z.string(),
|
|
405
|
-
terminalHash: z.string().min(1),
|
|
406
|
-
dom: z.string(),
|
|
407
|
-
domHash: z.string().min(1),
|
|
408
|
-
capturedAt: z.string().min(1)
|
|
409
|
-
});
|
|
410
|
-
const agentCassetteSchema = z.object({
|
|
411
|
-
$schema: z.string().optional(),
|
|
412
|
-
version: z.literal(1),
|
|
413
|
-
name: z.string().min(1),
|
|
414
|
-
createdAt: z.string().min(1),
|
|
415
|
-
spec: agentFlowSpecSchema.optional(),
|
|
416
|
-
frames: z.array(agentCassetteFrameSchema).min(1)
|
|
417
|
-
});
|
|
418
|
-
function createAgentCassette(name, spec) {
|
|
419
|
-
return {
|
|
420
|
-
$schema: AGENT_CASSETTE_SCHEMA_URL,
|
|
421
|
-
version: 1,
|
|
422
|
-
name,
|
|
423
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
424
|
-
spec: normalizeAgentFlowSpec(spec),
|
|
425
|
-
frames: []
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
function normalizeAgentCassette(input, fallbackSpec) {
|
|
429
|
-
const parsed = agentCassetteSchema.parse(input);
|
|
430
|
-
const specInput = parsed.spec ?? fallbackSpec;
|
|
431
|
-
if (!specInput) throw new Error("invalid agent cassette: missing spec");
|
|
432
|
-
validateCassetteFrameHashes(parsed.frames);
|
|
433
|
-
return {
|
|
434
|
-
...parsed,
|
|
435
|
-
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json",
|
|
436
|
-
spec: normalizeAgentFlowSpec(specInput)
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
function validateCassetteFrameHashes(frames) {
|
|
440
|
-
for (const frame of frames) {
|
|
441
|
-
if (shortHash(frame.terminalText) !== frame.terminalHash) throw new Error(`invalid agent cassette: terminal hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
442
|
-
if (shortHash(frame.dom) !== frame.domHash) throw new Error(`invalid agent cassette: dom hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
function readAgentCassettePath(path, fallbackSpec) {
|
|
446
|
-
return normalizeAgentCassette(JSON.parse(readFileSync(path, "utf8")), fallbackSpec);
|
|
447
|
-
}
|
|
448
|
-
function isAgentCassetteLike(input) {
|
|
449
|
-
return typeof input === "object" && input !== null && input.version === 1 && Array.isArray(input.frames);
|
|
197
|
+
//#region src/agent/html_escape.ts
|
|
198
|
+
function escapeHtml(input) {
|
|
199
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
450
200
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
...frame,
|
|
454
|
-
terminalHash: shortHash(frame.terminalText),
|
|
455
|
-
domHash: shortHash(frame.dom)
|
|
456
|
-
};
|
|
457
|
-
const index = cassette.frames.findIndex((candidate) => candidate.viewport.name === next.viewport.name && candidate.phase === next.phase);
|
|
458
|
-
if (index >= 0) {
|
|
459
|
-
cassette.frames[index] = next;
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
cassette.frames.push(next);
|
|
201
|
+
function escapeAttribute(input) {
|
|
202
|
+
return escapeHtml(input).replace(/'/g, "'");
|
|
463
203
|
}
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/agent/cassette_server.ts
|
|
464
206
|
async function startAgentCassetteServer(cassette) {
|
|
465
207
|
const sockets = /* @__PURE__ */ new Set();
|
|
466
208
|
const server = createServer((request, response) => {
|
|
@@ -496,7 +238,7 @@ async function startAgentCassetteServer(cassette) {
|
|
|
496
238
|
});
|
|
497
239
|
const address = server.address();
|
|
498
240
|
if (!address || typeof address === "string") {
|
|
499
|
-
await closeServer(server);
|
|
241
|
+
await closeServer(server, sockets);
|
|
500
242
|
throw new Error("failed to bind cassette replay server");
|
|
501
243
|
}
|
|
502
244
|
return {
|
|
@@ -511,7 +253,7 @@ function renderCassetteHtml(cassette) {
|
|
|
511
253
|
<head>
|
|
512
254
|
<meta charset="utf-8" />
|
|
513
255
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
514
|
-
<title>${escapeHtml
|
|
256
|
+
<title>${escapeHtml(cassette.name)} cassette replay</title>
|
|
515
257
|
<style>
|
|
516
258
|
:root {
|
|
517
259
|
color-scheme: dark;
|
|
@@ -542,10 +284,25 @@ function renderCassetteHtml(cassette) {
|
|
|
542
284
|
const root = document.querySelector("[data-terminal-root]");
|
|
543
285
|
let phase = 0;
|
|
544
286
|
|
|
287
|
+
function queryViewport() {
|
|
288
|
+
const params = new URLSearchParams(window.location.search);
|
|
289
|
+
const width = Number.parseInt(params.get("viewportWidth") || "", 10);
|
|
290
|
+
const height = Number.parseInt(params.get("viewportHeight") || "", 10);
|
|
291
|
+
const name = params.get("viewportName") || "";
|
|
292
|
+
return {
|
|
293
|
+
height: Number.isFinite(height) && height > 0 ? height : window.innerHeight,
|
|
294
|
+
name,
|
|
295
|
+
width: Number.isFinite(width) && width > 0 ? width : window.innerWidth,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
545
299
|
function viewportScore(frame) {
|
|
546
300
|
const viewport = frame.viewport || {};
|
|
547
|
-
|
|
548
|
-
|
|
301
|
+
const target = queryViewport();
|
|
302
|
+
const namePenalty = target.name && viewport.name !== target.name ? 100000 : 0;
|
|
303
|
+
return namePenalty +
|
|
304
|
+
Math.abs((viewport.width || target.width) - target.width) +
|
|
305
|
+
Math.abs((viewport.height || target.height) - target.height);
|
|
549
306
|
}
|
|
550
307
|
|
|
551
308
|
function chooseFrame() {
|
|
@@ -620,130 +377,340 @@ async function closeServer(server, sockets) {
|
|
|
620
377
|
});
|
|
621
378
|
});
|
|
622
379
|
}
|
|
623
|
-
function escapeHtml$1(input) {
|
|
624
|
-
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
625
|
-
}
|
|
626
380
|
//#endregion
|
|
627
|
-
//#region src/agent/
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
381
|
+
//#region src/agent/cassette.ts
|
|
382
|
+
const AGENT_CASSETTE_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json";
|
|
383
|
+
const agentCassetteFrameSchema = z.object({
|
|
384
|
+
viewport: agentViewportSchema,
|
|
385
|
+
phase: z.number().int().nonnegative(),
|
|
386
|
+
stepIndex: z.number().int().nonnegative().nullable(),
|
|
387
|
+
stepType: z.string().min(1),
|
|
388
|
+
terminalText: z.string(),
|
|
389
|
+
terminalHash: z.string().min(1),
|
|
390
|
+
dom: z.string(),
|
|
391
|
+
domHash: z.string().min(1),
|
|
392
|
+
capturedAt: z.string().min(1)
|
|
393
|
+
});
|
|
394
|
+
const agentCassetteSchema = z.object({
|
|
395
|
+
$schema: z.string().optional(),
|
|
396
|
+
version: z.literal(1),
|
|
397
|
+
name: z.string().min(1),
|
|
398
|
+
createdAt: z.string().min(1),
|
|
399
|
+
spec: agentFlowSpecSchema.optional(),
|
|
400
|
+
frames: z.array(agentCassetteFrameSchema).min(1)
|
|
401
|
+
});
|
|
402
|
+
function createAgentCassette(name, spec) {
|
|
403
|
+
return {
|
|
404
|
+
$schema: AGENT_CASSETTE_SCHEMA_URL,
|
|
405
|
+
version: 1,
|
|
406
|
+
name,
|
|
407
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
408
|
+
spec: normalizeAgentFlowSpec(spec),
|
|
409
|
+
frames: []
|
|
644
410
|
};
|
|
645
411
|
}
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
stdio: [
|
|
652
|
-
"ignore",
|
|
653
|
-
"pipe",
|
|
654
|
-
"pipe"
|
|
655
|
-
]
|
|
656
|
-
});
|
|
657
|
-
const stdoutChunks = [];
|
|
658
|
-
const stderrChunks = [];
|
|
412
|
+
function normalizeAgentCassette(input, fallbackSpec) {
|
|
413
|
+
const parsed = agentCassetteSchema.parse(input);
|
|
414
|
+
const specInput = parsed.spec ?? fallbackSpec;
|
|
415
|
+
if (!specInput) throw new Error("invalid agent cassette: missing spec");
|
|
416
|
+
validateCassetteFrameHashes(parsed.frames);
|
|
659
417
|
return {
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
readUrl();
|
|
683
|
-
};
|
|
684
|
-
const onStderr = (chunk) => {
|
|
685
|
-
stderrChunks.push(chunk.toString("utf8"));
|
|
686
|
-
readUrl();
|
|
687
|
-
};
|
|
688
|
-
const onError = (error) => finish(error);
|
|
689
|
-
const onExit = (code, signal) => {
|
|
690
|
-
finish(/* @__PURE__ */ new Error(`${command.label ?? command.file} exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
691
|
-
};
|
|
692
|
-
child.stdout.on("data", onStdout);
|
|
693
|
-
child.stderr.on("data", onStderr);
|
|
694
|
-
child.once("error", onError);
|
|
695
|
-
child.once("exit", onExit);
|
|
696
|
-
}),
|
|
697
|
-
process: child,
|
|
698
|
-
close: () => closeChild(child)
|
|
418
|
+
...parsed,
|
|
419
|
+
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-cassette.schema.json",
|
|
420
|
+
spec: normalizeAgentFlowSpec(specInput)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function validateCassetteFrameHashes(frames) {
|
|
424
|
+
for (const frame of frames) {
|
|
425
|
+
if (shortHash(frame.terminalText) !== frame.terminalHash) throw new Error(`invalid agent cassette: terminal hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
426
|
+
if (shortHash(frame.dom) !== frame.domHash) throw new Error(`invalid agent cassette: dom hash mismatch viewport=${frame.viewport.name} phase=${frame.phase}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function readAgentCassettePath(path, fallbackSpec) {
|
|
430
|
+
return normalizeAgentCassette(JSON.parse(readFileSync(path, "utf8")), fallbackSpec);
|
|
431
|
+
}
|
|
432
|
+
function isAgentCassetteLike(input) {
|
|
433
|
+
return typeof input === "object" && input !== null && input.version === 1 && Array.isArray(input.frames);
|
|
434
|
+
}
|
|
435
|
+
function upsertAgentCassetteFrame(cassette, frame) {
|
|
436
|
+
const next = {
|
|
437
|
+
...frame,
|
|
438
|
+
terminalHash: shortHash(frame.terminalText),
|
|
439
|
+
domHash: shortHash(frame.dom)
|
|
699
440
|
};
|
|
441
|
+
const index = cassette.frames.findIndex((candidate) => candidate.viewport.name === next.viewport.name && candidate.phase === next.phase);
|
|
442
|
+
if (index >= 0) {
|
|
443
|
+
cassette.frames[index] = next;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
cassette.frames.push(next);
|
|
447
|
+
}
|
|
448
|
+
//#endregion
|
|
449
|
+
//#region src/agent/manifest.ts
|
|
450
|
+
const AGENT_MANIFEST_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json";
|
|
451
|
+
const AGENT_MANIFEST_FILE_NAME = "ptywright-agent.manifest.json";
|
|
452
|
+
const agentManifestKindSchema = z.enum([
|
|
453
|
+
"run",
|
|
454
|
+
"replay-suite",
|
|
455
|
+
"check",
|
|
456
|
+
"promote"
|
|
457
|
+
]);
|
|
458
|
+
const agentManifestFileKindSchema = z.enum([
|
|
459
|
+
"flow",
|
|
460
|
+
"cassette",
|
|
461
|
+
"run-record",
|
|
462
|
+
"replay-summary",
|
|
463
|
+
"check-summary",
|
|
464
|
+
"promote-summary",
|
|
465
|
+
"report",
|
|
466
|
+
"terminal",
|
|
467
|
+
"dom",
|
|
468
|
+
"screenshot",
|
|
469
|
+
"diff"
|
|
470
|
+
]);
|
|
471
|
+
const agentManifestCommandSchema = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
|
|
472
|
+
const agentManifestValidationStageSchema = z.object({
|
|
473
|
+
name: z.string().min(1),
|
|
474
|
+
ok: z.boolean(),
|
|
475
|
+
totalCount: z.number().int().nonnegative(),
|
|
476
|
+
failureCount: z.number().int().nonnegative()
|
|
477
|
+
}).strict();
|
|
478
|
+
const agentManifestValidationSchema = z.object({
|
|
479
|
+
ok: z.boolean(),
|
|
480
|
+
stages: z.array(agentManifestValidationStageSchema)
|
|
481
|
+
}).strict().superRefine((validation, ctx) => {
|
|
482
|
+
const ok = validation.stages.every((stage) => stage.ok && stage.failureCount === 0);
|
|
483
|
+
if (validation.ok !== ok) ctx.addIssue({
|
|
484
|
+
code: z.ZodIssueCode.custom,
|
|
485
|
+
path: ["ok"],
|
|
486
|
+
message: "validation ok must match validation stages"
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
const agentManifestFileSchema = z.object({
|
|
490
|
+
path: z.string().min(1),
|
|
491
|
+
kind: agentManifestFileKindSchema,
|
|
492
|
+
role: z.string().min(1).optional(),
|
|
493
|
+
ok: z.boolean().optional(),
|
|
494
|
+
bytes: z.number().int().nonnegative(),
|
|
495
|
+
sha256: z.string().regex(/^[a-f0-9]{64}$/)
|
|
496
|
+
}).strict();
|
|
497
|
+
const agentManifestSchema = z.object({
|
|
498
|
+
$schema: z.string().optional(),
|
|
499
|
+
version: z.literal(1),
|
|
500
|
+
kind: agentManifestKindSchema,
|
|
501
|
+
ok: z.boolean(),
|
|
502
|
+
generatedAt: z.string().min(1),
|
|
503
|
+
rootDir: z.string().min(1),
|
|
504
|
+
primaryPath: z.string().min(1),
|
|
505
|
+
commands: z.record(agentManifestCommandSchema),
|
|
506
|
+
validation: agentManifestValidationSchema.optional(),
|
|
507
|
+
files: z.array(agentManifestFileSchema)
|
|
508
|
+
}).strict().superRefine((manifest, ctx) => {
|
|
509
|
+
const seen = /* @__PURE__ */ new Set();
|
|
510
|
+
for (const file of manifest.files) {
|
|
511
|
+
if (seen.has(file.path)) ctx.addIssue({
|
|
512
|
+
code: z.ZodIssueCode.custom,
|
|
513
|
+
path: ["files"],
|
|
514
|
+
message: `duplicate manifest file path: ${file.path}`
|
|
515
|
+
});
|
|
516
|
+
seen.add(file.path);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
function agentManifestPath(rootDir) {
|
|
520
|
+
return join(rootDir, AGENT_MANIFEST_FILE_NAME);
|
|
521
|
+
}
|
|
522
|
+
function createAgentManifest(options) {
|
|
523
|
+
return normalizeAgentManifest({
|
|
524
|
+
$schema: AGENT_MANIFEST_SCHEMA_URL,
|
|
525
|
+
version: 1,
|
|
526
|
+
kind: options.kind,
|
|
527
|
+
ok: options.ok,
|
|
528
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
529
|
+
rootDir: options.rootDir,
|
|
530
|
+
primaryPath: options.primaryPath,
|
|
531
|
+
commands: options.commands,
|
|
532
|
+
validation: options.validation,
|
|
533
|
+
files: collectManifestFiles(options.files, options.rootDir)
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function readAgentManifestPath(path) {
|
|
537
|
+
return normalizeAgentManifest(JSON.parse(readFileSync(path, "utf8")));
|
|
538
|
+
}
|
|
539
|
+
function writeAgentManifestPath(path, options) {
|
|
540
|
+
const manifest = createAgentManifest(options);
|
|
541
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
542
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
543
|
+
return manifest;
|
|
544
|
+
}
|
|
545
|
+
function normalizeAgentManifest(input) {
|
|
546
|
+
try {
|
|
547
|
+
const parsed = agentManifestSchema.parse(input);
|
|
548
|
+
return {
|
|
549
|
+
...parsed,
|
|
550
|
+
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-manifest.schema.json"
|
|
551
|
+
};
|
|
552
|
+
} catch (error) {
|
|
553
|
+
if (error instanceof z.ZodError) throw new Error(`invalid agent manifest: ${formatZodIssues(error)}`);
|
|
554
|
+
throw error;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
function validateAgentManifestFiles(manifest, manifestPath) {
|
|
558
|
+
validateManifestFiles({
|
|
559
|
+
files: manifest.files,
|
|
560
|
+
manifestPath,
|
|
561
|
+
rootDir: manifest.rootDir,
|
|
562
|
+
label: "agent"
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
function isAgentManifestLike(input) {
|
|
566
|
+
return typeof input === "object" && input !== null && input.version === 1 && typeof input.kind === "string" && Array.isArray(input.files) && typeof input.commands === "object";
|
|
567
|
+
}
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/common/argv.ts
|
|
570
|
+
function formatArgv(argv) {
|
|
571
|
+
return argv.map(formatArg).join(" ");
|
|
572
|
+
}
|
|
573
|
+
function formatArg(arg) {
|
|
574
|
+
if (/^[A-Za-z0-9_./:=@+-]+$/.test(arg)) return arg;
|
|
575
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
576
|
+
}
|
|
577
|
+
//#endregion
|
|
578
|
+
//#region src/agent/run_record.ts
|
|
579
|
+
const AGENT_RUN_RECORD_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-run.schema.json";
|
|
580
|
+
const agentRunModeSchema = z.enum(["live", "replay"]);
|
|
581
|
+
const agentRecordedStepSchema = z.object({
|
|
582
|
+
index: z.number().int().nonnegative(),
|
|
583
|
+
type: z.string().min(1),
|
|
584
|
+
label: z.string(),
|
|
585
|
+
durationMs: z.number().int().nonnegative(),
|
|
586
|
+
ok: z.boolean(),
|
|
587
|
+
error: z.string().optional()
|
|
588
|
+
}).strict();
|
|
589
|
+
const agentRunArtifactSchema = z.object({
|
|
590
|
+
name: z.string().min(1),
|
|
591
|
+
viewport: z.string().min(1),
|
|
592
|
+
kind: z.enum([
|
|
593
|
+
"terminal",
|
|
594
|
+
"dom",
|
|
595
|
+
"screenshot"
|
|
596
|
+
]),
|
|
597
|
+
path: z.string().min(1),
|
|
598
|
+
baselinePath: z.string().min(1).optional(),
|
|
599
|
+
diffPath: z.string().min(1).optional(),
|
|
600
|
+
hash: z.string().min(1).optional(),
|
|
601
|
+
ok: z.boolean(),
|
|
602
|
+
error: z.string().optional()
|
|
603
|
+
}).strict();
|
|
604
|
+
const agentCommandSchema = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
|
|
605
|
+
const agentRunRecordCommandsSchema = z.object({
|
|
606
|
+
replay: agentCommandSchema,
|
|
607
|
+
updateSnapshots: agentCommandSchema
|
|
608
|
+
}).strict();
|
|
609
|
+
const agentRunRecordSchema = z.object({
|
|
610
|
+
$schema: z.string().optional(),
|
|
611
|
+
version: z.literal(1),
|
|
612
|
+
name: z.string().min(1),
|
|
613
|
+
ok: z.boolean(),
|
|
614
|
+
startedAt: z.string().min(1),
|
|
615
|
+
durationMs: z.number().int().nonnegative(),
|
|
616
|
+
mode: agentRunModeSchema,
|
|
617
|
+
spec: agentFlowSpecSchema.optional(),
|
|
618
|
+
flowPath: z.string().min(1).optional(),
|
|
619
|
+
artifactsDir: z.string().min(1),
|
|
620
|
+
snapshotDir: z.string().min(1),
|
|
621
|
+
reportPath: z.string().min(1),
|
|
622
|
+
cassettePath: z.string().min(1).optional(),
|
|
623
|
+
cassetteFrameCount: z.number().int().nonnegative(),
|
|
624
|
+
replayCommand: z.string().min(1),
|
|
625
|
+
commands: agentRunRecordCommandsSchema,
|
|
626
|
+
steps: z.array(agentRecordedStepSchema),
|
|
627
|
+
artifacts: z.array(agentRunArtifactSchema),
|
|
628
|
+
errors: z.array(z.string())
|
|
629
|
+
}).strict().superRefine((record, ctx) => {
|
|
630
|
+
if (!record.cassettePath && !record.flowPath && !record.spec) ctx.addIssue({
|
|
631
|
+
code: z.ZodIssueCode.custom,
|
|
632
|
+
message: "agent run record requires cassettePath, flowPath, or spec"
|
|
633
|
+
});
|
|
634
|
+
const replayCommand = formatArgv(record.commands.replay.argv);
|
|
635
|
+
if (record.replayCommand !== replayCommand) ctx.addIssue({
|
|
636
|
+
code: z.ZodIssueCode.custom,
|
|
637
|
+
path: ["replayCommand"],
|
|
638
|
+
message: "replayCommand must match commands.replay.argv"
|
|
639
|
+
});
|
|
640
|
+
if (!isReplayArgv(record.commands.replay.argv)) ctx.addIssue({
|
|
641
|
+
code: z.ZodIssueCode.custom,
|
|
642
|
+
path: [
|
|
643
|
+
"commands",
|
|
644
|
+
"replay",
|
|
645
|
+
"argv"
|
|
646
|
+
],
|
|
647
|
+
message: "replay argv must be a ptywright agent replay command"
|
|
648
|
+
});
|
|
649
|
+
const expectedUpdateSnapshots = [...record.commands.replay.argv, "--update-snapshots"];
|
|
650
|
+
if (!sameArgv(record.commands.updateSnapshots.argv, expectedUpdateSnapshots)) ctx.addIssue({
|
|
651
|
+
code: z.ZodIssueCode.custom,
|
|
652
|
+
path: [
|
|
653
|
+
"commands",
|
|
654
|
+
"updateSnapshots",
|
|
655
|
+
"argv"
|
|
656
|
+
],
|
|
657
|
+
message: "updateSnapshots argv must extend commands.replay.argv"
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
function normalizeAgentRunRecord(input) {
|
|
661
|
+
try {
|
|
662
|
+
const parsed = agentRunRecordSchema.parse(input);
|
|
663
|
+
return {
|
|
664
|
+
...parsed,
|
|
665
|
+
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-run.schema.json",
|
|
666
|
+
spec: parsed.spec ? normalizeAgentFlowSpec(parsed.spec) : void 0
|
|
667
|
+
};
|
|
668
|
+
} catch (error) {
|
|
669
|
+
if (error instanceof z.ZodError) throw new Error(`invalid agent run record: ${formatZodIssues(error)}`);
|
|
670
|
+
throw error;
|
|
671
|
+
}
|
|
700
672
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
const match = output.match(new RegExp(regexSource, "m"));
|
|
704
|
-
return match?.[1] ?? match?.[0] ?? null;
|
|
673
|
+
function formatAgentArgv(argv) {
|
|
674
|
+
return formatArgv(argv);
|
|
705
675
|
}
|
|
706
|
-
function
|
|
707
|
-
return
|
|
676
|
+
function isReplayArgv(argv) {
|
|
677
|
+
return argv.length >= 4 && argv[0] === "ptywright" && argv[1] === "agent" && argv[2] === "replay";
|
|
708
678
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
child.once("exit", () => {
|
|
717
|
-
clearTimeout(timer);
|
|
718
|
-
resolveClose();
|
|
719
|
-
});
|
|
720
|
-
child.kill("SIGTERM");
|
|
679
|
+
function readAgentRunRecordPath(path) {
|
|
680
|
+
return normalizeAgentRunRecord(JSON.parse(readFileSync(path, "utf8")));
|
|
681
|
+
}
|
|
682
|
+
function writeAgentRunRecordPath(path, record) {
|
|
683
|
+
const normalized = normalizeAgentRunRecord({
|
|
684
|
+
...record,
|
|
685
|
+
$schema: record.$schema ?? "https://ptywright.local/schemas/ptywright-agent-run.schema.json"
|
|
721
686
|
});
|
|
687
|
+
writeFileSync(path, JSON.stringify(normalized, null, 2) + "\n", "utf8");
|
|
722
688
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
return buildCommandLaunchCommand(launch, options);
|
|
689
|
+
function isAgentRunRecordLike(input) {
|
|
690
|
+
if (typeof input !== "object" || input === null) return false;
|
|
691
|
+
const candidate = input;
|
|
692
|
+
return candidate.version === 1 && ("cassettePath" in candidate || "flowPath" in candidate || "spec" in candidate);
|
|
728
693
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
694
|
+
//#endregion
|
|
695
|
+
//#region src/agent/spec_loader.ts
|
|
696
|
+
async function loadAgentSpec(specPath) {
|
|
697
|
+
const resolved = resolve(process.cwd(), specPath);
|
|
698
|
+
if (resolved.endsWith(".json")) {
|
|
699
|
+
const raw = JSON.parse(readFileSync(resolved, "utf8"));
|
|
700
|
+
return {
|
|
701
|
+
spec: normalizeAgentFlowSpec(raw),
|
|
702
|
+
raw,
|
|
703
|
+
path: resolved
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
const mod = await import(`${pathToFileURL(resolved).href}?t=${Date.now()}`);
|
|
707
|
+
const raw = mod.default ?? mod.spec;
|
|
737
708
|
return {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
709
|
+
spec: normalizeAgentFlowSpec(raw),
|
|
710
|
+
raw,
|
|
711
|
+
path: resolved
|
|
741
712
|
};
|
|
742
713
|
}
|
|
743
|
-
function formatAgentLaunchCommand(launch) {
|
|
744
|
-
const command = buildAgentLaunchCommand(launch);
|
|
745
|
-
return command ? formatBrowserLaunchCommand(command) : "launch.mode=url";
|
|
746
|
-
}
|
|
747
714
|
//#endregion
|
|
748
715
|
//#region src/agent/presets.ts
|
|
749
716
|
const COMMON_AGENT_MASKS = [
|
|
@@ -777,7 +744,7 @@ const COMMON_AGENT_MASKS = [
|
|
|
777
744
|
];
|
|
778
745
|
const FLAVOR_MASKS = {
|
|
779
746
|
codex: [{
|
|
780
|
-
regex: "\\b(?:gpt
|
|
747
|
+
regex: "\\b(?:gpt-[A-Za-z0-9._:-]+|o[0-9][A-Za-z0-9._:-]*)\\b",
|
|
781
748
|
flags: "gi",
|
|
782
749
|
replacement: "<model>"
|
|
783
750
|
}, {
|
|
@@ -867,157 +834,982 @@ function createAgentTemplateSpec(flavor) {
|
|
|
867
834
|
};
|
|
868
835
|
}
|
|
869
836
|
//#endregion
|
|
870
|
-
//#region src/
|
|
871
|
-
function
|
|
872
|
-
|
|
837
|
+
//#region src/agent/browser.ts
|
|
838
|
+
async function launchAgentBrowser(args) {
|
|
839
|
+
let lastError;
|
|
840
|
+
for (let attempt = 0; attempt < 5; attempt += 1) try {
|
|
841
|
+
const browser = await chromium.launch({ headless: args.headless });
|
|
842
|
+
await verifyBrowserLaunch(browser);
|
|
843
|
+
return browser;
|
|
844
|
+
} catch (error) {
|
|
845
|
+
lastError = error;
|
|
846
|
+
if (!isTransientBrowserLaunchError(error) || attempt === 4) throw error;
|
|
847
|
+
await new Promise((resolveRetry) => setTimeout(resolveRetry, 250 * (attempt + 1)));
|
|
848
|
+
}
|
|
849
|
+
throw lastError;
|
|
850
|
+
}
|
|
851
|
+
async function verifyBrowserLaunch(browser) {
|
|
852
|
+
const context = await browser.newContext();
|
|
853
|
+
try {
|
|
854
|
+
const page = await context.newPage();
|
|
855
|
+
await page.goto("data:text/html,<title>ptywright</title>", { waitUntil: "load" });
|
|
856
|
+
await page.close();
|
|
857
|
+
} catch (error) {
|
|
858
|
+
await browser.close();
|
|
859
|
+
throw error;
|
|
860
|
+
} finally {
|
|
861
|
+
await context.close().catch(() => void 0);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function isTransientBrowserLaunchError(error) {
|
|
865
|
+
const fields = errorFields(error);
|
|
866
|
+
return fields.message.includes("Failed to connect") || fields.message.includes("Target page, context or browser has been closed") || fields.message.includes("browserType.launch") || fields.code === "ENOENT" || fields.code === "ECONNREFUSED" || fields.syscall === "connect";
|
|
867
|
+
}
|
|
868
|
+
function errorFields(error) {
|
|
869
|
+
const record = typeof error === "object" && error !== null ? error : {};
|
|
870
|
+
return {
|
|
871
|
+
message: error instanceof Error ? error.message : String(error),
|
|
872
|
+
code: typeof record.code === "string" ? record.code : "",
|
|
873
|
+
syscall: typeof record.syscall === "string" ? record.syscall : ""
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
//#endregion
|
|
877
|
+
//#region src/agent/aitty_report_assets.ts
|
|
878
|
+
function prepareAittyReportAssets(context) {
|
|
879
|
+
const sources = resolveAittyReportAssetSources(context);
|
|
880
|
+
if (!existsSync(sources.scriptSource) || !existsSync(sources.styleSource)) throw new Error("@aitty/snapshot report assets are missing. Run the package build before generating reports.");
|
|
881
|
+
const assetDir = join(context.artifactsDir, "assets");
|
|
882
|
+
const scriptPath = join(assetDir, "aitty-web-component.js");
|
|
883
|
+
const stylePath = join(assetDir, "aitty-terminal.css");
|
|
884
|
+
mkdirSync(assetDir, { recursive: true });
|
|
885
|
+
copyFileSync(sources.scriptSource, scriptPath);
|
|
886
|
+
copyFileSync(sources.styleSource, stylePath);
|
|
887
|
+
return {
|
|
888
|
+
scriptPath,
|
|
889
|
+
scriptType: sources.scriptType,
|
|
890
|
+
stylePath
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
function resolveAittyReportAssetSources(context) {
|
|
894
|
+
for (const resolverBase of resolveAittyReportResolverBases(context)) {
|
|
895
|
+
const sources = tryResolveAittyReportAssetSources(createRequire(resolverBase));
|
|
896
|
+
if (sources) return sources;
|
|
897
|
+
}
|
|
898
|
+
const fallback = tryResolveAittyReportAssetSources(createRequire(import.meta.url));
|
|
899
|
+
if (fallback) return fallback;
|
|
900
|
+
throw new Error("@aitty/snapshot report assets are missing. Install @aitty/snapshot or run the package build before generating reports.");
|
|
901
|
+
}
|
|
902
|
+
function resolveAittyReportResolverBases(context) {
|
|
903
|
+
const candidates = [
|
|
904
|
+
findNearestPackageJson(dirname(resolve(context.flowPath))),
|
|
905
|
+
findNearestPackageJson(dirname(resolve(context.reportPath))),
|
|
906
|
+
findNearestPackageJson(dirname(resolve(context.artifactsDir))),
|
|
907
|
+
findNearestPackageJson(process.cwd())
|
|
908
|
+
].filter((path) => Boolean(path));
|
|
909
|
+
return Array.from(new Set(candidates));
|
|
910
|
+
}
|
|
911
|
+
function findNearestPackageJson(startDir) {
|
|
912
|
+
let currentDir = resolve(startDir);
|
|
913
|
+
while (true) {
|
|
914
|
+
const packagePath = join(currentDir, "package.json");
|
|
915
|
+
if (existsSync(packagePath)) return packagePath;
|
|
916
|
+
const parentDir = dirname(currentDir);
|
|
917
|
+
if (parentDir === currentDir) return null;
|
|
918
|
+
currentDir = parentDir;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function tryResolveAittyReportAssetSources(resolver) {
|
|
922
|
+
return tryResolveAittyPackageAssetSources(resolver, "@aitty/snapshot") ?? tryResolveAittyPackageAssetSources(resolver, "@aitty/browser");
|
|
923
|
+
}
|
|
924
|
+
function tryResolveAittyPackageAssetSources(resolver, packageName) {
|
|
925
|
+
let scriptSource;
|
|
926
|
+
let scriptType = "classic";
|
|
927
|
+
let styleSource;
|
|
928
|
+
try {
|
|
929
|
+
scriptSource = resolver.resolve(`${packageName}/web-component.global.js`);
|
|
930
|
+
} catch {
|
|
931
|
+
try {
|
|
932
|
+
scriptSource = resolver.resolve(`${packageName}/web-component.js`);
|
|
933
|
+
scriptType = "module";
|
|
934
|
+
} catch {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
styleSource = resolver.resolve(`${packageName}/style.css`);
|
|
940
|
+
} catch {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
944
|
+
scriptSource,
|
|
945
|
+
scriptType,
|
|
946
|
+
styleSource
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region src/agent/report_paths.ts
|
|
951
|
+
function relativeHref(fromPath, targetPath, artifactsDir) {
|
|
952
|
+
if (targetPath.startsWith(artifactsDir)) return relative(dirname(fromPath), targetPath).replaceAll("\\", "/") || ".";
|
|
953
|
+
return targetPath;
|
|
954
|
+
}
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/agent/report_aitty_preview.ts
|
|
957
|
+
function resolveAittyPreviewAssets(previewPath, assets, artifactsDir) {
|
|
958
|
+
return {
|
|
959
|
+
scriptHref: relativeHref(previewPath, assets.scriptPath, artifactsDir),
|
|
960
|
+
scriptType: assets.scriptType,
|
|
961
|
+
styleHref: relativeHref(previewPath, assets.stylePath, artifactsDir)
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
function renderAittyPreviewAssetTags(assets) {
|
|
965
|
+
return ` <link rel="stylesheet" href="${escapeAttribute(assets.styleHref)}" />
|
|
966
|
+
<script${assets.scriptType === "module" ? " type=\"module\"" : ""} src="${escapeAttribute(assets.scriptHref)}"><\/script>
|
|
967
|
+
`;
|
|
968
|
+
}
|
|
969
|
+
function renderAittyPreviewBody(args) {
|
|
970
|
+
const { snapshot, snapshotLayout, viewOptions } = args;
|
|
971
|
+
return ` <aitty-snapshot
|
|
972
|
+
cols="${snapshotLayout.cols}"
|
|
973
|
+
rows="${snapshotLayout.rows}"
|
|
974
|
+
screen-mode="${escapeAttribute(viewOptions.screenMode)}"
|
|
975
|
+
theme="${escapeAttribute(viewOptions.theme)}"
|
|
976
|
+
font-size="${snapshotLayout.fontSize}"
|
|
977
|
+
line-height="${snapshotLayout.lineHeight}"
|
|
978
|
+
>${snapshot}</aitty-snapshot>`;
|
|
979
|
+
}
|
|
980
|
+
function renderAittyPreviewCss(viewOptions) {
|
|
981
|
+
return ` :root {
|
|
982
|
+
color-scheme: ${viewOptions.theme};
|
|
983
|
+
}
|
|
984
|
+
html,
|
|
985
|
+
body {
|
|
986
|
+
width: 100%;
|
|
987
|
+
height: 100%;
|
|
988
|
+
margin: 0;
|
|
989
|
+
background: var(--theme-term-bg, Canvas);
|
|
990
|
+
}
|
|
991
|
+
aitty-snapshot {
|
|
992
|
+
display: block;
|
|
993
|
+
width: 100%;
|
|
994
|
+
height: 100%;
|
|
995
|
+
}`;
|
|
996
|
+
}
|
|
997
|
+
//#endregion
|
|
998
|
+
//#region src/agent/report_artifact_paths.ts
|
|
999
|
+
function artifactViewerPath(artifact) {
|
|
1000
|
+
if (artifact.kind === "terminal") return artifact.path.endsWith(".terminal.txt") ? artifact.path.replace(/\.terminal\.txt$/, ".terminal.viewer.html") : `${artifact.path}.viewer.html`;
|
|
1001
|
+
if (artifact.kind === "dom") return artifact.path.endsWith(".dom.html") ? artifact.path.replace(/\.dom\.html$/, ".dom.viewer.html") : `${artifact.path}.viewer.html`;
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
function artifactDomPreviewPath(artifact) {
|
|
1005
|
+
return artifact.path.endsWith(".dom.html") ? artifact.path.replace(/\.dom\.html$/, ".dom.preview.html") : `${artifact.path}.preview.html`;
|
|
1006
|
+
}
|
|
1007
|
+
function artifactSnapshotKey(artifact) {
|
|
1008
|
+
return `${artifact.viewport}\u0000${artifact.name}`;
|
|
1009
|
+
}
|
|
1010
|
+
//#endregion
|
|
1011
|
+
//#region src/agent/report_dom_artifact_viewer.ts
|
|
1012
|
+
function resolveReportDomPreview(path) {
|
|
1013
|
+
return path ? { path } : null;
|
|
1014
|
+
}
|
|
1015
|
+
function renderDomArtifactViewer(args) {
|
|
1016
|
+
const { artifactsDir, mobile, preview, viewerPath, viewOptions } = args;
|
|
1017
|
+
const src = relativeHref(viewerPath, preview.path, artifactsDir);
|
|
1018
|
+
return `<div class="viewer-viewport dom-viewport" data-mobile="${mobile ? "true" : "false"}" data-screen-mode="${escapeAttribute(viewOptions.screenMode)}"><iframe class="dom-viewer-frame" sandbox="allow-same-origin allow-scripts" src="${escapeAttribute(src)}" title="terminal DOM artifact"></iframe></div>`;
|
|
1019
|
+
}
|
|
1020
|
+
function renderDomArtifactViewerFragment(args) {
|
|
1021
|
+
return {
|
|
1022
|
+
css: "",
|
|
1023
|
+
html: renderDomArtifactViewer(args),
|
|
1024
|
+
script: ""
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/agent/report_raw_ansi_text.ts
|
|
1029
|
+
function renderRawAnsiTextHtml(input) {
|
|
1030
|
+
const out = [];
|
|
1031
|
+
let style = {};
|
|
1032
|
+
let segmentStart = 0;
|
|
1033
|
+
let index = 0;
|
|
1034
|
+
while (index < input.length) {
|
|
1035
|
+
if (!isCsiStart(input, index)) {
|
|
1036
|
+
index += 1;
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const end = findCsiEnd(input, index + 2);
|
|
1040
|
+
if (end === -1) {
|
|
1041
|
+
index += 1;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
out.push(renderAnsiTextSegment(input.slice(segmentStart, index), style));
|
|
1045
|
+
if (input[end] === "m") style = applySgrCodes(style, parseSgrCodes(input.slice(index + 2, end)));
|
|
1046
|
+
index = end + 1;
|
|
1047
|
+
segmentStart = index;
|
|
1048
|
+
}
|
|
1049
|
+
out.push(renderAnsiTextSegment(input.slice(segmentStart), style));
|
|
1050
|
+
return out.join("");
|
|
1051
|
+
}
|
|
1052
|
+
function isCsiStart(input, index) {
|
|
1053
|
+
return input.charCodeAt(index) === 27 && input[index + 1] === "[";
|
|
1054
|
+
}
|
|
1055
|
+
function findCsiEnd(input, start) {
|
|
1056
|
+
for (let index = start; index < input.length; index += 1) {
|
|
1057
|
+
const code = input.charCodeAt(index);
|
|
1058
|
+
if (code >= 64 && code <= 126) return index;
|
|
1059
|
+
}
|
|
1060
|
+
return -1;
|
|
1061
|
+
}
|
|
1062
|
+
function parseSgrCodes(input) {
|
|
1063
|
+
if (!input) return [0];
|
|
1064
|
+
return input.split(";").map((value) => {
|
|
1065
|
+
const parsed = Number.parseInt(value, 10);
|
|
1066
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
function applySgrCodes(current, codes) {
|
|
1070
|
+
const next = { ...current };
|
|
1071
|
+
for (let index = 0; index < codes.length; index += 1) {
|
|
1072
|
+
const code = codes[index] ?? 0;
|
|
1073
|
+
if (code === 0) {
|
|
1074
|
+
resetStyle(next);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
if (code === 1) next.bold = true;
|
|
1078
|
+
else if (code === 2) next.dim = true;
|
|
1079
|
+
else if (code === 3) next.italic = true;
|
|
1080
|
+
else if (code === 4) next.underline = true;
|
|
1081
|
+
else if (code === 7) next.inverse = true;
|
|
1082
|
+
else if (code === 9) next.strikethrough = true;
|
|
1083
|
+
else if (code === 22) {
|
|
1084
|
+
next.bold = false;
|
|
1085
|
+
next.dim = false;
|
|
1086
|
+
} else if (code === 23) next.italic = false;
|
|
1087
|
+
else if (code === 24) next.underline = false;
|
|
1088
|
+
else if (code === 27) next.inverse = false;
|
|
1089
|
+
else if (code === 29) next.strikethrough = false;
|
|
1090
|
+
else if (code === 39) next.fg = void 0;
|
|
1091
|
+
else if (code === 49) next.bg = void 0;
|
|
1092
|
+
else if (code >= 30 && code <= 37) next.fg = ansiPaletteColor(code - 30);
|
|
1093
|
+
else if (code >= 90 && code <= 97) next.fg = ansiPaletteColor(code - 90 + 8);
|
|
1094
|
+
else if (code >= 40 && code <= 47) next.bg = ansiPaletteColor(code - 40);
|
|
1095
|
+
else if (code >= 100 && code <= 107) next.bg = ansiPaletteColor(code - 100 + 8);
|
|
1096
|
+
else if ((code === 38 || code === 48) && codes[index + 1] === 5) {
|
|
1097
|
+
const color = ansiPaletteColor(codes[index + 2] ?? 0);
|
|
1098
|
+
if (code === 38) next.fg = color;
|
|
1099
|
+
else next.bg = color;
|
|
1100
|
+
index += 2;
|
|
1101
|
+
} else if ((code === 38 || code === 48) && codes[index + 1] === 2) {
|
|
1102
|
+
const color = `rgb(${clampColor(codes[index + 2] ?? 0)} ${clampColor(codes[index + 3] ?? 0)} ${clampColor(codes[index + 4] ?? 0)})`;
|
|
1103
|
+
if (code === 38) next.fg = color;
|
|
1104
|
+
else next.bg = color;
|
|
1105
|
+
index += 4;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return next;
|
|
1109
|
+
}
|
|
1110
|
+
function resetStyle(style) {
|
|
1111
|
+
style.fg = void 0;
|
|
1112
|
+
style.bg = void 0;
|
|
1113
|
+
style.bold = false;
|
|
1114
|
+
style.dim = false;
|
|
1115
|
+
style.italic = false;
|
|
1116
|
+
style.underline = false;
|
|
1117
|
+
style.inverse = false;
|
|
1118
|
+
style.strikethrough = false;
|
|
1119
|
+
}
|
|
1120
|
+
function renderAnsiTextSegment(text, style) {
|
|
1121
|
+
if (!text) return "";
|
|
1122
|
+
const safeText = escapeHtml(text);
|
|
1123
|
+
const css = ansiStyleToCss(style);
|
|
1124
|
+
return css ? `<span style="${escapeAttribute(css)}">${safeText}</span>` : safeText;
|
|
1125
|
+
}
|
|
1126
|
+
function ansiStyleToCss(style) {
|
|
1127
|
+
const decls = [];
|
|
1128
|
+
const fg = style.inverse ? style.bg : style.fg;
|
|
1129
|
+
const bg = style.inverse ? style.fg : style.bg;
|
|
1130
|
+
if (fg) decls.push(`color: ${fg}`);
|
|
1131
|
+
if (bg) decls.push(`background-color: ${bg}`);
|
|
1132
|
+
if (style.bold) decls.push("font-weight: 700");
|
|
1133
|
+
if (style.italic) decls.push("font-style: italic");
|
|
1134
|
+
if (style.dim) decls.push("opacity: 0.72");
|
|
1135
|
+
const decorations = [];
|
|
1136
|
+
if (style.underline) decorations.push("underline");
|
|
1137
|
+
if (style.strikethrough) decorations.push("line-through");
|
|
1138
|
+
if (decorations.length > 0) decls.push(`text-decoration: ${decorations.join(" ")}`);
|
|
1139
|
+
return decls.join("; ");
|
|
1140
|
+
}
|
|
1141
|
+
function ansiPaletteColor(index) {
|
|
1142
|
+
const table16 = [
|
|
1143
|
+
"#151b23",
|
|
1144
|
+
"#ff7b72",
|
|
1145
|
+
"#7ee787",
|
|
1146
|
+
"#f2cc60",
|
|
1147
|
+
"#79c0ff",
|
|
1148
|
+
"#d2a8ff",
|
|
1149
|
+
"#70e1e8",
|
|
1150
|
+
"#e6edf7",
|
|
1151
|
+
"#6e7681",
|
|
1152
|
+
"#ffa198",
|
|
1153
|
+
"#aff5b4",
|
|
1154
|
+
"#ffdf80",
|
|
1155
|
+
"#a5d6ff",
|
|
1156
|
+
"#e2c5ff",
|
|
1157
|
+
"#96f0f5",
|
|
1158
|
+
"#ffffff"
|
|
1159
|
+
];
|
|
1160
|
+
const normalized = clampColor(index);
|
|
1161
|
+
if (normalized < table16.length) return table16[normalized] ?? "#e6edf7";
|
|
1162
|
+
if (normalized <= 231) {
|
|
1163
|
+
const values = [
|
|
1164
|
+
0,
|
|
1165
|
+
95,
|
|
1166
|
+
135,
|
|
1167
|
+
175,
|
|
1168
|
+
215,
|
|
1169
|
+
255
|
|
1170
|
+
];
|
|
1171
|
+
const offset = normalized - 16;
|
|
1172
|
+
return `rgb(${values[Math.trunc(offset / 36) % 6] ?? 0} ${values[Math.trunc(offset / 6) % 6] ?? 0} ${values[offset % 6] ?? 0})`;
|
|
1173
|
+
}
|
|
1174
|
+
const gray = clampColor(8 + (normalized - 232) * 10);
|
|
1175
|
+
return `rgb(${gray} ${gray} ${gray})`;
|
|
1176
|
+
}
|
|
1177
|
+
function clampColor(value) {
|
|
1178
|
+
if (!Number.isFinite(value)) return 0;
|
|
1179
|
+
return Math.max(0, Math.min(255, Math.trunc(value)));
|
|
1180
|
+
}
|
|
1181
|
+
//#endregion
|
|
1182
|
+
//#region src/agent/report_viewer_pan.ts
|
|
1183
|
+
function renderReportViewportPanCss() {
|
|
1184
|
+
return ` [data-ptywright-report-pan="true"] {
|
|
1185
|
+
cursor: grab;
|
|
1186
|
+
}
|
|
1187
|
+
[data-ptywright-report-panning="true"] {
|
|
1188
|
+
cursor: grabbing;
|
|
1189
|
+
}`;
|
|
1190
|
+
}
|
|
1191
|
+
function renderReportViewportPanScript(body) {
|
|
1192
|
+
return ` (() => {
|
|
1193
|
+
const isHtmlElement = (value) => {
|
|
1194
|
+
return Boolean(
|
|
1195
|
+
value &&
|
|
1196
|
+
value.nodeType === 1 &&
|
|
1197
|
+
value.dataset &&
|
|
1198
|
+
value.style &&
|
|
1199
|
+
typeof value.addEventListener === "function"
|
|
1200
|
+
);
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const scrollToBottom = (element) => {
|
|
1204
|
+
element.scrollTop = Math.max(0, element.scrollHeight - element.clientHeight);
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
const enableViewportPan = (element) => {
|
|
1208
|
+
if (!isHtmlElement(element) || element.dataset.ptywrightReportPan === "true") {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
element.dataset.ptywrightReportPan = "true";
|
|
1213
|
+
let activePointerId = null;
|
|
1214
|
+
let startX = 0;
|
|
1215
|
+
let startY = 0;
|
|
1216
|
+
let startScrollLeft = 0;
|
|
1217
|
+
let startScrollTop = 0;
|
|
1218
|
+
let moved = false;
|
|
1219
|
+
|
|
1220
|
+
element.addEventListener("pointerdown", (event) => {
|
|
1221
|
+
if (event.button !== 0) return;
|
|
1222
|
+
activePointerId = event.pointerId;
|
|
1223
|
+
startX = event.clientX;
|
|
1224
|
+
startY = event.clientY;
|
|
1225
|
+
startScrollLeft = element.scrollLeft;
|
|
1226
|
+
startScrollTop = element.scrollTop;
|
|
1227
|
+
moved = false;
|
|
1228
|
+
element.dataset.ptywrightReportPanning = "true";
|
|
1229
|
+
element.setPointerCapture?.(event.pointerId);
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
element.addEventListener("pointermove", (event) => {
|
|
1233
|
+
if (activePointerId !== event.pointerId) return;
|
|
1234
|
+
const deltaX = event.clientX - startX;
|
|
1235
|
+
const deltaY = event.clientY - startY;
|
|
1236
|
+
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
|
|
1237
|
+
moved = true;
|
|
1238
|
+
}
|
|
1239
|
+
element.scrollLeft = startScrollLeft - deltaX;
|
|
1240
|
+
element.scrollTop = startScrollTop - deltaY;
|
|
1241
|
+
if (moved) event.preventDefault();
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
const finish = (event) => {
|
|
1245
|
+
if (activePointerId !== event.pointerId) return;
|
|
1246
|
+
activePointerId = null;
|
|
1247
|
+
element.dataset.ptywrightReportPanning = "false";
|
|
1248
|
+
element.releasePointerCapture?.(event.pointerId);
|
|
1249
|
+
};
|
|
1250
|
+
|
|
1251
|
+
element.addEventListener("pointerup", finish);
|
|
1252
|
+
element.addEventListener("pointercancel", finish);
|
|
1253
|
+
};
|
|
1254
|
+
${body}
|
|
1255
|
+
})();`;
|
|
1256
|
+
}
|
|
1257
|
+
//#endregion
|
|
1258
|
+
//#region src/agent/report_raw_artifact_viewer.ts
|
|
1259
|
+
function renderRawArtifactViewer(content, mobile, viewOptions) {
|
|
1260
|
+
return `<div class="viewer-viewport raw-artifact-viewport" data-mobile="${mobile ? "true" : "false"}" data-screen-mode="${escapeAttribute(viewOptions.screenMode)}"><pre class="raw-artifact-text">${renderRawAnsiTextHtml(content)}</pre></div>`;
|
|
1261
|
+
}
|
|
1262
|
+
function renderRawArtifactViewerCss() {
|
|
1263
|
+
return ` .raw-artifact-text {
|
|
1264
|
+
min-width: 100%;
|
|
1265
|
+
width: max-content;
|
|
1266
|
+
min-height: 100%;
|
|
1267
|
+
margin: 0;
|
|
1268
|
+
background:
|
|
1269
|
+
linear-gradient(180deg, color-mix(in srgb, #162033 92%, black), #0c111d);
|
|
1270
|
+
color: #e6edf7;
|
|
1271
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
|
1272
|
+
font-size: 13px;
|
|
1273
|
+
line-height: 1.45;
|
|
1274
|
+
overflow: visible;
|
|
1275
|
+
padding: 12px;
|
|
1276
|
+
white-space: pre;
|
|
1277
|
+
}
|
|
1278
|
+
.viewer-page[data-theme="light"] .raw-artifact-text {
|
|
1279
|
+
background: #ffffff;
|
|
1280
|
+
color: #4c4f69;
|
|
1281
|
+
}
|
|
1282
|
+
${renderReportViewportPanCss()}`;
|
|
1283
|
+
}
|
|
1284
|
+
function renderRawArtifactViewerScript() {
|
|
1285
|
+
return renderReportViewportPanScript(`
|
|
1286
|
+
document.querySelectorAll(".raw-artifact-viewport").forEach((viewport) => {
|
|
1287
|
+
enableViewportPan(viewport);
|
|
1288
|
+
if (viewport.getAttribute("data-screen-mode") === "termvision") {
|
|
1289
|
+
requestAnimationFrame(() => scrollToBottom(viewport));
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
`);
|
|
1293
|
+
}
|
|
1294
|
+
function renderRawArtifactViewerFragment(args) {
|
|
1295
|
+
const { content, mobile, viewOptions } = args;
|
|
1296
|
+
return {
|
|
1297
|
+
css: renderRawArtifactViewerCss(),
|
|
1298
|
+
html: renderRawArtifactViewer(content, mobile, viewOptions),
|
|
1299
|
+
script: renderRawArtifactViewerScript()
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
//#endregion
|
|
1303
|
+
//#region src/agent/report_artifact_viewer_fragment.ts
|
|
1304
|
+
function renderArtifactViewerFragment(args) {
|
|
1305
|
+
const { artifactsDir, content, domViewerPreview, mobile, viewerPath, viewOptions } = args;
|
|
1306
|
+
if (domViewerPreview) return renderDomArtifactViewerFragment({
|
|
1307
|
+
artifactsDir,
|
|
1308
|
+
mobile,
|
|
1309
|
+
preview: domViewerPreview,
|
|
1310
|
+
viewerPath,
|
|
1311
|
+
viewOptions
|
|
1312
|
+
});
|
|
1313
|
+
return renderRawArtifactViewerFragment({
|
|
1314
|
+
content,
|
|
1315
|
+
mobile,
|
|
1316
|
+
viewOptions
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
//#endregion
|
|
1320
|
+
//#region src/agent/report_artifact_viewer_shell_css.ts
|
|
1321
|
+
function renderArtifactViewerShellCss() {
|
|
1322
|
+
return ` :root {
|
|
1323
|
+
color-scheme: dark;
|
|
1324
|
+
--bg: #080d16;
|
|
1325
|
+
--panel: #0c111d;
|
|
1326
|
+
--line: rgba(148, 163, 184, 0.26);
|
|
1327
|
+
--ink: #e6edf7;
|
|
1328
|
+
--muted: #91a0b8;
|
|
1329
|
+
--focus: #79c0ff;
|
|
1330
|
+
font-family:
|
|
1331
|
+
ui-sans-serif,
|
|
1332
|
+
system-ui,
|
|
1333
|
+
-apple-system,
|
|
1334
|
+
BlinkMacSystemFont,
|
|
1335
|
+
"Segoe UI",
|
|
1336
|
+
sans-serif;
|
|
1337
|
+
}
|
|
1338
|
+
* {
|
|
1339
|
+
box-sizing: border-box;
|
|
1340
|
+
}
|
|
1341
|
+
html,
|
|
1342
|
+
body {
|
|
1343
|
+
width: 100%;
|
|
1344
|
+
height: 100%;
|
|
1345
|
+
margin: 0;
|
|
1346
|
+
overflow: hidden;
|
|
1347
|
+
background: var(--bg);
|
|
1348
|
+
color: var(--ink);
|
|
1349
|
+
}
|
|
1350
|
+
.viewer-page {
|
|
1351
|
+
display: grid;
|
|
1352
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
1353
|
+
width: 100%;
|
|
1354
|
+
height: 100dvh;
|
|
1355
|
+
min-width: 0;
|
|
1356
|
+
min-height: 0;
|
|
1357
|
+
}
|
|
1358
|
+
.viewer-toolbar {
|
|
1359
|
+
display: flex;
|
|
1360
|
+
flex-wrap: wrap;
|
|
1361
|
+
gap: 8px;
|
|
1362
|
+
align-items: center;
|
|
1363
|
+
min-width: 0;
|
|
1364
|
+
border-bottom: 1px solid var(--line);
|
|
1365
|
+
background: color-mix(in srgb, var(--panel) 88%, black);
|
|
1366
|
+
padding: 10px 12px;
|
|
1367
|
+
}
|
|
1368
|
+
.viewer-title {
|
|
1369
|
+
min-width: min(100%, 220px);
|
|
1370
|
+
margin-right: auto;
|
|
1371
|
+
overflow-wrap: anywhere;
|
|
1372
|
+
font-size: 14px;
|
|
1373
|
+
font-weight: 700;
|
|
1374
|
+
}
|
|
1375
|
+
.viewer-link,
|
|
1376
|
+
.viewer-pill {
|
|
1377
|
+
display: inline-flex;
|
|
1378
|
+
min-height: 30px;
|
|
1379
|
+
align-items: center;
|
|
1380
|
+
border: 1px solid var(--line);
|
|
1381
|
+
border-radius: 999px;
|
|
1382
|
+
padding: 0 10px;
|
|
1383
|
+
color: var(--muted);
|
|
1384
|
+
font-size: 12px;
|
|
1385
|
+
text-decoration: none;
|
|
1386
|
+
}
|
|
1387
|
+
.viewer-link {
|
|
1388
|
+
color: var(--focus);
|
|
1389
|
+
font-weight: 700;
|
|
1390
|
+
}
|
|
1391
|
+
.viewer-stage {
|
|
1392
|
+
display: grid;
|
|
1393
|
+
grid-template-columns: max-content;
|
|
1394
|
+
grid-template-rows: max-content;
|
|
1395
|
+
min-width: 0;
|
|
1396
|
+
min-height: 0;
|
|
1397
|
+
justify-content: start;
|
|
1398
|
+
align-content: start;
|
|
1399
|
+
overflow: auto;
|
|
1400
|
+
background:
|
|
1401
|
+
radial-gradient(circle at top left, rgba(121, 192, 255, 0.1), transparent 32%),
|
|
1402
|
+
#060a13;
|
|
1403
|
+
padding: 14px;
|
|
1404
|
+
}
|
|
1405
|
+
.viewer-viewport {
|
|
1406
|
+
width: var(--config-viewport-width);
|
|
1407
|
+
height: var(--config-viewport-height);
|
|
1408
|
+
max-width: none;
|
|
1409
|
+
max-height: none;
|
|
1410
|
+
overflow: auto;
|
|
1411
|
+
overscroll-behavior: contain;
|
|
1412
|
+
border: 0;
|
|
1413
|
+
border-radius: 8px;
|
|
1414
|
+
background: #0c111d;
|
|
1415
|
+
outline: 1px solid var(--line);
|
|
1416
|
+
box-shadow: 0 18px 52px rgba(0, 0, 0, 0.34);
|
|
1417
|
+
}
|
|
1418
|
+
.viewer-viewport[data-mobile="true"] {
|
|
1419
|
+
width: var(--config-viewport-width);
|
|
1420
|
+
}
|
|
1421
|
+
.dom-viewport {
|
|
1422
|
+
overflow: hidden;
|
|
1423
|
+
}
|
|
1424
|
+
.dom-viewer-frame {
|
|
1425
|
+
display: block;
|
|
1426
|
+
width: 100%;
|
|
1427
|
+
height: 100%;
|
|
1428
|
+
border: 0;
|
|
1429
|
+
background: #0c111d;
|
|
1430
|
+
}
|
|
1431
|
+
.viewer-page[data-theme="light"] {
|
|
1432
|
+
--bg: #f8fafc;
|
|
1433
|
+
--panel: #ffffff;
|
|
1434
|
+
--line: rgba(15, 23, 42, 0.14);
|
|
1435
|
+
--ink: #0f172a;
|
|
1436
|
+
--muted: #64748b;
|
|
1437
|
+
--focus: #1e66f5;
|
|
1438
|
+
}
|
|
1439
|
+
.viewer-page[data-theme="light"] .viewer-stage {
|
|
1440
|
+
background: #f8fafc;
|
|
1441
|
+
}
|
|
1442
|
+
.viewer-page[data-theme="light"] .viewer-viewport,
|
|
1443
|
+
.viewer-page[data-theme="light"] .dom-viewer-frame {
|
|
1444
|
+
background: #ffffff;
|
|
1445
|
+
}
|
|
1446
|
+
@media (max-width: 720px) {
|
|
1447
|
+
.viewer-toolbar {
|
|
1448
|
+
padding: 8px;
|
|
1449
|
+
}
|
|
1450
|
+
.viewer-title {
|
|
1451
|
+
flex-basis: 100%;
|
|
1452
|
+
order: -1;
|
|
1453
|
+
}
|
|
1454
|
+
.viewer-stage {
|
|
1455
|
+
padding: 0;
|
|
1456
|
+
}
|
|
1457
|
+
.viewer-viewport {
|
|
1458
|
+
width: var(--config-viewport-width);
|
|
1459
|
+
height: var(--config-viewport-height);
|
|
1460
|
+
border-radius: 0;
|
|
1461
|
+
}
|
|
1462
|
+
}`;
|
|
1463
|
+
}
|
|
1464
|
+
//#endregion
|
|
1465
|
+
//#region src/agent/report_artifact_viewer_shell.ts
|
|
1466
|
+
function renderArtifactViewerShellHtml(args) {
|
|
1467
|
+
const { artifact, artifactsDir, contentFragment, mobile, reportPath, viewOptions, viewerPath } = args;
|
|
1468
|
+
const title = `${artifact.viewport} ${artifact.name} ${artifact.kind}`;
|
|
1469
|
+
const viewportStyle = renderConfiguredViewportStyle(args.viewport);
|
|
1470
|
+
const viewportLabel = args.viewport ? `${args.viewport.name} ${args.viewport.width}x${args.viewport.height}` : `${artifact.viewport} viewport`;
|
|
1471
|
+
const backHref = relativeHref(viewerPath, reportPath, artifactsDir);
|
|
1472
|
+
const rawHref = relativeHref(viewerPath, artifact.path, artifactsDir);
|
|
1473
|
+
const baselineHref = artifact.baselinePath ? relativeHref(viewerPath, artifact.baselinePath, artifactsDir) : "";
|
|
1474
|
+
const diffHref = artifact.diffPath ? relativeHref(viewerPath, artifact.diffPath, artifactsDir) : "";
|
|
1475
|
+
const scriptTag = contentFragment.script ? ` <script>
|
|
1476
|
+
${contentFragment.script}
|
|
1477
|
+
<\/script>
|
|
1478
|
+
` : "";
|
|
1479
|
+
return `<!doctype html>
|
|
1480
|
+
<html lang="en" data-theme="${escapeAttribute(viewOptions.theme)}">
|
|
1481
|
+
<head>
|
|
1482
|
+
<meta charset="utf-8" />
|
|
1483
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1484
|
+
<title>${escapeHtml(title)}</title>
|
|
1485
|
+
<style>
|
|
1486
|
+
${renderArtifactViewerShellCss()}
|
|
1487
|
+
${contentFragment.css}
|
|
1488
|
+
</style>
|
|
1489
|
+
</head>
|
|
1490
|
+
<body>
|
|
1491
|
+
<main class="viewer-page" data-report-screen-mode="${escapeAttribute(viewOptions.screenMode)}" data-theme="${escapeAttribute(viewOptions.theme)}" style="${escapeAttribute(viewportStyle)}">
|
|
1492
|
+
<header class="viewer-toolbar">
|
|
1493
|
+
<a class="viewer-link" href="${escapeAttribute(backHref)}">Report</a>
|
|
1494
|
+
<span class="viewer-title">${escapeHtml(title)}</span>
|
|
1495
|
+
<span class="viewer-pill">${escapeHtml(viewportLabel)}</span>
|
|
1496
|
+
<span class="viewer-pill">${escapeHtml(viewOptions.screenMode)}</span>
|
|
1497
|
+
<a class="viewer-link" href="${escapeAttribute(rawHref)}">Raw</a>
|
|
1498
|
+
${baselineHref ? `<a class="viewer-link" href="${escapeAttribute(baselineHref)}">Baseline</a>` : ""}
|
|
1499
|
+
${diffHref ? `<a class="viewer-link" href="${escapeAttribute(diffHref)}">Diff</a>` : ""}
|
|
1500
|
+
</header>
|
|
1501
|
+
<section class="viewer-stage" data-mobile="${mobile ? "true" : "false"}">
|
|
1502
|
+
${contentFragment.html}
|
|
1503
|
+
</section>
|
|
1504
|
+
</main>
|
|
1505
|
+
${scriptTag}
|
|
1506
|
+
</body>
|
|
1507
|
+
</html>`;
|
|
873
1508
|
}
|
|
874
|
-
function
|
|
875
|
-
if (
|
|
876
|
-
return
|
|
1509
|
+
function renderConfiguredViewportStyle(viewport) {
|
|
1510
|
+
if (!viewport) return "--config-viewport-width: 1280px; --config-viewport-height: 760px;";
|
|
1511
|
+
return `--config-viewport-width: ${viewport.width}px; --config-viewport-height: ${viewport.height}px;`;
|
|
877
1512
|
}
|
|
878
1513
|
//#endregion
|
|
879
|
-
//#region src/agent/
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
]),
|
|
898
|
-
path: z.string().min(1),
|
|
899
|
-
baselinePath: z.string().min(1).optional(),
|
|
900
|
-
diffPath: z.string().min(1).optional(),
|
|
901
|
-
hash: z.string().min(1).optional(),
|
|
902
|
-
ok: z.boolean(),
|
|
903
|
-
error: z.string().optional()
|
|
904
|
-
}).strict();
|
|
905
|
-
const agentCommandSchema = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
|
|
906
|
-
const agentRunRecordCommandsSchema = z.object({
|
|
907
|
-
replay: agentCommandSchema,
|
|
908
|
-
updateSnapshots: agentCommandSchema
|
|
909
|
-
}).strict();
|
|
910
|
-
const agentRunRecordSchema = z.object({
|
|
911
|
-
$schema: z.string().optional(),
|
|
912
|
-
version: z.literal(1),
|
|
913
|
-
name: z.string().min(1),
|
|
914
|
-
ok: z.boolean(),
|
|
915
|
-
startedAt: z.string().min(1),
|
|
916
|
-
durationMs: z.number().int().nonnegative(),
|
|
917
|
-
mode: agentRunModeSchema,
|
|
918
|
-
spec: agentFlowSpecSchema.optional(),
|
|
919
|
-
flowPath: z.string().min(1).optional(),
|
|
920
|
-
artifactsDir: z.string().min(1),
|
|
921
|
-
snapshotDir: z.string().min(1),
|
|
922
|
-
reportPath: z.string().min(1),
|
|
923
|
-
cassettePath: z.string().min(1).optional(),
|
|
924
|
-
cassetteFrameCount: z.number().int().nonnegative(),
|
|
925
|
-
replayCommand: z.string().min(1),
|
|
926
|
-
commands: agentRunRecordCommandsSchema,
|
|
927
|
-
steps: z.array(agentRecordedStepSchema),
|
|
928
|
-
artifacts: z.array(agentRunArtifactSchema),
|
|
929
|
-
errors: z.array(z.string())
|
|
930
|
-
}).strict().superRefine((record, ctx) => {
|
|
931
|
-
if (!record.cassettePath && !record.flowPath && !record.spec) ctx.addIssue({
|
|
932
|
-
code: z.ZodIssueCode.custom,
|
|
933
|
-
message: "agent run record requires cassettePath, flowPath, or spec"
|
|
934
|
-
});
|
|
935
|
-
const replayCommand = formatArgv(record.commands.replay.argv);
|
|
936
|
-
if (record.replayCommand !== replayCommand) ctx.addIssue({
|
|
937
|
-
code: z.ZodIssueCode.custom,
|
|
938
|
-
path: ["replayCommand"],
|
|
939
|
-
message: "replayCommand must match commands.replay.argv"
|
|
940
|
-
});
|
|
941
|
-
if (!isReplayArgv(record.commands.replay.argv)) ctx.addIssue({
|
|
942
|
-
code: z.ZodIssueCode.custom,
|
|
943
|
-
path: [
|
|
944
|
-
"commands",
|
|
945
|
-
"replay",
|
|
946
|
-
"argv"
|
|
947
|
-
],
|
|
948
|
-
message: "replay argv must be a ptywright agent replay command"
|
|
949
|
-
});
|
|
950
|
-
const expectedUpdateSnapshots = [...record.commands.replay.argv, "--update-snapshots"];
|
|
951
|
-
if (!sameArgv(record.commands.updateSnapshots.argv, expectedUpdateSnapshots)) ctx.addIssue({
|
|
952
|
-
code: z.ZodIssueCode.custom,
|
|
953
|
-
path: [
|
|
954
|
-
"commands",
|
|
955
|
-
"updateSnapshots",
|
|
956
|
-
"argv"
|
|
957
|
-
],
|
|
958
|
-
message: "updateSnapshots argv must extend commands.replay.argv"
|
|
959
|
-
});
|
|
960
|
-
});
|
|
961
|
-
function normalizeAgentRunRecord(input) {
|
|
1514
|
+
//#region src/agent/report_view_options.ts
|
|
1515
|
+
function isMobileViewport(viewport) {
|
|
1516
|
+
return Boolean(viewport?.isMobile || viewport?.hasTouch || (viewport?.width ?? 9999) <= 720);
|
|
1517
|
+
}
|
|
1518
|
+
function resolveReportViewOptions(result) {
|
|
1519
|
+
const launchArgSets = [readFlowLaunchArgs(result.flowPath), readCassetteLaunchArgs(result.replaySourceCassettePath ?? result.cassettePath)];
|
|
1520
|
+
const screenModeArg = readFlagValueFromArgSets(launchArgSets, "--experimental-screen-mode");
|
|
1521
|
+
const themeArg = readFlagValueFromArgSets(launchArgSets, "--theme");
|
|
1522
|
+
const fontSizeArg = readFlagValueFromArgSets(launchArgSets, "--font-size");
|
|
1523
|
+
const lineHeightArg = readFlagValueFromArgSets(launchArgSets, "--line-height");
|
|
1524
|
+
return {
|
|
1525
|
+
fontSize: parsePositiveNumber(fontSizeArg) ?? 15,
|
|
1526
|
+
lineHeight: parsePositiveNumber(lineHeightArg) ?? 1.6,
|
|
1527
|
+
screenMode: screenModeArg && screenModeArg !== "termvision" ? "plain" : "termvision",
|
|
1528
|
+
theme: themeArg === "light" ? "light" : "dark"
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function readCassetteLaunchArgs(cassettePath) {
|
|
962
1532
|
try {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
$schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-run.schema.json",
|
|
967
|
-
spec: parsed.spec ? normalizeAgentFlowSpec(parsed.spec) : void 0
|
|
968
|
-
};
|
|
969
|
-
} catch (error) {
|
|
970
|
-
if (error instanceof z.ZodError) throw new Error(`invalid agent run record: ${formatZodIssues(error)}`);
|
|
971
|
-
throw error;
|
|
1533
|
+
return normalizeStringArray(JSON.parse(readFileSync(cassettePath, "utf8")).spec?.launch?.args);
|
|
1534
|
+
} catch {
|
|
1535
|
+
return [];
|
|
972
1536
|
}
|
|
973
1537
|
}
|
|
974
|
-
function
|
|
975
|
-
|
|
1538
|
+
function readFlowLaunchArgs(flowPath) {
|
|
1539
|
+
try {
|
|
1540
|
+
return normalizeStringArray(JSON.parse(readFileSync(flowPath, "utf8")).launch?.args);
|
|
1541
|
+
} catch {
|
|
1542
|
+
return [];
|
|
1543
|
+
}
|
|
976
1544
|
}
|
|
977
|
-
function
|
|
978
|
-
return
|
|
1545
|
+
function normalizeStringArray(value) {
|
|
1546
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
979
1547
|
}
|
|
980
|
-
function
|
|
981
|
-
|
|
1548
|
+
function readFlagValueFromArgSets(argSets, flag) {
|
|
1549
|
+
for (const args of argSets) {
|
|
1550
|
+
const value = readFlagValue(args, flag);
|
|
1551
|
+
if (value !== void 0) return value;
|
|
1552
|
+
}
|
|
982
1553
|
}
|
|
983
|
-
function
|
|
984
|
-
|
|
1554
|
+
function readFlagValue(args, flag) {
|
|
1555
|
+
const index = args.indexOf(flag);
|
|
1556
|
+
return index >= 0 ? args[index + 1] : void 0;
|
|
985
1557
|
}
|
|
986
|
-
function
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1558
|
+
function parsePositiveNumber(value) {
|
|
1559
|
+
if (value === void 0) return void 0;
|
|
1560
|
+
const parsed = Number(value);
|
|
1561
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1562
|
+
}
|
|
1563
|
+
//#endregion
|
|
1564
|
+
//#region src/agent/report_artifact_viewer.ts
|
|
1565
|
+
function renderArtifactViewerHtml(args) {
|
|
1566
|
+
const { artifact, artifactsDir, content, domPreview, reportPath, terminalDomPreview, viewOptions, viewerPath, viewport } = args;
|
|
1567
|
+
const mobile = isMobileViewport(viewport);
|
|
1568
|
+
return renderArtifactViewerShellHtml({
|
|
1569
|
+
artifact,
|
|
1570
|
+
artifactsDir,
|
|
1571
|
+
contentFragment: renderArtifactViewerFragment({
|
|
1572
|
+
artifactsDir,
|
|
1573
|
+
content,
|
|
1574
|
+
domViewerPreview: artifact.kind === "dom" ? domPreview : artifact.kind === "terminal" ? terminalDomPreview : null,
|
|
1575
|
+
mobile,
|
|
1576
|
+
viewerPath,
|
|
1577
|
+
viewOptions
|
|
1578
|
+
}),
|
|
1579
|
+
mobile,
|
|
1580
|
+
reportPath,
|
|
1581
|
+
viewOptions,
|
|
1582
|
+
viewerPath,
|
|
1583
|
+
viewport
|
|
990
1584
|
});
|
|
991
|
-
writeFileSync(path, JSON.stringify(normalized, null, 2) + "\n", "utf8");
|
|
992
1585
|
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1586
|
+
//#endregion
|
|
1587
|
+
//#region src/agent/report_terminal_layout.ts
|
|
1588
|
+
function resolveTerminalSnapshotLayout(snapshot, viewport, viewOptions) {
|
|
1589
|
+
const cols = inferTerminalCols(snapshot);
|
|
1590
|
+
const rows = inferTerminalRows(snapshot);
|
|
1591
|
+
const mobile = isMobileViewport(viewport);
|
|
1592
|
+
return {
|
|
1593
|
+
cols,
|
|
1594
|
+
fontSize: viewOptions.fontSize,
|
|
1595
|
+
lineHeight: viewOptions.lineHeight,
|
|
1596
|
+
paddingInline: mobile ? 16 : 32,
|
|
1597
|
+
rows
|
|
1598
|
+
};
|
|
997
1599
|
}
|
|
998
|
-
function
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1600
|
+
function inferTerminalCols(snapshot) {
|
|
1601
|
+
const dataCols = /data-cols="(\d+)"/.exec(snapshot)?.[1];
|
|
1602
|
+
const styleCols = /--term-cols:\s*(\d+)/.exec(snapshot)?.[1];
|
|
1603
|
+
const parsed = Number.parseInt(dataCols ?? styleCols ?? "", 10);
|
|
1604
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 80;
|
|
1002
1605
|
}
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1606
|
+
function inferTerminalRows(snapshot) {
|
|
1607
|
+
const dataRows = /data-rows="(\d+)"/.exec(snapshot)?.[1];
|
|
1608
|
+
const styleRows = /--term-rows:\s*(\d+)/.exec(snapshot)?.[1];
|
|
1609
|
+
const parsed = Number.parseInt(dataRows ?? styleRows ?? "", 10);
|
|
1610
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 24;
|
|
1008
1611
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
const
|
|
1612
|
+
//#endregion
|
|
1613
|
+
//#region src/agent/report_terminal_preview.ts
|
|
1614
|
+
function renderDomPreviewDocument(snapshot, viewport, viewOptions, aittyAssets) {
|
|
1615
|
+
const body = renderAittyPreviewBody({
|
|
1616
|
+
snapshot,
|
|
1617
|
+
snapshotLayout: resolveTerminalSnapshotLayout(snapshot, viewport, viewOptions),
|
|
1618
|
+
viewOptions
|
|
1619
|
+
});
|
|
1620
|
+
const style = renderAittyPreviewCss(viewOptions);
|
|
1621
|
+
const assetTags = renderAittyPreviewAssetTags(aittyAssets);
|
|
1013
1622
|
return `<!doctype html>
|
|
1014
|
-
<html lang="en">
|
|
1623
|
+
<html lang="en" data-theme="${escapeAttribute(viewOptions.theme)}">
|
|
1015
1624
|
<head>
|
|
1016
1625
|
<meta charset="utf-8" />
|
|
1017
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1018
|
-
<
|
|
1019
|
-
|
|
1020
|
-
|
|
1626
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
1627
|
+
${assetTags} <style>
|
|
1628
|
+
${style}
|
|
1629
|
+
</style>
|
|
1630
|
+
</head>
|
|
1631
|
+
<body>
|
|
1632
|
+
${body}
|
|
1633
|
+
</body>
|
|
1634
|
+
</html>`;
|
|
1635
|
+
}
|
|
1636
|
+
//#endregion
|
|
1637
|
+
//#region src/agent/report_artifact_writer.ts
|
|
1638
|
+
function writeArtifactViewerPages(result) {
|
|
1639
|
+
const viewportsByName = new Map(result.viewports.map((viewport) => [viewport.name, viewport]));
|
|
1640
|
+
const viewOptions = resolveReportViewOptions(result);
|
|
1641
|
+
const readableArtifacts = [];
|
|
1642
|
+
for (const artifact of result.artifacts) {
|
|
1643
|
+
const viewerPath = artifactViewerPath(artifact);
|
|
1644
|
+
if (!viewerPath) continue;
|
|
1645
|
+
const content = readArtifactText(artifact.path);
|
|
1646
|
+
if (content === null) continue;
|
|
1647
|
+
readableArtifacts.push({
|
|
1648
|
+
artifact,
|
|
1649
|
+
content,
|
|
1650
|
+
viewerPath
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
const domPreviewPathsBySnapshot = new Map(readableArtifacts.filter(({ artifact }) => artifact.kind === "dom").map(({ artifact }) => [artifactSnapshotKey(artifact), artifactDomPreviewPath(artifact)]));
|
|
1654
|
+
const aittyAssets = domPreviewPathsBySnapshot.size > 0 ? prepareAittyReportAssets({
|
|
1655
|
+
artifactsDir: result.artifactsDir,
|
|
1656
|
+
flowPath: result.flowPath,
|
|
1657
|
+
reportPath: result.reportPath
|
|
1658
|
+
}) : null;
|
|
1659
|
+
const writtenDomPreviewPaths = /* @__PURE__ */ new Set();
|
|
1660
|
+
if (aittyAssets) for (const { artifact, content } of readableArtifacts) {
|
|
1661
|
+
if (artifact.kind !== "dom") continue;
|
|
1662
|
+
const viewport = viewportsByName.get(artifact.viewport);
|
|
1663
|
+
const domPreviewPath = artifactDomPreviewPath(artifact);
|
|
1664
|
+
mkdirSync(dirname(domPreviewPath), { recursive: true });
|
|
1665
|
+
writeFileSync(domPreviewPath, renderDomPreviewDocument(content, viewport, viewOptions, resolveAittyPreviewAssets(domPreviewPath, aittyAssets, result.artifactsDir)), "utf8");
|
|
1666
|
+
writtenDomPreviewPaths.add(domPreviewPath);
|
|
1667
|
+
}
|
|
1668
|
+
for (const { artifact, content, viewerPath } of readableArtifacts) {
|
|
1669
|
+
const viewport = viewportsByName.get(artifact.viewport);
|
|
1670
|
+
const domPreviewPath = artifact.kind === "dom" ? artifactDomPreviewPath(artifact) : null;
|
|
1671
|
+
mkdirSync(dirname(viewerPath), { recursive: true });
|
|
1672
|
+
writeFileSync(viewerPath, renderArtifactViewerHtml({
|
|
1673
|
+
artifact,
|
|
1674
|
+
artifactsDir: result.artifactsDir,
|
|
1675
|
+
content,
|
|
1676
|
+
reportPath: result.reportPath,
|
|
1677
|
+
viewerPath,
|
|
1678
|
+
domPreview: resolveReportDomPreview(domPreviewPath && writtenDomPreviewPaths.has(domPreviewPath) ? domPreviewPath : null),
|
|
1679
|
+
viewOptions,
|
|
1680
|
+
viewport,
|
|
1681
|
+
terminalDomPreview: artifact.kind === "terminal" ? resolveReportDomPreview(resolveWrittenDomPreviewPath(domPreviewPathsBySnapshot, writtenDomPreviewPaths, artifact)) : null
|
|
1682
|
+
}), "utf8");
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
function resolveWrittenDomPreviewPath(domPreviewPathsBySnapshot, writtenDomPreviewPaths, artifact) {
|
|
1686
|
+
const path = domPreviewPathsBySnapshot.get(artifactSnapshotKey(artifact));
|
|
1687
|
+
return path && writtenDomPreviewPaths.has(path) ? path : null;
|
|
1688
|
+
}
|
|
1689
|
+
function readArtifactText(path) {
|
|
1690
|
+
try {
|
|
1691
|
+
return readFileSync(path, "utf8");
|
|
1692
|
+
} catch {
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
//#endregion
|
|
1697
|
+
//#region src/agent/report_index_artifacts.ts
|
|
1698
|
+
function renderAgentReportArtifacts(args) {
|
|
1699
|
+
return args.artifacts.map((artifact) => renderArtifactRow({
|
|
1700
|
+
artifact,
|
|
1701
|
+
artifactsDir: args.artifactsDir,
|
|
1702
|
+
reportPath: args.reportPath
|
|
1703
|
+
})).join("\n") || "<p>No artifacts were captured.</p>";
|
|
1704
|
+
}
|
|
1705
|
+
function renderArtifactRow(args) {
|
|
1706
|
+
const { artifact, artifactsDir, reportPath } = args;
|
|
1707
|
+
const state = artifact.ok ? "pass" : "fail";
|
|
1708
|
+
const href = relativeHref(reportPath, artifact.path, artifactsDir);
|
|
1709
|
+
const baselineHref = artifact.baselinePath ? relativeHref(reportPath, artifact.baselinePath, artifactsDir) : "";
|
|
1710
|
+
const diffHref = artifact.diffPath ? relativeHref(reportPath, artifact.diffPath, artifactsDir) : "";
|
|
1711
|
+
const viewerPath = artifactViewerPath(artifact);
|
|
1712
|
+
const viewerHref = viewerPath ? relativeHref(reportPath, viewerPath, artifactsDir) : "";
|
|
1713
|
+
return `<article class="artifact">
|
|
1714
|
+
<div class="artifact-summary">
|
|
1715
|
+
<span class="badge ${state}">${state}</span>
|
|
1716
|
+
<div class="artifact-meta">
|
|
1717
|
+
<div class="artifact-links">
|
|
1718
|
+
<a href="${escapeAttribute(viewerHref || href)}">${escapeHtml(artifact.kind)}</a>
|
|
1719
|
+
${viewerHref ? `<a href="${escapeAttribute(href)}">raw</a>` : ""}
|
|
1720
|
+
${baselineHref ? `<a href="${escapeAttribute(baselineHref)}">baseline</a>` : ""}
|
|
1721
|
+
${diffHref ? `<a href="${escapeAttribute(diffHref)}">diff</a>` : ""}
|
|
1722
|
+
</div>
|
|
1723
|
+
<div><code>${escapeHtml(artifact.viewport)} / ${escapeHtml(artifact.name)}</code></div>
|
|
1724
|
+
${artifact.error ? `<div><code>${escapeHtml(artifact.error)}</code></div>` : ""}
|
|
1725
|
+
</div>
|
|
1726
|
+
<code class="artifact-hash">${escapeHtml(artifact.hash ?? "")}</code>
|
|
1727
|
+
</div>
|
|
1728
|
+
</article>`;
|
|
1729
|
+
}
|
|
1730
|
+
//#endregion
|
|
1731
|
+
//#region src/agent/report_index_commands.ts
|
|
1732
|
+
function renderAgentReportCommandBlock(label, argv) {
|
|
1733
|
+
return `<div class="command">
|
|
1734
|
+
<span>${escapeHtml(label)}</span>
|
|
1735
|
+
<pre>${escapeHtml(formatAgentArgv(argv))}</pre>
|
|
1736
|
+
</div>`;
|
|
1737
|
+
}
|
|
1738
|
+
//#endregion
|
|
1739
|
+
//#region src/agent/report_index_artifacts_css.ts
|
|
1740
|
+
function renderAgentReportArtifactsCss() {
|
|
1741
|
+
return ` .artifacts {
|
|
1742
|
+
display: grid;
|
|
1743
|
+
gap: 14px;
|
|
1744
|
+
}
|
|
1745
|
+
.artifact {
|
|
1746
|
+
display: grid;
|
|
1747
|
+
gap: 12px;
|
|
1748
|
+
border: 1px solid var(--line);
|
|
1749
|
+
border-radius: 8px;
|
|
1750
|
+
padding: 12px;
|
|
1751
|
+
background: var(--panel);
|
|
1752
|
+
}
|
|
1753
|
+
.artifact-summary {
|
|
1754
|
+
display: grid;
|
|
1755
|
+
grid-template-columns: auto minmax(0, 1fr) minmax(96px, auto);
|
|
1756
|
+
gap: 14px;
|
|
1757
|
+
align-items: start;
|
|
1758
|
+
}
|
|
1759
|
+
.artifact-meta {
|
|
1760
|
+
display: grid;
|
|
1761
|
+
gap: 4px;
|
|
1762
|
+
min-width: 0;
|
|
1763
|
+
}
|
|
1764
|
+
.artifact-links {
|
|
1765
|
+
display: flex;
|
|
1766
|
+
flex-wrap: wrap;
|
|
1767
|
+
gap: 8px 12px;
|
|
1768
|
+
}
|
|
1769
|
+
.artifact a {
|
|
1770
|
+
color: var(--focus);
|
|
1771
|
+
font-weight: 700;
|
|
1772
|
+
text-decoration: none;
|
|
1773
|
+
}
|
|
1774
|
+
.artifact code {
|
|
1775
|
+
color: var(--muted);
|
|
1776
|
+
overflow-wrap: anywhere;
|
|
1777
|
+
}
|
|
1778
|
+
.artifact-hash {
|
|
1779
|
+
justify-self: end;
|
|
1780
|
+
text-align: right;
|
|
1781
|
+
}
|
|
1782
|
+
.badge {
|
|
1783
|
+
justify-self: start;
|
|
1784
|
+
border-radius: 999px;
|
|
1785
|
+
padding: 5px 9px;
|
|
1786
|
+
background: color-mix(in oklch, var(--line) 52%, transparent);
|
|
1787
|
+
color: var(--muted);
|
|
1788
|
+
font-size: 12px;
|
|
1789
|
+
font-weight: 700;
|
|
1790
|
+
}
|
|
1791
|
+
.badge.fail {
|
|
1792
|
+
background: color-mix(in oklch, var(--bad) 12%, var(--panel));
|
|
1793
|
+
color: var(--bad);
|
|
1794
|
+
}
|
|
1795
|
+
.badge.pass {
|
|
1796
|
+
background: color-mix(in oklch, var(--good) 12%, var(--panel));
|
|
1797
|
+
color: var(--good);
|
|
1798
|
+
}
|
|
1799
|
+
@media (max-width: 720px) {
|
|
1800
|
+
.artifact-summary {
|
|
1801
|
+
grid-template-columns: 1fr;
|
|
1802
|
+
}
|
|
1803
|
+
.artifact-hash {
|
|
1804
|
+
justify-self: start;
|
|
1805
|
+
text-align: left;
|
|
1806
|
+
}
|
|
1807
|
+
}`;
|
|
1808
|
+
}
|
|
1809
|
+
//#endregion
|
|
1810
|
+
//#region src/agent/report_index_base_css.ts
|
|
1811
|
+
function renderAgentReportBaseCss() {
|
|
1812
|
+
return ` :root {
|
|
1021
1813
|
color-scheme: light;
|
|
1022
1814
|
--bg: oklch(97.5% 0.008 210);
|
|
1023
1815
|
--ink: oklch(19% 0.018 230);
|
|
@@ -1085,10 +1877,16 @@ function renderAgentReportHtml(result) {
|
|
|
1085
1877
|
font-size: 13px;
|
|
1086
1878
|
}
|
|
1087
1879
|
.status {
|
|
1088
|
-
border-color: ${result.ok ? "color-mix(in oklch, var(--good) 42%, var(--line))" : "color-mix(in oklch, var(--bad) 44%, var(--line))"};
|
|
1089
|
-
color: ${result.ok ? "var(--good)" : "var(--bad)"};
|
|
1090
1880
|
font-weight: 700;
|
|
1091
1881
|
}
|
|
1882
|
+
.status.pass {
|
|
1883
|
+
border-color: color-mix(in oklch, var(--good) 42%, var(--line));
|
|
1884
|
+
color: var(--good);
|
|
1885
|
+
}
|
|
1886
|
+
.status.fail {
|
|
1887
|
+
border-color: color-mix(in oklch, var(--bad) 44%, var(--line));
|
|
1888
|
+
color: var(--bad);
|
|
1889
|
+
}
|
|
1092
1890
|
.panel {
|
|
1093
1891
|
display: grid;
|
|
1094
1892
|
gap: 16px;
|
|
@@ -1097,73 +1895,23 @@ function renderAgentReportHtml(result) {
|
|
|
1097
1895
|
background: var(--panel);
|
|
1098
1896
|
padding: 18px;
|
|
1099
1897
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
.
|
|
1117
|
-
display: block;
|
|
1118
|
-
margin-top: 4px;
|
|
1119
|
-
color: var(--muted);
|
|
1120
|
-
font-size: 13px;
|
|
1121
|
-
}
|
|
1122
|
-
.artifacts {
|
|
1123
|
-
display: grid;
|
|
1124
|
-
gap: 10px;
|
|
1125
|
-
}
|
|
1126
|
-
.artifact {
|
|
1127
|
-
display: grid;
|
|
1128
|
-
grid-template-columns: 120px minmax(0, 1fr) auto;
|
|
1129
|
-
gap: 14px;
|
|
1130
|
-
align-items: center;
|
|
1131
|
-
border: 1px solid var(--line);
|
|
1132
|
-
border-radius: 8px;
|
|
1133
|
-
padding: 12px;
|
|
1134
|
-
background: var(--panel);
|
|
1135
|
-
}
|
|
1136
|
-
.artifact a {
|
|
1137
|
-
color: var(--focus);
|
|
1138
|
-
font-weight: 700;
|
|
1139
|
-
text-decoration: none;
|
|
1140
|
-
}
|
|
1141
|
-
.artifact code,
|
|
1142
|
-
pre {
|
|
1143
|
-
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
1144
|
-
}
|
|
1145
|
-
.artifact code {
|
|
1146
|
-
color: var(--muted);
|
|
1147
|
-
overflow-wrap: anywhere;
|
|
1148
|
-
}
|
|
1149
|
-
.badge {
|
|
1150
|
-
justify-self: start;
|
|
1151
|
-
border-radius: 999px;
|
|
1152
|
-
padding: 5px 9px;
|
|
1153
|
-
background: color-mix(in oklch, var(--line) 52%, transparent);
|
|
1154
|
-
color: var(--muted);
|
|
1155
|
-
font-size: 12px;
|
|
1156
|
-
font-weight: 700;
|
|
1157
|
-
}
|
|
1158
|
-
.badge.fail {
|
|
1159
|
-
background: color-mix(in oklch, var(--bad) 12%, var(--panel));
|
|
1160
|
-
color: var(--bad);
|
|
1161
|
-
}
|
|
1162
|
-
.badge.pass {
|
|
1163
|
-
background: color-mix(in oklch, var(--good) 12%, var(--panel));
|
|
1164
|
-
color: var(--good);
|
|
1165
|
-
}
|
|
1166
|
-
.commands {
|
|
1898
|
+
@media (max-width: 720px) {
|
|
1899
|
+
main {
|
|
1900
|
+
width: min(100vw - 20px, 1180px);
|
|
1901
|
+
padding-top: 18px;
|
|
1902
|
+
}
|
|
1903
|
+
header {
|
|
1904
|
+
grid-template-columns: 1fr;
|
|
1905
|
+
}
|
|
1906
|
+
.status {
|
|
1907
|
+
justify-self: start;
|
|
1908
|
+
}
|
|
1909
|
+
}`;
|
|
1910
|
+
}
|
|
1911
|
+
//#endregion
|
|
1912
|
+
//#region src/agent/report_index_commands_css.ts
|
|
1913
|
+
function renderAgentReportCommandsCss() {
|
|
1914
|
+
return ` .commands {
|
|
1167
1915
|
display: grid;
|
|
1168
1916
|
gap: 10px;
|
|
1169
1917
|
}
|
|
@@ -1176,6 +1924,10 @@ function renderAgentReportHtml(result) {
|
|
|
1176
1924
|
font-size: 13px;
|
|
1177
1925
|
font-weight: 700;
|
|
1178
1926
|
}
|
|
1927
|
+
.artifact code,
|
|
1928
|
+
pre {
|
|
1929
|
+
font-family: ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
|
1930
|
+
}
|
|
1179
1931
|
pre {
|
|
1180
1932
|
overflow: auto;
|
|
1181
1933
|
margin: 0;
|
|
@@ -1184,20 +1936,63 @@ function renderAgentReportHtml(result) {
|
|
|
1184
1936
|
color: oklch(92% 0.012 230);
|
|
1185
1937
|
padding: 14px;
|
|
1186
1938
|
line-height: 1.5;
|
|
1939
|
+
}`;
|
|
1940
|
+
}
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/agent/report_index_summary_css.ts
|
|
1943
|
+
function renderAgentReportSummaryCss() {
|
|
1944
|
+
return ` .summary {
|
|
1945
|
+
display: grid;
|
|
1946
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
1947
|
+
gap: 12px;
|
|
1187
1948
|
}
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
.
|
|
1198
|
-
justify-self: start;
|
|
1199
|
-
}
|
|
1949
|
+
.metric {
|
|
1950
|
+
border: 1px solid var(--line);
|
|
1951
|
+
border-radius: 8px;
|
|
1952
|
+
padding: 14px;
|
|
1953
|
+
background: color-mix(in oklch, var(--panel) 82%, var(--bg));
|
|
1954
|
+
}
|
|
1955
|
+
.metric strong {
|
|
1956
|
+
display: block;
|
|
1957
|
+
font-size: 24px;
|
|
1958
|
+
line-height: 1.1;
|
|
1200
1959
|
}
|
|
1960
|
+
.metric span {
|
|
1961
|
+
display: block;
|
|
1962
|
+
margin-top: 4px;
|
|
1963
|
+
color: var(--muted);
|
|
1964
|
+
font-size: 13px;
|
|
1965
|
+
}`;
|
|
1966
|
+
}
|
|
1967
|
+
//#endregion
|
|
1968
|
+
//#region src/agent/report_index_css.ts
|
|
1969
|
+
function renderAgentReportCss() {
|
|
1970
|
+
return [
|
|
1971
|
+
renderAgentReportBaseCss(),
|
|
1972
|
+
renderAgentReportSummaryCss(),
|
|
1973
|
+
renderAgentReportArtifactsCss(),
|
|
1974
|
+
renderAgentReportCommandsCss()
|
|
1975
|
+
].join("\n");
|
|
1976
|
+
}
|
|
1977
|
+
//#endregion
|
|
1978
|
+
//#region src/agent/report_index.ts
|
|
1979
|
+
function renderAgentReportHtml(result) {
|
|
1980
|
+
const artifacts = renderAgentReportArtifacts({
|
|
1981
|
+
artifacts: result.artifacts,
|
|
1982
|
+
artifactsDir: result.artifactsDir,
|
|
1983
|
+
reportPath: result.reportPath
|
|
1984
|
+
});
|
|
1985
|
+
const viewportTabs = result.viewports.map((viewport) => `<span class="pill">${escapeHtml(viewport.name)} ${viewport.width}x${viewport.height}</span>`).join("");
|
|
1986
|
+
const status = result.ok ? "passed" : "failed";
|
|
1987
|
+
const statusClass = result.ok ? "pass" : "fail";
|
|
1988
|
+
return `<!doctype html>
|
|
1989
|
+
<html lang="en">
|
|
1990
|
+
<head>
|
|
1991
|
+
<meta charset="utf-8" />
|
|
1992
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1993
|
+
<title>${escapeHtml(`${result.name} terminal-agent report`)}</title>
|
|
1994
|
+
<style>
|
|
1995
|
+
${renderAgentReportCss()}
|
|
1201
1996
|
</style>
|
|
1202
1997
|
</head>
|
|
1203
1998
|
<body>
|
|
@@ -1206,7 +2001,7 @@ function renderAgentReportHtml(result) {
|
|
|
1206
2001
|
<div>
|
|
1207
2002
|
<h1>${escapeHtml(result.name)}</h1>
|
|
1208
2003
|
<div class="meta">
|
|
1209
|
-
<span class="status">${status}</span>
|
|
2004
|
+
<span class="status ${statusClass}">${status}</span>
|
|
1210
2005
|
<span class="pill">${escapeHtml(result.mode)}</span>
|
|
1211
2006
|
<span class="pill">${escapeHtml(result.agentFlavor)}</span>
|
|
1212
2007
|
${viewportTabs}
|
|
@@ -1225,9 +2020,9 @@ function renderAgentReportHtml(result) {
|
|
|
1225
2020
|
<section class="panel">
|
|
1226
2021
|
<h2>Commands</h2>
|
|
1227
2022
|
<div class="commands">
|
|
1228
|
-
${
|
|
1229
|
-
${
|
|
1230
|
-
${
|
|
2023
|
+
${renderAgentReportCommandBlock("replay", result.commands.replay.argv)}
|
|
2024
|
+
${renderAgentReportCommandBlock("update snapshots", result.commands.updateSnapshots.argv)}
|
|
2025
|
+
${renderAgentReportCommandBlock("inspect commands", [
|
|
1231
2026
|
"ptywright",
|
|
1232
2027
|
"agent",
|
|
1233
2028
|
"commands",
|
|
@@ -1241,84 +2036,266 @@ function renderAgentReportHtml(result) {
|
|
|
1241
2036
|
<section class="panel">
|
|
1242
2037
|
<h2>Terminal Agent Artifacts</h2>
|
|
1243
2038
|
<div class="artifacts">
|
|
1244
|
-
${artifacts
|
|
2039
|
+
${artifacts}
|
|
1245
2040
|
</div>
|
|
1246
2041
|
</section>
|
|
1247
2042
|
</main>
|
|
1248
2043
|
</body>
|
|
1249
2044
|
</html>`;
|
|
1250
2045
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
2046
|
+
//#endregion
|
|
2047
|
+
//#region src/agent/report.ts
|
|
2048
|
+
function writeAgentReport(path, result) {
|
|
2049
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2050
|
+
writeArtifactViewerPages(result);
|
|
2051
|
+
writeFileSync(path, renderAgentReportHtml(result), "utf8");
|
|
2052
|
+
}
|
|
2053
|
+
//#endregion
|
|
2054
|
+
//#region src/agent/run_artifacts.ts
|
|
2055
|
+
function writeRunRecord(result, spec) {
|
|
2056
|
+
const record = {
|
|
2057
|
+
$schema: AGENT_RUN_RECORD_SCHEMA_URL,
|
|
2058
|
+
version: 1,
|
|
2059
|
+
name: result.name,
|
|
2060
|
+
ok: result.ok,
|
|
2061
|
+
startedAt: new Date(result.startedAt).toISOString(),
|
|
2062
|
+
durationMs: result.durationMs,
|
|
2063
|
+
mode: result.mode,
|
|
2064
|
+
spec,
|
|
2065
|
+
flowPath: relative(dirname(result.recordPath), result.flowPath),
|
|
2066
|
+
artifactsDir: result.artifactsDir,
|
|
2067
|
+
snapshotDir: result.snapshotDir,
|
|
2068
|
+
reportPath: result.reportPath,
|
|
2069
|
+
cassettePath: relative(dirname(result.recordPath), result.cassettePath),
|
|
2070
|
+
cassetteFrameCount: result.cassetteFrameCount,
|
|
2071
|
+
replayCommand: result.replayCommand,
|
|
2072
|
+
commands: result.commands,
|
|
2073
|
+
steps: result.steps,
|
|
2074
|
+
artifacts: result.artifacts,
|
|
2075
|
+
errors: result.errors
|
|
2076
|
+
};
|
|
2077
|
+
writeAgentRunRecordPath(result.recordPath, record);
|
|
2078
|
+
}
|
|
2079
|
+
function writeRunManifest(result) {
|
|
2080
|
+
writeAgentManifestPath(agentManifestPath(result.artifactsDir), {
|
|
2081
|
+
kind: "run",
|
|
2082
|
+
ok: result.ok,
|
|
2083
|
+
rootDir: result.artifactsDir,
|
|
2084
|
+
primaryPath: result.recordPath,
|
|
2085
|
+
commands: result.commands,
|
|
2086
|
+
validation: {
|
|
2087
|
+
ok: result.ok,
|
|
2088
|
+
stages: [{
|
|
2089
|
+
name: "run",
|
|
2090
|
+
ok: result.ok,
|
|
2091
|
+
totalCount: result.artifacts.length,
|
|
2092
|
+
failureCount: result.artifacts.filter((artifact) => !artifact.ok).length
|
|
2093
|
+
}]
|
|
2094
|
+
},
|
|
2095
|
+
files: [
|
|
2096
|
+
{
|
|
2097
|
+
path: result.flowPath,
|
|
2098
|
+
kind: "flow",
|
|
2099
|
+
role: "flow"
|
|
2100
|
+
},
|
|
2101
|
+
{
|
|
2102
|
+
path: result.cassettePath,
|
|
2103
|
+
kind: "cassette",
|
|
2104
|
+
role: "cassette"
|
|
2105
|
+
},
|
|
2106
|
+
{
|
|
2107
|
+
path: result.recordPath,
|
|
2108
|
+
kind: "run-record",
|
|
2109
|
+
role: "record",
|
|
2110
|
+
ok: result.ok
|
|
2111
|
+
},
|
|
2112
|
+
{
|
|
2113
|
+
path: result.reportPath,
|
|
2114
|
+
kind: "report",
|
|
2115
|
+
role: "report",
|
|
2116
|
+
ok: result.ok
|
|
2117
|
+
},
|
|
2118
|
+
...result.artifacts.flatMap((artifact) => [{
|
|
2119
|
+
path: artifact.path,
|
|
2120
|
+
kind: artifact.kind,
|
|
2121
|
+
role: "artifact",
|
|
2122
|
+
ok: artifact.ok
|
|
2123
|
+
}, {
|
|
2124
|
+
path: artifact.diffPath,
|
|
2125
|
+
kind: "diff",
|
|
2126
|
+
role: "diff",
|
|
2127
|
+
ok: artifact.ok
|
|
2128
|
+
}])
|
|
2129
|
+
]
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
function writeFlowArtifact(path, spec) {
|
|
2133
|
+
writeFileSync(path, JSON.stringify(spec, null, 2) + "\n", "utf8");
|
|
2134
|
+
}
|
|
2135
|
+
//#endregion
|
|
2136
|
+
//#region src/agent/config_defaults.ts
|
|
2137
|
+
function normalizeAgentFlowSpecWithConfig(input, config) {
|
|
2138
|
+
return normalizeAgentFlowSpec(applyAgentConfigDefaults(agentFlowSpecSchema.parse(input), config));
|
|
2139
|
+
}
|
|
2140
|
+
function applyAgentConfigDefaults(input, config) {
|
|
2141
|
+
const agent = config?.agent;
|
|
2142
|
+
if (!agent) return input;
|
|
2143
|
+
const name = sanitizeArtifactName(input.name ?? "agent-flow");
|
|
2144
|
+
const configDefaults = agent.defaults ?? {};
|
|
2145
|
+
const specDefaults = input.defaults ?? {};
|
|
2146
|
+
const viewports = input.viewports ? void 0 : cloneViewports(configDefaults.viewports);
|
|
2147
|
+
return {
|
|
2148
|
+
...input,
|
|
2149
|
+
artifactsDir: input.artifactsDir ?? resolveNamedDir(agent.artifactsRoot, name, config.rootDir),
|
|
2150
|
+
snapshotDir: input.snapshotDir ?? resolveNamedDir(agent.snapshotDir, name, config.rootDir),
|
|
2151
|
+
viewports: viewports ?? input.viewports,
|
|
2152
|
+
defaults: {
|
|
2153
|
+
...specDefaults,
|
|
2154
|
+
timeoutMs: specDefaults.timeoutMs ?? configDefaults.timeoutMs,
|
|
2155
|
+
screenshot: specDefaults.screenshot ?? configDefaults.screenshot,
|
|
2156
|
+
mask: mergeMaskRules(configDefaults.mask, specDefaults.mask)
|
|
2157
|
+
}
|
|
2158
|
+
};
|
|
2159
|
+
}
|
|
2160
|
+
function resolveNamedDir(root, name, configRoot) {
|
|
2161
|
+
if (!root) return void 0;
|
|
2162
|
+
const namedDir = join(root, name);
|
|
2163
|
+
return isAbsolute(namedDir) ? namedDir : resolve(configRoot, namedDir);
|
|
2164
|
+
}
|
|
2165
|
+
function cloneViewports(viewports) {
|
|
2166
|
+
return Array.isArray(viewports) && viewports.length > 0 ? viewports.map((viewport) => ({ ...viewport })) : void 0;
|
|
2167
|
+
}
|
|
2168
|
+
function mergeMaskRules(configMask, specMask) {
|
|
2169
|
+
const merged = [...configMask ?? [], ...specMask ?? []];
|
|
2170
|
+
return merged.length > 0 ? merged : void 0;
|
|
2171
|
+
}
|
|
2172
|
+
//#endregion
|
|
2173
|
+
//#region src/agent/command_launch.ts
|
|
2174
|
+
const DEFAULT_URL_REGEX = /https?:\/\/[^\s"'<>]+/;
|
|
2175
|
+
function buildCommandLaunchCommand(launch, options = {}) {
|
|
2176
|
+
if (!launch.command) throw new Error("launch.command is required when launch.mode is 'command'");
|
|
2177
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
2178
|
+
const cwd = launch.cwd ? resolve(rootDir, launch.cwd) : rootDir;
|
|
2179
|
+
return {
|
|
2180
|
+
file: launch.command,
|
|
2181
|
+
args: launch.args ?? [],
|
|
2182
|
+
cwd,
|
|
2183
|
+
env: {
|
|
2184
|
+
...options.env ?? process.env,
|
|
2185
|
+
...launch.env
|
|
2186
|
+
},
|
|
2187
|
+
label: launch.command,
|
|
2188
|
+
urlRegex: launch.urlRegex,
|
|
2189
|
+
waitForUrlMs: launch.waitForUrlMs
|
|
2190
|
+
};
|
|
1256
2191
|
}
|
|
1257
|
-
function
|
|
1258
|
-
const
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
2192
|
+
async function launchBrowserSessionFromCommand(command) {
|
|
2193
|
+
const timeoutMs = command.waitForUrlMs ?? 15e3;
|
|
2194
|
+
const child = spawn(command.file, command.args, {
|
|
2195
|
+
cwd: command.cwd,
|
|
2196
|
+
env: command.env,
|
|
2197
|
+
stdio: [
|
|
2198
|
+
"ignore",
|
|
2199
|
+
"pipe",
|
|
2200
|
+
"pipe"
|
|
2201
|
+
]
|
|
2202
|
+
});
|
|
2203
|
+
const stdoutChunks = [];
|
|
2204
|
+
const stderrChunks = [];
|
|
2205
|
+
return {
|
|
2206
|
+
url: await new Promise((resolveUrl, reject) => {
|
|
2207
|
+
let settled = false;
|
|
2208
|
+
const timer = setTimeout(() => {
|
|
2209
|
+
finish(/* @__PURE__ */ new Error(`timed out after ${timeoutMs}ms waiting for ${command.label ?? command.file} session URL\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
2210
|
+
}, timeoutMs);
|
|
2211
|
+
const finish = (result) => {
|
|
2212
|
+
if (settled) return;
|
|
2213
|
+
settled = true;
|
|
2214
|
+
clearTimeout(timer);
|
|
2215
|
+
child.stdout.off("data", onStdout);
|
|
2216
|
+
child.stderr.off("data", onStderr);
|
|
2217
|
+
child.off("error", onError);
|
|
2218
|
+
child.off("exit", onExit);
|
|
2219
|
+
if (result instanceof Error) reject(result);
|
|
2220
|
+
else resolveUrl(result);
|
|
2221
|
+
};
|
|
2222
|
+
const readUrl = () => {
|
|
2223
|
+
const found = extractUrlFromOutput(`${stdoutChunks.join("")}\n${stderrChunks.join("")}`, command.urlRegex);
|
|
2224
|
+
if (found) finish(found);
|
|
2225
|
+
};
|
|
2226
|
+
const onStdout = (chunk) => {
|
|
2227
|
+
stdoutChunks.push(chunk.toString("utf8"));
|
|
2228
|
+
readUrl();
|
|
2229
|
+
};
|
|
2230
|
+
const onStderr = (chunk) => {
|
|
2231
|
+
stderrChunks.push(chunk.toString("utf8"));
|
|
2232
|
+
readUrl();
|
|
2233
|
+
};
|
|
2234
|
+
const onError = (error) => finish(error);
|
|
2235
|
+
const onExit = (code, signal) => {
|
|
2236
|
+
finish(/* @__PURE__ */ new Error(`${command.label ?? command.file} exited before printing a session URL (code=${code ?? "null"} signal=${signal ?? "null"})\nstdout=${stdoutChunks.join("").trim()}\nstderr=${stderrChunks.join("").trim()}`));
|
|
2237
|
+
};
|
|
2238
|
+
child.stdout.on("data", onStdout);
|
|
2239
|
+
child.stderr.on("data", onStderr);
|
|
2240
|
+
child.once("error", onError);
|
|
2241
|
+
child.once("exit", onExit);
|
|
2242
|
+
}),
|
|
2243
|
+
process: child,
|
|
2244
|
+
close: () => closeChild(child)
|
|
2245
|
+
};
|
|
1273
2246
|
}
|
|
1274
|
-
function
|
|
1275
|
-
if (
|
|
1276
|
-
|
|
2247
|
+
function extractUrlFromOutput(output, regexSource) {
|
|
2248
|
+
if (!regexSource) return output.match(DEFAULT_URL_REGEX)?.[0] ?? null;
|
|
2249
|
+
const match = output.match(new RegExp(regexSource, "m"));
|
|
2250
|
+
return match?.[1] ?? match?.[0] ?? null;
|
|
1277
2251
|
}
|
|
1278
|
-
function
|
|
1279
|
-
return
|
|
2252
|
+
function formatBrowserLaunchCommand(command) {
|
|
2253
|
+
return [command.file, ...command.args].join(" ");
|
|
1280
2254
|
}
|
|
1281
|
-
function
|
|
1282
|
-
|
|
2255
|
+
async function closeChild(child) {
|
|
2256
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
2257
|
+
await new Promise((resolveClose) => {
|
|
2258
|
+
const timer = setTimeout(() => {
|
|
2259
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
2260
|
+
resolveClose();
|
|
2261
|
+
}, 2e3);
|
|
2262
|
+
child.once("exit", () => {
|
|
2263
|
+
clearTimeout(timer);
|
|
2264
|
+
resolveClose();
|
|
2265
|
+
});
|
|
2266
|
+
child.kill("SIGTERM");
|
|
2267
|
+
});
|
|
1283
2268
|
}
|
|
1284
2269
|
//#endregion
|
|
1285
|
-
//#region src/agent/
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
2270
|
+
//#region src/agent/launch.ts
|
|
2271
|
+
function buildAgentLaunchCommand(launch, options = {}) {
|
|
2272
|
+
if (resolveAgentLaunchMode(launch) === "url") return null;
|
|
2273
|
+
return buildCommandLaunchCommand(launch, options);
|
|
2274
|
+
}
|
|
2275
|
+
async function resolveAgentLaunchTarget(launch, options = {}) {
|
|
2276
|
+
const mode = resolveAgentLaunchMode(launch);
|
|
2277
|
+
if (mode === "url") return {
|
|
2278
|
+
mode,
|
|
2279
|
+
url: launch.url,
|
|
2280
|
+
session: null
|
|
1291
2281
|
};
|
|
1292
|
-
const
|
|
2282
|
+
const session = await launchBrowserSessionFromCommand(buildCommandLaunchCommand(launch, options));
|
|
1293
2283
|
return {
|
|
1294
|
-
|
|
1295
|
-
|
|
2284
|
+
mode,
|
|
2285
|
+
url: session.url,
|
|
2286
|
+
session
|
|
1296
2287
|
};
|
|
1297
2288
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
return runAgentSpec((await loadAgentSpec(specPath)).spec, options);
|
|
1302
|
-
}
|
|
1303
|
-
async function replayAgentRecordPath(recordPath, options = {}) {
|
|
1304
|
-
const raw = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
1305
|
-
if (isAgentCassetteLike(raw)) return replayAgentCassette(normalizeAgentCassette(raw), recordPath, options);
|
|
1306
|
-
const record = readAgentRunRecordPath(recordPath);
|
|
1307
|
-
if (record.cassettePath) {
|
|
1308
|
-
const cassettePath = isAbsolute(record.cassettePath) ? record.cassettePath : resolve(dirname(recordPath), record.cassettePath);
|
|
1309
|
-
return replayAgentCassette(readAgentCassettePath(cassettePath, record.spec), cassettePath, {
|
|
1310
|
-
...options,
|
|
1311
|
-
artifactsDir: options.artifactsDir ?? join(dirname(recordPath), "replay")
|
|
1312
|
-
});
|
|
1313
|
-
}
|
|
1314
|
-
if (record.spec) return runAgentSpec(record.spec, options);
|
|
1315
|
-
if (!record.flowPath) throw new Error(`invalid agent run record: missing replay source in ${recordPath}`);
|
|
1316
|
-
return runAgentSpecPath(isAbsolute(record.flowPath) ? record.flowPath : resolve(dirname(recordPath), record.flowPath), options);
|
|
2289
|
+
function formatAgentLaunchCommand(launch) {
|
|
2290
|
+
const command = buildAgentLaunchCommand(launch);
|
|
2291
|
+
return command ? formatBrowserLaunchCommand(command) : "launch.mode=url";
|
|
1317
2292
|
}
|
|
1318
|
-
|
|
2293
|
+
//#endregion
|
|
2294
|
+
//#region src/agent/runner_setup.ts
|
|
2295
|
+
function prepareAgentRun(input, options) {
|
|
1319
2296
|
const startedAt = Date.now();
|
|
1320
2297
|
const rootDir = options.rootDir ? resolve(process.cwd(), options.rootDir) : process.cwd();
|
|
1321
|
-
const spec =
|
|
2298
|
+
const spec = normalizeAgentFlowSpecWithConfig(input, options.replayCassette ? void 0 : options.config);
|
|
1322
2299
|
const name = sanitizeArtifactName(spec.name ?? "agent-flow");
|
|
1323
2300
|
const artifactsDir = resolve(rootDir, options.artifactsDir ?? spec.artifactsDir ?? join(".tmp", "agent", name));
|
|
1324
2301
|
const snapshotDir = resolve(rootDir, spec.snapshotDir ?? join("snapshots", name));
|
|
@@ -1336,161 +2313,51 @@ async function runAgentSpec(input, options = {}) {
|
|
|
1336
2313
|
"replay",
|
|
1337
2314
|
relative(process.cwd(), recordPath)
|
|
1338
2315
|
];
|
|
1339
|
-
|
|
1340
|
-
ok: true,
|
|
1341
|
-
name,
|
|
1342
|
-
mode: options.replayCassette ? "replay" : "live",
|
|
2316
|
+
return {
|
|
1343
2317
|
startedAt,
|
|
1344
|
-
|
|
1345
|
-
artifactsDir,
|
|
1346
|
-
snapshotDir,
|
|
1347
|
-
reportPath,
|
|
1348
|
-
recordPath,
|
|
1349
|
-
flowPath,
|
|
1350
|
-
cassettePath,
|
|
1351
|
-
replaySourceCassettePath: options.replaySourceCassettePath,
|
|
1352
|
-
replayCommand: formatAgentArgv(replayArgv),
|
|
1353
|
-
commands: {
|
|
1354
|
-
replay: { argv: replayArgv },
|
|
1355
|
-
updateSnapshots: { argv: [...replayArgv, "--update-snapshots"] }
|
|
1356
|
-
},
|
|
1357
|
-
agentFlavor: resolveAgentFlavor(spec),
|
|
1358
|
-
viewports: spec.viewports ?? [],
|
|
1359
|
-
cassetteFrameCount: cassette.frames.length,
|
|
1360
|
-
steps: [],
|
|
1361
|
-
artifacts: [],
|
|
1362
|
-
errors: []
|
|
1363
|
-
};
|
|
1364
|
-
writeFileSync(flowPath, JSON.stringify(spec, null, 2) + "\n", "utf8");
|
|
1365
|
-
let browser = null;
|
|
1366
|
-
try {
|
|
1367
|
-
browser = await launchAgentBrowser({ headless: options.headless ?? true });
|
|
1368
|
-
for (const viewport of spec.viewports ?? []) await runViewport({
|
|
1369
|
-
browser,
|
|
1370
|
-
spec,
|
|
1371
|
-
viewport,
|
|
1372
|
-
rootDir,
|
|
1373
|
-
artifactsDir,
|
|
1374
|
-
snapshotDir,
|
|
1375
|
-
updateSnapshots,
|
|
1376
|
-
recordCassette: !options.replayCassette,
|
|
1377
|
-
cassette,
|
|
1378
|
-
result
|
|
1379
|
-
});
|
|
1380
|
-
} catch (error) {
|
|
1381
|
-
result.ok = false;
|
|
1382
|
-
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
1383
|
-
} finally {
|
|
1384
|
-
await closeBrowserSafely(browser);
|
|
1385
|
-
result.durationMs = Date.now() - startedAt;
|
|
1386
|
-
if (options.replaySourceCassettePath) writeFileSync(cassettePath, readFileSync(options.replaySourceCassettePath, "utf8"), "utf8");
|
|
1387
|
-
else writeFileSync(cassettePath, JSON.stringify(cassette, null, 2) + "\n", "utf8");
|
|
1388
|
-
result.cassetteFrameCount = cassette.frames.length;
|
|
1389
|
-
writeRunRecord(result, spec);
|
|
1390
|
-
writeAgentReport(reportPath, result);
|
|
1391
|
-
writeRunManifest(result);
|
|
1392
|
-
}
|
|
1393
|
-
return result;
|
|
1394
|
-
}
|
|
1395
|
-
async function runViewport(args) {
|
|
1396
|
-
const { browser, spec, viewport, rootDir, artifactsDir, snapshotDir, updateSnapshots, recordCassette, cassette, result } = args;
|
|
1397
|
-
const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
|
|
1398
|
-
const context = await browser.newContext({
|
|
1399
|
-
viewport: {
|
|
1400
|
-
width: viewport.width,
|
|
1401
|
-
height: viewport.height
|
|
1402
|
-
},
|
|
1403
|
-
deviceScaleFactor: viewport.deviceScaleFactor,
|
|
1404
|
-
isMobile: viewport.isMobile,
|
|
1405
|
-
hasTouch: viewport.hasTouch
|
|
1406
|
-
});
|
|
1407
|
-
const page = await context.newPage();
|
|
1408
|
-
const ctx = {
|
|
2318
|
+
rootDir,
|
|
1409
2319
|
spec,
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
stepIndex: i,
|
|
1446
|
-
stepType: step.type
|
|
1447
|
-
});
|
|
1448
|
-
} catch (error) {
|
|
1449
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1450
|
-
result.ok = false;
|
|
1451
|
-
result.errors.push(`${viewport.name} step ${i + 1} ${step.type}: ${message}`);
|
|
1452
|
-
result.steps.push({
|
|
1453
|
-
index: i,
|
|
1454
|
-
type: step.type,
|
|
1455
|
-
label: formatStepLabel(step),
|
|
1456
|
-
durationMs: Date.now() - started,
|
|
1457
|
-
ok: false,
|
|
1458
|
-
error: message
|
|
1459
|
-
});
|
|
1460
|
-
break;
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
} finally {
|
|
1464
|
-
await withTimeout(context.close().catch(() => void 0), 5e3);
|
|
1465
|
-
await launchTarget.session?.close();
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
async function replayAgentCassette(cassette, cassettePath, options) {
|
|
1469
|
-
const server = await startAgentCassetteServer(cassette);
|
|
1470
|
-
try {
|
|
1471
|
-
const replaySpec = structuredClone(cassette.spec);
|
|
1472
|
-
const replayCassette = structuredClone(cassette);
|
|
1473
|
-
return await runAgentSpec({
|
|
1474
|
-
...replaySpec,
|
|
1475
|
-
launch: {
|
|
1476
|
-
mode: "url",
|
|
1477
|
-
url: server.url,
|
|
1478
|
-
agentFlavor: replaySpec.launch.agentFlavor
|
|
1479
|
-
}
|
|
1480
|
-
}, {
|
|
1481
|
-
...options,
|
|
1482
|
-
artifactsDir: options.artifactsDir ?? join(dirname(cassettePath), "replay"),
|
|
1483
|
-
replayCassette,
|
|
1484
|
-
replaySourceCassettePath: cassettePath
|
|
1485
|
-
});
|
|
1486
|
-
} finally {
|
|
1487
|
-
await server.close();
|
|
1488
|
-
}
|
|
2320
|
+
name,
|
|
2321
|
+
artifactsDir,
|
|
2322
|
+
snapshotDir,
|
|
2323
|
+
reportPath,
|
|
2324
|
+
recordPath,
|
|
2325
|
+
flowPath,
|
|
2326
|
+
cassettePath,
|
|
2327
|
+
updateSnapshots,
|
|
2328
|
+
cassette,
|
|
2329
|
+
result: {
|
|
2330
|
+
ok: true,
|
|
2331
|
+
name,
|
|
2332
|
+
mode: options.replayCassette ? "replay" : "live",
|
|
2333
|
+
startedAt,
|
|
2334
|
+
durationMs: 0,
|
|
2335
|
+
artifactsDir,
|
|
2336
|
+
snapshotDir,
|
|
2337
|
+
reportPath,
|
|
2338
|
+
recordPath,
|
|
2339
|
+
flowPath,
|
|
2340
|
+
cassettePath,
|
|
2341
|
+
replaySourceCassettePath: options.replaySourceCassettePath,
|
|
2342
|
+
replayCommand: formatAgentArgv(replayArgv),
|
|
2343
|
+
commands: {
|
|
2344
|
+
replay: { argv: replayArgv },
|
|
2345
|
+
updateSnapshots: { argv: [...replayArgv, "--update-snapshots"] }
|
|
2346
|
+
},
|
|
2347
|
+
agentFlavor: resolveAgentFlavor(spec),
|
|
2348
|
+
viewports: spec.viewports ?? [],
|
|
2349
|
+
cassetteFrameCount: cassette.frames.length,
|
|
2350
|
+
steps: [],
|
|
2351
|
+
artifacts: [],
|
|
2352
|
+
errors: []
|
|
2353
|
+
}
|
|
2354
|
+
};
|
|
1489
2355
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
await withTimeout(browser.close().catch(() => void 0), 5e3);
|
|
2356
|
+
function formatAgentLaunchPlan(input) {
|
|
2357
|
+
return formatAgentLaunchCommand(normalizeAgentFlowSpec(input).launch);
|
|
1493
2358
|
}
|
|
2359
|
+
//#endregion
|
|
2360
|
+
//#region src/agent/timeout.ts
|
|
1494
2361
|
async function withTimeout(promise, timeoutMs) {
|
|
1495
2362
|
let timer;
|
|
1496
2363
|
try {
|
|
@@ -1501,85 +2368,95 @@ async function withTimeout(promise, timeoutMs) {
|
|
|
1501
2368
|
if (timer) clearTimeout(timer);
|
|
1502
2369
|
}
|
|
1503
2370
|
}
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
2371
|
+
//#endregion
|
|
2372
|
+
//#region src/agent/step_label.ts
|
|
2373
|
+
function formatAgentStepLabel(step) {
|
|
2374
|
+
if (step.type === "snapshot") return `snapshot ${step.name}`;
|
|
2375
|
+
if (step.type === "waitForText") return `wait ${step.text ?? step.regex ?? ""}`;
|
|
2376
|
+
if (step.type === "typeText") return `type ${step.text.slice(0, 24)}`;
|
|
2377
|
+
if (step.type === "pressKey") return `press ${step.key}`;
|
|
2378
|
+
if (step.type === "click") return `click ${step.selector ?? step.text ?? `${step.x},${step.y}`}`;
|
|
2379
|
+
if (step.type === "mark") return `mark ${step.label ?? ""}`;
|
|
2380
|
+
if (step.type === "sleep") return `sleep ${step.ms}ms`;
|
|
2381
|
+
return step.type;
|
|
2382
|
+
}
|
|
2383
|
+
//#endregion
|
|
2384
|
+
//#region src/agent/snapshot_diff.ts
|
|
2385
|
+
function renderSnapshotDiff(expected, received) {
|
|
2386
|
+
const expectedLines = expected.split("\n");
|
|
2387
|
+
const receivedLines = received.split("\n");
|
|
2388
|
+
const max = Math.max(expectedLines.length, receivedLines.length);
|
|
2389
|
+
const out = ["--- expected", "+++ received"];
|
|
2390
|
+
for (let i = 0; i < max; i += 1) {
|
|
2391
|
+
const before = expectedLines[i];
|
|
2392
|
+
const after = receivedLines[i];
|
|
2393
|
+
if (before === after) {
|
|
2394
|
+
if (before !== void 0) out.push(` ${before}`);
|
|
2395
|
+
continue;
|
|
1520
2396
|
}
|
|
1521
|
-
|
|
2397
|
+
if (before !== void 0) out.push(`- ${before}`);
|
|
2398
|
+
if (after !== void 0) out.push(`+ ${after}`);
|
|
1522
2399
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
2400
|
+
return out.join("\n") + "\n";
|
|
2401
|
+
}
|
|
2402
|
+
//#endregion
|
|
2403
|
+
//#region src/agent/terminal_dom.ts
|
|
2404
|
+
async function waitForTerminalRoot(page, timeoutMs) {
|
|
2405
|
+
await page.locator("[data-terminal-root]").first().waitFor({
|
|
2406
|
+
state: "attached",
|
|
2407
|
+
timeout: timeoutMs
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
async function waitForTerminalText(page, args) {
|
|
2411
|
+
const started = Date.now();
|
|
2412
|
+
const matcher = args.regex ? new RegExp(args.regex) : null;
|
|
2413
|
+
while (Date.now() - started < args.timeoutMs) {
|
|
2414
|
+
const text = await readTerminalText(page);
|
|
2415
|
+
if (args.text && text.includes(args.text)) return;
|
|
2416
|
+
if (matcher?.test(text)) return;
|
|
2417
|
+
await new Promise((resolvePoll) => setTimeout(resolvePoll, 100));
|
|
1527
2418
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
2419
|
+
throw new Error(`timed out waiting for terminal text ${args.text ?? args.regex ?? ""}`);
|
|
2420
|
+
}
|
|
2421
|
+
async function waitForStableDom(page, args) {
|
|
2422
|
+
const started = Date.now();
|
|
2423
|
+
let last = "";
|
|
2424
|
+
let stableSince = Date.now();
|
|
2425
|
+
while (Date.now() - started < args.timeoutMs) {
|
|
2426
|
+
const current = await readTerminalDomIfPresent(page);
|
|
2427
|
+
if (current === null) {
|
|
2428
|
+
await new Promise((resolvePoll) => setTimeout(resolvePoll, args.intervalMs));
|
|
2429
|
+
continue;
|
|
1538
2430
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
await
|
|
1544
|
-
timeoutMs: step.timeoutMs ?? timeout,
|
|
1545
|
-
quietMs: step.quietMs ?? 350,
|
|
1546
|
-
intervalMs: step.intervalMs ?? 100
|
|
1547
|
-
});
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
if (step.type === "snapshot") {
|
|
1551
|
-
await captureSnapshotStep(ctx, step);
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
if (step.type === "sleep") {
|
|
1555
|
-
if (!ctx.replay) await new Promise((resolveSleep) => setTimeout(resolveSleep, step.ms));
|
|
1556
|
-
return;
|
|
2431
|
+
if (current !== last) {
|
|
2432
|
+
last = current;
|
|
2433
|
+
stableSince = Date.now();
|
|
2434
|
+
} else if (Date.now() - stableSince >= args.quietMs) return;
|
|
2435
|
+
await new Promise((resolvePoll) => setTimeout(resolvePoll, args.intervalMs));
|
|
1557
2436
|
}
|
|
1558
|
-
|
|
2437
|
+
throw new Error(`timed out waiting for stable terminal DOM`);
|
|
1559
2438
|
}
|
|
1560
|
-
async function
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
stepType: frame.stepType,
|
|
1568
|
-
terminalText: normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks),
|
|
1569
|
-
dom: normalizeDomSnapshot(await readTerminalDom(ctx.page), ctx.masks),
|
|
1570
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2439
|
+
async function readTerminalText(page) {
|
|
2440
|
+
const text = await page.evaluate(() => {
|
|
2441
|
+
const node = document.querySelector("[data-terminal-root]");
|
|
2442
|
+
if (!node) return null;
|
|
2443
|
+
const rows = Array.from(node.querySelectorAll(".term-grid .term-row"));
|
|
2444
|
+
if (rows.length > 0) return rows.map((row) => row.textContent ?? "").join("\n");
|
|
2445
|
+
return node.textContent ?? "";
|
|
1571
2446
|
});
|
|
2447
|
+
if (text === null) throw new Error("terminal root is not attached");
|
|
2448
|
+
return text;
|
|
1572
2449
|
}
|
|
1573
|
-
async function
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
window.__ptywrightReplaySetPhase?.(phase);
|
|
1581
|
-
}, ctx.nextReplayPhase);
|
|
2450
|
+
async function readTerminalDom(page) {
|
|
2451
|
+
const dom = await readTerminalDomIfPresent(page);
|
|
2452
|
+
if (dom === null) throw new Error("terminal root is not attached");
|
|
2453
|
+
return dom;
|
|
2454
|
+
}
|
|
2455
|
+
async function readTerminalDomIfPresent(page) {
|
|
2456
|
+
return page.evaluate(() => document.querySelector("[data-terminal-root]")?.innerHTML ?? null);
|
|
1582
2457
|
}
|
|
2458
|
+
//#endregion
|
|
2459
|
+
//#region src/agent/snapshot_artifacts.ts
|
|
1583
2460
|
async function captureSnapshotStep(ctx, step) {
|
|
1584
2461
|
const targets = step.targets ?? [
|
|
1585
2462
|
"terminal",
|
|
@@ -1587,7 +2464,8 @@ async function captureSnapshotStep(ctx, step) {
|
|
|
1587
2464
|
...ctx.spec.defaults?.screenshot ? ["screenshot"] : []
|
|
1588
2465
|
];
|
|
1589
2466
|
const base = `${sanitizeArtifactName(ctx.viewport.name)}.${sanitizeArtifactName(step.name)}`;
|
|
1590
|
-
|
|
2467
|
+
const errors = [];
|
|
2468
|
+
for (const target of targets) try {
|
|
1591
2469
|
if (target === "terminal") {
|
|
1592
2470
|
const text = normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks);
|
|
1593
2471
|
await writeComparableArtifact(ctx, {
|
|
@@ -1626,7 +2504,10 @@ async function captureSnapshotStep(ctx, step) {
|
|
|
1626
2504
|
path: screenshotPath,
|
|
1627
2505
|
ok: true
|
|
1628
2506
|
});
|
|
2507
|
+
} catch (error) {
|
|
2508
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
1629
2509
|
}
|
|
2510
|
+
if (errors.length > 0) throw new Error(errors.join("; "));
|
|
1630
2511
|
}
|
|
1631
2512
|
async function writeComparableArtifact(ctx, artifact) {
|
|
1632
2513
|
const artifactPath = join(ctx.artifactsDir, artifact.relativePath);
|
|
@@ -1703,172 +2584,255 @@ async function writeComparableArtifact(ctx, artifact) {
|
|
|
1703
2584
|
ok: true
|
|
1704
2585
|
});
|
|
1705
2586
|
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
const
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
2587
|
+
//#endregion
|
|
2588
|
+
//#region src/agent/step_runner.ts
|
|
2589
|
+
async function runAgentStep(ctx, step) {
|
|
2590
|
+
const timeout = ctx.spec.defaults?.timeoutMs ?? 3e4;
|
|
2591
|
+
if (step.type === "waitForText") {
|
|
2592
|
+
await waitForTerminalText(ctx.page, {
|
|
2593
|
+
text: step.text,
|
|
2594
|
+
regex: step.regex,
|
|
2595
|
+
timeoutMs: step.timeoutMs ?? timeout
|
|
2596
|
+
});
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (step.type === "typeText") {
|
|
2600
|
+
await advanceReplayPhase(ctx);
|
|
2601
|
+
if (!ctx.replay) {
|
|
2602
|
+
await ctx.page.locator("[data-terminal-root]").first().click({ timeout });
|
|
2603
|
+
await ctx.page.keyboard.type(step.text, { delay: step.delayMs });
|
|
2604
|
+
if (step.enter) await ctx.page.keyboard.press("Enter");
|
|
1717
2605
|
}
|
|
1718
|
-
|
|
1719
|
-
if (after !== void 0) out.push(`+ ${after}`);
|
|
2606
|
+
return;
|
|
1720
2607
|
}
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
state: "attached",
|
|
1726
|
-
timeout: timeoutMs
|
|
1727
|
-
});
|
|
1728
|
-
}
|
|
1729
|
-
async function waitForTerminalText(page, args) {
|
|
1730
|
-
const started = Date.now();
|
|
1731
|
-
const matcher = args.regex ? new RegExp(args.regex) : null;
|
|
1732
|
-
while (Date.now() - started < args.timeoutMs) {
|
|
1733
|
-
const text = await readTerminalText(page);
|
|
1734
|
-
if (args.text && text.includes(args.text)) return;
|
|
1735
|
-
if (matcher?.test(text)) return;
|
|
1736
|
-
await new Promise((resolvePoll) => setTimeout(resolvePoll, 100));
|
|
2608
|
+
if (step.type === "pressKey") {
|
|
2609
|
+
await advanceReplayPhase(ctx);
|
|
2610
|
+
if (!ctx.replay) await ctx.page.keyboard.press(step.key);
|
|
2611
|
+
return;
|
|
1737
2612
|
}
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
while (Date.now() - started < args.timeoutMs) {
|
|
1745
|
-
const current = await readTerminalDomIfPresent(page);
|
|
1746
|
-
if (current === null) {
|
|
1747
|
-
await new Promise((resolvePoll) => setTimeout(resolvePoll, args.intervalMs));
|
|
1748
|
-
continue;
|
|
2613
|
+
if (step.type === "click") {
|
|
2614
|
+
await advanceReplayPhase(ctx);
|
|
2615
|
+
if (ctx.replay) return;
|
|
2616
|
+
if (step.selector) {
|
|
2617
|
+
await ctx.page.locator(step.selector).first().click({ timeout });
|
|
2618
|
+
return;
|
|
1749
2619
|
}
|
|
1750
|
-
if (
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
}
|
|
1754
|
-
await
|
|
2620
|
+
if (step.text) {
|
|
2621
|
+
await ctx.page.getByText(step.text, { exact: false }).first().click({ timeout });
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
await ctx.page.mouse.click(step.x, step.y);
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
if (step.type === "waitForStableDom") {
|
|
2628
|
+
await waitForStableDom(ctx.page, {
|
|
2629
|
+
timeoutMs: step.timeoutMs ?? timeout,
|
|
2630
|
+
quietMs: step.quietMs ?? 350,
|
|
2631
|
+
intervalMs: step.intervalMs ?? 100
|
|
2632
|
+
});
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
if (step.type === "snapshot") {
|
|
2636
|
+
await captureSnapshotStep(ctx, step);
|
|
2637
|
+
return;
|
|
2638
|
+
}
|
|
2639
|
+
if (step.type === "sleep") {
|
|
2640
|
+
if (!ctx.replay) await new Promise((resolveSleep) => setTimeout(resolveSleep, step.ms));
|
|
2641
|
+
return;
|
|
1755
2642
|
}
|
|
1756
|
-
|
|
2643
|
+
if (step.type === "mark") return;
|
|
1757
2644
|
}
|
|
1758
|
-
async function
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
2645
|
+
async function captureCassetteFrame(ctx, frame) {
|
|
2646
|
+
if (!ctx.cassette || !ctx.recordCassette) return;
|
|
2647
|
+
const phase = ctx.nextReplayPhase;
|
|
2648
|
+
upsertAgentCassetteFrame(ctx.cassette, {
|
|
2649
|
+
viewport: ctx.viewport,
|
|
2650
|
+
phase,
|
|
2651
|
+
stepIndex: frame.stepIndex,
|
|
2652
|
+
stepType: frame.stepType,
|
|
2653
|
+
terminalText: normalizeTerminalText(await readTerminalText(ctx.page), ctx.masks),
|
|
2654
|
+
dom: normalizeDomSnapshot(await readTerminalDom(ctx.page), ctx.masks),
|
|
2655
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1765
2656
|
});
|
|
1766
|
-
if (text === null) throw new Error("terminal root is not attached");
|
|
1767
|
-
return text;
|
|
1768
2657
|
}
|
|
1769
|
-
async function
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
|
|
2658
|
+
async function advanceReplayPhase(ctx) {
|
|
2659
|
+
if (!ctx.replay) {
|
|
2660
|
+
ctx.nextReplayPhase += 1;
|
|
2661
|
+
return;
|
|
2662
|
+
}
|
|
2663
|
+
ctx.nextReplayPhase += 1;
|
|
2664
|
+
await ctx.page.evaluate((phase) => {
|
|
2665
|
+
window.__ptywrightReplaySetPhase?.(phase);
|
|
2666
|
+
}, ctx.nextReplayPhase);
|
|
1776
2667
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2668
|
+
//#endregion
|
|
2669
|
+
//#region src/agent/viewport_runner.ts
|
|
2670
|
+
async function runAgentViewport(args) {
|
|
2671
|
+
const { browser, spec, viewport, rootDir, artifactsDir, snapshotDir, updateSnapshots, recordCassette, cassette, result } = args;
|
|
2672
|
+
const launchTarget = await resolveAgentLaunchTarget(spec.launch, { rootDir });
|
|
2673
|
+
const context = await browser.newContext({
|
|
2674
|
+
viewport: {
|
|
2675
|
+
width: viewport.width,
|
|
2676
|
+
height: viewport.height
|
|
2677
|
+
},
|
|
2678
|
+
deviceScaleFactor: viewport.deviceScaleFactor,
|
|
2679
|
+
isMobile: viewport.isMobile,
|
|
2680
|
+
hasTouch: viewport.hasTouch
|
|
2681
|
+
});
|
|
2682
|
+
const page = await context.newPage();
|
|
2683
|
+
const ctx = {
|
|
1786
2684
|
spec,
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
commands: result.commands,
|
|
1795
|
-
steps: result.steps,
|
|
2685
|
+
viewport,
|
|
2686
|
+
page,
|
|
2687
|
+
artifactsDir,
|
|
2688
|
+
snapshotDir,
|
|
2689
|
+
updateSnapshots,
|
|
2690
|
+
recordCassette,
|
|
2691
|
+
masks: resolveAgentMasks(spec),
|
|
1796
2692
|
artifacts: result.artifacts,
|
|
1797
|
-
|
|
2693
|
+
replay: Boolean(args.cassette && !args.recordCassette),
|
|
2694
|
+
cassette,
|
|
2695
|
+
nextReplayPhase: 0
|
|
1798
2696
|
};
|
|
1799
|
-
|
|
2697
|
+
try {
|
|
2698
|
+
await page.goto(resolveViewportUrl(launchTarget.url, viewport), {
|
|
2699
|
+
waitUntil: "domcontentloaded",
|
|
2700
|
+
timeout: spec.defaults?.timeoutMs ?? 3e4
|
|
2701
|
+
});
|
|
2702
|
+
await waitForTerminalRoot(page, spec.defaults?.timeoutMs ?? 3e4);
|
|
2703
|
+
await captureCassetteFrame(ctx, {
|
|
2704
|
+
stepIndex: null,
|
|
2705
|
+
stepType: "initial"
|
|
2706
|
+
});
|
|
2707
|
+
for (let i = 0; i < spec.steps.length; i += 1) {
|
|
2708
|
+
const step = spec.steps[i];
|
|
2709
|
+
const started = Date.now();
|
|
2710
|
+
try {
|
|
2711
|
+
await runAgentStep(ctx, step);
|
|
2712
|
+
result.steps.push({
|
|
2713
|
+
index: i,
|
|
2714
|
+
type: step.type,
|
|
2715
|
+
label: formatAgentStepLabel(step),
|
|
2716
|
+
durationMs: Date.now() - started,
|
|
2717
|
+
ok: true
|
|
2718
|
+
});
|
|
2719
|
+
await captureCassetteFrame(ctx, {
|
|
2720
|
+
stepIndex: i,
|
|
2721
|
+
stepType: step.type
|
|
2722
|
+
});
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2725
|
+
result.ok = false;
|
|
2726
|
+
result.errors.push(`${viewport.name} step ${i + 1} ${step.type}: ${message}`);
|
|
2727
|
+
result.steps.push({
|
|
2728
|
+
index: i,
|
|
2729
|
+
type: step.type,
|
|
2730
|
+
label: formatAgentStepLabel(step),
|
|
2731
|
+
durationMs: Date.now() - started,
|
|
2732
|
+
ok: false,
|
|
2733
|
+
error: message
|
|
2734
|
+
});
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
} finally {
|
|
2739
|
+
await withTimeout(context.close().catch(() => void 0), 5e3);
|
|
2740
|
+
await launchTarget.session?.close();
|
|
2741
|
+
}
|
|
1800
2742
|
}
|
|
1801
|
-
function
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
{
|
|
1829
|
-
path: result.recordPath,
|
|
1830
|
-
kind: "run-record",
|
|
1831
|
-
role: "record",
|
|
1832
|
-
ok: result.ok
|
|
1833
|
-
},
|
|
1834
|
-
{
|
|
1835
|
-
path: result.reportPath,
|
|
1836
|
-
kind: "report",
|
|
1837
|
-
role: "report",
|
|
1838
|
-
ok: result.ok
|
|
1839
|
-
},
|
|
1840
|
-
...result.artifacts.flatMap((artifact) => [{
|
|
1841
|
-
path: artifact.path,
|
|
1842
|
-
kind: artifact.kind,
|
|
1843
|
-
role: "artifact",
|
|
1844
|
-
ok: artifact.ok
|
|
1845
|
-
}, {
|
|
1846
|
-
path: artifact.diffPath,
|
|
1847
|
-
kind: "diff",
|
|
1848
|
-
role: "diff",
|
|
1849
|
-
ok: artifact.ok
|
|
1850
|
-
}])
|
|
1851
|
-
]
|
|
2743
|
+
function resolveViewportUrl(url, viewport) {
|
|
2744
|
+
return url.replaceAll("{viewportName}", encodeURIComponent(viewport.name)).replaceAll("{viewportWidth}", encodeURIComponent(String(viewport.width))).replaceAll("{viewportHeight}", encodeURIComponent(String(viewport.height)));
|
|
2745
|
+
}
|
|
2746
|
+
//#endregion
|
|
2747
|
+
//#region src/agent/runner.ts
|
|
2748
|
+
async function runAgentSpecPath(specPath, options = {}) {
|
|
2749
|
+
return runAgentSpec((await loadAgentSpec(specPath)).raw, options);
|
|
2750
|
+
}
|
|
2751
|
+
async function replayAgentRecordPath(recordPath, options = {}) {
|
|
2752
|
+
const raw = JSON.parse(readFileSync(recordPath, "utf8"));
|
|
2753
|
+
if (isAgentCassetteLike(raw)) return replayAgentCassette(normalizeAgentCassette(raw), recordPath, options);
|
|
2754
|
+
const record = readAgentRunRecordPath(recordPath);
|
|
2755
|
+
if (record.cassettePath) {
|
|
2756
|
+
const cassettePath = isAbsolute(record.cassettePath) ? record.cassettePath : resolve(dirname(recordPath), record.cassettePath);
|
|
2757
|
+
return replayAgentCassette(readAgentCassettePath(cassettePath, record.spec), cassettePath, {
|
|
2758
|
+
...options,
|
|
2759
|
+
artifactsDir: options.artifactsDir ?? join(dirname(recordPath), "replay")
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
if (record.spec) return runAgentSpec(record.spec, {
|
|
2763
|
+
...options,
|
|
2764
|
+
config: void 0
|
|
2765
|
+
});
|
|
2766
|
+
if (!record.flowPath) throw new Error(`invalid agent run record: missing replay source in ${recordPath}`);
|
|
2767
|
+
return runAgentSpecPath(isAbsolute(record.flowPath) ? record.flowPath : resolve(dirname(recordPath), record.flowPath), {
|
|
2768
|
+
...options,
|
|
2769
|
+
config: void 0
|
|
1852
2770
|
});
|
|
1853
2771
|
}
|
|
1854
|
-
function
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
2772
|
+
async function runAgentSpec(input, options = {}) {
|
|
2773
|
+
const { artifactsDir, cassette, cassettePath, flowPath, reportPath, result, rootDir, snapshotDir, spec, startedAt, updateSnapshots } = prepareAgentRun(input, options);
|
|
2774
|
+
writeFlowArtifact(flowPath, spec);
|
|
2775
|
+
let browser = null;
|
|
2776
|
+
try {
|
|
2777
|
+
browser = await launchAgentBrowser({ headless: options.headless ?? true });
|
|
2778
|
+
for (const viewport of spec.viewports ?? []) await runAgentViewport({
|
|
2779
|
+
browser,
|
|
2780
|
+
spec,
|
|
2781
|
+
viewport,
|
|
2782
|
+
rootDir,
|
|
2783
|
+
artifactsDir,
|
|
2784
|
+
snapshotDir,
|
|
2785
|
+
updateSnapshots,
|
|
2786
|
+
recordCassette: !options.replayCassette,
|
|
2787
|
+
cassette,
|
|
2788
|
+
result
|
|
2789
|
+
});
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
result.ok = false;
|
|
2792
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
2793
|
+
} finally {
|
|
2794
|
+
await closeBrowserSafely(browser);
|
|
2795
|
+
result.durationMs = Date.now() - startedAt;
|
|
2796
|
+
if (options.replaySourceCassettePath) writeFileSync(cassettePath, readFileSync(options.replaySourceCassettePath, "utf8"), "utf8");
|
|
2797
|
+
else writeFileSync(cassettePath, JSON.stringify(cassette, null, 2) + "\n", "utf8");
|
|
2798
|
+
result.cassetteFrameCount = cassette.frames.length;
|
|
2799
|
+
writeRunRecord(result, spec);
|
|
2800
|
+
writeAgentReport(reportPath, result);
|
|
2801
|
+
writeRunManifest(result);
|
|
2802
|
+
}
|
|
2803
|
+
return result;
|
|
2804
|
+
}
|
|
2805
|
+
async function replayAgentCassette(cassette, cassettePath, options) {
|
|
2806
|
+
const server = await startAgentCassetteServer(cassette);
|
|
2807
|
+
try {
|
|
2808
|
+
const replaySpec = structuredClone(cassette.spec);
|
|
2809
|
+
const replayCassette = structuredClone(cassette);
|
|
2810
|
+
return await runAgentSpec({
|
|
2811
|
+
...replaySpec,
|
|
2812
|
+
launch: {
|
|
2813
|
+
mode: "url",
|
|
2814
|
+
url: withReplayViewportQuery(server.url),
|
|
2815
|
+
agentFlavor: replaySpec.launch.agentFlavor
|
|
2816
|
+
}
|
|
2817
|
+
}, {
|
|
2818
|
+
...options,
|
|
2819
|
+
artifactsDir: options.artifactsDir ?? join(dirname(cassettePath), "replay"),
|
|
2820
|
+
replayCassette,
|
|
2821
|
+
replaySourceCassettePath: cassettePath
|
|
2822
|
+
});
|
|
2823
|
+
} finally {
|
|
2824
|
+
await server.close();
|
|
2825
|
+
}
|
|
1863
2826
|
}
|
|
1864
|
-
function
|
|
1865
|
-
return
|
|
2827
|
+
function withReplayViewportQuery(url) {
|
|
2828
|
+
return `${url}${url.includes("?") ? "&" : "?"}viewportName={viewportName}&viewportWidth={viewportWidth}&viewportHeight={viewportHeight}`;
|
|
1866
2829
|
}
|
|
1867
|
-
function
|
|
1868
|
-
|
|
2830
|
+
async function closeBrowserSafely(browser) {
|
|
2831
|
+
if (!browser) return;
|
|
2832
|
+
await withTimeout(browser.close().catch(() => void 0), 5e3);
|
|
1869
2833
|
}
|
|
1870
2834
|
function defaultSpecNameForPath(path) {
|
|
1871
2835
|
return sanitizeArtifactName(basename(path, extname(path)));
|
|
1872
2836
|
}
|
|
1873
2837
|
//#endregion
|
|
1874
|
-
export {
|
|
2838
|
+
export { writeAgentManifestPath as C, escapeHtml as D, escapeAttribute as E, normalizeAgentFlowSpec as O, validateAgentManifestFiles as S, readAgentCassettePath as T, formatArgv as _, formatAgentLaunchPlan as a, isAgentManifestLike as b, launchAgentBrowser as c, AGENT_RUN_RECORD_SCHEMA_URL as d, agentRunModeSchema as f, writeAgentRunRecordPath as g, readAgentRunRecordPath as h, runAgentSpecPath as i, sanitizeArtifactName as k, createAgentTemplateSpec as l, isAgentRunRecordLike as m, replayAgentRecordPath as n, resolveAgentLaunchTarget as o, formatAgentArgv as p, runAgentSpec as r, normalizeAgentFlowSpecWithConfig as s, defaultSpecNameForPath as t, loadAgentSpec as u, AGENT_MANIFEST_FILE_NAME as v, isAgentCassetteLike as w, readAgentManifestPath as x, agentManifestPath as y };
|