@xcanwin/manyoyo 4.2.8 → 5.0.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/README.md CHANGED
@@ -46,9 +46,9 @@
46
46
  ```bash
47
47
  npm install -g @xcanwin/manyoyo # 安装
48
48
  podman pull ubuntu:24.04 # 仅 Podman 需要
49
- manyoyo --ib --iv 1.8.0-common # 构建镜像
50
- manyoyo --init-config all # 从本机 Agent 配置迁移到 ~/.manyoyo
51
- manyoyo -r claude # 使用 manyoyo.json 的 runs.claude 启动
49
+ manyoyo build --iv 1.8.0-common # 构建镜像
50
+ manyoyo init all # 从本机 Agent 配置迁移到 ~/.manyoyo
51
+ manyoyo run -r claude # 使用 manyoyo.json 的 runs.claude 启动
52
52
  ```
53
53
 
54
54
  注意:YOLO/SOLO 会跳过权限确认,请确保在可控环境中使用。
@@ -97,13 +97,13 @@ npm install -g @xcanwin/manyoyo
97
97
 
98
98
  ```bash
99
99
  # 构建 common 版本(推荐)
100
- manyoyo --ib --iv 1.8.0-common
100
+ manyoyo build --iv 1.8.0-common
101
101
 
102
102
  # 构建 full 版本
103
- manyoyo --ib --iv 1.8.0-full
103
+ manyoyo build --iv 1.8.0-full
104
104
 
105
105
  # 构建自定义版本
106
- manyoyo --ib --iba TOOL=go,codex,java,gemini
106
+ manyoyo build --iba TOOL=go,codex,java,gemini
107
107
  ```
108
108
 
109
109
  - 首次构建会自动下载依赖到 `docker/cache/`,2天内再次构建会使用缓存,速度提升约 **5 倍**
@@ -112,25 +112,25 @@ manyoyo --ib --iba TOOL=go,codex,java,gemini
112
112
 
113
113
  ```bash
114
114
  # 配置迁移(推荐首步)
115
- manyoyo --init-config all
115
+ manyoyo init all
116
116
 
117
117
  # 启动常见智能体
118
- manyoyo -y c # Claude Code(或 claude / cc)
119
- manyoyo -y gm # Gemini(或 gemini / g)
120
- manyoyo -y cx # Codex(或 codex)
121
- manyoyo -y oc # OpenCode(或 opencode)
122
- manyoyo --update # 更新 MANYOYO(全局 npm 安装场景)
118
+ manyoyo run -y c # Claude Code(或 claude / cc)
119
+ manyoyo run -y gm # Gemini(或 gemini / g)
120
+ manyoyo run -y cx # Codex(或 codex)
121
+ manyoyo run -y oc # OpenCode(或 opencode)
122
+ manyoyo update # 更新 MANYOYO(全局 npm 安装场景)
123
123
 
124
124
  # 容器管理
125
- manyoyo -l
126
- manyoyo -n my-dev -x /bin/bash
127
- manyoyo -n my-dev --crm
128
- manyoyo --server 3000
129
- manyoyo --server 3000 --server-user admin --server-pass 123456
125
+ manyoyo ls
126
+ manyoyo run -n my-dev -x /bin/bash
127
+ manyoyo rm my-dev
128
+ manyoyo serve 3000
129
+ manyoyo serve 3000 -u admin -P 123456
130
130
 
131
131
  # 调试配置与命令拼装
132
- manyoyo --show-config
133
- manyoyo --show-command
132
+ manyoyo config show
133
+ manyoyo config command
134
134
  ```
135
135
 
136
136
  ## 配置
package/bin/manyoyo.js CHANGED
@@ -55,18 +55,14 @@ let IMAGE_VERSION = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
55
55
  let EXEC_COMMAND = "";
56
56
  let EXEC_COMMAND_PREFIX = "";
57
57
  let EXEC_COMMAND_SUFFIX = "";
58
- let SHOULD_REMOVE = false;
59
- let IMAGE_BUILD_NEED = false;
60
58
  let IMAGE_BUILD_ARGS = [];
61
59
  let CONTAINER_ENVS = [];
62
60
  let CONTAINER_VOLUMES = [];
63
- let MANYOYO_NAME = detectCommandName();
61
+ let CONTAINER_PORTS = [];
62
+ const MANYOYO_NAME = detectCommandName();
64
63
  let CONT_MODE_ARGS = [];
65
64
  let QUIET = {};
66
- let SHOW_COMMAND = false;
67
- let YES_MODE = false;
68
65
  let RM_ON_EXIT = false;
69
- let SERVER_MODE = false;
70
66
  let SERVER_HOST = '127.0.0.1';
71
67
  let SERVER_PORT = 3000;
72
68
  let SERVER_AUTH_USER = "";
@@ -127,7 +123,7 @@ function validateServerHost(host, rawServer) {
127
123
  return value;
128
124
  }
129
125
 
