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/README.md +41 -1
- package/dist/index.js +179 -32
- package/dist/postinstall.js +25 -3
- package/docs/spec.md +110 -27
- package/package.json +1 -1
- package/src/commands/status.ts +68 -39
- package/src/commands/validate.ts +73 -6
- package/src/constants/messages/status.ts +10 -2
- package/src/constants/messages/validate.ts +21 -0
- package/src/types/index.ts +1 -1
- package/src/types/status.ts +14 -4
- package/src/utils/formatter.ts +42 -0
- package/src/utils/git.ts +20 -0
- package/src/utils/index.ts +5 -3
- package/src/utils/shell.ts +65 -0
- package/src/utils/validate-snapshot.ts +14 -1
- package/tests/unit/commands/status.test.ts +128 -9
- package/tests/unit/commands/validate.test.ts +103 -0
- package/tests/unit/utils/formatter.test.ts +42 -1
- package/tests/unit/utils/git.test.ts +36 -0
- package/tests/unit/utils/shell.test.ts +42 -1
- package/tests/unit/utils/validate-snapshot.test.ts +21 -1
package/package.json
CHANGED
package/src/commands/status.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
166
|
-
*
|
|
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 {
|
|
199
|
+
* @returns {SnapshotSummary} 快照摘要
|
|
170
200
|
*/
|
|
171
|
-
function collectSnapshots(projectName: string, worktrees: WorktreeInfo[]):
|
|
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
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
292
|
+
printInfo(` ${chalk.green(`+${wt.insertions}`)} ${chalk.red(`-${wt.deletions}`)}`);
|
|
265
293
|
}
|
|
266
294
|
|
|
267
295
|
// 本地提交数
|
|
268
296
|
if (wt.commitsAhead > 0) {
|
|
269
|
-
|
|
297
|
+
printInfo(` ${chalk.yellow(`${wt.commitsAhead} 个本地提交`)}`);
|
|
270
298
|
}
|
|
271
299
|
|
|
272
300
|
// 与主分支的同步状态
|
|
273
301
|
if (wt.commitsBehind > 0) {
|
|
274
|
-
|
|
302
|
+
printInfo(` ${chalk.yellow(`落后主分支 ${wt.commitsBehind} 个提交`)}`);
|
|
275
303
|
} else {
|
|
276
|
-
|
|
304
|
+
printInfo(` ${chalk.green('与主分支同步')}`);
|
|
277
305
|
}
|
|
278
306
|
|
|
279
|
-
|
|
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.
|
|
283
|
-
|
|
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 {
|
|
347
|
+
* 输出快照摘要区块
|
|
348
|
+
* @param {SnapshotSummary} snapshots - 快照摘要信息
|
|
310
349
|
*/
|
|
311
|
-
function printSnapshotsSection(snapshots:
|
|
312
|
-
printInfo(` ${chalk.bold('◆')} ${chalk.bold(MESSAGES.STATUS_SNAPSHOTS_SECTION)} (${snapshots.
|
|
313
|
-
|
|
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
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -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
|
-
*
|
|
309
|
-
* 命令执行失败不影响 validate 本身的结果,仅输出提示
|
|
310
|
+
* 执行单个命令(同步方式,保持原有行为不变)
|
|
310
311
|
* @param {string} command - 要执行的命令字符串
|
|
311
312
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
312
313
|
*/
|
|
313
|
-
function
|
|
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: '
|
|
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:
|
|
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;
|
package/src/types/index.ts
CHANGED
|
@@ -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';
|
package/src/types/status.ts
CHANGED
|
@@ -10,12 +10,14 @@ export interface WorktreeDetailedStatus {
|
|
|
10
10
|
commitsAhead: number;
|
|
11
11
|
/** 落后于主分支的提交数 */
|
|
12
12
|
commitsBehind: number;
|
|
13
|
-
/**
|
|
14
|
-
|
|
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
|
-
/**
|
|
46
|
-
snapshots:
|
|
55
|
+
/** validate 快照摘要 */
|
|
56
|
+
snapshots: SnapshotSummary;
|
|
47
57
|
/** worktree 总数 */
|
|
48
58
|
totalWorktrees: number;
|
|
49
59
|
}
|
package/src/utils/formatter.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -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';
|
package/src/utils/shell.ts
CHANGED
|
@@ -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 - 项目名
|