@xcanwin/manyoyo 5.4.4 → 5.4.12

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 CHANGED
@@ -53,7 +53,7 @@ AI Agent CLI 往往需要:
53
53
  ```bash
54
54
  npm install -g @xcanwin/manyoyo
55
55
  podman pull ubuntu:24.04 # 仅 Podman 需要
56
- manyoyo build --iv 1.8.8-common
56
+ manyoyo build --iv 1.8.12-common
57
57
  manyoyo init all
58
58
  manyoyo run -r claude
59
59
  ```
@@ -137,10 +137,10 @@ manyoyo config command
137
137
 
138
138
  ```bash
139
139
  # common 版本
140
- manyoyo build --iv 1.8.8-common
140
+ manyoyo build --iv 1.8.12-common
141
141
 
142
142
  # full 版本
143
- manyoyo build --iv 1.8.8-full
143
+ manyoyo build --iv 1.8.12-full
144
144
 
145
145
  # 自定义工具集
146
146
  manyoyo build --iba TOOL=go,codex,java,gemini
package/bin/manyoyo.js CHANGED
@@ -8,9 +8,9 @@ const crypto = require('crypto');
8
8
  const net = require('net');
9
9
  const readline = require('readline');
10
10
  const { Command } = require('commander');
11
- const JSON5 = require('json5');
12
11
  const { startWebServer } = require('../lib/web/server');
13
12
  const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
13
+ const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
14
14
  const { initAgentConfigs } = require('../lib/init-config');
15
15
  const { buildImage } = require('../lib/image-build');
16
16
  const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
@@ -309,19 +309,29 @@ function installServeProcessDiagnostics(logger) {
309
309
  * @returns {Config} 配置对象
310
310
  */
311
311
  function loadConfig() {
312
- const configPath = path.join(os.homedir(), '.manyoyo', 'manyoyo.json');
313
- if (fs.existsSync(configPath)) {
314
- try {
315
- const config = JSON5.parse(fs.readFileSync(configPath, 'utf-8'));
316
- return config;
317
- } catch (e) {
318
- console.error(`${YELLOW}⚠️ 配置文件格式错误: ${configPath}${NC}`);
312
+ const result = readManyoyoConfig();
313
+ if (result.exists) {
314
+ if (result.parseError) {
315
+ console.error(`${YELLOW}⚠️ 配置文件格式错误: ${result.path}${NC}`);
319
316
  return {};
320
317
  }
318
+ return result.config;
321
319
  }
322
320
  return {};
323
321
  }
324
322
 
323
+ function syncBuiltImageVersionToGlobalConfig(imageVersion) {
324
+ const syncResult = syncGlobalImageVersion(imageVersion);
325
+ if (syncResult.updated) {
326
+ console.log(`${GREEN}✅ 已同步 ${path.basename(getManyoyoConfigPath())} 的 imageVersion: ${imageVersion}${NC}`);
327
+ return;
328
+ }
329
+ if (syncResult.reason === 'unchanged') {
330
+ return;
331
+ }
332
+ console.log(`${YELLOW}⚠️ 镜像构建成功,但未更新 imageVersion: ${syncResult.path}${NC}`);
333
+ }
334
+
325
335
  function loadRunConfig(name, config) {
326
336
  const runName = String(name || '').trim();
327
337
  if (!runName) {
@@ -1061,6 +1071,14 @@ async function setupCommander() {
1061
1071
  host: options.host || ''
1062
1072
  }, options));
1063
1073
 
1074
+ command.command('cli-add')
1075
+ .description('输出 playwright-cli skill 安装命令')
1076
+ .action(() => selectPluginAction({
1077
+ action: 'cli-add',
1078
+ pluginName: 'playwright',
1079
+ scene: 'all'
1080
+ }));
1081
+
1064
1082
  command.command('ext-download')
1065
1083
  .description('下载并解压 Playwright 扩展到 ~/.manyoyo/plugin/playwright/extensions/')
1066
1084
  .option('--prodversion <ver>', 'CRX 下载使用的 Chrome 版本号 (默认 132.0.0.0)')
@@ -1099,8 +1117,8 @@ async function setupCommander() {
1099
1117
  ${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
1100
1118
  ${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
1101
1119
  ${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
1102
- ${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright 默认场景(推荐)
1103
- ${MANYOYO_NAME} plugin playwright up mcp-host-headless 通过 plugin 命名空间启动
1120
+ ${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright MCP 宿主场景(默认/推荐)
1121
+ ${MANYOYO_NAME} playwright up cli-host-headless 启动 playwright CLI 宿主场景(供容器内 playwright-cli 附着)
1104
1122
  ${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
1105
1123
  `);
