clawt 3.1.1 → 3.1.2

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 CHANGED
@@ -113,7 +113,7 @@ clawt resume -b <branch> # 指定分支
113
113
  clawt resume # 交互式多选(按创建日期分组)
114
114
  ```
115
115
 
116
- 不传 `-b` 时,分支列表按创建日期分组显示,支持全局全选和按组全选。选 1 个在当前终端恢复,选多个自动在独立终端 Tab 中批量恢复(仅 macOS)。
116
+ 不传 `-b` 时,分支列表按创建日期分组显示,支持全局全选和按组全选。选 1 个默认在新终端 Tab 中恢复(设置 `resumeInPlace: true` 可改为在当前终端就地恢复),选多个自动在独立终端 Tab 中批量恢复(仅 macOS)。
117
117
 
118
118
  如果目标 worktree 存在历史会话,会自动继续上次对话(`--continue`)。
119
119
 
@@ -290,6 +290,7 @@ clawt alias remove l
290
290
  | `confirmDestructiveOps` | `true` | 破坏性操作前确认 |
291
291
  | `maxConcurrency` | `0` | run 命令最大并发数,`0` 为不限制 |
292
292
  | `terminalApp` | `"auto"` | 批量 resume 使用的终端:`auto` / `iterm2` / `terminal` |
293
+ | `resumeInPlace` | `false` | resume 单选时在当前终端就地恢复,`false` 则在新 Tab 中打开 |
293
294
  | `aliases` | `{}` | 命令别名映射(如 `{"l": "list", "r": "run"}`) |
294
295
  | `autoUpdate` | `true` | 自动检查新版本(每 24 小时检查一次 npm registry) |
295
296
 
package/dist/index.js CHANGED
@@ -580,6 +580,10 @@ var CONFIG_DEFINITIONS = {
580
580
  description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09",
581
581
  allowedValues: VALID_TERMINAL_APPS
582
582
  },
583
+ resumeInPlace: {
584
+ defaultValue: false,
585
+ description: "resume \u5355\u9009\u65F6\u662F\u5426\u5728\u5F53\u524D\u7EC8\u7AEF\u5C31\u5730\u6253\u5F00\uFF0Cfalse \u5219\u901A\u8FC7 terminalApp \u5728\u65B0 Tab \u4E2D\u6253\u5F00"
586
+ },
583
587
  aliases: {
584
588
  defaultValue: {},
585
589
  description: "\u547D\u4EE4\u522B\u540D\u6620\u5C04"
@@ -3805,7 +3809,13 @@ async function handleResume(options) {
3805
3809
  return;
3806
3810
  }
3807
3811
  if (targetWorktrees.length === 1) {
3808
- launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
3812
+ const inPlace = getConfigValue("resumeInPlace");
3813
+ if (inPlace) {
3814
+ launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
3815
+ } else {
3816
+ const hasPreviousSession = hasClaudeSessionHistory(targetWorktrees[0].path);
3817
+ launchInteractiveClaudeInNewTerminal(targetWorktrees[0], hasPreviousSession);
3818
+ }
3809
3819
  } else {
3810
3820
  await handleBatchResume(targetWorktrees);
3811
3821
  }
@@ -525,6 +525,10 @@ var CONFIG_DEFINITIONS = {
525
525
  description: "\u6279\u91CF resume \u4F7F\u7528\u7684\u7EC8\u7AEF\u5E94\u7528\uFF1Aauto\uFF08\u81EA\u52A8\u68C0\u6D4B\uFF09\u3001iterm2\u3001terminal\uFF08macOS\uFF09",
526
526
  allowedValues: VALID_TERMINAL_APPS
527
527
  },
528
+ resumeInPlace: {
529
+ defaultValue: false,
530
+ description: "resume \u5355\u9009\u65F6\u662F\u5426\u5728\u5F53\u524D\u7EC8\u7AEF\u5C31\u5730\u6253\u5F00\uFF0Cfalse \u5219\u901A\u8FC7 terminalApp \u5728\u65B0 Tab \u4E2D\u6253\u5F00"
531
+ },
528
532
  aliases: {
529
533
  defaultValue: {},
530
534
  description: "\u547D\u4EE4\u522B\u540D\u6620\u5C04"
@@ -22,6 +22,7 @@
22
22
  "confirmDestructiveOps": true,
23
23
  "maxConcurrency": 0,
24
24
  "terminalApp": "auto",
25
+ "resumeInPlace": false,
25
26
  "aliases": {},
26
27
  "autoUpdate": true
27
28
  }
@@ -37,6 +38,7 @@
37
38
  | `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean)前是否提示确认 |
