playwriter 0.0.89 → 0.0.103
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aria-snapshot.d.ts +7 -3
- package/dist/aria-snapshot.d.ts.map +1 -1
- package/dist/aria-snapshot.js +76 -18
- package/dist/aria-snapshot.js.map +1 -1
- package/dist/aria-snapshot.test.js +94 -0
- package/dist/aria-snapshot.test.js.map +1 -1
- package/dist/bippy.js +1 -1
- package/dist/browser-config.d.ts +12 -1
- package/dist/browser-config.d.ts.map +1 -1
- package/dist/browser-config.js +101 -38
- package/dist/browser-config.js.map +1 -1
- package/dist/browser-launch.d.ts +17 -0
- package/dist/browser-launch.d.ts.map +1 -0
- package/dist/browser-launch.js +44 -0
- package/dist/browser-launch.js.map +1 -0
- package/dist/cdp-relay.d.ts.map +1 -1
- package/dist/cdp-relay.js +34 -10
- package/dist/cdp-relay.js.map +1 -1
- package/dist/channel-owner-inspect.test.d.ts +2 -0
- package/dist/channel-owner-inspect.test.d.ts.map +1 -0
- package/dist/channel-owner-inspect.test.js +75 -0
- package/dist/channel-owner-inspect.test.js.map +1 -0
- package/dist/chrome-discovery.d.ts +65 -0
- package/dist/chrome-discovery.d.ts.map +1 -0
- package/dist/chrome-discovery.js +165 -0
- package/dist/chrome-discovery.js.map +1 -0
- package/dist/chrome-discovery.test.d.ts +2 -0
- package/dist/chrome-discovery.test.d.ts.map +1 -0
- package/dist/chrome-discovery.test.js +41 -0
- package/dist/chrome-discovery.test.js.map +1 -0
- package/dist/cli.js +357 -53
- package/dist/cli.js.map +1 -1
- package/dist/executor.d.ts +44 -11
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +121 -55
- package/dist/executor.js.map +1 -1
- package/dist/executor.unit.test.js +29 -1
- package/dist/executor.unit.test.js.map +1 -1
- package/dist/extension/background.js +1704 -0
- package/dist/extension/icons/SolarCursorSquareBold.png +0 -0
- package/dist/extension/icons/icon-black-128.png +0 -0
- package/dist/extension/icons/icon-black-16.png +0 -0
- package/dist/extension/icons/icon-black-32.png +0 -0
- package/dist/extension/icons/icon-black-48.png +0 -0
- package/dist/extension/icons/icon-gray-128.png +0 -0
- package/dist/extension/icons/icon-gray-16.png +0 -0
- package/dist/extension/icons/icon-gray-32.png +0 -0
- package/dist/extension/icons/icon-gray-48.png +0 -0
- package/dist/extension/icons/icon-green-128.png +0 -0
- package/dist/extension/icons/icon-green-16.png +0 -0
- package/dist/extension/icons/icon-green-32.png +0 -0
- package/dist/extension/icons/icon-green-48.png +0 -0
- package/dist/extension/icons/playwriter-icon-black.png +0 -0
- package/dist/extension/icons/playwriter-icon-gray-disabled.png +0 -0
- package/dist/extension/icons/playwriter-icon-gray-disabled.svg +1 -0
- package/dist/extension/manifest.json +40 -0
- package/dist/extension/offscreen.js +214 -0
- package/dist/extension/src/offscreen.html +9 -0
- package/dist/extension/src/prism-bash.min.js +1 -0
- package/dist/extension/src/prism.min.js +1 -0
- package/dist/extension/src/welcome.html +399 -0
- package/dist/kitty-graphics.d.ts +22 -0
- package/dist/kitty-graphics.d.ts.map +1 -0
- package/dist/kitty-graphics.js +71 -0
- package/dist/kitty-graphics.js.map +1 -0
- package/dist/kitty-graphics.test.d.ts +2 -0
- package/dist/kitty-graphics.test.d.ts.map +1 -0
- package/dist/kitty-graphics.test.js +125 -0
- package/dist/kitty-graphics.test.js.map +1 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +76 -12
- package/dist/mcp.js.map +1 -1
- package/dist/package-paths.d.ts +3 -0
- package/dist/package-paths.d.ts.map +1 -0
- package/dist/package-paths.js +29 -0
- package/dist/package-paths.js.map +1 -0
- package/dist/prompt.md +4 -2
- package/dist/readability.js +1 -1
- package/dist/relay-core.test.js +14 -4
- package/dist/relay-core.test.js.map +1 -1
- package/dist/relay-navigation.test.d.ts.map +1 -1
- package/dist/relay-navigation.test.js +95 -1
- package/dist/relay-navigation.test.js.map +1 -1
- package/dist/scoped-fs.d.ts +5 -4
- package/dist/scoped-fs.d.ts.map +1 -1
- package/dist/scoped-fs.js +11 -9
- package/dist/scoped-fs.js.map +1 -1
- package/dist/scoped-fs.test.d.ts +5 -0
- package/dist/scoped-fs.test.d.ts.map +1 -0
- package/dist/scoped-fs.test.js +70 -0
- package/dist/scoped-fs.test.js.map +1 -0
- package/dist/screen-recording.d.ts +3 -6
- package/dist/screen-recording.d.ts.map +1 -1
- package/dist/screen-recording.js +7 -21
- package/dist/screen-recording.js.map +1 -1
- package/dist/selector-generator.js +1 -1
- package/dist/snapshot-tools.test.js +3 -4
- package/dist/snapshot-tools.test.js.map +1 -1
- package/dist/test-utils.d.ts +3 -1
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +24 -12
- package/dist/test-utils.js.map +1 -1
- package/package.json +6 -5
- package/src/aria-snapshot.test.ts +110 -0
- package/src/aria-snapshot.ts +86 -19
- package/src/aria-snapshots/contenteditable-interactive.txt +7 -0
- package/src/aria-snapshots/contenteditable-raw.txt +14 -0
- package/src/aria-snapshots/github-interactive.txt +0 -2
- package/src/aria-snapshots/github-raw.txt +0 -4
- package/src/aria-snapshots/hackernews-interactive.txt +240 -237
- package/src/aria-snapshots/hackernews-raw.txt +262 -258
- package/src/aria-snapshots/prosemirror-interactive.txt +63 -0
- package/src/aria-snapshots/prosemirror-raw.txt +141 -0
- package/src/assets/aria-labels-hacker-news.png +0 -0
- package/src/browser-config.ts +143 -37
- package/src/browser-launch.ts +75 -0
- package/src/cdp-relay.ts +46 -11
- package/src/channel-owner-inspect.test.ts +94 -0
- package/src/chrome-discovery.test.ts +50 -0
- package/src/chrome-discovery.ts +211 -0
- package/src/cli.ts +432 -59
- package/src/executor.ts +171 -72
- package/src/executor.unit.test.ts +32 -1
- package/src/kitty-graphics.test.ts +139 -0
- package/src/kitty-graphics.ts +79 -0
- package/src/mcp.ts +82 -12
- package/src/package-paths.ts +36 -0
- package/src/relay-core.test.ts +14 -4
- package/src/relay-navigation.test.ts +106 -1
- package/src/scoped-fs.test.ts +77 -0
- package/src/scoped-fs.ts +12 -9
- package/src/screen-recording.ts +7 -22
- package/src/skill.md +32 -2
- package/src/snapshot-tools.test.ts +3 -4
- package/src/snapshots/shadcn-ui-accessibility-full.md +181 -183
- package/src/snapshots/shadcn-ui-accessibility-interactive.md +119 -121
- package/src/test-utils.ts +25 -11
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { parseDevToolsActivePort } from './chrome-discovery.js';
|
|
3
|
+
describe('parseDevToolsActivePort', () => {
|
|
4
|
+
test('parses valid contents', () => {
|
|
5
|
+
const result = parseDevToolsActivePort('9222\n/devtools/browser/abc-123-def\n');
|
|
6
|
+
expect(result).toMatchInlineSnapshot(`
|
|
7
|
+
{
|
|
8
|
+
"port": 9222,
|
|
9
|
+
"wsPath": "/devtools/browser/abc-123-def",
|
|
10
|
+
}
|
|
11
|
+
`);
|
|
12
|
+
});
|
|
13
|
+
test('parses with extra whitespace', () => {
|
|
14
|
+
const result = parseDevToolsActivePort(' 9222 \n /devtools/browser/abc \n');
|
|
15
|
+
expect(result).toMatchInlineSnapshot(`
|
|
16
|
+
{
|
|
17
|
+
"port": 9222,
|
|
18
|
+
"wsPath": "/devtools/browser/abc",
|
|
19
|
+
}
|
|
20
|
+
`);
|
|
21
|
+
});
|
|
22
|
+
test('returns null for single line', () => {
|
|
23
|
+
expect(parseDevToolsActivePort('9222')).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
test('returns null for empty string', () => {
|
|
26
|
+
expect(parseDevToolsActivePort('')).toBeNull();
|
|
27
|
+
});
|
|
28
|
+
test('returns null for invalid port', () => {
|
|
29
|
+
expect(parseDevToolsActivePort('abc\n/devtools/browser/123')).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
test('returns null for port 0', () => {
|
|
32
|
+
expect(parseDevToolsActivePort('0\n/devtools/browser/123')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
test('returns null for port > 65535', () => {
|
|
35
|
+
expect(parseDevToolsActivePort('99999\n/devtools/browser/123')).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
test('returns null for invalid ws path', () => {
|
|
38
|
+
expect(parseDevToolsActivePort('9222\n/some/other/path')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
//# sourceMappingURL=chrome-discovery.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome-discovery.test.js","sourceRoot":"","sources":["../src/chrome-discovery.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AAE/D,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,MAAM,GAAG,uBAAuB,CAAC,uCAAuC,CAAC,CAAA;QAC/E,MAAM,CAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC;;;;;KAKpC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACxC,MAAM,MAAM,GAAG,uBAAuB,CAAC,uCAAuC,CAAC,CAAA;QAC/E,MAAM,CAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC;;;;;KAKpC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACpD,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,uBAAuB,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IAChD,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,uBAAuB,CAAC,4BAA4B,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IAC1E,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,uBAAuB,CAAC,0BAA0B,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,uBAAuB,CAAC,8BAA8B,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IAC5E,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,uBAAuB,CAAC,wBAAwB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACtE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
package/dist/cli.js
CHANGED
|
@@ -3,25 +3,83 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import util from 'node:util';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
6
|
+
import { goke } from 'goke';
|
|
7
|
+
import { z } from 'zod';
|
|
7
8
|
import pc from 'picocolors';
|
|
9
|
+
import { getBrowserLaunchArgs, getDefaultBrowserUserDataDir, startBrowserProcess, } from './browser-launch.js';
|
|
10
|
+
import { resolveBrowserExecutablePath, shouldUseHeadlessByDefault } from './browser-config.js';
|
|
11
|
+
import { getBundledExtensionPath } from './package-paths.js';
|
|
8
12
|
// Prevent Buffers from dumping hex bytes in util.inspect output.
|
|
9
13
|
Buffer.prototype[util.inspect.custom] = function () {
|
|
10
14
|
return `<Buffer ${this.length} bytes>`;
|
|
11
15
|
};
|
|
12
16
|
import { killPortProcess } from './kill-port.js';
|
|
17
|
+
import { canEmitKittyGraphics, emitKittyImage } from './kitty-graphics.js';
|
|
13
18
|
import { VERSION, LOG_FILE_PATH, LOG_CDP_FILE_PATH, parseRelayHost } from './utils.js';
|
|
14
19
|
import { ensureRelayServer, RELAY_PORT, waitForConnectedExtensions, getExtensionOutdatedWarning, getExtensionStatus, } from './relay-client.js';
|
|
20
|
+
import { discoverChromeInstances, resolveDirectInput } from './chrome-discovery.js';
|
|
15
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
22
|
const cliRelayEnv = { PLAYWRITER_AUTO_ENABLE: '1' };
|
|
17
|
-
const cli =
|
|
23
|
+
const cli = goke('playwriter');
|
|
24
|
+
cli
|
|
25
|
+
.command('browser start [binaryPath]', 'Start Chromium or Chrome for Testing with the bundled Playwriter extension')
|
|
26
|
+
.hidden()
|
|
27
|
+
.option('--user-data-dir <dir>', 'Persistent browser profile directory used for the managed browser')
|
|
28
|
+
.option('--headless', 'Run the browser in headless mode')
|
|
29
|
+
.option('--headed', 'Force headed mode even on Linux without DISPLAY/WAYLAND_DISPLAY')
|
|
30
|
+
.option('--disable-sandbox', 'Disable the browser sandbox, useful on some VPS setups')
|
|
31
|
+
.action(async (binaryPath, options) => {
|
|
32
|
+
if (options.headless && options.headed) {
|
|
33
|
+
console.error('Error: --headless and --headed cannot be used together.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
await ensureRelayServer({ logger: console, env: cliRelayEnv });
|
|
38
|
+
const browserPath = resolveBrowserExecutablePath({ browserPath: binaryPath });
|
|
39
|
+
const extensionPath = getBundledExtensionPath();
|
|
40
|
+
const userDataDir = path.resolve(options.userDataDir || getDefaultBrowserUserDataDir());
|
|
41
|
+
const headless = options.headed ? false : options.headless ? true : shouldUseHeadlessByDefault();
|
|
42
|
+
const args = getBrowserLaunchArgs({
|
|
43
|
+
extensionPath,
|
|
44
|
+
userDataDir,
|
|
45
|
+
headless,
|
|
46
|
+
noSandbox: options.disableSandbox,
|
|
47
|
+
});
|
|
48
|
+
const { pid } = startBrowserProcess({
|
|
49
|
+
browserPath,
|
|
50
|
+
args,
|
|
51
|
+
userDataDir,
|
|
52
|
+
});
|
|
53
|
+
const connectedExtensions = await waitForConnectedExtensions({
|
|
54
|
+
timeoutMs: 15000,
|
|
55
|
+
pollIntervalMs: 250,
|
|
56
|
+
logger: console,
|
|
57
|
+
});
|
|
58
|
+
console.log(`Browser started (pid ${pid}).`);
|
|
59
|
+
console.log(` Binary: ${browserPath}`);
|
|
60
|
+
console.log(` Extension: ${extensionPath}`);
|
|
61
|
+
console.log(` Profile: ${userDataDir}`);
|
|
62
|
+
console.log(` Mode: ${headless ? 'headless' : 'headed'}`);
|
|
63
|
+
console.log(' Permissions: recording/tabCapture flags enabled');
|
|
64
|
+
if (connectedExtensions.length > 0) {
|
|
65
|
+
console.log('Playwriter extension connected to the relay server.');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.log('Browser started, but the extension has not connected yet.');
|
|
69
|
+
console.log(`Check logs at: ${LOG_FILE_PATH}`);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error(`Error: ${error.message}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
18
76
|
cli
|
|
19
77
|
.command('', 'Start the MCP server or controls the browser with -e')
|
|
20
78
|
.option('--host <host>', 'Remote relay server host to connect to (or use PLAYWRITER_HOST env var)')
|
|
21
79
|
.option('--token <token>', 'Authentication token (or use PLAYWRITER_TOKEN env var)')
|
|
22
80
|
.option('-s, --session <name>', 'Session ID (required for -e, get one with `playwriter session new`)')
|
|
23
81
|
.option('-e, --eval <code>', 'Execute JavaScript code and exit, read https://playwriter.dev/SKILL.md for usage')
|
|
24
|
-
.option('--timeout
|
|
82
|
+
.option('--timeout [ms]', z.number().default(10000).describe('Execution timeout in milliseconds'))
|
|
25
83
|
.action(async (options) => {
|
|
26
84
|
// If -e flag is provided, execute code via relay server
|
|
27
85
|
if (options.eval) {
|
|
@@ -35,6 +93,7 @@ cli
|
|
|
35
93
|
return;
|
|
36
94
|
}
|
|
37
95
|
// Otherwise start the MCP server
|
|
96
|
+
// For direct CDP in MCP mode, use PLAYWRITER_DIRECT env var
|
|
38
97
|
const { startMcp } = await import('./mcp.js');
|
|
39
98
|
await startMcp({
|
|
40
99
|
host: options.host,
|
|
@@ -140,9 +199,33 @@ async function executeCode(options) {
|
|
|
140
199
|
console.log(result.text);
|
|
141
200
|
}
|
|
142
201
|
}
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
202
|
+
// Emit images via Kitty Graphics Protocol when AGENT_GRAPHICS=kitty.
|
|
203
|
+
// Agents with kitty-graphics-agent intercept these escape sequences and pass
|
|
204
|
+
// the PNG images to the LLM as media parts — no extra tool call needed.
|
|
205
|
+
const kittyEnabled = canEmitKittyGraphics();
|
|
206
|
+
// Track emitted base64 to avoid duplicates (screenshots appear in both
|
|
207
|
+
// result.screenshots and result.images from the same screenshotCollector)
|
|
208
|
+
const emittedImages = new Set();
|
|
209
|
+
if (result.screenshots && result.screenshots.length > 0) {
|
|
210
|
+
for (const s of result.screenshots) {
|
|
211
|
+
if (kittyEnabled && s.base64) {
|
|
212
|
+
emitKittyImage({ base64: s.base64 });
|
|
213
|
+
emittedImages.add(s.base64);
|
|
214
|
+
}
|
|
215
|
+
console.log(`\nScreenshot saved to: ${s.path}`);
|
|
216
|
+
console.log(`Labels shown: ${s.labelCount}\n`);
|
|
217
|
+
console.log(`Accessibility snapshot:\n${s.snapshot}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Emit resized images from resizeImageForAgent() calls that aren't
|
|
221
|
+
// already emitted as part of labeled screenshots
|
|
222
|
+
if (kittyEnabled && result.images && result.images.length > 0) {
|
|
223
|
+
for (const img of result.images) {
|
|
224
|
+
if (img.data && !emittedImages.has(img.data)) {
|
|
225
|
+
emitKittyImage({ base64: img.data });
|
|
226
|
+
emittedImages.add(img.data);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
146
229
|
}
|
|
147
230
|
if (result.isError) {
|
|
148
231
|
process.exit(1);
|
|
@@ -160,13 +243,83 @@ async function executeCode(options) {
|
|
|
160
243
|
process.exit(1);
|
|
161
244
|
}
|
|
162
245
|
}
|
|
163
|
-
// Session management commands
|
|
164
246
|
cli
|
|
165
247
|
.command('session new', 'Create a new session and print the session ID')
|
|
166
248
|
.option('--host <host>', 'Remote relay server host')
|
|
167
|
-
.option('--browser <
|
|
249
|
+
.option('--browser <key>', 'Browser key when multiple browsers are available')
|
|
250
|
+
.option('--direct [endpoint]', 'Use direct CDP connection without the extension. Enable debugging first at chrome://inspect/#remote-debugging or launch Chrome with --remote-debugging-port=9222. Auto-discovers instances or accepts an explicit ws:// endpoint')
|
|
168
251
|
.action(async (options) => {
|
|
169
252
|
const isLocal = !options.host && !process.env.PLAYWRITER_HOST;
|
|
253
|
+
const directEndpoint = typeof options.direct === 'string' ? options.direct : null;
|
|
254
|
+
// If --direct with explicit endpoint, resolve it (handles host:port → ws://) then skip discovery
|
|
255
|
+
if (directEndpoint) {
|
|
256
|
+
let cdpEndpoint;
|
|
257
|
+
try {
|
|
258
|
+
cdpEndpoint = await resolveDirectInput(directEndpoint);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
console.error(`Error: ${error.message}`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
await ensureRelayForSessionCreation(isLocal);
|
|
265
|
+
const serverUrl = await getServerUrl(options.host);
|
|
266
|
+
const result = await createDirectSession({ serverUrl, cdpEndpoint });
|
|
267
|
+
console.log(`Session ${result.id} created (direct CDP). Use with: playwriter -s ${result.id} -e "..."`);
|
|
268
|
+
console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// If --direct with no endpoint, discover Chrome instances
|
|
272
|
+
if (options.direct === true) {
|
|
273
|
+
if (!isLocal) {
|
|
274
|
+
console.error('Error: --direct auto-discovery only works locally.');
|
|
275
|
+
console.error('For remote relay, pass an explicit endpoint reachable from the relay host:');
|
|
276
|
+
console.error(' playwriter session new --host <host> --direct ws://relay-host:9222/devtools/browser/...');
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
await ensureRelayForSessionCreation(isLocal);
|
|
280
|
+
console.log(pc.dim('Discovering Chrome instances with debugging enabled...'));
|
|
281
|
+
const instances = await discoverChromeInstances();
|
|
282
|
+
if (instances.length === 0) {
|
|
283
|
+
console.error('No Chrome instances with debugging enabled found.');
|
|
284
|
+
console.error('');
|
|
285
|
+
console.error('Enable debugging in one of these ways:');
|
|
286
|
+
console.error(' 1. Open chrome://inspect/#remote-debugging in Chrome');
|
|
287
|
+
console.error(' 2. Launch Chrome with: chrome --remote-debugging-port=9222');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
if (instances.length === 1 && !options.browser) {
|
|
291
|
+
const instance = instances[0];
|
|
292
|
+
const serverUrl = await getServerUrl(options.host);
|
|
293
|
+
const result = await createDirectSession({ serverUrl, cdpEndpoint: instance.wsUrl, browser: instance.browser, profiles: instance.profiles });
|
|
294
|
+
const profileLabel = formatInstanceProfiles(instance);
|
|
295
|
+
console.log(`Session ${result.id} created (direct CDP, ${instance.browser}${profileLabel}). Use with: playwriter -s ${result.id} -e "..."`);
|
|
296
|
+
console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// Multiple instances or --browser specified
|
|
300
|
+
const directOptions = instances.map((instance) => {
|
|
301
|
+
return instanceToBrowserOption(instance);
|
|
302
|
+
});
|
|
303
|
+
if (options.browser) {
|
|
304
|
+
const selected = directOptions.find((opt) => {
|
|
305
|
+
return opt.key === options.browser;
|
|
306
|
+
});
|
|
307
|
+
if (!selected) {
|
|
308
|
+
console.error(`Browser not found: ${options.browser}`);
|
|
309
|
+
console.error('Available: ' + directOptions.map((opt) => opt.key).join(', '));
|
|
310
|
+
process.exit(1);
|
|
311
|
+
}
|
|
312
|
+
const serverUrl = await getServerUrl(options.host);
|
|
313
|
+
const result = await createDirectSession({ serverUrl, cdpEndpoint: selected.wsUrl, browser: selected.browser, profiles: selected.profiles });
|
|
314
|
+
console.log(`Session ${result.id} created (direct CDP). Use with: playwriter -s ${result.id} -e "..."`);
|
|
315
|
+
console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
printBrowserTable(directOptions);
|
|
319
|
+
console.log('\nRun again with --browser <key>.');
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
// Default mode: extension-based (existing behavior)
|
|
170
323
|
let extensions = [];
|
|
171
324
|
if (isLocal) {
|
|
172
325
|
await ensureRelayServer({ logger: console, env: cliRelayEnv });
|
|
@@ -189,6 +342,7 @@ cli
|
|
|
189
342
|
}
|
|
190
343
|
if (extensions.length === 0) {
|
|
191
344
|
console.error('No connected browsers detected. Click the Playwriter extension icon.');
|
|
345
|
+
console.error(pc.dim('Tip: Use --direct to connect via Chrome DevTools Protocol instead.'));
|
|
192
346
|
process.exit(1);
|
|
193
347
|
}
|
|
194
348
|
// Warn if any connected extension was built with an older playwriter version
|
|
@@ -199,58 +353,156 @@ cli
|
|
|
199
353
|
break;
|
|
200
354
|
}
|
|
201
355
|
}
|
|
202
|
-
|
|
203
|
-
if (extensions.length === 1) {
|
|
204
|
-
selectedExtension = extensions[0];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
356
|
+
// Single extension: auto-select (unchanged behavior)
|
|
357
|
+
if (extensions.length === 1 && !options.browser) {
|
|
358
|
+
const selectedExtension = extensions[0];
|
|
359
|
+
try {
|
|
360
|
+
const serverUrl = await getServerUrl(options.host);
|
|
361
|
+
const extensionId = selectedExtension.extensionId === 'default'
|
|
362
|
+
? null
|
|
363
|
+
: selectedExtension.stableKey || selectedExtension.extensionId;
|
|
364
|
+
const cwd = process.cwd();
|
|
365
|
+
const response = await fetch(`${serverUrl}/cli/session/new`, {
|
|
366
|
+
method: 'POST',
|
|
367
|
+
headers: { 'Content-Type': 'application/json' },
|
|
368
|
+
body: JSON.stringify({ extensionId, cwd }),
|
|
369
|
+
});
|
|
370
|
+
if (!response.ok) {
|
|
371
|
+
const text = await response.text();
|
|
372
|
+
console.error(`Error: ${response.status} ${text}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
const result = (await response.json());
|
|
376
|
+
console.log(`Session ${result.id} created. Use with: playwriter -s ${result.id} -e "..."`);
|
|
214
377
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
const browserArg = options.browser;
|
|
220
|
-
selectedExtension = extensions.find((extension) => extension.stableKey === browserArg) || null;
|
|
221
|
-
if (!selectedExtension) {
|
|
222
|
-
console.error(`Browser not found: ${browserArg}`);
|
|
378
|
+
catch (error) {
|
|
379
|
+
console.error(`Error: ${error.message}`);
|
|
223
380
|
process.exit(1);
|
|
224
381
|
}
|
|
382
|
+
return;
|
|
225
383
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
384
|
+
// Multiple extensions: also discover direct CDP instances and show unified table.
|
|
385
|
+
// Only discover locally — remote relay can't reach local Chrome debug ports.
|
|
386
|
+
const directInstances = isLocal ? await (async () => {
|
|
387
|
+
console.log(pc.dim('Discovering additional Chrome instances...'));
|
|
388
|
+
return await discoverChromeInstances();
|
|
389
|
+
})() : [];
|
|
390
|
+
const allOptions = [
|
|
391
|
+
...extensions.map((ext) => {
|
|
392
|
+
return {
|
|
393
|
+
key: ext.stableKey || ext.extensionId,
|
|
394
|
+
type: 'extension',
|
|
395
|
+
browser: ext.browser || 'Chrome',
|
|
396
|
+
profile: ext.profile?.email || '(not signed in)',
|
|
397
|
+
extensionId: ext.extensionId === 'default' ? null : ext.stableKey || ext.extensionId,
|
|
398
|
+
};
|
|
399
|
+
}),
|
|
400
|
+
...directInstances.map((instance) => {
|
|
401
|
+
return instanceToBrowserOption(instance);
|
|
402
|
+
}),
|
|
403
|
+
];
|
|
404
|
+
if (options.browser) {
|
|
405
|
+
const selected = allOptions.find((opt) => {
|
|
406
|
+
return opt.key === options.browser;
|
|
240
407
|
});
|
|
241
|
-
if (!
|
|
242
|
-
|
|
243
|
-
console.error(
|
|
408
|
+
if (!selected) {
|
|
409
|
+
console.error(`Browser not found: ${options.browser}`);
|
|
410
|
+
console.error('Available: ' + allOptions.map((opt) => opt.key).join(', '));
|
|
244
411
|
process.exit(1);
|
|
245
412
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
413
|
+
try {
|
|
414
|
+
const serverUrl = await getServerUrl(options.host);
|
|
415
|
+
if (selected.type === 'direct') {
|
|
416
|
+
const result = await createDirectSession({ serverUrl, cdpEndpoint: selected.wsUrl, browser: selected.browser, profiles: selected.profiles });
|
|
417
|
+
console.log(`Session ${result.id} created (direct CDP). Use with: playwriter -s ${result.id} -e "..."`);
|
|
418
|
+
console.log(pc.dim('NOTE: Recording unavailable in direct CDP mode.'));
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
const cwd = process.cwd();
|
|
422
|
+
const response = await fetch(`${serverUrl}/cli/session/new`, {
|
|
423
|
+
method: 'POST',
|
|
424
|
+
headers: { 'Content-Type': 'application/json' },
|
|
425
|
+
body: JSON.stringify({ extensionId: selected.extensionId, cwd }),
|
|
426
|
+
});
|
|
427
|
+
if (!response.ok) {
|
|
428
|
+
const text = await response.text();
|
|
429
|
+
console.error(`Error: ${response.status} ${text}`);
|
|
430
|
+
process.exit(1);
|
|
431
|
+
}
|
|
432
|
+
const result = (await response.json());
|
|
433
|
+
console.log(`Session ${result.id} created. Use with: playwriter -s ${result.id} -e "..."`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
console.error(`Error: ${error.message}`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
252
441
|
}
|
|
442
|
+
// Show unified table
|
|
443
|
+
console.log('\nMultiple browsers detected:\n');
|
|
444
|
+
printBrowserTable(allOptions);
|
|
445
|
+
console.log('\nRun again with --browser <key>.');
|
|
446
|
+
process.exit(1);
|
|
253
447
|
});
|
|
448
|
+
async function ensureRelayForSessionCreation(isLocal) {
|
|
449
|
+
if (isLocal) {
|
|
450
|
+
await ensureRelayServer({ logger: console, env: cliRelayEnv });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
async function createDirectSession({ serverUrl, cdpEndpoint, browser, profiles, }) {
|
|
454
|
+
const cwd = process.cwd();
|
|
455
|
+
const response = await fetch(`${serverUrl}/cli/session/new`, {
|
|
456
|
+
method: 'POST',
|
|
457
|
+
headers: { 'Content-Type': 'application/json' },
|
|
458
|
+
body: JSON.stringify({ cdpEndpoint, cwd, browser, profiles }),
|
|
459
|
+
});
|
|
460
|
+
if (!response.ok) {
|
|
461
|
+
const text = await response.text();
|
|
462
|
+
throw new Error(`${response.status} ${text}`);
|
|
463
|
+
}
|
|
464
|
+
return (await response.json());
|
|
465
|
+
}
|
|
466
|
+
function instanceToBrowserOption(instance) {
|
|
467
|
+
return {
|
|
468
|
+
key: `direct:${instance.port}`,
|
|
469
|
+
type: 'direct',
|
|
470
|
+
browser: instance.browser,
|
|
471
|
+
profile: formatInstanceProfiles(instance),
|
|
472
|
+
wsUrl: instance.wsUrl,
|
|
473
|
+
profiles: instance.profiles,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function formatInstanceProfiles(instance) {
|
|
477
|
+
if (instance.profiles.length === 0) {
|
|
478
|
+
return '(unknown)';
|
|
479
|
+
}
|
|
480
|
+
return instance.profiles
|
|
481
|
+
.map((p) => {
|
|
482
|
+
return p.email ? `${p.name} (${p.email})` : p.name;
|
|
483
|
+
})
|
|
484
|
+
.join(', ');
|
|
485
|
+
}
|
|
486
|
+
function printBrowserTable(options) {
|
|
487
|
+
const typeLabels = options.map((opt) => {
|
|
488
|
+
return opt.type === 'direct' ? '--direct' : opt.type;
|
|
489
|
+
});
|
|
490
|
+
const keyWidth = Math.max(3, ...options.map((opt) => opt.key.length));
|
|
491
|
+
const typeWidth = Math.max(4, ...typeLabels.map((t) => t.length));
|
|
492
|
+
const browserWidth = Math.max(7, ...options.map((opt) => opt.browser.length));
|
|
493
|
+
console.log('KEY'.padEnd(keyWidth) + ' ' + 'TYPE'.padEnd(typeWidth) + ' ' + 'BROWSER'.padEnd(browserWidth) + ' ' + 'PROFILE');
|
|
494
|
+
console.log('-'.repeat(keyWidth + typeWidth + browserWidth + 20));
|
|
495
|
+
for (let i = 0; i < options.length; i++) {
|
|
496
|
+
const opt = options[i];
|
|
497
|
+
console.log(opt.key.padEnd(keyWidth) +
|
|
498
|
+
' ' +
|
|
499
|
+
typeLabels[i].padEnd(typeWidth) +
|
|
500
|
+
' ' +
|
|
501
|
+
opt.browser.padEnd(browserWidth) +
|
|
502
|
+
' ' +
|
|
503
|
+
opt.profile);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
254
506
|
cli
|
|
255
507
|
.command('session list', 'List all active sessions')
|
|
256
508
|
.option('--host <host>', 'Remote relay server host')
|
|
@@ -283,6 +535,7 @@ cli
|
|
|
283
535
|
const browserWidth = Math.max(7, ...sessions.map((session) => (session.browser || 'Chrome').length));
|
|
284
536
|
const profileWidth = Math.max(7, ...sessions.map((session) => (session.profile?.email || '').length || 1));
|
|
285
537
|
const extensionWidth = Math.max(2, ...sessions.map((session) => (session.extensionId || '').length || 1));
|
|
538
|
+
const cwdWidth = Math.max(3, ...sessions.map((session) => (session.cwd || '').length || 1));
|
|
286
539
|
const stateWidth = Math.max(10, ...sessions.map((session) => session.stateKeys.join(', ').length || 1));
|
|
287
540
|
console.log('ID'.padEnd(idWidth) +
|
|
288
541
|
' ' +
|
|
@@ -292,11 +545,14 @@ cli
|
|
|
292
545
|
' ' +
|
|
293
546
|
'EXT'.padEnd(extensionWidth) +
|
|
294
547
|
' ' +
|
|
548
|
+
'CWD'.padEnd(cwdWidth) +
|
|
549
|
+
' ' +
|
|
295
550
|
'STATE KEYS');
|
|
296
|
-
console.log('-'.repeat(idWidth + browserWidth + profileWidth + extensionWidth + stateWidth +
|
|
551
|
+
console.log('-'.repeat(idWidth + browserWidth + profileWidth + extensionWidth + cwdWidth + stateWidth + 10));
|
|
297
552
|
for (const session of sessions) {
|
|
298
553
|
const stateStr = session.stateKeys.length > 0 ? session.stateKeys.join(', ') : '-';
|
|
299
554
|
const profileLabel = session.profile?.email || '-';
|
|
555
|
+
const cwdLabel = session.cwd || '-';
|
|
300
556
|
console.log(String(session.id).padEnd(idWidth) +
|
|
301
557
|
' ' +
|
|
302
558
|
(session.browser || 'Chrome').padEnd(browserWidth) +
|
|
@@ -305,6 +561,8 @@ cli
|
|
|
305
561
|
' ' +
|
|
306
562
|
(session.extensionId || '-').padEnd(extensionWidth) +
|
|
307
563
|
' ' +
|
|
564
|
+
cwdLabel.padEnd(cwdWidth) +
|
|
565
|
+
' ' +
|
|
308
566
|
stateStr);
|
|
309
567
|
}
|
|
310
568
|
});
|
|
@@ -364,7 +622,7 @@ cli
|
|
|
364
622
|
});
|
|
365
623
|
cli
|
|
366
624
|
.command('serve', `Start the relay server on this machine (must be the same host where Chrome is running). Remote clients (Docker, other machines) connect via PLAYWRITER_HOST. Use --host localhost for Docker (no token needed) — containers reach it via host.docker.internal. Use --host 0.0.0.0 for LAN/internet access (requires --token).`)
|
|
367
|
-
.option('--host
|
|
625
|
+
.option('--host [host]', z.string().default('0.0.0.0').describe('Host to bind to (use "localhost" for Docker, "0.0.0.0" for remote access)'))
|
|
368
626
|
.option('--token <token>', 'Authentication token, required when --host is 0.0.0.0 (or use PLAYWRITER_TOKEN env var)')
|
|
369
627
|
.option('--replace', 'Kill existing server if running')
|
|
370
628
|
.action(async (options) => {
|
|
@@ -443,6 +701,52 @@ cli
|
|
|
443
701
|
process.exit(0);
|
|
444
702
|
});
|
|
445
703
|
});
|
|
704
|
+
cli
|
|
705
|
+
.command('browser list', 'List all available browsers: extension-connected and direct CDP on port 9222')
|
|
706
|
+
.option('--host <host>', z.string().describe('Remote relay server host'))
|
|
707
|
+
.action(async (options) => {
|
|
708
|
+
const isLocal = !options.host && !process.env.PLAYWRITER_HOST;
|
|
709
|
+
// Start relay if local so the extension can connect, then fetch in parallel
|
|
710
|
+
if (isLocal) {
|
|
711
|
+
await ensureRelayServer({ logger: console, env: cliRelayEnv });
|
|
712
|
+
}
|
|
713
|
+
const [extensions, directInstances] = await Promise.all([
|
|
714
|
+
isLocal
|
|
715
|
+
? waitForConnectedExtensions({ timeoutMs: 2000, pollIntervalMs: 200, logger: console })
|
|
716
|
+
: fetchExtensionsStatus(options.host),
|
|
717
|
+
isLocal ? discoverChromeInstances() : Promise.resolve([]),
|
|
718
|
+
]);
|
|
719
|
+
const allOptions = [
|
|
720
|
+
...extensions.map((ext) => {
|
|
721
|
+
return {
|
|
722
|
+
key: ext.stableKey || ext.extensionId,
|
|
723
|
+
type: 'extension',
|
|
724
|
+
browser: ext.browser || 'Chrome',
|
|
725
|
+
profile: ext.profile?.email || '(not signed in)',
|
|
726
|
+
extensionId: ext.extensionId === 'default' ? null : ext.stableKey || ext.extensionId,
|
|
727
|
+
};
|
|
728
|
+
}),
|
|
729
|
+
...directInstances.map(instanceToBrowserOption),
|
|
730
|
+
];
|
|
731
|
+
if (allOptions.length === 0) {
|
|
732
|
+
console.log('No browsers detected.\n');
|
|
733
|
+
console.log(' Extension: click the Playwriter icon on a tab to connect');
|
|
734
|
+
console.log(' Direct: open chrome://inspect/#remote-debugging in Chrome');
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
printBrowserTable(allOptions);
|
|
738
|
+
console.log('');
|
|
739
|
+
const hasDirectInstances = allOptions.some((opt) => {
|
|
740
|
+
return opt.type === 'direct';
|
|
741
|
+
});
|
|
742
|
+
if (hasDirectInstances) {
|
|
743
|
+
console.log(pc.dim('Connect with: playwriter session new --direct'));
|
|
744
|
+
console.log(pc.dim('Chrome may ask to approve the debugging connection.'));
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
console.log(pc.dim('Use with: playwriter session new [--browser <key>]'));
|
|
748
|
+
}
|
|
749
|
+
});
|
|
446
750
|
cli.command('logfile', 'Print the path to the relay server log file').action(() => {
|
|
447
751
|
console.log(`relay: ${LOG_FILE_PATH}`);
|
|
448
752
|
console.log(`cdp: ${LOG_CDP_FILE_PATH}`);
|