1106
1124
 
@@ -1971,6 +1989,7 @@ async function main() {
1971
1989
  pruneDanglingImages,
1972
1990
  colors: { RED, GREEN, YELLOW, BLUE, CYAN, NC }
1973
1991
  });
1992
+ syncBuiltImageVersionToGlobalConfig(runtime.imageVersion);
1974
1993
  process.exit(0);
1975
1994
  }
1976
1995
 
@@ -127,6 +127,7 @@ EOX
127
127
 
128
128
  # 从 cache-stage 复制 Node.js(缓存或下载)
129
129
  COPY --from=cache-stage /opt/node /usr/local
130
+ COPY ./package.json /tmp/manyoyo-package.json
130
131
  COPY ./docker/res/playwright/cli-cont-headless.init.js /app/config/cli-cont-headless.init.js
131
132
  COPY ./docker/res/playwright/cli-cont-headless.json /app/config/cli-cont-headless.json
132
133
  COPY ./docker/res/ /tmp/docker-res/
@@ -202,22 +203,15 @@ RUN <<EOX
202
203
  ;; esac
203
204
 
204
205
  # 安装 Playwright CLI skills(不在镜像构建阶段下载浏览器)
205
- npm install -g @playwright/cli@latest
206
+ PLAYWRIGHT_CLI_VERSION=$(node -p "const pkg = require('/tmp/manyoyo-package.json'); const value = String(pkg.playwrightCliVersion || '').trim(); if (!value) { throw new Error('package.json.playwrightCliVersion is required'); } value")
207
+ npm install -g "@playwright/cli@${PLAYWRIGHT_CLI_VERSION}"
206
208
  PLAYWRIGHT_CLI_INSTALL_DIR=/tmp/playwright-cli-install
207
209
  mkdir -p "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright"
208
- cat > "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright/cli.config.json" <<'EOF'
209
- {
210
- "browser": {
211
- "browserName": "chromium",
212
- "launchOptions": {
213
- "channel": "chrome"
214
- }
215
- }
216
- }
217
- EOF
210
+ echo '{"browser":{"browserName":"chromium","launchOptions":{"channel":"chromium"}}}' > "${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright/cli.config.json"
218
211
  cd "${PLAYWRIGHT_CLI_INSTALL_DIR}"
219
- playwright-cli install --skills
212
+ playwright-cli --config="${PLAYWRIGHT_CLI_INSTALL_DIR}/.playwright/cli.config.json" install --skills
220
213
  mkdir -p "$HOME/.codex/skills/playwright-cli" ~/.gemini/skills/playwright-cli
214
+ cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.claude/skills/playwright-cli/"
221
215
  cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.codex/skills/playwright-cli/"
222
216
  cp -R "${PLAYWRIGHT_CLI_INSTALL_DIR}/.claude/skills/playwright-cli/." "$HOME/.gemini/skills/playwright-cli/"
223
217
  cd $OLDPWD
@@ -225,9 +219,13 @@ EOF
225
219
 
226
220
  # 清理
227
221
  npm cache clean --force
222
+ rm -f /tmp/manyoyo-package.json
228
223
  rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.npm ~/go/pkg/mod/cache
229
224
  EOX
230
225
 
226
+ COPY ./docker/res/playwright/playwright-cli-wrapper.sh /usr/local/bin/playwright-cli
227
+ RUN chmod +x /usr/local/bin/playwright-cli
228
+
231
229
  # 从 cache-stage 复制 JDT LSP(缓存或下载)
232
230
  COPY --from=cache-stage /opt/jdtls /tmp/jdtls-cache
