clawt 1.3.0 → 2.0.0

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/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { registerResumeCommand } from './commands/resume.js';
12
12
  import { registerValidateCommand } from './commands/validate.js';
13
13
  import { registerMergeCommand } from './commands/merge.js';
14
14
  import { registerConfigCommand } from './commands/config.js';
15
+ import { registerSyncCommand } from './commands/sync.js';
15
16
 
16
17
  // 从 package.json 读取版本号,避免硬编码
17
18
  const require = createRequire(import.meta.url);
@@ -36,6 +37,7 @@ registerResumeCommand(program);
36
37
  registerValidateCommand(program);
37
38
  registerMergeCommand(program);
38
39
  registerConfigCommand(program);
40
+ registerSyncCommand(program);
39
41
 
40
42
  // 全局未捕获异常处理
41
43
  process.on('uncaughtException', (error) => {
@@ -36,8 +36,6 @@ export interface RemoveOptions {
36
36
  all?: boolean;
37
37
  /** 分支名 */
38
38
  branch?: string;
39
- /** 指定索引 */
40
- index?: number;
41
39
  }
42
40
 
43
41
  /** resume 命令选项 */
@@ -45,3 +43,9 @@ export interface ResumeOptions {
45
43
  /** 要恢复的分支名 */
46
44
  branch: string;
47
45
  }
46
+
47
+ /** sync 命令选项 */
48
+ export interface SyncOptions {
49
+ /** 要同步的分支名 */
50
+ branch: string;
51
+ }
@@ -1,5 +1,5 @@
1
1
  export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
2
- export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions } from './command.js';
2
+ export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions } from './command.js';
3
3
  export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
4
4
  export type { ClaudeCodeResult } from './claudeCode.js';
5
5
  export type { TaskResult, TaskSummary } from './taskResult.js';
