@@ -17,6 +17,7 @@ import type {
1717} from './third_party/index.js' ;
1818import { puppeteer } from './third_party/index.js' ;
1919import { applyGhostMode , type GhostModeConfig } from './ghost-mode.js' ;
20+ import type { ResolvedProfile } from './config/profiles.js' ;
2021
2122let browser : Browser | undefined ;
2223
@@ -251,3 +252,113 @@ export async function ensureBrowserLaunched(
251252}
252253
253254export type Channel = 'stable' | 'canary' | 'beta' | 'dev' ;
255+
256+ // --- Profile-based browser management ---
257+
258+ const profileBrowsers = new Map < string , Browser > ( ) ;
259+
260+ /**
261+ * Launch or connect a browser for a ResolvedProfile.
262+ *
263+ * - 'managed' profiles launch a new Chrome via `launch()`.
264+ * - 'existing-session' profiles connect to a running Chrome via `ensureBrowserConnected()`.
265+ *
266+ * Returns the Browser instance and caches it by profile name.
267+ * If a connected browser already exists for the profile, returns it immediately.
268+ */
269+ export async function ensureBrowserForProfile (
270+ profile : ResolvedProfile ,
271+ ghostMode ?: Partial < GhostModeConfig > ,
272+ ) : Promise < Browser > {
273+ const existing = profileBrowsers . get ( profile . name ) ;
274+ if ( existing ?. connected ) {
275+ return existing ;
276+ }
277+
278+ let instance : Browser ;
279+
280+ if ( profile . driver === 'managed' ) {
281+ const userDataDir =
282+ profile . userDataDir ??
283+ path . join (
284+ os . homedir ( ) ,
285+ '.boss-ghost' ,
286+ 'profiles' ,
287+ profile . name ,
288+ 'chrome-data' ,
289+ ) ;
290+
291+ const args : string [ ] = [ ...profile . extraArgs ] ;
292+ if ( profile . cdpPort ) {
293+ args . push ( `--remote-debugging-port=${ profile . cdpPort } ` ) ;
294+ }
295+
296+ instance = await launch ( {
297+ headless : profile . headless ,
298+ channel : profile . channel ,
299+ executablePath : profile . executablePath ,
300+ userDataDir,
301+ args,
302+ isolated : false ,
303+ devtools : false ,
304+ ghostMode,
305+ } ) ;
306+ } else {
307+ // existing-session: connect to a running browser
308+ if ( profile . cdpUrl ) {
309+ instance = await ensureBrowserConnected ( {
310+ wsEndpoint : profile . cdpUrl ,
311+ devtools : false ,
312+ } ) ;
313+ } else {
314+ instance = await ensureBrowserConnected ( {
315+ browserURL : `http://127.0.0.1:${ profile . cdpPort } ` ,
316+ devtools : false ,
317+ } ) ;
318+ }
319+
320+ // Apply ghost mode to connected browsers manually since launch() won't do it
321+ if ( ghostMode ) {
322+ await applyGhostMode ( instance , ghostMode ) ;
323+ logger ( 'Ghost Mode applied to connected profile "%s"' , profile . name ) ;
324+ }
325+ }
326+
327+ profileBrowsers . set ( profile . name , instance ) ;
328+ logger ( 'Browser ready for profile "%s" (driver=%s)' , profile . name , profile . driver ) ;
329+ return instance ;
330+ }
331+
332+ /**
333+ * Retrieve a cached browser instance by profile name.
334+ * Returns undefined if no browser exists or it has disconnected.
335+ */
336+ export function getBrowserForProfile ( name : string ) : Browser | undefined {
337+ const instance = profileBrowsers . get ( name ) ;
338+ if ( instance && ! instance . connected ) {
339+ profileBrowsers . delete ( name ) ;
340+ return undefined ;
341+ }
342+ return instance ;
343+ }
344+
345+ /**
346+ * Close/disconnect a profile's browser and remove it from the cache.
347+ */
348+ export async function closeBrowserForProfile ( name : string ) : Promise < void > {
349+ const instance = profileBrowsers . get ( name ) ;
350+ if ( ! instance ) {
351+ return ;
352+ }
353+
354+ profileBrowsers . delete ( name ) ;
355+
356+ if ( instance . connected ) {
357+ try {
358+ await instance . close ( ) ;
359+ logger ( 'Closed browser for profile "%s"' , name ) ;
360+ } catch ( err ) {
361+ logger ( 'Error closing browser for profile "%s": %s' , name , err ) ;
362+ }
363+ }
364+ }
0 commit comments