clawt 1.0.4 → 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
@@ -72,10 +72,16 @@ var MESSAGES = {
72
72
  INTERRUPT_CLEANED: (count) => `\u2713 \u5DF2\u6E05\u7406 ${count} \u4E2A worktree \u548C\u5BF9\u5E94\u5206\u652F`,
73
73
  /** 中断后保留 worktree */
74
74
  INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406",
75
+ /** 配置文件损坏,已重新生成默认配置 */
76
+ CONFIG_CORRUPTED: "\u914D\u7F6E\u6587\u4EF6\u635F\u574F\u6216\u65E0\u6CD5\u89E3\u6790\uFF0C\u5DF2\u91CD\u65B0\u751F\u6210\u9ED8\u8BA4\u914D\u7F6E",
75
77
  /** 分隔线 */
76
78
  SEPARATOR: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
77
79
  /** 粗分隔线 */
78
- 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"
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
+ /** 创建数量参数无效 */
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)"
79
85
  };
80
86
 
81
87
  // src/constants/exitCodes.ts
@@ -262,6 +268,33 @@ function hasLocalCommits(branchName, cwd) {
262
268
  return false;
263
269
  }
264
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
+ }
265
298
 
266
299
  // src/utils/formatter.ts
267
300
  import chalk from "chalk";
@@ -296,6 +329,22 @@ function confirmAction(question) {
296
329
  });
297
330
  });
298
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
+ }
299
348
 
300
349
  // src/utils/branch.ts
