@xcanwin/manyoyo 3.2.0 → 3.4.5

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
@@ -1,4 +1,4 @@
1
- [English](docs/README_EN.md) | [ [中文](README.md) ]
1
+ [English](docs/README_EN.md) | << [中文](README.md) >>
2
2
 
3
3
  ---
4
4
 
@@ -49,17 +49,21 @@ npm install -g .
49
49
  # 拉取基础镜像
50
50
  podman pull ubuntu:24.04
51
51
 
52
- # 使用 manyoyo 构建镜像(推荐)
53
- manyoyo --ib all # 构建 all 版本(包含所有工具)
54
- manyoyo --ib common # 构建 common 版本(基础版本)
55
- manyoyo --ib go,codex,java,gemini # 构建 go 版本(包含 go,codex,java,gemini 工具)
52
+ # 使用 manyoyo 构建镜像(推荐,自动使用缓存加速)
53
+ manyoyo --ib # 默认构建 full 版本(推荐)
54
+ manyoyo --ib --iba TOOL=common # 构建常见组件版本
55
+ manyoyo --ib --iba TOOL=go,codex,java,gemini # 构建自定义组件版本
56
+ manyoyo --ib --iba --iba GIT_SSL_NO_VERIFY=true # 构建 full 版本且git跳过ssl认证
57
+ manyoyo --ib full --in myimage --iv 2.0.0 # 自定义镜像名称和版本,得到 myimage:2.0.0-full
58
+ manyoyo --ip # 清理悬空镜像和 <none> 镜像
56
59
 
57
- # 自定义镜像名称和版本
58
- manyoyo --ib all --in myimage --iv 2.0.0
59
- # 构建:myimage:2.0.0-all
60
+ # 工作原理:
61
+ # - 首次构建:自动下载 Node.js、JDT LSP、gopls 等到 docker/cache/
62
+ # - 2天内再次构建:直接使用本地缓存,速度提升约 5 倍
63
+ # - 缓存过期后:自动重新下载最新版本
60
64
 
61
65
  # 或手动构建(不推荐)
62
- iv=1.4.0 && podman build -t localhost/xcanwin/manyoyo:$iv-all -f docker/manyoyo.Dockerfile . --build-arg EXT=all --no-cache
66
+ iv=1.4.0 && podman build -t localhost/xcanwin/manyoyo:$iv-full -f docker/manyoyo.Dockerfile . --build-arg TOOL=full --no-cache
63
67
  podman image prune -f
