surf-cli 2.2.0 → 2.4.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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Parser for surf `do` workflow commands
3
+ *
4
+ * Parses newline-separated commands into structured step arrays:
5
+ *
6
+ * Input:
7
+ * 'go "https://example.com"
8
+ * click e5
9
+ * screenshot'
10
+ *
11
+ * Output:
12
+ * [
13
+ * { cmd: 'navigate', args: { url: 'https://example.com' } },
14
+ * { cmd: 'click', args: { ref: 'e5' } },
15
+ * { cmd: 'screenshot', args: {} }
16
+ * ]
17
+ */
18
+
19
+ // Aliases mapping (matches cli.cjs)
20
+ const ALIASES = {
21
+ snap: "screenshot",
22
+ read: "page.read",
23
+ find: "search",
24
+ go: "navigate",
25
+ net: "network",
26
+ "network.dump": "network.get",
27
+ };
28
+
29
+ // Primary argument mapping for positional args (matches cli.cjs)
30
+ const PRIMARY_ARG_MAP = {
31
+ ai: "query",
32
+ gemini: "query",
33
+ chatgpt: "query",
34
+ perplexity: "query",
35
+ grok: "query",
36
+ navigate: "url",
37
+ go: "url",
38
+ js: "code",
39
+ javascript_tool: "code",
40
+ key: "key",
41
+ wait: "duration",
42
+ health: "url",
43
+ new_tab: "url",
44
+ "tab.new": "url",
45
+ switch_tab: "tab_id",
46
+ "tab.switch": "id",
47
+ close_tab: "tab_id",
48
+ "tab.close": "id",
49
+ "tab.name": "name",
50
+ "tab.unname": "name",
51
+ scroll_to_position: "position",
52
+ type: "text",
53
+ smart_type: "text",
54
+ "emulate.network": "preset",
55
+ "emulate.cpu": "rate",
56
+ search: "term",
57
+ find: "term",
58
+ "wait.element": "selector",
59
+ "wait.url": "pattern",
60
+ zoom: "level",
61
+ "history.search": "query",
62
+ "network.get": "id",
63
+ "network.body": "id",
64
+ "network.curl": "id",
65
+ "network.path": "id",
66
+ "window.new": "url",
67
+ "window.focus": "id",
68
+ "window.close": "id",
69
+ "locate.role": "role",
70
+ "locate.text": "text",
71
+ "locate.label": "label",
72
+ "emulate.device": "device",
73
+ "frame.js": "code",
74
+ "element.styles": "selector",
75
+ "select": "selector",
76
+ };
77
+
78
+ /**
79
+ * Tokenize a command line, respecting single and double quotes
80
+ * @param {string} line - Single line to tokenize
81
+ * @returns {string[]} - Array of tokens
82
+ */
83
+ function tokenize(line) {
84
+ const tokens = [];
85
+ let current = '';
86
+ let inQuote = null;
87
+
88
+ for (let i = 0; i < line.length; i++) {
89
+ const ch = line[i];
90
+
91
+ if (inQuote) {
92
+ if (ch === inQuote) {
93
+ // End of quoted string
94
+ inQuote = null;
95
+ } else {
96
+ current += ch;
97
+ }
98
+ } else if (ch === '"' || ch === "'") {
99
+ // Start of quoted string
100
+ inQuote = ch;
101
+ } else if (ch === ' ' || ch === '\t') {
102
+ // Whitespace separator
103
+ if (current) {
104
+ tokens.push(current);
105
+ current = '';
106
+ }
107
+ } else {
108
+ current += ch;
109
+ }
110
+ }
111
+
112
+ // Don't forget last token
113
+ if (current) {
114
+ tokens.push(current);
115
+ }
116
+
117
+ return tokens;
118
+ }
119
+
120
+ /**
121
+ * Parse a single command line into a step object
122
+ * @param {string} line - Single command line
123
+ * @returns {{ cmd: string, args: object } | null}
124
+ */
125
+ function parseCommandLine(line) {
126
+ const tokens = tokenize(line);
127
+ if (tokens.length === 0) return null;
128
+
129
+ // Get command and apply alias
130
+ let cmd = tokens[0];
131
+ cmd = ALIASES[cmd] || cmd;
132
+
133
+ const args = {};
134
+ let i = 1;
135
+
136
+ // Handle first positional argument based on command type
137
+ if (i < tokens.length && !tokens[i].startsWith('--')) {
138
+ const firstArg = tokens[i];
139
+
140
+ // Special handling for click command
141
+ if (cmd === 'click') {
142
+ if (/^e\d+$/.test(firstArg)) {
143
+ // Element reference: e5 -> ref
144
+ args.ref = firstArg;
145
+ i++;
146
+ } else if (/^\d+$/.test(firstArg) && tokens[i + 1] && /^\d+$/.test(tokens[i + 1])) {
147
+ // Coordinates: 100 200 -> x, y
148
+ args.x = parseInt(firstArg, 10);
149
+ args.y = parseInt(tokens[i + 1], 10);
150
+ i += 2;
151
+ }
152
+ } else if (cmd === 'select') {
153
+ // Select takes selector + one or more values: select e5 "US" or select e5 "opt1" "opt2"
154
+ args.selector = firstArg;
155
+ i++;
156
+ // Collect remaining positional args as values
157
+ const values = [];
158
+ while (i < tokens.length && !tokens[i].startsWith('--')) {
159
+ values.push(tokens[i]);
160
+ i++;
161
+ }
162
+ // Host expects 'values' (always), matching CLI behavior
163
+ if (values.length === 1) {
164
+ args.values = values[0]; // Single value as string (host will wrap in array)
165
+ } else if (values.length > 1) {
166
+ args.values = values; // Multiple values as array
167
+ }
168
+ } else {
169
+ // Use PRIMARY_ARG_MAP for other commands
170
+ const primaryKey = PRIMARY_ARG_MAP[cmd];
171
+ if (primaryKey) {
172
+ args[primaryKey] = firstArg;
173
+ i++;
174
+ }
175
+ }
176
+ }
177
+
178
+ // Parse --flag value pairs
179
+ while (i < tokens.length) {
180
+ const token = tokens[i];
181
+ if (token.startsWith('--')) {
182
+ const key = token.slice(2);
183
+ const next = tokens[i + 1];
184
+ if (next && !next.startsWith('--')) {
185
+ // Flag with value
186
+ let val = next;
187
+ // Type coercion
188
+ if (val === "true") val = true;
189
+ else if (val === "false") val = false;
190
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
191
+ else if (/^-?\d+\.\d+$/.test(val)) val = parseFloat(val);
192
+ args[key] = val;
193
+ i += 2;
194
+ } else {
195
+ // Boolean flag
196
+ args[key] = true;
197
+ i++;
198
+ }
199
+ } else {
200
+ // Skip unrecognized positional (shouldn't happen normally)
201
+ i++;
202
+ }
203
+ }
204
+
205
+ return { cmd, args };
206
+ }
207
+
208
+ /**
209
+ * Parse a multi-line workflow string into step array
210
+ * @param {string} input - Multi-line workflow string
211
+ * @returns {Array<{ cmd: string, args: object }>}
212
+ */
213
+ function parseDoCommands(input) {
214
+ // Replace literal \n (backslash + n) with actual newlines
215
+ // This handles bash single-quoted strings like 'go "url"\nclick e5'
216
+ const normalized = input.replace(/\\n/g, '\n');
217
+
218
+ return normalized
219
+ .split('\n')
220
+ .map(line => line.trim())
221
+ .filter(line => line && !line.startsWith('#'))
222
+ .map(line => parseCommandLine(line))
223
+ .filter(step => step !== null);
224
+ }
225
+
226
+ module.exports = {
227
+ parseDoCommands,
228
+ parseCommandLine,
229
+ tokenize,
230
+ ALIASES,
231
+ PRIMARY_ARG_MAP
232
+ };