@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 +325 -1016
- package/docker/manyoyo.Dockerfile +36 -39
- package/lib/agent-resume.js +72 -0
- package/lib/container-run.js +39 -0
- package/lib/image-build.js +323 -0
- package/lib/init-config.js +401 -0
- package/lib/web/frontend/app.css +420 -190
- package/lib/web/frontend/app.html +71 -4
- package/lib/web/frontend/app.js +840 -136
- package/lib/web/frontend/login.css +77 -63
- package/lib/web/server.js +757 -128
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
539
|
+
if (normalizedMode === 'dind') {
|
|
540
|
+
CONT_MODE_ARGS = ['--privileged'];
|
|
541
|
+
console.log(`${GREEN}✅ 开启安全的容器嵌套容器模式, 手动在容器内启动服务: nohup dockerd &${NC}`);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
1033
544
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
1377
|
-
const
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
1569
|
-
|
|
828
|
+
const mergedContainerPath = pickConfigValue(options.contPath, runConfig.containerPath, config.containerPath);
|
|
829
|
+
if (mergedContainerPath) {
|
|
830
|
+
CONTAINER_PATH = mergedContainerPath;
|
|
1570
831
|
}
|
|
1571
|
-
IMAGE_NAME = options.imageName
|
|
1572
|
-
|
|
1573
|
-
|
|
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
|
-
|
|
1576
|
-
|
|
837
|
+
const mergedShellPrefix = pickConfigValue(options.shellPrefix, runConfig.shellPrefix, config.shellPrefix);
|
|
838
|
+
if (mergedShellPrefix) {
|
|
839
|
+
EXEC_COMMAND_PREFIX = `${mergedShellPrefix} `;
|
|
1577
840
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
841
|
+
const mergedShell = pickConfigValue(options.shell, runConfig.shell, config.shell);
|
|
842
|
+
if (mergedShell) {
|
|
843
|
+
EXEC_COMMAND = mergedShell;
|
|
1580
844
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
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 =
|
|
872
|
+
const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume);
|
|
1608
873
|
volumeList.forEach(v => addVolume(v));
|
|
1609
874
|
|
|
1610
|
-
const buildArgList =
|
|
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
|
|
879
|
+
const yoloValue = pickConfigValue(options.yolo, runConfig.yolo, config.yolo);
|
|
1615
880
|
if (yoloValue) setYolo(yoloValue);
|
|
1616
881
|
|
|
1617
|
-
const contModeValue = options.contMode
|
|
882
|
+
const contModeValue = pickConfigValue(options.contMode, runConfig.containerMode, config.containerMode);
|
|
1618
883
|
if (contModeValue) setContMode(contModeValue);
|
|
1619
884
|
|
|
1620
|
-
const quietValue = options.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
|
|
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
|
|
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
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
-
|
|
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(
|
|
1732
|
-
console.log(`${RED}⚠️ 错误: 宿主机路径不存在: ${
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1789
|
-
|
|
1078
|
+
runtime.execCommand = joinExecCommand(
|
|
1079
|
+
runtime.execCommandPrefix,
|
|
1080
|
+
runtime.execCommand,
|
|
1081
|
+
runtime.execCommandSuffix
|
|
1082
|
+
);
|
|
1083
|
+
const defaultCommand = runtime.execCommand;
|
|
1790
1084
|
|
|
1791
|
-
if (
|
|
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(
|
|
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
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
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 (
|
|
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(
|
|
1138
|
+
const status = getContainerStatus(runtime.containerName);
|
|
1857
1139
|
if (status !== 'running') {
|
|
1858
|
-
dockerExecArgs(['start',
|
|
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"}}',
|
|
1144
|
+
const defaultCommand = dockerExecArgs(['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}', runtime.containerName]).trim();
|
|
1863
1145
|
|
|
1864
|
-
if (!
|
|
1865
|
-
|
|
1146
|
+
if (!runtime.execCommand) {
|
|
1147
|
+
runtime.execCommand = joinExecCommand(runtime.execCommandPrefix, defaultCommand, runtime.execCommandSuffix);
|
|
1866
1148
|
} else {
|
|
1867
|
-
|
|
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 (
|
|
1875
|
-
if (containerExists(
|
|
1876
|
-
const defaultCommand = dockerExecArgs(['inspect', '-f', '{{index .Config.Labels "manyoyo.default_cmd"}}',
|
|
1877
|
-
const execCmd =
|
|
1878
|
-
?
|
|
1879
|
-
:
|
|
1880
|
-
console.log(`${DOCKER_CMD} exec -it ${
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1896
|
-
|
|
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}${
|
|
1189
|
+
console.log(`💻 执行命令: ${YELLOW}${runtime.execCommand || '交互式 Shell'}${NC}`);
|
|
1899
1190
|
}
|
|
1900
1191
|
|
|
1901
1192
|
// Execute command in container
|
|
1902
|
-
if (
|
|
1903
|
-
spawnSync(`${DOCKER_CMD}`, ['exec', '-it',
|
|
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',
|
|
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 (
|
|
1916
|
-
removeContainer(
|
|
1917
|
-
return;
|
|
1206
|
+
if (runtime.rmOnExit) {
|
|
1207
|
+
removeContainer(runtime.containerName);
|
|
1208
|
+
return false;
|
|
1918
1209
|
}
|
|
1919
1210
|
|
|
1920
|
-
getHelloTip(
|
|
1211
|
+
getHelloTip(runtime.containerName, defaultCommand, runtime.execCommand);
|
|
1921
1212
|
|
|
1922
|
-
let tipAskKeep = `❔ 会话已结束。是否保留此后台容器 ${
|
|
1923
|
-
if (
|
|
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(
|
|
1220
|
+
removeContainer(runtime.containerName);
|
|
1221
|
+
return false;
|
|
1930
1222
|
} else if (firstChar === '1') {
|
|
1931
|
-
if (
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
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 (
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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 (
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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}✅ 已退出连接。容器 ${
|
|
1242
|
+
console.log(`${GREEN}✅ 已退出连接。容器 ${runtime.containerName} 仍在后台运行。${NC}`);
|
|
1243
|
+
return false;
|
|
1952
1244
|
}
|
|
1953
1245
|
}
|
|
1954
1246
|
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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:
|
|
1964
|
-
serverPort:
|
|
1965
|
-
authUser:
|
|
1966
|
-
authPass:
|
|
1967
|
-
authPassAuto:
|
|
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:
|
|
1970
|
-
containerPath:
|
|
1971
|
-
imageName:
|
|
1972
|
-
imageVersion:
|
|
1973
|
-
execCommandPrefix:
|
|
1974
|
-
execCommand:
|
|
1975
|
-
execCommandSuffix:
|
|
1976
|
-
contModeArgs:
|
|
1977
|
-
containerEnvs:
|
|
1978
|
-
containerVolumes:
|
|
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 (
|
|
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(
|
|
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
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
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}`);
|