abelworkflow 0.9.1 → 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 (128) hide show
  1. package/README.md +55 -165
  2. package/bin/abelworkflow.mjs +5 -3
  3. package/extensions/{pi-gpt-responses-compat.ts → pi-gpt-responses-compat/index.ts} +17 -0
  4. package/extensions/pi-gpt-responses-compat/tls-fetch.mjs +188 -0
  5. package/lib/cli/args.mjs +76 -0
  6. package/lib/cli/main.mjs +387 -0
  7. package/lib/cli/prompts.mjs +119 -0
  8. package/lib/config/dotenv.mjs +102 -0
  9. package/lib/config/jsonc.mjs +80 -0
  10. package/lib/config/store.mjs +310 -0
  11. package/lib/config/toml.mjs +638 -0
  12. package/lib/installer/assets.mjs +326 -0
  13. package/lib/installer/install.mjs +167 -0
  14. package/lib/installer/links.mjs +427 -0
  15. package/lib/installer/render.mjs +46 -0
  16. package/lib/installer/state.mjs +182 -0
  17. package/lib/paths.mjs +99 -0
  18. package/lib/providers/claude.mjs +388 -0
  19. package/lib/providers/codex.mjs +433 -0
  20. package/lib/providers/pi.mjs +362 -0
  21. package/lib/providers/skills.mjs +164 -0
  22. package/lib/templates/codex/agents/default.toml +7 -0
  23. package/lib/templates/codex/agents/explorer.toml +7 -0
  24. package/lib/templates/codex/agents/planner.toml +7 -0
  25. package/lib/templates/codex/agents/reviewer.toml +7 -0
  26. package/lib/templates/codex/agents/worker.toml +7 -0
  27. package/lib/templates/codex/config-base.toml +11 -3
  28. package/{AGENTS.md → lib/templates/workflow/AGENTS.md} +8 -3
  29. package/{commands → lib/templates/workflow/commands}/abel-diagnose.md +5 -5
  30. package/{commands → lib/templates/workflow/commands}/abel-implement.md +3 -8
  31. package/{commands → lib/templates/workflow/commands}/abel-init.md +1 -1
  32. package/{commands → lib/templates/workflow/commands}/abel-plan.md +1 -7
  33. package/{commands → lib/templates/workflow/commands}/abel-research.md +1 -1
  34. package/{.gitignore → lib/templates/workflow/gitignore.template} +0 -2
  35. package/lib/tools/cli-installer.mjs +277 -0
  36. package/package.json +44 -10
  37. package/skills/context7-auto-research/SKILL.md +5 -5
  38. package/skills/context7-auto-research/{context7-api.js → context7-api.cjs} +5 -8
  39. package/skills/dev-browser/SKILL.md +53 -33
  40. package/skills/dev-browser/dist/scripts/start.d.ts +2 -0
  41. package/skills/dev-browser/dist/scripts/start.d.ts.map +1 -0
  42. package/skills/dev-browser/dist/scripts/start.js +118 -0
  43. package/skills/dev-browser/dist/scripts/start.js.map +1 -0
  44. package/skills/dev-browser/dist/src/client.d.ts +93 -0
  45. package/skills/dev-browser/dist/src/client.d.ts.map +1 -0
  46. package/skills/dev-browser/dist/src/client.js +310 -0
  47. package/skills/dev-browser/dist/src/client.js.map +1 -0
  48. package/skills/dev-browser/dist/src/entrypoint.d.ts +29 -0
  49. package/skills/dev-browser/dist/src/entrypoint.d.ts.map +1 -0
  50. package/skills/dev-browser/dist/src/entrypoint.js +106 -0
  51. package/skills/dev-browser/dist/src/entrypoint.js.map +1 -0
  52. package/skills/dev-browser/dist/src/index.d.ts +4 -0
  53. package/skills/dev-browser/dist/src/index.d.ts.map +1 -0
  54. package/skills/dev-browser/dist/src/index.js +2 -0
  55. package/skills/dev-browser/dist/src/index.js.map +1 -0
  56. package/skills/dev-browser/dist/src/page-api.d.ts +25 -0
  57. package/skills/dev-browser/dist/src/page-api.d.ts.map +1 -0
  58. package/skills/dev-browser/dist/src/page-api.js +104 -0
  59. package/skills/dev-browser/dist/src/page-api.js.map +1 -0
  60. package/skills/dev-browser/dist/src/relay.d.ts +27 -0
  61. package/skills/dev-browser/dist/src/relay.d.ts.map +1 -0
  62. package/skills/dev-browser/dist/src/relay.js +521 -0
  63. package/skills/dev-browser/dist/src/relay.js.map +1 -0
  64. package/skills/dev-browser/dist/src/runtime.d.ts +45 -0
  65. package/skills/dev-browser/dist/src/runtime.d.ts.map +1 -0
  66. package/skills/dev-browser/dist/src/runtime.js +54 -0
  67. package/skills/dev-browser/dist/src/runtime.js.map +1 -0
  68. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +23 -0
  69. package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +1 -0
  70. package/skills/dev-browser/{src/snapshot/browser-script.ts → dist/src/snapshot/browser-script.js} +21 -30
  71. package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +1 -0
  72. package/skills/dev-browser/dist/src/snapshot/index.d.ts +14 -0
  73. package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +1 -0
  74. package/skills/dev-browser/{src/snapshot/index.ts → dist/src/snapshot/index.js} +2 -2
  75. package/skills/dev-browser/dist/src/snapshot/index.js.map +1 -0
  76. package/skills/dev-browser/dist/src/snapshot/inject.d.ts +13 -0
  77. package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +1 -0
  78. package/skills/dev-browser/{src/snapshot/inject.ts → dist/src/snapshot/inject.js} +2 -2
  79. package/skills/dev-browser/dist/src/snapshot/inject.js.map +1 -0
  80. package/skills/dev-browser/dist/src/standalone.d.ts +32 -0
  81. package/skills/dev-browser/dist/src/standalone.d.ts.map +1 -0
  82. package/skills/dev-browser/dist/src/standalone.js +174 -0
  83. package/skills/dev-browser/dist/src/standalone.js.map +1 -0
  84. package/skills/dev-browser/dist/src/startup.d.ts +55 -0
  85. package/skills/dev-browser/dist/src/startup.d.ts.map +1 -0
  86. package/skills/dev-browser/dist/src/startup.js +81 -0
  87. package/skills/dev-browser/dist/src/startup.js.map +1 -0
  88. package/skills/dev-browser/dist/src/target-registry.d.ts +29 -0
  89. package/skills/dev-browser/dist/src/target-registry.d.ts.map +1 -0
  90. package/skills/dev-browser/dist/src/target-registry.js +135 -0
  91. package/skills/dev-browser/dist/src/target-registry.js.map +1 -0
  92. package/skills/dev-browser/dist/src/types.d.ts +27 -0
  93. package/skills/dev-browser/dist/src/types.d.ts.map +1 -0
  94. package/skills/dev-browser/dist/src/types.js +2 -0
  95. package/skills/dev-browser/dist/src/types.js.map +1 -0
  96. package/skills/dev-browser/package-lock.json +67 -1510
  97. package/skills/dev-browser/package.json +18 -14
  98. package/skills/dev-browser/references/scraping.md +94 -105
  99. package/skills/grok-search/defaults.json +3 -0
  100. package/skills/grok-search/gitignore.template +4 -0
  101. package/skills/grok-search/scripts/_dotenv.py +5 -1
  102. package/skills/grok-search/scripts/groksearch_cli.py +15 -33
  103. package/skills/prompt-enhancer/TEMPLATE.md +0 -4
  104. package/skills/prompt-enhancer/requirements.txt +0 -1
  105. package/skills/prompt-enhancer/scripts/_dotenv.py +5 -1
  106. package/skills/prompt-enhancer/scripts/enhance.py +4 -51
  107. package/skills/prompt-enhancer/scripts/prompt_enhancer_entry.py +1 -16
  108. package/.skill-lock.json +0 -29
  109. package/lib/cli/logic.mjs +0 -213
  110. package/lib/cli.mjs +0 -2825
  111. package/skills/confidence-check/SKILL.md +0 -34
  112. package/skills/confidence-check/confidence.ts +0 -276
  113. package/skills/dev-browser/bun.lock +0 -460
  114. package/skills/dev-browser/scripts/start.ts +0 -280
  115. package/skills/dev-browser/src/client.ts +0 -474
  116. package/skills/dev-browser/src/entrypoint.ts +0 -157
  117. package/skills/dev-browser/src/index.ts +0 -289
  118. package/skills/dev-browser/src/relay.ts +0 -731
  119. package/skills/dev-browser/src/runtime.test.ts +0 -60
  120. package/skills/dev-browser/src/runtime.ts +0 -171
  121. package/skills/dev-browser/src/startup.test.ts +0 -95
  122. package/skills/dev-browser/src/startup.ts +0 -153
  123. package/skills/dev-browser/src/types.ts +0 -35
  124. package/skills/dev-browser/tsconfig.json +0 -36
  125. package/skills/dev-browser/vitest.config.ts +0 -12
  126. package/skills/sequential-think/SKILL.md +0 -198
  127. package/skills/sequential-think/scripts/.env.example +0 -5
  128. package/skills/sequential-think/scripts/sequential_think_cli.py +0 -253
