@xcanwin/manyoyo 5.8.6 → 5.8.9
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/bin/manyoyo.js +121 -173
- package/lib/global-config.js +1 -198
- package/lib/image-build.js +20 -4
- package/lib/init-config.js +22 -10
- package/lib/json5-text-edit.js +238 -0
- package/lib/plugin/playwright-bootstrap.js +116 -0
- package/lib/plugin/playwright-command-output.js +95 -0
- package/lib/plugin/playwright-container-runtime.js +94 -0
- package/lib/plugin/playwright-extension-manager.js +265 -0
- package/lib/plugin/playwright-extension-paths.js +98 -0
- package/lib/plugin/playwright-host-runtime.js +114 -0
- package/lib/plugin/playwright-scene-config.js +137 -0
- package/lib/plugin/playwright-scene-drivers.js +285 -0
- package/lib/plugin/playwright-scene-state.js +80 -0
- package/lib/plugin/playwright.js +169 -1049
- package/lib/runtime-normalizers.js +65 -0
- package/lib/runtime-resolver.js +195 -0
- package/lib/web/agent-command.js +153 -0
- package/lib/web/api-route-helpers.js +88 -0
- package/lib/web/container-exec.js +215 -0
- package/lib/web/http-handlers.js +163 -0
- package/lib/web/runtime-state.js +50 -0
- package/lib/web/server-context.js +71 -0
- package/lib/web/server-lifecycle.js +129 -0
- package/lib/web/server.js +293 -2496
- package/lib/web/session-api-routes.js +390 -0
- package/lib/web/structured-output.js +149 -0
- package/lib/web/structured-trace.js +603 -0
- package/lib/web/system-api-routes.js +114 -0
- package/lib/web/terminal-session.js +205 -0
- package/lib/web/upgrade-handler.js +94 -0
- package/package.json +1 -1
package/lib/plugin/playwright.js
CHANGED
|
@@ -5,8 +5,17 @@ const net = require('net');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const crypto = require('crypto');
|
|
8
|
-
const {
|
|
8
|
+
const { spawnSync } = require('child_process');
|
|
9
9
|
const { playwrightCliVersion: PLAYWRIGHT_CLI_VERSION } = require('../../package.json');
|
|
10
|
+
const { PlaywrightExtensionManager } = require('./playwright-extension-manager');
|
|
11
|
+
const { createPlaywrightCommandOutputManager } = require('./playwright-command-output');
|
|
12
|
+
const { createPlaywrightContainerRuntimeManager } = require('./playwright-container-runtime');
|
|
13
|
+
const { createPlaywrightExtensionPathManager } = require('./playwright-extension-paths');
|
|
14
|
+
const { createPlaywrightHostRuntimeManager } = require('./playwright-host-runtime');
|
|
15
|
+
const { createPlaywrightBootstrapManager } = require('./playwright-bootstrap');
|
|
16
|
+
const { createPlaywrightSceneConfigManager } = require('./playwright-scene-config');
|
|
17
|
+
const { createPlaywrightSceneDrivers } = require('./playwright-scene-drivers');
|
|
18
|
+
const { createPlaywrightSceneStateManager } = require('./playwright-scene-state');
|
|
10
19
|
|
|
11
20
|
const EXTENSIONS = [
|
|
12
21
|
['ublock-origin-lite', 'ddkjiahejlhfcafbddmgiahcphecmpfh'],
|
|
@@ -77,6 +86,12 @@ const SCENE_DEFS = {
|
|
|
77
86
|
|
|
78
87
|
const VALID_RUNTIME = new Set(['container', 'host', 'mixed']);
|
|
79
88
|
const VALID_ACTIONS = new Set(['up', 'down', 'status', 'health', 'logs']);
|
|
89
|
+
const DIRECT_ACTION_HANDLERS = {
|
|
90
|
+
ls: (plugin) => plugin.printSummary(),
|
|
91
|
+
'mcp-add': (plugin, options) => plugin.printMcpAdd(options.host),
|
|
92
|
+
'cli-add': (plugin) => plugin.printCliAdd(),
|
|
93
|
+
'ext-download': (plugin, options) => plugin.downloadExtensions({ prodversion: options.prodversion })
|
|
94
|
+
};
|
|
80
95
|
const CONTAINER_EXTENSION_ROOT = '/app/extensions';
|
|
81
96
|
const DEFAULT_FINGERPRINT_PROFILE = {
|
|
82
97
|
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36',
|
|
@@ -154,129 +169,6 @@ function asBoolean(value, fallback = false) {
|
|
|
154
169
|
return fallback;
|
|
155
170
|
}
|
|
156
171
|
|
|
157
|
-
function isHostPermission(value) {
|
|
158
|
-
if (value === '<all_urls>') {
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
return /^(?:\*|http|https|file|ftp):\/\//.test(value);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function scriptSourcesFromHtml(htmlFile) {
|
|
165
|
-
const content = fs.readFileSync(htmlFile, { encoding: 'utf8' });
|
|
166
|
-
const scripts = [...content.matchAll(/<script[^>]+src=["']([^"']+)["']/gi)].map(m => m[1]);
|
|
167
|
-
return scripts.filter(src => !/^(?:https?:)?\/\//.test(src));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function convertManifestV2ToV3(extDir) {
|
|
171
|
-
const manifestFile = path.join(extDir, 'manifest.json');
|
|
172
|
-
const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
|
|
173
|
-
if (manifest.manifest_version !== 2) {
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
manifest.manifest_version = 3;
|
|
178
|
-
|
|
179
|
-
if (manifest.browser_action && !manifest.action) {
|
|
180
|
-
manifest.action = manifest.browser_action;
|
|
181
|
-
delete manifest.browser_action;
|
|
182
|
-
}
|
|
183
|
-
if (manifest.page_action && !manifest.action) {
|
|
184
|
-
manifest.action = manifest.page_action;
|
|
185
|
-
delete manifest.page_action;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const background = manifest.background;
|
|
189
|
-
if (background && typeof background === 'object' && !Array.isArray(background)) {
|
|
190
|
-
let scripts = [];
|
|
191
|
-
if (Array.isArray(background.scripts)) {
|
|
192
|
-
scripts = background.scripts.filter(s => typeof s === 'string');
|
|
193
|
-
} else if (typeof background.page === 'string') {
|
|
194
|
-
const pagePath = path.join(extDir, background.page);
|
|
195
|
-
if (fs.existsSync(pagePath)) {
|
|
196
|
-
scripts = scriptSourcesFromHtml(pagePath);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (scripts.length > 0) {
|
|
201
|
-
const swName = 'generated_background_sw.js';
|
|
202
|
-
const swFile = path.join(extDir, swName);
|
|
203
|
-
const swLines = [
|
|
204
|
-
'// Auto-generated by manyoyo playwright ext-download for MV3.',
|
|
205
|
-
`importScripts(${scripts.map(s => JSON.stringify(s)).join(', ')});`,
|
|
206
|
-
''
|
|
207
|
-
];
|
|
208
|
-
fs.writeFileSync(swFile, swLines.join('\n'), 'utf8');
|
|
209
|
-
manifest.background = { service_worker: swName };
|
|
210
|
-
} else {
|
|
211
|
-
delete manifest.background;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (typeof manifest.content_security_policy === 'string') {
|
|
216
|
-
manifest.content_security_policy = { extension_pages: manifest.content_security_policy };
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (Array.isArray(manifest.permissions)) {
|
|
220
|
-
const hostPermissions = Array.isArray(manifest.host_permissions) ? [...manifest.host_permissions] : [];
|
|
221
|
-
const keptPermissions = [];
|
|
222
|
-
|
|
223
|
-
for (const perm of manifest.permissions) {
|
|
224
|
-
if (typeof perm === 'string' && isHostPermission(perm)) {
|
|
225
|
-
if (!hostPermissions.includes(perm)) {
|
|
226
|
-
hostPermissions.push(perm);
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
keptPermissions.push(perm);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
manifest.permissions = keptPermissions;
|
|
234
|
-
if (hostPermissions.length > 0) {
|
|
235
|
-
manifest.host_permissions = hostPermissions;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const war = manifest.web_accessible_resources;
|
|
240
|
-
if (Array.isArray(war) && war.length > 0 && war.every(v => typeof v === 'string')) {
|
|
241
|
-
manifest.web_accessible_resources = [
|
|
242
|
-
{
|
|
243
|
-
resources: war,
|
|
244
|
-
matches: ['<all_urls>']
|
|
245
|
-
}
|
|
246
|
-
];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
250
|
-
return true;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function buildCrxUrl(extId, prodversion) {
|
|
254
|
-
return (
|
|
255
|
-
'https://clients2.google.com/service/update2/crx' +
|
|
256
|
-
`?response=redirect&prodversion=${prodversion}` +
|
|
257
|
-
'&acceptformat=crx2,crx3' +
|
|
258
|
-
`&x=id%3D${extId}%26installsource%3Dondemand%26uc`
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function crxZipOffset(data) {
|
|
263
|
-
if (data.subarray(0, 4).toString('ascii') !== 'Cr24') {
|
|
264
|
-
throw new Error('not a CRX file');
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const version = data.readUInt32LE(4);
|
|
268
|
-
if (version === 2) {
|
|
269
|
-
const pubLen = data.readUInt32LE(8);
|
|
270
|
-
const sigLen = data.readUInt32LE(12);
|
|
271
|
-
return 16 + pubLen + sigLen;
|
|
272
|
-
}
|
|
273
|
-
if (version === 3) {
|
|
274
|
-
const headerLen = data.readUInt32LE(8);
|
|
275
|
-
return 12 + headerLen;
|
|
276
|
-
}
|
|
277
|
-
throw new Error(`unsupported CRX version: ${version}`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
172
|
class PlaywrightPlugin {
|
|
281
173
|
constructor(options = {}) {
|
|
282
174
|
this.projectRoot = options.projectRoot || path.join(__dirname, '..', '..');
|
|
@@ -285,6 +177,58 @@ class PlaywrightPlugin {
|
|
|
285
177
|
this.globalConfig = asObject(options.globalConfig);
|
|
286
178
|
this.runConfig = asObject(options.runConfig);
|
|
287
179
|
this.config = this.resolveConfig();
|
|
180
|
+
this.extensionManager = new PlaywrightExtensionManager({
|
|
181
|
+
extensions: EXTENSIONS,
|
|
182
|
+
ensureCommandAvailable: this.ensureCommandAvailable.bind(this),
|
|
183
|
+
runCmd: this.runCmd.bind(this),
|
|
184
|
+
writeStdout: this.writeStdout.bind(this),
|
|
185
|
+
writeStderr: this.writeStderr.bind(this),
|
|
186
|
+
extensionDirPath: this.extensionDirPath.bind(this),
|
|
187
|
+
extensionTmpDirPath: this.extensionTmpDirPath.bind(this),
|
|
188
|
+
defaultProdversion: this.config.extensionProdversion
|
|
189
|
+
});
|
|
190
|
+
this.containerRuntimeManager = createPlaywrightContainerRuntimeManager({
|
|
191
|
+
plugin: this,
|
|
192
|
+
sceneDefs: SCENE_DEFS
|
|
193
|
+
});
|
|
194
|
+
this.commandOutputManager = createPlaywrightCommandOutputManager({
|
|
195
|
+
plugin: this,
|
|
196
|
+
isMcpScene,
|
|
197
|
+
playwrightCliVersion: PLAYWRIGHT_CLI_VERSION
|
|
198
|
+
});
|
|
199
|
+
this.hostRuntimeManager = createPlaywrightHostRuntimeManager({
|
|
200
|
+
plugin: this,
|
|
201
|
+
isCliScene,
|
|
202
|
+
sleep
|
|
203
|
+
});
|
|
204
|
+
this.extensionPathManager = createPlaywrightExtensionPathManager({
|
|
205
|
+
plugin: this,
|
|
206
|
+
asStringArray,
|
|
207
|
+
containerExtensionRoot: CONTAINER_EXTENSION_ROOT
|
|
208
|
+
});
|
|
209
|
+
this.bootstrapManager = createPlaywrightBootstrapManager({
|
|
210
|
+
plugin: this,
|
|
211
|
+
isCliScene
|
|
212
|
+
});
|
|
213
|
+
this.sceneConfigManager = createPlaywrightSceneConfigManager({
|
|
214
|
+
plugin: this,
|
|
215
|
+
sceneDefs: SCENE_DEFS,
|
|
216
|
+
isCliScene,
|
|
217
|
+
asStringArray,
|
|
218
|
+
defaultFingerprintProfile: DEFAULT_FINGERPRINT_PROFILE,
|
|
219
|
+
disableWebRtcLaunchArgs: DISABLE_WEBRTC_LAUNCH_ARGS
|
|
220
|
+
});
|
|
221
|
+
this.sceneStateManager = createPlaywrightSceneStateManager({
|
|
222
|
+
plugin: this
|
|
223
|
+
});
|
|
224
|
+
this.sceneDrivers = createPlaywrightSceneDrivers({
|
|
225
|
+
plugin: this,
|
|
226
|
+
sceneDefs: SCENE_DEFS,
|
|
227
|
+
isCliScene,
|
|
228
|
+
asStringArray,
|
|
229
|
+
tailText,
|
|
230
|
+
sleep
|
|
231
|
+
});
|
|
288
232
|
}
|
|
289
233
|
|
|
290
234
|
resolveConfig() {
|
|
@@ -472,41 +416,31 @@ class PlaywrightPlugin {
|
|
|
472
416
|
}
|
|
473
417
|
|
|
474
418
|
sceneEndpointPath(sceneName) {
|
|
475
|
-
return
|
|
419
|
+
return this.sceneStateManager.sceneEndpointPath(sceneName);
|
|
476
420
|
}
|
|
477
421
|
|
|
478
422
|
readSceneEndpoint(sceneName) {
|
|
479
|
-
|
|
480
|
-
if (!fs.existsSync(filePath)) {
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
try {
|
|
484
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
485
|
-
} catch {
|
|
486
|
-
return null;
|
|
487
|
-
}
|
|
423
|
+
return this.sceneStateManager.readSceneEndpoint(sceneName);
|
|
488
424
|
}
|
|
489
425
|
|
|
490
426
|
writeSceneEndpoint(sceneName, payload) {
|
|
491
|
-
|
|
492
|
-
fs.writeFileSync(this.sceneEndpointPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
427
|
+
this.sceneStateManager.writeSceneEndpoint(sceneName, payload);
|
|
493
428
|
}
|
|
494
429
|
|
|
495
430
|
removeSceneEndpoint(sceneName) {
|
|
496
|
-
|
|
431
|
+
this.sceneStateManager.removeSceneEndpoint(sceneName);
|
|
497
432
|
}
|
|
498
433
|
|
|
499
434
|
sceneCliAttachConfigPath(sceneName) {
|
|
500
|
-
return
|
|
435
|
+
return this.sceneStateManager.sceneCliAttachConfigPath(sceneName);
|
|
501
436
|
}
|
|
502
437
|
|
|
503
438
|
writeSceneCliAttachConfig(sceneName, payload) {
|
|
504
|
-
|
|
505
|
-
fs.writeFileSync(this.sceneCliAttachConfigPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
439
|
+
this.sceneStateManager.writeSceneCliAttachConfig(sceneName, payload);
|
|
506
440
|
}
|
|
507
441
|
|
|
508
442
|
removeSceneCliAttachConfig(sceneName) {
|
|
509
|
-
|
|
443
|
+
this.sceneStateManager.removeSceneCliAttachConfig(sceneName);
|
|
510
444
|
}
|
|
511
445
|
|
|
512
446
|
sceneInitScriptPath(sceneName) {
|
|
@@ -515,81 +449,23 @@ class PlaywrightPlugin {
|
|
|
515
449
|
}
|
|
516
450
|
|
|
517
451
|
buildInitScriptContent() {
|
|
518
|
-
|
|
519
|
-
"'use strict';",
|
|
520
|
-
'(function () {',
|
|
521
|
-
` const platformValue = ${JSON.stringify(this.config.navigatorPlatform)};`,
|
|
522
|
-
' try {',
|
|
523
|
-
' const navProto = Object.getPrototypeOf(navigator);',
|
|
524
|
-
" Object.defineProperty(navProto, 'platform', {",
|
|
525
|
-
' configurable: true,',
|
|
526
|
-
' get: () => platformValue',
|
|
527
|
-
' });',
|
|
528
|
-
' } catch (_) {}'
|
|
529
|
-
];
|
|
530
|
-
|
|
531
|
-
if (this.config.disableWebRTC) {
|
|
532
|
-
lines.push(
|
|
533
|
-
' try {',
|
|
534
|
-
' const scope = globalThis;',
|
|
535
|
-
" const blocked = ['RTCPeerConnection', 'webkitRTCPeerConnection', 'RTCIceCandidate', 'RTCRtpSender', 'RTCRtpReceiver', 'RTCRtpTransceiver', 'RTCDataChannel'];",
|
|
536
|
-
' for (const name of blocked) {',
|
|
537
|
-
" Object.defineProperty(scope, name, { configurable: true, writable: true, value: undefined });",
|
|
538
|
-
' }',
|
|
539
|
-
' if (navigator.mediaDevices) {',
|
|
540
|
-
' const errorFactory = () => {',
|
|
541
|
-
' try {',
|
|
542
|
-
" return new DOMException('WebRTC is disabled', 'NotAllowedError');",
|
|
543
|
-
' } catch (_) {',
|
|
544
|
-
" const error = new Error('WebRTC is disabled');",
|
|
545
|
-
" error.name = 'NotAllowedError';",
|
|
546
|
-
' return error;',
|
|
547
|
-
' }',
|
|
548
|
-
' };',
|
|
549
|
-
" Object.defineProperty(navigator.mediaDevices, 'getUserMedia', {",
|
|
550
|
-
' configurable: true,',
|
|
551
|
-
' writable: true,',
|
|
552
|
-
' value: async () => { throw errorFactory(); }',
|
|
553
|
-
' });',
|
|
554
|
-
' }',
|
|
555
|
-
' } catch (_) {}'
|
|
556
|
-
);
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
lines.push('})();', '');
|
|
560
|
-
return lines.join('\n');
|
|
452
|
+
return this.bootstrapManager.buildInitScriptContent();
|
|
561
453
|
}
|
|
562
454
|
|
|
563
455
|
ensureSceneInitScript(sceneName) {
|
|
564
|
-
|
|
565
|
-
const content = this.buildInitScriptContent();
|
|
566
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
567
|
-
return filePath;
|
|
456
|
+
return this.bootstrapManager.ensureSceneInitScript(sceneName);
|
|
568
457
|
}
|
|
569
458
|
|
|
570
459
|
defaultBrowserName(sceneName) {
|
|
571
|
-
|
|
572
|
-
return 'chromium';
|
|
573
|
-
}
|
|
574
|
-
const cfg = this.buildSceneConfig(sceneName);
|
|
575
|
-
const browserName = cfg && cfg.browser && cfg.browser.browserName;
|
|
576
|
-
return String(browserName || 'chromium');
|
|
460
|
+
return this.bootstrapManager.defaultBrowserName(sceneName);
|
|
577
461
|
}
|
|
578
462
|
|
|
579
463
|
ensureContainerScenePrerequisites(sceneName) {
|
|
580
|
-
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
const tag = String(this.config.dockerTag || 'latest').trim() || 'latest';
|
|
584
|
-
const image = `mcr.microsoft.com/playwright/mcp:${tag}`;
|
|
585
|
-
this.runCmd([this.config.containerRuntime, 'pull', image], { check: true });
|
|
464
|
+
this.bootstrapManager.ensureContainerScenePrerequisites(sceneName);
|
|
586
465
|
}
|
|
587
466
|
|
|
588
467
|
ensureHostScenePrerequisites(sceneName) {
|
|
589
|
-
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
this.runCmd([this.playwrightBinPath(sceneName), 'install', '--with-deps', this.defaultBrowserName(sceneName)], { check: true });
|
|
468
|
+
this.bootstrapManager.ensureHostScenePrerequisites(sceneName);
|
|
593
469
|
}
|
|
594
470
|
|
|
595
471
|
scenePidFile(sceneName) {
|
|
@@ -601,32 +477,11 @@ class PlaywrightPlugin {
|
|
|
601
477
|
}
|
|
602
478
|
|
|
603
479
|
localBinPath(binName) {
|
|
604
|
-
|
|
605
|
-
const binPath = path.join(this.projectRoot, 'node_modules', '.bin', filename);
|
|
606
|
-
if (!fs.existsSync(binPath)) {
|
|
607
|
-
throw new Error(`local binary not found: ${binPath}. Run npm install first.`);
|
|
608
|
-
}
|
|
609
|
-
return binPath;
|
|
480
|
+
return this.bootstrapManager.localBinPath(binName);
|
|
610
481
|
}
|
|
611
482
|
|
|
612
483
|
playwrightBinPath(sceneName) {
|
|
613
|
-
|
|
614
|
-
return this.localBinPath('playwright');
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const filename = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
|
|
618
|
-
const candidates = [
|
|
619
|
-
path.join(this.projectRoot, 'node_modules', '@playwright', 'mcp', 'node_modules', '.bin', filename),
|
|
620
|
-
path.join(this.projectRoot, 'node_modules', '.bin', filename)
|
|
621
|
-
];
|
|
622
|
-
|
|
623
|
-
for (const candidate of candidates) {
|
|
624
|
-
if (fs.existsSync(candidate)) {
|
|
625
|
-
return candidate;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
throw new Error(`local binary not found for ${sceneName}. Run npm install first.`);
|
|
484
|
+
return this.bootstrapManager.playwrightBinPath(sceneName);
|
|
630
485
|
}
|
|
631
486
|
|
|
632
487
|
extensionDirPath() {
|
|
@@ -680,216 +535,55 @@ class PlaywrightPlugin {
|
|
|
680
535
|
}
|
|
681
536
|
|
|
682
537
|
resolveExtensionPaths(extensionArgs = []) {
|
|
683
|
-
|
|
684
|
-
const uniquePaths = [];
|
|
685
|
-
const seen = new Set();
|
|
686
|
-
|
|
687
|
-
for (const item of inputs) {
|
|
688
|
-
const absPath = path.resolve(item);
|
|
689
|
-
if (!fs.existsSync(absPath)) {
|
|
690
|
-
throw new Error(`扩展路径不存在: ${absPath}`);
|
|
691
|
-
}
|
|
692
|
-
const stat = fs.statSync(absPath);
|
|
693
|
-
if (!stat.isDirectory()) {
|
|
694
|
-
throw new Error(`扩展路径必须是目录: ${absPath}`);
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const manifestPath = path.join(absPath, 'manifest.json');
|
|
698
|
-
if (fs.existsSync(manifestPath)) {
|
|
699
|
-
if (!seen.has(absPath)) {
|
|
700
|
-
seen.add(absPath);
|
|
701
|
-
uniquePaths.push(absPath);
|
|
702
|
-
}
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
const children = fs.readdirSync(absPath, { withFileTypes: true })
|
|
707
|
-
.filter(dirent => dirent.isDirectory())
|
|
708
|
-
.map(dirent => path.join(absPath, dirent.name))
|
|
709
|
-
.filter(child => fs.existsSync(path.join(child, 'manifest.json')));
|
|
710
|
-
|
|
711
|
-
if (children.length === 0) {
|
|
712
|
-
throw new Error(`目录下未找到扩展(manifest.json): ${absPath}`);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
for (const childPath of children) {
|
|
716
|
-
if (!seen.has(childPath)) {
|
|
717
|
-
seen.add(childPath);
|
|
718
|
-
uniquePaths.push(childPath);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
return uniquePaths;
|
|
538
|
+
return this.extensionPathManager.resolveExtensionPaths(extensionArgs);
|
|
724
539
|
}
|
|
725
540
|
|
|
726
541
|
resolveNamedExtensionPaths(extensionNames = []) {
|
|
727
|
-
|
|
728
|
-
const extensionRoot = path.resolve(this.extensionDirPath());
|
|
729
|
-
|
|
730
|
-
return names.map(name => {
|
|
731
|
-
if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
|
|
732
|
-
throw new Error(`扩展名称无效: ${name}`);
|
|
733
|
-
}
|
|
734
|
-
return path.join(extensionRoot, name);
|
|
735
|
-
});
|
|
542
|
+
return this.extensionPathManager.resolveNamedExtensionPaths(extensionNames);
|
|
736
543
|
}
|
|
737
544
|
|
|
738
545
|
resolveExtensionInputs(options = {}) {
|
|
739
|
-
|
|
740
|
-
const namedPaths = this.resolveNamedExtensionPaths(options.extensionNames || []);
|
|
741
|
-
return this.resolveExtensionPaths([...extensionPaths, ...namedPaths]);
|
|
546
|
+
return this.extensionPathManager.resolveExtensionInputs(options);
|
|
742
547
|
}
|
|
743
548
|
|
|
744
549
|
buildExtensionLaunchArgs(extensionPaths) {
|
|
745
|
-
|
|
746
|
-
return [
|
|
747
|
-
`--disable-extensions-except=${joined}`,
|
|
748
|
-
`--load-extension=${joined}`
|
|
749
|
-
];
|
|
550
|
+
return this.sceneConfigManager.buildExtensionLaunchArgs(extensionPaths);
|
|
750
551
|
}
|
|
751
552
|
|
|
752
553
|
sanitizeExtensionMountName(value) {
|
|
753
|
-
|
|
754
|
-
.trim()
|
|
755
|
-
.replace(/[^A-Za-z0-9._-]/g, '-')
|
|
756
|
-
.replace(/-+/g, '-')
|
|
757
|
-
.replace(/^-|-$/g, '');
|
|
758
|
-
return sanitized || 'ext';
|
|
554
|
+
return this.extensionPathManager.sanitizeExtensionMountName(value);
|
|
759
555
|
}
|
|
760
556
|
|
|
761
557
|
buildContainerExtensionMounts(extensionPaths = []) {
|
|
762
|
-
|
|
763
|
-
const containerPaths = [];
|
|
764
|
-
const volumeMounts = [];
|
|
765
|
-
|
|
766
|
-
hostPaths.forEach((hostPath, idx) => {
|
|
767
|
-
const safeName = this.sanitizeExtensionMountName(path.basename(hostPath));
|
|
768
|
-
const containerPath = path.posix.join(CONTAINER_EXTENSION_ROOT, `ext-${idx + 1}-${safeName}`);
|
|
769
|
-
containerPaths.push(containerPath);
|
|
770
|
-
volumeMounts.push(`${hostPath}:${containerPath}:ro`);
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
return { containerPaths, volumeMounts };
|
|
558
|
+
return this.extensionPathManager.buildContainerExtensionMounts(extensionPaths);
|
|
774
559
|
}
|
|
775
560
|
|
|
776
561
|
baseLaunchArgs() {
|
|
777
|
-
return
|
|
778
|
-
`--user-agent=${DEFAULT_FINGERPRINT_PROFILE.userAgent}`,
|
|
779
|
-
`--lang=${DEFAULT_FINGERPRINT_PROFILE.locale}`,
|
|
780
|
-
`--window-size=${DEFAULT_FINGERPRINT_PROFILE.width},${DEFAULT_FINGERPRINT_PROFILE.height}`,
|
|
781
|
-
'--disable-blink-features=AutomationControlled',
|
|
782
|
-
'--force-webrtc-ip-handling-policy=disable_non_proxied_udp'
|
|
783
|
-
];
|
|
562
|
+
return this.sceneConfigManager.baseLaunchArgs();
|
|
784
563
|
}
|
|
785
564
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
790
|
-
const initScript = asStringArray(options.initScript, []);
|
|
791
|
-
const launchOptions = {
|
|
792
|
-
channel: 'chromium',
|
|
793
|
-
headless: def.headless,
|
|
794
|
-
args: [...this.baseLaunchArgs()]
|
|
795
|
-
};
|
|
796
|
-
|
|
797
|
-
if (extensionPaths.length > 0) {
|
|
798
|
-
launchOptions.args.push(...this.buildExtensionLaunchArgs(extensionPaths));
|
|
799
|
-
}
|
|
800
|
-
if (this.config.disableWebRTC) {
|
|
801
|
-
launchOptions.args.push(...DISABLE_WEBRTC_LAUNCH_ARGS);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const contextOptions = {
|
|
805
|
-
userAgent: DEFAULT_FINGERPRINT_PROFILE.userAgent,
|
|
806
|
-
locale: DEFAULT_FINGERPRINT_PROFILE.locale,
|
|
807
|
-
timezoneId: DEFAULT_FINGERPRINT_PROFILE.timezoneId,
|
|
808
|
-
extraHTTPHeaders: {
|
|
809
|
-
'Accept-Language': DEFAULT_FINGERPRINT_PROFILE.acceptLanguage
|
|
810
|
-
}
|
|
811
|
-
};
|
|
812
|
-
if (sceneName !== 'mcp-host-headed') {
|
|
813
|
-
contextOptions.viewport = {
|
|
814
|
-
width: DEFAULT_FINGERPRINT_PROFILE.width,
|
|
815
|
-
height: DEFAULT_FINGERPRINT_PROFILE.height
|
|
816
|
-
};
|
|
817
|
-
contextOptions.screen = {
|
|
818
|
-
width: DEFAULT_FINGERPRINT_PROFILE.width,
|
|
819
|
-
height: DEFAULT_FINGERPRINT_PROFILE.height
|
|
820
|
-
};
|
|
821
|
-
}
|
|
565
|
+
buildSceneLaunchArgs(extensionPaths = []) {
|
|
566
|
+
return this.sceneConfigManager.buildSceneLaunchArgs(extensionPaths);
|
|
567
|
+
}
|
|
822
568
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
server: {
|
|
826
|
-
host: def.listenHost,
|
|
827
|
-
port,
|
|
828
|
-
allowedHosts: [
|
|
829
|
-
`localhost:${port}`,
|
|
830
|
-
`127.0.0.1:${port}`,
|
|
831
|
-
`host.docker.internal:${port}`,
|
|
832
|
-
`host.containers.internal:${port}`
|
|
833
|
-
]
|
|
834
|
-
},
|
|
835
|
-
browser: {
|
|
836
|
-
chromiumSandbox: true,
|
|
837
|
-
browserName: 'chromium',
|
|
838
|
-
initScript,
|
|
839
|
-
launchOptions,
|
|
840
|
-
contextOptions
|
|
841
|
-
}
|
|
842
|
-
};
|
|
569
|
+
buildMcpSceneConfig(sceneName, options = {}) {
|
|
570
|
+
return this.sceneConfigManager.buildMcpSceneConfig(sceneName, options);
|
|
843
571
|
}
|
|
844
572
|
|
|
845
573
|
buildCliSceneConfig(sceneName, options = {}) {
|
|
846
|
-
|
|
847
|
-
const extensionPaths = asStringArray(options.extensionPaths, []);
|
|
848
|
-
const payload = {
|
|
849
|
-
host: def.listenHost,
|
|
850
|
-
port: this.scenePort(sceneName),
|
|
851
|
-
wsPath: `/${sceneName}-${crypto.randomBytes(8).toString('hex')}`,
|
|
852
|
-
headless: def.headless,
|
|
853
|
-
channel: 'chromium',
|
|
854
|
-
chromiumSandbox: true,
|
|
855
|
-
args: [...this.baseLaunchArgs()]
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
if (extensionPaths.length > 0) {
|
|
859
|
-
payload.args.push(...this.buildExtensionLaunchArgs(extensionPaths));
|
|
860
|
-
}
|
|
861
|
-
if (this.config.disableWebRTC) {
|
|
862
|
-
payload.args.push(...DISABLE_WEBRTC_LAUNCH_ARGS);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
return payload;
|
|
574
|
+
return this.sceneConfigManager.buildCliSceneConfig(sceneName, options);
|
|
866
575
|
}
|
|
867
576
|
|
|
868
577
|
buildSceneConfig(sceneName, options = {}) {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
578
|
+
return this.sceneConfigManager.buildSceneConfig(sceneName, options);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
writeSceneConfigFile(sceneName, payload) {
|
|
582
|
+
return this.sceneConfigManager.writeSceneConfigFile(sceneName, payload);
|
|
873
583
|
}
|
|
874
584
|
|
|
875
585
|
ensureSceneConfig(sceneName, options = {}) {
|
|
876
|
-
|
|
877
|
-
if (isCliScene(sceneName)) {
|
|
878
|
-
const payload = this.buildCliSceneConfig(sceneName, options);
|
|
879
|
-
const filePath = this.sceneConfigPath(sceneName);
|
|
880
|
-
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
881
|
-
return filePath;
|
|
882
|
-
}
|
|
883
|
-
const initScriptPath = this.ensureSceneInitScript(sceneName);
|
|
884
|
-
const configuredInitScript = asStringArray(options.initScript, []);
|
|
885
|
-
const initScript = configuredInitScript.length > 0 ? configuredInitScript : [initScriptPath];
|
|
886
|
-
const payload = this.buildSceneConfig(sceneName, {
|
|
887
|
-
...options,
|
|
888
|
-
initScript
|
|
889
|
-
});
|
|
890
|
-
const filePath = this.sceneConfigPath(sceneName);
|
|
891
|
-
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
892
|
-
return filePath;
|
|
586
|
+
return this.sceneConfigManager.ensureSceneConfig(sceneName, options);
|
|
893
587
|
}
|
|
894
588
|
|
|
895
589
|
async portReady(port) {
|
|
@@ -926,465 +620,99 @@ class PlaywrightPlugin {
|
|
|
926
620
|
}
|
|
927
621
|
|
|
928
622
|
containerEnv(sceneName, cfgPath, options = {}) {
|
|
929
|
-
|
|
930
|
-
const requireVncPassword = options.requireVncPassword === true;
|
|
931
|
-
const env = {
|
|
932
|
-
...process.env,
|
|
933
|
-
PLAYWRIGHT_MCP_DOCKER_TAG: this.config.dockerTag,
|
|
934
|
-
PLAYWRIGHT_MCP_PORT: String(this.scenePort(sceneName)),
|
|
935
|
-
PLAYWRIGHT_MCP_CONFIG_PATH: cfgPath,
|
|
936
|
-
PLAYWRIGHT_MCP_CONTAINER_NAME: def.containerName,
|
|
937
|
-
PLAYWRIGHT_MCP_IMAGE: this.config.headedImage,
|
|
938
|
-
PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.mcpContHeadedNoVnc)
|
|
939
|
-
};
|
|
940
|
-
|
|
941
|
-
if (sceneName === 'mcp-cont-headed') {
|
|
942
|
-
const envKey = this.config.vncPasswordEnvKey;
|
|
943
|
-
let password = process.env[envKey];
|
|
944
|
-
if (!password) {
|
|
945
|
-
password = this.randomAlnum(16);
|
|
946
|
-
if (requireVncPassword) {
|
|
947
|
-
this.writeStdout(`[up] mcp-cont-headed ${envKey} not set; generated random 16-char password: ${password}`);
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
env.VNC_PASSWORD = password;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
return env;
|
|
623
|
+
return this.containerRuntimeManager.containerEnv(sceneName, cfgPath, options);
|
|
954
624
|
}
|
|
955
625
|
|
|
956
626
|
containerComposePath(sceneName) {
|
|
957
|
-
|
|
958
|
-
return path.join(this.config.composeDir, def.composeFile);
|
|
627
|
+
return this.containerRuntimeManager.containerComposePath(sceneName);
|
|
959
628
|
}
|
|
960
629
|
|
|
961
630
|
sceneComposeOverridePath(sceneName) {
|
|
962
|
-
return
|
|
631
|
+
return this.containerRuntimeManager.sceneComposeOverridePath(sceneName);
|
|
963
632
|
}
|
|
964
633
|
|
|
965
634
|
ensureContainerComposeOverride(sceneName, volumeMounts = []) {
|
|
966
|
-
|
|
967
|
-
if (!Array.isArray(volumeMounts) || volumeMounts.length === 0) {
|
|
968
|
-
fs.rmSync(overridePath, { force: true });
|
|
969
|
-
return '';
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
973
|
-
const lines = [
|
|
974
|
-
'services:',
|
|
975
|
-
' playwright:',
|
|
976
|
-
' volumes:'
|
|
977
|
-
];
|
|
978
|
-
volumeMounts.forEach(item => {
|
|
979
|
-
lines.push(` - ${JSON.stringify(String(item))}`);
|
|
980
|
-
});
|
|
981
|
-
fs.writeFileSync(overridePath, `${lines.join('\n')}\n`, 'utf8');
|
|
982
|
-
return overridePath;
|
|
635
|
+
return this.containerRuntimeManager.ensureContainerComposeOverride(sceneName, volumeMounts);
|
|
983
636
|
}
|
|
984
637
|
|
|
985
638
|
buildCliSessionIntegration(dockerCmd) {
|
|
986
|
-
|
|
987
|
-
if (!sceneName) {
|
|
988
|
-
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
const endpoint = this.readSceneEndpoint(sceneName);
|
|
992
|
-
if (!endpoint || !Number.isInteger(endpoint.port) || endpoint.port <= 0 || typeof endpoint.wsPath !== 'string' || !endpoint.wsPath) {
|
|
993
|
-
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
const normalizedDockerCmd = String(dockerCmd || '').trim().toLowerCase();
|
|
997
|
-
const connectHost = normalizedDockerCmd === 'podman' ? 'host.containers.internal' : 'host.docker.internal';
|
|
998
|
-
const remoteEndpoint = `ws://${connectHost}:${endpoint.port}${endpoint.wsPath}`;
|
|
999
|
-
const hostConfigPath = this.sceneCliAttachConfigPath(sceneName);
|
|
1000
|
-
const containerConfigPath = `/tmp/manyoyo-playwright/${sceneName}.cli-attach.json`;
|
|
1001
|
-
this.writeSceneCliAttachConfig(sceneName, {
|
|
1002
|
-
browser: {
|
|
1003
|
-
remoteEndpoint
|
|
1004
|
-
}
|
|
1005
|
-
});
|
|
1006
|
-
const envEntries = [
|
|
1007
|
-
`PLAYWRIGHT_MCP_CONFIG=${containerConfigPath}`
|
|
1008
|
-
];
|
|
1009
|
-
const extraArgs = normalizedDockerCmd === 'docker'
|
|
1010
|
-
? ['--add-host', 'host.docker.internal:host-gateway']
|
|
1011
|
-
: [];
|
|
1012
|
-
const volumeEntries = ['--volume', `${hostConfigPath}:${containerConfigPath}:ro`];
|
|
1013
|
-
return { envEntries, extraArgs, volumeEntries };
|
|
639
|
+
return this.sceneStateManager.buildCliSessionIntegration(dockerCmd);
|
|
1014
640
|
}
|
|
1015
641
|
|
|
1016
642
|
async startContainer(sceneName, options = {}) {
|
|
1017
|
-
|
|
1018
|
-
if (!this.ensureCommandAvailable(runtime)) {
|
|
1019
|
-
this.writeStderr(`[up] ${sceneName} failed: ${runtime} command not found.`);
|
|
1020
|
-
return 1;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
try {
|
|
1024
|
-
this.ensureContainerScenePrerequisites(sceneName);
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
return error.returncode || 1;
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
const incomingExtensionPaths = asStringArray(options.extensionPaths, []);
|
|
1030
|
-
const hostInitScriptPath = this.sceneInitScriptPath(sceneName);
|
|
1031
|
-
const containerInitScriptPath = path.posix.join('/app/config', path.basename(hostInitScriptPath));
|
|
1032
|
-
let configOptions = {
|
|
1033
|
-
...options,
|
|
1034
|
-
extensionPaths: incomingExtensionPaths,
|
|
1035
|
-
initScript: [containerInitScriptPath]
|
|
1036
|
-
};
|
|
1037
|
-
const composeFiles = [this.containerComposePath(sceneName)];
|
|
1038
|
-
const volumeMounts = [`${hostInitScriptPath}:${containerInitScriptPath}:ro`];
|
|
1039
|
-
|
|
1040
|
-
if (incomingExtensionPaths.length > 0) {
|
|
1041
|
-
const mapped = this.buildContainerExtensionMounts(incomingExtensionPaths);
|
|
1042
|
-
volumeMounts.push(...mapped.volumeMounts);
|
|
1043
|
-
configOptions = {
|
|
1044
|
-
...options,
|
|
1045
|
-
extensionPaths: mapped.containerPaths,
|
|
1046
|
-
initScript: [containerInitScriptPath]
|
|
1047
|
-
};
|
|
1048
|
-
}
|
|
1049
|
-
const cfgPath = this.ensureSceneConfig(sceneName, configOptions);
|
|
1050
|
-
const overridePath = this.ensureContainerComposeOverride(sceneName, volumeMounts);
|
|
1051
|
-
if (overridePath) {
|
|
1052
|
-
composeFiles.push(overridePath);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
const env = this.containerEnv(sceneName, cfgPath, { requireVncPassword: true });
|
|
1056
|
-
const def = SCENE_DEFS[sceneName];
|
|
1057
|
-
|
|
1058
|
-
try {
|
|
1059
|
-
const args = [
|
|
1060
|
-
runtime,
|
|
1061
|
-
'compose',
|
|
1062
|
-
'-p',
|
|
1063
|
-
def.projectName
|
|
1064
|
-
];
|
|
1065
|
-
composeFiles.forEach(filePath => {
|
|
1066
|
-
args.push('-f', filePath);
|
|
1067
|
-
});
|
|
1068
|
-
args.push('up', '-d');
|
|
1069
|
-
this.runCmd(args, { env, check: true });
|
|
1070
|
-
} catch (error) {
|
|
1071
|
-
return error.returncode || 1;
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
const port = this.scenePort(sceneName);
|
|
1075
|
-
if (await this.waitForPort(port)) {
|
|
1076
|
-
this.writeStdout(`[up] ${sceneName} ready on 127.0.0.1:${port}`);
|
|
1077
|
-
return 0;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
this.writeStderr(`[up] ${sceneName} did not become ready on 127.0.0.1:${port}`);
|
|
1081
|
-
return 1;
|
|
643
|
+
return await this.sceneDrivers.container.up(sceneName, options);
|
|
1082
644
|
}
|
|
1083
645
|
|
|
1084
646
|
stopContainer(sceneName) {
|
|
1085
|
-
|
|
1086
|
-
if (!this.ensureCommandAvailable(runtime)) {
|
|
1087
|
-
this.writeStderr(`[down] ${sceneName} failed: ${runtime} command not found.`);
|
|
1088
|
-
return 1;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
this.ensureContainerComposeOverride(sceneName, []);
|
|
1092
|
-
const cfgPath = this.sceneConfigPath(sceneName);
|
|
1093
|
-
const env = this.containerEnv(sceneName, cfgPath);
|
|
1094
|
-
const def = SCENE_DEFS[sceneName];
|
|
1095
|
-
|
|
1096
|
-
try {
|
|
1097
|
-
this.runCmd([
|
|
1098
|
-
runtime,
|
|
1099
|
-
'compose',
|
|
1100
|
-
'-p',
|
|
1101
|
-
def.projectName,
|
|
1102
|
-
'-f',
|
|
1103
|
-
this.containerComposePath(sceneName),
|
|
1104
|
-
'down'
|
|
1105
|
-
], { env, check: true });
|
|
1106
|
-
} catch (error) {
|
|
1107
|
-
return error.returncode || 1;
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
this.writeStdout(`[down] ${sceneName}`);
|
|
1111
|
-
return 0;
|
|
647
|
+
return this.sceneDrivers.container.down(sceneName);
|
|
1112
648
|
}
|
|
1113
649
|
|
|
1114
650
|
statusContainer(sceneName) {
|
|
1115
|
-
|
|
1116
|
-
if (!this.ensureCommandAvailable(runtime)) {
|
|
1117
|
-
this.writeStderr(`[status] ${sceneName} failed: ${runtime} command not found.`);
|
|
1118
|
-
return 1;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const def = SCENE_DEFS[sceneName];
|
|
1122
|
-
const cp = this.runCmd([
|
|
1123
|
-
runtime,
|
|
1124
|
-
'ps',
|
|
1125
|
-
'--filter',
|
|
1126
|
-
`name=${def.containerName}`,
|
|
1127
|
-
'--format',
|
|
1128
|
-
'{{.Names}}'
|
|
1129
|
-
], { captureOutput: true, check: false });
|
|
1130
|
-
|
|
1131
|
-
const names = new Set(
|
|
1132
|
-
cp.stdout
|
|
1133
|
-
.split(/\r?\n/)
|
|
1134
|
-
.map(line => line.trim())
|
|
1135
|
-
.filter(Boolean)
|
|
1136
|
-
);
|
|
1137
|
-
|
|
1138
|
-
if (names.has(def.containerName)) {
|
|
1139
|
-
this.writeStdout(`[status] ${sceneName} running`);
|
|
1140
|
-
} else {
|
|
1141
|
-
this.writeStdout(`[status] ${sceneName} stopped`);
|
|
1142
|
-
}
|
|
1143
|
-
return 0;
|
|
651
|
+
return this.sceneDrivers.container.status(sceneName);
|
|
1144
652
|
}
|
|
1145
653
|
|
|
1146
654
|
logsContainer(sceneName) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
this.writeStderr(`[logs] ${sceneName} failed: ${runtime} command not found.`);
|
|
1150
|
-
return 1;
|
|
1151
|
-
}
|
|
655
|
+
return this.sceneDrivers.container.logs(sceneName);
|
|
656
|
+
}
|
|
1152
657
|
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
'logs',
|
|
1157
|
-
'--tail',
|
|
1158
|
-
'80',
|
|
1159
|
-
def.containerName
|
|
1160
|
-
], { captureOutput: true, check: false });
|
|
1161
|
-
|
|
1162
|
-
const output = cp.stdout || cp.stderr;
|
|
1163
|
-
if (output.trim()) {
|
|
1164
|
-
this.writeStdout(output.trimEnd());
|
|
1165
|
-
} else {
|
|
1166
|
-
this.writeStdout(`[logs] ${sceneName} no logs`);
|
|
1167
|
-
}
|
|
658
|
+
ensureContainerRuntimeAvailable(action, sceneName) {
|
|
659
|
+
return this.containerRuntimeManager.ensureContainerRuntimeAvailable(action, sceneName);
|
|
660
|
+
}
|
|
1168
661
|
|
|
1169
|
-
|
|
662
|
+
buildContainerComposeCommand(sceneName, composeFiles = [], trailingArgs = []) {
|
|
663
|
+
return this.containerRuntimeManager.buildContainerComposeCommand(sceneName, composeFiles, trailingArgs);
|
|
1170
664
|
}
|
|
1171
665
|
|
|
1172
666
|
hostLaunchCommand(sceneName, cfgPath) {
|
|
1173
|
-
|
|
1174
|
-
return {
|
|
1175
|
-
command: this.playwrightBinPath(sceneName),
|
|
1176
|
-
args: ['launch-server', '--browser', this.defaultBrowserName(sceneName), '--config', String(cfgPath)]
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
return {
|
|
1180
|
-
command: this.localBinPath('playwright-mcp'),
|
|
1181
|
-
args: ['--config', String(cfgPath)]
|
|
1182
|
-
};
|
|
667
|
+
return this.hostRuntimeManager.hostLaunchCommand(sceneName, cfgPath);
|
|
1183
668
|
}
|
|
1184
669
|
|
|
1185
670
|
spawnHostProcess(command, args, logFd) {
|
|
1186
|
-
return
|
|
1187
|
-
detached: true,
|
|
1188
|
-
stdio: ['ignore', logFd, logFd]
|
|
1189
|
-
});
|
|
671
|
+
return this.hostRuntimeManager.spawnHostProcess(command, args, logFd);
|
|
1190
672
|
}
|
|
1191
673
|
|
|
1192
674
|
stopHostStarter(pid) {
|
|
1193
|
-
|
|
1194
|
-
return;
|
|
1195
|
-
}
|
|
1196
|
-
try {
|
|
1197
|
-
process.kill(-pid, 'SIGTERM');
|
|
1198
|
-
return;
|
|
1199
|
-
} catch {
|
|
1200
|
-
// no-op
|
|
1201
|
-
}
|
|
1202
|
-
try {
|
|
1203
|
-
process.kill(pid, 'SIGTERM');
|
|
1204
|
-
} catch {
|
|
1205
|
-
// no-op
|
|
1206
|
-
}
|
|
675
|
+
this.hostRuntimeManager.stopHostStarter(pid);
|
|
1207
676
|
}
|
|
1208
677
|
|
|
1209
678
|
hostScenePids(sceneName) {
|
|
1210
|
-
|
|
1211
|
-
const pattern = isCliScene(sceneName)
|
|
1212
|
-
? `playwright.*launch-server.*--config ${cfgPath}`
|
|
1213
|
-
: `playwright-mcp.*--config ${cfgPath}`;
|
|
1214
|
-
const cp = this.runCmd(['pgrep', '-f', pattern], { captureOutput: true, check: false });
|
|
1215
|
-
|
|
1216
|
-
if (cp.returncode !== 0 || !cp.stdout.trim()) {
|
|
1217
|
-
return [];
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const pids = [];
|
|
1221
|
-
for (const line of cp.stdout.split(/\r?\n/)) {
|
|
1222
|
-
const text = line.trim();
|
|
1223
|
-
if (/^\d+$/.test(text)) {
|
|
1224
|
-
pids.push(Number(text));
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
return pids;
|
|
679
|
+
return this.hostRuntimeManager.hostScenePids(sceneName);
|
|
1228
680
|
}
|
|
1229
681
|
|
|
1230
682
|
async waitForHostPids(sceneName, fallbackPid) {
|
|
1231
|
-
|
|
1232
|
-
const pids = this.hostScenePids(sceneName);
|
|
1233
|
-
if (pids.length > 0) {
|
|
1234
|
-
return pids;
|
|
1235
|
-
}
|
|
1236
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1237
|
-
await sleep(100);
|
|
1238
|
-
}
|
|
1239
|
-
if (Number.isInteger(fallbackPid) && fallbackPid > 0) {
|
|
1240
|
-
return [fallbackPid];
|
|
1241
|
-
}
|
|
1242
|
-
return [];
|
|
683
|
+
return await this.hostRuntimeManager.waitForHostPids(sceneName, fallbackPid);
|
|
1243
684
|
}
|
|
1244
685
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
this.ensureHostScenePrerequisites(sceneName);
|
|
1249
|
-
} catch (error) {
|
|
1250
|
-
this.writeStderr(`[up] ${sceneName} failed: ${error.message || String(error)}`);
|
|
1251
|
-
return error.returncode || 1;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
fs.mkdirSync(this.config.runDir, { recursive: true });
|
|
1255
|
-
const cfgPath = this.ensureSceneConfig(sceneName, options);
|
|
1256
|
-
const pidFile = this.scenePidFile(sceneName);
|
|
1257
|
-
const logFile = this.sceneLogFile(sceneName);
|
|
1258
|
-
const port = this.scenePort(sceneName);
|
|
1259
|
-
this.removeSceneEndpoint(sceneName);
|
|
1260
|
-
this.removeSceneCliAttachConfig(sceneName);
|
|
1261
|
-
|
|
1262
|
-
let managedPids = this.hostScenePids(sceneName);
|
|
1263
|
-
if (managedPids.length > 0 && (await this.portReady(port))) {
|
|
1264
|
-
this.writeStdout(`[up] ${sceneName} already running (pid(s) ${managedPids.join(' ')})`);
|
|
1265
|
-
return 0;
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
if (await this.portReady(port)) {
|
|
1269
|
-
this.writeStderr(`[up] ${sceneName} failed: port ${port} is already in use by another process.`);
|
|
1270
|
-
this.writeStderr('Stop the conflicting process first, then retry.');
|
|
1271
|
-
return 1;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
fs.rmSync(pidFile, { force: true });
|
|
1275
|
-
const logFd = fs.openSync(logFile, 'a');
|
|
1276
|
-
let launchCommand = null;
|
|
1277
|
-
try {
|
|
1278
|
-
launchCommand = this.hostLaunchCommand(sceneName, cfgPath);
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
fs.closeSync(logFd);
|
|
1281
|
-
this.writeStderr(`[up] ${sceneName} failed: ${error.message || String(error)}`);
|
|
1282
|
-
return 1;
|
|
1283
|
-
}
|
|
686
|
+
clearHostSceneRuntimeState(sceneName) {
|
|
687
|
+
this.sceneStateManager.clearHostSceneRuntimeState(sceneName);
|
|
688
|
+
}
|
|
1284
689
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
starter.unref();
|
|
1289
|
-
}
|
|
690
|
+
async getHostSceneRuntimeInfo(sceneName) {
|
|
691
|
+
return await this.hostRuntimeManager.getHostSceneRuntimeInfo(sceneName);
|
|
692
|
+
}
|
|
1290
693
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
this.writeSceneEndpoint(sceneName, {
|
|
1295
|
-
port,
|
|
1296
|
-
wsPath: String(cfg.wsPath || '')
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
managedPids = await this.waitForHostPids(sceneName, starter.pid);
|
|
1300
|
-
if (managedPids.length > 0) {
|
|
1301
|
-
fs.writeFileSync(pidFile, `${managedPids[0]}`, 'utf8');
|
|
1302
|
-
this.writeStdout(`[up] ${sceneName} ready on 127.0.0.1:${port} (pid(s) ${managedPids.join(' ')})`);
|
|
1303
|
-
this.remindCliSessionScene(sceneName);
|
|
1304
|
-
return 0;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
694
|
+
signalPids(pids, signal = 'SIGTERM') {
|
|
695
|
+
this.hostRuntimeManager.signalPids(pids, signal);
|
|
696
|
+
}
|
|
1307
697
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
this.writeStderr(tail);
|
|
1312
|
-
}
|
|
698
|
+
readPidFilePid(pidFile) {
|
|
699
|
+
return this.hostRuntimeManager.readPidFilePid(pidFile);
|
|
700
|
+
}
|
|
1313
701
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
702
|
+
writeScenePidFile(pidFile, pid) {
|
|
703
|
+
this.hostRuntimeManager.writeScenePidFile(pidFile, pid);
|
|
704
|
+
}
|
|
1317
705
|
|
|
1318
|
-
|
|
706
|
+
async startHost(sceneName, options = {}) {
|
|
707
|
+
return await this.sceneDrivers.host.up(sceneName, options);
|
|
1319
708
|
}
|
|
1320
709
|
|
|
1321
710
|
async stopHost(sceneName) {
|
|
1322
|
-
|
|
1323
|
-
const port = this.scenePort(sceneName);
|
|
1324
|
-
const managedPids = this.hostScenePids(sceneName);
|
|
1325
|
-
this.removeSceneEndpoint(sceneName);
|
|
1326
|
-
this.removeSceneCliAttachConfig(sceneName);
|
|
1327
|
-
|
|
1328
|
-
for (const pid of managedPids) {
|
|
1329
|
-
try {
|
|
1330
|
-
process.kill(pid, 'SIGTERM');
|
|
1331
|
-
} catch {
|
|
1332
|
-
// no-op
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (managedPids.length > 0) {
|
|
1337
|
-
await sleep(300);
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
if (fs.existsSync(pidFile)) {
|
|
1341
|
-
const text = fs.readFileSync(pidFile, 'utf8').trim();
|
|
1342
|
-
if (/^\d+$/.test(text)) {
|
|
1343
|
-
try {
|
|
1344
|
-
process.kill(Number(text), 'SIGTERM');
|
|
1345
|
-
} catch {
|
|
1346
|
-
// no-op
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
fs.rmSync(pidFile, { force: true });
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
if (await this.portReady(port)) {
|
|
1353
|
-
this.writeStderr(`[down] ${sceneName} warning: port ${port} is still in use (possibly unmanaged process)`);
|
|
1354
|
-
return 1;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
this.writeStdout(`[down] ${sceneName}`);
|
|
1358
|
-
return 0;
|
|
711
|
+
return await this.sceneDrivers.host.down(sceneName);
|
|
1359
712
|
}
|
|
1360
713
|
|
|
1361
714
|
async statusHost(sceneName) {
|
|
1362
|
-
|
|
1363
|
-
const port = this.scenePort(sceneName);
|
|
1364
|
-
const managedPids = this.hostScenePids(sceneName);
|
|
1365
|
-
|
|
1366
|
-
if (managedPids.length > 0 && (await this.portReady(port))) {
|
|
1367
|
-
this.writeStdout(`[status] ${sceneName} running (pid(s) ${managedPids.join(' ')})`);
|
|
1368
|
-
const pidfileValid = fs.existsSync(pidFile) && /^\d+$/.test(fs.readFileSync(pidFile, 'utf8').trim());
|
|
1369
|
-
if (!pidfileValid) {
|
|
1370
|
-
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
|
|
1371
|
-
fs.writeFileSync(pidFile, `${managedPids[0]}`, 'utf8');
|
|
1372
|
-
}
|
|
1373
|
-
return 0;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
if (managedPids.length > 0 && !(await this.portReady(port))) {
|
|
1377
|
-
this.writeStdout(`[status] ${sceneName} degraded (pid(s) ${managedPids.join(' ')}, port ${port} not reachable)`);
|
|
1378
|
-
return 0;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
fs.rmSync(pidFile, { force: true });
|
|
1382
|
-
if (await this.portReady(port)) {
|
|
1383
|
-
this.writeStdout(`[status] ${sceneName} conflict (port ${port} in use by unmanaged process)`);
|
|
1384
|
-
} else {
|
|
1385
|
-
this.writeStdout(`[status] ${sceneName} stopped`);
|
|
1386
|
-
}
|
|
1387
|
-
return 0;
|
|
715
|
+
return await this.sceneDrivers.host.status(sceneName);
|
|
1388
716
|
}
|
|
1389
717
|
|
|
1390
718
|
async healthScene(sceneName) {
|
|
@@ -1398,259 +726,61 @@ class PlaywrightPlugin {
|
|
|
1398
726
|
}
|
|
1399
727
|
|
|
1400
728
|
logsHost(sceneName) {
|
|
1401
|
-
|
|
1402
|
-
if (!fs.existsSync(logFile)) {
|
|
1403
|
-
this.writeStdout(`[logs] ${sceneName} no log file: ${logFile}`);
|
|
1404
|
-
return 0;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
const tail = tailText(logFile, 80);
|
|
1408
|
-
if (tail) {
|
|
1409
|
-
this.writeStdout(tail);
|
|
1410
|
-
}
|
|
1411
|
-
return 0;
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
async downloadFile(url, output, retries = 3, timeoutMs = 60_000) {
|
|
1415
|
-
const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
|
|
1416
|
-
if (!this.ensureCommandAvailable('curl')) {
|
|
1417
|
-
throw new Error('curl command not found');
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
let lastError = null;
|
|
1421
|
-
for (let i = 1; i <= retries; i += 1) {
|
|
1422
|
-
try {
|
|
1423
|
-
const result = this.runCmd([
|
|
1424
|
-
'curl',
|
|
1425
|
-
'--fail',
|
|
1426
|
-
'--location',
|
|
1427
|
-
'--silent',
|
|
1428
|
-
'--show-error',
|
|
1429
|
-
'--connect-timeout',
|
|
1430
|
-
String(timeoutSec),
|
|
1431
|
-
'--max-time',
|
|
1432
|
-
String(timeoutSec),
|
|
1433
|
-
'--output',
|
|
1434
|
-
output,
|
|
1435
|
-
url
|
|
1436
|
-
], { captureOutput: true, check: false });
|
|
1437
|
-
if (result.returncode !== 0) {
|
|
1438
|
-
throw new Error(result.stderr || `curl failed with exit code ${result.returncode}`);
|
|
1439
|
-
}
|
|
1440
|
-
return;
|
|
1441
|
-
} catch (error) {
|
|
1442
|
-
lastError = error;
|
|
1443
|
-
if (i < retries) {
|
|
1444
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1445
|
-
await sleep(1000);
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
throw new Error(`download failed after ${retries} attempts: ${url}; ${String(lastError)}`);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
extractZipBuffer(zipBuffer, outDir) {
|
|
1454
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
1455
|
-
const tempZip = path.join(os.tmpdir(), `manyoyo-playwright-ext-${process.pid}-${Date.now()}.zip`);
|
|
1456
|
-
fs.writeFileSync(tempZip, zipBuffer);
|
|
1457
|
-
|
|
1458
|
-
const result = spawnSync('unzip', ['-oq', tempZip, '-d', outDir], { encoding: 'utf8' });
|
|
1459
|
-
fs.rmSync(tempZip, { force: true });
|
|
1460
|
-
|
|
1461
|
-
if (result.error) {
|
|
1462
|
-
throw result.error;
|
|
1463
|
-
}
|
|
1464
|
-
if (result.status !== 0) {
|
|
1465
|
-
throw new Error(result.stderr || `unzip failed with exit code ${result.status}`);
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
extractCrx(crxFile, outDir) {
|
|
1470
|
-
const data = fs.readFileSync(crxFile);
|
|
1471
|
-
const offset = crxZipOffset(data);
|
|
1472
|
-
const zipBuffer = data.subarray(offset);
|
|
1473
|
-
|
|
1474
|
-
this.extractZipBuffer(zipBuffer, outDir);
|
|
1475
|
-
|
|
1476
|
-
const manifest = path.join(outDir, 'manifest.json');
|
|
1477
|
-
if (!fs.existsSync(manifest)) {
|
|
1478
|
-
throw new Error(`${crxFile} extracted but manifest.json missing`);
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
if (convertManifestV2ToV3(outDir)) {
|
|
1482
|
-
this.writeStdout(`[manifest] upgraded to MV3: ${path.basename(outDir)}`);
|
|
1483
|
-
}
|
|
729
|
+
return this.sceneDrivers.host.logs(sceneName);
|
|
1484
730
|
}
|
|
1485
731
|
|
|
1486
732
|
async downloadExtensions(options = {}) {
|
|
1487
|
-
|
|
1488
|
-
this.writeStderr('[ext-download] failed: unzip command not found.');
|
|
1489
|
-
return 1;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
const prodversion = String(options.prodversion || this.config.extensionProdversion || '132.0.0.0').trim();
|
|
1493
|
-
const extDir = path.resolve(this.extensionDirPath());
|
|
1494
|
-
const tmpDir = path.resolve(this.extensionTmpDirPath());
|
|
1495
|
-
|
|
1496
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1497
|
-
fs.mkdirSync(extDir, { recursive: true });
|
|
1498
|
-
fs.mkdirSync(tmpDir, { recursive: true });
|
|
1499
|
-
|
|
1500
|
-
try {
|
|
1501
|
-
this.writeStdout(`[info] ext dir: ${extDir}`);
|
|
1502
|
-
this.writeStdout(`[info] tmp dir: ${tmpDir}`);
|
|
1503
|
-
|
|
1504
|
-
for (const [name, extId] of EXTENSIONS) {
|
|
1505
|
-
const url = buildCrxUrl(extId, prodversion);
|
|
1506
|
-
const crxFile = path.join(tmpDir, `${name}.crx`);
|
|
1507
|
-
const outDir = path.join(extDir, name);
|
|
1508
|
-
|
|
1509
|
-
this.writeStdout(`[download] ${name}`);
|
|
1510
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1511
|
-
await this.downloadFile(url, crxFile);
|
|
1512
|
-
|
|
1513
|
-
this.writeStdout(`[extract] ${name}`);
|
|
1514
|
-
fs.rmSync(outDir, { recursive: true, force: true });
|
|
1515
|
-
this.extractCrx(crxFile, outDir);
|
|
1516
|
-
}
|
|
1517
|
-
} finally {
|
|
1518
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1519
|
-
this.writeStdout(`[cleanup] removed ${tmpDir}`);
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
this.writeStdout(`[done] all extensions are ready: ${extDir}`);
|
|
1523
|
-
return 0;
|
|
733
|
+
return await this.extensionManager.downloadExtensions(options);
|
|
1524
734
|
}
|
|
1525
735
|
|
|
1526
736
|
detectCurrentIPv4() {
|
|
1527
|
-
|
|
1528
|
-
for (const values of Object.values(interfaces)) {
|
|
1529
|
-
if (!Array.isArray(values)) {
|
|
1530
|
-
continue;
|
|
1531
|
-
}
|
|
1532
|
-
for (const item of values) {
|
|
1533
|
-
if (!item || item.internal) {
|
|
1534
|
-
continue;
|
|
1535
|
-
}
|
|
1536
|
-
if (item.family === 'IPv4') {
|
|
1537
|
-
return item.address;
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
return '';
|
|
737
|
+
return this.commandOutputManager.detectCurrentIPv4();
|
|
1542
738
|
}
|
|
1543
739
|
|
|
1544
740
|
resolveMcpAddHost(hostArg) {
|
|
1545
|
-
|
|
1546
|
-
return this.config.mcpDefaultHost;
|
|
1547
|
-
}
|
|
1548
|
-
const value = String(hostArg).trim();
|
|
1549
|
-
if (!value) {
|
|
1550
|
-
return '';
|
|
1551
|
-
}
|
|
1552
|
-
if (value === 'current-ip') {
|
|
1553
|
-
return this.detectCurrentIPv4();
|
|
1554
|
-
}
|
|
1555
|
-
return value;
|
|
741
|
+
return this.commandOutputManager.resolveMcpAddHost(hostArg);
|
|
1556
742
|
}
|
|
1557
743
|
|
|
1558
744
|
printMcpAdd(hostArg) {
|
|
1559
|
-
|
|
1560
|
-
if (!host) {
|
|
1561
|
-
this.writeStderr('[mcp-add] failed: cannot determine host. Use --host <host> to set one explicitly.');
|
|
1562
|
-
return 1;
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
const scenes = this.resolveTargets('all').filter(sceneName => isMcpScene(sceneName));
|
|
1566
|
-
for (const sceneName of scenes) {
|
|
1567
|
-
const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
|
|
1568
|
-
this.writeStdout(`claude mcp add -t http -s user playwright-${sceneName} ${url}`);
|
|
1569
|
-
}
|
|
1570
|
-
this.writeStdout('');
|
|
1571
|
-
for (const sceneName of scenes) {
|
|
1572
|
-
const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
|
|
1573
|
-
this.writeStdout(`codex mcp add playwright-${sceneName} --url ${url}`);
|
|
1574
|
-
}
|
|
1575
|
-
this.writeStdout('');
|
|
1576
|
-
for (const sceneName of scenes) {
|
|
1577
|
-
const url = `http://${host}:${this.scenePort(sceneName)}/mcp`;
|
|
1578
|
-
this.writeStdout(`gemini mcp add -t http -s user playwright-${sceneName} ${url}`);
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
return 0;
|
|
745
|
+
return this.commandOutputManager.printMcpAdd(hostArg);
|
|
1582
746
|
}
|
|
1583
747
|
|
|
1584
748
|
printCliAdd() {
|
|
1585
|
-
|
|
1586
|
-
'PLAYWRIGHT_CLI_INSTALL_DIR="${TMPDIR:-/tmp}/manyoyo-playwright-cli-install-$$"',
|
|
1587
|
-
'mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"',
|
|
1588
|
-
'echo \'{"browser":{"browserName":"chromium","launchOptions":{"channel":"chromium"}}}\' > "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright/cli.config.json"',
|
|
1589
|
-
'cd "$PLAYWRIGHT_CLI_INSTALL_DIR"',
|
|
1590
|
-
`npm install -g @playwright/cli@${PLAYWRIGHT_CLI_VERSION}`,
|
|
1591
|
-
'playwright-cli install --skills',
|
|
1592
|
-
'PLAYWRIGHT_CLI_SKILL_SOURCE="$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli"',
|
|
1593
|
-
'for target in ~/.claude/skills/playwright-cli ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli; do',
|
|
1594
|
-
' mkdir -p "$target"',
|
|
1595
|
-
' cp -R "$PLAYWRIGHT_CLI_SKILL_SOURCE/." "$target/"',
|
|
1596
|
-
'done',
|
|
1597
|
-
'cd "$OLDPWD"',
|
|
1598
|
-
'rm -rf "$PLAYWRIGHT_CLI_INSTALL_DIR"'
|
|
1599
|
-
];
|
|
1600
|
-
this.writeStdout(lines.join('\n'));
|
|
1601
|
-
return 0;
|
|
749
|
+
return this.commandOutputManager.printCliAdd();
|
|
1602
750
|
}
|
|
1603
751
|
|
|
1604
752
|
printSummary() {
|
|
1605
|
-
|
|
1606
|
-
this.writeStdout(`playwright\truntime=${this.config.runtime}\tscenes=${scenes.join(',')}`);
|
|
1607
|
-
return 0;
|
|
753
|
+
return this.commandOutputManager.printSummary();
|
|
1608
754
|
}
|
|
1609
755
|
|
|
1610
756
|
async runOnScene(action, sceneName, options = {}) {
|
|
1611
757
|
const def = SCENE_DEFS[sceneName];
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
return def.type === 'container'
|
|
1619
|
-
? this.stopContainer(sceneName)
|
|
1620
|
-
: await this.stopHost(sceneName);
|
|
1621
|
-
}
|
|
1622
|
-
if (action === 'status') {
|
|
1623
|
-
return def.type === 'container'
|
|
1624
|
-
? this.statusContainer(sceneName)
|
|
1625
|
-
: await this.statusHost(sceneName);
|
|
1626
|
-
}
|
|
1627
|
-
if (action === 'health') {
|
|
1628
|
-
return await this.healthScene(sceneName);
|
|
1629
|
-
}
|
|
1630
|
-
if (action === 'logs') {
|
|
1631
|
-
return def.type === 'container'
|
|
1632
|
-
? this.logsContainer(sceneName)
|
|
1633
|
-
: this.logsHost(sceneName);
|
|
758
|
+
const handler = action === 'health'
|
|
759
|
+
? () => this.healthScene(sceneName)
|
|
760
|
+
: this.sceneDrivers[def.type] && this.sceneDrivers[def.type][action];
|
|
761
|
+
if (!handler) {
|
|
762
|
+
this.writeStderr(`unknown action: ${action}`);
|
|
763
|
+
return 1;
|
|
1634
764
|
}
|
|
1635
|
-
|
|
1636
|
-
return 1;
|
|
765
|
+
return await handler(sceneName, options);
|
|
1637
766
|
}
|
|
1638
767
|
|
|
1639
|
-
async
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
if (action === 'cli-add') {
|
|
1649
|
-
return this.printCliAdd();
|
|
768
|
+
async runOnTargets(action, targets, options = {}) {
|
|
769
|
+
let rc = 0;
|
|
770
|
+
for (const sceneName of targets) {
|
|
771
|
+
// eslint-disable-next-line no-await-in-loop
|
|
772
|
+
const code = await this.runOnScene(action, sceneName, options);
|
|
773
|
+
if (code !== 0) {
|
|
774
|
+
rc = 1;
|
|
775
|
+
}
|
|
1650
776
|
}
|
|
777
|
+
return rc;
|
|
778
|
+
}
|
|
1651
779
|
|
|
1652
|
-
|
|
1653
|
-
|
|
780
|
+
async run({ action, scene = 'mcp-host-headless', host = '', extensionPaths = [], extensionNames = [], prodversion = '' }) {
|
|
781
|
+
const directHandler = DIRECT_ACTION_HANDLERS[action];
|
|
782
|
+
if (directHandler) {
|
|
783
|
+
return await directHandler(this, { host, prodversion });
|
|
1654
784
|
}
|
|
1655
785
|
|
|
1656
786
|
if (!VALID_ACTIONS.has(action)) {
|
|
@@ -1666,17 +796,7 @@ class PlaywrightPlugin {
|
|
|
1666
796
|
const resolvedExtensionPaths = action === 'up'
|
|
1667
797
|
? this.resolveExtensionInputs({ extensionPaths, extensionNames })
|
|
1668
798
|
: [];
|
|
1669
|
-
|
|
1670
|
-
let rc = 0;
|
|
1671
|
-
for (const sceneName of targets) {
|
|
1672
|
-
// eslint-disable-next-line no-await-in-loop
|
|
1673
|
-
const code = await this.runOnScene(action, sceneName, { extensionPaths: resolvedExtensionPaths });
|
|
1674
|
-
if (code !== 0) {
|
|
1675
|
-
rc = 1;
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
return rc;
|
|
799
|
+
return await this.runOnTargets(action, targets, { extensionPaths: resolvedExtensionPaths });
|
|
1680
800
|
}
|
|
1681
801
|
}
|
|
1682
802
|
|