@crouton-kit/capture 1.4.0 → 1.4.2
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/bin/capture +78 -46
- package/package.json +1 -1
- package/src/cdp/commands/motion/rec.ts +2 -2
- package/src/cdp/commands/page/click.ts +3 -3
- package/src/cdp/commands/page/index.ts +1 -1
- package/src/cdp/commands/page/scroll.ts +2 -2
- package/src/cdp/commands/page/shot.ts +2 -2
- package/src/cdp/commands/page/type.ts +1 -1
- package/src/cdp/detect.ts +61 -20
- package/src/cdp/leaf-grammar.ts +2 -2
- package/src/interact.ts +28 -2
package/bin/capture
CHANGED
|
@@ -2232,7 +2232,7 @@ var require_websocket = __commonJS({
|
|
|
2232
2232
|
"use strict";
|
|
2233
2233
|
var EventEmitter = require("events");
|
|
2234
2234
|
var https = require("https");
|
|
2235
|
-
var
|
|
2235
|
+
var http2 = require("http");
|
|
2236
2236
|
var net5 = require("net");
|
|
2237
2237
|
var tls = require("tls");
|
|
2238
2238
|
var { randomBytes: randomBytes10, createHash: createHash5 } = require("crypto");
|
|
@@ -2766,7 +2766,7 @@ var require_websocket = __commonJS({
|
|
|
2766
2766
|
}
|
|
2767
2767
|
const defaultPort = isSecure ? 443 : 80;
|
|
2768
2768
|
const key = randomBytes10(16).toString("base64");
|
|
2769
|
-
const request = isSecure ? https.request :
|
|
2769
|
+
const request = isSecure ? https.request : http2.request;
|
|
2770
2770
|
const protocolSet = /* @__PURE__ */ new Set();
|
|
2771
2771
|
let perMessageDeflate;
|
|
2772
2772
|
opts.createConnection = opts.createConnection || (isSecure ? tlsConnect : netConnect);
|
|
@@ -3260,7 +3260,7 @@ var require_websocket_server = __commonJS({
|
|
|
3260
3260
|
"node_modules/.pnpm/ws@8.20.0/node_modules/ws/lib/websocket-server.js"(exports2, module2) {
|
|
3261
3261
|
"use strict";
|
|
3262
3262
|
var EventEmitter = require("events");
|
|
3263
|
-
var
|
|
3263
|
+
var http2 = require("http");
|
|
3264
3264
|
var { Duplex } = require("stream");
|
|
3265
3265
|
var { createHash: createHash5 } = require("crypto");
|
|
3266
3266
|
var extension2 = require_extension();
|
|
@@ -3335,8 +3335,8 @@ var require_websocket_server = __commonJS({
|
|
|
3335
3335
|
);
|
|
3336
3336
|
}
|
|
3337
3337
|
if (options.port != null) {
|
|
3338
|
-
this._server =
|
|
3339
|
-
const body =
|
|
3338
|
+
this._server = http2.createServer((req, res) => {
|
|
3339
|
+
const body = http2.STATUS_CODES[426];
|
|
3340
3340
|
res.writeHead(426, {
|
|
3341
3341
|
"Content-Length": body.length,
|
|
3342
3342
|
"Content-Type": "text/plain"
|
|
@@ -3623,7 +3623,7 @@ var require_websocket_server = __commonJS({
|
|
|
3623
3623
|
this.destroy();
|
|
3624
3624
|
}
|
|
3625
3625
|
function abortHandshake(socket, code, message, headers2) {
|
|
3626
|
-
message = message ||
|
|
3626
|
+
message = message || http2.STATUS_CODES[code];
|
|
3627
3627
|
headers2 = {
|
|
3628
3628
|
Connection: "close",
|
|
3629
3629
|
"Content-Type": "text/html",
|
|
@@ -3632,7 +3632,7 @@ var require_websocket_server = __commonJS({
|
|
|
3632
3632
|
};
|
|
3633
3633
|
socket.once("finish", socket.destroy);
|
|
3634
3634
|
socket.end(
|
|
3635
|
-
`HTTP/1.1 ${code} ${
|
|
3635
|
+
`HTTP/1.1 ${code} ${http2.STATUS_CODES[code]}\r
|
|
3636
3636
|
` + Object.keys(headers2).map((h) => `${h}: ${headers2[h]}`).join("\r\n") + "\r\n\r\n" + message
|
|
3637
3637
|
);
|
|
3638
3638
|
}
|
|
@@ -3779,6 +3779,39 @@ __export(detect_exports, {
|
|
|
3779
3779
|
identifyBrowserBundleId: () => identifyBrowserBundleId,
|
|
3780
3780
|
pickPreferredEndpoint: () => pickPreferredEndpoint
|
|
3781
3781
|
});
|
|
3782
|
+
function httpGetJson(port, path33, timeoutMs) {
|
|
3783
|
+
return new Promise((resolve12) => {
|
|
3784
|
+
const req = http.get(
|
|
3785
|
+
{ host: "localhost", port, path: path33, timeout: timeoutMs },
|
|
3786
|
+
(res) => {
|
|
3787
|
+
const status = res.statusCode ?? 0;
|
|
3788
|
+
if (status < 200 || status >= 300) {
|
|
3789
|
+
res.resume();
|
|
3790
|
+
resolve12(null);
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3793
|
+
let body = "";
|
|
3794
|
+
res.setEncoding("utf-8");
|
|
3795
|
+
res.on("data", (chunk) => {
|
|
3796
|
+
body += chunk;
|
|
3797
|
+
});
|
|
3798
|
+
res.on("end", () => {
|
|
3799
|
+
try {
|
|
3800
|
+
resolve12(JSON.parse(body));
|
|
3801
|
+
} catch {
|
|
3802
|
+
resolve12(null);
|
|
3803
|
+
}
|
|
3804
|
+
});
|
|
3805
|
+
res.on("error", () => resolve12(null));
|
|
3806
|
+
}
|
|
3807
|
+
);
|
|
3808
|
+
req.on("timeout", () => {
|
|
3809
|
+
req.destroy();
|
|
3810
|
+
resolve12(null);
|
|
3811
|
+
});
|
|
3812
|
+
req.on("error", () => resolve12(null));
|
|
3813
|
+
});
|
|
3814
|
+
}
|
|
3782
3815
|
function getDefaultBrowserId() {
|
|
3783
3816
|
try {
|
|
3784
3817
|
const output = (0, import_child_process.execSync)(
|
|
@@ -3795,14 +3828,8 @@ function getDefaultBrowserId() {
|
|
|
3795
3828
|
}
|
|
3796
3829
|
async function probeCdpPort(port) {
|
|
3797
3830
|
try {
|
|
3798
|
-
const
|
|
3799
|
-
|
|
3800
|
-
const resp = await fetch(`http://localhost:${port}/json/version`, {
|
|
3801
|
-
signal: controller.signal
|
|
3802
|
-
});
|
|
3803
|
-
clearTimeout(timeout);
|
|
3804
|
-
if (resp.ok) {
|
|
3805
|
-
const version = await resp.json();
|
|
3831
|
+
const version = await httpGetJson(port, "/json/version", 500);
|
|
3832
|
+
if (version) {
|
|
3806
3833
|
const browser = version.Browser;
|
|
3807
3834
|
const userAgent = version["User-Agent"] ?? "";
|
|
3808
3835
|
if (!browser) return null;
|
|
@@ -3824,14 +3851,7 @@ async function probeCdpPort(port) {
|
|
|
3824
3851
|
}
|
|
3825
3852
|
async function probeHasPageTarget(port) {
|
|
3826
3853
|
try {
|
|
3827
|
-
const
|
|
3828
|
-
const timeout = setTimeout(() => controller.abort(), 500);
|
|
3829
|
-
const resp = await fetch(`http://localhost:${port}/json/list`, {
|
|
3830
|
-
signal: controller.signal
|
|
3831
|
-
});
|
|
3832
|
-
clearTimeout(timeout);
|
|
3833
|
-
if (!resp.ok) return false;
|
|
3834
|
-
const list2 = await resp.json();
|
|
3854
|
+
const list2 = await httpGetJson(port, "/json/list", 500);
|
|
3835
3855
|
return Array.isArray(list2) && list2.some((t) => t.type === "page");
|
|
3836
3856
|
} catch {
|
|
3837
3857
|
return false;
|
|
@@ -3926,10 +3946,11 @@ async function detectCdpPort() {
|
|
|
3926
3946
|
}
|
|
3927
3947
|
return pickPreferredEndpoint(endpoints, getDefaultBrowserId()).port;
|
|
3928
3948
|
}
|
|
3929
|
-
var import_child_process, BROWSER_PATTERNS;
|
|
3949
|
+
var import_child_process, http, BROWSER_PATTERNS;
|
|
3930
3950
|
var init_detect = __esm({
|
|
3931
3951
|
"src/cdp/detect.ts"() {
|
|
3932
3952
|
import_child_process = require("child_process");
|
|
3953
|
+
http = __toESM(require("http"));
|
|
3933
3954
|
BROWSER_PATTERNS = {
|
|
3934
3955
|
"company.thebrowser.browser": ["arc"],
|
|
3935
3956
|
"com.google.chrome": ["chrome", "google chrome"],
|
|
@@ -9059,7 +9080,7 @@ function assertScrollDestination(value) {
|
|
|
9059
9080
|
function parseDoAction(action) {
|
|
9060
9081
|
if (action.startsWith("click:")) {
|
|
9061
9082
|
const target = action.slice("click:".length);
|
|
9062
|
-
if (!target) throw invalidInput("Invalid --do action: click requires a target \u2014 a css selector, ax:<name>, axid:<id>, or backend:<id>.");
|
|
9083
|
+
if (!target) throw invalidInput("Invalid --do action: click requires a target \u2014 a css selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, or backend:<id>.");
|
|
9063
9084
|
return { verb: "click", target };
|
|
9064
9085
|
}
|
|
9065
9086
|
if (action.startsWith("scroll:")) {
|
|
@@ -9070,7 +9091,7 @@ function parseDoAction(action) {
|
|
|
9070
9091
|
if (!isScrollDestination(to)) throw invalidInput("Invalid --do action: scroll requires `scroll:<target>,to=<top|bottom|px>`.");
|
|
9071
9092
|
return { verb: "scroll", target: spec.slice(0, comma), to };
|
|
9072
9093
|
}
|
|
9073
|
-
throw invalidInput("Unsupported --do action. Supported actions: click:<target>; scroll:<target>,to=<top|bottom|px> \u2014 target is a css selector, ax:<name>, axid:<id>, or backend:<id>.");
|
|
9094
|
+
throw invalidInput("Unsupported --do action. Supported actions: click:<target>; scroll:<target>,to=<top|bottom|px> \u2014 target is a css selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, or backend:<id>.");
|
|
9074
9095
|
}
|
|
9075
9096
|
var SWEEP_NUMERIC_AXES, SWEEP_ENUM_VALUES;
|
|
9076
9097
|
var init_leaf_grammar = __esm({
|
|
@@ -10295,7 +10316,7 @@ function validateHarAppendBatch(value, source) {
|
|
|
10295
10316
|
return { entries, incompleteLifecycles };
|
|
10296
10317
|
}
|
|
10297
10318
|
function captureVersion() {
|
|
10298
|
-
const version = "1.4.
|
|
10319
|
+
const version = "1.4.2";
|
|
10299
10320
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
10300
10321
|
}
|
|
10301
10322
|
function token() {
|
|
@@ -12086,7 +12107,7 @@ function wallTime(value) {
|
|
|
12086
12107
|
}
|
|
12087
12108
|
}
|
|
12088
12109
|
function captureVersion2() {
|
|
12089
|
-
const version = "1.4.
|
|
12110
|
+
const version = "1.4.2";
|
|
12090
12111
|
return typeof version === "string" && version.length > 0 ? version : "development";
|
|
12091
12112
|
}
|
|
12092
12113
|
function deepFreeze(value) {
|
|
@@ -16027,7 +16048,7 @@ async function resolveLiveTarget(client, input) {
|
|
|
16027
16048
|
return settleAxMatches("axid", input, matches);
|
|
16028
16049
|
}
|
|
16029
16050
|
case "css":
|
|
16030
|
-
return
|
|
16051
|
+
return resolveBareTarget(client, input, parsed.value);
|
|
16031
16052
|
}
|
|
16032
16053
|
}
|
|
16033
16054
|
function preferActionable(matches) {
|
|
@@ -16060,6 +16081,17 @@ function settleAxMatches(kind, input, rawMatches) {
|
|
|
16060
16081
|
}))
|
|
16061
16082
|
};
|
|
16062
16083
|
}
|
|
16084
|
+
async function resolveBareTarget(client, input, selector) {
|
|
16085
|
+
const css = await resolveLiveCss(client, input, selector);
|
|
16086
|
+
if (css.ok || css.code === "ambiguous") return css;
|
|
16087
|
+
const normalized2 = normalizeAccessibleName(input);
|
|
16088
|
+
if (!normalized2) return css;
|
|
16089
|
+
const matches = (await readFullAXTree(client)).filter(
|
|
16090
|
+
(node) => node.backendDOMNodeId !== void 0 && node.name?.value !== void 0 && normalizeAccessibleName(node.name.value) === normalized2
|
|
16091
|
+
);
|
|
16092
|
+
const ax = settleAxMatches("ax", input, matches);
|
|
16093
|
+
return ax.ok || ax.code === "ambiguous" ? ax : css;
|
|
16094
|
+
}
|
|
16063
16095
|
async function resolveLiveCss(client, input, selector) {
|
|
16064
16096
|
await client.send("DOM.enable");
|
|
16065
16097
|
const { root } = await client.send("DOM.getDocument", { depth: 0 });
|
|
@@ -16287,7 +16319,7 @@ function emitResolutionError(parsed, command, failure) {
|
|
|
16287
16319
|
{
|
|
16288
16320
|
tag: "error",
|
|
16289
16321
|
attrs: { command, code: "unsupported_prefix" },
|
|
16290
|
-
summary: fact`received: target \`${failure.input}\` — the \`text:\` prefix is not accepted by live driving verbs; expected one of: bare CSS selector, ax:<name>, axid:<id>, backend:<id>.`,
|
|
16322
|
+
summary: fact`received: target \`${failure.input}\` — the \`text:\` prefix is not accepted by live driving verbs; expected one of: bare CSS selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, backend:<id>.`,
|
|
16291
16323
|
followUp: fact`Retry \`${command}\` with \`ax:<name>\` (accessible-name substring) or a CSS selector; \`text:\` matching exists only on the query leaves.`
|
|
16292
16324
|
},
|
|
16293
16325
|
{ json: parsed.json }
|
|
@@ -16339,7 +16371,7 @@ async function cmdPageClick(parsed, _args) {
|
|
|
16339
16371
|
return emitInvalidInput(
|
|
16340
16372
|
parsed,
|
|
16341
16373
|
"page click",
|
|
16342
|
-
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector, ax:<name>, axid:<id>, or backend:<id>).`
|
|
16374
|
+
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector or exact accessible name, ax:<name>, axid:<id>, or backend:<id>).`
|
|
16343
16375
|
);
|
|
16344
16376
|
}
|
|
16345
16377
|
const settle = effectiveSettle(parsed, { standalone: 1e3, session: 2500 });
|
|
@@ -16390,7 +16422,7 @@ var init_click = __esm({
|
|
|
16390
16422
|
USAGE = `capture page click <target> \u2014 dispatch one real click on exactly one resolved element
|
|
16391
16423
|
|
|
16392
16424
|
input:
|
|
16393
|
-
<target> resolved against the LIVE page: bare CSS selector, ax:<name> (case-insensitive substring over accessible names), axid:<id>, backend:<id>. text: is not accepted. Exactly one match required \u2014 zero or many matches is a structured error listing candidates with backend:<id> retry keys.
|
|
16425
|
+
<target> resolved against the LIVE page: bare CSS selector (takes precedence) or exact accessible name when CSS finds none, ax:<name> (case-insensitive substring over accessible names), axid:<id>, backend:<id>. text: is not accepted. Exactly one match required \u2014 zero or many matches is a structured error listing candidates with backend:<id> retry keys.
|
|
16394
16426
|
--settle <ms> network-settle window applied after the click (default: 1000; 2500 with an active session; 0 disables)
|
|
16395
16427
|
--no-screenshot skip the auto-screenshot
|
|
16396
16428
|
output:
|
|
@@ -16466,7 +16498,7 @@ var init_type = __esm({
|
|
|
16466
16498
|
|
|
16467
16499
|
input:
|
|
16468
16500
|
<text> the text to insert (agent-supplied; echoed back in the result block)
|
|
16469
|
-
--into <target> focus this field first \u2014 resolved against the LIVE page: bare CSS selector, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required
|
|
16501
|
+
--into <target> focus this field first \u2014 resolved against the LIVE page: bare CSS selector (takes precedence) or exact accessible name when CSS finds none, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required
|
|
16470
16502
|
--settle <ms> network-settle window applied after typing (default: 500; 1500 with an active session; 0 disables)
|
|
16471
16503
|
--no-screenshot skip the auto-screenshot
|
|
16472
16504
|
output:
|
|
@@ -16487,7 +16519,7 @@ async function cmdPageScroll(parsed, _args) {
|
|
|
16487
16519
|
return emitInvalidInput(
|
|
16488
16520
|
parsed,
|
|
16489
16521
|
"page scroll",
|
|
16490
|
-
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector, ax:<name>, axid:<id>, or backend:<id>) plus --to <top|bottom|px>.`
|
|
16522
|
+
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector or exact accessible name, ax:<name>, axid:<id>, or backend:<id>) plus --to <top|bottom|px>.`
|
|
16491
16523
|
);
|
|
16492
16524
|
}
|
|
16493
16525
|
if (parsed.to === void 0) {
|
|
@@ -16554,7 +16586,7 @@ var init_scroll = __esm({
|
|
|
16554
16586
|
USAGE3 = `capture page scroll <target> --to <top|bottom|px> \u2014 scroll one resolved container to a position
|
|
16555
16587
|
|
|
16556
16588
|
input:
|
|
16557
|
-
<target> resolved against the LIVE page: bare CSS selector, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required \u2014 zero or many matches is a structured error listing candidates
|
|
16589
|
+
<target> resolved against the LIVE page: bare CSS selector (takes precedence) or exact accessible name when CSS finds none, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required \u2014 zero or many matches is a structured error listing candidates
|
|
16558
16590
|
--to <dest> required destination: top, bottom, or a pixel offset (scrollTop value)
|
|
16559
16591
|
--settle <ms> network-settle window applied after the scroll (default: 1000; 2500 with an active session; 0 disables)
|
|
16560
16592
|
--no-screenshot skip the auto-screenshot
|
|
@@ -64688,8 +64720,8 @@ ${lanes.join("\n")}
|
|
|
64688
64720
|
}
|
|
64689
64721
|
function createPerModuleNameCache() {
|
|
64690
64722
|
const directoryPathMap = /* @__PURE__ */ new Map();
|
|
64691
|
-
return { get, set };
|
|
64692
|
-
function
|
|
64723
|
+
return { get: get2, set };
|
|
64724
|
+
function get2(directory) {
|
|
64693
64725
|
return directoryPathMap.get(toPath(directory, currentDirectory, getCanonicalFileName));
|
|
64694
64726
|
}
|
|
64695
64727
|
function set(directory, result) {
|
|
@@ -168021,12 +168053,12 @@ ${lanes.join("\n")}
|
|
|
168021
168053
|
...info,
|
|
168022
168054
|
parseable: !!content,
|
|
168023
168055
|
fileName,
|
|
168024
|
-
get,
|
|
168056
|
+
get: get2,
|
|
168025
168057
|
has(dependencyName, inGroups) {
|
|
168026
|
-
return !!
|
|
168058
|
+
return !!get2(dependencyName, inGroups);
|
|
168027
168059
|
}
|
|
168028
168060
|
};
|
|
168029
|
-
function
|
|
168061
|
+
function get2(dependencyName, inGroups = 15) {
|
|
168030
168062
|
for (const [group2, deps7] of dependencyGroups) {
|
|
168031
168063
|
if (deps7 && inGroups & group2) {
|
|
168032
168064
|
const dep = deps7.get(dependencyName);
|
|
@@ -228091,7 +228123,7 @@ async function cmdPageShot(parsed, _args) {
|
|
|
228091
228123
|
return;
|
|
228092
228124
|
}
|
|
228093
228125
|
if (parsed.cropSelector !== void 0 && !parsed.cropSelector.trim()) {
|
|
228094
|
-
emitResult({ tag: "error", attrs: { command: "page shot", code: "invalid_crop_selector" }, summary: text`received an empty --crop-selector; expected a bare CSS selector, ax:<name>, axid:<id>, or backend:<id>.` }, { json: parsed.json });
|
|
228126
|
+
emitResult({ tag: "error", attrs: { command: "page shot", code: "invalid_crop_selector" }, summary: text`received an empty --crop-selector; expected a bare CSS selector or exact accessible name, ax:<name>, axid:<id>, or backend:<id>.` }, { json: parsed.json });
|
|
228095
228127
|
process.exitCode = 1;
|
|
228096
228128
|
return;
|
|
228097
228129
|
}
|
|
@@ -228202,7 +228234,7 @@ input:
|
|
|
228202
228234
|
--full-page transient override to the full scrollable content height for this capture
|
|
228203
228235
|
--color-scheme <dark|light> transient prefers-color-scheme override for this capture; cleared afterward
|
|
228204
228236
|
--crop <x,y,w,h> capture this page-coordinate CSS-pixel rect, intersected with the live visual viewport; x/y may be fractional, w/h must be positive
|
|
228205
|
-
--crop-selector <sel> resolve exactly one live target (bare CSS
|
|
228237
|
+
--crop-selector <sel> resolve exactly one live target (bare CSS takes precedence; exact accessible name applies when CSS finds none; ax:<name>, axid:<id>, or backend:<id>), scroll it into view, then crop its border box and surroundings
|
|
228206
228238
|
--pad <px> nonnegative integer CSS pixels added on every side of --crop-selector (default: 0; not accepted with --crop)
|
|
228207
228239
|
--zoom <factor> positive decimal CSS-to-image scale requested for a CSS crop; the 1600px image cap may produce a smaller reported scale
|
|
228208
228240
|
--out <path> destination file; default: the active session's shots/ sequence, or a fresh oneshot-*/page/ dir under the capture root when no session is active
|
|
@@ -228343,7 +228375,7 @@ use when driving or inspecting the tab a session opened or adopted; all verbs au
|
|
|
228343
228375
|
|
|
228344
228376
|
All verbs auto-target the active session tab; --target/--url override. The
|
|
228345
228377
|
driving verbs (click, type, scroll) resolve exactly one element via the
|
|
228346
|
-
unified target grammar \u2014 bare CSS selector, ax:<name>, axid:<id>,
|
|
228378
|
+
unified target grammar \u2014 bare CSS selector (which takes precedence) or exact accessible name when CSS finds none, ax:<name>, axid:<id>,
|
|
228347
228379
|
backend:<id> \u2014 and reject an ambiguous target with the candidate list.
|
|
228348
228380
|
During a live composed recording every verb routes through the recorder.
|
|
228349
228381
|
|
|
@@ -241487,7 +241519,7 @@ function recorderLiveClient(recorder) {
|
|
|
241487
241519
|
function resolutionError(failure) {
|
|
241488
241520
|
if (failure.code === "unsupported-prefix") {
|
|
241489
241521
|
return new DoActionError(
|
|
241490
|
-
`Unsupported --do target prefix in "${failure.input}": text: is query-leaf-only. Accepted prefixes: bare css selector, ax:<name>, axid:<id>, backend:<id>.`,
|
|
241522
|
+
`Unsupported --do target prefix in "${failure.input}": text: is query-leaf-only. Accepted prefixes: bare css selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, backend:<id>.`,
|
|
241491
241523
|
"unsupported_target_prefix"
|
|
241492
241524
|
);
|
|
241493
241525
|
}
|
|
@@ -241793,7 +241825,7 @@ var init_rec = __esm({
|
|
|
241793
241825
|
input:
|
|
241794
241826
|
[url] --do <action> one-shot: open <url> (or record the active session tab when <url> is omitted), record one action, finalize
|
|
241795
241827
|
action click:<target> | scroll:<target>,to=<top|bottom|px>
|
|
241796
|
-
target css selector (bare string) | ax:<name> (case-insensitive substring) | axid:<id> | backend:<id>
|
|
241828
|
+
target css selector (takes precedence) or exact accessible name when CSS finds none (bare string) | ax:<name> (case-insensitive substring) | axid:<id> | backend:<id>
|
|
241797
241829
|
must resolve to exactly one live element; text: is not accepted by driving actions
|
|
241798
241830
|
--duration <seconds> keep recording after the action (default: 0)
|
|
241799
241831
|
--viewport <WxH> emulate a viewport for the recording window (restored after); exact <positive-safe-int>x<positive-safe-int> grammar with lowercase x and no whitespace
|
|
@@ -243312,7 +243344,7 @@ precedence: explicit flag > active session > env. \`session start\` adopts only
|
|
|
243312
243344
|
explicit --target.`;
|
|
243313
243345
|
}
|
|
243314
243346
|
function printVersion() {
|
|
243315
|
-
const declared = "1.4.
|
|
243347
|
+
const declared = "1.4.2";
|
|
243316
243348
|
if (declared) {
|
|
243317
243349
|
console.log(declared);
|
|
243318
243350
|
return;
|
package/package.json
CHANGED
|
@@ -72,7 +72,7 @@ const USAGE = `capture motion rec — record the page over time: composed (whate
|
|
|
72
72
|
input:
|
|
73
73
|
[url] --do <action> one-shot: open <url> (or record the active session tab when <url> is omitted), record one action, finalize
|
|
74
74
|
action click:<target> | scroll:<target>,to=<top|bottom|px>
|
|
75
|
-
target css selector (bare string) | ax:<name> (case-insensitive substring) | axid:<id> | backend:<id>
|
|
75
|
+
target css selector (takes precedence) or exact accessible name when CSS finds none (bare string) | ax:<name> (case-insensitive substring) | axid:<id> | backend:<id>
|
|
76
76
|
must resolve to exactly one live element; text: is not accepted by driving actions
|
|
77
77
|
--duration <seconds> keep recording after the action (default: 0)
|
|
78
78
|
--viewport <WxH> emulate a viewport for the recording window (restored after); exact <positive-safe-int>x<positive-safe-int> grammar with lowercase x and no whitespace
|
|
@@ -212,7 +212,7 @@ function recorderLiveClient(recorder: Pick<RecorderSession, 'handleCdp'>): LiveC
|
|
|
212
212
|
function resolutionError(failure: ResolutionFailure): DoActionError {
|
|
213
213
|
if (failure.code === 'unsupported-prefix') {
|
|
214
214
|
return new DoActionError(
|
|
215
|
-
`Unsupported --do target prefix in "${failure.input}": text: is query-leaf-only. Accepted prefixes: bare css selector, ax:<name>, axid:<id>, backend:<id>.`,
|
|
215
|
+
`Unsupported --do target prefix in "${failure.input}": text: is query-leaf-only. Accepted prefixes: bare css selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, backend:<id>.`,
|
|
216
216
|
'unsupported_target_prefix',
|
|
217
217
|
);
|
|
218
218
|
}
|
|
@@ -112,7 +112,7 @@ export function emitResolutionError(parsed: ParsedArgs, command: string, failure
|
|
|
112
112
|
{
|
|
113
113
|
tag: 'error',
|
|
114
114
|
attrs: { command, code: 'unsupported_prefix' },
|
|
115
|
-
summary: fact`received: target \`${failure.input}\` — the \`text:\` prefix is not accepted by live driving verbs; expected one of: bare CSS selector, ax:<name>, axid:<id>, backend:<id>.`,
|
|
115
|
+
summary: fact`received: target \`${failure.input}\` — the \`text:\` prefix is not accepted by live driving verbs; expected one of: bare CSS selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, backend:<id>.`,
|
|
116
116
|
followUp: fact`Retry \`${command}\` with \`ax:<name>\` (accessible-name substring) or a CSS selector; \`text:\` matching exists only on the query leaves.`,
|
|
117
117
|
},
|
|
118
118
|
{ json: parsed.json },
|
|
@@ -170,7 +170,7 @@ export function emitResolutionError(parsed: ParsedArgs, command: string, failure
|
|
|
170
170
|
const USAGE = `capture page click <target> — dispatch one real click on exactly one resolved element
|
|
171
171
|
|
|
172
172
|
input:
|
|
173
|
-
<target> resolved against the LIVE page: bare CSS selector, ax:<name> (case-insensitive substring over accessible names), axid:<id>, backend:<id>. text: is not accepted. Exactly one match required — zero or many matches is a structured error listing candidates with backend:<id> retry keys.
|
|
173
|
+
<target> resolved against the LIVE page: bare CSS selector (takes precedence) or exact accessible name when CSS finds none, ax:<name> (case-insensitive substring over accessible names), axid:<id>, backend:<id>. text: is not accepted. Exactly one match required — zero or many matches is a structured error listing candidates with backend:<id> retry keys.
|
|
174
174
|
--settle <ms> network-settle window applied after the click (default: 1000; 2500 with an active session; 0 disables)
|
|
175
175
|
--no-screenshot skip the auto-screenshot
|
|
176
176
|
output:
|
|
@@ -188,7 +188,7 @@ export async function cmdPageClick(parsed: ParsedArgs, _args: string[]): Promise
|
|
|
188
188
|
return emitInvalidInput(
|
|
189
189
|
parsed,
|
|
190
190
|
'page click',
|
|
191
|
-
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector, ax:<name>, axid:<id>, or backend:<id>).`,
|
|
191
|
+
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector or exact accessible name, ax:<name>, axid:<id>, or backend:<id>).`,
|
|
192
192
|
);
|
|
193
193
|
}
|
|
194
194
|
|
|
@@ -30,7 +30,7 @@ export const PAGE_USAGE = `capture page — verbs against the live session tab:
|
|
|
30
30
|
|
|
31
31
|
All verbs auto-target the active session tab; --target/--url override. The
|
|
32
32
|
driving verbs (click, type, scroll) resolve exactly one element via the
|
|
33
|
-
unified target grammar — bare CSS selector, ax:<name>, axid:<id>,
|
|
33
|
+
unified target grammar — bare CSS selector (which takes precedence) or exact accessible name when CSS finds none, ax:<name>, axid:<id>,
|
|
34
34
|
backend:<id> — and reject an ambiguous target with the candidate list.
|
|
35
35
|
During a live composed recording every verb routes through the recorder.
|
|
36
36
|
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
const USAGE = `capture page scroll <target> --to <top|bottom|px> — scroll one resolved container to a position
|
|
23
23
|
|
|
24
24
|
input:
|
|
25
|
-
<target> resolved against the LIVE page: bare CSS selector, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required — zero or many matches is a structured error listing candidates
|
|
25
|
+
<target> resolved against the LIVE page: bare CSS selector (takes precedence) or exact accessible name when CSS finds none, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required — zero or many matches is a structured error listing candidates
|
|
26
26
|
--to <dest> required destination: top, bottom, or a pixel offset (scrollTop value)
|
|
27
27
|
--settle <ms> network-settle window applied after the scroll (default: 1000; 2500 with an active session; 0 disables)
|
|
28
28
|
--no-screenshot skip the auto-screenshot
|
|
@@ -41,7 +41,7 @@ export async function cmdPageScroll(parsed: ParsedArgs, _args: string[]): Promis
|
|
|
41
41
|
return emitInvalidInput(
|
|
42
42
|
parsed,
|
|
43
43
|
'page scroll',
|
|
44
|
-
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector, ax:<name>, axid:<id>, or backend:<id>) plus --to <top|bottom|px>.`,
|
|
44
|
+
fact`received: ${parsed.positional.length} positional arguments; expected exactly one target (CSS selector or exact accessible name, ax:<name>, axid:<id>, or backend:<id>) plus --to <top|bottom|px>.`,
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
if (parsed.to === undefined) {
|
|
@@ -42,7 +42,7 @@ input:
|
|
|
42
42
|
--full-page transient override to the full scrollable content height for this capture
|
|
43
43
|
--color-scheme <dark|light> transient prefers-color-scheme override for this capture; cleared afterward
|
|
44
44
|
--crop <x,y,w,h> capture this page-coordinate CSS-pixel rect, intersected with the live visual viewport; x/y may be fractional, w/h must be positive
|
|
45
|
-
--crop-selector <sel> resolve exactly one live target (bare CSS
|
|
45
|
+
--crop-selector <sel> resolve exactly one live target (bare CSS takes precedence; exact accessible name applies when CSS finds none; ax:<name>, axid:<id>, or backend:<id>), scroll it into view, then crop its border box and surroundings
|
|
46
46
|
--pad <px> nonnegative integer CSS pixels added on every side of --crop-selector (default: 0; not accepted with --crop)
|
|
47
47
|
--zoom <factor> positive decimal CSS-to-image scale requested for a CSS crop; the 1600px image cap may produce a smaller reported scale
|
|
48
48
|
--out <path> destination file; default: the active session's shots/ sequence, or a fresh oneshot-*/page/ dir under the capture root when no session is active
|
|
@@ -266,7 +266,7 @@ export async function cmdPageShot(parsed: ParsedArgs, _args: string[]): Promise<
|
|
|
266
266
|
return;
|
|
267
267
|
}
|
|
268
268
|
if (parsed.cropSelector !== undefined && !parsed.cropSelector.trim()) {
|
|
269
|
-
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'invalid_crop_selector' }, summary: text`received an empty --crop-selector; expected a bare CSS selector, ax:<name>, axid:<id>, or backend:<id>.` }, { json: parsed.json });
|
|
269
|
+
emitResult({ tag: 'error', attrs: { command: 'page shot', code: 'invalid_crop_selector' }, summary: text`received an empty --crop-selector; expected a bare CSS selector or exact accessible name, ax:<name>, axid:<id>, or backend:<id>.` }, { json: parsed.json });
|
|
270
270
|
process.exitCode = 1;
|
|
271
271
|
return;
|
|
272
272
|
}
|
|
@@ -28,7 +28,7 @@ const USAGE = `capture page type <text> [--into <target>] — type text into the
|
|
|
28
28
|
|
|
29
29
|
input:
|
|
30
30
|
<text> the text to insert (agent-supplied; echoed back in the result block)
|
|
31
|
-
--into <target> focus this field first — resolved against the LIVE page: bare CSS selector, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required
|
|
31
|
+
--into <target> focus this field first — resolved against the LIVE page: bare CSS selector (takes precedence) or exact accessible name when CSS finds none, ax:<name> (case-insensitive substring), axid:<id>, backend:<id>; text: is not accepted; exactly one match required
|
|
32
32
|
--settle <ms> network-settle window applied after typing (default: 500; 1500 with an active session; 0 disables)
|
|
33
33
|
--no-screenshot skip the auto-screenshot
|
|
34
34
|
output:
|
package/src/cdp/detect.ts
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
import { execSync } from 'child_process';
|
|
2
|
+
import * as http from 'http';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A CDP-endpoint JSON probe that CANNOT leak a socket handle. Node's built-in
|
|
6
|
+
* `fetch` (undici) leaves a dangling socket + request handle when a
|
|
7
|
+
* connect-phase abort fires against a port that accepts TCP but never sends an
|
|
8
|
+
* HTTP response (e.g. a docker-proxied or half-open listener). That handle
|
|
9
|
+
* keeps this short-lived CLI process alive for undici's ~7s internal socket
|
|
10
|
+
* timeout AFTER all real work is done, so a single dead port on the machine
|
|
11
|
+
* adds ~7s to every command that runs port detection. A raw `http.get` whose
|
|
12
|
+
* request we explicitly `destroy()` on timeout releases the handle immediately.
|
|
13
|
+
*
|
|
14
|
+
* Resolves to the parsed JSON body on a 2xx response, or `null` on any
|
|
15
|
+
* timeout, connection error, non-2xx status, or unparseable body.
|
|
16
|
+
*/
|
|
17
|
+
function httpGetJson(
|
|
18
|
+
port: number,
|
|
19
|
+
path: string,
|
|
20
|
+
timeoutMs: number,
|
|
21
|
+
): Promise<unknown | null> {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const req = http.get(
|
|
24
|
+
{ host: 'localhost', port, path, timeout: timeoutMs },
|
|
25
|
+
(res) => {
|
|
26
|
+
const status = res.statusCode ?? 0;
|
|
27
|
+
if (status < 200 || status >= 300) {
|
|
28
|
+
res.resume();
|
|
29
|
+
resolve(null);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let body = '';
|
|
33
|
+
res.setEncoding('utf-8');
|
|
34
|
+
res.on('data', (chunk) => {
|
|
35
|
+
body += chunk;
|
|
36
|
+
});
|
|
37
|
+
res.on('end', () => {
|
|
38
|
+
try {
|
|
39
|
+
resolve(JSON.parse(body));
|
|
40
|
+
} catch {
|
|
41
|
+
resolve(null);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
res.on('error', () => resolve(null));
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
req.on('timeout', () => {
|
|
48
|
+
req.destroy();
|
|
49
|
+
resolve(null);
|
|
50
|
+
});
|
|
51
|
+
req.on('error', () => resolve(null));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
2
54
|
|
|
3
55
|
export const BROWSER_PATTERNS: Record<string, string[]> = {
|
|
4
56
|
'company.thebrowser.browser': ['arc'],
|
|
@@ -40,18 +92,12 @@ interface CdpProbeResult {
|
|
|
40
92
|
// being confused for the intended browser.
|
|
41
93
|
async function probeCdpPort(port: number): Promise<CdpProbeResult | null> {
|
|
42
94
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (resp.ok) {
|
|
50
|
-
const version = (await resp.json()) as {
|
|
51
|
-
Browser?: string;
|
|
52
|
-
'User-Agent'?: string;
|
|
53
|
-
webSocketDebuggerUrl?: string;
|
|
54
|
-
};
|
|
95
|
+
const version = (await httpGetJson(port, '/json/version', 500)) as {
|
|
96
|
+
Browser?: string;
|
|
97
|
+
'User-Agent'?: string;
|
|
98
|
+
webSocketDebuggerUrl?: string;
|
|
99
|
+
} | null;
|
|
100
|
+
if (version) {
|
|
55
101
|
const browser = version.Browser;
|
|
56
102
|
const userAgent = version['User-Agent'] ?? '';
|
|
57
103
|
if (!browser) return null;
|
|
@@ -83,14 +129,9 @@ async function probeCdpPort(port: number): Promise<CdpProbeResult | null> {
|
|
|
83
129
|
|
|
84
130
|
async function probeHasPageTarget(port: number): Promise<boolean> {
|
|
85
131
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
signal: controller.signal,
|
|
90
|
-
});
|
|
91
|
-
clearTimeout(timeout);
|
|
92
|
-
if (!resp.ok) return false;
|
|
93
|
-
const list = (await resp.json()) as Array<{ type?: string }>;
|
|
132
|
+
const list = (await httpGetJson(port, '/json/list', 500)) as Array<{
|
|
133
|
+
type?: string;
|
|
134
|
+
}> | null;
|
|
94
135
|
return Array.isArray(list) && list.some((t) => t.type === 'page');
|
|
95
136
|
} catch {
|
|
96
137
|
return false;
|
package/src/cdp/leaf-grammar.ts
CHANGED
|
@@ -84,7 +84,7 @@ export type DoAction =
|
|
|
84
84
|
export function parseDoAction(action: string): DoAction {
|
|
85
85
|
if (action.startsWith('click:')) {
|
|
86
86
|
const target = action.slice('click:'.length);
|
|
87
|
-
if (!target) throw invalidInput('Invalid --do action: click requires a target — a css selector, ax:<name>, axid:<id>, or backend:<id>.');
|
|
87
|
+
if (!target) throw invalidInput('Invalid --do action: click requires a target — a css selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, or backend:<id>.');
|
|
88
88
|
return { verb: 'click', target };
|
|
89
89
|
}
|
|
90
90
|
if (action.startsWith('scroll:')) {
|
|
@@ -95,5 +95,5 @@ export function parseDoAction(action: string): DoAction {
|
|
|
95
95
|
if (!isScrollDestination(to)) throw invalidInput('Invalid --do action: scroll requires `scroll:<target>,to=<top|bottom|px>`.');
|
|
96
96
|
return { verb: 'scroll', target: spec.slice(0, comma), to };
|
|
97
97
|
}
|
|
98
|
-
throw invalidInput('Unsupported --do action. Supported actions: click:<target>; scroll:<target>,to=<top|bottom|px> — target is a css selector, ax:<name>, axid:<id>, or backend:<id>.');
|
|
98
|
+
throw invalidInput('Unsupported --do action. Supported actions: click:<target>; scroll:<target>,to=<top|bottom|px> — target is a css selector (or exact accessible name when CSS finds none), ax:<name>, axid:<id>, or backend:<id>.');
|
|
99
99
|
}
|
package/src/interact.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (`src/output/selector.ts`'s `parseSelectorInput`) and resolves it against
|
|
7
7
|
* the LIVE page (not collector-stored records), per prefix:
|
|
8
8
|
*
|
|
9
|
-
* bare string live CSS query
|
|
9
|
+
* bare string live CSS query, or an exact accessible-name match when CSS finds none
|
|
10
10
|
* backend:<id> identity — the canonical retry key in every resolution failure
|
|
11
11
|
* ax:<name> case-insensitive SUBSTRING over live `Accessibility.getFullAXTree` names
|
|
12
12
|
* axid:<id> AX node id from the same live fetch
|
|
@@ -172,7 +172,7 @@ export async function resolveLiveTarget(
|
|
|
172
172
|
return settleAxMatches('axid', input, matches);
|
|
173
173
|
}
|
|
174
174
|
case 'css':
|
|
175
|
-
return
|
|
175
|
+
return resolveBareTarget(client, input, parsed.value);
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
|
|
@@ -220,6 +220,32 @@ function settleAxMatches(
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Bare targets preserve CSS behavior when they select one or many elements;
|
|
225
|
+
* when CSS selects none (or rejects the string), an exact accessible-name
|
|
226
|
+
* lookup lets an element name emitted by `page elements` drive directly.
|
|
227
|
+
* `ax:` remains the explicit case-insensitive substring query.
|
|
228
|
+
*/
|
|
229
|
+
async function resolveBareTarget(
|
|
230
|
+
client: LiveClient,
|
|
231
|
+
input: string,
|
|
232
|
+
selector: string,
|
|
233
|
+
): Promise<ResolvedTarget | ResolutionFailure> {
|
|
234
|
+
const css = await resolveLiveCss(client, input, selector);
|
|
235
|
+
if (css.ok || css.code === 'ambiguous') return css;
|
|
236
|
+
|
|
237
|
+
const normalized = normalizeAccessibleName(input);
|
|
238
|
+
if (!normalized) return css;
|
|
239
|
+
const matches = (await readFullAXTree(client)).filter(
|
|
240
|
+
(node) =>
|
|
241
|
+
node.backendDOMNodeId !== undefined &&
|
|
242
|
+
node.name?.value !== undefined &&
|
|
243
|
+
normalizeAccessibleName(node.name.value) === normalized,
|
|
244
|
+
);
|
|
245
|
+
const ax = settleAxMatches('ax', input, matches);
|
|
246
|
+
return ax.ok || ax.code === 'ambiguous' ? ax : css;
|
|
247
|
+
}
|
|
248
|
+
|
|
223
249
|
async function resolveLiveCss(
|
|
224
250
|
client: LiveClient,
|
|
225
251
|
input: string,
|