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 +33 -8
- package/dist/index.js +126 -8
- package/dist/postinstall.js +25 -2
- package/docs/spec.md +112 -2
- package/package.json +1 -1
- package/src/commands/alias.ts +132 -0
- package/src/commands/config.ts +9 -0
- package/src/commands/merge.ts +1 -1
- package/src/constants/config.ts +4 -0
- package/src/constants/messages/alias.ts +22 -0
- package/src/constants/messages/index.ts +2 -0
- package/src/constants/messages/merge.ts +2 -1
- package/src/constants/messages.ts +2 -1
- package/src/index.ts +7 -1
- package/src/types/config.ts +2 -0
- package/src/utils/alias.ts +20 -0
- package/src/utils/config.ts +9 -1
- package/src/utils/index.ts +2 -1
- package/tests/unit/commands/alias.test.ts +184 -0
- package/tests/unit/commands/merge.test.ts +2 -1
- package/tests/unit/constants/config.test.ts +1 -0
- package/tests/unit/constants/messages.test.ts +28 -0
- package/tests/unit/utils/alias.test.ts +51 -0
- package/tests/unit/utils/config.test.ts +13 -1
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
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
30
|
+
clawt validate -b branch-1
|
|
32
31
|
|
|
33
32
|
# 5. 确认无误后合并到主分支
|
|
34
|
-
clawt merge -b
|
|
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:
|
|
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
|
-
|
|
937
|
+
writeConfig(DEFAULT_CONFIG);
|
|
912
938
|
}
|
|
913
939
|
function getConfigValue(key) {
|
|
914
|
-
const
|
|
915
|
-
return
|
|
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
|
|
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 =
|
|
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);
|
package/dist/postinstall.js
CHANGED
|
@@ -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:
|
|
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`,提示
|
|
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
|
@@ -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
|
+
}
|
package/src/commands/config.ts
CHANGED
|
@@ -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
|
}
|
package/src/commands/merge.ts
CHANGED
|
@@ -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);
|
package/src/constants/config.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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) => {
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -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
|
-
|
|
39
|
+
writeConfig(DEFAULT_CONFIG);
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
package/src/utils/index.ts
CHANGED
|
@@ -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:
|
|
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 冲突',
|
|
@@ -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();
|