hamster-wheel-cli 0.2.0-beta.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  ## [0.2.0] - 2025-12-24
6
6
  ### Added
7
+ - 新增 `alias run` 子命令,支持执行 alias 时追加参数并覆盖同名选项。
7
8
  - 支持多任务执行与 multi-task 模式,提供 relay(接力)、serial(串行)、serial-continue(串行继续)、parallel(并行)四种执行方式。
8
9
  - 新增 `alias` 命令,提供交互式 alias 配置浏览界面,支持上下键选择与 q 退出。
9
10
  - Monitor 支持通过 `t` 键终止运行中的任务,增加确认对话框防止误操作。
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # hamster-wheel-cli
1
+ # wheel-ai
2
2
 
3
3
  基于 Node.js + commander 的持续迭代开发工具,结合外部 AI CLI(运行时指定)驱动完整软件交付流程:需求澄清、计划生成、编码、自审、测试(单元+e2e)、推送与 PR。
4
4
 
@@ -82,6 +82,13 @@ weekly = "run --task \"补充测试\" --run-tests"
82
82
  wheel-ai set alias daily --task "补充文档" --run-tests
83
83
  ```
84
84
 
85
+ 也可以直接执行 alias,并在末尾追加命令:
86
+ ```bash
87
+ wheel-ai alias run daily --run-e2e --task "补充 e2e"
88
+ ```
89
+ - `<addition...>` 支持多个命令/参数组合,等价于将追加内容放在 alias 命令末尾。
90
+ - 当追加的命令/参数与 alias 内已有内容重复时,以追加的为准。
91
+
85
92
  ## 持久化记忆
86
93
  - `docs/ai-workflow.md`:AI 执行前的工作流基线,需作为提示前置输入。
87
94
  - `memory/plan.md`:分阶段计划(可被 AI 重写保持最新)。
package/dist/cli.js CHANGED
@@ -3802,6 +3802,44 @@ async function tailLogFile(options) {
3802
3802
 
3803
3803
  // src/cli.ts
3804
3804
  var FOREGROUND_CHILD_ENV = "WHEEL_AI_FOREGROUND_CHILD";
3805
+ var RUN_OPTION_SPECS = [
3806
+ { name: "task", flags: ["-t", "--task"], valueMode: "single" },
3807
+ { name: "iterations", flags: ["-i", "--iterations"], valueMode: "single" },
3808
+ { name: "ai-cli", flags: ["--ai-cli"], valueMode: "single" },
3809
+ { name: "ai-args", flags: ["--ai-args"], valueMode: "variadic" },
3810
+ { name: "ai-prompt-arg", flags: ["--ai-prompt-arg"], valueMode: "single" },
3811
+ { name: "notes-file", flags: ["--notes-file"], valueMode: "single" },
3812
+ { name: "plan-file", flags: ["--plan-file"], valueMode: "single" },
3813
+ { name: "workflow-doc", flags: ["--workflow-doc"], valueMode: "single" },
3814
+ { name: "worktree", flags: ["--worktree"], valueMode: "none" },
3815
+ { name: "branch", flags: ["--branch"], valueMode: "single" },
3816
+ { name: "worktree-path", flags: ["--worktree-path"], valueMode: "single" },
3817
+ { name: "base-branch", flags: ["--base-branch"], valueMode: "single" },
3818
+ { name: "skip-install", flags: ["--skip-install"], valueMode: "none" },
3819
+ { name: "run-tests", flags: ["--run-tests"], valueMode: "none" },
3820
+ { name: "run-e2e", flags: ["--run-e2e"], valueMode: "none" },
3821
+ { name: "unit-command", flags: ["--unit-command"], valueMode: "single" },
3822
+ { name: "e2e-command", flags: ["--e2e-command"], valueMode: "single" },
3823
+ { name: "auto-commit", flags: ["--auto-commit"], valueMode: "none" },
3824
+ { name: "auto-push", flags: ["--auto-push"], valueMode: "none" },
3825
+ { name: "pr", flags: ["--pr"], valueMode: "none" },
3826
+ { name: "pr-title", flags: ["--pr-title"], valueMode: "single" },
3827
+ { name: "pr-body", flags: ["--pr-body"], valueMode: "single" },
3828
+ { name: "draft", flags: ["--draft"], valueMode: "none" },
3829
+ { name: "reviewer", flags: ["--reviewer"], valueMode: "variadic" },
3830
+ { name: "auto-merge", flags: ["--auto-merge"], valueMode: "none" },
3831
+ { name: "webhook", flags: ["--webhook"], valueMode: "single" },
3832
+ { name: "webhook-timeout", flags: ["--webhook-timeout"], valueMode: "single" },
3833
+ { name: "multi-task-mode", flags: ["--multi-task-mode"], valueMode: "single" },
3834
+ { name: "stop-signal", flags: ["--stop-signal"], valueMode: "single" },
3835
+ { name: "log-file", flags: ["--log-file"], valueMode: "single" },
3836
+ { name: "background", flags: ["--background"], valueMode: "none" },
3837
+ { name: "verbose", flags: ["-v", "--verbose"], valueMode: "none" },
3838
+ { name: "skip-quality", flags: ["--skip-quality"], valueMode: "none" }
3839
+ ];
3840
+ var RUN_OPTION_FLAG_MAP = new Map(
3841
+ RUN_OPTION_SPECS.flatMap((spec) => spec.flags.map((flag) => [flag, spec]))
3842
+ );
3805
3843
  function parseInteger(value, defaultValue) {
3806
3844
  const parsed = Number.parseInt(value, 10);
3807
3845
  if (Number.isNaN(parsed)) return defaultValue;
@@ -3837,6 +3875,102 @@ function extractAliasCommandArgs(argv, name) {
3837
3875
  if (rest[0] === "--") return rest.slice(1);
3838
3876
  return rest;
3839
3877
  }
3878
+ function isAliasCommandToken(token) {
3879
+ return token === "alias" || token === "aliases";
3880
+ }
3881
+ function extractAliasRunArgs(argv, name) {
3882
+ const args = argv.slice(2);
3883
+ const start = args.findIndex(
3884
+ (arg, index) => isAliasCommandToken(arg) && args[index + 1] === "run" && args[index + 2] === name
3885
+ );
3886
+ if (start < 0) return [];
3887
+ const rest = args.slice(start + 3);
3888
+ if (rest[0] === "--") return rest.slice(1);
3889
+ return rest;
3890
+ }
3891
+ function normalizeAliasCommandArgs(args) {
3892
+ let start = 0;
3893
+ if (args[start] === "wheel-ai") {
3894
+ start += 1;
3895
+ }
3896
+ if (args[start] === "run") {
3897
+ start += 1;
3898
+ }
3899
+ return args.slice(start);
3900
+ }
3901
+ function resolveRunOptionSpec(token) {
3902
+ const equalIndex = token.indexOf("=");
3903
+ const flag = equalIndex > 0 ? token.slice(0, equalIndex) : token;
3904
+ const spec = RUN_OPTION_FLAG_MAP.get(flag);
3905
+ if (!spec) return null;
3906
+ if (equalIndex > 0) {
3907
+ return { spec, inlineValue: token.slice(equalIndex + 1) };
3908
+ }
3909
+ return { spec };
3910
+ }
3911
+ function parseArgSegments(tokens) {
3912
+ const segments = [];
3913
+ let index = 0;
3914
+ while (index < tokens.length) {
3915
+ const token = tokens[index];
3916
+ if (token === "--") {
3917
+ segments.push({ tokens: tokens.slice(index) });
3918
+ break;
3919
+ }
3920
+ const match = resolveRunOptionSpec(token);
3921
+ if (!match) {
3922
+ segments.push({ tokens: [token] });
3923
+ index += 1;
3924
+ continue;
3925
+ }
3926
+ if (match.inlineValue !== void 0) {
3927
+ segments.push({ name: match.spec.name, tokens: [token] });
3928
+ index += 1;
3929
+ continue;
3930
+ }
3931
+ if (match.spec.valueMode === "none") {
3932
+ segments.push({ name: match.spec.name, tokens: [token] });
3933
+ index += 1;
3934
+ continue;
3935
+ }
3936
+ if (match.spec.valueMode === "single") {
3937
+ const next = tokens[index + 1];
3938
+ if (next !== void 0) {
3939
+ segments.push({ name: match.spec.name, tokens: [token, next] });
3940
+ index += 2;
3941
+ } else {
3942
+ segments.push({ name: match.spec.name, tokens: [token] });
3943
+ index += 1;
3944
+ }
3945
+ continue;
3946
+ }
3947
+ const values = [];
3948
+ let cursor = index + 1;
3949
+ while (cursor < tokens.length) {
3950
+ const next = tokens[cursor];
3951
+ if (next === "--") break;
3952
+ const nextMatch = resolveRunOptionSpec(next);
3953
+ if (nextMatch) break;
3954
+ values.push(next);
3955
+ cursor += 1;
3956
+ }
3957
+ segments.push({ name: match.spec.name, tokens: [token, ...values] });
3958
+ index = cursor;
3959
+ }
3960
+ return segments;
3961
+ }
3962
+ function mergeAliasCommandArgs(aliasTokens, additionTokens) {
3963
+ const aliasSegments = parseArgSegments(aliasTokens);
3964
+ const additionSegments = parseArgSegments(additionTokens);
3965
+ const overrideNames = new Set(
3966
+ additionSegments.flatMap((segment) => segment.name ? [segment.name] : [])
3967
+ );
3968
+ const merged = [
3969
+ ...aliasSegments.filter((segment) => !segment.name || !overrideNames.has(segment.name)),
3970
+ ...additionSegments
3971
+ ];
3972
+ return merged.flatMap((segment) => segment.tokens);
3973
+ }
3840
3974
  async function runForegroundWithDetach(options) {
3841
3975
  const args = buildBackgroundArgs(options.argv, options.logFile, options.branchName, options.injectBranch);
3842
3976
  const child = (0, import_node_child_process.spawn)(process.execPath, [...process.execArgv, ...args], {
@@ -3920,7 +4054,11 @@ async function runCli(argv) {
3920
4054
  const globalConfig = await loadGlobalConfig(defaultLogger);
3921
4055
  const effectiveArgv = applyShortcutArgv(argv, globalConfig);
3922
4056
  const program = new import_commander.Command();
3923
- program.name("wheel-ai").description("\u57FA\u4E8E AI CLI \u7684\u6301\u7EED\u8FED\u4EE3\u5F00\u53D1\u5DE5\u5177").version("1.0.0");
4057
+ program.name("wheel-ai").description("\u57FA\u4E8E AI CLI \u7684\u6301\u7EED\u8FED\u4EE3\u5F00\u53D1\u5DE5\u5177").version("0.2.0");
4058
+ program.addHelpText(
4059
+ "after",
4060
+ "\n\u522B\u540D\u6267\u884C\uFF1A\n wheel-ai alias run <alias> <addition...>\n \u8FFD\u52A0\u547D\u4EE4\u4E0E alias \u91CD\u53E0\u65F6\uFF0C\u4EE5\u8FFD\u52A0\u4E3A\u51C6\u3002\n"
4061
+ );
3924
4062
  program.command("run").option("-t, --task <task>", "\u9700\u8981\u5B8C\u6210\u7684\u4EFB\u52A1\u63CF\u8FF0\uFF08\u53EF\u91CD\u590D\u4F20\u5165\uFF0C\u72EC\u7ACB\u5904\u7406\uFF09", collect, []).option("-i, --iterations <number>", "\u6700\u5927\u8FED\u4EE3\u6B21\u6570", (value) => parseInteger(value, 5), 5).option("--ai-cli <command>", "AI CLI \u547D\u4EE4", "claude").option("--ai-args <args...>", "AI CLI \u53C2\u6570", []).option("--ai-prompt-arg <flag>", "\u7528\u4E8E\u4F20\u5165 prompt \u7684\u53C2\u6570\uFF08\u4E3A\u7A7A\u5219\u4F7F\u7528 stdin\uFF09").option("--notes-file <path>", "\u6301\u4E45\u5316\u8BB0\u5FC6\u6587\u4EF6", defaultNotesPath()).option("--plan-file <path>", "\u8BA1\u5212\u6587\u4EF6", defaultPlanPath()).option("--workflow-doc <path>", "AI \u5DE5\u4F5C\u6D41\u7A0B\u8BF4\u660E\u6587\u4EF6", defaultWorkflowDoc()).option("--worktree", "\u5728\u72EC\u7ACB worktree \u4E0A\u6267\u884C", false).option("--branch <name>", "worktree \u5206\u652F\u540D\uFF08\u9ED8\u8BA4\u81EA\u52A8\u751F\u6210\u6216\u5F53\u524D\u5206\u652F\uFF09").option("--worktree-path <path>", "worktree \u8DEF\u5F84\uFF0C\u9ED8\u8BA4 ../worktrees/<branch>").option("--base-branch <name>", "\u521B\u5EFA\u5206\u652F\u7684\u57FA\u7EBF\u5206\u652F", "main").option("--skip-install", "\u8DF3\u8FC7\u5F00\u59CB\u4EFB\u52A1\u524D\u7684\u4F9D\u8D56\u68C0\u67E5", false).option("--run-tests", "\u8FD0\u884C\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", false).option("--run-e2e", "\u8FD0\u884C e2e \u6D4B\u8BD5\u547D\u4EE4", false).option("--unit-command <cmd>", "\u5355\u5143\u6D4B\u8BD5\u547D\u4EE4", "yarn test").option("--e2e-command <cmd>", "e2e \u6D4B\u8BD5\u547D\u4EE4", "yarn e2e").option("--auto-commit", "\u81EA\u52A8 git commit", false).option("--auto-push", "\u81EA\u52A8 git push", false).option("--pr", "\u4F7F\u7528 gh \u521B\u5EFA PR", false).option("--pr-title <title>", "PR \u6807\u9898").option("--pr-body <path>", "PR \u63CF\u8FF0\u6587\u4EF6\u8DEF\u5F84\uFF08\u53EF\u7559\u7A7A\u81EA\u52A8\u751F\u6210\uFF09").option("--draft", "\u4EE5\u8349\u7A3F\u5F62\u5F0F\u521B\u5EFA PR", false).option("--reviewer <user...>", "PR reviewers", collect, []).option("--auto-merge", "PR \u68C0\u67E5\u901A\u8FC7\u540E\u81EA\u52A8\u5408\u5E76", false).option("--webhook <url>", "webhook \u901A\u77E5 URL\uFF08\u53EF\u91CD\u590D\uFF09", collect, []).option("--webhook-timeout <ms>", "webhook \u8BF7\u6C42\u8D85\u65F6\uFF08\u6BEB\u79D2\uFF09", (value) => parseInteger(value, 8e3)).option("--multi-task-mode <mode>", "\u591A\u4EFB\u52A1\u6267\u884C\u6A21\u5F0F\uFF08relay/serial/serial-continue/parallel\uFF0C\u6216\u4E2D\u6587\u63CF\u8FF0\uFF09", "relay").option("--stop-signal <token>", "AI \u8F93\u51FA\u4E2D\u7684\u505C\u6B62\u6807\u8BB0", "<<DONE>>").option("--log-file <path>", "\u65E5\u5FD7\u8F93\u51FA\u6587\u4EF6\u8DEF\u5F84").option("--background", "\u5207\u5165\u540E\u53F0\u8FD0\u884C", false).option("-v, --verbose", "\u8F93\u51FA\u8C03\u8BD5\u65E5\u5FD7", false).option("--skip-quality", "\u8DF3\u8FC7\u4EE3\u7801\u8D28\u91CF\u68C0\u67E5", false).action(async (options) => {
3925
4063
  const tasks = normalizeTaskList(options.task);
3926
4064
  if (tasks.length === 0) {
@@ -4097,7 +4235,39 @@ async function runCli(argv) {
4097
4235
  await upsertAliasEntry(normalized, commandLine);
4098
4236
  console.log(`\u5DF2\u5199\u5165 alias\uFF1A${normalized}`);
4099
4237
  });
4100
- program.command("alias").alias("aliases").description("\u6D4F\u89C8\u5168\u5C40 alias \u914D\u7F6E").action(async () => {
4238
+ const aliasCommand = program.command("alias").alias("aliases").description("\u6D4F\u89C8\u5168\u5C40 alias \u914D\u7F6E\uFF08alias run \u53EF\u6267\u884C\u5E76\u8FFD\u52A0\u547D\u4EE4\uFF09");
4239
+ aliasCommand.command("run <name> [addition...]").description("\u6267\u884C alias \u5E76\u8FFD\u52A0\u547D\u4EE4").allowUnknownOption(true).allowExcessArguments(true).action(async (name) => {
4240
+ const normalized = normalizeAliasName(name);
4241
+ if (!normalized) {
4242
+ throw new Error("alias \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A\u4E14\u4E0D\u80FD\u5305\u542B\u7A7A\u767D\u5B57\u7B26");
4243
+ }
4244
+ const filePath = getGlobalConfigPath();
4245
+ const exists = await import_fs_extra12.default.pathExists(filePath);
4246
+ if (!exists) {
4247
+ throw new Error(`\u672A\u627E\u5230 alias \u914D\u7F6E\u6587\u4EF6\uFF1A${filePath}`);
4248
+ }
4249
+ const content = await import_fs_extra12.default.readFile(filePath, "utf8");
4250
+ const entries = parseAliasEntries(content);
4251
+ const entry = entries.find((item) => item.name === normalized);
4252
+ if (!entry) {
4253
+ throw new Error(`\u672A\u627E\u5230 alias\uFF1A${normalized}`);
4254
+ }
4255
+ const aliasTokens = normalizeAliasCommandArgs(splitCommandArgs(entry.command));
4256
+ const additionTokens = extractAliasRunArgs(effectiveArgv, normalized);
4257
+ const mergedTokens = mergeAliasCommandArgs(aliasTokens, additionTokens);
4258
+ if (mergedTokens.length === 0) {
4259
+ throw new Error("alias \u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A");
4260
+ }
4261
+ const nextArgv = [process.argv[0], process.argv[1], "run", ...mergedTokens];
4262
+ const originalArgv = process.argv;
4263
+ process.argv = nextArgv;
4264
+ try {
4265
+ await runCli(nextArgv);
4266
+ } finally {
4267
+ process.argv = originalArgv;
4268
+ }
4269
+ });
4270
+ aliasCommand.action(async () => {
4101
4271
  await runAliasViewer();
4102
4272
  });
4103
4273
  await program.parseAsync(effectiveArgv);