abelworkflow 0.8.1 → 0.9.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 +15 -10
- package/extensions/pi-gpt-responses-compat.ts +25 -0
- package/lib/cli/logic.mjs +29 -4
- package/lib/cli.mjs +679 -40
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AbelWorkflow
|
|
2
2
|
|
|
3
|
-
Codex、OpenCode、Claude Code 的 Skills
|
|
3
|
+
Codex、OpenCode、Claude Code、Pi 的 Skills、Commands/Prompts 和扩展配置仓库。
|
|
4
4
|
|
|
5
5
|
## 目录结构
|
|
6
6
|
|
|
@@ -17,6 +17,7 @@ Codex、OpenCode、Claude Code 的 Skills 和 Commands 配置仓库。
|
|
|
17
17
|
│ └── prompt-enhancer/ # 提示词优化器
|
|
18
18
|
├── commands/ # 命令目录
|
|
19
19
|
│ └── oc/ # 工作流命令
|
|
20
|
+
├── extensions/ # Pi 扩展
|
|
20
21
|
├── AGENTS.md # Agent 全局系统 prompts
|
|
21
22
|
└── README.md
|
|
22
23
|
```
|
|
@@ -76,11 +77,12 @@ npx abelworkflow@latest
|
|
|
76
77
|
> 说明:
|
|
77
78
|
> - npm 发布包名必须使用小写,所以实际可执行命令是 `npx abelworkflow`。
|
|
78
79
|
> - 交互式模式下,默认会打开初始化菜单,支持:
|
|
79
|
-
> - 同步 `~/.agents` 并自动重建 `~/.claude` / `~/.codex` 链接
|
|
80
|
+
> - 同步 `~/.agents` 并自动重建 `~/.claude` / `~/.codex` / `~/.pi/agent` 链接
|
|
80
81
|
> - 交互式填写 `grok-search`、`context7-auto-research`、`prompt-enhancer` 的 `.env`
|
|
81
|
-
> - 一键安装或更新 `Claude Code`、`Codex`
|
|
82
|
+
> - 一键安装或更新 `Claude Code`、`Codex`、`Pi`
|
|
82
83
|
> - 配置 `Claude Code` 的第三方 API 到 `~/.claude/settings.json`
|
|
83
84
|
> - 配置 `Codex` 的第三方 API 到 `~/.codex/config.toml` 和 `~/.codex/auth.json`
|
|
85
|
+
> - 配置 `Pi` 的自定义 API 到 `~/.pi/agent/models.json` 中的 `gpt` provider,并设置 `~/.pi/agent/settings.json` 默认模型,同时携带 Pi 扩展
|
|
84
86
|
> - 非交互场景请显式使用 `npx abelworkflow install`,不再保留旧的默认自动同步逻辑。
|
|
85
87
|
|
|
86
88
|
### 交互式初始化能力
|
|
@@ -97,12 +99,14 @@ npx abelworkflow --help
|
|
|
97
99
|
其中完整初始化会按需引导你完成:
|
|
98
100
|
|
|
99
101
|
1. 安装 AbelWorkflow 到 `~/.agents`
|
|
100
|
-
2. 自动链接到 `~/.claude/` 和 `~/.
|
|
102
|
+
2. 自动链接到 `~/.claude/`、`~/.codex/` 和 `~/.pi/agent/`
|
|
101
103
|
3. 可选安装 `Claude Code` CLI
|
|
102
104
|
4. 可选配置 `Claude Code` 第三方 API
|
|
103
105
|
5. 可选安装 `Codex` CLI
|
|
104
106
|
6. 可选配置 `Codex` 第三方 API
|
|
105
|
-
7.
|
|
107
|
+
7. 可选安装 `Pi` CLI
|
|
108
|
+
8. 可选配置 Pi `gpt` provider 自定义 API,并链接 Pi 扩展
|
|
109
|
+
9. 可选填写三个技能的环境变量
|
|
106
110
|
|
|
107
111
|
### 技能环境写入位置
|
|
108
112
|
|
|
@@ -174,11 +178,12 @@ node .\bin\abelworkflow.mjs install
|
|
|
174
178
|
|
|
175
179
|
### 映射关系(本仓库 → 配置目录)
|
|
176
180
|
|
|
177
|
-
| 本仓库 | Claude Code | Codex | 说明 |
|
|
178
|
-
|
|
179
|
-
| `AGENTS.md` | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | 全局系统提示词/规则 |
|
|
180
|
-
| `skills/<skill>/` | `~/.claude/skills/<skill>/` | `~/.codex/skills/<skill>/` | Skills(每个目录一个技能) |
|
|
181
|
-
| `commands/abel-*.md` | `~/.claude/commands/abel-*.md` | `~/.codex/prompts/abel-*.md` | 扁平化部署,避免命名冲突 |
|
|
181
|
+
| 本仓库 | Claude Code | Codex | Pi | 说明 |
|
|
182
|
+
|---|---|---|---|---|
|
|
183
|
+
| `AGENTS.md` | `~/.claude/CLAUDE.md` | `~/.codex/AGENTS.md` | `~/.pi/agent/AGENTS.md` | 全局系统提示词/规则 |
|
|
184
|
+
| `skills/<skill>/` | `~/.claude/skills/<skill>/` | `~/.codex/skills/<skill>/` | `~/.pi/agent/skills/<skill>/` | Skills(每个目录一个技能) |
|
|
185
|
+
| `commands/abel-*.md` | `~/.claude/commands/abel-*.md` | `~/.codex/prompts/abel-*.md` | `~/.pi/agent/prompts/abel-*.md` | 扁平化部署,避免命名冲突 |
|
|
186
|
+
| `extensions/*.ts` | - | - | `~/.pi/agent/extensions/*.ts` | Pi 扩展 |
|
|
182
187
|
|
|
183
188
|
### 验证(可选)
|
|
184
189
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export default function (pi: ExtensionAPI) {
|
|
4
|
+
pi.on("before_provider_request", (event, ctx) => {
|
|
5
|
+
const payload = event.payload as any;
|
|
6
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
|
|
7
|
+
if (ctx.model?.provider !== "gpt") return;
|
|
8
|
+
if (!("input" in payload)) return;
|
|
9
|
+
|
|
10
|
+
const input = Array.isArray(payload.input) ? payload.input : [];
|
|
11
|
+
const first = input[0];
|
|
12
|
+
const isPrompt = first?.role === "system" || first?.role === "developer";
|
|
13
|
+
const instructions =
|
|
14
|
+
(typeof payload.instructions === "string" && payload.instructions.trim()) ||
|
|
15
|
+
(isPrompt && typeof first.content === "string" && first.content.trim()) ||
|
|
16
|
+
ctx.getSystemPrompt().trim();
|
|
17
|
+
|
|
18
|
+
const nextPayload = { ...payload };
|
|
19
|
+
delete nextPayload.prompt_cache_key;
|
|
20
|
+
delete nextPayload.prompt_cache_retention;
|
|
21
|
+
delete nextPayload.max_output_tokens;
|
|
22
|
+
|
|
23
|
+
return { ...nextPayload, instructions, input: isPrompt ? input.slice(1) : input };
|
|
24
|
+
});
|
|
25
|
+
}
|
package/lib/cli/logic.mjs
CHANGED
|
@@ -6,14 +6,38 @@ const interactiveMenuDescriptors = [
|
|
|
6
6
|
{ value: "grok-search", label: "配置 grok-search", hint: "技能", group: "skill" },
|
|
7
7
|
{ value: "context7", label: "配置 context7-auto-research", hint: "技能", group: "skill" },
|
|
8
8
|
{ value: "prompt-enhancer", label: "配置 prompt-enhancer", hint: "技能", group: "skill" },
|
|
9
|
-
{ value: "
|
|
10
|
-
{ value: "
|
|
11
|
-
{ value: "
|
|
12
|
-
{ value: "codex-api", label: "配置 Codex API", hint: "CLI", group: "cli" },
|
|
9
|
+
{ value: "pi-cli", label: "安装/配置 Pi", hint: "CLI", group: "cli" },
|
|
10
|
+
{ value: "codex-cli", label: "安装/配置 Codex", hint: "CLI", group: "cli" },
|
|
11
|
+
{ value: "claude-cli", label: "安装/配置 Claude Code", hint: "CLI", group: "cli" },
|
|
13
12
|
{ value: "exit", label: "退出", group: "exit" }
|
|
14
13
|
];
|
|
15
14
|
|
|
16
15
|
const interactiveMenuDefaultValue = "full-init";
|
|
16
|
+
const cliToolMenuDescriptorMap = {
|
|
17
|
+
pi: [
|
|
18
|
+
{ value: "pi-install", label: "安装/更新 Pi" },
|
|
19
|
+
{ value: "pi-api", label: "配置 Pi API" },
|
|
20
|
+
{ value: "back", label: "返回上一级" }
|
|
21
|
+
],
|
|
22
|
+
codex: [
|
|
23
|
+
{ value: "codex-install", label: "安装/更新 Codex" },
|
|
24
|
+
{ value: "codex-api", label: "配置 Codex API" },
|
|
25
|
+
{ value: "back", label: "返回上一级" }
|
|
26
|
+
],
|
|
27
|
+
claude: [
|
|
28
|
+
{ value: "claude-install", label: "安装/更新 Claude Code" },
|
|
29
|
+
{ value: "claude-api", label: "配置 Claude Code API" },
|
|
30
|
+
{ value: "back", label: "返回上一级" }
|
|
31
|
+
]
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function buildCliToolMenuDescriptors(tool) {
|
|
35
|
+
const descriptors = cliToolMenuDescriptorMap[tool];
|
|
36
|
+
if (!descriptors) {
|
|
37
|
+
throw new Error(`Unknown CLI tool: ${tool}`);
|
|
38
|
+
}
|
|
39
|
+
return descriptors.map((descriptor) => ({ ...descriptor }));
|
|
40
|
+
}
|
|
17
41
|
|
|
18
42
|
class CancelledError extends Error {
|
|
19
43
|
constructor(message = "用户取消") {
|
|
@@ -175,6 +199,7 @@ function getRunCommandSpawnOptions(platform = process.env.ABELWORKFLOW_TEST_PLAT
|
|
|
175
199
|
export {
|
|
176
200
|
assertInteractiveMenuSupported,
|
|
177
201
|
assertNotCancelled,
|
|
202
|
+
buildCliToolMenuDescriptors,
|
|
178
203
|
CancelledError,
|
|
179
204
|
confirmOrCancel,
|
|
180
205
|
getRunCommandSpawnOptions,
|
package/lib/cli.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import c from "picocolors";
|
|
|
9
9
|
import {
|
|
10
10
|
assertInteractiveMenuSupported,
|
|
11
11
|
assertNotCancelled,
|
|
12
|
+
buildCliToolMenuDescriptors,
|
|
12
13
|
CancelledError,
|
|
13
14
|
confirmOrCancel,
|
|
14
15
|
getRunCommandSpawnOptions,
|
|
@@ -33,6 +34,13 @@ const codexAuthPath = join(home, ".codex", "auth.json");
|
|
|
33
34
|
const codexTemplateRoot = join(packageRoot, "lib", "templates", "codex");
|
|
34
35
|
const codexTemplateConfigPath = join(codexTemplateRoot, "config-base.toml");
|
|
35
36
|
const codexTemplateAgentsPath = join(codexTemplateRoot, "agents");
|
|
37
|
+
const piAgentDir = join(home, ".pi", "agent");
|
|
38
|
+
const piModelsPath = join(piAgentDir, "models.json");
|
|
39
|
+
const piSettingsPath = join(piAgentDir, "settings.json");
|
|
40
|
+
const piProviderId = "gpt";
|
|
41
|
+
const piDefaultApi = "openai-responses";
|
|
42
|
+
const piDefaultBaseUrl = "https://api.openai.com/v1";
|
|
43
|
+
const piDefaultModel = "gpt-5.5";
|
|
36
44
|
const installBackupStamp = Date.now();
|
|
37
45
|
const createdBackupPaths = new Set();
|
|
38
46
|
const augmentContextEnginePermission = "mcp__augment-context-engine";
|
|
@@ -95,6 +103,7 @@ const managedEntries = [
|
|
|
95
103
|
{ target: "README.md" },
|
|
96
104
|
{ target: "commands", preserveExisting: true },
|
|
97
105
|
{ target: "skills", preserveExisting: true, filter: shouldCopySkillPath },
|
|
106
|
+
{ target: "extensions", preserveExisting: true },
|
|
98
107
|
{ target: ".skill-lock.json" },
|
|
99
108
|
{ target: ".gitignore", sourceCandidates: [".gitignore", ".npmignore"] }
|
|
100
109
|
];
|
|
@@ -390,6 +399,39 @@ async function writeInstallMetadata(agentsDir, metadata) {
|
|
|
390
399
|
await writeFile(join(agentsDir, installMetadataName), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
|
|
391
400
|
}
|
|
392
401
|
|
|
402
|
+
function linkedTargetsFromResults(results) {
|
|
403
|
+
return Object.fromEntries(
|
|
404
|
+
results
|
|
405
|
+
.filter((result) => result.sourcePath)
|
|
406
|
+
.map((result) => [
|
|
407
|
+
result.targetPath,
|
|
408
|
+
{
|
|
409
|
+
sourcePath: result.sourcePath,
|
|
410
|
+
kind: result.kind,
|
|
411
|
+
mode: result.mode
|
|
412
|
+
}
|
|
413
|
+
])
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function mergeLinkedTargets(previousLinkedTargets = {}, results = []) {
|
|
418
|
+
const nextLinkedTargets = { ...previousLinkedTargets };
|
|
419
|
+
for (const result of results) {
|
|
420
|
+
if (result.sourcePath) {
|
|
421
|
+
nextLinkedTargets[result.targetPath] = {
|
|
422
|
+
sourcePath: result.sourcePath,
|
|
423
|
+
kind: result.kind,
|
|
424
|
+
mode: result.mode
|
|
425
|
+
};
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (result.status === "removed") {
|
|
429
|
+
delete nextLinkedTargets[result.targetPath];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return nextLinkedTargets;
|
|
433
|
+
}
|
|
434
|
+
|
|
393
435
|
async function renderManagedWorkflowFile(path, augmentContextEngine) {
|
|
394
436
|
if (!(await pathIsFile(path))) {
|
|
395
437
|
return;
|
|
@@ -666,6 +708,18 @@ async function getCommandNames(commandsDir) {
|
|
|
666
708
|
return names.filter(Boolean);
|
|
667
709
|
}
|
|
668
710
|
|
|
711
|
+
async function getPiExtensionNames(extensionsDir) {
|
|
712
|
+
if (!(await pathIsDirectory(extensionsDir))) {
|
|
713
|
+
return [];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const entries = await readdir(extensionsDir, { withFileTypes: true });
|
|
717
|
+
const names = await Promise.all(
|
|
718
|
+
entries.map(async (entry) => ((await isPiExtensionEntry(extensionsDir, entry)) ? entry.name : null))
|
|
719
|
+
);
|
|
720
|
+
return names.filter(Boolean);
|
|
721
|
+
}
|
|
722
|
+
|
|
669
723
|
async function pathIsDirectory(path) {
|
|
670
724
|
try {
|
|
671
725
|
return (await stat(path)).isDirectory();
|
|
@@ -710,6 +764,23 @@ async function isMarkdownFileEntry(root, entry) {
|
|
|
710
764
|
return pathIsFile(join(root, entry.name));
|
|
711
765
|
}
|
|
712
766
|
|
|
767
|
+
async function isPiExtensionEntry(root, entry) {
|
|
768
|
+
const entryPath = join(root, entry.name);
|
|
769
|
+
if (entry.isDirectory()) {
|
|
770
|
+
return pathIsFile(join(entryPath, "index.ts"));
|
|
771
|
+
}
|
|
772
|
+
if (entry.isFile()) {
|
|
773
|
+
return entry.name.endsWith(".ts");
|
|
774
|
+
}
|
|
775
|
+
if (!entry.isSymbolicLink()) {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
if (entry.name.endsWith(".ts") && await pathIsFile(entryPath)) {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
return pathIsFile(join(entryPath, "index.ts"));
|
|
782
|
+
}
|
|
783
|
+
|
|
713
784
|
async function pruneManagedTargets(targetDir, managedSourceRoot, expectedNames, previousLinkedTargets) {
|
|
714
785
|
if (!(await pathExists(targetDir))) {
|
|
715
786
|
return [];
|
|
@@ -844,6 +915,71 @@ async function linkCodex(agentsDir, previousLinkedTargets) {
|
|
|
844
915
|
return results;
|
|
845
916
|
}
|
|
846
917
|
|
|
918
|
+
async function linkPi(agentsDir, previousLinkedTargets) {
|
|
919
|
+
const results = [];
|
|
920
|
+
await mkdir(piAgentDir, { recursive: true });
|
|
921
|
+
await removeIfNotDirectory(join(piAgentDir, "skills"));
|
|
922
|
+
await removeIfNotDirectory(join(piAgentDir, "prompts"));
|
|
923
|
+
await removeIfNotDirectory(join(piAgentDir, "extensions"));
|
|
924
|
+
await mkdir(join(piAgentDir, "skills"), { recursive: true });
|
|
925
|
+
await mkdir(join(piAgentDir, "prompts"), { recursive: true });
|
|
926
|
+
await mkdir(join(piAgentDir, "extensions"), { recursive: true });
|
|
927
|
+
|
|
928
|
+
results.push(
|
|
929
|
+
await ensureManagedLink(
|
|
930
|
+
join(piAgentDir, "AGENTS.md"),
|
|
931
|
+
join(agentsDir, "AGENTS.md"),
|
|
932
|
+
"file",
|
|
933
|
+
previousLinkedTargets
|
|
934
|
+
)
|
|
935
|
+
);
|
|
936
|
+
results.push(...(await linkSkillDirectories(piAgentDir, agentsDir, previousLinkedTargets)));
|
|
937
|
+
|
|
938
|
+
const commandFiles = await getCommandNames(join(agentsDir, "commands"));
|
|
939
|
+
results.push(
|
|
940
|
+
...(await pruneManagedTargets(
|
|
941
|
+
join(piAgentDir, "prompts"),
|
|
942
|
+
join(agentsDir, "commands"),
|
|
943
|
+
commandFiles,
|
|
944
|
+
previousLinkedTargets
|
|
945
|
+
))
|
|
946
|
+
);
|
|
947
|
+
for (const fileName of commandFiles) {
|
|
948
|
+
results.push(
|
|
949
|
+
await ensureManagedLink(
|
|
950
|
+
join(piAgentDir, "prompts", fileName),
|
|
951
|
+
join(agentsDir, "commands", fileName),
|
|
952
|
+
"file",
|
|
953
|
+
previousLinkedTargets
|
|
954
|
+
)
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const extensionNames = await getPiExtensionNames(join(agentsDir, "extensions"));
|
|
959
|
+
results.push(
|
|
960
|
+
...(await pruneManagedTargets(
|
|
961
|
+
join(piAgentDir, "extensions"),
|
|
962
|
+
join(agentsDir, "extensions"),
|
|
963
|
+
extensionNames,
|
|
964
|
+
previousLinkedTargets
|
|
965
|
+
))
|
|
966
|
+
);
|
|
967
|
+
for (const entryName of extensionNames) {
|
|
968
|
+
const sourcePath = join(agentsDir, "extensions", entryName);
|
|
969
|
+
const kind = await pathIsDirectory(sourcePath) ? "dir" : "file";
|
|
970
|
+
results.push(
|
|
971
|
+
await ensureManagedLink(
|
|
972
|
+
join(piAgentDir, "extensions", entryName),
|
|
973
|
+
sourcePath,
|
|
974
|
+
kind,
|
|
975
|
+
previousLinkedTargets
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return results;
|
|
981
|
+
}
|
|
982
|
+
|
|
847
983
|
async function installManagedWorkflow(options) {
|
|
848
984
|
let previousMetadata = {};
|
|
849
985
|
let managedChildren = {};
|
|
@@ -870,30 +1006,21 @@ async function installManagedWorkflow(options) {
|
|
|
870
1006
|
}
|
|
871
1007
|
|
|
872
1008
|
const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
|
|
873
|
-
s.start("正在链接 Claude / Codex...");
|
|
1009
|
+
s.start("正在链接 Claude / Codex / Pi...");
|
|
874
1010
|
let claudeResults;
|
|
875
1011
|
let codexResults;
|
|
1012
|
+
let piResults;
|
|
876
1013
|
try {
|
|
877
1014
|
claudeResults = await linkClaude(options.agentsDir, previousLinkedTargets);
|
|
878
1015
|
codexResults = await linkCodex(options.agentsDir, previousLinkedTargets);
|
|
1016
|
+
piResults = await linkPi(options.agentsDir, previousLinkedTargets);
|
|
879
1017
|
} catch (e) {
|
|
880
1018
|
s.cancel(c.red(`链接失败: ${e.message}`));
|
|
881
1019
|
throw e;
|
|
882
1020
|
}
|
|
883
1021
|
s.stop("链接完成");
|
|
884
1022
|
|
|
885
|
-
const linkedTargets =
|
|
886
|
-
[...claudeResults, ...codexResults]
|
|
887
|
-
.filter((result) => result.sourcePath)
|
|
888
|
-
.map((result) => [
|
|
889
|
-
result.targetPath,
|
|
890
|
-
{
|
|
891
|
-
sourcePath: result.sourcePath,
|
|
892
|
-
kind: result.kind,
|
|
893
|
-
mode: result.mode
|
|
894
|
-
}
|
|
895
|
-
])
|
|
896
|
-
);
|
|
1023
|
+
const linkedTargets = linkedTargetsFromResults([...claudeResults, ...codexResults, ...piResults]);
|
|
897
1024
|
|
|
898
1025
|
const managedClaudePermissions = await ensureClaudeSettingsForFeature(augmentContextEngine, previousMetadata);
|
|
899
1026
|
|
|
@@ -909,7 +1036,7 @@ async function installManagedWorkflow(options) {
|
|
|
909
1036
|
linkedTargets
|
|
910
1037
|
});
|
|
911
1038
|
|
|
912
|
-
const resultLines = [...claudeResults, ...codexResults].map((result) => {
|
|
1039
|
+
const resultLines = [...claudeResults, ...codexResults, ...piResults].map((result) => {
|
|
913
1040
|
const icon = result.status === "unchanged" ? c.gray("=")
|
|
914
1041
|
: result.status === "removed" ? c.yellow("−")
|
|
915
1042
|
: c.green("+");
|
|
@@ -943,6 +1070,188 @@ async function writeJsonFileWithBackup(path, data) {
|
|
|
943
1070
|
await writeJsonFileSafe(path, data);
|
|
944
1071
|
}
|
|
945
1072
|
|
|
1073
|
+
function stripJsonComments(content) {
|
|
1074
|
+
let output = "";
|
|
1075
|
+
let inString = false;
|
|
1076
|
+
let quote = "";
|
|
1077
|
+
let escaped = false;
|
|
1078
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
1079
|
+
const char = content[i];
|
|
1080
|
+
const next = content[i + 1];
|
|
1081
|
+
|
|
1082
|
+
if (inString) {
|
|
1083
|
+
output += char;
|
|
1084
|
+
if (escaped) {
|
|
1085
|
+
escaped = false;
|
|
1086
|
+
} else if (char === "\\") {
|
|
1087
|
+
escaped = true;
|
|
1088
|
+
} else if (char === quote) {
|
|
1089
|
+
inString = false;
|
|
1090
|
+
quote = "";
|
|
1091
|
+
}
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
if (char === "\"" || char === "'") {
|
|
1096
|
+
inString = true;
|
|
1097
|
+
quote = char;
|
|
1098
|
+
output += char;
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (char === "/" && next === "/") {
|
|
1103
|
+
while (i < content.length && content[i] !== "\n") {
|
|
1104
|
+
i += 1;
|
|
1105
|
+
}
|
|
1106
|
+
if (i < content.length) {
|
|
1107
|
+
output += content[i];
|
|
1108
|
+
}
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (char === "/" && next === "*") {
|
|
1113
|
+
i += 2;
|
|
1114
|
+
while (i < content.length && !(content[i] === "*" && content[i + 1] === "/")) {
|
|
1115
|
+
output += content[i] === "\n" ? "\n" : "";
|
|
1116
|
+
i += 1;
|
|
1117
|
+
}
|
|
1118
|
+
i += 1;
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
output += char;
|
|
1123
|
+
}
|
|
1124
|
+
return stripJsonTrailingCommas(output);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function stripJsonTrailingCommas(content) {
|
|
1128
|
+
let output = "";
|
|
1129
|
+
let inString = false;
|
|
1130
|
+
let quote = "";
|
|
1131
|
+
let escaped = false;
|
|
1132
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
1133
|
+
const char = content[i];
|
|
1134
|
+
|
|
1135
|
+
if (inString) {
|
|
1136
|
+
output += char;
|
|
1137
|
+
if (escaped) {
|
|
1138
|
+
escaped = false;
|
|
1139
|
+
} else if (char === "\\") {
|
|
1140
|
+
escaped = true;
|
|
1141
|
+
} else if (char === quote) {
|
|
1142
|
+
inString = false;
|
|
1143
|
+
quote = "";
|
|
1144
|
+
}
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
if (char === "\"" || char === "'") {
|
|
1149
|
+
inString = true;
|
|
1150
|
+
quote = char;
|
|
1151
|
+
output += char;
|
|
1152
|
+
continue;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (char === ",") {
|
|
1156
|
+
let nextIndex = i + 1;
|
|
1157
|
+
while (nextIndex < content.length && /\s/u.test(content[nextIndex])) {
|
|
1158
|
+
nextIndex += 1;
|
|
1159
|
+
}
|
|
1160
|
+
if (content[nextIndex] === "}" || content[nextIndex] === "]") {
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
output += char;
|
|
1166
|
+
}
|
|
1167
|
+
return output;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
async function readJsoncFileSafe(path, fallback = {}) {
|
|
1171
|
+
if (!(await pathExists(path))) {
|
|
1172
|
+
return fallback;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
try {
|
|
1176
|
+
return JSON.parse(stripJsonComments(await readFile(path, "utf8")));
|
|
1177
|
+
} catch {
|
|
1178
|
+
return fallback;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function parsePiModelIds(value) {
|
|
1183
|
+
return [...new Set(String(value || "")
|
|
1184
|
+
.split(/[\n,]+/u)
|
|
1185
|
+
.map((item) => item.trim())
|
|
1186
|
+
.filter(Boolean))];
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}) {
|
|
1190
|
+
const provider = modelsConfig.providers?.[piProviderId] && typeof modelsConfig.providers[piProviderId] === "object"
|
|
1191
|
+
? modelsConfig.providers[piProviderId]
|
|
1192
|
+
: {};
|
|
1193
|
+
const models = Array.isArray(provider.models) ? provider.models.filter((model) => model?.id) : [];
|
|
1194
|
+
return {
|
|
1195
|
+
baseUrl: provider.baseUrl || piDefaultBaseUrl,
|
|
1196
|
+
api: provider.api || piDefaultApi,
|
|
1197
|
+
apiKey: provider.apiKey || "",
|
|
1198
|
+
modelIds: models.map((model) => model.id),
|
|
1199
|
+
defaultModel: settings.defaultProvider === piProviderId && settings.defaultModel
|
|
1200
|
+
? settings.defaultModel
|
|
1201
|
+
: models[0]?.id || piDefaultModel
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function buildPiModelConfig(modelId, existingModel = {}) {
|
|
1206
|
+
return {
|
|
1207
|
+
...existingModel,
|
|
1208
|
+
id: modelId,
|
|
1209
|
+
name: existingModel.name || modelId,
|
|
1210
|
+
reasoning: existingModel.reasoning ?? true,
|
|
1211
|
+
input: Array.isArray(existingModel.input) ? existingModel.input : ["text", "image"],
|
|
1212
|
+
contextWindow: existingModel.contextWindow ?? 262144,
|
|
1213
|
+
maxTokens: existingModel.maxTokens ?? 64000
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function buildPiModelsConfig(modelsConfig = {}, { baseUrl, api, apiKey, modelIds }) {
|
|
1218
|
+
const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
|
|
1219
|
+
const currentProvider = providers[piProviderId] && typeof providers[piProviderId] === "object" ? providers[piProviderId] : {};
|
|
1220
|
+
const existingModels = new Map(
|
|
1221
|
+
(Array.isArray(currentProvider.models) ? currentProvider.models : [])
|
|
1222
|
+
.filter((model) => model?.id)
|
|
1223
|
+
.map((model) => [model.id, model])
|
|
1224
|
+
);
|
|
1225
|
+
|
|
1226
|
+
return {
|
|
1227
|
+
...modelsConfig,
|
|
1228
|
+
providers: {
|
|
1229
|
+
...providers,
|
|
1230
|
+
[piProviderId]: {
|
|
1231
|
+
...currentProvider,
|
|
1232
|
+
baseUrl,
|
|
1233
|
+
api,
|
|
1234
|
+
apiKey,
|
|
1235
|
+
compat: {
|
|
1236
|
+
...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
|
|
1237
|
+
supportsDeveloperRole: false
|
|
1238
|
+
},
|
|
1239
|
+
models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function buildPiSettingsConfig(settings = {}, defaultModel) {
|
|
1246
|
+
return {
|
|
1247
|
+
...settings,
|
|
1248
|
+
defaultProvider: piProviderId,
|
|
1249
|
+
defaultModel,
|
|
1250
|
+
defaultThinkingLevel: settings.defaultThinkingLevel || "high",
|
|
1251
|
+
enableSkillCommands: settings.enableSkillCommands ?? true
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
|
|
946
1255
|
function parseDotenv(content) {
|
|
947
1256
|
const values = {};
|
|
948
1257
|
for (const rawLine of content.split(/\r?\n/u)) {
|
|
@@ -1010,10 +1319,33 @@ async function updateDotenvFile(path, updates) {
|
|
|
1010
1319
|
await writeFile(path, renderDotenv(current), "utf8");
|
|
1011
1320
|
}
|
|
1012
1321
|
|
|
1013
|
-
function
|
|
1322
|
+
function getCommandPath(command) {
|
|
1014
1323
|
const checker = isWindows() ? "where" : "which";
|
|
1015
|
-
const result = spawnSync(checker, [command], {
|
|
1016
|
-
|
|
1324
|
+
const result = spawnSync(checker, [command], { encoding: "utf8" });
|
|
1325
|
+
if (result.status !== 0) {
|
|
1326
|
+
return undefined;
|
|
1327
|
+
}
|
|
1328
|
+
return result.stdout
|
|
1329
|
+
.split(/\r?\n/u)
|
|
1330
|
+
.map((line) => line.trim())
|
|
1331
|
+
.find(Boolean);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function commandExists(command) {
|
|
1335
|
+
return Boolean(getCommandPath(command));
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function readCommandOutput(command, args) {
|
|
1339
|
+
const result = spawnSync(command, args, {
|
|
1340
|
+
encoding: "utf8",
|
|
1341
|
+
shell: isWindows(),
|
|
1342
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1343
|
+
});
|
|
1344
|
+
if (result.status !== 0) {
|
|
1345
|
+
return undefined;
|
|
1346
|
+
}
|
|
1347
|
+
const outputValue = result.stdout.trim();
|
|
1348
|
+
return outputValue || undefined;
|
|
1017
1349
|
}
|
|
1018
1350
|
|
|
1019
1351
|
async function runCommand(command, args) {
|
|
@@ -1856,6 +2188,89 @@ async function getExistingCodexApiConfig() {
|
|
|
1856
2188
|
return resolveExistingCodexApiConfig(content, auth);
|
|
1857
2189
|
}
|
|
1858
2190
|
|
|
2191
|
+
async function ensurePiResourcesLinked(agentsDir) {
|
|
2192
|
+
const { previousMetadata, managedChildren } = await syncManagedFiles(agentsDir);
|
|
2193
|
+
const augmentContextEngine = resolveAugmentContextEngineFeature({}, previousMetadata);
|
|
2194
|
+
await renderManagedWorkflowFiles(agentsDir, augmentContextEngine);
|
|
2195
|
+
|
|
2196
|
+
const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
|
|
2197
|
+
const piResults = await linkPi(agentsDir, previousLinkedTargets);
|
|
2198
|
+
await writeInstallMetadata(agentsDir, {
|
|
2199
|
+
...previousMetadata,
|
|
2200
|
+
package: previousMetadata.package || "abelworkflow",
|
|
2201
|
+
installedAt: previousMetadata.installedAt || new Date().toISOString(),
|
|
2202
|
+
features: {
|
|
2203
|
+
...(previousMetadata.features && typeof previousMetadata.features === "object" ? previousMetadata.features : {}),
|
|
2204
|
+
augmentContextEngine
|
|
2205
|
+
},
|
|
2206
|
+
managedChildren,
|
|
2207
|
+
managedClaudePermissions: getPreviousManagedClaudePermissions(previousMetadata),
|
|
2208
|
+
linkedTargets: mergeLinkedTargets(previousLinkedTargets, piResults)
|
|
2209
|
+
});
|
|
2210
|
+
return piResults;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
async function configurePiApi(agentsDir) {
|
|
2214
|
+
const modelsConfig = await readJsoncFileSafe(piModelsPath, {});
|
|
2215
|
+
const settings = await readJsonFileSafe(piSettingsPath, {});
|
|
2216
|
+
const existing = resolveExistingPiApiConfig(modelsConfig, settings);
|
|
2217
|
+
|
|
2218
|
+
const baseUrl = await p.text({
|
|
2219
|
+
message: "Pi gpt Base URL",
|
|
2220
|
+
defaultValue: existing.baseUrl,
|
|
2221
|
+
validate: required()
|
|
2222
|
+
});
|
|
2223
|
+
assertNotCancelled(baseUrl);
|
|
2224
|
+
|
|
2225
|
+
const piApiOptions = ["openai-responses", "openai-completions"];
|
|
2226
|
+
const api = await selectOrCancel({
|
|
2227
|
+
message: "Pi gpt API 类型",
|
|
2228
|
+
options: [
|
|
2229
|
+
{ value: "openai-responses", label: "OpenAI Responses API" },
|
|
2230
|
+
{ value: "openai-completions", label: "OpenAI Chat Completions" }
|
|
2231
|
+
],
|
|
2232
|
+
initialValue: piApiOptions.includes(existing.api) ? existing.api : piDefaultApi
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
const apiKey = await p.password({
|
|
2236
|
+
message: "Pi gpt API Key(输入 - 清除)",
|
|
2237
|
+
mask: "*",
|
|
2238
|
+
defaultValue: existing.apiKey || undefined,
|
|
2239
|
+
validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
|
|
2240
|
+
});
|
|
2241
|
+
assertNotCancelled(apiKey);
|
|
2242
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
|
|
2243
|
+
|
|
2244
|
+
const modelIdsText = await p.text({
|
|
2245
|
+
message: "Pi gpt 模型 ID(多个用逗号分隔)",
|
|
2246
|
+
defaultValue: (existing.modelIds.length ? existing.modelIds : [existing.defaultModel]).join(","),
|
|
2247
|
+
validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
|
|
2248
|
+
});
|
|
2249
|
+
assertNotCancelled(modelIdsText);
|
|
2250
|
+
const modelIds = parsePiModelIds(modelIdsText);
|
|
2251
|
+
|
|
2252
|
+
const defaultModel = await p.text({
|
|
2253
|
+
message: "Pi 默认模型",
|
|
2254
|
+
defaultValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
|
|
2255
|
+
validate: (value) => modelIds.includes(String(value || "").trim()) ? undefined : "默认模型必须在模型 ID 列表中"
|
|
2256
|
+
});
|
|
2257
|
+
assertNotCancelled(defaultModel);
|
|
2258
|
+
|
|
2259
|
+
const finalDefaultModel = String(defaultModel).trim();
|
|
2260
|
+
await ensurePiResourcesLinked(agentsDir);
|
|
2261
|
+
await writeJsonFileWithBackup(piModelsPath, buildPiModelsConfig(modelsConfig, {
|
|
2262
|
+
baseUrl,
|
|
2263
|
+
api,
|
|
2264
|
+
apiKey: finalApiKey,
|
|
2265
|
+
modelIds
|
|
2266
|
+
}));
|
|
2267
|
+
await writeJsonFileWithBackup(piSettingsPath, buildPiSettingsConfig(settings, finalDefaultModel));
|
|
2268
|
+
|
|
2269
|
+
p.log.step(`已更新 ${pathToLabel(piModelsPath)} (${piProviderId}, ${baseUrl})`);
|
|
2270
|
+
p.log.step(`已更新 ${pathToLabel(piSettingsPath)} (默认模型: ${finalDefaultModel})`);
|
|
2271
|
+
p.log.step(`已链接 Pi 扩展到 ${pathToLabel(join(piAgentDir, "extensions"))}`);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
1859
2274
|
async function configureCodexApi() {
|
|
1860
2275
|
const existing = await getExistingCodexApiConfig();
|
|
1861
2276
|
const providerId = existing.providerId || "abelworkflow";
|
|
@@ -1945,14 +2360,140 @@ function buildCodexConfigContent(currentContent, {
|
|
|
1945
2360
|
}
|
|
1946
2361
|
|
|
1947
2362
|
function mergeCodexAuthData(auth, envKey, apiKey, legacyEnvKeys = []) {
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2363
|
+
return { [envKey]: apiKey };
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
function normalizeInstallPath(value) {
|
|
2367
|
+
if (!value) {
|
|
2368
|
+
return "";
|
|
2369
|
+
}
|
|
2370
|
+
return value.replace(/\\/gu, "/").replace(/\/+$/u, "").toLowerCase();
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
function installPathIsWithin(path, parentPath) {
|
|
2374
|
+
const normalizedPath = normalizeInstallPath(path);
|
|
2375
|
+
const normalizedParent = normalizeInstallPath(parentPath);
|
|
2376
|
+
return Boolean(normalizedPath && normalizedParent)
|
|
2377
|
+
&& (normalizedPath === normalizedParent || normalizedPath.startsWith(`${normalizedParent}/`));
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
function getNpmGlobalBinDirs(npmGlobalPrefix, platform = getPlatform()) {
|
|
2381
|
+
if (!npmGlobalPrefix) {
|
|
2382
|
+
return [];
|
|
2383
|
+
}
|
|
2384
|
+
return platform === "win32" ? [npmGlobalPrefix] : [join(npmGlobalPrefix, "bin")];
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
function inferPackageManagerFromCommandPath(commandPath, {
|
|
2388
|
+
bunGlobalBinDir,
|
|
2389
|
+
npmGlobalPrefix,
|
|
2390
|
+
platform = getPlatform()
|
|
2391
|
+
} = {}) {
|
|
2392
|
+
if (installPathIsWithin(commandPath, bunGlobalBinDir)) {
|
|
2393
|
+
return "bun";
|
|
2394
|
+
}
|
|
2395
|
+
if (getNpmGlobalBinDirs(npmGlobalPrefix, platform)
|
|
2396
|
+
.some((binDir) => installPathIsWithin(commandPath, binDir))) {
|
|
2397
|
+
return "npm";
|
|
2398
|
+
}
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
function chooseCliInstallPackageManager({
|
|
2403
|
+
commandPath,
|
|
2404
|
+
availablePackageManagers = [],
|
|
2405
|
+
bunGlobalBinDir,
|
|
2406
|
+
npmGlobalPrefix,
|
|
2407
|
+
platform = getPlatform(),
|
|
2408
|
+
nodeAvailable = true,
|
|
2409
|
+
supportedPackageManagers = ["bun", "npm"]
|
|
2410
|
+
} = {}) {
|
|
2411
|
+
const supported = new Set(supportedPackageManagers);
|
|
2412
|
+
const available = new Set(availablePackageManagers.filter((packageManager) => {
|
|
2413
|
+
if (!supported.has(packageManager)) {
|
|
2414
|
+
return false;
|
|
1952
2415
|
}
|
|
2416
|
+
return packageManager !== "bun" || nodeAvailable;
|
|
2417
|
+
}));
|
|
2418
|
+
const bunBlockedByMissingNode = !nodeAvailable
|
|
2419
|
+
&& supported.has("bun")
|
|
2420
|
+
&& availablePackageManagers.includes("bun");
|
|
2421
|
+
const existingPackageManager = inferPackageManagerFromCommandPath(commandPath, {
|
|
2422
|
+
bunGlobalBinDir,
|
|
2423
|
+
npmGlobalPrefix,
|
|
2424
|
+
platform
|
|
2425
|
+
});
|
|
2426
|
+
|
|
2427
|
+
if (existingPackageManager && available.has(existingPackageManager)) {
|
|
2428
|
+
return { packageManager: existingPackageManager, source: "existing" };
|
|
1953
2429
|
}
|
|
1954
|
-
|
|
1955
|
-
|
|
2430
|
+
for (const packageManager of ["bun", "npm"]) {
|
|
2431
|
+
if (available.has(packageManager)) {
|
|
2432
|
+
return { packageManager, source: "available" };
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
return {
|
|
2436
|
+
packageManager: null,
|
|
2437
|
+
source: bunBlockedByMissingNode ? "missing-node" : (availablePackageManagers.length > 0 ? "unsupported" : "missing")
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
function buildCliToolInstallCommand(packageManager, { packageName, skipScripts = false }) {
|
|
2442
|
+
if (packageManager === "npm") {
|
|
2443
|
+
return {
|
|
2444
|
+
command: "npm",
|
|
2445
|
+
args: [
|
|
2446
|
+
"install",
|
|
2447
|
+
"-g",
|
|
2448
|
+
...(skipScripts ? ["--ignore-scripts"] : []),
|
|
2449
|
+
packageName,
|
|
2450
|
+
"--force"
|
|
2451
|
+
]
|
|
2452
|
+
};
|
|
2453
|
+
}
|
|
2454
|
+
if (packageManager === "bun") {
|
|
2455
|
+
return {
|
|
2456
|
+
command: "bun",
|
|
2457
|
+
args: [
|
|
2458
|
+
"install",
|
|
2459
|
+
"-g",
|
|
2460
|
+
...(skipScripts ? ["--ignore-scripts"] : []),
|
|
2461
|
+
packageName
|
|
2462
|
+
]
|
|
2463
|
+
};
|
|
2464
|
+
}
|
|
2465
|
+
throw new Error(`Unsupported package manager: ${packageManager}`);
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function getPackageManagerInstallHelp(platform = getPlatform()) {
|
|
2469
|
+
const platformLabel = {
|
|
2470
|
+
darwin: "macOS",
|
|
2471
|
+
win32: "Windows",
|
|
2472
|
+
linux: "Linux"
|
|
2473
|
+
}[platform] || platform;
|
|
2474
|
+
return {
|
|
2475
|
+
platformLabel,
|
|
2476
|
+
mainlandUrl: "https://npmmirror.com/mirrors/node/",
|
|
2477
|
+
officialUrl: "https://nodejs.org/en/download/"
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function getAvailablePackageManagers() {
|
|
2482
|
+
return ["bun", "npm"].filter((packageManager) => commandExists(packageManager));
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function getBunGlobalBinDir(availablePackageManagers = getAvailablePackageManagers()) {
|
|
2486
|
+
if (!availablePackageManagers.includes("bun")) {
|
|
2487
|
+
return undefined;
|
|
2488
|
+
}
|
|
2489
|
+
return readCommandOutput("bun", ["pm", "bin", "-g"]) || join(home, ".bun", "bin");
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
function getNpmGlobalPrefix(availablePackageManagers = getAvailablePackageManagers()) {
|
|
2493
|
+
if (!availablePackageManagers.includes("npm")) {
|
|
2494
|
+
return undefined;
|
|
2495
|
+
}
|
|
2496
|
+
return readCommandOutput("npm", ["prefix", "-g"]);
|
|
1956
2497
|
}
|
|
1957
2498
|
|
|
1958
2499
|
async function installCliTool(tool) {
|
|
@@ -1960,12 +2501,20 @@ async function installCliTool(tool) {
|
|
|
1960
2501
|
claude: {
|
|
1961
2502
|
label: "Claude Code",
|
|
1962
2503
|
command: "claude",
|
|
1963
|
-
packageName: "@anthropic-ai/claude-code"
|
|
2504
|
+
packageName: "@anthropic-ai/claude-code",
|
|
2505
|
+
supportedPackageManagers: ["npm"],
|
|
2506
|
+
installRequirement: "Claude Code 安装需要执行 postinstall;Bun 默认会阻止该脚本,因此需使用 npm。"
|
|
1964
2507
|
},
|
|
1965
2508
|
codex: {
|
|
1966
2509
|
label: "Codex",
|
|
1967
2510
|
command: "codex",
|
|
1968
2511
|
packageName: "@openai/codex"
|
|
2512
|
+
},
|
|
2513
|
+
pi: {
|
|
2514
|
+
label: "Pi",
|
|
2515
|
+
command: "pi",
|
|
2516
|
+
packageName: "@earendil-works/pi-coding-agent",
|
|
2517
|
+
skipScripts: true
|
|
1969
2518
|
}
|
|
1970
2519
|
}[tool];
|
|
1971
2520
|
|
|
@@ -1973,10 +2522,40 @@ async function installCliTool(tool) {
|
|
|
1973
2522
|
throw new Error(`Unsupported tool: ${tool}`);
|
|
1974
2523
|
}
|
|
1975
2524
|
|
|
1976
|
-
const
|
|
2525
|
+
const commandPath = getCommandPath(toolConfig.command);
|
|
2526
|
+
const installed = Boolean(commandPath);
|
|
2527
|
+
const availablePackageManagers = getAvailablePackageManagers();
|
|
2528
|
+
const packageManagerChoice = chooseCliInstallPackageManager({
|
|
2529
|
+
commandPath,
|
|
2530
|
+
availablePackageManagers,
|
|
2531
|
+
bunGlobalBinDir: getBunGlobalBinDir(availablePackageManagers),
|
|
2532
|
+
npmGlobalPrefix: getNpmGlobalPrefix(availablePackageManagers),
|
|
2533
|
+
platform: getPlatform(),
|
|
2534
|
+
nodeAvailable: commandExists("node"),
|
|
2535
|
+
supportedPackageManagers: toolConfig.supportedPackageManagers
|
|
2536
|
+
});
|
|
2537
|
+
|
|
2538
|
+
if (!packageManagerChoice.packageManager) {
|
|
2539
|
+
const help = getPackageManagerInstallHelp();
|
|
2540
|
+
p.log.warn(`未检测到可用于安装 ${toolConfig.label} 的包管理器。`);
|
|
2541
|
+
if (packageManagerChoice.source === "missing-node") {
|
|
2542
|
+
p.log.message(`${toolConfig.label} 是 Node CLI;使用 Bun 安装前也需要先安装 Node.js。`);
|
|
2543
|
+
}
|
|
2544
|
+
if (toolConfig.installRequirement) {
|
|
2545
|
+
p.log.message(toolConfig.installRequirement);
|
|
2546
|
+
}
|
|
2547
|
+
p.log.message(`${help.platformLabel} 可先安装 Node.js/npm,再重新运行安装。`);
|
|
2548
|
+
p.log.message(`中国大陆镜像: ${help.mainlandUrl}`);
|
|
2549
|
+
p.log.message(`官方下载页: ${help.officialUrl}`);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
|
|
1977
2553
|
if (installed) {
|
|
2554
|
+
const managerMessage = packageManagerChoice.source === "existing"
|
|
2555
|
+
? `检测到原安装方式为 ${packageManagerChoice.packageManager}`
|
|
2556
|
+
: `未识别原安装方式,将使用 ${packageManagerChoice.packageManager}`;
|
|
1978
2557
|
const shouldUpdate = await confirmOrCancel({
|
|
1979
|
-
message: `${toolConfig.label}
|
|
2558
|
+
message: `${toolConfig.label} 已检测到(${managerMessage}),是否继续安装/更新?`,
|
|
1980
2559
|
initialValue: false
|
|
1981
2560
|
});
|
|
1982
2561
|
if (!shouldUpdate) {
|
|
@@ -1985,10 +2564,11 @@ async function installCliTool(tool) {
|
|
|
1985
2564
|
}
|
|
1986
2565
|
}
|
|
1987
2566
|
|
|
2567
|
+
const installCommand = buildCliToolInstallCommand(packageManagerChoice.packageManager, toolConfig);
|
|
1988
2568
|
const s = p.spinner();
|
|
1989
|
-
s.start(
|
|
2569
|
+
s.start(`正在使用 ${packageManagerChoice.packageManager} 安装 ${toolConfig.label}...`);
|
|
1990
2570
|
try {
|
|
1991
|
-
await runCommand(
|
|
2571
|
+
await runCommand(installCommand.command, installCommand.args);
|
|
1992
2572
|
s.stop(`${toolConfig.label} 安装完成`);
|
|
1993
2573
|
} catch (e) {
|
|
1994
2574
|
s.cancel(c.red(`${toolConfig.label} 安装失败: ${e.message}`));
|
|
@@ -2020,6 +2600,12 @@ async function runFullInit(options) {
|
|
|
2020
2600
|
if (await confirmOrCancel({ message: "是否配置 Codex 第三方 API?", initialValue: commandExists("codex") })) {
|
|
2021
2601
|
await configureCodexApi();
|
|
2022
2602
|
}
|
|
2603
|
+
if (await confirmOrCancel({ message: "是否安装或更新 Pi CLI?", initialValue: false })) {
|
|
2604
|
+
await installCliTool("pi");
|
|
2605
|
+
}
|
|
2606
|
+
if (await confirmOrCancel({ message: "是否配置 Pi gpt 自定义 API?", initialValue: commandExists("pi") })) {
|
|
2607
|
+
await configurePiApi(options.agentsDir);
|
|
2608
|
+
}
|
|
2023
2609
|
if (await confirmOrCancel({ message: "是否填写 grok-search 环境变量?", initialValue: false })) {
|
|
2024
2610
|
await configureGrokSearchEnv(options.agentsDir);
|
|
2025
2611
|
}
|
|
@@ -2037,6 +2623,59 @@ async function runInteractiveMenu(options) {
|
|
|
2037
2623
|
p.intro(c.bold(c.bgCyan(c.black(" AbelWorkflow Setup "))));
|
|
2038
2624
|
p.log.message(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
|
|
2039
2625
|
|
|
2626
|
+
const buildOption = (d) => {
|
|
2627
|
+
const opt = { value: d.value, label: d.label };
|
|
2628
|
+
if (d.hint) {
|
|
2629
|
+
opt.hint = d.hint;
|
|
2630
|
+
}
|
|
2631
|
+
return opt;
|
|
2632
|
+
};
|
|
2633
|
+
const cliToolMenus = {
|
|
2634
|
+
"pi-cli": {
|
|
2635
|
+
tool: "pi",
|
|
2636
|
+
title: "Pi",
|
|
2637
|
+
actions: {
|
|
2638
|
+
"pi-install": async () => installCliTool("pi"),
|
|
2639
|
+
"pi-api": async () => configurePiApi(options.agentsDir)
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
"codex-cli": {
|
|
2643
|
+
tool: "codex",
|
|
2644
|
+
title: "Codex",
|
|
2645
|
+
actions: {
|
|
2646
|
+
"codex-install": async () => installCliTool("codex"),
|
|
2647
|
+
"codex-api": async () => configureCodexApi()
|
|
2648
|
+
}
|
|
2649
|
+
},
|
|
2650
|
+
"claude-cli": {
|
|
2651
|
+
tool: "claude",
|
|
2652
|
+
title: "Claude Code",
|
|
2653
|
+
actions: {
|
|
2654
|
+
"claude-install": async () => installCliTool("claude"),
|
|
2655
|
+
"claude-api": async () => configureClaudeApi()
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
};
|
|
2659
|
+
const runCliToolMenu = async ({ tool, title, actions }) => {
|
|
2660
|
+
while (true) {
|
|
2661
|
+
const choice = await p.select({
|
|
2662
|
+
message: `请选择 ${title} 操作`,
|
|
2663
|
+
options: buildCliToolMenuDescriptors(tool).map(buildOption),
|
|
2664
|
+
initialValue: `${tool}-install`
|
|
2665
|
+
});
|
|
2666
|
+
|
|
2667
|
+
if (p.isCancel(choice) || choice === "back") {
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
const action = actions[choice];
|
|
2672
|
+
if (!action) {
|
|
2673
|
+
p.log.warn(`未知 CLI 工具菜单选项: ${choice}`);
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
await action();
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2040
2679
|
const menuActions = {
|
|
2041
2680
|
"full-init": async () => runFullInit(options),
|
|
2042
2681
|
install: async () => installManagedWorkflow({
|
|
@@ -2047,18 +2686,9 @@ async function runInteractiveMenu(options) {
|
|
|
2047
2686
|
"grok-search": async () => configureGrokSearchEnv(options.agentsDir),
|
|
2048
2687
|
context7: async () => configureContext7Env(options.agentsDir),
|
|
2049
2688
|
"prompt-enhancer": async () => configurePromptEnhancerEnv(options.agentsDir),
|
|
2050
|
-
"
|
|
2051
|
-
"
|
|
2052
|
-
"
|
|
2053
|
-
"codex-api": async () => configureCodexApi()
|
|
2054
|
-
};
|
|
2055
|
-
|
|
2056
|
-
const buildOption = (d) => {
|
|
2057
|
-
const opt = { value: d.value, label: d.label };
|
|
2058
|
-
if (d.hint) {
|
|
2059
|
-
opt.hint = d.hint;
|
|
2060
|
-
}
|
|
2061
|
-
return opt;
|
|
2689
|
+
"pi-cli": async () => runCliToolMenu(cliToolMenus["pi-cli"]),
|
|
2690
|
+
"codex-cli": async () => runCliToolMenu(cliToolMenus["codex-cli"]),
|
|
2691
|
+
"claude-cli": async () => runCliToolMenu(cliToolMenus["claude-cli"])
|
|
2062
2692
|
};
|
|
2063
2693
|
|
|
2064
2694
|
while (true) {
|
|
@@ -2169,11 +2799,17 @@ async function main() {
|
|
|
2169
2799
|
|
|
2170
2800
|
export {
|
|
2171
2801
|
applyClaudePermissionFeature,
|
|
2802
|
+
buildCliToolInstallCommand,
|
|
2172
2803
|
buildDefaultClaudeSettings,
|
|
2173
2804
|
buildCodexConfigContent,
|
|
2805
|
+
buildPiModelsConfig,
|
|
2806
|
+
buildPiSettingsConfig,
|
|
2807
|
+
chooseCliInstallPackageManager,
|
|
2808
|
+
getPackageManagerInstallHelp,
|
|
2174
2809
|
getAugmentContextEnginePromptOptions,
|
|
2175
2810
|
getRunCommandSpawnOptions,
|
|
2176
2811
|
hasPromptEnhancerApiConfig,
|
|
2812
|
+
inferPackageManagerFromCommandPath,
|
|
2177
2813
|
main,
|
|
2178
2814
|
mergeCodexAuthData,
|
|
2179
2815
|
mergeClaudeSettingsWithDefaults,
|
|
@@ -2181,5 +2817,8 @@ export {
|
|
|
2181
2817
|
resolveAugmentContextEngineFeature,
|
|
2182
2818
|
resolvePromptEnhancerMode,
|
|
2183
2819
|
resolveExistingCodexApiConfig,
|
|
2820
|
+
parsePiModelIds,
|
|
2821
|
+
resolveExistingPiApiConfig,
|
|
2822
|
+
stripJsonComments,
|
|
2184
2823
|
updateTomlSectionFields
|
|
2185
2824
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "abelworkflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"README.md",
|
|
18
18
|
"commands",
|
|
19
19
|
"skills",
|
|
20
|
+
"extensions",
|
|
20
21
|
".skill-lock.json",
|
|
21
22
|
".gitignore"
|
|
22
23
|
],
|