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
@@ -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
- }