@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/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 worktrees = await getWorktreesInBase(repoRoot);
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(` 🌿 ${colors.branch(currentBranch)}`);
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
- const action = await select({
105
- message: 'What would you like to do?',
106
- choices,
107
- theme: {
108
- prefix: icons.tree,
109
- style: {
110
- highlight: (text) => colors.primary(text),
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
- switch (action) {
116
- case 'new':
117
- await createWorktreeFlow();
118
- break;
119
- case 'list':
120
- await listWorktrees();
121
- break;
122
- case 'remove':
123
- await removeWorktreeFlow();
124
- break;
125
- case 'merge':
126
- await mergeWorktreeFlow();
127
- break;
128
- case 'home':
129
- await goHome();
130
- break;
131
- case 'go':
132
- await goToWorktree();
133
- break;
134
- case 'exit':
135
- spacer();
136
- info('Goodbye! ' + icons.sparkles);
137
- spacer();
138
- break;
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
- // Step 1: Choose source type
152
- const sourceType = await select({
153
- message: 'What do you want to base your worktree on?',
154
- choices: [
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 branchChoices = branches.map((b) => ({
195
- name: formatBranchChoice(b.name, 'local'),
196
- value: b.name,
197
- description: b.isCurrent ? '(current)' : undefined,
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
- const remoteBranches = await getRemoteBranches();
212
- spinner.stop();
213
+ let baseBranch = null;
214
+ let branchName = null;
215
+ let worktreeName = null;
213
216
 
214
- if (remoteBranches.length === 0) {
215
- error('No remote branches found');
216
- return;
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
- // Use search for large branch lists
220
- if (remoteBranches.length > 10) {
221
- baseBranch = await search({
222
- message: 'Search for a remote branch:',
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 remote branch:',
233
+ message: 'Select a local branch:',
242
234
  choices: branchChoices,
243
- theme: { prefix: icons.remote },
235
+ theme: { prefix: icons.local },
244
236
  });
245
- }
246
- } else if (sourceType === 'new') {
247
- const branches = await getAllBranches();
248
- const allChoices = branches.all.map((b) => ({
249
- name: formatBranchChoice(b.name, b.type),
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
- baseBranch = await select({
254
- message: 'Select base branch for your new branch:',
255
- choices: allChoices,
256
- theme: { prefix: icons.branch },
257
- });
258
- }
243
+ const remoteBranches = await getRemoteBranches();
244
+ spinner.stop();
259
245
 
260
- // Step 2: Get worktree name
261
- spacer();
246
+ if (remoteBranches.length === 0) {
247
+ error('No remote branches found');
248
+ return;
249
+ }
262
250
 
263
- worktreeName = await input({
264
- message: 'Worktree name (also used as branch name):',
265
- theme: { prefix: icons.folder },
266
- validate: (value) => {
267
- if (!value.trim()) return 'Name is required';
268
- if (!isValidBranchName(value.trim())) return 'Invalid name (avoid spaces and special characters)';
269
- return true;
270
- },
271
- transformer: (value) => colors.highlight(value),
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
- worktreeName = worktreeName.trim().replace(/ /g, '-');
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
- // Build branch name with optional prefix
277
- const config = getConfig();
278
- branchName = buildBranchName(worktreeName, config.branchPrefix);
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
- // Step 3: Confirm
281
- spacer();
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
- const confirmed = await confirm({
291
- message: 'Create this worktree?',
292
- default: true,
293
- theme: { prefix: icons.tree },
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
- if (!confirmed) {
297
- warning('Cancelled');
298
- return;
299
- }
311
+ worktreeName = worktreeName.trim().replace(/ /g, '-');
300
312
 
301
- // Step 4: Create worktree
302
- spacer();
303
- const spinner = ora({
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
- try {
309
- const result = await createWorktree(worktreeName, branchName, baseBranch);
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
- if (!result.success) {
312
- spinner.fail(colors.error('Failed to create worktree'));
313
- error(result.error);
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
- spinner.succeed(colors.success('Worktree created!'));
338
+ // Step 4: Create worktree
318
339
  spacer();
340
+ const spinner = ora({
341
+ text: 'Creating worktree...',
342
+ color: 'magenta',
343
+ }).start();
319
344
 
320
- success(`Created worktree at ${colors.path(result.path)}`);
321
- if (result.branchCreated) {
322
- success(`Created new branch ${colors.branch(branchName)}`);
323
- } else if (result.branchSource === 'updated-from-remote') {
324
- info(`Updated branch ${colors.branch(branchName)} to match remote`);
325
- } else {
326
- info(`Using existing branch ${colors.branch(branchName)} (${result.branchSource})`);
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
- showCdHint(result.path);
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
- spinner.fail(colors.error('Failed to create worktree'));
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 worktrees = await getWorktreesInBase(repoRoot);
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
- console.log(` ${icons.branch} ${colors.branch(wt.branch)}`);
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 worktrees = await getWorktreesInBase(repoRoot);
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
- const choices = worktrees.map((wt) => ({
385
- name: `${icons.folder} ${colors.highlight(wt.name)} ${colors.muted(`→ ${wt.branch}`)}`,
386
- value: wt,
387
- description: wt.path,
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
- choices.push({
391
- name: `${colors.muted(icons.cross + ' Cancel')}`,
392
- value: null,
393
- });
469
+ choices.push({
470
+ name: `${colors.muted(icons.cross + ' Cancel')}`,
471
+ value: null,
472
+ });
394
473
 
395
- const selected = await select({
396
- message: 'Select worktree to remove:',
397
- choices,
398
- theme: { prefix: icons.trash },
399
- });
474
+ const selected = await select({
475
+ message: 'Select worktree to remove:',
476
+ choices,
477
+ theme: { prefix: icons.trash },
478
+ });
400
479
 
401
- if (!selected) {
402
- info('Cancelled');
403
- return;
404
- }
480
+ if (!selected) {
481
+ info('Cancelled');
482
+ return;
483
+ }
405
484
 
406
- spacer();
407
- warning(`This will remove: ${colors.path(selected.path)}`);
408
- spacer();
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
- const confirmed = await confirm({
411
- message: `Are you sure you want to remove "${selected.name}"?`,
412
- default: false,
413
- theme: { prefix: icons.warning },
414
- });
493
+ spacer();
494
+ warning(`This will remove: ${colors.path(selected.path)}`);
495
+ spacer();
415
496
 
416
- if (!confirmed) {
417
- info('Cancelled');
418
- return;
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
- const spinner = ora({
422
- text: 'Removing worktree...',
423
- color: 'yellow',
424
- }).start();
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
- await removeWorktree(selected.path, false);
430
- } catch {
431
- const forceRemove = await confirm({
432
- message: 'Worktree has changes. Force remove?',
433
- default: false,
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
- if (forceRemove) {
437
- await removeWorktree(selected.path, true);
438
- } else {
439
- spinner.fail('Aborted');
440
- return;
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
- spinner.succeed(colors.success('Worktree removed!'));
445
- spacer();
446
- success(`Removed ${colors.highlight(selected.name)}`);
447
- spacer();
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
- spinner.fail(colors.error('Failed to remove worktree'));
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 worktrees = await getWorktreesInBase(repoRoot);
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
- // Select worktree to merge
473
- const wtChoices = worktrees.map((wt) => ({
474
- name: `${icons.folder} ${colors.highlight(wt.name)} ${colors.muted(`→ ${wt.branch}`)}`,
475
- value: wt,
476
- description: wt.path,
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
- const selectedWt = await select({
485
- message: 'Select worktree to merge:',
486
- choices: wtChoices,
487
- theme: { prefix: '🔀' },
488
- });
588
+ wtChoices.push({
589
+ name: `${colors.muted(icons.cross + ' Cancel')}`,
590
+ value: null,
591
+ });
489
592
 
490
- if (!selectedWt) {
491
- info('Cancelled');
492
- return;
493
- }
593
+ const selectedWt = await select({
594
+ message: 'Select worktree branch to merge:',
595
+ choices: wtChoices,
596
+ theme: { prefix: '🔀' },
597
+ });
494
598
 
495
- // Select target branch
496
- const mainBranch = await getMainBranch(mainPath);
497
- const currentBranch = await getCurrentBranch();
498
- const localBranches = await getLocalBranches(mainPath);
599
+ if (!selectedWt) {
600
+ info('Cancelled');
601
+ return;
602
+ }
499
603
 
500
- const targetChoices = [];
604
+ // Select target branch
605
+ const mainBranch = await getMainBranch(mainPath);
606
+ const currentBranch = await getCurrentBranch();
607
+ const localBranches = await getLocalBranches(mainPath);
501
608
 
502
- // Add main branch first if it exists
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
- // Add current branch if different and we're at home
511
- if (isAtHome && currentBranch && currentBranch !== mainBranch) {
512
- targetChoices.push({
513
- name: `${icons.pointer} ${colors.branch(currentBranch)} ${colors.muted('(current)')}`,
514
- value: currentBranch,
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
- // Add other branches
519
- for (const branch of localBranches) {
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.branch} ${colors.branch(branch.name)}`,
523
- value: branch.name,
622
+ name: `${icons.pointer} ${colors.branch(currentBranch)} ${colors.muted('(current)')}`,
623
+ value: currentBranch,
524
624
  });
525
625
  }
526
- }
527
626
 
528
- targetChoices.push({
529
- name: `${colors.muted(icons.cross + ' Cancel')}`,
530
- value: null,
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
- spacer();
534
- const targetBranch = await select({
535
- message: `Merge ${colors.highlight(selectedWt.branch)} into:`,
536
- choices: targetChoices,
537
- theme: { prefix: icons.arrowRight },
538
- });
637
+ if (targetChoices.length === 0) {
638
+ error('No target branches available to merge into');
639
+ spacer();
640
+ return;
641
+ }
539
642
 
540
- if (!targetBranch) {
541
- info('Cancelled');
542
- return;
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
- warning('Main repository has uncommitted changes!');
549
- const proceed = await confirm({
550
- message: 'Stash changes and continue?',
551
- default: false,
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 (!proceed) {
555
- info('Cancelled. Commit or stash your changes first.');
655
+ if (!targetBranch) {
656
+ info('Cancelled');
556
657
  return;
557
658
  }
558
659
 
559
- // Stash changes
560
- const { simpleGit } = await import('simple-git');
561
- const git = simpleGit(mainPath);
562
- await git.stash();
563
- info('Changes stashed');
564
- }
565
-
566
- // Confirm merge
567
- spacer();
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
- if (!confirmed) {
581
- info('Cancelled');
582
- return;
583
- }
670
+ if (!proceed) {
671
+ info('Cancelled. Commit or stash your changes first.');
672
+ return;
673
+ }
584
674
 
585
- // Perform merge
586
- const spinner = ora({
587
- text: 'Merging...',
588
- color: 'magenta',
589
- }).start();
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
- try {
592
- await mergeBranch(selectedWt.branch, targetBranch, mainPath);
593
- spinner.succeed(colors.success('Merged successfully!'));
682
+ // Confirm merge
594
683
  spacer();
595
- success(`Merged ${colors.highlight(selectedWt.branch)} into ${colors.branch(targetBranch)}`);
596
-
597
- // Ask about cleanup
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
- const cleanup = await confirm({
600
- message: `Remove the worktree "${selectedWt.name}" now that it's merged?`,
601
- default: false,
602
- theme: { prefix: icons.trash },
689
+
690
+ const confirmed = await confirm({
691
+ message: 'Proceed with merge?',
692
+ default: true,
693
+ theme: { prefix: '🔀' },
603
694
  });
604
695
 
605
- if (cleanup) {
606
- const cleanupSpinner = ora({
607
- text: 'Cleaning up...',
608
- color: 'yellow',
609
- }).start();
696
+ if (!confirmed) {
697
+ info('Cancelled');
698
+ return;
699
+ }
610
700
 
611
- try {
612
- await removeWorktree(selectedWt.path, false, mainPath);
613
- cleanupSpinner.succeed(colors.success('Worktree removed'));
701
+ // Perform merge
702
+ const spinner = ora({
703
+ text: 'Merging...',
704
+ color: 'magenta',
705
+ }).start();
614
706
 
615
- // Ask about deleting branch
616
- const deleteBr = await confirm({
617
- message: `Delete the branch "${selectedWt.branch}" too?`,
618
- default: false,
619
- theme: { prefix: icons.trash },
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
- if (deleteBr) {
623
- await deleteBranch(selectedWt.branch, false, mainPath);
624
- success(`Branch ${colors.branch(selectedWt.branch)} deleted`);
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
- spacer();
633
- console.log(` ${icons.sparkles} ${colors.success('All done!')}`);
634
- spacer();
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
- spinner.fail(colors.error('Merge failed'));
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 worktrees = await getWorktreesInBase(repoRoot);
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
- error(`Worktree "${name}" not found`);
700
- spacer();
701
- info('Available worktrees:');
702
- worktrees.forEach((wt) => listItem(wt.name));
703
- spacer();
704
- return;
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 choices = worktrees.map((wt) => ({
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
- choices.push({
717
- name: `${colors.muted(icons.cross + ' Cancel')}`,
718
- value: null,
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
- selected = await select({
722
- message: 'Select worktree:',
723
- choices,
724
- theme: { prefix: icons.rocket },
725
- });
857
+ choices.push({
858
+ name: `${colors.muted(icons.cross + ' Cancel')}`,
859
+ value: null,
860
+ });
726
861
 
727
- if (!selected) {
728
- info('Cancelled');
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
  }