playwright-repl 0.19.0 → 0.21.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # playwright-repl (CLI)
1
+ # playwright-repl
2
2
 
3
- Interactive terminal REPL for browser automation powered by Playwright. Type a command, see the result instantly — no code, no boilerplate.
3
+ Interactive terminal REPL for browser automation powered by Playwright. Type a command, see the result — no code, no boilerplate.
4
4
 
5
5
  ```bash
6
6
  npm install -g playwright-repl
@@ -9,9 +9,9 @@ playwright-repl --headed
9
9
 
10
10
  ```
11
11
  pw> goto https://demo.playwright.dev/todomvc/
12
- pw> fill "What needs to be done?" "Buy groceries"
12
+ pw> fill "What needs to be done?" Buy groceries
13
13
  pw> press Enter
14
- pw> verify-text "1 item left"
14
+ pw> verify text 1 item left
15
15
  pw> screenshot
16
16
  ```
17
17
 
@@ -26,10 +26,10 @@ npx playwright install
26
26
 
27
27
  ## Connection Modes
28
28
 
29
- | Mode | Flag | Browser Source | Use Case |
30
- |------|------|---------------|----------|
31
- | **Standalone** | *(default)* | Launches new Chromium via Playwright | General automation, CI |
32
- | **Bridge** | `--bridge` | Your real Chrome (via extension) | Drive your existing session — cookies and logins intact |
29
+ | Mode | Flag | How it works |
30
+ |------|------|--------------|
31
+ | **Standalone** | *(default)* | Launches Chromium via Playwright |
32
+ | **Bridge** | `--bridge` | Connects to your real Chrome via Dramaturg extension — cookies and logins intact |
33
33
 
34
34
  ### Standalone
35
35
 
@@ -44,18 +44,18 @@ playwright-repl --persistent # keeps profile between sessions
44
44
 
45
45
  ### Bridge
46
46
 
47
- The bridge mode starts an interactive REPL that acts as a **remote console for the Chrome extension** — your terminal becomes the command input for the extension's browser session. Commands run inside your real Chrome with your existing cookies and logins.
47
+ The bridge mode turns your terminal into a remote console for the Chrome extension. Commands run inside your real Chrome with your existing cookies and logins.
48
48
 
49
49
  ```bash
50
- playwright-repl --bridge # start bridge server, wait for extension to connect
50
+ playwright-repl --bridge # start bridge server, wait for extension
51
51
  playwright-repl --bridge --replay script.pw # replay a script via bridge
52
52
  playwright-repl --bridge --replay examples/ # replay all .pw files
53
53
  playwright-repl --bridge --bridge-port 9877 # custom port (default 9876)
54
54
  ```
55
55
 
56
- The extension connects automatically — no need to open the side panel. Once connected, all commands run against the active tab in your real browser.
56
+ The extension connects automatically — no need to open the side panel.
57
57
 
58
- > Requires the playwright-repl Chrome extension — see [packages/extension/README.md](../extension/README.md) for setup.
58
+ > Requires the [Dramaturg Chrome extension](../extension/README.md).
59
59
 
60
60
  ## Usage
61
61
 
@@ -86,10 +86,10 @@ echo -e "goto https://example.com\nsnapshot" | playwright-repl
86
86
 
87
87
  | Mode | Standalone | Bridge |
88
88
  |------|:---:|:---:|
89
- | **Keyword** — `click "Sign in"`, `goto https://...` | | |
90
- | **Playwright API / JS** — `await page.title()`, `1 + 1` | | auto-detected |
89
+ | **Keyword** — `click "Sign in"`, `goto https://...` | Yes | Yes |
90
+ | **Playwright API / JS** — `await page.title()`, `1 + 1` | No | Yes (auto-detected) |
91
91
 