64
68
  ```
65
69
 
@@ -207,7 +211,9 @@ docker ps -a
207
211
  | `-x CMD` | `--sf`, `--shell-full` | 完整命令(替代 --sp, -s 和 --) |
208
212
  | `-y CLI` | `--yolo` | 无需确认运行 AI 智能体 |
209
213
  | `-m MODE` | `--cm`, `--cont-mode` | 设置容器模式(common, dind, mdsock) |
210
- | `--ib EXT` | `--image-build` | 构建镜像,EXT 为镜像变体(all, go, common) |
214
+ | `--ib` | `--image-build` | 构建镜像 |
215
+ | `--iba XXX=YYY` | `--image-build-arg` | 构建镜像时传参给dockerfile |
216
+ | `--ip` | `--image-prune` | 清理悬空镜像和 `<none>` 镜像 |
211
217
  | `--install NAME` | | 安装 manyoyo 命令 |
212
218
  | `-V` | `--version` | 显示版本 |
213
219
  | `-h` | `--help` | 显示帮助 |
package/bin/manyoyo.js CHANGED
@@ -25,14 +25,14 @@ let CONTAINER_NAME = `myy-${formatDate()}`;
25
25
  let HOST_PATH = process.cwd();
26
26
  let CONTAINER_PATH = HOST_PATH;
27
27
  let IMAGE_NAME = "localhost/xcanwin/manyoyo";
28
- let IMAGE_VERSION = `${IMAGE_VERSION_BASE}-all`;
28
+ let IMAGE_VERSION = `${IMAGE_VERSION_BASE}-full`;
29
29
  let EXEC_COMMAND = "";
30
30
  let EXEC_COMMAND_PREFIX = "";
31
31
  let EXEC_COMMAND_SUFFIX = "";
32
32
  let ENV_FILE = "";
33
33
  let SHOULD_REMOVE = false;
34
- let SHOULD_BUILD_IMAGE = false;
35
- let BUILD_IMAGE_EXT = "";
34
+ let IMAGE_BUILD_NEED = false;
35
+ let IMAGE_BUILD_ARGS = [];
36
36
  let CONTAINER_ENVS = [];
37
37
  let CONTAINER_VOLUMES = [];
38
38
  let MANYOYO_NAME = "manyoyo";
@@ -67,33 +67,34 @@ function showHelp() {
67
67
  console.log(` ${MANYOYO_NAME} [--hp HOST_PATH] [-n CONTAINER_NAME] [--cp CONTAINER_PATH] [--ef ENV_FILE] [--sp COMMAND] [-s COMMAND] [-- COMMAND]`);
68
68
  console.log("");
69
69
  console.log(`${BLUE}Options:${NC}`);
70
- console.log(" -l|--ls|--list 列举容器");
71
- console.log(" --hp|--host-path PATH 设置宿主机工作目录 (默认当前路径)");
72
- console.log(" -n|--cn|--cont-name NAME 设置容器名称");
73
- console.log(" --cp|--cont-path PATH 设置容器工作目录");
74
- console.log(" --in|--image-name NAME 指定镜像名称");
75
- console.log(" --iv|--image-ver VERSION 指定镜像版本");
76
- console.log(" -e|--env STRING 设置环境变量");
77
- console.log(" --ef|--env-file ENV_FILE 设置环境变量通过文件");
78
- console.log(" -v|--volume STRING 绑定挂载卷");
79
- console.log(" --rm|--remove-cont 删除-n容器");
80
- console.log(" --sp|--shell-prefix COMMAND 临时环境变量 (作为-s前缀)");
81
- console.log(" -s|--shell COMMAND 指定命令执行");
82
- console.log(" --|--shell-suffix COMMAND 指定命令参数, --后面全部直传 (作为-s后缀)");
83
- console.log(" -x|--shell-full COMMAND 指定完整命令执行, -x后面全部直传 (代替--sp和-s和--命令)");
84
- console.log(" -y|--yolo CLI 使AGENT无需确认 (代替-s命令)");
85
- console.log(" 例如 claude / c, gemini / gm, codex / cx, opencode / oc");
86
- console.log(" -m|--cm|--cont-mode STRING 设置容器嵌套容器模式");
87
- console.log(" 例如 common, dind, mdsock");
88
- console.log(" --ib|--image-build EXT 构建镜像,EXT 为镜像变体,逗号分割");
89
- console.log(" 例如 \"common\" (默认值), \"all\", \"go,codex,java,gemini\" ...");
90
- console.log(" --install NAME 安装manyoyo命令");
91
- console.log(" 例如 docker-cli-plugin");
92
- console.log(" -V|--version 显示版本");
93
- console.log(" -h|--help 显示帮助");
70
+ console.log(" -l|--ls|--list 列举容器");
71
+ console.log(" --hp|--host-path PATH 设置宿主机工作目录 (默认当前路径)");
72
+ console.log(" -n|--cn|--cont-name NAME 设置容器名称");
73
+ console.log(" --cp|--cont-path PATH 设置容器工作目录");
74
+ console.log(" --in|--image-name NAME 指定镜像名称");
75
+ console.log(" --iv|--image-ver VERSION 指定镜像版本");
76
+ console.log(" -e|--env XXX=YYY 设置环境变量");
77
+ console.log(" --ef|--env-file ENV_FILE 设置环境变量通过文件");
78
+ console.log(" -v|--volume XXX:YYY 绑定挂载卷");
79
+ console.log(" --rm|--remove-cont 删除-n容器");
80
+ console.log(" --sp|--shell-prefix COMMAND 临时环境变量 (作为-s前缀)");
81
+ console.log(" -s|--shell COMMAND 指定命令执行");
82
+ console.log(" --|--shell-suffix COMMAND 指定命令参数, --后面全部直传 (作为-s后缀)");
83
+ console.log(" -x|--shell-full COMMAND 指定完整命令执行, -x后面全部直传 (代替--sp和-s和--命令)");
84
+ console.log(" -y|--yolo CLI 使AGENT无需确认 (代替-s命令)");
85
+ console.log(" 例如 claude / c, gemini / gm, codex / cx, opencode / oc");
86
+ console.log(" -m|--cm|--cont-mode STRING 设置容器嵌套容器模式");
87
+ console.log(" 例如 common, dind, mdsock");
88
+ console.log(" --ib|--image-build 构建镜像");
89
+ console.log(" --iba|--image-build-arg XXX=YYY 构建镜像时传参给dockerfile");
90
+ console.log(" --ip|--image-prune 清理悬空镜像和 <none> 镜像");
91
+ console.log(" --install NAME 安装manyoyo命令");
92
+ console.log(" 例如 docker-cli-plugin");
93
+ console.log(" -V|--version 显示版本");
94
+ console.log(" -h|--help 显示帮助");
94
95
  console.log("");
95
96
  console.log(`${BLUE}Example:${NC}`);
96
- console.log(` ${MANYOYO_NAME} --ib all 构建 all 版本镜像`);
97
+ console.log(` ${MANYOYO_NAME} --ib 构建镜像`);
97
98
  console.log(` ${MANYOYO_NAME} -n test --ef ./xxx.env -y c 设置环境变量并运行无需确认的AGENT`);
98
99
  console.log(` ${MANYOYO_NAME} -n test -- -c 恢复之前会话`);
99
100
  console.log(` ${MANYOYO_NAME} -x echo 123 指定命令执行`);
@@ -300,13 +301,199 @@ function getContList() {
300
301
  }
301
302
  }
302
303
 
303
- async function buildImage(ext, imageName, imageVersion) {
304
+ function pruneDanglingImages() {
305
+ console.log(`\n${YELLOW}清理悬空镜像...${NC}`);
306
+ execSync(`${DOCKER_CMD} image prune -f`, { stdio: 'inherit' });
307
+
308
+ // Remove remaining <none> images
309
+ try {
310
+ const imagesOutput = execSync(`${DOCKER_CMD} images -a --format "{{.ID}} {{.Repository}}"`, { encoding: 'utf-8' });
311
+ const noneImages = imagesOutput
312
+ .split('\n')
313
+ .filter(line => line.includes('<none>'))
314
+ .map(line => line.split(' ')[0])
315
+ .filter(id => id);
316
+
317
+ if (noneImages.length > 0) {
318
+ console.log(`${YELLOW}清理剩余的 <none> 镜像 (${noneImages.length} 个)...${NC}`);
319
+ execSync(`${DOCKER_CMD} rmi -f ${noneImages.join(' ')}`, { stdio: 'inherit' });
320
+ }
321
+ } catch (e) {
322
+ // Ignore errors if no <none> images found
323
+ }
324
+
325
+ console.log(`${GREEN}✅ 清理完成${NC}`);
326
+ }
327
+
328
+ async function prepareBuildCache(imageTool) {
329
+ const cacheDir = path.join(__dirname, '../docker/cache');
330
+ const timestampFile = path.join(cacheDir, '.timestamps.json');
331
+ const cacheTTLDays = 2;
332
+
333
+ console.log(`\n${CYAN}准备构建缓存...${NC}`);
334
+
335
+ // Create cache directory
336
+ if (!fs.existsSync(cacheDir)) {
337
+ fs.mkdirSync(cacheDir, { recursive: true });
338
+ }
339
+
340
+ // Load timestamps
341
+ let timestamps = {};
342
+ if (fs.existsSync(timestampFile)) {
343
+ try {
344
+ timestamps = JSON.parse(fs.readFileSync(timestampFile, 'utf-8'));
345
+ } catch (e) {
346
+ timestamps = {};
347
+ }
348
+ }
349
+
350
+ const now = new Date();
351
+ const isExpired = (key) => {
352
+ if (!timestamps[key]) return true;
353
+ const cachedTime = new Date(timestamps[key]);
354
+ const diffDays = (now - cachedTime) / (1000 * 60 * 60 * 24);
355
+ return diffDays > cacheTTLDays;
356
+ };
357
+
358
+ // Determine architecture
359
+ const arch = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : process.arch;
360
+ const archNode = arch === 'amd64' ? 'x64' : 'arm64';
361
+
362
+ // Prepare Node.js cache
363
+ const nodeCacheDir = path.join(cacheDir, 'node');
364
+ const nodeVersion = 24;
365
+ const nodeKey = 'node/'; // 使用目录级别的相对路径
366
+
367
+ if (!fs.existsSync(nodeCacheDir)) {
368
+ fs.mkdirSync(nodeCacheDir, { recursive: true });
369
+ }
370
+
371
+ const hasNodeCache = fs.existsSync(nodeCacheDir) && fs.readdirSync(nodeCacheDir).some(f => f.startsWith('node-') && f.includes(`linux-${archNode}`));
372
+ if (!hasNodeCache || isExpired(nodeKey)) {
373
+ console.log(`${YELLOW}下载 Node.js ${nodeVersion} (${archNode})...${NC}`);
374
+ const mirror = 'https://mirrors.tencent.com/nodejs-release';
375
+ try {
376
+ const shasum = execSync(`curl -sL ${mirror}/latest-v${nodeVersion}.x/SHASUMS256.txt | grep linux-${archNode}.tar.gz | awk '{print $2}'`, { encoding: 'utf-8' }).trim();
377
+ const nodeUrl = `${mirror}/latest-v${nodeVersion}.x/${shasum}`;
378
+ const nodeTargetPath = path.join(nodeCacheDir, shasum);
379
+ execSync(`curl -fsSL "${nodeUrl}" -o "${nodeTargetPath}"`, { stdio: 'inherit' });
380
+ timestamps[nodeKey] = now.toISOString();
381
+ fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 2));
382
+ console.log(`${GREEN}✓ Node.js 下载完成${NC}`);
383
+ } catch (e) {
384
+ console.error(`${RED}错误: Node.js 下载失败${NC}`);
385
+ throw e;
386
+ }
387
+ } else {
388
+ console.log(`${GREEN}✓ Node.js 缓存已存在${NC}`);
389
+ }
390
+
391
+ // Prepare JDT LSP cache (for java variant)
392
+ if (imageTool === 'full' || imageTool.includes('java')) {
393
+ const jdtlsCacheDir = path.join(cacheDir, 'jdtls');
394
+ const jdtlsKey = 'jdtls/jdt-language-server-latest.tar.gz'; // 使用相对路径
395
+ const jdtlsPath = path.join(cacheDir, jdtlsKey);
396
+
397
+ if (!fs.existsSync(jdtlsCacheDir)) {
398
+ fs.mkdirSync(jdtlsCacheDir, { recursive: true });
399
+ }
400
+
401
+ if (!fs.existsSync(jdtlsPath) || isExpired(jdtlsKey)) {
402
+ console.log(`${YELLOW}下载 JDT Language Server...${NC}`);
403
+ const jdtUrl = 'https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz';
404
+ try {
405
+ execSync(`curl -fsSL "${jdtUrl}" -o "${jdtlsPath}"`, { stdio: 'inherit' });
406
+ timestamps[jdtlsKey] = now.toISOString();
407
+ fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 2));
408
+ console.log(`${GREEN}✓ JDT LSP 下载完成${NC}`);
409
+ } catch (e) {
410
+ console.error(`${RED}错误: JDT LSP 下载失败${NC}`);
411
+ throw e;
412
+ }
413
+ } else {
414
+ console.log(`${GREEN}✓ JDT LSP 缓存已存在${NC}`);
415
+ }
416
+ }
417
+
418
+ // Prepare gopls cache (for go variant)
419
+ if (imageTool === 'full' || imageTool.includes('go')) {
420
+ const goplsCacheDir = path.join(cacheDir, 'gopls');
421
+ const goplsKey = `gopls/gopls-linux-${arch}`; // 使用相对路径
422
+ const goplsPath = path.join(cacheDir, goplsKey);
423
+
424
+ if (!fs.existsSync(goplsCacheDir)) {
425
+ fs.mkdirSync(goplsCacheDir, { recursive: true });
426
+ }
427
+
428
+ if (!fs.existsSync(goplsPath) || isExpired(goplsKey)) {
429
+ console.log(`${YELLOW}下载 gopls (${arch})...${NC}`);
430
+ try {
431
+ // Download using go install in temporary environment
432
+ const tmpGoPath = path.join(cacheDir, '.tmp-go');
433
+
434
+ // Clean up existing temp directory (with go clean for mod cache)
435
+ if (fs.existsSync(tmpGoPath)) {
436
+ try {
437
+ execSync(`GOPATH="${tmpGoPath}" go clean -modcache 2>/dev/null || true`, { stdio: 'inherit' });
438
+ execSync(`chmod -R u+w "${tmpGoPath}" 2>/dev/null || true`, { stdio: 'inherit' });
439
+ execSync(`rm -rf "${tmpGoPath}"`, { stdio: 'inherit' });
440
+ } catch (e) {
441
+ // Ignore cleanup errors
442
+ }
443
+ }
444
+ fs.mkdirSync(tmpGoPath, { recursive: true });
445
+
446
+ execSync(`GOPATH="${tmpGoPath}" GOOS=linux GOARCH=${arch} go install golang.org/x/tools/gopls@latest`, { stdio: 'inherit' });
447
+ execSync(`cp "${tmpGoPath}/bin/linux_${arch}/gopls" "${goplsPath}" || cp "${tmpGoPath}/bin/gopls" "${goplsPath}"`, { stdio: 'inherit' });
448
+ execSync(`chmod +x "${goplsPath}"`, { stdio: 'inherit' });
449
+
450
+ // Save timestamp immediately after successful download
451
+ timestamps[goplsKey] = now.toISOString();
452
+ fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 2));
453
+ console.log(`${GREEN}✓ gopls 下载完成${NC}`);
454
+
455
+ // Clean up temp directory (with go clean for mod cache)
456
+ try {
457
+ execSync(`GOPATH="${tmpGoPath}" go clean -modcache 2>/dev/null || true`, { stdio: 'inherit' });
458
+ execSync(`chmod -R u+w "${tmpGoPath}" 2>/dev/null || true`, { stdio: 'inherit' });
459
+ execSync(`rm -rf "${tmpGoPath}"`, { stdio: 'inherit' });
460
+ } catch (e) {
461
+ console.log(`${YELLOW}提示: 临时目录清理失败,可手动删除 ${tmpGoPath}${NC}`);
462
+ }
463
+ } catch (e) {
464
+ console.error(`${RED}错误: gopls 下载失败${NC}`);
465
+ throw e;
466
+ }
467
+ } else {
468
+ console.log(`${GREEN}✓ gopls 缓存已存在${NC}`);
469
+ }
470
+ }
471
+
472
+ // Save timestamps
473
+ fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 2));
474
+ console.log(`${GREEN}✅ 构建缓存准备完成${NC}\n`);
475
+ }
476
+
477
+ function addImageBuildArg(string) {
478
+ IMAGE_BUILD_ARGS.push("--build-arg", string);
479
+ }
480
+
481
+ async function buildImage(IMAGE_BUILD_ARGS, imageName, imageVersion) {
482
+ let imageTool = "full";
483
+ if (IMAGE_BUILD_ARGS.length === 0) {
484
+ IMAGE_BUILD_ARGS = ["--build-arg", `TOOL=${imageTool}`];
485
+ } else {
486
+ imageTool = IMAGE_BUILD_ARGS.filter(v => v.startsWith("TOOL=")).at(-1)?.slice("TOOL=".length) ?? imageTool;
487
+ }
304
488
  // Use package.json imageVersion if not specified
305
489
  const version = imageVersion || IMAGE_VERSION_BASE;
306
- const fullImageTag = `${imageName}:${version}-${ext}`;
490
+ const fullImageTag = `${imageName}:${version}-${imageTool}`;
307
491
 
308
492
  console.log(`${CYAN}🔨 正在构建镜像: ${YELLOW}${fullImageTag}${NC}`);
309
- console.log(`${BLUE}构建参数: EXT=${ext}${NC}\n`);
493
+ console.log(`${BLUE}构建组件类型: ${imageTool}${NC}\n`);
494
+
495
+ // Prepare cache (自动检测并下载缺失的文件)
496
+ await prepareBuildCache(imageTool);
310
497
 
311
498
  // Find Dockerfile path
312
499
  const dockerfilePath = path.join(__dirname, '../docker/manyoyo.Dockerfile');
@@ -316,7 +503,8 @@ async function buildImage(ext, imageName, imageVersion) {
316
503
  }
317
504
 
318
505
  // Build command
319
- const buildCmd = `${DOCKER_CMD} build -t "${fullImageTag}" -f "${dockerfilePath}" "${path.join(__dirname, '..')}" --build-arg EXT=${ext} --no-cache`;
506
+ const imageBuildArgs = IMAGE_BUILD_ARGS.join(' ');
507
+ const buildCmd = `${DOCKER_CMD} build -t "${fullImageTag}" -f "${dockerfilePath}" "${path.join(__dirname, '..')}" ${imageBuildArgs} --load --progress=plain --no-cache`;
320
508
 
321
509
  console.log(`${BLUE}准备执行命令:${NC}`);
322
510
  console.log(`${buildCmd}\n`);
@@ -328,12 +516,10 @@ async function buildImage(ext, imageName, imageVersion) {
328
516
  execSync(buildCmd, { stdio: 'inherit' });
329
517
  console.log(`\n${GREEN}✅ 镜像构建成功: ${fullImageTag}${NC}`);
330
518
  console.log(`${BLUE}使用镜像:${NC}`);
331
- console.log(` manyoyo -n test --in ${imageName} --iv ${version}-${ext} -y c`);
519
+ console.log(` manyoyo -n test --in ${imageName} --iv ${version}-${imageTool} -y c`);
332
520
 
333
521
  // Prune dangling images
334
- console.log(`\n${YELLOW}清理悬空镜像...${NC}`);
335
- execSync(`${DOCKER_CMD} image prune -f`, { stdio: 'inherit' });
336
- console.log(`${GREEN}✅ 清理完成${NC}`);
522
+ pruneDanglingImages();
337
523
  } catch (e) {
338
524
  console.error(`${RED}错误: 镜像构建失败${NC}`);
339
525
  process.exit(1);
@@ -486,11 +672,21 @@ function parseArguments(argv) {
486
672
 
487
673
  case '--ib':
488
674
  case '--image-build':
489
- SHOULD_BUILD_IMAGE = true;
490
- BUILD_IMAGE_EXT = args[i + 1];
675
+ IMAGE_BUILD_NEED = true;
676
+ i += 1;
677
+ break;
678
+
679
+ case '--iba':
680
+ case '--image-build-arg':
681
+ addImageBuildArg(args[i + 1]);
491
682
  i += 2;
492
683
  break;
493
684
 
685
+ case '--ip':
686
+ case '--image-prune':
687
+ pruneDanglingImages();
688
+ process.exit(0);
689
+
494
690
  case '--install':
495
691
  installManyoyo(args[i + 1]);
496
692
  process.exit(0);
@@ -686,8 +882,8 @@ async function main() {
686
882
  parseArguments(process.argv);
687
883
 
688
884
  // 3. Handle image build operation
689
- if (SHOULD_BUILD_IMAGE) {
690
- await buildImage(BUILD_IMAGE_EXT, IMAGE_NAME, IMAGE_VERSION.split('-')[0]);
885
+ if (IMAGE_BUILD_NEED) {
886
+ await buildImage(IMAGE_BUILD_ARGS, IMAGE_NAME, IMAGE_VERSION.split('-')[0]);
691
887
  process.exit(0);
692
888
  }
693
889
 
@@ -1,8 +1,65 @@
1
+ # ==============================================================================
2
+ # Stage 1: 缓存准备阶段 - 智能检测缓存或下载
3
+ # ==============================================================================
4
+ FROM ubuntu:24.04 AS cache-stage
5
+
6
+ ARG TARGETARCH
7
+
8
+ # 复制缓存目录(可能为空)
9
+ COPY ./docker/cache/ /cache/
10
+
11
+ RUN <<EOX
12
+ # 确定架构
13
+ case "$TARGETARCH" in
14
+ amd64) ARCH_NODE="x64"; ARCH_GO="amd64" ;;
15
+ arm64) ARCH_NODE="arm64"; ARCH_GO="arm64" ;;
16
+ *) ARCH_NODE="$TARGETARCH"; ARCH_GO="$TARGETARCH" ;;
17
+ esac
18
+
19
+ # Node.js: 检测缓存,不存在则下载
20
+ mkdir -p /opt/node
21
+ if ls /cache/node/node-*-linux-${ARCH_NODE}.tar.gz 1> /dev/null 2>&1; then
22
+ echo "使用 Node.js 缓存"
23
+ NODE_TAR=$(ls /cache/node/node-*-linux-${ARCH_NODE}.tar.gz | head -1)
24
+ tar -xzf ${NODE_TAR} -C /opt/node --strip-components=1 --exclude='*.md' --exclude='LICENSE' --no-same-owner
25
+ else
26
+ echo "下载 Node.js"
27
+ NVM_NODEJS_ORG_MIRROR=https://mirrors.tencent.com/nodejs-release/
28
+ NODE_TAR=$(curl -sL ${NVM_NODEJS_ORG_MIRROR}/latest-v24.x/SHASUMS256.txt | grep linux-${ARCH_NODE}.tar.gz | awk '{print $2}')
29
+ curl -fsSL ${NVM_NODEJS_ORG_MIRROR}/latest-v24.x/${NODE_TAR} | tar -xz -C /opt/node --strip-components=1 --exclude='*.md' --exclude='LICENSE'
30
+ fi
31
+
32
+ # JDT LSP: 检测缓存,不存在则下载
33
+ mkdir -p /opt/jdtls
34
+ if [ -f /cache/jdtls/jdt-language-server-latest.tar.gz ]; then
35
+ echo "使用 JDT LSP 缓存"
36
+ tar -xzf /cache/jdtls/jdt-language-server-latest.tar.gz -C /opt/jdtls --no-same-owner
37
+ else
38
+ echo "下载 JDT LSP"
39
+ curl -fsSL https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz | tar -xz -C /opt/jdtls
40
+ fi
41
+
42
+ # gopls: 检测缓存,不存在则下载
43
+ mkdir -p /opt/gopls
44
+ if [ -f /cache/gopls/gopls-linux-${ARCH_GO} ]; then
45
+ echo "使用 gopls 缓存"
46
+ cp /cache/gopls/gopls-linux-${ARCH_GO} /opt/gopls/gopls
47
+ chmod +x /opt/gopls/gopls
48
+ else
49
+ echo "下载 gopls (需要 go 环境)"
50
+ # gopls 需要编译,这里跳过,在最终阶段处理
51
+ touch /opt/gopls/.no-cache
52
+ fi
53
+ EOX
54
+
55
+ # ==============================================================================
56
+ # Stage 2: 最终镜像
57
+ # ==============================================================================
1
58
  FROM ubuntu:24.04
2
59
 
3
60
  ARG TARGETARCH
4
61
  ARG NODE_VERSION=24
5
- ARG EXT="common"
62
+ ARG TOOL="full"
6
63
 
7
64
  RUN <<EOX
8
65
  # 部署 system
@@ -23,7 +80,7 @@ RUN <<EOX
23
80
  supervisor
24
81
 
25
82
  # 安装 docker
26
- case ",$EXT," in *,all,*|*,docker,*)
83
+ case ",$TOOL," in *,full,*|*,docker,*)
27
84
  apt-get install -y --no-install-recommends docker.io
28
85
  ;; esac
29
86
 
@@ -45,16 +102,12 @@ RUN <<EOX
45
102
  rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.cache ~/.npm ~/go/pkg/mod/cache
46
103
  EOX
47
104
 
105
+ # 从 cache-stage 复制 Node.js(缓存或下载)
106
+ COPY --from=cache-stage /opt/node /usr/local
107
+ ARG GIT_SSL_NO_VERIFY=false
108
+
48
109
  RUN <<EOX
49
- # 安装 node.js
50
- case "$TARGETARCH" in
51
- amd64) ARCH_NODE="x64" ;;
52
- arm64) ARCH_NODE="arm64" ;;
53
- *) ARCH_NODE="$TARGETARCH" ;;
54
- esac
55
- NVM_NODEJS_ORG_MIRROR=https://mirrors.tencent.com/nodejs-release/
56
- NODE_TAR=$(curl -sL ${NVM_NODEJS_ORG_MIRROR}/latest-v${NODE_VERSION}.x/SHASUMS256.txt | grep linux-${ARCH_NODE}.tar.gz | awk '{print $2}')
57
- curl -fsSL ${NVM_NODEJS_ORG_MIRROR}/latest-v${NODE_VERSION}.x/${NODE_TAR} | tar -xz -C /usr/local --strip-components=1 --exclude='*.md' --exclude='LICENSE'
110
+ # 配置 node.js
58
111
  npm config set registry=https://mirrors.tencent.com/npm/
59
112
  npm install -g npm
60
113
 
@@ -75,7 +128,7 @@ RUN <<EOX
75
128
  }
76
129
  }
77
130
  EOF
78
- claude plugin marketplace add anthropics/claude-plugins-official
131
+ GIT_SSL_NO_VERIFY=$GIT_SSL_NO_VERIFY claude plugin marketplace add anthropics/claude-plugins-official
79
132
  claude plugin install ralph-loop@claude-plugins-official
80
133
  claude plugin install typescript-lsp@claude-plugins-official
81
134
  claude plugin install pyright-lsp@claude-plugins-official
@@ -83,7 +136,7 @@ EOF
83
136
  claude plugin install jdtls-lsp@claude-plugins-official
84
137
 
85
138
  # 安装 Gemini CLI
86
- case ",$EXT," in *,all,*|*,gemini,*)
139
+ case ",$TOOL," in *,full,*|*,gemini,*)
87
140
  npm install -g @google/gemini-cli
88
141
  mkdir -p ~/.gemini/
89
142
  cat > ~/.gemini/settings.json <<EOF
@@ -109,12 +162,12 @@ EOF
109
162
  ;; esac
110
163
 
111
164
  # 安装 Codex CLI
112
- case ",$EXT," in *,all,*|*,codex,*)
165
+ case ",$TOOL," in *,full,*|*,codex,*)
113
166
  npm install -g @openai/codex
114
167
  ;; esac
115
168
 
116
169
  # 安装 Copilot CLI
117
- case ",$EXT," in *,all,*|*,copilot,*)
170
+ case ",$TOOL," in *,full,*|*,copilot,*)
118
171
  npm install -g @github/copilot
119
172
  mkdir -p ~/.copilot/
120
173
  cat > ~/.copilot/config.json <<EOF
@@ -129,7 +182,7 @@ EOF
129
182
  ;; esac
130
183
 
131
184
  # 安装 OpenCode CLI
132
- case ",$EXT," in *,all,*|*,opencode,*)
185
+ case ",$TOOL," in *,full,*|*,opencode,*)
133
186
  npm install -g opencode-ai
134
187
  mkdir -p ~/.config/opencode/
135
188
  cat > ~/.config/opencode/opencode.json <<EOF
@@ -164,18 +217,16 @@ EOF
164
217
  rm -rf /tmp/* /var/tmp/* /var/log/apt /var/log/*.log /var/lib/apt/lists/* ~/.cache ~/.npm ~/go/pkg/mod/cache
165
218
  EOX
166
219
 
167
- COPY ./docker/lib/jdt-language-server-latest.tar.gz /tmp/
220
+ # 从 cache-stage 复制 JDT LSP(缓存或下载)
221
+ COPY --from=cache-stage /opt/jdtls /root/.local/share/jdtls
222
+
168
223
  RUN <<EOX
169
224
  # 安装 java
170
- case ",$EXT," in *,all,*|*,java,*)
225
+ case ",$TOOL," in *,full,*|*,java,*)
171
226
  apt-get update -y
172
227
  apt-get install -y --no-install-recommends openjdk-21-jdk maven
173
228
 
174
- # 安装 LSP服务(java)
175
- mkdir -p ~/.local/share/jdtls
176
- # wget -O /tmp/jdt-language-server-latest.tar.gz https://download.eclipse.org/jdtls/snapshots/jdt-language-server-latest.tar.gz
177
- tar -xzf /tmp/jdt-language-server-latest.tar.gz -C ~/.local/share/jdtls
178
- rm -f /tmp/jdt-language-server-latest.tar.gz
229
+ # 配置 LSP服务(java)
179
230
  ln -sf ~/.local/share/jdtls/bin/jdtls /usr/local/bin/jdtls
180
231
 
181
232
  # 清理
@@ -184,16 +235,27 @@ RUN <<EOX
184
235
  ;; esac
185
236
  EOX
186
237
 
238
+ # 从 cache-stage 复制 gopls(缓存或下载)
239
+ COPY --from=cache-stage /opt/gopls /tmp/gopls-cache
240
+
187
241
  RUN <<EOX
188
242
  # 安装 go
189
- case ",$EXT," in *,all,*|*,go,*)
243
+ case ",$TOOL," in *,full,*|*,go,*)
190
244
  apt-get update -y
191
245
  apt-get install -y --no-install-recommends golang golang-src gcc
192
246
  go env -w GOPROXY=https://mirrors.tencent.com/go
193
247
 
194
248
  # 安装 LSP服务(go)
195
- go install golang.org/x/tools/gopls@latest
196
- ln -sf ~/go/bin/gopls /usr/local/bin/gopls
249
+ if [ -f /tmp/gopls-cache/gopls ] && [ ! -f /tmp/gopls-cache/.no-cache ]; then
250
+ # 使用缓存
251
+ cp /tmp/gopls-cache/gopls /usr/local/bin/gopls
252
+ chmod +x /usr/local/bin/gopls
253
+ else
254
+ # 下载编译
255
+ go install golang.org/x/tools/gopls@latest
256
+ ln -sf ~/go/bin/gopls /usr/local/bin/gopls
257
+ fi
258
+ rm -rf /tmp/gopls-cache
197
259
 
198
260
  # 清理
199
261
  apt-get clean
package/docs/README_EN.md CHANGED
@@ -1,4 +1,4 @@
1
- [ [English](README_EN.md) ] | [中文](../README.md)
1
+ << [English](README_EN.md) >> | [中文](../README.md)
2
2
 
3
3
  ---
4
4
 
@@ -49,17 +49,21 @@ After installing manyoyo, use the built-in command to build images:
49
49
  # Pull base image
50
50
  podman pull ubuntu:24.04
51
51
 
52
- # Build using manyoyo (Recommended)
53
- manyoyo --ib all # Build all version (includes all tools)
54
- manyoyo --ib common # Build common version (basic version)
55
- manyoyo --ib go,codex,java,gemini # Build go version (includes go,codex,java,gemini tools)
52
+ # Build using manyoyo (Recommended, auto-cache enabled)
53
+ manyoyo --ib # Build full version by default (Recommended)
54
+ manyoyo --ib --iba TOOL=common # Build common version
55
+ manyoyo --ib --iba TOOL=go,codex,java,gemini # Build custom combination
56
+ manyoyo --ib --iba --iba GIT_SSL_NO_VERIFY=true # Build the full version and skip Git SSL verification
57
+ manyoyo --ib all --in myimage --iv 2.0.0 # Customize the image name and version to produce myimage:2.0.0-all
58
+ manyoyo --ip # Clean dangling images and <none> images
56
59
 
57
- # Custom image name and version
58
- manyoyo --ib all --in myimage --iv 2.0.0
59
- # Builds: myimage:2.0.0-all
60
+ # How it works:
61
+ # - First build: Auto-downloads Node.js, JDT LSP, gopls etc. to docker/cache/
62
+ # - Rebuild within 2 days: Uses local cache, ~5x faster
63
+ # - After cache expires: Auto-downloads latest versions
60
64
 
61
65
  # Or build manually (Not recommended)
62
- iv=1.4.0 && podman build -t localhost/xcanwin/manyoyo:$iv-all -f docker/manyoyo.Dockerfile . --build-arg EXT=all --no-cache
66
+ iv=1.4.0 && podman build -t localhost/xcanwin/manyoyo:$iv-all -f docker/manyoyo.Dockerfile . --build-arg TOOL=all --no-cache
63
67
  podman image prune -f
64
68
  ```
