clawt 2.16.1 → 2.16.3

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
@@ -123,11 +123,18 @@ clawt validate -b <branch> # 将变更迁移到主 worktree
123
123
  clawt validate -b <branch> --clean # 清理 validate 状态
124
124
  clawt validate -b <branch> -r "npm test" # validate 成功后自动运行测试
125
125
  clawt validate -b <branch> -r "npm run build" # validate 成功后自动构建
126
+ clawt validate -b <branch> -r "pnpm test & pnpm build" # 并行执行多个命令
126
127
  ```
127
128
 
128
129
  支持增量模式:再次 validate 同一分支时,可通过 `git diff` 查看两次之间的增量差异。
129
130
 
130
- `-r, --run` 选项可在 validate 成功后自动在主 worktree 中执行指定命令(如测试、构建等),命令执行失败不影响 validate 结果。
131
+ `-r, --run` 选项可在 validate 成功后自动在主 worktree 中执行指定命令(如测试、构建等),命令执行失败不影响 validate 结果。支持用 `&` 分隔多个命令并行执行:
132
+
133
+ | 用法 | 行为 |
134
+ | ---- | ---- |
135
+ | `-r "npm test"` | 单命令,同步执行 |
136
+ | `-r "npm lint && npm test"` | `&&` 不拆分,同步执行 |
137
+ | `-r "pnpm test & pnpm build"` | 并行执行,等全部完成后汇总结果 |
131
138
 
132
139
  ### `clawt sync` — 同步主分支代码到目标 worktree
133
140
 
@@ -164,6 +171,39 @@ clawt status # 文本格式
164
171
  clawt status --json # JSON 格式
165
172
  ```
166
173
 
174
+ 展示主 worktree 状态、各 worktree 的变更详情(含分支创建时间和验证状态)以及 validate 快照摘要:
175
+
176
+ ```
177
+ ════════════════════════════════════════
178
+ 项目状态总览: my-project
179
+ ════════════════════════════════════════
180
+
181
+ ◆ 主 Worktree
182
+ 分支: main
183
+ 状态: ✓ 干净
184
+
185
+ ────────────────────────────────────────
186
+
187
+ ◆ Worktree 列表 (2 个)
188
+
189
+ ● feature-login [已提交]
190
+ +120 -30 3 个本地提交 与主分支同步
191
+ 创建于 3 天前
192
+ 上次验证: 2 小时前
193
+
194
+ ● feature-signup [未提交修改]
195
+ +45 -10 1 个本地提交 落后主分支 2 个提交
196
+ 创建于 1 天前
197
+ ✗ 未验证
198
+
199
+ ────────────────────────────────────────
200
+
201
+ ◆ Validate 快照 (3 个)
202
+ 其中 1 个快照对应的 worktree 已不存在
203
+
204
+ ════════════════════════════════════════
205
+ ```
206
+
167
207
  ### `clawt reset` — 重置主 worktree 到干净状态
168
208
 
169
209
  ```bash
package/dist/index.js CHANGED
@@ -197,7 +197,21 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
197
197
  /** --run 命令执行失败(退出码非 0) */
198
198
  VALIDATE_RUN_FAILED: (command, exitCode) => `\u2717 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: ${exitCode}`,
199
199
  /** --run 命令执行异常(进程启动失败等) */
200
- VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`
200
+ VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`,
201
+ /** 并行命令开始执行提示 */
202
+ VALIDATE_PARALLEL_RUN_START: (count) => `\u6B63\u5728\u5E76\u884C\u6267\u884C ${count} \u4E2A\u547D\u4EE4...`,
203
+ /** 并行执行中单个命令开始提示(带序号) */
204
+ VALIDATE_PARALLEL_CMD_START: (index, total, command) => `[${index}/${total}] ${command}`,
205
+ /** 并行执行全部成功汇总提示 */
206
+ VALIDATE_PARALLEL_RUN_ALL_SUCCESS: (count) => `\u2713 \u5168\u90E8 ${count} \u4E2A\u547D\u4EE4\u6267\u884C\u6210\u529F`,
207
+ /** 并行执行部分失败汇总提示 */
208
+ VALIDATE_PARALLEL_RUN_SUMMARY: (successCount, failedCount) => `\u5171 ${successCount + failedCount} \u4E2A\u547D\u4EE4\uFF0C${successCount} \u4E2A\u6210\u529F\uFF0C${failedCount} \u4E2A\u5931\u8D25`,
209
+ /** 并行执行中单个命令成功 */
210
+ VALIDATE_PARALLEL_CMD_SUCCESS: (command) => ` \u2713 ${command}`,
211
+ /** 并行执行中单个命令失败 */
212
+ VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
213
+ /** 并行执行中单个命令启动失败 */
214
+ VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`
201
215
  };
202
216
 
