playwright-repl 0.1.0 → 0.2.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/{RELEASES.md → CHANGELOG.md} +111 -70
- package/README.md +53 -12
- package/bin/mcp-server.cjs +32 -0
- package/package.json +4 -3
- package/src/repl.mjs +604 -582
package/src/repl.mjs
CHANGED
|
@@ -1,582 +1,604 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Main REPL loop.
|
|
3
|
-
*
|
|
4
|
-
* Handles readline, command queue, meta-commands, and session management.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import readline from 'node:readline';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import { execSync } from 'node:child_process';
|
|
11
|
-
|
|
12
|
-
import { replVersion, COMMANDS } from './resolve.mjs';
|
|
13
|
-
import { DaemonConnection } from './connection.mjs';
|
|
14
|
-
import { socketPath, daemonProfilesDir, isDaemonRunning, startDaemon } from './workspace.mjs';
|
|
15
|
-
import { parseInput, ALIASES, ALL_COMMANDS } from './parser.mjs';
|
|
16
|
-
import { SessionManager } from './recorder.mjs';
|
|
17
|
-
import { c } from './colors.mjs';
|
|
18
|
-
|
|
19
|
-
// ─── Verify commands → run-code translation ─────────────────────────────────
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* The daemon has browser_verify_* tools but no CLI keyword mappings.
|
|
23
|
-
* We intercept verify-* commands here and translate them to run-code calls
|
|
24
|
-
* that use the equivalent Playwright API.
|
|
25
|
-
*/
|
|
26
|
-
export function verifyToRunCode(cmdName, positionalArgs) {
|
|
27
|
-
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
28
|
-
|
|
29
|
-
switch (cmdName) {
|
|
30
|
-
case 'verify-text': {
|
|
31
|
-
const text = positionalArgs.join(' ');
|
|
32
|
-
if (!text) return null;
|
|
33
|
-
return { _: ['run-code', `async (page) => { if (await page.getByText('${esc(text)}').filter({ visible: true }).count() === 0) throw new Error('Text not found: ${esc(text)}'); }`] };
|
|
34
|
-
}
|
|
35
|
-
case 'verify-element': {
|
|
36
|
-
const [role, ...nameParts] = positionalArgs;
|
|
37
|
-
const name = nameParts.join(' ');
|
|
38
|
-
if (!role || !name) return null;
|
|
39
|
-
return { _: ['run-code', `async (page) => { if (await page.getByRole('${esc(role)}', { name: '${esc(name)}' }).count() === 0) throw new Error('Element not found: ${esc(role)} "${esc(name)}"'); }`] };
|
|
40
|
-
}
|
|
41
|
-
case 'verify-value': {
|
|
42
|
-
const [ref, ...valueParts] = positionalArgs;
|
|
43
|
-
const value = valueParts.join(' ');
|
|
44
|
-
if (!ref || !value) return null;
|
|
45
|
-
return { _: ['run-code', `async (page) => { const el = page.locator('[aria-ref="${esc(ref)}"]'); const v = await el.inputValue(); if (v !== '${esc(value)}') throw new Error('Expected "${esc(value)}", got "' + v + '"'); }`] };
|
|
46
|
-
}
|
|
47
|
-
case 'verify-list': {
|
|
48
|
-
const [ref, ...items] = positionalArgs;
|
|
49
|
-
if (!ref || items.length === 0) return null;
|
|
50
|
-
const checks = items.map(item => `if (await loc.getByText('${esc(item)}').count() === 0) throw new Error('Item not found: ${esc(item)}');`).join(' ');
|
|
51
|
-
return { _: ['run-code', `async (page) => { const loc = page.locator('[aria-ref="${esc(ref)}"]'); ${checks} }`] };
|
|
52
|
-
}
|
|
53
|
-
default:
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─── Text-to-action via Playwright native locators ──────────────────────────
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Build a run-code args object that uses Playwright's native text locators.
|
|
62
|
-
* e.g. click "Active" → page.getByText("Active").click()
|
|
63
|
-
* fill "Email" "test" → page.getByLabel("Email").fill("test")
|
|
64
|
-
* check "Buy groceries" → listitem with text → checkbox.check()
|
|
65
|
-
*/
|
|
66
|
-
export function textToRunCode(cmdName, textArg, extraArgs) {
|
|
67
|
-
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
68
|
-
const text = esc(textArg);
|
|
69
|
-
|
|
70
|
-
switch (cmdName) {
|
|
71
|
-
case 'click':
|
|
72
|
-
return { _: ['run-code', `async (page) => {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return { _: ['run-code', `async (page) => {
|
|
81
|
-
let loc = page.
|
|
82
|
-
if (await loc.count() === 0) loc = page.
|
|
83
|
-
if (await loc.count() === 0) loc = page.getByRole('
|
|
84
|
-
await loc.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
case '
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (await loc.count() === 0) loc = page.getByRole('
|
|
92
|
-
await loc.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
case '
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (await loc.count() === 0) loc = page.getByRole('
|
|
102
|
-
await loc.
|
|
103
|
-
}`] };
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
let loc = page.getByLabel('${text}');
|
|
109
|
-
if (await loc.count() === 0) loc = page.getByRole('
|
|
110
|
-
await loc.
|
|
111
|
-
}`] };
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
console.log(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
console.log();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
console.log(`
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
console.log(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
console.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
console.log(`${c.green}✓${c.reset}
|
|
248
|
-
ctx.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (line === '.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
// ──
|
|
297
|
-
|
|
298
|
-
if (line
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
console.log(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
while (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
// ───
|
|
522
|
-
|
|
523
|
-
export
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
if (
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
if
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
}
|
|
582
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Main REPL loop.
|
|
3
|
+
*
|
|
4
|
+
* Handles readline, command queue, meta-commands, and session management.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import { execSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
import { replVersion, COMMANDS } from './resolve.mjs';
|
|
13
|
+
import { DaemonConnection } from './connection.mjs';
|
|
14
|
+
import { socketPath, daemonProfilesDir, isDaemonRunning, startDaemon } from './workspace.mjs';
|
|
15
|
+
import { parseInput, ALIASES, ALL_COMMANDS } from './parser.mjs';
|
|
16
|
+
import { SessionManager } from './recorder.mjs';
|
|
17
|
+
import { c } from './colors.mjs';
|
|
18
|
+
|
|
19
|
+
// ─── Verify commands → run-code translation ─────────────────────────────────
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The daemon has browser_verify_* tools but no CLI keyword mappings.
|
|
23
|
+
* We intercept verify-* commands here and translate them to run-code calls
|
|
24
|
+
* that use the equivalent Playwright API.
|
|
25
|
+
*/
|
|
26
|
+
export function verifyToRunCode(cmdName, positionalArgs) {
|
|
27
|
+
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
28
|
+
|
|
29
|
+
switch (cmdName) {
|
|
30
|
+
case 'verify-text': {
|
|
31
|
+
const text = positionalArgs.join(' ');
|
|
32
|
+
if (!text) return null;
|
|
33
|
+
return { _: ['run-code', `async (page) => { if (await page.getByText('${esc(text)}').filter({ visible: true }).count() === 0) throw new Error('Text not found: ${esc(text)}'); }`] };
|
|
34
|
+
}
|
|
35
|
+
case 'verify-element': {
|
|
36
|
+
const [role, ...nameParts] = positionalArgs;
|
|
37
|
+
const name = nameParts.join(' ');
|
|
38
|
+
if (!role || !name) return null;
|
|
39
|
+
return { _: ['run-code', `async (page) => { if (await page.getByRole('${esc(role)}', { name: '${esc(name)}' }).count() === 0) throw new Error('Element not found: ${esc(role)} "${esc(name)}"'); }`] };
|
|
40
|
+
}
|
|
41
|
+
case 'verify-value': {
|
|
42
|
+
const [ref, ...valueParts] = positionalArgs;
|
|
43
|
+
const value = valueParts.join(' ');
|
|
44
|
+
if (!ref || !value) return null;
|
|
45
|
+
return { _: ['run-code', `async (page) => { const el = page.locator('[aria-ref="${esc(ref)}"]'); const v = await el.inputValue(); if (v !== '${esc(value)}') throw new Error('Expected "${esc(value)}", got "' + v + '"'); }`] };
|
|
46
|
+
}
|
|
47
|
+
case 'verify-list': {
|
|
48
|
+
const [ref, ...items] = positionalArgs;
|
|
49
|
+
if (!ref || items.length === 0) return null;
|
|
50
|
+
const checks = items.map(item => `if (await loc.getByText('${esc(item)}').count() === 0) throw new Error('Item not found: ${esc(item)}');`).join(' ');
|
|
51
|
+
return { _: ['run-code', `async (page) => { const loc = page.locator('[aria-ref="${esc(ref)}"]'); ${checks} }`] };
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Text-to-action via Playwright native locators ──────────────────────────
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a run-code args object that uses Playwright's native text locators.
|
|
62
|
+
* e.g. click "Active" → page.getByText("Active").click()
|
|
63
|
+
* fill "Email" "test" → page.getByLabel("Email").fill("test")
|
|
64
|
+
* check "Buy groceries" → listitem with text → checkbox.check()
|
|
65
|
+
*/
|
|
66
|
+
export function textToRunCode(cmdName, textArg, extraArgs) {
|
|
67
|
+
const esc = (s) => s.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
68
|
+
const text = esc(textArg);
|
|
69
|
+
|
|
70
|
+
switch (cmdName) {
|
|
71
|
+
case 'click':
|
|
72
|
+
return { _: ['run-code', `async (page) => {
|
|
73
|
+
let loc = page.getByText('${text}', { exact: true });
|
|
74
|
+
if (await loc.count() === 0) loc = page.getByRole('button', { name: '${text}' });
|
|
75
|
+
if (await loc.count() === 0) loc = page.getByRole('link', { name: '${text}' });
|
|
76
|
+
if (await loc.count() === 0) loc = page.getByText('${text}');
|
|
77
|
+
await loc.click();
|
|
78
|
+
}`] };
|
|
79
|
+
case 'dblclick':
|
|
80
|
+
return { _: ['run-code', `async (page) => {
|
|
81
|
+
let loc = page.getByText('${text}', { exact: true });
|
|
82
|
+
if (await loc.count() === 0) loc = page.getByRole('button', { name: '${text}' });
|
|
83
|
+
if (await loc.count() === 0) loc = page.getByRole('link', { name: '${text}' });
|
|
84
|
+
if (await loc.count() === 0) loc = page.getByText('${text}');
|
|
85
|
+
await loc.dblclick();
|
|
86
|
+
}`] };
|
|
87
|
+
case 'hover':
|
|
88
|
+
return { _: ['run-code', `async (page) => {
|
|
89
|
+
let loc = page.getByText('${text}', { exact: true });
|
|
90
|
+
if (await loc.count() === 0) loc = page.getByRole('button', { name: '${text}' });
|
|
91
|
+
if (await loc.count() === 0) loc = page.getByRole('link', { name: '${text}' });
|
|
92
|
+
if (await loc.count() === 0) loc = page.getByText('${text}');
|
|
93
|
+
await loc.hover();
|
|
94
|
+
}`] };
|
|
95
|
+
case 'fill': {
|
|
96
|
+
const value = esc(extraArgs[0] || '');
|
|
97
|
+
// Try getByLabel first, fall back to getByPlaceholder, then getByRole('textbox')
|
|
98
|
+
return { _: ['run-code', `async (page) => {
|
|
99
|
+
let loc = page.getByLabel('${text}');
|
|
100
|
+
if (await loc.count() === 0) loc = page.getByPlaceholder('${text}');
|
|
101
|
+
if (await loc.count() === 0) loc = page.getByRole('textbox', { name: '${text}' });
|
|
102
|
+
await loc.fill('${value}');
|
|
103
|
+
}`] };
|
|
104
|
+
}
|
|
105
|
+
case 'select': {
|
|
106
|
+
const value = esc(extraArgs[0] || '');
|
|
107
|
+
return { _: ['run-code', `async (page) => {
|
|
108
|
+
let loc = page.getByLabel('${text}');
|
|
109
|
+
if (await loc.count() === 0) loc = page.getByRole('combobox', { name: '${text}' });
|
|
110
|
+
await loc.selectOption('${value}');
|
|
111
|
+
}`] };
|
|
112
|
+
}
|
|
113
|
+
case 'check':
|
|
114
|
+
// Scope to listitem/group with matching text, then find checkbox inside
|
|
115
|
+
return { _: ['run-code', `async (page) => {
|
|
116
|
+
const item = page.getByRole('listitem').filter({ hasText: '${text}' });
|
|
117
|
+
if (await item.count() > 0) { await item.getByRole('checkbox').check(); return; }
|
|
118
|
+
let loc = page.getByLabel('${text}');
|
|
119
|
+
if (await loc.count() === 0) loc = page.getByRole('checkbox', { name: '${text}' });
|
|
120
|
+
await loc.check();
|
|
121
|
+
}`] };
|
|
122
|
+
case 'uncheck':
|
|
123
|
+
return { _: ['run-code', `async (page) => {
|
|
124
|
+
const item = page.getByRole('listitem').filter({ hasText: '${text}' });
|
|
125
|
+
if (await item.count() > 0) { await item.getByRole('checkbox').uncheck(); return; }
|
|
126
|
+
let loc = page.getByLabel('${text}');
|
|
127
|
+
if (await loc.count() === 0) loc = page.getByRole('checkbox', { name: '${text}' });
|
|
128
|
+
await loc.uncheck();
|
|
129
|
+
}`] };
|
|
130
|
+
default:
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Response filtering ─────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export function filterResponse(text) {
|
|
138
|
+
const sections = text.split(/^### /m).slice(1);
|
|
139
|
+
const kept = [];
|
|
140
|
+
for (const section of sections) {
|
|
141
|
+
const newline = section.indexOf('\n');
|
|
142
|
+
if (newline === -1) continue;
|
|
143
|
+
const title = section.substring(0, newline).trim();
|
|
144
|
+
const content = section.substring(newline + 1).trim();
|
|
145
|
+
if (title === 'Result' || title === 'Error' || title === 'Modal state')
|
|
146
|
+
kept.push(content);
|
|
147
|
+
}
|
|
148
|
+
return kept.length > 0 ? kept.join('\n') : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Meta-command handlers ──────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
export function showHelp() {
|
|
154
|
+
console.log(`\n${c.bold}Available commands:${c.reset}`);
|
|
155
|
+
const categories = {
|
|
156
|
+
'Navigation': ['open', 'goto', 'go-back', 'go-forward', 'reload'],
|
|
157
|
+
'Interaction': ['click', 'dblclick', 'fill', 'type', 'press', 'hover', 'select', 'check', 'uncheck', 'drag'],
|
|
158
|
+
'Inspection': ['snapshot', 'screenshot', 'eval', 'console', 'network', 'run-code'],
|
|
159
|
+
'Tabs': ['tab-list', 'tab-new', 'tab-close', 'tab-select'],
|
|
160
|
+
'Storage': ['cookie-list', 'cookie-get', 'localstorage-list', 'localstorage-get', 'state-save', 'state-load'],
|
|
161
|
+
};
|
|
162
|
+
for (const [cat, cmds] of Object.entries(categories)) {
|
|
163
|
+
console.log(` ${c.bold}${cat}:${c.reset} ${cmds.join(', ')}`);
|
|
164
|
+
}
|
|
165
|
+
console.log(`\n ${c.dim}Use .aliases for shortcuts, or type any command with --help${c.reset}`);
|
|
166
|
+
console.log(`\n${c.bold}REPL meta-commands:${c.reset}`);
|
|
167
|
+
console.log(` .aliases Show command aliases`);
|
|
168
|
+
console.log(` .status Show connection status`);
|
|
169
|
+
console.log(` .reconnect Reconnect to daemon`);
|
|
170
|
+
console.log(` .record [filename] Start recording commands`);
|
|
171
|
+
console.log(` .save Stop recording and save`);
|
|
172
|
+
console.log(` .pause Pause/resume recording`);
|
|
173
|
+
console.log(` .discard Discard recording`);
|
|
174
|
+
console.log(` .replay <filename> Replay a recorded session`);
|
|
175
|
+
console.log(` .exit Exit REPL\n`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function showAliases() {
|
|
179
|
+
console.log(`\n${c.bold}Command aliases:${c.reset}`);
|
|
180
|
+
const groups = {};
|
|
181
|
+
for (const [alias, cmd] of Object.entries(ALIASES)) {
|
|
182
|
+
if (!groups[cmd]) groups[cmd] = [];
|
|
183
|
+
groups[cmd].push(alias);
|
|
184
|
+
}
|
|
185
|
+
for (const [cmd, aliases] of Object.entries(groups).sort()) {
|
|
186
|
+
console.log(` ${c.cyan}${aliases.join(', ')}${c.reset} → ${cmd}`);
|
|
187
|
+
}
|
|
188
|
+
console.log();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function showStatus(ctx) {
|
|
192
|
+
const { conn, sessionName, session } = ctx;
|
|
193
|
+
console.log(`Connected: ${conn.connected ? `${c.green}yes${c.reset}` : `${c.red}no${c.reset}`}`);
|
|
194
|
+
console.log(`Session: ${sessionName}`);
|
|
195
|
+
console.log(`Socket: ${socketPath(sessionName)}`);
|
|
196
|
+
console.log(`Commands sent: ${ctx.commandCount}`);
|
|
197
|
+
console.log(`Mode: ${session.mode}`);
|
|
198
|
+
if (session.mode === 'recording' || session.mode === 'paused') {
|
|
199
|
+
console.log(`Recording: ${c.red}⏺${c.reset} ${session.recordingFilename} (${session.recordedCount} commands${session.mode === 'paused' ? ', paused' : ''})`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── Session-level commands ─────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
export async function handleKillAll(ctx) {
|
|
206
|
+
try {
|
|
207
|
+
let killed = 0;
|
|
208
|
+
if (process.platform === 'win32') {
|
|
209
|
+
let result = '';
|
|
210
|
+
try {
|
|
211
|
+
result = execSync(
|
|
212
|
+
'powershell -NoProfile -Command "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like \'*run-mcp-server*\' -and $_.CommandLine -like \'*--daemon-session*\' } | Select-Object -ExpandProperty ProcessId"',
|
|
213
|
+
{ encoding: 'utf-8' }
|
|
214
|
+
);
|
|
215
|
+
} catch (err) {
|
|
216
|
+
result = err.stdout || '';
|
|
217
|
+
}
|
|
218
|
+
for (const line of result.trim().split(/\r?\n/)) {
|
|
219
|
+
const pid = line.trim();
|
|
220
|
+
if (/^\d+$/.test(pid)) {
|
|
221
|
+
try { process.kill(parseInt(pid, 10)); killed++; } catch {}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
const result = execSync('ps aux', { encoding: 'utf-8' });
|
|
226
|
+
for (const ln of result.split('\n')) {
|
|
227
|
+
if (ln.includes('run-mcp-server') && ln.includes('--daemon-session')) {
|
|
228
|
+
const pid = ln.trim().split(/\s+/)[1];
|
|
229
|
+
if (pid && /^\d+$/.test(pid)) {
|
|
230
|
+
try { process.kill(parseInt(pid, 10), 'SIGKILL'); killed++; } catch {}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
console.log(killed > 0
|
|
236
|
+
? `${c.green}✓${c.reset} Killed ${killed} daemon process${killed === 1 ? '' : 'es'}`
|
|
237
|
+
: `${c.dim}No daemon processes found${c.reset}`);
|
|
238
|
+
ctx.conn.close();
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(`${c.red}Error:${c.reset} ${err.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function handleClose(ctx) {
|
|
245
|
+
try {
|
|
246
|
+
await ctx.conn.send('stop', {});
|
|
247
|
+
console.log(`${c.green}✓${c.reset} Daemon stopped`);
|
|
248
|
+
ctx.conn.close();
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error(`${c.red}Error:${c.reset} ${err.message}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── Session meta-commands (.record, .save, .pause, .discard, .replay) ──────
|
|
255
|
+
|
|
256
|
+
export function handleSessionCommand(ctx, line) {
|
|
257
|
+
const { session } = ctx;
|
|
258
|
+
|
|
259
|
+
if (line.startsWith('.record')) {
|
|
260
|
+
const filename = line.split(/\s+/)[1] || undefined;
|
|
261
|
+
const file = session.startRecording(filename);
|
|
262
|
+
console.log(`${c.red}⏺${c.reset} Recording to ${c.bold}${file}${c.reset}`);
|
|
263
|
+
ctx.rl.setPrompt(promptStr(ctx));
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (line === '.save') {
|
|
268
|
+
const { filename, count } = session.save();
|
|
269
|
+
console.log(`${c.green}✓${c.reset} Saved ${count} commands to ${c.bold}${filename}${c.reset}`);
|
|
270
|
+
ctx.rl.setPrompt(promptStr(ctx));
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (line === '.pause') {
|
|
275
|
+
const paused = session.togglePause();
|
|
276
|
+
console.log(paused ? `${c.yellow}⏸${c.reset} Recording paused` : `${c.red}⏺${c.reset} Recording resumed`);
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (line === '.discard') {
|
|
281
|
+
session.discard();
|
|
282
|
+
console.log(`${c.yellow}Recording discarded${c.reset}`);
|
|
283
|
+
ctx.rl.setPrompt(promptStr(ctx));
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Process a single line ──────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
export async function processLine(ctx, line) {
|
|
293
|
+
line = line.trim();
|
|
294
|
+
if (!line) return;
|
|
295
|
+
|
|
296
|
+
// ── Meta-commands ────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
if (line === '.help' || line === '?') return showHelp();
|
|
299
|
+
if (line === '.aliases') return showAliases();
|
|
300
|
+
if (line === '.status') return showStatus(ctx);
|
|
301
|
+
|
|
302
|
+
if (line === '.exit' || line === '.quit') {
|
|
303
|
+
ctx.conn.close();
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (line === '.reconnect') {
|
|
308
|
+
ctx.conn.close();
|
|
309
|
+
try {
|
|
310
|
+
await ctx.conn.connect();
|
|
311
|
+
console.log(`${c.green}✓${c.reset} Reconnected`);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.error(`${c.red}✗${c.reset} ${err.message}`);
|
|
314
|
+
}
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Session commands (record/save/pause/discard) ────────────────
|
|
319
|
+
|
|
320
|
+
if (line.startsWith('.')) {
|
|
321
|
+
try {
|
|
322
|
+
if (handleSessionCommand(ctx, line)) return;
|
|
323
|
+
} catch (err) {
|
|
324
|
+
console.log(`${c.yellow}${err.message}${c.reset}`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Inline replay ──────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
if (line.startsWith('.replay')) {
|
|
332
|
+
const filename = line.split(/\s+/)[1];
|
|
333
|
+
if (!filename) {
|
|
334
|
+
console.log(`${c.yellow}Usage: .replay <filename>${c.reset}`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const player = ctx.session.startReplay(filename);
|
|
339
|
+
console.log(`${c.blue}▶${c.reset} Replaying ${c.bold}${filename}${c.reset} (${player.commands.length} commands)\n`);
|
|
340
|
+
while (!player.done) {
|
|
341
|
+
const cmd = player.next();
|
|
342
|
+
console.log(`${c.dim}${player.progress}${c.reset} ${cmd}`);
|
|
343
|
+
await processLine(ctx, cmd);
|
|
344
|
+
}
|
|
345
|
+
ctx.session.endReplay();
|
|
346
|
+
console.log(`\n${c.green}✓${c.reset} Replay complete`);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error(`${c.red}Error:${c.reset} ${err.message}`);
|
|
349
|
+
ctx.session.endReplay();
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Regular command — parse and send ─────────────────────────────
|
|
355
|
+
|
|
356
|
+
let args = parseInput(line);
|
|
357
|
+
if (!args) return;
|
|
358
|
+
|
|
359
|
+
const cmdName = args._[0];
|
|
360
|
+
if (!cmdName) return;
|
|
361
|
+
|
|
362
|
+
// Validate command exists
|
|
363
|
+
const knownExtras = ['help', 'list', 'close-all', 'kill-all', 'install', 'install-browser',
|
|
364
|
+
'verify-text', 'verify-element', 'verify-value', 'verify-list'];
|
|
365
|
+
if (!ALL_COMMANDS.includes(cmdName) && !knownExtras.includes(cmdName)) {
|
|
366
|
+
console.log(`${c.yellow}Unknown command: ${cmdName}${c.reset}`);
|
|
367
|
+
console.log(`${c.dim}Type .help for available commands${c.reset}`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Session-level commands (not forwarded to daemon) ──────────
|
|
372
|
+
if (cmdName === 'kill-all') return handleKillAll(ctx);
|
|
373
|
+
if (cmdName === 'close' || cmdName === 'close-all') return handleClose(ctx);
|
|
374
|
+
|
|
375
|
+
// ── Verify commands → run-code translation ──────────────────
|
|
376
|
+
const verifyCommands = ['verify-text', 'verify-element', 'verify-value', 'verify-list'];
|
|
377
|
+
if (verifyCommands.includes(cmdName)) {
|
|
378
|
+
const translated = verifyToRunCode(cmdName, args._.slice(1));
|
|
379
|
+
if (translated) {
|
|
380
|
+
args = translated;
|
|
381
|
+
} else {
|
|
382
|
+
console.log(`${c.yellow}Usage: ${cmdName} <args>${c.reset}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Auto-resolve text to native Playwright locator ─────────
|
|
388
|
+
const refCommands = ['click', 'dblclick', 'hover', 'fill', 'select', 'check', 'uncheck'];
|
|
389
|
+
if (refCommands.includes(cmdName) && args._[1] && !/^e\d+$/.test(args._[1])) {
|
|
390
|
+
const textArg = args._[1];
|
|
391
|
+
const extraArgs = args._.slice(2);
|
|
392
|
+
const runCodeArgs = textToRunCode(cmdName, textArg, extraArgs);
|
|
393
|
+
if (runCodeArgs) {
|
|
394
|
+
ctx.log(`${c.dim}→ ${runCodeArgs._[1]}${c.reset}`);
|
|
395
|
+
args = runCodeArgs;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const startTime = performance.now();
|
|
400
|
+
try {
|
|
401
|
+
const result = await ctx.conn.run(args);
|
|
402
|
+
const elapsed = (performance.now() - startTime).toFixed(0);
|
|
403
|
+
if (result?.text) {
|
|
404
|
+
const output = filterResponse(result.text);
|
|
405
|
+
if (output) console.log(output);
|
|
406
|
+
}
|
|
407
|
+
ctx.commandCount++;
|
|
408
|
+
ctx.session.record(line);
|
|
409
|
+
|
|
410
|
+
if (elapsed > 500) {
|
|
411
|
+
ctx.log(`${c.dim}(${elapsed}ms)${c.reset}`);
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
console.error(`${c.red}Error:${c.reset} ${err.message}`);
|
|
415
|
+
if (!ctx.conn.connected) {
|
|
416
|
+
console.log(`${c.yellow}Connection lost. Trying to reconnect...${c.reset}`);
|
|
417
|
+
try {
|
|
418
|
+
await ctx.conn.connect();
|
|
419
|
+
console.log(`${c.green}✓${c.reset} Reconnected. Try your command again.`);
|
|
420
|
+
} catch {
|
|
421
|
+
console.error(`${c.red}✗${c.reset} Could not reconnect. Use .reconnect or restart.`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─── Replay mode (non-interactive, --replay flag) ───────────────────────────
|
|
428
|
+
|
|
429
|
+
export async function runReplayMode(ctx, replayFile, step) {
|
|
430
|
+
try {
|
|
431
|
+
const player = ctx.session.startReplay(replayFile, step);
|
|
432
|
+
console.log(`${c.blue}▶${c.reset} Replaying ${c.bold}${replayFile}${c.reset} (${player.commands.length} commands)\n`);
|
|
433
|
+
while (!player.done) {
|
|
434
|
+
const cmd = player.next();
|
|
435
|
+
console.log(`${c.dim}${player.progress}${c.reset} ${cmd}`);
|
|
436
|
+
await processLine(ctx, cmd);
|
|
437
|
+
|
|
438
|
+
if (ctx.session.step && !player.done) {
|
|
439
|
+
await new Promise((resolve) => {
|
|
440
|
+
process.stdout.write(`${c.dim} Press Enter to continue...${c.reset}`);
|
|
441
|
+
process.stdin.once('data', () => {
|
|
442
|
+
process.stdout.write('\r\x1b[K');
|
|
443
|
+
resolve();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
ctx.session.endReplay();
|
|
449
|
+
console.log(`\n${c.green}✓${c.reset} Replay complete`);
|
|
450
|
+
ctx.conn.close();
|
|
451
|
+
process.exit(0);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
console.error(`${c.red}Error:${c.reset} ${err.message}`);
|
|
454
|
+
ctx.conn.close();
|
|
455
|
+
process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ─── Command loop (interactive) ─────────────────────────────────────────────
|
|
460
|
+
|
|
461
|
+
export function startCommandLoop(ctx) {
|
|
462
|
+
let processing = false;
|
|
463
|
+
const commandQueue = [];
|
|
464
|
+
|
|
465
|
+
async function processQueue() {
|
|
466
|
+
if (processing) return;
|
|
467
|
+
processing = true;
|
|
468
|
+
while (commandQueue.length > 0) {
|
|
469
|
+
const line = commandQueue.shift();
|
|
470
|
+
await processLine(ctx, line);
|
|
471
|
+
if (line.trim()) {
|
|
472
|
+
try {
|
|
473
|
+
fs.mkdirSync(path.dirname(ctx.historyFile), { recursive: true });
|
|
474
|
+
fs.appendFileSync(ctx.historyFile, line.trim() + '\n');
|
|
475
|
+
} catch {}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
processing = false;
|
|
479
|
+
ctx.rl.prompt();
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
ctx.rl.prompt();
|
|
483
|
+
|
|
484
|
+
ctx.rl.on('line', (line) => {
|
|
485
|
+
commandQueue.push(line);
|
|
486
|
+
processQueue();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
ctx.rl.on('close', async () => {
|
|
490
|
+
while (processing || commandQueue.length > 0) {
|
|
491
|
+
await new Promise(r => setTimeout(r, 50));
|
|
492
|
+
}
|
|
493
|
+
ctx.log(`\n${c.dim}Disconnecting... (daemon stays running)${c.reset}`);
|
|
494
|
+
ctx.conn.close();
|
|
495
|
+
process.exit(0);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
let lastSigint = 0;
|
|
499
|
+
ctx.rl.on('SIGINT', () => {
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
if (now - lastSigint < 500) {
|
|
502
|
+
ctx.conn.close();
|
|
503
|
+
process.exit(0);
|
|
504
|
+
}
|
|
505
|
+
lastSigint = now;
|
|
506
|
+
ctx.log(`\n${c.dim}(Ctrl+C again to exit, or type .exit)${c.reset}`);
|
|
507
|
+
ctx.rl.prompt();
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ─── Prompt string ──────────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
export function promptStr(ctx) {
|
|
514
|
+
const mode = ctx.session.mode;
|
|
515
|
+
const prefix = mode === 'recording' ? `${c.red}⏺${c.reset} `
|
|
516
|
+
: mode === 'paused' ? `${c.yellow}⏸${c.reset} `
|
|
517
|
+
: '';
|
|
518
|
+
return `${prefix}${c.cyan}pw>${c.reset} `;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ─── Tab completer ──────────────────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
export function completer(line) {
|
|
524
|
+
const parts = line.split(/\s+/);
|
|
525
|
+
if (parts.length <= 1) {
|
|
526
|
+
const prefix = parts[0] || '';
|
|
527
|
+
const allNames = [...ALL_COMMANDS, ...Object.keys(ALIASES)];
|
|
528
|
+
const metas = ['.help', '.aliases', '.status', '.reconnect', '.exit',
|
|
529
|
+
'.record', '.save', '.replay', '.pause', '.discard'];
|
|
530
|
+
const hits = [...allNames, ...metas].filter(n => n.startsWith(prefix));
|
|
531
|
+
return [hits.length ? hits : allNames, prefix];
|
|
532
|
+
}
|
|
533
|
+
const cmd = ALIASES[parts[0]] || parts[0];
|
|
534
|
+
const helpText = COMMANDS[cmd]?.options || [];
|
|
535
|
+
const lastPart = parts[parts.length - 1];
|
|
536
|
+
if (lastPart.startsWith('--')) {
|
|
537
|
+
const hits = helpText.filter(o => o.startsWith(lastPart));
|
|
538
|
+
return [hits, lastPart];
|
|
539
|
+
}
|
|
540
|
+
return [[], line];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ─── REPL ────────────────────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
export async function startRepl(opts = {}) {
|
|
546
|
+
const sessionName = opts.session || 'default';
|
|
547
|
+
const silent = opts.silent || false;
|
|
548
|
+
const log = (...args) => { if (!silent) console.log(...args); };
|
|
549
|
+
|
|
550
|
+
log(`${c.bold}${c.magenta}🎭 Playwright REPL${c.reset} ${c.dim}v${replVersion}${c.reset}`);
|
|
551
|
+
log(`${c.dim}Session: ${sessionName} | Type .help for commands${c.reset}\n`);
|
|
552
|
+
|
|
553
|
+
// ─── Connect to daemon ───────────────────────────────────────────
|
|
554
|
+
|
|
555
|
+
const running = await isDaemonRunning(sessionName);
|
|
556
|
+
if (!running) {
|
|
557
|
+
await startDaemon(sessionName, opts);
|
|
558
|
+
await new Promise(r => setTimeout(r, 500));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const conn = new DaemonConnection(socketPath(sessionName), replVersion);
|
|
562
|
+
try {
|
|
563
|
+
await conn.connect();
|
|
564
|
+
log(`${c.green}✓${c.reset} Connected to daemon${running ? '' : ' (newly started)'}\n`);
|
|
565
|
+
} catch (err) {
|
|
566
|
+
console.error(`${c.red}✗${c.reset} Failed to connect: ${err.message}`);
|
|
567
|
+
console.error(` Try: playwright-cli open`);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ─── Session + readline ──────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
const session = new SessionManager();
|
|
574
|
+
const historyFile = path.join(daemonProfilesDir, '.repl-history');
|
|
575
|
+
const ctx = { conn, session, rl: null, sessionName, log, historyFile, commandCount: 0 };
|
|
576
|
+
|
|
577
|
+
// Auto-start recording if --record was passed
|
|
578
|
+
if (opts.record) {
|
|
579
|
+
const file = session.startRecording(opts.record);
|
|
580
|
+
log(`${c.red}⏺${c.reset} Recording to ${c.bold}${file}${c.reset}`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const rl = readline.createInterface({
|
|
584
|
+
input: process.stdin,
|
|
585
|
+
output: process.stdout,
|
|
586
|
+
prompt: promptStr(ctx),
|
|
587
|
+
historySize: 500,
|
|
588
|
+
completer,
|
|
589
|
+
});
|
|
590
|
+
ctx.rl = rl;
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const hist = fs.readFileSync(historyFile, 'utf-8').split('\n').filter(Boolean).reverse();
|
|
594
|
+
for (const line of hist) rl.history.push(line);
|
|
595
|
+
} catch {}
|
|
596
|
+
|
|
597
|
+
// ─── Start ───────────────────────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
if (opts.replay) {
|
|
600
|
+
await runReplayMode(ctx, opts.replay, opts.step);
|
|
601
|
+
} else {
|
|
602
|
+
startCommandLoop(ctx);
|
|
603
|
+
}
|
|
604
|
+
}
|