package/lib/cli.mjs DELETED
@@ -1,2825 +0,0 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
3
- import { homedir } from "node:os";
4
- import { dirname, join, relative, resolve } from "node:path";
5
- import { stdin as input, stdout as output } from "node:process";
6
- import { fileURLToPath } from "node:url";
7
- import * as p from "@clack/prompts";
8
- import c from "picocolors";
9
- import {
10
- assertInteractiveMenuSupported,
11
- assertNotCancelled,
12
- buildCliToolMenuDescriptors,
13
- CancelledError,
14
- confirmOrCancel,
15
- getRunCommandSpawnOptions,
16
- interactiveMenuDefaultValue,
17
- interactiveMenuDescriptors,
18
- parseArgs,
19
- required,
20
- requiredUnlessExisting,
21
- resolvePasswordValue,
22
- selectOrCancel
23
- } from "./cli/logic.mjs";
24
-
25
- const __filename = fileURLToPath(import.meta.url);
26
- const packageRoot = dirname(dirname(__filename));
27
- const home = homedir();
28
- const defaultAgentsDir = join(home, ".agents");
29
- const installMetadataName = ".abelworkflow-install.json";
30
- const claudeSettingsPath = join(home, ".claude", "settings.json");
31
- const claudeMetaConfigPath = join(home, ".claude.json");
32
- const codexConfigPath = join(home, ".codex", "config.toml");
33
- const codexAuthPath = join(home, ".codex", "auth.json");
34
- const codexTemplateRoot = join(packageRoot, "lib", "templates", "codex");
35
- const codexTemplateConfigPath = join(codexTemplateRoot, "config-base.toml");
36
- const codexTemplateAgentsPath = join(codexTemplateRoot, "agents");
37
- const piAgentDir = join(home, ".pi", "agent");
38
- const piModelsPath = join(piAgentDir, "models.json");
39
- const piSettingsPath = join(piAgentDir, "settings.json");
40
- const piProviderId = "gpt";
41
- const piDefaultApi = "openai-responses";
42
- const piDefaultBaseUrl = "https://api.openai.com/v1";
43
- const piDefaultModel = "gpt-5.5";
44
- const installBackupStamp = Date.now();
45
- const createdBackupPaths = new Set();
46
- const augmentContextEnginePermission = "mcp__augment-context-engine";
47
- const augmentContextEngineRetrievalTool = "mcp__augment-context-engine__codebase-retrieval";
48
- const augmentContextEngineFeaturePrompt = {
49
- message: "是否启用 augment-context-engine MCP 代码检索支持?不确定建议选否,可减少 MCP 安装和配置麻烦。",
50
- initialValue: false
51
- };
52
- const localCodebaseRetrievalPolicy = "Use local codebase retrieval with `rg`, `rg --files`, `git grep`, and direct file reads. Do not require augment-context-engine MCP.";
53
- const augmentCodebaseRetrievalPolicy = `Use \`${augmentContextEngineRetrievalTool}\` as the primary codebase search tool.`;
54
- const claudeModelEnvKeys = [
55
- "ANTHROPIC_MODEL",
56
- "ANTHROPIC_DEFAULT_OPUS_MODEL",
57
- "ANTHROPIC_DEFAULT_SONNET_MODEL",
58
- "ANTHROPIC_DEFAULT_HAIKU_MODEL",
59
- "CLAUDE_CODE_SUBAGENT_MODEL"
60
- ];
61
- const defaultClaudeSettings = {
62
- $schema: "https://json.schemastore.org/claude-code-settings.json",
63
- env: {
64
- DISABLE_TELEMETRY: "1",
65
- DISABLE_ERROR_REPORTING: "1",
66
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
67
- ANTHROPIC_BASE_URL: "",
68
- ANTHROPIC_API_KEY: "",
69
- ANTHROPIC_MODEL: "",
70
- ANTHROPIC_DEFAULT_OPUS_MODEL: "",
71
- ANTHROPIC_DEFAULT_SONNET_MODEL: "",
72
- ANTHROPIC_DEFAULT_HAIKU_MODEL: "",
73
- CLAUDE_CODE_SUBAGENT_MODEL: "",
74
- API_TIMEOUT_MS: "1000000"
75
- },
76
- includeCoAuthoredBy: false,
77
- permissions: {
78
- allow: [
79
- "Bash",
80
- "Skill",
81
- "LS",
82
- "Read",
83
- "Agent",
84
- "Write",
85
- "Edit",
86
- "MultiEdit",
87
- "Glob",
88
- "Grep",
89
- "WebFetch",
90
- "WebSearch",
91
- "TodoWrite",
92
- "NotebookRead",
93
- "NotebookEdit"
94
- ],
95
- deny: []
96
- },
97
- hooks: {},
98
- alwaysThinkingEnabled: true,
99
- language: "Chinese"
100
- };
101
- const managedEntries = [
102
- { target: "AGENTS.md" },
103
- { target: "README.md" },
104
- { target: "commands", preserveExisting: true },
105
- { target: "skills", preserveExisting: true, filter: shouldCopySkillPath },
106
- { target: "extensions", preserveExisting: true },
107
- { target: ".skill-lock.json" },
108
- { target: ".gitignore", sourceCandidates: [".gitignore", ".npmignore"] }
109
- ];
110
- const ignoredSkillPathPatterns = [
111
- /(^|\/)\.env$/,
112
- /(^|\/)\.venv(\/|$)/,
113
- /(^|\/)__pycache__(\/|$)/,
114
- /(^|\/)node_modules(\/|$)/,
115
- /(^|\/)tmp(\/|$)/,
116
- /(^|\/)dist(\/|$)/,
117
- /(^|\/)build(\/|$)/,
118
- /^dev-browser\/profiles(\/|$)/,
119
- /^dev-browser\/tmp(\/|$)/
120
- ];
121
-
122
- function buildDefaultClaudeSettings({ augmentContextEngine = false } = {}) {
123
- const settings = {
124
- ...defaultClaudeSettings,
125
- env: { ...defaultClaudeSettings.env },
126
- permissions: {
127
- ...defaultClaudeSettings.permissions,
128
- allow: [...defaultClaudeSettings.permissions.allow],
129
- deny: [...defaultClaudeSettings.permissions.deny]
130
- },
131
- hooks: { ...defaultClaudeSettings.hooks }
132
- };
133
-
134
- if (augmentContextEngine && !settings.permissions.allow.includes(augmentContextEnginePermission)) {
135
- settings.permissions.allow.push(augmentContextEnginePermission);
136
- }
137
-
138
- return settings;
139
- }
140
-
141
- function getAugmentContextEnginePromptOptions() {
142
- return { ...augmentContextEngineFeaturePrompt };
143
- }
144
-
145
- function resolveAugmentContextEngineFeature(options = {}, previousMetadata = {}) {
146
- if (typeof options.augmentContextEngine === "boolean") {
147
- return options.augmentContextEngine;
148
- }
149
-
150
- if (typeof previousMetadata?.features?.augmentContextEngine === "boolean") {
151
- return previousMetadata.features.augmentContextEngine;
152
- }
153
-
154
- return false;
155
- }
156
-
157
- function getWorkflowRenderValues(augmentContextEngine) {
158
- return {
159
- CODEBASE_RETRIEVAL_POLICY: augmentContextEngine
160
- ? augmentCodebaseRetrievalPolicy
161
- : localCodebaseRetrievalPolicy,
162
- AUGMENT_CONTEXT_ENGINE_VALIDATION: augmentContextEngine
163
- ? `Verify MCP availability:\n - \`${augmentContextEngineRetrievalTool}\``
164
- : "Skip augment-context-engine MCP validation; use local retrieval tools.",
165
- CODEBASE_RETRIEVAL_MANDATORY_RULE: augmentContextEngine
166
- ? `Mandatory use of \`${augmentContextEngineRetrievalTool}\``
167
- : "Mandatory use of configured codebase retrieval policy.",
168
- CODEBASE_RETRIEVAL_STRUCTURE_REFERENCE: augmentContextEngine
169
- ? `Inspect codebase structure: \`${augmentContextEngineRetrievalTool}\` with \`file list --recursive\`.`
170
- : "Inspect codebase structure with `rg --files`, `git grep`, and direct file reads.",
171
- CODEBASE_RETRIEVAL_PATTERN_AUDIT: augmentContextEngine
172
- ? `Use augment-context-engine to validate against existing codebase patterns.\n ${augmentContextEngineRetrievalTool}: "Search for existing implementations similar to change <change_name>. Keywords: [key concepts from proposal]"`
173
- : "Use `rg`, `rg --files`, `git grep`, and direct file reads to validate against existing codebase patterns."
174
- };
175
- }
176
-
177
- function renderManagedWorkflowContent(content, { augmentContextEngine = false } = {}) {
178
- let nextContent = content;
179
- for (const [key, value] of Object.entries(getWorkflowRenderValues(augmentContextEngine))) {
180
- nextContent = nextContent.replaceAll(`{{${key}}}`, value);
181
- }
182
- return nextContent;
183
- }
184
-
185
- function printHelp() {
186
- console.log(`${c.bold("AbelWorkflow")} ${c.cyan("installer")}
187
-
188
- ${c.bold("Usage:")}
189
- ${c.cyan("npx abelworkflow")}
190
- ${c.cyan("npx abelworkflow init")}
191
- ${c.cyan("npx abelworkflow install")}
192
- ${c.cyan("npx abelworkflow install --force")}
193
- ${c.cyan("npx abelworkflow install --link-only")}
194
- ${c.cyan("npx abelworkflow install --agents-dir /custom/path")}
195
- ${c.cyan("npx abelworkflow --non-interactive")}
196
-
197
- ${c.bold("Default behavior:")}
198
- - npx abelworkflow: open the interactive setup menu.
199
- - npx abelworkflow install: sync managed files and links explicitly.
200
- - --non-interactive: auto-execute install (skip interactive menu); auto-enabled in CI.
201
- `);
202
- }
203
-
204
- function pathToLabel(path) {
205
- return path.replace(home, "~");
206
- }
207
-
208
- function maskSecret(value) {
209
- if (!value) {
210
- return "未配置";
211
- }
212
- if (value.length <= 4) {
213
- return "*".repeat(value.length);
214
- }
215
- return `${"*".repeat(Math.max(4, value.length - 4))}${value.slice(-4)}`;
216
- }
217
-
218
- function escapeRegExp(value) {
219
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
220
- }
221
-
222
- function detectLineEnding(content) {
223
- return content.includes("\r\n") ? "\r\n" : "\n";
224
- }
225
-
226
- function collapseBlankLines(content, lineEnding) {
227
- return content.replace(/(?:\r?\n){3,}/gu, `${lineEnding}${lineEnding}`);
228
- }
229
-
230
- async function pathExists(path) {
231
- try {
232
- await lstat(path);
233
- return true;
234
- } catch {
235
- return false;
236
- }
237
- }
238
-
239
- async function pathTargetExists(path) {
240
- try {
241
- await stat(path);
242
- return true;
243
- } catch {
244
- return false;
245
- }
246
- }
247
-
248
- async function createBackupPath(targetPath) {
249
- let index = 0;
250
- while (true) {
251
- const backupPath = `${targetPath}.bak.${installBackupStamp}${index ? `-${index}` : ""}`;
252
- if (!(await pathExists(backupPath))) {
253
- return backupPath;
254
- }
255
- index += 1;
256
- }
257
- }
258
-
259
- async function backupExistingPath(targetPath) {
260
- if (createdBackupPaths.has(targetPath) || !(await pathExists(targetPath))) {
261
- return null;
262
- }
263
-
264
- const backupPath = await createBackupPath(targetPath);
265
- await cp(targetPath, backupPath, { recursive: true, force: false });
266
- createdBackupPaths.add(targetPath);
267
- p.log.message(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
268
- return backupPath;
269
- }
270
-
271
- async function backupIfNeeded(targetPath) {
272
- if (!(await pathExists(targetPath))) {
273
- return null;
274
- }
275
-
276
- const backupPath = await createBackupPath(targetPath);
277
- await rename(targetPath, backupPath);
278
- p.log.message(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
279
- return backupPath;
280
- }
281
-
282
- async function syncManagedFiles(agentsDir) {
283
- await mkdir(agentsDir, { recursive: true });
284
-
285
- const previousMetadata = await readInstallMetadata(agentsDir);
286
- const managedChildren = {};
287
-
288
- for (const entry of managedEntries) {
289
- const source = await resolveManagedEntrySource(entry);
290
- const target = join(agentsDir, entry.target);
291
- if (entry.preserveExisting) {
292
- managedChildren[entry.target] = await syncPreservedManagedEntry(
293
- source,
294
- target,
295
- entry,
296
- previousMetadata.managedChildren?.[entry.target] ?? []
297
- );
298
- } else {
299
- await replaceManagedEntry(source, target, entry);
300
- }
301
- }
302
-
303
- return { previousMetadata, managedChildren };
304
- }
305
-
306
- async function resolveManagedEntrySource(entry) {
307
- for (const candidate of entry.sourceCandidates ?? [entry.target]) {
308
- const source = join(packageRoot, candidate);
309
- if (await pathExists(source)) {
310
- return source;
311
- }
312
- }
313
-
314
- const expected = (entry.sourceCandidates ?? [entry.target]).join(", ");
315
- throw new Error(`Missing managed entry in package: ${expected}`);
316
- }
317
-
318
- async function removeIfNotDirectory(path) {
319
- if (!(await pathExists(path))) {
320
- return;
321
- }
322
-
323
- const entryStat = await lstat(path);
324
- if (entryStat.isDirectory()) {
325
- return;
326
- }
327
-
328
- try {
329
- if (entryStat.isSymbolicLink() && (await stat(path)).isDirectory()) {
330
- return;
331
- }
332
- } catch {
333
- }
334
-
335
- await rm(path, { recursive: true, force: true });
336
- }
337
-
338
- async function replaceManagedEntry(source, target, entry) {
339
- if (await pathsReferToSameEntry(source, target)) {
340
- return;
341
- }
342
-
343
- const sourceStat = await lstat(source);
344
- if (sourceStat.isDirectory()) {
345
- await backupExistingPath(target);
346
- await rm(target, { recursive: true, force: true });
347
- } else if (await pathExists(target)) {
348
- await backupExistingPath(target);
349
- const targetStat = await lstat(target);
350
- if (targetStat.isDirectory()) {
351
- await rm(target, { recursive: true, force: true });
352
- }
353
- }
354
-
355
- await cp(source, target, {
356
- recursive: true,
357
- force: true,
358
- filter: entry.filter ? (sourcePath) => entry.filter(source, sourcePath) : undefined
359
- });
360
- }
361
-
362
- async function pathsReferToSameEntry(sourcePath, targetPath) {
363
- if (resolve(sourcePath) === resolve(targetPath)) {
364
- return true;
365
- }
366
-
367
- try {
368
- const [sourceRealPath, targetRealPath] = await Promise.all([realpath(sourcePath), realpath(targetPath)]);
369
- return sourceRealPath === targetRealPath;
370
- } catch {
371
- return false;
372
- }
373
- }
374
-
375
- function shouldCopySkillPath(skillsRoot, sourcePath) {
376
- const relativePath = relative(skillsRoot, sourcePath);
377
- if (!relativePath) {
378
- return true;
379
- }
380
-
381
- const normalizedPath = relativePath.replaceAll("\\", "/");
382
- return !ignoredSkillPathPatterns.some((pattern) => pattern.test(normalizedPath));
383
- }
384
-
385
- async function readInstallMetadata(agentsDir) {
386
- const metadataPath = join(agentsDir, installMetadataName);
387
- if (!(await pathExists(metadataPath))) {
388
- return {};
389
- }
390
-
391
- try {
392
- return JSON.parse(await readFile(metadataPath, "utf8"));
393
- } catch {
394
- return {};
395
- }
396
- }
397
-
398
- async function writeInstallMetadata(agentsDir, metadata) {
399
- await writeFile(join(agentsDir, installMetadataName), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
400
- }
401
-
402
- function linkedTargetsFromResults(results) {
403
- return Object.fromEntries(
404
- results
405
- .filter((result) => result.sourcePath)
406
- .map((result) => [
407
- result.targetPath,
408
- {
409
- sourcePath: result.sourcePath,
410
- kind: result.kind,
411
- mode: result.mode
412
- }
413
- ])
414
- );
415
- }
416
-
417
- function mergeLinkedTargets(previousLinkedTargets = {}, results = []) {
418
- const nextLinkedTargets = { ...previousLinkedTargets };
419
- for (const result of results) {
420
- if (result.sourcePath) {
421
- nextLinkedTargets[result.targetPath] = {
422
- sourcePath: result.sourcePath,
423
- kind: result.kind,
424
- mode: result.mode
425
- };
426
- continue;
427
- }
428
- if (result.status === "removed") {
429
- delete nextLinkedTargets[result.targetPath];
430
- }
431
- }
432
- return nextLinkedTargets;
433
- }
434
-
435
- async function renderManagedWorkflowFile(path, augmentContextEngine) {
436
- if (!(await pathIsFile(path))) {
437
- return;
438
- }
439
-
440
- const content = await readFile(path, "utf8");
441
- const rendered = renderManagedWorkflowContent(content, { augmentContextEngine });
442
- if (rendered !== content) {
443
- await writeFile(path, rendered, "utf8");
444
- }
445
- }
446
-
447
- async function renderManagedWorkflowFiles(agentsDir, augmentContextEngine) {
448
- await renderManagedWorkflowFile(join(agentsDir, "AGENTS.md"), augmentContextEngine);
449
-
450
- const commandsDir = join(agentsDir, "commands");
451
- const commandFiles = await getCommandNames(commandsDir);
452
- for (const fileName of commandFiles) {
453
- await renderManagedWorkflowFile(join(commandsDir, fileName), augmentContextEngine);
454
- }
455
- }
456
-
457
- async function syncPreservedManagedEntry(sourceRoot, targetRoot, entry, previousManagedChildren) {
458
- await removeIfNotDirectory(targetRoot);
459
- await mkdir(targetRoot, { recursive: true });
460
-
461
- const previousManagedChildSet = new Set(previousManagedChildren);
462
- const sourceChildren = await getManagedChildNames(sourceRoot, entry.filter);
463
- const sourceChildSet = new Set(sourceChildren);
464
- const currentManagedChildren = [];
465
-
466
- for (const childName of previousManagedChildren) {
467
- if (!sourceChildSet.has(childName)) {
468
- await rm(join(targetRoot, childName), { recursive: true, force: true });
469
- }
470
- }
471
-
472
- for (const childName of sourceChildren) {
473
- if (!(await shouldSyncManagedChild(join(targetRoot, childName), previousManagedChildSet.has(childName)))) {
474
- continue;
475
- }
476
-
477
- await syncManagedSubtree(
478
- join(sourceRoot, childName),
479
- join(targetRoot, childName),
480
- sourceRoot,
481
- entry.filter
482
- );
483
- currentManagedChildren.push(childName);
484
- }
485
-
486
- return currentManagedChildren;
487
- }
488
-
489
- async function shouldSyncManagedChild(targetPath, wasPreviouslyManaged) {
490
- if (wasPreviouslyManaged) {
491
- return true;
492
- }
493
-
494
- return !(await pathTargetExists(targetPath));
495
- }
496
-
497
- async function getManagedChildNames(sourceRoot, filter) {
498
- const entries = await readdir(sourceRoot, { withFileTypes: true });
499
- return entries
500
- .filter((entry) => !filter || filter(sourceRoot, join(sourceRoot, entry.name)))
501
- .map((entry) => entry.name);
502
- }
503
-
504
- async function syncManagedSubtree(sourcePath, targetPath, managedRoot, filter) {
505
- if (await pathsReferToSameEntry(sourcePath, targetPath)) {
506
- return;
507
- }
508
-
509
- const sourceStat = await lstat(sourcePath);
510
- if (!sourceStat.isDirectory()) {
511
- if (await pathExists(targetPath)) {
512
- const targetStat = await lstat(targetPath);
513
- if (targetStat.isDirectory()) {
514
- await rm(targetPath, { recursive: true, force: true });
515
- }
516
- }
517
-
518
- await cp(sourcePath, targetPath, { recursive: true, force: true });
519
- return;
520
- }
521
-
522
- await removeIfNotDirectory(targetPath);
523
- await mkdir(targetPath, { recursive: true });
524
- await pruneMissingManagedPaths(sourcePath, targetPath, managedRoot, filter);
525
- await cp(sourcePath, targetPath, {
526
- recursive: true,
527
- force: true,
528
- filter: filter ? (candidatePath) => filter(managedRoot, candidatePath) : undefined
529
- });
530
- }
531
-
532
- async function pruneMissingManagedPaths(sourcePath, targetPath, managedRoot, filter) {
533
- if (!(await pathExists(targetPath))) {
534
- return;
535
- }
536
-
537
- const entries = await readdir(targetPath, { withFileTypes: true });
538
- for (const entry of entries) {
539
- const targetEntryPath = join(targetPath, entry.name);
540
- const sourceEntryPath = join(sourcePath, entry.name);
541
- if (filter && !filter(managedRoot, sourceEntryPath)) {
542
- continue;
543
- }
544
-
545
- if (!(await pathExists(sourceEntryPath))) {
546
- await rm(targetEntryPath, { recursive: true, force: true });
547
- continue;
548
- }
549
-
550
- const sourceEntryStat = await lstat(sourceEntryPath);
551
- if (entry.isDirectory()) {
552
- if (!sourceEntryStat.isDirectory()) {
553
- await rm(targetEntryPath, { recursive: true, force: true });
554
- continue;
555
- }
556
-
557
- await pruneMissingManagedPaths(sourceEntryPath, targetEntryPath, managedRoot, filter);
558
- continue;
559
- }
560
-
561
- if (sourceEntryStat.isDirectory()) {
562
- await rm(targetEntryPath, { recursive: true, force: true });
563
- }
564
- }
565
- }
566
-
567
- function getPlatform() {
568
- return process.env.ABELWORKFLOW_TEST_PLATFORM || process.platform;
569
- }
570
-
571
- function isWindows() {
572
- return getPlatform() === "win32";
573
- }
574
-
575
- function shouldForceFileSymlinkFailure(kind) {
576
- return process.env.ABELWORKFLOW_TEST_FORCE_FILE_SYMLINK_EPERM === "1" && isWindows() && kind === "file";
577
- }
578
-
579
- function createManagedTargetState(targetPath, sourcePath, kind, mode, status) {
580
- return { targetPath, sourcePath, kind, mode, status };
581
- }
582
-
583
- async function createSymlink(targetPath, sourcePath, linkType, kind) {
584
- if (shouldForceFileSymlinkFailure(kind)) {
585
- const error = new Error("simulated EPERM");
586
- error.code = "EPERM";
587
- throw error;
588
- }
589
-
590
- await symlink(sourcePath, targetPath, linkType);
591
- }
592
-
593
- async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTargets) {
594
- await mkdir(dirname(targetPath), { recursive: true });
595
- const sourceResolved = resolve(sourcePath);
596
- const sourceExists = await pathTargetExists(sourcePath);
597
-
598
- if (await pathExists(targetPath)) {
599
- const targetStat = await lstat(targetPath);
600
- if (targetStat.isSymbolicLink()) {
601
- const existing = await readlink(targetPath);
602
- const existingResolved = resolve(dirname(targetPath), existing);
603
- if (existingResolved === sourceResolved) {
604
- if (!sourceExists) {
605
- await rm(targetPath, { recursive: true, force: true });
606
- return { targetPath, status: "removed" };
607
- }
608
-
609
- return createManagedTargetState(targetPath, sourcePath, kind, "symlink", "unchanged");
610
- }
611
- }
612
-
613
- const previousState = previousLinkedTargets[targetPath];
614
- const wasPreviouslyManaged =
615
- previousState &&
616
- resolve(previousState.sourcePath) === sourceResolved &&
617
- previousState.kind === kind;
618
-
619
- if (!sourceExists) {
620
- if (wasPreviouslyManaged) {
621
- await rm(targetPath, { recursive: true, force: true });
622
- return { targetPath, status: "removed" };
623
- }
624
-
625
- return { targetPath, status: "skipped" };
626
- }
627
-
628
- if (wasPreviouslyManaged) {
629
- await rm(targetPath, { recursive: true, force: true });
630
- } else {
631
- await backupIfNeeded(targetPath);
632
- }
633
- } else if (!sourceExists) {
634
- return { targetPath, status: "skipped" };
635
- }
636
-
637
- const linkType = isWindows() ? (kind === "dir" ? "junction" : "file") : kind;
638
-
639
- try {
640
- await createSymlink(targetPath, sourcePath, linkType, kind);
641
- return createManagedTargetState(targetPath, sourcePath, kind, "symlink", "linked");
642
- } catch (error) {
643
- if (!shouldFallbackToManagedFile(error, kind)) {
644
- throw error;
645
- }
646
- }
647
-
648
- try {
649
- await link(sourcePath, targetPath);
650
- return createManagedTargetState(targetPath, sourcePath, kind, "hardlink", "linked");
651
- } catch (error) {
652
- if (!shouldCopyManagedFile(error)) {
653
- throw error;
654
- }
655
- }
656
-
657
- await cp(sourcePath, targetPath, { recursive: true, force: true });
658
- return createManagedTargetState(targetPath, sourcePath, kind, "copy", "copied");
659
- }
660
-
661
- function shouldFallbackToManagedFile(error, kind) {
662
- return kind === "file" && isWindows() && ["EPERM", "EACCES"].includes(error?.code);
663
- }
664
-
665
- function shouldCopyManagedFile(error) {
666
- return ["EPERM", "EACCES", "EXDEV", "EINVAL", "UNKNOWN"].includes(error?.code);
667
- }
668
-
669
- async function linkSkillDirectories(baseDir, agentsDir, previousLinkedTargets) {
670
- const results = [];
671
- const skillsRoot = join(agentsDir, "skills");
672
- const skillNames = (await getDirectoryNames(skillsRoot)).filter((skillName) => skillName !== ".system");
673
- results.push(...(await pruneManagedTargets(join(baseDir, "skills"), skillsRoot, skillNames, previousLinkedTargets)));
674
- for (const skillName of skillNames) {
675
- results.push(
676
- await ensureManagedLink(
677
- join(baseDir, "skills", skillName),
678
- join(skillsRoot, skillName),
679
- "dir",
680
- previousLinkedTargets
681
- )
682
- );
683
- }
684
- return results;
685
- }
686
-
687
- async function getDirectoryNames(root) {
688
- if (!(await pathIsDirectory(root))) {
689
- return [];
690
- }
691
-
692
- const entries = await readdir(root, { withFileTypes: true });
693
- const names = await Promise.all(
694
- entries.map(async (entry) => ((await isDirectoryEntry(root, entry)) ? entry.name : null))
695
- );
696
- return names.filter(Boolean);
697
- }
698
-
699
- async function getCommandNames(commandsDir) {
700
- if (!(await pathIsDirectory(commandsDir))) {
701
- return [];
702
- }
703
-
704
- const entries = await readdir(commandsDir, { withFileTypes: true });
705
- const names = await Promise.all(
706
- entries.map(async (entry) => ((await isMarkdownFileEntry(commandsDir, entry)) ? entry.name : null))
707
- );
708
- return names.filter(Boolean);
709
- }
710
-
711
- async function getPiExtensionNames(extensionsDir) {
712
- if (!(await pathIsDirectory(extensionsDir))) {
713
- return [];
714
- }
715
-
716
- const entries = await readdir(extensionsDir, { withFileTypes: true });
717
- const names = await Promise.all(
718
- entries.map(async (entry) => ((await isPiExtensionEntry(extensionsDir, entry)) ? entry.name : null))
719
- );
720
- return names.filter(Boolean);
721
- }
722
-
723
- async function pathIsDirectory(path) {
724
- try {
725
- return (await stat(path)).isDirectory();
726
- } catch {
727
- return false;
728
- }
729
- }
730
-
731
- async function pathIsFile(path) {
732
- try {
733
- return (await stat(path)).isFile();
734
- } catch {
735
- return false;
736
- }
737
- }
738
-
739
- async function isDirectoryEntry(root, entry) {
740
- if (entry.isDirectory()) {
741
- return true;
742
- }
743
-
744
- if (!entry.isSymbolicLink()) {
745
- return false;
746
- }
747
-
748
- return pathIsDirectory(join(root, entry.name));
749
- }
750
-
751
- async function isMarkdownFileEntry(root, entry) {
752
- if (!entry.name.endsWith(".md")) {
753
- return false;
754
- }
755
-
756
- if (entry.isFile()) {
757
- return true;
758
- }
759
-
760
- if (!entry.isSymbolicLink()) {
761
- return false;
762
- }
763
-
764
- return pathIsFile(join(root, entry.name));
765
- }
766
-
767
- async function isPiExtensionEntry(root, entry) {
768
- const entryPath = join(root, entry.name);
769
- if (entry.isDirectory()) {
770
- return pathIsFile(join(entryPath, "index.ts"));
771
- }
772
- if (entry.isFile()) {
773
- return entry.name.endsWith(".ts");
774
- }
775
- if (!entry.isSymbolicLink()) {
776
- return false;
777
- }
778
- if (entry.name.endsWith(".ts") && await pathIsFile(entryPath)) {
779
- return true;
780
- }
781
- return pathIsFile(join(entryPath, "index.ts"));
782
- }
783
-
784
- async function pruneManagedTargets(targetDir, managedSourceRoot, expectedNames, previousLinkedTargets) {
785
- if (!(await pathExists(targetDir))) {
786
- return [];
787
- }
788
-
789
- const expectedNameSet = new Set(expectedNames);
790
- const results = [];
791
- const entries = await readdir(targetDir, { withFileTypes: true });
792
-
793
- for (const entry of entries) {
794
- const targetPath = join(targetDir, entry.name);
795
- if (entry.isSymbolicLink()) {
796
- const existing = await readlink(targetPath);
797
- const existingResolved = resolve(dirname(targetPath), existing);
798
- if (!isWithinManagedRoot(existingResolved, managedSourceRoot)) {
799
- continue;
800
- }
801
-
802
- if (expectedNameSet.has(entry.name) && (await pathTargetExists(existingResolved))) {
803
- continue;
804
- }
805
-
806
- await rm(targetPath, { recursive: true, force: true });
807
- results.push({ targetPath, status: "removed" });
808
- continue;
809
- }
810
-
811
- const previousState = previousLinkedTargets[targetPath];
812
- if (!previousState || !isWithinManagedRoot(resolve(previousState.sourcePath), managedSourceRoot)) {
813
- continue;
814
- }
815
-
816
- if (expectedNameSet.has(entry.name) && (await pathTargetExists(previousState.sourcePath))) {
817
- continue;
818
- }
819
-
820
- await rm(targetPath, { recursive: true, force: true });
821
- results.push({ targetPath, status: "removed" });
822
- }
823
-
824
- return results;
825
- }
826
-
827
- function isWithinManagedRoot(targetPath, managedSourceRoot) {
828
- const relativePath = relative(managedSourceRoot, targetPath);
829
- if (!relativePath) {
830
- return false;
831
- }
832
-
833
- return relativePath !== ".." && !relativePath.startsWith(`..${isWindows() ? "\\" : "/"}`);
834
- }
835
-
836
- async function linkClaude(agentsDir, previousLinkedTargets) {
837
- const claudeDir = join(home, ".claude");
838
- await mkdir(claudeDir, { recursive: true });
839
- await removeIfNotDirectory(join(claudeDir, "commands"));
840
- await removeIfNotDirectory(join(claudeDir, "skills"));
841
- await mkdir(join(claudeDir, "commands"), { recursive: true });
842
- await mkdir(join(claudeDir, "skills"), { recursive: true });
843
-
844
- return [
845
- await ensureManagedLink(
846
- join(claudeDir, "CLAUDE.md"),
847
- join(agentsDir, "AGENTS.md"),
848
- "file",
849
- previousLinkedTargets
850
- ),
851
- ...(await (async () => {
852
- const commandFiles = await getCommandNames(join(agentsDir, "commands"));
853
- const r = [];
854
- r.push(...(await pruneManagedTargets(
855
- join(claudeDir, "commands"),
856
- join(agentsDir, "commands"),
857
- commandFiles,
858
- previousLinkedTargets
859
- )));
860
- for (const fileName of commandFiles) {
861
- r.push(
862
- await ensureManagedLink(
863
- join(claudeDir, "commands", fileName),
864
- join(agentsDir, "commands", fileName),
865
- "file",
866
- previousLinkedTargets
867
- )
868
- );
869
- }
870
- return r;
871
- })()),
872
- ...(await linkSkillDirectories(claudeDir, agentsDir, previousLinkedTargets))
873
- ];
874
- }
875
-
876
- async function linkCodex(agentsDir, previousLinkedTargets) {
877
- const results = [];
878
- const codexDir = join(home, ".codex");
879
- await mkdir(codexDir, { recursive: true });
880
- await removeIfNotDirectory(join(codexDir, "skills"));
881
- await removeIfNotDirectory(join(codexDir, "prompts"));
882
- await mkdir(join(codexDir, "skills"), { recursive: true });
883
- await mkdir(join(codexDir, "prompts"), { recursive: true });
884
-
885
- results.push(
886
- await ensureManagedLink(
887
- join(codexDir, "AGENTS.md"),
888
- join(agentsDir, "AGENTS.md"),
889
- "file",
890
- previousLinkedTargets
891
- )
892
- );
893
- results.push(...(await linkSkillDirectories(codexDir, agentsDir, previousLinkedTargets)));
894
-
895
- const commandFiles = await getCommandNames(join(agentsDir, "commands"));
896
- results.push(
897
- ...(await pruneManagedTargets(
898
- join(codexDir, "prompts"),
899
- join(agentsDir, "commands"),
900
- commandFiles,
901
- previousLinkedTargets
902
- ))
903
- );
904
- for (const fileName of commandFiles) {
905
- results.push(
906
- await ensureManagedLink(
907
- join(codexDir, "prompts", fileName),
908
- join(agentsDir, "commands", fileName),
909
- "file",
910
- previousLinkedTargets
911
- )
912
- );
913
- }
914
-
915
- return results;
916
- }
917
-
918
- async function linkPi(agentsDir, previousLinkedTargets) {
919
- const results = [];
920
- await mkdir(piAgentDir, { recursive: true });
921
- await removeIfNotDirectory(join(piAgentDir, "skills"));
922
- await removeIfNotDirectory(join(piAgentDir, "prompts"));
923
- await removeIfNotDirectory(join(piAgentDir, "extensions"));
924
- await mkdir(join(piAgentDir, "skills"), { recursive: true });
925
- await mkdir(join(piAgentDir, "prompts"), { recursive: true });
926
- await mkdir(join(piAgentDir, "extensions"), { recursive: true });
927
-
928
- results.push(
929
- await ensureManagedLink(
930
- join(piAgentDir, "AGENTS.md"),
931
- join(agentsDir, "AGENTS.md"),
932
- "file",
933
- previousLinkedTargets
934
- )
935
- );
936
- results.push(...(await linkSkillDirectories(piAgentDir, agentsDir, previousLinkedTargets)));
937
-
938
- const commandFiles = await getCommandNames(join(agentsDir, "commands"));
939
- results.push(
940
- ...(await pruneManagedTargets(
941
- join(piAgentDir, "prompts"),
942
- join(agentsDir, "commands"),
943
- commandFiles,
944
- previousLinkedTargets
945
- ))
946
- );
947
- for (const fileName of commandFiles) {
948
- results.push(
949
- await ensureManagedLink(
950
- join(piAgentDir, "prompts", fileName),
951
- join(agentsDir, "commands", fileName),
952
- "file",
953
- previousLinkedTargets
954
- )
955
- );
956
- }
957
-
958
- const extensionNames = await getPiExtensionNames(join(agentsDir, "extensions"));
959
- results.push(
960
- ...(await pruneManagedTargets(
961
- join(piAgentDir, "extensions"),
962
- join(agentsDir, "extensions"),
963
- extensionNames,
964
- previousLinkedTargets
965
- ))
966
- );
967
- for (const entryName of extensionNames) {
968
- const sourcePath = join(agentsDir, "extensions", entryName);
969
- const kind = await pathIsDirectory(sourcePath) ? "dir" : "file";
970
- results.push(
971
- await ensureManagedLink(
972
- join(piAgentDir, "extensions", entryName),
973
- sourcePath,
974
- kind,
975
- previousLinkedTargets
976
- )
977
- );
978
- }
979
-
980
- return results;
981
- }
982
-
983
- async function installManagedWorkflow(options) {
984
- let previousMetadata = {};
985
- let managedChildren = {};
986
- let augmentContextEngine = false;
987
- const s = p.spinner();
988
-
989
- if (!options.relinkOnly) {
990
- s.start("正在同步工作流文件...");
991
- try {
992
- ({ previousMetadata, managedChildren } = await syncManagedFiles(options.agentsDir));
993
- augmentContextEngine = resolveAugmentContextEngineFeature(options, previousMetadata);
994
- await renderManagedWorkflowFiles(options.agentsDir, augmentContextEngine);
995
- } catch (e) {
996
- s.cancel(c.red(`同步失败: ${e.message}`));
997
- throw e;
998
- }
999
- s.stop("工作流文件已同步");
1000
- } else if (!(await pathExists(options.agentsDir))) {
1001
- throw new Error(`${options.agentsDir} does not exist; remove --link-only or install first`);
1002
- } else {
1003
- previousMetadata = await readInstallMetadata(options.agentsDir);
1004
- managedChildren = previousMetadata.managedChildren ?? {};
1005
- augmentContextEngine = resolveAugmentContextEngineFeature(options, previousMetadata);
1006
- }
1007
-
1008
- const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
1009
- s.start("正在链接 Claude / Codex / Pi...");
1010
- let claudeResults;
1011
- let codexResults;
1012
- let piResults;
1013
- try {
1014
- claudeResults = await linkClaude(options.agentsDir, previousLinkedTargets);
1015
- codexResults = await linkCodex(options.agentsDir, previousLinkedTargets);
1016
- piResults = await linkPi(options.agentsDir, previousLinkedTargets);
1017
- } catch (e) {
1018
- s.cancel(c.red(`链接失败: ${e.message}`));
1019
- throw e;
1020
- }
1021
- s.stop("链接完成");
1022
-
1023
- const linkedTargets = linkedTargetsFromResults([...claudeResults, ...codexResults, ...piResults]);
1024
-
1025
- const managedClaudePermissions = await ensureClaudeSettingsForFeature(augmentContextEngine, previousMetadata);
1026
-
1027
- await writeInstallMetadata(options.agentsDir, {
1028
- package: "abelworkflow",
1029
- installedAt: new Date().toISOString(),
1030
- features: {
1031
- ...(previousMetadata.features && typeof previousMetadata.features === "object" ? previousMetadata.features : {}),
1032
- augmentContextEngine
1033
- },
1034
- managedChildren,
1035
- managedClaudePermissions,
1036
- linkedTargets
1037
- });
1038
-
1039
- const resultLines = [...claudeResults, ...codexResults, ...piResults].map((result) => {
1040
- const icon = result.status === "unchanged" ? c.gray("=")
1041
- : result.status === "removed" ? c.yellow("−")
1042
- : c.green("+");
1043
- return `${icon} ${pathToLabel(result.targetPath)}`;
1044
- }).join("\n");
1045
-
1046
- p.note(resultLines, "链接结果");
1047
- p.log.step(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
1048
- p.log.message(`完成后可运行 ${c.cyan("npx abelworkflow@latest")} 更新托管文件`);
1049
- }
1050
-
1051
- async function readJsonFileSafe(path, fallback = {}) {
1052
- if (!(await pathExists(path))) {
1053
- return fallback;
1054
- }
1055
-
1056
- try {
1057
- return JSON.parse(await readFile(path, "utf8"));
1058
- } catch {
1059
- return fallback;
1060
- }
1061
- }
1062
-
1063
- async function writeJsonFileSafe(path, data) {
1064
- await mkdir(dirname(path), { recursive: true });
1065
- await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
1066
- }
1067
-
1068
- async function writeJsonFileWithBackup(path, data) {
1069
- await backupExistingPath(path);
1070
- await writeJsonFileSafe(path, data);
1071
- }
1072
-
1073
- function stripJsonComments(content) {
1074
- let output = "";
1075
- let inString = false;
1076
- let quote = "";
1077
- let escaped = false;
1078
- for (let i = 0; i < content.length; i += 1) {
1079
- const char = content[i];
1080
- const next = content[i + 1];
1081
-
1082
- if (inString) {
1083
- output += char;
1084
- if (escaped) {
1085
- escaped = false;
1086
- } else if (char === "\\") {
1087
- escaped = true;
1088
- } else if (char === quote) {
1089
- inString = false;
1090
- quote = "";
1091
- }
1092
- continue;
1093
- }
1094
-
1095
- if (char === "\"" || char === "'") {
1096
- inString = true;
1097
- quote = char;
1098
- output += char;
1099
- continue;
1100
- }
1101
-
1102
- if (char === "/" && next === "/") {
1103
- while (i < content.length && content[i] !== "\n") {
1104
- i += 1;
1105
- }
1106
- if (i < content.length) {
1107
- output += content[i];
1108
- }
1109
- continue;
1110
- }
1111
-
1112
- if (char === "/" && next === "*") {
1113
- i += 2;
1114
- while (i < content.length && !(content[i] === "*" && content[i + 1] === "/")) {
1115
- output += content[i] === "\n" ? "\n" : "";
1116
- i += 1;
1117
- }
1118
- i += 1;
1119
- continue;
1120
- }
1121
-
1122
- output += char;
1123
- }
1124
- return stripJsonTrailingCommas(output);
1125
- }
1126
-
1127
- function stripJsonTrailingCommas(content) {
1128
- let output = "";
1129
- let inString = false;
1130
- let quote = "";
1131
- let escaped = false;
1132
- for (let i = 0; i < content.length; i += 1) {
1133
- const char = content[i];
1134
-
1135
- if (inString) {
1136
- output += char;
1137
- if (escaped) {
1138
- escaped = false;
1139
- } else if (char === "\\") {
1140
- escaped = true;
1141
- } else if (char === quote) {
1142
- inString = false;
1143
- quote = "";
1144
- }
1145
- continue;
1146
- }
1147
-
1148
- if (char === "\"" || char === "'") {
1149
- inString = true;
1150
- quote = char;
1151
- output += char;
1152
- continue;
1153
- }
1154
-
1155
- if (char === ",") {
1156
- let nextIndex = i + 1;
1157
- while (nextIndex < content.length && /\s/u.test(content[nextIndex])) {
1158
- nextIndex += 1;
1159
- }
1160
- if (content[nextIndex] === "}" || content[nextIndex] === "]") {
1161
- continue;
1162
- }
1163
- }
1164
-
1165
- output += char;
1166
- }
1167
- return output;
1168
- }
1169
-
1170
- async function readJsoncFileSafe(path, fallback = {}) {
1171
- if (!(await pathExists(path))) {
1172
- return fallback;
1173
- }
1174
-
1175
- try {
1176
- return JSON.parse(stripJsonComments(await readFile(path, "utf8")));
1177
- } catch {
1178
- return fallback;
1179
- }
1180
- }
1181
-
1182
- function parsePiModelIds(value) {
1183
- return [...new Set(String(value || "")
1184
- .split(/[\n,]+/u)
1185
- .map((item) => item.trim())
1186
- .filter(Boolean))];
1187
- }
1188
-
1189
- function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}) {
1190
- const provider = modelsConfig.providers?.[piProviderId] && typeof modelsConfig.providers[piProviderId] === "object"
1191
- ? modelsConfig.providers[piProviderId]
1192
- : {};
1193
- const models = Array.isArray(provider.models) ? provider.models.filter((model) => model?.id) : [];
1194
- return {
1195
- baseUrl: provider.baseUrl || piDefaultBaseUrl,
1196
- api: provider.api || piDefaultApi,
1197
- apiKey: provider.apiKey || "",
1198
- modelIds: models.map((model) => model.id),
1199
- defaultModel: settings.defaultProvider === piProviderId && settings.defaultModel
1200
- ? settings.defaultModel
1201
- : models[0]?.id || piDefaultModel
1202
- };
1203
- }
1204
-
1205
- function buildPiModelConfig(modelId, existingModel = {}) {
1206
- return {
1207
- ...existingModel,
1208
- id: modelId,
1209
- name: existingModel.name || modelId,
1210
- reasoning: existingModel.reasoning ?? true,
1211
- input: Array.isArray(existingModel.input) ? existingModel.input : ["text", "image"],
1212
- contextWindow: existingModel.contextWindow ?? 262144,
1213
- maxTokens: existingModel.maxTokens ?? 64000
1214
- };
1215
- }
1216
-
1217
- function buildPiModelsConfig(modelsConfig = {}, { baseUrl, api, apiKey, modelIds }) {
1218
- const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
1219
- const currentProvider = providers[piProviderId] && typeof providers[piProviderId] === "object" ? providers[piProviderId] : {};
1220
- const existingModels = new Map(
1221
- (Array.isArray(currentProvider.models) ? currentProvider.models : [])
1222
- .filter((model) => model?.id)
1223
- .map((model) => [model.id, model])
1224
- );
1225
-
1226
- return {
1227
- ...modelsConfig,
1228
- providers: {
1229
- ...providers,
1230
- [piProviderId]: {
1231
- ...currentProvider,
1232
- baseUrl,
1233
- api,
1234
- apiKey,
1235
- compat: {
1236
- ...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
1237
- supportsDeveloperRole: false
1238
- },
1239
- models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
1240
- }
1241
- }
1242
- };
1243
- }
1244
-
1245
- function buildPiSettingsConfig(settings = {}, defaultModel) {
1246
- return {
1247
- ...settings,
1248
- defaultProvider: piProviderId,
1249
- defaultModel,
1250
- defaultThinkingLevel: settings.defaultThinkingLevel || "high",
1251
- enableSkillCommands: settings.enableSkillCommands ?? true
1252
- };
1253
- }
1254
-
1255
- function parseDotenv(content) {
1256
- const values = {};
1257
- for (const rawLine of content.split(/\r?\n/u)) {
1258
- const line = rawLine.trim();
1259
- if (!line || line.startsWith("#")) {
1260
- continue;
1261
- }
1262
- const separatorIndex = line.indexOf("=");
1263
- if (separatorIndex === -1) {
1264
- continue;
1265
- }
1266
- const key = line.slice(0, separatorIndex).trim();
1267
- let value = line.slice(separatorIndex + 1).trim();
1268
- if (
1269
- (value.startsWith("\"") && value.endsWith("\"")) ||
1270
- (value.startsWith("'") && value.endsWith("'"))
1271
- ) {
1272
- value = value.slice(1, -1);
1273
- }
1274
- if (key) {
1275
- values[key] = value;
1276
- }
1277
- }
1278
- return values;
1279
- }
1280
-
1281
- async function readDotenvFile(path) {
1282
- if (!(await pathExists(path))) {
1283
- return {};
1284
- }
1285
-
1286
- try {
1287
- return parseDotenv(await readFile(path, "utf8"));
1288
- } catch {
1289
- return {};
1290
- }
1291
- }
1292
-
1293
- function quoteEnvValue(value) {
1294
- if (/^[A-Za-z0-9_./:@-]+$/u.test(value)) {
1295
- return value;
1296
- }
1297
- return JSON.stringify(value);
1298
- }
1299
-
1300
- function renderDotenv(values) {
1301
- const lines = Object.entries(values)
1302
- .filter(([, value]) => value !== undefined && value !== null && value !== "")
1303
- .sort(([left], [right]) => left.localeCompare(right))
1304
- .map(([key, value]) => `${key}=${quoteEnvValue(String(value))}`);
1305
- return lines.length ? `${lines.join("\n")}\n` : "";
1306
- }
1307
-
1308
- async function updateDotenvFile(path, updates) {
1309
- const current = await readDotenvFile(path);
1310
- for (const [key, value] of Object.entries(updates)) {
1311
- if (value === null || value === undefined || value === "") {
1312
- delete current[key];
1313
- } else {
1314
- current[key] = String(value);
1315
- }
1316
- }
1317
- await backupExistingPath(path);
1318
- await mkdir(dirname(path), { recursive: true });
1319
- await writeFile(path, renderDotenv(current), "utf8");
1320
- }
1321
-
1322
- function getCommandPath(command) {
1323
- const checker = isWindows() ? "where" : "which";
1324
- const result = spawnSync(checker, [command], { encoding: "utf8" });
1325
- if (result.status !== 0) {
1326
- return undefined;
1327
- }
1328
- return result.stdout
1329
- .split(/\r?\n/u)
1330
- .map((line) => line.trim())
1331
- .find(Boolean);
1332
- }
1333
-
1334
- function commandExists(command) {
1335
- return Boolean(getCommandPath(command));
1336
- }
1337
-
1338
- function readCommandOutput(command, args) {
1339
- const result = spawnSync(command, args, {
1340
- encoding: "utf8",
1341
- shell: isWindows(),
1342
- stdio: ["ignore", "pipe", "ignore"]
1343
- });
1344
- if (result.status !== 0) {
1345
- return undefined;
1346
- }
1347
- const outputValue = result.stdout.trim();
1348
- return outputValue || undefined;
1349
- }
1350
-
1351
- async function runCommand(command, args) {
1352
- await new Promise((resolvePromise, rejectPromise) => {
1353
- const child = spawn(command, args, getRunCommandSpawnOptions(getPlatform()));
1354
- child.on("error", rejectPromise);
1355
- child.on("close", (code) => {
1356
- if (code === 0) {
1357
- resolvePromise();
1358
- return;
1359
- }
1360
- rejectPromise(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
1361
- });
1362
- });
1363
- }
1364
-
1365
- async function ensureWorkflowPresent(agentsDir) {
1366
- if (await pathExists(join(agentsDir, "AGENTS.md"))) {
1367
- return;
1368
- }
1369
-
1370
- p.log.message("未检测到已安装的 AbelWorkflow,先执行一次工作流同步。");
1371
- await installManagedWorkflow({
1372
- agentsDir,
1373
- force: false,
1374
- relinkOnly: false
1375
- });
1376
- }
1377
-
1378
- async function configureGrokSearchEnv(agentsDir) {
1379
- await ensureWorkflowPresent(agentsDir);
1380
- const envPath = join(agentsDir, "skills", "grok-search", ".env");
1381
- const existing = await readDotenvFile(envPath);
1382
- const baseUrl = await p.text({
1383
- message: "Grok API URL",
1384
- defaultValue: existing.GROK_API_URL || "https://api.x.ai/v1",
1385
- validate: required()
1386
- });
1387
- assertNotCancelled(baseUrl);
1388
-
1389
- const apiKey = await p.password({
1390
- message: "Grok API Key(输入 - 清除)",
1391
- mask: "*",
1392
- defaultValue: existing.GROK_API_KEY || undefined,
1393
- validate: requiredUnlessExisting(existing.GROK_API_KEY, "Grok API Key 不能为空")
1394
- });
1395
- assertNotCancelled(apiKey);
1396
- const finalApiKey = resolvePasswordValue(apiKey, existing.GROK_API_KEY);
1397
-
1398
- const model = await p.text({
1399
- message: "Grok 默认模型",
1400
- defaultValue: existing.GROK_MODEL || "grok-4-fast",
1401
- validate: required()
1402
- });
1403
- assertNotCancelled(model);
1404
-
1405
- const useTavily = await confirmOrCancel({
1406
- message: "是否同时配置 Tavily 作为额外搜索源?",
1407
- initialValue: Boolean(existing.TAVILY_API_KEY)
1408
- });
1409
-
1410
- const tavilyKey = useTavily
1411
- ? await p.password({
1412
- message: "Tavily API Key(输入 - 清除)",
1413
- mask: "*",
1414
- defaultValue: existing.TAVILY_API_KEY || undefined,
1415
- validate: requiredUnlessExisting(existing.TAVILY_API_KEY, "Tavily API Key 不能为空")
1416
- })
1417
- : "";
1418
- if (useTavily) assertNotCancelled(tavilyKey);
1419
- const finalTavilyKey = useTavily ? resolvePasswordValue(tavilyKey, existing.TAVILY_API_KEY) : null;
1420
-
1421
- await updateDotenvFile(envPath, {
1422
- GROK_API_URL: baseUrl,
1423
- GROK_API_KEY: finalApiKey,
1424
- GROK_MODEL: model,
1425
- TAVILY_API_KEY: finalTavilyKey,
1426
- TAVILY_ENABLED: useTavily ? "true" : null
1427
- });
1428
-
1429
- p.log.step(`已写入 ${pathToLabel(envPath)}`);
1430
- }
1431
-
1432
- async function configureContext7Env(agentsDir) {
1433
- await ensureWorkflowPresent(agentsDir);
1434
- const envPath = join(agentsDir, "skills", "context7-auto-research", ".env");
1435
- const existing = await readDotenvFile(envPath);
1436
- const apiKey = await p.password({
1437
- message: "Context7 API Key (可选,输入 - 清除)",
1438
- mask: "*",
1439
- defaultValue: existing.CONTEXT7_API_KEY || undefined
1440
- });
1441
- assertNotCancelled(apiKey);
1442
- const finalApiKey = resolvePasswordValue(apiKey, existing.CONTEXT7_API_KEY);
1443
-
1444
- await updateDotenvFile(envPath, {
1445
- CONTEXT7_API_KEY: finalApiKey
1446
- });
1447
-
1448
- p.log.step(`已写入 ${pathToLabel(envPath)}`);
1449
- }
1450
-
1451
- function hasPromptEnhancerApiConfig(config) {
1452
- return [config.PE_API_URL, config.PE_API_KEY, config.PE_MODEL]
1453
- .every((value) => typeof value === "string" && value.trim() !== "");
1454
- }
1455
-
1456
- function resolvePromptEnhancerMode(existing) {
1457
- return hasPromptEnhancerApiConfig(existing) ? "openai-compatible" : "agent";
1458
- }
1459
-
1460
- async function configurePromptEnhancerEnv(agentsDir) {
1461
- await ensureWorkflowPresent(agentsDir);
1462
- const envPath = join(agentsDir, "skills", "prompt-enhancer", ".env");
1463
- const existing = await readDotenvFile(envPath);
1464
- const mode = await selectOrCancel({
1465
- message: "请选择 prompt-enhancer 的运行方式",
1466
- options: [
1467
- { value: "openai-compatible", label: "第三方 OpenAI 兼容接口" },
1468
- { value: "agent", label: "直接使用当前 Agent" }
1469
- ],
1470
- initialValue: resolvePromptEnhancerMode(existing)
1471
- });
1472
-
1473
- if (mode === "openai-compatible") {
1474
- const apiUrl = await p.text({
1475
- message: "PE_API_URL",
1476
- defaultValue: existing.PE_API_URL || undefined,
1477
- validate: required()
1478
- });
1479
- assertNotCancelled(apiUrl);
1480
-
1481
- const apiKey = await p.password({
1482
- message: "PE_API_KEY(输入 - 清除)",
1483
- mask: "*",
1484
- defaultValue: existing.PE_API_KEY || undefined,
1485
- validate: requiredUnlessExisting(existing.PE_API_KEY, "PE_API_KEY 不能为空")
1486
- });
1487
- assertNotCancelled(apiKey);
1488
- const finalApiKey = resolvePasswordValue(apiKey, existing.PE_API_KEY);
1489
-
1490
- const model = await p.text({
1491
- message: "PE_MODEL",
1492
- defaultValue: existing.PE_MODEL || undefined,
1493
- validate: required()
1494
- });
1495
- assertNotCancelled(model);
1496
-
1497
- await updateDotenvFile(envPath, {
1498
- PE_API_URL: apiUrl,
1499
- PE_API_KEY: finalApiKey,
1500
- PE_MODEL: model,
1501
- ANTHROPIC_API_KEY: null,
1502
- OPENAI_API_KEY: null
1503
- });
1504
- } else {
1505
- await updateDotenvFile(envPath, {
1506
- PE_API_URL: null,
1507
- PE_API_KEY: null,
1508
- PE_MODEL: null,
1509
- ANTHROPIC_API_KEY: null,
1510
- OPENAI_API_KEY: null
1511
- });
1512
- }
1513
-
1514
- p.log.step(`已写入 ${pathToLabel(envPath)}`);
1515
- }
1516
-
1517
- function mergeClaudeSettingsWithDefaults(settings, { augmentContextEngine = false } = {}) {
1518
- const defaults = buildDefaultClaudeSettings({ augmentContextEngine });
1519
- const env = settings?.env && typeof settings.env === "object" ? settings.env : {};
1520
- const permissions = settings?.permissions && typeof settings.permissions === "object" ? settings.permissions : {};
1521
- return {
1522
- ...defaults,
1523
- ...settings,
1524
- env: {
1525
- ...defaults.env,
1526
- ...env
1527
- },
1528
- permissions: {
1529
- ...defaults.permissions,
1530
- ...permissions,
1531
- allow: Array.isArray(permissions.allow) ? permissions.allow : defaults.permissions.allow,
1532
- deny: Array.isArray(permissions.deny) ? permissions.deny : defaults.permissions.deny
1533
- },
1534
- hooks: settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : defaults.hooks
1535
- };
1536
- }
1537
-
1538
- function getPreviousManagedClaudePermissions(previousMetadata = {}) {
1539
- return Array.isArray(previousMetadata.managedClaudePermissions)
1540
- ? previousMetadata.managedClaudePermissions.filter((value) => typeof value === "string")
1541
- : [];
1542
- }
1543
-
1544
- function applyClaudePermissionFeature(settings, {
1545
- augmentContextEngine = false,
1546
- previousManagedPermissions = []
1547
- } = {}) {
1548
- const hasSettings = settings && typeof settings === "object";
1549
- const wasManaged = previousManagedPermissions.includes(augmentContextEnginePermission);
1550
- if (!hasSettings && !augmentContextEngine) {
1551
- return { settings, changed: false, managedPermissions: [] };
1552
- }
1553
-
1554
- const defaults = buildDefaultClaudeSettings({ augmentContextEngine: false });
1555
- const nextSettings = hasSettings
1556
- ? {
1557
- ...settings,
1558
- permissions: settings.permissions && typeof settings.permissions === "object"
1559
- ? { ...settings.permissions }
1560
- : {}
1561
- }
1562
- : defaults;
1563
- const permissions = nextSettings.permissions;
1564
- const allow = Array.isArray(permissions.allow)
1565
- ? [...permissions.allow]
1566
- : [...defaults.permissions.allow];
1567
- let changed = !hasSettings;
1568
- let isManaged = wasManaged;
1569
-
1570
- if (augmentContextEngine) {
1571
- if (!allow.includes(augmentContextEnginePermission)) {
1572
- allow.push(augmentContextEnginePermission);
1573
- changed = true;
1574
- isManaged = true;
1575
- }
1576
- } else if (wasManaged && allow.includes(augmentContextEnginePermission)) {
1577
- allow.splice(allow.indexOf(augmentContextEnginePermission), 1);
1578
- changed = true;
1579
- isManaged = false;
1580
- } else {
1581
- isManaged = false;
1582
- }
1583
-
1584
- permissions.allow = allow;
1585
- if (!Array.isArray(permissions.deny)) {
1586
- permissions.deny = [...defaults.permissions.deny];
1587
- }
1588
- nextSettings.permissions = permissions;
1589
-
1590
- return {
1591
- settings: nextSettings,
1592
- changed,
1593
- managedPermissions: isManaged ? [augmentContextEnginePermission] : []
1594
- };
1595
- }
1596
-
1597
- async function ensureClaudeSettingsForFeature(augmentContextEngine, previousMetadata) {
1598
- const settingsExists = await pathExists(claudeSettingsPath);
1599
- const settings = settingsExists ? await readJsonFileSafe(claudeSettingsPath, {}) : undefined;
1600
- const result = applyClaudePermissionFeature(settings, {
1601
- augmentContextEngine,
1602
- previousManagedPermissions: getPreviousManagedClaudePermissions(previousMetadata)
1603
- });
1604
-
1605
- if (result.changed && result.settings) {
1606
- if (settingsExists) {
1607
- await backupExistingPath(claudeSettingsPath);
1608
- }
1609
- await writeJsonFileSafe(claudeSettingsPath, result.settings);
1610
- }
1611
-
1612
- return result.managedPermissions;
1613
- }
1614
-
1615
- function getExistingClaudeApiConfig(settings) {
1616
- const env = mergeClaudeSettingsWithDefaults(settings).env;
1617
- return {
1618
- baseUrl: env.ANTHROPIC_BASE_URL || "https://api.anthropic.com",
1619
- authType: env.ANTHROPIC_AUTH_TOKEN ? "auth_token" : "api_key",
1620
- key: env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || "",
1621
- model: claudeModelEnvKeys.map((field) => env[field]).find(Boolean) || ""
1622
- };
1623
- }
1624
-
1625
- function ensureApprovedClaudeApiKey(config, apiKey) {
1626
- if (!apiKey) {
1627
- return config;
1628
- }
1629
-
1630
- const truncated = apiKey.slice(0, 20);
1631
- if (!config.customApiKeyResponses || typeof config.customApiKeyResponses !== "object") {
1632
- config.customApiKeyResponses = { approved: [], rejected: [] };
1633
- }
1634
- if (!Array.isArray(config.customApiKeyResponses.approved)) {
1635
- config.customApiKeyResponses.approved = [];
1636
- }
1637
- if (!Array.isArray(config.customApiKeyResponses.rejected)) {
1638
- config.customApiKeyResponses.rejected = [];
1639
- }
1640
-
1641
- config.customApiKeyResponses.rejected = config.customApiKeyResponses.rejected.filter((item) => item !== truncated);
1642
- if (!config.customApiKeyResponses.approved.includes(truncated)) {
1643
- config.customApiKeyResponses.approved.push(truncated);
1644
- }
1645
-
1646
- return config;
1647
- }
1648
-
1649
- async function configureClaudeApi() {
1650
- const settings = await readJsonFileSafe(claudeSettingsPath, {});
1651
- const existing = getExistingClaudeApiConfig(settings);
1652
- const authType = await selectOrCancel({
1653
- message: "Claude Code 第三方 API 认证方式",
1654
- options: [
1655
- { value: "api_key", label: "API Key" },
1656
- { value: "auth_token", label: "Auth Token" }
1657
- ],
1658
- initialValue: existing.authType
1659
- });
1660
-
1661
- const baseUrl = await p.text({
1662
- message: "Claude Code Base URL",
1663
- defaultValue: existing.baseUrl,
1664
- validate: required()
1665
- });
1666
- assertNotCancelled(baseUrl);
1667
-
1668
- const key = await p.password({
1669
- message: authType === "auth_token" ? "Claude Code Auth Token(输入 - 清除)" : "Claude Code API Key(输入 - 清除)",
1670
- mask: "*",
1671
- defaultValue: existing.key || undefined,
1672
- validate: requiredUnlessExisting(existing.key, "API Key / Auth Token 不能为空")
1673
- });
1674
- assertNotCancelled(key);
1675
- const finalKey = resolvePasswordValue(key, existing.key);
1676
-
1677
- const model = await p.text({
1678
- message: "Claude Code 模型",
1679
- defaultValue: existing.model || undefined,
1680
- validate: required()
1681
- });
1682
- assertNotCancelled(model);
1683
-
1684
- const nextSettings = mergeClaudeSettingsWithDefaults(settings);
1685
- nextSettings.env.ANTHROPIC_BASE_URL = baseUrl;
1686
-
1687
- if (authType === "auth_token") {
1688
- nextSettings.env.ANTHROPIC_AUTH_TOKEN = finalKey;
1689
- delete nextSettings.env.ANTHROPIC_API_KEY;
1690
- } else {
1691
- nextSettings.env.ANTHROPIC_API_KEY = finalKey;
1692
- delete nextSettings.env.ANTHROPIC_AUTH_TOKEN;
1693
- }
1694
- for (const field of claudeModelEnvKeys) {
1695
- nextSettings.env[field] = model;
1696
- }
1697
-
1698
- await writeJsonFileWithBackup(claudeSettingsPath, nextSettings);
1699
-
1700
- const metaConfig = await readJsonFileSafe(claudeMetaConfigPath, {});
1701
- metaConfig.hasCompletedOnboarding = true;
1702
- ensureApprovedClaudeApiKey(metaConfig, finalKey);
1703
- await writeJsonFileWithBackup(claudeMetaConfigPath, metaConfig);
1704
-
1705
- p.log.step(`已更新 ${pathToLabel(claudeSettingsPath)} (${authType}, ${baseUrl}, ${maskSecret(finalKey)})`);
1706
- }
1707
-
1708
- function updateTopLevelTomlField(content, field, value) {
1709
- const lineEnding = detectLineEnding(content);
1710
- if (value === null) {
1711
- return removeTopLevelTomlField(content, field);
1712
- }
1713
-
1714
- const { topLevel, rest } = splitTopLevelTomlContent(content);
1715
- const entry = extractTopLevelTomlEntries(content).find((item) => item.field === field);
1716
- const nextLine = `${field} = ${JSON.stringify(value)}`;
1717
- let nextTopLevel;
1718
-
1719
- if (entry) {
1720
- const start = topLevel.indexOf(entry.raw);
1721
- if (start === -1) {
1722
- return content;
1723
- }
1724
- nextTopLevel = `${topLevel.slice(0, start)}${nextLine}${topLevel.slice(start + entry.raw.length)}`;
1725
- } else {
1726
- nextTopLevel = topLevel.trimEnd()
1727
- ? `${topLevel.trimEnd()}${lineEnding}${nextLine}${lineEnding}`
1728
- : `${nextLine}${lineEnding}`;
1729
- }
1730
-
1731
- nextTopLevel = nextTopLevel.trimEnd();
1732
-
1733
- if (rest && nextTopLevel) {
1734
- nextTopLevel = `${nextTopLevel}${lineEnding}${lineEnding}`;
1735
- }
1736
-
1737
- return `${nextTopLevel}${rest}`;
1738
- }
1739
-
1740
- function removeTopLevelTomlField(content, field) {
1741
- const lineEnding = detectLineEnding(content);
1742
- const { topLevel, rest } = splitTopLevelTomlContent(content);
1743
- const entry = extractTopLevelTomlEntries(content).find((item) => item.field === field);
1744
- if (!entry) {
1745
- return content;
1746
- }
1747
-
1748
- const start = topLevel.indexOf(entry.raw);
1749
- if (start === -1) {
1750
- return content;
1751
- }
1752
-
1753
- let nextTopLevel = `${topLevel.slice(0, start)}${topLevel.slice(start + entry.raw.length)}`;
1754
- nextTopLevel = collapseBlankLines(nextTopLevel, lineEnding).trimEnd();
1755
-
1756
- if (rest && nextTopLevel) {
1757
- nextTopLevel = `${nextTopLevel}${lineEnding}${lineEnding}`;
1758
- }
1759
-
1760
- return `${nextTopLevel}${rest}`;
1761
- }
1762
-
1763
- function removeTomlSection(content, sectionName) {
1764
- const lineEnding = detectLineEnding(content);
1765
- const sectionHeaderRegex = new RegExp(
1766
- `(?:^|\\r?\\n)\\[${escapeRegExp(sectionName)}\\](?:[ \\t]+#.*)?[ \\t]*\\r?$`,
1767
- "mu"
1768
- );
1769
- const headerMatch = sectionHeaderRegex.exec(content);
1770
- if (!headerMatch) {
1771
- return content;
1772
- }
1773
-
1774
- const sectionStart = headerMatch[0].startsWith("\n") || headerMatch[0].startsWith("\r\n")
1775
- ? headerMatch.index + headerMatch[0].indexOf("[")
1776
- : headerMatch.index;
1777
- const headerLineEnd = content.indexOf(lineEnding, sectionStart);
1778
- const bodyStart = headerLineEnd === -1 ? content.length : headerLineEnd + lineEnding.length;
1779
- const nextSectionStart = findTomlSectionStart(content, bodyStart);
1780
- const sectionEnd = nextSectionStart === -1 ? content.length : nextSectionStart;
1781
- return collapseBlankLines(`${content.slice(0, sectionStart)}${content.slice(sectionEnd)}`, lineEnding).trimEnd();
1782
- }
1783
-
1784
- function buildTomlSection(sectionName, values, lineEnding = "\n") {
1785
- const lines = [`[${sectionName}]`];
1786
- for (const [key, value] of Object.entries(values)) {
1787
- if (value === undefined || value === null || value === "") {
1788
- continue;
1789
- }
1790
- if (typeof value === "string") {
1791
- lines.push(`${key} = ${JSON.stringify(value)}`);
1792
- } else if (typeof value === "boolean") {
1793
- lines.push(`${key} = ${value ? "true" : "false"}`);
1794
- } else {
1795
- lines.push(`${key} = ${String(value)}`);
1796
- }
1797
- }
1798
- return `${lines.join(lineEnding)}${lineEnding}`;
1799
- }
1800
-
1801
- function formatTomlValue(value) {
1802
- if (typeof value === "string") {
1803
- return JSON.stringify(value);
1804
- }
1805
- if (typeof value === "boolean") {
1806
- return value ? "true" : "false";
1807
- }
1808
- return String(value);
1809
- }
1810
-
1811
- function getTomlLines(content) {
1812
- if (!content) {
1813
- return [];
1814
- }
1815
-
1816
- const chunks = content.match(/[^\r\n]*(?:\r?\n|$)/gu) || [];
1817
- const lines = [];
1818
- let offset = 0;
1819
-
1820
- for (const chunk of chunks) {
1821
- if (!chunk && offset >= content.length) {
1822
- break;
1823
- }
1824
-
1825
- const lineEndingMatch = chunk.match(/\r?\n$/u);
1826
- const lineEnding = lineEndingMatch ? lineEndingMatch[0] : "";
1827
- lines.push({
1828
- line: lineEnding ? chunk.slice(0, -lineEnding.length) : chunk,
1829
- start: offset
1830
- });
1831
- offset += chunk.length;
1832
-
1833
- if (!lineEnding && offset >= content.length) {
1834
- break;
1835
- }
1836
- }
1837
-
1838
- return lines;
1839
- }
1840
-
1841
- function findTomlSectionStart(content, fromIndex = 0) {
1842
- const scopedContent = content.slice(fromIndex);
1843
- let multilineDelimiter = "";
1844
-
1845
- for (const { line, start } of getTomlLines(scopedContent)) {
1846
- const trimmed = line.trim();
1847
-
1848
- if (multilineDelimiter) {
1849
- if (line.includes(multilineDelimiter)) {
1850
- multilineDelimiter = "";
1851
- }
1852
- continue;
1853
- }
1854
-
1855
- if (!trimmed || trimmed.startsWith("#")) {
1856
- continue;
1857
- }
1858
-
1859
- if (/^\[[^\]]+\]\s*(?:#.*)?$/u.test(trimmed)) {
1860
- return fromIndex + start;
1861
- }
1862
-
1863
- const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*)$/u);
1864
- if (!match) {
1865
- continue;
1866
- }
1867
-
1868
- const delimiter = getTomlMultilineStringDelimiter(match[2].trimStart());
1869
- if (delimiter && match[2].indexOf(delimiter, delimiter.length) === -1) {
1870
- multilineDelimiter = delimiter;
1871
- }
1872
- }
1873
-
1874
- return -1;
1875
- }
1876
-
1877
- function splitTopLevelTomlContent(content) {
1878
- const topLevelEnd = findTomlSectionStart(content);
1879
- return {
1880
- topLevel: topLevelEnd === -1 ? content : content.slice(0, topLevelEnd),
1881
- rest: topLevelEnd === -1 ? "" : content.slice(topLevelEnd)
1882
- };
1883
- }
1884
-
1885
- function getTomlMultilineStringDelimiter(value) {
1886
- if (value.startsWith(`"""`)) {
1887
- return `"""`;
1888
- }
1889
- if (value.startsWith(`'''`)) {
1890
- return `'''`;
1891
- }
1892
- return "";
1893
- }
1894
-
1895
- function extractTopLevelTomlEntries(content) {
1896
- const { topLevel } = splitTopLevelTomlContent(content);
1897
- const lineEnding = detectLineEnding(topLevel);
1898
- const lines = topLevel.split(/\r?\n/u);
1899
- const entries = [];
1900
-
1901
- for (let index = 0; index < lines.length; index += 1) {
1902
- const line = lines[index];
1903
- const trimmed = line.trim();
1904
- if (!trimmed || trimmed.startsWith("#")) {
1905
- continue;
1906
- }
1907
-
1908
- const match = line.match(/^\s*([A-Za-z0-9_]+)\s*=\s*(.*)$/u);
1909
- if (!match) {
1910
- continue;
1911
- }
1912
-
1913
- const rawLines = [line];
1914
- const delimiter = getTomlMultilineStringDelimiter(match[2].trimStart());
1915
- if (delimiter && match[2].indexOf(delimiter, delimiter.length) === -1) {
1916
- for (index += 1; index < lines.length; index += 1) {
1917
- rawLines.push(lines[index]);
1918
- if (lines[index].includes(delimiter)) {
1919
- break;
1920
- }
1921
- }
1922
- }
1923
-
1924
- entries.push({
1925
- field: match[1],
1926
- raw: rawLines.join(lineEnding)
1927
- });
1928
- }
1929
-
1930
- return entries;
1931
- }
1932
-
1933
- function mergeMissingTopLevelTomlEntries(content, entries) {
1934
- if (!entries.length) {
1935
- return content;
1936
- }
1937
-
1938
- const lineEnding = detectLineEnding(content);
1939
- const { topLevel, rest } = splitTopLevelTomlContent(content);
1940
- const existingFields = new Set(extractTopLevelTomlEntries(content).map(({ field }) => field));
1941
- const missingEntries = entries.filter(({ field }) => !existingFields.has(field));
1942
- if (!missingEntries.length) {
1943
- return content;
1944
- }
1945
-
1946
- let nextTopLevel = topLevel.trimEnd();
1947
- for (const entry of missingEntries) {
1948
- nextTopLevel = nextTopLevel
1949
- ? `${nextTopLevel}${lineEnding}${entry.raw}`
1950
- : entry.raw;
1951
- }
1952
-
1953
- nextTopLevel = nextTopLevel.trimEnd();
1954
- if (rest && nextTopLevel) {
1955
- nextTopLevel = `${nextTopLevel}${lineEnding}${lineEnding}`;
1956
- }
1957
- return `${nextTopLevel}${rest}`;
1958
- }
1959
-
1960
- function updateTomlBodyFields(body, values, lineEnding = "\n") {
1961
- const normalizedBody = body.replace(/\r?\n$/u, "");
1962
- const lines = normalizedBody ? normalizedBody.split(/\r?\n/u) : [];
1963
- const remaining = new Map(
1964
- Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== "")
1965
- );
1966
- const managedFields = new Set(remaining.keys());
1967
- const seenFields = new Set();
1968
- const nextLines = [];
1969
-
1970
- for (const rawLine of lines) {
1971
- const match = rawLine.match(/^(\s*)([A-Za-z0-9_]+)\s*=\s*.*$/u);
1972
- if (!match || !managedFields.has(match[2])) {
1973
- nextLines.push(rawLine);
1974
- continue;
1975
- }
1976
-
1977
- if (seenFields.has(match[2])) {
1978
- continue;
1979
- }
1980
-
1981
- nextLines.push(`${match[1]}${match[2]} = ${formatTomlValue(remaining.get(match[2]))}`);
1982
- seenFields.add(match[2]);
1983
- remaining.delete(match[2]);
1984
- }
1985
-
1986
- for (const [field, value] of remaining) {
1987
- nextLines.push(`${field} = ${formatTomlValue(value)}`);
1988
- }
1989
-
1990
- return nextLines.join(lineEnding);
1991
- }
1992
-
1993
- function updateTomlSectionFields(content, sectionName, values) {
1994
- const lineEnding = detectLineEnding(content);
1995
- const sectionHeaderRegex = new RegExp(
1996
- `^\\[${escapeRegExp(sectionName)}\\](?:[ \\t]+#.*)?[ \\t]*\\r?$`,
1997
- "mu"
1998
- );
1999
- const headerMatch = sectionHeaderRegex.exec(content);
2000
-
2001
- if (!headerMatch) {
2002
- const nextSection = buildTomlSection(sectionName, values, lineEnding).trimEnd();
2003
- return content.trimEnd()
2004
- ? `${content.trimEnd()}${lineEnding}${lineEnding}${nextSection}${lineEnding}`
2005
- : `${nextSection}${lineEnding}`;
2006
- }
2007
-
2008
- const sectionStart = headerMatch.index;
2009
- const headerLineEnd = content.indexOf(lineEnding, sectionStart);
2010
- const bodyStart = headerLineEnd === -1 ? content.length : headerLineEnd + lineEnding.length;
2011
- const headerLine = content.slice(sectionStart, headerLineEnd === -1 ? content.length : headerLineEnd);
2012
- const nextSectionStart = findTomlSectionStart(content, bodyStart);
2013
- const sectionEnd = nextSectionStart === -1 ? content.length : nextSectionStart;
2014
- const prefix = content.slice(0, sectionStart);
2015
- const body = content.slice(bodyStart, sectionEnd);
2016
- const suffix = content.slice(sectionEnd);
2017
- const nextBody = updateTomlBodyFields(body, values, lineEnding);
2018
- const renderedSection = nextBody
2019
- ? `${headerLine}${lineEnding}${nextBody}${lineEnding}`
2020
- : `${headerLine}${lineEnding}`;
2021
-
2022
- return `${prefix}${renderedSection}${suffix.replace(/^\r?\n/u, "")}`;
2023
- }
2024
-
2025
- function readTopLevelTomlString(content, field) {
2026
- const { topLevel } = splitTopLevelTomlContent(content);
2027
- for (const line of topLevel.split(/\r?\n/u)) {
2028
- const trimmed = line.trim();
2029
- if (!trimmed || trimmed.startsWith("#")) {
2030
- continue;
2031
- }
2032
- const match = trimmed.match(new RegExp(`^${escapeRegExp(field)}\\s*=\\s*"([^"]+)"$`, "u"));
2033
- if (match) {
2034
- return match[1];
2035
- }
2036
- }
2037
- return "";
2038
- }
2039
-
2040
- function parseTomlSection(content, sectionName) {
2041
- const lineEnding = detectLineEnding(content);
2042
- const sectionHeaderRegex = new RegExp(
2043
- `^\\[${escapeRegExp(sectionName)}\\](?:[ \\t]+#.*)?[ \\t]*\\r?$`,
2044
- "mu"
2045
- );
2046
- const headerMatch = sectionHeaderRegex.exec(content);
2047
- if (!headerMatch) {
2048
- return {};
2049
- }
2050
-
2051
- const sectionStart = headerMatch.index;
2052
- const headerLineEnd = content.indexOf(lineEnding, sectionStart);
2053
- const bodyStart = headerLineEnd === -1 ? content.length : headerLineEnd + lineEnding.length;
2054
- const nextSectionStart = findTomlSectionStart(content, bodyStart);
2055
- const body = content.slice(bodyStart, nextSectionStart === -1 ? content.length : nextSectionStart);
2056
-
2057
- const values = {};
2058
- for (const rawLine of body.split(/\r?\n/u)) {
2059
- const line = rawLine.trim();
2060
- if (!line || line.startsWith("#")) {
2061
- continue;
2062
- }
2063
- const stringMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*"([^"]*)"\s*(?:#.*)?$/u);
2064
- if (stringMatch) {
2065
- values[stringMatch[1]] = stringMatch[2];
2066
- continue;
2067
- }
2068
- const boolMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*(true|false)\s*(?:#.*)?$/u);
2069
- if (boolMatch) {
2070
- values[boolMatch[1]] = boolMatch[2] === "true";
2071
- continue;
2072
- }
2073
- const numberMatch = line.match(/^([A-Za-z0-9_]+)\s*=\s*(-?\d+(?:\.\d+)?)\s*(?:#.*)?$/u);
2074
- if (numberMatch) {
2075
- values[numberMatch[1]] = Number(numberMatch[2]);
2076
- }
2077
- }
2078
- return values;
2079
- }
2080
-
2081
- function stripCodexSubagentDefaults(content) {
2082
- const lineEnding = detectLineEnding(content);
2083
- let nextContent = removeTopLevelTomlField(content, "approvals_reviewer");
2084
- nextContent = removeTopLevelTomlField(nextContent, "developer_instructions");
2085
- nextContent = removeTomlSection(nextContent, "agents");
2086
-
2087
- const featureValues = parseTomlSection(nextContent, "features");
2088
- delete featureValues.multi_agent;
2089
- delete featureValues.guardian_approval;
2090
- nextContent = removeTomlSection(nextContent, "features");
2091
-
2092
- if (Object.keys(featureValues).length) {
2093
- const featuresSection = buildTomlSection("features", featureValues, lineEnding).trimEnd();
2094
- nextContent = nextContent.trimEnd()
2095
- ? `${nextContent.trimEnd()}${lineEnding}${lineEnding}${featuresSection}${lineEnding}`
2096
- : `${featuresSection}${lineEnding}`;
2097
- }
2098
-
2099
- return nextContent;
2100
- }
2101
-
2102
- function mergeCodexTemplateDefaults(content, templateContent) {
2103
- const defaultTopLevelFields = new Set([
2104
- "personality",
2105
- "disable_response_storage",
2106
- "approvals_reviewer",
2107
- "approval_policy",
2108
- "sandbox_mode",
2109
- "service_tier",
2110
- "model",
2111
- "model_reasoning_effort",
2112
- "developer_instructions"
2113
- ]);
2114
- const templateEntries = extractTopLevelTomlEntries(templateContent)
2115
- .filter(({ field }) => defaultTopLevelFields.has(field));
2116
- let nextContent = mergeMissingTopLevelTomlEntries(content, templateEntries);
2117
-
2118
- for (const sectionName of ["agents", "features"]) {
2119
- const templateValues = parseTomlSection(templateContent, sectionName);
2120
- const currentValues = parseTomlSection(nextContent, sectionName);
2121
- const missingValues = Object.fromEntries(
2122
- Object.entries(templateValues).filter(([field]) => !(field in currentValues))
2123
- );
2124
- if (Object.keys(missingValues).length) {
2125
- nextContent = updateTomlSectionFields(nextContent, sectionName, missingValues);
2126
- }
2127
- }
2128
-
2129
- return nextContent;
2130
- }
2131
-
2132
- async function loadBundledCodexConfigTemplate() {
2133
- return readFile(codexTemplateConfigPath, "utf8");
2134
- }
2135
-
2136
- async function deployBundledCodexAgents() {
2137
- const targetDir = join(home, ".codex", "agents");
2138
- await mkdir(targetDir, { recursive: true });
2139
- const entries = (await readdir(codexTemplateAgentsPath, { withFileTypes: true }))
2140
- .filter((entry) => entry.isFile() && entry.name.endsWith(".toml"))
2141
- .map((entry) => entry.name)
2142
- .sort((left, right) => left.localeCompare(right));
2143
-
2144
- const deployed = [];
2145
- for (const name of entries) {
2146
- const source = join(codexTemplateAgentsPath, name);
2147
- const target = join(targetDir, name);
2148
- await backupExistingPath(target);
2149
- await cp(source, target, { force: true });
2150
- deployed.push(target);
2151
- }
2152
- return deployed;
2153
- }
2154
-
2155
- function getDefaultCodexEnvKey(providerId) {
2156
- return `${providerId.toUpperCase().replace(/-/gu, "_")}_API_KEY`;
2157
- }
2158
-
2159
- function resolveExistingCodexApiConfig(content, auth = {}) {
2160
- const providerId = readTopLevelTomlString(content, "model_provider") || "abelworkflow";
2161
- const provider = parseTomlSection(content, `model_providers.${providerId}`);
2162
- const requiresOpenAiAuth = provider.requires_openai_auth !== false;
2163
- const configuredEnvKey = provider.temp_env_key || "";
2164
- const defaultEnvKey = requiresOpenAiAuth ? "OPENAI_API_KEY" : getDefaultCodexEnvKey(providerId);
2165
- const envKey = configuredEnvKey || defaultEnvKey;
2166
- const apiKeyCandidates = [
2167
- configuredEnvKey,
2168
- envKey,
2169
- defaultEnvKey,
2170
- "OPENAI_API_KEY"
2171
- ].filter(Boolean);
2172
- const apiKeyMatch = apiKeyCandidates.find((key) => typeof auth[key] === "string" && auth[key]);
2173
- const apiKey = apiKeyMatch ? auth[apiKeyMatch] : "";
2174
-
2175
- return {
2176
- providerId,
2177
- providerName: provider.name || providerId,
2178
- baseUrl: provider.base_url || "https://api.openai.com/v1",
2179
- envKey,
2180
- legacyEnvKeys: configuredEnvKey && configuredEnvKey !== envKey ? [configuredEnvKey] : [],
2181
- apiKey
2182
- };
2183
- }
2184
-
2185
- async function getExistingCodexApiConfig() {
2186
- const content = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
2187
- const auth = await readJsonFileSafe(codexAuthPath, {});
2188
- return resolveExistingCodexApiConfig(content, auth);
2189
- }
2190
-
2191
- async function ensurePiResourcesLinked(agentsDir) {
2192
- const { previousMetadata, managedChildren } = await syncManagedFiles(agentsDir);
2193
- const augmentContextEngine = resolveAugmentContextEngineFeature({}, previousMetadata);
2194
- await renderManagedWorkflowFiles(agentsDir, augmentContextEngine);
2195
-
2196
- const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
2197
- const piResults = await linkPi(agentsDir, previousLinkedTargets);
2198
- await writeInstallMetadata(agentsDir, {
2199
- ...previousMetadata,
2200
- package: previousMetadata.package || "abelworkflow",
2201
- installedAt: previousMetadata.installedAt || new Date().toISOString(),
2202
- features: {
2203
- ...(previousMetadata.features && typeof previousMetadata.features === "object" ? previousMetadata.features : {}),
2204
- augmentContextEngine
2205
- },
2206
- managedChildren,
2207
- managedClaudePermissions: getPreviousManagedClaudePermissions(previousMetadata),
2208
- linkedTargets: mergeLinkedTargets(previousLinkedTargets, piResults)
2209
- });
2210
- return piResults;
2211
- }
2212
-
2213
- async function configurePiApi(agentsDir) {
2214
- const modelsConfig = await readJsoncFileSafe(piModelsPath, {});
2215
- const settings = await readJsonFileSafe(piSettingsPath, {});
2216
- const existing = resolveExistingPiApiConfig(modelsConfig, settings);
2217
-
2218
- const baseUrl = await p.text({
2219
- message: "Pi gpt Base URL",
2220
- defaultValue: existing.baseUrl,
2221
- validate: required()
2222
- });
2223
- assertNotCancelled(baseUrl);
2224
-
2225
- const piApiOptions = ["openai-responses", "openai-completions"];
2226
- const api = await selectOrCancel({
2227
- message: "Pi gpt API 类型",
2228
- options: [
2229
- { value: "openai-responses", label: "OpenAI Responses API" },
2230
- { value: "openai-completions", label: "OpenAI Chat Completions" }
2231
- ],
2232
- initialValue: piApiOptions.includes(existing.api) ? existing.api : piDefaultApi
2233
- });
2234
-
2235
- const apiKey = await p.password({
2236
- message: "Pi gpt API Key(输入 - 清除)",
2237
- mask: "*",
2238
- defaultValue: existing.apiKey || undefined,
2239
- validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
2240
- });
2241
- assertNotCancelled(apiKey);
2242
- const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
2243
-
2244
- const modelIdsText = await p.text({
2245
- message: "Pi gpt 模型 ID(多个用逗号分隔)",
2246
- defaultValue: (existing.modelIds.length ? existing.modelIds : [existing.defaultModel]).join(","),
2247
- validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
2248
- });
2249
- assertNotCancelled(modelIdsText);
2250
- const modelIds = parsePiModelIds(modelIdsText);
2251
-
2252
- const defaultModel = await p.text({
2253
- message: "Pi 默认模型",
2254
- defaultValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
2255
- validate: (value) => modelIds.includes(String(value || "").trim()) ? undefined : "默认模型必须在模型 ID 列表中"
2256
- });
2257
- assertNotCancelled(defaultModel);
2258
-
2259
- const finalDefaultModel = String(defaultModel).trim();
2260
- await ensurePiResourcesLinked(agentsDir);
2261
- await writeJsonFileWithBackup(piModelsPath, buildPiModelsConfig(modelsConfig, {
2262
- baseUrl,
2263
- api,
2264
- apiKey: finalApiKey,
2265
- modelIds
2266
- }));
2267
- await writeJsonFileWithBackup(piSettingsPath, buildPiSettingsConfig(settings, finalDefaultModel));
2268
-
2269
- p.log.step(`已更新 ${pathToLabel(piModelsPath)} (${piProviderId}, ${baseUrl})`);
2270
- p.log.step(`已更新 ${pathToLabel(piSettingsPath)} (默认模型: ${finalDefaultModel})`);
2271
- p.log.step(`已链接 Pi 扩展到 ${pathToLabel(join(piAgentDir, "extensions"))}`);
2272
- }
2273
-
2274
- async function configureCodexApi() {
2275
- const existing = await getExistingCodexApiConfig();
2276
- const providerId = existing.providerId || "abelworkflow";
2277
- const providerName = existing.providerName || providerId;
2278
- const baseUrl = await p.text({
2279
- message: "Codex Base URL",
2280
- defaultValue: existing.baseUrl,
2281
- validate: required()
2282
- });
2283
- assertNotCancelled(baseUrl);
2284
-
2285
- const apiKey = await p.password({
2286
- message: "Codex 第三方 API Key(输入 - 清除)",
2287
- mask: "*",
2288
- defaultValue: existing.apiKey || undefined,
2289
- validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
2290
- });
2291
- assertNotCancelled(apiKey);
2292
- const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
2293
-
2294
- const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
2295
-
2296
- const envKey = existing.envKey || "OPENAI_API_KEY";
2297
- const currentContent = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
2298
- const templateContent = await loadBundledCodexConfigTemplate();
2299
- const content = buildCodexConfigContent(currentContent, {
2300
- templateContent,
2301
- mergeMissingTemplateDefaults: true,
2302
- includeSubagentDefaults: shouldDeploySubagents,
2303
- providerId,
2304
- providerName,
2305
- baseUrl,
2306
- envKey
2307
- });
2308
-
2309
- await backupExistingPath(codexConfigPath);
2310
- await mkdir(dirname(codexConfigPath), { recursive: true });
2311
- await writeFile(codexConfigPath, content, "utf8");
2312
-
2313
- const auth = mergeCodexAuthData(await readJsonFileSafe(codexAuthPath, {}), envKey, finalApiKey, existing.legacyEnvKeys || []);
2314
- await writeJsonFileWithBackup(codexAuthPath, auth);
2315
-
2316
- p.log.step(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
2317
- p.log.step(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(finalApiKey)})`);
2318
- if (shouldDeploySubagents) {
2319
- const deployed = await deployBundledCodexAgents();
2320
- p.log.step(`已部署 ${deployed.length} 个 Codex subagents 到 ${pathToLabel(join(home, ".codex", "agents"))}`);
2321
- } else {
2322
- p.log.message("已跳过 Codex subagents 部署。");
2323
- }
2324
- }
2325
-
2326
- function buildCodexConfigContent(currentContent, {
2327
- templateContent = "",
2328
- mergeMissingTemplateDefaults = false,
2329
- includeSubagentDefaults = true,
2330
- providerId,
2331
- providerName,
2332
- baseUrl,
2333
- envKey
2334
- }) {
2335
- const effectiveTemplateContent = includeSubagentDefaults
2336
- ? templateContent
2337
- : stripCodexSubagentDefaults(templateContent);
2338
- const hasCurrentContent = Boolean(currentContent.trim());
2339
- let content = hasCurrentContent ? currentContent : effectiveTemplateContent;
2340
- if (!includeSubagentDefaults && hasCurrentContent) {
2341
- content = stripCodexSubagentDefaults(content);
2342
- }
2343
- if (hasCurrentContent && mergeMissingTemplateDefaults && effectiveTemplateContent.trim()) {
2344
- content = mergeCodexTemplateDefaults(content, effectiveTemplateContent);
2345
- }
2346
- const lineEnding = detectLineEnding(content);
2347
- if (includeSubagentDefaults && readTopLevelTomlString(content, "approvals_reviewer") === "reviewer") {
2348
- content = updateTopLevelTomlField(content, "approvals_reviewer", "guardian_subagent");
2349
- }
2350
- content = updateTopLevelTomlField(content, "model_provider", providerId);
2351
- content = updateTopLevelTomlField(content, "preferred_auth_method", "apikey");
2352
- content = updateTomlSectionFields(content, `model_providers.${providerId}`, {
2353
- name: providerName,
2354
- base_url: baseUrl,
2355
- wire_api: "responses",
2356
- temp_env_key: envKey,
2357
- requires_openai_auth: true,
2358
- supports_websockets: true
2359
- });
2360
- return `${content.trim()}${lineEnding}`;
2361
- }
2362
-
2363
- function mergeCodexAuthData(auth, envKey, apiKey, legacyEnvKeys = []) {
2364
- return { [envKey]: apiKey };
2365
- }
2366
-
2367
- function normalizeInstallPath(value) {
2368
- if (!value) {
2369
- return "";
2370
- }
2371
- return value.replace(/\\/gu, "/").replace(/\/+$/u, "").toLowerCase();
2372
- }
2373
-
2374
- function installPathIsWithin(path, parentPath) {
2375
- const normalizedPath = normalizeInstallPath(path);
2376
- const normalizedParent = normalizeInstallPath(parentPath);
2377
- return Boolean(normalizedPath && normalizedParent)
2378
- && (normalizedPath === normalizedParent || normalizedPath.startsWith(`${normalizedParent}/`));
2379
- }
2380
-
2381
- function getNpmGlobalBinDirs(npmGlobalPrefix, platform = getPlatform()) {
2382
- if (!npmGlobalPrefix) {
2383
- return [];
2384
- }
2385
- return platform === "win32" ? [npmGlobalPrefix] : [join(npmGlobalPrefix, "bin")];
2386
- }
2387
-
2388
- function inferPackageManagerFromCommandPath(commandPath, {
2389
- bunGlobalBinDir,
2390
- npmGlobalPrefix,
2391
- platform = getPlatform()
2392
- } = {}) {
2393
- if (installPathIsWithin(commandPath, bunGlobalBinDir)) {
2394
- return "bun";
2395
- }
2396
- if (getNpmGlobalBinDirs(npmGlobalPrefix, platform)
2397
- .some((binDir) => installPathIsWithin(commandPath, binDir))) {
2398
- return "npm";
2399
- }
2400
- return null;
2401
- }
2402
-
2403
- function chooseCliInstallPackageManager({
2404
- commandPath,
2405
- availablePackageManagers = [],
2406
- bunGlobalBinDir,
2407
- npmGlobalPrefix,
2408
- platform = getPlatform(),
2409
- nodeAvailable = true,
2410
- supportedPackageManagers = ["bun", "npm"]
2411
- } = {}) {
2412
- const supported = new Set(supportedPackageManagers);
2413
- const available = new Set(availablePackageManagers.filter((packageManager) => {
2414
- if (!supported.has(packageManager)) {
2415
- return false;
2416
- }
2417
- return packageManager !== "bun" || nodeAvailable;
2418
- }));
2419
- const bunBlockedByMissingNode = !nodeAvailable
2420
- && supported.has("bun")
2421
- && availablePackageManagers.includes("bun");
2422
- const existingPackageManager = inferPackageManagerFromCommandPath(commandPath, {
2423
- bunGlobalBinDir,
2424
- npmGlobalPrefix,
2425
- platform
2426
- });
2427
-
2428
- if (existingPackageManager && available.has(existingPackageManager)) {
2429
- return { packageManager: existingPackageManager, source: "existing" };
2430
- }
2431
- for (const packageManager of ["bun", "npm"]) {
2432
- if (available.has(packageManager)) {
2433
- return { packageManager, source: "available" };
2434
- }
2435
- }
2436
- return {
2437
- packageManager: null,
2438
- source: bunBlockedByMissingNode ? "missing-node" : (availablePackageManagers.length > 0 ? "unsupported" : "missing")
2439
- };
2440
- }
2441
-
2442
- function buildCliToolInstallCommand(packageManager, { packageName, skipScripts = false }) {
2443
- if (packageManager === "npm") {
2444
- return {
2445
- command: "npm",
2446
- args: [
2447
- "install",
2448
- "-g",
2449
- ...(skipScripts ? ["--ignore-scripts"] : []),
2450
- packageName,
2451
- "--force"
2452
- ]
2453
- };
2454
- }
2455
- if (packageManager === "bun") {
2456
- return {
2457
- command: "bun",
2458
- args: [
2459
- "install",
2460
- "-g",
2461
- ...(skipScripts ? ["--ignore-scripts"] : []),
2462
- packageName
2463
- ]
2464
- };
2465
- }
2466
- throw new Error(`Unsupported package manager: ${packageManager}`);
2467
- }
2468
-
2469
- function getPackageManagerInstallHelp(platform = getPlatform()) {
2470
- const platformLabel = {
2471
- darwin: "macOS",
2472
- win32: "Windows",
2473
- linux: "Linux"
2474
- }[platform] || platform;
2475
- return {
2476
- platformLabel,
2477
- mainlandUrl: "https://npmmirror.com/mirrors/node/",
2478
- officialUrl: "https://nodejs.org/en/download/"
2479
- };
2480
- }
2481
-
2482
- function getAvailablePackageManagers() {
2483
- return ["bun", "npm"].filter((packageManager) => commandExists(packageManager));
2484
- }
2485
-
2486
- function getBunGlobalBinDir(availablePackageManagers = getAvailablePackageManagers()) {
2487
- if (!availablePackageManagers.includes("bun")) {
2488
- return undefined;
2489
- }
2490
- return readCommandOutput("bun", ["pm", "bin", "-g"]) || join(home, ".bun", "bin");
2491
- }
2492
-
2493
- function getNpmGlobalPrefix(availablePackageManagers = getAvailablePackageManagers()) {
2494
- if (!availablePackageManagers.includes("npm")) {
2495
- return undefined;
2496
- }
2497
- return readCommandOutput("npm", ["prefix", "-g"]);
2498
- }
2499
-
2500
- async function installCliTool(tool) {
2501
- const toolConfig = {
2502
- claude: {
2503
- label: "Claude Code",
2504
- command: "claude",
2505
- packageName: "@anthropic-ai/claude-code",
2506
- supportedPackageManagers: ["npm"],
2507
- installRequirement: "Claude Code 安装需要执行 postinstall;Bun 默认会阻止该脚本,因此需使用 npm。"
2508
- },
2509
- codex: {
2510
- label: "Codex",
2511
- command: "codex",
2512
- packageName: "@openai/codex"
2513
- },
2514
- pi: {
2515
- label: "Pi",
2516
- command: "pi",
2517
- packageName: "@earendil-works/pi-coding-agent",
2518
- skipScripts: true
2519
- }
2520
- }[tool];
2521
-
2522
- if (!toolConfig) {
2523
- throw new Error(`Unsupported tool: ${tool}`);
2524
- }
2525
-
2526
- const commandPath = getCommandPath(toolConfig.command);
2527
- const installed = Boolean(commandPath);
2528
- const availablePackageManagers = getAvailablePackageManagers();
2529
- const packageManagerChoice = chooseCliInstallPackageManager({
2530
- commandPath,
2531
- availablePackageManagers,
2532
- bunGlobalBinDir: getBunGlobalBinDir(availablePackageManagers),
2533
- npmGlobalPrefix: getNpmGlobalPrefix(availablePackageManagers),
2534
- platform: getPlatform(),
2535
- nodeAvailable: commandExists("node"),
2536
- supportedPackageManagers: toolConfig.supportedPackageManagers
2537
- });
2538
-
2539
- if (!packageManagerChoice.packageManager) {
2540
- const help = getPackageManagerInstallHelp();
2541
- p.log.warn(`未检测到可用于安装 ${toolConfig.label} 的包管理器。`);
2542
- if (packageManagerChoice.source === "missing-node") {
2543
- p.log.message(`${toolConfig.label} 是 Node CLI;使用 Bun 安装前也需要先安装 Node.js。`);
2544
- }
2545
- if (toolConfig.installRequirement) {
2546
- p.log.message(toolConfig.installRequirement);
2547
- }
2548
- p.log.message(`${help.platformLabel} 可先安装 Node.js/npm,再重新运行安装。`);
2549
- p.log.message(`中国大陆镜像: ${help.mainlandUrl}`);
2550
- p.log.message(`官方下载页: ${help.officialUrl}`);
2551
- return;
2552
- }
2553
-
2554
- if (installed) {
2555
- const managerMessage = packageManagerChoice.source === "existing"
2556
- ? `检测到原安装方式为 ${packageManagerChoice.packageManager}`
2557
- : `未识别原安装方式,将使用 ${packageManagerChoice.packageManager}`;
2558
- const shouldUpdate = await confirmOrCancel({
2559
- message: `${toolConfig.label} 已检测到(${managerMessage}),是否继续安装/更新?`,
2560
- initialValue: false
2561
- });
2562
- if (!shouldUpdate) {
2563
- p.log.message(`跳过 ${toolConfig.label} 安装。`);
2564
- return;
2565
- }
2566
- }
2567
-
2568
- const installCommand = buildCliToolInstallCommand(packageManagerChoice.packageManager, toolConfig);
2569
- const s = p.spinner();
2570
- s.start(`正在使用 ${packageManagerChoice.packageManager} 安装 ${toolConfig.label}...`);
2571
- try {
2572
- await runCommand(installCommand.command, installCommand.args);
2573
- s.stop(`${toolConfig.label} 安装完成`);
2574
- } catch (e) {
2575
- s.cancel(c.red(`${toolConfig.label} 安装失败: ${e.message}`));
2576
- throw e;
2577
- }
2578
- }
2579
-
2580
- async function runFullInit(options) {
2581
- const augmentContextEngine = options.nonInteractive
2582
- ? false
2583
- : await confirmOrCancel(getAugmentContextEnginePromptOptions());
2584
-
2585
- await installManagedWorkflow({
2586
- agentsDir: options.agentsDir,
2587
- force: options.force,
2588
- relinkOnly: false,
2589
- augmentContextEngine
2590
- });
2591
-
2592
- if (await confirmOrCancel({ message: "是否安装或更新 Claude Code CLI?", initialValue: false })) {
2593
- await installCliTool("claude");
2594
- }
2595
- if (await confirmOrCancel({ message: "是否配置 Claude Code 第三方 API?", initialValue: commandExists("claude") })) {
2596
- await configureClaudeApi();
2597
- }
2598
- if (await confirmOrCancel({ message: "是否安装或更新 Codex CLI?", initialValue: false })) {
2599
- await installCliTool("codex");
2600
- }
2601
- if (await confirmOrCancel({ message: "是否配置 Codex 第三方 API?", initialValue: commandExists("codex") })) {
2602
- await configureCodexApi();
2603
- }
2604
- if (await confirmOrCancel({ message: "是否安装或更新 Pi CLI?", initialValue: false })) {
2605
- await installCliTool("pi");
2606
- }
2607
- if (await confirmOrCancel({ message: "是否配置 Pi gpt 自定义 API?", initialValue: commandExists("pi") })) {
2608
- await configurePiApi(options.agentsDir);
2609
- }
2610
- if (await confirmOrCancel({ message: "是否填写 grok-search 环境变量?", initialValue: false })) {
2611
- await configureGrokSearchEnv(options.agentsDir);
2612
- }
2613
- if (await confirmOrCancel({ message: "是否填写 context7-auto-research 环境变量?", initialValue: false })) {
2614
- await configureContext7Env(options.agentsDir);
2615
- }
2616
- if (await confirmOrCancel({ message: "是否填写 prompt-enhancer 环境变量?", initialValue: false })) {
2617
- await configurePromptEnhancerEnv(options.agentsDir);
2618
- }
2619
-
2620
- p.log.success(c.green("AbelWorkflow 完整初始化完成"));
2621
- }
2622
-
2623
- async function runInteractiveMenu(options) {
2624
- p.intro(c.bold(c.bgCyan(c.black(" AbelWorkflow Setup "))));
2625
- p.log.message(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
2626
-
2627
- const buildOption = (d) => {
2628
- const opt = { value: d.value, label: d.label };
2629
- if (d.hint) {
2630
- opt.hint = d.hint;
2631
- }
2632
- return opt;
2633
- };
2634
- const cliToolMenus = {
2635
- "pi-cli": {
2636
- tool: "pi",
2637
- title: "Pi",
2638
- actions: {
2639
- "pi-install": async () => installCliTool("pi"),
2640
- "pi-api": async () => configurePiApi(options.agentsDir)
2641
- }
2642
- },
2643
- "codex-cli": {
2644
- tool: "codex",
2645
- title: "Codex",
2646
- actions: {
2647
- "codex-install": async () => installCliTool("codex"),
2648
- "codex-api": async () => configureCodexApi()
2649
- }
2650
- },
2651
- "claude-cli": {
2652
- tool: "claude",
2653
- title: "Claude Code",
2654
- actions: {
2655
- "claude-install": async () => installCliTool("claude"),
2656
- "claude-api": async () => configureClaudeApi()
2657
- }
2658
- }
2659
- };
2660
- const runCliToolMenu = async ({ tool, title, actions }) => {
2661
- while (true) {
2662
- const choice = await p.select({
2663
- message: `请选择 ${title} 操作`,
2664
- options: buildCliToolMenuDescriptors(tool).map(buildOption),
2665
- initialValue: `${tool}-install`
2666
- });
2667
-
2668
- if (p.isCancel(choice) || choice === "back") {
2669
- return;
2670
- }
2671
-
2672
- const action = actions[choice];
2673
- if (!action) {
2674
- p.log.warn(`未知 CLI 工具菜单选项: ${choice}`);
2675
- continue;
2676
- }
2677
- await action();
2678
- }
2679
- };
2680
- const menuActions = {
2681
- "full-init": async () => runFullInit(options),
2682
- install: async () => installManagedWorkflow({
2683
- agentsDir: options.agentsDir,
2684
- force: options.force,
2685
- relinkOnly: options.relinkOnly
2686
- }),
2687
- "grok-search": async () => configureGrokSearchEnv(options.agentsDir),
2688
- context7: async () => configureContext7Env(options.agentsDir),
2689
- "prompt-enhancer": async () => configurePromptEnhancerEnv(options.agentsDir),
2690
- "pi-cli": async () => runCliToolMenu(cliToolMenus["pi-cli"]),
2691
- "codex-cli": async () => runCliToolMenu(cliToolMenus["codex-cli"]),
2692
- "claude-cli": async () => runCliToolMenu(cliToolMenus["claude-cli"])
2693
- };
2694
-
2695
- while (true) {
2696
- const selectOptions = [
2697
- ...interactiveMenuDescriptors
2698
- .filter((d) => d.group === "main")
2699
- .map(buildOption),
2700
- { value: "__sep_skills__", label: "─── 技能配置 ───", disabled: true },
2701
- ...interactiveMenuDescriptors
2702
- .filter((d) => d.group === "skill")
2703
- .map(buildOption),
2704
- { value: "__sep_cli__", label: "─── CLI 工具 ───", disabled: true },
2705
- ...interactiveMenuDescriptors
2706
- .filter((d) => d.group === "cli")
2707
- .map(buildOption),
2708
- { value: "__sep_exit__", label: "────────────────", disabled: true },
2709
- ...interactiveMenuDescriptors
2710
- .filter((d) => d.group === "exit")
2711
- .map(buildOption)
2712
- ];
2713
-
2714
- const choice = await p.select({
2715
- message: "请选择操作",
2716
- options: selectOptions,
2717
- initialValue: interactiveMenuDefaultValue
2718
- });
2719
-
2720
- if (p.isCancel(choice)) {
2721
- p.outro(c.gray("已退出"));
2722
- return;
2723
- }
2724
- if (choice === "exit") {
2725
- p.outro(c.gray("已退出"));
2726
- return;
2727
- }
2728
-
2729
- const action = menuActions[choice];
2730
- if (!action) {
2731
- p.log.warn(`未知菜单选项: ${choice}`);
2732
- continue;
2733
- }
2734
-
2735
- try {
2736
- await action();
2737
- } catch (error) {
2738
- if (error instanceof CancelledError) {
2739
- p.log.warn("操作已取消,返回菜单");
2740
- continue;
2741
- }
2742
- throw error;
2743
- }
2744
- }
2745
- }
2746
-
2747
- async function main() {
2748
- const options = parseArgs(process.argv.slice(2), {
2749
- defaultAgentsDir,
2750
- resolvePath: resolve
2751
- });
2752
-
2753
- if (options.command === "help") {
2754
- printHelp();
2755
- return;
2756
- }
2757
-
2758
- // 非交互模式下菜单命令自动回退为 install
2759
- if (options.command === "menu" && options.nonInteractive) {
2760
- console.log("检测到非交互模式,自动执行工作流安装...");
2761
- options.command = "install";
2762
- }
2763
-
2764
- if (options.command === "install") {
2765
- try {
2766
- await installManagedWorkflow(options);
2767
- } catch (error) {
2768
- const message = `操作失败: ${error.message || String(error)}`;
2769
- // 非交互模式输出纯文本便于日志捕获/管道处理;交互模式使用 clack 格式化输出
2770
- if (options.nonInteractive) {
2771
- console.error(message);
2772
- } else {
2773
- p.outro(c.red(message));
2774
- }
2775
- process.exit(1);
2776
- }
2777
- return;
2778
- }
2779
-
2780
- assertInteractiveMenuSupported({
2781
- command: options.command,
2782
- inputIsTTY: input.isTTY,
2783
- outputIsTTY: output.isTTY,
2784
- nonInteractive: options.nonInteractive
2785
- });
2786
-
2787
- try {
2788
- await runInteractiveMenu(options);
2789
- } catch (error) {
2790
- const message = `操作失败: ${error.message || String(error)}`;
2791
- // 非交互模式输出纯文本便于日志捕获/管道处理;交互模式使用 clack 格式化输出
2792
- if (options.nonInteractive) {
2793
- console.error(message);
2794
- } else {
2795
- p.outro(c.red(message));
2796
- }
2797
- process.exit(1);
2798
- }
2799
- }
2800
-
2801
- export {
2802
- applyClaudePermissionFeature,
2803
- buildCliToolInstallCommand,
2804
- buildDefaultClaudeSettings,
2805
- buildCodexConfigContent,
2806
- buildPiModelsConfig,
2807
- buildPiSettingsConfig,
2808
- chooseCliInstallPackageManager,
2809
- getPackageManagerInstallHelp,
2810
- getAugmentContextEnginePromptOptions,
2811
- getRunCommandSpawnOptions,
2812
- hasPromptEnhancerApiConfig,
2813
- inferPackageManagerFromCommandPath,
2814
- main,
2815
- mergeCodexAuthData,
2816
- mergeClaudeSettingsWithDefaults,
2817
- renderManagedWorkflowContent,
2818
- resolveAugmentContextEngineFeature,
2819
- resolvePromptEnhancerMode,
2820
- resolveExistingCodexApiConfig,
2821
- parsePiModelIds,
2822
- resolveExistingPiApiConfig,
2823
- stripJsonComments,
2824
- updateTomlSectionFields
2825
- };