clawt 2.19.0 → 3.0.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.
Files changed (75) hide show
  1. package/README.md +12 -2
  2. package/dist/index.js +626 -219
  3. package/dist/postinstall.js +39 -6
  4. package/docs/alias.md +108 -0
  5. package/docs/completion.md +55 -0
  6. package/docs/config-file.md +43 -0
  7. package/docs/config.md +91 -0
  8. package/docs/create.md +85 -0
  9. package/docs/init.md +65 -0
  10. package/docs/list.md +67 -0
  11. package/docs/log.md +67 -0
  12. package/docs/merge.md +137 -0
  13. package/docs/notification.md +94 -0
  14. package/docs/projects.md +135 -0
  15. package/docs/remove.md +79 -0
  16. package/docs/reset.md +35 -0
  17. package/docs/resume.md +99 -0
  18. package/docs/run.md +146 -0
  19. package/docs/spec.md +156 -1879
  20. package/docs/status.md +155 -0
  21. package/docs/sync.md +114 -0
  22. package/docs/update-check.md +95 -0
  23. package/docs/validate.md +368 -0
  24. package/package.json +1 -1
  25. package/src/commands/alias.ts +1 -1
  26. package/src/commands/create.ts +10 -5
  27. package/src/commands/init.ts +75 -0
  28. package/src/commands/list.ts +1 -1
  29. package/src/commands/merge.ts +11 -4
  30. package/src/commands/remove.ts +10 -3
  31. package/src/commands/reset.ts +3 -0
  32. package/src/commands/resume.ts +9 -3
  33. package/src/commands/run.ts +9 -3
  34. package/src/commands/status.ts +1 -1
  35. package/src/commands/sync.ts +18 -6
  36. package/src/commands/validate.ts +46 -52
  37. package/src/constants/branch.ts +3 -0
  38. package/src/constants/config.ts +1 -1
  39. package/src/constants/index.ts +3 -3
  40. package/src/constants/messages/completion.ts +1 -1
  41. package/src/constants/messages/create.ts +3 -0
  42. package/src/constants/messages/index.ts +2 -0
  43. package/src/constants/messages/init.ts +18 -0
  44. package/src/constants/messages/remove.ts +2 -0
  45. package/src/constants/messages/sync.ts +3 -0
  46. package/src/constants/messages/validate.ts +6 -0
  47. package/src/constants/paths.ts +3 -0
  48. package/src/constants/prompt.ts +28 -0
  49. package/src/index.ts +2 -0
  50. package/src/types/command.ts +7 -1
  51. package/src/types/index.ts +2 -1
  52. package/src/types/projectConfig.ts +5 -0
  53. package/src/utils/config.ts +2 -1
  54. package/src/utils/git.ts +18 -0
  55. package/src/utils/index.ts +6 -1
  56. package/src/utils/json.ts +67 -0
  57. package/src/utils/project-config.ts +77 -0
  58. package/src/utils/validate-branch.ts +166 -0
  59. package/src/utils/worktree-matcher.ts +268 -1
  60. package/src/utils/worktree.ts +6 -2
  61. package/tests/unit/commands/create.test.ts +20 -16
  62. package/tests/unit/commands/init.test.ts +146 -0
  63. package/tests/unit/commands/merge.test.ts +7 -1
  64. package/tests/unit/commands/remove.test.ts +4 -0
  65. package/tests/unit/commands/reset.test.ts +2 -0
  66. package/tests/unit/commands/resume.test.ts +29 -8
  67. package/tests/unit/commands/run.test.ts +2 -0
  68. package/tests/unit/commands/sync.test.ts +6 -0
  69. package/tests/unit/commands/validate.test.ts +13 -0
  70. package/tests/unit/utils/config.test.ts +2 -2
  71. package/tests/unit/utils/project-config.test.ts +136 -0
  72. package/tests/unit/utils/update-checker.test.ts +28 -7
  73. package/tests/unit/utils/validate-branch.test.ts +272 -0
  74. package/tests/unit/utils/worktree-matcher.test.ts +142 -1
  75. package/tests/unit/utils/worktree.test.ts +6 -0
@@ -19,21 +19,22 @@ import {
19
19
  parseTasksFromOptions,
20
20
  executeBatchTasks,
21
21
  printDryRunPreview,
22
+ ensureOnMainWorkBranch,
22
23
  } from '../utils/index.js';
