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