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.
@@ -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
@@ -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
@@ -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
  ]);
@@ -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.bundleId
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 resolveWorktreeByName(worktrees, wtName) {
187
- return findWorktreeByName(worktrees, wtName);
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.bundleId
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();
@@ -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 { welcome, log } from './lib/ui.js';
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 { notifyIfUpdateAvailable } from './lib/update-check.js';
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 a new worktree (Smart branch detection)')
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('Create multiple worktrees (Smart branch detection)')
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 repo-linked worktrees outside ~/.yggtree (except main/current)')
87
- .action(async (options) => {
88
- await deleteCommand(options);
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 sandbox worktree from current branch')
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', 'Carry uncommitted changes to sandbox')
120
- .option('--no-carry', 'Do not carry uncommitted changes')
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 uncommitted work into a sandbox worktree')
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('Apply sandbox changes to origin directory')
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('Undo applied sandbox changes in origin')
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
- // Interactive Menu if no command is provided
167
- await welcome();
168
- await notifyIfUpdateAvailable();
169
- const isInSandbox = Boolean(await findSandboxRoot(process.cwd()));
170
- const realmChoices = [
171
- { name: `🌿 Grow New Realm ${chalk.dim('(create worktree)')}`, value: 'create-smart' },
172
- { name: `πŸ”€ Traverse to Another Realm ${chalk.dim('(checkout existing branch in new worktree)')}`, value: 'worktree-checkout' },
173
- { name: `🌳 Grow Many Realms ${chalk.dim('(create multiple worktrees)')}`, value: 'create-multi' },
174
- { name: `🀝 Hand Off Current Work ${chalk.dim('(carry dirty work into a sandbox)')}`, value: 'handoff' },
175
- { name: `πŸ§ͺ Forge Sandbox Realm ${chalk.dim('(create sandbox worktree)')}`, value: 'create-sandbox' },
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: 'What would you like to do?',
272
+ message: 'Which route should Yggtree follow?',
212
273
  loop: false,
213
- pageSize: 12,
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);