yggtree 1.0.0 → 1.1.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,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import path from 'path';
4
- import { getRepoRoot, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
4
+ import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
5
5
  import { runBootstrap } from '../../lib/config.js';
6
6
  import { WORKTREES_ROOT } from '../../lib/paths.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
@@ -32,7 +32,7 @@ export async function createCommandMulti(options) {
32
32
  { name: 'Local', value: 'local' },
33
33
  ],
34
34
  default: 'remote',
35
- when: !options.base,
35
+ when: !options.base && !options.source,
36
36
  },
37
37
  {
38
38
  type: 'input',
@@ -45,15 +45,17 @@ export async function createCommandMulti(options) {
45
45
  name: 'bootstrap',
46
46
  message: 'Run bootstrap for all worktrees? (npm install + submodules)',
47
47
  default: true,
48
- when: options.bootstrap !== false,
49
- },
48
+ when: options.bootstrap !== false && options.bootstrap !== true,
49
+ }
50
50
  ]);
51
- let baseRef = options.base || answers.base;
52
- if (!options.base && answers.source === 'remote' && !baseRef.startsWith('origin/')) {
51
+ const baseRefRaw = options.base || answers.base;
52
+ const source = options.source || answers.source;
53
+ let baseRef = baseRefRaw;
54
+ if (!options.base && source === 'remote' && !baseRef.startsWith('origin/')) {
53
55
  baseRef = `origin/${baseRef}`;
54
56
  }
55
57
  const branchNames = answers.branches.split(/\s+/).filter((b) => b.length > 0);
56
- const shouldBootstrap = options.bootstrap === false ? false : answers.bootstrap;
58
+ const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
57
59
  // 2. Validation of base ref
58
60
  const spinner = createSpinner('Fetching...').start();
59
61
  await fetchAll();
@@ -66,14 +68,15 @@ export async function createCommandMulti(options) {
66
68
  spinner.succeed(`Base ref ${chalk.cyan(baseRef)} verified.`);
67
69
  const createdWorktrees = [];
68
70
  // 3. Execution for each branch
71
+ const repoName = await getRepoName();
69
72
  for (const branchName of branchNames) {
70
73
  const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
71
- const wtPath = path.join(WORKTREES_ROOT, slug);
74
+ const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
72
75
  log.header(`Processing: ${branchName}`);
73
76
  const wtSpinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
77
+ // Check if target branch already exists
78
+ const targetBranchExists = await verifyRef(branchName);
74
79
  try {
75
- // Check if target branch already exists
76
- const targetBranchExists = await verifyRef(branchName);
77
80
  await fs.ensureDir(path.dirname(wtPath));
78
81
  if (targetBranchExists) {
79
82
  await execa('git', ['worktree', 'add', wtPath, branchName]);
@@ -90,7 +93,15 @@ export async function createCommandMulti(options) {
90
93
  }
91
94
  catch (error) {
92
95
  wtSpinner.fail(`Failed to create worktree for ${branchName}.`);
93
- log.error(error.message);
96
+ const cmd = targetBranchExists
97
+ ? `git worktree add ${wtPath} ${branchName}`
98
+ : `git worktree add -b ${branchName} ${wtPath} ${baseRef}`;
99
+ log.actionableError(error.message, cmd, wtPath, [
100
+ 'Check if the folder already exists: ls ' + wtPath,
101
+ 'Check if the branch is already used: git worktree list',
102
+ 'Try pruning stale worktrees: yggtree wt prune',
103
+ `Run manually: ${cmd}`
104
+ ]);
94
105
  }
95
106
  }
96
107
  // 5. Final Output
@@ -103,7 +114,7 @@ export async function createCommandMulti(options) {
103
114
  }
104
115
  }
105
116
  catch (error) {
106
- log.error(error.message);
117
+ log.actionableError(error.message, 'yggtree wt create-multi');
107
118
  process.exit(1);
108
119
  }
109
120
  }
@@ -1,10 +1,11 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import path from 'path';
4
- import { getRepoRoot, verifyRef, createWorktree, fetchAll, getCurrentBranch } from '../../lib/git.js';
4
+ import { getRepoRoot, getRepoName, verifyRef, createWorktree, fetchAll, getCurrentBranch } from '../../lib/git.js';
5
5
  import { runBootstrap } from '../../lib/config.js';
6
6
  import { WORKTREES_ROOT } from '../../lib/paths.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
8
+ import { execa } from 'execa';
8
9
  import { spawn } from 'child_process';
9
10
  import fs from 'fs-extra';
10
11
  export async function createCommand(options) {
@@ -40,35 +41,43 @@ export async function createCommand(options) {
40
41
  { name: 'Local', value: 'local' },
41
42
  ],
42
43
  default: 'remote',
43
- when: !options.ref,
44
+ when: !options.ref && !options.source,
44
45
  },
45
46
  {
46
47
  type: 'confirm',
47
48
  name: 'bootstrap',
48
49
  message: 'Run bootstrap? (npm install + submodules)',
49
50
  default: true,
50
- when: options.bootstrap !== false,
51
+ when: options.bootstrap !== false && options.bootstrap !== true,
52
+ },
53
+ {
54
+ type: 'confirm',
55
+ name: 'shouldEnter',
56
+ message: 'Do you want to enter the new worktree now?',
57
+ default: true,
58
+ when: options.enter === undefined,
51
59
  },
60
+ {
61
+ type: 'input',
62
+ name: 'exec',
63
+ message: 'Command to run after creation (optional):',
64
+ default: options.exec,
65
+ when: options.exec === undefined,
66
+ }
52
67
  ]);