38
39
  | `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
39
40
  | `terminalApp` | `string` | `"auto"` | 批量 resume 使用的终端应用:`auto`(自动检测)、`iterm2`、`terminal`(macOS) |
41
+ | `resumeInPlace` | `boolean` | `false` | resume 单选时是否在当前终端就地打开,`false` 则通过 `terminalApp` 在新 Tab 中打开 |
40
42
  | `aliases` | `Record<string, string>` | `{}` | 命令别名映射,键为别名,值为目标内置命令名 |
41
43
  | `autoUpdate` | `boolean` | `true` | 是否启用自动更新检查(每 24 小时通过 npm registry 检查一次新版本) |
42
44
 
package/docs/resume.md CHANGED
@@ -38,8 +38,10 @@ clawt resume
38
38
  3. **无匹配** → 报错退出,并列出所有可用分支名
39
39
  4. **根据选中数量自动分发**:
40
40
  - **用户未选择任何分支** → 直接退出
41
- - **选中 1 个** → 在当前终端恢复(同原有行为),通过 `launchInteractiveClaude()` 启动(使用 `spawnSync` + `inherit stdio`)
42
- - **选中多个**进入批量恢复流程(见下文)
41
+ - **选中 1 个** → 根据全局配置项 `resumeInPlace` 决定打开方式:
42
+ - `resumeInPlace: true` 在当前终端就地恢复,通过 `launchInteractiveClaude()` 启动(使用 `spawnSync` + `inherit stdio`)
43
+ - `resumeInPlace: false`(默认) → 通过 `launchInteractiveClaudeInNewTerminal()` 在新终端 Tab 中恢复,终端类型由 `terminalApp` 配置控制
44
+ - **选中多个** → 进入批量恢复流程(见下文),始终在新终端 Tab 中打开,不受 `resumeInPlace` 影响
43
45
 
44
46
  **批量恢复流程:**
45
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawt",
3
- "version": "3.1.1",
3
+ "version": "3.1.2",
4
4
  "description": "本地并行执行多个Claude Code Agent任务,融合 Git Worktree 与 Claude Code CLI 的命令行工具",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,6 +15,7 @@ import {
15
15
  printInfo,
16
16
  printSuccess,
17
17
  confirmAction,
18
+ getConfigValue,
18
19
  } from '../utils/index.js';
19
20
  import type { WorktreeMultiResolveMessages } from '../utils/index.js';
20
21
 
@@ -66,10 +67,18 @@ async function handleResume(options: ResumeOptions): Promise<void> {
66
67
  }
67
68
 
68
69
  if (targetWorktrees.length === 1) {
69
- // 选中 1 个 → 当前终端恢复(resume 自动续接历史会话)
70
- launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
70
+ // 选中 1 个 → 根据 resumeInPlace 配置决定打开方式
71
+ const inPlace = getConfigValue('resumeInPlace');
72
+ if (inPlace) {
73
+ // 就地在当前终端恢复
74
+ launchInteractiveClaude(targetWorktrees[0], { autoContinue: true });
75
+ } else {
76
+ // 默认通过 terminalApp 在新 Tab 中恢复
77
+ const hasPreviousSession = hasClaudeSessionHistory(targetWorktrees[0].path);
78
+ launchInteractiveClaudeInNewTerminal(targetWorktrees[0], hasPreviousSession);
79
+ }
71
80
  } else {
72
- // 选中多个 → 逐个在新终端 Tab 中启动
81
+ // 选中多个 → 逐个在新终端 Tab 中启动(不受 resumeInPlace 影响)
73
82
  await handleBatchResume(targetWorktrees);
74
83
  }
75
84
  }
@@ -35,6 +35,10 @@ export const CONFIG_DEFINITIONS: ConfigDefinitions = {
35
35
  description: '批量 resume 使用的终端应用:auto(自动检测)、iterm2、terminal(macOS)',
36
36
  allowedValues: VALID_TERMINAL_APPS,
37
37
  },
38
+ resumeInPlace: {
39
+ defaultValue: false,
40
+ description: 'resume 单选时是否在当前终端就地打开,false 则通过 terminalApp 在新 Tab 中打开',
41
+ },
38
42
  aliases: {
39
43
  defaultValue: {} as Record<string, string>,
40
44
  description: '命令别名映射',
@@ -17,3 +17,4 @@ export const VALID_TERMINAL_APPS: readonly string[] = ['auto', 'iterm2', 'termin
17
17
 
18
18
  /** iTerm2 应用路径,用于 auto 模式检测是否已安装 */
19
19
  export const ITERM2_APP_PATH = '/Applications/iTerm.app';
20
+
@@ -12,6 +12,8 @@ export interface ClawtConfig {
12
12
  maxConcurrency: number;
13
13
  /** 批量 resume 使用的终端应用:'auto'(自动检测)、'iterm2'、'terminal'(macOS) */
14
14
  terminalApp: string;
15
+ /** resume 单选时是否在当前终端就地打开,false 则通过 terminalApp 在新 Tab 中打开 */
16
+ resumeInPlace: boolean;
15
17
  /** 命令别名映射,键为别名,值为目标内置命令名 */
16
18
  aliases: Record<string, string>;
17
19
  /** 是否启用自动更新检查 */
@@ -11,6 +11,8 @@ vi.mock('../../../src/constants/index.js', () => ({
11
11
  RESUME_SELECT_BRANCH: '选择要恢复的分支',
12
12
  RESUME_MULTIPLE_MATCHES: (keyword: string) => `找到多个匹配 "${keyword}" 的分支`,
13
13
  RESUME_NO_MATCH: (keyword: string, branches: string[]) => `未找到匹配 "${keyword}" 的分支`,
14
+ RESUME_ALL_CONFIRM: (count: number) => `确认恢复 ${count} 个分支?`,
15
+ RESUME_ALL_SUCCESS: (count: number) => `已恢复 ${count} 个分支`,
14
16
  },
15
17
  }));
16
18
 
@@ -19,8 +21,14 @@ vi.mock('../../../src/utils/index.js', () => ({
19
21
  validateClaudeCodeInstalled: vi.fn(),
20
22
  getProjectWorktrees: vi.fn(),
21
23
  launchInteractiveClaude: vi.fn(),
24
+ launchInteractiveClaudeInNewTerminal: vi.fn(),
25
+ hasClaudeSessionHistory: vi.fn(),
22
26
  resolveTargetWorktrees: vi.fn(),
23
27
  promptGroupedMultiSelectBranches: vi.fn(),
28
+ printInfo: vi.fn(),
29
+ printSuccess: vi.fn(),
30
+ confirmAction: vi.fn(),
31
+ getConfigValue: vi.fn(),
24
32
  }));
25
33
 
26
34
  import { registerResumeCommand } from '../../../src/commands/resume.js';
@@ -29,24 +37,36 @@ import {
29
37
  validateClaudeCodeInstalled,
30
38
  getProjectWorktrees,
31
39
  launchInteractiveClaude,
40
+ launchInteractiveClaudeInNewTerminal,
41
+ hasClaudeSessionHistory,
32
42
  resolveTargetWorktrees,
33
43
  promptGroupedMultiSelectBranches,
44
+ confirmAction,
45
+ getConfigValue,
34
46
  } from '../../../src/utils/index.js';
35
47
 
36
48
  const mockedValidateMainWorktree = vi.mocked(validateMainWorktree);
37
49
  const mockedValidateClaudeCodeInstalled = vi.mocked(validateClaudeCodeInstalled);
38
50
  const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
39
51
  const mockedLaunchInteractiveClaude = vi.mocked(launchInteractiveClaude);
52
+ const mockedLaunchInteractiveClaudeInNewTerminal = vi.mocked(launchInteractiveClaudeInNewTerminal);
53
+ const mockedHasClaudeSessionHistory = vi.mocked(hasClaudeSessionHistory);
40
54
  const mockedResolveTargetWorktrees = vi.mocked(resolveTargetWorktrees);
41
55
  const mockedPromptGroupedMultiSelectBranches = vi.mocked(promptGroupedMultiSelectBranches);
56
+ const mockedConfirmAction = vi.mocked(confirmAction);
57
+ const mockedGetConfigValue = vi.mocked(getConfigValue);
42
58
 
43
59
  beforeEach(() => {
44
60
  mockedValidateMainWorktree.mockReset();
45
61
  mockedValidateClaudeCodeInstalled.mockReset();
46
62
  mockedGetProjectWorktrees.mockReset();
47
63
  mockedLaunchInteractiveClaude.mockReset();
64
+ mockedLaunchInteractiveClaudeInNewTerminal.mockReset();
65
+ mockedHasClaudeSessionHistory.mockReset();
48
66
  mockedResolveTargetWorktrees.mockReset();
49
67
  mockedPromptGroupedMultiSelectBranches.mockReset();
68
+ mockedConfirmAction.mockReset();
69
+ mockedGetConfigValue.mockReset();
50
70
  });
51
71
 
52
72
  describe('registerResumeCommand', () => {
@@ -63,6 +83,7 @@ describe('handleResume', () => {
63
83
  const worktree = { path: '/path/feature', branch: 'feature' };
64
84
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
65
85
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
86
+ mockedGetConfigValue.mockReturnValue(true);
66
87
 
67
88
  const program = new Command();
68
89
  program.exitOverride();
@@ -83,6 +104,7 @@ describe('handleResume', () => {
83
104
  ];
84
105
  mockedGetProjectWorktrees.mockReturnValue(worktrees);
85
106
  mockedPromptGroupedMultiSelectBranches.mockResolvedValue([worktrees[0]]);
107
+ mockedGetConfigValue.mockReturnValue(true);
86
108
 
87
109
  const program = new Command();
88
110
  program.exitOverride();
@@ -100,6 +122,7 @@ describe('handleResume', () => {
100
122
  const worktree = { path: '/path/feature', branch: 'feature' };
101
123
  mockedGetProjectWorktrees.mockReturnValue([worktree]);
102
124
  mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
125
+ mockedGetConfigValue.mockReturnValue(true);
103
126
 
104
127
  const program = new Command();
105
128
  program.exitOverride();
@@ -110,3 +133,93 @@ describe('handleResume', () => {
110
133
  expect(mockedPromptGroupedMultiSelectBranches).not.toHaveBeenCalled();
111
134
  });
112
135
  });
136
+
137
+ describe('handleResume — resumeInPlace 配置', () => {
138
+ it('resumeInPlace 为 true 时,单选在当前终端就地恢复', async () => {
139
+ const worktree = { path: '/path/feature', branch: 'feature' };
140
+ mockedGetProjectWorktrees.mockReturnValue([worktree]);
141
+ mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
142
+ mockedGetConfigValue.mockReturnValue(true);
143
+
144
+ const program = new Command();
145
+ program.exitOverride();
146
+ registerResumeCommand(program);
147
+ await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
148
+
149
+ expect(mockedGetConfigValue).toHaveBeenCalledWith('resumeInPlace');
150
+ expect(mockedLaunchInteractiveClaude).toHaveBeenCalledWith(worktree, { autoContinue: true });
151
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('resumeInPlace 为 false 时,单选在新终端 Tab 中恢复', async () => {
155
+ const worktree = { path: '/path/feature', branch: 'feature' };
156
+ mockedGetProjectWorktrees.mockReturnValue([worktree]);
157
+ mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
158
+ mockedGetConfigValue.mockReturnValue(false);
159
+ mockedHasClaudeSessionHistory.mockReturnValue(true);
160
+
161
+ const program = new Command();
162
+ program.exitOverride();
163
+ registerResumeCommand(program);
164
+ await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
165
+
166
+ expect(mockedGetConfigValue).toHaveBeenCalledWith('resumeInPlace');
167
+ expect(mockedHasClaudeSessionHistory).toHaveBeenCalledWith(worktree.path);
168
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).toHaveBeenCalledWith(worktree, true);
169
+ expect(mockedLaunchInteractiveClaude).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it('resumeInPlace 为 false 且无历史会话时,传 false 给新终端启动', async () => {
173
+ const worktree = { path: '/path/feature', branch: 'feature' };
174
+ mockedGetProjectWorktrees.mockReturnValue([worktree]);
175
+ mockedResolveTargetWorktrees.mockResolvedValue([worktree]);
176
+ mockedGetConfigValue.mockReturnValue(false);
177
+ mockedHasClaudeSessionHistory.mockReturnValue(false);
178
+
179
+ const program = new Command();
180
+ program.exitOverride();
181
+ registerResumeCommand(program);
182
+ await program.parseAsync(['resume', '-b', 'feature'], { from: 'user' });
183
+
184
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).toHaveBeenCalledWith(worktree, false);
185
+ });
186
+
187
+ it('多选时不受 resumeInPlace 影响,始终在新 Tab 中打开', async () => {
188
+ const worktrees = [
189
+ { path: '/path/feature-a', branch: 'feature-a' },
190
+ { path: '/path/feature-b', branch: 'feature-b' },
191
+ ];
192
+ mockedGetProjectWorktrees.mockReturnValue(worktrees);
193
+ mockedPromptGroupedMultiSelectBranches.mockResolvedValue(worktrees);
194
+ mockedConfirmAction.mockResolvedValue(true);
195
+ mockedHasClaudeSessionHistory.mockReturnValue(false);
196
+ mockedGetConfigValue.mockReturnValue(true);
197
+
198
+ const program = new Command();
199
+ program.exitOverride();
200
+ registerResumeCommand(program);
201
+ await program.parseAsync(['resume'], { from: 'user' });
202
+
203
+ // 多选走 handleBatchResume,不读取 resumeInPlace
204
+ expect(mockedLaunchInteractiveClaude).not.toHaveBeenCalled();
205
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).toHaveBeenCalledTimes(2);
206
+ });
207
+
208
+ it('用户未选择任何分支时直接退出', async () => {
209
+ const worktrees = [
210
+ { path: '/path/feature-a', branch: 'feature-a' },
211
+ { path: '/path/feature-b', branch: 'feature-b' },
212
+ ];
213
+ mockedGetProjectWorktrees.mockReturnValue(worktrees);
214
+ mockedPromptGroupedMultiSelectBranches.mockResolvedValue([]);
215
+
216
+ const program = new Command();
217
+ program.exitOverride();
218
+ registerResumeCommand(program);
219
+ await program.parseAsync(['resume'], { from: 'user' });
220
+
221
+ expect(mockedLaunchInteractiveClaude).not.toHaveBeenCalled();
222
+ expect(mockedLaunchInteractiveClaudeInNewTerminal).not.toHaveBeenCalled();
223
+ expect(mockedGetConfigValue).not.toHaveBeenCalled();
224
+ });
225
+ });
@@ -33,6 +33,7 @@ describe('DEFAULT_CONFIG', () => {
33
33
  expect(DEFAULT_CONFIG.confirmDestructiveOps).toBe(true);
34
34
  expect(DEFAULT_CONFIG.maxConcurrency).toBe(0);
35
35
  expect(DEFAULT_CONFIG.aliases).toEqual({});
36
+ expect(DEFAULT_CONFIG.resumeInPlace).toBe(false);
36
37
  });
37
38
  });
38
39