65
69
 
@@ -207,7 +211,9 @@ docker ps -a
207
211
  | `-x CMD` | `--sf`, `--shell-full` | Full command (replaces --sp, -s, and --) |
208
212
  | `-y CLI` | `--yolo` | Run AI agent without confirmation |
209
213
  | `-m MODE` | `--cm`, `--cont-mode` | Set container mode (common, dind, mdsock) |
210
- | `--ib EXT` | `--image-build` | Build image, EXT is image variant (all, go, common) |
214
+ | `--ib` | `--image-build` | Build image |
215
+ | `--iba` | `--image-build-arg` | Pass arguments to a Dockerfile during image build |
216
+ | `--ip` | `--image-prune` | Clean dangling images and `<none>` images |
211
217
  | `--install NAME` | | Install manyoyo command |
212
218
  | `-V` | `--version` | Show version |
213
219
  | `-h` | `--help` | Show help |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xcanwin/manyoyo",
3
- "version": "3.2.0",
4
- "imageVersion": "1.5.0",
3
+ "version": "3.4.5",
4
+ "imageVersion": "1.6.2",
5
5
  "description": "AI Agent CLI Security Sandbox",
6
6
  "keywords": [
7
7
  "ai", "agent", "sandbox", "docker", "cli", "container", "development"