abelworkflow 0.9.2 → 1.0.0-rc.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.
Files changed (126) hide show
  1. package/README.md +55 -182
  2. package/bin/abelworkflow.mjs +5 -3
  3. package/lib/cli/args.mjs +76 -0
  4. package/lib/cli/main.mjs +387 -0
  5. package/lib/cli/prompts.mjs +119 -0
  6. package/lib/config/dotenv.mjs +102 -0
  7. package/lib/config/jsonc.mjs +80 -0
  8. package/lib/config/store.mjs +310 -0
  9. package/lib/config/toml.mjs +638 -0
  10. package/lib/installer/assets.mjs +326 -0
  11. package/lib/installer/install.mjs +167 -0
  12. package/lib/installer/links.mjs +427 -0
  13. package/lib/installer/render.mjs +46 -0
  14. package/lib/installer/state.mjs +182 -0
  15. package/lib/paths.mjs +99 -0
  16. package/lib/providers/claude.mjs +388 -0
  17. package/lib/providers/codex.mjs +433 -0
  18. package/lib/providers/pi.mjs +362 -0
  19. package/lib/providers/skills.mjs +164 -0
  20. package/lib/templates/codex/agents/default.toml +7 -0
  21. package/lib/templates/codex/agents/explorer.toml +7 -0
  22. package/lib/templates/codex/agents/planner.toml +7 -0
  23. package/lib/templates/codex/agents/reviewer.toml +7 -0
  24. package/lib/templates/codex/agents/worker.toml +7 -0
  25. package/lib/templates/codex/config-base.toml +11 -3
  26. package/{AGENTS.md → lib/templates/workflow/AGENTS.md} +8 -3
  27. package/{commands → lib/templates/workflow/commands}/abel-diagnose.md +5 -5
  28. package/{commands → lib/templates/workflow/commands}/abel-implement.md +3 -8
  29. package/{commands → lib/templates/workflow/commands}/abel-init.md +1 -1
  30. package/{commands → lib/templates/workflow/commands}/abel-plan.md +1 -7
  31. package/{commands → lib/templates/workflow/commands}/abel-research.md +1 -1
  32. package/{.gitignore → lib/templates/workflow/gitignore.template} +0 -2
  33. package/lib/tools/cli-installer.mjs +277 -0
  34. package/package.json +43 -10
  35. package/skills/context7-auto-research/SKILL.md +5 -5
  36. package/skills/context7-auto-research/{context7-api.js → context7-api.cjs} +5 -8
  37. package/skills/dev-browser/SKILL.md +53 -33
  38. package/skills/dev-browser/dist/scripts/start.d.ts +2 -0
  39. package/skills/dev-browser/dist/scripts/start.d.ts.map +1 -0
  40. package/skills/dev-browser/dist/scripts/start.js +118 -0
  41. package/skills/dev-browser/dist/scripts/start.js.map +1 -0
  42. package/skills/dev-browser/dist/src/client.d.ts +93 -0
  43. package/skills/dev-browser/dist/src/client.d.ts.map +1 -0
  44. package/skills/dev-browser/dist/src/client.js +310 -0
  45. package/skills/dev-browser/dist/src/client.js.map +1 -0
  46. package/skills/dev-browser/dist/src/entrypoint.d.ts +29 -0
  47. package/skills/dev-browser/dist/src/entrypoint.d.ts.map +1 -0
  48. package/skills/dev-browser/dist/src/entrypoint.js +106 -0
  49. package/skills/dev-browser/dist/src/entrypoint.js.map +1 -0
  50. package/skills/dev-browser/dist/src/index.d.ts +4 -0
  51. package/skills/dev-browser/dist/src/index.d.ts.map +1 -0
  52. package/skills/dev-browser/dist/src/index.js +2 -0
  53. package/skills/dev-browser/dist/src/index.js.map +1 -0
  54. package/skills/dev-browser/dist/src/page-api.d.ts +25 -0
  55. package/skills/dev-browser/dist/src/page-api.d.ts.map +1 -0
  56. package/skills/dev-browser/dist/src/page-api.js +104 -0
  57. package/skills/dev-browser/dist/src/page-api.js.map +1 -0
  58. package/skills/dev-browser/dist/src/relay.d.ts +27 -0
  59. package/skills/dev-browser/dist/src/relay.d.ts.map +1 -0
  60. package/skills/dev-browser/dist/src/relay.js +521 -0
  61. package/skills/dev-browser/dist/src/relay.js.map +1 -0
  62. package/skills/dev-browser/dist/src/runtime.d.ts +45 -0
  63. package/skills/dev-browser/dist/src/runtime.d.ts.map +1 -0
  64. package/skills/dev-browser/dist/src/runtime.js +54 -0
  65. package/skills/dev-browser/dist/src/runtime.js.map +1 -0
  66. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +23 -0
  67. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +1 -0
  68. package/skills/dev-browser/{src/snapshot/browser-script.ts → dist/src/snapshot/browser-script.js} +21 -30
  69. package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +1 -0
  70. package/skills/dev-browser/dist/src/snapshot/index.d.ts +14 -0
  71. package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +1 -0
  72. package/skills/dev-browser/{src/snapshot/index.ts → dist/src/snapshot/index.js} +2 -2
  73. package/skills/dev-browser/dist/src/snapshot/index.js.map +1 -0
  74. package/skills/dev-browser/dist/src/snapshot/inject.d.ts +13 -0
  75. package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +1 -0
  76. package/skills/dev-browser/{src/snapshot/inject.ts → dist/src/snapshot/inject.js} +2 -2
  77. package/skills/dev-browser/dist/src/snapshot/inject.js.map +1 -0
  78. package/skills/dev-browser/dist/src/standalone.d.ts +32 -0
  79. package/skills/dev-browser/dist/src/standalone.d.ts.map +1 -0
  80. package/skills/dev-browser/dist/src/standalone.js +174 -0
  81. package/skills/dev-browser/dist/src/standalone.js.map +1 -0
  82. package/skills/dev-browser/dist/src/startup.d.ts +55 -0
  83. package/skills/dev-browser/dist/src/startup.d.ts.map +1 -0
  84. package/skills/dev-browser/dist/src/startup.js +81 -0
  85. package/skills/dev-browser/dist/src/startup.js.map +1 -0
  86. package/skills/dev-browser/dist/src/target-registry.d.ts +29 -0
  87. package/skills/dev-browser/dist/src/target-registry.d.ts.map +1 -0
  88. package/skills/dev-browser/dist/src/target-registry.js +135 -0
  89. package/skills/dev-browser/dist/src/target-registry.js.map +1 -0
  90. package/skills/dev-browser/dist/src/types.d.ts +27 -0
  91. package/skills/dev-browser/dist/src/types.d.ts.map +1 -0
  92. package/skills/dev-browser/dist/src/types.js +2 -0
  93. package/skills/dev-browser/dist/src/types.js.map +1 -0
  94. package/skills/dev-browser/package-lock.json +67 -1510
  95. package/skills/dev-browser/package.json +18 -14
  96. package/skills/dev-browser/references/scraping.md +94 -105
  97. package/skills/grok-search/defaults.json +3 -0
  98. package/skills/grok-search/gitignore.template +4 -0
  99. package/skills/grok-search/scripts/_dotenv.py +5 -1
  100. package/skills/grok-search/scripts/groksearch_cli.py +15 -33
  101. package/skills/prompt-enhancer/TEMPLATE.md +0 -4
  102. package/skills/prompt-enhancer/requirements.txt +0 -1
  103. package/skills/prompt-enhancer/scripts/_dotenv.py +5 -1
  104. package/skills/prompt-enhancer/scripts/enhance.py +4 -51
  105. package/skills/prompt-enhancer/scripts/prompt_enhancer_entry.py +1 -16
  106. package/.skill-lock.json +0 -29
  107. package/lib/cli/logic.mjs +0 -213
  108. package/lib/cli.mjs +0 -3016
  109. package/skills/confidence-check/SKILL.md +0 -34
  110. package/skills/confidence-check/confidence.ts +0 -276
  111. package/skills/dev-browser/bun.lock +0 -460
  112. package/skills/dev-browser/scripts/start.ts +0 -280
  113. package/skills/dev-browser/src/client.ts +0 -474
  114. package/skills/dev-browser/src/entrypoint.ts +0 -157
  115. package/skills/dev-browser/src/index.ts +0 -289
  116. package/skills/dev-browser/src/relay.ts +0 -731
  117. package/skills/dev-browser/src/runtime.test.ts +0 -60
  118. package/skills/dev-browser/src/runtime.ts +0 -171
  119. package/skills/dev-browser/src/startup.test.ts +0 -95
  120. package/skills/dev-browser/src/startup.ts +0 -153
  121. package/skills/dev-browser/src/types.ts +0 -35
  122. package/skills/dev-browser/tsconfig.json +0 -36
  123. package/skills/dev-browser/vitest.config.ts +0 -12
  124. package/skills/sequential-think/SKILL.md +0 -198
  125. package/skills/sequential-think/scripts/.env.example +0 -5
  126. package/skills/sequential-think/scripts/sequential_think_cli.py +0 -253
