@crouton-kit/capture 1.3.8 → 1.3.9
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 +29841 -8539
- package/package.json +6 -3
- package/src/capture.ts +86 -173
- package/src/cdp/a11y.ts +113 -14
- package/src/cdp/args.ts +300 -205
- package/src/cdp/bridge/protocol.ts +93 -6
- package/src/cdp/bridge/server.ts +118 -53
- package/src/cdp/bridge/spawn.ts +53 -6
- package/src/cdp/client.ts +13 -3
- package/src/cdp/commands/bridge-serve.ts +20 -14
- package/src/cdp/commands/cdp.ts +259 -90
- package/src/cdp/commands/gate-guard.ts +43 -0
- package/src/cdp/commands/lib.ts +339 -213
- package/src/cdp/commands/measure/census.ts +76 -20
- package/src/cdp/commands/measure/check.ts +100 -26
- package/src/cdp/commands/measure/diff.ts +109 -21
- package/src/cdp/commands/measure/explain.ts +121 -19
- package/src/cdp/commands/measure/index.ts +31 -39
- package/src/cdp/commands/measure/map-ax.ts +52 -0
- package/src/cdp/commands/measure/map-focus.ts +41 -13
- package/src/cdp/commands/measure/map-layers.ts +49 -15
- package/src/cdp/commands/measure/map-scroll.ts +85 -15
- package/src/cdp/commands/measure/snap.ts +405 -25
- package/src/cdp/commands/measure/sweep.ts +217 -20
- package/src/cdp/commands/motion/index.ts +18 -17
- package/src/cdp/commands/motion/jank.ts +119 -9
- package/src/cdp/commands/motion/mask.ts +81 -12
- package/src/cdp/commands/motion/rec.ts +520 -19
- package/src/cdp/commands/motion/response.ts +65 -15
- package/src/cdp/commands/motion/timeline.ts +139 -14
- package/src/cdp/commands/page/click.ts +194 -0
- package/src/cdp/commands/page/elements.ts +154 -0
- package/src/cdp/commands/page/exec.ts +317 -0
- package/src/cdp/commands/page/index.ts +72 -0
- package/src/cdp/commands/page/navigate.ts +330 -0
- package/src/cdp/commands/page/scroll.ts +107 -0
- package/src/cdp/commands/page/shot.ts +208 -0
- package/src/cdp/commands/page/type.ts +104 -0
- package/src/cdp/commands/tab/index.ts +59 -0
- package/src/cdp/commands/tab/list.ts +167 -0
- package/src/cdp/commands/tab/network.ts +162 -0
- package/src/cdp/commands/tab/open.ts +90 -0
- package/src/cdp/commands/tab/reset.ts +160 -0
- package/src/cdp/connection.ts +368 -37
- package/src/cdp/detect.ts +42 -13
- package/src/cdp/dispatch.ts +74 -28
- package/src/cdp/har-recorder.ts +648 -115
- package/src/cdp/leaf-grammar.ts +99 -0
- package/src/cdp/measure/census.ts +205 -0
- package/src/cdp/measure/check.ts +318 -0
- package/src/cdp/measure/collectors/animation.ts +385 -0
- package/src/cdp/measure/collectors/ax.ts +278 -0
- package/src/cdp/measure/collectors/focus.ts +1041 -0
- package/src/cdp/measure/collectors/forms.ts +905 -0
- package/src/cdp/measure/collectors/geometry.ts +1038 -0
- package/src/cdp/measure/collectors/hittest.ts +835 -0
- package/src/cdp/measure/collectors/layers.ts +697 -0
- package/src/cdp/measure/collectors/media.ts +522 -0
- package/src/cdp/measure/collectors/pixels.ts +1584 -0
- package/src/cdp/measure/collectors/queries.ts +464 -0
- package/src/cdp/measure/collectors/scroll.ts +599 -0
- package/src/cdp/measure/collectors/states.ts +992 -0
- package/src/cdp/measure/collectors/style-provenance.ts +643 -0
- package/src/cdp/measure/collectors/styles.ts +355 -0
- package/src/cdp/measure/collectors/text.ts +713 -0
- package/src/cdp/measure/diff.ts +470 -0
- package/src/cdp/measure/explain.ts +498 -0
- package/src/cdp/measure/map-ax.ts +633 -0
- package/src/cdp/measure/map-focus.ts +188 -0
- package/src/cdp/measure/map-layers.ts +177 -0
- package/src/cdp/measure/map-scroll.ts +317 -0
- package/src/cdp/measure/redaction.ts +51 -0
- package/src/cdp/measure/settle.ts +1051 -0
- package/src/cdp/measure/snapshot.ts +390 -0
- package/src/cdp/measure/sweep.ts +330 -0
- package/src/cdp/measure/types.ts +220 -0
- package/src/cdp/measure/v2/collectors.ts +19 -0
- package/src/cdp/measure/v2/orchestrator.ts +46 -0
- package/src/cdp/measure/v2/snap-selection.ts +33 -0
- package/src/cdp/measure/v2/target-lock.ts +15 -0
- package/src/cdp/measure/v2/target.ts +16 -0
- package/src/cdp/motion/jank.ts +433 -0
- package/src/cdp/motion/mask.ts +421 -0
- package/src/cdp/motion/recorder.ts +764 -0
- package/src/cdp/motion/response.ts +356 -0
- package/src/cdp/motion/timeline.ts +215 -0
- package/src/cdp/record.ts +5 -212
- package/src/cdp/recorder-bridge.ts +1478 -105
- package/src/cdp/recorder-client.ts +326 -0
- package/src/cdp/scope-lock.ts +73 -0
- package/src/cdp/screenshot.ts +199 -66
- package/src/cdp/targets.ts +21 -1
- package/src/cdp/timing.ts +15 -10
- package/src/cdp/types.ts +22 -7
- package/src/cdp/viewport.ts +21 -0
- package/src/cdp.ts +12 -58
- package/src/command-help.ts +29 -0
- package/src/command-resolution.ts +22 -0
- package/src/command-walker.ts +35 -0
- package/src/contracts/a11y.ts +33 -0
- package/src/contracts/cursor.ts +35 -0
- package/src/contracts/index.ts +15 -0
- package/src/contracts/motion.ts +19 -0
- package/src/contracts/neutral.ts +56 -0
- package/src/contracts/primitives.ts +205 -0
- package/src/contracts/release.ts +17 -0
- package/src/contracts/results.ts +208 -0
- package/src/contracts/routes.ts +140 -0
- package/src/contracts/snapshot.ts +92 -0
- package/src/cursor/index.ts +13 -0
- package/src/cursor/service.ts +310 -0
- package/src/errors.ts +71 -0
- package/src/har-manager.ts +992 -41
- package/src/interact.ts +410 -98
- package/src/output/artifact-lifecycle.ts +371 -0
- package/src/output/artifact.ts +6 -0
- package/src/output/exact-raw.ts +114 -0
- package/src/output/render.ts +396 -2
- package/src/output/structured.ts +211 -0
- package/src/registry.ts +208 -0
- package/src/session/artifacts.ts +452 -200
- package/src/session/commands.ts +1162 -397
- package/src/session/coordinator.ts +251 -0
- package/src/session/har-filter.ts +57 -0
- package/src/session/log-tailer.ts +653 -0
- package/src/session-context.ts +279 -51
- package/src/vault/bundle.ts +32 -17
- package/src/cdp/commands/exec.ts +0 -105
- package/src/cdp/commands/har.ts +0 -128
- package/src/cdp/commands/tabs.ts +0 -183
- package/src/cdp/commands/traffic.ts +0 -121
- package/src/cdp/commands/ui.ts +0 -172
- package/src/cdp/execute.ts +0 -113
- package/src/cdp/har-output.ts +0 -47
- package/src/cdp/usage.ts +0 -79
- package/src/tab-session.ts +0 -91
package/src/cdp/commands/cdp.ts
CHANGED
|
@@ -9,70 +9,161 @@
|
|
|
9
9
|
* per-CLIENT and reverts the instant its websocket disconnects, so a plain
|
|
10
10
|
* one-shot call here only lasts for that single command \u2014 running inside a
|
|
11
11
|
* held session is what keeps it alive across commands.
|
|
12
|
+
*
|
|
13
|
+
* Output: one `<cdp-result>` block via `src/output/render.ts` \u2014 the protocol
|
|
14
|
+
* result/event as a capped inline data payload, mirrored at full fidelity
|
|
15
|
+
* under `--json` (D11). Params stay inline `--params <json>`, a documented
|
|
16
|
+
* spec deviation: raw CDP params are small protocol-defined objects and this
|
|
17
|
+
* leaf is the diagnostic escape hatch, so file indirection buys nothing.
|
|
12
18
|
*/
|
|
13
19
|
|
|
14
20
|
import * as fs from 'fs';
|
|
21
|
+
import { CaptureError, captureError, invalidInput } from '../../errors.js';
|
|
15
22
|
import { getActiveSession } from '../../session-context.js';
|
|
16
23
|
import { connectForCommand } from '../connection.js';
|
|
17
24
|
import { getBrowserClient, findTabById } from '../targets.js';
|
|
18
25
|
import { detectCdpPort } from '../detect.js';
|
|
19
26
|
import { sendBridgeRequest } from '../bridge/client.js';
|
|
20
|
-
import {
|
|
27
|
+
import { isRecorderHeldClient } from '../recorder-client.js';
|
|
21
28
|
import { type ParsedArgs } from '../types.js';
|
|
29
|
+
import {
|
|
30
|
+
capped,
|
|
31
|
+
data,
|
|
32
|
+
emitResult,
|
|
33
|
+
line,
|
|
34
|
+
text,
|
|
35
|
+
type FactLine,
|
|
36
|
+
} from '../../output/render.js';
|
|
22
37
|
|
|
23
38
|
const DEFAULT_TIMEOUT_MS = 10000;
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
--
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
--
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
40
|
+
/** Generous inline cap for the protocol result/event payload (D11): big
|
|
41
|
+
* enough for typical protocol objects, still bounded in the prose block.
|
|
42
|
+
* `--json` carries the payload uncapped (full fidelity). */
|
|
43
|
+
const GENEROUS_RESULT_CAP = 4000;
|
|
44
|
+
|
|
45
|
+
/** Root-help representation of this leaf, assembled by `src/capture.ts`. */
|
|
46
|
+
export const COMMAND_BLOCK = `<command name="cdp">
|
|
47
|
+
raw CDP escape hatch — send any protocol method capture doesn't wrap, or wait for a protocol event
|
|
48
|
+
use when no other capture command covers the protocol surface you need (Browser.*, ServiceWorker.*, Target.*, ...)
|
|
49
|
+
cdp [<Domain.method>] [--params <json>] [--browser] [--wait-event <name>] — \`capture cdp -h\`
|
|
50
|
+
</command>`;
|
|
51
|
+
|
|
52
|
+
const HELP = `capture cdp — send a raw CDP protocol method and/or wait for a protocol event: the escape hatch for domains no other capture command wraps (Browser.*, ServiceWorker.*, Target.*, ...).
|
|
53
|
+
|
|
54
|
+
Input:
|
|
55
|
+
<Domain.method> protocol method to send. At least one of <Domain.method> / --wait-event is required.
|
|
56
|
+
--params <json> JSON-encoded params object for the method. Spec deviation: params stay inline JSON (no file indirection) — raw CDP params are small protocol-defined objects.
|
|
57
|
+
--wait-event <Domain.event> wait for (and return) the next occurrence of a protocol event; combinable with a method (the method is sent first) or usable alone.
|
|
58
|
+
--browser route through the held connection (session start --hold) instead of a one-shot page websocket. Connection-scoped state (permission grants, domain enables) reverts the instant its connection closes — it survives across commands only inside a held session.
|
|
59
|
+
--target <id> with --browser: attach a flattened CDP session on the held connection to this target (for target-scoped domains); without --browser: the page target to run against. 8-char id prefix accepted.
|
|
60
|
+
--timeout <ms> event-wait timeout (default ${DEFAULT_TIMEOUT_MS}ms).
|
|
61
|
+
|
|
62
|
+
Output:
|
|
63
|
+
<cdp-result method=… wait-event=… scope=…> — the protocol result and/or awaited event as an escaped, length-capped JSON payload. --json mirrors the same block with the payload at full fidelity.
|
|
64
|
+
|
|
65
|
+
Effects:
|
|
66
|
+
Sends the method verbatim to the browser — whatever browser/page state the protocol call mutates, it mutates. Writes no artifacts.`;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The minimal client surface both scopes drive. Satisfied structurally by
|
|
70
|
+
* `CDPClient`, `RecorderHeldClient`, and test stubs \u2014 the injectable
|
|
71
|
+
* `connect` parameter on `runPageScope` exists so tests can exercise the
|
|
72
|
+
* command wiring against a stub without a live browser.
|
|
73
|
+
*/
|
|
74
|
+
export interface CdpScopeClient {
|
|
75
|
+
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
76
|
+
on(event: string, handler: (params: unknown) => void): void;
|
|
77
|
+
close(): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The browser-level one-shot client surface: the same shape as {@link CdpScopeClient}
|
|
81
|
+
* plus the flattened-session `send(..., timeout, sessionId)` overload. Satisfied
|
|
82
|
+
* by `CDPClient`; the injectable `deps` on `runBrowserScope` lets tests drive
|
|
83
|
+
* this without a live browser. */
|
|
84
|
+
export interface BrowserScopeClient extends CdpScopeClient {
|
|
85
|
+
send(method: string, params?: Record<string, unknown>, timeout?: number, sessionId?: string): Promise<unknown>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Seams `runBrowserScope` reaches the browser through — real by default,
|
|
89
|
+
* injectable in `test/cdp-command.test.ts`. */
|
|
90
|
+
export interface BrowserScopeDeps {
|
|
91
|
+
sendBridgeRequest: typeof sendBridgeRequest;
|
|
92
|
+
getBrowserClient: (port: number) => Promise<{ client: BrowserScopeClient }>;
|
|
93
|
+
findTabById: (port: number, targetId: string) => Promise<{ id: string } | null>;
|
|
94
|
+
detectCdpPort: typeof detectCdpPort;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const DEFAULT_BROWSER_DEPS: BrowserScopeDeps = { sendBridgeRequest, getBrowserClient, findTabById, detectCdpPort };
|
|
98
|
+
|
|
99
|
+
export interface CdpResultOptions {
|
|
100
|
+
method?: string;
|
|
101
|
+
waitEvent?: string;
|
|
102
|
+
scope: 'browser' | 'page';
|
|
103
|
+
target?: string;
|
|
104
|
+
result?: unknown;
|
|
105
|
+
event?: unknown;
|
|
106
|
+
json?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Emit the `<cdp-result>` block. The protocol result/event is embedded as a
|
|
111
|
+
* JSON-stringified data payload: capped at `GENEROUS_RESULT_CAP` in prose,
|
|
112
|
+
* uncapped (full fidelity) under `--json`. Exported for `test/cdp-command.test.ts`.
|
|
113
|
+
*/
|
|
114
|
+
export function emitCdpResult(opts: CdpResultOptions): void {
|
|
115
|
+
const sections: FactLine[] = [];
|
|
116
|
+
const payloadCap = (payload: string): number => (opts.json ? payload.length : GENEROUS_RESULT_CAP);
|
|
117
|
+
if (opts.method) {
|
|
118
|
+
const payload = JSON.stringify(opts.result) ?? 'undefined';
|
|
119
|
+
sections.push(line(text`result: `, data(capped(payload, payloadCap(payload)))));
|
|
120
|
+
}
|
|
121
|
+
if (opts.waitEvent) {
|
|
122
|
+
const payload = JSON.stringify(opts.event) ?? 'undefined';
|
|
123
|
+
sections.push(line(text`event: `, data(capped(payload, payloadCap(payload)))));
|
|
124
|
+
}
|
|
125
|
+
emitResult(
|
|
126
|
+
{
|
|
127
|
+
tag: 'cdp-result',
|
|
128
|
+
attrs: {
|
|
129
|
+
method: opts.method,
|
|
130
|
+
'wait-event': opts.waitEvent,
|
|
131
|
+
scope: opts.scope,
|
|
132
|
+
target: opts.target,
|
|
133
|
+
},
|
|
134
|
+
sections,
|
|
135
|
+
},
|
|
136
|
+
{ json: opts.json },
|
|
62
137
|
);
|
|
63
138
|
}
|
|
64
139
|
|
|
140
|
+
/** Recovery line for a rejected bare (target-less) protocol call: many CDP
|
|
141
|
+
* domains are tab-scoped, and Chrome often rejects them with only "Internal
|
|
142
|
+
* error" when sent on the bare browser connection. */
|
|
143
|
+
const TAB_SCOPE_RECOVERY =
|
|
144
|
+
'Many CDP domains (Storage.*, Page.*, DOM.*, Emulation.*, ...) are tab-scoped, not connection-scoped — re-run with --target <tabId> (`capture tab list` shows available tabs).';
|
|
145
|
+
|
|
146
|
+
/** Appends the tab-scope recovery hint when the failed call was target-less. */
|
|
147
|
+
function cdpFailed(message: string, flagTarget: string | undefined, cause?: unknown): CaptureError {
|
|
148
|
+
return captureError('world', 'cdp_failed', flagTarget ? message : `${message} ${TAB_SCOPE_RECOVERY}`, cause);
|
|
149
|
+
}
|
|
150
|
+
|
|
65
151
|
export async function cmdCdp(parsed: ParsedArgs, _args: string[]): Promise<void> {
|
|
66
152
|
if (parsed.help) {
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
console.log(HELP);
|
|
154
|
+
return;
|
|
69
155
|
}
|
|
70
156
|
|
|
157
|
+
// Positional cardinality (0..1, where 0 requires --wait-event) is enforced
|
|
158
|
+
// by `validateCliInvocation` before dispatch reaches this leaf; this guard
|
|
159
|
+
// covers direct programmatic callers only. Failures cross the boundary as
|
|
160
|
+
// typed CaptureErrors — capture.ts is the sole renderer/exit-status owner.
|
|
71
161
|
const method = parsed.positional[0];
|
|
72
162
|
if (!method && !parsed.waitEvent) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
163
|
+
throw invalidInput(
|
|
164
|
+
'received: neither a <Domain.method> positional nor --wait-event; expected: at least one of the two. Run `capture cdp -h` for the input schema.',
|
|
165
|
+
'missing_method_and_event',
|
|
166
|
+
);
|
|
76
167
|
}
|
|
77
168
|
|
|
78
169
|
let params: Record<string, unknown> | undefined;
|
|
@@ -80,70 +171,102 @@ export async function cmdCdp(parsed: ParsedArgs, _args: string[]): Promise<void>
|
|
|
80
171
|
try {
|
|
81
172
|
params = JSON.parse(parsed.params);
|
|
82
173
|
} catch (err) {
|
|
83
|
-
|
|
84
|
-
|
|
174
|
+
throw invalidInput(
|
|
175
|
+
`received: --params that is not valid JSON (${err instanceof Error ? err.message : String(err)}); expected: a JSON-encoded params object.`,
|
|
176
|
+
'invalid_params_json',
|
|
177
|
+
);
|
|
85
178
|
}
|
|
86
179
|
}
|
|
87
180
|
|
|
88
181
|
const timeoutMs = parsed.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
89
182
|
|
|
90
183
|
if (parsed.browser) {
|
|
91
|
-
await runBrowserScope(method, params, parsed, timeoutMs);
|
|
184
|
+
await runBrowserScope(method, params, parsed, timeoutMs, DEFAULT_BROWSER_DEPS);
|
|
92
185
|
return;
|
|
93
186
|
}
|
|
94
187
|
|
|
95
|
-
|
|
188
|
+
try {
|
|
189
|
+
await runPageScope(method, params, parsed, timeoutMs);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (err instanceof CaptureError) throw err;
|
|
192
|
+
throw captureError(
|
|
193
|
+
'world',
|
|
194
|
+
'cdp_failed',
|
|
195
|
+
`\`${invocationLabel(method, parsed.waitEvent)}\` failed on the page connection: ${err instanceof Error ? err.message : String(err)}`,
|
|
196
|
+
err,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function invocationLabel(method: string | undefined, waitEvent: string | undefined): string {
|
|
202
|
+
return method ?? `--wait-event ${waitEvent}`;
|
|
96
203
|
}
|
|
97
204
|
|
|
98
|
-
|
|
205
|
+
/**
|
|
206
|
+
* Exported for `test/cdp-command.test.ts`. Browser-scope CDP is connection-
|
|
207
|
+
* level, so a target is only meaningful when the caller *explicitly* asked for
|
|
208
|
+
* one with `--target` (`parsed.targetSource === 'flag'`). Session/env target
|
|
209
|
+
* autofill exists to scope ordinary PAGE commands and must never silently
|
|
210
|
+
* flatten a browser-level call onto some other tab — so this reads the flag-
|
|
211
|
+
* sourced target directly instead of inferring provenance from the final
|
|
212
|
+
* string.
|
|
213
|
+
*/
|
|
214
|
+
export async function runBrowserScope(
|
|
99
215
|
method: string | undefined,
|
|
100
216
|
params: Record<string, unknown> | undefined,
|
|
101
217
|
parsed: ParsedArgs,
|
|
102
218
|
timeoutMs: number,
|
|
219
|
+
deps: BrowserScopeDeps = DEFAULT_BROWSER_DEPS,
|
|
103
220
|
): Promise<void> {
|
|
221
|
+
const flagTarget = parsed.targetSource === 'flag' ? parsed.target : undefined;
|
|
104
222
|
const session = getActiveSession();
|
|
105
223
|
if (session?.bridgeSocket && fs.existsSync(session.bridgeSocket)) {
|
|
106
|
-
const resp = await sendBridgeRequest(session.bridgeSocket, {
|
|
224
|
+
const resp = await deps.sendBridgeRequest(session.bridgeSocket, {
|
|
107
225
|
method,
|
|
108
226
|
params,
|
|
109
|
-
targetId:
|
|
227
|
+
targetId: flagTarget,
|
|
110
228
|
waitEvent: parsed.waitEvent,
|
|
111
229
|
timeoutMs,
|
|
112
230
|
});
|
|
113
231
|
if (!resp.ok) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
'tab, not the browser connection as a whole \u2014 sent bare like this, Chrome often rejects them ' +
|
|
119
|
-
'with just "Internal error" and no further detail. Pass --target <tabId> to attach to the tab ' +
|
|
120
|
-
'the call should apply to (run "capture list" to find one).',
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
process.exit(1);
|
|
232
|
+
throw cdpFailed(
|
|
233
|
+
`\`${invocationLabel(method, parsed.waitEvent)}\` failed over the held connection: ${resp.error ?? 'unknown bridge error'}`,
|
|
234
|
+
flagTarget,
|
|
235
|
+
);
|
|
124
236
|
}
|
|
125
|
-
|
|
237
|
+
emitCdpResult({
|
|
238
|
+
method,
|
|
239
|
+
waitEvent: parsed.waitEvent,
|
|
240
|
+
scope: 'browser',
|
|
241
|
+
target: flagTarget,
|
|
242
|
+
result: resp.result,
|
|
243
|
+
event: resp.event,
|
|
244
|
+
json: parsed.json,
|
|
245
|
+
});
|
|
126
246
|
return;
|
|
127
247
|
}
|
|
128
248
|
|
|
249
|
+
// In-flight diagnostic (stderr): the one-shot connection semantics fact.
|
|
129
250
|
console.error(
|
|
130
|
-
'No held session bridge active \u2014 this connection closes when the command exits, ' +
|
|
131
|
-
'so any browser-level grant or target enablement made here reverts immediately after
|
|
132
|
-
'For state that must survive multiple commands: capture session start --hold\n',
|
|
251
|
+
'No held session bridge active \u2014 this one-shot connection closes when the command exits, ' +
|
|
252
|
+
'so any browser-level grant or target enablement made here reverts immediately after.',
|
|
133
253
|
);
|
|
134
254
|
|
|
135
|
-
const port = parsed.port ?? (await detectCdpPort());
|
|
136
|
-
const { client } = await getBrowserClient(port);
|
|
255
|
+
const port = parsed.port ?? (await deps.detectCdpPort());
|
|
256
|
+
const { client } = await deps.getBrowserClient(port);
|
|
137
257
|
try {
|
|
138
258
|
let sessionId: string | undefined;
|
|
139
|
-
if (
|
|
259
|
+
if (flagTarget) {
|
|
140
260
|
// Accept the same 8-char-prefix targeting every other capture command
|
|
141
|
-
// promises (see the
|
|
261
|
+
// promises (see the root help's targeting contract) instead of
|
|
142
262
|
// requiring the full 32-char target id here.
|
|
143
|
-
const tab = await findTabById(port,
|
|
263
|
+
const tab = await deps.findTabById(port, flagTarget);
|
|
144
264
|
if (!tab) {
|
|
145
|
-
|
|
146
|
-
|
|
265
|
+
throw captureError(
|
|
266
|
+
'precondition',
|
|
267
|
+
'target_not_found',
|
|
268
|
+
`received: --target ${flagTarget} with no matching target on port ${port}; expected: an existing target id (8-char prefix accepted). \`capture tab list\` shows available targets.`,
|
|
269
|
+
);
|
|
147
270
|
}
|
|
148
271
|
const attached = (await client.send('Target.attachToTarget', {
|
|
149
272
|
targetId: tab.id,
|
|
@@ -154,42 +277,88 @@ async function runBrowserScope(
|
|
|
154
277
|
const eventPromise = parsed.waitEvent ? waitForEventOnce(client, parsed.waitEvent, timeoutMs) : undefined;
|
|
155
278
|
const result = method ? await client.send(method, params ?? {}, 60000, sessionId) : undefined;
|
|
156
279
|
const event = eventPromise ? await eventPromise : undefined;
|
|
157
|
-
|
|
280
|
+
emitCdpResult({
|
|
281
|
+
method,
|
|
282
|
+
waitEvent: parsed.waitEvent,
|
|
283
|
+
scope: 'browser',
|
|
284
|
+
target: flagTarget,
|
|
285
|
+
result,
|
|
286
|
+
event,
|
|
287
|
+
json: parsed.json,
|
|
288
|
+
});
|
|
158
289
|
} catch (err) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
'with just "Internal error" and no further detail. Pass --target <tabId> to attach to the tab ' +
|
|
166
|
-
`the call should apply to (run "capture list" to find one${method ? `, e.g. capture cdp ${method} --browser --target <tabId> ...` : ''}).`,
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
process.exit(1);
|
|
290
|
+
if (err instanceof CaptureError) throw err;
|
|
291
|
+
throw cdpFailed(
|
|
292
|
+
`\`${invocationLabel(method, parsed.waitEvent)}\` failed on the one-shot browser connection: ${err instanceof Error ? err.message : String(err)}`,
|
|
293
|
+
flagTarget,
|
|
294
|
+
err,
|
|
295
|
+
);
|
|
170
296
|
} finally {
|
|
171
297
|
client.close();
|
|
172
298
|
}
|
|
173
299
|
}
|
|
174
300
|
|
|
175
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Exported for testing (`test/recorder-navigate-waitevent.test.ts`,
|
|
303
|
+
* `test/cdp-command.test.ts`): the recorder-held branch below
|
|
304
|
+
* (`isRecorderHeldClient(client)`) is the actual command wiring
|
|
305
|
+
* `capture cdp --wait-event` runs under an active recording, and the
|
|
306
|
+
* injectable `connect` lets `test/cdp-command.test.ts` drive the non-held
|
|
307
|
+
* branch against a stub client.
|
|
308
|
+
*/
|
|
309
|
+
export async function runPageScope(
|
|
176
310
|
method: string | undefined,
|
|
177
311
|
params: Record<string, unknown> | undefined,
|
|
178
312
|
parsed: ParsedArgs,
|
|
179
313
|
timeoutMs: number,
|
|
314
|
+
connect: (parsed: ParsedArgs) => Promise<{ client: CdpScopeClient }> = connectForCommand,
|
|
180
315
|
): Promise<void> {
|
|
181
|
-
const { client } = await
|
|
316
|
+
const { client } = await connect(parsed);
|
|
182
317
|
try {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
318
|
+
let result: unknown;
|
|
319
|
+
let event: unknown;
|
|
320
|
+
if (isRecorderHeldClient(client)) {
|
|
321
|
+
// The recorder-held adapter's `.on()` is a documented no-op (nothing
|
|
322
|
+
// pushes unsolicited events back over its one-request-one-response
|
|
323
|
+
// socket) — `waitForEventOnce` below would hang until its own timeout.
|
|
324
|
+
// `.waitEvent()`/`.dispatch()` are the real event-wait surface for this
|
|
325
|
+
// adapter, routing the wait through the recorder bridge's own event
|
|
326
|
+
// broker. When both a method and a wait-event are requested,
|
|
327
|
+
// `.dispatch()` carries them in ONE request so the bridge arms the wait
|
|
328
|
+
// before dispatching the call — sending them as two separate requests
|
|
329
|
+
// (`.send()` then `.waitEvent()`) risks the action firing the event
|
|
330
|
+
// before the wait-only request even reaches the bridge. This command has
|
|
331
|
+
// no fragment-nav-style multi-call logic and already throws on an
|
|
332
|
+
// event-wait timeout, so one combined request preserves the complete
|
|
333
|
+
// result contract (contrast `page/navigate.ts`'s multi-call
|
|
334
|
+
// `navigateAtomicWithFragmentFix`).
|
|
335
|
+
if (method) {
|
|
336
|
+
const combined = await client.dispatch(method, params ?? {}, parsed.waitEvent, timeoutMs);
|
|
337
|
+
result = combined.result;
|
|
338
|
+
event = combined.event;
|
|
339
|
+
} else if (parsed.waitEvent) {
|
|
340
|
+
event = await client.waitEvent(parsed.waitEvent, timeoutMs);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
const eventPromise = parsed.waitEvent ? waitForEventOnce(client, parsed.waitEvent, timeoutMs) : undefined;
|
|
344
|
+
result = method ? await client.send(method, params ?? {}) : undefined;
|
|
345
|
+
event = eventPromise ? await eventPromise : undefined;
|
|
346
|
+
}
|
|
347
|
+
emitCdpResult({
|
|
348
|
+
method,
|
|
349
|
+
waitEvent: parsed.waitEvent,
|
|
350
|
+
scope: 'page',
|
|
351
|
+
target: parsed.target,
|
|
352
|
+
result,
|
|
353
|
+
event,
|
|
354
|
+
json: parsed.json,
|
|
355
|
+
});
|
|
187
356
|
} finally {
|
|
188
357
|
client.close();
|
|
189
358
|
}
|
|
190
359
|
}
|
|
191
360
|
|
|
192
|
-
function waitForEventOnce(client:
|
|
361
|
+
function waitForEventOnce(client: CdpScopeClient, eventName: string, timeoutMs: number): Promise<unknown> {
|
|
193
362
|
return new Promise((resolve, reject) => {
|
|
194
363
|
const timer = setTimeout(
|
|
195
364
|
() => reject(new Error(`Timed out after ${timeoutMs}ms waiting for event "${eventName}"`)),
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--gate` (exit 2 on findings/changes) is scoped to exactly two leaves —
|
|
3
|
+
* `measure check` and `measure diff` — per the observational-collector
|
|
4
|
+
* posture invariant (no collector or query leaf grades/gates its own
|
|
5
|
+
* output; see I-8). The shared dispatch path (`src/cdp/dispatch.ts`)
|
|
6
|
+
* invokes this guard ONCE per invocation, via `isGateLeaf`, for every
|
|
7
|
+
* command on the whole surface, so a caller that supplies `--gate`
|
|
8
|
+
* anywhere else gets a typed rejection (thrown here, rendered at the
|
|
9
|
+
* `src/capture.ts` root boundary) before any branch main runs instead of
|
|
10
|
+
* the flag being silently accepted and ignored. The dispatch guard is the
|
|
11
|
+
* ONLY caller of `rejectUnsupportedGate`; no leaf calls it directly.
|
|
12
|
+
*/
|
|
13
|
+
import { type ParsedArgs } from '../types.js';
|
|
14
|
+
import { invalidInput } from '../../errors.js';
|
|
15
|
+
|
|
16
|
+
/** True iff this invocation is one of the exactly two leaves that accept
|
|
17
|
+
* `--gate`: `measure check` and `measure diff`. Leaf detection is
|
|
18
|
+
* `parsed.command` plus the first positional (the branch-leaf token —
|
|
19
|
+
* branch routers shift it off before their leaves run, but dispatch sees
|
|
20
|
+
* it in place). */
|
|
21
|
+
export function isGateLeaf(parsed: ParsedArgs): boolean {
|
|
22
|
+
if (parsed.command !== 'measure') return false;
|
|
23
|
+
const leaf = parsed.positional[0];
|
|
24
|
+
return leaf === 'check' || leaf === 'diff';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Throws the typed `--gate` rejection for a leaf that doesn't support it;
|
|
29
|
+
* returns normally when `parsed.gate` is unset. The throw crosses the shared
|
|
30
|
+
* dispatch path unrendered — the root boundary in `src/capture.ts` is the one
|
|
31
|
+
* place that renders it and sets the exit code.
|
|
32
|
+
*
|
|
33
|
+
* `command` is the leaf's own dotted usage name (e.g. `"measure snap"`,
|
|
34
|
+
* `"motion jank"`), carried in the message so the rejection still names the
|
|
35
|
+
* exact leaf.
|
|
36
|
+
*/
|
|
37
|
+
export function rejectUnsupportedGate(parsed: ParsedArgs, command: string): void {
|
|
38
|
+
if (!parsed.gate) return;
|
|
39
|
+
throw invalidInput(
|
|
40
|
+
`\`--gate\` is not accepted on \`${command}\`. received: --gate; expected: no gate flag on this leaf. \`--gate\` (exit 2 on findings/changes) is accepted only by \`measure check\` and \`measure diff\`.`,
|
|
41
|
+
'unsupported_flag',
|
|
42
|
+
);
|
|
43
|
+
}
|