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.
- package/README.md +287 -1
- package/dist/agent.mjs +2 -0
- package/dist/bin/ptywright.mjs +6 -0
- package/dist/cli-DIUx2w6X.mjs +3587 -0
- package/dist/cli.mjs +2 -0
- package/{src/index.ts → dist/index.mjs} +7 -9
- package/dist/mcp.mjs +2 -0
- package/dist/pty-cassette.mjs +24 -0
- package/dist/pty_like-Cpkh_O9B.mjs +404 -0
- package/dist/runner-DzZlFrt1.mjs +1897 -0
- package/dist/runner-zApMYWZx.mjs +3257 -0
- package/dist/script.mjs +2 -0
- package/dist/server-VHuEWWj_.mjs +3068 -0
- package/dist/session.mjs +2 -0
- package/dist/terminal_session-DopC7Xg6.mjs +893 -0
- package/package.json +28 -21
- package/schemas/ptywright-agent-cassette.schema.json +57 -0
- package/schemas/ptywright-agent-check.schema.json +122 -0
- package/schemas/ptywright-agent-manifest.schema.json +107 -0
- package/schemas/ptywright-agent-promote.schema.json +146 -0
- package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
- package/schemas/ptywright-agent-run.schema.json +126 -0
- package/schemas/ptywright-agent.schema.json +182 -0
- package/schemas/ptywright-pty-cassette.schema.json +86 -0
- package/schemas/ptywright-script-manifest.schema.json +75 -0
- package/schemas/ptywright-script-run-summary.schema.json +114 -0
- package/schemas/ptywright-script.schema.json +55 -3
- package/bin/ptywright +0 -4
- package/src/cli.ts +0 -414
- package/src/generator/doc_parser.ts +0 -341
- package/src/generator/generate.ts +0 -161
- package/src/generator/index.ts +0 -10
- package/src/generator/script_generator.ts +0 -209
- package/src/generator/step_extractor.ts +0 -397
- package/src/mcp/http_server.ts +0 -174
- package/src/mcp/script_recording.ts +0 -238
- package/src/mcp/server.ts +0 -1348
- package/src/pty/bun_pty_adapter.ts +0 -34
- package/src/pty/bun_terminal_adapter.ts +0 -149
- package/src/pty/pty_adapter.ts +0 -31
- package/src/script/dsl.ts +0 -188
- package/src/script/module.ts +0 -43
- package/src/script/path.ts +0 -151
- package/src/script/run.ts +0 -108
- package/src/script/run_all.ts +0 -229
- package/src/script/runner.ts +0 -983
- package/src/script/schema.ts +0 -237
- package/src/script/steps/assert_snapshot_equals.ts +0 -21
- package/src/script/steps/index.ts +0 -2
- package/src/script/suite_report.ts +0 -626
- package/src/session/session_manager.ts +0 -145
- package/src/session/terminal_session.ts +0 -473
- package/src/terminal/ansi.ts +0 -142
- package/src/terminal/keys.ts +0 -180
- package/src/terminal/mask.ts +0 -70
- package/src/terminal/mouse.ts +0 -75
- package/src/terminal/snapshot.ts +0 -196
- package/src/terminal/style.ts +0 -121
- package/src/terminal/view.ts +0 -49
- package/src/trace/asciicast.ts +0 -20
- package/src/trace/asciinema_player_assets.ts +0 -44
- package/src/trace/cast_to_txt.ts +0 -116
- package/src/trace/recorder.ts +0 -110
- package/src/trace/report.ts +0 -2092
- package/src/types.ts +0 -86
- package/src/util/hash.ts +0 -8
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
761
|
+
}
|
|
762
|
+
function escapeAttribute(input) {
|
|
763
|
+
return escapeHtml(input).replace(/'/g, "'");
|
|
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 };
|