yggtree 1.3.0 β†’ 1.4.0

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/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import inquirer from 'inquirer';
3
+ import chalk from 'chalk';
3
4
  import { welcome, log } from './lib/ui.js';
4
5
  import { listCommand } from './commands/wt/list.js';
5
6
  import { createCommand } from './commands/wt/create.js';
@@ -12,9 +13,11 @@ import { pruneCommand } from './commands/wt/prune.js';
12
13
  import { execCommand } from './commands/wt/exec.js';
13
14
  import { enterCommand } from './commands/wt/enter.js';
14
15
  import { pathCommand } from './commands/wt/path.js';
16
+ import { openCommand } from './commands/wt/open.js';
15
17
  import { applyCommand } from './commands/wt/apply.js';
16
18
  import { unapplyCommand } from './commands/wt/unapply.js';
17
19
  import { getVersion } from './lib/version.js';
20
+ import { findSandboxRoot } from './lib/sandbox.js';
18
21
  const program = new Command();
19
22
  program
20
23
  .name('yggtree')
@@ -23,30 +26,48 @@ program
23
26
  .action(async () => {
24
27
  // Interactive Menu if no command is provided
25
28
  await welcome();
29
+ const isInSandbox = Boolean(await findSandboxRoot(process.cwd()));
30
+ const realmChoices = [
31
+ { name: `🌿 Grow New Realm ${chalk.dim('(create worktree)')}`, value: 'create-smart' },
32
+ { name: `πŸ”€ Traverse to Another Realm ${chalk.dim('(checkout existing branch in new worktree)')}`, value: 'worktree-checkout' },
33
+ { name: `🌳 Grow Many Realms ${chalk.dim('(create multiple worktrees)')}`, value: 'create-multi' },
34
+ { name: `πŸ§ͺ Forge Sandbox Realm ${chalk.dim('(create sandbox worktree)')}`, value: 'create-sandbox' },
35
+ { name: `πŸ—ΊοΈ Survey Realms ${chalk.dim('(list worktrees)')}`, value: 'list' },
36
+ { name: `🧭 Open Realm in Tool ${chalk.dim('(open worktree in IDE/agent)')}`, value: 'open' },
37
+ { name: `πŸͺ“ Fell a Realm ${chalk.dim('(delete worktree)')}`, value: 'delete' },
38
+ { name: `πŸš€ Bless Realm Setup ${chalk.dim('(bootstrap worktree)')}`, value: 'bootstrap' },
39
+ { name: `🧹 Prune Withered Realms ${chalk.dim('(prune stale worktrees)')}`, value: 'prune' },
40
+ { name: `🐚 Cast a Command ${chalk.dim('(exec command in worktree)')}`, value: 'exec' },
41
+ { name: `πŸšͺ Enter Realm Shell ${chalk.dim('(enter worktree)')}`, value: 'enter' },
42
+ { name: `πŸ“ Reveal Realm Path ${chalk.dim('(show worktree path)')}`, value: 'path' },
43
+ ];
44
+ const sandboxChoices = [
45
+ { name: `βœ… Graft Sandbox Changes ${chalk.dim('(apply sandbox changes)')}`, value: 'apply' },
46
+ { name: `↩️ Undo Sandbox Graft ${chalk.dim('(unapply sandbox changes)')}`, value: 'unapply' },
47
+ ];
48
+ const choices = isInSandbox
49
+ ? [
50
+ ...sandboxChoices,
51
+ new inquirer.Separator(),
52
+ ...realmChoices,
53
+ new inquirer.Separator(),
54
+ { name: `πŸšͺ Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
55
+ ]
56
+ : [
57
+ ...realmChoices,
58
+ new inquirer.Separator(),
59
+ ...sandboxChoices,
60
+ new inquirer.Separator(),
61
+ { name: `πŸšͺ Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
62
+ ];
26
63
  const { action } = await inquirer.prompt([
27
64
  {
28
65
  type: 'list',
29
66
  name: 'action',
30
67
  message: 'What would you like to do?',
31
68
  loop: false,
32
- choices: [
33
- { name: '🌿 Create new worktree (Smart Branch)', value: 'create-smart' },
34
- { name: '🌳 Create multiple worktrees', value: 'create-multi' },
35
- { name: 'πŸ§ͺ Create sandbox worktree', value: 'create-sandbox' },
36
- // { name: '🌱 Create new worktree (Manual Slug)', value: 'create-slug' },
37
- { name: 'πŸ“‹ List worktrees', value: 'list' },
38
- { name: 'πŸͺ“ Delete worktree', value: 'delete' },
39
- { name: 'πŸš€ Bootstrap worktree', value: 'bootstrap' },
40
- { name: '🧹 Prune stale worktrees', value: 'prune' },
41
- { name: '🐚 Exec command', value: 'exec' },
42
- { name: 'πŸšͺ Enter worktree', value: 'enter' },
43
- { name: 'πŸ“ Get worktree path', value: 'path' },
44
- new inquirer.Separator(),
45
- { name: 'βœ… Apply sandbox changes', value: 'apply' },
46
- { name: '↩️ Unapply sandbox changes', value: 'unapply' },
47
- new inquirer.Separator(),
48
- { name: 'πŸšͺ Exit', value: 'exit' },
49
- ],
69
+ pageSize: 12,
70
+ choices,
50
71
  },
51
72
  ]);
52
73
  switch (action) {
@@ -56,12 +77,15 @@ program
56
77
  case 'create-multi':
57
78
  await createCommandMulti({ bootstrap: true });
58
79
  break;
59
- case 'create-slug':
80
+ case 'worktree-checkout':
60
81
  await createCommand({ bootstrap: true });
61
82
  break;
62
83
  case 'list':
63
84
  await listCommand();
64
85
  break;
86
+ case 'open':
87
+ await openCommand();
88
+ break;
65
89
  case 'delete':
66
90
  await deleteCommand();
67
91
  break;
@@ -97,8 +121,15 @@ program
97
121
  // --- Worktree Commands ---
98
122
  const wt = program.command('wt').description('Manage git worktrees');
99
123
  wt.command('list')
100
- .description('List all worktrees')
101
- .action(listCommand);
124
+ .description('List all repo-linked worktrees')
125
+ .option('--open', 'Open a worktree in an IDE/agent tool instead of listing')
126
+ .action(async (options) => {
127
+ if (options.open) {
128
+ await openCommand();
129
+ return;
130
+ }
131
+ await listCommand();
132
+ });
102
133
  wt.command('create [branch]')
103
134
  .description('Create a new worktree (Smart branch detection)')
104
135
  .option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
@@ -122,8 +153,8 @@ wt.command('create-multi')
122
153
  .action(async (options) => {
123
154
  await createCommandMulti(options);
124
155
  });
125
- wt.command('create-slug [name] [ref]')
126
- .description('Create a new worktree (Manual slug/ref)')
156
+ wt.command('worktree-checkout [name] [ref]')
157
+ .description('Create a checkout-style worktree from an existing branch')
127
158
  .option('-n, --name <slug>', 'Worktree name (slug)')
128
159
  .option('-r, --ref <ref>', 'Existing branch or ref')
129
160
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
@@ -138,8 +169,17 @@ wt.command('create-slug [name] [ref]')
138
169
  });
139
170
  });
