clawt 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,22 +19,19 @@ npm i -g clawt
19
19
  ```bash
20
20
  # 1. 在项目根目录(包含 .git 的目录)下执行
21
21
  # 2. 并行执行 3 个任务,每个任务在独立的 worktree 中运行
22
- clawt run -b feature-auth \
23
- --tasks "实现用户登录功能" \
24
- --tasks "实现用户注册功能" \
25
- --tasks "实现密码重置功能"
22
+ clawt run -b <branch-1>
23
+ clawt run -b <branch-2>
24
+ clawt run -b <branch-3>
26
25
 
27
26
  # 3. 查看所有 worktree 状态
28
27
  clawt status
29
28
 
30
29
  # 4. 验证某个分支的变更(在主 worktree 中测试)
31
- clawt validate -b feature-auth-1
30
+ clawt validate -b branch-1
32
31
 
33
32
  # 5. 确认无误后合并到主分支
34
- clawt merge -b feature-auth-1 -m "feat: 实现用户登录功能"
33
+ clawt merge -b branch-1 -m "feat: 实现xxx功能"
35
34
 
36
- # 6. 清理不需要的 worktree
37
- clawt remove -b feature-auth
38
35
  ```
39
36
 
40
37
  ## 命令一览
@@ -153,6 +150,33 @@ clawt config # 查看当前配置
153
150
  clawt config reset # 恢复默认配置
154
151
  ```
155
152
 
153
+ ### `clawt alias` — 管理命令别名
154
+
155
+ ```bash
156
+ clawt alias # 列出所有命令别名
157
+ clawt alias list # 列出所有命令别名
158
+ clawt alias set <alias> <command> # 设置命令别名
159
+ clawt alias remove <alias> # 移除命令别名
160
+ ```
161
+
162
+ **使用示例:**
163
+
164
+ ```bash
165
+ # 设置别名
166
+ clawt alias set l list
167
+ clawt alias set r run
168
+ clawt alias set v validate
169
+
170
+ # 使用别名(等同于对应的完整命令)
171
+ clawt l # 等同于 clawt list
172
+ clawt r task.md # 等同于 clawt run task.md
173
+
174
+ # 移除别名
175
+ clawt alias remove l
176
+ ```
177
+
178
+ > **约束:** 别名不能覆盖内置命令名,目标必须是已注册的内置命令。别名的选项和参数会完全透传给目标命令。
179
+
156
180
  ## 配置
157
181
 
158
182
  配置文件位于 `~/.clawt/config.json`,安装后自动生成:
@@ -165,6 +189,7 @@ clawt config reset # 恢复默认配置
165
189
  | `confirmDestructiveOps` | `true` | 破坏性操作前确认 |
166
190
  | `maxConcurrency` | `0` | run 命令最大并发数,`0` 为不限制 |
167
191
  | `terminalApp` | `"auto"` | 批量 resume 使用的终端:`auto` / `iterm2` / `terminal` |
192
+ | `aliases` | `{}` | 命令别名映射(如 `{"l": "list", "r": "run"}`) |
168
193
 
169
194
  ## 全局选项
170
195
 
package/dist/index.js CHANGED
@@ -119,7 +119,7 @@ var MERGE_MESSAGES = {
119
119
  /** merge 后清理 worktree 和分支成功 */
120
120
  WORKTREE_CLEANED: (branch) => `\u2713 \u5DF2\u6E05\u7406 worktree \u548C\u5206\u652F: ${branch}`,
121
121
  /** 目标 worktree 有未提交修改但未指定 -m */
122
- TARGET_WORKTREE_DIRTY_NO_MESSAGE: "\u76EE\u6807 worktree \u6709\u672A\u63D0\u4EA4\u7684\u4FEE\u6539\uFF0C\u8BF7\u901A\u8FC7 -m \u53C2\u6570\u63D0\u4F9B\u63D0\u4EA4\u4FE1\u606F",
122
+ TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath) => `${worktreePath} \u6709\u672A\u63D0\u4EA4\u7684\u4FEE\u6539\uFF0C\u8BF7\u901A\u8FC7 -m \u53C2\u6570\u63D0\u4F9B\u63D0\u4EA4\u4FE1\u606F`,
123
123
  /** 目标 worktree 既干净又无本地提交 */
124
124
  TARGET_WORKTREE_NO_CHANGES: "\u76EE\u6807 worktree \u6CA1\u6709\u4EFB\u4F55\u53EF\u5408\u5E76\u7684\u53D8\u66F4\uFF08\u5DE5\u4F5C\u533A\u5E72\u51C0\u4E14\u65E0\u672C\u5730\u63D0\u4EA4\uFF09",
125
125
  /** merge 命令检测到 validate 状态的提示 */
@@ -280,6 +280,24 @@ var STATUS_MESSAGES = {
280
280
  STATUS_SNAPSHOT_ORPHANED: "(\u5BF9\u5E94 worktree \u5DF2\u4E0D\u5B58\u5728)"
281
281
  };
282
282
 
283
+ // src/constants/messages/alias.ts
284
+ var ALIAS_MESSAGES = {
285
+ /** 别名列表为空 */
286
+ ALIAS_LIST_EMPTY: "(\u65E0\u522B\u540D)",
287
+ /** 别名设置成功 */
288
+ ALIAS_SET_SUCCESS: (alias, command) => `\u2713 \u5DF2\u8BBE\u7F6E\u522B\u540D: ${alias} \u2192 ${command}`,
289
+ /** 别名移除成功 */
290
+ ALIAS_REMOVE_SUCCESS: (alias) => `\u2713 \u5DF2\u79FB\u9664\u522B\u540D: ${alias}`,
291
+ /** 别名不存在 */
292
+ ALIAS_NOT_FOUND: (alias) => `\u522B\u540D "${alias}" \u4E0D\u5B58\u5728`,
293
+ /** 别名与内置命令冲突 */
294
+ ALIAS_CONFLICTS_BUILTIN: (alias) => `\u522B\u540D "${alias}" \u4E0E\u5185\u7F6E\u547D\u4EE4\u51B2\u7A81\uFF0C\u4E0D\u5141\u8BB8\u8986\u76D6\u5185\u7F6E\u547D\u4EE4`,
295
+ /** 目标命令不存在 */
296
+ ALIAS_TARGET_NOT_FOUND: (command) => `\u76EE\u6807\u547D\u4EE4 "${command}" \u4E0D\u5B58\u5728\uFF0C\u8BF7\u6307\u5B9A\u5DF2\u6CE8\u518C\u7684\u5185\u7F6E\u547D\u4EE4\u540D`,
297
+ /** 别名列表标题 */
298
+ ALIAS_LIST_TITLE: "\u5F53\u524D\u522B\u540D\u5217\u8868\uFF1A"
299
+ };
300
+
283
301
  // src/constants/messages/index.ts
284
302
  var MESSAGES = {
285
303
  ...COMMON_MESSAGES,
@@ -292,7 +310,8 @@ var MESSAGES = {
292
310
  ...REMOVE_MESSAGES,
293
311
  ...RESET_MESSAGES,
294
312
  ...CONFIG_CMD_MESSAGES,
295
- ...STATUS_MESSAGES
313
+ ...STATUS_MESSAGES,
314
+ ...ALIAS_MESSAGES
296
315
  };
297
316
 
298
317
  // src/constants/exitCodes.ts
@@ -335,6 +354,10 @@ var CONFIG_DEFINITIONS = {
335
354
  terminalApp: {
336
355
  defaultValue: "auto",
337
356
  description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09"
357
+ },
358
+ aliases: {
359
+ defaultValue: {},
360
+ description: "\u547D\u4EE4\u522B\u540D\u6620\u5C04\uFF08\u901A\u8FC7 clawt alias \u547D\u4EE4\u7BA1\u7406\uFF09"
338
361
  }
339
362
  };
340
363
  function deriveDefaultConfig(definitions) {
@@ -907,12 +930,15 @@ function loadConfig() {
907
930
  return { ...DEFAULT_CONFIG };
908
931
  }
909
932
  }
933
+ function writeConfig(config2) {
934
+ writeFileSync(CONFIG_PATH, JSON.stringify(config2, null, 2), "utf-8");
935
+ }
910
936
  function writeDefaultConfig() {
911
- writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
937
+ writeConfig(DEFAULT_CONFIG);
912
938
  }
913
939
  function getConfigValue(key) {
914
- const config = loadConfig();
915
- return config[key];
940
+ const config2 = loadConfig();
941
+ return config2[key];
916
942
  }
917
943
  function ensureClawtDirs() {
918
944
  ensureDir(CLAWT_HOME);
@@ -1692,6 +1718,19 @@ async function executeBatchTasks(worktrees, tasks, concurrency) {
1692
1718
  printTaskSummary(summary);
1693
1719
  }
1694
1720
 
1721
+ // src/utils/alias.ts
1722
+ function applyAliases(program2, aliases) {
1723
+ for (const [alias, commandName] of Object.entries(aliases)) {
1724
+ const targetCmd = program2.commands.find((cmd) => cmd.name() === commandName);
1725
+ if (targetCmd) {
1726
+ targetCmd.alias(alias);
1727
+ logger.debug(`\u5DF2\u6CE8\u518C\u522B\u540D: ${alias} \u2192 ${commandName}`);
1728
+ } else {
1729
+ logger.warn(`\u522B\u540D "${alias}" \u7684\u76EE\u6807\u547D\u4EE4 "${commandName}" \u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7`);
1730
+ }
1731
+ }
1732
+ }
1733
+
1695
1734
  // src/commands/list.ts
1696
1735
  import chalk4 from "chalk";
1697
1736
  function registerListCommand(program2) {
@@ -2205,7 +2244,7 @@ async function handleMerge(options) {
2205
2244
  const targetClean = isWorkingDirClean(targetWorktreePath);
2206
2245
  if (!targetClean) {
2207
2246
  if (!options.message) {
2208
- throw new ClawtError(MESSAGES.TARGET_WORKTREE_DIRTY_NO_MESSAGE);
2247
+ throw new ClawtError(MESSAGES.TARGET_WORKTREE_DIRTY_NO_MESSAGE(targetWorktreePath));
2209
2248
  }
2210
2249
  gitAddAll(targetWorktreePath);
2211
2250
  gitCommit(options.message, targetWorktreePath);
@@ -2270,7 +2309,7 @@ function registerConfigCommand(program2) {
2270
2309
  });
2271
2310
  }
2272
2311
  function handleConfig() {
2273
- const config = loadConfig();
2312
+ const config2 = loadConfig();
2274
2313
  logger.info("config \u547D\u4EE4\u6267\u884C\uFF0C\u5C55\u793A\u5168\u5C40\u914D\u7F6E");
2275
2314
  printInfo(`
