abelworkflow 0.6.4 → 0.6.5

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.
@@ -0,0 +1,130 @@
1
+ const interactiveMenuDescriptors = [
2
+ { value: "full-init", label: "完整初始化:同步工作流 + 可选安装/配置 Claude Code、Codex、技能环境" },
3
+ { value: "install", label: "仅同步/更新工作流到 ~/.agents 并重新链接 Claude/Codex" },
4
+ { value: "grok-search", label: "配置 grok-search 环境变量" },
5
+ { value: "context7", label: "配置 context7-auto-research 环境变量" },
6
+ { value: "prompt-enhancer", label: "配置 prompt-enhancer 环境变量" },
7
+ { value: "claude-install", label: "安装或更新 Claude Code CLI" },
8
+ { value: "claude-api", label: "配置 Claude Code 第三方 API" },
9
+ { value: "codex-install", label: "安装或更新 Codex CLI" },
10
+ { value: "codex-api", label: "配置 Codex 第三方 API" },
11
+ { value: "exit", label: "退出" }
12
+ ];
13
+
14
+ const interactiveMenuDefaultValue = "full-init";
15
+
16
+ function parseArgs(argv, { defaultAgentsDir, resolvePath }) {
17
+ const options = {
18
+ agentsDir: defaultAgentsDir,
19
+ force: false,
20
+ relinkOnly: false,
21
+ command: "menu"
22
+ };
23
+ const positional = [];
24
+ let helpRequested = false;
25
+
26
+ for (let i = 0; i < argv.length; i += 1) {
27
+ const arg = argv[i];
28
+ if (arg === "--force" || arg === "-f") {
29
+ options.force = true;
30
+ continue;
31
+ }
32
+ if (arg === "--link-only") {
33
+ options.relinkOnly = true;
34
+ continue;
35
+ }
36
+ if (arg === "--agents-dir") {
37
+ const value = argv[i + 1];
38
+ if (!value) {
39
+ throw new Error("--agents-dir requires a path");
40
+ }
41
+ options.agentsDir = resolvePath(value);
42
+ i += 1;
43
+ continue;
44
+ }
45
+ if (arg === "--help" || arg === "-h" || arg === "help") {
46
+ helpRequested = true;
47
+ options.command = "help";
48
+ continue;
49
+ }
50
+ if (arg.startsWith("-")) {
51
+ throw new Error(`Unknown argument: ${arg}`);
52
+ }
53
+ positional.push(arg);
54
+ }
55
+
56
+ if (positional.length > 1) {
57
+ throw new Error(`Unknown argument: ${positional.slice(1).join(" ")}`);
58
+ }
59
+
60
+ if (positional[0]) {
61
+ if (["menu", "init"].includes(positional[0])) {
62
+ if (!helpRequested) {
63
+ options.command = "menu";
64
+ }
65
+ } else if (["install", "sync"].includes(positional[0])) {
66
+ if (!helpRequested) {
67
+ options.command = "install";
68
+ }
69
+ } else {
70
+ throw new Error(`Unknown command: ${positional[0]}`);
71
+ }
72
+ }
73
+
74
+ if (options.command !== "install" && (options.force || options.relinkOnly || options.agentsDir !== defaultAgentsDir)) {
75
+ throw new Error("`--force`、`--link-only`、`--agents-dir` 仅能与 `install` 命令一起使用");
76
+ }
77
+
78
+ return options;
79
+ }
80
+
81
+ function assertInteractiveMenuSupported({ command, inputIsTTY, outputIsTTY }) {
82
+ if (command === "menu" && (!inputIsTTY || !outputIsTTY)) {
83
+ throw new Error("交互式菜单需要 TTY 终端;非交互场景请显式使用 `npx abelworkflow install`");
84
+ }
85
+ }
86
+
87
+ function resolvePromptValue(answer, { defaultValue, allowEmpty = false } = {}) {
88
+ const value = String(answer).trim();
89
+ if (!value && defaultValue !== undefined) {
90
+ return { ok: true, value: defaultValue };
91
+ }
92
+ if (!value && !allowEmpty) {
93
+ return { ok: false, error: "此项不能为空。" };
94
+ }
95
+ return { ok: true, value };
96
+ }
97
+
98
+ function resolveSelectValue(answer, choices) {
99
+ const index = Number(answer) - 1;
100
+ if (Number.isInteger(index) && index >= 0 && index < choices.length) {
101
+ return { ok: true, value: choices[index].value };
102
+ }
103
+ const direct = choices.find((choice) => choice.value === answer);
104
+ if (direct) {
105
+ return { ok: true, value: direct.value };
106
+ }
107
+ return { ok: false, error: "无效选择,请重新输入。" };
108
+ }
109
+
110
+ function shouldUseVisibleSecretFallback({ inputIsTTY, platform }) {
111
+ return !inputIsTTY || platform === "win32";
112
+ }
113
+
114
+ function getRunCommandSpawnOptions(platform = process.env.ABELWORKFLOW_TEST_PLATFORM || process.platform) {
115
+ return {
116
+ stdio: "inherit",
117
+ shell: platform === "win32"
118
+ };
119
+ }
120
+
121
+ export {
122
+ assertInteractiveMenuSupported,
123
+ getRunCommandSpawnOptions,
124
+ interactiveMenuDefaultValue,
125
+ interactiveMenuDescriptors,
126
+ parseArgs,
127
+ resolvePromptValue,
128
+ resolveSelectValue,
129
+ shouldUseVisibleSecretFallback
130
+ };
package/lib/cli.mjs CHANGED
@@ -5,6 +5,16 @@ import { dirname, join, relative, resolve } from "node:path";
5
5
  import { stdin as input, stdout as output } from "node:process";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { fileURLToPath } from "node:url";