233
231
 
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env sh
2
+ set -eu
3
+
4
+ PLAYWRIGHT_NODE_BIN="${PLAYWRIGHT_NODE_BIN:-node}"
5
+ PLAYWRIGHT_CLI_ROOT="/usr/local/lib/node_modules/@playwright/cli/node_modules/playwright"
6
+ PLAYWRIGHT_CLI_PROGRAM="${PLAYWRIGHT_CLI_ROOT}/lib/cli/client/program.js"
7
+ PLAYWRIGHT_REAL_CLI="${PLAYWRIGHT_CLI_ROOT}/cli.js"
8
+
9
+ resolve_browser() {
10
+ browser=""
11
+
12
+ while [ "$#" -gt 0 ]; do
13
+ case "$1" in
14
+ --browser=*)
15
+ browser="${1#--browser=}"
16
+ ;;
17
+ --browser)
18
+ shift
19
+ if [ "$#" -gt 0 ]; then
20
+ browser="$1"
21
+ shift
22
+ fi
23
+ continue
24
+ ;;
25
+ --)
26
+ break
27
+ ;;
28
+ -*)
29
+ ;;
30
+ *)
31
+ if [ -z "$browser" ]; then
32
+ browser="$1"
33
+ fi
34
+ ;;
35
+ esac
36
+ shift
37
+ done
38
+
39
+ if [ -n "$browser" ]; then
40
+ printf '%s\n' "$browser"
41
+ return 0
42
+ fi
43
+
44
+ "$PLAYWRIGHT_NODE_BIN" <<'NODE'
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+
48
+ const candidates = [
49
+ process.env.PLAYWRIGHT_MCP_CONFIG,
50
+ path.resolve('.playwright/cli.config.json')
51
+ ].filter(Boolean);
52
+
53
+ for (const filePath of candidates) {
54
+ try {
55
+ const config = JSON.parse(fs.readFileSync(filePath, 'utf8'));
56
+ const browser = config && config.browser ? config.browser : {};
57
+ const launchOptions = browser.launchOptions || {};
58
+ const channel = String(launchOptions.channel || browser.browserName || '').trim();
59
+ if (channel) {
60
+ process.stdout.write(channel);
61
+ process.exit(0);
62
+ }
63
+ } catch (_) {}
64
+ }
65
+
66
+ process.stdout.write('chromium');
67
+ NODE
68
+ }
69
+
70
+ if [ "${1-}" = "install-browser" ]; then
71
+ shift
72
+ for arg in "$@"; do
73
+ if [ "$arg" = "--help" ] || [ "$arg" = "-h" ]; then
74
+ exec "$PLAYWRIGHT_NODE_BIN" "$PLAYWRIGHT_CLI_PROGRAM" install-browser "$@"
75
+ fi
76
+ done
77
+
78
+ BROWSER="$(resolve_browser "$@")"
79
+ exec "$PLAYWRIGHT_NODE_BIN" "$PLAYWRIGHT_REAL_CLI" install "$BROWSER"
80
+ fi
81
+
82
+ exec "$PLAYWRIGHT_NODE_BIN" "$PLAYWRIGHT_CLI_PROGRAM" "$@"
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const JSON5 = require('json5');
7
+
8
+ function getManyoyoConfigPath(homeDir = os.homedir()) {
9
+ return path.join(homeDir, '.manyoyo', 'manyoyo.json');
10
+ }
11
+
12
+ function readManyoyoConfig(homeDir = os.homedir()) {
13
+ const configPath = getManyoyoConfigPath(homeDir);
14
+ if (!fs.existsSync(configPath)) {
15
+ return {
16
+ path: configPath,
17
+ exists: false,
18
+ config: {}
19
+ };
20
+ }
21
+
22
+ try {
23
+ const config = JSON5.parse(fs.readFileSync(configPath, 'utf-8'));
24
+ return {
25
+ path: configPath,
26
+ exists: true,
27
+ config
28
+ };
29
+ } catch (error) {
30
+ return {
31
+ path: configPath,
32
+ exists: true,
33
+ config: {},
34
+ parseError: error
35
+ };
36
+ }
37
+ }
38
+
39
+ function readQuotedString(text, startIndex) {
40
+ const quote = text[startIndex];
41
+ let value = '';
42
+
43
+ for (let i = startIndex + 1; i < text.length; i += 1) {
44
+ const ch = text[i];
45
+ if (ch === '\\') {
46
+ value += ch;
47
+ if (i + 1 < text.length) {
48
+ value += text[i + 1];
49
+ i += 1;
50
+ }
51
+ continue;
52
+ }
53
+ if (ch === quote) {
54
+ return {
55
+ value,
56
+ end: i + 1
57
+ };
58
+ }
59
+ value += ch;
60
+ }
61
+
62
+ return null;
63
+ }
64
+
65
+ function isIdentifierStart(ch) {
66
+ return /[A-Za-z_$]/.test(ch);
67
+ }
68
+
69
+ function isIdentifierPart(ch) {
70
+ return /[A-Za-z0-9_$]/.test(ch);
71
+ }
72
+
73
+ function skipWhitespace(text, index) {
74
+ let i = index;
75
+ while (i < text.length && /\s/.test(text[i])) {
76
+ i += 1;
77
+ }
78
+ return i;
79
+ }
80
+
81
+ function findTopLevelPropertyValueRange(text, propertyName) {
82
+ let depth = 0;
83
+ let inString = '';
84
+ let inLineComment = false;
85
+ let inBlockComment = false;
86
+
87
+ for (let i = 0; i < text.length; i += 1) {
88
+ const ch = text[i];
89
+ const next = text[i + 1];
90
+
91
+ if (inLineComment) {
92
+ if (ch === '\n') inLineComment = false;
93
+ continue;
94
+ }
95
+ if (inBlockComment) {
96
+ if (ch === '*' && next === '/') {
97
+ inBlockComment = false;
98
+ i += 1;
99
+ }
100
+ continue;
101
+ }
102
+ if (inString) {
103
+ if (ch === '\\') {
104
+ i += 1;
105
+ continue;
106
+ }
107
+ if (ch === inString) inString = '';
108
+ continue;
109
+ }
110
+
111
+ if (ch === '/' && next === '/') {
112
+ inLineComment = true;
113
+ i += 1;
114
+ continue;
115
+ }
116
+ if (ch === '/' && next === '*') {
117
+ inBlockComment = true;
118
+ i += 1;
119
+ continue;
120
+ }
121
+ if (depth === 1 && !/\s|,/.test(ch)) {
122
+ let property = '';
123
+ let cursor = i;
124
+ if (ch === '"' || ch === '\'') {
125
+ const token = readQuotedString(text, i);
126
+ if (!token) {
127
+ return null;
128
+ }
129
+ property = token.value;
130
+ cursor = token.end;
131
+ } else if (isIdentifierStart(ch)) {
132
+ cursor = i + 1;
133
+ while (cursor < text.length && isIdentifierPart(text[cursor])) {
134
+ cursor += 1;
135
+ }
136
+ property = text.slice(i, cursor);
137
+ }
138
+
139
+ if (property) {
140
+ const colonIndex = skipWhitespace(text, cursor);
141
+ if (text[colonIndex] === ':') {
142
+ if (property !== propertyName) {
143
+ i = colonIndex;
144
+ continue;
145
+ }
146
+
147
+ let valueStart = skipWhitespace(text, colonIndex + 1);
148
+ let valueEnd = valueStart;
149
+ let valueString = '';
150
+ let valueLineComment = false;
151
+ let valueBlockComment = false;
152
+ let valueDepth = 0;
153
+
154
+ for (; valueEnd < text.length; valueEnd += 1) {
155
+ const valueCh = text[valueEnd];
156
+ const valueNext = text[valueEnd + 1];
157
+
158
+ if (valueLineComment) {
159
+ if (valueCh === '\n') valueLineComment = false;
160
+ continue;
161
+ }
162
+ if (valueBlockComment) {
163
+ if (valueCh === '*' && valueNext === '/') {
164
+ valueBlockComment = false;
165
+ valueEnd += 1;
166
+ }
167
+ continue;
168
+ }
169
+ if (valueString) {
170
+ if (valueCh === '\\') {
171
+ valueEnd += 1;
172
+ continue;
173
+ }
174
+ if (valueCh === valueString) valueString = '';
175
+ continue;
176
+ }
177
+
178
+ if (valueCh === '/' && valueNext === '/') {
179
+ valueLineComment = true;
180
+ valueEnd += 1;
181
+ continue;
182
+ }
183
+ if (valueCh === '/' && valueNext === '*') {
184
+ valueBlockComment = true;
185
+ valueEnd += 1;
186
+ continue;
187
+ }
188
+ if (valueCh === '"' || valueCh === '\'') {
189
+ valueString = valueCh;
190
+ continue;
191
+ }
192
+ if (valueCh === '{' || valueCh === '[' || valueCh === '(') {
193
+ valueDepth += 1;
194
+ continue;
195
+ }
196
+ if (valueCh === '}' || valueCh === ']' || valueCh === ')') {
197
+ if (valueDepth === 0) {
198
+ break;
199
+ }
200
+ valueDepth -= 1;
201
+ continue;
202
+ }
203
+ if (valueDepth === 0 && valueCh === ',') {
204
+ break;
205
+ }
206
+ }
207
+
208
+ while (valueEnd > valueStart && /\s/.test(text[valueEnd - 1])) {
209
+ valueEnd -= 1;
210
+ }
211
+
212
+ return {
213
+ start: valueStart,
214
+ end: valueEnd
215
+ };
216
+ }
217
+ }
218
+ }
219
+
220
+ if (ch === '"' || ch === '\'') {
221
+ inString = ch;
222
+ continue;
223
+ }
224
+ if (ch === '{' || ch === '[') {
225
+ depth += 1;
226
+ continue;
227
+ }
228
+ if (ch === '}' || ch === ']') {
229
+ depth -= 1;
230
+ continue;
231
+ }
232
+ }
233
+
234
+ return null;
235
+ }
236
+
237
+ function insertTopLevelImageVersion(text, imageVersion) {
238
+ const openBraceIndex = text.indexOf('{');
239
+ if (openBraceIndex === -1) {
240
+ return null;
241
+ }
242
+
243
+ const newlineIndex = text.indexOf('\n', openBraceIndex);
244
+ const insertIndex = newlineIndex === -1 ? openBraceIndex + 1 : newlineIndex + 1;
245
+ return `${text.slice(0, insertIndex)} imageVersion: ${JSON.stringify(imageVersion)},\n${text.slice(insertIndex)}`;
246
+ }
247
+
248
+ function updateImageVersionText(text, imageVersion) {
249
+ const range = findTopLevelPropertyValueRange(text, 'imageVersion');
250
+ if (range) {
251
+ return `${text.slice(0, range.start)}${JSON.stringify(imageVersion)}${text.slice(range.end)}`;
252
+ }
253
+ return insertTopLevelImageVersion(text, imageVersion);
254
+ }
255
+
256
+ function syncGlobalImageVersion(imageVersion, options = {}) {
257
+ const homeDir = options.homeDir || os.homedir();
258
+ const result = readManyoyoConfig(homeDir);
259
+ const configPath = result.path;
260
+
261
+ if (result.parseError) {
262
+ return {
263
+ updated: false,
264
+ path: configPath,
265
+ reason: 'parse-error'
266
+ };
267
+ }
268
+
269
+ const currentConfig = result.config;
270
+ if (typeof currentConfig !== 'object' || currentConfig === null || Array.isArray(currentConfig)) {
271
+ return {
272
+ updated: false,
273
+ path: configPath,
274
+ reason: 'invalid-root'
275
+ };
276
+ }
277
+
278
+ if (currentConfig.imageVersion === imageVersion) {
279
+ return {
280
+ updated: false,
281
+ path: configPath,
282
+ reason: 'unchanged'
283
+ };
284
+ }
285
+
286
+ const nextConfig = {
287
+ ...currentConfig,
288
+ imageVersion
289
+ };
290
+
291
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
292
+ if (result.exists) {
293
+ const currentText = fs.readFileSync(configPath, 'utf-8');
294
+ const updatedText = updateImageVersionText(currentText, imageVersion);
295
+ if (updatedText) {
296
+ fs.writeFileSync(configPath, updatedText.endsWith('\n') ? updatedText : `${updatedText}\n`);
297
+ } else {
298
+ fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 4)}\n`);
299
+ }
300
+ } else {
301
+ fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 4)}\n`);
302
+ }
303
+
304
+ return {
305
+ updated: true,
306
+ path: configPath,
307
+ reason: result.exists ? 'updated' : 'created'
308
+ };
309
+ }
310
+
311
+ module.exports = {
312
+ getManyoyoConfigPath,
313
+ readManyoyoConfig,
314
+ syncGlobalImageVersion
315
+ };
@@ -6,6 +6,7 @@ const os = require('os');
6
6
  const path = require('path');