203
217
  // src/constants/messages/sync.ts
@@ -309,7 +323,7 @@ var STATUS_MESSAGES = {
309
323
  /** status worktrees 区块标题 */
310
324
  STATUS_WORKTREES_SECTION: "Worktree \u5217\u8868",
311
325
  /** status 快照区块标题 */
312
- STATUS_SNAPSHOTS_SECTION: "\u672A\u6E05\u7406\u7684 Validate \u5FEB\u7167",
326
+ STATUS_SNAPSHOTS_SECTION: "Validate \u5FEB\u7167",
313
327
  /** status 无 worktree */
314
328
  STATUS_NO_WORKTREES: "(\u65E0\u6D3B\u8DC3 worktree)",
315
329
  /** status 无未清理快照 */
@@ -323,7 +337,15 @@ var STATUS_MESSAGES = {
323
337
  /** status 变更状态:无变更 */
324
338
  STATUS_CHANGE_CLEAN: "\u65E0\u53D8\u66F4",
325
339
  /** status 快照对应 worktree 已不存在 */
326
- 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"
327
349
  };
328
350
 
329
351
  // src/constants/messages/alias.ts
@@ -573,6 +595,31 @@ function runCommandInherited(command, options) {
573
595
  shell: true
574
596
  });
575
597
  }
598
+ function parseParallelCommands(commandString) {
599
+ const placeholder = "\0AND\0";
600
+ const escaped = commandString.replace(/&&/g, placeholder);
601
+ const parts = escaped.split("&");
602
+ return parts.map((part) => part.replace(new RegExp(placeholder, "g"), "&&").trim()).filter((part) => part.length > 0);
603
+ }
604
+ function runParallelCommands(commands, options) {
605
+ const promises = commands.map((command) => {
606
+ return new Promise((resolve2) => {
607
+ logger.debug(`\u5E76\u884C\u542F\u52A8\u547D\u4EE4: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ""}`);
608
+ const child = spawn(command, {
609
+ cwd: options?.cwd,
610
+ stdio: "inherit",
611
+ shell: true
612
+ });
613
+ child.on("error", (err) => {
614
+ resolve2({ command, exitCode: 1, error: err.message });
615
+ });
616
+ child.on("close", (code) => {
617
+ resolve2({ command, exitCode: code ?? 1 });
618
+ });
619
+ });
620
+ });
621
+ return Promise.all(promises);
622
+ }
576
623
 
577
624
  // src/utils/git.ts
578
625
  import { basename } from "path";
@@ -748,6 +795,17 @@ function gitApplyCachedCheck(patchContent, cwd) {
748
795
  return false;
749
796
  }
750
797
  }
798
+ function getBranchCreatedAt(branchName, cwd) {
799
+ try {
800
+ const output = execCommand(`git reflog show ${branchName} --format=%cI`, { cwd });
801
+ if (!output.trim()) return null;
802
+ const lines = output.trim().split("\n");
803
+ const lastLine = lines[lines.length - 1];
804
+ return lastLine || null;
805
+ } catch {
806
+ return null;
807
+ }
808
+ }
751
809
 
752
810
  // src/utils/formatter.ts
753
811
  import chalk2 from "chalk";
@@ -817,6 +875,35 @@ function formatDuration(ms) {
817
875
  const seconds = Math.floor(totalSeconds % 60);
818
876
  return `${minutes}m${String(seconds).padStart(2, "0")}s`;
819
877
  }
878
+ function formatRelativeTime(isoDateString) {
879
+ const date = new Date(isoDateString);
880
+ const now = /* @__PURE__ */ new Date();
881
+ const diffMs = now.getTime() - date.getTime();
882
+ if (isNaN(diffMs)) {
883
+ return null;
884
+ }
885
+ if (diffMs < 0 || diffMs < 60 * 1e3) {
886
+ return "\u521A\u521A";
887
+ }
888
+ const diffMinutes = Math.floor(diffMs / (1e3 * 60));
889
+ const diffHours = Math.floor(diffMs / (1e3 * 60 * 60));
890
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
891
+ if (diffHours < 1) {
892
+ return `${diffMinutes} \u5206\u949F\u524D`;
893
+ }
894
+ if (diffDays < 1) {
895
+ return `${diffHours} \u5C0F\u65F6\u524D`;
896
+ }
897
+ if (diffDays < 30) {
898
+ return `${diffDays} \u5929\u524D`;
899
+ }
900
+ if (diffDays < 365) {
901
+ const months = Math.floor(diffDays / 30);
902
+ return `${months} \u4E2A\u6708\u524D`;
903
+ }
904
+ const years = Math.floor(diffDays / 365);
905
+ return `${years} \u5E74\u524D`;
906
+ }
820
907
 
821
908
  // src/utils/branch.ts
