yggtree 1.4.5 → 1.4.7

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 CHANGED
@@ -16,12 +16,14 @@ For guided workflows, command examples, and safety notes, read the full docs:
16
16
 
17
17
  ### Installation
18
18
 
19
- Install globally:
19
+ Install the latest release:
20
20
 
21
21
  ```bash
22
- npm install -g yggtree
22
+ curl -fsSL https://yggtree.logbookfordevs.com/install.sh | bash
23
23
  ```
24
24
 
25
+ Prefer npm globals? `npm install -g yggtree` still works.
26
+
25
27
  Or try it without installing:
26
28
 
27
29
  ```bash
@@ -286,6 +288,18 @@ That preset creates new managed worktrees like:
286
288
  ~/.codex/worktrees/<worktree-slug>/<repo-name>
287
289
  ```
288
290
 
291
+ Use the Claude Code layout when you want Yggtree-created worktrees to match Claude's native repo-local worktree directory:
292
+
293
+ ```bash
294
+ yggtree config use claude
295
+ ```
296
+
297
+ That preset creates new managed worktrees like:
298
+
299
+ ```bash
300
+ <repo-root>/.claude/worktrees/<worktree-slug>
301
+ ```
302
+
289
303
  Set a custom managed root while keeping the Yggtree layout:
290
304
 
291
305
  ```bash
@@ -298,7 +312,14 @@ Return to defaults:
298
312
  yggtree config reset
299
313
  ```
300
314
 
301
- Claude and Cursor do not currently have a confirmed native worktree directory pattern in Yggtree. Until that is explicit, use `set-worktrees-root` with the directory you want rather than relying on an unverified preset.
315
+ Use a path preset for one worktree without changing the saved global config:
316
+
317
+ ```bash
318
+ yggtree create feat/agent-native --config claude
319
+ yggtree wc --ref main --name fresh-main --config yggtree
320
+ ```
321
+
322
+ Cursor does not currently have a confirmed native worktree directory pattern in Yggtree. Until that is explicit, use `set-worktrees-root` with the directory you want rather than relying on an unverified preset.
302
323
 
303
324
  ---
304
325
 
@@ -323,6 +344,7 @@ Options:
323
344
  * `--open / --no-open`
324
345
  * `--enter / --no-enter`
325
346
  * `--exec "<command>"`
347
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
326
348
 
327
349
  Interactive flow:
328
350
 
@@ -364,6 +386,7 @@ Options:
364
386
  * `--tool <command>`: open a specific editor, app, or terminal target and skip the open prompt (`cursor`, `code`, `codex-app`, `cmux`, `tmux`)
365
387
  * `--no-enter`: finish after create/open and return to the caller
366
388
  * `--exec "<command>"`
389
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
367
390
 
368
391
  Interactive flow:
369
392
 
@@ -401,6 +424,7 @@ Options:
401
424
  * `--no-bootstrap`
402
425
  * `--open / --no-open`
403
426
  * `--exec "<command>"`
427
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
404
428
 
405
429
  Interactive flow:
406
430
 
@@ -420,6 +444,7 @@ Options:
420
444
  * `--no-bootstrap`
421
445
  * `--open / --no-open`
422
446
  * `--exec "<command>"`
447
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
423
448
 
424
449
  This is the continuation-focused semantic path for dirty work. It uses sandbox metadata and apply/unapply behavior, but defaults to carrying staged, unstaged, and untracked files.
425
450
 
@@ -453,6 +478,7 @@ Options:
453
478
  * `--base <ref>`
454
479
  * `--source local|remote`
455
480
  * `--no-bootstrap`
481
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
456
482
 
457
483
  <details>
458
484
  <summary>Example</summary>
@@ -550,20 +576,31 @@ Re‑run bootstrap commands for a worktree.
550
576
 
551
577
  ---
552
578
 
553
- ### `yggtree delete`
579
+ ### `yggtree delete [worktrees...]`
554
580
 
555
- Interactively delete worktrees.
581
+ Delete worktrees interactively or by explicit name.
556
582
 
557
583
  Behavior:
558
584
 
559
585
  * Default flow targets managed worktrees.
560
586
  * In interactive mode, yggtree asks whether to include external linked worktrees.
561
- * In direct CLI usage, `--all` includes external linked worktrees (main/current are still excluded for safety).
587
+ * In direct CLI usage, `--all` includes external worktrees labeled `LINKED` in `yggtree list` (main/current are still excluded for safety).
588
+ * Non-interactive deletion requires explicit targets or `--all`, plus `--yes`.
562
589
  * The delete selector shows 6 items per page.
563
590
 
564
591
  Optional:
565
592
 
566
- * `--all` includes linked worktrees outside the configured managed root (main/current worktree is excluded for safety)
593
+ * `--all` includes external worktrees outside the configured managed root (main/current worktree is excluded for safety)
594
+ * `-y, --yes` confirms deletion without prompts
595
+
596
+ Examples:
597
+
598
+ ```bash
599
+ yggtree delete my-feature --yes
600
+ yggtree delete my-feature other-feature --yes
601
+ yggtree delete external-feature --all --yes
602
+ yggtree delete --all --yes
603
+ ```
567
604
 
568
605
  ---
569
606
 
