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 <id>' to reset a session
- Update SKILL.md with new CLI docs and multiline examples
- Put -s flag before -e in all examples
This commit is contained in:
Tommy D. Rossi
2026-01-24 12:57:21 +01:00
parent b7edfb52b1
commit 1af5aac0cc
5 changed files with 147 additions and 48 deletions
+2 -1
View File
@@ -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",
+75 -28
View File
@@ -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 <host>', 'Remote relay server host to connect to (or use PLAYWRITER_HOST env var)')
.option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
.option('-s, --session <name>', 'Session ID (required for -e, get one with `playwriter session new`)')
.option('-e, --eval <code>', 'Execute JavaScript code and exit, read https://playwriter.dev/SKILL.md for usage')
.option('--timeout <ms>', 'Execution timeout in milliseconds', { default: 5000 })
.option('-s, --session <name>', '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<string> {
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 <name>', 'Session name (required)')
.command('session new', 'Create a new session and print the session ID')
.option('--host <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 <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 <sessionId>', 'Reset the browser connection for a session')
.option('--host <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`, {
+12 -2
View File
@@ -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(),
}
})
}
}
+9 -3
View File
@@ -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
+49 -14
View File
@@ -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 <sessionId>
```
### Execute code
```bash
playwriter -e "<code>" -s <session>
playwriter -s <sessionId> -e "<code>"
```
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 <session>
# 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