@crouton-kit/capture 1.3.6 → 1.3.7

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/src/cdp/usage.ts CHANGED
@@ -21,6 +21,7 @@ Commands:
21
21
  lib search "<q>" Fuzzy-search lib functions [--limit N]
22
22
  lib show <name> Lib + function summaries
23
23
  lib read <name> [fn…] Full input/output schemas (+ .ts source path)
24
+ cdp <Domain.method> Send a raw CDP command capture doesn't wrap [--browser] [--params json] [--wait-event name]
24
25
 
25
26
  Options:
26
27
  --port <port> Override CDP port
@@ -37,6 +38,10 @@ Options:
37
38
  --filter-status <s> Filter HAR by status: code, prefix (4), or range (400-499)
38
39
  --filter-method <m> Filter HAR by HTTP method (har read)
39
40
  --limit <N> Return only first N matching HAR entries (har read)
41
+ --browser Target the browser-level connection (cdp)
42
+ --params '<json>' JSON params for the method (cdp)
43
+ --wait-event <name> Wait for a CDP event and return it (cdp)
44
+ --timeout <ms> Wait timeout, default 10000 (cdp)
40
45
  --out <path> Output path (screenshot)
41
46
  --viewport <preset> Viewport preset: desktop-wide|desktop|tablet|mobile (default: desktop)
42
47
  --height <px> Override viewport height (screenshot)
@@ -67,4 +72,6 @@ Examples:
67
72
  capture lib search "search emails"
68
73
  capture lib read gmail searchEmails
69
74
  capture exec "import {searchEmails} from 'libs/gmail'; const ctx = await getContext(); return await searchEmails({ ...ctx, query: 'invoice' })"
75
+ capture cdp Browser.grantPermissions --browser --params '{"origin":"http://localhost:3000","permissions":["notifications"]}'
76
+ capture cdp --help Raw CDP escape hatch (Browser.*, ServiceWorker.*, ...) + how session --hold keeps grants alive
70
77
  `;
@@ -13,6 +13,7 @@ import {
13
13
  clearActiveSession,
14
14
  } from '../session-context.js';
15
15
  import { expandEqualsFlags } from '../cdp/args.js';
16
+ import { startBridge, stopBridge } from '../cdp/bridge/spawn.js';
16
17
 
17
18
  const CAPTURE_ROOT = path.join(os.tmpdir(), 'capture-sessions');
18
19
 
@@ -31,6 +32,9 @@ interface Session {
31
32
  targetId: string | null;
32
33
  stepCount: number;
33
34
  logPids: LogPid[];
35
+ /** Set when started with --hold: one CDP browser connection held open for the session. */
36
+ bridgeSocket: string | null;
37
+ bridgePid: number | null;
34
38
  }
35
39
 
36
40
  interface BundleManifest {
@@ -90,7 +94,8 @@ function printSessionHelp(): void {
90
94
  console.log(`capture session — manage capture sessions
91
95
 
92
96
  Sub-commands:
93
- start [--url <url>] Start a session (opens tab, records HAR, sets active context)
97
+ start [--url <url>] [--hold] Start a session (opens tab, records HAR, sets active context)
98
+ --hold keeps one CDP browser connection open for the session
94
99
  stop <session-id> Finalize and bundle artifacts (screenshots, HAR, a11y, logs)
95
100
  list List active and stopped sessions
96
101
  view <id> [--filter section] View bundle manifest; section = screenshots|har|a11y|logs
@@ -110,6 +115,17 @@ Typical flow:
110
115
  3. capture session stop <session-id>
111
116
  4. capture session view <session-id>
112
117
 
118
+ --hold: keeps one CDP browser connection open for the session's lifetime, so
119
+ browser-level state (Browser.grantPermissions, ServiceWorker.enable, ...)
120
+ survives across separate commands instead of reverting the instant each
121
+ command's own connection closes. Use it together with \`capture cdp --browser\`:
122
+
123
+ capture session start --url http://localhost:3000 --hold
124
+ capture cdp Browser.grantPermissions --browser --params '{"origin":"http://localhost:3000","permissions":["notifications"]}'
125
+ capture cdp ServiceWorker.enable --browser --target <pageTabId>
126
+ capture cdp ServiceWorker.deliverPushMessage --browser --target <pageTabId> --params '{...}'
127
+ capture session stop <session-id> # also tears down the held connection
128
+
113
129
  Related: capture log <path> [--name label] Tail a log into the active session
