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.
@@ -1,35 +1,60 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
- import path from 'path';
4
3
  import { listWorktrees, removeWorktree, getRepoRoot, getLastActivity } from '../../lib/git.js';
5
- import { WORKTREES_ROOT } from '../../lib/paths.js';
6
4
  import { log, createSpinner, timeAgo } from '../../lib/ui.js';
7
- export async function deleteCommand() {
5
+ import { detectWorktreeType, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, isManagedWorktreePath, } from '../../lib/worktree.js';
6
+ export async function deleteCommand(options = {}) {
8
7
  try {
9
- const _ = await getRepoRoot();
8
+ const currentWorktreePath = await getRepoRoot();
10
9
  const worktrees = await listWorktrees();
11
- // Filter only managed worktrees
12
- const managedWts = worktrees.filter(wt => wt.path.startsWith(WORKTREES_ROOT));
13
- if (managedWts.length === 0) {
14
- log.info('No managed worktrees found to delete.');
10
+ let showAll = options.all;
11
+ if (showAll === undefined) {
12
+ const { includeExternal } = await inquirer.prompt([
13
+ {
14
+ type: 'confirm',
15
+ name: 'includeExternal',
16
+ message: 'Include external linked worktrees (outside ~/.yggtree)?',
17
+ default: false,
18
+ },
19
+ ]);
20
+ showAll = includeExternal;
21
+ }
22
+ const includeAll = Boolean(showAll);
23
+ const mainWorktreePath = worktrees[0]?.path;
24
+ const deletableWts = worktrees.filter(wt => {
25
+ if (includeAll) {
26
+ const isMainWorktree = wt.path === mainWorktreePath;
27
+ const isCurrentWorktree = wt.path === currentWorktreePath;
28
+ return !isMainWorktree && !isCurrentWorktree;
29
+ }
30
+ return isManagedWorktreePath(wt.path);
31
+ });
32
+ if (deletableWts.length === 0) {
33
+ log.info(includeAll
34
+ ? 'No deletable linked worktrees found.'
35
+ : 'No managed worktrees found to delete. Use "yggtree wt delete --all" to include external linked worktrees.');
15
36
  return;
16
37
  }
17
- // Pre-fetch activity for all managed worktrees in parallel
18
- const activities = await Promise.all(managedWts.map(wt => getLastActivity(wt.path)));
19
- const choices = managedWts.map((wt, i) => {
20
- const branchName = wt.branch || wt.HEAD || 'detached';
21
- const active = activities[i] ? chalk.magenta(timeAgo(activities[i])) : chalk.dim('—');
38
+ const choices = await Promise.all(deletableWts.map(async (wt) => {
39
+ const [activity, type] = await Promise.all([
40
+ getLastActivity(wt.path),
41
+ detectWorktreeType(wt, mainWorktreePath || ''),
42
+ ]);
43
+ const branchName = getWorktreeBranchName(wt);
44
+ const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('—');
45
+ const displayPath = formatWorktreeDisplayPath(wt.path);
22
46
  return {
23
- name: `${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active}`,
47
+ name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
24
48
  value: wt.path,
25
49
  };
26
- });
50
+ }));
27
51
  const { selectedPaths } = await inquirer.prompt([
28
52
  {
29
53
  type: 'checkbox',
30
54
  name: 'selectedPaths',
31
- message: 'Select worktrees to delete:',
55
+ message: includeAll ? 'Select worktrees to delete:' : 'Select managed worktrees to delete:',
32
56
  choices: choices,
57
+ pageSize: 10,
33
58
  },
34
59
  ]);
35
60
  if (!selectedPaths || selectedPaths.length === 0) {
@@ -37,7 +62,7 @@ export async function deleteCommand() {
37
62
  return;
38
63
  }
39
64
  const count = selectedPaths.length;
40
- const names = selectedPaths.map((p) => path.relative(WORKTREES_ROOT, p));
65
+ const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p));
41
66
  const { confirm } = await inquirer.prompt([
42
67
  {
43
68
  type: 'confirm',
@@ -51,7 +76,7 @@ export async function deleteCommand() {
51
76
  return;
52
77
  }
53
78
  for (const wtPath of selectedPaths) {
54
- const worktreeName = path.relative(WORKTREES_ROOT, wtPath);
79
+ const worktreeName = formatWorktreeDisplayPath(wtPath);
55
80
  const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
56
81
  try {
57
82
  await removeWorktree(wtPath);
@@ -1,30 +1,51 @@
1
1
  import inquirer from 'inquirer';
2
2
  import { spawn } from 'child_process';
3
3
  import { execa } from 'execa';
4
- import path from 'path';
4
+ import chalk from 'chalk';
5
5
  import { listWorktrees, getRepoRoot } from '../../lib/git.js';
6
- import { WORKTREES_ROOT } from '../../lib/paths.js';
7
6
  import { log, ui } from '../../lib/ui.js';
7
+ import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
8
+ function truncateEnd(value, maxLen) {
9
+ if (maxLen <= 0)
10
+ return '';
11
+ if (value.length <= maxLen)
12
+ return value;
13
+ if (maxLen <= 1)
14
+ return '…';
15
+ return `${value.slice(0, maxLen - 1)}…`;
16
+ }
17
+ function truncateStart(value, maxLen) {
18
+ if (maxLen <= 0)
19
+ return '';
20
+ if (value.length <= maxLen)
21
+ return value;
22
+ if (maxLen <= 1)
23
+ return '…';
24
+ return `…${value.slice(-(maxLen - 1))}`;
25
+ }
26
+ function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
27
+ const maxRowWidth = Math.max(40, terminalColumns - 10);
28
+ const typeWidth = 8;
29
+ const availableWidth = Math.max(22, maxRowWidth - typeWidth);
30
+ const branchWidth = Math.min(44, Math.max(12, Math.floor(availableWidth * 0.55)));
31
+ const pathWidth = Math.max(10, availableWidth - branchWidth - 1);
32
+ const branchText = truncateEnd(branchName, branchWidth).padEnd(branchWidth);
33
+ const pathText = truncateStart(displayPath, pathWidth);
34
+ const typeText = formatWorktreeType(type);
35
+ return `${typeText} ${chalk.yellow(branchText)} ${chalk.cyan(pathText)}`;
36
+ }
8
37
  export async function enterCommand(wtName, options = {}) {
9
38
  try {
10
39
  await getRepoRoot();
11
40
  const worktrees = await listWorktrees();
41
+ const mainWorktreePath = worktrees[0]?.path || '';
12
42
  if (worktrees.length === 0) {
13
43
  log.info('No worktrees found.');
14
44
  return;
15
45
  }
16
46
  let targetWt;
17
47
  if (wtName) {
18
- // Find worktree by name (branch name, relative path, or slug/basename)
19
- targetWt = worktrees.find(wt => {
20
- const branchName = wt.branch || wt.HEAD || '';
21
- const relativePath = path.relative(WORKTREES_ROOT, wt.path);
22
- const basename = path.basename(wt.path);
23
- return branchName === wtName ||
24
- relativePath === wtName ||
25
- wt.path === wtName ||
26
- basename === wtName;
27
- });
48
+ targetWt = findWorktreeByName(worktrees, wtName);
28
49
  if (!targetWt) {
29
50
  log.error(`Worktree "${wtName}" not found.`);
30
51
  return;
@@ -32,38 +53,29 @@ export async function enterCommand(wtName, options = {}) {
32
53
  }
33
54
  else {
34
55
  // Interactive Selection
35
- const choices = worktrees.map(wt => {
36
- const branchName = wt.branch || wt.HEAD || 'detached';
37
- const isManaged = wt.path.startsWith(WORKTREES_ROOT);
38
- const displayPath = isManaged
39
- ? path.relative(WORKTREES_ROOT, wt.path)
40
- : wt.path.replace(process.env.HOME || '', '~');
56
+ const terminalColumns = process.stdout.columns || 100;
57
+ const choices = await Promise.all(worktrees.map(async (wt) => {
58
+ const type = await detectWorktreeType(wt, mainWorktreePath);
59
+ const branchName = getWorktreeBranchName(wt);
60
+ const displayPath = formatWorktreeDisplayPath(wt.path);
41
61
  return {
42
- name: `${branchName.padEnd(20)} ${displayPath}`,
62
+ name: formatChoiceLabel(type, branchName, displayPath, terminalColumns),
43
63
  value: wt
44
64
  };
45
- });
65
+ }));
46
66
  const { selectedWt } = await inquirer.prompt([
47
67
  {
48
68
  type: 'list',
49
69
  name: 'selectedWt',
50
70
  message: 'Select a worktree to enter:',
51
71
  choices,
52
- loop: false
72
+ loop: false,
73
+ pageSize: 10,
53
74
  }
54
75
  ]);
55
76
  targetWt = selectedWt;
56
77
  }
57
- const { execCommandStr } = await inquirer.prompt([
58
- {
59
- type: 'input',
60
- name: 'execCommandStr',
61
- message: 'Command to run before entering (optional):',
62
- default: options.exec,
63
- when: options.exec === undefined,
64
- }
65
- ]);
66
- const finalExec = options.exec || execCommandStr;
78
+ const finalExec = options.exec;
67
79
  if (finalExec && finalExec.trim()) {
68
80
  log.info(`Executing: ${finalExec} in ${ui.path(targetWt?.path || '')}`);
69
81
  try {
@@ -1,9 +1,8 @@
1
1
  import inquirer from 'inquirer';
2
2
  import { execa } from 'execa';
3
- import path from 'path';
4
3
  import { listWorktrees, getRepoRoot } from '../../lib/git.js';
5
- import { WORKTREES_ROOT } from '../../lib/paths.js';
6
4
  import { log } from '../../lib/ui.js';
5
+ import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
7
6
  export async function execCommand(wtName, commandArgs) {
8
7
  let targetWt;
9
8
  let command = '';
@@ -16,16 +15,7 @@ export async function execCommand(wtName, commandArgs) {
16
15
  return;
17
16
  }
18
17
  if (wtName) {
19
- // Find worktree by name (branch name, relative path, or slug/basename)
20
- targetWt = worktrees.find(wt => {
21
- const branchName = wt.branch || wt.HEAD || '';
22
- const relativePath = path.relative(WORKTREES_ROOT, wt.path);
23
- const basename = path.basename(wt.path);
24
- return branchName === wtName ||
25
- relativePath === wtName ||
26
- wt.path === wtName ||
27
- basename === wtName;
28
- });
18
+ targetWt = findWorktreeByName(worktrees, wtName);
29
19
  if (!targetWt) {
30
20
  log.error(`Worktree "${wtName}" not found.`);
31
21
  return;
@@ -34,11 +24,8 @@ export async function execCommand(wtName, commandArgs) {
34
24
  else {
35
25
  // Interactive Selection
36
26
  const choices = worktrees.map(wt => {
37
- const branchName = wt.branch || wt.HEAD || 'detached';
38
- const isManaged = wt.path.startsWith(WORKTREES_ROOT);
39
- const displayPath = isManaged
40
- ? path.relative(WORKTREES_ROOT, wt.path)
41
- : wt.path.replace(process.env.HOME || '', '~');
27
+ const branchName = getWorktreeBranchName(wt);
28
+ const displayPath = formatWorktreeDisplayPath(wt.path);
42
29
  return {
43
30
  name: `${branchName.padEnd(20)} ${displayPath}`,
44
31
  value: wt
@@ -1,11 +1,12 @@
1
1
  import chalk from 'chalk';
2
2
  import { listWorktrees, getRepoRoot, isGitClean, getLastActivity } from '../../lib/git.js';
3
- import { WORKTREES_ROOT } from '../../lib/paths.js';
4
3
  import { log, timeAgo } from '../../lib/ui.js';
4
+ import { detectWorktreeType, formatWorktreeType, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
5
5
  export async function listCommand() {
6
6
  try {
7
- const _ = await getRepoRoot(); // Verify we are in a git repo
7
+ await getRepoRoot(); // Verify we are in a git repo
8
8
  const worktrees = await listWorktrees();
9
+ const mainWorktreePath = worktrees[0]?.path || '';
9
10
  if (worktrees.length === 0) {
10
11
  log.info('No worktrees found.');
11
12
  return;
@@ -14,21 +15,30 @@ export async function listCommand() {
14
15
  // Header
15
16
  console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('LAST ACTIVE')} ${chalk.dim('BRANCH')}`);
16
17
  console.log(chalk.dim(' ' + '-'.repeat(70)));
17
- for (const wt of worktrees) {
18
- const isManaged = wt.path.startsWith(WORKTREES_ROOT);
19
- const type = isManaged ? chalk.green('MANAGED') : chalk.blue('MAIN ');
20
- const branchName = wt.branch || wt.HEAD || 'detached';
21
- // Fetch state and activity in parallel
22
- const [isClean, lastActive] = await Promise.all([
18
+ const rows = await Promise.all(worktrees.map(async (wt, index) => {
19
+ const [typeKey, isClean, lastActive] = await Promise.all([
20
+ detectWorktreeType(wt, mainWorktreePath),
23
21
  isGitClean(wt.path),
24
22
  getLastActivity(wt.path),
25
23
  ]);
24
+ const type = formatWorktreeType(typeKey);
25
+ const branchName = getWorktreeBranchName(wt);
26
26
  const stateLabel = (isClean ? 'clean' : 'dirty').padEnd(8);
27
27
  const stateText = isClean ? chalk.green(stateLabel) : chalk.yellow(stateLabel);
28
28
  const activeLabel = lastActive ? timeAgo(lastActive) : '—';
29
29
  const activeText = chalk.magenta(activeLabel.padEnd(14));
30
- console.log(` ${type} ${stateText} ${activeText} ${chalk.yellow(branchName)}`);
31
- }
30
+ return {
31
+ text: ` ${type} ${stateText} ${activeText} ${chalk.yellow(branchName)}`,
32
+ sortType: WORKTREE_TYPE_ORDER[typeKey],
33
+ sortBranch: branchName.toLowerCase(),
34
+ sortIndex: index,
35
+ };
36
+ }));
37
+ rows
38
+ .sort((a, b) => a.sortType - b.sortType ||
39
+ a.sortBranch.localeCompare(b.sortBranch) ||
40
+ a.sortIndex - b.sortIndex)
41
+ .forEach(row => console.log(row.text));
32
42
  console.log('');
33
43
  }
34
44
  catch (error) {
@@ -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