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