130
- console.error(`${RED}⚠️ 错误: --server 地址格式应为 端口 或 host:port (例如 3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
126
+ console.error(`${RED}⚠️ 错误: serve 地址格式应为 端口 或 host:port (例如 3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
131
127
  process.exit(1);
132
128
  }
133
129
 
@@ -160,13 +156,13 @@ function parseServerListen(rawServer) {
160
156
  }
161
157
 
162
158
  if (!/^\d+$/.test(portText)) {
163
- console.error(`${RED}⚠️ 错误: --server 端口必须是 1-65535 的整数: ${rawServer}${NC}`);
159
+ console.error(`${RED}⚠️ 错误: serve 端口必须是 1-65535 的整数: ${rawServer}${NC}`);
164
160
  process.exit(1);
165
161
  }
166
162
 
167
163
  const port = Number(portText);
168
164
  if (port < 1 || port > 65535) {
169
- console.error(`${RED}⚠️ 错误: --server 端口超出范围 (1-65535): ${rawServer}${NC}`);
165
+ console.error(`${RED}⚠️ 错误: serve 端口超出范围 (1-65535): ${rawServer}${NC}`);
170
166
  process.exit(1);
171
167
  }
172
168
 
@@ -188,7 +184,7 @@ function ensureWebServerAuthCredentials() {
188
184
  }
189
185
 
190
186
  /**
191
- * 敏感信息脱敏(用于 --show-config 输出)
187
+ * 敏感信息脱敏(用于 config show 输出)
192
188
  * @param {Object} obj - 配置对象
193
189
  * @returns {Object} 脱敏后的配置对象
194
190
  */
@@ -298,12 +294,12 @@ function getHelloTip(containerName, defaultCommand, runningCommand) {
298
294
  console.log(`${BLUE}----------------------------------------${NC}`);
299
295
  console.log(`📦 首次命令 : ${defaultCommand}`);
300
296
  if (resumeArg) {
301
- console.log(`⚫ 恢复首次命令会话: ${CYAN}${MANYOYO_NAME} -n ${containerName} -- ${resumeArg}${NC}`);
297
+ console.log(`⚫ 恢复首次命令会话: ${CYAN}${MANYOYO_NAME} run -n ${containerName} -- ${resumeArg}${NC}`);
302
298
  }
303
- console.log(`⚫ 执行首次命令 : ${GREEN}${MANYOYO_NAME} -n ${containerName}${NC}`);
304
- console.log(`⚫ 执行指定命令 : ${GREEN}${MANYOYO_NAME} -n ${containerName} -x /bin/bash${NC}`);
299
+ console.log(`⚫ 执行首次命令 : ${GREEN}${MANYOYO_NAME} run -n ${containerName}${NC}`);
300
+ console.log(`⚫ 执行指定命令 : ${GREEN}${MANYOYO_NAME} run -n ${containerName} -x /bin/bash${NC}`);
305
301
  console.log(`⚫ 执行指定命令 : ${GREEN}docker exec -it ${containerName} /bin/bash${NC}`);
306
- console.log(`⚫ 删除容器 : ${MANYOYO_NAME} -n ${containerName} --crm`);
302
+ console.log(`⚫ 删除容器 : ${MANYOYO_NAME} rm ${containerName}`);
307
303
  console.log("");
308
304
  }
309
305
  }
@@ -488,6 +484,10 @@ function addVolume(volume) {
488
484
  CONTAINER_VOLUMES.push("--volume", volume);
489
485
  }
490
486
 
487
+ function addPort(port) {
488
+ CONTAINER_PORTS.push("--publish", String(port));
489
+ }
490
+
491
491
  function addImageBuildArg(value) {
492
492
  IMAGE_BUILD_ARGS.push("--build-arg", value);
493
493
  }
@@ -567,7 +567,7 @@ function showImagePullHint(err) {
567
567
  }
568
568
  const image = `${IMAGE_NAME}:${IMAGE_VERSION}`;
569
569
  console.log(`${YELLOW}💡 提示: 本地未找到镜像 ${image},并且从 localhost 注册表拉取失败。${NC}`);
570
- console.log(`${YELLOW} 你可以: (1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion。 (2) 或先执行 ${MANYOYO_NAME} --ib --iv <x.y.z-后缀> 构建镜像。${NC}`);
570
+ console.log(`${YELLOW} 你可以: (1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion。 (2) 或先执行 ${MANYOYO_NAME} build --iv <x.y.z-后缀> 构建镜像。${NC}`);
571
571
  }
572
572
 
573
573
  function runCmd(cmd, args, options = {}) {
@@ -758,11 +758,74 @@ function normalizeShellFullArgv(argv) {
758
758
  }
759
759
  }
760
760
 
