yggtree 1.3.0 → 1.4.1

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.
@@ -0,0 +1,245 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import path from 'path';
4
+ import fs from 'fs-extra';
5
+ import { constants as fsConstants } from 'fs';
6
+ import { execa } from 'execa';
7
+ import { listWorktrees, getRepoRoot } from '../../lib/git.js';
8
+ import { log, ui } from '../../lib/ui.js';
9
+ import { ensureAutocompletePrompt } from '../../lib/prompt.js';
10
+ import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
11
+ import { enterCommand } from './enter.js';
12
+ const OPEN_TOOL_CANDIDATES = [
13
+ { id: 'cursor', name: 'Cursor', command: 'cursor', kind: 'ide', aliases: ['cursor'] },
14
+ { id: 'code', name: 'VS Code', command: 'code', kind: 'ide', aliases: ['vscode', 'code'] },
15
+ { id: 'windsurf', name: 'Windsurf', command: 'windsurf', kind: 'ide', aliases: ['windsurf'] },
16
+ { id: 'zed', name: 'Zed', command: 'zed', kind: 'ide', aliases: ['zed'] },
17
+ { id: 'agy', name: 'Agy', command: 'agy', kind: 'ide', aliases: ['agy'] },
18
+ { id: 'idea', name: 'IntelliJ IDEA', command: 'idea', kind: 'ide', aliases: ['idea', 'intellij'] },
19
+ { id: 'webstorm', name: 'WebStorm', command: 'webstorm', kind: 'ide', aliases: ['webstorm'] },
20
+ { id: 'subl', name: 'Sublime Text', command: 'subl', kind: 'ide', aliases: ['subl', 'sublime'] },
21
+ { id: 'claude', name: 'Claude Code', command: 'claude', kind: 'agent', aliases: ['claude', 'claude-code'] },
22
+ { id: 'codex', name: 'Codex', command: 'codex', kind: 'agent', aliases: ['codex'] },
23
+ { id: 'gemini', name: 'Gemini CLI', command: 'gemini', kind: 'agent', aliases: ['gemini'] },
24
+ { id: 'opencode', name: 'OpenCode', command: 'opencode', kind: 'agent', aliases: ['opencode'] },
25
+ ];
26
+ function truncateEnd(value, maxLen) {
27
+ if (maxLen <= 0)
28
+ return '';
29
+ if (value.length <= maxLen)
30
+ return value;
31
+ if (maxLen <= 1)
32
+ return '…';
33
+ return `${value.slice(0, maxLen - 1)}…`;
34
+ }
35
+ function truncateStart(value, maxLen) {
36
+ if (maxLen <= 0)
37
+ return '';
38
+ if (value.length <= maxLen)
39
+ return value;
40
+ if (maxLen <= 1)
41
+ return '…';
42
+ return `…${value.slice(-(maxLen - 1))}`;
43
+ }
44
+ function formatWorktreeChoiceLabel(type, branchName, displayPath, terminalColumns) {
45
+ const maxRowWidth = Math.max(40, terminalColumns - 10);
46
+ const typeWidth = 8;
47
+ const availableWidth = Math.max(22, maxRowWidth - typeWidth);
48
+ const branchWidth = Math.min(44, Math.max(12, Math.floor(availableWidth * 0.55)));
49
+ const pathWidth = Math.max(10, availableWidth - branchWidth - 1);
50
+ const branchText = truncateEnd(branchName, branchWidth).padEnd(branchWidth);
51
+ const pathText = truncateStart(displayPath, pathWidth);
52
+ const typeText = formatWorktreeType(type);
53
+ return `${typeText} ${chalk.yellow(branchText)} ${chalk.cyan(pathText)}`;
54
+ }
55
+ export async function commandExists(command) {
56
+ if (!command)
57
+ return false;
58
+ const isWindows = process.platform === 'win32';
59
+ const pathEntries = (process.env.PATH || '')
60
+ .split(path.delimiter)
61
+ .filter(Boolean);
62
+ const extensions = isWindows
63
+ ? (process.env.PATHEXT || '.EXE;.CMD;.BAT')
64
+ .split(';')
65
+ .map(ext => ext.toLowerCase())
66
+ : [''];
67
+ const hasPathSeparator = command.includes('/') || command.includes('\\');
68
+ const candidates = hasPathSeparator
69
+ ? [command]
70
+ : pathEntries.flatMap(dir => extensions.map(ext => path.join(dir, `${command}${ext}`)));
71
+ const mode = isWindows ? fsConstants.F_OK : fsConstants.X_OK;
72
+ for (const candidate of candidates) {
73
+ try {
74
+ await fs.access(candidate, mode);
75
+ return true;
76
+ }
77
+ catch {
78
+ // Continue scanning
79
+ }
80
+ }
81
+ return false;
82
+ }
83
+ export async function detectInstalledOpenTools() {
84
+ const checks = await Promise.all(OPEN_TOOL_CANDIDATES.map(async (tool) => ({
85
+ tool,
86
+ exists: await commandExists(tool.command),
87
+ })));
88
+ return checks
89
+ .filter(check => check.exists)
90
+ .map(check => check.tool);
91
+ }
92
+ function resolveWorktreeByName(worktrees, wtName) {
93
+ return findWorktreeByName(worktrees, wtName);
94
+ }
95
+ export function resolveOpenToolOption(input, installed) {
96
+ const normalized = input.trim().toLowerCase();
97
+ return installed.find(tool => tool.id === normalized ||
98
+ tool.command.toLowerCase() === normalized ||
99
+ tool.aliases?.some(alias => alias.toLowerCase() === normalized));
100
+ }
101
+ export function isAgentTool(tool) {
102
+ return tool.kind === 'agent';
103
+ }
104
+ export function buildAgentExecCommand(tool) {
105
+ return tool.command;
106
+ }
107
+ export async function promptOpenToolSelection(installedTools, message = 'Select tool to open:') {
108
+ const ideChoices = installedTools
109
+ .filter(tool => tool.kind === 'ide')
110
+ .map(tool => ({
111
+ name: `${tool.name} ${chalk.dim(`(${tool.command})`)}`,
112
+ value: tool,
113
+ }));
114
+ const agentChoices = installedTools
115
+ .filter(tool => tool.kind === 'agent')
116
+ .map(tool => ({
117
+ name: `${tool.name} ${chalk.dim(`(${tool.command})`)}`,
118
+ value: tool,
119
+ }));
120
+ const choices = [];
121
+ if (ideChoices.length > 0) {
122
+ choices.push(new inquirer.Separator(chalk.dim('── IDEs ──')));
123
+ choices.push(...ideChoices);
124
+ }
125
+ if (agentChoices.length > 0) {
126
+ if (choices.length > 0)
127
+ choices.push(new inquirer.Separator(" "));
128
+ choices.push(new inquirer.Separator(chalk.dim('── Agent CLIs ──')));
129
+ choices.push(...agentChoices);
130
+ }
131
+ const { selectedTool } = await inquirer.prompt([
132
+ {
133
+ type: 'list',
134
+ name: 'selectedTool',
135
+ message,
136
+ choices,
137
+ loop: false,
138
+ pageSize: 10,
139
+ },
140
+ ]);
141
+ return selectedTool;
142
+ }
143
+ export async function launchOpenTool(tool, wtPath) {
144
+ if (isAgentTool(tool)) {
145
+ await enterCommand(wtPath, { exec: buildAgentExecCommand(tool) });
146
+ return;
147
+ }
148
+ await execa(tool.command, [wtPath], {
149
+ cwd: wtPath,
150
+ stdio: 'ignore',
151
+ });
152
+ }
153
+ export async function openCommand(wtName, options = {}) {
154
+ try {
155
+ await getRepoRoot();
156
+ const worktrees = await listWorktrees();
157
+ const mainWorktreePath = worktrees[0]?.path || '';
158
+ if (worktrees.length === 0) {
159
+ log.info('No worktrees found.');
160
+ return;
161
+ }
162
+ let targetWt;
163
+ if (wtName) {
164
+ targetWt = resolveWorktreeByName(worktrees, wtName);
165
+ if (!targetWt) {
166
+ log.error(`Worktree "${wtName}" not found.`);
167
+ return;
168
+ }
169
+ }
170
+ else {
171
+ ensureAutocompletePrompt();
172
+ const terminalColumns = process.stdout.columns || 100;
173
+ const candidates = await Promise.all(worktrees.map(async (wt) => {
174
+ const type = await detectWorktreeType(wt, mainWorktreePath);
175
+ const branchName = getWorktreeBranchName(wt);
176
+ const displayPath = formatWorktreeDisplayPath(wt.path);
177
+ const rawType = type.toLowerCase();
178
+ const rawDisplayPath = displayPath.toLowerCase();
179
+ const rawBranchName = branchName.toLowerCase();
180
+ return {
181
+ worktree: wt,
182
+ label: formatWorktreeChoiceLabel(type, branchName, displayPath, terminalColumns),
183
+ searchText: `${rawType} ${rawBranchName} ${rawDisplayPath}`,
184
+ };
185
+ }));
186
+ const { selectedWt } = await inquirer.prompt([
187
+ {
188
+ type: 'autocomplete',
189
+ name: 'selectedWt',
190
+ message: 'Select a worktree to open (type to filter):',
191
+ source: async (_answers, input = '') => {
192
+ const term = input.trim().toLowerCase();
193
+ const filtered = term
194
+ ? candidates.filter(candidate => candidate.searchText.includes(term))
195
+ : candidates;
196
+ return filtered.map(candidate => ({
197
+ name: candidate.label,
198
+ value: candidate.worktree,
199
+ }));
200
+ },
201
+ pageSize: 10,
202
+ },
203
+ ]);
204
+ targetWt = selectedWt;
205
+ }
206
+ if (!targetWt) {
207
+ log.error('No worktree selected.');
208
+ return;
209
+ }
210
+ const installedTools = await detectInstalledOpenTools();
211
+ let chosenTool;
212
+ const requestedTool = options.tool;
213
+ if (requestedTool) {
214
+ chosenTool = resolveOpenToolOption(requestedTool, installedTools);
215
+ if (!chosenTool && await commandExists(requestedTool)) {
216
+ chosenTool = {
217
+ id: 'custom',
218
+ name: requestedTool,
219
+ command: requestedTool,
220
+ kind: 'ide',
221
+ };
222
+ }
223
+ if (!chosenTool) {
224
+ const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
225
+ log.error(`Tool "${requestedTool}" not found.`);
226
+ log.dim(`Detected tool commands: ${available}`);
227
+ return;
228
+ }
229
+ }
230
+ else {
231
+ if (installedTools.length === 0) {
232
+ log.error('No supported open tool command was detected in PATH.');
233
+ log.dim('Try: yggtree wt open --tool <command> (e.g. --tool cursor, --tool claude)');
234
+ return;
235
+ }
236
+ chosenTool = await promptOpenToolSelection(installedTools);
237
+ }
238
+ log.info(`Opening ${ui.path(targetWt.path)} in ${chalk.cyan(chosenTool.name)}...`);
239
+ await launchOpenTool(chosenTool, targetWt.path);
240
+ log.success(isAgentTool(chosenTool) ? 'Agent launched.' : 'IDE launched.');
241
+ }
242
+ catch (error) {
243
+ log.error(error.message);
244
+ }
245
+ }
@@ -1,8 +1,7 @@
1
1
  import inquirer from 'inquirer';
