ptywright 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +287 -1
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-DIUx2w6X.mjs +3587 -0
  5. package/dist/cli.mjs +2 -0
  6. package/{src/index.ts → dist/index.mjs} +7 -9
  7. package/dist/mcp.mjs +2 -0
  8. package/dist/pty-cassette.mjs +24 -0
  9. package/dist/pty_like-Cpkh_O9B.mjs +404 -0
  10. package/dist/runner-DzZlFrt1.mjs +1897 -0
  11. package/dist/runner-zApMYWZx.mjs +3257 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-VHuEWWj_.mjs +3068 -0
  14. package/dist/session.mjs +2 -0
  15. package/dist/terminal_session-DopC7Xg6.mjs +893 -0
  16. package/package.json +28 -21
  17. package/schemas/ptywright-agent-cassette.schema.json +57 -0
  18. package/schemas/ptywright-agent-check.schema.json +122 -0
  19. package/schemas/ptywright-agent-manifest.schema.json +107 -0
  20. package/schemas/ptywright-agent-promote.schema.json +146 -0
  21. package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
  22. package/schemas/ptywright-agent-run.schema.json +126 -0
  23. package/schemas/ptywright-agent.schema.json +182 -0
  24. package/schemas/ptywright-pty-cassette.schema.json +86 -0
  25. package/schemas/ptywright-script-manifest.schema.json +75 -0
  26. package/schemas/ptywright-script-run-summary.schema.json +114 -0
  27. package/schemas/ptywright-script.schema.json +55 -3
  28. package/bin/ptywright +0 -4
  29. package/src/cli.ts +0 -414
  30. package/src/generator/doc_parser.ts +0 -341
  31. package/src/generator/generate.ts +0 -161
  32. package/src/generator/index.ts +0 -10
  33. package/src/generator/script_generator.ts +0 -209
  34. package/src/generator/step_extractor.ts +0 -397
  35. package/src/mcp/http_server.ts +0 -174
  36. package/src/mcp/script_recording.ts +0 -238
  37. package/src/mcp/server.ts +0 -1348
  38. package/src/pty/bun_pty_adapter.ts +0 -34
  39. package/src/pty/bun_terminal_adapter.ts +0 -149
  40. package/src/pty/pty_adapter.ts +0 -31
  41. package/src/script/dsl.ts +0 -188
  42. package/src/script/module.ts +0 -43
  43. package/src/script/path.ts +0 -151
  44. package/src/script/run.ts +0 -108
  45. package/src/script/run_all.ts +0 -229
  46. package/src/script/runner.ts +0 -983
  47. package/src/script/schema.ts +0 -237
  48. package/src/script/steps/assert_snapshot_equals.ts +0 -21
  49. package/src/script/steps/index.ts +0 -2
  50. package/src/script/suite_report.ts +0 -626
  51. package/src/session/session_manager.ts +0 -145
  52. package/src/session/terminal_session.ts +0 -473
  53. package/src/terminal/ansi.ts +0 -142
  54. package/src/terminal/keys.ts +0 -180
  55. package/src/terminal/mask.ts +0 -70
  56. package/src/terminal/mouse.ts +0 -75
  57. package/src/terminal/snapshot.ts +0 -196
  58. package/src/terminal/style.ts +0 -121
  59. package/src/terminal/view.ts +0 -49
  60. package/src/trace/asciicast.ts +0 -20
  61. package/src/trace/asciinema_player_assets.ts +0 -44
  62. package/src/trace/cast_to_txt.ts +0 -116
  63. package/src/trace/recorder.ts +0 -110
  64. package/src/trace/report.ts +0 -2092
  65. package/src/types.ts +0 -86
  66. package/src/util/hash.ts +0 -8
  67. package/src/util/sleep.ts +0 -5
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 };