92
- Bridge mode auto-detects Playwright API and JavaScript expressions — just type them directly. For DOM access use `await page.evaluate(() => document.title)`. For the full keyword command list, see [Command Reference](#command-reference).
92
+ Bridge mode auto-detects Playwright API and JavaScript expressions. For DOM access use `await page.evaluate(() => document.title)`. For keyword commands, see [Command Reference](#command-reference).
93
93
 
94
94
  ## CLI Options
95
95
 
@@ -127,7 +127,7 @@ Bridge mode auto-detects Playwright API and JavaScript expressions — just type
127
127
 
128
128
  ## Recording & Replay
129
129
 
130
- Record your browser interactions and replay them later — great for regression tests, onboarding demos, or CI smoke tests.
130
+ Record browser interactions and replay them later — great for regression tests, onboarding demos, or CI smoke tests.
131
131
 
132
132
  ### Record
133
133
 
@@ -139,9 +139,9 @@ playwright-repl --record my-test.pw --headed
139
139
  pw> .record my-test
140
140
  ⏺ Recording to my-test.pw
141
141
  pw> goto https://demo.playwright.dev/todomvc/
142
- pw> fill "What needs to be done?" "Buy groceries"
142
+ pw> fill "What needs to be done?" Buy groceries
143
143
  pw> press Enter
144
- pw> verify-text "1 item left"
144
+ pw> verify text 1 item left
145
145
  pw> .save
146
146
  ✓ Saved 4 commands to my-test.pw
147
147
  ```
@@ -162,7 +162,7 @@ playwright-repl --replay examples/ --silent
162
162
  pw> .replay my-test.pw
163
163
  ```
164
164
 
165
- Multi-file replay runs all files sequentially, writes a `replay-<timestamp>.log` with per-command results, and prints a pass/fail summary. Exit code 0 if all pass, 1 if any fail.
165
+ Multi-file replay runs all files sequentially, writes a `replay-<timestamp>.log`, and prints a pass/fail summary. Exit code 0 if all pass, 1 if any fail.
166
166
 
167
167
  ### .pw File Format
168
168
 
@@ -173,10 +173,10 @@ Plain text — human-readable, diffable, version-controllable:
173
173
  # App: https://demo.playwright.dev/todomvc/
174
174
 
175
175
  goto https://demo.playwright.dev/todomvc/
176
- fill "What needs to be done?" "Buy groceries"
176
+ fill "What needs to be done?" Buy groceries
177
177
  press Enter
178
- verify-text "Buy groceries"
179
- verify-text "1 item left"
178
+ verify text Buy groceries
179
+ verify text 1 item left
180
180
  ```
181
181
 
182
182
  ## Examples
@@ -200,7 +200,7 @@ playwright-repl --replay examples/ --silent
200
200
  ## Requirements
201
201
 
202
202
  - Node.js >= 20
203
- - `playwright` >= 1.59.0-alpha (browser binaries only needed for standalone mode)
203
+ - `playwright` >= 1.59
204
204
 
205
205
  ---
206
206
 
@@ -273,11 +273,11 @@ pw> highlight --clear # dismiss the highlight overlay
273
273
 
274
274
  | Command | Alias | Description |
275
275
  |---------|-------|-------------|
276
- | `verify-text <text>` | `vt` | Verify text is visible on page |
277
- | `verify-no-text <text>` | `vnt` | Verify text is not visible |
278
- | `verify-element <role> <name>` | `ve` | Verify element exists by role and name |
279
- | `verify-value <ref> <value>` | `vv` | Verify input/select/checkbox value |
280
- | `verify-list <ref> <items>` | `vl` | Verify list contains expected items |
276
+ | `verify text <text>` | `vt` | Verify text is visible on page |
277
+ | `verify no-text <text>` | `vnt` | Verify text is not visible |
278
+ | `verify element <role> <name>` | `ve` | Verify element exists by role and name |
279
+ | `verify value <ref> <value>` | `vv` | Verify input/select/checkbox value |
280
+ | `verify list <ref> <items>` | `vl` | Verify list contains expected items |
281
281
 
282
282
  ### Tabs
283
283
 
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Engine — in-process Playwright backend.
3
+ *
4
+ * Wraps BrowserServerBackend directly, eliminating the daemon process.
5
+ * Provides the same interface as DaemonConnection: run(args), connected, close().
6
+ *
7
+ * Three connection modes:
8
+ * - launch: new browser via Playwright (default)
9
+ * - connect: existing Chrome via CDP port (--connect [port])
10
+ * - extension: DevTools extension CDP relay (--extension)
11
+ */
12
+ import type { EngineOpts, EngineResult, ParsedArgs } from '@playwright-repl/core';
13
+ export type { EngineOpts, EngineResult, ParsedArgs };
14
+ interface PlaywrightDeps {
15
+ BrowserServerBackend: new (config: any, factory: any, opts: any) => any;
16
+ contextFactory: (config: any) => {
17
+ createContext: (info: any, signal: AbortSignal, opts: any) => Promise<{
18
+ browserContext: any;
19
+ close: () => Promise<void>;
20
+ }>;
21
+ };
22
+ playwright: any;
23
+ registry: {
24
+ findExecutable: (name: string) => {
25
+ executablePath: () => string | undefined;
26
+ } | undefined;
27
+ };
28
+ resolveConfig: (config: any) => Promise<any> | any;
29
+ commands: Record<string, any>;
30
+ parseCommand: (command: any, args: any) => {
31
+ toolName: string;
32
+ toolParams: Record<string, any>;
33
+ };
34
+ }
35
+ export declare class Engine {
36
+ private _deps;
37
+ private _backend;
38
+ private _browserContext;
39
+ private _close;
40
+ private _connected;
41
+ private _commandServer;
42
+ private _chromeProc;
43
+ private _isReconnecting;
44
+ private _reconnectInfo;
45
+ constructor(deps?: PlaywrightDeps);
46
+ get connected(): boolean;
47
+ /**
48
+ * Start the engine with given options.
49
+ */
50
+ start(opts?: EngineOpts): Promise<void>;
51
+ /**
52
+ * Run a command given minimist-parsed args.
53
+ * Returns { text, isError } matching DaemonConnection.run() shape.
54
+ */
55
+ run(args: ParsedArgs): Promise<EngineResult>;
56
+ /**
57
+ * Select the Playwright page matching the given URL.
58
+ * Uses backend.callTool('browser_tabs') to properly update the tab tracker.
59
+ */
60
+ selectPageByUrl(targetUrl: string): Promise<void>;
61
+ /**
62
+ * Shut down the browser and backend.
63
+ */
64
+ close(): Promise<void>;
65
+ private _connectToCdp;
66
+ private _scheduleReconnect;
67
+ private _doReconnect;
68
+ private _buildConfig;
69
+ }
70
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAElF,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,CAAC;AAGrD,UAAU,cAAc;IACtB,oBAAoB,EAAE,KAAK,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK,GAAG,CAAC;IACxE,cAAc,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK;QAAE,aAAa,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC;YAAE,cAAc,EAAE,GAAG,CAAC;YAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;IAChK,UAAU,EAAE,GAAG,CAAC;IAChB,QAAQ,EAAE;QAAE,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK;YAAE,cAAc,EAAE,MAAM,MAAM,GAAG,SAAS,CAAA;SAAE,GAAG,SAAS,CAAA;KAAE,CAAC;IACzG,aAAa,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACnD,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,YAAY,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,KAAK;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAAE,CAAC;CAClG;AA6BD,qBAAa,MAAM;IAEjB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,QAAQ,CAAa;IAC7B,OAAO,CAAC,eAAe,CAAa;IACpC,OAAO,CAAC,MAAM,CAAsC;IACpD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,cAAc,CAA+C;IACrE,OAAO,CAAC,WAAW,CAAwD;IAC3E,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,cAAc,CAAmE;gBAG7E,IAAI,CAAC,EAAE,cAAc;IAIjC,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED;;OAEG;IACG,KAAK,CAAC,IAAI,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAsGjD;;;OAGG;IACG,GAAG,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC;IAiFlD;;;OAGG;IACG,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBvD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAuBd,aAAa;IAuC3B,OAAO,CAAC,kBAAkB;YAQZ,YAAY;YAsDZ,YAAY;CA0D3B"}
package/dist/engine.js ADDED
@@ -0,0 +1,458 @@
1
+ /**
2
+ * Engine — in-process Playwright backend.
3
+ *
4
+ * Wraps BrowserServerBackend directly, eliminating the daemon process.
5
+ * Provides the same interface as DaemonConnection: run(args), connected, close().
6
+ *
7
+ * Three connection modes:
8
+ * - launch: new browser via Playwright (default)
9
+ * - connect: existing Chrome via CDP port (--connect [port])
10
+ * - extension: DevTools extension CDP relay (--extension)
11
+ */
12
+ import { createRequire } from 'node:module';
13
+ import path from 'node:path';
14
+ import url from 'node:url';
15
+ import { replVersion } from '@playwright-repl/core';
16
+ /* eslint-enable @typescript-eslint/no-explicit-any */
17
+ // ─── Lazy-loaded Playwright dependencies ────────────────────────────────────
18
+ let _deps;
19
+ function loadDeps() {
20
+ if (_deps)
21
+ return _deps;
22
+ const require = createRequire(import.meta.url);
23
+ // Resolve absolute paths to bypass Playwright's exports map.
24
+ const pwDir = path.dirname(require.resolve('playwright/package.json'));
25
+ const pwReq = (sub) => require(path.join(pwDir, sub));
26
+ const pwCoreDir = path.dirname(require.resolve('playwright-core/package.json'));
27
+ const pwCoreReq = (sub) => require(path.join(pwCoreDir, sub));
28
+ _deps = {
29
+ BrowserServerBackend: pwReq('lib/mcp/browser/browserServerBackend.js').BrowserServerBackend,
30
+ contextFactory: pwReq('lib/mcp/browser/browserContextFactory.js').contextFactory,
31
+ playwright: require('playwright-core'),
32
+ registry: pwCoreReq('lib/server/registry/index.js').registry,
33
+ resolveConfig: pwReq('lib/mcp/browser/config.js').resolveConfig,
34
+ commands: pwReq('lib/cli/daemon/commands.js').commands,
35
+ parseCommand: pwReq('lib/cli/daemon/command.js').parseCommand,
36
+ };
37
+ return _deps;
38
+ }
39
+ // ─── Engine ─────────────────────────────────────────────────────────────────
40
+ export class Engine {
41
+ /* eslint-disable @typescript-eslint/no-explicit-any */
42
+ _deps;
43
+ _backend = null;
44
+ _browserContext = null;
45
+ _close = null;
46
+ _connected = false;
47
+ _commandServer = null;
48
+ _chromeProc = null;
49
+ _isReconnecting = false;
50
+ _reconnectInfo = null;
51
+ /* eslint-enable @typescript-eslint/no-explicit-any */
52
+ constructor(deps) {
53
+ this._deps = deps;
54
+ }
55
+ get connected() {
56
+ return this._connected;
57
+ }
58
+ /**
59
+ * Start the engine with given options.
60
+ */
61
+ async start(opts = {}) {
62
+ const deps = this._deps || loadDeps();
63
+ const config = await this._buildConfig(opts, deps);
64
+ const cwd = url.pathToFileURL(process.cwd()).href;
65
+ const clientInfo = {
66
+ name: 'playwright-repl',
67
+ version: replVersion,
68
+ roots: [{ uri: cwd, name: 'cwd' }],
69
+ timestamp: Date.now(),
70
+ };
71
+ // Choose context factory based on mode.
72
+ if (opts.extension) {
73
+ const serverPort = opts.port || 6781;
74
+ const cdpPort = opts.cdpPort || 9222;
75
+ // 1. Start CommandServer for panel HTTP commands.
76
+ const { CommandServer } = await import('@playwright-repl/core');
77
+ const cmdServer = new CommandServer(this);
78
+ await cmdServer.start(serverPort);
79
+ this._commandServer = cmdServer;
80
+ console.log(`CommandServer listening on http://localhost:${serverPort}`);
81
+ // 2. Spawn Chrome (only with --spawn).
82
+ if (opts.spawn) {
83
+ const extPath = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '../../extension/dist');
84
+ const execInfo = deps.registry.findExecutable(opts.browser || 'chrome');
85
+ const execPath = execInfo?.executablePath();
86
+ if (!execPath)
87
+ throw new Error('Chrome executable not found. Make sure Chrome is installed.');
88
+ // Chrome 136+ requires --user-data-dir for CDP. Use a dedicated profile dir.
89
+ const os = await import('node:os');
90
+ const fs = await import('node:fs');
91
+ const userDataDir = opts.profile || path.join(os.default.homedir(), '.playwright-repl', 'chrome-profile');
92
+ fs.default.mkdirSync(userDataDir, { recursive: true });
93
+ const chromeArgs = [
94
+ `--remote-debugging-port=${cdpPort}`,
95
+ `--user-data-dir=${userDataDir}`,
96
+ `--load-extension=${extPath}`,
97
+ '--no-first-run',
98
+ '--no-default-browser-check',
99
+ ];
100
+ const { spawn } = await import('node:child_process');
101
+ const chromeProc = spawn(execPath, chromeArgs, {
102
+ detached: true, stdio: 'ignore',
103
+ });
104
+ chromeProc.unref();
105
+ this._chromeProc = chromeProc;
106
+ console.log(`Chrome profile: ${userDataDir}`);
107
+ }
108
+ else {
109
+ console.log('Connecting to existing Chrome on port ' + cdpPort + ' (use --spawn to launch Chrome automatically)');
110
+ }
111
+ // 3. Wait for Chrome CDP to be ready (30s timeout).
112
+ console.log('Waiting for Chrome CDP...');
113
+ const cdpUrl = `http://localhost:${cdpPort}`;
114
+ const cdpTimeout = 30_000;
115
+ const cdpStart = Date.now();
116
+ while (true) {
117
+ if (Date.now() - cdpStart > cdpTimeout) {
118
+ throw new Error(`Timeout: Chrome CDP not available at ${cdpUrl} after ${cdpTimeout / 1000}s`);
119
+ }
120
+ try {
121
+ const res = await fetch(`${cdpUrl}/json/version`);
122
+ if (res.ok)
123
+ break;
124
+ }
125
+ catch { /* retry */ }
126
+ await new Promise(r => setTimeout(r, 500));
127
+ }
128
+ console.log('Chrome CDP ready. Connecting Playwright...');
129
+ // 4. Connect Playwright via CDP.
130
+ config.browser.cdpEndpoint = cdpUrl;
131
+ this._reconnectInfo = { opts, config, clientInfo };
132
+ await this._connectToCdp(deps, config, clientInfo);
133
+ console.log('Ready! Side panel can send commands.');
134
+ }
135
+ else {
136
+ // Launch/connect mode: eagerly create context for immediate feedback.
137
+ const factory = deps.contextFactory(config);
138
+ const { browserContext, close } = await factory.createContext(clientInfo, new AbortController().signal, {});
139
+ this._browserContext = browserContext;
140
+ this._close = close;
141
+ const existingContextFactory = {
142
+ createContext: () => Promise.resolve({ browserContext, close }),
143
+ };
144
+ this._backend = new deps.BrowserServerBackend(config, existingContextFactory, { allTools: true });
145
+ await this._backend.initialize?.(clientInfo);
146
+ this._connected = true;
147
+ browserContext.on('close', () => {
148
+ this._connected = false;
149
+ });
150
+ }
151
+ }
152
+ /**
153
+ * Run a command given minimist-parsed args.
154
+ * Returns { text, isError } matching DaemonConnection.run() shape.
155
+ */
156
+ async run(args) {
157
+ if (!this._backend)
158
+ throw new Error('Engine not started');
159
+ // ── highlight → run-code translation ──
160
+ if (args._[0] === 'highlight') {
161
+ if (args.clear) {
162
+ args = { _: ['run-code', `async (page) => { await page.locator('#__pw_clear__').highlight().catch(() => {}); return "Cleared"; }`] };
163
+ }
164
+ else {
165
+ const parts = args._.slice(1);
166
+ if (!parts.length)
167
+ return { text: 'Usage: highlight <locator>', isError: true };
168
+ // highlight <ref> → use aria-ref selector
169
+ if (parts.length === 1 && /^e\d+$/.test(parts[0])) {
170
+ args = { _: ['run-code', `async (page) => { await page.locator('aria-ref=${parts[0]}').highlight(); return "Highlighted"; }`] };
171
+ }
172
+ else {
173
+ const nth = args.nth !== undefined ? parseInt(String(args.nth), 10) : undefined;
174
+ let locExpr;
175
+ // highlight <role> "<name>" → getByRole(role, { name })
176
+ if (parts.length >= 2 && /^[a-z]+$/.test(parts[0])) {
177
+ const role = parts[0];
178
+ const name = parts.slice(1).join(' ');
179
+ locExpr = `page.getByRole(${JSON.stringify(role)}, { name: ${JSON.stringify(name)}, exact: true })`;
180
+ }
181
+ else {
182
+ const loc = parts.join(' ');
183
+ const isSelector = /[.#\[\]>:=]/.test(loc);
184
+ locExpr = isSelector
185
+ ? `page.locator(${JSON.stringify(loc)})`
186
+ : `page.getByText(${JSON.stringify(loc)})`;
187
+ }
188
+ if (nth !== undefined)
189
+ locExpr += `.nth(${nth})`;
190
+ args = { _: ['run-code', `async (page) => { await ${locExpr}.highlight(); return "Highlighted"; }`] };
191
+ }
192
+ }
193
+ }
194
+ // ── >> chaining → run-code translation ──
195
+ const LOCATOR_ACTIONS = {
196
+ click: 'click', dblclick: 'dblclick', hover: 'hover',
197
+ check: 'check', uncheck: 'uncheck',
198
+ fill: 'fill', select: 'selectOption',
199
+ };
200
+ if (LOCATOR_ACTIONS[args._[0]] && args._.some(a => a.includes('>>'))) {
201
+ const action = LOCATOR_ACTIONS[args._[0]];
202
+ const positional = args._.slice(1);
203
+ // Find last >> — everything up to the token after it is the selector,
204
+ // everything after that is the action argument (e.g., value for fill).
205
+ let lastChainIdx = -1;
206
+ for (let i = 0; i < positional.length; i++) {
207
+ if (positional[i] === '>>' || positional[i].includes('>>'))
208
+ lastChainIdx = i;
209
+ }
210
+ const selectorEnd = positional[lastChainIdx] !== '>>' && positional[lastChainIdx]?.includes('>>')
211
+ ? lastChainIdx // >> inside quoted token like ".nav >> button"
212
+ : lastChainIdx + 1;
213
+ const selector = positional.slice(0, selectorEnd + 1).join(' ');
214
+ const rest = positional.slice(selectorEnd + 1).join(' ');
215
+ const locExpr = `page.locator(${JSON.stringify(selector)})`;
216
+ const actionCall = rest
217
+ ? `${locExpr}.${action}(${JSON.stringify(rest)})`
218
+ : `${locExpr}.${action}()`;
219
+ args = { _: ['run-code', `async (page) => { await ${actionCall}; return "Done"; }`] };
220
+ }
221
+ const deps = this._deps || loadDeps();
222
+ const command = deps.commands[args._[0]];
223
+ if (!command)
224
+ throw new Error(`Unknown command: ${args._[0]}`);
225
+ const { toolName, toolParams } = deps.parseCommand(command, args);
226
+ // Commands like "close", "list", "kill-all" have empty toolName.
227
+ if (!toolName)
228
+ return { text: `Command "${args._[0]}" is not supported in engine mode.`, isError: true };
229
+ toolParams._meta = { cwd: args.cwd || process.cwd() };
230
+ const response = await this._backend.callTool(toolName, toolParams);
231
+ return formatResult(response);
232
+ }
233
+ /**
234
+ * Select the Playwright page matching the given URL.
235
+ * Uses backend.callTool('browser_tabs') to properly update the tab tracker.
236
+ */
237
+ async selectPageByUrl(targetUrl) {
238
+ if (!this._browserContext || !this._backend || !targetUrl)
239
+ return;
240
+ const pages = this._browserContext.pages();
241
+ const normalize = (u) => {
242
+ try {
243
+ const p = new URL(u);
244
+ return (p.origin + p.pathname).replace(/\/+$/, '');
245
+ }
246
+ catch {
247
+ return u.replace(/\/+$/, '');
248
+ }
249
+ };
250
+ const target = normalize(targetUrl);
251
+ for (let i = 0; i < pages.length; i++) {
252
+ if (normalize(pages[i].url()) === target) {
253
+ try {
254
+ await this._backend.callTool('browser_tabs', { action: 'select', index: i });
255
+ }
256
+ catch { /* ignore */ }
257
+ return;
258
+ }
259
+ }
260
+ }
261
+ /**
262
+ * Shut down the browser and backend.
263
+ */
264
+ async close() {
265
+ this._connected = false;
266
+ if (this._commandServer) {
267
+ await this._commandServer.close();
268
+ this._commandServer = null;
269
+ }
270
+ if (this._backend) {
271
+ this._backend.serverClosed();
272
+ this._backend = null;
273
+ }
274
+ if (this._close) {
275
+ await this._close();
276
+ this._close = null;
277
+ }
278
+ if (this._chromeProc) {
279
+ try {
280
+ this._chromeProc.kill();
281
+ }
282
+ catch { /* ignore */ }
283
+ this._chromeProc = null;
284
+ }
285
+ }
286
+ // ─── CDP connect / reconnect ─────────────────────────────────────────────
287
+ /* eslint-disable @typescript-eslint/no-explicit-any */
288
+ async _connectToCdp(deps, config, clientInfo) {
289
+ const factory = deps.contextFactory(config);
290
+ const { browserContext, close } = await factory.createContext(clientInfo, new AbortController().signal, {});
291
+ this._browserContext = browserContext;
292
+ this._close = close;
293
+ const existingContextFactory = {
294
+ createContext: () => Promise.resolve({ browserContext, close }),
295
+ };
296
+ this._backend = new deps.BrowserServerBackend(config, existingContextFactory, { allTools: true });
297
+ await this._backend.initialize?.(clientInfo);
298
+ this._connected = true;
299
+ // Auto-select the first visible web page.
300
+ const pages = browserContext.pages();
301
+ const INTERNAL = /^(chrome|devtools|chrome-extension|about):/;
302
+ let selectedIdx = -1;
303
+ for (let i = 0; i < pages.length; i++) {
304
+ const pageUrl = pages[i].url();
305
+ if (!pageUrl || INTERNAL.test(pageUrl))
306
+ continue;
307
+ try {
308
+ const state = await pages[i].evaluate(() => document.visibilityState);
309
+ if (state === 'visible' && selectedIdx === -1) {
310
+ selectedIdx = i;
311
+ }
312
+ }
313
+ catch { /* skip */ }
314
+ }
315
+ if (selectedIdx > 0) {
316
+ await this._backend.callTool('browser_tabs', { action: 'select', index: selectedIdx });
317
+ }
318
+ browserContext.on('close', () => {
319
+ this._connected = false;
320
+ if (this._reconnectInfo)
321
+ this._scheduleReconnect();
322
+ });
323
+ }
324
+ _scheduleReconnect() {
325
+ if (this._isReconnecting)
326
+ return;
327
+ this._isReconnecting = true;
328
+ this._doReconnect().catch((e) => {
329
+ console.warn('Reconnection failed:', e instanceof Error ? e.message : String(e));
330
+ });
331
+ }
332
+ async _doReconnect() {
333
+ if (!this._reconnectInfo)
334
+ return;
335
+ const { opts, config, clientInfo } = this._reconnectInfo;
336
+ const deps = this._deps || loadDeps();
337
+ const cdpPort = opts.cdpPort || 9222;
338
+ const cdpUrl = `http://localhost:${cdpPort}`;
339
+ console.log('Browser context closed. Waiting for Chrome to reconnect...');
340
+ let attempt = 0;
341
+ while (!this._connected) {
342
+ attempt++;
343
+ await new Promise(r => setTimeout(r, 1500));
344
+ // Check if engine was closed during reconnect
345
+ if (!this._reconnectInfo) {
346
+ console.log('Reconnection cancelled (engine closed).');
347
+ break;
348
+ }
349
+ try {
350
+ const res = await fetch(`${cdpUrl}/json/version`);
351
+ if (!res.ok) {
352
+ console.warn(`Reconnect attempt ${attempt}: CDP responded with status ${res.status}`);
353
+ continue;
354
+ }
355
+ }
356
+ catch (e) {
357
+ console.warn(`Reconnect attempt ${attempt}: CDP not available (${e instanceof Error ? e.message : String(e)})`);
358
+ continue;
359
+ }
360
+ // CDP is responding — reconnect
361
+ try {
362
+ // Clean up old backend before reconnecting
363
+ if (this._backend) {
364
+ try {
365
+ this._backend.serverClosed();
366
+ }
367
+ catch { /* ignore */ }
368
+ }
369
+ if (this._close) {
370
+ try {
371
+ await this._close();
372
+ }
373
+ catch { /* ignore */ }
374
+ }
375
+ this._backend = null;
376
+ this._browserContext = null;
377
+ this._close = null;
378
+ config.browser.cdpEndpoint = cdpUrl;
379
+ await this._connectToCdp(deps, config, clientInfo);
380
+ console.log(`Reconnected to Chrome CDP after ${attempt} attempt(s).`);
381
+ }
382
+ catch (e) {
383
+ console.warn(`Reconnect attempt ${attempt}: connection failed (${e instanceof Error ? e.message : String(e)})`);
384
+ }
385
+ }
386
+ this._isReconnecting = false;
387
+ }
388
+ /* eslint-enable @typescript-eslint/no-explicit-any */
389
+ // ─── Config builder ───────────────────────────────────────────────────────
390
+ async _buildConfig(opts, deps) {
391
+ const config = {
392
+ browser: {
393
+ browserName: 'chromium',
394
+ launchOptions: {
395
+ channel: 'chrome',
396
+ headless: !opts.headed,
397
+ },
398
+ contextOptions: {
399
+ viewport: null,
400
+ },
401
+ isolated: false,
402
+ userDataDir: undefined,
403
+ cdpEndpoint: undefined,
404
+ },
405
+ server: {},
406
+ network: {},
407
+ timeouts: {
408
+ action: 5000,
409
+ navigation: 15000,
410
+ },
411
+ };
412
+ // Browser selection
413
+ if (opts.browser) {
414
+ switch (opts.browser) {
415
+ case 'firefox':
416
+ config.browser.browserName = 'firefox';
417
+ config.browser.launchOptions.channel = undefined;
418
+ break;
419
+ case 'webkit':
420
+ config.browser.browserName = 'webkit';
421
+ config.browser.launchOptions.channel = undefined;
422
+ break;
423
+ default:
424
+ // chrome, msedge, chrome-beta, etc.
425
+ config.browser.browserName = 'chromium';
426
+ config.browser.launchOptions.channel = opts.browser;
427
+ break;
428
+ }
429
+ }
430
+ // Persistent profile
431
+ if (opts.persistent || opts.profile) {
432
+ config.browser.userDataDir = opts.profile || undefined;
433
+ }
434
+ else if (!opts.extension) {
435
+ config.browser.isolated = true;
436
+ }
437
+ // CDP connect mode
438
+ if (opts.connect) {
439
+ const port = typeof opts.connect === 'number' ? opts.connect : 9222;
440
+ config.browser.cdpEndpoint = `http://localhost:${port}`;
441
+ config.browser.isolated = false;
442
+ }
443
+ return await deps.resolveConfig(config);
444
+ }
445
+ }
446
+ function formatResult(result) {
447
+ const isError = result.isError;
448
+ let text;
449
+ let image;
450
+ for (const item of result.content) {
451
+ if (item.type === 'text' && !text)
452
+ text = item.text;
453
+ if (item.type === 'image' && !image)
454
+ image = `data:${item.mimeType || 'image/png'};base64,${item.data}`;
455
+ }
456
+ return { isError, text, image };
457
+ }
458
+ //# sourceMappingURL=engine.js.map