clawt 2.16.3 → 2.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -128,6 +128,8 @@ clawt validate -b <branch> -r "pnpm test & pnpm build" # 并行执行多个命
128
128
 
129
129
  支持增量模式:再次 validate 同一分支时,可通过 `git diff` 查看两次之间的增量差异。
130
130
 
131
+ 当 patch apply 失败(目标分支与主分支差异过大)时,会自动询问是否执行 `sync` 同步主分支到目标 worktree,无需手动操作。
132
+
131
133
  `-r, --run` 选项可在 validate 成功后自动在主 worktree 中执行指定命令(如测试、构建等),命令执行失败不影响 validate 结果。支持用 `&` 分隔多个命令并行执行:
132
134
 
133
135
  | 用法 | 行为 |
@@ -142,6 +144,8 @@ clawt validate -b <branch> -r "pnpm test & pnpm build" # 并行执行多个命
142
144
  clawt sync -b <branch>
143
145
  ```
144
146
 
147
+ 当 `validate` 的 patch apply 失败时也会自动提示执行 sync,通常无需手动调用。
148
+
145
149
  ### `clawt merge` — 合并分支到主 worktree
146
150
 
147
151
  ```bash
package/dist/index.js CHANGED
@@ -211,7 +211,16 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
211
211
  /** 并行执行中单个命令失败 */
212
212
  VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
213
213
  /** 并行执行中单个命令启动失败 */
