@xcanwin/manyoyo 7.1.5 → 8.0.1
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 +24 -14
- package/bin/manyoyo.js +490 -83
- package/docker/manyoyo.Dockerfile +10 -4
- package/lib/app-launcher.js +220 -0
- package/lib/app-update.js +325 -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/frontend/shadcn.html +86 -86
- package/lib/web/server.js +641 -14
- package/lib/worktrees.js +15 -0
- package/manyoyo.example.json +13 -2
- package/package.json +9 -9
package/bin/manyoyo.js
CHANGED
|
@@ -11,11 +11,26 @@ const { Command } = 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,7 +50,8 @@ 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
|
|
53
|
+
const GLOBAL_CONFIG_IMAGE_VERSION = String(readManyoyoConfig().config.imageVersion || '').trim();
|
|
54
|
+
const IMAGE_VERSION_HELP_EXAMPLE = GLOBAL_CONFIG_IMAGE_VERSION || IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
37
55
|
|
|
38
56
|
// Helper function to format date like bash $(date +%m%d-%H%M)
|
|
39
57
|
function formatDate() {
|
|
@@ -46,6 +64,10 @@ function formatDate() {
|
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
function detectCommandName() {
|
|
67
|
+
// 离线安装器生成的 manyoyo / my 包装脚本都指向同一个 manyoyo.js,用环境变量区分显示的命令名
|
|
68
|
+
const fromEnv = String(process.env.MANYOYO_COMMAND_NAME || '').trim();
|
|
69
|
+
if (/^[A-Za-z][A-Za-z0-9_-]*$/.test(fromEnv)) return fromEnv;
|
|
70
|
+
|
|
49
71
|
const rawArgv1 = process.argv[1] || '';
|
|
50
72
|
const baseName = path.basename(rawArgv1).replace(/\.(cjs|mjs|js)$/i, '');
|
|
51
73
|
|
|
@@ -67,7 +89,7 @@ const CONFIG = {
|
|
|
67
89
|
let CONTAINER_NAME = `my-${formatDate()}`;
|
|
68
90
|
let HOST_PATH = process.cwd();
|
|
69
91
|
let CONTAINER_PATH = HOST_PATH;
|
|
70
|
-
let IMAGE_NAME = "
|
|
92
|
+
let IMAGE_NAME = "ghcr.io/xcanwin/manyoyo";
|
|
71
93
|
let IMAGE_VERSION = IMAGE_VERSION_DEFAULT || `${IMAGE_VERSION_BASE}-common`;
|
|
72
94
|
let EXEC_COMMAND = "";
|
|
73
95
|
let EXEC_COMMAND_PREFIX = "";
|
|
@@ -104,6 +126,16 @@ const IMAGE_VERSION_TAG_PATTERN = /^(\d+\.\d+\.\d+)-([A-Za-z0-9][A-Za-z0-9_.-]*)
|
|
|
104
126
|
|
|
105
127
|
// Docker command (will be set by ensure_docker)
|
|
106
128
|
let DOCKER_CMD = 'docker';
|
|
129
|
+
// 仅运行时子进程使用的完整 env(私有 Podman 才有值),不要写回 process.env
|
|
130
|
+
let DOCKER_ENV;
|
|
131
|
+
let CONTAINER_RUNTIME = null;
|
|
132
|
+
// 全局配置 updateCheck(默认 true):serve 是否每天检查一次新版本
|
|
133
|
+
let UPDATE_CHECK_ENABLED = true;
|
|
134
|
+
// 全局配置 mirrors(apt/npm/pip 软件源,空 = 官方默认):容器创建时在容器层生效,镜像不变
|
|
135
|
+
let MIRRORS = { apt: '', npm: '', pip: '' };
|
|
136
|
+
// serve 的容器环境状态,供 GET /api/system/runtime 读取
|
|
137
|
+
const RUNTIME_STATE = { status: 'ready', message: '' };
|
|
138
|
+
const DOCKER_DAEMON_ERROR_CODES = new Set(['PODMAN_MACHINE_UNAVAILABLE', 'DOCKER_DAEMON_UNAVAILABLE', 'PORT_IN_USE']);
|
|
107
139
|
const SUPPORTED_INIT_AGENTS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
108
140
|
|
|
109
141
|
function sleep(ms) {
|
|
@@ -311,6 +343,9 @@ function installServeProcessDiagnostics(logger) {
|
|
|
311
343
|
* @property {Object.<string, Object>} [runs] - 运行配置映射(-r <name>)
|
|
312
344
|
* @property {string} [yolo] - YOLO 模式
|
|
313
345
|
* @property {string} [containerMode] - 容器模式
|
|
346
|
+
* @property {string} [containerRuntime] - 容器运行时(auto/docker/podman,默认 auto;仅全局配置生效)
|
|
347
|
+
* @property {{apt?: string, npm?: string, pip?: string}} [mirrors] - 容器内 apt/npm/pip 软件源(http/https URL,空/缺省为官方默认;仅全局配置生效)
|
|
348
|
+
* @property {boolean} [updateCheck] - serve 是否每天检查一次新版本(默认 true;仅全局配置生效,请求不附带任何本机信息)
|
|
314
349
|
* @property {number} [cacheTTL] - 缓存过期天数
|
|
315
350
|
* @property {string} [nodeMirror] - Node.js 镜像源
|
|
316
351
|
*/
|
|
@@ -671,17 +706,39 @@ function setContMode(mode) {
|
|
|
671
706
|
}
|
|
672
707
|
}
|
|
673
708
|
|
|
709
|
+
// 本地没有镜像时自动拉取(终端显示拉取进度),失败给出原因与下一步
|
|
710
|
+
async function ensureRunImage(runtime) {
|
|
711
|
+
const imageRef = `${runtime.imageName}:${runtime.imageVersion}`;
|
|
712
|
+
// 离线安装器还在后台导入镜像时先等它,避免重复去仓库拉
|
|
713
|
+
await waitForImport({ onWait: state => console.log(`${YELLOW}⏳ ${state.message},等待完成...${NC}`) });
|
|
714
|
+
await ensureImagePresent({
|
|
715
|
+
imageRef,
|
|
716
|
+
command: DOCKER_CMD,
|
|
717
|
+
isPresent: () => {
|
|
718
|
+
try {
|
|
719
|
+
dockerExecArgs(['image', 'inspect', imageRef], { stdio: 'pipe' });
|
|
720
|
+
return true;
|
|
721
|
+
} catch (e) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
},
|
|
725
|
+
pull: () => pullImageProcess({
|
|
726
|
+
command: DOCKER_CMD,
|
|
727
|
+
env: DOCKER_ENV,
|
|
728
|
+
imageRef,
|
|
729
|
+
onOutput: (text, stream) => (stream === 'stderr' ? process.stderr : process.stdout).write(text)
|
|
730
|
+
}),
|
|
731
|
+
onStart: () => console.log(`${YELLOW}⏬ 本地没有镜像 ${imageRef},正在拉取...${NC}`)
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
674
735
|
function showImagePullHint(err) {
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
const message = err && err.message ? err.message : '';
|
|
678
|
-
const combined = `${message}\n${stderr}\n${stdout}`;
|
|
679
|
-
if (!/localhost\/v2|pinging container registry localhost|connection refused|dial tcp .*:443/i.test(combined)) {
|
|
736
|
+
const info = describeError(getCommandFailureText(err), { imageRef: `${IMAGE_NAME}:${IMAGE_VERSION}` });
|
|
737
|
+
if (!info || !['IMAGE_PULL_FAILED', 'IMAGE_NOT_FOUND'].includes(info.code)) {
|
|
680
738
|
return;
|
|
681
739
|
}
|
|
682
|
-
|
|
683
|
-
console.log(`${YELLOW}
|
|
684
|
-
console.log(`${YELLOW} 你可以: (1) 更新 ~/.manyoyo/manyoyo.json 的 imageVersion。 (2) 或先执行 ${MANYOYO_NAME} build --iv <x.y.z-后缀> 构建镜像。${NC}`);
|
|
740
|
+
console.log(`${YELLOW}💡 提示: ${info.reason}${NC}`);
|
|
741
|
+
console.log(`${YELLOW} ${info.action}${NC}`);
|
|
685
742
|
}
|
|
686
743
|
|
|
687
744
|
function getCommandFailureText(err) {
|
|
@@ -691,25 +748,6 @@ function getCommandFailureText(err) {
|
|
|
691
748
|
return `${message}\n${stderr}\n${stdout}`;
|
|
692
749
|
}
|
|
693
750
|
|
|
694
|
-
function getContainerRuntimeUnavailableHint(command, err) {
|
|
695
|
-
const text = getCommandFailureText(err);
|
|
696
|
-
if (/Cannot connect to Podman|unable to connect to Podman socket|podman machine start|podman system connection/i.test(text)) {
|
|
697
|
-
return [
|
|
698
|
-
'',
|
|
699
|
-
`提示: 当前 ${command} 命令正在连接 Podman machine,但连接不可用。`,
|
|
700
|
-
'请先在宿主机执行: podman machine start'
|
|
701
|
-
].join('\n');
|
|
702
|
-
}
|
|
703
|
-
if (/Cannot connect to the Docker daemon|docker daemon is not running|Is the docker daemon running/i.test(text)) {
|
|
704
|
-
return [
|
|
705
|
-
'',
|
|
706
|
-
`提示: 当前 ${command} 命令无法连接容器运行时。`,
|
|
707
|
-
'请先启动 Docker Desktop / Docker daemon,或确认 Podman machine 已启动。'
|
|
708
|
-
].join('\n');
|
|
709
|
-
}
|
|
710
|
-
return '';
|
|
711
|
-
}
|
|
712
|
-
|
|
713
751
|
function runCmd(cmd, args, options = {}) {
|
|
714
752
|
const result = spawnSync(cmd, args, { encoding: 'utf-8', ...options });
|
|
715
753
|
if (result.error) {
|
|
@@ -738,11 +776,12 @@ function checkPortAvailability(port) {
|
|
|
738
776
|
|
|
739
777
|
function dockerExecArgs(args, options = {}) {
|
|
740
778
|
try {
|
|
741
|
-
return runCmd(DOCKER_CMD, args, options);
|
|
779
|
+
return runCmd(DOCKER_CMD, args, { env: DOCKER_ENV, ...options });
|
|
742
780
|
} catch (e) {
|
|
743
|
-
const
|
|
744
|
-
if (
|
|
745
|
-
|
|
781
|
+
const info = describeError(getCommandFailureText(e), { command: DOCKER_CMD });
|
|
782
|
+
if (info && DOCKER_DAEMON_ERROR_CODES.has(info.code) && e && e.message) {
|
|
783
|
+
const hint = `\n提示: ${info.reason}\n${info.action}`;
|
|
784
|
+
if (!e.message.includes(hint)) e.message = `${e.message}${hint}`;
|
|
746
785
|
}
|
|
747
786
|
throw e;
|
|
748
787
|
}
|
|
@@ -763,19 +802,51 @@ function removeContainer(name) {
|
|
|
763
802
|
if ( !(QUIET.crm || QUIET.full) ) console.log(`${GREEN}✅ 已彻底删除。${NC}`);
|
|
764
803
|
}
|
|
765
804
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
805
|
+
// 运行时子进程执行器:env 只是增量,合并后仅传给这个子进程
|
|
806
|
+
function runRuntimeCommand(command, args, options = {}) {
|
|
807
|
+
return runCmd(command, args, {
|
|
808
|
+
stdio: 'pipe',
|
|
809
|
+
timeout: options.timeout,
|
|
810
|
+
env: mergeRuntimeEnv(options.env)
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// serve 里的自愈用异步执行器:machine start 最长 120s,同步执行会冻住整个事件循环(向导进度、登录页都没响应)
|
|
815
|
+
function runRuntimeCommandAsync(command, args, options = {}) {
|
|
816
|
+
return runCommandAsync(command, args, { timeout: options.timeout, env: mergeRuntimeEnv(options.env) });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function healContainerRuntime(onStatus, options = {}) {
|
|
820
|
+
return ensureRuntimeReady({ runtime: CONTAINER_RUNTIME, run: options.async ? runRuntimeCommandAsync : runRuntimeCommand, onStatus });
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// 选择阶段已确认 daemon 可用(info 通过)时无需再自愈
|
|
824
|
+
function isRuntimeProven() {
|
|
825
|
+
return /-daemon$/.test(CONTAINER_RUNTIME.source);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async function ensureDocker(configuredRuntime, options = {}) {
|
|
829
|
+
try {
|
|
830
|
+
CONTAINER_RUNTIME = selectContainerRuntime({ configured: configuredRuntime });
|
|
831
|
+
} catch (e) {
|
|
832
|
+
console.error(e.message);
|
|
833
|
+
process.exit(1);
|
|
776
834
|
}
|
|
777
|
-
|
|
778
|
-
|
|
835
|
+
DOCKER_CMD = CONTAINER_RUNTIME.command;
|
|
836
|
+
DOCKER_ENV = mergeRuntimeEnv(CONTAINER_RUNTIME.env);
|
|
837
|
+
if (options.deferHeal || isRuntimeProven()) {
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const result = await healContainerRuntime(state => {
|
|
842
|
+
if (state.status === 'starting') console.log(`${YELLOW}⏳ ${state.message}...${NC}`);
|
|
843
|
+
});
|
|
844
|
+
if (result.status === 'started') {
|
|
845
|
+
console.log(`${GREEN}✅ 容器环境已就绪${NC}`);
|
|
846
|
+
} else if (result.status !== 'ready') {
|
|
847
|
+
console.log(`${YELLOW}⚠️ ${result.message}${NC}`);
|
|
848
|
+
}
|
|
849
|
+
return true;
|
|
779
850
|
}
|
|
780
851
|
|
|
781
852
|
function installManyoyo(name) {
|
|
@@ -796,7 +867,130 @@ function installManyoyo(name) {
|
|
|
796
867
|
process.exit(0);
|
|
797
868
|
}
|
|
798
869
|
|
|
799
|
-
|
|
870
|
+
// 升级后让旧版本的后台服务退出(pid 必须确实是 manyoyo serve),下次执行 manyoyo 就会启动新版本
|
|
871
|
+
function stopBackgroundApp({ quiet = false } = {}) {
|
|
872
|
+
const pid = readPid(path.join(os.homedir(), '.manyoyo', 'serve', 'app.json'));
|
|
873
|
+
if (pid && pid !== process.pid && defaultIsManyoyoServe(pid) && defaultKill(pid)) {
|
|
874
|
+
if (!quiet) console.log(`${GREEN}✅ 已停止旧版本的后台服务 (pid ${pid}),下次执行 ${MANYOYO_NAME} 会启动新版本。${NC}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// 升级流程里准备容器运行时:和 ensureDocker 一样选择并(必要时)自愈,但没有运行时也不退出进程
|
|
879
|
+
async function prepareRuntimeForUpdate(configuredRuntime) {
|
|
880
|
+
try {
|
|
881
|
+
CONTAINER_RUNTIME = selectContainerRuntime({ configured: configuredRuntime });
|
|
882
|
+
} catch (error) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
DOCKER_CMD = CONTAINER_RUNTIME.command;
|
|
886
|
+
DOCKER_ENV = mergeRuntimeEnv(CONTAINER_RUNTIME.env);
|
|
887
|
+
if (!isRuntimeProven()) {
|
|
888
|
+
const result = await healContainerRuntime(state => {
|
|
889
|
+
if (state.status === 'starting') console.log(`${YELLOW}⏳ ${state.message}...${NC}`);
|
|
890
|
+
});
|
|
891
|
+
if (result.status !== 'ready' && result.status !== 'started') {
|
|
892
|
+
console.log(`${YELLOW}⚠️ ${result.message}${NC}`);
|
|
893
|
+
return false;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function updateOfflineInstall(mode, options, globalConfig) {
|
|
900
|
+
const { appRoot } = mode;
|
|
901
|
+
if (options.rollback) {
|
|
902
|
+
const result = appUpdate.rollbackApp({ appRoot });
|
|
903
|
+
console.log(`${GREEN}✅ 已回滚: ${result.from || '未知'} → ${result.to}${NC}`);
|
|
904
|
+
stopBackgroundApp();
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const installed = appUpdate.currentVersion(appRoot) || mode.version;
|
|
909
|
+
console.log(`${CYAN}🔄 当前版本: ${installed}${NC}`);
|
|
910
|
+
console.log(`${CYAN}🔄 正在查询最新版本...${NC}`);
|
|
911
|
+
const release = await appUpdate.fetchLatestRelease();
|
|
912
|
+
if (appUpdate.compareVersions(release.version, installed) <= 0) {
|
|
913
|
+
console.log(`${GREEN}✅ 已是最新版本 ${installed}${NC}`);
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
console.log(`${CYAN}⬇️ 发现新版本 ${release.version},只下载 manyoyo 本体(数十 MB)${NC}`);
|
|
917
|
+
const result = await appUpdate.installAppUpdate({ appRoot, release, targetOs: appUpdate.platformOs(), log: line => console.log(` ${line}`) });
|
|
918
|
+
console.log(`${GREEN}✅ 更新完成: ${installed} → ${result.version}(上一版本 ${result.previous || '无'} 已保留,可用 ${MANYOYO_NAME} update --rollback 回滚)${NC}`);
|
|
919
|
+
|
|
920
|
+
// Podman / VM 磁盘有变化只提示(需要新的完整包)
|
|
921
|
+
try {
|
|
922
|
+
const names = appUpdate.releaseAssetNames(release.version, appUpdate.arch(), appUpdate.platformOs());
|
|
923
|
+
if (release.assets[names.manifest]) {
|
|
924
|
+
const remote = JSON.parse(await appUpdate.fetchText(fetch, release.assets[names.manifest]));
|
|
925
|
+
const hint = appUpdate.describeRuntimeChange(appUpdate.readInstalledRecord(os.homedir()), remote);
|
|
926
|
+
if (hint) console.log(`${YELLOW}ℹ️ ${hint}${NC}`);
|
|
927
|
+
}
|
|
928
|
+
} catch (error) {
|
|
929
|
+
// 提示信息拿不到不影响升级
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// 新版本要求的镜像不存在就拉取;仍用旧镜像的容器只列出来,不动
|
|
933
|
+
const imageName = String(globalConfig.imageName || 'ghcr.io/xcanwin/manyoyo');
|
|
934
|
+
const versionPolicy = resolveUpdateImageVersion({
|
|
935
|
+
configured: globalConfig.imageVersion,
|
|
936
|
+
previousDefault: require('../package.json').imageVersion,
|
|
937
|
+
nextDefault: result.manifest.imageVersion
|
|
938
|
+
});
|
|
939
|
+
const imageVersion = versionPolicy.imageVersion;
|
|
940
|
+
if (versionPolicy.advance) {
|
|
941
|
+
const synced = syncGlobalImageVersion(imageVersion);
|
|
942
|
+
console.log(`${CYAN}ℹ️ 配置里的 imageVersion(${globalConfig.imageVersion})是旧版本默认值,已随新版本更新为 ${imageVersion}${synced.updated ? '' : '(写入配置失败,请手动修改)'}${NC}`);
|
|
943
|
+
} else if (versionPolicy.pinned) {
|
|
944
|
+
console.log(`${YELLOW}ℹ️ 你在配置里固定了 imageVersion=${imageVersion},新版本默认是 ${result.manifest.imageVersion};如需使用新镜像请修改 ~/.manyoyo/manyoyo.json${NC}`);
|
|
945
|
+
}
|
|
946
|
+
if (imageVersion && await prepareRuntimeForUpdate(globalConfig.containerRuntime)) {
|
|
947
|
+
const imageRef = `${imageName}:${imageVersion}`;
|
|
948
|
+
try {
|
|
949
|
+
await ensureImagePresent({
|
|
950
|
+
imageRef,
|
|
951
|
+
command: DOCKER_CMD,
|
|
952
|
+
isPresent: () => {
|
|
953
|
+
try {
|
|
954
|
+
dockerExecArgs(['image', 'inspect', imageRef], { stdio: 'pipe' });
|
|
955
|
+
return true;
|
|
956
|
+
} catch (e) {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
},
|
|
960
|
+
pull: () => pullImageProcess({ command: DOCKER_CMD, env: DOCKER_ENV, imageRef, onOutput: (text, stream) => (stream === 'stderr' ? process.stderr : process.stdout).write(text) }),
|
|
961
|
+
onStart: () => console.log(`${YELLOW}⏬ 新版本需要镜像 ${imageRef},正在拉取...${NC}`)
|
|
962
|
+
});
|
|
963
|
+
} catch (error) {
|
|
964
|
+
console.log(`${YELLOW}⚠️ 镜像还没准备好:${error.message}${NC}`);
|
|
965
|
+
}
|
|
966
|
+
const outdated = appUpdate.findOutdatedContainers({ run: runCmd, runtime: { command: DOCKER_CMD, env: DOCKER_ENV }, imageRef });
|
|
967
|
+
if (outdated.length > 0) {
|
|
968
|
+
console.log(`${YELLOW}ℹ️ 有 ${outdated.length} 个容器还在用旧镜像:${outdated.map(item => `${item.name}(${item.image})`).join('、')}${NC}`);
|
|
969
|
+
console.log(`${YELLOW} 它们可以继续使用;想换成新镜像,请新建容器(会话历史与凭据都在宿主机上,不会丢)。${NC}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
stopBackgroundApp();
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
async function updateManyoyo(options = {}, globalConfig = {}) {
|
|
976
|
+
const mode = appUpdate.detectInstallMode({ scriptPath: __filename });
|
|
977
|
+
if (mode.mode === 'offline') {
|
|
978
|
+
try {
|
|
979
|
+
await updateOfflineInstall(mode, options, globalConfig);
|
|
980
|
+
} catch (error) {
|
|
981
|
+
if (error instanceof appUpdate.UpdateError) {
|
|
982
|
+
console.error(`${RED}❌ ${error.message}${NC}`);
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (options.rollback) {
|
|
990
|
+
console.error(`${RED}❌ --rollback 只适用于离线包安装;npm 安装的版本请用 npm install -g @xcanwin/manyoyo@<版本> 回退。${NC}`);
|
|
991
|
+
process.exit(1);
|
|
992
|
+
}
|
|
993
|
+
|
|
800
994
|
let isLocalFileInstall = false;
|
|
801
995
|
let currentVersion = 'unknown';
|
|
802
996
|
|
|
@@ -901,27 +1095,11 @@ function getImageList() {
|
|
|
901
1095
|
}
|
|
902
1096
|
|
|
903
1097
|
function pruneDanglingImages() {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
const imagesOutput = dockerExecArgs(['images', '-a', '--format', '{{.ID}} {{.Repository}}']);
|
|
910
|
-
const noneImages = imagesOutput
|
|
911
|
-
.split('\n')
|
|
912
|
-
.filter(line => line.includes('<none>'))
|
|
913
|
-
.map(line => line.split(' ')[0])
|
|
914
|
-
.filter(id => id);
|
|
915
|
-
|
|
916
|
-
if (noneImages.length > 0) {
|
|
917
|
-
console.log(`${YELLOW}清理剩余的 <none> 镜像 (${noneImages.length} 个)...${NC}`);
|
|
918
|
-
dockerExecArgs(['rmi', '-f', ...noneImages], { stdio: 'inherit' });
|
|
919
|
-
}
|
|
920
|
-
} catch (e) {
|
|
921
|
-
// Ignore errors if no <none> images found
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
console.log(`${GREEN}✅ 清理完成${NC}`);
|
|
1098
|
+
pruneDanglingImagesSafely({
|
|
1099
|
+
dockerExecArgs,
|
|
1100
|
+
log: line => console.log(line),
|
|
1101
|
+
colors: { YELLOW, GREEN, NC }
|
|
1102
|
+
});
|
|
925
1103
|
}
|
|
926
1104
|
|
|
927
1105
|
function maybeHandleDockerPluginMetadata(argv) {
|
|
@@ -1262,6 +1440,11 @@ Notes:
|
|
|
1262
1440
|
selectAction('config-command', options);
|
|
1263
1441
|
});
|
|
1264
1442
|
|
|
1443
|
+
program.command('uninstall')
|
|
1444
|
+
.description('卸载离线包安装的 MANYOYO:停止服务,删除程序与 PATH 配置,用户数据逐项询问(--yes 不会删用户数据)')
|
|
1445
|
+
.option('--yes', '确认卸载程序本身,不再询问(不会删除配置、历史、日志、工作目录和外部运行时里的容器镜像)')
|
|
1446
|
+
.action(options => selectAction('uninstall', options));
|
|
1447
|
+
|
|
1265
1448
|
const initCommand = program.command('init [agents]').description('初始化 Agent 配置到 ~/.manyoyo');
|
|
1266
1449
|
initCommand
|
|
1267
1450
|
.option('--yes', '所有提示自动确认 (用于CI/脚本)')
|
|
@@ -1272,11 +1455,24 @@ Notes:
|
|
|
1272
1455
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1273
1456
|
.option('--port <port>', '检查指定监听端口')
|
|
1274
1457
|
.option('--json', '以 JSON 输出稳定诊断结果')
|
|
1458
|
+
.option('--fix', '自动修复可修复项(启动容器环境、拉取镜像、生成默认配置,端口占用时给出建议端口)')
|
|
1275
1459
|
.action(options => selectAction('doctor', { ...options, doctor: true }));
|
|
1276
1460
|
|
|
1277
1461
|
program.command('update')
|
|
1278
|
-
.description('更新 MANYOYO
|
|
1279
|
-
.
|
|
1462
|
+
.description('更新 MANYOYO(离线包安装只下载变化部分并保留上一版本;若检测为本地 file 安装则跳过)')
|
|
1463
|
+
.option('--rollback', '离线包安装:切回上一版本')
|
|
1464
|
+
.action(options => selectAction('update', { update: true, rollback: Boolean(options.rollback) }));
|
|
1465
|
+
|
|
1466
|
+
program.command('podman [args...]')
|
|
1467
|
+
.description('用 MANYOYO 的私有 Podman 执行命令(参数原样传给 podman);eval "$(manyoyo podman env)" 可在当前终端定义 podman 函数')
|
|
1468
|
+
.helpOption(false)
|
|
1469
|
+
.allowUnknownOption()
|
|
1470
|
+
.passThroughOptions()
|
|
1471
|
+
.action(args => selectAction('podman', { podmanArgs: args || [] }));
|
|
1472
|
+
|
|
1473
|
+
program.command('setup')
|
|
1474
|
+
.description('命令行配置向导(无头环境:SSH、没有图形界面):选 Agent、填 Key、设登录密码,可选软件源')
|
|
1475
|
+
.action(() => selectAction('setup', {}));
|
|
1280
1476
|
|
|
1281
1477
|
program.command('install <name>')
|
|
1282
1478
|
.description(`安装 ${MANYOYO_NAME} 命令 (docker-cli-plugin)`)
|
|
@@ -1294,9 +1490,10 @@ Notes:
|
|
|
1294
1490
|
// Docker CLI plugin mode - remove first arg if running as plugin
|
|
1295
1491
|
normalizeDockerPluginArgv(process.argv);
|
|
1296
1492
|
|
|
1297
|
-
// No args:
|
|
1298
|
-
|
|
1299
|
-
|
|
1493
|
+
// No args (or only --headless / --gui): start (or reuse) the local web app; open the browser already logged in unless headless
|
|
1494
|
+
const launcherArgs = parseLauncherArgs(process.argv.slice(2));
|
|
1495
|
+
if (launcherArgs) {
|
|
1496
|
+
await runAppLauncher({ force: launcherArgs.force });
|
|
1300
1497
|
}
|
|
1301
1498
|
|
|
1302
1499
|
// Pre-handle -x/--shell-full: treat all following args as a single command
|
|
@@ -1329,16 +1526,58 @@ Notes:
|
|
|
1329
1526
|
throw new Error('serve --stop 与 --restart 不能同时使用');
|
|
1330
1527
|
}
|
|
1331
1528
|
|
|
1332
|
-
|
|
1529
|
+
UPDATE_CHECK_ENABLED = config.updateCheck !== false;
|
|
1530
|
+
MIRRORS = normalizeMirrors(config.mirrors);
|
|
1531
|
+
const noDockerActions = new Set(['init', 'update', 'install', 'config-show', 'plugin', 'doctor', 'uninstall', 'podman', 'setup']);
|
|
1333
1532
|
if (isServerStopMode) {
|
|
1334
1533
|
noDockerActions.add('serve');
|
|
1335
1534
|
}
|
|
1336
1535
|
if (!noDockerActions.has(selectedAction)) {
|
|
1337
|
-
ensureDocker();
|
|
1536
|
+
await ensureDocker(config.containerRuntime, { deferHeal: selectedAction === 'serve' });
|
|
1338
1537
|
}
|
|
1339
1538
|
|
|
1340
1539
|
if (options.update) {
|
|
1341
|
-
updateManyoyo();
|
|
1540
|
+
await updateManyoyo(options, config);
|
|
1541
|
+
process.exit(0);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
if (selectedAction === 'setup') {
|
|
1545
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
1546
|
+
const code = await runSetupCli({
|
|
1547
|
+
isTTY: interactive,
|
|
1548
|
+
prompter: interactive ? createPrompter() : null,
|
|
1549
|
+
configPath: getManyoyoConfigPath(),
|
|
1550
|
+
defaultWorkpath: path.join(os.homedir(), '.manyoyo', 'work'),
|
|
1551
|
+
validateHostPath: value => validateHostPathOrThrow(value),
|
|
1552
|
+
commandName: MANYOYO_NAME,
|
|
1553
|
+
log: line => console.log(line),
|
|
1554
|
+
// 新密码要让后台服务重新读配置:先停掉旧的,再按当前环境(有头 / 无头)启动
|
|
1555
|
+
afterSave: async () => {
|
|
1556
|
+
stopBackgroundApp({ quiet: true });
|
|
1557
|
+
await runAppLauncher({ exit: false });
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
process.exit(code);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (selectedAction === 'podman') {
|
|
1564
|
+
process.exit(runPodmanCommand(options.podmanArgs, { commandName: MANYOYO_NAME }));
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (selectedAction === 'uninstall') {
|
|
1568
|
+
await runUninstall({
|
|
1569
|
+
yes: yesMode,
|
|
1570
|
+
ask: askQuestion,
|
|
1571
|
+
log: line => console.log(line),
|
|
1572
|
+
selectExternalRuntime: () => {
|
|
1573
|
+
try {
|
|
1574
|
+
const selected = selectContainerRuntime({ configured: config.containerRuntime });
|
|
1575
|
+
return selected.source === 'private-podman' ? null : selected;
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
return null;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1342
1581
|
process.exit(0);
|
|
1343
1582
|
}
|
|
1344
1583
|
|
|
@@ -1482,6 +1721,8 @@ Notes:
|
|
|
1482
1721
|
worktreeRepoRoot: resolvedRuntime.worktreeRepoRoot,
|
|
1483
1722
|
worktreeMainRepoRoot: resolvedRuntime.worktreeMainRepoRoot,
|
|
1484
1723
|
containerMode: contModeValue || "",
|
|
1724
|
+
containerRuntime: config.containerRuntime || "auto",
|
|
1725
|
+
mirrors: MIRRORS,
|
|
1485
1726
|
shellPrefix: EXEC_COMMAND_PREFIX.trim(),
|
|
1486
1727
|
shell: EXEC_COMMAND || "",
|
|
1487
1728
|
shellSuffix: EXEC_COMMAND_SUFFIX || "",
|
|
@@ -1524,8 +1765,13 @@ Notes:
|
|
|
1524
1765
|
const portStatus = Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535
|
|
1525
1766
|
? await checkPortAvailability(parsedPort)
|
|
1526
1767
|
: undefined;
|
|
1527
|
-
|
|
1528
|
-
|
|
1768
|
+
let doctorRuntime = null;
|
|
1769
|
+
let report = await runDoctorChecks({
|
|
1770
|
+
selectRuntime: () => {
|
|
1771
|
+
doctorRuntime = selectContainerRuntime({ configured: config.containerRuntime });
|
|
1772
|
+
return doctorRuntime;
|
|
1773
|
+
},
|
|
1774
|
+
runCommand: runRuntimeCommand,
|
|
1529
1775
|
configExists: fs.existsSync(getManyoyoConfigPath()),
|
|
1530
1776
|
imageName: IMAGE_NAME,
|
|
1531
1777
|
imageVersion: IMAGE_VERSION,
|
|
@@ -1534,11 +1780,49 @@ Notes:
|
|
|
1534
1780
|
pluginConfig: config.plugins,
|
|
1535
1781
|
portStatus
|
|
1536
1782
|
});
|
|
1783
|
+
if (options.fix) {
|
|
1784
|
+
report = await applyDoctorFixes(report, {
|
|
1785
|
+
startRuntime: async () => {
|
|
1786
|
+
const result = await ensureRuntimeReady({ runtime: doctorRuntime, run: runRuntimeCommand });
|
|
1787
|
+
return { fixed: result.status === 'started' || result.status === 'ready', message: result.message };
|
|
1788
|
+
},
|
|
1789
|
+
pullImage: async () => {
|
|
1790
|
+
const image = `${IMAGE_NAME}:${IMAGE_VERSION}`;
|
|
1791
|
+
try {
|
|
1792
|
+
console.error(`正在拉取 ${image} ...`);
|
|
1793
|
+
await pullImageProcess({
|
|
1794
|
+
command: doctorRuntime.command,
|
|
1795
|
+
env: mergeRuntimeEnv(doctorRuntime.env),
|
|
1796
|
+
imageRef: image,
|
|
1797
|
+
onOutput: text => process.stderr.write(text)
|
|
1798
|
+
});
|
|
1799
|
+
} catch (e) {
|
|
1800
|
+
throw new Error(`拉取 ${image} 失败;可执行 ${MANYOYO_NAME} build --iv ${IMAGE_VERSION} 本地构建`);
|
|
1801
|
+
}
|
|
1802
|
+
return { fixed: true, message: `已拉取 ${image}` };
|
|
1803
|
+
},
|
|
1804
|
+
createConfig: () => {
|
|
1805
|
+
const result = syncGlobalImageVersion(IMAGE_VERSION);
|
|
1806
|
+
return { fixed: result.reason === 'created', message: result.reason === 'created' ? `已生成 ${result.path}` : `未生成配置: ${result.reason}` };
|
|
1807
|
+
},
|
|
1808
|
+
suggestPort: async () => {
|
|
1809
|
+
for (let candidate = parsedPort + 1; candidate <= Math.min(parsedPort + 50, 65535); candidate += 1) {
|
|
1810
|
+
if (await checkPortAvailability(candidate) === 'available') {
|
|
1811
|
+
return { fixed: false, message: `建议使用空闲端口 ${candidate}` };
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
return { fixed: false, message: '附近没有空闲端口,请手动指定' };
|
|
1815
|
+
}
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1537
1818
|
if (options.json) {
|
|
1538
1819
|
console.log(JSON.stringify(report, null, 4));
|
|
1539
1820
|
} else {
|
|
1540
1821
|
report.checks.forEach(check => {
|
|
1541
1822
|
console.log(`[${check.status.toUpperCase()}] ${check.code}: ${check.summary}${check.action ? ` (${check.action})` : ''}`);
|
|
1823
|
+
if (check.fix && check.fix.attempted) {
|
|
1824
|
+
console.log(` ${check.fix.fixed ? '已修复' : '未修复'}: ${check.fix.message}`);
|
|
1825
|
+
}
|
|
1542
1826
|
});
|
|
1543
1827
|
}
|
|
1544
1828
|
process.exit(report.ok ? 0 : 1);
|
|
@@ -1580,6 +1864,7 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1580
1864
|
contModeArgs: CONT_MODE_ARGS,
|
|
1581
1865
|
containerExtraArgs: CONTAINER_EXTRA_ARGS,
|
|
1582
1866
|
containerEnvs: CONTAINER_ENVS,
|
|
1867
|
+
mirrors: MIRRORS,
|
|
1583
1868
|
firstContainerEnvs: FIRST_CONTAINER_ENVS,
|
|
1584
1869
|
containerVolumes: CONTAINER_VOLUMES,
|
|
1585
1870
|
containerPorts: CONTAINER_PORTS,
|
|
@@ -1815,8 +2100,9 @@ function relaunchServeDetached(runtime) {
|
|
|
1815
2100
|
/**
|
|
1816
2101
|
* 等待容器就绪(使用指数退避算法)
|
|
1817
2102
|
* @param {string} containerName - 容器名称
|
|
2103
|
+
* @param {{throwOnFailure?: boolean}} [options] serve 路径传 true:失败抛错而不是 process.exit,调用方才能清理容器
|
|
1818
2104
|
*/
|
|
1819
|
-
async function waitForContainerReady(containerName) {
|
|
2105
|
+
async function waitForContainerReady(containerName, options = {}) {
|
|
1820
2106
|
const MAX_RETRIES = CONFIG.CONTAINER_READY_MAX_RETRIES;
|
|
1821
2107
|
let retryDelay = CONFIG.CONTAINER_READY_INITIAL_DELAY;
|
|
1822
2108
|
|
|
@@ -1829,6 +2115,9 @@ async function waitForContainerReady(containerName) {
|
|
|
1829
2115
|
}
|
|
1830
2116
|
|
|
1831
2117
|
if (status === 'exited') {
|
|
2118
|
+
if (options.throwOnFailure) {
|
|
2119
|
+
throw Object.assign(new Error('容器启动后立即退出'), { containerExited: true });
|
|
2120
|
+
}
|
|
1832
2121
|
console.log(`${RED}⚠️ 错误: 容器启动后立即退出。${NC}`);
|
|
1833
2122
|
dockerExecArgs(['logs', containerName], { stdio: 'inherit' });
|
|
1834
2123
|
process.exit(1);
|
|
@@ -1837,11 +2126,15 @@ async function waitForContainerReady(containerName) {
|
|
|
1837
2126
|
await sleep(retryDelay);
|
|
1838
2127
|
retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
|
|
1839
2128
|
} catch (e) {
|
|
2129
|
+
if (e && e.containerExited) throw e;
|
|
1840
2130
|
await sleep(retryDelay);
|
|
1841
2131
|
retryDelay = Math.min(retryDelay * 2, CONFIG.CONTAINER_READY_MAX_DELAY);
|
|
1842
2132
|
}
|
|
1843
2133
|
}
|
|
1844
2134
|
|
|
2135
|
+
if (options.throwOnFailure) {
|
|
2136
|
+
throw new Error('容器启动超时');
|
|
2137
|
+
}
|
|
1845
2138
|
console.log(`${RED}⚠️ 错误: 容器启动超时。${NC}`);
|
|
1846
2139
|
process.exit(1);
|
|
1847
2140
|
}
|
|
@@ -1874,7 +2167,7 @@ function executeFirstCommand(runtime) {
|
|
|
1874
2167
|
'-c',
|
|
1875
2168
|
firstCommand
|
|
1876
2169
|
];
|
|
1877
|
-
const firstExecResult = spawnSync(`${DOCKER_CMD}`, firstExecArgs, { stdio: 'inherit' });
|
|
2170
|
+
const firstExecResult = spawnSync(`${DOCKER_CMD}`, firstExecArgs, { stdio: 'inherit', env: DOCKER_ENV });
|
|
1878
2171
|
if (firstExecResult.error) {
|
|
1879
2172
|
throw firstExecResult.error;
|
|
1880
2173
|
}
|
|
@@ -1907,6 +2200,8 @@ async function createNewContainer(runtime) {
|
|
|
1907
2200
|
process.exit(0);
|
|
1908
2201
|
}
|
|
1909
2202
|
|
|
2203
|
+
await ensureRunImage(runtime);
|
|
2204
|
+
|
|
1910
2205
|
// 使用数组参数执行命令(安全方式)
|
|
1911
2206
|
try {
|
|
1912
2207
|
const args = buildDockerRunArgs(runtime);
|
|
@@ -1919,6 +2214,13 @@ async function createNewContainer(runtime) {
|
|
|
1919
2214
|
// Wait for container to be ready
|
|
1920
2215
|
await waitForContainerReady(runtime.containerName);
|
|
1921
2216
|
|
|
2217
|
+
applyAptMirror({
|
|
2218
|
+
dockerExecArgs,
|
|
2219
|
+
containerName: runtime.containerName,
|
|
2220
|
+
mirrors: runtime.mirrors,
|
|
2221
|
+
warn: message => console.warn(`${YELLOW}⚠️ ${message}${NC}`)
|
|
2222
|
+
});
|
|
2223
|
+
|
|
1922
2224
|
// Run one-time bootstrap command for newly created containers only.
|
|
1923
2225
|
executeFirstCommand(runtime);
|
|
1924
2226
|
|
|
@@ -1941,6 +2243,7 @@ function buildDockerRunArgs(runtime) {
|
|
|
1941
2243
|
containerEnvs: runtime.containerEnvs,
|
|
1942
2244
|
containerVolumes: runtime.containerVolumes,
|
|
1943
2245
|
containerPorts: runtime.containerPorts,
|
|
2246
|
+
mirrors: runtime.mirrors,
|
|
1944
2247
|
defaultCommand: runtime.execCommand
|
|
1945
2248
|
});
|
|
1946
2249
|
}
|
|
@@ -2016,9 +2319,9 @@ function executeInContainer(runtime, defaultCommand) {
|
|
|
2016
2319
|
|
|
2017
2320
|
// Execute command in container
|
|
2018
2321
|
if (runtime.execCommand) {
|
|
2019
|
-
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash', '-c', runtime.execCommand], { stdio: 'inherit' });
|
|
2322
|
+
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash', '-c', runtime.execCommand], { stdio: 'inherit', env: DOCKER_ENV });
|
|
2020
2323
|
} else {
|
|
2021
|
-
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash'], { stdio: 'inherit' });
|
|
2324
|
+
spawnSync(`${DOCKER_CMD}`, ['exec', '-it', runtime.containerName, '/bin/bash'], { stdio: 'inherit', env: DOCKER_ENV });
|
|
2022
2325
|
}
|
|
2023
2326
|
}
|
|
2024
2327
|
|
|
@@ -2079,6 +2382,38 @@ async function handlePostExit(runtime, defaultCommand) {
|
|
|
2079
2382
|
}
|
|
2080
2383
|
}
|
|
2081
2384
|
|
|
2385
|
+
async function runAppLauncher({ force = null, exit = true } = {}) {
|
|
2386
|
+
try {
|
|
2387
|
+
const { headless } = detectHeadless({ force });
|
|
2388
|
+
await launchApp({
|
|
2389
|
+
headless,
|
|
2390
|
+
authUser: String(readManyoyoConfig().config.serverUser || '').trim() || 'admin',
|
|
2391
|
+
statePath: getAppStatePath(),
|
|
2392
|
+
isProcessRunning,
|
|
2393
|
+
spawnServe: port => {
|
|
2394
|
+
// 登录走一次性令牌;未配置密码时 serve 自己生成随机密码(标记为自动生成,向导据此让用户设置)
|
|
2395
|
+
const child = spawn(process.argv[0], [process.argv[1], 'serve', `127.0.0.1:${port}`], {
|
|
2396
|
+
detached: true,
|
|
2397
|
+
stdio: 'ignore'
|
|
2398
|
+
});
|
|
2399
|
+
child.unref();
|
|
2400
|
+
return child;
|
|
2401
|
+
},
|
|
2402
|
+
issueToken: () => issueLoginToken(getLoginTokenDir()),
|
|
2403
|
+
open: url => openBrowser(url),
|
|
2404
|
+
log: line => console.log(line),
|
|
2405
|
+
logPathHint: buildManyoyoLogPath('serve').path,
|
|
2406
|
+
commandName: MANYOYO_NAME
|
|
2407
|
+
});
|
|
2408
|
+
} catch (e) {
|
|
2409
|
+
console.error(`${RED}${e.message}${NC}`);
|
|
2410
|
+
if (!exit) return false;
|
|
2411
|
+
process.exit(1);
|
|
2412
|
+
}
|
|
2413
|
+
if (!exit) return true;
|
|
2414
|
+
process.exit(0);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2082
2417
|
async function runWebServerMode(runtime) {
|
|
2083
2418
|
if (!runtime.serverAuthUser || !runtime.serverAuthPass) {
|
|
2084
2419
|
ensureWebServerAuthCredentials();
|
|
@@ -2087,6 +2422,48 @@ async function runWebServerMode(runtime) {
|
|
|
2087
2422
|
runtime.serverAuthPassAuto = SERVER_AUTH_PASS_AUTO;
|
|
2088
2423
|
}
|
|
2089
2424
|
|
|
2425
|
+
const updateChecker = createUpdateChecker({
|
|
2426
|
+
currentVersion: require('../package.json').version,
|
|
2427
|
+
installMode: appUpdate.detectInstallMode({ scriptPath: __filename }).mode,
|
|
2428
|
+
enabled: UPDATE_CHECK_ENABLED,
|
|
2429
|
+
fetchLatest: () => appUpdate.fetchLatestRelease()
|
|
2430
|
+
});
|
|
2431
|
+
|
|
2432
|
+
const needRuntimeHeal = !isRuntimeProven();
|
|
2433
|
+
if (needRuntimeHeal) {
|
|
2434
|
+
RUNTIME_STATE.status = 'starting';
|
|
2435
|
+
RUNTIME_STATE.message = '正在检查容器环境';
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// 自愈失败/超时不是终态:用户手动修好(或点向导里的“重试”)后可以再来一次
|
|
2439
|
+
let runtimeHealing = false;
|
|
2440
|
+
function startServeRuntimeHeal() {
|
|
2441
|
+
if (runtimeHealing) return false;
|
|
2442
|
+
runtimeHealing = true;
|
|
2443
|
+
RUNTIME_STATE.status = 'starting';
|
|
2444
|
+
RUNTIME_STATE.message = '正在检查容器环境';
|
|
2445
|
+
healContainerRuntime(state => {
|
|
2446
|
+
RUNTIME_STATE.status = state.status;
|
|
2447
|
+
RUNTIME_STATE.message = state.message;
|
|
2448
|
+
if (state.status === 'starting') console.log(`${YELLOW}⏳ ${state.message}...${NC}`);
|
|
2449
|
+
}, { async: true }).then(result => {
|
|
2450
|
+
if (result.status === 'ready' || result.status === 'started') {
|
|
2451
|
+
RUNTIME_STATE.status = 'ready';
|
|
2452
|
+
RUNTIME_STATE.message = '';
|
|
2453
|
+
} else {
|
|
2454
|
+
RUNTIME_STATE.status = 'failed';
|
|
2455
|
+
RUNTIME_STATE.message = result.message;
|
|
2456
|
+
console.log(`${YELLOW}⚠️ ${result.message}${NC}`);
|
|
2457
|
+
}
|
|
2458
|
+
}).catch(error => {
|
|
2459
|
+
RUNTIME_STATE.status = 'failed';
|
|
2460
|
+
RUNTIME_STATE.message = String(error && error.message || error);
|
|
2461
|
+
}).finally(() => {
|
|
2462
|
+
runtimeHealing = false;
|
|
2463
|
+
});
|
|
2464
|
+
return true;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2090
2467
|
const serverHandle = await startWebServer({
|
|
2091
2468
|
serverHost: runtime.serverHost,
|
|
2092
2469
|
serverPort: runtime.serverPort,
|
|
@@ -2095,6 +2472,31 @@ async function runWebServerMode(runtime) {
|
|
|
2095
2472
|
authPassAuto: runtime.serverAuthPassAuto,
|
|
2096
2473
|
serveTitle: runtime.serveTitle,
|
|
2097
2474
|
dockerCmd: DOCKER_CMD,
|
|
2475
|
+
dockerEnv: DOCKER_ENV,
|
|
2476
|
+
runtimeState: RUNTIME_STATE,
|
|
2477
|
+
retryRuntimeHeal: () => (needRuntimeHeal ? startServeRuntimeHeal() : false),
|
|
2478
|
+
autoPullImage: true,
|
|
2479
|
+
importState: () => readImportState(),
|
|
2480
|
+
updateInfo: () => updateChecker.getInfo(),
|
|
2481
|
+
loginTokenDir: getLoginTokenDir(),
|
|
2482
|
+
doctorCheck: async () => runDoctorChecks({
|
|
2483
|
+
selectRuntime: () => CONTAINER_RUNTIME,
|
|
2484
|
+
runCommand: runRuntimeCommandAsync,
|
|
2485
|
+
configExists: fs.existsSync(getManyoyoConfigPath()),
|
|
2486
|
+
imageName: runtime.imageName,
|
|
2487
|
+
imageVersion: runtime.imageVersion,
|
|
2488
|
+
containerMode: 'common',
|
|
2489
|
+
pluginConfig: {}
|
|
2490
|
+
}),
|
|
2491
|
+
doctorStartRuntime: async () => {
|
|
2492
|
+
const result = await ensureRuntimeReady({ runtime: CONTAINER_RUNTIME, run: runRuntimeCommandAsync });
|
|
2493
|
+
const fixed = result.status === 'started' || result.status === 'ready';
|
|
2494
|
+
if (fixed) {
|
|
2495
|
+
RUNTIME_STATE.status = 'ready';
|
|
2496
|
+
RUNTIME_STATE.message = '';
|
|
2497
|
+
}
|
|
2498
|
+
return { fixed, message: result.message };
|
|
2499
|
+
},
|
|
2098
2500
|
hostPath: runtime.hostPath,
|
|
2099
2501
|
containerPath: runtime.containerPath,
|
|
2100
2502
|
imageName: runtime.imageName,
|
|
@@ -2114,6 +2516,7 @@ async function runWebServerMode(runtime) {
|
|
|
2114
2516
|
getContainerStatus,
|
|
2115
2517
|
waitForContainerReady,
|
|
2116
2518
|
dockerExecArgs,
|
|
2519
|
+
dockerExecAsync: (args, options = {}) => runCommandAsync(DOCKER_CMD, args, { env: DOCKER_ENV, timeout: options.timeout }),
|
|
2117
2520
|
showImagePullHint,
|
|
2118
2521
|
removeContainer,
|
|
2119
2522
|
webHistoryDir: path.join(os.homedir(), '.manyoyo', 'web-history'),
|
|
@@ -2128,6 +2531,9 @@ async function runWebServerMode(runtime) {
|
|
|
2128
2531
|
logger: runtime.logger
|
|
2129
2532
|
});
|
|
2130
2533
|
writeServePidFile(runtime, serverHandle);
|
|
2534
|
+
if (needRuntimeHeal) {
|
|
2535
|
+
startServeRuntimeHeal();
|
|
2536
|
+
}
|
|
2131
2537
|
return serverHandle;
|
|
2132
2538
|
}
|
|
2133
2539
|
|
|
@@ -2189,6 +2595,7 @@ async function main() {
|
|
|
2189
2595
|
yesMode: Boolean(modeState.yesMode),
|
|
2190
2596
|
updateAgents: Boolean(modeState.updateAgents),
|
|
2191
2597
|
dockerCmd: DOCKER_CMD,
|
|
2598
|
+
dockerEnv: DOCKER_ENV,
|
|
2192
2599
|
rootDir: path.join(__dirname, '..'),
|
|
2193
2600
|
loadConfig,
|
|
2194
2601
|
runCmd,
|