@xcanwin/manyoyo 5.8.6 → 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/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
- const envText = String(env);
458
- const idx = envText.indexOf('=');
459
- if (idx <= 0) {
460
- console.error(`${RED}⚠️ 错误: env 格式应为 KEY=VALUE: ${envText}${NC}`);
461
- process.exit(1);
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 applyRunStyleOptions(command, options = {}) {
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
- appendArrayOption(command, '-q, --quiet <item>', '静默输出 (可多次使用: cnew, crm, tip, cmd, full)');
1035
+ applyQuietOptions(command);
1043
1036
 
1044
1037
  if (includeServePreview) {
1045
1038
  command
1046
- .option('--serve [listen]', '按 serve 模式解析配置 (仅支持 <ip:port>)')
1047
- .option('-U, --user <username>', '网页服务登录用户名 (默认 admin)')
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,7 +1204,11 @@ 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, { includeRmOnExit: false, includeWebAuthOptions: true });
1207
+ applyRunStyleOptions(serveCommand, {
1208
+ includeRmOnExit: false,
1209
+ includeWebAuthOptions: true,
1210
+ includeFirstExecOptions: false
1211
+ });
1218
1212
  serveCommand.option('-d, --detach', '后台启动网页服务并立即返回');
1219
1213
  serveCommand.option('--stop', '停止后台网页服务;必须显式传入 listen');
1220
1214
  serveCommand.action((listen, options) => {
@@ -1260,7 +1254,10 @@ Notes:
1260
1254
  });
1261
1255
 
1262
1256
  const configRunCommand = configCommand.command('command').description('显示将执行的 docker run 命令并退出');
1263
- applyRunStyleOptions(configRunCommand, { includeRmOnExit: false });
1257
+ applyRunStyleOptions(configRunCommand, {
1258
+ includeRmOnExit: false,
1259
+ includeFirstExecOptions: false
1260
+ });
1264
1261
  enableShellSuffixPassThrough(configRunCommand);
1265
1262
  configRunCommand.action((options, command) => {
1266
1263
  validateShellSuffixPassThroughArgs(command);
@@ -1367,145 +1364,96 @@ Notes:
1367
1364
  const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
1368
1365
  const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
1369
1366
 
1370
- // Merge configs: command line > run config > global config > defaults
1371
- // Override mode (scalar values): use first defined value
1372
- HOST_PATH = pickConfigValue(options.hostPath, runConfig.hostPath, config.hostPath, HOST_PATH) || HOST_PATH;
1373
- const mergedContainerName = pickConfigValue(options.contName, runConfig.containerName, config.containerName);
1374
- if (mergedContainerName) {
1375
- CONTAINER_NAME = mergedContainerName;
1376
- }
1377
- CONTAINER_NAME = resolveContainerNameTemplate(CONTAINER_NAME);
1378
- const mergedContainerPath = pickConfigValue(options.contPath, runConfig.containerPath, config.containerPath);
1379
- if (mergedContainerPath) {
1380
- CONTAINER_PATH = mergedContainerPath;
1381
- }
1382
- IMAGE_NAME = pickConfigValue(options.imageName, runConfig.imageName, config.imageName, IMAGE_NAME) || IMAGE_NAME;
1383
- const mergedImageVersion = pickConfigValue(options.imageVer, runConfig.imageVersion, config.imageVersion);
1384
- if (mergedImageVersion) {
1385
- IMAGE_VERSION = mergedImageVersion;
1386
- }
1387
- const mergedShellPrefix = pickConfigValue(options.shellPrefix, runConfig.shellPrefix, config.shellPrefix);
1388
- if (mergedShellPrefix) {
1389
- EXEC_COMMAND_PREFIX = `${mergedShellPrefix} `;
1390
- }
1391
- const mergedShell = pickConfigValue(options.shell, runConfig.shell, config.shell);
1392
- if (mergedShell) {
1393
- EXEC_COMMAND = mergedShell;
1394
- }
1395
- const mergedShellSuffix = pickConfigValue(options.shellSuffix, runConfig.shellSuffix, config.shellSuffix);
1396
- if (mergedShellSuffix) {
1397
- EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedShellSuffix);
1398
- }
1399
- const mergedFirstShellPrefix = pickConfigValue(options.firstShellPrefix, runFirstConfig.shellPrefix, globalFirstConfig.shellPrefix);
1400
- if (mergedFirstShellPrefix) {
1401
- FIRST_EXEC_COMMAND_PREFIX = `${mergedFirstShellPrefix} `;
1402
- }
1403
- const mergedFirstShell = pickConfigValue(options.firstShell, runFirstConfig.shell, globalFirstConfig.shell);
1404
- if (mergedFirstShell) {
1405
- FIRST_EXEC_COMMAND = mergedFirstShell;
1406
- }
1407
- const mergedFirstShellSuffix = pickConfigValue(options.firstShellSuffix, runFirstConfig.shellSuffix, globalFirstConfig.shellSuffix);
1408
- if (mergedFirstShellSuffix) {
1409
- FIRST_EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(mergedFirstShellSuffix);
1410
- }
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;
1411
1405
 
1412
1406
  // Basic name validation to reduce injection risk
1413
1407
  validateName('containerName', CONTAINER_NAME, SAFE_CONTAINER_NAME_PATTERN);
1414
1408
  validateName('imageName', IMAGE_NAME, /^[A-Za-z0-9][A-Za-z0-9._/:-]*$/);
1415
1409
  validateImageVersion(IMAGE_VERSION);
1416
1410
 
1417
- // Merge mode (array values): concatenate all sources
1418
- const toArray = (val) => Array.isArray(val) ? val : (val ? [val] : []);
1419
- const envFileList = [
1420
- ...toArray(config.envFile),
1421
- ...toArray(runConfig.envFile),
1422
- ...(options.envFile || [])
1423
- ].filter(Boolean);
1411
+ const envFileList = resolvedRuntime.envFile;
1424
1412
  envFileList.forEach(ef => addEnvFile(ef));
1425
1413
 
1426
1414
  // env in JSON config uses map type, and is merged by key with CLI priority.
1427
- const envMap = {
1428
- ...normalizeJsonEnvMap(config.env, '全局配置'),
1429
- ...normalizeJsonEnvMap(runConfig.env, '运行配置'),
1430
- ...normalizeCliEnvMap(options.env)
1431
- };
1415
+ const envMap = resolvedRuntime.env;
1432
1416
  Object.entries(envMap).forEach(([key, value]) => addEnv(`${key}=${value}`));
1433
1417
 
1434
- const firstEnvFileList = [
1435
- ...toArray(globalFirstConfig.envFile),
1436
- ...toArray(runFirstConfig.envFile),
1437
- ...(options.firstEnvFile || [])
1438
- ].filter(Boolean);
1418
+ const firstEnvFileList = resolvedRuntime.first.envFile;
1439
1419
  firstEnvFileList.forEach(ef => addEnvFileTo(FIRST_CONTAINER_ENVS, ef));
1440
1420
 
1441
- const firstEnvMap = {
1442
- ...normalizeJsonEnvMap(globalFirstConfig.env, '全局配置 first'),
1443
- ...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first'),
1444
- ...normalizeCliEnvMap(options.firstEnv)
1445
- };
1421
+ const firstEnvMap = resolvedRuntime.first.env;
1446
1422
  Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
1447
1423
 
1448
1424
  applyPlaywrightCliSessionIntegration(config, runConfig);
1449
1425
 
1450
- const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume)
1451
- .map(normalizeVolume);
1426
+ const volumeList = resolvedRuntime.volumes;
1452
1427
  volumeList.forEach(v => addVolume(v));
1453
1428
 
1454
- const portList = mergeArrayConfig(config.ports, runConfig.ports, options.port);
1429
+ const portList = resolvedRuntime.ports;
1455
1430
  portList.forEach(p => addPort(p));
1456
1431
 
1457
- const buildArgList = mergeArrayConfig(config.imageBuildArgs, runConfig.imageBuildArgs, options.imageBuildArg);
1432
+ const buildArgList = resolvedRuntime.imageBuildArgs;
1458
1433
  buildArgList.forEach(arg => addImageBuildArg(arg));
1459
1434
 
1460
1435
  // Override mode for special options
1461
- const yoloValue = pickConfigValue(options.yolo, runConfig.yolo, config.yolo);
1436
+ const yoloValue = resolvedRuntime.yolo;
1462
1437
  if (yoloValue) setYolo(yoloValue);
1463
1438
 
1464
- const contModeValue = pickConfigValue(options.contMode, runConfig.containerMode, config.containerMode);
1439
+ const contModeValue = resolvedRuntime.containerMode;
1465
1440
  if (contModeValue) setContMode(contModeValue);
1466
1441
 
1467
- const quietValue = pickConfigValue(options.quiet, runConfig.quiet, config.quiet);
1442
+ const quietValue = resolvedRuntime.quiet;
1468
1443
  if (quietValue) setQuiet(quietValue);
1469
1444
 
1470
- // Handle shell-full (variadic arguments)
1471
- if (options.shellFull) {
1472
- EXEC_COMMAND = options.shellFull.join(' ');
1473
- EXEC_COMMAND_PREFIX = "";
1474
- EXEC_COMMAND_SUFFIX = "";
1475
- }
1476
-
1477
- // Handle -- suffix arguments
1478
- if (!options.shellFull) {
1479
- const doubleDashIndex = process.argv.indexOf('--');
1480
- if (doubleDashIndex !== -1 && doubleDashIndex < process.argv.length - 1) {
1481
- EXEC_COMMAND_SUFFIX = normalizeCommandSuffix(process.argv.slice(doubleDashIndex + 1).join(' '));
1482
- }
1483
- }
1484
-
1485
1445
  if (options.rmOnExit) {
1486
1446
  RM_ON_EXIT = true;
1487
1447
  }
1488
1448
 
1489
1449
  if (isServerMode) {
1490
- const serverListen = parseServerListen(options.server);
1491
- SERVER_HOST = serverListen.host;
1492
- SERVER_PORT = serverListen.port;
1450
+ SERVER_HOST = resolvedRuntime.serverHost;
1451
+ SERVER_PORT = resolvedRuntime.serverPort;
1493
1452
  }
1494
1453
 
1495
- const serverUserValue = pickConfigValue(options.serverUser, runConfig.serverUser, config.serverUser, process.env.MANYOYO_SERVER_USER);
1496
- if (serverUserValue) {
1497
- SERVER_AUTH_USER = String(serverUserValue);
1498
- }
1499
-
1500
- const serverPassValue = pickConfigValue(options.serverPass, runConfig.serverPass, config.serverPass, process.env.MANYOYO_SERVER_PASS);
1501
- if (serverPassValue) {
1502
- SERVER_AUTH_PASS = String(serverPassValue);
1503
- SERVER_AUTH_PASS_AUTO = false;
1504
- }
1505
-
1506
- if (isServerMode && !isServerStopMode) {
1507
- ensureWebServerAuthCredentials();
1508
- }
1454
+ SERVER_AUTH_USER = resolvedRuntime.serverUser || '';
1455
+ SERVER_AUTH_PASS = resolvedRuntime.serverPass || '';
1456
+ SERVER_AUTH_PASS_AUTO = resolvedRuntime.serverPassAuto === true;
1509
1457
 
1510
1458
  if (isShowConfigMode) {
1511
1459
  const finalConfig = {
@@ -1525,9 +1473,9 @@ Notes:
1525
1473
  shellSuffix: EXEC_COMMAND_SUFFIX || "",
1526
1474
  yolo: yoloValue || "",
1527
1475
  quiet: quietValue || [],
1528
- server: isServerMode,
1529
- serverHost: isServerMode ? SERVER_HOST : null,
1530
- serverPort: isServerMode ? SERVER_PORT : null,
1476
+ server: resolvedRuntime.server,
1477
+ serverHost: resolvedRuntime.server ? SERVER_HOST : null,
1478
+ serverPort: resolvedRuntime.server ? SERVER_PORT : null,
1531
1479
  serverUser: SERVER_AUTH_USER || "",
1532
1480
  serverPass: SERVER_AUTH_PASS || "",
1533
1481
  exec: {
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const JSON5 = require('json5');
7
+ const { findTopLevelPropertyValueRange } = require('./json5-text-edit');
7
8
 
8
9
  function getManyoyoConfigPath(homeDir = os.homedir()) {
9
10
  return path.join(homeDir, '.manyoyo', 'manyoyo.json');
@@ -36,204 +37,6 @@ function readManyoyoConfig(homeDir = os.homedir()) {
36
37
  }
37
38
  }
38
39
 
39
- function readQuotedString(text, startIndex) {
40
- const quote = text[startIndex];
41
- let value = '';
42
-
43
- for (let i = startIndex + 1; i < text.length; i += 1) {
44
- const ch = text[i];
45
- if (ch === '\\') {
46
- value += ch;
47
- if (i + 1 < text.length) {
48
- value += text[i + 1];
49
- i += 1;
50
- }
51
- continue;
52
- }
53
- if (ch === quote) {
54
- return {
55
- value,
56
- end: i + 1
57
- };
58
- }
59
- value += ch;
60
- }
61
-
62
- return null;
63
- }
64
-
65
- function isIdentifierStart(ch) {
66
- return /[A-Za-z_$]/.test(ch);
67
- }
68
-
69
- function isIdentifierPart(ch) {
70
- return /[A-Za-z0-9_$]/.test(ch);
71
- }
72
-
73
- function skipWhitespace(text, index) {
74
- let i = index;
75
- while (i < text.length && /\s/.test(text[i])) {
76
- i += 1;
77
- }
78
- return i;
79
- }
80
-
81
- function findTopLevelPropertyValueRange(text, propertyName) {
82
- let depth = 0;
83
- let inString = '';
84
- let inLineComment = false;
85
- let inBlockComment = false;
86
-
87
- for (let i = 0; i < text.length; i += 1) {
88
- const ch = text[i];
89
- const next = text[i + 1];
90
-
91
- if (inLineComment) {
92
- if (ch === '\n') inLineComment = false;
93
- continue;
94
- }
95
- if (inBlockComment) {
96
- if (ch === '*' && next === '/') {
97
- inBlockComment = false;
98
- i += 1;
99
- }
100
- continue;
101
- }
102
- if (inString) {
103
- if (ch === '\\') {
104
- i += 1;
105
- continue;
106
- }
107
- if (ch === inString) inString = '';
108
- continue;
109
- }
110
-
111
- if (ch === '/' && next === '/') {
112
- inLineComment = true;
113
- i += 1;
114
- continue;
115
- }
116
- if (ch === '/' && next === '*') {
117
- inBlockComment = true;
118
- i += 1;
119
- continue;
120
- }
121
- if (depth === 1 && !/\s|,/.test(ch)) {
122
- let property = '';
123
- let cursor = i;
124
- if (ch === '"' || ch === '\'') {
125
- const token = readQuotedString(text, i);
126
- if (!token) {
127
- return null;
128
- }
129
- property = token.value;
130
- cursor = token.end;
131
- } else if (isIdentifierStart(ch)) {
132
- cursor = i + 1;
133
- while (cursor < text.length && isIdentifierPart(text[cursor])) {
134
- cursor += 1;
135
- }
136
- property = text.slice(i, cursor);
137
- }
138
-
139
- if (property) {
140
- const colonIndex = skipWhitespace(text, cursor);
141
- if (text[colonIndex] === ':') {
142
- if (property !== propertyName) {
143
- i = colonIndex;
144
- continue;
145
- }
146
-
147
- let valueStart = skipWhitespace(text, colonIndex + 1);
148
- let valueEnd = valueStart;
149
- let valueString = '';
150
- let valueLineComment = false;
151
- let valueBlockComment = false;
152
- let valueDepth = 0;
153
-
154
- for (; valueEnd < text.length; valueEnd += 1) {
155
- const valueCh = text[valueEnd];
156
- const valueNext = text[valueEnd + 1];
157
-
158
- if (valueLineComment) {
159
- if (valueCh === '\n') valueLineComment = false;
160
- continue;
161
- }
162
- if (valueBlockComment) {
163
- if (valueCh === '*' && valueNext === '/') {
164
- valueBlockComment = false;
165
- valueEnd += 1;
166
- }
167
- continue;
168
- }
169
- if (valueString) {
170
- if (valueCh === '\\') {
171
- valueEnd += 1;
172
- continue;
173
- }
174
- if (valueCh === valueString) valueString = '';
175
- continue;
176
- }
177
-
178
- if (valueCh === '/' && valueNext === '/') {
179
- valueLineComment = true;
180
- valueEnd += 1;
181
- continue;
182
- }
183
- if (valueCh === '/' && valueNext === '*') {
184
- valueBlockComment = true;
185
- valueEnd += 1;
186
- continue;
187
- }
188
- if (valueCh === '"' || valueCh === '\'') {
189
- valueString = valueCh;
190
- continue;
191
- }
192
- if (valueCh === '{' || valueCh === '[' || valueCh === '(') {
193
- valueDepth += 1;
194
- continue;
195
- }
196
- if (valueCh === '}' || valueCh === ']' || valueCh === ')') {
197
- if (valueDepth === 0) {
198
- break;
199
- }
200
- valueDepth -= 1;
201
- continue;
202
- }
203
- if (valueDepth === 0 && valueCh === ',') {
204
- break;
205
- }
206
- }
207
-
208
- while (valueEnd > valueStart && /\s/.test(text[valueEnd - 1])) {
209
- valueEnd -= 1;
210
- }
211
-
212
- return {
213
- start: valueStart,
214
- end: valueEnd
215
- };
216
- }
217
- }
218
- }
219
-
220
- if (ch === '"' || ch === '\'') {
221
- inString = ch;
222
- continue;
223
- }
224
- if (ch === '{' || ch === '[') {
225
- depth += 1;
226
- continue;
227
- }
228
- if (ch === '}' || ch === ']') {
229
- depth -= 1;
230
- continue;
231
- }
232
- }
233
-
234
- return null;
235
- }
236
-
237
40
  function insertTopLevelImageVersion(text, imageVersion) {
238
41
  const openBraceIndex = text.indexOf('{');
239
42
  if (openBraceIndex === -1) {
@@ -230,15 +230,31 @@ function prepareGoplsBuildCache(ctx, cache, imageTool, arch) {
230
230
  }
231
231
  }
232
232
 
233
+ function createBuildCacheArtifacts(ctx, cache, imageTool, archInfo) {
234
+ return [
235
+ {
236
+ name: 'node',
237
+ prepare: () => prepareNodeBuildCache(ctx, cache, archInfo.archNode)
238
+ },
239
+ {
240
+ name: 'jdtls',
241
+ prepare: () => prepareJdtlsBuildCache(ctx, cache, imageTool)
242
+ },
243
+ {
244
+ name: 'gopls',
245
+ prepare: () => prepareGoplsBuildCache(ctx, cache, imageTool, archInfo.arch)
246
+ }
247
+ ];
248
+ }
249
+
233
250
  async function prepareBuildCache(ctx, imageTool) {
234
251
  const { CYAN, GREEN, NC } = ctx.colors;
235
252
  const cache = createBuildCacheContext(ctx);
236
- const { arch, archNode } = resolveBuildCacheArch();
253
+ const archInfo = resolveBuildCacheArch();
254
+ const artifacts = createBuildCacheArtifacts(ctx, cache, imageTool, archInfo);
237
255
 
238
256
  ctx.log(`\n${CYAN}准备构建缓存...${NC}`);
239
- prepareNodeBuildCache(ctx, cache, archNode);
240
- prepareJdtlsBuildCache(ctx, cache, imageTool);
241
- prepareGoplsBuildCache(ctx, cache, imageTool, arch);
257
+ artifacts.forEach(artifact => artifact.prepare());
242
258
  saveBuildCacheTimestamps(cache.timestampFile, cache.timestamps);
243
259
  ctx.log(`${GREEN}✅ 构建缓存准备完成${NC}\n`);
244
260
  }