214
- VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`
214
+ VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`,
215
+ /** patch apply 失败后询问用户是否执行 sync */
216
+ VALIDATE_CONFIRM_AUTO_SYNC: (branch) => `\u662F\u5426\u7ACB\u5373\u6267\u884C sync \u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch}\uFF1F`,
217
+ /** 自动 sync 开始提示 */
218
+ VALIDATE_AUTO_SYNC_START: (branch) => `\u6B63\u5728\u81EA\u52A8\u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch} ...`,
219
+ /** 自动 sync 存在冲突,无法重试 */
220
+ VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath) => `\u540C\u6B65\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u8FDB\u5165\u76EE\u6807 worktree \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u91CD\u8BD5
221
+ cd ${worktreePath}`,
222
+ /** 用户拒绝自动 sync */
223
+ VALIDATE_AUTO_SYNC_DECLINED: (branch) => `\u8BF7\u624B\u52A8\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
215
224
  };
216
225
 
217
226
  // src/constants/messages/sync.ts
@@ -2337,6 +2346,66 @@ async function handleBatchResume(worktrees) {
2337
2346
 
2338
2347
  // src/commands/validate.ts
2339
2348
  import Enquirer4 from "enquirer";
2349
+
2350
+ // src/commands/sync.ts
2351
+ function registerSyncCommand(program2) {
2352
+ program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").option("-b, --branch <branchName>", "\u8981\u540C\u6B65\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").action(async (options) => {
2353
+ await handleSync(options);
2354
+ });
2355
+ }
2356
+ var SYNC_RESOLVE_MESSAGES = {
2357
+ noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
2358
+ selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
2359
+ multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
2360
+ noMatch: MESSAGES.SYNC_NO_MATCH
2361
+ };
2362
+ function autoSaveChanges(worktreePath, branch) {
2363
+ gitAddAll(worktreePath);
2364
+ gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
2365
+ printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
2366
+ logger.info(`\u5DF2\u81EA\u52A8\u4FDD\u5B58 ${branch} \u5206\u652F\u7684\u672A\u63D0\u4EA4\u53D8\u66F4`);
2367
+ }
2368
+ function mergeMainBranch(worktreePath, mainBranch) {
2369
+ try {
2370
+ gitMerge(mainBranch, worktreePath);
2371
+ return false;
2372
+ } catch {
2373
+ if (hasMergeConflict(worktreePath)) {
2374
+ return true;
2375
+ }
2376
+ throw new ClawtError(`\u5408\u5E76 ${mainBranch} \u5931\u8D25`);
2377
+ }
2378
+ }
2379
+ function executeSyncForBranch(targetWorktreePath, branch) {
2380
+ const mainWorktreePath = getGitTopLevel();
2381
+ const mainBranch = getCurrentBranch(mainWorktreePath);
2382
+ if (!isWorkingDirClean(targetWorktreePath)) {
2383
+ autoSaveChanges(targetWorktreePath, branch);
2384
+ }
2385
+ printInfo(MESSAGES.SYNC_MERGING(branch, mainBranch));
2386
+ const hasConflict = mergeMainBranch(targetWorktreePath, mainBranch);
2387
+ if (hasConflict) {
2388
+ printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
2389
+ return { success: false, hasConflict: true };
2390
+ }
2391
+ const projectName = getProjectName();
2392
+ if (hasSnapshot(projectName, branch)) {
2393
+ removeSnapshot(projectName, branch);
2394
+ logger.info(`\u5DF2\u6E05\u9664\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167`);
2395
+ }
2396
+ printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
2397
+ return { success: true, hasConflict: false };
2398
+ }
2399
+ async function handleSync(options) {
2400
+ validateMainWorktree();
2401
+ logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
2402
+ const worktrees = getProjectWorktrees();
2403
+ const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
2404
+ const { path: targetWorktreePath, branch } = worktree;
2405
+ executeSyncForBranch(targetWorktreePath, branch);
2406
+ }
2407
+
2408
+ // src/commands/validate.ts
2340
2409
  var VALIDATE_RESOLVE_MESSAGES = {
2341
2410
  noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
2342
2411
  selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
@@ -2397,9 +2466,10 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
2397
2466
  } catch (error) {
2398
2467
  logger.warn(`patch apply \u5931\u8D25: ${error}`);
2399
2468
  printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
2400
- throw error;
2469
+ return { success: false };
2401
2470
  }
2402
2471
  }
2472
+ return { success: true };
2403
2473
  } finally {
2404
2474
  if (didTempCommit) {
2405
2475
  try {
@@ -2415,6 +2485,18 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
2415
2485
  }
2416
2486
  }
2417
2487
  }
2488
+ async function handlePatchApplyFailure(targetWorktreePath, branchName) {
2489
+ const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
2490
+ if (!confirmed) {
2491
+ printWarning(MESSAGES.VALIDATE_AUTO_SYNC_DECLINED(branchName));
2492
+ return;
2493
+ }
2494
+ printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
2495
+ const syncResult = executeSyncForBranch(targetWorktreePath, branchName);
2496
+ if (syncResult.hasConflict) {
2497
+ printWarning(MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT(targetWorktreePath));
2498
+ }
2499
+ }
2418
2500
  function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
2419
2501
  gitAddAll(mainWorktreePath);
2420
2502
  const treeHash = gitWriteTree(mainWorktreePath);
@@ -2448,18 +2530,26 @@ async function handleValidateClean(options) {
2448
2530
  removeSnapshot(projectName, branchName);
2449
2531
  printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
2450
2532
  }
2451
- function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
2452
- migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
2533
+ async function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
2534
+ const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
2535
+ if (!result.success) {
2536
+ await handlePatchApplyFailure(targetWorktreePath, branchName);
2537
+ return;
2538
+ }
2453
2539
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
2454
2540
  printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
2455
2541
  }
2456
- function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
2542
+ async function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
2457
2543
  const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
2458
2544
  if (!isWorkingDirClean(mainWorktreePath)) {
2459
2545
  gitResetHard(mainWorktreePath);
2460
2546
  gitCleanForce(mainWorktreePath);
2461
2547
  }
2462
- migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
2548
+ const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
2549
+ if (!result.success) {
2550
+ await handlePatchApplyFailure(targetWorktreePath, branchName);
2551
+ return;
2552
+ }
2463
2553
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
2464
2554
  try {
2465
2555
  const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
@@ -2562,12 +2652,12 @@ async function handleValidate(options) {
2562
2652
  if (!isWorkingDirClean(mainWorktreePath)) {
2563
2653
  await handleDirtyMainWorktree(mainWorktreePath);
2564
2654
  }
2565
- handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2655
+ await handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2566
2656
  } else {
2567
2657
  if (!isWorkingDirClean(mainWorktreePath)) {
2568
2658
  await handleDirtyMainWorktree(mainWorktreePath);
2569
2659
  }
2570
- handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2660
+ await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2571
2661
  }
2572
2662
  if (options.run) {
2573
2663
  await executeRunCommand(options.run, mainWorktreePath);
@@ -2780,60 +2870,6 @@ function handleConfigGet(key) {
2780
2870
  printInfo(MESSAGES.CONFIG_GET_VALUE(key, String(value)));
2781
2871
  }
2782
2872
 
2783
- // src/commands/sync.ts
2784
- function registerSyncCommand(program2) {
2785
- program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").option("-b, --branch <branchName>", "\u8981\u540C\u6B65\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").action(async (options) => {
2786
- await handleSync(options);
2787
- });
2788
- }
2789
- var SYNC_RESOLVE_MESSAGES = {
2790
- noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
2791
- selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
2792
- multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
2793
- noMatch: MESSAGES.SYNC_NO_MATCH
2794
- };
2795
- function autoSaveChanges(worktreePath, branch) {
2796
- gitAddAll(worktreePath);
2797
- gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
2798
- printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
2799
- logger.info(`\u5DF2\u81EA\u52A8\u4FDD\u5B58 ${branch} \u5206\u652F\u7684\u672A\u63D0\u4EA4\u53D8\u66F4`);
2800
- }
2801
- function mergeMainBranch(worktreePath, mainBranch) {
2802
- try {
2803
- gitMerge(mainBranch, worktreePath);
2804
- return false;
2805
- } catch {
2806
- if (hasMergeConflict(worktreePath)) {
2807
- return true;
2808
- }
2809
- throw new ClawtError(`\u5408\u5E76 ${mainBranch} \u5931\u8D25`);
2810
- }
2811
- }
2812
- async function handleSync(options) {
2813
- validateMainWorktree();
2814
- logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
2815
- const worktrees = getProjectWorktrees();
2816
- const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
2817
- const { path: targetWorktreePath, branch } = worktree;
2818
- const mainWorktreePath = getGitTopLevel();
2819
- const mainBranch = getCurrentBranch(mainWorktreePath);
2820
- if (!isWorkingDirClean(targetWorktreePath)) {
2821
- autoSaveChanges(targetWorktreePath, branch);
2822
- }
2823
- printInfo(MESSAGES.SYNC_MERGING(branch, mainBranch));
2824
- const hasConflict = mergeMainBranch(targetWorktreePath, mainBranch);
2825
- if (hasConflict) {
2826
- printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
2827
- return;
2828
- }
2829
- const projectName = getProjectName();
2830
- if (hasSnapshot(projectName, branch)) {
2831
- removeSnapshot(projectName, branch);
2832
- logger.info(`\u5DF2\u6E05\u9664\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167`);
2833
- }
2834
- printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
2835
- }
2836
-
2837
2873
  // src/commands/reset.ts
2838
2874
  function registerResetCommand(program2) {
2839
2875
  program2.command("reset").description("\u91CD\u7F6E\u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\uFF08\u4FDD\u7559 validate \u5FEB\u7167\uFF09").action(async () => {
@@ -203,7 +203,16 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
203
203
  /** 并行执行中单个命令失败 */
204
204
  VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
205
205
  /** 并行执行中单个命令启动失败 */
206
- VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`
206
+ VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`,
207
+ /** patch apply 失败后询问用户是否执行 sync */
208
+ VALIDATE_CONFIRM_AUTO_SYNC: (branch) => `\u662F\u5426\u7ACB\u5373\u6267\u884C sync \u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch}\uFF1F`,
209
+ /** 自动 sync 开始提示 */
210
+ VALIDATE_AUTO_SYNC_START: (branch) => `\u6B63\u5728\u81EA\u52A8\u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch} ...`,
211
+ /** 自动 sync 存在冲突,无法重试 */
212
+ VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath) => `\u540C\u6B65\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u8FDB\u5165\u76EE\u6807 worktree \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u91CD\u8BD5
213
+ cd ${worktreePath}`,
214
+ /** 用户拒绝自动 sync */
215
+ VALIDATE_AUTO_SYNC_DECLINED: (branch) => `\u8BF7\u624B\u52A8\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
207
216
  };
