clawt 2.16.2 → 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
@@ -171,6 +175,39 @@ clawt status # 文本格式
171
175
  clawt status --json # JSON 格式
172
176
  ```
173
177
 
178
+ 展示主 worktree 状态、各 worktree 的变更详情(含分支创建时间和验证状态)以及 validate 快照摘要:
179
+
180
+ ```
181
+ ════════════════════════════════════════
182
+ 项目状态总览: my-project
183
+ ════════════════════════════════════════
184
+
185
+ ◆ 主 Worktree
186
+ 分支: main
187
+ 状态: ✓ 干净
188
+
189
+ ────────────────────────────────────────
190
+
191
+ ◆ Worktree 列表 (2 个)
192
+
193
+ ● feature-login [已提交]
194
+ +120 -30 3 个本地提交 与主分支同步
195
+ 创建于 3 天前
196
+ 上次验证: 2 小时前
197
+
198
+ ● feature-signup [未提交修改]
199
+ +45 -10 1 个本地提交 落后主分支 2 个提交
200
+ 创建于 1 天前
201
+ ✗ 未验证
202
+
203
+ ────────────────────────────────────────
204
+
205
+ ◆ Validate 快照 (3 个)
206
+ 其中 1 个快照对应的 worktree 已不存在
207
+
208
+ ════════════════════════════════════════
209
+ ```
210
+
174
211
  ### `clawt reset` — 重置主 worktree 到干净状态
175
212
 
176
213
  ```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
@@ -323,7 +332,7 @@ var STATUS_MESSAGES = {
323
332
  /** status worktrees 区块标题 */
324
333
  STATUS_WORKTREES_SECTION: "Worktree \u5217\u8868",
325
334
  /** status 快照区块标题 */
326
- STATUS_SNAPSHOTS_SECTION: "\u672A\u6E05\u7406\u7684 Validate \u5FEB\u7167",
335
+ STATUS_SNAPSHOTS_SECTION: "Validate \u5FEB\u7167",
327
336
  /** status 无 worktree */
328
337
  STATUS_NO_WORKTREES: "(\u65E0\u6D3B\u8DC3 worktree)",
329
338
  /** status 无未清理快照 */
@@ -337,7 +346,15 @@ var STATUS_MESSAGES = {
337
346
  /** status 变更状态:无变更 */
338
347
  STATUS_CHANGE_CLEAN: "\u65E0\u53D8\u66F4",
339
348
  /** status 快照对应 worktree 已不存在 */
340
- STATUS_SNAPSHOT_ORPHANED: "(\u5BF9\u5E94 worktree \u5DF2\u4E0D\u5B58\u5728)"
349
+ STATUS_SNAPSHOT_ORPHANED: (count) => `\u5176\u4E2D ${count} \u4E2A\u5FEB\u7167\u5BF9\u5E94\u7684 worktree \u5DF2\u4E0D\u5B58\u5728`,
350
+ /** status 分支创建时间标签 */
351
+ STATUS_CREATED_AT: (relativeTime) => `\u521B\u5EFA\u4E8E ${relativeTime}`,
352
+ /** status 分支无分叉提交时的提示 */
353
+ STATUS_NO_DIVERGED_COMMITS: "\u5C1A\u65E0\u5206\u53C9\u63D0\u4EA4",
354
+ /** status 上次验证时间标签 */
355
+ STATUS_LAST_VALIDATED: (relativeTime) => `\u4E0A\u6B21\u9A8C\u8BC1: ${relativeTime}`,
356
+ /** status 未验证警示 */
357
+ STATUS_NOT_VALIDATED: "\u2717 \u672A\u9A8C\u8BC1"
341
358
  };
342
359
 
343
360
  // src/constants/messages/alias.ts
@@ -787,6 +804,17 @@ function gitApplyCachedCheck(patchContent, cwd) {
787
804
  return false;
788
805
  }
789
806
  }
