yggtree 1.4.1 → 1.4.3
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 +131 -87
- package/dist/commands/wt/create-branch.js +21 -35
- package/dist/commands/wt/create-multi.js +11 -2
- package/dist/commands/wt/create-sandbox.js +21 -35
- package/dist/commands/wt/create.js +109 -58
- package/dist/commands/wt/enter.js +3 -2
- package/dist/commands/wt/list.js +23 -6
- package/dist/commands/wt/open.js +207 -53
- package/dist/index.js +133 -111
- package/dist/lib/env-files.js +72 -0
- package/dist/lib/git.js +89 -6
- package/dist/lib/registry.js +81 -0
- package/dist/lib/repo-context.js +46 -0
- package/dist/lib/update-check.js +55 -0
- package/dist/lib/worktree.js +12 -0
- package/package.json +12 -3
|
@@ -6,14 +6,17 @@ 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
8
|
import { generateSandboxName, normalizeSandboxName, writeSandboxMeta } from '../../lib/sandbox.js';
|
|
9
|
-
import {
|
|
9
|
+
import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
|
|
10
|
+
import { detectInstalledOpenTools, launchOpenTool, promptOpenToolSelection, } from './open.js';
|
|
10
11
|
import { execa } from 'execa';
|
|
11
|
-
import { spawn } from 'child_process';
|
|
12
12
|
import fs from 'fs-extra';
|
|
13
13
|
export async function createSandboxCommand(options = {}) {
|
|
14
14
|
try {
|
|
15
15
|
const repoRoot = await getRepoRoot();
|
|
16
16
|
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
17
|
+
if (options.enter !== undefined) {
|
|
18
|
+
log.warning('`--enter` / `--no-enter` is deprecated. Use `--open` / `--no-open` instead.');
|
|
19
|
+
}
|
|
17
20
|
// 1. Auto-detect current branch (no prompt!)
|
|
18
21
|
const currentBranch = await getCurrentBranch();
|
|
19
22
|
if (!currentBranch) {
|
|
@@ -61,19 +64,12 @@ export async function createSandboxCommand(options = {}) {
|
|
|
61
64
|
default: true,
|
|
62
65
|
when: options.bootstrap === undefined,
|
|
63
66
|
},
|
|
64
|
-
{
|
|
65
|
-
type: 'confirm',
|
|
66
|
-
name: 'shouldEnter',
|
|
67
|
-
message: 'Do you want to enter the sandbox now?',
|
|
68
|
-
default: true,
|
|
69
|
-
when: options.enter === undefined,
|
|
70
|
-
},
|
|
71
67
|
{
|
|
72
68
|
type: 'confirm',
|
|
73
69
|
name: 'shouldOpenTool',
|
|
74
|
-
message: 'Open
|
|
70
|
+
message: 'Open an editor after creation?',
|
|
75
71
|
default: false,
|
|
76
|
-
when: options.exec === undefined,
|
|
72
|
+
when: options.exec === undefined && options.open === undefined && options.enter === undefined,
|
|
77
73
|
},
|
|
78
74
|
]);
|
|
79
75
|
const requestedSandboxName = options.name !== undefined ? options.name : answers.name || '';
|
|
@@ -89,10 +85,14 @@ export async function createSandboxCommand(options = {}) {
|
|
|
89
85
|
}
|
|
90
86
|
log.info(`Sandbox name: ${chalk.yellow(sandboxName)}`);
|
|
91
87
|
const shouldCarry = options.carry !== undefined ? options.carry : answers.carry;
|
|
92
|
-
const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
|
|
93
88
|
const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
89
|
+
const shouldOpenTool = options.open !== undefined
|
|
90
|
+
? options.open
|
|
91
|
+
: options.enter !== undefined
|
|
92
|
+
? options.enter
|
|
93
|
+
: Boolean(answers.shouldOpenTool);
|
|
94
94
|
let selectedTool;
|
|
95
|
-
if (options.exec === undefined &&
|
|
95
|
+
if (options.exec === undefined && shouldOpenTool) {
|
|
96
96
|
const installedTools = await detectInstalledOpenTools();
|
|
97
97
|
if (installedTools.length === 0) {
|
|
98
98
|
log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
|
|
@@ -101,7 +101,6 @@ export async function createSandboxCommand(options = {}) {
|
|
|
101
101
|
selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
-
const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
|
|
105
104
|
// 4. Detect uncommitted changes before creating worktree
|
|
106
105
|
let changedFiles = [];
|
|
107
106
|
let submodulePaths = [];
|
|
@@ -183,27 +182,28 @@ export async function createSandboxCommand(options = {}) {
|
|
|
183
182
|
spinner.succeed(`Sandbox created: ${chalk.cyan(sandboxName)}`);
|
|
184
183
|
log.dim('This is a local sandbox - no remote branch will be pushed.');
|
|
185
184
|
// 8. Bootstrap
|
|
185
|
+
await promptAndCopyEnvFiles(repoRoot, wtPath);
|
|
186
186
|
if (shouldBootstrap) {
|
|
187
187
|
await runBootstrap(wtPath, repoRoot);
|
|
188
188
|
}
|
|
189
189
|
// 9. Exec command
|
|
190
|
-
if (
|
|
191
|
-
log.info(`Executing: ${
|
|
190
|
+
if (options.exec && options.exec.trim()) {
|
|
191
|
+
log.info(`Executing: ${options.exec} in ${ui.path(wtPath)}`);
|
|
192
192
|
try {
|
|
193
|
-
await execa(
|
|
193
|
+
await execa(options.exec, {
|
|
194
194
|
cwd: wtPath,
|
|
195
195
|
stdio: 'inherit',
|
|
196
196
|
shell: true
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
catch (error) {
|
|
200
|
-
log.actionableError(error.message,
|
|
201
|
-
`cd ${wtPath} && ${
|
|
200
|
+
log.actionableError(error.message, options.exec, wtPath, [
|
|
201
|
+
`cd ${wtPath} && ${options.exec}`,
|
|
202
202
|
'Check your command syntax and environment variables'
|
|
203
203
|
]);
|
|
204
204
|
}
|
|
205
205
|
}
|
|
206
|
-
if (selectedTool
|
|
206
|
+
else if (selectedTool) {
|
|
207
207
|
try {
|
|
208
208
|
log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
|
|
209
209
|
await launchOpenTool(selectedTool, wtPath);
|
|
@@ -216,21 +216,7 @@ export async function createSandboxCommand(options = {}) {
|
|
|
216
216
|
log.success('Sandbox ready!');
|
|
217
217
|
log.info(`Use ${chalk.cyan('yggtree wt apply')} to apply changes to origin.`);
|
|
218
218
|
log.info(`Use ${chalk.cyan('yggtree wt unapply')} to undo applied changes.`);
|
|
219
|
-
|
|
220
|
-
log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
|
|
221
|
-
log.dim('Type "exit" to return to the main terminal.');
|
|
222
|
-
const shell = process.env.SHELL || 'zsh';
|
|
223
|
-
const child = spawn(shell, [], {
|
|
224
|
-
cwd: wtPath,
|
|
225
|
-
stdio: 'inherit',
|
|
226
|
-
});
|
|
227
|
-
child.on('close', () => {
|
|
228
|
-
log.info('Exited sub-shell.');
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
log.header(`cd "${wtPath}"`);
|
|
233
|
-
}
|
|
219
|
+
log.header(`cd "${wtPath}"`);
|
|
234
220
|
}
|
|
235
221
|
catch (error) {
|
|
236
222
|
log.actionableError(error.message, 'yggtree wt create-sandbox');
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { 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
8
|
import { ensureAutocompletePrompt } from '../../lib/prompt.js';
|
|
9
|
+
import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
|
|
10
|
+
import { ensureRepoContext } from '../../lib/repo-context.js';
|
|
9
11
|
import { enterCommand } from './enter.js';
|
|
10
|
-
import {
|
|
12
|
+
import { detectInstalledOpenTools, promptOpenActions, resolveOpenToolAction, runOpenActions, } from './open.js';
|
|
11
13
|
import { execa } from 'execa';
|
|
12
|
-
import { spawn } from 'child_process';
|
|
13
14
|
import fs from 'fs-extra';
|
|
14
15
|
function toSlug(value) {
|
|
15
16
|
return value
|
|
@@ -17,7 +18,7 @@ function toSlug(value) {
|
|
|
17
18
|
.replace(/[\/\\]/g, '-')
|
|
18
19
|
.replace(/\s+/g, '-');
|
|
19
20
|
}
|
|
20
|
-
async function listBranchCandidates() {
|
|
21
|
+
export async function listBranchCandidates() {
|
|
21
22
|
const [localRefs, remoteRefs] = await Promise.all([
|
|
22
23
|
execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/heads']),
|
|
23
24
|
execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/remotes/origin']),
|
|
@@ -33,21 +34,26 @@ async function listBranchCandidates() {
|
|
|
33
34
|
.filter(ref => ref !== 'origin/HEAD' && !ref.endsWith('/HEAD'))
|
|
34
35
|
.map(ref => ref.replace(/^origin\//, ''));
|
|
35
36
|
const localSet = new Set(localBranches);
|
|
36
|
-
const
|
|
37
|
+
const uniqueRemoteBranches = [...new Set(remoteBranches)];
|
|
37
38
|
const localCandidates = localBranches.map(branchName => ({
|
|
38
39
|
branchName,
|
|
39
40
|
checkoutRef: branchName,
|
|
40
41
|
createLocalBranch: false,
|
|
41
42
|
source: 'local',
|
|
43
|
+
attachedBranchName: branchName,
|
|
44
|
+
sourceLabel: 'local',
|
|
42
45
|
}));
|
|
43
|
-
const remoteCandidates =
|
|
46
|
+
const remoteCandidates = uniqueRemoteBranches.map(branchName => ({
|
|
44
47
|
branchName,
|
|
45
48
|
checkoutRef: `origin/${branchName}`,
|
|
46
|
-
createLocalBranch:
|
|
49
|
+
createLocalBranch: !localSet.has(branchName),
|
|
47
50
|
source: 'remote',
|
|
51
|
+
attachedBranchName: localSet.has(branchName) ? undefined : branchName,
|
|
52
|
+
sourceLabel: localSet.has(branchName) ? 'remote tip, detached' : 'remote, creates local branch',
|
|
48
53
|
}));
|
|
49
54
|
return [...localCandidates, ...remoteCandidates]
|
|
50
|
-
.sort((a, b) => a.branchName.localeCompare(b.branchName)
|
|
55
|
+
.sort((a, b) => a.branchName.localeCompare(b.branchName) ||
|
|
56
|
+
a.checkoutRef.localeCompare(b.checkoutRef));
|
|
51
57
|
}
|
|
52
58
|
function resolveCandidateFromRef(ref, candidates) {
|
|
53
59
|
const trimmedRef = ref.trim();
|
|
@@ -55,14 +61,22 @@ function resolveCandidateFromRef(ref, candidates) {
|
|
|
55
61
|
return undefined;
|
|
56
62
|
if (trimmedRef.startsWith('origin/')) {
|
|
57
63
|
const branchName = trimmedRef.replace(/^origin\//, '');
|
|
58
|
-
return candidates.find(candidate => candidate.branchName === branchName
|
|
64
|
+
return candidates.find(candidate => candidate.branchName === branchName &&
|
|
65
|
+
candidate.checkoutRef === trimmedRef);
|
|
59
66
|
}
|
|
60
67
|
return candidates.find(candidate => candidate.branchName === trimmedRef ||
|
|
61
68
|
candidate.checkoutRef === trimmedRef);
|
|
62
69
|
}
|
|
70
|
+
export function findExistingBranchWorktree(worktrees, branchName) {
|
|
71
|
+
if (!branchName)
|
|
72
|
+
return undefined;
|
|
73
|
+
return worktrees.find(wt => wt.branch === branchName &&
|
|
74
|
+
!wt.prunable &&
|
|
75
|
+
fs.pathExistsSync(wt.path));
|
|
76
|
+
}
|
|
63
77
|
export async function createCommand(options) {
|
|
64
78
|
try {
|
|
65
|
-
const repoRoot = await
|
|
79
|
+
const repoRoot = await ensureRepoContext();
|
|
66
80
|
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
67
81
|
// 1. Load branches
|
|
68
82
|
const loadingSpinner = createSpinner('Fetching branches...').start();
|
|
@@ -94,10 +108,11 @@ export async function createCommand(options) {
|
|
|
94
108
|
source: async (_answers, input = '') => {
|
|
95
109
|
const term = input.trim().toLowerCase();
|
|
96
110
|
const filtered = term
|
|
97
|
-
? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term)
|
|
111
|
+
? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term) ||
|
|
112
|
+
candidate.checkoutRef.toLowerCase().includes(term))
|
|
98
113
|
: candidates;
|
|
99
114
|
return filtered.map(candidate => ({
|
|
100
|
-
name: `${chalk.yellow(candidate.
|
|
115
|
+
name: `${chalk.yellow(candidate.checkoutRef)} ${chalk.dim(`(${candidate.sourceLabel})`)}`,
|
|
101
116
|
value: candidate,
|
|
102
117
|
}));
|
|
103
118
|
},
|
|
@@ -111,15 +126,55 @@ export async function createCommand(options) {
|
|
|
111
126
|
return;
|
|
112
127
|
}
|
|
113
128
|
const existingWorktrees = await listWorktrees();
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
129
|
+
const existingBranchWorktree = findExistingBranchWorktree(existingWorktrees, selectedBranch.attachedBranchName);
|
|
130
|
+
const shouldEnterShell = options.enter !== false;
|
|
131
|
+
if (existingBranchWorktree) {
|
|
132
|
+
log.info(`Branch ${chalk.cyan(selectedBranch.branchName)} is already active in ${ui.path(existingBranchWorktree.path)}.`);
|
|
133
|
+
if (options.exec && options.exec.trim()) {
|
|
134
|
+
log.info('Reusing the existing worktree and running the requested command...');
|
|
135
|
+
await enterCommand(existingBranchWorktree.path, { exec: options.exec });
|
|
119
136
|
return;
|
|
120
137
|
}
|
|
138
|
+
const shouldOpenExisting = options.tool
|
|
139
|
+
? true
|
|
140
|
+
: options.open !== undefined
|
|
141
|
+
? options.open
|
|
142
|
+
: (await inquirer.prompt([
|
|
143
|
+
{
|
|
144
|
+
type: 'confirm',
|
|
145
|
+
name: 'shouldOpenTool',
|
|
146
|
+
message: shouldEnterShell
|
|
147
|
+
? 'Open anything before entering the existing worktree shell?'
|
|
148
|
+
: 'Open anything in the existing worktree?',
|
|
149
|
+
default: true,
|
|
150
|
+
},
|
|
151
|
+
])).shouldOpenTool;
|
|
152
|
+
if (!shouldOpenExisting) {
|
|
153
|
+
if (shouldEnterShell) {
|
|
154
|
+
await enterCommand(existingBranchWorktree.path);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
log.header(`cd "${existingBranchWorktree.path}"`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const installedTools = await detectInstalledOpenTools();
|
|
121
161
|
log.info('Opening existing worktree instead of creating a duplicate...');
|
|
122
|
-
|
|
162
|
+
const actions = options.tool
|
|
163
|
+
? [await resolveOpenToolAction(options.tool, installedTools)].filter((action) => Boolean(action))
|
|
164
|
+
: await promptOpenActions(installedTools, {
|
|
165
|
+
allowShellAction: shouldEnterShell,
|
|
166
|
+
message: shouldEnterShell
|
|
167
|
+
? 'Open anything before entering the existing worktree shell?'
|
|
168
|
+
: 'Open anything in the existing worktree?',
|
|
169
|
+
});
|
|
170
|
+
if (options.tool && actions.length === 0) {
|
|
171
|
+
const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
|
|
172
|
+
log.error(`Tool "${options.tool}" not found.`);
|
|
173
|
+
log.dim(`Detected tool commands: ${available}`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
await runOpenActions(actions, existingBranchWorktree.path, { enter: shouldEnterShell });
|
|
177
|
+
log.success('Existing worktree opened.');
|
|
123
178
|
return;
|
|
124
179
|
}
|
|
125
180
|
// 3. Gather remaining inputs
|
|
@@ -140,35 +195,45 @@ export async function createCommand(options) {
|
|
|
140
195
|
default: true,
|
|
141
196
|
when: options.bootstrap !== false && options.bootstrap !== true,
|
|
142
197
|
},
|
|
143
|
-
{
|
|
144
|
-
type: 'confirm',
|
|
145
|
-
name: 'shouldEnter',
|
|
146
|
-
message: 'Do you want to enter the new worktree now?',
|
|
147
|
-
default: true,
|
|
148
|
-
when: options.enter === undefined,
|
|
149
|
-
},
|
|
150
198
|
{
|
|
151
199
|
type: 'confirm',
|
|
152
200
|
name: 'shouldOpenTool',
|
|
153
|
-
message:
|
|
201
|
+
message: shouldEnterShell
|
|
202
|
+
? 'Open anything before entering the worktree shell?'
|
|
203
|
+
: 'Open anything after creation?',
|
|
154
204
|
default: false,
|
|
155
|
-
when: options.exec === undefined,
|
|
205
|
+
when: options.exec === undefined && options.open === undefined && options.tool === undefined,
|
|
156
206
|
},
|
|
157
207
|
]);
|
|
158
208
|
const name = options.name || answers.name;
|
|
159
|
-
const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
|
|
160
209
|
const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
161
|
-
|
|
162
|
-
|
|
210
|
+
const shouldOpenTool = options.tool
|
|
211
|
+
? true
|
|
212
|
+
: options.open !== undefined
|
|
213
|
+
? options.open
|
|
214
|
+
: Boolean(answers.shouldOpenTool);
|
|
215
|
+
let openActions = [];
|
|
216
|
+
if (options.exec === undefined && shouldOpenTool) {
|
|
163
217
|
const installedTools = await detectInstalledOpenTools();
|
|
164
|
-
if (
|
|
165
|
-
|
|
218
|
+
if (options.tool) {
|
|
219
|
+
const toolAction = await resolveOpenToolAction(options.tool, installedTools);
|
|
220
|
+
if (!toolAction) {
|
|
221
|
+
const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
|
|
222
|
+
log.error(`Tool "${options.tool}" not found.`);
|
|
223
|
+
log.dim(`Detected tool commands: ${available}`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
openActions = [toolAction];
|
|
166
227
|
}
|
|
167
228
|
else {
|
|
168
|
-
|
|
229
|
+
openActions = await promptOpenActions(installedTools, {
|
|
230
|
+
allowShellAction: shouldEnterShell,
|
|
231
|
+
message: shouldEnterShell
|
|
232
|
+
? 'Open anything before entering the worktree shell?'
|
|
233
|
+
: 'Open anything after creation?',
|
|
234
|
+
});
|
|
169
235
|
}
|
|
170
236
|
}
|
|
171
|
-
const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
|
|
172
237
|
const slug = toSlug(name);
|
|
173
238
|
const repoName = await getRepoName();
|
|
174
239
|
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
@@ -200,52 +265,38 @@ export async function createCommand(options) {
|
|
|
200
265
|
]);
|
|
201
266
|
return;
|
|
202
267
|
}
|
|
268
|
+
await promptAndCopyEnvFiles(repoRoot, wtPath);
|
|
203
269
|
if (shouldBootstrap) {
|
|
204
270
|
await runBootstrap(wtPath, repoRoot);
|
|
205
271
|
}
|
|
206
272
|
// 6. Exec Command
|
|
207
|
-
if (
|
|
208
|
-
log.info(`Executing: ${
|
|
273
|
+
if (options.exec && options.exec.trim()) {
|
|
274
|
+
log.info(`Executing: ${options.exec} in ${ui.path(wtPath)}`);
|
|
209
275
|
try {
|
|
210
|
-
await execa(
|
|
276
|
+
await execa(options.exec, {
|
|
211
277
|
cwd: wtPath,
|
|
212
278
|
stdio: 'inherit',
|
|
213
279
|
shell: true
|
|
214
280
|
});
|
|
215
281
|
}
|
|
216
282
|
catch (error) {
|
|
217
|
-
log.actionableError(error.message,
|
|
218
|
-
`cd ${wtPath} && ${
|
|
283
|
+
log.actionableError(error.message, options.exec, wtPath, [
|
|
284
|
+
`cd ${wtPath} && ${options.exec}`,
|
|
219
285
|
'Check your command syntax and environment variables'
|
|
220
286
|
]);
|
|
221
287
|
}
|
|
222
288
|
}
|
|
223
|
-
|
|
289
|
+
else {
|
|
224
290
|
try {
|
|
225
|
-
|
|
226
|
-
await launchOpenTool(selectedTool, wtPath);
|
|
291
|
+
await runOpenActions(openActions, wtPath, { enter: shouldEnterShell });
|
|
227
292
|
}
|
|
228
293
|
catch (error) {
|
|
229
|
-
log.warning(`Could not
|
|
294
|
+
log.warning(`Could not complete post-checkout action: ${error.message}`);
|
|
230
295
|
}
|
|
231
296
|
}
|
|
232
297
|
// 7. Final Output
|
|
233
298
|
log.success('Worktree ready!');
|
|
234
|
-
|
|
235
|
-
log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
|
|
236
|
-
log.dim('Type "exit" to return to the main terminal.');
|
|
237
|
-
const shell = process.env.SHELL || 'zsh';
|
|
238
|
-
const child = spawn(shell, [], {
|
|
239
|
-
cwd: wtPath,
|
|
240
|
-
stdio: 'inherit',
|
|
241
|
-
});
|
|
242
|
-
child.on('close', () => {
|
|
243
|
-
log.info('Exited sub-shell.');
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
log.header(`cd "${wtPath}"`);
|
|
248
|
-
}
|
|
299
|
+
log.header(`cd "${wtPath}"`);
|
|
249
300
|
}
|
|
250
301
|
catch (error) {
|
|
251
302
|
log.actionableError(error.message, 'yggtree wt worktree-checkout');
|
|
@@ -2,8 +2,9 @@ import inquirer from 'inquirer';
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
|
-
import { listWorktrees
|
|
5
|
+
import { listWorktrees } from '../../lib/git.js';
|
|
6
6
|
import { log, ui } from '../../lib/ui.js';
|
|
7
|
+
import { ensureRepoContext } from '../../lib/repo-context.js';
|
|
7
8
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
8
9
|
function truncateEnd(value, maxLen) {
|
|
9
10
|
if (maxLen <= 0)
|
|
@@ -36,7 +37,7 @@ function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
|
|
|
36
37
|
}
|
|
37
38
|
export async function enterCommand(wtName, options = {}) {
|
|
38
39
|
try {
|
|
39
|
-
await
|
|
40
|
+
await ensureRepoContext();
|
|
40
41
|
const worktrees = await listWorktrees();
|
|
41
42
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
42
43
|
if (worktrees.length === 0) {
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import { listWorktrees, getRepoRoot, isGitClean, getLastActivity } from '../../lib/git.js';
|
|
2
|
+
import { listWorktrees, getRepoRoot, isGitClean, getLastActivity, isGhAvailable, getPrStatusBatch } from '../../lib/git.js';
|
|
3
3
|
import { log, timeAgo } from '../../lib/ui.js';
|
|
4
|
-
import { detectWorktreeType, formatWorktreeType, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
|
|
4
|
+
import { detectWorktreeType, formatWorktreeType, formatPrStatus, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
|
|
5
5
|
export async function listCommand() {
|
|
6
6
|
try {
|
|
7
7
|
await getRepoRoot(); // Verify we are in a git repo
|
|
@@ -11,10 +11,17 @@ export async function listCommand() {
|
|
|
11
11
|
log.info('No worktrees found.');
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
+
// Determine if PR status column should be shown
|
|
15
|
+
const ghReady = await isGhAvailable();
|
|
16
|
+
// Collect branch names for batch PR lookup
|
|
17
|
+
const branches = worktrees.map(wt => getWorktreeBranchName(wt));
|
|
18
|
+
const prStatusMap = ghReady ? await getPrStatusBatch(branches) : new Map();
|
|
19
|
+
const showPr = prStatusMap.size > 0;
|
|
14
20
|
console.log(chalk.bold('\n Active Worktrees:\n'));
|
|
15
|
-
// Header
|
|
16
|
-
|
|
17
|
-
console.log(chalk.dim('
|
|
21
|
+
// Header — PR column only appears when there's data
|
|
22
|
+
const headerPr = showPr ? `${chalk.dim('PR')} ` : '';
|
|
23
|
+
console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('LAST ACTIVE')} ${headerPr}${chalk.dim('BRANCH')}`);
|
|
24
|
+
console.log(chalk.dim(' ' + '-'.repeat(showPr ? 90 : 70)));
|
|
18
25
|
const rows = await Promise.all(worktrees.map(async (wt, index) => {
|
|
19
26
|
const [typeKey, isClean, lastActive] = await Promise.all([
|
|
20
27
|
detectWorktreeType(wt, mainWorktreePath),
|
|
@@ -27,8 +34,12 @@ export async function listCommand() {
|
|
|
27
34
|
const stateText = isClean ? chalk.green(stateLabel) : chalk.yellow(stateLabel);
|
|
28
35
|
const activeLabel = lastActive ? timeAgo(lastActive) : '—';
|
|
29
36
|
const activeText = chalk.magenta(activeLabel.padEnd(14));
|
|
37
|
+
const prStatus = prStatusMap.get(branchName);
|
|
38
|
+
const prText = showPr
|
|
39
|
+
? (prStatus ? formatPrStatus(prStatus).padEnd(24) : chalk.dim('—'.padEnd(14)))
|
|
40
|
+
: '';
|
|
30
41
|
return {
|
|
31
|
-
text: ` ${type} ${stateText} ${activeText} ${chalk.yellow(branchName)}`,
|
|
42
|
+
text: ` ${type} ${stateText} ${activeText} ${prText}${chalk.yellow(branchName)}`,
|
|
32
43
|
sortType: WORKTREE_TYPE_ORDER[typeKey],
|
|
33
44
|
sortBranch: branchName.toLowerCase(),
|
|
34
45
|
sortIndex: index,
|
|
@@ -39,6 +50,12 @@ export async function listCommand() {
|
|
|
39
50
|
a.sortBranch.localeCompare(b.sortBranch) ||
|
|
40
51
|
a.sortIndex - b.sortIndex)
|
|
41
52
|
.forEach(row => console.log(row.text));
|
|
53
|
+
if (ghReady && !showPr) {
|
|
54
|
+
console.log(chalk.dim('\n ℹ No open PRs found for any worktree branch.'));
|
|
55
|
+
}
|
|
56
|
+
else if (!ghReady) {
|
|
57
|
+
console.log(chalk.dim('\n ℹ PR status omitted (gh CLI not found). Install GitHub CLI for PR tracking.'));
|
|
58
|
+
}
|
|
42
59
|
console.log('');
|
|
43
60
|
}
|
|
44
61
|
catch (error) {
|