140
171
  wt.command('delete')
141
- .description('Delete a managed worktree')
142
- .action(deleteCommand);
172
+ .description('Delete managed worktrees')
173
+ .option('-a, --all', 'Include repo-linked worktrees outside ~/.yggtree (except main/current)')
174
+ .action(async (options) => {
175
+ await deleteCommand(options);
176
+ });
177
+ wt.command('open [worktree]')
178
+ .description('Open a worktree in an IDE or agent CLI')
179
+ .option('--tool <command>', 'Tool command to use (e.g. cursor, code, claude, codex)')
180
+ .action(async (worktree, options) => {
181
+ await openCommand(worktree, options);
182
+ });
143
183
  wt.command('bootstrap')
144
184
  .description('Bootstrap dependencies in a worktree')
145
185
  .action(bootstrapCommand);
@@ -167,6 +207,7 @@ wt.command('path [worktree]')
167
207
  });
168
208
  wt.command('create-sandbox')
169
209
  .description('Create a sandbox worktree from current branch')
210
+ .option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
170
211
  .option('--carry', 'Carry uncommitted changes to sandbox')
171
212
  .option('--no-carry', 'Do not carry uncommitted changes')
172
213
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
@@ -0,0 +1,9 @@
1
+ import inquirer from 'inquirer';
2
+ import autocompletePrompt from 'inquirer-autocomplete-prompt';
3
+ let autocompleteRegistered = false;
4
+ export function ensureAutocompletePrompt() {
5
+ if (!autocompleteRegistered) {
6
+ inquirer.registerPrompt('autocomplete', autocompletePrompt);
7
+ autocompleteRegistered = true;
8
+ }
9
+ }
@@ -11,6 +11,20 @@ export function generateSandboxName(branch) {
11
11
  const safeBranch = branch.replace(/[\\/]/g, '-');
12
12
  return `sandbox-${hash}_${safeBranch}`;
13
13
  }
