@xcanwin/manyoyo 4.1.1 → 4.1.10

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/bin/manyoyo.js CHANGED
@@ -1,9 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // ==============================================================================
4
- // manyoyo - AI Agent CLI Sandbox - xcanwin
5
- // ==============================================================================
6
-
7
3
  const { execSync, spawnSync } = require('child_process');
8
4
  const fs = require('fs');
9
5
  const path = require('path');
@@ -14,6 +10,10 @@ const readline = require('readline');
14
10
  const { Command } = require('commander');
15
11
  const JSON5 = require('json5');
16
12
  const { startWebServer } = require('../lib/web/server');
13
+ const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
14
+ const { initAgentConfigs } = require('../lib/init-config');
15
+ const { buildImage } = require('../lib/image-build');
16
+ const { resolveAgentResumeArg } = require('../lib/agent-resume');
17
17
  const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
18
18
  const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
19
19
  const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
@@ -40,12 +40,7 @@ function detectCommandName() {
40
40
  return baseName || 'manyoyo';
41
41
  }
42
42
 
43
- // ==============================================================================
44
- // Configuration Constants
45
- // ==============================================================================
46
-
47
43
  const CONFIG = {
48
- CACHE_TTL_DAYS: 2, // 缓存过期天数
49
44
  CONTAINER_READY_MAX_RETRIES: 30, // 容器就绪最大重试次数
50
45
  CONTAINER_READY_INITIAL_DELAY: 100, // 容器就绪初始延迟(ms)
51
46
  CONTAINER_READY_MAX_DELAY: 2000, // 容器就绪最大延迟(ms)
@@ -60,14 +55,12 @@ let IMAGE_VERSION = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
60
55
  let EXEC_COMMAND = "";
61
56
  let EXEC_COMMAND_PREFIX = "";
62
57
  let EXEC_COMMAND_SUFFIX = "";
63
- let ENV_FILE = "";
64
58
  let SHOULD_REMOVE = false;
65
59
  let IMAGE_BUILD_NEED = false;
66
60
  let IMAGE_BUILD_ARGS = [];
67
61
  let CONTAINER_ENVS = [];
68
62
  let CONTAINER_VOLUMES = [];
69
63
  let MANYOYO_NAME = detectCommandName();
70
- let CONT_MODE = "";
71
64
  let CONT_MODE_ARGS = [];
72
65
  let QUIET = {};
73
66
  let SHOW_COMMAND = false;
@@ -94,10 +87,6 @@ const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)
94
87
  let DOCKER_CMD = 'docker';
95
88
  const SUPPORTED_INIT_AGENTS = ['claude', 'codex', 'gemini', 'opencode'];
96
89
 
97
- // ==============================================================================
98
- // SECTION: Utility Functions
99
- // ==============================================================================
100
-
101
90
  function sleep(ms) {
102
91
  return new Promise(resolve => setTimeout(resolve, ms));
103
92
  }
@@ -116,6 +105,19 @@ function resolveContainerNameTemplate(name) {
116
105
  return name.replace(/\{now\}|\$\{now\}/g, nowValue);
117
106
  }
118
107
 
108
+ function pickConfigValue(...values) {
109
+ for (const value of values) {
110
+ if (value) {
111
+ return value;
112
+ }
113
+ }
114
+ return undefined;
115
+ }
116
+
117
+ function mergeArrayConfig(globalValue, runValue, cliValue) {
118
+ return [...(globalValue || []), ...(runValue || []), ...(cliValue || [])];
119
+ }
120
+
119
121
  function validateServerHost(host, rawServer) {
120
122
  const value = String(host || '').trim();
121
123
  const isIp = net.isIP(value) !== 0;
@@ -185,18 +187,6 @@ function ensureWebServerAuthCredentials() {
185
187
  }
186
188
  }
187
189
 
