@xcanwin/manyoyo 3.9.6 → 4.0.0

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
@@ -9,6 +9,7 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const os = require('os');
11
11
  const crypto = require('crypto');
12
+ const net = require('net');
12
13
  const readline = require('readline');
13
14
  const { Command } = require('commander');
14
15
  const JSON5 = require('json5');
@@ -59,6 +60,7 @@ let SHOW_COMMAND = false;
59
60
  let YES_MODE = false;
60
61
  let RM_ON_EXIT = false;
61
62
  let SERVER_MODE = false;
63
+ let SERVER_HOST = '127.0.0.1';
62
64
  let SERVER_PORT = 3000;
63
65
  let SERVER_AUTH_USER = "";
64
66
  let SERVER_AUTH_PASS = "";
@@ -75,6 +77,7 @@ const NC = '\x1b[0m'; // No Color
75
77
 
76
78
  // Docker command (will be set by ensure_docker)
77
79
  let DOCKER_CMD = 'docker';
80
+ const SUPPORTED_INIT_AGENTS = ['claude', 'codex', 'gemini', 'opencode'];
78
81
 
79
82
  // ==============================================================================
80
83
  // SECTION: Utility Functions
@@ -90,24 +93,70 @@ function normalizeCommandSuffix(suffix) {
90
93
  return trimmed ? ` ${trimmed}` : "";
91
94
  }
92
95
 
