surf-cli 2.9.0 → 2.10.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 +48 -4
- package/dist/content/accessibility-tree.js +11 -0
- package/dist/content/accessibility-tree.js.map +1 -0
- package/dist/content/visual-indicator.js +111 -0
- package/dist/content/visual-indicator.js.map +1 -0
- package/dist/manifest.json +11 -2
- package/dist/options/options.js +3 -3
- package/dist/options/options.js.map +1 -1
- package/dist/service-worker/index.js +61 -261
- package/dist/service-worker/index.js.map +1 -1
- package/native/activity-journal.cjs +55 -0
- package/native/chatgpt-client.cjs +2 -0
- package/native/cli.cjs +52 -278
- package/native/do-executor.cjs +52 -475
- package/native/do-parser.cjs +8 -249
- package/native/host-helpers.cjs +6 -14
- package/native/host-sessions.cjs +5 -1
- package/native/host.cjs +199 -1
- package/native/network-export.cjs +20 -17
- package/native/network-store.cjs +38 -58
- package/native/playbook-authoring.cjs +44 -0
- package/native/playbook-cli.cjs +157 -0
- package/native/playbook-client.cjs +259 -0
- package/native/playbook-receipts.cjs +109 -0
- package/native/playbook-records.cjs +208 -0
- package/native/playbook-runtime.cjs +177 -0
- package/native/playbooks.cjs +235 -0
- package/native/private-state.cjs +156 -0
- package/native/redaction.cjs +104 -0
- package/native/workflow-definition.cjs +368 -0
- package/native/workflow-runtime.cjs +225 -0
- package/package.json +2 -1
- package/playbooks/page/ops/read.json +22 -0
- package/playbooks/page/playbook.json +7 -0
- package/skills/surf/SKILL.md +41 -1
- package/dist/content/index.js +0 -116
- package/dist/content/index.js.map +0 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { appendPrivateJsonLine, atomicWriteFile, ensurePrivateDir, getPrivateStateRoot, readPrivateFile } = require("./private-state.cjs");
|
|
4
|
+
const { commandMetadata, redactCommandArgs } = require("./workflow-definition.cjs");
|
|
5
|
+
|
|
6
|
+
const MAX_JOURNAL_BYTES = 1024 * 1024;
|
|
7
|
+
const MAX_JOURNAL_EVENTS = 500;
|
|
8
|
+
|
|
9
|
+
function journalPath(root = getPrivateStateRoot()) {
|
|
10
|
+
return path.join(root, "activity-journal", "events.jsonl");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function compactJournal(filePath, root) {
|
|
14
|
+
const stat = fs.statSync(filePath);
|
|
15
|
+
if (stat.size <= MAX_JOURNAL_BYTES) return;
|
|
16
|
+
const lines = readPrivateFile(filePath, { root, encoding: "utf8" }).trim().split("\n").filter(Boolean).slice(-MAX_JOURNAL_EVENTS);
|
|
17
|
+
atomicWriteFile(filePath, `${lines.join("\n")}\n`, { root, encoding: "utf8" });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function appendActivity(event, { root = getPrivateStateRoot() } = {}) {
|
|
21
|
+
const filePath = journalPath(root);
|
|
22
|
+
ensurePrivateDir(path.dirname(filePath), root);
|
|
23
|
+
appendPrivateJsonLine(filePath, { version: 1, ...event }, { root });
|
|
24
|
+
compactJournal(filePath, root);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function journalCommand(command, args, options = {}) {
|
|
28
|
+
const metadata = commandMetadata(command);
|
|
29
|
+
if (!metadata.recordable) return;
|
|
30
|
+
appendActivity({
|
|
31
|
+
type: "tool.issued",
|
|
32
|
+
command: metadata.name,
|
|
33
|
+
argsRedacted: redactCommandArgs(command, args, options.includeInputValues === true),
|
|
34
|
+
effect: metadata.effect,
|
|
35
|
+
...(options.tabId ? { tabId: options.tabId } : {}),
|
|
36
|
+
...(options.origin ? { origin: options.origin } : {}),
|
|
37
|
+
startedAt: new Date().toISOString(),
|
|
38
|
+
}, options);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sinceMilliseconds(value) {
|
|
42
|
+
if (!value) return 60 * 60 * 1000;
|
|
43
|
+
const match = String(value).match(/^(\d+)(m|h|d)$/);
|
|
44
|
+
if (!match) throw new Error("--since must be a duration such as 30m, 1h, or 2d");
|
|
45
|
+
return Number(match[1]) * { m: 60000, h: 3600000, d: 86400000 }[match[2]];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readRecent({ since = "1h", root = getPrivateStateRoot() } = {}) {
|
|
49
|
+
const filePath = journalPath(root);
|
|
50
|
+
const content = readPrivateFile(filePath, { root, allowMissing: true, fallback: "", encoding: "utf8" });
|
|
51
|
+
const cutoff = Date.now() - sinceMilliseconds(since);
|
|
52
|
+
return content.split("\n").filter(Boolean).map((line) => JSON.parse(line)).filter((event) => Date.parse(event.startedAt || event.timestamp) >= cutoff);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = { appendActivity, journalCommand, journalPath, readRecent };
|
|
@@ -658,6 +658,7 @@ async function query(options) {
|
|
|
658
658
|
cdpEvaluate,
|
|
659
659
|
cdpCommand,
|
|
660
660
|
uploadFile,
|
|
661
|
+
beforeSubmit,
|
|
661
662
|
log = () => {},
|
|
662
663
|
signal,
|
|
663
664
|
} = options;
|
|
@@ -729,6 +730,7 @@ async function query(options) {
|
|
|
729
730
|
await typePrompt(cdp, inputCdp, prompt, signal);
|
|
730
731
|
log("Prompt typed");
|
|
731
732
|
const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
|
|
733
|
+
if (beforeSubmit) await raceAbort(beforeSubmit, signal);
|
|
732
734
|
await clickSend(cdp, inputCdp, signal);
|
|
733
735
|
log("Prompt sent, waiting for response...");
|
|
734
736
|
const response = await waitForResponse(
|
package/native/cli.cjs
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
const fs = require("fs");
|
|
3
3
|
const path = require("path");
|
|
4
|
-
const os = require("os");
|
|
5
4
|
const { execFileSync, execSync } = require("child_process");
|
|
6
5
|
const { loadConfig, getConfigPath, createStarterConfig } = require("./config.cjs");
|
|
7
6
|
const networkFormatters = require("./formatters/network.cjs");
|
|
8
|
-
const
|
|
9
|
-
|
|
7
|
+
const {
|
|
8
|
+
applyArgDefaults,
|
|
9
|
+
formatStep,
|
|
10
|
+
getWorkflowDirs,
|
|
11
|
+
getWorkflowInfo,
|
|
12
|
+
listWorkflows,
|
|
13
|
+
normalizeWorkflow,
|
|
14
|
+
parseDoCommands,
|
|
15
|
+
resolveWorkflow,
|
|
16
|
+
validateWorkflowArgs,
|
|
17
|
+
validateWorkflowFile,
|
|
18
|
+
} = require("./workflow-definition.cjs");
|
|
10
19
|
const { executeDoSteps } = require("./do-executor.cjs");
|
|
11
20
|
const { openClientTransport } = require("./client-transport.cjs");
|
|
12
21
|
const { version: VERSION } = require("../package.json");
|
|
22
|
+
const { formatPlaybookOutput, handlePlaybookCli, playbookCommandNeedsBrowser } = require("./playbook-cli.cjs");
|
|
13
23
|
|
|
14
24
|
const IS_WIN = process.platform === "win32";
|
|
15
25
|
const { SURF_TMP, formatSocketError } = require("./socket-path.cjs");
|
|
@@ -63,249 +73,6 @@ function installBrowserLock({ noLock, timeoutMs }, endpoint) {
|
|
|
63
73
|
});
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
// ============================================================================
|
|
67
|
-
// Workflow Resolution and Management
|
|
68
|
-
// ============================================================================
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Get workflow search directories
|
|
72
|
-
* @returns {Array<{path: string, scope: string}>}
|
|
73
|
-
*/
|
|
74
|
-
function getWorkflowDirs() {
|
|
75
|
-
return [
|
|
76
|
-
{ path: path.join(process.cwd(), '.surf', 'workflows'), scope: 'project' },
|
|
77
|
-
{ path: path.join(os.homedir(), '.surf', 'workflows'), scope: 'user' },
|
|
78
|
-
];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Resolve a workflow by name or path
|
|
83
|
-
* @param {string} nameOrPath - Workflow name or file path
|
|
84
|
-
* @returns {{ type: 'inline'|'file'|'not_found', content?: string, path?: string, name?: string }}
|
|
85
|
-
*/
|
|
86
|
-
function resolveWorkflow(nameOrPath) {
|
|
87
|
-
// Check if it's an inline workflow (contains pipe)
|
|
88
|
-
if (nameOrPath.includes('|')) {
|
|
89
|
-
return { type: 'inline', content: nameOrPath };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Check if it's a direct file path (with extension or path separator)
|
|
93
|
-
if (nameOrPath.includes('/') || nameOrPath.includes('\\') || nameOrPath.endsWith('.json')) {
|
|
94
|
-
if (fs.existsSync(nameOrPath)) {
|
|
95
|
-
return { type: 'file', path: nameOrPath };
|
|
96
|
-
}
|
|
97
|
-
return { type: 'not_found', name: nameOrPath };
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Look up by name in workflow directories
|
|
101
|
-
const searchDirs = getWorkflowDirs();
|
|
102
|
-
|
|
103
|
-
for (const { path: dir } of searchDirs) {
|
|
104
|
-
const filePath = path.join(dir, `${nameOrPath}.json`);
|
|
105
|
-
if (fs.existsSync(filePath)) {
|
|
106
|
-
return { type: 'file', path: filePath };
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { type: 'not_found', name: nameOrPath };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* List all available workflows
|
|
115
|
-
* @returns {Array<{name: string, description: string, scope: string, path: string, args?: object}>}
|
|
116
|
-
*/
|
|
117
|
-
function listWorkflows() {
|
|
118
|
-
const workflows = [];
|
|
119
|
-
const searchDirs = getWorkflowDirs();
|
|
120
|
-
|
|
121
|
-
for (const { path: dir, scope } of searchDirs) {
|
|
122
|
-
if (fs.existsSync(dir)) {
|
|
123
|
-
try {
|
|
124
|
-
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
125
|
-
for (const file of files) {
|
|
126
|
-
const filePath = path.join(dir, file);
|
|
127
|
-
try {
|
|
128
|
-
const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
129
|
-
workflows.push({
|
|
130
|
-
name: content.name || file.replace('.json', ''),
|
|
131
|
-
description: content.description || '',
|
|
132
|
-
scope,
|
|
133
|
-
path: filePath,
|
|
134
|
-
args: content.args,
|
|
135
|
-
stepCount: content.steps?.length || 0,
|
|
136
|
-
});
|
|
137
|
-
} catch {
|
|
138
|
-
// Skip invalid JSON files
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
} catch {
|
|
142
|
-
// Skip inaccessible directories
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return workflows;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Get detailed info about a workflow
|
|
152
|
-
* @param {string} name - Workflow name
|
|
153
|
-
* @returns {{ error?: string, name?: string, description?: string, args?: object, steps?: Array, path?: string }}
|
|
154
|
-
*/
|
|
155
|
-
function getWorkflowInfo(name) {
|
|
156
|
-
const resolved = resolveWorkflow(name);
|
|
157
|
-
|
|
158
|
-
if (resolved.type === 'not_found') {
|
|
159
|
-
return { error: `Workflow not found: ${name}` };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (resolved.type === 'inline') {
|
|
163
|
-
return { error: 'Cannot get info for inline workflows' };
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const content = JSON.parse(fs.readFileSync(resolved.path, 'utf8'));
|
|
168
|
-
return {
|
|
169
|
-
name: content.name || name,
|
|
170
|
-
description: content.description || '',
|
|
171
|
-
args: content.args || {},
|
|
172
|
-
steps: content.steps || [],
|
|
173
|
-
path: resolved.path,
|
|
174
|
-
};
|
|
175
|
-
} catch (e) {
|
|
176
|
-
return { error: `Failed to parse workflow: ${e.message}` };
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Validate workflow args against schema
|
|
182
|
-
* @param {object} workflow - Workflow with args schema
|
|
183
|
-
* @param {object} providedArgs - User-provided args
|
|
184
|
-
* @returns {string[]} - Array of error messages
|
|
185
|
-
*/
|
|
186
|
-
function validateWorkflowArgs(workflow, providedArgs) {
|
|
187
|
-
const errors = [];
|
|
188
|
-
if (workflow.args) {
|
|
189
|
-
for (const [name, spec] of Object.entries(workflow.args)) {
|
|
190
|
-
if (spec.required && providedArgs[name] === undefined) {
|
|
191
|
-
errors.push(`Missing required argument: --${name}`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
return errors;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Apply default values to workflow args
|
|
200
|
-
* @param {object} workflow - Workflow with args schema
|
|
201
|
-
* @param {object} providedArgs - User-provided args
|
|
202
|
-
* @returns {object} - Args with defaults applied
|
|
203
|
-
*/
|
|
204
|
-
function applyArgDefaults(workflow, providedArgs) {
|
|
205
|
-
const vars = { ...providedArgs };
|
|
206
|
-
if (workflow.args) {
|
|
207
|
-
for (const [name, spec] of Object.entries(workflow.args)) {
|
|
208
|
-
if (vars[name] === undefined && spec.default !== undefined) {
|
|
209
|
-
vars[name] = spec.default;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
return vars;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Validate a workflow JSON file
|
|
218
|
-
* @param {string} filePath - Path to workflow file
|
|
219
|
-
* @returns {{ valid: boolean, error?: string, workflow?: object }}
|
|
220
|
-
*/
|
|
221
|
-
function validateWorkflowFile(filePath) {
|
|
222
|
-
if (!fs.existsSync(filePath)) {
|
|
223
|
-
return { valid: false, error: `File not found: ${filePath}` };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
228
|
-
const workflow = JSON.parse(content);
|
|
229
|
-
|
|
230
|
-
// Basic structure validation
|
|
231
|
-
if (!workflow.steps || !Array.isArray(workflow.steps)) {
|
|
232
|
-
return { valid: false, error: "Workflow must have a 'steps' array" };
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (workflow.steps.length === 0) {
|
|
236
|
-
return { valid: false, error: "Workflow has no steps" };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Validate each step
|
|
240
|
-
for (let i = 0; i < workflow.steps.length; i++) {
|
|
241
|
-
const step = workflow.steps[i];
|
|
242
|
-
|
|
243
|
-
// Check for loops
|
|
244
|
-
if (step.repeat !== undefined || step.each !== undefined) {
|
|
245
|
-
if (!step.steps || !Array.isArray(step.steps)) {
|
|
246
|
-
return { valid: false, error: `Step ${i + 1}: loop must have a 'steps' array` };
|
|
247
|
-
}
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Regular step must have tool/cmd
|
|
252
|
-
if (!step.tool && !step.cmd) {
|
|
253
|
-
return { valid: false, error: `Step ${i + 1}: must have 'tool' field` };
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Validate args schema if present
|
|
258
|
-
if (workflow.args && typeof workflow.args !== 'object') {
|
|
259
|
-
return { valid: false, error: "'args' must be an object" };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
return { valid: true, workflow };
|
|
263
|
-
} catch (e) {
|
|
264
|
-
return { valid: false, error: `Invalid JSON: ${e.message}` };
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* Format a step for display
|
|
270
|
-
* @param {object} step - Workflow step
|
|
271
|
-
* @param {number} indent - Indentation level
|
|
272
|
-
* @returns {string}
|
|
273
|
-
*/
|
|
274
|
-
function formatStep(step, indent = 0) {
|
|
275
|
-
const pad = ' '.repeat(indent);
|
|
276
|
-
|
|
277
|
-
if (step.repeat !== undefined) {
|
|
278
|
-
const lines = [`${pad}repeat ${step.repeat} times:`];
|
|
279
|
-
for (const s of step.steps || []) {
|
|
280
|
-
lines.push(formatStep(s, indent + 1));
|
|
281
|
-
}
|
|
282
|
-
if (step.until) {
|
|
283
|
-
lines.push(`${pad} until: ${step.until.tool || step.until.cmd}`);
|
|
284
|
-
}
|
|
285
|
-
return lines.join('\n');
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (step.each !== undefined) {
|
|
289
|
-
const lines = [`${pad}each ${step.each} as ${step.as || 'item'}:`];
|
|
290
|
-
for (const s of step.steps || []) {
|
|
291
|
-
lines.push(formatStep(s, indent + 1));
|
|
292
|
-
}
|
|
293
|
-
return lines.join('\n');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const tool = step.tool || step.cmd;
|
|
297
|
-
const args = step.args || {};
|
|
298
|
-
const argStr = Object.entries(args)
|
|
299
|
-
.map(([k, v]) => `${k}=${JSON.stringify(v)}`)
|
|
300
|
-
.join(' ');
|
|
301
|
-
|
|
302
|
-
let line = `${pad}${tool}`;
|
|
303
|
-
if (argStr) line += ` ${argStr}`;
|
|
304
|
-
if (step.as) line += ` → ${step.as}`;
|
|
305
|
-
|
|
306
|
-
return line;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
76
|
// Cross-platform image resize (macOS: sips, Linux: ImageMagick)
|
|
310
77
|
function resizeImage(filePath, maxSize) {
|
|
311
78
|
const platform = process.platform;
|
|
@@ -386,6 +153,23 @@ try {
|
|
|
386
153
|
process.exit(1);
|
|
387
154
|
}
|
|
388
155
|
|
|
156
|
+
if (["playbook", "pb", "use"].includes(args[0])) {
|
|
157
|
+
if (playbookCommandNeedsBrowser(args)) {
|
|
158
|
+
installBrowserLock(parseBrowserLockOptions(args.includes("--no-lock")), endpoint);
|
|
159
|
+
}
|
|
160
|
+
handlePlaybookCli(args, { endpoint, cwd: process.cwd() })
|
|
161
|
+
.then((result) => {
|
|
162
|
+
if (!result.handled) throw new Error("Playbook command was not handled");
|
|
163
|
+
if (result.value !== undefined) console.log(formatPlaybookOutput(result.value, result.json));
|
|
164
|
+
process.exit(0);
|
|
165
|
+
})
|
|
166
|
+
.catch((error) => {
|
|
167
|
+
console.error(`Error: ${error.message}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
389
173
|
const ALIASES = {
|
|
390
174
|
snap: "screenshot",
|
|
391
175
|
read: "page.read",
|
|
@@ -951,6 +735,9 @@ const TOOLS = {
|
|
|
951
735
|
all: "Show all (no limit)",
|
|
952
736
|
v: "Verbose output",
|
|
953
737
|
vv: "Very verbose output",
|
|
738
|
+
"body-mode": "Response bodies: none, text, or all (default: text)",
|
|
739
|
+
"per-body-bytes": "Maximum captured bytes per response body",
|
|
740
|
+
"total-body-bytes": "Maximum captured response-body bytes per tab session",
|
|
954
741
|
clear: "Clear after reading",
|
|
955
742
|
stream: "Continuous output"
|
|
956
743
|
},
|
|
@@ -1015,9 +802,17 @@ const TOOLS = {
|
|
|
1015
802
|
"network.export": {
|
|
1016
803
|
desc: "Export captured requests",
|
|
1017
804
|
args: [],
|
|
1018
|
-
opts: {
|
|
805
|
+
opts: {
|
|
806
|
+
har: "Export as HAR 1.2",
|
|
807
|
+
jsonl: "Export as JSONL",
|
|
808
|
+
output: "Output file path",
|
|
809
|
+
"body-mode": "Response bodies: none, text, or all (default: text)",
|
|
810
|
+
"per-body-bytes": "Maximum captured bytes per response body",
|
|
811
|
+
"total-body-bytes": "Maximum captured response-body bytes per tab session",
|
|
812
|
+
},
|
|
1019
813
|
examples: [
|
|
1020
|
-
{ cmd: "network.export --jsonl --output /tmp/requests.jsonl", desc: "Export as JSONL" }
|
|
814
|
+
{ cmd: "network.export --jsonl --output /tmp/requests.jsonl", desc: "Export as JSONL" },
|
|
815
|
+
{ cmd: "network.export --har --output /tmp/requests.har", desc: "Export as HAR" },
|
|
1021
816
|
]
|
|
1022
817
|
},
|
|
1023
818
|
"network.path": {
|
|
@@ -1760,6 +1555,10 @@ const showFullHelp = () => {
|
|
|
1760
1555
|
|
|
1761
1556
|
Usage: surf <command> [args] [options]
|
|
1762
1557
|
|
|
1558
|
+
Playbooks:
|
|
1559
|
+
surf playbook|pb <list|show|ops|run|record|suggest|save|client|trace|export|import>
|
|
1560
|
+
surf use <playbook> <op> [--arg value]
|
|
1561
|
+
|
|
1763
1562
|
`);
|
|
1764
1563
|
for (const [groupName, group] of Object.entries(TOOLS)) {
|
|
1765
1564
|
console.log(`${groupName.toUpperCase()} - ${group.desc}`);
|
|
@@ -2402,9 +2201,7 @@ if (args[0] === "do") {
|
|
|
2402
2201
|
|
|
2403
2202
|
// Process workflow file if loaded
|
|
2404
2203
|
if (workflow) {
|
|
2405
|
-
|
|
2406
|
-
throw new Error("Workflow must have a 'steps' array");
|
|
2407
|
-
}
|
|
2204
|
+
workflow = normalizeWorkflow(workflow);
|
|
2408
2205
|
|
|
2409
2206
|
// Validate required args
|
|
2410
2207
|
const argErrors = validateWorkflowArgs(workflow, workflowArgs);
|
|
@@ -2424,30 +2221,7 @@ if (args[0] === "do") {
|
|
|
2424
2221
|
process.exit(1);
|
|
2425
2222
|
}
|
|
2426
2223
|
|
|
2427
|
-
|
|
2428
|
-
// Also preserve loop steps as-is
|
|
2429
|
-
steps = workflow.steps.map(s => {
|
|
2430
|
-
if (s.repeat !== undefined || s.each !== undefined) {
|
|
2431
|
-
// Loop step - convert nested steps recursively
|
|
2432
|
-
const convertSteps = (stepsArr) => stepsArr.map(ns => {
|
|
2433
|
-
if (ns.repeat !== undefined || ns.each !== undefined) {
|
|
2434
|
-
// Recursively convert nested loop steps and until condition
|
|
2435
|
-
return {
|
|
2436
|
-
...ns,
|
|
2437
|
-
steps: convertSteps(ns.steps || []),
|
|
2438
|
-
until: ns.until ? { cmd: ns.until.tool || ns.until.cmd, args: ns.until.args || {} } : undefined
|
|
2439
|
-
};
|
|
2440
|
-
}
|
|
2441
|
-
return { cmd: ns.tool || ns.cmd, args: ns.args || {}, as: ns.as };
|
|
2442
|
-
});
|
|
2443
|
-
return {
|
|
2444
|
-
...s,
|
|
2445
|
-
steps: convertSteps(s.steps || []),
|
|
2446
|
-
until: s.until ? { cmd: s.until.tool || s.until.cmd, args: s.until.args || {} } : undefined
|
|
2447
|
-
};
|
|
2448
|
-
}
|
|
2449
|
-
return { cmd: s.tool || s.cmd, args: s.args || {}, as: s.as };
|
|
2450
|
-
});
|
|
2224
|
+
steps = workflow.steps;
|
|
2451
2225
|
}
|
|
2452
2226
|
} catch (e) {
|
|
2453
2227
|
console.error(`Error: Failed to parse workflow: ${e.message}`);
|
|
@@ -2948,9 +2722,9 @@ if (toolArgs["window-id"] !== undefined) {
|
|
|
2948
2722
|
globalOpts.windowId = wid;
|
|
2949
2723
|
delete toolArgs["window-id"];
|
|
2950
2724
|
}
|
|
2951
|
-
if (toolArgs["network-path"] !== undefined) {
|
|
2952
|
-
|
|
2953
|
-
|
|
2725
|
+
if (toolArgs["network-path"] !== undefined && typeof toolArgs["network-path"] !== "string") {
|
|
2726
|
+
console.error("Error: --network-path requires a directory");
|
|
2727
|
+
process.exit(1);
|
|
2954
2728
|
}
|
|
2955
2729
|
const wantJson = toolArgs.json === true;
|
|
2956
2730
|
delete toolArgs.json;
|