@flrande/browserctl 0.5.0-dev.22.1 → 0.6.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 (136) hide show
  1. package/dist/client.d.ts +34 -0
  2. package/dist/client.js +138 -0
  3. package/dist/commandRegistry.d.ts +16 -0
  4. package/dist/commandRegistry.js +21 -0
  5. package/dist/help.d.ts +4 -0
  6. package/dist/help.js +24 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.js +23 -0
  9. package/dist/runCli.d.ts +5 -0
  10. package/dist/runCli.js +170 -0
  11. package/package.json +32 -59
  12. package/INSTALL-CN.md +0 -92
  13. package/INSTALL.md +0 -92
  14. package/LICENSE +0 -21
  15. package/README-CN.md +0 -69
  16. package/README.md +0 -69
  17. package/apps/browserctl/src/commands/a11y-snapshot.ts +0 -20
  18. package/apps/browserctl/src/commands/act.test.ts +0 -71
  19. package/apps/browserctl/src/commands/act.ts +0 -64
  20. package/apps/browserctl/src/commands/command-wrappers.test.ts +0 -688
  21. package/apps/browserctl/src/commands/common.test.ts +0 -87
  22. package/apps/browserctl/src/commands/common.ts +0 -191
  23. package/apps/browserctl/src/commands/console-list.test.ts +0 -102
  24. package/apps/browserctl/src/commands/console-list.ts +0 -108
  25. package/apps/browserctl/src/commands/cookie-clear.ts +0 -18
  26. package/apps/browserctl/src/commands/cookie-get.ts +0 -18
  27. package/apps/browserctl/src/commands/cookie-set.ts +0 -22
  28. package/apps/browserctl/src/commands/dialog-arm.ts +0 -20
  29. package/apps/browserctl/src/commands/dom-query-all.ts +0 -18
  30. package/apps/browserctl/src/commands/dom-query.ts +0 -18
  31. package/apps/browserctl/src/commands/download-trigger.ts +0 -22
  32. package/apps/browserctl/src/commands/download-wait.test.ts +0 -67
  33. package/apps/browserctl/src/commands/download-wait.ts +0 -27
  34. package/apps/browserctl/src/commands/element-screenshot.ts +0 -20
  35. package/apps/browserctl/src/commands/frame-list.ts +0 -16
  36. package/apps/browserctl/src/commands/frame-snapshot.ts +0 -18
  37. package/apps/browserctl/src/commands/har-export.test.ts +0 -112
  38. package/apps/browserctl/src/commands/har-export.ts +0 -120
  39. package/apps/browserctl/src/commands/memory-delete.ts +0 -20
  40. package/apps/browserctl/src/commands/memory-inspect.ts +0 -20
  41. package/apps/browserctl/src/commands/memory-list.ts +0 -90
  42. package/apps/browserctl/src/commands/memory-mode-set.ts +0 -29
  43. package/apps/browserctl/src/commands/memory-purge.ts +0 -16
  44. package/apps/browserctl/src/commands/memory-resolve.ts +0 -56
  45. package/apps/browserctl/src/commands/memory-status.ts +0 -16
  46. package/apps/browserctl/src/commands/memory-ttl-set.ts +0 -28
  47. package/apps/browserctl/src/commands/memory-upsert.ts +0 -142
  48. package/apps/browserctl/src/commands/network-list.test.ts +0 -110
  49. package/apps/browserctl/src/commands/network-list.ts +0 -112
  50. package/apps/browserctl/src/commands/network-wait-for.test.ts +0 -90
  51. package/apps/browserctl/src/commands/network-wait-for.ts +0 -100
  52. package/apps/browserctl/src/commands/profile-list.ts +0 -16
  53. package/apps/browserctl/src/commands/profile-use.ts +0 -18
  54. package/apps/browserctl/src/commands/response-body.ts +0 -24
  55. package/apps/browserctl/src/commands/screenshot.ts +0 -16
  56. package/apps/browserctl/src/commands/session-drop.test.ts +0 -36
  57. package/apps/browserctl/src/commands/session-drop.ts +0 -16
  58. package/apps/browserctl/src/commands/session-list.test.ts +0 -81
  59. package/apps/browserctl/src/commands/session-list.ts +0 -70
  60. package/apps/browserctl/src/commands/snapshot.ts +0 -16
  61. package/apps/browserctl/src/commands/status.ts +0 -10
  62. package/apps/browserctl/src/commands/storage-get.ts +0 -20
  63. package/apps/browserctl/src/commands/storage-set.ts +0 -22
  64. package/apps/browserctl/src/commands/tab-close.ts +0 -20
  65. package/apps/browserctl/src/commands/tab-focus.ts +0 -20
  66. package/apps/browserctl/src/commands/tab-open.ts +0 -19
  67. package/apps/browserctl/src/commands/tabs.ts +0 -13
  68. package/apps/browserctl/src/commands/trace-get.test.ts +0 -61
  69. package/apps/browserctl/src/commands/trace-get.ts +0 -62
  70. package/apps/browserctl/src/commands/upload-arm.ts +0 -26
  71. package/apps/browserctl/src/commands/wait-element.test.ts +0 -80
  72. package/apps/browserctl/src/commands/wait-element.ts +0 -76
  73. package/apps/browserctl/src/commands/wait-text.test.ts +0 -110
  74. package/apps/browserctl/src/commands/wait-text.ts +0 -93
  75. package/apps/browserctl/src/commands/wait-url.test.ts +0 -80
  76. package/apps/browserctl/src/commands/wait-url.ts +0 -76
  77. package/apps/browserctl/src/daemon-client.test.ts +0 -512
  78. package/apps/browserctl/src/daemon-client.ts +0 -632
  79. package/apps/browserctl/src/e2e.test.ts +0 -103
  80. package/apps/browserctl/src/main.dispatch.test.ts +0 -461
  81. package/apps/browserctl/src/main.test.ts +0 -334
  82. package/apps/browserctl/src/main.ts +0 -957
  83. package/apps/browserctl/src/smoke.e2e.test.ts +0 -97
  84. package/apps/browserctl/src/test-port.ts +0 -26
  85. package/apps/browserd/src/bootstrap.ts +0 -432
  86. package/apps/browserd/src/chrome-relay-extension-bridge.test.ts +0 -250
  87. package/apps/browserd/src/chrome-relay-extension-bridge.ts +0 -506
  88. package/apps/browserd/src/container.ts +0 -3088
  89. package/apps/browserd/src/main.test.ts +0 -1522
  90. package/apps/browserd/src/main.ts +0 -7
  91. package/apps/browserd/src/test-port.ts +0 -26
  92. package/apps/browserd/src/tool-matrix.test.ts +0 -887
  93. package/bin/browserctl.cjs +0 -21
  94. package/bin/browserd.cjs +0 -21
  95. package/extensions/chrome-relay/README-CN.md +0 -39
  96. package/extensions/chrome-relay/README.md +0 -39
  97. package/extensions/chrome-relay/background.js +0 -1687
  98. package/extensions/chrome-relay/manifest.json +0 -15
  99. package/extensions/chrome-relay/popup.html +0 -369
  100. package/extensions/chrome-relay/popup.js +0 -972
  101. package/packages/core/src/bootstrap.test.ts +0 -10
  102. package/packages/core/src/driver-registry.test.ts +0 -45
  103. package/packages/core/src/driver-registry.ts +0 -22
  104. package/packages/core/src/driver.ts +0 -47
  105. package/packages/core/src/index.ts +0 -6
  106. package/packages/core/src/navigation-memory.test.ts +0 -259
  107. package/packages/core/src/navigation-memory.ts +0 -360
  108. package/packages/core/src/ref-cache.test.ts +0 -61
  109. package/packages/core/src/ref-cache.ts +0 -28
  110. package/packages/core/src/session-store.test.ts +0 -82
  111. package/packages/core/src/session-store.ts +0 -138
  112. package/packages/core/src/types.ts +0 -9
  113. package/packages/driver-chrome-relay/src/chrome-relay-driver.test.ts +0 -744
  114. package/packages/driver-chrome-relay/src/chrome-relay-driver.ts +0 -2429
  115. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.test.ts +0 -264
  116. package/packages/driver-chrome-relay/src/chrome-relay-extension-runtime.ts +0 -521
  117. package/packages/driver-chrome-relay/src/index.ts +0 -26
  118. package/packages/driver-managed/src/index.ts +0 -22
  119. package/packages/driver-managed/src/managed-driver.test.ts +0 -183
  120. package/packages/driver-managed/src/managed-driver.ts +0 -341
  121. package/packages/driver-managed/src/managed-local-driver.test.ts +0 -608
  122. package/packages/driver-managed/src/managed-local-driver.ts +0 -2243
  123. package/packages/driver-remote-cdp/src/index.ts +0 -19
  124. package/packages/driver-remote-cdp/src/remote-cdp-driver.test.ts +0 -727
  125. package/packages/driver-remote-cdp/src/remote-cdp-driver.ts +0 -2264
  126. package/packages/protocol/src/envelope.test.ts +0 -25
  127. package/packages/protocol/src/envelope.ts +0 -31
  128. package/packages/protocol/src/errors.test.ts +0 -17
  129. package/packages/protocol/src/errors.ts +0 -11
  130. package/packages/protocol/src/index.ts +0 -3
  131. package/packages/protocol/src/tools.ts +0 -3
  132. package/packages/transport-mcp-stdio/src/index.ts +0 -3
  133. package/packages/transport-mcp-stdio/src/sdk-server.ts +0 -139
  134. package/packages/transport-mcp-stdio/src/server.test.ts +0 -281
  135. package/packages/transport-mcp-stdio/src/server.ts +0 -183
  136. package/packages/transport-mcp-stdio/src/tool-map.ts +0 -84
