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