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/bin/abelworkflow.mjs +6 -1
- package/lib/cli.mjs +543 -72
- package/lib/templates/codex/agents/default.toml +56 -0
- package/lib/templates/codex/agents/explorer.toml +63 -0
- package/lib/templates/codex/agents/planner.toml +89 -0
- package/lib/templates/codex/agents/reviewer.toml +71 -0
- package/lib/templates/codex/agents/worker.toml +67 -0
- package/lib/templates/codex/config-base.toml +95 -0
- package/package.json +2 -2
- package/skills/dev-browser/SKILL.md +61 -39
- package/skills/dev-browser/bun.lock +17 -0
- package/skills/dev-browser/package.json +2 -2
- package/skills/dev-browser/scripts/start.ts +279 -0
- package/skills/dev-browser/src/entrypoint.ts +157 -0
- package/skills/dev-browser/src/index.ts +4 -2
- package/skills/dev-browser/src/runtime.ts +147 -0
- package/skills/dev-browser/src/snapshot/browser-script.ts +2 -1
- package/skills/dev-browser/src/startup.test.ts +95 -0
- package/skills/dev-browser/src/startup.ts +153 -0
- package/skills/dev-browser/src/types.ts +1 -0
- package/skills/dev-browser/scripts/start-relay.ts +0 -32
- package/skills/dev-browser/scripts/start-server.ts +0 -117
- package/skills/dev-browser/server.sh +0 -24
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
-
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
-
|
|
1374
|
+
let nextTopLevel = `${topLevel.slice(0, start)}${topLevel.slice(start + entry.raw.length)}`;
|
|
1375
|
+
nextTopLevel = collapseBlankLines(nextTopLevel, lineEnding).trimEnd();
|
|
1315
1376
|
|
|
1316
|
-
if (rest &&
|
|
1317
|
-
|
|
1377
|
+
if (rest && nextTopLevel) {
|
|
1378
|
+
nextTopLevel = `${nextTopLevel}${lineEnding}${lineEnding}`;
|
|
1318
1379
|
}
|
|
1319
1380
|
|
|
1320
|
-
return `${
|
|
1381
|
+
return `${nextTopLevel}${rest}`;
|
|
1321
1382
|
}
|
|
1322
1383
|
|
|
1323
1384
|
function removeTomlSection(content, sectionName) {
|
|
1324
1385
|
const lineEnding = detectLineEnding(content);
|
|
1325
|
-
const
|
|
1326
|
-
|
|
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
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
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
|
|
1370
|
-
|
|
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
|
|
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*"([^"]*)"
|
|
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)
|
|
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
|
-
|
|
1394
|
-
const
|
|
1395
|
-
|
|
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
|
|
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 ||
|
|
1798
|
+
providerName: provider.name || providerId,
|
|
1402
1799
|
baseUrl: provider.base_url || "https://api.openai.com/v1",
|
|
1403
1800
|
envKey,
|
|
1404
|
-
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
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 =
|
|
1424
|
-
content =
|
|
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
|
-
}
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
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
|
+
}
|