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/README.md +114 -10
- package/dist/commands/wt/create-branch.js +26 -6
- package/dist/commands/wt/create-sandbox.js +63 -10
- package/dist/commands/wt/create.js +157 -57
- package/dist/commands/wt/delete.js +43 -18
- package/dist/commands/wt/enter.js +43 -31
- package/dist/commands/wt/exec.js +4 -17
- package/dist/commands/wt/list.js +20 -10
- package/dist/commands/wt/open.js +245 -0
- package/dist/commands/wt/path.js +4 -17
- package/dist/index.js +66 -25
- package/dist/lib/prompt.js +9 -0
- package/dist/lib/sandbox.js +14 -0
- package/dist/lib/worktree.js +52 -0
- package/package.json +2 -1
|
@@ -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
|
-
|
|
5
|
+
import { detectWorktreeType, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, isManagedWorktreePath, } from '../../lib/worktree.js';
|
|
6
|
+
export async function deleteCommand(options = {}) {
|
|
8
7
|
try {
|
|
9
|
-
const
|
|
8
|
+
const currentWorktreePath = await getRepoRoot();
|
|
10
9
|
const worktrees = await listWorktrees();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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) =>
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
|
|
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:
|
|
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
|
|
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 {
|
package/dist/commands/wt/exec.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
38
|
-
const
|
|
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
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/commands/wt/path.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
35
|
-
const
|
|
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
|