@@ -15,7 +15,7 @@ export async function configUseCommand(preset) {
15
15
  const presetConfig = getPresetConfig(preset);
16
16
  if (!presetConfig) {
17
17
  log.error(`Unknown config preset "${preset}".`);
18
- log.dim('Available presets: default, yggtree, codex');
18
+ log.dim('Available presets: default, yggtree, codex, claude');
19
19
  return;
20
20
  }
21
21
  await writeGlobalConfig(presetConfig);
@@ -33,9 +33,9 @@ export async function configSetWorktreesRootCommand(root) {
33
33
  await configGetCommand();
34
34
  }
35
35
  export async function configSetWorktreeLayoutCommand(layout) {
36
- if (layout !== 'yggtree' && layout !== 'codex') {
36
+ if (layout !== 'yggtree' && layout !== 'codex' && layout !== 'claude') {
37
37
  log.error(`Unknown worktree layout "${layout}".`);
38
- log.dim('Available layouts: yggtree, codex');
38
+ log.dim('Available layouts: yggtree, codex, claude');
39
39
  return;
40
40
  }
41
41
  const currentConfig = await readGlobalConfig();
@@ -8,9 +8,9 @@ import { runBootstrap } from '../../lib/config.js';
8
8
  import { isManagedWorktreePath } from '../../lib/worktree.js';
9
9
  export async function bootstrapCommand() {
10
10
  try {
11
- const _ = await getRepoRoot();
11
+ const repoRoot = await getRepoRoot();
12
12
  const worktrees = await listWorktrees();
13
- const managedRoot = await getManagedWorktreesRoot();
13
+ const managedRoot = await getManagedWorktreesRoot(repoRoot);
14
14
  // 1. Select Worktree
15
15
  // Filter managed or just show all? The prompt said "managed", let's prioritize managed but maybe allow all if needed?
16
16
  // User requirements say "managed", sticking to that for consistency.
@@ -35,7 +35,6 @@ export async function bootstrapCommand() {
35
35
  },
36
36
  ]);
37
37
  const wtPath = selectedPath;
38
- const repoRoot = await getRepoRoot();
39
38
  await runBootstrap(wtPath, repoRoot);
40
39
  log.success('Bootstrap completed!');
41
40
  }
@@ -0,0 +1,31 @@
1
+ import path from 'path';
2
+ import { findLocalEnvFiles, promptAndCopyEnvFiles } from '../../lib/env-files.js';
3
+ import { getRepoRoot, listWorktrees } from '../../lib/git.js';
4
+ import { log } from '../../lib/ui.js';
5
+ export async function copyEnvCommand() {
6
+ try {
7
+ const currentWorktreePath = await getRepoRoot();
8
+ const worktrees = await listWorktrees();
9
+ const mainWorktreePath = worktrees[0]?.path;
10
+ if (!mainWorktreePath) {
11
+ log.info('No main worktree found.');
12
+ return;
13
+ }
14
+ if (path.resolve(currentWorktreePath) === path.resolve(mainWorktreePath)) {
15
+ log.info('Already in the main worktree. Nothing to copy.');
16
+ return;
17
+ }
18
+ const envFiles = await findLocalEnvFiles(mainWorktreePath);
19
+ if (envFiles.length === 0) {
20
+ log.info('No local env files found in the main worktree.');
21
+ return;
22
+ }
23
+ await promptAndCopyEnvFiles(mainWorktreePath, currentWorktreePath, envFiles, {
24
+ promptMessage: `Copy local env file${envFiles.length === 1 ? '' : 's'} from the main worktree to this worktree? (${envFiles.join(', ')})`,
25
+ });
26
+ }
27
+ catch (error) {
28
+ log.actionableError(error.message, 'yggtree wt copy-env');
29
+ process.exit(1);
30
+ }
31
+ }
@@ -89,7 +89,7 @@ export async function createCommandNew(options) {
89
89
  // e.g. feat/new-button -> feat-new-button
90
90
  const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
91
91
  const repoName = await getRepoName();
92
- const worktreePathConfig = await getWorktreePathConfig();
92
+ const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
93
93
  const wtPath = buildManagedWorktreePath(repoName, slug, worktreePathConfig);
94
94
  // 2. Validation
95
95
  if (!slug)
@@ -70,7 +70,7 @@ export async function createCommandMulti(options) {
70
70
  const createdWorktrees = [];
71
71
  // 3. Execution for each branch
72
72
  const repoName = await getRepoName();
73
- const worktreePathConfig = await getWorktreePathConfig();
73
+ const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
74
74
  for (const branchName of branchNames) {
75
75
  const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
76
76
  const wtPath = buildManagedWorktreePath(repoName, slug, worktreePathConfig);
@@ -136,7 +136,7 @@ export async function createSandboxCommand(options = {}) {
136
136
  }
137
137
  // 5. Create worktree
138
138
  const repoName = await getRepoName();
139
- const worktreePathConfig = await getWorktreePathConfig();
139
+ const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
140
140
  const wtPath = buildManagedWorktreePath(repoName, sandboxName, worktreePathConfig);
141
141
  const spinner = createSpinner('Creating sandbox worktree...').start();
142
142
  try {
@@ -204,7 +204,7 @@ export async function createCommand(options) {
204
204
  // 3. Gather remaining inputs
205
205
  const defaultSlug = toSlug(selectedBranch.branchName);
206
206
  const repoName = await getRepoName();
207
- const worktreePathConfig = await getWorktreePathConfig();
207
+ const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
208
208
  const resolveWorktreePath = (name) => buildManagedWorktreePath(repoName, toSlug(name), worktreePathConfig);
209
209
  const answers = await inquirer.prompt([
210
210
  {
@@ -4,13 +4,52 @@ import { listWorktrees, removeWorktree, getRepoRoot, getLastActivity } from '../
4
4
  import { getManagedWorktreesRoot } from '../../lib/global-config.js';
5
5
  import { log, createSpinner, timeAgo } from '../../lib/ui.js';
6
6
  import { detectWorktreeType, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, isManagedWorktreePath, } from '../../lib/worktree.js';
7
- export async function deleteCommand(options = {}) {
7
+ function normalizeTarget(value) {
8
+ return value.trim().toLowerCase();
9
+ }
10
+ function matchesDeleteTarget(wtPath, branchName, displayPath, target) {
11
+ const normalizedTarget = normalizeTarget(target);
12
+ return [
13
+ wtPath,
14
+ branchName,
15
+ displayPath,
16
+ wtPath.split('/').pop() || '',
17
+ ].some(value => normalizeTarget(value) === normalizedTarget);
18
+ }
19
+ function resolveDeleteWorktrees(worktrees, targets, options) {
20
+ const eligibleWts = worktrees.filter(wt => {
21
+ const isMainWorktree = wt.path === options.mainWorktreePath;
22
+ const isCurrentWorktree = wt.path === options.currentWorktreePath;
23
+ if (isMainWorktree || isCurrentWorktree)
24
+ return false;
25
+ if (options.includeAll) {
26
+ return true;
27
+ }
28
+ return isManagedWorktreePath(wt.path, options.managedRoot);
29
+ });
30
+ if (targets.length === 0) {
31
+ return eligibleWts;
32
+ }
33
+ return eligibleWts.filter(wt => {
34
+ const branchName = getWorktreeBranchName(wt);
35
+ const displayPath = formatWorktreeDisplayPath(wt.path, options.managedRoot);
36
+ return targets.some(target => matchesDeleteTarget(wt.path, branchName, displayPath, target));
37
+ });
38
+ }
39
+ export async function deleteCommand(targets = [], options = {}) {
8
40
  try {
9
41
  const currentWorktreePath = await getRepoRoot();
10
42
  const worktrees = await listWorktrees();
11
- const managedRoot = await getManagedWorktreesRoot();
43
+ const managedRoot = await getManagedWorktreesRoot(currentWorktreePath);
12
44
  let showAll = options.all;
13
- if (showAll === undefined) {
45
+ const hasExplicitTargets = targets.length > 0;
46
+ const isNonInteractive = process.env.CI === 'true' || !process.stdin.isTTY;
47
+ if (showAll === undefined && !hasExplicitTargets) {
48
+ if (isNonInteractive) {
49
+ log.error('Non-interactive delete requires worktree names or --all.');
50
+ log.dim('Try: yggtree delete <worktree> --yes');
51
+ return;
52
+ }
14
53
  const { includeExternal } = await inquirer.prompt([
15
54
  {
16
55
  type: 'confirm',
@@ -23,59 +62,76 @@ export async function deleteCommand(options = {}) {
23
62
  }
24
63
  const includeAll = Boolean(showAll);
25
64
  const mainWorktreePath = worktrees[0]?.path;
26
- const deletableWts = worktrees.filter(wt => {
27
- if (includeAll) {
28
- const isMainWorktree = wt.path === mainWorktreePath;
29
- const isCurrentWorktree = wt.path === currentWorktreePath;
30
- return !isMainWorktree && !isCurrentWorktree;
31
- }
32
- return isManagedWorktreePath(wt.path, managedRoot);
65
+ const deletableWts = resolveDeleteWorktrees(worktrees, targets, {
66
+ includeAll,
67
+ currentWorktreePath,
68
+ mainWorktreePath,
69
+ managedRoot,
33
70
  });
34
71
  if (deletableWts.length === 0) {
72
+ if (hasExplicitTargets) {
73
+ log.error(`No deletable worktrees matched: ${targets.join(', ')}`);
74
+ log.dim(includeAll
75
+ ? 'Main and current worktrees are protected from deletion.'
76
+ : 'Use "yggtree delete --all <worktree> --yes" to include linked worktrees outside the managed root.');
77
+ return;
78
+ }
35
79
  log.info(includeAll
36
80
  ? 'No deletable linked worktrees found.'
37
81
  : 'No managed worktrees found to delete. Use "yggtree wt delete --all" to include external linked worktrees.');
38
82
  return;
39
83
  }
40
- const choices = await Promise.all(deletableWts.map(async (wt) => {
41
- const [activity, type] = await Promise.all([
42
- getLastActivity(wt.path),
43
- detectWorktreeType(wt, mainWorktreePath || '', managedRoot),
84
+ let selectedPaths = deletableWts.map(wt => wt.path);
85
+ const shouldPromptForSelection = !hasExplicitTargets && !(options.all === true && options.yes);
86
+ if (shouldPromptForSelection) {
87
+ const choices = await Promise.all(deletableWts.map(async (wt) => {
88
+ const [activity, type] = await Promise.all([
89
+ getLastActivity(wt.path),
90
+ detectWorktreeType(wt, mainWorktreePath || '', managedRoot),
91
+ ]);
92
+ const branchName = getWorktreeBranchName(wt);
93
+ const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('—');
94
+ const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
95
+ return {
96
+ name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
97
+ value: wt.path,
98
+ };
99
+ }));
100
+ const answer = await inquirer.prompt([
101
+ {
102
+ type: 'checkbox',
103
+ name: 'selectedPaths',
104
+ message: includeAll ? 'Select worktrees to delete:' : 'Select managed worktrees to delete:',
105
+ choices: choices,
106
+ pageSize: 10,
107
+ },
44
108
  ]);
45
- const branchName = getWorktreeBranchName(wt);
46
- const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('—');
47
- const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
48
- return {
49
- name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
50
- value: wt.path,
51
- };
52
- }));
53
- const { selectedPaths } = await inquirer.prompt([
54
- {
55
- type: 'checkbox',
56
- name: 'selectedPaths',
57
- message: includeAll ? 'Select worktrees to delete:' : 'Select managed worktrees to delete:',
58
- choices: choices,
59
- pageSize: 10,
60
- },
61
- ]);
109
+ selectedPaths = answer.selectedPaths;
110
+ }
62
111
  if (!selectedPaths || selectedPaths.length === 0) {
63
112
  log.info('No worktrees selected.');
64
113
  return;
65
114
  }
66
115
  const count = selectedPaths.length;
67
116
  const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p, managedRoot));
68
- const { confirm } = await inquirer.prompt([
69
- {
70
- type: 'confirm',
71
- name: 'confirm',
72
- message: `Are you sure you want to delete ${count > 1 ? `${count} worktrees` : `"${names[0]}"`}?`,
73
- default: false,
74
- },
75
- ]);
76
- if (!confirm) {
77
- log.info('Deletion aborted.');
78
- return;
117
+ if (!options.yes) {
118
+ if (isNonInteractive) {
119
+ log.error('Non-interactive delete requires --yes.');
120
+ log.dim(`Try: yggtree delete ${targets.join(' ')} --yes`);
121
+ return;
122
+ }
123
+ const { confirm } = await inquirer.prompt([
124
+ {
125
+ type: 'confirm',
126
+ name: 'confirm',
127
+ message: `Are you sure you want to delete ${count > 1 ? `${count} worktrees` : `"${names[0]}"`}?`,
128
+ default: false,
129
+ },
130
+ ]);
131
+ if (!confirm) {
132
+ log.info('Deletion aborted.');
133
+ return;
134
+ }
79
135
  }
80
136
  for (const wtPath of selectedPaths) {
81
137
  const worktreeName = formatWorktreeDisplayPath(wtPath, managedRoot);
@@ -38,10 +38,10 @@ function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
38
38
  }
39
39
  export async function enterCommand(wtName, options = {}) {
40
40
  try {
41
- await ensureRepoContext();
41
+ const repoRoot = await ensureRepoContext();
42
42
  const worktrees = await listWorktrees();
43
43
  const mainWorktreePath = worktrees[0]?.path || '';
44
- const managedRoot = await getManagedWorktreesRoot();
44
+ const managedRoot = await getManagedWorktreesRoot(repoRoot);
45
45
  if (worktrees.length === 0) {
46
46
  log.info('No worktrees found.');
47
47
  return;
@@ -9,9 +9,9 @@ export async function execCommand(wtName, commandArgs) {
9
9
  let command = '';
10
10
  let args = [];
11
11
  try {
12
- await getRepoRoot();
12
+ const repoRoot = await getRepoRoot();
13
13
  const worktrees = await listWorktrees();
14
- const managedRoot = await getManagedWorktreesRoot();
14
+ const managedRoot = await getManagedWorktreesRoot(repoRoot);
15
15
  if (worktrees.length === 0) {
16
16
  log.info('No worktrees found.');
17
17
  return;
@@ -5,10 +5,10 @@ import { log, timeAgo } from '../../lib/ui.js';
5
5
  import { detectWorktreeType, formatWorktreeType, formatPrStatus, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
6
6
  export async function listCommand() {
7
7
  try {
8
- await getRepoRoot(); // Verify we are in a git repo
8
+ const repoRoot = await getRepoRoot(); // Verify we are in a git repo
9
9
  const worktrees = await listWorktrees();
10
10
  const mainWorktreePath = worktrees[0]?.path || '';
11
- const managedRoot = await getManagedWorktreesRoot();
11
+ const managedRoot = await getManagedWorktreesRoot(repoRoot);
12
12
  if (worktrees.length === 0) {
13
13
  log.info('No worktrees found.');
14
14
  return;
@@ -381,10 +381,10 @@ export async function runOpenActions(actions, wtPath, options = {}) {
381
381
  }
382
382
  export async function openCommand(wtName, options = {}) {
383
383
  try {
384
- await getRepoRoot();
384
+ const repoRoot = await getRepoRoot();
385
385
  const worktrees = await listWorktrees();
386
386
  const mainWorktreePath = worktrees[0]?.path || '';
387
- const managedRoot = await getManagedWorktreesRoot();
387
+ const managedRoot = await getManagedWorktreesRoot(repoRoot);
388
388
  if (worktrees.length === 0) {
389
389
  log.info('No worktrees found.');
390
390
  return;
@@ -5,9 +5,9 @@ import { log } from '../../lib/ui.js';
5
5
  import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
6
6
  export async function pathCommand(wtName) {
7
7
  try {
8
- await getRepoRoot();
8
+ const repoRoot = await getRepoRoot();
9
9
  const worktrees = await listWorktrees();
10
- const managedRoot = await getManagedWorktreesRoot();
10
+ const managedRoot = await getManagedWorktreesRoot(repoRoot);
11
11
  if (worktrees.length === 0) {
12
12
  log.info('No worktrees found.');
13
13
  return;
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { Command, Option } from 'commander';
2
2
  import inquirer from 'inquirer';
3
3
  import chalk from 'chalk';
4
+ import path from 'path';
5
+ import { execa } from 'execa';
4
6
  import { formatMainMenuChoice, renderMainMenuIntro, welcome, log, } from './lib/ui.js';
5
- import { applyHelp, checkoutHelp, createHelp, createMultiHelp, createSandboxHelp, handoffHelp, intentRouterHelp, openHelp, unapplyHelp, } from './lib/help.js';
7
+ import { applyHelp, checkoutHelp, createHelp, createMultiHelp, createSandboxHelp, deleteHelp, handoffHelp, intentRouterHelp, openHelp, unapplyHelp, } from './lib/help.js';
6
8
  import { listCommand } from './commands/wt/list.js';
7
9
  import { createCommand } from './commands/wt/create.js';
8
10
  import { createCommandNew } from './commands/wt/create-branch.js';
@@ -17,14 +19,18 @@ import { openCommand } from './commands/wt/open.js';
17
19
  import { applyCommand } from './commands/wt/apply.js';
18
20
  import { unapplyCommand } from './commands/wt/unapply.js';
19
21
  import { handoffCommand } from './commands/wt/handoff.js';
22
+ import { copyEnvCommand } from './commands/wt/copy-env.js';
20
23
  import { configGetCommand, configResetCommand, configSetWorktreeLayoutCommand, configSetWorktreesRootCommand, configUseCommand, } from './commands/config.js';
21
24
  import { getVersion } from './lib/version.js';
22
25
  import { checkForUpdate } from './lib/update-check.js';
23
26
  import { findSandboxRoot } from './lib/sandbox.js';
24
27
  import { bifrostCommand } from './commands/bifrost.js';
25
28
  import { thorCommand } from './commands/thor.js';
29
+ import { buildMainMenuEntries, } from './lib/main-menu.js';
30
+ import { getRepoRoot, listWorktrees } from './lib/git.js';
26
31
  const program = new Command();
27
32
  const argv = process.argv.map((arg) => arg === '-v' || arg === '—version' ? '--version' : arg);
33
+ const DOCS_URL = 'https://yggtree.logbookfordevs.com/docs';
28
34
  function mainMenuChoice(value, glyph, label, detail, tone) {
29
35
  return {
30
36
  name: formatMainMenuChoice({ glyph, label, detail, tone }),
@@ -34,6 +40,62 @@ function mainMenuChoice(value, glyph, label, detail, tone) {
34
40
  function mainMenuSeparator(label) {
35
41
  return new inquirer.Separator(chalk.dim(` · ${label}`));
36
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
+ }
37
99
  function registerWorktreeCommands(parent) {
38
100
  parent.command('list')
39
101
  .description('List all repo-linked worktrees')
@@ -56,6 +118,7 @@ function registerWorktreeCommands(parent) {
56
118
  .option('--enter', 'Enter the worktree sub-shell after creation')
57
119
  .option('--no-enter', 'Do not enter the worktree sub-shell after creation')
58
120
  .option('--exec <command>', 'Command to execute after creation')
121
+ .option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
59
122
  .addHelpText('after', createHelp)
60
123
  .action(async (branch, options) => {
61
124
  await createCommandNew({
@@ -68,6 +131,7 @@ function registerWorktreeCommands(parent) {
68
131
  .option('--base <ref>', 'Base ref (e.g. main)')
69
132
  .option('--source <type>', 'Base source (local or remote)')
70
133
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
134
+ .option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
71
135
  .addHelpText('after', createMultiHelp)
72
136
  .action(async (options) => {
73
137
  await createCommandMulti(options);
@@ -84,6 +148,7 @@ function registerWorktreeCommands(parent) {
84
148
  .addOption(new Option('--enter', 'Enter the worktree sub-shell after checkout/opening').hideHelp())
85
149
  .option('--no-enter', 'Do not enter the worktree sub-shell after checkout/opening')
86
150
  .option('--exec <command>', 'Command to execute after creation')
151
+ .option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
87
152
  .addHelpText('after', checkoutHelp)
88
153
  .action(async (name, ref, options) => {
89
154
  await createCommand({
@@ -95,11 +160,13 @@ function registerWorktreeCommands(parent) {
95
160
  };
96
161
  registerWorktreeCheckout('worktree-checkout', 'Create a checkout-style worktree from an existing branch');
97
162
  registerWorktreeCheckout('wc', 'Alias for worktree-checkout');
98
- parent.command('delete')
163
+ parent.command('delete [worktrees...]')
99
164
  .description('Delete managed worktrees')
100
- .option('-a, --all', 'Include repo-linked worktrees outside the managed root (except main/current)')
101
- .action(async (options) => {
102
- 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);
103
170
  });
104
171
  parent.command('open [worktree]')
105
172
  .description('Open a worktree in an editor, supported app, or terminal target')
@@ -128,6 +195,9 @@ function registerWorktreeCommands(parent) {
128
195
  .action(async (worktree) => {
129
196
  await pathCommand(worktree);
130
197
  });
198
+ parent.command('copy-env')
199
+ .description('Copy local env files from the main worktree to the current worktree')
200
+ .action(copyEnvCommand);
131
201
  parent.command('create-sandbox')
132
202
  .description('Create a local-only disposable experiment sandbox')
133
203
  .option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
@@ -139,6 +209,7 @@ function registerWorktreeCommands(parent) {
139
209
  .addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
140
210
  .addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
141
211
  .option('--exec <command>', 'Command to execute after creation')
212
+ .option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
142
213
  .addHelpText('after', createSandboxHelp)
143
214
  .action(async (options) => {
144
215
  await createSandboxCommand(options);
@@ -152,6 +223,7 @@ function registerWorktreeCommands(parent) {
152
223
  .addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
153
224
  .addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
154
225
  .option('--exec <command>', 'Command to execute after creation')
226
+ .option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
155
227
  .addHelpText('after', handoffHelp)
156
228
  .action(async (options) => {
157
229
  await handoffCommand(options);
@@ -185,58 +257,14 @@ program
185
257
  .action(async () => {
186
258
  const update = await checkForUpdate();
187
259
  await welcome({ update });
188
- const isInSandbox = Boolean(await findSandboxRoot(process.cwd()));
189
- const dailyChoices = [
190
- mainMenuChoice('create-smart', '◆', 'Grow new realm', 'create a branch-backed worktree', 'growth'),
191
- mainMenuChoice('worktree-checkout', '⇄', 'Check out branch', 'open an existing ref in a new realm', 'travel'),
192
- mainMenuChoice('handoff', '✦', 'Hand off current work', 'carry dirty work into a named sandbox', 'sandbox'),
193
- mainMenuChoice('open', '◇', 'Open realm', 'jump into an existing worktree', 'travel'),
194
- mainMenuChoice('list', '○', 'Survey realms', 'scan active worktrees and PR state', 'tending'),
195
- ];
196
- const growthChoices = [
197
- mainMenuChoice('create-multi', '✧', 'Grow many realms', 'create multiple branch worktrees', 'growth'),
198
- mainMenuChoice('create-sandbox', '△', 'Forge sandbox', 'create a local experiment realm', 'sandbox'),
199
- ];
200
- const tendingChoices = [
201
- mainMenuChoice('bootstrap', '↳', 'Bootstrap realm', 'install dependencies and submodules', 'tending'),
202
- mainMenuChoice('exec', '⌁', 'Cast command', 'run a command inside a worktree', 'tending'),
203
- mainMenuChoice('path', '⌖', 'Reveal path', 'print a worktree cd command', 'tending'),
204
- mainMenuChoice('delete', '×', 'Fell realm', 'delete managed worktrees', 'danger'),
205
- mainMenuChoice('prune', '⌫', 'Prune stale realms', 'remove stale worktree metadata', 'danger'),
206
- ];
207
- const sandboxChoices = [
208
- mainMenuChoice('apply', '◆', 'Graft sandbox changes', 'apply sandbox changes to origin', 'sandbox'),
209
- mainMenuChoice('unapply', '↶', 'Undo sandbox graft', 'restore origin from sandbox backups', 'sandbox'),
210
- ];
211
- const loreChoices = [
212
- mainMenuChoice('bifrost', '✺', 'Summon the Bifrost', 'open the long way around', 'lore'),
213
- mainMenuChoice('thor', 'ϟ', 'Consult Thor', 'ask the thunder route', 'lore'),
214
- mainMenuChoice('exit', '·', 'Leave Yggdrasil', 'exit without changing anything', 'exit'),
215
- ];
216
- const choices = isInSandbox
217
- ? [
218
- ...sandboxChoices,
219
- mainMenuSeparator('Daily routes'),
220
- ...dailyChoices,
221
- mainMenuSeparator('Growth and experiments'),
222
- ...growthChoices,
223
- mainMenuSeparator('Tend realms'),
224
- ...tendingChoices,
225
- mainMenuSeparator('Lore'),
226
- ...loreChoices,
227
- ]
228
- : [
229
- ...dailyChoices,
230
- mainMenuSeparator('Growth and experiments'),
231
- ...growthChoices,
232
- mainMenuSeparator('Tend realms'),
233
- ...tendingChoices,
234
- mainMenuSeparator('Sandbox grafts'),
235
- ...sandboxChoices,
236
- mainMenuSeparator('Lore'),
237
- ...loreChoices,
238
- ];
239
- console.log(renderMainMenuIntro({ isInSandbox }));
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 }));
240
268
  const { action } = await inquirer.prompt([
241
269
  {
242
270
  type: 'list',
@@ -278,6 +306,9 @@ program
278
306
  case 'path':
279
307
  await pathCommand();
280
308
  break;
309
+ case 'copy-env':
310
+ await copyEnvCommand();
311
+ break;
281
312
  case 'apply':
282
313
  await applyCommand();
283
314
  break;
@@ -296,6 +327,9 @@ program
296
327
  case 'thor':
297
328
  await thorCommand();
298
329
  break;
330
+ case 'docs':
331
+ await openDocs();
332
+ break;
299
333
  case 'exit':
300
334
  log.info('Bye! 👋');
301
335
  process.exit(0);
@@ -318,13 +352,13 @@ config.command('get')
318
352
  .description('Show resolved global settings')
319
353
  .action(configGetCommand);
320
354
  config.command('use <preset>')
321
- .description('Use a global worktree path preset (default, yggtree, codex)')
355
+ .description('Use a global worktree path preset (default, yggtree, codex, claude)')
322
356
  .action(configUseCommand);
323
357
  config.command('set-worktrees-root <path>')
324
358
  .description('Set the global managed worktrees root')
325
359
  .action(configSetWorktreesRootCommand);
326
360
  config.command('set-worktree-layout <layout>')
327
- .description('Set the managed worktree path layout (yggtree or codex)')
361
+ .description('Set the managed worktree path layout (yggtree, codex, or claude)')
328
362
  .action(configSetWorktreeLayoutCommand);
329
363
  config.command('reset')
330
364
  .description('Reset global settings to defaults')
@@ -19,8 +19,13 @@ export function formatHome(value) {
19
19
  return `~/${path.relative(home, value)}`;
20
20
  return value;
21
21
  }
22
- function normalizeRoot(value) {
23
- return path.resolve(expandHome(value || YGG_ROOT));
22
+ function normalizeRoot(value, layout, repoRoot) {
23
+ if (value)
24
+ return path.resolve(expandHome(value));
25
+ if (layout === 'claude') {
26
+ return path.join(path.resolve(repoRoot || process.cwd()), '.claude', 'worktrees');
27
+ }
28
+ return path.resolve(YGG_ROOT);
24
29
  }
25
30
  export function normalizeWorktreesRootInput(value) {
26
31
  return path.resolve(expandHome(value));
@@ -28,13 +33,16 @@ export function normalizeWorktreesRootInput(value) {
28
33
  function normalizeLayout(value) {
29
34
  return value || DEFAULT_WORKTREE_LAYOUT;
30
35
  }
36
+ function isWorktreeLayout(value) {
37
+ return value === 'yggtree' || value === 'codex' || value === 'claude';
38
+ }
31
39
  export async function readGlobalConfig() {
32
40
  try {
33
41
  if (await fs.pathExists(CONFIG_PATH)) {
34
42
  const config = await fs.readJSON(CONFIG_PATH);
35
43
  return {
36
44
  worktreesRoot: typeof config.worktreesRoot === 'string' ? config.worktreesRoot : undefined,
37
- worktreeLayout: config.worktreeLayout === 'codex' ? 'codex' : undefined,
45
+ worktreeLayout: isWorktreeLayout(config.worktreeLayout) ? config.worktreeLayout : undefined,
38
46
  };
39
47
  }
40
48
  }
@@ -47,21 +55,30 @@ export async function writeGlobalConfig(config) {
47
55
  await fs.ensureDir(YGG_ROOT);
48
56
  await fs.writeJSON(CONFIG_PATH, config, { spaces: 2 });
49
57
  }
50
- export async function getWorktreePathConfig() {
51
- const config = await readGlobalConfig();
58
+ export async function getWorktreePathConfig(repoRoot, presetOverride) {
59
+ const config = presetOverride === undefined
60
+ ? await readGlobalConfig()
61
+ : getPresetConfig(presetOverride);
62
+ if (!config) {
63
+ throw new Error(`Unknown config preset "${presetOverride}". Available presets: default, yggtree, codex, claude.`);
64
+ }
65
+ const layout = normalizeLayout(config.worktreeLayout);
52
66
  return {
53
- root: normalizeRoot(config.worktreesRoot),
54
- layout: normalizeLayout(config.worktreeLayout),
67
+ root: normalizeRoot(config.worktreesRoot, layout, repoRoot),
68
+ layout,
55
69
  };
56
70
  }
57
- export async function getManagedWorktreesRoot() {
58
- const config = await getWorktreePathConfig();
71
+ export async function getManagedWorktreesRoot(repoRoot) {
72
+ const config = await getWorktreePathConfig(repoRoot);
59
73
  return config.root;
60
74
  }
61
75
  export function buildManagedWorktreePath(repoName, worktreeSlug, config) {
62
76
  if (config.layout === 'codex') {
63
77
  return path.join(config.root, worktreeSlug, repoName);
64
78
  }
79
+ if (config.layout === 'claude') {
80
+ return path.join(config.root, worktreeSlug);
81
+ }
65
82
  return path.join(config.root, repoName, worktreeSlug);
66
83
  }
67
84
  export function getPresetConfig(preset) {
@@ -75,15 +92,27 @@ export function getPresetConfig(preset) {
75
92
  worktreeLayout: 'codex',
76
93
  };
77
94
  }
95
+ if (normalized === 'claude') {
96
+ return {
97
+ worktreeLayout: 'claude',
98
+ };
99
+ }
78
100
  return undefined;
79
101
  }
102
+ function inferPreset(config) {
103
+ if (!config.worktreesRoot && !config.worktreeLayout)
104
+ return 'default';
105
+ if (!config.worktreesRoot && config.worktreeLayout === 'claude')
106
+ return 'claude';
107
+ if (config.worktreesRoot === '~/.codex/worktrees' && config.worktreeLayout === 'codex')
108
+ return 'codex';
109
+ return 'custom';
110
+ }
80
111
  export function formatGlobalConfig(config, resolved) {
81
112
  return [
82
113
  `worktreesRoot: ${formatHome(resolved.root)}`,
83
114
  `worktreeLayout: ${resolved.layout}`,
84
115
  `configFile: ${formatHome(CONFIG_PATH)}`,
85
- config.worktreesRoot || config.worktreeLayout
86
- ? 'preset: custom'
87
- : 'preset: default',
116
+ `preset: ${inferPreset(config)}`,
88
117
  ];
89
118
  }
package/dist/lib/help.js CHANGED
@@ -6,25 +6,38 @@ Choose by intent:
6
6
  Disposable local experiment yggtree create-sandbox
7
7
  Copy sandbox files to origin yggtree apply # from inside sandbox, not a Git merge
8
8
  Bulk official worktrees yggtree create-multi
9
+ Agent-native path layout yggtree config use claude # or codex
10
+ Delete without prompts yggtree delete branch-name --yes
9
11
 
10
12
  More detail: run yggtree help <command>, for example yggtree help handoff.
11
13
  `;
14
+ const agentPathTip = `
15
+ Agent-native paths:
16
+ Add --config claude for Claude Code's repo-local .claude/worktrees layout.
17
+ Add --config codex for Codex's ~/.codex/worktrees layout.
18
+ `;
12
19
  export const createHelp = `
13
20
  Behavior:
14
21
  Creates an official branch-backed worktree, publishes the branch to origin, and enters the shell by default.
15
22
  Use for real task branches. For disposable experiments, use create-sandbox.
16
23
 
24
+ ${agentPathTip}
25
+
17
26
  Examples:
18
27
  yggtree create feat/new-flow --base main --source remote
19
28
  yggtree create feat/background-task --base main --source remote --no-open --no-enter
29
+ yggtree create feat/claude-layout --base main --source remote --config claude
20
30
  `;
21
31
  export const createMultiHelp = `
22
32
  Behavior:
23
33
  Bulk-creates official branch-backed worktrees.
24
34
  This does not share create's open/enter/exec lifecycle. Use create for one task branch.
25
35
 
36
+ ${agentPathTip}
37
+
26
38
  Example:
27
39
  yggtree create-multi --base main --source remote
40
+ yggtree create-multi --base main --source remote --config codex
28
41
  `;
29
42
  export const checkoutHelp = `
30
43
  Behavior:
@@ -32,9 +45,12 @@ Behavior:
32
45
  Use for interruptions and branch review. Use create for a new official task branch.
33
46
  Enters the worktree shell by default; add --no-enter when automation should return.
34
47
 
48
+ ${agentPathTip}
49
+
35
50
  Examples:
36
51
  yggtree wc --ref hotfix/payment-timeout
37
52
  yggtree wc --ref main --name fresh-main --no-open --no-enter
53
+ yggtree wc --ref main --name fresh-main --config yggtree
38
54
  `;
39
55
  export const openHelp = `
40
56
  Behavior:
@@ -46,14 +62,29 @@ Examples:
46
62
  yggtree open my-feature --tool codex-app
47
63
  yggtree list --open
48
64
  `;
65
+ export const deleteHelp = `
66
+ Behavior:
67
+ Deletes selected worktrees. Main and current worktrees are always protected.
68
+ Without arguments, delete stays interactive. For automation, pass explicit targets and --yes.
69
+ Add --all only when the target is outside Yggtree's managed root and appears as LINKED in list.
70
+
71
+ Examples:
72
+ yggtree delete my-feature --yes
73
+ yggtree delete my-feature other-feature --yes
74
+ yggtree delete --all --yes
75
+ yggtree delete external-feature --all --yes
76
+ `;
49
77
  export const createSandboxHelp = `
50
78
  Behavior:
51
79
  Creates a local-only disposable experiment from the current branch.
52
80
  For continuing current dirty work, prefer handoff instead of teaching create-sandbox --carry.
53
81
 
82
+ ${agentPathTip}
83
+
54
84
  Examples:
55
85
  yggtree create-sandbox
56
86
  yggtree create-sandbox --name ui-option-a --no-open
87
+ yggtree create-sandbox --name claude-scratch --config claude
57
88
 
58
89
  Related:
59
90
  handoff carries current dirty work into a named sandbox.
@@ -64,8 +95,11 @@ Behavior:
64
95
  Carry staged, unstaged, and untracked work into a named sandbox so you can continue there.
65
96
  The origin checkout is not cleaned; review or reset it separately if needed.
66
97
 
98
+ ${agentPathTip}
99
+
67
100
  Example:
68
101
  yggtree handoff --name continue-auth-refactor
102
+ yggtree handoff --name continue-auth-refactor --config codex
69
103
 
70
104
  Related:
71
105
  create-sandbox starts a disposable local experiment.
@@ -0,0 +1,71 @@
1
+ export function buildMainMenuEntries(context) {
2
+ const dailyChoices = actions([
3
+ 'create-smart',
4
+ 'worktree-checkout',
5
+ 'handoff',
6
+ 'open',
7
+ 'list',
8
+ ]);
9
+ const growthChoices = actions([
10
+ 'create-multi',
11
+ 'create-sandbox',
12
+ ]);
13
+ const tendingChoices = actions([
14
+ 'bootstrap',
15
+ 'exec',
16
+ 'path',
17
+ 'delete',
18
+ 'prune',
19
+ ]);
20
+ const sandboxChoices = actions([
21
+ 'apply',
22
+ 'unapply',
23
+ ]);
24
+ const loreChoices = actions([
25
+ 'bifrost',
26
+ 'thor',
27
+ 'docs',
28
+ 'exit',
29
+ ]);
30
+ if (context === 'sandbox') {
31
+ return [
32
+ ...sandboxChoices,
33
+ separator('Daily routes'),
34
+ ...dailyChoices,
35
+ separator('Growth and experiments'),
36
+ ...growthChoices,
37
+ separator('Tend realms'),
38
+ ...tendingChoices,
39
+ separator('Lore'),
40
+ ...loreChoices,
41
+ ];
42
+ }
43
+ if (context === 'worktree') {
44
+ return [
45
+ ...actions(['copy-env', 'path', 'exec', 'bootstrap']),
46
+ separator('Daily routes'),
47
+ ...dailyChoices,
48
+ separator('Growth and experiments'),
49
+ ...growthChoices,
50
+ separator('Tend realms'),
51
+ ...actions(['delete', 'prune']),
52
+ separator('Lore'),
53
+ ...loreChoices,
54
+ ];
55
+ }
56
+ return [
57
+ ...dailyChoices,
58
+ separator('Growth and experiments'),
59
+ ...growthChoices,
60
+ separator('Tend realms'),
61
+ ...tendingChoices,
62
+ separator('Lore'),
63
+ ...loreChoices,
64
+ ];
65
+ }
66
+ function actions(values) {
67
+ return values.map(value => ({ type: 'action', value }));
68
+ }
69
+ function separator(label) {
70
+ return { type: 'separator', label };
71
+ }
package/dist/lib/ui.js CHANGED
@@ -34,9 +34,15 @@ export const welcome = async (options = {}) => {
34
34
  console.log(renderWelcome(options));
35
35
  };
36
36
  export function renderMainMenuIntro(options) {
37
- const detail = options.isInSandbox
38
- ? 'Sandbox tools appear first because this realm can apply or undo grafted changes.'
39
- : 'Create, enter, inspect, or tend your worktrees.';
37
+ const detail = (() => {
38
+ if (options.context === 'sandbox') {
39
+ return 'Sandbox tools appear first because this realm can apply or undo grafted changes.';
40
+ }
41
+ if (options.context === 'worktree') {
42
+ return 'Current-realm tools appear first for this worktree.';
43
+ }
44
+ return 'Create, enter, inspect, or tend your worktrees.';
45
+ })();
40
46
  return [
41
47
  'Choose the next realm action.',
42
48
  yggMuted('Daily routes first. Maintenance and lore wait below.'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yggtree",
3
- "version": "1.4.5",
3
+ "version": "1.4.7",
4
4
  "packageManager": "pnpm@11.0.9",
5
5
  "description": "Interactive CLI for managing git worktrees and configs",
6
6
  "main": "dist/index.js",
@@ -22,10 +22,14 @@
22
22
  "build": "tsc",
23
23
  "start": "node dist/index.js",
24
24
  "dev": "tsc --watch",
25
+ "release:pack": "./scripts/package-yggtree-release.sh",
25
26
  "site:dev": "pnpm --dir apps/site dev",
26
27
  "test": "pnpm build && pnpm test:typecheck && vitest run",
27
28
  "test:typecheck": "tsc --noEmit -p tsconfig.test.json"
28
29
  },
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
29
33
  "keywords": [
30
34
  "cli",
31
35
  "git",