@monoes/monobrowse 1.0.17 → 1.0.21

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 (68) hide show
  1. package/dist/src/__tests__/browse-concurrency.test.d.ts +15 -0
  2. package/dist/src/__tests__/browse-concurrency.test.d.ts.map +1 -0
  3. package/dist/src/__tests__/browse-concurrency.test.js +198 -0
  4. package/dist/src/__tests__/browse-concurrency.test.js.map +1 -0
  5. package/dist/src/__tests__/browser-launch.test.js +132 -1
  6. package/dist/src/__tests__/browser-launch.test.js.map +1 -1
  7. package/dist/src/__tests__/close-browser.test.js +2 -2
  8. package/dist/src/__tests__/close-browser.test.js.map +1 -1
  9. package/dist/src/__tests__/frame-command.test.js +12 -2
  10. package/dist/src/__tests__/frame-command.test.js.map +1 -1
  11. package/dist/src/__tests__/reap-idle-browser.test.js +13 -13
  12. package/dist/src/__tests__/reap-idle-browser.test.js.map +1 -1
  13. package/dist/src/__tests__/ref-cache.test.js +96 -58
  14. package/dist/src/__tests__/ref-cache.test.js.map +1 -1
  15. package/dist/src/__tests__/report-exit-code.test.js +1 -1
  16. package/dist/src/__tests__/session-discovery.test.d.ts +13 -0
  17. package/dist/src/__tests__/session-discovery.test.d.ts.map +1 -0
  18. package/dist/src/__tests__/session-discovery.test.js +218 -0
  19. package/dist/src/__tests__/session-discovery.test.js.map +1 -0
  20. package/dist/src/browser/browser.d.ts +2 -1
  21. package/dist/src/browser/browser.d.ts.map +1 -1
  22. package/dist/src/browser/browser.js +145 -42
  23. package/dist/src/browser/browser.js.map +1 -1
  24. package/dist/src/browser/ref-cache.d.ts +50 -22
  25. package/dist/src/browser/ref-cache.d.ts.map +1 -1
  26. package/dist/src/browser/ref-cache.js +122 -57
  27. package/dist/src/browser/ref-cache.js.map +1 -1
  28. package/dist/src/browser/types.d.ts +2 -0
  29. package/dist/src/browser/types.d.ts.map +1 -1
  30. package/dist/src/browser/types.js.map +1 -1
  31. package/dist/src/cli/commands-context.js +1 -1
  32. package/dist/src/cli/commands-context.js.map +1 -1
  33. package/dist/src/cli/commands-navigate.js +2 -2
  34. package/dist/src/cli/commands-navigate.js.map +1 -1
  35. package/dist/src/cli/commands-page.js +2 -2
  36. package/dist/src/cli/commands-page.js.map +1 -1
  37. package/dist/src/cli/commands-report.js +2 -2
  38. package/dist/src/cli/commands-report.js.map +1 -1
  39. package/dist/src/cli/commands-session.d.ts.map +1 -1
  40. package/dist/src/cli/commands-session.js +70 -56
  41. package/dist/src/cli/commands-session.js.map +1 -1
  42. package/dist/src/cli/commands.d.ts.map +1 -1
  43. package/dist/src/cli/commands.js +102 -66
  44. package/dist/src/cli/commands.js.map +1 -1
  45. package/dist/src/cli/session.d.ts +66 -2
  46. package/dist/src/cli/session.d.ts.map +1 -1
  47. package/dist/src/cli/session.js +190 -33
  48. package/dist/src/cli/session.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +1 -1
  51. package/src/__tests__/browse-concurrency.test.ts +233 -0
  52. package/src/__tests__/browser-launch.test.ts +142 -1
  53. package/src/__tests__/close-browser.test.ts +2 -2
  54. package/src/__tests__/frame-command.test.ts +12 -2
  55. package/src/__tests__/reap-idle-browser.test.ts +15 -14
  56. package/src/__tests__/ref-cache.test.ts +112 -59
  57. package/src/__tests__/report-exit-code.test.ts +1 -1
  58. package/src/__tests__/session-discovery.test.ts +259 -0
  59. package/src/browser/browser.ts +149 -39
  60. package/src/browser/ref-cache.ts +144 -72
  61. package/src/browser/types.ts +2 -0
  62. package/src/cli/commands-context.ts +1 -1
  63. package/src/cli/commands-navigate.ts +2 -2
  64. package/src/cli/commands-page.ts +2 -2
  65. package/src/cli/commands-report.ts +2 -2
  66. package/src/cli/commands-session.ts +74 -55
  67. package/src/cli/commands.ts +106 -66
  68. package/src/cli/session.ts +208 -33