188
- /**
189
- * 计算文件的 SHA256 哈希值(跨平台)
190
- * @param {string} filePath - 文件路径
191
- * @returns {string} SHA256 哈希值(十六进制)
192
- */
193
- function getFileSha256(filePath) {
194
- const fileBuffer = fs.readFileSync(filePath);
195
- const hashSum = crypto.createHash('sha256');
196
- hashSum.update(fileBuffer);
197
- return hashSum.digest('hex');
198
- }
199
-
200
190
  /**
201
191
  * 敏感信息脱敏(用于 --show-config 输出)
202
192
  * @param {Object} obj - 配置对象
@@ -240,10 +230,6 @@ function sanitizeSensitiveData(obj) {
240
230
  return result;
241
231
  }
242
232
 
243
- // ==============================================================================
244
- // SECTION: Configuration Management
245
- // ==============================================================================
246
-
247
233
  /**
248
234
  * @typedef {Object} Config
249
235
  * @property {string} [containerName] - 容器名称
@@ -305,466 +291,15 @@ function loadRunConfig(name, config) {
305
291
  return runConfig;
306
292
  }
307
293
 
308
- function readJsonFileSafely(filePath, label) {
309
- if (!fs.existsSync(filePath)) {
310
- return null;
311
- }
312
- try {
313
- return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
314
- } catch (e) {
315
- console.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
316
- return null;
317
- }
318
- }
319
-
320
- function parseSimpleToml(content) {
321
- const result = {};
322
- let current = result;
323
- const lines = String(content || '').split('\n');
324
-
325
- for (const rawLine of lines) {
326
- const line = rawLine.trim();
327
- if (!line || line.startsWith('#')) {
328
- continue;
329
- }
330
-
331
- const sectionMatch = line.match(/^\[([^\]]+)\]$/);
332
- if (sectionMatch) {
333
- const parts = sectionMatch[1].split('.').map(p => p.trim()).filter(Boolean);
334
- current = result;
335
- for (const part of parts) {
336
- if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) {
337
- current[part] = {};
338
- }
339
- current = current[part];
340
- }
341
- continue;
342
- }
343
-
344
- const keyValueMatch = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
345
- if (!keyValueMatch) {
346
- continue;
347
- }
348
-
349
- const key = keyValueMatch[1];
350
- let valueText = keyValueMatch[2].trim();
351
- if ((valueText.startsWith('"') && valueText.endsWith('"')) || (valueText.startsWith("'") && valueText.endsWith("'"))) {
352
- valueText = valueText.slice(1, -1);
353
- } else if (valueText === 'true') {
354
- valueText = true;
355
- } else if (valueText === 'false') {
356
- valueText = false;
357
- } else if (/^-?\d+(\.\d+)?$/.test(valueText)) {
358
- valueText = Number(valueText);
359
- }
360
-
361
- current[key] = valueText;
362
- }
363
-
364
- return result;
365
- }
366
-
367
- function readTomlFileSafely(filePath, label) {
368
- if (!fs.existsSync(filePath)) {
369
- return null;
370
- }
371
- try {
372
- const content = fs.readFileSync(filePath, 'utf-8');
373
- return parseSimpleToml(content);
374
- } catch (e) {
375
- console.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
376
- return null;
377
- }
378
- }
379
-
380
- function normalizeInitConfigAgents(rawAgents) {
381
- const aliasMap = {
382
- all: 'all',
383
- claude: 'claude',
384
- c: 'claude',
385
- cc: 'claude',
386
- codex: 'codex',
387
- cx: 'codex',
388
- gemini: 'gemini',
389
- gm: 'gemini',
390
- g: 'gemini',
391
- opencode: 'opencode',
392
- oc: 'opencode'
393
- };
394
-
395
- if (rawAgents === true || rawAgents === undefined || rawAgents === null || rawAgents === '') {
396
- return [...SUPPORTED_INIT_AGENTS];
397
- }
398
-
399
- const tokens = String(rawAgents).split(/[,\s]+/).map(v => v.trim().toLowerCase()).filter(Boolean);
400
- if (tokens.length === 0) {
401
- return [...SUPPORTED_INIT_AGENTS];
402
- }
403
-
404
- const normalized = [];
405
- for (const token of tokens) {
406
- const mapped = aliasMap[token];
407
- if (!mapped) {
408
- console.error(`${RED}⚠️ 错误: --init-config 不支持的 Agent: ${token}${NC}`);
409
- console.error(`${YELLOW}支持: ${SUPPORTED_INIT_AGENTS.join(', ')} 或 all${NC}`);
410
- process.exit(1);
411
- }
412
- if (mapped === 'all') {
413
- return [...SUPPORTED_INIT_AGENTS];
414
- }
415
- if (!normalized.includes(mapped)) {
416
- normalized.push(mapped);
417
- }
418
- }
419
- return normalized;
420
- }
421
-
422
- function isSafeInitEnvValue(value) {
423
- if (value === undefined || value === null) {
424
- return false;
425
- }
426
- const text = String(value).replace(/[\r\n\0]/g, '').trim();
427
- if (!text) {
428
- return false;
429
- }
430
- if (/[\$\(\)\`\|\&\*\{\};<>]/.test(text)) {
431
- return false;
432
- }
433
- if (/^\(/.test(text)) {
434
- return false;
435
- }
436
- return true;
437
- }
438
-
439
- function setInitValue(values, key, value) {
440
- if (value === undefined || value === null) {
441
- return;
442
- }
443
- const text = String(value).replace(/[\r\n\0]/g, '').trim();
444
- if (!text) {
445
- return;
446
- }
447
- values[key] = text;
448
- }
449
-
450
- function dedupeList(list) {
451
- return Array.from(new Set((list || []).filter(Boolean)));
452
- }
453
-
454
- function resolveEnvPlaceholder(value) {
455
- if (typeof value !== 'string') {
456
- return "";
457
- }
458
- const match = value.match(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/);
459
- if (!match) {
460
- return "";
461
- }
462
- const envName = match[1];
463
- return process.env[envName] ? String(process.env[envName]).trim() : "";
464
- }
465
-
466
- function collectClaudeInitData(homeDir) {
467
- const keys = [
468
- 'ANTHROPIC_AUTH_TOKEN',
469
- 'CLAUDE_CODE_OAUTH_TOKEN',
470
- 'ANTHROPIC_BASE_URL',
471
- 'ANTHROPIC_MODEL',
472
- 'ANTHROPIC_DEFAULT_OPUS_MODEL',
473
- 'ANTHROPIC_DEFAULT_SONNET_MODEL',
474
- 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
475
- 'CLAUDE_CODE_SUBAGENT_MODEL'
476
- ];
477
- const values = {};
478
- const notes = [];
479
- const volumes = [];
480
-
481
- const claudeDir = path.join(homeDir, '.claude');
482
- const claudeSettingsPath = path.join(claudeDir, 'settings.json');
483
- const settingsJson = readJsonFileSafely(claudeSettingsPath, 'Claude settings');
484
-
485
- keys.forEach(key => setInitValue(values, key, process.env[key]));
486
-
487
- if (settingsJson && settingsJson.env && typeof settingsJson.env === 'object') {
488
- keys.forEach(key => setInitValue(values, key, settingsJson.env[key]));
489
- }
490
-
491
- return { keys, values, notes, volumes: dedupeList(volumes) };
492
- }
493
-
494
- function collectGeminiInitData(homeDir) {
495
- const keys = [
496
- 'GOOGLE_GEMINI_BASE_URL',
497
- 'GEMINI_API_KEY',
498
- 'GEMINI_MODEL'
499
- ];
500
- const values = {};
501
- const notes = [];
502
- const volumes = [];
503
- const geminiDir = path.join(homeDir, '.gemini');
504
-
505
- keys.forEach(key => setInitValue(values, key, process.env[key]));
506
-
507
- if (fs.existsSync(geminiDir)) {
508
- volumes.push(`${geminiDir}:/root/.gemini`);
509
- } else {
510
- notes.push('未检测到 Gemini 本地配置目录(~/.gemini),已生成占位模板。');
511
- }
512
-
513
- return { keys, values, notes, volumes: dedupeList(volumes) };
514
- }
515
-
516
- function collectCodexInitData(homeDir) {
517
- const keys = [
518
- 'OPENAI_API_KEY',
519
- 'OPENAI_BASE_URL',
520
- 'OPENAI_MODEL'
521
- ];
522
- const values = {};
523
- const notes = [];
524
- const volumes = [];
525
-
526
- const codexDir = path.join(homeDir, '.codex');
527
- const authPath = path.join(codexDir, 'auth.json');
528
- const configPath = path.join(codexDir, 'config.toml');
529
- const authJson = readJsonFileSafely(authPath, 'Codex auth');
530
- const configToml = readTomlFileSafely(configPath, 'Codex TOML');
531
-
532
- keys.forEach(key => setInitValue(values, key, process.env[key]));
533
-
534
- if (authJson && typeof authJson === 'object') {
535
- setInitValue(values, 'OPENAI_API_KEY', authJson.OPENAI_API_KEY);
536
- }
537
-
538
- if (configToml && typeof configToml === 'object') {
539
- setInitValue(values, 'OPENAI_MODEL', configToml.model);
540
-
541
- let providerConfig = null;
542
- const providers = configToml.model_providers;
543
- if (providers && typeof providers === 'object') {
544
- if (typeof configToml.model_provider === 'string' && providers[configToml.model_provider]) {
545
- providerConfig = providers[configToml.model_provider];
546
- } else {
547
- const firstProviderName = Object.keys(providers)[0];
548
- if (firstProviderName) {
549
- providerConfig = providers[firstProviderName];
550
- }
551
- }
552
- }
553
- if (providerConfig && typeof providerConfig === 'object') {
554
- setInitValue(values, 'OPENAI_BASE_URL', providerConfig.base_url);
555
- }
556
- }
557
-
558
- if (fs.existsSync(codexDir)) {
559
- volumes.push(`${codexDir}:/root/.codex`);
560
- } else {
561
- notes.push('未检测到 Codex 本地配置目录(~/.codex),已生成占位模板。');
562
- }
563
-
564
- return { keys, values, notes, volumes: dedupeList(volumes) };
565
- }
566
-
567
- function collectOpenCodeInitData(homeDir) {
568
- const keys = [
569
- 'OPENAI_API_KEY',
570
- 'OPENAI_BASE_URL',
571
- 'OPENAI_MODEL'
572
- ];
573
- const values = {};
574
- const notes = [];
575
- const volumes = [];
576
-
577
- const opencodeDir = path.join(homeDir, '.config', 'opencode');
578
- const opencodePath = path.join(opencodeDir, 'opencode.json');
579
- const opencodeAuthPath = path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
580
- const opencodeJson = readJsonFileSafely(opencodePath, 'OpenCode config');
581
-
582
- keys.forEach(key => setInitValue(values, key, process.env[key]));
583
-
584
- if (opencodeJson && typeof opencodeJson === 'object') {
585
- const providers = opencodeJson.provider && typeof opencodeJson.provider === 'object'
586
- ? Object.values(opencodeJson.provider).filter(v => v && typeof v === 'object')
587
- : [];
588
- const provider = providers[0];
589
-
590
- if (provider) {
591
- const options = provider.options && typeof provider.options === 'object' ? provider.options : {};
592
- const apiKeyValue = resolveEnvPlaceholder(options.apiKey) || options.apiKey;
593
- const baseUrlValue = resolveEnvPlaceholder(options.baseURL) || options.baseURL;
594
- setInitValue(values, 'OPENAI_API_KEY', apiKeyValue);
595
- setInitValue(values, 'OPENAI_BASE_URL', baseUrlValue);
596
-
597
- if (provider.models && typeof provider.models === 'object') {
598
- const firstModelName = Object.keys(provider.models)[0];
599
- if (firstModelName) {
600
- setInitValue(values, 'OPENAI_MODEL', firstModelName);
601
- }
602
- }
603
- }
604
-
605
- if (typeof opencodeJson.model === 'string') {
606
- const modelFromEnv = resolveEnvPlaceholder(opencodeJson.model);
607
- if (modelFromEnv) {
608
- setInitValue(values, 'OPENAI_MODEL', modelFromEnv);
609
- }
610
- }
611
- }
612
-
613
- if (fs.existsSync(opencodePath)) {
614
- volumes.push(`${opencodePath}:/root/.config/opencode/opencode.json`);
615
- } else {
616
- notes.push('未检测到 OpenCode 配置文件(~/.config/opencode/opencode.json),已生成占位模板。');
617
- }
618
- if (fs.existsSync(opencodeAuthPath)) {
619
- volumes.push(`${opencodeAuthPath}:/root/.local/share/opencode/auth.json`);
620
- }
621
-
622
- return { keys, values, notes, volumes: dedupeList(volumes) };
623
- }
624
-
625
- function buildInitRunEnv(keys, values) {
626
- const envMap = {};
627
- const missingKeys = [];
628
- const unsafeKeys = [];
629
-
630
- for (const key of keys) {
631
- const value = values[key];
632
- if (isSafeInitEnvValue(value)) {
633
- envMap[key] = String(value).replace(/[\r\n\0]/g, '');
634
- } else if (value !== undefined && value !== null && String(value).trim() !== '') {
635
- envMap[key] = "";
636
- unsafeKeys.push(key);
637
- } else {
638
- envMap[key] = "";
639
- missingKeys.push(key);
640
- }
641
- }
642
- return { envMap, missingKeys, unsafeKeys };
643
- }
644
-
645
- function buildInitRunProfile(agent, yolo, volumes, keys, values) {
646
- const envBuildResult = buildInitRunEnv(keys, values);
647
- const runProfile = {
648
- containerName: `my-${agent}-{now}`,
649
- env: envBuildResult.envMap,
650
- yolo
651
- };
652
- const volumeList = dedupeList(volumes);
653
- if (volumeList.length > 0) {
654
- runProfile.volumes = volumeList;
655
- }
656
- return {
657
- runProfile,
658
- missingKeys: envBuildResult.missingKeys,
659
- unsafeKeys: envBuildResult.unsafeKeys
660
- };
661
- }
662
-
663
- async function shouldOverwriteInitRunEntry(runName, exists) {
664
- if (!exists) {
665
- return true;
666
- }
667
-
668
- if (YES_MODE) {
669
- console.log(`${YELLOW}⚠️ runs.${runName} 已存在,--yes 模式自动覆盖${NC}`);
670
- return true;
671
- }
672
-
673
- const reply = await askQuestion(`❔ runs.${runName} 已存在,是否覆盖? [y/N]: `);
674
- const firstChar = String(reply || '').trim().toLowerCase()[0];
675
- if (firstChar === 'y') {
676
- return true;
677
- }
678
- console.log(`${YELLOW}⏭️ 已保留原配置: runs.${runName}${NC}`);
679
- return false;
680
- }
681
-
682
- async function initAgentConfigs(rawAgents) {
683
- const agents = normalizeInitConfigAgents(rawAgents);
684
- const homeDir = os.homedir();
685
- const manyoyoHome = path.join(homeDir, '.manyoyo');
686
- const manyoyoConfigPath = path.join(manyoyoHome, 'manyoyo.json');
687
-
688
- fs.mkdirSync(manyoyoHome, { recursive: true });
689
-
690
- const manyoyoConfig = loadConfig();
691
- let runsMap = {};
692
- if (manyoyoConfig.runs !== undefined) {
693
- if (typeof manyoyoConfig.runs !== 'object' || manyoyoConfig.runs === null || Array.isArray(manyoyoConfig.runs)) {
694
- console.error(`${RED}⚠️ 错误: ~/.manyoyo/manyoyo.json 的 runs 必须是对象(map)${NC}`);
695
- process.exit(1);
696
- }
697
- runsMap = { ...manyoyoConfig.runs };
698
- }
699
- let hasConfigChanged = false;
700
-
701
- const extractors = {
702
- claude: collectClaudeInitData,
703
- codex: collectCodexInitData,
704
- gemini: collectGeminiInitData,
705
- opencode: collectOpenCodeInitData
706
- };
707
- const yoloMap = {
708
- claude: 'c',
709
- codex: 'cx',
710
- gemini: 'gm',
711
- opencode: 'oc'
712
- };
713
-
714
- console.log(`${CYAN}🧭 正在初始化 MANYOYO 配置: ${agents.join(', ')}${NC}`);
715
-
716
- for (const agent of agents) {
717
- const data = extractors[agent](homeDir);
718
- const shouldWriteRun = await shouldOverwriteInitRunEntry(
719
- agent,
720
- Object.prototype.hasOwnProperty.call(runsMap, agent)
721
- );
722
-
723
- let writeResult = { missingKeys: [], unsafeKeys: [] };
724
- if (shouldWriteRun) {
725
- const buildResult = buildInitRunProfile(agent, yoloMap[agent], data.volumes, data.keys, data.values);
726
- runsMap[agent] = buildResult.runProfile;
727
- writeResult = {
728
- missingKeys: buildResult.missingKeys,
729
- unsafeKeys: buildResult.unsafeKeys
730
- };
731
- hasConfigChanged = true;
732
- }
733
-
734
- if (shouldWriteRun) {
735
- console.log(`${GREEN}✅ [${agent}] 初始化完成${NC}`);
736
- } else {
737
- console.log(`${YELLOW}⚠️ [${agent}] 已跳过(配置保留)${NC}`);
738
- }
739
- console.log(` run: ${shouldWriteRun ? '已写入' : '保留'} runs.${agent}`);
740
-
741
- if (shouldWriteRun && writeResult.missingKeys.length > 0) {
742
- console.log(`${YELLOW}⚠️ [${agent}] 以下变量未找到,请手动填写:${NC} ${writeResult.missingKeys.join(', ')}`);
743
- }
744
- if (shouldWriteRun && writeResult.unsafeKeys.length > 0) {
745
- console.log(`${YELLOW}⚠️ [${agent}] 以下变量包含不安全字符,已留空 env 键:${NC} ${writeResult.unsafeKeys.join(', ')}`);
746
- }
747
- if (data.notes && data.notes.length > 0) {
748
- data.notes.forEach(note => console.log(`${YELLOW}⚠️ [${agent}] ${note}${NC}`));
749
- }
750
- }
751
-
752
- if (hasConfigChanged || !fs.existsSync(manyoyoConfigPath)) {
753
- manyoyoConfig.runs = runsMap;
754
- fs.writeFileSync(manyoyoConfigPath, `${JSON.stringify(manyoyoConfig, null, 4)}\n`);
755
- }
756
- }
757
-
758
- // ==============================================================================
759
- // SECTION: UI Functions
760
- // ==============================================================================
761
-
762
- function getHelloTip(containerName, defaultCommand) {
294
+ function getHelloTip(containerName, defaultCommand, runningCommand) {
763
295
  if ( !(QUIET.tip || QUIET.full) ) {
296
+ const resumeArg = resolveAgentResumeArg(runningCommand);
764
297
  console.log("");
765
298
  console.log(`${BLUE}----------------------------------------${NC}`);
766
299
  console.log(`📦 首次命令 : ${defaultCommand}`);
767
- console.log(`⚫ 恢复首次命令会话: ${CYAN}${MANYOYO_NAME} -n ${containerName} -- -c${NC}`);
300
+ if (resumeArg) {
301
+ console.log(`⚫ 恢复首次命令会话: ${CYAN}${MANYOYO_NAME} -n ${containerName} -- ${resumeArg}${NC}`);
302
+ }
768
303
  console.log(`⚫ 执行首次命令 : ${GREEN}${MANYOYO_NAME} -n ${containerName}${NC}`);
769
304
  console.log(`⚫ 执行指定命令 : ${GREEN}${MANYOYO_NAME} -n ${containerName} -x /bin/bash${NC}`);
770
305
  console.log(`⚫ 执行指定命令 : ${GREEN}docker exec -it ${containerName} /bin/bash${NC}`);
@@ -833,10 +368,6 @@ function isValidContainerName(value) {
833
368
  return typeof value === 'string' && SAFE_CONTAINER_NAME_PATTERN.test(value);
834
369
  }
835
370
 
836
- // ==============================================================================
837
- // SECTION: Environment Variables and Volume Handling
838
- // ==============================================================================
839
-
840
371
  async function askQuestion(prompt) {
841
372
  const rl = readline.createInterface({
842
373
  input: process.stdin,
@@ -851,10 +382,6 @@ async function askQuestion(prompt) {
851
382
  });
852
383
  }
853
384
 
854
- // ==============================================================================
855
- // Configuration Functions
856
- // ==============================================================================
857
-
858
385
  /**
859
386
  * 添加环境变量
860
387
  * @param {string} env - 环境变量字符串 (KEY=VALUE)
@@ -923,7 +450,6 @@ function addEnvFile(envFile) {
923
450
  process.exit(1);
924
451
  }
925
452
 
926
- ENV_FILE = filePath;
927
453
  if (fs.existsSync(filePath)) {
928
454
  const content = fs.readFileSync(filePath, 'utf-8');
929
455
  const lines = content.split('\n');
@@ -962,34 +488,31 @@ function addVolume(volume) {
962
488
  CONTAINER_VOLUMES.push("--volume", volume);
963
489
  }
964
490
 
965
- // ==============================================================================
966
- // SECTION: YOLO Mode and Container Mode Configuration
967
- // ==============================================================================
491
+ function addImageBuildArg(value) {
492
+ IMAGE_BUILD_ARGS.push("--build-arg", value);
493
+ }
494
+
495
+ const YOLO_COMMAND_MAP = {
496
+ claude: "IS_SANDBOX=1 claude --dangerously-skip-permissions",
497
+ cc: "IS_SANDBOX=1 claude --dangerously-skip-permissions",
498
+ c: "IS_SANDBOX=1 claude --dangerously-skip-permissions",
499
+ gemini: "gemini --yolo",
500
+ gm: "gemini --yolo",
501
+ g: "gemini --yolo",
502
+ codex: "codex --dangerously-bypass-approvals-and-sandbox",
503
+ cx: "codex --dangerously-bypass-approvals-and-sandbox",
504
+ opencode: "OPENCODE_PERMISSION='{\"*\":\"allow\"}' opencode",
505
+ oc: "OPENCODE_PERMISSION='{\"*\":\"allow\"}' opencode"
506
+ };
968
507
 
969
508
  function setYolo(cli) {
970
- switch (cli) {
971
- case 'claude':
972
- case 'cc':
973
- case 'c':
974
- EXEC_COMMAND = "IS_SANDBOX=1 claude --dangerously-skip-permissions";
975
- break;
976
- case 'gemini':
977
- case 'gm':
978
- case 'g':
979
- EXEC_COMMAND = "gemini --yolo";
980
- break;
981
- case 'codex':
982
- case 'cx':
983
- EXEC_COMMAND = "codex --dangerously-bypass-approvals-and-sandbox";
984
- break;
985
- case 'opencode':
986
- case 'oc':
987
- EXEC_COMMAND = "OPENCODE_PERMISSION='{\"*\":\"allow\"}' opencode";
988
- break;
989
- default:
990
- console.log(`${RED}⚠️ 未知LLM CLI: ${cli}${NC}`);
991
- process.exit(0);
509
+ const key = String(cli || '').trim().toLowerCase();
510
+ const mappedCommand = YOLO_COMMAND_MAP[key];
511
+ if (!mappedCommand) {
512
+ console.log(`${RED}⚠️ 未知LLM CLI: ${cli}${NC}`);
513
+ process.exit(0);
992
514
  }
515
+ EXEC_COMMAND = mappedCommand;
993
516
  }
994
517
 
995
518
  /**
@@ -997,49 +520,41 @@ function setYolo(cli) {
997
520
  * @param {string} mode - 模式名称 (common, dind, sock)
998
521
  */