208
217
 
209
218
  // src/constants/messages/sync.ts
package/docs/spec.md CHANGED
@@ -576,7 +576,26 @@ git restore --staged .
576
576
  ```
577
577
 
578
578
  > 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
579
- > 如果 patch apply 失败(目标分支与主分支差异过大),会提示用户先执行 `clawt sync -b <branchName>` 同步主分支后重试。
579
+ > 如果 patch apply 失败(目标分支与主分支差异过大),`migrateChangesViaPatch` 返回 `{ success: false }`,进入自动 sync 交互流程(见下文 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程))。
580
+
581
+ ##### patch apply 失败后的自动 sync 流程
582
+
583
+ 当 patch apply 失败时,validate 不再直接退出,而是通过 `handlePatchApplyFailure()` 函数进入交互流程:
584
+
585
+ 1. **询问用户**:提示 `是否立即执行 sync 同步主分支到 <branchName>?`
586
+ 2. **用户拒绝** → 输出提示 `请手动执行 clawt sync -b <branchName> 同步主分支后重试`,退出
587
+ 3. **用户确认** → 调用 `executeSyncForBranch(targetWorktreePath, branchName)` 自动执行 sync
588
+ - **sync 成功** → validate 流程结束(用户需重新执行 validate)
589
+ - **sync 存在冲突** → 输出提示 `同步存在冲突,请进入目标 worktree 手动解决冲突后重试`,退出
590
+
591
+ > `executeSyncForBranch` 为 sync 命令抽取的核心操作函数(见 [5.12](#512-将主分支代码同步到目标-worktree)),供 validate 等命令复用。
592
+
593
+ **实现要点:**
594
+
595
+ - `migrateChangesViaPatch()` 返回类型从 `void` 改为 `{ success: boolean }`,patch apply 失败时返回 `{ success: false }` 而非抛出异常
596
+ - `handleFirstValidate()` 和 `handleIncrementalValidate()` 从同步函数改为 `async` 函数,以支持交互式确认
597
+ - `handlePatchApplyFailure()` 为新增的异步函数(`src/commands/validate.ts`),负责 patch 失败后的交互逻辑
598
+ - 消息常量:`MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC`、`MESSAGES.VALIDATE_AUTO_SYNC_START`、`MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT`、`MESSAGES.VALIDATE_AUTO_SYNC_DECLINED`(`src/constants/messages/validate.ts`)
580
599
 
581
600
  ##### 步骤 4:保存快照为 git tree 对象
582
601
 
@@ -715,7 +734,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
715
734
 
716
735
  ##### 步骤 3:从目标分支获取最新全量变更
717
736
 
718
- 通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。
737
+ 通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
719
738
 
720
739
  ##### 步骤 4:保存最新快照为 git tree 对象
721
740
 
@@ -1356,16 +1375,36 @@ clawt sync
1356
1375
  - 唯一匹配 → 直接使用
1357
1376
  - 多个匹配 → 通过交互式列表让用户从匹配结果中选择
1358
1377
  3. **无匹配** → 报错退出,并列出所有可用分支名
1359
- 3. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
1360
- 4. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
1378
+ 3. 调用 `executeSyncForBranch(targetWorktreePath, branch)` 执行核心同步逻辑
1379
+
1380
+ #### `executeSyncForBranch` — sync 核心操作函数
1381
+
1382
+ `executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult` 是从 `handleSync` 中抽取的核心同步逻辑,不包含 worktree 解析交互,供 validate 等命令复用。
1383
+
1384
+ **接口定义:**
1385
+
1386
+ ```typescript
1387
+ /** sync 核心操作的执行结果 */
1388
+ export interface SyncResult {
1389
+ /** 是否同步成功 */
1390
+ success: boolean;
1391
+ /** 是否存在合并冲突 */
1392
+ hasConflict: boolean;
1393
+ }
1394
+ ```
1395
+
1396
+ **执行流程:**
1397
+
1398
+ 1. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
1399
+ 2. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
1361
1400
  - 有修改 → 自动执行 `git add . && git commit -m "<AUTO_SAVE_COMMIT_MESSAGE>"` 保存变更(commit message 由常量 `AUTO_SAVE_COMMIT_MESSAGE` 定义,值为 `chore: auto-save before sync`,同时用于 merge 命令的 squash 检测)
1362
1401
  - 无修改 → 跳过
1363
- 5. **在目标 worktree 中合并主分支**:
1402
+ 3. **在目标 worktree 中合并主分支**:
1364
1403
  ```bash
