Skip to content

Commit 5db56c1

Browse files
authored
chore(cli): do not pick legacy sessions, list all sessions (#39146)
1 parent 429a39b commit 5db56c1

File tree

3 files changed

+137
-41
lines changed

3 files changed

+137
-41
lines changed

packages/playwright/src/mcp/terminal/commands.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,9 @@ const sessionList = declareCommand({
758758
description: 'List all sessions',
759759
category: 'session',
760760
args: z.object({}),
761+
options: z.object({
762+
all: z.boolean().optional().describe('List all sessions across all workspaces'),
763+
}),
761764
toolName: '',
762765
toolParams: () => ({}),
763766
});

packages/playwright/src/mcp/terminal/program.ts

Lines changed: 107 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,14 @@ export type SessionConfig = {
4343
config?: string;
4444
};
4545
userDataDirPrefix?: string;
46+
workspaceDir?: string;
4647
};
4748

4849
type ClientInfo = {
4950
version: string;
5051
workspaceDirHash: string;
5152
daemonProfilesDir: string;
53+
workspaceDir: string | undefined;
5254
};
5355

5456
class 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

396384
function 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

441434
type 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+
680746
function formatWithGap(prefix: string, text: string, threshold: number = 40) {
681747
const indent = Math.max(1, threshold - prefix.length);
682748
return prefix + ' '.repeat(indent) + text;

tests/mcp/cli-session.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,30 @@ test('workspace isolation - sessions in different workspaces are isolated', asyn
168168

169169
await cli('close', { cwd: workspace2 });
170170
});
171+
172+
test('session-list --all lists sessions from all workspaces', async ({ cli, server }, testInfo) => {
173+
// Create two separate workspaces with their own daemon dirs
174+
const workspace1 = testInfo.outputPath('workspace1');
175+
const workspace2 = testInfo.outputPath('workspace2');
176+
await fs.promises.mkdir(workspace1, { recursive: true });
177+
await fs.promises.mkdir(workspace2, { recursive: true });
178+
179+
await cli('install', { cwd: workspace1 });
180+
await cli('install', { cwd: workspace2 });
181+
182+
// Open sessions in both workspaces
183+
await cli('--session=session1', 'open', server.HELLO_WORLD, { cwd: workspace1 });
184+
await cli('--session=session2', 'open', server.HELLO_WORLD, { cwd: workspace2 });
185+
186+
// List all sessions from workspace1
187+
const { output: allList } = await cli('session-list', '--all', { cwd: workspace1 });
188+
189+
// Should include both workspace folders and sessions
190+
expect(allList).toContain(workspace1);
191+
expect(allList).toContain(workspace2);
192+
expect(allList).toContain('session1');
193+
expect(allList).toContain('session2');
194+
195+
await cli('--session=session1', 'close', { cwd: workspace1 });
196+
await cli('--session=session2', 'close', { cwd: workspace2 });
197+
});

0 commit comments

Comments
 (0)