@xcanwin/manyoyo 8.0.1 → 8.2.0

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 CHANGED
@@ -7,7 +7,7 @@ const os = require('os');
7
7
  const crypto = require('crypto');
8
8
  const net = require('net');
9
9
  const readline = require('readline');
10
- const { Command } = require('commander');
10
+ const { Command, Help } = require('commander');
11
11
  const { startWebServer } = require('../lib/web/server');
12
12
  const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
13
13
  const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
@@ -50,8 +50,7 @@ const {
50
50
  } = require('../lib/serve-log');
51
51
  const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
52
52
  const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
53
- const GLOBAL_CONFIG_IMAGE_VERSION = String(readManyoyoConfig().config.imageVersion || '').trim();
54
- const IMAGE_VERSION_HELP_EXAMPLE = GLOBAL_CONFIG_IMAGE_VERSION || IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
53
+ const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
55
54
 
56
55
  // Helper function to format date like bash $(date +%m%d-%H%M)
57
56
  function formatDate() {
@@ -71,11 +70,6 @@ function detectCommandName() {
71
70
  const rawArgv1 = process.argv[1] || '';
72
71
  const baseName = path.basename(rawArgv1).replace(/\.(cjs|mjs|js)$/i, '');
73
72
 
74
- if (baseName === 'docker-manyoyo') {
75
- const pluginCommand = String(process.argv[2] || '').trim();
76
- return pluginCommand || 'manyoyo';
77
- }
78
-
79
73
  return baseName || 'manyoyo';
80
74
  }
81
75
 
@@ -472,7 +466,7 @@ function parseImageVersionTag(version) {
472
466
  function validateImageVersion(value) {
473
467
  validateName('imageVersion', value, /^[A-Za-z0-9][A-Za-z0-9_.-]*$/);
474
468
  if (!parseImageVersionTag(value)) {
475
- console.error(`${RED}⚠️ 错误: imageVersion 格式必须为 <x.y.z-后缀>,例如 1.7.4-common。当前值: ${value}${NC}`);
469
+ console.error(`${RED}⚠️ 错误: imageVersion 格式必须为 <x.y.z-后缀>,例如 ${IMAGE_VERSION_HELP_EXAMPLE}。当前值: ${value}${NC}`);
476
470
  process.exit(1);
477
471
  }
478
472
  }
@@ -849,24 +843,6 @@ async function ensureDocker(configuredRuntime, options = {}) {
849
843
  return true;
850
844
  }
851
845
 
852
- function installManyoyo(name) {
853
- const MANYOYO_FILE = fs.realpathSync(__filename);
854
- switch (name) {
855
- case 'docker-cli-plugin':
856
- const pluginDir = path.join(process.env.HOME, '.docker/cli-plugins');
857
- fs.mkdirSync(pluginDir, { recursive: true });
858
- const targetPath = path.join(pluginDir, 'docker-manyoyo');
859
- if (fs.existsSync(targetPath)) {
860
- fs.unlinkSync(targetPath);
861
- }
862
- fs.symlinkSync(MANYOYO_FILE, targetPath);
863
- break;
864
- default:
865
- console.log("");
866
- }
867
- process.exit(0);
868
- }
869
-
870
846
  // 升级后让旧版本的后台服务退出(pid 必须确实是 manyoyo serve),下次执行 manyoyo 就会启动新版本
871
847
  function stopBackgroundApp({ quiet = false } = {}) {
872
848
  const pid = readPid(path.join(os.homedir(), '.manyoyo', 'serve', 'app.json'));
@@ -918,16 +894,8 @@ async function updateOfflineInstall(mode, options, globalConfig) {
918
894
  console.log(`${GREEN}✅ 更新完成: ${installed} → ${result.version}(上一版本 ${result.previous || '无'} 已保留,可用 ${MANYOYO_NAME} update --rollback 回滚)${NC}`);
919
895
 
920
896
  // Podman / VM 磁盘有变化只提示(需要新的完整包)
921
- try {
922
- const names = appUpdate.releaseAssetNames(release.version, appUpdate.arch(), appUpdate.platformOs());
923
- if (release.assets[names.manifest]) {
924
- const remote = JSON.parse(await appUpdate.fetchText(fetch, release.assets[names.manifest]));
925
- const hint = appUpdate.describeRuntimeChange(appUpdate.readInstalledRecord(os.homedir()), remote);
926
- if (hint) console.log(`${YELLOW}ℹ️ ${hint}${NC}`);
927
- }
928
- } catch (error) {
929
- // 提示信息拿不到不影响升级
930
- }
897
+ const runtimeHint = appUpdate.describeRuntimeChange(appUpdate.readInstalledRecord(os.homedir()), result.manifest.runtime);
898
+ if (runtimeHint && appUpdate.markRuntimeHintShown(os.homedir(), result.manifest.runtime)) console.log(`${YELLOW}ℹ️ ${runtimeHint}${NC}`);
931
899
 
932
900
  // 新版本要求的镜像不存在就拉取;仍用旧镜像的容器只列出来,不动
933
901
  const imageName = String(globalConfig.imageName || 'ghcr.io/xcanwin/manyoyo');
@@ -1102,26 +1070,6 @@ function pruneDanglingImages() {
1102
1070
  });
1103
1071
  }
1104
1072
 
1105
- function maybeHandleDockerPluginMetadata(argv) {
1106
- if (argv[2] !== 'docker-cli-plugin-metadata') {
1107
- return false;
1108
- }
1109
- console.log(JSON.stringify({
1110
- "SchemaVersion": "0.1.0",
1111
- "Vendor": "xcanwin",
1112
- "Version": "v1.0.0",
1113
- "Description": "AI Agent CLI Sandbox"
1114
- }, null, 4));
1115
- return true;
1116
- }
1117
-
1118
- function normalizeDockerPluginArgv(argv) {
1119
- const dockerPluginPath = path.join(process.env.HOME || '', '.docker/cli-plugins/docker-manyoyo');
1120
- if (argv[1] === dockerPluginPath && argv[2] === 'manyoyo') {
1121
- argv.splice(2, 1);
1122
- }
1123
- }
1124
-
1125
1073
  function normalizeShellFullArgv(argv) {
1126
1074
  const shellFullIndex = argv.findIndex(arg => arg === '-x' || arg === '--shell-full');
1127
1075
  if (shellFullIndex !== -1 && shellFullIndex < argv.length - 1) {
@@ -1183,7 +1131,7 @@ function applyRunStyleOptions(command, options = {}) {
1183
1131
  .option('--cp, --cont-path <path>', '设置容器工作目录')
1184
1132
  .option('-m, --cont-mode <mode>', '设置容器嵌套模式 (common, dind, sock; 注意: sock 模式可访问宿主机 Docker socket,风险较高)')
1185
1133
  .option('--in, --image-name <name>', '指定镜像名称')
1186
- .option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)');
1134
+ .option('--iv, --image-ver <version>', `指定镜像版本 (格式: x.y.z-后缀,如 ${IMAGE_VERSION_HELP_EXAMPLE})`);
1187
1135
 
1188
1136
  appendArrayOption(command, '-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)');
1189
1137
  appendArrayOption(command, '--ef, --env-file <file>', '从环境文件加载变量 (仅支持绝对路径,如 /abs/path.env; 相对路径会报错)');
@@ -1200,7 +1148,7 @@ function applyRunStyleOptions(command, options = {}) {
1200
1148
  .option('--first-shell <command>', '首次预执行命令 (仅新建容器生效; 容器已存在时忽略)')
1201
1149
  .option('--first-shell-suffix <command>', '首次预执行命令后缀 (仅新建容器生效; 容器已存在时忽略)')
1202
1150
  .option('-x, --shell-full <command...>', '完整命令 (与 --sp/-s/--ss/-- 互斥)')
1203
- .option('-y, --yolo <cli>', '使 AGENT 无需确认 (claude(c), gemini(gm), codex(cx), opencode(oc))');
1151
+ .option('-y, --yolo <cli>', '以免确认模式启动 Agent: c=Claude, cx=Codex, gm=Gemini, oc=OpenCode');
1204
1152
  appendArrayOption(command, '--first-env <env>', '首次预执行环境变量 XXX=YYY (可多次使用)');
1205
1153
  appendArrayOption(command, '--first-env-file <file>', '首次预执行环境变量文件 (仅支持绝对路径,如 /abs/path.env)');
1206
1154
 
@@ -1208,7 +1156,7 @@ function applyRunStyleOptions(command, options = {}) {
1208
1156
  command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
1209
1157
  }
1210
1158
 
1211
- appendArrayOption(command, '-q, --quiet <item>', '静默输出 (可多次使用: cnew, crm, tip, cmd, full)');
1159
+ appendArrayOption(command, '-q, --quiet <item>', '隐藏部分输出 (可多次使用,可选项见下方“-q 可选项”)');
1212
1160
 
1213
1161
  if (includeServePreview) {
1214
1162
  command
@@ -1313,46 +1261,82 @@ async function setupCommander() {
1313
1261
 
1314
1262
  program
1315
1263
  .name(MANYOYO_NAME)
1264
+ .optionsGroup('选项:')
1265
+ .commandsGroup('其他:')
1316
1266
  .version(BIN_VERSION, '-v, --version', '显示版本')
1317
- .description('MANYOYO - AI Agent CLI Sandbox\nhttps://github.com/xcanwin/manyoyo')
1267
+ .helpOption('-h, --help', '显示帮助')
1268
+ .helpCommand('help [command]', '显示指定命令的帮助')
1269
+ .configureHelp({
1270
+ optionDescription(option) {
1271
+ return Help.prototype.optionDescription.call(this, option).replace(/\s*\(default: \[\]\)/, '');
1272
+ }
1273
+ })
1274
+ .description(`MANYOYO - AI Agent CLI Sandbox
1275
+ https://github.com/xcanwin/manyoyo
1276
+
1277
+ 不带参数直接运行,打开网页界面(首次使用进入配置向导):
1278
+ ${MANYOYO_NAME} 自动判断有无图形界面
1279
+ ${MANYOYO_NAME} --headless 强制按无图形界面处理(不打开浏览器,用密码登录)
1280
+ ${MANYOYO_NAME} --gui 强制按有图形界面处理(自动打开浏览器)`)
1318
1281
  .addHelpText('after', `
1319
- 配置文件:
1320
- ~/.manyoyo/manyoyo.json 全局配置文件 (JSON5格式,支持注释)
1321
- ~/.manyoyo/run/c.json 运行配置示例
1322
-
1323
- 路径规则:
1324
- run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
1325
- run --ef /abs/path.env → 绝对路径环境文件
1326
- run --ss "<args>" → 显式设置命令后缀
1327
- run -- <args...> → 直接透传命令后缀(优先级最高)
1328
-
1329
1282
  示例:
1330
- ${MANYOYO_NAME} update 更新 MANYOYO 到最新版本
1331
- ${MANYOYO_NAME} build --iv ${IMAGE_VERSION_HELP_EXAMPLE} --yes 构建镜像
1332
- ${MANYOYO_NAME} build --update-agents --yes 仅更新已有镜像内已存在的 Agent CLI
1333
- ${MANYOYO_NAME} init all 从本机 Agent 配置初始化 ~/.manyoyo
1334
- ${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 快速启动
1335
- ${MANYOYO_NAME} run -r codex --ss "resume --last" 使用命令后缀
1336
- ${MANYOYO_NAME} run -n test --ef /path/ab.env -y c 使用绝对路径环境变量文件
1337
- ${MANYOYO_NAME} run -n test -- -c 恢复之前会话
1338
- ${MANYOYO_NAME} run -x "echo 123" 使用完整命令
1339
- ${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
1340
- ${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
1341
- ${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
1342
- ${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d --restart 重启指定后台网页服务
1343
- ${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright MCP 宿主场景(默认/推荐)
1344
- ${MANYOYO_NAME} playwright up cli-host-headless 启动 playwright CLI 宿主场景(供容器内 playwright-cli 附着)
1345
- ${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
1283
+ ${MANYOYO_NAME} 打开网页界面
1284
+ ${MANYOYO_NAME} init all 导入本机已有的 Agent 配置
1285
+ ${MANYOYO_NAME} run -y c 在沙箱里以免确认模式启动 Claude Code
1286
+ ${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 启动
1287
+ ${MANYOYO_NAME} update 升级到最新版本
1288
+
1289
+ 配置文件: ~/.manyoyo/manyoyo.json (JSON5 格式,支持注释)
1290
+ 各命令的更多用法: ${MANYOYO_NAME} <命令> --help
1346
1291
  `);
1347
1292
 
1348
- const runCommand = program.command('run').description('启动(容器不存在时)或连接(容器已存在时)容器并执行命令');
1293
+ // 日常
1294
+ program.command('update')
1295
+ .helpGroup('日常:')
1296
+ .description('升级到最新版本')
1297
+ .option('--rollback', '回到上一版本')
1298
+ .action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback) }));
1299
+
1300
+ program.command('uninstall')
1301
+ .helpGroup('日常:')
1302
+ .description('卸载 MANYOYO(配置和数据默认保留)')
1303
+ .option('--yes', '确认卸载程序本身,不再询问(不会删除配置、历史、日志、工作目录和外部运行时里的容器镜像)')
1304
+ .action(options => selectAction('uninstall', options));
1305
+
1306
+ program.command('setup')
1307
+ .helpGroup('日常:')
1308
+ .description('命令行配置向导(无图形界面时使用)')
1309
+ .action(() => selectAction('setup', {}));
1310
+
1311
+ program.command('doctor')
1312
+ .helpGroup('日常:')
1313
+ .description('诊断容器运行时、镜像、配置和端口')
1314
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1315
+ .option('--port <port>', '检查指定监听端口')
1316
+ .option('--json', '以 JSON 输出稳定诊断结果')
1317
+ .option('--fix', '自动修复可修复项(启动容器环境、拉取镜像、生成默认配置,端口占用时给出建议端口)')
1318
+ .action(options => selectAction('doctor', { ...options, doctor: true }));
1319
+
1320
+ // 命令行运行
1321
+ const runCommand = program.command('run').helpGroup('命令行运行:').description('启动容器并运行命令(容器已存在则连接)');
1349
1322
  runCommand.addHelpText('after', `
1350
- Examples:
1323
+ 示例:
1351
1324
  ${MANYOYO_NAME} run -r codex
1325
+ ${MANYOYO_NAME} run -n test --ef /path/ab.env -y c
1326
+ ${MANYOYO_NAME} run -n test -- -c
1327
+ ${MANYOYO_NAME} run -r codex --ss "resume --last"
1352
1328
  ${MANYOYO_NAME} run --rm-on-exit -x /bin/bash -lc "node -v"
1353
1329
  ${MANYOYO_NAME} run -n demo --first-shell "npm ci" -s "npm test"
1354
1330
 
1355
- Notes:
1331
+ -q 可选项(可多次使用):
1332
+ cnew 隐藏“创建/连接容器”提示
1333
+ crm 隐藏“删除容器”提示
1334
+ tip 隐藏首次命令与恢复会话提示
1335
+ cmd 隐藏将执行的命令
1336
+ askkeep 简化“是否保留容器”的提问
1337
+ full 隐藏以上全部
1338
+
1339
+ 说明:
1356
1340
  参数优先级与合并规则(标量覆盖、数组追加、env 按 key 合并)请用 ${MANYOYO_NAME} config show --help 或查看文档。
1357
1341
  `);
1358
1342
  applyRunStyleOptions(runCommand);
@@ -1362,60 +1346,13 @@ Notes:
1362
1346
  selectAction('run', options);
1363
1347
  });
1364
1348
 
1365
- const buildCommand = program.command('build').description('构建 manyoyo 沙箱镜像');
1366
- buildCommand
1367
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1368
- .option('--in, --image-name <name>', '指定镜像名称')
1369
- .option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)')
1370
- .option('--update-agents', '仅更新已有镜像内 Agent CLI 到 latest (Claude/Codex/Gemini/OpenCode)')
1371
- .option('--yes', '所有提示自动确认 (用于CI/脚本)');
1372
- appendArrayOption(buildCommand, '--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)');
1373
- buildCommand.action(options => selectAction('build', options));
1374
-
1375
- const removeCommand = program.command('rm <name>').description('删除指定容器');
1376
- removeCommand
1377
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1378
- .action((name, options) => selectAction('rm', { ...options, contName: name }));
1379
-
1380
- program.command('ps')
1381
- .description('列举容器')
1382
- .action(() => selectAction('ps', { contList: true }));
1383
-
1384
- program.command('images')
1385
- .description('列举镜像')
1386
- .action(() => selectAction('images', { imageList: true }));
1387
-
1388
- const serveCommand = program.command('serve [listen]').description('启动网页交互服务 (默认 127.0.0.1:3000)');
1389
- applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
1390
- serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
1391
- serveCommand.option('--stop', '停止后台网页服务;必须显式传入 listen');
1392
- serveCommand.option('--restart', '重启后台网页服务;必须显式传入 listen');
1393
- serveCommand.action((listen, options) => {
1394
- selectAction('serve', {
1395
- ...options,
1396
- server: listen === undefined ? true : listen,
1397
- serverUser: options.user,
1398
- serverPass: options.pass
1399
- });
1400
- });
1401
-
1402
- const playwrightCommand = program.command('playwright').description('管理 playwright 插件服务(推荐)');
1403
- registerPlaywrightAliasCommands(playwrightCommand);
1404
-
1405
- const pluginCommand = program.command('plugin').description('管理 manyoyo 插件');
1406
- pluginCommand.command('ls')
1407
- .description('列出可用插件与启用场景')
1408
- .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1409
- .action(options => selectPluginAction({
1410
- action: 'ls',
1411
- pluginName: 'playwright',
1412
- scene: 'all'
1413
- }, options));
1414
- const pluginPlaywrightCommand = pluginCommand.command('playwright').description('管理 playwright 插件服务');
1415
- registerPlaywrightAliasCommands(pluginPlaywrightCommand);
1349
+ const initCommand = program.command('init [agents]').helpGroup('命令行运行:').description('导入本机已有的 Agent 配置到 ~/.manyoyo');
1350
+ initCommand
1351
+ .option('--yes', '所有提示自动确认 (用于CI/脚本)')
1352
+ .action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
1416
1353
 
