ptywright 0.1.1 → 0.2.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.
Files changed (67) hide show
  1. package/README.md +287 -1
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-DIUx2w6X.mjs +3587 -0
  5. package/dist/cli.mjs +2 -0
  6. package/{src/index.ts → dist/index.mjs} +7 -9
  7. package/dist/mcp.mjs +2 -0
  8. package/dist/pty-cassette.mjs +24 -0
  9. package/dist/pty_like-Cpkh_O9B.mjs +404 -0
  10. package/dist/runner-DzZlFrt1.mjs +1897 -0
  11. package/dist/runner-zApMYWZx.mjs +3257 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-VHuEWWj_.mjs +3068 -0
  14. package/dist/session.mjs +2 -0
  15. package/dist/terminal_session-DopC7Xg6.mjs +893 -0
  16. package/package.json +28 -21
  17. package/schemas/ptywright-agent-cassette.schema.json +57 -0
  18. package/schemas/ptywright-agent-check.schema.json +122 -0
  19. package/schemas/ptywright-agent-manifest.schema.json +107 -0
  20. package/schemas/ptywright-agent-promote.schema.json +146 -0
  21. package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
  22. package/schemas/ptywright-agent-run.schema.json +126 -0
  23. package/schemas/ptywright-agent.schema.json +182 -0
  24. package/schemas/ptywright-pty-cassette.schema.json +86 -0
  25. package/schemas/ptywright-script-manifest.schema.json +75 -0
  26. package/schemas/ptywright-script-run-summary.schema.json +114 -0
  27. package/schemas/ptywright-script.schema.json +55 -3
  28. package/bin/ptywright +0 -4
  29. package/src/cli.ts +0 -414
  30. package/src/generator/doc_parser.ts +0 -341
  31. package/src/generator/generate.ts +0 -161
  32. package/src/generator/index.ts +0 -10
  33. package/src/generator/script_generator.ts +0 -209
  34. package/src/generator/step_extractor.ts +0 -397
  35. package/src/mcp/http_server.ts +0 -174
  36. package/src/mcp/script_recording.ts +0 -238
  37. package/src/mcp/server.ts +0 -1348
  38. package/src/pty/bun_pty_adapter.ts +0 -34
  39. package/src/pty/bun_terminal_adapter.ts +0 -149
  40. package/src/pty/pty_adapter.ts +0 -31
  41. package/src/script/dsl.ts +0 -188
  42. package/src/script/module.ts +0 -43
  43. package/src/script/path.ts +0 -151
  44. package/src/script/run.ts +0 -108
  45. package/src/script/run_all.ts +0 -229
  46. package/src/script/runner.ts +0 -983
  47. package/src/script/schema.ts +0 -237
  48. package/src/script/steps/assert_snapshot_equals.ts +0 -21
  49. package/src/script/steps/index.ts +0 -2
  50. package/src/script/suite_report.ts +0 -626
  51. package/src/session/session_manager.ts +0 -145
  52. package/src/session/terminal_session.ts +0 -473
  53. package/src/terminal/ansi.ts +0 -142
  54. package/src/terminal/keys.ts +0 -180
  55. package/src/terminal/mask.ts +0 -70
  56. package/src/terminal/mouse.ts +0 -75
  57. package/src/terminal/snapshot.ts +0 -196
  58. package/src/terminal/style.ts +0 -121
  59. package/src/terminal/view.ts +0 -49
  60. package/src/trace/asciicast.ts +0 -20
  61. package/src/trace/asciinema_player_assets.ts +0 -44
  62. package/src/trace/cast_to_txt.ts +0 -116
  63. package/src/trace/recorder.ts +0 -110
  64. package/src/trace/report.ts +0 -2092
  65. package/src/types.ts +0 -86
  66. package/src/util/hash.ts +0 -8
  67. package/src/util/sleep.ts +0 -5
