@xcanwin/manyoyo 8.5.0 → 8.5.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 +66 -25
- package/bin/manyoyo.js +46 -9
- package/lib/app-update.js +93 -35
- package/lib/net-proxy.js +183 -0
- package/lib/plugin/playwright.js +4 -2
- package/package.json +5 -3
- package/README.en.md +0 -88
package/README.md
CHANGED
|
@@ -3,35 +3,36 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# <p align="center"><a href="https://github.com/xcanwin/manyoyo">MANYOYO(慢悠悠)</a></p>
|
|
6
|
-
<p align="center"><b>
|
|
7
|
-
<p align="center">开源 AI
|
|
6
|
+
<p align="center"><b>Open-source sandbox for AI coding agents: run Claude Code, Codex, Gemini CLI and OpenCode in YOLO mode safely inside Docker/Podman</b></p>
|
|
7
|
+
<p align="center">开源 AI 编程智能体安全沙箱:让 Claude Code / Codex / Gemini CLI / OpenCode 在 Docker/Podman 容器里放心跑 YOLO 模式,AI 随便折腾,伤不到你的电脑。</p>
|
|
8
8
|
<p align="center">
|
|
9
9
|
<a href="https://www.npmjs.com/package/@xcanwin/manyoyo"><img alt="npm" src="https://img.shields.io/npm/v/@xcanwin/manyoyo?style=flat-square" /></a>
|
|
10
10
|
<a href="https://github.com/xcanwin/manyoyo/actions/workflows/npm-publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/xcanwin/manyoyo/npm-publish.yml?style=flat-square" /></a>
|
|
11
|
-
<a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-
|
|
11
|
+
<a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-Apache--2.0-blue.svg" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
15
|
-
<a href="
|
|
16
|
-
<a href="
|
|
17
|
-
</p>
|
|
18
|
-
<p align="center">
|
|
19
|
-
文档:<a href="https://xcanwin.github.io/manyoyo/">https://xcanwin.github.io/manyoyo/</a>
|
|
15
|
+
<a href="#中文"><b>中文</b></a> |
|
|
16
|
+
<a href="#english">English</a>
|
|
20
17
|
</p>
|
|
21
18
|
|
|
22
19
|
---
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
<a name="中文"></a>
|
|
25
22
|
|
|
26
|
-
|
|
23
|
+
## 安装(二选一即可)
|
|
24
|
+
|
|
25
|
+
### 1. macOS / Linux(推荐)
|
|
27
26
|
|
|
28
27
|
```bash
|
|
29
|
-
curl -fsSL https://github.
|
|
28
|
+
curl -fsSL https://xcanwin.github.io/manyoyo/install.sh | sh
|
|
30
29
|
```
|
|
31
30
|
|
|
32
31
|
装完按屏幕提示完成配置。macOS 不需要预装任何东西。Linux:缺少容器环境时,安装器会说明原因,征得你同意后用 sudo 自动安装。
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
下载慢或打不开 GitHub?先在终端设置代理(`export https_proxy=http://127.0.0.1:7890`,macOS 的系统代理会自动识别),或见[安装文档](https://xcanwin.github.io/manyoyo/guide/quick-start)里的手动下载办法。
|
|
34
|
+
|
|
35
|
+
### 2. 已有 Node.js 和 Docker/Podman
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
npm install -g @xcanwin/manyoyo
|
|
@@ -63,26 +64,66 @@ manyoyo run -y c # 在沙箱里以 YOLO 模式启动 Claude Code
|
|
|
63
64
|
- **统一配置**:运行配置、环境变量、挂载与镜像参数集中在 `~/.manyoyo/manyoyo.json`
|
|
64
65
|
- **一条命令**安装、升级、卸载
|
|
65
66
|
|
|
66
|
-
| 对比项 | 裸跑 Agent CLI | MANYOYO |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| 宿主机暴露面 | 高 | 更低 |
|
|
69
|
-
| 运行边界 | 分散 | 集中到容器与配置 |
|
|
70
|
-
| 环境复现 | 弱 | 强(镜像 + 配置) |
|
|
71
|
-
| 高风险模式说明 | 通常依赖工具自身 | 明确提示 YOLO / SOLO / sock 风险 |
|
|
72
|
-
|
|
73
67
|
## 安全须知
|
|
74
68
|
|
|
75
69
|
MANYOYO 降低风险,但不是“绝对安全”:主要隔离手段是容器而不是虚拟机;`YOLO / SOLO` 仍可能执行危险命令;`sock` 模式会暴露宿主机 Docker socket;对外监听必须设强密码。详见[安全须知](https://xcanwin.github.io/manyoyo/guide/security)。
|
|
76
70
|
|
|
77
71
|
## 文档
|
|
78
72
|
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
<https://xcanwin.github.io/manyoyo/>:[安装](https://xcanwin.github.io/manyoyo/guide/quick-start)、[命令速查](https://xcanwin.github.io/manyoyo/reference/cli-options)、[配置](https://xcanwin.github.io/manyoyo/configuration/)、[故障排查](https://xcanwin.github.io/manyoyo/troubleshooting/)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
<a name="english"></a>
|
|
78
|
+
|
|
79
|
+
## Install (pick one)
|
|
80
|
+
|
|
81
|
+
### 1. macOS / Linux (recommended)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
curl -fsSL https://xcanwin.github.io/manyoyo/install.sh | sh
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
When it finishes, follow the on-screen prompts to finish setup. macOS needs nothing installed first. Linux: if the container runtime is missing, the installer explains why and, with your consent, installs it with sudo.
|
|
88
|
+
|
|
89
|
+
Slow or cannot reach GitHub? Set a proxy in the terminal first (`export https_proxy=http://127.0.0.1:7890`; the macOS system proxy is detected automatically), or see the manual download steps in the [install guide](https://xcanwin.github.io/manyoyo/en/guide/quick-start).
|
|
90
|
+
|
|
91
|
+
### 2. Already have Node.js and Docker/Podman
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install -g @xcanwin/manyoyo
|
|
95
|
+
manyoyo
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Daily use
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
manyoyo # open the web UI
|
|
102
|
+
manyoyo update # upgrade
|
|
103
|
+
manyoyo uninstall # uninstall (config and data are kept by default)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Command line (optional)
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
manyoyo init all # import the agent configs already on this machine
|
|
110
|
+
manyoyo run -y c # start Claude Code in YOLO mode inside the sandbox
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The default image `ghcr.io/xcanwin/manyoyo` is pulled automatically on first use; run `manyoyo build --iv 2.2.0-common` only if you need a custom image. More commands: [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options).
|
|
114
|
+
|
|
115
|
+
## What it does
|
|
116
|
+
|
|
117
|
+
- **Multiple agents**: `claude`, `codex`, `gemini`, `opencode` behind one entry point
|
|
118
|
+
- **Container isolation**: Docker / Podman; the agent only sees the work directory you give it
|
|
119
|
+
- **Web UI**: chat, browse files and open a terminal in the browser, phone friendly
|
|
120
|
+
- **One config**: run profiles, environment variables, mounts and image arguments in `~/.manyoyo/manyoyo.json`
|
|
121
|
+
- **One command** to install, upgrade and uninstall
|
|
81
122
|
|
|
82
|
-
##
|
|
123
|
+
## Security notes
|
|
83
124
|
|
|
84
|
-
|
|
125
|
+
MANYOYO reduces risk but is not "absolutely safe": the main isolation is a container, not a virtual machine; `YOLO / SOLO` can still run dangerous commands; `sock` mode exposes the host Docker socket; set a strong password before listening on a public address. See [Security notes](https://xcanwin.github.io/manyoyo/en/guide/security).
|
|
85
126
|
|
|
86
|
-
##
|
|
127
|
+
## Docs
|
|
87
128
|
|
|
88
|
-
|
|
129
|
+
<https://xcanwin.github.io/manyoyo/en/>: [Install](https://xcanwin.github.io/manyoyo/en/guide/quick-start), [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options), [Configuration](https://xcanwin.github.io/manyoyo/en/configuration/), [FAQ](https://xcanwin.github.io/manyoyo/en/troubleshooting/)
|
package/bin/manyoyo.js
CHANGED
|
@@ -26,6 +26,7 @@ const { listServeInstances, formatServeTable } = require('../lib/serve-instances
|
|
|
26
26
|
const { pruneDanglingImages: pruneDanglingImagesSafely } = require('../lib/image-prune');
|
|
27
27
|
const { runUninstall, readPid, defaultIsManyoyoServe, defaultKill } = require('../lib/uninstall');
|
|
28
28
|
const appUpdate = require('../lib/app-update');
|
|
29
|
+
const { getGithubFetch } = require('../lib/net-proxy');
|
|
29
30
|
const { createUpdateChecker } = require('../lib/update-check');
|
|
30
31
|
const { getLoginTokenDir, issueLoginToken } = require('../lib/login-token');
|
|
31
32
|
const { launchApp, getAppStatePath, openBrowser } = require('../lib/app-launcher');
|
|
@@ -836,6 +837,18 @@ async function prepareRuntimeForUpdate(configuredRuntime) {
|
|
|
836
837
|
return true;
|
|
837
838
|
}
|
|
838
839
|
|
|
840
|
+
// 网络失败后的“下一步”:先说代理,再给手动下载升级包的办法(地址写完整,能直接复制)
|
|
841
|
+
function updateNetworkNextSteps(proxy, release) {
|
|
842
|
+
const lines = [proxy && proxy.proxyEnv
|
|
843
|
+
? `当前代理: ${proxy.display},请确认代理可用`
|
|
844
|
+
: '有代理的话先在终端设置: export https_proxy=http://127.0.0.1:7890'];
|
|
845
|
+
const names = appUpdate.releaseAssetNames(release ? release.version : '<版本>', appUpdate.arch(), appUpdate.platformOs());
|
|
846
|
+
lines.push(`也可以手动下载这两个文件到同一目录,再执行: ${MANYOYO_NAME} update --file <下载的 ${names.app}>`);
|
|
847
|
+
const base = release ? `https://github.com/xcanwin/manyoyo/releases/download/v${release.version}` : 'https://github.com/xcanwin/manyoyo/releases/latest';
|
|
848
|
+
lines.push(release ? `${base}/${names.app}` : base, release ? `${base}/${names.sums}` : '');
|
|
849
|
+
return `\n ${lines.filter(Boolean).join('\n ')}`;
|
|
850
|
+
}
|
|
851
|
+
|
|
839
852
|
async function updateOfflineInstall(mode, options, globalConfig) {
|
|
840
853
|
const { appRoot } = mode;
|
|
841
854
|
if (options.rollback) {
|
|
@@ -847,14 +860,33 @@ async function updateOfflineInstall(mode, options, globalConfig) {
|
|
|
847
860
|
|
|
848
861
|
const installed = appUpdate.currentVersion(appRoot) || mode.version;
|
|
849
862
|
console.log(`${CYAN}🔄 当前版本: ${installed}${NC}`);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
863
|
+
let result;
|
|
864
|
+
if (options.file) {
|
|
865
|
+
const info = appUpdate.parseAppFileName(options.file);
|
|
866
|
+
if (info && appUpdate.compareVersions(info.version, installed) <= 0) {
|
|
867
|
+
console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
console.log(`${CYAN}⬇️ 安装本地升级包 ${path.basename(options.file)}${NC}`);
|
|
871
|
+
result = await appUpdate.installAppFile({ appRoot, file: path.resolve(options.file), targetOs: appUpdate.platformOs() });
|
|
872
|
+
} else {
|
|
873
|
+
const github = getGithubFetch();
|
|
874
|
+
if (github.notice) console.log(`${CYAN}🌐 ${github.notice}${NC}`);
|
|
875
|
+
let release = null;
|
|
876
|
+
try {
|
|
877
|
+
console.log(`${CYAN}🔄 正在查询最新版本...${NC}`);
|
|
878
|
+
release = await appUpdate.fetchLatestRelease({ fetchImpl: github.fetch });
|
|
879
|
+
if (appUpdate.compareVersions(release.version, installed) <= 0) {
|
|
880
|
+
console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
console.log(`${CYAN}⬇️ 发现新版本 ${release.version},只下载 manyoyo 本体(数十 MB)${NC}`);
|
|
884
|
+
result = await appUpdate.installAppUpdate({ appRoot, release, fetchImpl: github.fetch, targetOs: appUpdate.platformOs(), log: line => console.log(` ${line}`) });
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if (error instanceof appUpdate.UpdateError && error.code === 'NETWORK') error.message += updateNetworkNextSteps(github.proxy, release);
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
855
889
|
}
|
|
856
|
-
console.log(`${CYAN}⬇️ 发现新版本 ${release.version},只下载 manyoyo 本体(数十 MB)${NC}`);
|
|
857
|
-
const result = await appUpdate.installAppUpdate({ appRoot, release, targetOs: appUpdate.platformOs(), log: line => console.log(` ${line}`) });
|
|
858
890
|
console.log(`${GREEN}✅ 更新完成: ${installed} → ${result.version}(上一版本 ${result.previous || '无'} 已保留,可用 ${MANYOYO_NAME} update --rollback 回滚)${NC}`);
|
|
859
891
|
|
|
860
892
|
// Podman / VM 磁盘有变化只提示(需要新的完整包)
|
|
@@ -922,6 +954,10 @@ async function updateManyoyo(options = {}, globalConfig = {}) {
|
|
|
922
954
|
console.error(`${RED}❌ --rollback 只适用于离线包安装;npm 安装的版本请用 npm install -g @xcanwin/manyoyo@<版本> 回退。${NC}`);
|
|
923
955
|
process.exit(1);
|
|
924
956
|
}
|
|
957
|
+
if (options.file) {
|
|
958
|
+
console.error(`${RED}❌ --file 只适用于离线包安装;npm 安装请用 npm install -g @xcanwin/manyoyo 升级。${NC}`);
|
|
959
|
+
process.exit(1);
|
|
960
|
+
}
|
|
925
961
|
|
|
926
962
|
let isLocalFileInstall = false;
|
|
927
963
|
let currentVersion = 'unknown';
|
|
@@ -1187,7 +1223,8 @@ https://github.com/xcanwin/manyoyo
|
|
|
1187
1223
|
.helpGroup('日常:')
|
|
1188
1224
|
.description('升级到最新版本')
|
|
1189
1225
|
.option('--rollback', '回到上一版本')
|
|
1190
|
-
.
|
|
1226
|
+
.option('--file <path>', '安装手动下载的升级包(manyoyo-<版本>-<系统>-<架构>-app.tar.gz,同目录需有 SHA256SUMS)')
|
|
1227
|
+
.action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback), file: options.file || '' }));
|
|
1191
1228
|
|
|
1192
1229
|
program.command('uninstall')
|
|
1193
1230
|
.helpGroup('日常:')
|
|
@@ -2434,7 +2471,7 @@ async function runWebServerMode(runtime) {
|
|
|
2434
2471
|
currentVersion: require('../package.json').version,
|
|
2435
2472
|
installMode: appUpdate.detectInstallMode({ scriptPath: __filename }).mode,
|
|
2436
2473
|
enabled: UPDATE_CHECK_ENABLED,
|
|
2437
|
-
fetchLatest: () => appUpdate.fetchLatestRelease()
|
|
2474
|
+
fetchLatest: () => appUpdate.fetchLatestRelease({ fetchImpl: getGithubFetch().fetch })
|
|
2438
2475
|
});
|
|
2439
2476
|
|
|
2440
2477
|
// 启动时检查 Playwright 当前模式是否还活着,失效就回退到默认模式(只警告,不阻塞启动)
|
package/lib/app-update.js
CHANGED
|
@@ -18,12 +18,30 @@ const VERSION_PATTERN = /^\d+\.\d+\.\d+$/;
|
|
|
18
18
|
const FETCH_TIMEOUT_MS = 15000;
|
|
19
19
|
|
|
20
20
|
class UpdateError extends Error {
|
|
21
|
-
constructor(code, message) {
|
|
21
|
+
constructor(code, message, extra = {}) {
|
|
22
22
|
super(message);
|
|
23
23
|
this.code = code;
|
|
24
|
+
Object.assign(this, extra);
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
// 网络错误码 → 中文原因;没见过的显示原始 code
|
|
29
|
+
function describeNetworkReason(error) {
|
|
30
|
+
const code = error && (error.cause && error.cause.code || error.code) || '';
|
|
31
|
+
const name = error && (error.cause && error.cause.name || error.name) || '';
|
|
32
|
+
if (/TIMEOUT|ETIMEDOUT|ABORT/i.test(code) || /TimeoutError|AbortError/.test(name)) return '连接超时';
|
|
33
|
+
if (code === 'ECONNRESET') return '连接被重置';
|
|
34
|
+
if (code === 'ECONNREFUSED') return '连接被拒绝';
|
|
35
|
+
if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') return '域名解析失败';
|
|
36
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO_VERIFY|ERR_TLS/i.test(code)) return '证书校验失败';
|
|
37
|
+
return code || name || '网络错误';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function networkError(url, error, prefix = '连不上') {
|
|
41
|
+
const host = (() => { try { return new URL(url).host; } catch (e) { return String(url); } })();
|
|
42
|
+
return new UpdateError('NETWORK', `${prefix} ${host}(${describeNetworkReason(error)})\n 地址: ${url}`, { url });
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
function parseVersion(text) {
|
|
28
46
|
const match = String(text || '').trim().replace(/^v/, '').match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
29
47
|
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
@@ -80,7 +98,7 @@ async function request(fetchImpl, url, options = {}) {
|
|
|
80
98
|
headers: { Accept: 'application/vnd.github+json', 'User-Agent': 'manyoyo-update', ...(options.headers || {}) }
|
|
81
99
|
});
|
|
82
100
|
} catch (error) {
|
|
83
|
-
throw
|
|
101
|
+
throw networkError(url, error);
|
|
84
102
|
}
|
|
85
103
|
}
|
|
86
104
|
|
|
@@ -135,7 +153,7 @@ function releaseAssetNames(version, targetArch, targetOs = 'macos') {
|
|
|
135
153
|
|
|
136
154
|
async function fetchText(fetchImpl, url) {
|
|
137
155
|
const response = await request(fetchImpl, url);
|
|
138
|
-
if (!response.ok) throw new UpdateError('HTTP', `下载 ${url.split('/').pop()} 失败(HTTP ${response.status}
|
|
156
|
+
if (!response.ok) throw new UpdateError('HTTP', `下载 ${url.split('/').pop()} 失败(HTTP ${response.status})。\n 地址: ${url}`, { url });
|
|
139
157
|
return response.text();
|
|
140
158
|
}
|
|
141
159
|
|
|
@@ -183,6 +201,44 @@ function pruneApps(appRoot, keep) {
|
|
|
183
201
|
}
|
|
184
202
|
}
|
|
185
203
|
|
|
204
|
+
/**
|
|
205
|
+
* 解开已校验过的 -app.tar.gz 并切换 current(网络下载与 --file 共用)。包不完整/内容不符都在切换前中止。
|
|
206
|
+
*/
|
|
207
|
+
async function installFromArchive({ appRoot, archive, version, targetArch, targetOs, run = defaultRun }) {
|
|
208
|
+
const staging = path.join(appRoot, `.tmp-${version}-${process.pid}`);
|
|
209
|
+
try {
|
|
210
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
211
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
212
|
+
run(TAR_COMMAND, ['-xzf', archive, '-C', staging]);
|
|
213
|
+
|
|
214
|
+
for (const required of ['node/bin/node', 'manyoyo/bin/manyoyo.js', 'manifest.json']) {
|
|
215
|
+
if (!fs.existsSync(path.join(staging, required))) throw new UpdateError('BAD_PACKAGE', `升级包不完整(缺少 ${required}),已中止升级。`);
|
|
216
|
+
}
|
|
217
|
+
let manifest;
|
|
218
|
+
try {
|
|
219
|
+
manifest = JSON.parse(fs.readFileSync(path.join(staging, 'manifest.json'), 'utf-8'));
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new UpdateError('BAD_PACKAGE', '升级包的 manifest.json 无法解析,已中止升级。');
|
|
222
|
+
}
|
|
223
|
+
if (manifest.version !== version || manifest.kind !== 'app' || manifest.arch !== targetArch || (manifest.os && manifest.os !== targetOs)) {
|
|
224
|
+
throw new UpdateError('BAD_PACKAGE', `升级包内容与预期不符(版本 ${manifest.version}、类型 ${manifest.kind}、架构 ${manifest.arch}、系统 ${manifest.os || targetOs}),已中止升级。`);
|
|
225
|
+
}
|
|
226
|
+
fs.writeFileSync(path.join(staging, '.installed'), await sha256File(path.join(staging, 'manifest.json')));
|
|
227
|
+
|
|
228
|
+
const previous = currentVersion(appRoot);
|
|
229
|
+
const target = path.join(appRoot, version);
|
|
230
|
+
if (version !== previous) fs.rmSync(target, { recursive: true, force: true });
|
|
231
|
+
if (version !== previous) fs.renameSync(staging, target);
|
|
232
|
+
switchCurrent(appRoot, version);
|
|
233
|
+
writeUpdateRecord(appRoot, { previous: previous && previous !== version ? previous : readUpdateRecord(appRoot).previous || '', current: version, updatedAt: new Date().toISOString() });
|
|
234
|
+
const record = readUpdateRecord(appRoot);
|
|
235
|
+
pruneApps(appRoot, [version, record.previous].filter(Boolean));
|
|
236
|
+
return { version, previous: record.previous || '', manifest };
|
|
237
|
+
} finally {
|
|
238
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
186
242
|
/**
|
|
187
243
|
* 下载并安装 -app.tar.gz,成功后切换 current。校验失败/包不完整都在切换前中止,不留半成品。
|
|
188
244
|
*/
|
|
@@ -194,57 +250,56 @@ async function installAppUpdate({ appRoot, release, fetchImpl = fetch, targetArc
|
|
|
194
250
|
throw new UpdateError('NO_ASSET', `Release ${release.version} 里没有适合这台机器(${targetOs}-${targetArch})的升级包(需要 ${names.app} 与 ${names.sums})。`);
|
|
195
251
|
}
|
|
196
252
|
|
|
197
|
-
log(`校验清单: ${
|
|
253
|
+
log(`校验清单: ${sumsUrl}`);
|
|
198
254
|
const sums = parseSha256Sums(await fetchText(fetchImpl, sumsUrl));
|
|
199
255
|
const expected = sums[names.app];
|
|
200
256
|
if (!expected) throw new UpdateError('CHECKSUM', `校验清单里没有 ${names.app},已中止升级。`);
|
|
201
257
|
|
|
202
258
|
const workDir = fs.mkdtempSync(path.join(tmpRoot, 'manyoyo-update-'));
|
|
203
|
-
const staging = path.join(appRoot, `.tmp-${release.version}-${process.pid}`);
|
|
204
259
|
try {
|
|
205
|
-
log(`下载升级包: ${names.app}`);
|
|
206
260
|
const archive = path.join(workDir, names.app);
|
|
207
261
|
try {
|
|
208
262
|
await downloadVerified({ url: appUrl, sha256: expected, dest: archive, fetchImpl: (url, options) => request(fetchImpl, url, { ...options, noTimeout: true }), log });
|
|
209
263
|
} catch (error) {
|
|
210
264
|
if (/SHA256 校验失败/.test(error.message)) throw new UpdateError('CHECKSUM', `升级包 SHA256 校验失败,已中止升级,当前版本不受影响。`);
|
|
211
265
|
if (error instanceof UpdateError) throw error;
|
|
212
|
-
throw
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
216
|
-
fs.mkdirSync(staging, { recursive: true });
|
|
217
|
-
run(TAR_COMMAND, ['-xzf', archive, '-C', staging]);
|
|
218
|
-
|
|
219
|
-
for (const required of ['node/bin/node', 'manyoyo/bin/manyoyo.js', 'manifest.json']) {
|
|
220
|
-
if (!fs.existsSync(path.join(staging, required))) throw new UpdateError('BAD_PACKAGE', `升级包不完整(缺少 ${required}),已中止升级。`);
|
|
221
|
-
}
|
|
222
|
-
let manifest;
|
|
223
|
-
try {
|
|
224
|
-
manifest = JSON.parse(fs.readFileSync(path.join(staging, 'manifest.json'), 'utf-8'));
|
|
225
|
-
} catch (error) {
|
|
226
|
-
throw new UpdateError('BAD_PACKAGE', '升级包的 manifest.json 无法解析,已中止升级。');
|
|
266
|
+
throw networkError(appUrl, error, '下载中断,连不上');
|
|
227
267
|
}
|
|
228
|
-
if (manifest.version !== release.version || manifest.kind !== 'app' || manifest.arch !== targetArch || (manifest.os && manifest.os !== targetOs)) {
|
|
229
|
-
throw new UpdateError('BAD_PACKAGE', `升级包内容与预期不符(版本 ${manifest.version}、类型 ${manifest.kind}、架构 ${manifest.arch}、系统 ${manifest.os || targetOs}),已中止升级。`);
|
|
230
|
-
}
|
|
231
|
-
fs.writeFileSync(path.join(staging, '.installed'), await sha256File(path.join(staging, 'manifest.json')));
|
|
232
268
|
|
|
233
|
-
|
|
234
|
-
const target = path.join(appRoot, release.version);
|
|
235
|
-
if (release.version !== previous) fs.rmSync(target, { recursive: true, force: true });
|
|
236
|
-
if (release.version !== previous) fs.renameSync(staging, target);
|
|
237
|
-
switchCurrent(appRoot, release.version);
|
|
238
|
-
writeUpdateRecord(appRoot, { previous: previous && previous !== release.version ? previous : readUpdateRecord(appRoot).previous || '', current: release.version, updatedAt: new Date().toISOString() });
|
|
239
|
-
const record = readUpdateRecord(appRoot);
|
|
240
|
-
pruneApps(appRoot, [release.version, record.previous].filter(Boolean));
|
|
241
|
-
return { version: release.version, previous: record.previous || '', manifest };
|
|
269
|
+
return await installFromArchive({ appRoot, archive, version: release.version, targetArch, targetOs, run });
|
|
242
270
|
} finally {
|
|
243
271
|
fs.rmSync(workDir, { recursive: true, force: true });
|
|
244
|
-
fs.rmSync(staging, { recursive: true, force: true });
|
|
245
272
|
}
|
|
246
273
|
}
|
|
247
274
|
|
|
275
|
+
const APP_FILE_PATTERN = /^manyoyo-(\d+\.\d+\.\d+)-(macos|linux)-(arm64|x64)-app\.tar\.gz$/;
|
|
276
|
+
|
|
277
|
+
/** 从升级包文件名解析版本 / 系统 / 架构,不符合命名就返回 null。 */
|
|
278
|
+
function parseAppFileName(file) {
|
|
279
|
+
const match = path.basename(String(file)).match(APP_FILE_PATTERN);
|
|
280
|
+
return match ? { version: match[1], os: match[2], arch: match[3] } : null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* 安装手动下载的 -app.tar.gz:同目录的 SHA256SUMS 必须列出它且哈希一致(没有跳过校验的办法),之后与网络升级走同一段安装逻辑。
|
|
285
|
+
*/
|
|
286
|
+
async function installAppFile({ appRoot, file, targetArch = arch(), targetOs = 'macos', run = defaultRun }) {
|
|
287
|
+
const info = parseAppFileName(file);
|
|
288
|
+
if (!info) throw new UpdateError('BAD_FILE', `文件名不符合升级包命名(manyoyo-<版本>-<系统>-<架构>-app.tar.gz): ${path.basename(String(file))}`);
|
|
289
|
+
if (info.os !== targetOs || info.arch !== targetArch) {
|
|
290
|
+
throw new UpdateError('BAD_FILE', `这个升级包是 ${info.os}-${info.arch} 的,这台机器需要 ${targetOs}-${targetArch}。`);
|
|
291
|
+
}
|
|
292
|
+
if (!fs.existsSync(file)) throw new UpdateError('BAD_FILE', `找不到文件: ${file}`);
|
|
293
|
+
const sumsFile = path.join(path.dirname(path.resolve(file)), 'SHA256SUMS');
|
|
294
|
+
if (!fs.existsSync(sumsFile)) {
|
|
295
|
+
throw new UpdateError('CHECKSUM', `同目录下没有 SHA256SUMS,无法校验。\n 请从 https://github.com/${REPO}/releases/download/v${info.version}/SHA256SUMS 下载,放到升级包旁边再试。`);
|
|
296
|
+
}
|
|
297
|
+
const expected = parseSha256Sums(fs.readFileSync(sumsFile, 'utf-8'))[path.basename(file)];
|
|
298
|
+
if (!expected) throw new UpdateError('CHECKSUM', `SHA256SUMS 里没有 ${path.basename(file)},已中止升级。`);
|
|
299
|
+
if (await sha256File(file) !== expected) throw new UpdateError('CHECKSUM', '升级包 SHA256 校验失败,已中止升级,当前版本不受影响。');
|
|
300
|
+
return installFromArchive({ appRoot, archive: file, version: info.version, targetArch, targetOs, run });
|
|
301
|
+
}
|
|
302
|
+
|
|
248
303
|
function rollbackApp({ appRoot }) {
|
|
249
304
|
const current = currentVersion(appRoot);
|
|
250
305
|
const record = readUpdateRecord(appRoot);
|
|
@@ -334,6 +389,9 @@ module.exports = {
|
|
|
334
389
|
parseSha256Sums,
|
|
335
390
|
releaseAssetNames,
|
|
336
391
|
installAppUpdate,
|
|
392
|
+
installAppFile,
|
|
393
|
+
parseAppFileName,
|
|
394
|
+
networkError,
|
|
337
395
|
rollbackApp,
|
|
338
396
|
currentVersion,
|
|
339
397
|
describeRuntimeChange,
|
package/lib/net-proxy.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 访问 GitHub 的请求读取代理:环境变量(https_proxy 等)或 macOS 系统代理。
|
|
4
|
+
// Node 内置 fetch 不读代理变量,所以这里按请求构造带代理的 fetch,绝不修改全局(serve 进程里还有访问本机/容器的请求)。
|
|
5
|
+
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const https = require('https');
|
|
8
|
+
const { Readable } = require('stream');
|
|
9
|
+
const { spawnSync } = require('child_process');
|
|
10
|
+
|
|
11
|
+
const MAX_REDIRECTS = 5;
|
|
12
|
+
const IDLE_TIMEOUT_MS = 60000;
|
|
13
|
+
|
|
14
|
+
function pick(env, names) {
|
|
15
|
+
for (const name of names) {
|
|
16
|
+
const value = String(env[name] || env[name.toUpperCase()] || '').trim();
|
|
17
|
+
if (value) return value;
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 去掉账号密码,只留 scheme://host:port,用于打印
|
|
23
|
+
function displayProxy(url) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = new URL(url);
|
|
26
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeProxyUrl(value) {
|
|
33
|
+
return /^[a-z][a-z0-9+.-]*:\/\//i.test(value) ? value : `http://${value}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 解析 macOS `scutil --proxy` 的输出。返回 { url, noProxy } 或 { unsupported } 或 null。
|
|
38
|
+
* 只认 HTTPS 代理(没有就退到 HTTP 代理);PAC / 自动代理不解析。
|
|
39
|
+
*/
|
|
40
|
+
function parseScutilProxy(text) {
|
|
41
|
+
const values = {};
|
|
42
|
+
const exceptions = [];
|
|
43
|
+
let inExceptions = false;
|
|
44
|
+
for (const rawLine of String(text || '').split('\n')) {
|
|
45
|
+
const line = rawLine.trim();
|
|
46
|
+
if (/^ExceptionsList\s*:/.test(line)) { inExceptions = true; continue; }
|
|
47
|
+
if (inExceptions) {
|
|
48
|
+
const item = line.match(/^\d+\s*:\s*(.+)$/);
|
|
49
|
+
if (item) { exceptions.push(item[1].trim()); continue; }
|
|
50
|
+
inExceptions = false;
|
|
51
|
+
}
|
|
52
|
+
const pair = line.match(/^(\w+)\s*:\s*(.+)$/);
|
|
53
|
+
if (pair) values[pair[1]] = pair[2].trim();
|
|
54
|
+
}
|
|
55
|
+
const noProxy = exceptions.map(item => item.replace(/^\*\./, '.')).join(',');
|
|
56
|
+
for (const prefix of ['HTTPS', 'HTTP']) {
|
|
57
|
+
if (values[`${prefix}Enable`] === '1' && values[`${prefix}Proxy`] && /^\d+$/.test(values[`${prefix}Port`] || '')) {
|
|
58
|
+
return { url: `http://${values[`${prefix}Proxy`]}:${values[`${prefix}Port`]}`, noProxy };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (values.ProxyAutoConfigEnable === '1' || values.SOCKSEnable === '1') return { unsupported: true };
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function readScutilProxy() {
|
|
66
|
+
const result = spawnSync('/usr/sbin/scutil', ['--proxy'], { encoding: 'utf-8', timeout: 3000 });
|
|
67
|
+
return result.status === 0 ? result.stdout : '';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 找出当前该用的代理。优先级:环境变量 > macOS 系统代理 > 无。
|
|
72
|
+
* 返回 { proxyEnv, source: 'env'|'system'|null, display, notice };notice 是需要告诉用户的一句话(如暂不支持 socks)。
|
|
73
|
+
*/
|
|
74
|
+
function resolveProxy({ env = process.env, platform = process.platform, readSystemProxy = readScutilProxy } = {}) {
|
|
75
|
+
const noProxy = pick(env, ['no_proxy']);
|
|
76
|
+
const candidates = [pick(env, ['https_proxy']), pick(env, ['http_proxy']), pick(env, ['all_proxy'])].filter(Boolean);
|
|
77
|
+
let notice = '';
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
const url = normalizeProxyUrl(candidate);
|
|
80
|
+
if (/^socks/i.test(url)) {
|
|
81
|
+
notice = '暂不支持 socks 代理,请改用 http 代理端口。';
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!/^https?:\/\//i.test(url) || !displayProxy(url)) continue;
|
|
85
|
+
return { proxyEnv: { HTTPS_PROXY: url, HTTP_PROXY: url, ...(noProxy ? { NO_PROXY: noProxy } : {}) }, source: 'env', display: displayProxy(url), notice };
|
|
86
|
+
}
|
|
87
|
+
if (platform === 'darwin') {
|
|
88
|
+
let parsed = null;
|
|
89
|
+
try {
|
|
90
|
+
parsed = parseScutilProxy(readSystemProxy());
|
|
91
|
+
} catch (error) {
|
|
92
|
+
parsed = null;
|
|
93
|
+
}
|
|
94
|
+
if (parsed && parsed.url) {
|
|
95
|
+
const merged = [noProxy, parsed.noProxy].filter(Boolean).join(',');
|
|
96
|
+
return { proxyEnv: { HTTPS_PROXY: parsed.url, HTTP_PROXY: parsed.url, ...(merged ? { NO_PROXY: merged } : {}) }, source: 'system', display: displayProxy(parsed.url), notice };
|
|
97
|
+
}
|
|
98
|
+
if (parsed && parsed.unsupported) notice = '系统代理用了自动配置或 socks,暂不支持,请在终端设置 https_proxy 指向 http 代理端口。';
|
|
99
|
+
}
|
|
100
|
+
return { proxyEnv: null, source: null, display: '', notice };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Agent 的 proxyEnv 选项:Node 22.21+ / 24.5+ 才有
|
|
104
|
+
function supportsProxyEnv(version = process.versions.node) {
|
|
105
|
+
const [major, minor] = String(version).split('.').map(Number);
|
|
106
|
+
if (major >= 25) return true;
|
|
107
|
+
if (major === 24) return minor >= 5;
|
|
108
|
+
if (major === 22) return minor >= 21;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function requestOnce(url, options, agents) {
|
|
113
|
+
const target = new URL(url);
|
|
114
|
+
const client = target.protocol === 'http:' ? http : https;
|
|
115
|
+
const agent = target.protocol === 'http:' ? agents.http : agents.https;
|
|
116
|
+
return new Promise((resolve, reject) => {
|
|
117
|
+
const req = client.request(target, { method: options.method || 'GET', headers: options.headers || {}, agent, signal: options.signal }, resolve);
|
|
118
|
+
// 连接或下载中途卡住(代理停止转发等)时不能永远挂着:空闲超过 60 秒按超时处理
|
|
119
|
+
req.setTimeout(IDLE_TIMEOUT_MS, () => req.destroy(Object.assign(new Error('空闲超时'), { code: 'ETIMEDOUT' })));
|
|
120
|
+
req.on('error', reject);
|
|
121
|
+
req.end();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 带代理的最小 fetch 兼容实现(status / ok / headers.get / json / text / body)。
|
|
127
|
+
* 手动跟随最多 5 次重定向,每一跳都由 Agent 按 NO_PROXY 重新判断是否走代理;HTTPS 目标走 CONNECT 隧道,证书照常校验。
|
|
128
|
+
*/
|
|
129
|
+
function createProxyFetch(proxyEnv, agentOptions = {}) {
|
|
130
|
+
const agents = {
|
|
131
|
+
http: new http.Agent({ proxyEnv, keepAlive: false }),
|
|
132
|
+
https: new https.Agent({ proxyEnv, keepAlive: false, ...agentOptions })
|
|
133
|
+
};
|
|
134
|
+
return async function proxyFetch(url, options = {}) {
|
|
135
|
+
let current = String(url);
|
|
136
|
+
for (let hop = 0; hop <= MAX_REDIRECTS; hop += 1) {
|
|
137
|
+
const res = await requestOnce(current, options, agents);
|
|
138
|
+
const location = res.headers.location;
|
|
139
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && location && options.redirect !== 'manual') {
|
|
140
|
+
res.resume();
|
|
141
|
+
const next = new URL(location, current);
|
|
142
|
+
// 跨站跳转不带凭据类请求头
|
|
143
|
+
if (next.host !== new URL(current).host && options.headers) {
|
|
144
|
+
options = { ...options, headers: Object.fromEntries(Object.entries(options.headers).filter(([key]) => !/^(authorization|cookie)$/i.test(key))) };
|
|
145
|
+
}
|
|
146
|
+
current = next.toString();
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const headers = { get: name => { const value = res.headers[String(name).toLowerCase()]; return Array.isArray(value) ? value.join(', ') : value === undefined ? null : value; } };
|
|
150
|
+
const readText = async () => {
|
|
151
|
+
const chunks = [];
|
|
152
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
153
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
status: res.statusCode,
|
|
157
|
+
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
158
|
+
headers,
|
|
159
|
+
url: current,
|
|
160
|
+
body: Readable.toWeb(res),
|
|
161
|
+
text: readText,
|
|
162
|
+
json: async () => JSON.parse(await readText())
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
throw Object.assign(new Error('重定向次数过多'), { cause: { code: 'TOO_MANY_REDIRECTS' } });
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 访问 GitHub 用的 fetch:有可用代理时返回带代理的版本,否则返回内置 fetch。
|
|
171
|
+
* 返回 { fetch, proxy, notice };notice 是要打印给用户的提示(使用了哪个代理 / 为什么没用)。
|
|
172
|
+
*/
|
|
173
|
+
function getGithubFetch({ env = process.env, platform = process.platform, readSystemProxy, nodeVersion = process.versions.node, baseFetch = globalThis.fetch } = {}) {
|
|
174
|
+
const proxy = resolveProxy({ env, platform, readSystemProxy });
|
|
175
|
+
if (!proxy.proxyEnv) return { fetch: baseFetch, proxy, notice: proxy.notice };
|
|
176
|
+
if (!supportsProxyEnv(nodeVersion)) {
|
|
177
|
+
return { fetch: baseFetch, proxy, notice: '当前 Node 版本不支持代理,请升级到 Node 22.21+ 或 24+。' };
|
|
178
|
+
}
|
|
179
|
+
const label = proxy.source === 'system' ? '使用系统代理' : '使用代理';
|
|
180
|
+
return { fetch: createProxyFetch(proxy.proxyEnv), proxy, notice: `${label}: ${proxy.display}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = { resolveProxy, parseScutilProxy, displayProxy, supportsProxyEnv, createProxyFetch, getGithubFetch };
|
package/lib/plugin/playwright.js
CHANGED
|
@@ -612,7 +612,7 @@ class PlaywrightPlugin {
|
|
|
612
612
|
async startChrome() {
|
|
613
613
|
const { port } = this.config;
|
|
614
614
|
const candidates = this.devtoolsCandidates();
|
|
615
|
-
const enableHint = '请在 Chrome
|
|
615
|
+
const enableHint = '请在 Chrome 地址栏进入:\nchrome://inspect/#remote-debugging\n勾选“Allow remote debugging”(Chrome 没开就先打开它),然后重试。\n'
|
|
616
616
|
+ '(自定义用户目录时,用 plugins.playwright.devtoolsActivePortPath 指定 DevToolsActivePort 文件。)';
|
|
617
617
|
const target = findDevToolsActivePort(candidates);
|
|
618
618
|
if (!target) {
|
|
@@ -821,7 +821,9 @@ class PlaywrightPlugin {
|
|
|
821
821
|
if (!result.ok) {
|
|
822
822
|
if (mode === 'chrome') {
|
|
823
823
|
this.err('[up] chrome 失败: 连不上 Chrome 的调试接口。');
|
|
824
|
-
this.err('请确认已在
|
|
824
|
+
this.err('请确认已在 Chrome 地址栏进入:');
|
|
825
|
+
this.err('chrome://inspect/#remote-debugging');
|
|
826
|
+
this.err('开启远程调试,并在 Chrome 弹出的“要允许远程调试吗?”里点“允许”(60 秒内),然后重试。');
|
|
825
827
|
} else {
|
|
826
828
|
this.err(`[up] ${mode} 失败: 浏览器服务已启动,但打不开页面(${result.error})。`);
|
|
827
829
|
this.err(tailText(this.logPath(), 10));
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xcanwin/manyoyo",
|
|
3
|
-
"version": "8.5.
|
|
3
|
+
"version": "8.5.1",
|
|
4
4
|
"imageVersion": "2.2.0-common",
|
|
5
5
|
"playwrightCliVersion": "0.1.19",
|
|
6
6
|
"patchrightCoreVersion": "1.63.0",
|
|
7
7
|
"patchrightCoreIntegrity": "sha512-nuRJVTMVKoOFzY8vmwFqJZR3F0trUCKiPzDSgCOPq/yHHTefm9czILV23dXJsi5mvS7sCy7U1CqG7edA/21DtQ==",
|
|
8
|
-
"description": "MANYOYO
|
|
8
|
+
"description": "MANYOYO(慢悠悠)– open-source AI coding agent sandbox: run Claude Code, Codex, Gemini CLI and OpenCode in YOLO mode safely inside Docker/Podman",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"manyoyo",
|
|
11
11
|
"ai-agent",
|
|
12
|
+
"ai-sandbox",
|
|
13
|
+
"coding-agent",
|
|
12
14
|
"agent-cli",
|
|
13
15
|
"security-sandbox",
|
|
14
16
|
"claude-code",
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
"type": "git",
|
|
24
26
|
"url": "git+https://github.com/xcanwin/manyoyo.git"
|
|
25
27
|
},
|
|
26
|
-
"license": "
|
|
28
|
+
"license": "Apache-2.0",
|
|
27
29
|
"author": "xcanwin",
|
|
28
30
|
"type": "commonjs",
|
|
29
31
|
"main": "bin/manyoyo.js",
|
package/README.en.md
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="./assets/manyoyo-logo-09-cyberpunk-terminal.svg" alt="MANYOYO logo" width="560" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
|
-
# <p align="center"><a href="https://github.com/xcanwin/manyoyo">MANYOYO</a></p>
|
|
6
|
-
<p align="center"><b>MANYOYO – open-source sandbox for running AI coding agents (Claude Code / Codex / Gemini) safely in Docker/Podman</b></p>
|
|
7
|
-
<p align="center">Run Claude Code / Codex / Gemini / OpenCode in YOLO mode inside a container: let the AI go wild without hurting your computer.</p>
|
|
8
|
-
<p align="center">
|
|
9
|
-
<a href="https://www.npmjs.com/package/@xcanwin/manyoyo"><img alt="npm" src="https://img.shields.io/npm/v/@xcanwin/manyoyo?style=flat-square" /></a>
|
|
10
|
-
<a href="https://github.com/xcanwin/manyoyo/actions/workflows/npm-publish.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/xcanwin/manyoyo/npm-publish.yml?style=flat-square" /></a>
|
|
11
|
-
<a href="https://github.com/xcanwin/manyoyo/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
|
|
12
|
-
</p>
|
|
13
|
-
|
|
14
|
-
<p align="center">
|
|
15
|
-
<a href="README.md">中文</a> |
|
|
16
|
-
<a href="README.en.md"><b>English</b></a>
|
|
17
|
-
</p>
|
|
18
|
-
<p align="center">
|
|
19
|
-
Docs: <a href="https://xcanwin.github.io/manyoyo/en/">https://xcanwin.github.io/manyoyo/en/</a>
|
|
20
|
-
</p>
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
## Install
|
|
25
|
-
|
|
26
|
-
### macOS / Linux (recommended)
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
curl -fsSL https://github.com/xcanwin/manyoyo/raw/main/scripts/install.sh | sh
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
When it finishes, follow the on-screen prompts to finish setup. macOS needs nothing installed first. Linux: if the container runtime is missing, the installer explains why and, with your consent, installs it with sudo.
|
|
33
|
-
|
|
34
|
-
### Already have Node.js and Docker/Podman
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
npm install -g @xcanwin/manyoyo
|
|
38
|
-
manyoyo
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Daily use
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
manyoyo # open the web UI
|
|
45
|
-
manyoyo update # upgrade
|
|
46
|
-
manyoyo uninstall # uninstall (config and data are kept by default)
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Command line (optional)
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
manyoyo init all # import the agent configs already on this machine
|
|
53
|
-
manyoyo run -y c # start Claude Code in YOLO mode inside the sandbox
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
The default image `ghcr.io/xcanwin/manyoyo` is pulled automatically on first use; run `manyoyo build --iv 2.1.1-common` only if you need a custom image. More commands: [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options).
|
|
57
|
-
|
|
58
|
-
## What it does
|
|
59
|
-
|
|
60
|
-
- **Multiple agents**: `claude`, `codex`, `gemini`, `opencode` behind one entry point
|
|
61
|
-
- **Container isolation**: Docker / Podman; the agent only sees the work directory you give it
|
|
62
|
-
- **Web UI**: chat, browse files and open a terminal in the browser, phone friendly
|
|
63
|
-
- **One config**: run profiles, environment variables, mounts and image arguments in `~/.manyoyo/manyoyo.json`
|
|
64
|
-
- **One command** to install, upgrade and uninstall
|
|
65
|
-
|
|
66
|
-
| | Running the agent CLI directly | MANYOYO |
|
|
67
|
-
| --- | --- | --- |
|
|
68
|
-
| Host exposure | High | Lower |
|
|
69
|
-
| Run boundary | Scattered | Centralised in the container and config |
|
|
70
|
-
| Reproducible environment | Weak | Strong (image + config) |
|
|
71
|
-
| High-risk mode warnings | Left to the tool | Explicit YOLO / SOLO / sock warnings |
|
|
72
|
-
|
|
73
|
-
## Security notes
|
|
74
|
-
|
|
75
|
-
MANYOYO reduces risk but is not "absolutely safe": the main isolation is a container, not a virtual machine; `YOLO / SOLO` can still run dangerous commands; `sock` mode exposes the host Docker socket; set a strong password before listening on a public address. See [Security notes](https://xcanwin.github.io/manyoyo/en/guide/security).
|
|
76
|
-
|
|
77
|
-
## Docs
|
|
78
|
-
|
|
79
|
-
- 中文: <https://xcanwin.github.io/manyoyo/>
|
|
80
|
-
- English: <https://xcanwin.github.io/manyoyo/en/> ([Install](https://xcanwin.github.io/manyoyo/en/guide/quick-start), [CLI reference](https://xcanwin.github.io/manyoyo/en/reference/cli-options), [Configuration](https://xcanwin.github.io/manyoyo/en/configuration/), [FAQ](https://xcanwin.github.io/manyoyo/en/troubleshooting/))
|
|
81
|
-
|
|
82
|
-
## License
|
|
83
|
-
|
|
84
|
-
MIT
|
|
85
|
-
|
|
86
|
-
## Contributing
|
|
87
|
-
|
|
88
|
-
[Issues](https://github.com/xcanwin/manyoyo/issues) and pull requests are welcome.
|