@browserbridge/bbx 1.0.1 → 1.1.0

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 (63) hide show
  1. package/README.md +4 -4
  2. package/package.json +11 -13
  3. package/packages/agent-client/src/cli-helpers.js +33 -0
  4. package/packages/agent-client/src/cli.js +116 -41
  5. package/packages/agent-client/src/client.js +29 -4
  6. package/packages/agent-client/src/command-registry.js +3 -0
  7. package/packages/agent-client/src/detect.js +159 -48
  8. package/packages/agent-client/src/install.js +24 -1
  9. package/packages/agent-client/src/mcp-config.js +29 -10
  10. package/packages/agent-client/src/setup-status.js +12 -4
  11. package/packages/mcp-server/src/bin.js +57 -5
  12. package/packages/mcp-server/src/handlers.js +28 -7
  13. package/packages/mcp-server/src/server.js +12 -2
  14. package/packages/native-host/bin/bridge-daemon.js +33 -4
  15. package/packages/native-host/bin/install-manifest.js +24 -2
  16. package/packages/native-host/src/config.js +131 -6
  17. package/packages/native-host/src/daemon-process.js +396 -0
  18. package/packages/native-host/src/daemon.js +217 -68
  19. package/packages/native-host/src/framing.js +131 -11
  20. package/packages/native-host/src/install-manifest.js +121 -7
  21. package/packages/native-host/src/native-host.js +110 -73
  22. package/packages/protocol/src/capabilities.js +3 -0
  23. package/packages/protocol/src/defaults.js +1 -0
  24. package/packages/protocol/src/errors.js +4 -0
  25. package/packages/protocol/src/payload-cost.js +19 -6
  26. package/packages/protocol/src/protocol.js +143 -7
  27. package/packages/protocol/src/registry.js +11 -0
  28. package/packages/protocol/src/summary.js +18 -10
  29. package/packages/protocol/src/types.js +28 -3
  30. package/skills/browser-bridge/SKILL.md +2 -1
  31. package/skills/browser-bridge/references/interaction.md +1 -0
  32. package/skills/browser-bridge/references/protocol.md +2 -1
  33. package/CHANGELOG.md +0 -55
  34. package/assets/banner.jpg +0 -0
  35. package/assets/logo.png +0 -0
  36. package/assets/logo.svg +0 -65
  37. package/docs/api-reference.md +0 -157
  38. package/docs/cli-guide.md +0 -128
  39. package/docs/index.md +0 -25
  40. package/docs/manual-setup.md +0 -140
  41. package/docs/mcp-vs-cli.md +0 -258
  42. package/docs/publishing.md +0 -112
  43. package/docs/quickstart.md +0 -104
  44. package/docs/troubleshooting.md +0 -59
  45. package/docs/unpacked-extension.md +0 -72
  46. package/docs/usage-scenarios.md +0 -136
  47. package/manifest.json +0 -38
  48. package/packages/extension/assets/icon-128.png +0 -0
  49. package/packages/extension/assets/icon-16.png +0 -0
  50. package/packages/extension/assets/icon-32.png +0 -0
  51. package/packages/extension/assets/icon-48.png +0 -0
  52. package/packages/extension/src/background-helpers.js +0 -474
  53. package/packages/extension/src/background-routing.js +0 -89
  54. package/packages/extension/src/background.js +0 -3490
  55. package/packages/extension/src/content-script-helpers.js +0 -282
  56. package/packages/extension/src/content-script.js +0 -2043
  57. package/packages/extension/src/debugger-coordinator.js +0 -188
  58. package/packages/extension/src/sidepanel-helpers.js +0 -104
  59. package/packages/extension/ui/popup.html +0 -35
  60. package/packages/extension/ui/popup.js +0 -298
  61. package/packages/extension/ui/sidepanel.html +0 -102
  62. package/packages/extension/ui/sidepanel.js +0 -1771
  63. package/packages/extension/ui/ui.css +0 -1160
@@ -1,12 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
  // @ts-check
3
3
  import { BridgeDaemon } from '../src/daemon.js';
4
- import { getSocketPath } from '../src/config.js';
4
+ import {
5
+ applyWindowsTcpTransportDefaults,
6
+ formatBridgeTransport,
7
+ getBridgeTransport,
8
+ } from '../src/config.js';
9
+ import { clearDaemonPidFile, writeDaemonPidFile } from '../src/daemon-process.js';
5
10
 
