@xcanwin/manyoyo 5.8.5 → 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/README.md +1 -0
- package/bin/manyoyo.js +265 -174
- 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
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function createPlaywrightSceneDrivers(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 tailText = options.tailText || (() => '');
|
|
12
|
+
const sleep = options.sleep || (async () => {});
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
container: {
|
|
16
|
+
up: async (sceneName, actionOptions = {}) => {
|
|
17
|
+
const runtime = plugin.ensureContainerRuntimeAvailable('up', sceneName);
|
|
18
|
+
if (!runtime) {
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
plugin.ensureContainerScenePrerequisites(sceneName);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return error.returncode || 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const incomingExtensionPaths = asStringArray(actionOptions.extensionPaths, []);
|
|
29
|
+
const hostInitScriptPath = plugin.sceneInitScriptPath(sceneName);
|
|
30
|
+
const containerInitScriptPath = path.posix.join('/app/config', path.basename(hostInitScriptPath));
|
|
31
|
+
let configOptions = {
|
|
32
|
+
...actionOptions,
|
|
33
|
+
extensionPaths: incomingExtensionPaths,
|
|
34
|
+
initScript: [containerInitScriptPath]
|
|
35
|
+
};
|
|
36
|
+
const composeFiles = [plugin.containerComposePath(sceneName)];
|
|
37
|
+
const volumeMounts = [`${hostInitScriptPath}:${containerInitScriptPath}:ro`];
|
|
38
|
+
|
|
39
|
+
if (incomingExtensionPaths.length > 0) {
|
|
40
|
+
const mapped = plugin.buildContainerExtensionMounts(incomingExtensionPaths);
|
|
41
|
+
volumeMounts.push(...mapped.volumeMounts);
|
|
42
|
+
configOptions = {
|
|
43
|
+
...actionOptions,
|
|
44
|
+
extensionPaths: mapped.containerPaths,
|
|
45
|
+
initScript: [containerInitScriptPath]
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const cfgPath = plugin.ensureSceneConfig(sceneName, configOptions);
|
|
49
|
+
const overridePath = plugin.ensureContainerComposeOverride(sceneName, volumeMounts);
|
|
50
|
+
if (overridePath) {
|
|
51
|
+
composeFiles.push(overridePath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const env = plugin.containerEnv(sceneName, cfgPath, { requireVncPassword: true });
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const args = plugin.buildContainerComposeCommand(sceneName, composeFiles, ['up', '-d']);
|
|
58
|
+
plugin.runCmd(args, { env, check: true });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
return error.returncode || 1;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const port = plugin.scenePort(sceneName);
|
|
64
|
+
if (await plugin.waitForPort(port)) {
|
|
65
|
+
plugin.writeStdout(`[up] ${sceneName} ready on 127.0.0.1:${port}`);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
plugin.writeStderr(`[up] ${sceneName} did not become ready on 127.0.0.1:${port}`);
|
|
70
|
+
return 1;
|
|
71
|
+
},
|
|
72
|
+
down: (sceneName) => {
|
|
73
|
+
if (!plugin.ensureContainerRuntimeAvailable('down', sceneName)) {
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
plugin.ensureContainerComposeOverride(sceneName, []);
|
|
78
|
+
const cfgPath = plugin.sceneConfigPath(sceneName);
|
|
79
|
+
const env = plugin.containerEnv(sceneName, cfgPath);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
plugin.runCmd(plugin.buildContainerComposeCommand(sceneName, [], ['down']), { env, check: true });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return error.returncode || 1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
plugin.writeStdout(`[down] ${sceneName}`);
|
|
88
|
+
return 0;
|
|
89
|
+
},
|
|
90
|
+
status: (sceneName) => {
|
|
91
|
+
const runtime = plugin.ensureContainerRuntimeAvailable('status', sceneName);
|
|
92
|
+
if (!runtime) {
|
|
93
|
+
return 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const def = sceneDefs[sceneName];
|
|
97
|
+
const cp = plugin.runCmd([
|
|
98
|
+
runtime,
|
|
99
|
+
'ps',
|
|
100
|
+
'--filter',
|
|
101
|
+
`name=${def.containerName}`,
|
|
102
|
+
'--format',
|
|
103
|
+
'{{.Names}}'
|
|
104
|
+
], { captureOutput: true, check: false });
|
|
105
|
+
|
|
106
|
+
const names = new Set(
|
|
107
|
+
cp.stdout
|
|
108
|
+
.split(/\r?\n/)
|
|
109
|
+
.map(line => line.trim())
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (names.has(def.containerName)) {
|
|
114
|
+
plugin.writeStdout(`[status] ${sceneName} running`);
|
|
115
|
+
} else {
|
|
116
|
+
plugin.writeStdout(`[status] ${sceneName} stopped`);
|
|
117
|
+
}
|
|
118
|
+
return 0;
|
|
119
|
+
},
|
|
120
|
+
logs: (sceneName) => {
|
|
121
|
+
const runtime = plugin.ensureContainerRuntimeAvailable('logs', sceneName);
|
|
122
|
+
if (!runtime) {
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const def = sceneDefs[sceneName];
|
|
127
|
+
const cp = plugin.runCmd([
|
|
128
|
+
runtime,
|
|
129
|
+
'logs',
|
|
130
|
+
'--tail',
|
|
131
|
+
'80',
|
|
132
|
+
def.containerName
|
|
133
|
+
], { captureOutput: true, check: false });
|
|
134
|
+
|
|
135
|
+
const output = cp.stdout || cp.stderr;
|
|
136
|
+
if (output.trim()) {
|
|
137
|
+
plugin.writeStdout(output.trimEnd());
|
|
138
|
+
} else {
|
|
139
|
+
plugin.writeStdout(`[logs] ${sceneName} no logs`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return cp.returncode === 0 ? 0 : 1;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
host: {
|
|
146
|
+
up: async (sceneName, actionOptions = {}) => {
|
|
147
|
+
try {
|
|
148
|
+
plugin.ensureCliHostHeadedCacheDir(sceneName);
|
|
149
|
+
plugin.ensureHostScenePrerequisites(sceneName);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
plugin.writeStderr(`[up] ${sceneName} failed: ${error.message || String(error)}`);
|
|
152
|
+
return error.returncode || 1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fs.mkdirSync(plugin.config.runDir, { recursive: true });
|
|
156
|
+
const cfgPath = plugin.ensureSceneConfig(sceneName, actionOptions);
|
|
157
|
+
const pidFile = plugin.scenePidFile(sceneName);
|
|
158
|
+
const logFile = plugin.sceneLogFile(sceneName);
|
|
159
|
+
plugin.clearHostSceneRuntimeState(sceneName);
|
|
160
|
+
|
|
161
|
+
let { port, managedPids, portReachable } = await plugin.getHostSceneRuntimeInfo(sceneName);
|
|
162
|
+
if (managedPids.length > 0 && portReachable) {
|
|
163
|
+
plugin.writeStdout(`[up] ${sceneName} already running (pid(s) ${managedPids.join(' ')})`);
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (portReachable) {
|
|
168
|
+
plugin.writeStderr(`[up] ${sceneName} failed: port ${port} is already in use by another process.`);
|
|
169
|
+
plugin.writeStderr('Stop the conflicting process first, then retry.');
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fs.rmSync(pidFile, { force: true });
|
|
174
|
+
const logFd = fs.openSync(logFile, 'a');
|
|
175
|
+
let launchCommand = null;
|
|
176
|
+
try {
|
|
177
|
+
launchCommand = plugin.hostLaunchCommand(sceneName, cfgPath);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
fs.closeSync(logFd);
|
|
180
|
+
plugin.writeStderr(`[up] ${sceneName} failed: ${error.message || String(error)}`);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const starter = plugin.spawnHostProcess(launchCommand.command, launchCommand.args, logFd);
|
|
185
|
+
fs.closeSync(logFd);
|
|
186
|
+
if (typeof starter.unref === 'function') {
|
|
187
|
+
starter.unref();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (await plugin.waitForPort(port)) {
|
|
191
|
+
if (isCliScene(sceneName)) {
|
|
192
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
193
|
+
plugin.writeSceneEndpoint(sceneName, {
|
|
194
|
+
port,
|
|
195
|
+
wsPath: String(cfg.wsPath || '')
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
managedPids = await plugin.waitForHostPids(sceneName, starter.pid);
|
|
199
|
+
if (managedPids.length > 0) {
|
|
200
|
+
plugin.writeScenePidFile(pidFile, managedPids[0]);
|
|
201
|
+
plugin.writeStdout(`[up] ${sceneName} ready on 127.0.0.1:${port} (pid(s) ${managedPids.join(' ')})`);
|
|
202
|
+
plugin.remindCliSessionScene(sceneName);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
plugin.writeStderr(`[up] ${sceneName} failed to start. tail ${logFile}:`);
|
|
208
|
+
const tail = tailText(logFile, 30);
|
|
209
|
+
if (tail) {
|
|
210
|
+
plugin.writeStderr(tail);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (starter.exitCode === null && !starter.killed) {
|
|
214
|
+
plugin.stopHostStarter(starter.pid);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return 1;
|
|
218
|
+
},
|
|
219
|
+
down: async (sceneName) => {
|
|
220
|
+
const { pidFile, port, managedPids } = await plugin.getHostSceneRuntimeInfo(sceneName);
|
|
221
|
+
plugin.clearHostSceneRuntimeState(sceneName);
|
|
222
|
+
plugin.signalPids(managedPids);
|
|
223
|
+
|
|
224
|
+
if (managedPids.length > 0) {
|
|
225
|
+
await sleep(300);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const pidFromFile = plugin.readPidFilePid(pidFile);
|
|
229
|
+
if (pidFromFile > 0) {
|
|
230
|
+
plugin.signalPids([pidFromFile]);
|
|
231
|
+
fs.rmSync(pidFile, { force: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (await plugin.portReady(port)) {
|
|
235
|
+
plugin.writeStderr(`[down] ${sceneName} warning: port ${port} is still in use (possibly unmanaged process)`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
plugin.writeStdout(`[down] ${sceneName}`);
|
|
240
|
+
return 0;
|
|
241
|
+
},
|
|
242
|
+
status: async (sceneName) => {
|
|
243
|
+
const { pidFile, port, managedPids, portReachable } = await plugin.getHostSceneRuntimeInfo(sceneName);
|
|
244
|
+
|
|
245
|
+
if (managedPids.length > 0 && portReachable) {
|
|
246
|
+
plugin.writeStdout(`[status] ${sceneName} running (pid(s) ${managedPids.join(' ')})`);
|
|
247
|
+
if (plugin.readPidFilePid(pidFile) <= 0) {
|
|
248
|
+
plugin.writeScenePidFile(pidFile, managedPids[0]);
|
|
249
|
+
}
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (managedPids.length > 0 && !portReachable) {
|
|
254
|
+
plugin.writeStdout(`[status] ${sceneName} degraded (pid(s) ${managedPids.join(' ')}, port ${port} not reachable)`);
|
|
255
|
+
return 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fs.rmSync(pidFile, { force: true });
|
|
259
|
+
if (portReachable) {
|
|
260
|
+
plugin.writeStdout(`[status] ${sceneName} conflict (port ${port} in use by unmanaged process)`);
|
|
261
|
+
} else {
|
|
262
|
+
plugin.writeStdout(`[status] ${sceneName} stopped`);
|
|
263
|
+
}
|
|
264
|
+
return 0;
|
|
265
|
+
},
|
|
266
|
+
logs: (sceneName) => {
|
|
267
|
+
const logFile = plugin.sceneLogFile(sceneName);
|
|
268
|
+
if (!fs.existsSync(logFile)) {
|
|
269
|
+
plugin.writeStdout(`[logs] ${sceneName} no log file: ${logFile}`);
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const tail = tailText(logFile, 80);
|
|
274
|
+
if (tail) {
|
|
275
|
+
plugin.writeStdout(tail);
|
|
276
|
+
}
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
createPlaywrightSceneDrivers
|
|
285
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function createPlaywrightSceneStateManager(options = {}) {
|
|
7
|
+
const plugin = options.plugin;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
sceneEndpointPath(sceneName) {
|
|
11
|
+
return path.join(plugin.config.runDir, `${sceneName}.endpoint.json`);
|
|
12
|
+
},
|
|
13
|
+
readSceneEndpoint(sceneName) {
|
|
14
|
+
const filePath = this.sceneEndpointPath(sceneName);
|
|
15
|
+
if (!fs.existsSync(filePath)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
writeSceneEndpoint(sceneName, payload) {
|
|
25
|
+
fs.mkdirSync(plugin.config.runDir, { recursive: true });
|
|
26
|
+
fs.writeFileSync(this.sceneEndpointPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
27
|
+
},
|
|
28
|
+
removeSceneEndpoint(sceneName) {
|
|
29
|
+
fs.rmSync(this.sceneEndpointPath(sceneName), { force: true });
|
|
30
|
+
},
|
|
31
|
+
sceneCliAttachConfigPath(sceneName) {
|
|
32
|
+
return path.join(plugin.config.runDir, `${sceneName}.cli-attach.json`);
|
|
33
|
+
},
|
|
34
|
+
writeSceneCliAttachConfig(sceneName, payload) {
|
|
35
|
+
fs.mkdirSync(plugin.config.runDir, { recursive: true });
|
|
36
|
+
fs.writeFileSync(this.sceneCliAttachConfigPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
|
|
37
|
+
},
|
|
38
|
+
removeSceneCliAttachConfig(sceneName) {
|
|
39
|
+
fs.rmSync(this.sceneCliAttachConfigPath(sceneName), { force: true });
|
|
40
|
+
},
|
|
41
|
+
clearHostSceneRuntimeState(sceneName) {
|
|
42
|
+
this.removeSceneEndpoint(sceneName);
|
|
43
|
+
this.removeSceneCliAttachConfig(sceneName);
|
|
44
|
+
},
|
|
45
|
+
buildCliSessionIntegration(dockerCmd) {
|
|
46
|
+
const sceneName = plugin.config.cliSessionScene;
|
|
47
|
+
if (!sceneName) {
|
|
48
|
+
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const endpoint = this.readSceneEndpoint(sceneName);
|
|
52
|
+
if (!endpoint || !Number.isInteger(endpoint.port) || endpoint.port <= 0 || typeof endpoint.wsPath !== 'string' || !endpoint.wsPath) {
|
|
53
|
+
return { envEntries: [], extraArgs: [], volumeEntries: [] };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const normalizedDockerCmd = String(dockerCmd || '').trim().toLowerCase();
|
|
57
|
+
const connectHost = normalizedDockerCmd === 'podman' ? 'host.containers.internal' : 'host.docker.internal';
|
|
58
|
+
const remoteEndpoint = `ws://${connectHost}:${endpoint.port}${endpoint.wsPath}`;
|
|
59
|
+
const hostConfigPath = this.sceneCliAttachConfigPath(sceneName);
|
|
60
|
+
const containerConfigPath = `/tmp/manyoyo-playwright/${sceneName}.cli-attach.json`;
|
|
61
|
+
this.writeSceneCliAttachConfig(sceneName, {
|
|
62
|
+
browser: {
|
|
63
|
+
remoteEndpoint
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const envEntries = [
|
|
67
|
+
`PLAYWRIGHT_MCP_CONFIG=${containerConfigPath}`
|
|
68
|
+
];
|
|
69
|
+
const extraArgs = normalizedDockerCmd === 'docker'
|
|
70
|
+
? ['--add-host', 'host.docker.internal:host-gateway']
|
|
71
|
+
: [];
|
|
72
|
+
const volumeEntries = ['--volume', `${hostConfigPath}:${containerConfigPath}:ro`];
|
|
73
|
+
return { envEntries, extraArgs, volumeEntries };
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
createPlaywrightSceneStateManager
|
|
80
|
+
};
|