clawt 1.0.5 → 1.0.6
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/dist/index.js +66 -3
- package/package.json +1 -1
- package/src/commands/list.ts +14 -1
- package/src/constants/messages.ts +2 -0
- package/src/types/index.ts +1 -1
- package/src/types/worktree.ts +12 -0
- package/src/utils/formatter.ts +30 -0
- package/src/utils/git.ts +55 -0
- package/src/utils/index.ts +4 -2
- package/src/utils/worktree.ts +21 -2
package/dist/index.js
CHANGED
|
@@ -79,7 +79,9 @@ var MESSAGES = {
|
|
|
79
79
|
/** 粗分隔线 */
|
|
80
80
|
DOUBLE_SEPARATOR: "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
81
81
|
/** 创建数量参数无效 */
|
|
82
|
-
INVALID_COUNT: (value) => `\u65E0\u6548\u7684\u521B\u5EFA\u6570\u91CF: "${value}"\uFF0C\u8BF7\u8F93\u5165\u6B63\u6574\u6570
|
|
82
|
+
INVALID_COUNT: (value) => `\u65E0\u6548\u7684\u521B\u5EFA\u6570\u91CF: "${value}"\uFF0C\u8BF7\u8F93\u5165\u6B63\u6574\u6570`,
|
|
83
|
+
/** worktree 状态获取失败 */
|
|
84
|
+
WORKTREE_STATUS_UNAVAILABLE: "(\u72B6\u6001\u4E0D\u53EF\u7528)"
|
|
83
85
|
};
|
|
84
86
|
|
|
85
87
|
// src/constants/exitCodes.ts
|
|
@@ -266,6 +268,33 @@ function hasLocalCommits(branchName, cwd) {
|
|
|
266
268
|
return false;
|
|
267
269
|
}
|
|
268
270
|
}
|
|
271
|
+
function getCommitCountAhead(branchName, cwd) {
|
|
272
|
+
const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
|
|
273
|
+
return parseInt(output, 10) || 0;
|
|
274
|
+
}
|
|
275
|
+
function parseShortStat(output) {
|
|
276
|
+
let insertions = 0;
|
|
277
|
+
let deletions = 0;
|
|
278
|
+
const insertMatch = output.match(/(\d+)\s+insertion/);
|
|
279
|
+
if (insertMatch) {
|
|
280
|
+
insertions = parseInt(insertMatch[1], 10);
|
|
281
|
+
}
|
|
282
|
+
const deleteMatch = output.match(/(\d+)\s+deletion/);
|
|
283
|
+
if (deleteMatch) {
|
|
284
|
+
deletions = parseInt(deleteMatch[1], 10);
|
|
285
|
+
}
|
|
286
|
+
return { insertions, deletions };
|
|
287
|
+
}
|
|
288
|
+
function getDiffStat(branchName, worktreePath, cwd) {
|
|
289
|
+
const committedOutput = execCommand(`git diff --shortstat HEAD...${branchName}`, { cwd });
|
|
290
|
+
const committed = parseShortStat(committedOutput);
|
|
291
|
+
const uncommittedOutput = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
|
|
292
|
+
const uncommitted = parseShortStat(uncommittedOutput);
|
|
293
|
+
return {
|
|
294
|
+
insertions: committed.insertions + uncommitted.insertions,
|
|
295
|
+
deletions: committed.deletions + uncommitted.deletions
|
|
296
|
+
};
|
|
297
|
+
}
|
|
269
298
|
|
|
270
299
|
// src/utils/formatter.ts
|
|
271
300
|
import chalk from "chalk";
|
|
@@ -300,6 +329,22 @@ function confirmAction(question) {
|
|
|
300
329
|
});
|
|
301
330
|
});
|
|
302
331
|
}
|
|
332
|
+
function formatWorktreeStatus(status) {
|
|
333
|
+
const parts = [];
|
|
334
|
+
parts.push(chalk.yellow(`${status.commitCount} \u4E2A\u63D0\u4EA4`));
|
|
335
|
+
if (status.insertions === 0 && status.deletions === 0) {
|
|
336
|
+
parts.push("\u65E0\u53D8\u66F4");
|
|
337
|
+
} else {
|
|
338
|
+
const diffParts = [];
|
|
339
|
+
diffParts.push(chalk.green(`+${status.insertions}`));
|
|
340
|
+
diffParts.push(chalk.red(`-${status.deletions}`));
|
|
341
|
+
parts.push(diffParts.join(" "));
|
|
342
|
+
}
|
|
343
|
+
if (status.hasDirtyFiles) {
|
|
344
|
+
parts.push(chalk.gray("(\u672A\u63D0\u4EA4\u4FEE\u6539)"));
|
|
345
|
+
}
|
|
346
|
+
return parts.join(" ");
|
|
347
|
+
}
|
|
303
348
|
|
|
304
349
|
// src/utils/branch.ts
|
|
305
350
|
function sanitizeBranchName(branchName) {
|
|
@@ -428,6 +473,17 @@ function cleanupWorktrees(worktrees) {
|
|
|
428
473
|
const projectDir = getProjectWorktreeDir();
|
|
429
474
|
removeEmptyDir(projectDir);
|
|
430
475
|
}
|
|
476
|
+
function getWorktreeStatus(worktree) {
|
|
477
|
+
try {
|
|
478
|
+
const commitCount = getCommitCountAhead(worktree.branch);
|
|
479
|
+
const { insertions, deletions } = getDiffStat(worktree.branch, worktree.path);
|
|
480
|
+
const hasDirtyFiles = !isWorkingDirClean(worktree.path);
|
|
481
|
+
return { commitCount, insertions, deletions, hasDirtyFiles };
|
|
482
|
+
} catch (error) {
|
|
483
|
+
logger.error(`\u83B7\u53D6 worktree \u72B6\u6001\u5931\u8D25: ${worktree.path} - ${error}`);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
431
487
|
|
|
432
488
|
// src/utils/config.ts
|
|
433
489
|
import { existsSync as existsSync4, readFileSync, writeFileSync } from "fs";
|
|
@@ -461,6 +517,7 @@ function ensureClawtDirs() {
|
|
|
461
517
|
import Enquirer from "enquirer";
|
|
462
518
|
|
|
463
519
|
// src/commands/list.ts
|
|
520
|
+
import chalk2 from "chalk";
|
|
464
521
|
function registerListCommand(program2) {
|
|
465
522
|
program2.command("list").description("\u5217\u51FA\u5F53\u524D\u9879\u76EE\u6240\u6709 worktree").action(() => {
|
|
466
523
|
handleList();
|
|
@@ -478,9 +535,15 @@ function handleList() {
|
|
|
478
535
|
} else {
|
|
479
536
|
for (const wt of worktrees) {
|
|
480
537
|
printInfo(` ${wt.path} [${wt.branch}]`);
|
|
538
|
+
const status = getWorktreeStatus(wt);
|
|
539
|
+
if (status) {
|
|
540
|
+
printInfo(` ${formatWorktreeStatus(status)}`);
|
|
541
|
+
} else {
|
|
542
|
+
printInfo(` ${chalk2.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
|
|
543
|
+
}
|
|
544
|
+
printInfo("");
|
|
481
545
|
}
|
|
482
|
-
printInfo(`
|
|
483
|
-
\u5171 ${worktrees.length} \u4E2A worktree`);
|
|
546
|
+
printInfo(`\u5171 ${worktrees.length} \u4E2A worktree`);
|
|
484
547
|
}
|
|
485
548
|
}
|
|
486
549
|
|
package/package.json
CHANGED
package/src/commands/list.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { MESSAGES } from '../constants/index.js';
|
|
3
4
|
import { logger } from '../logger/index.js';
|
|
4
5
|
import {
|
|
5
6
|
validateMainWorktree,
|
|
6
7
|
getProjectName,
|
|
7
8
|
getProjectWorktrees,
|
|
9
|
+
getWorktreeStatus,
|
|
10
|
+
formatWorktreeStatus,
|
|
8
11
|
printInfo,
|
|
9
12
|
} from '../utils/index.js';
|
|
10
13
|
|
|
@@ -39,7 +42,17 @@ function handleList(): void {
|
|
|
39
42
|
} else {
|
|
40
43
|
for (const wt of worktrees) {
|
|
41
44
|
printInfo(` ${wt.path} [${wt.branch}]`);
|
|
45
|
+
|
|
46
|
+
// 获取并展示 worktree 变更状态
|
|
47
|
+
const status = getWorktreeStatus(wt);
|
|
48
|
+
if (status) {
|
|
49
|
+
printInfo(` ${formatWorktreeStatus(status)}`);
|
|
50
|
+
} else {
|
|
51
|
+
printInfo(` ${chalk.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printInfo('');
|
|
42
55
|
}
|
|
43
|
-
printInfo(
|
|
56
|
+
printInfo(`共 ${worktrees.length} 个 worktree`);
|
|
44
57
|
}
|
|
45
58
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { ClawtConfig } from './config.js';
|
|
2
2
|
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions } from './command.js';
|
|
3
|
-
export type { WorktreeInfo } from './worktree.js';
|
|
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
|
@@ -5,3 +5,15 @@ export interface WorktreeInfo {
|
|
|
5
5
|
/** 分支名 */
|
|
6
6
|
branch: string;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** worktree 变更统计信息 */
|
|
10
|
+
export interface WorktreeStatus {
|
|
11
|
+
/** 相对于主分支的新增提交数 */
|
|
12
|
+
commitCount: number;
|
|
13
|
+
/** 新增行数 */
|
|
14
|
+
insertions: number;
|
|
15
|
+
/** 删除行数 */
|
|
16
|
+
deletions: number;
|
|
17
|
+
/** 工作区是否有未提交修改 */
|
|
18
|
+
hasDirtyFiles: boolean;
|
|
19
|
+
}
|
package/src/utils/formatter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { MESSAGES } from '../constants/index.js';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
+
import type { WorktreeStatus } from '../types/index.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 输出成功信息
|
|
@@ -65,3 +66,32 @@ export function confirmAction(question: string): Promise<boolean> {
|
|
|
65
66
|
});
|
|
66
67
|
});
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 将 WorktreeStatus 格式化为带颜色的字符串
|
|
72
|
+
* @param {WorktreeStatus} status - worktree 变更统计信息
|
|
73
|
+
* @returns {string} 格式化后的状态字符串
|
|
74
|
+
*/
|
|
75
|
+
export function formatWorktreeStatus(status: WorktreeStatus): string {
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
|
|
78
|
+
// 提交数(黄色)
|
|
79
|
+
parts.push(chalk.yellow(`${status.commitCount} 个提交`));
|
|
80
|
+
|
|
81
|
+
// 变更统计
|
|
82
|
+
if (status.insertions === 0 && status.deletions === 0) {
|
|
83
|
+
parts.push('无变更');
|
|
84
|
+
} else {
|
|
85
|
+
const diffParts: string[] = [];
|
|
86
|
+
diffParts.push(chalk.green(`+${status.insertions}`));
|
|
87
|
+
diffParts.push(chalk.red(`-${status.deletions}`));
|
|
88
|
+
parts.push(diffParts.join(' '));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 未提交修改提示(灰色)
|
|
92
|
+
if (status.hasDirtyFiles) {
|
|
93
|
+
parts.push(chalk.gray('(未提交修改)'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return parts.join(' ');
|
|
97
|
+
}
|
package/src/utils/git.ts
CHANGED
|
@@ -241,3 +241,58 @@ export function hasLocalCommits(branchName: string, cwd?: string): boolean {
|
|
|
241
241
|
return false;
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 获取目标分支相对于当前分支的新增提交数
|
|
247
|
+
* @param {string} branchName - 目标分支名
|
|
248
|
+
* @param {string} [cwd] - 工作目录
|
|
249
|
+
* @returns {number} 新增提交数
|
|
250
|
+
*/
|
|
251
|
+
export function getCommitCountAhead(branchName: string, cwd?: string): number {
|
|
252
|
+
const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
|
|
253
|
+
return parseInt(output, 10) || 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 解析 git diff --shortstat 输出,提取新增行数和删除行数
|
|
258
|
+
* @param {string} output - shortstat 输出字符串
|
|
259
|
+
* @returns {{ insertions: number; deletions: number }} 新增和删除行数
|
|
260
|
+
*/
|
|
261
|
+
function parseShortStat(output: string): { insertions: number; deletions: number } {
|
|
262
|
+
let insertions = 0;
|
|
263
|
+
let deletions = 0;
|
|
264
|
+
|
|
265
|
+
const insertMatch = output.match(/(\d+)\s+insertion/);
|
|
266
|
+
if (insertMatch) {
|
|
267
|
+
insertions = parseInt(insertMatch[1], 10);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const deleteMatch = output.match(/(\d+)\s+deletion/);
|
|
271
|
+
if (deleteMatch) {
|
|
272
|
+
deletions = parseInt(deleteMatch[1], 10);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { insertions, deletions };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 获取目标分支的变更统计(已提交 + 未提交)
|
|
280
|
+
* @param {string} branchName - 目标分支名
|
|
281
|
+
* @param {string} worktreePath - worktree 目录路径
|
|
282
|
+
* @param {string} [cwd] - 执行 git diff HEAD...branch 的工作目录
|
|
283
|
+
* @returns {{ insertions: number; deletions: number }} 聚合后的新增和删除行数
|
|
284
|
+
*/
|
|
285
|
+
export function getDiffStat(branchName: string, worktreePath: string, cwd?: string): { insertions: number; deletions: number } {
|
|
286
|
+
// 已提交的变更(当前分支与目标分支的差异)
|
|
287
|
+
const committedOutput = execCommand(`git diff --shortstat HEAD...${branchName}`, { cwd });
|
|
288
|
+
const committed = parseShortStat(committedOutput);
|
|
289
|
+
|
|
290
|
+
// 未提交的变更(在 worktree 内执行)
|
|
291
|
+
const uncommittedOutput = execCommand('git diff --shortstat HEAD', { cwd: worktreePath });
|
|
292
|
+
const uncommitted = parseShortStat(uncommittedOutput);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
insertions: committed.insertions + uncommitted.insertions,
|
|
296
|
+
deletions: committed.deletions + uncommitted.deletions,
|
|
297
|
+
};
|
|
298
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -25,11 +25,13 @@ export {
|
|
|
25
25
|
gitWorktreeList,
|
|
26
26
|
gitWorktreePrune,
|
|
27
27
|
hasLocalCommits,
|
|
28
|
+
getCommitCountAhead,
|
|
29
|
+
getDiffStat,
|
|
28
30
|
} from './git.js';
|
|
29
31
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
30
32
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
31
|
-
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees } from './worktree.js';
|
|
33
|
+
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus } from './worktree.js';
|
|
32
34
|
export { loadConfig, getConfigValue, ensureClawtDirs } from './config.js';
|
|
33
|
-
export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction } from './formatter.js';
|
|
35
|
+
export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction, formatWorktreeStatus } from './formatter.js';
|
|
34
36
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
35
37
|
export { multilineInput } from './prompt.js';
|
package/src/utils/worktree.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { existsSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { WORKTREES_DIR } from '../constants/index.js';
|
|
4
4
|
import { logger } from '../logger/index.js';
|
|
5
|
-
import { createWorktree as gitCreateWorktree, getProjectName, gitWorktreeList, removeWorktreeByPath, deleteBranch, gitWorktreePrune } from './git.js';
|
|
5
|
+
import { createWorktree as gitCreateWorktree, getProjectName, gitWorktreeList, removeWorktreeByPath, deleteBranch, gitWorktreePrune, getCommitCountAhead, getDiffStat, isWorkingDirClean } from './git.js';
|
|
6
6
|
import { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
7
7
|
import { ensureDir, removeEmptyDir } from './fs.js';
|
|
8
|
-
import type { WorktreeInfo } from '../types/index.js';
|
|
8
|
+
import type { WorktreeInfo, WorktreeStatus } from '../types/index.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 获取当前项目的 worktree 存放目录
|
|
@@ -105,3 +105,22 @@ export function cleanupWorktrees(worktrees: WorktreeInfo[]): void {
|
|
|
105
105
|
const projectDir = getProjectWorktreeDir();
|
|
106
106
|
removeEmptyDir(projectDir);
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 获取 worktree 的变更统计信息
|
|
111
|
+
* 聚合提交数、变更行数、未提交修改状态
|
|
112
|
+
* @param {WorktreeInfo} worktree - worktree 信息
|
|
113
|
+
* @returns {WorktreeStatus | null} 变更统计信息,获取失败时返回 null
|
|
114
|
+
*/
|
|
115
|
+
export function getWorktreeStatus(worktree: WorktreeInfo): WorktreeStatus | null {
|
|
116
|
+
try {
|
|
117
|
+
const commitCount = getCommitCountAhead(worktree.branch);
|
|
118
|
+
const { insertions, deletions } = getDiffStat(worktree.branch, worktree.path);
|
|
119
|
+
const hasDirtyFiles = !isWorkingDirClean(worktree.path);
|
|
120
|
+
|
|
121
|
+
return { commitCount, insertions, deletions, hasDirtyFiles };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error(`获取 worktree 状态失败: ${worktree.path} - ${error}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|