abelworkflow 0.6.5 → 0.8.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);
@@ -30,6 +35,14 @@ const codexTemplateConfigPath = join(codexTemplateRoot, "config-base.toml");
30
35
  const codexTemplateAgentsPath = join(codexTemplateRoot, "agents");
31
36
  const installBackupStamp = Date.now();
32
37
  const createdBackupPaths = new Set();
38
+ const augmentContextEnginePermission = "mcp__augment-context-engine";
39
+ const augmentContextEngineRetrievalTool = "mcp__augment-context-engine__codebase-retrieval";
40
+ const augmentContextEngineFeaturePrompt = {
41
+ message: "是否启用 augment-context-engine MCP 代码检索支持?不确定建议选否,可减少 MCP 安装和配置麻烦。",
42
+ initialValue: false
43
+ };
44
+ const localCodebaseRetrievalPolicy = "Use local codebase retrieval with `rg`, `rg --files`, `git grep`, and direct file reads. Do not require augment-context-engine MCP.";
45
+ const augmentCodebaseRetrievalPolicy = `Use \`${augmentContextEngineRetrievalTool}\` as the primary codebase search tool.`;
33
46
  const claudeModelEnvKeys = [
34
47
  "ANTHROPIC_MODEL",
35
48
  "ANTHROPIC_DEFAULT_OPUS_MODEL",
@@ -69,8 +82,7 @@ const defaultClaudeSettings = {
69
82
  "WebSearch",
70
83
  "TodoWrite",
71
84
  "NotebookRead",
72
- "NotebookEdit",
73
- "mcp__augment-context-engine"
85
+ "NotebookEdit"
74
86
  ],
75
87
  deny: []
76
88
  },
@@ -98,20 +110,85 @@ const ignoredSkillPathPatterns = [
98
110
  /^dev-browser\/tmp(\/|$)/
99
111
  ];
100
112
 
101
- function printHelp() {
102
- console.log(`AbelWorkflow installer
113
+ function buildDefaultClaudeSettings({ augmentContextEngine = false } = {}) {
114
+ const settings = {
115
+ ...defaultClaudeSettings,
116
+ env: { ...defaultClaudeSettings.env },
117
+ permissions: {
118
+ ...defaultClaudeSettings.permissions,
119
+ allow: [...defaultClaudeSettings.permissions.allow],
120
+ deny: [...defaultClaudeSettings.permissions.deny]
121
+ },
122
+ hooks: { ...defaultClaudeSettings.hooks }
123
+ };
124
+
125
+ if (augmentContextEngine && !settings.permissions.allow.includes(augmentContextEnginePermission)) {
126
+ settings.permissions.allow.push(augmentContextEnginePermission);
127
+ }
128
+
129
+ return settings;
130
+ }
131
+
132
+ function getAugmentContextEnginePromptOptions() {
133
+ return { ...augmentContextEngineFeaturePrompt };
134
+ }
135
+
136
+ function resolveAugmentContextEngineFeature(options = {}, previousMetadata = {}) {
137
+ if (typeof options.augmentContextEngine === "boolean") {
138
+ return options.augmentContextEngine;
139
+ }
140
+
141
+ if (typeof previousMetadata?.features?.augmentContextEngine === "boolean") {
142
+ return previousMetadata.features.augmentContextEngine;
143
+ }
144
+
145
+ return false;
146
+ }
147
+
148
+ function getWorkflowRenderValues(augmentContextEngine) {
149
+ return {
150
+ CODEBASE_RETRIEVAL_POLICY: augmentContextEngine
151
+ ? augmentCodebaseRetrievalPolicy
152
+ : localCodebaseRetrievalPolicy,
153
+ AUGMENT_CONTEXT_ENGINE_VALIDATION: augmentContextEngine
154
+ ? `Verify MCP availability:\n - \`${augmentContextEngineRetrievalTool}\``
155
+ : "Skip augment-context-engine MCP validation; use local retrieval tools.",
156
+ CODEBASE_RETRIEVAL_MANDATORY_RULE: augmentContextEngine
157
+ ? `Mandatory use of \`${augmentContextEngineRetrievalTool}\``
158
+ : "Mandatory use of configured codebase retrieval policy.",
159
+ CODEBASE_RETRIEVAL_STRUCTURE_REFERENCE: augmentContextEngine
160
+ ? `Inspect codebase structure: \`${augmentContextEngineRetrievalTool}\` with \`file list --recursive\`.`
161
+ : "Inspect codebase structure with `rg --files`, `git grep`, and direct file reads.",
162
+ CODEBASE_RETRIEVAL_PATTERN_AUDIT: augmentContextEngine
163
+ ? `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]"`
164
+ : "Use `rg`, `rg --files`, `git grep`, and direct file reads to validate against existing codebase patterns."
165
+ };
166
+ }
103
167
 
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
168
+ function renderManagedWorkflowContent(content, { augmentContextEngine = false } = {}) {
169
+ let nextContent = content;
170
+ for (const [key, value] of Object.entries(getWorkflowRenderValues(augmentContextEngine))) {
171
+ nextContent = nextContent.replaceAll(`{{${key}}}`, value);
172
+ }
173
+ return nextContent;
174
+ }
111
175
 
112
- Default behavior:
176
+ function printHelp() {
177
+ console.log(`${c.bold("AbelWorkflow")} ${c.cyan("installer")}
178
+
179
+ ${c.bold("Usage:")}
180
+ ${c.cyan("npx abelworkflow")}
181
+ ${c.cyan("npx abelworkflow init")}
182
+ ${c.cyan("npx abelworkflow install")}
183
+ ${c.cyan("npx abelworkflow install --force")}
184
+ ${c.cyan("npx abelworkflow install --link-only")}
185
+ ${c.cyan("npx abelworkflow install --agents-dir /custom/path")}
186
+ ${c.cyan("npx abelworkflow --non-interactive")}
187
+
188
+ ${c.bold("Default behavior:")}
113
189
  - npx abelworkflow: open the interactive setup menu.
114
190
  - npx abelworkflow install: sync managed files and links explicitly.
191
+ - --non-interactive: auto-execute install (skip interactive menu); auto-enabled in CI.
115
192
  `);
116
193
  }
117
194
 
