clawt 3.4.3 → 3.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ import {
16
16
  hasLocalCommits,
17
17
  getSnapshotModifiedTime,
18
18
  getProjectSnapshotBranches,
19
- getBranchCreatedAt,
19
+ getWorktreeCreatedTime,
20
20
  formatRelativeTime,
21
21
  printInfo,
22
22
  printDoubleSeparator,
@@ -46,7 +46,7 @@ export function registerStatusCommand(program: Command): void {
46
46
  * @param {StatusOptions} options - 命令选项
47
47
  */
48
48
  async function handleStatus(options: StatusOptions): Promise<void> {
49
- runPreChecks({ mainWorktree: true, headExists: true });
49
+ await runPreChecks({ requireMainWorktree: true, requireHead: true });
50
50
 
51
51
  // 交互式面板模式
52
52
  if (options.interactive) {
@@ -116,7 +116,7 @@ function collectWorktreeDetailedStatus(worktree: WorktreeInfo, projectName: stri
116
116
  const changeStatus = detectChangeStatus(worktree);
117
117
  const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
118
118
  const { insertions, deletions } = countDiffStat(worktree.path);
119
- const createdAt = resolveBranchCreatedAt(worktree.branch);
119
+ const createdAt = getWorktreeCreatedTime(worktree.path);
120
120
 
121
121
  return {
122
122
  path: worktree.path,
@@ -183,19 +183,6 @@ function countDiffStat(worktreePath: string): { insertions: number; deletions: n
183
183
  }
184
184
  }
185
185
 
186
- /**
187
- * 获取分支的创建时间
188
- * @param {string} branchName - 分支名
189
- * @returns {string | null} ISO 8601 格式的创建时间,获取失败时返回 null
190
- */
191
- function resolveBranchCreatedAt(branchName: string): string | null {
192
- try {
193
- return getBranchCreatedAt(branchName);
194
- } catch {
195
- return null;
196
- }
197
- }
198
-
199
186
  /**
200
187
  * 获取分支的 validate 快照修改时间
201
188
  * @param {string} projectName - 项目名
@@ -3,6 +3,7 @@ import { logger } from '../logger/index.js';
3
3
  import { ClawtError } from '../errors/index.js';
4
4
  import { MESSAGES, AUTO_SAVE_COMMIT_MESSAGE } from '../constants/index.js';
5
5
  import type { SyncOptions } from '../types/index.js';
6
+ import { PRE_CHECK_SYNC } from '../constants/index.js';
6
7
  import {
7
8
  runPreChecks,
8
9
  getGitTopLevel,
@@ -19,7 +20,6 @@ import {
19
20
  getMainWorkBranch,
20
21
  rebuildValidateBranch,
21
22
  getValidateBranchName,
22
- guardMainWorkBranch,
23
23
  } from '../utils/index.js';
24
24
  import type { WorktreeResolveMessages } from '../utils/index.js';
25
25
 
@@ -127,8 +127,7 @@ export async function executeSyncForBranch(targetWorktreePath: string, branch: s
127
127
  * @param {SyncOptions} options - 命令选项
128
128
  */
129
129
  async function handleSync(options: SyncOptions): Promise<void> {
130
- runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
131
- await guardMainWorkBranch();
130
+ await runPreChecks(PRE_CHECK_SYNC);
132
131
 
133
132
  logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
134
133
 
@@ -97,7 +97,7 @@ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: s
97
97
  * @param {ValidateOptions} options - 命令选项
98
98
  */
99
99
  async function handleValidateClean(options: ValidateOptions): Promise<void> {
100
- runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
100
+ await runPreChecks({ requireMainWorktree: true, requireHead: true, requireProjectConfig: true });
101
101
 
102
102
  const projectName = getProjectName();
103
103
  const mainWorktreePath = getGitTopLevel();
@@ -261,7 +261,7 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
261
261
  return;
262
262
  }
263
263
 
264
- runPreChecks({ mainWorktree: true, headExists: true, projectConfig: true });
264
+ await runPreChecks({ requireMainWorktree: true, requireHead: true, requireProjectConfig: true });
265
265
 
266
266
  const projectName = getProjectName();
267
267
  const mainWorktreePath = getGitTopLevel();
@@ -43,3 +43,4 @@ export {
43
43
  PANEL_DATE_SEPARATOR_PREFIX,
44
44
  PANEL_FIXED_ROWS,
45
45
  } from './interactive-panel.js';
46
+ export { PRE_CHECK_CREATE, PRE_CHECK_RUN, PRE_CHECK_DRY_RUN, PRE_CHECK_MERGE, PRE_CHECK_SYNC, PRE_CHECK_RESUME } from './pre-checks.js';
@@ -31,6 +31,8 @@ export const PANEL_SHORTCUT_KEYS = {
31
31
  RESUME: 'r',
32
32
  /** 同步 */
33
33
  SYNC: 's',
34
+ /** 覆盖 */
35
+ COVER: 'c',
34
36
  /** 手动刷新 */
35
37
  REFRESH: 'f',
36
38
  /** 退出 */
@@ -8,6 +8,7 @@ const SHORTCUT_LABELS: Record<keyof typeof PANEL_SHORTCUT_KEYS, string> = {
8
8
  DELETE: '删除',
9
9
  RESUME: '恢复',
10
10
  SYNC: '同步',
11
+ COVER: '覆盖',
11
12
  REFRESH: '刷新',
12
13
  QUIT: '退出',
13
14
  };
@@ -0,0 +1,30 @@
1
+ import type { PreCheckOptions } from '../utils/validation.js';
2
+
3
+ /** create 命令:主 worktree + HEAD + 切换分支 + 工作区干净 */
4
+ export const PRE_CHECK_CREATE: PreCheckOptions = {
5
+ requireMainWorktree: true, requireHead: true,
6
+ ensureOnClawtMainWorkBranch: true, requireCleanWorkingDir: true,
7
+ } as const;
8
+
9
+ /** run 命令正常模式(不含 requireClaudeCode,因为 claudeCode 在 dryRun 分支之后才需要) */
10
+ export const PRE_CHECK_RUN: PreCheckOptions = { ...PRE_CHECK_CREATE } as const;
11
+
12
+ /** run dry-run 模式 */
13
+ export const PRE_CHECK_DRY_RUN: PreCheckOptions = { requireMainWorktree: true, requireHead: true } as const;
14
+
15
+ /** merge 命令 */
16
+ export const PRE_CHECK_MERGE: PreCheckOptions = {
17
+ requireMainWorktree: true, requireHead: true,
18
+ ensureOnClawtMainWorkBranch: true,
19
+ } as const;
20
+
21
+ /** sync 命令 */
22
+ export const PRE_CHECK_SYNC: PreCheckOptions = {
23
+ requireMainWorktree: true, requireHead: true, requireProjectConfig: true,
24
+ ensureOnClawtMainWorkBranch: true,
25
+ } as const;
26
+
27
+ /** resume 命令 */
28
+ export const PRE_CHECK_RESUME: PreCheckOptions = {
29
+ requireMainWorktree: true, requireHead: true, requireClaudeCode: true,
30
+ } as const;
@@ -50,7 +50,8 @@ export {
50
50
  createBranch,
51
51
  } from './git.js';
52
52
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
53
- export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, runPreChecks } from './validation.js';
53
+ export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, validateWorkingDirClean, runPreChecks } from './validation.js';
54
+ export type { PreCheckOptions } from './validation.js';
54
55
  export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
55
56
  export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs, parseConcurrency } from './config.js';
56
57
  export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration, formatRelativeTime, formatDiskSize, formatLocalISOString } from './formatter.js';
@@ -58,7 +59,7 @@ export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
58
59
  export { multilineInput } from './prompt.js';
59
60
  export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
60
61
  export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
61
- export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate } from './worktree-matcher.js';
62
+ export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate, getWorktreeCreatedTime } from './worktree-matcher.js';
62
63
  export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
63
64
  export { ProgressRenderer } from './progress.js';
64
65
  export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
@@ -70,7 +71,7 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
70
71
  export { applyAliases } from './alias.js';
71
72
  export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue, interactiveConfigEditor } from './config-strategy.js';