1417
- const configCommand = program.command('config').description('查看解析后的配置或命令');
1418
- const configShowCommand = configCommand.command('show').description('显示最终生效配置并退出');
1354
+ const configCommand = program.command('config').helpGroup('命令行运行:').description('查看生效的配置或将执行的命令');
1355
+ const configShowCommand = configCommand.command('show').description('显示最终生效的配置');
1419
1356
  applyRunStyleOptions(configShowCommand, { includeRmOnExit: false, includeServePreview: true });
1420
1357
  enableShellSuffixPassThrough(configShowCommand);
1421
1358
  configShowCommand.action((options, command) => {
@@ -1432,7 +1369,7 @@ Notes:
1432
1369
  selectAction('config-show', finalOptions);
1433
1370
  });
1434
1371
 
1435
- const configRunCommand = configCommand.command('command').description('显示将执行的 docker run 命令并退出');
1372
+ const configRunCommand = configCommand.command('command').description('显示将执行的容器命令');
1436
1373
  applyRunStyleOptions(configRunCommand, { includeRmOnExit: false });
1437
1374
  enableShellSuffixPassThrough(configRunCommand);
1438
1375
  configRunCommand.action((options, command) => {
@@ -1440,55 +1377,86 @@ Notes:
1440
1377
  selectAction('config-command', options);
1441
1378
  });