14
+ /**
15
+ * Normalize a custom sandbox name into a branch-safe slug.
16
+ * Keeps current sandbox prefix conventions for easier filtering.
17
+ */
18
+ export function normalizeSandboxName(input) {
19
+ const cleaned = input
20
+ .trim()
21
+ .replace(/[\\/]/g, '-')
22
+ .replace(/\s+/g, '-')
23
+ .replace(/^-+|-+$/g, '');
24
+ if (!cleaned)
25
+ return '';
26
+ return cleaned.startsWith('sandbox-') ? cleaned : `sandbox-${cleaned}`;
27
+ }
14
28
  /**
15
29
  * Get the path to the sandbox metadata file
16
30
  */
@@ -0,0 +1,52 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import { WORKTREES_ROOT } from './paths.js';
5
+ import { getSandboxMetaPath } from './sandbox.js';
6
+ export const WORKTREE_TYPE_ORDER = {
7
+ MAIN: 0,
8
+ MANAGED: 1,
9
+ SANDBOX: 2,
10
+ LINKED: 3,
11
+ };
12
+ export function getWorktreeBranchName(worktree) {
13
+ return worktree.branch || worktree.HEAD || 'detached';
14
+ }
15
+ export function isManagedWorktreePath(worktreePath) {
16
+ return worktreePath.startsWith(WORKTREES_ROOT);
17
+ }
18
+ export function formatWorktreeDisplayPath(worktreePath) {
19
+ if (isManagedWorktreePath(worktreePath)) {
20
+ return path.relative(WORKTREES_ROOT, worktreePath);
21
+ }
22
+ return worktreePath.replace(process.env.HOME || '', '~');
23
+ }
24
+ export function findWorktreeByName(worktrees, worktreeName) {
25
+ return worktrees.find(worktree => {
26
+ const branchName = getWorktreeBranchName(worktree);
27
+ const relativePath = path.relative(WORKTREES_ROOT, worktree.path);
28
+ const basename = path.basename(worktree.path);
29
+ return branchName === worktreeName ||
30
+ relativePath === worktreeName ||
31
+ worktree.path === worktreeName ||
32
+ basename === worktreeName;
33
+ });
34
+ }
35
+ export async function detectWorktreeType(worktree, mainWorktreePath) {
36
+ const isManaged = isManagedWorktreePath(worktree.path);
37
+ if (!isManaged) {
38
+ return worktree.path === mainWorktreePath ? 'MAIN' : 'LINKED';
39
+ }
40
+ const hasSandboxMeta = await fs.pathExists(getSandboxMetaPath(worktree.path));
41
+ const isSandboxBranch = (worktree.branch || '').startsWith('sandbox-');
42
+ return hasSandboxMeta || isSandboxBranch ? 'SANDBOX' : 'MANAGED';
43
+ }
44
+ export function formatWorktreeType(type) {
45
+ if (type === 'SANDBOX')
46
+ return chalk.magenta('SANDBOX');
47
+ if (type === 'MANAGED')
48
+ return chalk.green('MANAGED');
49
+ if (type === 'MAIN')
50
+ return chalk.blue('MAIN ');
51
+ return chalk.cyan('LINKED ');
52
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yggtree",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Interactive CLI for managing git worktrees and configs",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -34,6 +34,7 @@
34
34
  "fs-extra": "^11.2.0",
35
35
  "gradient-string": "^2.0.2",
36
36
  "inquirer": "^9.2.14",
37
+ "inquirer-autocomplete-prompt": "^3.0.1",
37
38
  "ora": "^8.0.1",
38
39
  "zod": "^3.22.4"
39
40
  },