23
24
 
24
25
  /**
25
- * 注册 run 命令:批量创建 worktree 并执行 Claude Code 任务
26
+ * 注册 run 命令:批量创建 worktree + 启动 Claude Code 执行任务(支持任务文件)
26
27
  * @param {Command} program - Commander 实例
27
28
  */
28
29
  export function registerRunCommand(program: Command): void {
29
30
  program
30
31
  .command('run')
31
- .description('批量创建 worktree 并启动 Claude Code 执行任务')
32
+ .description('批量创建 worktree + 启动 Claude Code 执行任务(支持任务文件)')
32
33
  .option('-b, --branch <branchName>', '分支名')
33
34
  .option('--tasks <task...>', '任务列表(可多次指定),不传则在 worktree 中打开 Claude Code 交互式界面')
34
35
  .option('-c, --concurrency <n>', '最大并发数,0 表示不限制')
35
36
  .option('-f, --file <path>', '从任务文件读取任务列表(与 --tasks 互斥)')
36
- .option('-d, --dry-run', '预览模式,仅展示任务计划不实际执行')
37
+ .option('--dry-run', '预览模式,仅展示任务计划不实际执行')
37
38
  .action(async (options: RunOptions) => {
38
39
  await handleRun(options);
39
40
  });
@@ -120,6 +121,11 @@ function handleDryRunFromFile(options: RunOptions): void {
120
121
  async function handleRun(options: RunOptions): Promise<void> {
121
122
  validateMainWorktree();
122
123
 
124
+ // dry-run 模式跳过项目配置前置校验
125
+ if (!options.dryRun) {
126
+ await ensureOnMainWorkBranch();
127
+ }
128
+
123
129
  // 互斥校验:--file 和 --tasks 不能同时使用
124
130
  if (options.file && options.tasks) {
125
131
  throw new ClawtError(MESSAGES.FILE_AND_TASKS_CONFLICT);
@@ -30,7 +30,7 @@ import {
30
30
  export function registerStatusCommand(program: Command): void {
31
31
  program
32
32
  .command('status')
33
- .description('显示项目全局状态总览')
33
+ .description('显示项目全局状态总览(支持 --json 格式输出)')
34
34
  .option('--json', '以 JSON 格式输出')
35
35
  .action((options: StatusOptions) => {
36
36
  handleStatus(options);
@@ -13,18 +13,22 @@ import {
13
13
  gitCommit,
14
14
  gitMerge,
15
15
  hasMergeConflict,
16
- getCurrentBranch,
17
16
  hasSnapshot,
18
17
  removeSnapshot,
19
18
  printSuccess,
20
19
  printInfo,
21
20
  printWarning,
22
21
  resolveTargetWorktree,
22
+ requireProjectConfig,
23
+ getMainWorkBranch,
24
+ rebuildValidateBranch,
25
+ getValidateBranchName,
26
+ ensureOnMainWorkBranch,
23
27
  } from '../utils/index.js';
24
28
  import type { WorktreeResolveMessages } from '../utils/index.js';
25
29
 
26
30
  /**
27
- * 注册 sync 命令:将主分支最新代码同步到目标 worktree
31
+ * 注册 sync 命令:将主分支最新代码同步到目标 worktree(含验证分支重建)
28
32
  * @param {Command} program - Commander 实例
29
33
  */
30
34
  export function registerSyncCommand(program: Command): void {
@@ -92,10 +96,10 @@ export interface SyncResult {
92
96
  * @param {string} branch - 分支名
93
97
  * @returns {SyncResult} sync 执行结果
94
98
  */
95
- export function executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult {
96
- // 获取主分支名(不硬编码 main/master)
99
+ export async function executeSyncForBranch(targetWorktreePath: string, branch: string): Promise<SyncResult> {
100
+ // 从项目配置获取主工作分支名
97
101
  const mainWorktreePath = getGitTopLevel();
98
- const mainBranch = getCurrentBranch(mainWorktreePath);
102
+ const mainBranch = getMainWorkBranch();
99
103
 
100
104
  // 检查目标 worktree 是否有未提交变更,有则自动保存
101
105
  if (!isWorkingDirClean(targetWorktreePath)) {
@@ -119,6 +123,12 @@ export function executeSyncForBranch(targetWorktreePath: string, branch: string)
119
123
  }
120
124
 
121
125
  printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
126
+
127
+ // 合并成功后重建验证分支(基于最新的主分支 HEAD)
128
+ await rebuildValidateBranch(branch, mainWorktreePath);
129
+ const validateBranchName = getValidateBranchName(branch);
130
+ printInfo(MESSAGES.SYNC_VALIDATE_BRANCH_REBUILT(validateBranchName));
131
+
122
132
  return { success: true, hasConflict: false };
123
133
  }
124
134
 
@@ -129,6 +139,8 @@ export function executeSyncForBranch(targetWorktreePath: string, branch: string)
129
139
  */
130
140
  async function handleSync(options: SyncOptions): Promise<void> {
131
141
  validateMainWorktree();
142
+ requireProjectConfig();
143
+ await ensureOnMainWorkBranch();
132
144
 
133
145
  logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
134
146
 
@@ -137,5 +149,5 @@ async function handleSync(options: SyncOptions): Promise<void> {
137
149
  const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
138
150
  const { path: targetWorktreePath, branch } = worktree;
139
151
 
140
- executeSyncForBranch(targetWorktreePath, branch);
152
+ await executeSyncForBranch(targetWorktreePath, branch);
141
153
  }
@@ -1,5 +1,4 @@
1
1
  import type { Command } from 'commander';
2
- import Enquirer from 'enquirer';
3
2
  import { logger } from '../logger/index.js';
4
3
  import { ClawtError } from '../errors/index.js';
5
4
  import { MESSAGES } from '../constants/index.js';
@@ -14,7 +13,6 @@ import {
14
13
  isWorkingDirClean,
15
14
  gitAddAll,
16
15
  gitCommit,
17
- gitStashPush,
18
16
  gitRestoreStaged,
19
17
  gitResetHard,
20
18
  gitCleanForce,
@@ -44,6 +42,13 @@ import {
44
42
  runCommandInherited,
45
43
  parseParallelCommands,
46
44
  runParallelCommands,
45
+ requireProjectConfig,
46
+ getValidateBranchName,
47
+ gitCheckout,
48
+ ensureOnMainWorkBranch,
49
+ checkBranchExists,
50
+ getCurrentBranch,
51
+ handleDirtyWorkingDir,
47
52
  } from '../utils/index.js';
48
53
  import type { WorktreeResolveMessages, ParallelCommandResult } from '../utils/index.js';
49
54
 
@@ -56,13 +61,13 @@ const VALIDATE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
56
61
  };
57
62
 
58
63
  /**
59
- * 注册 validate 命令:在主 worktree 验证其他分支的变更
64
+ * 注册 validate 命令:在主 worktree 验证其他分支的变更(通过验证分支)
60
65
  * @param {Command} program - Commander 实例
61
66
  */
62
67
  export function registerValidateCommand(program: Command): void {
63
68
  program
64
69
  .command('validate')
65
- .description('在主 worktree 验证某个 worktree 分支的变更')
70
+ .description('在主 worktree 验证某个 worktree 分支的变更(通过验证分支)')
66
71
  .option('-b, --branch <branchName>', '要验证的分支名(支持模糊匹配,不传则列出所有分支)')
67
72
  .option('--clean', '清理 validate 状态(重置主 worktree 并删除快照)')
68
73
  .option('-r, --run <command>', 'validate 成功后在主 worktree 中执行的命令')
@@ -73,47 +78,11 @@ export function registerValidateCommand(program: Command): void {
73
78
 
74
79
  /**
75
80
  * 处理主 worktree 工作区有未提交更改的情况(首次 validate 时使用)
81
+ * 委托给通用的 handleDirtyWorkingDir 函数处理
76
82
  * @param {string} mainWorktreePath - 主 worktree 路径
77
83
  */
78
84
  async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void> {
79
- printWarning('主 worktree 当前分支有未提交的更改,请选择处理方式:\n');
80
-
81
- // @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
82
- const choice = await new Enquirer.Select({
83
- message: '选择处理方式',
84
- choices: [
85
- {
86
- name: 'reset',
87
- message: 'reset (推荐) - 丢弃所有更改 (git reset --hard HEAD && git clean -fd)',
88
- },
89
- {
90
- name: 'stash',
91
- message: 'stash - 暂存更改 (git add . && git stash)',
92
- },
93
- {
94
- name: 'exit',
95
- message: 'exit - 退出,手动处理',
96
- },
97
- ],
98
- initial: 0,
99
- }).run();
100
-
101
- if (choice === 'exit') {
102
- throw new ClawtError('用户选择退出');
103
- }
104
-
105
- if (choice === 'reset') {
106
- gitResetHard(mainWorktreePath);
107
- gitCleanForce(mainWorktreePath);
108
- } else if (choice === 'stash') {
109
- gitAddAll(mainWorktreePath);
110
- gitStashPush('clawt:auto-stash', mainWorktreePath);
111
- }
112
-
113
- // 再次检查是否干净
114
- if (!isWorkingDirClean(mainWorktreePath)) {
115
- throw new ClawtError('工作区仍然不干净,请手动处理');
116
- }
85
+ await handleDirtyWorkingDir(mainWorktreePath);
117
86
  }
118
87
 
119
88
  /**
@@ -186,7 +155,7 @@ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: s
186
155
 
187
156
  // 用户确认,执行 sync
188
157
  printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
189
- const syncResult = executeSyncForBranch(targetWorktreePath, branchName);
158
+ const syncResult = await executeSyncForBranch(targetWorktreePath, branchName);
190
159
 
191
160
  // sync 冲突提示已在 executeSyncForBranch 内部输出(SYNC_CONFLICT),此处无需重复提示
192
161
  }
@@ -215,6 +184,8 @@ function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string,
215
184
  */
216
185
  async function handleValidateClean(options: ValidateOptions): Promise<void> {
217
186
  validateMainWorktree();
187
+ // 显式前置校验:确保项目已初始化
188
+ requireProjectConfig();
218
189
 
219
190
  const projectName = getProjectName();
220
191
  const mainWorktreePath = getGitTopLevel();
@@ -244,6 +215,9 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
244
215
  gitCleanForce(mainWorktreePath);
245
216
  }
246
217
 
218
+ // 确保当前在主工作分支上
219
+ await ensureOnMainWorkBranch(mainWorktreePath);
220
+
247
221
  // 删除对应的快照文件
248
222
  removeSnapshot(projectName, branchName);
249
223
 
@@ -259,11 +233,19 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
259
233
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
260
234
  */
261
235
  async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
236
+ // 切换主 worktree 到验证分支
237
+ const validateBranchName = getValidateBranchName(branchName);
238
+ if (!checkBranchExists(validateBranchName)) {
239
+ throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
240
+ }
241
+ gitCheckout(validateBranchName, mainWorktreePath);
242
+
262
243
  // 通过 patch 迁移目标分支全量变更到主 worktree
263
244
  const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
264
245
 
265
246
  if (!result.success) {
266
- // patch 失败,询问用户是否自动 sync
247
+ // patch 失败,确保在主工作分支上后询问用户是否自动 sync
248
+ await ensureOnMainWorkBranch(mainWorktreePath);
267
249
  await handlePatchApplyFailure(targetWorktreePath, branchName);
268
250
  return;
269
251
  }
@@ -272,7 +254,7 @@ async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath:
272
254
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
273
255
 
274
256
  // 结果:暂存区=空,工作目录=全量变更
275
- printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
257
+ printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
276
258
  }
277
259
 
278
260
  /**
@@ -294,24 +276,35 @@ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktre
294
276
  gitCleanForce(mainWorktreePath);
295
277
  }
296
278
 
297
- // 步骤 3:通过 patch 从目标分支获取最新全量变更
279
+ // 步骤 3:切换到验证分支(如果已在该分支上则跳过)
280
+ const validateBranchName = getValidateBranchName(branchName);
281
+ if (!checkBranchExists(validateBranchName)) {
282
+ throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
283
+ }
284
+ const currentBranch = getCurrentBranch(mainWorktreePath);
285
+ if (currentBranch !== validateBranchName) {
286
+ gitCheckout(validateBranchName, mainWorktreePath);
287
+ }
288
+
289
+ // 步骤 4:通过 patch 从目标分支获取最新全量变更
298
290
  const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
299
291
 
300
292
  if (!result.success) {
301
- // patch 失败,询问用户是否自动 sync
293
+ // patch 失败,确保在主工作分支上后询问用户是否自动 sync
294
+ await ensureOnMainWorkBranch(mainWorktreePath);
302
295
  await handlePatchApplyFailure(targetWorktreePath, branchName);
303
296
  return;
304
297
  }
305
298
 
306
- // 步骤 4:保存最新快照为 git tree 对象(同时记录当前 HEAD)
299
+ // 步骤 5:保存最新快照为 git tree 对象(同时记录当前 HEAD)
307
300
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
308
301
 
309
- // 步骤 5:将旧变更状态载入暂存区
302
+ // 步骤 6:将旧变更状态载入暂存区
310
303
  try {
311
304
  const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
312
305
 
313
306
  if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
314
- // HEAD 发生了变化(如主分支合并了其他分支):
307
+ // HEAD 发生了变化:
315
308
  // 将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,
316
309
  // 避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容
317
310
  const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
@@ -324,7 +317,7 @@ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktre
324
317
  // 有冲突:降级为全量模式(暂存区保持为空)
325
318
  logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
326
319
  printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
327
- printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
320
+ printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
328
321
  return;
329
322
  }
330
323
  // oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
@@ -337,7 +330,7 @@ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktre
337
330
  logger.warn(`增量 read-tree 失败: ${error}`);
338
331
  printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
339
332
  // 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
340
- printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
333
+ printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
341
334
  return;
342
335
  }
343
336
 
@@ -451,6 +444,7 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
451
444
  }
452
445
 
453
446
  validateMainWorktree();
447
+ requireProjectConfig();
454
448
 
455
449
  const projectName = getProjectName();
456
450
  const mainWorktreePath = getGitTopLevel();
@@ -4,3 +4,6 @@
4
4
  * 以及连续的 ..(目录遍历)
5
5
  */
6
6
  export const INVALID_BRANCH_CHARS = /[\/\\.\s~:*?[\]^]+/g;
7
+
8
+ /** 验证分支名前缀 */
9
+ export const VALIDATE_BRANCH_PREFIX = 'clawt-validate-';
@@ -1,7 +1,7 @@
1
1
  import type { ClawtConfig, ConfigDefinitions } from '../types/index.js';
2
2
  import { VALID_TERMINAL_APPS } from './terminal.js';
3
3
 
4
- /** Claude Code 系统约束提示,禁止代码执行完成后构建项目验证 */
4
+ /** Claude Code 系统约束提示 */
5
5
  export const APPEND_SYSTEM_PROMPT =
6
6
  'After the code execution is completed, it is prohibited to build the project for verification.';
7
7
 
@@ -1,5 +1,5 @@
1
- export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR, CLAUDE_PROJECTS_DIR, UPDATE_CHECK_PATH } from './paths.js';
2
- export { INVALID_BRANCH_CHARS } from './branch.js';
1
+ export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR, CLAUDE_PROJECTS_DIR, UPDATE_CHECK_PATH, PROJECTS_CONFIG_DIR } from './paths.js';
2
+ export { INVALID_BRANCH_CHARS, VALIDATE_BRANCH_PREFIX } from './branch.js';
3
3
  export { MESSAGES } from './messages/index.js';
4
4
  export { CONFIG_ALIAS_DISABLED_HINT } from './messages/index.js';
5
5
  export { UPDATE_MESSAGES, UPDATE_COMMANDS } from './messages/update.js';
@@ -29,4 +29,4 @@ export {
29
29
  CLEAR_SCREEN,
30
30
  CURSOR_HOME,
31
31
  } from './progress.js';
32
- export { SELECT_ALL_NAME, SELECT_ALL_LABEL } from './prompt.js';
32
+ export { SELECT_ALL_NAME, SELECT_ALL_LABEL, GROUP_SELECT_ALL_PREFIX, GROUP_SELECT_ALL_LABEL, GROUP_SEPARATOR_LABEL, UNKNOWN_DATE_GROUP, UNKNOWN_DATE_SEPARATOR_LABEL } from './prompt.js';
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const COMPLETION_MESSAGES = {
5
5
  /** completion 命令的主描述 */
6
- COMPLETION_COMMAND_DESC: '生成和安装 shell 自动补全脚本',
6
+ COMPLETION_COMMAND_DESC: '为终端提供 shell 自动补全功能(bash/zsh)',
7
7
  /** bash 子命令描述 */
8
8
  COMPLETION_BASH_DESC: '输出 bash 自动补全脚本',
9
9
  /** zsh 子命令描述 */
@@ -2,4 +2,7 @@
2
2
  export const CREATE_MESSAGES = {
3
3
  /** 创建数量参数无效 */
4
4
  INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
5
+ /** 当前不在主工作分支上的警告 */
6
+ CREATE_WARN_NOT_ON_MAIN_BRANCH: (mainBranch: string, currentBranch: string) =>
7
+ `当前不在主工作分支 ${mainBranch} 上(当前: ${currentBranch})`,
5
8
  } as const;
@@ -13,6 +13,7 @@ import { ALIAS_MESSAGES } from './alias.js';
13
13
  import { PROJECTS_MESSAGES } from './projects.js';
14
14
  import { COMPLETION_MESSAGES } from './completion.js';
15
15
  import { UPDATE_MESSAGES, UPDATE_COMMANDS } from './update.js';
16
+ import { INIT_MESSAGES } from './init.js';
16
17
 
17
18
  export { CONFIG_ALIAS_DISABLED_HINT };
18
19
  export { UPDATE_MESSAGES, UPDATE_COMMANDS };
@@ -36,4 +37,5 @@ export const MESSAGES = {
36
37
  ...ALIAS_MESSAGES,
37
38
  ...PROJECTS_MESSAGES,
38
39
  ...COMPLETION_MESSAGES,
40
+ ...INIT_MESSAGES,
39
41
  } as const;
@@ -0,0 +1,18 @@
1
+ /** init 命令专属提示消息 */
2
+ export const INIT_MESSAGES = {
3
+ /** init 成功 */
4
+ INIT_SUCCESS: (branch: string) =>
5
+ `✓ 项目初始化成功,主工作分支设置为: ${branch}`,
6
+ /** init 更新 */
7
+ INIT_UPDATED: (oldBranch: string, newBranch: string) =>
8
+ `✓ 已将主工作分支从 ${oldBranch} 更新为 ${newBranch}`,
9
+ /** init show 查看当前项目完整配置(JSON 格式) */
10
+ INIT_SHOW: (configJson: string) =>
11
+ `${configJson}`,
12
+ /** init 未初始化 */
13
+ INIT_NOT_INITIALIZED: '项目尚未初始化,请先执行 clawt init 设置主工作分支',
14
+ /** 项目未初始化(requireProjectConfig 使用) */
15
+ PROJECT_NOT_INITIALIZED: '项目尚未初始化,请先执行 clawt init 设置主工作分支',
16
+ /** 项目配置缺少 clawtMainWorkBranch 字段 */
17
+ PROJECT_CONFIG_MISSING_BRANCH: '项目配置缺少主工作分支信息,请重新执行 clawt init 设置主工作分支',
18
+ } as const;
@@ -14,4 +14,6 @@ export const REMOVE_MESSAGES = {
14
14
  `以下 worktree 移除失败:\n${failures.map((f) => ` ✗ ${f.path}: ${f.error}`).join('\n')}`,
15
15
  /** 用户选择保留本地分支 */
16
16
  REMOVE_BRANCHES_KEPT: '已保留本地分支,可稍后使用 git branch -D <分支名> 手动删除',
17
+ /** 确认删除本地分支和验证分支 */
18
+ REMOVE_CONFIRM_DELETE_BRANCHES: '是否同时删除对应的本地分支和验证分支?',
17
19
  } as const;
@@ -21,4 +21,7 @@ export const SYNC_MESSAGES = {
21
21
  SYNC_SELECT_BRANCH: '请选择要同步的分支',
22
22
  /** sync 模糊匹配到多个结果提示 */
23
23
  SYNC_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
24
+ /** sync 后验证分支已重建提示 */
25
+ SYNC_VALIDATE_BRANCH_REBUILT: (validateBranch: string) =>
26
+ `验证分支 ${validateBranch} 已重建`,
24
27
  } as const;
@@ -62,4 +62,10 @@ export const VALIDATE_MESSAGES = {
62
62
  /** 用户拒绝自动 sync */
63
63
  VALIDATE_AUTO_SYNC_DECLINED: (branch: string) =>
64
64
  `请手动执行 clawt sync -b ${branch} 同步主分支后重试`,
65
+ /** 验证分支不存在 */
66
+ VALIDATE_BRANCH_NOT_FOUND: (validateBranch: string, branch: string) =>
67
+ `验证分支 ${validateBranch} 不存在,请先执行 clawt create 或 clawt run 创建分支 ${branch}`,
68
+ /** validate 成功(含验证分支信息) */
69
+ VALIDATE_SUCCESS_WITH_BRANCH: (branch: string, validateBranch: string) =>
70
+ `✓ 已切换到验证分支 ${validateBranch} 并应用分支 ${branch} 的变更\n 可以开始验证了`,
65
71
  } as const;
@@ -21,3 +21,6 @@ export const CLAUDE_PROJECTS_DIR = join(homedir(), '.claude', 'projects');
21
21
 
22
22
  /** 更新检查缓存文件路径 ~/.clawt/update-check.json */
23
23
  export const UPDATE_CHECK_PATH = join(CLAWT_HOME, 'update-check.json');
24
+
25
+ /** 项目配置目录 ~/.clawt/projects/ */
26
+ export const PROJECTS_CONFIG_DIR = join(CLAWT_HOME, 'projects');
@@ -1,5 +1,33 @@
1
+ import chalk from 'chalk';
2
+
1
3
  /** 多选列表中全选选项的标识名称 */
2
4
  export const SELECT_ALL_NAME = '__select_all__';
3
5
 
4
6
  /** 多选列表中全选选项的显示文本 */
5
7
  export const SELECT_ALL_LABEL = '[select-all]';
8
+
9
+ /** 组级全选选项的 name 前缀 */
10
+ export const GROUP_SELECT_ALL_PREFIX = '__group_select_all_';
11
+
12
+ /**
13
+ * 生成组级全选选项的显示文本
14
+ * @param {string} dateLabel - 日期标签
15
+ * @returns {string} 格式化的组全选显示文本
16
+ */
17
+ export const GROUP_SELECT_ALL_LABEL = (dateLabel: string): string => `[select-all: ${dateLabel}]`;
18
+
19
+ /**
20
+ * 生成日期分组分隔线的显示文本
21
+ * 日期部分高亮显示,两侧使用 === 分隔线增强视觉区分
22
+ * @param {string} dateLabel - 日期标签
23
+ * @param {string} relativeTime - 相对时间描述
24
+ * @returns {string} 格式化的分隔线文本
25
+ */
26
+ export const GROUP_SEPARATOR_LABEL = (dateLabel: string, relativeTime: string): string =>
27
+ `════ ${chalk.bold.hex('#FF8C00')(dateLabel)}(${chalk.hex('#FF8C00')(relativeTime)}) ════`;
28
+
29
+ /** 无法获取创建日期时的默认分组名称 */
30
+ export const UNKNOWN_DATE_GROUP = '未知日期';
31
+
32
+ /** 未知日期分组的分隔线显示文本 */
33
+ export const UNKNOWN_DATE_SEPARATOR_LABEL = `════ ${chalk.bold.hex('#FF8C00')('未知日期')} ════`;
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import { registerStatusCommand } from './commands/status.js';
18
18
  import { registerAliasCommand } from './commands/alias.js';
19
19
  import { registerProjectsCommand } from './commands/projects.js';
20
20
  import { registerCompletionCommand } from './commands/completion.js';
21
+ import { registerInitCommand } from './commands/init.js';
21
22
 
22
23
  // 从 package.json 读取版本号,避免硬编码
23
24
  const require = createRequire(import.meta.url);
@@ -56,6 +57,7 @@ registerStatusCommand(program);
56
57
  registerAliasCommand(program);
57
58
  registerProjectsCommand(program);
58
59
  registerCompletionCommand(program);
60
+ registerInitCommand(program);
59
61
 
60
62
  // 加载配置并应用命令别名
61
63
  const config = loadConfig();
@@ -34,7 +34,7 @@ export interface ValidateOptions {
34
34
  export interface MergeOptions {
35
35
  /** 要合并的分支名(可选,支持模糊匹配,不传则列出所有分支供选择) */
36
36
  branch?: string;
37
- /** 提交信息(工作区有修改时必填) */
37
+ /** 提交信息(目标 worktree 工作区有修改时必填) */
38
38
  message?: string;
39
39
  }
40
40
 
@@ -77,3 +77,9 @@ export interface ProjectsOptions {
77
77
  /** 以 JSON 格式输出 */
78
78
  json?: boolean;
79
79
  }
80
+
81
+ /** init 命令选项 */
82
+ export interface InitOptions {
83
+ /** 指定主工作分支名(可选,默认使用当前分支) */
84
+ branch?: string;
85
+ }
@@ -1,8 +1,9 @@
1
1
  export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
2
- export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions } from './command.js';
2
+ export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions, InitOptions } from './command.js';
3
3
  export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
4
4
  export type { ClaudeCodeResult } from './claudeCode.js';
5
5
  export type { TaskResult, TaskSummary } from './taskResult.js';
6
6
  export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, SnapshotSummary, StatusResult } from './status.js';
7
7
  export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
8
8
  export type { ProjectOverview, ProjectWorktreeDetail, ProjectDetailResult, ProjectsOverviewResult } from './project.js';
9
+ export type { ProjectConfig } from './projectConfig.js';
@@ -0,0 +1,5 @@
1
+ /** 项目级配置(存储在 ~/.clawt/projects/<projectName>.json) */
2
+ export interface ProjectConfig {
3
+ /** 主 worktree 的工作分支名 */
4
+ clawtMainWorkBranch: string;
5
+ }
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG, MESSAGES } from '../constants/index.js';
2
+ import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG, MESSAGES, PROJECTS_CONFIG_DIR } from '../constants/index.js';
3
3
  import { ClawtError } from '../errors/index.js';
4
4
  import { ensureDir } from './fs.js';
5
5
  import { logger } from '../logger/index.js';
@@ -65,6 +65,7 @@ export function ensureClawtDirs(): void {
65
65
  ensureDir(CLAWT_HOME);
66
66
  ensureDir(LOGS_DIR);
67
67
  ensureDir(WORKTREES_DIR);
68
+ ensureDir(PROJECTS_CONFIG_DIR);
68
69
  }
69
70
 
70
71
  /**
package/src/utils/git.ts CHANGED
@@ -501,3 +501,21 @@ export function getBranchCreatedAt(branchName: string, cwd?: string): string | n
501
501
  return null;
502
502
  }
503
503
  }
504
+
505
+ /**
506
+ * 切换到指定分支
507
+ * @param {string} branchName - 目标分支名
508
+ * @param {string} [cwd] - 工作目录
509
+ */
510
+ export function gitCheckout(branchName: string, cwd?: string): void {
511
+ execCommand(`git checkout ${branchName}`, { cwd });
512
+ }
513
+
514
+ /**
515
+ * 创建本地分支(不切换)
516
+ * @param {string} branchName - 新分支名
517
+ * @param {string} [cwd] - 工作目录
518
+ */
519
+ export function createBranch(branchName: string, cwd?: string): void {
520
+ execCommand(`git branch ${branchName}`, { cwd });
521
+ }
@@ -46,6 +46,8 @@ export {
46
46
  gitDiffTree,
47
47
  gitApplyCachedCheck,
48
48
  getBranchCreatedAt,
49
+ gitCheckout,
50
+ createBranch,
49
51
  } from './git.js';
50
52
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
51
53
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
@@ -56,7 +58,7 @@ export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
56
58
  export { multilineInput } from './prompt.js';
57
59
  export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
58
60
  export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
59
- export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees } from './worktree-matcher.js';
61
+ export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap } from './worktree-matcher.js';
60
62
  export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
61
63
  export { ProgressRenderer } from './progress.js';
62
64
  export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
@@ -68,4 +70,7 @@ export { truncateTaskDesc, printDryRunPreview } from './dry-run.js';
68
70
  export { applyAliases } from './alias.js';
69
71
  export { isValidConfigKey, getValidConfigKeys, parseConfigValue, promptConfigValue, formatConfigValue } from './config-strategy.js';
70
72
  export { checkForUpdates } from './update-checker.js';
73
+ export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch } from './project-config.js';
74
+ export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
75
+ export { safeStringify } from './json.js';
71
76