1365
1404
  cd ~/.clawt/worktrees/<project>/<branchName>
1366
1405
  git merge <mainBranch>
1367
1406
  ```
1368
- 6. **冲突处理**:
1407
+ 4. **冲突处理**:
1369
1408
  - **有冲突** → 输出警告,提示用户进入目标 worktree 手动解决:
1370
1409
  ```
1371
1410
  合并存在冲突,请进入目标 worktree 手动解决:
@@ -1373,9 +1412,10 @@ clawt sync
1373
1412
  解决冲突后执行 git add . && git merge --continue
1374
1413
  clawt validate -b <branch> 验证变更
1375
1414
  ```
1415
+ - 返回 `{ success: false, hasConflict: true }`
1376
1416
  - **无冲突** → 继续
1377
- 7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
1378
- 8. **输出成功提示**:
1417
+ 5. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
1418
+ 6. **输出成功提示**并返回 `{ success: true, hasConflict: false }`:
1379
1419
  ```
1380
1420
  ✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
1381
1421
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.16.3",
3
+ "version": "2.16.4",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -77,21 +77,22 @@ function mergeMainBranch(worktreePath: string, mainBranch: string): boolean {
77
77
  }
78
78
  }
79
79
 
80
+ /** sync 核心操作的执行结果 */
81
+ export interface SyncResult {
82
+ /** 是否同步成功 */
83
+ success: boolean;
84
+ /** 是否存在合并冲突 */
85
+ hasConflict: boolean;
86
+ }
87
+
80
88
  /**
81
- * 执行 sync 命令的核心逻辑
82
- * 将主分支最新代码同步到目标 worktree
83
- * @param {SyncOptions} options - 命令选项
89
+ * 执行 sync 核心操作(检查未提交→自动保存→merge 主分支→清除快照)
90
+ * 不包含 worktree 解析交互,供 validate 等命令复用
91
+ * @param {string} targetWorktreePath - 目标 worktree 路径
92
+ * @param {string} branch - 分支名
93
+ * @returns {SyncResult} sync 执行结果
84
94
  */
