@crouton-kit/capture 1.3.7 → 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.
Files changed (141) hide show
  1. package/bin/capture +32786 -9989
  2. package/package.json +9 -3
  3. package/src/capture.ts +86 -163
  4. package/src/cdp/a11y.ts +113 -14
  5. package/src/cdp/args.ts +300 -116
  6. package/src/cdp/bridge/protocol.ts +187 -1
  7. package/src/cdp/bridge/server.ts +211 -84
  8. package/src/cdp/bridge/spawn.ts +110 -1
  9. package/src/cdp/client.ts +13 -3
  10. package/src/cdp/commands/bridge-serve.ts +35 -4
  11. package/src/cdp/commands/cdp.ts +266 -69
  12. package/src/cdp/commands/gate-guard.ts +43 -0
  13. package/src/cdp/commands/lib.ts +339 -213
  14. package/src/cdp/commands/measure/census.ts +87 -0
  15. package/src/cdp/commands/measure/check.ts +109 -0
  16. package/src/cdp/commands/measure/diff.ts +120 -0
  17. package/src/cdp/commands/measure/explain.ts +133 -0
  18. package/src/cdp/commands/measure/index.ts +106 -0
  19. package/src/cdp/commands/measure/map-ax.ts +52 -0
  20. package/src/cdp/commands/measure/map-focus.ts +52 -0
  21. package/src/cdp/commands/measure/map-layers.ts +59 -0
  22. package/src/cdp/commands/measure/map-scroll.ts +94 -0
  23. package/src/cdp/commands/measure/snap.ts +416 -0
  24. package/src/cdp/commands/measure/sweep.ts +227 -0
  25. package/src/cdp/commands/motion/index.ts +61 -0
  26. package/src/cdp/commands/motion/jank.ts +132 -0
  27. package/src/cdp/commands/motion/mask.ts +92 -0
  28. package/src/cdp/commands/motion/rec.ts +534 -0
  29. package/src/cdp/commands/motion/response.ts +76 -0
  30. package/src/cdp/commands/motion/timeline.ts +152 -0
  31. package/src/cdp/commands/page/click.ts +194 -0
  32. package/src/cdp/commands/page/elements.ts +154 -0
  33. package/src/cdp/commands/page/exec.ts +317 -0
  34. package/src/cdp/commands/page/index.ts +72 -0
  35. package/src/cdp/commands/page/navigate.ts +330 -0
  36. package/src/cdp/commands/page/scroll.ts +107 -0
  37. package/src/cdp/commands/page/shot.ts +208 -0
  38. package/src/cdp/commands/page/type.ts +104 -0
  39. package/src/cdp/commands/tab/index.ts +59 -0
  40. package/src/cdp/commands/tab/list.ts +167 -0
  41. package/src/cdp/commands/tab/network.ts +162 -0
  42. package/src/cdp/commands/tab/open.ts +90 -0
  43. package/src/cdp/commands/tab/reset.ts +160 -0
  44. package/src/cdp/connection.ts +368 -37
  45. package/src/cdp/coordinates.ts +158 -0
  46. package/src/cdp/detect.ts +42 -13
  47. package/src/cdp/dispatch.ts +76 -26
  48. package/src/cdp/domains.ts +75 -0
  49. package/src/cdp/har-recorder.ts +648 -115
  50. package/src/cdp/leaf-grammar.ts +99 -0
  51. package/src/cdp/measure/census.ts +205 -0
  52. package/src/cdp/measure/check.ts +318 -0
  53. package/src/cdp/measure/collectors/animation.ts +385 -0
  54. package/src/cdp/measure/collectors/ax.ts +278 -0
  55. package/src/cdp/measure/collectors/focus.ts +1041 -0
  56. package/src/cdp/measure/collectors/forms.ts +905 -0
  57. package/src/cdp/measure/collectors/geometry.ts +1038 -0
  58. package/src/cdp/measure/collectors/hittest.ts +835 -0
  59. package/src/cdp/measure/collectors/layers.ts +697 -0
  60. package/src/cdp/measure/collectors/media.ts +522 -0
  61. package/src/cdp/measure/collectors/pixels.ts +1584 -0
  62. package/src/cdp/measure/collectors/queries.ts +464 -0
  63. package/src/cdp/measure/collectors/scroll.ts +599 -0
  64. package/src/cdp/measure/collectors/states.ts +992 -0
  65. package/src/cdp/measure/collectors/style-provenance.ts +643 -0
  66. package/src/cdp/measure/collectors/styles.ts +355 -0
  67. package/src/cdp/measure/collectors/text.ts +713 -0
  68. package/src/cdp/measure/diff.ts +470 -0
  69. package/src/cdp/measure/explain.ts +498 -0
  70. package/src/cdp/measure/map-ax.ts +633 -0
  71. package/src/cdp/measure/map-focus.ts +188 -0
  72. package/src/cdp/measure/map-layers.ts +177 -0
  73. package/src/cdp/measure/map-scroll.ts +317 -0
  74. package/src/cdp/measure/redaction.ts +51 -0
  75. package/src/cdp/measure/settle.ts +1051 -0
  76. package/src/cdp/measure/snapshot.ts +390 -0
  77. package/src/cdp/measure/sweep.ts +330 -0
  78. package/src/cdp/measure/types.ts +220 -0
  79. package/src/cdp/measure/v2/collectors.ts +19 -0
  80. package/src/cdp/measure/v2/orchestrator.ts +46 -0
  81. package/src/cdp/measure/v2/snap-selection.ts +33 -0
  82. package/src/cdp/measure/v2/target-lock.ts +15 -0
  83. package/src/cdp/measure/v2/target.ts +16 -0
  84. package/src/cdp/motion/jank.ts +433 -0
  85. package/src/cdp/motion/mask.ts +421 -0
  86. package/src/cdp/motion/recorder.ts +764 -0
  87. package/src/cdp/motion/response.ts +356 -0
  88. package/src/cdp/motion/timeline.ts +215 -0
  89. package/src/cdp/record.ts +5 -212
  90. package/src/cdp/recorder-bridge.ts +1894 -0
  91. package/src/cdp/recorder-client.ts +326 -0
  92. package/src/cdp/scope-lock.ts +73 -0
  93. package/src/cdp/screenshot.ts +199 -66
  94. package/src/cdp/source-map.ts +309 -0
  95. package/src/cdp/targets.ts +21 -1
  96. package/src/cdp/timing.ts +87 -0
  97. package/src/cdp/types.ts +87 -6
  98. package/src/cdp/viewport.ts +21 -0
  99. package/src/cdp.ts +12 -58
  100. package/src/command-help.ts +29 -0
  101. package/src/command-resolution.ts +22 -0
  102. package/src/command-walker.ts +35 -0
  103. package/src/contracts/a11y.ts +33 -0
  104. package/src/contracts/cursor.ts +35 -0
  105. package/src/contracts/index.ts +15 -0
  106. package/src/contracts/motion.ts +19 -0
  107. package/src/contracts/neutral.ts +56 -0
  108. package/src/contracts/primitives.ts +205 -0
  109. package/src/contracts/release.ts +17 -0
  110. package/src/contracts/results.ts +208 -0
  111. package/src/contracts/routes.ts +140 -0
  112. package/src/contracts/snapshot.ts +92 -0
  113. package/src/cursor/index.ts +13 -0
  114. package/src/cursor/service.ts +310 -0
  115. package/src/errors.ts +71 -0
  116. package/src/har-manager.ts +992 -41
  117. package/src/interact.ts +410 -98
  118. package/src/output/artifact-lifecycle.ts +371 -0
  119. package/src/output/artifact.ts +526 -0
  120. package/src/output/diff.ts +111 -0
  121. package/src/output/exact-raw.ts +114 -0
  122. package/src/output/render.ts +987 -0
  123. package/src/output/selector.ts +185 -0
  124. package/src/output/structured.ts +211 -0
  125. package/src/registry.ts +208 -0
  126. package/src/session/artifacts.ts +481 -0
  127. package/src/session/commands.ts +1237 -368
  128. package/src/session/coordinator.ts +251 -0
  129. package/src/session/har-filter.ts +57 -0
  130. package/src/session/log-tailer.ts +653 -0
  131. package/src/session-context.ts +302 -34
  132. package/src/vault/bundle.ts +32 -17
  133. package/src/cdp/commands/exec.ts +0 -105
  134. package/src/cdp/commands/har.ts +0 -128
  135. package/src/cdp/commands/tabs.ts +0 -183
  136. package/src/cdp/commands/traffic.ts +0 -121
  137. package/src/cdp/commands/ui.ts +0 -172
  138. package/src/cdp/execute.ts +0 -113
  139. package/src/cdp/har-output.ts +0 -47
  140. package/src/cdp/usage.ts +0 -77
  141. package/src/tab-session.ts +0 -91
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Shared quad math and frame/shadow coordinate stitching helpers for `measure snap`.
3
+ *
4
+ * These helpers are pure (`axisAlignedRectFromQuad`, `composeFrameTransform`,
5
+ * `toTopViewportQuad`) except `getContentQuadBox`, which calls the existing `CDPClient.send`
6
+ * directly (no new client abstraction). Quad helpers always preserve the original four
7
+ * points from CDP; an axis-aligned rect is only ever a derived convenience alongside them,
8
+ * never a replacement — several instruments (raster crops, hit-test, motion masks) need the
9
+ * real quad for transformed/rotated/wrapped elements.
10
+ */
11
+
12
+ import type { CDPClient } from './client.js';
13
+
14
+ /** Four corner points clockwise from top-left, in a single local frame's coordinate space: [x1,y1,x2,y2,x3,y3,x4,y4]. */
15
+ export type Quad = [number, number, number, number, number, number, number, number];
16
+
17
+ export interface Rect {
18
+ x: number;
19
+ y: number;
20
+ width: number;
21
+ height: number;
22
+ }
23
+
24
+ export interface BoxModelQuads {
25
+ content: Quad;
26
+ padding: Quad;
27
+ border: Quad;
28
+ margin: Quad;
29
+ width: number;
30
+ height: number;
31
+ }
32
+
33
+ export interface ContentQuadBox {
34
+ /**
35
+ * One quad per DOM fragment. Most elements produce exactly one; an inline element wrapped
36
+ * across lines (or an element split by column/fragmentation) produces more than one, and
37
+ * every fragment is preserved rather than collapsed into a single bounding rect.
38
+ */
39
+ quads: Quad[];
40
+ /** `DOM.getBoxModel`'s content/padding/border/margin quads plus width/height, in the same local coordinate space as `quads`. */
41
+ boxModel: BoxModelQuads;
42
+ }
43
+
44
+ /** Identifies a DOM node the way CDP's `DOM.getContentQuads`/`DOM.getBoxModel` accept it. */
45
+ export type NodeRef = { backendNodeId: number } | { nodeId: number } | { objectId: string };
46
+
47
+ /**
48
+ * Reads both the content quad(s) and the full box model (content/padding/border/margin) for
49
+ * one node via `DOM.getContentQuads` and `DOM.getBoxModel`. Coordinates are in the node's own
50
+ * frame's local viewport space — use `composeFrameTransform`/`toTopViewportQuad` to stitch
51
+ * iframe-nested coordinates into top-viewport space.
52
+ */
53
+ export async function getContentQuadBox(client: CDPClient, ref: NodeRef): Promise<ContentQuadBox> {
54
+ const [quadsResult, boxResult] = await Promise.all([
55
+ client.send('DOM.getContentQuads', ref as unknown as Record<string, unknown>) as Promise<{
56
+ quads: number[][];
57
+ }>,
58
+ client.send('DOM.getBoxModel', ref as unknown as Record<string, unknown>) as Promise<{
59
+ model: {
60
+ content: number[];
61
+ padding: number[];
62
+ border: number[];
63
+ margin: number[];
64
+ width: number;
65
+ height: number;
66
+ };
67
+ }>,
68
+ ]);
69
+
70
+ const model = boxResult.model;
71
+
72
+ return {
73
+ quads: quadsResult.quads.map(asQuad),
74
+ boxModel: {
75
+ content: asQuad(model.content),
76
+ padding: asQuad(model.padding),
77
+ border: asQuad(model.border),
78
+ margin: asQuad(model.margin),
79
+ width: model.width,
80
+ height: model.height,
81
+ },
82
+ };
83
+ }
84
+
85
+ function asQuad(points: number[]): Quad {
86
+ if (points.length !== 8) {
87
+ throw new Error(`Expected an 8-number quad (x1,y1,x2,y2,x3,y3,x4,y4), got ${points.length} numbers`);
88
+ }
89
+ return points as Quad;
90
+ }
91
+
92
+ /** Derives the axis-aligned bounding rect of a quad. Purely a convenience — `quad` itself stays the source of truth. */
93
+ export function axisAlignedRectFromQuad(quad: Quad): Rect {
94
+ const xs = [quad[0], quad[2], quad[4], quad[6]];
95
+ const ys = [quad[1], quad[3], quad[5], quad[7]];
96
+ const minX = Math.min(...xs);
97
+ const maxX = Math.max(...xs);
98
+ const minY = Math.min(...ys);
99
+ const maxY = Math.max(...ys);
100
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
101
+ }
102
+
103
+ /**
104
+ * One nested frame's offset: the translation (and optional scale, e.g. CSS zoom) of a child
105
+ * frame's content-box origin within its parent frame's local coordinate space.
106
+ */
107
+ export interface FrameOffset {
108
+ dx: number;
109
+ dy: number;
110
+ /** Defaults to 1 (no scale) when omitted. */
111
+ scaleX?: number;
112
+ scaleY?: number;
113
+ }
114
+
115
+ export interface FrameTransform {
116
+ dx: number;
117
+ dy: number;
118
+ scaleX: number;
119
+ scaleY: number;
120
+ }
121
+
122
+ /**
123
+ * Composes a chain of nested iframe offsets into one affine transform from the innermost
124
+ * (target) frame's local coordinates to top-viewport coordinates. `chain` is ordered from the
125
+ * outermost transition first (the top frame's offset into its direct child) to the innermost
126
+ * transition last (the target's immediate parent frame's offset into the target frame).
127
+ *
128
+ * Shadow DOM does not need this — shadow-root content shares its host element's coordinate
129
+ * space, so shadow-root traversal only affects which node a query resolves to, not its quad.
130
+ */
131
+ export function composeFrameTransform(chain: FrameOffset[]): FrameTransform {
132
+ let transform: FrameTransform = { dx: 0, dy: 0, scaleX: 1, scaleY: 1 };
133
+ for (const offset of chain) {
134
+ const scaleX = offset.scaleX ?? 1;
135
+ const scaleY = offset.scaleY ?? 1;
136
+ transform = {
137
+ dx: transform.dx + offset.dx * transform.scaleX,
138
+ dy: transform.dy + offset.dy * transform.scaleY,
139
+ scaleX: transform.scaleX * scaleX,
140
+ scaleY: transform.scaleY * scaleY,
141
+ };
142
+ }
143
+ return transform;
144
+ }
145
+
146
+ /** Applies a composed `FrameTransform` to every point of a quad, mapping it into top-viewport space. */
147
+ export function toTopViewportQuad(quad: Quad, transform: FrameTransform): Quad {
148
+ return [
149
+ quad[0] * transform.scaleX + transform.dx,
150
+ quad[1] * transform.scaleY + transform.dy,
151
+ quad[2] * transform.scaleX + transform.dx,
152
+ quad[3] * transform.scaleY + transform.dy,
153
+ quad[4] * transform.scaleX + transform.dx,
154
+ quad[5] * transform.scaleY + transform.dy,
155
+ quad[6] * transform.scaleX + transform.dx,
156
+ quad[7] * transform.scaleY + transform.dy,
157
+ ];
158
+ }
package/src/cdp/detect.ts CHANGED
@@ -118,6 +118,37 @@ function getLocalhostListeningPorts(): number[] {
118
118
  }