53
- let shouldEnter = false;
54
- if (!options.ref) {
55
- const finalAnswer = await inquirer.prompt([{
56
- type: 'confirm',
57
- name: 'shouldEnter',
58
- message: 'Do you want to enter the new worktree now?',
59
- default: true
60
- }]);
61
- shouldEnter = finalAnswer.shouldEnter;
62
- }
63
68
  const name = options.name || answers.name;
64
69
  let ref = options.ref || answers.ref;
70
+ const source = options.source || answers.source;
71
+ const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
72
+ const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
73
+ const execCommandStr = options.exec || answers.exec;
65
74
  // Append origin/ if remote is selected and not already present
66
- if (!options.ref && answers.source === 'remote' && !ref.startsWith('origin/')) {
75
+ if (!options.ref && source === 'remote' && !ref.startsWith('origin/')) {
67
76
  ref = `origin/${ref}`;
68
77
  }
69
- const shouldBootstrap = options.bootstrap === false ? false : answers.bootstrap;
70
78
  const slug = name.replace(/\s+/g, '-');
71
- const wtPath = path.join(WORKTREES_ROOT, slug);
79
+ const repoName = await getRepoName();
80
+ const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
72
81
  // 2. Validation
73
82
  if (!slug)
74
83
  throw new Error('Invalid name');
@@ -92,14 +101,35 @@ export async function createCommand(options) {
92
101
  }
93
102
  catch (e) {
94
103
  spinner.fail('Failed to create worktree.');
95
- log.error(e.message);
104
+ log.actionableError(e.message, `git worktree add ${wtPath} ${ref}`, wtPath, [
105
+ 'Check if the folder already exists: ls ' + wtPath,
106
+ 'Check if the branch is already used: git worktree list',
107
+ 'Try pruning stale worktrees: yggtree wt prune',
108
+ `Run manually: git worktree add ${wtPath} ${ref}`
109
+ ]);
96
110
  return;
97
111
  }
98
- // 4. Bootstrap
99
112
  if (shouldBootstrap) {
100
113
  await runBootstrap(wtPath, repoRoot);
101
114
  }
102
- // 5. Final Output
115
+ // 5. Exec Command
116
+ if (execCommandStr && execCommandStr.trim()) {
117
+ log.info(`Executing: ${execCommandStr} in ${ui.path(wtPath)}`);
118
+ try {
119
+ await execa(execCommandStr, {
120
+ cwd: wtPath,
121
+ stdio: 'inherit',
122
+ shell: true
123
+ });
124
+ }
125
+ catch (error) {
126
+ log.actionableError(error.message, execCommandStr, wtPath, [
127
+ `cd ${wtPath} && ${execCommandStr}`,
128
+ 'Check your command syntax and environment variables'
129
+ ]);
130
+ }
131
+ }
132
+ // 6. Final Output
103
133
  log.success('Worktree ready!');
104
134
  if (shouldEnter) {
105
135
  log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
@@ -118,7 +148,7 @@ export async function createCommand(options) {
118
148
  }
119
149
  }
120
150
  catch (error) {
121
- log.error(error.message);
151
+ log.actionableError(error.message, 'yggtree wt create');
122
152
  process.exit(1);
123
153
  }
124
154
  }
