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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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(`\n共 ${worktrees.length} 个 worktree`);
56
+ printInfo(`共 ${worktrees.length} 个 worktree`);
44
57
  }
45
58
  }
@@ -62,4 +62,6 @@ export const MESSAGES = {
62
62
  DOUBLE_SEPARATOR: '════════════════════════════════════════',
63
63
  /** 创建数量参数无效 */
64
64
  INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
65
+ /** worktree 状态获取失败 */
66
+ WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
65
67
  } as const;
@@ -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';
@@ -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
+ }
@@ -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
+ }
@@ -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';
@@ -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
+ }