yggtree 1.2.1 → 1.4.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 +118 -13
- package/dist/commands/wt/apply.js +7 -3
- package/dist/commands/wt/create-branch.js +40 -20
- package/dist/commands/wt/create-multi.js +13 -10
- package/dist/commands/wt/create-sandbox.js +63 -10
- package/dist/commands/wt/create.js +157 -57
- package/dist/commands/wt/delete.js +45 -17
- package/dist/commands/wt/enter.js +43 -31
- package/dist/commands/wt/exec.js +4 -17
- package/dist/commands/wt/list.js +29 -19
- package/dist/commands/wt/open.js +245 -0
- package/dist/commands/wt/path.js +4 -17
- package/dist/index.js +66 -25
- package/dist/lib/config.js +17 -4
- package/dist/lib/git.js +63 -13
- package/dist/lib/prompt.js +9 -0
- package/dist/lib/sandbox.js +19 -3
- package/dist/lib/ui.js +20 -0
- package/dist/lib/worktree.js +52 -0
- package/package.json +2 -1
- package/dist/commands/wt/leave.js +0 -13
|
@@ -1,48 +1,138 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { getRepoRoot, getRepoName,
|
|
4
|
+
import { getRepoRoot, getRepoName, createWorktree, fetchAll, listWorktrees } from '../../lib/git.js';
|
|
5
5
|
import { runBootstrap } from '../../lib/config.js';
|
|
6
6
|
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
7
|
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
8
|
+
import { ensureAutocompletePrompt } from '../../lib/prompt.js';
|
|
9
|
+
import { enterCommand } from './enter.js';
|
|
10
|
+
import { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
|
|
8
11
|
import { execa } from 'execa';
|
|
9
12
|
import { spawn } from 'child_process';
|
|
10
13
|
import fs from 'fs-extra';
|
|
14
|
+
function toSlug(value) {
|
|
15
|
+
return value
|
|
16
|
+
.trim()
|
|
17
|
+
.replace(/[\/\\]/g, '-')
|
|
18
|
+
.replace(/\s+/g, '-');
|
|
19
|
+
}
|
|
20
|
+
async function listBranchCandidates() {
|
|
21
|
+
const [localRefs, remoteRefs] = await Promise.all([
|
|
22
|
+
execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/heads']),
|
|
23
|
+
execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/remotes/origin']),
|
|
24
|
+
]);
|
|
25
|
+
const localBranches = localRefs.stdout
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map(line => line.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
const remoteBranches = remoteRefs.stdout
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map(line => line.trim())
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.filter(ref => ref !== 'origin/HEAD' && !ref.endsWith('/HEAD'))
|
|
34
|
+
.map(ref => ref.replace(/^origin\//, ''));
|
|
35
|
+
const localSet = new Set(localBranches);
|
|
36
|
+
const remoteOnly = [...new Set(remoteBranches)].filter(branch => !localSet.has(branch));
|
|
37
|
+
const localCandidates = localBranches.map(branchName => ({
|
|
38
|
+
branchName,
|
|
39
|
+
checkoutRef: branchName,
|
|
40
|
+
createLocalBranch: false,
|
|
41
|
+
source: 'local',
|
|
42
|
+
}));
|
|
43
|
+
const remoteCandidates = remoteOnly.map(branchName => ({
|
|
44
|
+
branchName,
|
|
45
|
+
checkoutRef: `origin/${branchName}`,
|
|
46
|
+
createLocalBranch: true,
|
|
47
|
+
source: 'remote',
|
|
48
|
+
}));
|
|
49
|
+
return [...localCandidates, ...remoteCandidates]
|
|
50
|
+
.sort((a, b) => a.branchName.localeCompare(b.branchName));
|
|
51
|
+
}
|
|
52
|
+
function resolveCandidateFromRef(ref, candidates) {
|
|
53
|
+
const trimmedRef = ref.trim();
|
|
54
|
+
if (!trimmedRef)
|
|
55
|
+
return undefined;
|
|
56
|
+
if (trimmedRef.startsWith('origin/')) {
|
|
57
|
+
const branchName = trimmedRef.replace(/^origin\//, '');
|
|
58
|
+
return candidates.find(candidate => candidate.branchName === branchName);
|
|
59
|
+
}
|
|
60
|
+
return candidates.find(candidate => candidate.branchName === trimmedRef ||
|
|
61
|
+
candidate.checkoutRef === trimmedRef);
|
|
62
|
+
}
|
|
11
63
|
export async function createCommand(options) {
|
|
12
64
|
try {
|
|
13
65
|
const repoRoot = await getRepoRoot();
|
|
14
66
|
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
15
|
-
// 1.
|
|
16
|
-
const
|
|
67
|
+
// 1. Load branches
|
|
68
|
+
const loadingSpinner = createSpinner('Fetching branches...').start();
|
|
69
|
+
await fetchAll();
|
|
70
|
+
const candidates = await listBranchCandidates();
|
|
71
|
+
if (candidates.length === 0) {
|
|
72
|
+
loadingSpinner.fail('No branches found.');
|
|
73
|
+
log.warning('Create a branch first, then run worktree-checkout again.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
loadingSpinner.succeed(`Loaded ${candidates.length} branches.`);
|
|
77
|
+
// 2. Select branch
|
|
78
|
+
ensureAutocompletePrompt();
|
|
79
|
+
let selectedBranch;
|
|
80
|
+
if (options.ref) {
|
|
81
|
+
selectedBranch = resolveCandidateFromRef(options.ref, candidates);
|
|
82
|
+
if (!selectedBranch) {
|
|
83
|
+
log.error(`Branch "${options.ref}" not found.`);
|
|
84
|
+
log.warning('Tip: use a local branch name or origin/<branch>.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const { branchChoice } = await inquirer.prompt([
|
|
90
|
+
{
|
|
91
|
+
type: 'autocomplete',
|
|
92
|
+
name: 'branchChoice',
|
|
93
|
+
message: 'Select branch (type to filter):',
|
|
94
|
+
source: async (_answers, input = '') => {
|
|
95
|
+
const term = input.trim().toLowerCase();
|
|
96
|
+
const filtered = term
|
|
97
|
+
? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term))
|
|
98
|
+
: candidates;
|
|
99
|
+
return filtered.map(candidate => ({
|
|
100
|
+
name: `${chalk.yellow(candidate.branchName)} ${chalk.dim(`(${candidate.source})`)}`,
|
|
101
|
+
value: candidate,
|
|
102
|
+
}));
|
|
103
|
+
},
|
|
104
|
+
pageSize: 10,
|
|
105
|
+
},
|
|
106
|
+
]);
|
|
107
|
+
selectedBranch = branchChoice;
|
|
108
|
+
}
|
|
109
|
+
if (!selectedBranch) {
|
|
110
|
+
log.error('No branch selected.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const existingWorktrees = await listWorktrees();
|
|
114
|
+
const existingManagedWorktree = existingWorktrees.find(wt => wt.branch === selectedBranch.branchName && wt.path.startsWith(WORKTREES_ROOT));
|
|
115
|
+
if (existingManagedWorktree) {
|
|
116
|
+
log.info(`Branch ${chalk.cyan(selectedBranch.branchName)} is already active in ${ui.path(existingManagedWorktree.path)}.`);
|
|
117
|
+
if (options.enter === false) {
|
|
118
|
+
log.header(`cd "${existingManagedWorktree.path}"`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
log.info('Opening existing worktree instead of creating a duplicate...');
|
|
122
|
+
await enterCommand(existingManagedWorktree.path, { exec: options.exec ?? '' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// 3. Gather remaining inputs
|
|
126
|
+
const defaultSlug = toSlug(selectedBranch.branchName);
|
|
17
127
|
const answers = await inquirer.prompt([
|
|
18
128
|
{
|
|
19
129
|
type: 'input',
|
|
20
130
|
name: 'name',
|
|
21
131
|
message: 'Worktree name (slug):',
|
|
22
|
-
default: options.name,
|
|
132
|
+
default: options.name || defaultSlug,
|
|
23
133
|
when: !options.name,
|
|
24
134
|
validate: (input) => input.trim().length > 0 || 'Name is required',
|
|
25
135
|
},
|
|
26
|
-
{
|
|
27
|
-
type: 'input',
|
|
28
|
-
name: 'ref',
|
|
29
|
-
message: 'Base branch name:',
|
|
30
|
-
default: options.ref || currentBranch,
|
|
31
|
-
when: !options.ref,
|
|
32
|
-
validate: (input) => input.trim().length > 0 || 'Ref is required',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
type: 'list',
|
|
36
|
-
name: 'source',
|
|
37
|
-
message: 'Base on:',
|
|
38
|
-
loop: false,
|
|
39
|
-
choices: [
|
|
40
|
-
{ name: 'Remote (origin)', value: 'remote' },
|
|
41
|
-
{ name: 'Local', value: 'local' },
|
|
42
|
-
],
|
|
43
|
-
default: 'remote',
|
|
44
|
-
when: !options.ref && !options.source,
|
|
45
|
-
},
|
|
46
136
|
{
|
|
47
137
|
type: 'confirm',
|
|
48
138
|
name: 'bootstrap',
|
|
@@ -58,61 +148,62 @@ export async function createCommand(options) {
|
|
|
58
148
|
when: options.enter === undefined,
|
|
59
149
|
},
|
|
60
150
|
{
|
|
61
|
-
type: '
|
|
62
|
-
name: '
|
|
63
|
-
message: '
|
|
64
|
-
default:
|
|
151
|
+
type: 'confirm',
|
|
152
|
+
name: 'shouldOpenTool',
|
|
153
|
+
message: 'Open a tool after creation? (IDE or agent CLI)',
|
|
154
|
+
default: false,
|
|
65
155
|
when: options.exec === undefined,
|
|
66
|
-
}
|
|
156
|
+
},
|
|
67
157
|
]);
|
|
68
158
|
const name = options.name || answers.name;
|
|
69
|
-
let ref = options.ref || answers.ref;
|
|
70
|
-
const source = options.source || answers.source;
|
|
71
159
|
const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
|
|
72
160
|
const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
161
|
+
let selectedTool;
|
|
162
|
+
if (options.exec === undefined && answers.shouldOpenTool) {
|
|
163
|
+
const installedTools = await detectInstalledOpenTools();
|
|
164
|
+
if (installedTools.length === 0) {
|
|
165
|
+
log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
|
|
169
|
+
}
|
|
77
170
|
}
|
|
78
|
-
const
|
|
171
|
+
const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
|
|
172
|
+
const slug = toSlug(name);
|
|
79
173
|
const repoName = await getRepoName();
|
|
80
174
|
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
81
|
-
//
|
|
175
|
+
// 4. Validation
|
|
82
176
|
if (!slug)
|
|
83
177
|
throw new Error('Invalid name');
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// 3. Execution
|
|
87
|
-
const spinner = createSpinner('Fetching...').start();
|
|
88
|
-
await fetchAll();
|
|
89
|
-
spinner.text = 'Verifying ref...';
|
|
90
|
-
const exists = await verifyRef(ref);
|
|
91
|
-
if (!exists) {
|
|
92
|
-
spinner.fail(`Ref not found: ${ref}`);
|
|
93
|
-
log.warning(`Tip: try 'origin/${ref}' or check if the branch exists.`);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
spinner.text = `Creating worktree at ${ui.path(wtPath)}...`;
|
|
178
|
+
// 5. Execution (checkout-style: attach to selected branch)
|
|
179
|
+
const spinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
|
|
97
180
|
try {
|
|
98
181
|
await fs.ensureDir(path.dirname(wtPath));
|
|
99
|
-
|
|
182
|
+
if (selectedBranch.createLocalBranch) {
|
|
183
|
+
await execa('git', ['worktree', 'add', '-b', selectedBranch.branchName, wtPath, selectedBranch.checkoutRef]);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
await createWorktree(wtPath, selectedBranch.checkoutRef);
|
|
187
|
+
}
|
|
100
188
|
spinner.succeed('Worktree created.');
|
|
101
189
|
}
|
|
102
190
|
catch (e) {
|
|
103
191
|
spinner.fail('Failed to create worktree.');
|
|
104
|
-
|
|
192
|
+
const command = selectedBranch.createLocalBranch
|
|
193
|
+
? `git worktree add -b ${selectedBranch.branchName} ${wtPath} ${selectedBranch.checkoutRef}`
|
|
194
|
+
: `git worktree add ${wtPath} ${selectedBranch.checkoutRef}`;
|
|
195
|
+
log.actionableError(e.message, command, wtPath, [
|
|
105
196
|
'Check if the folder already exists: ls ' + wtPath,
|
|
106
|
-
|
|
197
|
+
`Check if branch "${selectedBranch.branchName}" is already checked out in another worktree: git worktree list`,
|
|
107
198
|
'Try pruning stale worktrees: yggtree wt prune',
|
|
108
|
-
`Run manually:
|
|
199
|
+
`Run manually: ${command}`
|
|
109
200
|
]);
|
|
110
201
|
return;
|
|
111
202
|
}
|
|
112
203
|
if (shouldBootstrap) {
|
|
113
204
|
await runBootstrap(wtPath, repoRoot);
|
|
114
205
|
}
|
|
115
|
-
//
|
|
206
|
+
// 6. Exec Command
|
|
116
207
|
if (execCommandStr && execCommandStr.trim()) {
|
|
117
208
|
log.info(`Executing: ${execCommandStr} in ${ui.path(wtPath)}`);
|
|
118
209
|
try {
|
|
@@ -129,7 +220,16 @@ export async function createCommand(options) {
|
|
|
129
220
|
]);
|
|
130
221
|
}
|
|
131
222
|
}
|
|
132
|
-
|
|
223
|
+
if (selectedTool && !isAgentTool(selectedTool)) {
|
|
224
|
+
try {
|
|
225
|
+
log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
|
|
226
|
+
await launchOpenTool(selectedTool, wtPath);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// 7. Final Output
|
|
133
233
|
log.success('Worktree ready!');
|
|
134
234
|
if (shouldEnter) {
|
|
135
235
|
log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
|
|
@@ -148,7 +248,7 @@ export async function createCommand(options) {
|
|
|
148
248
|
}
|
|
149
249
|
}
|
|
150
250
|
catch (error) {
|
|
151
|
-
log.actionableError(error.message, 'yggtree wt
|
|
251
|
+
log.actionableError(error.message, 'yggtree wt worktree-checkout');
|
|
152
252
|
process.exit(1);
|
|
153
253
|
}
|
|
154
254
|
}
|
|
@@ -1,32 +1,60 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
export async function deleteCommand() {
|
|
3
|
+
import { listWorktrees, removeWorktree, getRepoRoot, getLastActivity } from '../../lib/git.js';
|
|
4
|
+
import { log, createSpinner, timeAgo } from '../../lib/ui.js';
|
|
5
|
+
import { detectWorktreeType, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, isManagedWorktreePath, } from '../../lib/worktree.js';
|
|
6
|
+
export async function deleteCommand(options = {}) {
|
|
8
7
|
try {
|
|
9
|
-
const
|
|
8
|
+
const currentWorktreePath = await getRepoRoot();
|
|
10
9
|
const worktrees = await listWorktrees();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
let showAll = options.all;
|
|
11
|
+
if (showAll === undefined) {
|
|
12
|
+
const { includeExternal } = await inquirer.prompt([
|
|
13
|
+
{
|
|
14
|
+
type: 'confirm',
|
|
15
|
+
name: 'includeExternal',
|
|
16
|
+
message: 'Include external linked worktrees (outside ~/.yggtree)?',
|
|
17
|
+
default: false,
|
|
18
|
+
},
|
|
19
|
+
]);
|
|
20
|
+
showAll = includeExternal;
|
|
21
|
+
}
|
|
22
|
+
const includeAll = Boolean(showAll);
|
|
23
|
+
const mainWorktreePath = worktrees[0]?.path;
|
|
24
|
+
const deletableWts = worktrees.filter(wt => {
|
|
25
|
+
if (includeAll) {
|
|
26
|
+
const isMainWorktree = wt.path === mainWorktreePath;
|
|
27
|
+
const isCurrentWorktree = wt.path === currentWorktreePath;
|
|
28
|
+
return !isMainWorktree && !isCurrentWorktree;
|
|
29
|
+
}
|
|
30
|
+
return isManagedWorktreePath(wt.path);
|
|
31
|
+
});
|
|
32
|
+
if (deletableWts.length === 0) {
|
|
33
|
+
log.info(includeAll
|
|
34
|
+
? 'No deletable linked worktrees found.'
|
|
35
|
+
: 'No managed worktrees found to delete. Use "yggtree wt delete --all" to include external linked worktrees.');
|
|
15
36
|
return;
|
|
16
37
|
}
|
|
17
|
-
const choices =
|
|
18
|
-
const
|
|
38
|
+
const choices = await Promise.all(deletableWts.map(async (wt) => {
|
|
39
|
+
const [activity, type] = await Promise.all([
|
|
40
|
+
getLastActivity(wt.path),
|
|
41
|
+
detectWorktreeType(wt, mainWorktreePath || ''),
|
|
42
|
+
]);
|
|
43
|
+
const branchName = getWorktreeBranchName(wt);
|
|
44
|
+
const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('—');
|
|
45
|
+
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
19
46
|
return {
|
|
20
|
-
name: `${chalk.bold(
|
|
47
|
+
name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
|
|
21
48
|
value: wt.path,
|
|
22
49
|
};
|
|
23
|
-
});
|
|
50
|
+
}));
|
|
24
51
|
const { selectedPaths } = await inquirer.prompt([
|
|
25
52
|
{
|
|
26
53
|
type: 'checkbox',
|
|
27
54
|
name: 'selectedPaths',
|
|
28
|
-
message: 'Select worktrees to delete:',
|
|
55
|
+
message: includeAll ? 'Select worktrees to delete:' : 'Select managed worktrees to delete:',
|
|
29
56
|
choices: choices,
|
|
57
|
+
pageSize: 10,
|
|
30
58
|
},
|
|
31
59
|
]);
|
|
32
60
|
if (!selectedPaths || selectedPaths.length === 0) {
|
|
@@ -34,7 +62,7 @@ export async function deleteCommand() {
|
|
|
34
62
|
return;
|
|
35
63
|
}
|
|
36
64
|
const count = selectedPaths.length;
|
|
37
|
-
const names = selectedPaths.map((p) =>
|
|
65
|
+
const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p));
|
|
38
66
|
const { confirm } = await inquirer.prompt([
|
|
39
67
|
{
|
|
40
68
|
type: 'confirm',
|
|
@@ -48,7 +76,7 @@ export async function deleteCommand() {
|
|
|
48
76
|
return;
|
|
49
77
|
}
|
|
50
78
|
for (const wtPath of selectedPaths) {
|
|
51
|
-
const worktreeName =
|
|
79
|
+
const worktreeName = formatWorktreeDisplayPath(wtPath);
|
|
52
80
|
const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
|
|
53
81
|
try {
|
|
54
82
|
await removeWorktree(wtPath);
|
|
@@ -1,30 +1,51 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
|
-
import
|
|
4
|
+
import chalk from 'chalk';
|
|
5
5
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
6
|
-
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
6
|
import { log, ui } from '../../lib/ui.js';
|
|
7
|
+
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
8
|
+
function truncateEnd(value, maxLen) {
|
|
9
|
+
if (maxLen <= 0)
|
|
10
|
+
return '';
|
|
11
|
+
if (value.length <= maxLen)
|
|
12
|
+
return value;
|
|
13
|
+
if (maxLen <= 1)
|
|
14
|
+
return '…';
|
|
15
|
+
return `${value.slice(0, maxLen - 1)}…`;
|
|
16
|
+
}
|
|
17
|
+
function truncateStart(value, maxLen) {
|
|
18
|
+
if (maxLen <= 0)
|
|
19
|
+
return '';
|
|
20
|
+
if (value.length <= maxLen)
|
|
21
|
+
return value;
|
|
22
|
+
if (maxLen <= 1)
|
|
23
|
+
return '…';
|
|
24
|
+
return `…${value.slice(-(maxLen - 1))}`;
|
|
25
|
+
}
|
|
26
|
+
function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
|
|
27
|
+
const maxRowWidth = Math.max(40, terminalColumns - 10);
|
|
28
|
+
const typeWidth = 8;
|
|
29
|
+
const availableWidth = Math.max(22, maxRowWidth - typeWidth);
|
|
30
|
+
const branchWidth = Math.min(44, Math.max(12, Math.floor(availableWidth * 0.55)));
|
|
31
|
+
const pathWidth = Math.max(10, availableWidth - branchWidth - 1);
|
|
32
|
+
const branchText = truncateEnd(branchName, branchWidth).padEnd(branchWidth);
|
|
33
|
+
const pathText = truncateStart(displayPath, pathWidth);
|
|
34
|
+
const typeText = formatWorktreeType(type);
|
|
35
|
+
return `${typeText} ${chalk.yellow(branchText)} ${chalk.cyan(pathText)}`;
|
|
36
|
+
}
|
|
8
37
|
export async function enterCommand(wtName, options = {}) {
|
|
9
38
|
try {
|
|
10
39
|
await getRepoRoot();
|
|
11
40
|
const worktrees = await listWorktrees();
|
|
41
|
+
const mainWorktreePath = worktrees[0]?.path || '';
|
|
12
42
|
if (worktrees.length === 0) {
|
|
13
43
|
log.info('No worktrees found.');
|
|
14
44
|
return;
|
|
15
45
|
}
|
|
16
46
|
let targetWt;
|
|
17
47
|
if (wtName) {
|
|
18
|
-
|
|
19
|
-
targetWt = worktrees.find(wt => {
|
|
20
|
-
const branchName = wt.branch || wt.HEAD || '';
|
|
21
|
-
const relativePath = path.relative(WORKTREES_ROOT, wt.path);
|
|
22
|
-
const basename = path.basename(wt.path);
|
|
23
|
-
return branchName === wtName ||
|
|
24
|
-
relativePath === wtName ||
|
|
25
|
-
wt.path === wtName ||
|
|
26
|
-
basename === wtName;
|
|
27
|
-
});
|
|
48
|
+
targetWt = findWorktreeByName(worktrees, wtName);
|
|
28
49
|
if (!targetWt) {
|
|
29
50
|
log.error(`Worktree "${wtName}" not found.`);
|
|
30
51
|
return;
|
|
@@ -32,38 +53,29 @@ export async function enterCommand(wtName, options = {}) {
|
|
|
32
53
|
}
|
|
33
54
|
else {
|
|
34
55
|
// Interactive Selection
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
: wt.path.replace(process.env.HOME || '', '~');
|
|
56
|
+
const terminalColumns = process.stdout.columns || 100;
|
|
57
|
+
const choices = await Promise.all(worktrees.map(async (wt) => {
|
|
58
|
+
const type = await detectWorktreeType(wt, mainWorktreePath);
|
|
59
|
+
const branchName = getWorktreeBranchName(wt);
|
|
60
|
+
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
41
61
|
return {
|
|
42
|
-
name:
|
|
62
|
+
name: formatChoiceLabel(type, branchName, displayPath, terminalColumns),
|
|
43
63
|
value: wt
|
|
44
64
|
};
|
|
45
|
-
});
|
|
65
|
+
}));
|
|
46
66
|
const { selectedWt } = await inquirer.prompt([
|
|
47
67
|
{
|
|
48
68
|
type: 'list',
|
|
49
69
|
name: 'selectedWt',
|
|
50
70
|
message: 'Select a worktree to enter:',
|
|
51
71
|
choices,
|
|
52
|
-
loop: false
|
|
72
|
+
loop: false,
|
|
73
|
+
pageSize: 10,
|
|
53
74
|
}
|
|
54
75
|
]);
|
|
55
76
|
targetWt = selectedWt;
|
|
56
77
|
}
|
|
57
|
-
const
|
|
58
|
-
{
|
|
59
|
-
type: 'input',
|
|
60
|
-
name: 'execCommandStr',
|
|
61
|
-
message: 'Command to run before entering (optional):',
|
|
62
|
-
default: options.exec,
|
|
63
|
-
when: options.exec === undefined,
|
|
64
|
-
}
|
|
65
|
-
]);
|
|
66
|
-
const finalExec = options.exec || execCommandStr;
|
|
78
|
+
const finalExec = options.exec;
|
|
67
79
|
if (finalExec && finalExec.trim()) {
|
|
68
80
|
log.info(`Executing: ${finalExec} in ${ui.path(targetWt?.path || '')}`);
|
|
69
81
|
try {
|
package/dist/commands/wt/exec.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
|
-
import path from 'path';
|
|
4
3
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
5
|
-
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
6
4
|
import { log } from '../../lib/ui.js';
|
|
5
|
+
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
7
6
|
export async function execCommand(wtName, commandArgs) {
|
|
8
7
|
let targetWt;
|
|
9
8
|
let command = '';
|
|
@@ -16,16 +15,7 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
16
15
|
return;
|
|
17
16
|
}
|
|
18
17
|
if (wtName) {
|
|
19
|
-
|
|
20
|
-
targetWt = worktrees.find(wt => {
|
|
21
|
-
const branchName = wt.branch || wt.HEAD || '';
|
|
22
|
-
const relativePath = path.relative(WORKTREES_ROOT, wt.path);
|
|
23
|
-
const basename = path.basename(wt.path);
|
|
24
|
-
return branchName === wtName ||
|
|
25
|
-
relativePath === wtName ||
|
|
26
|
-
wt.path === wtName ||
|
|
27
|
-
basename === wtName;
|
|
28
|
-
});
|
|
18
|
+
targetWt = findWorktreeByName(worktrees, wtName);
|
|
29
19
|
if (!targetWt) {
|
|
30
20
|
log.error(`Worktree "${wtName}" not found.`);
|
|
31
21
|
return;
|
|
@@ -34,11 +24,8 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
34
24
|
else {
|
|
35
25
|
// Interactive Selection
|
|
36
26
|
const choices = worktrees.map(wt => {
|
|
37
|
-
const branchName = wt
|
|
38
|
-
const
|
|
39
|
-
const displayPath = isManaged
|
|
40
|
-
? path.relative(WORKTREES_ROOT, wt.path)
|
|
41
|
-
: wt.path.replace(process.env.HOME || '', '~');
|
|
27
|
+
const branchName = getWorktreeBranchName(wt);
|
|
28
|
+
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
42
29
|
return {
|
|
43
30
|
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
44
31
|
value: wt
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -1,34 +1,44 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { log } from '../../lib/ui.js';
|
|
2
|
+
import { listWorktrees, getRepoRoot, isGitClean, getLastActivity } from '../../lib/git.js';
|
|
3
|
+
import { log, timeAgo } from '../../lib/ui.js';
|
|
4
|
+
import { detectWorktreeType, formatWorktreeType, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
|
|
6
5
|
export async function listCommand() {
|
|
7
6
|
try {
|
|
8
|
-
|
|
7
|
+
await getRepoRoot(); // Verify we are in a git repo
|
|
9
8
|
const worktrees = await listWorktrees();
|
|
9
|
+
const mainWorktreePath = worktrees[0]?.path || '';
|
|
10
10
|
if (worktrees.length === 0) {
|
|
11
11
|
log.info('No worktrees found.');
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
console.log(chalk.bold('\n Active Worktrees:\n'));
|
|
15
15
|
// Header
|
|
16
|
-
console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('
|
|
17
|
-
console.log(chalk.dim(' ' + '-'.repeat(
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const colorPath = isManaged ? chalk.cyan(displayPath) : chalk.dim(displayPath);
|
|
27
|
-
const isClean = await isGitClean(wt.path);
|
|
16
|
+
console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('LAST ACTIVE')} ${chalk.dim('BRANCH')}`);
|
|
17
|
+
console.log(chalk.dim(' ' + '-'.repeat(70)));
|
|
18
|
+
const rows = await Promise.all(worktrees.map(async (wt, index) => {
|
|
19
|
+
const [typeKey, isClean, lastActive] = await Promise.all([
|
|
20
|
+
detectWorktreeType(wt, mainWorktreePath),
|
|
21
|
+
isGitClean(wt.path),
|
|
22
|
+
getLastActivity(wt.path),
|
|
23
|
+
]);
|
|
24
|
+
const type = formatWorktreeType(typeKey);
|
|
25
|
+
const branchName = getWorktreeBranchName(wt);
|
|
28
26
|
const stateLabel = (isClean ? 'clean' : 'dirty').padEnd(8);
|
|
29
27
|
const stateText = isClean ? chalk.green(stateLabel) : chalk.yellow(stateLabel);
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const activeLabel = lastActive ? timeAgo(lastActive) : '—';
|
|
29
|
+
const activeText = chalk.magenta(activeLabel.padEnd(14));
|
|
30
|
+
return {
|
|
31
|
+
text: ` ${type} ${stateText} ${activeText} ${chalk.yellow(branchName)}`,
|
|
32
|
+
sortType: WORKTREE_TYPE_ORDER[typeKey],
|
|
33
|
+
sortBranch: branchName.toLowerCase(),
|
|
34
|
+
sortIndex: index,
|
|
35
|
+
};
|
|
36
|
+
}));
|
|
37
|
+
rows
|
|
38
|
+
.sort((a, b) => a.sortType - b.sortType ||
|
|
39
|
+
a.sortBranch.localeCompare(b.sortBranch) ||
|
|
40
|
+
a.sortIndex - b.sortIndex)
|
|
41
|
+
.forEach(row => console.log(row.text));
|
|
32
42
|
console.log('');
|
|
33
43
|
}
|
|
34
44
|
catch (error) {
|