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 +37 -0
- package/dist/index.js +198 -90
- package/dist/postinstall.js +20 -3
- package/docs/spec.md +89 -30
- package/package.json +1 -1
- package/src/commands/status.ts +68 -39
- package/src/commands/sync.ts +34 -14
- package/src/commands/validate.ts +50 -8
- package/src/constants/messages/status.ts +10 -2
- package/src/constants/messages/validate.ts +12 -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 +3 -2
- package/src/utils/validate-snapshot.ts +14 -1
- package/tests/unit/commands/status.test.ts +128 -9
- package/tests/unit/utils/formatter.test.ts +42 -1
- package/tests/unit/utils/git.test.ts +36 -0
- package/tests/unit/utils/validate-snapshot.test.ts +21 -1
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: "
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
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
|
-
|
|
3053
|
+
printInfo(` ${chalk8.green(`+${wt.insertions}`)} ${chalk8.red(`-${wt.deletions}`)}`);
|
|
2948
3054
|
}
|
|
2949
3055
|
if (wt.commitsAhead > 0) {
|
|
2950
|
-
|
|
3056
|
+
printInfo(` ${chalk8.yellow(`${wt.commitsAhead} \u4E2A\u672C\u5730\u63D0\u4EA4`)}`);
|
|
2951
3057
|
}
|
|
2952
3058
|
if (wt.commitsBehind > 0) {
|
|
2953
|
-
|
|
3059
|
+
printInfo(` ${chalk8.yellow(`\u843D\u540E\u4E3B\u5206\u652F ${wt.commitsBehind} \u4E2A\u63D0\u4EA4`)}`);
|
|
2954
3060
|
} else {
|
|
2955
|
-
|
|
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
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
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.
|
|
2977
|
-
|
|
2978
|
-
|
|
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
|
}
|
package/dist/postinstall.js
CHANGED
|
@@ -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: "
|
|
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:
|
|
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
|