@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.
Files changed (136) hide show
  1. package/bin/capture +29841 -8539
  2. package/package.json +6 -3
  3. package/src/capture.ts +86 -173
  4. package/src/cdp/a11y.ts +113 -14
  5. package/src/cdp/args.ts +300 -205
  6. package/src/cdp/bridge/protocol.ts +93 -6
  7. package/src/cdp/bridge/server.ts +118 -53
  8. package/src/cdp/bridge/spawn.ts +53 -6
  9. package/src/cdp/client.ts +13 -3
  10. package/src/cdp/commands/bridge-serve.ts +20 -14
  11. package/src/cdp/commands/cdp.ts +259 -90
  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 +76 -20
  15. package/src/cdp/commands/measure/check.ts +100 -26
  16. package/src/cdp/commands/measure/diff.ts +109 -21
  17. package/src/cdp/commands/measure/explain.ts +121 -19
  18. package/src/cdp/commands/measure/index.ts +31 -39
  19. package/src/cdp/commands/measure/map-ax.ts +52 -0
  20. package/src/cdp/commands/measure/map-focus.ts +41 -13
  21. package/src/cdp/commands/measure/map-layers.ts +49 -15
  22. package/src/cdp/commands/measure/map-scroll.ts +85 -15
  23. package/src/cdp/commands/measure/snap.ts +405 -25
  24. package/src/cdp/commands/measure/sweep.ts +217 -20
  25. package/src/cdp/commands/motion/index.ts +18 -17
  26. package/src/cdp/commands/motion/jank.ts +119 -9
  27. package/src/cdp/commands/motion/mask.ts +81 -12
  28. package/src/cdp/commands/motion/rec.ts +520 -19
  29. package/src/cdp/commands/motion/response.ts +65 -15
  30. package/src/cdp/commands/motion/timeline.ts +139 -14
  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/detect.ts +42 -13
  46. package/src/cdp/dispatch.ts +74 -28
  47. package/src/cdp/har-recorder.ts +648 -115
  48. package/src/cdp/leaf-grammar.ts +99 -0
  49. package/src/cdp/measure/census.ts +205 -0
  50. package/src/cdp/measure/check.ts +318 -0
  51. package/src/cdp/measure/collectors/animation.ts +385 -0
  52. package/src/cdp/measure/collectors/ax.ts +278 -0
  53. package/src/cdp/measure/collectors/focus.ts +1041 -0
  54. package/src/cdp/measure/collectors/forms.ts +905 -0
  55. package/src/cdp/measure/collectors/geometry.ts +1038 -0
  56. package/src/cdp/measure/collectors/hittest.ts +835 -0
  57. package/src/cdp/measure/collectors/layers.ts +697 -0
  58. package/src/cdp/measure/collectors/media.ts +522 -0
  59. package/src/cdp/measure/collectors/pixels.ts +1584 -0
  60. package/src/cdp/measure/collectors/queries.ts +464 -0
  61. package/src/cdp/measure/collectors/scroll.ts +599 -0
  62. package/src/cdp/measure/collectors/states.ts +992 -0
  63. package/src/cdp/measure/collectors/style-provenance.ts +643 -0
  64. package/src/cdp/measure/collectors/styles.ts +355 -0
  65. package/src/cdp/measure/collectors/text.ts +713 -0
  66. package/src/cdp/measure/diff.ts +470 -0
  67. package/src/cdp/measure/explain.ts +498 -0
  68. package/src/cdp/measure/map-ax.ts +633 -0
  69. package/src/cdp/measure/map-focus.ts +188 -0
  70. package/src/cdp/measure/map-layers.ts +177 -0
  71. package/src/cdp/measure/map-scroll.ts +317 -0
  72. package/src/cdp/measure/redaction.ts +51 -0
  73. package/src/cdp/measure/settle.ts +1051 -0
  74. package/src/cdp/measure/snapshot.ts +390 -0
  75. package/src/cdp/measure/sweep.ts +330 -0
  76. package/src/cdp/measure/types.ts +220 -0
  77. package/src/cdp/measure/v2/collectors.ts +19 -0
  78. package/src/cdp/measure/v2/orchestrator.ts +46 -0
  79. package/src/cdp/measure/v2/snap-selection.ts +33 -0
  80. package/src/cdp/measure/v2/target-lock.ts +15 -0
  81. package/src/cdp/measure/v2/target.ts +16 -0
  82. package/src/cdp/motion/jank.ts +433 -0
  83. package/src/cdp/motion/mask.ts +421 -0
  84. package/src/cdp/motion/recorder.ts +764 -0
  85. package/src/cdp/motion/response.ts +356 -0
  86. package/src/cdp/motion/timeline.ts +215 -0
  87. package/src/cdp/record.ts +5 -212
  88. package/src/cdp/recorder-bridge.ts +1478 -105
  89. package/src/cdp/recorder-client.ts +326 -0
  90. package/src/cdp/scope-lock.ts +73 -0
  91. package/src/cdp/screenshot.ts +199 -66
  92. package/src/cdp/targets.ts +21 -1
  93. package/src/cdp/timing.ts +15 -10
  94. package/src/cdp/types.ts +22 -7
  95. package/src/cdp/viewport.ts +21 -0
  96. package/src/cdp.ts +12 -58
  97. package/src/command-help.ts +29 -0
  98. package/src/command-resolution.ts +22 -0
  99. package/src/command-walker.ts +35 -0
  100. package/src/contracts/a11y.ts +33 -0
  101. package/src/contracts/cursor.ts +35 -0
  102. package/src/contracts/index.ts +15 -0
  103. package/src/contracts/motion.ts +19 -0
  104. package/src/contracts/neutral.ts +56 -0
  105. package/src/contracts/primitives.ts +205 -0
  106. package/src/contracts/release.ts +17 -0
  107. package/src/contracts/results.ts +208 -0
  108. package/src/contracts/routes.ts +140 -0
  109. package/src/contracts/snapshot.ts +92 -0
  110. package/src/cursor/index.ts +13 -0
  111. package/src/cursor/service.ts +310 -0
  112. package/src/errors.ts +71 -0
  113. package/src/har-manager.ts +992 -41
  114. package/src/interact.ts +410 -98
  115. package/src/output/artifact-lifecycle.ts +371 -0
  116. package/src/output/artifact.ts +6 -0
  117. package/src/output/exact-raw.ts +114 -0
  118. package/src/output/render.ts +396 -2
  119. package/src/output/structured.ts +211 -0
  120. package/src/registry.ts +208 -0
  121. package/src/session/artifacts.ts +452 -200
  122. package/src/session/commands.ts +1162 -397
  123. package/src/session/coordinator.ts +251 -0
  124. package/src/session/har-filter.ts +57 -0
  125. package/src/session/log-tailer.ts +653 -0
  126. package/src/session-context.ts +279 -51
  127. package/src/vault/bundle.ts +32 -17
  128. package/src/cdp/commands/exec.ts +0 -105
  129. package/src/cdp/commands/har.ts +0 -128
  130. package/src/cdp/commands/tabs.ts +0 -183
  131. package/src/cdp/commands/traffic.ts +0 -121
  132. package/src/cdp/commands/ui.ts +0 -172
  133. package/src/cdp/execute.ts +0 -113
  134. package/src/cdp/har-output.ts +0 -47
  135. package/src/cdp/usage.ts +0 -79
  136. package/src/tab-session.ts +0 -91
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,40 +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';
11
21
  import { measureMain } from './commands/measure/index.js';
