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:
@@ -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
@@ -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`, {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Generated
+9
-3
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user