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
package/native/network-store.cjs
CHANGED
|
@@ -11,11 +11,17 @@ const fs = require("fs");
|
|
|
11
11
|
const path = require("path");
|
|
12
12
|
const crypto = require("crypto");
|
|
13
13
|
const readline = require("readline");
|
|
14
|
+
const {
|
|
15
|
+
appendPrivateJsonLine,
|
|
16
|
+
assertNotSymlink,
|
|
17
|
+
atomicWriteFile,
|
|
18
|
+
atomicWriteJson,
|
|
19
|
+
ensurePrivateDir,
|
|
20
|
+
privateStatePath,
|
|
21
|
+
} = require("./private-state.cjs");
|
|
14
22
|
|
|
15
23
|
// Configuration
|
|
16
|
-
const DEFAULT_BASE =
|
|
17
|
-
? require("path").join(require("os").tmpdir(), "surf")
|
|
18
|
-
: "/tmp/surf";
|
|
24
|
+
const DEFAULT_BASE = privateStatePath("network");
|
|
19
25
|
const DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
|
20
26
|
const DEFAULT_MAX_SIZE = 200 * 1024 * 1024; // 200MB
|
|
21
27
|
const AUTO_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
@@ -23,36 +29,26 @@ const AUTO_CLEANUP_INTERVAL = 60 * 60 * 1000; // 1 hour
|
|
|
23
29
|
// Lock file for concurrent access
|
|
24
30
|
let writeLock = Promise.resolve();
|
|
25
31
|
|
|
26
|
-
// Runtime override for base path (set via CLI --network-path)
|
|
27
|
-
let runtimeBasePath = null;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Set base path at runtime (from CLI --network-path flag)
|
|
31
|
-
*/
|
|
32
|
-
function setBasePath(newPath) {
|
|
33
|
-
runtimeBasePath = newPath;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
32
|
/**
|
|
37
33
|
* Get base path for network storage
|
|
38
|
-
* Priority:
|
|
34
|
+
* Priority: SURF_NETWORK_PATH env var > default
|
|
39
35
|
*/
|
|
40
|
-
function getBasePath() {
|
|
41
|
-
return
|
|
36
|
+
function getBasePath(basePath) {
|
|
37
|
+
return basePath || process.env.SURF_NETWORK_PATH || DEFAULT_BASE;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
40
|
/**
|
|
45
41
|
* Get path to requests.jsonl
|
|
46
42
|
*/
|
|
47
|
-
function getRequestsPath() {
|
|
48
|
-
return path.join(getBasePath(), "requests.jsonl");
|
|
43
|
+
function getRequestsPath(basePath) {
|
|
44
|
+
return path.join(getBasePath(basePath), "requests.jsonl");
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
/**
|
|
52
48
|
* Get path to bodies directory
|
|
53
49
|
*/
|
|
54
|
-
function getBodiesPath() {
|
|
55
|
-
return path.join(getBasePath(), "bodies");
|
|
50
|
+
function getBodiesPath(basePath) {
|
|
51
|
+
return path.join(getBasePath(basePath), "bodies");
|
|
56
52
|
}
|
|
57
53
|
|
|
58
54
|
/**
|
|
@@ -65,16 +61,11 @@ function getMetaPath() {
|
|
|
65
61
|
/**
|
|
66
62
|
* Ensure all required directories exist
|
|
67
63
|
*/
|
|
68
|
-
function ensureDirectories() {
|
|
69
|
-
const base = getBasePath();
|
|
70
|
-
const bodies = getBodiesPath();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fs.mkdirSync(base, { recursive: true });
|
|
74
|
-
}
|
|
75
|
-
if (!fs.existsSync(bodies)) {
|
|
76
|
-
fs.mkdirSync(bodies, { recursive: true });
|
|
77
|
-
}
|
|
64
|
+
function ensureDirectories(basePath) {
|
|
65
|
+
const base = getBasePath(basePath);
|
|
66
|
+
const bodies = getBodiesPath(basePath);
|
|
67
|
+
ensurePrivateDir(base, base);
|
|
68
|
+
ensurePrivateDir(bodies, base);
|
|
78
69
|
}
|
|
79
70
|
|
|
80
71
|
/**
|
|
@@ -84,6 +75,7 @@ function readMeta() {
|
|
|
84
75
|
const metaPath = getMetaPath();
|
|
85
76
|
try {
|
|
86
77
|
if (fs.existsSync(metaPath)) {
|
|
78
|
+
assertNotSymlink(metaPath, false);
|
|
87
79
|
return JSON.parse(fs.readFileSync(metaPath, "utf-8"));
|
|
88
80
|
}
|
|
89
81
|
} catch (err) {
|
|
@@ -98,7 +90,7 @@ function readMeta() {
|
|
|
98
90
|
function writeMeta(meta) {
|
|
99
91
|
const metaPath = getMetaPath();
|
|
100
92
|
ensureDirectories();
|
|
101
|
-
|
|
93
|
+
atomicWriteJson(metaPath, meta, { root: getBasePath() });
|
|
102
94
|
}
|
|
103
95
|
|
|
104
96
|
/**
|
|
@@ -124,7 +116,7 @@ function storeBody(content, isRequest = false) {
|
|
|
124
116
|
|
|
125
117
|
// Only write if doesn't exist (dedup)
|
|
126
118
|
if (!fs.existsSync(bodyPath)) {
|
|
127
|
-
|
|
119
|
+
atomicWriteFile(bodyPath, buffer, { root: getBasePath() });
|
|
128
120
|
}
|
|
129
121
|
|
|
130
122
|
return hash;
|
|
@@ -142,6 +134,7 @@ function readBody(hash, isRequest = false) {
|
|
|
142
134
|
|
|
143
135
|
try {
|
|
144
136
|
if (fs.existsSync(bodyPath)) {
|
|
137
|
+
assertNotSymlink(bodyPath, false);
|
|
145
138
|
return fs.readFileSync(bodyPath);
|
|
146
139
|
}
|
|
147
140
|
} catch (err) {
|
|
@@ -186,10 +179,7 @@ async function appendEntry(entry) {
|
|
|
186
179
|
...entry
|
|
187
180
|
};
|
|
188
181
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Atomic append using flag 'a'
|
|
192
|
-
fs.appendFileSync(getRequestsPath(), line, { flag: "a" });
|
|
182
|
+
appendPrivateJsonLine(getRequestsPath(), fullEntry, { root: getBasePath() });
|
|
193
183
|
|
|
194
184
|
return fullEntry;
|
|
195
185
|
} finally {
|
|
@@ -202,8 +192,8 @@ async function appendEntry(entry) {
|
|
|
202
192
|
* @param {Object} entry - Network entry to append
|
|
203
193
|
* @returns {Object} The entry with assigned ID
|
|
204
194
|
*/
|
|
205
|
-
function appendEntrySync(entry) {
|
|
206
|
-
ensureDirectories();
|
|
195
|
+
function appendEntrySync(entry, basePath) {
|
|
196
|
+
ensureDirectories(basePath);
|
|
207
197
|
|
|
208
198
|
const id = entry.id || generateId();
|
|
209
199
|
const timestamp = entry.timestamp || Date.now();
|
|
@@ -214,15 +204,14 @@ function appendEntrySync(entry) {
|
|
|
214
204
|
...entry
|
|
215
205
|
};
|
|
216
206
|
|
|
217
|
-
const line = JSON.stringify(fullEntry) + "\n";
|
|
218
|
-
|
|
219
207
|
// Use a simple lock file for synchronous operations
|
|
220
|
-
const lockPath = path.join(getBasePath(), ".lock");
|
|
208
|
+
const lockPath = path.join(getBasePath(basePath), ".lock");
|
|
221
209
|
let lockFd;
|
|
222
210
|
|
|
223
211
|
try {
|
|
224
212
|
// Try to acquire lock
|
|
225
|
-
|
|
213
|
+
assertNotSymlink(lockPath, true);
|
|
214
|
+
lockFd = fs.openSync(lockPath, "wx", 0o600);
|
|
226
215
|
} catch (err) {
|
|
227
216
|
// Lock exists - check if stale and remove, otherwise proceed without lock
|
|
228
217
|
try {
|
|
@@ -230,7 +219,7 @@ function appendEntrySync(entry) {
|
|
|
230
219
|
if (Date.now() - stat.mtimeMs > 5000) {
|
|
231
220
|
fs.unlinkSync(lockPath);
|
|
232
221
|
try {
|
|
233
|
-
lockFd = fs.openSync(lockPath, "wx");
|
|
222
|
+
lockFd = fs.openSync(lockPath, "wx", 0o600);
|
|
234
223
|
} catch (e) {
|
|
235
224
|
// Still can't get lock, proceed without it
|
|
236
225
|
}
|
|
@@ -241,13 +230,13 @@ function appendEntrySync(entry) {
|
|
|
241
230
|
|
|
242
231
|
if (lockFd === undefined) {
|
|
243
232
|
// Proceed without lock as fallback
|
|
244
|
-
|
|
233
|
+
appendPrivateJsonLine(getRequestsPath(basePath), fullEntry, { root: getBasePath(basePath) });
|
|
245
234
|
return fullEntry;
|
|
246
235
|
}
|
|
247
236
|
}
|
|
248
237
|
|
|
249
238
|
try {
|
|
250
|
-
|
|
239
|
+
appendPrivateJsonLine(getRequestsPath(basePath), fullEntry, { root: getBasePath(basePath) });
|
|
251
240
|
} finally {
|
|
252
241
|
if (lockFd !== undefined) {
|
|
253
242
|
fs.closeSync(lockFd);
|
|
@@ -385,6 +374,7 @@ async function readEntries(filters = {}) {
|
|
|
385
374
|
if (!fs.existsSync(requestsPath)) {
|
|
386
375
|
return [];
|
|
387
376
|
}
|
|
377
|
+
assertNotSymlink(requestsPath, false);
|
|
388
378
|
|
|
389
379
|
const { last } = filters;
|
|
390
380
|
const entries = [];
|
|
@@ -433,6 +423,7 @@ function readEntriesSync(filters = {}) {
|
|
|
433
423
|
if (!fs.existsSync(requestsPath)) {
|
|
434
424
|
return [];
|
|
435
425
|
}
|
|
426
|
+
assertNotSymlink(requestsPath, false);
|
|
436
427
|
|
|
437
428
|
const { last } = filters;
|
|
438
429
|
const entries = [];
|
|
@@ -682,10 +673,8 @@ async function cleanup(options = {}) {
|
|
|
682
673
|
|
|
683
674
|
if (deletedEntries > 0 || entries.length === 0) {
|
|
684
675
|
// Atomic write: write to temp then rename
|
|
685
|
-
const tempPath = requestsPath + ".tmp";
|
|
686
676
|
const content = entries.map(e => JSON.stringify(e)).join("\n") + (entries.length > 0 ? "\n" : "");
|
|
687
|
-
|
|
688
|
-
fs.renameSync(tempPath, requestsPath);
|
|
677
|
+
atomicWriteFile(requestsPath, content, { root: getBasePath(), encoding: "utf8" });
|
|
689
678
|
}
|
|
690
679
|
|
|
691
680
|
// 6. Update meta
|
|
@@ -777,10 +766,8 @@ async function clear(options = {}) {
|
|
|
777
766
|
|
|
778
767
|
// Rewrite entries file
|
|
779
768
|
if (deletedEntries > 0) {
|
|
780
|
-
const tempPath = requestsPath + ".tmp";
|
|
781
769
|
const content = remaining.map(e => JSON.stringify(e)).join("\n") + (remaining.length > 0 ? "\n" : "");
|
|
782
|
-
|
|
783
|
-
fs.renameSync(tempPath, requestsPath);
|
|
770
|
+
atomicWriteFile(requestsPath, content, { root: getBasePath(), encoding: "utf8" });
|
|
784
771
|
}
|
|
785
772
|
|
|
786
773
|
return { deletedEntries, deletedBodies };
|
|
@@ -807,9 +794,6 @@ function maybeAutoCleanup() {
|
|
|
807
794
|
}
|
|
808
795
|
}
|
|
809
796
|
|
|
810
|
-
// Run auto-cleanup check on module load
|
|
811
|
-
maybeAutoCleanup();
|
|
812
|
-
|
|
813
797
|
module.exports = {
|
|
814
798
|
// Configuration
|
|
815
799
|
getBasePath,
|
|
@@ -841,10 +825,6 @@ module.exports = {
|
|
|
841
825
|
clear,
|
|
842
826
|
maybeAutoCleanup,
|
|
843
827
|
|
|
844
|
-
// Configuration
|
|
845
|
-
setBasePath,
|
|
846
|
-
getBasePath,
|
|
847
|
-
|
|
848
828
|
// Constants
|
|
849
829
|
DEFAULT_BASE,
|
|
850
830
|
DEFAULT_TTL,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { readRecent } = require("./activity-journal.cjs");
|
|
4
|
+
const { writeNetworkExport } = require("./network-export.cjs");
|
|
5
|
+
const { getPrivateStateRoot, readPrivateJson } = require("./private-state.cjs");
|
|
6
|
+
const { draftFromRecord, readRecord, recordsRoot } = require("./playbook-records.cjs");
|
|
7
|
+
const { savePlaybook, validateOp } = require("./playbooks.cjs");
|
|
8
|
+
const { commandMetadata, promoteRedactedStepArgs } = require("./workflow-definition.cjs");
|
|
9
|
+
|
|
10
|
+
function suggestions({ since = "1h", root } = {}) {
|
|
11
|
+
const events = readRecent({ since, root });
|
|
12
|
+
const counts = new Map();
|
|
13
|
+
for (const event of events.filter((entry) => entry.type === "tool.issued")) counts.set(event.command, (counts.get(event.command) || 0) + 1);
|
|
14
|
+
return [...counts.entries()].map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count || a.command.localeCompare(b.command));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function saveFromRecent({ site, op: opId, since = "1h", scope = "user", cwd, home, root }) {
|
|
18
|
+
const events = readRecent({ since, root }).filter((event) => event.type === "tool.issued");
|
|
19
|
+
if (events.length === 0) throw new Error("no recent Surf activity to save");
|
|
20
|
+
if (events.some((event) => ["page-write", "unknown"].includes(commandMetadata(event.command).effect))) {
|
|
21
|
+
throw new Error("recent activity includes write-capable commands; use an explicit record and review its draft before saving");
|
|
22
|
+
}
|
|
23
|
+
const promoted = promoteRedactedStepArgs(events.map((event) => ({ tool: event.command, args: event.argsRedacted || {} })));
|
|
24
|
+
const op = { id: opId, description: "Drafted from recent Surf activity", effect: "read", args: promoted.args, run: [{ using: "workflow", steps: promoted.steps }], provenance: { recentSince: since } };
|
|
25
|
+
validateOp(op, { origins: [] });
|
|
26
|
+
return savePlaybook({ manifest: { id: site, name: site, version: "1.0.0", origins: [] }, op, scope, cwd, home });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveFromRecord({ recordId, scope = "user", cwd, home, root = getPrivateStateRoot() }) {
|
|
30
|
+
const record = readRecord(recordId, root);
|
|
31
|
+
if (!record) throw new Error(`record not found: ${recordId}`);
|
|
32
|
+
const draftPath = path.join(recordsRoot(root), recordId, "draft", "op.json");
|
|
33
|
+
const op = fs.existsSync(draftPath) ? readPrivateJson(draftPath, null, { root }) : draftFromRecord(recordId, root);
|
|
34
|
+
validateOp(op, { origins: record.origin ? [record.origin] : [] });
|
|
35
|
+
return savePlaybook({ manifest: { id: record.site, name: record.site, version: "1.0.0", origins: record.origin ? [record.origin] : [] }, op, scope, cwd, home });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function exportRecordHar(recordId, output, root = getPrivateStateRoot()) {
|
|
39
|
+
const trace = readPrivateJson(path.join(recordsRoot(root), recordId, "network", "trace.json"), null, { root });
|
|
40
|
+
if (!trace) throw new Error(`record ${recordId} has no network trace`);
|
|
41
|
+
return writeNetworkExport(path.resolve(output), trace.entries, "har");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { exportRecordHar, saveFromRecent, saveFromRecord, suggestions };
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const { openClientTransport } = require("./client-transport.cjs");
|
|
3
|
+
const { resolveRequestDeadlineMs } = require("./host-sessions.cjs");
|
|
4
|
+
const { exportRecordHar, saveFromRecent, saveFromRecord, suggestions } = require("./playbook-authoring.cjs");
|
|
5
|
+
const { deriveClient, exportClient, verifyClient } = require("./playbook-client.cjs");
|
|
6
|
+
const { exportPlaybookDirectory, importPlaybookDirectory, listPlaybooks, resolvePlaybook } = require("./playbooks.cjs");
|
|
7
|
+
|
|
8
|
+
function parseCommandArgs(argv) {
|
|
9
|
+
const positional = [];
|
|
10
|
+
const options = {};
|
|
11
|
+
for (let index = 0; index < argv.length; index++) {
|
|
12
|
+
const value = argv[index];
|
|
13
|
+
if (!value.startsWith("--")) {
|
|
14
|
+
positional.push(value);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const name = value.slice(2);
|
|
18
|
+
const next = argv[index + 1];
|
|
19
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
20
|
+
options[name] = /^-?\d+(?:\.\d+)?$/.test(next) ? Number(next) : next;
|
|
21
|
+
index++;
|
|
22
|
+
} else options[name] = true;
|
|
23
|
+
}
|
|
24
|
+
return { positional, options };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function unwrapResponse(response) {
|
|
28
|
+
if (response.error) throw new Error(response.error.content?.[0]?.text || JSON.stringify(response.error));
|
|
29
|
+
const text = response.result?.content?.[0]?.text;
|
|
30
|
+
if (text === undefined) return response.result;
|
|
31
|
+
try { return JSON.parse(text); } catch { return text; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function requestHost(endpoint, tool, args, options = {}) {
|
|
35
|
+
const transport = await openClientTransport(endpoint, { requestTimeoutMs: options.timeoutMs || 11 * 60 * 1000 });
|
|
36
|
+
try {
|
|
37
|
+
const request = { type: "tool_request", method: "execute_tool", params: { tool, args }, id: `playbook-${Date.now()}-${Math.random()}` };
|
|
38
|
+
if (options.tabId) request.tabId = options.tabId;
|
|
39
|
+
return unwrapResponse(await transport.request(request, options.timeoutMs || 11 * 60 * 1000));
|
|
40
|
+
} finally {
|
|
41
|
+
await transport.close();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function runSpec(argv) {
|
|
46
|
+
const direct = argv[0] === "use";
|
|
47
|
+
const offset = direct ? 1 : 2;
|
|
48
|
+
const parsed = parseCommandArgs(argv.slice(offset));
|
|
49
|
+
const [playbook, op] = parsed.positional;
|
|
50
|
+
if (!playbook || !op) throw new Error(direct ? "Usage: surf use <playbook> <op> [--arg value]" : "Usage: surf pb run <playbook> <op> [--arg value]");
|
|
51
|
+
const reserved = new Set(["json", "no-lock", "tab-id", "write", "repeat", "retry-attempt", "override-in-doubt", "pin-built-in"]);
|
|
52
|
+
const args = Object.fromEntries(Object.entries(parsed.options).filter(([name]) => !reserved.has(name)));
|
|
53
|
+
return { playbook, op, args, options: parsed.options };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveRunTimeout(spec, cwd) {
|
|
57
|
+
const explicit = Number(spec.args.timeout);
|
|
58
|
+
if (Number.isFinite(explicit) && explicit > 0) return explicit;
|
|
59
|
+
try {
|
|
60
|
+
const playbook = resolvePlaybook(spec.playbook, { cwd, pinBuiltIn: spec.options["pin-built-in"] === true });
|
|
61
|
+
const op = playbook.ops.get(spec.op);
|
|
62
|
+
const value = Number(op?.args?.timeout?.default);
|
|
63
|
+
return Number.isFinite(value) && value > 0 ? value : undefined;
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function playbookCommandNeedsBrowser(argv) {
|
|
70
|
+
if (argv[0] === "use") return true;
|
|
71
|
+
const subcommand = argv[1];
|
|
72
|
+
if (subcommand === "run") return true;
|
|
73
|
+
return subcommand === "record" && ["start", "stop", "discard"].includes(argv[2]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function handlePlaybookCli(argv, { endpoint, cwd = process.cwd() }) {
|
|
77
|
+
if (!["playbook", "pb", "use"].includes(argv[0])) return { handled: false };
|
|
78
|
+
if (argv[0] === "use" || argv[1] === "run") {
|
|
79
|
+
const spec = runSpec(argv);
|
|
80
|
+
const timeout = resolveRunTimeout(spec, cwd);
|
|
81
|
+
const args = {
|
|
82
|
+
playbook: spec.playbook,
|
|
83
|
+
op: spec.op,
|
|
84
|
+
args: spec.args,
|
|
85
|
+
projectDir: cwd,
|
|
86
|
+
...(timeout ? { timeout } : {}),
|
|
87
|
+
write: spec.options.write === true,
|
|
88
|
+
repeat: spec.options.repeat === true,
|
|
89
|
+
retryAttempt: spec.options["retry-attempt"],
|
|
90
|
+
overrideInDoubt: spec.options["override-in-doubt"] === true,
|
|
91
|
+
pinBuiltIn: spec.options["pin-built-in"] === true,
|
|
92
|
+
};
|
|
93
|
+
const value = await requestHost(endpoint, "playbook.run", args, {
|
|
94
|
+
tabId: spec.options["tab-id"],
|
|
95
|
+
timeoutMs: resolveRequestDeadlineMs("playbook.run", args),
|
|
96
|
+
});
|
|
97
|
+
return { handled: true, value, json: spec.options.json === true };
|
|
98
|
+
}
|
|
99
|
+
const command = argv[1];
|
|
100
|
+
const parsed = parseCommandArgs(argv.slice(2));
|
|
101
|
+
if (!command || command === "help") return { handled: true, value: "Usage: surf playbook|pb <list|show|ops|run|record|suggest|save|client|trace|export|import>" };
|
|
102
|
+
if (endpoint?.kind === "remote" && ["list", "show", "ops"].includes(command)) throw new Error(`playbook ${command} is local-only with --remote because runs resolve on the browser host`);
|
|
103
|
+
if (command === "list") return { handled: true, value: listPlaybooks({ cwd }), json: parsed.options.json === true };
|
|
104
|
+
if (command === "show") {
|
|
105
|
+
const playbook = resolvePlaybook(parsed.positional[0], { cwd });
|
|
106
|
+
return { handled: true, value: { id: playbook.id, name: playbook.name, version: playbook.version, description: playbook.description, origins: playbook.origins, provenance: playbook.provenance, ops: [...playbook.ops.keys()] }, json: parsed.options.json === true };
|
|
107
|
+
}
|
|
108
|
+
if (command === "ops") {
|
|
109
|
+
const playbook = resolvePlaybook(parsed.positional[0], { cwd });
|
|
110
|
+
return { handled: true, value: [...playbook.ops.values()].map((op) => ({ id: op.id, description: op.description || "", effect: op.effect, strategies: op.run.map((strategy) => strategy.using) })), json: parsed.options.json === true };
|
|
111
|
+
}
|
|
112
|
+
if (command === "record") {
|
|
113
|
+
const action = parsed.positional[0];
|
|
114
|
+
const tool = `playbook.record.${action}`;
|
|
115
|
+
let args = {};
|
|
116
|
+
if (action === "start") args = { site: parsed.positional[1], op: parsed.options.op, watch: parsed.options.watch === true, network: parsed.options.network === true, includeInputValues: parsed.options["include-input-values"] === true };
|
|
117
|
+
else if (action === "mark") args = { label: parsed.positional.slice(1).join(" ") };
|
|
118
|
+
else if (action === "stop") args = { draft: parsed.options.draft === true };
|
|
119
|
+
else if (!["status", "pause", "resume", "discard"].includes(action)) throw new Error("Unknown record command");
|
|
120
|
+
const value = await requestHost(endpoint, tool, args, { tabId: parsed.options["tab-id"] });
|
|
121
|
+
return { handled: true, value, json: parsed.options.json === true };
|
|
122
|
+
}
|
|
123
|
+
if (command === "suggest") return { handled: true, value: suggestions({ since: parsed.options.since || "1h" }), json: parsed.options.json === true };
|
|
124
|
+
if (command === "save") {
|
|
125
|
+
let value;
|
|
126
|
+
if (parsed.options["from-record"]) value = saveFromRecord({ recordId: parsed.options["from-record"], scope: parsed.options.project ? "project" : "user", cwd });
|
|
127
|
+
else if (parsed.options["from-recent"] || parsed.positional[0]) value = saveFromRecent({ site: parsed.positional[0], op: parsed.options.op, since: parsed.options["from-recent"] === true ? "1h" : parsed.options["from-recent"] || "1h", scope: parsed.options.project ? "project" : "user", cwd });
|
|
128
|
+
else throw new Error("save requires --from-record <id> or <site> --op <name> --from-recent");
|
|
129
|
+
return { handled: true, value, json: parsed.options.json === true };
|
|
130
|
+
}
|
|
131
|
+
if (command === "client") {
|
|
132
|
+
const action = parsed.positional[0];
|
|
133
|
+
if (action === "derive") return { handled: true, value: deriveClient(parsed.positional[1], parsed.options.op, parsed.options.out, { recordId: parsed.options["from-record"], requestId: parsed.options["request-id"] }), json: parsed.options.json === true };
|
|
134
|
+
if (action === "export") {
|
|
135
|
+
const playbook = parsed.positional[1];
|
|
136
|
+
const resolved = resolvePlaybook(playbook, { cwd });
|
|
137
|
+
const op = parsed.options.op || [...resolved.ops.keys()][0];
|
|
138
|
+
return { handled: true, value: exportClient(playbook, op, parsed.options.out, { cwd }), json: parsed.options.json === true };
|
|
139
|
+
}
|
|
140
|
+
if (action === "verify") return { handled: true, value: await verifyClient(parsed.positional[1], { live: parsed.options.live === true ? true : undefined }), json: parsed.options.json === true };
|
|
141
|
+
throw new Error("Unknown client command");
|
|
142
|
+
}
|
|
143
|
+
if (command === "trace" && parsed.positional[0] === "export") {
|
|
144
|
+
if (!parsed.options["from-record"] || !parsed.options.har) throw new Error("trace export requires --from-record <id> --har <path>");
|
|
145
|
+
return { handled: true, value: exportRecordHar(parsed.options["from-record"], path.resolve(parsed.options.har)), json: parsed.options.json === true };
|
|
146
|
+
}
|
|
147
|
+
if (command === "export") return { handled: true, value: exportPlaybookDirectory(parsed.positional[0], { out: parsed.options.out, cwd }), json: parsed.options.json === true };
|
|
148
|
+
if (command === "import") return { handled: true, value: importPlaybookDirectory(parsed.positional[0], { scope: parsed.options.project ? "project" : "user", cwd }), json: parsed.options.json === true };
|
|
149
|
+
throw new Error(`Unknown playbook command: ${command}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatPlaybookOutput(value, json = false) {
|
|
153
|
+
if (json || typeof value !== "string") return JSON.stringify(value, null, 2);
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { formatPlaybookOutput, handlePlaybookCli, playbookCommandNeedsBrowser };
|