1442
1379
 
1443
- program.command('uninstall')
1444
- .description('卸载离线包安装的 MANYOYO:停止服务,删除程序与 PATH 配置,用户数据逐项询问(--yes 不会删用户数据)')
1445
- .option('--yes', '确认卸载程序本身,不再询问(不会删除配置、历史、日志、工作目录和外部运行时里的容器镜像)')
1446
- .action(options => selectAction('uninstall', options));
1380
+ // 容器与镜像
1381
+ program.command('ps')
1382
+ .helpGroup('容器与镜像:')
1383
+ .description('列出容器')
1384
+ .action(() => selectAction('ps', { contList: true }));
1447
1385
 
1448
- const initCommand = program.command('init [agents]').description('初始化 Agent 配置到 ~/.manyoyo');
1449
- initCommand
1450
- .option('--yes', '所有提示自动确认 (用于CI/脚本)')
1451
- .action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
1386
+ program.command('images')
1387
+ .helpGroup('容器与镜像:')
1388
+ .description('列出镜像')
1389
+ .action(() => selectAction('images', { imageList: true }));
1452
1390
 
1453
- program.command('doctor')
1454
- .description('诊断容器运行时、镜像、配置、Agent、模式、插件和端口')
1391
+ const removeCommand = program.command('rm <name>').helpGroup('容器与镜像:').description('删除指定容器');
1392
+ removeCommand
1455
1393
  .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1456
