@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/init-config.js
CHANGED
|
@@ -251,6 +251,25 @@ function collectOpenCodeInitData(homeDir, ctx) {
|
|
|
251
251
|
return { keys, values, notes, volumes: dedupeList(volumes) };
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
const AGENT_INIT_SPECS = {
|
|
255
|
+
claude: {
|
|
256
|
+
yolo: 'c',
|
|
257
|
+
collect: (homeDir, ctx) => collectClaudeInitData(homeDir, ctx)
|
|
258
|
+
},
|
|
259
|
+
codex: {
|
|
260
|
+
yolo: 'cx',
|
|
261
|
+
collect: (homeDir, ctx) => collectCodexInitData(homeDir, ctx)
|
|
262
|
+
},
|
|
263
|
+
gemini: {
|
|
264
|
+
yolo: 'gm',
|
|
265
|
+
collect: (homeDir) => collectGeminiInitData(homeDir)
|
|
266
|
+
},
|
|
267
|
+
opencode: {
|
|
268
|
+
yolo: 'oc',
|
|
269
|
+
collect: (homeDir, ctx) => collectOpenCodeInitData(homeDir, ctx)
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
254
273
|
function buildInitRunEnv(keys, values) {
|
|
255
274
|
const envMap = {};
|
|
256
275
|
const missingKeys = [];
|
|
@@ -338,24 +357,17 @@ async function initAgentConfigs(rawAgents, options = {}) {
|
|
|
338
357
|
runsMap = { ...manyoyoConfig.runs };
|
|
339
358
|
}
|
|
340
359
|
|
|
341
|
-
const extractors = {
|
|
342
|
-
claude: homeDir => collectClaudeInitData(homeDir, ctx),
|
|
343
|
-
codex: homeDir => collectCodexInitData(homeDir, ctx),
|
|
344
|
-
gemini: collectGeminiInitData,
|
|
345
|
-
opencode: homeDir => collectOpenCodeInitData(homeDir, ctx)
|
|
346
|
-
};
|
|
347
|
-
const yoloMap = { claude: 'c', codex: 'cx', gemini: 'gm', opencode: 'oc' };
|
|
348
|
-
|
|
349
360
|
let hasConfigChanged = false;
|
|
350
361
|
ctx.log(`${CYAN}🧭 正在初始化 MANYOYO 配置: ${agents.join(', ')}${NC}`);
|
|
351
362
|
|
|
352
363
|
for (const agent of agents) {
|
|
353
|
-
const
|
|
364
|
+
const spec = AGENT_INIT_SPECS[agent];
|
|
365
|
+
const data = spec.collect(ctx.homeDir, ctx);
|
|
354
366
|
const shouldWriteRun = await shouldOverwriteInitRunEntry(agent, Object.prototype.hasOwnProperty.call(runsMap, agent), ctx);
|
|
355
367
|
|
|
356
368
|
let writeResult = { missingKeys: [], unsafeKeys: [] };
|
|
357
369
|
if (shouldWriteRun) {
|
|
358
|
-
const buildResult = buildInitRunProfile(agent,
|
|
370
|
+
const buildResult = buildInitRunProfile(agent, spec.yolo, data.volumes, data.keys, data.values);
|
|
359
371
|
runsMap[agent] = buildResult.runProfile;
|
|
360
372
|
writeResult = { missingKeys: buildResult.missingKeys, unsafeKeys: buildResult.unsafeKeys };
|
|
361
373
|
hasConfigChanged = true;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function readQuotedString(text, startIndex) {
|
|
4
|
+
const quote = text[startIndex];
|
|
5
|
+
let value = '';
|
|
6
|
+
|
|
7
|
+
for (let i = startIndex + 1; i < text.length; i += 1) {
|
|
8
|
+
const ch = text[i];
|
|
9
|
+
if (ch === '\\') {
|
|
10
|
+
value += ch;
|
|
11
|
+
if (i + 1 < text.length) {
|
|
12
|
+
value += text[i + 1];
|
|
13
|
+
i += 1;
|
|
14
|
+
}
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (ch === quote) {
|
|
18
|
+
return {
|
|
19
|
+
value,
|
|
20
|
+
end: i + 1
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
value += ch;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isIdentifierStart(ch) {
|
|
30
|
+
return /[A-Za-z_$]/.test(ch);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isIdentifierPart(ch) {
|
|
34
|
+
return /[A-Za-z0-9_$]/.test(ch);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function skipTrivia(text, index) {
|
|
38
|
+
let cursor = index;
|
|
39
|
+
while (cursor < text.length) {
|
|
40
|
+
const ch = text[cursor];
|
|
41
|
+
const next = text[cursor + 1];
|
|
42
|
+
if (/\s/.test(ch)) {
|
|
43
|
+
cursor += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (ch === '/' && next === '/') {
|
|
47
|
+
cursor += 2;
|
|
48
|
+
while (cursor < text.length && text[cursor] !== '\n') {
|
|
49
|
+
cursor += 1;
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === '/' && next === '*') {
|
|
54
|
+
cursor += 2;
|
|
55
|
+
while (cursor + 1 < text.length && !(text[cursor] === '*' && text[cursor + 1] === '/')) {
|
|
56
|
+
cursor += 1;
|
|
57
|
+
}
|
|
58
|
+
cursor = cursor + 1 < text.length ? cursor + 2 : text.length;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
return cursor;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scanValueEnd(text, startIndex) {
|
|
67
|
+
let cursor = startIndex;
|
|
68
|
+
let stringQuote = '';
|
|
69
|
+
let lineComment = false;
|
|
70
|
+
let blockComment = false;
|
|
71
|
+
let depth = 0;
|
|
72
|
+
|
|
73
|
+
for (; cursor < text.length; cursor += 1) {
|
|
74
|
+
const ch = text[cursor];
|
|
75
|
+
const next = text[cursor + 1];
|
|
76
|
+
|
|
77
|
+
if (lineComment) {
|
|
78
|
+
if (ch === '\n') {
|
|
79
|
+
lineComment = false;
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (blockComment) {
|
|
84
|
+
if (ch === '*' && next === '/') {
|
|
85
|
+
blockComment = false;
|
|
86
|
+
cursor += 1;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (stringQuote) {
|
|
91
|
+
if (ch === '\\') {
|
|
92
|
+
cursor += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (ch === stringQuote) {
|
|
96
|
+
stringQuote = '';
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (ch === '/' && next === '/') {
|
|
102
|
+
lineComment = true;
|
|
103
|
+
cursor += 1;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (ch === '/' && next === '*') {
|
|
107
|
+
blockComment = true;
|
|
108
|
+
cursor += 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (ch === '"' || ch === '\'') {
|
|
112
|
+
stringQuote = ch;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (ch === '{' || ch === '[' || ch === '(') {
|
|
116
|
+
depth += 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (ch === '}' || ch === ']' || ch === ')') {
|
|
120
|
+
if (depth === 0) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
depth -= 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (depth === 0 && ch === ',') {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let end = cursor;
|
|
132
|
+
while (end > startIndex && /\s/.test(text[end - 1])) {
|
|
133
|
+
end -= 1;
|
|
134
|
+
}
|
|
135
|
+
return end;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function findRootObjectStart(text) {
|
|
139
|
+
const source = String(text || '');
|
|
140
|
+
const start = skipTrivia(source, 0);
|
|
141
|
+
return source[start] === '{' ? start : -1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readPropertyToken(text, startIndex) {
|
|
145
|
+
const ch = text[startIndex];
|
|
146
|
+
if (ch === '"' || ch === '\'') {
|
|
147
|
+
return readQuotedString(text, startIndex);
|
|
148
|
+
}
|
|
149
|
+
if (!isIdentifierStart(ch)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let end = startIndex + 1;
|
|
154
|
+
while (end < text.length && isIdentifierPart(text[end])) {
|
|
155
|
+
end += 1;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
value: text.slice(startIndex, end),
|
|
159
|
+
end
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function findObjectPropertyValueRange(text, objectStartIndex, propertyName) {
|
|
164
|
+
let cursor = skipTrivia(text, objectStartIndex + 1);
|
|
165
|
+
while (cursor < text.length) {
|
|
166
|
+
cursor = skipTrivia(text, cursor);
|
|
167
|
+
if (text[cursor] === '}') {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const token = readPropertyToken(text, cursor);
|
|
171
|
+
if (!token) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
cursor = skipTrivia(text, token.end);
|
|
175
|
+
if (text[cursor] !== ':') {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const valueStart = skipTrivia(text, cursor + 1);
|
|
179
|
+
const valueEnd = scanValueEnd(text, valueStart);
|
|
180
|
+
if (token.value === propertyName) {
|
|
181
|
+
return { start: valueStart, end: valueEnd };
|
|
182
|
+
}
|
|
183
|
+
cursor = skipTrivia(text, valueEnd);
|
|
184
|
+
if (text[cursor] === ',') {
|
|
185
|
+
cursor += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (text[cursor] === '}') {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function findValueRangeByPath(text, pathParts) {
|
|
196
|
+
if (!Array.isArray(pathParts) || pathParts.length === 0) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let objectStart = findRootObjectStart(text);
|
|
201
|
+
if (objectStart === -1) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let range = null;
|
|
206
|
+
for (let i = 0; i < pathParts.length; i += 1) {
|
|
207
|
+
range = findObjectPropertyValueRange(text, objectStart, pathParts[i]);
|
|
208
|
+
if (!range) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
if (i === pathParts.length - 1) {
|
|
212
|
+
return range;
|
|
213
|
+
}
|
|
214
|
+
const nextObjectStart = skipTrivia(text, range.start);
|
|
215
|
+
if (text[nextObjectStart] !== '{') {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
objectStart = nextObjectStart;
|
|
219
|
+
}
|
|
220
|
+
return range;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function findTopLevelPropertyValueRange(text, propertyName) {
|
|
224
|
+
return findValueRangeByPath(text, [propertyName]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function applyTextReplacements(text, replacements) {
|
|
228
|
+
return replacements
|
|
229
|
+
.slice()
|
|
230
|
+
.sort((a, b) => b.start - a.start)
|
|
231
|
+
.reduce((result, item) => `${result.slice(0, item.start)}${item.text}${result.slice(item.end)}`, text);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
findTopLevelPropertyValueRange,
|
|
236
|
+
findValueRangeByPath,
|
|
237
|
+
applyTextReplacements
|
|
238
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function createPlaywrightBootstrapManager(options = {}) {
|
|
7
|
+
const plugin = options.plugin;
|
|
8
|
+
const isCliScene = options.isCliScene || (() => false);
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
buildInitScriptContent() {
|
|
12
|
+
const lines = [
|
|
13
|
+
"'use strict';",
|
|
14
|
+
'(function () {',
|
|
15
|
+
` const platformValue = ${JSON.stringify(plugin.config.navigatorPlatform)};`,
|
|
16
|
+
' try {',
|
|
17
|
+
' const navProto = Object.getPrototypeOf(navigator);',
|
|
18
|
+
" Object.defineProperty(navProto, 'platform', {",
|
|
19
|
+
' configurable: true,',
|
|
20
|
+
' get: () => platformValue',
|
|
21
|
+
' });',
|
|
22
|
+
' } catch (_) {}'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
if (plugin.config.disableWebRTC) {
|
|
26
|
+
lines.push(
|
|
27
|
+
' try {',
|
|
28
|
+
' const scope = globalThis;',
|
|
29
|
+
" const blocked = ['RTCPeerConnection', 'webkitRTCPeerConnection', 'RTCIceCandidate', 'RTCRtpSender', 'RTCRtpReceiver', 'RTCRtpTransceiver', 'RTCDataChannel'];",
|
|
30
|
+
' for (const name of blocked) {',
|
|
31
|
+
" Object.defineProperty(scope, name, { configurable: true, writable: true, value: undefined });",
|
|
32
|
+
' }',
|
|
33
|
+
' if (navigator.mediaDevices) {',
|
|
34
|
+
' const errorFactory = () => {',
|
|
35
|
+
' try {',
|
|
36
|
+
" return new DOMException('WebRTC is disabled', 'NotAllowedError');",
|
|
37
|
+
' } catch (_) {',
|
|
38
|
+
" const error = new Error('WebRTC is disabled');",
|
|
39
|
+
" error.name = 'NotAllowedError';",
|
|
40
|
+
' return error;',
|
|
41
|
+
' }',
|
|
42
|
+
' };',
|
|
43
|
+
" Object.defineProperty(navigator.mediaDevices, 'getUserMedia', {",
|
|
44
|
+
' configurable: true,',
|
|
45
|
+
' writable: true,',
|
|
46
|
+
' value: async () => { throw errorFactory(); }',
|
|
47
|
+
' });',
|
|
48
|
+
' }',
|
|
49
|
+
' } catch (_) {}'
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push('})();', '');
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
},
|
|
56
|
+
ensureSceneInitScript(sceneName) {
|
|
57
|
+
const filePath = plugin.sceneInitScriptPath(sceneName);
|
|
58
|
+
const content = this.buildInitScriptContent();
|
|
59
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
60
|
+
return filePath;
|
|
61
|
+
},
|
|
62
|
+
defaultBrowserName(sceneName) {
|
|
63
|
+
if (isCliScene(sceneName)) {
|
|
64
|
+
return 'chromium';
|
|
65
|
+
}
|
|
66
|
+
const cfg = plugin.buildSceneConfig(sceneName);
|
|
67
|
+
const browserName = cfg && cfg.browser && cfg.browser.browserName;
|
|
68
|
+
return String(browserName || 'chromium');
|
|
69
|
+
},
|
|
70
|
+
ensureContainerScenePrerequisites(sceneName) {
|
|
71
|
+
if (!plugin.sceneConfigMissing(sceneName)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const tag = String(plugin.config.dockerTag || 'latest').trim() || 'latest';
|
|
75
|
+
const image = `mcr.microsoft.com/playwright/mcp:${tag}`;
|
|
76
|
+
plugin.runCmd([plugin.config.containerRuntime, 'pull', image], { check: true });
|
|
77
|
+
},
|
|
78
|
+
ensureHostScenePrerequisites(sceneName) {
|
|
79
|
+
if (!isCliScene(sceneName) && !plugin.sceneConfigMissing(sceneName)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
plugin.runCmd([plugin.playwrightBinPath(sceneName), 'install', '--with-deps', plugin.defaultBrowserName(sceneName)], { check: true });
|
|
83
|
+
},
|
|
84
|
+
localBinPath(binName) {
|
|
85
|
+
const filename = process.platform === 'win32' ? `${binName}.cmd` : binName;
|
|
86
|
+
const binPath = path.join(plugin.projectRoot, 'node_modules', '.bin', filename);
|
|
87
|
+
if (!fs.existsSync(binPath)) {
|
|
88
|
+
throw new Error(`local binary not found: ${binPath}. Run npm install first.`);
|
|
89
|
+
}
|
|
90
|
+
return binPath;
|
|
91
|
+
},
|
|
92
|
+
playwrightBinPath(sceneName) {
|
|
93
|
+
if (!isCliScene(sceneName)) {
|
|
94
|
+
return plugin.localBinPath('playwright');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const filename = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
|
|
98
|
+
const candidates = [
|
|
99
|
+
path.join(plugin.projectRoot, 'node_modules', '@playwright', 'mcp', 'node_modules', '.bin', filename),
|
|
100
|
+
path.join(plugin.projectRoot, 'node_modules', '.bin', filename)
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
if (fs.existsSync(candidate)) {
|
|
105
|
+
return candidate;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error(`local binary not found for ${sceneName}. Run npm install first.`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
createPlaywrightBootstrapManager
|
|
116
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
function createPlaywrightCommandOutputManager(options = {}) {
|
|
6
|
+
const plugin = options.plugin;
|
|
7
|
+
const isMcpScene = options.isMcpScene || (() => false);
|
|
8
|
+
const playwrightCliVersion = options.playwrightCliVersion || '';
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
detectCurrentIPv4() {
|
|
12
|
+
const interfaces = os.networkInterfaces();
|
|
13
|
+
for (const values of Object.values(interfaces)) {
|
|
14
|
+
if (!Array.isArray(values)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
for (const item of values) {
|
|
18
|
+
if (!item || item.internal) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (item.family === 'IPv4') {
|
|
22
|
+
return item.address;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
},
|
|
28
|
+
resolveMcpAddHost(hostArg) {
|
|
29
|
+
if (!hostArg) {
|
|
30
|
+
return plugin.config.mcpDefaultHost;
|
|
31
|
+
}
|
|
32
|
+
const value = String(hostArg).trim();
|
|
33
|
+
if (!value) {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
if (value === 'current-ip') {
|
|
37
|
+
return this.detectCurrentIPv4();
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
},
|
|
41
|
+
printMcpAdd(hostArg) {
|
|
42
|
+
const host = this.resolveMcpAddHost(hostArg);
|
|
43
|
+
if (!host) {
|
|
44
|
+
plugin.writeStderr('[mcp-add] failed: cannot determine host. Use --host <host> to set one explicitly.');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const scenes = plugin.resolveTargets('all').filter(sceneName => isMcpScene(sceneName));
|
|
49
|
+
for (const sceneName of scenes) {
|
|
50
|
+
const url = `http://${host}:${plugin.scenePort(sceneName)}/mcp`;
|
|
51
|
+
plugin.writeStdout(`claude mcp add -t http -s user playwright-${sceneName} ${url}`);
|
|
52
|
+
}
|
|
53
|
+
plugin.writeStdout('');
|
|
54
|
+
for (const sceneName of scenes) {
|
|
55
|
+
const url = `http://${host}:${plugin.scenePort(sceneName)}/mcp`;
|
|
56
|
+
plugin.writeStdout(`codex mcp add playwright-${sceneName} --url ${url}`);
|
|
57
|
+
}
|
|
58
|
+
plugin.writeStdout('');
|
|
59
|
+
for (const sceneName of scenes) {
|
|
60
|
+
const url = `http://${host}:${plugin.scenePort(sceneName)}/mcp`;
|
|
61
|
+
plugin.writeStdout(`gemini mcp add -t http -s user playwright-${sceneName} ${url}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return 0;
|
|
65
|
+
},
|
|
66
|
+
printCliAdd() {
|
|
67
|
+
const lines = [
|
|
68
|
+
'PLAYWRIGHT_CLI_INSTALL_DIR="${TMPDIR:-/tmp}/manyoyo-playwright-cli-install-$$"',
|
|
69
|
+
'mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"',
|
|
70
|
+
'echo \'{"browser":{"browserName":"chromium","launchOptions":{"channel":"chromium"}}}\' > "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright/cli.config.json"',
|
|
71
|
+
'cd "$PLAYWRIGHT_CLI_INSTALL_DIR"',
|
|
72
|
+
`npm install -g @playwright/cli@${playwrightCliVersion}`,
|
|
73
|
+
'playwright-cli install --skills',
|
|
74
|
+
'PLAYWRIGHT_CLI_SKILL_SOURCE="$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli"',
|
|
75
|
+
'for target in ~/.claude/skills/playwright-cli ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli; do',
|
|
76
|
+
' mkdir -p "$target"',
|
|
77
|
+
' cp -R "$PLAYWRIGHT_CLI_SKILL_SOURCE/." "$target/"',
|
|
78
|
+
'done',
|
|
79
|
+
'cd "$OLDPWD"',
|
|
80
|
+
'rm -rf "$PLAYWRIGHT_CLI_INSTALL_DIR"'
|
|
81
|
+
];
|
|
82
|
+
plugin.writeStdout(lines.join('\n'));
|
|
83
|
+
return 0;
|
|
84
|
+
},
|
|
85
|
+
printSummary() {
|
|
86
|
+
const scenes = plugin.resolveTargets('all');
|
|
87
|
+
plugin.writeStdout(`playwright\truntime=${plugin.config.runtime}\tscenes=${scenes.join(',')}`);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = {
|
|
94
|
+
createPlaywrightCommandOutputManager
|
|
95
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function createPlaywrightContainerRuntimeManager(options = {}) {
|
|
7
|
+
const plugin = options.plugin;
|
|
8
|
+
const sceneDefs = options.sceneDefs || {};
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
containerEnv(sceneName, cfgPath, actionOptions = {}) {
|
|
12
|
+
const def = sceneDefs[sceneName];
|
|
13
|
+
const requireVncPassword = actionOptions.requireVncPassword === true;
|
|
14
|
+
const env = {
|
|
15
|
+
...process.env,
|
|
16
|
+
PLAYWRIGHT_MCP_DOCKER_TAG: plugin.config.dockerTag,
|
|
17
|
+
PLAYWRIGHT_MCP_PORT: String(plugin.scenePort(sceneName)),
|
|
18
|
+
PLAYWRIGHT_MCP_CONFIG_PATH: cfgPath,
|
|
19
|
+
PLAYWRIGHT_MCP_CONTAINER_NAME: def.containerName,
|
|
20
|
+
PLAYWRIGHT_MCP_IMAGE: plugin.config.headedImage,
|
|
21
|
+
PLAYWRIGHT_MCP_NOVNC_PORT: String(plugin.config.ports.mcpContHeadedNoVnc)
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (sceneName === 'mcp-cont-headed') {
|
|
25
|
+
const envKey = plugin.config.vncPasswordEnvKey;
|
|
26
|
+
let password = process.env[envKey];
|
|
27
|
+
if (!password) {
|
|
28
|
+
password = plugin.randomAlnum(16);
|
|
29
|
+
if (requireVncPassword) {
|
|
30
|
+
plugin.writeStdout(`[up] mcp-cont-headed ${envKey} not set; generated random 16-char password: ${password}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
env.VNC_PASSWORD = password;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return env;
|
|
37
|
+
},
|
|
38
|
+
containerComposePath(sceneName) {
|
|
39
|
+
const def = sceneDefs[sceneName];
|
|
40
|
+
return path.join(plugin.config.composeDir, def.composeFile);
|
|
41
|
+
},
|
|
42
|
+
sceneComposeOverridePath(sceneName) {
|
|
43
|
+
return path.join(plugin.config.runDir, `${sceneName}.compose.override.yaml`);
|
|
44
|
+
},
|
|
45
|
+
ensureContainerComposeOverride(sceneName, volumeMounts = []) {
|
|
46
|
+
const overridePath = this.sceneComposeOverridePath(sceneName);
|
|
47
|
+
if (!Array.isArray(volumeMounts) || volumeMounts.length === 0) {
|
|
48
|
+
fs.rmSync(overridePath, { force: true });
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fs.mkdirSync(plugin.config.runDir, { recursive: true });
|
|
53
|
+
const lines = [
|
|
54
|
+
'services:',
|
|
55
|
+
' playwright:',
|
|
56
|
+
' volumes:'
|
|
57
|
+
];
|
|
58
|
+
volumeMounts.forEach(item => {
|
|
59
|
+
lines.push(` - ${JSON.stringify(String(item))}`);
|
|
60
|
+
});
|
|
61
|
+
fs.writeFileSync(overridePath, `${lines.join('\n')}\n`, 'utf8');
|
|
62
|
+
return overridePath;
|
|
63
|
+
},
|
|
64
|
+
ensureContainerRuntimeAvailable(action, sceneName) {
|
|
65
|
+
const runtime = plugin.config.containerRuntime;
|
|
66
|
+
if (!plugin.ensureCommandAvailable(runtime)) {
|
|
67
|
+
plugin.writeStderr(`[${action}] ${sceneName} failed: ${runtime} command not found.`);
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
return runtime;
|
|
71
|
+
},
|
|
72
|
+
buildContainerComposeCommand(sceneName, composeFiles = [], trailingArgs = []) {
|
|
73
|
+
const def = sceneDefs[sceneName];
|
|
74
|
+
const files = Array.isArray(composeFiles) && composeFiles.length > 0
|
|
75
|
+
? composeFiles
|
|
76
|
+
: [this.containerComposePath(sceneName)];
|
|
77
|
+
const args = [
|
|
78
|
+
plugin.config.containerRuntime,
|
|
79
|
+
'compose',
|
|
80
|
+
'-p',
|
|
81
|
+
def.projectName
|
|
82
|
+
];
|
|
83
|
+
files.forEach(filePath => {
|
|
84
|
+
args.push('-f', filePath);
|
|
85
|
+
});
|
|
86
|
+
args.push(...trailingArgs);
|
|
87
|
+
return args;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
createPlaywrightContainerRuntimeManager
|
|
94
|
+
};
|