clawt 3.5.3 → 3.6.1

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.
@@ -1,5 +1,6 @@
1
1
  import type { Command } from 'commander';
2
2
  import { logger } from '../logger/index.js';
3
+ import { ClawtError } from '../errors/index.js';
3
4
  import { MESSAGES } from '../constants/index.js';
4
5
  import type { ResumeOptions } from '../types/index.js';
5
6
  import type { WorktreeInfo } from '../types/index.js';
@@ -12,10 +13,14 @@ import {
12
13
  hasClaudeSessionHistory,
13
14
  resolveTargetWorktrees,
14
15
  promptGroupedMultiSelectBranches,
16
+ findExactMatch,
15
17
  printInfo,
16
18
  printSuccess,
17
19
  confirmAction,
18
20
  getConfigValue,
21
+ parseConcurrency,
22
+ loadTaskFile,
23
+ executeBatchTasks,
19
24
  } from '../utils/index.js';
20
25
  import type { WorktreeMultiResolveMessages } from '../utils/index.js';
21
26
 
@@ -36,6 +41,9 @@ export function registerResumeCommand(program: Command): void {
36
41
  .command('resume')
37
42
  .description('在已有 worktree 中恢复 Claude Code 会话(支持多选批量恢复)')
38
43
  .option('-b, --branch <branchName>', '要恢复的分支名(支持模糊匹配,不传则列出所有分支)')
44
+ .option('--prompt <content>', '非交互式追问(需配合 -b 指定分支)')
45
+ .option('-f, --file <path>', '从任务文件批量追问(通过 branch 名匹配已有 worktree)')
46
+ .option('-c, --concurrency <n>', '批量追问最大并发数,0 表示不限制')
39
47
  .action(async (options: ResumeOptions) => {
40
48
  await handleResume(options);
41
49
  });
@@ -49,6 +57,25 @@ export function registerResumeCommand(program: Command): void {
49
57
  async function handleResume(options: ResumeOptions): Promise<void> {
50
58
  await runPreChecks(PRE_CHECK_RESUME);
51
59
 
60
+ // 非交互式追问逻辑分支
61
+ if (options.prompt && options.file) {
62
+ throw new ClawtError(MESSAGES.RESUME_PROMPT_FILE_CONFLICT);
63
+ }
64
+
65
+ if (options.prompt) {
66
+ if (!options.branch) {
67
+ throw new ClawtError(MESSAGES.RESUME_PROMPT_REQUIRES_BRANCH);
68
+ }
69
+ const worktrees = getProjectWorktrees();
70
+ const worktree = resolveWorktreeByBranch(options.branch, worktrees);
71
+ return handleNonInteractiveSingleResume(worktree, options.prompt);
72
+ }
73
+
74
+ if (options.file) {
75
+ return handleNonInteractiveBatchResume(options.file, options);
76
+ }
77
+
78
+ // 原有交互式逻辑
52
79
  logger.info(`resume 命令执行,分支过滤: ${options.branch ?? '(无)'}`);
53
80
  const worktrees = getProjectWorktrees();
54
81
 
@@ -82,6 +109,70 @@ async function handleResume(options: ResumeOptions): Promise<void> {
82
109
  }
83
110
  }
84
111
 
112
+ /**
113
+ * 精确匹配分支对应的 worktree
114
+ * 找不到时抛出包含可用分支列表的错误
115
+ * @param {string} branch - 目标分支名
116
+ * @param {WorktreeInfo[]} worktrees - 可用的 worktree 列表
117
+ * @returns {WorktreeInfo} 匹配的 worktree
118
+ * @throws {ClawtError} 未找到匹配分支时抛出
119
+ */
120
+ function resolveWorktreeByBranch(branch: string, worktrees: WorktreeInfo[]): WorktreeInfo {
121
+ const match = findExactMatch(worktrees, branch);
122
+ if (!match) {
123
+ const available = worktrees.map((wt) => wt.branch);
124
+ throw new ClawtError(MESSAGES.RESUME_WORKTREE_NOT_FOUND(branch, available));
125
+ }
126
+ return match;
127
+ }
128
+
129
+ /**
130
+ * 非交互式单分支追问
131
+ * 检查 worktree 是否有历史会话,有则通过 --continue 继续最新会话
132
+ * @param {WorktreeInfo} worktree - 目标 worktree
133
+ * @param {string} prompt - 追问内容
134
+ */
135
+ async function handleNonInteractiveSingleResume(worktree: WorktreeInfo, prompt: string): Promise<void> {
136
+ // 检查是否有历史会话,有则追加 --continue
137
+ const hasPrevious = hasClaudeSessionHistory(worktree.path);
138
+ await executeBatchTasks([worktree], [prompt], 0, [hasPrevious]);
139
+ }
140
+
141
+ /**
142
+ * 非交互式批量追问
143
+ * 从任务文件解析追问内容,按 branch 名精确匹配已有 worktree,批量执行
144
+ * @param {string} filePath - 追问任务文件路径
145
+ * @param {ResumeOptions} options - 命令选项(含并发数配置)
146
+ */
147
+ async function handleNonInteractiveBatchResume(filePath: string, options: ResumeOptions): Promise<void> {
148
+ // 解析追问文件,branch 为必填字段
149
+ const entries = loadTaskFile(filePath, { branchRequired: true });
150
+ printSuccess(MESSAGES.RESUME_FOLLOW_UP_FILE_LOADED(entries.length, filePath));
151
+
152
+ // 获取所有已有 worktree
153
+ const allWorktrees = getProjectWorktrees();
154
+
155
+ // 按 branch 名精确匹配 worktree,构建执行参数
156
+ const worktrees: WorktreeInfo[] = [];
157
+ const tasks: string[] = [];
158
+ const continueFlags: boolean[] = [];
159
+
160
+ for (const entry of entries) {
161
+ const worktree = resolveWorktreeByBranch(entry.branch!, allWorktrees);
162
+ worktrees.push(worktree);
163
+ tasks.push(entry.task);
164
+ // 按 worktree 独立检查是否有历史会话
165
+ continueFlags.push(hasClaudeSessionHistory(worktree.path));
166
+ }
167
+
168
+ // 解析并发数
169
+ const concurrency = parseConcurrency(options.concurrency, getConfigValue('maxConcurrency'));
170
+
171
+ logger.info(`resume 命令(批量追问模式)执行,任务数: ${entries.length},并发数: ${concurrency || '不限制'}`);
172
+
173
+ await executeBatchTasks(worktrees, tasks, concurrency, continueFlags);
174
+ }
175
+
85
176
  /**
86
177
  * 输出即将恢复的分支列表(含会话状态:继续/新对话)
87
178
  * @param {WorktreeInfo[]} worktrees - 待恢复的 worktree 列表
@@ -4,4 +4,6 @@ export const HOME_MESSAGES = {
4
4
  HOME_ALREADY_ON_MAIN: (branch: string) => `已在主工作分支 ${branch} 上,无需切换`,
5
5
  /** 切换成功 */
6
6
  HOME_SWITCH_SUCCESS: (from: string, to: string) => `✓ 已从 ${from} 切换到主工作分支 ${to}`,
7
+ /** 当前在子 worktree,提示用户手动 cd 到主 worktree */
8
+ HOME_NOT_IN_MAIN_WORKTREE: (mainPath: string) => `当前不在主 worktree,请先切换到主 worktree:\n\n cd ${mainPath}`,
7
9
  } as const;
@@ -18,4 +18,14 @@ export const RESUME_MESSAGES = {
18
18
  RESUME_ALL_PLATFORM_UNSUPPORTED: '批量 resume 目前仅支持 macOS 平台(通过 AppleScript 打开终端 Tab)',
19
19
  /** 批量 resume 无匹配分支提示 */
20
20
  RESUME_ALL_NO_MATCH: (keyword: string) => `未找到与 "${keyword}" 匹配的分支`,
21
+
22
+ /** --prompt 必须配合 -b 指定目标分支 */
23
+ RESUME_PROMPT_REQUIRES_BRANCH: '--prompt 必须配合 -b 指定目标分支',
24
+ /** --prompt 和 -f 不能同时使用 */
25
+ RESUME_PROMPT_FILE_CONFLICT: '--prompt 和 -f 不能同时使用',
26
+ /** 未找到对应 worktree */
27
+ RESUME_WORKTREE_NOT_FOUND: (branch: string, available: string[]) =>
28
+ `未找到分支 "${branch}" 对应的 worktree\n 可用分支:\n${available.map((b) => ` - ${b}`).join('\n')}`,
29
+ /** 追问文件加载完成 */
30
+ RESUME_FOLLOW_UP_FILE_LOADED: (count: number, path: string) => `从 ${path} 加载了 ${count} 个追问任务`,
21
31
  } as const;
@@ -52,6 +52,12 @@ export interface RemoveOptions {
52
52
  export interface ResumeOptions {
53
53
  /** 要恢复的分支名(可选,不传则列出所有分支供选择) */
54
54
  branch?: string;
55
+ /** 非交互式追问内容(需配合 -b 指定分支) */
56
+ prompt?: string;
57
+ /** 从任务文件批量追问(通过 branch 名匹配已有 worktree) */
58
+ file?: string;
59
+ /** 批量追问最大并发数,0 表示不限制(Commander 传入为字符串) */
60
+ concurrency?: string;
55
61
  }
56
62
 
57
63
  /** sync 命令选项 */
@@ -88,6 +94,12 @@ export interface InitOptions {
88
94
  branch?: string;
89
95
  }
90
96
 
97
+ /** init show 子命令选项 */
98
+ export interface InitShowOptions {
99
+ /** 以 JSON 格式输出 */
100
+ json?: boolean;
101
+ }
102
+
91
103
  /** tasks init 命令选项 */
92
104
  export interface TasksInitOptions {
93
105
  /** 输出文件路径(可选,默认 tasks.md) */
@@ -1,5 +1,5 @@
1
1
  export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
2
- export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions, InitOptions, TasksInitOptions } from './command.js';
2
+ export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions, ListOptions, StatusOptions, ProjectsOptions, InitOptions, InitShowOptions, TasksInitOptions } from './command.js';
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';
@@ -12,6 +12,34 @@ export function getGitCommonDir(cwd?: string): string {
12
12
  return execCommand('git rev-parse --git-common-dir', { cwd });
13
13
  }
14
14
 
15
+ /**
16
+ * 判断当前是否在主 worktree 中
17
+ * 条件:git rev-parse --git-common-dir === ".git"
18
+ * @param {string} [cwd] - 工作目录
19
+ * @returns {boolean} 是否在主 worktree 中
20
+ */
21
+ export function isMainWorktree(cwd?: string): boolean {
22
+ try {
23
+ return getGitCommonDir(cwd) === '.git';
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * 判断当前是否在 git 仓库中
31
+ * @param {string} [cwd] - 工作目录
32
+ * @returns {boolean} 是否在 git 仓库中
33
+ */
34
+ export function isInsideGitRepo(cwd?: string): boolean {
35
+ try {
36
+ execCommand('git rev-parse --is-inside-work-tree', { cwd });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
42
+
15
43
  /**
16
44
  * 获取 git 仓库根目录的绝对路径
17
45
  * @param {string} cwd - 工作目录
@@ -21,6 +49,18 @@ export function getGitTopLevel(cwd?: string): string {
21
49
  return execCommand('git rev-parse --show-toplevel', { cwd });
22
50
  }
23
51
 
52
+ /**
53
+ * 获取主 worktree 的绝对路径
54
+ * 通过 git worktree list --porcelain 解析第一行(始终为主 worktree)
55
+ * @param {string} [cwd] - 工作目录
56
+ * @returns {string} 主 worktree 的绝对路径
57
+ */
58
+ export function getMainWorktreePath(cwd?: string): string {
59
+ const output = execCommand('git worktree list --porcelain', { cwd });
60
+ const firstLine = output.split('\n')[0] || '';
61
+ return firstLine.replace('worktree ', '');
62
+ }
63
+
24
64
  /**
25
65
  * 获取项目名(仓库根目录名称)
26
66
  * @param {string} cwd - 工作目录
@@ -3,7 +3,10 @@ export type { ParallelCommandResult, CommandResultWithStderr, ParallelCommandRes
3
3
  export { copyToClipboard } from './clipboard.js';
4
4
  export {
5
5
  getGitCommonDir,
6
+ isMainWorktree,
7
+ isInsideGitRepo,
6
8
  getGitTopLevel,
9
+ getMainWorktreePath,
7
10
  getProjectName,
8
11
  checkBranchExists,
9
12
  createWorktree,
@@ -9,6 +9,7 @@ import { printSuccess, printWarning, printInfo, printDoubleSeparator, confirmAct
9
9
  import { ProgressRenderer } from './progress.js';
10
10
  import { createLineBuffer, parseStreamLine, parseStreamEvent, truncateText } from './stream-parser.js';
11
11
  import { RESULT_PREVIEW_MAX_LENGTH } from '../constants/index.js';
12
+ import { ClawtError } from '../errors/index.js';
12
13
 
13
14
  /** executeClaudeTask 的返回结构,包含子进程引用和结果 Promise */
14
15
  interface ClaudeTaskHandle {
@@ -29,13 +30,21 @@ type ActivityCallback = (activityText: string) => void;
29
30
  * @param {WorktreeInfo} worktree - worktree 信息
30
31
  * @param {string} task - 任务描述
31
32
  * @param {ActivityCallback} [onActivity] - 活动更新回调(可选)
33
+ * @param {boolean} [continueSession] - 是否继续该 worktree 目录下最新的会话
32
34
  * @returns {ClaudeTaskHandle} 包含子进程引用和结果 Promise
33
35
  */
34
- function executeClaudeTask(worktree: WorktreeInfo, task: string, onActivity?: ActivityCallback): ClaudeTaskHandle {
36
+ function executeClaudeTask(worktree: WorktreeInfo, task: string, onActivity?: ActivityCallback, continueSession?: boolean): ClaudeTaskHandle {
35
37
  // 旧版使用 --output-format json,现改为 stream-json --verbose 以支持实时活动信息
38
+ const args = ['-p', task, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions'];
39
+
40
+ // 追问模式:追加 --continue 继续该目录下最新会话
41
+ if (continueSession) {
42
+ args.push('--continue');
43
+ }
44
+
36
45
  const child = spawnProcess(
37
46
  'claude',
38
- ['-p', task, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'bypassPermissions'],
47
+ args,
39
48
  {
40
49
  cwd: worktree.path,
41
50
  // stdin 必须设置为 'ignore',不能用 'pipe'
@@ -198,6 +207,7 @@ function updateRendererStatus(renderer: ProgressRenderer, index: number, result:
198
207
  * @param {number} startTime - 任务批次启动时间戳
199
208
  * @param {() => boolean} isInterrupted - 检查是否已中断的函数
200
209
  * @param {ChildProcess[]} childProcesses - 共享子进程数组,执行过程中动态追加
210
+ * @param {boolean[]} [continueFlags] - 按索引对应每个任务是否继续已有会话(追问模式,追加 --continue)
201
211
  * @returns {Promise<TaskResult[]>} 所有任务结果
202
212
  */
203
213
  async function executeWithConcurrency(
@@ -208,6 +218,7 @@ async function executeWithConcurrency(
208
218
  startTime: number,
209
219
  isInterrupted: () => boolean,
210
220
  childProcesses: ChildProcess[],
221
+ continueFlags?: boolean[],
211
222
  ): Promise<TaskResult[]> {
212
223
  const total = tasks.length;
213
224
  const results: TaskResult[] = new Array(total);
@@ -234,7 +245,7 @@ async function executeWithConcurrency(
234
245
 
235
246
  const handle = executeClaudeTask(wt, task, (activityText) => {
236
247
  renderer.updateActivityText(index, activityText);
237
- });
248
+ }, continueFlags?.[index]);
238
249
  childProcesses.push(handle.child);
239
250
 
240
251
  handle.promise.then((result) => {
@@ -272,6 +283,7 @@ async function executeWithConcurrency(
272
283
  * @param {number} startTime - 任务批次启动时间戳
273
284
  * @param {() => boolean} isInterrupted - 检查是否已中断的函数
274
285
  * @param {ChildProcess[]} childProcesses - 共享子进程数组,启动时追加
286
+ * @param {boolean[]} [continueFlags] - 按索引对应每个任务是否继续已有会话(追问模式,追加 --continue)
275
287
  * @returns {Promise<TaskResult[]>} 所有任务结果
276
288
  */
277
289
  async function executeAllParallel(
@@ -281,13 +293,14 @@ async function executeAllParallel(
281
293
  startTime: number,
282
294
  isInterrupted: () => boolean,
283
295
  childProcesses: ChildProcess[],
296
+ continueFlags?: boolean[],
284
297
  ): Promise<TaskResult[]> {
285
298
  const handles = worktrees.map((wt, index) => {
286
299
  const task = tasks[index];
287
300
  logger.info(`启动任务 ${index + 1}: ${task} (worktree: ${wt.path})`);
288
301
  const handle = executeClaudeTask(wt, task, (activityText) => {
289
302
  renderer.updateActivityText(index, activityText);
290
- });
303
+ }, continueFlags?.[index]);
291
304
  childProcesses.push(handle.child);
292
305
 
293
306
  return handle;
@@ -314,14 +327,25 @@ async function executeAllParallel(
314
327
  * @param {WorktreeInfo[]} worktrees - worktree 列表
315
328
  * @param {string[]} tasks - 任务描述列表
316
329
  * @param {number} concurrency - 最大并发数,0 表示不限制
330
+ * @param {boolean[]} [continueFlags] - 按索引对应每个任务是否继续已有会话(追问模式,追加 --continue)
331
+ * @returns {Promise<TaskResult[]>} 所有任务的执行结果
332
+ * @throws {ClawtError} continueFlags 长度与任务数不一致时抛出
317
333
  */
318
334
  export async function executeBatchTasks(
319
335
  worktrees: WorktreeInfo[],
320
336
  tasks: string[],
321
337
  concurrency: number,
322
- ): Promise<void> {
338
+ continueFlags?: boolean[],
339
+ ): Promise<TaskResult[]> {
323
340
  const count = tasks.length;
324
341
 
342
+ // 校验 continueFlags 长度与 worktrees 一致,防止调用方传入不匹配的数组
343
+ if (continueFlags && continueFlags.length !== count) {
344
+ throw new ClawtError(
345
+ `continueFlags 长度 (${continueFlags.length}) 与任务数 (${count}) 不一致`,
346
+ );
347
+ }
348
+
325
349
  // 有并发限制时输出提示
326
350
  if (concurrency > 0) {
327
351
  printInfo(MESSAGES.CONCURRENCY_INFO(concurrency, count));
@@ -374,15 +398,15 @@ export async function executeBatchTasks(
374
398
 
375
399
  // 根据并发限制选择执行模式
376
400
  const results = concurrency > 0
377
- ? await executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses)
378
- : await executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses);
401
+ ? await executeWithConcurrency(worktrees, tasks, concurrency, renderer, startTime, isInterrupted, childProcesses, continueFlags)
402
+ : await executeAllParallel(worktrees, tasks, renderer, startTime, isInterrupted, childProcesses, continueFlags);
379
403
 
380
404
  // 正常完成,停止进度面板并移除 SIGINT 监听器
381
405
  renderer.stop();
382
406
  process.removeListener('SIGINT', sigintHandler);
383
407
 
384
408
  // 被中断时不输出汇总(已在 sigintHandler 中处理退出)
385
- if (interrupted) return;
409
+ if (interrupted) return [];
386
410
 
387
411
  const totalDurationMs = Date.now() - startTime;
388
412
 
@@ -396,4 +420,6 @@ export async function executeBatchTasks(
396
420
  };
397
421
 
398
422
  printTaskSummary(summary);
423
+
424
+ return results;
399
425
  }
@@ -26,7 +26,7 @@ export interface PreCheckOptions {
26
26
  /**
27
27
  * 校验当前目录是否为主 worktree 的根目录
28
28
  * 条件:git rev-parse --git-common-dir === ".git"
29
- * @throws {ClawtError} 不在主 worktree 根目录时抛出
29
+ * @throws {ClawtError} 不在主 worktree 根目录时抛出(包括不在 git 仓库中的情况)
30
30
  */
31
31
  export function validateMainWorktree(): void {
32
32
  try {