ptywright 0.1.1 → 0.3.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 +318 -1
  2. package/dist/agent.mjs +2 -0
  3. package/dist/bin/ptywright.mjs +6 -0
  4. package/dist/cli-CfvlbRoZ.mjs +3585 -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-zApMYWZx.mjs +3257 -0
  11. package/dist/runner-zi0nItvB.mjs +1874 -0
  12. package/dist/script.mjs +2 -0
  13. package/dist/server-BC3yo-dq.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 +166 -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
@@ -1,174 +0,0 @@
1
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2
-
3
- import type { PtywrightCapability } from "./server";
4
- import { createPtywrightServer } from "./server";
5
-
6
- export type PtywrightHttpServerOptions = {
7
- hostname?: string;
8
- port?: number;
9
- capabilities?: PtywrightCapability[];
10
-
11
- /**
12
- * If the request includes an Origin header, it must match one of these values.
13
- * Use ["*"] to allow any Origin (NOT recommended).
14
- */
15
- allowedOrigins?: string[];
16
-
17
- /**
18
- * Add CORS headers for browser-based clients.
19
- * If false, this server is intended for non-browser clients only.
20
- */
21
- cors?: boolean;
22
- };
23
-
24
- export type PtywrightHttpServerHandle = {
25
- url: string;
26
- hostname: string;
27
- port: number;
28
- close: () => Promise<void>;
29
- };
30
-
31
- function parseAllowedOrigins(value: string | undefined): string[] | undefined {
32
- if (!value?.trim()) return undefined;
33
- return value
34
- .split(/[\s,]+/g)
35
- .map((v) => v.trim())
36
- .filter(Boolean);
37
- }
38
-
39
- function isOriginAllowed(origin: string, allowed: string[]): boolean {
40
- if (allowed.includes("*")) return true;
41
- return allowed.includes(origin);
42
- }
43
-
44
- function withCorsHeaders(
45
- init: ResponseInit,
46
- origin: string | null,
47
- allowed: string[],
48
- ): ResponseInit {
49
- if (!origin) return init;
50
- if (!isOriginAllowed(origin, allowed)) return init;
51
-
52
- const headers = new Headers(init.headers);
53
- headers.set("access-control-allow-origin", origin);
54
- headers.set("vary", "origin");
55
- headers.set("access-control-allow-methods", "GET,POST,DELETE,OPTIONS");
56
- headers.set(
57
- "access-control-allow-headers",
58
- "content-type,mcp-session-id,last-event-id,mcp-protocol-version",
59
- );
60
- headers.set("access-control-expose-headers", "mcp-session-id,mcp-protocol-version");
61
-
62
- return { ...init, headers };
63
- }
64
-
65
- export async function startPtywrightHttpServer(
66
- options?: PtywrightHttpServerOptions,
67
- ): Promise<PtywrightHttpServerHandle> {
68
- const hostname = options?.hostname?.trim() ? options.hostname.trim() : "127.0.0.1";
69
- const desiredPort = options?.port ?? 3000;
70
- const cors = options?.cors ?? true;
71
-
72
- const { server, sessions } = createPtywrightServer({
73
- capabilities: options?.capabilities,
74
- });
75
-
76
- const transport = new WebStandardStreamableHTTPServerTransport();
77
- await server.connect(transport);
78
-
79
- let allowedOrigins: string[] =
80
- options?.allowedOrigins ??
81
- parseAllowedOrigins(process.env.PTYWRIGHT_HTTP_ALLOWED_ORIGINS) ??
82
- [];
83
-
84
- const srv = Bun.serve({
85
- hostname,
86
- port: desiredPort,
87
- fetch: async (req: Request) => {
88
- const url = new URL(req.url);
89
- const origin = req.headers.get("origin");
90
-
91
- if (url.pathname === "/health") {
92
- const init = withCorsHeaders(
93
- {
94
- status: 200,
95
- headers: { "content-type": "application/json" },
96
- },
97
- cors ? origin : null,
98
- allowedOrigins,
99
- );
100
- return new Response(JSON.stringify({ status: "ok" }), init);
101
- }
102
-
103
- if (url.pathname !== "/mcp") {
104
- const init = withCorsHeaders(
105
- {
106
- status: 404,
107
- headers: { "content-type": "text/plain; charset=utf-8" },
108
- },
109
- cors ? origin : null,
110
- allowedOrigins,
111
- );
112
- return new Response("not found", init);
113
- }
114
-
115
- // Per MCP Streamable HTTP security guidance: validate Origin (when present).
116
- if (origin && !isOriginAllowed(origin, allowedOrigins)) {
117
- return new Response("forbidden", {
118
- status: 403,
119
- headers: cors
120
- ? {
121
- "content-type": "text/plain; charset=utf-8",
122
- "access-control-allow-origin": origin,
123
- vary: "origin",
124
- }
125
- : { "content-type": "text/plain; charset=utf-8" },
126
- });
127
- }
128
-
129
- if (req.method === "OPTIONS") {
130
- return new Response(null, withCorsHeaders({ status: 204 }, origin, allowedOrigins));
131
- }
132
-
133
- const res = await transport.handleRequest(req);
134
-
135
- if (!cors) return res;
136
-
137
- const init = withCorsHeaders(
138
- {
139
- status: res.status,
140
- statusText: res.statusText,
141
- headers: res.headers,
142
- },
143
- origin,
144
- allowedOrigins,
145
- );
146
- return new Response(res.body, init);
147
- },
148
- });
149
-
150
- const port = srv.port;
151
- if (port === undefined) {
152
- await srv.stop();
153
- sessions.closeAll();
154
- await server.close();
155
- throw new Error("failed to bind HTTP server port");
156
- }
157
-
158
- if (allowedOrigins.length === 0) {
159
- allowedOrigins = [`http://localhost:${port}`, `http://127.0.0.1:${port}`];
160
- }
161
-
162
- const url = `http://${hostname}:${port}/mcp`;
163
-
164
- return {
165
- url,
166
- hostname,
167
- port,
168
- close: async () => {
169
- await srv.stop();
170
- sessions.closeAll();
171
- await server.close();
172
- },
173
- };
174
- }
@@ -1,238 +0,0 @@
1
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
- import { dirname, relative, resolve } from "node:path";
3
-
4
- import { scriptSchema } from "../script/schema";
5
- import type { Script, ScriptStep } from "../script/schema";
6
- import type { TerminalSession } from "../session/terminal_session";
7
- import type { TextMaskRule } from "../terminal/mask";
8
-
9
- export type StartScriptRecordingArgs = {
10
- name: string;
11
- outPath?: string;
12
- goldenDir?: string;
13
- overwrite?: boolean;
14
- checkpoint?: {
15
- scope?: "visible" | "buffer";
16
- trimRight?: boolean;
17
- trimBottom?: boolean;
18
- mask?: TextMaskRule[];
19
- };
20
- };
21
-
22
- export type StopScriptRecordingArgs = {
23
- recordingId: string;
24
- writeFiles?: boolean;
25
- };
26
-
27
- export type ScriptRecordingStatus = {
28
- recordingId: string;
29
- name: string;
30
- outPath: string;
31
- goldenDir: string;
32
- hasLaunch: boolean;
33
- stepCount: number;
34
- checkpointCount: number;
35
- };
36
-
37
- export type StopScriptRecordingResult = {
38
- scriptPath?: string;
39
- goldenPaths: string[];
40
- script: Script & { $schema?: string };
41
- };
42
-
43
- type GoldenWrite = { path: string; text: string };
44
-
45
- type CheckpointConfig = {
46
- scope: "visible" | "buffer";
47
- trimRight: boolean;
48
- trimBottom: boolean;
49
- mask?: TextMaskRule[];
50
- };
51
-
52
- type ScriptRecording = {
53
- id: string;
54
- name: string;
55
- outPath: string;
56
- goldenDir: string;
57
- overwrite: boolean;
58
- checkpoint: CheckpointConfig;
59
- launch: Script["launch"] | null;
60
- sessionId: string | null;
61
- steps: ScriptStep[];
62
- checkpointIndex: number;
63
- goldenWrites: GoldenWrite[];
64
- };
65
-
66
- export class ScriptRecordingManager {
67
- private active: ScriptRecording | null = null;
68
-
69
- start(args: StartScriptRecordingArgs): ScriptRecordingStatus {
70
- if (this.active) {
71
- throw new Error(`recording already active: ${this.active.id}`);
72
- }
73
-
74
- const name = args.name.trim();
75
- if (!name) throw new Error("name is required");
76
-
77
- const outPath = (args.outPath?.trim() ? args.outPath.trim() : `scripts/${name}.json`).trim();
78
- const goldenDir = (
79
- args.goldenDir?.trim() ? args.goldenDir.trim() : `tests/golden/scripts/${name}`
80
- ).trim();
81
-
82
- this.active = {
83
- id: crypto.randomUUID(),
84
- name,
85
- outPath,
86
- goldenDir,
87
- overwrite: args.overwrite ?? false,
88
- checkpoint: {
89
- scope: args.checkpoint?.scope ?? "visible",
90
- trimRight: args.checkpoint?.trimRight ?? true,
91
- trimBottom: args.checkpoint?.trimBottom ?? true,
92
- mask: args.checkpoint?.mask,
93
- },
94
- launch: null,
95
- sessionId: null,
96
- steps: [],
97
- checkpointIndex: 0,
98
- goldenWrites: [],
99
- };
100
-
101
- return this.status();
102
- }
103
-
104
- stop(args: StopScriptRecordingArgs): StopScriptRecordingResult {
105
- if (!this.active) {
106
- throw new Error("no active recording");
107
- }
108
- if (args.recordingId !== this.active.id) {
109
- throw new Error(`recording not found: ${args.recordingId}`);
110
- }
111
-
112
- const writeFiles = args.writeFiles ?? true;
113
- const recording = this.active;
114
- this.active = null;
115
-
116
- if (!recording.launch) {
117
- throw new Error("recording has no launch_session");
118
- }
119
-
120
- const schemaAbs = resolve("schemas/ptywright-script.schema.json");
121
- const schemaRel = toPosixPath(relative(dirname(resolve(recording.outPath)), schemaAbs));
122
-
123
- const built: Script & { $schema?: string } = {
124
- $schema: schemaRel,
125
- name: recording.name,
126
- launch: recording.launch,
127
- steps: recording.steps,
128
- };
129
-
130
- const parsed = scriptSchema.parse(built) as Script;
131
- const script = { ...parsed, $schema: built.$schema };
132
-
133
- const goldenPaths = recording.goldenWrites.map((w) => w.path);
134
-
135
- if (writeFiles) {
136
- writeOrThrow(recording.outPath, `${JSON.stringify(script, null, 2)}\n`, recording.overwrite);
137
-
138
- for (const w of recording.goldenWrites) {
139
- writeOrThrow(w.path, `${w.text}\n`, recording.overwrite);
140
- }
141
- }
142
-
143
- return { scriptPath: writeFiles ? recording.outPath : undefined, goldenPaths, script };
144
- }
145
-
146
- status(): ScriptRecordingStatus {
147
- if (!this.active) {
148
- throw new Error("no active recording");
149
- }
150
- return {
151
- recordingId: this.active.id,
152
- name: this.active.name,
153
- outPath: this.active.outPath,
154
- goldenDir: this.active.goldenDir,
155
- hasLaunch: this.active.launch !== null,
156
- stepCount: this.active.steps.length,
157
- checkpointCount: this.active.checkpointIndex,
158
- };
159
- }
160
-
161
- recordLaunch(args: Script["launch"], sessionId: string): void {
162
- const rec = this.active;
163
- if (!rec) return;
164
-
165
- if (rec.launch) return;
166
- rec.launch = args;
167
- rec.sessionId = sessionId;
168
- }
169
-
170
- recordStep(step: ScriptStep): void {
171
- const rec = this.active;
172
- if (!rec) return;
173
- rec.steps.push(step);
174
- }
175
-
176
- async recordCheckpoint(args: { session: TerminalSession; label?: string }): Promise<void> {
177
- const rec = this.active;
178
- if (!rec) return;
179
- if (!rec.sessionId || rec.sessionId !== args.session.id) return;
180
-
181
- const label = (args.label ?? "").trim();
182
- const safe = sanitizeLabel(label || `checkpoint_${rec.checkpointIndex + 1}`);
183
- rec.checkpointIndex += 1;
184
-
185
- const snapshot = await args.session.snapshotText({
186
- scope: rec.checkpoint.scope,
187
- trimRight: rec.checkpoint.trimRight,
188
- trimBottom: rec.checkpoint.trimBottom,
189
- captureFrame: true,
190
- mask: rec.checkpoint.mask,
191
- });
192
-
193
- const goldenPath = toPosixPath(resolvePathLike(joinPosix(rec.goldenDir, `${safe}.txt`), false));
194
- rec.goldenWrites.push({ path: goldenPath, text: snapshot.text });
195
-
196
- rec.steps.push({
197
- type: "snapshot",
198
- kind: "text",
199
- scope: rec.checkpoint.scope,
200
- trimRight: rec.checkpoint.trimRight,
201
- trimBottom: rec.checkpoint.trimBottom,
202
- mask: rec.checkpoint.mask,
203
- } as Extract<ScriptStep, { type: "snapshot" }>);
204
-
205
- rec.steps.push({
206
- type: "expectGolden",
207
- path: goldenPath,
208
- } as Extract<ScriptStep, { type: "expectGolden" }>);
209
- }
210
- }
211
-
212
- function writeOrThrow(path: string, text: string, overwrite: boolean): void {
213
- const abs = resolvePathLike(path, true);
214
- if (!overwrite && existsSync(abs)) {
215
- throw new Error(`refusing to overwrite: ${path}`);
216
- }
217
- mkdirSync(dirname(abs), { recursive: true });
218
- writeFileSync(abs, text, "utf8");
219
- }
220
-
221
- function resolvePathLike(path: string, absolute: boolean): string {
222
- if (!absolute) return toPosixPath(path);
223
- return resolve(process.cwd(), path);
224
- }
225
-
226
- function sanitizeLabel(label: string): string {
227
- return label.replace(/[^a-z0-9._-]+/gi, "_").replace(/^_+|_+$/g, "") || "checkpoint";
228
- }
229
-
230
- function toPosixPath(path: string): string {
231
- return path.replace(/\\/g, "/");
232
- }
233
-
234
- function joinPosix(a: string, b: string): string {
235
- const left = a.replace(/\\/g, "/").replace(/\/+$/g, "");
236
- const right = b.replace(/\\/g, "/").replace(/^\/+/g, "");
237
- return `${left}/${right}`;
238
- }