@@ -43,12 +43,14 @@ export type SessionConfig = {
4343 config ?: string ;
4444 } ;
4545 userDataDirPrefix ?: string ;
46+ workspaceDir ?: string ;
4647} ;
4748
4849type ClientInfo = {
4950 version : string ;
5051 workspaceDirHash : string ;
5152 daemonProfilesDir : string ;
53+ workspaceDir : string | undefined ;
5254} ;
5355
5456class Session {
@@ -308,26 +310,12 @@ class SessionManager {
308310 const sessions = new Map < string , Session > ( ) ;
309311 const files = await fs . promises . readdir ( dir ) . catch ( ( ) => [ ] ) ;
310312 for ( const file of files ) {
313+ if ( ! file . endsWith ( '.session' ) )
314+ continue ;
311315 try {
312- if ( file . endsWith ( '.session' ) ) {
313- const sessionName = path . basename ( file , '.session' ) ;
314- const sessionConfig = await fs . promises . readFile ( path . join ( dir , file ) , 'utf-8' ) . then ( data => JSON . parse ( data ) ) as SessionConfig ;
315- sessions . set ( sessionName , new Session ( clientInfo , sessionName , sessionConfig ) ) ;
316- continue ;
317- }
318-
319- // Legacy session support.
320- if ( file . startsWith ( 'ud-' ) ) {
321- // Session is like ud-<sessionName>-browserName
322- const sessionName = file . split ( '-' ) [ 1 ] ;
323- if ( ! sessions . has ( sessionName ) ) {
324- const sessionConfig = sessionConfigFromArgs ( {
325- ...clientInfo ,
326- version : '0.0.61'
327- } , sessionName , { _ : [ ] } ) ;
328- sessions . set ( sessionName , new Session ( clientInfo , sessionName , sessionConfig ) ) ;
329- }
330- }
316+ const sessionName = path . basename ( file , '.session' ) ;
317+ const sessionConfig = await fs . promises . readFile ( path . join ( dir , file ) , 'utf-8' ) . then ( data => JSON . parse ( data ) ) as SessionConfig ;
318+ sessions . set ( sessionName , new Session ( clientInfo , sessionName , sessionConfig ) ) ;
331319 } catch {
332320 }
333321 }
@@ -395,15 +383,16 @@ class SessionManager {
395383
396384function createClientInfo ( packageLocation : string ) : ClientInfo {
397385 const packageJSON = require ( packageLocation ) ;
398- const workspaceDir = findWorkspaceDir ( process . cwd ( ) ) || packageLocation ;
386+ const workspaceDir = findWorkspaceDir ( process . cwd ( ) ) ;
399387 const version = process . env . PLAYWRIGHT_CLI_VERSION_FOR_TEST || packageJSON . version ;
400388
401389 const hash = crypto . createHash ( 'sha1' ) ;
402- hash . update ( workspaceDir ) ;
390+ hash . update ( workspaceDir || packageLocation ) ;
403391 const workspaceDirHash = hash . digest ( 'hex' ) . substring ( 0 , 16 ) ;
404392
405393 return {
406394 version,
395+ workspaceDir,
407396 workspaceDirHash,
408397 daemonProfilesDir : daemonProfilesDir ( workspaceDirHash ) ,
409398 } ;
@@ -422,9 +411,9 @@ function findWorkspaceDir(startDir: string): string | undefined {
422411 return undefined ;
423412}
424413
425- const daemonProfilesDir = ( workspaceDirHash : string ) => {
414+ const baseDaemonDir = ( ( ) => {
426415 if ( process . env . PLAYWRIGHT_DAEMON_SESSION_DIR )
427- return path . join ( process . env . PLAYWRIGHT_DAEMON_SESSION_DIR , workspaceDirHash ) ;
416+ return process . env . PLAYWRIGHT_DAEMON_SESSION_DIR ;
428417
429418 let localCacheDir : string | undefined ;
430419 if ( process . platform === 'linux' )
@@ -435,7 +424,11 @@ const daemonProfilesDir = (workspaceDirHash: string) => {
435424 localCacheDir = process . env . LOCALAPPDATA || path . join ( os . homedir ( ) , 'AppData' , 'Local' ) ;
436425 if ( ! localCacheDir )
437426 throw new Error ( 'Unsupported platform: ' + process . platform ) ;
438- return path . join ( localCacheDir , 'ms-playwright' , 'daemon' , workspaceDirHash ) ;
427+ return path . join ( localCacheDir , 'ms-playwright' , 'daemon' ) ;
428+ } ) ( ) ;
429+
430+ const daemonProfilesDir = ( workspaceDirHash : string ) => {
431+ return path . join ( baseDaemonDir , workspaceDirHash ) ;
439432} ;
440433
441434type GlobalOptions = {
@@ -465,7 +458,8 @@ const globalOptions: (keyof (GlobalOptions & OpenOptions))[] = [
465458 'version' ,
466459] ;
467460
468- const booleanOptions : ( keyof ( GlobalOptions & OpenOptions ) ) [ ] = [
461+ const booleanOptions : ( keyof ( GlobalOptions & OpenOptions & { all ?: boolean } ) ) [ ] = [
462+ 'all' ,
469463 'help' ,
470464 'version' ,
471465 'extension' ,
@@ -512,22 +506,10 @@ export async function program(packageLocation: string) {
512506
513507 switch ( commandName ) {
514508 case 'session-list' : {
515- const sessions = sessionManager . sessions ;
516- console . log ( 'Sessions:' ) ;
517- for ( const session of sessions . values ( ) ) {
518- const canConnect = await session . canConnect ( ) ;
519- if ( ! canConnect ) {
520- console . log ( ` ${ session . name } is stale, removing` ) ;
521- await session . deleteSession ( ) ;
522- } else {
523- const restartMarker = ! session . isCompatible ( ) ? ` - v${ session . config ( ) . version } , please reopen` : '' ;
524- console . log ( ` ${ session . name } ${ restartMarker } ` ) ;
525- const config = session . config ( ) ;
526- configToFormattedArgs ( config . cli ) . forEach ( arg => console . log ( ` ${ arg } ` ) ) ;
527- }
528- }
529- if ( sessions . size === 0 )
530- console . log ( ' (no sessions)' ) ;
509+ if ( args . all )
510+ await listAllSessions ( clientInfo ) ;
511+ else
512+ await listSessions ( sessionManager ) ;
531513 return ;
532514 }
533515 case 'session-close-all' : {
@@ -609,6 +591,7 @@ function sessionConfigFromArgs(clientInfo: ClientInfo, sessionName: string, args
609591 config,
610592 } ,
611593 userDataDirPrefix : path . resolve ( clientInfo . daemonProfilesDir , `ud-${ sessionName } ` ) ,
594+ workspaceDir : clientInfo . workspaceDir ,
612595 } ;
613596}
614597
@@ -677,6 +660,89 @@ async function killAllDaemons(): Promise<void> {
677660 console . log ( `Killed ${ killed } daemon process${ killed === 1 ? '' : 'es' } .` ) ;
678661}
679662
663+ async function listSessions ( sessionManager : SessionManager ) : Promise < void > {
664+ const sessions = sessionManager . sessions ;
665+ console . log ( 'Sessions:' ) ;
666+ for ( const session of sessions . values ( ) ) {
667+ const canConnect = await session . canConnect ( ) ;
668+ if ( ! canConnect ) {
669+ console . log ( ` ${ session . name } is stale, removing` ) ;
670+ await session . deleteSession ( ) ;
671+ } else {
672+ const restartMarker = ! session . isCompatible ( ) ? ` - v${ session . config ( ) . version } , please reopen` : '' ;
673+ console . log ( ` ${ session . name } ${ restartMarker } ` ) ;
674+ const config = session . config ( ) ;
675+ configToFormattedArgs ( config . cli ) . forEach ( arg => console . log ( ` ${ arg } ` ) ) ;
676+ }
677+ }
678+ if ( sessions . size === 0 )
679+ console . log ( ' (no sessions)' ) ;
680+ }
681+
682+ async function listAllSessions ( clientInfo : ClientInfo ) : Promise < void > {
683+ const hashes = await fs . promises . readdir ( baseDaemonDir ) . catch ( ( ) => [ ] ) ;
684+
685+ // Group sessions by workspace folder
686+ const sessionsByWorkspace = new Map < string , { name : string , config : SessionConfig , canConnect : boolean , isCompatible : boolean } [ ] > ( ) ;
687+
688+ for ( const hash of hashes ) {
689+ const hashDir = path . join ( baseDaemonDir , hash ) ;
690+ const stat = await fs . promises . stat ( hashDir ) . catch ( ( ) => null ) ;
691+ if ( ! stat ?. isDirectory ( ) )
692+ continue ;
693+
694+ const files = await fs . promises . readdir ( hashDir ) . catch ( ( ) => [ ] ) ;
695+ for ( const file of files ) {
696+ if ( ! file . endsWith ( '.session' ) )
697+ continue ;
698+ try {
699+ const sessionName = path . basename ( file , '.session' ) ;
700+ const sessionConfig = await fs . promises . readFile ( path . join ( hashDir , file ) , 'utf-8' ) . then ( data => JSON . parse ( data ) ) as SessionConfig ;
701+ const session = new Session ( clientInfo , sessionName , sessionConfig ) ;
702+ const canConnect = await session . canConnect ( ) ;
703+ const isCompatible = session . isCompatible ( ) ;
704+
705+ // Use workspace folder from config, or empty string if not set (installation folder case)
706+ const workspaceKey = sessionConfig . workspaceDir || '' ;
707+ if ( ! sessionsByWorkspace . has ( workspaceKey ) )
708+ sessionsByWorkspace . set ( workspaceKey , [ ] ) ;
709+ sessionsByWorkspace . get ( workspaceKey ) ! . push ( { name : sessionName , config : sessionConfig , canConnect, isCompatible } ) ;
710+ } catch {
711+ }
712+ }
713+ }
714+
715+ if ( sessionsByWorkspace . size === 0 ) {
716+ console . log ( 'No sessions found.' ) ;
717+ return ;
718+ }
719+
720+ // Sort workspace keys: empty string (no workspace) last, others alphabetically
721+ const sortedWorkspaces = [ ...sessionsByWorkspace . keys ( ) ] . sort ( ( a , b ) => {
722+ if ( a === '' && b !== '' )
723+ return 1 ;
724+ if ( a !== '' && b === '' )
725+ return - 1 ;
726+ return a . localeCompare ( b ) ;
727+ } ) ;
728+
729+ for ( const workspace of sortedWorkspaces ) {
730+ const sessions = sessionsByWorkspace . get ( workspace ) ! ;
731+ // Only print workspace folder if it's set
732+ if ( workspace )
733+ console . log ( `${ workspace } :` ) ;
734+ for ( const { name, config, canConnect, isCompatible } of sessions ) {
735+ if ( ! canConnect ) {
736+ console . log ( ` ${ name } (stale)` ) ;
737+ } else {
738+ const restartMarker = ! isCompatible ? ` - v${ config . version } , please reopen` : '' ;
739+ console . log ( ` ${ name } ${ restartMarker } ` ) ;
740+ configToFormattedArgs ( config . cli ) . forEach ( arg => console . log ( ` ${ arg } ` ) ) ;
741+ }
742+ }
743+ }
744+ }
745+
680746function formatWithGap ( prefix : string , text : string , threshold : number = 40 ) {
681747 const indent = Math . max ( 1 , threshold - prefix . length ) ;
682748 return prefix + ' ' . repeat ( indent ) + text ;
0 commit comments