807
+ function getBranchCreatedAt(branchName, cwd) {
808
+ try {
809
+ const output = execCommand(`git reflog show ${branchName} --format=%cI`, { cwd });
810
+ if (!output.trim()) return null;
811
+ const lines = output.trim().split("\n");
812
+ const lastLine = lines[lines.length - 1];
813
+ return lastLine || null;
814
+ } catch {
815
+ return null;
816
+ }
817
+ }
790
818
 
791
819
  // src/utils/formatter.ts
792
820
  import chalk2 from "chalk";
@@ -856,6 +884,35 @@ function formatDuration(ms) {
856
884
  const seconds = Math.floor(totalSeconds % 60);
857
885
  return `${minutes}m${String(seconds).padStart(2, "0")}s`;
858
886
  }
887
+ function formatRelativeTime(isoDateString) {
888
+ const date = new Date(isoDateString);
889
+ const now = /* @__PURE__ */ new Date();
890
+ const diffMs = now.getTime() - date.getTime();
891
+ if (isNaN(diffMs)) {
892
+ return null;
893
+ }
894
+ if (diffMs < 0 || diffMs < 60 * 1e3) {
895
+ return "\u521A\u521A";
896
+ }
897
+ const diffMinutes = Math.floor(diffMs / (1e3 * 60));
898
+ const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
899
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
900
+ if (diffHours < 1) {
901
+ return `${diffMinutes} \u5206\u949F\u524D`;
902
+ }
903
+ if (diffDays < 1) {
904
+ return `${diffHours} \u5C0F\u65F6\u524D`;
905
+ }
906
+ if (diffDays < 30) {
907
+ return `${diffDays} \u5929\u524D`;
908
+ }
909
+ if (diffDays < 365) {
910
+ const months = Math.floor(diffDays / 30);
911
+ return `${months} \u4E2A\u6708\u524D`;
912
+ }
913
+ const years = Math.floor(diffDays / 365);
914
+ return `${years} \u5E74\u524D`;
915
+ }
859
916
 
860
917
  // src/utils/branch.ts
861
918
  function sanitizeBranchName(branchName) {
@@ -1199,7 +1256,7 @@ function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
1199
1256
 
1200
1257
  // src/utils/validate-snapshot.ts
1201
1258
  import { join as join4 } from "path";
1202
- import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2 } from "fs";
1259
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2, statSync } from "fs";
1203
1260
  function getSnapshotPath(projectName, branchName) {
1204
1261
  return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
1205
1262
  }
@@ -1209,6 +1266,12 @@ function getSnapshotHeadPath(projectName, branchName) {
1209
1266
  function hasSnapshot(projectName, branchName) {
1210
1267
  return existsSync7(getSnapshotPath(projectName, branchName));
1211
1268
  }
1269
+ function getSnapshotModifiedTime(projectName, branchName) {
1270
+ const snapshotPath = getSnapshotPath(projectName, branchName);
1271
+ if (!existsSync7(snapshotPath)) return null;
1272
+ const stat = statSync(snapshotPath);
1273
+ return stat.mtime.toISOString();
1274
+ }
1212
1275
  function readSnapshot(projectName, branchName) {
1213
1276
  const snapshotPath = getSnapshotPath(projectName, branchName);
1214
1277
  const headPath = getSnapshotHeadPath(projectName, branchName);
@@ -2283,6 +2346,66 @@ async function handleBatchResume(worktrees) {
2283
2346
 
2284
2347
  // src/commands/validate.ts
2285
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
2286
2409
  var VALIDATE_RESOLVE_MESSAGES = {
2287
2410
  noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
2288
2411
  selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
@@ -2343,9 +2466,10 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
2343
2466
  } catch (error) {
2344
2467
  logger.warn(`patch apply \u5931\u8D25: ${error}`);
2345
2468
  printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
2346
- throw error;
2469
+ return { success: false };
2347
2470
  }
2348
2471
  }
2472
+ return { success: true };
2349
2473
  } finally {
2350
2474
  if (didTempCommit) {
2351
2475
  try {
@@ -2361,6 +2485,18 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
2361
2485
  }
2362
2486
  }
2363
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
+ }
2364
2500
  function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
2365
2501
  gitAddAll(mainWorktreePath);
2366
2502
  const treeHash = gitWriteTree(mainWorktreePath);
@@ -2394,18 +2530,26 @@ async function handleValidateClean(options) {
2394
2530
  removeSnapshot(projectName, branchName);
2395
2531
  printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
2396
2532
  }
2397
- function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
2398
- 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
+ }
2399
2539
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
2400
2540
  printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
