ptywright 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +459 -116
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-DIUx2w6X.mjs +3587 -0
  5. package/dist/cli.mjs +2 -0
  6. package/{src/index.ts → dist/index.mjs} +7 -9
  7. package/dist/mcp.mjs +2 -0
  8. package/dist/pty-cassette.mjs +24 -0
  9. package/dist/pty_like-Cpkh_O9B.mjs +404 -0
  10. package/dist/runner-DzZlFrt1.mjs +1897 -0
  11. package/dist/runner-zApMYWZx.mjs +3257 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-VHuEWWj_.mjs +3068 -0
  14. package/dist/session.mjs +2 -0
  15. package/dist/terminal_session-DopC7Xg6.mjs +893 -0
  16. package/package.json +28 -21
  17. package/schemas/ptywright-agent-cassette.schema.json +57 -0
  18. package/schemas/ptywright-agent-check.schema.json +122 -0
  19. package/schemas/ptywright-agent-manifest.schema.json +107 -0
  20. package/schemas/ptywright-agent-promote.schema.json +146 -0
  21. package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
  22. package/schemas/ptywright-agent-run.schema.json +126 -0
  23. package/schemas/ptywright-agent.schema.json +182 -0
  24. package/schemas/ptywright-pty-cassette.schema.json +86 -0
  25. package/schemas/ptywright-script-manifest.schema.json +75 -0
  26. package/schemas/ptywright-script-run-summary.schema.json +114 -0
  27. package/schemas/ptywright-script.schema.json +55 -3
  28. package/skills/ptywright-testing/SKILL.md +53 -33
  29. package/bin/ptywright +0 -4
  30. package/src/cli.ts +0 -414
  31. package/src/generator/doc_parser.ts +0 -341
  32. package/src/generator/generate.ts +0 -161
  33. package/src/generator/index.ts +0 -10
  34. package/src/generator/script_generator.ts +0 -209
  35. package/src/generator/step_extractor.ts +0 -397
  36. package/src/mcp/http_server.ts +0 -174
  37. package/src/mcp/script_recording.ts +0 -238
  38. package/src/mcp/server.ts +0 -1348
  39. package/src/pty/bun_pty_adapter.ts +0 -34
  40. package/src/pty/bun_terminal_adapter.ts +0 -149
  41. package/src/pty/pty_adapter.ts +0 -31
  42. package/src/script/dsl.ts +0 -188
  43. package/src/script/module.ts +0 -43
  44. package/src/script/path.ts +0 -151
  45. package/src/script/run.ts +0 -108
  46. package/src/script/run_all.ts +0 -229
  47. package/src/script/runner.ts +0 -983
  48. package/src/script/schema.ts +0 -237
  49. package/src/script/steps/assert_snapshot_equals.ts +0 -21
  50. package/src/script/steps/index.ts +0 -2
  51. package/src/script/suite_report.ts +0 -626
  52. package/src/session/session_manager.ts +0 -145
  53. package/src/session/terminal_session.ts +0 -473
  54. package/src/terminal/ansi.ts +0 -142
  55. package/src/terminal/keys.ts +0 -180
  56. package/src/terminal/mask.ts +0 -70
  57. package/src/terminal/mouse.ts +0 -75
  58. package/src/terminal/snapshot.ts +0 -196
  59. package/src/terminal/style.ts +0 -121
  60. package/src/terminal/view.ts +0 -49
  61. package/src/trace/asciicast.ts +0 -20
  62. package/src/trace/asciinema_player_assets.ts +0 -44
  63. package/src/trace/cast_to_txt.ts +0 -116
  64. package/src/trace/recorder.ts +0 -110
  65. package/src/trace/report.ts +0 -2092
  66. package/src/types.ts +0 -86
  67. package/src/util/hash.ts +0 -8
  68. package/src/util/sleep.ts +0 -5