@@ -10,9 +10,9 @@ export interface WorktreeInfo {
10
10
  export interface WorktreeStatus {
11
11
  /** 相对于主分支的新增提交数 */
12
12
  commitCount: number;
13
- /** 新增行数 */
13
+ /** 工作区和暂存区的新增行数 */
14
14
  insertions: number;
15
- /** 删除行数 */
15
+ /** 工作区和暂存区的删除行数 */
16
16
  deletions: number;
17
17
  /** 工作区是否有未提交修改 */
18
18
  hasDirtyFiles: boolean;
package/src/utils/git.ts CHANGED
@@ -190,6 +190,15 @@ export function gitStashPop(index: number = 0, cwd?: string): void {
190
190
  execCommand(`git stash pop stash@{${index}}`, { cwd });
191
191
  }
192
192
 
193
+ /**
194
+ * git stash drop stash@{index}
195
+ * @param {number} index - stash 索引
196
+ * @param {string} [cwd] - 工作目录
197
+ */
198
+ export function gitStashDrop(index: number = 0, cwd?: string): void {
199
+ execCommand(`git stash drop stash@{${index}}`, { cwd });
200
+ }
201
+
193
202
  /**
194
203
  * git stash list
195
204
  * @param {string} cwd - 工作目录
@@ -277,25 +286,14 @@ function parseShortStat(output: string): { insertions: number; deletions: number
277
286
  }
278
287
 
279
288
  /**
280
- * 获取目标分支的变更统计(已提交 + 未提交)
281
- * @param {string} branchName - 目标分支名
289
+ * 获取 worktree 中工作区和暂存区的变更统计
282
290
  * @param {string} worktreePath - worktree 目录路径
283
- * @param {string} [cwd] - 执行 git diff HEAD...branch 的工作目录
284
- * @returns {{ insertions: number; deletions: number }} 聚合后的新增和删除行数
291
+ * @returns {{ insertions: number; deletions: number }} 新增和删除行数
285
292
  */
286
- export function getDiffStat(branchName: string, worktreePath: string, cwd?: string): { insertions: number; deletions: number } {
287
- // 已提交的变更(当前分支与目标分支的差异)
288
- const committedOutput = execCommand(`git diff --shortstat HEAD...${branchName}`, { cwd });
289
- const committed = parseShortStat(committedOutput);
290
-
291
- // 未提交的变更(在 worktree 内执行)
292
- const uncommittedOutput = execCommand('git diff --shortstat HEAD', { cwd: worktreePath });
293
- const uncommitted = parseShortStat(uncommittedOutput);
294
-
295
- return {
296
- insertions: committed.insertions + uncommitted.insertions,
297
- deletions: committed.deletions + uncommitted.deletions,
298
- };
293
+ export function getDiffStat(worktreePath: string): { insertions: number; deletions: number } {
294
+ // 工作区和暂存区相对于 HEAD 的变更
295
+ const output = execCommand('git diff --shortstat HEAD', { cwd: worktreePath });
296
+ return parseShortStat(output);
299
297
  }
300
298
 
301
299
  /**
@@ -320,3 +318,54 @@ export function gitDiffCachedBinary(cwd?: string): Buffer {
320
318
  export function gitApplyCachedFromStdin(patchContent: Buffer, cwd?: string): void {
321
319
  execCommandWithInput('git', ['apply', '--cached'], { input: patchContent, cwd });
322
320
  }
321
+
322
+ /**
323
+ * 获取当前分支名
324
+ * @param {string} [cwd] - 工作目录
325
+ * @returns {string} 当前分支名
326
+ */
327
+ export function getCurrentBranch(cwd?: string): string {
328
+ return execCommand('git rev-parse --abbrev-ref HEAD', { cwd });
329
+ }
330
+
331
+ /**
332
+ * 获取当前 HEAD 的 commit hash
333
+ * @param {string} [cwd] - 工作目录
334
+ * @returns {string} commit hash
335
+ */
336
+ export function getHeadCommitHash(cwd?: string): string {
337
+ return execCommand('git rev-parse HEAD', { cwd });
338
+ }
339
+
340
+ /**
341
+ * 获取目标分支相对于当前分支的已提交变更(含二进制文件)
342
+ * 使用三点 diff(HEAD...branchName)获取自分叉点以来的变更
343
+ * @param {string} branchName - 目标分支名
344
+ * @param {string} [cwd] - 工作目录(应在主 worktree 中执行)
345
+ * @returns {Buffer} diff 原始输出
346
+ */
347
+ export function gitDiffBinaryAgainstBranch(branchName: string, cwd?: string): Buffer {
348
+ logger.debug(`执行命令: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ''}`);
349
+ return execSync(`git diff HEAD...${branchName} --binary`, {
350
+ cwd,
351
+ stdio: ['pipe', 'pipe', 'pipe'],
352
+ });
353
+ }
354
+
355
+ /**
356
+ * 将 patch 内容通过 stdin 应用到工作目录(不带 --cached)
357
+ * @param {Buffer} patchContent - patch 内容
358
+ * @param {string} [cwd] - 工作目录
359
+ */
360
+ export function gitApplyFromStdin(patchContent: Buffer, cwd?: string): void {
361
+ execCommandWithInput('git', ['apply'], { input: patchContent, cwd });
362
+ }
363
+
364
+ /**
365
+ * git reset --soft HEAD~<count>,撤销 commit 但保留变更在暂存区
366
+ * @param {number} count - 撤销的 commit 数量
367
+ * @param {string} [cwd] - 工作目录
368
+ */
369
+ export function gitResetSoft(count: number = 1, cwd?: string): void {
370
+ execCommand(`git reset --soft HEAD~${count}`, { cwd });
371
+ }
@@ -20,6 +20,7 @@ export {
20
20
  gitStashPush,
21
21
  gitStashApply,
22
22
  gitStashPop,
23
+ gitStashDrop,
23
24
  gitStashList,
24
25
  gitRestoreStaged,
25
26
  gitWorktreeList,
@@ -29,6 +30,11 @@ export {
29
30
  getDiffStat,
30
31
  gitDiffCachedBinary,
31
32
  gitApplyCachedFromStdin,
33
+ getCurrentBranch,
34
+ getHeadCommitHash,
35
+ gitDiffBinaryAgainstBranch,
36
+ gitApplyFromStdin,
37
+ gitResetSoft,
32
38
  } from './git.js';
33
39
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
34
40
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
@@ -38,4 +44,4 @@ export { printSuccess, printError, printWarning, printInfo, printSeparator, prin
38
44
  export { ensureDir, removeEmptyDir } from './fs.js';
39
45
  export { multilineInput } from './prompt.js';
40
46
  export { launchInteractiveClaude } from './claude.js';
41
- export { getSnapshotPath, hasSnapshot, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
47
+ export { getSnapshotPath, hasSnapshot, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, readSnapshotHead } from './validate-snapshot.js';
@@ -14,6 +14,16 @@ export function getSnapshotPath(projectName: string, branchName: string): string
14
14
  return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.patch`);
15
15
  }
16
16
 
17
+ /**
18
+ * 获取指定项目和分支的快照 HEAD hash 文件路径
19
+ * @param {string} projectName - 项目名
20
+ * @param {string} branchName - 分支名
21
+ * @returns {string} head 文件的绝对路径
22
+ */
23
+ function getSnapshotHeadPath(projectName: string, branchName: string): string {
24
+ return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
25
+ }
26
+
17
27
  /**
18
28
  * 判断指定项目和分支是否存在 validate 快照
19
29
  * @param {string} projectName - 项目名
@@ -41,12 +51,17 @@ export function readSnapshot(projectName: string, branchName: string): Buffer {
41
51
  * @param {string} projectName - 项目名
42
52
  * @param {string} branchName - 分支名
43
53
  * @param {Buffer} patch - patch 内容(Buffer 格式)
54
+ * @param {string} [headHash] - 主分支 HEAD commit hash(用于增量 validate 一致性校验)
44
55
  */
45
- export function writeSnapshot(projectName: string, branchName: string, patch: Buffer): void {
56
+ export function writeSnapshot(projectName: string, branchName: string, patch: Buffer, headHash?: string): void {
46
57
  const snapshotPath = getSnapshotPath(projectName, branchName);
47
58
  const snapshotDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
48
59
  ensureDir(snapshotDir);
49
60
  writeFileSync(snapshotPath, patch);
61
+ // 保存主分支 HEAD hash,用于下次增量 validate 时校验一致性
62
+ if (headHash) {
63
+ writeFileSync(getSnapshotHeadPath(projectName, branchName), headHash, 'utf-8');
64
+ }
50
65
  logger.info(`已保存 validate 快照: ${snapshotPath}`);
51
66
  }
52
67
 
@@ -61,6 +76,25 @@ export function removeSnapshot(projectName: string, branchName: string): void {
61
76
  unlinkSync(snapshotPath);
62
77
  logger.info(`已删除 validate 快照: ${snapshotPath}`);
63
78
  }
79
+ // 同时删除对应的 .head 文件
80
+ const headPath = getSnapshotHeadPath(projectName, branchName);
81
+ if (existsSync(headPath)) {
82
+ unlinkSync(headPath);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 读取快照保存时的主分支 HEAD commit hash
88
+ * @param {string} projectName - 项目名
89
+ * @param {string} branchName - 分支名
90
+ * @returns {string | null} HEAD hash,不存在则返回 null
91
+ */
92
+ export function readSnapshotHead(projectName: string, branchName: string): string | null {
93
+ const headPath = getSnapshotHeadPath(projectName, branchName);
94
+ if (!existsSync(headPath)) {
95
+ return null;
96
+ }
97
+ return readFileSync(headPath, 'utf-8').trim();
64
98
  }
65
99
 
66
100
  /**
@@ -115,7 +115,7 @@ export function cleanupWorktrees(worktrees: WorktreeInfo[]): void {
115
115
  export function getWorktreeStatus(worktree: WorktreeInfo): WorktreeStatus | null {
116
116
  try {
117
117
  const commitCount = getCommitCountAhead(worktree.branch);
118
- const { insertions, deletions } = getDiffStat(worktree.branch, worktree.path);
118
+ const { insertions, deletions } = getDiffStat(worktree.path);
119
119
  const hasDirtyFiles = !isWorkingDirClean(worktree.path);
120
120
 
121
121
  return { commitCount, insertions, deletions, hasDirtyFiles };