Skip to content

Commit c4a4efa

Browse files
authored
chore(cli): scope daemon by workspace (#39144)
1 parent 08752e9 commit c4a4efa

File tree

12 files changed

+180
-99
lines changed

12 files changed

+180
-99
lines changed

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

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,14 @@ import type { AnyCommandSchema } from './command';
2323

2424
const open = declareCommand({
2525
name: 'open',
26-
description: 'Open browser',
26+
description: 'Open the browser',
2727
category: 'core',
2828
args: z.object({
2929
url: z.string().optional().describe('The URL to navigate to'),
3030
}),
3131
options: z.object({
3232
browser: z.string().optional().describe('Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.'),
33-
config: z.string().optional().describe('Path to the configuration file'),
33+
config: z.string().optional().describe('Path to the configuration file, defaults to .playwright/cli.config.json'),
3434
extension: z.boolean().optional().describe('Connect to browser extension'),
3535
headed: z.boolean().optional().describe('Run browser in headed mode'),
3636
persistent: z.boolean().optional().describe('Use persistent browser profile'),
@@ -42,7 +42,7 @@ const open = declareCommand({
4242

4343
const close = declareCommand({
4444
name: 'close',
45-
description: 'Close the page',
45+
description: 'Close the browser',
4646
category: 'core',
4747
args: z.object({}),
4848
toolName: '',
@@ -771,7 +771,7 @@ const sessionCloseAll = declareCommand({
771771
});
772772

773773
const killAll = declareCommand({
774-
name: 'kill-all',
774+
name: 'session-kill-all',
775775
description: 'Forcefully kill all daemon processes (for stale/zombie processes)',
776776
category: 'session',
777777
toolName: '',
@@ -796,22 +796,25 @@ const configPrint = declareCommand({
796796
});
797797

798798
const install = declareCommand({
799-
name: 'install-browser',
800-
description: 'Install browser',
799+
name: 'install',
800+
description: 'Initialize workspace',
801801
category: 'install',
802+
args: z.object({}),
802803
options: z.object({
803-
browser: z.string().optional().describe('Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge'),
804+
skills: z.boolean().optional().describe('Install skills for Claude / GitHub Copilot'),
804805
}),
805-
toolName: 'browser_install',
806+
toolName: '',
806807
toolParams: () => ({}),
807808
});
808809

809-
const installSkills = declareCommand({
810-
name: 'install-skills',
811-
description: 'Install Claude / GitGub Copilot skills to the local workspace',
810+
const installBrowser = declareCommand({
811+
name: 'install-browser',
812+
description: 'Install browser',
812813
category: 'install',
813-
args: z.object({}),
814-
toolName: '',
814+
options: z.object({
815+
browser: z.string().optional().describe('Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge'),
816+
}),
817+
toolName: 'browser_install',
815818
toolParams: () => ({}),
816819
});
817820

@@ -894,7 +897,7 @@ const commandsArray: AnyCommandSchema[] = [
894897

895898
// install category
896899
install,
897-
installSkills,
900+
installBrowser,
898901

899902
// devtools category
900903
networkRequests,

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

Lines changed: 59 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ export type SessionConfig = {
4747

4848
type ClientInfo = {
4949
version: string;
50-
installationDir: string;
51-
installationDirHash: string;
50+
workspaceDirHash: string;
5251
daemonProfilesDir: string;
5352
};
5453

@@ -249,12 +248,15 @@ to restart the session daemon.`);
249248
console.log(formatWithGap(`- playwright-cli${sessionOption} close`, `# to stop when done.`));
250249
console.log(formatWithGap(`- playwright-cli${sessionOption} open [options]`, `# to reopen with new config.`));
251250
console.log(formatWithGap(`- playwright-cli${sessionOption} delete-data`, `# to delete session data.`));
251+
console.log(`---`);
252+
console.log(``);
252253

253254
// Wait for the socket to become available with retries.
254-
const maxRetries = 50;
255-
const retryDelay = 100; // ms
256-
for (let i = 0; i < maxRetries; i++) {
257-
await new Promise(resolve => setTimeout(resolve, retryDelay));
255+
const retryDelay = [100, 200, 400]; // ms
256+
let totalWaited = 0;
257+
for (let i = 0; i < 10; i++) {
258+
await new Promise(resolve => setTimeout(resolve, retryDelay[i] || 1000));
259+
totalWaited += retryDelay[i] || 1000;
258260
try {
259261
const { socket } = await this._connect();
260262
if (socket)
@@ -268,7 +270,7 @@ to restart the session daemon.`);
268270
const outData = await fs.promises.readFile(outLog, 'utf-8').catch(() => '');
269271
const errData = await fs.promises.readFile(errLog, 'utf-8').catch(() => '');
270272

271-
console.error(`Failed to connect to daemon at ${this._config.socketPath} after ${maxRetries * retryDelay}ms`);
273+
console.error(`Failed to connect to daemon at ${this._config.socketPath} after ${totalWaited}ms`);
272274
if (outData.length)
273275
console.log(outData);
274276
if (errData.length)
@@ -347,11 +349,9 @@ class SessionManager {
347349
const sessionName = this._resolveSessionName(args.session);
348350
const session = this.sessions.get(sessionName);
349351
if (!session) {
350-
const configFromArgs = sessionConfigFromArgs(this.clientInfo, sessionName, args);
351-
const formattedArgs = configToFormattedArgs(configFromArgs.cli);
352-
console.log(`The session '${sessionName}' is not open, please run open first:`);
352+
console.log(`The session '${sessionName}' is not open, please run open first`);
353353
console.log('');
354-
console.log(` playwright-cli${sessionName !== 'default' ? ` --session=${sessionName}` : ''} open ${formattedArgs.join(' ')}`);
354+
console.log(` playwright-cli${sessionName !== 'default' ? ` --session=${sessionName}` : ''} open [params]`);
355355
process.exit(1);
356356
}
357357

@@ -395,24 +395,36 @@ class SessionManager {
395395

396396
function createClientInfo(packageLocation: string): ClientInfo {
397397
const packageJSON = require(packageLocation);
398-
const installationDir = process.env.PLAYWRIGHT_CLI_INSTALLATION_FOR_TEST || packageLocation;
398+
const workspaceDir = findWorkspaceDir(process.cwd()) || packageLocation;
399399
const version = process.env.PLAYWRIGHT_CLI_VERSION_FOR_TEST || packageJSON.version;
400400

401401
const hash = crypto.createHash('sha1');
402-
hash.update(installationDir);
403-
const installationDirHash = hash.digest('hex').substring(0, 16);
402+
hash.update(workspaceDir);
403+
const workspaceDirHash = hash.digest('hex').substring(0, 16);
404404

405405
return {
406406
version,
407-
installationDir,
408-
installationDirHash,
409-
daemonProfilesDir: daemonProfilesDir(installationDirHash),
407+
workspaceDirHash,
408+
daemonProfilesDir: daemonProfilesDir(workspaceDirHash),
410409
};
411410
}
412411

413-
const daemonProfilesDir = (installationDirHash: string) => {
412+
function findWorkspaceDir(startDir: string): string | undefined {
413+
let dir = startDir;
414+
for (let i = 0; i < 10; i++) {
415+
if (fs.existsSync(path.join(dir, '.playwright')))
416+
return dir;
417+
const parentDir = path.dirname(dir);
418+
if (parentDir === dir)
419+
break;
420+
dir = parentDir;
421+
}
422+
return undefined;
423+
}
424+
425+
const daemonProfilesDir = (workspaceDirHash: string) => {
414426
if (process.env.PLAYWRIGHT_DAEMON_SESSION_DIR)
415-
return process.env.PLAYWRIGHT_DAEMON_SESSION_DIR;
427+
return path.join(process.env.PLAYWRIGHT_DAEMON_SESSION_DIR, workspaceDirHash);
416428

417429
let localCacheDir: string | undefined;
418430
if (process.platform === 'linux')
@@ -423,7 +435,7 @@ const daemonProfilesDir = (installationDirHash: string) => {
423435
localCacheDir = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
424436
if (!localCacheDir)
425437
throw new Error('Unsupported platform: ' + process.platform);
426-
return path.join(localCacheDir, 'ms-playwright', 'daemon', installationDirHash);
438+
return path.join(localCacheDir, 'ms-playwright', 'daemon', workspaceDirHash);
427439
};
428440

429441
type GlobalOptions = {
@@ -510,6 +522,8 @@ export async function program(packageLocation: string) {
510522
} else {
511523
const restartMarker = !session.isCompatible() ? ` - v${session.config().version}, please reopen` : '';
512524
console.log(` ${session.name}${restartMarker}`);
525+
const config = session.config();
526+
configToFormattedArgs(config.cli).forEach(arg => console.log(` ${arg}`));
513527
}
514528
}
515529
if (sessions.size === 0)
@@ -525,7 +539,7 @@ export async function program(packageLocation: string) {
525539
case 'delete-data':
526540
await sessionManager.deleteData(args as GlobalOptions);
527541
return;
528-
case 'kill-all':
542+
case 'session-kill-all':
529543
await killAllDaemons();
530544
return;
531545
case 'open':
@@ -534,40 +548,49 @@ export async function program(packageLocation: string) {
534548
case 'close':
535549
await sessionManager.close(args as GlobalOptions);
536550
return;
537-
case 'install-skills':
538-
await installSkills();
551+
case 'install':
552+
await install(args);
539553
return;
540554
default:
541555
await sessionManager.run(args);
542556
}
543557
}
544558

545-
async function installSkills() {
546-
const skillSourceDir = path.join(__dirname, '../../skill');
547-
const skillDestDir = path.join(process.cwd(), '.claude', 'skills', 'playwright-cli');
559+
async function install(args: MinimistArgs) {
560+
const cwd = process.cwd();
548561

549-
if (!fs.existsSync(skillSourceDir)) {
550-
console.error('Skills source directory not found:', skillSourceDir);
551-
process.exit(1);
552-
}
562+
// Create .playwright folder to mark workspace root
563+
const playwrightDir = path.join(cwd, '.playwright');
564+
await fs.promises.mkdir(playwrightDir, { recursive: true });
565+
console.log(`Workspace initialized at ${cwd}`);
566+
567+
if (args.skills) {
568+
const skillSourceDir = path.join(__dirname, '../../skill');
569+
const skillDestDir = path.join(cwd, '.claude', 'skills', 'playwright-cli');
553570

554-
await fs.promises.cp(skillSourceDir, skillDestDir, { recursive: true });
555-
console.log(`Skills installed to ${path.relative(process.cwd(), skillDestDir)}`);
571+
if (!fs.existsSync(skillSourceDir)) {
572+
console.error('Skills source directory not found:', skillSourceDir);
573+
process.exit(1);
574+
}
575+
576+
await fs.promises.cp(skillSourceDir, skillDestDir, { recursive: true });
577+
console.log(`Skills installed to ${path.relative(cwd, skillDestDir)}`);
578+
}
556579
}
557580

558581
function daemonSocketPath(clientInfo: ClientInfo, sessionName: string): string {
559582
const socketName = `${sessionName}.sock`;
560583
if (os.platform() === 'win32')
561-
return `\\\\.\\pipe\\${clientInfo.installationDirHash}-${socketName}`;
584+
return `\\\\.\\pipe\\${clientInfo.workspaceDirHash}-${socketName}`;
562585
const socketsDir = process.env.PLAYWRIGHT_DAEMON_SOCKETS_DIR || path.join(os.tmpdir(), 'playwright-cli');
563-
return path.join(socketsDir, clientInfo.installationDirHash, socketName);
586+
return path.join(socketsDir, clientInfo.workspaceDirHash, socketName);
564587
}
565588

566589
function sessionConfigFromArgs(clientInfo: ClientInfo, sessionName: string, args: MinimistArgs): SessionConfig {
567590
let config = args.config ? path.resolve(args.config) : undefined;
568591
try {
569-
if (!config && fs.existsSync('playwright-cli.json'))
570-
config = path.resolve('playwright-cli.json');
592+
if (!config && fs.existsSync(path.join('.playwright', 'cli.config.json')))
593+
config = path.resolve('.playwright', 'cli.config.json');
571594
} catch {
572595
}
573596

packages/playwright/src/skill/SKILL.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ playwright-cli open --profile=/path/to/profile
175175
# Start with config file
176176
playwright-cli open --config=my-config.json
177177

178-
playwright-cli close # stop the default session
178+
# Close the browser
179+
playwright-cli close
180+
# Delete user data for the default session
181+
playwright-cli delete-data
179182
```
180183
181184
### Sessions
@@ -187,9 +190,10 @@ playwright-cli --session=mysession close # stop a named session
187190
playwright-cli --session=mysession delete-data # delete user data for named session
188191

189192
playwright-cli session-list
190-
playwright-cli session-close-all # stop all sessions
191-
playwright-cli delete-data # delete user data for default session
192-
playwright-cli kill-all # forcefully kill all daemon processes (for stale/zombie processes)
193+
# Close all browsers
194+
playwright-cli session-close-all
195+
# Forcefully kill all browser processes
196+
playwright-cli session-kill-all
193197
```
194198
195199
## Example: Form submission

packages/playwright/src/skill/references/session-management.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ playwright-cli --session=mysession close # stop a named session
4242
playwright-cli session-close-all
4343

4444
# Forcefully kill all daemon processes (for stale/zombie processes)
45-
playwright-cli kill-all
45+
playwright-cli session-kill-all
4646

4747
# Delete session user data (profile directory)
4848
playwright-cli delete-data # delete default session data
@@ -122,7 +122,7 @@ Configure a session with specific settings when opening:
122122

123123
```bash
124124
# Open with config file
125-
playwright-cli open https://example.com --config=playwright-cli.json
125+
playwright-cli open https://example.com --config=.playwright/my-cli.json
126126

127127
# Open with specific browser
128128
playwright-cli open https://example.com --browser=firefox
@@ -158,7 +158,7 @@ playwright-cli --session=scrape close
158158
playwright-cli session-close-all
159159

160160
# If sessions become unresponsive or zombie processes remain
161-
playwright-cli kill-all
161+
playwright-cli session-kill-all
162162
```
163163

164164
### 3. Delete Stale Session Data

tests/mcp/cli-config.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test('context options', async ({ cli, server }, testInfo) => {
3636
},
3737
},
3838
};
39-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(config, null, 2));
39+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(config, null, 2));
4040
await cli('open', server.PREFIX);
4141
const { output } = await cli('eval', 'window.innerWidth + "x" + window.innerHeight');
4242
expect(output).toContain('800x600');
@@ -61,7 +61,7 @@ test('config-print prints merged config from file, env and cli', async ({ cli, s
6161
navigation: 30000,
6262
},
6363
};
64-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(fileConfig, null, 2));
64+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(fileConfig, null, 2));
6565

6666
// Env var overrides navigation timeout (30000 from file → 45000 from env).
6767
const env = { PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION: '45000' };
@@ -95,7 +95,7 @@ test('isolated', async ({ cli, server }, testInfo) => {
9595
isolated: true,
9696
},
9797
};
98-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(config, null, 2));
98+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(config, null, 2));
9999
await cli('open', server.PREFIX);
100100
expect(fs.existsSync(testInfo.outputPath('daemon', 'default-user-data'))).toBe(false);
101101
});

tests/mcp/cli-cookies.spec.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { test, expect } from './cli-fixtures';
2222
test('cookie-list shows no cookies when empty', async ({ cli, server, mcpBrowser }, testInfo) => {
2323
test.skip(mcpBrowser === 'msedge', 'Edge is leaking some internal cookies');
2424
const config = { capabilities: ['storage'] };
25-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(config, null, 2));
25+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(config, null, 2));
2626

2727
await cli('open', server.EMPTY_PAGE);
2828
const { output } = await cli('cookie-list');
@@ -31,7 +31,7 @@ test('cookie-list shows no cookies when empty', async ({ cli, server, mcpBrowser
3131

3232
test('cookie-set and cookie-get', async ({ cli, server }, testInfo) => {
3333
const config = { capabilities: ['storage'] };
34-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(config, null, 2));
34+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(config, null, 2));
3535

3636
await cli('open', server.EMPTY_PAGE);
3737

@@ -45,7 +45,7 @@ test('cookie-set and cookie-get', async ({ cli, server }, testInfo) => {
4545

4646
test('cookie-list shows cookies', async ({ cli, server }, testInfo) => {
4747
const config = { capabilities: ['storage'] };
48-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(config, null, 2));
48+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(config, null, 2));
4949

5050
await cli('open', server.EMPTY_PAGE);
5151
await cli('cookie-set', 'cookie1', 'value1');
@@ -58,7 +58,7 @@ test('cookie-list shows cookies', async ({ cli, server }, testInfo) => {
5858

5959
test('cookie-delete removes cookie', async ({ cli, server }, testInfo) => {
6060
const config = { capabilities: ['storage'] };
61-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(config, null, 2));
61+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(config, null, 2));
6262

6363
await cli('open', server.EMPTY_PAGE);
6464
await cli('cookie-set', 'testCookie', 'testValue');
@@ -73,7 +73,7 @@ test('cookie-delete removes cookie', async ({ cli, server }, testInfo) => {
7373

7474
test('cookie-clear removes all cookies', async ({ cli, server }, testInfo) => {
7575
const config = { capabilities: ['storage'] };
76-
await fs.promises.writeFile(testInfo.outputPath('playwright-cli.json'), JSON.stringify(config, null, 2));
76+
await fs.promises.writeFile(testInfo.outputPath('.playwright', 'cli.config.json'), JSON.stringify(config, null, 2));
7777

7878
await cli('open', server.EMPTY_PAGE);
7979
await cli('cookie-set', 'cookie1', 'value1');

0 commit comments

Comments
 (0)