93
- function parseServerPort(rawPort) {
94
- if (rawPort === true || rawPort === undefined || rawPort === null || rawPort === '') {
95
- return 3000;
96
+ function resolveContainerNameTemplate(name) {
97
+ if (typeof name !== 'string') {
98
+ return name;
99
+ }
100
+ const nowValue = formatDate();
101
+ return name.replace(/\{now\}|\$\{now\}/g, nowValue);
102
+ }
103
+
104
+ function validateServerHost(host, rawServer) {
105
+ const value = String(host || '').trim();
106
+ const isIp = net.isIP(value) !== 0;
107
+ const isHostName = /^[A-Za-z0-9.-]+$/.test(value);
108
+
109
+ if (isIp || isHostName) {
110
+ return value;
111
+ }
112
+
113
+ console.error(`${RED}⚠️ 错误: --server 地址格式应为 端口 或 host:port (例如 3000 / 0.0.0.0:3000): ${rawServer}${NC}`);
114
+ process.exit(1);
115
+ }
116
+
117
+ function parseServerListen(rawServer) {
118
+ if (rawServer === true || rawServer === undefined || rawServer === null || rawServer === '') {
119
+ return { host: '127.0.0.1', port: 3000 };
120
+ }
121
+
122
+ const value = String(rawServer).trim();
123
+ if (!value) {
124
+ return { host: '127.0.0.1', port: 3000 };
96
125
  }
97
126
 
98
- const value = String(rawPort).trim();
99
- if (!/^\d+$/.test(value)) {
100
- console.error(`${RED}⚠️ 错误: --server 端口必须是 1-65535 的整数: ${rawPort}${NC}`);
127
+ let host = '127.0.0.1';
128
+ let portText = value;
129
+
130
+ const ipv6Match = value.match(/^\[([^\]]+)\]:(\d+)$/);
131
+ if (ipv6Match) {
132
+ host = ipv6Match[1].trim();
133
+ portText = ipv6Match[2].trim();
134
+ } else {
135
+ const lastColonIndex = value.lastIndexOf(':');
136
+ if (lastColonIndex > 0) {
137
+ const maybePort = value.slice(lastColonIndex + 1).trim();
138
+ if (/^\d+$/.test(maybePort)) {
139
+ host = value.slice(0, lastColonIndex).trim();
140
+ portText = maybePort;
141
+ }
142
+ }
143
+ }
144
+
145
+ if (!/^\d+$/.test(portText)) {
146
+ console.error(`${RED}⚠️ 错误: --server 端口必须是 1-65535 的整数: ${rawServer}${NC}`);
101
147
  process.exit(1);
102
148
  }
103
149
 
104
- const port = Number(value);
150
+ const port = Number(portText);
105
151
  if (port < 1 || port > 65535) {
106
- console.error(`${RED}⚠️ 错误: --server 端口超出范围 (1-65535): ${rawPort}${NC}`);
152
+ console.error(`${RED}⚠️ 错误: --server 端口超出范围 (1-65535): ${rawServer}${NC}`);
107
153
  process.exit(1);
108
154
  }
109
155
 
110
- return port;
156
+ return {
157
+ host: validateServerHost(host, rawServer),
158
+ port
159
+ };
111
160
  }
112
161
 
113
162
  function ensureWebServerAuthCredentials() {
@@ -247,6 +296,457 @@ function loadRunConfig(name) {
247
296
  return {};
248
297
  }
249
298
 
299
+ function readJsonFileSafely(filePath, label) {
300
+ if (!fs.existsSync(filePath)) {
301
+ return null;
302
+ }
303
+ try {
304
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
305
+ } catch (e) {
306
+ console.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
307
+ return null;
308
+ }
309
+ }
310
+
311
+ function parseSimpleToml(content) {
312
+ const result = {};
313
+ let current = result;
314
+ const lines = String(content || '').split('\n');
315
+
316
+ for (const rawLine of lines) {
317
+ const line = rawLine.trim();
318
+ if (!line || line.startsWith('#')) {
319
+ continue;
320
+ }
321
+
322
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
323
+ if (sectionMatch) {
324
+ const parts = sectionMatch[1].split('.').map(p => p.trim()).filter(Boolean);
325
+ current = result;
326
+ for (const part of parts) {
327
+ if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) {
328
+ current[part] = {};
329
+ }
330
+ current = current[part];
331
+ }
332
+ continue;
333
+ }
334
+
335
+ const keyValueMatch = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
336
+ if (!keyValueMatch) {
337
+ continue;
338
+ }
339
+
340
+ const key = keyValueMatch[1];
341
+ let valueText = keyValueMatch[2].trim();
342
+ if ((valueText.startsWith('"') && valueText.endsWith('"')) || (valueText.startsWith("'") && valueText.endsWith("'"))) {
343
+ valueText = valueText.slice(1, -1);
344
+ } else if (valueText === 'true') {
345
+ valueText = true;
346
+ } else if (valueText === 'false') {
347
+ valueText = false;
348
+ } else if (/^-?\d+(\.\d+)?$/.test(valueText)) {
349
+ valueText = Number(valueText);
350
+ }
351
+
352
+ current[key] = valueText;
353
+ }
354
+
355
+ return result;
356
+ }
357
+
358
+ function readTomlFileSafely(filePath, label) {
359
+ if (!fs.existsSync(filePath)) {
360
+ return null;
361
+ }
362
+ try {
363
+ const content = fs.readFileSync(filePath, 'utf-8');
364
+ return parseSimpleToml(content);
365
+ } catch (e) {
366
+ console.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
367
+ return null;
368
+ }
369
+ }
370
+
371
+ function normalizeInitConfigAgents(rawAgents) {
372
+ const aliasMap = {
373
+ all: 'all',
374
+ claude: 'claude',
375
+ c: 'claude',
376
+ cc: 'claude',
377
+ codex: 'codex',
378
+ cx: 'codex',
379
+ gemini: 'gemini',
380
+ gm: 'gemini',
381
+ g: 'gemini',
382
+ opencode: 'opencode',
383
+ oc: 'opencode'
384
+ };
385
+
386
+ if (rawAgents === true || rawAgents === undefined || rawAgents === null || rawAgents === '') {
387
+ return [...SUPPORTED_INIT_AGENTS];
388
+ }
389
+
390
+ const tokens = String(rawAgents).split(/[,\s]+/).map(v => v.trim().toLowerCase()).filter(Boolean);
391
+ if (tokens.length === 0) {
392
+ return [...SUPPORTED_INIT_AGENTS];
393
+ }
394
+
395
+ const normalized = [];
396
+ for (const token of tokens) {
397
+ const mapped = aliasMap[token];
398
+ if (!mapped) {
399
+ console.error(`${RED}⚠️ 错误: --init-config 不支持的 Agent: ${token}${NC}`);
400
+ console.error(`${YELLOW}支持: ${SUPPORTED_INIT_AGENTS.join(', ')} 或 all${NC}`);
401
+ process.exit(1);
402
+ }
403
+ if (mapped === 'all') {
404
+ return [...SUPPORTED_INIT_AGENTS];
405
+ }
406
+ if (!normalized.includes(mapped)) {
407
+ normalized.push(mapped);
408
+ }
409
+ }
410
+ return normalized;
411
+ }
412
+
413
+ function isSafeInitEnvValue(value) {
414
+ if (value === undefined || value === null) {
415
+ return false;
416
+ }
417
+ const text = String(value).replace(/[\r\n\0]/g, '').trim();
418
+ if (!text) {
419
+ return false;
420
+ }
421
+ if (/[\$\(\)\`\|\&\*\{\};<>]/.test(text)) {
422
+ return false;
423
+ }
424
+ if (/^\(/.test(text)) {
425
+ return false;
426
+ }
427
+ return true;
428
+ }
429
+
430
+ function setInitValue(values, key, value) {
431
+ if (value === undefined || value === null) {
432
+ return;
433
+ }
434
+ const text = String(value).replace(/[\r\n\0]/g, '').trim();
435
+ if (!text) {
436
+ return;
437
+ }
438
+ values[key] = text;
439
+ }
440
+
441
+ function dedupeList(list) {
442
+ return Array.from(new Set((list || []).filter(Boolean)));
443
+ }
444
+
445
+ function resolveEnvPlaceholder(value) {
446
+ if (typeof value !== 'string') {
447
+ return "";
448
+ }
449
+ const match = value.match(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/);
450
+ if (!match) {
451
+ return "";
452
+ }
453
+ const envName = match[1];
454
+ return process.env[envName] ? String(process.env[envName]).trim() : "";
455
+ }
456
+
457
+ function collectClaudeInitData(homeDir) {
458
+ const keys = [
459
+ 'ANTHROPIC_AUTH_TOKEN',
460
+ 'CLAUDE_CODE_OAUTH_TOKEN',
461
+ 'ANTHROPIC_BASE_URL',
462
+ 'ANTHROPIC_MODEL',
463
+ 'ANTHROPIC_DEFAULT_OPUS_MODEL',
464
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL',
465
+ 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
466
+ 'CLAUDE_CODE_SUBAGENT_MODEL'
467
+ ];
468
+ const values = {};
469
+ const notes = [];
470
+ const volumes = [];
471
+
472
+ const claudeDir = path.join(homeDir, '.claude');
473
+ const claudeSettingsPath = path.join(claudeDir, 'settings.json');
474
+ const claudeJsonPath = path.join(homeDir, '.claude.json');
475
+ const settingsJson = readJsonFileSafely(claudeSettingsPath, 'Claude settings');
476
+ const claudeJson = readJsonFileSafely(claudeJsonPath, 'Claude config');
477
+
478
+ keys.forEach(key => setInitValue(values, key, process.env[key]));
479
+
480
+ if (claudeJson && claudeJson.env && typeof claudeJson.env === 'object') {
481
+ keys.forEach(key => setInitValue(values, key, claudeJson.env[key]));
482
+ }
483
+ if (settingsJson && settingsJson.env && typeof settingsJson.env === 'object') {
484
+ keys.forEach(key => setInitValue(values, key, settingsJson.env[key]));
485
+ }
486
+
487
+ if (fs.existsSync(claudeDir)) {
488
+ volumes.push(`${claudeDir}:/root/.claude`);
489
+ }
490
+ if (fs.existsSync(claudeJsonPath)) {
491
+ volumes.push(`${claudeJsonPath}:/root/.claude.json`);
492
+ }
493
+ if (!fs.existsSync(claudeDir) && !fs.existsSync(claudeJsonPath)) {
494
+ notes.push('未检测到 Claude 本地配置(~/.claude 或 ~/.claude.json),已生成占位模板。');
495
+ }
496
+
497
+ return { keys, values, notes, volumes: dedupeList(volumes) };
498
+ }
499
+
500
+ function collectGeminiInitData(homeDir) {
501
+ const keys = [
502
+ 'GOOGLE_GEMINI_BASE_URL',
503
+ 'GEMINI_API_KEY',
504
+ 'GEMINI_MODEL'
505
+ ];
506
+ const values = {};
507
+ const notes = [];
508
+ const volumes = [];
509
+ const geminiDir = path.join(homeDir, '.gemini');
510
+
511
+ keys.forEach(key => setInitValue(values, key, process.env[key]));
512
+
513
+ if (fs.existsSync(geminiDir)) {
514
+ volumes.push(`${geminiDir}:/root/.gemini`);
515
+ } else {
516
+ notes.push('未检测到 Gemini 本地配置目录(~/.gemini),已生成占位模板。');
517
+ }
518
+
519
+ return { keys, values, notes, volumes: dedupeList(volumes) };
520
+ }
521
+
522
+ function collectCodexInitData(homeDir) {
523
+ const keys = [
524
+ 'OPENAI_API_KEY',
525
+ 'OPENAI_BASE_URL',
526
+ 'OPENAI_MODEL'
527
+ ];
528
+ const values = {};
529
+ const notes = [];
530
+ const volumes = [];
531
+
532
+ const codexDir = path.join(homeDir, '.codex');
533
+ const authPath = path.join(codexDir, 'auth.json');
534
+ const configPath = path.join(codexDir, 'config.toml');
535
+ const authJson = readJsonFileSafely(authPath, 'Codex auth');
536
+ const configToml = readTomlFileSafely(configPath, 'Codex TOML');
537
+
538
+ keys.forEach(key => setInitValue(values, key, process.env[key]));
539
+
540
+ if (authJson && typeof authJson === 'object') {
541
+ setInitValue(values, 'OPENAI_API_KEY', authJson.OPENAI_API_KEY);
542
+ }
543
+
544
+ if (configToml && typeof configToml === 'object') {
545
+ setInitValue(values, 'OPENAI_MODEL', configToml.model);
546
+
547
+ let providerConfig = null;
548
+ const providers = configToml.model_providers;
549
+ if (providers && typeof providers === 'object') {
550
+ if (typeof configToml.model_provider === 'string' && providers[configToml.model_provider]) {
551
+ providerConfig = providers[configToml.model_provider];
552
+ } else {
553
+ const firstProviderName = Object.keys(providers)[0];
554
+ if (firstProviderName) {
555
+ providerConfig = providers[firstProviderName];
556
+ }
557
+ }
558
+ }
559
+ if (providerConfig && typeof providerConfig === 'object') {
560
+ setInitValue(values, 'OPENAI_BASE_URL', providerConfig.base_url);
561
+ }
562
+ }
563
+
564
+ if (fs.existsSync(codexDir)) {
565
+ volumes.push(`${codexDir}:/root/.codex`);
566
+ } else {
567
+ notes.push('未检测到 Codex 本地配置目录(~/.codex),已生成占位模板。');
568
+ }
569
+
570
+ return { keys, values, notes, volumes: dedupeList(volumes) };
571
+ }
572
+
573
+ function collectOpenCodeInitData(homeDir) {
574
+ const keys = [
575
+ 'OPENAI_API_KEY',
576
+ 'OPENAI_BASE_URL',
577
+ 'OPENAI_MODEL'
578
+ ];
579
+ const values = {};
580
+ const notes = [];
581
+ const volumes = [];
582
+
583
+ const opencodeDir = path.join(homeDir, '.config', 'opencode');
584
+ const opencodePath = path.join(opencodeDir, 'opencode.json');
585
+ const opencodeAuthPath = path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
586
+ const opencodeJson = readJsonFileSafely(opencodePath, 'OpenCode config');
587
+
588
+ keys.forEach(key => setInitValue(values, key, process.env[key]));
589
+
590
+ if (opencodeJson && typeof opencodeJson === 'object') {
591
+ const providers = opencodeJson.provider && typeof opencodeJson.provider === 'object'
592
+ ? Object.values(opencodeJson.provider).filter(v => v && typeof v === 'object')
593
+ : [];
594
+ const provider = providers[0];
595
+
596
+ if (provider) {
597
+ const options = provider.options && typeof provider.options === 'object' ? provider.options : {};
598
+ const apiKeyValue = resolveEnvPlaceholder(options.apiKey) || options.apiKey;
599
+ const baseUrlValue = resolveEnvPlaceholder(options.baseURL) || options.baseURL;
600
+ setInitValue(values, 'OPENAI_API_KEY', apiKeyValue);
601
+ setInitValue(values, 'OPENAI_BASE_URL', baseUrlValue);
602
+
603
+ if (provider.models && typeof provider.models === 'object') {
604
+ const firstModelName = Object.keys(provider.models)[0];
605
+ if (firstModelName) {
606
+ setInitValue(values, 'OPENAI_MODEL', firstModelName);
607
+ }
608
+ }
609
+ }
610
+
611
+ if (typeof opencodeJson.model === 'string') {
612
+ const modelFromEnv = resolveEnvPlaceholder(opencodeJson.model);
613
+ if (modelFromEnv) {
614
+ setInitValue(values, 'OPENAI_MODEL', modelFromEnv);
615
+ }
616
+ }
617
+ }
618
+
619
+ if (fs.existsSync(opencodePath)) {
620
+ volumes.push(`${opencodePath}:/root/.config/opencode/opencode.json`);
621
+ } else {
622
+ notes.push('未检测到 OpenCode 配置文件(~/.config/opencode/opencode.json),已生成占位模板。');
623
+ }
624
+ if (fs.existsSync(opencodeAuthPath)) {
625
+ volumes.push(`${opencodeAuthPath}:/root/.local/share/opencode/auth.json`);
626
+ }
627
+
628
+ return { keys, values, notes, volumes: dedupeList(volumes) };
629
+ }
630
+
631
+ function writeInitEnvFile(filePath, keys, values) {
632
+ const lines = [
633
+ '# Auto-generated by manyoyo --init-config',
634
+ '# Remove leading # and fill values if missing.',
635
+ ''
636
+ ];
637
+ const missingKeys = [];
638
+ const unsafeKeys = [];
639
+
640
+ for (const key of keys) {
641
+ const value = values[key];
642
+ if (isSafeInitEnvValue(value)) {
643
+ lines.push(`export ${key}=${String(value).replace(/[\r\n\0]/g, '')}`);
644
+ } else if (value !== undefined && value !== null && String(value).trim() !== '') {
645
+ lines.push(`# export ${key}=""`);
646
+ unsafeKeys.push(key);
647
+ } else {
648
+ lines.push(`# export ${key}=""`);
649
+ missingKeys.push(key);
650
+ }
651
+ }
652
+ lines.push('');
653
+
654
+ fs.writeFileSync(filePath, lines.join('\n'));
655
+ return { missingKeys, unsafeKeys };
656
+ }
657
+
658
+ function writeInitRunFile(filePath, agent, yolo, volumes) {
659
+ const runConfig = {
660
+ containerName: `my-${agent}-{now}`,
661
+ envFile: [agent],
662
+ yolo
663
+ };
664
+ const volumeList = dedupeList(volumes);
665
+ if (volumeList.length > 0) {
666
+ runConfig.volumes = volumeList;
667
+ }
668
+ fs.writeFileSync(filePath, `${JSON.stringify(runConfig, null, 4)}\n`);
669
+ }
670
+
671
+ async function shouldOverwriteInitFile(filePath, fileLabel) {
672
+ if (!fs.existsSync(filePath)) {
673
+ return true;
674
+ }
675
+
676
+ if (YES_MODE) {
677
+ console.log(`${YELLOW}⚠️ ${filePath} 已存在,--yes 模式自动覆盖 (${fileLabel})${NC}`);
678
+ return true;
679
+ }
680
+
681
+ const reply = await askQuestion(`❔ ${filePath} 已存在,是否覆盖? [y/N]: `);
682
+ const firstChar = String(reply || '').trim().toLowerCase()[0];
683
+ if (firstChar === 'y') {
684
+ return true;
685
+ }
686
+ console.log(`${YELLOW}⏭️ 已保留原文件: ${filePath}${NC}`);
687
+ return false;
688
+ }
689
+
690
+ async function initAgentConfigs(rawAgents) {
691
+ const agents = normalizeInitConfigAgents(rawAgents);
692
+ const homeDir = os.homedir();
693
+ const manyoyoHome = path.join(homeDir, '.manyoyo');
694
+ const runDir = path.join(manyoyoHome, 'run');
695
+ const envDir = path.join(manyoyoHome, 'env');
696
+
697
+ fs.mkdirSync(envDir, { recursive: true });
698
+ fs.mkdirSync(runDir, { recursive: true });
699
+
700
+ const extractors = {
701
+ claude: collectClaudeInitData,
702
+ codex: collectCodexInitData,
703
+ gemini: collectGeminiInitData,
704
+ opencode: collectOpenCodeInitData
705
+ };
706
+ const yoloMap = {
707
+ claude: 'c',
708
+ codex: 'cx',
709
+ gemini: 'gm',
710
+ opencode: 'oc'
711
+ };
712
+
713
+ console.log(`${CYAN}🧭 正在初始化 MANYOYO 配置: ${agents.join(', ')}${NC}`);
714
+
715
+ for (const agent of agents) {
716
+ const data = extractors[agent](homeDir);
717
+ const runFilePath = path.join(runDir, `${agent}.json`);
718
+ const envFilePath = path.join(envDir, `${agent}.env`);
719
+ const shouldWriteRun = await shouldOverwriteInitFile(runFilePath, `${agent}.json`);
720
+ const shouldWriteEnv = await shouldOverwriteInitFile(envFilePath, `${agent}.env`);
721
+
722
+ let writeResult = { missingKeys: [], unsafeKeys: [] };
723
+ if (shouldWriteEnv) {
724
+ writeResult = writeInitEnvFile(envFilePath, data.keys, data.values);
725
+ }
726
+ if (shouldWriteRun) {
727
+ writeInitRunFile(runFilePath, agent, yoloMap[agent], data.volumes);
728
+ }
729
+
730
+ if (shouldWriteEnv || shouldWriteRun) {
731
+ console.log(`${GREEN}✅ [${agent}] 初始化完成${NC}`);
732
+ } else {
733
+ console.log(`${YELLOW}⚠️ [${agent}] 已跳过(文件均保留)${NC}`);
734
+ }
735
+ console.log(` run: ${shouldWriteRun ? '已写入' : '保留'} ${runFilePath}`);
736
+ console.log(` env: ${shouldWriteEnv ? '已写入' : '保留'} ${envFilePath}`);
737
+
738
+ if (shouldWriteEnv && writeResult.missingKeys.length > 0) {
739
+ console.log(`${YELLOW}⚠️ [${agent}] 以下变量未找到,请手动填写:${NC} ${writeResult.missingKeys.join(', ')}`);
740
+ }
741
+ if (shouldWriteEnv && writeResult.unsafeKeys.length > 0) {
742
+ console.log(`${YELLOW}⚠️ [${agent}] 以下变量包含不安全字符,已留空模板:${NC} ${writeResult.unsafeKeys.join(', ')}`);
743
+ }
744
+ if (data.notes && data.notes.length > 0) {
745
+ data.notes.forEach(note => console.log(`${YELLOW}⚠️ [${agent}] ${note}${NC}`));
746
+ }
747
+ }
748
+ }
749
+
250
750
  // ==============================================================================
251
751
  // SECTION: UI Functions
252
752
  // ==============================================================================
@@ -493,7 +993,7 @@ function showImagePullHint(err) {
493
993
  }
494
994
  const image = `${IMAGE_NAME}:${IMAGE_VERSION}`;
495
995
  console.log(`${YELLOW}💡 提示: 本地未找到镜像 ${image},并且从 localhost 注册表拉取失败。${NC}`);
496
- console.log(`${YELLOW} 你可以: 1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion 2) 或先执行 manyoyo --ib --iv <version> 构建镜像。${NC}`);
996
+ console.log(`${YELLOW} 你可以: (1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion (2) 或先执行 manyoyo --ib --iv <version> 构建镜像。${NC}`);
497
997
  }
498
998
 
499
999
  function runCmd(cmd, args, options = {}) {
@@ -868,7 +1368,7 @@ async function buildImage(IMAGE_BUILD_ARGS, imageName, imageVersion) {
868
1368
  // SECTION: Command Line Interface
869
1369
  // ==============================================================================
870
1370
 
871
- function setupCommander() {
1371
+ async function setupCommander() {
872
1372
  // Load config file
873
1373
  const config = loadConfig();
874
1374
 
@@ -898,10 +1398,12 @@ function setupCommander() {
898
1398
  ${MANYOYO_NAME} -r ./myconfig.json 使用当前目录 ./myconfig.json 配置
899
1399
  ${MANYOYO_NAME} -n test --ef claude -y c 使用 ~/.manyoyo/env/claude.env 环境变量文件
900
1400
  ${MANYOYO_NAME} -n test --ef ./myenv.env -y c 使用当前目录 ./myenv.env 环境变量文件
1401
+ ${MANYOYO_NAME} --init-config all 从本机 Agent 配置初始化 ~/.manyoyo
901
1402
  ${MANYOYO_NAME} -n test -- -c 恢复之前会话
902
1403
  ${MANYOYO_NAME} -x echo 123 指定命令执行
903
1404
  ${MANYOYO_NAME} --server --server-user admin --server-pass 123456 启动带登录认证的网页服务
904
1405
  ${MANYOYO_NAME} --server 3000 启动网页交互服务
1406
+ ${MANYOYO_NAME} --server 0.0.0.0:3000 监听全部网卡,便于局域网访问
905
1407
  ${MANYOYO_NAME} -n test -q tip -q cmd 多次使用静默选项
906
1408
  `);
907
1409
 
@@ -927,10 +1429,11 @@ function setupCommander() {
927
1429
  .option('--ss, --shell-suffix <command>', '指定命令后缀 (追加到-s之后,等价于 -- <args>)')
928
1430
  .option('-x, --shell-full <command...>', '指定完整命令执行 (代替--sp和-s和--命令)')
929
1431
  .option('-y, --yolo <cli>', '使AGENT无需确认 (claude/c, gemini/gm, codex/cx, opencode/oc)')
1432
+ .option('--init-config [agents]', '初始化 Agent 配置到 ~/.manyoyo (all 或逗号分隔: claude,codex,gemini,opencode)')
930
1433
  .option('--install <name>', '安装manyoyo命令 (docker-cli-plugin)')
931
1434
  .option('--show-config', '显示最终生效配置并退出')
932
1435
  .option('--show-command', '显示将执行的 docker run 命令并退出')
933
- .option('--server [port]', '启动网页交互服务 (默认端口: 3000)')
1436
+ .option('--server [port]', '启动网页交互服务 (默认 127.0.0.1:3000,支持 host:port)')
934
1437
  .option('--server-user <username>', '网页服务登录用户名 (默认 admin)')
935
1438
  .option('--server-pass <password>', '网页服务登录密码 (默认自动生成随机密码)')
936
1439
  .option('--yes', '所有提示自动确认 (用于CI/脚本)')
@@ -959,8 +1462,12 @@ function setupCommander() {
959
1462
  program.help();
960
1463
  }
961
1464
 
962
- // Ensure docker/podman is available
963
- ensureDocker();
1465
+ const isInitConfigMode = process.argv.some(arg => arg === '--init-config' || arg.startsWith('--init-config='));
1466
+ // init-config 只处理本地文件,不依赖 docker/podman
1467
+ if (!isInitConfigMode) {
1468
+ // Ensure docker/podman is available
1469
+ ensureDocker();
1470
+ }
964
1471
 
965
1472
  // Pre-handle -x/--shell-full: treat all following args as a single command
966
1473
  const shellFullIndex = process.argv.findIndex(arg => arg === '-x' || arg === '--shell-full');
@@ -975,6 +1482,15 @@ function setupCommander() {
975
1482
 
976
1483
  const options = program.opts();
977
1484
 
1485
+ if (options.yes) {
1486
+ YES_MODE = true;
1487
+ }
1488
+
1489
+ if (options.initConfig !== undefined) {
1490
+ await initAgentConfigs(options.initConfig);
1491
+ process.exit(0);
1492
+ }
1493
+
978
1494
  // Load run config if specified
979
1495
  const runConfig = options.run ? loadRunConfig(options.run) : {};
980
1496
 
@@ -984,6 +1500,7 @@ function setupCommander() {
984
1500
  if (options.contName || runConfig.containerName || config.containerName) {
985
1501
  CONTAINER_NAME = options.contName || runConfig.containerName || config.containerName;
986
1502
  }
1503
+ CONTAINER_NAME = resolveContainerNameTemplate(CONTAINER_NAME);
987
1504
  if (options.contPath || runConfig.containerPath || config.containerPath) {
988
1505
  CONTAINER_PATH = options.contPath || runConfig.containerPath || config.containerPath;
989
1506
  }
@@ -1059,7 +1576,9 @@ function setupCommander() {
1059
1576
 
1060
1577
  if (options.server !== undefined) {
1061
1578
  SERVER_MODE = true;
1062
- SERVER_PORT = parseServerPort(options.server);
1579
+ const serverListen = parseServerListen(options.server);
1580
+ SERVER_HOST = serverListen.host;
1581
+ SERVER_PORT = serverListen.port;
1063
1582
  }
1064
1583
 
1065
1584
  const serverUserValue = options.serverUser || runConfig.serverUser || config.serverUser || process.env.MANYOYO_SERVER_USER;
@@ -1095,6 +1614,7 @@ function setupCommander() {
1095
1614
  yolo: yoloValue || "",
1096
1615
  quiet: quietValue || [],
1097
1616
  server: SERVER_MODE,
1617
+ serverHost: SERVER_MODE ? SERVER_HOST : null,
1098
1618
  serverPort: SERVER_MODE ? SERVER_PORT : null,
1099
1619
  serverUser: SERVER_AUTH_USER || "",
1100
1620
  serverPass: SERVER_AUTH_PASS || "",
@@ -1371,6 +1891,7 @@ async function runWebServerMode() {
1371
1891
  ensureWebServerAuthCredentials();
1372
1892
 
1373
1893
  await startWebServer({
1894
+ serverHost: SERVER_HOST,
1374
1895
  serverPort: SERVER_PORT,
1375
1896
  authUser: SERVER_AUTH_USER,
1376
1897
  authPass: SERVER_AUTH_PASS,
@@ -1414,7 +1935,7 @@ async function runWebServerMode() {
1414
1935
  async function main() {
1415
1936
  try {
1416
1937
  // 1. Setup commander and parse arguments
1417
- setupCommander();
1938
+ await setupCommander();
1418
1939
 
1419
1940
  // 2. Start web server mode
1420
1941
  if (SERVER_MODE) {