- .option('--port <port>', '检查指定监听端口')
1457
- .option('--json', '以 JSON 输出稳定诊断结果')
1458
- .option('--fix', '自动修复可修复项(启动容器环境、拉取镜像、生成默认配置,端口占用时给出建议端口)')
1459
- .action(options => selectAction('doctor', { ...options, doctor: true }));
1394
+ .action((name, options) => selectAction('rm', { ...options, contName: name }));
1460
1395
 
1461
- program.command('update')
1462
- .description('更新 MANYOYO(离线包安装只下载变化部分并保留上一版本;若检测为本地 file 安装则跳过)')
1463
- .option('--rollback', '离线包安装:切回上一版本')
1464
- .action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback) }));
1396
+ const buildCommand = program.command('build').helpGroup('容器与镜像:').description('构建沙箱镜像');
1397
+ buildCommand
1398
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1399
+ .option('--in, --image-name <name>', '指定镜像名称')
1400
+ .option('--iv, --image-ver <version>', `指定镜像版本 (格式: x.y.z-后缀,如 ${IMAGE_VERSION_HELP_EXAMPLE})`)
1401
+ .option('--update-agents', '仅更新已有镜像内 Agent CLI 到 latest (Claude/Codex/Gemini/OpenCode)')
1402
+ .option('--yes', '所有提示自动确认 (用于CI/脚本)');
1403
+ appendArrayOption(buildCommand, '--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)');
1404
+ buildCommand.action(options => selectAction('build', options));
1405
+
1406
+ program.command('prune')
1407
+ .helpGroup('容器与镜像:')
1408
+ .description('清理悬空镜像')
1409
+ .action(() => selectAction('prune', { imageRemove: true }));
1465
1410
 