761
+ function appendArrayOption(command, flags, description) {
762
+ return command.option(
763
+ flags,
764
+ description,
765
+ (value, previous) => [...(previous || []), value],
766
+ []
767
+ );
768
+ }
769
+
770
+ function applyRunStyleOptions(command, options = {}) {
771
+ const includeRmOnExit = options.includeRmOnExit !== false;
772
+ const includeServePreview = options.includeServePreview === true;
773
+ const includeWebAuthOptions = options.includeWebAuthOptions === true;
774
+
775
+ command
776
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
777
+ .option('--hp, --host-path <path>', '设置宿主机工作目录 (默认当前路径)')
778
+ .option('-n, --cont-name <name>', '设置容器名称')
779
+ .option('--cp, --cont-path <path>', '设置容器工作目录')
780
+ .option('-m, --cont-mode <mode>', '设置容器嵌套容器模式 (common, dind, sock)')
781
+ .option('--in, --image-name <name>', '指定镜像名称')
782
+ .option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)');
783
+
784
+ appendArrayOption(command, '-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)');
785
+ appendArrayOption(command, '--ef, --env-file <file>', '设置环境变量通过文件 (仅支持绝对路径,如 /abs/path.env)');
786
+ appendArrayOption(command, '-v, --volume <volume>', '绑定挂载卷 XXX:YYY (可多次使用)');
787
+ appendArrayOption(command, '-p, --port <port>', '设置端口映射 XXX:YYY (可多次使用)');
788
+
789
+ command
790
+ .option('--sp, --shell-prefix <command>', '临时环境变量 (作为-s前缀)')
791
+ .option('-s, --shell <command>', '指定命令执行')
792
+ .option('--ss, --shell-suffix <command>', '指定命令后缀 (追加到-s之后,等价于 -- <args>)')
793
+ .option('-x, --shell-full <command...>', '指定完整命令执行 (代替--sp和-s和--命令)')
794
+ .option('-y, --yolo <cli>', '使AGENT无需确认 (claude/c, gemini/gm, codex/cx, opencode/oc)');
795
+
796
+ if (includeRmOnExit) {
797
+ command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
798
+ }
799
+
800
+ appendArrayOption(command, '-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)');
801
+
802
+ if (includeServePreview) {
803
+ command
804
+ .option('--serve [listen]', '按 serve 模式解析配置 (支持 port 或 host:port)')
805
+ .option('-u, --user <username>', '网页服务登录用户名 (默认 admin)')
806
+ .option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
807
+ }
808
+
809
+ if (includeWebAuthOptions) {
810
+ command
811
+ .option('-u, --user <username>', '网页服务登录用户名 (默认 admin)')
812
+ .option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
813
+ }
814
+
815
+ return command;
816
+ }
817
+
761
818
  async function setupCommander() {
762
819
  // Load config file
763
820
  const config = loadConfig();
764
821
 
765
822
  const program = new Command();
823
+ let selectedAction = '';
824
+ let selectedOptions = {};
825
+ const selectAction = (action, options = {}) => {
826
+ selectedAction = action;
827
+ selectedOptions = options;
828
+ };
766
829
 
767
830
  program
768
831
  .name(MANYOYO_NAME)
@@ -774,59 +837,94 @@ async function setupCommander() {
774
837
  ~/.manyoyo/run/c.json 运行配置示例
775
838
 
776
839
  路径规则:
777
- -r name → ~/.manyoyo/manyoyo.json 的 runs.name
778
- --ef /abs/path.env → 绝对路径环境文件
779
- --ss "<args>" → 显式设置命令后缀
780
- -- <args...> → 直接透传命令后缀(优先级最高)
840
+ run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
841
+ run --ef /abs/path.env → 绝对路径环境文件
842
+ run --ss "<args>" → 显式设置命令后缀
843
+ run -- <args...> → 直接透传命令后缀(优先级最高)
781
844
 
782
845
  示例:
783
- ${MANYOYO_NAME} --update 更新 MANYOYO 到最新版本
784
- ${MANYOYO_NAME} --ib --iv ${IMAGE_VERSION_HELP_EXAMPLE} 构建镜像
785
- ${MANYOYO_NAME} --init-config all 从本机 Agent 配置初始化 ~/.manyoyo
786
- ${MANYOYO_NAME} -r claude 使用 manyoyo.json 的 runs.claude 快速启动
787
- ${MANYOYO_NAME} -r codex --ss "resume --last" 使用命令后缀
788
- ${MANYOYO_NAME} -n test --ef /abs/path/myenv.env -y c 使用绝对路径环境变量文件
789
- ${MANYOYO_NAME} -n test -- -c 恢复之前会话
790
- ${MANYOYO_NAME} -x echo 123 指定命令执行
791
- ${MANYOYO_NAME} --server --server-user admin --server-pass 123456 启动带登录认证的网页服务
792
- ${MANYOYO_NAME} --server 3000 启动网页交互服务
793
- ${MANYOYO_NAME} --server 0.0.0.0:3000 监听全部网卡,便于局域网访问
794
- ${MANYOYO_NAME} -n test -q tip -q cmd 多次使用静默选项
846
+ ${MANYOYO_NAME} update 更新 MANYOYO 到最新版本
847
+ ${MANYOYO_NAME} build --iv ${IMAGE_VERSION_HELP_EXAMPLE} 构建镜像
848
+ ${MANYOYO_NAME} init all 从本机 Agent 配置初始化 ~/.manyoyo
849
+ ${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 快速启动
850
+ ${MANYOYO_NAME} run -r codex --ss "resume --last" 使用命令后缀
851
+ ${MANYOYO_NAME} run -n test --ef /abs/path/myenv.env -y c 使用绝对路径环境变量文件
852
+ ${MANYOYO_NAME} run -n test -- -c 恢复之前会话
853
+ ${MANYOYO_NAME} run -x "echo 123" 指定命令执行
854
+ ${MANYOYO_NAME} serve 3000 -u admin -P 123456 启动带登录认证的网页服务
855
+ ${MANYOYO_NAME} serve 0.0.0.0:3000 监听全部网卡,便于局域网访问
856
+ ${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
795
857
  `);
796
858
 
797
- // Options
798
- program
859
+ const runCommand = program.command('run').description('启动或连接容器并执行命令');
860
+ applyRunStyleOptions(runCommand);
861
+ runCommand.action(options => selectAction('run', options));
862
+
863
+ const buildCommand = program.command('build').description('构建 manyoyo 沙箱镜像');
864
+ buildCommand
799
865
  .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
800
- .option('--hp, --host-path <path>', '设置宿主机工作目录 (默认当前路径)')
801
- .option('-n, --cont-name <name>', '设置容器名称')
802
- .option('--cp, --cont-path <path>', '设置容器工作目录')
803
- .option('-l, --cont-list', '列举容器')
804
- .option('--crm, --cont-remove', '删除-n指定容器')
805
- .option('-m, --cont-mode <mode>', '设置容器嵌套容器模式 (common, dind, sock)')
806
866
  .option('--in, --image-name <name>', '指定镜像名称')
807
867
  .option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)')
808
- .option('--ib, --image-build', '构建镜像')
809
- .option('--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)', (value, previous) => [...(previous || []), value], [])
810
- .option('--init-config [agents]', '初始化 Agent 配置到 ~/.manyoyo (all 或逗号分隔: claude,codex,gemini,opencode)')
811
- .option('--update', '更新 MANYOYO(若检测为本地 file 安装则跳过)')
812
- .option('--irm, --image-remove', '清理悬空镜像和 <none> 镜像')
813
- .option('-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)', (value, previous) => [...(previous || []), value], [])
814
- .option('--ef, --env-file <file>', '设置环境变量通过文件 (仅支持绝对路径,如 /abs/path.env)', (value, previous) => [...(previous || []), value], [])
815
- .option('-v, --volume <volume>', '绑定挂载卷 XXX:YYY (可多次使用)', (value, previous) => [...(previous || []), value], [])
816
- .option('--sp, --shell-prefix <command>', '临时环境变量 (作为-s前缀)')
817
- .option('-s, --shell <command>', '指定命令执行')
818
- .option('--ss, --shell-suffix <command>', '指定命令后缀 (追加到-s之后,等价于 -- <args>)')
819
- .option('-x, --shell-full <command...>', '指定完整命令执行 (代替--sp和-s和--命令)')
820
- .option('-y, --yolo <cli>', '使AGENT无需确认 (claude/c, gemini/gm, codex/cx, opencode/oc)')
821
- .option('--install <name>', `安装${MANYOYO_NAME}命令 (docker-cli-plugin)`)
822
- .option('--show-config', '显示最终生效配置并退出')
823
- .option('--show-command', '显示将执行的 docker run 命令并退出')
824
- .option('--server [port]', '启动网页交互服务 (默认 127.0.0.1:3000,支持 host:port)')
825
- .option('--server-user <username>', '网页服务登录用户名 (默认 admin)')
826
- .option('--server-pass <password>', '网页服务登录密码 (默认自动生成随机密码)')
868
+ .option('--yes', '所有提示自动确认 (用于CI/脚本)');
869
+ appendArrayOption(buildCommand, '--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)');
870
+ buildCommand.action(options => selectAction('build', options));
871
+
872
+ const removeCommand = program.command('rm <name>').description('删除指定容器');
873
+ removeCommand
874
+ .option('-r, --run <name>', '加载运行配置 ( ~/.manyoyo/manyoyo.json runs.<name> 读取)')
875
+ .action((name, options) => selectAction('rm', { ...options, contName: name }));
876
+
877
+ program.command('ls')
878
+ .description('列举容器')
879
+ .action(() => selectAction('ls', { contList: true }));
880
+
881
+ const serveCommand = program.command('serve [listen]').description('启动网页交互服务 (默认 127.0.0.1:3000)');
882
+ applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
883
+ serveCommand.action((listen, options) => {
884
+ selectAction('serve', {
885
+ ...options,
886
+ server: listen === undefined ? true : listen,
887
+ serverUser: options.user,
888
+ serverPass: options.pass
889
+ });
890
+ });
891
+
892
+ const configCommand = program.command('config').description('查看解析后的配置或命令');
893
+ const configShowCommand = configCommand.command('show').description('显示最终生效配置并退出');
894
+ applyRunStyleOptions(configShowCommand, { includeRmOnExit: false, includeServePreview: true });
895
+ configShowCommand.action(options => {
896
+ const finalOptions = {
897
+ ...options,
898
+ showConfig: true
899
+ };
900
+ if (options.serve !== undefined) {
901
+ finalOptions.server = options.serve;
902
+ finalOptions.serverUser = options.user;
903
+ finalOptions.serverPass = options.pass;
904
+ }
905
+ selectAction('config-show', finalOptions);
906
+ });
907
+
908
+ const configRunCommand = configCommand.command('command').description('显示将执行的 docker run 命令并退出');
909
+ applyRunStyleOptions(configRunCommand, { includeRmOnExit: false });
910
+ configRunCommand.action(options => selectAction('config-command', options));
911
+
912
+ const initCommand = program.command('init [agents]').description('初始化 Agent 配置到 ~/.manyoyo');
913
+ initCommand
827
914
  .option('--yes', '所有提示自动确认 (用于CI/脚本)')
828
- .option('--rm-on-exit', '退出后自动删除容器 (一次性模式)')
829
- .option('-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)', (value, previous) => [...(previous || []), value], []);
915
+ .action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
916
+
917
+ program.command('update')
918
+ .description('更新 MANYOYO(若检测为本地 file 安装则跳过)')
919
+ .action(() => selectAction('update', { update: true }));
920
+
921
+ program.command('install <name>')
922
+ .description(`安装${MANYOYO_NAME}命令 (docker-cli-plugin)`)
923
+ .action(name => selectAction('install', { install: name }));
924
+
925
+ program.command('prune')
926
+ .description('清理悬空镜像和 <none> 镜像')
927
+ .action(() => selectAction('prune', { imageRemove: true }));
830
928
 
831
929
  // Docker CLI plugin metadata check
832
930
  if (maybeHandleDockerPluginMetadata(process.argv)) {
@@ -841,25 +939,30 @@ async function setupCommander() {
841
939
  program.help();
842
940
  }
843
941
 
844
- const isInitConfigMode = process.argv.some(arg => arg === '--init-config' || arg.startsWith('--init-config='));
845
- const isUpdateMode = process.argv.includes('--update');
846
- // init-config/update 只处理本地文件或 npm,不依赖 docker/podman
847
- if (!isInitConfigMode && !isUpdateMode) {
848
- // Ensure docker/podman is available
849
- ensureDocker();
850
- }
851
-
852
942
  // Pre-handle -x/--shell-full: treat all following args as a single command
853
943
  normalizeShellFullArgv(process.argv);
854
944
 
855
945
  // Parse arguments
856
946
  program.allowUnknownOption(false);
857
- program.parse(process.argv);
947
+ await program.parseAsync(process.argv);
858
948
 
859
- const options = program.opts();
949
+ if (!selectedAction) {
950
+ program.help();
951
+ }
952
+
953
+ const options = selectedOptions;
954
+ const yesMode = Boolean(options.yes);
955
+ const isBuildMode = selectedAction === 'build';
956
+ const isRemoveMode = selectedAction === 'rm';
957
+ const isListMode = selectedAction === 'ls';
958
+ const isPruneMode = selectedAction === 'prune';
959
+ const isShowConfigMode = selectedAction === 'config-show';
960
+ const isShowCommandMode = selectedAction === 'config-command';
961
+ const isServerMode = options.server !== undefined;
860
962
 
861
- if (options.yes) {
862
- YES_MODE = true;
963
+ const noDockerActions = new Set(['init', 'update', 'install', 'config-show']);
964
+ if (!noDockerActions.has(selectedAction)) {
965
+ ensureDocker();
863
966
  }
864
967
 
865
968
  if (options.update) {
@@ -869,7 +972,7 @@ async function setupCommander() {
869
972
 
870
973
  if (options.initConfig !== undefined) {
871
974
  await initAgentConfigs(options.initConfig, {
872
- yesMode: YES_MODE,
975
+ yesMode,
873
976
  askQuestion,
874
977
  loadConfig,
875
978
  supportedAgents: SUPPORTED_INIT_AGENTS,
@@ -936,6 +1039,9 @@ async function setupCommander() {
936
1039
  const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
937
1040
  volumeList.forEach(v => addVolume(v));
938
1041
 
1042
+ const portList = mergeArrayConfig(config.ports, runConfig.ports, options.port);
1043
+ portList.forEach(p => addPort(p));
1044
+
939
1045
  const buildArgList = mergeArrayConfig(config.imageBuildArgs, runConfig.imageBuildArgs, options.imageBuildArg);
940
1046
  buildArgList.forEach(arg => addImageBuildArg(arg));
941
1047
 
@@ -968,8 +1074,7 @@ async function setupCommander() {
968
1074
  RM_ON_EXIT = true;
969
1075
  }
970
1076
 
971
- if (options.server !== undefined) {
972
- SERVER_MODE = true;
1077
+ if (isServerMode) {
973
1078
  const serverListen = parseServerListen(options.server);
974
1079
  SERVER_HOST = serverListen.host;
975
1080
  SERVER_PORT = serverListen.port;
@@ -986,11 +1091,11 @@ async function setupCommander() {
986
1091
  SERVER_AUTH_PASS_AUTO = false;
987
1092
  }
988
1093
 
989
- if (SERVER_MODE) {
1094
+ if (isServerMode) {
990
1095
  ensureWebServerAuthCredentials();
991
1096
  }
992
1097
 
993
- if (options.showConfig) {
1098
+ if (isShowConfigMode) {
994
1099
  const finalConfig = {
995
1100
  hostPath: HOST_PATH,
996
1101
  containerName: CONTAINER_NAME,
@@ -1000,6 +1105,7 @@ async function setupCommander() {
1000
1105
  envFile: envFileList,
1001
1106
  env: envMap,
1002
1107
  volumes: volumeList,
1108
+ ports: portList,
1003
1109
  imageBuildArgs: buildArgList,
1004
1110
  containerMode: contModeValue || "",
1005
1111
  shellPrefix: EXEC_COMMAND_PREFIX.trim(),
@@ -1007,9 +1113,9 @@ async function setupCommander() {
1007
1113
  shellSuffix: EXEC_COMMAND_SUFFIX || "",
1008
1114
  yolo: yoloValue || "",
1009
1115
  quiet: quietValue || [],
1010
- server: SERVER_MODE,
1011
- serverHost: SERVER_MODE ? SERVER_HOST : null,
1012
- serverPort: SERVER_MODE ? SERVER_PORT : null,
1116
+ server: isServerMode,
1117
+ serverHost: isServerMode ? SERVER_HOST : null,
1118
+ serverPort: isServerMode ? SERVER_PORT : null,
1013
1119
  serverUser: SERVER_AUTH_USER || "",
1014
1120
  serverPass: SERVER_AUTH_PASS || "",
1015
1121
  exec: {
@@ -1024,20 +1130,20 @@ async function setupCommander() {
1024
1130
  process.exit(0);
1025
1131
  }
1026
1132
 
1027
- if (options.showCommand) {
1028
- SHOW_COMMAND = true;
1029
- }
1030
-
1031
- if (options.contList) { getContList(); process.exit(0); }
1032
- if (options.contRemove) SHOULD_REMOVE = true;
1033
- if (options.imageBuild) IMAGE_BUILD_NEED = true;
1034
- if (options.imageRemove) { pruneDanglingImages(); process.exit(0); }
1035
- if (options.install) { installManyoyo(options.install); process.exit(0); }
1133
+ if (isListMode) { getContList(); process.exit(0); }
1134
+ if (isPruneMode) { pruneDanglingImages(); process.exit(0); }
1135
+ if (selectedAction === 'install') { installManyoyo(options.install); process.exit(0); }
1036
1136
 
1037
- return program;
1137
+ return {
1138
+ yesMode,
1139
+ isBuildMode,
1140
+ isRemoveMode,
1141
+ isShowCommandMode,
1142
+ isServerMode
1143
+ };
1038
1144
  }
1039
1145
 
1040
- function createRuntimeContext() {
1146
+ function createRuntimeContext(modeState = {}) {
1041
1147
  return {
1042
1148
  containerName: CONTAINER_NAME,
1043
1149
  hostPath: HOST_PATH,
@@ -1050,10 +1156,11 @@ function createRuntimeContext() {
1050
1156
  contModeArgs: CONT_MODE_ARGS,
1051
1157
  containerEnvs: CONTAINER_ENVS,
1052
1158
  containerVolumes: CONTAINER_VOLUMES,
1159
+ containerPorts: CONTAINER_PORTS,
1053
1160
  quiet: QUIET,
1054
- showCommand: SHOW_COMMAND,
1161
+ showCommand: Boolean(modeState.isShowCommandMode),
1055
1162
  rmOnExit: RM_ON_EXIT,
1056
- serverMode: SERVER_MODE,
1163
+ serverMode: Boolean(modeState.isServerMode),
1057
1164
  serverHost: SERVER_HOST,
1058
1165
  serverPort: SERVER_PORT,
1059
1166
  serverAuthUser: SERVER_AUTH_USER,
@@ -1063,10 +1170,6 @@ function createRuntimeContext() {
1063
1170
  }
1064
1171
 
1065
1172
  function handleRemoveContainer(runtime) {
1066
- if (!SHOULD_REMOVE) {
1067
- return false;
1068
- }
1069
-
1070
1173
  try {
1071
1174
  if (containerExists(runtime.containerName)) {
1072
1175
  removeContainer(runtime.containerName);
@@ -1076,7 +1179,6 @@ function handleRemoveContainer(runtime) {
1076
1179
  } catch (e) {
1077
1180
  console.log(`${RED}⚠️ 错误: 未找到名为 ${runtime.containerName} 的容器。${NC}`);
1078
1181
  }
1079
- return true;
1080
1182
  }
1081
1183
 
1082
1184
  function validateHostPath(runtime) {
@@ -1180,6 +1282,7 @@ function buildDockerRunArgs(runtime) {
1180
1282
  contModeArgs: runtime.contModeArgs,
1181
1283
  containerEnvs: runtime.containerEnvs,
1182
1284
  containerVolumes: runtime.containerVolumes,
1285
+ containerPorts: runtime.containerPorts,
1183
1286
  defaultCommand: runtime.execCommand
1184
1287
  });
1185
1288
  }
@@ -1343,6 +1446,7 @@ async function runWebServerMode(runtime) {
1343
1446
  contModeArgs: runtime.contModeArgs,
1344
1447
  containerEnvs: runtime.containerEnvs,
1345
1448
  containerVolumes: runtime.containerVolumes,
1449
+ containerPorts: runtime.containerPorts,
1346
1450
  validateHostPath: () => validateHostPath(runtime),
1347
1451
  formatDate,
1348
1452
  isValidContainerName,
@@ -1367,8 +1471,8 @@ async function runWebServerMode(runtime) {
1367
1471
  async function main() {
1368
1472
  try {
1369
1473
  // 1. Setup commander and parse arguments
1370
- await setupCommander();
1371
- const runtime = createRuntimeContext();
1474
+ const modeState = await setupCommander();
1475
+ const runtime = createRuntimeContext(modeState);
1372
1476
 
1373
1477
  // 2. Start web server mode
1374
1478
  if (runtime.serverMode) {
@@ -1377,7 +1481,7 @@ async function main() {
1377
1481
  }
1378
1482
 
1379
1483
  // 3. Handle image build operation
1380
- if (IMAGE_BUILD_NEED) {
1484
+ if (modeState.isBuildMode) {
1381
1485
  await buildImage({
1382
1486
  imageBuildArgs: IMAGE_BUILD_ARGS,
1383
1487
  imageName: runtime.imageName,
@@ -1386,7 +1490,7 @@ async function main() {
1386
1490
  imageVersionBase: IMAGE_VERSION_BASE,
1387
1491
  parseImageVersionTag,
1388
1492
  manyoyoName: MANYOYO_NAME,
1389
- yesMode: YES_MODE,
1493
+ yesMode: Boolean(modeState.yesMode),
1390
1494
  dockerCmd: DOCKER_CMD,
1391
1495
  rootDir: path.join(__dirname, '..'),
1392
1496
  loadConfig,
@@ -1399,7 +1503,8 @@ async function main() {
1399
1503
  }
1400
1504
 
1401
1505
  // 4. Handle remove container operation
1402
- if (handleRemoveContainer(runtime)) {
1506
+ if (modeState.isRemoveMode) {
1507
+ handleRemoveContainer(runtime);
1403
1508
  return;
1404
1509
  }
1405
1510
 
@@ -21,6 +21,7 @@
21
21
  },
22
22
  "envFile": [],
23
23
  "volumes": [],
24
+ "ports": ["8888:8888"],
24
25
  "imageBuildArgs": [],
25
26
  "quiet": ["tip", "cmd"],
26
27
 
@@ -12,6 +12,7 @@ function buildContainerRunArgs(options) {
12
12
  ...(options.contModeArgs || []),
13
13
  ...(options.containerEnvs || []),
14
14
  ...(options.containerVolumes || []),
15
+ ...(options.containerPorts || []),
15
16
  '--volume', `${options.hostPath}:${options.containerPath}`,
16
17
  '--workdir', options.containerPath,
17
18
  '--label', `manyoyo.default_cmd=${sanitizeDefaultCommand(options.defaultCommand)}`,
@@ -126,7 +126,7 @@ function normalizeInitConfigAgents(rawAgents, ctx) {
126
126
  const mapped = aliasMap[token];
127
127
  if (!mapped) {
128
128
  const { RED, YELLOW, NC } = ctx.colors;
129
- ctx.error(`${RED}⚠️ 错误: --init-config 不支持的 Agent: ${token}${NC}`);
129
+ ctx.error(`${RED}⚠️ 错误: init 不支持的 Agent: ${token}${NC}`);
130
130
  ctx.error(`${YELLOW}支持: ${ctx.supportedAgents.join(', ')} 或 all${NC}`);
131
131
  ctx.exit(1);
132
132
  return [];
package/lib/web/server.js CHANGED
@@ -51,7 +51,8 @@ const DEFAULT_WEB_CONFIG_TEMPLATE = `{
51
51
  "yolo": "",
52
52
  "env": {},
53
53
  "envFile": [],
54
- "volumes": []
54
+ "volumes": [],
55
+ "ports": []
55
56
  }
56
57
  `;
57
58
 
@@ -474,6 +475,9 @@ function validateWebConfigShape(configObject) {
474
475
  if (hasOwn(config, 'volumes')) {
475
476
  normalizeStringArray(config.volumes, 'volumes');
476
477
  }
478
+ if (hasOwn(config, 'ports')) {
479
+ normalizeStringArray(config.ports, 'ports');
480
+ }
477
481
  if (hasOwn(config, 'runs')) {
478
482
  const runs = config.runs;
479
483
  if (runs !== undefined && (typeof runs !== 'object' || runs === null || Array.isArray(runs))) {
@@ -555,7 +559,8 @@ function buildConfigDefaults(ctx, config) {
555
559
  yolo: hasOwn(parsed, 'yolo') ? String(parsed.yolo || '') : '',
556
560
  env: {},
557
561
  envFile: [],
558
- volumes: []
562
+ volumes: [],
563
+ ports: []
559
564
  };
560
565
 
561
566
  try {
@@ -573,6 +578,11 @@ function buildConfigDefaults(ctx, config) {
573
578
  } catch (e) {
574
579
  defaults.volumes = [];
575
580
  }
581
+ try {
582
+ defaults.ports = normalizeStringArray(parsed.ports, 'ports');
583
+ } catch (e) {
584
+ defaults.ports = [];
585
+ }
576
586
 
577
587
  return defaults;
578
588
  }
@@ -587,6 +597,7 @@ function buildStaticContainerRuntime(ctx, containerName) {
587
597
  contModeArgs: Array.isArray(ctx.contModeArgs) ? ctx.contModeArgs.slice() : [],
588
598
  containerEnvs: Array.isArray(ctx.containerEnvs) ? ctx.containerEnvs.slice() : [],
589
599
  containerVolumes: Array.isArray(ctx.containerVolumes) ? ctx.containerVolumes.slice() : [],
600
+ containerPorts: Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [],
590
601
  defaultCommand: buildDefaultCommand(ctx.execCommandPrefix, ctx.execCommand, ctx.execCommandSuffix) || '/bin/bash'
591
602
  };
592
603
  }
@@ -600,9 +611,11 @@ function buildCreateRuntime(ctx, state, payload) {
600
611
  const hasRequestEnv = hasOwn(requestOptions, 'env');
601
612
  const hasRequestEnvFile = hasOwn(requestOptions, 'envFile');
602
613
  const hasRequestVolumes = hasOwn(requestOptions, 'volumes');
614
+ const hasRequestPorts = hasOwn(requestOptions, 'ports');
603
615
  const hasConfigEnv = hasOwn(config, 'env');
604
616
  const hasConfigEnvFile = hasOwn(config, 'envFile');
605
617
  const hasConfigVolumes = hasOwn(config, 'volumes');
618
+ const hasConfigPorts = hasOwn(config, 'ports');
606
619
 
607
620
  const requestName = pickFirstString(requestOptions.containerName, body.name);
608
621
  let containerName = pickFirstString(requestName, config.containerName);
@@ -683,6 +696,17 @@ function buildCreateRuntime(ctx, state, payload) {
683
696
  });
684
697
  }
685
698
 
699
+ let containerPorts = Array.isArray(ctx.containerPorts) ? ctx.containerPorts.slice() : [];
700
+ if (hasRequestPorts || hasConfigPorts) {
701
+ const portList = hasRequestPorts
702
+ ? normalizeStringArray(requestOptions.ports, 'createOptions.ports')
703
+ : normalizeStringArray(config.ports, 'config.ports');
704
+ containerPorts = [];
705
+ portList.forEach(port => {
706
+ containerPorts.push('--publish', port);
707
+ });
708
+ }
709
+
686
710
  return {
687
711
  containerName,
688
712
  hostPath,
@@ -692,6 +716,7 @@ function buildCreateRuntime(ctx, state, payload) {
692
716
  contModeArgs,
693
717
  containerEnvs,
694
718
  containerVolumes,
719
+ containerPorts,
695
720
  defaultCommand: buildDefaultCommand(shellPrefix, shell, shellSuffix) || '/bin/bash',
696
721
  applied: {
697
722
  containerName,
@@ -705,7 +730,8 @@ function buildCreateRuntime(ctx, state, payload) {
705
730
  shellSuffix: shellSuffix || '',
706
731
  yolo: yolo || '',
707
732
  envCount: Math.floor(containerEnvs.length / 2),
708
- volumeCount: Math.floor(containerVolumes.length / 2)
733
+ volumeCount: Math.floor(containerVolumes.length / 2),
734
+ portCount: Math.floor(containerPorts.length / 2)
709
735
  }
710
736
  };
711
737
  }
@@ -755,6 +781,7 @@ async function ensureWebContainer(ctx, state, containerInput) {
755
781
  contModeArgs: runtime.contModeArgs,
756
782
  containerEnvs: runtime.containerEnvs,
757
783
  containerVolumes: runtime.containerVolumes,
784
+ containerPorts: runtime.containerPorts,
758
785
  defaultCommand: runtime.defaultCommand
759
786
  });
760
787
 
@@ -1407,6 +1434,7 @@ async function startWebServer(options) {
1407
1434
  contModeArgs: options.contModeArgs,
1408
1435
  containerEnvs: options.containerEnvs,
1409
1436
  containerVolumes: options.containerVolumes,
1437
+ containerPorts: options.containerPorts,
1410
1438
  validateHostPath: options.validateHostPath,
1411
1439
  formatDate: options.formatDate,
1412
1440
  isValidContainerName: options.isValidContainerName,
@@ -1425,7 +1453,7 @@ async function startWebServer(options) {
1425
1453
  };
1426
1454
 
1427
1455
  if (!ctx.authUser || !ctx.authPass) {
1428
- throw new Error('Web 认证配置缺失,请设置 --server-user / --server-pass');
1456
+ throw new Error('Web 认证配置缺失,请设置 serve -u / serve -P');
1429
1457
  }
1430
1458
 
1431
1459
  const state = {
@@ -1593,7 +1621,7 @@ async function startWebServer(options) {
1593
1621
  if (ctx.authPassAuto) {
1594
1622
  console.log(`${CYAN}🔐 登录密码(本次随机): ${YELLOW}${ctx.authPass}${NC}`);
1595
1623
  } else {
1596
- console.log(`${CYAN}🔐 登录密码: 使用你配置的 --server-pass / serverPass / MANYOYO_SERVER_PASS${NC}`);
1624
+ console.log(`${CYAN}🔐 登录密码: 使用你配置的 serve -P / serverPass / MANYOYO_SERVER_PASS${NC}`);
1597
1625
  }
1598
1626
  resolve();
1599
1627
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "4.2.8",
3
+ "version": "5.0.0",
4
4
  "imageVersion": "1.8.0-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [
@@ -60,9 +60,14 @@
60
60
  "ws": "^8.19.0"
61
61
  },
62
62
  "devDependencies": {
63
- "jest": "^29.7.0",
63
+ "jest": "^30.2.0",
64
64
  "vitepress": "^2.0.0-alpha.16"
65
65
  },
66
+ "overrides": {
67
+ "glob": "^13.0.6",
68
+ "minimatch": "^10.2.2",
69
+ "test-exclude": "^8.0.0"
70
+ },
66
71
  "jest": {
67
72
  "testMatch": [
68
73
  "**/test/**/*.test.js"