85
- async function handleSync(options: SyncOptions): Promise<void> {
86
- validateMainWorktree();
87
-
88
- logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
89
-
90
- // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
91
- const worktrees = getProjectWorktrees();
92
- const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
93
- const { path: targetWorktreePath, branch } = worktree;
94
-
95
+ export function executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult {
95
96
  // 获取主分支名(不硬编码 main/master)
96
97
  const mainWorktreePath = getGitTopLevel();
97
98
  const mainBranch = getCurrentBranch(mainWorktreePath);
@@ -107,7 +108,7 @@ async function handleSync(options: SyncOptions): Promise<void> {
107
108
 
108
109
  if (hasConflict) {
109
110
  printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
110
- return;
111
+ return { success: false, hasConflict: true };
111
112
  }
112
113
 
113
114
  // 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
@@ -118,4 +119,23 @@ async function handleSync(options: SyncOptions): Promise<void> {
118
119
  }
119
120
 
120
121
  printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
122
+ return { success: true, hasConflict: false };
123
+ }
124
+
125
+ /**
126
+ * 执行 sync 命令的核心逻辑
127
+ * 将主分支最新代码同步到目标 worktree
128
+ * @param {SyncOptions} options - 命令选项
129
+ */
130
+ async function handleSync(options: SyncOptions): Promise<void> {
131
+ validateMainWorktree();
132
+
133
+ logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
134
+
135
+ // 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
136
+ const worktrees = getProjectWorktrees();
137
+ const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
138
+ const { path: targetWorktreePath, branch } = worktree;
139
+
140
+ executeSyncForBranch(targetWorktreePath, branch);
121
141
  }
@@ -4,6 +4,7 @@ import { logger } from '../logger/index.js';
4
4
  import { ClawtError } from '../errors/index.js';
5
5
  import { MESSAGES } from '../constants/index.js';
6
6
  import type { ValidateOptions } from '../types/index.js';
7
+ import { executeSyncForBranch } from './sync.js';
7
8
  import {
8
9
  validateMainWorktree,
9
10
  getProjectName,
@@ -33,6 +34,7 @@ import {
33
34
  writeSnapshot,
34
35
  removeSnapshot,
35
36
  confirmDestructiveAction,
37
+ confirmAction,
36
38
  printSuccess,
37
39
  printError,
38
40
  printWarning,
@@ -121,8 +123,9 @@ async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void>
121
123
  * @param {string} mainWorktreePath - 主 worktree 路径
122
124
  * @param {string} branchName - 分支名
123
125
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
126
+ * @returns {{ success: boolean }} patch 迁移结果
124
127
  */
125
- function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): void {
128
+ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
126
129
  let didTempCommit = false;
127
130
 
128
131
  try {
@@ -143,9 +146,11 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
143
146
  } catch (error) {
144
147
  logger.warn(`patch apply 失败: ${error}`);
145
148
  printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
146
- throw error;
149
+ return { success: false };
147
150
  }
148
151
  }
152
+
153
+ return { success: true };
149
154
  } finally {
150
155
  // 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
151
156
  // 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
@@ -164,6 +169,31 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
164
169
  }
165
170
  }
166
171
 
172
+ /**
173
+ * patch apply 失败后的交互处理:询问用户是否自动执行 sync
174
+ * @param {string} targetWorktreePath - 目标 worktree 路径
175
+ * @param {string} branchName - 分支名
176
+ */
177
+ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: string): Promise<void> {
178
+ // 询问用户是否自动执行 sync
179
+ const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
180
+
181
+ if (!confirmed) {
182
+ // 用户拒绝自动 sync
183
+ printWarning(MESSAGES.VALIDATE_AUTO_SYNC_DECLINED(branchName));
184
+ return;
185
+ }
186
+
187
+ // 用户确认,执行 sync
188
+ printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
189
+ const syncResult = executeSyncForBranch(targetWorktreePath, branchName);
190
+
191
+ if (syncResult.hasConflict) {
192
+ // sync 存在冲突,提示用户手动解决
193
+ printWarning(MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT(targetWorktreePath));
194
+ }
195
+ }
196
+
167
197
  /**
168
198
  * 保存当前主 worktree 工作目录变更为 git tree 对象快照
169
199
  * 操作序列:git add . → git write-tree → git restore --staged .
@@ -231,9 +261,15 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
231
261
  * @param {string} branchName - 分支名
232
262
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
233
263
  */