114
130
  See also: capture --help Full command list`);
115
131
  }
@@ -121,10 +137,13 @@ See also: capture --help Full command list`);
121
137
  async function start(rawArgs: string[]): Promise<void> {
122
138
  const args = expandEqualsFlags(rawArgs);
123
139
  let url: string | null = null;
140
+ let hold = false;
124
141
 
125
142
  for (let i = 0; i < args.length; i++) {
126
143
  if (args[i] === '--url' && args[i + 1]) {
127
144
  url = args[++i];
145
+ } else if (args[i] === '--hold') {
146
+ hold = true;
128
147
  }
129
148
  }
130
149
 
@@ -145,11 +164,15 @@ async function start(rawArgs: string[]): Promise<void> {
145
164
  // Open tab if URL provided. Fail fast if the new target cannot attach.
146
165
  let targetId: string | null = null;
147
166
  let pageLoadTimedOut = false;
167
+ let cdpPort: number | null = null;
168
+ if (url || hold) {
169
+ const { detectCdpPort } = await import('../cdp.js');
170
+ cdpPort = await detectCdpPort();
171
+ }
148
172
  if (url) {
149
173
  try {
150
- const { detectCdpPort, openTab, CDPClient } = await import('../cdp.js');
151
- const port = await detectCdpPort();
152
- const tab = await openTab(port, url);
174
+ const { openTab, CDPClient } = await import('../cdp.js');
175
+ const tab = await openTab(cdpPort!, url);
153
176
  targetId = tab.id;
154
177
 
155
178
  if (!targetId) {
@@ -188,6 +211,29 @@ async function start(rawArgs: string[]): Promise<void> {
188
211
  }
189
212
  }
190
213
 
214
+ // Hold a CDP browser connection open for the session's lifetime so
215
+ // browser-level state (permission grants, ServiceWorker enablement)
216
+ // survives across separate `capture` commands instead of reverting the
217
+ // instant each command's own connection closes.
218
+ let bridgeSocket: string | null = null;
219
+ let bridgePid: number | null = null;
220
+ if (hold) {
221
+ try {
222
+ const bridge = await startBridge(dir, cdpPort!);
223
+ bridgeSocket = bridge.socketPath;
224
+ bridgePid = bridge.pid;
225
+ } catch (err) {
226
+ if (harId) {
227
+ try {
228
+ deleteHarRecording(harId);
229
+ } catch {
230
+ /* best effort */
231
+ }
232
+ }
233
+ throw err;
234
+ }
235
+ }
236
+
191
237
  const session: Session = {
192
238
  id,
193
239
  dir,
@@ -197,6 +243,8 @@ async function start(rawArgs: string[]): Promise<void> {
197
243
  targetId,
198
244
  stepCount: 0,
199
245
  logPids: [],
246
+ bridgeSocket,
247
+ bridgePid,
200
248
  };
201
249
  fs.writeFileSync(sessionMetaPath(id), JSON.stringify(session, null, 2));
202
250
 
@@ -207,6 +255,7 @@ async function start(rawArgs: string[]): Promise<void> {
207
255
  harId,
208
256
  targetId,
209
257
  stepCount: 0,
258
+ bridgeSocket,
210
259
  });
211
260
 
212
261
  // Output for agent consumption
@@ -218,6 +267,7 @@ async function start(rawArgs: string[]): Promise<void> {
218
267
  pageLoadTimedOut,
219
268
  shotsDir: path.join(dir, 'shots'),
220
269
  a11yDir: path.join(dir, 'a11y'),
270
+ held: hold,
221
271
  };
222
272
  console.log(JSON.stringify(result, null, 2));
223
273
 
@@ -229,6 +279,9 @@ async function start(rawArgs: string[]): Promise<void> {
229
279
  console.error(`Page load timed out, but the session is attached to target ${targetId.slice(0, 8)}. Use \`capture exec --target ${targetId.slice(0, 8)}\` or \`capture list\` if you need to recover it.`);
230
280
  }
231
281
  }
282
+ if (hold) {
283
+ console.error(`CDP bridge held (pid ${bridgePid}) — browser-level state now survives across commands via: capture cdp <Method> --browser [--params '<json>']`);
284
+ }
232
285
  console.error(`\nWhen done: capture session stop ${id}`);
233
286
  }
234
287
 
@@ -309,6 +362,12 @@ async function stop(args: string[]): Promise<void> {
309
362
  await new Promise((r) => setTimeout(r, 200));
310
363
  }
311
364
 
365
+ // Tear down the held CDP bridge, if any — releases the browser-level
366
+ // connection (and any grants/target-enablement it was keeping alive).
367
+ if (session.bridgePid || session.bridgeSocket) {
368
+ stopBridge(session.bridgePid, session.bridgeSocket);
369
+ }
370
+
312
371
  // Collect screenshots
313
372
  const shotsDir = path.join(session.dir, 'shots');
314
373
  const screenshots = fs.existsSync(shotsDir)
@@ -357,7 +416,7 @@ async function stop(args: string[]): Promise<void> {
357
416
 
358
417
  // Collect anything else dropped in the session dir
359
418
  const knownDirs = new Set(['shots', 'a11y', 'logs']);
360
- const knownFiles = new Set(['.session.json', 'har.json', 'bundle.json']);
419
+ const knownFiles = new Set(['.session.json', 'har.json', 'bundle.json', 'bridge.sock']);
361
420
  const other = fs.readdirSync(session.dir)
362
421
  .filter((f) => !knownDirs.has(f) && !knownFiles.has(f))
363
422
  .map((f) => ({ name: f, path: path.join(session.dir, f) }));
@@ -19,6 +19,8 @@ export interface ActiveSessionState {
19
19
  harId: string | null;
20
20
  targetId: string | null;
21
21
  stepCount: number;
22
+ /** Unix socket of the session's held CDP bridge (`session start --hold`), if any. */
23
+ bridgeSocket?: string | null;
22
24
  }
23
25
 
24
26
  export function getActiveSession(): ActiveSessionState | null {