8
+ import {
9
+ assertInteractiveMenuSupported,
10
+ getRunCommandSpawnOptions,
11
+ interactiveMenuDefaultValue,
12
+ interactiveMenuDescriptors,
13
+ parseArgs,
14
+ resolvePromptValue,
15
+ resolveSelectValue,
16
+ shouldUseVisibleSecretFallback
17
+ } from "./cli/logic.mjs";
8
18
 
9
19
  const __filename = fileURLToPath(import.meta.url);
10
20
  const packageRoot = dirname(dirname(__filename));
@@ -87,83 +97,6 @@ const ignoredSkillPathPatterns = [
87
97
  /^dev-browser\/profiles(\/|$)/,
88
98
  /^dev-browser\/tmp(\/|$)/
89
99
  ];
90
- const menuChoices = [
91
- { value: "full-init", label: "完整初始化:同步工作流 + 可选安装/配置 Claude Code、Codex、技能环境" },
92
- { value: "install", label: "仅同步/更新工作流到 ~/.agents 并重新链接 Claude/Codex" },
93
- { value: "grok-search", label: "配置 grok-search 环境变量" },
94
- { value: "context7", label: "配置 context7-auto-research 环境变量" },
95
- { value: "prompt-enhancer", label: "配置 prompt-enhancer 环境变量" },
96
- { value: "claude-install", label: "安装或更新 Claude Code CLI" },
97
- { value: "claude-api", label: "配置 Claude Code 第三方 API" },
98
- { value: "codex-install", label: "安装或更新 Codex CLI" },
99
- { value: "codex-api", label: "配置 Codex 第三方 API" },
100
- { value: "exit", label: "退出" }
101
- ];
102
-
103
- function parseArgs(argv) {
104
- const options = {
105
- agentsDir: defaultAgentsDir,
106
- force: false,
107
- relinkOnly: false,
108
- command: "menu"
109
- };
110
- const positional = [];
111
- let helpRequested = false;
112
-
113
- for (let i = 0; i < argv.length; i += 1) {
114
- const arg = argv[i];
115
- if (arg === "--force" || arg === "-f") {
116
- options.force = true;
117
- continue;
118
- }
119
- if (arg === "--link-only") {
120
- options.relinkOnly = true;
121
- continue;
122
- }
123
- if (arg === "--agents-dir") {
124
- const value = argv[i + 1];
125
- if (!value) {
126
- throw new Error("--agents-dir requires a path");
127
- }
128
- options.agentsDir = resolve(value);
129
- i += 1;
130
- continue;
131
- }
132
- if (arg === "--help" || arg === "-h" || arg === "help") {
133
- helpRequested = true;
134
- options.command = "help";
135
- continue;
136
- }
137
- if (arg.startsWith("-")) {
138
- throw new Error(`Unknown argument: ${arg}`);
139
- }
140
- positional.push(arg);
141
- }
142
-
143
- if (positional.length > 1) {
144
- throw new Error(`Unknown argument: ${positional.slice(1).join(" ")}`);
145
- }
146
-
147
- if (positional[0]) {
148
- if (["menu", "init"].includes(positional[0])) {
149
- if (!helpRequested) {
150
- options.command = "menu";
151
- }
152
- } else if (["install", "sync"].includes(positional[0])) {
153
- if (!helpRequested) {
154
- options.command = "install";
155
- }
156
- } else {
157
- throw new Error(`Unknown command: ${positional[0]}`);
158
- }
159
- }
160
-
161
- if (options.command === "menu" && (options.force || options.relinkOnly || options.agentsDir !== defaultAgentsDir)) {
162
- throw new Error("`--force`、`--link-only`、`--agents-dir` 仅能与 `install` 命令一起使用");
163
- }
164
-
165
- return options;
166
- }
167
100
 
