@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.
@@ -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 data = extractors[agent](ctx.homeDir);
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, yoloMap[agent], data.volumes, data.keys, data.values);
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
+ };