1466
1411
  program.command('podman [args...]')
1467
- .description('用 MANYOYO 的私有 Podman 执行命令(参数原样传给 podman);eval "$(manyoyo podman env)" 可在当前终端定义 podman 函数')
1412
+ .helpGroup('容器与镜像:')
1413
+ .description('用私有 Podman 执行命令(参数原样传入)')
1468
1414
  .helpOption(false)
1469
1415
  .allowUnknownOption()
1470
1416
  .passThroughOptions()
1471
1417
  .action(args => selectAction('podman', { podmanArgs: args || [] }));
1472
1418
 
1473
- program.command('setup')
1474
- .description('命令行配置向导(无头环境:SSH、没有图形界面):选 Agent、填 Key、设登录密码,可选软件源')
1475
- .action(() => selectAction('setup', {}));
1476
-
1477
- program.command('install <name>')
1478
- .description(`安装 ${MANYOYO_NAME} 命令 (docker-cli-plugin)`)
1479
- .action(name => selectAction('install', { install: name }));
1480
-
1481
- program.command('prune')
1482
- .description('清理悬空镜像和 <none> 镜像')
1483
- .action(() => selectAction('prune', { imageRemove: true }));
1419
+ // 网页服务与插件
1420
+ const serveCommand = program.command('serve [listen]').helpGroup('网页服务与插件:').description('启动网页服务 (默认 127.0.0.1:3000)');
1421
+ serveCommand.addHelpText('after', `
1422
+ 示例:
1423
+ ${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
1424
+ ${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
1425
+ ${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P <密码> -d 后台启动并监听全部网卡
1426
+ ${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P <密码> -d --restart 重启指定后台网页服务
1427
+ `);
1428
+ applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
1429
+ serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
1430
+ serveCommand.option('--stop', '停止后台网页服务;必须显式传入 listen');
1431
+ serveCommand.option('--restart', '重启后台网页服务;必须显式传入 listen');
1432
+ serveCommand.action((listen, options) => {
1433
+ selectAction('serve', {
1434
+ ...options,
1435
+ server: listen === undefined ? true : listen,
1436
+ serverUser: options.user,
1437
+ serverPass: options.pass
1438
+ });
1439
+ });
1484
1440
 
