surf-cli 2.4.2 → 2.5.1

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/native/cli.cjs CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  const net = require("net");
3
3
  const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
4
6
  const { execSync } = require("child_process");
5
7
  const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
6
8
  const networkFormatters = require("./formatters/network.cjs");
@@ -10,6 +12,249 @@ const { executeDoSteps } = require("./do-executor.cjs");
10
12
 
11
13
  const SOCKET_PATH = "/tmp/surf.sock";
12
14
 
15
+ // ============================================================================
16
+ // Workflow Resolution and Management
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Get workflow search directories
21
+ * @returns {Array<{path: string, scope: string}>}
22
+ */
23
+ function getWorkflowDirs() {
24
+ return [
25
+ { path: path.join(process.cwd(), '.surf', 'workflows'), scope: 'project' },
26
+ { path: path.join(os.homedir(), '.surf', 'workflows'), scope: 'user' },
27
+ ];
28
+ }
29
+
30
+ /**
31
+ * Resolve a workflow by name or path
32
+ * @param {string} nameOrPath - Workflow name or file path
33
+ * @returns {{ type: 'inline'|'file'|'not_found', content?: string, path?: string, name?: string }}
34
+ */
35
+ function resolveWorkflow(nameOrPath) {
36
+ // Check if it's an inline workflow (contains pipe)
37
+ if (nameOrPath.includes('|')) {
38
+ return { type: 'inline', content: nameOrPath };
39
+ }
40
+
41
+ // Check if it's a direct file path (with extension or path separator)
42
+ if (nameOrPath.includes('/') || nameOrPath.includes('\\') || nameOrPath.endsWith('.json')) {
43
+ if (fs.existsSync(nameOrPath)) {
44
+ return { type: 'file', path: nameOrPath };
45
+ }
46
+ return { type: 'not_found', name: nameOrPath };
47
+ }
48
+
49
+ // Look up by name in workflow directories
50
+ const searchDirs = getWorkflowDirs();
51
+
52
+ for (const { path: dir } of searchDirs) {
53
+ const filePath = path.join(dir, `${nameOrPath}.json`);
54
+ if (fs.existsSync(filePath)) {
55
+ return { type: 'file', path: filePath };
56
+ }
57
+ }
58
+
59
+ return { type: 'not_found', name: nameOrPath };
60
+ }
61
+
62
+ /**
63
+ * List all available workflows
64
+ * @returns {Array<{name: string, description: string, scope: string, path: string, args?: object}>}
65
+ */
66
+ function listWorkflows() {
67
+ const workflows = [];
68
+ const searchDirs = getWorkflowDirs();
69
+
70
+ for (const { path: dir, scope } of searchDirs) {
71
+ if (fs.existsSync(dir)) {
72
+ try {
73
+ const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
74
+ for (const file of files) {
75
+ const filePath = path.join(dir, file);
76
+ try {
77
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
78
+ workflows.push({
79
+ name: content.name || file.replace('.json', ''),
80
+ description: content.description || '',
81
+ scope,
82
+ path: filePath,
83
+ args: content.args,
84
+ stepCount: content.steps?.length || 0,
85
+ });
86
+ } catch {
87
+ // Skip invalid JSON files
88
+ }
89
+ }
90
+ } catch {
91
+ // Skip inaccessible directories
92
+ }
93
+ }
94
+ }
95
+
96
+ return workflows;
97
+ }
98
+
99
+ /**
100
+ * Get detailed info about a workflow
101
+ * @param {string} name - Workflow name
102
+ * @returns {{ error?: string, name?: string, description?: string, args?: object, steps?: Array, path?: string }}
103
+ */
104
+ function getWorkflowInfo(name) {
105
+ const resolved = resolveWorkflow(name);
106
+
107
+ if (resolved.type === 'not_found') {
108
+ return { error: `Workflow not found: ${name}` };
109
+ }
110
+
111
+ if (resolved.type === 'inline') {
112
+ return { error: 'Cannot get info for inline workflows' };
113
+ }
114
+
115
+ try {
116
+ const content = JSON.parse(fs.readFileSync(resolved.path, 'utf8'));
117
+ return {
118
+ name: content.name || name,
119
+ description: content.description || '',
120
+ args: content.args || {},
121
+ steps: content.steps || [],
122
+ path: resolved.path,
123
+ };
124
+ } catch (e) {
125
+ return { error: `Failed to parse workflow: ${e.message}` };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Validate workflow args against schema
131
+ * @param {object} workflow - Workflow with args schema
132
+ * @param {object} providedArgs - User-provided args
133
+ * @returns {string[]} - Array of error messages
134
+ */
135
+ function validateWorkflowArgs(workflow, providedArgs) {
136
+ const errors = [];
137
+ if (workflow.args) {
138
+ for (const [name, spec] of Object.entries(workflow.args)) {
139
+ if (spec.required && providedArgs[name] === undefined) {
140
+ errors.push(`Missing required argument: --${name}`);
141
+ }
142
+ }
143
+ }
144
+ return errors;
145
+ }
146
+
147
+ /**
148
+ * Apply default values to workflow args
149
+ * @param {object} workflow - Workflow with args schema
150
+ * @param {object} providedArgs - User-provided args
151
+ * @returns {object} - Args with defaults applied
152
+ */
153
+ function applyArgDefaults(workflow, providedArgs) {
154
+ const vars = { ...providedArgs };
155
+ if (workflow.args) {
156
+ for (const [name, spec] of Object.entries(workflow.args)) {
157
+ if (vars[name] === undefined && spec.default !== undefined) {
158
+ vars[name] = spec.default;
159
+ }
160
+ }
161
+ }
162
+ return vars;
163
+ }
164
+
165
+ /**
166
+ * Validate a workflow JSON file
167
+ * @param {string} filePath - Path to workflow file
168
+ * @returns {{ valid: boolean, error?: string, workflow?: object }}
169
+ */
170
+ function validateWorkflowFile(filePath) {
171
+ if (!fs.existsSync(filePath)) {
172
+ return { valid: false, error: `File not found: ${filePath}` };
173
+ }
174
+
175
+ try {
176
+ const content = fs.readFileSync(filePath, 'utf8');
177
+ const workflow = JSON.parse(content);
178
+
179
+ // Basic structure validation
180
+ if (!workflow.steps || !Array.isArray(workflow.steps)) {
181
+ return { valid: false, error: "Workflow must have a 'steps' array" };
182
+ }
183
+
184
+ if (workflow.steps.length === 0) {
185
+ return { valid: false, error: "Workflow has no steps" };
186
+ }
187
+
188
+ // Validate each step
189
+ for (let i = 0; i < workflow.steps.length; i++) {
190
+ const step = workflow.steps[i];
191
+
192
+ // Check for loops
193
+ if (step.repeat !== undefined || step.each !== undefined) {
194
+ if (!step.steps || !Array.isArray(step.steps)) {
195
+ return { valid: false, error: `Step ${i + 1}: loop must have a 'steps' array` };
196
+ }
197
+ continue;
198
+ }
199
+
200
+ // Regular step must have tool/cmd
201
+ if (!step.tool && !step.cmd) {
202
+ return { valid: false, error: `Step ${i + 1}: must have 'tool' field` };
203
+ }
204
+ }
205
+
206
+ // Validate args schema if present
207
+ if (workflow.args && typeof workflow.args !== 'object') {
208
+ return { valid: false, error: "'args' must be an object" };
209
+ }
210
+
211
+ return { valid: true, workflow };
212
+ } catch (e) {
213
+ return { valid: false, error: `Invalid JSON: ${e.message}` };
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Format a step for display
219
+ * @param {object} step - Workflow step
220
+ * @param {number} indent - Indentation level
221
+ * @returns {string}
222
+ */
223
+ function formatStep(step, indent = 0) {
224
+ const pad = ' '.repeat(indent);
225
+
226
+ if (step.repeat !== undefined) {
227
+ const lines = [`${pad}repeat ${step.repeat} times:`];
228
+ for (const s of step.steps || []) {
229
+ lines.push(formatStep(s, indent + 1));
230
+ }
231
+ if (step.until) {
232
+ lines.push(`${pad} until: ${step.until.tool || step.until.cmd}`);
233
+ }
234
+ return lines.join('\n');
235
+ }
236
+
237
+ if (step.each !== undefined) {
238
+ const lines = [`${pad}each ${step.each} as ${step.as || 'item'}:`];
239
+ for (const s of step.steps || []) {
240
+ lines.push(formatStep(s, indent + 1));
241
+ }
242
+ return lines.join('\n');
243
+ }
244
+
245
+ const tool = step.tool || step.cmd;
246
+ const args = step.args || {};
247
+ const argStr = Object.entries(args)
248
+ .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
249
+ .join(' ');
250
+
251
+ let line = `${pad}${tool}`;
252
+ if (argStr) line += ` ${argStr}`;
253
+ if (step.as) line += ` → ${step.as}`;
254
+
255
+ return line;
256
+ }
257
+
13
258
  // Cross-platform image resize (macOS: sips, Linux: ImageMagick)
14
259
  function resizeImage(filePath, maxSize) {
15
260
  const platform = process.platform;
@@ -804,7 +1049,7 @@ const TOOLS = {
804
1049
  }
805
1050
  },
806
1051
  workflow: {
807
- desc: "Workflow execution",
1052
+ desc: "Workflow execution and management",
808
1053
  commands: {
809
1054
  "do": {
810
1055
  desc: "Execute multiple commands as a single workflow",
@@ -819,9 +1064,34 @@ const TOOLS = {
819
1064
  examples: [
820
1065
  { cmd: 'do \'go "https://example.com" | click e5 | screenshot\'', desc: "Inline workflow" },
821
1066
  { cmd: 'do -f login.json', desc: "From JSON file" },
1067
+ { cmd: 'do github-login --email "x" --password "y"', desc: "Named workflow with args" },
822
1068
  { cmd: 'do \'go "url" | click e5\' --dry-run', desc: "Validate without running" },
823
1069
  ]
824
1070
  },
1071
+ "workflow.list": {
1072
+ desc: "List available workflows",
1073
+ args: [],
1074
+ opts: {},
1075
+ examples: [
1076
+ { cmd: 'workflow.list', desc: "Show all workflows" },
1077
+ ]
1078
+ },
1079
+ "workflow.info": {
1080
+ desc: "Show workflow details and arguments",
1081
+ args: ["name"],
1082
+ opts: {},
1083
+ examples: [
1084
+ { cmd: 'workflow.info github-login', desc: "Show workflow details" },
1085
+ ]
1086
+ },
1087
+ "workflow.validate": {
1088
+ desc: "Validate workflow JSON file",
1089
+ args: ["file"],
1090
+ opts: {},
1091
+ examples: [
1092
+ { cmd: 'workflow.validate ./my-flow.json', desc: "Check JSON validity" },
1093
+ ]
1094
+ },
825
1095
  }
826
1096
  },
827
1097
  zoom: {
@@ -1762,6 +2032,12 @@ if (args[0] === "do") {
1762
2032
  let tabId = undefined;
1763
2033
  let windowId = undefined;
1764
2034
 
2035
+ // Reserved flags that aren't workflow args
2036
+ const reservedFlags = ['file', 'f', 'dry-run', 'on-error', 'no-auto-wait', 'step-delay', 'json', 'tab-id', 'window-id'];
2037
+
2038
+ // Workflow-specific args (collected for variable substitution)
2039
+ const workflowArgs = {};
2040
+
1765
2041
  // Parse do-specific arguments
1766
2042
  for (let i = 0; i < doArgs.length; i++) {
1767
2043
  const arg = doArgs[i];
@@ -1787,61 +2063,165 @@ if (args[0] === "do") {
1787
2063
  } else if (arg === "--window-id") {
1788
2064
  windowId = parseInt(doArgs[i + 1], 10);
1789
2065
  i++;
2066
+ } else if (arg.startsWith("--")) {
2067
+ // Workflow-specific arg (e.g., --email, --password)
2068
+ const key = arg.slice(2);
2069
+ if (!reservedFlags.includes(key)) {
2070
+ const next = doArgs[i + 1];
2071
+ if (next !== undefined && !next.startsWith("--")) {
2072
+ // Type coercion
2073
+ let val = next;
2074
+ if (val === "true") val = true;
2075
+ else if (val === "false") val = false;
2076
+ else if (/^-?\d+$/.test(val)) val = parseInt(val, 10);
2077
+ else if (/^-?\d+\.\d+$/.test(val)) val = parseFloat(val);
2078
+ workflowArgs[key] = val;
2079
+ i++;
2080
+ } else {
2081
+ workflowArgs[key] = true;
2082
+ }
2083
+ }
1790
2084
  } else if (!arg.startsWith("-")) {
1791
2085
  commandsInput = arg;
1792
2086
  }
1793
2087
  }
1794
2088
 
1795
2089
  if (!commandsInput && !fileInput) {
1796
- console.error("Error: commands string or --file required");
1797
- console.error('Usage: surf do \'go "url"\\nclick e5\'');
2090
+ console.error("Error: commands string, workflow name, or --file required");
2091
+ console.error('Usage: surf do \'go "url" | click e5\'');
1798
2092
  console.error(" surf do --file workflow.json");
2093
+ console.error(" surf do my-workflow --arg1 value1 --arg2 value2");
1799
2094
  process.exit(1);
1800
2095
  }
1801
2096
 
1802
2097
  let steps;
2098
+ let workflow = null; // Full workflow object (for arg validation)
2099
+ let workflowName = null;
2100
+
1803
2101
  try {
1804
2102
  if (fileInput) {
2103
+ // Explicit file path via --file
1805
2104
  if (!fs.existsSync(fileInput)) {
1806
2105
  console.error(`Error: File not found: ${fileInput}`);
1807
2106
  process.exit(1);
1808
2107
  }
1809
2108
  const content = fs.readFileSync(fileInput, "utf8");
1810
- // JSON file format (same as --script)
1811
- const script = JSON.parse(content);
1812
- if (!script.steps || !Array.isArray(script.steps)) {
1813
- throw new Error("JSON must have a 'steps' array");
1814
- }
1815
- // Convert --script format { tool, args } to do format { cmd, args }
1816
- steps = script.steps.map(s => ({ cmd: s.tool, args: s.args || {} }));
2109
+ workflow = JSON.parse(content);
2110
+ workflowName = workflow.name || fileInput;
1817
2111
  } else {
1818
- // Inline string parsing
1819
- steps = parseDoCommands(commandsInput);
2112
+ // Resolve: inline | file path | named workflow
2113
+ const resolved = resolveWorkflow(commandsInput);
2114
+
2115
+ if (resolved.type === 'inline') {
2116
+ // Inline pipe syntax
2117
+ steps = parseDoCommands(resolved.content);
2118
+ } else if (resolved.type === 'file') {
2119
+ // Found workflow file
2120
+ const content = fs.readFileSync(resolved.path, "utf8");
2121
+ workflow = JSON.parse(content);
2122
+ workflowName = workflow.name || commandsInput;
2123
+ } else {
2124
+ // Not found - try parsing as inline (might be a single command)
2125
+ steps = parseDoCommands(commandsInput);
2126
+ if (steps.length === 0) {
2127
+ console.error(`Error: Workflow not found: ${commandsInput}`);
2128
+ console.error(`Searched in:`);
2129
+ for (const { path: dir } of getWorkflowDirs()) {
2130
+ console.error(` ${dir}`);
2131
+ }
2132
+ console.error(`\nRun 'surf workflow.list' to see available workflows.`);
2133
+ process.exit(1);
2134
+ }
2135
+ }
2136
+ }
2137
+
2138
+ // Process workflow file if loaded
2139
+ if (workflow) {
2140
+ if (!workflow.steps || !Array.isArray(workflow.steps)) {
2141
+ throw new Error("Workflow must have a 'steps' array");
2142
+ }
2143
+
2144
+ // Validate required args
2145
+ const argErrors = validateWorkflowArgs(workflow, workflowArgs);
2146
+ if (argErrors.length > 0) {
2147
+ console.error("Error: Missing required arguments:");
2148
+ argErrors.forEach(e => console.error(` ${e}`));
2149
+ if (workflow.args) {
2150
+ console.error(`\nWorkflow arguments:`);
2151
+ for (const [name, spec] of Object.entries(workflow.args)) {
2152
+ const req = spec.required ? ' (required)' : '';
2153
+ const def = spec.default !== undefined ? ` [default: ${spec.default}]` : '';
2154
+ const desc = spec.desc || spec.description || '';
2155
+ console.error(` --${name}${req}${def}${desc ? ` - ${desc}` : ''}`);
2156
+ }
2157
+ }
2158
+ console.error(`\nRun 'surf workflow.info ${workflowName}' for details.`);
2159
+ process.exit(1);
2160
+ }
2161
+
2162
+ // Convert steps: support both { tool, args } and { cmd, args } formats
2163
+ // Also preserve loop steps as-is
2164
+ steps = workflow.steps.map(s => {
2165
+ if (s.repeat !== undefined || s.each !== undefined) {
2166
+ // Loop step - convert nested steps recursively
2167
+ const convertSteps = (stepsArr) => stepsArr.map(ns => {
2168
+ if (ns.repeat !== undefined || ns.each !== undefined) {
2169
+ // Recursively convert nested loop steps and until condition
2170
+ return {
2171
+ ...ns,
2172
+ steps: convertSteps(ns.steps || []),
2173
+ until: ns.until ? { cmd: ns.until.tool || ns.until.cmd, args: ns.until.args || {} } : undefined
2174
+ };
2175
+ }
2176
+ return { cmd: ns.tool || ns.cmd, args: ns.args || {}, as: ns.as };
2177
+ });
2178
+ return {
2179
+ ...s,
2180
+ steps: convertSteps(s.steps || []),
2181
+ until: s.until ? { cmd: s.until.tool || s.until.cmd, args: s.until.args || {} } : undefined
2182
+ };
2183
+ }
2184
+ return { cmd: s.tool || s.cmd, args: s.args || {}, as: s.as };
2185
+ });
1820
2186
  }
1821
2187
  } catch (e) {
1822
2188
  console.error(`Error: Failed to parse workflow: ${e.message}`);
1823
2189
  process.exit(1);
1824
2190
  }
1825
2191
 
1826
- if (steps.length === 0) {
2192
+ if (!steps || steps.length === 0) {
1827
2193
  console.error("Error: No commands found in workflow");
1828
2194
  process.exit(1);
1829
2195
  }
1830
2196
 
2197
+ // Apply arg defaults
2198
+ const vars = workflow ? applyArgDefaults(workflow, workflowArgs) : workflowArgs;
2199
+
1831
2200
  // Validate with --dry-run
1832
2201
  if (dryRun) {
1833
- console.log(`Would execute ${steps.length} steps:`);
2202
+ if (workflowName) {
2203
+ console.log(`Workflow: ${workflowName}`);
2204
+ if (workflow?.description) console.log(`Description: ${workflow.description}`);
2205
+ }
2206
+ console.log(`\nWould execute ${steps.length} steps:`);
1834
2207
  steps.forEach((s, i) => {
1835
- const argStr = Object.entries(s.args || {})
1836
- .map(([k, v]) => `${k}=${JSON.stringify(v)}`)
1837
- .join(" ");
1838
- console.log(` ${i + 1}. ${s.cmd} ${argStr}`);
2208
+ console.log(` ${i + 1}. ${formatStep(s)}`);
1839
2209
  });
2210
+ if (Object.keys(vars).length > 0) {
2211
+ console.log(`\nVariables:`);
2212
+ for (const [k, v] of Object.entries(vars)) {
2213
+ console.log(` ${k} = ${JSON.stringify(v)}`);
2214
+ }
2215
+ }
1840
2216
  process.exit(0);
1841
2217
  }
1842
2218
 
1843
2219
  if (!wantJson) {
1844
- console.log(`Running workflow (${steps.length} steps)...\n`);
2220
+ if (workflowName) {
2221
+ console.log(`Running workflow: ${workflowName} (${steps.length} steps)...\n`);
2222
+ } else {
2223
+ console.log(`Running workflow (${steps.length} steps)...\n`);
2224
+ }
1845
2225
  }
1846
2226
 
1847
2227
  const runWorkflow = async () => {
@@ -1850,6 +2230,7 @@ if (args[0] === "do") {
1850
2230
  autoWait: !noAutoWait,
1851
2231
  stepDelay,
1852
2232
  quiet: wantJson,
2233
+ vars,
1853
2234
  context: {
1854
2235
  tabId,
1855
2236
  windowId,
@@ -1880,6 +2261,127 @@ if (args[0] === "do") {
1880
2261
  return;
1881
2262
  }
1882
2263
 
2264
+ // Handle workflow management commands
2265
+ if (args[0] === "workflow.list") {
2266
+ const workflows = listWorkflows();
2267
+
2268
+ if (workflows.length === 0) {
2269
+ console.log("No workflows found.");
2270
+ console.log(`\nWorkflow directories:`);
2271
+ for (const { path: dir, scope } of getWorkflowDirs()) {
2272
+ console.log(` ${scope}: ${dir}`);
2273
+ }
2274
+ console.log(`\nCreate a workflow JSON file in one of these directories.`);
2275
+ process.exit(0);
2276
+ }
2277
+
2278
+ // Group by scope
2279
+ const byScope = { project: [], user: [] };
2280
+ for (const w of workflows) {
2281
+ byScope[w.scope].push(w);
2282
+ }
2283
+
2284
+ if (byScope.user.length > 0) {
2285
+ console.log(`User Workflows (~/.surf/workflows/):`);
2286
+ for (const w of byScope.user) {
2287
+ const desc = w.description ? ` - ${w.description}` : '';
2288
+ console.log(` ${w.name.padEnd(20)} ${desc}`);
2289
+ }
2290
+ console.log("");
2291
+ }
2292
+
2293
+ if (byScope.project.length > 0) {
2294
+ console.log(`Project Workflows (./.surf/workflows/):`);
2295
+ for (const w of byScope.project) {
2296
+ const desc = w.description ? ` - ${w.description}` : '';
2297
+ console.log(` ${w.name.padEnd(20)} ${desc}`);
2298
+ }
2299
+ console.log("");
2300
+ }
2301
+
2302
+ console.log(`Run 'surf workflow.info <name>' for details.`);
2303
+ process.exit(0);
2304
+ }
2305
+
2306
+ if (args[0] === "workflow.info") {
2307
+ const name = args[1];
2308
+ if (!name) {
2309
+ console.error("Error: workflow name required");
2310
+ console.error("Usage: surf workflow.info <name>");
2311
+ process.exit(1);
2312
+ }
2313
+
2314
+ const info = getWorkflowInfo(name);
2315
+ if (info.error) {
2316
+ console.error(`Error: ${info.error}`);
2317
+ process.exit(1);
2318
+ }
2319
+
2320
+ console.log(`${info.name}${info.description ? ` - ${info.description}` : ''}`);
2321
+ console.log("");
2322
+
2323
+ // Arguments
2324
+ if (info.args && Object.keys(info.args).length > 0) {
2325
+ console.log("Arguments:");
2326
+ for (const [argName, spec] of Object.entries(info.args)) {
2327
+ const req = spec.required ? ' (required)' : '';
2328
+ const def = spec.default !== undefined ? ` [default: ${spec.default}]` : '';
2329
+ const desc = spec.desc || spec.description || '';
2330
+ console.log(` --${argName}${req}${def}`);
2331
+ if (desc) console.log(` ${desc}`);
2332
+ }
2333
+ console.log("");
2334
+ }
2335
+
2336
+ // Steps
2337
+ console.log(`Steps (${info.steps.length}):`);
2338
+ info.steps.forEach((step, i) => {
2339
+ console.log(` ${i + 1}. ${formatStep(step)}`);
2340
+ });
2341
+ console.log("");
2342
+
2343
+ // Location
2344
+ console.log(`Location: ${info.path}`);
2345
+ console.log("");
2346
+
2347
+ // Example run command
2348
+ const argExample = Object.entries(info.args || {})
2349
+ .filter(([_, spec]) => spec.required)
2350
+ .map(([name, _]) => `--${name} "..."`)
2351
+ .join(' ');
2352
+ console.log(`Run:`);
2353
+ console.log(` surf do ${name}${argExample ? ' ' + argExample : ''}`);
2354
+
2355
+ process.exit(0);
2356
+ }
2357
+
2358
+ if (args[0] === "workflow.validate") {
2359
+ const filePath = args[1];
2360
+ if (!filePath) {
2361
+ console.error("Error: file path required");
2362
+ console.error("Usage: surf workflow.validate <file>");
2363
+ process.exit(1);
2364
+ }
2365
+
2366
+ const result = validateWorkflowFile(filePath);
2367
+
2368
+ if (result.valid) {
2369
+ console.log(`✓ Valid workflow: ${filePath}`);
2370
+ console.log(` Name: ${result.workflow.name || '(unnamed)'}`);
2371
+ console.log(` Steps: ${result.workflow.steps.length}`);
2372
+ if (result.workflow.args) {
2373
+ const argCount = Object.keys(result.workflow.args).length;
2374
+ const reqCount = Object.values(result.workflow.args).filter(a => a.required).length;
2375
+ console.log(` Args: ${argCount} (${reqCount} required)`);
2376
+ }
2377
+ process.exit(0);
2378
+ } else {
2379
+ console.error(`✗ Invalid workflow: ${filePath}`);
2380
+ console.error(` Error: ${result.error}`);
2381
+ process.exit(1);
2382
+ }
2383
+ }
2384
+
1883
2385
  const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait"];
1884
2386
 
1885
2387
  const AUTO_SCREENSHOT_TOOLS = ["click", "type", "key", "smart_type", "form.fill", "form_input", "drag", "hover", "scroll", "scroll.top", "scroll.bottom", "scroll.to", "dialog.accept", "dialog.dismiss", "js", "eval"];