119
119
  }
120
120
 
121
+ function getListeningProcessNames(): Map<number, string> {
122
+ const names = new Map<number, string>();
123
+ try {
124
+ const output = execSync('lsof -nP -iTCP -sTCP:LISTEN -Fpcn 2>/dev/null', {
125
+ encoding: 'utf-8',
126
+ });
127
+ let command: string | null = null;
128
+ for (const line of output.split('\n')) {
129
+ if (line.startsWith('c')) {
130
+ command = line.slice(1);
131
+ } else if (command && line.startsWith('n')) {
132
+ const match = line.match(/:(\d+)(?:\s|$)/);
133
+ if (match) names.set(Number(match[1]), command);
134
+ }
135
+ }
136
+ } catch {
137
+ // Process identity is an optional refinement; CDP probing still works.
138
+ }
139
+ return names;
140
+ }
141
+
142
+ export function identifyBrowserBundleId(browser: string, processName?: string): string {
143
+ // A listening process is stronger identity than Chromium's generic
144
+ // Browser/User-Agent strings: Arc and Spotify both report as "Chrome".
145
+ const identity = (processName ?? browser).toLowerCase();
146
+ for (const [bundleId, patterns] of Object.entries(BROWSER_PATTERNS)) {
147
+ if (patterns.some((pattern) => identity.includes(pattern))) return bundleId;
148
+ }
149
+ return 'unknown';
150
+ }
151
+
121
152
  export interface CdpEndpoint {
122
153
  port: number;
123
154
  app: string;
@@ -128,25 +159,18 @@ export interface CdpEndpoint {
128
159
 
129
160
  export async function detectCdpPortsAsync(): Promise<CdpEndpoint[]> {
130
161
  const ports = getLocalhostListeningPorts();
162
+ const processNames = getListeningProcessNames();
131
163
  const results: CdpEndpoint[] = [];
132
164
 
133
165
  // Probe ports in parallel for speed
134
166
  const probes = ports.map(async (port) => {
135
167
  const probe = await probeCdpPort(port);
136
168
  if (probe) {
137
- // Map browser string to bundle ID for default browser matching
138
- const lower = probe.browser.toLowerCase();
139
- let bundleId = 'unknown';
140
- for (const [bid, patterns] of Object.entries(BROWSER_PATTERNS)) {
141
- if (patterns.some((p) => lower.includes(p))) {
142
- bundleId = bid;
143
- break;
144
- }
145
- }
169
+ const processName = processNames.get(port);
146
170
  results.push({
147
171
  port,
148
- app: probe.app,
149
- bundleId,
172
+ app: processName ?? probe.app,
173
+ bundleId: identifyBrowserBundleId(probe.browser, processName),
150
174
  isElectron: probe.isElectron,
151
175
  hasPageTarget: probe.hasPageTarget,
152
176
  });
@@ -179,8 +203,13 @@ export function pickPreferredEndpoint(
179
203
  if (match) return match;
180
204
  }
181
205
 
182
- // Fall back to first non-Electron, then first found
183
- const nonElectron = candidates.find((p) => !p.isElectron);
206
+ // Prefer a recognized browser over other Chromium hosts (Spotify, app
207
+ // shells), then fall back to the original non-Electron ordering.
208
+ const knownBrowser = candidates.find(
209
+ (endpoint) => endpoint.bundleId !== 'unknown' && !endpoint.isElectron,
210
+ );
211
+ if (knownBrowser) return knownBrowser;
212
+ const nonElectron = candidates.find((endpoint) => !endpoint.isElectron);
184
213
  return nonElectron ?? candidates[0];
185
214
  }
186
215
 
@@ -1,36 +1,86 @@
1
- import { parseCliArgs } from './args.js';
2
- import { CDP_USAGE } from './usage.js';
3
- import { cmdDetect, cmdList, cmdOpen, cmdResetTab } from './commands/tabs.js';
4
- import { cmdExec } from './commands/exec.js';
5
- import { cmdScreenshot, cmdClick, cmdType, cmdA11y } from './commands/ui.js';
6
- import { cmdRecord, cmdNavigate, cmdNetwork } from './commands/traffic.js';
7
- import { cmdHar } from './commands/har.js';
1
+ /**
2
+ * Shared dispatch for every routed command. Two jobs live here:
3
+ *
4
+ * 1. Route each root (`session`, `page`, `tab`, `measure`, `motion`,
5
+ * `cdp`, `lib`, hidden `__bridge-serve`) to its branch main.
6
+ * 2. Invoke the `--gate` guard ONCE for the whole surface: every command
7
+ * that is not `measure check|diff` rejects the flag structurally here,
8
+ * before any branch main runs.
9
+ */
10
+ import { parseCliSyntax, resolveCliContext, validateCliInvocation } from './args.js';
11
+ import { type ParsedArgs } from './types.js';
12
+ import { isGateLeaf, rejectUnsupportedGate } from './commands/gate-guard.js';
13
+ import { getActiveSession } from '../session-context.js';
14
+ import { admitSessionOperation } from '../session/coordinator.js';
15
+ import { sessionMain } from '../session/commands.js';
16
+ import { pageMain } from './commands/page/index.js';
17
+ import { tabMain } from './commands/tab/index.js';
8
18
  import { cmdLib } from './commands/lib.js';
9
19
  import { cmdCdp } from './commands/cdp.js';
10
20
  import { cmdBridgeServe } from './commands/bridge-serve.js';
21
+ import { measureMain } from './commands/measure/index.js';
22
+ import { motionMain } from './commands/motion/index.js';
23
+
24
+ /** The dotted leaf name the gate rejection reports, derived from the parsed
25
+ * command plus its first positional (the branch-leaf token). */
26
+ function gateLeafName(parsed: ParsedArgs): string {
27
+ const branch =
28
+ parsed.command === 'session' ||
29
+ parsed.command === 'page' ||
30
+ parsed.command === 'tab' ||
31
+ parsed.command === 'measure' ||
32
+ parsed.command === 'motion' ||
33
+ parsed.command === 'lib';
34
+ const leaf = parsed.positional[0];
35
+ return branch && leaf ? `${parsed.command} ${leaf}` : parsed.command;
36
+ }
37
+
38
+ /**
39
+ * Registers a session-bound operation (`page`/`measure`/`motion`) with the
40
+ * lifecycle coordinator BEFORE its first side effect and releases in `finally`,
41
+ * so a concurrent `session stop` (via `beginSessionStop`) waits for in-flight
42
+ * artifact writers to drain and excludes any operation that starts after stop
43
+ * marking (admission throws `session_stopping`). When there is no active
44
+ * session the operation is a one-shot with nothing to coordinate against, so it
45
+ * runs unwrapped. The active-session read here is never the first for these
46
+ * leaves — `resolveCliContext` already hydrated the same session — so it adds
47
+ * no new active-index side effect on the validation-rejected path.
48
+ */
49
+ async function withActiveSessionAdmission(parsed: ParsedArgs, run: () => Promise<void>): Promise<void> {
50
+ const active = getActiveSession();
51
+ if (!active) return run();
52
+ const operation = await admitSessionOperation(active.dir);
53
+ try {
54
+ return await run();
55
+ } finally {
56
+ await operation.release();
57
+ }
58
+ }
11
59
 
12
60
  export async function cdpMain(): Promise<void> {
13
61
  const args = process.argv.slice(2);
14
- const parsed = parseCliArgs(args);
62
+ const parsed = parseCliSyntax(args);
63
+
64
+ if (parsed.gate && !isGateLeaf(parsed)) {
65
+ rejectUnsupportedGate(parsed, gateLeafName(parsed));
66
+ return;
67
+ }
68
+
69
+ validateCliInvocation(parsed);
70
+ const resolved = resolveCliContext(parsed);
15
71
 
16
- switch (parsed.command) {
17
- case 'detect': return cmdDetect(parsed, args);
18
- case 'list': return cmdList(parsed, args);
19
- case 'open': return cmdOpen(parsed, args);
20
- case 'reset-tab': return cmdResetTab(parsed, args);
21
- case 'exec': return cmdExec(parsed, args);
22
- case 'screenshot': return cmdScreenshot(parsed, args);
23
- case 'click': return cmdClick(parsed, args);
24
- case 'type': return cmdType(parsed, args);
25
- case 'a11y': return cmdA11y(parsed, args);
26
- case 'record': return cmdRecord(parsed, args);
27
- case 'navigate': return cmdNavigate(parsed, args);
28
- case 'network': return cmdNetwork(parsed, args);
29
- case 'har': return cmdHar(parsed, args);
30
- case 'lib': return cmdLib(parsed, args);
31
- case 'cdp': return cmdCdp(parsed, args);
32
- case '__bridge-serve': return cmdBridgeServe(parsed, args);
72
+ switch (resolved.command) {
73
+ case 'session': return sessionMain(resolved, args);
74
+ case 'page': return withActiveSessionAdmission(resolved, () => pageMain(resolved, args));
75
+ case 'tab': return tabMain(resolved, args);
76
+ case 'lib': return cmdLib(resolved, args);
77
+ case 'cdp': return cmdCdp(resolved, args);
78
+ case 'measure': return withActiveSessionAdmission(resolved, () => measureMain(resolved, args));
79
+ case 'motion': return withActiveSessionAdmission(resolved, () => motionMain(resolved, args));
80
+ case '__bridge-serve': return cmdBridgeServe(resolved, args);
33
81
  default:
34
- console.log(CDP_USAGE);
82
+ // capture.ts routes only the known roots into cdpMain(); an unknown
83
+ // token gets its structured error there and never reaches this switch.
84
+ throw new Error(`dispatch: unroutable command ${JSON.stringify(resolved.command)}`);
35
85
  }
36
86
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared CDP domain enablement and reversible state-forcing helpers for `measure`/`motion`.
3
+ *
4
+ * Every helper here takes an already-connected `CDPClient` (see ./client.ts) and calls
5
+ * `client.send(...)` directly — there is no separate client abstraction. Domain `.enable`
6
+ * calls are idempotent in CDP, so callers may call these more than once per connection
7
+ * (e.g. once per collector) without ill effect.
8
+ */
9
+
10
+ import type { CDPClient } from './client.js';
11
+
12
+ /**
13
+ * Enables the CDP domains the `measure snap` substrate collectors need: frame/navigation
14
+ * metadata (Page), element geometry and tree access (DOM), computed style + winning-rule
15
+ * provenance (CSS — requires DOM.enable first), the accessibility tree (Accessibility),
16
+ * compositor layer facts (LayerTree), the animation inventory (Animation), and script
17
+ * execution for hit-test/text/form introspection (Runtime).
18
+ */
19
+ export async function enableDomainsForSnap(client: CDPClient): Promise<void> {
20
+ await client.send('Page.enable');
21
+ await client.send('DOM.enable');
22
+ await client.send('CSS.enable');
23
+ await Promise.all([
24
+ client.send('Accessibility.enable'),
25
+ client.send('LayerTree.enable'),
26
+ client.send('Animation.enable'),
27
+ client.send('Runtime.enable'),
28
+ ]);
29
+ }
30
+
31
+ /**
32
+ * Enables the CDP domains the motion recorder bridge needs: frame/navigation metadata and
33
+ * `Page.startScreencast` (Page), element geometry for rect/selector resolution (DOM), script
34
+ * execution for the injected Mutation/Resize/Performance observers (Runtime), request/response
35
+ * timing for `motion response` (Network), and playback-rate control for the animation inventory
36
+ * (Animation). `Tracing.start` is configured with its own category set by the recorder bridge
37
+ * (U13) and is not a plain `.enable` call, so it is not included here.
38
+ */
39
+ export async function enableDomainsForMotionRec(client: CDPClient): Promise<void> {
40
+ await client.send('Page.enable');
41
+ await client.send('DOM.enable');
42
+ await Promise.all([
43
+ client.send('Runtime.enable'),
44
+ client.send('Network.enable'),
45
+ client.send('Animation.enable'),
46
+ ]);
47
+ }
48
+
49
+ /**
50
+ * CSS pseudo-classes CDP can force directly via `CSS.forcePseudoState`. Real control states
51
+ * (checked/open/disabled/invalid) are not pseudo-classes in this sense — collectors toggle
52
+ * those with reversible DOM property writes and restore them after capture instead of using
53
+ * this helper.
54
+ */
55
+ export type ForcedPseudoClass =
56
+ | 'active'
57
+ | 'focus'
58
+ | 'focus-visible'
59
+ | 'focus-within'
60
+ | 'hover'
61
+ | 'target'
62
+ | 'visited';
63
+
64
+ /**
65
+ * Forces (or, called with an empty array, clears) a set of CSS pseudo-classes on a node.
66
+ * Requires `CSS.enable` (see `enableDomainsForSnap`). The caller is responsible for restoring
67
+ * state after capture by calling this again with `[]`.
68
+ */
69
+ export async function forcePseudoStateForNode(
70
+ client: CDPClient,
71
+ nodeId: number,
72
+ forcedPseudoClasses: ForcedPseudoClass[],
73
+ ): Promise<void> {
74
+ await client.send('CSS.forcePseudoState', { nodeId, forcedPseudoClasses });
75
+ }