@@ -178,18 +255,18 @@ async function backupExistingPath(targetPath) {
178
255
  const backupPath = await createBackupPath(targetPath);
179
256
  await cp(targetPath, backupPath, { recursive: true, force: false });
180
257
  createdBackupPaths.add(targetPath);
181
- console.log(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
258
+ p.log.message(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
182
259
  return backupPath;
183
260
  }
184
261
 
185
- async function backupIfNeeded(targetPath, force) {
262
+ async function backupIfNeeded(targetPath) {
186
263
  if (!(await pathExists(targetPath))) {
187
264
  return null;
188
265
  }
189
266
 
190
267
  const backupPath = await createBackupPath(targetPath);
191
268
  await rename(targetPath, backupPath);
192
- console.log(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
269
+ p.log.message(`已备份已有配置: ${pathToLabel(targetPath)} -> ${pathToLabel(backupPath)}`);
193
270
  return backupPath;
194
271
  }
195
272
 
@@ -313,6 +390,28 @@ async function writeInstallMetadata(agentsDir, metadata) {
313
390
  await writeFile(join(agentsDir, installMetadataName), `${JSON.stringify(metadata, null, 2)}\n`, "utf8");
314
391
  }
315
392
 
393
+ async function renderManagedWorkflowFile(path, augmentContextEngine) {
394
+ if (!(await pathIsFile(path))) {
395
+ return;
396
+ }
397
+
398
+ const content = await readFile(path, "utf8");
399
+ const rendered = renderManagedWorkflowContent(content, { augmentContextEngine });
400
+ if (rendered !== content) {
401
+ await writeFile(path, rendered, "utf8");
402
+ }
403
+ }
404
+
405
+ async function renderManagedWorkflowFiles(agentsDir, augmentContextEngine) {
406
+ await renderManagedWorkflowFile(join(agentsDir, "AGENTS.md"), augmentContextEngine);
407
+
408
+ const commandsDir = join(agentsDir, "commands");
409
+ const commandFiles = await getCommandNames(commandsDir);
410
+ for (const fileName of commandFiles) {
411
+ await renderManagedWorkflowFile(join(commandsDir, fileName), augmentContextEngine);
412
+ }
413
+ }
414
+
316
415
  async function syncPreservedManagedEntry(sourceRoot, targetRoot, entry, previousManagedChildren) {
317
416
  await removeIfNotDirectory(targetRoot);
318
417
  await mkdir(targetRoot, { recursive: true });
@@ -449,7 +548,7 @@ async function createSymlink(targetPath, sourcePath, linkType, kind) {
449
548
  await symlink(sourcePath, targetPath, linkType);
450
549
  }
451
550
 
452
- async function ensureManagedLink(targetPath, sourcePath, kind, force, previousLinkedTargets) {
551
+ async function ensureManagedLink(targetPath, sourcePath, kind, previousLinkedTargets) {
453
552
  await mkdir(dirname(targetPath), { recursive: true });
454
553
  const sourceResolved = resolve(sourcePath);
455
554
  const sourceExists = await pathTargetExists(sourcePath);
@@ -487,7 +586,7 @@ async function ensureManagedLink(targetPath, sourcePath, kind, force, previousLi
487
586
  if (wasPreviouslyManaged) {
488
587
  await rm(targetPath, { recursive: true, force: true });
489
588
  } else {
490
- await backupIfNeeded(targetPath, force);
589
+ await backupIfNeeded(targetPath);
491
590
  }
492
591
  } else if (!sourceExists) {
493
592
  return { targetPath, status: "skipped" };
@@ -525,7 +624,7 @@ function shouldCopyManagedFile(error) {
525
624
  return ["EPERM", "EACCES", "EXDEV", "EINVAL", "UNKNOWN"].includes(error?.code);
526
625
  }
527
626
 
528
- async function linkSkillDirectories(baseDir, agentsDir, force, previousLinkedTargets) {
627
+ async function linkSkillDirectories(baseDir, agentsDir, previousLinkedTargets) {
529
628
  const results = [];
530
629
  const skillsRoot = join(agentsDir, "skills");
531
630
  const skillNames = (await getDirectoryNames(skillsRoot)).filter((skillName) => skillName !== ".system");
@@ -536,7 +635,6 @@ async function linkSkillDirectories(baseDir, agentsDir, force, previousLinkedTar
536
635
  join(baseDir, "skills", skillName),
537
636
  join(skillsRoot, skillName),
538
637
  "dir",
539
- force,
540
638
  previousLinkedTargets
541
639
  )
542
640
  );
@@ -664,19 +762,7 @@ function isWithinManagedRoot(targetPath, managedSourceRoot) {
664
762
  return relativePath !== ".." && !relativePath.startsWith(`..${isWindows() ? "\\" : "/"}`);
665
763
  }
666
764
 
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) {
765
+ async function linkClaude(agentsDir, previousLinkedTargets) {
680
766
  const claudeDir = join(home, ".claude");
681
767
  await mkdir(claudeDir, { recursive: true });
682
768
  await removeIfNotDirectory(join(claudeDir, "commands"));
@@ -689,21 +775,34 @@ async function linkClaude(agentsDir, force, previousLinkedTargets) {
689
775
  join(claudeDir, "CLAUDE.md"),
690
776
  join(agentsDir, "AGENTS.md"),
691
777
  "file",
692
- force,
693
778
  previousLinkedTargets
694
779
  ),
695
- await ensureManagedLink(
696
- join(claudeDir, "commands", "oc"),
697
- join(agentsDir, "commands", "oc"),
698
- "dir",
699
- force,
700
- previousLinkedTargets
701
- ),
702
- ...(await linkSkillDirectories(claudeDir, agentsDir, force, previousLinkedTargets))
780
+ ...(await (async () => {
781
+ const commandFiles = await getCommandNames(join(agentsDir, "commands"));
782
+ const r = [];
783
+ r.push(...(await pruneManagedTargets(
784
+ join(claudeDir, "commands"),
785
+ join(agentsDir, "commands"),
786
+ commandFiles,
787
+ previousLinkedTargets
788
+ )));
789
+ for (const fileName of commandFiles) {
790
+ r.push(
791
+ await ensureManagedLink(
792
+ join(claudeDir, "commands", fileName),
793
+ join(agentsDir, "commands", fileName),
794
+ "file",
795
+ previousLinkedTargets
796
+ )
797
+ );
798
+ }
799
+ return r;
800
+ })()),
801
+ ...(await linkSkillDirectories(claudeDir, agentsDir, previousLinkedTargets))
703
802
  ];
704
803
  }
705
804
 
706
- async function linkCodex(agentsDir, force, previousLinkedTargets) {
805
+ async function linkCodex(agentsDir, previousLinkedTargets) {
707
806
  const results = [];
708
807
  const codexDir = join(home, ".codex");
709
808
  await mkdir(codexDir, { recursive: true });
@@ -717,17 +816,16 @@ async function linkCodex(agentsDir, force, previousLinkedTargets) {
717
816
  join(codexDir, "AGENTS.md"),
718
817
  join(agentsDir, "AGENTS.md"),
719
818
  "file",
720
- force,
721
819
  previousLinkedTargets
722
820
  )
723
821
  );
724
- results.push(...(await linkSkillDirectories(codexDir, agentsDir, force, previousLinkedTargets)));
822
+ results.push(...(await linkSkillDirectories(codexDir, agentsDir, previousLinkedTargets)));
725
823
 
726
- const commandFiles = await getCommandNames(join(agentsDir, "commands", "oc"));
824
+ const commandFiles = await getCommandNames(join(agentsDir, "commands"));
727
825
  results.push(
728
826
  ...(await pruneManagedTargets(
729
827
  join(codexDir, "prompts"),
730
- join(agentsDir, "commands", "oc"),
828
+ join(agentsDir, "commands"),
731
829
  commandFiles,
732
830
  previousLinkedTargets
733
831
  ))
@@ -736,9 +834,8 @@ async function linkCodex(agentsDir, force, previousLinkedTargets) {
736
834
  results.push(
737
835
  await ensureManagedLink(
738
836
  join(codexDir, "prompts", fileName),
739
- join(agentsDir, "commands", "oc", fileName),
837
+ join(agentsDir, "commands", fileName),
740
838
  "file",
741
- force,
742
839
  previousLinkedTargets
743
840
  )
744
841
  );
@@ -750,19 +847,41 @@ async function linkCodex(agentsDir, force, previousLinkedTargets) {
750
847
  async function installManagedWorkflow(options) {
751
848
  let previousMetadata = {};
752
849
  let managedChildren = {};
850
+ let augmentContextEngine = false;
851
+ const s = p.spinner();
753
852
 
754
853
  if (!options.relinkOnly) {
755
- ({ previousMetadata, managedChildren } = await syncManagedFiles(options.agentsDir));
854
+ s.start("正在同步工作流文件...");
855
+ try {
856
+ ({ previousMetadata, managedChildren } = await syncManagedFiles(options.agentsDir));
857
+ augmentContextEngine = resolveAugmentContextEngineFeature(options, previousMetadata);
858
+ await renderManagedWorkflowFiles(options.agentsDir, augmentContextEngine);
859
+ } catch (e) {
860
+ s.cancel(c.red(`同步失败: ${e.message}`));
861
+ throw e;
862
+ }
863
+ s.stop("工作流文件已同步");
756
864
  } else if (!(await pathExists(options.agentsDir))) {
757
865
  throw new Error(`${options.agentsDir} does not exist; remove --link-only or install first`);
758
866
  } else {
759
867
  previousMetadata = await readInstallMetadata(options.agentsDir);
760
868
  managedChildren = previousMetadata.managedChildren ?? {};
869
+ augmentContextEngine = resolveAugmentContextEngineFeature(options, previousMetadata);
761
870
  }
762
871
 
763
872
  const previousLinkedTargets = previousMetadata.linkedTargets ?? {};
764
- const claudeResults = await linkClaude(options.agentsDir, options.force, previousLinkedTargets);
765
- const codexResults = await linkCodex(options.agentsDir, options.force, previousLinkedTargets);
873
+ s.start("正在链接 Claude / Codex...");
874
+ let claudeResults;
875
+ let codexResults;
876
+ try {
877
+ claudeResults = await linkClaude(options.agentsDir, previousLinkedTargets);
878
+ codexResults = await linkCodex(options.agentsDir, previousLinkedTargets);
879
+ } catch (e) {
880
+ s.cancel(c.red(`链接失败: ${e.message}`));
881
+ throw e;
882
+ }
883
+ s.stop("链接完成");
884
+
766
885
  const linkedTargets = Object.fromEntries(
767
886
  [...claudeResults, ...codexResults]
768
887
  .filter((result) => result.sourcePath)
@@ -776,21 +895,30 @@ async function installManagedWorkflow(options) {
776
895
  ])
777
896
  );
778
897
 
898
+ const managedClaudePermissions = await ensureClaudeSettingsForFeature(augmentContextEngine, previousMetadata);
899
+
779
900
  await writeInstallMetadata(options.agentsDir, {
780
901
  package: "abelworkflow",
781
902
  installedAt: new Date().toISOString(),
903
+ features: {
904
+ ...(previousMetadata.features && typeof previousMetadata.features === "object" ? previousMetadata.features : {}),
905
+ augmentContextEngine
906
+ },
782
907
  managedChildren,
908
+ managedClaudePermissions,
783
909
  linkedTargets
784
910
  });
785
911
 
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.");
912
+ const resultLines = [...claudeResults, ...codexResults].map((result) => {
913
+ const icon = result.status === "unchanged" ? c.gray("=")
914
+ : result.status === "removed" ? c.yellow("−")
915
+ : c.green("+");
916
+ return `${icon} ${pathToLabel(result.targetPath)}`;
917
+ }).join("\n");
918
+
919
+ p.note(resultLines, "链接结果");
920
+ p.log.step(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
921
+ p.log.message(`完成后可运行 ${c.cyan("npx abelworkflow@latest")} 更新托管文件`);
794
922
  }
795
923
 
796
924
  async function readJsonFileSafe(path, fallback = {}) {
@@ -882,121 +1010,6 @@ async function updateDotenvFile(path, updates) {
882
1010
  await writeFile(path, renderDotenv(current), "utf8");
883
1011
  }
884
1012
 
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
1013
  function commandExists(command) {
1001
1014
  const checker = isWindows() ? "where" : "which";
1002
1015
  const result = spawnSync(checker, [command], { stdio: "ignore" });
@@ -1017,22 +1030,12 @@ async function runCommand(command, args) {
1017
1030
  });
1018
1031
  }
1019
1032
 
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
1033
  async function ensureWorkflowPresent(agentsDir) {
1031
1034
  if (await pathExists(join(agentsDir, "AGENTS.md"))) {
1032
1035
  return;
1033
1036
  }
1034
1037
 
1035
- console.log("未检测到已安装的 AbelWorkflow,先执行一次工作流同步。");
1038
+ p.log.message("未检测到已安装的 AbelWorkflow,先执行一次工作流同步。");
1036
1039
  await installManagedWorkflow({
1037
1040
  agentsDir,
1038
1041
  force: false,
@@ -1044,123 +1047,239 @@ async function configureGrokSearchEnv(agentsDir) {
1044
1047
  await ensureWorkflowPresent(agentsDir);
1045
1048
  const envPath = join(agentsDir, "skills", "grok-search", ".env");
1046
1049
  const existing = await readDotenvFile(envPath);
1047
- const baseUrl = await promptText("Grok API URL", {
1048
- defaultValue: existing.GROK_API_URL || "https://api.x.ai/v1"
1050
+ const baseUrl = await p.text({
1051
+ message: "Grok API URL",
1052
+ defaultValue: existing.GROK_API_URL || "https://api.x.ai/v1",
1053
+ validate: required()
1049
1054
  });
1050
- const apiKey = await promptSecret("Grok API Key", {
1051
- defaultValue: existing.GROK_API_KEY || undefined
1055
+ assertNotCancelled(baseUrl);
1056
+
1057
+ const apiKey = await p.password({
1058
+ message: "Grok API Key(输入 - 清除)",
1059
+ mask: "*",
1060
+ defaultValue: existing.GROK_API_KEY || undefined,
1061
+ validate: requiredUnlessExisting(existing.GROK_API_KEY, "Grok API Key 不能为空")
1052
1062
  });
1053
- const model = await promptText("Grok 默认模型", {
1054
- defaultValue: existing.GROK_MODEL || "grok-4-fast"
1063
+ assertNotCancelled(apiKey);
1064
+ const finalApiKey = resolvePasswordValue(apiKey, existing.GROK_API_KEY);
1065
+
1066
+ const model = await p.text({
1067
+ message: "Grok 默认模型",
1068
+ defaultValue: existing.GROK_MODEL || "grok-4-fast",
1069
+ validate: required()
1070
+ });
1071
+ assertNotCancelled(model);
1072
+
1073
+ const useTavily = await confirmOrCancel({
1074
+ message: "是否同时配置 Tavily 作为额外搜索源?",
1075
+ initialValue: Boolean(existing.TAVILY_API_KEY)
1055
1076
  });
1056
- const useTavily = await promptConfirm("是否同时配置 Tavily 作为额外搜索源?", Boolean(existing.TAVILY_API_KEY));
1077
+
1057
1078
  const tavilyKey = useTavily
1058
- ? await promptSecret("Tavily API Key", { defaultValue: existing.TAVILY_API_KEY || undefined })
1079
+ ? await p.password({
1080
+ message: "Tavily API Key(输入 - 清除)",
1081
+ mask: "*",
1082
+ defaultValue: existing.TAVILY_API_KEY || undefined,
1083
+ validate: requiredUnlessExisting(existing.TAVILY_API_KEY, "Tavily API Key 不能为空")
1084
+ })
1059
1085
  : "";
1086
+ if (useTavily) assertNotCancelled(tavilyKey);
1087
+ const finalTavilyKey = useTavily ? resolvePasswordValue(tavilyKey, existing.TAVILY_API_KEY) : null;
1060
1088
 
1061
1089
  await updateDotenvFile(envPath, {
1062
1090
  GROK_API_URL: baseUrl,
1063
- GROK_API_KEY: apiKey,
1091
+ GROK_API_KEY: finalApiKey,
1064
1092
  GROK_MODEL: model,
1065
- TAVILY_API_KEY: useTavily ? tavilyKey : null,
1093
+ TAVILY_API_KEY: finalTavilyKey,
1066
1094
  TAVILY_ENABLED: useTavily ? "true" : null
1067
1095
  });
1068
1096
 
1069
- console.log(`已写入 ${pathToLabel(envPath)}`);
1097
+ p.log.step(`已写入 ${pathToLabel(envPath)}`);
1070
1098
  }
1071
1099
 
1072
1100
  async function configureContext7Env(agentsDir) {
1073
1101
  await ensureWorkflowPresent(agentsDir);
1074
1102
  const envPath = join(agentsDir, "skills", "context7-auto-research", ".env");
1075
1103
  const existing = await readDotenvFile(envPath);
1076
- const apiKey = await promptSecret("Context7 API Key", {
1077
- defaultValue: existing.CONTEXT7_API_KEY || undefined,
1078
- allowEmpty: true
1104
+ const apiKey = await p.password({
1105
+ message: "Context7 API Key (可选,输入 - 清除)",
1106
+ mask: "*",
1107
+ defaultValue: existing.CONTEXT7_API_KEY || undefined
1079
1108
  });
1109
+ assertNotCancelled(apiKey);
1110
+ const finalApiKey = resolvePasswordValue(apiKey, existing.CONTEXT7_API_KEY);
1080
1111
 
1081
1112
  await updateDotenvFile(envPath, {
1082
- CONTEXT7_API_KEY: apiKey || null
1113
+ CONTEXT7_API_KEY: finalApiKey
1083
1114
  });
1084
1115
 
1085
- console.log(`已写入 ${pathToLabel(envPath)}`);
1116
+ p.log.step(`已写入 ${pathToLabel(envPath)}`);
1117
+ }
1118
+
1119
+ function hasPromptEnhancerApiConfig(config) {
1120
+ return [config.PE_API_URL, config.PE_API_KEY, config.PE_MODEL]
1121
+ .every((value) => typeof value === "string" && value.trim() !== "");
1086
1122
  }
1087
1123
 
1088
1124
  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";
1125
+ return hasPromptEnhancerApiConfig(existing) ? "openai-compatible" : "agent";
1096
1126
  }
1097
1127
 
1098
1128
  async function configurePromptEnhancerEnv(agentsDir) {
1099
1129
  await ensureWorkflowPresent(agentsDir);
1100
1130
  const envPath = join(agentsDir, "skills", "prompt-enhancer", ".env");
1101
1131
  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
- });
1132
+ const mode = await selectOrCancel({
1133
+ message: "请选择 prompt-enhancer 的运行方式",
1134
+ options: [
1135
+ { value: "openai-compatible", label: "第三方 OpenAI 兼容接口" },
1136
+ { value: "agent", label: "直接使用当前 Agent" }
1137
+ ],
1138
+ initialValue: resolvePromptEnhancerMode(existing)
1139
+ });
1115
1140
 
1116
- await updateDotenvFile(envPath, {
1117
- ANTHROPIC_API_KEY: apiKey,
1118
- OPENAI_API_KEY: null,
1119
- PE_MODEL: model
1141
+ if (mode === "openai-compatible") {
1142
+ const apiUrl = await p.text({
1143
+ message: "PE_API_URL",
1144
+ defaultValue: existing.PE_API_URL || undefined,
1145
+ validate: required()
1120
1146
  });
1121
- } else if (mode === "openai") {
1122
- const apiKey = await promptSecret("OPENAI_API_KEY", {
1123
- defaultValue: existing.OPENAI_API_KEY || undefined
1147
+ assertNotCancelled(apiUrl);
1148
+
1149
+ const apiKey = await p.password({
1150
+ message: "PE_API_KEY(输入 - 清除)",
1151
+ mask: "*",
1152
+ defaultValue: existing.PE_API_KEY || undefined,
1153
+ validate: requiredUnlessExisting(existing.PE_API_KEY, "PE_API_KEY 不能为空")
1124
1154
  });
1125
- const model = await promptText("PE_MODEL", {
1126
- defaultValue: existing.PE_MODEL || "gpt-4o"
1155
+ assertNotCancelled(apiKey);
1156
+ const finalApiKey = resolvePasswordValue(apiKey, existing.PE_API_KEY);
1157
+
1158
+ const model = await p.text({
1159
+ message: "PE_MODEL",
1160
+ defaultValue: existing.PE_MODEL || undefined,
1161
+ validate: required()
1127
1162
  });
1163
+ assertNotCancelled(model);
1128
1164
 
1129
1165
  await updateDotenvFile(envPath, {
1130
- OPENAI_API_KEY: apiKey,
1166
+ PE_API_URL: apiUrl,
1167
+ PE_API_KEY: finalApiKey,
1168
+ PE_MODEL: model,
1131
1169
  ANTHROPIC_API_KEY: null,
1132
- PE_MODEL: model
1170
+ OPENAI_API_KEY: null
1133
1171
  });
1134
1172
  } else {
1135
1173
  await updateDotenvFile(envPath, {
1174
+ PE_API_URL: null,
1175
+ PE_API_KEY: null,
1176
+ PE_MODEL: null,
1136
1177
  ANTHROPIC_API_KEY: null,
1137
1178
  OPENAI_API_KEY: null
1138
1179
  });
1139
1180
  }
1140
1181
 
1141
- console.log(`已写入 ${pathToLabel(envPath)}`);
1182
+ p.log.step(`已写入 ${pathToLabel(envPath)}`);
1142
1183
  }
1143
1184
 
1144
- function mergeClaudeSettingsWithDefaults(settings) {
1185
+ function mergeClaudeSettingsWithDefaults(settings, { augmentContextEngine = false } = {}) {
1186
+ const defaults = buildDefaultClaudeSettings({ augmentContextEngine });
1145
1187
  const env = settings?.env && typeof settings.env === "object" ? settings.env : {};
1146
1188
  const permissions = settings?.permissions && typeof settings.permissions === "object" ? settings.permissions : {};
1147
1189
  return {
1148
- ...defaultClaudeSettings,
1190
+ ...defaults,
1149
1191
  ...settings,
1150
1192
  env: {
1151
- ...defaultClaudeSettings.env,
1193
+ ...defaults.env,
1152
1194
  ...env
1153
1195
  },
1154
1196
  permissions: {
1155
- ...defaultClaudeSettings.permissions,
1197
+ ...defaults.permissions,
1156
1198
  ...permissions,
1157
- allow: Array.isArray(permissions.allow) ? permissions.allow : defaultClaudeSettings.permissions.allow,
1158
- deny: Array.isArray(permissions.deny) ? permissions.deny : defaultClaudeSettings.permissions.deny
1199
+ allow: Array.isArray(permissions.allow) ? permissions.allow : defaults.permissions.allow,
1200
+ deny: Array.isArray(permissions.deny) ? permissions.deny : defaults.permissions.deny
1159
1201
  },
1160
- hooks: settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : defaultClaudeSettings.hooks
1202
+ hooks: settings?.hooks && typeof settings.hooks === "object" ? settings.hooks : defaults.hooks
1203
+ };
1204
+ }
1205
+
1206
+ function getPreviousManagedClaudePermissions(previousMetadata = {}) {
1207
+ return Array.isArray(previousMetadata.managedClaudePermissions)
1208
+ ? previousMetadata.managedClaudePermissions.filter((value) => typeof value === "string")
1209
+ : [];
1210
+ }
1211
+
1212
+ function applyClaudePermissionFeature(settings, {
1213
+ augmentContextEngine = false,
1214
+ previousManagedPermissions = []
1215
+ } = {}) {
1216
+ const hasSettings = settings && typeof settings === "object";
1217
+ const wasManaged = previousManagedPermissions.includes(augmentContextEnginePermission);
1218
+ if (!hasSettings && !augmentContextEngine) {
1219
+ return { settings, changed: false, managedPermissions: [] };
1220
+ }
1221
+
1222
+ const defaults = buildDefaultClaudeSettings({ augmentContextEngine: false });
1223
+ const nextSettings = hasSettings
1224
+ ? {
1225
+ ...settings,
1226
+ permissions: settings.permissions && typeof settings.permissions === "object"
1227
+ ? { ...settings.permissions }
1228
+ : {}
1229
+ }
1230
+ : defaults;
1231
+ const permissions = nextSettings.permissions;
1232
+ const allow = Array.isArray(permissions.allow)
1233
+ ? [...permissions.allow]
1234
+ : [...defaults.permissions.allow];
1235
+ let changed = !hasSettings;
1236
+ let isManaged = wasManaged;
1237
+
1238
+ if (augmentContextEngine) {
1239
+ if (!allow.includes(augmentContextEnginePermission)) {
1240
+ allow.push(augmentContextEnginePermission);
1241
+ changed = true;
1242
+ isManaged = true;
1243
+ }
1244
+ } else if (wasManaged && allow.includes(augmentContextEnginePermission)) {
1245
+ allow.splice(allow.indexOf(augmentContextEnginePermission), 1);
1246
+ changed = true;
1247
+ isManaged = false;
1248
+ } else {
1249
+ isManaged = false;
1250
+ }
1251
+
1252
+ permissions.allow = allow;
1253
+ if (!Array.isArray(permissions.deny)) {
1254
+ permissions.deny = [...defaults.permissions.deny];
1255
+ }
1256
+ nextSettings.permissions = permissions;
1257
+
1258
+ return {
1259
+ settings: nextSettings,
1260
+ changed,
1261
+ managedPermissions: isManaged ? [augmentContextEnginePermission] : []
1161
1262
  };
1162
1263
  }
1163
1264
 
1265
+ async function ensureClaudeSettingsForFeature(augmentContextEngine, previousMetadata) {
1266
+ const settingsExists = await pathExists(claudeSettingsPath);
1267
+ const settings = settingsExists ? await readJsonFileSafe(claudeSettingsPath, {}) : undefined;
1268
+ const result = applyClaudePermissionFeature(settings, {
1269
+ augmentContextEngine,
1270
+ previousManagedPermissions: getPreviousManagedClaudePermissions(previousMetadata)
1271
+ });
1272
+
1273
+ if (result.changed && result.settings) {
1274
+ if (settingsExists) {
1275
+ await backupExistingPath(claudeSettingsPath);
1276
+ }
1277
+ await writeJsonFileSafe(claudeSettingsPath, result.settings);
1278
+ }
1279
+
1280
+ return result.managedPermissions;
1281
+ }
1282
+
1164
1283
  function getExistingClaudeApiConfig(settings) {
1165
1284
  const env = mergeClaudeSettingsWithDefaults(settings).env;
1166
1285
  return {
@@ -1198,28 +1317,46 @@ function ensureApprovedClaudeApiKey(config, apiKey) {
1198
1317
  async function configureClaudeApi() {
1199
1318
  const settings = await readJsonFileSafe(claudeSettingsPath, {});
1200
1319
  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
1320
+ const authType = await selectOrCancel({
1321
+ message: "Claude Code 第三方 API 认证方式",
1322
+ options: [
1323
+ { value: "api_key", label: "API Key" },
1324
+ { value: "auth_token", label: "Auth Token" }
1325
+ ],
1326
+ initialValue: existing.authType
1327
+ });
1328
+
1329
+ const baseUrl = await p.text({
1330
+ message: "Claude Code Base URL",
1331
+ defaultValue: existing.baseUrl,
1332
+ validate: required()
1207
1333
  });
1208
- const key = await promptSecret(authType === "auth_token" ? "Claude Code Auth Token" : "Claude Code API Key", {
1209
- defaultValue: existing.key || undefined
1334
+ assertNotCancelled(baseUrl);
1335
+
1336
+ const key = await p.password({
1337
+ message: authType === "auth_token" ? "Claude Code Auth Token(输入 - 清除)" : "Claude Code API Key(输入 - 清除)",
1338
+ mask: "*",
1339
+ defaultValue: existing.key || undefined,
1340
+ validate: requiredUnlessExisting(existing.key, "API Key / Auth Token 不能为空")
1210
1341
  });
1211
- const model = await promptText("Claude Code 模型", {
1212
- defaultValue: existing.model || undefined
1342
+ assertNotCancelled(key);
1343
+ const finalKey = resolvePasswordValue(key, existing.key);
1344
+
1345
+ const model = await p.text({
1346
+ message: "Claude Code 模型",
1347
+ defaultValue: existing.model || undefined,
1348
+ validate: required()
1213
1349
  });
1350
+ assertNotCancelled(model);
1214
1351
 
1215
1352
  const nextSettings = mergeClaudeSettingsWithDefaults(settings);
1216
1353
  nextSettings.env.ANTHROPIC_BASE_URL = baseUrl;
1217
1354
 
1218
1355
  if (authType === "auth_token") {
1219
- nextSettings.env.ANTHROPIC_AUTH_TOKEN = key;
1356
+ nextSettings.env.ANTHROPIC_AUTH_TOKEN = finalKey;
1220
1357
  delete nextSettings.env.ANTHROPIC_API_KEY;
1221
1358
  } else {
1222
- nextSettings.env.ANTHROPIC_API_KEY = key;
1359
+ nextSettings.env.ANTHROPIC_API_KEY = finalKey;
1223
1360
  delete nextSettings.env.ANTHROPIC_AUTH_TOKEN;
1224
1361
  }
1225
1362
  for (const field of claudeModelEnvKeys) {
@@ -1230,10 +1367,10 @@ async function configureClaudeApi() {
1230
1367
 
1231
1368
  const metaConfig = await readJsonFileSafe(claudeMetaConfigPath, {});
1232
1369
  metaConfig.hasCompletedOnboarding = true;
1233
- ensureApprovedClaudeApiKey(metaConfig, key);
1370
+ ensureApprovedClaudeApiKey(metaConfig, finalKey);
1234
1371
  await writeJsonFileWithBackup(claudeMetaConfigPath, metaConfig);
1235
1372
 
1236
- console.log(`已更新 ${pathToLabel(claudeSettingsPath)} (${authType}, ${baseUrl}, ${maskSecret(key)})`);
1373
+ p.log.step(`已更新 ${pathToLabel(claudeSettingsPath)} (${authType}, ${baseUrl}, ${maskSecret(finalKey)})`);
1237
1374
  }
1238
1375
 
1239
1376
  function updateTopLevelTomlField(content, field, value) {
@@ -1723,13 +1860,24 @@ async function configureCodexApi() {
1723
1860
  const existing = await getExistingCodexApiConfig();
1724
1861
  const providerId = existing.providerId || "abelworkflow";
1725
1862
  const providerName = existing.providerName || providerId;
1726
- const baseUrl = await promptText("Codex Base URL", {
1727
- defaultValue: existing.baseUrl
1863
+ const baseUrl = await p.text({
1864
+ message: "Codex Base URL",
1865
+ defaultValue: existing.baseUrl,
1866
+ validate: required()
1728
1867
  });
1729
- const apiKey = await promptSecret("Codex 第三方 API Key", {
1730
- defaultValue: existing.apiKey || undefined
1868
+ assertNotCancelled(baseUrl);
1869
+
1870
+ const apiKey = await p.password({
1871
+ message: "Codex 第三方 API Key(输入 - 清除)",
1872
+ mask: "*",
1873
+ defaultValue: existing.apiKey || undefined,
1874
+ validate: requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
1731
1875
  });
1732
- const shouldDeploySubagents = await promptConfirm("是否部署 Codex subagents 配置?", true);
1876
+ assertNotCancelled(apiKey);
1877
+ const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
1878
+
1879
+ const shouldDeploySubagents = await confirmOrCancel({ message: "是否部署 Codex subagents 配置?", initialValue: true });
1880
+
1733
1881
  const envKey = existing.envKey || "OPENAI_API_KEY";
1734
1882
  const currentContent = await pathExists(codexConfigPath) ? await readFile(codexConfigPath, "utf8") : "";
1735
1883
  const templateContent = await loadBundledCodexConfigTemplate();
@@ -1747,16 +1895,16 @@ async function configureCodexApi() {
1747
1895
  await mkdir(dirname(codexConfigPath), { recursive: true });
1748
1896
  await writeFile(codexConfigPath, content, "utf8");
1749
1897
 
1750
- const auth = mergeCodexAuthData(await readJsonFileSafe(codexAuthPath, {}), envKey, apiKey, existing.legacyEnvKeys || []);
1898
+ const auth = mergeCodexAuthData(await readJsonFileSafe(codexAuthPath, {}), envKey, finalApiKey, existing.legacyEnvKeys || []);
1751
1899
  await writeJsonFileWithBackup(codexAuthPath, auth);
1752
1900
 
1753
- console.log(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
1754
- console.log(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(apiKey)})`);
1901
+ p.log.step(`已更新 ${pathToLabel(codexConfigPath)} (${providerId}, ${baseUrl})`);
1902
+ p.log.step(`已更新 ${pathToLabel(codexAuthPath)} (${maskSecret(finalApiKey)})`);
1755
1903
  if (shouldDeploySubagents) {
1756
1904
  const deployed = await deployBundledCodexAgents();
1757
- console.log(`已部署 ${deployed.length} 个 Codex subagents 到 ${pathToLabel(join(home, ".codex", "agents"))}`);
1905
+ p.log.step(`已部署 ${deployed.length} 个 Codex subagents 到 ${pathToLabel(join(home, ".codex", "agents"))}`);
1758
1906
  } else {
1759
- console.log("已跳过 Codex subagents 部署。");
1907
+ p.log.message("已跳过 Codex subagents 部署。");
1760
1908
  }
1761
1909
  }
1762
1910
 
@@ -1827,55 +1975,68 @@ async function installCliTool(tool) {
1827
1975
 
1828
1976
  const installed = commandExists(toolConfig.command);
1829
1977
  if (installed) {
1830
- const shouldUpdate = await promptConfirm(`${toolConfig.label} 已检测到,是否继续执行 npm 强制安装/更新?`, false);
1978
+ const shouldUpdate = await confirmOrCancel({
1979
+ message: `${toolConfig.label} 已检测到,是否继续执行 npm 强制安装/更新?`,
1980
+ initialValue: false
1981
+ });
1831
1982
  if (!shouldUpdate) {
1832
- console.log(`跳过 ${toolConfig.label} 安装。`);
1983
+ p.log.message(`跳过 ${toolConfig.label} 安装。`);
1833
1984
  return;
1834
1985
  }
1835
1986
  }
1836
1987
 
1837
- console.log(`开始安装 ${toolConfig.label}...`);
1838
- await runCommand("npm", ["install", "-g", toolConfig.packageName, "--force"]);
1839
- console.log(`${toolConfig.label} 安装完成。`);
1988
+ const s = p.spinner();
1989
+ s.start(`正在安装 ${toolConfig.label}...`);
1990
+ try {
1991
+ await runCommand("npm", ["install", "-g", toolConfig.packageName, "--force"]);
1992
+ s.stop(`${toolConfig.label} 安装完成`);
1993
+ } catch (e) {
1994
+ s.cancel(c.red(`${toolConfig.label} 安装失败: ${e.message}`));
1995
+ throw e;
1996
+ }
1840
1997
  }
1841
1998
 
1842
1999
  async function runFullInit(options) {
2000
+ const augmentContextEngine = options.nonInteractive
2001
+ ? false
2002
+ : await confirmOrCancel(getAugmentContextEnginePromptOptions());
2003
+
1843
2004
  await installManagedWorkflow({
1844
2005
  agentsDir: options.agentsDir,
1845
2006
  force: options.force,
1846
- relinkOnly: false
2007
+ relinkOnly: false,
2008
+ augmentContextEngine
1847
2009
  });
1848
2010
 
1849
- if (await promptConfirm("是否安装或更新 Claude Code CLI?", false)) {
2011
+ if (await confirmOrCancel({ message: "是否安装或更新 Claude Code CLI?", initialValue: false })) {
1850
2012
  await installCliTool("claude");
1851
2013
  }
1852
- if (await promptConfirm("是否配置 Claude Code 第三方 API?", commandExists("claude"))) {
2014
+ if (await confirmOrCancel({ message: "是否配置 Claude Code 第三方 API?", initialValue: commandExists("claude") })) {
1853
2015
  await configureClaudeApi();
1854
2016
  }
1855
- if (await promptConfirm("是否安装或更新 Codex CLI?", false)) {
2017
+ if (await confirmOrCancel({ message: "是否安装或更新 Codex CLI?", initialValue: false })) {
1856
2018
  await installCliTool("codex");
1857
2019
  }
1858
- if (await promptConfirm("是否配置 Codex 第三方 API?", commandExists("codex"))) {
2020
+ if (await confirmOrCancel({ message: "是否配置 Codex 第三方 API?", initialValue: commandExists("codex") })) {
1859
2021
  await configureCodexApi();
1860
2022
  }
1861
- if (await promptConfirm("是否填写 grok-search 环境变量?", true)) {
2023
+ if (await confirmOrCancel({ message: "是否填写 grok-search 环境变量?", initialValue: false })) {
1862
2024
  await configureGrokSearchEnv(options.agentsDir);
1863
2025
  }
1864
- if (await promptConfirm("是否填写 context7-auto-research 环境变量?", true)) {
2026
+ if (await confirmOrCancel({ message: "是否填写 context7-auto-research 环境变量?", initialValue: false })) {
1865
2027
  await configureContext7Env(options.agentsDir);
1866
2028
  }
1867
- if (await promptConfirm("是否填写 prompt-enhancer 环境变量?", true)) {
2029
+ if (await confirmOrCancel({ message: "是否填写 prompt-enhancer 环境变量?", initialValue: false })) {
1868
2030
  await configurePromptEnhancerEnv(options.agentsDir);
1869
2031
  }
1870
2032
 
1871
- console.log("\nAbelWorkflow 完整初始化完成。");
2033
+ p.log.success(c.green("AbelWorkflow 完整初始化完成"));
1872
2034
  }
1873
2035
 
1874
2036
  async function runInteractiveMenu(options) {
1875
- console.log("AbelWorkflow Setup");
1876
- console.log(`工作流目录: ${pathToLabel(options.agentsDir)}`);
2037
+ p.intro(c.bold(c.bgCyan(c.black(" AbelWorkflow Setup "))));
2038
+ p.log.message(`工作流目录: ${c.cyan(pathToLabel(options.agentsDir))}`);
1877
2039
 
1878
- const menuChoices = interactiveMenuDescriptors.map(({ value, label }) => ({ value, label }));
1879
2040
  const menuActions = {
1880
2041
  "full-init": async () => runFullInit(options),
1881
2042
  install: async () => installManagedWorkflow({
@@ -1892,14 +2053,63 @@ async function runInteractiveMenu(options) {
1892
2053
  "codex-api": async () => configureCodexApi()
1893
2054
  };
1894
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;
2062
+ };
2063
+
1895
2064
  while (true) {
1896
- const choice = await promptSelect("请选择操作", menuChoices, { defaultValue: interactiveMenuDefaultValue });
1897
- const descriptor = interactiveMenuDescriptors.find((item) => item.value === choice);
1898
- if (descriptor?.value === "exit") {
2065
+ const selectOptions = [
2066
+ ...interactiveMenuDescriptors
2067
+ .filter((d) => d.group === "main")
2068
+ .map(buildOption),
2069
+ { value: "__sep_skills__", label: "─── 技能配置 ───", disabled: true },
2070
+ ...interactiveMenuDescriptors
2071
+ .filter((d) => d.group === "skill")
2072
+ .map(buildOption),
2073
+ { value: "__sep_cli__", label: "─── CLI 工具 ───", disabled: true },
2074
+ ...interactiveMenuDescriptors
2075
+ .filter((d) => d.group === "cli")
2076
+ .map(buildOption),
2077
+ { value: "__sep_exit__", label: "────────────────", disabled: true },
2078
+ ...interactiveMenuDescriptors
2079
+ .filter((d) => d.group === "exit")
2080
+ .map(buildOption)
2081
+ ];
2082
+
2083
+ const choice = await p.select({
2084
+ message: "请选择操作",
2085
+ options: selectOptions,
2086
+ initialValue: interactiveMenuDefaultValue
2087
+ });
2088
+
2089
+ if (p.isCancel(choice)) {
2090
+ p.outro(c.gray("已退出"));
2091
+ return;
2092
+ }
2093
+ if (choice === "exit") {
2094
+ p.outro(c.gray("已退出"));
1899
2095
  return;
1900
2096
  }
1901
2097
 
1902
- await menuActions[descriptor.value]();
2098
+ const action = menuActions[choice];
2099
+ if (!action) {
2100
+ p.log.warn(`未知菜单选项: ${choice}`);
2101
+ continue;
2102
+ }
2103
+
2104
+ try {
2105
+ await action();
2106
+ } catch (error) {
2107
+ if (error instanceof CancelledError) {
2108
+ p.log.warn("操作已取消,返回菜单");
2109
+ continue;
2110
+ }
2111
+ throw error;
2112
+ }
1903
2113
  }
1904
2114
  }
1905
2115
 
@@ -1914,26 +2124,62 @@ async function main() {
1914
2124
  return;
1915
2125
  }
1916
2126
 
2127
+ // 非交互模式下菜单命令自动回退为 install
2128
+ if (options.command === "menu" && options.nonInteractive) {
2129
+ console.log("检测到非交互模式,自动执行工作流安装...");
2130
+ options.command = "install";
2131
+ }
2132
+
1917
2133
  if (options.command === "install") {
1918
- await installManagedWorkflow(options);
2134
+ try {
2135
+ await installManagedWorkflow(options);
2136
+ } catch (error) {
2137
+ const message = `操作失败: ${error.message || String(error)}`;
2138
+ // 非交互模式输出纯文本便于日志捕获/管道处理;交互模式使用 clack 格式化输出
2139
+ if (options.nonInteractive) {
2140
+ console.error(message);
2141
+ } else {
2142
+ p.outro(c.red(message));
2143
+ }
2144
+ process.exit(1);
2145
+ }
1919
2146
  return;
1920
2147
  }
1921
2148
 
1922
2149
  assertInteractiveMenuSupported({
1923
2150
  command: options.command,
1924
2151
  inputIsTTY: input.isTTY,
1925
- outputIsTTY: output.isTTY
2152
+ outputIsTTY: output.isTTY,
2153
+ nonInteractive: options.nonInteractive
1926
2154
  });
1927
2155
 
1928
- await runInteractiveMenu(options);
2156
+ try {
2157
+ await runInteractiveMenu(options);
2158
+ } catch (error) {
2159
+ const message = `操作失败: ${error.message || String(error)}`;
2160
+ // 非交互模式输出纯文本便于日志捕获/管道处理;交互模式使用 clack 格式化输出
2161
+ if (options.nonInteractive) {
2162
+ console.error(message);
2163
+ } else {
2164
+ p.outro(c.red(message));
2165
+ }
2166
+ process.exit(1);
2167
+ }
1929
2168
  }
1930
2169
 
1931
2170
  export {
2171
+ applyClaudePermissionFeature,
2172
+ buildDefaultClaudeSettings,
1932
2173
  buildCodexConfigContent,
2174
+ getAugmentContextEnginePromptOptions,
1933
2175
  getRunCommandSpawnOptions,
2176
+ hasPromptEnhancerApiConfig,
1934
2177
  main,
1935
2178
  mergeCodexAuthData,
1936
2179
  mergeClaudeSettingsWithDefaults,
2180
+ renderManagedWorkflowContent,
2181
+ resolveAugmentContextEngineFeature,
2182
+ resolvePromptEnhancerMode,
1937
2183
  resolveExistingCodexApiConfig,
1938
2184
  updateTomlSectionFields
1939
2185
  };