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,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";