clawt 3.1.0 → 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 +12 -3
- package/dist/postinstall.js +4 -0
- package/docs/config-file.md +2 -0
- package/docs/resume.md +4 -2
- package/docs/spec.md +1 -1
- package/package.json +1 -1
- package/src/commands/remove.ts +2 -3
- package/src/commands/resume.ts +12 -3
- package/src/commands/sync.ts +0 -2
- 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/remove.test.ts +0 -1
- package/tests/unit/commands/resume.test.ts +113 -0
- package/tests/unit/commands/sync.test.ts +0 -1
- 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"
|
|
@@ -3631,6 +3635,7 @@ function registerRemoveCommand(program2) {
|
|
|
3631
3635
|
}
|
|
3632
3636
|
async function handleRemove(options) {
|
|
3633
3637
|
validateMainWorktree();
|
|
3638
|
+
requireProjectConfig();
|
|
3634
3639
|
const projectName = getProjectName();
|
|
3635
3640
|
logger.info(`remove \u547D\u4EE4\u6267\u884C\uFF0C\u9879\u76EE: ${projectName}`);
|
|
3636
3641
|
const allWorktrees = getProjectWorktrees();
|
|
@@ -3660,7 +3665,6 @@ async function handleRemove(options) {
|
|
|
3660
3665
|
const failures = [];
|
|
3661
3666
|
for (const wt of worktreesToRemove) {
|
|
3662
3667
|
try {
|
|
3663
|
-
await ensureOnMainWorkBranch();
|
|
3664
3668
|
removeWorktreeByPath(wt.path);
|
|
3665
3669
|
if (shouldDeleteBranch) {
|
|
3666
3670
|
deleteBranch(wt.branch);
|
|
@@ -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
|
}
|
|
@@ -3893,7 +3903,6 @@ async function executeSyncForBranch(targetWorktreePath, branch) {
|
|
|
3893
3903
|
async function handleSync(options) {
|
|
3894
3904
|
validateMainWorktree();
|
|
3895
3905
|
requireProjectConfig();
|
|
3896
|
-
await ensureOnMainWorkBranch();
|
|
3897
3906
|
logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
|
|
3898
3907
|
const worktrees = getProjectWorktrees();
|
|
3899
3908
|
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
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/docs/spec.md
CHANGED
|
@@ -328,7 +328,7 @@ export const PROJECTS_CONFIG_DIR = join(CLAWT_HOME, 'projects');
|
|
|
328
328
|
✗ 该项目尚未初始化,请先执行 clawt init -b<branchName>设置主工作分支
|
|
329
329
|
```
|
|
330
330
|
其他命令(list、resume、config、status、alias、projects、completion)不受影响,无需添加该校验。
|
|
331
|
-
> **实现细节**:`ensureOnMainWorkBranch()` 内部已通过 `getMainWorkBranch()` → `requireProjectConfig()` 完成了项目配置校验,因此调用了 `ensureOnMainWorkBranch` 的命令(create、run、validate、
|
|
331
|
+
> **实现细节**:`ensureOnMainWorkBranch()` 内部已通过 `getMainWorkBranch()` → `requireProjectConfig()` 完成了项目配置校验,因此调用了 `ensureOnMainWorkBranch` 的命令(create、run、validate、merge)**无需再显式调用 `requireProjectConfig()`**,避免重复校验。sync 和 remove 命令因不依赖主 worktree 的分支状态而不调用 `ensureOnMainWorkBranch`,需自行显式调用 `requireProjectConfig()`。reset 命令同理,也需自行调用 `requireProjectConfig()`。
|
|
332
332
|
3. **主分支名统一从项目级配置获取**:所有需要获取主分支名的场景(sync 中合并主分支、merge 中计算 merge-base、切回主分支等),统一使用项目级配置中的 `clawtMainWorkBranch`,不再通过 `getCurrentBranch(mainWorktreePath)` 动态获取。因为在新架构下,主 worktree 可能处于验证分支上,`getCurrentBranch` 会返回验证分支名而非真正的主工作分支名。
|
|
333
333
|
4. **测试文件全量更新**:本次重构涉及的所有命令(init、create、run、validate、sync、remove、merge、reset),其对应的测试文件必须同步更新,确保覆盖新增的验证分支逻辑、项目级配置逻辑和变更后的流程。
|
|
334
334
|
|
package/package.json
CHANGED
package/src/commands/remove.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
resolveTargetWorktrees,
|
|
24
24
|
getValidateBranchName,
|
|
25
25
|
deleteValidateBranch,
|
|
26
|
-
|
|
26
|
+
requireProjectConfig,
|
|
27
27
|
} from '../utils/index.js';
|
|
28
28
|
import type { WorktreeMultiResolveMessages } from '../utils/index.js';
|
|
29
29
|
|
|
@@ -56,6 +56,7 @@ export function registerRemoveCommand(program: Command): void {
|
|
|
56
56
|
*/
|
|
57
57
|
async function handleRemove(options: RemoveOptions): Promise<void> {
|
|
58
58
|
validateMainWorktree();
|
|
59
|
+
requireProjectConfig();
|
|
59
60
|
|
|
60
61
|
const projectName = getProjectName();
|
|
61
62
|
logger.info(`remove 命令执行,项目: ${projectName}`);
|
|
@@ -98,8 +99,6 @@ async function handleRemove(options: RemoveOptions): Promise<void> {
|
|
|
98
99
|
const failures: Array<{ path: string; error: string }> = [];
|
|
99
100
|
for (const wt of worktreesToRemove) {
|
|
100
101
|
try {
|
|
101
|
-
// 确保当前在主工作分支上
|
|
102
|
-
await ensureOnMainWorkBranch();
|
|
103
102
|
removeWorktreeByPath(wt.path);
|
|
104
103
|
if (shouldDeleteBranch) {
|
|
105
104
|
deleteBranch(wt.branch);
|
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/commands/sync.ts
CHANGED
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
getMainWorkBranch,
|
|
24
24
|
rebuildValidateBranch,
|
|
25
25
|
getValidateBranchName,
|
|
26
|
-
ensureOnMainWorkBranch,
|
|
27
26
|
} from '../utils/index.js';
|
|
28
27
|
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
29
28
|
|
|
@@ -140,7 +139,6 @@ export async function executeSyncForBranch(targetWorktreePath: string, branch: s
|
|
|
140
139
|
async function handleSync(options: SyncOptions): Promise<void> {
|
|
141
140
|
validateMainWorktree();
|
|
142
141
|
requireProjectConfig();
|
|
143
|
-
await ensureOnMainWorkBranch();
|
|
144
142
|
|
|
145
143
|
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
146
144
|
|
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
|
/** 是否启用自动更新检查 */
|
|
@@ -49,7 +49,6 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
49
49
|
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
50
50
|
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
51
51
|
deleteValidateBranch: vi.fn(),
|
|
52
|
-
ensureOnMainWorkBranch: vi.fn(),
|
|
53
52
|
}));
|
|
54
53
|
|
|
55
54
|
import { registerRemoveCommand } from '../../../src/commands/remove.js';
|
|
@@ -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
|
+
});
|
|
@@ -51,7 +51,6 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
51
51
|
getMainWorkBranch: vi.fn().mockReturnValue('main'),
|
|
52
52
|
rebuildValidateBranch: vi.fn(),
|
|
53
53
|
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
54
|
-
ensureOnMainWorkBranch: vi.fn(),
|
|
55
54
|
}));
|
|
56
55
|
|
|
57
56
|
import { registerSyncCommand } from '../../../src/commands/sync.js';
|