999
522
  function setContMode(mode) {
1000
- switch (mode) {
1001
- case 'common':
1002
- CONT_MODE = "";
1003
- CONT_MODE_ARGS = [];
1004
- break;
1005
- case 'docker-in-docker':
1006
- case 'dind':
1007
- case 'd':
1008
- CONT_MODE = "--privileged";
1009
- CONT_MODE_ARGS = ['--privileged'];
1010
- console.log(`${GREEN}✅ 开启安全的容器嵌套容器模式, 手动在容器内启动服务: nohup dockerd &${NC}`);
1011
- break;
1012
- case 'mount-docker-socket':
1013
- case 'sock':
1014
- case 's':
1015
- CONT_MODE = "--privileged --volume /var/run/docker.sock:/var/run/docker.sock --env DOCKER_HOST=unix:///var/run/docker.sock --env CONTAINER_HOST=unix:///var/run/docker.sock";
1016
- CONT_MODE_ARGS = [
1017
- '--privileged',
1018
- '--volume', '/var/run/docker.sock:/var/run/docker.sock',
1019
- '--env', 'DOCKER_HOST=unix:///var/run/docker.sock',
1020
- '--env', 'CONTAINER_HOST=unix:///var/run/docker.sock'
1021
- ];
1022
- console.log(`${RED}⚠️ 开启危险的容器嵌套容器模式, 危害: 容器可访问宿主机文件${NC}`);
1023
- break;
1024
- default:
1025
- console.log(`${RED}⚠️ 未知模式: ${mode}${NC}`);
1026
- process.exit(0);
523
+ const modeAliasMap = {
524
+ common: 'common',
525
+ 'docker-in-docker': 'dind',
526
+ dind: 'dind',
527
+ d: 'dind',
528
+ 'mount-docker-socket': 'sock',
529
+ sock: 'sock',
530
+ s: 'sock'
531
+ };
532
+ const normalizedMode = modeAliasMap[String(mode || '').trim().toLowerCase()];
533
+
534
+ if (normalizedMode === 'common') {
535
+ CONT_MODE_ARGS = [];
536
+ return;
1027
537
  }
1028
- }
1029
538
 
1030
- // ==============================================================================
1031
- // Docker Helper Functions
1032
- // ==============================================================================
539
+ if (normalizedMode === 'dind') {
540
+ CONT_MODE_ARGS = ['--privileged'];
541
+ console.log(`${GREEN}✅ 开启安全的容器嵌套容器模式, 手动在容器内启动服务: nohup dockerd &${NC}`);
542
+ return;
543
+ }
1033
544
 
1034
- function dockerExec(cmd, options = {}) {
1035
- try {
1036
- return execSync(cmd, { encoding: 'utf-8', ...options });
1037
- } catch (e) {
1038
- if (options.ignoreError) {
1039
- return e.stdout || '';
1040
- }
1041
- throw e;
545
+ if (normalizedMode === 'sock') {
546
+ CONT_MODE_ARGS = [
547
+ '--privileged',
548
+ '--volume', '/var/run/docker.sock:/var/run/docker.sock',
549
+ '--env', 'DOCKER_HOST=unix:///var/run/docker.sock',
550
+ '--env', 'CONTAINER_HOST=unix:///var/run/docker.sock'
551
+ ];
552
+ console.log(`${RED}⚠️ 开启危险的容器嵌套容器模式, 危害: 容器可访问宿主机文件${NC}`);
553
+ return;
1042
554
  }
555
+
556
+ console.log(`${RED}⚠️ 未知模式: ${mode}${NC}`);
557
+ process.exit(0);
1043
558
  }
1044
559
 
1045
560
  function showImagePullHint(err) {
@@ -1092,10 +607,6 @@ function removeContainer(name) {
1092
607
  if ( !(QUIET.crm || QUIET.full) ) console.log(`${GREEN}✅ 已彻底删除。${NC}`);
1093
608
  }
1094
609
 
1095
- // ==============================================================================
1096
- // SECTION: Docker Operations
1097
- // ==============================================================================
1098
-
1099
610
  function ensureDocker() {
1100
611
  const commands = ['docker', 'podman'];
1101
612
  for (const cmd of commands) {
@@ -1163,279 +674,34 @@ function pruneDanglingImages() {
1163
674
  console.log(`${GREEN}✅ 清理完成${NC}`);
1164
675
  }
1165
676
 
1166
- // ==============================================================================
1167
- // SECTION: Image Build System
1168
- // ==============================================================================
1169
-
1170
- /**
1171
- * 准备构建缓存(Node.js、JDT LSP、gopls)
1172
- * @param {string} imageTool - 构建工具类型
1173
- */
1174
- async function prepareBuildCache(imageTool) {
1175
- const cacheDir = path.join(__dirname, '../docker/cache');
1176
- const timestampFile = path.join(cacheDir, '.timestamps.json');
1177
-
1178
- // 从配置文件读取 TTL,默认 2 天
1179
- const config = loadConfig();
1180
- const cacheTTLDays = config.cacheTTL || CONFIG.CACHE_TTL_DAYS;
1181
-
1182
- // 镜像源优先级:用户配置 > 腾讯云 > 官方
1183
- const nodeMirrors = [
1184
- config.nodeMirror,
1185
- 'https://mirrors.tencent.com/nodejs-release',
1186
- 'https://nodejs.org/dist'
1187
- ].filter(Boolean);
1188
-
1189
- console.log(`\n${CYAN}准备构建缓存...${NC}`);
1190
-
1191
- // Create cache directory
1192
- if (!fs.existsSync(cacheDir)) {
1193
- fs.mkdirSync(cacheDir, { recursive: true });
1194
- }
1195
-
1196
- // Load timestamps
1197
- let timestamps = {};
1198
- if (fs.existsSync(timestampFile)) {
1199
- try {
1200
- timestamps = JSON.parse(fs.readFileSync(timestampFile, 'utf-8'));
1201
- } catch (e) {
1202
- timestamps = {};
1203
- }
1204
- }
1205
-
1206
- const now = new Date();
1207
- const isExpired = (key) => {
1208
- if (!timestamps[key]) return true;
1209
- const cachedTime = new Date(timestamps[key]);
1210
- const diffDays = (now - cachedTime) / (1000 * 60 * 60 * 24);
1211
- return diffDays > cacheTTLDays;
1212
- };
1213
-
1214
- // Determine architecture
1215
- const arch = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? 'arm64' : process.arch;
1216
- const archNode = arch === 'amd64' ? 'x64' : 'arm64';
1217
-
1218
- // Prepare Node.js cache
1219
- const nodeCacheDir = path.join(cacheDir, 'node');
1220
- const nodeVersion = 24;
1221
- const nodeKey = 'node/'; // 使用目录级别的相对路径
1222
-
1223
- if (!fs.existsSync(nodeCacheDir)) {
1224
- fs.mkdirSync(nodeCacheDir, { recursive: true });
1225
- }
1226
-
1227
- const hasNodeCache = fs.existsSync(nodeCacheDir) && fs.readdirSync(nodeCacheDir).some(f => f.startsWith('node-') && f.includes(`linux-${archNode}`));
1228
- if (!hasNodeCache || isExpired(nodeKey)) {
1229
- console.log(`${YELLOW}下载 Node.js ${nodeVersion} (${archNode})...${NC}`);
1230
-
1231
- // 尝试多个镜像源
1232
- let downloadSuccess = false;
1233
- for (const mirror of nodeMirrors) {
1234
- try {
1235
- console.log(`${BLUE}尝试镜像源: ${mirror}${NC}`);
1236
- const shasumUrl = `${mirror}/latest-v${nodeVersion}.x/SHASUMS256.txt`;
1237
- const shasumContent = execSync(`curl -sL ${shasumUrl}`, { encoding: 'utf-8' });
1238
- const shasumLine = shasumContent.split('\n').find(line => line.includes(`linux-${archNode}.tar.gz`));
1239
- if (!shasumLine) continue;
1240
-
1241
- const [expectedHash, fileName] = shasumLine.trim().split(/\s+/);
1242
- const nodeUrl = `${mirror}/latest-v${nodeVersion}.x/${fileName}`;
1243
- const nodeTargetPath = path.join(nodeCacheDir, fileName);
1244
-
1245
- // 下载文件
1246
- runCmd('curl', ['-fsSL', nodeUrl, '-o', nodeTargetPath], { stdio: 'inherit' });
1247
-
1248
- // SHA256 校验(使用 Node.js crypto 模块,跨平台)
1249
- const actualHash = getFileSha256(nodeTargetPath);
1250
- if (actualHash !== expectedHash) {
1251
- console.log(`${RED}SHA256 校验失败,删除文件${NC}`);
1252
- fs.unlinkSync(nodeTargetPath);
1253
- continue;
1254
- }
1255
-
1256
- console.log(`${GREEN}✓ SHA256 校验通过${NC}`);
1257
- timestamps[nodeKey] = now.toISOString();
1258
- fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
1259
- console.log(`${GREEN}✓ Node.js 下载完成${NC}`);
1260
- downloadSuccess = true;
1261
- break;
1262
- } catch (e) {
1263
- console.log(`${YELLOW}镜像源 ${mirror} 失败,尝试下一个...${NC}`);
1264
- }
1265
- }
1266
-
1267
- if (!downloadSuccess) {
1268
- console.error(`${RED}错误: Node.js 下载失败(所有镜像源均不可用)${NC}`);
1269
- throw new Error('Node.js download failed');
1270
- }
1271
- } else {
1272
- console.log(`${GREEN}✓ Node.js 缓存已存在${NC}`);
1273
- }
1274
-
1275
- // Prepare JDT LSP cache (for java variant)
1276
- if (imageTool === 'full' || imageTool.includes('java')) {
1277
- const jdtlsCacheDir = path.join(cacheDir, 'jdtls');
1278
- const jdtlsKey = 'jdtls/jdt-language-server-latest.tar.gz'; // 使用相对路径
1279
- const jdtlsPath = path.join(cacheDir, jdtlsKey);
1280
-
1281
- if (!fs.existsSync(jdtlsCacheDir)) {
1282
- fs.mkdirSync(jdtlsCacheDir, { recursive: true });
1283
- }
1284
-
1285
- if (!fs.existsSync(jdtlsPath) || isExpired(jdtlsKey)) {
1286
- console.log(`${YELLOW}下载 JDT Language Server...${NC}`);
1287
- const apkUrl = 'https://mirrors.tencent.com/alpine/latest-stable/community/x86_64/jdtls-1.53.0-r0.apk';
1288
- const tmpDir = path.join(jdtlsCacheDir, '.tmp-apk');
1289
- const apkPath = path.join(tmpDir, 'jdtls.apk');
1290
- try {
1291
- fs.mkdirSync(tmpDir, { recursive: true });
1292
- runCmd('curl', ['-fsSL', apkUrl, '-o', apkPath], { stdio: 'inherit' });
1293
- runCmd('tar', ['-xzf', apkPath, '-C', tmpDir], { stdio: 'inherit' });
1294
- const srcDir = path.join(tmpDir, 'usr', 'share', 'jdtls');
1295
- runCmd('tar', ['-czf', jdtlsPath, '-C', srcDir, '.'], { stdio: 'inherit' });
1296
- timestamps[jdtlsKey] = now.toISOString();
1297
- fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
1298
- console.log(`${GREEN}✓ JDT LSP 下载完成${NC}`);
1299
- } catch (e) {
1300
- console.error(`${RED}错误: JDT LSP 下载失败${NC}`);
1301
- throw e;
1302
- } finally {
1303
- try { runCmd('rm', ['-rf', tmpDir], { stdio: 'inherit', ignoreError: true }); } catch {}
1304
- }
1305
- } else {
1306
- console.log(`${GREEN}✓ JDT LSP 缓存已存在${NC}`);
1307
- }
1308
- }
1309
-
1310
- // Prepare gopls cache (for go variant)
1311
- if (imageTool === 'full' || imageTool.includes('go')) {
1312
- const goplsCacheDir = path.join(cacheDir, 'gopls');
1313
- const goplsKey = `gopls/gopls-linux-${arch}`; // 使用相对路径
1314
- const goplsPath = path.join(cacheDir, goplsKey);
1315
-
1316
- if (!fs.existsSync(goplsCacheDir)) {
1317
- fs.mkdirSync(goplsCacheDir, { recursive: true });
1318
- }
1319
-
1320
- if (!fs.existsSync(goplsPath) || isExpired(goplsKey)) {
1321
- console.log(`${YELLOW}下载 gopls (${arch})...${NC}`);
1322
- try {
1323
- // Download using go install in temporary environment
1324
- const tmpGoPath = path.join(cacheDir, '.tmp-go');
1325
-
1326
- // Clean up existing temp directory (with go clean for mod cache)
1327
- if (fs.existsSync(tmpGoPath)) {
1328
- try {
1329
- execSync(`GOPATH="${tmpGoPath}" go clean -modcache 2>/dev/null || true`, { stdio: 'inherit' });
1330
- execSync(`chmod -R u+w "${tmpGoPath}" 2>/dev/null || true`, { stdio: 'inherit' });
1331
- execSync(`rm -rf "${tmpGoPath}"`, { stdio: 'inherit' });
1332
- } catch (e) {
1333
- // Ignore cleanup errors
1334
- }
1335
- }
1336
- fs.mkdirSync(tmpGoPath, { recursive: true });
1337
-
1338
- runCmd('go', ['install', 'golang.org/x/tools/gopls@latest'], {
1339
- stdio: 'inherit',
1340
- env: { ...process.env, GOPATH: tmpGoPath, GOOS: 'linux', GOARCH: arch }
1341
- });
1342
- execSync(`cp "${tmpGoPath}/bin/linux_${arch}/gopls" "${goplsPath}" || cp "${tmpGoPath}/bin/gopls" "${goplsPath}"`, { stdio: 'inherit' });
1343
- runCmd('chmod', ['+x', goplsPath], { stdio: 'inherit' });
1344
-
1345
- // Save timestamp immediately after successful download
1346
- timestamps[goplsKey] = now.toISOString();
1347
- fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
1348
- console.log(`${GREEN}✓ gopls 下载完成${NC}`);
1349
-
1350
- // Clean up temp directory (with go clean for mod cache)
1351
- try {
1352
- execSync(`GOPATH="${tmpGoPath}" go clean -modcache 2>/dev/null || true`, { stdio: 'inherit' });
1353
- execSync(`chmod -R u+w "${tmpGoPath}" 2>/dev/null || true`, { stdio: 'inherit' });
1354
- execSync(`rm -rf "${tmpGoPath}"`, { stdio: 'inherit' });
1355
- } catch (e) {
1356
- console.log(`${YELLOW}提示: 临时目录清理失败,可手动删除 ${tmpGoPath}${NC}`);
1357
- }
1358
- } catch (e) {
1359
- console.error(`${RED}错误: gopls 下载失败${NC}`);
1360
- throw e;
1361
- }
1362
- } else {
1363
- console.log(`${GREEN}✓ gopls 缓存已存在${NC}`);
1364
- }
677
+ function maybeHandleDockerPluginMetadata(argv) {
678
+ if (argv[2] !== 'docker-cli-plugin-metadata') {
679
+ return false;
1365
680
  }
1366
-
1367
- // Save timestamps
1368
- fs.writeFileSync(timestampFile, JSON.stringify(timestamps, null, 4));
1369
- console.log(`${GREEN}✅ 构建缓存准备完成${NC}\n`);
1370
- }
1371
-
1372
- function addImageBuildArg(string) {
1373
- IMAGE_BUILD_ARGS.push("--build-arg", string);
681
+ console.log(JSON.stringify({
682
+ "SchemaVersion": "0.1.0",
683
+ "Vendor": "xcanwin",
684
+ "Version": "v1.0.0",
685
+ "Description": "AI Agent CLI Sandbox"
686
+ }, null, 4));
687
+ return true;
1374
688
  }
1375
689
 
1376
- async function buildImage(IMAGE_BUILD_ARGS, imageName, imageVersionTag) {
1377
- const versionTag = imageVersionTag || IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
1378
- const parsedVersion = parseImageVersionTag(versionTag);
1379
- if (!parsedVersion) {
1380
- console.error(`${RED}错误: 镜像版本格式错误,必须为 <x.y.z-后缀>,例如 1.7.4-common: ${versionTag}${NC}`);
1381
- process.exit(1);
1382
- }
1383
-
1384
- const version = parsedVersion.baseVersion;
1385
- let imageTool = parsedVersion.tool;
1386
- const toolFromArgs = IMAGE_BUILD_ARGS.filter(v => v.startsWith("TOOL=")).at(-1)?.slice("TOOL=".length);
1387
-
1388
- if (!toolFromArgs) {
1389
- IMAGE_BUILD_ARGS = [...IMAGE_BUILD_ARGS, "--build-arg", `TOOL=${imageTool}`];
1390
- } else {
1391
- imageTool = toolFromArgs;
1392
- }
1393
-
1394
- const fullImageTag = `${imageName}:${version}-${imageTool}`;
1395
-
1396
- console.log(`${CYAN}🔨 正在构建镜像: ${YELLOW}${fullImageTag}${NC}`);
1397
- console.log(`${BLUE}构建组件类型: ${imageTool}${NC}\n`);
1398
-
1399
- // Prepare cache (自动检测并下载缺失的文件)
1400
- await prepareBuildCache(imageTool);
1401
-
1402
- // Find Dockerfile path
1403
- const dockerfilePath = path.join(__dirname, '../docker/manyoyo.Dockerfile');
1404
- if (!fs.existsSync(dockerfilePath)) {
1405
- console.error(`${RED}错误: 找不到 Dockerfile: ${dockerfilePath}${NC}`);
1406
- process.exit(1);
1407
- }
1408
-
1409
- // Build command
1410
- const imageBuildArgs = IMAGE_BUILD_ARGS.join(' ');
1411
- const buildCmd = `${DOCKER_CMD} build -t "${fullImageTag}" -f "${dockerfilePath}" "${path.join(__dirname, '..')}" ${imageBuildArgs} --load --progress=plain --no-cache`;
1412
-
1413
- console.log(`${BLUE}准备执行命令:${NC}`);
1414
- console.log(`${buildCmd}\n`);
1415
-
1416
- if (!YES_MODE) {
1417
- await askQuestion(`❔ 是否继续构建? [ 直接回车=继续, ctrl+c=取消 ]: `);
1418
- console.log("");
690
+ function normalizeDockerPluginArgv(argv) {
691
+ const dockerPluginPath = path.join(process.env.HOME || '', '.docker/cli-plugins/docker-manyoyo');
692
+ if (argv[1] === dockerPluginPath && argv[2] === 'manyoyo') {
693
+ argv.splice(2, 1);
1419
694
  }
695
+ }
1420
696
 
1421
- try {
1422
- execSync(buildCmd, { stdio: 'inherit' });
1423
- console.log(`\n${GREEN}✅ 镜像构建成功: ${fullImageTag}${NC}`);
1424
- console.log(`${BLUE}使用镜像:${NC}`);
1425
- console.log(` ${MANYOYO_NAME} -n test --in ${imageName} --iv ${version}-${imageTool} -y c`);
1426
-
1427
- // Prune dangling images
1428
- pruneDanglingImages();
1429
- } catch (e) {
1430
- console.error(`${RED}错误: 镜像构建失败${NC}`);
1431
- process.exit(1);
697
+ function normalizeShellFullArgv(argv) {
698
+ const shellFullIndex = argv.findIndex(arg => arg === '-x' || arg === '--shell-full');
699
+ if (shellFullIndex !== -1 && shellFullIndex < argv.length - 1) {
700
+ const shellFullArgs = argv.slice(shellFullIndex + 1).join(' ');
701
+ argv.splice(shellFullIndex + 1, argv.length - (shellFullIndex + 1), shellFullArgs);
1432
702
  }
1433
703
  }
1434
704
 
1435
- // ==============================================================================
1436
- // SECTION: Command Line Interface
1437
- // ==============================================================================
1438
-
1439
705
  async function setupCommander() {
1440
706
  // Load config file
1441
707
  const config = loadConfig();
@@ -1505,21 +771,12 @@ async function setupCommander() {
1505
771
  .option('-q, --quiet <item>', '静默显示 (可多次使用: cnew,crm,tip,cmd,full)', (value, previous) => [...(previous || []), value], []);
1506
772
 
1507
773
  // Docker CLI plugin metadata check
1508
- if (process.argv[2] === 'docker-cli-plugin-metadata') {
1509
- console.log(JSON.stringify({
1510
- "SchemaVersion": "0.1.0",
1511
- "Vendor": "xcanwin",
1512
- "Version": "v1.0.0",
1513
- "Description": "AI Agent CLI Sandbox"
1514
- }, null, 4));
774
+ if (maybeHandleDockerPluginMetadata(process.argv)) {
1515
775
  process.exit(0);
1516
776
  }
1517
777
 
1518
778
  // Docker CLI plugin mode - remove first arg if running as plugin
1519
- const dockerPluginPath = path.join(process.env.HOME || '', '.docker/cli-plugins/docker-manyoyo');
1520
- if (process.argv[1] === dockerPluginPath && process.argv[2] === 'manyoyo') {
1521
- process.argv.splice(2, 1);
1522
- }
779
+ normalizeDockerPluginArgv(process.argv);
1523
780
 
1524
781
  // No args: show help instead of starting container
1525
782
  if (process.argv.length <= 2) {
@@ -1534,11 +791,7 @@ async function setupCommander() {
1534
791
  }
1535
792
 
1536
793
  // Pre-handle -x/--shell-full: treat all following args as a single command
1537
- const shellFullIndex = process.argv.findIndex(arg => arg === '-x' || arg === '--shell-full');
1538
- if (shellFullIndex !== -1 && shellFullIndex < process.argv.length - 1) {
1539
- const shellFullArgs = process.argv.slice(shellFullIndex + 1).join(' ');
1540
- process.argv.splice(shellFullIndex + 1, process.argv.length - (shellFullIndex + 1), shellFullArgs);
1541
- }
794
+ normalizeShellFullArgv(process.argv);
1542
795
 
1543
796
  // Parse arguments
1544
797
  program.allowUnknownOption(false);
@@ -1551,7 +804,13 @@ async function setupCommander() {
1551
804
  }
1552
805
 
1553
806
  if (options.initConfig !== undefined) {
1554
- await initAgentConfigs(options.initConfig);
807
+ await initAgentConfigs(options.initConfig, {
808
+ yesMode: YES_MODE,
809
+ askQuestion,
810
+ loadConfig,
811
+ supportedAgents: SUPPORTED_INIT_AGENTS,
812
+ colors: { RED, GREEN, YELLOW, CYAN, NC }
813
+ });
1555
814
  process.exit(0);
1556
815
  }
1557
816
 
@@ -1560,26 +819,32 @@ async function setupCommander() {
1560
819
 
1561
820
  // Merge configs: command line > run config > global config > defaults
1562
821
  // Override mode (scalar values): use first defined value
1563
- HOST_PATH = options.hostPath || runConfig.hostPath || config.hostPath || HOST_PATH;
1564
- if (options.contName || runConfig.containerName || config.containerName) {
1565
- CONTAINER_NAME = options.contName || runConfig.containerName || config.containerName;
822
+ HOST_PATH = pickConfigValue(options.hostPath, runConfig.hostPath, config.hostPath, HOST_PATH) || HOST_PATH;
823
+ const mergedContainerName = pickConfigValue(options.contName, runConfig.containerName, config.containerName);
824
+ if (mergedContainerName) {
825
+ CONTAINER_NAME = mergedContainerName;
1566
826
  }
1567
827
  CONTAINER_NAME = resolveContainerNameTemplate(CONTAINER_NAME);
1568
- if (options.contPath || runConfig.containerPath || config.containerPath) {
1569
- CONTAINER_PATH = options.contPath || runConfig.containerPath || config.containerPath;
828
+ const mergedContainerPath = pickConfigValue(options.contPath, runConfig.containerPath, config.containerPath);
829
+ if (mergedContainerPath) {
830
+ CONTAINER_PATH = mergedContainerPath;
1570
831
  }
1571
- IMAGE_NAME = options.imageName || runConfig.imageName || config.imageName || IMAGE_NAME;
1572
- if (options.imageVer || runConfig.imageVersion || config.imageVersion) {
1573
- IMAGE_VERSION = options.imageVer || runConfig.imageVersion || config.imageVersion;
832
+ IMAGE_NAME = pickConfigValue(options.imageName, runConfig.imageName, config.imageName, IMAGE_NAME) || IMAGE_NAME;
833
+ const mergedImageVersion = pickConfigValue(options.imageVer, runConfig.imageVersion, config.imageVersion);
834
+ if (mergedImageVersion) {
835
+ IMAGE_VERSION = mergedImageVersion;
1574
836
  }
1575
- if (options.shellPrefix || runConfig.shellPrefix || config.shellPrefix) {
1576
- EXEC_COMMAND_PREFIX = (options.shellPrefix || runConfig.shellPrefix || config.shellPrefix) + " ";
837
+ const mergedShellPrefix = pickConfigValue(options.shellPrefix, runConfig.shellPrefix, config.shellPrefix);
838
+ if (mergedShellPrefix) {
839
+ EXEC_COMMAND_PREFIX = `${mergedShellPrefix} `;
1577
840
  }
1578
- if (options.shell || runConfig.shell || config.shell) {
1579
- EXEC_COMMAND = options.shell || runConfig.shell || config.shell;
841
+ const mergedShell = pickConfigValue(options.shell, runConfig.shell, config.shell);
842
+ if (mergedShell) {
843
+ EXEC_COMMAND = mergedShell;
1580
844
  }
1581
- if (options.shellSuffix || runConfig.shellSuffix || config.shellSuffix) {
1582
- EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(options.shellSuffix || runConfig.shellSuffix || config.shellSuffix);
845
+ const mergedShellSuffix = pickConfigValue(options.shellSuffix, runConfig.shellSuffix, config.shellSuffix);
846
+ if (mergedShellSuffix) {
847
+ EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedShellSuffix);
1583
848
  }
1584
849
 
1585
850
  // Basic name validation to reduce injection risk
@@ -1604,20 +869,20 @@ async function setupCommander() {
1604
869
  };
1605
870
  Object.entries(envMap).forEach(([key, value]) => addEnv(`${key}=${value}`));
1606
871
 
1607
- const volumeList = [...(config.volumes || []), ...(runConfig.volumes || []), ...(options.volume || [])];
872
+ const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
1608
873
  volumeList.forEach(v => addVolume(v));
1609
874
 
1610
- const buildArgList = [...(config.imageBuildArgs || []), ...(runConfig.imageBuildArgs || []), ...(options.imageBuildArg || [])];
875
+ const buildArgList = mergeArrayConfig(config.imageBuildArgs, runConfig.imageBuildArgs, options.imageBuildArg);
1611
876
  buildArgList.forEach(arg => addImageBuildArg(arg));
1612
877
 
1613
878
  // Override mode for special options
1614
- const yoloValue = options.yolo || runConfig.yolo || config.yolo;
879
+ const yoloValue = pickConfigValue(options.yolo, runConfig.yolo, config.yolo);
1615
880
  if (yoloValue) setYolo(yoloValue);
1616
881
 
1617
- const contModeValue = options.contMode || runConfig.containerMode || config.containerMode;
882
+ const contModeValue = pickConfigValue(options.contMode, runConfig.containerMode, config.containerMode);
1618
883
  if (contModeValue) setContMode(contModeValue);
1619
884
 
1620
- const quietValue = options.quiet || runConfig.quiet || config.quiet;
885
+ const quietValue = pickConfigValue(options.quiet, runConfig.quiet, config.quiet);
1621
886
  if (quietValue) setQuiet(quietValue);
1622
887
 
1623
888
  // Handle shell-full (variadic arguments)
@@ -1635,10 +900,6 @@ async function setupCommander() {
1635
900
  }
1636
901
  }
1637
902
 
1638
- if (options.yes) {
1639
- YES_MODE = true;
1640
- }
1641
-
1642
903
  if (options.rmOnExit) {
1643
904
  RM_ON_EXIT = true;
1644
905
  }
@@ -1650,12 +911,12 @@ async function setupCommander() {
1650
911
  SERVER_PORT = serverListen.port;
1651
912
  }
1652
913
 
1653
- const serverUserValue = options.serverUser || runConfig.serverUser || config.serverUser || process.env.MANYOYO_SERVER_USER;
914
+ const serverUserValue = pickConfigValue(options.serverUser, runConfig.serverUser, config.serverUser, process.env.MANYOYO_SERVER_USER);
1654
915
  if (serverUserValue) {
1655
916
  SERVER_AUTH_USER = String(serverUserValue);
1656
917
  }
1657
918
 
1658
- const serverPassValue = options.serverPass || runConfig.serverPass || config.serverPass || process.env.MANYOYO_SERVER_PASS;
919
+ const serverPassValue = pickConfigValue(options.serverPass, runConfig.serverPass, config.serverPass, process.env.MANYOYO_SERVER_PASS);
1659
920
  if (serverPassValue) {
1660
921
  SERVER_AUTH_PASS = String(serverPassValue);
1661
922
  SERVER_AUTH_PASS_AUTO = false;
@@ -1712,27 +973,54 @@ async function setupCommander() {
1712
973
  return program;
1713
974
  }
1714
975
 
1715
- function handleRemoveContainer() {
1716
- if (SHOULD_REMOVE) {
1717
- try {
1718
- if (containerExists(CONTAINER_NAME)) {
1719
- removeContainer(CONTAINER_NAME);
1720
- } else {
1721
- console.log(`${RED}⚠️ 错误: 未找到名为 ${CONTAINER_NAME} 的容器。${NC}`);
1722
- }
1723
- } catch (e) {
1724
- console.log(`${RED}⚠️ 错误: 未找到名为 ${CONTAINER_NAME} 的容器。${NC}`);
976
+ function createRuntimeContext() {
977
+ return {
978
+ containerName: CONTAINER_NAME,
979
+ hostPath: HOST_PATH,
980
+ containerPath: CONTAINER_PATH,
981
+ imageName: IMAGE_NAME,
982
+ imageVersion: IMAGE_VERSION,
983
+ execCommand: EXEC_COMMAND,
984
+ execCommandPrefix: EXEC_COMMAND_PREFIX,
985
+ execCommandSuffix: EXEC_COMMAND_SUFFIX,
986
+ contModeArgs: CONT_MODE_ARGS,
987
+ containerEnvs: CONTAINER_ENVS,
988
+ containerVolumes: CONTAINER_VOLUMES,
989
+ quiet: QUIET,
990
+ showCommand: SHOW_COMMAND,
991
+ rmOnExit: RM_ON_EXIT,
992
+ serverMode: SERVER_MODE,
993
+ serverHost: SERVER_HOST,
994
+ serverPort: SERVER_PORT,
995
+ serverAuthUser: SERVER_AUTH_USER,
996
+ serverAuthPass: SERVER_AUTH_PASS,
997
+ serverAuthPassAuto: SERVER_AUTH_PASS_AUTO
998
+ };
999
+ }
1000
+
1001
+ function handleRemoveContainer(runtime) {
1002
+ if (!SHOULD_REMOVE) {
1003
+ return false;
1004
+ }
1005
+
1006
+ try {
1007
+ if (containerExists(runtime.containerName)) {
1008
+ removeContainer(runtime.containerName);
1009
+ } else {
1010
+ console.log(`${RED}⚠️ 错误: 未找到名为 ${runtime.containerName} 的容器。${NC}`);
1725
1011
  }
1726
- process.exit(0);
1012
+ } catch (e) {
1013
+ console.log(`${RED}⚠️ 错误: 未找到名为 ${runtime.containerName} 的容器。${NC}`);
1727
1014
  }
1015
+ return true;
1728
1016
  }
1729
1017
 
1730
- function validateHostPath() {
1731
- if (!fs.existsSync(HOST_PATH)) {
1732
- console.log(`${RED}⚠️ 错误: 宿主机路径不存在: ${HOST_PATH}${NC}`);
1018
+ function validateHostPath(runtime) {
1019
+ if (!fs.existsSync(runtime.hostPath)) {
1020
+ console.log(`${RED}⚠️ 错误: 宿主机路径不存在: ${runtime.hostPath}${NC}`);
1733
1021
  process.exit(1);
1734
1022
  }
1735
- const realHostPath = fs.realpathSync(HOST_PATH);
1023
+ const realHostPath = fs.realpathSync(runtime.hostPath);
1736
1024
  const homeDir = process.env.HOME || '/home';
1737
1025
  if (realHostPath === '/' || realHostPath === '/home' || realHostPath === homeDir) {
1738
1026
  console.log(`${RED}⚠️ 错误: 不允许挂载根目录或home目录。${NC}`);
@@ -1774,28 +1062,34 @@ async function waitForContainerReady(containerName) {
1774
1062
  process.exit(1);
1775
1063
  }
1776
1064
 
1777
- // ==============================================================================
1778
- // SECTION: Container Lifecycle Management
1779
- // ==============================================================================
1065
+ function joinExecCommand(prefix, command, suffix) {
1066
+ return `${prefix || ''}${command || ''}${suffix || ''}`;
1067
+ }
1780
1068
 
1781
1069
  /**
1782
1070
  * 创建新容器
1783
1071
  * @returns {Promise<string>} 默认命令
1784
1072
  */
1785
- async function createNewContainer() {
1786
- if ( !(QUIET.cnew || QUIET.full) ) console.log(`${CYAN}📦 manyoyo by xcanwin 正在创建新容器: ${YELLOW}${CONTAINER_NAME}${NC}`);
1073
+ async function createNewContainer(runtime) {
1074
+ if (!(runtime.quiet.cnew || runtime.quiet.full)) {
1075
+ console.log(`${CYAN}📦 manyoyo by xcanwin 正在创建新容器: ${YELLOW}${runtime.containerName}${NC}`);
1076
+ }
1787
1077
 
1788
- EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
1789
- const defaultCommand = EXEC_COMMAND;
1078
+ runtime.execCommand = joinExecCommand(
1079
+ runtime.execCommandPrefix,
1080
+ runtime.execCommand,
1081
+ runtime.execCommandSuffix
1082
+ );
1083
+ const defaultCommand = runtime.execCommand;
1790
1084
 
1791
- if (SHOW_COMMAND) {
1792
- console.log(buildDockerRunCmd());
1085
+ if (runtime.showCommand) {
1086
+ console.log(buildDockerRunCmd(runtime));
1793
1087
  process.exit(0);
1794
1088
  }
1795
1089
 
1796
1090
  // 使用数组参数执行命令(安全方式)
1797
1091
  try {
1798
- const args = buildDockerRunArgs();
1092
+ const args = buildDockerRunArgs(runtime);
1799
1093
  dockerExecArgs(args, { stdio: 'pipe' });
1800
1094
  } catch (e) {
1801
1095
  showImagePullHint(e);
@@ -1803,7 +1097,7 @@ async function createNewContainer() {
1803
1097
  }
1804
1098
 
1805
1099
  // Wait for container to be ready
1806
- await waitForContainerReady(CONTAINER_NAME);
1100
+ await waitForContainerReady(runtime.containerName);
1807
1101
 
1808
1102
  return defaultCommand;
1809
1103
  }
@@ -1812,97 +1106,94 @@ async function createNewContainer() {
1812
1106
  * 构建 Docker run 命令参数数组(安全方式,避免命令注入)
1813
1107
  * @returns {string[]} 命令参数数组
1814
1108
  */
1815
- function buildDockerRunArgs() {
1816
- const fullImage = `${IMAGE_NAME}:${IMAGE_VERSION}`;
1817
- const safeLabelCmd = EXEC_COMMAND.replace(/[\r\n]/g, ' ');
1818
-
1819
- const args = [
1820
- 'run', '-d',
1821
- '--name', CONTAINER_NAME,
1822
- '--entrypoint', '',
1823
- ...CONT_MODE_ARGS,
1824
- ...CONTAINER_ENVS,
1825
- ...CONTAINER_VOLUMES,
1826
- '--volume', `${HOST_PATH}:${CONTAINER_PATH}`,
1827
- '--workdir', CONTAINER_PATH,
1828
- '--label', `manyoyo.default_cmd=${safeLabelCmd}`,
1829
- fullImage,
1830
- 'tail', '-f', '/dev/null'
1831
- ];
1832
-
1833
- return args;
1109
+ function buildDockerRunArgs(runtime) {
1110
+ return buildContainerRunArgs({
1111
+ containerName: runtime.containerName,
1112
+ hostPath: runtime.hostPath,
1113
+ containerPath: runtime.containerPath,
1114
+ imageName: runtime.imageName,
1115
+ imageVersion: runtime.imageVersion,
1116
+ contModeArgs: runtime.contModeArgs,
1117
+ containerEnvs: runtime.containerEnvs,
1118
+ containerVolumes: runtime.containerVolumes,
1119
+ defaultCommand: runtime.execCommand
1120
+ });
1834
1121
  }
1835
1122
 
1836
1123
  /**
1837
1124
  * 构建 Docker run 命令字符串(用于显示)
1838
1125
  * @returns {string} 命令字符串
1839
1126
  */
1840
- function buildDockerRunCmd() {
1841
- const args = buildDockerRunArgs();
1842
- // 对包含空格或特殊字符的参数加引号
1843
- const quotedArgs = args.map(arg => {
1844
- if (arg.includes(' ') || arg.includes('"') || arg.includes('=')) {
1845
- return `"${arg.replace(/"/g, '\\"')}"`;
1846
- }
1847
- return arg;
1848
- });
1849
- return `${DOCKER_CMD} ${quotedArgs.join(' ')}`;
1127
+ function buildDockerRunCmd(runtime) {
1128
+ const args = buildDockerRunArgs(runtime);
1129
+ return buildContainerRunCommand(DOCKER_CMD, args);
1850
1130
  }
1851
1131
 
1852
- async function connectExistingContainer() {
1853
- if ( !(QUIET.cnew || QUIET.full) ) console.log(`${CYAN}🔄 manyoyo by xcanwin 正在连接到现有容器: ${YELLOW}${CONTAINER_NAME}${NC}`);
1132
+ async function connectExistingContainer(runtime) {
1133
+ if (!(runtime.quiet.cnew || runtime.quiet.full)) {
1134
+ console.log(`${CYAN}🔄 manyoyo by xcanwin 正在连接到现有容器: ${YELLOW}${runtime.containerName}${NC}`);
1135
+ }
1854
1136
 
1855
1137
  // Start container if stopped
1856
- const status = getContainerStatus(CONTAINER_NAME);
1138
+ const status = getContainerStatus(runtime.containerName);
1857
1139
  if (status !== 'running') {
1858
- dockerExecArgs(['start', CONTAINER_NAME], { stdio: 'pipe' });
1140
+ dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
1859
1141
  }
1860
1142
 
1861
1143
  // Get default command from label
1862
- const defaultCommand = dockerExecArgs(['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', CONTAINER_NAME]).trim();
1144
+ const defaultCommand = dockerExecArgs(['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', runtime.containerName]).trim();
1863
1145
 
1864
- if (!EXEC_COMMAND) {
1865
- EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${defaultCommand}${EXEC_COMMAND_SUFFIX}`;
1146
+ if (!runtime.execCommand) {
1147
+ runtime.execCommand = joinExecCommand(runtime.execCommandPrefix, defaultCommand, runtime.execCommandSuffix);
1866
1148
  } else {
1867
- EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
1149
+ runtime.execCommand = joinExecCommand(runtime.execCommandPrefix, runtime.execCommand, runtime.execCommandSuffix);
1868
1150
  }
1869
1151
 
1870
1152
  return defaultCommand;
1871
1153
  }
1872
1154
 
1873
- async function setupContainer() {
1874
- if (SHOW_COMMAND) {
1875
- if (containerExists(CONTAINER_NAME)) {
1876
- const defaultCommand = dockerExecArgs(['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', CONTAINER_NAME]).trim();
1877
- const execCmd = EXEC_COMMAND
1878
- ? `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`
1879
- : `${EXEC_COMMAND_PREFIX}${defaultCommand}${EXEC_COMMAND_SUFFIX}`;
1880
- console.log(`${DOCKER_CMD} exec -it ${CONTAINER_NAME} /bin/bash -c "${execCmd.replace(/"/g, '\\"')}"`);
1155
+ async function setupContainer(runtime) {
1156
+ if (runtime.showCommand) {
1157
+ if (containerExists(runtime.containerName)) {
1158
+ const defaultCommand = dockerExecArgs(['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', runtime.containerName]).trim();
1159
+ const execCmd = runtime.execCommand
1160
+ ? joinExecCommand(runtime.execCommandPrefix, runtime.execCommand, runtime.execCommandSuffix)
1161
+ : joinExecCommand(runtime.execCommandPrefix, defaultCommand, runtime.execCommandSuffix);
1162
+ console.log(`${DOCKER_CMD} exec -it ${runtime.containerName} /bin/bash -c "${execCmd.replace(/"/g, '\\"')}"`);
1881
1163
  process.exit(0);
1882
1164
  }
1883
- EXEC_COMMAND = `${EXEC_COMMAND_PREFIX}${EXEC_COMMAND}${EXEC_COMMAND_SUFFIX}`;
1884
- console.log(buildDockerRunCmd());
1165
+ runtime.execCommand = joinExecCommand(runtime.execCommandPrefix, runtime.execCommand, runtime.execCommandSuffix);
1166
+ console.log(buildDockerRunCmd(runtime));
1885
1167
  process.exit(0);
1886
1168
  }
1887
- if (!containerExists(CONTAINER_NAME)) {
1888
- return await createNewContainer();
1169
+ if (!containerExists(runtime.containerName)) {
1170
+ return await createNewContainer(runtime);
1889
1171
  } else {
1890
- return await connectExistingContainer();
1172
+ return await connectExistingContainer(runtime);
1891
1173
  }
1892
1174
  }
1893
1175
 
1894
- function executeInContainer(defaultCommand) {
1895
- getHelloTip(CONTAINER_NAME, defaultCommand);
1896
- if ( !(QUIET.cmd || QUIET.full) ) {
1176
+ function executeInContainer(runtime, defaultCommand) {
1177
+ if (!containerExists(runtime.containerName)) {
1178
+ throw new Error(`未找到容器: ${runtime.containerName}`);
1179
+ }
1180
+
1181
+ const status = getContainerStatus(runtime.containerName);
1182
+ if (status !== 'running') {
1183
+ dockerExecArgs(['start', runtime.containerName], { stdio: 'pipe' });
1184
+ }
1185
+
1186
+ getHelloTip(runtime.containerName, defaultCommand, runtime.execCommand);
1187
+ if (!(runtime.quiet.cmd || runtime.quiet.full)) {
1897
1188
  console.log(`${BLUE}----------------------------------------${NC}`);
1898
- console.log(`💻 执行命令: ${YELLOW}${EXEC_COMMAND || '交互式 Shell'}${NC}`);
1189
+ console.log(`💻 执行命令: ${YELLOW}${runtime.execCommand || '交互式 Shell'}${NC}`);
1899
1190
  }
1900
1191
 
1901
1192
  // Execute command in container
1902
- if (EXEC_COMMAND) {
1903
- spawnSync(`${DOCKER_CMD}`, ['exec', '-it', CONTAINER_NAME, '/bin/bash', '-c', EXEC_COMMAND], { stdio: 'inherit' });
1193
+ if (runtime.execCommand) {
1194
+ spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash', '-c', runtime.execCommand], { stdio: 'inherit' });
1904
1195
  } else {
1905
- spawnSync(`${DOCKER_CMD}`, ['exec', '-it', CONTAINER_NAME, '/bin/bash'], { stdio: 'inherit' });
1196
+ spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash'], { stdio: 'inherit' });
1906
1197
  }
1907
1198
  }
1908
1199
 
@@ -1910,73 +1201,75 @@ function executeInContainer(defaultCommand) {
1910
1201
  * 处理会话退出后的交互
1911
1202
  * @param {string} defaultCommand - 默认命令
1912
1203
  */
1913
- async function handlePostExit(defaultCommand) {
1204
+ async function handlePostExit(runtime, defaultCommand) {
1914
1205
  // --rm-on-exit 模式:自动删除容器
1915
- if (RM_ON_EXIT) {
1916
- removeContainer(CONTAINER_NAME);
1917
- return;
1206
+ if (runtime.rmOnExit) {
1207
+ removeContainer(runtime.containerName);
1208
+ return false;
1918
1209
  }
1919
1210
 
1920
- getHelloTip(CONTAINER_NAME, defaultCommand);
1211
+ getHelloTip(runtime.containerName, defaultCommand, runtime.execCommand);
1921
1212
 
1922
- let tipAskKeep = `❔ 会话已结束。是否保留此后台容器 ${CONTAINER_NAME}? [ y=默认保留, n=删除, 1=首次命令进入, x=执行命令, i=交互式SHELL ]: `;
1923
- if ( QUIET.askkeep || QUIET.full ) tipAskKeep = `保留容器吗? [y n 1 x i] `;
1213
+ let tipAskKeep = `❔ 会话已结束。是否保留此后台容器 ${runtime.containerName}? [ y=默认保留, n=删除, 1=首次命令进入, x=执行命令, i=交互式SHELL ]: `;
1214
+ if (runtime.quiet.askkeep || runtime.quiet.full) tipAskKeep = `保留容器吗? [y n 1 x i] `;
1924
1215
  const reply = await askQuestion(tipAskKeep);
1925
1216
 
1926
1217
  const firstChar = reply.trim().toLowerCase()[0];
1927
1218
 
1928
1219
  if (firstChar === 'n') {
1929
- removeContainer(CONTAINER_NAME);
1220
+ removeContainer(runtime.containerName);
1221
+ return false;
1930
1222
  } else if (firstChar === '1') {
1931
- if ( !(QUIET.full) ) console.log(`${GREEN}✅ 离开当前连接,用首次命令进入。${NC}`);
1932
- // Reset command variables to use default command
1933
- EXEC_COMMAND = "";
1934
- EXEC_COMMAND_PREFIX = "";
1935
- EXEC_COMMAND_SUFFIX = "";
1936
- const newArgs = ['-n', CONTAINER_NAME];
1937
- process.argv = [process.argv[0], process.argv[1], ...newArgs];
1938
- await main();
1223
+ if (!(runtime.quiet.full)) console.log(`${GREEN}✅ 离开当前连接,用首次命令进入。${NC}`);
1224
+ runtime.execCommandPrefix = "";
1225
+ runtime.execCommandSuffix = "";
1226
+ runtime.execCommand = defaultCommand;
1227
+ return true;
1939
1228
  } else if (firstChar === 'x') {
1940
1229
  const command = await askQuestion('❔ 输入要执行的命令: ');
1941
- if ( !(QUIET.cmd || QUIET.full) ) console.log(`${GREEN}✅ 离开当前连接,执行命令。${NC}`);
1942
- const newArgs = ['-n', CONTAINER_NAME, '-x', command];
1943
- process.argv = [process.argv[0], process.argv[1], ...newArgs];
1944
- await main();
1230
+ if (!(runtime.quiet.cmd || runtime.quiet.full)) console.log(`${GREEN}✅ 离开当前连接,执行命令。${NC}`);
1231
+ runtime.execCommandPrefix = "";
1232
+ runtime.execCommandSuffix = "";
1233
+ runtime.execCommand = command;
1234
+ return true;
1945
1235
  } else if (firstChar === 'i') {
1946
- if ( !(QUIET.full) ) console.log(`${GREEN}✅ 离开当前连接,进入容器交互式SHELL。${NC}`);
1947
- const newArgs = ['-n', CONTAINER_NAME, '-x', '/bin/bash'];
1948
- process.argv = [process.argv[0], process.argv[1], ...newArgs];
1949
- await main();
1236
+ if (!(runtime.quiet.full)) console.log(`${GREEN}✅ 离开当前连接,进入容器交互式SHELL。${NC}`);
1237
+ runtime.execCommandPrefix = "";
1238
+ runtime.execCommandSuffix = "";
1239
+ runtime.execCommand = '/bin/bash';
1240
+ return true;
1950
1241
  } else {
1951
- console.log(`${GREEN}✅ 已退出连接。容器 ${CONTAINER_NAME} 仍在后台运行。${NC}`);
1242
+ console.log(`${GREEN}✅ 已退出连接。容器 ${runtime.containerName} 仍在后台运行。${NC}`);
1243
+ return false;
1952
1244
  }
1953
1245
  }
1954
1246
 
1955
- // ==============================================================================
1956
- // SECTION: Web Server
1957
- // ==============================================================================
1958
-
1959
- async function runWebServerMode() {
1960
- ensureWebServerAuthCredentials();
1247
+ async function runWebServerMode(runtime) {
1248
+ if (!runtime.serverAuthUser || !runtime.serverAuthPass) {
1249
+ ensureWebServerAuthCredentials();
1250
+ runtime.serverAuthUser = SERVER_AUTH_USER;
1251
+ runtime.serverAuthPass = SERVER_AUTH_PASS;
1252
+ runtime.serverAuthPassAuto = SERVER_AUTH_PASS_AUTO;
1253
+ }
1961
1254
 
1962
1255
  await startWebServer({
1963
- serverHost: SERVER_HOST,
1964
- serverPort: SERVER_PORT,
1965
- authUser: SERVER_AUTH_USER,
1966
- authPass: SERVER_AUTH_PASS,
1967
- authPassAuto: SERVER_AUTH_PASS_AUTO,
1256
+ serverHost: runtime.serverHost,
1257
+ serverPort: runtime.serverPort,
1258
+ authUser: runtime.serverAuthUser,
1259
+ authPass: runtime.serverAuthPass,
1260
+ authPassAuto: runtime.serverAuthPassAuto,
1968
1261
  dockerCmd: DOCKER_CMD,
1969
- hostPath: HOST_PATH,
1970
- containerPath: CONTAINER_PATH,
1971
- imageName: IMAGE_NAME,
1972
- imageVersion: IMAGE_VERSION,
1973
- execCommandPrefix: EXEC_COMMAND_PREFIX,
1974
- execCommand: EXEC_COMMAND,
1975
- execCommandSuffix: EXEC_COMMAND_SUFFIX,
1976
- contModeArgs: CONT_MODE_ARGS,
1977
- containerEnvs: CONTAINER_ENVS,
1978
- containerVolumes: CONTAINER_VOLUMES,
1979
- validateHostPath,
1262
+ hostPath: runtime.hostPath,
1263
+ containerPath: runtime.containerPath,
1264
+ imageName: runtime.imageName,
1265
+ imageVersion: runtime.imageVersion,
1266
+ execCommandPrefix: runtime.execCommandPrefix,
1267
+ execCommand: runtime.execCommand,
1268
+ execCommandSuffix: runtime.execCommandSuffix,
1269
+ contModeArgs: runtime.contModeArgs,
1270
+ containerEnvs: runtime.containerEnvs,
1271
+ containerVolumes: runtime.containerVolumes,
1272
+ validateHostPath: () => validateHostPath(runtime),
1980
1273
  formatDate,
1981
1274
  isValidContainerName,
1982
1275
  containerExists,
@@ -1997,41 +1290,57 @@ async function runWebServerMode() {
1997
1290
  });
1998
1291
  }
1999
1292
 
2000
- // ==============================================================================
2001
- // Main Function
2002
- // ==============================================================================
2003
-
2004
1293
  async function main() {
2005
1294
  try {
2006
1295
  // 1. Setup commander and parse arguments
2007
1296
  await setupCommander();
1297
+ const runtime = createRuntimeContext();
2008
1298
 
2009
1299
  // 2. Start web server mode
2010
- if (SERVER_MODE) {
2011
- await runWebServerMode();
1300
+ if (runtime.serverMode) {
1301
+ await runWebServerMode(runtime);
2012
1302
  return;
2013
1303
  }
2014
1304
 
2015
1305
  // 3. Handle image build operation
2016
1306
  if (IMAGE_BUILD_NEED) {
2017
- await buildImage(IMAGE_BUILD_ARGS, IMAGE_NAME, IMAGE_VERSION);
1307
+ await buildImage({
1308
+ imageBuildArgs: IMAGE_BUILD_ARGS,
1309
+ imageName: runtime.imageName,
1310
+ imageVersionTag: runtime.imageVersion,
1311
+ imageVersionDefault: IMAGE_VERSION_DEFAULT,
1312
+ imageVersionBase: IMAGE_VERSION_BASE,
1313
+ parseImageVersionTag,
1314
+ manyoyoName: MANYOYO_NAME,
1315
+ yesMode: YES_MODE,
1316
+ dockerCmd: DOCKER_CMD,
1317
+ rootDir: path.join(__dirname, '..'),
1318
+ loadConfig,
1319
+ runCmd,
1320
+ askQuestion,
1321
+ pruneDanglingImages,
1322
+ colors: { RED, GREEN, YELLOW, BLUE, CYAN, NC }
1323
+ });
2018
1324
  process.exit(0);
2019
1325
  }
2020
1326
 
2021
1327
  // 4. Handle remove container operation
2022
- handleRemoveContainer();
1328
+ if (handleRemoveContainer(runtime)) {
1329
+ return;
1330
+ }
2023
1331
 
2024
1332
  // 5. Validate host path safety
2025
- validateHostPath();
1333
+ validateHostPath(runtime);
2026
1334
 
2027
1335
  // 6. Setup container (create or connect)
2028
- const defaultCommand = await setupContainer();
1336
+ const defaultCommand = await setupContainer(runtime);
2029
1337
 
2030
- // 7. Execute command in container
2031
- executeInContainer(defaultCommand);
2032
-
2033
- // 8. Handle post-exit interactions
2034
- await handlePostExit(defaultCommand);
1338
+ // 7-8. Execute command and handle post-exit interactions
1339
+ let shouldContinue = true;
1340
+ while (shouldContinue) {
1341
+ executeInContainer(runtime, defaultCommand);
1342
+ shouldContinue = await handlePostExit(runtime, defaultCommand);
1343
+ }
2035
1344
 
2036
1345
  } catch (e) {
2037
1346
  console.error(`${RED}Error: ${e.message}${NC}`);