@@ -14,35 +14,54 @@ export async function deleteCommand() {
14
14
  log.info('No managed worktrees found to delete.');
15
15
  return;
16
16
  }
17
- const choices = managedWts.map(wt => ({
18
- name: `${chalk.bold(path.basename(wt.path))} (${chalk.dim(wt.branch || wt.HEAD)})`,
19
- value: wt.path,
20
- }));
21
- const { selectedPath } = await inquirer.prompt([
17
+ const choices = managedWts.map(wt => {
18
+ const relative = path.relative(WORKTREES_ROOT, wt.path);
19
+ return {
20
+ name: `${chalk.bold(relative)} (${chalk.dim(wt.branch || wt.HEAD)})`,
21
+ value: wt.path,
22
+ };
23
+ });
24
+ const { selectedPaths } = await inquirer.prompt([
22
25
  {
23
- type: 'list',
24
- name: 'selectedPath',
25
- message: 'Select worktree to delete:',
26
+ type: 'checkbox',
27
+ name: 'selectedPaths',
28
+ message: 'Select worktrees to delete:',
26
29
  choices: choices,
27
30
  },
28
31
  ]);
29
- const worktreeName = path.basename(selectedPath);
32
+ if (!selectedPaths || selectedPaths.length === 0) {
33
+ log.info('No worktrees selected.');
34
+ return;
35
+ }
36
+ const count = selectedPaths.length;
37
+ const names = selectedPaths.map((p) => path.relative(WORKTREES_ROOT, p));
30
38
  const { confirm } = await inquirer.prompt([
31
39
  {
32
- type: 'input',
40
+ type: 'confirm',
33
41
  name: 'confirm',
34
- message: `Type "${chalk.bold(worktreeName)}" to confirm deletion:`,
35
- validate: (input) => input === worktreeName || 'Incorrect name, deletion aborted.',
42
+ message: `Are you sure you want to delete ${count > 1 ? `${count} worktrees` : `"${names[0]}"`}?`,
43
+ default: false,
36
44
  },
37
45
  ]);
38
- const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
39
- try {
40
- await removeWorktree(selectedPath);
41
- spinner.succeed(`Deleted worktree: ${worktreeName}`);
46
+ if (!confirm) {
47
+ log.info('Deletion aborted.');
48
+ return;
42
49
  }
43
- catch (e) {
44
- spinner.fail(`Failed to delete ${worktreeName}`);
45
- log.error(e.message);
50
+ for (const wtPath of selectedPaths) {
51
+ const worktreeName = path.relative(WORKTREES_ROOT, wtPath);
52
+ const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
53
+ try {
54
+ await removeWorktree(wtPath);
55
+ spinner.succeed(`Deleted worktree: ${worktreeName}`);
56
+ }
57
+ catch (e) {
58
+ spinner.fail(`Failed to delete ${worktreeName}`);
59
+ log.actionableError(e.message, `git worktree remove ${wtPath} --force`, wtPath, [
60
+ `Try running manually: git worktree remove ${wtPath} --force`,
61
+ 'Check if any files in the worktree are open or locked',
62
+ 'Try running yggtree wt prune to clean up git metadata'
63
+ ]);
64
+ }
46
65
  }
47
66
  }
48
67
  catch (error) {
@@ -0,0 +1,110 @@
1
+ import inquirer from 'inquirer';
2
+ import { spawn } from 'child_process';
3
+ import { execa } from 'execa';
4
+ import path from 'path';
5
+ import { listWorktrees, getRepoRoot } from '../../lib/git.js';
6
+ import { WORKTREES_ROOT } from '../../lib/paths.js';
7
+ import { log, ui } from '../../lib/ui.js';
8
+ export async function enterCommand(wtName, options = {}) {
9
+ try {
10
+ await getRepoRoot();
11
+ const worktrees = await listWorktrees();
12
+ if (worktrees.length === 0) {
13
+ log.info('No worktrees found.');
14
+ return;
15
+ }
16
+ let targetWt;
17
+ 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
+ });
28
+ if (!targetWt) {
29
+ log.error(`Worktree "${wtName}" not found.`);
30
+ return;
31
+ }
32
+ }
33
+ else {
34
+ // 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 || '', '~');
41
+ return {
42
+ name: `${branchName.padEnd(20)} ${displayPath}`,
43
+ value: wt
44
+ };
45
+ });
46
+ const { selectedWt } = await inquirer.prompt([
47
+ {
48
+ type: 'list',
49
+ name: 'selectedWt',
50
+ message: 'Select a worktree to enter:',
51
+ choices,
52
+ loop: false
53
+ }
54
+ ]);
55
+ targetWt = selectedWt;
56
+ }
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;
67
+ if (finalExec && finalExec.trim()) {
68
+ log.info(`Executing: ${finalExec} in ${ui.path(targetWt?.path || '')}`);
69
+ try {
70
+ await execa(finalExec, {
71
+ cwd: targetWt?.path,
72
+ stdio: 'inherit',
73
+ shell: true
74
+ });
75
+ }
76
+ catch (error) {
77
+ log.error(`Command failed: ${error.message}`);
78
+ // Ask if still want to enter?
79
+ const { stillEnter } = await inquirer.prompt([{
80
+ type: 'confirm',
81
+ name: 'stillEnter',
82
+ message: 'Command failed. Do you still want to enter the sub-shell?',
83
+ default: true
84
+ }]);
85
+ if (!stillEnter)
86
+ return;
87
+ }
88
+ }
89
+ log.info(`Spawning sub-shell in ${ui.path(targetWt?.path || '')}...`);
90
+ log.dim('Type "exit" to return to the main terminal.');
91
+ const shell = process.env.SHELL || 'zsh';
92
+ const child = spawn(shell, [], {
93
+ cwd: targetWt?.path,
94
+ stdio: 'inherit',
95
+ env: {
96
+ ...process.env,
97
+ YGGTREE_SHELL: 'true'
98
+ }
99
+ });
100
+ child.on('exit', (code) => {
101
+ if (code !== 0 && code !== null) {
102
+ // Command failed with non-zero exit code
103
+ }
104
+ log.info('Exited sub-shell.');
105
+ });
106
+ }
107
+ catch (error) {
108
+ log.error(error.message);
109
+ }
110
+ }
@@ -0,0 +1,93 @@
1
+ import inquirer from 'inquirer';
2
+ import { execa } from 'execa';
3
+ import path from 'path';
4
+ import { listWorktrees, getRepoRoot } from '../../lib/git.js';
5
+ import { WORKTREES_ROOT } from '../../lib/paths.js';
6
+ import { log } from '../../lib/ui.js';
7
+ export async function execCommand(wtName, commandArgs) {
8
+ let targetWt;
9
+ let command = '';
10
+ let args = [];
11
+ try {
12
+ await getRepoRoot();
13
+ const worktrees = await listWorktrees();
14
+ if (worktrees.length === 0) {
15
+ log.info('No worktrees found.');
16
+ return;
17
+ }
18
+ 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
+ });
29
+ if (!targetWt) {
30
+ log.error(`Worktree "${wtName}" not found.`);
31
+ return;
32
+ }
33
+ }
34
+ else {
35
+ // Interactive Selection
36
+ 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 || '', '~');
42
+ return {
43
+ name: `${branchName.padEnd(20)} ${displayPath}`,
44
+ value: wt
45
+ };
46
+ });
47
+ const { selectedWt } = await inquirer.prompt([
48
+ {
49
+ type: 'list',
50
+ name: 'selectedWt',
51
+ message: 'Select a worktree:',
52
+ choices,
53
+ loop: false
54
+ }
55
+ ]);
56
+ targetWt = selectedWt;
57
+ }
58
+ if (commandArgs && commandArgs.length > 0) {
59
+ command = commandArgs[0];
60
+ args = commandArgs.slice(1);
61
+ }
62
+ else {
63
+ const { inputCommand } = await inquirer.prompt([
64
+ {
65
+ type: 'input',
66
+ name: 'inputCommand',
67
+ message: `Enter command to run in ${targetWt?.path}:`,
68
+ validate: (input) => input.trim().length > 0 || 'Command cannot be empty'
69
+ }
70
+ ]);
71
+ // Basic shell-like parsing for the interactive input
72
+ const parts = inputCommand.trim().split(/\s+/);
73
+ command = parts[0];
74
+ args = parts.slice(1);
75
+ }
76
+ log.info(`Executing: ${command} ${args.join(' ')} in ${targetWt?.path}`);
77
+ await execa(command, args, {
78
+ cwd: targetWt?.path,
79
+ stdio: 'inherit',
80
+ shell: false
81
+ });
82
+ }
83
+ catch (error) {
84
+ if (error.exitCode !== undefined) {
85
+ // Command failed, but it already printed its error to inherited stdio
86
+ return;
87
+ }
88
+ log.actionableError(error.message, `${command} ${args.join(' ')}`, targetWt?.path, [
89
+ `cd ${targetWt?.path} && ${command} ${args.join(' ')}`,
90
+ 'Check if the command exists and is in your PATH'
91
+ ]);
92
+ }
93
+ }
@@ -0,0 +1,13 @@
1
+ import { log } from '../../lib/ui.js';
2
+ export async function leaveCommand() {
3
+ if (process.env.YGGTREE_SHELL === 'true') {
4
+ log.info('Leaving worktree sub-shell...');
5
+ // SIGHUP is standard for closing shells
6
+ process.kill(process.ppid, 'SIGHUP');
7
+ }
8
+ else {
9
+ log.warning('You are not in a yggtree sub-shell.');
10
+ log.dim('Try using "yggtree wt enter" first.');
11
+ process.exit(0);
12
+ }
13
+ }
@@ -1,5 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { listWorktrees, getRepoRoot } from '../../lib/git.js';
2
+ import path from 'path';
3
+ import { listWorktrees, getRepoRoot, isGitClean } from '../../lib/git.js';
3
4
  import { WORKTREES_ROOT } from '../../lib/paths.js';
