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 +41 -1
- package/dist/index.js +179 -32
- package/dist/postinstall.js +25 -3
- package/docs/spec.md +110 -27
- package/package.json +1 -1
- package/src/commands/status.ts +68 -39
- package/src/commands/validate.ts +73 -6
- package/src/constants/messages/status.ts +10 -2
- package/src/constants/messages/validate.ts +21 -0
- package/src/types/index.ts +1 -1
- package/src/types/status.ts +14 -4
- package/src/utils/formatter.ts +42 -0
- package/src/utils/git.ts +20 -0
- package/src/utils/index.ts +5 -3
- package/src/utils/shell.ts +65 -0
- package/src/utils/validate-snapshot.ts +14 -1
- package/tests/unit/commands/status.test.ts +128 -9
- package/tests/unit/commands/validate.test.ts +103 -0
- package/tests/unit/utils/formatter.test.ts +42 -1
- package/tests/unit/utils/git.test.ts +36 -0
- package/tests/unit/utils/shell.test.ts +42 -1
- package/tests/unit/utils/validate-snapshot.test.ts +21 -1
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: "
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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
|
-
|
|
3017
|
+
printInfo(` ${chalk8.green(`+${wt.insertions}`)} ${chalk8.red(`-${wt.deletions}`)}`);
|
|
2873
3018
|
}
|
|
2874
3019
|
if (wt.commitsAhead > 0) {
|
|
2875
|
-
|
|
3020
|
+
printInfo(` ${chalk8.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
|
|
2876
3021
|
}
|
|
2877
3022
|
if (wt.commitsBehind > 0) {
|
|
2878
|
-
|
|
3023
|
+
printInfo(` ${chalk8.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
|
|
2879
3024
|
} else {
|
|
2880
|
-
|
|
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
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
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.
|
|
2902
|
-
|
|
2903
|
-
|
|
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
|
}
|
package/dist/postinstall.js
CHANGED
|
@@ -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: "
|
|
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:
|
|
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
|
-
#
|
|
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
|
|
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
|
-
-
|
|
1386
|
-
|
|
1449
|
+
- **快照时间**(validate 快照文件的 mtime,通过 `getSnapshotModifiedTime()` 获取,返回 ISO 8601 时间字符串或 null)
|
|
1450
|
+
- **分支创建时间**(通过 `getBranchCreatedAt()` 从 git reflog 获取分支创建时的时间戳)
|
|
1451
|
+
4. **收集 validate 快照摘要**:
|
|
1387
1452
|
- 通过 `getProjectSnapshotBranches()` 扫描快照目录下的 `.tree` 文件获取所有存在快照的分支名
|
|
1388
|
-
-
|
|
1453
|
+
- 统计快照总数和孤立快照数(对应 worktree 已不存在的快照)
|
|
1389
1454
|
5. **输出状态信息**:
|
|
1390
1455
|
- 指定 `--json` → 以 JSON 格式输出完整状态数据(`JSON.stringify`)
|
|
1391
1456
|
- 未指定 → 以文本格式输出
|
|
1392
1457
|
|
|
1393
1458
|
**文本输出格式(默认):**
|
|
1394
1459
|
|
|
1395
|
-
输出分为三个区块:主 Worktree、Worktree
|
|
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
|
-
|
|
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
|
-
◆
|
|
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
|
-
-
|
|
1445
|
-
-
|
|
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
|
-
"
|
|
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
|
|
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
|
|