clawt 3.4.5 → 3.5.0
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/.claude/settings.local.json +12 -0
- package/README.md +0 -4
- package/dist/index.js +430 -306
- package/dist/postinstall.js +12 -1
- package/docs/alias.md +7 -1
- package/docs/completion.md +1 -1
- package/docs/config.md +4 -3
- package/docs/cover-validate.md +4 -3
- package/docs/create.md +28 -12
- package/docs/home.md +12 -8
- package/docs/init.md +16 -9
- package/docs/list.md +13 -7
- package/docs/merge.md +12 -12
- package/docs/remove.md +24 -13
- package/docs/reset.md +6 -4
- package/docs/resume.md +3 -4
- package/docs/status.md +75 -30
- package/docs/sync.md +26 -26
- package/docs/validate.md +13 -7
- package/package.json +1 -1
- package/src/commands/init.ts +6 -2
- package/src/commands/tasks.ts +51 -0
- package/src/constants/index.ts +3 -0
- package/src/constants/interactive-panel.ts +6 -0
- package/src/constants/messages/index.ts +4 -2
- package/src/constants/messages/interactive-panel.ts +12 -0
- package/src/constants/messages/tasks.ts +9 -0
- package/src/constants/tasks-template.ts +28 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +6 -0
- package/src/types/index.ts +1 -1
- package/src/utils/formatter.ts +19 -0
- package/src/utils/git-branch.ts +116 -0
- package/src/utils/git-core.ts +369 -0
- package/src/utils/git-worktree.ts +40 -0
- package/src/utils/git.ts +3 -521
- package/src/utils/index.ts +1 -1
- package/src/utils/interactive-panel-render.ts +12 -6
- package/src/utils/interactive-panel-state.ts +137 -0
- package/src/utils/interactive-panel.ts +44 -188
- package/src/utils/keyboard-controller.ts +48 -0
- package/src/utils/ui-prompts.ts +240 -0
- package/src/utils/worktree-matcher.ts +21 -251
- package/tests/unit/commands/tasks.test.ts +153 -0
- package/tests/unit/utils/formatter.test.ts +26 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import Enquirer from 'enquirer';
|
|
2
|
+
import {
|
|
3
|
+
SELECT_ALL_NAME,
|
|
4
|
+
SELECT_ALL_LABEL,
|
|
5
|
+
} from '../constants/index.js';
|
|
6
|
+
import type { WorktreeInfo } from '../types/index.js';
|
|
7
|
+
import { groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap } from './worktree-matcher.js';
|
|
8
|
+
|
|
9
|
+
/** enquirer MultiSelect 选项条目的运行时结构 */
|
|
10
|
+
export interface MultiSelectChoice {
|
|
11
|
+
name: string;
|
|
12
|
+
message: string;
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* enquirer MultiSelect 实例的运行时接口
|
|
18
|
+
* enquirer 类型声明未导出 MultiSelect,手动声明以消除 TypeScript 类型错误
|
|
19
|
+
*/
|
|
20
|
+
export interface MultiSelectInstance {
|
|
21
|
+
focused: MultiSelectChoice | undefined;
|
|
22
|
+
choices: MultiSelectChoice[];
|
|
23
|
+
render(): void;
|
|
24
|
+
toggle(choice: MultiSelectChoice): void;
|
|
25
|
+
run(): Promise<string[]>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** enquirer MultiSelect 分隔线条目结构 */
|
|
29
|
+
export interface MultiSelectSeparator {
|
|
30
|
+
role: 'separator';
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** enquirer MultiSelect choices 数组的条目类型 */
|
|
35
|
+
export type GroupedChoice = { name: string; message: string } | MultiSelectSeparator;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 通过交互式列表让用户从 worktree 列表中选择一个分支
|
|
39
|
+
* @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
|
|
40
|
+
* @param {string} message - 选择提示信息
|
|
41
|
+
* @returns {Promise<WorktreeInfo>} 用户选择的 worktree
|
|
42
|
+
*/
|
|
43
|
+
export async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
|
|
44
|
+
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
45
|
+
const selectedBranch: string = await new Enquirer.Select({
|
|
46
|
+
message,
|
|
47
|
+
choices: worktrees.map((wt) => ({
|
|
48
|
+
name: wt.branch,
|
|
49
|
+
message: wt.branch,
|
|
50
|
+
})),
|
|
51
|
+
}).run();
|
|
52
|
+
|
|
53
|
+
return worktrees.find((wt) => wt.branch === selectedBranch)!;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 通过交互式多选列表让用户从 worktree 列表中选择多个分支
|
|
58
|
+
* 顶部提供「全选」选项,点击可切换全选/全不选
|
|
59
|
+
* 用户可通过空格键选择/取消,回车键确认
|
|
60
|
+
* @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
|
|
61
|
+
* @param {string} message - 选择提示信息
|
|
62
|
+
* @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
|
|
63
|
+
*/
|
|
64
|
+
export async function promptMultiSelectBranches(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo[]> {
|
|
65
|
+
// 构建 choices 列表,顶部插入全选选项
|
|
66
|
+
const branchChoices = worktrees.map((wt) => ({
|
|
67
|
+
name: wt.branch,
|
|
68
|
+
message: wt.branch,
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
const choices = [
|
|
72
|
+
{ name: SELECT_ALL_NAME, message: SELECT_ALL_LABEL },
|
|
73
|
+
...branchChoices,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
|
|
77
|
+
const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 扩展 MultiSelect,覆写 space() 方法实现全选 toggle
|
|
81
|
+
* 当焦点在「全选」选项上按空格时,切换所有分支选项的选中状态
|
|
82
|
+
*/
|
|
83
|
+
class MultiSelectWithSelectAll extends MultiSelect {
|
|
84
|
+
space(this: MultiSelectInstance) {
|
|
85
|
+
if (!this.focused) return;
|
|
86
|
+
|
|
87
|
+
if (this.focused.name === SELECT_ALL_NAME) {
|
|
88
|
+
// 切换全选:如果全选项当前未选中则全选,否则全不选
|
|
89
|
+
const willEnable = !this.focused.enabled;
|
|
90
|
+
for (const ch of this.choices) {
|
|
91
|
+
ch.enabled = willEnable;
|
|
92
|
+
}
|
|
93
|
+
return this.render();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 非全选选项:执行默认的 toggle 行为
|
|
97
|
+
this.toggle(this.focused);
|
|
98
|
+
|
|
99
|
+
// 同步全选选项状态:所有分支选项都选中时自动勾选全选,否则取消
|
|
100
|
+
const selectAllChoice = this.choices.find((ch) => ch.name === SELECT_ALL_NAME);
|
|
101
|
+
const branchItems = this.choices.filter((ch) => ch.name !== SELECT_ALL_NAME);
|
|
102
|
+
if (selectAllChoice) {
|
|
103
|
+
selectAllChoice.enabled = branchItems.every((ch) => ch.enabled);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return this.render();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const selectedBranches: string[] = await new MultiSelectWithSelectAll({
|
|
111
|
+
message,
|
|
112
|
+
choices,
|
|
113
|
+
// 使用空心圆/实心圆作为选中指示符
|
|
114
|
+
symbols: {
|
|
115
|
+
indicator: { on: '●', off: '○' },
|
|
116
|
+
},
|
|
117
|
+
}).run();
|
|
118
|
+
|
|
119
|
+
// 过滤掉全选选项,只返回实际的 worktree
|
|
120
|
+
return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 通过交互式多选列表(按日期分组)让用户选择多个分支
|
|
125
|
+
* 提供三级联动:全局全选、组级全选、单个分支
|
|
126
|
+
* @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
|
|
127
|
+
* @param {string} message - 选择提示信息
|
|
128
|
+
* @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
|
|
129
|
+
*/
|
|
130
|
+
export async function promptGroupedMultiSelectBranches(
|
|
131
|
+
worktrees: WorktreeInfo[],
|
|
132
|
+
message: string,
|
|
133
|
+
): Promise<WorktreeInfo[]> {
|
|
134
|
+
const groups = groupWorktreesByDate(worktrees);
|
|
135
|
+
const choices = buildGroupedChoices(groups);
|
|
136
|
+
const groupMembershipMap = buildGroupMembershipMap(groups);
|
|
137
|
+
|
|
138
|
+
// 收集所有组全选的 name,用于判断某个 choice 是否为组全选项
|
|
139
|
+
const groupSelectAllNames = new Set(groupMembershipMap.keys());
|
|
140
|
+
|
|
141
|
+
// 收集所有实际分支的 name
|
|
142
|
+
const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
|
|
143
|
+
|
|
144
|
+
// @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
|
|
145
|
+
const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 扩展 MultiSelect,实现三级联动的 space() 覆写
|
|
149
|
+
* - 全局全选:切换所有 choices(含组全选)
|
|
150
|
+
* - 组级全选:切换该组内所有分支,同步全局全选状态
|
|
151
|
+
* - 普通分支:toggle 该分支,同步所属组全选和全局全选状态
|
|
152
|
+
*/
|
|
153
|
+
class MultiSelectWithGroupSelectAll extends MultiSelect {
|
|
154
|
+
space(this: MultiSelectInstance) {
|
|
155
|
+
if (!this.focused) return;
|
|
156
|
+
|
|
157
|
+
const focusedName = this.focused.name;
|
|
158
|
+
|
|
159
|
+
if (focusedName === SELECT_ALL_NAME) {
|
|
160
|
+
// 全局全选:切换所有 choices
|
|
161
|
+
const willEnable = !this.focused.enabled;
|
|
162
|
+
for (const ch of this.choices) {
|
|
163
|
+
ch.enabled = willEnable;
|
|
164
|
+
}
|
|
165
|
+
return this.render();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (groupSelectAllNames.has(focusedName)) {
|
|
169
|
+
// 组级全选:切换该组内所有分支
|
|
170
|
+
const willEnable = !this.focused.enabled;
|
|
171
|
+
const memberNames = groupMembershipMap.get(focusedName)!;
|
|
172
|
+
// 切换组全选自身
|
|
173
|
+
this.focused.enabled = willEnable;
|
|
174
|
+
// 切换该组的所有分支
|
|
175
|
+
for (const ch of this.choices) {
|
|
176
|
+
if (memberNames.includes(ch.name)) {
|
|
177
|
+
ch.enabled = willEnable;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 同步全局全选状态:检查所有实际分支是否全选
|
|
181
|
+
syncGlobalSelectAll(this.choices);
|
|
182
|
+
return this.render();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 普通分支:toggle 该分支
|
|
186
|
+
this.toggle(this.focused);
|
|
187
|
+
|
|
188
|
+
// 同步所属组全选状态
|
|
189
|
+
syncGroupSelectAll(this.choices, focusedName);
|
|
190
|
+
// 同步全局全选状态
|
|
191
|
+
syncGlobalSelectAll(this.choices);
|
|
192
|
+
|
|
193
|
+
return this.render();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 同步全局全选状态
|
|
199
|
+
* 根据所有实际分支的选中状态更新全局全选项
|
|
200
|
+
* @param {MultiSelectChoice[]} choiceList - choices 列表
|
|
201
|
+
*/
|
|
202
|
+
function syncGlobalSelectAll(choiceList: MultiSelectChoice[]): void {
|
|
203
|
+
const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
|
|
204
|
+
if (!selectAllChoice) return;
|
|
205
|
+
|
|
206
|
+
const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
|
|
207
|
+
selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 同步指定分支所属组的全选状态
|
|
212
|
+
* 根据该组内所有分支的选中状态更新组全选项
|
|
213
|
+
* @param {MultiSelectChoice[]} choiceList - choices 列表
|
|
214
|
+
* @param {string} branchName - 刚被 toggle 的分支名
|
|
215
|
+
*/
|
|
216
|
+
function syncGroupSelectAll(choiceList: MultiSelectChoice[], branchName: string): void {
|
|
217
|
+
for (const [groupName, memberNames] of groupMembershipMap) {
|
|
218
|
+
if (!memberNames.includes(branchName)) continue;
|
|
219
|
+
|
|
220
|
+
const groupChoice = choiceList.find((ch) => ch.name === groupName);
|
|
221
|
+
if (!groupChoice) continue;
|
|
222
|
+
|
|
223
|
+
const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
|
|
224
|
+
groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const selectedBranches: string[] = await new MultiSelectWithGroupSelectAll({
|
|
230
|
+
message,
|
|
231
|
+
choices,
|
|
232
|
+
// 使用空心圆/实心圆作为选中指示符
|
|
233
|
+
symbols: {
|
|
234
|
+
indicator: { on: '●', off: '○' },
|
|
235
|
+
},
|
|
236
|
+
}).run();
|
|
237
|
+
|
|
238
|
+
// 过滤掉全选项和组全选项,只返回实际选中的 worktree
|
|
239
|
+
return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
|
|
240
|
+
}
|
|
@@ -1,35 +1,17 @@
|
|
|
1
|
-
import Enquirer from 'enquirer';
|
|
2
1
|
import { statSync } from 'node:fs';
|
|
3
2
|
import { ClawtError } from '../errors/index.js';
|
|
4
3
|
import {
|
|
5
|
-
SELECT_ALL_NAME,
|
|
6
|
-
SELECT_ALL_LABEL,
|
|
7
|
-
GROUP_SELECT_ALL_PREFIX,
|
|
8
|
-
GROUP_SELECT_ALL_LABEL,
|
|
9
|
-
GROUP_SEPARATOR_LABEL,
|
|
10
4
|
UNKNOWN_DATE_GROUP,
|
|
11
5
|
UNKNOWN_DATE_SEPARATOR_LABEL,
|
|
6
|
+
GROUP_SEPARATOR_LABEL,
|
|
7
|
+
GROUP_SELECT_ALL_PREFIX,
|
|
8
|
+
GROUP_SELECT_ALL_LABEL,
|
|
9
|
+
SELECT_ALL_NAME,
|
|
10
|
+
SELECT_ALL_LABEL
|
|
12
11
|
} from '../constants/index.js';
|
|
13
12
|
import type { WorktreeInfo } from '../types/index.js';
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
interface MultiSelectChoice {
|
|
17
|
-
name: string;
|
|
18
|
-
message: string;
|
|
19
|
-
enabled: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* enquirer MultiSelect 实例的运行时接口
|
|
24
|
-
* enquirer 类型声明未导出 MultiSelect,手动声明以消除 TypeScript 类型错误
|
|
25
|
-
*/
|
|
26
|
-
interface MultiSelectInstance {
|
|
27
|
-
focused: MultiSelectChoice | undefined;
|
|
28
|
-
choices: MultiSelectChoice[];
|
|
29
|
-
render(): void;
|
|
30
|
-
toggle(choice: MultiSelectChoice): void;
|
|
31
|
-
run(): Promise<string[]>;
|
|
32
|
-
}
|
|
13
|
+
import { promptSelectBranch, promptMultiSelectBranches } from './ui-prompts.js';
|
|
14
|
+
import type { GroupedChoice } from './ui-prompts.js';
|
|
33
15
|
|
|
34
16
|
/**
|
|
35
17
|
* 分支解析时使用的消息文案配置
|
|
@@ -46,27 +28,6 @@ export interface WorktreeResolveMessages {
|
|
|
46
28
|
noMatch: (keyword: string, branches: string[]) => string;
|
|
47
29
|
}
|
|
48
30
|
|
|
49
|
-
/**
|
|
50
|
-
* 在 worktree 列表中精确匹配分支名
|
|
51
|
-
* @param {WorktreeInfo[]} worktrees - worktree 列表
|
|
52
|
-
* @param {string} branchName - 目标分支名
|
|
53
|
-
* @returns {WorktreeInfo | undefined} 匹配的 worktree,未找到返回 undefined
|
|
54
|
-
*/
|
|
55
|
-
export function findExactMatch(worktrees: WorktreeInfo[], branchName: string): WorktreeInfo | undefined {
|
|
56
|
-
return worktrees.find((wt) => wt.branch === branchName);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* 在 worktree 列表中进行模糊匹配(子串匹配,大小写不敏感)
|
|
61
|
-
* @param {WorktreeInfo[]} worktrees - worktree 列表
|
|
62
|
-
* @param {string} keyword - 匹配关键词
|
|
63
|
-
* @returns {WorktreeInfo[]} 匹配到的 worktree 列表
|
|
64
|
-
*/
|
|
65
|
-
export function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): WorktreeInfo[] {
|
|
66
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
67
|
-
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
31
|
/**
|
|
71
32
|
* 多选场景下的分支解析消息文案配置
|
|
72
33
|
* 与 WorktreeResolveMessages 类似,但用于需要多选的命令(如 remove)
|
|
@@ -83,89 +44,24 @@ export interface WorktreeMultiResolveMessages {
|
|
|
83
44
|
}
|
|
84
45
|
|
|
85
46
|
/**
|
|
86
|
-
*
|
|
87
|
-
* @param {WorktreeInfo[]} worktrees -
|
|
88
|
-
* @param {string}
|
|
89
|
-
* @returns {
|
|
47
|
+
* 在 worktree 列表中精确匹配分支名
|
|
48
|
+
* @param {WorktreeInfo[]} worktrees - worktree 列表
|
|
49
|
+
* @param {string} branchName - 目标分支名
|
|
50
|
+
* @returns {WorktreeInfo | undefined} 匹配的 worktree,未找到返回 undefined
|
|
90
51
|
*/
|
|
91
|
-
export
|
|
92
|
-
|
|
93
|
-
const selectedBranch: string = await new Enquirer.Select({
|
|
94
|
-
message,
|
|
95
|
-
choices: worktrees.map((wt) => ({
|
|
96
|
-
name: wt.branch,
|
|
97
|
-
message: wt.branch,
|
|
98
|
-
})),
|
|
99
|
-
}).run();
|
|
100
|
-
|
|
101
|
-
return worktrees.find((wt) => wt.branch === selectedBranch)!;
|
|
52
|
+
export function findExactMatch(worktrees: WorktreeInfo[], branchName: string): WorktreeInfo | undefined {
|
|
53
|
+
return worktrees.find((wt) => wt.branch === branchName);
|
|
102
54
|
}
|
|
103
55
|
|
|
104
56
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
* @
|
|
109
|
-
* @param {string} message - 选择提示信息
|
|
110
|
-
* @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
|
|
57
|
+
* 在 worktree 列表中进行模糊匹配(子串匹配,大小写不敏感)
|
|
58
|
+
* @param {WorktreeInfo[]} worktrees - worktree 列表
|
|
59
|
+
* @param {string} keyword - 匹配关键词
|
|
60
|
+
* @returns {WorktreeInfo[]} 匹配到的 worktree 列表
|
|
111
61
|
*/
|
|
112
|
-
export
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
name: wt.branch,
|
|
116
|
-
message: wt.branch,
|
|
117
|
-
}));
|
|
118
|
-
|
|
119
|
-
const choices = [
|
|
120
|
-
{ name: SELECT_ALL_NAME, message: SELECT_ALL_LABEL },
|
|
121
|
-
...branchChoices,
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
// @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
|
|
125
|
-
const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* 扩展 MultiSelect,覆写 space() 方法实现全选 toggle
|
|
129
|
-
* 当焦点在「全选」选项上按空格时,切换所有分支选项的选中状态
|
|
130
|
-
*/
|
|
131
|
-
class MultiSelectWithSelectAll extends MultiSelect {
|
|
132
|
-
space(this: MultiSelectInstance) {
|
|
133
|
-
if (!this.focused) return;
|
|
134
|
-
|
|
135
|
-
if (this.focused.name === SELECT_ALL_NAME) {
|
|
136
|
-
// 切换全选:如果全选项当前未选中则全选,否则全不选
|
|
137
|
-
const willEnable = !this.focused.enabled;
|
|
138
|
-
for (const ch of this.choices) {
|
|
139
|
-
ch.enabled = willEnable;
|
|
140
|
-
}
|
|
141
|
-
return this.render();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// 非全选选项:执行默认的 toggle 行为
|
|
145
|
-
this.toggle(this.focused);
|
|
146
|
-
|
|
147
|
-
// 同步全选选项状态:所有分支选项都选中时自动勾选全选,否则取消
|
|
148
|
-
const selectAllChoice = this.choices.find((ch) => ch.name === SELECT_ALL_NAME);
|
|
149
|
-
const branchItems = this.choices.filter((ch) => ch.name !== SELECT_ALL_NAME);
|
|
150
|
-
if (selectAllChoice) {
|
|
151
|
-
selectAllChoice.enabled = branchItems.every((ch) => ch.enabled);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return this.render();
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const selectedBranches: string[] = await new MultiSelectWithSelectAll({
|
|
159
|
-
message,
|
|
160
|
-
choices,
|
|
161
|
-
// 使用空心圆/实心圆作为选中指示符
|
|
162
|
-
symbols: {
|
|
163
|
-
indicator: { on: '●', off: '○' },
|
|
164
|
-
},
|
|
165
|
-
}).run();
|
|
166
|
-
|
|
167
|
-
// 过滤掉全选选项,只返回实际的 worktree
|
|
168
|
-
return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
|
|
62
|
+
export function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): WorktreeInfo[] {
|
|
63
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
64
|
+
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
169
65
|
}
|
|
170
66
|
|
|
171
67
|
/**
|
|
@@ -274,15 +170,6 @@ export async function resolveTargetWorktree(
|
|
|
274
170
|
throw new ClawtError(messages.noMatch(branchName, allBranches));
|
|
275
171
|
}
|
|
276
172
|
|
|
277
|
-
/** enquirer MultiSelect 分隔线条目结构 */
|
|
278
|
-
interface MultiSelectSeparator {
|
|
279
|
-
role: 'separator';
|
|
280
|
-
message: string;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/** enquirer MultiSelect choices 数组的条目类型 */
|
|
284
|
-
type GroupedChoice = { name: string; message: string } | MultiSelectSeparator;
|
|
285
|
-
|
|
286
173
|
/**
|
|
287
174
|
* 将 Date 对象格式化为本地时区的 YYYY-MM-DD 字符串
|
|
288
175
|
* @param {Date} date - 日期对象
|
|
@@ -428,121 +315,4 @@ export function buildGroupMembershipMap(groups: Map<string, WorktreeInfo[]>): Ma
|
|
|
428
315
|
return map;
|
|
429
316
|
}
|
|
430
317
|
|
|
431
|
-
|
|
432
|
-
* 通过交互式多选列表(按日期分组)让用户选择多个分支
|
|
433
|
-
* 提供三级联动:全局全选、组级全选、单个分支
|
|
434
|
-
* @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
|
|
435
|
-
* @param {string} message - 选择提示信息
|
|
436
|
-
* @returns {Promise<WorktreeInfo[]>} 用户选择的 worktree 列表
|
|
437
|
-
*/
|
|
438
|
-
export async function promptGroupedMultiSelectBranches(
|
|
439
|
-
worktrees: WorktreeInfo[],
|
|
440
|
-
message: string,
|
|
441
|
-
): Promise<WorktreeInfo[]> {
|
|
442
|
-
const groups = groupWorktreesByDate(worktrees);
|
|
443
|
-
const choices = buildGroupedChoices(groups);
|
|
444
|
-
const groupMembershipMap = buildGroupMembershipMap(groups);
|
|
445
|
-
|
|
446
|
-
// 收集所有组全选的 name,用于判断某个 choice 是否为组全选项
|
|
447
|
-
const groupSelectAllNames = new Set(groupMembershipMap.keys());
|
|
448
|
-
|
|
449
|
-
// 收集所有实际分支的 name
|
|
450
|
-
const allBranchNames = new Set(worktrees.map((wt) => wt.branch));
|
|
451
|
-
|
|
452
|
-
// @ts-expect-error enquirer 类型声明未导出 MultiSelect 类,但运行时存在
|
|
453
|
-
const MultiSelect: new (options: Record<string, unknown>) => MultiSelectInstance = Enquirer.MultiSelect;
|
|
454
|
-
|
|
455
|
-
/**
|
|
456
|
-
* 扩展 MultiSelect,实现三级联动的 space() 覆写
|
|
457
|
-
* - 全局全选:切换所有 choices(含组全选)
|
|
458
|
-
* - 组级全选:切换该组内所有分支,同步全局全选状态
|
|
459
|
-
* - 普通分支:toggle 该分支,同步所属组全选和全局全选状态
|
|
460
|
-
*/
|
|
461
|
-
class MultiSelectWithGroupSelectAll extends MultiSelect {
|
|
462
|
-
space(this: MultiSelectInstance) {
|
|
463
|
-
if (!this.focused) return;
|
|
464
|
-
|
|
465
|
-
const focusedName = this.focused.name;
|
|
466
|
-
|
|
467
|
-
if (focusedName === SELECT_ALL_NAME) {
|
|
468
|
-
// 全局全选:切换所有 choices
|
|
469
|
-
const willEnable = !this.focused.enabled;
|
|
470
|
-
for (const ch of this.choices) {
|
|
471
|
-
ch.enabled = willEnable;
|
|
472
|
-
}
|
|
473
|
-
return this.render();
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (groupSelectAllNames.has(focusedName)) {
|
|
477
|
-
// 组级全选:切换该组内所有分支
|
|
478
|
-
const willEnable = !this.focused.enabled;
|
|
479
|
-
const memberNames = groupMembershipMap.get(focusedName)!;
|
|
480
|
-
// 切换组全选自身
|
|
481
|
-
this.focused.enabled = willEnable;
|
|
482
|
-
// 切换该组的所有分支
|
|
483
|
-
for (const ch of this.choices) {
|
|
484
|
-
if (memberNames.includes(ch.name)) {
|
|
485
|
-
ch.enabled = willEnable;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
// 同步全局全选状态:检查所有实际分支是否全选
|
|
489
|
-
syncGlobalSelectAll(this.choices);
|
|
490
|
-
return this.render();
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// 普通分支:toggle 该分支
|
|
494
|
-
this.toggle(this.focused);
|
|
495
|
-
|
|
496
|
-
// 同步所属组全选状态
|
|
497
|
-
syncGroupSelectAll(this.choices, focusedName);
|
|
498
|
-
// 同步全局全选状态
|
|
499
|
-
syncGlobalSelectAll(this.choices);
|
|
500
|
-
|
|
501
|
-
return this.render();
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* 同步全局全选状态
|
|
507
|
-
* 根据所有实际分支的选中状态更新全局全选项
|
|
508
|
-
* @param {MultiSelectChoice[]} choiceList - choices 列表
|
|
509
|
-
*/
|
|
510
|
-
function syncGlobalSelectAll(choiceList: MultiSelectChoice[]): void {
|
|
511
|
-
const selectAllChoice = choiceList.find((ch) => ch.name === SELECT_ALL_NAME);
|
|
512
|
-
if (!selectAllChoice) return;
|
|
513
|
-
|
|
514
|
-
const branchItems = choiceList.filter((ch) => allBranchNames.has(ch.name));
|
|
515
|
-
selectAllChoice.enabled = branchItems.length > 0 && branchItems.every((ch) => ch.enabled);
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* 同步指定分支所属组的全选状态
|
|
520
|
-
* 根据该组内所有分支的选中状态更新组全选项
|
|
521
|
-
* @param {MultiSelectChoice[]} choiceList - choices 列表
|
|
522
|
-
* @param {string} branchName - 刚被 toggle 的分支名
|
|
523
|
-
*/
|
|
524
|
-
function syncGroupSelectAll(choiceList: MultiSelectChoice[], branchName: string): void {
|
|
525
|
-
for (const [groupName, memberNames] of groupMembershipMap) {
|
|
526
|
-
if (!memberNames.includes(branchName)) continue;
|
|
527
|
-
|
|
528
|
-
const groupChoice = choiceList.find((ch) => ch.name === groupName);
|
|
529
|
-
if (!groupChoice) continue;
|
|
530
|
-
|
|
531
|
-
const memberChoices = choiceList.filter((ch) => memberNames.includes(ch.name));
|
|
532
|
-
groupChoice.enabled = memberChoices.length > 0 && memberChoices.every((ch) => ch.enabled);
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
const selectedBranches: string[] = await new MultiSelectWithGroupSelectAll({
|
|
538
|
-
message,
|
|
539
|
-
choices,
|
|
540
|
-
// 使用空心圆/实心圆作为选中指示符
|
|
541
|
-
symbols: {
|
|
542
|
-
indicator: { on: '●', off: '○' },
|
|
543
|
-
},
|
|
544
|
-
}).run();
|
|
545
|
-
|
|
546
|
-
// 过滤掉全选项和组全选项,只返回实际选中的 worktree
|
|
547
|
-
return worktrees.filter((wt) => selectedBranches.includes(wt.branch));
|
|
548
|
-
}
|
|
318
|
+
export { promptGroupedMultiSelectBranches } from './ui-prompts.js';
|