surf-cli 2.18.0 → 2.19.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 +107 -5
- package/native/cli.cjs +322 -9
- package/native/doctor.cjs +11 -2
- package/native/endpoint.cjs +121 -36
- package/native/extract.cjs +362 -0
- package/native/host-helpers.cjs +39 -2
- package/native/host-sessions.cjs +10 -0
- package/native/host.cjs +29 -21
- package/native/mcp-server.cjs +25 -0
- package/native/script-options.cjs +33 -0
- package/native/stdin-frames.cjs +33 -0
- package/native/tool-scope.cjs +2 -2
- package/package.json +6 -6
- package/skills/surf/SKILL.md +18 -0
package/README.md
CHANGED
|
@@ -135,6 +135,30 @@ SURF_REMOTE_CREDENTIAL=~/.config/surf/agent-macbook.json \
|
|
|
135
135
|
surf tab.list
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
For a TLS-terminating reverse proxy in front of the native host's existing clear-TCP
|
|
139
|
+
listener, opt in on the client:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
surf --remote surf.example.com:443 --remote-tls \
|
|
143
|
+
--remote-credential ~/.config/surf/agent-macbook.json tab.list
|
|
144
|
+
|
|
145
|
+
# Private CA and an IP destination with a DNS certificate identity
|
|
146
|
+
surf --remote 100.101.102.103:443 --remote-tls \
|
|
147
|
+
--remote-tls-ca ~/.config/surf/private-ca.pem \
|
|
148
|
+
--remote-tls-server-name surf.example.com \
|
|
149
|
+
--remote-credential ~/.config/surf/agent-macbook.json tab.list
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`--remote-tls-ca` replaces Node's system roots rather than adding to them. DNS endpoints
|
|
153
|
+
use their hostname for SNI and certificate validation; IP endpoints omit SNI and validate
|
|
154
|
+
the certificate's IP SAN unless `--remote-tls-server-name` supplies a DNS identity. TLS
|
|
155
|
+
certificate validation finishes before the mandatory Ed25519 authentication. Surf has no
|
|
156
|
+
insecure mode, downgrade, or plaintext retry. `SURF_REMOTE_TLS=1` is the only accepted
|
|
157
|
+
environment spelling; unset it to disable TLS. There is no CLI negation for env-enabled TLS.
|
|
158
|
+
CLI values override `SURF_REMOTE_TLS_CA` and `SURF_REMOTE_TLS_SERVER_NAME` independently.
|
|
159
|
+
`SURF_LISTEN` remains a plaintext listener behind the reverse proxy; Surf does not terminate
|
|
160
|
+
TLS on the browser host.
|
|
161
|
+
|
|
138
162
|
Surf performs mutual Ed25519 challenge-response with fresh nonces and checks authorization throughout the connection. A credential grants the same browser and host-file authority as a trusted local Surf user. Give each client its own credential, do not share it, and revoke it immediately if the client or file is lost:
|
|
139
163
|
|
|
140
164
|
```bash
|
|
@@ -162,7 +186,9 @@ Keep Tailscale policy restrictions as defense in depth. For example:
|
|
|
162
186
|
}
|
|
163
187
|
```
|
|
164
188
|
|
|
165
|
-
Adapt tags and ports to your Tailnet. Surf authentication does not replace Tailnet policy
|
|
189
|
+
Adapt tags and ports to your Tailnet. Surf authentication does not replace Tailnet policy.
|
|
190
|
+
Optional outbound remote TLS protects the client-to-proxy connection; Surf does not add an
|
|
191
|
+
SSH tunnel or a TLS listener.
|
|
166
192
|
|
|
167
193
|
**Operations and troubleshooting**
|
|
168
194
|
|
|
@@ -280,6 +306,13 @@ surf locate.role button --action click
|
|
|
280
306
|
surf frame.main # Return to main page
|
|
281
307
|
```
|
|
282
308
|
|
|
309
|
+
When a selector never matches, `frame.diagnose` shows the three frame views side by side (DOM `<iframe>` elements, the extension's frames with content-script reachability, and the CDP frame tree) and explains the mismatches: `srcdoc`/`about:blank` frames (matched to their CDP frame by `name`/`id`), sandboxes without `allow-scripts`, cross-origin frames, out-of-process frames that the CDP tree does not list (`frame.js` cannot reach them; `frame.switch` and `page.read` can when the content script answers), and frames still loading. The DOM inventory walks open shadow roots, so frames rendered by custom elements are listed with their `shadowHost` path. The text report abbreviates long frame URLs; `--json` keeps them whole.
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
surf frame.diagnose # Human-readable report with warnings
|
|
313
|
+
surf frame.diagnose --json # Full inventories
|
|
314
|
+
```
|
|
315
|
+
|
|
283
316
|
### Interaction
|
|
284
317
|
|
|
285
318
|
```bash
|
|
@@ -569,12 +602,55 @@ surf wait 2 # Wait 2 seconds
|
|
|
569
602
|
surf wait.element ".loaded" # Wait for element
|
|
570
603
|
surf wait.network # Wait for network idle
|
|
571
604
|
surf wait.url "/dashboard" # Wait for URL pattern
|
|
605
|
+
surf wait.ready --selector ".results" # Wait for content, fail fast on a bounce
|
|
606
|
+
surf page.readiness --json # Classify the page once
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
`wait.ready` polls with a bounded budget and reports a typed state instead of timing out silently: `ready`, `empty` (the page showed its own no-results message, `--empty-text`), or one of the negative states `login`, `challenge` (anti-bot interstitial), `not-found`, `error`. A negative state exits non-zero with codes `page_login`, `page_challenge`, `page_not_found`, `page_error`; `page_timeout` reports the last observed state. Pass `--accept login` to return a state to the caller instead. Detection uses visible UI state (a rendered password field, a login-looking route, the page's own wording, `--url-prefix` bounces), never site-specific selectors.
|
|
610
|
+
|
|
611
|
+
```bash
|
|
612
|
+
surf wait.ready --url-prefix "https://app.example.com/" --empty-text "No results"
|
|
613
|
+
surf wait.ready --accept login --json # {"state":"login","evidence":[...]} instead of an error
|
|
572
614
|
```
|
|
573
615
|
|
|
616
|
+
### Extracting structured data
|
|
617
|
+
|
|
618
|
+
`surf extract` composes existing client-side tools: it opens an owned tab,
|
|
619
|
+
waits for explicit readiness, runs a page script, validates its JSON result,
|
|
620
|
+
and closes the tab. It prints concise Markdown by default or structured JSON
|
|
621
|
+
with `--json`. Return an array, or an object containing a conventional row key
|
|
622
|
+
such as `rows`, `items`, or `results`; use `--rows <key>` for another key.
|
|
623
|
+
|
|
624
|
+
```bash
|
|
625
|
+
surf extract "https://example.com/list" --file rows.js --ready-selector ".item"
|
|
626
|
+
surf extract "https://example.com/search" --file rows.js --options '{"limit":20}' --empty-text "No results" --json
|
|
627
|
+
surf extract --tab-id 42 --code 'return [...document.querySelectorAll("h2")].map(h => ({title: h.textContent}))'
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Owned-tab failures always attempt cleanup. Zero rows retry unless
|
|
631
|
+
`--allow-empty` is set or `--empty-text` identifies the page's accepted empty
|
|
632
|
+
state. Fresh-tab retries are bounded (`--retry`, default 1, maximum 5) and are
|
|
633
|
+
limited to readiness timeouts, zero rows, and lost tab/execution-context
|
|
634
|
+
failures. Login, challenge, not-found, page-error, script/output, and cleanup
|
|
635
|
+
failures do not retry. `--tab-id` and `--session` target an existing page and
|
|
636
|
+
never retry or close it; `--keep-tab` preserves a successfully owned tab.
|
|
637
|
+
|
|
638
|
+
Extract is intended for read-only or otherwise idempotent caller scripts.
|
|
639
|
+
JavaScript is not inherently read-only: a retry can replay the script, so avoid
|
|
640
|
+
mutations or make them idempotent.
|
|
641
|
+
|
|
574
642
|
### Other
|
|
575
643
|
|
|
644
|
+
`js` and `frame.js` accept `--options '{"limit": 20}'` with inline code or
|
|
645
|
+
`--file`. This defines `SURF_OPTIONS` by parsing the JSON and freezing the
|
|
646
|
+
result; use an explicit `return` for the script's result. The freeze is shallow.
|
|
647
|
+
Invalid JSON and non-object values are rejected before sending a request;
|
|
648
|
+
`--options ''` defines an empty object. Without `--options`, code is unchanged.
|
|
649
|
+
|
|
576
650
|
```bash
|
|
577
651
|
surf js "return document.title" # Execute JavaScript
|
|
652
|
+
surf js "piHelpers.setValue(document.querySelector('#q'), 'hello')" # Native value setter + input/change events
|
|
653
|
+
surf js --file script.js --options '{"limit": 20}' # Script reads SURF_OPTIONS.limit
|
|
578
654
|
surf record --duration 2000 --fps 10 --output /tmp/anim.gif # Animated GIF capture
|
|
579
655
|
surf animate-audit --selector ".thing" --duration 2000 --fps 10 # JSON animation timeline
|
|
580
656
|
surf perf-audit --duration 3000 --output /tmp/perf.json # PerformanceObserver snapshot
|
|
@@ -811,12 +887,32 @@ Generated manifests declare provenance and authentication environment inputs. Su
|
|
|
811
887
|
--window-id <id> # Target a specific window
|
|
812
888
|
--no-wait # Return tab_busy/browser_busy instead of queueing
|
|
813
889
|
--json # Raw JSON including resolved target metadata
|
|
814
|
-
--soft-fail #
|
|
890
|
+
--soft-fail # Host tool errors: stderr warning, exit 0, no JSON error output
|
|
815
891
|
--no-lock # Bypass the legacy lock for compound client-side commands
|
|
816
892
|
--no-screenshot # Skip auto-screenshot after actions
|
|
817
893
|
--full # Full resolution screenshots (skip resize)
|
|
818
894
|
```
|
|
819
895
|
|
|
896
|
+
### Host tool-response errors
|
|
897
|
+
|
|
898
|
+
For ordinary socket-backed commands, a host response with a top-level `error`
|
|
899
|
+
exits 1 and prints `Error: ...` on stderr. A supplied code is appended as `[code]`
|
|
900
|
+
to the first line unless already present there; subsequent recovery lines are
|
|
901
|
+
preserved. Without a code, no suffix is added.
|
|
902
|
+
|
|
903
|
+
`--json` additionally writes `{"error":{"code":"...","message":"...","details":{...}}}`
|
|
904
|
+
to stdout, while retaining stderr and exit 1. The JSON code defaults to `"error"`;
|
|
905
|
+
the message uses the host's message, or the first display line if absent. Optional
|
|
906
|
+
details retain the host's fields except redundant `code` and `message` fields.
|
|
907
|
+
|
|
908
|
+
`--soft-fail` takes precedence: the original host display text is printed as a
|
|
909
|
+
stderr warning, without adding a code, stdout stays empty even with `--json`,
|
|
910
|
+
and the command exits 0. This is **not a universal JSON error envelope**: local
|
|
911
|
+
validation, transport/parser failures, compound commands and errors embedded in
|
|
912
|
+
successful result payloads retain their existing behavior. In particular, a
|
|
913
|
+
connection failure still prints stderr, leaves stdout empty and exits 1 with
|
|
914
|
+
`--json`, even with `--soft-fail`.
|
|
915
|
+
|
|
820
916
|
## Environment Variables
|
|
821
917
|
|
|
822
918
|
```bash
|
|
@@ -826,6 +922,9 @@ SURF_SESSION # Default named browser session for tab-scoped command
|
|
|
826
922
|
SURF_SOCKET # Socket path or named pipe (default: /tmp/surf.sock, Windows: //./pipe/surf)
|
|
827
923
|
SURF_REMOTE # Remote Surf endpoint as host:port (overrides SURF_SOCKET)
|
|
828
924
|
SURF_REMOTE_CREDENTIAL # Client Ed25519 credential for the selected remote endpoint
|
|
925
|
+
SURF_REMOTE_TLS # Exactly 1 enables TLS for a selected remote endpoint
|
|
926
|
+
SURF_REMOTE_TLS_CA # Custom CA bundle that replaces system roots
|
|
927
|
+
SURF_REMOTE_TLS_SERVER_NAME # DNS SNI and certificate identity override
|
|
829
928
|
SURF_REMOTE_STATE_DIR # Host identity/authorization directory (default: ~/.surf/remote)
|
|
830
929
|
SURF_LISTEN # Native-host Tailnet bind address as <tailscale-ip>:<port>
|
|
831
930
|
SURF_SOCKET_MODE # Advanced POSIX local socket mode: 600 (default) or 660
|
|
@@ -841,6 +940,9 @@ SURF_EXTENSION_PATH # Path to extension dist/ directory
|
|
|
841
940
|
- `SURF_SOCKET`: Advanced socket override. Set it for both the native host and CLI when separate browser/profile instances need hard isolation.
|
|
842
941
|
- `SURF_REMOTE`: Remote client endpoint. `--remote <host>:<port>` overrides it; both override `SURF_SOCKET`.
|
|
843
942
|
- `SURF_REMOTE_CREDENTIAL`: Credential used for mutual remote authentication. `--remote-credential <path>` overrides it.
|
|
943
|
+
- `SURF_REMOTE_TLS`: Set exactly `1` for TLS through a terminating reverse proxy; `--remote-tls` also enables it and cannot negate an env-enabled setting.
|
|
944
|
+
- `SURF_REMOTE_TLS_CA`: CA bundle for remote TLS, replacing system roots. `--remote-tls-ca <path>` overrides it.
|
|
945
|
+
- `SURF_REMOTE_TLS_SERVER_NAME`: DNS SNI and certificate identity override. `--remote-tls-server-name <name>` overrides it.
|
|
844
946
|
- `SURF_REMOTE_STATE_DIR`: Advanced host-side override for the mode-0700 identity and client registry directory.
|
|
845
947
|
- `SURF_LISTEN`: Native-host listener address on the browser machine. Use `surf install ... --listen <tailscale-ip>:<port>` to persist it in that host's wrapper.
|
|
846
948
|
- `SURF_SOCKET_MODE` / `SURF_SOCKET_GROUP`: Advanced POSIX native-host settings. Use `surf install ... --socket-mode 660 --socket-group <group>` to persist group access; mode `660` grants full Surf authority to every member of that group.
|
|
@@ -938,11 +1040,11 @@ echo '{"type":"tool_request","method":"execute_tool","params":{"tool":"tab.list"
|
|
|
938
1040
|
| `window.*` | `new`, `list`, `focus`, `close`, `resize` |
|
|
939
1041
|
| `tab.*` | `list`, `new`, `switch`, `close`, `name`, `unname`, `named`, `group`, `ungroup`, `groups`, `reload` |
|
|
940
1042
|
| `scroll.*` | `top`, `bottom`, `to`, `info` |
|
|
941
|
-
| `page.*` | `read`, `text`, `state` |
|
|
1043
|
+
| `page.*` | `read`, `text`, `state`, `readiness` |
|
|
942
1044
|
| `locate.*` | `role`, `text`, `label` |
|
|
943
1045
|
| `element.*` | `styles` |
|
|
944
|
-
| `frame.*` | `list`, `switch`, `main`, `js` |
|
|
945
|
-
| `wait.*` | `element`, `network`, `url`, `dom`, `load` |
|
|
1046
|
+
| `frame.*` | `list`, `diagnose`, `switch`, `main`, `js` |
|
|
1047
|
+
| `wait.*` | `element`, `network`, `url`, `dom`, `load`, `ready` |
|
|
946
1048
|
| `cookie` / `cookie.*` | `list`, `get`, `set`, `clear`, `delete` |
|
|
947
1049
|
| `bookmark.*` | `add`, `remove`, `list` |
|
|
948
1050
|
| `history.*` | `list`, `search` |
|
package/native/cli.cjs
CHANGED
|
@@ -16,7 +16,9 @@ const {
|
|
|
16
16
|
validateWorkflowArgs,
|
|
17
17
|
validateWorkflowFile,
|
|
18
18
|
} = require("./workflow-definition.cjs");
|
|
19
|
-
const { executeDoSteps } = require("./do-executor.cjs");
|
|
19
|
+
const { executeDoSteps, sendDoRequest } = require("./do-executor.cjs");
|
|
20
|
+
const { runExtraction, renderExtractionMarkdown } = require("./extract.cjs");
|
|
21
|
+
const { applyOptionsPrelude, parseScriptOptions } = require("./script-options.cjs");
|
|
20
22
|
const { openClientTransport } = require("./client-transport.cjs");
|
|
21
23
|
const { version: VERSION } = require("../package.json");
|
|
22
24
|
const {
|
|
@@ -73,7 +75,7 @@ function positiveIdFlag(argv, flag) {
|
|
|
73
75
|
return parsed;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
function resolveEarlyTargetOptions(argv, { allowWindow = true } = {}) {
|
|
78
|
+
function resolveEarlyTargetOptions(argv, { allowWindow = true, allowEnvironmentSession = true } = {}) {
|
|
77
79
|
const explicitSession = flagValue(argv, "--session");
|
|
78
80
|
const tabId = positiveIdFlag(argv, "--tab-id");
|
|
79
81
|
const windowId = allowWindow ? positiveIdFlag(argv, "--window-id") : undefined;
|
|
@@ -82,7 +84,7 @@ function resolveEarlyTargetOptions(argv, { allowWindow = true } = {}) {
|
|
|
82
84
|
process.exit(1);
|
|
83
85
|
}
|
|
84
86
|
const environmentSession = process.env.SURF_SESSION;
|
|
85
|
-
const session = explicitSession || (!tabId && !windowId ? environmentSession : undefined);
|
|
87
|
+
const session = explicitSession || (allowEnvironmentSession && !tabId && !windowId ? environmentSession : undefined);
|
|
86
88
|
return {
|
|
87
89
|
...(session ? { session, sessionSource: explicitSession ? "explicit" : "environment" } : {}),
|
|
88
90
|
...(tabId ? { tabId } : {}),
|
|
@@ -721,6 +723,17 @@ const TOOLS = {
|
|
|
721
723
|
examples: [{ cmd: "page.save --output page.html", desc: "Save current document HTML" }],
|
|
722
724
|
},
|
|
723
725
|
"page.state": { desc: "Get page state (modals, loading, etc.)", args: [] },
|
|
726
|
+
"page.readiness": {
|
|
727
|
+
desc: "Classify the page once: ready, empty, loading, login, challenge, not-found, error",
|
|
728
|
+
args: [],
|
|
729
|
+
opts: {
|
|
730
|
+
selector: "Visible CSS selector that marks a ready page",
|
|
731
|
+
text: "Page text that marks a ready page",
|
|
732
|
+
"url-prefix": "Expected URL prefix",
|
|
733
|
+
"empty-text": "Text of an explicit no-results render",
|
|
734
|
+
},
|
|
735
|
+
examples: [{ cmd: "page.readiness --json", desc: "State plus evidence as JSON" }]
|
|
736
|
+
},
|
|
724
737
|
}
|
|
725
738
|
},
|
|
726
739
|
locate: {
|
|
@@ -824,6 +837,24 @@ const TOOLS = {
|
|
|
824
837
|
},
|
|
825
838
|
"wait.dom": { desc: "Wait for DOM to stabilize", args: [], opts: { stable: "Stability window in ms (default: 100)", timeout: "Max wait time in ms" } },
|
|
826
839
|
"wait.load": { desc: "Wait for page to fully load", args: [], opts: { timeout: "Max wait time in ms (default: 30000)" } },
|
|
840
|
+
"wait.ready": {
|
|
841
|
+
desc: "Wait until the page is ready, or fail fast with a typed state (challenge, login, not-found, error)",
|
|
842
|
+
args: [],
|
|
843
|
+
opts: {
|
|
844
|
+
selector: "Visible CSS selector that marks a ready page",
|
|
845
|
+
text: "Page text that marks a ready page",
|
|
846
|
+
"url-prefix": "Expected URL prefix; anything else is a bounce",
|
|
847
|
+
"empty-text": "Text of an explicit no-results render (reports state 'empty')",
|
|
848
|
+
accept: "Negative states to return instead of fail (comma list)",
|
|
849
|
+
timeout: "Max wait time in ms (default: 20000, max: 120000)",
|
|
850
|
+
interval: "Poll interval in ms (default: 400)",
|
|
851
|
+
},
|
|
852
|
+
examples: [
|
|
853
|
+
{ cmd: 'wait.ready --selector ".results"', desc: "Wait for content; fail fast on a login bounce" },
|
|
854
|
+
{ cmd: 'wait.ready --url-prefix "https://app.example.com/" --empty-text "No results"', desc: "Distinguish empty from blocked" },
|
|
855
|
+
{ cmd: "wait.ready --accept login --json", desc: "Return the login state to the caller" },
|
|
856
|
+
]
|
|
857
|
+
},
|
|
827
858
|
}
|
|
828
859
|
},
|
|
829
860
|
input: {
|
|
@@ -878,17 +909,52 @@ const TOOLS = {
|
|
|
878
909
|
"drag": { desc: "Drag between points", args: [], opts: { from: "Start x,y", to: "End x,y" } },
|
|
879
910
|
}
|
|
880
911
|
},
|
|
912
|
+
extract: {
|
|
913
|
+
desc: "Scripted extraction in an owned tab",
|
|
914
|
+
commands: {
|
|
915
|
+
"extract": {
|
|
916
|
+
desc: "Open a URL in a fresh tab, wait until it is ready, run a page-side script that returns JSON, print rows",
|
|
917
|
+
args: ["url"],
|
|
918
|
+
opts: {
|
|
919
|
+
file: "Script file; must `return` JSON (an array, or an object with a rows/items/results array)",
|
|
920
|
+
code: "Inline script instead of --file",
|
|
921
|
+
options: "JSON object exposed to the script as SURF_OPTIONS",
|
|
922
|
+
"options-file": "Read the options object from a JSON file",
|
|
923
|
+
"ready-selector": "wait.ready --selector before extracting",
|
|
924
|
+
"ready-text": "wait.ready --text before extracting",
|
|
925
|
+
"ready-url-prefix": "wait.ready --url-prefix; a different URL is a bounce",
|
|
926
|
+
"empty-text": "wait.ready --empty-text; lets an explicit no-results page pass the zero-rows check",
|
|
927
|
+
"ready-timeout": "Readiness timeout in ms (default: 20000)",
|
|
928
|
+
"ready-interval": "Readiness polling interval in ms (default: 400)",
|
|
929
|
+
rows: "Key of the row array in the script result (default: auto)",
|
|
930
|
+
retry: "Fresh-tab retries on transient failures (default: 1, max: 5)",
|
|
931
|
+
"retry-delay-ms": "Delay between attempts (default: 500)",
|
|
932
|
+
"allow-empty": "Accept zero rows",
|
|
933
|
+
"keep-tab": "Leave the owned tab open on success and report its id",
|
|
934
|
+
"tab-id": "Extract from an existing tab instead (no fresh tab, no retry; navigates only if a URL is given)",
|
|
935
|
+
session: "Extract from a session's tab instead (same rules as --tab-id)",
|
|
936
|
+
json: "Print {data, rows, rowCount, attempts, readiness} as JSON",
|
|
937
|
+
},
|
|
938
|
+
examples: [
|
|
939
|
+
{ cmd: 'extract "https://example.com/list" --file rows.js --ready-selector ".item"', desc: "Fresh tab, wait for items, print a Markdown table" },
|
|
940
|
+
{ cmd: 'extract "https://example.com/search?q=x" --file rows.js --options \'{"limit": 20}\' --empty-text "No results" --json', desc: "Options prelude, explicit empty state, JSON output" },
|
|
941
|
+
{ cmd: "extract --tab-id 42 --code 'return [...document.querySelectorAll(\"h2\")].map(h => ({ title: h.textContent }))'", desc: "Read an existing tab in place" },
|
|
942
|
+
]
|
|
943
|
+
},
|
|
944
|
+
}
|
|
945
|
+
},
|
|
881
946
|
js: {
|
|
882
947
|
desc: "JavaScript execution",
|
|
883
948
|
commands: {
|
|
884
949
|
"js": {
|
|
885
950
|
desc: "Execute JavaScript (use 'return' for values)",
|
|
886
951
|
args: ["code"],
|
|
887
|
-
opts: { file: "Run JS from file" },
|
|
952
|
+
opts: { file: "Run JS from file", options: "JSON object exposed to the script as a frozen SURF_OPTIONS constant" },
|
|
888
953
|
examples: [
|
|
889
954
|
{ cmd: 'js "return document.title"', desc: "Get title" },
|
|
890
955
|
{ cmd: 'js "document.body.style.background = \'red\'"', desc: "Run code" },
|
|
891
956
|
{ cmd: "js --file script.js", desc: "Run file" },
|
|
957
|
+
{ cmd: 'js --file script.js --options \'{"limit": 20}\'', desc: "Run file with SURF_OPTIONS.limit" },
|
|
892
958
|
]
|
|
893
959
|
},
|
|
894
960
|
}
|
|
@@ -1156,7 +1222,7 @@ const TOOLS = {
|
|
|
1156
1222
|
"frame.js": {
|
|
1157
1223
|
desc: "Execute JS in specific frame",
|
|
1158
1224
|
args: ["code"],
|
|
1159
|
-
opts: { id: "Frame ID from frame.list", file: "Run JS from file" },
|
|
1225
|
+
opts: { id: "Frame ID from frame.list", file: "Run JS from file", options: "JSON object exposed as SURF_OPTIONS" },
|
|
1160
1226
|
examples: [
|
|
1161
1227
|
{ cmd: 'frame.js "return document.title" --id frame1', desc: "JS in specific frame" },
|
|
1162
1228
|
]
|
|
@@ -1614,7 +1680,8 @@ const ALL_SOCKET_TOOLS = [
|
|
|
1614
1680
|
"tab.list", "tab.new", "tab.switch", "tab.close", "tab.move", "tab.name", "tab.unname", "tab.named",
|
|
1615
1681
|
"tab.group", "tab.ungroup", "tab.groups", "tab.reload",
|
|
1616
1682
|
"scroll.top", "scroll.bottom", "scroll.to", "scroll.info",
|
|
1617
|
-
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load",
|
|
1683
|
+
"wait.element", "wait.network", "wait.url", "wait.dom", "wait.load", "wait.ready",
|
|
1684
|
+
"page.readiness",
|
|
1618
1685
|
"click", "hover", "drag",
|
|
1619
1686
|
"js", "console", "network",
|
|
1620
1687
|
"network.get", "network.body", "network.curl", "network.origins",
|
|
@@ -1668,7 +1735,9 @@ const SEE_ALSO = {
|
|
|
1668
1735
|
"animate-audit": ["screenshot", "record", "perf-audit", "js"],
|
|
1669
1736
|
"perf-audit": ["record", "animate-audit", "perf.metrics", "console"],
|
|
1670
1737
|
"search": ["locate.text", "page.read"],
|
|
1671
|
-
"wait.element": ["wait.load", "wait.network"],
|
|
1738
|
+
"wait.element": ["wait.load", "wait.network", "wait.ready"],
|
|
1739
|
+
"wait.ready": ["page.readiness", "wait.element", "wait.url"],
|
|
1740
|
+
"page.readiness": ["wait.ready", "page.state"],
|
|
1672
1741
|
"wait.load": ["wait.element", "wait.network"],
|
|
1673
1742
|
"wait.network": ["wait.load", "wait.element"],
|
|
1674
1743
|
"scroll.to": ["click", "page.read"],
|
|
@@ -1722,6 +1791,9 @@ More Help:
|
|
|
1722
1791
|
--no-wait Return tab_busy/browser_busy instead of queueing
|
|
1723
1792
|
--remote <host>:<port> Route requests to a remote native host
|
|
1724
1793
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1794
|
+
--remote-tls Use TLS through a TLS-terminating reverse proxy
|
|
1795
|
+
--remote-tls-ca <path> Replace system roots with a custom CA bundle
|
|
1796
|
+
--remote-tls-server-name <name> Override TLS SNI and certificate identity
|
|
1725
1797
|
surf remote authorize <label> --output <path>
|
|
1726
1798
|
surf remote list | surf remote revoke <label>
|
|
1727
1799
|
surf --help-full All commands
|
|
@@ -1739,6 +1811,7 @@ Purpose: control Chrome from shell. Commands are \`surf <command> [args] [option
|
|
|
1739
1811
|
Core loop: navigate -> wait/read -> act -> screenshot/read.
|
|
1740
1812
|
Navigate: surf navigate "https://example.com" # alias: surf go "..."
|
|
1741
1813
|
Wait after navigation: surf wait 2 # or wait.load for load complete
|
|
1814
|
+
Wait for real content: surf wait.ready --selector ".results" # fails fast with page_login / page_challenge / page_not_found; --accept login returns the state
|
|
1742
1815
|
Read DOM/refs: surf page.read --depth 3 --compact # alias: surf read
|
|
1743
1816
|
Refs: use e1/e2 refs from page.read; prefer refs over CSS when available.
|
|
1744
1817
|
Click ref: surf click e5
|
|
@@ -1751,6 +1824,7 @@ Video recording: surf video start ./demo.webm --fps 30; surf video stop
|
|
|
1751
1824
|
Animation audit: surf animate-audit --selector ".thing" --duration 2000 --fps 10
|
|
1752
1825
|
Performance audit: surf perf-audit --duration 3000 --trigger "click:.cta" --output /tmp/perf.json
|
|
1753
1826
|
JavaScript: surf js "return document.title"
|
|
1827
|
+
Frames: surf frame.list | surf frame.diagnose # diagnose explains why a selector misses inside iframes (shadow roots, srcdoc, out-of-process)
|
|
1754
1828
|
Scroll: surf scroll down 800 | surf scroll up 400 | surf scroll bottom | surf scroll top
|
|
1755
1829
|
Find by semantics: surf locate.role button --name "Submit" --action click
|
|
1756
1830
|
Device/viewport: surf emulate.device "iPhone 14" | surf resize 375 812
|
|
@@ -1793,15 +1867,25 @@ Playbooks:
|
|
|
1793
1867
|
Options:
|
|
1794
1868
|
--remote <host>:<port> Route requests to a remote native host
|
|
1795
1869
|
--remote-credential <path> Use a mode-0600 Ed25519 remote credential file
|
|
1870
|
+
--remote-tls Use TLS through a TLS-terminating reverse proxy
|
|
1871
|
+
--remote-tls-ca <path> Replace system roots with a custom CA bundle
|
|
1872
|
+
--remote-tls-server-name <name> Override TLS SNI and certificate identity
|
|
1796
1873
|
--session <name> Target a durable named session (or set SURF_SESSION)
|
|
1797
1874
|
--tab-id <id> Target specific tab
|
|
1798
1875
|
--window-id <id> Target specific window
|
|
1799
1876
|
--no-wait Return immediately when the tab/browser is busy
|
|
1800
1877
|
--json Output raw JSON including target metadata
|
|
1801
1878
|
--auto-capture On error: capture screenshot + console to /tmp
|
|
1802
|
-
--soft-fail
|
|
1879
|
+
--soft-fail Host tool errors: warn on stderr, exit 0, no JSON error output
|
|
1803
1880
|
--no-lock Bypass the legacy lock for compound client-side commands
|
|
1804
1881
|
|
|
1882
|
+
Host tool-response errors: stderr includes [code] on the first line when supplied;
|
|
1883
|
+
--json also writes {"error":{"code":"...","message":"..."}} to stdout; exit 1.
|
|
1884
|
+
Host details, when present, are included without redundant code/message fields.
|
|
1885
|
+
Missing codes use "error" in JSON. --soft-fail keeps the original warning text.
|
|
1886
|
+
This is not a universal error format: local validation, transport and parser
|
|
1887
|
+
failures keep their existing output/status; --soft-fail does not mask them.
|
|
1888
|
+
|
|
1805
1889
|
Remote Credentials (run on the browser host):
|
|
1806
1890
|
surf remote authorize <label> --output <credential-file>
|
|
1807
1891
|
surf remote list
|
|
@@ -2570,6 +2654,150 @@ if (args[0] === "do") {
|
|
|
2570
2654
|
return;
|
|
2571
2655
|
}
|
|
2572
2656
|
|
|
2657
|
+
// Handle `surf extract`: a page-side script in an owned tab with a
|
|
2658
|
+
// readiness gate, bounded fresh-tab retry and the zero-rows invariant.
|
|
2659
|
+
if (args[0] === "extract") {
|
|
2660
|
+
const extractArgs = args.slice(1);
|
|
2661
|
+
const valueFlags = new Set([
|
|
2662
|
+
"file", "code", "options", "options-file", "ready-selector", "ready-text", "ready-url-prefix",
|
|
2663
|
+
"ready-timeout", "ready-interval", "empty-text", "rows", "retry", "retry-delay-ms",
|
|
2664
|
+
"tab-id", "session",
|
|
2665
|
+
]);
|
|
2666
|
+
const boolFlags = new Set(["allow-empty", "keep-tab", "json", "no-wait", "help"]);
|
|
2667
|
+
const opts = {};
|
|
2668
|
+
let url = null;
|
|
2669
|
+
for (let i = 0; i < extractArgs.length; i++) {
|
|
2670
|
+
const arg = extractArgs[i];
|
|
2671
|
+
if (arg === "-f") {
|
|
2672
|
+
opts.file = flagValue(extractArgs, arg);
|
|
2673
|
+
i++;
|
|
2674
|
+
} else if (arg.startsWith("--")) {
|
|
2675
|
+
const key = arg.slice(2);
|
|
2676
|
+
if (boolFlags.has(key)) opts[key] = true;
|
|
2677
|
+
else if (valueFlags.has(key)) {
|
|
2678
|
+
opts[key] = key === "options" && extractArgs[i + 1] === ""
|
|
2679
|
+
? ""
|
|
2680
|
+
: flagValue(extractArgs, arg);
|
|
2681
|
+
i++;
|
|
2682
|
+
} else {
|
|
2683
|
+
console.error(`Error: unknown extract option --${key}`);
|
|
2684
|
+
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
} else if (url === null) {
|
|
2687
|
+
url = arg;
|
|
2688
|
+
} else {
|
|
2689
|
+
console.error(`Error: unexpected argument ${arg}`);
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (opts.help) {
|
|
2694
|
+
showToolHelp("extract");
|
|
2695
|
+
process.exit(0);
|
|
2696
|
+
}
|
|
2697
|
+
const wantJson = opts.json === true;
|
|
2698
|
+
const fail = (code, message, details) => {
|
|
2699
|
+
if (wantJson) {
|
|
2700
|
+
console.log(JSON.stringify({ error: { code, message, ...(details ? { details } : {}) } }, null, 2));
|
|
2701
|
+
} else {
|
|
2702
|
+
console.error(`Error: ${message}${code ? ` [${code}]` : ""}`);
|
|
2703
|
+
}
|
|
2704
|
+
process.exit(1);
|
|
2705
|
+
};
|
|
2706
|
+
if (opts.options !== undefined && opts["options-file"] !== undefined) {
|
|
2707
|
+
fail("usage", "use either --options or --options-file, not both");
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
let code = null;
|
|
2711
|
+
try {
|
|
2712
|
+
if (opts.file && opts.code) fail("usage", "use either --file or --code, not both");
|
|
2713
|
+
if (opts.file) code = fs.readFileSync(opts.file, "utf8");
|
|
2714
|
+
else if (typeof opts.code === "string") code = opts.code;
|
|
2715
|
+
else fail("usage", "an extraction script is required: --file script.js or --code 'return {...}'");
|
|
2716
|
+
} catch (error) {
|
|
2717
|
+
fail("usage", `Failed to read script: ${error.message}`);
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
let scriptOptions = {};
|
|
2721
|
+
try {
|
|
2722
|
+
if (opts["options-file"]) scriptOptions = parseScriptOptions(fs.readFileSync(opts["options-file"], "utf8"));
|
|
2723
|
+
else scriptOptions = parseScriptOptions(opts.options);
|
|
2724
|
+
} catch (error) {
|
|
2725
|
+
fail("usage", error.message);
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
const toInt = (key, fallback) => {
|
|
2729
|
+
if (opts[key] === undefined) return fallback;
|
|
2730
|
+
const parsed = Number(opts[key]);
|
|
2731
|
+
if (!/^\d+$/.test(opts[key]) || !Number.isSafeInteger(parsed)) {
|
|
2732
|
+
fail("usage", `--${key} must be a non-negative integer`);
|
|
2733
|
+
}
|
|
2734
|
+
return parsed;
|
|
2735
|
+
};
|
|
2736
|
+
|
|
2737
|
+
const targetOptions = resolveEarlyTargetOptions(extractArgs, {
|
|
2738
|
+
allowWindow: false,
|
|
2739
|
+
allowEnvironmentSession: false,
|
|
2740
|
+
});
|
|
2741
|
+
const hasTarget = Boolean(targetOptions.tabId || targetOptions.session);
|
|
2742
|
+
if (!hasTarget && !url) fail("usage", "a URL is required unless --tab-id or --session names the page to read");
|
|
2743
|
+
|
|
2744
|
+
const settings = {
|
|
2745
|
+
code,
|
|
2746
|
+
url: url ?? undefined,
|
|
2747
|
+
options: scriptOptions,
|
|
2748
|
+
ready: {
|
|
2749
|
+
selector: opts["ready-selector"],
|
|
2750
|
+
text: opts["ready-text"],
|
|
2751
|
+
urlPrefix: opts["ready-url-prefix"],
|
|
2752
|
+
emptyText: opts["empty-text"],
|
|
2753
|
+
timeout: toInt("ready-timeout", undefined),
|
|
2754
|
+
interval: toInt("ready-interval", undefined),
|
|
2755
|
+
},
|
|
2756
|
+
retry: { count: toInt("retry", undefined), delayMs: toInt("retry-delay-ms", undefined) },
|
|
2757
|
+
keepTab: opts["keep-tab"] === true,
|
|
2758
|
+
allowEmpty: opts["allow-empty"] === true,
|
|
2759
|
+
rowsKey: opts.rows,
|
|
2760
|
+
target: hasTarget,
|
|
2761
|
+
};
|
|
2762
|
+
|
|
2763
|
+
const runExtract = async () => {
|
|
2764
|
+
let transport;
|
|
2765
|
+
try {
|
|
2766
|
+
transport = await openClientTransport(endpoint);
|
|
2767
|
+
const baseContext = { ...targetOptions, endpoint, transport };
|
|
2768
|
+
const executeTool = (toolName, toolArgs, ownedTabId) => {
|
|
2769
|
+
const context = ownedTabId
|
|
2770
|
+
? { tabId: ownedTabId, admission: targetOptions.admission, endpoint, transport }
|
|
2771
|
+
: baseContext;
|
|
2772
|
+
return sendDoRequest(toolName, toolArgs, context);
|
|
2773
|
+
};
|
|
2774
|
+
const result = await runExtraction({
|
|
2775
|
+
...settings,
|
|
2776
|
+
executeTool,
|
|
2777
|
+
onEvent: (event) => {
|
|
2778
|
+
if (wantJson) return;
|
|
2779
|
+
if (event.type === "attempt" && event.of > 1) console.error(`[surf] extract attempt ${event.attempt}/${event.of}`);
|
|
2780
|
+
if (event.type === "attempt-failed" && event.retryable) console.error(`[surf] attempt ${event.attempt} failed (${event.error}); retrying with a fresh tab`);
|
|
2781
|
+
},
|
|
2782
|
+
});
|
|
2783
|
+
if (wantJson) {
|
|
2784
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2785
|
+
} else {
|
|
2786
|
+
console.log(renderExtractionMarkdown(result.data, result.rows, { title: url ? `Extraction from ${url}` : "Extraction" }));
|
|
2787
|
+
if (result.tabId) console.error(`[surf] tab ${result.tabId} left open (--keep-tab)`);
|
|
2788
|
+
}
|
|
2789
|
+
return 0;
|
|
2790
|
+
} catch (error) {
|
|
2791
|
+
fail(error.code || "extraction_failed", error.message, error.details);
|
|
2792
|
+
} finally {
|
|
2793
|
+
await transport?.close();
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2796
|
+
|
|
2797
|
+
runExtract().then((exitCode) => process.exit(exitCode));
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2573
2801
|
// Handle workflow management commands
|
|
2574
2802
|
if (args[0] === "workflow.list") {
|
|
2575
2803
|
const workflows = listWorkflows();
|
|
@@ -2981,6 +3209,17 @@ if ((tool === "js" || tool === "frame.js") && toolArgs.file) {
|
|
|
2981
3209
|
}
|
|
2982
3210
|
}
|
|
2983
3211
|
|
|
3212
|
+
if ((tool === "js" || tool === "frame.js") && toolArgs.options !== undefined) {
|
|
3213
|
+
try {
|
|
3214
|
+
if (typeof toolArgs.code !== "string") throw new Error("--options needs code (inline or --file)");
|
|
3215
|
+
toolArgs.code = applyOptionsPrelude(toolArgs.code, toolArgs.options);
|
|
3216
|
+
delete toolArgs.options;
|
|
3217
|
+
} catch (e) {
|
|
3218
|
+
console.error(`Error: ${e.message}`);
|
|
3219
|
+
process.exit(1);
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
2984
3223
|
if (tool === "batch" && toolArgs.file) {
|
|
2985
3224
|
try {
|
|
2986
3225
|
const parsed = JSON.parse(fs.readFileSync(toolArgs.file, "utf8"));
|
|
@@ -3630,7 +3869,25 @@ async function handleResponse(response) {
|
|
|
3630
3869
|
socket.end();
|
|
3631
3870
|
process.exit(0);
|
|
3632
3871
|
}
|
|
3633
|
-
|
|
3872
|
+
// Host tool-response errors carry codes separately from their display text.
|
|
3873
|
+
const errorCode = typeof response.error.code === "string" ? response.error.code : null;
|
|
3874
|
+
const [firstLine, ...restLines] = errContent.split("\n");
|
|
3875
|
+
const display = errorCode && !firstLine.includes(`[${errorCode}]`)
|
|
3876
|
+
? [`${firstLine} [${errorCode}]`, ...restLines].join("\n")
|
|
3877
|
+
: errContent;
|
|
3878
|
+
console.error("Error:", display);
|
|
3879
|
+
if (wantJson) {
|
|
3880
|
+
// details repeats code/message when the error serialises itself; keep the rest.
|
|
3881
|
+
const { code: _code, message: _message, ...details } =
|
|
3882
|
+
response.error.details && typeof response.error.details === "object" ? response.error.details : {};
|
|
3883
|
+
console.log(JSON.stringify({
|
|
3884
|
+
error: {
|
|
3885
|
+
code: errorCode || "error",
|
|
3886
|
+
message: typeof response.error.message === "string" ? response.error.message : firstLine,
|
|
3887
|
+
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
3888
|
+
},
|
|
3889
|
+
}, null, 2));
|
|
3890
|
+
}
|
|
3634
3891
|
|
|
3635
3892
|
if (autoCapture) {
|
|
3636
3893
|
await performAutoCapture();
|
|
@@ -3822,6 +4079,62 @@ async function handleResponse(response) {
|
|
|
3822
4079
|
}
|
|
3823
4080
|
console.log("\nUsage: surf emulate.device \"<device name>\"");
|
|
3824
4081
|
console.log('Reset: surf emulate.device "reset"');
|
|
4082
|
+
} else if (tool === "wait.ready" || tool === "page.readiness") {
|
|
4083
|
+
const lines = [`state: ${data?.state ?? "unknown"}`];
|
|
4084
|
+
if (data?.href) lines.push(`url: ${data.href}`);
|
|
4085
|
+
if (data?.title) lines.push(`title: ${data.title}`);
|
|
4086
|
+
if (typeof data?.waited === "number") lines.push(`waited: ${data.waited}ms (${data.polls} poll${data.polls === 1 ? "" : "s"})`);
|
|
4087
|
+
if (data?.accepted) lines.push("accepted: negative state returned because of --accept");
|
|
4088
|
+
for (const item of Array.isArray(data?.evidence) ? data.evidence : []) lines.push(`- ${item}`);
|
|
4089
|
+
console.log(lines.join("\n"));
|
|
4090
|
+
} else if (tool === "frame.diagnose" && data?.counts) {
|
|
4091
|
+
// Keep frame URLs readable: embed runners carry kilobyte-long state
|
|
4092
|
+
// parameters that bury the report (use --json for the full URLs).
|
|
4093
|
+
const abbreviateUrl = (url, max = 100) => {
|
|
4094
|
+
if (typeof url !== "string" || url.length <= max) return url;
|
|
4095
|
+
try {
|
|
4096
|
+
const parsed = new URL(url);
|
|
4097
|
+
const base = `${parsed.origin}${parsed.pathname}`;
|
|
4098
|
+
const trailing = url.length - base.length;
|
|
4099
|
+
if (trailing > 0 && base.length <= max - 12) return `${base}?...(+${trailing} chars)`;
|
|
4100
|
+
} catch {}
|
|
4101
|
+
return `${url.slice(0, max - 3)}...`;
|
|
4102
|
+
};
|
|
4103
|
+
const lines = [];
|
|
4104
|
+
lines.push(`Frame diagnosis for ${data.mainPage?.href ?? "?"}${data.mainPage?.title ? ` (${data.mainPage.title})` : ""}`);
|
|
4105
|
+
lines.push(`DOM iframes: ${data.counts.domIframes}, extension frames: ${data.counts.extensionFrames} (incl. main), CDP frames: ${data.counts.cdpFrames}`);
|
|
4106
|
+
if (Array.isArray(data.domIframes) && data.domIframes.length > 0) {
|
|
4107
|
+
lines.push("", "DOM iframes:");
|
|
4108
|
+
for (const f of data.domIframes) {
|
|
4109
|
+
const flags = [
|
|
4110
|
+
f.blank ? "blank" : null,
|
|
4111
|
+
f.crossOrigin ? "cross-origin" : null,
|
|
4112
|
+
f.scriptsBlocked ? "scripts-blocked" : null,
|
|
4113
|
+
f.zeroSize ? "0-size" : null,
|
|
4114
|
+
].filter(Boolean).join(",");
|
|
4115
|
+
const links = [
|
|
4116
|
+
f.extensionFrameIds?.length ? `ext ${f.extensionFrameIds.join("/")}` : "ext -",
|
|
4117
|
+
f.cdpFrameIds?.length ? `cdp ${f.cdpFrameIds.join("/")}` : "cdp -",
|
|
4118
|
+
].join(", ");
|
|
4119
|
+
lines.push(` [${f.domIndex}] ${f.srcdoc ? "srcdoc" : abbreviateUrl(f.src || "about:blank")} ${Math.round(f.rect?.width ?? 0)}x${Math.round(f.rect?.height ?? 0)}${f.name ? ` name=${f.name}` : f.id ? ` id=${f.id}` : ""}${f.sandbox !== null && f.sandbox !== undefined ? ` sandbox="${f.sandbox}"` : ""}${f.shadowHost ? ` in shadow root of ${f.shadowHost}` : ""}${flags ? ` [${flags}]` : ""} -> ${links}`);
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
if (Array.isArray(data.extensionFrames) && data.extensionFrames.length > 0) {
|
|
4123
|
+
lines.push("", "Extension frames (frame.switch indexes, webNavigation ids):");
|
|
4124
|
+
for (const f of data.extensionFrames) {
|
|
4125
|
+
const reach = f.contentScriptReachable ? "content-script ok" : `content-script unreachable${f.contentScriptError ? ` (${f.contentScriptError})` : ""}`;
|
|
4126
|
+
lines.push(` ${f.isMain ? "main" : `[${f.switchIndex}]`} #${f.frameId}${f.isMain ? "" : ` parent ${f.parentFrameId}`} ${abbreviateUrl(f.url)}${f.crossOrigin ? " [cross-origin]" : ""} - ${reach}`);
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
if (Array.isArray(data.cdpFrames) && data.cdpFrames.length > 0) {
|
|
4130
|
+
lines.push("", "CDP frames (frame.js ids):");
|
|
4131
|
+
for (const f of data.cdpFrames) {
|
|
4132
|
+
lines.push(` ${f.frameId}${f.isMain ? " main" : ` parent ${f.parentId}`} ${abbreviateUrl(f.url)}${f.name ? ` name=${f.name}` : ""}${f.extensionFrameIds?.length ? ` -> ext ${f.extensionFrameIds.join("/")}` : ""}`);
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
lines.push("", Array.isArray(data.warnings) && data.warnings.length > 0 ? "Warnings:" : "No warnings.");
|
|
4136
|
+
for (const w of Array.isArray(data.warnings) ? data.warnings : []) lines.push(` - ${w}`);
|
|
4137
|
+
console.log(lines.join("\n"));
|
|
3825
4138
|
} else if (tool === "js") {
|
|
3826
4139
|
if (data?.result !== undefined) {
|
|
3827
4140
|
const val = data.result.value ?? data.result;
|