yggtree 1.4.4 β 1.4.6
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 +103 -22
- package/dist/commands/config.js +53 -0
- package/dist/commands/wt/bootstrap.js +6 -5
- package/dist/commands/wt/copy-env.js +31 -0
- package/dist/commands/wt/create-branch.js +3 -2
- package/dist/commands/wt/create-multi.js +3 -2
- package/dist/commands/wt/create-sandbox.js +3 -2
- package/dist/commands/wt/create.js +3 -2
- package/dist/commands/wt/delete.js +102 -44
- package/dist/commands/wt/enter.js +6 -4
- package/dist/commands/wt/exec.js +5 -3
- package/dist/commands/wt/list.js +4 -2
- package/dist/commands/wt/open.js +30 -12
- package/dist/commands/wt/path.js +5 -3
- package/dist/index.js +141 -57
- package/dist/lib/global-config.js +118 -0
- package/dist/lib/help.js +127 -0
- package/dist/lib/main-menu.js +71 -0
- package/dist/lib/paths.js +0 -1
- package/dist/lib/repo-context.js +3 -1
- package/dist/lib/ui.js +77 -6
- package/dist/lib/worktree.js +14 -10
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { listWorktrees } from '../../lib/git.js';
|
|
6
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
6
7
|
import { log, ui } from '../../lib/ui.js';
|
|
7
8
|
import { ensureRepoContext } from '../../lib/repo-context.js';
|
|
8
9
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
@@ -37,16 +38,17 @@ function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
|
|
|
37
38
|
}
|
|
38
39
|
export async function enterCommand(wtName, options = {}) {
|
|
39
40
|
try {
|
|
40
|
-
await ensureRepoContext();
|
|
41
|
+
const repoRoot = await ensureRepoContext();
|
|
41
42
|
const worktrees = await listWorktrees();
|
|
42
43
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
44
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
43
45
|
if (worktrees.length === 0) {
|
|
44
46
|
log.info('No worktrees found.');
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
let targetWt;
|
|
48
50
|
if (wtName) {
|
|
49
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
51
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
50
52
|
if (!targetWt) {
|
|
51
53
|
log.error(`Worktree "${wtName}" not found.`);
|
|
52
54
|
return;
|
|
@@ -56,9 +58,9 @@ export async function enterCommand(wtName, options = {}) {
|
|
|
56
58
|
// Interactive Selection
|
|
57
59
|
const terminalColumns = process.stdout.columns || 100;
|
|
58
60
|
const choices = await Promise.all(worktrees.map(async (wt) => {
|
|
59
|
-
const type = await detectWorktreeType(wt, mainWorktreePath);
|
|
61
|
+
const type = await detectWorktreeType(wt, mainWorktreePath, managedRoot);
|
|
60
62
|
const branchName = getWorktreeBranchName(wt);
|
|
61
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
63
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
62
64
|
return {
|
|
63
65
|
name: formatChoiceLabel(type, branchName, displayPath, terminalColumns),
|
|
64
66
|
value: wt
|
package/dist/commands/wt/exec.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
3
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
4
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
4
5
|
import { log } from '../../lib/ui.js';
|
|
5
6
|
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
6
7
|
export async function execCommand(wtName, commandArgs) {
|
|
@@ -8,14 +9,15 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
8
9
|
let command = '';
|
|
9
10
|
let args = [];
|
|
10
11
|
try {
|
|
11
|
-
await getRepoRoot();
|
|
12
|
+
const repoRoot = await getRepoRoot();
|
|
12
13
|
const worktrees = await listWorktrees();
|
|
14
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
13
15
|
if (worktrees.length === 0) {
|
|
14
16
|
log.info('No worktrees found.');
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
19
|
if (wtName) {
|
|
18
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
20
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
19
21
|
if (!targetWt) {
|
|
20
22
|
log.error(`Worktree "${wtName}" not found.`);
|
|
21
23
|
return;
|
|
@@ -25,7 +27,7 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
25
27
|
// Interactive Selection
|
|
26
28
|
const choices = worktrees.map(wt => {
|
|
27
29
|
const branchName = getWorktreeBranchName(wt);
|
|
28
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
30
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
29
31
|
return {
|
|
30
32
|
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
31
33
|
value: wt
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { listWorktrees, getRepoRoot, isGitClean, getLastActivity, isGhAvailable, getPrStatusBatch } from '../../lib/git.js';
|
|
3
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
3
4
|
import { log, timeAgo } from '../../lib/ui.js';
|
|
4
5
|
import { detectWorktreeType, formatWorktreeType, formatPrStatus, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
|
|
5
6
|
export async function listCommand() {
|
|
6
7
|
try {
|
|
7
|
-
await getRepoRoot(); // Verify we are in a git repo
|
|
8
|
+
const repoRoot = await getRepoRoot(); // Verify we are in a git repo
|
|
8
9
|
const worktrees = await listWorktrees();
|
|
9
10
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
11
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
10
12
|
if (worktrees.length === 0) {
|
|
11
13
|
log.info('No worktrees found.');
|
|
12
14
|
return;
|
|
@@ -24,7 +26,7 @@ export async function listCommand() {
|
|
|
24
26
|
console.log(chalk.dim(' ' + '-'.repeat(showPr ? 90 : 70)));
|
|
25
27
|
const rows = await Promise.all(worktrees.map(async (wt, index) => {
|
|
26
28
|
const [typeKey, isClean, lastActive] = await Promise.all([
|
|
27
|
-
detectWorktreeType(wt, mainWorktreePath),
|
|
29
|
+
detectWorktreeType(wt, mainWorktreePath, managedRoot),
|
|
28
30
|
isGitClean(wt.path),
|
|
29
31
|
getLastActivity(wt.path),
|
|
30
32
|
]);
|
package/dist/commands/wt/open.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
|
|
|
5
5
|
import { constants as fsConstants } from 'fs';
|
|
6
6
|
import { execa } from 'execa';
|
|
7
7
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
8
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
8
9
|
import { log, ui } from '../../lib/ui.js';
|
|
9
10
|
import { ensureAutocompletePrompt } from '../../lib/prompt.js';
|
|
10
11
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
@@ -24,6 +25,7 @@ export const OPEN_TOOL_CANDIDATES = [
|
|
|
24
25
|
kind: 'app',
|
|
25
26
|
aliases: ['codex', 'codex-app'],
|
|
26
27
|
bundleId: 'com.openai.codex',
|
|
28
|
+
requiredCommand: 'codex',
|
|
27
29
|
},
|
|
28
30
|
{ id: 'cmux', name: 'Cmux Panel', command: 'cmux', kind: 'terminal', aliases: ['cmux', 'cmux-panel'] },
|
|
29
31
|
{ id: 'tmux', name: 'Tmux Session', command: 'tmux', kind: 'terminal', aliases: ['tmux', 'tmux-session'] },
|
|
@@ -175,16 +177,27 @@ export async function macOSAppBundleExists(bundleId) {
|
|
|
175
177
|
export async function detectInstalledOpenTools() {
|
|
176
178
|
const checks = await Promise.all(OPEN_TOOL_CANDIDATES.map(async (tool) => ({
|
|
177
179
|
tool,
|
|
178
|
-
exists: tool
|
|
179
|
-
? await macOSAppBundleExists(tool.bundleId)
|
|
180
|
-
: await commandExists(tool.command),
|
|
180
|
+
exists: await isOpenToolAvailable(tool),
|
|
181
181
|
})));
|
|
182
182
|
return checks
|
|
183
183
|
.filter(check => check.exists)
|
|
184
184
|
.map(check => check.tool);
|
|
185
185
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
186
|
+
export async function isOpenToolAvailable(tool, checks = {}) {
|
|
187
|
+
const hasCommand = checks.commandExists || commandExists;
|
|
188
|
+
const hasAppBundle = checks.macOSAppBundleExists || macOSAppBundleExists;
|
|
189
|
+
if (tool.requiredCommand) {
|
|
190
|
+
const commandAvailable = await hasCommand(tool.requiredCommand);
|
|
191
|
+
if (!commandAvailable)
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (tool.bundleId) {
|
|
195
|
+
return hasAppBundle(tool.bundleId);
|
|
196
|
+
}
|
|
197
|
+
return hasCommand(tool.command);
|
|
198
|
+
}
|
|
199
|
+
function resolveWorktreeByName(worktrees, wtName, managedRoot) {
|
|
200
|
+
return findWorktreeByName(worktrees, wtName, managedRoot);
|
|
188
201
|
}
|
|
189
202
|
export function resolveOpenToolOption(input, installed) {
|
|
190
203
|
const normalized = input.trim().toLowerCase();
|
|
@@ -202,9 +215,7 @@ async function resolveKnownOpenToolOption(input) {
|
|
|
202
215
|
const candidate = resolveOpenToolCandidate(input);
|
|
203
216
|
if (!candidate)
|
|
204
217
|
return undefined;
|
|
205
|
-
const exists = candidate
|
|
206
|
-
? await macOSAppBundleExists(candidate.bundleId)
|
|
207
|
-
: await commandExists(candidate.command);
|
|
218
|
+
const exists = await isOpenToolAvailable(candidate);
|
|
208
219
|
return exists ? candidate : undefined;
|
|
209
220
|
}
|
|
210
221
|
export async function resolveOpenToolAction(input, installed) {
|
|
@@ -331,6 +342,12 @@ export async function launchOpenTool(tool, wtPath) {
|
|
|
331
342
|
});
|
|
332
343
|
}
|
|
333
344
|
export function buildOpenToolLaunchCommand(tool, wtPath) {
|
|
345
|
+
if (tool.id === 'codex-app') {
|
|
346
|
+
return {
|
|
347
|
+
executable: 'codex',
|
|
348
|
+
args: ['app', wtPath],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
334
351
|
if (tool.bundleId) {
|
|
335
352
|
return {
|
|
336
353
|
executable: '/usr/bin/open',
|
|
@@ -364,16 +381,17 @@ export async function runOpenActions(actions, wtPath, options = {}) {
|
|
|
364
381
|
}
|
|
365
382
|
export async function openCommand(wtName, options = {}) {
|
|
366
383
|
try {
|
|
367
|
-
await getRepoRoot();
|
|
384
|
+
const repoRoot = await getRepoRoot();
|
|
368
385
|
const worktrees = await listWorktrees();
|
|
369
386
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
387
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
370
388
|
if (worktrees.length === 0) {
|
|
371
389
|
log.info('No worktrees found.');
|
|
372
390
|
return;
|
|
373
391
|
}
|
|
374
392
|
let targetWt;
|
|
375
393
|
if (wtName) {
|
|
376
|
-
targetWt = resolveWorktreeByName(worktrees, wtName);
|
|
394
|
+
targetWt = resolveWorktreeByName(worktrees, wtName, managedRoot);
|
|
377
395
|
if (!targetWt) {
|
|
378
396
|
log.error(`Worktree "${wtName}" not found.`);
|
|
379
397
|
return;
|
|
@@ -383,9 +401,9 @@ export async function openCommand(wtName, options = {}) {
|
|
|
383
401
|
ensureAutocompletePrompt();
|
|
384
402
|
const terminalColumns = process.stdout.columns || 100;
|
|
385
403
|
const candidates = await Promise.all(worktrees.map(async (wt) => {
|
|
386
|
-
const type = await detectWorktreeType(wt, mainWorktreePath);
|
|
404
|
+
const type = await detectWorktreeType(wt, mainWorktreePath, managedRoot);
|
|
387
405
|
const branchName = getWorktreeBranchName(wt);
|
|
388
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
406
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
389
407
|
const rawType = type.toLowerCase();
|
|
390
408
|
const rawDisplayPath = displayPath.toLowerCase();
|
|
391
409
|
const rawBranchName = branchName.toLowerCase();
|
package/dist/commands/wt/path.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
3
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
3
4
|
import { log } from '../../lib/ui.js';
|
|
4
5
|
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
5
6
|
export async function pathCommand(wtName) {
|
|
6
7
|
try {
|
|
7
|
-
await getRepoRoot();
|
|
8
|
+
const repoRoot = await getRepoRoot();
|
|
8
9
|
const worktrees = await listWorktrees();
|
|
10
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
9
11
|
if (worktrees.length === 0) {
|
|
10
12
|
log.info('No worktrees found.');
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
let targetWt;
|
|
14
16
|
if (wtName) {
|
|
15
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
17
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
16
18
|
if (!targetWt) {
|
|
17
19
|
log.error(`Worktree "${wtName}" not found.`);
|
|
18
20
|
return;
|
|
@@ -22,7 +24,7 @@ export async function pathCommand(wtName) {
|
|
|
22
24
|
// Interactive Selection
|
|
23
25
|
const choices = worktrees.map(wt => {
|
|
24
26
|
const branchName = getWorktreeBranchName(wt);
|
|
25
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
27
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
26
28
|
return {
|
|
27
29
|
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
28
30
|
value: wt
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Command, Option } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import { formatMainMenuChoice, renderMainMenuIntro, welcome, log, } from './lib/ui.js';
|
|
7
|
+
import { applyHelp, checkoutHelp, createHelp, createMultiHelp, createSandboxHelp, deleteHelp, handoffHelp, intentRouterHelp, openHelp, unapplyHelp, } from './lib/help.js';
|
|
5
8
|
import { listCommand } from './commands/wt/list.js';
|
|
6
9
|
import { createCommand } from './commands/wt/create.js';
|
|
7
10
|
import { createCommandNew } from './commands/wt/create-branch.js';
|
|
@@ -16,13 +19,83 @@ import { openCommand } from './commands/wt/open.js';
|
|
|
16
19
|
import { applyCommand } from './commands/wt/apply.js';
|
|
17
20
|
import { unapplyCommand } from './commands/wt/unapply.js';
|
|
18
21
|
import { handoffCommand } from './commands/wt/handoff.js';
|
|
22
|
+
import { copyEnvCommand } from './commands/wt/copy-env.js';
|
|
23
|
+
import { configGetCommand, configResetCommand, configSetWorktreeLayoutCommand, configSetWorktreesRootCommand, configUseCommand, } from './commands/config.js';
|
|
19
24
|
import { getVersion } from './lib/version.js';
|
|
20
|
-
import {
|
|
25
|
+
import { checkForUpdate } from './lib/update-check.js';
|
|
21
26
|
import { findSandboxRoot } from './lib/sandbox.js';
|
|
22
27
|
import { bifrostCommand } from './commands/bifrost.js';
|
|
23
28
|
import { thorCommand } from './commands/thor.js';
|
|
29
|
+
import { buildMainMenuEntries, } from './lib/main-menu.js';
|
|
30
|
+
import { getRepoRoot, listWorktrees } from './lib/git.js';
|
|
24
31
|
const program = new Command();
|
|
25
32
|
const argv = process.argv.map((arg) => arg === '-v' || arg === 'βversion' ? '--version' : arg);
|
|
33
|
+
const DOCS_URL = 'https://yggtree.logbookfordevs.com/docs';
|
|
34
|
+
function mainMenuChoice(value, glyph, label, detail, tone) {
|
|
35
|
+
return {
|
|
36
|
+
name: formatMainMenuChoice({ glyph, label, detail, tone }),
|
|
37
|
+
value,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function mainMenuSeparator(label) {
|
|
41
|
+
return new inquirer.Separator(chalk.dim(` Β· ${label}`));
|
|
42
|
+
}
|
|
43
|
+
const mainMenuChoices = {
|
|
44
|
+
'create-smart': mainMenuChoice('create-smart', 'β', 'Grow new realm', 'create a branch-backed worktree', 'growth'),
|
|
45
|
+
'worktree-checkout': mainMenuChoice('worktree-checkout', 'β', 'Check out branch', 'open an existing ref in a new realm', 'travel'),
|
|
46
|
+
handoff: mainMenuChoice('handoff', 'β¦', 'Hand off current work', 'carry dirty work into a named sandbox', 'sandbox'),
|
|
47
|
+
open: mainMenuChoice('open', 'β', 'Open realm', 'jump into an existing worktree', 'travel'),
|
|
48
|
+
list: mainMenuChoice('list', 'β', 'Survey realms', 'scan active worktrees and PR state', 'tending'),
|
|
49
|
+
'create-multi': mainMenuChoice('create-multi', 'β§', 'Grow many realms', 'create multiple branch worktrees', 'growth'),
|
|
50
|
+
'create-sandbox': mainMenuChoice('create-sandbox', 'β³', 'Forge sandbox', 'create a local experiment realm', 'sandbox'),
|
|
51
|
+
bootstrap: mainMenuChoice('bootstrap', 'β³', 'Bootstrap realm', 'install dependencies and submodules', 'tending'),
|
|
52
|
+
exec: mainMenuChoice('exec', 'β', 'Cast command', 'run a command inside a worktree', 'tending'),
|
|
53
|
+
path: mainMenuChoice('path', 'β', 'Reveal path', 'print a worktree cd command', 'tending'),
|
|
54
|
+
'copy-env': mainMenuChoice('copy-env', 'β', 'Bring env files', 'copy local env files from main realm', 'tending'),
|
|
55
|
+
delete: mainMenuChoice('delete', 'Γ', 'Fell realm', 'delete managed worktrees', 'danger'),
|
|
56
|
+
prune: mainMenuChoice('prune', 'β«', 'Prune stale realms', 'remove stale worktree metadata', 'danger'),
|
|
57
|
+
apply: mainMenuChoice('apply', 'β', 'Graft sandbox changes', 'apply sandbox changes to origin', 'sandbox'),
|
|
58
|
+
unapply: mainMenuChoice('unapply', 'βΆ', 'Undo sandbox graft', 'restore origin from sandbox backups', 'sandbox'),
|
|
59
|
+
bifrost: mainMenuChoice('bifrost', 'βΊ', 'Summon the Bifrost', 'open the long way around', 'lore'),
|
|
60
|
+
thor: mainMenuChoice('thor', 'Ο', 'Consult Thor', 'ask the thunder route', 'lore'),
|
|
61
|
+
docs: mainMenuChoice('docs', '?', 'Open docs', 'read the Yggtree documentation', 'tending'),
|
|
62
|
+
exit: mainMenuChoice('exit', 'Β·', 'Leave Yggdrasil', 'exit without changing anything', 'exit'),
|
|
63
|
+
};
|
|
64
|
+
async function detectMainMenuContext() {
|
|
65
|
+
if (await findSandboxRoot(process.cwd())) {
|
|
66
|
+
return 'sandbox';
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const repoRoot = await getRepoRoot();
|
|
70
|
+
const worktrees = await listWorktrees();
|
|
71
|
+
const mainWorktreePath = worktrees[0]?.path;
|
|
72
|
+
if (mainWorktreePath && path.resolve(repoRoot) !== path.resolve(mainWorktreePath)) {
|
|
73
|
+
return 'worktree';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return 'main';
|
|
78
|
+
}
|
|
79
|
+
return 'main';
|
|
80
|
+
}
|
|
81
|
+
async function openDocs() {
|
|
82
|
+
const command = process.platform === 'darwin'
|
|
83
|
+
? 'open'
|
|
84
|
+
: process.platform === 'win32'
|
|
85
|
+
? 'cmd'
|
|
86
|
+
: 'xdg-open';
|
|
87
|
+
const args = process.platform === 'win32'
|
|
88
|
+
? ['/c', 'start', '', DOCS_URL]
|
|
89
|
+
: [DOCS_URL];
|
|
90
|
+
try {
|
|
91
|
+
await execa(command, args, { stdio: 'ignore' });
|
|
92
|
+
log.success(`Opened docs: ${DOCS_URL}`);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
log.warning('Could not open the docs in your browser.');
|
|
96
|
+
log.info(DOCS_URL);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
26
99
|
function registerWorktreeCommands(parent) {
|
|
27
100
|
parent.command('list')
|
|
28
101
|
.description('List all repo-linked worktrees')
|
|
@@ -35,7 +108,7 @@ function registerWorktreeCommands(parent) {
|
|
|
35
108
|
await listCommand();
|
|
36
109
|
});
|
|
37
110
|
parent.command('create [branch]')
|
|
38
|
-
.description('Create
|
|
111
|
+
.description('Create an official branch-backed task worktree')
|
|
39
112
|
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
40
113
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
41
114
|
.option('--source <type>', 'Base source (local or remote)')
|
|
@@ -45,6 +118,8 @@ function registerWorktreeCommands(parent) {
|
|
|
45
118
|
.option('--enter', 'Enter the worktree sub-shell after creation')
|
|
46
119
|
.option('--no-enter', 'Do not enter the worktree sub-shell after creation')
|
|
47
120
|
.option('--exec <command>', 'Command to execute after creation')
|
|
121
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
122
|
+
.addHelpText('after', createHelp)
|
|
48
123
|
.action(async (branch, options) => {
|
|
49
124
|
await createCommandNew({
|
|
50
125
|
...options,
|
|
@@ -52,10 +127,12 @@ function registerWorktreeCommands(parent) {
|
|
|
52
127
|
});
|
|
53
128
|
});
|
|
54
129
|
parent.command('create-multi')
|
|
55
|
-
.description('
|
|
130
|
+
.description('Bulk-create official branch-backed worktrees')
|
|
56
131
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
57
132
|
.option('--source <type>', 'Base source (local or remote)')
|
|
58
133
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
134
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
135
|
+
.addHelpText('after', createMultiHelp)
|
|
59
136
|
.action(async (options) => {
|
|
60
137
|
await createCommandMulti(options);
|
|
61
138
|
});
|
|
@@ -71,6 +148,8 @@ function registerWorktreeCommands(parent) {
|
|
|
71
148
|
.addOption(new Option('--enter', 'Enter the worktree sub-shell after checkout/opening').hideHelp())
|
|
72
149
|
.option('--no-enter', 'Do not enter the worktree sub-shell after checkout/opening')
|
|
73
150
|
.option('--exec <command>', 'Command to execute after creation')
|
|
151
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
152
|
+
.addHelpText('after', checkoutHelp)
|
|
74
153
|
.action(async (name, ref, options) => {
|
|
75
154
|
await createCommand({
|
|
76
155
|
...options,
|
|
@@ -81,17 +160,20 @@ function registerWorktreeCommands(parent) {
|
|
|
81
160
|
};
|
|
82
161
|
registerWorktreeCheckout('worktree-checkout', 'Create a checkout-style worktree from an existing branch');
|
|
83
162
|
registerWorktreeCheckout('wc', 'Alias for worktree-checkout');
|
|
84
|
-
parent.command('delete')
|
|
163
|
+
parent.command('delete [worktrees...]')
|
|
85
164
|
.description('Delete managed worktrees')
|
|
86
|
-
.option('-a, --all', 'Include
|
|
87
|
-
.
|
|
88
|
-
|
|
165
|
+
.option('-a, --all', 'Include external LINKED worktrees outside the managed root (except main/current)')
|
|
166
|
+
.option('-y, --yes', 'Confirm deletion without prompts')
|
|
167
|
+
.addHelpText('after', deleteHelp)
|
|
168
|
+
.action(async (worktrees, options) => {
|
|
169
|
+
await deleteCommand(worktrees, options);
|
|
89
170
|
});
|
|
90
171
|
parent.command('open [worktree]')
|
|
91
172
|
.description('Open a worktree in an editor, supported app, or terminal target')
|
|
92
173
|
.option('--tool <command>', 'Editor, app, or terminal command to use (e.g. cursor, code, codex-app, tmux)')
|
|
93
174
|
.option('--enter', 'Enter the worktree sub-shell after opening')
|
|
94
175
|
.addOption(new Option('--no-enter', 'Do not enter the worktree sub-shell after opening').hideHelp())
|
|
176
|
+
.addHelpText('after', openHelp)
|
|
95
177
|
.action(async (worktree, options) => {
|
|
96
178
|
await openCommand(worktree, options);
|
|
97
179
|
});
|
|
@@ -113,22 +195,27 @@ function registerWorktreeCommands(parent) {
|
|
|
113
195
|
.action(async (worktree) => {
|
|
114
196
|
await pathCommand(worktree);
|
|
115
197
|
});
|
|
198
|
+
parent.command('copy-env')
|
|
199
|
+
.description('Copy local env files from the main worktree to the current worktree')
|
|
200
|
+
.action(copyEnvCommand);
|
|
116
201
|
parent.command('create-sandbox')
|
|
117
|
-
.description('Create a
|
|
202
|
+
.description('Create a local-only disposable experiment sandbox')
|
|
118
203
|
.option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
|
|
119
|
-
.option('--carry', '
|
|
120
|
-
.option('--no-carry', 'Do not
|
|
204
|
+
.option('--carry', 'Copy uncommitted changes to sandbox')
|
|
205
|
+
.option('--no-carry', 'Do not copy uncommitted changes')
|
|
121
206
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
122
207
|
.option('--open', 'Open an editor after creation')
|
|
123
208
|
.option('--no-open', 'Skip opening an editor after creation')
|
|
124
209
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
125
210
|
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
126
211
|
.option('--exec <command>', 'Command to execute after creation')
|
|
212
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
213
|
+
.addHelpText('after', createSandboxHelp)
|
|
127
214
|
.action(async (options) => {
|
|
128
215
|
await createSandboxCommand(options);
|
|
129
216
|
});
|
|
130
217
|
parent.command('handoff')
|
|
131
|
-
.description('Carry
|
|
218
|
+
.description('Carry dirty current work into a named sandbox')
|
|
132
219
|
.option('-n, --name <name>', 'Optional handoff name (prompted when omitted)')
|
|
133
220
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
134
221
|
.option('--open', 'Open an editor after creation')
|
|
@@ -136,14 +223,18 @@ function registerWorktreeCommands(parent) {
|
|
|
136
223
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
137
224
|
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
138
225
|
.option('--exec <command>', 'Command to execute after creation')
|
|
226
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
227
|
+
.addHelpText('after', handoffHelp)
|
|
139
228
|
.action(async (options) => {
|
|
140
229
|
await handoffCommand(options);
|
|
141
230
|
});
|
|
142
231
|
parent.command('apply')
|
|
143
|
-
.description('
|
|
232
|
+
.description('Copy sandbox file changes to the origin checkout')
|
|
233
|
+
.addHelpText('after', applyHelp)
|
|
144
234
|
.action(applyCommand);
|
|
145
235
|
parent.command('unapply')
|
|
146
|
-
.description('
|
|
236
|
+
.description('Restore origin files from sandbox apply backups')
|
|
237
|
+
.addHelpText('after', unapplyHelp)
|
|
147
238
|
.action(unapplyCommand);
|
|
148
239
|
}
|
|
149
240
|
function rejectUnknownTopLevelCommand(args) {
|
|
@@ -162,55 +253,25 @@ program
|
|
|
162
253
|
.description('Interactive CLI for managing git worktrees and configs')
|
|
163
254
|
.version(getVersion())
|
|
164
255
|
.allowExcessArguments(false)
|
|
256
|
+
.addHelpText('after', intentRouterHelp)
|
|
165
257
|
.action(async () => {
|
|
166
|
-
|
|
167
|
-
await welcome();
|
|
168
|
-
await
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
{ name: `πΊοΈ Survey Realms ${chalk.dim('(list worktrees)')}`, value: 'list' },
|
|
177
|
-
{ name: `π§ Open Realm in Editor ${chalk.dim('(open worktree in editor)')}`, value: 'open' },
|
|
178
|
-
{ name: `πͺ Fell a Realm ${chalk.dim('(delete worktree)')}`, value: 'delete' },
|
|
179
|
-
{ name: `π Bless Realm Setup ${chalk.dim('(bootstrap worktree)')}`, value: 'bootstrap' },
|
|
180
|
-
{ name: `π§Ή Prune Withered Realms ${chalk.dim('(prune stale worktrees)')}`, value: 'prune' },
|
|
181
|
-
{ name: `π Cast a Command ${chalk.dim('(exec command in worktree)')}`, value: 'exec' },
|
|
182
|
-
{ name: `π Reveal Realm Path ${chalk.dim('(show worktree path)')}`, value: 'path' },
|
|
183
|
-
];
|
|
184
|
-
const sandboxChoices = [
|
|
185
|
-
{ name: `β
Graft Sandbox Changes ${chalk.dim('(apply sandbox changes)')}`, value: 'apply' },
|
|
186
|
-
{ name: `β©οΈ Undo Sandbox Graft ${chalk.dim('(unapply sandbox changes)')}`, value: 'unapply' },
|
|
187
|
-
];
|
|
188
|
-
const choices = isInSandbox
|
|
189
|
-
? [
|
|
190
|
-
...sandboxChoices,
|
|
191
|
-
new inquirer.Separator(),
|
|
192
|
-
...realmChoices,
|
|
193
|
-
new inquirer.Separator(),
|
|
194
|
-
{ name: `π Summon the Bifrost ${chalk.dim('(easter egg)')}`, value: 'bifrost' },
|
|
195
|
-
{ name: `β‘ Consult Thor ${chalk.dim('(easter egg)')}`, value: 'thor' },
|
|
196
|
-
{ name: `πͺ Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
|
|
197
|
-
]
|
|
198
|
-
: [
|
|
199
|
-
...realmChoices,
|
|
200
|
-
new inquirer.Separator(),
|
|
201
|
-
...sandboxChoices,
|
|
202
|
-
new inquirer.Separator(),
|
|
203
|
-
{ name: `π Summon the Bifrost ${chalk.dim('(easter egg)')}`, value: 'bifrost' },
|
|
204
|
-
{ name: `β‘ Consult Thor ${chalk.dim('(easter egg)')}`, value: 'thor' },
|
|
205
|
-
{ name: `πͺ Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
|
|
206
|
-
];
|
|
258
|
+
const update = await checkForUpdate();
|
|
259
|
+
await welcome({ update });
|
|
260
|
+
const menuContext = await detectMainMenuContext();
|
|
261
|
+
const choices = buildMainMenuEntries(menuContext).map((entry) => {
|
|
262
|
+
if (entry.type === 'separator') {
|
|
263
|
+
return mainMenuSeparator(entry.label);
|
|
264
|
+
}
|
|
265
|
+
return mainMenuChoices[entry.value];
|
|
266
|
+
});
|
|
267
|
+
console.log(renderMainMenuIntro({ context: menuContext }));
|
|
207
268
|
const { action } = await inquirer.prompt([
|
|
208
269
|
{
|
|
209
270
|
type: 'list',
|
|
210
271
|
name: 'action',
|
|
211
|
-
message: '
|
|
272
|
+
message: 'Which route should Yggtree follow?',
|
|
212
273
|
loop: false,
|
|
213
|
-
pageSize:
|
|
274
|
+
pageSize: 14,
|
|
214
275
|
choices,
|
|
215
276
|
},
|
|
216
277
|
]);
|
|
@@ -245,6 +306,9 @@ program
|
|
|
245
306
|
case 'path':
|
|
246
307
|
await pathCommand();
|
|
247
308
|
break;
|
|
309
|
+
case 'copy-env':
|
|
310
|
+
await copyEnvCommand();
|
|
311
|
+
break;
|
|
248
312
|
case 'apply':
|
|
249
313
|
await applyCommand();
|
|
250
314
|
break;
|
|
@@ -263,6 +327,9 @@ program
|
|
|
263
327
|
case 'thor':
|
|
264
328
|
await thorCommand();
|
|
265
329
|
break;
|
|
330
|
+
case 'docs':
|
|
331
|
+
await openDocs();
|
|
332
|
+
break;
|
|
266
333
|
case 'exit':
|
|
267
334
|
log.info('Bye! π');
|
|
268
335
|
process.exit(0);
|
|
@@ -270,6 +337,7 @@ program
|
|
|
270
337
|
});
|
|
271
338
|
// --- Worktree Commands ---
|
|
272
339
|
const wt = program.command('wt').description('Manage git worktrees');
|
|
340
|
+
wt.addHelpText('after', intentRouterHelp);
|
|
273
341
|
registerWorktreeCommands(program);
|
|
274
342
|
registerWorktreeCommands(wt);
|
|
275
343
|
program.addHelpCommand(true);
|
|
@@ -279,5 +347,21 @@ program.command('bifrost')
|
|
|
279
347
|
program.command('thor')
|
|
280
348
|
.description('Consult the God of Thunder (Easter Egg)')
|
|
281
349
|
.action(thorCommand);
|
|
350
|
+
const config = program.command('config').description('Inspect or update global Yggtree settings');
|
|
351
|
+
config.command('get')
|
|
352
|
+
.description('Show resolved global settings')
|
|
353
|
+
.action(configGetCommand);
|
|
354
|
+
config.command('use <preset>')
|
|
355
|
+
.description('Use a global worktree path preset (default, yggtree, codex, claude)')
|
|
356
|
+
.action(configUseCommand);
|
|
357
|
+
config.command('set-worktrees-root <path>')
|
|
358
|
+
.description('Set the global managed worktrees root')
|
|
359
|
+
.action(configSetWorktreesRootCommand);
|
|
360
|
+
config.command('set-worktree-layout <layout>')
|
|
361
|
+
.description('Set the managed worktree path layout (yggtree, codex, or claude)')
|
|
362
|
+
.action(configSetWorktreeLayoutCommand);
|
|
363
|
+
config.command('reset')
|
|
364
|
+
.description('Reset global settings to defaults')
|
|
365
|
+
.action(configResetCommand);
|
|
282
366
|
rejectUnknownTopLevelCommand(argv);
|
|
283
367
|
program.parse(argv);
|