2401
2541
  }
2402
- function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
2542
+ async function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
2403
2543
  const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
2404
2544
  if (!isWorkingDirClean(mainWorktreePath)) {
2405
2545
  gitResetHard(mainWorktreePath);
2406
2546
  gitCleanForce(mainWorktreePath);
2407
2547
  }
2408
- 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
+ }
2409
2553
  saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
2410
2554
  try {
2411
2555
  const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
@@ -2508,12 +2652,12 @@ async function handleValidate(options) {
2508
2652
  if (!isWorkingDirClean(mainWorktreePath)) {
2509
2653
  await handleDirtyMainWorktree(mainWorktreePath);
2510
2654
  }
2511
- handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2655
+ await handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2512
2656
  } else {
2513
2657
  if (!isWorkingDirClean(mainWorktreePath)) {
2514
2658
  await handleDirtyMainWorktree(mainWorktreePath);
2515
2659
  }
2516
- handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2660
+ await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2517
2661
  }
2518
2662
  if (options.run) {
2519
2663
  await executeRunCommand(options.run, mainWorktreePath);
@@ -2726,60 +2870,6 @@ function handleConfigGet(key) {
2726
2870
  printInfo(MESSAGES.CONFIG_GET_VALUE(key, String(value)));
2727
2871
  }
2728
2872
 
2729
- // src/commands/sync.ts
2730
- function registerSyncCommand(program2) {
2731
- 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) => {
2732
- await handleSync(options);
2733
- });
2734
- }
2735
- var SYNC_RESOLVE_MESSAGES = {
2736
- noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
2737
- selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
2738
- multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
2739
- noMatch: MESSAGES.SYNC_NO_MATCH
2740
- };
2741
- function autoSaveChanges(worktreePath, branch) {
2742
- gitAddAll(worktreePath);
2743
- gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
2744
- printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
2745
- logger.info(`\u5DF2\u81EA\u52A8\u4FDD\u5B58 ${branch} \u5206\u652F\u7684\u672A\u63D0\u4EA4\u53D8\u66F4`);
2746
- }
2747
- function mergeMainBranch(worktreePath, mainBranch) {
2748
- try {
2749
- gitMerge(mainBranch, worktreePath);
2750
- return false;
2751
- } catch {
2752
- if (hasMergeConflict(worktreePath)) {
2753
- return true;
2754
- }
2755
- throw new ClawtError(`\u5408\u5E76 ${mainBranch} \u5931\u8D25`);
2756
- }
2757
- }
2758
- async function handleSync(options) {
2759
- validateMainWorktree();
2760
- logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
2761
- const worktrees = getProjectWorktrees();
2762
- const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
2763
- const { path: targetWorktreePath, branch } = worktree;
2764
- const mainWorktreePath = getGitTopLevel();
2765
- const mainBranch = getCurrentBranch(mainWorktreePath);
2766
- if (!isWorkingDirClean(targetWorktreePath)) {
2767
- autoSaveChanges(targetWorktreePath, branch);
2768
- }
2769
- printInfo(MESSAGES.SYNC_MERGING(branch, mainBranch));
2770
- const hasConflict = mergeMainBranch(targetWorktreePath, mainBranch);
2771
- if (hasConflict) {
2772
- printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
2773
- return;
2774
- }
2775
- const projectName = getProjectName();
2776
- if (hasSnapshot(projectName, branch)) {
2777
- removeSnapshot(projectName, branch);
2778
- logger.info(`\u5DF2\u6E05\u9664\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167`);
2779
- }
2780
- printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
2781
- }
2782
-
2783
2873
  // src/commands/reset.ts