12
22
  import { motionMain } from './commands/motion/index.js';
13
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
+ }
59
+
14
60
  export async function cdpMain(): Promise<void> {
15
61
  const args = process.argv.slice(2);
16
- 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);
17
71
 
18
- switch (parsed.command) {
19
- case 'detect': return cmdDetect(parsed, args);
20
- case 'list': return cmdList(parsed, args);
21
- case 'open': return cmdOpen(parsed, args);
22
- case 'reset-tab': return cmdResetTab(parsed, args);
23
- case 'exec': return cmdExec(parsed, args);
24
- case 'screenshot': return cmdScreenshot(parsed, args);
25
- case 'click': return cmdClick(parsed, args);
26
- case 'type': return cmdType(parsed, args);
27
- case 'a11y': return cmdA11y(parsed, args);
28
- case 'record': return cmdRecord(parsed, args);
29
- case 'navigate': return cmdNavigate(parsed, args);
30
- case 'network': return cmdNetwork(parsed, args);
31
- case 'har': return cmdHar(parsed, args);
32
- case 'lib': return cmdLib(parsed, args);
33
- case 'cdp': return cmdCdp(parsed, args);
34
- case 'measure': return measureMain(parsed, args);
35
- case 'motion': return motionMain(parsed, args);
36
- 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);
37
81
  default:
38
- 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)}`);
39
85
  }
40
86
  }