2276
2315
  ${chalk5.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
@@ -2279,7 +2318,7 @@ ${chalk5.dim("\u914D\u7F6E\u6587\u4EF6\u8DEF\u5F84:")} ${CONFIG_PATH}
2279
2318
  const keys = Object.keys(DEFAULT_CONFIG);
2280
2319
  for (let i = 0; i < keys.length; i++) {
2281
2320
  const key = keys[i];
2282
- const value = config[key];
2321
+ const value = config2[key];
2283
2322
  const description = CONFIG_DESCRIPTIONS[key];
2284
2323
  const formattedValue = formatConfigValue(value);
2285
2324
  if (i === 0) printInfo("");
@@ -2306,6 +2345,14 @@ function formatConfigValue(value) {
2306
2345
  if (typeof value === "boolean") {
2307
2346
  return value ? chalk5.green("true") : chalk5.yellow("false");
2308
2347
  }
2348
+ if (typeof value === "object" && value !== null) {
2349
+ const entries = Object.entries(value);
2350
+ if (entries.length === 0) {
2351
+ return chalk5.dim("(\u65E0)");
2352
+ }
2353
+ const lines = entries.map(([k, v]) => ` ${chalk5.cyan(k)} \u2192 ${chalk5.cyan(String(v))}`);
2354
+ return "\n" + lines.join("\n");
2355
+ }
2309
2356
  return chalk5.cyan(String(value));
2310
2357
  }
2311
2358
 
@@ -2571,6 +2618,74 @@ function printSnapshotsSection(snapshots) {
2571
2618
  printInfo("");
2572
2619
  }
2573
2620
 
2621
+ // src/commands/alias.ts
2622
+ import chalk7 from "chalk";
2623
+ function getRegisteredCommandNames(program2) {
2624
+ return program2.commands.map((cmd) => cmd.name());
2625
+ }
2626
+ function isBuiltinCommand(program2, alias) {
2627
+ return getRegisteredCommandNames(program2).includes(alias);
2628
+ }
2629
+ function handleAliasList() {
2630
+ const config2 = loadConfig();
2631
+ const { aliases } = config2;
2632
+ const entries = Object.entries(aliases);
2633
+ logger.debug("alias list \u547D\u4EE4\u6267\u884C\uFF0C\u5C55\u793A\u522B\u540D\u5217\u8868");
2634
+ if (entries.length === 0) {
2635
+ printInfo(MESSAGES.ALIAS_LIST_EMPTY);
2636
+ return;
2637
+ }
2638
+ printInfo(`
2639
+ ${MESSAGES.ALIAS_LIST_TITLE}
2640
+ `);
2641
+ printSeparator();
2642
+ for (const [alias, command] of entries) {
2643
+ printInfo(` ${chalk7.bold(alias)} \u2192 ${chalk7.cyan(command)}`);
2644
+ }
2645
+ printInfo("");
2646
+ printSeparator();
2647
+ }
2648
+ function handleAliasSet(program2, alias, command) {
2649
+ logger.debug(`alias set \u547D\u4EE4\u6267\u884C\uFF0C\u522B\u540D: ${alias}\uFF0C\u76EE\u6807: ${command}`);
2650
+ if (isBuiltinCommand(program2, alias)) {
2651
+ printError(MESSAGES.ALIAS_CONFLICTS_BUILTIN(alias));
2652
+ return;
2653
+ }
2654
+ if (!isBuiltinCommand(program2, command)) {
2655
+ printError(MESSAGES.ALIAS_TARGET_NOT_FOUND(command));
2656
+ return;
2657
+ }
2658
+ const config2 = loadConfig();
2659
+ config2.aliases[alias] = command;
2660
+ writeConfig(config2);
2661
+ printSuccess(MESSAGES.ALIAS_SET_SUCCESS(alias, command));
2662
+ }
2663
+ function handleAliasRemove(alias) {
2664
+ logger.debug(`alias remove \u547D\u4EE4\u6267\u884C\uFF0C\u522B\u540D: ${alias}`);
2665
+ const config2 = loadConfig();
2666
+ if (!(alias in config2.aliases)) {
2667
+ printError(MESSAGES.ALIAS_NOT_FOUND(alias));
2668
+ return;
2669
+ }
2670
+ delete config2.aliases[alias];
2671
+ writeConfig(config2);
2672
+ printSuccess(MESSAGES.ALIAS_REMOVE_SUCCESS(alias));
2673
+ }
2674
+ function registerAliasCommand(program2) {
2675
+ const aliasCmd = program2.command("alias").description("\u7BA1\u7406\u547D\u4EE4\u522B\u540D").action(() => {
2676
+ handleAliasList();
2677
+ });
2678
+ aliasCmd.command("list").description("\u5217\u51FA\u6240\u6709\u522B\u540D").action(() => {
2679
+ handleAliasList();
2680
+ });
2681
+ aliasCmd.command("set <alias> <command>").description("\u8BBE\u7F6E\u547D\u4EE4\u522B\u540D").action((alias, command) => {
2682
+ handleAliasSet(program2, alias, command);
2683
+ });
2684
+ aliasCmd.command("remove <alias>").description("\u79FB\u9664\u547D\u4EE4\u522B\u540D").action((alias) => {
2685
+ handleAliasRemove(alias);
2686
+ });
2687
+ }
2688
+
2574
2689
  // src/index.ts
2575
2690
  var require2 = createRequire(import.meta.url);
2576
2691
  var { version } = require2("../package.json");
@@ -2593,6 +2708,9 @@ registerConfigCommand(program);
2593
2708
  registerSyncCommand(program);
2594
2709
  registerResetCommand(program);
2595
2710
  registerStatusCommand(program);
2711
+ registerAliasCommand(program);
2712
+ var config = loadConfig();
2713
+ applyAliases(program, config.aliases);
2596
2714
  process.on("uncaughtException", (error) => {
2597
2715
  if (error instanceof ClawtError) {
2598
2716
  printError(error.message);
@@ -111,7 +111,7 @@ var MERGE_MESSAGES = {
111
111
  /** merge 后清理 worktree 和分支成功 */
112
112
  WORKTREE_CLEANED: (branch) => `\u2713 \u5DF2\u6E05\u7406 worktree \u548C\u5206\u652F: ${branch}`,
113
113
  /** 目标 worktree 有未提交修改但未指定 -m */
114
- TARGET_WORKTREE_DIRTY_NO_MESSAGE: "\u76EE\u6807 worktree \u6709\u672A\u63D0\u4EA4\u7684\u4FEE\u6539\uFF0C\u8BF7\u901A\u8FC7 -m \u53C2\u6570\u63D0\u4F9B\u63D0\u4EA4\u4FE1\u606F",
114
+ TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath) => `${worktreePath} \u6709\u672A\u63D0\u4EA4\u7684\u4FEE\u6539\uFF0C\u8BF7\u901A\u8FC7 -m \u53C2\u6570\u63D0\u4F9B\u63D0\u4EA4\u4FE1\u606F`,
115
115
  /** 目标 worktree 既干净又无本地提交 */
116
116
  TARGET_WORKTREE_NO_CHANGES: "\u76EE\u6807 worktree \u6CA1\u6709\u4EFB\u4F55\u53EF\u5408\u5E76\u7684\u53D8\u66F4\uFF08\u5DE5\u4F5C\u533A\u5E72\u51C0\u4E14\u65E0\u672C\u5730\u63D0\u4EA4\uFF09",
117
117
  /** merge 命令检测到 validate 状态的提示 */
@@ -272,6 +272,24 @@ var STATUS_MESSAGES = {
272
272
  STATUS_SNAPSHOT_ORPHANED: "(\u5BF9\u5E94 worktree \u5DF2\u4E0D\u5B58\u5728)"
273
273
  };
274
274
 
275
+ // src/constants/messages/alias.ts
276
+ var ALIAS_MESSAGES = {
277
+ /** 别名列表为空 */
278
+ ALIAS_LIST_EMPTY: "(\u65E0\u522B\u540D)",
279
+ /** 别名设置成功 */
280
+ ALIAS_SET_SUCCESS: (alias, command) => `\u2713 \u5DF2\u8BBE\u7F6E\u522B\u540D: ${alias} \u2192 ${command}`,
281
+ /** 别名移除成功 */
282
+ ALIAS_REMOVE_SUCCESS: (alias) => `\u2713 \u5DF2\u79FB\u9664\u522B\u540D: ${alias}`,
283
+ /** 别名不存在 */
284
+ ALIAS_NOT_FOUND: (alias) => `\u522B\u540D "${alias}" \u4E0D\u5B58\u5728`,
285
+ /** 别名与内置命令冲突 */
286
+ ALIAS_CONFLICTS_BUILTIN: (alias) => `\u522B\u540D "${alias}" \u4E0E\u5185\u7F6E\u547D\u4EE4\u51B2\u7A81\uFF0C\u4E0D\u5141\u8BB8\u8986\u76D6\u5185\u7F6E\u547D\u4EE4`,
287
+ /** 目标命令不存在 */
288
+ ALIAS_TARGET_NOT_FOUND: (command) => `\u76EE\u6807\u547D\u4EE4 "${command}" \u4E0D\u5B58\u5728\uFF0C\u8BF7\u6307\u5B9A\u5DF2\u6CE8\u518C\u7684\u5185\u7F6E\u547D\u4EE4\u540D`,
289
+ /** 别名列表标题 */
290
+ ALIAS_LIST_TITLE: "\u5F53\u524D\u522B\u540D\u5217\u8868\uFF1A"
291
+ };
292
+
275
293
  // src/constants/messages/index.ts
276
294
  var MESSAGES = {
277
295
  ...COMMON_MESSAGES,
@@ -284,7 +302,8 @@ var MESSAGES = {
284
302
  ...REMOVE_MESSAGES,
285
303
  ...RESET_MESSAGES,
286
304
  ...CONFIG_CMD_MESSAGES,
287
- ...STATUS_MESSAGES
305
+ ...STATUS_MESSAGES,
306
+ ...ALIAS_MESSAGES
288
307
  };
289
308
 
290
309
  // src/constants/config.ts
@@ -312,6 +331,10 @@ var CONFIG_DEFINITIONS = {
312
331
  terminalApp: {
313
332
  defaultValue: "auto",
314
333
  description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09"
334
+ },
335
+ aliases: {
336
+ defaultValue: {},
337
+ description: "\u547D\u4EE4\u522B\u540D\u6620\u5C04\uFF08\u901A\u8FC7 clawt alias \u547D\u4EE4\u7BA1\u7406\uFF09"
315
338
  }
316
339
  };
317
340
  function deriveDefaultConfig(definitions) {
package/docs/spec.md CHANGED
@@ -26,6 +26,7 @@
26
26
  - [5.12 将主分支代码同步到目标 Worktree](#512-将主分支代码同步到目标-worktree)
27
27
  - [5.13 重置主 Worktree 工作区和暂存区](#513-重置主-worktree-工作区和暂存区)
28
28
  - [5.14 项目全局状态总览](#514-项目全局状态总览)
29
+ - [5.15 命令别名管理](#515-命令别名管理)
29
30
  - [6. 错误处理规范](#6-错误处理规范)
30
31
  - [7. 非功能性需求](#7-非功能性需求)
31
32
  - [7.1 性能](#71-性能)
@@ -179,6 +180,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
179
180
  | `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
180
181
  | `clawt reset` | 重置主 worktree 工作区和暂存区 | 5.13 |
181
182
  | `clawt status` | 显示项目全局状态总览(支持 `--json` 格式输出) | 5.14 |
183
+ | `clawt alias` | 管理命令别名(列出 / 设置 / 移除) | 5.15 |
182
184
 
183
185
  **全局选项:**
184
186
 
@@ -751,7 +753,7 @@ clawt merge [-m <commitMessage>]
751
753
  5. **根据目标 worktree 状态决定是否需要提交**
752
754
  - 检测目标 worktree 工作区是否干净(`git status --porcelain`)
753
755
  - **工作区有未提交修改**:
754
- - 如果用户未提供 `-m`,提示 `目标 worktree 有未提交的修改,请通过 -m 参数提供提交信息`,退出
756
+ - 如果用户未提供 `-m`,提示 `<worktreePath> 有未提交的修改,请通过 -m 参数提供提交信息`(其中 `<worktreePath>` 为目标 worktree 的完整路径),退出
755
757
  - 提供了 `-m` → 执行提交:
756
758
  ```bash
757
759
  cd ~/.clawt/worktrees/<project>/<branchName>
@@ -842,7 +844,8 @@ clawt merge [-m <commitMessage>]
842
844
  "autoPullPush": false,
843
845
  "confirmDestructiveOps": true,
844
846
  "maxConcurrency": 0,
845
- "terminalApp": "auto"
847
+ "terminalApp": "auto",
848
+ "aliases": {}
846
849
  }
847
850
  ```
848
851
 
@@ -856,6 +859,7 @@ clawt merge [-m <commitMessage>]
856
859
  | `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean、config reset)前是否提示确认 |
857
860
  | `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
858
861
  | `terminalApp` | `string` | `"auto"` | 批量 resume 使用的终端应用:`auto`(自动检测)、`iterm2`、`terminal`(macOS) |
862
+ | `aliases` | `Record<string, string>` | `{}` | 命令别名映射,键为别名,值为目标内置命令名 |
859
863
 
860
864
  ---
861
865
 
@@ -1350,6 +1354,112 @@ clawt status [--json]
1350
1354
 
1351
1355
  ---
1352
1356
 
1357
+ ### 5.15 命令别名管理
1358
+
1359
+ **命令:**
1360
+
1361
+ ```bash
1362
+ # 列出所有命令别名
1363
+ clawt alias
1364
+ clawt alias list
1365
+
1366
+ # 设置命令别名
1367
+ clawt alias set <alias> <command>
1368
+
1369
+ # 移除命令别名
1370
+ clawt alias remove <alias>
1371
+ ```
1372
+
1373
+ **子命令:**
1374
+
1375
+ | 子命令 | 说明 |
1376
+ | ------ | ---- |
1377
+ | `clawt alias` / `clawt alias list` | 列出所有已配置的命令别名 |
1378
+ | `clawt alias set <alias> <command>` | 设置命令别名,将 `<alias>` 映射到 `<command>` |
1379
+ | `clawt alias remove <alias>` | 移除指定的命令别名 |
1380
+
1381
+ **参数:**
1382
+
1383
+ | 参数 | 必填 | 说明 |
1384
+ | ---- | ---- | ---- |
1385
+ | `<alias>` | 是(set / remove) | 别名名称 |
1386
+ | `<command>` | 是(set) | 目标内置命令名 |
1387
+
1388
+ **约束规则:**
1389
+
1390
+ 1. **别名不能覆盖内置命令名**:别名不能与已注册的内置命令同名(`list`、`create`、`remove`、`run`、`resume`、`validate`、`merge`、`config`、`sync`、`reset`、`status`、`alias`)。如果用户尝试设置与内置命令同名的别名,报错退出
1391
+ 2. **目标必须是内置命令**:别名的目标(`<command>`)必须是已注册的内置命令名。如果指定了不存在的目标命令,报错退出
1392
+ 3. **参数透传**:通过别名调用时,所有选项和参数会完全透传给目标命令,行为与直接调用目标命令完全一致
1393
+
1394
+ **持久化:**
1395
+
1396
+ 别名配置存储在 `~/.clawt/config.json` 的 `aliases` 字段中(类型 `Record<string, string>`,默认 `{}`)。
1397
+
1398
+ **运行流程:**
1399
+
1400
+ #### `alias list`(默认)
1401
+
1402
+ 1. 读取配置文件中的 `aliases` 字段
1403
+ 2. 如果没有配置任何别名,输出提示 `当前没有配置任何命令别名`
1404
+ 3. 如果有别名,逐行输出所有别名映射
1405
+
1406
+ **输出格式:**
1407
+
1408
+ ```
1409
+ 命令别名列表:
1410
+
1411
+ l → list
1412
+ r → run
1413
+ v → validate
1414
+ ```
1415
+
1416
+ #### `alias set <alias> <command>`
1417
+
1418
+ 1. **校验别名不与内置命令冲突**:检查 `<alias>` 是否为内置命令名,是则报错退出
1419
+ 2. **校验目标命令存在**:检查 `<command>` 是否为已注册的内置命令名,不是则报错退出
1420
+ 3. 将别名写入配置文件的 `aliases` 字段(如果别名已存在,覆盖旧值)
1421
+ 4. 输出成功提示
1422
+
1423
+ **输出格式:**
1424
+
1425
+ ```
1426
+ ✓ 已设置别名: l → list
1427
+ ```
1428
+
1429
+ #### `alias remove <alias>`
1430
+
1431
+ 1. 读取配置文件中的 `aliases` 字段
1432
+ 2. 检查指定的别名是否存在,不存在则报错退出
1433
+ 3. 从 `aliases` 中删除该别名并写入配置文件
1434
+ 4. 输出成功提示
1435
+
1436
+ **输出格式:**
1437
+
1438
+ ```
1439
+ ✓ 已移除别名: l
1440
+ ```
1441
+
1442
+ **别名使用示例:**
1443
+
1444
+ ```bash
1445
+ # 设置别名
1446
+ clawt alias set l list
1447
+ clawt alias set r run
1448
+ clawt alias set v validate
1449
+
1450
+ # 使用别名(等同于对应的完整命令)
1451
+ clawt l # 等同于 clawt list
1452
+ clawt r task.md # 等同于 clawt run task.md
1453
+
1454
+ # 查看所有别名
1455
+ clawt alias list
1456
+
1457
+ # 移除别名
1458
+ clawt alias remove l
1459
+ ```
1460
+
1461
+ ---
1462
+
1353
1463
  ## 6. 错误处理规范
1354
1464
 
1355
1465
  ### 6.1 通用错误处理
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,132 @@
1
+ import type { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { MESSAGES } from '../constants/index.js';
4
+ import { logger } from '../logger/index.js';
5
+ import { loadConfig, writeConfig, printInfo, printSuccess, printError, printSeparator } from '../utils/index.js';
6
+
7
+ /**
8
+ * 从 Commander 实例动态获取所有已注册的命令名
9
+ * @param {Command} program - Commander 实例
10
+ * @returns {string[]} 已注册的命令名列表
11
+ */
12
+ function getRegisteredCommandNames(program: Command): string[] {
13
+ return program.commands.map((cmd) => cmd.name());
14
+ }
15
+
16
+ /**
17
+ * 校验别名名称是否与已注册命令冲突
18
+ * @param {Command} program - Commander 实例
19
+ * @param {string} alias - 别名
20
+ * @returns {boolean} 是否冲突
21
+ */
22
+ function isBuiltinCommand(program: Command, alias: string): boolean {
23
+ return getRegisteredCommandNames(program).includes(alias);
24
+ }
25
+
26
+ /**
27
+ * 列出所有已配置的别名
28
+ */
29
+ function handleAliasList(): void {
30
+ const config = loadConfig();
31
+ const { aliases } = config;
32
+ const entries = Object.entries(aliases);
33
+
34
+ logger.debug('alias list 命令执行,展示别名列表');
35
+
36
+ if (entries.length === 0) {
37
+ printInfo(MESSAGES.ALIAS_LIST_EMPTY);
38
+ return;
39
+ }
40
+
41
+ printInfo(`\n${MESSAGES.ALIAS_LIST_TITLE}\n`);
42
+ printSeparator();
43
+
44
+ for (const [alias, command] of entries) {
45
+ printInfo(` ${chalk.bold(alias)} → ${chalk.cyan(command)}`);
46
+ }
47
+
48
+ printInfo('');
49
+ printSeparator();
50
+ }
51
+
52
+ /**
53
+ * 设置命令别名
54
+ * @param {Command} program - Commander 实例
55
+ * @param {string} alias - 别名
56
+ * @param {string} command - 目标命令名
57
+ */
58
+ function handleAliasSet(program: Command, alias: string, command: string): void {
59
+ logger.debug(`alias set 命令执行,别名: ${alias},目标: ${command}`);
60
+
61
+ // 校验别名不能与内置命令冲突
62
+ if (isBuiltinCommand(program, alias)) {
63
+ printError(MESSAGES.ALIAS_CONFLICTS_BUILTIN(alias));
64
+ return;
65
+ }
66
+
67
+ // 校验目标命令必须是已注册的内置命令
68
+ if (!isBuiltinCommand(program, command)) {
69
+ printError(MESSAGES.ALIAS_TARGET_NOT_FOUND(command));
70
+ return;
71
+ }
72
+
73
+ const config = loadConfig();
74
+ config.aliases[alias] = command;
75
+ writeConfig(config);
76
+
77
+ printSuccess(MESSAGES.ALIAS_SET_SUCCESS(alias, command));
78
+ }
79
+
80
+ /**
81
+ * 移除命令别名
82
+ * @param {string} alias - 要移除的别名
83
+ */
84
+ function handleAliasRemove(alias: string): void {
85
+ logger.debug(`alias remove 命令执行,别名: ${alias}`);
86
+
87
+ const config = loadConfig();
88
+
89
+ if (!(alias in config.aliases)) {
90
+ printError(MESSAGES.ALIAS_NOT_FOUND(alias));
91
+ return;
92
+ }
93
+
94
+ delete config.aliases[alias];
95
+ writeConfig(config);
96
+
97
+ printSuccess(MESSAGES.ALIAS_REMOVE_SUCCESS(alias));
98
+ }
99
+
100
+ /**
101
+ * 注册 alias 命令组:管理命令别名
102
+ * @param {Command} program - Commander 实例
103
+ */
104
+ export function registerAliasCommand(program: Command): void {
105
+ const aliasCmd = program
106
+ .command('alias')
107
+ .description('管理命令别名')
108
+ .action(() => {
109
+ handleAliasList();
110
+ });
111
+
112
+ aliasCmd
113
+ .command('list')
114
+ .description('列出所有别名')
115
+ .action(() => {
116
+ handleAliasList();
117
+ });
118
+
119
+ aliasCmd
120
+ .command('set <alias> <command>')
121
+ .description('设置命令别名')
122
+ .action((alias: string, command: string) => {
123
+ handleAliasSet(program, alias, command);
124
+ });
125
+
126
+ aliasCmd
127
+ .command('remove <alias>')
128
+ .description('移除命令别名')
129
+ .action((alias: string) => {
130
+ handleAliasRemove(alias);
131
+ });
132
+ }
@@ -84,5 +84,14 @@ function formatConfigValue(value: ClawtConfig[keyof ClawtConfig]): string {
84
84
  if (typeof value === 'boolean') {
85
85
  return value ? chalk.green('true') : chalk.yellow('false');
86
86
  }
87
+ // 对象类型(如 aliases)按键值对逐行展示
88
+ if (typeof value === 'object' && value !== null) {
89
+ const entries = Object.entries(value);
90
+ if (entries.length === 0) {
91
+ return chalk.dim('(无)');
92
+ }
93
+ const lines = entries.map(([k, v]) => ` ${chalk.cyan(k)} → ${chalk.cyan(String(v))}`);
94
+ return '\n' + lines.join('\n');
95
+ }
87
96
  return chalk.cyan(String(value));
88
97
  }
@@ -165,7 +165,7 @@ async function handleMerge(options: MergeOptions): Promise<void> {
165
165
  if (!targetClean) {
166
166
  // 目标 worktree 有未提交修改,必须提供 -m
167
167
  if (!options.message) {
168
- throw new ClawtError(MESSAGES.TARGET_WORKTREE_DIRTY_NO_MESSAGE);
168
+ throw new ClawtError(MESSAGES.TARGET_WORKTREE_DIRTY_NO_MESSAGE(targetWorktreePath));
169
169
  }
170
170
  gitAddAll(targetWorktreePath);
171
171
  gitCommit(options.message, targetWorktreePath);
@@ -33,6 +33,10 @@ export const CONFIG_DEFINITIONS: ConfigDefinitions = {
33
33
  defaultValue: 'auto',
34
34
  description: '批量 resume 使用的终端应用:auto(自动检测)、iterm2、terminal(macOS)',
35
35
  },
36
+ aliases: {
37
+ defaultValue: {} as Record<string, string>,
38
+ description: '命令别名映射(通过 clawt alias 命令管理)',
39
+ },
36
40
  };
37
41
 
38
42
  /**
@@ -0,0 +1,22 @@
1
+ /** alias 命令专属提示消息 */
2
+ export const ALIAS_MESSAGES = {
3
+ /** 别名列表为空 */
4
+ ALIAS_LIST_EMPTY: '(无别名)',
5
+ /** 别名设置成功 */
6
+ ALIAS_SET_SUCCESS: (alias: string, command: string) =>
7
+ `✓ 已设置别名: ${alias} → ${command}`,
8
+ /** 别名移除成功 */
9
+ ALIAS_REMOVE_SUCCESS: (alias: string) =>
10
+ `✓ 已移除别名: ${alias}`,
11
+ /** 别名不存在 */
12
+ ALIAS_NOT_FOUND: (alias: string) =>
13
+ `别名 "${alias}" 不存在`,
14
+ /** 别名与内置命令冲突 */
15
+ ALIAS_CONFLICTS_BUILTIN: (alias: string) =>
16
+ `别名 "${alias}" 与内置命令冲突,不允许覆盖内置命令`,
17
+ /** 目标命令不存在 */
18
+ ALIAS_TARGET_NOT_FOUND: (command: string) =>
19
+ `目标命令 "${command}" 不存在,请指定已注册的内置命令名`,
20
+ /** 别名列表标题 */
21
+ ALIAS_LIST_TITLE: '当前别名列表:',
22
+ } as const;
@@ -9,6 +9,7 @@ import { REMOVE_MESSAGES } from './remove.js';
9
9
  import { RESET_MESSAGES } from './reset.js';
10
10
  import { CONFIG_CMD_MESSAGES } from './config.js';
11
11
  import { STATUS_MESSAGES } from './status.js';
12
+ import { ALIAS_MESSAGES } from './alias.js';
12
13
 
13
14
  /**
14
15
  * 提示消息模板
@@ -26,4 +27,5 @@ export const MESSAGES = {
26
27
  ...RESET_MESSAGES,
27
28
  ...CONFIG_CMD_MESSAGES,
28
29
  ...STATUS_MESSAGES,
30
+ ...ALIAS_MESSAGES,
29
31
  } as const;
@@ -11,7 +11,8 @@ export const MERGE_MESSAGES = {
11
11
  /** merge 后清理 worktree 和分支成功 */
12
12
  WORKTREE_CLEANED: (branch: string) => `✓ 已清理 worktree 和分支: ${branch}`,
13
13
  /** 目标 worktree 有未提交修改但未指定 -m */
14
- TARGET_WORKTREE_DIRTY_NO_MESSAGE: '目标 worktree 有未提交的修改,请通过 -m 参数提供提交信息',
14
+ TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath: string) =>
15
+ `${worktreePath} 有未提交的修改,请通过 -m 参数提供提交信息`,
15
16
  /** 目标 worktree 既干净又无本地提交 */
16
17
  TARGET_WORKTREE_NO_CHANGES: '目标 worktree 没有任何可合并的变更(工作区干净且无本地提交)',
17
18
  /** merge 命令检测到 validate 状态的提示 */
@@ -45,7 +45,8 @@ export const MESSAGES = {
45
45
  /** 请提供提交信息 */
46
46
  COMMIT_MESSAGE_REQUIRED: '请提供提交信息(-m 参数)',
47
47
  /** 目标 worktree 有未提交修改但未指定 -m */
48
- TARGET_WORKTREE_DIRTY_NO_MESSAGE: '目标 worktree 有未提交的修改,请通过 -m 参数提供提交信息',
48
+ TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath: string) =>
49
+ `${worktreePath} 有未提交的修改,请通过 -m 参数提供提交信息`,
49
50
  /** 目标 worktree 既干净又无本地提交 */
50
51
  TARGET_WORKTREE_NO_CHANGES: '目标 worktree 没有任何可合并的变更(工作区干净且无本地提交)',
51
52
  /** 检测到用户中断 */
package/src/index.ts CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander';
3
3
  import { ClawtError } from './errors/index.js';
4
4
  import { logger, enableConsoleTransport } from './logger/index.js';
5
5
  import { EXIT_CODES } from './constants/index.js';
6
- import { printError, ensureClawtDirs } from './utils/index.js';
6
+ import { printError, ensureClawtDirs, loadConfig, applyAliases } from './utils/index.js';
7
7
  import { registerListCommand } from './commands/list.js';
8
8
  import { registerCreateCommand } from './commands/create.js';
9
9
  import { registerRemoveCommand } from './commands/remove.js';
@@ -15,6 +15,7 @@ import { registerConfigCommand } from './commands/config.js';
15
15
  import { registerSyncCommand } from './commands/sync.js';
16
16
  import { registerResetCommand } from './commands/reset.js';
17
17
  import { registerStatusCommand } from './commands/status.js';
18
+ import { registerAliasCommand } from './commands/alias.js';
18
19
 
19
20
  // 从 package.json 读取版本号,避免硬编码
20
21
  const require = createRequire(import.meta.url);
@@ -50,6 +51,11 @@ registerConfigCommand(program);
50
51
  registerSyncCommand(program);
51
52
  registerResetCommand(program);
52
53
  registerStatusCommand(program);
54
+ registerAliasCommand(program);
55
+
56
+ // 加载配置并应用命令别名
57
+ const config = loadConfig();
58
+ applyAliases(program, config.aliases);
53
59
 
54
60
  // 全局未捕获异常处理
55
61
  process.on('uncaughtException', (error) => {
@@ -12,6 +12,8 @@ export interface ClawtConfig {
12
12
  maxConcurrency: number;
13
13
  /** 批量 resume 使用的终端应用:'auto'(自动检测)、'iterm2'、'terminal'(macOS) */
14
14
  terminalApp: string;
15
+ /** 命令别名映射,键为别名,值为目标内置命令名 */
16
+ aliases: Record<string, string>;
15
17
  }
16
18
 
17
19
  /** 单个配置项的完整定义(默认值 + 描述) */
@@ -0,0 +1,20 @@
1
+ import type { Command } from 'commander';
2
+ import type { ClawtConfig } from '../types/index.js';
3
+ import { logger } from '../logger/index.js';
4
+
5
+ /**
6
+ * 根据配置中的别名映射,为已注册的命令添加 Commander.js 别名
7
+ * @param {Command} program - Commander 实例
8
+ * @param {ClawtConfig['aliases']} aliases - 别名映射
9
+ */
10
+ export function applyAliases(program: Command, aliases: ClawtConfig['aliases']): void {
11
+ for (const [alias, commandName] of Object.entries(aliases)) {
12
+ const targetCmd = program.commands.find((cmd) => cmd.name() === commandName);
13
+ if (targetCmd) {
14
+ targetCmd.alias(alias);
15
+ logger.debug(`已注册别名: ${alias} → ${commandName}`);
16
+ } else {
17
+ logger.warn(`别名 "${alias}" 的目标命令 "${commandName}" 不存在,已跳过`);
18
+ }
19
+ }
20
+ }
@@ -24,11 +24,19 @@ export function loadConfig(): ClawtConfig {
24
24
  }
25
25
  }
26
26
 
27
+ /**
28
+ * 将指定配置对象写入配置文件
29
+ * @param {ClawtConfig} config - 要写入的完整配置对象
30
+ */
31
+ export function writeConfig(config: ClawtConfig): void {
32
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
33
+ }
34
+
27
35
  /**
28
36
  * 将默认配置写入配置文件
29
37
  */
30
38
  export function writeDefaultConfig(): void {
31
- writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
39
+ writeConfig(DEFAULT_CONFIG);
32
40
  }
33
41
 
34
42
  /**
@@ -48,7 +48,7 @@ export {
48
48
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
49
49
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
50
50
  export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
51
- export { loadConfig, writeDefaultConfig, getConfigValue, ensureClawtDirs } from './config.js';
51
+ export { loadConfig, writeDefaultConfig, writeConfig, getConfigValue, ensureClawtDirs } from './config.js';
52
52
  export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration } from './formatter.js';
53
53
  export { ensureDir, removeEmptyDir } from './fs.js';
54
54
  export { multilineInput } from './prompt.js';
@@ -60,4 +60,5 @@ export { ProgressRenderer } from './progress.js';
60
60
  export { parseTaskFile, loadTaskFile } from './task-file.js';
61
61
  export { executeBatchTasks } from './task-executor.js';
62
62
  export { detectTerminalApp, openCommandInNewTerminalTab } from './terminal.js';
63
+ export { applyAliases } from './alias.js';
63
64
 
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { Command } from 'commander';
3
+
4
+ // mock 依赖模块
5
+ vi.mock('../../../src/logger/index.js', () => ({
6
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
7
+ }));
8
+
9
+ vi.mock('../../../src/utils/index.js', () => ({
10
+ loadConfig: vi.fn(),
11
+ writeConfig: vi.fn(),
12
+ printInfo: vi.fn(),
13
+ printSuccess: vi.fn(),
14
+ printError: vi.fn(),
15
+ printSeparator: vi.fn(),
16
+ }));
17
+
18
+ vi.mock('../../../src/constants/index.js', () => ({
19
+ MESSAGES: {
20
+ ALIAS_LIST_EMPTY: '(无别名)',
21
+ ALIAS_LIST_TITLE: '当前别名列表:',
22
+ ALIAS_SET_SUCCESS: (a: string, c: string) => `✓ 已设置别名: ${a} → ${c}`,
23
+ ALIAS_REMOVE_SUCCESS: (a: string) => `✓ 已移除别名: ${a}`,
24
+ ALIAS_NOT_FOUND: (a: string) => `别名 "${a}" 不存在`,
25
+ ALIAS_CONFLICTS_BUILTIN: (a: string) => `别名 "${a}" 与内置命令冲突,不允许覆盖内置命令`,
26
+ ALIAS_TARGET_NOT_FOUND: (c: string) => `目标命令 "${c}" 不存在,请指定已注册的内置命令名`,
27
+ },
28
+ }));
29
+
30
+ import { registerAliasCommand } from '../../../src/commands/alias.js';
31
+ import { loadConfig, writeConfig, printInfo, printSuccess, printError } from '../../../src/utils/index.js';
32
+
33
+ const mockedLoadConfig = vi.mocked(loadConfig);
34
+ const mockedWriteConfig = vi.mocked(writeConfig);
35
+ const mockedPrintInfo = vi.mocked(printInfo);
36
+ const mockedPrintSuccess = vi.mocked(printSuccess);
37
+ const mockedPrintError = vi.mocked(printError);
38
+
39
+ beforeEach(() => {
40
+ vi.clearAllMocks();
41
+ });
42
+
43
+ /** 构造默认配置 mock 数据 */
44
+ function mockDefaultConfig(aliases: Record<string, string> = {}) {
45
+ return {
46
+ autoDeleteBranch: false,
47
+ claudeCodeCommand: 'claude',
48
+ autoPullPush: false,
49
+ confirmDestructiveOps: true,
50
+ maxConcurrency: 0,
51
+ terminalApp: 'auto',
52
+ aliases,
53
+ };
54
+ }
55
+
56
+ describe('registerAliasCommand', () => {
57
+ it('注册 alias 命令及 list/set/remove 子命令', () => {
58
+ const program = new Command();
59
+ registerAliasCommand(program);
60
+ const aliasCmd = program.commands.find((c) => c.name() === 'alias');
61
+ expect(aliasCmd).toBeDefined();
62
+ expect(aliasCmd!.commands.find((c) => c.name() === 'list')).toBeDefined();
63
+ expect(aliasCmd!.commands.find((c) => c.name() === 'set')).toBeDefined();
64
+ expect(aliasCmd!.commands.find((c) => c.name() === 'remove')).toBeDefined();
65
+ });
66
+ });
67
+
68
+ describe('alias list(通过 action 间接测试)', () => {
69
+ it('无别名时展示空提示', () => {
70
+ mockedLoadConfig.mockReturnValue(mockDefaultConfig());
71
+
72
+ const program = new Command();
73
+ program.exitOverride();
74
+ registerAliasCommand(program);
75
+ program.parse(['alias'], { from: 'user' });
76
+
77
+ expect(mockedPrintInfo).toHaveBeenCalledWith('(无别名)');
78
+ });
79
+
80
+ it('通过 alias list 子命令展示别名列表', () => {
81
+ mockedLoadConfig.mockReturnValue(mockDefaultConfig({ ls: 'list', rm: 'remove' }));
82
+
83
+ const program = new Command();
84
+ program.exitOverride();
85
+ registerAliasCommand(program);
86
+ program.parse(['alias', 'list'], { from: 'user' });
87
+
88
+ // 应展示列表标题
89
+ expect(mockedPrintInfo).toHaveBeenCalled();
90
+ const calls = mockedPrintInfo.mock.calls.map((c) => c[0]);
91
+ // 至少有一个调用包含别名信息
92
+ expect(calls.some((c) => typeof c === 'string' && c.includes('ls'))).toBe(true);
93
+ });
94
+ });
95
+
96
+ describe('alias set(通过 action 间接测试)', () => {
97
+ it('别名与内置命令冲突时报错', () => {
98
+ const program = new Command();
99
+ program.exitOverride();
100
+ program.command('list').action(() => {});
101
+ registerAliasCommand(program);
102
+ program.parse(['alias', 'set', 'list', 'create'], { from: 'user' });
103
+
104
+ expect(mockedPrintError).toHaveBeenCalled();
105
+ expect(mockedWriteConfig).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('目标命令不存在时报错', () => {
109
+ mockedLoadConfig.mockReturnValue(mockDefaultConfig());
110
+
111
+ const program = new Command();
112
+ program.exitOverride();
113
+ registerAliasCommand(program);
114
+ program.parse(['alias', 'set', 'ls', 'nonexistent'], { from: 'user' });
115
+
116
+ expect(mockedPrintError).toHaveBeenCalled();
117
+ expect(mockedWriteConfig).not.toHaveBeenCalled();
118
+ });
119
+
120
+ it('正常设置别名', () => {
121
+ mockedLoadConfig.mockReturnValue(mockDefaultConfig());
122
+
123
+ const program = new Command();
124
+ program.exitOverride();
125
+ program.command('list').action(() => {});
126
+ registerAliasCommand(program);
127
+ program.parse(['alias', 'set', 'ls', 'list'], { from: 'user' });
128
+
129
+ expect(mockedWriteConfig).toHaveBeenCalledWith(
130
+ expect.objectContaining({
131
+ aliases: { ls: 'list' },
132
+ }),
133
+ );
134
+ expect(mockedPrintSuccess).toHaveBeenCalled();
135
+ });
136
+
137
+ it('覆盖已有别名', () => {
138
+ mockedLoadConfig.mockReturnValue(mockDefaultConfig({ ls: 'list' }));
139
+
140
+ const program = new Command();
141
+ program.exitOverride();
142
+ program.command('list').action(() => {});
143
+ program.command('status').action(() => {});
144
+ registerAliasCommand(program);
145
+ program.parse(['alias', 'set', 'ls', 'status'], { from: 'user' });
146
+
147
+ expect(mockedWriteConfig).toHaveBeenCalledWith(
148
+ expect.objectContaining({
149
+ aliases: { ls: 'status' },
150
+ }),
151
+ );
152
+ expect(mockedPrintSuccess).toHaveBeenCalled();
153
+ });
154
+ });
155
+
156
+ describe('alias remove(通过 action 间接测试)', () => {
157
+ it('别名不存在时报错', () => {
158
+ mockedLoadConfig.mockReturnValue(mockDefaultConfig());
159
+
160
+ const program = new Command();
161
+ program.exitOverride();
162
+ registerAliasCommand(program);
163
+ program.parse(['alias', 'remove', 'nonexistent'], { from: 'user' });
164
+
165
+ expect(mockedPrintError).toHaveBeenCalled();
166
+ expect(mockedWriteConfig).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('正常移除别名', () => {
170
+ mockedLoadConfig.mockReturnValue(mockDefaultConfig({ ls: 'list' }));
171
+
172
+ const program = new Command();
173
+ program.exitOverride();
174
+ registerAliasCommand(program);
175
+ program.parse(['alias', 'remove', 'ls'], { from: 'user' });
176
+
177
+ expect(mockedWriteConfig).toHaveBeenCalledWith(
178
+ expect.objectContaining({
179
+ aliases: {},
180
+ }),
181
+ );
182
+ expect(mockedPrintSuccess).toHaveBeenCalled();
183
+ });
184
+ });
@@ -26,7 +26,8 @@ vi.mock('../../../src/constants/index.js', () => ({
26
26
  MERGE_SQUASH_PENDING: (path: string, branch: string) => `请手动提交: ${path}`,
27
27
  MERGE_VALIDATE_STATE_HINT: (branch: string) => `分支 ${branch} 存在 validate 状态`,
28
28
  MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改',
29
- TARGET_WORKTREE_DIRTY_NO_MESSAGE: '目标 worktree 有未提交修改,请提供 -m 参数',
29
+ TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath: string) =>
30
+ `${worktreePath} 有未提交修改,请提供 -m 参数`,
30
31
  TARGET_WORKTREE_NO_CHANGES: '没有可合并的变更',
31
32
  MERGE_CONFLICT: '合并冲突',
32
33
  PULL_CONFLICT: 'pull 冲突',
@@ -31,6 +31,7 @@ describe('DEFAULT_CONFIG', () => {
31
31
  expect(DEFAULT_CONFIG.autoPullPush).toBe(false);
32
32
  expect(DEFAULT_CONFIG.confirmDestructiveOps).toBe(true);
33
33
  expect(DEFAULT_CONFIG.maxConcurrency).toBe(0);
34
+ expect(DEFAULT_CONFIG.aliases).toEqual({});
34
35
  });
35
36
  });
36
37
 
@@ -37,6 +37,8 @@ describe('MESSAGES', () => {
37
37
  'SYNC_SELECT_BRANCH',
38
38
  'PULL_CONFLICT',
39
39
  'PUSH_FAILED',
40
+ 'ALIAS_LIST_EMPTY',
41
+ 'ALIAS_LIST_TITLE',
40
42
  ] as const;
41
43
 
42
44
  it.each(stringKeys)('%s 是非空字符串', (key) => {
@@ -236,5 +238,31 @@ describe('MESSAGES', () => {
236
238
  const result = MESSAGES.SYNC_MULTIPLE_MATCHES('feat');
237
239
  expect(result).toContain('feat');
238
240
  });
241
+
242
+ it('ALIAS_SET_SUCCESS 包含别名和命令', () => {
243
+ const result = MESSAGES.ALIAS_SET_SUCCESS('ls', 'list');
244
+ expect(result).toContain('ls');
245
+ expect(result).toContain('list');
246
+ });
247
+
248
+ it('ALIAS_REMOVE_SUCCESS 包含别名', () => {
249
+ const result = MESSAGES.ALIAS_REMOVE_SUCCESS('ls');
250
+ expect(result).toContain('ls');
251
+ });
252
+
253
+ it('ALIAS_NOT_FOUND 包含别名', () => {
254
+ const result = MESSAGES.ALIAS_NOT_FOUND('xxx');
255
+ expect(result).toContain('xxx');
256
+ });
257
+
258
+ it('ALIAS_CONFLICTS_BUILTIN 包含别名', () => {
259
+ const result = MESSAGES.ALIAS_CONFLICTS_BUILTIN('list');
260
+ expect(result).toContain('list');
261
+ });
262
+
263
+ it('ALIAS_TARGET_NOT_FOUND 包含命令名', () => {
264
+ const result = MESSAGES.ALIAS_TARGET_NOT_FOUND('xxx');
265
+ expect(result).toContain('xxx');
266
+ });
239
267
  });
240
268
  });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Command } from 'commander';
3
+
4
+ vi.mock('../../../src/logger/index.js', () => ({
5
+ logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
6
+ }));
7
+
8
+ import { applyAliases } from '../../../src/utils/alias.js';
9
+
10
+ describe('applyAliases', () => {
11
+ it('为已注册命令添加别名', () => {
12
+ const program = new Command();
13
+ const listCmd = program.command('list').action(() => {});
14
+
15
+ applyAliases(program, { ls: 'list' });
16
+
17
+ expect(listCmd.aliases()).toContain('ls');
18
+ });
19
+
20
+ it('目标命令不存在时静默跳过', () => {
21
+ const program = new Command();
22
+ program.command('list').action(() => {});
23
+
24
+ // 不应抛出异常
25
+ applyAliases(program, { xx: 'nonexistent' });
26
+
27
+ const listCmd = program.commands.find((c) => c.name() === 'list');
28
+ expect(listCmd!.aliases()).not.toContain('xx');
29
+ });
30
+
31
+ it('空别名映射时不做任何操作', () => {
32
+ const program = new Command();
33
+ program.command('list').action(() => {});
34
+
35
+ applyAliases(program, {});
36
+
37
+ const listCmd = program.commands.find((c) => c.name() === 'list');
38
+ expect(listCmd!.aliases()).toEqual([]);
39
+ });
40
+
41
+ it('支持多个别名映射到不同命令', () => {
42
+ const program = new Command();
43
+ const listCmd = program.command('list').action(() => {});
44
+ const removeCmd = program.command('remove').action(() => {});
45
+
46
+ applyAliases(program, { ls: 'list', rm: 'remove' });
47
+
48
+ expect(listCmd.aliases()).toContain('ls');
49
+ expect(removeCmd.aliases()).toContain('rm');
50
+ });
51
+ });
@@ -18,7 +18,7 @@ vi.mock('../../../src/utils/fs.js', () => ({
18
18
  }));
19
19
 
20
20
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
21
- import { loadConfig, getConfigValue, writeDefaultConfig, ensureClawtDirs } from '../../../src/utils/config.js';
21
+ import { loadConfig, getConfigValue, writeDefaultConfig, writeConfig, ensureClawtDirs } from '../../../src/utils/config.js';
22
22
  import { DEFAULT_CONFIG } from '../../../src/constants/index.js';
23
23
  import { ensureDir } from '../../../src/utils/fs.js';
24
24
 
@@ -77,6 +77,18 @@ describe('writeDefaultConfig', () => {
77
77
  });
78
78
  });
79
79
 
80
+ describe('writeConfig', () => {
81
+ it('将指定配置写入配置文件', () => {
82
+ const customConfig = { ...DEFAULT_CONFIG, aliases: { ls: 'list' } };
83
+ writeConfig(customConfig);
84
+ expect(mockedWriteFileSync).toHaveBeenCalledWith(
85
+ expect.any(String),
86
+ JSON.stringify(customConfig, null, 2),
87
+ 'utf-8',
88
+ );
89
+ });
90
+ });
91
+
80
92
  describe('ensureClawtDirs', () => {
81
93
  it('确保三个全局目录存在', () => {
82
94
  ensureClawtDirs();