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,341 +0,0 @@
1
- import { readFileSync, existsSync } from "node:fs";
2
- import { extname } from "node:path";
3
-
4
- export type DocumentSource = {
5
- type: "local" | "url" | "raw";
6
- path?: string;
7
- url?: string;
8
- content?: string;
9
- };
10
-
11
- export type CodeBlock = {
12
- language: string;
13
- code: string;
14
- lineNumber: number;
15
- };
16
-
17
- export type ParsedDocument = {
18
- title?: string;
19
- description?: string;
20
- codeBlocks: CodeBlock[];
21
- steps: string[];
22
- rawContent: string;
23
- format: "markdown" | "html" | "json" | "yaml" | "text";
24
- };
25
-
26
- export async function parseDocument(source: DocumentSource): Promise<ParsedDocument> {
27
- const content = await fetchContent(source);
28
- const format = detectFormat(source, content);
29
-
30
- switch (format) {
31
- case "markdown":
32
- return parseMarkdown(content);
33
- case "json":
34
- return parseJson(content);
35
- case "yaml":
36
- return parseYaml(content);
37
- case "html":
38
- return parseHtml(content);
39
- default:
40
- return parsePlainText(content);
41
- }
42
- }
43
-
44
- async function fetchContent(source: DocumentSource): Promise<string> {
45
- if (source.content) {
46
- return source.content;
47
- }
48
-
49
- if (source.type === "local" && source.path) {
50
- if (!existsSync(source.path)) {
51
- throw new Error(`File not found: ${source.path}`);
52
- }
53
- return readFileSync(source.path, "utf8");
54
- }
55
-
56
- if (source.type === "url" && source.url) {
57
- const response = await fetch(source.url);
58
- if (!response.ok) {
59
- throw new Error(`Failed to fetch URL: ${source.url} (${response.status})`);
60
- }
61
- return response.text();
62
- }
63
-
64
- throw new Error("Invalid document source: must provide path, url, or content");
65
- }
66
-
67
- function detectFormat(
68
- source: DocumentSource,
69
- content: string,
70
- ): "markdown" | "html" | "json" | "yaml" | "text" {
71
- if (source.path) {
72
- const ext = extname(source.path).toLowerCase();
73
- if (ext === ".md" || ext === ".markdown") return "markdown";
74
- if (ext === ".html" || ext === ".htm") return "html";
75
- if (ext === ".json") return "json";
76
- if (ext === ".yaml" || ext === ".yml") return "yaml";
77
- }
78
-
79
- const trimmed = content.trim();
80
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) return "json";
81
- if (trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html")) return "html";
82
- if (/^#\s/.test(trimmed) || /\n##\s/.test(content) || /```/.test(content)) return "markdown";
83
-
84
- return "text";
85
- }
86
-
87
- function parseMarkdown(content: string): ParsedDocument {
88
- const lines = content.split("\n");
89
- const codeBlocks: CodeBlock[] = [];
90
- const steps: string[] = [];
91
- let title: string | undefined;
92
- let description: string | undefined;
93
-
94
- let inCodeBlock = false;
95
- let currentLang = "";
96
- let currentCode: string[] = [];
97
- let codeBlockStart = 0;
98
-
99
- for (let i = 0; i < lines.length; i++) {
100
- const line = lines[i] ?? "";
101
-
102
- // Extract title from first h1
103
- if (!title && /^#\s+(.+)$/.test(line)) {
104
- title = line.replace(/^#\s+/, "").trim();
105
- continue;
106
- }
107
-
108
- // Extract description from first paragraph after title
109
- if (title && !description && !inCodeBlock && line.trim() && !/^[#-]/.test(line)) {
110
- description = line.trim();
111
- }
112
-
113
- // Handle code blocks
114
- if (line.startsWith("```")) {
115
- if (!inCodeBlock) {
116
- inCodeBlock = true;
117
- currentLang = line.slice(3).trim().toLowerCase();
118
- currentCode = [];
119
- codeBlockStart = i + 1;
120
- } else {
121
- inCodeBlock = false;
122
- if (currentCode.length > 0) {
123
- codeBlocks.push({
124
- language: currentLang || "text",
125
- code: currentCode.join("\n"),
126
- lineNumber: codeBlockStart,
127
- });
128
- }
129
- }
130
- continue;
131
- }
132
-
133
- if (inCodeBlock) {
134
- currentCode.push(line);
135
- continue;
136
- }
137
-
138
- // Extract numbered steps
139
- const stepMatch = line.match(/^\s*(\d+)\.\s+(.+)$/);
140
- if (stepMatch && stepMatch[2]) {
141
- steps.push(stepMatch[2].trim());
142
- }
143
-
144
- // Extract bullet points that look like steps
145
- const bulletMatch = line.match(/^\s*[-*]\s+(.+)$/);
146
- if (bulletMatch && bulletMatch[1]) {
147
- const text = bulletMatch[1].trim();
148
- if (isLikelyStep(text)) {
149
- steps.push(text);
150
- }
151
- }
152
- }
153
-
154
- return {
155
- title,
156
- description,
157
- codeBlocks,
158
- steps,
159
- rawContent: content,
160
- format: "markdown",
161
- };
162
- }
163
-
164
- function parseJson(content: string): ParsedDocument {
165
- const parsed = JSON.parse(content) as unknown;
166
- const codeBlocks: CodeBlock[] = [];
167
- const steps: string[] = [];
168
-
169
- if (typeof parsed === "object" && parsed !== null) {
170
- const obj = parsed as Record<string, unknown>;
171
-
172
- // Check if it's a ptywright script format
173
- if ("launch" in obj && "steps" in obj) {
174
- return {
175
- title: (obj.name as string) ?? "Imported Script",
176
- description: "Parsed from JSON script",
177
- codeBlocks: [],
178
- steps: [],
179
- rawContent: content,
180
- format: "json",
181
- };
182
- }
183
-
184
- // Extract commands from various JSON structures
185
- if (Array.isArray(obj.commands)) {
186
- for (const cmd of obj.commands) {
187
- if (typeof cmd === "string") steps.push(cmd);
188
- else if (typeof cmd === "object" && cmd && "command" in cmd) {
189
- steps.push(String((cmd as Record<string, unknown>).command));
190
- }
191
- }
192
- }
193
-
194
- if (Array.isArray(obj.steps)) {
195
- for (const step of obj.steps) {
196
- if (typeof step === "string") steps.push(step);
197
- else if (typeof step === "object" && step) {
198
- const s = step as Record<string, unknown>;
199
- if (typeof s.description === "string") steps.push(s.description);
200
- else if (typeof s.command === "string") steps.push(s.command);
201
- }
202
- }
203
- }
204
- }
205
-
206
- return {
207
- codeBlocks,
208
- steps,
209
- rawContent: content,
210
- format: "json",
211
- };
212
- }
213
-
214
- function parseYaml(content: string): ParsedDocument {
215
- // Simple YAML parsing for common patterns
216
- const steps: string[] = [];
217
- const lines = content.split("\n");
218
-
219
- for (const line of lines) {
220
- // Match YAML list items
221
- const match = line.match(/^\s*-\s+(.+)$/);
222
- if (match && match[1]) {
223
- const value = match[1].trim();
224
- if (!value.startsWith("{") && !value.includes(":")) {
225
- steps.push(value);
226
- }
227
- }
228
- }
229
-
230
- return {
231
- codeBlocks: [],
232
- steps,
233
- rawContent: content,
234
- format: "yaml",
235
- };
236
- }
237
-
238
- function parseHtml(content: string): ParsedDocument {
239
- const codeBlocks: CodeBlock[] = [];
240
- const steps: string[] = [];
241
- let title: string | undefined;
242
-
243
- // Extract title
244
- const titleMatch = content.match(/<title>([^<]+)<\/title>/i);
245
- if (titleMatch && titleMatch[1]) {
246
- title = titleMatch[1].trim();
247
- }
248
-
249
- // Extract code blocks from <pre><code> or <code>
250
- const codeRegex = /<code[^>]*(?:class="[^"]*language-(\w+)[^"]*")?[^>]*>([\s\S]*?)<\/code>/gi;
251
- let match;
252
- while ((match = codeRegex.exec(content)) !== null) {
253
- const lang = match[1] ?? "text";
254
- const code = decodeHtmlEntities(match[2] ?? "");
255
- if (code.trim()) {
256
- codeBlocks.push({
257
- language: lang,
258
- code: code.trim(),
259
- lineNumber: 0,
260
- });
261
- }
262
- }
263
-
264
- // Extract list items
265
- const liRegex = /<li[^>]*>([\s\S]*?)<\/li>/gi;
266
- while ((match = liRegex.exec(content)) !== null) {
267
- const text = stripHtmlTags(match[1] ?? "").trim();
268
- if (text && isLikelyStep(text)) {
269
- steps.push(text);
270
- }
271
- }
272
-
273
- return {
274
- title,
275
- codeBlocks,
276
- steps,
277
- rawContent: content,
278
- format: "html",
279
- };
280
- }
281
-
282
- function parsePlainText(content: string): ParsedDocument {
283
- const lines = content.split("\n");
284
- const steps: string[] = [];
285
-
286
- for (const line of lines) {
287
- const trimmed = line.trim();
288
- if (!trimmed) continue;
289
-
290
- // Extract numbered steps (e.g., "1. do something")
291
- const numberedMatch = trimmed.match(/^\d+\.\s+(.+)$/);
292
- if (numberedMatch && numberedMatch[1]) {
293
- steps.push(numberedMatch[1].trim());
294
- continue;
295
- }
296
-
297
- // Extract bullet points
298
- const bulletMatch = trimmed.match(/^[-*]\s+(.+)$/);
299
- if (bulletMatch && bulletMatch[1]) {
300
- steps.push(bulletMatch[1].trim());
301
- continue;
302
- }
303
-
304
- // Check if it looks like a step
305
- if (isLikelyStep(trimmed)) {
306
- steps.push(trimmed);
307
- }
308
- }
309
-
310
- return {
311
- codeBlocks: [],
312
- steps,
313
- rawContent: content,
314
- format: "text",
315
- };
316
- }
317
-
318
- function isLikelyStep(text: string): boolean {
319
- const stepPatterns = [
320
- /^(run|execute|type|enter|press|click|open|start|stop|wait|check|verify|assert)/i,
321
- /^(输入|执行|运行|点击|打开|启动|停止|等待|检查|验证)/,
322
- /\$\s+\w+/, // Shell prompt pattern
323
- /^>\s+\w+/, // Alternative prompt
324
- ];
325
-
326
- return stepPatterns.some((pattern) => pattern.test(text));
327
- }
328
-
329
- function decodeHtmlEntities(text: string): string {
330
- return text
331
- .replace(/&lt;/g, "<")
332
- .replace(/&gt;/g, ">")
333
- .replace(/&amp;/g, "&")
334
- .replace(/&quot;/g, '"')
335
- .replace(/&#39;/g, "'")
336
- .replace(/&nbsp;/g, " ");
337
- }
338
-
339
- function stripHtmlTags(html: string): string {
340
- return html.replace(/<[^>]+>/g, "");
341
- }
@@ -1,161 +0,0 @@
1
- import { resolve, basename, extname } from "node:path";
2
-
3
- import { parseDocument, type DocumentSource, type ParsedDocument } from "./doc_parser";
4
- import { extractSteps } from "./step_extractor";
5
- import { generateScript } from "./script_generator";
6
-
7
- export type GenerateTestOptions = {
8
- source: string;
9
- sourceType?: "local" | "url" | "auto";
10
- outputDir?: string;
11
- outputFormat?: "json" | "ts" | "both";
12
- targetCommand?: string;
13
- targetArgs?: string[];
14
- name?: string;
15
- cols?: number;
16
- rows?: number;
17
- env?: Record<string, string>;
18
- trace?: {
19
- saveCast?: boolean;
20
- saveReport?: boolean;
21
- };
22
- };
23
-
24
- export type GenerateTestResult = {
25
- ok: boolean;
26
- name: string;
27
- jsonPath?: string;
28
- tsPath?: string;
29
- stepCount: number;
30
- warnings: string[];
31
- error?: string;
32
- parsed?: {
33
- title?: string;
34
- format: string;
35
- codeBlockCount: number;
36
- textStepCount: number;
37
- };
38
- };
39
-
40
- export async function generateTestFromDoc(
41
- options: GenerateTestOptions,
42
- ): Promise<GenerateTestResult> {
43
- const warnings: string[] = [];
44
-
45
- try {
46
- // 1. Resolve document source
47
- const docSource = resolveDocumentSource(options.source, options.sourceType);
48
-
49
- // 2. Parse document
50
- const parsed = await parseDocument(docSource);
51
-
52
- // 3. Extract steps
53
- const extraction = extractSteps(parsed);
54
- warnings.push(...extraction.warnings);
55
-
56
- if (extraction.steps.length === 0) {
57
- return {
58
- ok: false,
59
- name: options.name ?? "unknown",
60
- stepCount: 0,
61
- warnings,
62
- error: "No test steps could be extracted from the document",
63
- parsed: {
64
- title: parsed.title,
65
- format: parsed.format,
66
- codeBlockCount: parsed.codeBlocks.length,
67
- textStepCount: parsed.steps.length,
68
- },
69
- };
70
- }
71
-
72
- // 4. Generate script
73
- const scriptName = resolveScriptName(options.name, options.source, parsed);
74
- const outputDir = options.outputDir ?? resolve(".tmp", "generated");
75
-
76
- const generated = generateScript(extraction.steps, {
77
- name: scriptName,
78
- launch: extraction.launch,
79
- targetCommand: options.targetCommand,
80
- targetArgs: options.targetArgs,
81
- outputDir,
82
- format: options.outputFormat ?? "both",
83
- cols: options.cols,
84
- rows: options.rows,
85
- env: options.env,
86
- trace: options.trace,
87
- });
88
-
89
- return {
90
- ok: true,
91
- name: generated.name,
92
- jsonPath: generated.jsonPath,
93
- tsPath: generated.tsPath,
94
- stepCount: generated.stepCount,
95
- warnings,
96
- parsed: {
97
- title: parsed.title,
98
- format: parsed.format,
99
- codeBlockCount: parsed.codeBlocks.length,
100
- textStepCount: parsed.steps.length,
101
- },
102
- };
103
- } catch (error) {
104
- return {
105
- ok: false,
106
- name: options.name ?? "unknown",
107
- stepCount: 0,
108
- warnings,
109
- error: error instanceof Error ? error.message : String(error),
110
- };
111
- }
112
- }
113
-
114
- function resolveDocumentSource(
115
- source: string,
116
- sourceType?: "local" | "url" | "auto",
117
- ): DocumentSource {
118
- const type = !sourceType || sourceType === "auto" ? detectSourceType(source) : sourceType;
119
-
120
- if (type === "url") {
121
- return { type: "url", url: source };
122
- }
123
-
124
- return { type: "local", path: resolve(source) };
125
- }
126
-
127
- function detectSourceType(source: string): "local" | "url" {
128
- if (source.startsWith("http://") || source.startsWith("https://")) {
129
- return "url";
130
- }
131
- return "local";
132
- }
133
-
134
- function resolveScriptName(
135
- explicitName: string | undefined,
136
- source: string,
137
- parsed: ParsedDocument,
138
- ): string {
139
- if (explicitName) {
140
- return sanitizeName(explicitName);
141
- }
142
-
143
- if (parsed.title) {
144
- return sanitizeName(parsed.title);
145
- }
146
-
147
- // Extract from file path
148
- const base = basename(source, extname(source));
149
- return sanitizeName(base);
150
- }
151
-
152
- function sanitizeName(name: string): string {
153
- return (
154
- name
155
- .toLowerCase()
156
- .replace(/[^a-z0-9_-]/g, "_")
157
- .replace(/_+/g, "_")
158
- .replace(/^_|_$/g, "")
159
- .slice(0, 64) || "generated_test"
160
- );
161
- }
@@ -1,10 +0,0 @@
1
- export { parseDocument, type ParsedDocument, type DocumentSource } from "./doc_parser";
2
- export { extractSteps, type ExtractedStep } from "./step_extractor";
3
- export {
4
- generateScript,
5
- generateJsonScript,
6
- generateTypeScriptScript,
7
- type GenerateOptions,
8
- type GeneratedScript,
9
- } from "./script_generator";
10
- export { generateTestFromDoc, type GenerateTestOptions } from "./generate";