package/dist/cli.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { t as main } from "./cli-DIUx2w6X.mjs";
2
+ export { main };
@@ -1,16 +1,14 @@
1
+ import { t as createPtywrightServer } from "./server-VHuEWWj_.mjs";
1
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2
-
3
- import { createPtywrightServer } from "./mcp/server";
4
-
3
+ //#region src/index.ts
5
4
  const { server, sessions } = createPtywrightServer();
6
-
7
5
  const transport = new StdioServerTransport();
8
6
  await server.connect(transport);
9
-
10
- function shutdown(): void {
11
- sessions.closeAll();
12
- void server.close();
7
+ function shutdown() {
8
+ sessions.closeAll();
9
+ server.close();
13
10
  }
14
-
15
11
  process.on("SIGINT", shutdown);
16
12
  process.on("SIGTERM", shutdown);
13
+ //#endregion
14
+ export {};
package/dist/mcp.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { t as createPtywrightServer } from "./server-VHuEWWj_.mjs";
2
+ export { createPtywrightServer };
@@ -0,0 +1,24 @@
1
+ import { S as dataToBytes, _ as ptyCassetteSchema, a as inspectPtyCassette, b as byteLength, c as createPtyCassetteReplay, d as PTY_CASSETTE_SCHEMA_URL, f as normalizePtyCassette, g as ptyCassetteResizeEventSchema, h as ptyCassetteExitEventSchema, i as formatPtyCassetteInspectLines, l as readPtyCassettePath, m as ptyCassetteEventSchema, n as PtyCassetteRecorder, o as inspectPtyCassettePath, p as ptyCassetteDataEventSchema, r as createPtyCassetteRecorder, s as PtyCassetteReplay, t as wrapPtyLike, u as writePtyCassettePath, v as validatePtyCassette, x as dataToBase64, y as base64ToBytes } from "./pty_like-Cpkh_O9B.mjs";
2
+ //#region src/pty-cassette/bun_terminal.ts
3
+ function wrapBunTerminalOptions(options, recorder) {
4
+ const onData = options.data;
5
+ const onExit = options.exit;
6
+ return {
7
+ ...options,
8
+ data: (terminal, data) => {
9
+ recorder.recordOutput(data);
10
+ onData?.(terminal, data);
11
+ },
12
+ exit: onExit ? (terminal) => onExit(terminal) : void 0
13
+ };
14
+ }
15
+ function writeBunTerminalRecorded(terminal, recorder, data) {
16
+ recorder.recordInput(data);
17
+ terminal.write(data);
18
+ }
19
+ function resizeBunTerminalRecorded(terminal, recorder, cols, rows) {
20
+ recorder.recordResize(cols, rows);
21
+ terminal.resize(cols, rows);
22
+ }
23
+ //#endregion
24
+ export { PTY_CASSETTE_SCHEMA_URL, PtyCassetteRecorder, PtyCassetteReplay, base64ToBytes, byteLength, createPtyCassetteRecorder, createPtyCassetteReplay, dataToBase64, dataToBytes, formatPtyCassetteInspectLines, inspectPtyCassette, inspectPtyCassettePath, normalizePtyCassette, ptyCassetteDataEventSchema, ptyCassetteEventSchema, ptyCassetteExitEventSchema, ptyCassetteResizeEventSchema, ptyCassetteSchema, readPtyCassettePath, resizeBunTerminalRecorded, validatePtyCassette, wrapBunTerminalOptions, wrapPtyLike, writeBunTerminalRecorded, writePtyCassettePath };
@@ -0,0 +1,404 @@
1
+ import { z } from "zod";
2
+ import { dirname } from "node:path";
3
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { Buffer } from "node:buffer";
5
+ //#region src/pty-cassette/data.ts
6
+ function dataToBytes(data) {
7
+ if (typeof data === "string") return Buffer.from(data, "utf8");
8
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
9
+ return data;
10
+ }
11
+ function dataToBase64(data) {
12
+ return Buffer.from(dataToBytes(data)).toString("base64");
13
+ }
14
+ function base64ToBytes(dataBase64) {
15
+ return Buffer.from(dataBase64, "base64");
16
+ }
17
+ function byteLength(data) {
18
+ return dataToBytes(data).byteLength;
19
+ }
20
+ //#endregion
21
+ //#region src/pty-cassette/schema.ts
22
+ const PTY_CASSETTE_SCHEMA_URL = "https://ptywright.local/schemas/ptywright-pty-cassette.schema.json";
23
+ const base64Schema = z.string().refine((value) => value.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(value), { message: "expected base64-encoded data" });
24
+ const ptyCassetteDataEventSchema = z.object({
25
+ atMs: z.number().int().nonnegative(),
26
+ type: z.enum(["output", "input"]),
27
+ dataBase64: base64Schema
28
+ });
29
+ const ptyCassetteResizeEventSchema = z.object({
30
+ atMs: z.number().int().nonnegative(),
31
+ type: z.literal("resize"),
32
+ cols: z.number().int().positive(),
33
+ rows: z.number().int().positive()
34
+ });
35
+ const ptyCassetteExitEventSchema = z.object({
36
+ atMs: z.number().int().nonnegative(),
37
+ type: z.literal("exit"),
38
+ exitCode: z.number().int(),
39
+ signal: z.union([
40
+ z.number().int(),
41
+ z.string(),
42
+ z.null()
43
+ ]).optional()
44
+ });
45
+ const ptyCassetteEventSchema = z.union([
46
+ ptyCassetteDataEventSchema,
47
+ ptyCassetteResizeEventSchema,
48
+ ptyCassetteExitEventSchema
49
+ ]);
50
+ const ptyCassetteSchema = z.object({
51
+ $schema: z.string().optional(),
52
+ version: z.literal(1),
53
+ createdAt: z.string().min(1),
54
+ durationMs: z.number().int().nonnegative(),
55
+ terminal: z.object({
56
+ cols: z.number().int().positive(),
57
+ rows: z.number().int().positive(),
58
+ term: z.string().min(1).optional()
59
+ }),
60
+ command: z.object({
61
+ file: z.string().min(1),
62
+ args: z.array(z.string()).optional(),
63
+ cwd: z.string().optional(),
64
+ env: z.record(z.string()).optional()
65
+ }).optional(),
66
+ metadata: z.record(z.union([
67
+ z.string(),
68
+ z.number(),
69
+ z.boolean(),
70
+ z.null()
71
+ ])).optional(),
72
+ events: z.array(ptyCassetteEventSchema)
73
+ }).superRefine((value, ctx) => {
74
+ let last = -1;
75
+ for (let i = 0; i < value.events.length; i += 1) {
76
+ const event = value.events[i];
77
+ if (event.atMs < last) ctx.addIssue({
78
+ code: z.ZodIssueCode.custom,
79
+ path: [
80
+ "events",
81
+ i,
82
+ "atMs"
83
+ ],
84
+ message: "events must be ordered by atMs"
85
+ });
86
+ last = event.atMs;
87
+ }
88
+ });
89
+ function normalizePtyCassette(input) {
90
+ return ptyCassetteSchema.parse(input);
91
+ }
92
+ function validatePtyCassette(input) {
93
+ const result = ptyCassetteSchema.safeParse(input);
94
+ if (result.success) return {
95
+ ok: true,
96
+ cassette: result.data
97
+ };
98
+ return {
99
+ ok: false,
100
+ errors: result.error.issues.map((issue) => {
101
+ return `${issue.path.length ? `${issue.path.join(".")}: ` : ""}${issue.message}`;
102
+ })
103
+ };
104
+ }
105
+ //#endregion
106
+ //#region src/pty-cassette/io.ts
107
+ function readPtyCassettePath(path) {
108
+ return normalizePtyCassette(JSON.parse(readFileSync(path, "utf8")));
109
+ }
110
+ function writePtyCassettePath(path, cassette) {
111
+ mkdirSync(dirname(path), { recursive: true });
112
+ writeFileSync(path, JSON.stringify(normalizePtyCassette(cassette), null, 2) + "\n", "utf8");
113
+ return path;
114
+ }
115
+ //#endregion
116
+ //#region src/pty-cassette/replay.ts
117
+ var PtyCassetteReplay = class {
118
+ cassette;
119
+ speed;
120
+ outputListeners = /* @__PURE__ */ new Set();
121
+ dataListeners = /* @__PURE__ */ new Set();
122
+ inputListeners = /* @__PURE__ */ new Set();
123
+ resizeListeners = /* @__PURE__ */ new Set();
124
+ exitListeners = /* @__PURE__ */ new Set();
125
+ stopped = false;
126
+ started = false;
127
+ constructor(cassette, options = {}) {
128
+ this.cassette = cassette;
129
+ this.speed = Math.max(0, options.speed ?? 0);
130
+ }
131
+ onOutput(listener) {
132
+ this.outputListeners.add(listener);
133
+ return disposable(this.outputListeners, listener);
134
+ }
135
+ onData(listener) {
136
+ this.dataListeners.add(listener);
137
+ return disposable(this.dataListeners, listener);
138
+ }
139
+ onInput(listener) {
140
+ this.inputListeners.add(listener);
141
+ return disposable(this.inputListeners, listener);
142
+ }
143
+ onResize(listener) {
144
+ this.resizeListeners.add(listener);
145
+ return disposable(this.resizeListeners, listener);
146
+ }
147
+ onExit(listener) {
148
+ this.exitListeners.add(listener);
149
+ return disposable(this.exitListeners, listener);
150
+ }
151
+ stop() {
152
+ this.stopped = true;
153
+ }
154
+ async start() {
155
+ if (this.started) throw new Error("pty cassette replay already started");
156
+ this.started = true;
157
+ const outputDecoder = new TextDecoder();
158
+ const inputDecoder = new TextDecoder();
159
+ let lastAtMs = 0;
160
+ for (const event of this.cassette.events) {
161
+ if (this.stopped) break;
162
+ if (this.speed > 0) {
163
+ const delayMs = Math.max(0, event.atMs - lastAtMs) / this.speed;
164
+ if (delayMs > 0) await sleep(delayMs);
165
+ }
166
+ lastAtMs = event.atMs;
167
+ if (event.type === "output") {
168
+ const data = decodeReplayData(event, outputDecoder);
169
+ for (const listener of this.outputListeners) listener(data);
170
+ if (data.text) for (const listener of this.dataListeners) listener(data.text);
171
+ continue;
172
+ }
173
+ if (event.type === "input") {
174
+ const data = decodeReplayData(event, inputDecoder);
175
+ for (const listener of this.inputListeners) listener(data);
176
+ continue;
177
+ }
178
+ if (event.type === "resize") {
179
+ for (const listener of this.resizeListeners) listener(event);
180
+ continue;
181
+ }
182
+ for (const listener of this.exitListeners) listener(event);
183
+ }
184
+ const tail = outputDecoder.decode();
185
+ if (tail) for (const listener of this.dataListeners) listener(tail);
186
+ }
187
+ };
188
+ function createPtyCassetteReplay(cassetteOrPath, options) {
189
+ return new PtyCassetteReplay(typeof cassetteOrPath === "string" ? readPtyCassettePath(cassetteOrPath) : cassetteOrPath, options);
190
+ }
191
+ function decodeReplayData(event, decoder) {
192
+ const bytes = base64ToBytes(event.dataBase64);
193
+ return {
194
+ event,
195
+ bytes,
196
+ text: decoder.decode(bytes, { stream: true })
197
+ };
198
+ }
199
+ function disposable(set, listener) {
200
+ return { dispose: () => {
201
+ set.delete(listener);
202
+ } };
203
+ }
204
+ async function sleep(ms) {
205
+ await new Promise((resolve) => setTimeout(resolve, ms));
206
+ }
207
+ //#endregion
208
+ //#region src/pty-cassette/inspect.ts
209
+ function inspectPtyCassette(cassette, path) {
210
+ let outputCount = 0;
211
+ let inputCount = 0;
212
+ let resizeCount = 0;
213
+ let exitCount = 0;
214
+ let outputBytes = 0;
215
+ let inputBytes = 0;
216
+ for (const event of cassette.events) if (event.type === "output") {
217
+ outputCount += 1;
218
+ outputBytes += base64ToBytes(event.dataBase64).byteLength;
219
+ } else if (event.type === "input") {
220
+ inputCount += 1;
221
+ inputBytes += base64ToBytes(event.dataBase64).byteLength;
222
+ } else if (event.type === "resize") resizeCount += 1;
223
+ else exitCount += 1;
224
+ return {
225
+ version: cassette.version,
226
+ path,
227
+ createdAt: cassette.createdAt,
228
+ durationMs: cassette.durationMs,
229
+ terminal: cassette.terminal,
230
+ command: cassette.command,
231
+ eventCount: cassette.events.length,
232
+ outputCount,
233
+ inputCount,
234
+ resizeCount,
235
+ exitCount,
236
+ outputBytes,
237
+ inputBytes
238
+ };
239
+ }
240
+ function inspectPtyCassettePath(path) {
241
+ return inspectPtyCassette(readPtyCassettePath(path), path);
242
+ }
243
+ function formatPtyCassetteInspectLines(result) {
244
+ const command = result.command ? [result.command.file, ...result.command.args ?? []].join(" ") : null;
245
+ return [
246
+ "ok pty-cassette",
247
+ result.path ? `path=${result.path}` : "",
248
+ `version=${result.version}`,
249
+ `createdAt=${result.createdAt}`,
250
+ `durationMs=${result.durationMs}`,
251
+ `terminal=${result.terminal.cols}x${result.terminal.rows}`,
252
+ result.terminal.term ? `term=${result.terminal.term}` : "",
253
+ command ? `command=${command}` : "",
254
+ `events=${result.eventCount}`,
255
+ `output=${result.outputCount} chunks/${result.outputBytes} bytes`,
256
+ `input=${result.inputCount} chunks/${result.inputBytes} bytes`,
257
+ `resize=${result.resizeCount}`,
258
+ `exit=${result.exitCount}`
259
+ ].filter(Boolean);
260
+ }
261
+ //#endregion
262
+ //#region src/pty-cassette/recorder.ts
263
+ var PtyCassetteRecorder = class {
264
+ startedAtMs = performance.now();
265
+ cassette;
266
+ constructor(options) {
267
+ this.cassette = {
268
+ $schema: PTY_CASSETTE_SCHEMA_URL,
269
+ version: 1,
270
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
271
+ durationMs: 0,
272
+ terminal: options.terminal,
273
+ command: options.command,
274
+ metadata: options.metadata,
275
+ events: []
276
+ };
277
+ }
278
+ recordOutput(data) {
279
+ this.recordData("output", data);
280
+ }
281
+ recordInput(data) {
282
+ this.recordData("input", data);
283
+ }
284
+ recordResize(cols, rows) {
285
+ this.pushEvent({
286
+ atMs: this.nowMs(),
287
+ type: "resize",
288
+ cols,
289
+ rows
290
+ });
291
+ }
292
+ recordExit(event) {
293
+ this.pushEvent({
294
+ atMs: this.nowMs(),
295
+ type: "exit",
296
+ exitCode: event.exitCode,
297
+ signal: event.signal ?? null
298
+ });
299
+ this.cassette.durationMs = this.nowMs();
300
+ }
301
+ snapshot() {
302
+ this.cassette.durationMs = this.nowMs();
303
+ return normalizePtyCassette({
304
+ ...this.cassette,
305
+ terminal: { ...this.cassette.terminal },
306
+ command: this.cassette.command ? {
307
+ ...this.cassette.command,
308
+ args: this.cassette.command.args ? [...this.cassette.command.args] : void 0,
309
+ env: this.cassette.command.env ? { ...this.cassette.command.env } : void 0
310
+ } : void 0,
311
+ metadata: this.cassette.metadata ? { ...this.cassette.metadata } : void 0,
312
+ events: this.cassette.events.map((event) => ({ ...event }))
313
+ });
314
+ }
315
+ stop() {
316
+ return this.snapshot();
317
+ }
318
+ writePath(path) {
319
+ return writePtyCassettePath(path, this.snapshot());
320
+ }
321
+ recordData(type, data) {
322
+ if (byteLength(data) === 0) return;
323
+ this.pushEvent({
324
+ atMs: this.nowMs(),
325
+ type,
326
+ dataBase64: dataToBase64(data)
327
+ });
328
+ }
329
+ pushEvent(event) {
330
+ this.cassette.events.push(event);
331
+ this.cassette.durationMs = Math.max(this.cassette.durationMs, event.atMs);
332
+ }
333
+ nowMs() {
334
+ return Math.max(0, Math.round(performance.now() - this.startedAtMs));
335
+ }
336
+ };
337
+ function createPtyCassetteRecorder(options) {
338
+ return new PtyCassetteRecorder(options);
339
+ }
340
+ //#endregion
341
+ //#region src/pty-cassette/pty_like.ts
342
+ function wrapPtyLike(pty, options = {}) {
343
+ const recorder = options.recorder ?? createPtyCassetteRecorder({
344
+ terminal: options.terminal ?? {
345
+ cols: 80,
346
+ rows: 24,
347
+ term: "xterm-256color"
348
+ },
349
+ command: options.command,
350
+ metadata: options.metadata
351
+ });
352
+ const path = options.path;
353
+ const autoWriteOnExit = options.autoWriteOnExit ?? Boolean(path);
354
+ const disposables = [];
355
+ let disposed = false;
356
+ disposables.push(pty.onData((data) => recorder.recordOutput(data)));
357
+ disposables.push(pty.onExit((event) => {
358
+ recorder.recordExit(event);
359
+ if (path && autoWriteOnExit) recorder.writePath(path);
360
+ }));
361
+ return {
362
+ pty,
363
+ recorder,
364
+ write(data) {
365
+ recorder.recordInput(data);
366
+ return pty.write(data);
367
+ },
368
+ resize(cols, rows) {
369
+ recorder.recordResize(cols, rows);
370
+ return pty.resize?.(cols, rows);
371
+ },
372
+ kill(signal) {
373
+ return pty.kill?.(signal);
374
+ },
375
+ onData(listener) {
376
+ return pty.onData(listener);
377
+ },
378
+ onExit(listener) {
379
+ return pty.onExit(listener);
380
+ },
381
+ stopRecording() {
382
+ return recorder.stop();
383
+ },
384
+ writeCassette(nextPath = path) {
385
+ if (!nextPath) throw new Error("writeCassette requires a path");
386
+ return recorder.writePath(nextPath);
387
+ },
388
+ dispose() {
389
+ if (disposed) return;
390
+ disposed = true;
391
+ for (const disposable of disposables) dispose(disposable);
392
+ }
393
+ };
394
+ }
395
+ function dispose(disposable) {
396
+ if (!disposable) return;
397
+ if (typeof disposable === "function") {
398
+ disposable();
399
+ return;
400
+ }
401
+ disposable.dispose();
402
+ }
403
+ //#endregion
404
+ export { dataToBytes as S, ptyCassetteSchema as _, inspectPtyCassette as a, byteLength as b, createPtyCassetteReplay as c, PTY_CASSETTE_SCHEMA_URL as d, normalizePtyCassette as f, ptyCassetteResizeEventSchema as g, ptyCassetteExitEventSchema as h, formatPtyCassetteInspectLines as i, readPtyCassettePath as l, ptyCassetteEventSchema as m, PtyCassetteRecorder as n, inspectPtyCassettePath as o, ptyCassetteDataEventSchema as p, createPtyCassetteRecorder as r, PtyCassetteReplay as s, wrapPtyLike as t, writePtyCassettePath as u, validatePtyCassette as v, dataToBase64 as x, base64ToBytes as y };