301
350
  function sanitizeBranchName(branchName) {
@@ -424,15 +473,35 @@ function cleanupWorktrees(worktrees) {
424
473
  const projectDir = getProjectWorktreeDir();
425
474
  removeEmptyDir(projectDir);
426
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
+ }
427
487
 
428
488
  // src/utils/config.ts
429
- import { existsSync as existsSync4, readFileSync } from "fs";
489
+ import { existsSync as existsSync4, readFileSync, writeFileSync } from "fs";
430
490
  function loadConfig() {
431
491
  if (!existsSync4(CONFIG_PATH)) {
432
492
  return { ...DEFAULT_CONFIG };
433
493
  }
434
- const raw = readFileSync(CONFIG_PATH, "utf-8");
435
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
494
+ try {
495
+ const raw = readFileSync(CONFIG_PATH, "utf-8");
496
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
497
+ } catch {
498
+ logger.warn(MESSAGES.CONFIG_CORRUPTED);
499
+ writeDefaultConfig();
500
+ return { ...DEFAULT_CONFIG };
501
+ }
502
+ }
503
+ function writeDefaultConfig() {
504
+ writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
436
505
  }
437
506
  function getConfigValue(key) {
438
507
  const config = loadConfig();
@@ -448,6 +517,7 @@ function ensureClawtDirs() {
448
517
  import Enquirer from "enquirer";
449
518
 
450
519
  // src/commands/list.ts
520
+ import chalk2 from "chalk";
451
521
  function registerListCommand(program2) {
452
522
  program2.command("list").description("\u5217\u51FA\u5F53\u524D\u9879\u76EE\u6240\u6709 worktree").action(() => {
453
523
  handleList();
@@ -465,9 +535,15 @@ function handleList() {
465
535
  } else {
466
536
  for (const wt of worktrees) {
467
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("");
468
545
  }
469
- printInfo(`
470
- \u5171 ${worktrees.length} \u4E2A worktree`);
546
+ printInfo(`\u5171 ${worktrees.length} \u4E2A worktree`);
471
547
  }
472
548
  }
473
549
 
@@ -480,6 +556,12 @@ function registerCreateCommand(program2) {
480
556
  function handleCreate(options) {
481
557
  validateMainWorktree();
482
558
  const count = Number(options.number);
559
+ if (!Number.isInteger(count) || count <= 0) {
560
+ throw new ClawtError(
561
+ MESSAGES.INVALID_COUNT(options.number),
562
+ EXIT_CODES.ARGUMENT_ERROR
563
+ );
564
+ }
483
565
  logger.info(`create \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u6570\u91CF: ${count}`);
484
566
  const worktrees = createWorktrees(options.branch, count);
485
567
  printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "1.0.4",
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,5 +1,6 @@
1
1
  import type { Command } from 'commander';
2
- import { MESSAGES } from '../constants/index.js';
2
+ import { MESSAGES, EXIT_CODES } from '../constants/index.js';
3
+ import { ClawtError } from '../errors/index.js';
3
4
  import { logger } from '../logger/index.js';
4
5
  import type { CreateOptions } from '../types/index.js';
5
6
  import {
@@ -33,6 +34,15 @@ function handleCreate(options: CreateOptions): void {
33
34
  validateMainWorktree();
34
35
 
35
36
  const count = Number(options.number);
37
+
38
+ // 校验创建数量必须为正整数
39
+ if (!Number.isInteger(count) || count <= 0) {
40
+ throw new ClawtError(
41
+ MESSAGES.INVALID_COUNT(options.number),
42
+ EXIT_CODES.ARGUMENT_ERROR,
43
+ );
44
+ }
45
+
36
46
  logger.info(`create 命令执行,分支: ${options.branch},数量: ${count}`);
37
47
 
38
48
  const worktrees = createWorktrees(options.branch, count);
@@ -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
  }
@@ -54,8 +54,14 @@ export const MESSAGES = {
54
54
  INTERRUPT_CLEANED: (count: number) => `✓ 已清理 ${count} 个 worktree 和对应分支`,
55
55
  /** 中断后保留 worktree */
56
56
  INTERRUPT_KEPT: '已保留 worktree,可稍后使用 clawt remove 手动清理',
57
+ /** 配置文件损坏,已重新生成默认配置 */
58
+ CONFIG_CORRUPTED: '配置文件损坏或无法解析,已重新生成默认配置',
57
59
  /** 分隔线 */
58
60
  SEPARATOR: '────────────────────────────────────────',
59
61
  /** 粗分隔线 */
60
62
  DOUBLE_SEPARATOR: '════════════════════════════════════════',
63
+ /** 创建数量参数无效 */
64
+ INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
65
+ /** worktree 状态获取失败 */
66
+ WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
61
67
  } as const;
@@ -2,8 +2,8 @@
2
2
  export interface CreateOptions {
3
3
  /** 分支名 */
4
4
  branch: string;
5
- /** 创建数量,默认 1 */
6
- number: number;
5
+ /** 创建数量(Commander 传入为字符串),默认 '1' */
6
+ number: string;
7
7
  }
8
8
 
9
9
  /** run 命令选项 */
@@ -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,18 +1,34 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG } from '../constants/index.js';
2
+ import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG, MESSAGES } from '../constants/index.js';
3
3
  import { ensureDir } from './fs.js';
4
+ import { logger } from '../logger/index.js';
4
5
  import type { ClawtConfig } from '../types/index.js';
5
6
 
6
7
  /**
7
8
  * 加载全局配置,不存在则返回默认配置
9
+ * 配置文件损坏或无法解析时,视为不存在,重新生成默认配置
8
10
  * @returns {ClawtConfig} 配置对象
9
11
  */
10
12
  export function loadConfig(): ClawtConfig {
11
13
  if (!existsSync(CONFIG_PATH)) {
12
14
  return { ...DEFAULT_CONFIG };
13
15
  }
14
- const raw = readFileSync(CONFIG_PATH, 'utf-8');
15
- return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
16
+ try {
17
+ const raw = readFileSync(CONFIG_PATH, 'utf-8');
18
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
19
+ } catch {
20
+ // 配置文件损坏或无法解析时,重新生成默认配置
21
+ logger.warn(MESSAGES.CONFIG_CORRUPTED);
22
+ writeDefaultConfig();
23
+ return { ...DEFAULT_CONFIG };
24
+ }
25
+ }
26
+
27
+ /**
28
+ * 将默认配置写入配置文件
29
+ */
30
+ function writeDefaultConfig(): void {
31
+ writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
16
32
  }
17
33
 
18
34
  /**
@@ -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
+ }