1485
- // Docker CLI plugin metadata check
1486
- if (maybeHandleDockerPluginMetadata(process.argv)) {
1487
- process.exit(0);
1488
- }
1441
+ const playwrightCommand = program.command('playwright').helpGroup('网页服务与插件:').description('管理 Playwright 插件服务');
1442
+ playwrightCommand.addHelpText('after', `
1443
+ 示例:
1444
+ ${MANYOYO_NAME} playwright up mcp-host-headless 启动 MCP 宿主场景(默认/推荐)
1445
+ ${MANYOYO_NAME} playwright up cli-host-headless 启动 CLI 宿主场景(供容器内 playwright-cli 附着)
1446
+ `);
1447
+ registerPlaywrightAliasCommands(playwrightCommand);
1489
1448
 
1490
- // Docker CLI plugin mode - remove first arg if running as plugin
1491
- normalizeDockerPluginArgv(process.argv);
1449
+ const pluginCommand = program.command('plugin', { hidden: true }).description('管理 manyoyo 插件');
1450
+ pluginCommand.command('ls')
1451
+ .description('列出可用插件与启用场景')
1452
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
1453
+ .action(options => selectPluginAction({
1454
+ action: 'ls',
1455
+ pluginName: 'playwright',
1456
+ scene: 'all'
1457
+ }, options));
1458
+ const pluginPlaywrightCommand = pluginCommand.command('playwright').description('管理 playwright 插件服务');
1459
+ registerPlaywrightAliasCommands(pluginPlaywrightCommand);
1492
1460
 
1493
1461
  // No args (or only --headless / --gui): start (or reuse) the local web app; open the browser already logged in unless headless
1494
1462
  const launcherArgs = parseLauncherArgs(process.argv.slice(2));
@@ -1528,7 +1496,7 @@ Notes:
1528
1496
 
1529
1497
  UPDATE_CHECK_ENABLED = config.updateCheck !== false;
1530
1498
  MIRRORS = normalizeMirrors(config.mirrors);
1531
- const noDockerActions = new Set(['init', 'update', 'install', 'config-show', 'plugin', 'doctor', 'uninstall', 'podman', 'setup']);
1499
+ const noDockerActions = new Set(['init', 'update', 'config-show', 'plugin', 'doctor', 'uninstall', 'podman', 'setup']);
1532
1500
  if (isServerStopMode) {
1533
1501
  noDockerActions.add('serve');
1534
1502
  }
@@ -1831,7 +1799,6 @@ Notes:
1831
1799
  if (isPsMode) { getContList(); process.exit(0); }
1832
1800
  if (isImagesMode) { getImageList(); process.exit(0); }
1833
1801
  if (isPruneMode) { pruneDanglingImages(); process.exit(0); }
1834
- if (selectedAction === 'install') { installManyoyo(options.install); process.exit(0); }
1835
1802
 