72
73
  export { checkForUpdates } from './update-checker.js';
73
- export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, guardMainWorkBranch, getValidateRunCommand } from './project-config.js';
74
+ export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, guardMainWorkBranchExists, getValidateRunCommand } from './project-config.js';
74
75
  export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
75
76
  export { safeStringify } from './json.js';
76
77
  export { executeRunCommand } from './validate-runner.js';
@@ -281,6 +281,11 @@ export class InteractivePanel {
281
281
  this.executeOperation(() => this.handleSync());
282
282
  return;
283
283
  }
284
+
285
+ if (key === PANEL_SHORTCUT_KEYS.COVER) {
286
+ this.executeOperation(() => this.handleCover());
287
+ return;
288
+ }
284
289
  }
285
290
 
286
291
  /**
@@ -571,6 +576,14 @@ export class InteractivePanel {
571
576
  runCommandInherited(`clawt sync -b ${branch}`);
572
577
  }
573
578
 
579
+ /**
580
+ * 执行覆盖操作
581
+ * cover 命令从主 worktree 当前所在的验证分支名自动推导目标分支
582
+ */
583
+ private handleCover(): void {
584
+ runCommandInherited('clawt cover');
585
+ }
586
+
574
587
  /**
575
588
  * 等待用户按回车键
576
589
  * @returns {Promise<void>} 用户按回车时 resolve
@@ -1,11 +1,10 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { PROJECTS_CONFIG_DIR, MESSAGES, VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
3
+ import { PROJECTS_CONFIG_DIR, MESSAGES } from '../constants/index.js';
4
4
  import { ClawtError } from '../errors/index.js';
5
5
  import { logger } from '../logger/index.js';
6
- import { getProjectName, checkBranchExists, getCurrentBranch } from './git.js';
6
+ import { getProjectName, checkBranchExists } from './git.js';
7
7
  import { ensureDir } from './fs.js';
8
- import { printWarning, confirmAction } from './formatter.js';
9
8
  import type { ProjectConfig } from '../types/index.js';
10
9
 
11
10
  /**
@@ -92,27 +91,6 @@ export function guardMainWorkBranchExists(cwd?: string): void {
92
91
  }
93
92
  }
94
93
 
95
- /**
96
- * 守卫检测:验证配置中的主工作分支是否有效
97
- * 分支不存在 → 抛出 ClawtError(致命错误)
98
- * 当前分支与配置分支不一致且非验证分支 → 警告并交互确认是否继续
99
- * @param {string} [cwd] - 工作目录
100
- */
101
- export async function guardMainWorkBranch(cwd?: string): Promise<void> {
102
- guardMainWorkBranchExists(cwd);
103
-
104
- const config = requireProjectConfig();
105
- const mainBranch = config.clawtMainWorkBranch;
106
- const currentBranch = getCurrentBranch(cwd);
107
- if (currentBranch !== mainBranch && !currentBranch.startsWith(VALIDATE_BRANCH_PREFIX)) {
108
- printWarning(MESSAGES.GUARD_BRANCH_MISMATCH(mainBranch, currentBranch));
109
- const confirmed = await confirmAction('是否继续执行?');
110
- if (!confirmed) {
111
- throw new ClawtError(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
112
- }
113
- }
114
- }
115
-
116
94
  /**
117
95
  * 从项目配置中获取 validate 成功后自动执行的命令
118
96
  * @returns {string | undefined} 配置的命令字符串,未配置时返回 undefined
@@ -1,9 +1,9 @@
1
1
  import Enquirer from 'enquirer';
2
- import { VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
2
+ import { VALIDATE_BRANCH_PREFIX, MESSAGES } from '../constants/index.js';
3
3
  import { logger } from '../logger/index.js';
4
4
  import { checkBranchExists, createBranch, deleteBranch, getCurrentBranch, gitCheckout, gitResetHard, gitCleanForce, isWorkingDirClean, gitAddAll, gitStashPush } from './git.js';
5
5
  import { getMainWorkBranch } from './project-config.js';
6
- import { printWarning } from './formatter.js';
6
+ import { printWarning, confirmAction } from './formatter.js';
7
7
  import { ClawtError } from '../errors/index.js';
8
8
 
9
9
  /**
@@ -133,7 +133,7 @@ export async function handleDirtyWorkingDir(cwd?: string): Promise<void> {
133
133
  * 三种情况:
134
134
  * 1. 已在主工作分支上 → 直接返回
135
135
  * 2. 在验证分支上 → 清理工作区后自动切回主工作分支
136
- * 3. 在其他分支上 → 处理脏工作区后切换到主工作分支
136
+ * 3. 在其他分支上 → 警告并确认后,处理脏工作区再切换到主工作分支
137
137
  * @param {string} [cwd] - 工作目录
138
138
  */
139
139
  export async function ensureOnMainWorkBranch(cwd?: string): Promise<void> {
@@ -157,7 +157,12 @@ export async function ensureOnMainWorkBranch(cwd?: string): Promise<void> {
157
157
  return;
158
158
  }
159
159
 
160
- // 当前在其他分支上,处理脏工作区后切换
160
+ // 当前在其他分支上,警告并确认后处理脏工作区再切换
161
+ printWarning(MESSAGES.GUARD_BRANCH_MISMATCH(mainBranch, currentBranch));
162
+ const confirmed = await confirmAction('是否继续执行?');
163
+ if (!confirmed) {
164
+ throw new ClawtError(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
165
+ }
161
166
  logger.info(`当前在分支 ${currentBranch} 上,需切换到主工作分支 ${mainBranch}`);
162
167
  if (!isWorkingDirClean(cwd)) {
163
168
  await handleDirtyWorkingDir(cwd);
@@ -1,19 +1,26 @@
1
1
  import { MESSAGES } from '../constants/index.js';
2
2
  import { ClawtError } from '../errors/index.js';
3
3
  import { execCommand } from './shell.js';
4
- import { getGitCommonDir } from './git.js';
4
+ import { getGitCommonDir, isWorkingDirClean } from './git.js';
5
5
  import { requireProjectConfig, guardMainWorkBranchExists } from './project-config.js';
6
+ import { ensureOnMainWorkBranch } from './validate-branch.js';
6
7
 
7
8
  /** 统一前置校验选项 */
8
- interface PreCheckOptions {
9
- /** 校验是否在主 worktree 根目录 */
10
- mainWorktree?: boolean;
9
+ export interface PreCheckOptions {
10
+ /** 校验当前目录是否在主 worktree 根目录 */
11
+ requireMainWorktree?: boolean;
11
12
  /** 校验 HEAD 是否存在(仓库有至少一次 commit) */
12
- headExists?: boolean;
13
- /** 校验项目是否已初始化(配置文件存在) */
14
- projectConfig?: boolean;
15
- /** 校验配置的主工作分支是否存在 */
16
- branchExists?: boolean;
13
+ requireHead?: boolean;
14
+ /** 校验项目配置文件是否存在且合法 */
15
+ requireProjectConfig?: boolean;
16
+ /** 校验配置中的主工作分支在 git 仓库中是否存在 */
17
+ requireMainBranchExists?: boolean;
18
+ /** 确保当前在主工作分支上,不在则自动切换 */
19
+ ensureOnClawtMainWorkBranch?: boolean;
20
+ /** 校验主分支工作区和暂存区是否干净 */
21
+ requireCleanWorkingDir?: boolean;
22
+ /** 校验 Claude Code CLI 是否已安装 */
23
+ requireClaudeCode?: boolean;
17
24
  }
18
25
 
19
26
  /**
@@ -38,6 +45,7 @@ export function validateMainWorktree(): void {
38
45
 
39
46
  /**
40
47
  * 校验 Git 是否已安装
48
+ * @deprecated 当前无调用方,保留函数不删除
41
49
  * @throws {ClawtError} Git 未安装时抛出
42
50
  */
43
51
  export function validateGitInstalled(): void {
@@ -73,26 +81,55 @@ export function validateHeadExists(): void {
73
81
  }
74
82
  }
75
83
 
84
+ /**
85
+ * 校验主分支工作区和暂存区是否干净
86
+ * 当存在未提交的更改时抛出错误,防止基于脏状态创建 worktree
87
+ * @throws {ClawtError} 工作区或暂存区不干净时抛出
88
+ */
89
+ export function validateWorkingDirClean(): void {
90
+ if (!isWorkingDirClean()) {
91
+ throw new ClawtError(MESSAGES.MAIN_WORKTREE_DIRTY);
92
+ }
93
+ }
94
+
76
95
  /**
77
96
  * 统一前置校验入口,按需执行各项校验
97
+ * 执行顺序:同步快速校验 → 异步交互校验 → 依赖前序结果的校验
78
98
  * @param {PreCheckOptions} options - 校验选项
79
- * @param {boolean} [options.mainWorktree] - 校验是否在主 worktree 根目录
80
- * @param {boolean} [options.headExists] - 校验 HEAD 是否存在
81
- * @param {boolean} [options.projectConfig] - 校验项目是否已初始化
82
- * @param {boolean} [options.branchExists] - 校验配置的主工作分支是否存在
99
+ * @param {boolean} [options.requireMainWorktree] - 校验当前目录是否在主 worktree 根目录
100
+ * @param {boolean} [options.requireHead] - 校验 HEAD 是否存在
101
+ * @param {boolean} [options.requireProjectConfig] - 校验项目配置文件是否存在且合法
102
+ * @param {boolean} [options.requireMainBranchExists] - 校验配置中的主工作分支在 git 仓库中是否存在
103
+ * @param {boolean} [options.ensureOnClawtMainWorkBranch] - 确保当前在主工作分支上,不在则自动切换
104
+ * @param {boolean} [options.requireCleanWorkingDir] - 校验主分支工作区和暂存区是否干净
105
+ * @param {boolean} [options.requireClaudeCode] - 校验 Claude Code CLI 是否已安装
83
106
  * @throws {ClawtError} 任一校验未通过时抛出
84
107
  */
85
- export function runPreChecks(options: PreCheckOptions): void {
86
- if (options.mainWorktree) {
108
+ export async function runPreChecks(options: PreCheckOptions): Promise<void> {
109
+ // 阶段 1:同步快速校验
110
+ if (options.requireMainWorktree) {
87
111
  validateMainWorktree();
88
112
  }
89
- if (options.headExists) {
113
+ if (options.requireHead) {
90
114
  validateHeadExists();
91
115
  }
92
- if (options.projectConfig) {
116
+ if (options.requireProjectConfig) {
93
117
  requireProjectConfig();
94
118
  }
95
- if (options.branchExists) {
119
+ if (options.requireMainBranchExists) {
96
120
  guardMainWorkBranchExists();
97
121
  }
122
+
123
+ // 阶段 2:异步交互校验
124
+ if (options.ensureOnClawtMainWorkBranch) {
125
+ await ensureOnMainWorkBranch();
126
+ }
127
+
128
+ // 阶段 3:依赖前序结果的校验
129
+ if (options.requireCleanWorkingDir) {
130
+ validateWorkingDirClean();
131
+ }
132
+ if (options.requireClaudeCode) {
133
+ validateClaudeCodeInstalled();
134
+ }
98
135
  }
@@ -310,6 +310,21 @@ export function getWorktreeCreatedDate(dirPath: string): string | null {
310
310
  }
311
311
  }
312
312
 
313
+ /**
314
+ * 获取 worktree 目录的创建时间(ISO 8601 格式)
315
+ * 通过文件系统的 birthtime 获取目录实际创建时间,保留小时/分钟级精度
316
+ * @param {string} dirPath - worktree 目录路径
317
+ * @returns {string | null} ISO 8601 格式的时间字符串,无法获取时返回 null
318
+ */
319
+ export function getWorktreeCreatedTime(dirPath: string): string | null {
320
+ try {
321
+ const stat = statSync(dirPath);
322
+ return stat.birthtime.toISOString();
323
+ } catch {
324
+ return null;
325
+ }
326
+ }
327
+
313
328
  /**
314
329
  * 将 YYYY-MM-DD 日期字符串格式化为中文相对日期描述
315
330
  * 基于自然日计算,适用于日期分组场景
@@ -15,13 +15,17 @@ vi.mock('../../../src/errors/index.js', () => ({
15
15
  },
16
16
  }));
17
17
 
18
- vi.mock('../../../src/constants/index.js', () => ({
19
- MESSAGES: {
20
- INVALID_COUNT: (val: string) => `数量必须为正整数: ${val}`,
21
- WORKTREE_CREATED: (count: number) => `✓ 已创建 ${count} 个 worktree`,
22
- },
23
- EXIT_CODES: { SUCCESS: 0, ERROR: 1, ARGUMENT_ERROR: 2 },
24
- }));
18
+ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
20
+ return {
21
+ ...actual,
22
+ MESSAGES: {
23
+ INVALID_COUNT: (val: string) => `数量必须为正整数: ${val}`,
24
+ WORKTREE_CREATED: (count: number) => `✓ 已创建 ${count} 个 worktree`,
25
+ },
26
+ EXIT_CODES: { SUCCESS: 0, ERROR: 1, ARGUMENT_ERROR: 2 },
27
+ };
28
+ });
25
29
 
26
30
  vi.mock('../../../src/utils/index.js', () => ({
27
31
  runPreChecks: vi.fn(),
@@ -5,12 +5,16 @@ vi.mock('../../../src/logger/index.js', () => ({
5
5
  logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
6
6
  }));
7
7
 
8
- vi.mock('../../../src/constants/index.js', () => ({
9
- MESSAGES: {
10
- NO_WORKTREES: '(无 worktree)',
11
- WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
12
- },
13
- }));
8
+ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
9
+ const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
10
+ return {
11
+ ...actual,
12
+ MESSAGES: {
13
+ NO_WORKTREES: '(无 worktree)',
14
+ WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
15
+ },
16
+ };
17
+ });
14
18
 
15
19
  vi.mock('../../../src/utils/index.js', () => ({
16
20
  runPreChecks: vi.fn(),
@@ -49,20 +53,20 @@ describe('registerListCommand', () => {
49
53
  });
50
54
 
51
55
  describe('handleList', () => {
52
- it('无 worktree 时文本输出', () => {
56
+ it('无 worktree 时文本输出', async () => {
53
57
  mockedGetProjectName.mockReturnValue('test-project');
54
58
  mockedGetProjectWorktrees.mockReturnValue([]);
55
59
 
56
60
  const program = new Command();
57
61
  program.exitOverride();
58
62
  registerListCommand(program);
59
- program.parse(['list'], { from: 'user' });
63
+ await program.parseAsync(['list'], { from: 'user' });
60
64
 
61
65
  expect(mockedRunPreChecks).toHaveBeenCalled();
62
66
  expect(mockedPrintInfo).toHaveBeenCalled();
63
67
  });
64
68
 
65
- it('有 worktree 时文本输出', () => {
69
+ it('有 worktree 时文本输出', async () => {
66
70
  mockedGetProjectName.mockReturnValue('test-project');
67
71
  mockedGetProjectWorktrees.mockReturnValue([
68
72
  { path: '/path/feature', branch: 'feature' },
@@ -74,12 +78,12 @@ describe('handleList', () => {
74
78
  const program = new Command();
75
79
  program.exitOverride();
76
80
  registerListCommand(program);
77
- program.parse(['list'], { from: 'user' });
81
+ await program.parseAsync(['list'], { from: 'user' });
78
82
 
79
83
  expect(mockedGetWorktreeStatus).toHaveBeenCalled();
80
84
  });
81
85
 
82
- it('--json 输出 JSON 格式', () => {
86
+ it('--json 输出 JSON 格式', async () => {
83
87
  mockedGetProjectName.mockReturnValue('test-project');
84
88
  mockedGetProjectWorktrees.mockReturnValue([
85
89
  { path: '/path/feature', branch: 'feature' },
@@ -89,7 +93,7 @@ describe('handleList', () => {
89
93
  const program = new Command();
90
94
  program.exitOverride();
91
95
  registerListCommand(program);
92
- program.parse(['list', '--json'], { from: 'user' });
96
+ await program.parseAsync(['list', '--json'], { from: 'user' });
93
97
 
94
98
  // 应通过 console.log 输出 JSON
95
99
  const jsonCall = consoleSpy.mock.calls.find((call) => {
@@ -101,7 +105,7 @@ describe('handleList', () => {
101
105
  expect(parsed.total).toBe(1);
102
106
  });
103
107
 
104
- it('worktree 状态不可用时显示提示', () => {
108
+ it('worktree 状态不可用时显示提示', async () => {
105
109
  mockedGetProjectName.mockReturnValue('test-project');
106
110
  mockedGetProjectWorktrees.mockReturnValue([
107
111
  { path: '/path/feature', branch: 'feature' },
@@ -111,7 +115,7 @@ describe('handleList', () => {
111
115
  const program = new Command();
112
116
  program.exitOverride();
113
117
  registerListCommand(program);
114
- program.parse(['list'], { from: 'user' });
118
+ await program.parseAsync(['list'], { from: 'user' });
115
119
 
116
120
  expect(mockedGetWorktreeStatus).toHaveBeenCalled();
117
121
  });
@@ -15,29 +15,33 @@ vi.mock('../../../src/errors/index.js', () => ({
15
15
  },
16
16
  }));
17
17
 
18
- vi.mock('../../../src/constants/index.js', () => ({
19
- MESSAGES: {
20
- MERGE_NO_WORKTREES: '没有可用的 worktree',
21
- MERGE_SELECT_BRANCH: '选择要合并的分支',
22
- MERGE_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
23
- MERGE_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
24
- MERGE_SQUASH_PROMPT: '是否压缩提交?',
25
- MERGE_SQUASH_COMMITTED: (branch: string) => `已压缩提交: ${branch}`,
26
- MERGE_SQUASH_PENDING: (path: string, branch: string) => `请手动提交: ${path}`,
27
- MERGE_VALIDATE_STATE_HINT: (branch: string) => `分支 ${branch} 存在 validate 状态`,
28
- MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改',
29
- TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath: string) =>
30
- `${worktreePath} 有未提交修改,请提供 -m 参数`,
31
- TARGET_WORKTREE_NO_CHANGES: '没有可合并的变更',
32
- MERGE_CONFLICT: '合并冲突',
33
- PULL_CONFLICT: 'pull 冲突',
34
- PUSH_FAILED: 'push 失败',
35
- MERGE_SUCCESS: (branch: string, message: string, autoPullPush: boolean) => `合并成功: ${branch}`,
36
- MERGE_SUCCESS_NO_MESSAGE: (branch: string, autoPullPush: boolean) => `合并成功: ${branch}`,
37
- WORKTREE_CLEANED: (branch: string) => `已清理: ${branch}`,
38
- },
39
- AUTO_SAVE_COMMIT_MESSAGE: 'clawt:auto-save',
40
- }));
18
+ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
19
+ const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
20
+ return {
21
+ ...actual,
22
+ MESSAGES: {
23
+ MERGE_NO_WORKTREES: '没有可用的 worktree',
24
+ MERGE_SELECT_BRANCH: '选择要合并的分支',
25
+ MERGE_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
26
+ MERGE_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
27
+ MERGE_SQUASH_PROMPT: '是否压缩提交?',
28
+ MERGE_SQUASH_COMMITTED: (branch: string) => `已压缩提交: ${branch}`,
29
+ MERGE_SQUASH_PENDING: (path: string, branch: string) => `请手动提交: ${path}`,
30
+ MERGE_VALIDATE_STATE_HINT: (branch: string) => `分支 ${branch} 存在 validate 状态`,
31
+ MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改',
32
+ TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath: string) =>
33
+ `${worktreePath} 有未提交修改,请提供 -m 参数`,
34
+ TARGET_WORKTREE_NO_CHANGES: '没有可合并的变更',
35
+ MERGE_CONFLICT: '合并冲突',
36
+ PULL_CONFLICT: 'pull 冲突',
37
+ PUSH_FAILED: 'push 失败',
38
+ MERGE_SUCCESS: (branch: string, message: string, autoPullPush: boolean) => `合并成功: ${branch}`,
39
+ MERGE_SUCCESS_NO_MESSAGE: (branch: string, autoPullPush: boolean) => `合并成功: ${branch}`,
40
+ WORKTREE_CLEANED: (branch: string) => `已清理: ${branch}`,
41
+ },
42
+ AUTO_SAVE_COMMIT_MESSAGE: 'clawt:auto-save',
43
+ };
44
+ });
41
45
 
42
46
  vi.mock('../../../src/utils/index.js', () => ({
43
47
  runPreChecks: vi.fn(),
@@ -5,16 +5,20 @@ vi.mock('../../../src/logger/index.js', () => ({
5
5
  logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
6
6
  }));
7
7
 
8
- vi.mock('../../../src/constants/index.js', () => ({
9
- MESSAGES: {
10
- RESUME_NO_WORKTREES: '没有可用的 worktree',
11
- RESUME_SELECT_BRANCH: '选择要恢复的分支',
12
- RESUME_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
13
- RESUME_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
14
- RESUME_ALL_CONFIRM: (count: number) => `确认恢复 ${count} 个分支?`,
15
- RESUME_ALL_SUCCESS: (count: number) => `已恢复 ${count} 个分支`,
16
- },
17
- }));
8
+ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
9
+ const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
10
+ return {
11
+ ...actual,
12
+ MESSAGES: {
13
+ RESUME_NO_WORKTREES: '没有可用的 worktree',
14
+ RESUME_SELECT_BRANCH: '选择要恢复的分支',
15
+ RESUME_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
16
+ RESUME_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
17
+ RESUME_ALL_CONFIRM: (count: number) => `确认恢复 ${count} 个分支?`,
18
+ RESUME_ALL_SUCCESS: (count: number) => `已恢复 ${count} 个分支`,
19
+ },
20
+ };
21
+ });
18
22
 
19
23
  vi.mock('../../../src/utils/index.js', () => ({
20
24
  runPreChecks: vi.fn(),
@@ -91,7 +95,6 @@ describe('handleResume', () => {
91
95
  await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
92
96
 
93
97
  expect(mockedRunPreChecks).toHaveBeenCalled();
94
- expect(mockedValidateClaudeCodeInstalled).toHaveBeenCalled();
95
98
  expect(mockedResolveTargetWorktrees).toHaveBeenCalled();
96
99
  expect(mockedPromptGroupedMultiSelectBranches).not.toHaveBeenCalled();
97
100
  expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree, { autoContinue: true });