@xcanwin/manyoyo 4.2.8 → 5.0.2

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,26 @@ 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 ps
126
+ manyoyo images
127
+ manyoyo run -n my-dev -x /bin/bash
128
+ manyoyo rm my-dev
129
+ manyoyo serve 3000
130
+ manyoyo serve 3000 -u admin -P 123456
130
131
 
131
132
  # 调试配置与命令拼装
132
- manyoyo --show-config
133
- manyoyo --show-command
133
+ manyoyo config show
134
+ manyoyo config command
134
135
  ```
135
136
 
136
137
  ## 配置
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 = {}) {
@@ -706,6 +706,22 @@ function getContList() {
706
706
  }
707
707
  }
708
708
 
709
+ function getImageList() {
710
+ try {
711
+ const output = dockerExecArgs(['images', '-a', '--format', '{{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.Size}}']);
712
+ const lines = output
713
+ .split('\n')
714
+ .map(line => line.trim())
715
+ .filter(line => line && line.includes('manyoyo'));
716
+ console.log('REPOSITORY\tTAG\tIMAGE ID\tCREATED\tSIZE');
717
+ if (lines.length > 0) {
718
+ console.log(lines.join('\n'));
719
+ }
720
+ } catch (e) {
721
+ console.log((e && e.stdout) || '');
722
+ }
723
+ }
724
+
709
725
  function pruneDanglingImages() {
710
726
  console.log(`\n${YELLOW}清理悬空镜像...${NC}`);
711
727
  dockerExecArgs(['image', 'prune', '-f'], { stdio: 'inherit' });
@@ -758,15 +774,79 @@ function normalizeShellFullArgv(argv) {
758
774
  }
759
775
  }
760
776
 
777
+ function appendArrayOption(command, flags, description) {
778
+ return command.option(
779
+ flags,
780
+ description,
781
+ (value, previous) => [...(previous || []), value],
782
+ []
783
+ );
784
+ }
785
+
786
+ function applyRunStyleOptions(command, options = {}) {
787
+ const includeRmOnExit = options.includeRmOnExit !== false;
788
+ const includeServePreview = options.includeServePreview === true;
789
+ const includeWebAuthOptions = options.includeWebAuthOptions === true;
790
+
791
+ command
792
+ .option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
793
+ .option('--hp, --host-path <path>', '设置宿主机工作目录 (默认当前路径)')
794
+ .option('-n, --cont-name <name>', '设置容器名称')
795
+ .option('--cp, --cont-path <path>', '设置容器工作目录')
796
+ .option('-m, --cont-mode <mode>', '设置容器嵌套容器模式 (common, dind, sock)')
797
+ .option('--in, --image-name <name>', '指定镜像名称')
798
+ .option('--iv, --image-ver <version>', '指定镜像版本 (格式: x.y.z-后缀,如 1.7.4-common)');
799
+
800
+ appendArrayOption(command, '-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)');
801
+ appendArrayOption(command, '--ef, --env-file <file>', '设置环境变量通过文件 (仅支持绝对路径,如 /abs/path.env)');
802
+ appendArrayOption(command, '-v, --volume <volume>', '绑定挂载卷 XXX:YYY (可多次使用)');
803
+ appendArrayOption(command, '-p, --port <port>', '设置端口映射 XXX:YYY (可多次使用)');
804
+
805
+ command
806
+ .option('--sp, --shell-prefix <command>', '临时环境变量 (作为-s前缀)')
807
+ .option('-s, --shell <command>', '指定命令执行')
808
+ .option('--ss, --shell-suffix <command>', '指定命令后缀 (追加到-s之后,等价于 -- <args>)')
809
+ .option('-x, --shell-full <command...>', '指定完整命令执行 (代替--sp和-s和--命令)')
810
+ .option('-y, --yolo <cli>', '使AGENT无需确认 (claude/c, gemini/gm, codex/cx, opencode/oc)');
811
+
812
+ if (includeRmOnExit) {
813
+ command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
814
+ }
815
+
816
+ appendArrayOption(command, '-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)');
817
+
818
+ if (includeServePreview) {
819
+ command
820
+ .option('--serve [listen]', '按 serve 模式解析配置 (支持 port 或 host:port)')
821
+ .option('-u, --user <username>', '网页服务登录用户名 (默认 admin)')
822
+ .option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
823
+ }
824
+
825
+ if (includeWebAuthOptions) {
826
+ command
827
+ .option('-u, --user <username>', '网页服务登录用户名 (默认 admin)')
828
+ .option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
829
+ }
830
+
831
+ return command;
832
+ }
833
+
761
834
  async function setupCommander() {
762
835
  // Load config file
763
836
  const config = loadConfig();
764
837
 
765
838
  const program = new Command();
839
+ program.enablePositionalOptions();
840
+ let selectedAction = '';
841
+ let selectedOptions = {};
842
+ const selectAction = (action, options = {}) => {
843
+ selectedAction = action;
844
+ selectedOptions = options;
845
+ };
766
846
 
767
847
  program
768
848
  .name(MANYOYO_NAME)
769
- .version(BIN_VERSION, '-V, --version', '显示版本')
849
+ .version(BIN_VERSION, '-v, --version', '显示版本')
770
850
  .description('MANYOYO - AI Agent CLI Sandbox\nhttps://github.com/xcanwin/manyoyo')
771
851
  .addHelpText('after', `