7
7
  const crypto = require('crypto');
8
8
  const { spawn, spawnSync } = require('child_process');
9
+ const { playwrightCliVersion: PLAYWRIGHT_CLI_VERSION } = require('../../package.json');
9
10
 
10
11
  const EXTENSIONS = [
11
12
  ['ublock-origin-lite', 'ddkjiahejlhfcafbddmgiahcphecmpfh'],
@@ -25,7 +26,7 @@ const SCENE_DEFS = {
25
26
  composeFile: 'compose-headless.yaml',
26
27
  projectName: 'my-playwright-mcp-cont-headless',
27
28
  containerName: 'my-playwright-mcp-cont-headless',
28
- portKey: 'contHeadless',
29
+ portKey: 'mcpContHeadless',
29
30
  headless: true,
30
31
  listenHost: '0.0.0.0'
31
32
  },
@@ -36,7 +37,7 @@ const SCENE_DEFS = {
36
37
  composeFile: 'compose-headed.yaml',
37
38
  projectName: 'my-playwright-mcp-cont-headed',
38
39
  containerName: 'my-playwright-mcp-cont-headed',
39
- portKey: 'contHeaded',
40
+ portKey: 'mcpContHeaded',
40
41
  headless: false,
41
42
  listenHost: '0.0.0.0'
42
43
  },
@@ -44,7 +45,7 @@ const SCENE_DEFS = {
44
45
  type: 'host',
45
46
  engine: 'mcp',
46
47
  configFile: 'mcp-host-headless.json',
47
- portKey: 'hostHeadless',
48
+ portKey: 'mcpHostHeadless',
48
49
  headless: true,
49
50
  listenHost: '127.0.0.1'
50
51
  },
@@ -52,7 +53,7 @@ const SCENE_DEFS = {
52
53
  type: 'host',
53
54
  engine: 'mcp',
54
55
  configFile: 'mcp-host-headed.json',
55
- portKey: 'hostHeaded',
56
+ portKey: 'mcpHostHeaded',
56
57
  headless: false,
57
58
  listenHost: '127.0.0.1'
58
59
  },
@@ -293,7 +294,6 @@ class PlaywrightPlugin {
293
294
  runtime: 'mixed',
294
295
  enabledScenes: [...SCENE_ORDER],
295
296
  cliSessionScene: 'cli-host-headless',
296
- hostListen: '127.0.0.1',
297
297
  mcpDefaultHost: 'host.docker.internal',
298
298
  dockerTag: process.env.PLAYWRIGHT_MCP_DOCKER_TAG || 'latest',
299
299
  containerRuntime: '',
@@ -306,13 +306,13 @@ class PlaywrightPlugin {
306
306
  disableWebRTC: false,
307
307
  composeDir: path.join(__dirname, 'playwright-assets'),
308
308
  ports: {
309
- contHeadless: 8931,
310
- contHeaded: 8932,
311
- hostHeadless: 8933,
312
- hostHeaded: 8934,
309
+ mcpContHeadless: 8931,
310
+ mcpContHeaded: 8932,
311
+ mcpHostHeadless: 8933,
312
+ mcpHostHeaded: 8934,
313
313
  cliHostHeadless: 8935,
314
314
  cliHostHeaded: 8936,
315
- contHeadedNoVnc: 6080
315
+ mcpContHeadedNoVnc: 6080
316
316
  }
317
317
  };
318
318
 
@@ -380,6 +380,24 @@ class PlaywrightPlugin {
380
380
  this.stderr.write(`${line}\n`);
381
381
  }
382
382
 
383
+ remindCliSessionScene(sceneName) {
384
+ if (!isCliScene(sceneName)) {
385
+ return;
386
+ }
387
+ if (sceneName !== 'cli-host-headed') {
388
+ return;
389
+ }
390
+ if (this.config.cliSessionScene === sceneName) {
391
+ return;
392
+ }
393
+ this.writeStdout('[tip] 如果希望容器内 manyoyo run 自动附着到当前 CLI 宿主场景,请在 ~/.manyoyo/manyoyo.json 中设置:');
394
+ this.writeStdout(' "plugins": {');
395
+ this.writeStdout(' "playwright": {');
396
+ this.writeStdout(' "cliSessionScene": "cli-host-headed"');
397
+ this.writeStdout(' }');
398
+ this.writeStdout(' }');
399
+ }
400
+
383
401
  randomAlnum(length = 16) {
384
402
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
385
403
  let out = '';
@@ -481,15 +499,15 @@ class PlaywrightPlugin {
481
499
  fs.writeFileSync(this.sceneCliAttachConfigPath(sceneName), `${JSON.stringify(payload, null, 4)}\n`, 'utf8');
482
500
  }
483
501
 
502
+ removeSceneCliAttachConfig(sceneName) {
503
+ fs.rmSync(this.sceneCliAttachConfigPath(sceneName), { force: true });
504
+ }
505
+
484
506
  sceneInitScriptPath(sceneName) {
485
507
  const configFile = path.basename(this.sceneConfigPath(sceneName), '.json');
486
508
  return path.join(this.config.configDir, `${configFile}.init.js`);
487
509
  }
488
510
 
489
- legacySceneInitScriptPath(sceneName) {
490
- return path.join(this.config.configDir, `${sceneName}.init.js`);
491
- }
492
-
493
511
  buildInitScriptContent() {
494
512
  const lines = [
495
513
  "'use strict';",
@@ -540,10 +558,6 @@ class PlaywrightPlugin {
540
558
  const filePath = this.sceneInitScriptPath(sceneName);
541
559
  const content = this.buildInitScriptContent();
542
560
  fs.writeFileSync(filePath, content, 'utf8');
543
- const legacyFilePath = this.legacySceneInitScriptPath(sceneName);
544
- if (legacyFilePath !== filePath) {
545
- fs.rmSync(legacyFilePath, { force: true });
546
- }
547
561
  return filePath;
548
562
  }
549
563
 
@@ -900,7 +914,7 @@ class PlaywrightPlugin {
900
914
  PLAYWRIGHT_MCP_CONFIG_PATH: cfgPath,
901
915
  PLAYWRIGHT_MCP_CONTAINER_NAME: def.containerName,
902
916
  PLAYWRIGHT_MCP_IMAGE: this.config.headedImage,
903
- PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.contHeadedNoVnc)
917
+ PLAYWRIGHT_MCP_NOVNC_PORT: String(this.config.ports.mcpContHeadedNoVnc)
904
918
  };
905
919
 
906
920
  if (sceneName === 'mcp-cont-headed') {
@@ -1221,6 +1235,7 @@ class PlaywrightPlugin {
1221
1235
  const logFile = this.sceneLogFile(sceneName);
1222
1236
  const port = this.scenePort(sceneName);
1223
1237
  this.removeSceneEndpoint(sceneName);
1238
+ this.removeSceneCliAttachConfig(sceneName);
1224
1239
 
1225
1240
  let managedPids = this.hostScenePids(sceneName);
1226
1241
  if (managedPids.length > 0 && (await this.portReady(port))) {
@@ -1263,6 +1278,7 @@ class PlaywrightPlugin {
1263
1278
  if (managedPids.length > 0) {
1264
1279
  fs.writeFileSync(pidFile, `${managedPids[0]}`, 'utf8');
1265
1280
  this.writeStdout(`[up] ${sceneName} ready on 127.0.0.1:${port} (pid(s) ${managedPids.join(' ')})`);
1281
+ this.remindCliSessionScene(sceneName);
1266
1282
  return 0;
1267
1283
  }
1268
1284
  }
@@ -1285,6 +1301,7 @@ class PlaywrightPlugin {
1285
1301
  const port = this.scenePort(sceneName);
1286
1302
  const managedPids = this.hostScenePids(sceneName);
1287
1303
  this.removeSceneEndpoint(sceneName);
1304
+ this.removeSceneCliAttachConfig(sceneName);
1288
1305
 
1289
1306
  for (const pid of managedPids) {
1290
1307
  try {
@@ -1542,6 +1559,25 @@ class PlaywrightPlugin {
1542
1559
  return 0;
1543
1560
  }
1544
1561
 
1562
+ printCliAdd() {
1563
+ const lines = [
1564
+ 'PLAYWRIGHT_CLI_INSTALL_DIR="${TMPDIR:-/tmp}/manyoyo-playwright-cli-install-$$"',
1565
+ 'mkdir -p "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright"',
1566
+ 'echo \'{"browser":{"browserName":"chromium","launchOptions":{"channel":"chromium"}}}\' > "$PLAYWRIGHT_CLI_INSTALL_DIR/.playwright/cli.config.json"',
1567
+ 'cd "$PLAYWRIGHT_CLI_INSTALL_DIR"',
1568
+ `npm install -g @playwright/cli@${PLAYWRIGHT_CLI_VERSION}`,
1569
+ 'playwright-cli install --skills',
1570
+ 'mkdir -p ~/.codex/skills/playwright-cli ~/.gemini/skills/playwright-cli',
1571
+ 'cp -R "$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli/." ~/.claude/skills/playwright-cli/',
1572
+ 'cp -R "$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli/." ~/.codex/skills/playwright-cli/',
1573
+ 'cp -R "$PLAYWRIGHT_CLI_INSTALL_DIR/.claude/skills/playwright-cli/." ~/.gemini/skills/playwright-cli/',
1574
+ 'cd $OLDPWD',
1575
+ 'rm -rf "$PLAYWRIGHT_CLI_INSTALL_DIR"'
1576
+ ];
1577
+ this.writeStdout(lines.join('\n'));
1578
+ return 0;
1579
+ }
1580
+
1545
1581
  printSummary() {
1546
1582
  const scenes = this.resolveTargets('all');
1547
1583
  this.writeStdout(`playwright\truntime=${this.config.runtime}\tscenes=${scenes.join(',')}`);
@@ -1586,6 +1622,10 @@ class PlaywrightPlugin {
1586
1622
  return this.printMcpAdd(host);
1587
1623
  }
1588
1624
 
1625
+ if (action === 'cli-add') {
1626
+ return this.printCliAdd();
1627
+ }
1628
+
1589
1629
  if (action === 'ext-download') {
1590
1630
  return await this.downloadExtensions({ prodversion });
1591
1631
  }
@@ -64,13 +64,13 @@
64
64
  // 是否禁用 WebRTC(默认 false)
65
65
  "disableWebRTC": false,
66
66
  "ports": {
67
- "contHeadless": 8931,
68
- "contHeaded": 8932,
69
- "hostHeadless": 8933,
70
- "hostHeaded": 8934,
67
+ "mcpContHeadless": 8931,
68
+ "mcpContHeaded": 8932,
69
+ "mcpHostHeadless": 8933,
70
+ "mcpHostHeaded": 8934,
71
71
  "cliHostHeadless": 8935,
72
72
  "cliHostHeaded": 8936,
73
- "contHeadedNoVnc": 6080
73
+ "mcpContHeadedNoVnc": 6080
74
74
  }
75
75
  }
76
76
  },
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "5.4.4",
4
- "imageVersion": "1.8.8-common",
3
+ "version": "5.4.12",
4
+ "imageVersion": "1.8.12-common",
5
+ "playwrightCliVersion": "0.1.1",
5
6
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
7
  "keywords": [
7
8
  "manyoyo",