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 +2 -1
- package/dist/index.js +11 -1
- package/dist/postinstall.js +4 -0
- package/docs/config-file.md +2 -0
- package/docs/resume.md +4 -2
- package/package.json +1 -1
- package/src/commands/resume.ts +12 -3
- package/src/constants/config.ts +4 -0
- package/src/constants/terminal.ts +1 -0
- package/src/types/config.ts +2 -0
- package/tests/unit/commands/resume.test.ts +113 -0
- package/tests/unit/constants/config.test.ts +1 -0
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ clawt resume -b <branch> # 指定分支
|
|
|
113
113
|
clawt resume # 交互式多选(按创建日期分组)
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
不传 `-b` 时,分支列表按创建日期分组显示,支持全局全选和按组全选。选 1
|
|
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
|
-
|
|
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
|
}
|
package/dist/postinstall.js
CHANGED
|
@@ -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"
|
package/docs/config-file.md
CHANGED
|
@@ -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 个** →
|
|
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
package/src/commands/resume.ts
CHANGED
|
@@ -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 个 →
|
|
70
|
-
|
|
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
|
}
|
package/src/constants/config.ts
CHANGED
|
@@ -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: '命令别名映射',
|
package/src/types/config.ts
CHANGED
|
@@ -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
|
+
});
|