822
909
  function sanitizeBranchName(branchName) {
@@ -1160,7 +1247,7 @@ function launchInteractiveClaudeInNewTerminal(worktree, hasPreviousSession) {
1160
1247
 
1161
1248
  // src/utils/validate-snapshot.ts
1162
1249
  import { join as join4 } from "path";
1163
- import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2 } from "fs";
1250
+ import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync4, rmdirSync as rmdirSync2, statSync } from "fs";
1164
1251
  function getSnapshotPath(projectName, branchName) {
1165
1252
  return join4(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
1166
1253
  }
@@ -1170,6 +1257,12 @@ function getSnapshotHeadPath(projectName, branchName) {
1170
1257
  function hasSnapshot(projectName, branchName) {
1171
1258
  return existsSync7(getSnapshotPath(projectName, branchName));
1172
1259
  }
1260
+ function getSnapshotModifiedTime(projectName, branchName) {
1261
+ const snapshotPath = getSnapshotPath(projectName, branchName);
1262
+ if (!existsSync7(snapshotPath)) return null;
1263
+ const stat = statSync(snapshotPath);
1264
+ return stat.mtime.toISOString();
1265
+ }
1173
1266
  function readSnapshot(projectName, branchName) {
1174
1267
  const snapshotPath = getSnapshotPath(projectName, branchName);
1175
1268
  const headPath = getSnapshotHeadPath(projectName, branchName);
@@ -2392,8 +2485,7 @@ function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, project
2392
2485
  }
2393
2486
  printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
2394
2487
  }
2395
- function executeRunCommand(command, mainWorktreePath) {
2396
- printInfo("");
2488
+ function executeSingleCommand(command, mainWorktreePath) {
2397
2489
  printInfo(MESSAGES.VALIDATE_RUN_START(command));
2398
2490
  printSeparator();
2399
2491
  const result = runCommandInherited(command, { cwd: mainWorktreePath });
@@ -2409,6 +2501,43 @@ function executeRunCommand(command, mainWorktreePath) {
2409
2501
  printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
2410
2502
  }
2411
2503
  }
2504
+ function reportParallelResults(results) {
2505
+ printSeparator();
2506
+ const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
2507
+ const failedCount = results.length - successCount;
2508
+ for (const result of results) {
2509
+ if (result.error) {
2510
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
2511
+ } else if (result.exitCode === 0) {
2512
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
2513
+ } else {
2514
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
2515
+ }
2516
+ }
2517
+ if (failedCount === 0) {
2518
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
2519
+ } else {
2520
+ printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
2521
+ }
2522
+ }
2523
+ async function executeParallelCommands(commands, mainWorktreePath) {
2524
+ printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
2525
+ for (let i = 0; i < commands.length; i++) {
2526
+ printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
2527
+ }
2528
+ printSeparator();
2529
+ const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
2530
+ reportParallelResults(results);
2531
+ }
2532
+ async function executeRunCommand(command, mainWorktreePath) {
2533
+ printInfo("");
2534
+ const commands = parseParallelCommands(command);
2535
+ if (commands.length <= 1) {
2536
+ executeSingleCommand(commands[0] || command, mainWorktreePath);
2537
+ } else {
2538
+ await executeParallelCommands(commands, mainWorktreePath);
2539
+ }
2540
+ }
2412
2541
  async function handleValidate(options) {
2413
2542
  if (options.clean) {
2414
2543
  await handleValidateClean(options);
@@ -2441,7 +2570,7 @@ async function handleValidate(options) {
2441
2570
  handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
2442
2571
  }
2443
2572
  if (options.run) {
2444
- executeRunCommand(options.run, mainWorktreePath);
2573
+ await executeRunCommand(options.run, mainWorktreePath);
2445
2574
  }
2446
2575
  }
2447
2576
 
@@ -2774,15 +2903,17 @@ function collectWorktreeDetailedStatus(worktree, projectName) {
2774
2903
  const changeStatus = detectChangeStatus(worktree);
2775
2904
  const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
2776
2905
  const { insertions, deletions } = countDiffStat(worktree.path);
2906
+ const createdAt = resolveBranchCreatedAt(worktree.branch);
2777
2907
  return {
2778
2908
  path: worktree.path,
2779
2909
  branch: worktree.branch,
2780
2910
  changeStatus,
2781
2911
  commitsAhead,
2782
2912
  commitsBehind,
2783
- hasSnapshot: hasSnapshot(projectName, worktree.branch),
2913
+ snapshotTime: resolveSnapshotTime(projectName, worktree.branch),
2784
2914
  insertions,
2785
- deletions
2915
+ deletions,
2916
+ createdAt
2786
2917
  };
2787
2918
  }
2788
2919
  function detectChangeStatus(worktree) {
@@ -2818,13 +2949,28 @@ function countDiffStat(worktreePath) {
2818
2949
  return { insertions: 0, deletions: 0 };
2819
2950
  }
2820
2951
  }
2952
+ function resolveBranchCreatedAt(branchName) {
2953
+ try {
2954
+ return getBranchCreatedAt(branchName);
2955
+ } catch {
2956
+ return null;
2957
+ }
2958
+ }
2959
+ function resolveSnapshotTime(projectName, branchName) {
2960
+ try {
2961
+ return getSnapshotModifiedTime(projectName, branchName);
2962
+ } catch {
2963
+ return null;
2964
+ }
2965
+ }
2821
2966
  function collectSnapshots(projectName, worktrees) {
2822
2967
  const snapshotBranches = getProjectSnapshotBranches(projectName);
2823
2968
  const worktreeBranchSet = new Set(worktrees.map((wt) => wt.branch));
2824
- return snapshotBranches.map((branch) => ({
2825
- branch,
2826
- worktreeExists: worktreeBranchSet.has(branch)
2827
- }));
2969
+ const orphaned = snapshotBranches.filter((branch) => !worktreeBranchSet.has(branch)).length;
2970
+ return {
2971
+ total: snapshotBranches.length,
2972
+ orphaned
2973
+ };
2828
2974
  }
2829
2975
  function printStatusAsJson(result) {
2830
2976
  console.log(JSON.stringify(result, null, 2));
@@ -2867,21 +3013,30 @@ function printWorktreesSection(worktrees, total) {
2867
3013
  function printWorktreeItem(wt) {
2868
3014
  const statusLabel = formatChangeStatusLabel(wt.changeStatus);
2869
3015
  printInfo(` ${chalk8.bold("\u25CF")} ${chalk8.bold(wt.branch)} [${statusLabel}]`);
2870
- const parts = [];
2871
3016
  if (wt.insertions > 0 || wt.deletions > 0) {
2872
- parts.push(`${chalk8.green(`+${wt.insertions}`)} ${chalk8.red(`-${wt.deletions}`)}`);
3017
+ printInfo(` ${chalk8.green(`+${wt.insertions}`)} ${chalk8.red(`-${wt.deletions}`)}`);
2873
3018
  }
2874
3019
  if (wt.commitsAhead > 0) {
2875
- parts.push(chalk8.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`));
3020
+ printInfo(` ${chalk8.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
2876
3021
  }
2877
3022
  if (wt.commitsBehind > 0) {
2878
- parts.push(chalk8.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`));
3023
+ printInfo(` ${chalk8.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
2879
3024
  } else {
2880
- parts.push(chalk8.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65"));
3025
+ printInfo(` ${chalk8.green("\u4E0E\u4E3B\u5206\u652F\u540C\u6B65")}`);
3026
+ }
3027
+ if (wt.createdAt) {
3028
+ const relativeTime = formatRelativeTime(wt.createdAt);
3029
+ if (relativeTime) {
3030
+ printInfo(` ${chalk8.gray(MESSAGES.STATUS_CREATED_AT(relativeTime))}`);
3031
+ }
2881
3032
  }
2882
- printInfo(` ${parts.join(" ")}`);
2883
- if (wt.hasSnapshot) {
2884
- printInfo(` ${chalk8.blue("\u6709 validate \u5FEB\u7167")}`);
3033
+ if (wt.snapshotTime) {
3034
+ const relativeTime = formatRelativeTime(wt.snapshotTime);
3035
+ if (relativeTime) {
3036
+ printInfo(` ${chalk8.green(MESSAGES.STATUS_LAST_VALIDATED(relativeTime))}`);
3037
+ }
3038
+ } else {
3039
+ printInfo(` ${chalk8.red(MESSAGES.STATUS_NOT_VALIDATED)}`);
2885
3040
  }
2886
3041
  printInfo("");
2887
3042
  }
@@ -2898,17 +3053,9 @@ function formatChangeStatusLabel(status) {
2898
3053
  }
2899
3054
  }
2900
3055
  function printSnapshotsSection(snapshots) {
2901
- printInfo(` ${chalk8.bold("\u25C6")} ${chalk8.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} \u4E2A)`);
2902
- printInfo("");
2903
- if (snapshots.length === 0) {
2904
- printInfo(` ${MESSAGES.STATUS_NO_SNAPSHOTS}`);
2905
- printInfo("");
2906
- return;
2907
- }
2908
- for (const snap of snapshots) {
2909
- const orphanLabel = snap.worktreeExists ? "" : ` ${chalk8.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
2910
- const icon = snap.worktreeExists ? chalk8.blue("\u25CF") : chalk8.yellow("\u26A0");
2911
- printInfo(` ${icon} ${snap.branch}${orphanLabel}`);
3056
+ printInfo(` ${chalk8.bold("\u25C6")} ${chalk8.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.total} \u4E2A)`);
3057
+ if (snapshots.orphaned > 0) {
3058
+ printInfo(` ${chalk8.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED(snapshots.orphaned))}`);
2912
3059
  }
2913
3060
  printInfo("");
2914
3061
  }
@@ -189,7 +189,21 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
189
189
  /** --run 命令执行失败(退出码非 0) */
190
190
  VALIDATE_RUN_FAILED: (command, exitCode) => `\u2717 \u547D\u4EE4\u6267\u884C\u5B8C\u6210: ${command}\uFF0C\u9000\u51FA\u7801: ${exitCode}`,
191
191
  /** --run 命令执行异常(进程启动失败等) */
192
- VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`
192
+ VALIDATE_RUN_ERROR: (command, errorMessage) => `\u2717 \u547D\u4EE4\u6267\u884C\u51FA\u9519: ${errorMessage}`,
193
+ /** 并行命令开始执行提示 */
194
+ VALIDATE_PARALLEL_RUN_START: (count) => `\u6B63\u5728\u5E76\u884C\u6267\u884C ${count} \u4E2A\u547D\u4EE4...`,
195
+ /** 并行执行中单个命令开始提示(带序号) */
196
+ VALIDATE_PARALLEL_CMD_START: (index, total, command) => `[${index}/${total}] ${command}`,
197
+ /** 并行执行全部成功汇总提示 */
198
+ VALIDATE_PARALLEL_RUN_ALL_SUCCESS: (count) => `\u2713 \u5168\u90E8 ${count} \u4E2A\u547D\u4EE4\u6267\u884C\u6210\u529F`,
199
+ /** 并行执行部分失败汇总提示 */
200
+ VALIDATE_PARALLEL_RUN_SUMMARY: (successCount, failedCount) => `\u5171 ${successCount + failedCount} \u4E2A\u547D\u4EE4\uFF0C${successCount} \u4E2A\u6210\u529F\uFF0C${failedCount} \u4E2A\u5931\u8D25`,
201
+ /** 并行执行中单个命令成功 */
202
+ VALIDATE_PARALLEL_CMD_SUCCESS: (command) => ` \u2713 ${command}`,
203
+ /** 并行执行中单个命令失败 */
204
+ VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
205
+ /** 并行执行中单个命令启动失败 */
206
+ VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`
193
207
  };
194
208
 
195
209
  // src/constants/messages/sync.ts
@@ -300,7 +314,7 @@ var STATUS_MESSAGES = {
300
314
  /** status worktrees 区块标题 */
301
315
  STATUS_WORKTREES_SECTION: "Worktree \u5217\u8868",
302
316
  /** status 快照区块标题 */
303
- STATUS_SNAPSHOTS_SECTION: "\u672A\u6E05\u7406\u7684 Validate \u5FEB\u7167",
317
+ STATUS_SNAPSHOTS_SECTION: "Validate \u5FEB\u7167",
304
318
  /** status 无 worktree */
305
319
  STATUS_NO_WORKTREES: "(\u65E0\u6D3B\u8DC3 worktree)",
306
320
  /** status 无未清理快照 */
@@ -314,7 +328,15 @@ var STATUS_MESSAGES = {
314
328
  /** status 变更状态:无变更 */
315
329
  STATUS_CHANGE_CLEAN: "\u65E0\u53D8\u66F4",
316
330
  /** status 快照对应 worktree 已不存在 */
317
- STATUS_SNAPSHOT_ORPHANED: "(\u5BF9\u5E94 worktree \u5DF2\u4E0D\u5B58\u5728)"
331
+ STATUS_SNAPSHOT_ORPHANED: (count) => `\u5176\u4E2D ${count} \u4E2A\u5FEB\u7167\u5BF9\u5E94\u7684 worktree \u5DF2\u4E0D\u5B58\u5728`,
332
+ /** status 分支创建时间标签 */
333
+ STATUS_CREATED_AT: (relativeTime) => `\u521B\u5EFA\u4E8E ${relativeTime}`,
334
+ /** status 分支无分叉提交时的提示 */
335
+ STATUS_NO_DIVERGED_COMMITS: "\u5C1A\u65E0\u5206\u53C9\u63D0\u4EA4",
336
+ /** status 上次验证时间标签 */
337
+ STATUS_LAST_VALIDATED: (relativeTime) => `\u4E0A\u6B21\u9A8C\u8BC1: ${relativeTime}`,
338
+ /** status 未验证警示 */
339
+ STATUS_NOT_VALIDATED: "\u2717 \u672A\u9A8C\u8BC1"
318
340
  };
319
341
 
320
342
  // src/constants/messages/alias.ts
package/docs/spec.md CHANGED
@@ -603,40 +603,104 @@ git restore --staged .
603
603
  如果用户传入了 `-r, --run` 选项,在 validate 成功后自动在主 worktree 中执行指定命令:
604
604
 
605
605
  ```bash
606
- # 示例
606
+ # 示例:单命令
607
607
  clawt validate -b feature-scheme-1 -r "npm test"
608
+
609
+ # 示例:并行执行多个命令(& 为并行分隔符)
610
+ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
608
611
  ```
609
612
 
610
613
  **执行说明:**
611
614
 
612
- - 命令通过 `spawnSync` + `inherit` stdio 模式在主 worktree 中执行,输出实时显示在终端
613
615
  - 命令执行失败(退出码非 0 或进程启动失败)**不影响** validate 本身的结果,仅输出提示信息
614
616
  - `--clean` 模式下传入 `--run` 会被忽略(只执行 clean 逻辑)
615
617
 
618
+ **命令解析规则:**
619
+
620
+ `-r` 选项支持通过 `&` 将多个命令并行执行。解析由 `parseParallelCommands()`(`src/utils/shell.ts`)负责:
621
+
622
+ 1. 先将命令字符串中的 `&&` 临时替换为占位符,避免被误拆
623
+ 2. 按单个 `&` 分割为多个独立命令
624
+ 3. 还原占位符为 `&&`,去除首尾空白,过滤空串
625
+
626
+ | 输入示例 | 解析结果 | 执行方式 |
627
+ | -------- | -------- | -------- |
628
+ | `"npm test"` | `["npm test"]` | 单命令,同步执行(`spawnSync` + `inherit`) |
629
+ | `"npm lint && npm test"` | `["npm lint && npm test"]` | 单命令(`&&` 不拆分),同步执行 |
630
+ | `"npm test & npm build"` | `["npm test", "npm build"]` | 并行执行(`spawn` + `Promise.all`) |
631
+ | `"npm lint && npm test & npm build"` | `["npm lint && npm test", "npm build"]` | 并行执行 2 个命令 |
632
+
633
+ **单命令执行:**
634
+
635
+ 当解析后只有 1 个命令时,通过 `spawnSync` + `inherit` stdio 模式同步执行,输出实时显示在终端。
636
+
637
+ **并行命令执行:**
638
+
639
+ 当解析后有多个命令时,通过 `runParallelCommands()`(`src/utils/shell.ts`)执行:
640
+
641
+ - 每个命令通过 Node.js `spawn` 以 shell 模式启动,`stdio: 'inherit'`
642
+ - 使用 `Promise.all` 等待全部命令完成
643
+ - 完成后汇总输出各命令的执行结果
644
+
645
+ **向后兼容性:**
646
+
647
+ - `-r "npm test"` — 单命令,走原有同步路径,行为无变化
648
+ - `-r "npm lint && npm test"` — `&&` 不拆分,走原有同步路径,行为无变化
649
+ - `-r "npm test & npm build"` — **新行为**:并行执行,等全部完成后汇总
650
+
616
651
  **输出格式:**
617
652
 
618
653
  ```
619
- # 命令执行成功
654
+ # 单命令执行成功
620
655
  正在主 worktree 中执行命令: npm test
621
656
  ────────────────────────────────────────
622
657
  ... 命令的实时输出 ...
623
658
  ────────────────────────────────────────
624
659
  ✓ 命令执行完成: npm test,退出码: 0
625
660
 
626
- # 命令执行失败(退出码非 0)
661
+ # 单命令执行失败(退出码非 0)
627
662
  正在主 worktree 中执行命令: npm test
628
663
  ────────────────────────────────────────
629
664
  ... 命令的实时输出 ...
630
665
  ────────────────────────────────────────
631
666
  ✗ 命令执行完成: npm test,退出码: 1
632
667
 
633
- # 命令执行出错(进程启动失败)
668
+ # 单命令执行出错(进程启动失败)
634
669
  正在主 worktree 中执行命令: nonexistent
635
670
  ────────────────────────────────────────
636
671
  ────────────────────────────────────────
637
672
  ✗ 命令执行出错: spawn ENOENT
673
+
674
+ # 并行命令执行(全部成功)
675
+ 正在并行执行 2 个命令...
676
+ [1/2] pnpm test
677
+ [2/2] pnpm build
678
+ ────────────────────────────────────────
679
+ ... 各命令的实时输出(交错显示) ...
680
+ ────────────────────────────────────────
681
+ ✓ pnpm test
682
+ ✓ pnpm build
683
+ ✓ 全部 2 个命令执行成功
684
+
685
+ # 并行命令执行(部分失败)
686
+ 正在并行执行 2 个命令...
687
+ [1/2] pnpm test
688
+ [2/2] pnpm build
689
+ ────────────────────────────────────────
690
+ ... 各命令的实时输出(交错显示) ...
691
+ ────────────────────────────────────────
692
+ ✗ pnpm test(退出码: 1)
693
+ ✓ pnpm build
694
+ 共 2 个命令,1 个成功,1 个失败
638
695
  ```
639
696
 
697
+ **实现要点:**
698
+
699
+ - 命令解析:`parseParallelCommands()`(`src/utils/shell.ts`)
700
+ - 并行执行:`runParallelCommands()`(`src/utils/shell.ts`),返回 `ParallelCommandResult[]`
701
+ - 结果汇总:`reportParallelResults()`(`src/commands/validate.ts`)
702
+ - 消息常量:`MESSAGES.VALIDATE_PARALLEL_*` 系列(`src/constants/messages/validate.ts`)
703
+
640
704
  #### 增量 validate(存在历史快照)
641
705
 
642
706
  当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
@@ -1367,7 +1431,7 @@ clawt status [--json]
1367
1431
 
1368
1432
  **使用场景:**
1369
1433
 
1370
- 在管理多个 worktree 时,快速了解项目全局状态:主 worktree 当前分支及干净状态、所有 worktree 的变更情况和与主分支的同步状态、未清理的 validate 快照。
1434
+ 在管理多个 worktree 时,快速了解项目全局状态:主 worktree 当前分支及干净状态、所有 worktree 的变更情况和与主分支的同步状态、validate 快照摘要。
1371
1435
 
1372
1436
  **运行流程:**
1373
1437
 
@@ -1382,17 +1446,18 @@ clawt status [--json]
1382
1446
  - **变更状态**(优先级:合并冲突 > 未提交修改 > 已提交 > 无变更)
1383
1447
  - **行数差异**(新增/删除行数,通过 `getDiffStat()` 获取)
1384
1448
  - **提交差异**(相对于主分支的领先提交数 `getCommitCountAhead()` 和落后提交数 `getCommitCountBehind()`)
1385
- - **快照状态**(是否存在 validate 快照)
1386
- 4. **收集未清理的 validate 快照**:
1449
+ - **快照时间**(validate 快照文件的 mtime,通过 `getSnapshotModifiedTime()` 获取,返回 ISO 8601 时间字符串或 null)
1450
+ - **分支创建时间**(通过 `getBranchCreatedAt()` 从 git reflog 获取分支创建时的时间戳)
1451
+ 4. **收集 validate 快照摘要**:
1387
1452
  - 通过 `getProjectSnapshotBranches()` 扫描快照目录下的 `.tree` 文件获取所有存在快照的分支名
1388
- - 对比现有 worktree 分支列表,标识孤立快照(对应 worktree 已不存在的快照)
1453
+ - 统计快照总数和孤立快照数(对应 worktree 已不存在的快照)
1389
1454
  5. **输出状态信息**:
1390
1455
  - 指定 `--json` → 以 JSON 格式输出完整状态数据(`JSON.stringify`)
1391
1456
  - 未指定 → 以文本格式输出
1392
1457
 
1393
1458
  **文本输出格式(默认):**
1394
1459
 
1395
- 输出分为三个区块:主 Worktree、Worktree 列表、未清理的 Validate 快照。
1460
+ 输出分为三个区块:主 Worktree、Worktree 列表、Validate 快照摘要。每个 worktree 条目每行展示一种信息。
1396
1461
 
1397
1462
  ```
1398
1463
  ════════════════════════════════════════
@@ -1409,16 +1474,18 @@ clawt status [--json]
1409
1474
 
1410
1475
  ● feature-login [已提交]
1411
1476
  +120 -30 3 个本地提交 与主分支同步
1412
- validate 快照
1477
+ 创建于 3 天前
1478
+ 上次验证: 2 小时前
1413
1479
 
1414
1480
  ● feature-signup [未提交修改]
1415
1481
  +45 -10 1 个本地提交 落后主分支 2 个提交
1482
+ 创建于 1 天前
1483
+ ✗ 未验证
1416
1484
 
1417
1485
  ────────────────────────────────────────
1418
1486
 
1419
- 未清理的 Validate 快照 (1 个)
1420
-
1421
- ⚠ old-feature (对应 worktree 已不存在)
1487
+ ◆ Validate 快照 (3 个)
1488
+ 其中 1 个快照对应的 worktree 已不存在
1422
1489
 
1423
1490
  ════════════════════════════════════════
1424
1491
  ```
@@ -1438,11 +1505,21 @@ clawt status [--json]
1438
1505
  - 本地提交数(`N 个本地提交`)仅在有提交时展示
1439
1506
  - 与主分支同步状态始终展示(落后时显示黄色,同步时显示绿色)
1440
1507
 
1508
+ **分支创建时间行:**
1509
+
1510
+ - 通过 `getBranchCreatedAt()` 从 git reflog 获取分支创建时间,以 `formatRelativeTime()` 格式化为中文相对时间(如"3 天前"、"2 小时前"、"刚刚")
1511
+ - 展示为灰色文本 `创建于 X前`,无法获取时不展示
1512
+
1513
+ **验证状态行:**
1514
+
1515
+ - 有快照时:显示绿色 `上次验证: X前`(通过 `getSnapshotModifiedTime()` 获取快照文件 mtime,再用 `formatRelativeTime()` 格式化)
1516
+ - 无快照时:显示红色 `✗ 未验证` 警示
1517
+
1441
1518
  **快照区块:**
1442
1519
 
1443
- - 每个快照显示对应的分支名
1444
- - 如果对应的 worktree 仍存在,显示蓝色圆点图标
1445
- - 如果对应的 worktree 已不存在(孤立快照),显示黄色警告图标并标注 `(对应 worktree 已不存在)`
1520
+ - 标题显示快照总数
1521
+ - 如果存在孤立快照(对应 worktree 已不存在),显示黄色警告 `其中 N 个快照对应的 worktree 已不存在`
1522
+ - 无孤立快照时不显示额外信息
1446
1523
 
1447
1524
  **JSON 输出格式(`--json`):**
1448
1525
 
@@ -1460,25 +1537,31 @@ clawt status [--json]
1460
1537
  "changeStatus": "committed",
1461
1538
  "commitsAhead": 3,
1462
1539
  "commitsBehind": 0,
1463
- "hasSnapshot": true,
1540
+ "snapshotTime": "2025-02-06T12:30:00.000Z",
1464
1541
  "insertions": 120,
1465
- "deletions": 30
1466
- }
1467
- ],
1468
- "snapshots": [
1469
- {
1470
- "branch": "old-feature",
1471
- "worktreeExists": false
1542
+ "deletions": 30,
1543
+ "createdAt": "2025-02-03T10:00:00.000Z"
1472
1544
  }
1473
1545
  ],
1546
+ "snapshots": {
1547
+ "total": 3,
1548
+ "orphaned": 1
1549
+ },
1474
1550
  "totalWorktrees": 1
1475
1551
  }
1476
1552
  ```
1477
1553
 
1478
1554
  **实现要点:**
1479
1555
 
1480
- - 类型定义在 `src/types/status.ts`:`WorktreeDetailedStatus`、`MainWorktreeStatus`、`SnapshotInfo`、`StatusResult`
1481
- - 消息常量在 `MESSAGES.STATUS_*` 系列
1556
+ - 类型定义在 `src/types/status.ts`:`WorktreeDetailedStatus`(`hasSnapshot` 已改为 `snapshotTime: string | null`,新增 `createdAt: string | null`)、`MainWorktreeStatus`、`SnapshotInfo`、`SnapshotSummary`(新增,包含 `total` 和 `orphaned`)、`StatusResult`(`snapshots` 已从 `SnapshotInfo[]` 改为 `SnapshotSummary`)
1557
+ - 消息常量在 `MESSAGES.STATUS_*` 系列,新增:
1558
+ - `STATUS_LAST_VALIDATED`:上次验证时间标签(如 `上次验证: 2 小时前`)
1559
+ - `STATUS_NOT_VALIDATED`:未验证红色警示文本(`✗ 未验证`)
1560
+ - `STATUS_CREATED_AT`:分支创建时间标签(如 `创建于 3 天前`)
1561
+ - `STATUS_SNAPSHOT_ORPHANED`:改为接受数量参数的函数(如 `其中 1 个快照对应的 worktree 已不存在`)
1562
+ - `getBranchCreatedAt()` 是新增的工具函数(在 `src/utils/git.ts`),通过 `git reflog show <branch> --format=%cI` 获取 reflog 最后一条记录的时间戳(即分支创建时间),返回 ISO 8601 格式字符串或 null
1563
+ - `getSnapshotModifiedTime()` 是新增的工具函数(在 `src/utils/validate-snapshot.ts`),通过 `fs.statSync` 获取快照文件的修改时间(mtime),返回 ISO 8601 格式字符串或 null
1564
+ - `formatRelativeTime()` 是新增的格式化函数(在 `src/utils/formatter.ts`),将 ISO 8601 日期字符串转换为中文相对时间描述(如"3 天前"、"2 小时前"、"刚刚"),无效日期时返回 null
1482
1565
  - `getCommitCountBehind()` 是新增的工具函数(在 `src/utils/git.ts`),通过 `git rev-list --count <branch>..HEAD` 计算落后提交数
1483
1566
  - `getProjectSnapshotBranches()` 是新增的工具函数(在 `src/utils/validate-snapshot.ts`),通过扫描快照目录下的 `.tree` 文件提取分支名列表
1484
1567