@xcanwin/manyoyo 5.8.5 → 5.8.9
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 +1 -0
- package/bin/manyoyo.js +265 -174
- package/lib/global-config.js +1 -198
- package/lib/image-build.js +20 -4
- package/lib/init-config.js +22 -10
- package/lib/json5-text-edit.js +238 -0
- package/lib/plugin/playwright-bootstrap.js +116 -0
- package/lib/plugin/playwright-command-output.js +95 -0
- package/lib/plugin/playwright-container-runtime.js +94 -0
- package/lib/plugin/playwright-extension-manager.js +265 -0
- package/lib/plugin/playwright-extension-paths.js +98 -0
- package/lib/plugin/playwright-host-runtime.js +114 -0
- package/lib/plugin/playwright-scene-config.js +137 -0
- package/lib/plugin/playwright-scene-drivers.js +285 -0
- package/lib/plugin/playwright-scene-state.js +80 -0
- package/lib/plugin/playwright.js +169 -1049
- package/lib/runtime-normalizers.js +65 -0
- package/lib/runtime-resolver.js +195 -0
- package/lib/web/agent-command.js +153 -0
- package/lib/web/api-route-helpers.js +88 -0
- package/lib/web/container-exec.js +215 -0
- package/lib/web/http-handlers.js +163 -0
- package/lib/web/runtime-state.js +50 -0
- package/lib/web/server-context.js +71 -0
- package/lib/web/server-lifecycle.js +129 -0
- package/lib/web/server.js +293 -2496
- package/lib/web/session-api-routes.js +390 -0
- package/lib/web/structured-output.js +149 -0
- package/lib/web/structured-trace.js +603 -0
- package/lib/web/system-api-routes.js +114 -0
- package/lib/web/terminal-session.js +205 -0
- package/lib/web/upgrade-handler.js +94 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,6 +128,7 @@ manyoyo serve 127.0.0.1:3000
|
|
|
128
128
|
manyoyo serve 127.0.0.1:3000 -U admin -P 123456
|
|
129
129
|
manyoyo serve 127.0.0.1:3000 -U admin -P 123456 -d
|
|
130
130
|
manyoyo serve 127.0.0.1:3000 -d # 未设置密码时会打印本次随机密码
|
|
131
|
+
manyoyo serve 127.0.0.1:3000 --stop # 停止指定后台服务
|
|
131
132
|
|
|
132
133
|
# 查看配置与命令拼装
|
|
133
134
|
manyoyo config show
|
package/bin/manyoyo.js
CHANGED
|
@@ -16,6 +16,12 @@ const { buildImage } = require('../lib/image-build');
|
|
|
16
16
|
const { resolveAgentResumeArg, buildAgentResumeCommand } = require('../lib/agent-resume');
|
|
17
17
|
const { runPluginCommand, createPlugin } = require('../lib/plugin');
|
|
18
18
|
const { buildManyoyoLogPath } = require('../lib/log-path');
|
|
19
|
+
const { resolveRuntimeConfig } = require('../lib/runtime-resolver');
|
|
20
|
+
const {
|
|
21
|
+
parseEnvEntry: parseEnvEntryOrThrow,
|
|
22
|
+
expandHomeAliasPath,
|
|
23
|
+
normalizeVolume
|
|
24
|
+
} = require('../lib/runtime-normalizers');
|
|
19
25
|
const {
|
|
20
26
|
sanitizeSensitiveData,
|
|
21
27
|
sanitizeServeLogText,
|
|
@@ -454,23 +460,13 @@ async function askQuestion(prompt) {
|
|
|
454
460
|
* @param {string} env - 环境变量字符串 (KEY=VALUE)
|
|
455
461
|
*/
|
|
456
462
|
function parseEnvEntry(env) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
const key = envText.slice(0, idx);
|
|
464
|
-
const value = envText.slice(idx + 1);
|
|
465
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
466
|
-
console.error(`${RED}⚠️ 错误: env key 非法: ${key}${NC}`);
|
|
467
|
-
process.exit(1);
|
|
468
|
-
}
|
|
469
|
-
if (/[\r\n\0]/.test(value) || /[;&|`$<>]/.test(value)) {
|
|
470
|
-
console.error(`${RED}⚠️ 错误: env value 含非法字符: ${key}${NC}`);
|
|
463
|
+
try {
|
|
464
|
+
return parseEnvEntryOrThrow(env);
|
|
465
|
+
} catch (e) {
|
|
466
|
+
const message = e && e.message ? e.message : String(e);
|
|
467
|
+
console.error(`${RED}⚠️ 错误: ${message}${NC}`);
|
|
471
468
|
process.exit(1);
|
|
472
469
|
}
|
|
473
|
-
return { key, value };
|
|
474
470
|
}
|
|
475
471
|
|
|
476
472
|
function normalizeJsonEnvMap(envConfig, sourceLabel) {
|
|
@@ -570,42 +566,6 @@ function addEnvFile(envFile) {
|
|
|
570
566
|
return addEnvFileTo(CONTAINER_ENVS, envFile);
|
|
571
567
|
}
|
|
572
568
|
|
|
573
|
-
function expandHomeAliasPath(filePath) {
|
|
574
|
-
const text = String(filePath || '').trim();
|
|
575
|
-
const homeDir = process.env.HOME || os.homedir();
|
|
576
|
-
|
|
577
|
-
if (text === '~') {
|
|
578
|
-
return homeDir;
|
|
579
|
-
}
|
|
580
|
-
if (text.startsWith('~/')) {
|
|
581
|
-
return path.join(homeDir, text.slice(2));
|
|
582
|
-
}
|
|
583
|
-
if (text === '$HOME') {
|
|
584
|
-
return homeDir;
|
|
585
|
-
}
|
|
586
|
-
if (text.startsWith('$HOME/')) {
|
|
587
|
-
return path.join(homeDir, text.slice('$HOME/'.length));
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
return text;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
function normalizeVolume(volume) {
|
|
594
|
-
const text = String(volume || '').trim();
|
|
595
|
-
if (!text.startsWith('~') && !text.startsWith('$HOME')) {
|
|
596
|
-
return text;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const separatorIndex = text.indexOf(':');
|
|
600
|
-
if (separatorIndex === -1) {
|
|
601
|
-
return expandHomeAliasPath(text);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
const hostPath = text.slice(0, separatorIndex);
|
|
605
|
-
const rest = text.slice(separatorIndex);
|
|
606
|
-
return `${expandHomeAliasPath(hostPath)}${rest}`;
|
|
607
|
-
}
|
|
608
|
-
|
|
609
569
|
function hasEnvKey(targetEnvs, key) {
|
|
610
570
|
for (let i = 0; i < targetEnvs.length; i += 2) {
|
|
611
571
|
if (targetEnvs[i] !== '--env') {
|
|
@@ -1004,11 +964,7 @@ function validateShellSuffixPassThroughArgs(command) {
|
|
|
1004
964
|
}
|
|
1005
965
|
}
|
|
1006
966
|
|
|
1007
|
-
function
|
|
1008
|
-
const includeRmOnExit = options.includeRmOnExit !== false;
|
|
1009
|
-
const includeServePreview = options.includeServePreview === true;
|
|
1010
|
-
const includeWebAuthOptions = options.includeWebAuthOptions === true;
|
|
1011
|
-
|
|
967
|
+
function applyContainerBaseOptions(command) {
|
|
1012
968
|
command
|
|
1013
969
|
.option('-r, --run <name>', '加载运行配置 (从 ~/.manyoyo/manyoyo.json 的 runs.<name> 读取)')
|
|
1014
970
|
.option('--hp, --host-path <path>', '设置宿主机工作目录 (默认: 当前路径)')
|
|
@@ -1023,35 +979,69 @@ function applyRunStyleOptions(command, options = {}) {
|
|
|
1023
979
|
appendArrayOption(command, '-v, --volume <volume>', '绑定挂载卷 XXX:YYY (可多次使用)');
|
|
1024
980
|
appendArrayOption(command, '-p, --port <port>', '设置端口映射 XXX:YYY (可多次使用)');
|
|
1025
981
|
|
|
982
|
+
return command;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function applyExecOptions(command) {
|
|
1026
986
|
command
|
|
1027
987
|
.option('--sp, --shell-prefix <command>', '主命令前缀 (常用于临时环境变量)')
|
|
1028
988
|
.option('-s, --shell <command>', '主命令')
|
|
1029
989
|
.option('--ss, --shell-suffix <command>', '主命令后缀 (追加到 -s 之后,等价于 -- <args>)')
|
|
1030
|
-
.option('--first-shell-prefix <command>', '首次预执行命令前缀 (仅新建容器生效; 容器已存在时忽略)')
|
|
1031
|
-
.option('--first-shell <command>', '首次预执行命令 (仅新建容器生效; 容器已存在时忽略)')
|
|
1032
|
-
.option('--first-shell-suffix <command>', '首次预执行命令后缀 (仅新建容器生效; 容器已存在时忽略)')
|
|
1033
990
|
.option('-x, --shell-full <command...>', '完整命令 (与 --sp/-s/--ss/-- 互斥)')
|
|
1034
991
|
.option('-y, --yolo <cli>', '使 AGENT 无需确认 (claude(c), gemini(gm), codex(cx), opencode(oc))');
|
|
992
|
+
|
|
993
|
+
return command;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function applyFirstExecOptions(command) {
|
|
997
|
+
command
|
|
998
|
+
.option('--first-shell-prefix <command>', '首次预执行命令前缀 (仅新建容器生效; 容器已存在时忽略)')
|
|
999
|
+
.option('--first-shell <command>', '首次预执行命令 (仅新建容器生效; 容器已存在时忽略)')
|
|
1000
|
+
.option('--first-shell-suffix <command>', '首次预执行命令后缀 (仅新建容器生效; 容器已存在时忽略)');
|
|
1035
1001
|
appendArrayOption(command, '--first-env <env>', '首次预执行环境变量 XXX=YYY (可多次使用)');
|
|
1036
1002
|
appendArrayOption(command, '--first-env-file <file>', '首次预执行环境变量文件 (仅支持绝对路径,如 /abs/path.env)');
|
|
1037
1003
|
|
|
1004
|
+
return command;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function applyQuietOptions(command) {
|
|
1008
|
+
appendArrayOption(command, '-q, --quiet <item>', '静默输出 (可多次使用: cnew, crm, tip, cmd, full)');
|
|
1009
|
+
return command;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function applyServeAuthOptions(command) {
|
|
1013
|
+
command
|
|
1014
|
+
.option('-U, --user <username>', '网页服务登录用户名 (默认 admin)')
|
|
1015
|
+
.option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
|
|
1016
|
+
return command;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function applyRunStyleOptions(command, options = {}) {
|
|
1020
|
+
const includeRmOnExit = options.includeRmOnExit !== false;
|
|
1021
|
+
const includeServePreview = options.includeServePreview === true;
|
|
1022
|
+
const includeWebAuthOptions = options.includeWebAuthOptions === true;
|
|
1023
|
+
const includeFirstExecOptions = options.includeFirstExecOptions !== false;
|
|
1024
|
+
|
|
1025
|
+
applyContainerBaseOptions(command);
|
|
1026
|
+
applyExecOptions(command);
|
|
1027
|
+
if (includeFirstExecOptions) {
|
|
1028
|
+
applyFirstExecOptions(command);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1038
1031
|
if (includeRmOnExit) {
|
|
1039
1032
|
command.option('--rm-on-exit', '退出后自动删除容器 (一次性模式)');
|
|
1040
1033
|
}
|
|
1041
1034
|
|
|
1042
|
-
|
|
1035
|
+
applyQuietOptions(command);
|
|
1043
1036
|
|
|
1044
1037
|
if (includeServePreview) {
|
|
1045
1038
|
command
|
|
1046
|
-
.option('--serve [listen]', '按 serve 模式解析配置 (仅支持 <ip:port>)')
|
|
1047
|
-
|
|
1048
|
-
.option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
|
|
1039
|
+
.option('--serve [listen]', '按 serve 模式解析配置 (仅支持 <ip:port>)');
|
|
1040
|
+
applyServeAuthOptions(command);
|
|
1049
1041
|
}
|
|
1050
1042
|
|
|
1051
1043
|
if (includeWebAuthOptions) {
|
|
1052
|
-
command
|
|
1053
|
-
.option('-U, --user <username>', '网页服务登录用户名 (默认 admin)')
|
|
1054
|
-
.option('-P, --pass <password>', '网页服务登录密码 (默认自动生成随机密码)');
|
|
1044
|
+
applyServeAuthOptions(command);
|
|
1055
1045
|
}
|
|
1056
1046
|
|
|
1057
1047
|
return command;
|
|
@@ -1214,8 +1204,13 @@ Notes:
|
|
|
1214
1204
|
.action(() => selectAction('images', { imageList: true }));
|
|
1215
1205
|
|
|
1216
1206
|
const serveCommand = program.command('serve [listen]').description('启动网页交互服务 (默认 127.0.0.1:3000)');
|
|
1217
|
-
applyRunStyleOptions(serveCommand, {
|
|
1207
|
+
applyRunStyleOptions(serveCommand, {
|
|
1208
|
+
includeRmOnExit: false,
|
|
1209
|
+
includeWebAuthOptions: true,
|
|
1210
|
+
includeFirstExecOptions: false
|
|
1211
|
+
});
|
|
1218
1212
|
serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
|
|
1213
|
+
serveCommand.option('--stop', '停止后台网页服务;必须显式传入 listen');
|
|
1219
1214
|
serveCommand.action((listen, options) => {
|
|
1220
1215
|
selectAction('serve', {
|
|
1221
1216
|
...options,
|
|
@@ -1259,7 +1254,10 @@ Notes:
|
|
|
1259
1254
|
});
|
|
1260
1255
|
|
|
1261
1256
|
const configRunCommand = configCommand.command('command').description('显示将执行的 docker run 命令并退出');
|
|
1262
|
-
applyRunStyleOptions(configRunCommand, {
|
|
1257
|
+
applyRunStyleOptions(configRunCommand, {
|
|
1258
|
+
includeRmOnExit: false,
|
|
1259
|
+
includeFirstExecOptions: false
|
|
1260
|
+
});
|
|
1263
1261
|
enableShellSuffixPassThrough(configRunCommand);
|
|
1264
1262
|
configRunCommand.action((options, command) => {
|
|
1265
1263
|
validateShellSuffixPassThroughArgs(command);
|
|
@@ -1317,8 +1315,12 @@ Notes:
|
|
|
1317
1315
|
const isShowConfigMode = selectedAction === 'config-show';
|
|
1318
1316
|
const isShowCommandMode = selectedAction === 'config-command';
|
|
1319
1317
|
const isServerMode = options.server !== undefined;
|
|
1318
|
+
const isServerStopMode = Boolean(selectedAction === 'serve' && options.stop);
|
|
1320
1319
|
|
|
1321
1320
|
const noDockerActions = new Set(['init', 'update', 'install', 'config-show', 'plugin']);
|
|
1321
|
+
if (isServerStopMode) {
|
|
1322
|
+
noDockerActions.add('serve');
|
|
1323
|
+
}
|
|
1322
1324
|
if (!noDockerActions.has(selectedAction)) {
|
|
1323
1325
|
ensureDocker();
|
|
1324
1326
|
}
|
|
@@ -1362,145 +1364,96 @@ Notes:
|
|
|
1362
1364
|
const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
|
|
1363
1365
|
const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
|
|
1364
1366
|
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
if (mergedFirstShellSuffix) {
|
|
1404
|
-
FIRST_EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedFirstShellSuffix);
|
|
1405
|
-
}
|
|
1367
|
+
const resolvedRuntime = resolveRuntimeConfig({
|
|
1368
|
+
cliOptions: options,
|
|
1369
|
+
globalConfig: config,
|
|
1370
|
+
runConfig,
|
|
1371
|
+
globalFirstConfig,
|
|
1372
|
+
runFirstConfig,
|
|
1373
|
+
defaults: {
|
|
1374
|
+
hostPath: HOST_PATH,
|
|
1375
|
+
containerName: CONTAINER_NAME,
|
|
1376
|
+
containerPath: CONTAINER_PATH,
|
|
1377
|
+
imageName: IMAGE_NAME,
|
|
1378
|
+
imageVersion: IMAGE_VERSION
|
|
1379
|
+
},
|
|
1380
|
+
envVars: process.env,
|
|
1381
|
+
argv: process.argv,
|
|
1382
|
+
isServerMode,
|
|
1383
|
+
isServerStopMode,
|
|
1384
|
+
pickConfigValue,
|
|
1385
|
+
resolveContainerNameTemplate,
|
|
1386
|
+
normalizeCommandSuffix,
|
|
1387
|
+
normalizeJsonEnvMap,
|
|
1388
|
+
normalizeCliEnvMap,
|
|
1389
|
+
mergeArrayConfig,
|
|
1390
|
+
normalizeVolume,
|
|
1391
|
+
parseServerListen
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
HOST_PATH = resolvedRuntime.hostPath;
|
|
1395
|
+
CONTAINER_NAME = resolvedRuntime.containerName;
|
|
1396
|
+
CONTAINER_PATH = resolvedRuntime.containerPath;
|
|
1397
|
+
IMAGE_NAME = resolvedRuntime.imageName;
|
|
1398
|
+
IMAGE_VERSION = resolvedRuntime.imageVersion;
|
|
1399
|
+
EXEC_COMMAND_PREFIX = resolvedRuntime.exec.prefix;
|
|
1400
|
+
EXEC_COMMAND = resolvedRuntime.exec.shell;
|
|
1401
|
+
EXEC_COMMAND_SUFFIX = resolvedRuntime.exec.suffix;
|
|
1402
|
+
FIRST_EXEC_COMMAND_PREFIX = resolvedRuntime.first.exec.prefix;
|
|
1403
|
+
FIRST_EXEC_COMMAND = resolvedRuntime.first.exec.shell;
|
|
1404
|
+
FIRST_EXEC_COMMAND_SUFFIX = resolvedRuntime.first.exec.suffix;
|
|
1406
1405
|
|
|
1407
1406
|
// Basic name validation to reduce injection risk
|
|
1408
1407
|
validateName('containerName', CONTAINER_NAME, SAFE_CONTAINER_NAME_PATTERN);
|
|
1409
1408
|
validateName('imageName', IMAGE_NAME, /^[A-Za-z0-9][A-Za-z0-9._/:-]*$/);
|
|
1410
1409
|
validateImageVersion(IMAGE_VERSION);
|
|
1411
1410
|
|
|
1412
|
-
|
|
1413
|
-
const toArray = (val) => Array.isArray(val) ? val : (val ? [val] : []);
|
|
1414
|
-
const envFileList = [
|
|
1415
|
-
...toArray(config.envFile),
|
|
1416
|
-
...toArray(runConfig.envFile),
|
|
1417
|
-
...(options.envFile || [])
|
|
1418
|
-
].filter(Boolean);
|
|
1411
|
+
const envFileList = resolvedRuntime.envFile;
|
|
1419
1412
|
envFileList.forEach(ef => addEnvFile(ef));
|
|
1420
1413
|
|
|
1421
1414
|
// env in JSON config uses map type, and is merged by key with CLI priority.
|
|
1422
|
-
const envMap =
|
|
1423
|
-
...normalizeJsonEnvMap(config.env, '全局配置'),
|
|
1424
|
-
...normalizeJsonEnvMap(runConfig.env, '运行配置'),
|
|
1425
|
-
...normalizeCliEnvMap(options.env)
|
|
1426
|
-
};
|
|
1415
|
+
const envMap = resolvedRuntime.env;
|
|
1427
1416
|
Object.entries(envMap).forEach(([key, value]) => addEnv(`${key}=${value}`));
|
|
1428
1417
|
|
|
1429
|
-
const firstEnvFileList =
|
|
1430
|
-
...toArray(globalFirstConfig.envFile),
|
|
1431
|
-
...toArray(runFirstConfig.envFile),
|
|
1432
|
-
...(options.firstEnvFile || [])
|
|
1433
|
-
].filter(Boolean);
|
|
1418
|
+
const firstEnvFileList = resolvedRuntime.first.envFile;
|
|
1434
1419
|
firstEnvFileList.forEach(ef => addEnvFileTo(FIRST_CONTAINER_ENVS, ef));
|
|
1435
1420
|
|
|
1436
|
-
const firstEnvMap =
|
|
1437
|
-
...normalizeJsonEnvMap(globalFirstConfig.env, '全局配置 first'),
|
|
1438
|
-
...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first'),
|
|
1439
|
-
...normalizeCliEnvMap(options.firstEnv)
|
|
1440
|
-
};
|
|
1421
|
+
const firstEnvMap = resolvedRuntime.first.env;
|
|
1441
1422
|
Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
|
|
1442
1423
|
|
|
1443
1424
|
applyPlaywrightCliSessionIntegration(config, runConfig);
|
|
1444
1425
|
|
|
1445
|
-
const volumeList =
|
|
1446
|
-
.map(normalizeVolume);
|
|
1426
|
+
const volumeList = resolvedRuntime.volumes;
|
|
1447
1427
|
volumeList.forEach(v => addVolume(v));
|
|
1448
1428
|
|
|
1449
|
-
const portList =
|
|
1429
|
+
const portList = resolvedRuntime.ports;
|
|
1450
1430
|
portList.forEach(p => addPort(p));
|
|
1451
1431
|
|
|
1452
|
-
const buildArgList =
|
|
1432
|
+
const buildArgList = resolvedRuntime.imageBuildArgs;
|
|
1453
1433
|
buildArgList.forEach(arg => addImageBuildArg(arg));
|
|
1454
1434
|
|
|
1455
1435
|
// Override mode for special options
|
|
1456
|
-
const yoloValue =
|
|
1436
|
+
const yoloValue = resolvedRuntime.yolo;
|
|
1457
1437
|
if (yoloValue) setYolo(yoloValue);
|
|
1458
1438
|
|
|
1459
|
-
const contModeValue =
|
|
1439
|
+
const contModeValue = resolvedRuntime.containerMode;
|
|
1460
1440
|
if (contModeValue) setContMode(contModeValue);
|
|
1461
1441
|
|
|
1462
|
-
const quietValue =
|
|
1442
|
+
const quietValue = resolvedRuntime.quiet;
|
|
1463
1443
|
if (quietValue) setQuiet(quietValue);
|
|
1464
1444
|
|
|
1465
|
-
// Handle shell-full (variadic arguments)
|
|
1466
|
-
if (options.shellFull) {
|
|
1467
|
-
EXEC_COMMAND = options.shellFull.join(' ');
|
|
1468
|
-
EXEC_COMMAND_PREFIX = "";
|
|
1469
|
-
EXEC_COMMAND_SUFFIX = "";
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// Handle -- suffix arguments
|
|
1473
|
-
if (!options.shellFull) {
|
|
1474
|
-
const doubleDashIndex = process.argv.indexOf('--');
|
|
1475
|
-
if (doubleDashIndex !== -1 && doubleDashIndex < process.argv.length - 1) {
|
|
1476
|
-
EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(process.argv.slice(doubleDashIndex + 1).join(' '));
|
|
1477
|
-
}
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
1445
|
if (options.rmOnExit) {
|
|
1481
1446
|
RM_ON_EXIT = true;
|
|
1482
1447
|
}
|
|
1483
1448
|
|
|
1484
1449
|
if (isServerMode) {
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
SERVER_PORT = serverListen.port;
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
const serverUserValue = pickConfigValue(options.serverUser, runConfig.serverUser, config.serverUser, process.env.MANYOYO_SERVER_USER);
|
|
1491
|
-
if (serverUserValue) {
|
|
1492
|
-
SERVER_AUTH_USER = String(serverUserValue);
|
|
1450
|
+
SERVER_HOST = resolvedRuntime.serverHost;
|
|
1451
|
+
SERVER_PORT = resolvedRuntime.serverPort;
|
|
1493
1452
|
}
|
|
1494
1453
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
SERVER_AUTH_PASS_AUTO = false;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
if (isServerMode) {
|
|
1502
|
-
ensureWebServerAuthCredentials();
|
|
1503
|
-
}
|
|
1454
|
+
SERVER_AUTH_USER = resolvedRuntime.serverUser || '';
|
|
1455
|
+
SERVER_AUTH_PASS = resolvedRuntime.serverPass || '';
|
|
1456
|
+
SERVER_AUTH_PASS_AUTO = resolvedRuntime.serverPassAuto === true;
|
|
1504
1457
|
|
|
1505
1458
|
if (isShowConfigMode) {
|
|
1506
1459
|
const finalConfig = {
|
|
@@ -1520,9 +1473,9 @@ Notes:
|
|
|
1520
1473
|
shellSuffix: EXEC_COMMAND_SUFFIX || "",
|
|
1521
1474
|
yolo: yoloValue || "",
|
|
1522
1475
|
quiet: quietValue || [],
|
|
1523
|
-
server:
|
|
1524
|
-
serverHost:
|
|
1525
|
-
serverPort:
|
|
1476
|
+
server: resolvedRuntime.server,
|
|
1477
|
+
serverHost: resolvedRuntime.server ? SERVER_HOST : null,
|
|
1478
|
+
serverPort: resolvedRuntime.server ? SERVER_PORT : null,
|
|
1526
1479
|
serverUser: SERVER_AUTH_USER || "",
|
|
1527
1480
|
serverPass: SERVER_AUTH_PASS || "",
|
|
1528
1481
|
exec: {
|
|
@@ -1560,7 +1513,9 @@ Notes:
|
|
|
1560
1513
|
isRemoveMode,
|
|
1561
1514
|
isShowCommandMode,
|
|
1562
1515
|
isServerMode,
|
|
1516
|
+
isServerStop: isServerStopMode,
|
|
1563
1517
|
isServerDetach: Boolean(selectedAction === 'serve' && options.detach),
|
|
1518
|
+
isServerListenSpecified: Boolean(isServerMode && options.server !== true),
|
|
1564
1519
|
isPluginMode: false
|
|
1565
1520
|
};
|
|
1566
1521
|
}
|
|
@@ -1588,7 +1543,9 @@ function createRuntimeContext(modeState = {}) {
|
|
|
1588
1543
|
showCommand: Boolean(modeState.isShowCommandMode),
|
|
1589
1544
|
rmOnExit: RM_ON_EXIT,
|
|
1590
1545
|
serverMode: Boolean(modeState.isServerMode),
|
|
1546
|
+
serverStop: Boolean(modeState.isServerStop),
|
|
1591
1547
|
serverDetach: Boolean(modeState.isServerDetach),
|
|
1548
|
+
serverListenSpecified: Boolean(modeState.isServerListenSpecified),
|
|
1592
1549
|
serverHost: SERVER_HOST,
|
|
1593
1550
|
serverPort: SERVER_PORT,
|
|
1594
1551
|
serverAuthUser: SERVER_AUTH_USER,
|
|
@@ -1657,10 +1614,138 @@ function buildDetachedServeEnv(runtime) {
|
|
|
1657
1614
|
return env;
|
|
1658
1615
|
}
|
|
1659
1616
|
|
|
1617
|
+
function formatServeListenHost(host) {
|
|
1618
|
+
const text = String(host || '').trim() || '127.0.0.1';
|
|
1619
|
+
if (text.includes(':') && !text.startsWith('[')) {
|
|
1620
|
+
return `[${text}]`;
|
|
1621
|
+
}
|
|
1622
|
+
return text;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
function buildServeListenLabel(host, port) {
|
|
1626
|
+
return `${formatServeListenHost(host)}:${port}`;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function buildServePidFile(host, port, homeDir = os.homedir()) {
|
|
1630
|
+
const dir = path.join(homeDir, '.manyoyo', 'run', 'serve');
|
|
1631
|
+
const listen = buildServeListenLabel(host, port);
|
|
1632
|
+
const safeName = listen.replace(/[^A-Za-z0-9_.-]+/g, '_');
|
|
1633
|
+
return {
|
|
1634
|
+
dir,
|
|
1635
|
+
listen,
|
|
1636
|
+
path: path.join(dir, `${safeName}.pid`)
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function removeServePidFile(filePath) {
|
|
1641
|
+
if (!filePath) return;
|
|
1642
|
+
try {
|
|
1643
|
+
fs.rmSync(filePath, { force: true });
|
|
1644
|
+
} catch (e) {
|
|
1645
|
+
// ignore cleanup failures
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function isProcessRunning(pid) {
|
|
1650
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
try {
|
|
1654
|
+
process.kill(pid, 0);
|
|
1655
|
+
return true;
|
|
1656
|
+
} catch (e) {
|
|
1657
|
+
return e && e.code !== 'ESRCH';
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function readServePidFile(filePath) {
|
|
1662
|
+
try {
|
|
1663
|
+
const text = fs.readFileSync(filePath, 'utf-8').trim();
|
|
1664
|
+
if (!/^\d+$/.test(text)) {
|
|
1665
|
+
return 0;
|
|
1666
|
+
}
|
|
1667
|
+
return Number(text);
|
|
1668
|
+
} catch (e) {
|
|
1669
|
+
return 0;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function getServePidTarget(host, port, homeDir = os.homedir()) {
|
|
1674
|
+
const pidFile = buildServePidFile(host, port, homeDir);
|
|
1675
|
+
const pid = readServePidFile(pidFile.path);
|
|
1676
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isProcessRunning(pid)) {
|
|
1677
|
+
removeServePidFile(pidFile.path);
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
return {
|
|
1681
|
+
pid,
|
|
1682
|
+
listen: pidFile.listen,
|
|
1683
|
+
path: pidFile.path
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function installServePidCleanup(pidFilePath, logger) {
|
|
1688
|
+
if (!pidFilePath || global.__manyoyoServePidCleanupInstalled) {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
global.__manyoyoServePidCleanupInstalled = true;
|
|
1692
|
+
process.on('exit', () => {
|
|
1693
|
+
removeServePidFile(pidFilePath);
|
|
1694
|
+
if (logger && typeof logger.info === 'function') {
|
|
1695
|
+
logger.info('serve pid file removed', { pidFilePath });
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function writeServePidFile(runtime, serverHandle) {
|
|
1701
|
+
const pidFile = buildServePidFile(serverHandle.host, serverHandle.port);
|
|
1702
|
+
fs.mkdirSync(pidFile.dir, { recursive: true });
|
|
1703
|
+
fs.writeFileSync(pidFile.path, `${process.pid}\n`);
|
|
1704
|
+
installServePidCleanup(pidFile.path, runtime && runtime.logger);
|
|
1705
|
+
return pidFile.path;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
async function stopServeProcess(runtime) {
|
|
1709
|
+
if (!runtime || !runtime.serverListenSpecified) {
|
|
1710
|
+
throw new Error('serve --stop 必须显式传入 listen,例如 manyoyo serve 127.0.0.1:3000 --stop');
|
|
1711
|
+
}
|
|
1712
|
+
const target = getServePidTarget(runtime.serverHost, runtime.serverPort);
|
|
1713
|
+
if (!target) {
|
|
1714
|
+
const label = buildServeListenLabel(runtime.serverHost, runtime.serverPort);
|
|
1715
|
+
console.log(`${YELLOW}⚠️ 未发现运行中的 serve 实例: ${label}${NC}`);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
try {
|
|
1719
|
+
process.kill(target.pid, 'SIGTERM');
|
|
1720
|
+
} catch (e) {
|
|
1721
|
+
if (!e || e.code !== 'ESRCH') {
|
|
1722
|
+
throw e;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
await sleep(200);
|
|
1726
|
+
if (isProcessRunning(target.pid)) {
|
|
1727
|
+
try {
|
|
1728
|
+
process.kill(target.pid, 'SIGKILL');
|
|
1729
|
+
} catch (e) {
|
|
1730
|
+
if (!e || e.code !== 'ESRCH') {
|
|
1731
|
+
throw e;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
removeServePidFile(target.path);
|
|
1736
|
+
console.log(`${GREEN}✅ 已停止 serve: ${target.listen} (pid: ${target.pid})${NC}`);
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1660
1739
|
function relaunchServeDetached(runtime) {
|
|
1661
1740
|
const serveLog = buildManyoyoLogPath('serve');
|
|
1662
1741
|
fs.mkdirSync(serveLog.dir, { recursive: true });
|
|
1663
1742
|
|
|
1743
|
+
const existing = getServePidTarget(runtime.serverHost, runtime.serverPort);
|
|
1744
|
+
if (existing) {
|
|
1745
|
+
console.log(`${YELLOW}⚠️ serve 已在后台运行: ${existing.listen} (pid: ${existing.pid})${NC}`);
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1664
1749
|
const child = spawn(process.argv[0], buildDetachedServeArgv(process.argv.slice(1)), {
|
|
1665
1750
|
detached: true,
|
|
1666
1751
|
stdio: 'ignore',
|
|
@@ -1954,7 +2039,7 @@ async function runWebServerMode(runtime) {
|
|
|
1954
2039
|
runtime.serverAuthPassAuto = SERVER_AUTH_PASS_AUTO;
|
|
1955
2040
|
}
|
|
1956
2041
|
|
|
1957
|
-
await startWebServer({
|
|
2042
|
+
const serverHandle = await startWebServer({
|
|
1958
2043
|
serverHost: runtime.serverHost,
|
|
1959
2044
|
serverPort: runtime.serverPort,
|
|
1960
2045
|
authUser: runtime.serverAuthUser,
|
|
@@ -1993,6 +2078,8 @@ async function runWebServerMode(runtime) {
|
|
|
1993
2078
|
},
|
|
1994
2079
|
logger: runtime.logger
|
|
1995
2080
|
});
|
|
2081
|
+
writeServePidFile(runtime, serverHandle);
|
|
2082
|
+
return serverHandle;
|
|
1996
2083
|
}
|
|
1997
2084
|
|
|
1998
2085
|
async function main() {
|
|
@@ -2015,6 +2102,10 @@ async function main() {
|
|
|
2015
2102
|
|
|
2016
2103
|
// 2. Start web server mode
|
|
2017
2104
|
if (runtime.serverMode) {
|
|
2105
|
+
if (runtime.serverStop) {
|
|
2106
|
+
await stopServeProcess(runtime);
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2018
2109
|
if (runtime.serverDetach) {
|
|
2019
2110
|
relaunchServeDetached(runtime);
|
|
2020
2111
|
return;
|