@@ -1,632 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { spawn } from "node:child_process";
3
- import { dirname, join, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
6
- import { Socket } from "node:net";
7
-
8
- import { DAEMON_STARTUP_ARGUMENT, type DaemonStartupConfig } from "./commands/common";
9
-
10
- const DEFAULT_DAEMON_HOST = "127.0.0.1";
11
- const DEFAULT_DAEMON_PORT = 41337;
12
- const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 10_000;
13
- const DEFAULT_DAEMON_REQUEST_TIMEOUT_MS = 5_000;
14
- const STARTUP_POLL_INTERVAL_MS = 200;
15
-
16
- type ErrorPayload = {
17
- code: string;
18
- message: string;
19
- };
20
-
21
- type ToolEnvelope<TData = Record<string, unknown>> = {
22
- id?: string;
23
- ok: boolean;
24
- traceId: string;
25
- sessionId: string;
26
- profile?: string;
27
- targetId?: string;
28
- data?: TData;
29
- error?: ErrorPayload;
30
- };
31
-
32
- type ToolRequest = {
33
- id: string;
34
- name: string;
35
- traceId: string;
36
- arguments: Record<string, unknown>;
37
- };
38
-
39
- type DaemonLifecycleStatus = {
40
- running: boolean;
41
- port: number;
42
- pid?: number;
43
- };
44
-
45
- type DaemonRequestOptions = {
46
- port?: number;
47
- authToken?: string;
48
- startup?: DaemonStartupConfig;
49
- };
50
-
51
- type DaemonRuntimeRecord = {
52
- pid: number;
53
- authToken?: string;
54
- };
55
-
56
- const startupLocks = new Map<number, Promise<void>>();
57
-
58
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
59
- return typeof value === "object" && value !== null && !Array.isArray(value);
60
- }
61
-
62
- function toNumber(value: string | undefined): number | undefined {
63
- if (value === undefined) {
64
- return undefined;
65
- }
66
-
67
- const parsed = Number.parseInt(value, 10);
68
- return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
69
- }
70
-
71
- function resolveDaemonPort(): number {
72
- return toNumber(process.env.BROWSERCTL_DAEMON_PORT) ?? DEFAULT_DAEMON_PORT;
73
- }
74
-
75
- function resolveDaemonStartupTimeoutMs(): number {
76
- return (
77
- toNumber(process.env.BROWSERCTL_DAEMON_STARTUP_TIMEOUT_MS) ??
78
- DEFAULT_DAEMON_STARTUP_TIMEOUT_MS
79
- );
80
- }
81
-
82
- function resolveDaemonRequestTimeoutMs(): number {
83
- return (
84
- toNumber(process.env.BROWSERCTL_DAEMON_REQUEST_TIMEOUT_MS) ??
85
- DEFAULT_DAEMON_REQUEST_TIMEOUT_MS
86
- );
87
- }
88
-
89
- function normalizeAuthToken(value: unknown): string | undefined {
90
- if (typeof value !== "string") {
91
- return undefined;
92
- }
93
-
94
- const trimmedValue = value.trim();
95
- return trimmedValue.length === 0 ? undefined : trimmedValue;
96
- }
97
-
98
- function resolveDefaultAuthToken(port = resolveDaemonPort()): string | undefined {
99
- return normalizeAuthToken(process.env.BROWSERCTL_AUTH_TOKEN) ?? readDaemonRuntimeRecord(port)?.authToken;
100
- }
101
-
102
- function withAuthToken(
103
- args: Record<string, unknown>,
104
- explicitAuthToken?: string
105
- ): Record<string, unknown> {
106
- const authToken =
107
- normalizeAuthToken(explicitAuthToken) ??
108
- normalizeAuthToken(args.authToken) ??
109
- resolveDefaultAuthToken();
110
-
111
- if (authToken === undefined) {
112
- return args;
113
- }
114
-
115
- return {
116
- ...args,
117
- authToken
118
- };
119
- }
120
-
121
- function isDaemonStartupConfig(value: unknown): value is DaemonStartupConfig {
122
- if (!isObjectRecord(value)) {
123
- return false;
124
- }
125
-
126
- const managedLocal = value.managedLocal;
127
- if (!isObjectRecord(managedLocal)) {
128
- return false;
129
- }
130
-
131
- if (managedLocal.browserName !== "chromium") {
132
- return false;
133
- }
134
-
135
- if (managedLocal.channel !== undefined && typeof managedLocal.channel !== "string") {
136
- return false;
137
- }
138
-
139
- return true;
140
- }
141
-
142
- function extractDaemonStartupConfig(args: Record<string, unknown>): {
143
- requestArgs: Record<string, unknown>;
144
- startup?: DaemonStartupConfig;
145
- } {
146
- const rawStartup = args[DAEMON_STARTUP_ARGUMENT];
147
- if (rawStartup === undefined) {
148
- return {
149
- requestArgs: args
150
- };
151
- }
152
-
153
- const { [DAEMON_STARTUP_ARGUMENT]: _ignored, ...requestArgs } = args;
154
- if (!isDaemonStartupConfig(rawStartup)) {
155
- return {
156
- requestArgs
157
- };
158
- }
159
-
160
- return {
161
- requestArgs,
162
- startup: {
163
- managedLocal: {
164
- browserName: "chromium",
165
- ...(rawStartup.managedLocal.channel !== undefined
166
- ? { channel: rawStartup.managedLocal.channel }
167
- : {})
168
- }
169
- }
170
- };
171
- }
172
-
173
- function resolveRepoRoot(): string {
174
- const currentFile = fileURLToPath(import.meta.url);
175
- return resolve(dirname(currentFile), "../../..");
176
- }
177
-
178
- function resolveBrowserdEntry(): string {
179
- return resolve(resolveRepoRoot(), "apps/browserd/src/main.ts");
180
- }
181
-
182
- function resolveRuntimeDir(): string {
183
- return join(resolveRepoRoot(), ".browserctl-runtime");
184
- }
185
-
186
- function resolvePidFile(port: number): string {
187
- return join(resolveRuntimeDir(), `daemon-${port}.pid`);
188
- }
189
-
190
- function ensureRuntimeDir(): void {
191
- mkdirSync(resolveRuntimeDir(), { recursive: true });
192
- }
193
-
194
- function delay(ms: number): Promise<void> {
195
- return new Promise((resolvePromise) => {
196
- setTimeout(resolvePromise, ms);
197
- });
198
- }
199
-
200
- function getErrorCode(error: unknown): string | undefined {
201
- if (!isObjectRecord(error)) {
202
- return undefined;
203
- }
204
-
205
- const code = error.code;
206
- return typeof code === "string" ? code : undefined;
207
- }
208
-
209
- function isConnectionError(error: unknown): boolean {
210
- const code = getErrorCode(error);
211
- return code === "ECONNREFUSED" || code === "ENOENT" || code === "ECONNRESET";
212
- }
213
-
214
- function parseResponseLine(stdoutLine: string, requestId: string): ToolEnvelope {
215
- const parsed = JSON.parse(stdoutLine);
216
- if (!isObjectRecord(parsed)) {
217
- throw new Error("Daemon returned a non-object response.");
218
- }
219
-
220
- if (typeof parsed.ok !== "boolean") {
221
- throw new Error("Daemon response is missing boolean ok.");
222
- }
223
-
224
- if (typeof parsed.id === "string" && parsed.id !== requestId) {
225
- throw new Error(`Daemon response id mismatch. expected=${requestId} got=${parsed.id}`);
226
- }
227
-
228
- return parsed as ToolEnvelope;
229
- }
230
-
231
- function createToolRequest(name: string, args: Record<string, unknown>): ToolRequest {
232
- return {
233
- id: randomUUID(),
234
- name,
235
- traceId: `trace:browserctl:${randomUUID()}`,
236
- arguments: args
237
- };
238
- }
239
-
240
- async function requestDaemon(
241
- request: ToolRequest,
242
- port: number,
243
- timeoutMs: number
244
- ): Promise<ToolEnvelope> {
245
- return await new Promise<ToolEnvelope>((resolvePromise, rejectPromise) => {
246
- const socket = new Socket();
247
- socket.setEncoding("utf8");
248
-
249
- let settled = false;
250
- let buffer = "";
251
-
252
- const timer = setTimeout(() => {
253
- if (settled) {
254
- return;
255
- }
256
-
257
- settled = true;
258
- socket.destroy();
259
- rejectPromise(
260
- new Error(`Timed out waiting for daemon response after ${timeoutMs}ms (port ${port}).`)
261
- );
262
- }, timeoutMs);
263
-
264
- function settleResolve(value: ToolEnvelope): void {
265
- if (settled) {
266
- return;
267
- }
268
-
269
- settled = true;
270
- clearTimeout(timer);
271
- socket.end();
272
- resolvePromise(value);
273
- }
274
-
275
- function settleReject(error: unknown): void {
276
- if (settled) {
277
- return;
278
- }
279
-
280
- settled = true;
281
- clearTimeout(timer);
282
- socket.destroy();
283
- rejectPromise(error);
284
- }
285
-
286
- socket.on("error", settleReject);
287
-
288
- socket.on("connect", () => {
289
- socket.write(`${JSON.stringify(request)}\n`);
290
- });
291
-
292
- socket.on("data", (chunk: string) => {
293
- buffer += chunk;
294
-
295
- let lineBreakIndex = buffer.indexOf("\n");
296
- while (lineBreakIndex >= 0) {
297
- const line = buffer.slice(0, lineBreakIndex).trim();
298
- buffer = buffer.slice(lineBreakIndex + 1);
299
-
300
- if (line.length === 0) {
301
- lineBreakIndex = buffer.indexOf("\n");
302
- continue;
303
- }
304
-
305
- try {
306
- settleResolve(parseResponseLine(line, request.id));
307
- } catch (error) {
308
- settleReject(error);
309
- }
310
-
311
- return;
312
- }
313
- });
314
-
315
- socket.connect(port, DEFAULT_DAEMON_HOST);
316
- });
317
- }
318
-
319
- function parseDaemonRuntimeRecord(raw: string): DaemonRuntimeRecord | undefined {
320
- const trimmedRaw = raw.trim();
321
- if (trimmedRaw.length === 0) {
322
- return undefined;
323
- }
324
-
325
- const legacyPid = Number.parseInt(trimmedRaw, 10);
326
- if (Number.isFinite(legacyPid) && String(legacyPid) === trimmedRaw) {
327
- return {
328
- pid: legacyPid
329
- };
330
- }
331
-
332
- try {
333
- const parsed = JSON.parse(trimmedRaw);
334
- if (!isObjectRecord(parsed)) {
335
- return undefined;
336
- }
337
-
338
- const rawPid = parsed.pid;
339
- if (typeof rawPid !== "number" || !Number.isFinite(rawPid) || rawPid <= 0) {
340
- return undefined;
341
- }
342
-
343
- return {
344
- pid: rawPid,
345
- authToken: normalizeAuthToken(parsed.authToken)
346
- };
347
- } catch {
348
- return undefined;
349
- }
350
- }
351
-
352
- function persistDaemonRuntimeRecord(port: number, record: DaemonRuntimeRecord): void {
353
- ensureRuntimeDir();
354
- writeFileSync(resolvePidFile(port), JSON.stringify(record), { encoding: "utf8" });
355
- }
356
-
357
- function readDaemonRuntimeRecord(port: number): DaemonRuntimeRecord | undefined {
358
- try {
359
- const content = readFileSync(resolvePidFile(port), { encoding: "utf8" });
360
- return parseDaemonRuntimeRecord(content);
361
- } catch {
362
- return undefined;
363
- }
364
- }
365
-
366
- function readDaemonPid(port: number): number | undefined {
367
- return readDaemonRuntimeRecord(port)?.pid;
368
- }
369
-
370
- function clearDaemonPid(port: number): void {
371
- rmSync(resolvePidFile(port), { force: true });
372
- }
373
-
374
- function createDaemonSpawnEnv(
375
- port: number,
376
- authToken: string,
377
- startup?: DaemonStartupConfig
378
- ): NodeJS.ProcessEnv {
379
- const env: NodeJS.ProcessEnv = {
380
- ...process.env,
381
- BROWSERD_TRANSPORT: "tcp",
382
- BROWSERD_PORT: String(port),
383
- BROWSERD_AUTH_TOKEN: authToken
384
- };
385
-
386
- if (startup !== undefined) {
387
- env.BROWSERD_MANAGED_LOCAL_ENABLED = "true";
388
- env.BROWSERD_MANAGED_LOCAL_BROWSER = startup.managedLocal.browserName;
389
- if (startup.managedLocal.channel === undefined) {
390
- delete env.BROWSERD_MANAGED_LOCAL_CHANNEL;
391
- } else {
392
- env.BROWSERD_MANAGED_LOCAL_CHANNEL = startup.managedLocal.channel;
393
- }
394
- }
395
-
396
- return env;
397
- }
398
-
399
- function spawnDaemon(port: number, authToken: string, startup?: DaemonStartupConfig): void {
400
- const daemonEntry = resolveBrowserdEntry();
401
- const child = spawn(process.execPath, ["--import", "tsx", daemonEntry], {
402
- cwd: resolveRepoRoot(),
403
- detached: true,
404
- stdio: "ignore",
405
- windowsHide: true,
406
- env: createDaemonSpawnEnv(port, authToken, startup)
407
- });
408
-
409
- if (child.pid === undefined) {
410
- throw new Error("Failed to spawn daemon: missing pid.");
411
- }
412
-
413
- persistDaemonRuntimeRecord(port, {
414
- pid: child.pid,
415
- authToken
416
- });
417
- child.unref();
418
- }
419
-
420
- async function waitForDaemonReady(port: number, authToken?: string): Promise<void> {
421
- const timeoutMs = resolveDaemonStartupTimeoutMs();
422
- const requestTimeoutMs = resolveDaemonRequestTimeoutMs();
423
- const start = Date.now();
424
- let lastError: unknown;
425
-
426
- while (Date.now() - start <= timeoutMs) {
427
- try {
428
- await requestDaemon(
429
- createToolRequest(
430
- "browser.status",
431
- withAuthToken(
432
- {
433
- sessionId: "cli:daemon-ready-check"
434
- },
435
- authToken
436
- )
437
- ),
438
- port,
439
- requestTimeoutMs
440
- );
441
- return;
442
- } catch (error) {
443
- lastError = error;
444
- await delay(STARTUP_POLL_INTERVAL_MS);
445
- }
446
- }
447
-
448
- const message = lastError instanceof Error ? lastError.message : String(lastError);
449
- throw new Error(`Daemon did not become ready on port ${port}: ${message}`);
450
- }
451
-
452
- async function ensureDaemonStarted(
453
- port: number,
454
- authToken?: string,
455
- startup?: DaemonStartupConfig
456
- ): Promise<void> {
457
- const existingLock = startupLocks.get(port);
458
- if (existingLock !== undefined) {
459
- await existingLock;
460
- return;
461
- }
462
-
463
- const lock = (async () => {
464
- const startupAuthToken = normalizeAuthToken(authToken) ?? `daemon-token:${randomUUID()}`;
465
- spawnDaemon(port, startupAuthToken, startup);
466
- await waitForDaemonReady(port, startupAuthToken);
467
- })();
468
-
469
- startupLocks.set(port, lock);
470
- try {
471
- await lock;
472
- } finally {
473
- startupLocks.delete(port);
474
- }
475
- }
476
-
477
- export async function getDaemonStatus(options: DaemonRequestOptions = {}): Promise<DaemonLifecycleStatus> {
478
- const port = options.port ?? resolveDaemonPort();
479
- const authToken = options.authToken ?? resolveDefaultAuthToken(port);
480
- const requestTimeoutMs = resolveDaemonRequestTimeoutMs();
481
- try {
482
- await requestDaemon(
483
- createToolRequest(
484
- "browser.status",
485
- withAuthToken(
486
- {
487
- sessionId: "cli:daemon-status"
488
- },
489
- authToken
490
- )
491
- ),
492
- port,
493
- requestTimeoutMs
494
- );
495
- return {
496
- running: true,
497
- port,
498
- pid: readDaemonPid(port)
499
- };
500
- } catch (error) {
501
- if (isConnectionError(error)) {
502
- return {
503
- running: false,
504
- port,
505
- pid: readDaemonPid(port)
506
- };
507
- }
508
-
509
- throw error;
510
- }
511
- }
512
-
513
- export async function ensureDaemonRunning(
514
- options: DaemonRequestOptions = {}
515
- ): Promise<DaemonLifecycleStatus> {
516
- const port = options.port ?? resolveDaemonPort();
517
- const authToken = options.authToken ?? resolveDefaultAuthToken(port);
518
- const status = await getDaemonStatus({
519
- port,
520
- authToken
521
- });
522
- if (status.running) {
523
- return status;
524
- }
525
-
526
- await ensureDaemonStarted(port, authToken, options.startup);
527
- return {
528
- running: true,
529
- port,
530
- pid: readDaemonPid(port)
531
- };
532
- }
533
-
534
- export async function stopDaemon(
535
- port = resolveDaemonPort()
536
- ): Promise<{ stopped: boolean; port: number; pid?: number }> {
537
- const runtimeRecord = readDaemonRuntimeRecord(port);
538
- if (runtimeRecord === undefined) {
539
- return {
540
- stopped: false,
541
- port
542
- };
543
- }
544
-
545
- const pid = runtimeRecord.pid;
546
- const authToken = runtimeRecord.authToken;
547
- const requestTimeoutMs = resolveDaemonRequestTimeoutMs();
548
-
549
- try {
550
- const probe = await requestDaemon(
551
- createToolRequest(
552
- "browser.status",
553
- withAuthToken(
554
- {
555
- sessionId: "cli:daemon-stop-check"
556
- },
557
- authToken
558
- )
559
- ),
560
- port,
561
- requestTimeoutMs
562
- );
563
- if (!probe.ok) {
564
- clearDaemonPid(port);
565
- return {
566
- stopped: false,
567
- port,
568
- pid
569
- };
570
- }
571
- } catch {
572
- clearDaemonPid(port);
573
- return {
574
- stopped: false,
575
- port,
576
- pid
577
- };
578
- }
579
-
580
- try {
581
- process.kill(pid);
582
- clearDaemonPid(port);
583
- return {
584
- stopped: true,
585
- port,
586
- pid
587
- };
588
- } catch {
589
- clearDaemonPid(port);
590
- return {
591
- stopped: false,
592
- port,
593
- pid
594
- };
595
- }
596
- }
597
-
598
- export async function callDaemonTool<TData = Record<string, unknown>>(
599
- name: string,
600
- args: Record<string, unknown>
601
- ): Promise<TData> {
602
- const port = resolveDaemonPort();
603
- const extracted = extractDaemonStartupConfig(args);
604
- const authToken = normalizeAuthToken(extracted.requestArgs.authToken) ?? resolveDefaultAuthToken(port);
605
- const requestArgs = withAuthToken(extracted.requestArgs, authToken);
606
- const requestTimeoutMs = resolveDaemonRequestTimeoutMs();
607
- const request = createToolRequest(name, requestArgs);
608
-
609
- let response: ToolEnvelope;
610
- try {
611
- response = await requestDaemon(request, port, requestTimeoutMs);
612
- } catch (error) {
613
- if (!isConnectionError(error)) {
614
- throw error;
615
- }
616
-
617
- await ensureDaemonStarted(port, authToken, extracted.startup);
618
- response = await requestDaemon(request, port, requestTimeoutMs);
619
- }
620
-
621
- if (!response.ok) {
622
- const code = response.error?.code ?? "E_INTERNAL";
623
- const message = response.error?.message ?? "Unknown daemon error.";
624
- throw new Error(`${code}: ${message}`);
625
- }
626
-
627
- if (!isObjectRecord(response.data)) {
628
- return {} as TData;
629
- }
630
-
631
- return response.data as TData;
632
- }