From 1af5aac0cc456be046e464ba7b52e70f4f680e43 Mon Sep 17 00:00:00 2001 From: "Tommy D. Rossi" Date: Sat, 24 Jan 2026 12:57:21 +0100 Subject: [PATCH] feat: add session management CLI commands with @xmorse/cac - Replace cac with @xmorse/cac for space-separated commands - Add 'playwriter session new' to get new session ID - Add 'playwriter session list' to show sessions and state keys - Add 'playwriter session reset ' to reset a session - Update SKILL.md with new CLI docs and multiline examples - Put -s flag before -e in all examples --- playwriter/package.json | 3 +- playwriter/src/cli.ts | 103 +++++++++++++++++++++++++++---------- playwriter/src/executor.ts | 14 ++++- pnpm-lock.yaml | 12 +++-- skills/playwriter/SKILL.md | 63 ++++++++++++++++++----- 5 files changed, 147 insertions(+), 48 deletions(-) diff --git a/playwriter/package.json b/playwriter/package.json index 943e05d..9f706d7 100644 --- a/playwriter/package.json +++ b/playwriter/package.json @@ -10,6 +10,7 @@ "build": "rm -rf dist *.tsbuildinfo && mkdir dist && bun scripts/build-selector-generator.ts && bun scripts/build-bippy.ts && tsc && bun scripts/build-resources.ts", "prepublishOnly": "pnpm build", "watch": "tsc -w", + "cli": "vite-node src/cli.ts", "typecheck": "tsc --noEmit", "mcp": "vite-node src/mcp.ts", "test": "vitest run -u", @@ -43,7 +44,7 @@ "@hono/node-server": "^1.19.6", "@hono/node-ws": "^1.2.0", "@modelcontextprotocol/sdk": "^1.25.2", - "cac": "^6.7.14", + "@xmorse/cac": "^6.0.7", "diff": "^8.0.2", "hono": "^4.10.6", "kill-port-process": "^3.2.1", diff --git a/playwriter/src/cli.ts b/playwriter/src/cli.ts index dda96f6..c6dc2e4 100644 --- a/playwriter/src/cli.ts +++ b/playwriter/src/cli.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { cac } from 'cac' +import { cac } from '@xmorse/cac' import { VERSION, LOG_FILE_PATH } from './utils.js' import { ensureRelayServer, RELAY_PORT } from './relay-client.js' @@ -10,9 +10,9 @@ cli .command('', 'Start the MCP server or controls the browser with -e') .option('--host ', 'Remote relay server host to connect to (or use PLAYWRITER_HOST env var)') .option('--token ', 'Authentication token (or use PLAYWRITER_TOKEN env var)') + .option('-s, --session ', 'Session ID (required for -e, get one with `playwriter session new`)') .option('-e, --eval ', 'Execute JavaScript code and exit, read https://playwriter.dev/SKILL.md for usage') .option('--timeout ', 'Execution timeout in milliseconds', { default: 5000 }) - .option('-s, --session ', 'Session name (required for -e)') .action(async (options: { host?: string; token?: string; eval?: string; timeout?: number; session?: string }) => { // If -e flag is provided, execute code via relay server if (options.eval) { @@ -34,6 +34,11 @@ cli }) }) +async function getServerUrl(host?: string): Promise { + const serverHost = host || process.env.PLAYWRITER_HOST || '127.0.0.1' + return `http://${serverHost}:${RELAY_PORT}` +} + async function executeCode(options: { code: string timeout: number @@ -45,9 +50,7 @@ async function executeCode(options: { const cwd = process.cwd() const sessionId = options.sessionId || process.env.PLAYWRITER_SESSION - // Determine server URL - const serverHost = host || process.env.PLAYWRITER_HOST || '127.0.0.1' - const serverUrl = `http://${serverHost}:${RELAY_PORT}` + const serverUrl = await getServerUrl(host) // Ensure relay server is running (only for local) if (!host && !process.env.PLAYWRITER_HOST) { @@ -56,13 +59,8 @@ async function executeCode(options: { // Session is required if (!sessionId) { - try { - const res = await fetch(`${serverUrl}/cli/session/suggest`) - const { next } = await res.json() as { next: number } - console.error(`Error: -s/--session is required. Use -s ${next} for a new session.`) - } catch { - console.error(`Error: -s/--session is required. Use -s 1 for a new session.`) - } + console.error('Error: -s/--session is required.') + console.error('Run `playwriter session new` to get a session ID.') process.exit(1) } @@ -116,31 +114,80 @@ async function executeCode(options: { } } -// Reset command for CLI +// Session management commands cli - .command('reset', 'Reset the browser connection for current session') - .option('-s, --session ', 'Session name (required)') + .command('session new', 'Create a new session and print the session ID') .option('--host ', 'Remote relay server host') - .action(async (options: { session?: string; host?: string }) => { - const cwd = process.cwd() - const sessionId = options.session || process.env.PLAYWRITER_SESSION - const serverHost = options.host || process.env.PLAYWRITER_HOST || '127.0.0.1' - const serverUrl = `http://${serverHost}:${RELAY_PORT}` + .action(async (options: { host?: string }) => { + const serverUrl = await getServerUrl(options.host) if (!options.host && !process.env.PLAYWRITER_HOST) { await ensureRelayServer({ logger: console }) } - if (!sessionId) { - try { - const res = await fetch(`${serverUrl}/cli/session/suggest`) - const { next } = await res.json() as { next: number } - console.error(`Error: -s/--session is required. Use -s ${next} for a new session.`) - } catch { - console.error(`Error: -s/--session is required. Use -s 1 for a new session.`) - } + try { + const res = await fetch(`${serverUrl}/cli/session/suggest`) + const { next } = await res.json() as { next: number } + console.log(next) + } catch (error: any) { + console.error(`Error: ${error.message}`) process.exit(1) } + }) + +cli + .command('session list', 'List all active sessions') + .option('--host ', 'Remote relay server host') + .action(async (options: { host?: string }) => { + const serverUrl = await getServerUrl(options.host) + + if (!options.host && !process.env.PLAYWRITER_HOST) { + await ensureRelayServer({ logger: console }) + } + + try { + const res = await fetch(`${serverUrl}/cli/sessions`) + const { sessions } = await res.json() as { + sessions: Array<{ + id: string + stateKeys: string[] + }> + } + + if (sessions.length === 0) { + console.log('No active sessions') + return + } + + // Calculate column widths for aligned table + const idWidth = Math.max(2, ...sessions.map((s) => { return String(s.id).length })) + const stateWidth = Math.max(10, ...sessions.map((s) => { return s.stateKeys.join(', ').length || 1 })) + + // Header + console.log('ID'.padEnd(idWidth) + ' ' + 'State Keys') + console.log('-'.repeat(idWidth + stateWidth + 2)) + + // Rows + for (const session of sessions) { + const stateStr = session.stateKeys.length > 0 ? session.stateKeys.join(', ') : '-' + console.log(String(session.id).padEnd(idWidth) + ' ' + stateStr) + } + } catch (error: any) { + console.error(`Error: ${error.message}`) + process.exit(1) + } + }) + +cli + .command('session reset ', 'Reset the browser connection for a session') + .option('--host ', 'Remote relay server host') + .action(async (sessionId: string, options: { host?: string }) => { + const cwd = process.cwd() + const serverUrl = await getServerUrl(options.host) + + if (!options.host && !process.env.PLAYWRITER_HOST) { + await ensureRelayServer({ logger: console }) + } try { const response = await fetch(`${serverUrl}/cli/reset`, { diff --git a/playwriter/src/executor.ts b/playwriter/src/executor.ts index 034e21c..fc52226 100644 --- a/playwriter/src/executor.ts +++ b/playwriter/src/executor.ts @@ -674,6 +674,11 @@ export class PlaywrightExecutor { pagesCount: this.context?.pages().length || 0, } } + + /** Get keys of user-defined state */ + getStateKeys(): string[] { + return Object.keys(this.userState) + } } /** @@ -706,8 +711,13 @@ export class ExecutorManager { return this.executors.delete(sessionId) } - listSessions(): string[] { - return [...this.executors.keys()] + listSessions(): Array<{ id: string; stateKeys: string[] }> { + return [...this.executors.entries()].map(([id, executor]) => { + return { + id, + stateKeys: executor.getStateKeys(), + } + }) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a44d7bc..50742fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,9 +67,9 @@ importers: '@modelcontextprotocol/sdk': specifier: ^1.25.2 version: 1.25.2(@cfworker/json-schema@4.1.1)(hono@4.10.6)(zod@4.3.5) - cac: - specifier: ^6.7.14 - version: 6.7.14 + '@xmorse/cac': + specifier: ^6.0.7 + version: 6.0.7 diff: specifier: ^8.0.2 version: 8.0.2 @@ -2443,6 +2443,10 @@ packages: '@vitest/utils@4.0.8': resolution: {integrity: sha512-pdk2phO5NDvEFfUTxcTP8RFYjVj/kfLSPIN5ebP2Mu9kcIMeAQTbknqcFEyBcC4z2pJlJI9aS5UQjcYfhmKAow==} + '@xmorse/cac@6.0.7': + resolution: {integrity: sha512-eCKgJIn0Yswt4JreHNXUslyuOe8amDK6gE+wVgG8/Tu1HmCxuxAUOGPQRoma4mFO77759/YYE5opAbmaU6+Gxg==} + engines: {node: '>=8'} + '@xmorse/deployment-utils@0.7.2': resolution: {integrity: sha512-iRnHKcuPHK1dGHH+HqWJrPbodeR0mlnmS6yhC0nS5rLkxlgoqq6UK0Xv2/6fL0uLZGoUhopXimaFs1GbYrLH2g==} @@ -7691,6 +7695,8 @@ snapshots: '@vitest/pretty-format': 4.0.8 tinyrainbow: 3.0.3 + '@xmorse/cac@6.0.7': {} + '@xmorse/deployment-utils@0.7.2(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)': dependencies: '@actions/cache': 3.3.0 diff --git a/skills/playwriter/SKILL.md b/skills/playwriter/SKILL.md index 05ca93f..09b4032 100644 --- a/skills/playwriter/SKILL.md +++ b/skills/playwriter/SKILL.md @@ -8,45 +8,80 @@ description: Control Chrome browser via Playwright code snippets. Automate web i If `playwriter` command is not found, install globally or use npx/bunx: ```bash -npm install -g playwriter +npm install -g playwriter@latest # or use without installing: -npx playwriter -e "..." -s 1 -bunx playwriter -e "..." -s 1 +npx playwriter session new +bunx playwriter session new +``` + +### Session management + +Get a new session ID to use in commands: + +```bash +playwriter session new +# outputs: 1 +``` + +List all active sessions with their state: + +```bash +playwriter session list +# ID State Keys +# -------------- +# 1 myPage, userData +# 2 - +``` + +Reset a session if the browser connection is stale or broken: + +```bash +playwriter session reset ``` ### Execute code ```bash -playwriter -e "" -s +playwriter -s -e "" ``` -The `-s` flag specifies a session name (required). Use the same session to persist state across commands. +The `-s` flag specifies a session ID (required). Get one with `playwriter session new`. Use the same session to persist state across commands. **Examples:** ```bash # Navigate to a page -playwriter -e "await page.goto('https://example.com')" -s 1 +playwriter -s 1 -e "await page.goto('https://example.com')" # Click a button -playwriter -e "await page.click('button')" -s 1 +playwriter -s 1 -e "await page.click('button')" # Get page title -playwriter -e "console.log(await page.title())" -s 1 +playwriter -s 1 -e "console.log(await page.title())" # Take a screenshot -playwriter -e "await page.screenshot({ path: 'screenshot.png', scale: 'css' })" -s 1 +playwriter -s 1 -e "await page.screenshot({ path: 'screenshot.png', scale: 'css' })" # Get accessibility snapshot -playwriter -e "console.log(await accessibilitySnapshot({ page }))" -s 1 +playwriter -s 1 -e "console.log(await accessibilitySnapshot({ page }))" ``` -### Reset connection - -If the browser connection is stale or broken: +**Multiline code:** ```bash -playwriter reset -s +# Using $'...' syntax for multiline code +playwriter -s 1 -e $' +const title = await page.title(); +const url = page.url(); +console.log({ title, url }); +' + +# Or use heredoc +playwriter -s 1 -e "$(cat <<'EOF' +const links = await page.$$eval('a', els => els.map(e => e.href)); +console.log('Found', links.length, 'links'); +EOF +)" ``` # playwriter execute