@xbrowser/cli 0.16.0 → 1.0.2

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 (52) hide show
  1. package/README.md +17 -26
  2. package/dist/{browser-R7B255ML.js → browser-GITRHHFO.js} +4 -1
  3. package/dist/{browser-GWBH6OJK.js → browser-R56O3CW6.js} +3 -1
  4. package/dist/{browser-I2HJZ7IP.js → browser-ZJOZB5CR.js} +4 -2
  5. package/dist/cdp-driver-BE3FOMRN.js +2803 -0
  6. package/dist/cdp-driver-TOPYJIFL.js +47 -0
  7. package/dist/chunk-2SVQTI2O.js +2794 -0
  8. package/dist/{chunk-KDYXFLAC.js → chunk-ACFE6PKF.js} +1015 -121
  9. package/dist/chunk-BBMRDUYQ.js +260 -0
  10. package/dist/chunk-CAFNSGYM.js +4834 -0
  11. package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
  12. package/dist/{chunk-RS6YYWTK.js → chunk-JPA2ZT2R.js} +140 -72
  13. package/dist/chunk-JPHCY4TC.js +260 -0
  14. package/dist/chunk-KFQGP6VL.js +33 -0
  15. package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
  16. package/dist/chunk-OZKD3W4X.js +417 -0
  17. package/dist/chunk-PPG4D2EW.js +2796 -0
  18. package/dist/{chunk-ATFTAKMN.js → chunk-Q4IGYTKR.js} +39 -7
  19. package/dist/{chunk-F3ZWFCJJ.js → chunk-QIK2I3VQ.js} +141 -72
  20. package/dist/chunk-WJRE55TN.js +83 -0
  21. package/dist/cli.js +2358 -1086
  22. package/dist/{convert-4DUWZIKH.js → convert-LB3GJTLR.js} +4 -2
  23. package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
  24. package/dist/{daemon-client-GX2UYIW4.js → daemon-client-DRCUMNHK.js} +45 -72
  25. package/dist/{daemon-client-XWSSQBEA.js → daemon-client-UZZEHHIV.js} +8 -1
  26. package/dist/daemon-main.js +3067 -1688
  27. package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
  28. package/dist/{extract-EGRXZSSK.js → extract-BSYBM4MR.js} +2 -0
  29. package/dist/{filter-OLAE26HN.js → filter-KCFO4RSV.js} +2 -0
  30. package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
  31. package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
  32. package/dist/index.d.ts +745 -148
  33. package/dist/index.js +3488 -1719
  34. package/dist/launcher-QUJ4M2VS.js +19 -0
  35. package/dist/launcher-YARP45UY.js +19 -0
  36. package/dist/{network-store-YAF5OIBH.js → network-store-XGZ25FFC.js} +1 -0
  37. package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
  38. package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
  39. package/dist/{proxy-WKGUCH2C.js → proxy-LV4BJ5RC.js} +1 -1
  40. package/dist/session-recorder-RTDGURIJ.js +8 -0
  41. package/dist/session-recorder-YI7YYM36.js +7 -0
  42. package/dist/session-replayer-GLTUICSD.js +276 -0
  43. package/dist/site-knowledge-SYC6VCDB.js +23 -0
  44. package/package.json +6 -6
  45. package/dist/chunk-2ONMTDLK.js +0 -2050
  46. package/dist/daemon-client-3IJD6X4B.js +0 -59
  47. package/dist/network-store-2S5HATEV.js +0 -194
  48. package/dist/parse-action-dsl-DRSPBALP.js +0 -72
  49. package/dist/screenshot-CWAWMXVA.js +0 -28
  50. package/dist/screenshot-MB6R7RSS.js +0 -26
  51. package/dist/session-recorder-ILSSV2UC.js +0 -6
  52. package/dist/session-recorder-XET3DNML.js +0 -7
