@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/README.md +39 -3
- package/dist/bin/wt.d.ts +3 -0
- package/dist/bin/wt.d.ts.map +1 -0
- package/dist/bin/wt.js +83 -0
- package/dist/bin/wt.js.map +1 -0
- package/dist/src/commands.d.ts +9 -0
- package/dist/src/commands.d.ts.map +1 -0
- package/dist/src/commands.js +924 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/config.d.ts +51 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +384 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/git.d.ts +55 -0
- package/dist/src/git.d.ts.map +1 -0
- package/dist/src/git.js +387 -0
- package/dist/src/git.js.map +1 -0
- package/dist/src/setup.d.ts +8 -0
- package/dist/src/setup.d.ts.map +1 -0
- package/dist/src/setup.js +245 -0
- package/dist/src/setup.js.map +1 -0
- package/dist/src/types.d.ts +64 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/ui.d.ts +93 -0
- package/dist/src/ui.d.ts.map +1 -0
- package/dist/src/ui.js +273 -0
- package/dist/src/ui.js.map +1 -0
- package/package.json +20 -6
- package/bin/wt.js +0 -87
- package/shell/wt.sh +0 -66
- package/src/commands.js +0 -883
- package/src/config.js +0 -257
- package/src/git.js +0 -397
- package/src/setup.js +0 -267
- package/src/ui.js +0 -147
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
|
-
}
|