2
- import path from 'path';
3
2
  import { listWorktrees, getRepoRoot } from '../../lib/git.js';
4
- import { WORKTREES_ROOT } from '../../lib/paths.js';
5
3
  import { log } from '../../lib/ui.js';
4
+ import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
6
5
  export async function pathCommand(wtName) {
7
6
  try {
8
7
  await getRepoRoot();
@@ -13,16 +12,7 @@ export async function pathCommand(wtName) {
13
12
  }
14
13
  let targetWt;
15
14
  if (wtName) {
16
- // Find worktree by name (branch name, relative path, or slug/basename)
17
- targetWt = worktrees.find(wt => {
18
- const branchName = wt.branch || wt.HEAD || '';
19
- const relativePath = path.relative(WORKTREES_ROOT, wt.path);
20
- const basename = path.basename(wt.path);
21
- return branchName === wtName ||
22
- relativePath === wtName ||
23
- wt.path === wtName ||
24
- basename === wtName;
25
- });
15
+ targetWt = findWorktreeByName(worktrees, wtName);
26
16
  if (!targetWt) {
27
17
  log.error(`Worktree "${wtName}" not found.`);
28
18
  return;
@@ -31,11 +21,8 @@ export async function pathCommand(wtName) {
31
21
  else {
32
22
  // Interactive Selection
33
23
  const choices = worktrees.map(wt => {
34
- const branchName = wt.branch || wt.HEAD || 'detached';
35
- const isManaged = wt.path.startsWith(WORKTREES_ROOT);
36
- const displayPath = isManaged
37
- ? path.relative(WORKTREES_ROOT, wt.path)
38
- : wt.path.replace(process.env.HOME || '', '~');
24
+ const branchName = getWorktreeBranchName(wt);
25
+ const displayPath = formatWorktreeDisplayPath(wt.path);
39
26
  return {
40
27
  name: `${branchName.padEnd(20)} ${displayPath}`,
41
28
  value: wt
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,13 @@ 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';
21
+ import { bifrostCommand } from './commands/bifrost.js';
22
+ import { thorCommand } from './commands/thor.js';
18
23
  const program = new Command();
19
24
  program
20
25
  .name('yggtree')
@@ -23,30 +28,52 @@ program
23
28
  .action(async () => {
24
29
  // Interactive Menu if no command is provided
25
30
  await welcome();
31
+ const isInSandbox = Boolean(await findSandboxRoot(process.cwd()));
32
+ const realmChoices = [
33
+ { name: `🌿 Grow New Realm ${chalk.dim('(create worktree)')}`, value: 'create-smart' },
34
+ { name: `🔀 Traverse to Another Realm ${chalk.dim('(checkout existing branch in new worktree)')}`, value: 'worktree-checkout' },
35
+ { name: `🌳 Grow Many Realms ${chalk.dim('(create multiple worktrees)')}`, value: 'create-multi' },
36
+ { name: `🧪 Forge Sandbox Realm ${chalk.dim('(create sandbox worktree)')}`, value: 'create-sandbox' },
37
+ { name: `🗺️ Survey Realms ${chalk.dim('(list worktrees)')}`, value: 'list' },
38
+ { name: `🧭 Open Realm in Tool ${chalk.dim('(open worktree in IDE/agent)')}`, value: 'open' },
39
+ { name: `🪓 Fell a Realm ${chalk.dim('(delete worktree)')}`, value: 'delete' },
40
+ { name: `🚀 Bless Realm Setup ${chalk.dim('(bootstrap worktree)')}`, value: 'bootstrap' },
41
+ { name: `🧹 Prune Withered Realms ${chalk.dim('(prune stale worktrees)')}`, value: 'prune' },
42
+ { name: `🐚 Cast a Command ${chalk.dim('(exec command in worktree)')}`, value: 'exec' },
43
+ { name: `🚪 Enter Realm Shell ${chalk.dim('(enter worktree)')}`, value: 'enter' },
44
+ { name: `📍 Reveal Realm Path ${chalk.dim('(show worktree path)')}`, value: 'path' },
45
+ ];
46
+ const sandboxChoices = [
47
+ { name: `✅ Graft Sandbox Changes ${chalk.dim('(apply sandbox changes)')}`, value: 'apply' },
48
+ { name: `↩️ Undo Sandbox Graft ${chalk.dim('(unapply sandbox changes)')}`, value: 'unapply' },
49
+ ];
50
+ const choices = isInSandbox
51
+ ? [
52
+ ...sandboxChoices,
53
+ new inquirer.Separator(),
54
+ ...realmChoices,
55
+ new inquirer.Separator(),
56
+ { name: `🌈 Summon the Bifrost ${chalk.dim('(easter egg)')}`, value: 'bifrost' },
57
+ { name: `⚡ Consult Thor ${chalk.dim('(easter egg)')}`, value: 'thor' },
58
+ { name: `🚪 Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
59
+ ]
60
+ : [
61
+ ...realmChoices,
62
+ new inquirer.Separator(),
63
+ ...sandboxChoices,
64
+ new inquirer.Separator(),
65
+ { name: `🌈 Summon the Bifrost ${chalk.dim('(easter egg)')}`, value: 'bifrost' },
66
+ { name: `⚡ Consult Thor ${chalk.dim('(easter egg)')}`, value: 'thor' },
67
+ { name: `🚪 Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
68
+ ];
26
69
  const { action } = await inquirer.prompt([
27
70
  {
28
71
  type: 'list',
29
72
  name: 'action',
30
73
  message: 'What would you like to do?',
31
74
  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
- ],
75
+ pageSize: 12,
76
+ choices,
50
77
  },
51
78
  ]);
52
79
  switch (action) {
@@ -56,12 +83,15 @@ program
56
83
  case 'create-multi':
57
84
  await createCommandMulti({ bootstrap: true });
58
85
  break;
59
- case 'create-slug':
86
+ case 'worktree-checkout':
60
87
  await createCommand({ bootstrap: true });
61
88
  break;
62
89
  case 'list':
63
90
  await listCommand();
64
91
  break;
92
+ case 'open':
93
+ await openCommand();
94
+ break;
65
95
  case 'delete':
66
96
  await deleteCommand();
67
97
  break;
@@ -89,6 +119,12 @@ program
89
119
  case 'create-sandbox':
90
120
  await createSandboxCommand({ bootstrap: true });
91
121
  break;
122
+ case 'bifrost':
123
+ await bifrostCommand();
124
+ break;
125
+ case 'thor':
126
+ await thorCommand();
127
+ break;
92
128
  case 'exit':
93
129
  log.info('Bye! 👋');
94
130
  process.exit(0);
@@ -97,8 +133,15 @@ program
97
133
  // --- Worktree Commands ---
98
134
  const wt = program.command('wt').description('Manage git worktrees');
99
135
  wt.command('list')
100
- .description('List all worktrees')
101
- .action(listCommand);
136
+ .description('List all repo-linked worktrees')
137
+ .option('--open', 'Open a worktree in an IDE/agent tool instead of listing')
138
+ .action(async (options) => {
139
+ if (options.open) {
140
+ await openCommand();
141
+ return;
142
+ }
143
+ await listCommand();
144
+ });
102
145
  wt.command('create [branch]')
103
146
  .description('Create a new worktree (Smart branch detection)')
104
147
  .option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
@@ -122,8 +165,8 @@ wt.command('create-multi')
122
165
  .action(async (options) => {
123
166
  await createCommandMulti(options);
124
167
  });
125
- wt.command('create-slug [name] [ref]')
126
- .description('Create a new worktree (Manual slug/ref)')
168
+ wt.command('worktree-checkout [name] [ref]')
169
+ .description('Create a checkout-style worktree from an existing branch')
127
170
  .option('-n, --name <slug>', 'Worktree name (slug)')
128
171
  .option('-r, --ref <ref>', 'Existing branch or ref')
129
172
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
@@ -138,8 +181,17 @@ wt.command('create-slug [name] [ref]')
138
181
  });
139
182
  });
140
183
  wt.command('delete')
141
- .description('Delete a managed worktree')
142
- .action(deleteCommand);
184
+ .description('Delete managed worktrees')
185
+ .option('-a, --all', 'Include repo-linked worktrees outside ~/.yggtree (except main/current)')
186
+ .action(async (options) => {
187
+ await deleteCommand(options);
188
+ });
189
+ wt.command('open [worktree]')
190
+ .description('Open a worktree in an IDE or agent CLI')
191
+ .option('--tool <command>', 'Tool command to use (e.g. cursor, code, claude, codex)')
192
+ .action(async (worktree, options) => {
193
+ await openCommand(worktree, options);
194
+ });
143
195
  wt.command('bootstrap')
144
196
  .description('Bootstrap dependencies in a worktree')
145
197
  .action(bootstrapCommand);
@@ -167,6 +219,7 @@ wt.command('path [worktree]')
167
219
  });
168
220
  wt.command('create-sandbox')
169
221
  .description('Create a sandbox worktree from current branch')
222
+ .option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
170
223
  .option('--carry', 'Carry uncommitted changes to sandbox')
171
224
  .option('--no-carry', 'Do not carry uncommitted changes')
172
225
  .option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
@@ -182,4 +235,10 @@ wt.command('apply')
182
235
  wt.command('unapply')
183
236
  .description('Undo applied sandbox changes in origin')
184
237
  .action(unapplyCommand);
238
+ program.command('bifrost')
239
+ .description('Summon the Bifrost (Easter Egg)')
240
+ .action(bifrostCommand);
241
+ program.command('thor')
242
+ .description('Consult the God of Thunder (Easter Egg)')
243
+ .action(thorCommand);
185
244
  program.parse(process.argv);
@@ -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.1",
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
  },