abelworkflow 0.6.5 → 0.7.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/lib/cli.mjs CHANGED
@@ -3,17 +3,22 @@ import { cp, link, lstat, mkdir, readFile, readdir, readlink, realpath, rename,
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
- import { createInterface } from "node:readline/promises";
7
6
  import { fileURLToPath } from "node:url";
7
+ import * as p from "@clack/prompts";
8
+ import c from "picocolors";
8
9
  import {
9
10
  assertInteractiveMenuSupported,
11
+ assertNotCancelled,
12
+ CancelledError,
13
+ confirmOrCancel,
10
14
  getRunCommandSpawnOptions,
11
15
  interactiveMenuDefaultValue,
12
16
  interactiveMenuDescriptors,
13
17
  parseArgs,
14
- resolvePromptValue,
15
- resolveSelectValue,
16
- shouldUseVisibleSecretFallback
18
+ required,
19
+ requiredUnlessExisting,
20
+ resolvePasswordValue,
21
+ selectOrCancel
17
22
  } from "./cli/logic.mjs";
18
23
 
19
24
  const __filename = fileURLToPath(import.meta.url);
@@ -99,19 +104,21 @@ const ignoredSkillPathPatterns = [
99
104
  ];
100
105
 
101
106
  function printHelp() {
102
- console.log(`AbelWorkflow installer
103
-
104
- Usage:
105
- npx abelworkflow
106
- npx abelworkflow init
107
- npx abelworkflow install
108
- npx abelworkflow install --force
109
- npx abelworkflow install --link-only
110
- npx abelworkflow install --agents-dir /custom/path
111
-
112
- Default behavior:
107
+ console.log(`${c.bold("AbelWorkflow")} ${c.cyan("installer")}
108
+
109
+ ${c.bold("Usage:")}
110
+ ${c.cyan("npx abelworkflow")}
111
+ ${c.cyan("npx abelworkflow init")}
112
+ ${c.cyan("npx abelworkflow install")}
113
+ ${c.cyan("npx abelworkflow install --force")}
114
+ ${c.cyan("npx abelworkflow install --link-only")}
115
+ ${c.cyan("npx abelworkflow install --agents-dir /custom/path")}
116
+ ${c.cyan("npx abelworkflow --non-interactive")}
117
+
118
+ ${c.bold("Default behavior:")}
113
119
  - npx abelworkflow: open the interactive setup menu.
114
120
  - npx abelworkflow install: sync managed files and links explicitly.
121
+ - --non-interactive: auto-execute install (skip interactive menu); auto-enabled in CI.
115
122
  `);
116
123
  }
117
124
 
@@ -178,18 +185,18 @@ async function backupExistingPath(targetPath) {
178
185
  const backupPath = await createBackupPath(targetPath);
179
186
  await cp(targetPath, backupPath, { recursive: true, force: false });
180
187
  createdBackupPaths.add(targetPath);
181
- console.log(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
188
+ p.log.message(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
182
189
  return backupPath;
183
190
  }
184
191
 
185
- async function backupIfNeeded(targetPath, force) {
192
+ async function backupIfNeeded(targetPath) {
186
193
  if (!(await pathExists(targetPath))) {
187
194
  return null;
188
195
  }
189
196
 
190
197
  const backupPath = await createBackupPath(targetPath);
191
198
  await rename(targetPath, backupPath);
192
- console.log(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
199
+ p.log.message(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
193
200
  return backupPath;
194
201
  }
195
202
 
@@ -449,7 +456,7 @@ async function createSymlink(targetPath, sourcePath, linkType, kind) {
449
456
  await symlink(sourcePath, targetPath, linkType);
450
457
  }
451
458
 
452
- async function ensureManagedLink(targetPath, sourcePath, kind, force, previousLinkedTargets) {
459
+ async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTargets) {
453
460
  await mkdir(dirname(targetPath), { recursive: true });
454
461
  const sourceResolved = resolve(sourcePath);
455
462
  const sourceExists = await pathTargetExists(sourcePath);
@@ -487,7 +494,7 @@ async function ensureManagedLink(targetPath, sourcePath, kind, force, previousLi
487
494
  if (wasPreviouslyManaged) {
488
495
  await rm(targetPath, { recursive: true, force: true });
489
496
  } else {
490
- await backupIfNeeded(targetPath, force);
497
+ await backupIfNeeded(targetPath);
491
498
  }
492
499
  } else if (!sourceExists) {
493
500
  return { targetPath, status: "skipped" };
@@ -525,7 +532,7 @@ function shouldCopyManagedFile(error) {
525
532
  return ["EPERM", "EACCES", "EXDEV", "EINVAL", "UNKNOWN"].includes(error?.code);
526
533
  }
527
534
 
528
- async function linkSkillDirectories(baseDir, agentsDir, force, previousLinkedTargets) {
535
+ async function linkSkillDirectories(baseDir, agentsDir, previousLinkedTargets) {
529
536
  const results = [];
530
537
  const skillsRoot = join(agentsDir, "skills");
531
538
  const skillNames = (await getDirectoryNames(skillsRoot)).filter((skillName) => skillName !== ".system");
@@ -536,7 +543,6 @@ async function linkSkillDirectories(baseDir, agentsDir, force, previousLinkedTar
536
543
  join(baseDir, "skills", skillName),
537
544
  join(skillsRoot, skillName),
538
545
  "dir",
539
- force,
540
546
  previousLinkedTargets
541
547
  )
542
548
  );
@@ -664,19 +670,7 @@ function isWithinManagedRoot(targetPath, managedSourceRoot) {
664
670
  return relativePath !== ".." && !relativePath.startsWith(`..${isWindows() ? "\\" : "/"}`);
665
671
  }
666
672
 
667
- function getResultMarker(status) {
668
- if (status === "unchanged") {
669
- return "=";
670
- }
671
-
672
- if (status === "removed") {
673
- return "-";
674
- }
675
-
676
- return "+";
677
- }
678
-
679
- async function linkClaude(agentsDir, force, previousLinkedTargets) {
673
+ async function linkClaude(agentsDir, previousLinkedTargets) {
680
674
  const claudeDir = join(home, ".claude");
681
675
  await mkdir(claudeDir, { recursive: true });
682
676
  await removeIfNotDirectory(join(claudeDir, "commands"));
@@ -689,21 +683,19 @@ async function linkClaude(agentsDir, force, previousLinkedTargets) {
689
683
  join(claudeDir, "CLAUDE.md"),
690
684
  join(agentsDir, "AGENTS.md"),
691
685
  "file",
692
- force,
693
686
  previousLinkedTargets
694
687
  ),
695
688
  await ensureManagedLink(
696
689
  join(claudeDir, "commands", "oc"),
697
690
  join(agentsDir, "commands", "oc"),
698
691
  "dir",
699
- force,
700
692
  previousLinkedTargets
701
693
  ),
702
- ...(await linkSkillDirectories(claudeDir, agentsDir, force, previousLinkedTargets))
694
+ ...(await linkSkillDirectories(claudeDir, agentsDir, previousLinkedTargets))
703
695
  ];
704
696
  }
705
697
 
706
- async function linkCodex(agentsDir, force, previousLinkedTargets) {
698
+ async function linkCodex(agentsDir, previousLinkedTargets) {
707
699
  const results = [];
708
700
  const codexDir = join(home, ".codex");
709
701
  await mkdir(codexDir, { recursive: true });
@@ -717,11 +709,10 @@ async function linkCodex(agentsDir, force, previousLinkedTargets) {
717
709
  join(codexDir, "AGENTS.md"),
718
710
  join(agentsDir, "AGENTS.md"),
719
711
  "file",
720
- force,
721
712
  previousLinkedTargets
722
713
  )
723
714
  );
724
- results.push(...(await linkSkillDirectories(codexDir, agentsDir, force, previousLinkedTargets)));
715
+ results.push(...(await linkSkillDirectories(codexDir, agentsDir, previousLinkedTargets)));
725
716
 
726
717
  const commandFiles = await getCommandNames(join(agentsDir, "commands", "oc"));
727
718
  results.push(
@@ -738,7 +729,6 @@ async function linkCodex(agentsDir, force, previousLinkedTargets) {
738
729
  join(codexDir, "prompts", fileName),
739
730
  join(agentsDir, "commands", "oc", fileName),
740
731
  "file",
741
- force,
742
732
  previousLinkedTargets
743
733
  )
744
734
  );
@@ -750,9 +740,17 @@ async function linkCodex(agentsDir, force, previousLinkedTargets) {
750
740
  async function installManagedWorkflow(options) {
751
741
  let previousMetadata = {};
752
742
  let managedChildren = {};
743
+ const s = p.spinner();
753
744
 
754
745
  if (!options.relinkOnly) {
755
- ({ previousMetadata, managedChildren } = await syncManagedFiles(options.agentsDir));
746
+ s.start("正在同步工作流文件...");
747
+ try {
748
+ ({ previousMetadata, managedChildren } = await syncManagedFiles(options.agentsDir));
749
+ } catch (e) {
750
+ s.cancel(c.red(`同步失败: ${e.message}`));
751
+ throw e;
752
+ }
753
+ s.stop("工作流文件已同步");
756
754
  } else if (!(await pathExists(options.agentsDir))) {
757
755
  throw new Error(`${options.agentsDir} does not exist; remove --link-only or install first`);
758
756
  } else {
@@ -761,8 +759,18 @@ async function installManagedWorkflow(options) {
761
759
  }
762
760
 
763
761
  const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
764
- const claudeResults = await linkClaude(options.agentsDir, options.force, previousLinkedTargets);
765
- const codexResults = await linkCodex(options.agentsDir, options.force, previousLinkedTargets);
762
+ s.start("正在链接 Claude / Codex...");
763
+ let claudeResults;
764
+ let codexResults;
765
+ try {
766
+ claudeResults = await linkClaude(options.agentsDir, previousLinkedTargets);
767
+ codexResults = await linkCodex(options.agentsDir, previousLinkedTargets);
768
+ } catch (e) {
769
+ s.cancel(c.red(`链接失败: ${e.message}`));
770
+ throw e;
771
+ }
772
+ s.stop("链接完成");
773
+
766
774
  const linkedTargets = Object.fromEntries(
767
775
  [...claudeResults, ...codexResults]
768
776
  .filter((result) => result.sourcePath)
@@ -783,14 +791,16 @@ async function installManagedWorkflow(options) {
783
791
  linkedTargets
784
792
  });
785
793
 
786
- console.log(`Installed AbelWorkflow into ${options.agentsDir}`);
787
- console.log("");
788
- console.log("Linked targets:");
789
- for (const result of [...claudeResults, ...codexResults]) {
790
- console.log(`- ${getResultMarker(result.status)} ${result.targetPath}`);
791
- }
792
- console.log("");
793
- console.log("Done. Re-run `npx abelworkflow@latest` to update the managed files.");
794
+ const resultLines = [...claudeResults, ...codexResults].map((result) => {
795
+ const icon = result.status === "unchanged" ? c.gray("=")
796
+ : result.status === "removed" ? c.yellow("−")
797
+ : c.green("+");
798
+ return `${icon} ${pathToLabel(result.targetPath)}`;
799
+ }).join("\n");
800
+
801
+ p.note(resultLines, "链接结果");
802
+ p.log.step(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
803
+ p.log.message(`完成后可运行 ${c.cyan("npx abelworkflow@latest")} 更新托管文件`);
794
804
  }
795
805
 
796
806
  async function readJsonFileSafe(path, fallback = {}) {
@@ -882,121 +892,6 @@ async function updateDotenvFile(path, updates) {
882
892
  await writeFile(path, renderDotenv(current), "utf8");
883
893
  }
884
894
 
885
- function currentChoiceIndex(choices, defaultValue) {
886
- if (defaultValue === undefined) {
887
- return -1;
888
- }
889
- return choices.findIndex((choice) => choice.value === defaultValue);
890
- }
891
-
892
- async function setTerminalEcho(enabled) {
893
- if (!input.isTTY || isWindows()) {
894
- return;
895
- }
896
-
897
- const result = spawnSync("stty", [enabled ? "echo" : "-echo"], { stdio: ["inherit", "ignore", "ignore"] });
898
- if (result.error) {
899
- throw result.error;
900
- }
901
- }
902
-
903
- async function promptText(message, options = {}) {
904
- const { defaultValue, allowEmpty = false } = options;
905
-
906
- while (true) {
907
- const suffix = defaultValue !== undefined && defaultValue !== ""
908
- ? ` [${defaultValue}]`
909
- : "";
910
- const rl = createInterface({ input, output });
911
- let answer;
912
- try {
913
- answer = await rl.question(`${message}${suffix}: `);
914
- } finally {
915
- rl.close();
916
- }
917
-
918
- const resolved = resolvePromptValue(answer, { defaultValue, allowEmpty });
919
- if (resolved.ok) {
920
- return resolved.value;
921
- }
922
- console.log(resolved.error);
923
- }
924
- }
925
-
926
- async function promptSecret(message, options = {}) {
927
- const { defaultValue, allowEmpty = false } = options;
928
-
929
- if (shouldUseVisibleSecretFallback({ inputIsTTY: input.isTTY, platform: getPlatform() })) {
930
- while (true) {
931
- const suffix = defaultValue !== undefined && defaultValue !== ""
932
- ? " [直接回车保留现有值]"
933
- : "";
934
- const rl = createInterface({ input, output });
935
- let answer;
936
- try {
937
- answer = await rl.question(`${message}${suffix}: `);
938
- } finally {
939
- rl.close();
940
- }
941
-
942
- const resolved = resolvePromptValue(answer, { defaultValue, allowEmpty });
943
- if (resolved.ok) {
944
- return resolved.value;
945
- }
946
- console.log(resolved.error);
947
- }
948
- }
949
-
950
- while (true) {
951
- const suffix = defaultValue !== undefined && defaultValue !== ""
952
- ? " [直接回车保留现有值]"
953
- : "";
954
- const rl = createInterface({ input, output, terminal: true });
955
- let answer;
956
- try {
957
- await setTerminalEcho(false);
958
- answer = await rl.question(`${message}${suffix}: `);
959
- output.write("\n");
960
- } finally {
961
- await setTerminalEcho(true);
962
- rl.close();
963
- }
964
-
965
- const resolved = resolvePromptValue(answer, { defaultValue, allowEmpty });
966
- if (resolved.ok) {
967
- return resolved.value;
968
- }
969
- console.log(resolved.error);
970
- }
971
- }
972
-
973
- async function promptSelect(message, choices, options = {}) {
974
- const defaultIndex = currentChoiceIndex(choices, options.defaultValue);
975
- console.log(`\n${message}`);
976
- choices.forEach((choice, index) => {
977
- const defaultMarker = index === defaultIndex ? " [默认]" : "";
978
- console.log(` ${index + 1}. ${choice.label}${defaultMarker}`);
979
- });
980
-
981
- while (true) {
982
- const fallbackValue = defaultIndex >= 0 ? String(defaultIndex + 1) : undefined;
983
- const answer = await promptText("请输入序号", { defaultValue: fallbackValue, allowEmpty: defaultIndex >= 0 });
984
- const resolved = resolveSelectValue(answer, choices);
985
- if (resolved.ok) {
986
- return resolved.value;
987
- }
988
- console.log(resolved.error);
989
- }
990
- }
991
-
992
- async function promptConfirm(message, defaultValue = true) {
993
- const value = await promptSelect(message, [
994
- { value: true, label: "是" },
995
- { value: false, label: "否" }
996
- ], { defaultValue });
997
- return value;
998
- }
999
-
1000
895
  function commandExists(command) {
1001
896
  const checker = isWindows() ? "where" : "which";
1002
897
  const result = spawnSync(checker, [command], { stdio: "ignore" });
@@ -1017,22 +912,12 @@ async function runCommand(command, args) {
1017
912
  });
1018
913
  }
1019
914
 
1020
- function sanitizeProviderId(name) {
1021
- return name
1022
- .trim()
1023
- .toLowerCase()
1024
- .replace(/[\s.]+/gu, "-")
1025
- .replace(/[^a-z0-9_-]/gu, "")
1026
- .replace(/-+/gu, "-")
1027
- .replace(/^-|-$/gu, "") || "abelworkflow";
1028
- }
1029
-
1030
915
  async function ensureWorkflowPresent(agentsDir) {
1031
916
  if (await pathExists(join(agentsDir, "AGENTS.md"))) {
1032
917
  return;
1033
918
  }
1034
919
 
1035
- console.log("未检测到已安装的 AbelWorkflow,先执行一次工作流同步。");
920
+ p.log.message("未检测到已安装的 AbelWorkflow,先执行一次工作流同步。");
1036
921
  await installManagedWorkflow({
1037
922
  agentsDir,
1038
923
  force: false,
@@ -1044,101 +929,139 @@ async function configureGrokSearchEnv(agentsDir) {
1044
929
  await ensureWorkflowPresent(agentsDir);
1045
930
  const envPath = join(agentsDir, "skills", "grok-search", ".env");
1046
931
  const existing = await readDotenvFile(envPath);
1047
- const baseUrl = await promptText("Grok API URL", {
1048
- defaultValue: existing.GROK_API_URL || "https://api.x.ai/v1"
932
+ const baseUrl = await p.text({
933
+ message: "Grok API URL",
934
+ defaultValue: existing.GROK_API_URL || "https://api.x.ai/v1",
935
+ validate: required()
1049
936
  });
1050
- const apiKey = await promptSecret("Grok API Key", {
1051
- defaultValue: existing.GROK_API_KEY || undefined
937
+ assertNotCancelled(baseUrl);
938
+
939
+ const apiKey = await p.password({
940
+ message: "Grok API Key(输入 - 清除)",
941
+ mask: "*",
942
+ defaultValue: existing.GROK_API_KEY || undefined,
943
+ validate: requiredUnlessExisting(existing.GROK_API_KEY, "Grok API Key 不能为空")
1052
944
  });
1053
- const model = await promptText("Grok 默认模型", {
1054
- defaultValue: existing.GROK_MODEL || "grok-4-fast"
945
+ assertNotCancelled(apiKey);
946
+ const finalApiKey = resolvePasswordValue(apiKey, existing.GROK_API_KEY);
947
+
948
+ const model = await p.text({
949
+ message: "Grok 默认模型",
950
+ defaultValue: existing.GROK_MODEL || "grok-4-fast",
951
+ validate: required()
952
+ });
953
+ assertNotCancelled(model);
954
+
955
+ const useTavily = await confirmOrCancel({
956
+ message: "是否同时配置 Tavily 作为额外搜索源?",
957
+ initialValue: Boolean(existing.TAVILY_API_KEY)
1055
958
  });
1056
- const useTavily = await promptConfirm("是否同时配置 Tavily 作为额外搜索源?", Boolean(existing.TAVILY_API_KEY));
959
+
1057
960
  const tavilyKey = useTavily
1058
- ? await promptSecret("Tavily API Key", { defaultValue: existing.TAVILY_API_KEY || undefined })
961
+ ? await p.password({
962
+ message: "Tavily API Key(输入 - 清除)",
963
+ mask: "*",
964
+ defaultValue: existing.TAVILY_API_KEY || undefined,
965
+ validate: requiredUnlessExisting(existing.TAVILY_API_KEY, "Tavily API Key 不能为空")
966
+ })
1059
967
  : "";
968
+ if (useTavily) assertNotCancelled(tavilyKey);
969
+ const finalTavilyKey = useTavily ? resolvePasswordValue(tavilyKey, existing.TAVILY_API_KEY) : null;
1060
970
 
1061
971
  await updateDotenvFile(envPath, {
1062
972
  GROK_API_URL: baseUrl,
1063
- GROK_API_KEY: apiKey,
973
+ GROK_API_KEY: finalApiKey,
1064
974
  GROK_MODEL: model,
1065
- TAVILY_API_KEY: useTavily ? tavilyKey : null,
975
+ TAVILY_API_KEY: finalTavilyKey,
1066
976
  TAVILY_ENABLED: useTavily ? "true" : null
1067
977
  });
1068
978
 
1069
- console.log(`已写入 ${pathToLabel(envPath)}`);
979
+ p.log.step(`已写入 ${pathToLabel(envPath)}`);
1070
980
  }
1071
981
 
1072
982
  async function configureContext7Env(agentsDir) {
1073
983
  await ensureWorkflowPresent(agentsDir);
1074
984
  const envPath = join(agentsDir, "skills", "context7-auto-research", ".env");
1075
985
  const existing = await readDotenvFile(envPath);
1076
- const apiKey = await promptSecret("Context7 API Key", {
1077
- defaultValue: existing.CONTEXT7_API_KEY || undefined,
1078
- allowEmpty: true
986
+ const apiKey = await p.password({
987
+ message: "Context7 API Key (可选,输入 - 清除)",
988
+ mask: "*",
989
+ defaultValue: existing.CONTEXT7_API_KEY || undefined
1079
990
  });
991
+ assertNotCancelled(apiKey);
992
+ const finalApiKey = resolvePasswordValue(apiKey, existing.CONTEXT7_API_KEY);
1080
993
 
1081
994
  await updateDotenvFile(envPath, {
1082
- CONTEXT7_API_KEY: apiKey || null
995
+ CONTEXT7_API_KEY: finalApiKey
1083
996
  });
1084
997
 
1085
- console.log(`已写入 ${pathToLabel(envPath)}`);
998
+ p.log.step(`已写入 ${pathToLabel(envPath)}`);
999
+ }
1000
+
1001
+ function hasPromptEnhancerApiConfig(config) {
1002
+ return [config.PE_API_URL, config.PE_API_KEY, config.PE_MODEL]
1003
+ .every((value) => typeof value === "string" && value.trim() !== "");
1086
1004
  }
1087
1005
 
1088
1006
  function resolvePromptEnhancerMode(existing) {
1089
- if (existing.ANTHROPIC_API_KEY) {
1090
- return "anthropic";
1091
- }
1092
- if (existing.OPENAI_API_KEY) {
1093
- return "openai";
1094
- }
1095
- return "local";
1007
+ return hasPromptEnhancerApiConfig(existing) ? "openai-compatible" : "agent";
1096
1008
  }
1097
1009
 
1098
1010
  async function configurePromptEnhancerEnv(agentsDir) {
1099
1011
  await ensureWorkflowPresent(agentsDir);
1100
1012
  const envPath = join(agentsDir, "skills", "prompt-enhancer", ".env");
1101
1013
  const existing = await readDotenvFile(envPath);
1102
- const mode = await promptSelect("请选择 prompt-enhancer 使用的提供方", [
1103
- { value: "anthropic", label: "Anthropic 兼容 Key" },
1104
- { value: "openai", label: "OpenAI 兼容 Key" },
1105
- { value: "local", label: "仅保留本地模板兜底,不写 API Key" }
1106
- ], { defaultValue: resolvePromptEnhancerMode(existing) });
1107
-
1108
- if (mode === "anthropic") {
1109
- const apiKey = await promptSecret("ANTHROPIC_API_KEY", {
1110
- defaultValue: existing.ANTHROPIC_API_KEY || undefined
1111
- });
1112
- const model = await promptText("PE_MODEL", {
1113
- defaultValue: existing.PE_MODEL || "claude-sonnet-4-20250514"
1114
- });
1014
+ const mode = await selectOrCancel({
1015
+ message: "请选择 prompt-enhancer 的运行方式",
1016
+ options: [
1017
+ { value: "openai-compatible", label: "第三方 OpenAI 兼容接口" },
1018
+ { value: "agent", label: "直接使用当前 Agent" }
1019
+ ],
1020
+ initialValue: resolvePromptEnhancerMode(existing)
1021
+ });
1115
1022
 
1116
- await updateDotenvFile(envPath, {
1117
- ANTHROPIC_API_KEY: apiKey,
1118
- OPENAI_API_KEY: null,
1119
- PE_MODEL: model
1023
+ if (mode === "openai-compatible") {
1024
+ const apiUrl = await p.text({
1025
+ message: "PE_API_URL",
1026
+ defaultValue: existing.PE_API_URL || undefined,
1027
+ validate: required()
1120
1028
  });
1121
- } else if (mode === "openai") {
1122
- const apiKey = await promptSecret("OPENAI_API_KEY", {
1123
- defaultValue: existing.OPENAI_API_KEY || undefined
1029
+ assertNotCancelled(apiUrl);
1030
+
1031
+ const apiKey = await p.password({
1032
+ message: "PE_API_KEY(输入 - 清除)",
1033
+ mask: "*",
1034
+ defaultValue: existing.PE_API_KEY || undefined,
1035
+ validate: requiredUnlessExisting(existing.PE_API_KEY, "PE_API_KEY 不能为空")
1124
1036
  });
1125
- const model = await promptText("PE_MODEL", {
1126
- defaultValue: existing.PE_MODEL || "gpt-4o"
1037
+ assertNotCancelled(apiKey);
1038
+ const finalApiKey = resolvePasswordValue(apiKey, existing.PE_API_KEY);
1039
+
1040
+ const model = await p.text({
1041
+ message: "PE_MODEL",
1042
+ defaultValue: existing.PE_MODEL || undefined,
1043
+ validate: required()
1127
1044
  });
1045
+ assertNotCancelled(model);
1128
1046
 
1129
1047
  await updateDotenvFile(envPath, {
1130
- OPENAI_API_KEY: apiKey,
1048
+ PE_API_URL: apiUrl,
1049
+ PE_API_KEY: finalApiKey,
1050
+ PE_MODEL: model,
1131
1051
  ANTHROPIC_API_KEY: null,
1132
- PE_MODEL: model
1052
+ OPENAI_API_KEY: null
1133
1053
  });
1134
1054
  } else {
1135
1055
  await updateDotenvFile(envPath, {
1056
+ PE_API_URL: null,
1057
+ PE_API_KEY: null,
1058
+ PE_MODEL: null,
1136
1059
  ANTHROPIC_API_KEY: null,
1137
1060
  OPENAI_API_KEY: null
1138
1061
  });
1139
1062
  }
1140
1063
 
1141
- console.log(`已写入 ${pathToLabel(envPath)}`);
1064
+ p.log.step(`已写入 ${pathToLabel(envPath)}`);
1142
1065
  }
1143
1066
 
1144
1067
  function mergeClaudeSettingsWithDefaults(settings) {
@@ -1198,28 +1121,46 @@ function ensureApprovedClaudeApiKey(config, apiKey) {
1198
1121
  async function configureClaudeApi() {
1199
1122
  const settings = await readJsonFileSafe(claudeSettingsPath, {});
1200
1123
  const existing = getExistingClaudeApiConfig(settings);
1201
- const authType = await promptSelect("Claude Code 第三方 API 认证方式", [
1202
- { value: "api_key", label: "API Key" },
1203
- { value: "auth_token", label: "Auth Token" }
1204
- ], { defaultValue: existing.authType });
1205
- const baseUrl = await promptText("Claude Code Base URL", {
1206
- defaultValue: existing.baseUrl
1124
+ const authType = await selectOrCancel({
1125
+ message: "Claude Code 第三方 API 认证方式",
1126
+ options: [
1127
+ { value: "api_key", label: "API Key" },
1128
+ { value: "auth_token", label: "Auth Token" }
1129
+ ],
1130
+ initialValue: existing.authType
1207
1131
  });
1208
- const key = await promptSecret(authType === "auth_token" ? "Claude Code Auth Token" : "Claude Code API Key", {
1209
- defaultValue: existing.key || undefined
1132
+
1133
+ const baseUrl = await p.text({
1134
+ message: "Claude Code Base URL",
1135
+ defaultValue: existing.baseUrl,
1136
+ validate: required()
1137
+ });
1138
+ assertNotCancelled(baseUrl);
1139
+
1140
+ const key = await p.password({
1141
+ message: authType === "auth_token" ? "Claude Code Auth Token(输入 - 清除)" : "Claude Code API Key(输入 - 清除)",
1142
+ mask: "*",
1143
+ defaultValue: existing.key || undefined,
1144
+ validate: requiredUnlessExisting(existing.key, "API Key / Auth Token 不能为空")
1210
1145
  });
1211
- const model = await promptText("Claude Code 模型", {
1212
- defaultValue: existing.model || undefined
1146
+ assertNotCancelled(key);
1147
+ const finalKey = resolvePasswordValue(key, existing.key);
1148
+
1149
+ const model = await p.text({
1150
+ message: "Claude Code 模型",
1151
+ defaultValue: existing.model || undefined,
1152
+ validate: required()
1213
1153
  });
1154
+ assertNotCancelled(model);
1214
1155
 
1215
1156
  const nextSettings = mergeClaudeSettingsWithDefaults(settings);
1216
1157
  nextSettings.env.ANTHROPIC_BASE_URL = baseUrl;
1217
1158
 
1218
1159
  if (authType === "auth_token") {
1219
- nextSettings.env.ANTHROPIC_AUTH_TOKEN = key;
1160
+ nextSettings.env.ANTHROPIC_AUTH_TOKEN = finalKey;
1220
1161
  delete nextSettings.env.ANTHROPIC_API_KEY;
1221
1162
  } else {
1222
- nextSettings.env.ANTHROPIC_API_KEY = key;
1163
+ nextSettings.env.ANTHROPIC_API_KEY = finalKey;
1223
1164
  delete nextSettings.env.ANTHROPIC_AUTH_TOKEN;
1224
1165
  }
1225
1166
  for (const field of claudeModelEnvKeys) {
@@ -1230,10 +1171,10 @@ async function configureClaudeApi() {
1230
1171
 
1231
1172
  const metaConfig = await readJsonFileSafe(claudeMetaConfigPath, {});
1232
1173
  metaConfig.hasCompletedOnboarding = true;
1233
- ensureApprovedClaudeApiKey(metaConfig, key);
1174
+ ensureApprovedClaudeApiKey(metaConfig, finalKey);
1234
1175
  await writeJsonFileWithBackup(claudeMetaConfigPath, metaConfig);
1235
1176
 
1236
- console.log(`已更新 ${pathToLabel(claudeSettingsPath)} (${authType}, ${baseUrl}, ${maskSecret(key)})`);
1177
+ p.log.step(`已更新 ${pathToLabel(claudeSettingsPath)} (${authType}, ${baseUrl}, ${maskSecret(finalKey)})`);
1237
1178
  }
1238
1179
 
1239
1180
  function updateTopLevelTomlField(content, field, value) {
@@ -1723,13 +1664,24 @@ async function configureCodexApi() {
1723
1664
  const existing = await getExistingCodexApiConfig();
1724
1665
  const providerId = existing.providerId || "abelworkflow";
1725
1666
  const providerName = existing.providerName || providerId;
1726
- const baseUrl = await promptText("Codex Base URL", {
1727
- defaultValue: existing.baseUrl
1667
+ const baseUrl = await p.text({
1668
+ message: "Codex Base URL",
1669
+ defaultValue: existing.baseUrl,
1670
+ validate: required()
1728
1671
  });
1729
- const apiKey = await promptSecret("Codex 第三方 API Key", {
1730
- defaultValue: existing.apiKey || undefined
1672
+ assertNotCancelled(baseUrl);
1673
+
1674
+ const apiKey = await p.password({
1675
+ message: "Codex 第三方 API Key(输入 - 清除)",
1676
+ mask: "*",
1677
+ defaultValue: existing.apiKey || undefined,
1678
+ validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
1731
1679
  });
1732
- const shouldDeploySubagents = await promptConfirm("是否部署 Codex subagents 配置?", true);
1680
+ assertNotCancelled(apiKey);
1681
+ const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
1682
+
1683
+ const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
1684
+
1733
1685
  const envKey = existing.envKey || "OPENAI_API_KEY";
1734
1686
  const currentContent = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
1735
1687
  const templateContent = await loadBundledCodexConfigTemplate();
@@ -1747,16 +1699,16 @@ async function configureCodexApi() {
1747
1699
  await mkdir(dirname(codexConfigPath), { recursive: true });
1748
1700
  await writeFile(codexConfigPath, content, "utf8");
1749
1701
 
1750
- const auth = mergeCodexAuthData(await readJsonFileSafe(codexAuthPath, {}), envKey, apiKey, existing.legacyEnvKeys || []);
1702
+ const auth = mergeCodexAuthData(await readJsonFileSafe(codexAuthPath, {}), envKey, finalApiKey, existing.legacyEnvKeys || []);
1751
1703
  await writeJsonFileWithBackup(codexAuthPath, auth);
1752
1704
 
1753
- console.log(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
1754
- console.log(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(apiKey)})`);
1705
+ p.log.step(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
1706
+ p.log.step(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(finalApiKey)})`);
1755
1707
  if (shouldDeploySubagents) {
1756
1708
  const deployed = await deployBundledCodexAgents();
1757
- console.log(`已部署 ${deployed.length} 个 Codex subagents 到 ${pathToLabel(join(home, ".codex", "agents"))}`);
1709
+ p.log.step(`已部署 ${deployed.length} 个 Codex subagents 到 ${pathToLabel(join(home, ".codex", "agents"))}`);
1758
1710
  } else {
1759
- console.log("已跳过 Codex subagents 部署。");
1711
+ p.log.message("已跳过 Codex subagents 部署。");
1760
1712
  }
1761
1713
  }
1762
1714
 
@@ -1827,16 +1779,25 @@ async function installCliTool(tool) {
1827
1779
 
1828
1780
  const installed = commandExists(toolConfig.command);
1829
1781
  if (installed) {
1830
- const shouldUpdate = await promptConfirm(`${toolConfig.label} 已检测到,是否继续执行 npm 强制安装/更新?`, false);
1782
+ const shouldUpdate = await confirmOrCancel({
1783
+ message: `${toolConfig.label} 已检测到,是否继续执行 npm 强制安装/更新?`,
1784
+ initialValue: false
1785
+ });
1831
1786
  if (!shouldUpdate) {
1832
- console.log(`跳过 ${toolConfig.label} 安装。`);
1787
+ p.log.message(`跳过 ${toolConfig.label} 安装。`);
1833
1788
  return;
1834
1789
  }
1835
1790
  }
1836
1791
 
1837
- console.log(`开始安装 ${toolConfig.label}...`);
1838
- await runCommand("npm", ["install", "-g", toolConfig.packageName, "--force"]);
1839
- console.log(`${toolConfig.label} 安装完成。`);
1792
+ const s = p.spinner();
1793
+ s.start(`正在安装 ${toolConfig.label}...`);
1794
+ try {
1795
+ await runCommand("npm", ["install", "-g", toolConfig.packageName, "--force"]);
1796
+ s.stop(`${toolConfig.label} 安装完成`);
1797
+ } catch (e) {
1798
+ s.cancel(c.red(`${toolConfig.label} 安装失败: ${e.message}`));
1799
+ throw e;
1800
+ }
1840
1801
  }
1841
1802
 
1842
1803
  async function runFullInit(options) {
@@ -1846,36 +1807,35 @@ async function runFullInit(options) {
1846
1807
  relinkOnly: false
1847
1808
  });
1848
1809
 
1849
- if (await promptConfirm("是否安装或更新 Claude Code CLI?", false)) {
1810
+ if (await confirmOrCancel({ message: "是否安装或更新 Claude Code CLI?", initialValue: false })) {
1850
1811
  await installCliTool("claude");
1851
1812
  }
1852
- if (await promptConfirm("是否配置 Claude Code 第三方 API?", commandExists("claude"))) {
1813
+ if (await confirmOrCancel({ message: "是否配置 Claude Code 第三方 API?", initialValue: commandExists("claude") })) {
1853
1814
  await configureClaudeApi();
1854
1815
  }
1855
- if (await promptConfirm("是否安装或更新 Codex CLI?", false)) {
1816
+ if (await confirmOrCancel({ message: "是否安装或更新 Codex CLI?", initialValue: false })) {
1856
1817
  await installCliTool("codex");
1857
1818
  }
1858
- if (await promptConfirm("是否配置 Codex 第三方 API?", commandExists("codex"))) {
1819
+ if (await confirmOrCancel({ message: "是否配置 Codex 第三方 API?", initialValue: commandExists("codex") })) {
1859
1820
  await configureCodexApi();
1860
1821
  }
1861
- if (await promptConfirm("是否填写 grok-search 环境变量?", true)) {
1822
+ if (await confirmOrCancel({ message: "是否填写 grok-search 环境变量?", initialValue: false })) {
1862
1823
  await configureGrokSearchEnv(options.agentsDir);
1863
1824
  }
1864
- if (await promptConfirm("是否填写 context7-auto-research 环境变量?", true)) {
1825
+ if (await confirmOrCancel({ message: "是否填写 context7-auto-research 环境变量?", initialValue: false })) {
1865
1826
  await configureContext7Env(options.agentsDir);
1866
1827
  }
1867
- if (await promptConfirm("是否填写 prompt-enhancer 环境变量?", true)) {
1828
+ if (await confirmOrCancel({ message: "是否填写 prompt-enhancer 环境变量?", initialValue: false })) {
1868
1829
  await configurePromptEnhancerEnv(options.agentsDir);
1869
1830
  }
1870
1831
 
1871
- console.log("\nAbelWorkflow 完整初始化完成。");
1832
+ p.log.success(c.green("AbelWorkflow 完整初始化完成"));
1872
1833
  }
1873
1834
 
1874
1835
  async function runInteractiveMenu(options) {
1875
- console.log("AbelWorkflow Setup");
1876
- console.log(`工作流目录: ${pathToLabel(options.agentsDir)}`);
1836
+ p.intro(c.bold(c.bgCyan(c.black(" AbelWorkflow Setup "))));
1837
+ p.log.message(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
1877
1838
 
1878
- const menuChoices = interactiveMenuDescriptors.map(({ value, label }) => ({ value, label }));
1879
1839
  const menuActions = {
1880
1840
  "full-init": async () => runFullInit(options),
1881
1841
  install: async () => installManagedWorkflow({
@@ -1892,14 +1852,63 @@ async function runInteractiveMenu(options) {
1892
1852
  "codex-api": async () => configureCodexApi()
1893
1853
  };
1894
1854
 
1855
+ const buildOption = (d) => {
1856
+ const opt = { value: d.value, label: d.label };
1857
+ if (d.hint) {
1858
+ opt.hint = d.hint;
1859
+ }
1860
+ return opt;
1861
+ };
1862
+
1895
1863
  while (true) {
1896
- const choice = await promptSelect("请选择操作", menuChoices, { defaultValue: interactiveMenuDefaultValue });
1897
- const descriptor = interactiveMenuDescriptors.find((item) => item.value === choice);
1898
- if (descriptor?.value === "exit") {
1864
+ const selectOptions = [
1865
+ ...interactiveMenuDescriptors
1866
+ .filter((d) => d.group === "main")
1867
+ .map(buildOption),
1868
+ { value: "__sep_skills__", label: "─── 技能配置 ───", disabled: true },
1869
+ ...interactiveMenuDescriptors
1870
+ .filter((d) => d.group === "skill")
1871
+ .map(buildOption),
1872
+ { value: "__sep_cli__", label: "─── CLI 工具 ───", disabled: true },
1873
+ ...interactiveMenuDescriptors
1874
+ .filter((d) => d.group === "cli")
1875
+ .map(buildOption),
1876
+ { value: "__sep_exit__", label: "────────────────", disabled: true },
1877
+ ...interactiveMenuDescriptors
1878
+ .filter((d) => d.group === "exit")
1879
+ .map(buildOption)
1880
+ ];
1881
+
1882
+ const choice = await p.select({
1883
+ message: "请选择操作",
1884
+ options: selectOptions,
1885
+ initialValue: interactiveMenuDefaultValue
1886
+ });
1887
+
1888
+ if (p.isCancel(choice)) {
1889
+ p.outro(c.gray("已退出"));
1890
+ return;
1891
+ }
1892
+ if (choice === "exit") {
1893
+ p.outro(c.gray("已退出"));
1899
1894
  return;
1900
1895
  }
1901
1896
 
1902
- await menuActions[descriptor.value]();
1897
+ const action = menuActions[choice];
1898
+ if (!action) {
1899
+ p.log.warn(`未知菜单选项: ${choice}`);
1900
+ continue;
1901
+ }
1902
+
1903
+ try {
1904
+ await action();
1905
+ } catch (error) {
1906
+ if (error instanceof CancelledError) {
1907
+ p.log.warn("操作已取消,返回菜单");
1908
+ continue;
1909
+ }
1910
+ throw error;
1911
+ }
1903
1912
  }
1904
1913
  }
1905
1914
 
@@ -1914,26 +1923,57 @@ async function main() {
1914
1923
  return;
1915
1924
  }
1916
1925
 
1926
+ // 非交互模式下菜单命令自动回退为 install
1927
+ if (options.command === "menu" && options.nonInteractive) {
1928
+ console.log("检测到非交互模式,自动执行工作流安装...");
1929
+ options.command = "install";
1930
+ }
1931
+
1917
1932
  if (options.command === "install") {
1918
- await installManagedWorkflow(options);
1933
+ try {
1934
+ await installManagedWorkflow(options);
1935
+ } catch (error) {
1936
+ const message = `操作失败: ${error.message || String(error)}`;
1937
+ // 非交互模式输出纯文本便于日志捕获/管道处理;交互模式使用 clack 格式化输出
1938
+ if (options.nonInteractive) {
1939
+ console.error(message);
1940
+ } else {
1941
+ p.outro(c.red(message));
1942
+ }
1943
+ process.exit(1);
1944
+ }
1919
1945
  return;
1920
1946
  }
1921
1947
 
1922
1948
  assertInteractiveMenuSupported({
1923
1949
  command: options.command,
1924
1950
  inputIsTTY: input.isTTY,
1925
- outputIsTTY: output.isTTY
1951
+ outputIsTTY: output.isTTY,
1952
+ nonInteractive: options.nonInteractive
1926
1953
  });
1927
1954
 
1928
- await runInteractiveMenu(options);
1955
+ try {
1956
+ await runInteractiveMenu(options);
1957
+ } catch (error) {
1958
+ const message = `操作失败: ${error.message || String(error)}`;
1959
+ // 非交互模式输出纯文本便于日志捕获/管道处理;交互模式使用 clack 格式化输出
1960
+ if (options.nonInteractive) {
1961
+ console.error(message);
1962
+ } else {
1963
+ p.outro(c.red(message));
1964
+ }
1965
+ process.exit(1);
1966
+ }
1929
1967
  }
1930
1968
 
1931
1969
  export {
1932
1970
  buildCodexConfigContent,
1933
1971
  getRunCommandSpawnOptions,
1972
+ hasPromptEnhancerApiConfig,
1934
1973
  main,
1935
1974
  mergeCodexAuthData,
1936
1975
  mergeClaudeSettingsWithDefaults,
1976
+ resolvePromptEnhancerMode,
1937
1977
  resolveExistingCodexApiConfig,
1938
1978
  updateTomlSectionFields
1939
1979
  };