@xcanwin/manyoyo 2.2.1 → 3.0.1
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 +84 -80
- package/bin/manyoyo.js +315 -230
- package/docs/README_EN.md +218 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
|
+
[English](docs/README_EN.md) | [ [中文](README.md) ]
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# MANYOYO(慢悠悠)
|
|
2
6
|
|
|
3
7
|
**MANYOYO** 是一款 AI 智能体提效安全沙箱,安全、高效、省 token,专为 Agent YOLO 模式设计,保障宿主机安全。
|
|
4
8
|
|
|
5
|
-
预装常见 Agent 与工具,进一步节省 token。循环自由切换 Agent 和
|
|
9
|
+
预装常见 Agent 与工具,进一步节省 token。循环自由切换 Agent 和 `/bin/bash`,进一步提效。
|
|
6
10
|
|
|
7
|
-
MANYOYO
|
|
11
|
+
**MANYOYO** 提供隔离的 Docker/Podman 容器环境,用于安全运行 AI 智能体命令行工具。
|
|
8
12
|
|
|
9
13
|
## 功能亮点
|
|
10
14
|
|
|
11
|
-
-
|
|
15
|
+
- **多智能体支持**:支持 claude code, gemini, codex, opencode
|
|
12
16
|
- **安全隔离**:保护宿主机,支持安全容器嵌套(Docker-in-Docker)
|
|
13
|
-
-
|
|
17
|
+
- **快速启动**:快捷开启常见 Agent YOLO / SOLO 模式(例如 claude --dangerously-skip-permissions)
|
|
14
18
|
- **便捷操作**:快速进入 `/bin/bash`
|
|
15
19
|
- **会话恢复**:安装 Skills Marketplace 可快速恢复会话
|
|
16
|
-
-
|
|
20
|
+
- **灵活自定义**:支持自定义 `BASEURL`、`AUTH_TOKEN` 等变量
|
|
17
21
|
- **配置管理**:快捷导入配置文件
|
|
18
22
|
- **高级模式**:支持危险容器嵌套(mount-docker-socket)、自定义沙箱镜像
|
|
19
23
|
|
|
@@ -25,7 +29,7 @@ MANYOYO provides an isolated Docker/Podman container environment for running AI
|
|
|
25
29
|
|
|
26
30
|
## 2. 编译镜像
|
|
27
31
|
|
|
28
|
-
```
|
|
32
|
+
```bash
|
|
29
33
|
podman pull ubuntu:24.04
|
|
30
34
|
iv=1.4.0-all && podman build -t localhost/xcanwin/manyoyo:$iv -f docker/manyoyo.Dockerfile . --build-arg EXT=all --no-cache
|
|
31
35
|
podman image prune -f
|
|
@@ -33,13 +37,13 @@ podman image prune -f
|
|
|
33
37
|
|
|
34
38
|
## 3. 安装 manyoyo(选一种)
|
|
35
39
|
|
|
36
|
-
###
|
|
40
|
+
### 全局安装(推荐)
|
|
37
41
|
|
|
38
42
|
```bash
|
|
39
43
|
npm install -g @xcanwin/manyoyo
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
###
|
|
46
|
+
### 本地开发
|
|
43
47
|
|
|
44
48
|
```bash
|
|
45
49
|
npm install -g .
|
|
@@ -47,57 +51,57 @@ npm install -g .
|
|
|
47
51
|
|
|
48
52
|
## 4. 使用方法
|
|
49
53
|
|
|
50
|
-
###
|
|
54
|
+
### 基础命令
|
|
51
55
|
|
|
52
56
|
```bash
|
|
53
|
-
#
|
|
57
|
+
# 显示帮助
|
|
54
58
|
manyoyo -h
|
|
55
59
|
|
|
56
|
-
#
|
|
60
|
+
# 显示版本
|
|
57
61
|
manyoyo -V
|
|
58
62
|
|
|
59
|
-
#
|
|
63
|
+
# 列出所有容器
|
|
60
64
|
manyoyo -l
|
|
61
65
|
|
|
62
|
-
#
|
|
66
|
+
# 创建新容器并使用环境文件
|
|
63
67
|
manyoyo -n test --ef .env -y c
|
|
64
68
|
|
|
65
|
-
#
|
|
69
|
+
# 恢复现有会话
|
|
66
70
|
manyoyo -n test -- -c
|
|
67
71
|
|
|
68
|
-
#
|
|
72
|
+
# 在交互式 shell 中执行命令
|
|
69
73
|
manyoyo -n test -x /bin/bash
|
|
70
74
|
|
|
71
|
-
#
|
|
75
|
+
# 执行自定义命令
|
|
72
76
|
manyoyo -n test -x echo "hello world"
|
|
73
77
|
|
|
74
|
-
#
|
|
78
|
+
# 删除容器
|
|
75
79
|
manyoyo -n test --rm
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
###
|
|
82
|
+
### 环境变量
|
|
79
83
|
|
|
80
|
-
####
|
|
84
|
+
#### 字符串格式
|
|
81
85
|
|
|
82
86
|
```bash
|
|
83
|
-
#
|
|
87
|
+
# 直接传递
|
|
84
88
|
manyoyo -e "VAR=value" -x env
|
|
85
89
|
|
|
86
|
-
#
|
|
90
|
+
# 多个变量
|
|
87
91
|
manyoyo -e "A=1" -e "B=2" -x env
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
####
|
|
94
|
+
#### 文件格式
|
|
91
95
|
|
|
92
96
|
```bash
|
|
93
|
-
#
|
|
97
|
+
# 从文件加载
|
|
94
98
|
manyoyo --ef .env -x env
|
|
95
99
|
```
|
|
96
100
|
|
|
97
|
-
|
|
101
|
+
环境文件(`.env`)支持以下格式:
|
|
98
102
|
|
|
99
103
|
```bash
|
|
100
|
-
#
|
|
104
|
+
# 使用 export 语句
|
|
101
105
|
export ANTHROPIC_BASE_URL="https://api.anthropic.com"
|
|
102
106
|
# export CLAUDE_CODE_OAUTH_TOKEN="sk-xxxxxxxx"
|
|
103
107
|
export ANTHROPIC_AUTH_TOKEN="sk-xxxxxxxx"
|
|
@@ -108,107 +112,107 @@ export ANTHROPIC_DEFAULT_SONNET_MODEL="claude-sonnet-4-5"
|
|
|
108
112
|
export ANTHROPIC_DEFAULT_HAIKU_MODEL="claude-haiku-4-5"
|
|
109
113
|
export CLAUDE_CODE_SUBAGENT_MODEL="claude-sonnet-4-5"
|
|
110
114
|
|
|
111
|
-
#
|
|
115
|
+
# 简单的键值对
|
|
112
116
|
API_KEY=your-api-key-here
|
|
113
117
|
|
|
114
|
-
#
|
|
118
|
+
# 带引号的值(引号会被移除)
|
|
115
119
|
MESSAGE="Hello World"
|
|
116
120
|
PATH='/usr/local/bin'
|
|
117
121
|
```
|
|
118
122
|
|
|
119
|
-
### AI CLI
|
|
123
|
+
### AI CLI 快捷方式(跳过权限确认)
|
|
120
124
|
|
|
121
125
|
```bash
|
|
122
126
|
# Claude Code
|
|
123
|
-
manyoyo -y c #
|
|
127
|
+
manyoyo -y c # 或: claude, cc
|
|
124
128
|
|
|
125
129
|
# Gemini
|
|
126
|
-
manyoyo -y gm #
|
|
130
|
+
manyoyo -y gm # 或: gemini, g
|
|
127
131
|
|
|
128
132
|
# Codex
|
|
129
|
-
manyoyo -y cx #
|
|
133
|
+
manyoyo -y cx # 或: codex
|
|
130
134
|
|
|
131
135
|
# OpenCode
|
|
132
|
-
manyoyo -y oc #
|
|
136
|
+
manyoyo -y oc # 或: opencode
|
|
133
137
|
```
|
|
134
138
|
|
|
135
|
-
###
|
|
139
|
+
### 交互式会话管理
|
|
136
140
|
|
|
137
|
-
|
|
141
|
+
退出容器会话后,系统将提示您选择操作:
|
|
138
142
|
|
|
139
|
-
- `y` -
|
|
140
|
-
- `n` -
|
|
141
|
-
- `1` -
|
|
142
|
-
- `
|
|
143
|
-
- `i` -
|
|
143
|
+
- `y` - 保持容器在后台运行(默认)
|
|
144
|
+
- `n` - 删除容器
|
|
145
|
+
- `1` - 使用首次命令重新进入
|
|
146
|
+
- `x` - 执行新命令
|
|
147
|
+
- `i` - 进入交互式 shell
|
|
144
148
|
|
|
145
|
-
###
|
|
149
|
+
### 容器模式
|
|
146
150
|
|
|
147
|
-
#### Docker-in-Docker
|
|
151
|
+
#### Docker-in-Docker 开发
|
|
148
152
|
|
|
149
153
|
```bash
|
|
150
|
-
# Docker-in-Docker
|
|
151
|
-
#
|
|
154
|
+
# Docker-in-Docker(安全的嵌套容器)
|
|
155
|
+
# 创建支持 Docker-in-Docker 的容器
|
|
152
156
|
manyoyo -n docker-dev -m dind -x /bin/bash
|
|
153
157
|
|
|
154
|
-
#
|
|
158
|
+
# 在容器内启动 dockerd
|
|
155
159
|
nohup dockerd &
|
|
156
160
|
|
|
157
|
-
#
|
|
161
|
+
# 现在可以在容器内使用 docker 命令
|
|
158
162
|
docker run hello-world
|
|
159
163
|
```
|
|
160
164
|
|
|
161
|
-
####
|
|
165
|
+
#### 挂载 Docker Socket 开发
|
|
162
166
|
|
|
163
167
|
```bash
|
|
164
|
-
#
|
|
168
|
+
# 挂载 Docker Socket(危险 - 容器可以访问宿主机)
|
|
165
169
|
manyoyo -n socket-dev -m mdsock -x docker ps
|
|
166
170
|
```
|
|
167
171
|
|
|
168
|
-
###
|
|
169
|
-
|
|
170
|
-
|
|
|
171
|
-
|
|
172
|
-
| `-l` | `--ls`, `--list` |
|
|
173
|
-
| `--hp PATH` | `--host-path` |
|
|
174
|
-
| `-n NAME` | `--cn`, `--cont-name` |
|
|
175
|
-
| `--cp PATH` | `--cont-path` |
|
|
176
|
-
| `--in NAME` | `--image-name` |
|
|
177
|
-
| `--iv VERSION` | `--image-ver` |
|
|
178
|
-
| `-e STRING` | `--env` |
|
|
179
|
-
| `--ef FILE` | `--env-file` |
|
|
180
|
-
| `-v STRING` | `--volume` |
|
|
181
|
-
| `--rm` | `--rmc`, `--remove-cont` |
|
|
182
|
-
| `--sp CMD` | `--shell-prefix` |
|
|
183
|
-
| `-s CMD` | `--shell` |
|
|
184
|
-
| `--` | `--ss`, `--shell-suffix` |
|
|
185
|
-
| `-x CMD` | `--sf`, `--shell-full` |
|
|
186
|
-
| `-y CLI` | `--yolo` |
|
|
187
|
-
| `-m MODE` | `--cm`, `--cont-mode` |
|
|
188
|
-
| `--install NAME` | |
|
|
189
|
-
| `-V` | `--version` |
|
|
190
|
-
| `-h` | `--help` |
|
|
172
|
+
### 命令行选项
|
|
173
|
+
|
|
174
|
+
| 选项 | 别名 | 描述 |
|
|
175
|
+
|------|------|------|
|
|
176
|
+
| `-l` | `--ls`, `--list` | 列出所有 manyoyo 容器 |
|
|
177
|
+
| `--hp PATH` | `--host-path` | 设置宿主机工作目录(默认:当前路径) |
|
|
178
|
+
| `-n NAME` | `--cn`, `--cont-name` | 设置容器名称 |
|
|
179
|
+
| `--cp PATH` | `--cont-path` | 设置容器工作目录 |
|
|
180
|
+
| `--in NAME` | `--image-name` | 指定镜像名称 |
|
|
181
|
+
| `--iv VERSION` | `--image-ver` | 指定镜像版本 |
|
|
182
|
+
| `-e STRING` | `--env` | 设置环境变量 |
|
|
183
|
+
| `--ef FILE` | `--env-file` | 从文件加载环境变量 |
|
|
184
|
+
| `-v STRING` | `--volume` | 绑定挂载卷 |
|
|
185
|
+
| `--rm` | `--rmc`, `--remove-cont` | 删除容器 |
|
|
186
|
+
| `--sp CMD` | `--shell-prefix` | 临时环境变量(作为 -s 的前缀) |
|
|
187
|
+
| `-s CMD` | `--shell` | 指定要执行的命令 |
|
|
188
|
+
| `--` | `--ss`, `--shell-suffix` | 命令参数(作为 -s 的后缀) |
|
|
189
|
+
| `-x CMD` | `--sf`, `--shell-full` | 完整命令(替代 --sp, -s 和 --) |
|
|
190
|
+
| `-y CLI` | `--yolo` | 无需确认运行 AI 智能体 |
|
|
191
|
+
| `-m MODE` | `--cm`, `--cont-mode` | 设置容器模式(common, dind, mdsock) |
|
|
192
|
+
| `--install NAME` | | 安装 manyoyo 命令 |
|
|
193
|
+
| `-V` | `--version` | 显示版本 |
|
|
194
|
+
| `-h` | `--help` | 显示帮助 |
|
|
191
195
|
|
|
192
196
|
## 其他说明
|
|
193
197
|
|
|
194
|
-
###
|
|
198
|
+
### 默认配置
|
|
195
199
|
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
+
- **容器名称**:`myy-{月日-时分}`(基于当前时间自动生成)
|
|
201
|
+
- **宿主机路径**:当前工作目录
|
|
202
|
+
- **容器路径**:与宿主机路径相同
|
|
203
|
+
- **镜像**:`localhost/xcanwin/manyoyo:xxx`
|
|
200
204
|
|
|
201
|
-
###
|
|
205
|
+
### 系统要求
|
|
202
206
|
|
|
203
|
-
- Node.js >=
|
|
204
|
-
- Docker
|
|
207
|
+
- Node.js >= 24.0.0
|
|
208
|
+
- Docker 或 Podman
|
|
205
209
|
|
|
206
|
-
###
|
|
210
|
+
### 卸载
|
|
207
211
|
|
|
208
212
|
```bash
|
|
209
213
|
npm uninstall -g @xcanwin/manyoyo
|
|
210
214
|
```
|
|
211
215
|
|
|
212
|
-
##
|
|
216
|
+
## 许可证
|
|
213
217
|
|
|
214
218
|
MIT
|
package/bin/manyoyo.js
CHANGED
|
@@ -47,7 +47,19 @@ const NC = '\x1b[0m'; // No Color
|
|
|
47
47
|
// Docker command (will be set by ensure_docker)
|
|
48
48
|
let DOCKER_CMD = 'docker';
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// ==============================================================================
|
|
51
|
+
// Utility Functions
|
|
52
|
+
// ==============================================================================
|
|
53
|
+
|
|
54
|
+
function sleep(ms) {
|
|
55
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ==============================================================================
|
|
59
|
+
// UI Functions
|
|
60
|
+
// ==============================================================================
|
|
61
|
+
|
|
62
|
+
function showHelp() {
|
|
51
63
|
console.log(`${BLUE}Usage:${NC}`);
|
|
52
64
|
console.log(` ${MANYOYO_NAME} [OPTIONS]`);
|
|
53
65
|
console.log(` ${MANYOYO_NAME} [--hp HOST_PATH] [-n CONTAINER_NAME] [--cp CONTAINER_PATH] [--ef ENV_FILE] [--sp COMMAND] [-s COMMAND] [-- COMMAND]`);
|
|
@@ -84,41 +96,80 @@ function show_help() {
|
|
|
84
96
|
console.log(` ${MANYOYO_NAME} -n test -x claude -c 恢复之前会话`);
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
function
|
|
88
|
-
|
|
89
|
-
execSync('docker --version', { stdio: 'pipe' });
|
|
90
|
-
DOCKER_CMD = 'docker';
|
|
91
|
-
return true;
|
|
92
|
-
} catch (e) {
|
|
93
|
-
try {
|
|
94
|
-
execSync('podman --version', { stdio: 'pipe' });
|
|
95
|
-
DOCKER_CMD = 'podman';
|
|
96
|
-
return true;
|
|
97
|
-
} catch (e2) {
|
|
98
|
-
console.error("docker/podman not found");
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
99
|
+
function showVersion() {
|
|
100
|
+
console.log(`manyoyo by xcanwin, ${BIN_VERSION}`);
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
function getHelloTip(containerName, defaultCommand) {
|
|
104
|
+
console.log(`${BLUE}----------------------------------------${NC}`);
|
|
105
|
+
console.log(`📦 首次命令 : ${defaultCommand}`);
|
|
106
|
+
console.log(`⚫ 恢复首次命令会话: ${CYAN}${MANYOYO_NAME} -n ${containerName} -- -c${NC}`);
|
|
107
|
+
console.log(`⚫ 执行首次命令 : ${GREEN}${MANYOYO_NAME} -n ${containerName}${NC}`);
|
|
108
|
+
console.log(`⚫ 执行指定命令 : ${GREEN}${MANYOYO_NAME} -n ${containerName} -x /bin/bash${NC}`);
|
|
109
|
+
console.log(`⚫ 执行指定命令 : ${GREEN}docker exec -it ${containerName} /bin/bash${NC}`);
|
|
110
|
+
console.log(`⚫ 删除容器 : ${MANYOYO_NAME} -n ${containerName} --rm`);
|
|
111
|
+
console.log("");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function askQuestion(prompt) {
|
|
115
|
+
const rl = readline.createInterface({
|
|
116
|
+
input: process.stdin,
|
|
117
|
+
output: process.stdout
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
rl.question(prompt, (answer) => {
|
|
122
|
+
rl.close();
|
|
123
|
+
resolve(answer);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ==============================================================================
|
|
129
|
+
// Configuration Functions
|
|
130
|
+
// ==============================================================================
|
|
131
|
+
|
|
132
|
+
function addEnv(env) {
|
|
133
|
+
CONTAINER_ENVS.push("--env", env);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function addEnvFile(envFile) {
|
|
137
|
+
ENV_FILE = envFile;
|
|
138
|
+
if (ENV_FILE && fs.existsSync(ENV_FILE)) {
|
|
139
|
+
const content = fs.readFileSync(ENV_FILE, 'utf-8');
|
|
140
|
+
const lines = content.split('\n');
|
|
141
|
+
|
|
142
|
+
for (let line of lines) {
|
|
143
|
+
// Match pattern: (export )?(KEY)=(VALUE)
|
|
144
|
+
const match = line.match(/^(?:export\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/);
|
|
145
|
+
if (match) {
|
|
146
|
+
let key = match[1];
|
|
147
|
+
let value = match[2].trim();
|
|
148
|
+
|
|
149
|
+
// Filter malicious characters
|
|
150
|
+
if (/[\$\(\)\`\|\&\*\{\}]/.test(value)) continue;
|
|
151
|
+
if (/^\(/.test(value)) continue;
|
|
152
|
+
|
|
153
|
+
// Remove quotes
|
|
154
|
+
if (/^"(.*)"$/.test(value)) {
|
|
155
|
+
value = value.slice(1, -1);
|
|
156
|
+
} else if (/^'(.*)'$/.test(value)) {
|
|
157
|
+
value = value.slice(1, -1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (key) {
|
|
161
|
+
CONTAINER_ENVS.push("--env", `${key}=${value}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
113
165
|
}
|
|
114
|
-
process.exit(0);
|
|
115
166
|
}
|
|
116
167
|
|
|
117
|
-
function
|
|
118
|
-
|
|
168
|
+
function addVolume(volume) {
|
|
169
|
+
CONTAINER_VOLUMES.push("--volume", volume);
|
|
119
170
|
}
|
|
120
171
|
|
|
121
|
-
function
|
|
172
|
+
function setYolo(cli) {
|
|
122
173
|
switch (cli) {
|
|
123
174
|
case 'claude':
|
|
124
175
|
case 'cc':
|
|
@@ -144,7 +195,7 @@ function set_yolo(cli) {
|
|
|
144
195
|
}
|
|
145
196
|
}
|
|
146
197
|
|
|
147
|
-
function
|
|
198
|
+
function setContMode(mode) {
|
|
148
199
|
switch (mode) {
|
|
149
200
|
case 'common':
|
|
150
201
|
CONT_MODE = "";
|
|
@@ -167,105 +218,96 @@ function set_cont_mode(mode) {
|
|
|
167
218
|
}
|
|
168
219
|
}
|
|
169
220
|
|
|
170
|
-
|
|
221
|
+
// ==============================================================================
|
|
222
|
+
// Docker Helper Functions
|
|
223
|
+
// ==============================================================================
|
|
224
|
+
|
|
225
|
+
function dockerExec(cmd, options = {}) {
|
|
171
226
|
try {
|
|
172
|
-
|
|
173
|
-
{ encoding: 'utf-8' });
|
|
174
|
-
console.log(result);
|
|
227
|
+
return execSync(cmd, { encoding: 'utf-8', ...options });
|
|
175
228
|
} catch (e) {
|
|
176
|
-
|
|
229
|
+
if (options.ignoreError) {
|
|
230
|
+
return e.stdout || '';
|
|
231
|
+
}
|
|
232
|
+
throw e;
|
|
177
233
|
}
|
|
178
234
|
}
|
|
179
235
|
|
|
180
|
-
function
|
|
181
|
-
|
|
236
|
+
function containerExists(name) {
|
|
237
|
+
const containers = dockerExec(`${DOCKER_CMD} ps -a --format '{{.Names}}'`);
|
|
238
|
+
return containers.split('\n').some(n => n.trim() === name);
|
|
182
239
|
}
|
|
183
240
|
|
|
184
|
-
function
|
|
185
|
-
|
|
241
|
+
function getContainerStatus(name) {
|
|
242
|
+
return dockerExec(`${DOCKER_CMD} inspect -f '{{.State.Status}}' "${name}"`).trim();
|
|
186
243
|
}
|
|
187
244
|
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
for (let line of lines) {
|
|
195
|
-
// Match pattern: (export )?(KEY)=(VALUE)
|
|
196
|
-
const match = line.match(/^(?:export\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.*)$/);
|
|
197
|
-
if (match) {
|
|
198
|
-
let key = match[1];
|
|
199
|
-
let value = match[2].trim();
|
|
200
|
-
|
|
201
|
-
// Filter malicious characters
|
|
202
|
-
if (/[\$\(\)\`\|\&\*\{\}]/.test(value)) continue;
|
|
203
|
-
if (/^\(/.test(value)) continue;
|
|
245
|
+
function removeContainer(name) {
|
|
246
|
+
console.log(`${YELLOW}🗑️ 正在删除容器: ${name}...${NC}`);
|
|
247
|
+
dockerExec(`${DOCKER_CMD} rm -f "${name}"`, { stdio: 'pipe' });
|
|
248
|
+
console.log(`${GREEN}✅ 已彻底删除。${NC}`);
|
|
249
|
+
}
|
|
204
250
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
} else if (/^'(.*)'$/.test(value)) {
|
|
209
|
-
value = value.slice(1, -1);
|
|
210
|
-
}
|
|
251
|
+
// ==============================================================================
|
|
252
|
+
// Docker Operations
|
|
253
|
+
// ==============================================================================
|
|
211
254
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
255
|
+
function ensureDocker() {
|
|
256
|
+
const commands = ['docker', 'podman'];
|
|
257
|
+
for (const cmd of commands) {
|
|
258
|
+
try {
|
|
259
|
+
execSync(`${cmd} --version`, { stdio: 'pipe' });
|
|
260
|
+
DOCKER_CMD = cmd;
|
|
261
|
+
return true;
|
|
262
|
+
} catch (e) {
|
|
263
|
+
// Try next command
|
|
216
264
|
}
|
|
217
265
|
}
|
|
266
|
+
console.error("docker/podman not found");
|
|
267
|
+
process.exit(1);
|
|
218
268
|
}
|
|
219
269
|
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
270
|
+
function installManyoyo(name) {
|
|
271
|
+
const MANYOYO_FILE = fs.realpathSync(__filename);
|
|
272
|
+
switch (name) {
|
|
273
|
+
case 'docker-cli-plugin':
|
|
274
|
+
const pluginDir = path.join(process.env.HOME, '.docker/cli-plugins');
|
|
275
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
276
|
+
const targetPath = path.join(pluginDir, 'docker-manyoyo');
|
|
277
|
+
if (fs.existsSync(targetPath)) {
|
|
278
|
+
fs.unlinkSync(targetPath);
|
|
279
|
+
}
|
|
280
|
+
fs.symlinkSync(MANYOYO_FILE, targetPath);
|
|
281
|
+
break;
|
|
282
|
+
default:
|
|
283
|
+
console.log("");
|
|
284
|
+
}
|
|
285
|
+
process.exit(0);
|
|
229
286
|
}
|
|
230
287
|
|
|
231
|
-
function
|
|
288
|
+
function getContList() {
|
|
232
289
|
try {
|
|
233
|
-
|
|
290
|
+
const result = execSync(`${DOCKER_CMD} ps -a --size --filter "ancestor=manyoyo" --filter "ancestor=$(${DOCKER_CMD} images -a --format '{{.Repository}}:{{.Tag}}' | grep manyoyo)" --format "table {{.Names}}\\t{{.Status}}\\t{{.Size}}\\t{{.ID}}\\t{{.Image}}\\t{{.Ports}}\\t{{.Networks}}\\t{{.Mounts}}"`,
|
|
291
|
+
{ encoding: 'utf-8' });
|
|
292
|
+
console.log(result);
|
|
234
293
|
} catch (e) {
|
|
235
|
-
|
|
236
|
-
return e.stdout || '';
|
|
237
|
-
}
|
|
238
|
-
throw e;
|
|
294
|
+
console.log(e.stdout || '');
|
|
239
295
|
}
|
|
240
296
|
}
|
|
241
297
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
async function askQuestion(prompt) {
|
|
247
|
-
const rl = readline.createInterface({
|
|
248
|
-
input: process.stdin,
|
|
249
|
-
output: process.stdout
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
return new Promise((resolve) => {
|
|
253
|
-
rl.question(prompt, (answer) => {
|
|
254
|
-
rl.close();
|
|
255
|
-
resolve(answer);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
}
|
|
298
|
+
// ==============================================================================
|
|
299
|
+
// Main Function Helpers
|
|
300
|
+
// ==============================================================================
|
|
259
301
|
|
|
260
|
-
|
|
302
|
+
function validateAndInitialize() {
|
|
261
303
|
// Check if no arguments provided
|
|
262
304
|
if (process.argv.length <= 2) {
|
|
263
|
-
|
|
305
|
+
showHelp();
|
|
264
306
|
process.exit(1);
|
|
265
307
|
}
|
|
266
308
|
|
|
267
309
|
// Ensure docker/podman is available
|
|
268
|
-
|
|
310
|
+
ensureDocker();
|
|
269
311
|
|
|
270
312
|
// Docker CLI plugin metadata
|
|
271
313
|
if (process.argv[2] === 'docker-cli-plugin-metadata') {
|
|
@@ -278,13 +320,15 @@ async function main() {
|
|
|
278
320
|
console.log(JSON.stringify(metadata, null, 2));
|
|
279
321
|
process.exit(0);
|
|
280
322
|
}
|
|
323
|
+
}
|
|
281
324
|
|
|
325
|
+
function parseArguments(argv) {
|
|
282
326
|
// Parse arguments
|
|
283
|
-
let args =
|
|
327
|
+
let args = argv.slice(2);
|
|
284
328
|
|
|
285
329
|
// Docker CLI plugin mode - remove first arg if running as plugin
|
|
286
330
|
const dockerPluginPath = path.join(process.env.HOME || '', '.docker/cli-plugins/docker-manyoyo');
|
|
287
|
-
if (
|
|
331
|
+
if (argv[1] === dockerPluginPath && args[0] === 'manyoyo') {
|
|
288
332
|
args.shift();
|
|
289
333
|
}
|
|
290
334
|
|
|
@@ -297,7 +341,7 @@ async function main() {
|
|
|
297
341
|
case '-l':
|
|
298
342
|
case '--ls':
|
|
299
343
|
case '--list':
|
|
300
|
-
|
|
344
|
+
getContList();
|
|
301
345
|
process.exit(0);
|
|
302
346
|
|
|
303
347
|
case '--hp':
|
|
@@ -333,19 +377,19 @@ async function main() {
|
|
|
333
377
|
|
|
334
378
|
case '-e':
|
|
335
379
|
case '--env':
|
|
336
|
-
|
|
380
|
+
addEnv(args[i + 1]);
|
|
337
381
|
i += 2;
|
|
338
382
|
break;
|
|
339
383
|
|
|
340
384
|
case '--ef':
|
|
341
385
|
case '--env-file':
|
|
342
|
-
|
|
386
|
+
addEnvFile(args[i + 1]);
|
|
343
387
|
i += 2;
|
|
344
388
|
break;
|
|
345
389
|
|
|
346
390
|
case '-v':
|
|
347
391
|
case '--volume':
|
|
348
|
-
|
|
392
|
+
addVolume(args[i + 1]);
|
|
349
393
|
i += 2;
|
|
350
394
|
break;
|
|
351
395
|
|
|
@@ -384,46 +428,44 @@ async function main() {
|
|
|
384
428
|
|
|
385
429
|
case '-y':
|
|
386
430
|
case '--yolo':
|
|
387
|
-
|
|
431
|
+
setYolo(args[i + 1]);
|
|
388
432
|
i += 2;
|
|
389
433
|
break;
|
|
390
434
|
|
|
391
435
|
case '-m':
|
|
392
436
|
case '--cm':
|
|
393
437
|
case '--cont-mode':
|
|
394
|
-
|
|
438
|
+
setContMode(args[i + 1]);
|
|
395
439
|
i += 2;
|
|
396
440
|
break;
|
|
397
441
|
|
|
398
442
|
case '--install':
|
|
399
|
-
|
|
443
|
+
installManyoyo(args[i + 1]);
|
|
400
444
|
process.exit(0);
|
|
401
445
|
|
|
402
446
|
case '-V':
|
|
403
447
|
case '--version':
|
|
404
|
-
|
|
448
|
+
showVersion();
|
|
405
449
|
process.exit(0);
|
|
406
450
|
|
|
407
451
|
case '-h':
|
|
408
452
|
case '--help':
|
|
409
|
-
|
|
453
|
+
showHelp();
|
|
410
454
|
process.exit(0);
|
|
411
455
|
|
|
412
456
|
default:
|
|
413
457
|
console.log(`${RED}⚠️ 未知参数: ${arg}${NC}`);
|
|
414
|
-
|
|
458
|
+
showHelp();
|
|
415
459
|
process.exit(1);
|
|
416
460
|
}
|
|
417
461
|
}
|
|
462
|
+
}
|
|
418
463
|
|
|
419
|
-
|
|
464
|
+
function handleRemoveContainer() {
|
|
420
465
|
if (SHOULD_REMOVE) {
|
|
421
466
|
try {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
console.log(`${YELLOW}🗑️ 正在删除容器: ${CONTAINER_NAME}...${NC}`);
|
|
425
|
-
docker_exec(`${DOCKER_CMD} rm -f "${CONTAINER_NAME}"`, { stdio: 'pipe' });
|
|
426
|
-
console.log(`${GREEN}✅ 已彻底删除。${NC}`);
|
|
467
|
+
if (containerExists(CONTAINER_NAME)) {
|
|
468
|
+
removeContainer(CONTAINER_NAME);
|
|
427
469
|
} else {
|
|
428
470
|
console.log(`${RED}⚠️ 错误: 未找到名为 ${CONTAINER_NAME} 的容器。${NC}`);
|
|
429
471
|
}
|
|
@@ -432,136 +474,179 @@ async function main() {
|
|
|
432
474
|
}
|
|
433
475
|
process.exit(0);
|
|
434
476
|
}
|
|
477
|
+
}
|
|
435
478
|
|
|
436
|
-
|
|
479
|
+
function validateHostPath() {
|
|
437
480
|
const realHostPath = fs.realpathSync(HOST_PATH);
|
|
438
481
|
const homeDir = process.env.HOME || '/home';
|
|
439
482
|
if (realHostPath === '/' || realHostPath === '/home' || realHostPath === homeDir) {
|
|
440
483
|
console.log(`${RED}⚠️ 错误: 不允许挂载根目录或home目录。${NC}`);
|
|
441
484
|
process.exit(1);
|
|
442
485
|
}
|
|
486
|
+
}
|
|
443
487
|
|
|
444
|
-
|
|
445
|
-
|
|
488
|
+
async function waitForContainerReady(containerName) {
|
|
489
|
+
const MAX_RETRIES = 50;
|
|
490
|
+
let count = 0;
|
|
491
|
+
while (true) {
|
|
492
|
+
try {
|
|
493
|
+
const status = getContainerStatus(containerName);
|
|
446
494
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const containers = docker_exec(`${DOCKER_CMD} ps -a --format '{{.Names}}'`);
|
|
450
|
-
const containerExists = containers.split('\n').some(name => name.trim() === CONTAINER_NAME);
|
|
451
|
-
|
|
452
|
-
if (!containerExists) {
|
|
453
|
-
// Create new container
|
|
454
|
-
console.log(`${CYAN}📦 manyoyo by xcanwin 正在创建新容器: ${YELLOW}${CONTAINER_NAME}${NC}\n`);
|
|
455
|
-
EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
|
|
456
|
-
DEFAULT_COMMAND = EXEC_COMMAND;
|
|
457
|
-
|
|
458
|
-
// Build docker run command
|
|
459
|
-
const envArgs = CONTAINER_ENVS.join(' ');
|
|
460
|
-
const volumeArgs = CONTAINER_VOLUMES.join(' ');
|
|
461
|
-
const contModeArg = CONT_MODE ? CONT_MODE : '';
|
|
462
|
-
|
|
463
|
-
const dockerRunCmd = `${DOCKER_CMD} run -d --name "${CONTAINER_NAME}" --entrypoint "" ${contModeArg} ${envArgs} ${volumeArgs} --volume "${HOST_PATH}:${CONTAINER_PATH}" --workdir "${CONTAINER_PATH}" --label "manyoyo.default_cmd=${EXEC_COMMAND}" "${FULL_IMAGE}" tail -f /dev/null`;
|
|
464
|
-
|
|
465
|
-
docker_exec(dockerRunCmd, { stdio: 'pipe' });
|
|
466
|
-
|
|
467
|
-
// Wait for container to be ready
|
|
468
|
-
const MAX_RETRIES = 50;
|
|
469
|
-
let count = 0;
|
|
470
|
-
while (true) {
|
|
471
|
-
try {
|
|
472
|
-
const status = docker_exec(`${DOCKER_CMD} inspect -f '{{.State.Status}}' "${CONTAINER_NAME}"`).trim();
|
|
473
|
-
|
|
474
|
-
if (status === 'running') {
|
|
475
|
-
break;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
if (status === 'exited') {
|
|
479
|
-
console.log(`${RED}⚠️ 错误: 容器启动后立即退出。${NC}`);
|
|
480
|
-
docker_exec(`${DOCKER_CMD} logs "${CONTAINER_NAME}"`, { stdio: 'inherit' });
|
|
481
|
-
process.exit(1);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
await sleep(100);
|
|
485
|
-
count++;
|
|
486
|
-
|
|
487
|
-
if (count >= MAX_RETRIES) {
|
|
488
|
-
console.log(`${RED}⚠️ 错误: 容器启动超时(当前状态: ${status})。${NC}`);
|
|
489
|
-
docker_exec(`${DOCKER_CMD} logs "${CONTAINER_NAME}"`, { stdio: 'inherit' });
|
|
490
|
-
process.exit(1);
|
|
491
|
-
}
|
|
492
|
-
} catch (e) {
|
|
493
|
-
await sleep(100);
|
|
494
|
-
count++;
|
|
495
|
-
if (count >= MAX_RETRIES) {
|
|
496
|
-
console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
|
|
497
|
-
process.exit(1);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
495
|
+
if (status === 'running') {
|
|
496
|
+
break;
|
|
500
497
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const status = docker_exec(`${DOCKER_CMD} inspect -f '{{.State.Status}}' "${CONTAINER_NAME}"`).trim();
|
|
507
|
-
if (status !== 'running') {
|
|
508
|
-
docker_exec(`${DOCKER_CMD} start "${CONTAINER_NAME}"`, { stdio: 'pipe' });
|
|
498
|
+
|
|
499
|
+
if (status === 'exited') {
|
|
500
|
+
console.log(`${RED}⚠️ 错误: 容器启动后立即退出。${NC}`);
|
|
501
|
+
dockerExec(`${DOCKER_CMD} logs "${containerName}"`, { stdio: 'inherit' });
|
|
502
|
+
process.exit(1);
|
|
509
503
|
}
|
|
510
504
|
|
|
511
|
-
|
|
512
|
-
|
|
505
|
+
await sleep(100);
|
|
506
|
+
count++;
|
|
513
507
|
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
508
|
+
if (count >= MAX_RETRIES) {
|
|
509
|
+
console.log(`${RED}⚠️ 错误: 容器启动超时(当前状态: ${status})。${NC}`);
|
|
510
|
+
dockerExec(`${DOCKER_CMD} logs "${containerName}"`, { stdio: 'inherit' });
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
} catch (e) {
|
|
514
|
+
await sleep(100);
|
|
515
|
+
count++;
|
|
516
|
+
if (count >= MAX_RETRIES) {
|
|
517
|
+
console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
|
|
518
|
+
process.exit(1);
|
|
518
519
|
}
|
|
519
520
|
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
520
523
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
console.log(`💻 执行命令: ${YELLOW}${EXEC_COMMAND || '交互式 Shell'}${NC}`);
|
|
524
|
+
async function createNewContainer() {
|
|
525
|
+
console.log(`${CYAN}📦 manyoyo by xcanwin 正在创建新容器: ${YELLOW}${CONTAINER_NAME}${NC}\n`);
|
|
524
526
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', CONTAINER_NAME, '/bin/bash', '-c', EXEC_COMMAND], { stdio: 'inherit' });
|
|
528
|
-
} else {
|
|
529
|
-
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', CONTAINER_NAME, '/bin/bash'], { stdio: 'inherit' });
|
|
530
|
-
}
|
|
527
|
+
EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
|
|
528
|
+
const defaultCommand = EXEC_COMMAND;
|
|
531
529
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
530
|
+
// Build docker run command
|
|
531
|
+
const fullImage = `${IMAGE_NAME}:${IMAGE_VERSION}`;
|
|
532
|
+
const envArgs = CONTAINER_ENVS.join(' ');
|
|
533
|
+
const volumeArgs = CONTAINER_VOLUMES.join(' ');
|
|
534
|
+
const contModeArg = CONT_MODE || '';
|
|
535
|
+
|
|
536
|
+
const dockerRunCmd = `${DOCKER_CMD} run -d --name "${CONTAINER_NAME}" --entrypoint "" ${contModeArg} ${envArgs} ${volumeArgs} --volume "${HOST_PATH}:${CONTAINER_PATH}" --workdir "${CONTAINER_PATH}" --label "manyoyo.default_cmd=${EXEC_COMMAND}" "${fullImage}" tail -f /dev/null`;
|
|
537
|
+
|
|
538
|
+
dockerExec(dockerRunCmd, { stdio: 'pipe' });
|
|
539
|
+
|
|
540
|
+
// Wait for container to be ready
|
|
541
|
+
await waitForContainerReady(CONTAINER_NAME);
|
|
542
|
+
|
|
543
|
+
return defaultCommand;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function connectExistingContainer() {
|
|
547
|
+
console.log(`${CYAN}🔄 manyoyo by xcanwin 正在连接到现有容器: ${YELLOW}${CONTAINER_NAME}${NC}`);
|
|
548
|
+
|
|
549
|
+
// Start container if stopped
|
|
550
|
+
const status = getContainerStatus(CONTAINER_NAME);
|
|
551
|
+
if (status !== 'running') {
|
|
552
|
+
dockerExec(`${DOCKER_CMD} start "${CONTAINER_NAME}"`, { stdio: 'pipe' });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Get default command from label
|
|
556
|
+
const defaultCommand = dockerExec(`${DOCKER_CMD} inspect -f '{{index .Config.Labels "manyoyo.default_cmd"}}' "${CONTAINER_NAME}"`).trim();
|
|
557
|
+
|
|
558
|
+
if (!EXEC_COMMAND) {
|
|
559
|
+
EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${defaultCommand}${EXEC_COMMAND_SUFFIX}`;
|
|
560
|
+
} else {
|
|
561
|
+
EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return defaultCommand;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function setupContainer() {
|
|
568
|
+
if (!containerExists(CONTAINER_NAME)) {
|
|
569
|
+
return await createNewContainer();
|
|
570
|
+
} else {
|
|
571
|
+
return await connectExistingContainer();
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function executeInContainer(defaultCommand) {
|
|
576
|
+
getHelloTip(CONTAINER_NAME, defaultCommand);
|
|
577
|
+
console.log(`${BLUE}----------------------------------------${NC}`);
|
|
578
|
+
console.log(`💻 执行命令: ${YELLOW}${EXEC_COMMAND || '交互式 Shell'}${NC}`);
|
|
579
|
+
|
|
580
|
+
// Execute command in container
|
|
581
|
+
if (EXEC_COMMAND) {
|
|
582
|
+
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', CONTAINER_NAME, '/bin/bash', '-c', EXEC_COMMAND], { stdio: 'inherit' });
|
|
583
|
+
} else {
|
|
584
|
+
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', CONTAINER_NAME, '/bin/bash'], { stdio: 'inherit' });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
async function handlePostExit(defaultCommand) {
|
|
589
|
+
console.log("");
|
|
590
|
+
getHelloTip(CONTAINER_NAME, defaultCommand);
|
|
591
|
+
|
|
592
|
+
const reply = await askQuestion(`❔ 会话已结束。是否保留此后台容器 ${CONTAINER_NAME}? [ y=默认保留, n=删除, 1=首次命令进入, x=执行命令, i=交互式SHELL ]: `);
|
|
593
|
+
console.log("");
|
|
594
|
+
|
|
595
|
+
const firstChar = reply.trim().toLowerCase()[0];
|
|
596
|
+
|
|
597
|
+
if (firstChar === 'n') {
|
|
598
|
+
removeContainer(CONTAINER_NAME);
|
|
599
|
+
} else if (firstChar === '1') {
|
|
600
|
+
console.log(`${GREEN}✅ 离开当前连接,用首次命令进入。${NC}`);
|
|
601
|
+
// Reset command variables to use default command
|
|
602
|
+
EXEC_COMMAND = "";
|
|
603
|
+
EXEC_COMMAND_PREFIX = "";
|
|
604
|
+
EXEC_COMMAND_SUFFIX = "";
|
|
605
|
+
const newArgs = ['-n', CONTAINER_NAME];
|
|
606
|
+
process.argv = [process.argv[0], process.argv[1], ...newArgs];
|
|
607
|
+
await main();
|
|
608
|
+
} else if (firstChar === 'x') {
|
|
609
|
+
const command = await askQuestion('❔ 输入要执行的命令: ');
|
|
610
|
+
console.log(`${GREEN}✅ 离开当前连接,执行命令。${NC}`);
|
|
611
|
+
const newArgs = ['-n', CONTAINER_NAME, '-x', command];
|
|
612
|
+
process.argv = [process.argv[0], process.argv[1], ...newArgs];
|
|
613
|
+
await main();
|
|
614
|
+
} else if (firstChar === 'i') {
|
|
615
|
+
console.log(`${GREEN}✅ 离开当前连接,进入容器交互式SHELL。${NC}`);
|
|
616
|
+
const newArgs = ['-n', CONTAINER_NAME, '-x', '/bin/bash'];
|
|
617
|
+
process.argv = [process.argv[0], process.argv[1], ...newArgs];
|
|
618
|
+
await main();
|
|
619
|
+
} else {
|
|
620
|
+
console.log(`${GREEN}✅ 已退出连接。容器 ${CONTAINER_NAME} 仍在后台运行。${NC}`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// ==============================================================================
|
|
625
|
+
// Main Function
|
|
626
|
+
// ==============================================================================
|
|
627
|
+
|
|
628
|
+
async function main() {
|
|
629
|
+
try {
|
|
630
|
+
// 1. Validate and initialize
|
|
631
|
+
validateAndInitialize();
|
|
632
|
+
|
|
633
|
+
// 2. Parse command-line arguments
|
|
634
|
+
parseArguments(process.argv);
|
|
635
|
+
|
|
636
|
+
// 3. Handle remove container operation
|
|
637
|
+
handleRemoveContainer();
|
|
638
|
+
|
|
639
|
+
// 4. Validate host path safety
|
|
640
|
+
validateHostPath();
|
|
641
|
+
|
|
642
|
+
// 5. Setup container (create or connect)
|
|
643
|
+
const defaultCommand = await setupContainer();
|
|
644
|
+
|
|
645
|
+
// 6. Execute command in container
|
|
646
|
+
executeInContainer(defaultCommand);
|
|
647
|
+
|
|
648
|
+
// 7. Handle post-exit interactions
|
|
649
|
+
await handlePostExit(defaultCommand);
|
|
565
650
|
|
|
566
651
|
} catch (e) {
|
|
567
652
|
console.error(`${RED}Error: ${e.message}${NC}`);
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
[ [English](README_EN.md) ] | [中文](../README.md)
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# MANYOYO (Man-Yo-Yo)
|
|
6
|
+
|
|
7
|
+
**MANYOYO** is an AI agent security sandbox that is safe, efficient, and token-saving. Designed specifically for Agent YOLO mode to protect the host machine.
|
|
8
|
+
|
|
9
|
+
Pre-installed with common agents and tools to further save tokens. Freely switch between agents and `/bin/bash` in a loop for enhanced efficiency.
|
|
10
|
+
|
|
11
|
+
**MANYOYO** provides an isolated Docker/Podman container environment for running AI agent CLIs safely.
|
|
12
|
+
|
|
13
|
+
## Key Features
|
|
14
|
+
|
|
15
|
+
- **Multi-Agent Support**: Supports claude code, gemini, codex, opencode
|
|
16
|
+
- **Security Isolation**: Protects host machine, supports safe nested containers (Docker-in-Docker)
|
|
17
|
+
- **Quick Launch**: Quickly enable common Agent YOLO / SOLO mode (e.g., claude --dangerously-skip-permissions)
|
|
18
|
+
- **Convenient Operations**: Quick access to `/bin/bash`
|
|
19
|
+
- **Session Recovery**: Install Skills Marketplace to quickly resume sessions
|
|
20
|
+
- **Flexible Customization**: Support custom `BASEURL`, `AUTH_TOKEN`, and other variables
|
|
21
|
+
- **Configuration Management**: Quick import of configuration files
|
|
22
|
+
- **Advanced Mode**: Supports dangerous nested containers (mount-docker-socket), custom sandbox images
|
|
23
|
+
|
|
24
|
+
# Usage
|
|
25
|
+
|
|
26
|
+
## 1. Install podman
|
|
27
|
+
|
|
28
|
+
- Install [podman](https://podman.io/docs/installation)
|
|
29
|
+
|
|
30
|
+
## 2. Build Image
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
podman pull ubuntu:24.04
|
|
34
|
+
iv=1.4.0-all && podman build -t localhost/xcanwin/manyoyo:$iv -f docker/manyoyo.Dockerfile . --build-arg EXT=all --no-cache
|
|
35
|
+
podman image prune -f
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 3. Install manyoyo (Choose One)
|
|
39
|
+
|
|
40
|
+
### Global Installation (Recommended)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g @xcanwin/manyoyo
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Local Development
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npm install -g .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 4. Usage
|
|
53
|
+
|
|
54
|
+
### Basic Commands
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Show help
|
|
58
|
+
manyoyo -h
|
|
59
|
+
|
|
60
|
+
# Show version
|
|
61
|
+
manyoyo -V
|
|
62
|
+
|
|
63
|
+
# List all containers
|
|
64
|
+
manyoyo -l
|
|
65
|
+
|
|
66
|
+
# Create new container with environment file
|
|
67
|
+
manyoyo -n test --ef .env -y c
|
|
68
|
+
|
|
69
|
+
# Resume existing session
|
|
70
|
+
manyoyo -n test -- -c
|
|
71
|
+
|
|
72
|
+
# Execute command in interactive shell
|
|
73
|
+
manyoyo -n test -x /bin/bash
|
|
74
|
+
|
|
75
|
+
# Execute custom command
|
|
76
|
+
manyoyo -n test -x echo "hello world"
|
|
77
|
+
|
|
78
|
+
# Remove container
|
|
79
|
+
manyoyo -n test --rm
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Environment Variables
|
|
83
|
+
|
|
84
|
+
#### String Format
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Direct
|
|
88
|
+
manyoyo -e "VAR=value" -x env
|
|
89
|
+
|
|
90
|
+
# Multiple
|
|
91
|
+
manyoyo -e "A=1" -e "B=2" -x env
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### File Format
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# From file
|
|
98
|
+
manyoyo --ef .env -x env
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Environment files (`.env`) support the following formats:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# With export statement
|
|
105
|
+
export ANTHROPIC_BASE_URL="https://api.anthropic.com"
|
|
106
|
+
# export CLAUDE_CODE_OAUTH_TOKEN="sk-xxxxxxxx"
|
|
107
|
+
export ANTHROPIC_AUTH_TOKEN="sk-xxxxxxxx"
|
|
108
|
+
export API_TIMEOUT_MS=3000000
|
|
109
|
+
export ANTHROPIC_MODEL="claude-sonnet-4-5"
|
|
110
|
+
export ANTHROPIC_DEFAULT_OPUS_MODEL="claude-opus-4-5"
|
|
111
|
+
export ANTHROPIC_DEFAULT_SONNET_MODEL="claude-sonnet-4-5"
|
|
112
|
+
export ANTHROPIC_DEFAULT_HAIKU_MODEL="claude-haiku-4-5"
|
|
113
|
+
export CLAUDE_CODE_SUBAGENT_MODEL="claude-sonnet-4-5"
|
|
114
|
+
|
|
115
|
+
# Simple key=value
|
|
116
|
+
API_KEY=your-api-key-here
|
|
117
|
+
|
|
118
|
+
# Quoted values (quotes will be stripped)
|
|
119
|
+
MESSAGE="Hello World"
|
|
120
|
+
PATH='/usr/local/bin'
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### AI CLI Shortcuts (skip permissions)
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
# Claude Code
|
|
127
|
+
manyoyo -y c # or: claude, cc
|
|
128
|
+
|
|
129
|
+
# Gemini
|
|
130
|
+
manyoyo -y gm # or: gemini, g
|
|
131
|
+
|
|
132
|
+
# Codex
|
|
133
|
+
manyoyo -y cx # or: codex
|
|
134
|
+
|
|
135
|
+
# OpenCode
|
|
136
|
+
manyoyo -y oc # or: opencode
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Interactive Session Management
|
|
140
|
+
|
|
141
|
+
After exiting a container session, you'll be prompted with options:
|
|
142
|
+
|
|
143
|
+
- `y` - Keep container running in background (default)
|
|
144
|
+
- `n` - Delete the container
|
|
145
|
+
- `1` - Re-enter with the original command
|
|
146
|
+
- `x` - Execute a new command
|
|
147
|
+
- `i` - Enter interactive shell
|
|
148
|
+
|
|
149
|
+
### Container Modes
|
|
150
|
+
|
|
151
|
+
#### Docker-in-Docker Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Docker-in-Docker (safe nested containers)
|
|
155
|
+
# Create a container with Docker-in-Docker support
|
|
156
|
+
manyoyo -n docker-dev -m dind -x /bin/bash
|
|
157
|
+
|
|
158
|
+
# Inside the container, start dockerd
|
|
159
|
+
nohup dockerd &
|
|
160
|
+
|
|
161
|
+
# Now you can use docker commands inside the container
|
|
162
|
+
docker run hello-world
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### Mount Docker socket Development
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Mount Docker socket (dangerous - container can access host)
|
|
169
|
+
manyoyo -n socket-dev -m mdsock -x docker ps
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Command-Line Options
|
|
173
|
+
|
|
174
|
+
| Option | Aliases | Description |
|
|
175
|
+
|--------|---------|-------------|
|
|
176
|
+
| `-l` | `--ls`, `--list` | List all manyoyo containers |
|
|
177
|
+
| `--hp PATH` | `--host-path` | Set host working directory (default: current path) |
|
|
178
|
+
| `-n NAME` | `--cn`, `--cont-name` | Set container name |
|
|
179
|
+
| `--cp PATH` | `--cont-path` | Set container working directory |
|
|
180
|
+
| `--in NAME` | `--image-name` | Specify image name |
|
|
181
|
+
| `--iv VERSION` | `--image-ver` | Specify image version |
|
|
182
|
+
| `-e STRING` | `--env` | Set environment variable |
|
|
183
|
+
| `--ef FILE` | `--env-file` | Load environment variables from file |
|
|
184
|
+
| `-v STRING` | `--volume` | Bind mount volume |
|
|
185
|
+
| `--rm` | `--rmc`, `--remove-cont` | Remove container |
|
|
186
|
+
| `--sp CMD` | `--shell-prefix` | Temporary environment variable (prefix for -s) |
|
|
187
|
+
| `-s CMD` | `--shell` | Specify command to execute |
|
|
188
|
+
| `--` | `--ss`, `--shell-suffix` | Command arguments (suffix for -s) |
|
|
189
|
+
| `-x CMD` | `--sf`, `--shell-full` | Full command (replaces --sp, -s, and --) |
|
|
190
|
+
| `-y CLI` | `--yolo` | Run AI agent without confirmation |
|
|
191
|
+
| `-m MODE` | `--cm`, `--cont-mode` | Set container mode (common, dind, mdsock) |
|
|
192
|
+
| `--install NAME` | | Install manyoyo command |
|
|
193
|
+
| `-V` | `--version` | Show version |
|
|
194
|
+
| `-h` | `--help` | Show help |
|
|
195
|
+
|
|
196
|
+
## Additional Information
|
|
197
|
+
|
|
198
|
+
### Default Configuration
|
|
199
|
+
|
|
200
|
+
- **Container Name**: `myy-{MMDD-HHMM}` (auto-generated based on current time)
|
|
201
|
+
- **Host Path**: Current working directory
|
|
202
|
+
- **Container Path**: Same as host path
|
|
203
|
+
- **Image**: `localhost/xcanwin/manyoyo:xxx`
|
|
204
|
+
|
|
205
|
+
### Requirements
|
|
206
|
+
|
|
207
|
+
- Node.js >= 24.0.0
|
|
208
|
+
- Docker or Podman
|
|
209
|
+
|
|
210
|
+
### Uninstall
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npm uninstall -g @xcanwin/manyoyo
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## License
|
|
217
|
+
|
|
218
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "AI Agent CLI Security Sandbox",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai", "agent", "sandbox", "docker", "cli", "container", "development"
|
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
"node": ">=14.0.0"
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
|
-
"manyoyo.js",
|
|
29
|
+
"bin/manyoyo.js",
|
|
30
30
|
"README.md",
|
|
31
|
+
"docs/README_EN.md",
|
|
31
32
|
"LICENSE",
|
|
32
33
|
"docker/manyoyo.Dockerfile"
|
|
33
34
|
]
|