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/CLAUDE.md +19 -7
- package/README.md +24 -5
- package/dist/index.js +167 -75
- package/docs/spec.md +101 -46
- package/package.json +1 -1
- package/src/commands/remove.ts +2 -17
- package/src/commands/sync.ts +116 -0
- package/src/commands/validate.ts +84 -44
- package/src/constants/messages.ts +15 -4
- package/src/index.ts +2 -0
- package/src/types/command.ts +6 -2
- package/src/types/index.ts +1 -1
- package/src/types/worktree.ts +2 -2
- package/src/utils/git.ts +66 -17
- package/src/utils/index.ts +7 -1
- package/src/utils/validate-snapshot.ts +35 -1
- package/src/utils/worktree.ts +1 -1
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) => {
|
package/src/types/command.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -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';
|
package/src/types/worktree.ts
CHANGED
|
@@ -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
|
-
* @
|
|
284
|
-
* @returns {{ insertions: number; deletions: number }} 聚合后的新增和删除行数
|
|
291
|
+
* @returns {{ insertions: number; deletions: number }} 新增和删除行数
|
|
285
292
|
*/
|
|
286
|
-
export function getDiffStat(
|
|
287
|
-
//
|
|
288
|
-
const
|
|
289
|
-
|
|
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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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
|
/**
|
package/src/utils/worktree.ts
CHANGED
|
@@ -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.
|
|
118
|
+
const { insertions, deletions } = getDiffStat(worktree.path);
|
|
119
119
|
const hasDirtyFiles = !isWorkingDirClean(worktree.path);
|
|
120
120
|
|
|
121
121
|
return { commitCount, insertions, deletions, hasDirtyFiles };
|