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,209 +0,0 @@
1
- import { mkdirSync, writeFileSync } from "node:fs";
2
- import { join, relative, resolve } from "node:path";
3
-
4
- import type { ExtractedStep, ExtractedLaunch } from "./step_extractor";
5
- import type { Script, ScriptStep } from "../script/schema";
6
-
7
- export type GenerateOptions = {
8
- name: string;
9
- launch?: ExtractedLaunch;
10
- targetCommand?: string;
11
- targetArgs?: string[];
12
- outputDir: string;
13
- format: "json" | "ts" | "both";
14
- cols?: number;
15
- rows?: number;
16
- env?: Record<string, string>;
17
- trace?: {
18
- saveCast?: boolean;
19
- saveReport?: boolean;
20
- };
21
- };
22
-
23
- export type GeneratedScript = {
24
- name: string;
25
- jsonPath?: string;
26
- tsPath?: string;
27
- script: Script;
28
- stepCount: number;
29
- };
30
-
31
- export function generateScript(steps: ExtractedStep[], options: GenerateOptions): GeneratedScript {
32
- const script = buildScript(steps, options);
33
-
34
- mkdirSync(options.outputDir, { recursive: true });
35
-
36
- let jsonPath: string | undefined;
37
- let tsPath: string | undefined;
38
-
39
- if (options.format === "json" || options.format === "both") {
40
- jsonPath = join(options.outputDir, `${options.name}.json`);
41
- const jsonContent = generateJsonScript(script, {
42
- schemaPath: resolveJsonSchemaPath(options.outputDir),
43
- });
44
- writeFileSync(jsonPath, jsonContent, "utf8");
45
- }
46
-
47
- if (options.format === "ts" || options.format === "both") {
48
- tsPath = join(options.outputDir, `${options.name}.ts`);
49
- const tsContent = generateTypeScriptScript(script);
50
- writeFileSync(tsPath, tsContent, "utf8");
51
- }
52
-
53
- return {
54
- name: options.name,
55
- jsonPath,
56
- tsPath,
57
- script,
58
- stepCount: script.steps.length,
59
- };
60
- }
61
-
62
- function buildScript(steps: ExtractedStep[], options: GenerateOptions): Script {
63
- const launch = resolveLaunch(options);
64
- const scriptSteps = steps.map(convertToScriptStep);
65
-
66
- // Add final snapshot step if not present
67
- const hasSnapshot = scriptSteps.some(
68
- (s) => s.type === "snapshot" || s.type === "expect" || s.type === "expectGolden",
69
- );
70
- if (!hasSnapshot && scriptSteps.length > 0) {
71
- scriptSteps.push({
72
- type: "snapshot",
73
- kind: "view",
74
- scope: "visible",
75
- trimRight: true,
76
- trimBottom: true,
77
- });
78
- }
79
-
80
- return {
81
- name: options.name,
82
- launch,
83
- trace: options.trace ?? {
84
- saveCast: true,
85
- saveReport: true,
86
- },
87
- steps: scriptSteps,
88
- };
89
- }
90
-
91
- function resolveLaunch(options: GenerateOptions): Script["launch"] {
92
- if (options.targetCommand) {
93
- return {
94
- command: options.targetCommand,
95
- args: options.targetArgs,
96
- cols: options.cols ?? 80,
97
- rows: options.rows ?? 24,
98
- env: options.env,
99
- };
100
- }
101
-
102
- if (options.launch) {
103
- return {
104
- command: options.launch.command,
105
- args: options.launch.args,
106
- cwd: options.launch.cwd,
107
- cols: options.cols ?? 80,
108
- rows: options.rows ?? 24,
109
- env: options.env ?? options.launch.env,
110
- };
111
- }
112
-
113
- // Default to bash for interactive testing
114
- return {
115
- command: "bash",
116
- cols: options.cols ?? 80,
117
- rows: options.rows ?? 24,
118
- };
119
- }
120
-
121
- function convertToScriptStep(extracted: ExtractedStep): ScriptStep {
122
- const { type, params } = extracted;
123
-
124
- switch (type) {
125
- case "sendText":
126
- return {
127
- type: "sendText",
128
- text: typeof params.text === "string" ? params.text : "",
129
- enter: params.enter as boolean | undefined,
130
- };
131
-
132
- case "pressKey":
133
- return {
134
- type: "pressKey",
135
- key: typeof params.key === "string" ? params.key : "Enter",
136
- };
137
-
138
- case "waitForText":
139
- return {
140
- type: "waitForText",
141
- text: params.text as string | undefined,
142
- regex: params.regex as string | undefined,
143
- scope: (params.scope as "visible" | "buffer") ?? "visible",
144
- timeoutMs: (params.timeoutMs as number) ?? 10000,
145
- };
146
-
147
- case "waitForStableScreen":
148
- return {
149
- type: "waitForStableScreen",
150
- timeoutMs: (params.timeoutMs as number) ?? 5000,
151
- quietMs: (params.quietMs as number) ?? 300,
152
- };
153
-
154
- case "assert":
155
- return {
156
- type: "assert",
157
- text: params.text as string | undefined,
158
- regex: params.regex as string | undefined,
159
- description: params.description as string | undefined,
160
- };
161
-
162
- case "sleep":
163
- return {
164
- type: "sleep",
165
- ms: (params.ms as number) ?? 1000,
166
- };
167
-
168
- case "snapshot":
169
- return {
170
- type: "snapshot",
171
- kind: (params.kind as "text" | "view" | "ansi" | "view_ansi" | "grid") ?? "view",
172
- scope: (params.scope as "visible" | "buffer") ?? "visible",
173
- trimRight: true,
174
- trimBottom: true,
175
- };
176
-
177
- default:
178
- // Fallback to sendText for unknown types
179
- return {
180
- type: "sendText",
181
- text: typeof params.text === "string" ? params.text : (extracted.rawText ?? ""),
182
- enter: true,
183
- };
184
- }
185
- }
186
-
187
- export function generateJsonScript(script: Script, options?: { schemaPath?: string }): string {
188
- const output = {
189
- $schema: options?.schemaPath ?? "../schemas/ptywright-script.schema.json",
190
- ...script,
191
- };
192
-
193
- return JSON.stringify(output, null, 2) + "\n";
194
- }
195
-
196
- export function generateTypeScriptScript(script: Script): string {
197
- return `export default ${JSON.stringify(script, null, 2)};\n`;
198
- }
199
-
200
- function resolveJsonSchemaPath(outputDir: string): string {
201
- const absOutputDir = resolve(process.cwd(), outputDir);
202
- const absSchemaPath = resolve(process.cwd(), "schemas", "ptywright-script.schema.json");
203
-
204
- let schemaPath = relative(absOutputDir, absSchemaPath);
205
- if (!schemaPath.startsWith(".")) {
206
- schemaPath = `./${schemaPath}`;
207
- }
208
- return schemaPath.replaceAll("\\", "/");
209
- }
@@ -1,397 +0,0 @@
1
- import type { CodeBlock, ParsedDocument } from "./doc_parser";
2
- import type { ScriptStep } from "../script/schema";
3
-
4
- export type ExtractedStep = {
5
- type: ScriptStep["type"];
6
- params: Record<string, unknown>;
7
- source: "code_block" | "text_step" | "inferred";
8
- confidence: "high" | "medium" | "low";
9
- rawText?: string;
10
- };
11
-
12
- export type ExtractedLaunch = {
13
- command: string;
14
- args?: string[];
15
- cwd?: string;
16
- env?: Record<string, string>;
17
- confidence: "high" | "medium" | "low";
18
- };
19
-
20
- export type ExtractionResult = {
21
- launch?: ExtractedLaunch;
22
- steps: ExtractedStep[];
23
- warnings: string[];
24
- };
25
-
26
- export function extractSteps(doc: ParsedDocument): ExtractionResult {
27
- const warnings: string[] = [];
28
- const steps: ExtractedStep[] = [];
29
- let launch: ExtractedLaunch | undefined;
30
-
31
- // Priority 1: Extract from shell/bash code blocks
32
- for (const block of doc.codeBlocks) {
33
- if (isShellLanguage(block.language)) {
34
- const extracted = extractFromShellBlock(block);
35
- if (!launch && extracted.launch) {
36
- launch = extracted.launch;
37
- }
38
- steps.push(...extracted.steps);
39
- }
40
- }
41
-
42
- // Priority 2: Extract from text steps if no code blocks found
43
- if (steps.length === 0 && doc.steps.length > 0) {
44
- for (const textStep of doc.steps) {
45
- const extracted = extractFromTextStep(textStep);
46
- if (extracted) {
47
- steps.push(extracted);
48
- }
49
- }
50
- }
51
-
52
- // Priority 3: Try to extract from any code blocks
53
- if (steps.length === 0) {
54
- for (const block of doc.codeBlocks) {
55
- if (!isShellLanguage(block.language)) {
56
- const extracted = extractFromGenericCodeBlock(block);
57
- steps.push(...extracted);
58
- }
59
- }
60
- }
61
-
62
- // Add default waits between input steps
63
- const stepsWithWaits = insertDefaultWaits(steps);
64
-
65
- // Validate and add warnings
66
- if (!launch && stepsWithWaits.length > 0) {
67
- warnings.push("No launch command detected. You may need to specify targetCommand.");
68
- }
69
-
70
- if (stepsWithWaits.length === 0) {
71
- warnings.push("No test steps could be extracted from the document.");
72
- }
73
-
74
- return {
75
- launch,
76
- steps: stepsWithWaits,
77
- warnings,
78
- };
79
- }
80
-
81
- function isShellLanguage(lang: string): boolean {
82
- const shellLangs = [
83
- "bash",
84
- "sh",
85
- "shell",
86
- "zsh",
87
- "fish",
88
- "console",
89
- "terminal",
90
- "cmd",
91
- "powershell",
92
- ];
93
- return shellLangs.includes(lang.toLowerCase());
94
- }
95
-
96
- function extractFromShellBlock(block: CodeBlock): {
97
- launch?: ExtractedLaunch;
98
- steps: ExtractedStep[];
99
- } {
100
- const lines = block.code.split("\n").filter((l) => l.trim());
101
- const steps: ExtractedStep[] = [];
102
- let launch: ExtractedLaunch | undefined;
103
-
104
- for (const line of lines) {
105
- const trimmed = line.trim();
106
-
107
- // Skip comments
108
- if (trimmed.startsWith("#") || trimmed.startsWith("//")) {
109
- continue;
110
- }
111
-
112
- // Remove shell prompt prefixes
113
- const command = trimmed
114
- .replace(/^\$\s+/, "")
115
- .replace(/^>\s+/, "")
116
- .replace(/^%\s+/, "");
117
-
118
- if (!command) continue;
119
-
120
- // Check if this is a launch command (first meaningful command)
121
- if (!launch && isLaunchCandidate(command)) {
122
- launch = parseLaunchCommand(command);
123
- continue;
124
- }
125
-
126
- // Parse as input step
127
- const step = parseCommandAsStep(command);
128
- if (step) {
129
- steps.push(step);
130
- }
131
- }
132
-
133
- return { launch, steps };
134
- }
135
-
136
- function extractFromTextStep(text: string): ExtractedStep | null {
137
- const normalized = text.toLowerCase();
138
-
139
- // Pattern: "type X" or "enter X"
140
- const typeMatch = text.match(/^(?:type|enter|input)\s+["`']?(.+?)["`']?$/i);
141
- if (typeMatch && typeMatch[1]) {
142
- return {
143
- type: "sendText",
144
- params: { text: typeMatch[1], enter: true },
145
- source: "text_step",
146
- confidence: "medium",
147
- rawText: text,
148
- };
149
- }
150
-
151
- // Pattern: "press X"
152
- const pressMatch = text.match(/^press\s+(.+)$/i);
153
- if (pressMatch && pressMatch[1]) {
154
- return {
155
- type: "pressKey",
156
- params: { key: normalizeKeyName(pressMatch[1]) },
157
- source: "text_step",
158
- confidence: "medium",
159
- rawText: text,
160
- };
161
- }
162
-
163
- // Pattern: "wait for X"
164
- const waitMatch = text.match(/^wait\s+(?:for\s+)?["`']?(.+?)["`']?$/i);
165
- if (waitMatch && waitMatch[1]) {
166
- return {
167
- type: "waitForText",
168
- params: { text: waitMatch[1], timeoutMs: 10000 },
169
- source: "text_step",
170
- confidence: "medium",
171
- rawText: text,
172
- };
173
- }
174
-
175
- // Pattern: "check/verify/assert X"
176
- const assertMatch = text.match(
177
- /^(?:check|verify|assert|expect)\s+(?:that\s+)?["`']?(.+?)["`']?$/i,
178
- );
179
- if (assertMatch && assertMatch[1]) {
180
- return {
181
- type: "assert",
182
- params: { text: assertMatch[1], description: text },
183
- source: "text_step",
184
- confidence: "medium",
185
- rawText: text,
186
- };
187
- }
188
-
189
- // Pattern: "run X" or "execute X"
190
- const runMatch = text.match(/^(?:run|execute)\s+(.+)$/i);
191
- if (runMatch && runMatch[1]) {
192
- return {
193
- type: "sendText",
194
- params: { text: runMatch[1], enter: true },
195
- source: "text_step",
196
- confidence: "low",
197
- rawText: text,
198
- };
199
- }
200
-
201
- // Chinese patterns
202
- if (/^输入\s+/.test(text)) {
203
- const input = text.replace(/^输入\s+/, "").trim();
204
- return {
205
- type: "sendText",
206
- params: { text: input, enter: true },
207
- source: "text_step",
208
- confidence: "medium",
209
- rawText: text,
210
- };
211
- }
212
-
213
- if (/^等待\s+/.test(text)) {
214
- const target = text.replace(/^等待\s+/, "").trim();
215
- return {
216
- type: "waitForText",
217
- params: { text: target, timeoutMs: 10000 },
218
- source: "text_step",
219
- confidence: "medium",
220
- rawText: text,
221
- };
222
- }
223
-
224
- // Fallback: treat as sendText if it looks like a command
225
- if (/^[a-z_][a-z0-9_-]*\s/i.test(normalized) || text.startsWith("./")) {
226
- return {
227
- type: "sendText",
228
- params: { text: text.trim(), enter: true },
229
- source: "text_step",
230
- confidence: "low",
231
- rawText: text,
232
- };
233
- }
234
-
235
- return null;
236
- }
237
-
238
- function extractFromGenericCodeBlock(block: CodeBlock): ExtractedStep[] {
239
- const steps: ExtractedStep[] = [];
240
- const lines = block.code.split("\n").filter((l) => l.trim());
241
-
242
- for (const line of lines) {
243
- // Only extract if it looks like a command
244
- if (/^[a-z_][a-z0-9_-]*(\s|$)/i.test(line.trim())) {
245
- steps.push({
246
- type: "sendText",
247
- params: { text: line.trim(), enter: true },
248
- source: "code_block",
249
- confidence: "low",
250
- rawText: line,
251
- });
252
- }
253
- }
254
-
255
- return steps;
256
- }
257
-
258
- function isLaunchCandidate(command: string): boolean {
259
- const launchPatterns = [
260
- /^(node|bun|deno|python|python3|ruby|perl)\s+/i,
261
- /^(npm|yarn|pnpm|bun)\s+(run|start|test)/i,
262
- /^(cargo|go|rust)\s+run/i,
263
- /^\.\//,
264
- /^[a-z_][a-z0-9_-]*$/i, // Single command name
265
- ];
266
-
267
- return launchPatterns.some((p) => p.test(command));
268
- }
269
-
270
- function parseLaunchCommand(command: string): ExtractedLaunch {
271
- const parts = parseCommandLine(command);
272
- const [cmd, ...args] = parts;
273
-
274
- return {
275
- command: cmd ?? command,
276
- args: args.length > 0 ? args : undefined,
277
- confidence: "high",
278
- };
279
- }
280
-
281
- function parseCommandLine(command: string): string[] {
282
- const parts: string[] = [];
283
- let current = "";
284
- let inQuote: string | null = null;
285
- let escape = false;
286
-
287
- for (const char of command) {
288
- if (escape) {
289
- current += char;
290
- escape = false;
291
- continue;
292
- }
293
-
294
- if (char === "\\") {
295
- escape = true;
296
- continue;
297
- }
298
-
299
- if ((char === '"' || char === "'") && !inQuote) {
300
- inQuote = char;
301
- continue;
302
- }
303
-
304
- if (char === inQuote) {
305
- inQuote = null;
306
- continue;
307
- }
308
-
309
- if (char === " " && !inQuote) {
310
- if (current) {
311
- parts.push(current);
312
- current = "";
313
- }
314
- continue;
315
- }
316
-
317
- current += char;
318
- }
319
-
320
- if (current) {
321
- parts.push(current);
322
- }
323
-
324
- return parts;
325
- }
326
-
327
- function parseCommandAsStep(command: string): ExtractedStep | null {
328
- // Skip if it looks like output, not input
329
- if (/^(error|warning|info|debug|note):/i.test(command)) {
330
- return null;
331
- }
332
-
333
- return {
334
- type: "sendText",
335
- params: { text: command, enter: true },
336
- source: "code_block",
337
- confidence: "high",
338
- rawText: command,
339
- };
340
- }
341
-
342
- function normalizeKeyName(key: string): string {
343
- const keyMap: Record<string, string> = {
344
- enter: "Enter",
345
- return: "Enter",
346
- tab: "Tab",
347
- escape: "Escape",
348
- esc: "Escape",
349
- space: "Space",
350
- backspace: "Backspace",
351
- delete: "Delete",
352
- up: "ArrowUp",
353
- down: "ArrowDown",
354
- left: "ArrowLeft",
355
- right: "ArrowRight",
356
- home: "Home",
357
- end: "End",
358
- pageup: "PageUp",
359
- pagedown: "PageDown",
360
- "ctrl+c": "Ctrl+C",
361
- "ctrl+d": "Ctrl+D",
362
- "ctrl+z": "Ctrl+Z",
363
- };
364
-
365
- const normalized = key.toLowerCase().trim();
366
- return keyMap[normalized] ?? key;
367
- }
368
-
369
- function insertDefaultWaits(steps: ExtractedStep[]): ExtractedStep[] {
370
- const result: ExtractedStep[] = [];
371
-
372
- for (let i = 0; i < steps.length; i++) {
373
- const step = steps[i];
374
- if (!step) continue;
375
-
376
- result.push(step);
377
-
378
- // Add waitForStableScreen after input steps (unless next step is already a wait)
379
- const nextStep = steps[i + 1];
380
- const isInputStep = step.type === "sendText" || step.type === "pressKey";
381
- const nextIsWait =
382
- nextStep?.type === "waitForText" ||
383
- nextStep?.type === "waitForStableScreen" ||
384
- nextStep?.type === "sleep";
385
-
386
- if (isInputStep && !nextIsWait && i < steps.length - 1) {
387
- result.push({
388
- type: "waitForStableScreen",
389
- params: { timeoutMs: 5000, quietMs: 300 },
390
- source: "inferred",
391
- confidence: "low",
392
- });
393
- }
394
- }
395
-
396
- return result;
397
- }