clawt 2.16.1 → 2.16.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "2.16.1",
3
+ "version": "2.16.3",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -2,7 +2,7 @@ import type { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { MESSAGES } from '../constants/index.js';
4
4
  import { logger } from '../logger/index.js';
5
- import type { StatusOptions, WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, StatusResult, WorktreeInfo } from '../types/index.js';
5
+ import type { StatusOptions, WorktreeDetailedStatus, MainWorktreeStatus, SnapshotSummary, StatusResult, WorktreeInfo } from '../types/index.js';
6
6
  import {
7
7
  validateMainWorktree,
8
8
  getProjectName,
@@ -14,8 +14,10 @@ import {
14
14
  getDiffStat,
15
15
  hasMergeConflict,
16
16
  hasLocalCommits,
17
- hasSnapshot,
17
+ getSnapshotModifiedTime,
18
18
  getProjectSnapshotBranches,
19
+ getBranchCreatedAt,
20
+ formatRelativeTime,
19
21
  printInfo,
20
22
  printDoubleSeparator,
21
23
  printSeparator,
@@ -96,6 +98,7 @@ function collectWorktreeDetailedStatus(worktree: WorktreeInfo, projectName: stri
96
98
  const changeStatus = detectChangeStatus(worktree);
97
99
  const { commitsAhead, commitsBehind } = countCommitDivergence(worktree.branch);
98
100
  const { insertions, deletions } = countDiffStat(worktree.path);
101
+ const createdAt = resolveBranchCreatedAt(worktree.branch);
99
102
 
100
103
  return {
101
104
  path: worktree.path,
@@ -103,9 +106,10 @@ function collectWorktreeDetailedStatus(worktree: WorktreeInfo, projectName: stri
103
106
  changeStatus,
104
107
  commitsAhead,
105
108
  commitsBehind,
106
- hasSnapshot: hasSnapshot(projectName, worktree.branch),
109
+ snapshotTime: resolveSnapshotTime(projectName, worktree.branch),
107
110
  insertions,
108
111
  deletions,
112
+ createdAt,
109
113
  };
110
114
  }
111
115
 
@@ -162,20 +166,47 @@ function countDiffStat(worktreePath: string): { insertions: number; deletions: n
162
166
  }
163
167
 
164
168
  /**
165
- * 收集未清理的 validate 快照信息
166
- * 对比快照分支与现有 worktree 分支,标识孤立快照
169
+ * 获取分支的创建时间
170
+ * @param {string} branchName - 分支名
171
+ * @returns {string | null} ISO 8601 格式的创建时间,获取失败时返回 null
172
+ */
173
+ function resolveBranchCreatedAt(branchName: string): string | null {
174
+ try {
175
+ return getBranchCreatedAt(branchName);
176
+ } catch {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ /**
182
+ * 获取分支的 validate 快照修改时间
183
+ * @param {string} projectName - 项目名
184
+ * @param {string} branchName - 分支名
185
+ * @returns {string | null} ISO 8601 格式的快照时间,无快照时返回 null
186
+ */
187
+ function resolveSnapshotTime(projectName: string, branchName: string): string | null {
188
+ try {
189
+ return getSnapshotModifiedTime(projectName, branchName);
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * 收集 validate 快照摘要信息
167
197
  * @param {string} projectName - 项目名
168
198
  * @param {WorktreeInfo[]} worktrees - 当前有效的 worktree 列表
169
- * @returns {SnapshotInfo[]} 快照信息列表
199
+ * @returns {SnapshotSummary} 快照摘要
170
200
  */
171
- function collectSnapshots(projectName: string, worktrees: WorktreeInfo[]): SnapshotInfo[] {
201
+ function collectSnapshots(projectName: string, worktrees: WorktreeInfo[]): SnapshotSummary {
172
202
  const snapshotBranches = getProjectSnapshotBranches(projectName);
173
203
  const worktreeBranchSet = new Set(worktrees.map((wt) => wt.branch));
204
+ const orphaned = snapshotBranches.filter((branch) => !worktreeBranchSet.has(branch)).length;
174
205
 
175
- return snapshotBranches.map((branch) => ({
176
- branch,
177
- worktreeExists: worktreeBranchSet.has(branch),
178
- }));
206
+ return {
207
+ total: snapshotBranches.length,
208
+ orphaned,
209
+ };
179
210
  }
180
211
 
181
212
  /**
@@ -256,31 +287,39 @@ function printWorktreeItem(wt: WorktreeDetailedStatus): void {
256
287
  const statusLabel = formatChangeStatusLabel(wt.changeStatus);
257
288
  printInfo(` ${chalk.bold('●')} ${chalk.bold(wt.branch)} [${statusLabel}]`);
258
289
 
259
- // 差异统计行
260
- const parts: string[] = [];
261
-
262
- // 行数变更(仅在有变更时展示)
290
+ // 变更行数
263
291
  if (wt.insertions > 0 || wt.deletions > 0) {
264
- parts.push(`${chalk.green(`+${wt.insertions}`)} ${chalk.red(`-${wt.deletions}`)}`);
292
+ printInfo(` ${chalk.green(`+${wt.insertions}`)} ${chalk.red(`-${wt.deletions}`)}`);
265
293
  }
266
294
 
267
295
  // 本地提交数
268
296
  if (wt.commitsAhead > 0) {
269
- parts.push(chalk.yellow(`${wt.commitsAhead} 个本地提交`));
297
+ printInfo(` ${chalk.yellow(`${wt.commitsAhead} 个本地提交`)}`);
270
298
  }
271
299
 
272
300
  // 与主分支的同步状态
273
301
  if (wt.commitsBehind > 0) {
274
- parts.push(chalk.yellow(`落后主分支 ${wt.commitsBehind} 个提交`));
302
+ printInfo(` ${chalk.yellow(`落后主分支 ${wt.commitsBehind} 个提交`)}`);
275
303
  } else {
276
- parts.push(chalk.green('与主分支同步'));
304
+ printInfo(` ${chalk.green('与主分支同步')}`);
277
305
  }
278
306
 
279
- printInfo(` ${parts.join(' ')}`);
307
+ // 分支创建时间
308
+ if (wt.createdAt) {
309
+ const relativeTime = formatRelativeTime(wt.createdAt);
310
+ if (relativeTime) {
311
+ printInfo(` ${chalk.gray(MESSAGES.STATUS_CREATED_AT(relativeTime))}`);
312
+ }
313
+ }
280
314
 
281
- // 快照状态
282
- if (wt.hasSnapshot) {
283
- printInfo(` ${chalk.blue('有 validate 快照')}`);
315
+ // 验证状态
316
+ if (wt.snapshotTime) {
317
+ const relativeTime = formatRelativeTime(wt.snapshotTime);
318
+ if (relativeTime) {
319
+ printInfo(` ${chalk.green(MESSAGES.STATUS_LAST_VALIDATED(relativeTime))}`);
320
+ }
321
+ } else {
322
+ printInfo(` ${chalk.red(MESSAGES.STATUS_NOT_VALIDATED)}`);
284
323
  }
285
324
 
286
325
  printInfo('');
@@ -305,23 +344,13 @@ function formatChangeStatusLabel(status: WorktreeDetailedStatus['changeStatus'])
305
344
  }
306
345
 
307
346
  /**
308
- * 输出未清理快照区块
309
- * @param {SnapshotInfo[]} snapshots - 快照信息列表
347
+ * 输出快照摘要区块
348
+ * @param {SnapshotSummary} snapshots - 快照摘要信息
310
349
  */
311
- function printSnapshotsSection(snapshots: SnapshotInfo[]): void {
312
- printInfo(` ${chalk.bold('◆')} ${chalk.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.length} 个)`);
313
- printInfo('');
314
-
315
- if (snapshots.length === 0) {
316
- printInfo(` ${MESSAGES.STATUS_NO_SNAPSHOTS}`);
317
- printInfo('');
318
- return;
319
- }
320
-
321
- for (const snap of snapshots) {
322
- const orphanLabel = snap.worktreeExists ? '' : ` ${chalk.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED)}`;
323
- const icon = snap.worktreeExists ? chalk.blue('●') : chalk.yellow('⚠');
324
- printInfo(` ${icon} ${snap.branch}${orphanLabel}`);
350
+ function printSnapshotsSection(snapshots: SnapshotSummary): void {
351
+ printInfo(` ${chalk.bold('◆')} ${chalk.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.total} 个)`);
352
+ if (snapshots.orphaned > 0) {
353
+ printInfo(` ${chalk.yellow(MESSAGES.STATUS_SNAPSHOT_ORPHANED(snapshots.orphaned))}`);
325
354
  }
326
355
  printInfo('');
327
356
  }
@@ -40,8 +40,10 @@ import {
40
40
  printSeparator,
41
41
  resolveTargetWorktree,
42
42
  runCommandInherited,
43
+ parseParallelCommands,
44
+ runParallelCommands,
43
45
  } from '../utils/index.js';
44
- import type { WorktreeResolveMessages } from '../utils/index.js';
46
+ import type { WorktreeResolveMessages, ParallelCommandResult } from '../utils/index.js';
45
47
 
46
48
  /** validate 命令的分支解析消息配置 */
47
49
  const VALIDATE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
@@ -305,13 +307,11 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
305
307
  }
306
308
 
307
309
  /**
308
- * 在主 worktree 中执行用户指定的命令
309
- * 命令执行失败不影响 validate 本身的结果,仅输出提示
310
+ * 执行单个命令(同步方式,保持原有行为不变)
310
311
  * @param {string} command - 要执行的命令字符串
311
312
  * @param {string} mainWorktreePath - 主 worktree 路径
312
313
  */
313
- function executeRunCommand(command: string, mainWorktreePath: string): void {
314
- printInfo('');
314
+ function executeSingleCommand(command: string, mainWorktreePath: string): void {
315
315
  printInfo(MESSAGES.VALIDATE_RUN_START(command));
316
316
  printSeparator();
317
317
 
@@ -333,6 +333,73 @@ function executeRunCommand(command: string, mainWorktreePath: string): void {
333
333
  }
334
334
  }
335
335
 
336
+ /**
337
+ * 汇总输出并行命令的执行结果
338
+ * @param {ParallelCommandResult[]} results - 各命令的执行结果数组
339
+ */
340
+ function reportParallelResults(results: ParallelCommandResult[]): void {
341
+ printSeparator();
342
+
343
+ const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
344
+ const failedCount = results.length - successCount;
345
+
346
+ for (const result of results) {
347
+ if (result.error) {
348
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
349
+ } else if (result.exitCode === 0) {
350
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
351
+ } else {
352
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
353
+ }
354
+ }
355
+
356
+ if (failedCount === 0) {
357
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
358
+ } else {
359
+ printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
360
+ }
361
+ }
362
+
363
+ /**
364
+ * 并行执行多个命令并汇总结果
365
+ * @param {string[]} commands - 要并行执行的命令数组
366
+ * @param {string} mainWorktreePath - 主 worktree 路径
367
+ */
368
+ async function executeParallelCommands(commands: string[], mainWorktreePath: string): Promise<void> {
369
+ printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
370
+
371
+ for (let i = 0; i < commands.length; i++) {
372
+ printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
373
+ }
374
+
375
+ printSeparator();
376
+
377
+ const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
378
+
379
+ reportParallelResults(results);
380
+ }
381
+
382
+ /**
383
+ * 在主 worktree 中执行用户指定的命令
384
+ * 根据命令字符串中的 & 分隔符决定是单命令执行还是并行执行
385
+ * 命令执行失败不影响 validate 本身的结果,仅输出提示
386
+ * @param {string} command - 要执行的命令字符串
387
+ * @param {string} mainWorktreePath - 主 worktree 路径
388
+ */
389
+ async function executeRunCommand(command: string, mainWorktreePath: string): Promise<void> {
390
+ printInfo('');
391
+
392
+ const commands = parseParallelCommands(command);
393
+
394
+ if (commands.length <= 1) {
395
+ // 单命令(包括含 && 的串行命令),走原有同步路径
396
+ executeSingleCommand(commands[0] || command, mainWorktreePath);
397
+ } else {
398
+ // 多命令,并行执行
399
+ await executeParallelCommands(commands, mainWorktreePath);
400
+ }
401
+ }
402
+
336
403
  /**
337
404
  * 执行 validate 命令的核心逻辑
338
405
  * @param {ValidateOptions} options - 命令选项
@@ -386,6 +453,6 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
386
453
 
387
454
  // validate 成功后执行用户指定的命令
388
455
  if (options.run) {
389
- executeRunCommand(options.run, mainWorktreePath);
456
+ await executeRunCommand(options.run, mainWorktreePath);
390
457
  }
391
458
  }
@@ -7,7 +7,7 @@ export const STATUS_MESSAGES = {
7
7
  /** status worktrees 区块标题 */
8
8
  STATUS_WORKTREES_SECTION: 'Worktree 列表',
9
9
  /** status 快照区块标题 */
10
- STATUS_SNAPSHOTS_SECTION: '未清理的 Validate 快照',
10
+ STATUS_SNAPSHOTS_SECTION: 'Validate 快照',
11
11
  /** status 无 worktree */
12
12
  STATUS_NO_WORKTREES: '(无活跃 worktree)',
13
13
  /** status 无未清理快照 */
@@ -21,5 +21,13 @@ export const STATUS_MESSAGES = {
21
21
  /** status 变更状态:无变更 */
22
22
  STATUS_CHANGE_CLEAN: '无变更',
23
23
  /** status 快照对应 worktree 已不存在 */
24
- STATUS_SNAPSHOT_ORPHANED: '(对应 worktree 已不存在)',
24
+ STATUS_SNAPSHOT_ORPHANED: (count: number) => `其中 ${count} 个快照对应的 worktree 已不存在`,
25
+ /** status 分支创建时间标签 */
26
+ STATUS_CREATED_AT: (relativeTime: string) => `创建于 ${relativeTime}`,
27
+ /** status 分支无分叉提交时的提示 */
28
+ STATUS_NO_DIVERGED_COMMITS: '尚无分叉提交',
29
+ /** status 上次验证时间标签 */
30
+ STATUS_LAST_VALIDATED: (relativeTime: string) => `上次验证: ${relativeTime}`,
31
+ /** status 未验证警示 */
32
+ STATUS_NOT_VALIDATED: '✗ 未验证',
25
33
  } as const;
@@ -32,4 +32,25 @@ export const VALIDATE_MESSAGES = {
32
32
  /** --run 命令执行异常(进程启动失败等) */
33
33
  VALIDATE_RUN_ERROR: (command: string, errorMessage: string) =>
34
34
  `✗ 命令执行出错: ${errorMessage}`,
35
+ /** 并行命令开始执行提示 */
36
+ VALIDATE_PARALLEL_RUN_START: (count: number) =>
37
+ `正在并行执行 ${count} 个命令...`,
38
+ /** 并行执行中单个命令开始提示(带序号) */
39
+ VALIDATE_PARALLEL_CMD_START: (index: number, total: number, command: string) =>
40
+ `[${index}/${total}] ${command}`,
41
+ /** 并行执行全部成功汇总提示 */
42
+ VALIDATE_PARALLEL_RUN_ALL_SUCCESS: (count: number) =>
43
+ `✓ 全部 ${count} 个命令执行成功`,
44
+ /** 并行执行部分失败汇总提示 */
45
+ VALIDATE_PARALLEL_RUN_SUMMARY: (successCount: number, failedCount: number) =>
46
+ `共 ${successCount + failedCount} 个命令,${successCount} 个成功,${failedCount} 个失败`,
47
+ /** 并行执行中单个命令成功 */
48
+ VALIDATE_PARALLEL_CMD_SUCCESS: (command: string) =>
49
+ ` ✓ ${command}`,
50
+ /** 并行执行中单个命令失败 */
51
+ VALIDATE_PARALLEL_CMD_FAILED: (command: string, exitCode: number) =>
52
+ ` ✗ ${command}(退出码: ${exitCode})`,
53
+ /** 并行执行中单个命令启动失败 */
54
+ VALIDATE_PARALLEL_CMD_ERROR: (command: string, errorMessage: string) =>
55
+ ` ✗ ${command}(错误: ${errorMessage})`,
35
56
  } as const;
@@ -3,5 +3,5 @@ export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOp
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';
6
- export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, StatusResult } from './status.js';
6
+ export type { WorktreeDetailedStatus, MainWorktreeStatus, SnapshotInfo, SnapshotSummary, StatusResult } from './status.js';
7
7
  export type { TaskFileEntry, ParseTaskFileOptions } from './taskFile.js';
@@ -10,12 +10,14 @@ export interface WorktreeDetailedStatus {
10
10
  commitsAhead: number;
11
11
  /** 落后于主分支的提交数 */
12
12
  commitsBehind: number;
13
- /** 是否存在 validate 快照 */
14
- hasSnapshot: boolean;
13
+ /** 上次 validate 验证时间(ISO 8601 时间字符串),无快照时为 null */
14
+ snapshotTime: string | null;
15
15
  /** 工作区和暂存区的新增行数 */
16
16
  insertions: number;
17
17
  /** 工作区和暂存区的删除行数 */
18
18
  deletions: number;
19
+ /** 分支创建时间(首次分叉提交的 ISO 8601 时间字符串),无分叉提交时为 null */
20
+ createdAt: string | null;
19
21
  }
20
22
 
21
23
  /** 主 worktree 状态信息 */
@@ -36,14 +38,22 @@ export interface SnapshotInfo {
36
38
  worktreeExists: boolean;
37
39
  }
38
40
 
41
+ /** validate 快照摘要信息 */
42
+ export interface SnapshotSummary {
43
+ /** 快照总数 */
44
+ total: number;
45
+ /** 孤立快照数(对应 worktree 已不存在) */
46
+ orphaned: number;
47
+ }
48
+
39
49
  /** status 命令的完整输出结构 */
40
50
  export interface StatusResult {
41
51
  /** 主 worktree 状态 */
42
52
  main: MainWorktreeStatus;
43
53
  /** 各 worktree 的详细状态 */
44
54
  worktrees: WorktreeDetailedStatus[];
45
- /** 未清理的 validate 快照列表 */
46
- snapshots: SnapshotInfo[];
55
+ /** validate 快照摘要 */
56
+ snapshots: SnapshotSummary;
47
57
  /** worktree 总数 */
48
58
  totalWorktrees: number;
49
59
  }
@@ -142,3 +142,45 @@ export function formatDuration(ms: number): string {
142
142
  const seconds = Math.floor(totalSeconds % 60);
143
143
  return `${minutes}m${String(seconds).padStart(2, '0')}s`;
144
144
  }
145
+
146
+ /**
147
+ * 将 ISO 8601 日期字符串格式化为中文相对时间描述
148
+ * 例如: "3 天前"、"2 小时前"、"刚刚"
149
+ * @param {string} isoDateString - ISO 8601 格式的日期字符串
150
+ * @returns {string | null} 中文相对时间描述,无效日期时返回 null
151
+ */
152
+ export function formatRelativeTime(isoDateString: string): string | null {
153
+ const date = new Date(isoDateString);
154
+ const now = new Date();
155
+ const diffMs = now.getTime() - date.getTime();
156
+
157
+ // 无效日期返回 null
158
+ if (isNaN(diffMs)) {
159
+ return null;
160
+ }
161
+
162
+ // 未来时间或不到 1 分钟
163
+ if (diffMs < 0 || diffMs < 60 * 1000) {
164
+ return '刚刚';
165
+ }
166
+
167
+ const diffMinutes = Math.floor(diffMs / (1000 * 60));
168
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
169
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
170
+
171
+ if (diffHours < 1) {
172
+ return `${diffMinutes} 分钟前`;
173
+ }
174
+ if (diffDays < 1) {
175
+ return `${diffHours} 小时前`;
176
+ }
177
+ if (diffDays < 30) {
178
+ return `${diffDays} 天前`;
179
+ }
180
+ if (diffDays < 365) {
181
+ const months = Math.floor(diffDays / 30);
182
+ return `${months} 个月前`;
183
+ }
184
+ const years = Math.floor(diffDays / 365);
185
+ return `${years} 年前`;
186
+ }
package/src/utils/git.ts CHANGED
@@ -481,3 +481,23 @@ export function gitApplyCachedCheck(patchContent: Buffer, cwd?: string): boolean
481
481
  return false;
482
482
  }
483
483
  }
484
+
485
+ /**
486
+ * 获取分支的创建时间(通过 reflog 获取分支创建时的时间戳)
487
+ * reflog 的最后一条记录即为分支创建时的记录
488
+ * @param {string} branchName - 目标分支名
489
+ * @param {string} [cwd] - 工作目录
490
+ * @returns {string | null} ISO 8601 格式的时间字符串,无法获取时返回 null
491
+ */
492
+ export function getBranchCreatedAt(branchName: string, cwd?: string): string | null {
493
+ try {
494
+ const output = execCommand(`git reflog show ${branchName} --format=%cI`, { cwd });
495
+ if (!output.trim()) return null;
496
+ // 取最后一行,即分支创建时的 reflog 记录
497
+ const lines = output.trim().split('\n');
498
+ const lastLine = lines[lines.length - 1];
499
+ return lastLine || null;
500
+ } catch {
501
+ return null;
502
+ }
503
+ }
@@ -1,4 +1,5 @@
1
- export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited } from './shell.js';
1
+ export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput, runCommandInherited, parseParallelCommands, runParallelCommands } from './shell.js';
2
+ export type { ParallelCommandResult } from './shell.js';
2
3
  export {
3
4
  getGitCommonDir,
4
5
  getGitTopLevel,
@@ -44,16 +45,17 @@ export {
44
45
  getCommitTreeHash,
45
46
  gitDiffTree,
46
47
  gitApplyCachedCheck,
48
+ getBranchCreatedAt,
47
49
  } from './git.js';
48
50
  export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
49
51
  export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
50
52
  export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus, createWorktreesByBranches } from './worktree.js';
51
53
  export { loadConfig, writeDefaultConfig, writeConfig, saveConfig, getConfigValue, ensureClawtDirs, parseConcurrency } from './config.js';
52
- export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration } from './formatter.js';
54
+ export { printSuccess, printError, printWarning, printInfo, printHint, printSeparator, printDoubleSeparator, confirmAction, confirmDestructiveAction, formatWorktreeStatus, isWorktreeIdle, formatDuration, formatRelativeTime } from './formatter.js';
53
55
  export { ensureDir, removeEmptyDir } from './fs.js';
54
56
  export { multilineInput } from './prompt.js';
55
57
  export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
56
- export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
58
+ export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
57
59
  export { findExactMatch, findFuzzyMatches, promptSelectBranch, promptMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees } from './worktree-matcher.js';
58
60
  export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
59
61
  export { ProgressRenderer } from './progress.js';
@@ -1,6 +1,16 @@
1
1
  import { execSync, execFileSync, spawn, spawnSync, type ChildProcess, type SpawnSyncReturns, type StdioOptions } from 'node:child_process';
2
2
  import { logger } from '../logger/index.js';
3
3
 
4
+ /** 并行命令执行的单个结果 */
5
+ export interface ParallelCommandResult {
6
+ /** 执行的命令字符串 */
7
+ command: string;
8
+ /** 进程退出码 */
9
+ exitCode: number;
10
+ /** 进程启动失败时的错误信息 */
11
+ error?: string;
12
+ }
13
+
4
14
  /**
5
15
  * 同步执行 shell 命令并返回 stdout
6
16
  * @param {string} command - 要执行的命令
@@ -92,3 +102,58 @@ export function runCommandInherited(
92
102
  shell: true,
93
103
  });
94
104
  }
105
+
106
+ /**
107
+ * 解析命令字符串中的并行分隔符 `&`,将其拆分为多个独立命令
108
+ * `&&` 不会被拆分(属于 shell 的串行逻辑与操作符)
109
+ * @param {string} commandString - 命令字符串
110
+ * @returns {string[]} 拆分后的命令数组
111
+ */
112
+ export function parseParallelCommands(commandString: string): string[] {
113
+ // 将 && 临时替换为占位符,避免被 & 分割逻辑误拆
114
+ const placeholder = '\x00AND\x00';
115
+ const escaped = commandString.replace(/&&/g, placeholder);
116
+
117
+ // 按单个 & 分割
118
+ const parts = escaped.split('&');
119
+
120
+ // 还原占位符为 &&,并去除首尾空白
121
+ return parts
122
+ .map((part) => part.replace(new RegExp(placeholder, 'g'), '&&').trim())
123
+ .filter((part) => part.length > 0);
124
+ }
125
+
126
+ /**
127
+ * 并行执行多个命令,等待全部完成后返回结果
128
+ * 每个命令通过 spawn 以 shell 模式启动,stdio 继承父进程(实时输出到终端)
129
+ * @param {string[]} commands - 要并行执行的命令数组
130
+ * @param {object} options - 可选配置
131
+ * @param {string} options.cwd - 工作目录
132
+ * @returns {Promise<ParallelCommandResult[]>} 各命令的执行结果
133
+ */
134
+ export function runParallelCommands(
135
+ commands: string[],
136
+ options?: { cwd?: string },
137
+ ): Promise<ParallelCommandResult[]> {
138
+ const promises = commands.map((command) => {
139
+ return new Promise<ParallelCommandResult>((resolve) => {
140
+ logger.debug(`并行启动命令: ${command}${options?.cwd ? ` (cwd: ${options.cwd})` : ''}`);
141
+
142
+ const child = spawn(command, {
143
+ cwd: options?.cwd,
144
+ stdio: 'inherit',
145
+ shell: true,
146
+ });
147
+
148
+ child.on('error', (err) => {
149
+ resolve({ command, exitCode: 1, error: err.message });
150
+ });
151
+
152
+ child.on('close', (code) => {
153
+ resolve({ command, exitCode: code ?? 1 });
154
+ });
155
+ });
156
+ });
157
+
158
+ return Promise.all(promises);
159
+ }
@@ -1,5 +1,5 @@
1
1
  import { join } from 'node:path';
2
- import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, rmdirSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, rmdirSync, statSync } from 'node:fs';
3
3
  import { VALIDATE_SNAPSHOTS_DIR } from '../constants/index.js';
4
4
  import { ensureDir } from './fs.js';
5
5
  import { logger } from '../logger/index.js';
@@ -34,6 +34,19 @@ export function hasSnapshot(projectName: string, branchName: string): boolean {
34
34
  return existsSync(getSnapshotPath(projectName, branchName));
35
35
  }
36
36
 
37
+ /**
38
+ * 获取指定项目和分支的 validate 快照文件修改时间
39
+ * @param {string} projectName - 项目名
40
+ * @param {string} branchName - 分支名
41
+ * @returns {string | null} ISO 8601 格式的修改时间,快照不存在时返回 null
42
+ */
43
+ export function getSnapshotModifiedTime(projectName: string, branchName: string): string | null {
44
+ const snapshotPath = getSnapshotPath(projectName, branchName);
45
+ if (!existsSync(snapshotPath)) return null;
46
+ const stat = statSync(snapshotPath);
47
+ return stat.mtime.toISOString();
48
+ }
49
+
37
50
  /**
38
51
  * 读取指定项目和分支的 validate 快照中存储的 tree hash
39
52
  * @param {string} projectName - 项目名