@@ -10,10 +10,13 @@
10
10
 
11
11
  import { output } from './output.js';
12
12
  import {
13
+ adoptLegacySession,
13
14
  detectAttentionNeeded,
14
15
  ensureConnected,
15
- ensureSignalCleanupHandlers,
16
16
  getBrowser,
17
+ launchSessionBrowser,
18
+ pinnedPort,
19
+ resolveLiveSession,
17
20
  session,
18
21
  switchToHeaded,
19
22
  } from './session.js';
@@ -23,7 +26,13 @@ export const openCommand: Command = {
23
26
  name: 'open',
24
27
  description: 'Open a URL in the browser. Usage: monomind browse open <url>',
25
28
  options: [
26
- { name: 'port', short: 'p', type: 'number', description: 'CDP port', default: 9222 },
29
+ {
30
+ name: 'port',
31
+ short: 'p',
32
+ type: 'number',
33
+ description:
34
+ 'Attach to (or launch on) this CDP port. Default: a free port of this session’s own',
35
+ },
27
36
  {
28
37
  name: 'headed',
29
38
  type: 'boolean',
@@ -37,7 +46,10 @@ export const openCommand: Command = {
37
46
  const url = ctx.args[0] as string;
38
47
  if (!url) throw new Error('URL required. Usage: monomind browse open <url>');
39
48
 
40
- const port = (ctx.flags.port as number) ?? 9222;
49
+ // No --port means "start a session of my own on a free port" (#318) —
50
+ // never "join whatever is on 9222", which is what made two uncoordinated
51
+ // `open` calls race for one browser and one profile directory.
52
+ const port = pinnedPort(ctx.flags);
41
53
  const forceHeaded = ctx.flags.headed as boolean;
42
54
  const browser = await getBrowser();
43
55
 
@@ -77,38 +89,9 @@ export const openCommand: Command = {
77
89
  session.refs = new Map();
78
90
  }
79
91
 
80
- session.port = await browser.launchBrowser({ port, headless: !forceHeaded });
81
- // Persist the active port so subsequent CLI invocations (each a fresh
82
- // process) default to attaching here instead of hardcoded 9222. Also
83
- // persist the launched PID/userDataDir so a later process's closeBrowser
84
- // can still kill this Chrome even though launchedPids (browser.ts) is
85
- // per-process and empty there.
86
- //
87
- // launchBrowser() can either LAUNCH a fresh Chrome or ATTACH to one
88
- // already listening on the requested port (see its own "attach if
89
- // already Chrome" comment) — it returns only a port number, with no
90
- // signal telling this caller which happened. getLaunchedPid(session.port) is
91
- // undefined on the attach path (this process never spawned anything).
92
- // Bug fixed here: unconditionally saving {pid: undefined, ...} on
93
- // attach used to CLOBBER a real PID a previous `open` had already
94
- // persisted for this exact port, destroying the only way a later
95
- // process's closeBrowser() PID-kill fallback could ever find it.
96
- const freshPid = browser.getLaunchedPid(session.port);
97
- const freshUserDataDir = browser.getLaunchedUserDataDir(session.port);
98
- if (freshPid !== undefined) {
99
- await browser.saveActivePort(session.port, { pid: freshPid, userDataDir: freshUserDataDir });
100
- } else {
101
- // Attach path: preserve whatever PID/userDataDir was already on file
102
- // for this port (most likely from the process that originally
103
- // launched it) instead of overwriting with undefined.
104
- const existing = await browser.loadActivePortInfo();
105
- const preserved = existing && existing.port === session.port ? existing : undefined;
106
- await browser.saveActivePort(session.port, {
107
- pid: preserved?.pid,
108
- userDataDir: preserved?.userDataDir,
109
- });
110
- }
111
- ensureSignalCleanupHandlers();
92
+ // Starts this session's own browser (and records it) unless --port named
93
+ // one to attach to — see the session rule in session.ts.
94
+ session.port = await launchSessionBrowser(browser, { port, headless: !forceHeaded });
112
95
  const conn = await browser.connectToTarget(session.port);
113
96
  session.client = conn.client;
114
97
  session.sessionId = conn.sessionId;
@@ -117,7 +100,7 @@ export const openCommand: Command = {
117
100
  // A snapshot taken before this navigation is no longer valid for the new
118
101
  // page — drop the persisted ref cache so a stale process's weak
119
102
  // time-based check can't resurrect it before the next explicit snapshot.
120
- await browser.clearRefCache();
103
+ await browser.clearRefCache(session.port);
121
104
 
122
105
  if (ctx.flags.state && ctx.flags.session) {
123
106
  output.printWarning('Both --state and --session provided; --state takes precedence');
@@ -142,7 +125,7 @@ export const openCommand: Command = {
142
125
  output.printWarning(
143
126
  `${attentionType === 'captcha' ? 'CAPTCHA' : 'Login'} detected — switching to headed mode`,
144
127
  );
145
- await switchToHeaded(currentUrl, port);
128
+ await switchToHeaded(currentUrl, session.port);
146
129
  await browser.openUrl(session.client!, session.sessionId, currentUrl);
147
130
  output.printSuccess(
148
131
  `Resumed headless after ${attentionType === 'captcha' ? 'CAPTCHA' : 'login'}`,
@@ -153,15 +136,27 @@ export const openCommand: Command = {
153
136
  const finalUrl = await browser.getCurrentUrl(session.client!, session.sessionId);
154
137
  const title = await browser.getCurrentTitle(session.client!, session.sessionId);
155
138
 
156
- output.printSuccess(`Opened: ${title} (${finalUrl})`);
157
- return { success: true, data: { url: finalUrl, title } };
139
+ // The port is part of the result, not decoration: it is this session's
140
+ // handle, and the only way a concurrent caller can pin it from another
141
+ // process (`--port N`) instead of resolving "the newest live session".
142
+ output.printSuccess(`Opened: ${title} (${finalUrl}) [port ${session.port}]`);
143
+ return { success: true, data: { url: finalUrl, title, port: session.port } };
158
144
  },
159
145
  };
160
146
 
161
147
  export const closeCommand: Command = {
162
148
  name: 'close',
163
- description: 'Close the active browser session',
164
- action: async (_ctx: CommandContext): Promise<CommandResult> => {
149
+ description:
150
+ 'Close a browser session. Usage: monomind browse close [--port <port>] (default: the newest live session here)',
151
+ options: [
152
+ {
153
+ name: 'port',
154
+ short: 'p',
155
+ type: 'number',
156
+ description: 'Close the session on this CDP port instead of the newest live one',
157
+ },
158
+ ],
159
+ action: async (ctx: CommandContext): Promise<CommandResult> => {
165
160
  if (session.client) {
166
161
  const browser = await getBrowser();
167
162
  const sid = session.sessionId;
@@ -192,26 +187,48 @@ export const closeCommand: Command = {
192
187
  browser.stopRequestCapture(sid);
193
188
  browser.teardownDialogHandling(sid);
194
189
  browser.teardownConsoleCapture(sid);
190
+ // Terminate the browser this process launched, not just our socket to
191
+ // it: with a session of its own on a free port (#318), a Chrome left
192
+ // behind here is one nothing else knows how to find. A browser we
193
+ // merely attached to (`connect`) is the user's — never killed.
194
+ const record = await browser.loadSessionRecord(session.port);
195
+ if (record?.launched !== false) {
196
+ try {
197
+ await browser.closeBrowser(client, session.port);
198
+ } catch {
199
+ /* best-effort — the record cleanup below still runs */
200
+ }
201
+ }
195
202
  client.close();
196
203
  session.client = null;
197
204
  session.sessionId = '';
198
205
  session.parentSessionId = '';
199
206
  session.targetId = '';
200
207
  session.refs = new Map();
201
- // Session is gone — forget the persisted port and stale refs so later
208
+ // This session is gone — forget its record and stale refs so later
202
209
  // invocations don't chase a dead endpoint (or the wrong elements).
203
- await browser.clearActivePort();
204
- await browser.clearRefCache();
205
- output.printSuccess('Browser session closed');
210
+ // Other sessions' records are left alone.
211
+ await browser.removeSessionRecord(session.port);
212
+ await browser.clearRefCache(session.port);
213
+ output.printSuccess(`Browser session closed (port ${session.port})`);
206
214
  } else {
207
215
  // Each CLI invocation is a fresh process, so `close` almost always
208
- // lands here. The persisted port file is the real session handle:
209
- // if monobrowse LAUNCHED that browser (open), gracefully Browser.close
216
+ // lands here. The session record is the real handle: `--port` names
217
+ // one, otherwise it is the newest session whose browser still answers
218
+ // (strict:false — a dead one is cleaned up, not raised as an error).
219
+ // If monobrowse LAUNCHED that browser (open), gracefully Browser.close
210
220
  // it — otherwise every open→close cycle leaks a headless Chrome. If we
211
221
  // merely ATTACHED to it (connect, launched:false), never kill it: it's
212
- // the user's own browser. Either way, forget the port and refs.
222
+ // the user's own browser. Either way, forget that one session.
213
223
  const browser = await getBrowser();
214
- const persisted = await browser.loadActivePortInfo();
224
+ const pinned = pinnedPort(ctx.flags);
225
+ const persisted = pinned
226
+ ? ((await browser.loadSessionRecord(pinned)) ??
227
+ (await adoptLegacySession(browser, { port: pinned })) ?? {
228
+ port: pinned,
229
+ launched: true,
230
+ })
231
+ : await resolveLiveSession(browser, { strict: false });
215
232
  if (persisted?.launched) {
216
233
  try {
217
234
  const conn = await browser.connectToTarget(persisted.port);
@@ -257,8 +274,10 @@ export const closeCommand: Command = {
257
274
  : 'No active browser session',
258
275
  );
259
276
  }
260
- await browser.clearActivePort();
261
- await browser.clearRefCache();
277
+ if (persisted) {
278
+ await browser.removeSessionRecord(persisted.port);
279
+ await browser.clearRefCache(persisted.port);
280
+ }
262
281
  }
263
282
  return { success: true };
264
283
  },
@@ -346,12 +365,12 @@ export const connectCommand: Command = {
346
365
  session.targetId = conn.target.id;
347
366
  session.port = port;
348
367
  session.refs = new Map();
349
- // Persist the port like `open` does — without this, the NEXT CLI process
350
- // (each command is a fresh process) resolves the hardcoded default and
351
- // tries to launch its own Chrome on 9222 instead of reusing this session.
368
+ // Record the session like `open` does — without this, the NEXT CLI
369
+ // process (each command is a fresh process) has nothing to resolve and
370
+ // launches a browser of its own instead of reusing this session.
352
371
  // launched:false marks this browser as someone else's — close must never
353
372
  // kill it, and a dead endpoint must not be silently relaunched.
354
- await browser.saveActivePort(port, { launched: false });
373
+ await browser.saveSessionRecord(port, { launched: false });
355
374
  const url = await browser.getCurrentUrl(session.client, session.sessionId);
356
375
  const title = await browser.getCurrentTitle(session.client, session.sessionId);
357
376
  output.printSuccess(`Connected: ${title} (${url})`);
@@ -90,6 +90,7 @@ import {
90
90
  } from './commands-trace.js';
91
91
  import { output } from './output.js';
92
92
  import { createPlatformCommand } from './platform.js';
93
+ import { applySessionPortFlag } from './session.js';
93
94
  import type { Command, CommandContext, CommandResult } from './types.js';
94
95
 
95
96
  // Re-exported for direct unit testing — these were exported from this file
@@ -212,80 +213,119 @@ const batchCommand: Command = {
212
213
  const actionSubcommand: Command = wrapCommanderCommand(createActionCommand);
213
214
  const platformSubcommand: Command = wrapCommanderCommand(createPlatformCommand);
214
215
 
216
+ // ---------------------------------------------------------------------------
217
+ // Session selector
218
+ //
219
+ // Sessions are per-port (#318), so `--port N` is how any command says which
220
+ // one it means; without it a command resolves the newest live session in this
221
+ // directory. Wiring it here, once, gives every subcommand the selector
222
+ // without each action having to read the flag: session.port is set before the
223
+ // action runs, and that is what every ensureConnected(session.port) resolves.
224
+ // ---------------------------------------------------------------------------
225
+
226
+ const PORT_OPTION = {
227
+ name: 'port',
228
+ short: 'p',
229
+ type: 'number',
230
+ description: 'Act on the browse session on this CDP port (default: the newest live session)',
231
+ } as const;
232
+
233
+ function withSessionSelector(cmd: Command): Command {
234
+ const action = cmd.action;
235
+ return {
236
+ ...cmd,
237
+ options: cmd.options?.some((o) => o.name === 'port')
238
+ ? cmd.options
239
+ : [...(cmd.options ?? []), { ...PORT_OPTION }],
240
+ action: action
241
+ ? async (ctx: CommandContext) => {
242
+ applySessionPortFlag(ctx.flags);
243
+ return action(ctx);
244
+ }
245
+ : undefined,
246
+ subcommands: cmd.subcommands?.map(withSessionSelector),
247
+ };
248
+ }
249
+
215
250
  // ---------------------------------------------------------------------------
216
251
  // Root browse command
217
252
  // ---------------------------------------------------------------------------
218
253
 
254
+ // Every subcommand gets the `--port` session selector (see
255
+ // withSessionSelector). batchCommand dispatches over this same array, so a
256
+ // batched command honours the selector too.
257
+ const subcommands: Command[] = [
258
+ openCommand,
259
+ snapshotCommand,
260
+ clickCommand,
261
+ dblclickCommand,
262
+ fillCommand,
263
+ typeCommand,
264
+ pressCommand,
265
+ keyboardCommand,
266
+ keydownCommand,
267
+ keyupCommand,
268
+ hoverCommand,
269
+ focusCommand,
270
+ selectCommand,
271
+ checkCommand,
272
+ uncheckCommand,
273
+ isvisibleCommand,
274
+ isenabledCommand,
275
+ ischeckedCommand,
276
+ tapCommand,
277
+ swipeCommand,
278
+ scrollIntoViewCommand,
279
+ dragCommand,
280
+ uploadCommand,
281
+ downloadCommand,
282
+ mouseCommand,
283
+ clipboardCommand,
284
+ waitCommand,
285
+ screenshotCommand,
286
+ getCommand,
287
+ scrollCommand,
288
+ navigateCommand,
289
+ setCommand,
290
+ stateCommand,
291
+ networkCommand,
292
+ evalCommand,
293
+ dialogCommand,
294
+ frameCommand,
295
+ tabCommand,
296
+ windowCommand,
297
+ consoleLogCommand,
298
+ errorsCommand,
299
+ storageCommand,
300
+ cookiesCommand,
301
+ pdfCommand,
302
+ isCommand,
303
+ findCommand,
304
+ highlightCommand,
305
+ diffCommand,
306
+ pushstateCommand,
307
+ batchCommand,
308
+ addinitscriptCommand,
309
+ removeinitscriptCommand,
310
+ connectCommand,
311
+ recordCommand,
312
+ traceCommand,
313
+ profilerCommand,
314
+ vitalsCommand,
315
+ reportCommand,
316
+ harCommand,
317
+ resizeCommand,
318
+ closeCommand,
319
+ actionSubcommand,
320
+ platformSubcommand,
321
+ ].map(withSessionSelector);
322
+
219
323
  const browseCommand: Command = {
220
324
  name: 'browse',
221
325
  description: 'Native browser automation via Chrome DevTools Protocol',
222
- subcommands: [
223
- openCommand,
224
- snapshotCommand,
225
- clickCommand,
226
- dblclickCommand,
227
- fillCommand,
228
- typeCommand,
229
- pressCommand,
230
- keyboardCommand,
231
- keydownCommand,
232
- keyupCommand,
233
- hoverCommand,
234
- focusCommand,
235
- selectCommand,
236
- checkCommand,
237
- uncheckCommand,
238
- isvisibleCommand,
239
- isenabledCommand,
240
- ischeckedCommand,
241
- tapCommand,
242
- swipeCommand,
243
- scrollIntoViewCommand,
244
- dragCommand,
245
- uploadCommand,
246
- downloadCommand,
247
- mouseCommand,
248
- clipboardCommand,
249
- waitCommand,
250
- screenshotCommand,
251
- getCommand,
252
- scrollCommand,
253
- navigateCommand,
254
- setCommand,
255
- stateCommand,
256
- networkCommand,
257
- evalCommand,
258
- dialogCommand,
259
- frameCommand,
260
- tabCommand,
261
- windowCommand,
262
- consoleLogCommand,
263
- errorsCommand,
264
- storageCommand,
265
- cookiesCommand,
266
- pdfCommand,
267
- isCommand,
268
- findCommand,
269
- highlightCommand,
270
- diffCommand,
271
- pushstateCommand,
272
- batchCommand,
273
- addinitscriptCommand,
274
- removeinitscriptCommand,
275
- connectCommand,
276
- recordCommand,
277
- traceCommand,
278
- profilerCommand,
279
- vitalsCommand,
280
- reportCommand,
281
- harCommand,
282
- resizeCommand,
283
- closeCommand,
284
- actionSubcommand,
285
- platformSubcommand,
286
- ],
326
+ subcommands,
287
327
  options: [
288
- { name: 'port', short: 'p', type: 'number', description: 'CDP debug port', default: 9222 },
328
+ { ...PORT_OPTION },
289
329
  { name: 'session', short: 's', type: 'string', description: 'Named session to use' },
290
330
  ],
291
331
  examples: [