772
852
  配置文件:
@@ -774,59 +854,98 @@ async function setupCommander() {
774
854
  ~/.manyoyo/run/c.json 运行配置示例
775
855
 
776
856
  路径规则:
777
- -r name → ~/.manyoyo/manyoyo.json 的 runs.name
778
- --ef /abs/path.env → 绝对路径环境文件
779
- --ss "<args>" → 显式设置命令后缀
780
- -- <args...> → 直接透传命令后缀(优先级最高)
857
+ run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
858
+ run --ef /abs/path.env → 绝对路径环境文件
859
+ run --ss "<args>" → 显式设置命令后缀
860
+ run -- <args...> → 直接透传命令后缀(优先级最高)
781
861
 
782
862
  示例:
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 多次使用静默选项
863
+ ${MANYOYO_NAME} update 更新 MANYOYO 到最新版本
864
+ ${MANYOYO_NAME} build --iv ${IMAGE_VERSION_HELP_EXAMPLE} --yes 构建镜像
865
+ ${MANYOYO_NAME} init all 从本机 Agent 配置初始化 ~/.manyoyo
866
+ ${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 快速启动
867
+ ${MANYOYO_NAME} run -r codex --ss "resume --last" 使用命令后缀
868
+ ${MANYOYO_NAME} run -n test --ef /abs/path/myenv.env -y c 使用绝对路径环境变量文件
869
+ ${MANYOYO_NAME} run -n test -- -c 恢复之前会话
870
+ ${MANYOYO_NAME} run -x "echo 123" 指定命令执行
871
+ ${MANYOYO_NAME} serve 3000 -u admin -P 123456 启动带登录认证的网页服务
872
+ ${MANYOYO_NAME} serve 0.0.0.0:3000 监听全部网卡,便于局域网访问
873
+ ${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
795
874
  `);
796
875
 
797
- // Options
798
- program
876
+ const runCommand = program.command('run').description('启动或连接容器并执行命令');
877
+ applyRunStyleOptions(runCommand);
878
+ runCommand.action(options => selectAction('run', options));
879
+
880
+ const buildCommand = program.command('build').description('构建 manyoyo 沙箱镜像');
881
+ buildCommand
799
882
  .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
883
  .option('--in, --image-name <name>', '指定镜像名称')
807
884
  .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>', '网页服务登录密码 (默认自动生成随机密码)')
885
+ .option('--yes', '所有提示自动确认 (用于CI/脚本)');
886
+ appendArrayOption(buildCommand, '--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)');
887
+ buildCommand.action(options => selectAction('build', options));
888
+
889
+ const removeCommand = program.command('rm <name>').description('删除指定容器');
890
+ removeCommand
891
+ .option('-r, --run <name>', '加载运行配置 ( ~/.manyoyo/manyoyo.json runs.<name> 读取)')
892
+ .action((name, options) => selectAction('rm', { ...options, contName: name }));
893
+
894
+ program.command('ps')
895
+ .description('列举容器')
896
+ .action(() => selectAction('ps', { contList: true }));
897
+
898
+ program.command('images')
899
+ .description('列举镜像')
900
+ .action(() => selectAction('images', { imageList: true }));
901
+
902
+ const serveCommand = program.command('serve [listen]').description('启动网页交互服务 (默认 127.0.0.1:3000)');
903
+ applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
904
+ serveCommand.action((listen, options) => {
905
+ selectAction('serve', {
906
+ ...options,
907
+ server: listen === undefined ? true : listen,
908
+ serverUser: options.user,
909
+ serverPass: options.pass
910
+ });
911
+ });
912
+
913
+ const configCommand = program.command('config').description('查看解析后的配置或命令');
914
+ const configShowCommand = configCommand.command('show').description('显示最终生效配置并退出');
915
+ applyRunStyleOptions(configShowCommand, { includeRmOnExit: false, includeServePreview: true });
916
+ configShowCommand.action(options => {
917
+ const finalOptions = {
918
+ ...options,
919
+ showConfig: true
920
+ };
921
+ if (options.serve !== undefined) {
922
+ finalOptions.server = options.serve;
923
+ finalOptions.serverUser = options.user;
924
+ finalOptions.serverPass = options.pass;
925
+ }
926
+ selectAction('config-show', finalOptions);
927
+ });
928
+
929
+ const configRunCommand = configCommand.command('command').description('显示将执行的 docker run 命令并退出');
930
+ applyRunStyleOptions(configRunCommand, { includeRmOnExit: false });
931
+ configRunCommand.action(options => selectAction('config-command', options));
932
+
933
+ const initCommand = program.command('init [agents]').description('初始化 Agent 配置到 ~/.manyoyo');
934
+ initCommand
827
935
  .option('--yes', '所有提示自动确认 (用于CI/脚本)')
828
- .option('--rm-on-exit', '退出后自动删除容器 (一次性模式)')
829
- .option('-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)', (value, previous) => [...(previous || []), value], []);
936
+ .action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
937
+
938
+ program.command('update')
939
+ .description('更新 MANYOYO(若检测为本地 file 安装则跳过)')
940
+ .action(() => selectAction('update', { update: true }));
941
+
942
+ program.command('install <name>')
943
+ .description(`安装${MANYOYO_NAME}命令 (docker-cli-plugin)`)
944
+ .action(name => selectAction('install', { install: name }));
945
+
946
+ program.command('prune')
947
+ .description('清理悬空镜像和 <none> 镜像')
948
+ .action(() => selectAction('prune', { imageRemove: true }));
830
949
 
831
950
  // Docker CLI plugin metadata check
832
951
  if (maybeHandleDockerPluginMetadata(process.argv)) {
@@ -841,25 +960,31 @@ async function setupCommander() {
841
960
  program.help();
842
961
  }
843
962
 
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
963
  // Pre-handle -x/--shell-full: treat all following args as a single command
853
964
  normalizeShellFullArgv(process.argv);
854
965
 
855
966
  // Parse arguments
856
967
  program.allowUnknownOption(false);
857
- program.parse(process.argv);
968
+ await program.parseAsync(process.argv);
858
969
 
859
- const options = program.opts();
970
+ if (!selectedAction) {
971
+ program.help();
972
+ }
860
973
 
861
- if (options.yes) {
862
- YES_MODE = true;
974
+ const options = selectedOptions;
975
+ const yesMode = Boolean(options.yes);
976
+ const isBuildMode = selectedAction === 'build';
977
+ const isRemoveMode = selectedAction === 'rm';
978
+ const isPsMode = selectedAction === 'ps';
979
+ const isImagesMode = selectedAction === 'images';
980
+ const isPruneMode = selectedAction === 'prune';
981
+ const isShowConfigMode = selectedAction === 'config-show';
982
+ const isShowCommandMode = selectedAction === 'config-command';
983
+ const isServerMode = options.server !== undefined;
984
+
985
+ const noDockerActions = new Set(['init', 'update', 'install', 'config-show']);
986
+ if (!noDockerActions.has(selectedAction)) {
987
+ ensureDocker();
863
988
  }
864
989
 
865
990
  if (options.update) {
@@ -869,7 +994,7 @@ async function setupCommander() {
869
994
 
870
995
  if (options.initConfig !== undefined) {
871
996
  await initAgentConfigs(options.initConfig, {
872
- yesMode: YES_MODE,
997
+ yesMode,
873
998
  askQuestion,
874
999
  loadConfig,
875
1000
  supportedAgents: SUPPORTED_INIT_AGENTS,
@@ -936,6 +1061,9 @@ async function setupCommander() {
936
1061
  const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
937
1062
  volumeList.forEach(v => addVolume(v));
938
1063
 
1064
+ const portList = mergeArrayConfig(config.ports, runConfig.ports, options.port);
1065
+ portList.forEach(p => addPort(p));
1066
+
939
1067
  const buildArgList = mergeArrayConfig(config.imageBuildArgs, runConfig.imageBuildArgs, options.imageBuildArg);
940
1068
  buildArgList.forEach(arg => addImageBuildArg(arg));
941
1069
 
@@ -968,8 +1096,7 @@ async function setupCommander() {
968
1096
  RM_ON_EXIT = true;
969
1097
  }
970
1098
 
971
- if (options.server !== undefined) {
972
- SERVER_MODE = true;
1099
+ if (isServerMode) {
973
1100
  const serverListen = parseServerListen(options.server);
974
1101
  SERVER_HOST = serverListen.host;
975
1102
  SERVER_PORT = serverListen.port;
@@ -986,11 +1113,11 @@ async function setupCommander() {
986
1113
  SERVER_AUTH_PASS_AUTO = false;
987
1114
  }
988
1115
 
989
- if (SERVER_MODE) {
1116
+ if (isServerMode) {
990
1117
  ensureWebServerAuthCredentials();
991
1118
  }
992
1119
 
993
- if (options.showConfig) {
1120
+ if (isShowConfigMode) {
994
1121
  const finalConfig = {
995
1122
  hostPath: HOST_PATH,
996
1123
  containerName: CONTAINER_NAME,
@@ -1000,6 +1127,7 @@ async function setupCommander() {
1000
1127
  envFile: envFileList,
1001
1128
  env: envMap,
1002
1129
  volumes: volumeList,
1130
+ ports: portList,
1003
1131
  imageBuildArgs: buildArgList,
1004
1132
  containerMode: contModeValue || "",
1005
1133
  shellPrefix: EXEC_COMMAND_PREFIX.trim(),
@@ -1007,9 +1135,9 @@ async function setupCommander() {
1007
1135
  shellSuffix: EXEC_COMMAND_SUFFIX || "",
1008
1136
  yolo: yoloValue || "",
1009
1137
  quiet: quietValue || [],
1010
- server: SERVER_MODE,
1011
- serverHost: SERVER_MODE ? SERVER_HOST : null,
1012
- serverPort: SERVER_MODE ? SERVER_PORT : null,
1138
+ server: isServerMode,
1139
+ serverHost: isServerMode ? SERVER_HOST : null,
1140
+ serverPort: isServerMode ? SERVER_PORT : null,
1013
1141
  serverUser: SERVER_AUTH_USER || "",
1014
1142
  serverPass: SERVER_AUTH_PASS || "",
1015
1143
  exec: {
@@ -1024,20 +1152,21 @@ async function setupCommander() {
1024
1152
  process.exit(0);
1025
1153
  }
1026
1154
 
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); }
1155
+ if (isPsMode) { getContList(); process.exit(0); }
1156
+ if (isImagesMode) { getImageList(); process.exit(0); }
1157
+ if (isPruneMode) { pruneDanglingImages(); process.exit(0); }
1158
+ if (selectedAction === 'install') { installManyoyo(options.install); process.exit(0); }
1036
1159
 
1037
- return program;
1160
+ return {
1161
+ yesMode,
1162
+ isBuildMode,
1163
+ isRemoveMode,
1164
+ isShowCommandMode,
1165
+ isServerMode
1166
+ };
1038
1167
  }
1039
1168
 
1040
- function createRuntimeContext() {
1169
+ function createRuntimeContext(modeState = {}) {
1041
1170
  return {
1042
1171
  containerName: CONTAINER_NAME,
1043
1172
  hostPath: HOST_PATH,
@@ -1050,10 +1179,11 @@ function createRuntimeContext() {
1050
1179
  contModeArgs: CONT_MODE_ARGS,
1051
1180
  containerEnvs: CONTAINER_ENVS,
1052
1181
  containerVolumes: CONTAINER_VOLUMES,
1182
+ containerPorts: CONTAINER_PORTS,
1053
1183
  quiet: QUIET,
1054
- showCommand: SHOW_COMMAND,
1184
+ showCommand: Boolean(modeState.isShowCommandMode),
1055
1185
  rmOnExit: RM_ON_EXIT,
1056
- serverMode: SERVER_MODE,
1186
+ serverMode: Boolean(modeState.isServerMode),
1057
1187
  serverHost: SERVER_HOST,
1058
1188
  serverPort: SERVER_PORT,
1059
1189
  serverAuthUser: SERVER_AUTH_USER,
@@ -1063,10 +1193,6 @@ function createRuntimeContext() {
1063
1193
  }
1064
1194
 
1065
1195
  function handleRemoveContainer(runtime) {
1066
- if (!SHOULD_REMOVE) {
1067
- return false;
1068
- }
1069
-
1070
1196
  try {
1071
1197
  if (containerExists(runtime.containerName)) {
1072
1198
  removeContainer(runtime.containerName);
@@ -1076,7 +1202,6 @@ function handleRemoveContainer(runtime) {
1076
1202
  } catch (e) {
1077
1203
  console.log(`${RED}⚠️ 错误: 未找到名为 ${runtime.containerName} 的容器。${NC}`);
1078
1204
  }
1079
- return true;
1080
1205
  }
1081
1206
 
1082
1207
  function validateHostPath(runtime) {
@@ -1180,6 +1305,7 @@ function buildDockerRunArgs(runtime) {
1180
1305
  contModeArgs: runtime.contModeArgs,
1181
1306
  containerEnvs: runtime.containerEnvs,
1182
1307
  containerVolumes: runtime.containerVolumes,
1308
+ containerPorts: runtime.containerPorts,
1183
1309
  defaultCommand: runtime.execCommand
1184
1310
  });
1185
1311
  }
@@ -1343,6 +1469,7 @@ async function runWebServerMode(runtime) {
1343
1469
  contModeArgs: runtime.contModeArgs,
1344
1470
  containerEnvs: runtime.containerEnvs,
1345
1471
  containerVolumes: runtime.containerVolumes,
1472
+ containerPorts: runtime.containerPorts,
1346
1473
  validateHostPath: () => validateHostPath(runtime),
1347
1474
  formatDate,
1348
1475
  isValidContainerName,
@@ -1367,8 +1494,8 @@ async function runWebServerMode(runtime) {
1367
1494
  async function main() {
1368
1495
  try {
1369
1496
  // 1. Setup commander and parse arguments
1370
- await setupCommander();
1371
- const runtime = createRuntimeContext();
1497
+ const modeState = await setupCommander();
1498
+ const runtime = createRuntimeContext(modeState);
1372
1499
 
1373
1500
  // 2. Start web server mode
1374
1501
  if (runtime.serverMode) {
@@ -1377,7 +1504,7 @@ async function main() {
1377
1504
  }
1378
1505
 
1379
1506
  // 3. Handle image build operation
1380
- if (IMAGE_BUILD_NEED) {
1507
+ if (modeState.isBuildMode) {
1381
1508
  await buildImage({
1382
1509
  imageBuildArgs: IMAGE_BUILD_ARGS,
1383
1510
  imageName: runtime.imageName,
@@ -1386,7 +1513,7 @@ async function main() {
1386
1513
  imageVersionBase: IMAGE_VERSION_BASE,
1387
1514
  parseImageVersionTag,
1388
1515
  manyoyoName: MANYOYO_NAME,
1389
- yesMode: YES_MODE,
1516
+ yesMode: Boolean(modeState.yesMode),
1390
1517
  dockerCmd: DOCKER_CMD,
1391
1518
  rootDir: path.join(__dirname, '..'),
1392
1519
  loadConfig,
@@ -1399,7 +1526,8 @@ async function main() {
1399
1526
  }
1400
1527
 
1401
1528
  // 4. Handle remove container operation
1402
- if (handleRemoveContainer(runtime)) {
1529
+ if (modeState.isRemoveMode) {
1530
+ handleRemoveContainer(runtime);
1403
1531
  return;
1404
1532
  }
1405
1533
 
@@ -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
 
@@ -136,6 +136,8 @@ RUN <<EOX
136
136
  # 配置 node.js
137
137
  npm config set registry=${NPM_REGISTRY}
138
138
 
139
+ export GIT_SSL_NO_VERIFY=$GIT_SSL_NO_VERIFY
140
+
139
141
  # 安装 LSP服务(python、typescript)
140
142
  npm install -g pyright typescript-language-server typescript
141
143
 
@@ -153,7 +155,7 @@ RUN <<EOX
153
155
  }
154
156
  }
155
157
  EOF
156
- GIT_SSL_NO_VERIFY=$GIT_SSL_NO_VERIFY claude plugin marketplace add https://github.com/anthropics/claude-plugins-official
158
+ claude plugin marketplace add https://github.com/anthropics/claude-plugins-official
157
159
  claude plugin install ralph-loop@claude-plugins-official
158
160
  claude plugin install typescript-lsp@claude-plugins-official
159
161
  claude plugin install pyright-lsp@claude-plugins-official
@@ -163,11 +165,39 @@ EOF
163
165
  case ",$TOOL," in *,full,*|*,java,*)
164
166
  claude plugin install jdtls-lsp@claude-plugins-official
165
167
  ;; esac
166
-
167
- GIT_SSL_NO_VERIFY=$GIT_SSL_NO_VERIFY claude plugin marketplace add https://github.com/anthropics/skills
168
- claude plugin install example-skills@anthropic-agent-skills
168
+ claude plugin marketplace add https://github.com/anthropics/skills
169
169
  claude plugin install document-skills@anthropic-agent-skills
170
170
 
171
+ # 安装 Codex CLI
172
+ npm install -g @openai/codex
173
+ mkdir -p ~/.codex
174
+ cat > ~/.codex/config.toml <<EOF
175
+ check_for_update_on_startup = false
176
+
177
+ [analytics]
178
+ enabled = false
179
+ EOF
180
+ mkdir -p "$HOME/.codex/skills"
181
+ git clone --depth 1 https://github.com/openai/skills.git /tmp/openai-skills
182
+ cp -a /tmp/openai-skills/skills/.system "$HOME/.codex/skills/.system"
183
+ rm -rf /tmp/openai-skills
184
+ CODEX_INSTALLER="$HOME/.codex/skills/.system/skill-installer/scripts/install-skill-from-github.py"
185
+ python "$CODEX_INSTALLER" --repo openai/skills --path \
186
+ skills/.curated/doc \
187
+ skills/.curated/spreadsheet \
188
+ skills/.curated/pdf \
189
+ skills/.curated/security-best-practices \
190
+ skills/.curated/security-threat-model
191
+ python "$CODEX_INSTALLER" --repo anthropics/skills --path \
192
+ skills/pptx \
193
+ skills/theme-factory \
194
+ skills/frontend-design \
195
+ skills/canvas-design \
196
+ skills/doc-coauthoring \
197
+ skills/internal-comms \
198
+ skills/web-artifacts-builder \
199
+ skills/webapp-testing
200
+
171
201
  # 安装 Gemini CLI
172
202
  case ",$TOOL," in *,full,*|*,gemini,*)
173
203
  npm install -g @google/gemini-cli
@@ -198,18 +228,6 @@ EOF
198
228
  EOF
199
229
  ;; esac
200
230
 
201
- # 安装 Codex CLI
202
- case ",$TOOL," in *,full,*|*,codex,*)
203
- npm install -g @openai/codex
204
- mkdir -p ~/.codex
205
- cat > ~/.codex/config.toml <<EOF
206
- check_for_update_on_startup = false
207
-
208
- [analytics]
209
- enabled = false
210
- EOF
211
- ;; esac
212
-
213
231
  # 安装 OpenCode CLI
214
232
  case ",$TOOL," in *,full,*|*,opencode,*)
215
233
  npm install -g opencode-ai
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "4.2.8",
4
- "imageVersion": "1.8.0-common",
3
+ "version": "5.0.2",
4
+ "imageVersion": "1.8.1-common",
5
5
  "description": "AI Agent CLI Security Sandbox for Docker and Podman",
6
6
  "keywords": [
7
7
  "manyoyo",
@@ -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"