@skills-store/rednote 0.1.0 → 0.1.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.
- package/bin/rednote.js +16 -24
- package/dist/browser/connect-browser.js +172 -0
- package/dist/browser/create-browser.js +52 -0
- package/dist/browser/index.js +35 -0
- package/dist/browser/list-browser.js +50 -0
- package/dist/browser/remove-browser.js +69 -0
- package/{scripts/index.ts → dist/index.js} +19 -25
- package/dist/rednote/checkLogin.js +139 -0
- package/dist/rednote/env.js +69 -0
- package/dist/rednote/getFeedDetail.js +268 -0
- package/dist/rednote/getProfile.js +327 -0
- package/dist/rednote/home.js +210 -0
- package/dist/rednote/index.js +130 -0
- package/dist/rednote/login.js +109 -0
- package/dist/rednote/output-format.js +116 -0
- package/dist/rednote/publish.js +376 -0
- package/dist/rednote/search.js +207 -0
- package/dist/rednote/status.js +201 -0
- package/dist/utils/browser-cli.js +155 -0
- package/dist/utils/browser-core.js +705 -0
- package/package.json +7 -4
- package/scripts/browser/connect-browser.ts +0 -218
- package/scripts/browser/create-browser.ts +0 -81
- package/scripts/browser/index.ts +0 -49
- package/scripts/browser/list-browser.ts +0 -74
- package/scripts/browser/remove-browser.ts +0 -109
- package/scripts/rednote/checkLogin.ts +0 -171
- package/scripts/rednote/env.ts +0 -79
- package/scripts/rednote/getFeedDetail.ts +0 -351
- package/scripts/rednote/getProfile.ts +0 -420
- package/scripts/rednote/home.ts +0 -316
- package/scripts/rednote/index.ts +0 -122
- package/scripts/rednote/login.ts +0 -142
- package/scripts/rednote/output-format.ts +0 -156
- package/scripts/rednote/post-types.ts +0 -51
- package/scripts/rednote/search.ts +0 -316
- package/scripts/rednote/status.ts +0 -280
- package/scripts/utils/browser-cli.ts +0 -176
- package/scripts/utils/browser-core.ts +0 -906
- package/tsconfig.json +0 -13
- /package/{scripts/rednote/collect.ts → dist/rednote/collect.js} +0 -0
- /package/{scripts/rednote/publish.ts → dist/rednote/post-types.js} +0 -0
|
@@ -1,906 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import os from 'node:os';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import net from 'node:net';
|
|
5
|
-
import { execFile, spawn, spawnSync } from 'node:child_process';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
7
|
-
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import chromePath from 'chrome-paths';
|
|
9
|
-
import { getEdgePath } from 'edge-paths';
|
|
10
|
-
import psList from 'ps-list';
|
|
11
|
-
import { portToPid } from 'pid-port';
|
|
12
|
-
import { stringifyError } from './browser-cli.ts';
|
|
13
|
-
|
|
14
|
-
const execFileAsync = promisify(execFile);
|
|
15
|
-
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
-
|
|
17
|
-
export type BrowserType = 'chrome' | 'edge' | 'chromium' | 'brave';
|
|
18
|
-
|
|
19
|
-
export type BrowserSpec = {
|
|
20
|
-
type: BrowserType;
|
|
21
|
-
displayName: string;
|
|
22
|
-
executableCandidates: string[];
|
|
23
|
-
userDataDir: string;
|
|
24
|
-
processNames: string[];
|
|
25
|
-
pathCommands: string[];
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type ProcessInfo = {
|
|
29
|
-
pid: number;
|
|
30
|
-
name: string;
|
|
31
|
-
cmdline: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type BrowserInstanceInfo = {
|
|
35
|
-
type: BrowserType;
|
|
36
|
-
name: string;
|
|
37
|
-
executablePath: string;
|
|
38
|
-
userDataDir: string;
|
|
39
|
-
exists: boolean;
|
|
40
|
-
inUse: boolean;
|
|
41
|
-
pid: number | null;
|
|
42
|
-
lockFiles: string[];
|
|
43
|
-
matchedProcess: ProcessInfo | null;
|
|
44
|
-
staleLock: boolean;
|
|
45
|
-
remotePort: number | null;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export type InspectedBrowserInstance = BrowserInstanceInfo & {
|
|
49
|
-
pids: number[];
|
|
50
|
-
matchedProcesses: ProcessInfo[];
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export type PersistedInstance = {
|
|
54
|
-
name: string;
|
|
55
|
-
browser: BrowserType;
|
|
56
|
-
userDataDir: string;
|
|
57
|
-
createdAt: string;
|
|
58
|
-
remoteDebuggingPort?: number;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export type LastConnectInfo = {
|
|
62
|
-
scope: 'default' | 'custom';
|
|
63
|
-
name: string;
|
|
64
|
-
browser: BrowserType;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export type InstanceStore = {
|
|
68
|
-
version: 1;
|
|
69
|
-
lastConnect: LastConnectInfo | null;
|
|
70
|
-
instances: PersistedInstance[];
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export type ListedBrowserInstance = BrowserInstanceInfo & {
|
|
74
|
-
scope: 'default' | 'custom';
|
|
75
|
-
instanceName: string;
|
|
76
|
-
createdAt: string | null;
|
|
77
|
-
lastConnect: boolean;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type ConnectBrowserOptions = {
|
|
81
|
-
browser?: BrowserType;
|
|
82
|
-
executablePath?: string;
|
|
83
|
-
userDataDir?: string;
|
|
84
|
-
force?: boolean;
|
|
85
|
-
remoteDebuggingPort?: number;
|
|
86
|
-
connectTimeoutMs?: number;
|
|
87
|
-
killTimeoutMs?: number;
|
|
88
|
-
startupUrl?: string;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export type ConnectBrowserResult = {
|
|
92
|
-
ok: true;
|
|
93
|
-
type: BrowserType;
|
|
94
|
-
executablePath: string;
|
|
95
|
-
userDataDir: string;
|
|
96
|
-
remoteDebuggingPort: number;
|
|
97
|
-
wsUrl: string;
|
|
98
|
-
pid: number | null;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export type CreateInstanceOptions = {
|
|
102
|
-
name: string;
|
|
103
|
-
browser?: BrowserType;
|
|
104
|
-
remoteDebuggingPort?: number;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export type CreateInstanceResult = {
|
|
108
|
-
ok: true;
|
|
109
|
-
instance: PersistedInstance;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export type RemoveInstanceOptions = {
|
|
113
|
-
name: string;
|
|
114
|
-
force?: boolean;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
export type RemoveInstanceResult = {
|
|
118
|
-
ok: true;
|
|
119
|
-
removedInstance: PersistedInstance;
|
|
120
|
-
removedDir: boolean;
|
|
121
|
-
closedPids: number[];
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
export type ListBrowserInstancesResult = {
|
|
125
|
-
lastConnect: LastConnectInfo | null;
|
|
126
|
-
instances: ListedBrowserInstance[];
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
130
|
-
export const PACKAGE_ROOT = path.resolve(SCRIPT_DIR, '../..');
|
|
131
|
-
export const SKILLS_ROUTER_HOME = path.join(os.homedir(), '.skills-router');
|
|
132
|
-
export const REDNOTE_STORAGE_ROOT = path.join(SKILLS_ROUTER_HOME, 'rednote');
|
|
133
|
-
export const INSTANCES_DIR = path.join(REDNOTE_STORAGE_ROOT, 'instances');
|
|
134
|
-
export const INSTANCE_STORE_PATH = path.join(INSTANCES_DIR, 'data.json');
|
|
135
|
-
export const LEGACY_PACKAGE_INSTANCES_DIR = path.join(PACKAGE_ROOT, 'instances');
|
|
136
|
-
|
|
137
|
-
export function normalizePath(inputPath: string) {
|
|
138
|
-
let resolved = path.resolve(inputPath);
|
|
139
|
-
if (process.platform === 'win32') {
|
|
140
|
-
resolved = resolved.toLowerCase();
|
|
141
|
-
}
|
|
142
|
-
return resolved.replace(/[\\/]+$/, '');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function unique<T>(values: T[]) {
|
|
146
|
-
return [...new Set(values)];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function defaultInstanceStore(): InstanceStore {
|
|
150
|
-
return {
|
|
151
|
-
version: 1,
|
|
152
|
-
lastConnect: null,
|
|
153
|
-
instances: [],
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function currentManagedInstanceDir(name: string) {
|
|
158
|
-
return path.join(INSTANCES_DIR, name);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function legacyManagedInstanceDirs(name: string) {
|
|
162
|
-
return [
|
|
163
|
-
path.join(LEGACY_PACKAGE_INSTANCES_DIR, name),
|
|
164
|
-
path.resolve(PACKAGE_ROOT, '../../skills/rednote/instances', name),
|
|
165
|
-
];
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function resolvePersistedInstanceUserDataDir(name: string, userDataDir: string) {
|
|
169
|
-
const normalizedInput = normalizePath(userDataDir);
|
|
170
|
-
const managedCurrentDir = currentManagedInstanceDir(name);
|
|
171
|
-
|
|
172
|
-
if (normalizedInput === normalizePath(managedCurrentDir)) {
|
|
173
|
-
return managedCurrentDir;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (legacyManagedInstanceDirs(name).some((dirPath) => normalizedInput === normalizePath(dirPath))) {
|
|
177
|
-
return managedCurrentDir;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const instancesSuffix = path.sep + 'instances' + path.sep + name;
|
|
181
|
-
if (normalizedInput.endsWith(instancesSuffix)) {
|
|
182
|
-
return managedCurrentDir;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return userDataDir;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function exists(filePath: string) {
|
|
189
|
-
try {
|
|
190
|
-
fs.accessSync(filePath, fs.constants.F_OK);
|
|
191
|
-
return true;
|
|
192
|
-
} catch {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
function migrateLegacyInstanceStoreIfNeeded() {
|
|
198
|
-
if (normalizePath(LEGACY_PACKAGE_INSTANCES_DIR) === normalizePath(INSTANCES_DIR)) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (exists(INSTANCE_STORE_PATH) || !exists(LEGACY_PACKAGE_INSTANCES_DIR)) {
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
fs.mkdirSync(REDNOTE_STORAGE_ROOT, { recursive: true });
|
|
207
|
-
fs.cpSync(LEGACY_PACKAGE_INSTANCES_DIR, INSTANCES_DIR, {
|
|
208
|
-
recursive: true,
|
|
209
|
-
force: false,
|
|
210
|
-
errorOnExist: false,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function ensureInstanceStoreDir() {
|
|
215
|
-
migrateLegacyInstanceStoreIfNeeded();
|
|
216
|
-
fs.mkdirSync(INSTANCES_DIR, { recursive: true });
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export function getRednoteEnvironmentInfo() {
|
|
220
|
-
return {
|
|
221
|
-
packageRoot: PACKAGE_ROOT,
|
|
222
|
-
homeDir: os.homedir(),
|
|
223
|
-
platform: process.platform,
|
|
224
|
-
nodeVersion: process.version,
|
|
225
|
-
storageHome: SKILLS_ROUTER_HOME,
|
|
226
|
-
storageRoot: REDNOTE_STORAGE_ROOT,
|
|
227
|
-
instancesDir: INSTANCES_DIR,
|
|
228
|
-
instanceStorePath: INSTANCE_STORE_PATH,
|
|
229
|
-
legacyPackageInstancesDir: LEGACY_PACKAGE_INSTANCES_DIR,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function readInstanceStore(): InstanceStore {
|
|
234
|
-
ensureInstanceStoreDir();
|
|
235
|
-
|
|
236
|
-
if (!exists(INSTANCE_STORE_PATH)) {
|
|
237
|
-
return defaultInstanceStore();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
try {
|
|
241
|
-
const raw = fs.readFileSync(INSTANCE_STORE_PATH, 'utf8');
|
|
242
|
-
const parsed = JSON.parse(raw) as Partial<InstanceStore>;
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
version: 1,
|
|
246
|
-
lastConnect:
|
|
247
|
-
parsed.lastConnect &&
|
|
248
|
-
(parsed.lastConnect.scope === 'default' || parsed.lastConnect.scope === 'custom') &&
|
|
249
|
-
typeof parsed.lastConnect.name === 'string' &&
|
|
250
|
-
typeof parsed.lastConnect.browser === 'string'
|
|
251
|
-
? {
|
|
252
|
-
scope: parsed.lastConnect.scope,
|
|
253
|
-
name: parsed.lastConnect.name,
|
|
254
|
-
browser: parsed.lastConnect.browser as BrowserType,
|
|
255
|
-
}
|
|
256
|
-
: null,
|
|
257
|
-
instances: Array.isArray(parsed.instances)
|
|
258
|
-
? parsed.instances.flatMap((item) => {
|
|
259
|
-
if (
|
|
260
|
-
item &&
|
|
261
|
-
typeof item.name === 'string' &&
|
|
262
|
-
typeof item.browser === 'string' &&
|
|
263
|
-
typeof item.userDataDir === 'string' &&
|
|
264
|
-
typeof item.createdAt === 'string'
|
|
265
|
-
) {
|
|
266
|
-
return [{
|
|
267
|
-
name: item.name,
|
|
268
|
-
browser: item.browser as BrowserType,
|
|
269
|
-
userDataDir: resolvePersistedInstanceUserDataDir(item.name, item.userDataDir),
|
|
270
|
-
createdAt: item.createdAt,
|
|
271
|
-
remoteDebuggingPort:
|
|
272
|
-
typeof item.remoteDebuggingPort === 'number' && Number.isInteger(item.remoteDebuggingPort) && item.remoteDebuggingPort > 0
|
|
273
|
-
? item.remoteDebuggingPort
|
|
274
|
-
: undefined,
|
|
275
|
-
} satisfies PersistedInstance];
|
|
276
|
-
}
|
|
277
|
-
return [];
|
|
278
|
-
})
|
|
279
|
-
: [],
|
|
280
|
-
};
|
|
281
|
-
} catch {
|
|
282
|
-
return defaultInstanceStore();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function writeInstanceStore(store: InstanceStore) {
|
|
287
|
-
ensureInstanceStoreDir();
|
|
288
|
-
fs.writeFileSync(INSTANCE_STORE_PATH, `${JSON.stringify(store, null, 2)}
|
|
289
|
-
`, 'utf8');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function updateInstanceRemoteDebuggingPort(name: string, remoteDebuggingPort: number) {
|
|
293
|
-
const store = readInstanceStore();
|
|
294
|
-
const instanceName = validateInstanceName(name);
|
|
295
|
-
const nextInstances = store.instances.map((instance) =>
|
|
296
|
-
instance.name === instanceName
|
|
297
|
-
? {
|
|
298
|
-
...instance,
|
|
299
|
-
remoteDebuggingPort,
|
|
300
|
-
}
|
|
301
|
-
: instance,
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
if (!nextInstances.some((instance) => instance.name === instanceName)) {
|
|
305
|
-
throw new Error(`Instance not found: ${instanceName}`);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
writeInstanceStore({
|
|
309
|
-
...store,
|
|
310
|
-
instances: nextInstances,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export async function getRandomAvailablePort() {
|
|
315
|
-
return await new Promise<number>((resolve, reject) => {
|
|
316
|
-
const server = net.createServer();
|
|
317
|
-
|
|
318
|
-
server.once('error', reject);
|
|
319
|
-
server.listen(0, '127.0.0.1', () => {
|
|
320
|
-
const address = server.address();
|
|
321
|
-
if (!address || typeof address === 'string') {
|
|
322
|
-
server.close(() => reject(new Error('Failed to allocate a random port')));
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
server.close((error) => {
|
|
327
|
-
if (error) {
|
|
328
|
-
reject(error);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
resolve(address.port);
|
|
332
|
-
});
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
export function customInstanceUserDataDir(name: string) {
|
|
338
|
-
return path.join(INSTANCES_DIR, name);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export function isSubPath(parentPath: string, childPath: string) {
|
|
342
|
-
const normalizedParent = normalizePath(parentPath);
|
|
343
|
-
const normalizedChild = normalizePath(childPath);
|
|
344
|
-
|
|
345
|
-
return normalizedChild === normalizedParent || normalizedChild.startsWith(`${normalizedParent}${path.sep}`);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
export function validateInstanceName(name: string) {
|
|
349
|
-
const trimmed = name.trim();
|
|
350
|
-
if (!trimmed) {
|
|
351
|
-
throw new Error('Instance name cannot be empty');
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{1,63}$/.test(trimmed)) {
|
|
355
|
-
throw new Error('Instance name must match /^[a-zA-Z0-9][a-zA-Z0-9._-]{1,63}$/');
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return trimmed;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
export function isLastConnectMatch(
|
|
362
|
-
lastConnect: LastConnectInfo | null,
|
|
363
|
-
scope: 'default' | 'custom',
|
|
364
|
-
instanceName: string,
|
|
365
|
-
browser: BrowserType,
|
|
366
|
-
) {
|
|
367
|
-
return Boolean(
|
|
368
|
-
lastConnect &&
|
|
369
|
-
lastConnect.scope === scope &&
|
|
370
|
-
lastConnect.name === instanceName &&
|
|
371
|
-
lastConnect.browser === browser,
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
export function updateLastConnect(lastConnect: LastConnectInfo) {
|
|
376
|
-
const store = readInstanceStore();
|
|
377
|
-
writeInstanceStore({
|
|
378
|
-
...store,
|
|
379
|
-
lastConnect,
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
function basenameMatches(procName: string, names: string[]) {
|
|
384
|
-
const base = path.basename(procName).toLowerCase();
|
|
385
|
-
return names.some((name) => base === name.toLowerCase());
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function spawnSyncText(command: string, args: string[]) {
|
|
389
|
-
const result = spawnSync(command, args, {
|
|
390
|
-
encoding: 'utf8',
|
|
391
|
-
windowsHide: true,
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
if (result.error) {
|
|
395
|
-
throw result.error;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (result.status !== 0) {
|
|
399
|
-
throw new Error(result.stderr || `Command failed: ${command} ${args.join(' ')}`);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return result.stdout;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function commandExists(command: string) {
|
|
406
|
-
try {
|
|
407
|
-
const probe = process.platform === 'win32' ? 'where' : 'command';
|
|
408
|
-
const args = process.platform === 'win32' ? [command] : ['-v', command];
|
|
409
|
-
const result = spawnSync(probe, args, { stdio: 'ignore', shell: process.platform !== 'win32' });
|
|
410
|
-
return result.status === 0;
|
|
411
|
-
} catch {
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function findExecutableInPath(commands: string[]) {
|
|
417
|
-
for (const command of commands) {
|
|
418
|
-
if (!commandExists(command)) {
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
if (process.platform === 'win32') {
|
|
424
|
-
const output = spawnSyncText('where', [command]).trim().split(/\r?\n/)[0];
|
|
425
|
-
if (output) {
|
|
426
|
-
return output;
|
|
427
|
-
}
|
|
428
|
-
} else {
|
|
429
|
-
const output = spawnSyncText('command', ['-v', command]);
|
|
430
|
-
const resolved = output.trim().split(/\r?\n/)[0];
|
|
431
|
-
if (resolved) {
|
|
432
|
-
return resolved;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
} catch {
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return null;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export function browserSpecs(): BrowserSpec[] {
|
|
444
|
-
const homeDir = os.homedir();
|
|
445
|
-
|
|
446
|
-
return [
|
|
447
|
-
{
|
|
448
|
-
type: 'chrome',
|
|
449
|
-
displayName: 'Google Chrome',
|
|
450
|
-
executableCandidates: process.platform === 'darwin'
|
|
451
|
-
? ['/Applications/Google Chrome.app/Contents/MacOS/Google Chrome']
|
|
452
|
-
: process.platform === 'win32'
|
|
453
|
-
? [
|
|
454
|
-
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
455
|
-
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
456
|
-
]
|
|
457
|
-
: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'],
|
|
458
|
-
userDataDir: process.platform === 'darwin'
|
|
459
|
-
? path.join(homeDir, 'Library/Application Support/Google/Chrome')
|
|
460
|
-
: process.platform === 'win32'
|
|
461
|
-
? path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData/Local'), 'Google/Chrome/User Data')
|
|
462
|
-
: path.join(homeDir, '.config/google-chrome'),
|
|
463
|
-
processNames: ['Google Chrome', 'Google Chrome Helper', 'chrome.exe', 'google-chrome', 'google-chrome-stable'],
|
|
464
|
-
pathCommands: ['google-chrome', 'google-chrome-stable'],
|
|
465
|
-
},
|
|
466
|
-
{
|
|
467
|
-
type: 'edge',
|
|
468
|
-
displayName: 'Microsoft Edge',
|
|
469
|
-
executableCandidates: process.platform === 'darwin'
|
|
470
|
-
? ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge']
|
|
471
|
-
: process.platform === 'win32'
|
|
472
|
-
? [
|
|
473
|
-
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
474
|
-
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
475
|
-
]
|
|
476
|
-
: ['/usr/bin/microsoft-edge', '/usr/bin/microsoft-edge-stable'],
|
|
477
|
-
userDataDir: process.platform === 'darwin'
|
|
478
|
-
? path.join(homeDir, 'Library/Application Support/Microsoft Edge')
|
|
479
|
-
: process.platform === 'win32'
|
|
480
|
-
? path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData/Local'), 'Microsoft/Edge/User Data')
|
|
481
|
-
: path.join(homeDir, '.config/microsoft-edge'),
|
|
482
|
-
processNames: ['Microsoft Edge', 'msedge.exe', 'microsoft-edge', 'microsoft-edge-stable'],
|
|
483
|
-
pathCommands: ['microsoft-edge', 'microsoft-edge-stable'],
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
type: 'chromium',
|
|
487
|
-
displayName: 'Chromium',
|
|
488
|
-
executableCandidates: process.platform === 'darwin'
|
|
489
|
-
? ['/Applications/Chromium.app/Contents/MacOS/Chromium']
|
|
490
|
-
: process.platform === 'win32'
|
|
491
|
-
? [
|
|
492
|
-
'C:\\Program Files\\Chromium\\Application\\chrome.exe',
|
|
493
|
-
'C:\\Program Files (x86)\\Chromium\\Application\\chrome.exe',
|
|
494
|
-
]
|
|
495
|
-
: ['/usr/bin/chromium', '/usr/bin/chromium-browser'],
|
|
496
|
-
userDataDir: process.platform === 'darwin'
|
|
497
|
-
? path.join(homeDir, 'Library/Application Support/Chromium')
|
|
498
|
-
: process.platform === 'win32'
|
|
499
|
-
? path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData/Local'), 'Chromium/User Data')
|
|
500
|
-
: path.join(homeDir, '.config/chromium'),
|
|
501
|
-
processNames: ['Chromium', 'chromium', 'chromium-browser', 'chrome.exe'],
|
|
502
|
-
pathCommands: ['chromium', 'chromium-browser'],
|
|
503
|
-
},
|
|
504
|
-
{
|
|
505
|
-
type: 'brave',
|
|
506
|
-
displayName: 'Brave',
|
|
507
|
-
executableCandidates: process.platform === 'darwin'
|
|
508
|
-
? ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser']
|
|
509
|
-
: process.platform === 'win32'
|
|
510
|
-
? [
|
|
511
|
-
'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
512
|
-
'C:\\Program Files (x86)\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
513
|
-
]
|
|
514
|
-
: ['/usr/bin/brave-browser', '/usr/bin/brave'],
|
|
515
|
-
userDataDir: process.platform === 'darwin'
|
|
516
|
-
? path.join(homeDir, 'Library/Application Support/BraveSoftware/Brave-Browser')
|
|
517
|
-
: process.platform === 'win32'
|
|
518
|
-
? path.join(process.env.LOCALAPPDATA || path.join(homeDir, 'AppData/Local'), 'BraveSoftware/Brave-Browser/User Data')
|
|
519
|
-
: path.join(homeDir, '.config/BraveSoftware/Brave-Browser'),
|
|
520
|
-
processNames: ['Brave Browser', 'brave.exe', 'brave-browser', 'brave'],
|
|
521
|
-
pathCommands: ['brave-browser', 'brave'],
|
|
522
|
-
},
|
|
523
|
-
];
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
export function resolveExecutablePath(spec: BrowserSpec) {
|
|
527
|
-
if (spec.type === 'chrome' && chromePath?.chrome) {
|
|
528
|
-
return chromePath.chrome;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
if (spec.type === 'edge') {
|
|
532
|
-
const edge = getEdgePath();
|
|
533
|
-
if (edge) {
|
|
534
|
-
return edge;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
for (const candidate of spec.executableCandidates) {
|
|
539
|
-
if (exists(candidate)) {
|
|
540
|
-
return candidate;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return findExecutableInPath(spec.pathCommands);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
export async function listProcesses(): Promise<ProcessInfo[]> {
|
|
548
|
-
try {
|
|
549
|
-
return (await psList()).map((proc) => ({
|
|
550
|
-
pid: proc.pid,
|
|
551
|
-
name: proc.name,
|
|
552
|
-
cmdline: proc.cmd ?? '',
|
|
553
|
-
}));
|
|
554
|
-
} catch {
|
|
555
|
-
return [];
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function extractUserDataDirFromCmdline(cmdline: string) {
|
|
560
|
-
const patterns = [
|
|
561
|
-
/--user-data-dir=(?:"([^"]+)"|'([^']+)'|(.+?))(?=\s--[a-zA-Z]|$)/i,
|
|
562
|
-
/--user-data-dir\s+(?:"([^"]+)"|'([^']+)'|(.+?))(?=\s--[a-zA-Z]|$)/i,
|
|
563
|
-
];
|
|
564
|
-
|
|
565
|
-
for (const pattern of patterns) {
|
|
566
|
-
const match = cmdline.match(pattern);
|
|
567
|
-
if (!match) {
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const value = match[1] || match[2] || match[3] || null;
|
|
572
|
-
return value ? value.trim() : null;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return null;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function extractRemoteDebuggingPortFromCmdline(cmdline: string) {
|
|
579
|
-
const patterns = [
|
|
580
|
-
/--remote-debugging-port=(\d+)/i,
|
|
581
|
-
/--remote-debugging-port\s+(\d+)/i,
|
|
582
|
-
];
|
|
583
|
-
|
|
584
|
-
for (const pattern of patterns) {
|
|
585
|
-
const match = cmdline.match(pattern);
|
|
586
|
-
if (match) {
|
|
587
|
-
return Number(match[1]);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async function findPidsUsingFiles(filePaths: string[]) {
|
|
595
|
-
if (process.platform === 'win32') {
|
|
596
|
-
return [];
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
if (!commandExists('lsof')) {
|
|
600
|
-
return [];
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
const stdout = await execFileAsync('lsof', ['-F', 'p', '--', ...filePaths], {
|
|
605
|
-
encoding: 'utf8',
|
|
606
|
-
maxBuffer: 1024 * 1024,
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
return unique(
|
|
610
|
-
stdout.stdout
|
|
611
|
-
.split(/\r?\n/)
|
|
612
|
-
.flatMap((line) => (line.startsWith('p') ? [Number(line.slice(1))] : []))
|
|
613
|
-
.filter((pid) => Number.isInteger(pid) && pid > 0),
|
|
614
|
-
);
|
|
615
|
-
} catch {
|
|
616
|
-
return [];
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
export async function findListeningPortsByPid(pid: number) {
|
|
621
|
-
try {
|
|
622
|
-
const ports = await portToPid(pid, 'tcp');
|
|
623
|
-
return unique(ports.filter((port) => Number.isInteger(port) && port > 0));
|
|
624
|
-
} catch {
|
|
625
|
-
return [];
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
function lockFilesFor(userDataDir: string) {
|
|
630
|
-
return [path.join(userDataDir, 'SingletonLock'), path.join(userDataDir, 'SingletonCookie'), path.join(userDataDir, 'SingletonSocket')]
|
|
631
|
-
.filter((filePath) => exists(filePath));
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function processMatchesBrowser(spec: BrowserSpec, executablePath: string, userDataDir: string, proc: ProcessInfo) {
|
|
635
|
-
if (!basenameMatches(proc.name, spec.processNames) && !proc.cmdline.includes(executablePath)) {
|
|
636
|
-
return false;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const cmdlineUserDataDir = extractUserDataDirFromCmdline(proc.cmdline);
|
|
640
|
-
if (!cmdlineUserDataDir) {
|
|
641
|
-
return normalizePath(userDataDir) === normalizePath(spec.userDataDir);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
return normalizePath(cmdlineUserDataDir) === normalizePath(userDataDir);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function pickPrimaryProcess(processes: ProcessInfo[]) {
|
|
648
|
-
return (
|
|
649
|
-
processes.find((proc) => !proc.cmdline.includes('--type=')) ??
|
|
650
|
-
processes.find((proc) => proc.cmdline.includes('--remote-debugging-port=')) ??
|
|
651
|
-
processes[0] ??
|
|
652
|
-
null
|
|
653
|
-
);
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
export function removeLockFiles(lockFiles: string[]) {
|
|
657
|
-
for (const filePath of lockFiles) {
|
|
658
|
-
try {
|
|
659
|
-
fs.rmSync(filePath, { force: true });
|
|
660
|
-
} catch {
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
export function toBrowserInstanceInfo(instance: InspectedBrowserInstance): BrowserInstanceInfo {
|
|
666
|
-
return {
|
|
667
|
-
type: instance.type,
|
|
668
|
-
name: instance.name,
|
|
669
|
-
executablePath: instance.executablePath,
|
|
670
|
-
userDataDir: instance.userDataDir,
|
|
671
|
-
exists: instance.exists,
|
|
672
|
-
inUse: instance.inUse,
|
|
673
|
-
pid: instance.pid,
|
|
674
|
-
lockFiles: instance.lockFiles,
|
|
675
|
-
matchedProcess: instance.matchedProcess,
|
|
676
|
-
staleLock: instance.staleLock,
|
|
677
|
-
remotePort: instance.remotePort,
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
export async function inspectBrowserInstance(spec: BrowserSpec, executablePath?: string, userDataDir?: string) {
|
|
682
|
-
const resolvedExecutablePath = executablePath || resolveExecutablePath(spec) || spec.executableCandidates[0] || spec.displayName;
|
|
683
|
-
const resolvedUserDataDir = userDataDir ? path.resolve(userDataDir) : spec.userDataDir;
|
|
684
|
-
const lockFiles = lockFilesFor(resolvedUserDataDir);
|
|
685
|
-
const processes = await listProcesses();
|
|
686
|
-
const matchedProcesses = processes.filter((proc) => processMatchesBrowser(spec, resolvedExecutablePath, resolvedUserDataDir, proc));
|
|
687
|
-
const primaryProcess = pickPrimaryProcess(matchedProcesses);
|
|
688
|
-
const pids = unique([
|
|
689
|
-
...matchedProcesses.map((proc) => proc.pid),
|
|
690
|
-
...(lockFiles.length > 0 ? await findPidsUsingFiles(lockFiles) : []),
|
|
691
|
-
]);
|
|
692
|
-
const listeningPorts = unique((await Promise.all(pids.map((pid) => findListeningPortsByPid(pid)))).flat());
|
|
693
|
-
const cmdlineRemotePort = primaryProcess ? extractRemoteDebuggingPortFromCmdline(primaryProcess.cmdline) : null;
|
|
694
|
-
const remotePort = cmdlineRemotePort ?? listeningPorts[0] ?? null;
|
|
695
|
-
|
|
696
|
-
return {
|
|
697
|
-
type: spec.type,
|
|
698
|
-
name: spec.type,
|
|
699
|
-
executablePath: resolvedExecutablePath,
|
|
700
|
-
userDataDir: resolvedUserDataDir,
|
|
701
|
-
exists: exists(resolvedUserDataDir),
|
|
702
|
-
inUse: pids.length > 0,
|
|
703
|
-
pid: primaryProcess?.pid ?? pids[0] ?? null,
|
|
704
|
-
pids,
|
|
705
|
-
lockFiles,
|
|
706
|
-
matchedProcess: primaryProcess,
|
|
707
|
-
matchedProcesses,
|
|
708
|
-
staleLock: lockFiles.length > 0 && pids.length === 0,
|
|
709
|
-
remotePort,
|
|
710
|
-
} satisfies InspectedBrowserInstance;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
export function isDefaultInstanceName(name: string) {
|
|
714
|
-
return browserSpecs().some((spec) => spec.type === name);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
export function findSpec(browserType: BrowserType) {
|
|
718
|
-
const spec = browserSpecs().find((item) => item.type === browserType);
|
|
719
|
-
if (!spec) {
|
|
720
|
-
throw new Error(`Unsupported browser type: ${browserType}`);
|
|
721
|
-
}
|
|
722
|
-
return spec;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export async function getPidsListeningOnPort(port: number) {
|
|
726
|
-
try {
|
|
727
|
-
return unique(await portToPid(port, 'tcp'));
|
|
728
|
-
} catch {
|
|
729
|
-
return [];
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function isPidAlive(pid: number) {
|
|
734
|
-
try {
|
|
735
|
-
process.kill(pid, 0);
|
|
736
|
-
return true;
|
|
737
|
-
} catch {
|
|
738
|
-
return false;
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
async function closePidGracefully(pid: number) {
|
|
743
|
-
try {
|
|
744
|
-
process.kill(pid, 'SIGTERM');
|
|
745
|
-
return true;
|
|
746
|
-
} catch {
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
async function killPidForce(pid: number) {
|
|
752
|
-
try {
|
|
753
|
-
process.kill(pid, 'SIGKILL');
|
|
754
|
-
return true;
|
|
755
|
-
} catch {
|
|
756
|
-
return false;
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
async function waitForPidExit(pid: number, timeoutMs = 8_000, intervalMs = 250) {
|
|
761
|
-
const deadline = Date.now() + timeoutMs;
|
|
762
|
-
while (Date.now() < deadline) {
|
|
763
|
-
if (!isPidAlive(pid)) {
|
|
764
|
-
return true;
|
|
765
|
-
}
|
|
766
|
-
await sleep(intervalMs);
|
|
767
|
-
}
|
|
768
|
-
return !isPidAlive(pid);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
export async function waitForPortToClose(port: number, timeoutMs = 8_000) {
|
|
772
|
-
const deadline = Date.now() + timeoutMs;
|
|
773
|
-
while (Date.now() < deadline) {
|
|
774
|
-
if ((await getPidsListeningOnPort(port)).length === 0) {
|
|
775
|
-
return true;
|
|
776
|
-
}
|
|
777
|
-
await sleep(250);
|
|
778
|
-
}
|
|
779
|
-
return (await getPidsListeningOnPort(port)).length === 0;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
export async function getCdpWebSocketUrl(port: number) {
|
|
783
|
-
try {
|
|
784
|
-
const response = await fetch(`http://127.0.0.1:${port}/json/version`);
|
|
785
|
-
if (!response.ok) {
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
const data = await response.json() as { webSocketDebuggerUrl?: string };
|
|
789
|
-
return data.webSocketDebuggerUrl || null;
|
|
790
|
-
} catch {
|
|
791
|
-
return null;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
export async function waitForCdpReady(port: number, timeoutMs = 15_000) {
|
|
796
|
-
const deadline = Date.now() + timeoutMs;
|
|
797
|
-
while (Date.now() < deadline) {
|
|
798
|
-
const wsUrl = await getCdpWebSocketUrl(port);
|
|
799
|
-
if (wsUrl) {
|
|
800
|
-
return wsUrl;
|
|
801
|
-
}
|
|
802
|
-
await sleep(250);
|
|
803
|
-
}
|
|
804
|
-
return null;
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
async function waitForPortBound(port: number, timeoutMs = 5_000) {
|
|
808
|
-
const deadline = Date.now() + timeoutMs;
|
|
809
|
-
while (Date.now() < deadline) {
|
|
810
|
-
await new Promise<void>((resolve) => {
|
|
811
|
-
const socket = net.connect({ host: '127.0.0.1', port }, () => {
|
|
812
|
-
socket.end();
|
|
813
|
-
resolve();
|
|
814
|
-
});
|
|
815
|
-
socket.on('error', () => resolve());
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
if ((await getPidsListeningOnPort(port)).length > 0) {
|
|
819
|
-
return true;
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
await sleep(200);
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
return false;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
async function loadPlaywrightChromium() {
|
|
829
|
-
const playwright = await import('playwright-core');
|
|
830
|
-
return playwright.chromium;
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
export async function connectOverCdp(endpoint: number | string) {
|
|
834
|
-
const chromium = await loadPlaywrightChromium();
|
|
835
|
-
const url = typeof endpoint === 'number' ? `http://127.0.0.1:${endpoint}` : endpoint;
|
|
836
|
-
const browser = await chromium.connectOverCDP(url);
|
|
837
|
-
return browser;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
export function ensureDir(dirPath: string) {
|
|
841
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
842
|
-
return dirPath;
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
export function resolveUserDataDir(spec: BrowserSpec, options: ConnectBrowserOptions) {
|
|
846
|
-
return ensureDir(options.userDataDir ? path.resolve(options.userDataDir) : spec.userDataDir);
|
|
847
|
-
}
|
|
848
|
-
|
|
849
|
-
export function isBrowserProcess(spec: BrowserSpec, proc: ProcessInfo) {
|
|
850
|
-
return basenameMatches(proc.name, spec.processNames);
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
export async function closeProcessesByPid(pids: number[], timeoutMs: number) {
|
|
854
|
-
const closedPids: number[] = [];
|
|
855
|
-
|
|
856
|
-
for (const pid of unique(pids.filter((value) => Number.isInteger(value) && value > 0))) {
|
|
857
|
-
if (!isPidAlive(pid)) {
|
|
858
|
-
closedPids.push(pid);
|
|
859
|
-
continue;
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
await closePidGracefully(pid);
|
|
863
|
-
if (!(await waitForPidExit(pid, timeoutMs))) {
|
|
864
|
-
await killPidForce(pid);
|
|
865
|
-
await waitForPidExit(pid, timeoutMs);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
if (!isPidAlive(pid)) {
|
|
869
|
-
closedPids.push(pid);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
return closedPids;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
export async function launchBrowser(spec: BrowserSpec, executablePath: string, userDataDir: string, port: number, startupUrl: string) {
|
|
877
|
-
const browserProcess = spawn(
|
|
878
|
-
executablePath,
|
|
879
|
-
[
|
|
880
|
-
`--remote-debugging-port=${port}`,
|
|
881
|
-
`--user-data-dir=${userDataDir}`,
|
|
882
|
-
'--no-first-run',
|
|
883
|
-
'--no-default-browser-check',
|
|
884
|
-
startupUrl,
|
|
885
|
-
],
|
|
886
|
-
{
|
|
887
|
-
detached: process.platform !== 'win32',
|
|
888
|
-
stdio: 'ignore',
|
|
889
|
-
windowsHide: true,
|
|
890
|
-
env: process.env,
|
|
891
|
-
},
|
|
892
|
-
);
|
|
893
|
-
|
|
894
|
-
browserProcess.unref();
|
|
895
|
-
await waitForPortBound(port, 5_000);
|
|
896
|
-
const wsUrl = await waitForCdpReady(port);
|
|
897
|
-
if (!wsUrl) {
|
|
898
|
-
throw new Error(`Browser started but CDP did not become ready on port ${port}`);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
return {
|
|
902
|
-
pid: browserProcess.pid ?? null,
|
|
903
|
-
wsUrl,
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
|