@@ -0,0 +1,433 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, readFile, readdir, unlink } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import * as p from "@clack/prompts";
5
+ import {
6
+ pathExists,
7
+ readJsonFileSafe,
8
+ writeJsonFileWithBackup,
9
+ writeText
10
+ } from "../config/store.mjs";
11
+ import {
12
+ buildTomlSection,
13
+ detectLineEnding,
14
+ extractTopLevelTomlEntries,
15
+ getTomlSectionFieldNames,
16
+ formatTomlKeySegment,
17
+ mergeMissingTopLevelTomlEntries,
18
+ parseTomlSection,
19
+ readTopLevelTomlString,
20
+ removeTomlSection,
21
+ removeTopLevelTomlField,
22
+ updateTomlSectionFields,
23
+ updateTopLevelTomlField
24
+ } from "../config/toml.mjs";
25
+ import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
26
+ import { normalizeOpenAiBaseUrl } from "./pi.mjs";
27
+
28
+ const publishedCodexDeveloperInstructionHashes = new Set([
29
+ "957adb227dd3fc298985acca38db4c22b8e018d387dab91f1ea8287db35385db"
30
+ ]);
31
+
32
+ const publishedCodexAgentHashes = {
33
+ "default.toml": new Set([
34
+ "7add720eb7ed030871088fe9f994a7e34a2b3cae1f898fe7f97c4e302f4a1844",
35
+ "72d3da0275f4add64ff05da94f391443e8e4b5c43b646e0f89f2d17fa4345cb4",
36
+ "7dc002559f5d78534e70c7c4683d51a48ffe94725c199855a35f61124aead876"
37
+ ]),
38
+ "explorer.toml": new Set([
39
+ "e4e9c4eed4c028dded095b4cf36cb0ac408abcd86b9ab7c139440e1c58355644",
40
+ "a236d7b586c4e312a62bb8b36115fe9780e28502c0af160ce5cd45b67c5b8479",
41
+ "4740f13e9a478528dcb097188997445efa7607b1b07ae64774b1dd26fcd6f853"
42
+ ]),
43
+ "planner.toml": new Set([
44
+ "2b16f821b14224cdfc9dab1fe14c41b9ff2b63642b0d9b340ef84b67a753a3b7",
45
+ "741c9dfce5085d6bdc49f6e5004475974d54c2bde95c0b589eda86a63e96e58f",
46
+ "7fde442a5ba5e996936b4ec853b036ebbb9f06ee9baef6540124d081c7107196"
47
+ ]),
48
+ "reviewer.toml": new Set([
49
+ "08dcf8f5fee281f3b6120b63559086b7c675a619be72f761de4add711543b353",
50
+ "9455d0189f324f7c606f7ae94ca338e9bd5c468f072188ac1f18e3749c918cd1",
51
+ "a7292a533c9146bf5a2eb292cbfe889ca47bf4808324cddc1732c3c1721d437c"
52
+ ]),
53
+ "worker.toml": new Set([
54
+ "13533715464b1f58a9c311e16dc3a29e92ecb95793fd8aff69a5ca731ae46ac8",
55
+ "ea12b5b6173eb8fcaa57a8b447b1f81b35a6ac59e75025b88198c3f85175922c",
56
+ "bfbc214e7b582774de759b118544c3f1fb61e1284d6ffd6a61ab772d28b6eaa0"
57
+ ])
58
+ };
59
+
60
+ function stripCodexSubagentDefaults(content) {
61
+ const lineEnding = detectLineEnding(content);
62
+ let nextContent = removeTopLevelTomlField(content, "approvals_reviewer");
63
+ nextContent = removeTopLevelTomlField(nextContent, "developer_instructions");
64
+ nextContent = removeTomlSection(nextContent, "agents");
65
+
66
+ const featureValues = parseTomlSection(nextContent, "features");
67
+ delete featureValues.multi_agent;
68
+ delete featureValues.guardian_approval;
69
+ nextContent = removeTomlSection(nextContent, "features");
70
+
71
+ if (Object.keys(featureValues).length) {
72
+ const featuresSection = buildTomlSection("features", featureValues, lineEnding).trimEnd();
73
+ nextContent = nextContent.trimEnd()
74
+ ? `${nextContent.trimEnd()}${lineEnding}${lineEnding}${featuresSection}${lineEnding}`
75
+ : `${featuresSection}${lineEnding}`;
76
+ }
77
+
78
+ return nextContent;
79
+ }
80
+
81
+ function mergeCodexTemplateDefaults(content, templateContent) {
82
+ const defaultTopLevelFields = new Set([
83
+ "personality",
84
+ "disable_response_storage",
85
+ "approvals_reviewer",
86
+ "approval_policy",
87
+ "sandbox_mode",
88
+ "service_tier",
89
+ "model",
90
+ "model_reasoning_effort",
91
+ "developer_instructions"
92
+ ]);
93
+ const templateEntries = extractTopLevelTomlEntries(templateContent)
94
+ .filter(({ field }) => defaultTopLevelFields.has(field));
95
+ let nextContent = mergeMissingTopLevelTomlEntries(content, templateEntries);
96
+ const currentDeveloperInstructions = readTopLevelTomlString(nextContent, "developer_instructions");
97
+ const templateDeveloperInstructions = readTopLevelTomlString(templateContent, "developer_instructions");
98
+ if (templateDeveloperInstructions && publishedCodexDeveloperInstructionHashes.has(
99
+ sha256(currentDeveloperInstructions)
100
+ )) {
101
+ nextContent = updateTopLevelTomlField(
102
+ nextContent,
103
+ "developer_instructions",
104
+ templateDeveloperInstructions
105
+ );
106
+ }
107
+
108
+ for (const sectionName of ["agents", "features"]) {
109
+ const templateValues = parseTomlSection(templateContent, sectionName);
110
+ const currentFields = new Set(getTomlSectionFieldNames(nextContent, sectionName));
111
+ const missingValues = Object.fromEntries(
112
+ Object.entries(templateValues).filter(([field]) => !currentFields.has(field))
113
+ );
114
+ if (Object.keys(missingValues).length) {
115
+ nextContent = updateTomlSectionFields(nextContent, sectionName, missingValues);
116
+ }
117
+ }
118
+
119
+ return nextContent;
120
+ }
121
+
122
+ async function loadBundledCodexConfigTemplate(paths = defaultPaths) {
123
+ return readFile(paths.codexTemplateConfigPath, "utf8");
124
+ }
125
+
126
+ function sha256(content) {
127
+ return createHash("sha256").update(content).digest("hex");
128
+ }
129
+
130
+ async function readCodexAgentTarget(path) {
131
+ try {
132
+ const targetStat = await lstat(path);
133
+ return targetStat.isFile()
134
+ ? { exists: true, content: await readFile(path) }
135
+ : { exists: true, content: null };
136
+ } catch (error) {
137
+ if (error?.code === "ENOENT") return { exists: false, content: null };
138
+ throw error;
139
+ }
140
+ }
141
+
142
+ function assertCodexAgentDirectory(targetDir, targetStat, kind) {
143
+ if (targetStat.isSymbolicLink() || !targetStat.isDirectory()) {
144
+ throw new Error(`Codex agent ${kind} conflict at ${targetDir}`);
145
+ }
146
+ }
147
+
148
+ async function ensureCodexAgentDirectory(targetDir, kind) {
149
+ try {
150
+ assertCodexAgentDirectory(targetDir, await lstat(targetDir), kind);
151
+ return;
152
+ } catch (error) {
153
+ if (error?.code !== "ENOENT") throw error;
154
+ }
155
+
156
+ try {
157
+ await mkdir(targetDir);
158
+ } catch (error) {
159
+ if (error?.code === "EEXIST" || error?.code === "ENOTDIR") {
160
+ throw new Error(`Codex agent ${kind} conflict at ${targetDir}`, { cause: error });
161
+ }
162
+ throw error;
163
+ }
164
+ assertCodexAgentDirectory(targetDir, await lstat(targetDir), kind);
165
+ }
166
+
167
+ async function ensureCodexAgentContainer(homeDir) {
168
+ await mkdir(homeDir, { recursive: true });
169
+ const codexDir = join(homeDir, ".codex");
170
+ await ensureCodexAgentDirectory(codexDir, "ancestor");
171
+ const targetDir = join(codexDir, "agents");
172
+ await ensureCodexAgentDirectory(targetDir, "container");
173
+ return targetDir;
174
+ }
175
+
176
+ function isCodexAgentOwnership(name, hash) {
177
+ return name.endsWith(".toml")
178
+ && !name.includes("/")
179
+ && !name.includes("\\")
180
+ && typeof hash === "string"
181
+ && /^[a-f0-9]{64}$/u.test(hash);
182
+ }
183
+
184
+ function isPublishedCodexAgent(name, hash) {
185
+ return publishedCodexAgentHashes[name]?.has(hash) ?? false;
186
+ }
187
+
188
+ async function deployBundledCodexAgents(paths = defaultPaths, previousManagedFiles = {}) {
189
+ const targetDir = await ensureCodexAgentContainer(paths.homeDir);
190
+ const entries = (await readdir(paths.codexTemplateAgentsPath, { withFileTypes: true }))
191
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".toml"))
192
+ .map((entry) => entry.name)
193
+ .sort((left, right) => left.localeCompare(right));
194
+
195
+ const result = {
196
+ managedFiles: {},
197
+ created: [],
198
+ updated: [],
199
+ unchanged: [],
200
+ conflicts: []
201
+ };
202
+ for (const name of entries) {
203
+ const source = join(paths.codexTemplateAgentsPath, name);
204
+ const target = join(targetDir, name);
205
+ const sourceContent = await readFile(source);
206
+ const sourceHash = sha256(sourceContent);
207
+ const current = await readCodexAgentTarget(target);
208
+ const currentHash = current.content ? sha256(current.content) : "";
209
+
210
+ if (!current.exists) {
211
+ await writeText(target, sourceContent, { backupLimit: 0 });
212
+ result.created.push(target);
213
+ } else if (currentHash === sourceHash) {
214
+ result.unchanged.push(target);
215
+ } else if (current.content && (
216
+ previousManagedFiles?.[name] === currentHash
217
+ || isPublishedCodexAgent(name, currentHash)
218
+ )) {
219
+ await writeText(target, sourceContent, { backupLimit: 0 });
220
+ result.updated.push(target);
221
+ } else {
222
+ result.conflicts.push(target);
223
+ if (isCodexAgentOwnership(name, previousManagedFiles?.[name])) {
224
+ result.managedFiles[name] = previousManagedFiles[name];
225
+ }
226
+ continue;
227
+ }
228
+ result.managedFiles[name] = sourceHash;
229
+ }
230
+
231
+ const bundledNames = new Set(entries);
232
+ const previousEntries = previousManagedFiles
233
+ && typeof previousManagedFiles === "object"
234
+ && !Array.isArray(previousManagedFiles)
235
+ ? Object.entries(previousManagedFiles).sort(([left], [right]) => left.localeCompare(right))
236
+ : [];
237
+ for (const [name, previousHash] of previousEntries) {
238
+ if (bundledNames.has(name) || !isCodexAgentOwnership(name, previousHash)) continue;
239
+ const target = join(targetDir, name);
240
+ const current = await readCodexAgentTarget(target);
241
+ if (!current.exists) continue;
242
+ if (current.content && sha256(current.content) === previousHash) {
243
+ await unlink(target);
244
+ continue;
245
+ }
246
+ result.managedFiles[name] = previousHash;
247
+ result.conflicts.push(target);
248
+ }
249
+ return result;
250
+ }
251
+
252
+ function getDefaultCodexEnvKey(providerId) {
253
+ if (providerId === "openai") return "OPENAI_API_KEY";
254
+ if (providerId === "abelworkflow") return "ABELWORKFLOW_API_KEY";
255
+ const digest = createHash("sha256").update(providerId).digest("hex").toUpperCase();
256
+ return `ABELWORKFLOW_PROVIDER_${digest}_API_KEY`;
257
+ }
258
+
259
+ function getCodexProviderSectionName(providerId) {
260
+ return `model_providers.${formatTomlKeySegment(providerId)}`;
261
+ }
262
+
263
+ function assertCodexAuthKeyAvailable(auth, envKey) {
264
+ if (
265
+ auth
266
+ && typeof auth === "object"
267
+ && !Array.isArray(auth)
268
+ && Object.hasOwn(auth, envKey)
269
+ && typeof auth[envKey] !== "string"
270
+ ) {
271
+ throw new Error(`Environment key ${envKey} conflicts with structured Codex auth state`);
272
+ }
273
+ }
274
+
275
+ function resolveExistingCodexApiConfig(content, auth = {}) {
276
+ const providerId = readTopLevelTomlString(content, "model_provider") || "abelworkflow";
277
+ const provider = parseTomlSection(content, getCodexProviderSectionName(providerId));
278
+ const configuredEnvKey = provider.temp_env_key || "";
279
+ const envKey = configuredEnvKey || getDefaultCodexEnvKey(providerId);
280
+ assertCodexAuthKeyAvailable(auth, envKey);
281
+ const apiKey = typeof auth[envKey] === "string" ? auth[envKey] : "";
282
+
283
+ return {
284
+ providerId,
285
+ providerName: provider.name || providerId,
286
+ baseUrl: provider.base_url || "https://api.openai.com/v1",
287
+ envKey,
288
+ apiKey
289
+ };
290
+ }
291
+
292
+ async function getExistingCodexApiConfig(paths = defaultPaths) {
293
+ const content = await pathExists(paths.codexConfigPath) ? await readFile(paths.codexConfigPath, "utf8") : "";
294
+ const auth = await readJsonFileSafe(paths.codexAuthPath, {}, { sensitive: true });
295
+ return resolveExistingCodexApiConfig(content, auth);
296
+ }
297
+
298
+ async function persistCodexConfiguration(paths, { content, auth }) {
299
+ await writeText(paths.codexConfigPath, content);
300
+ await writeJsonFileWithBackup(paths.codexAuthPath, auth, { sensitive: true });
301
+ }
302
+
303
+ async function configureCodexApi(paths = defaultPaths, promptApi, ownership = {}) {
304
+ const {
305
+ assertNotCancelled,
306
+ confirmOrCancel,
307
+ required,
308
+ requiredUnlessExisting,
309
+ resolvePasswordValue
310
+ } = promptApi;
311
+ const existing = await getExistingCodexApiConfig(paths);
312
+ const providerId = existing.providerId || "abelworkflow";
313
+ const providerName = existing.providerName || providerId;
314
+ const baseUrlInput = await p.text({
315
+ message: "Codex Base URL",
316
+ defaultValue: existing.baseUrl,
317
+ validate: required()
318
+ });
319
+ assertNotCancelled(baseUrlInput);
320
+ const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
321
+
322
+ const apiKey = await p.password({
323
+ message: "Codex 第三方 API Key(输入 - 清除)",
324
+ mask: "*",
325
+ defaultValue: existing.apiKey || undefined,
326
+ validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
327
+ });
328
+ assertNotCancelled(apiKey);
329
+ const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
330
+
331
+ const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
332
+ let managedCodexAgentFiles = { ...(ownership.managedCodexAgentFiles ?? {}) };
333
+
334
+ const envKey = existing.envKey || "OPENAI_API_KEY";
335
+ const currentContent = await pathExists(paths.codexConfigPath) ? await readFile(paths.codexConfigPath, "utf8") : "";
336
+ const templateContent = await loadBundledCodexConfigTemplate(paths);
337
+ const content = buildCodexConfigContent(currentContent, {
338
+ templateContent,
339
+ mergeMissingTemplateDefaults: true,
340
+ includeSubagentDefaults: shouldDeploySubagents,
341
+ providerId,
342
+ providerName,
343
+ baseUrl,
344
+ envKey
345
+ });
346
+
347
+ const auth = mergeCodexAuthData(
348
+ await readJsonFileSafe(paths.codexAuthPath, {}, { sensitive: true }),
349
+ envKey,
350
+ finalApiKey,
351
+ ownership.managedAuthKeys ?? []
352
+ );
353
+ await persistCodexConfiguration(paths, { content, auth });
354
+
355
+ p.log.step(`已更新 ${pathToLabel(paths.codexConfigPath, paths.homeDir)} (${providerId}, ${baseUrl})`);
356
+ p.log.step(`已更新 ${pathToLabel(paths.codexAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
357
+ if (String(baseUrl).startsWith("https://") && !process.env.CODEX_CA_CERTIFICATE && !process.env.SSL_CERT_FILE) {
358
+ p.log.message("自签名证书需在启动 Codex 前设置 CODEX_CA_CERTIFICATE=/absolute/path/relay-ca.pem。");
359
+ }
360
+ if (shouldDeploySubagents) {
361
+ const deployment = await deployBundledCodexAgents(paths, managedCodexAgentFiles);
362
+ managedCodexAgentFiles = deployment.managedFiles;
363
+ p.log.step(`Codex subagents 同步完成:新增 ${deployment.created.length},更新 ${deployment.updated.length},未变 ${deployment.unchanged.length}`);
364
+ if (deployment.conflicts.length) {
365
+ p.log.warn([
366
+ `检测到 ${deployment.conflicts.length} 个 Codex subagent 冲突,已保留原文件:`,
367
+ ...deployment.conflicts.map((path) => `- ${pathToLabel(path, paths.homeDir)}`)
368
+ ].join("\n"));
369
+ }
370
+ } else {
371
+ p.log.message("已跳过 Codex subagents 部署。");
372
+ }
373
+ return {
374
+ managedAuthKeys: finalApiKey ? [envKey] : [],
375
+ managedCodexAgentFiles
376
+ };
377
+ }
378
+
379
+ function buildCodexConfigContent(currentContent, {
380
+ templateContent = "",
381
+ mergeMissingTemplateDefaults = false,
382
+ includeSubagentDefaults = true,
383
+ providerId,
384
+ providerName,
385
+ baseUrl,
386
+ envKey
387
+ }) {
388
+ const effectiveTemplateContent = includeSubagentDefaults
389
+ ? templateContent
390
+ : stripCodexSubagentDefaults(templateContent);
391
+ const hasCurrentContent = Boolean(currentContent.trim());
392
+ let content = hasCurrentContent ? currentContent : effectiveTemplateContent;
393
+ if (hasCurrentContent && mergeMissingTemplateDefaults && effectiveTemplateContent.trim()) {
394
+ content = mergeCodexTemplateDefaults(content, effectiveTemplateContent);
395
+ }
396
+ const lineEnding = detectLineEnding(content);
397
+ if (includeSubagentDefaults && readTopLevelTomlString(content, "approvals_reviewer") === "reviewer") {
398
+ content = updateTopLevelTomlField(content, "approvals_reviewer", "guardian_subagent");
399
+ }
400
+ content = updateTopLevelTomlField(content, "model_provider", providerId);
401
+ content = updateTopLevelTomlField(content, "preferred_auth_method", "apikey");
402
+ content = updateTomlSectionFields(content, getCodexProviderSectionName(providerId), {
403
+ name: providerName,
404
+ base_url: baseUrl,
405
+ wire_api: "responses",
406
+ temp_env_key: envKey,
407
+ requires_openai_auth: true,
408
+ supports_websockets: true
409
+ });
410
+ return `${content.trim()}${lineEnding}`;
411
+ }
412
+
413
+ function mergeCodexAuthData(auth, envKey, apiKey, managedAuthKeys = []) {
414
+ const nextAuth = auth && typeof auth === "object" && !Array.isArray(auth) ? { ...auth } : {};
415
+ assertCodexAuthKeyAvailable(nextAuth, envKey);
416
+ for (const managedAuthKey of managedAuthKeys) {
417
+ delete nextAuth[managedAuthKey];
418
+ }
419
+ if (apiKey) nextAuth[envKey] = apiKey;
420
+ else delete nextAuth[envKey];
421
+ return nextAuth;
422
+ }
423
+
424
+ export {
425
+ buildCodexConfigContent,
426
+ configureCodexApi,
427
+ deployBundledCodexAgents,
428
+ mergeCodexAuthData,
429
+ mergeCodexTemplateDefaults,
430
+ persistCodexConfiguration,
431
+ resolveExistingCodexApiConfig,
432
+ stripCodexSubagentDefaults
433
+ };