@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.
@@ -0,0 +1,265 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function sleep(ms) {
9
+ return new Promise(resolve => setTimeout(resolve, ms));
10
+ }
11
+
12
+ function isHostPermission(value) {
13
+ if (value === '<all_urls>') {
14
+ return true;
15
+ }
16
+ return /^(?:\*|http|https|file|ftp):\/\//.test(value);
17
+ }
18
+
19
+ function scriptSourcesFromHtml(htmlFile) {
20
+ const content = fs.readFileSync(htmlFile, { encoding: 'utf8' });
21
+ const scripts = [...content.matchAll(/<script[^>]+src=["']([^"']+)["']/gi)].map(m => m[1]);
22
+ return scripts.filter(src => !/^(?:https?:)?\/\//.test(src));
23
+ }
24
+
25
+ function convertManifestV2ToV3(extDir) {
26
+ const manifestFile = path.join(extDir, 'manifest.json');
27
+ const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8'));
28
+ if (manifest.manifest_version !== 2) {
29
+ return false;
30
+ }
31
+
32
+ manifest.manifest_version = 3;
33
+
34
+ if (manifest.browser_action && !manifest.action) {
35
+ manifest.action = manifest.browser_action;
36
+ delete manifest.browser_action;
37
+ }
38
+ if (manifest.page_action && !manifest.action) {
39
+ manifest.action = manifest.page_action;
40
+ delete manifest.page_action;
41
+ }
42
+
43
+ const background = manifest.background;
44
+ if (background && typeof background === 'object' && !Array.isArray(background)) {
45
+ let scripts = [];
46
+ if (Array.isArray(background.scripts)) {
47
+ scripts = background.scripts.filter(s => typeof s === 'string');
48
+ } else if (typeof background.page === 'string') {
49
+ const pagePath = path.join(extDir, background.page);
50
+ if (fs.existsSync(pagePath)) {
51
+ scripts = scriptSourcesFromHtml(pagePath);
52
+ }
53
+ }
54
+
55
+ if (scripts.length > 0) {
56
+ const swName = 'generated_background_sw.js';
57
+ const swFile = path.join(extDir, swName);
58
+ const swLines = [
59
+ '// Auto-generated by manyoyo playwright ext-download for MV3.',
60
+ `importScripts(${scripts.map(s => JSON.stringify(s)).join(', ')});`,
61
+ ''
62
+ ];
63
+ fs.writeFileSync(swFile, swLines.join('\n'), 'utf8');
64
+ manifest.background = { service_worker: swName };
65
+ } else {
66
+ delete manifest.background;
67
+ }
68
+ }
69
+
70
+ if (typeof manifest.content_security_policy === 'string') {
71
+ manifest.content_security_policy = { extension_pages: manifest.content_security_policy };
72
+ }
73
+
74
+ if (Array.isArray(manifest.permissions)) {
75
+ const hostPermissions = Array.isArray(manifest.host_permissions) ? [...manifest.host_permissions] : [];
76
+ const keptPermissions = [];
77
+
78
+ for (const perm of manifest.permissions) {
79
+ if (typeof perm === 'string' && isHostPermission(perm)) {
80
+ if (!hostPermissions.includes(perm)) {
81
+ hostPermissions.push(perm);
82
+ }
83
+ } else {
84
+ keptPermissions.push(perm);
85
+ }
86
+ }
87
+
88
+ manifest.permissions = keptPermissions;
89
+ if (hostPermissions.length > 0) {
90
+ manifest.host_permissions = hostPermissions;
91
+ }
92
+ }
93
+
94
+ const war = manifest.web_accessible_resources;
95
+ if (Array.isArray(war) && war.length > 0 && war.every(v => typeof v === 'string')) {
96
+ manifest.web_accessible_resources = [
97
+ {
98
+ resources: war,
99
+ matches: ['<all_urls>']
100
+ }
101
+ ];
102
+ }
103
+
104
+ fs.writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
105
+ return true;
106
+ }
107
+
108
+ function buildCrxUrl(extId, prodversion) {
109
+ return (
110
+ 'https://clients2.google.com/service/update2/crx' +
111
+ `?response=redirect&prodversion=${prodversion}` +
112
+ '&acceptformat=crx2,crx3' +
113
+ `&x=id%3D${extId}%26installsource%3Dondemand%26uc`
114
+ );
115
+ }
116
+
117
+ function crxZipOffset(data) {
118
+ if (data.subarray(0, 4).toString('ascii') !== 'Cr24') {
119
+ throw new Error('not a CRX file');
120
+ }
121
+
122
+ const version = data.readUInt32LE(4);
123
+ if (version === 2) {
124
+ const pubLen = data.readUInt32LE(8);
125
+ const sigLen = data.readUInt32LE(12);
126
+ return 16 + pubLen + sigLen;
127
+ }
128
+ if (version === 3) {
129
+ const headerLen = data.readUInt32LE(8);
130
+ return 12 + headerLen;
131
+ }
132
+ throw new Error(`unsupported CRX version: ${version}`);
133
+ }
134
+
135
+ class PlaywrightExtensionManager {
136
+ constructor(options = {}) {
137
+ this.extensions = Array.isArray(options.extensions) ? options.extensions : [];
138
+ this.ensureCommandAvailable = options.ensureCommandAvailable;
139
+ this.runCmd = options.runCmd;
140
+ this.writeStdout = options.writeStdout;
141
+ this.writeStderr = options.writeStderr;
142
+ this.extensionDirPath = options.extensionDirPath;
143
+ this.extensionTmpDirPath = options.extensionTmpDirPath;
144
+ this.defaultProdversion = String(options.defaultProdversion || '132.0.0.0').trim();
145
+ }
146
+
147
+ async downloadFile(url, output, retries = 3, timeoutMs = 60_000) {
148
+ const timeoutSec = Math.max(1, Math.ceil(timeoutMs / 1000));
149
+ if (!this.ensureCommandAvailable('curl')) {
150
+ throw new Error('curl command not found');
151
+ }
152
+
153
+ let lastError = null;
154
+ for (let i = 1; i <= retries; i += 1) {
155
+ try {
156
+ const result = this.runCmd([
157
+ 'curl',
158
+ '--fail',
159
+ '--location',
160
+ '--silent',
161
+ '--show-error',
162
+ '--connect-timeout',
163
+ String(timeoutSec),
164
+ '--max-time',
165
+ String(timeoutSec),
166
+ '--output',
167
+ output,
168
+ url
169
+ ], { captureOutput: true, check: false });
170
+ if (result.returncode !== 0) {
171
+ throw new Error(result.stderr || `curl failed with exit code ${result.returncode}`);
172
+ }
173
+ return;
174
+ } catch (error) {
175
+ lastError = error;
176
+ if (i < retries) {
177
+ // eslint-disable-next-line no-await-in-loop
178
+ await sleep(1000);
179
+ }
180
+ }
181
+ }
182
+
183
+ throw new Error(`download failed after ${retries} attempts: ${url}; ${String(lastError)}`);
184
+ }
185
+
186
+ extractZipBuffer(zipBuffer, outDir) {
187
+ fs.mkdirSync(outDir, { recursive: true });
188
+ const tempZip = path.join(os.tmpdir(), `manyoyo-playwright-ext-${process.pid}-${Date.now()}.zip`);
189
+ fs.writeFileSync(tempZip, zipBuffer);
190
+
191
+ const result = spawnSync('unzip', ['-oq', tempZip, '-d', outDir], { encoding: 'utf8' });
192
+ fs.rmSync(tempZip, { force: true });
193
+
194
+ if (result.error) {
195
+ throw result.error;
196
+ }
197
+ if (result.status !== 0) {
198
+ throw new Error(result.stderr || `unzip failed with exit code ${result.status}`);
199
+ }
200
+ }
201
+
202
+ extractCrx(crxFile, outDir) {
203
+ const data = fs.readFileSync(crxFile);
204
+ const offset = crxZipOffset(data);
205
+ const zipBuffer = data.subarray(offset);
206
+
207
+ this.extractZipBuffer(zipBuffer, outDir);
208
+
209
+ const manifest = path.join(outDir, 'manifest.json');
210
+ if (!fs.existsSync(manifest)) {
211
+ throw new Error(`${crxFile} extracted but manifest.json missing`);
212
+ }
213
+
214
+ if (convertManifestV2ToV3(outDir)) {
215
+ this.writeStdout(`[manifest] upgraded to MV3: ${path.basename(outDir)}`);
216
+ }
217
+ }
218
+
219
+ async downloadExtensions(options = {}) {
220
+ if (!this.ensureCommandAvailable('unzip')) {
221
+ this.writeStderr('[ext-download] failed: unzip command not found.');
222
+ return 1;
223
+ }
224
+
225
+ const prodversion = String(options.prodversion || this.defaultProdversion).trim();
226
+ const extDir = path.resolve(this.extensionDirPath());
227
+ const tmpDir = path.resolve(this.extensionTmpDirPath());
228
+
229
+ fs.rmSync(tmpDir, { recursive: true, force: true });
230
+ fs.mkdirSync(extDir, { recursive: true });
231
+ fs.mkdirSync(tmpDir, { recursive: true });
232
+
233
+ try {
234
+ this.writeStdout(`[info] ext dir: ${extDir}`);
235
+ this.writeStdout(`[info] tmp dir: ${tmpDir}`);
236
+
237
+ for (const [name, extId] of this.extensions) {
238
+ const url = buildCrxUrl(extId, prodversion);
239
+ const crxFile = path.join(tmpDir, `${name}.crx`);
240
+ const outDir = path.join(extDir, name);
241
+
242
+ this.writeStdout(`[download] ${name}`);
243
+ // eslint-disable-next-line no-await-in-loop
244
+ await this.downloadFile(url, crxFile);
245
+
246
+ this.writeStdout(`[extract] ${name}`);
247
+ fs.rmSync(outDir, { recursive: true, force: true });
248
+ this.extractCrx(crxFile, outDir);
249
+ }
250
+ } finally {
251
+ fs.rmSync(tmpDir, { recursive: true, force: true });
252
+ this.writeStdout(`[cleanup] removed ${tmpDir}`);
253
+ }
254
+
255
+ this.writeStdout(`[done] all extensions are ready: ${extDir}`);
256
+ return 0;
257
+ }
258
+ }
259
+
260
+ module.exports = {
261
+ buildCrxUrl,
262
+ convertManifestV2ToV3,
263
+ crxZipOffset,
264
+ PlaywrightExtensionManager
265
+ };
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function createPlaywrightExtensionPathManager(options = {}) {
7
+ const plugin = options.plugin;
8
+ const asStringArray = options.asStringArray || ((value, fallback) => fallback);
9
+ const containerExtensionRoot = options.containerExtensionRoot || '/app/extensions';
10
+
11
+ return {
12
+ resolveExtensionPaths(extensionArgs = []) {
13
+ const inputs = asStringArray(extensionArgs, []);
14
+ const uniquePaths = [];
15
+ const seen = new Set();
16
+
17
+ for (const item of inputs) {
18
+ const absPath = path.resolve(item);
19
+ if (!fs.existsSync(absPath)) {
20
+ throw new Error(`扩展路径不存在: ${absPath}`);
21
+ }
22
+ const stat = fs.statSync(absPath);
23
+ if (!stat.isDirectory()) {
24
+ throw new Error(`扩展路径必须是目录: ${absPath}`);
25
+ }
26
+
27
+ const manifestPath = path.join(absPath, 'manifest.json');
28
+ if (fs.existsSync(manifestPath)) {
29
+ if (!seen.has(absPath)) {
30
+ seen.add(absPath);
31
+ uniquePaths.push(absPath);
32
+ }
33
+ continue;
34
+ }
35
+
36
+ const children = fs.readdirSync(absPath, { withFileTypes: true })
37
+ .filter(dirent => dirent.isDirectory())
38
+ .map(dirent => path.join(absPath, dirent.name))
39
+ .filter(child => fs.existsSync(path.join(child, 'manifest.json')));
40
+
41
+ if (children.length === 0) {
42
+ throw new Error(`目录下未找到扩展(manifest.json): ${absPath}`);
43
+ }
44
+
45
+ for (const childPath of children) {
46
+ if (!seen.has(childPath)) {
47
+ seen.add(childPath);
48
+ uniquePaths.push(childPath);
49
+ }
50
+ }
51
+ }
52
+
53
+ return uniquePaths;
54
+ },
55
+ resolveNamedExtensionPaths(extensionNames = []) {
56
+ const names = asStringArray(extensionNames, []);
57
+ const extensionRoot = path.resolve(plugin.extensionDirPath());
58
+
59
+ return names.map(name => {
60
+ if (name.includes('/') || name.includes('\\') || name === '.' || name === '..') {
61
+ throw new Error(`扩展名称无效: ${name}`);
62
+ }
63
+ return path.join(extensionRoot, name);
64
+ });
65
+ },
66
+ resolveExtensionInputs(inputOptions = {}) {
67
+ const extensionPaths = asStringArray(inputOptions.extensionPaths, []);
68
+ const namedPaths = this.resolveNamedExtensionPaths(inputOptions.extensionNames || []);
69
+ return this.resolveExtensionPaths([...extensionPaths, ...namedPaths]);
70
+ },
71
+ sanitizeExtensionMountName(value) {
72
+ const sanitized = String(value || '')
73
+ .trim()
74
+ .replace(/[^A-Za-z0-9._-]/g, '-')
75
+ .replace(/-+/g, '-')
76
+ .replace(/^-|-$/g, '');
77
+ return sanitized || 'ext';
78
+ },
79
+ buildContainerExtensionMounts(extensionPaths = []) {
80
+ const hostPaths = asStringArray(extensionPaths, []);
81
+ const containerPaths = [];
82
+ const volumeMounts = [];
83
+
84
+ hostPaths.forEach((hostPath, idx) => {
85
+ const safeName = this.sanitizeExtensionMountName(path.basename(hostPath));
86
+ const containerPath = path.posix.join(containerExtensionRoot, `ext-${idx + 1}-${safeName}`);
87
+ containerPaths.push(containerPath);
88
+ volumeMounts.push(`${hostPath}:${containerPath}:ro`);
89
+ });
90
+
91
+ return { containerPaths, volumeMounts };
92
+ }
93
+ };
94
+ }
95
+
96
+ module.exports = {
97
+ createPlaywrightExtensionPathManager
98
+ };
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { spawn } = require('child_process');
6
+
7
+ function createPlaywrightHostRuntimeManager(options = {}) {
8
+ const plugin = options.plugin;
9
+ const isCliScene = options.isCliScene || (() => false);
10
+ const sleep = options.sleep || (async () => {});
11
+
12
+ return {
13
+ hostLaunchCommand(sceneName, cfgPath) {
14
+ if (isCliScene(sceneName)) {
15
+ return {
16
+ command: plugin.playwrightBinPath(sceneName),
17
+ args: ['launch-server', '--browser', plugin.defaultBrowserName(sceneName), '--config', String(cfgPath)]
18
+ };
19
+ }
20
+ return {
21
+ command: plugin.localBinPath('playwright-mcp'),
22
+ args: ['--config', String(cfgPath)]
23
+ };
24
+ },
25
+ spawnHostProcess(command, args, logFd) {
26
+ return spawn(command, args, {
27
+ detached: true,
28
+ stdio: ['ignore', logFd, logFd]
29
+ });
30
+ },
31
+ stopHostStarter(pid) {
32
+ if (!Number.isInteger(pid) || pid <= 0) {
33
+ return;
34
+ }
35
+ try {
36
+ process.kill(-pid, 'SIGTERM');
37
+ return;
38
+ } catch {
39
+ // no-op
40
+ }
41
+ try {
42
+ process.kill(pid, 'SIGTERM');
43
+ } catch {
44
+ // no-op
45
+ }
46
+ },
47
+ hostScenePids(sceneName) {
48
+ const cfgPath = plugin.sceneConfigPath(sceneName);
49
+ const pattern = isCliScene(sceneName)
50
+ ? `playwright.*launch-server.*--config ${cfgPath}`
51
+ : `playwright-mcp.*--config ${cfgPath}`;
52
+ const cp = plugin.runCmd(['pgrep', '-f', pattern], { captureOutput: true, check: false });
53
+
54
+ if (cp.returncode !== 0 || !cp.stdout.trim()) {
55
+ return [];
56
+ }
57
+
58
+ const pids = [];
59
+ for (const line of cp.stdout.split(/\r?\n/)) {
60
+ const text = line.trim();
61
+ if (/^\d+$/.test(text)) {
62
+ pids.push(Number(text));
63
+ }
64
+ }
65
+ return pids;
66
+ },
67
+ async waitForHostPids(sceneName, fallbackPid) {
68
+ for (let i = 0; i < 5; i += 1) {
69
+ const pids = this.hostScenePids(sceneName);
70
+ if (pids.length > 0) {
71
+ return pids;
72
+ }
73
+ // eslint-disable-next-line no-await-in-loop
74
+ await sleep(100);
75
+ }
76
+ if (Number.isInteger(fallbackPid) && fallbackPid > 0) {
77
+ return [fallbackPid];
78
+ }
79
+ return [];
80
+ },
81
+ async getHostSceneRuntimeInfo(sceneName) {
82
+ const pidFile = plugin.scenePidFile(sceneName);
83
+ const port = plugin.scenePort(sceneName);
84
+ const managedPids = this.hostScenePids(sceneName);
85
+ const portReachable = await plugin.portReady(port);
86
+ return { pidFile, port, managedPids, portReachable };
87
+ },
88
+ signalPids(pids, signal = 'SIGTERM') {
89
+ const values = Array.isArray(pids) ? pids : [];
90
+ values.forEach(pid => {
91
+ try {
92
+ process.kill(pid, signal);
93
+ } catch {
94
+ // no-op
95
+ }
96
+ });
97
+ },
98
+ readPidFilePid(pidFile) {
99
+ if (!fs.existsSync(pidFile)) {
100
+ return 0;
101
+ }
102
+ const text = fs.readFileSync(pidFile, 'utf8').trim();
103
+ return /^\d+$/.test(text) ? Number(text) : 0;
104
+ },
105
+ writeScenePidFile(pidFile, pid) {
106
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
107
+ fs.writeFileSync(pidFile, `${pid}`, 'utf8');
108
+ }
109
+ };
110
+ }
111
+
112
+ module.exports = {
113
+ createPlaywrightHostRuntimeManager
114
+ };
@@ -0,0 +1,137 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const crypto = require('crypto');
5
+
6
+ function createPlaywrightSceneConfigManager(options = {}) {
7
+ const plugin = options.plugin;
8
+ const sceneDefs = options.sceneDefs || {};
9
+ const isCliScene = options.isCliScene || (() => false);
10
+ const asStringArray = options.asStringArray || ((value, fallback) => fallback);
11
+ const defaultFingerprintProfile = options.defaultFingerprintProfile || {};
12
+ const disableWebRtcLaunchArgs = options.disableWebRtcLaunchArgs || [];
13
+
14
+ return {
15
+ buildExtensionLaunchArgs(extensionPaths) {
16
+ const joined = extensionPaths.join(',');
17
+ return [
18
+ `--disable-extensions-except=${joined}`,
19
+ `--load-extension=${joined}`
20
+ ];
21
+ },
22
+ baseLaunchArgs() {
23
+ return [
24
+ `--user-agent=${defaultFingerprintProfile.userAgent}`,
25
+ `--lang=${defaultFingerprintProfile.locale}`,
26
+ `--window-size=${defaultFingerprintProfile.width},${defaultFingerprintProfile.height}`,
27
+ '--disable-blink-features=AutomationControlled',
28
+ '--force-webrtc-ip-handling-policy=disable_non_proxied_udp'
29
+ ];
30
+ },
31
+ buildSceneLaunchArgs(extensionPaths = []) {
32
+ const args = [...this.baseLaunchArgs()];
33
+ if (Array.isArray(extensionPaths) && extensionPaths.length > 0) {
34
+ args.push(...this.buildExtensionLaunchArgs(extensionPaths));
35
+ }
36
+ if (plugin.config.disableWebRTC) {
37
+ args.push(...disableWebRtcLaunchArgs);
38
+ }
39
+ return args;
40
+ },
41
+ buildMcpSceneConfig(sceneName, actionOptions = {}) {
42
+ const def = sceneDefs[sceneName];
43
+ const port = plugin.scenePort(sceneName);
44
+ const extensionPaths = asStringArray(actionOptions.extensionPaths, []);
45
+ const initScript = asStringArray(actionOptions.initScript, []);
46
+ const launchOptions = {
47
+ channel: 'chromium',
48
+ headless: def.headless,
49
+ args: this.buildSceneLaunchArgs(extensionPaths)
50
+ };
51
+
52
+ const contextOptions = {
53
+ userAgent: defaultFingerprintProfile.userAgent,
54
+ locale: defaultFingerprintProfile.locale,
55
+ timezoneId: defaultFingerprintProfile.timezoneId,
56
+ extraHTTPHeaders: {
57
+ 'Accept-Language': defaultFingerprintProfile.acceptLanguage
58
+ }
59
+ };
60
+ if (sceneName !== 'mcp-host-headed') {
61
+ contextOptions.viewport = {
62
+ width: defaultFingerprintProfile.width,
63
+ height: defaultFingerprintProfile.height
64
+ };
65
+ contextOptions.screen = {
66
+ width: defaultFingerprintProfile.width,
67
+ height: defaultFingerprintProfile.height
68
+ };
69
+ }
70
+
71
+ return {
72
+ outputDir: '/tmp/.playwright-mcp',
73
+ server: {
74
+ host: def.listenHost,
75
+ port,
76
+ allowedHosts: [
77
+ `localhost:${port}`,
78
+ `127.0.0.1:${port}`,
79
+ `host.docker.internal:${port}`,
80
+ `host.containers.internal:${port}`
81
+ ]
82
+ },
83
+ browser: {
84
+ chromiumSandbox: true,
85
+ browserName: 'chromium',
86
+ initScript,
87
+ launchOptions,
88
+ contextOptions
89
+ }
90
+ };
91
+ },
92
+ buildCliSceneConfig(sceneName, actionOptions = {}) {
93
+ const def = sceneDefs[sceneName];
94
+ const extensionPaths = asStringArray(actionOptions.extensionPaths, []);
95
+ return {
96
+ host: def.listenHost,
97
+ port: plugin.scenePort(sceneName),
98
+ wsPath: `/${sceneName}-${crypto.randomBytes(8).toString('hex')}`,
99
+ headless: def.headless,
100
+ channel: 'chromium',
101
+ chromiumSandbox: true,
102
+ args: this.buildSceneLaunchArgs(extensionPaths)
103
+ };
104
+ },
105
+ buildSceneConfig(sceneName, actionOptions = {}) {
106
+ if (isCliScene(sceneName)) {
107
+ return this.buildCliSceneConfig(sceneName, actionOptions);
108
+ }
109
+ return this.buildMcpSceneConfig(sceneName, actionOptions);
110
+ },
111
+ writeSceneConfigFile(sceneName, payload) {
112
+ const filePath = plugin.sceneConfigPath(sceneName);
113
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
114
+ return filePath;
115
+ },
116
+ ensureSceneConfig(sceneName, actionOptions = {}) {
117
+ fs.mkdirSync(plugin.config.configDir, { recursive: true });
118
+ let payload = null;
119
+ if (isCliScene(sceneName)) {
120
+ payload = this.buildCliSceneConfig(sceneName, actionOptions);
121
+ } else {
122
+ const initScriptPath = plugin.ensureSceneInitScript(sceneName);
123
+ const configuredInitScript = asStringArray(actionOptions.initScript, []);
124
+ const initScript = configuredInitScript.length > 0 ? configuredInitScript : [initScriptPath];
125
+ payload = this.buildSceneConfig(sceneName, {
126
+ ...actionOptions,
127
+ initScript
128
+ });
129
+ }
130
+ return this.writeSceneConfigFile(sceneName, payload);
131
+ }
132
+ };
133
+ }
134
+
135
+ module.exports = {
136
+ createPlaywrightSceneConfigManager
137
+ };