abelworkflow 0.2.0 → 0.6.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/lib/cli.mjs CHANGED
@@ -12,10 +12,14 @@ const home = homedir();
12
12
  const defaultAgentsDir = join(home, ".agents");
13
13
  const installMetadataName = ".abelworkflow-install.json";
14
14
  const claudeSettingsPath = join(home, ".claude", "settings.json");
15
- const claudeVscodeConfigPath = join(home, ".claude", "config.json");
16
15
  const claudeMetaConfigPath = join(home, ".claude.json");
17
16
  const codexConfigPath = join(home, ".codex", "config.toml");
18
17
  const codexAuthPath = join(home, ".codex", "auth.json");
18
+ const codexTemplateRoot = join(packageRoot, "lib", "templates", "codex");
19
+ const codexTemplateConfigPath = join(codexTemplateRoot, "config-base.toml");
20
+ const codexTemplateAgentsPath = join(codexTemplateRoot, "agents");
21
+ const installBackupStamp = Date.now();
22
+ const createdBackupPaths = new Set();
19
23
  const claudeModelEnvKeys = [
20
24
  "ANTHROPIC_MODEL",
21
25
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
@@ -222,17 +226,38 @@ async function pathTargetExists(path) {
222
226
  }
223
227
  }
224
228
 
229
+ async function createBackupPath(targetPath) {
230
+ let index = 0;
231
+ while (true) {
232
+ const backupPath = `${targetPath}.bak.${installBackupStamp}${index ? `-${index}` : ""}`;
233
+ if (!(await pathExists(backupPath))) {
234
+ return backupPath;
235
+ }
236
+ index += 1;
237
+ }
238
+ }
239
+
240
+ async function backupExistingPath(targetPath) {
241
+ if (createdBackupPaths.has(targetPath) || !(await pathExists(targetPath))) {
242
+ return null;
243
+ }
244
+
245
+ const backupPath = await createBackupPath(targetPath);
246
+ await cp(targetPath, backupPath, { recursive: true, force: false });
247
+ createdBackupPaths.add(targetPath);
248
+ console.log(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
249
+ return backupPath;
250
+ }
251
+
225
252
  async function backupIfNeeded(targetPath, force) {
226
253
  if (!(await pathExists(targetPath))) {
227
254
  return null;
228
255
  }
229
- if (!force) {
230
- const backupPath = `${targetPath}.bak.${Date.now()}`;
231
- await rename(targetPath, backupPath);
232
- return backupPath;
233
- }
234
- await rm(targetPath, { recursive: true, force: true });
235
- return null;
256
+
257
+ const backupPath = await createBackupPath(targetPath);
258
+ await rename(targetPath, backupPath);
259
+ console.log(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
260
+ return backupPath;
236
261
  }
237
262
 
238
263
  async function syncManagedFiles(agentsDir) {
@@ -298,8 +323,10 @@ async function replaceManagedEntry(source, target, entry) {
298
323
 
299
324
  const sourceStat = await lstat(source);
300
325
  if (sourceStat.isDirectory()) {
326
+ await backupExistingPath(target);
301
327
  await rm(target, { recursive: true, force: true });
302
328
  } else if (await pathExists(target)) {
329
+ await backupExistingPath(target);
303
330
  const targetStat = await lstat(target);
304
331
  if (targetStat.isDirectory()) {
305
332
  await rm(target, { recursive: true, force: true });
@@ -850,6 +877,11 @@ async function writeJsonFileSafe(path, data) {
850
877
  await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
851
878
  }
852
879
 
880
+ async function writeJsonFileWithBackup(path, data) {
881
+ await backupExistingPath(path);
882
+ await writeJsonFileSafe(path, data);
883
+ }
884
+
853
885
  function parseDotenv(content) {
854
886
  const values = {};
855
887
  for (const rawLine of content.split(/\r?\n/u)) {
@@ -912,6 +944,7 @@ async function updateDotenvFile(path, updates) {
912
944
  current[key] = String(value);
913
945
  }
914
946
  }
947
+ await backupExistingPath(path);
915
948
  await mkdir(dirname(path), { recursive: true });
916
949
  await writeFile(path, renderDotenv(current), "utf8");
917
950
  }
@@ -1053,9 +1086,16 @@ function commandExists(command) {
1053
1086
  return result.status === 0;
1054
1087
  }
1055
1088
 
1089
+ function getRunCommandSpawnOptions(platform = getPlatform()) {
1090
+ return {
1091
+ stdio: "inherit",
1092
+ shell: platform === "win32"
1093
+ };
1094
+ }
1095
+
1056
1096
  async function runCommand(command, args) {
1057
1097
  await new Promise((resolvePromise, rejectPromise) => {
1058
- const child = spawn(command, args, { stdio: "inherit" });
1098
+ const child = spawn(command, args, getRunCommandSpawnOptions());
1059
1099
  child.on("error", rejectPromise);
1060
1100
  child.on("close", (code) => {
1061
1101
  if (code === 0) {
@@ -1276,54 +1316,90 @@ async function configureClaudeApi() {
1276
1316
  nextSettings.env[field] = model;
1277
1317
  }
1278
1318
 
1279
- await writeJsonFileSafe(claudeSettingsPath, nextSettings);
1280
-
1281
- const vscodeConfig = await readJsonFileSafe(claudeVscodeConfigPath, {});
1282
- vscodeConfig.primaryApiKey = "abelworkflow";
1283
- await writeJsonFileSafe(claudeVscodeConfigPath, vscodeConfig);
1319
+ await writeJsonFileWithBackup(claudeSettingsPath, nextSettings);
1284
1320
 
1285
1321
  const metaConfig = await readJsonFileSafe(claudeMetaConfigPath, {});
1286
1322
  metaConfig.hasCompletedOnboarding = true;
1287
1323
  ensureApprovedClaudeApiKey(metaConfig, key);
1288
- await writeJsonFileSafe(claudeMetaConfigPath, metaConfig);
1324
+ await writeJsonFileWithBackup(claudeMetaConfigPath, metaConfig);
1289
1325
 
1290
1326
  console.log(`已更新 ${pathToLabel(claudeSettingsPath)} (${authType}, ${baseUrl}, ${maskSecret(key)})`);
1291
1327
  }
1292
1328
 
1293
1329
  function updateTopLevelTomlField(content, field, value) {
1294
1330
  const lineEnding = detectLineEnding(content);
1295
- const firstSectionMatch = content.match(/^\[/mu);
1296
- const topLevelEnd = firstSectionMatch?.index ?? content.length;
1297
- let topLevel = content.slice(0, topLevelEnd);
1298
- const rest = content.slice(topLevelEnd);
1299
- const fieldRegex = new RegExp(`^(#\\s*)?${escapeRegExp(field)}\\s*=\\s*["'][^"']*["'][ \\t]*(?:#.*)?\\r?$`, "mu");
1300
-
1301
1331
  if (value === null) {
1302
- topLevel = collapseBlankLines(topLevel.replace(fieldRegex, ""), lineEnding);
1303
- } else {
1304
- const nextLine = `${field} = ${JSON.stringify(value)}`;
1305
- if (fieldRegex.test(topLevel)) {
1306
- topLevel = topLevel.replace(fieldRegex, nextLine);
1307
- } else {
1308
- topLevel = topLevel.trimEnd()
1309
- ? `${topLevel.trimEnd()}${lineEnding}${nextLine}${lineEnding}`
1310
- : `${nextLine}${lineEnding}`;
1332
+ return removeTopLevelTomlField(content, field);
1333
+ }
1334
+
1335
+ const { topLevel, rest } = splitTopLevelTomlContent(content);
1336
+ const entry = extractTopLevelTomlEntries(content).find((item) => item.field === field);
1337
+ const nextLine = `${field} = ${JSON.stringify(value)}`;
1338
+ let nextTopLevel;
1339
+
1340
+ if (entry) {
1341
+ const start = topLevel.indexOf(entry.raw);
1342
+ if (start === -1) {
1343
+ return content;
1311
1344
  }
1345
+ nextTopLevel = `${topLevel.slice(0, start)}${nextLine}${topLevel.slice(start + entry.raw.length)}`;
1346
+ } else {
1347
+ nextTopLevel = topLevel.trimEnd()
1348
+ ? `${topLevel.trimEnd()}${lineEnding}${nextLine}${lineEnding}`
1349
+ : `${nextLine}${lineEnding}`;
1350
+ }
1351
+
1352
+ nextTopLevel = nextTopLevel.trimEnd();
1353
+
1354
+ if (rest && nextTopLevel) {
1355
+ nextTopLevel = `${nextTopLevel}${lineEnding}${lineEnding}`;
1356
+ }
1357
+
1358
+ return `${nextTopLevel}${rest}`;
1359
+ }
1360
+
1361
+ function removeTopLevelTomlField(content, field) {
1362
+ const lineEnding = detectLineEnding(content);
1363
+ const { topLevel, rest } = splitTopLevelTomlContent(content);
1364
+ const entry = extractTopLevelTomlEntries(content).find((item) => item.field === field);
1365
+ if (!entry) {
1366
+ return content;
1367
+ }
1368
+
1369
+ const start = topLevel.indexOf(entry.raw);
1370
+ if (start === -1) {
1371
+ return content;
1312
1372
  }
1313
1373
 
1314
- topLevel = topLevel.trimEnd();
1374
+ let nextTopLevel = `${topLevel.slice(0, start)}${topLevel.slice(start + entry.raw.length)}`;
1375
+ nextTopLevel = collapseBlankLines(nextTopLevel, lineEnding).trimEnd();
1315
1376
 
1316
- if (rest && topLevel) {
1317
- topLevel = `${topLevel}${lineEnding}${lineEnding}`;
1377
+ if (rest && nextTopLevel) {
1378
+ nextTopLevel = `${nextTopLevel}${lineEnding}${lineEnding}`;
1318
1379
  }
1319
1380
 
1320
- return `${topLevel}${rest}`;
1381
+ return `${nextTopLevel}${rest}`;
1321
1382
  }
1322
1383
 
1323
1384
  function removeTomlSection(content, sectionName) {
1324
1385
  const lineEnding = detectLineEnding(content);
1325
- const sectionRegex = new RegExp(`(?:\\r?\\n)?\\[${escapeRegExp(sectionName)}\\][\\s\\S]*?(?=\\r?\\n\\[|$)`, "gu");
1326
- return collapseBlankLines(content.replace(sectionRegex, ""), lineEnding).trimEnd();
1386
+ const sectionHeaderRegex = new RegExp(
1387
+ `(?:^|\\r?\\n)\\[${escapeRegExp(sectionName)}\\](?:[ \\t]+#.*)?[ \\t]*\\r?$`,
1388
+ "mu"
1389
+ );
1390
+ const headerMatch = sectionHeaderRegex.exec(content);
1391
+ if (!headerMatch) {
1392
+ return content;
1393
+ }
1394
+
1395
+ const sectionStart = headerMatch[0].startsWith("\n") || headerMatch[0].startsWith("\r\n")
1396
+ ? headerMatch.index + headerMatch[0].indexOf("[")
1397
+ : headerMatch.index;
1398
+ const headerLineEnd = content.indexOf(lineEnding, sectionStart);
1399
+ const bodyStart = headerLineEnd === -1 ? content.length : headerLineEnd + lineEnding.length;
1400
+ const nextSectionStart = findTomlSectionStart(content, bodyStart);
1401
+ const sectionEnd = nextSectionStart === -1 ? content.length : nextSectionStart;
1402
+ return collapseBlankLines(`${content.slice(0, sectionStart)}${content.slice(sectionEnd)}`, lineEnding).trimEnd();
1327
1403
  }
1328
1404
 
1329
1405
  function buildTomlSection(sectionName, values, lineEnding = "\n") {
@@ -1343,18 +1419,235 @@ function buildTomlSection(sectionName, values, lineEnding = "\n") {
1343
1419
  return `${lines.join(lineEnding)}${lineEnding}`;
1344
1420
  }
1345
1421
 
1346
- function readTopLevelTomlString(content, field) {
1347
- let inSection = false;
1348
- for (const line of content.split(/\r?\n/u)) {
1422
+ function formatTomlValue(value) {
1423
+ if (typeof value === "string") {
1424
+ return JSON.stringify(value);
1425
+ }
1426
+ if (typeof value === "boolean") {
1427
+ return value ? "true" : "false";
1428
+ }
1429
+ return String(value);
1430
+ }
1431
+
1432
+ function getTomlLines(content) {
1433
+ if (!content) {
1434
+ return [];
1435
+ }
1436
+
1437
+ const chunks = content.match(/[^\r\n]*(?:\r?\n|$)/gu) || [];
1438
+ const lines = [];
1439
+ let offset = 0;
1440
+
1441
+ for (const chunk of chunks) {
1442
+ if (!chunk && offset >= content.length) {
1443
+ break;
1444
+ }
1445
+
1446
+ const lineEndingMatch = chunk.match(/\r?\n$/u);
1447
+ const lineEnding = lineEndingMatch ? lineEndingMatch[0] : "";
1448
+ lines.push({
1449
+ line: lineEnding ? chunk.slice(0, -lineEnding.length) : chunk,
1450
+ start: offset
1451
+ });
1452
+ offset += chunk.length;
1453
+
1454
+ if (!lineEnding && offset >= content.length) {
1455
+ break;
1456
+ }
1457
+ }
1458
+
1459
+ return lines;
1460
+ }
1461
+
1462
+ function findTomlSectionStart(content, fromIndex = 0) {
1463
+ const scopedContent = content.slice(fromIndex);
1464
+ let multilineDelimiter = "";
1465
+
1466
+ for (const { line, start } of getTomlLines(scopedContent)) {
1349
1467
  const trimmed = line.trim();
1468
+
1469
+ if (multilineDelimiter) {
1470
+ if (line.includes(multilineDelimiter)) {
1471
+ multilineDelimiter = "";
1472
+ }
1473
+ continue;
1474
+ }
1475
+
1350
1476
  if (!trimmed || trimmed.startsWith("#")) {
1351
1477
  continue;
1352
1478
  }
1353
- if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
1354
- inSection = true;
1479
+
1480
+ if (/^\[[^\]]+\]\s*(?:#.*)?$/u.test(trimmed)) {
1481
+ return fromIndex + start;
1482
+ }
1483
+
1484
+ const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*)$/u);
1485
+ if (!match) {
1486
+ continue;
1487
+ }
1488
+
1489
+ const delimiter = getTomlMultilineStringDelimiter(match[2].trimStart());
1490
+ if (delimiter && match[2].indexOf(delimiter, delimiter.length) === -1) {
1491
+ multilineDelimiter = delimiter;
1492
+ }
1493
+ }
1494
+
1495
+ return -1;
1496
+ }
1497
+
1498
+ function splitTopLevelTomlContent(content) {
1499
+ const topLevelEnd = findTomlSectionStart(content);
1500
+ return {
1501
+ topLevel: topLevelEnd === -1 ? content : content.slice(0, topLevelEnd),
1502
+ rest: topLevelEnd === -1 ? "" : content.slice(topLevelEnd)
1503
+ };
1504
+ }
1505
+
1506
+ function getTomlMultilineStringDelimiter(value) {
1507
+ if (value.startsWith(`"""`)) {
1508
+ return `"""`;
1509
+ }
1510
+ if (value.startsWith(`'''`)) {
1511
+ return `'''`;
1512
+ }
1513
+ return "";
1514
+ }
1515
+
1516
+ function extractTopLevelTomlEntries(content) {
1517
+ const { topLevel } = splitTopLevelTomlContent(content);
1518
+ const lineEnding = detectLineEnding(topLevel);
1519
+ const lines = topLevel.split(/\r?\n/u);
1520
+ const entries = [];
1521
+
1522
+ for (let index = 0; index < lines.length; index += 1) {
1523
+ const line = lines[index];
1524
+ const trimmed = line.trim();
1525
+ if (!trimmed || trimmed.startsWith("#")) {
1526
+ continue;
1527
+ }
1528
+
1529
+ const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*)$/u);
1530
+ if (!match) {
1531
+ continue;
1532
+ }
1533
+
1534
+ const rawLines = [line];
1535
+ const delimiter = getTomlMultilineStringDelimiter(match[2].trimStart());
1536
+ if (delimiter && match[2].indexOf(delimiter, delimiter.length) === -1) {
1537
+ for (index += 1; index < lines.length; index += 1) {
1538
+ rawLines.push(lines[index]);
1539
+ if (lines[index].includes(delimiter)) {
1540
+ break;
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ entries.push({
1546
+ field: match[1],
1547
+ raw: rawLines.join(lineEnding)
1548
+ });
1549
+ }
1550
+
1551
+ return entries;
1552
+ }
1553
+
1554
+ function mergeMissingTopLevelTomlEntries(content, entries) {
1555
+ if (!entries.length) {
1556
+ return content;
1557
+ }
1558
+
1559
+ const lineEnding = detectLineEnding(content);
1560
+ const { topLevel, rest } = splitTopLevelTomlContent(content);
1561
+ const existingFields = new Set(extractTopLevelTomlEntries(content).map(({ field }) => field));
1562
+ const missingEntries = entries.filter(({ field }) => !existingFields.has(field));
1563
+ if (!missingEntries.length) {
1564
+ return content;
1565
+ }
1566
+
1567
+ let nextTopLevel = topLevel.trimEnd();
1568
+ for (const entry of missingEntries) {
1569
+ nextTopLevel = nextTopLevel
1570
+ ? `${nextTopLevel}${lineEnding}${entry.raw}`
1571
+ : entry.raw;
1572
+ }
1573
+
1574
+ nextTopLevel = nextTopLevel.trimEnd();
1575
+ if (rest && nextTopLevel) {
1576
+ nextTopLevel = `${nextTopLevel}${lineEnding}${lineEnding}`;
1577
+ }
1578
+ return `${nextTopLevel}${rest}`;
1579
+ }
1580
+
1581
+ function updateTomlBodyFields(body, values, lineEnding = "\n") {
1582
+ const normalizedBody = body.replace(/\r?\n$/u, "");
1583
+ const lines = normalizedBody ? normalizedBody.split(/\r?\n/u) : [];
1584
+ const remaining = new Map(
1585
+ Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== "")
1586
+ );
1587
+ const managedFields = new Set(remaining.keys());
1588
+ const seenFields = new Set();
1589
+ const nextLines = [];
1590
+
1591
+ for (const rawLine of lines) {
1592
+ const match = rawLine.match(/^(\s*)([A-Za-z0-9_]+)\s*=\s*.*$/u);
1593
+ if (!match || !managedFields.has(match[2])) {
1594
+ nextLines.push(rawLine);
1595
+ continue;
1596
+ }
1597
+
1598
+ if (seenFields.has(match[2])) {
1355
1599
  continue;
1356
1600
  }
1357
- if (inSection) {
1601
+
1602
+ nextLines.push(`${match[1]}${match[2]} = ${formatTomlValue(remaining.get(match[2]))}`);
1603
+ seenFields.add(match[2]);
1604
+ remaining.delete(match[2]);
1605
+ }
1606
+
1607
+ for (const [field, value] of remaining) {
1608
+ nextLines.push(`${field} = ${formatTomlValue(value)}`);
1609
+ }
1610
+
1611
+ return nextLines.join(lineEnding);
1612
+ }
1613
+
1614
+ function updateTomlSectionFields(content, sectionName, values) {
1615
+ const lineEnding = detectLineEnding(content);
1616
+ const sectionHeaderRegex = new RegExp(
1617
+ `^\\[${escapeRegExp(sectionName)}\\](?:[ \\t]+#.*)?[ \\t]*\\r?$`,
1618
+ "mu"
1619
+ );
1620
+ const headerMatch = sectionHeaderRegex.exec(content);
1621
+
1622
+ if (!headerMatch) {
1623
+ const nextSection = buildTomlSection(sectionName, values, lineEnding).trimEnd();
1624
+ return content.trimEnd()
1625
+ ? `${content.trimEnd()}${lineEnding}${lineEnding}${nextSection}${lineEnding}`
1626
+ : `${nextSection}${lineEnding}`;
1627
+ }
1628
+
1629
+ const sectionStart = headerMatch.index;
1630
+ const headerLineEnd = content.indexOf(lineEnding, sectionStart);
1631
+ const bodyStart = headerLineEnd === -1 ? content.length : headerLineEnd + lineEnding.length;
1632
+ const headerLine = content.slice(sectionStart, headerLineEnd === -1 ? content.length : headerLineEnd);
1633
+ const nextSectionStart = findTomlSectionStart(content, bodyStart);
1634
+ const sectionEnd = nextSectionStart === -1 ? content.length : nextSectionStart;
1635
+ const prefix = content.slice(0, sectionStart);
1636
+ const body = content.slice(bodyStart, sectionEnd);
1637
+ const suffix = content.slice(sectionEnd);
1638
+ const nextBody = updateTomlBodyFields(body, values, lineEnding);
1639
+ const renderedSection = nextBody
1640
+ ? `${headerLine}${lineEnding}${nextBody}${lineEnding}`
1641
+ : `${headerLine}${lineEnding}`;
1642
+
1643
+ return `${prefix}${renderedSection}${suffix.replace(/^\r?\n/u, "")}`;
1644
+ }
1645
+
1646
+ function readTopLevelTomlString(content, field) {
1647
+ const { topLevel } = splitTopLevelTomlContent(content);
1648
+ for (const line of topLevel.split(/\r?\n/u)) {
1649
+ const trimmed = line.trim();
1650
+ if (!trimmed || trimmed.startsWith("#")) {
1358
1651
  continue;
1359
1652
  }
1360
1653
  const match = trimmed.match(new RegExp(`^${escapeRegExp(field)}\\s*=\\s*"([^"]+)"$`, "u"));
@@ -1366,78 +1659,242 @@ function readTopLevelTomlString(content, field) {
1366
1659
  }
1367
1660
 
1368
1661
  function parseTomlSection(content, sectionName) {
1369
- const match = content.match(new RegExp(`\\[${escapeRegExp(sectionName)}\\]\\r?\\n([\\s\\S]*?)(?=\\r?\\n\\[|$)`, "u"));
1370
- if (!match) {
1662
+ const lineEnding = detectLineEnding(content);
1663
+ const sectionHeaderRegex = new RegExp(
1664
+ `^\\[${escapeRegExp(sectionName)}\\](?:[ \\t]+#.*)?[ \\t]*\\r?$`,
1665
+ "mu"
1666
+ );
1667
+ const headerMatch = sectionHeaderRegex.exec(content);
1668
+ if (!headerMatch) {
1371
1669
  return {};
1372
1670
  }
1373
1671
 
1672
+ const sectionStart = headerMatch.index;
1673
+ const headerLineEnd = content.indexOf(lineEnding, sectionStart);
1674
+ const bodyStart = headerLineEnd === -1 ? content.length : headerLineEnd + lineEnding.length;
1675
+ const nextSectionStart = findTomlSectionStart(content, bodyStart);
1676
+ const body = content.slice(bodyStart, nextSectionStart === -1 ? content.length : nextSectionStart);
1677
+
1374
1678
  const values = {};
1375
- for (const rawLine of match[1].split(/\r?\n/u)) {
1679
+ for (const rawLine of body.split(/\r?\n/u)) {
1376
1680
  const line = rawLine.trim();
1377
1681
  if (!line || line.startsWith("#")) {
1378
1682
  continue;
1379
1683
  }
1380
- const stringMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*"([^"]*)"$/u);
1684
+ const stringMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/u);
1381
1685
  if (stringMatch) {
1382
1686
  values[stringMatch[1]] = stringMatch[2];
1383
1687
  continue;
1384
1688
  }
1385
- const boolMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*(true|false)$/u);
1689
+ const boolMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*(true|false)\s*(?:#.*)?$/u);
1386
1690
  if (boolMatch) {
1387
1691
  values[boolMatch[1]] = boolMatch[2] === "true";
1692
+ continue;
1693
+ }
1694
+ const numberMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*(-?\d+(?:\.\d+)?)\s*(?:#.*)?$/u);
1695
+ if (numberMatch) {
1696
+ values[numberMatch[1]] = Number(numberMatch[2]);
1388
1697
  }
1389
1698
  }
1390
1699
  return values;
1391
1700
  }
1392
1701
 
1393
- async function getExistingCodexApiConfig() {
1394
- const content = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
1395
- const auth = await readJsonFileSafe(codexAuthPath, {});
1702
+ function stripCodexSubagentDefaults(content) {
1703
+ const lineEnding = detectLineEnding(content);
1704
+ let nextContent = removeTopLevelTomlField(content, "approvals_reviewer");
1705
+ nextContent = removeTopLevelTomlField(nextContent, "developer_instructions");
1706
+ nextContent = removeTomlSection(nextContent, "agents");
1707
+
1708
+ const featureValues = parseTomlSection(nextContent, "features");
1709
+ delete featureValues.multi_agent;
1710
+ delete featureValues.guardian_approval;
1711
+ nextContent = removeTomlSection(nextContent, "features");
1712
+
1713
+ if (Object.keys(featureValues).length) {
1714
+ const featuresSection = buildTomlSection("features", featureValues, lineEnding).trimEnd();
1715
+ nextContent = nextContent.trimEnd()
1716
+ ? `${nextContent.trimEnd()}${lineEnding}${lineEnding}${featuresSection}${lineEnding}`
1717
+ : `${featuresSection}${lineEnding}`;
1718
+ }
1719
+
1720
+ return nextContent;
1721
+ }
1722
+
1723
+ function mergeCodexTemplateDefaults(content, templateContent) {
1724
+ const defaultTopLevelFields = new Set([
1725
+ "personality",
1726
+ "disable_response_storage",
1727
+ "approvals_reviewer",
1728
+ "approval_policy",
1729
+ "sandbox_mode",
1730
+ "service_tier",
1731
+ "model",
1732
+ "model_reasoning_effort",
1733
+ "developer_instructions"
1734
+ ]);
1735
+ const templateEntries = extractTopLevelTomlEntries(templateContent)
1736
+ .filter(({ field }) => defaultTopLevelFields.has(field));
1737
+ let nextContent = mergeMissingTopLevelTomlEntries(content, templateEntries);
1738
+
1739
+ for (const sectionName of ["agents", "features"]) {
1740
+ const templateValues = parseTomlSection(templateContent, sectionName);
1741
+ const currentValues = parseTomlSection(nextContent, sectionName);
1742
+ const missingValues = Object.fromEntries(
1743
+ Object.entries(templateValues).filter(([field]) => !(field in currentValues))
1744
+ );
1745
+ if (Object.keys(missingValues).length) {
1746
+ nextContent = updateTomlSectionFields(nextContent, sectionName, missingValues);
1747
+ }
1748
+ }
1749
+
1750
+ return nextContent;
1751
+ }
1752
+
1753
+ async function loadBundledCodexConfigTemplate() {
1754
+ return readFile(codexTemplateConfigPath, "utf8");
1755
+ }
1756
+
1757
+ async function deployBundledCodexAgents() {
1758
+ const targetDir = join(home, ".codex", "agents");
1759
+ await mkdir(targetDir, { recursive: true });
1760
+ const entries = (await readdir(codexTemplateAgentsPath, { withFileTypes: true }))
1761
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".toml"))
1762
+ .map((entry) => entry.name)
1763
+ .sort((left, right) => left.localeCompare(right));
1764
+
1765
+ const deployed = [];
1766
+ for (const name of entries) {
1767
+ const source = join(codexTemplateAgentsPath, name);
1768
+ const target = join(targetDir, name);
1769
+ await backupExistingPath(target);
1770
+ await cp(source, target, { force: true });
1771
+ deployed.push(target);
1772
+ }
1773
+ return deployed;
1774
+ }
1775
+
1776
+ function getDefaultCodexEnvKey(providerId) {
1777
+ return `${providerId.toUpperCase().replace(/-/gu, "_")}_API_KEY`;
1778
+ }
1779
+
1780
+ function resolveExistingCodexApiConfig(content, auth = {}) {
1396
1781
  const providerId = readTopLevelTomlString(content, "model_provider") || "abelworkflow";
1397
1782
  const provider = parseTomlSection(content, `model_providers.${providerId}`);
1398
- const envKey = provider.temp_env_key || "OPENAI_API_KEY";
1783
+ const requiresOpenAiAuth = provider.requires_openai_auth !== false;
1784
+ const configuredEnvKey = provider.temp_env_key || "";
1785
+ const defaultEnvKey = requiresOpenAiAuth ? "OPENAI_API_KEY" : getDefaultCodexEnvKey(providerId);
1786
+ const envKey = configuredEnvKey || defaultEnvKey;
1787
+ const apiKeyCandidates = [
1788
+ configuredEnvKey,
1789
+ envKey,
1790
+ defaultEnvKey,
1791
+ "OPENAI_API_KEY"
1792
+ ].filter(Boolean);
1793
+ const apiKeyMatch = apiKeyCandidates.find((key) => typeof auth[key] === "string" && auth[key]);
1794
+ const apiKey = apiKeyMatch ? auth[apiKeyMatch] : "";
1795
+
1399
1796
  return {
1400
1797
  providerId,
1401
- providerName: provider.name || (providerId === "abelworkflow" ? "AbelWorkflow" : providerId),
1798
+ providerName: provider.name || providerId,
1402
1799
  baseUrl: provider.base_url || "https://api.openai.com/v1",
1403
1800
  envKey,
1404
- apiKey: auth[envKey] || ""
1801
+ legacyEnvKeys: configuredEnvKey && configuredEnvKey !== envKey ? [configuredEnvKey] : [],
1802
+ apiKey
1405
1803
  };
1406
1804
  }
1407
1805
 
1806
+ async function getExistingCodexApiConfig() {
1807
+ const content = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
1808
+ const auth = await readJsonFileSafe(codexAuthPath, {});
1809
+ return resolveExistingCodexApiConfig(content, auth);
1810
+ }
1811
+
1408
1812
  async function configureCodexApi() {
1409
1813
  const existing = await getExistingCodexApiConfig();
1410
1814
  const providerId = existing.providerId || "abelworkflow";
1411
- const providerName = existing.providerName || "AbelWorkflow";
1815
+ const providerName = existing.providerName || providerId;
1412
1816
  const baseUrl = await promptText("Codex Base URL", {
1413
1817
  defaultValue: existing.baseUrl
1414
1818
  });
1415
1819
  const apiKey = await promptSecret("Codex 第三方 API Key", {
1416
1820
  defaultValue: existing.apiKey || undefined
1417
1821
  });
1418
- const envKey = `${providerId.toUpperCase().replace(/-/gu, "_")}_API_KEY`;
1822
+ const shouldDeploySubagents = await promptConfirm("是否部署 Codex subagents 配置?", true);
1823
+ const envKey = existing.envKey || "OPENAI_API_KEY";
1824
+ const currentContent = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
1825
+ const templateContent = await loadBundledCodexConfigTemplate();
1826
+ const content = buildCodexConfigContent(currentContent, {
1827
+ templateContent,
1828
+ mergeMissingTemplateDefaults: true,
1829
+ includeSubagentDefaults: shouldDeploySubagents,
1830
+ providerId,
1831
+ providerName,
1832
+ baseUrl,
1833
+ envKey
1834
+ });
1419
1835
 
1420
- let content = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
1836
+ await backupExistingPath(codexConfigPath);
1837
+ await mkdir(dirname(codexConfigPath), { recursive: true });
1838
+ await writeFile(codexConfigPath, content, "utf8");
1839
+
1840
+ const auth = mergeCodexAuthData(await readJsonFileSafe(codexAuthPath, {}), envKey, apiKey, existing.legacyEnvKeys || []);
1841
+ await writeJsonFileWithBackup(codexAuthPath, auth);
1842
+
1843
+ console.log(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
1844
+ console.log(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(apiKey)})`);
1845
+ if (shouldDeploySubagents) {
1846
+ const deployed = await deployBundledCodexAgents();
1847
+ console.log(`已部署 ${deployed.length} 个 Codex subagents 到 ${pathToLabel(join(home, ".codex", "agents"))}`);
1848
+ } else {
1849
+ console.log("已跳过 Codex subagents 部署。");
1850
+ }
1851
+ }
1852
+
1853
+ function buildCodexConfigContent(currentContent, {
1854
+ templateContent = "",
1855
+ mergeMissingTemplateDefaults = false,
1856
+ includeSubagentDefaults = true,
1857
+ providerId,
1858
+ providerName,
1859
+ baseUrl,
1860
+ envKey
1861
+ }) {
1862
+ const effectiveTemplateContent = includeSubagentDefaults
1863
+ ? templateContent
1864
+ : stripCodexSubagentDefaults(templateContent);
1865
+ const hasCurrentContent = Boolean(currentContent.trim());
1866
+ let content = hasCurrentContent ? currentContent : effectiveTemplateContent;
1867
+ if (!includeSubagentDefaults && hasCurrentContent) {
1868
+ content = stripCodexSubagentDefaults(content);
1869
+ }
1870
+ if (hasCurrentContent && mergeMissingTemplateDefaults && effectiveTemplateContent.trim()) {
1871
+ content = mergeCodexTemplateDefaults(content, effectiveTemplateContent);
1872
+ }
1421
1873
  const lineEnding = detectLineEnding(content);
1874
+ if (includeSubagentDefaults && readTopLevelTomlString(content, "approvals_reviewer") === "guardian_subagent") {
1875
+ content = updateTopLevelTomlField(content, "approvals_reviewer", "reviewer");
1876
+ }
1422
1877
  content = updateTopLevelTomlField(content, "model_provider", providerId);
1423
- content = removeTomlSection(content, `model_providers.${providerId}`);
1424
- content = `${content.trimEnd() ? `${content.trimEnd()}${lineEnding}${lineEnding}` : ""}${buildTomlSection(`model_providers.${providerId}`, {
1878
+ content = updateTopLevelTomlField(content, "preferred_auth_method", "apikey");
1879
+ content = updateTomlSectionFields(content, `model_providers.${providerId}`, {
1425
1880
  name: providerName,
1426
1881
  base_url: baseUrl,
1427
1882
  wire_api: "responses",
1428
1883
  temp_env_key: envKey,
1429
1884
  requires_openai_auth: true
1430
- }, lineEnding)}`;
1431
-
1432
- await mkdir(dirname(codexConfigPath), { recursive: true });
1433
- await writeFile(codexConfigPath, `${content.trim()}${lineEnding}`, "utf8");
1434
-
1435
- const auth = await readJsonFileSafe(codexAuthPath, {});
1436
- auth[envKey] = apiKey;
1437
- await writeJsonFileSafe(codexAuthPath, auth);
1885
+ });
1886
+ return `${content.trim()}${lineEnding}`;
1887
+ }
1438
1888
 
1439
- console.log(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
1440
- console.log(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(apiKey)})`);
1889
+ function mergeCodexAuthData(auth, envKey, apiKey, legacyEnvKeys = []) {
1890
+ const nextAuth = auth && typeof auth === "object" ? { ...auth } : {};
1891
+ for (const key of legacyEnvKeys) {
1892
+ if (key && key !== envKey) {
1893
+ delete nextAuth[key];
1894
+ }
1895
+ }
1896
+ nextAuth[envKey] = apiKey;
1897
+ return nextAuth;
1441
1898
  }
1442
1899
 
1443
1900
  async function installCliTool(tool) {
@@ -1577,7 +2034,21 @@ async function main() {
1577
2034
  await runInteractiveMenu(options);
1578
2035
  }
1579
2036
 
1580
- main().catch((error) => {
1581
- console.error(error instanceof Error ? error.message : String(error));
1582
- process.exit(1);
1583
- });
2037
+ export {
2038
+ buildCodexConfigContent,
2039
+ getRunCommandSpawnOptions,
2040
+ main,
2041
+ mergeCodexAuthData,
2042
+ mergeClaudeSettingsWithDefaults,
2043
+ resolveExistingCodexApiConfig,
2044
+ updateTomlSectionFields
2045
+ };
2046
+
2047
+ const isDirectExecution = process.argv[1] ? resolve(process.argv[1]) === __filename : false;
2048
+
2049
+ if (isDirectExecution) {
2050
+ main().catch((error) => {
2051
+ console.error(error instanceof Error ? error.message : String(error));
2052
+ process.exit(1);
2053
+ });
2054
+ }