@xcanwin/manyoyo 7.1.6 → 8.1.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/README.md +33 -169
- package/bin/manyoyo.js +604 -231
- package/docker/manyoyo.Dockerfile +10 -4
- package/lib/app-launcher.js +220 -0
- package/lib/app-update.js +324 -0
- package/lib/container-run.js +3 -0
- package/lib/container-runtime.js +121 -0
- package/lib/doctor.js +77 -14
- package/lib/download-verified.js +48 -0
- package/lib/error-hints.js +96 -0
- package/lib/global-config.js +4 -4
- package/lib/headless.js +53 -0
- package/lib/image-build.js +11 -9
- package/lib/image-prune.js +44 -0
- package/lib/image-pull.js +76 -0
- package/lib/image-version-policy.js +26 -0
- package/lib/init-config.js +79 -19
- package/lib/login-token.js +92 -0
- package/lib/mirrors.js +71 -0
- package/lib/offline-import.js +60 -0
- package/lib/plugin/playwright.js +15 -3
- package/lib/podman-passthrough.js +80 -0
- package/lib/proxy-config.js +138 -0
- package/lib/runtime-heal.js +175 -0
- package/lib/runtime-normalizers.js +50 -0
- package/lib/secure-file.js +38 -0
- package/lib/setup-cli.js +215 -0
- package/lib/setup-config.js +32 -0
- package/lib/setup.js +188 -0
- package/lib/uninstall.js +296 -0
- package/lib/update-check.js +91 -0
- package/lib/web/AGENTS.md +4 -6
- package/lib/web/index.html +213 -0
- package/lib/web/server.js +653 -51
- package/lib/worktrees.js +15 -0
- package/manyoyo.example.json +14 -3
- package/package.json +14 -10
- package/lib/web/frontend/shadcn.html +0 -213
package/bin/manyoyo.js
CHANGED
|
@@ -7,15 +7,30 @@ const os = require('os');
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
const net = require('net');
|
|
9
9
|
const readline = require('readline');
|
|
10
|
-
const { Command } = require('commander');
|
|
10
|
+
const { Command, Help } = require('commander');
|
|
11
11
|
const { startWebServer } = require('../lib/web/server');
|
|
12
12
|
const { buildContainerRunArgs, buildContainerRunCommand } = require('../lib/container-run');
|
|
13
13
|
const { getManyoyoConfigPath, readManyoyoConfig, syncGlobalImageVersion } = require('../lib/global-config');
|
|
14
|
+
const { resolveUpdateImageVersion } = require('../lib/image-version-policy');
|
|
15
|
+
const { selectContainerRuntime, mergeRuntimeEnv } = require('../lib/container-runtime');
|
|
16
|
+
const { describeError } = require('../lib/error-hints');
|
|
17
|
+
const { ensureRuntimeReady, runCommandAsync } = require('../lib/runtime-heal');
|
|
18
|
+
const { ensureImagePresent, pullImageProcess } = require('../lib/image-pull');
|
|
19
|
+
const { readImportState, waitForImport } = require('../lib/offline-import');
|
|
20
|
+
const { runPodmanCommand } = require('../lib/podman-passthrough');
|
|
21
|
+
const { detectHeadless, parseLauncherArgs } = require('../lib/headless');
|
|
22
|
+
const { runSetupCli, createPrompter } = require('../lib/setup-cli');
|
|
23
|
+
const { pruneDanglingImages: pruneDanglingImagesSafely } = require('../lib/image-prune');
|
|
24
|
+
const { runUninstall, readPid, defaultIsManyoyoServe, defaultKill } = require('../lib/uninstall');
|
|
25
|
+
const appUpdate = require('../lib/app-update');
|
|
26
|
+
const { createUpdateChecker } = require('../lib/update-check');
|
|
27
|
+
const { getLoginTokenDir, issueLoginToken } = require('../lib/login-token');
|
|
28
|
+
const { launchApp, getAppStatePath, openBrowser } = require('../lib/app-launcher');
|
|
14
29
|
const { initAgentConfigs } = require('../lib/init-config');
|
|
15
30
|
const { buildImage } = require('../lib/image-build');
|
|
16
31
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
32
|
const { resolveYoloCommand } = require('../lib/agent-adapters');
|
|
18
|
-
const { runDoctorChecks } = require('../lib/doctor');
|
|
33
|
+
const { runDoctorChecks, applyDoctorFixes } = require('../lib/doctor');
|
|
19
34
|
const { resolveContainerMode } = require('../lib/container-modes');
|
|
20
35
|
const { runPluginCommand, createPlugin } = require('../lib/plugin');
|
|
21
36
|
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
@@ -23,8 +38,10 @@ const { resolveRuntimeConfig } = require('../lib/runtime-resolver');
|
|
|
23
38
|
const { resolveWorktreeSupport } = require('../lib/worktrees');
|
|
24
39
|
const {
|
|
25
40
|
parseEnvEntry: parseEnvEntryOrThrow,
|
|
26
|
-
normalizeVolume
|
|
41
|
+
normalizeVolume,
|
|
42
|
+
normalizeMirrors
|
|
27
43
|
} = require('../lib/runtime-normalizers');
|
|
44
|
+
const { applyAptMirror } = require('../lib/mirrors');
|
|
28
45
|
const {
|
|
29
46
|
sanitizeSensitiveData,
|
|
30
47
|
sanitizeServeLogText,
|
|
@@ -33,8 +50,7 @@ const {
|
|
|
33
50
|
} = require('../lib/serve-log');
|
|
34
51
|
const { version: BIN_VERSION, imageVersion: IMAGE_VERSION_DEFAULT } = require('../package.json');
|
|
35
52
|
const IMAGE_VERSION_BASE = String(IMAGE_VERSION_DEFAULT || '1.0.0').split('-')[0];
|
|
36
|
-
const
|
|
37
|
-
const IMAGE_VERSION_HELP_EXAMPLE = GLOBAL_CONFIG_IMAGE_VERSION || IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
53
|
+
const IMAGE_VERSION_HELP_EXAMPLE = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
38
54
|
|
|
39
55
|
// Helper function to format date like bash $(date +%m%d-%H%M)
|
|
40
56
|
function formatDate() {
|
|
@@ -47,14 +63,13 @@ function formatDate() {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
function detectCommandName() {
|
|
66
|
+
// 离线安装器生成的 manyoyo / my 包装脚本都指向同一个 manyoyo.js,用环境变量区分显示的命令名
|
|
67
|
+
const fromEnv = String(process.env.MANYOYO_COMMAND_NAME || '').trim();
|
|
68
|
+
if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(fromEnv)) return fromEnv;
|
|
69
|
+
|
|
50
70
|
const rawArgv1 = process.argv[1] || '';
|
|
51
71
|
const baseName = path.basename(rawArgv1).replace(/\.(cjs|mjs|js)$/i, '');
|
|
52
72
|
|
|
53
|
-
if (baseName === 'docker-manyoyo') {
|
|
54
|
-
const pluginCommand = String(process.argv[2] || '').trim();
|
|
55
|
-
return pluginCommand || 'manyoyo';
|
|
56
|
-
}
|
|
57
|
-
|
|
58
73
|
return baseName || 'manyoyo';
|
|
59
74
|
}
|
|
60
75
|
|
|
@@ -68,7 +83,7 @@ const CONFIG = {
|
|
|
68
83
|
let CONTAINER_NAME = `my-${formatDate()}`;
|
|
69
84
|
let HOST_PATH = process.cwd();
|
|
70
85
|
let CONTAINER_PATH = HOST_PATH;
|
|
71
|
-
let IMAGE_NAME = "
|
|
86
|
+
let IMAGE_NAME = "ghcr.io/xcanwin/manyoyo";
|
|
72
87
|
let IMAGE_VERSION = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
73
88
|
let EXEC_COMMAND = "";
|
|
74
89
|
let EXEC_COMMAND_PREFIX = "";
|
|
@@ -105,6 +120,16 @@ const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)
|
|
|
105
120
|
|
|
106
121
|
// Docker command (will be set by ensure_docker)
|
|
107
122
|
let DOCKER_CMD = 'docker';
|
|
123
|
+
// 仅运行时子进程使用的完整 env(私有 Podman 才有值),不要写回 process.env
|
|
124
|
+
let DOCKER_ENV;
|
|
125
|
+
let CONTAINER_RUNTIME = null;
|
|
126
|
+
// 全局配置 updateCheck(默认 true):serve 是否每天检查一次新版本
|
|
127
|
+
let UPDATE_CHECK_ENABLED = true;
|
|
128
|
+
// 全局配置 mirrors(apt/npm/pip 软件源,空 = 官方默认):容器创建时在容器层生效,镜像不变
|
|
129
|
+
let MIRRORS = { apt: '', npm: '', pip: '' };
|
|
130
|
+
// serve 的容器环境状态,供 GET /api/system/runtime 读取
|
|
131
|
+
const RUNTIME_STATE = { status: 'ready', message: '' };
|
|
132
|
+
const DOCKER_DAEMON_ERROR_CODES = new Set(['PODMAN_MACHINE_UNAVAILABLE', 'DOCKER_DAEMON_UNAVAILABLE', 'PORT_IN_USE']);
|
|
108
133
|
const SUPPORTED_INIT_AGENTS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
109
134
|
|
|
110
135
|
function sleep(ms) {
|
|
@@ -312,6 +337,9 @@ function installServeProcessDiagnostics(logger) {
|
|
|
312
337
|
* @property {Object.<string, Object>} [runs] - 运行配置映射(-r <name>)
|
|
313
338
|
* @property {string} [yolo] - YOLO 模式
|
|
314
339
|
* @property {string} [containerMode] - 容器模式
|
|
340
|
+
* @property {string} [containerRuntime] - 容器运行时(auto/docker/podman,默认 auto;仅全局配置生效)
|
|
341
|
+
* @property {{apt?: string, npm?: string, pip?: string}} [mirrors] - 容器内 apt/npm/pip 软件源(http/https URL,空/缺省为官方默认;仅全局配置生效)
|
|
342
|
+
* @property {boolean} [updateCheck] - serve 是否每天检查一次新版本(默认 true;仅全局配置生效,请求不附带任何本机信息)
|
|
315
343
|
* @property {number} [cacheTTL] - 缓存过期天数
|
|
316
344
|
* @property {string} [nodeMirror] - Node.js 镜像源
|
|
317
345
|
*/
|
|
@@ -438,7 +466,7 @@ function parseImageVersionTag(version) {
|
|
|
438
466
|
function validateImageVersion(value) {
|
|
439
467
|
validateName('imageVersion', value, /^[A-Za-z0-9][A-Za-z0-9_.-]*$/);
|
|
440
468
|
if (!parseImageVersionTag(value)) {
|
|
441
|
-
console.error(`${RED}⚠️ 错误: imageVersion 格式必须为 <x.y.z-后缀>,例如
|
|
469
|
+
console.error(`${RED}⚠️ 错误: imageVersion 格式必须为 <x.y.z-后缀>,例如 ${IMAGE_VERSION_HELP_EXAMPLE}。当前值: ${value}${NC}`);
|
|
442
470
|
process.exit(1);
|
|
443
471
|
}
|
|
444
472
|
}
|
|
@@ -672,17 +700,39 @@ function setContMode(mode) {
|
|
|
672
700
|
}
|
|
673
701
|
}
|
|
674
702
|
|
|
703
|
+
// 本地没有镜像时自动拉取(终端显示拉取进度),失败给出原因与下一步
|
|
704
|
+
async function ensureRunImage(runtime) {
|
|
705
|
+
const imageRef = `${runtime.imageName}:${runtime.imageVersion}`;
|
|
706
|
+
// 离线安装器还在后台导入镜像时先等它,避免重复去仓库拉
|
|
707
|
+
await waitForImport({ onWait: state => console.log(`${YELLOW}⏳ ${state.message},等待完成...${NC}`) });
|
|
708
|
+
await ensureImagePresent({
|
|
709
|
+
imageRef,
|
|
710
|
+
command: DOCKER_CMD,
|
|
711
|
+
isPresent: () => {
|
|
712
|
+
try {
|
|
713
|
+
dockerExecArgs(['image', 'inspect', imageRef], { stdio: 'pipe' });
|
|
714
|
+
return true;
|
|
715
|
+
} catch (e) {
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
pull: () => pullImageProcess({
|
|
720
|
+
command: DOCKER_CMD,
|
|
721
|
+
env: DOCKER_ENV,
|
|
722
|
+
imageRef,
|
|
723
|
+
onOutput: (text, stream) => (stream === 'stderr' ? process.stderr : process.stdout).write(text)
|
|
724
|
+
}),
|
|
725
|
+
onStart: () => console.log(`${YELLOW}⏬ 本地没有镜像 ${imageRef},正在拉取...${NC}`)
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
|
|
675
729
|
function showImagePullHint(err) {
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
const message = err && err.message ? err.message : '';
|
|
679
|
-
const combined = `${message}\n${stderr}\n${stdout}`;
|
|
680
|
-
if (!/localhost\/v2|pinging container registry localhost|connection refused|dial tcp .*:443/i.test(combined)) {
|
|
730
|
+
const info = describeError(getCommandFailureText(err), { imageRef: `${IMAGE_NAME}:${IMAGE_VERSION}` });
|
|
731
|
+
if (!info || !['IMAGE_PULL_FAILED', 'IMAGE_NOT_FOUND'].includes(info.code)) {
|
|
681
732
|
return;
|
|
682
733
|
}
|
|
683
|
-
|
|
684
|
-
console.log(`${YELLOW}
|
|
685
|
-
console.log(`${YELLOW} 你可以: (1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion。 (2) 或先执行 ${MANYOYO_NAME} build --iv <x.y.z-后缀> 构建镜像。${NC}`);
|
|
734
|
+
console.log(`${YELLOW}💡 提示: ${info.reason}${NC}`);
|
|
735
|
+
console.log(`${YELLOW} ${info.action}${NC}`);
|
|
686
736
|
}
|
|
687
737
|
|
|
688
738
|
function getCommandFailureText(err) {
|
|
@@ -692,25 +742,6 @@ function getCommandFailureText(err) {
|
|
|
692
742
|
return `${message}\n${stderr}\n${stdout}`;
|
|
693
743
|
}
|
|
694
744
|
|
|
695
|
-
function getContainerRuntimeUnavailableHint(command, err) {
|
|
696
|
-
const text = getCommandFailureText(err);
|
|
697
|
-
if (/Cannot connect to Podman|unable to connect to Podman socket|podman machine start|podman system connection/i.test(text)) {
|
|
698
|
-
return [
|
|
699
|
-
'',
|
|
700
|
-
`提示: 当前 ${command} 命令正在连接 Podman machine,但连接不可用。`,
|
|
701
|
-
'请先在宿主机执行: podman machine start'
|
|
702
|
-
].join('\n');
|
|
703
|
-
}
|
|
704
|
-
if (/Cannot connect to the Docker daemon|docker daemon is not running|Is the docker daemon running/i.test(text)) {
|
|
705
|
-
return [
|
|
706
|
-
'',
|
|
707
|
-
`提示: 当前 ${command} 命令无法连接容器运行时。`,
|
|
708
|
-
'请先启动 Docker Desktop / Docker daemon,或确认 Podman machine 已启动。'
|
|
709
|
-
].join('\n');
|
|
710
|
-
}
|
|
711
|
-
return '';
|
|
712
|
-
}
|
|
713
|
-
|
|
714
745
|
function runCmd(cmd, args, options = {}) {
|
|
715
746
|
const result = spawnSync(cmd, args, { encoding: 'utf-8', ...options });
|
|
716
747
|
if (result.error) {
|
|
@@ -739,11 +770,12 @@ function checkPortAvailability(port) {
|
|
|
739
770
|
|
|
740
771
|
function dockerExecArgs(args, options = {}) {
|
|
741
772
|
try {
|
|
742
|
-
return runCmd(DOCKER_CMD, args, options);
|
|
773
|
+
return runCmd(DOCKER_CMD, args, { env: DOCKER_ENV, ...options });
|
|
743
774
|
} catch (e) {
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
746
|
-
|
|
775
|
+
const info = describeError(getCommandFailureText(e), { command: DOCKER_CMD });
|
|
776
|
+
if (info && DOCKER_DAEMON_ERROR_CODES.has(info.code) && e && e.message) {
|
|
777
|
+
const hint = `\n提示: ${info.reason}\n${info.action}`;
|
|
778
|
+
if (!e.message.includes(hint)) e.message = `${e.message}${hint}`;
|
|
747
779
|
}
|
|
748
780
|
throw e;
|
|
749
781
|
}
|
|
@@ -764,40 +796,169 @@ function removeContainer(name) {
|
|
|
764
796
|
if ( !(QUIET.crm || QUIET.full) ) console.log(`${GREEN}✅ 已彻底删除。${NC}`);
|
|
765
797
|
}
|
|
766
798
|
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
799
|
+
// 运行时子进程执行器:env 只是增量,合并后仅传给这个子进程
|
|
800
|
+
function runRuntimeCommand(command, args, options = {}) {
|
|
801
|
+
return runCmd(command, args, {
|
|
802
|
+
stdio: 'pipe',
|
|
803
|
+
timeout: options.timeout,
|
|
804
|
+
env: mergeRuntimeEnv(options.env)
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// serve 里的自愈用异步执行器:machine start 最长 120s,同步执行会冻住整个事件循环(向导进度、登录页都没响应)
|
|
809
|
+
function runRuntimeCommandAsync(command, args, options = {}) {
|
|
810
|
+
return runCommandAsync(command, args, { timeout: options.timeout, env: mergeRuntimeEnv(options.env) });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function healContainerRuntime(onStatus, options = {}) {
|
|
814
|
+
return ensureRuntimeReady({ runtime: CONTAINER_RUNTIME, run: options.async ? runRuntimeCommandAsync : runRuntimeCommand, onStatus });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// 选择阶段已确认 daemon 可用(info 通过)时无需再自愈
|
|
818
|
+
function isRuntimeProven() {
|
|
819
|
+
return /-daemon$/.test(CONTAINER_RUNTIME.source);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async function ensureDocker(configuredRuntime, options = {}) {
|
|
823
|
+
try {
|
|
824
|
+
CONTAINER_RUNTIME = selectContainerRuntime({ configured: configuredRuntime });
|
|
825
|
+
} catch (e) {
|
|
826
|
+
console.error(e.message);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
DOCKER_CMD = CONTAINER_RUNTIME.command;
|
|
830
|
+
DOCKER_ENV = mergeRuntimeEnv(CONTAINER_RUNTIME.env);
|
|
831
|
+
if (options.deferHeal || isRuntimeProven()) {
|
|
832
|
+
return true;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
const result = await healContainerRuntime(state => {
|
|
836
|
+
if (state.status === 'starting') console.log(`${YELLOW}⏳ ${state.message}...${NC}`);
|
|
837
|
+
});
|
|
838
|
+
if (result.status === 'started') {
|
|
839
|
+
console.log(`${GREEN}✅ 容器环境已就绪${NC}`);
|
|
840
|
+
} else if (result.status !== 'ready') {
|
|
841
|
+
console.log(`${YELLOW}⚠️ ${result.message}${NC}`);
|
|
842
|
+
}
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// 升级后让旧版本的后台服务退出(pid 必须确实是 manyoyo serve),下次执行 manyoyo 就会启动新版本
|
|
847
|
+
function stopBackgroundApp({ quiet = false } = {}) {
|
|
848
|
+
const pid = readPid(path.join(os.homedir(), '.manyoyo', 'serve', 'app.json'));
|
|
849
|
+
if (pid && pid !== process.pid && defaultIsManyoyoServe(pid) && defaultKill(pid)) {
|
|
850
|
+
if (!quiet) console.log(`${GREEN}✅ 已停止旧版本的后台服务 (pid ${pid}),下次执行 ${MANYOYO_NAME} 会启动新版本。${NC}`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// 升级流程里准备容器运行时:和 ensureDocker 一样选择并(必要时)自愈,但没有运行时也不退出进程
|
|
855
|
+
async function prepareRuntimeForUpdate(configuredRuntime) {
|
|
856
|
+
try {
|
|
857
|
+
CONTAINER_RUNTIME = selectContainerRuntime({ configured: configuredRuntime });
|
|
858
|
+
} catch (error) {
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
DOCKER_CMD = CONTAINER_RUNTIME.command;
|
|
862
|
+
DOCKER_ENV = mergeRuntimeEnv(CONTAINER_RUNTIME.env);
|
|
863
|
+
if (!isRuntimeProven()) {
|
|
864
|
+
const result = await healContainerRuntime(state => {
|
|
865
|
+
if (state.status === 'starting') console.log(`${YELLOW}⏳ ${state.message}...${NC}`);
|
|
866
|
+
});
|
|
867
|
+
if (result.status !== 'ready' && result.status !== 'started') {
|
|
868
|
+
console.log(`${YELLOW}⚠️ ${result.message}${NC}`);
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function updateOfflineInstall(mode, options, globalConfig) {
|
|
876
|
+
const { appRoot } = mode;
|
|
877
|
+
if (options.rollback) {
|
|
878
|
+
const result = appUpdate.rollbackApp({ appRoot });
|
|
879
|
+
console.log(`${GREEN}✅ 已回滚: ${result.from || '未知'} → ${result.to}${NC}`);
|
|
880
|
+
stopBackgroundApp();
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const installed = appUpdate.currentVersion(appRoot) || mode.version;
|
|
885
|
+
console.log(`${CYAN}🔄 当前版本: ${installed}${NC}`);
|
|
886
|
+
console.log(`${CYAN}🔄 正在查询最新版本...${NC}`);
|
|
887
|
+
const release = await appUpdate.fetchLatestRelease();
|
|
888
|
+
if (appUpdate.compareVersions(release.version, installed) <= 0) {
|
|
889
|
+
console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
console.log(`${CYAN}⬇️ 发现新版本 ${release.version},只下载 manyoyo 本体(数十 MB)${NC}`);
|
|
893
|
+
const result = await appUpdate.installAppUpdate({ appRoot, release, targetOs: appUpdate.platformOs(), log: line => console.log(` ${line}`) });
|
|
894
|
+
console.log(`${GREEN}✅ 更新完成: ${installed} → ${result.version}(上一版本 ${result.previous || '无'} 已保留,可用 ${MANYOYO_NAME} update --rollback 回滚)${NC}`);
|
|
895
|
+
|
|
896
|
+
// Podman / VM 磁盘有变化只提示(需要新的完整包)
|
|
897
|
+
const runtimeHint = appUpdate.describeRuntimeChange(appUpdate.readInstalledRecord(os.homedir()), result.manifest.runtime);
|
|
898
|
+
if (runtimeHint) console.log(`${YELLOW}ℹ️ ${runtimeHint}${NC}`);
|
|
899
|
+
|
|
900
|
+
// 新版本要求的镜像不存在就拉取;仍用旧镜像的容器只列出来,不动
|
|
901
|
+
const imageName = String(globalConfig.imageName || 'ghcr.io/xcanwin/manyoyo');
|
|
902
|
+
const versionPolicy = resolveUpdateImageVersion({
|
|
903
|
+
configured: globalConfig.imageVersion,
|
|
904
|
+
previousDefault: require('../package.json').imageVersion,
|
|
905
|
+
nextDefault: result.manifest.imageVersion
|
|
906
|
+
});
|
|
907
|
+
const imageVersion = versionPolicy.imageVersion;
|
|
908
|
+
if (versionPolicy.advance) {
|
|
909
|
+
const synced = syncGlobalImageVersion(imageVersion);
|
|
910
|
+
console.log(`${CYAN}ℹ️ 配置里的 imageVersion(${globalConfig.imageVersion})是旧版本默认值,已随新版本更新为 ${imageVersion}${synced.updated ? '' : '(写入配置失败,请手动修改)'}${NC}`);
|
|
911
|
+
} else if (versionPolicy.pinned) {
|
|
912
|
+
console.log(`${YELLOW}ℹ️ 你在配置里固定了 imageVersion=${imageVersion},新版本默认是 ${result.manifest.imageVersion};如需使用新镜像请修改 ~/.manyoyo/manyoyo.json${NC}`);
|
|
913
|
+
}
|
|
914
|
+
if (imageVersion && await prepareRuntimeForUpdate(globalConfig.containerRuntime)) {
|
|
915
|
+
const imageRef = `${imageName}:${imageVersion}`;
|
|
770
916
|
try {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
917
|
+
await ensureImagePresent({
|
|
918
|
+
imageRef,
|
|
919
|
+
command: DOCKER_CMD,
|
|
920
|
+
isPresent: () => {
|
|
921
|
+
try {
|
|
922
|
+
dockerExecArgs(['image', 'inspect', imageRef], { stdio: 'pipe' });
|
|
923
|
+
return true;
|
|
924
|
+
} catch (e) {
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
pull: () => pullImageProcess({ command: DOCKER_CMD, env: DOCKER_ENV, imageRef, onOutput: (text, stream) => (stream === 'stderr' ? process.stderr : process.stdout).write(text) }),
|
|
929
|
+
onStart: () => console.log(`${YELLOW}⏬ 新版本需要镜像 ${imageRef},正在拉取...${NC}`)
|
|
930
|
+
});
|
|
931
|
+
} catch (error) {
|
|
932
|
+
console.log(`${YELLOW}⚠️ 镜像还没准备好:${error.message}${NC}`);
|
|
933
|
+
}
|
|
934
|
+
const outdated = appUpdate.findOutdatedContainers({ run: runCmd, runtime: { command: DOCKER_CMD, env: DOCKER_ENV }, imageRef });
|
|
935
|
+
if (outdated.length > 0) {
|
|
936
|
+
console.log(`${YELLOW}ℹ️ 有 ${outdated.length} 个容器还在用旧镜像:${outdated.map(item => `${item.name}(${item.image})`).join('、')}${NC}`);
|
|
937
|
+
console.log(`${YELLOW} 它们可以继续使用;想换成新镜像,请新建容器(会话历史与凭据都在宿主机上,不会丢)。${NC}`);
|
|
776
938
|
}
|
|
777
939
|
}
|
|
778
|
-
|
|
779
|
-
process.exit(1);
|
|
940
|
+
stopBackgroundApp();
|
|
780
941
|
}
|
|
781
942
|
|
|
782
|
-
function
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
943
|
+
async function updateManyoyo(options = {}, globalConfig = {}) {
|
|
944
|
+
const mode = appUpdate.detectInstallMode({ scriptPath: __filename });
|
|
945
|
+
if (mode.mode === 'offline') {
|
|
946
|
+
try {
|
|
947
|
+
await updateOfflineInstall(mode, options, globalConfig);
|
|
948
|
+
} catch (error) {
|
|
949
|
+
if (error instanceof appUpdate.UpdateError) {
|
|
950
|
+
console.error(`${RED}❌ ${error.message}${NC}`);
|
|
951
|
+
process.exit(1);
|
|
791
952
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (options.rollback) {
|
|
958
|
+
console.error(`${RED}❌ --rollback 只适用于离线包安装;npm 安装的版本请用 npm install -g @xcanwin/manyoyo@<版本> 回退。${NC}`);
|
|
959
|
+
process.exit(1);
|
|
796
960
|
}
|
|
797
|
-
process.exit(0);
|
|
798
|
-
}
|
|
799
961
|
|
|
800
|
-
function updateManyoyo() {
|
|
801
962
|
let isLocalFileInstall = false;
|
|
802
963
|
let currentVersion = 'unknown';
|
|
803
964
|
|
|
@@ -902,47 +1063,11 @@ function getImageList() {
|
|
|
902
1063
|
}
|
|
903
1064
|
|
|
904
1065
|
function pruneDanglingImages() {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
const imagesOutput = dockerExecArgs(['images', '-a', '--format', '{{.ID}} {{.Repository}}']);
|
|
911
|
-
const noneImages = imagesOutput
|
|
912
|
-
.split('\n')
|
|
913
|
-
.filter(line => line.includes('<none>'))
|
|
914
|
-
.map(line => line.split(' ')[0])
|
|
915
|
-
.filter(id => id);
|
|
916
|
-
|
|
917
|
-
if (noneImages.length > 0) {
|
|
918
|
-
console.log(`${YELLOW}清理剩余的 <none> 镜像 (${noneImages.length} 个)...${NC}`);
|
|
919
|
-
dockerExecArgs(['rmi', '-f', ...noneImages], { stdio: 'inherit' });
|
|
920
|
-
}
|
|
921
|
-
} catch (e) {
|
|
922
|
-
// Ignore errors if no <none> images found
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
console.log(`${GREEN}✅ 清理完成${NC}`);
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
function maybeHandleDockerPluginMetadata(argv) {
|
|
929
|
-
if (argv[2] !== 'docker-cli-plugin-metadata') {
|
|
930
|
-
return false;
|
|
931
|
-
}
|
|
932
|
-
console.log(JSON.stringify({
|
|
933
|
-
"SchemaVersion": "0.1.0",
|
|
934
|
-
"Vendor": "xcanwin",
|
|
935
|
-
"Version": "v1.0.0",
|
|
936
|
-
"Description": "AI Agent CLI Sandbox"
|
|
937
|
-
}, null, 4));
|
|
938
|
-
return true;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function normalizeDockerPluginArgv(argv) {
|
|
942
|
-
const dockerPluginPath = path.join(process.env.HOME || '', '.docker/cli-plugins/docker-manyoyo');
|
|
943
|
-
if (argv[1] === dockerPluginPath && argv[2] === 'manyoyo') {
|
|
944
|
-
argv.splice(2, 1);
|
|
945
|
-
}
|
|
1066
|
+
pruneDanglingImagesSafely({
|
|
1067
|
+
dockerExecArgs,
|
|
1068
|
+
log: line => console.log(line),
|
|
1069
|
+
colors: { YELLOW, GREEN, NC }
|
|
1070
|
+
});
|
|
946
1071
|
}
|
|
947
1072
|
|
|
948
1073
|
function normalizeShellFullArgv(argv) {
|
|
@@ -1006,7 +1131,7 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
1006
1131
|
.option('--cp, --cont-path <path>', '设置容器工作目录')
|
|
1007
1132
|
.option('-m, --cont-mode <mode>', '设置容器嵌套模式 (common, dind, sock; 注意: sock 模式可访问宿主机 Docker socket,风险较高)')
|
|
1008
1133
|
.option('--in, --image-name <name>', '指定镜像名称')
|
|
1009
|
-
.option('--iv, --image-ver <version>',
|
|
1134
|
+
.option('--iv, --image-ver <version>', `指定镜像版本 (格式: x.y.z-后缀,如 ${IMAGE_VERSION_HELP_EXAMPLE})`);
|
|
1010
1135
|
|
|
1011
1136
|
appendArrayOption(command, '-e, --env <env>', '设置环境变量 XXX=YYY (可多次使用)');
|
|
1012
1137
|
appendArrayOption(command, '--ef, --env-file <file>', '从环境文件加载变量 (仅支持绝对路径,如 /abs/path.env; 相对路径会报错)');
|
|
@@ -1023,7 +1148,7 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
1023
1148
|
.option('--first-shell <command>', '首次预执行命令 (仅新建容器生效; 容器已存在时忽略)')
|
|
1024
1149
|
.option('--first-shell-suffix <command>', '首次预执行命令后缀 (仅新建容器生效; 容器已存在时忽略)')
|
|
1025
1150
|
.option('-x, --shell-full <command...>', '完整命令 (与 --sp/-s/--ss/-- 互斥)')
|
|
1026
|
-
.option('-y, --yolo <cli>', '
|
|
1151
|
+
.option('-y, --yolo <cli>', '以免确认模式启动 Agent: c=Claude, cx=Codex, gm=Gemini, oc=OpenCode');
|
|
1027
1152
|
appendArrayOption(command, '--first-env <env>', '首次预执行环境变量 XXX=YYY (可多次使用)');
|
|
1028
1153
|
appendArrayOption(command, '--first-env-file <file>', '首次预执行环境变量文件 (仅支持绝对路径,如 /abs/path.env)');
|
|
1029
1154
|
|
|
@@ -1031,7 +1156,7 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
1031
1156
|
command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
|
|
1032
1157
|
}
|
|
1033
1158
|
|
|
1034
|
-
appendArrayOption(command, '-q, --quiet <item>', '
|
|
1159
|
+
appendArrayOption(command, '-q, --quiet <item>', '隐藏部分输出 (可多次使用,可选项见下方“-q 可选项”)');
|
|
1035
1160
|
|
|
1036
1161
|
if (includeServePreview) {
|
|
1037
1162
|
command
|
|
@@ -1136,46 +1261,82 @@ async function setupCommander() {
|
|
|
1136
1261
|
|
|
1137
1262
|
program
|
|
1138
1263
|
.name(MANYOYO_NAME)
|
|
1264
|
+
.optionsGroup('选项:')
|
|
1265
|
+
.commandsGroup('其他:')
|
|
1139
1266
|
.version(BIN_VERSION, '-v, --version', '显示版本')
|
|
1140
|
-
.
|
|
1267
|
+
.helpOption('-h, --help', '显示帮助')
|
|
1268
|
+
.helpCommand('help [command]', '显示指定命令的帮助')
|
|
1269
|
+
.configureHelp({
|
|
1270
|
+
optionDescription(option) {
|
|
1271
|
+
return Help.prototype.optionDescription.call(this, option).replace(/\s*\(default: \[\]\)/, '');
|
|
1272
|
+
}
|
|
1273
|
+
})
|
|
1274
|
+
.description(`MANYOYO - AI Agent CLI Sandbox
|
|
1275
|
+
https://github.com/xcanwin/manyoyo
|
|
1276
|
+
|
|
1277
|
+
不带参数直接运行,打开网页界面(首次使用进入配置向导):
|
|
1278
|
+
${MANYOYO_NAME} 自动判断有无图形界面
|
|
1279
|
+
${MANYOYO_NAME} --headless 强制按无图形界面处理(不打开浏览器,用密码登录)
|
|
1280
|
+
${MANYOYO_NAME} --gui 强制按有图形界面处理(自动打开浏览器)`)
|
|
1141
1281
|
.addHelpText('after', `
|
|
1142
|
-
配置文件:
|
|
1143
|
-
~/.manyoyo/manyoyo.json 全局配置文件 (JSON5格式,支持注释)
|
|
1144
|
-
~/.manyoyo/run/c.json 运行配置示例
|
|
1145
|
-
|
|
1146
|
-
路径规则:
|
|
1147
|
-
run -r name → ~/.manyoyo/manyoyo.json 的 runs.name
|
|
1148
|
-
run --ef /abs/path.env → 绝对路径环境文件
|
|
1149
|
-
run --ss "<args>" → 显式设置命令后缀
|
|
1150
|
-
run -- <args...> → 直接透传命令后缀(优先级最高)
|
|
1151
|
-
|
|
1152
1282
|
示例:
|
|
1153
|
-
${MANYOYO_NAME}
|
|
1154
|
-
${MANYOYO_NAME}
|
|
1155
|
-
${MANYOYO_NAME}
|
|
1156
|
-
${MANYOYO_NAME}
|
|
1157
|
-
${MANYOYO_NAME}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
${MANYOYO_NAME} run -x "echo 123" 使用完整命令
|
|
1162
|
-
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1163
|
-
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1164
|
-
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d 后台启动并监听全部网卡
|
|
1165
|
-
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P 123 -d --restart 重启指定后台网页服务
|
|
1166
|
-
${MANYOYO_NAME} playwright up mcp-host-headless 启动 playwright MCP 宿主场景(默认/推荐)
|
|
1167
|
-
${MANYOYO_NAME} playwright up cli-host-headless 启动 playwright CLI 宿主场景(供容器内 playwright-cli 附着)
|
|
1168
|
-
${MANYOYO_NAME} run -n test -q tip -q cmd 多次使用静默选项
|
|
1283
|
+
${MANYOYO_NAME} 打开网页界面
|
|
1284
|
+
${MANYOYO_NAME} init all 导入本机已有的 Agent 配置
|
|
1285
|
+
${MANYOYO_NAME} run -y c 在沙箱里以免确认模式启动 Claude Code
|
|
1286
|
+
${MANYOYO_NAME} run -r claude 使用 manyoyo.json 的 runs.claude 启动
|
|
1287
|
+
${MANYOYO_NAME} update 升级到最新版本
|
|
1288
|
+
|
|
1289
|
+
配置文件: ~/.manyoyo/manyoyo.json (JSON5 格式,支持注释)
|
|
1290
|
+
各命令的更多用法: ${MANYOYO_NAME} <命令> --help
|
|
1169
1291
|
`);
|
|
1170
1292
|
|
|
1171
|
-
|
|
1293
|
+
// 日常
|
|
1294
|
+
program.command('update')
|
|
1295
|
+
.helpGroup('日常:')
|
|
1296
|
+
.description('升级到最新版本')
|
|
1297
|
+
.option('--rollback', '回到上一版本')
|
|
1298
|
+
.action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback) }));
|
|
1299
|
+
|
|
1300
|
+
program.command('uninstall')
|
|
1301
|
+
.helpGroup('日常:')
|
|
1302
|
+
.description('卸载 MANYOYO(配置和数据默认保留)')
|
|
1303
|
+
.option('--yes', '确认卸载程序本身,不再询问(不会删除配置、历史、日志、工作目录和外部运行时里的容器镜像)')
|
|
1304
|
+
.action(options => selectAction('uninstall', options));
|
|
1305
|
+
|
|
1306
|
+
program.command('setup')
|
|
1307
|
+
.helpGroup('日常:')
|
|
1308
|
+
.description('命令行配置向导(无图形界面时使用)')
|
|
1309
|
+
.action(() => selectAction('setup', {}));
|
|
1310
|
+
|
|
1311
|
+
program.command('doctor')
|
|
1312
|
+
.helpGroup('日常:')
|
|
1313
|
+
.description('诊断容器运行时、镜像、配置和端口')
|
|
1314
|
+
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1315
|
+
.option('--port <port>', '检查指定监听端口')
|
|
1316
|
+
.option('--json', '以 JSON 输出稳定诊断结果')
|
|
1317
|
+
.option('--fix', '自动修复可修复项(启动容器环境、拉取镜像、生成默认配置,端口占用时给出建议端口)')
|
|
1318
|
+
.action(options => selectAction('doctor', { ...options, doctor: true }));
|
|
1319
|
+
|
|
1320
|
+
// 命令行运行
|
|
1321
|
+
const runCommand = program.command('run').helpGroup('命令行运行:').description('启动容器并运行命令(容器已存在则连接)');
|
|
1172
1322
|
runCommand.addHelpText('after', `
|
|
1173
|
-
|
|
1323
|
+
示例:
|
|
1174
1324
|
${MANYOYO_NAME} run -r codex
|
|
1325
|
+
${MANYOYO_NAME} run -n test --ef /path/ab.env -y c
|
|
1326
|
+
${MANYOYO_NAME} run -n test -- -c
|
|
1327
|
+
${MANYOYO_NAME} run -r codex --ss "resume --last"
|
|
1175
1328
|
${MANYOYO_NAME} run --rm-on-exit -x /bin/bash -lc "node -v"
|
|
1176
1329
|
${MANYOYO_NAME} run -n demo --first-shell "npm ci" -s "npm test"
|
|
1177
1330
|
|
|
1178
|
-
|
|
1331
|
+
-q 可选项(可多次使用):
|
|
1332
|
+
cnew 隐藏“创建/连接容器”提示
|
|
1333
|
+
crm 隐藏“删除容器”提示
|
|
1334
|
+
tip 隐藏首次命令与恢复会话提示
|
|
1335
|
+
cmd 隐藏将执行的命令
|
|
1336
|
+
askkeep 简化“是否保留容器”的提问
|
|
1337
|
+
full 隐藏以上全部
|
|
1338
|
+
|
|
1339
|
+
说明:
|
|
1179
1340
|
参数优先级与合并规则(标量覆盖、数组追加、env 按 key 合并)请用 ${MANYOYO_NAME} config show --help 或查看文档。
|
|
1180
1341
|
`);
|
|
1181
1342
|
applyRunStyleOptions(runCommand);
|
|
@@ -1185,30 +1346,85 @@ Notes:
|
|
|
1185
1346
|
selectAction('run', options);
|
|
1186
1347
|
});
|
|
1187
1348
|
|
|
1188
|
-
const
|
|
1349
|
+
const initCommand = program.command('init [agents]').helpGroup('命令行运行:').description('导入本机已有的 Agent 配置到 ~/.manyoyo');
|
|
1350
|
+
initCommand
|
|
1351
|
+
.option('--yes', '所有提示自动确认 (用于CI/脚本)')
|
|
1352
|
+
.action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
|
|
1353
|
+
|
|
1354
|
+
const configCommand = program.command('config').helpGroup('命令行运行:').description('查看生效的配置或将执行的命令');
|
|
1355
|
+
const configShowCommand = configCommand.command('show').description('显示最终生效的配置');
|
|
1356
|
+
applyRunStyleOptions(configShowCommand, { includeRmOnExit: false, includeServePreview: true });
|
|
1357
|
+
enableShellSuffixPassThrough(configShowCommand);
|
|
1358
|
+
configShowCommand.action((options, command) => {
|
|
1359
|
+
validateShellSuffixPassThroughArgs(command);
|
|
1360
|
+
const finalOptions = {
|
|
1361
|
+
...options,
|
|
1362
|
+
showConfig: true
|
|
1363
|
+
};
|
|
1364
|
+
if (options.serve !== undefined) {
|
|
1365
|
+
finalOptions.server = options.serve;
|
|
1366
|
+
finalOptions.serverUser = options.user;
|
|
1367
|
+
finalOptions.serverPass = options.pass;
|
|
1368
|
+
}
|
|
1369
|
+
selectAction('config-show', finalOptions);
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
const configRunCommand = configCommand.command('command').description('显示将执行的容器命令');
|
|
1373
|
+
applyRunStyleOptions(configRunCommand, { includeRmOnExit: false });
|
|
1374
|
+
enableShellSuffixPassThrough(configRunCommand);
|
|
1375
|
+
configRunCommand.action((options, command) => {
|
|
1376
|
+
validateShellSuffixPassThroughArgs(command);
|
|
1377
|
+
selectAction('config-command', options);
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
// 容器与镜像
|
|
1381
|
+
program.command('ps')
|
|
1382
|
+
.helpGroup('容器与镜像:')
|
|
1383
|
+
.description('列出容器')
|
|
1384
|
+
.action(() => selectAction('ps', { contList: true }));
|
|
1385
|
+
|
|
1386
|
+
program.command('images')
|
|
1387
|
+
.helpGroup('容器与镜像:')
|
|
1388
|
+
.description('列出镜像')
|
|
1389
|
+
.action(() => selectAction('images', { imageList: true }));
|
|
1390
|
+
|
|
1391
|
+
const removeCommand = program.command('rm <name>').helpGroup('容器与镜像:').description('删除指定容器');
|
|
1392
|
+
removeCommand
|
|
1393
|
+
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1394
|
+
.action((name, options) => selectAction('rm', { ...options, contName: name }));
|
|
1395
|
+
|
|
1396
|
+
const buildCommand = program.command('build').helpGroup('容器与镜像:').description('构建沙箱镜像');
|
|
1189
1397
|
buildCommand
|
|
1190
1398
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1191
1399
|
.option('--in, --image-name <name>', '指定镜像名称')
|
|
1192
|
-
.option('--iv, --image-ver <version>',
|
|
1400
|
+
.option('--iv, --image-ver <version>', `指定镜像版本 (格式: x.y.z-后缀,如 ${IMAGE_VERSION_HELP_EXAMPLE})`)
|
|
1193
1401
|
.option('--update-agents', '仅更新已有镜像内 Agent CLI 到 latest (Claude/Codex/Gemini/OpenCode)')
|
|
1194
1402
|
.option('--yes', '所有提示自动确认 (用于CI/脚本)');
|
|
1195
1403
|
appendArrayOption(buildCommand, '--iba, --image-build-arg <arg>', '构建镜像时传参给dockerfile (可多次使用)');
|
|
1196
1404
|
buildCommand.action(options => selectAction('build', options));
|
|
1197
1405
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
.
|
|
1201
|
-
.action((
|
|
1202
|
-
|
|
1203
|
-
program.command('ps')
|
|
1204
|
-
.description('列举容器')
|
|
1205
|
-
.action(() => selectAction('ps', { contList: true }));
|
|
1206
|
-
|
|
1207
|
-
program.command('images')
|
|
1208
|
-
.description('列举镜像')
|
|
1209
|
-
.action(() => selectAction('images', { imageList: true }));
|
|
1406
|
+
program.command('prune')
|
|
1407
|
+
.helpGroup('容器与镜像:')
|
|
1408
|
+
.description('清理悬空镜像')
|
|
1409
|
+
.action(() => selectAction('prune', { imageRemove: true }));
|
|
1210
1410
|
|
|
1211
|
-
|
|
1411
|
+
program.command('podman [args...]')
|
|
1412
|
+
.helpGroup('容器与镜像:')
|
|
1413
|
+
.description('用私有 Podman 执行命令(参数原样传入)')
|
|
1414
|
+
.helpOption(false)
|
|
1415
|
+
.allowUnknownOption()
|
|
1416
|
+
.passThroughOptions()
|
|
1417
|
+
.action(args => selectAction('podman', { podmanArgs: args || [] }));
|
|
1418
|
+
|
|
1419
|
+
// 网页服务与插件
|
|
1420
|
+
const serveCommand = program.command('serve [listen]').helpGroup('网页服务与插件:').description('启动网页服务 (默认 127.0.0.1:3000)');
|
|
1421
|
+
serveCommand.addHelpText('after', `
|
|
1422
|
+
示例:
|
|
1423
|
+
${MANYOYO_NAME} serve 127.0.0.1:3000 启动本机网页服务
|
|
1424
|
+
${MANYOYO_NAME} serve 127.0.0.1:3000 -d 后台启动;未设密码时会打印本次随机密码
|
|
1425
|
+
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P <密码> -d 后台启动并监听全部网卡
|
|
1426
|
+
${MANYOYO_NAME} serve 0.0.0.0:3000 -U admin -P <密码> -d --restart 重启指定后台网页服务
|
|
1427
|
+
`);
|
|
1212
1428
|
applyRunStyleOptions(serveCommand, { includeRmOnExit: false, includeWebAuthOptions: true });
|
|
1213
1429
|
serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
|
|
1214
1430
|
serveCommand.option('--stop', '停止后台网页服务;必须显式传入 listen');
|
|
@@ -1222,10 +1438,15 @@ Notes:
|
|
|
1222
1438
|
});
|
|
1223
1439
|
});
|
|
1224
1440
|
|
|
1225
|
-
const playwrightCommand = program.command('playwright').description('管理
|
|
1441
|
+
const playwrightCommand = program.command('playwright').helpGroup('网页服务与插件:').description('管理 Playwright 插件服务');
|
|
1442
|
+
playwrightCommand.addHelpText('after', `
|
|
1443
|
+
示例:
|
|
1444
|
+
${MANYOYO_NAME} playwright up mcp-host-headless 启动 MCP 宿主场景(默认/推荐)
|
|
1445
|
+
${MANYOYO_NAME} playwright up cli-host-headless 启动 CLI 宿主场景(供容器内 playwright-cli 附着)
|
|
1446
|
+
`);
|
|
1226
1447
|
registerPlaywrightAliasCommands(playwrightCommand);
|
|
1227
1448
|
|
|
1228
|
-
const pluginCommand = program.command('plugin').description('管理 manyoyo 插件');
|
|
1449
|
+
const pluginCommand = program.command('plugin', { hidden: true }).description('管理 manyoyo 插件');
|
|
1229
1450
|
pluginCommand.command('ls')
|
|
1230
1451
|
.description('列出可用插件与启用场景')
|
|
1231
1452
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
@@ -1237,67 +1458,10 @@ Notes:
|
|
|
1237
1458
|
const pluginPlaywrightCommand = pluginCommand.command('playwright').description('管理 playwright 插件服务');
|
|
1238
1459
|
registerPlaywrightAliasCommands(pluginPlaywrightCommand);
|
|
1239
1460
|
|
|
1240
|
-
|
|
1241
|
-
const
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
configShowCommand.action((options, command) => {
|
|
1245
|
-
validateShellSuffixPassThroughArgs(command);
|
|
1246
|
-
const finalOptions = {
|
|
1247
|
-
...options,
|
|
1248
|
-
showConfig: true
|
|
1249
|
-
};
|
|
1250
|
-
if (options.serve !== undefined) {
|
|
1251
|
-
finalOptions.server = options.serve;
|
|
1252
|
-
finalOptions.serverUser = options.user;
|
|
1253
|
-
finalOptions.serverPass = options.pass;
|
|
1254
|
-
}
|
|
1255
|
-
selectAction('config-show', finalOptions);
|
|
1256
|
-
});
|
|
1257
|
-
|
|
1258
|
-
const configRunCommand = configCommand.command('command').description('显示将执行的 docker run 命令并退出');
|
|
1259
|
-
applyRunStyleOptions(configRunCommand, { includeRmOnExit: false });
|
|
1260
|
-
enableShellSuffixPassThrough(configRunCommand);
|
|
1261
|
-
configRunCommand.action((options, command) => {
|
|
1262
|
-
validateShellSuffixPassThroughArgs(command);
|
|
1263
|
-
selectAction('config-command', options);
|
|
1264
|
-
});
|
|
1265
|
-
|
|
1266
|
-
const initCommand = program.command('init [agents]').description('初始化 Agent 配置到 ~/.manyoyo');
|
|
1267
|
-
initCommand
|
|
1268
|
-
.option('--yes', '所有提示自动确认 (用于CI/脚本)')
|
|
1269
|
-
.action((agents, options) => selectAction('init', { ...options, initConfig: agents === undefined ? 'all' : agents }));
|
|
1270
|
-
|
|
1271
|
-
program.command('doctor')
|
|
1272
|
-
.description('诊断容器运行时、镜像、配置、Agent、模式、插件和端口')
|
|
1273
|
-
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1274
|
-
.option('--port <port>', '检查指定监听端口')
|
|
1275
|
-
.option('--json', '以 JSON 输出稳定诊断结果')
|
|
1276
|
-
.action(options => selectAction('doctor', { ...options, doctor: true }));
|
|
1277
|
-
|
|
1278
|
-
program.command('update')
|
|
1279
|
-
.description('更新 MANYOYO(若检测为本地 file 安装则跳过)')
|
|
1280
|
-
.action(() => selectAction('update', { update: true }));
|
|
1281
|
-
|
|
1282
|
-
program.command('install <name>')
|
|
1283
|
-
.description(`安装 ${MANYOYO_NAME} 命令 (docker-cli-plugin)`)
|
|
1284
|
-
.action(name => selectAction('install', { install: name }));
|
|
1285
|
-
|
|
1286
|
-
program.command('prune')
|
|
1287
|
-
.description('清理悬空镜像和 <none> 镜像')
|
|
1288
|
-
.action(() => selectAction('prune', { imageRemove: true }));
|
|
1289
|
-
|
|
1290
|
-
// Docker CLI plugin metadata check
|
|
1291
|
-
if (maybeHandleDockerPluginMetadata(process.argv)) {
|
|
1292
|
-
process.exit(0);
|
|
1293
|
-
}
|
|
1294
|
-
|
|
1295
|
-
// Docker CLI plugin mode - remove first arg if running as plugin
|
|
1296
|
-
normalizeDockerPluginArgv(process.argv);
|
|
1297
|
-
|
|
1298
|
-
// No args: show help instead of starting container
|
|
1299
|
-
if (process.argv.length <= 2) {
|
|
1300
|
-
program.help();
|
|
1461
|
+
// No args (or only --headless / --gui): start (or reuse) the local web app; open the browser already logged in unless headless
|
|
1462
|
+
const launcherArgs = parseLauncherArgs(process.argv.slice(2));
|
|
1463
|
+
if (launcherArgs) {
|
|
1464
|
+
await runAppLauncher({ force: launcherArgs.force });
|
|
1301
1465
|
}
|
|
1302
1466
|
|
|
1303
1467
|
// Pre-handle -x/--shell-full: treat all following args as a single command
|
|
@@ -1330,16 +1494,58 @@ Notes:
|
|
|
1330
1494
|
throw new Error('serve --stop 与 --restart 不能同时使用');
|
|
1331
1495
|
}
|
|
1332
1496
|
|
|
1333
|
-
|
|
1497
|
+
UPDATE_CHECK_ENABLED = config.updateCheck !== false;
|
|
1498
|
+
MIRRORS = normalizeMirrors(config.mirrors);
|
|
1499
|
+
const noDockerActions = new Set(['init', 'update', 'config-show', 'plugin', 'doctor', 'uninstall', 'podman', 'setup']);
|
|
1334
1500
|
if (isServerStopMode) {
|
|
1335
1501
|
noDockerActions.add('serve');
|
|
1336
1502
|
}
|
|
1337
1503
|
if (!noDockerActions.has(selectedAction)) {
|
|
1338
|
-
ensureDocker();
|
|
1504
|
+
await ensureDocker(config.containerRuntime, { deferHeal: selectedAction === 'serve' });
|
|
1339
1505
|
}
|
|
1340
1506
|
|
|
1341
1507
|
if (options.update) {
|
|
1342
|
-
updateManyoyo();
|
|
1508
|
+
await updateManyoyo(options, config);
|
|
1509
|
+
process.exit(0);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (selectedAction === 'setup') {
|
|
1513
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
1514
|
+
const code = await runSetupCli({
|
|
1515
|
+
isTTY: interactive,
|
|
1516
|
+
prompter: interactive ? createPrompter() : null,
|
|
1517
|
+
configPath: getManyoyoConfigPath(),
|
|
1518
|
+
defaultWorkpath: path.join(os.homedir(), '.manyoyo', 'work'),
|
|
1519
|
+
validateHostPath: value => validateHostPathOrThrow(value),
|
|
1520
|
+
commandName: MANYOYO_NAME,
|
|
1521
|
+
log: line => console.log(line),
|
|
1522
|
+
// 新密码要让后台服务重新读配置:先停掉旧的,再按当前环境(有头 / 无头)启动
|
|
1523
|
+
afterSave: async () => {
|
|
1524
|
+
stopBackgroundApp({ quiet: true });
|
|
1525
|
+
await runAppLauncher({ exit: false });
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
process.exit(code);
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (selectedAction === 'podman') {
|
|
1532
|
+
process.exit(runPodmanCommand(options.podmanArgs, { commandName: MANYOYO_NAME }));
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
if (selectedAction === 'uninstall') {
|
|
1536
|
+
await runUninstall({
|
|
1537
|
+
yes: yesMode,
|
|
1538
|
+
ask: askQuestion,
|
|
1539
|
+
log: line => console.log(line),
|
|
1540
|
+
selectExternalRuntime: () => {
|
|
1541
|
+
try {
|
|
1542
|
+
const selected = selectContainerRuntime({ configured: config.containerRuntime });
|
|
1543
|
+
return selected.source === 'private-podman' ? null : selected;
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1343
1549
|
process.exit(0);
|
|
1344
1550
|
}
|
|
1345
1551
|
|
|
@@ -1483,6 +1689,8 @@ Notes:
|
|
|
1483
1689
|
worktreeRepoRoot: resolvedRuntime.worktreeRepoRoot,
|
|
1484
1690
|
worktreeMainRepoRoot: resolvedRuntime.worktreeMainRepoRoot,
|
|
1485
1691
|
containerMode: contModeValue || "",
|
|
1692
|
+
containerRuntime: config.containerRuntime || "auto",
|
|
1693
|
+
mirrors: MIRRORS,
|
|
1486
1694
|
shellPrefix: EXEC_COMMAND_PREFIX.trim(),
|
|
1487
1695
|
shell: EXEC_COMMAND || "",
|
|
1488
1696
|
shellSuffix: EXEC_COMMAND_SUFFIX || "",
|
|
@@ -1525,8 +1733,13 @@ Notes:
|
|
|
1525
1733
|
const portStatus = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
|
1526
1734
|
? await checkPortAvailability(parsedPort)
|
|
1527
1735
|
: undefined;
|
|
1528
|
-
|
|
1529
|
-
|
|
1736
|
+
let doctorRuntime = null;
|
|
1737
|
+
let report = await runDoctorChecks({
|
|
1738
|
+
selectRuntime: () => {
|
|
1739
|
+
doctorRuntime = selectContainerRuntime({ configured: config.containerRuntime });
|
|
1740
|
+
return doctorRuntime;
|
|
1741
|
+
},
|
|
1742
|
+
runCommand: runRuntimeCommand,
|
|
1530
1743
|
configExists: fs.existsSync(getManyoyoConfigPath()),
|
|
1531
1744
|
imageName: IMAGE_NAME,
|
|
1532
1745
|
imageVersion: IMAGE_VERSION,
|
|
@@ -1535,11 +1748,49 @@ Notes:
|
|
|
1535
1748
|
pluginConfig: config.plugins,
|
|
1536
1749
|
portStatus
|
|
1537
1750
|
});
|
|
1751
|
+
if (options.fix) {
|
|
1752
|
+
report = await applyDoctorFixes(report, {
|
|
1753
|
+
startRuntime: async () => {
|
|
1754
|
+
const result = await ensureRuntimeReady({ runtime: doctorRuntime, run: runRuntimeCommand });
|
|
1755
|
+
return { fixed: result.status === 'started' || result.status === 'ready', message: result.message };
|
|
1756
|
+
},
|
|
1757
|
+
pullImage: async () => {
|
|
1758
|
+
const image = `${IMAGE_NAME}:${IMAGE_VERSION}`;
|
|
1759
|
+
try {
|
|
1760
|
+
console.error(`正在拉取 ${image} ...`);
|
|
1761
|
+
await pullImageProcess({
|
|
1762
|
+
command: doctorRuntime.command,
|
|
1763
|
+
env: mergeRuntimeEnv(doctorRuntime.env),
|
|
1764
|
+
imageRef: image,
|
|
1765
|
+
onOutput: text => process.stderr.write(text)
|
|
1766
|
+
});
|
|
1767
|
+
} catch (e) {
|
|
1768
|
+
throw new Error(`拉取 ${image} 失败;可执行 ${MANYOYO_NAME} build --iv ${IMAGE_VERSION} 本地构建`);
|
|
1769
|
+
}
|
|
1770
|
+
return { fixed: true, message: `已拉取 ${image}` };
|
|
1771
|
+
},
|
|
1772
|
+
createConfig: () => {
|
|
1773
|
+
const result = syncGlobalImageVersion(IMAGE_VERSION);
|
|
1774
|
+
return { fixed: result.reason === 'created', message: result.reason === 'created' ? `已生成 ${result.path}` : `未生成配置: ${result.reason}` };
|
|
1775
|
+
},
|
|
1776
|
+
suggestPort: async () => {
|
|
1777
|
+
for (let candidate = parsedPort + 1; candidate <= Math.min(parsedPort + 50, 65535); candidate += 1) {
|
|
1778
|
+
if (await checkPortAvailability(candidate) === 'available') {
|
|
1779
|
+
return { fixed: false, message: `建议使用空闲端口 ${candidate}` };
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return { fixed: false, message: '附近没有空闲端口,请手动指定' };
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1538
1786
|
if (options.json) {
|
|
1539
1787
|
console.log(JSON.stringify(report, null, 4));
|
|
1540
1788
|
} else {
|
|
1541
1789
|
report.checks.forEach(check => {
|
|
1542
1790
|
console.log(`[${check.status.toUpperCase()}] ${check.code}: ${check.summary}${check.action ? ` (${check.action})` : ''}`);
|
|
1791
|
+
if (check.fix && check.fix.attempted) {
|
|
1792
|
+
console.log(` ${check.fix.fixed ? '已修复' : '未修复'}: ${check.fix.message}`);
|
|
1793
|
+
}
|
|
1543
1794
|
});
|
|
1544
1795
|
}
|
|
1545
1796
|
process.exit(report.ok ? 0 : 1);
|
|
@@ -1548,7 +1799,6 @@ Notes:
|
|
|
1548
1799
|
if (isPsMode) { getContList(); process.exit(0); }
|
|
1549
1800
|
if (isImagesMode) { getImageList(); process.exit(0); }
|
|
1550
1801
|
if (isPruneMode) { pruneDanglingImages(); process.exit(0); }
|
|
1551
|
-
if (selectedAction === 'install') { installManyoyo(options.install); process.exit(0); }
|
|
1552
1802
|
|
|
1553
1803
|
return {
|
|
1554
1804
|
yesMode,
|
|
@@ -1581,6 +1831,7 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1581
1831
|
contModeArgs: CONT_MODE_ARGS,
|
|
1582
1832
|
containerExtraArgs: CONTAINER_EXTRA_ARGS,
|
|
1583
1833
|
containerEnvs: CONTAINER_ENVS,
|
|
1834
|
+
mirrors: MIRRORS,
|
|
1584
1835
|
firstContainerEnvs: FIRST_CONTAINER_ENVS,
|
|
1585
1836
|
containerVolumes: CONTAINER_VOLUMES,
|
|
1586
1837
|
containerPorts: CONTAINER_PORTS,
|
|
@@ -1816,8 +2067,9 @@ function relaunchServeDetached(runtime) {
|
|
|
1816
2067
|
/**
|
|
1817
2068
|
* 等待容器就绪(使用指数退避算法)
|
|
1818
2069
|
* @param {string} containerName - 容器名称
|
|
2070
|
+
* @param {{throwOnFailure?: boolean}} [options] serve 路径传 true:失败抛错而不是 process.exit,调用方才能清理容器
|
|
1819
2071
|
*/
|
|
1820
|
-
async function waitForContainerReady(containerName) {
|
|
2072
|
+
async function waitForContainerReady(containerName, options = {}) {
|
|
1821
2073
|
const MAX_RETRIES = CONFIG.CONTAINER_READY_MAX_RETRIES;
|
|
1822
2074
|
let retryDelay = CONFIG.CONTAINER_READY_INITIAL_DELAY;
|
|
1823
2075
|
|
|
@@ -1830,6 +2082,9 @@ async function waitForContainerReady(containerName) {
|
|
|
1830
2082
|
}
|
|
1831
2083
|
|
|
1832
2084
|
if (status === 'exited') {
|
|
2085
|
+
if (options.throwOnFailure) {
|
|
2086
|
+
throw Object.assign(new Error('容器启动后立即退出'), { containerExited: true });
|
|
2087
|
+
}
|
|
1833
2088
|
console.log(`${RED}⚠️ 错误: 容器启动后立即退出。${NC}`);
|
|
1834
2089
|
dockerExecArgs(['logs', containerName], { stdio: 'inherit' });
|
|
1835
2090
|
process.exit(1);
|
|
@@ -1838,11 +2093,15 @@ async function waitForContainerReady(containerName) {
|
|
|
1838
2093
|
await sleep(retryDelay);
|
|
1839
2094
|
retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
|
|
1840
2095
|
} catch (e) {
|
|
2096
|
+
if (e && e.containerExited) throw e;
|
|
1841
2097
|
await sleep(retryDelay);
|
|
1842
2098
|
retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
|
|
1843
2099
|
}
|
|
1844
2100
|
}
|
|
1845
2101
|
|
|
2102
|
+
if (options.throwOnFailure) {
|
|
2103
|
+
throw new Error('容器启动超时');
|
|
2104
|
+
}
|
|
1846
2105
|
console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
|
|
1847
2106
|
process.exit(1);
|
|
1848
2107
|
}
|
|
@@ -1875,7 +2134,7 @@ function executeFirstCommand(runtime) {
|
|
|
1875
2134
|
'-c',
|
|
1876
2135
|
firstCommand
|
|
1877
2136
|
];
|
|
1878
|
-
const firstExecResult = spawnSync(`${DOCKER_CMD}`, firstExecArgs, { stdio: 'inherit' });
|
|
2137
|
+
const firstExecResult = spawnSync(`${DOCKER_CMD}`, firstExecArgs, { stdio: 'inherit', env: DOCKER_ENV });
|
|
1879
2138
|
if (firstExecResult.error) {
|
|
1880
2139
|
throw firstExecResult.error;
|
|
1881
2140
|
}
|
|
@@ -1908,6 +2167,8 @@ async function createNewContainer(runtime) {
|
|
|
1908
2167
|
process.exit(0);
|
|
1909
2168
|
}
|
|
1910
2169
|
|
|
2170
|
+
await ensureRunImage(runtime);
|
|
2171
|
+
|
|
1911
2172
|
// 使用数组参数执行命令(安全方式)
|
|
1912
2173
|
try {
|
|
1913
2174
|
const args = buildDockerRunArgs(runtime);
|
|
@@ -1920,6 +2181,13 @@ async function createNewContainer(runtime) {
|
|
|
1920
2181
|
// Wait for container to be ready
|
|
1921
2182
|
await waitForContainerReady(runtime.containerName);
|
|
1922
2183
|
|
|
2184
|
+
applyAptMirror({
|
|
2185
|
+
dockerExecArgs,
|
|
2186
|
+
containerName: runtime.containerName,
|
|
2187
|
+
mirrors: runtime.mirrors,
|
|
2188
|
+
warn: message => console.warn(`${YELLOW}⚠️ ${message}${NC}`)
|
|
2189
|
+
});
|
|
2190
|
+
|
|
1923
2191
|
// Run one-time bootstrap command for newly created containers only.
|
|
1924
2192
|
executeFirstCommand(runtime);
|
|
1925
2193
|
|
|
@@ -1942,6 +2210,7 @@ function buildDockerRunArgs(runtime) {
|
|
|
1942
2210
|
containerEnvs: runtime.containerEnvs,
|
|
1943
2211
|
containerVolumes: runtime.containerVolumes,
|
|
1944
2212
|
containerPorts: runtime.containerPorts,
|
|
2213
|
+
mirrors: runtime.mirrors,
|
|
1945
2214
|
defaultCommand: runtime.execCommand
|
|
1946
2215
|
});
|
|
1947
2216
|
}
|
|
@@ -2017,9 +2286,9 @@ function executeInContainer(runtime, defaultCommand) {
|
|
|
2017
2286
|
|
|
2018
2287
|
// Execute command in container
|
|
2019
2288
|
if (runtime.execCommand) {
|
|
2020
|
-
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash', '-c', runtime.execCommand], { stdio: 'inherit' });
|
|
2289
|
+
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash', '-c', runtime.execCommand], { stdio: 'inherit', env: DOCKER_ENV });
|
|
2021
2290
|
} else {
|
|
2022
|
-
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash'], { stdio: 'inherit' });
|
|
2291
|
+
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash'], { stdio: 'inherit', env: DOCKER_ENV });
|
|
2023
2292
|
}
|
|
2024
2293
|
}
|
|
2025
2294
|
|
|
@@ -2080,6 +2349,38 @@ async function handlePostExit(runtime, defaultCommand) {
|
|
|
2080
2349
|
}
|
|
2081
2350
|
}
|
|
2082
2351
|
|
|
2352
|
+
async function runAppLauncher({ force = null, exit = true } = {}) {
|
|
2353
|
+
try {
|
|
2354
|
+
const { headless } = detectHeadless({ force });
|
|
2355
|
+
await launchApp({
|
|
2356
|
+
headless,
|
|
2357
|
+
authUser: String(readManyoyoConfig().config.serverUser || '').trim() || 'admin',
|
|
2358
|
+
statePath: getAppStatePath(),
|
|
2359
|
+
isProcessRunning,
|
|
2360
|
+
spawnServe: port => {
|
|
2361
|
+
// 登录走一次性令牌;未配置密码时 serve 自己生成随机密码(标记为自动生成,向导据此让用户设置)
|
|
2362
|
+
const child = spawn(process.argv[0], [process.argv[1], 'serve', `127.0.0.1:${port}`], {
|
|
2363
|
+
detached: true,
|
|
2364
|
+
stdio: 'ignore'
|
|
2365
|
+
});
|
|
2366
|
+
child.unref();
|
|
2367
|
+
return child;
|
|
2368
|
+
},
|
|
2369
|
+
issueToken: () => issueLoginToken(getLoginTokenDir()),
|
|
2370
|
+
open: url => openBrowser(url),
|
|
2371
|
+
log: line => console.log(line),
|
|
2372
|
+
logPathHint: buildManyoyoLogPath('serve').path,
|
|
2373
|
+
commandName: MANYOYO_NAME
|
|
2374
|
+
});
|
|
2375
|
+
} catch (e) {
|
|
2376
|
+
console.error(`${RED}${e.message}${NC}`);
|
|
2377
|
+
if (!exit) return false;
|
|
2378
|
+
process.exit(1);
|
|
2379
|
+
}
|
|
2380
|
+
if (!exit) return true;
|
|
2381
|
+
process.exit(0);
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2083
2384
|
async function runWebServerMode(runtime) {
|
|
2084
2385
|
if (!runtime.serverAuthUser || !runtime.serverAuthPass) {
|
|
2085
2386
|
ensureWebServerAuthCredentials();
|
|
@@ -2088,6 +2389,48 @@ async function runWebServerMode(runtime) {
|
|
|
2088
2389
|
runtime.serverAuthPassAuto = SERVER_AUTH_PASS_AUTO;
|
|
2089
2390
|
}
|
|
2090
2391
|
|
|
2392
|
+
const updateChecker = createUpdateChecker({
|
|
2393
|
+
currentVersion: require('../package.json').version,
|
|
2394
|
+
installMode: appUpdate.detectInstallMode({ scriptPath: __filename }).mode,
|
|
2395
|
+
enabled: UPDATE_CHECK_ENABLED,
|
|
2396
|
+
fetchLatest: () => appUpdate.fetchLatestRelease()
|
|
2397
|
+
});
|
|
2398
|
+
|
|
2399
|
+
const needRuntimeHeal = !isRuntimeProven();
|
|
2400
|
+
if (needRuntimeHeal) {
|
|
2401
|
+
RUNTIME_STATE.status = 'starting';
|
|
2402
|
+
RUNTIME_STATE.message = '正在检查容器环境';
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
// 自愈失败/超时不是终态:用户手动修好(或点向导里的“重试”)后可以再来一次
|
|
2406
|
+
let runtimeHealing = false;
|
|
2407
|
+
function startServeRuntimeHeal() {
|
|
2408
|
+
if (runtimeHealing) return false;
|
|
2409
|
+
runtimeHealing = true;
|
|
2410
|
+
RUNTIME_STATE.status = 'starting';
|
|
2411
|
+
RUNTIME_STATE.message = '正在检查容器环境';
|
|
2412
|
+
healContainerRuntime(state => {
|
|
2413
|
+
RUNTIME_STATE.status = state.status;
|
|
2414
|
+
RUNTIME_STATE.message = state.message;
|
|
2415
|
+
if (state.status === 'starting') console.log(`${YELLOW}⏳ ${state.message}...${NC}`);
|
|
2416
|
+
}, { async: true }).then(result => {
|
|
2417
|
+
if (result.status === 'ready' || result.status === 'started') {
|
|
2418
|
+
RUNTIME_STATE.status = 'ready';
|
|
2419
|
+
RUNTIME_STATE.message = '';
|
|
2420
|
+
} else {
|
|
2421
|
+
RUNTIME_STATE.status = 'failed';
|
|
2422
|
+
RUNTIME_STATE.message = result.message;
|
|
2423
|
+
console.log(`${YELLOW}⚠️ ${result.message}${NC}`);
|
|
2424
|
+
}
|
|
2425
|
+
}).catch(error => {
|
|
2426
|
+
RUNTIME_STATE.status = 'failed';
|
|
2427
|
+
RUNTIME_STATE.message = String(error && error.message || error);
|
|
2428
|
+
}).finally(() => {
|
|
2429
|
+
runtimeHealing = false;
|
|
2430
|
+
});
|
|
2431
|
+
return true;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2091
2434
|
const serverHandle = await startWebServer({
|
|
2092
2435
|
serverHost: runtime.serverHost,
|
|
2093
2436
|
serverPort: runtime.serverPort,
|
|
@@ -2096,6 +2439,31 @@ async function runWebServerMode(runtime) {
|
|
|
2096
2439
|
authPassAuto: runtime.serverAuthPassAuto,
|
|
2097
2440
|
serveTitle: runtime.serveTitle,
|
|
2098
2441
|
dockerCmd: DOCKER_CMD,
|
|
2442
|
+
dockerEnv: DOCKER_ENV,
|
|
2443
|
+
runtimeState: RUNTIME_STATE,
|
|
2444
|
+
retryRuntimeHeal: () => (needRuntimeHeal ? startServeRuntimeHeal() : false),
|
|
2445
|
+
autoPullImage: true,
|
|
2446
|
+
importState: () => readImportState(),
|
|
2447
|
+
updateInfo: () => updateChecker.getInfo(),
|
|
2448
|
+
loginTokenDir: getLoginTokenDir(),
|
|
2449
|
+
doctorCheck: async () => runDoctorChecks({
|
|
2450
|
+
selectRuntime: () => CONTAINER_RUNTIME,
|
|
2451
|
+
runCommand: runRuntimeCommandAsync,
|
|
2452
|
+
configExists: fs.existsSync(getManyoyoConfigPath()),
|
|
2453
|
+
imageName: runtime.imageName,
|
|
2454
|
+
imageVersion: runtime.imageVersion,
|
|
2455
|
+
containerMode: 'common',
|
|
2456
|
+
pluginConfig: {}
|
|
2457
|
+
}),
|
|
2458
|
+
doctorStartRuntime: async () => {
|
|
2459
|
+
const result = await ensureRuntimeReady({ runtime: CONTAINER_RUNTIME, run: runRuntimeCommandAsync });
|
|
2460
|
+
const fixed = result.status === 'started' || result.status === 'ready';
|
|
2461
|
+
if (fixed) {
|
|
2462
|
+
RUNTIME_STATE.status = 'ready';
|
|
2463
|
+
RUNTIME_STATE.message = '';
|
|
2464
|
+
}
|
|
2465
|
+
return { fixed, message: result.message };
|
|
2466
|
+
},
|
|
2099
2467
|
hostPath: runtime.hostPath,
|
|
2100
2468
|
containerPath: runtime.containerPath,
|
|
2101
2469
|
imageName: runtime.imageName,
|
|
@@ -2115,6 +2483,7 @@ async function runWebServerMode(runtime) {
|
|
|
2115
2483
|
getContainerStatus,
|
|
2116
2484
|
waitForContainerReady,
|
|
2117
2485
|
dockerExecArgs,
|
|
2486
|
+
dockerExecAsync: (args, options = {}) => runCommandAsync(DOCKER_CMD, args, { env: DOCKER_ENV, timeout: options.timeout }),
|
|
2118
2487
|
showImagePullHint,
|
|
2119
2488
|
removeContainer,
|
|
2120
2489
|
webHistoryDir: path.join(os.homedir(), '.manyoyo', 'web-history'),
|
|
@@ -2129,6 +2498,9 @@ async function runWebServerMode(runtime) {
|
|
|
2129
2498
|
logger: runtime.logger
|
|
2130
2499
|
});
|
|
2131
2500
|
writeServePidFile(runtime, serverHandle);
|
|
2501
|
+
if (needRuntimeHeal) {
|
|
2502
|
+
startServeRuntimeHeal();
|
|
2503
|
+
}
|
|
2132
2504
|
return serverHandle;
|
|
2133
2505
|
}
|
|
2134
2506
|
|
|
@@ -2190,6 +2562,7 @@ async function main() {
|
|
|
2190
2562
|
yesMode: Boolean(modeState.yesMode),
|
|
2191
2563
|
updateAgents: Boolean(modeState.updateAgents),
|
|
2192
2564
|
dockerCmd: DOCKER_CMD,
|
|
2565
|
+
dockerEnv: DOCKER_ENV,
|
|
2193
2566
|
rootDir: path.join(__dirname, '..'),
|
|
2194
2567
|
loadConfig,
|
|
2195
2568
|
runCmd,
|