2784
2874
  function registerResetCommand(program2) {
2785
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 () => {
@@ -2849,15 +2939,17 @@ function collectWorktreeDetailedStatus(worktree, projectName) {
2849
2939
  const changeStatus = detectChangeStatus(worktree);
2850
2940
  const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
2851
2941
  const { insertions, deletions } = countDiffStat(worktree.path);
2942
+ const createdAt = resolveBranchCreatedAt(worktree.branch);
2852
2943
  return {
2853
2944
  path: worktree.path,
2854
2945
  branch: worktree.branch,
2855
2946
  changeStatus,
2856
2947
  commitsAhead,
2857
2948
  commitsBehind,
2858
- hasSnapshot: hasSnapshot(projectName, worktree.branch),
2949
+ snapshotTime: resolveSnapshotTime(projectName, worktree.branch),
2859
2950
  insertions,
2860
- deletions
2951
+ deletions,
2952
+ createdAt
2861
2953
  };
2862
2954
  }
2863
2955
  function detectChangeStatus(worktree) {
@@ -2893,13 +2985,28 @@ function countDiffStat(worktreePath) {
2893
2985
  return { insertions: 0, deletions: 0 };
2894
2986
  }
2895
2987
  }
2988
+ function resolveBranchCreatedAt(branchName) {
2989
+ try {
2990
+ return getBranchCreatedAt(branchName);
2991
+ } catch {
2992
+ return null;
2993
+ }
2994
+ }
2995
+ function resolveSnapshotTime(projectName, branchName) {
2996
+ try {
2997
+ return getSnapshotModifiedTime(projectName, branchName);
2998
+ } catch {
2999
+ return null;
3000
+ }
3001
+ }
2896
3002
  function collectSnapshots(projectName, worktrees) {
2897
3003
  const snapshotBranches = getProjectSnapshotBranches(projectName);
2898
3004
  const worktreeBranchSet = new Set(worktrees.map((wt) => wt.branch));
2899
- return snapshotBranches.map((branch) => ({
2900
- branch,
2901
- worktreeExists: worktreeBranchSet.has(branch)
2902
- }));
3005
+ const orphaned = snapshotBranches.filter((branch) => !worktreeBranchSet.has(branch)).length;
3006
+ return {
3007
+ total: snapshotBranches.length,
3008
+ orphaned
3009
+ };
2903
3010
  }
2904
3011
  function printStatusAsJson(result) {
2905
3012
  console.log(JSON.stringify(result, null, 2));
@@ -2942,21 +3049,30 @@ function printWorktreesSection(worktrees, total) {
2942
3049
  function printWorktreeItem(wt) {
2943
3050
  const statusLabel = formatChangeStatusLabel(wt.changeStatus);
2944
3051
  printInfo(` ${chalk8.bold("\u25CF")} ${chalk8.bold(wt.branch)} [${statusLabel}]`);
2945
- const parts = [];
2946
3052
  if (wt.insertions > 0 || wt.deletions > 0) {
2947
- parts.push(`${chalk8.green(`+${wt.insertions}`)} ${chalk8.red(`-${wt.deletions}`)}`);
3053
+ printInfo(` ${chalk8.green(`+${wt.insertions}`)} ${chalk8.red(`-${wt.deletions}`)}`);
2948
3054
  }
2949
3055
  if (wt.commitsAhead > 0) {
2950
- parts.push(chalk8.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`));
3056
+ printInfo(` ${chalk8.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
2951
3057
  }
2952
3058
  if (wt.commitsBehind > 0) {
2953
- parts.push(chalk8.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`));
3059
+ printInfo(` ${chalk8.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
2954
3060
  } else {
2955
- parts.push(chalk8.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65"));
3061
+ printInfo(` ${chalk8.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65")}`);
3062
+ }
3063
+ if (wt.createdAt) {
3064
+ const relativeTime = formatRelativeTime(wt.createdAt);
3065
+ if (relativeTime) {
3066
+ printInfo(` ${chalk8.gray(MESSAGES.STATUS_CREATED_AT(relativeTime))}`);
3067
+ }
2956
3068
  }
2957
- printInfo(` ${parts.join(" ")}`);
2958
- if (wt.hasSnapshot) {
2959
- printInfo(` ${chalk8.blue("\u6709 validate \u5FEB\u7167")}`);
3069
+ if (wt.snapshotTime) {
3070
+ const relativeTime = formatRelativeTime(wt.snapshotTime);
3071
+ if (relativeTime) {
3072
+ printInfo(` ${chalk8.green(MESSAGES.STATUS_LAST_VALIDATED(relativeTime))}`);
3073
+ }
3074
+ } else {
3075
+ printInfo(` ${chalk8.red(MESSAGES.STATUS_NOT_VALIDATED)}`);
2960
3076
  }
2961
3077
  printInfo("");
2962
3078
  }
@@ -2973,17 +3089,9 @@ function formatChangeStatusLabel(status) {
2973
3089
  }
2974
3090
  }
2975
3091
  function printSnapshotsSection(snapshots) {
2976
- printInfo(` ${chalk8.bold("\u25C6")} ${chalk8.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} \u4E2A)`);
2977
- printInfo("");
2978
- if (snapshots.length === 0) {
2979
- printInfo(` ${MESSAGES.STATUS_NO_SNAPSHOTS}`);
2980
- printInfo("");
2981
- return;
2982
- }
2983
- for (const snap of snapshots) {
2984
- const orphanLabel = snap.worktreeExists ? "" : ` ${chalk8.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
2985
- const icon = snap.worktreeExists ? chalk8.blue("\u25CF") : chalk8.yellow("\u26A0");
2986
- printInfo(` ${icon} ${snap.branch}${orphanLabel}`);
3092
+ printInfo(` ${chalk8.bold("\u25C6")} ${chalk8.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.total} \u4E2A)`);
3093
+ if (snapshots.orphaned > 0) {
3094
+ printInfo(` ${chalk8.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED(snapshots.orphaned))}`);
2987
3095
  }
2988
3096
  printInfo("");
2989
3097
  }
@@ -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
@@ -314,7 +323,7 @@ var STATUS_MESSAGES = {
314
323
  /** status worktrees 区块标题 */
315
324
  STATUS_WORKTREES_SECTION: "Worktree \u5217\u8868",
316
325
  /** status 快照区块标题 */
317
- STATUS_SNAPSHOTS_SECTION: "\u672A\u6E05\u7406\u7684 Validate \u5FEB\u7167",
326
+ STATUS_SNAPSHOTS_SECTION: "Validate \u5FEB\u7167",
318
327
  /** status 无 worktree */
319
328
  STATUS_NO_WORKTREES: "(\u65E0\u6D3B\u8DC3 worktree)",
320
329
  /** status 无未清理快照 */
@@ -328,7 +337,15 @@ var STATUS_MESSAGES = {
328
337
  /** status 变更状态:无变更 */
329
338
  STATUS_CHANGE_CLEAN: "\u65E0\u53D8\u66F4",
330
339
  /** status 快照对应 worktree 已不存在 */
331
- STATUS_SNAPSHOT_ORPHANED: "(\u5BF9\u5E94 worktree \u5DF2\u4E0D\u5B58\u5728)"
340
+ STATUS_SNAPSHOT_ORPHANED: (count) => `\u5176\u4E2D ${count} \u4E2A\u5FEB\u7167\u5BF9\u5E94\u7684 worktree \u5DF2\u4E0D\u5B58\u5728`,
341
+ /** status 分支创建时间标签 */
342
+ STATUS_CREATED_AT: (relativeTime) => `\u521B\u5EFA\u4E8E ${relativeTime}`,
343
+ /** status 分支无分叉提交时的提示 */
344
+ STATUS_NO_DIVERGED_COMMITS: "\u5C1A\u65E0\u5206\u53C9\u63D0\u4EA4",
345
+ /** status 上次验证时间标签 */
346
+ STATUS_LAST_VALIDATED: (relativeTime) => `\u4E0A\u6B21\u9A8C\u8BC1: ${relativeTime}`,
347
+ /** status 未验证警示 */
348
+ STATUS_NOT_VALIDATED: "\u2717 \u672A\u9A8C\u8BC1"
332
349
  };
333
350
 
334
351
  // src/constants/messages/alias.ts