playwright-repl 0.26.0 → 0.27.0

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/repl.js CHANGED
@@ -8,9 +8,7 @@ import path from 'node:path';
8
8
  import fs from 'node:fs';
9
9
  import os from 'node:os';
10
10
  import http from 'node:http';
11
- import { replVersion, parseInput, ALIASES, ALL_COMMANDS, buildCompletionItems, c, prettyJson, BridgeServer, COMMANDS, CATEGORIES, JS_CATEGORIES, filterResponse as filterResponseBase, resolveArgs, handleLocalCommand, } from '@playwright-repl/core';
12
- import { Engine } from './engine.js';
13
- import { SessionManager } from './recorder.js';
11
+ import { replVersion, parseInput, ALIASES, ALL_COMMANDS, c, prettyJson, CDPRelayServer, COMMANDS, CATEGORIES, JS_CATEGORIES, filterResponse as filterResponseBase, resolveArgs, resolveCommand, } from '@playwright-repl/core';
14
12
  // ─── Response filtering ─────────────────────────────────────────────────────
15
13
  /** CLI wrapper: uses core filterResponse + ANSI-colors for error lines. */
16
14
  export function filterResponse(text, cmdName, opts) {
@@ -23,16 +21,14 @@ export function filterResponse(text, cmdName, opts) {
23
21
  return body.replace(/^(Error: .*)$/gm, `${c.red}$1${c.reset}`);
24
22
  }
25
23
  // ─── Meta-command handlers ──────────────────────────────────────────────────
26
- export function showHelp(bridge = false) {
24
+ export function showHelp() {
27
25
  console.log(`\n${c.bold}Available commands:${c.reset}`);
28
26
  for (const [cat, cmds] of Object.entries(CATEGORIES)) {
29
27
  console.log(` ${c.bold}${cat}:${c.reset} ${cmds.join(', ')}`);
30
28
  }
31
- if (bridge) {
32
- console.log(`\n${c.bold}JavaScript mode:${c.reset}`);
33
- console.log(` ${c.dim}Use Playwright API directly: await page.title(), page.locator('h1').click(), ...${c.reset}`);
34
- console.log(` ${c.dim}Type .help js for available Playwright methods${c.reset}`);
35
- }
29
+ console.log(`\n${c.bold}JavaScript mode:${c.reset}`);
30
+ console.log(` ${c.dim}Use Playwright API directly: await page.title(), page.locator('h1').click(), ...${c.reset}`);
31
+ console.log(` ${c.dim}Type .help js for available Playwright methods${c.reset}`);
36
32
  console.log(`\n ${c.dim}Type .help <command> for details, or .aliases for shortcuts${c.reset}`);
37
33
  console.log(`\n${c.bold}REPL meta-commands:${c.reset}`);
38
34
  console.log(` .aliases Show command aliases`);
@@ -48,12 +44,8 @@ export function showHelp(bridge = false) {
48
44
  console.log(` .history clear Clear command history`);
49
45
  console.log(` .exit Exit REPL\n`);
50
46
  }
51
- export function showCommandHelp(cmd, bridge = false) {
47
+ export function showCommandHelp(cmd) {
52
48
  if (cmd === 'js' || cmd === 'javascript') {
53
- if (!bridge) {
54
- console.log(`\n${c.dim}JavaScript mode is only available in bridge mode (--bridge).${c.reset}\n`);
55
- return;
56
- }
57
49
  console.log(`\n${c.bold}JavaScript mode — Playwright API:${c.reset}`);
58
50
  console.log(` ${c.dim}Prefix with ${c.reset}await${c.dim} for async methods${c.reset}\n`);
59
51
  console.log(` ${c.bold}Available globals:${c.reset}`);
@@ -127,6 +119,55 @@ export async function handleClose(ctx) {
127
119
  console.error(`${c.red}Error:${c.reset} ${err.message}`);
128
120
  }
129
121
  }
122
+ // ─── Recording commands (start-recording, stop-recording, etc.) ────────────
123
+ function handleRecordingCommand(ctx, cmdName, args) {
124
+ const { session } = ctx;
125
+ if (cmdName === 'start-recording') {
126
+ try {
127
+ const filename = args._[1] || undefined;
128
+ const file = session.startRecording(filename);
129
+ console.log(`${c.red}⏺${c.reset} Recording to ${c.bold}${file}${c.reset}`);
130
+ ctx.rl?.setPrompt(promptStr(ctx));
131
+ }
132
+ catch (err) {
133
+ console.log(`${c.yellow}${err.message}${c.reset}`);
134
+ }
135
+ return true;
136
+ }
137
+ if (cmdName === 'stop-recording') {
138
+ try {
139
+ const { filename, count } = session.save();
140
+ console.log(`${c.green}✓${c.reset} Saved ${count} commands to ${c.bold}${filename}${c.reset}`);
141
+ ctx.rl?.setPrompt(promptStr(ctx));
142
+ }
143
+ catch (err) {
144
+ console.log(`${c.yellow}${err.message}${c.reset}`);
145
+ }
146
+ return true;
147
+ }
148
+ if (cmdName === 'pause-recording') {
149
+ try {
150
+ const paused = session.togglePause();
151
+ console.log(paused ? `${c.yellow}⏸${c.reset} Recording paused` : `${c.red}⏺${c.reset} Recording resumed`);
152
+ }
153
+ catch (err) {
154
+ console.log(`${c.yellow}${err.message}${c.reset}`);
155
+ }
156
+ return true;
157
+ }
158
+ if (cmdName === 'discard-recording') {
159
+ try {
160
+ session.discard();
161
+ console.log(`${c.yellow}Recording discarded${c.reset}`);
162
+ ctx.rl?.setPrompt(promptStr(ctx));
163
+ }
164
+ catch (err) {
165
+ console.log(`${c.yellow}${err.message}${c.reset}`);
166
+ }
167
+ return true;
168
+ }
169
+ return false;
170
+ }
130
171
  // ─── Session meta-commands (.record, .save, .pause, .discard, .replay) ──────
131
172
  export function handleSessionCommand(ctx, line) {
132
173
  const { session } = ctx;
@@ -213,13 +254,23 @@ export async function processLine(ctx, line) {
213
254
  }
214
255
  // ── Inline replay ──────────────────────────────────────────────
215
256
  if (line.startsWith('.replay')) {
216
- const filename = line.split(/\s+/)[1];
257
+ const parts = line.split(/\s+/);
258
+ const filename = parts[1];
217
259
  if (!filename) {
218
- console.log(`${c.yellow}Usage: .replay <filename>${c.reset}`);
260
+ console.log(`${c.yellow}Usage: .replay <filename> [--variable key=value ...]${c.reset}`);
219
261
  return;
220
262
  }
263
+ // Parse --variable key=value args
264
+ const variables = {};
265
+ for (let i = 2; i < parts.length; i++) {
266
+ if (parts[i] === '--variable' && parts[i + 1]) {
267
+ const [key, ...rest] = parts[++i].split('=');
268
+ if (key && rest.length > 0)
269
+ variables[key] = rest.join('=');
270
+ }
271
+ }
221
272
  try {
222
- const player = ctx.session.startReplay(filename);
273
+ const player = ctx.session.startReplay(filename, false, Object.keys(variables).length > 0 ? variables : undefined);
223
274
  console.log(`${c.blue}▶${c.reset} Replaying ${c.bold}${filename}${c.reset} (${player.commands.length} commands)\n`);
224
275
  while (!player.done) {
225
276
  const cmd = player.next();
@@ -254,6 +305,9 @@ export async function processLine(ctx, line) {
254
305
  return handleKillAll(ctx);
255
306
  if (cmdName === 'close' || cmdName === 'close-all')
256
307
  return handleClose(ctx);
308
+ // ── Recording commands (start/stop/pause/discard-recording) ──
309
+ if (handleRecordingCommand(ctx, cmdName, args))
310
+ return;
257
311
  // ── Command transformations (verify, role-based, text, run-code) ──
258
312
  const resolved = resolveArgs(args);
259
313
  if (resolved._[0] === args._[0] && cmdName === 'verify') {
@@ -278,6 +332,10 @@ export async function processLine(ctx, line) {
278
332
  if (filtered !== null)
279
333
  console.log(filtered);
280
334
  }
335
+ // Feed snapshot to recorder for ref-to-locator resolution
336
+ if (cmdName === 'snapshot' && result?.text && !result.isError) {
337
+ ctx.session.setSnapshot(result.text);
338
+ }
281
339
  if (result?.isError)
282
340
  ctx.errors++;
283
341
  ctx.commandCount++;
@@ -583,7 +641,7 @@ function attachGhostCompletion(rl, items) {
583
641
  // ─── Multi-line completion check ────────────────────────────────────────────
584
642
  /**
585
643
  * Returns true if the code string has balanced brackets/parens/braces.
586
- * Used to detect multi-line continuation in bridge mode.
644
+ * Used to detect multi-line continuation.
587
645
  */
588
646
  function isComplete(code) {
589
647
  let depth = 0;
@@ -610,8 +668,8 @@ function isComplete(code) {
610
668
  }
611
669
  return depth <= 0;
612
670
  }
613
- // ─── Bridge shared helpers ───────────────────────────────────────────────────
614
- function displayBridgeResult(result, silent) {
671
+ // ─── Display helpers ────────────────────────────────────────────────────────
672
+ function displayResult(result, silent) {
615
673
  if (result.image) {
616
674
  const isPdf = result.image.startsWith('data:application/pdf');
617
675
  const outDir = path.join(os.homedir(), isPdf ? 'pw-pdfs' : 'pw-screenshots');
@@ -621,8 +679,7 @@ function displayBridgeResult(result, silent) {
621
679
  const outPath = path.join(outDir, `${prefix}-${Date.now()}${ext}`);
622
680
  const b64 = result.image.replace(/^data:[^;]+;base64,/, '');
623
681
  fs.writeFileSync(outPath, Buffer.from(b64, 'base64'));
624
- if (!silent)
625
- console.log(`${isPdf ? 'PDF' : 'Screenshot'} saved to ${outPath}`);
682
+ console.log(`${isPdf ? 'PDF' : 'Screenshot'} saved to ${outPath}`);
626
683
  }
627
684
  else if (result.text) {
628
685
  const t = result.text.trim();
@@ -634,109 +691,148 @@ function displayBridgeResult(result, silent) {
634
691
  }
635
692
  }
636
693
  }
637
- // ─── Bridge replay mode ──────────────────────────────────────────────────────
638
- async function runSingleBridgeFile(srv, file, step, silent, prefixed = false) {
639
- const log = (...args) => { if (!silent)
640
- console.log(...args); };
641
- const commands = loadReplayFile(file);
642
- if (!prefixed) {
643
- log(`${c.blue}▶${c.reset} Replaying ${c.bold}${file}${c.reset} (${commands.length} commands)\n`);
694
+ // ─── Relay execution (keyword + JS) ─────────────────────────────────────────
695
+ const AsyncFn = Object.getPrototypeOf(async function () { }).constructor;
696
+ function isRelayExpression(code) {
697
+ const trimmed = code.trim();
698
+ if (trimmed.includes('\n'))
699
+ return false;
700
+ const withoutTrailing = trimmed.replace(/;$/, '');
701
+ if (withoutTrailing.includes(';'))
702
+ return false;
703
+ if (/^(const |let |var |if |for |while |switch |try |class |function )/.test(trimmed))
704
+ return false;
705
+ return true;
706
+ }
707
+ function formatRelayResult(value) {
708
+ if (value === undefined || value === null)
709
+ return 'Done';
710
+ if (typeof value === 'string')
711
+ return value;
712
+ if (typeof value === 'number' || typeof value === 'boolean')
713
+ return String(value);
714
+ try {
715
+ return JSON.stringify(value, null, 2);
644
716
  }
645
- let commandsRun = 0;
646
- for (const cmd of commands) {
647
- commandsRun++;
648
- const indent = prefixed ? ' ' : '';
649
- log(`${indent}${c.dim}[${commandsRun}/${commands.length}]${c.reset} ${cmd}`);
650
- const startTime = performance.now();
651
- // Local commands handle in Node.js (e.g. video needs real filesystem)
652
- const localResult = await handleLocalCommand(cmd, srv.context);
653
- if (localResult) {
654
- const elapsed = (performance.now() - startTime).toFixed(0);
655
- log(localResult.isError ? `${c.red}${localResult.text}${c.reset}` : localResult.text);
656
- log(`${c.dim}(${elapsed}ms)${c.reset}`);
657
- if (localResult.isError) {
658
- return { passed: false, commandsRun, errorMsg: `failed at [${commandsRun}/${commands.length}]: ${cmd}` };
659
- }
660
- continue;
661
- }
662
- const result = await srv.run(cmd);
663
- const elapsed = (performance.now() - startTime).toFixed(0);
664
- displayBridgeResult(result, silent);
665
- log(`${c.dim}(${elapsed}ms)${c.reset}`);
666
- if (result.isError) {
667
- return { passed: false, commandsRun, errorMsg: `failed at [${commandsRun}/${commands.length}]: ${cmd}` };
717
+ catch {
718
+ return String(value);
719
+ }
720
+ }
721
+ async function relayExec(command, page, context, expect) {
722
+ const trimmed = command.trim();
723
+ // Keyword command resolveCommand jsExpr direct execution
724
+ const resolved = resolveCommand(trimmed);
725
+ if (resolved) {
726
+ try {
727
+ const fn = new AsyncFn('page', 'context', 'expect', resolved.jsExpr);
728
+ const result = await fn(page, context, expect);
729
+ return { text: formatRelayResult(result), isError: false };
668
730
  }
669
- if (step && commandsRun < commands.length) {
670
- await new Promise((resolve) => {
671
- process.stdout.write(`${c.dim} Press Enter to continue...${c.reset}`);
672
- process.stdin.once('data', () => { process.stdout.write('\r\x1b[K'); resolve(); });
673
- });
731
+ catch (e) {
732
+ return { text: e instanceof Error ? e.message : String(e), isError: true };
674
733
  }
675
734
  }
676
- if (!prefixed)
677
- log(`\n${c.green}✓${c.reset} Replay complete`);
678
- return { passed: true, commandsRun };
735
+ // JavaScript → AsyncFunction
736
+ const script = isRelayExpression(trimmed)
737
+ ? `return ${trimmed.replace(/;$/, '')}`
738
+ : trimmed;
739
+ try {
740
+ const fn = new AsyncFn('page', 'context', 'expect', script);
741
+ const result = await fn(page, context, expect);
742
+ return { text: formatRelayResult(result), isError: false };
743
+ }
744
+ catch (e) {
745
+ return { text: e instanceof Error ? e.message : String(e), isError: true };
746
+ }
679
747
  }
680
- async function runBridgeReplayMode(opts, srv) {
748
+ // ─── Relay replay mode ──────────────────────────────────────────────────────
749
+ async function runRelayReplayMode(opts, relay, browser, page, context, expect) {
681
750
  const silent = opts.silent || false;
682
751
  const log = (...args) => { if (!silent)
683
752
  console.log(...args); };
684
- log('Waiting for extension to connect...');
685
- try {
686
- await srv.waitForConnection(30000);
687
- }
688
- catch (err) {
689
- console.error(`${c.red}Error:${c.reset} ${err.message}`);
690
- await srv.close();
691
- process.exit(1);
692
- }
693
- log(`${c.green}✓${c.reset} Extension connected`);
694
753
  const files = resolveReplayFiles(opts.replay, ['.pw', '.js']);
695
754
  if (files.length === 0) {
696
755
  console.error(`${c.red}Error:${c.reset} No .pw or .js files found`);
697
- await srv.close();
756
+ await browser.close().catch(() => { });
757
+ if (relay)
758
+ await relay.close();
698
759
  process.exit(1);
699
760
  }
700
- if (files.length === 1) {
701
- const { passed } = await runSingleBridgeFile(srv, files[0], opts.step || false, silent);
702
- await srv.close();
703
- process.exit(passed ? 0 : 1);
704
- }
705
- // Multi-file
706
- log(`${c.blue}▶${c.reset} Running ${c.bold}${files.length}${c.reset} files\n`);
707
761
  const results = [];
708
762
  const totalStart = performance.now();
763
+ if (files.length > 1)
764
+ log(`${c.blue}▶${c.reset} Running ${c.bold}${files.length}${c.reset} files\n`);
709
765
  for (const file of files) {
710
766
  const basename = path.basename(file);
711
- log(`${c.blue}▶${c.reset} ${c.bold}${basename}${c.reset}`);
767
+ const commands = loadReplayFile(file);
768
+ const prefixed = files.length > 1;
769
+ if (prefixed)
770
+ log(`${c.blue}▶${c.reset} ${c.bold}${basename}${c.reset}`);
771
+ else
772
+ log(`${c.blue}▶${c.reset} Replaying ${c.bold}${file}${c.reset} (${commands.length} commands)\n`);
712
773
  const fileStart = performance.now();
713
- const { passed, commandsRun, errorMsg } = await runSingleBridgeFile(srv, file, opts.step || false, silent, true);
774
+ let commandsRun = 0;
775
+ let passed = true;
776
+ let errorMsg;
777
+ for (const cmd of commands) {
778
+ commandsRun++;
779
+ const indent = prefixed ? ' ' : '';
780
+ log(`${indent}${c.dim}[${commandsRun}/${commands.length}]${c.reset} ${cmd}`);
781
+ const startTime = performance.now();
782
+ const result = await relayExec(cmd, page, context, expect);
783
+ const elapsed = (performance.now() - startTime).toFixed(0);
784
+ if (result.text && result.text !== 'Done') {
785
+ log(`${indent}${result.isError ? `${c.red}${result.text}${c.reset}` : result.text}`);
786
+ }
787
+ log(`${indent}${c.dim}(${elapsed}ms)${c.reset}`);
788
+ if (result.isError) {
789
+ passed = false;
790
+ errorMsg = `failed at [${commandsRun}/${commands.length}]: ${cmd}`;
791
+ break;
792
+ }
793
+ if (opts.step && commandsRun < commands.length) {
794
+ await new Promise((resolve) => {
795
+ process.stdout.write(`${c.dim} Press Enter to continue...${c.reset}`);
796
+ process.stdin.once('data', () => { process.stdout.write('\r\x1b[K'); resolve(); });
797
+ });
798
+ }
799
+ }
714
800
  const fileElapsed = ((performance.now() - fileStart) / 1000).toFixed(1);
715
- const status = passed ? `${c.green}PASS${c.reset}` : `${c.red}FAIL${c.reset}`;
716
- log(` ${status} ${basename} ${c.dim}(${fileElapsed}s)${c.reset}\n`);
801
+ if (prefixed) {
802
+ const status = passed ? `${c.green}PASS${c.reset}` : `${c.red}FAIL${c.reset}`;
803
+ log(` ${status} ${basename} ${c.dim}(${fileElapsed}s)${c.reset}\n`);
804
+ }
805
+ else if (passed) {
806
+ log(`\n${c.green}✓${c.reset} Replay complete`);
807
+ }
717
808
  results.push({ file: basename, passed, commands: commandsRun, error: errorMsg });
718
809
  }
719
- const totalElapsed = ((performance.now() - totalStart) / 1000).toFixed(1);
720
- const passCount = results.filter(r => r.passed).length;
721
- const failCount = results.filter(r => !r.passed).length;
722
- log(`${c.bold}─── Results ───${c.reset}`);
723
- for (const r of results) {
724
- const icon = r.passed ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
725
- log(` ${icon} ${r.file}${r.error ? ` — ${r.error}` : ''}`);
810
+ // Multi-file summary
811
+ if (files.length > 1) {
812
+ const totalElapsed = ((performance.now() - totalStart) / 1000).toFixed(1);
813
+ const passCount = results.filter(r => r.passed).length;
814
+ const failCount = results.filter(r => !r.passed).length;
815
+ log(`${c.bold}─── Results ───${c.reset}`);
816
+ for (const r of results) {
817
+ const icon = r.passed ? `${c.green}✓${c.reset}` : `${c.red}✗${c.reset}`;
818
+ log(` ${icon} ${r.file}${r.error ? ` — ${r.error}` : ''}`);
819
+ }
820
+ log(`\n${passCount} passed, ${failCount} failed (${results.length} total, ${totalElapsed}s)`);
726
821
  }
727
- log(`\n${passCount} passed, ${failCount} failed (${results.length} total, ${totalElapsed}s)`);
728
- await srv.close();
822
+ await browser.close().catch(() => { });
823
+ if (relay)
824
+ await relay.close();
825
+ const failCount = results.filter(r => !r.passed).length;
729
826
  process.exit(failCount > 0 ? 1 : 0);
730
827
  }
731
- // ─── Bridge REPL loop ────────────────────────────────────────────────────────
732
- async function startBridgeLoop(opts, srv) {
828
+ // ─── Relay REPL loop ────────────────────────────────────────────────────────
829
+ async function startRelayLoop(opts, relay, browser, page, context, expect) {
733
830
  const silent = opts.silent || false;
734
831
  const log = (...args) => { if (!silent)
735
832
  console.log(...args); };
736
833
  const historyDir = path.join(os.homedir(), '.playwright-repl');
737
834
  const historyFile = path.join(historyDir, '.repl-history');
738
- const sessionHistory = [];
739
- const promptReady = `${c.cyan}pw>${c.reset} `;
835
+ const promptReady = `${c.cyan}relay>${c.reset} `;
740
836
  const promptCont = `${c.dim}...${c.reset} `;
741
837
  const rl = readline.createInterface({
742
838
  input: process.stdin,
@@ -750,12 +846,10 @@ async function startBridgeLoop(opts, srv) {
750
846
  rl.history.push(line);
751
847
  }
752
848
  catch { /* ignore */ }
753
- attachGhostCompletion(rl, buildCompletionItems());
754
849
  let buffer = '';
755
850
  let processing = false;
756
851
  const commandQueue = [];
757
852
  async function handleLine(line) {
758
- // Multi-line accumulation
759
853
  buffer = buffer ? buffer + '\n' + line : line;
760
854
  if (!isComplete(buffer)) {
761
855
  rl.setPrompt(promptCont);
@@ -768,58 +862,33 @@ async function startBridgeLoop(opts, srv) {
768
862
  return;
769
863
  // Meta-commands
770
864
  if (command === '.exit' || command === '.quit') {
771
- await srv.close();
865
+ await browser.close().catch(() => { });
866
+ if (relay)
867
+ await relay.close();
772
868
  process.exit(0);
773
869
  }
774
870
  if (command === '.clear') {
775
871
  console.clear();
776
872
  return;
777
873
  }
778
- if (command === '.history clear') {
779
- sessionHistory.length = 0;
780
- log('History cleared.');
781
- return;
782
- }
783
- if (command === '.history') {
784
- log(sessionHistory.length ? sessionHistory.join('\n') : '(no history)');
785
- return;
786
- }
787
- if (command === '.help' || command === '?') {
788
- showHelp(true);
789
- return;
790
- }
791
- if (command.startsWith('.help ')) {
792
- showCommandHelp(command.slice(6).trim(), true);
793
- return;
794
- }
795
- if (command === '.aliases') {
796
- showAliases();
797
- return;
798
- }
799
874
  // Record to history
800
- sessionHistory.push(command);
801
875
  try {
802
876
  fs.mkdirSync(path.dirname(historyFile), { recursive: true });
803
877
  fs.appendFileSync(historyFile, command + '\n');
804
878
  }
805
- catch (err) {
806
- console.error(`${c.dim}Warning: could not write history: ${err.message}${c.reset}`);
807
- }
808
- // ── Local commands (video, etc. — need Node.js filesystem) ─────
809
- const localResult = await handleLocalCommand(command, srv.context);
810
- if (localResult) {
811
- log(localResult.isError ? `${c.red}${localResult.text}${c.reset}` : localResult.text);
812
- return;
813
- }
814
- if (!srv.connected) {
815
- log(`${c.yellow}[not connected] Waiting for extension...${c.reset}`);
816
- return;
817
- }
879
+ catch { /* ignore */ }
818
880
  const startTime = performance.now();
819
- const runOpts = opts.includeSnapshot ? { includeSnapshot: true } : undefined;
820
- const result = await srv.run(command, runOpts);
881
+ const result = await relayExec(command, page, context, expect);
821
882
  const elapsed = (performance.now() - startTime).toFixed(0);
822
- displayBridgeResult(result, silent);
883
+ if (result.text) {
884
+ // Pass command name so filterResponse keeps the right sections
885
+ const parsed = parseInput(command);
886
+ const cmdName = parsed?._[0];
887
+ const filtered = filterResponse(result.text, cmdName);
888
+ if (filtered !== null) {
889
+ log(result.isError ? `${c.red}${filtered}${c.reset}` : filtered);
890
+ }
891
+ }
823
892
  log(`${c.dim}(${elapsed}ms)${c.reset}`);
824
893
  }
825
894
  async function processQueue() {
@@ -832,23 +901,14 @@ async function startBridgeLoop(opts, srv) {
832
901
  processing = false;
833
902
  rl.prompt();
834
903
  }
835
- // Print a status message above the current prompt line
836
- function printStatus(msg) {
837
- process.stdout.write('\r\x1b[K'); // clear current prompt line
838
- console.log(msg);
839
- rl.prompt(true);
840
- }
841
- if (!silent) {
842
- srv.onConnect(() => printStatus(`${c.green}✓${c.reset} Extension connected`));
843
- srv.onDisconnect(() => printStatus(`${c.yellow}Extension disconnected${c.reset}`));
844
- srv.onEvent((event) => {
845
- if (event.type === 'tab-attached' && event.url)
846
- printStatus(`${c.green}✓${c.reset} Attached to tab: ${event.url}`);
847
- });
848
- }
849
904
  rl.prompt();
850
905
  rl.on('line', (line) => { commandQueue.push(line); processQueue(); });
851
- rl.on('close', async () => { await srv.close(); process.exit(0); });
906
+ rl.on('close', async () => {
907
+ await browser.close().catch(() => { });
908
+ if (relay)
909
+ await relay.close();
910
+ process.exit(0);
911
+ });
852
912
  rl.on('SIGINT', () => {
853
913
  if (buffer) {
854
914
  buffer = '';
@@ -970,9 +1030,14 @@ async function tryHttpCommand(command, port) {
970
1030
  req.write(body);
971
1031
  req.end();
972
1032
  });
973
- const text = (result.text ?? '').replace(/^\s*\n+|\n+\s*$/g, '');
974
- if (text)
975
- process.stdout.write(text + '\n');
1033
+ if (result.image) {
1034
+ displayResult(result, false);
1035
+ }
1036
+ else {
1037
+ const text = (result.text ?? '').replace(/^\s*\n+|\n+\s*$/g, '');
1038
+ if (text)
1039
+ process.stdout.write(text + '\n');
1040
+ }
976
1041
  process.exit(result.isError ? 1 : 0);
977
1042
  }
978
1043
  catch (e) {
@@ -1000,145 +1065,70 @@ export async function startRepl(opts = {}) {
1000
1065
  process.exit(1);
1001
1066
  }
1002
1067
  log(`${c.bold}${c.magenta}🎭 Playwright REPL${c.reset} ${c.dim}v${replVersion}${c.reset}`);
1003
- // ─── Standalone mode (new: serviceWorker.evaluate) ─────────────
1004
- if (!opts.bridge && !opts.connect && !opts.engine) {
1005
- const { EvaluateConnection, findExtensionPath } = await import('@playwright-repl/core');
1006
- const extPath = process.env.VITEST ? null : findExtensionPath(import.meta.url);
1007
- if (extPath) {
1008
- const conn = new EvaluateConnection();
1009
- log(`${c.dim}Launching Chromium with extension...${c.reset}`);
1010
- try {
1011
- const { chromium } = await import('playwright');
1012
- // Default to headed for evaluate mode (interactive REPL with extension)
1013
- await conn.start(extPath, { headed: opts.headed ?? true, chromium });
1014
- log(`${c.green}✓${c.reset} Browser ready (with extension)`);
1015
- if (opts.command) {
1016
- const result = await conn.run(opts.command);
1017
- process.stdout.write((result.text ?? '') + '\n');
1018
- await conn.close();
1019
- process.exit(result.isError ? 1 : 0);
1020
- }
1021
- // Start HTTP server if --http
1022
- if (opts.http) {
1023
- try {
1024
- await startHttpServer(opts.httpPort ?? DEFAULT_HTTP_PORT, conn, log);
1025
- }
1026
- catch (e) {
1027
- log(`${c.yellow}⚠${c.reset} HTTP server failed: ${e.message}`);
1028
- }
1029
- }
1030
- // --http runs as a console (no readline) unless --interactive is also set
1031
- if (opts.http && !opts.interactive && !(opts.replay && opts.replay.length > 0)) {
1032
- log(`${c.dim}Console mode — send commands via HTTP. Ctrl+C to exit.${c.reset}\n`);
1033
- await waitForShutdown(log);
1034
- await conn.close();
1035
- process.exit(0);
1036
- }
1037
- log(`${c.dim}Type .help for commands, JavaScript supported${c.reset}\n`);
1038
- if (opts.replay && opts.replay.length > 0) {
1039
- await runBridgeReplayMode(opts, conn);
1040
- }
1041
- else {
1042
- await startBridgeLoop(opts, conn);
1043
- }
1044
- return;
1045
- }
1046
- catch (err) {
1047
- log(`${c.yellow}⚠${c.reset} ${c.dim}Could not launch with extension: ${err.message}${c.reset}`);
1048
- log(`${c.dim}Falling back to standard engine...${c.reset}\n`);
1068
+ // Default to standalone mode (launch own browser)
1069
+ if (!opts.relay && !opts.connect) {
1070
+ opts.relay = true;
1071
+ }
1072
+ // ─── Relay mode ───────────────────────────────────────────────────
1073
+ if (opts.relay || opts.connect) {
1074
+ const dynamicImport = Function('m', 'return import(m)');
1075
+ const { chromium } = await dynamicImport('playwright');
1076
+ const { expect: pwExpect } = await dynamicImport('@playwright/test').catch(() => ({ expect: undefined }));
1077
+ let browser;
1078
+ let relayCtx;
1079
+ let relayPage;
1080
+ let relay = null;
1081
+ const headless = opts.headed === false; // explicit --headless
1082
+ if (opts.connect) {
1083
+ // Connect mode: attach to existing Chrome via extension + CDP relay
1084
+ relay = new CDPRelayServer();
1085
+ await relay.start();
1086
+ log(`CDP relay listening on ${relay.cdpEndpoint()}`);
1087
+ log(`Extension endpoint: ${relay.relayEndpoint()}`);
1088
+ log('Waiting for extension to connect...');
1089
+ await relay.waitForExtension(30000);
1090
+ log(`${c.green}✓${c.reset} Extension connected`);
1091
+ browser = await chromium.connectOverCDP(relay.cdpEndpoint());
1092
+ relayCtx = browser.contexts()[0];
1093
+ relayPage = relayCtx.pages()[0];
1094
+ if (!relayPage) {
1095
+ console.error(`${c.red}✗${c.reset} No page found make sure a tab is open in Chrome`);
1096
+ await relay.close();
1097
+ process.exit(1);
1049
1098
  }
1050
1099
  }
1051
- }
1052
- // ─── Bridge mode ─────────────────────────────────────────────────
1053
- if (opts.bridge) {
1054
- const port = opts.bridgePort ?? 9876;
1055
- const srv = new BridgeServer();
1056
- await srv.start(port, { silent: !!opts.command });
1057
- log(`Bridge server listening on ws://localhost:${port}`);
1100
+ else {
1101
+ // Standalone mode (default): launch own browser
1102
+ log(`${c.dim}Launching ${headless ? 'headless' : 'headed'} browser...${c.reset}`);
1103
+ browser = await chromium.launch({
1104
+ headless,
1105
+ args: ['--no-first-run', '--no-default-browser-check'],
1106
+ });
1107
+ relayCtx = await browser.newContext();
1108
+ relayPage = await relayCtx.newPage();
1109
+ }
1110
+ log(`${c.green}✓${c.reset} Connected to page: ${relayPage.url()}`);
1111
+ const cleanup = async () => {
1112
+ await browser.close().catch(() => { });
1113
+ if (relay)
1114
+ await relay.close();
1115
+ };
1058
1116
  if (opts.command) {
1059
- await srv.waitForConnection(30000);
1060
- const result = await srv.run(opts.command);
1061
- process.stdout.write((result.text ?? '') + '\n');
1062
- srv.close();
1117
+ const result = await relayExec(opts.command, relayPage, relayCtx, pwExpect);
1118
+ if (result.text)
1119
+ process.stdout.write(result.text + '\n');
1120
+ await cleanup();
1063
1121
  process.exit(result.isError ? 1 : 0);
1064
1122
  }
1065
- // Start HTTP server if --http
1066
- if (opts.http) {
1067
- try {
1068
- await startHttpServer(opts.httpPort ?? DEFAULT_HTTP_PORT, srv, log);
1069
- }
1070
- catch (e) {
1071
- log(`${c.yellow}⚠${c.reset} HTTP server failed: ${e.message}`);
1072
- }
1073
- }
1074
1123
  if (opts.replay && opts.replay.length > 0) {
1075
- await runBridgeReplayMode(opts, srv);
1076
- }
1077
- else if (opts.http && !opts.interactive) {
1078
- log(`${c.dim}Console mode — send commands via HTTP. Ctrl+C to exit.${c.reset}\n`);
1079
- await waitForShutdown(log);
1080
- srv.close();
1081
- process.exit(0);
1082
- }
1083
- else {
1084
- log('Waiting for extension to connect...');
1085
- await startBridgeLoop(opts, srv);
1124
+ await runRelayReplayMode(opts, relay, browser, relayPage, relayCtx, pwExpect);
1125
+ return;
1086
1126
  }
1127
+ log(`${c.dim}Type .help for commands, JavaScript supported${c.reset}\n`);
1128
+ await startRelayLoop(opts, relay, browser, relayPage, relayCtx, pwExpect);
1087
1129
  return;
1088
1130
  }
1089
- // ─── Start engine (fallback) ────────────────────────────────────
1090
- log(`${c.dim}Type .help for commands${c.reset}\n`);
1091
- const conn = new Engine();
1092
- try {
1093
- await conn.start(opts);
1094
- log(`${c.green}✓${c.reset} Browser ready\n`);
1095
- }
1096
- catch (err) {
1097
- console.error(`${c.red}✗${c.reset} Failed to start: ${err.message}`);
1098
- process.exit(1);
1099
- }
1100
- if (opts.command) {
1101
- const parsed = parseInput(opts.command);
1102
- if (!parsed) {
1103
- process.stderr.write('Invalid command\n');
1104
- conn.close();
1105
- process.exit(1);
1106
- return; // unreachable, but satisfies TS control-flow
1107
- }
1108
- const result = await conn.run(parsed);
1109
- process.stdout.write((result.text ?? '') + '\n');
1110
- conn.close();
1111
- process.exit(result.isError ? 1 : 0);
1112
- }
1113
- // ─── Session + readline ──────────────────────────────────────────
1114
- const session = new SessionManager();
1115
- const historyDir = path.join(os.homedir(), '.playwright-repl');
1116
- const historyFile = path.join(historyDir, '.repl-history');
1117
- const ctx = { conn, session, rl: null, opts, log, historyFile, sessionHistory: [], commandCount: 0, errors: 0 };
1118
- // Auto-start recording if --record was passed
1119
- if (opts.record) {
1120
- const file = session.startRecording(opts.record);
1121
- log(`${c.red}⏺${c.reset} Recording to ${c.bold}${file}${c.reset}`);
1122
- }
1123
- const rl = readline.createInterface({
1124
- input: process.stdin,
1125
- output: process.stdout,
1126
- prompt: promptStr(ctx),
1127
- historySize: 500,
1128
- });
1129
- ctx.rl = rl;
1130
- try {
1131
- const hist = fs.readFileSync(historyFile, 'utf-8').split('\n').filter(Boolean).reverse();
1132
- for (const line of hist)
1133
- rl.history.push(line);
1134
- }
1135
- catch { /* ignore */ }
1136
- attachGhostCompletion(rl, buildCompletionItems());
1137
- // ─── Start ───────────────────────────────────────────────────────
1138
- if (opts.replay && opts.replay.length > 0) {
1139
- await runMultiReplayMode(ctx, opts.replay, opts.step || false);
1140
- }
1141
- else {
1142
- startCommandLoop(ctx);
1143
- }
1131
+ // No mode matched this shouldn't happen since relay is now the default
1132
+ console.error(`${c.red}✗${c.reset} No execution mode selected. Use --relay or --connect.`);
1133
+ process.exit(1);
1144
1134
  }