@xcanwin/manyoyo 5.8.6 → 5.8.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/manyoyo.js CHANGED
@@ -16,6 +16,11 @@ 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
+ normalizeVolume
23
+ } = require('../lib/runtime-normalizers');
19
24
  const {
20
25
  sanitizeSensitiveData,
21
26
  sanitizeServeLogText,
@@ -454,23 +459,13 @@ async function askQuestion(prompt) {
454
459
  * @param {string} env - 环境变量字符串 (KEY=VALUE)
455
460
  */
456
461
  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}`);
462
+ try {
463
+ return parseEnvEntryOrThrow(env);
464
+ } catch (e) {
465
+ const message = e && e.message ? e.message : String(e);
466
+ console.error(`${RED}⚠️ 错误: ${message}${NC}`);
471
467
  process.exit(1);
472
468
  }
473
- return { key, value };
474
469
  }
475
470
 
476
471
  function normalizeJsonEnvMap(envConfig, sourceLabel) {
@@ -570,42 +565,6 @@ function addEnvFile(envFile) {
570
565
  return addEnvFileTo(CONTAINER_ENVS, envFile);
571
566
  }
572
567
 
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
568
  function hasEnvKey(targetEnvs, key) {
610
569
  for (let i = 0; i < targetEnvs.length; i += 2) {
611
570
  if (targetEnvs[i] !== '--env') {
@@ -1367,47 +1326,44 @@ Notes:
1367
1326
  const globalFirstConfig = normalizeFirstConfig(config.first, '全局配置');
1368
1327
  const runFirstConfig = normalizeFirstConfig(runConfig.first, '运行配置');
1369
1328
 
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
- }
1329
+ const resolvedRuntime = resolveRuntimeConfig({
1330
+ cliOptions: options,
1331
+ globalConfig: config,
1332
+ runConfig,
1333
+ globalFirstConfig,
1334
+ runFirstConfig,
1335
+ defaults: {
1336
+ hostPath: HOST_PATH,
1337
+ containerName: CONTAINER_NAME,
1338
+ containerPath: CONTAINER_PATH,
1339
+ imageName: IMAGE_NAME,
1340
+ imageVersion: IMAGE_VERSION
1341
+ },
1342
+ envVars: process.env,
1343
+ argv: process.argv,
1344
+ isServerMode,
1345
+ isServerStopMode,
1346
+ pickConfigValue,
1347
+ resolveContainerNameTemplate,
1348
+ normalizeCommandSuffix,
1349
+ normalizeJsonEnvMap,
1350
+ normalizeCliEnvMap,
1351
+ mergeArrayConfig,
1352
+ normalizeVolume,
1353
+ parseServerListen
1354
+ });
1355
+
1356
+ HOST_PATH = resolvedRuntime.hostPath;
1357
+ CONTAINER_NAME = resolvedRuntime.containerName;
1358
+ CONTAINER_PATH = resolvedRuntime.containerPath;
1359
+ IMAGE_NAME = resolvedRuntime.imageName;
1360
+ IMAGE_VERSION = resolvedRuntime.imageVersion;
1361
+ EXEC_COMMAND_PREFIX = resolvedRuntime.exec.prefix;
1362
+ EXEC_COMMAND = resolvedRuntime.exec.shell;
1363
+ EXEC_COMMAND_SUFFIX = resolvedRuntime.exec.suffix;
1364
+ FIRST_EXEC_COMMAND_PREFIX = resolvedRuntime.first.exec.prefix;
1365
+ FIRST_EXEC_COMMAND = resolvedRuntime.first.exec.shell;
1366
+ FIRST_EXEC_COMMAND_SUFFIX = resolvedRuntime.first.exec.suffix;
1411
1367
 
1412
1368
  // Basic name validation to reduce injection risk
1413
1369
  validateName('containerName', CONTAINER_NAME, SAFE_CONTAINER_NAME_PATTERN);
@@ -1415,97 +1371,47 @@ Notes:
1415
1371
  validateImageVersion(IMAGE_VERSION);
1416
1372
 
1417
1373
  // 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);
1374
+ const envFileList = resolvedRuntime.envFile;
1424
1375
  envFileList.forEach(ef => addEnvFile(ef));
1425
1376
 
1426
- // 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
- };
1377
+ const envMap = resolvedRuntime.env;
1432
1378
  Object.entries(envMap).forEach(([key, value]) => addEnv(`${key}=${value}`));
1433
1379
 
1434
- const firstEnvFileList = [
1435
- ...toArray(globalFirstConfig.envFile),
1436
- ...toArray(runFirstConfig.envFile),
1437
- ...(options.firstEnvFile || [])
1438
- ].filter(Boolean);
1380
+ const firstEnvFileList = resolvedRuntime.first.envFile;
1439
1381
  firstEnvFileList.forEach(ef => addEnvFileTo(FIRST_CONTAINER_ENVS, ef));
1440
1382
 
1441
- const firstEnvMap = {
1442
- ...normalizeJsonEnvMap(globalFirstConfig.env, '全局配置 first'),
1443
- ...normalizeJsonEnvMap(runFirstConfig.env, '运行配置 first'),
1444
- ...normalizeCliEnvMap(options.firstEnv)
1445
- };
1383
+ const firstEnvMap = resolvedRuntime.first.env;
1446
1384
  Object.entries(firstEnvMap).forEach(([key, value]) => addEnvTo(FIRST_CONTAINER_ENVS, `${key}=${value}`));
1447
1385
 
1448
1386
  applyPlaywrightCliSessionIntegration(config, runConfig);
1449
1387
 
1450
- const volumeList = mergeArrayConfig(config.volumes, runConfig.volumes, options.volume)
1451
- .map(normalizeVolume);
1388
+ const volumeList = resolvedRuntime.volumes;
1452
1389
  volumeList.forEach(v => addVolume(v));
1453
1390
 
1454
- const portList = mergeArrayConfig(config.ports, runConfig.ports, options.port);
1391
+ const portList = resolvedRuntime.ports;
1455
1392
  portList.forEach(p => addPort(p));
1456
1393
 
1457
- const buildArgList = mergeArrayConfig(config.imageBuildArgs, runConfig.imageBuildArgs, options.imageBuildArg);
1394
+ const buildArgList = resolvedRuntime.imageBuildArgs;
1458
1395
  buildArgList.forEach(arg => addImageBuildArg(arg));
1459
1396
 
1460
- // Override mode for special options
1461
- const yoloValue = pickConfigValue(options.yolo, runConfig.yolo, config.yolo);
1397
+ const yoloValue = resolvedRuntime.yolo;
1462
1398
  if (yoloValue) setYolo(yoloValue);
1463
1399
 
1464
- const contModeValue = pickConfigValue(options.contMode, runConfig.containerMode, config.containerMode);
1400
+ const contModeValue = resolvedRuntime.containerMode;
1465
1401
  if (contModeValue) setContMode(contModeValue);
1466
1402
 
1467
- const quietValue = pickConfigValue(options.quiet, runConfig.quiet, config.quiet);
1403
+ const quietValue = resolvedRuntime.quiet;
1468
1404
  if (quietValue) setQuiet(quietValue);
1469
1405
 
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
1406
  if (options.rmOnExit) {
1486
1407
  RM_ON_EXIT = true;
1487
1408
  }
1488
1409
 
1489
- if (isServerMode) {
1490
- const serverListen = parseServerListen(options.server);
1491
- SERVER_HOST = serverListen.host;
1492
- SERVER_PORT = serverListen.port;
1493
- }
1494
-
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
- }
1410
+ SERVER_HOST = resolvedRuntime.serverHost || SERVER_HOST;
1411
+ SERVER_PORT = resolvedRuntime.serverPort || SERVER_PORT;
1412
+ SERVER_AUTH_USER = resolvedRuntime.serverUser || '';
1413
+ SERVER_AUTH_PASS = resolvedRuntime.serverPass || '';
1414
+ SERVER_AUTH_PASS_AUTO = Boolean(resolvedRuntime.serverPassAuto);
1509
1415
 
1510
1416
  if (isShowConfigMode) {
1511
1417
  const finalConfig = {
@@ -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
  }
@@ -251,6 +251,25 @@ function collectOpenCodeInitData(homeDir, ctx) {
251
251
  return { keys, values, notes, volumes: dedupeList(volumes) };
252
252
  }
253
253
 
254
+ const AGENT_INIT_SPECS = {
255
+ claude: {
256
+ yolo: 'c',
257
+ collect: (homeDir, ctx) => collectClaudeInitData(homeDir, ctx)
258
+ },
259
+ codex: {
260
+ yolo: 'cx',
261
+ collect: (homeDir, ctx) => collectCodexInitData(homeDir, ctx)
262
+ },
263
+ gemini: {
264
+ yolo: 'gm',
265
+ collect: (homeDir) => collectGeminiInitData(homeDir)
266
+ },
267
+ opencode: {
268
+ yolo: 'oc',
269
+ collect: (homeDir, ctx) => collectOpenCodeInitData(homeDir, ctx)
270
+ }
271
+ };
272
+
254
273
  function buildInitRunEnv(keys, values) {
255
274
  const envMap = {};
256
275
  const missingKeys = [];
@@ -338,24 +357,17 @@ async function initAgentConfigs(rawAgents, options = {}) {
338
357
  runsMap = { ...manyoyoConfig.runs };
339
358
  }
340
359
 
341
- const extractors = {
342
- claude: homeDir => collectClaudeInitData(homeDir, ctx),
343
- codex: homeDir => collectCodexInitData(homeDir, ctx),
344
- gemini: collectGeminiInitData,
345
- opencode: homeDir => collectOpenCodeInitData(homeDir, ctx)
346
- };
347
- const yoloMap = { claude: 'c', codex: 'cx', gemini: 'gm', opencode: 'oc' };
348
-
349
360
  let hasConfigChanged = false;
350
361
  ctx.log(`${CYAN}🧭 正在初始化 MANYOYO 配置: ${agents.join(', ')}${NC}`);
351
362
 
352
363
  for (const agent of agents) {
353
- const data = extractors[agent](ctx.homeDir);
364
+ const spec = AGENT_INIT_SPECS[agent];
365
+ const data = spec.collect(ctx.homeDir, ctx);
354
366
  const shouldWriteRun = await shouldOverwriteInitRunEntry(agent, Object.prototype.hasOwnProperty.call(runsMap, agent), ctx);
355
367
 
356
368
  let writeResult = { missingKeys: [], unsafeKeys: [] };
357
369
  if (shouldWriteRun) {
358
- const buildResult = buildInitRunProfile(agent, yoloMap[agent], data.volumes, data.keys, data.values);
370
+ const buildResult = buildInitRunProfile(agent, spec.yolo, data.volumes, data.keys, data.values);
359
371
  runsMap[agent] = buildResult.runProfile;
360
372
  writeResult = { missingKeys: buildResult.missingKeys, unsafeKeys: buildResult.unsafeKeys };
361
373
  hasConfigChanged = true;