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.
- package/README.md +459 -116
- package/dist/agent.mjs +2 -0
- package/dist/bin/ptywright.mjs +6 -0
- package/dist/cli-DIUx2w6X.mjs +3587 -0
- package/dist/cli.mjs +2 -0
- package/{src/index.ts → dist/index.mjs} +7 -9
- package/dist/mcp.mjs +2 -0
- package/dist/pty-cassette.mjs +24 -0
- package/dist/pty_like-Cpkh_O9B.mjs +404 -0
- package/dist/runner-DzZlFrt1.mjs +1897 -0
- package/dist/runner-zApMYWZx.mjs +3257 -0
- package/dist/script.mjs +2 -0
- package/dist/server-VHuEWWj_.mjs +3068 -0
- package/dist/session.mjs +2 -0
- package/dist/terminal_session-DopC7Xg6.mjs +893 -0
- package/package.json +28 -21
- package/schemas/ptywright-agent-cassette.schema.json +57 -0
- package/schemas/ptywright-agent-check.schema.json +122 -0
- package/schemas/ptywright-agent-manifest.schema.json +107 -0
- package/schemas/ptywright-agent-promote.schema.json +146 -0
- package/schemas/ptywright-agent-replay-summary.schema.json +140 -0
- package/schemas/ptywright-agent-run.schema.json +126 -0
- package/schemas/ptywright-agent.schema.json +182 -0
- package/schemas/ptywright-pty-cassette.schema.json +86 -0
- package/schemas/ptywright-script-manifest.schema.json +75 -0
- package/schemas/ptywright-script-run-summary.schema.json +114 -0
- package/schemas/ptywright-script.schema.json +55 -3
- package/skills/ptywright-testing/SKILL.md +53 -33
- package/bin/ptywright +0 -4
- package/src/cli.ts +0 -414
- package/src/generator/doc_parser.ts +0 -341
- package/src/generator/generate.ts +0 -161
- package/src/generator/index.ts +0 -10
- package/src/generator/script_generator.ts +0 -209
- package/src/generator/step_extractor.ts +0 -397
- package/src/mcp/http_server.ts +0 -174
- package/src/mcp/script_recording.ts +0 -238
- package/src/mcp/server.ts +0 -1348
- package/src/pty/bun_pty_adapter.ts +0 -34
- package/src/pty/bun_terminal_adapter.ts +0 -149
- package/src/pty/pty_adapter.ts +0 -31
- package/src/script/dsl.ts +0 -188
- package/src/script/module.ts +0 -43
- package/src/script/path.ts +0 -151
- package/src/script/run.ts +0 -108
- package/src/script/run_all.ts +0 -229
- package/src/script/runner.ts +0 -983
- package/src/script/schema.ts +0 -237
- package/src/script/steps/assert_snapshot_equals.ts +0 -21
- package/src/script/steps/index.ts +0 -2
- package/src/script/suite_report.ts +0 -626
- package/src/session/session_manager.ts +0 -145
- package/src/session/terminal_session.ts +0 -473
- package/src/terminal/ansi.ts +0 -142
- package/src/terminal/keys.ts +0 -180
- package/src/terminal/mask.ts +0 -70
- package/src/terminal/mouse.ts +0 -75
- package/src/terminal/snapshot.ts +0 -196
- package/src/terminal/style.ts +0 -121
- package/src/terminal/view.ts +0 -49
- package/src/trace/asciicast.ts +0 -20
- package/src/trace/asciinema_player_assets.ts +0 -44
- package/src/trace/cast_to_txt.ts +0 -116
- package/src/trace/recorder.ts +0 -110
- package/src/trace/report.ts +0 -2092
- package/src/types.ts +0 -86
- package/src/util/hash.ts +0 -8
- 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
|
-
}
|