6
- const daemon = new BridgeDaemon({ socketPath: getSocketPath() });
7
- await daemon.start();
11
+ applyWindowsTcpTransportDefaults();
12
+ const transport = getBridgeTransport();
13
+ const daemon = new BridgeDaemon({ transport });
8
14
 
9
- process.stdout.write(`Browser Bridge daemon listening on ${getSocketPath()}\n`);
15
+ /**
16
+ * @param {unknown} error
17
+ * @returns {boolean}
18
+ */
19
+ function isExistingDaemonError(error) {
20
+ return (
21
+ error instanceof Error && error.message.startsWith('Another daemon is already running on ')
22
+ );
23
+ }
24
+
25
+ try {
26
+ await daemon.start();
27
+ await writeDaemonPidFile(process.pid);
28
+ } catch (error) {
29
+ if (isExistingDaemonError(error)) {
30
+ process.stdout.write(`${error.message}\n`);
31
+ process.exit(0);
32
+ }
33
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
34
+ process.exit(1);
35
+ }
36
+
37
+ process.stdout.write(`Browser Bridge daemon listening on ${formatBridgeTransport(transport)}\n`);
10
38
 
11
39
  let shuttingDown = false;
12
40
 
@@ -21,6 +49,7 @@ async function shutdown() {
21
49
  shuttingDown = true;
22
50
  try {
23
51
  await daemon.stop();
52
+ await clearDaemonPidFile({ pid: process.pid });
24
53
  process.exit(0);
25
54
  } catch (error) {
26
55
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
@@ -3,22 +3,31 @@
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
 
6
- import { installNativeManifest, parseExtensionId } from '../src/install-manifest.js';
6
+ import {
7
+ installNativeManifest,
8
+ parseExtensionId,
9
+ uninstallNativeManifest,
10
+ } from '../src/install-manifest.js';
7
11
  import { SUPPORTED_BROWSERS } from '../src/config.js';
8
12
 
9
13
  /** @typedef {import('../src/config.js').SupportedBrowser} SupportedBrowser */
10
14
 
15
+ const USAGE = 'Usage: bbx-install [<extension-id>] [--browser <browser>] [--all] [--uninstall]\n';
16
+
11
17
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
18
  const repoRoot = path.resolve(__dirname, '../../..');
13
19
 
14
20
  const args = process.argv.slice(2);
15
21
  let extensionIdArg = /** @type {string | undefined} */ (undefined);
16
22
  let installAll = false;
23
+ let uninstall = false;
17
24
  const browsers = /** @type {SupportedBrowser[]} */ ([]);
18
25
 
19
26
  for (let i = 0; i < args.length; i++) {
20
27
  if (args[i] === '--all') {
21
28
  installAll = true;
29
+ } else if (args[i] === '--uninstall') {
30
+ uninstall = true;
22
31
  } else if (args[i] === '--browser' && args[i + 1]) {
23
32
  const candidate = args[i + 1];
24
33
  if (!SUPPORTED_BROWSERS.includes(/** @type {SupportedBrowser} */ (candidate))) {
@@ -34,6 +43,11 @@ for (let i = 0; i < args.length; i++) {
34
43
  }
35
44
  }
36
45
 
46
+ if (uninstall && extensionIdArg) {
47
+ process.stderr.write(USAGE);
48
+ process.exit(1);
49
+ }
50
+
37
51
  if (extensionIdArg && !parseExtensionId(extensionIdArg)) {
38
52
  process.stderr.write(
39
53
  `Invalid extension ID: ${extensionIdArg}\n` +
@@ -48,7 +62,15 @@ const targets = installAll
48
62
  ? browsers
49
63
  : [/** @type {SupportedBrowser} */ ('chrome')];
50
64
 
51
- for (const target of targets) {
65
+ for (const [index, target] of targets.entries()) {
66
+ if (uninstall) {
67
+ await uninstallNativeManifest({
68
+ browser: target,
69
+ removeBridgeDir: index === targets.length - 1,
70
+ });
71
+ continue;
72
+ }
73
+
52
74
  await installNativeManifest({
53
75
  repoRoot,
54
76
  extensionIdArg,
@@ -5,6 +5,10 @@ import path from 'node:path';
5
5
 
6
6
  export const APP_NAME = 'com.browserbridge.browser_bridge';
7
7
  export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
8
+ export const BRIDGE_TCP_PORT_ENV = 'BBX_TCP_PORT';
9
+ export const DEFAULT_WINDOWS_TCP_PORT = 9223;
10
+
11
+ /** @typedef {{ type: 'socket', socketPath: string, label: string } | { type: 'tcp', host: string, port: number, label: string }} BridgeTransport */
8
12
 
9
13
  /**
10
14
  * The official Chrome Web Store extension ID used when callers do not provide
@@ -15,10 +19,11 @@ export const BRIDGE_HOME_ENV = 'BROWSER_BRIDGE_HOME';
15
19
  export const PUBLISHED_EXTENSION_ID = 'jjjkmmcdkpcgamlopogicbnnhdgebhie';
16
20
 
17
21
  /**
22
+ * @param {NodeJS.ProcessEnv} [env=process.env]
18
23
  * @returns {string}
19
24
  */
20
- export function getBridgeDir() {
21
- const override = process.env[BRIDGE_HOME_ENV];
25
+ export function getBridgeDir(env = process.env) {
26
+ const override = env[BRIDGE_HOME_ENV];
22
27
  if (override) {
23
28
  return override;
24
29
  }
@@ -31,19 +36,139 @@ export function getBridgeDir() {
31
36
  }
32
37
 
33
38
  if (platform === 'win32') {
34
- const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
39
+ const localAppData = env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
35
40
  return path.join(localAppData, 'Browser Bridge');
36
41
  }
37
42
 
38
- const xdgDataHome = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share');
43
+ const xdgDataHome = env.XDG_DATA_HOME || path.join(home, '.local', 'share');
39
44
  return path.join(xdgDataHome, 'browser-bridge');
40
45
  }
41
46
 
42
47
  /**
48
+ * Resolve the IPC endpoint the daemon listens on and the CLI / native host
49
+ * connect to. On Windows we use a Named Pipe by default because Node's AF_UNIX
50
+ * support fails with EACCES on listen() under recent Node + Windows 11
51
+ * combinations, while Named Pipes are the historical and reliable Windows IPC
52
+ * mechanism. An explicit bridge-home override keeps the legacy socket path so
53
+ * callers can opt into custom test or compatibility setups.
54
+ *
55
+ * @param {NodeJS.ProcessEnv} [env=process.env]
56
+ * @returns {string}
57
+ */
58
+ export function getSocketPath(env = process.env) {
59
+ if (os.platform() === 'win32' && !env[BRIDGE_HOME_ENV]) {
60
+ return `\\\\.\\pipe\\${APP_NAME}`;
61
+ }
62
+ return path.join(getBridgeDir(env), 'bridge.sock');
63
+ }
64
+
65
+ /**
66
+ * @param {string} socketPath
67
+ * @returns {BridgeTransport}
68
+ */
69
+ export function createSocketBridgeTransport(socketPath) {
70
+ return {
71
+ type: 'socket',
72
+ socketPath,
73
+ label: socketPath,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * @param {number} port
79
+ * @param {string} [host='127.0.0.1']
80
+ * @returns {BridgeTransport}
81
+ */
82
+ export function createTcpBridgeTransport(port, host = '127.0.0.1') {
83
+ return {
84
+ type: 'tcp',
85
+ host,
86
+ port,
87
+ label: `${host}:${port}`,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * @param {NodeJS.ProcessEnv} [env=process.env]
93
+ * @returns {number | null}
94
+ */
95
+ export function getBridgeTcpPort(env = process.env) {
96
+ const raw = env[BRIDGE_TCP_PORT_ENV];
97
+ if (raw == null || raw === '') {
98
+ return null;
99
+ }
100
+
101
+ const port = Number.parseInt(raw, 10);
102
+ if (!Number.isInteger(port) || String(port) !== String(raw).trim() || port < 1 || port > 65535) {
103
+ throw new Error(
104
+ `${BRIDGE_TCP_PORT_ENV} must be an integer between 1 and 65535 (got ${JSON.stringify(raw)}).`
105
+ );
106
+ }
107
+
108
+ return port;
109
+ }
110
+
111
+ /**
112
+ * Align Windows CLI/daemon entrypoints with the installed native-host launcher,
113
+ * which uses TCP by default. Preserve explicit overrides and custom bridge-home
114
+ * test setups that rely on the socket transport.
115
+ *
116
+ * @param {NodeJS.ProcessEnv} [env=process.env]
117
+ * @returns {boolean}
118
+ */
119
+ export function applyWindowsTcpTransportDefaults(env = process.env) {
120
+ if (os.platform() !== 'win32') {
121
+ return false;
122
+ }
123
+ if (env[BRIDGE_TCP_PORT_ENV] != null && env[BRIDGE_TCP_PORT_ENV] !== '') {
124
+ return false;
125
+ }
126
+ if (env[BRIDGE_HOME_ENV]) {
127
+ return false;
128
+ }
129
+
130
+ env[BRIDGE_TCP_PORT_ENV] = String(DEFAULT_WINDOWS_TCP_PORT);
131
+ return true;
132
+ }
133
+
134
+ /**
135
+ * @param {NodeJS.ProcessEnv} [env=process.env]
136
+ * @returns {BridgeTransport}
137
+ */
138
+ export function getBridgeTransport(env = process.env) {
139
+ const tcpPort = getBridgeTcpPort(env);
140
+ if (tcpPort !== null) {
141
+ return createTcpBridgeTransport(tcpPort);
142
+ }
143
+
144
+ return createSocketBridgeTransport(getSocketPath(env));
145
+ }
146
+
147
+ /**
148
+ * @param {BridgeTransport} [transport=getBridgeTransport()]
149
+ * @returns {import('node:net').ListenOptions | string}
150
+ */
151
+ export function getBridgeListenTarget(transport = getBridgeTransport()) {
152
+ if (transport.type === 'tcp') {
153
+ return { host: transport.host, port: transport.port };
154
+ }
155
+ return transport.socketPath;
156
+ }
157
+
158
+ /**
159
+ * @param {BridgeTransport} [transport=getBridgeTransport()]
160
+ * @returns {string}
161
+ */
162
+ export function formatBridgeTransport(transport = getBridgeTransport()) {
163
+ return transport.label;
164
+ }
165
+
166
+ /**
167
+ * @param {NodeJS.ProcessEnv} [env=process.env]
43
168
  * @returns {string}
44
169
  */
45
- export function getSocketPath() {
46
- return path.join(getBridgeDir(), 'bridge.sock');
170
+ export function getDaemonPidPath(env = process.env) {
171
+ return path.join(getBridgeDir(env), 'daemon.pid');
47
172
  }
48
173
 
49
174
  /**
@@ -0,0 +1,396 @@
1
+ // @ts-check
2
+
3
+ import { execFile, spawn } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { promisify } from 'node:util';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ import { pingExistingDaemon } from './daemon.js';
10
+ import {
11
+ createSocketBridgeTransport,
12
+ formatBridgeTransport,
13
+ getBridgeDir,
14
+ getBridgeTransport,
15
+ getDaemonPidPath,
16
+ } from './config.js';
17
+
18
+ /** @typedef {import('./config.js').BridgeTransport} BridgeTransport */
19
+
20
+ const execFileAsync = promisify(execFile);
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ const daemonEntryPath = path.resolve(__dirname, '../bin/bridge-daemon.js');
23
+ const DEFAULT_DAEMON_RESTART_TIMEOUT_MS = 5_000;
24
+ const DEFAULT_DAEMON_POLL_INTERVAL_MS = 100;
25
+
26
+ /**
27
+ * @typedef {{
28
+ * transport?: BridgeTransport,
29
+ * socketPath?: string,
30
+ * pidPath?: string,
31
+ * timeoutMs?: number,
32
+ * pollIntervalMs?: number,
33
+ * pingDaemonFn?: (transport: BridgeTransport) => Promise<boolean>,
34
+ * readPidFn?: (pidPath?: string) => Promise<number | null>,
35
+ * findPidByTransportFn?: (transport: BridgeTransport) => Promise<number | null>,
36
+ * killFn?: typeof process.kill,
37
+ * rmFn?: typeof fs.promises.rm,
38
+ * sleepFn?: (ms: number) => Promise<void>,
39
+ * }} StopBridgeDaemonOptions
40
+ */
41
+
42
+ /**
43
+ * @typedef {{
44
+ * transport?: BridgeTransport,
45
+ * socketPath?: string,
46
+ * pidPath?: string,
47
+ * timeoutMs?: number,
48
+ * pollIntervalMs?: number,
49
+ * pingDaemonFn?: (transport: BridgeTransport) => Promise<boolean>,
50
+ * readPidFn?: (pidPath?: string) => Promise<number | null>,
51
+ * findPidByTransportFn?: (transport: BridgeTransport) => Promise<number | null>,
52
+ * killFn?: typeof process.kill,
53
+ * rmFn?: typeof fs.promises.rm,
54
+ * sleepFn?: (ms: number) => Promise<void>,
55
+ * spawnDaemonFn?: typeof spawnBridgeDaemonProcess,
56
+ * }} RestartBridgeDaemonOptions
57
+ */
58
+
59
+ /**
60
+ * @returns {import('node:child_process').ChildProcess}
61
+ */
62
+ export function spawnBridgeDaemonProcess() {
63
+ const child = spawn(process.execPath, [daemonEntryPath], {
64
+ detached: true,
65
+ stdio: 'ignore',
66
+ });
67
+ child.unref();
68
+ return child;
69
+ }
70
+
71
+ /**
72
+ * @param {number} [pid=process.pid]
73
+ * @param {string} [pidPath=getDaemonPidPath()]
74
+ * @returns {Promise<void>}
75
+ */
76
+ export async function writeDaemonPidFile(pid = process.pid, pidPath = getDaemonPidPath()) {
77
+ await fs.promises.mkdir(getBridgeDir(), { recursive: true });
78
+ await fs.promises.writeFile(pidPath, `${pid}\n`, 'utf8');
79
+ }
80
+
81
+ /**
82
+ * @param {string} [pidPath=getDaemonPidPath()]
83
+ * @returns {Promise<number | null>}
84
+ */
85
+ export async function readDaemonPidFile(pidPath = getDaemonPidPath()) {
86
+ try {
87
+ const raw = await fs.promises.readFile(pidPath, 'utf8');
88
+ const pid = Number.parseInt(raw.trim(), 10);
89
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
90
+ } catch (error) {
91
+ if (isMissingFileError(error)) {
92
+ return null;
93
+ }
94
+ throw error;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * @param {{ pid?: number | null, pidPath?: string, rmFn?: typeof fs.promises.rm }} [options={}]
100
+ * @returns {Promise<void>}
101
+ */
102
+ export async function clearDaemonPidFile(options = {}) {
103
+ const { pid = null, pidPath = getDaemonPidPath(), rmFn = fs.promises.rm } = options;
104
+
105
+ if (pid !== null) {
106
+ const currentPid = await readDaemonPidFile(pidPath);
107
+ if (currentPid !== pid) {
108
+ return;
109
+ }
110
+ }
111
+
112
+ try {
113
+ await rmFn(pidPath, { force: true });
114
+ } catch (error) {
115
+ if (isMissingFileError(error)) {
116
+ return;
117
+ }
118
+ throw error;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * @param {StopBridgeDaemonOptions} [options={}]
124
+ * @returns {Promise<{ transport: string, socketPath: string, previouslyRunning: boolean, previousPid: number | null, removedStaleSocket: boolean }>}
125
+ */
126
+ export async function stopBridgeDaemon(options = {}) {
127
+ const {
128
+ transport = getBridgeTransport(),
129
+ socketPath = undefined,
130
+ pidPath = getDaemonPidPath(),
131
+ timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
132
+ pollIntervalMs = DEFAULT_DAEMON_POLL_INTERVAL_MS,
133
+ pingDaemonFn = pingExistingDaemon,
134
+ readPidFn = readDaemonPidFile,
135
+ findPidByTransportFn = findDaemonPidByTransport,
136
+ killFn = process.kill.bind(process),
137
+ rmFn = fs.promises.rm,
138
+ sleepFn = sleep,
139
+ } = options;
140
+ const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
141
+ const resolvedSocketPath =
142
+ resolvedTransport.type === 'socket' ? resolvedTransport.socketPath : '';
143
+
144
+ let previousPid = await readPidFn(pidPath);
145
+ let previouslyRunning = previousPid !== null;
146
+
147
+ if (previousPid === null && (await safePingDaemon(resolvedTransport, pingDaemonFn))) {
148
+ previousPid = await findPidByTransportFn(resolvedTransport);
149
+ previouslyRunning = true;
150
+ }
151
+
152
+ if (previousPid !== null) {
153
+ try {
154
+ killFn(previousPid, 'SIGTERM');
155
+ } catch (error) {
156
+ if (!isMissingProcessError(error)) {
157
+ throw error;
158
+ }
159
+ }
160
+
161
+ const stopped = await waitForDaemonReachability({
162
+ transport: resolvedTransport,
163
+ reachable: false,
164
+ timeoutMs,
165
+ pollIntervalMs,
166
+ pingDaemonFn,
167
+ sleepFn,
168
+ });
169
+ if (!stopped) {
170
+ throw new Error(`Timed out waiting for Browser Bridge daemon (pid ${previousPid}) to stop.`);
171
+ }
172
+ }
173
+
174
+ await clearDaemonPidFile({ pid: previousPid, pidPath, rmFn });
175
+
176
+ const removedStaleSocket = await removeStaleSocket(resolvedTransport, rmFn, pingDaemonFn);
177
+ return {
178
+ transport: formatBridgeTransport(resolvedTransport),
179
+ socketPath: resolvedSocketPath,
180
+ previouslyRunning,
181
+ previousPid,
182
+ removedStaleSocket,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * @param {RestartBridgeDaemonOptions} [options={}]
188
+ * @returns {Promise<{
189
+ * transport: string,
190
+ * socketPath: string,
191
+ * pidPath: string,
192
+ * pid: number | null,
193
+ * previouslyRunning: boolean,
194
+ * previousPid: number | null,
195
+ * removedStaleSocket: boolean,
196
+ * }>}
197
+ */
198
+ export async function restartBridgeDaemon(options = {}) {
199
+ const {
200
+ transport = getBridgeTransport(),
201
+ socketPath = undefined,
202
+ pidPath = getDaemonPidPath(),
203
+ timeoutMs = DEFAULT_DAEMON_RESTART_TIMEOUT_MS,
204
+ pollIntervalMs = DEFAULT_DAEMON_POLL_INTERVAL_MS,
205
+ pingDaemonFn = pingExistingDaemon,
206
+ readPidFn = readDaemonPidFile,
207
+ findPidByTransportFn = findDaemonPidByTransport,
208
+ killFn = process.kill.bind(process),
209
+ rmFn = fs.promises.rm,
210
+ sleepFn = sleep,
211
+ spawnDaemonFn = spawnBridgeDaemonProcess,
212
+ } = options;
213
+ const resolvedTransport = socketPath ? createSocketBridgeTransport(socketPath) : transport;
214
+
215
+ const stopResult = await stopBridgeDaemon({
216
+ transport: resolvedTransport,
217
+ pidPath,
218
+ timeoutMs,
219
+ pollIntervalMs,
220
+ pingDaemonFn,
221
+ readPidFn,
222
+ findPidByTransportFn,
223
+ killFn,
224
+ rmFn,
225
+ sleepFn,
226
+ });
227
+
228
+ spawnDaemonFn();
229
+
230
+ const started = await waitForDaemonReachability({
231
+ transport: resolvedTransport,
232
+ reachable: true,
233
+ timeoutMs,
234
+ pollIntervalMs,
235
+ pingDaemonFn,
236
+ sleepFn,
237
+ });
238
+ if (!started) {
239
+ throw new Error('Timed out waiting for Browser Bridge daemon to start.');
240
+ }
241
+
242
+ return {
243
+ ...stopResult,
244
+ pidPath,
245
+ pid: await readPidFn(pidPath),
246
+ };
247
+ }
248
+
249
+ /**
250
+ * @param {BridgeTransport} transport
251
+ * @returns {Promise<number | null>}
252
+ */
253
+ export async function findDaemonPidByTransport(transport) {
254
+ if (transport.type !== 'socket') {
255
+ return null;
256
+ }
257
+ return findDaemonPidBySocket(transport.socketPath);
258
+ }
259
+
260
+ /**
261
+ * @param {string} socketPath
262
+ * @returns {Promise<number | null>}
263
+ */
264
+ export async function findDaemonPidBySocket(socketPath) {
265
+ if (process.platform === 'win32') {
266
+ return null;
267
+ }
268
+
269
+ try {
270
+ const { stdout } = await execFileAsync('lsof', ['-t', '--', socketPath]);
271
+ const pid = Number.parseInt(
272
+ stdout
273
+ .split(/\r?\n/u)
274
+ .map((line) => line.trim())
275
+ .find(Boolean) ?? '',
276
+ 10
277
+ );
278
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
279
+ } catch (error) {
280
+ if (isCommandNotFoundError(error) || isLsofNoResultsError(error)) {
281
+ return null;
282
+ }
283
+ throw error;
284
+ }
285
+ }
286
+
287
+ /**
288
+ * @param {{
289
+ * transport: BridgeTransport,
290
+ * reachable: boolean,
291
+ * timeoutMs: number,
292
+ * pollIntervalMs: number,
293
+ * pingDaemonFn: (transport: BridgeTransport) => Promise<boolean>,
294
+ * sleepFn: (ms: number) => Promise<void>,
295
+ * }} options
296
+ * @returns {Promise<boolean>}
297
+ */
298
+ async function waitForDaemonReachability(options) {
299
+ const { transport, reachable, timeoutMs, pollIntervalMs, pingDaemonFn, sleepFn } = options;
300
+ const deadline = Date.now() + timeoutMs;
301
+ while (Date.now() <= deadline) {
302
+ if ((await safePingDaemon(transport, pingDaemonFn)) === reachable) {
303
+ return true;
304
+ }
305
+ await sleepFn(pollIntervalMs);
306
+ }
307
+ return false;
308
+ }
309
+
310
+ /**
311
+ * @param {BridgeTransport} transport
312
+ * @param {typeof fs.promises.rm} rmFn
313
+ * @param {(transport: BridgeTransport) => Promise<boolean>} pingDaemonFn
314
+ * @returns {Promise<boolean>}
315
+ */
316
+ async function removeStaleSocket(transport, rmFn, pingDaemonFn) {
317
+ if (transport.type !== 'socket') {
318
+ return false;
319
+ }
320
+
321
+ if (await safePingDaemon(transport, pingDaemonFn)) {
322
+ return false;
323
+ }
324
+
325
+ try {
326
+ await fs.promises.access(transport.socketPath);
327
+ } catch (error) {
328
+ if (isMissingFileError(error)) {
329
+ return false;
330
+ }
331
+ throw error;
332
+ }
333
+
334
+ try {
335
+ await rmFn(transport.socketPath, { force: true });
336
+ return true;
337
+ } catch (error) {
338
+ if (isMissingFileError(error)) {
339
+ return false;
340
+ }
341
+ throw error;
342
+ }
343
+ }
344
+
345
+ /**
346
+ * @param {BridgeTransport} transport
347
+ * @param {(transport: BridgeTransport) => Promise<boolean>} pingDaemonFn
348
+ * @returns {Promise<boolean>}
349
+ */
350
+ async function safePingDaemon(transport, pingDaemonFn) {
351
+ try {
352
+ return await pingDaemonFn(transport);
353
+ } catch {
354
+ return false;
355
+ }
356
+ }
357
+
358
+ /**
359
+ * @param {number} ms
360
+ * @returns {Promise<void>}
361
+ */
362
+ function sleep(ms) {
363
+ return new Promise((resolve) => setTimeout(resolve, ms));
364
+ }
365
+
366
+ /**
367
+ * @param {unknown} error
368
+ * @returns {boolean}
369
+ */
370
+ function isMissingFileError(error) {
371
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
372
+ }
373
+
374
+ /**
375
+ * @param {unknown} error
376
+ * @returns {boolean}
377
+ */
378
+ function isMissingProcessError(error) {
379
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ESRCH');
380
+ }
381
+
382
+ /**
383
+ * @param {unknown} error
384
+ * @returns {boolean}
385
+ */
386
+ function isCommandNotFoundError(error) {
387
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
388
+ }
389
+
390
+ /**
391
+ * @param {unknown} error
392
+ * @returns {boolean}
393
+ */
394
+ function isLsofNoResultsError(error) {
395
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 1);
396
+ }