1836
1803
  return {
1837
1804
  yesMode,
package/lib/app-update.js CHANGED
@@ -125,11 +125,11 @@ function parseSha256Sums(text) {
125
125
  return result;
126
126
  }
127
127
 
128
+ // Release 里只有一个校验清单 SHA256SUMS,列出全部安装包与升级包
128
129
  function releaseAssetNames(version, targetArch, targetOs = 'macos') {
129
130
  return {
130
131
  app: `manyoyo-${version}-${targetOs}-${targetArch}-app.tar.gz`,
131
- sums: `SHA256SUMS-${targetOs}-${targetArch}`,
132
- manifest: `release-manifest-${targetOs}-${targetArch}.json`
132
+ sums: 'SHA256SUMS'
133
133
  };
134
134
  }
135
135
 
@@ -189,7 +189,7 @@ function pruneApps(appRoot, keep) {
189
189
  async function installAppUpdate({ appRoot, release, fetchImpl = fetch, targetArch = arch(), targetOs = 'macos', run = defaultRun, log = () => {}, tmpRoot = os.tmpdir() }) {
190
190
  const names = releaseAssetNames(release.version, targetArch, targetOs);
191
191
  const appUrl = release.assets[names.app];
192
- const sumsUrl = release.assets[names.sums] || release.assets.SHA256SUMS;
192
+ const sumsUrl = release.assets[names.sums];
193
193
  if (!appUrl || !sumsUrl) {
194
194
  throw new UpdateError('NO_ASSET', `Release ${release.version} 里没有适合这台机器(${targetOs}-${targetArch})的升级包(需要 ${names.app} 与 ${names.sums})。`);
195
195
  }
@@ -262,20 +262,40 @@ function rollbackApp({ appRoot }) {
262
262
 
263
263
  /**
264
264
  * Podman / VM 磁盘版本变化只提示,不自动替换(需要下载新的完整包)。
265
- * installed:安装器写的 ~/.manyoyo/.install/installed.json;remoteManifest:Release 里的 release-manifest(含完整包 manifest)。
265
+ * installed:安装器写的 ~/.manyoyo/.install/installed.json;remoteRuntime:升级包 manifest.json 里的 runtime({ podmanVersion, vmDiskSha256 },仅 macOS 包有)。
266
266
  */
267
- function describeRuntimeChange(installed, remoteManifest) {
268
- const remote = remoteManifest && remoteManifest.manifest && remoteManifest.manifest.components;
269
- if (!installed || !installed.podmanVersion || !remote || !remote.podman) return '';
270
- const podmanChanged = remote.podman.version !== installed.podmanVersion;
271
- const vmChanged = Boolean(installed.vmDiskSha256) && remote.vmDisk && remote.vmDisk.sha256 !== installed.vmDiskSha256;
267
+ function describeRuntimeChange(installed, remoteRuntime) {
268
+ if (!installed || !installed.podmanVersion || !remoteRuntime || !remoteRuntime.podmanVersion) return '';
269
+ const podmanChanged = remoteRuntime.podmanVersion !== installed.podmanVersion;
270
+ const vmChanged = Boolean(installed.vmDiskSha256) && Boolean(remoteRuntime.vmDiskSha256) && remoteRuntime.vmDiskSha256 !== installed.vmDiskSha256;
272
271
  if (!podmanChanged && !vmChanged) return '';
273
272
  const parts = [];
274
- if (podmanChanged) parts.push(`Podman ${installed.podmanVersion} → ${remote.podman.version}`);
273
+ if (podmanChanged) parts.push(`Podman ${installed.podmanVersion} → ${remoteRuntime.podmanVersion}`);
275
274
  if (vmChanged) parts.push('虚拟机磁盘有更新');
276
275
  return `新版本捆绑的运行环境有变化(${parts.join(',')})。本次只升级了 manyoyo 本体;要换 Podman / 虚拟机,请下载新的完整安装包重新安装。`;
277
276
  }
278
277
 
278
+ /**
279
+ * 同一份新运行环境的提示只显示一次(每次 update 都重复提示只是噪音):
280
+ * 记下已提示过的 runtime(~/.manyoyo/.install/runtime-hint.json),返回这次是否应该显示。
281
+ */
282
+ function markRuntimeHintShown(homeDir, runtime) {
283
+ const file = path.join(homeDir, '.manyoyo', '.install', 'runtime-hint.json');
284
+ const key = JSON.stringify({ podmanVersion: runtime && runtime.podmanVersion || '', vmDiskSha256: runtime && runtime.vmDiskSha256 || '' });
285
+ try {
286
+ if (fs.readFileSync(file, 'utf-8').trim() === key) return false;
287
+ } catch (error) {
288
+ // 没有记录或记录损坏:当作没提示过
289
+ }
290
+ try {
291
+ fs.mkdirSync(path.dirname(file), { recursive: true });
292
+ fs.writeFileSync(file, `${key}\n`);
293
+ } catch (error) {
294
+ // 记录写不进去不影响升级,最多下次再提示一次
295
+ }
296
+ return true;
297
+ }
298
+
279
299
  function readInstalledRecord(homeDir) {
280
300
  try {
281
301
  return JSON.parse(fs.readFileSync(path.join(homeDir, '.manyoyo', '.install', 'installed.json'), 'utf-8'));
@@ -317,6 +337,7 @@ module.exports = {
317
337
  rollbackApp,
318
338
  currentVersion,
319
339
  describeRuntimeChange,
340
+ markRuntimeHintShown,
320
341
  readInstalledRecord,
321
342
  findOutdatedContainers,
322
343
  fetchText,
@@ -48,14 +48,14 @@ function runPodmanCommand(args, options = {}) {
48
48
  const commandName = options.commandName || 'manyoyo';
49
49
  const paths = getPrivatePodmanPaths(options.homeDir);
50
50
  const exists = options.exists || (file => require('fs').existsSync(file));
51
- if (!exists(paths.bin)) {
52
- errLog(`没有找到 ${commandName} 的私有 Podman(${paths.bin})。当前没有使用私有 Podman:离线完整包安装后才有;使用你自己的 docker / podman 时直接用它们即可。`);
53
- return 1;
54
- }
55
51
  if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
56
52
  log(usageText(commandName));
57
53
  return 0;
58
54
  }
55
+ if (!exists(paths.bin)) {
56
+ errLog(`没有找到 ${commandName} 的私有 Podman(${paths.bin})。当前没有使用私有 Podman:离线完整包安装后才有;使用你自己的 docker / podman 时直接用它们即可。`);
57
+ return 1;
58
+ }
59
59
  if (args[0] === 'env') {
60
60
  const rest = args.slice(1);
61
61
  let shell = '';
package/lib/web/AGENTS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # lib/web/ 协作指引
2
2
 
3
- 根目录 `AGENTS.md` 的补充,仅在改动 Web 服务端时适用。前端在 `frontend-shadcn/`,见该目录的 `AGENTS.md`。
3
+ 根目录 `AGENTS.md` 的补充,仅在改动 Web 服务端时适用。前端在 `frontend/`,见该目录的 `AGENTS.md`。
4
4
 
5
5
  ## lib/web/server.js(6000+ 行单文件)
6
6
 
@@ -9,7 +9,7 @@
9
9
  - `resolveYoloCommand()` 委托到 `lib/agent-adapters/index.js`,与 `bin/manyoyo.js` 共用同一份映射,无需分别维护。
10
10
  - Agent 会话恢复参数:Claude/Gemini → `-r`,Codex → `resume`,OpenCode → `-c`。
11
11
  - 会话控制事件:`/agent/stream`、`/agent/stop` 通过 `createWebStreamEmitter()` / `appendWebSessionControlEvent()` 写入 `lib/core/event-store.js`(`FileEventStore`),`GET /api/sessions/:name/audit` 导出该会话的事件与投影。
12
- - **`/agent/stream` 的 NDJSON 事件协议是跨前后端的契约**,改一处必须同步另一处:服务端 `lib/web/server.js`、前端 `frontend-shadcn/src/lib/api.ts` 的 `StreamEvent` + `workspace-panel.tsx` 的事件分支
12
+ - **`/agent/stream` 的 NDJSON 事件协议是跨前后端的契约**,改一处必须同步另一处:服务端 `lib/web/server.js`、前端 `frontend/src/lib/api.ts` 的 `StreamEvent` + `workspace-panel.tsx` 的事件分支
13
13
  - `content_chunk`:token 级增量,前端**追加**;`reset: true` 表示换了一条 assistant 消息、从空白重新开始。只发新增片段,不要改成重发累计全文(几千个增量就是 O(n²) 流量)。
14
14
  - `content_delta`:每条 assistant 消息落地时下发一次的权威全文,前端**整体覆盖**。
15
15
  - `trace`:**不保证带结构化 `traceEvent`**。stderr 行(`emitStderrTraceLine`)和解析不出 JSON 的 stdout 行只有 `text` + `stream`,前端靠 `toTraceEvent()` 兜底成 `kind="output"` 的事件展示。新增只认 `traceEvent` 的分支等于把自定义 CLI、CLI 报错的输出全吞掉。
@@ -21,11 +21,9 @@
21
21
  - 空闲保活是 `/agent/stream` 那条心跳规则的同类问题,**两个入口都要守**:终端侧服务端每 `WEB_TERMINAL_PING_INTERVAL_MS`(30s)发 WebSocket ping 帧,连续 `WEB_TERMINAL_MAX_MISSED_PONGS`(3 次,约 90s)无 pong 才判死;前端另发应用层 `ping`(浏览器 JS 发不出 ping 帧,上行需要自己造流量)。判死阈值不要收紧到一个周期:手机切后台会让连接短暂挂起,误杀等于用户的 shell 没了。升级后的 socket 还要 `setTimeout(0)` + `setKeepAlive`,解除 HTTP 侧空闲超时。
22
22
  - 新增接口/页面必须走全局认证网关,禁止在业务路由里零散补认证;匿名白名单见根 `AGENTS.md` 的安全约束。
23
23
 
24
- ## lib/web/frontend/
24
+ ## lib/web/index.html
25
25
 
26
- 只剩一个文件:`shadcn.html`,是 `npm run build:web-shadcn` 的产物(已 `.gitignore`),由 `loadTemplate()` 在每次请求时读取。服务端不再托管其它前端静态资源,别往这个目录里放新的散装 js/css。
27
-
28
- 目录里没有任何被跟踪的文件,而 git 不记录空目录——**全新检出(CI、别人第一次 clone)是没有 `lib/web/frontend/` 这个目录的**。任何往这里写文件的构建步骤都必须自己 `mkdir -p`(见 `frontend-shadcn/scripts/emit-shadcn-html.mjs`),否则本地有目录跑得好好的,CI 上直接 ENOENT。
26
+ 前端构建产物(`npm run build:web`,已 `.gitignore`),由 `loadIndexHtml()` 在每次请求时读取,`/` 与登录页 `/auth/login` 共用。服务端不托管其它前端静态资源,别往 `lib/web/` 里放散装 js/css。
29
27
 
30
28
  ## 测试
31
29