234
- function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
264
+ async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
235
265
  // 通过 patch 迁移目标分支全量变更到主 worktree
236
- migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
266
+ const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
267
+
268
+ if (!result.success) {
269
+ // patch 失败,询问用户是否自动 sync
270
+ await handlePatchApplyFailure(targetWorktreePath, branchName);
271
+ return;
272
+ }
237
273
 
238
274
  // 保存快照为 git tree 对象
239
275
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
@@ -250,7 +286,7 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
250
286
  * @param {string} branchName - 分支名
251
287
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
252
288
  */
253
- function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
289
+ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
254
290
  // 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
255
291
  const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
256
292
 
@@ -262,7 +298,13 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
262
298
  }
263
299
 
264
300
  // 步骤 3:通过 patch 从目标分支获取最新全量变更
265
- migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
301
+ const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
302
+
303
+ if (!result.success) {
304
+ // patch 失败,询问用户是否自动 sync
305
+ await handlePatchApplyFailure(targetWorktreePath, branchName);
306
+ return;
307
+ }
266
308
 
267
309
  // 步骤 4:保存最新快照为 git tree 对象(同时记录当前 HEAD)
268
310
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
@@ -441,14 +483,14 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
441
483
  if (!isWorkingDirClean(mainWorktreePath)) {
442
484
  await handleDirtyMainWorktree(mainWorktreePath);
443
485
  }
444
- handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
486
+ await handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
445
487
  } else {
446
488
  // 首次模式:先确保主 worktree 干净
447
489
  if (!isWorkingDirClean(mainWorktreePath)) {
448
490
  await handleDirtyMainWorktree(mainWorktreePath);
449
491
  }
450
492
 
451
- handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
493
+ await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
452
494
  }
453
495
 
454
496
  // validate 成功后执行用户指定的命令
@@ -53,4 +53,16 @@ export const VALIDATE_MESSAGES = {
53
53
  /** 并行执行中单个命令启动失败 */
54
54
  VALIDATE_PARALLEL_CMD_ERROR: (command: string, errorMessage: string) =>
55
55
  ` ✗ ${command}(错误: ${errorMessage})`,
56
+ /** patch apply 失败后询问用户是否执行 sync */
57
+ VALIDATE_CONFIRM_AUTO_SYNC: (branch: string) =>
58
+ `是否立即执行 sync 同步主分支到 ${branch}?`,
59
+ /** 自动 sync 开始提示 */
60
+ VALIDATE_AUTO_SYNC_START: (branch: string) =>
61
+ `正在自动同步主分支到 ${branch} ...`,
62
+ /** 自动 sync 存在冲突,无法重试 */
63
+ VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath: string) =>
64
+ `同步存在冲突,请进入目标 worktree 手动解决冲突后重试\n cd ${worktreePath}`,
65
+ /** 用户拒绝自动 sync */
66
+ VALIDATE_AUTO_SYNC_DECLINED: (branch: string) =>
67
+ `请手动执行 clawt sync -b ${branch} 同步主分支后重试`,
56
68
  } as const;