yggtree 1.4.2 → 1.4.4
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 +160 -113
- package/dist/commands/wt/create-branch.js +41 -27
- package/dist/commands/wt/create-multi.js +11 -2
- package/dist/commands/wt/create-sandbox.js +3 -1
- package/dist/commands/wt/create.js +132 -51
- package/dist/commands/wt/enter.js +3 -27
- package/dist/commands/wt/handoff.js +7 -0
- package/dist/commands/wt/open.js +269 -60
- package/dist/index.js +149 -126
- package/dist/lib/env-files.js +72 -0
- package/dist/lib/git.js +5 -0
- package/dist/lib/repo-context.js +46 -0
- package/dist/lib/update-check.js +55 -0
- package/package.json +8 -4
- package/dist/commands/wt/close.js +0 -96
|
@@ -5,6 +5,7 @@ import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensure
|
|
|
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 { findLocalEnvFiles, promptAndCopyEnvFilesToWorktrees } from '../../lib/env-files.js';
|
|
8
9
|
import { execa } from 'execa';
|
|
9
10
|
import fs from 'fs-extra';
|
|
10
11
|
export async function createCommandMulti(options) {
|
|
@@ -117,8 +118,16 @@ export async function createCommandMulti(options) {
|
|
|
117
118
|
]);
|
|
118
119
|
}
|
|
119
120
|
createdWorktrees.push(wtPath);
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
}
|
|
122
|
+
const envFiles = await findLocalEnvFiles(repoRoot);
|
|
123
|
+
if (envFiles.length > 0 && createdWorktrees.length > 0) {
|
|
124
|
+
await promptAndCopyEnvFilesToWorktrees(repoRoot, createdWorktrees, envFiles, {
|
|
125
|
+
promptMessage: `Copy local env file${envFiles.length === 1 ? '' : 's'} to all created worktrees? (${envFiles.join(', ')})`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// 4. Bootstrap
|
|
129
|
+
if (shouldBootstrap) {
|
|
130
|
+
for (const wtPath of createdWorktrees) {
|
|
122
131
|
await runBootstrap(wtPath, repoRoot);
|
|
123
132
|
}
|
|
124
133
|
}
|
|
@@ -6,6 +6,7 @@ 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 { promptAndCopyEnvFiles } from '../../lib/env-files.js';
|
|
9
10
|
import { detectInstalledOpenTools, launchOpenTool, promptOpenToolSelection, } from './open.js';
|
|
10
11
|
import { execa } from 'execa';
|
|
11
12
|
import fs from 'fs-extra';
|
|
@@ -66,7 +67,7 @@ export async function createSandboxCommand(options = {}) {
|
|
|
66
67
|
{
|
|
67
68
|
type: 'confirm',
|
|
68
69
|
name: 'shouldOpenTool',
|
|
69
|
-
message: 'Open
|
|
70
|
+
message: 'Open an editor after creation?',
|
|
70
71
|
default: false,
|
|
71
72
|
when: options.exec === undefined && options.open === undefined && options.enter === undefined,
|
|
72
73
|
},
|
|
@@ -181,6 +182,7 @@ export async function createSandboxCommand(options = {}) {
|
|
|
181
182
|
spinner.succeed(`Sandbox created: ${chalk.cyan(sandboxName)}`);
|
|
182
183
|
log.dim('This is a local sandbox - no remote branch will be pushed.');
|
|
183
184
|
// 8. Bootstrap
|
|
185
|
+
await promptAndCopyEnvFiles(repoRoot, wtPath);
|
|
184
186
|
if (shouldBootstrap) {
|
|
185
187
|
await runBootstrap(wtPath, repoRoot);
|
|
186
188
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
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 { detectInstalledOpenTools,
|
|
12
|
+
import { detectInstalledOpenTools, promptOpenActions, resolveOpenToolAction, runOpenActions, } from './open.js';
|
|
11
13
|
import { execa } from 'execa';
|
|
12
14
|
import fs from 'fs-extra';
|
|
13
15
|
function toSlug(value) {
|
|
@@ -16,7 +18,7 @@ function toSlug(value) {
|
|
|
16
18
|
.replace(/[\/\\]/g, '-')
|
|
17
19
|
.replace(/\s+/g, '-');
|
|
18
20
|
}
|
|
19
|
-
async function listBranchCandidates() {
|
|
21
|
+
export async function listBranchCandidates() {
|
|
20
22
|
const [localRefs, remoteRefs] = await Promise.all([
|
|
21
23
|
execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/heads']),
|
|
22
24
|
execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/remotes/origin']),
|
|
@@ -32,21 +34,26 @@ async function listBranchCandidates() {
|
|
|
32
34
|
.filter(ref => ref !== 'origin/HEAD' && !ref.endsWith('/HEAD'))
|
|
33
35
|
.map(ref => ref.replace(/^origin\//, ''));
|
|
34
36
|
const localSet = new Set(localBranches);
|
|
35
|
-
const
|
|
37
|
+
const uniqueRemoteBranches = [...new Set(remoteBranches)];
|
|
36
38
|
const localCandidates = localBranches.map(branchName => ({
|
|
37
39
|
branchName,
|
|
38
40
|
checkoutRef: branchName,
|
|
39
41
|
createLocalBranch: false,
|
|
40
42
|
source: 'local',
|
|
43
|
+
attachedBranchName: branchName,
|
|
44
|
+
sourceLabel: 'local',
|
|
41
45
|
}));
|
|
42
|
-
const remoteCandidates =
|
|
46
|
+
const remoteCandidates = uniqueRemoteBranches.map(branchName => ({
|
|
43
47
|
branchName,
|
|
44
48
|
checkoutRef: `origin/${branchName}`,
|
|
45
|
-
createLocalBranch:
|
|
49
|
+
createLocalBranch: !localSet.has(branchName),
|
|
46
50
|
source: 'remote',
|
|
51
|
+
attachedBranchName: localSet.has(branchName) ? undefined : branchName,
|
|
52
|
+
sourceLabel: localSet.has(branchName) ? 'remote tip, detached' : 'remote, creates local branch',
|
|
47
53
|
}));
|
|
48
54
|
return [...localCandidates, ...remoteCandidates]
|
|
49
|
-
.sort((a, b) => a.branchName.localeCompare(b.branchName)
|
|
55
|
+
.sort((a, b) => a.branchName.localeCompare(b.branchName) ||
|
|
56
|
+
a.checkoutRef.localeCompare(b.checkoutRef));
|
|
50
57
|
}
|
|
51
58
|
function resolveCandidateFromRef(ref, candidates) {
|
|
52
59
|
const trimmedRef = ref.trim();
|
|
@@ -54,18 +61,47 @@ function resolveCandidateFromRef(ref, candidates) {
|
|
|
54
61
|
return undefined;
|
|
55
62
|
if (trimmedRef.startsWith('origin/')) {
|
|
56
63
|
const branchName = trimmedRef.replace(/^origin\//, '');
|
|
57
|
-
return candidates.find(candidate => candidate.branchName === branchName
|
|
64
|
+
return candidates.find(candidate => candidate.branchName === branchName &&
|
|
65
|
+
candidate.checkoutRef === trimmedRef);
|
|
58
66
|
}
|
|
59
67
|
return candidates.find(candidate => candidate.branchName === trimmedRef ||
|
|
60
68
|
candidate.checkoutRef === trimmedRef);
|
|
61
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
|
+
}
|
|
77
|
+
function normalizeExistingPath(value) {
|
|
78
|
+
try {
|
|
79
|
+
return fs.realpathSync(value);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return path.resolve(value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function findWorktreeByPath(worktrees, wtPath) {
|
|
86
|
+
const normalizedPath = normalizeExistingPath(wtPath);
|
|
87
|
+
return worktrees.find(wt => normalizeExistingPath(wt.path) === normalizedPath);
|
|
88
|
+
}
|
|
89
|
+
export function getWorktreePathCollisionMessage(worktreeName, wtPath, worktrees) {
|
|
90
|
+
if (!fs.pathExistsSync(wtPath))
|
|
91
|
+
return undefined;
|
|
92
|
+
const occupyingWorktree = findWorktreeByPath(worktrees, wtPath);
|
|
93
|
+
if (occupyingWorktree?.branch) {
|
|
94
|
+
return `Worktree name "${worktreeName}" is already used by branch "${occupyingWorktree.branch}".`;
|
|
95
|
+
}
|
|
96
|
+
if (occupyingWorktree?.prunable) {
|
|
97
|
+
return `Worktree path exists but Git marks its metadata as prunable: ${occupyingWorktree.prunable}.`;
|
|
98
|
+
}
|
|
99
|
+
return 'Worktree path already exists.';
|
|
100
|
+
}
|
|
62
101
|
export async function createCommand(options) {
|
|
63
102
|
try {
|
|
64
|
-
const repoRoot = await
|
|
103
|
+
const repoRoot = await ensureRepoContext();
|
|
65
104
|
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
66
|
-
if (options.enter !== undefined) {
|
|
67
|
-
log.warning('`--enter` / `--no-enter` is deprecated. Use `--open` / `--no-open` instead.');
|
|
68
|
-
}
|
|
69
105
|
// 1. Load branches
|
|
70
106
|
const loadingSpinner = createSpinner('Fetching branches...').start();
|
|
71
107
|
await fetchAll();
|
|
@@ -96,10 +132,11 @@ export async function createCommand(options) {
|
|
|
96
132
|
source: async (_answers, input = '') => {
|
|
97
133
|
const term = input.trim().toLowerCase();
|
|
98
134
|
const filtered = term
|
|
99
|
-
? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term)
|
|
135
|
+
? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term) ||
|
|
136
|
+
candidate.checkoutRef.toLowerCase().includes(term))
|
|
100
137
|
: candidates;
|
|
101
138
|
return filtered.map(candidate => ({
|
|
102
|
-
name: `${chalk.yellow(candidate.
|
|
139
|
+
name: `${chalk.yellow(candidate.checkoutRef)} ${chalk.dim(`(${candidate.sourceLabel})`)}`,
|
|
103
140
|
value: candidate,
|
|
104
141
|
}));
|
|
105
142
|
},
|
|
@@ -113,44 +150,61 @@ export async function createCommand(options) {
|
|
|
113
150
|
return;
|
|
114
151
|
}
|
|
115
152
|
const existingWorktrees = await listWorktrees();
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
const existingBranchWorktree = findExistingBranchWorktree(existingWorktrees, selectedBranch.attachedBranchName);
|
|
154
|
+
const shouldEnterShell = options.enter !== false;
|
|
155
|
+
if (existingBranchWorktree) {
|
|
156
|
+
log.info(`Branch ${chalk.cyan(selectedBranch.branchName)} is already active in ${ui.path(existingBranchWorktree.path)}.`);
|
|
119
157
|
if (options.exec && options.exec.trim()) {
|
|
120
158
|
log.info('Reusing the existing worktree and running the requested command...');
|
|
121
|
-
await enterCommand(
|
|
159
|
+
await enterCommand(existingBranchWorktree.path, { exec: options.exec });
|
|
122
160
|
return;
|
|
123
161
|
}
|
|
124
|
-
const shouldOpenExisting = options.
|
|
125
|
-
?
|
|
126
|
-
: options.
|
|
127
|
-
? options.
|
|
162
|
+
const shouldOpenExisting = options.tool
|
|
163
|
+
? true
|
|
164
|
+
: options.open !== undefined
|
|
165
|
+
? options.open
|
|
128
166
|
: (await inquirer.prompt([
|
|
129
167
|
{
|
|
130
168
|
type: 'confirm',
|
|
131
169
|
name: 'shouldOpenTool',
|
|
132
|
-
message:
|
|
170
|
+
message: shouldEnterShell
|
|
171
|
+
? 'Open anything before entering the existing worktree shell?'
|
|
172
|
+
: 'Open anything in the existing worktree?',
|
|
133
173
|
default: true,
|
|
134
174
|
},
|
|
135
175
|
])).shouldOpenTool;
|
|
136
176
|
if (!shouldOpenExisting) {
|
|
137
|
-
|
|
177
|
+
if (shouldEnterShell) {
|
|
178
|
+
await enterCommand(existingBranchWorktree.path);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
log.header(`cd "${existingBranchWorktree.path}"`);
|
|
138
182
|
return;
|
|
139
183
|
}
|
|
140
184
|
const installedTools = await detectInstalledOpenTools();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
185
|
+
log.info('Opening existing worktree instead of creating a duplicate...');
|
|
186
|
+
const actions = options.tool
|
|
187
|
+
? [await resolveOpenToolAction(options.tool, installedTools)].filter((action) => Boolean(action))
|
|
188
|
+
: await promptOpenActions(installedTools, {
|
|
189
|
+
allowShellAction: shouldEnterShell,
|
|
190
|
+
message: shouldEnterShell
|
|
191
|
+
? 'Open anything before entering the existing worktree shell?'
|
|
192
|
+
: 'Open anything in the existing worktree?',
|
|
193
|
+
});
|
|
194
|
+
if (options.tool && actions.length === 0) {
|
|
195
|
+
const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
|
|
196
|
+
log.error(`Tool "${options.tool}" not found.`);
|
|
197
|
+
log.dim(`Detected tool commands: ${available}`);
|
|
144
198
|
return;
|
|
145
199
|
}
|
|
146
|
-
|
|
147
|
-
log.info('Opening existing worktree instead of creating a duplicate...');
|
|
148
|
-
await launchOpenTool(selectedTool, existingManagedWorktree.path);
|
|
200
|
+
await runOpenActions(actions, existingBranchWorktree.path, { enter: shouldEnterShell });
|
|
149
201
|
log.success('Existing worktree opened.');
|
|
150
202
|
return;
|
|
151
203
|
}
|
|
152
204
|
// 3. Gather remaining inputs
|
|
153
205
|
const defaultSlug = toSlug(selectedBranch.branchName);
|
|
206
|
+
const repoName = await getRepoName();
|
|
207
|
+
const resolveWorktreePath = (name) => path.join(WORKTREES_ROOT, repoName, toSlug(name));
|
|
154
208
|
const answers = await inquirer.prompt([
|
|
155
209
|
{
|
|
156
210
|
type: 'input',
|
|
@@ -158,7 +212,12 @@ export async function createCommand(options) {
|
|
|
158
212
|
message: 'Worktree name (slug):',
|
|
159
213
|
default: options.name || defaultSlug,
|
|
160
214
|
when: !options.name,
|
|
161
|
-
validate: (input) =>
|
|
215
|
+
validate: (input) => {
|
|
216
|
+
const slug = toSlug(input);
|
|
217
|
+
if (!slug)
|
|
218
|
+
return 'Name is required';
|
|
219
|
+
return getWorktreePathCollisionMessage(slug, resolveWorktreePath(input), existingWorktrees) || true;
|
|
220
|
+
},
|
|
162
221
|
},
|
|
163
222
|
{
|
|
164
223
|
type: 'confirm',
|
|
@@ -170,34 +229,56 @@ export async function createCommand(options) {
|
|
|
170
229
|
{
|
|
171
230
|
type: 'confirm',
|
|
172
231
|
name: 'shouldOpenTool',
|
|
173
|
-
message:
|
|
232
|
+
message: shouldEnterShell
|
|
233
|
+
? 'Open anything before entering the worktree shell?'
|
|
234
|
+
: 'Open anything after creation?',
|
|
174
235
|
default: false,
|
|
175
|
-
when: options.exec === undefined && options.open === undefined && options.
|
|
236
|
+
when: options.exec === undefined && options.open === undefined && options.tool === undefined,
|
|
176
237
|
},
|
|
177
238
|
]);
|
|
178
239
|
const name = options.name || answers.name;
|
|
240
|
+
const slug = toSlug(name);
|
|
241
|
+
const wtPath = resolveWorktreePath(name);
|
|
242
|
+
// 4. Validation
|
|
243
|
+
if (!slug)
|
|
244
|
+
throw new Error('Invalid name');
|
|
245
|
+
const pathCollisionMessage = getWorktreePathCollisionMessage(slug, wtPath, existingWorktrees);
|
|
246
|
+
if (pathCollisionMessage) {
|
|
247
|
+
log.actionableError(pathCollisionMessage, `yggtree wc --name ${slug}`, wtPath, [
|
|
248
|
+
'Choose a different worktree name with --name',
|
|
249
|
+
`Inspect the occupying path: ls ${wtPath}`,
|
|
250
|
+
'Use yggtree wt prune only if Git reports stale worktree metadata',
|
|
251
|
+
]);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
179
254
|
const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
180
|
-
const shouldOpenTool = options.
|
|
181
|
-
?
|
|
182
|
-
: options.
|
|
183
|
-
? options.
|
|
255
|
+
const shouldOpenTool = options.tool
|
|
256
|
+
? true
|
|
257
|
+
: options.open !== undefined
|
|
258
|
+
? options.open
|
|
184
259
|
: Boolean(answers.shouldOpenTool);
|
|
185
|
-
let
|
|
260
|
+
let openActions = [];
|
|
186
261
|
if (options.exec === undefined && shouldOpenTool) {
|
|
187
262
|
const installedTools = await detectInstalledOpenTools();
|
|
188
|
-
if (
|
|
189
|
-
|
|
263
|
+
if (options.tool) {
|
|
264
|
+
const toolAction = await resolveOpenToolAction(options.tool, installedTools);
|
|
265
|
+
if (!toolAction) {
|
|
266
|
+
const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
|
|
267
|
+
log.error(`Tool "${options.tool}" not found.`);
|
|
268
|
+
log.dim(`Detected tool commands: ${available}`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
openActions = [toolAction];
|
|
190
272
|
}
|
|
191
273
|
else {
|
|
192
|
-
|
|
274
|
+
openActions = await promptOpenActions(installedTools, {
|
|
275
|
+
allowShellAction: shouldEnterShell,
|
|
276
|
+
message: shouldEnterShell
|
|
277
|
+
? 'Open anything before entering the worktree shell?'
|
|
278
|
+
: 'Open anything after creation?',
|
|
279
|
+
});
|
|
193
280
|
}
|
|
194
281
|
}
|
|
195
|
-
const slug = toSlug(name);
|
|
196
|
-
const repoName = await getRepoName();
|
|
197
|
-
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
198
|
-
// 4. Validation
|
|
199
|
-
if (!slug)
|
|
200
|
-
throw new Error('Invalid name');
|
|
201
282
|
// 5. Execution (checkout-style: attach to selected branch)
|
|
202
283
|
const spinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
|
|
203
284
|
try {
|
|
@@ -223,6 +304,7 @@ export async function createCommand(options) {
|
|
|
223
304
|
]);
|
|
224
305
|
return;
|
|
225
306
|
}
|
|
307
|
+
await promptAndCopyEnvFiles(repoRoot, wtPath);
|
|
226
308
|
if (shouldBootstrap) {
|
|
227
309
|
await runBootstrap(wtPath, repoRoot);
|
|
228
310
|
}
|
|
@@ -243,13 +325,12 @@ export async function createCommand(options) {
|
|
|
243
325
|
]);
|
|
244
326
|
}
|
|
245
327
|
}
|
|
246
|
-
else
|
|
328
|
+
else {
|
|
247
329
|
try {
|
|
248
|
-
|
|
249
|
-
await launchOpenTool(selectedTool, wtPath);
|
|
330
|
+
await runOpenActions(openActions, wtPath, { enter: shouldEnterShell });
|
|
250
331
|
}
|
|
251
332
|
catch (error) {
|
|
252
|
-
log.warning(`Could not
|
|
333
|
+
log.warning(`Could not complete post-checkout action: ${error.message}`);
|
|
253
334
|
}
|
|
254
335
|
}
|
|
255
336
|
// 7. Final Output
|
|
@@ -2,10 +2,10 @@ 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
|
-
import { getValidRegisteredRepos } from '../../lib/registry.js';
|
|
9
9
|
function truncateEnd(value, maxLen) {
|
|
10
10
|
if (maxLen <= 0)
|
|
11
11
|
return '';
|
|
@@ -37,31 +37,7 @@ function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
|
|
|
37
37
|
}
|
|
38
38
|
export async function enterCommand(wtName, options = {}) {
|
|
39
39
|
try {
|
|
40
|
-
|
|
41
|
-
await getRepoRoot();
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
const validRepos = await getValidRegisteredRepos();
|
|
45
|
-
const repoNames = Object.keys(validRepos);
|
|
46
|
-
if (repoNames.length === 0) {
|
|
47
|
-
log.error('Not inside a git repository and no registered realms found.');
|
|
48
|
-
log.dim('Run `yggtree` inside an existing git project first to register it.');
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
console.log(chalk.bold('\n Not inside a realm. Pick a known one:'));
|
|
52
|
-
const { selectedRepoName } = await inquirer.prompt([{
|
|
53
|
-
type: 'list',
|
|
54
|
-
name: 'selectedRepoName',
|
|
55
|
-
message: 'Select a realm:',
|
|
56
|
-
choices: Object.entries(validRepos).map(([name, rPath]) => ({
|
|
57
|
-
name: `${chalk.bold.yellow(name)} ${chalk.dim(formatWorktreeDisplayPath(rPath))}`,
|
|
58
|
-
value: name,
|
|
59
|
-
})),
|
|
60
|
-
pageSize: 10,
|
|
61
|
-
}]);
|
|
62
|
-
const repoPath = validRepos[selectedRepoName];
|
|
63
|
-
process.chdir(repoPath);
|
|
64
|
-
}
|
|
40
|
+
await ensureRepoContext();
|
|
65
41
|
const worktrees = await listWorktrees();
|
|
66
42
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
67
43
|
if (worktrees.length === 0) {
|