@jhorst11/wt 2.0.1 → 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/src/commands.js DELETED
@@ -1,883 +0,0 @@
1
- import { select, input, confirm, search } from '@inquirer/prompts';
2
- import { ExitPromptError } from '@inquirer/core';
3
- import ora from 'ora';
4
- import chalk from 'chalk';
5
- import {
6
- showLogo,
7
- showMiniLogo,
8
- success,
9
- error,
10
- warning,
11
- info,
12
- heading,
13
- subheading,
14
- listItem,
15
- worktreeItem,
16
- divider,
17
- spacer,
18
- colors,
19
- icons,
20
- formatBranchChoice,
21
- } from './ui.js';
22
- import { showCdHint } from './setup.js';
23
- import { resolveConfig, loadConfig, runHooks } from './config.js';
24
- import {
25
- isGitRepo,
26
- getRepoRoot,
27
- getCurrentBranch,
28
- getLocalBranches,
29
- getRemoteBranches,
30
- getAllBranches,
31
- getWorktreesInBase,
32
- getMainRepoPath,
33
- createWorktree,
34
- removeWorktree,
35
- buildBranchName,
36
- isValidBranchName,
37
- getWorktreesBase,
38
- mergeBranch,
39
- getMainBranch,
40
- hasUncommittedChanges,
41
- deleteBranch,
42
- } from './git.js';
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
-
58
- async function ensureGitRepo() {
59
- if (!(await isGitRepo())) {
60
- error('Not in a git repository');
61
- process.exit(1);
62
- }
63
- }
64
-
65
- export async function mainMenu() {
66
- showLogo();
67
-
68
- await ensureGitRepo();
69
-
70
- const repoRoot = await getRepoRoot();
71
- const currentBranch = await getCurrentBranch();
72
- const config = resolveConfig(process.cwd(), repoRoot);
73
- const worktrees = await getWorktreesInBase(repoRoot, config);
74
-
75
- const branchDisplay = currentBranch && currentBranch !== 'HEAD'
76
- ? colors.branch(currentBranch)
77
- : colors.warning('detached HEAD');
78
- subheading(` 📍 ${colors.path(repoRoot)}`);
79
- subheading(` 🌿 ${branchDisplay}`);
80
- spacer();
81
-
82
- const choices = [
83
- {
84
- name: `${icons.plus} Create new worktree`,
85
- value: 'new',
86
- description: 'Create a new worktree from a branch',
87
- },
88
- {
89
- name: `${icons.folder} List worktrees`,
90
- value: 'list',
91
- description: 'View all worktrees for this repo',
92
- },
93
- {
94
- name: `${icons.trash} Remove worktree`,
95
- value: 'remove',
96
- description: 'Delete a worktree',
97
- },
98
- {
99
- name: `${icons.home} Go home`,
100
- value: 'home',
101
- description: 'Return to the main repository',
102
- },
103
- ];
104
-
105
- if (worktrees.length > 0) {
106
- choices.splice(1, 0, {
107
- name: `${icons.rocket} Jump to worktree`,
108
- value: 'go',
109
- description: 'Switch to an existing worktree',
110
- });
111
- choices.splice(3, 0, {
112
- name: `🔀 Merge worktree`,
113
- value: 'merge',
114
- description: 'Merge a worktree branch back to main',
115
- });
116
- }
117
-
118
- choices.push({
119
- name: `${colors.muted(icons.cross + ' Exit')}`,
120
- value: 'exit',
121
- });
122
-
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
- },
132
- },
133
- });
134
-
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);
162
- }
163
- }
164
-
165
- export async function createWorktreeFlow() {
166
- showMiniLogo();
167
- await ensureGitRepo();
168
-
169
- heading(`${icons.plus} Create New Worktree`);
170
-
171
- const currentBranch = await getCurrentBranch();
172
- const repoRoot = await getRepoRoot();
173
- const isDetached = !currentBranch || currentBranch === 'HEAD';
174
-
175
- try {
176
- // Step 1: Choose source type
177
- const sourceChoices = [];
178
-
179
- if (!isDetached) {
180
- sourceChoices.push({
181
- name: `${icons.branch} Current branch (${colors.branch(currentBranch)})`,
182
- value: 'current',
183
- description: 'Create from your current branch',
184
- });
185
- }
186
-
187
- sourceChoices.push(
188
- {
189
- name: `${icons.local} Local branch`,
190
- value: 'local',
191
- description: 'Choose from existing local branches',
192
- },
193
- {
194
- name: `${icons.remote} Remote branch`,
195
- value: 'remote',
196
- description: 'Choose from remote branches',
197
- },
198
- {
199
- name: `${icons.sparkles} New branch`,
200
- value: 'new',
201
- description: 'Create a fresh branch from a base',
202
- },
203
- );
204
-
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
- },
211
- });
212
-
213
- let baseBranch = null;
214
- let branchName = null;
215
- let worktreeName = null;
216
-
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
- }
225
-
226
- const branchChoices = branches.map((b) => ({
227
- name: formatBranchChoice(b.name, 'local'),
228
- value: b.name,
229
- description: b.isCurrent ? '(current)' : undefined,
230
- }));
231
-
232
- baseBranch = await select({
233
- message: 'Select a local branch:',
234
- choices: branchChoices,
235
- theme: { prefix: icons.local },
236
- });
237
- } else if (sourceType === 'remote') {
238
- const spinner = ora({
239
- text: 'Fetching remote branches...',
240
- color: 'magenta',
241
- }).start();
242
-
243
- const remoteBranches = await getRemoteBranches();
244
- spinner.stop();
245
-
246
- if (remoteBranches.length === 0) {
247
- error('No remote branches found');
248
- return;
249
- }
250
-
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
- }
284
-
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
- }));
289
-
290
- baseBranch = await select({
291
- message: 'Select base branch for your new branch:',
292
- choices: allChoices,
293
- theme: { prefix: icons.branch },
294
- });
295
- }
296
-
297
- // Step 2: Get worktree name
298
- spacer();
299
-
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
- });
310
-
311
- worktreeName = worktreeName.trim().replace(/ /g, '-');
312
-
313
- // Build branch name with hierarchical config resolution
314
- const config = resolveConfig(process.cwd(), repoRoot);
315
- branchName = buildBranchName(worktreeName, config);
316
-
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();
326
-
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');
335
- return;
336
- }
337
-
338
- // Step 4: Create worktree
339
- spacer();
340
- const spinner = ora({
341
- text: 'Creating worktree...',
342
- color: 'magenta',
343
- }).start();
344
-
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
- }
391
-
392
- showCdHint(result.path);
393
- } catch (err) {
394
- spinner.fail(colors.error('Failed to create worktree'));
395
- error(err.message);
396
- }
397
- } catch (err) {
398
- handlePromptError(err);
399
- }
400
- }
401
-
402
- export async function listWorktrees() {
403
- showMiniLogo();
404
- await ensureGitRepo();
405
-
406
- const repoRoot = await getRepoRoot();
407
- const config = resolveConfig(process.cwd(), repoRoot);
408
- const worktrees = await getWorktreesInBase(repoRoot, config);
409
- const currentPath = process.cwd();
410
-
411
- heading(`${icons.folder} Worktrees`);
412
-
413
- if (worktrees.length === 0) {
414
- info('No worktrees found for this repository');
415
- spacer();
416
- console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
417
- spacer();
418
- return;
419
- }
420
-
421
- subheading(`Found ${worktrees.length} worktree${worktrees.length === 1 ? '' : 's'}:`);
422
- spacer();
423
-
424
- for (const wt of worktrees) {
425
- const isCurrent = currentPath === wt.path || currentPath.startsWith(wt.path + '/');
426
- worktreeItem(wt.name, wt.path, isCurrent);
427
- const branchDisplay = wt.branch === 'unknown'
428
- ? colors.warning('detached HEAD')
429
- : colors.branch(wt.branch);
430
- console.log(` ${icons.branch} ${branchDisplay}`);
431
- spacer();
432
- }
433
-
434
- divider();
435
- console.log(` ${colors.muted('Main repo:')} ${colors.path(repoRoot)}`);
436
- spacer();
437
- }
438
-
439
- export async function removeWorktreeFlow() {
440
- showMiniLogo();
441
- await ensureGitRepo();
442
-
443
- heading(`${icons.trash} Remove Worktree`);
444
-
445
- const repoRoot = await getRepoRoot();
446
- const config = resolveConfig(process.cwd(), repoRoot);
447
- const worktrees = await getWorktreesInBase(repoRoot, config);
448
- const currentPath = process.cwd();
449
-
450
- if (worktrees.length === 0) {
451
- info('No worktrees found to remove');
452
- spacer();
453
- console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
454
- spacer();
455
- return;
456
- }
457
-
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
- });
468
-
469
- choices.push({
470
- name: `${colors.muted(icons.cross + ' Cancel')}`,
471
- value: null,
472
- });
473
-
474
- const selected = await select({
475
- message: 'Select worktree to remove:',
476
- choices,
477
- theme: { prefix: icons.trash },
478
- });
479
-
480
- if (!selected) {
481
- info('Cancelled');
482
- return;
483
- }
484
-
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
- }
492
-
493
- spacer();
494
- warning(`This will remove: ${colors.path(selected.path)}`);
495
- spacer();
496
-
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
- });
502
-
503
- if (!confirmed) {
504
- info('Cancelled');
505
- return;
506
- }
507
-
508
- const spinner = ora({
509
- text: 'Removing worktree...',
510
- color: 'yellow',
511
- }).start();
512
-
513
- try {
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
- });
527
-
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
- }
535
- }
536
-
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
- }
554
- } catch (err) {
555
- handlePromptError(err);
556
- }
557
- }
558
-
559
- export async function mergeWorktreeFlow() {
560
- showMiniLogo();
561
- await ensureGitRepo();
562
-
563
- heading(`🔀 Merge Worktree`);
564
-
565
- const repoRoot = await getRepoRoot();
566
- const mainPath = await getMainRepoPath();
567
- const config = resolveConfig(process.cwd(), repoRoot);
568
- const worktrees = await getWorktreesInBase(repoRoot, config);
569
- const currentPath = process.cwd();
570
- const isAtHome = currentPath === mainPath;
571
-
572
- if (worktrees.length === 0) {
573
- info('No worktrees found to merge');
574
- spacer();
575
- console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
576
- spacer();
577
- return;
578
- }
579
-
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
- }));
587
-
588
- wtChoices.push({
589
- name: `${colors.muted(icons.cross + ' Cancel')}`,
590
- value: null,
591
- });
592
-
593
- const selectedWt = await select({
594
- message: 'Select worktree branch to merge:',
595
- choices: wtChoices,
596
- theme: { prefix: '🔀' },
597
- });
598
-
599
- if (!selectedWt) {
600
- info('Cancelled');
601
- return;
602
- }
603
-
604
- // Select target branch
605
- const mainBranch = await getMainBranch(mainPath);
606
- const currentBranch = await getCurrentBranch();
607
- const localBranches = await getLocalBranches(mainPath);
608
-
609
- const targetChoices = [];
610
-
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
- }
618
-
619
- // Add current branch if different and we're at home
620
- if (isAtHome && currentBranch && currentBranch !== mainBranch && currentBranch !== 'HEAD') {
621
- targetChoices.push({
622
- name: `${icons.pointer} ${colors.branch(currentBranch)} ${colors.muted('(current)')}`,
623
- value: currentBranch,
624
- });
625
- }
626
-
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
- }
636
-
637
- if (targetChoices.length === 0) {
638
- error('No target branches available to merge into');
639
- spacer();
640
- return;
641
- }
642
-
643
- targetChoices.push({
644
- name: `${colors.muted(icons.cross + ' Cancel')}`,
645
- value: null,
646
- });
647
-
648
- spacer();
649
- const targetBranch = await select({
650
- message: `Merge ${colors.highlight(selectedWt.branch)} into:`,
651
- choices: targetChoices,
652
- theme: { prefix: icons.arrowRight },
653
- });
654
-
655
- if (!targetBranch) {
656
- info('Cancelled');
657
- return;
658
- }
659
-
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
- });
669
-
670
- if (!proceed) {
671
- info('Cancelled. Commit or stash your changes first.');
672
- return;
673
- }
674
-
675
- // Stash changes
676
- const { simpleGit } = await import('simple-git');
677
- const git = simpleGit(mainPath);
678
- await git.stash();
679
- info('Changes stashed');
680
- }
681
-
682
- // Confirm merge
683
- spacer();
684
- divider();
685
- info(`From: ${colors.highlight(selectedWt.branch)} ${colors.muted(`(${selectedWt.name})`)}`);
686
- info(`Into: ${colors.branch(targetBranch)}`);
687
- divider();
688
- spacer();
689
-
690
- const confirmed = await confirm({
691
- message: 'Proceed with merge?',
692
- default: true,
693
- theme: { prefix: '🔀' },
694
- });
695
-
696
- if (!confirmed) {
697
- info('Cancelled');
698
- return;
699
- }
700
-
701
- // Perform merge
702
- const spinner = ora({
703
- text: 'Merging...',
704
- color: 'magenta',
705
- }).start();
706
-
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)}`);
712
-
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);
745
- }
746
- }
747
-
748
- spacer();
749
- console.log(` ${icons.sparkles} ${colors.success('All done!')}`);
750
- spacer();
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
- }
761
- } catch (err) {
762
- handlePromptError(err);
763
- }
764
- }
765
-
766
- export async function goHome() {
767
- showMiniLogo();
768
- await ensureGitRepo();
769
-
770
- const mainPath = await getMainRepoPath();
771
- const currentPath = process.cwd();
772
-
773
- if (!mainPath) {
774
- error('Could not find main repository');
775
- return;
776
- }
777
-
778
- // Check if we're already home
779
- if (currentPath === mainPath || currentPath.startsWith(mainPath + '/')) {
780
- const isExactlyHome = currentPath === mainPath;
781
- spacer();
782
- if (isExactlyHome) {
783
- console.log(` ${icons.home} ${colors.success("You're already home!")} ${icons.sparkles}`);
784
- } else {
785
- console.log(` ${icons.home} ${colors.success("You're in the main repo")} ${icons.sparkles}`);
786
- }
787
- console.log(` ${colors.muted('Path:')} ${colors.path(mainPath)}`);
788
- spacer();
789
- return;
790
- }
791
-
792
- spacer();
793
- success(`Heading home... ${icons.home}`);
794
- console.log(` ${colors.muted('Path:')} ${colors.path(mainPath)}`);
795
-
796
- showCdHint(mainPath);
797
- }
798
-
799
- export async function goToWorktree(name) {
800
- showMiniLogo();
801
- await ensureGitRepo();
802
-
803
- const repoRoot = await getRepoRoot();
804
- const config = resolveConfig(process.cwd(), repoRoot);
805
- const worktrees = await getWorktreesInBase(repoRoot, config);
806
-
807
- if (worktrees.length === 0) {
808
- heading(`${icons.rocket} Jump to Worktree`);
809
- info('No worktrees found');
810
- spacer();
811
- console.log(` ${colors.muted('Create one with')} ${colors.primary('wt new')}`);
812
- spacer();
813
- return;
814
- }
815
-
816
- let selected;
817
-
818
- if (name) {
819
- // Direct jump by name - also try partial/fuzzy match
820
- selected = worktrees.find((wt) => wt.name === name);
821
- if (!selected) {
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
- }
839
- }
840
- } else {
841
- // Interactive selection
842
- heading(`${icons.rocket} Jump to Worktree`);
843
-
844
- const currentPath = process.cwd();
845
-
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
- });
856
-
857
- choices.push({
858
- name: `${colors.muted(icons.cross + ' Cancel')}`,
859
- value: null,
860
- });
861
-
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);
874
- return;
875
- }
876
- }
877
-
878
- spacer();
879
- success(`Jumping to ${colors.highlight(selected.name)}`);
880
- console.log(` ${colors.muted('Path:')} ${colors.path(selected.path)}`);
881
-
882
- showCdHint(selected.path);
883
- }