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