@@ -9,12 +9,12 @@ function generateJSScript(recording) {
9
9
  // Start URL: ${recording.startUrl}
10
10
  // Events: ${events.length}
11
11
 
12
- import { chromium } from 'playwright';
12
+ import { launch } from '@xbrowser/cli';
13
13
 
14
14
  const START_URL = '${escapeString(recording.startUrl)}';
15
15
 
16
16
  async function main() {
17
- const browser = await chromium.launch({ headless: true });
17
+ const { browser } = await launch({ headless: true });
18
18
  const context = await browser.newContext();
19
19
  const page = await context.newPage();
20
20
 
@@ -1,3 +1,6 @@
1
+ import {
2
+ launch
3
+ } from "./chunk-PPG4D2EW.js";
1
4
  import {
2
5
  CDPInterceptorProxy
3
6
  } from "./chunk-ZZ2TFWIV.js";
@@ -7,7 +10,6 @@ import { randomUUID } from "crypto";
7
10
  import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from "fs";
8
11
  import { join } from "path";
9
12
  import { homedir } from "os";
10
- import { chromium } from "playwright";
11
13
 
12
14
  // src/utils/cdp.ts
13
15
  async function fetchNoProxy(url) {
@@ -63,6 +65,7 @@ async function resolveCDPEndpoint(raw) {
63
65
  }
64
66
 
65
67
  // src/browser.ts
68
+ import { SessionStore } from "@dyyz1993/xcli-core";
66
69
  function logSessionEvent(event, details) {
67
70
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
68
71
  const pid = process.pid;
@@ -75,7 +78,7 @@ function sessionFile(name) {
75
78
  function ensureSessionDir() {
76
79
  mkdirSync(SESSION_DIR, { recursive: true });
77
80
  }
78
- var sessions = /* @__PURE__ */ new Map();
81
+ var sessions = new SessionStore();
79
82
  var _sharedBrowser = null;
80
83
  var _sharedCdpProxy = null;
81
84
  var IDLE_TIMEOUT_MS = (process.env.XBROWSER_IDLE_TIMEOUT ? parseInt(process.env.XBROWSER_IDLE_TIMEOUT, 10) : 30) * 60 * 1e3;
@@ -86,7 +89,7 @@ function resetIdleTimer() {
86
89
  const now = Date.now();
87
90
  let allIdle = true;
88
91
  const idleSessions = [];
89
- for (const [, s] of sessions) {
92
+ for (const s of sessions) {
90
93
  if (now - s.lastActivityAt < IDLE_TIMEOUT_MS) {
91
94
  allIdle = false;
92
95
  } else {
@@ -109,7 +112,7 @@ function touchSession(id) {
109
112
  resetIdleTimer();
110
113
  }
111
114
  process.on("exit", () => {
112
- for (const session of sessions.values()) {
115
+ for (const session of sessions.list()) {
113
116
  if (session.isCDP) {
114
117
  logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
115
118
  } else {
@@ -192,15 +195,21 @@ async function createBrowser(options) {
192
195
  const realEndpoint = await resolveCDPEndpoint(options.cdpEndpoint);
193
196
  if (options.intercept) {
194
197
  const config = typeof options.intercept === "object" ? { ...options.intercept, cdpEndpoint: realEndpoint } : { cdpEndpoint: realEndpoint };
195
- const proxy = new CDPInterceptorProxy(config);
196
- const proxyPort = await proxy.start();
198
+ _sharedCdpProxy = new CDPInterceptorProxy(config);
199
+ const proxyPort = await _sharedCdpProxy.start();
197
200
  console.error(`[CDP Interceptor] Proxy running on ws://localhost:${proxyPort}, forwarding to ${realEndpoint}`);
198
- return await chromium.connectOverCDP(`ws://localhost:${proxyPort}`);
201
+ const { browser: browser3 } = await launch({ cdpEndpoint: `ws://localhost:${proxyPort}` });
202
+ return browser3;
199
203
  }
200
- return await chromium.connectOverCDP(realEndpoint);
204
+ const { browser: browser2 } = await launch({ cdpEndpoint: realEndpoint });
205
+ await browser2.discoverContexts().catch((err) => {
206
+ console.error(`[browser] discoverContexts failed: ${err.message}`);
207
+ });
208
+ return browser2;
201
209
  }
202
210
  const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
203
- return await chromium.launch({ executablePath, headless: options?.headless ?? true });
211
+ const { browser } = await launch({ executablePath, headless: options?.headless ?? true });
212
+ return browser;
204
213
  }
205
214
  async function getBrowser(options) {
206
215
  if (_sharedBrowser) return _sharedBrowser;
@@ -210,10 +219,7 @@ async function getBrowser(options) {
210
219
  return _sharedBrowser;
211
220
  }
212
221
  function findSession(name) {
213
- for (const [, session] of sessions) {
214
- if (session.name === name) return session;
215
- }
216
- return void 0;
222
+ return sessions.find(name);
217
223
  }
218
224
  function getSessionById(id) {
219
225
  return sessions.get(id);
@@ -317,9 +323,14 @@ async function findOrRestoreSession(name, cdpEndpoint) {
317
323
  return void 0;
318
324
  }
319
325
  const targetUrl = meta.conversationUrl || meta.url;
320
- if (targetUrl && page.url() !== targetUrl && !page.url().includes(new URL(targetUrl).hostname)) {
321
- await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
322
- });
326
+ if (targetUrl && page.url() !== targetUrl) {
327
+ try {
328
+ if (!page.url().includes(new URL(targetUrl).hostname)) {
329
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
330
+ });
331
+ }
332
+ } catch {
333
+ }
323
334
  }
324
335
  const session = {
325
336
  id: meta.id || randomUUID(),
@@ -332,13 +343,13 @@ async function findOrRestoreSession(name, cdpEndpoint) {
332
343
  isCDP: true,
333
344
  cdpEndpoint: ep
334
345
  };
335
- for (const [existingId, existingSession] of sessions) {
346
+ for (const existingSession of sessions.list()) {
336
347
  if (existingSession.name === name) {
337
- logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingId}" during restore`);
338
- sessions.delete(existingId);
348
+ logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingSession.id}" during restore`);
349
+ sessions.removeById(existingSession.id);
339
350
  }
340
351
  }
341
- sessions.set(session.id, session);
352
+ sessions.set(session);
342
353
  resetIdleTimer();
343
354
  await installNetworkCapture(page, name);
344
355
  return session;
@@ -351,10 +362,15 @@ async function findOrRestoreSession(name, cdpEndpoint) {
351
362
  async function createEphemeralContext(options) {
352
363
  if (options?.cdpEndpoint) {
353
364
  const endpoint = await resolveCDPEndpoint(options.cdpEndpoint);
354
- const b2 = await chromium.connectOverCDP(endpoint);
365
+ const { browser: b2 } = await launch({ cdpEndpoint: endpoint });
355
366
  const contexts = b2.contexts();
356
367
  const ctx = contexts[0] || await b2.newContext();
357
- const page2 = await ctx.newPage();
368
+ const allPages = ctx.pages();
369
+ const existingPages = allPages.filter((p) => {
370
+ const url = p.url();
371
+ return url !== "about:blank" && !url.startsWith("chrome://");
372
+ });
373
+ const page2 = existingPages.length > 0 ? existingPages[0] : allPages.length > 0 ? allPages[0] : await ctx.newPage();
358
374
  resetIdleTimer();
359
375
  ephemeralConnections.set(page2, b2);
360
376
  return { context: ctx, page: page2 };
@@ -386,38 +402,59 @@ async function closeEphemeralContext(context) {
386
402
  }
387
403
  }
388
404
  function getAllSessions() {
389
- return Array.from(sessions.values());
405
+ return sessions.list();
390
406
  }
391
407
  async function installNetworkCapture(page, sessionName) {
392
408
  if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
393
- const { networkStore } = await import("./network-store-BN6QEZ7R.js");
394
- page.on("response", async (response) => {
409
+ const { networkStore } = await import("./network-store-YVDNUREI.js");
410
+ const requestData = /* @__PURE__ */ new Map();
411
+ const responseMeta = /* @__PURE__ */ new Map();
412
+ const xbPage = page;
413
+ xbPage.on("request", (params) => {
395
414
  try {
396
- const request = response.request();
397
- const url = response.url();
398
- const contentType = response.headers()["content-type"] || "";
399
- const headers = {};
400
- for (const [k, v] of Object.entries(response.headers())) {
401
- headers[k] = v;
402
- }
403
- const requestHeaders = {};
404
- for (const [k, v] of Object.entries(request.headers())) {
405
- requestHeaders[k] = v;
406
- }
415
+ const p = params;
416
+ requestData.set(p.requestId, {
417
+ method: p.request.method,
418
+ headers: p.request.headers,
419
+ postData: p.request.postData ?? null,
420
+ resourceType: p.type
421
+ });
422
+ } catch {
423
+ }
424
+ });
425
+ xbPage.on("response", (params) => {
426
+ try {
427
+ const p = params;
428
+ responseMeta.set(p.requestId, {
429
+ status: p.response.status,
430
+ url: p.response.url,
431
+ headers: p.response.headers,
432
+ mimeType: p.response.mimeType,
433
+ type: p.type
434
+ });
435
+ } catch {
436
+ }
437
+ });
438
+ xbPage.on("requestfinished", async (params) => {
439
+ try {
440
+ const p = params;
441
+ const meta = responseMeta.get(p.requestId);
442
+ if (!meta) return;
443
+ const req = requestData.get(p.requestId);
444
+ const method = req?.method ?? "GET";
445
+ const contentType = meta.headers["content-type"] || meta.headers["Content-Type"] || "";
446
+ const resourceType = req?.resourceType ?? meta.type;
447
+ const requestHeaders = req?.headers ?? {};
407
448
  let requestBody = void 0;
408
- const method = request.method();
409
449
  const isPostLike = ["POST", "PATCH", "PUT"].includes(method);
410
450
  if (isPostLike && requestHeaders["content-type"]?.includes("application/json")) {
411
- try {
412
- const postData = request.postData();
413
- if (postData) {
414
- try {
415
- requestBody = JSON.parse(postData);
416
- } catch {
417
- requestBody = postData;
418
- }
451
+ const postData = req?.postData;
452
+ if (postData) {
453
+ try {
454
+ requestBody = JSON.parse(postData);
455
+ } catch {
456
+ requestBody = postData;
419
457
  }
420
- } catch {
421
458
  }
422
459
  }
423
460
  let responseBody = void 0;
@@ -425,7 +462,11 @@ async function installNetworkCapture(page, sessionName) {
425
462
  const isJsonish = contentType.includes("json") || contentType.includes("javascript") || contentType.includes("text/");
426
463
  if (isJsonish) {
427
464
  try {
428
- const text = await response.text();
465
+ const bodyResult = await xbPage._cdpSend(
466
+ "Network.getResponseBody",
467
+ { requestId: p.requestId }
468
+ );
469
+ const text = bodyResult.body ?? "";
429
470
  size = text.length;
430
471
  if (size <= 10240) {
431
472
  try {
@@ -438,8 +479,11 @@ async function installNetworkCapture(page, sessionName) {
438
479
  }
439
480
  } else {
440
481
  try {
441
- const text = await response.text();
442
- size = text.length;
482
+ const bodyResult = await xbPage._cdpSend(
483
+ "Network.getResponseBody",
484
+ { requestId: p.requestId }
485
+ );
486
+ size = bodyResult.body?.length ?? 0;
443
487
  } catch {
444
488
  size = 0;
445
489
  }
@@ -447,17 +491,19 @@ async function installNetworkCapture(page, sessionName) {
447
491
  networkStore.add(sessionName, {
448
492
  timestamp: Date.now(),
449
493
  method,
450
- url,
451
- path: new URL(url).pathname,
452
- status: response.status(),
494
+ url: meta.url,
495
+ path: new URL(meta.url).pathname,
496
+ status: meta.status,
453
497
  contentType,
454
498
  size,
455
- headers,
499
+ headers: meta.headers,
456
500
  body: responseBody,
457
501
  requestHeaders,
458
502
  requestBody,
459
- resourceType: request.resourceType()
503
+ resourceType
460
504
  });
505
+ requestData.delete(p.requestId);
506
+ responseMeta.delete(p.requestId);
461
507
  } catch {
462
508
  }
463
509
  });
@@ -481,16 +527,38 @@ async function createSession(name, url, options) {
481
527
  }
482
528
  context = contexts[0] || await b.newContext();
483
529
  let targetPage = null;
484
- for (const ctx of contexts) {
485
- const pages = ctx.pages();
486
- for (const p of pages) {
487
- const pUrl = p.url();
488
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
489
- targetPage = p;
490
- break;
530
+ const targetHostname = url ? (() => {
531
+ try {
532
+ return new URL(url).hostname;
533
+ } catch {
534
+ return "";
535
+ }
536
+ })() : "";
537
+ if (targetHostname) {
538
+ for (const ctx of contexts) {
539
+ const pages = ctx.pages();
540
+ for (const p of pages) {
541
+ const pUrl = p.url();
542
+ if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && pUrl.includes(targetHostname)) {
543
+ targetPage = p;
544
+ break;
545
+ }
546
+ }
547
+ if (targetPage) break;
548
+ }
549
+ }
550
+ if (!targetPage) {
551
+ for (const ctx of contexts) {
552
+ const pages = ctx.pages();
553
+ for (const p of pages) {
554
+ const pUrl = p.url();
555
+ if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
556
+ targetPage = p;
557
+ break;
558
+ }
491
559
  }
560
+ if (targetPage) break;
492
561
  }
493
- if (targetPage) break;
494
562
  }
495
563
  if (!targetPage && options?.cdpEndpoint) {
496
564
  const targets = await getCDPTargets(options.cdpEndpoint);
@@ -531,14 +599,14 @@ async function createSession(name, url, options) {
531
599
  isCDP,
532
600
  cdpEndpoint: options?.cdpEndpoint
533
601
  };
534
- sessions.set(session.id, session);
602
+ sessions.set(session);
535
603
  logSessionEvent("create_session", `name="${name}" id="${session.id}" url="${url || "(no url)"}" isCDP=${isCDP} cdpEndpoint=${options?.cdpEndpoint || "(none)"}`);
536
604
  resetIdleTimer();
537
605
  await installNetworkCapture(page, name);
538
606
  return session;
539
607
  }
540
608
  async function closeSessionByName(name) {
541
- for (const [id, session] of sessions) {
609
+ for (const session of sessions) {
542
610
  if (session.name === name || session.id === name) {
543
611
  logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
544
612
  if (session.isCDP) {
@@ -557,20 +625,20 @@ async function closeSessionByName(name) {
557
625
  });
558
626
  }
559
627
  }
560
- sessions.delete(id);
628
+ sessions.removeById(session.id);
561
629
  const file2 = sessionFile(session.name);
562
630
  try {
563
631
  unlinkSync(file2);
564
632
  } catch {
565
633
  }
566
634
  try {
567
- const { networkStore, commandLogStore } = await import("./network-store-BN6QEZ7R.js");
635
+ const { networkStore, commandLogStore } = await import("./network-store-YVDNUREI.js");
568
636
  networkStore.clear(session.name);
569
637
  commandLogStore.clear(session.name);
570
638
  } catch {
571
639
  }
572
640
  try {
573
- const { SessionRecorder } = await import("./session-recorder-XET3DNML.js");
641
+ const { SessionRecorder } = await import("./session-recorder-RTDGURIJ.js");
574
642
  SessionRecorder.cleanup(session.name);
575
643
  } catch {
576
644
  }
@@ -585,9 +653,9 @@ async function closeSessionByName(name) {
585
653
  return false;
586
654
  }
587
655
  async function closeAllSessions() {
588
- const names = [...sessions.values()].map((s) => `${s.name}(${s.page.url()})`).join(", ");
656
+ const names = sessions.list().map((s) => `${s.name}(${s.page.url()})`).join(", ");
589
657
  if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
590
- for (const [id, session] of sessions) {
658
+ for (const session of sessions.list()) {
591
659
  try {
592
660
  if (!session.isCDP) {
593
661
  await session.context.close();
@@ -596,9 +664,9 @@ async function closeAllSessions() {
596
664
  await session.browser.close().catch(() => {
597
665
  });
598
666
  }
599
- sessions.delete(id);
667
+ sessions.removeById(session.id);
600
668
  } catch {
601
- sessions.delete(id);
669
+ sessions.removeById(session.id);
602
670
  }
603
671
  }
604
672
  }
@@ -636,7 +704,7 @@ async function ensureProcessCanExit() {
636
704
  clearTimeout(idleTimer);
637
705
  idleTimer = null;
638
706
  }
639
- for (const session of sessions.values()) {
707
+ for (const session of sessions.list()) {
640
708
  if (session.browser) {
641
709
  if (session.isCDP) {
642
710
  await session.browser.close().catch(() => {
@@ -0,0 +1,260 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-3RG5ZIWI.js";
4
+
5
+ // src/cdp-driver/launcher.ts
6
+ import { spawn } from "child_process";
7
+ import { existsSync as fsExistsSync } from "fs";
8
+ var DEFAULT_CHROME_PATHS = {
9
+ darwin: [
10
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
11
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
12
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
13
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
14
+ ],
15
+ linux: [
16
+ "/usr/bin/google-chrome",
17
+ "/usr/bin/google-chrome-stable",
18
+ "/usr/bin/chromium",
19
+ "/usr/bin/chromium-browser",
20
+ "/usr/bin/microsoft-edge"
21
+ ],
22
+ win32: [
23
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
24
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
25
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"
26
+ ]
27
+ };
28
+ var DEFAULT_ARGS = [
29
+ "--no-first-run",
30
+ "--no-default-browser-check",
31
+ "--no-sandbox",
32
+ "--disable-background-networking",
33
+ "--disable-background-timer-throttling",
34
+ "--disable-backgrounding-occluded-windows",
35
+ "--disable-breakpad",
36
+ "--disable-client-side-phishing-detection",
37
+ "--disable-default-apps",
38
+ "--disable-extensions",
39
+ "--disable-hang-monitor",
40
+ "--disable-ipc-flood-protection",
41
+ "--disable-popup-blocking",
42
+ "--disable-prompt-on-repost",
43
+ "--disable-renderer-backgrounding",
44
+ "--disable-sync",
45
+ "--disable-translate",
46
+ "--metrics-recording-only",
47
+ "--password-store=basic",
48
+ "--use-mock-keychain"
49
+ ];
50
+ var ANTI_DETECT_ARGS = [
51
+ "--disable-blink-features=AutomationControlled",
52
+ "--disable-features=IsolateOrigins,site-per-process"
53
+ ];
54
+ function findChrome() {
55
+ const platform = process.platform;
56
+ const paths = DEFAULT_CHROME_PATHS[platform] ?? [];
57
+ for (const p of paths) {
58
+ if (fsExistsSync(p)) return p;
59
+ }
60
+ return null;
61
+ }
62
+ async function launchChrome(options = {}) {
63
+ const {
64
+ executablePath,
65
+ headless = true,
66
+ args: extraArgs = [],
67
+ userDataDir,
68
+ timeout = 3e4,
69
+ env
70
+ } = options;
71
+ const chromePath = executablePath ?? findChrome();
72
+ if (!chromePath) {
73
+ throw new Error(
74
+ [
75
+ "Chrome/Chromium not found.",
76
+ "",
77
+ "\u63A8\u8350\uFF1A\u7528 cdp-tunnel \u590D\u7528\u4F60\u5DF2\u6709\u7684 Chrome\uFF08\u542B\u767B\u5F55\u6001\u3001\u53CD\u722C\u53CB\u597D\uFF09",
78
+ " npx cdp-tunnel setup # \u96F6\u5B89\u88C5\u4E00\u952E\u542F\u52A8\u4EE3\u7406 + \u52A0\u8F7D Chrome \u6269\u5C55",
79
+ " xbrowser goto https://example.com --cdp http://localhost:9221",
80
+ "",
81
+ "\u6216\u6307\u5B9A Chrome \u8DEF\u5F84\uFF1A",
82
+ ' xbrowser config set browser.executablePath "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"'
83
+ ].join("\n")
84
+ );
85
+ }
86
+ const port = await findFreePort();
87
+ const allArgs = [
88
+ `--remote-debugging-port=${port}`,
89
+ "--remote-allow-origins=*",
90
+ "--no-sandbox",
91
+ "--no-first-run",
92
+ "--no-default-browser-check",
93
+ "--disable-background-timer-throttling",
94
+ "--disable-backgrounding-occluded-windows",
95
+ "--disable-renderer-backgrounding",
96
+ "--disable-features=Translate",
97
+ "--disable-popup-blocking"
98
+ ];
99
+ if (headless) {
100
+ allArgs.push("--headless", "--hide-scrollbars", "--mute-audio");
101
+ }
102
+ let tmpDir;
103
+ if (userDataDir) {
104
+ allArgs.push(`--user-data-dir=${userDataDir}`);
105
+ } else {
106
+ const { mkdirSync } = await import("fs");
107
+ tmpDir = `/tmp/xbrowser-chrome-${process.pid}-${Date.now()}`;
108
+ mkdirSync(tmpDir, { recursive: true });
109
+ allArgs.push(`--user-data-dir=${tmpDir}`);
110
+ }
111
+ allArgs.push(...extraArgs, "about:blank");
112
+ const childEnv = {
113
+ ...process.env,
114
+ ...env
115
+ };
116
+ const quotedPath = chromePath.includes(" ") ? `"${chromePath}"` : chromePath;
117
+ const quotedArgs = allArgs.map((a) => {
118
+ if (a.includes(" ")) return `"${a}"`;
119
+ return a;
120
+ }).join(" ");
121
+ const fullCmd = `${quotedPath} ${quotedArgs}`;
122
+ const child = process.platform === "darwin" ? spawn("/bin/sh", ["-c", fullCmd], {
123
+ stdio: ["ignore", "pipe", "pipe"],
124
+ env: childEnv
125
+ }) : spawn(chromePath, allArgs, {
126
+ stdio: ["ignore", "pipe", "pipe"],
127
+ env: childEnv
128
+ });
129
+ const stderrLines = [];
130
+ child.stderr?.on("data", (data) => {
131
+ const line = data.toString().trim();
132
+ if (line) stderrLines.push(line);
133
+ });
134
+ child.stdout?.on("data", (data) => {
135
+ const line = data.toString().trim();
136
+ if (line) stderrLines.push(`[stdout] ${line}`);
137
+ });
138
+ child.on("error", (err) => {
139
+ if (!child.killed) {
140
+ console.error(`Chrome process error: ${err.message}`);
141
+ }
142
+ });
143
+ try {
144
+ const wsEndpoint = await waitForCDPReady(port, timeout, child);
145
+ return { process: child, wsEndpoint, port, tmpDir };
146
+ } catch (err) {
147
+ const stderr = stderrLines.slice(-20).join("\n");
148
+ const exitInfo = child.exitCode !== null ? ` (exit code: ${child.exitCode})` : " (still running)";
149
+ throw new Error(`${err.message}${exitInfo}
150
+ Chrome stderr:
151
+ ${stderr || "(empty)"}`);
152
+ }
153
+ }
154
+ async function connectToCDP(rawEndpoint) {
155
+ if (rawEndpoint.startsWith("ws://") || rawEndpoint.startsWith("wss://")) {
156
+ return rawEndpoint;
157
+ }
158
+ return resolveEndpointFromHTTP(rawEndpoint);
159
+ }
160
+ async function findFreePort() {
161
+ const { createServer } = await import("net");
162
+ return new Promise((resolve, reject) => {
163
+ const srv = createServer();
164
+ srv.listen(0, "127.0.0.1", () => {
165
+ const addr = srv.address();
166
+ if (addr && typeof addr === "object") {
167
+ const port = addr.port;
168
+ srv.close(() => resolve(port));
169
+ } else {
170
+ srv.close();
171
+ reject(new Error("Failed to find free port"));
172
+ }
173
+ });
174
+ srv.on("error", reject);
175
+ });
176
+ }
177
+ async function waitForCDPReady(port, timeoutMs, child) {
178
+ const deadline = Date.now() + timeoutMs;
179
+ while (Date.now() < deadline) {
180
+ if (child.exitCode !== null && child.exitCode !== 0) {
181
+ throw new Error(`Chrome exited with code ${child.exitCode} before CDP became ready`);
182
+ }
183
+ try {
184
+ const wsEndpoint = await resolveEndpointFromHTTP(`http://127.0.0.1:${port}`);
185
+ return wsEndpoint;
186
+ } catch {
187
+ await sleep(200);
188
+ }
189
+ }
190
+ throw new Error(`Chrome CDP not ready after ${timeoutMs}ms (port ${port})`);
191
+ }
192
+ async function resolveEndpointFromHTTP(baseURL) {
193
+ const url = `${baseURL}/json/version`;
194
+ const resp = await fetch(url, { signal: AbortSignal.timeout(3e3) });
195
+ if (!resp.ok) {
196
+ throw new Error(`CDP HTTP ${resp.status}: ${url}`);
197
+ }
198
+ const data = await resp.json();
199
+ if (!data.webSocketDebuggerUrl) {
200
+ throw new Error("No webSocketDebuggerUrl in CDP response");
201
+ }
202
+ return data.webSocketDebuggerUrl;
203
+ }
204
+ async function getCDPTargets(baseURL) {
205
+ const url = `${baseURL}/json/list`;
206
+ const resp = await fetch(url, { signal: AbortSignal.timeout(5e3) });
207
+ if (!resp.ok) {
208
+ throw new Error(`CDP list HTTP ${resp.status}: ${url}`);
209
+ }
210
+ const targets = await resp.json();
211
+ return targets;
212
+ }
213
+ async function killChrome(child, tmpDir) {
214
+ if (child.exitCode !== null) {
215
+ if (tmpDir) cleanupTmpDir(tmpDir);
216
+ return;
217
+ }
218
+ return new Promise((resolve) => {
219
+ const timer = setTimeout(() => {
220
+ try {
221
+ child.kill("SIGKILL");
222
+ } catch {
223
+ }
224
+ if (tmpDir) cleanupTmpDir(tmpDir);
225
+ resolve();
226
+ }, 5e3);
227
+ child.once("exit", () => {
228
+ clearTimeout(timer);
229
+ if (tmpDir) cleanupTmpDir(tmpDir);
230
+ resolve();
231
+ });
232
+ try {
233
+ child.kill("SIGTERM");
234
+ } catch {
235
+ clearTimeout(timer);
236
+ if (tmpDir) cleanupTmpDir(tmpDir);
237
+ resolve();
238
+ }
239
+ });
240
+ }
241
+ function cleanupTmpDir(dir) {
242
+ try {
243
+ const { rmSync } = __require("fs");
244
+ rmSync(dir, { recursive: true, force: true });
245
+ } catch {
246
+ }
247
+ }
248
+ function sleep(ms) {
249
+ return new Promise((resolve) => setTimeout(resolve, ms));
250
+ }
251
+
252
+ export {
253
+ DEFAULT_ARGS,
254
+ ANTI_DETECT_ARGS,
255
+ findChrome,
256
+ launchChrome,
257
+ connectToCDP,
258
+ getCDPTargets,
259
+ killChrome
260
+ };