@jhorst11/wt 2.0.2 → 2.2.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/README.md +2 -0
- package/dist/bin/wt.d.ts +3 -0
- package/dist/bin/wt.d.ts.map +1 -0
- package/dist/bin/wt.js +99 -0
- package/dist/bin/wt.js.map +1 -0
- package/dist/src/commands.d.ts +9 -0
- package/dist/src/commands.d.ts.map +1 -0
- package/dist/src/commands.js +1101 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/config.d.ts +51 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +384 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/git.d.ts +55 -0
- package/dist/src/git.d.ts.map +1 -0
- package/dist/src/git.js +387 -0
- package/dist/src/git.js.map +1 -0
- package/dist/src/setup.d.ts +8 -0
- package/dist/src/setup.d.ts.map +1 -0
- package/dist/src/setup.js +245 -0
- package/dist/src/setup.js.map +1 -0
- package/dist/src/types.d.ts +70 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/ui.d.ts +93 -0
- package/dist/src/ui.d.ts.map +1 -0
- package/dist/src/ui.js +273 -0
- package/dist/src/ui.js.map +1 -0
- package/package.json +20 -6
- package/bin/wt.js +0 -88
- package/shell/wt.sh +0 -66
- package/src/commands.js +0 -1019
- package/src/config.js +0 -426
- package/src/git.js +0 -416
- package/src/setup.js +0 -267
- package/src/ui.js +0 -302
|
@@ -0,0 +1,1101 @@
|
|
|
1
|
+
import { select, input, confirm, search } from '@inquirer/prompts';
|
|
2
|
+
import { ExitPromptError } from '@inquirer/core';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { showLogo, showMiniLogo, success, error, warning, info, heading, subheading, listItem, worktreeItem, divider, spacer, colors, icons, formatBranchChoice, formatWorktreeChoice, setTabColor, resetTabColor, colorIndicator, } from './ui.js';
|
|
5
|
+
import { showCdHint, checkWrapperInRcFile, setupCommand } from './setup.js';
|
|
6
|
+
import { resolveConfig, runHooks, assignWorktreeColor, getWorktreeColor, removeWorktreeColor } from './config.js';
|
|
7
|
+
import { isGitRepo, getRepoRoot, getCurrentBranch, getLocalBranches, getRemoteBranches, getAllBranches, getWorktreesInBase, getMainRepoPath, createWorktree, removeWorktree, buildBranchName, isValidBranchName, getWorktreesBase, mergeBranch, getMainBranch, hasUncommittedChanges, deleteBranch, getCurrentWorktreeInfo, } from './git.js';
|
|
8
|
+
function isUserCancellation(err) {
|
|
9
|
+
return err instanceof ExitPromptError || (err instanceof Error && err.message === 'User force closed the prompt with 0 null');
|
|
10
|
+
}
|
|
11
|
+
function handlePromptError(err) {
|
|
12
|
+
if (isUserCancellation(err)) {
|
|
13
|
+
spacer();
|
|
14
|
+
info('Cancelled');
|
|
15
|
+
spacer();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
20
|
+
function findWorktreeByName(worktrees, name) {
|
|
21
|
+
const exact = worktrees.find((wt) => wt.name === name);
|
|
22
|
+
if (exact)
|
|
23
|
+
return exact;
|
|
24
|
+
const partial = worktrees.filter((wt) => wt.name.includes(name));
|
|
25
|
+
if (partial.length === 1)
|
|
26
|
+
return partial[0];
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
async function ensureGitRepo() {
|
|
30
|
+
if (!(await isGitRepo())) {
|
|
31
|
+
error('Not in a git repository');
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function mainMenu() {
|
|
36
|
+
showLogo();
|
|
37
|
+
await ensureGitRepo();
|
|
38
|
+
const repoRoot = await getRepoRoot();
|
|
39
|
+
if (!repoRoot) {
|
|
40
|
+
error('Not in a git repository');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const currentBranch = await getCurrentBranch();
|
|
44
|
+
const config = resolveConfig(process.cwd(), repoRoot);
|
|
45
|
+
const worktrees = await getWorktreesInBase(repoRoot, config);
|
|
46
|
+
const currentWt = await getCurrentWorktreeInfo(repoRoot, config);
|
|
47
|
+
const branchDisplay = currentBranch && currentBranch !== 'HEAD'
|
|
48
|
+
? colors.branch(currentBranch)
|
|
49
|
+
: colors.warning('detached HEAD');
|
|
50
|
+
subheading(` 📍 ${colors.path(repoRoot)}`);
|
|
51
|
+
subheading(` 🌿 ${branchDisplay}`);
|
|
52
|
+
if (currentWt) {
|
|
53
|
+
const wtColor = getWorktreeColor(repoRoot, currentWt.name);
|
|
54
|
+
const colorDot = colorIndicator(wtColor);
|
|
55
|
+
subheading(` ${colorDot} ${colors.highlight(currentWt.name)}`);
|
|
56
|
+
}
|
|
57
|
+
spacer();
|
|
58
|
+
const wrapperStatus = checkWrapperInRcFile();
|
|
59
|
+
if (!wrapperStatus.installed) {
|
|
60
|
+
console.log(` ${icons.warning} ${colors.warning('Shell integration not configured')} ${colors.muted('— directory jumping is disabled')}`);
|
|
61
|
+
console.log(` ${colors.muted(' Run')} ${colors.secondary('wt setup')} ${colors.muted('or select Setup below to enable auto-navigation')}`);
|
|
62
|
+
spacer();
|
|
63
|
+
}
|
|
64
|
+
const choices = [
|
|
65
|
+
{
|
|
66
|
+
name: `${icons.plus} Create new worktree`,
|
|
67
|
+
value: 'new',
|
|
68
|
+
description: 'Create a new worktree from a branch',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: `${icons.folder} List worktrees`,
|
|
72
|
+
value: 'list',
|
|
73
|
+
description: 'View all worktrees for this repo',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: `${icons.trash} Remove worktree`,
|
|
77
|
+
value: 'remove',
|
|
78
|
+
description: 'Delete a worktree',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: `${icons.home} Go home`,
|
|
82
|
+
value: 'home',
|
|
83
|
+
description: 'Return to the main repository',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
if (worktrees.length > 0) {
|
|
87
|
+
choices.splice(1, 0, {
|
|
88
|
+
name: `${icons.rocket} Jump to worktree`,
|
|
89
|
+
value: 'go',
|
|
90
|
+
description: 'Switch to an existing worktree',
|
|
91
|
+
});
|
|
92
|
+
choices.splice(3, 0, {
|
|
93
|
+
name: `🔀 Merge worktree`,
|
|
94
|
+
value: 'merge',
|
|
95
|
+
description: 'Merge a worktree branch back to main',
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
if (!wrapperStatus.installed) {
|
|
99
|
+
choices.push({
|
|
100
|
+
name: `${icons.sparkles} Setup shell integration`,
|
|
101
|
+
value: 'setup',
|
|
102
|
+
description: 'Enable auto-navigation for wt go, wt home, and wt new',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
choices.push({
|
|
106
|
+
name: `${colors.muted(icons.cross + ' Exit')}`,
|
|
107
|
+
value: 'exit',
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
const action = await select({
|
|
111
|
+
message: 'What would you like to do?',
|
|
112
|
+
choices,
|
|
113
|
+
theme: {
|
|
114
|
+
prefix: icons.tree,
|
|
115
|
+
style: {
|
|
116
|
+
highlight: (text) => colors.primary(text),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
switch (action) {
|
|
121
|
+
case 'new':
|
|
122
|
+
await createWorktreeFlow();
|
|
123
|
+
break;
|
|
124
|
+
case 'list':
|
|
125
|
+
await listWorktrees();
|
|
126
|
+
break;
|
|
127
|
+
case 'remove':
|
|
128
|
+
await removeWorktreeFlow();
|
|
129
|
+
break;
|
|
130
|
+
case 'merge':
|
|
131
|
+
await mergeWorktreeFlow();
|
|
132
|
+
break;
|
|
133
|
+
case 'home':
|
|
134
|
+
await goHome();
|
|
135
|
+
break;
|
|
136
|
+
case 'go':
|
|
137
|
+
await goToWorktree();
|
|
138
|
+
break;
|
|
139
|
+
case 'setup':
|
|
140
|
+
await setupCommand();
|
|
141
|
+
break;
|
|
142
|
+
case 'exit':
|
|
143
|
+
spacer();
|
|
144
|
+
info('Goodbye! ' + icons.sparkles);
|
|
145
|
+
spacer();
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
handlePromptError(err);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export async function createWorktreeFlow(options = {}) {
|
|
154
|
+
await ensureGitRepo();
|
|
155
|
+
const repoRoot = await getRepoRoot();
|
|
156
|
+
if (!repoRoot) {
|
|
157
|
+
error('Not in a git repository');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const config = resolveConfig(process.cwd(), repoRoot);
|
|
161
|
+
const currentWt = await getCurrentWorktreeInfo(repoRoot, config);
|
|
162
|
+
const wtColor = currentWt ? getWorktreeColor(repoRoot, currentWt.name) : null;
|
|
163
|
+
showMiniLogo(currentWt ? { ...currentWt, color: wtColor } : null);
|
|
164
|
+
heading(`${icons.plus} Create New Worktree`);
|
|
165
|
+
const currentBranch = await getCurrentBranch();
|
|
166
|
+
const isDetached = !currentBranch || currentBranch === 'HEAD';
|
|
167
|
+
// Non-interactive mode: name + from provided
|
|
168
|
+
const nonInteractive = !!(options.name && options.from);
|
|
169
|
+
try {
|
|
170
|
+
let baseBranch = null;
|
|
171
|
+
let branchName = null;
|
|
172
|
+
let worktreeName = null;
|
|
173
|
+
if (nonInteractive) {
|
|
174
|
+
worktreeName = options.name.trim().replace(/ /g, '-');
|
|
175
|
+
baseBranch = options.from;
|
|
176
|
+
if (!isValidBranchName(worktreeName)) {
|
|
177
|
+
error(`Invalid worktree name: "${worktreeName}"`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
branchName = buildBranchName(worktreeName, config);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Step 1: Choose source type
|
|
184
|
+
const sourceChoices = [];
|
|
185
|
+
if (!isDetached && currentBranch) {
|
|
186
|
+
sourceChoices.push({
|
|
187
|
+
name: `${icons.branch} Current branch (${colors.branch(currentBranch)})`,
|
|
188
|
+
value: 'current',
|
|
189
|
+
description: 'Create from your current branch',
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
sourceChoices.push({
|
|
193
|
+
name: `${icons.local} Local branch`,
|
|
194
|
+
value: 'local',
|
|
195
|
+
description: 'Choose from existing local branches',
|
|
196
|
+
}, {
|
|
197
|
+
name: `${icons.remote} Remote branch`,
|
|
198
|
+
value: 'remote',
|
|
199
|
+
description: 'Choose from remote branches',
|
|
200
|
+
}, {
|
|
201
|
+
name: `${icons.sparkles} New branch`,
|
|
202
|
+
value: 'new',
|
|
203
|
+
description: 'Create a fresh branch from a base',
|
|
204
|
+
});
|
|
205
|
+
if (options.from) {
|
|
206
|
+
baseBranch = options.from;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
const sourceType = await select({
|
|
210
|
+
message: 'What do you want to base your worktree on?',
|
|
211
|
+
choices: sourceChoices,
|
|
212
|
+
theme: {
|
|
213
|
+
prefix: icons.tree,
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
if (sourceType === 'current') {
|
|
217
|
+
baseBranch = currentBranch;
|
|
218
|
+
}
|
|
219
|
+
else if (sourceType === 'local') {
|
|
220
|
+
const branches = await getLocalBranches();
|
|
221
|
+
if (branches.length === 0) {
|
|
222
|
+
error('No local branches found');
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const branchChoices = branches.map((b) => ({
|
|
226
|
+
name: formatBranchChoice(b.name, 'local'),
|
|
227
|
+
value: b.name,
|
|
228
|
+
description: b.isCurrent ? '(current)' : undefined,
|
|
229
|
+
}));
|
|
230
|
+
baseBranch = await select({
|
|
231
|
+
message: 'Select a local branch:',
|
|
232
|
+
choices: branchChoices,
|
|
233
|
+
theme: { prefix: icons.local },
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
else if (sourceType === 'remote') {
|
|
237
|
+
const spinner = ora({
|
|
238
|
+
text: 'Fetching remote branches...',
|
|
239
|
+
color: 'magenta',
|
|
240
|
+
}).start();
|
|
241
|
+
const remoteBranches = await getRemoteBranches();
|
|
242
|
+
spinner.stop();
|
|
243
|
+
if (remoteBranches.length === 0) {
|
|
244
|
+
error('No remote branches found');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
// Use search for large branch lists
|
|
248
|
+
if (remoteBranches.length > 10) {
|
|
249
|
+
baseBranch = await search({
|
|
250
|
+
message: 'Search for a remote branch:',
|
|
251
|
+
source: async (term) => {
|
|
252
|
+
const filtered = term
|
|
253
|
+
? remoteBranches.filter((b) => b.name.toLowerCase().includes(term.toLowerCase()))
|
|
254
|
+
: remoteBranches.slice(0, 15);
|
|
255
|
+
return filtered.map((b) => ({
|
|
256
|
+
name: formatBranchChoice(b.name, 'remote'),
|
|
257
|
+
value: `origin/${b.name}`,
|
|
258
|
+
}));
|
|
259
|
+
},
|
|
260
|
+
theme: { prefix: icons.remote },
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const branchChoices = remoteBranches.map((b) => ({
|
|
265
|
+
name: formatBranchChoice(b.name, 'remote'),
|
|
266
|
+
value: `origin/${b.name}`,
|
|
267
|
+
}));
|
|
268
|
+
baseBranch = await select({
|
|
269
|
+
message: 'Select a remote branch:',
|
|
270
|
+
choices: branchChoices,
|
|
271
|
+
theme: { prefix: icons.remote },
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
else if (sourceType === 'new') {
|
|
276
|
+
const branches = await getAllBranches();
|
|
277
|
+
if (branches.all.length === 0) {
|
|
278
|
+
error('No branches found. Make sure you have at least one commit.');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const allChoices = branches.all.map((b) => ({
|
|
282
|
+
name: formatBranchChoice(b.name, b.type || 'local'),
|
|
283
|
+
value: b.type === 'remote' ? `origin/${b.name}` : b.name,
|
|
284
|
+
}));
|
|
285
|
+
baseBranch = await select({
|
|
286
|
+
message: 'Select base branch for your new branch:',
|
|
287
|
+
choices: allChoices,
|
|
288
|
+
theme: { prefix: icons.branch },
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (options.name) {
|
|
293
|
+
worktreeName = options.name.trim().replace(/ /g, '-');
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
// Step 2: Get worktree name
|
|
297
|
+
spacer();
|
|
298
|
+
worktreeName = await input({
|
|
299
|
+
message: 'Worktree name (also used as directory and branch name):',
|
|
300
|
+
theme: { prefix: icons.folder },
|
|
301
|
+
validate: (value) => {
|
|
302
|
+
if (!value.trim())
|
|
303
|
+
return 'Name is required';
|
|
304
|
+
if (!isValidBranchName(value.trim()))
|
|
305
|
+
return 'Invalid name (avoid spaces and special characters)';
|
|
306
|
+
return true;
|
|
307
|
+
},
|
|
308
|
+
transformer: (value) => colors.highlight(value),
|
|
309
|
+
});
|
|
310
|
+
worktreeName = worktreeName.trim().replace(/ /g, '-');
|
|
311
|
+
}
|
|
312
|
+
// Build branch name with hierarchical config resolution
|
|
313
|
+
branchName = buildBranchName(worktreeName, config);
|
|
314
|
+
}
|
|
315
|
+
// Show summary and confirm (skip confirmation in non-interactive mode)
|
|
316
|
+
spacer();
|
|
317
|
+
divider();
|
|
318
|
+
info(`Worktree: ${colors.highlight(worktreeName)}`);
|
|
319
|
+
info(`Branch: ${colors.branch(branchName)}`);
|
|
320
|
+
info(`Base: ${colors.muted(baseBranch || 'HEAD')}`);
|
|
321
|
+
info(`Path: ${colors.path(getWorktreesBase(repoRoot, config) + '/' + worktreeName)}`);
|
|
322
|
+
const postCreateHooks = config.hooks?.['post-create'];
|
|
323
|
+
if (postCreateHooks?.length) {
|
|
324
|
+
if (options.hooks === false) {
|
|
325
|
+
info(`Hooks: ${colors.muted('skipped (--no-hooks)')}`);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
info(`Hooks: ${colors.muted(`post-create (${postCreateHooks.length} command${postCreateHooks.length === 1 ? '' : 's'})`)}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
divider();
|
|
332
|
+
spacer();
|
|
333
|
+
if (!nonInteractive) {
|
|
334
|
+
const confirmed = await confirm({
|
|
335
|
+
message: 'Create this worktree?',
|
|
336
|
+
default: true,
|
|
337
|
+
theme: { prefix: icons.tree },
|
|
338
|
+
});
|
|
339
|
+
if (!confirmed) {
|
|
340
|
+
warning('Cancelled');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Create worktree
|
|
345
|
+
spacer();
|
|
346
|
+
const spinner = ora({
|
|
347
|
+
text: 'Creating worktree...',
|
|
348
|
+
color: 'magenta',
|
|
349
|
+
}).start();
|
|
350
|
+
try {
|
|
351
|
+
const result = await createWorktree(worktreeName, branchName, baseBranch);
|
|
352
|
+
if (!result.success) {
|
|
353
|
+
spinner.fail(colors.error('Failed to create worktree'));
|
|
354
|
+
error(result.error || 'Unknown error');
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
spinner.succeed(colors.success('Worktree created!'));
|
|
358
|
+
spacer();
|
|
359
|
+
const worktreeColor = assignWorktreeColor(repoRoot, worktreeName);
|
|
360
|
+
setTabColor(worktreeColor);
|
|
361
|
+
const colorDot = colorIndicator(worktreeColor);
|
|
362
|
+
success(`${colorDot} Created worktree at ${colors.path(result.path || '')}`);
|
|
363
|
+
if (result.branchCreated) {
|
|
364
|
+
success(`Created new branch ${colors.branch(branchName)}`);
|
|
365
|
+
}
|
|
366
|
+
else if (result.branchSource === 'updated-from-remote') {
|
|
367
|
+
info(`Updated branch ${colors.branch(branchName)} to match remote`);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
info(`Using existing branch ${colors.branch(branchName)} (${result.branchSource || 'unknown'})`);
|
|
371
|
+
}
|
|
372
|
+
// Run post-create hooks
|
|
373
|
+
const hookCommands = config.hooks?.['post-create'];
|
|
374
|
+
if (options.hooks === false) {
|
|
375
|
+
info(colors.muted('Skipping post-create hooks (--no-hooks)'));
|
|
376
|
+
}
|
|
377
|
+
else if (hookCommands && hookCommands.length > 0 && result.path) {
|
|
378
|
+
spacer();
|
|
379
|
+
const hookSpinner = ora({
|
|
380
|
+
text: 'Running post-create hooks...',
|
|
381
|
+
color: 'magenta',
|
|
382
|
+
}).start();
|
|
383
|
+
const hookResults = await runHooks('post-create', config, { source: repoRoot, path: result.path, branch: branchName, name: worktreeName, color: worktreeColor }, {
|
|
384
|
+
verbose: options.verbose,
|
|
385
|
+
onCommandStart: (cmd, i, total) => {
|
|
386
|
+
hookSpinner.text = total > 1
|
|
387
|
+
? `Running post-create hooks... (${i}/${total}: ${cmd})`
|
|
388
|
+
: `Running post-create hooks... (${cmd})`;
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
const failed = hookResults.filter((r) => !r.success);
|
|
392
|
+
if (failed.length === 0) {
|
|
393
|
+
hookSpinner.succeed(colors.success(`Ran ${hookResults.length} post-create hook${hookResults.length === 1 ? '' : 's'}`));
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
hookSpinner.warn(colors.warning(`${failed.length} of ${hookResults.length} hook${hookResults.length === 1 ? '' : 's'} failed`));
|
|
397
|
+
for (const f of failed) {
|
|
398
|
+
warning(`Hook failed: ${colors.muted(f.command)}`);
|
|
399
|
+
if (f.error)
|
|
400
|
+
info(colors.muted(f.error));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (result.path) {
|
|
405
|
+
showCdHint(result.path);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
spinner.fail(colors.error('Failed to create worktree'));
|
|
410
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
411
|
+
error(errorMessage);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
handlePromptError(err);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
export async function listWorktrees() {
|
|
419
|
+
await ensureGitRepo();
|
|
420
|
+
const repoRoot = await getRepoRoot();
|
|
421
|
+
if (!repoRoot) {
|
|
422
|
+
error('Not in a git repository');
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const config = resolveConfig(process.cwd(), repoRoot);
|
|
426
|
+
const worktrees = await getWorktreesInBase(repoRoot, config);
|
|
427
|
+
const currentPath = process.cwd();
|
|
428
|
+
const currentWt = await getCurrentWorktreeInfo(repoRoot, config);
|
|
429
|
+
const wtColor = currentWt ? getWorktreeColor(repoRoot, currentWt.name) : null;
|
|
430
|
+
showMiniLogo(currentWt ? { ...currentWt, color: wtColor } : null);
|
|
431
|
+
heading(`${icons.folder} Worktrees`);
|
|
432
|
+
if (worktrees.length === 0) {
|
|
433
|
+
info('No worktrees found for this repository');
|
|
434
|
+
spacer();
|
|
435
|
+
console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
|
|
436
|
+
spacer();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
subheading(`Found ${worktrees.length} worktree${worktrees.length === 1 ? '' : 's'}:`);
|
|
440
|
+
spacer();
|
|
441
|
+
for (const wt of worktrees) {
|
|
442
|
+
const isCurrent = currentPath === wt.path || currentPath.startsWith(wt.path + '/');
|
|
443
|
+
const wtColor = getWorktreeColor(repoRoot, wt.name);
|
|
444
|
+
worktreeItem(wt.name, wt.path, isCurrent, wtColor);
|
|
445
|
+
const branchDisplay = wt.branch === 'unknown'
|
|
446
|
+
? colors.warning('detached HEAD')
|
|
447
|
+
: colors.branch(wt.branch);
|
|
448
|
+
console.log(` ${icons.branch} ${branchDisplay}`);
|
|
449
|
+
spacer();
|
|
450
|
+
}
|
|
451
|
+
divider();
|
|
452
|
+
console.log(` ${colors.muted('Main repo:')} ${colors.path(repoRoot)}`);
|
|
453
|
+
spacer();
|
|
454
|
+
}
|
|
455
|
+
export async function removeWorktreeFlow(options = {}) {
|
|
456
|
+
await ensureGitRepo();
|
|
457
|
+
const repoRoot = await getRepoRoot();
|
|
458
|
+
if (!repoRoot) {
|
|
459
|
+
error('Not in a git repository');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const config = resolveConfig(process.cwd(), repoRoot);
|
|
463
|
+
const currentWt = await getCurrentWorktreeInfo(repoRoot, config);
|
|
464
|
+
const wtColor = currentWt ? getWorktreeColor(repoRoot, currentWt.name) : null;
|
|
465
|
+
showMiniLogo(currentWt ? { ...currentWt, color: wtColor } : null);
|
|
466
|
+
heading(`${icons.trash} Remove Worktree`);
|
|
467
|
+
const worktrees = await getWorktreesInBase(repoRoot, config);
|
|
468
|
+
const currentPath = process.cwd();
|
|
469
|
+
if (worktrees.length === 0) {
|
|
470
|
+
info('No worktrees found to remove');
|
|
471
|
+
spacer();
|
|
472
|
+
console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
|
|
473
|
+
spacer();
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
try {
|
|
477
|
+
let selected = null;
|
|
478
|
+
if (options.name) {
|
|
479
|
+
// Non-interactive: find by name
|
|
480
|
+
selected = findWorktreeByName(worktrees, options.name);
|
|
481
|
+
if (!selected) {
|
|
482
|
+
error(`Worktree "${options.name}" not found`);
|
|
483
|
+
spacer();
|
|
484
|
+
info('Available worktrees:');
|
|
485
|
+
worktrees.forEach((wt) => listItem(`${wt.name} ${colors.muted(`→ ${wt.branch}`)}`));
|
|
486
|
+
spacer();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
// Interactive selection
|
|
492
|
+
const choices = worktrees.map((wt) => {
|
|
493
|
+
const isCurrent = currentPath === wt.path || currentPath.startsWith(wt.path + '/');
|
|
494
|
+
const currentLabel = isCurrent ? colors.warning(' (you are here)') : '';
|
|
495
|
+
const wtColor = getWorktreeColor(repoRoot, wt.name);
|
|
496
|
+
return {
|
|
497
|
+
name: formatWorktreeChoice(wt, wtColor) + currentLabel,
|
|
498
|
+
value: wt,
|
|
499
|
+
description: wt.path,
|
|
500
|
+
};
|
|
501
|
+
});
|
|
502
|
+
choices.push({
|
|
503
|
+
name: `${colors.muted(icons.cross + ' Cancel')}`,
|
|
504
|
+
value: null,
|
|
505
|
+
});
|
|
506
|
+
selected = await select({
|
|
507
|
+
message: 'Select worktree to remove:',
|
|
508
|
+
choices,
|
|
509
|
+
theme: { prefix: icons.trash },
|
|
510
|
+
});
|
|
511
|
+
if (!selected) {
|
|
512
|
+
info('Cancelled');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Warn if user is inside the worktree they're removing
|
|
517
|
+
const isInsideSelected = currentPath === selected.path || currentPath.startsWith(selected.path + '/');
|
|
518
|
+
if (isInsideSelected) {
|
|
519
|
+
spacer();
|
|
520
|
+
warning('You are currently inside this worktree!');
|
|
521
|
+
info(`You will need to ${colors.primary('cd')} out after removal.`);
|
|
522
|
+
}
|
|
523
|
+
spacer();
|
|
524
|
+
const selectedColor = getWorktreeColor(repoRoot, selected.name);
|
|
525
|
+
const colorDot = colorIndicator(selectedColor);
|
|
526
|
+
warning(`${colorDot} This will remove: ${colors.path(selected.path)}`);
|
|
527
|
+
const preDestroyHooks = config.hooks?.['pre-destroy'];
|
|
528
|
+
if (preDestroyHooks?.length) {
|
|
529
|
+
if (options.hooks === false) {
|
|
530
|
+
info(`Hooks: ${colors.muted('skipped (--no-hooks)')}`);
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
info(`Hooks: ${colors.muted(`pre-destroy (${preDestroyHooks.length} command${preDestroyHooks.length === 1 ? '' : 's'}) will run first`)}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
spacer();
|
|
537
|
+
if (!options.force) {
|
|
538
|
+
const confirmed = await confirm({
|
|
539
|
+
message: `Are you sure you want to remove "${selected.name}"?`,
|
|
540
|
+
default: false,
|
|
541
|
+
theme: { prefix: icons.warning },
|
|
542
|
+
});
|
|
543
|
+
if (!confirmed) {
|
|
544
|
+
info('Cancelled');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
// Run pre-destroy hooks
|
|
549
|
+
if (options.hooks !== false) {
|
|
550
|
+
const preDestroyCommands = config.hooks?.['pre-destroy'];
|
|
551
|
+
if (preDestroyCommands && preDestroyCommands.length > 0) {
|
|
552
|
+
spacer();
|
|
553
|
+
const hookSpinner = ora({
|
|
554
|
+
text: 'Running pre-destroy hooks...',
|
|
555
|
+
color: 'magenta',
|
|
556
|
+
}).start();
|
|
557
|
+
const hookResults = await runHooks('pre-destroy', config, { source: repoRoot, path: selected.path, branch: selected.branch, name: selected.name, color: getWorktreeColor(repoRoot, selected.name) }, {
|
|
558
|
+
verbose: options.verbose,
|
|
559
|
+
onCommandStart: (cmd, i, total) => {
|
|
560
|
+
hookSpinner.text = total > 1
|
|
561
|
+
? `Running pre-destroy hooks... (${i}/${total}: ${cmd})`
|
|
562
|
+
: `Running pre-destroy hooks... (${cmd})`;
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
const failed = hookResults.filter((r) => !r.success);
|
|
566
|
+
if (failed.length === 0) {
|
|
567
|
+
hookSpinner.succeed(colors.success(`Ran ${hookResults.length} pre-destroy hook${hookResults.length === 1 ? '' : 's'}`));
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
hookSpinner.warn(colors.warning(`${failed.length} of ${hookResults.length} hook${hookResults.length === 1 ? '' : 's'} failed`));
|
|
571
|
+
for (const f of failed) {
|
|
572
|
+
warning(`Hook failed: ${colors.muted(f.command)}`);
|
|
573
|
+
if (f.error)
|
|
574
|
+
info(colors.muted(f.error));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
spacer();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
info(colors.muted('Skipping pre-destroy hooks (--no-hooks)'));
|
|
582
|
+
}
|
|
583
|
+
const spinner = ora({
|
|
584
|
+
text: 'Removing worktree...',
|
|
585
|
+
color: 'yellow',
|
|
586
|
+
}).start();
|
|
587
|
+
try {
|
|
588
|
+
// First try normal remove, then force if needed
|
|
589
|
+
try {
|
|
590
|
+
await removeWorktree(selected.path, !!options.force);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
if (options.force) {
|
|
594
|
+
throw new Error('Force remove failed');
|
|
595
|
+
}
|
|
596
|
+
// Stop spinner before showing interactive prompt
|
|
597
|
+
spinner.stop();
|
|
598
|
+
warning('Worktree has uncommitted or untracked changes.');
|
|
599
|
+
const forceRemove = await confirm({
|
|
600
|
+
message: 'Force remove anyway? (changes will be lost)',
|
|
601
|
+
default: false,
|
|
602
|
+
theme: { prefix: icons.warning },
|
|
603
|
+
});
|
|
604
|
+
if (forceRemove) {
|
|
605
|
+
spinner.start('Force removing worktree...');
|
|
606
|
+
await removeWorktree(selected.path, true);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
info('Aborted. Commit or stash your changes first.');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
spinner.succeed(colors.success('Worktree removed!'));
|
|
614
|
+
spacer();
|
|
615
|
+
const removedColor = getWorktreeColor(repoRoot, selected.name);
|
|
616
|
+
const removedColorDot = colorIndicator(removedColor);
|
|
617
|
+
removeWorktreeColor(repoRoot, selected.name);
|
|
618
|
+
success(`${removedColorDot} Removed ${colors.highlight(selected.name)}`);
|
|
619
|
+
if (isInsideSelected) {
|
|
620
|
+
spacer();
|
|
621
|
+
const mainPath = await getMainRepoPath();
|
|
622
|
+
if (mainPath) {
|
|
623
|
+
info(`Run ${colors.primary('wt home')} or ${colors.primary(`cd "${mainPath}"`)} to return to the main repo.`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
spacer();
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
spinner.fail(colors.error('Failed to remove worktree'));
|
|
630
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
631
|
+
error(errorMessage);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (err) {
|
|
635
|
+
handlePromptError(err);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
export async function mergeWorktreeFlow(options = {}) {
|
|
639
|
+
await ensureGitRepo();
|
|
640
|
+
const repoRoot = await getRepoRoot();
|
|
641
|
+
if (!repoRoot) {
|
|
642
|
+
error('Not in a git repository');
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const config = resolveConfig(process.cwd(), repoRoot);
|
|
646
|
+
const currentWt = await getCurrentWorktreeInfo(repoRoot, config);
|
|
647
|
+
const wtColor = currentWt ? getWorktreeColor(repoRoot, currentWt.name) : null;
|
|
648
|
+
showMiniLogo(currentWt ? { ...currentWt, color: wtColor } : null);
|
|
649
|
+
heading(`🔀 Merge Worktree`);
|
|
650
|
+
const mainPath = await getMainRepoPath();
|
|
651
|
+
const worktrees = await getWorktreesInBase(repoRoot, config);
|
|
652
|
+
const currentPath = process.cwd();
|
|
653
|
+
const isAtHome = currentPath === mainPath;
|
|
654
|
+
if (worktrees.length === 0) {
|
|
655
|
+
info('No worktrees found to merge');
|
|
656
|
+
spacer();
|
|
657
|
+
console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
|
|
658
|
+
spacer();
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
// Determine if we have enough args for non-interactive merge
|
|
662
|
+
const nonInteractive = !!(options.name && options.into);
|
|
663
|
+
try {
|
|
664
|
+
let selectedWt = null;
|
|
665
|
+
let targetBranch = null;
|
|
666
|
+
if (options.name) {
|
|
667
|
+
// Find worktree by name
|
|
668
|
+
selectedWt = findWorktreeByName(worktrees, options.name);
|
|
669
|
+
if (!selectedWt) {
|
|
670
|
+
error(`Worktree "${options.name}" not found`);
|
|
671
|
+
spacer();
|
|
672
|
+
info('Available worktrees:');
|
|
673
|
+
worktrees.forEach((wt) => listItem(`${wt.name} ${colors.muted(`→ ${wt.branch}`)}`));
|
|
674
|
+
spacer();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
// Select worktree to merge
|
|
680
|
+
const wtChoices = worktrees.map((wt) => {
|
|
681
|
+
const wtColor = getWorktreeColor(repoRoot, wt.name);
|
|
682
|
+
return {
|
|
683
|
+
name: formatWorktreeChoice(wt, wtColor),
|
|
684
|
+
value: wt,
|
|
685
|
+
description: wt.path,
|
|
686
|
+
};
|
|
687
|
+
});
|
|
688
|
+
wtChoices.push({
|
|
689
|
+
name: `${colors.muted(icons.cross + ' Cancel')}`,
|
|
690
|
+
value: null,
|
|
691
|
+
});
|
|
692
|
+
selectedWt = await select({
|
|
693
|
+
message: 'Select worktree branch to merge:',
|
|
694
|
+
choices: wtChoices,
|
|
695
|
+
theme: { prefix: '🔀' },
|
|
696
|
+
});
|
|
697
|
+
if (!selectedWt) {
|
|
698
|
+
info('Cancelled');
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (options.into) {
|
|
703
|
+
targetBranch = options.into;
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// Select target branch
|
|
707
|
+
const mainBranch = await getMainBranch(mainPath || repoRoot);
|
|
708
|
+
const currentBranch = await getCurrentBranch();
|
|
709
|
+
const localBranches = await getLocalBranches(mainPath || repoRoot);
|
|
710
|
+
const targetChoices = [];
|
|
711
|
+
// Add main branch first if it exists
|
|
712
|
+
if (localBranches.some(b => b.name === mainBranch)) {
|
|
713
|
+
targetChoices.push({
|
|
714
|
+
name: `${icons.home} ${colors.branch(mainBranch)} ${colors.muted('(main branch)')}`,
|
|
715
|
+
value: mainBranch,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
// Add current branch if different and we're at home
|
|
719
|
+
if (isAtHome && currentBranch && currentBranch !== mainBranch && currentBranch !== 'HEAD') {
|
|
720
|
+
targetChoices.push({
|
|
721
|
+
name: `${icons.pointer} ${colors.branch(currentBranch)} ${colors.muted('(current)')}`,
|
|
722
|
+
value: currentBranch,
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
// Add other branches (excluding the source worktree branch to prevent merging into itself)
|
|
726
|
+
for (const branch of localBranches) {
|
|
727
|
+
if (branch.name !== mainBranch && branch.name !== currentBranch && branch.name !== selectedWt.branch) {
|
|
728
|
+
targetChoices.push({
|
|
729
|
+
name: `${icons.branch} ${colors.branch(branch.name)}`,
|
|
730
|
+
value: branch.name,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (targetChoices.length === 0) {
|
|
735
|
+
error('No target branches available to merge into');
|
|
736
|
+
spacer();
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
targetChoices.push({
|
|
740
|
+
name: `${colors.muted(icons.cross + ' Cancel')}`,
|
|
741
|
+
value: null,
|
|
742
|
+
});
|
|
743
|
+
spacer();
|
|
744
|
+
targetBranch = await select({
|
|
745
|
+
message: `Merge ${colors.highlight(selectedWt.branch)} into:`,
|
|
746
|
+
choices: targetChoices,
|
|
747
|
+
theme: { prefix: icons.arrowRight },
|
|
748
|
+
});
|
|
749
|
+
if (!targetBranch) {
|
|
750
|
+
info('Cancelled');
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
// Check for uncommitted changes in main repo
|
|
755
|
+
if (mainPath && await hasUncommittedChanges(mainPath)) {
|
|
756
|
+
spacer();
|
|
757
|
+
warning('Main repository has uncommitted changes!');
|
|
758
|
+
if (nonInteractive) {
|
|
759
|
+
error('Aborting: main repository has uncommitted changes. Commit or stash them first.');
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
const proceed = await confirm({
|
|
763
|
+
message: 'Stash changes and continue?',
|
|
764
|
+
default: false,
|
|
765
|
+
theme: { prefix: icons.warning },
|
|
766
|
+
});
|
|
767
|
+
if (!proceed) {
|
|
768
|
+
info('Cancelled. Commit or stash your changes first.');
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
// Stash changes
|
|
772
|
+
const { simpleGit } = await import('simple-git');
|
|
773
|
+
const git = simpleGit(mainPath);
|
|
774
|
+
await git.stash();
|
|
775
|
+
info('Changes stashed');
|
|
776
|
+
}
|
|
777
|
+
// Confirm merge (skip in non-interactive mode)
|
|
778
|
+
spacer();
|
|
779
|
+
divider();
|
|
780
|
+
const selectedColor = getWorktreeColor(repoRoot, selectedWt.name);
|
|
781
|
+
const selectedColorDot = colorIndicator(selectedColor);
|
|
782
|
+
info(`${selectedColorDot} From: ${colors.highlight(selectedWt.branch)} ${colors.muted(`(${selectedWt.name})`)}`);
|
|
783
|
+
info(`Into: ${colors.branch(targetBranch)}`);
|
|
784
|
+
divider();
|
|
785
|
+
spacer();
|
|
786
|
+
if (!nonInteractive) {
|
|
787
|
+
const confirmed = await confirm({
|
|
788
|
+
message: 'Proceed with merge?',
|
|
789
|
+
default: true,
|
|
790
|
+
theme: { prefix: '🔀' },
|
|
791
|
+
});
|
|
792
|
+
if (!confirmed) {
|
|
793
|
+
info('Cancelled');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// Perform merge
|
|
798
|
+
const spinner = ora({
|
|
799
|
+
text: 'Merging...',
|
|
800
|
+
color: 'magenta',
|
|
801
|
+
}).start();
|
|
802
|
+
try {
|
|
803
|
+
await mergeBranch(selectedWt.branch, targetBranch, mainPath || repoRoot);
|
|
804
|
+
spinner.succeed(colors.success('Merged successfully!'));
|
|
805
|
+
spacer();
|
|
806
|
+
success(`Merged ${colors.highlight(selectedWt.branch)} into ${colors.branch(targetBranch)}`);
|
|
807
|
+
// Cleanup: auto if --cleanup, prompt otherwise
|
|
808
|
+
const shouldCleanup = options.cleanup ?? (nonInteractive ? false : await (async () => {
|
|
809
|
+
spacer();
|
|
810
|
+
return confirm({
|
|
811
|
+
message: `Remove the worktree "${selectedWt.name}" now that it's merged?`,
|
|
812
|
+
default: false,
|
|
813
|
+
theme: { prefix: icons.trash },
|
|
814
|
+
});
|
|
815
|
+
})());
|
|
816
|
+
if (shouldCleanup) {
|
|
817
|
+
// Run pre-destroy hooks before removing the worktree
|
|
818
|
+
if (options.hooks !== false) {
|
|
819
|
+
const preDestroyCommands = config.hooks?.['pre-destroy'];
|
|
820
|
+
if (preDestroyCommands && preDestroyCommands.length > 0) {
|
|
821
|
+
spacer();
|
|
822
|
+
const hookSpinner = ora({
|
|
823
|
+
text: 'Running pre-destroy hooks...',
|
|
824
|
+
color: 'magenta',
|
|
825
|
+
}).start();
|
|
826
|
+
const hookResults = await runHooks('pre-destroy', config, { source: repoRoot, path: selectedWt.path, branch: selectedWt.branch, name: selectedWt.name, color: getWorktreeColor(repoRoot, selectedWt.name) }, {
|
|
827
|
+
verbose: options.verbose,
|
|
828
|
+
onCommandStart: (cmd, i, total) => {
|
|
829
|
+
hookSpinner.text = total > 1
|
|
830
|
+
? `Running pre-destroy hooks... (${i}/${total}: ${cmd})`
|
|
831
|
+
: `Running pre-destroy hooks... (${cmd})`;
|
|
832
|
+
},
|
|
833
|
+
});
|
|
834
|
+
const failed = hookResults.filter((r) => !r.success);
|
|
835
|
+
if (failed.length === 0) {
|
|
836
|
+
hookSpinner.succeed(colors.success(`Ran ${hookResults.length} pre-destroy hook${hookResults.length === 1 ? '' : 's'}`));
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
hookSpinner.warn(colors.warning(`${failed.length} of ${hookResults.length} hook${hookResults.length === 1 ? '' : 's'} failed`));
|
|
840
|
+
for (const f of failed) {
|
|
841
|
+
warning(`Hook failed: ${colors.muted(f.command)}`);
|
|
842
|
+
if (f.error)
|
|
843
|
+
info(colors.muted(f.error));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
spacer();
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
else {
|
|
850
|
+
info(colors.muted('Skipping pre-destroy hooks (--no-hooks)'));
|
|
851
|
+
}
|
|
852
|
+
const cleanupSpinner = ora({
|
|
853
|
+
text: 'Cleaning up...',
|
|
854
|
+
color: 'yellow',
|
|
855
|
+
}).start();
|
|
856
|
+
try {
|
|
857
|
+
await removeWorktree(selectedWt.path, false, mainPath || repoRoot);
|
|
858
|
+
removeWorktreeColor(repoRoot, selectedWt.name);
|
|
859
|
+
cleanupSpinner.succeed(colors.success('Worktree removed'));
|
|
860
|
+
if (options.cleanup) {
|
|
861
|
+
// Non-interactive cleanup: also delete the branch
|
|
862
|
+
try {
|
|
863
|
+
await deleteBranch(selectedWt.branch, false, mainPath || repoRoot);
|
|
864
|
+
success(`Branch ${colors.branch(selectedWt.branch)} deleted`);
|
|
865
|
+
}
|
|
866
|
+
catch {
|
|
867
|
+
// Branch deletion is best-effort in non-interactive mode
|
|
868
|
+
warning(`Could not delete branch ${colors.branch(selectedWt.branch)} (may not be fully merged)`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
else {
|
|
872
|
+
// Ask about deleting branch
|
|
873
|
+
const deleteBr = await confirm({
|
|
874
|
+
message: `Delete the branch "${selectedWt.branch}" too?`,
|
|
875
|
+
default: false,
|
|
876
|
+
theme: { prefix: icons.trash },
|
|
877
|
+
});
|
|
878
|
+
if (deleteBr) {
|
|
879
|
+
await deleteBranch(selectedWt.branch, false, mainPath || repoRoot);
|
|
880
|
+
success(`Branch ${colors.branch(selectedWt.branch)} deleted`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
cleanupSpinner.fail('Failed to remove worktree');
|
|
886
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
887
|
+
error(errorMessage);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
spacer();
|
|
891
|
+
console.log(` ${icons.sparkles} ${colors.success('All done!')}`);
|
|
892
|
+
spacer();
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
spinner.fail(colors.error('Merge failed'));
|
|
896
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
897
|
+
error(errorMessage);
|
|
898
|
+
spacer();
|
|
899
|
+
warning('You may need to resolve merge conflicts manually.');
|
|
900
|
+
if (mainPath) {
|
|
901
|
+
info(`Go to the main repo: ${colors.primary(`cd "${mainPath}"`)}`);
|
|
902
|
+
}
|
|
903
|
+
info(`Then resolve conflicts and run: ${colors.primary('git merge --continue')}`);
|
|
904
|
+
spacer();
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
catch (err) {
|
|
908
|
+
handlePromptError(err);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
export async function goHome(options = {}) {
|
|
912
|
+
await ensureGitRepo();
|
|
913
|
+
const repoRoot = await getRepoRoot();
|
|
914
|
+
if (!repoRoot) {
|
|
915
|
+
error('Not in a git repository');
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
const config = resolveConfig(process.cwd(), repoRoot);
|
|
919
|
+
const currentWt = await getCurrentWorktreeInfo(repoRoot, config);
|
|
920
|
+
const wtColor = currentWt ? getWorktreeColor(repoRoot, currentWt.name) : null;
|
|
921
|
+
showMiniLogo(currentWt ? { ...currentWt, color: wtColor } : null);
|
|
922
|
+
const mainPath = await getMainRepoPath();
|
|
923
|
+
const currentPath = process.cwd();
|
|
924
|
+
if (!mainPath) {
|
|
925
|
+
error('Could not find main repository');
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
// Check if we're already home
|
|
929
|
+
if (currentPath === mainPath || currentPath.startsWith(mainPath + '/')) {
|
|
930
|
+
const isExactlyHome = currentPath === mainPath;
|
|
931
|
+
spacer();
|
|
932
|
+
if (isExactlyHome) {
|
|
933
|
+
console.log(` ${icons.home} ${colors.success("You're already home!")} ${icons.sparkles}`);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
console.log(` ${icons.home} ${colors.success("You're in the main repo")} ${icons.sparkles}`);
|
|
937
|
+
}
|
|
938
|
+
console.log(` ${colors.muted('Path:')} ${colors.path(mainPath)}`);
|
|
939
|
+
spacer();
|
|
940
|
+
if (options.delete) {
|
|
941
|
+
warning('Cannot use --delete: you are not inside a worktree');
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
// If --delete, capture current worktree info before navigating home
|
|
946
|
+
const wtToDelete = options.delete ? currentWt : null;
|
|
947
|
+
if (options.delete && !wtToDelete) {
|
|
948
|
+
warning('Cannot use --delete: you are not inside a worktree');
|
|
949
|
+
}
|
|
950
|
+
spacer();
|
|
951
|
+
success(`Heading home... ${icons.home}`);
|
|
952
|
+
console.log(` ${colors.muted('Path:')} ${colors.path(mainPath)}`);
|
|
953
|
+
resetTabColor();
|
|
954
|
+
showCdHint(mainPath);
|
|
955
|
+
// Delete the worktree we just left
|
|
956
|
+
if (wtToDelete) {
|
|
957
|
+
spacer();
|
|
958
|
+
// Run pre-destroy hooks
|
|
959
|
+
if (options.hooks !== false) {
|
|
960
|
+
const preDestroyCommands = config.hooks?.['pre-destroy'];
|
|
961
|
+
if (preDestroyCommands && preDestroyCommands.length > 0) {
|
|
962
|
+
const hookSpinner = ora({
|
|
963
|
+
text: 'Running pre-destroy hooks...',
|
|
964
|
+
color: 'magenta',
|
|
965
|
+
}).start();
|
|
966
|
+
const hookResults = await runHooks('pre-destroy', config, { source: repoRoot, path: wtToDelete.path, branch: wtToDelete.branch, name: wtToDelete.name, color: getWorktreeColor(repoRoot, wtToDelete.name) }, {
|
|
967
|
+
verbose: options.verbose,
|
|
968
|
+
onCommandStart: (cmd, i, total) => {
|
|
969
|
+
hookSpinner.text = total > 1
|
|
970
|
+
? `Running pre-destroy hooks... (${i}/${total}: ${cmd})`
|
|
971
|
+
: `Running pre-destroy hooks... (${cmd})`;
|
|
972
|
+
},
|
|
973
|
+
});
|
|
974
|
+
const failed = hookResults.filter((r) => !r.success);
|
|
975
|
+
if (failed.length === 0) {
|
|
976
|
+
hookSpinner.succeed(colors.success(`Ran ${hookResults.length} pre-destroy hook${hookResults.length === 1 ? '' : 's'}`));
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
hookSpinner.warn(colors.warning(`${failed.length} of ${hookResults.length} hook${hookResults.length === 1 ? '' : 's'} failed`));
|
|
980
|
+
for (const f of failed) {
|
|
981
|
+
warning(`Hook failed: ${colors.muted(f.command)}`);
|
|
982
|
+
if (f.error)
|
|
983
|
+
info(colors.muted(f.error));
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
info(colors.muted('Skipping pre-destroy hooks (--no-hooks)'));
|
|
990
|
+
}
|
|
991
|
+
const spinner = ora({
|
|
992
|
+
text: `Removing worktree "${wtToDelete.name}"...`,
|
|
993
|
+
color: 'yellow',
|
|
994
|
+
}).start();
|
|
995
|
+
try {
|
|
996
|
+
await removeWorktree(wtToDelete.path, true);
|
|
997
|
+
spinner.succeed(colors.success(`Worktree "${wtToDelete.name}" removed`));
|
|
998
|
+
const deletedColor = getWorktreeColor(repoRoot, wtToDelete.name);
|
|
999
|
+
const deletedColorDot = colorIndicator(deletedColor);
|
|
1000
|
+
removeWorktreeColor(repoRoot, wtToDelete.name);
|
|
1001
|
+
success(`${deletedColorDot} Removed ${colors.highlight(wtToDelete.name)}`);
|
|
1002
|
+
}
|
|
1003
|
+
catch (err) {
|
|
1004
|
+
spinner.fail(colors.error('Failed to remove worktree'));
|
|
1005
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
1006
|
+
error(errorMessage);
|
|
1007
|
+
}
|
|
1008
|
+
spacer();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
export async function goToWorktree(name) {
|
|
1012
|
+
await ensureGitRepo();
|
|
1013
|
+
const repoRoot = await getRepoRoot();
|
|
1014
|
+
if (!repoRoot) {
|
|
1015
|
+
error('Not in a git repository');
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const config = resolveConfig(process.cwd(), repoRoot);
|
|
1019
|
+
const currentWt = await getCurrentWorktreeInfo(repoRoot, config);
|
|
1020
|
+
const wtColor = currentWt ? getWorktreeColor(repoRoot, currentWt.name) : null;
|
|
1021
|
+
showMiniLogo(currentWt ? { ...currentWt, color: wtColor } : null);
|
|
1022
|
+
const worktrees = await getWorktreesInBase(repoRoot, config);
|
|
1023
|
+
if (worktrees.length === 0) {
|
|
1024
|
+
heading(`${icons.rocket} Jump to Worktree`);
|
|
1025
|
+
info('No worktrees found');
|
|
1026
|
+
spacer();
|
|
1027
|
+
console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
|
|
1028
|
+
spacer();
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
let selected;
|
|
1032
|
+
if (name) {
|
|
1033
|
+
// Direct jump by name - also try partial/fuzzy match
|
|
1034
|
+
selected = worktrees.find((wt) => wt.name === name);
|
|
1035
|
+
if (!selected) {
|
|
1036
|
+
// Try partial match
|
|
1037
|
+
const partialMatches = worktrees.filter((wt) => wt.name.includes(name));
|
|
1038
|
+
if (partialMatches.length === 1) {
|
|
1039
|
+
selected = partialMatches[0];
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
error(`Worktree "${name}" not found`);
|
|
1043
|
+
spacer();
|
|
1044
|
+
if (partialMatches.length > 1) {
|
|
1045
|
+
info('Did you mean one of these?');
|
|
1046
|
+
partialMatches.forEach((wt) => listItem(`${wt.name} ${colors.muted(`→ ${wt.branch}`)}`));
|
|
1047
|
+
}
|
|
1048
|
+
else {
|
|
1049
|
+
info('Available worktrees:');
|
|
1050
|
+
worktrees.forEach((wt) => listItem(`${wt.name} ${colors.muted(`→ ${wt.branch}`)}`));
|
|
1051
|
+
}
|
|
1052
|
+
spacer();
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
// Interactive selection
|
|
1059
|
+
heading(`${icons.rocket} Jump to Worktree`);
|
|
1060
|
+
const currentPath = process.cwd();
|
|
1061
|
+
try {
|
|
1062
|
+
const choices = worktrees.map((wt) => {
|
|
1063
|
+
const isCurrent = currentPath === wt.path || currentPath.startsWith(wt.path + '/');
|
|
1064
|
+
const currentLabel = isCurrent ? colors.muted(' (current)') : '';
|
|
1065
|
+
const wtColor = getWorktreeColor(repoRoot, wt.name);
|
|
1066
|
+
return {
|
|
1067
|
+
name: formatWorktreeChoice(wt, wtColor) + currentLabel,
|
|
1068
|
+
value: wt,
|
|
1069
|
+
description: wt.path,
|
|
1070
|
+
};
|
|
1071
|
+
});
|
|
1072
|
+
choices.push({
|
|
1073
|
+
name: `${colors.muted(icons.cross + ' Cancel')}`,
|
|
1074
|
+
value: null,
|
|
1075
|
+
description: '',
|
|
1076
|
+
});
|
|
1077
|
+
selected = await select({
|
|
1078
|
+
message: 'Select worktree:',
|
|
1079
|
+
choices,
|
|
1080
|
+
theme: { prefix: icons.rocket },
|
|
1081
|
+
}) || undefined;
|
|
1082
|
+
if (!selected) {
|
|
1083
|
+
info('Cancelled');
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
catch (err) {
|
|
1088
|
+
handlePromptError(err);
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
spacer();
|
|
1093
|
+
const selectedColor = getWorktreeColor(repoRoot, selected.name);
|
|
1094
|
+
const selectedColorDot = colorIndicator(selectedColor);
|
|
1095
|
+
success(`${selectedColorDot} Jumping to ${colors.highlight(selected.name)}`);
|
|
1096
|
+
console.log(` ${colors.muted('Path:')} ${colors.path(selected.path)}`);
|
|
1097
|
+
if (selectedColor)
|
|
1098
|
+
setTabColor(selectedColor);
|
|
1099
|
+
showCdHint(selected.path);
|
|
1100
|
+
}
|
|
1101
|
+
//# sourceMappingURL=commands.js.map
|