168
101
  function printHelp() {
169
102
  console.log(`AbelWorkflow installer
@@ -982,22 +915,18 @@ async function promptText(message, options = {}) {
982
915
  rl.close();
983
916
  }
984
917
 
985
- const value = answer.trim();
986
- if (!value && defaultValue !== undefined) {
987
- return defaultValue;
988
- }
989
- if (!value && !allowEmpty) {
990
- console.log("此项不能为空。");
991
- continue;
918
+ const resolved = resolvePromptValue(answer, { defaultValue, allowEmpty });
919
+ if (resolved.ok) {
920
+ return resolved.value;
992
921
  }
993
- return value;
922
+ console.log(resolved.error);
994
923
  }
995
924
  }
996
925
 
997
926
  async function promptSecret(message, options = {}) {
998
927
  const { defaultValue, allowEmpty = false } = options;
999
928
 
1000
- if (!input.isTTY || isWindows()) {
929
+ if (shouldUseVisibleSecretFallback({ inputIsTTY: input.isTTY, platform: getPlatform() })) {
1001
930
  while (true) {
1002
931
  const suffix = defaultValue !== undefined && defaultValue !== ""
1003
932
  ? " [直接回车保留现有值]"
@@ -1010,15 +939,11 @@ async function promptSecret(message, options = {}) {
1010
939
  rl.close();
1011
940
  }
1012
941
 
1013
- const value = answer.trim();
1014
- if (!value && defaultValue !== undefined) {
1015
- return defaultValue;
1016
- }
1017
- if (!value && !allowEmpty) {
1018
- console.log("此项不能为空。");
1019
- continue;
942
+ const resolved = resolvePromptValue(answer, { defaultValue, allowEmpty });
943
+ if (resolved.ok) {
944
+ return resolved.value;
1020
945
  }
1021
- return value;
946
+ console.log(resolved.error);
1022
947
  }
1023
948
  }
1024
949
 
@@ -1037,15 +962,11 @@ async function promptSecret(message, options = {}) {
1037
962
  rl.close();
1038
963
  }
1039
964
 
1040
- const value = answer.trim();
1041
- if (!value && defaultValue !== undefined) {
1042
- return defaultValue;
965
+ const resolved = resolvePromptValue(answer, { defaultValue, allowEmpty });
966
+ if (resolved.ok) {
967
+ return resolved.value;
1043
968
  }
1044
- if (!value && !allowEmpty) {
1045
- console.log("此项不能为空。");
1046
- continue;
1047
- }
1048
- return value;
969
+ console.log(resolved.error);
1049
970
  }
1050
971
  }
1051
972
 
@@ -1060,15 +981,11 @@ async function promptSelect(message, choices, options = {}) {
1060
981
  while (true) {
1061
982
  const fallbackValue = defaultIndex >= 0 ? String(defaultIndex + 1) : undefined;
1062
983
  const answer = await promptText("请输入序号", { defaultValue: fallbackValue, allowEmpty: defaultIndex >= 0 });
1063
- const index = Number(answer) - 1;
1064
- if (Number.isInteger(index) && index >= 0 && index < choices.length) {
1065
- return choices[index].value;
984
+ const resolved = resolveSelectValue(answer, choices);
985
+ if (resolved.ok) {
986
+ return resolved.value;
1066
987
  }
1067
- const direct = choices.find((choice) => choice.value === answer);
1068
- if (direct) {
1069
- return direct.value;
1070
- }
1071
- console.log("无效选择,请重新输入。");
988
+ console.log(resolved.error);
1072
989
  }
1073
990
  }
1074
991
 
@@ -1086,16 +1003,9 @@ function commandExists(command) {
1086
1003
  return result.status === 0;
1087
1004
  }
1088
1005
 
1089
- function getRunCommandSpawnOptions(platform = getPlatform()) {
1090
- return {
1091
- stdio: "inherit",
1092
- shell: platform === "win32"
1093
- };
1094
- }
1095
-
1096
1006
  async function runCommand(command, args) {
1097
1007
  await new Promise((resolvePromise, rejectPromise) => {
1098
- const child = spawn(command, args, getRunCommandSpawnOptions());
1008
+ const child = spawn(command, args, getRunCommandSpawnOptions(getPlatform()));
1099
1009
  child.on("error", rejectPromise);
1100
1010
  child.on("close", (code) => {
1101
1011
  if (code === 0) {
@@ -1965,57 +1875,39 @@ async function runInteractiveMenu(options) {
1965
1875
  console.log("AbelWorkflow Setup");
1966
1876
  console.log(`工作流目录: ${pathToLabel(options.agentsDir)}`);
1967
1877
 
1968
- while (true) {
1969
- const choice = await promptSelect("请选择操作", menuChoices, { defaultValue: "full-init" });
1878
+ const menuChoices = interactiveMenuDescriptors.map(({ value, label }) => ({ value, label }));
1879
+ const menuActions = {
1880
+ "full-init": async () => runFullInit(options),
1881
+ install: async () => installManagedWorkflow({
1882
+ agentsDir: options.agentsDir,
1883
+ force: options.force,
1884
+ relinkOnly: options.relinkOnly
1885
+ }),
1886
+ "grok-search": async () => configureGrokSearchEnv(options.agentsDir),
1887
+ context7: async () => configureContext7Env(options.agentsDir),
1888
+ "prompt-enhancer": async () => configurePromptEnhancerEnv(options.agentsDir),
1889
+ "claude-install": async () => installCliTool("claude"),
1890
+ "claude-api": async () => configureClaudeApi(),
1891
+ "codex-install": async () => installCliTool("codex"),
1892
+ "codex-api": async () => configureCodexApi()
1893
+ };
1970
1894
 
1971
- if (choice === "exit") {
1895
+ while (true) {
1896
+ const choice = await promptSelect("请选择操作", menuChoices, { defaultValue: interactiveMenuDefaultValue });
1897
+ const descriptor = interactiveMenuDescriptors.find((item) => item.value === choice);
1898
+ if (descriptor?.value === "exit") {
1972
1899
  return;
1973
1900
  }
1974
1901
 
1975
- if (choice === "full-init") {
1976
- await runFullInit(options);
1977
- continue;
1978
- }
1979
- if (choice === "install") {
1980
- await installManagedWorkflow({
1981
- agentsDir: options.agentsDir,
1982
- force: options.force,
1983
- relinkOnly: options.relinkOnly
1984
- });
1985
- continue;
1986
- }
1987
- if (choice === "grok-search") {
1988
- await configureGrokSearchEnv(options.agentsDir);
1989
- continue;
1990
- }
1991
- if (choice === "context7") {
1992
- await configureContext7Env(options.agentsDir);
1993
- continue;
1994
- }
1995
- if (choice === "prompt-enhancer") {
1996
- await configurePromptEnhancerEnv(options.agentsDir);
1997
- continue;
1998
- }
1999
- if (choice === "claude-install") {
2000
- await installCliTool("claude");
2001
- continue;
2002
- }
2003
- if (choice === "claude-api") {
2004
- await configureClaudeApi();
2005
- continue;
2006
- }
2007
- if (choice === "codex-install") {
2008
- await installCliTool("codex");
2009
- continue;
2010
- }
2011
- if (choice === "codex-api") {
2012
- await configureCodexApi();
2013
- }
1902
+ await menuActions[descriptor.value]();
2014
1903
  }
2015
1904
  }
2016
1905
 
2017
1906
  async function main() {
2018
- const options = parseArgs(process.argv.slice(2));
1907
+ const options = parseArgs(process.argv.slice(2), {
1908
+ defaultAgentsDir,
1909
+ resolvePath: resolve
1910
+ });
2019
1911
 
2020
1912
  if (options.command === "help") {
2021
1913
  printHelp();
@@ -2027,9 +1919,11 @@ async function main() {
2027
1919
  return;
2028
1920
  }
2029
1921
 
2030
- if (!input.isTTY || !output.isTTY) {
2031
- throw new Error("交互式菜单需要 TTY 终端;非交互场景请显式使用 `npx abelworkflow install`");
2032
- }
1922
+ assertInteractiveMenuSupported({
1923
+ command: options.command,
1924
+ inputIsTTY: input.isTTY,
1925
+ outputIsTTY: output.isTTY
1926
+ });
2033
1927
 
2034
1928
  await runInteractiveMenu(options);
2035
1929
  }
@@ -2043,12 +1937,3 @@ export {
2043
1937
  resolveExistingCodexApiConfig,
2044
1938
  updateTomlSectionFields
2045
1939
  };
2046
-
2047
- const isDirectExecution = process.argv[1] ? resolve(process.argv[1]) === __filename : false;
2048
-
2049
- if (isDirectExecution) {
2050
- main().catch((error) => {
2051
- console.error(error instanceof Error ? error.message : String(error));
2052
- process.exit(1);
2053
- });
2054
- }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude/Codex symlinks.",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test:contracts": "node --test test/runtime-doc-contracts.test.mjs"
7
+ "test:contracts": "node --test test/runtime-doc-contracts.test.mjs test/cli-contracts.test.mjs"
8
8
  },
9
9
  "bin": {
10
10
  "abelworkflow": "bin/abelworkflow.mjs"