4
5
  import { log } from '../../lib/ui.js';
5
6
  export async function listCommand() {
@@ -12,21 +13,26 @@ export async function listCommand() {
12
13
  }
13
14
  console.log(chalk.bold('\n Active Worktrees:\n'));
14
15
  // Header
15
- console.log(` ${chalk.dim('TYPE')} ${chalk.dim('BRANCH')} ${chalk.dim('PATH')}`);
16
- console.log(chalk.dim(' ' + '-'.repeat(60)));
16
+ console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('BRANCH')} ${chalk.dim('PATH')}`);
17
+ console.log(chalk.dim(' ' + '-'.repeat(75)));
17
18
  for (const wt of worktrees) {
18
19
  const isManaged = wt.path.startsWith(WORKTREES_ROOT);
19
- const isMain = !isManaged; // Simplification: assume main repo is not in managed dir
20
20
  const type = isManaged ? chalk.green('MANAGED') : chalk.blue('MAIN ');
21
21
  const branchName = wt.branch || wt.HEAD || 'detached';
22
- const displayPath = wt.path.replace(process.env.HOME || '', '~');
22
+ let displayPath = wt.path.replace(process.env.HOME || '', '~');
23
+ if (isManaged) {
24
+ displayPath = path.relative(WORKTREES_ROOT, wt.path);
25
+ }
23
26
  const colorPath = isManaged ? chalk.cyan(displayPath) : chalk.dim(displayPath);
24
- console.log(` ${type} ${chalk.yellow(branchName.padEnd(18))} ${colorPath}`);
27
+ const isClean = await isGitClean(wt.path);
28
+ const stateLabel = (isClean ? 'clean' : 'dirty').padEnd(8);
29
+ const stateText = isClean ? chalk.green(stateLabel) : chalk.yellow(stateLabel);
30
+ console.log(` ${type} ${stateText} ${chalk.yellow(branchName.padEnd(18))} ${colorPath}`);
25
31
  }
26
32
  console.log('');
27
33
  }
28
34
  catch (error) {
29
- log.error(error.message);
35
+ log.actionableError(error.message, 'yggtree wt list');
30
36
  process.exit(1);
31
37
  }
32
38
  }
@@ -0,0 +1,62 @@
1
+ import inquirer from 'inquirer';
2
+ import path from 'path';
3
+ import { listWorktrees, getRepoRoot } from '../../lib/git.js';
4
+ import { WORKTREES_ROOT } from '../../lib/paths.js';
5
+ import { log } from '../../lib/ui.js';
6
+ export async function pathCommand(wtName) {
7
+ try {
8
+ await getRepoRoot();
9
+ const worktrees = await listWorktrees();
10
+ if (worktrees.length === 0) {
11
+ log.info('No worktrees found.');
12
+ return;
13
+ }
14
+ let targetWt;
15
+ 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
+ });
26
+ if (!targetWt) {
27
+ log.error(`Worktree "${wtName}" not found.`);
28
+ return;
29
+ }
30
+ }
31
+ else {
32
+ // Interactive Selection
33
+ 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 || '', '~');
39
+ return {
40
+ name: `${branchName.padEnd(20)} ${displayPath}`,
41
+ value: wt
42
+ };
43
+ });
44
+ const { selectedWt } = await inquirer.prompt([
45
+ {
46
+ type: 'list',
47
+ name: 'selectedWt',
48
+ message: 'Select a worktree to get path:',
49
+ choices,
50
+ loop: false
51
+ }
52
+ ]);
53
+ targetWt = selectedWt;
54
+ }
55
+ if (targetWt) {
56
+ console.log(`cd "${targetWt.path}"`);
57
+ }
58
+ }
59
+ catch (error) {
60
+ log.error(error.message);
61
+ }
62
+ }
@@ -10,7 +10,10 @@ export async function pruneCommand() {
10
10
  }
11
11
  catch (e) {
12
12
  spinner.fail('Failed to prune worktrees.');
13
- log.error(e.message);
13
+ log.actionableError(e.message, 'git worktree prune', undefined, [
14
+ 'Try running manually: git worktree prune',
15
+ 'Check if any worktree folders were deleted manually without using git'
16
+ ]);
14
17
  }
15
18
  }
16
19
  catch (error) {