@@ -0,0 +1,3587 @@
1
+ import { c as createDefaultPtyAdapter, l as resolvePtyBackend } from "./runner-zApMYWZx.mjs";
2
+ import { a as readScriptManifestPath, c as resolveScriptManifestPath, d as resolveScriptRunSummaryPath, f as runScriptPath, i as findScriptSummaryManifest, l as validateScriptManifest, n as runAllScripts, o as relocateScriptManifestCommands, s as resolveManifestPrimaryPath$1, t as createPtywrightServer, u as readScriptRunSummaryPath } from "./server-VHuEWWj_.mjs";
3
+ import { C as isAgentManifestLike, E as writeAgentManifestPath, S as agentManifestPath, T as validateAgentManifestFiles, _ as normalizeAgentFlowSpec, a as runAgentSpecPath, b as launchAittyBrowserSession, c as agentRunModeSchema, d as readAgentRunRecordPath, f as writeAgentRunRecordPath, g as readAgentCassettePath, h as isAgentCassetteLike, l as formatAgentArgv, m as createAgentTemplateSpec, o as loadAgentSpec, p as formatArgv, r as replayAgentRecordPath, s as AGENT_RUN_RECORD_SCHEMA_URL, u as isAgentRunRecordLike, v as sanitizeArtifactName, w as readAgentManifestPath, x as AGENT_MANIFEST_FILE_NAME, y as launchAgentBrowser } from "./runner-DzZlFrt1.mjs";
4
+ import { c as createPtyCassetteReplay, i as formatPtyCassetteInspectLines, l as readPtyCassettePath, o as inspectPtyCassettePath, r as createPtyCassetteRecorder, t as wrapPtyLike, v as validatePtyCassette } from "./pty_like-Cpkh_O9B.mjs";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
8
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
9
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
10
+ import { Buffer } from "node:buffer";
11
+ //#region src/agent/check_summary.ts
12
+ const AGENT_CHECK_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-check.schema.json";
13
+ const countSummarySchema$1 = z.object({
14
+ totalCount: z.number().int().nonnegative(),
15
+ failureCount: z.number().int().nonnegative()
16
+ }).strict();
17
+ const agentCommandSchema$2 = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
18
+ const agentCheckCommandsSchema = z.object({
19
+ check: agentCommandSchema$2,
20
+ updateSnapshots: agentCommandSchema$2,
21
+ rerun: agentCommandSchema$2
22
+ }).strict();
23
+ const agentCheckJsonSummarySchema = z.object({
24
+ $schema: z.string().optional(),
25
+ version: z.literal(1),
26
+ ok: z.boolean(),
27
+ cassetteDir: z.string().min(1),
28
+ artifactsRoot: z.string().min(1),
29
+ summaryPath: z.string().min(1),
30
+ commands: agentCheckCommandsSchema,
31
+ inputs: countSummarySchema$1,
32
+ replay: z.object({
33
+ ok: z.boolean(),
34
+ totalCount: z.number().int().nonnegative(),
35
+ failureCount: z.number().int().nonnegative(),
36
+ reportPath: z.string(),
37
+ summaryPath: z.string()
38
+ }).strict(),
39
+ outputs: countSummarySchema$1,
40
+ failures: z.array(z.object({
41
+ stage: z.enum([
42
+ "input",
43
+ "replay",
44
+ "output"
45
+ ]),
46
+ filePath: z.string().min(1),
47
+ kind: z.string().optional(),
48
+ errors: z.array(z.string())
49
+ }).strict())
50
+ }).strict().superRefine((summary, ctx) => {
51
+ const failureCount = summary.inputs.failureCount + summary.replay.failureCount + summary.outputs.failureCount;
52
+ if (summary.ok !== (failureCount === 0)) ctx.addIssue({
53
+ code: z.ZodIssueCode.custom,
54
+ path: ["ok"],
55
+ message: "ok must be true only when all stages have zero failures"
56
+ });
57
+ if (summary.failures.length !== failureCount) ctx.addIssue({
58
+ code: z.ZodIssueCode.custom,
59
+ path: ["failures"],
60
+ message: "failures.length must equal the sum of stage failure counts"
61
+ });
62
+ const expected = defaultAgentCheckCommands(summary);
63
+ if (!sameArgv$3(summary.commands.check.argv, expected.check.argv)) ctx.addIssue({
64
+ code: z.ZodIssueCode.custom,
65
+ path: [
66
+ "commands",
67
+ "check",
68
+ "argv"
69
+ ],
70
+ message: "check argv must match cassetteDir and artifactsRoot"
71
+ });
72
+ if (!sameArgv$3(summary.commands.updateSnapshots.argv, expected.updateSnapshots.argv)) ctx.addIssue({
73
+ code: z.ZodIssueCode.custom,
74
+ path: [
75
+ "commands",
76
+ "updateSnapshots",
77
+ "argv"
78
+ ],
79
+ message: "updateSnapshots argv must match cassetteDir and artifactsRoot"
80
+ });
81
+ if (!sameArgv$3(summary.commands.rerun.argv, expected.rerun.argv)) ctx.addIssue({
82
+ code: z.ZodIssueCode.custom,
83
+ path: [
84
+ "commands",
85
+ "rerun",
86
+ "argv"
87
+ ],
88
+ message: "rerun argv must match summaryPath"
89
+ });
90
+ });
91
+ function normalizeAgentCheckJsonSummary(input) {
92
+ try {
93
+ const parsed = agentCheckJsonSummarySchema.parse(input);
94
+ return {
95
+ ...parsed,
96
+ $schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-check.schema.json"
97
+ };
98
+ } catch (error) {
99
+ if (error instanceof z.ZodError) throw new Error(`invalid agent check summary: ${formatZodIssues$2(error)}`);
100
+ throw error;
101
+ }
102
+ }
103
+ function readAgentCheckSummaryPath(path) {
104
+ return normalizeAgentCheckJsonSummary(JSON.parse(readFileSync(path, "utf8")));
105
+ }
106
+ function writeAgentCheckSummaryPath(path, summary) {
107
+ mkdirSync(dirname(path), { recursive: true });
108
+ writeFileSync(path, JSON.stringify(normalizeAgentCheckJsonSummary(summary), null, 2) + "\n", "utf8");
109
+ }
110
+ function defaultAgentCheckCommands(summary) {
111
+ const check = [
112
+ "ptywright",
113
+ "agent",
114
+ "check",
115
+ summary.cassetteDir,
116
+ "--artifacts-root",
117
+ summary.artifactsRoot
118
+ ];
119
+ return {
120
+ check: { argv: check },
121
+ updateSnapshots: { argv: [...check, "--update-snapshots"] },
122
+ rerun: { argv: [
123
+ "ptywright",
124
+ "agent",
125
+ "rerun",
126
+ summary.summaryPath
127
+ ] }
128
+ };
129
+ }
130
+ function sameArgv$3(left, right) {
131
+ return left.length === right.length && left.every((value, index) => value === right[index]);
132
+ }
133
+ function formatZodIssues$2(error) {
134
+ return error.issues.map((issue) => {
135
+ return `${issue.path.length ? issue.path.join(".") : "<root>"}: ${issue.message}`;
136
+ }).join("; ");
137
+ }
138
+ //#endregion
139
+ //#region src/agent/summary.ts
140
+ const AGENT_REPLAY_SUMMARY_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-replay-summary.schema.json";
141
+ const agentCommandSchema$1 = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
142
+ const agentReplaySummaryCommandsSchema = z.object({
143
+ replayAll: agentCommandSchema$1,
144
+ updateSnapshots: agentCommandSchema$1,
145
+ rerun: agentCommandSchema$1
146
+ }).strict();
147
+ const agentReplayFailedArtifactSchema = z.object({
148
+ name: z.string().min(1),
149
+ viewport: z.string().min(1),
150
+ kind: z.enum([
151
+ "terminal",
152
+ "dom",
153
+ "screenshot"
154
+ ]),
155
+ path: z.string().min(1),
156
+ baselinePath: z.string().min(1).optional(),
157
+ diffPath: z.string().min(1).optional(),
158
+ error: z.string().optional()
159
+ }).strict();
160
+ const agentReplaySummaryEntrySchema = z.object({
161
+ filePath: z.string().min(1),
162
+ durationMs: z.number().int().nonnegative(),
163
+ ok: z.boolean(),
164
+ mode: agentRunModeSchema,
165
+ frames: z.number().int().nonnegative(),
166
+ reportPath: z.string().min(1),
167
+ recordPath: z.string().min(1),
168
+ cassettePath: z.string().min(1),
169
+ failedArtifacts: z.array(agentReplayFailedArtifactSchema),
170
+ errors: z.array(z.string())
171
+ }).strict();
172
+ const agentReplaySummarySchema = z.object({
173
+ $schema: z.string().optional(),
174
+ version: z.literal(1),
175
+ ok: z.boolean(),
176
+ dir: z.string().min(1),
177
+ suiteDir: z.string().min(1),
178
+ durationMs: z.number().int().nonnegative(),
179
+ reportPath: z.string().min(1),
180
+ summaryPath: z.string().min(1),
181
+ commands: agentReplaySummaryCommandsSchema,
182
+ updateSnapshots: z.boolean(),
183
+ totalCount: z.number().int().nonnegative(),
184
+ failureCount: z.number().int().nonnegative(),
185
+ entries: z.array(agentReplaySummaryEntrySchema)
186
+ }).strict().superRefine((summary, ctx) => {
187
+ if (summary.totalCount !== summary.entries.length) ctx.addIssue({
188
+ code: z.ZodIssueCode.custom,
189
+ path: ["totalCount"],
190
+ message: "totalCount must equal entries.length"
191
+ });
192
+ const failureCount = summary.entries.filter((entry) => !entry.ok).length;
193
+ if (summary.failureCount !== failureCount) ctx.addIssue({
194
+ code: z.ZodIssueCode.custom,
195
+ path: ["failureCount"],
196
+ message: "failureCount must equal failed entries"
197
+ });
198
+ if (summary.ok !== (failureCount === 0)) ctx.addIssue({
199
+ code: z.ZodIssueCode.custom,
200
+ path: ["ok"],
201
+ message: "ok must be true only when failureCount is zero"
202
+ });
203
+ const expected = defaultAgentReplaySummaryCommands(summary);
204
+ if (!sameArgv$2(summary.commands.replayAll.argv, expected.replayAll.argv)) ctx.addIssue({
205
+ code: z.ZodIssueCode.custom,
206
+ path: [
207
+ "commands",
208
+ "replayAll",
209
+ "argv"
210
+ ],
211
+ message: "replayAll argv must match dir and suiteDir"
212
+ });
213
+ if (!sameArgv$2(summary.commands.updateSnapshots.argv, expected.updateSnapshots.argv)) ctx.addIssue({
214
+ code: z.ZodIssueCode.custom,
215
+ path: [
216
+ "commands",
217
+ "updateSnapshots",
218
+ "argv"
219
+ ],
220
+ message: "updateSnapshots argv must match dir and suiteDir"
221
+ });
222
+ if (!sameArgv$2(summary.commands.rerun.argv, expected.rerun.argv)) ctx.addIssue({
223
+ code: z.ZodIssueCode.custom,
224
+ path: [
225
+ "commands",
226
+ "rerun",
227
+ "argv"
228
+ ],
229
+ message: "rerun argv must match summaryPath"
230
+ });
231
+ });
232
+ function normalizeAgentReplaySummary(input) {
233
+ try {
234
+ const parsed = agentReplaySummarySchema.parse(input);
235
+ return {
236
+ ...parsed,
237
+ $schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-replay-summary.schema.json"
238
+ };
239
+ } catch (error) {
240
+ if (error instanceof z.ZodError) throw new Error(`invalid agent replay summary: ${formatZodIssues$1(error)}`);
241
+ throw error;
242
+ }
243
+ }
244
+ function readAgentReplaySummaryPath(path) {
245
+ return normalizeAgentReplaySummary(JSON.parse(readFileSync(path, "utf8")));
246
+ }
247
+ function writeAgentReplaySummaryPath(path, summary) {
248
+ const normalized = normalizeAgentReplaySummary({
249
+ ...summary,
250
+ $schema: summary.$schema ?? "https://ptywright.local/schemas/ptywright-agent-replay-summary.schema.json"
251
+ });
252
+ writeFileSync(path, JSON.stringify(normalized, null, 2) + "\n", "utf8");
253
+ }
254
+ function defaultAgentReplaySummaryCommands(summary) {
255
+ const replayAll = [
256
+ "ptywright",
257
+ "agent",
258
+ "replay-all",
259
+ summary.dir,
260
+ "--artifacts-root",
261
+ summary.suiteDir
262
+ ];
263
+ return {
264
+ replayAll: { argv: replayAll },
265
+ updateSnapshots: { argv: [...replayAll, "--update-snapshots"] },
266
+ rerun: { argv: [
267
+ "ptywright",
268
+ "agent",
269
+ "rerun",
270
+ summary.summaryPath
271
+ ] }
272
+ };
273
+ }
274
+ function sameArgv$2(left, right) {
275
+ return left.length === right.length && left.every((value, index) => value === right[index]);
276
+ }
277
+ function formatZodIssues$1(error) {
278
+ return error.issues.map((issue) => {
279
+ return `${issue.path.length ? issue.path.join(".") : "<root>"}: ${issue.message}`;
280
+ }).join("; ");
281
+ }
282
+ //#endregion
283
+ //#region src/agent/replay_all.ts
284
+ async function replayAllAgentRecords(options = {}) {
285
+ const dir = resolve(options.dir?.trim() ? options.dir.trim() : join(".tmp", "agent"));
286
+ const suiteDir = resolve(options.artifactsRoot?.trim() ? options.artifactsRoot.trim() : join(".tmp", "agent-replay-all"));
287
+ const filePaths = listAgentReplayFiles(dir, { artifactsRoot: suiteDir });
288
+ const entries = [];
289
+ const startedAt = Date.now();
290
+ const updateSnapshots = options.updateSnapshots ?? false;
291
+ for (const filePath of filePaths) {
292
+ const artifactsDir = join(suiteDir, "tests", safeArtifactsDirName(relative(dir, filePath)));
293
+ const entryStartedAt = Date.now();
294
+ const result = await replayRecordEntry(filePath, artifactsDir, {
295
+ headless: options.headless ?? true,
296
+ updateSnapshots
297
+ });
298
+ entries.push({
299
+ filePath,
300
+ durationMs: Date.now() - entryStartedAt,
301
+ result
302
+ });
303
+ }
304
+ const durationMs = Date.now() - startedAt;
305
+ const reportPath = join(suiteDir, "index.html");
306
+ const summaryPath = join(suiteDir, "agent-replay.summary.json");
307
+ writeReplayAllSummary(summaryPath, {
308
+ ok: entries.every((entry) => entry.result.ok),
309
+ dir,
310
+ suiteDir,
311
+ durationMs,
312
+ reportPath,
313
+ summaryPath,
314
+ updateSnapshots,
315
+ entries
316
+ });
317
+ writeReplayAllReport(reportPath, {
318
+ dir,
319
+ durationMs,
320
+ updateSnapshots,
321
+ entries,
322
+ summaryPath
323
+ });
324
+ writeReplayAllManifest({
325
+ ok: entries.every((entry) => entry.result.ok),
326
+ dir,
327
+ suiteDir,
328
+ reportPath,
329
+ summaryPath,
330
+ updateSnapshots,
331
+ entries
332
+ });
333
+ return {
334
+ ok: entries.every((entry) => entry.result.ok),
335
+ dir,
336
+ suiteDir,
337
+ durationMs,
338
+ reportPath,
339
+ summaryPath,
340
+ updateSnapshots,
341
+ entries
342
+ };
343
+ }
344
+ function writeReplayAllManifest(result) {
345
+ const summary = formatAgentReplaySummary({
346
+ ok: result.ok,
347
+ dir: result.dir,
348
+ suiteDir: result.suiteDir,
349
+ durationMs: 0,
350
+ reportPath: result.reportPath,
351
+ summaryPath: result.summaryPath,
352
+ updateSnapshots: result.updateSnapshots,
353
+ entries: result.entries
354
+ });
355
+ writeAgentManifestPath(agentManifestPath(result.suiteDir), {
356
+ kind: "replay-suite",
357
+ ok: result.ok,
358
+ rootDir: result.suiteDir,
359
+ primaryPath: result.summaryPath,
360
+ commands: summary.commands,
361
+ validation: {
362
+ ok: result.ok,
363
+ stages: [{
364
+ name: "replay",
365
+ ok: result.ok,
366
+ totalCount: result.entries.length,
367
+ failureCount: result.entries.filter((entry) => !entry.result.ok).length
368
+ }]
369
+ },
370
+ files: [
371
+ {
372
+ path: result.summaryPath,
373
+ kind: "replay-summary",
374
+ role: "summary",
375
+ ok: result.ok
376
+ },
377
+ {
378
+ path: result.reportPath,
379
+ kind: "report",
380
+ role: "report",
381
+ ok: result.ok
382
+ },
383
+ ...result.entries.flatMap((entry) => [
384
+ {
385
+ path: entry.result.recordPath,
386
+ kind: "run-record",
387
+ role: "record",
388
+ ok: entry.result.ok
389
+ },
390
+ {
391
+ path: entry.result.reportPath,
392
+ kind: "report",
393
+ role: "entry-report",
394
+ ok: entry.result.ok
395
+ },
396
+ ...entry.result.artifacts.flatMap((artifact) => [{
397
+ path: artifact.path,
398
+ kind: artifact.kind,
399
+ role: "artifact",
400
+ ok: artifact.ok
401
+ }, {
402
+ path: artifact.diffPath,
403
+ kind: "diff",
404
+ role: "diff",
405
+ ok: artifact.ok
406
+ }])
407
+ ])
408
+ ]
409
+ });
410
+ }
411
+ async function replayRecordEntry(filePath, artifactsDir, options) {
412
+ try {
413
+ return await replayAgentRecordPath(filePath, {
414
+ artifactsDir,
415
+ headless: options.headless,
416
+ updateSnapshots: options.updateSnapshots
417
+ });
418
+ } catch (error) {
419
+ const message = error instanceof Error ? error.message : String(error);
420
+ const startedAt = Date.now();
421
+ mkdirSync(artifactsDir, { recursive: true });
422
+ const replayArgv = [
423
+ "ptywright",
424
+ "agent",
425
+ "replay",
426
+ filePath
427
+ ];
428
+ const result = {
429
+ ok: false,
430
+ name: safeArtifactsDirName(filePath),
431
+ mode: "replay",
432
+ agentFlavor: "generic",
433
+ startedAt,
434
+ durationMs: 0,
435
+ artifactsDir,
436
+ snapshotDir: join(artifactsDir, "snapshots"),
437
+ reportPath: join(artifactsDir, "index.html"),
438
+ recordPath: join(artifactsDir, "failed.agent-run.json"),
439
+ flowPath: "",
440
+ cassettePath: filePath,
441
+ replayCommand: formatAgentArgv(replayArgv),
442
+ commands: {
443
+ replay: { argv: replayArgv },
444
+ updateSnapshots: { argv: [...replayArgv, "--update-snapshots"] }
445
+ },
446
+ viewports: [],
447
+ cassetteFrameCount: 0,
448
+ steps: [],
449
+ artifacts: [],
450
+ errors: [message]
451
+ };
452
+ writeAgentRunRecordPath(result.recordPath, {
453
+ $schema: AGENT_RUN_RECORD_SCHEMA_URL,
454
+ version: 1,
455
+ name: result.name,
456
+ ok: result.ok,
457
+ startedAt: new Date(result.startedAt).toISOString(),
458
+ durationMs: result.durationMs,
459
+ mode: result.mode,
460
+ artifactsDir: result.artifactsDir,
461
+ snapshotDir: result.snapshotDir,
462
+ reportPath: result.reportPath,
463
+ cassettePath: result.cassettePath,
464
+ cassetteFrameCount: result.cassetteFrameCount,
465
+ replayCommand: result.replayCommand,
466
+ commands: result.commands,
467
+ steps: result.steps,
468
+ artifacts: result.artifacts,
469
+ errors: result.errors
470
+ });
471
+ writeFileSync(result.reportPath, renderFailedEntryReport(result), "utf8");
472
+ return result;
473
+ }
474
+ }
475
+ function renderFailedEntryReport(result) {
476
+ return `<!doctype html>
477
+ <html lang="en">
478
+ <head>
479
+ <meta charset="utf-8" />
480
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
481
+ <title>${escapeHtml(result.name)} failed replay</title>
482
+ <style>
483
+ :root { color-scheme: light; font-family: ui-sans-serif, system-ui, sans-serif; }
484
+ body { margin: 0; padding: 32px; background: oklch(97.5% 0.008 210); color: oklch(19% 0.018 230); }
485
+ main { display: grid; gap: 16px; max-width: 960px; }
486
+ pre { overflow: auto; border-radius: 8px; background: oklch(20% 0.015 230); color: oklch(92% 0.012 230); padding: 14px; }
487
+ </style>
488
+ </head>
489
+ <body>
490
+ <main>
491
+ <h1>${escapeHtml(result.name)}</h1>
492
+ <p>Replay failed before the agent runner could start.</p>
493
+ <pre>${escapeHtml(result.errors.join("\n"))}</pre>
494
+ </main>
495
+ </body>
496
+ </html>`;
497
+ }
498
+ function listAgentReplayFiles(dir, options = {}) {
499
+ const resolvedDir = resolve(process.cwd(), dir);
500
+ const suiteDir = options.artifactsRoot?.trim() ? resolve(process.cwd(), options.artifactsRoot) : null;
501
+ return collectReplayFiles(resolvedDir, { skipGeneratedOutputDirs: suiteDir ? isSubpath(resolvedDir, suiteDir) : false });
502
+ }
503
+ function collectReplayFiles(dir, options = {}) {
504
+ const out = [];
505
+ const entries = readdirSync(dir);
506
+ const hasRunRecord = entries.some((entry) => entry.endsWith(".agent-run.json"));
507
+ for (const entry of entries) {
508
+ const abs = join(dir, entry);
509
+ if (statSync(abs).isDirectory()) {
510
+ if (entry === "replay") continue;
511
+ if (options.skipGeneratedOutputDirs && isGeneratedReplayOutputDir(abs)) continue;
512
+ out.push(...collectReplayFiles(abs, options));
513
+ continue;
514
+ }
515
+ if (hasRunRecord && entry.endsWith(".cassette.json")) continue;
516
+ if (entry.endsWith(".cassette.json") || entry.endsWith(".agent-run.json")) out.push(abs);
517
+ }
518
+ return out.sort((a, b) => a.localeCompare(b));
519
+ }
520
+ function isGeneratedReplayOutputDir(dir) {
521
+ const manifestPath = join(dir, AGENT_MANIFEST_FILE_NAME);
522
+ try {
523
+ if (samePath$1(readAgentManifestPath(manifestPath).rootDir, dir)) return true;
524
+ } catch {}
525
+ for (const entry of readdirSync(dir)) {
526
+ if (!entry.endsWith(".agent-run.json")) continue;
527
+ try {
528
+ const parsed = JSON.parse(readFileSync(join(dir, entry), "utf8"));
529
+ if (typeof parsed.artifactsDir === "string" && samePath$1(parsed.artifactsDir, dir)) return true;
530
+ } catch {}
531
+ }
532
+ return false;
533
+ }
534
+ function isSubpath(path, maybeParent) {
535
+ const child = resolve(process.cwd(), path);
536
+ const rel = relative(resolve(process.cwd(), maybeParent), child);
537
+ return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
538
+ }
539
+ function samePath$1(left, right) {
540
+ return resolve(process.cwd(), left) === resolve(process.cwd(), right);
541
+ }
542
+ function formatAgentReplaySummary(result) {
543
+ const entries = result.entries.map((entry) => ({
544
+ filePath: entry.filePath,
545
+ durationMs: entry.durationMs,
546
+ ok: entry.result.ok,
547
+ mode: entry.result.mode,
548
+ frames: entry.result.cassetteFrameCount,
549
+ reportPath: entry.result.reportPath,
550
+ recordPath: entry.result.recordPath,
551
+ cassettePath: entry.result.replaySourceCassettePath ?? entry.result.cassettePath,
552
+ failedArtifacts: entry.result.artifacts.filter((artifact) => !artifact.ok).map((artifact) => ({
553
+ name: artifact.name,
554
+ viewport: artifact.viewport,
555
+ kind: artifact.kind,
556
+ path: artifact.path,
557
+ baselinePath: artifact.baselinePath,
558
+ diffPath: artifact.diffPath,
559
+ error: artifact.error
560
+ })),
561
+ errors: entry.result.errors
562
+ }));
563
+ const failureCount = entries.filter((entry) => !entry.ok).length;
564
+ return normalizeAgentReplaySummary({
565
+ $schema: AGENT_REPLAY_SUMMARY_SCHEMA_URL,
566
+ version: 1,
567
+ ok: result.ok,
568
+ dir: result.dir,
569
+ suiteDir: result.suiteDir,
570
+ durationMs: result.durationMs,
571
+ reportPath: result.reportPath,
572
+ summaryPath: result.summaryPath,
573
+ commands: {
574
+ replayAll: { argv: [
575
+ "ptywright",
576
+ "agent",
577
+ "replay-all",
578
+ result.dir,
579
+ "--artifacts-root",
580
+ result.suiteDir
581
+ ] },
582
+ updateSnapshots: { argv: [
583
+ "ptywright",
584
+ "agent",
585
+ "replay-all",
586
+ result.dir,
587
+ "--artifacts-root",
588
+ result.suiteDir,
589
+ "--update-snapshots"
590
+ ] },
591
+ rerun: { argv: [
592
+ "ptywright",
593
+ "agent",
594
+ "rerun",
595
+ result.summaryPath
596
+ ] }
597
+ },
598
+ updateSnapshots: result.updateSnapshots,
599
+ totalCount: entries.length,
600
+ failureCount,
601
+ entries
602
+ });
603
+ }
604
+ function writeReplayAllSummary(path, result) {
605
+ mkdirSync(dirname(path), { recursive: true });
606
+ writeAgentReplaySummaryPath(path, formatAgentReplaySummary(result));
607
+ }
608
+ function writeReplayAllReport(path, args) {
609
+ mkdirSync(dirname(path), { recursive: true });
610
+ const rows = args.entries.map((entry) => renderEntry(entry, path)).join("\n");
611
+ const ok = args.entries.every((entry) => entry.result.ok);
612
+ writeFileSync(path, `<!doctype html>
613
+ <html lang="en">
614
+ <head>
615
+ <meta charset="utf-8" />
616
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
617
+ <title>ptywright agent replay report</title>
618
+ <style>
619
+ :root {
620
+ color-scheme: light;
621
+ --bg: oklch(97.5% 0.008 210);
622
+ --ink: oklch(19% 0.018 230);
623
+ --muted: oklch(48% 0.02 230);
624
+ --line: oklch(86% 0.018 230);
625
+ --panel: oklch(99% 0.006 210);
626
+ --good: oklch(55% 0.15 155);
627
+ --bad: oklch(58% 0.19 25);
628
+ --focus: oklch(55% 0.14 235);
629
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
630
+ }
631
+ * { box-sizing: border-box; }
632
+ body { margin: 0; background: var(--bg); color: var(--ink); }
633
+ main {
634
+ display: grid;
635
+ gap: 22px;
636
+ width: min(1180px, calc(100vw - 32px));
637
+ margin: 0 auto;
638
+ padding: 32px 0 48px;
639
+ }
640
+ header {
641
+ display: grid;
642
+ gap: 10px;
643
+ border-bottom: 1px solid var(--line);
644
+ padding-bottom: 20px;
645
+ }
646
+ h1 { margin: 0; font-size: 28px; line-height: 1.15; letter-spacing: 0; }
647
+ .meta { display: flex; flex-wrap: wrap; gap: 8px; }
648
+ .pill {
649
+ display: inline-flex;
650
+ min-height: 32px;
651
+ align-items: center;
652
+ border: 1px solid var(--line);
653
+ border-radius: 999px;
654
+ padding: 0 12px;
655
+ color: var(--muted);
656
+ font-size: 13px;
657
+ }
658
+ .pill.pass { color: var(--good); border-color: color-mix(in oklch, var(--good) 42%, var(--line)); }
659
+ .pill.fail { color: var(--bad); border-color: color-mix(in oklch, var(--bad) 42%, var(--line)); }
660
+ .entries { display: grid; gap: 10px; }
661
+ .entry {
662
+ display: grid;
663
+ grid-template-columns: 92px minmax(0, 1fr) auto;
664
+ gap: 14px;
665
+ align-items: center;
666
+ border: 1px solid var(--line);
667
+ border-radius: 8px;
668
+ background: var(--panel);
669
+ padding: 12px;
670
+ }
671
+ .badge {
672
+ justify-self: start;
673
+ border-radius: 999px;
674
+ padding: 5px 9px;
675
+ background: color-mix(in oklch, var(--line) 52%, transparent);
676
+ color: var(--muted);
677
+ font-size: 12px;
678
+ font-weight: 700;
679
+ }
680
+ .badge.pass { background: color-mix(in oklch, var(--good) 12%, var(--panel)); color: var(--good); }
681
+ .badge.fail { background: color-mix(in oklch, var(--bad) 12%, var(--panel)); color: var(--bad); }
682
+ a { color: var(--focus); font-weight: 700; text-decoration: none; }
683
+ code { color: var(--muted); overflow-wrap: anywhere; }
684
+ .commands {
685
+ display: grid;
686
+ gap: 4px;
687
+ margin-top: 8px;
688
+ }
689
+ .commands code {
690
+ display: block;
691
+ }
692
+ @media (max-width: 720px) {
693
+ main { width: min(100vw - 20px, 1180px); padding-top: 18px; }
694
+ .entry { grid-template-columns: 1fr; }
695
+ }
696
+ </style>
697
+ </head>
698
+ <body>
699
+ <main>
700
+ <header>
701
+ <h1>ptywright agent replay report</h1>
702
+ <div class="meta">
703
+ <span class="pill ${ok ? "pass" : "fail"}">${ok ? "passed" : "failed"}</span>
704
+ <span class="pill">${args.entries.length} entries</span>
705
+ <span class="pill">${args.updateSnapshots ? "update snapshots" : "compare snapshots"}</span>
706
+ <span class="pill">${args.durationMs}ms</span>
707
+ <span class="pill">${escapeHtml(args.dir)}</span>
708
+ <a class="pill" href="${escapeAttribute(relativeHref(path, args.summaryPath))}">agent-replay.summary.json</a>
709
+ </div>
710
+ </header>
711
+ <section class="entries">
712
+ ${rows || "<p>No replay artifacts were found.</p>"}
713
+ </section>
714
+ </main>
715
+ </body>
716
+ </html>`, "utf8");
717
+ }
718
+ function renderEntry(entry, reportPath) {
719
+ const state = entry.result.ok ? "pass" : "fail";
720
+ const source = entry.result.replaySourceCassettePath ?? entry.result.cassettePath;
721
+ const failedArtifacts = entry.result.artifacts.filter((artifact) => !artifact.ok);
722
+ return `<article class="entry">
723
+ <span class="badge ${state}">${state}</span>
724
+ <div>
725
+ <a href="${escapeAttribute(relativeHref(reportPath, entry.result.reportPath))}">${escapeHtml(entry.result.name)}</a>
726
+ <div><code>${escapeHtml(entry.filePath)}</code></div>
727
+ <div><code>${escapeHtml(source)}</code></div>
728
+ <div class="commands">
729
+ <code>replay ${escapeHtml(formatAgentArgv(entry.result.commands.replay.argv))}</code>
730
+ <code>update ${escapeHtml(formatAgentArgv(entry.result.commands.updateSnapshots.argv))}</code>
731
+ <code>commands ${escapeHtml(formatAgentArgv([
732
+ "ptywright",
733
+ "agent",
734
+ "commands",
735
+ entry.result.recordPath,
736
+ "--json"
737
+ ]))}</code>
738
+ </div>
739
+ ${failedArtifacts.map((artifact) => renderFailedArtifact(artifact, reportPath)).join("")}
740
+ ${entry.result.errors.map((error) => `<div><code>${escapeHtml(error)}</code></div>`).join("")}
741
+ </div>
742
+ <code>${entry.result.mode} / ${entry.result.cassetteFrameCount} frames / ${entry.durationMs}ms</code>
743
+ </article>`;
744
+ }
745
+ function renderFailedArtifact(artifact, reportPath) {
746
+ const diffLink = artifact.diffPath ? `<a href="${escapeAttribute(relativeHref(reportPath, artifact.diffPath))}">diff</a>` : "";
747
+ return `<div>
748
+ ${`<a href="${escapeAttribute(relativeHref(reportPath, artifact.path))}">${escapeHtml(artifact.kind)}</a>`}${diffLink ? ` ${diffLink}` : ""}
749
+ <code>${escapeHtml(artifact.viewport)} / ${escapeHtml(artifact.name)}${artifact.error ? ` / ${artifact.error}` : ""}</code>
750
+ </div>`;
751
+ }
752
+ function safeArtifactsDirName(relPath) {
753
+ return relPath.replace(/[/\\]/g, "__");
754
+ }
755
+ function relativeHref(fromPath, targetPath) {
756
+ const href = relative(dirname(fromPath), targetPath);
757
+ return href.startsWith(".") ? href : `./${href}`;
758
+ }
759
+ function escapeHtml(input) {
760
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
761
+ }
762
+ function escapeAttribute(input) {
763
+ return escapeHtml(input).replace(/'/g, "&#39;");
764
+ }
765
+ //#endregion
766
+ //#region src/agent/promote_summary.ts
767
+ const AGENT_PROMOTE_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-agent-promote.schema.json";
768
+ const countSummarySchema = z.object({
769
+ ok: z.boolean(),
770
+ totalCount: z.number().int().nonnegative(),
771
+ failureCount: z.number().int().nonnegative()
772
+ }).strict();
773
+ const agentCommandSchema = z.object({ argv: z.array(z.string().min(1)).min(1) }).strict();
774
+ const agentPromoteCommandsSchema = z.object({
775
+ promote: agentCommandSchema,
776
+ check: agentCommandSchema,
777
+ updateSnapshots: agentCommandSchema,
778
+ rerun: agentCommandSchema
779
+ }).strict();
780
+ const agentPromoteSummarySchema = z.object({
781
+ $schema: z.string().optional(),
782
+ version: z.literal(1),
783
+ ok: z.boolean(),
784
+ sourcePath: z.string().min(1),
785
+ cassetteDir: z.string().min(1),
786
+ targetDir: z.string().min(1),
787
+ targetCassettePath: z.string().min(1),
788
+ snapshotDir: z.string().min(1),
789
+ artifactsRoot: z.string().min(1),
790
+ summaryPath: z.string().min(1),
791
+ updateSnapshots: z.boolean(),
792
+ commands: agentPromoteCommandsSchema,
793
+ validation: countSummarySchema,
794
+ replay: z.object({
795
+ ok: z.boolean(),
796
+ totalCount: z.number().int().nonnegative(),
797
+ failureCount: z.number().int().nonnegative(),
798
+ reportPath: z.string(),
799
+ summaryPath: z.string()
800
+ }).strict(),
801
+ failures: z.array(z.object({
802
+ stage: z.enum(["validation", "replay"]),
803
+ filePath: z.string().min(1),
804
+ kind: z.string().optional(),
805
+ errors: z.array(z.string())
806
+ }).strict())
807
+ }).strict().superRefine((summary, ctx) => {
808
+ const failureCount = summary.validation.failureCount + summary.replay.failureCount;
809
+ if (summary.ok !== (summary.validation.ok && summary.replay.ok && failureCount === 0)) ctx.addIssue({
810
+ code: z.ZodIssueCode.custom,
811
+ path: ["ok"],
812
+ message: "ok must be true only when validation and replay have zero failures"
813
+ });
814
+ if (summary.failures.length !== failureCount) ctx.addIssue({
815
+ code: z.ZodIssueCode.custom,
816
+ path: ["failures"],
817
+ message: "failures.length must equal validation plus replay failure counts"
818
+ });
819
+ const expected = defaultAgentPromoteCommands(summary);
820
+ if (!sameArgv$1(summary.commands.promote.argv, expected.promote.argv)) ctx.addIssue({
821
+ code: z.ZodIssueCode.custom,
822
+ path: [
823
+ "commands",
824
+ "promote",
825
+ "argv"
826
+ ],
827
+ message: "promote argv must match sourcePath, cassetteDir, snapshotDir, and artifactsRoot"
828
+ });
829
+ if (!sameArgv$1(summary.commands.check.argv, expected.check.argv)) ctx.addIssue({
830
+ code: z.ZodIssueCode.custom,
831
+ path: [
832
+ "commands",
833
+ "check",
834
+ "argv"
835
+ ],
836
+ message: "check argv must match cassetteDir and artifactsRoot"
837
+ });
838
+ if (!sameArgv$1(summary.commands.updateSnapshots.argv, expected.updateSnapshots.argv)) ctx.addIssue({
839
+ code: z.ZodIssueCode.custom,
840
+ path: [
841
+ "commands",
842
+ "updateSnapshots",
843
+ "argv"
844
+ ],
845
+ message: "updateSnapshots argv must match cassetteDir and artifactsRoot"
846
+ });
847
+ if (!sameArgv$1(summary.commands.rerun.argv, expected.rerun.argv)) ctx.addIssue({
848
+ code: z.ZodIssueCode.custom,
849
+ path: [
850
+ "commands",
851
+ "rerun",
852
+ "argv"
853
+ ],
854
+ message: "rerun argv must match summaryPath"
855
+ });
856
+ });
857
+ function normalizeAgentPromoteSummary(input) {
858
+ try {
859
+ const parsed = agentPromoteSummarySchema.parse(input);
860
+ return {
861
+ ...parsed,
862
+ $schema: parsed.$schema ?? "https://ptywright.local/schemas/ptywright-agent-promote.schema.json"
863
+ };
864
+ } catch (error) {
865
+ if (error instanceof z.ZodError) throw new Error(`invalid agent promote summary: ${formatZodIssues(error)}`);
866
+ throw error;
867
+ }
868
+ }
869
+ function readAgentPromoteSummaryPath(path) {
870
+ return normalizeAgentPromoteSummary(JSON.parse(readFileSync(path, "utf8")));
871
+ }
872
+ function writeAgentPromoteSummaryPath(path, summary) {
873
+ mkdirSync(dirname(path), { recursive: true });
874
+ writeFileSync(path, JSON.stringify(normalizeAgentPromoteSummary(summary), null, 2) + "\n", "utf8");
875
+ }
876
+ function defaultAgentPromoteCommands(summary) {
877
+ const promote = [
878
+ "ptywright",
879
+ "agent",
880
+ "promote",
881
+ summary.sourcePath,
882
+ "--cassette-dir",
883
+ summary.cassetteDir,
884
+ "--snapshot-dir",
885
+ summary.snapshotDir,
886
+ "--artifacts-root",
887
+ summary.artifactsRoot
888
+ ];
889
+ if (summary.updateSnapshots) promote.push("--update-snapshots");
890
+ const check = [
891
+ "ptywright",
892
+ "agent",
893
+ "check",
894
+ summary.cassetteDir,
895
+ "--artifacts-root",
896
+ summary.artifactsRoot
897
+ ];
898
+ return {
899
+ promote: { argv: promote },
900
+ check: { argv: check },
901
+ updateSnapshots: { argv: [...check, "--update-snapshots"] },
902
+ rerun: { argv: [
903
+ "ptywright",
904
+ "agent",
905
+ "rerun",
906
+ summary.summaryPath
907
+ ] }
908
+ };
909
+ }
910
+ function sameArgv$1(left, right) {
911
+ return left.length === right.length && left.every((value, index) => value === right[index]);
912
+ }
913
+ function formatZodIssues(error) {
914
+ return error.issues.map((issue) => {
915
+ return `${issue.path.length ? issue.path.join(".") : "<root>"}: ${issue.message}`;
916
+ }).join("; ");
917
+ }
918
+ //#endregion
919
+ //#region src/agent/commands.ts
920
+ async function readAgentArtifactCommandsPath(path) {
921
+ const resolved = resolveAgentArtifactCommandsPath(path);
922
+ const name = basename(resolved);
923
+ if (name.endsWith(".agent-run.json")) {
924
+ const bundleCommands = readPrimaryManifestCommands(resolved, "run-record");
925
+ if (bundleCommands) return bundleCommands;
926
+ return createArtifactCommands(resolved, "run-record", readAgentRunRecordPath(resolved).commands);
927
+ }
928
+ if (name === "agent-replay.summary.json") {
929
+ const bundleCommands = readPrimaryManifestCommands(resolved, "replay-summary");
930
+ if (bundleCommands) return bundleCommands;
931
+ return createArtifactCommands(resolved, "replay-summary", readAgentReplaySummaryPath(resolved).commands);
932
+ }
933
+ if (name === "agent-promote.summary.json") {
934
+ const bundleCommands = readPrimaryManifestCommands(resolved, "promote-summary");
935
+ if (bundleCommands) return bundleCommands;
936
+ return createArtifactCommands(resolved, "promote-summary", readAgentPromoteSummaryPath(resolved).commands);
937
+ }
938
+ if (name === "agent-check.summary.json") {
939
+ const bundleCommands = readPrimaryManifestCommands(resolved, "check-summary");
940
+ if (bundleCommands) return bundleCommands;
941
+ return createArtifactCommands(resolved, "check-summary", readAgentCheckSummaryPath(resolved).commands);
942
+ }
943
+ if (name === "ptywright-agent.manifest.json") return createArtifactCommands(resolved, "manifest", relocateManifestCommands(readAgentManifestPath(resolved), resolved));
944
+ if (name.endsWith(".cassette.json")) {
945
+ readAgentCassettePath(resolved);
946
+ return createArtifactCommands(resolved, "cassette", replayCommands(path));
947
+ }
948
+ if (name.endsWith(".flow.json") || name.endsWith(".flow.ts")) {
949
+ await loadAgentSpec(resolved);
950
+ return createArtifactCommands(resolved, "flow", runCommands(path));
951
+ }
952
+ return inferJsonCommands(resolved, path);
953
+ }
954
+ function resolveAgentArtifactCommandsPath(path) {
955
+ const resolved = resolve(process.cwd(), path);
956
+ if (!statSync(resolved, { throwIfNoEntry: false })?.isDirectory()) return resolved;
957
+ const manifestPath = join(resolved, AGENT_MANIFEST_FILE_NAME);
958
+ if (existsSync(manifestPath)) return manifestPath;
959
+ throw new Error(`agent artifact directory is missing ${AGENT_MANIFEST_FILE_NAME}: ${path}. Pass a supported artifact file, or a manifest bundle directory.`);
960
+ }
961
+ function readPrimaryManifestCommands(artifactPath, kind) {
962
+ const bundle = readMovedPrimaryManifest(artifactPath, kind);
963
+ if (!bundle) return null;
964
+ return createArtifactCommands(artifactPath, kind, relocateManifestCommands(bundle.manifest, bundle.manifestPath), { manifestPath: bundle.manifestPath });
965
+ }
966
+ function findMovedPrimaryManifestBundle(artifactPath, kind) {
967
+ const bundle = readMovedPrimaryManifest(resolve(process.cwd(), artifactPath), kind);
968
+ if (!bundle) return null;
969
+ const manifestDir = dirname(bundle.manifestPath);
970
+ return {
971
+ manifestPath: bundle.manifestPath,
972
+ artifactsRoot: portableCliPath(manifestDir),
973
+ replayInputDir: findManifestReplayInputDir(bundle.manifest, manifestDir)
974
+ };
975
+ }
976
+ function readMovedPrimaryManifest(artifactPath, kind) {
977
+ const manifestPath = join(dirname(artifactPath), AGENT_MANIFEST_FILE_NAME);
978
+ if (!existsSync(manifestPath)) return null;
979
+ let manifest;
980
+ try {
981
+ manifest = readAgentManifestPath(manifestPath);
982
+ } catch {
983
+ return null;
984
+ }
985
+ const primary = manifestPrimaryFile(manifest);
986
+ if (!primary || primary.kind !== kind) return null;
987
+ if (!samePath(isAbsolute(primary.path) ? primary.path : resolve(dirname(manifestPath), primary.path), artifactPath)) return null;
988
+ if (samePath(manifest.rootDir, dirname(manifestPath))) return null;
989
+ return {
990
+ manifest,
991
+ manifestPath
992
+ };
993
+ }
994
+ function formatAgentArtifactCommandLines(result) {
995
+ return [
996
+ `kind=${result.kind}`,
997
+ `path=${result.path}`,
998
+ result.manifestPath ? `manifest=${result.manifestPath}` : null,
999
+ ...Object.entries(result.commands).map(([name, command]) => `${name}: ${formatAgentArgv(command.argv)}`)
1000
+ ].filter((line) => line !== null);
1001
+ }
1002
+ function selectAgentArtifactCommand(result, name) {
1003
+ const command = result.commands[name];
1004
+ if (!command) {
1005
+ const available = Object.keys(result.commands).sort().join(", ");
1006
+ throw new Error(`unknown agent artifact command: ${name}${available ? ` (available: ${available})` : ""}`);
1007
+ }
1008
+ return {
1009
+ path: result.path,
1010
+ kind: result.kind,
1011
+ manifestPath: result.manifestPath,
1012
+ cwd: result.cwd,
1013
+ name,
1014
+ command,
1015
+ shell: result.shell[name] ?? formatAgentArgv(command.argv)
1016
+ };
1017
+ }
1018
+ function validateAgentArtifactCommands(result) {
1019
+ for (const [name, command] of Object.entries(result.commands)) validateAgentCommandArgv(command.argv, name);
1020
+ }
1021
+ function validateAgentManifestCommandTargets(manifest, manifestPath) {
1022
+ const failures = [];
1023
+ const primaryCommands = readManifestPrimaryCommands(manifest, manifestPath, failures);
1024
+ if (primaryCommands) compareManifestCommandMaps(manifest.commands, primaryCommands, failures);
1025
+ if (manifest.kind === "run") {
1026
+ const recordPath = findManifestFileStoredPath(manifest, "run-record", "record");
1027
+ if (!recordPath) failures.push("missing manifest run-record file for replay command");
1028
+ else {
1029
+ checkPathCommand(manifest, "replay", "replay", recordPath, manifestPath, failures);
1030
+ checkPathCommand(manifest, "updateSnapshots", "replay", recordPath, manifestPath, failures);
1031
+ }
1032
+ }
1033
+ if (manifest.kind === "check") {
1034
+ const summaryPath = findManifestFileStoredPath(manifest, "check-summary", "summary");
1035
+ if (!summaryPath) failures.push("missing manifest check-summary file for rerun command");
1036
+ else checkPathCommand(manifest, "rerun", "rerun", summaryPath, manifestPath, failures);
1037
+ checkRootFlag(manifest, "check", failures);
1038
+ checkRootFlag(manifest, "updateSnapshots", failures);
1039
+ }
1040
+ if (manifest.kind === "replay-suite") {
1041
+ const summaryPath = findManifestFileStoredPath(manifest, "replay-summary", "summary");
1042
+ if (!summaryPath) failures.push("missing manifest replay-summary file for rerun command");
1043
+ else checkPathCommand(manifest, "rerun", "rerun", summaryPath, manifestPath, failures);
1044
+ checkRootFlag(manifest, "replayAll", failures);
1045
+ checkRootFlag(manifest, "updateSnapshots", failures);
1046
+ }
1047
+ if (manifest.kind === "promote") {
1048
+ const summaryPath = findManifestFileStoredPath(manifest, "promote-summary", "summary");
1049
+ if (!summaryPath) failures.push("missing manifest promote-summary file for rerun command");
1050
+ else checkPathCommand(manifest, "rerun", "rerun", summaryPath, manifestPath, failures);
1051
+ checkRootFlag(manifest, "promote", failures);
1052
+ checkRootFlag(manifest, "check", failures);
1053
+ checkRootFlag(manifest, "updateSnapshots", failures);
1054
+ }
1055
+ if (failures.length > 0) throw new Error(`invalid agent manifest commands: ${failures.join("; ")}`);
1056
+ }
1057
+ function validateAgentCommandArgv(argv, name = "<unknown>") {
1058
+ const [binary, group, subcommand] = argv;
1059
+ if (binary !== "ptywright" || group !== "agent" || !isSupportedAgentSubcommand(subcommand)) throw new Error(`command ${name} argv must start with a supported ptywright agent command`);
1060
+ }
1061
+ function sameArgv(left, right) {
1062
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1063
+ }
1064
+ function findManifestFileStoredPath(manifest, kind, role) {
1065
+ return (manifest.files.find((candidate) => candidate.kind === kind && candidate.role === role) ?? manifest.files.find((candidate) => candidate.kind === kind))?.path ?? null;
1066
+ }
1067
+ function readManifestPrimaryCommands(manifest, manifestPath, failures) {
1068
+ const primary = manifestPrimaryFile(manifest);
1069
+ if (!primary) {
1070
+ failures.push(`missing manifest primary artifact for ${manifest.kind}`);
1071
+ return null;
1072
+ }
1073
+ const baseDir = manifestPath ? dirname(resolve(process.cwd(), manifestPath)) : resolve(process.cwd(), manifest.rootDir);
1074
+ const filePath = isAbsolute(primary.path) ? primary.path : resolve(baseDir, primary.path);
1075
+ try {
1076
+ if (primary.kind === "run-record") return readAgentRunRecordPath(filePath).commands;
1077
+ if (primary.kind === "check-summary") return readAgentCheckSummaryPath(filePath).commands;
1078
+ if (primary.kind === "replay-summary") return readAgentReplaySummaryPath(filePath).commands;
1079
+ if (primary.kind === "promote-summary") return readAgentPromoteSummaryPath(filePath).commands;
1080
+ } catch (error) {
1081
+ failures.push(`unable to read manifest primary artifact ${primary.path}: ${error instanceof Error ? error.message : String(error)}`);
1082
+ }
1083
+ return null;
1084
+ }
1085
+ function manifestPrimaryFile(manifest) {
1086
+ if (manifest.kind === "run") return findManifestFile(manifest, "run-record", "record");
1087
+ if (manifest.kind === "check") return findManifestFile(manifest, "check-summary", "summary");
1088
+ if (manifest.kind === "replay-suite") return findManifestFile(manifest, "replay-summary", "summary");
1089
+ return findManifestFile(manifest, "promote-summary", "summary");
1090
+ }
1091
+ function findManifestFile(manifest, kind, role) {
1092
+ const file = manifest.files.find((candidate) => candidate.kind === kind && candidate.role === role) ?? manifest.files.find((candidate) => candidate.kind === kind);
1093
+ if (!file) return null;
1094
+ return {
1095
+ path: file.path,
1096
+ kind
1097
+ };
1098
+ }
1099
+ function compareManifestCommandMaps(actual, expected, failures) {
1100
+ const actualNames = Object.keys(actual).sort();
1101
+ const expectedNames = Object.keys(expected).sort();
1102
+ if (!sameStringList(actualNames, expectedNames)) failures.push(`manifest command names must match primary artifact commands: ${expectedNames.join(",")}`);
1103
+ for (const [name, command] of Object.entries(expected)) {
1104
+ const actualCommand = actual[name];
1105
+ if (!actualCommand) continue;
1106
+ if (!sameArgv(actualCommand.argv, command.argv)) failures.push(`command ${name} argv must match primary artifact ${formatAgentArgv(command.argv)}`);
1107
+ }
1108
+ }
1109
+ function sameStringList(left, right) {
1110
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1111
+ }
1112
+ function checkPathCommand(manifest, name, subcommand, expectedStoredPath, manifestPath, failures) {
1113
+ const command = manifest.commands[name];
1114
+ if (!command) return;
1115
+ const [binary, group, actualSubcommand, targetPath] = command.argv;
1116
+ if (binary !== "ptywright" || group !== "agent" || actualSubcommand !== subcommand) {
1117
+ failures.push(`command ${name} argv must be ptywright agent ${subcommand}`);
1118
+ return;
1119
+ }
1120
+ if (!targetPath || !sameManifestStoredPath(targetPath, manifest, expectedStoredPath, manifestPath)) failures.push(`command ${name} argv must target manifest file ${expectedStoredPath}`);
1121
+ }
1122
+ function checkRootFlag(manifest, name, failures) {
1123
+ const command = manifest.commands[name];
1124
+ if (!command) return;
1125
+ const value = getArgvFlag(command.argv, "--artifacts-root");
1126
+ if (!value || !samePath(value, manifest.rootDir)) failures.push(`command ${name} argv must target manifest rootDir`);
1127
+ }
1128
+ function manifestStoredPath(manifest, path) {
1129
+ if (isAbsolute(path)) return path;
1130
+ return resolve(process.cwd(), manifest.rootDir, path);
1131
+ }
1132
+ function sameManifestStoredPath(actual, manifest, expectedStoredPath, manifestPath) {
1133
+ if (samePath(actual, manifestStoredPath(manifest, expectedStoredPath))) return true;
1134
+ if (!manifestPath || isAbsolute(expectedStoredPath)) return false;
1135
+ return samePath(actual, resolve(dirname(resolve(process.cwd(), manifestPath)), expectedStoredPath));
1136
+ }
1137
+ function samePath(left, right) {
1138
+ return resolve(process.cwd(), left) === resolve(process.cwd(), right);
1139
+ }
1140
+ function getArgvFlag(argv, flag) {
1141
+ const index = argv.indexOf(flag);
1142
+ if (index < 0) return void 0;
1143
+ return argv[index + 1];
1144
+ }
1145
+ async function inferJsonCommands(resolved, originalPath) {
1146
+ const ext = extname(resolved);
1147
+ if (ext !== ".json" && ext !== ".ts") throw new Error(`unsupported agent artifact for commands: ${originalPath}`);
1148
+ if (ext === ".ts") {
1149
+ await loadAgentSpec(resolved);
1150
+ return createArtifactCommands(resolved, "flow", runCommands(originalPath));
1151
+ }
1152
+ const parsed = JSON.parse(readFileSync(resolved, "utf8"));
1153
+ if (isAgentCassetteLike(parsed)) {
1154
+ readAgentCassettePath(resolved);
1155
+ return createArtifactCommands(resolved, "cassette", replayCommands(originalPath));
1156
+ }
1157
+ if (isAgentRunRecordLike(parsed)) return createArtifactCommands(resolved, "run-record", readAgentRunRecordPath(resolved).commands);
1158
+ if (isReplaySummaryLike$2(parsed)) return createArtifactCommands(resolved, "replay-summary", readAgentReplaySummaryPath(resolved).commands);
1159
+ if (isPromoteSummaryLike$2(parsed)) return createArtifactCommands(resolved, "promote-summary", readAgentPromoteSummaryPath(resolved).commands);
1160
+ if (isCheckSummaryLike$2(parsed)) return createArtifactCommands(resolved, "check-summary", readAgentCheckSummaryPath(resolved).commands);
1161
+ if (isAgentManifestLike(parsed)) return createArtifactCommands(resolved, "manifest", relocateManifestCommands(readAgentManifestPath(resolved), resolved));
1162
+ if (isAgentFlowLike$1(parsed)) {
1163
+ await loadAgentSpec(resolved);
1164
+ return createArtifactCommands(resolved, "flow", runCommands(originalPath));
1165
+ }
1166
+ throw new Error(`unsupported agent artifact for commands: ${originalPath}`);
1167
+ }
1168
+ function createArtifactCommands(path, kind, commands, options = {}) {
1169
+ return {
1170
+ path,
1171
+ kind,
1172
+ manifestPath: options.manifestPath,
1173
+ cwd: process.cwd(),
1174
+ shell: Object.fromEntries(Object.entries(commands).map(([name, command]) => [name, formatAgentArgv(command.argv)])),
1175
+ commands
1176
+ };
1177
+ }
1178
+ function relocateManifestCommands(manifest, manifestPath) {
1179
+ return Object.fromEntries(Object.entries(manifest.commands).map(([name, command]) => [name, { argv: relocateManifestArgv(command.argv, manifest, manifestPath) }]));
1180
+ }
1181
+ function relocateManifestArgv(argv, manifest, manifestPath) {
1182
+ const [, , subcommand] = argv;
1183
+ if (argv[0] !== "ptywright" || argv[1] !== "agent") return [...argv];
1184
+ const manifestDir = dirname(manifestPath);
1185
+ const artifactsRootArg = portableCliPath(manifestDir);
1186
+ if (subcommand === "replay") {
1187
+ const recordPath = findManifestFilePath(manifest, manifestDir, "run-record", "record");
1188
+ if (!recordPath) return [...argv];
1189
+ return [
1190
+ argv[0],
1191
+ argv[1],
1192
+ argv[2],
1193
+ recordPath,
1194
+ ...argv.slice(4)
1195
+ ];
1196
+ }
1197
+ if (subcommand === "rerun") {
1198
+ if (manifest.kind === "replay-suite") {
1199
+ const replayDir = findManifestReplayInputDir(manifest, manifestDir);
1200
+ if (replayDir) return setArgvFlag([
1201
+ argv[0],
1202
+ argv[1],
1203
+ "replay-all",
1204
+ replayDir,
1205
+ ...argv.slice(4)
1206
+ ], "--artifacts-root", artifactsRootArg);
1207
+ }
1208
+ const summaryPath = findManifestSummaryPath(manifest, manifestDir);
1209
+ if (!summaryPath) return [...argv];
1210
+ return setArgvFlag([
1211
+ argv[0],
1212
+ argv[1],
1213
+ argv[2],
1214
+ summaryPath,
1215
+ ...argv.slice(4)
1216
+ ], "--artifacts-root", artifactsRootArg);
1217
+ }
1218
+ if (subcommand === "replay-all" && manifest.kind === "replay-suite") {
1219
+ const replayDir = findManifestReplayInputDir(manifest, manifestDir);
1220
+ return setArgvFlag([
1221
+ argv[0],
1222
+ argv[1],
1223
+ argv[2],
1224
+ replayDir ?? argv[3] ?? "",
1225
+ ...argv.slice(4)
1226
+ ], "--artifacts-root", artifactsRootArg);
1227
+ }
1228
+ if (subcommand === "check" || subcommand === "replay-all" || subcommand === "promote") return setArgvFlag([...argv], "--artifacts-root", artifactsRootArg);
1229
+ return [...argv];
1230
+ }
1231
+ function findManifestSummaryPath(manifest, manifestDir) {
1232
+ if (manifest.kind === "check") return findManifestFilePath(manifest, manifestDir, "check-summary", "summary");
1233
+ if (manifest.kind === "replay-suite") return findManifestFilePath(manifest, manifestDir, "replay-summary", "summary");
1234
+ if (manifest.kind === "promote") return findManifestFilePath(manifest, manifestDir, "promote-summary", "summary");
1235
+ return null;
1236
+ }
1237
+ function findManifestReplayInputDir(manifest, manifestDir) {
1238
+ const [replayRoot] = (manifest.files.find((file) => file.kind === "run-record" && !isAbsolute(file.path))?.path)?.split(/[/\\]+/g) ?? [];
1239
+ if (replayRoot) return portableCliPath(join(manifestDir, replayRoot));
1240
+ const recordPaths = manifest.files.filter((file) => file.kind === "run-record").map((file) => isAbsolute(file.path) ? file.path : join(manifestDir, file.path));
1241
+ if (recordPaths.length === 0) return null;
1242
+ const commonDir = commonAncestorDir(recordPaths);
1243
+ return commonDir ? portableCliPath(commonDir) : null;
1244
+ }
1245
+ function findManifestFilePath(manifest, manifestDir, kind, role) {
1246
+ const file = manifest.files.find((candidate) => candidate.kind === kind && candidate.role === role) ?? manifest.files.find((candidate) => candidate.kind === kind);
1247
+ if (!file) return null;
1248
+ return portableCliPath(isAbsolute(file.path) ? file.path : join(manifestDir, file.path));
1249
+ }
1250
+ function commonAncestorDir(paths) {
1251
+ const [first, ...rest] = paths.map((path) => resolve(process.cwd(), path));
1252
+ if (!first) return null;
1253
+ let parts = dirname(first).split(/[\\/]+/g);
1254
+ for (const path of rest) {
1255
+ const nextParts = dirname(path).split(/[\\/]+/g);
1256
+ const limit = Math.min(parts.length, nextParts.length);
1257
+ let index = 0;
1258
+ while (index < limit && parts[index] === nextParts[index]) index += 1;
1259
+ parts = parts.slice(0, index);
1260
+ }
1261
+ if (parts.length === 0) return null;
1262
+ return parts.join("/") || "/";
1263
+ }
1264
+ function setArgvFlag(argv, flag, value) {
1265
+ const index = argv.indexOf(flag);
1266
+ if (index >= 0) return [
1267
+ ...argv.slice(0, index + 1),
1268
+ value,
1269
+ ...argv.slice(index + 2)
1270
+ ];
1271
+ return [
1272
+ ...argv,
1273
+ flag,
1274
+ value
1275
+ ];
1276
+ }
1277
+ function portableCliPath(path) {
1278
+ const abs = resolve(process.cwd(), path);
1279
+ const rel = relative(process.cwd(), abs);
1280
+ if (rel && !rel.startsWith("..") && !isAbsolute(rel)) return rel;
1281
+ return abs;
1282
+ }
1283
+ function replayCommands(path) {
1284
+ const replay = [
1285
+ "ptywright",
1286
+ "agent",
1287
+ "replay",
1288
+ path
1289
+ ];
1290
+ return {
1291
+ replay: { argv: replay },
1292
+ updateSnapshots: { argv: [...replay, "--update-snapshots"] }
1293
+ };
1294
+ }
1295
+ function runCommands(path) {
1296
+ const run = [
1297
+ "ptywright",
1298
+ "agent",
1299
+ "run",
1300
+ path
1301
+ ];
1302
+ return {
1303
+ run: { argv: run },
1304
+ updateSnapshots: { argv: [...run, "--update-snapshots"] }
1305
+ };
1306
+ }
1307
+ function isSupportedAgentSubcommand(value) {
1308
+ return value === "run" || value === "record" || value === "replay" || value === "promote" || value === "replay-all" || value === "rerun" || value === "commands" || value === "inspect" || value === "exec" || value === "check" || value === "validate" || value === "init";
1309
+ }
1310
+ function isReplaySummaryLike$2(input) {
1311
+ return typeof input === "object" && input !== null && Array.isArray(input.entries) && "totalCount" in input && "failureCount" in input;
1312
+ }
1313
+ function isPromoteSummaryLike$2(input) {
1314
+ return typeof input === "object" && input !== null && "targetCassettePath" in input && "validation" in input && "replay" in input && Array.isArray(input.failures);
1315
+ }
1316
+ function isCheckSummaryLike$2(input) {
1317
+ return typeof input === "object" && input !== null && "inputs" in input && "outputs" in input && "replay" in input && Array.isArray(input.failures);
1318
+ }
1319
+ function isAgentFlowLike$1(input) {
1320
+ return typeof input === "object" && input !== null && "launch" in input && Array.isArray(input.steps);
1321
+ }
1322
+ //#endregion
1323
+ //#region src/agent/validate.ts
1324
+ async function validateAgentArtifactsPath(path, options = {}) {
1325
+ const resolved = resolve(process.cwd(), path);
1326
+ const files = statSync(resolved).isDirectory() ? listAgentArtifactFiles(resolved, Boolean(options.preferManifestBundle)) : [resolved];
1327
+ const entries = [];
1328
+ for (const filePath of files) entries.push(await validateAgentArtifactFile(filePath));
1329
+ if (entries.length === 0) entries.push({
1330
+ filePath: resolved,
1331
+ kind: "unknown",
1332
+ ok: false,
1333
+ error: "no agent artifacts found"
1334
+ });
1335
+ const failureCount = entries.filter((entry) => !entry.ok).length;
1336
+ return {
1337
+ ok: failureCount === 0,
1338
+ path: resolved,
1339
+ totalCount: entries.length,
1340
+ failureCount,
1341
+ entries
1342
+ };
1343
+ }
1344
+ async function validateAgentArtifactFile(filePath) {
1345
+ const resolved = resolve(process.cwd(), filePath);
1346
+ const kind = inferAgentArtifactKind(resolved, true);
1347
+ if (!kind) return {
1348
+ filePath: resolved,
1349
+ kind: "unknown",
1350
+ ok: false,
1351
+ error: "unsupported agent artifact"
1352
+ };
1353
+ try {
1354
+ await validateByKind(resolved, kind);
1355
+ return {
1356
+ filePath: resolved,
1357
+ kind,
1358
+ ok: true
1359
+ };
1360
+ } catch (error) {
1361
+ return {
1362
+ filePath: resolved,
1363
+ kind,
1364
+ ok: false,
1365
+ error: error instanceof Error ? error.message : String(error)
1366
+ };
1367
+ }
1368
+ }
1369
+ function listAgentArtifactFiles(dir, topLevel = false) {
1370
+ const manifestPath = join(dir, AGENT_MANIFEST_FILE_NAME);
1371
+ if (topLevel && safeIsFile(manifestPath)) return [manifestPath];
1372
+ const out = [];
1373
+ for (const entry of readdirSync(dir)) {
1374
+ if (entry === ".git" || entry === "node_modules") continue;
1375
+ const abs = join(dir, entry);
1376
+ if (statSync(abs).isDirectory()) {
1377
+ out.push(...listAgentArtifactFiles(abs));
1378
+ continue;
1379
+ }
1380
+ if (inferAgentArtifactKind(abs, false)) out.push(abs);
1381
+ }
1382
+ return out.sort((a, b) => a.localeCompare(b));
1383
+ }
1384
+ function safeIsFile(path) {
1385
+ try {
1386
+ return statSync(path).isFile();
1387
+ } catch {
1388
+ return false;
1389
+ }
1390
+ }
1391
+ async function validateByKind(path, kind) {
1392
+ if (kind === "flow") {
1393
+ if (extname(path) === ".json") {
1394
+ normalizeAgentFlowSpec(JSON.parse(readFileSync(path, "utf8")));
1395
+ return;
1396
+ }
1397
+ await loadAgentSpec(path);
1398
+ return;
1399
+ }
1400
+ if (kind === "cassette") {
1401
+ readAgentCassettePath(path);
1402
+ return;
1403
+ }
1404
+ if (kind === "run-record") {
1405
+ validateRawAgentCommandArgv(path);
1406
+ readAgentRunRecordPath(path);
1407
+ await validateResolvedAgentArtifactCommands(path);
1408
+ return;
1409
+ }
1410
+ if (kind === "replay-summary") {
1411
+ validateRawAgentCommandArgv(path);
1412
+ readAgentReplaySummaryPath(path);
1413
+ await validateResolvedAgentArtifactCommands(path);
1414
+ return;
1415
+ }
1416
+ if (kind === "promote-summary") {
1417
+ validateRawAgentCommandArgv(path);
1418
+ readAgentPromoteSummaryPath(path);
1419
+ await validateResolvedAgentArtifactCommands(path);
1420
+ return;
1421
+ }
1422
+ if (kind === "manifest") {
1423
+ const manifest = readAgentManifestPath(path);
1424
+ validateAgentArtifactCommands(await readAgentArtifactCommandsPath(path));
1425
+ validateAgentManifestCommandTargets(manifest, path);
1426
+ validateAgentManifestFiles(manifest, path);
1427
+ return;
1428
+ }
1429
+ validateRawAgentCommandArgv(path);
1430
+ readAgentCheckSummaryPath(path);
1431
+ await validateResolvedAgentArtifactCommands(path);
1432
+ }
1433
+ async function validateResolvedAgentArtifactCommands(path) {
1434
+ const commands = await readAgentArtifactCommandsPath(path);
1435
+ validateAgentArtifactCommands(commands);
1436
+ if (!commands.manifestPath) return;
1437
+ const manifest = readAgentManifestPath(commands.manifestPath);
1438
+ validateAgentManifestCommandTargets(manifest, commands.manifestPath);
1439
+ validateAgentManifestFiles(manifest, commands.manifestPath);
1440
+ }
1441
+ function validateRawAgentCommandArgv(path) {
1442
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
1443
+ if (typeof parsed !== "object" || parsed === null) return;
1444
+ const commands = parsed.commands;
1445
+ if (typeof commands !== "object" || commands === null || Array.isArray(commands)) return;
1446
+ for (const [name, command] of Object.entries(commands)) {
1447
+ const argv = typeof command === "object" && command !== null ? command.argv : void 0;
1448
+ if (!Array.isArray(argv) || !argv.every((arg) => typeof arg === "string")) continue;
1449
+ validateAgentCommandArgv(argv, name);
1450
+ }
1451
+ }
1452
+ function inferAgentArtifactKind(path, allowExplicitFlowFile) {
1453
+ const name = basename(path);
1454
+ if (name.endsWith(".cassette.json")) return "cassette";
1455
+ if (name.endsWith(".agent-run.json")) return "run-record";
1456
+ if (name === "agent-replay.summary.json") return "replay-summary";
1457
+ if (name === "agent-promote.summary.json") return "promote-summary";
1458
+ if (name === "agent-check.summary.json") return "check-summary";
1459
+ if (name === "ptywright-agent.manifest.json") return "manifest";
1460
+ if (name.endsWith(".flow.json") || name.endsWith(".flow.ts")) return "flow";
1461
+ const ext = extname(path);
1462
+ if (allowExplicitFlowFile && (ext === ".json" || ext === ".ts")) return inferExplicitArtifactKind(path);
1463
+ if (ext === ".json") return inferJsonArtifactKind(path);
1464
+ return null;
1465
+ }
1466
+ function inferExplicitArtifactKind(path) {
1467
+ if (extname(path) === ".ts") return "flow";
1468
+ return inferJsonArtifactKind(path) ?? "flow";
1469
+ }
1470
+ function inferJsonArtifactKind(path) {
1471
+ let parsed;
1472
+ try {
1473
+ parsed = JSON.parse(readFileSync(path, "utf8"));
1474
+ } catch {
1475
+ return null;
1476
+ }
1477
+ if (isAgentCassetteLike(parsed)) return "cassette";
1478
+ if (isAgentRunRecordLike(parsed)) return "run-record";
1479
+ if (isPromoteSummaryLike$1(parsed)) return "promote-summary";
1480
+ if (isCheckSummaryLike$1(parsed)) return "check-summary";
1481
+ if (isReplaySummaryLike$1(parsed)) return "replay-summary";
1482
+ if (isAgentManifestLike(parsed)) return "manifest";
1483
+ if (isAgentFlowLike(parsed)) return "flow";
1484
+ return null;
1485
+ }
1486
+ function isPromoteSummaryLike$1(input) {
1487
+ return typeof input === "object" && input !== null && "targetCassettePath" in input && "validation" in input && "replay" in input && Array.isArray(input.failures);
1488
+ }
1489
+ function isReplaySummaryLike$1(input) {
1490
+ return typeof input === "object" && input !== null && Array.isArray(input.entries) && "totalCount" in input && "failureCount" in input;
1491
+ }
1492
+ function isCheckSummaryLike$1(input) {
1493
+ return typeof input === "object" && input !== null && "inputs" in input && "outputs" in input && "replay" in input && Array.isArray(input.failures);
1494
+ }
1495
+ function isAgentFlowLike(input) {
1496
+ return typeof input === "object" && input !== null && "launch" in input && Array.isArray(input.steps);
1497
+ }
1498
+ //#endregion
1499
+ //#region src/agent/check.ts
1500
+ async function checkAgentRegression(options = {}) {
1501
+ const cassetteDir = options.cassetteDir ?? "tests/agent-cassettes";
1502
+ const artifactsRoot = options.artifactsRoot ?? ".tmp/agent-check";
1503
+ const summaryPath = join(artifactsRoot, "agent-check.summary.json");
1504
+ const validationBefore = await validateAgentArtifactsPath(cassetteDir);
1505
+ if (!validationBefore.ok) return writeSummaryAndValidateOutputs({
1506
+ ok: false,
1507
+ cassetteDir,
1508
+ artifactsRoot,
1509
+ summaryPath,
1510
+ validationBefore,
1511
+ replay: emptyReplayResult$1(cassetteDir, artifactsRoot),
1512
+ validationAfter: emptyValidationResult(artifactsRoot)
1513
+ });
1514
+ const replay = await replayAllAgentRecords({
1515
+ dir: cassetteDir,
1516
+ artifactsRoot,
1517
+ headless: options.headless ?? true,
1518
+ updateSnapshots: options.updateSnapshots ?? false
1519
+ });
1520
+ return writeSummaryAndValidateOutputs({
1521
+ ok: validationBefore.ok && replay.ok,
1522
+ cassetteDir,
1523
+ artifactsRoot,
1524
+ summaryPath,
1525
+ validationBefore,
1526
+ replay,
1527
+ validationAfter: emptyValidationResult(artifactsRoot)
1528
+ });
1529
+ }
1530
+ function emptyReplayResult$1(dir, suiteDir) {
1531
+ return {
1532
+ ok: false,
1533
+ dir,
1534
+ suiteDir,
1535
+ durationMs: 0,
1536
+ reportPath: "",
1537
+ summaryPath: "",
1538
+ updateSnapshots: false,
1539
+ entries: []
1540
+ };
1541
+ }
1542
+ async function writeSummaryAndValidateOutputs(result) {
1543
+ writeAgentCheckSummaryPath(result.summaryPath, formatAgentCheckJson(result));
1544
+ const validationAfter = await validateAgentArtifactsPath(result.artifactsRoot);
1545
+ const finalResult = {
1546
+ ...result,
1547
+ ok: result.validationBefore.ok && result.replay.ok && validationAfter.ok,
1548
+ validationAfter
1549
+ };
1550
+ writeAgentCheckSummaryPath(finalResult.summaryPath, formatAgentCheckJson(finalResult));
1551
+ writeCheckManifest(finalResult);
1552
+ return finalResult;
1553
+ }
1554
+ function writeCheckManifest(result) {
1555
+ const summary = formatAgentCheckJson(result);
1556
+ writeAgentManifestPath(agentManifestPath(result.artifactsRoot), {
1557
+ kind: "check",
1558
+ ok: result.ok,
1559
+ rootDir: result.artifactsRoot,
1560
+ primaryPath: result.summaryPath,
1561
+ commands: summary.commands,
1562
+ validation: {
1563
+ ok: result.validationBefore.ok && result.replay.ok && result.validationAfter.ok,
1564
+ stages: [
1565
+ {
1566
+ name: "inputs",
1567
+ ok: result.validationBefore.ok,
1568
+ totalCount: result.validationBefore.totalCount,
1569
+ failureCount: result.validationBefore.failureCount
1570
+ },
1571
+ {
1572
+ name: "replay",
1573
+ ok: result.replay.ok,
1574
+ totalCount: result.replay.entries.length,
1575
+ failureCount: result.replay.entries.filter((entry) => !entry.result.ok).length
1576
+ },
1577
+ {
1578
+ name: "outputs",
1579
+ ok: result.validationAfter.ok,
1580
+ totalCount: result.validationAfter.totalCount,
1581
+ failureCount: result.validationAfter.failureCount
1582
+ }
1583
+ ]
1584
+ },
1585
+ files: [
1586
+ {
1587
+ path: result.summaryPath,
1588
+ kind: "check-summary",
1589
+ role: "summary",
1590
+ ok: result.ok
1591
+ },
1592
+ {
1593
+ path: result.replay.summaryPath,
1594
+ kind: "replay-summary",
1595
+ role: "replay-summary",
1596
+ ok: result.replay.ok
1597
+ },
1598
+ {
1599
+ path: result.replay.reportPath,
1600
+ kind: "report",
1601
+ role: "replay-report",
1602
+ ok: result.replay.ok
1603
+ },
1604
+ ...result.replay.entries.flatMap((entry) => [
1605
+ {
1606
+ path: entry.result.recordPath,
1607
+ kind: "run-record",
1608
+ role: "record",
1609
+ ok: entry.result.ok
1610
+ },
1611
+ {
1612
+ path: entry.result.reportPath,
1613
+ kind: "report",
1614
+ role: "entry-report",
1615
+ ok: entry.result.ok
1616
+ },
1617
+ ...entry.result.artifacts.flatMap((artifact) => [{
1618
+ path: artifact.path,
1619
+ kind: artifact.kind,
1620
+ role: "artifact",
1621
+ ok: artifact.ok
1622
+ }, {
1623
+ path: artifact.diffPath,
1624
+ kind: "diff",
1625
+ role: "diff",
1626
+ ok: artifact.ok
1627
+ }])
1628
+ ])
1629
+ ]
1630
+ });
1631
+ }
1632
+ function emptyValidationResult(path) {
1633
+ return {
1634
+ ok: true,
1635
+ path,
1636
+ totalCount: 0,
1637
+ failureCount: 0,
1638
+ entries: []
1639
+ };
1640
+ }
1641
+ function parseArgs(argv) {
1642
+ const out = {};
1643
+ for (let i = 0; i < argv.length; i += 1) {
1644
+ const arg = argv[i];
1645
+ const next = argv[i + 1];
1646
+ if (arg === "--dir" && next) {
1647
+ out.cassetteDir = next;
1648
+ i += 1;
1649
+ continue;
1650
+ }
1651
+ if (arg === "--artifacts-root" && next) {
1652
+ out.artifactsRoot = next;
1653
+ i += 1;
1654
+ continue;
1655
+ }
1656
+ if (arg === "--update-snapshots") {
1657
+ out.updateSnapshots = true;
1658
+ continue;
1659
+ }
1660
+ if (arg === "--headed") {
1661
+ out.headless = false;
1662
+ continue;
1663
+ }
1664
+ if (arg === "--json") {
1665
+ out.json = true;
1666
+ continue;
1667
+ }
1668
+ throw new Error(`unknown arg: ${arg ?? ""}`);
1669
+ }
1670
+ return out;
1671
+ }
1672
+ function formatAgentCheckLines(result) {
1673
+ return [
1674
+ `${result.ok ? "ok" : "failed"} agent-check`,
1675
+ `inputs=${result.validationBefore.totalCount} failures=${result.validationBefore.failureCount}`,
1676
+ result.replay.summaryPath ? `summary=${result.replay.summaryPath}` : null,
1677
+ `checkSummary=${result.summaryPath}`,
1678
+ result.replay.reportPath ? `report=${result.replay.reportPath}` : null,
1679
+ `outputs=${result.validationAfter.totalCount} failures=${result.validationAfter.failureCount}`,
1680
+ ...result.validationBefore.entries.filter((entry) => !entry.ok).flatMap((entry) => [`- input ${entry.filePath}`, ` error=${entry.error ?? ""}`]),
1681
+ ...result.replay.entries.filter((entry) => !entry.result.ok).flatMap((entry) => [`- replay ${entry.filePath}`, ...entry.result.errors.map((error) => ` error=${error}`)]),
1682
+ ...result.validationAfter.entries.filter((entry) => !entry.ok).flatMap((entry) => [`- output ${entry.filePath}`, ` error=${entry.error ?? ""}`])
1683
+ ].filter(Boolean);
1684
+ }
1685
+ function formatAgentCheckJson(result) {
1686
+ const replayFailures = result.replay.entries.filter((entry) => !entry.result.ok);
1687
+ return normalizeAgentCheckJsonSummary({
1688
+ $schema: AGENT_CHECK_SCHEMA_URL,
1689
+ version: 1,
1690
+ ok: result.ok,
1691
+ cassetteDir: result.cassetteDir,
1692
+ artifactsRoot: result.artifactsRoot,
1693
+ summaryPath: result.summaryPath,
1694
+ commands: {
1695
+ check: { argv: [
1696
+ "ptywright",
1697
+ "agent",
1698
+ "check",
1699
+ result.cassetteDir,
1700
+ "--artifacts-root",
1701
+ result.artifactsRoot
1702
+ ] },
1703
+ updateSnapshots: { argv: [
1704
+ "ptywright",
1705
+ "agent",
1706
+ "check",
1707
+ result.cassetteDir,
1708
+ "--artifacts-root",
1709
+ result.artifactsRoot,
1710
+ "--update-snapshots"
1711
+ ] },
1712
+ rerun: { argv: [
1713
+ "ptywright",
1714
+ "agent",
1715
+ "rerun",
1716
+ result.summaryPath
1717
+ ] }
1718
+ },
1719
+ inputs: {
1720
+ totalCount: result.validationBefore.totalCount,
1721
+ failureCount: result.validationBefore.failureCount
1722
+ },
1723
+ replay: {
1724
+ ok: result.replay.ok,
1725
+ totalCount: result.replay.entries.length,
1726
+ failureCount: replayFailures.length,
1727
+ reportPath: result.replay.reportPath,
1728
+ summaryPath: result.replay.summaryPath
1729
+ },
1730
+ outputs: {
1731
+ totalCount: result.validationAfter.totalCount,
1732
+ failureCount: result.validationAfter.failureCount
1733
+ },
1734
+ failures: [
1735
+ ...result.validationBefore.entries.filter((entry) => !entry.ok).map((entry) => ({
1736
+ stage: "input",
1737
+ filePath: entry.filePath,
1738
+ kind: entry.kind,
1739
+ errors: entry.error ? [entry.error] : []
1740
+ })),
1741
+ ...replayFailures.map((entry) => ({
1742
+ stage: "replay",
1743
+ filePath: entry.filePath,
1744
+ errors: entry.result.errors
1745
+ })),
1746
+ ...result.validationAfter.entries.filter((entry) => !entry.ok).map((entry) => ({
1747
+ stage: "output",
1748
+ filePath: entry.filePath,
1749
+ kind: entry.kind,
1750
+ errors: entry.error ? [entry.error] : []
1751
+ }))
1752
+ ]
1753
+ });
1754
+ }
1755
+ function logCheckResult(result) {
1756
+ for (const line of formatAgentCheckLines(result)) (result.ok ? console.log : console.error)(line);
1757
+ }
1758
+ if (import.meta.main) try {
1759
+ const args = parseArgs(process.argv.slice(2));
1760
+ const result = await checkAgentRegression(args);
1761
+ if (args.json) console.log(JSON.stringify(formatAgentCheckJson(result), null, 2));
1762
+ else logCheckResult(result);
1763
+ process.exitCode = result.ok ? 0 : 1;
1764
+ } catch (error) {
1765
+ console.error(error instanceof Error ? error.message : String(error));
1766
+ process.exitCode = 1;
1767
+ }
1768
+ //#endregion
1769
+ //#region src/agent/inspect.ts
1770
+ async function inspectAgentArtifactPath(path) {
1771
+ const target = resolveInspectTarget(path);
1772
+ const targetPath = target.targetPath;
1773
+ const validation = await validateInspectTarget(path, targetPath);
1774
+ const commands = await maybeReadCommands(targetPath);
1775
+ const manifest = maybeReadManifest$1(targetPath);
1776
+ return {
1777
+ path: resolve(process.cwd(), path),
1778
+ targetPath,
1779
+ kind: commands?.kind ?? validation.entries[0]?.kind ?? "unknown",
1780
+ ok: validation.ok,
1781
+ directory: target.directory,
1782
+ validation,
1783
+ commands,
1784
+ manifest
1785
+ };
1786
+ }
1787
+ function formatAgentInspectLines(result) {
1788
+ const lines = [
1789
+ `${result.ok ? "ok" : "failed"} agent-inspect`,
1790
+ `kind=${result.kind}`,
1791
+ `path=${result.targetPath}`,
1792
+ `validation=${result.validation.ok ? "ok" : "failed"} count=${result.validation.totalCount}`
1793
+ ];
1794
+ if (result.validation.failureCount > 0) {
1795
+ lines.push(`failures=${result.validation.failureCount}`);
1796
+ lines.push(...formatValidationFailures(result.validation.entries));
1797
+ }
1798
+ if (result.directory) {
1799
+ lines.push(`directoryManifest=${result.directory.hasManifest ? "found" : "missing"} path=${result.directory.manifestPath}`);
1800
+ if (result.directory.hint) lines.push(`hint=${result.directory.hint}`);
1801
+ }
1802
+ if (result.manifest) {
1803
+ lines.push(`manifest=${result.manifest.path}`, `manifestKind=${result.manifest.kind}`, `manifestFiles=${result.manifest.files.totalCount}`, `manifestBytes=${result.manifest.files.totalBytes}`);
1804
+ for (const [kind, count] of Object.entries(result.manifest.files.byKind)) lines.push(`manifestFileKind.${kind}=${count}`);
1805
+ if (result.manifest.validation) lines.push(`manifestValidation=${result.manifest.validation.ok ? "ok" : "failed"}`, ...result.manifest.validation.stages.map((stage) => `manifestStage.${stage.name}=${stage.ok ? "ok" : "failed"} count=${stage.totalCount} failures=${stage.failureCount}`));
1806
+ if (result.manifest.files.failures.length > 0) lines.push(...result.manifest.files.failures.map((file) => `manifestFileFailure=${file.path} kind=${file.kind}${file.role ? ` role=${file.role}` : ""}`));
1807
+ }
1808
+ if (result.commands) {
1809
+ if (result.commands.manifestPath) lines.push(`commandsManifest=${result.commands.manifestPath}`);
1810
+ lines.push(`commands=${Object.keys(result.commands.commands).sort().join(",")}`, ...formatAgentArtifactCommandLines(result.commands).filter((line) => !line.startsWith("kind=") && !line.startsWith("path=")).map((line) => `command.${line}`));
1811
+ }
1812
+ return lines;
1813
+ }
1814
+ function resolveInspectTarget(path) {
1815
+ const resolved = resolve(process.cwd(), path);
1816
+ if (!statSync(resolved).isDirectory()) return { targetPath: resolved };
1817
+ const manifestPath = join(resolved, AGENT_MANIFEST_FILE_NAME);
1818
+ const hasManifest = existsSync(manifestPath);
1819
+ return {
1820
+ targetPath: hasManifest ? manifestPath : resolved,
1821
+ directory: {
1822
+ isDirectory: true,
1823
+ manifestPath,
1824
+ hasManifest,
1825
+ hint: hasManifest ? void 0 : `${AGENT_MANIFEST_FILE_NAME} is required for portable commands/exec bundle workflows; use agent validate <dir> for recursive discovery.`
1826
+ }
1827
+ };
1828
+ }
1829
+ async function validateInspectTarget(originalPath, targetPath) {
1830
+ if (targetPath !== resolve(process.cwd(), originalPath)) {
1831
+ const entry = await validateAgentArtifactFile(targetPath);
1832
+ return {
1833
+ ok: entry.ok,
1834
+ path: targetPath,
1835
+ totalCount: 1,
1836
+ failureCount: entry.ok ? 0 : 1,
1837
+ entries: [entry]
1838
+ };
1839
+ }
1840
+ return validateAgentArtifactsPath(targetPath);
1841
+ }
1842
+ async function maybeReadCommands(path) {
1843
+ try {
1844
+ return await readAgentArtifactCommandsPath(path);
1845
+ } catch {
1846
+ return;
1847
+ }
1848
+ }
1849
+ function maybeReadManifest$1(path) {
1850
+ if (basename(path) !== "ptywright-agent.manifest.json") return;
1851
+ let manifest;
1852
+ try {
1853
+ manifest = readAgentManifestPath(path);
1854
+ } catch {
1855
+ return;
1856
+ }
1857
+ const byKind = {};
1858
+ let totalBytes = 0;
1859
+ const failures = [];
1860
+ for (const file of manifest.files) {
1861
+ byKind[file.kind] = (byKind[file.kind] ?? 0) + 1;
1862
+ totalBytes += file.bytes;
1863
+ if (file.ok === false) failures.push({
1864
+ path: file.path,
1865
+ kind: file.kind,
1866
+ role: file.role
1867
+ });
1868
+ }
1869
+ return {
1870
+ path,
1871
+ kind: manifest.kind,
1872
+ ok: manifest.ok,
1873
+ rootDir: manifest.rootDir,
1874
+ primaryPath: resolveManifestPrimaryPath(manifest, path),
1875
+ generatedAt: manifest.generatedAt,
1876
+ validation: manifest.validation,
1877
+ files: {
1878
+ totalCount: manifest.files.length,
1879
+ totalBytes,
1880
+ byKind: Object.fromEntries(Object.entries(byKind).sort(([a], [b]) => a.localeCompare(b))),
1881
+ failures
1882
+ }
1883
+ };
1884
+ }
1885
+ function resolveManifestPrimaryPath(manifest, manifestPath) {
1886
+ const manifestDir = dirname(manifestPath);
1887
+ const primaryFile = manifest.files.find((file) => file.role === "summary") ?? manifest.files.find((file) => file.role === "record") ?? manifest.files.find((file) => file.kind === "run-record") ?? manifest.files.find((file) => file.kind.endsWith("-summary"));
1888
+ if (primaryFile) return isAbsolute(primaryFile.path) ? primaryFile.path : join(manifestDir, primaryFile.path);
1889
+ return isAbsolute(manifest.primaryPath) ? manifest.primaryPath : resolve(process.cwd(), manifest.primaryPath);
1890
+ }
1891
+ function formatValidationFailures(entries) {
1892
+ return entries.filter((entry) => !entry.ok).flatMap((entry) => [
1893
+ `- ${entry.filePath}`,
1894
+ ` kind=${entry.kind}`,
1895
+ entry.error ? ` error=${entry.error}` : null
1896
+ ]).filter((line) => line !== null);
1897
+ }
1898
+ //#endregion
1899
+ //#region src/agent/promote.ts
1900
+ async function promoteAgentCassette(options) {
1901
+ const sourceCassette = readAgentCassettePath(resolveSourceCassettePath(resolve(process.cwd(), options.sourcePath)));
1902
+ const name = sanitizeArtifactName(sourceCassette.name);
1903
+ const cassetteDir = options.cassetteDir ?? "tests/agent-cassettes";
1904
+ const snapshotDir = options.snapshotDir ?? join("tests", "agent-snapshots", name);
1905
+ const artifactsRoot = options.artifactsRoot ?? join(".tmp", "agent-promote", name);
1906
+ const targetDir = join(cassetteDir, name);
1907
+ const targetCassettePath = join(targetDir, `${name}.cassette.json`);
1908
+ const summaryPath = join(artifactsRoot, "agent-promote.summary.json");
1909
+ const updateSnapshots = options.updateSnapshots ?? false;
1910
+ mkdirSync(targetDir, { recursive: true });
1911
+ mkdirSync(artifactsRoot, { recursive: true });
1912
+ const promotedCassette = {
1913
+ ...sourceCassette,
1914
+ spec: {
1915
+ ...sourceCassette.spec,
1916
+ snapshotDir
1917
+ }
1918
+ };
1919
+ writeFileSync(targetCassettePath, JSON.stringify(promotedCassette, null, 2) + "\n", "utf8");
1920
+ const validation = await validateAgentArtifactsPath(targetCassettePath);
1921
+ let replay = emptyReplayResult(targetDir, artifactsRoot, updateSnapshots);
1922
+ if (validation.ok) replay = await replayAllAgentRecords({
1923
+ dir: targetDir,
1924
+ artifactsRoot,
1925
+ headless: options.headless ?? true,
1926
+ updateSnapshots
1927
+ });
1928
+ const result = {
1929
+ ok: validation.ok && replay.ok,
1930
+ sourcePath: options.sourcePath,
1931
+ cassetteDir,
1932
+ targetDir,
1933
+ targetCassettePath,
1934
+ snapshotDir,
1935
+ artifactsRoot,
1936
+ summaryPath,
1937
+ updateSnapshots,
1938
+ validation,
1939
+ replay
1940
+ };
1941
+ writeAgentPromoteSummaryPath(summaryPath, formatAgentPromoteSummary(result));
1942
+ writePromoteManifest(result);
1943
+ return result;
1944
+ }
1945
+ function formatAgentPromoteSummary(result) {
1946
+ const replayFailures = result.replay.entries.filter((entry) => !entry.result.ok);
1947
+ return normalizeAgentPromoteSummary({
1948
+ $schema: AGENT_PROMOTE_SCHEMA_URL,
1949
+ version: 1,
1950
+ ok: result.ok,
1951
+ sourcePath: result.sourcePath,
1952
+ cassetteDir: result.cassetteDir,
1953
+ targetDir: result.targetDir,
1954
+ targetCassettePath: result.targetCassettePath,
1955
+ snapshotDir: result.snapshotDir,
1956
+ artifactsRoot: result.artifactsRoot,
1957
+ summaryPath: result.summaryPath,
1958
+ updateSnapshots: result.updateSnapshots,
1959
+ commands: {
1960
+ promote: { argv: [
1961
+ "ptywright",
1962
+ "agent",
1963
+ "promote",
1964
+ result.sourcePath,
1965
+ "--cassette-dir",
1966
+ result.cassetteDir,
1967
+ "--snapshot-dir",
1968
+ result.snapshotDir,
1969
+ "--artifacts-root",
1970
+ result.artifactsRoot,
1971
+ ...result.updateSnapshots ? ["--update-snapshots"] : []
1972
+ ] },
1973
+ check: { argv: [
1974
+ "ptywright",
1975
+ "agent",
1976
+ "check",
1977
+ result.cassetteDir,
1978
+ "--artifacts-root",
1979
+ result.artifactsRoot
1980
+ ] },
1981
+ updateSnapshots: { argv: [
1982
+ "ptywright",
1983
+ "agent",
1984
+ "check",
1985
+ result.cassetteDir,
1986
+ "--artifacts-root",
1987
+ result.artifactsRoot,
1988
+ "--update-snapshots"
1989
+ ] },
1990
+ rerun: { argv: [
1991
+ "ptywright",
1992
+ "agent",
1993
+ "rerun",
1994
+ result.summaryPath
1995
+ ] }
1996
+ },
1997
+ validation: {
1998
+ ok: result.validation.ok,
1999
+ totalCount: result.validation.totalCount,
2000
+ failureCount: result.validation.failureCount
2001
+ },
2002
+ replay: {
2003
+ ok: result.replay.ok,
2004
+ totalCount: result.replay.entries.length,
2005
+ failureCount: replayFailures.length,
2006
+ reportPath: result.replay.reportPath,
2007
+ summaryPath: result.replay.summaryPath
2008
+ },
2009
+ failures: [...result.validation.entries.filter((entry) => !entry.ok).map((entry) => ({
2010
+ stage: "validation",
2011
+ filePath: entry.filePath,
2012
+ kind: entry.kind,
2013
+ errors: entry.error ? [entry.error] : []
2014
+ })), ...replayFailures.map((entry) => ({
2015
+ stage: "replay",
2016
+ filePath: entry.filePath,
2017
+ errors: entry.result.errors
2018
+ }))]
2019
+ });
2020
+ }
2021
+ function formatAgentPromoteLines(result) {
2022
+ return [
2023
+ `${result.ok ? "ok" : "failed"} agent-promote`,
2024
+ `source=${result.sourcePath}`,
2025
+ `cassette=${result.targetCassettePath}`,
2026
+ `snapshots=${result.snapshotDir}`,
2027
+ `summary=${result.summaryPath}`,
2028
+ result.replay.reportPath ? `report=${result.replay.reportPath}` : null,
2029
+ `validation=${result.validation.failureCount}/${result.validation.totalCount}`,
2030
+ `replay=${result.replay.entries.filter((entry) => !entry.result.ok).length}/${result.replay.entries.length}`,
2031
+ ...result.validation.entries.filter((entry) => !entry.ok).flatMap((entry) => [`- validation ${entry.filePath}`, ` error=${entry.error ?? ""}`]),
2032
+ ...result.replay.entries.filter((entry) => !entry.result.ok).flatMap((entry) => [`- replay ${entry.filePath}`, ...entry.result.errors.map((error) => ` error=${error}`)])
2033
+ ].filter(Boolean);
2034
+ }
2035
+ function writePromoteManifest(result) {
2036
+ const summary = formatAgentPromoteSummary(result);
2037
+ writeAgentManifestPath(agentManifestPath(result.artifactsRoot), {
2038
+ kind: "promote",
2039
+ ok: result.ok,
2040
+ rootDir: result.artifactsRoot,
2041
+ primaryPath: result.summaryPath,
2042
+ commands: summary.commands,
2043
+ validation: {
2044
+ ok: result.validation.ok && result.replay.ok,
2045
+ stages: [{
2046
+ name: "validation",
2047
+ ok: result.validation.ok,
2048
+ totalCount: result.validation.totalCount,
2049
+ failureCount: result.validation.failureCount
2050
+ }, {
2051
+ name: "replay",
2052
+ ok: result.replay.ok,
2053
+ totalCount: result.replay.entries.length,
2054
+ failureCount: result.replay.entries.filter((entry) => !entry.result.ok).length
2055
+ }]
2056
+ },
2057
+ files: [
2058
+ {
2059
+ path: result.summaryPath,
2060
+ kind: "promote-summary",
2061
+ role: "summary",
2062
+ ok: result.ok
2063
+ },
2064
+ {
2065
+ path: result.targetCassettePath,
2066
+ kind: "cassette",
2067
+ role: "promoted-cassette"
2068
+ },
2069
+ {
2070
+ path: result.replay.summaryPath,
2071
+ kind: "replay-summary",
2072
+ role: "replay-summary",
2073
+ ok: result.replay.ok
2074
+ },
2075
+ {
2076
+ path: result.replay.reportPath,
2077
+ kind: "report",
2078
+ role: "replay-report",
2079
+ ok: result.replay.ok
2080
+ },
2081
+ ...result.replay.entries.flatMap((entry) => [
2082
+ {
2083
+ path: entry.result.recordPath,
2084
+ kind: "run-record",
2085
+ role: "record",
2086
+ ok: entry.result.ok
2087
+ },
2088
+ {
2089
+ path: entry.result.reportPath,
2090
+ kind: "report",
2091
+ role: "entry-report",
2092
+ ok: entry.result.ok
2093
+ },
2094
+ ...entry.result.artifacts.flatMap((artifact) => [{
2095
+ path: artifact.path,
2096
+ kind: artifact.kind,
2097
+ role: "artifact",
2098
+ ok: artifact.ok
2099
+ }, {
2100
+ path: artifact.diffPath,
2101
+ kind: "diff",
2102
+ role: "diff",
2103
+ ok: artifact.ok
2104
+ }])
2105
+ ])
2106
+ ]
2107
+ });
2108
+ }
2109
+ function resolveSourceCassettePath(sourcePath) {
2110
+ if (sourcePath.endsWith(".cassette.json")) return sourcePath;
2111
+ if (sourcePath.endsWith(".agent-run.json")) {
2112
+ const record = readAgentRunRecordPath(sourcePath);
2113
+ if (!record.cassettePath) throw new Error(`agent run record does not reference a cassette: ${sourcePath}`);
2114
+ return resolve(dirname(sourcePath), record.cassettePath);
2115
+ }
2116
+ throw new Error(`agent promote requires .cassette.json or .agent-run.json: ${sourcePath}`);
2117
+ }
2118
+ function emptyReplayResult(dir, suiteDir, updateSnapshots) {
2119
+ return {
2120
+ ok: false,
2121
+ dir,
2122
+ suiteDir,
2123
+ durationMs: 0,
2124
+ reportPath: "",
2125
+ summaryPath: "",
2126
+ updateSnapshots,
2127
+ entries: []
2128
+ };
2129
+ }
2130
+ //#endregion
2131
+ //#region src/agent/recorder.ts
2132
+ async function recordAgentSpecPath(specPath, options) {
2133
+ return recordAgentSpec((await loadAgentSpec(specPath)).spec, options);
2134
+ }
2135
+ async function recordAgentSpec(input, options) {
2136
+ const spec = normalizeAgentFlowSpec(input);
2137
+ const rootDir = options.rootDir ? resolve(process.cwd(), options.rootDir) : process.cwd();
2138
+ const launchMode = spec.launch.mode ?? (spec.launch.url ? "url" : "aitty");
2139
+ const outPath = isAbsolute(options.outPath) ? options.outPath : resolve(process.cwd(), options.outPath);
2140
+ const durationMs = options.durationMs ?? 3e4;
2141
+ const steps = [];
2142
+ let browser = null;
2143
+ const session = launchMode === "aitty" ? await launchAittyBrowserSession(spec.launch, { rootDir }) : null;
2144
+ const url = launchMode === "url" ? spec.launch.url : session.url;
2145
+ try {
2146
+ browser = await launchAgentBrowser({ headless: options.headless ?? false });
2147
+ const viewport = spec.viewports?.[0] ?? {
2148
+ name: "desktop",
2149
+ width: 1280,
2150
+ height: 820
2151
+ };
2152
+ const context = await browser.newContext({
2153
+ viewport: {
2154
+ width: viewport.width,
2155
+ height: viewport.height
2156
+ },
2157
+ deviceScaleFactor: viewport.deviceScaleFactor,
2158
+ isMobile: viewport.isMobile,
2159
+ hasTouch: viewport.hasTouch
2160
+ });
2161
+ const page = await context.newPage();
2162
+ await installRecorderHooks(page);
2163
+ await page.goto(url, {
2164
+ waitUntil: "domcontentloaded",
2165
+ timeout: spec.defaults?.timeoutMs ?? 3e4
2166
+ });
2167
+ await page.locator("[data-terminal-root]").first().waitFor({
2168
+ state: "attached",
2169
+ timeout: spec.defaults?.timeoutMs ?? 3e4
2170
+ });
2171
+ await page.waitForTimeout(durationMs);
2172
+ steps.push(...await readRecordedSteps(page));
2173
+ await context.close();
2174
+ if (options.includeSnapshot ?? true) {
2175
+ steps.push({
2176
+ type: "waitForStableDom",
2177
+ quietMs: 600,
2178
+ intervalMs: 150,
2179
+ timeoutMs: spec.defaults?.timeoutMs ?? 3e4
2180
+ });
2181
+ steps.push({
2182
+ type: "snapshot",
2183
+ name: "recorded-final",
2184
+ targets: [
2185
+ "terminal",
2186
+ "dom",
2187
+ "screenshot"
2188
+ ]
2189
+ });
2190
+ }
2191
+ const recorded = {
2192
+ ...spec,
2193
+ steps: steps.length > 0 ? steps : spec.steps
2194
+ };
2195
+ mkdirSync(dirname(outPath), { recursive: true });
2196
+ writeFileSync(outPath, JSON.stringify(recorded, null, 2) + "\n", "utf8");
2197
+ return {
2198
+ ok: true,
2199
+ outPath,
2200
+ stepCount: recorded.steps.length,
2201
+ url
2202
+ };
2203
+ } catch (error) {
2204
+ return {
2205
+ ok: false,
2206
+ outPath,
2207
+ stepCount: steps.length,
2208
+ url,
2209
+ error: error instanceof Error ? error.message : String(error)
2210
+ };
2211
+ } finally {
2212
+ await browser?.close();
2213
+ await session?.close();
2214
+ }
2215
+ }
2216
+ async function installRecorderHooks(page) {
2217
+ await page.addInitScript(() => {
2218
+ const state = {
2219
+ steps: [],
2220
+ textBuffer: ""
2221
+ };
2222
+ const flushText = () => {
2223
+ if (!state.textBuffer) return;
2224
+ state.steps.push({
2225
+ type: "typeText",
2226
+ text: state.textBuffer
2227
+ });
2228
+ state.textBuffer = "";
2229
+ };
2230
+ window.addEventListener("keydown", (event) => {
2231
+ if (event.metaKey || event.ctrlKey || event.altKey) {
2232
+ flushText();
2233
+ state.steps.push({
2234
+ type: "pressKey",
2235
+ key: formatKey(event)
2236
+ });
2237
+ return;
2238
+ }
2239
+ if (event.key === "Enter") {
2240
+ if (state.textBuffer) {
2241
+ state.steps.push({
2242
+ type: "typeText",
2243
+ text: state.textBuffer,
2244
+ enter: true
2245
+ });
2246
+ state.textBuffer = "";
2247
+ } else state.steps.push({
2248
+ type: "pressKey",
2249
+ key: "Enter"
2250
+ });
2251
+ return;
2252
+ }
2253
+ if (event.key.length === 1) {
2254
+ state.textBuffer += event.key;
2255
+ return;
2256
+ }
2257
+ flushText();
2258
+ state.steps.push({
2259
+ type: "pressKey",
2260
+ key: formatKey(event)
2261
+ });
2262
+ }, true);
2263
+ window.addEventListener("click", (event) => {
2264
+ flushText();
2265
+ state.steps.push({
2266
+ type: "click",
2267
+ x: Math.max(0, Math.round(event.clientX)),
2268
+ y: Math.max(0, Math.round(event.clientY))
2269
+ });
2270
+ }, true);
2271
+ Object.defineProperty(window, "__ptywrightAgentRecorder", {
2272
+ value: { read() {
2273
+ flushText();
2274
+ return state.steps.slice();
2275
+ } },
2276
+ configurable: true
2277
+ });
2278
+ function formatKey(event) {
2279
+ const parts = [];
2280
+ if (event.ctrlKey) parts.push("Control");
2281
+ if (event.altKey) parts.push("Alt");
2282
+ if (event.metaKey) parts.push("Meta");
2283
+ if (event.shiftKey && event.key.length !== 1) parts.push("Shift");
2284
+ parts.push(event.key === " " ? "Space" : event.key);
2285
+ return parts.join("+");
2286
+ }
2287
+ });
2288
+ }
2289
+ async function readRecordedSteps(page) {
2290
+ return page.evaluate(() => {
2291
+ return window.__ptywrightAgentRecorder?.read() ?? [];
2292
+ });
2293
+ }
2294
+ //#endregion
2295
+ //#region src/agent/rerun.ts
2296
+ async function rerunAgentSummary(options) {
2297
+ const path = resolve(process.cwd(), options.path);
2298
+ const kind = inferSummaryKind(path);
2299
+ if (kind === "check-summary") {
2300
+ const summary = readAgentCheckSummaryPath(path);
2301
+ const movedBundle = findMovedPrimaryManifestBundle(path, "check-summary");
2302
+ return {
2303
+ kind,
2304
+ result: await checkAgentRegression({
2305
+ cassetteDir: movedBundle?.replayInputDir ?? summary.cassetteDir,
2306
+ artifactsRoot: options.artifactsRoot ?? movedBundle?.artifactsRoot ?? summary.artifactsRoot,
2307
+ headless: options.headless ?? true,
2308
+ updateSnapshots: options.updateSnapshots ?? false
2309
+ })
2310
+ };
2311
+ }
2312
+ if (kind === "promote-summary") {
2313
+ const summary = readAgentPromoteSummaryPath(path);
2314
+ const movedBundle = findMovedPrimaryManifestBundle(path, "promote-summary");
2315
+ return {
2316
+ kind,
2317
+ result: await promoteAgentCassette({
2318
+ sourcePath: summary.sourcePath,
2319
+ cassetteDir: summary.cassetteDir,
2320
+ snapshotDir: summary.snapshotDir,
2321
+ artifactsRoot: options.artifactsRoot ?? movedBundle?.artifactsRoot ?? summary.artifactsRoot,
2322
+ headless: options.headless ?? true,
2323
+ updateSnapshots: options.updateSnapshots ?? summary.updateSnapshots
2324
+ })
2325
+ };
2326
+ }
2327
+ if (kind === "replay-summary") {
2328
+ const summary = readAgentReplaySummaryPath(path);
2329
+ const movedReplayBundle = findMovedPrimaryManifestBundle(path, "replay-summary");
2330
+ return {
2331
+ kind,
2332
+ result: await replayAllAgentRecords({
2333
+ dir: movedReplayBundle?.replayInputDir ?? summary.dir,
2334
+ artifactsRoot: options.artifactsRoot ?? movedReplayBundle?.artifactsRoot ?? summary.suiteDir,
2335
+ headless: options.headless ?? true,
2336
+ updateSnapshots: options.updateSnapshots ?? false
2337
+ })
2338
+ };
2339
+ }
2340
+ throw new Error(`unsupported agent summary: ${options.path}`);
2341
+ }
2342
+ function inferSummaryKind(path) {
2343
+ const name = basename(path);
2344
+ if (name === "agent-check.summary.json") return "check-summary";
2345
+ if (name === "agent-promote.summary.json") return "promote-summary";
2346
+ if (name === "agent-replay.summary.json") return "replay-summary";
2347
+ let parsed;
2348
+ try {
2349
+ parsed = JSON.parse(readFileSync(path, "utf8"));
2350
+ } catch {
2351
+ return null;
2352
+ }
2353
+ if (isCheckSummaryLike(parsed)) return "check-summary";
2354
+ if (isPromoteSummaryLike(parsed)) return "promote-summary";
2355
+ if (isReplaySummaryLike(parsed)) return "replay-summary";
2356
+ return null;
2357
+ }
2358
+ function isPromoteSummaryLike(input) {
2359
+ return typeof input === "object" && input !== null && "targetCassettePath" in input && "validation" in input && "replay" in input && Array.isArray(input.failures);
2360
+ }
2361
+ function isCheckSummaryLike(input) {
2362
+ return typeof input === "object" && input !== null && "inputs" in input && "outputs" in input && "replay" in input && Array.isArray(input.failures);
2363
+ }
2364
+ function isReplaySummaryLike(input) {
2365
+ return typeof input === "object" && input !== null && Array.isArray(input.entries) && "totalCount" in input && "failureCount" in input;
2366
+ }
2367
+ //#endregion
2368
+ //#region src/mcp/http_server.ts
2369
+ function parseAllowedOrigins(value) {
2370
+ if (!value?.trim()) return void 0;
2371
+ return value.split(/[\s,]+/g).map((v) => v.trim()).filter(Boolean);
2372
+ }
2373
+ function isOriginAllowed(origin, allowed) {
2374
+ if (allowed.includes("*")) return true;
2375
+ return allowed.includes(origin);
2376
+ }
2377
+ function withCorsHeaders(init, origin, allowed) {
2378
+ if (!origin) return init;
2379
+ if (!isOriginAllowed(origin, allowed)) return init;
2380
+ const headers = new Headers(init.headers);
2381
+ headers.set("access-control-allow-origin", origin);
2382
+ headers.set("vary", "origin");
2383
+ headers.set("access-control-allow-methods", "GET,POST,DELETE,OPTIONS");
2384
+ headers.set("access-control-allow-headers", "content-type,mcp-session-id,last-event-id,mcp-protocol-version");
2385
+ headers.set("access-control-expose-headers", "mcp-session-id,mcp-protocol-version");
2386
+ return {
2387
+ ...init,
2388
+ headers
2389
+ };
2390
+ }
2391
+ async function startPtywrightHttpServer(options) {
2392
+ const hostname = options?.hostname?.trim() ? options.hostname.trim() : "127.0.0.1";
2393
+ const desiredPort = options?.port ?? 3e3;
2394
+ const cors = options?.cors ?? true;
2395
+ const { server, sessions } = createPtywrightServer({ capabilities: options?.capabilities });
2396
+ const transport = new WebStandardStreamableHTTPServerTransport();
2397
+ await server.connect(transport);
2398
+ let allowedOrigins = options?.allowedOrigins ?? parseAllowedOrigins(process.env.PTYWRIGHT_HTTP_ALLOWED_ORIGINS) ?? [];
2399
+ const srv = Bun.serve({
2400
+ hostname,
2401
+ port: desiredPort,
2402
+ fetch: async (req) => {
2403
+ const url = new URL(req.url);
2404
+ const origin = req.headers.get("origin");
2405
+ if (url.pathname === "/health") {
2406
+ const init = withCorsHeaders({
2407
+ status: 200,
2408
+ headers: { "content-type": "application/json" }
2409
+ }, cors ? origin : null, allowedOrigins);
2410
+ return new Response(JSON.stringify({ status: "ok" }), init);
2411
+ }
2412
+ if (url.pathname !== "/mcp") {
2413
+ const init = withCorsHeaders({
2414
+ status: 404,
2415
+ headers: { "content-type": "text/plain; charset=utf-8" }
2416
+ }, cors ? origin : null, allowedOrigins);
2417
+ return new Response("not found", init);
2418
+ }
2419
+ if (origin && !isOriginAllowed(origin, allowedOrigins)) return new Response("forbidden", {
2420
+ status: 403,
2421
+ headers: cors ? {
2422
+ "content-type": "text/plain; charset=utf-8",
2423
+ "access-control-allow-origin": origin,
2424
+ vary: "origin"
2425
+ } : { "content-type": "text/plain; charset=utf-8" }
2426
+ });
2427
+ if (req.method === "OPTIONS") return new Response(null, withCorsHeaders({ status: 204 }, origin, allowedOrigins));
2428
+ const res = await transport.handleRequest(req);
2429
+ if (!cors) return res;
2430
+ const init = withCorsHeaders({
2431
+ status: res.status,
2432
+ statusText: res.statusText,
2433
+ headers: res.headers
2434
+ }, origin, allowedOrigins);
2435
+ return new Response(res.body, init);
2436
+ }
2437
+ });
2438
+ const port = srv.port;
2439
+ if (port === void 0) {
2440
+ await srv.stop();
2441
+ sessions.closeAll();
2442
+ await server.close();
2443
+ throw new Error("failed to bind HTTP server port");
2444
+ }
2445
+ if (allowedOrigins.length === 0) allowedOrigins = [`http://localhost:${port}`, `http://127.0.0.1:${port}`];
2446
+ return {
2447
+ url: `http://${hostname}:${port}/mcp`,
2448
+ hostname,
2449
+ port,
2450
+ close: async () => {
2451
+ await srv.stop();
2452
+ sessions.closeAll();
2453
+ await server.close();
2454
+ }
2455
+ };
2456
+ }
2457
+ //#endregion
2458
+ //#region src/pty-cassette/cli.ts
2459
+ function ptyUsage() {
2460
+ return [
2461
+ "ptywright pty <command>",
2462
+ "",
2463
+ "Commands:",
2464
+ " pty record --out <file> -- <command> [args...] Record a raw PTY cassette",
2465
+ " pty replay <file> Replay recorded PTY output",
2466
+ " pty inspect <file> Print cassette summary",
2467
+ " pty validate <file> Validate cassette schema",
2468
+ "",
2469
+ "Record options:",
2470
+ " --out <file> Output cassette JSON path",
2471
+ " --cols <n> Terminal columns (default: stdout cols or 80)",
2472
+ " --rows <n> Terminal rows (default: stdout rows or 24)",
2473
+ " --term <name> TERM/name value (default: xterm-256color)",
2474
+ " --cwd <dir> Child working directory (default: cwd)",
2475
+ " --backend <name> auto|bun-terminal|bun-pty",
2476
+ " --env KEY=VALUE Add/override child env (repeatable)",
2477
+ "",
2478
+ "Replay/inspect/validate options:",
2479
+ " --speed <n> Replay timing multiplier; 0 means instant (default: 0)",
2480
+ " --json Print machine-readable output"
2481
+ ].join("\n");
2482
+ }
2483
+ async function cmdPty(argv) {
2484
+ const [mode, ...rest] = argv;
2485
+ if (isHelp$1(mode)) {
2486
+ console.log(ptyUsage());
2487
+ return 0;
2488
+ }
2489
+ if (mode === "record") {
2490
+ const result = await recordPtyCassetteCommand(parseRecordArgs(rest));
2491
+ console.log(`record=${result.path}`);
2492
+ console.log(`events=${result.eventCount}`);
2493
+ return result.exitCode;
2494
+ }
2495
+ if (mode === "replay") {
2496
+ const args = parseReplayArgs(rest);
2497
+ const replay = createPtyCassetteReplay(args.path, { speed: args.speed });
2498
+ replay.onData((data) => {
2499
+ process.stdout.write(data);
2500
+ });
2501
+ await replay.start();
2502
+ return 0;
2503
+ }
2504
+ if (mode === "inspect") {
2505
+ const args = parseArtifactArgs(rest);
2506
+ const result = inspectPtyCassettePath(args.path);
2507
+ if (args.json) console.log(JSON.stringify(result, null, 2));
2508
+ else for (const line of formatPtyCassetteInspectLines(result)) console.log(line);
2509
+ return 0;
2510
+ }
2511
+ if (mode === "validate") {
2512
+ const args = parseArtifactArgs(rest);
2513
+ const result = validatePtyCassette(JSON.parse(await Bun.file(args.path).text()));
2514
+ if (args.json) console.log(JSON.stringify(result.ok ? {
2515
+ ok: true,
2516
+ path: args.path
2517
+ } : result, null, 2));
2518
+ else if (result.ok) console.log(`ok pty-cassette path=${args.path}`);
2519
+ else for (const error of result.errors) console.error(error);
2520
+ return result.ok ? 0 : 1;
2521
+ }
2522
+ throw new Error("missing pty subcommand: record|replay|inspect|validate\n\n" + ptyUsage());
2523
+ }
2524
+ async function recordPtyCassetteCommand(args) {
2525
+ const env = mergeEnv({
2526
+ TERM: args.term,
2527
+ COLORTERM: "truecolor"
2528
+ }, args.env);
2529
+ const pty = createDefaultPtyAdapter(args.backend).spawn(args.command, args.args, {
2530
+ cols: args.cols,
2531
+ rows: args.rows,
2532
+ cwd: args.cwd,
2533
+ env,
2534
+ name: args.term
2535
+ });
2536
+ const recorder = createPtyCassetteRecorder({
2537
+ terminal: {
2538
+ cols: args.cols,
2539
+ rows: args.rows,
2540
+ term: args.term
2541
+ },
2542
+ command: {
2543
+ file: args.command,
2544
+ args: args.args,
2545
+ cwd: args.cwd,
2546
+ env: args.env
2547
+ }
2548
+ });
2549
+ const wrapped = wrapPtyLike(toPtyLike(pty), { recorder });
2550
+ const cleanup = attachInteractiveBridge(wrapped, args);
2551
+ const exit = await new Promise((resolveExit) => {
2552
+ wrapped.onExit((event) => {
2553
+ resolveExit({ exitCode: event.exitCode });
2554
+ });
2555
+ });
2556
+ cleanup();
2557
+ wrapped.writeCassette(args.outPath);
2558
+ const cassette = readPtyCassettePath(args.outPath);
2559
+ wrapped.dispose();
2560
+ return {
2561
+ path: args.outPath,
2562
+ eventCount: cassette.events.length,
2563
+ exitCode: exit.exitCode
2564
+ };
2565
+ }
2566
+ function parseRecordArgs(argv) {
2567
+ const out = {
2568
+ cols: terminalCols(),
2569
+ rows: terminalRows(),
2570
+ term: "xterm-256color",
2571
+ cwd: process.cwd(),
2572
+ backend: resolvePtyBackend(void 0),
2573
+ env: {}
2574
+ };
2575
+ for (let i = 0; i < argv.length; i += 1) {
2576
+ const arg = argv[i];
2577
+ const next = argv[i + 1];
2578
+ if (arg === "--") {
2579
+ const [command, ...args] = argv.slice(i + 1);
2580
+ if (!command) throw new Error("missing command after --\n\n" + ptyUsage());
2581
+ out.command = command;
2582
+ out.args = args;
2583
+ break;
2584
+ }
2585
+ if (!arg) continue;
2586
+ if (!arg.startsWith("-")) {
2587
+ out.command = arg;
2588
+ out.args = argv.slice(i + 1);
2589
+ break;
2590
+ }
2591
+ if (arg === "--out" && next) {
2592
+ out.outPath = next;
2593
+ i += 1;
2594
+ continue;
2595
+ }
2596
+ if (arg === "--cols" && next) {
2597
+ out.cols = parsePositiveInt(next, "--cols");
2598
+ i += 1;
2599
+ continue;
2600
+ }
2601
+ if (arg === "--rows" && next) {
2602
+ out.rows = parsePositiveInt(next, "--rows");
2603
+ i += 1;
2604
+ continue;
2605
+ }
2606
+ if (arg === "--term" && next) {
2607
+ out.term = next;
2608
+ i += 1;
2609
+ continue;
2610
+ }
2611
+ if (arg === "--cwd" && next) {
2612
+ out.cwd = next;
2613
+ i += 1;
2614
+ continue;
2615
+ }
2616
+ if (arg === "--backend" && next) {
2617
+ out.backend = resolvePtyBackend(next);
2618
+ i += 1;
2619
+ continue;
2620
+ }
2621
+ if (arg === "--env" && next) {
2622
+ const eq = next.indexOf("=");
2623
+ if (eq <= 0) throw new Error(`invalid --env: ${next}`);
2624
+ out.env[next.slice(0, eq)] = next.slice(eq + 1);
2625
+ i += 1;
2626
+ continue;
2627
+ }
2628
+ throw new Error(`unknown arg: ${arg}\n\n` + ptyUsage());
2629
+ }
2630
+ if (!out.outPath) throw new Error("missing --out <file>\n\n" + ptyUsage());
2631
+ if (!out.command) throw new Error("missing command to record\n\n" + ptyUsage());
2632
+ return {
2633
+ outPath: out.outPath,
2634
+ command: out.command,
2635
+ args: out.args ?? [],
2636
+ cols: out.cols ?? terminalCols(),
2637
+ rows: out.rows ?? terminalRows(),
2638
+ term: out.term ?? "xterm-256color",
2639
+ cwd: out.cwd ?? process.cwd(),
2640
+ backend: out.backend ?? "auto",
2641
+ env: out.env
2642
+ };
2643
+ }
2644
+ function parseReplayArgs(argv) {
2645
+ const out = { speed: 0 };
2646
+ for (let i = 0; i < argv.length; i += 1) {
2647
+ const arg = argv[i];
2648
+ const next = argv[i + 1];
2649
+ if (!out.path && arg && !arg.startsWith("-")) {
2650
+ out.path = arg;
2651
+ continue;
2652
+ }
2653
+ if (arg === "--speed" && next) {
2654
+ const speed = Number.parseFloat(next);
2655
+ if (!Number.isFinite(speed) || speed < 0) throw new Error(`invalid --speed: ${next}`);
2656
+ out.speed = speed;
2657
+ i += 1;
2658
+ continue;
2659
+ }
2660
+ throw new Error(`unknown arg: ${arg ?? ""}\n\n` + ptyUsage());
2661
+ }
2662
+ if (!out.path) throw new Error("missing <file>\n\n" + ptyUsage());
2663
+ return {
2664
+ path: out.path,
2665
+ speed: out.speed
2666
+ };
2667
+ }
2668
+ function parseArtifactArgs(argv) {
2669
+ const out = { json: false };
2670
+ for (const arg of argv) {
2671
+ if (!out.path && arg && !arg.startsWith("-")) {
2672
+ out.path = arg;
2673
+ continue;
2674
+ }
2675
+ if (arg === "--json") {
2676
+ out.json = true;
2677
+ continue;
2678
+ }
2679
+ throw new Error(`unknown arg: ${arg ?? ""}\n\n` + ptyUsage());
2680
+ }
2681
+ if (!out.path) throw new Error("missing <file>\n\n" + ptyUsage());
2682
+ return {
2683
+ path: out.path,
2684
+ json: out.json
2685
+ };
2686
+ }
2687
+ function attachInteractiveBridge(pty, args) {
2688
+ const onData = (data) => {
2689
+ process.stdout.write(typeof data === "string" ? data : Buffer.from(data));
2690
+ };
2691
+ const onInput = (data) => {
2692
+ pty.write(data);
2693
+ };
2694
+ const onResize = () => {
2695
+ pty.resize?.(terminalCols(args.cols), terminalRows(args.rows));
2696
+ };
2697
+ const rawState = setRawMode(true);
2698
+ const outputDisposable = pty.onData(onData);
2699
+ process.stdin.on("data", onInput);
2700
+ process.on("SIGWINCH", onResize);
2701
+ return () => {
2702
+ dispose(outputDisposable);
2703
+ process.stdin.off("data", onInput);
2704
+ process.off("SIGWINCH", onResize);
2705
+ setRawMode(rawState);
2706
+ };
2707
+ }
2708
+ function toPtyLike(pty) {
2709
+ return {
2710
+ write: (data) => pty.write(typeof data === "string" ? data : Buffer.from(data).toString()),
2711
+ resize: (cols, rows) => pty.resize(cols, rows),
2712
+ kill: (signal) => pty.kill(signal),
2713
+ onData: (listener) => pty.onData(listener),
2714
+ onExit: (listener) => pty.onExit(listener)
2715
+ };
2716
+ }
2717
+ function terminalCols(fallback = 80) {
2718
+ return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : fallback;
2719
+ }
2720
+ function terminalRows(fallback = 24) {
2721
+ return process.stdout.rows && process.stdout.rows > 0 ? process.stdout.rows : fallback;
2722
+ }
2723
+ function parsePositiveInt(value, name) {
2724
+ const parsed = Number.parseInt(value, 10);
2725
+ if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`invalid ${name}: ${value}`);
2726
+ return parsed;
2727
+ }
2728
+ function mergeEnv(base, override) {
2729
+ const env = {};
2730
+ for (const [key, value] of Object.entries(process.env)) if (typeof value === "string") env[key] = value;
2731
+ for (const [key, value] of Object.entries(base)) env[key] = value;
2732
+ for (const [key, value] of Object.entries(override)) env[key] = value;
2733
+ return env;
2734
+ }
2735
+ function setRawMode(enabled) {
2736
+ if (!process.stdin.isTTY) return false;
2737
+ const stdin = process.stdin;
2738
+ const wasRaw = Boolean(stdin.isRaw);
2739
+ process.stdin.setRawMode(enabled);
2740
+ process.stdin.resume();
2741
+ return wasRaw;
2742
+ }
2743
+ function dispose(disposable) {
2744
+ if (!disposable) return;
2745
+ if (typeof disposable === "function") disposable();
2746
+ else disposable.dispose();
2747
+ }
2748
+ function isHelp$1(arg) {
2749
+ return arg === "-h" || arg === "--help" || arg === "help";
2750
+ }
2751
+ //#endregion
2752
+ //#region src/script/commands.ts
2753
+ function readScriptArtifactCommandsPath(path) {
2754
+ const manifestCommands = readManifestBackedCommands(path);
2755
+ if (manifestCommands) return manifestCommands;
2756
+ const resolved = resolveScriptRunSummaryPath(path);
2757
+ const summaryManifest = findScriptSummaryManifest(resolved);
2758
+ if (summaryManifest) {
2759
+ validateScriptManifest(summaryManifest.manifest, summaryManifest.manifestPath);
2760
+ return createScriptArtifactCommands(resolved, relocateScriptManifestCommands(summaryManifest.manifest, summaryManifest.manifestPath), { manifestPath: summaryManifest.manifestPath });
2761
+ }
2762
+ return createScriptArtifactCommands(resolved, readScriptRunSummaryPath(resolved).commands);
2763
+ }
2764
+ function formatScriptArtifactCommandLines(result) {
2765
+ return [
2766
+ `kind=${result.kind}`,
2767
+ `path=${result.path}`,
2768
+ result.manifestPath ? `manifest=${result.manifestPath}` : null,
2769
+ ...Object.entries(result.commands).map(([name, command]) => `${name}: ${formatArgv(command.argv)}`)
2770
+ ].filter((line) => line !== null);
2771
+ }
2772
+ function selectScriptArtifactCommand(result, name) {
2773
+ if (!isScriptCommandName(name)) {
2774
+ const available = Object.keys(result.commands).sort().join(", ");
2775
+ throw new Error(`unknown script artifact command: ${name}${available ? ` (available: ${available})` : ""}`);
2776
+ }
2777
+ const command = result.commands[name];
2778
+ return {
2779
+ path: result.path,
2780
+ kind: result.kind,
2781
+ manifestPath: result.manifestPath,
2782
+ cwd: result.cwd,
2783
+ name,
2784
+ command,
2785
+ shell: result.shell[name] ?? formatArgv(command.argv)
2786
+ };
2787
+ }
2788
+ function validateScriptCommandArgv(argv, name = "<unknown>") {
2789
+ const [binary, subcommand] = argv;
2790
+ if (binary !== "ptywright" || subcommand !== "run-all") throw new Error(`command ${name} argv must start with a supported ptywright script command`);
2791
+ }
2792
+ function createScriptArtifactCommands(path, commands, options = {}) {
2793
+ return {
2794
+ path,
2795
+ kind: "run-summary",
2796
+ manifestPath: options.manifestPath,
2797
+ cwd: process.cwd(),
2798
+ shell: Object.fromEntries(Object.entries(commands).map(([name, command]) => [name, formatArgv(command.argv)])),
2799
+ commands
2800
+ };
2801
+ }
2802
+ function readManifestBackedCommands(path) {
2803
+ const manifestPath = resolveScriptManifestPath(path);
2804
+ if (!manifestPath.endsWith("ptywright-script.manifest.json")) return null;
2805
+ if (!existsSync(manifestPath)) return null;
2806
+ const manifest = readScriptManifestPath(manifestPath);
2807
+ validateScriptManifest(manifest, manifestPath);
2808
+ return createScriptArtifactCommands(resolveManifestPrimaryPath$1(manifest, manifestPath), relocateScriptManifestCommands(manifest, manifestPath), { manifestPath });
2809
+ }
2810
+ function isScriptCommandName(name) {
2811
+ return name === "runAll" || name === "updateGoldens";
2812
+ }
2813
+ //#endregion
2814
+ //#region src/script/inspect.ts
2815
+ function inspectScriptArtifactPath(path) {
2816
+ const manifestPath = resolveScriptManifestPath(path);
2817
+ const hasManifest = existsSync(manifestPath);
2818
+ const commands = readScriptArtifactCommandsPath(hasManifest ? manifestPath : path);
2819
+ const manifest = hasManifest ? maybeReadManifest(manifestPath) : void 0;
2820
+ if (!manifest) readScriptRunSummaryPath(path);
2821
+ return {
2822
+ path: resolve(process.cwd(), path),
2823
+ targetPath: hasManifest ? manifestPath : resolve(process.cwd(), path),
2824
+ kind: hasManifest ? "manifest" : "run-summary",
2825
+ ok: true,
2826
+ commands,
2827
+ manifest
2828
+ };
2829
+ }
2830
+ function formatScriptInspectLines(result) {
2831
+ const lines = [
2832
+ "ok script-inspect",
2833
+ `kind=${result.kind}`,
2834
+ `path=${result.targetPath}`
2835
+ ];
2836
+ if (result.manifest) {
2837
+ lines.push(`manifest=${result.manifest.path}`, `manifestFiles=${result.manifest.files.totalCount}`, `manifestBytes=${result.manifest.files.totalBytes}`, `total=${result.manifest.totalCount}`, `failures=${result.manifest.failureCount}`);
2838
+ for (const [kind, count] of Object.entries(result.manifest.files.byKind)) lines.push(`manifestFileKind.${kind}=${count}`);
2839
+ if (result.manifest.files.failures.length > 0) lines.push(...result.manifest.files.failures.map((file) => `manifestFileFailure=${file.path} kind=${file.kind}${file.role ? ` role=${file.role}` : ""}`));
2840
+ }
2841
+ if (result.commands) {
2842
+ if (result.commands.manifestPath) lines.push(`commandsManifest=${result.commands.manifestPath}`);
2843
+ lines.push(`commands=${Object.keys(result.commands.commands).sort().join(",")}`, ...formatScriptArtifactCommandLines(result.commands).filter((line) => !line.startsWith("kind=") && !line.startsWith("path=")).map((line) => `command.${line}`));
2844
+ }
2845
+ return lines;
2846
+ }
2847
+ function maybeReadManifest(path) {
2848
+ if (basename(path) !== "ptywright-script.manifest.json") return void 0;
2849
+ const manifest = readScriptManifestPath(path);
2850
+ validateScriptManifest(manifest, path);
2851
+ const byKind = {};
2852
+ let totalBytes = 0;
2853
+ const failures = [];
2854
+ for (const file of manifest.files) {
2855
+ byKind[file.kind] = (byKind[file.kind] ?? 0) + 1;
2856
+ totalBytes += file.bytes;
2857
+ if (file.ok === false) failures.push({
2858
+ path: file.path,
2859
+ kind: file.kind,
2860
+ role: file.role
2861
+ });
2862
+ }
2863
+ return {
2864
+ path,
2865
+ ok: manifest.ok,
2866
+ rootDir: manifest.rootDir,
2867
+ primaryPath: resolveManifestPrimaryPath$1(manifest, path),
2868
+ generatedAt: manifest.generatedAt,
2869
+ totalCount: manifest.totalCount,
2870
+ failureCount: manifest.failureCount,
2871
+ files: {
2872
+ totalCount: manifest.files.length,
2873
+ totalBytes,
2874
+ byKind: Object.fromEntries(Object.entries(byKind).sort(([a], [b]) => a.localeCompare(b))),
2875
+ failures
2876
+ }
2877
+ };
2878
+ }
2879
+ //#endregion
2880
+ //#region src/cli.ts
2881
+ function usage() {
2882
+ return [
2883
+ "ptywright <command>",
2884
+ "",
2885
+ "Commands:",
2886
+ " mcp Start the MCP server over stdio (default)",
2887
+ " mcp-http Start the MCP server over Streamable HTTP",
2888
+ " agent run <file> Run a browser-hosted terminal-agent flow",
2889
+ " agent record <file> --out <file> Record browser interactions into a flow",
2890
+ " agent replay <run> Replay a recorded terminal-agent flow without AI",
2891
+ " agent promote <run> Promote a run/cassette into the committed cassette suite",
2892
+ " agent replay-all [dir] Replay all agent cassettes/run records in a directory",
2893
+ " agent rerun <summary> Rerun from agent replay/check/promote summary metadata",
2894
+ " agent commands <artifact> Print replay/update argv from an agent artifact",
2895
+ " agent inspect <artifact|dir> Inspect validation, files, and commands for an agent artifact",
2896
+ " agent exec <artifact> --command <name> Execute one command from an agent artifact",
2897
+ " agent check [dir] Validate and replay committed agent cassettes",
2898
+ " agent validate <path> Validate agent flow/cassette/run-record/summary artifacts",
2899
+ " agent init <flavor> <file> Write a starter agent flow spec",
2900
+ " pty record --out <file> -- <command> [args...] Record a raw PTY cassette",
2901
+ " pty replay <file> Replay a raw PTY cassette without rerunning the command",
2902
+ " pty inspect <file> Inspect a raw PTY cassette",
2903
+ " pty validate <file> Validate a raw PTY cassette",
2904
+ " run <file> Run one script (JSON/TS) and write artifacts",
2905
+ " run-all [dir] Run all scripts in a directory and write a suite report",
2906
+ " script commands <summary|dir> Print replay/update argv from a script run summary",
2907
+ " script inspect <summary|dir> Inspect validation, files, and commands for a script artifact",
2908
+ " script exec <summary|dir> --command <name> Execute one command from a script summary",
2909
+ " script validate <summary|dir> Validate a script run summary artifact",
2910
+ " help Show help",
2911
+ "",
2912
+ "Run options:",
2913
+ " --artifacts-dir <dir> Override artifacts directory",
2914
+ " --steps <module.ts> Inject custom step handlers",
2915
+ " --update-goldens Update golden snapshots",
2916
+ "",
2917
+ "Run-all options:",
2918
+ " --dir <dir> Directory to scan (default: scripts)",
2919
+ " --artifacts-root <dir> Suite artifacts root (default: .tmp/run-all)",
2920
+ " --steps <module.ts> Inject custom step handlers",
2921
+ " --update-goldens Update golden snapshots",
2922
+ "",
2923
+ "Script artifact options:",
2924
+ " --command <name> Select a reusable command (runAll|updateGoldens)",
2925
+ " --json Print machine-readable script artifact output",
2926
+ "",
2927
+ "Agent options:",
2928
+ " --artifacts-dir <dir> Override agent run artifact directory",
2929
+ " --cassette-dir <dir> Committed cassette directory for promote/check",
2930
+ " --snapshot-dir <dir> Snapshot directory for promoted cassettes",
2931
+ " --out <file> Output path for agent record",
2932
+ " --duration-ms <ms> Recording window duration",
2933
+ " --artifacts-root <dir> Override agent replay-all artifact root",
2934
+ " --command <name> Print one agent artifact command by name",
2935
+ " --update-snapshots Update terminal/DOM snapshots",
2936
+ " --headed Show the browser while running",
2937
+ " --json Print machine-readable agent check output",
2938
+ "",
2939
+ "PTY cassette options:",
2940
+ " --out <file> Output cassette JSON path",
2941
+ " --cols <n> / --rows <n> Terminal size for recording",
2942
+ " --term <name> TERM/name value (default: xterm-256color)",
2943
+ " --backend <name> auto|bun-terminal|bun-pty",
2944
+ " --speed <n> Replay timing multiplier; 0 means instant",
2945
+ "",
2946
+ "MCP options:",
2947
+ " --caps <list> Capabilities: all|core|debug|script|recording",
2948
+ "",
2949
+ "MCP HTTP options (mcp-http):",
2950
+ " --host <host> Bind host (default: 127.0.0.1)",
2951
+ " --port <port> Bind port (default: 3000)",
2952
+ " --allowed-origins <list> Comma/space separated Origin allowlist",
2953
+ " --no-cors Disable CORS headers"
2954
+ ].join("\n");
2955
+ }
2956
+ function isHelp(arg) {
2957
+ return arg === "-h" || arg === "--help" || arg === "help";
2958
+ }
2959
+ function logLines(lines, stderr) {
2960
+ const filtered = lines.map((l) => l?.trim()).filter(Boolean);
2961
+ for (const line of filtered) (stderr ? console.error : console.log)(line);
2962
+ }
2963
+ function parseCaps(value) {
2964
+ const parts = value.split(/[\s,]+/g).map((p) => p.trim().toLowerCase()).filter(Boolean);
2965
+ const out = [];
2966
+ for (const p of parts) if (p === "all") out.push("all");
2967
+ else if (p === "core") out.push("core");
2968
+ else if (p === "debug") out.push("debug");
2969
+ else if (p === "script" || p === "scripts" || p === "runner" || p === "run") out.push("script");
2970
+ else if (p === "recording" || p === "record" || p === "rec") out.push("recording");
2971
+ else throw new Error(`unknown capability: ${p}`);
2972
+ return out;
2973
+ }
2974
+ function parseRunArgs(argv) {
2975
+ const out = { updateGoldens: false };
2976
+ for (let i = 0; i < argv.length; i += 1) {
2977
+ const arg = argv[i];
2978
+ const next = argv[i + 1];
2979
+ if (!out.scriptPath && arg && !arg.startsWith("-")) {
2980
+ out.scriptPath = arg;
2981
+ continue;
2982
+ }
2983
+ if (arg === "--artifacts-dir" && next) {
2984
+ out.artifactsDir = next;
2985
+ i += 1;
2986
+ continue;
2987
+ }
2988
+ if (arg === "--steps" && next) {
2989
+ out.stepsPath = next;
2990
+ i += 1;
2991
+ continue;
2992
+ }
2993
+ if (arg === "--update-goldens") {
2994
+ out.updateGoldens = true;
2995
+ continue;
2996
+ }
2997
+ throw new Error(`unknown arg: ${arg ?? ""}`);
2998
+ }
2999
+ if (!out.scriptPath) throw new Error("missing <file>\n\n" + usage());
3000
+ return out;
3001
+ }
3002
+ function parseRunAllArgs(argv) {
3003
+ const out = { updateGoldens: false };
3004
+ for (let i = 0; i < argv.length; i += 1) {
3005
+ const arg = argv[i];
3006
+ const next = argv[i + 1];
3007
+ if (!out.dir && arg && !arg.startsWith("-")) {
3008
+ out.dir = arg;
3009
+ continue;
3010
+ }
3011
+ if (arg === "--dir" && next) {
3012
+ out.dir = next;
3013
+ i += 1;
3014
+ continue;
3015
+ }
3016
+ if (arg === "--artifacts-root" && next) {
3017
+ out.artifactsRoot = next;
3018
+ i += 1;
3019
+ continue;
3020
+ }
3021
+ if (arg === "--steps" && next) {
3022
+ out.stepsPath = next;
3023
+ i += 1;
3024
+ continue;
3025
+ }
3026
+ if (arg === "--update-goldens") {
3027
+ out.updateGoldens = true;
3028
+ continue;
3029
+ }
3030
+ throw new Error(`unknown arg: ${arg ?? ""}`);
3031
+ }
3032
+ return out;
3033
+ }
3034
+ async function cmdMcp(argv) {
3035
+ let capabilities;
3036
+ for (let i = 0; i < argv.length; i += 1) {
3037
+ const arg = argv[i];
3038
+ const next = argv[i + 1];
3039
+ if (isHelp(arg)) {
3040
+ console.log(usage());
3041
+ return;
3042
+ }
3043
+ if (arg === "--caps" && next) {
3044
+ capabilities = parseCaps(next);
3045
+ i += 1;
3046
+ continue;
3047
+ }
3048
+ throw new Error(`unknown arg: ${arg ?? ""}`);
3049
+ }
3050
+ const { server, sessions } = createPtywrightServer({ capabilities });
3051
+ const transport = new StdioServerTransport();
3052
+ await server.connect(transport);
3053
+ function shutdown() {
3054
+ sessions.closeAll();
3055
+ server.close();
3056
+ }
3057
+ process.on("SIGINT", shutdown);
3058
+ process.on("SIGTERM", shutdown);
3059
+ }
3060
+ async function cmdMcpHttp(argv) {
3061
+ let capabilities;
3062
+ let hostname;
3063
+ let port;
3064
+ let allowedOrigins;
3065
+ let cors = true;
3066
+ for (let i = 0; i < argv.length; i += 1) {
3067
+ const arg = argv[i];
3068
+ const next = argv[i + 1];
3069
+ if (isHelp(arg)) {
3070
+ console.log(usage());
3071
+ return;
3072
+ }
3073
+ if (arg === "--caps" && next) {
3074
+ capabilities = parseCaps(next);
3075
+ i += 1;
3076
+ continue;
3077
+ }
3078
+ if ((arg === "--host" || arg === "--hostname") && next) {
3079
+ hostname = next;
3080
+ i += 1;
3081
+ continue;
3082
+ }
3083
+ if (arg === "--port" && next) {
3084
+ const value = Number.parseInt(next, 10);
3085
+ if (!Number.isFinite(value) || value < 0) throw new Error(`invalid --port: ${next}`);
3086
+ port = value;
3087
+ i += 1;
3088
+ continue;
3089
+ }
3090
+ if (arg === "--allowed-origins" && next) {
3091
+ allowedOrigins = next.split(/[\s,]+/g).map((v) => v.trim()).filter(Boolean);
3092
+ i += 1;
3093
+ continue;
3094
+ }
3095
+ if (arg === "--no-cors") {
3096
+ cors = false;
3097
+ continue;
3098
+ }
3099
+ throw new Error(`unknown arg: ${arg ?? ""}`);
3100
+ }
3101
+ const handle = await startPtywrightHttpServer({
3102
+ hostname,
3103
+ port,
3104
+ capabilities,
3105
+ allowedOrigins,
3106
+ cors
3107
+ });
3108
+ console.log(`listening ${handle.url}`);
3109
+ console.log(`health http://${handle.hostname}:${handle.port}/health`);
3110
+ function shutdown() {
3111
+ handle.close();
3112
+ }
3113
+ process.on("SIGINT", shutdown);
3114
+ process.on("SIGTERM", shutdown);
3115
+ }
3116
+ async function cmdRun(argv) {
3117
+ const args = parseRunArgs(argv);
3118
+ const result = await runScriptPath(args.scriptPath, {
3119
+ artifactsDir: args.artifactsDir,
3120
+ updateGoldens: args.updateGoldens,
3121
+ stepsPath: args.stepsPath
3122
+ });
3123
+ if (!result.ok) {
3124
+ logLines([
3125
+ result.error,
3126
+ result.artifactsDir ? `artifacts=${result.artifactsDir}` : null,
3127
+ result.reportPath ? `report=${result.reportPath}` : null,
3128
+ result.castPath ? `cast=${result.castPath}` : null,
3129
+ result.failureArtifacts?.lastViewPath ? `last=${result.failureArtifacts.lastViewPath}` : null,
3130
+ result.failureArtifacts?.errorPath ? `error=${result.failureArtifacts.errorPath}` : null
3131
+ ], true);
3132
+ return 1;
3133
+ }
3134
+ logLines([
3135
+ `ok artifacts=${result.artifactsDir}`,
3136
+ result.reportPath ? `report=${result.reportPath}` : null,
3137
+ result.castPath ? `cast=${result.castPath}` : null
3138
+ ], false);
3139
+ return 0;
3140
+ }
3141
+ async function cmdRunAll(argv) {
3142
+ const args = parseRunAllArgs(argv);
3143
+ const result = await runAllScripts({
3144
+ dir: args.dir,
3145
+ artifactsRoot: args.artifactsRoot,
3146
+ stepsPath: args.stepsPath,
3147
+ updateGoldens: args.updateGoldens
3148
+ });
3149
+ const failures = result.entries.filter((e) => !e.result.ok);
3150
+ if (failures.length === 0) {
3151
+ console.log(`ok count=${result.entries.length} dir=${result.dir}\nreport=${result.reportPath}\nsummary=${result.summaryPath}`);
3152
+ return 0;
3153
+ }
3154
+ console.error(`failed count=${failures.length}/${result.entries.length} dir=${result.dir}\nreport=${result.reportPath}\nsummary=${result.summaryPath}`);
3155
+ for (const f of failures) {
3156
+ if (f.result.ok) continue;
3157
+ console.error(`- ${f.filePath}: ${f.result.error}`);
3158
+ if (f.result.failureArtifacts) {
3159
+ console.error(` artifacts=${f.result.artifactsDir ?? ""}`);
3160
+ console.error(` last=${f.result.failureArtifacts.lastViewPath}`);
3161
+ console.error(` error=${f.result.failureArtifacts.errorPath}`);
3162
+ }
3163
+ }
3164
+ return 1;
3165
+ }
3166
+ function parseScriptArgs(argv) {
3167
+ const [mode, ...rest] = argv;
3168
+ if (mode !== "commands" && mode !== "exec" && mode !== "inspect" && mode !== "validate") throw new Error("missing script subcommand: commands|inspect|exec|validate\n\n" + usage());
3169
+ const out = { json: false };
3170
+ for (let i = 0; i < rest.length; i += 1) {
3171
+ const arg = rest[i];
3172
+ const next = rest[i + 1];
3173
+ if (!out.path && arg && !arg.startsWith("-")) {
3174
+ out.path = arg;
3175
+ continue;
3176
+ }
3177
+ if (arg === "--command" && next) {
3178
+ out.commandName = next;
3179
+ i += 1;
3180
+ continue;
3181
+ }
3182
+ if (arg === "--json") {
3183
+ out.json = true;
3184
+ continue;
3185
+ }
3186
+ throw new Error(`unknown arg: ${arg ?? ""}`);
3187
+ }
3188
+ if (!out.path) throw new Error(`missing ${mode === "validate" ? "<summary|dir>" : "<artifact>"} for script ${mode}\n\n` + usage());
3189
+ if (mode === "exec" && !out.commandName) throw new Error(`missing --command <name> for script exec\n\n` + usage());
3190
+ return {
3191
+ mode,
3192
+ path: out.path,
3193
+ commandName: out.commandName,
3194
+ json: out.json
3195
+ };
3196
+ }
3197
+ async function cmdScript(argv) {
3198
+ const args = parseScriptArgs(argv);
3199
+ if (args.mode === "validate") {
3200
+ const manifestPath = resolveScriptManifestPath(args.path);
3201
+ const hasManifest = manifestPath.endsWith("ptywright-script.manifest.json") ? readOptionalScriptManifest(manifestPath) : null;
3202
+ const summaryPath = hasManifest ? resolveScriptRunSummaryPath(hasManifest.primaryPath) : resolveScriptRunSummaryPath(args.path);
3203
+ if (hasManifest) validateScriptManifest(hasManifest, manifestPath);
3204
+ const summary = readScriptRunSummaryPath(summaryPath);
3205
+ if (args.json) logLines([JSON.stringify({
3206
+ ok: true,
3207
+ kind: hasManifest ? "manifest" : "run-summary",
3208
+ path: summaryPath,
3209
+ manifestPath: hasManifest ? manifestPath : void 0,
3210
+ totalCount: summary.totalCount,
3211
+ failureCount: summary.failureCount
3212
+ }, null, 2)], false);
3213
+ else logLines([
3214
+ "ok script-summary",
3215
+ `path=${summaryPath}`,
3216
+ hasManifest ? `manifest=${manifestPath}` : null,
3217
+ `count=${summary.totalCount}`,
3218
+ `failures=${summary.failureCount}`
3219
+ ], false);
3220
+ return 0;
3221
+ }
3222
+ if (args.mode === "inspect") {
3223
+ const result = inspectScriptArtifactPath(args.path);
3224
+ if (args.json) logLines([JSON.stringify(result, null, 2)], false);
3225
+ else logLines(formatScriptInspectLines(result), false);
3226
+ return 0;
3227
+ }
3228
+ const result = readScriptArtifactCommandsPath(args.path);
3229
+ if (args.mode === "commands") {
3230
+ if (args.commandName) {
3231
+ const selected = selectScriptArtifactCommand(result, args.commandName);
3232
+ if (args.json) logLines([JSON.stringify(selected, null, 2)], false);
3233
+ else logLines([selected.shell], false);
3234
+ return 0;
3235
+ }
3236
+ if (args.json) logLines([JSON.stringify(result, null, 2)], false);
3237
+ else logLines(formatScriptArtifactCommandLines(result), false);
3238
+ return 0;
3239
+ }
3240
+ const selected = selectScriptArtifactCommand(result, args.commandName);
3241
+ const commandArgv = selected.command.argv;
3242
+ validateScriptCommandArgv(commandArgv, selected.name);
3243
+ const [, subcommand, ...rest] = commandArgv;
3244
+ if (subcommand === "run-all") return cmdRunAll(rest);
3245
+ throw new Error(`unsupported script artifact command: ${subcommand ?? ""}`);
3246
+ }
3247
+ function readOptionalScriptManifest(path) {
3248
+ try {
3249
+ return readScriptManifestPath(path);
3250
+ } catch {
3251
+ return null;
3252
+ }
3253
+ }
3254
+ function parseAgentArgs(argv) {
3255
+ const [mode, ...rest] = argv;
3256
+ if (mode !== "run" && mode !== "replay" && mode !== "promote" && mode !== "replay-all" && mode !== "rerun" && mode !== "commands" && mode !== "inspect" && mode !== "exec" && mode !== "init" && mode !== "record" && mode !== "validate" && mode !== "check") throw new Error("missing agent subcommand: run|record|replay|promote|replay-all|rerun|commands|inspect|exec|check|validate|init\n\n" + usage());
3257
+ const out = {
3258
+ updateSnapshots: false,
3259
+ headed: false,
3260
+ json: false
3261
+ };
3262
+ for (let i = 0; i < rest.length; i += 1) {
3263
+ const arg = rest[i];
3264
+ const next = rest[i + 1];
3265
+ if (mode === "init" && !out.flavor && arg && !arg.startsWith("-")) {
3266
+ out.flavor = parseAgentFlavor(arg);
3267
+ continue;
3268
+ }
3269
+ if (arg === "--artifacts-root" && next) {
3270
+ out.artifactsRoot = next;
3271
+ i += 1;
3272
+ continue;
3273
+ }
3274
+ if ((arg === "--cassette-dir" || arg === "--dir") && next) {
3275
+ out.cassetteDir = next;
3276
+ i += 1;
3277
+ continue;
3278
+ }
3279
+ if (arg === "--snapshot-dir" && next) {
3280
+ out.snapshotDir = next;
3281
+ i += 1;
3282
+ continue;
3283
+ }
3284
+ if (!out.path && arg && !arg.startsWith("-")) {
3285
+ out.path = arg;
3286
+ continue;
3287
+ }
3288
+ if (arg === "--artifacts-dir" && next) {
3289
+ out.artifactsDir = next;
3290
+ i += 1;
3291
+ continue;
3292
+ }
3293
+ if (arg === "--out" && next) {
3294
+ out.outPath = next;
3295
+ i += 1;
3296
+ continue;
3297
+ }
3298
+ if (arg === "--duration-ms" && next) {
3299
+ const value = Number.parseInt(next, 10);
3300
+ if (!Number.isFinite(value) || value < 0) throw new Error(`invalid --duration-ms: ${next}`);
3301
+ out.durationMs = value;
3302
+ i += 1;
3303
+ continue;
3304
+ }
3305
+ if (arg === "--command" && next) {
3306
+ out.commandName = next;
3307
+ i += 1;
3308
+ continue;
3309
+ }
3310
+ if (arg === "--update-snapshots") {
3311
+ out.updateSnapshots = true;
3312
+ continue;
3313
+ }
3314
+ if (arg === "--headed") {
3315
+ out.headed = true;
3316
+ continue;
3317
+ }
3318
+ if (arg === "--json") {
3319
+ out.json = true;
3320
+ continue;
3321
+ }
3322
+ throw new Error(`unknown arg: ${arg ?? ""}`);
3323
+ }
3324
+ if (mode === "init" && !out.flavor) throw new Error(`missing <flavor> for agent init\n\n` + usage());
3325
+ if (!out.path && mode !== "replay-all" && mode !== "check") throw new Error(`missing ${mode === "rerun" ? "<summary>" : mode === "commands" || mode === "inspect" || mode === "exec" ? "<artifact>" : "<file>"} for agent ${mode}\n\n` + usage());
3326
+ if (mode === "record" && !out.outPath) throw new Error(`missing --out <file> for agent record\n\n` + usage());
3327
+ if (mode === "exec" && !out.commandName) throw new Error(`missing --command <name> for agent exec\n\n` + usage());
3328
+ return {
3329
+ mode,
3330
+ path: out.path,
3331
+ flavor: out.flavor,
3332
+ artifactsDir: out.artifactsDir,
3333
+ artifactsRoot: out.artifactsRoot,
3334
+ cassetteDir: out.cassetteDir,
3335
+ snapshotDir: out.snapshotDir,
3336
+ outPath: out.outPath,
3337
+ durationMs: out.durationMs,
3338
+ commandName: out.commandName,
3339
+ updateSnapshots: out.updateSnapshots,
3340
+ headed: out.headed,
3341
+ json: out.json
3342
+ };
3343
+ }
3344
+ async function cmdAgent(argv) {
3345
+ const args = parseAgentArgs(argv);
3346
+ if (args.mode === "init") {
3347
+ const spec = createAgentTemplateSpec(args.flavor ?? "generic");
3348
+ const path = args.path;
3349
+ mkdirSync(dirname(path), { recursive: true });
3350
+ writeFileSync(path, JSON.stringify({
3351
+ $schema: "../schemas/ptywright-agent.schema.json",
3352
+ ...spec
3353
+ }, null, 2) + "\n", "utf8");
3354
+ logLines([`ok wrote ${path}`], false);
3355
+ return 0;
3356
+ }
3357
+ if (args.mode === "record") {
3358
+ const result = await recordAgentSpecPath(args.path, {
3359
+ outPath: args.outPath,
3360
+ durationMs: args.durationMs,
3361
+ headless: !args.headed
3362
+ });
3363
+ logLines([
3364
+ `${result.ok ? "ok" : "failed"} record=${result.outPath}`,
3365
+ `steps=${result.stepCount}`,
3366
+ result.url ? `url=${result.url}` : null,
3367
+ result.error ? `error=${result.error}` : null
3368
+ ], !result.ok);
3369
+ return result.ok ? 0 : 1;
3370
+ }
3371
+ if (args.mode === "validate") {
3372
+ const result = await validateAgentArtifactsPath(args.path, { preferManifestBundle: true });
3373
+ if (args.json) {
3374
+ logLines([JSON.stringify(result, null, 2)], false);
3375
+ return result.ok ? 0 : 1;
3376
+ }
3377
+ const failures = result.entries.filter((entry) => !entry.ok);
3378
+ logLines([
3379
+ `${result.ok ? "ok" : "failed"} count=${result.totalCount} path=${result.path}`,
3380
+ result.failureCount > 0 ? `failures=${result.failureCount}` : null,
3381
+ ...failures.flatMap((entry) => [
3382
+ `- ${entry.filePath}`,
3383
+ ` kind=${entry.kind}`,
3384
+ entry.error ? ` error=${entry.error}` : null
3385
+ ])
3386
+ ], !result.ok);
3387
+ return result.ok ? 0 : 1;
3388
+ }
3389
+ if (args.mode === "commands") {
3390
+ const result = await readAgentArtifactCommandsPath(args.path);
3391
+ const manifestPath = result.kind === "manifest" ? result.path : result.manifestPath;
3392
+ if (manifestPath) {
3393
+ const manifest = readAgentManifestPath(manifestPath);
3394
+ validateAgentManifestCommandTargets(manifest, manifestPath);
3395
+ validateAgentManifestFiles(manifest, manifestPath);
3396
+ }
3397
+ if (args.commandName) {
3398
+ const selected = selectAgentArtifactCommand(result, args.commandName);
3399
+ if (args.json) logLines([JSON.stringify(selected, null, 2)], false);
3400
+ else logLines([selected.shell], false);
3401
+ return 0;
3402
+ }
3403
+ if (args.json) logLines([JSON.stringify(result, null, 2)], false);
3404
+ else logLines(formatAgentArtifactCommandLines(result), false);
3405
+ return 0;
3406
+ }
3407
+ if (args.mode === "inspect") {
3408
+ const result = await inspectAgentArtifactPath(args.path);
3409
+ if (args.json) logLines([JSON.stringify(result, null, 2)], false);
3410
+ else logLines(formatAgentInspectLines(result), !result.ok);
3411
+ return result.ok ? 0 : 1;
3412
+ }
3413
+ if (args.mode === "exec") {
3414
+ const result = await readAgentArtifactCommandsPath(args.path);
3415
+ const manifestPath = result.kind === "manifest" ? result.path : result.manifestPath;
3416
+ if (manifestPath) {
3417
+ const manifest = readAgentManifestPath(manifestPath);
3418
+ validateAgentManifestCommandTargets(manifest, manifestPath);
3419
+ validateAgentManifestFiles(manifest, manifestPath);
3420
+ }
3421
+ const selected = selectAgentArtifactCommand(result, args.commandName);
3422
+ const argv = selected.command.argv;
3423
+ validateAgentCommandArgv(argv, selected.name);
3424
+ const [, , subcommand, ...rest] = argv;
3425
+ return cmdAgent([subcommand ?? "", ...rest]);
3426
+ }
3427
+ if (args.mode === "check") {
3428
+ const result = await checkAgentRegression({
3429
+ cassetteDir: args.path ?? args.cassetteDir,
3430
+ artifactsRoot: args.artifactsRoot,
3431
+ headless: !args.headed,
3432
+ updateSnapshots: args.updateSnapshots
3433
+ });
3434
+ if (args.json) logLines([JSON.stringify(formatAgentCheckJson(result), null, 2)], false);
3435
+ else logLines(formatAgentCheckLines(result), !result.ok);
3436
+ return result.ok ? 0 : 1;
3437
+ }
3438
+ if (args.mode === "promote") {
3439
+ const result = await promoteAgentCassette({
3440
+ sourcePath: args.path,
3441
+ cassetteDir: args.cassetteDir,
3442
+ snapshotDir: args.snapshotDir,
3443
+ artifactsRoot: args.artifactsRoot,
3444
+ headless: !args.headed,
3445
+ updateSnapshots: args.updateSnapshots
3446
+ });
3447
+ if (args.json) logLines([JSON.stringify(formatAgentPromoteSummary(result), null, 2)], false);
3448
+ else logLines(formatAgentPromoteLines(result), !result.ok);
3449
+ return result.ok ? 0 : 1;
3450
+ }
3451
+ if (args.mode === "rerun") {
3452
+ const rerun = await rerunAgentSummary({
3453
+ path: args.path,
3454
+ artifactsRoot: args.artifactsRoot,
3455
+ headless: !args.headed,
3456
+ updateSnapshots: args.updateSnapshots
3457
+ });
3458
+ if (rerun.kind === "check-summary") {
3459
+ if (args.json) logLines([JSON.stringify(formatAgentCheckJson(rerun.result), null, 2)], false);
3460
+ else logLines(formatAgentCheckLines(rerun.result), !rerun.result.ok);
3461
+ return rerun.result.ok ? 0 : 1;
3462
+ }
3463
+ if (rerun.kind === "promote-summary") {
3464
+ if (args.json) logLines([JSON.stringify(formatAgentPromoteSummary(rerun.result), null, 2)], false);
3465
+ else logLines(formatAgentPromoteLines(rerun.result), !rerun.result.ok);
3466
+ return rerun.result.ok ? 0 : 1;
3467
+ }
3468
+ const failures = rerun.result.entries.filter((entry) => !entry.result.ok);
3469
+ if (args.json) {
3470
+ logLines([JSON.stringify(formatAgentReplaySummary(rerun.result), null, 2)], false);
3471
+ return failures.length === 0 ? 0 : 1;
3472
+ }
3473
+ logLines([
3474
+ `${failures.length === 0 ? "ok" : "failed"} rerun=${rerun.kind}`,
3475
+ `count=${rerun.result.entries.length}`,
3476
+ `dir=${rerun.result.dir}`,
3477
+ `report=${rerun.result.reportPath}`,
3478
+ `summary=${rerun.result.summaryPath}`,
3479
+ ...failures.flatMap((entry) => [`- ${entry.filePath}`, ...entry.result.errors.map((error) => ` error=${error}`)])
3480
+ ], failures.length > 0);
3481
+ return failures.length === 0 ? 0 : 1;
3482
+ }
3483
+ if (args.mode === "replay-all") {
3484
+ const result = await replayAllAgentRecords({
3485
+ dir: args.path,
3486
+ artifactsRoot: args.artifactsRoot,
3487
+ headless: !args.headed,
3488
+ updateSnapshots: args.updateSnapshots
3489
+ });
3490
+ const failures = result.entries.filter((entry) => !entry.result.ok);
3491
+ if (args.json) {
3492
+ logLines([JSON.stringify(formatAgentReplaySummary(result), null, 2)], false);
3493
+ return failures.length === 0 ? 0 : 1;
3494
+ }
3495
+ if (failures.length === 0) {
3496
+ logLines([
3497
+ `ok count=${result.entries.length} dir=${result.dir}`,
3498
+ `report=${result.reportPath}`,
3499
+ `summary=${result.summaryPath}`
3500
+ ], false);
3501
+ return 0;
3502
+ }
3503
+ logLines([
3504
+ `failed count=${failures.length}/${result.entries.length} dir=${result.dir}`,
3505
+ `report=${result.reportPath}`,
3506
+ `summary=${result.summaryPath}`,
3507
+ ...failures.flatMap((entry) => [`- ${entry.filePath}`, ...entry.result.errors.map((error) => ` error=${error}`)])
3508
+ ], true);
3509
+ return 1;
3510
+ }
3511
+ const options = {
3512
+ artifactsDir: args.artifactsDir,
3513
+ updateSnapshots: args.updateSnapshots,
3514
+ headless: !args.headed
3515
+ };
3516
+ const result = args.mode === "run" ? await runAgentSpecPath(args.path, options) : await replayAgentRecordPath(args.path, options);
3517
+ if (args.json) {
3518
+ logLines([JSON.stringify(readAgentRunRecordPath(result.recordPath), null, 2)], false);
3519
+ return result.ok ? 0 : 1;
3520
+ }
3521
+ logLines([
3522
+ `${result.ok ? "ok" : "failed"} agent=${result.name}`,
3523
+ `report=${result.reportPath}`,
3524
+ `record=${result.recordPath}`,
3525
+ `flow=${result.flowPath}`,
3526
+ `cassette=${result.cassettePath}`,
3527
+ `snapshots=${result.snapshotDir}`,
3528
+ `mode=${result.mode}`,
3529
+ `frames=${result.cassetteFrameCount}`,
3530
+ result.replayCommand ? `replay=${result.replayCommand}` : null,
3531
+ ...result.errors.map((error) => `error=${error}`)
3532
+ ], !result.ok);
3533
+ return result.ok ? 0 : 1;
3534
+ }
3535
+ function parseAgentFlavor(value) {
3536
+ if (value === "codex" || value === "claude" || value === "droid" || value === "generic") return value;
3537
+ if (value === "droidx") return "droid";
3538
+ throw new Error(`unknown agent flavor: ${value}`);
3539
+ }
3540
+ async function main(argv = process.argv.slice(2)) {
3541
+ const [command, ...rest] = argv;
3542
+ if (!command) {
3543
+ await cmdMcp([]);
3544
+ return;
3545
+ }
3546
+ if (isHelp(command)) {
3547
+ console.log(usage());
3548
+ return;
3549
+ }
3550
+ if (command === "mcp") {
3551
+ await cmdMcp(rest);
3552
+ return;
3553
+ }
3554
+ if (command === "mcp-http") {
3555
+ await cmdMcpHttp(rest);
3556
+ return;
3557
+ }
3558
+ if (command === "agent") {
3559
+ process.exitCode = await cmdAgent(rest);
3560
+ return;
3561
+ }
3562
+ if (command === "pty") {
3563
+ process.exitCode = await cmdPty(rest);
3564
+ return;
3565
+ }
3566
+ if (command === "run") {
3567
+ process.exitCode = await cmdRun(rest);
3568
+ return;
3569
+ }
3570
+ if (command === "run-all") {
3571
+ process.exitCode = await cmdRunAll(rest);
3572
+ return;
3573
+ }
3574
+ if (command === "script") {
3575
+ process.exitCode = await cmdScript(rest);
3576
+ return;
3577
+ }
3578
+ throw new Error(`unknown command: ${command}\n\n` + usage());
3579
+ }
3580
+ if (import.meta.main) try {
3581
+ await main();
3582
+ } catch (error) {
3583
+ console.error(error.message);
3584
+ process.exitCode = 1;
3585
+ }
3586
+ //#endregion
3587
+ export { main as t };