yggtree 1.4.3 → 1.4.5
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 +105 -28
- package/dist/commands/config.js +53 -0
- package/dist/commands/wt/bootstrap.js +5 -3
- package/dist/commands/wt/create-branch.js +41 -28
- package/dist/commands/wt/create-multi.js +3 -2
- package/dist/commands/wt/create-sandbox.js +3 -2
- package/dist/commands/wt/create.js +48 -8
- package/dist/commands/wt/delete.js +8 -6
- package/dist/commands/wt/enter.js +5 -3
- package/dist/commands/wt/exec.js +4 -2
- package/dist/commands/wt/handoff.js +7 -0
- package/dist/commands/wt/list.js +3 -1
- package/dist/commands/wt/open.js +152 -79
- package/dist/commands/wt/path.js +4 -2
- package/dist/index.js +113 -46
- package/dist/lib/global-config.js +89 -0
- package/dist/lib/help.js +93 -0
- package/dist/lib/paths.js +0 -1
- package/dist/lib/repo-context.js +3 -1
- package/dist/lib/ui.js +71 -6
- package/dist/lib/worktree.js +14 -10
- package/package.json +1 -1
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { listWorktrees, removeWorktree, getRepoRoot, getLastActivity } from '../../lib/git.js';
|
|
4
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
4
5
|
import { log, createSpinner, timeAgo } from '../../lib/ui.js';
|
|
5
6
|
import { detectWorktreeType, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, isManagedWorktreePath, } from '../../lib/worktree.js';
|
|
6
7
|
export async function deleteCommand(options = {}) {
|
|
7
8
|
try {
|
|
8
9
|
const currentWorktreePath = await getRepoRoot();
|
|
9
10
|
const worktrees = await listWorktrees();
|
|
11
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
10
12
|
let showAll = options.all;
|
|
11
13
|
if (showAll === undefined) {
|
|
12
14
|
const { includeExternal } = await inquirer.prompt([
|
|
13
15
|
{
|
|
14
16
|
type: 'confirm',
|
|
15
17
|
name: 'includeExternal',
|
|
16
|
-
message: 'Include external linked worktrees (outside
|
|
18
|
+
message: 'Include external linked worktrees (outside the managed root)?',
|
|
17
19
|
default: false,
|
|
18
20
|
},
|
|
19
21
|
]);
|
|
@@ -27,7 +29,7 @@ export async function deleteCommand(options = {}) {
|
|
|
27
29
|
const isCurrentWorktree = wt.path === currentWorktreePath;
|
|
28
30
|
return !isMainWorktree && !isCurrentWorktree;
|
|
29
31
|
}
|
|
30
|
-
return isManagedWorktreePath(wt.path);
|
|
32
|
+
return isManagedWorktreePath(wt.path, managedRoot);
|
|
31
33
|
});
|
|
32
34
|
if (deletableWts.length === 0) {
|
|
33
35
|
log.info(includeAll
|
|
@@ -38,11 +40,11 @@ export async function deleteCommand(options = {}) {
|
|
|
38
40
|
const choices = await Promise.all(deletableWts.map(async (wt) => {
|
|
39
41
|
const [activity, type] = await Promise.all([
|
|
40
42
|
getLastActivity(wt.path),
|
|
41
|
-
detectWorktreeType(wt, mainWorktreePath || ''),
|
|
43
|
+
detectWorktreeType(wt, mainWorktreePath || '', managedRoot),
|
|
42
44
|
]);
|
|
43
45
|
const branchName = getWorktreeBranchName(wt);
|
|
44
46
|
const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('—');
|
|
45
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
47
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
46
48
|
return {
|
|
47
49
|
name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
|
|
48
50
|
value: wt.path,
|
|
@@ -62,7 +64,7 @@ export async function deleteCommand(options = {}) {
|
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
const count = selectedPaths.length;
|
|
65
|
-
const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p));
|
|
67
|
+
const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p, managedRoot));
|
|
66
68
|
const { confirm } = await inquirer.prompt([
|
|
67
69
|
{
|
|
68
70
|
type: 'confirm',
|
|
@@ -76,7 +78,7 @@ export async function deleteCommand(options = {}) {
|
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
for (const wtPath of selectedPaths) {
|
|
79
|
-
const worktreeName = formatWorktreeDisplayPath(wtPath);
|
|
81
|
+
const worktreeName = formatWorktreeDisplayPath(wtPath, managedRoot);
|
|
80
82
|
const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
|
|
81
83
|
try {
|
|
82
84
|
await removeWorktree(wtPath);
|
|
@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { listWorktrees } from '../../lib/git.js';
|
|
6
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
6
7
|
import { log, ui } from '../../lib/ui.js';
|
|
7
8
|
import { ensureRepoContext } from '../../lib/repo-context.js';
|
|
8
9
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
@@ -40,13 +41,14 @@ export async function enterCommand(wtName, options = {}) {
|
|
|
40
41
|
await ensureRepoContext();
|
|
41
42
|
const worktrees = await listWorktrees();
|
|
42
43
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
44
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
43
45
|
if (worktrees.length === 0) {
|
|
44
46
|
log.info('No worktrees found.');
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
let targetWt;
|
|
48
50
|
if (wtName) {
|
|
49
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
51
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
50
52
|
if (!targetWt) {
|
|
51
53
|
log.error(`Worktree "${wtName}" not found.`);
|
|
52
54
|
return;
|
|
@@ -56,9 +58,9 @@ export async function enterCommand(wtName, options = {}) {
|
|
|
56
58
|
// Interactive Selection
|
|
57
59
|
const terminalColumns = process.stdout.columns || 100;
|
|
58
60
|
const choices = await Promise.all(worktrees.map(async (wt) => {
|
|
59
|
-
const type = await detectWorktreeType(wt, mainWorktreePath);
|
|
61
|
+
const type = await detectWorktreeType(wt, mainWorktreePath, managedRoot);
|
|
60
62
|
const branchName = getWorktreeBranchName(wt);
|
|
61
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
63
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
62
64
|
return {
|
|
63
65
|
name: formatChoiceLabel(type, branchName, displayPath, terminalColumns),
|
|
64
66
|
value: wt
|
package/dist/commands/wt/exec.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
3
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
4
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
4
5
|
import { log } from '../../lib/ui.js';
|
|
5
6
|
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
6
7
|
export async function execCommand(wtName, commandArgs) {
|
|
@@ -10,12 +11,13 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
10
11
|
try {
|
|
11
12
|
await getRepoRoot();
|
|
12
13
|
const worktrees = await listWorktrees();
|
|
14
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
13
15
|
if (worktrees.length === 0) {
|
|
14
16
|
log.info('No worktrees found.');
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
19
|
if (wtName) {
|
|
18
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
20
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
19
21
|
if (!targetWt) {
|
|
20
22
|
log.error(`Worktree "${wtName}" not found.`);
|
|
21
23
|
return;
|
|
@@ -25,7 +27,7 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
25
27
|
// Interactive Selection
|
|
26
28
|
const choices = worktrees.map(wt => {
|
|
27
29
|
const branchName = getWorktreeBranchName(wt);
|
|
28
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
30
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
29
31
|
return {
|
|
30
32
|
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
31
33
|
value: wt
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { listWorktrees, getRepoRoot, isGitClean, getLastActivity, isGhAvailable, getPrStatusBatch } from '../../lib/git.js';
|
|
3
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
3
4
|
import { log, timeAgo } from '../../lib/ui.js';
|
|
4
5
|
import { detectWorktreeType, formatWorktreeType, formatPrStatus, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
|
|
5
6
|
export async function listCommand() {
|
|
@@ -7,6 +8,7 @@ export async function listCommand() {
|
|
|
7
8
|
await getRepoRoot(); // Verify we are in a git repo
|
|
8
9
|
const worktrees = await listWorktrees();
|
|
9
10
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
11
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
10
12
|
if (worktrees.length === 0) {
|
|
11
13
|
log.info('No worktrees found.');
|
|
12
14
|
return;
|
|
@@ -24,7 +26,7 @@ export async function listCommand() {
|
|
|
24
26
|
console.log(chalk.dim(' ' + '-'.repeat(showPr ? 90 : 70)));
|
|
25
27
|
const rows = await Promise.all(worktrees.map(async (wt, index) => {
|
|
26
28
|
const [typeKey, isClean, lastActive] = await Promise.all([
|
|
27
|
-
detectWorktreeType(wt, mainWorktreePath),
|
|
29
|
+
detectWorktreeType(wt, mainWorktreePath, managedRoot),
|
|
28
30
|
isGitClean(wt.path),
|
|
29
31
|
getLastActivity(wt.path),
|
|
30
32
|
]);
|
package/dist/commands/wt/open.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
|
|
|
5
5
|
import { constants as fsConstants } from 'fs';
|
|
6
6
|
import { execa } from 'execa';
|
|
7
7
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
8
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
8
9
|
import { log, ui } from '../../lib/ui.js';
|
|
9
10
|
import { ensureAutocompletePrompt } from '../../lib/prompt.js';
|
|
10
11
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
@@ -24,10 +25,15 @@ export const OPEN_TOOL_CANDIDATES = [
|
|
|
24
25
|
kind: 'app',
|
|
25
26
|
aliases: ['codex', 'codex-app'],
|
|
26
27
|
bundleId: 'com.openai.codex',
|
|
28
|
+
requiredCommand: 'codex',
|
|
27
29
|
},
|
|
30
|
+
{ id: 'cmux', name: 'Cmux Panel', command: 'cmux', kind: 'terminal', aliases: ['cmux', 'cmux-panel'] },
|
|
31
|
+
{ id: 'tmux', name: 'Tmux Session', command: 'tmux', kind: 'terminal', aliases: ['tmux', 'tmux-session'] },
|
|
28
32
|
];
|
|
29
33
|
const OTHER_COMMAND_ACTION = '__other_command__';
|
|
30
34
|
const NO_OPEN_ACTION = '__no_open__';
|
|
35
|
+
const OPEN_ACTION_PAGE_SIZE = 16;
|
|
36
|
+
const CMUX_TERMINAL_STARTUP_DELAY_MS = 350;
|
|
31
37
|
const yggtreeAccent = chalk.hex('#DEADED');
|
|
32
38
|
function truncateEnd(value, maxLen) {
|
|
33
39
|
if (maxLen <= 0)
|
|
@@ -61,9 +67,6 @@ function formatWorktreeChoiceLabel(type, branchName, displayPath, terminalColumn
|
|
|
61
67
|
function openRail() {
|
|
62
68
|
return yggtreeAccent('│');
|
|
63
69
|
}
|
|
64
|
-
function openSectionLabel(label) {
|
|
65
|
-
return `${yggtreeAccent('┌')} ${chalk.bold(label)}`;
|
|
66
|
-
}
|
|
67
70
|
function formatOpenColumns(name, command, detail) {
|
|
68
71
|
const paddedName = name.padEnd(16);
|
|
69
72
|
const paddedCommand = command.padEnd(13);
|
|
@@ -73,6 +76,51 @@ function formatOpenColumns(name, command, detail) {
|
|
|
73
76
|
export function formatOpenToolChoice(tool) {
|
|
74
77
|
return formatOpenColumns(tool.name, tool.command, '');
|
|
75
78
|
}
|
|
79
|
+
function shellQuote(value) {
|
|
80
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
81
|
+
}
|
|
82
|
+
function formatTerminalSessionName(wtPath) {
|
|
83
|
+
const basename = path.basename(wtPath) || 'worktree';
|
|
84
|
+
const slug = basename.toLowerCase().replace(/[^a-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
85
|
+
return `yggtree-${slug || 'worktree'}`.slice(0, 80);
|
|
86
|
+
}
|
|
87
|
+
export function buildTmuxLaunchCommand(wtPath, env = process.env) {
|
|
88
|
+
const sessionName = formatTerminalSessionName(wtPath);
|
|
89
|
+
if (env.TMUX) {
|
|
90
|
+
return {
|
|
91
|
+
executable: 'tmux',
|
|
92
|
+
args: ['new-window', '-c', wtPath, '-n', sessionName],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
executable: 'tmux',
|
|
97
|
+
args: ['new-session', '-A', '-s', sessionName, '-c', wtPath],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function buildCmuxNewPaneCommand() {
|
|
101
|
+
return {
|
|
102
|
+
executable: 'cmux',
|
|
103
|
+
args: ['new-pane', '--type', 'terminal', '--direction', 'right', '--focus', 'true'],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function parseCmuxSurfaceRef(output) {
|
|
107
|
+
return output.match(/\bsurface:\S+/)?.[0];
|
|
108
|
+
}
|
|
109
|
+
function delay(ms) {
|
|
110
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
111
|
+
}
|
|
112
|
+
export function buildCmuxSendCommand(surfaceRef, wtPath) {
|
|
113
|
+
return {
|
|
114
|
+
executable: 'cmux',
|
|
115
|
+
args: ['send', '--surface', surfaceRef, `cd ${shellQuote(wtPath)}`],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function buildCmuxEnterCommand(surfaceRef) {
|
|
119
|
+
return {
|
|
120
|
+
executable: 'cmux',
|
|
121
|
+
args: ['send-key', '--surface', surfaceRef, 'Enter'],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
76
124
|
export function formatOpenSpecialChoice(value, allowShellAction) {
|
|
77
125
|
if (value === OTHER_COMMAND_ACTION) {
|
|
78
126
|
return formatOpenColumns('Other command', 'custom', 'run first, then stay in shell');
|
|
@@ -129,16 +177,27 @@ export async function macOSAppBundleExists(bundleId) {
|
|
|
129
177
|
export async function detectInstalledOpenTools() {
|
|
130
178
|
const checks = await Promise.all(OPEN_TOOL_CANDIDATES.map(async (tool) => ({
|
|
131
179
|
tool,
|
|
132
|
-
exists: tool
|
|
133
|
-
? await macOSAppBundleExists(tool.bundleId)
|
|
134
|
-
: await commandExists(tool.command),
|
|
180
|
+
exists: await isOpenToolAvailable(tool),
|
|
135
181
|
})));
|
|
136
182
|
return checks
|
|
137
183
|
.filter(check => check.exists)
|
|
138
184
|
.map(check => check.tool);
|
|
139
185
|
}
|
|
140
|
-
function
|
|
141
|
-
|
|
186
|
+
export async function isOpenToolAvailable(tool, checks = {}) {
|
|
187
|
+
const hasCommand = checks.commandExists || commandExists;
|
|
188
|
+
const hasAppBundle = checks.macOSAppBundleExists || macOSAppBundleExists;
|
|
189
|
+
if (tool.requiredCommand) {
|
|
190
|
+
const commandAvailable = await hasCommand(tool.requiredCommand);
|
|
191
|
+
if (!commandAvailable)
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (tool.bundleId) {
|
|
195
|
+
return hasAppBundle(tool.bundleId);
|
|
196
|
+
}
|
|
197
|
+
return hasCommand(tool.command);
|
|
198
|
+
}
|
|
199
|
+
function resolveWorktreeByName(worktrees, wtName, managedRoot) {
|
|
200
|
+
return findWorktreeByName(worktrees, wtName, managedRoot);
|
|
142
201
|
}
|
|
143
202
|
export function resolveOpenToolOption(input, installed) {
|
|
144
203
|
const normalized = input.trim().toLowerCase();
|
|
@@ -146,8 +205,21 @@ export function resolveOpenToolOption(input, installed) {
|
|
|
146
205
|
tool.command.toLowerCase() === normalized ||
|
|
147
206
|
tool.aliases?.some(alias => alias.toLowerCase() === normalized));
|
|
148
207
|
}
|
|
208
|
+
export function resolveOpenToolCandidate(input) {
|
|
209
|
+
const normalized = input.trim().toLowerCase();
|
|
210
|
+
return OPEN_TOOL_CANDIDATES.find(tool => tool.id === normalized ||
|
|
211
|
+
tool.command.toLowerCase() === normalized ||
|
|
212
|
+
tool.aliases?.some(alias => alias.toLowerCase() === normalized));
|
|
213
|
+
}
|
|
214
|
+
async function resolveKnownOpenToolOption(input) {
|
|
215
|
+
const candidate = resolveOpenToolCandidate(input);
|
|
216
|
+
if (!candidate)
|
|
217
|
+
return undefined;
|
|
218
|
+
const exists = await isOpenToolAvailable(candidate);
|
|
219
|
+
return exists ? candidate : undefined;
|
|
220
|
+
}
|
|
149
221
|
export async function resolveOpenToolAction(input, installed) {
|
|
150
|
-
let chosenTool = resolveOpenToolOption(input, installed);
|
|
222
|
+
let chosenTool = resolveOpenToolOption(input, installed) || await resolveKnownOpenToolOption(input);
|
|
151
223
|
if (!chosenTool && await commandExists(input)) {
|
|
152
224
|
chosenTool = {
|
|
153
225
|
id: 'custom',
|
|
@@ -159,27 +231,11 @@ export async function resolveOpenToolAction(input, installed) {
|
|
|
159
231
|
return chosenTool ? { type: 'tool', tool: chosenTool } : undefined;
|
|
160
232
|
}
|
|
161
233
|
export async function promptOpenToolSelection(installedTools, message = 'Select tool to open:') {
|
|
162
|
-
const
|
|
163
|
-
.filter(tool => tool.kind === 'editor')
|
|
164
|
-
.map(tool => ({
|
|
165
|
-
name: formatOpenToolChoice(tool),
|
|
166
|
-
value: tool,
|
|
167
|
-
}));
|
|
168
|
-
const appChoices = installedTools
|
|
169
|
-
.filter(tool => tool.kind === 'app')
|
|
234
|
+
const choices = installedTools
|
|
170
235
|
.map(tool => ({
|
|
171
236
|
name: formatOpenToolChoice(tool),
|
|
172
237
|
value: tool,
|
|
173
238
|
}));
|
|
174
|
-
const choices = [];
|
|
175
|
-
if (ideChoices.length > 0) {
|
|
176
|
-
choices.push(new inquirer.Separator(openSectionLabel('Editors & IDEs')));
|
|
177
|
-
choices.push(...ideChoices);
|
|
178
|
-
}
|
|
179
|
-
if (appChoices.length > 0) {
|
|
180
|
-
choices.push(new inquirer.Separator(openSectionLabel('Apps')));
|
|
181
|
-
choices.push(...appChoices);
|
|
182
|
-
}
|
|
183
239
|
const { selectedTool } = await inquirer.prompt([
|
|
184
240
|
{
|
|
185
241
|
type: 'list',
|
|
@@ -187,79 +243,55 @@ export async function promptOpenToolSelection(installedTools, message = 'Select
|
|
|
187
243
|
message,
|
|
188
244
|
choices,
|
|
189
245
|
loop: false,
|
|
190
|
-
pageSize:
|
|
246
|
+
pageSize: OPEN_ACTION_PAGE_SIZE,
|
|
191
247
|
},
|
|
192
248
|
]);
|
|
193
249
|
return selectedTool;
|
|
194
250
|
}
|
|
195
|
-
export function buildOpenActionsFromSelection(
|
|
196
|
-
if (
|
|
251
|
+
export function buildOpenActionsFromSelection(selectedValue, installedTools, otherCommand) {
|
|
252
|
+
if (selectedValue === NO_OPEN_ACTION)
|
|
197
253
|
return [];
|
|
198
|
-
|
|
199
|
-
.
|
|
200
|
-
|
|
201
|
-
.filter((tool) => Boolean(tool))
|
|
202
|
-
.map(tool => ({ type: 'tool', tool }));
|
|
203
|
-
if (selectedValues.includes(OTHER_COMMAND_ACTION) && otherCommand?.trim()) {
|
|
204
|
-
actions.push({ type: 'other-command', command: otherCommand.trim() });
|
|
254
|
+
if (selectedValue.startsWith('tool:')) {
|
|
255
|
+
const tool = installedTools.find(candidate => `tool:${candidate.id}` === selectedValue);
|
|
256
|
+
return tool ? [{ type: 'tool', tool }] : [];
|
|
205
257
|
}
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
export function validateOpenActionSelection(selectedValues) {
|
|
209
|
-
if (selectedValues.includes(NO_OPEN_ACTION) && selectedValues.length > 1) {
|
|
210
|
-
return 'Choose either "Nothing" or one or more actions, not both.';
|
|
258
|
+
if (selectedValue === OTHER_COMMAND_ACTION && otherCommand?.trim()) {
|
|
259
|
+
return [{ type: 'other-command', command: otherCommand.trim() }];
|
|
211
260
|
}
|
|
212
|
-
return
|
|
261
|
+
return [];
|
|
213
262
|
}
|
|
214
|
-
export
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const appTools = installedTools.filter(tool => tool.kind === 'app');
|
|
220
|
-
if (editorTools.length > 0) {
|
|
221
|
-
choices.push(new inquirer.Separator(openSectionLabel('Editors & IDEs')));
|
|
222
|
-
choices.push(...editorTools.map(tool => ({
|
|
223
|
-
name: formatOpenToolChoice(tool),
|
|
224
|
-
value: `tool:${tool.id}`,
|
|
225
|
-
})));
|
|
226
|
-
}
|
|
227
|
-
if (appTools.length > 0) {
|
|
228
|
-
choices.push(new inquirer.Separator(openSectionLabel('Apps')));
|
|
229
|
-
choices.push(...appTools.map(tool => ({
|
|
230
|
-
name: formatOpenToolChoice(tool),
|
|
231
|
-
value: `tool:${tool.id}`,
|
|
232
|
-
})));
|
|
233
|
-
}
|
|
234
|
-
}
|
|
263
|
+
export function buildOpenActionChoices(installedTools, allowShellAction) {
|
|
264
|
+
const choices = installedTools.map(tool => ({
|
|
265
|
+
name: formatOpenToolChoice(tool),
|
|
266
|
+
value: `tool:${tool.id}`,
|
|
267
|
+
}));
|
|
235
268
|
if (allowShellAction) {
|
|
236
|
-
if (choices.length > 0)
|
|
237
|
-
choices.push(new inquirer.Separator(' '));
|
|
238
|
-
choices.push(new inquirer.Separator(openSectionLabel('Shell')));
|
|
239
269
|
choices.push({
|
|
240
270
|
name: formatOpenSpecialChoice(OTHER_COMMAND_ACTION, allowShellAction),
|
|
241
271
|
value: OTHER_COMMAND_ACTION,
|
|
242
272
|
});
|
|
243
273
|
}
|
|
244
|
-
if (choices.length > 0)
|
|
245
|
-
choices.push(new inquirer.Separator(' '));
|
|
246
274
|
choices.push({
|
|
247
275
|
name: formatOpenSpecialChoice(NO_OPEN_ACTION, allowShellAction),
|
|
248
276
|
value: NO_OPEN_ACTION,
|
|
249
277
|
});
|
|
250
|
-
|
|
278
|
+
return choices;
|
|
279
|
+
}
|
|
280
|
+
export async function promptOpenActions(installedTools, options = {}) {
|
|
281
|
+
const allowShellAction = options.allowShellAction ?? true;
|
|
282
|
+
const choices = buildOpenActionChoices(installedTools, allowShellAction);
|
|
283
|
+
const { selectedValue } = await inquirer.prompt([
|
|
251
284
|
{
|
|
252
|
-
type: '
|
|
253
|
-
name: '
|
|
285
|
+
type: 'list',
|
|
286
|
+
name: 'selectedValue',
|
|
254
287
|
message: options.message || 'Open anything before entering the shell?',
|
|
255
288
|
choices,
|
|
256
289
|
loop: false,
|
|
257
|
-
pageSize:
|
|
258
|
-
validate: validateOpenActionSelection,
|
|
290
|
+
pageSize: OPEN_ACTION_PAGE_SIZE,
|
|
259
291
|
},
|
|
260
292
|
]);
|
|
261
293
|
let otherCommand;
|
|
262
|
-
if (
|
|
294
|
+
if (selectedValue === OTHER_COMMAND_ACTION) {
|
|
263
295
|
const answer = await inquirer.prompt([
|
|
264
296
|
{
|
|
265
297
|
type: 'input',
|
|
@@ -270,9 +302,39 @@ export async function promptOpenActions(installedTools, options = {}) {
|
|
|
270
302
|
]);
|
|
271
303
|
otherCommand = answer.otherCommand;
|
|
272
304
|
}
|
|
273
|
-
return buildOpenActionsFromSelection(
|
|
305
|
+
return buildOpenActionsFromSelection(selectedValue, installedTools, otherCommand);
|
|
274
306
|
}
|
|
275
307
|
export async function launchOpenTool(tool, wtPath) {
|
|
308
|
+
if (tool.id === 'cmux') {
|
|
309
|
+
const newPaneCommand = buildCmuxNewPaneCommand();
|
|
310
|
+
const newPaneResult = await execa(newPaneCommand.executable, newPaneCommand.args, {
|
|
311
|
+
cwd: wtPath,
|
|
312
|
+
});
|
|
313
|
+
const surfaceRef = parseCmuxSurfaceRef(newPaneResult.stdout);
|
|
314
|
+
if (!surfaceRef) {
|
|
315
|
+
throw new Error(`Cmux did not return a terminal surface handle: ${newPaneResult.stdout || '<empty output>'}`);
|
|
316
|
+
}
|
|
317
|
+
await delay(CMUX_TERMINAL_STARTUP_DELAY_MS);
|
|
318
|
+
const sendCommand = buildCmuxSendCommand(surfaceRef, wtPath);
|
|
319
|
+
await execa(sendCommand.executable, sendCommand.args, {
|
|
320
|
+
cwd: wtPath,
|
|
321
|
+
stdio: 'ignore',
|
|
322
|
+
});
|
|
323
|
+
const enterCommand = buildCmuxEnterCommand(surfaceRef);
|
|
324
|
+
await execa(enterCommand.executable, enterCommand.args, {
|
|
325
|
+
cwd: wtPath,
|
|
326
|
+
stdio: 'ignore',
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (tool.id === 'tmux') {
|
|
331
|
+
const { executable, args } = buildTmuxLaunchCommand(wtPath);
|
|
332
|
+
await execa(executable, args, {
|
|
333
|
+
cwd: wtPath,
|
|
334
|
+
stdio: 'inherit',
|
|
335
|
+
});
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
276
338
|
const { executable, args } = buildOpenToolLaunchCommand(tool, wtPath);
|
|
277
339
|
await execa(executable, args, {
|
|
278
340
|
cwd: wtPath,
|
|
@@ -280,6 +342,12 @@ export async function launchOpenTool(tool, wtPath) {
|
|
|
280
342
|
});
|
|
281
343
|
}
|
|
282
344
|
export function buildOpenToolLaunchCommand(tool, wtPath) {
|
|
345
|
+
if (tool.id === 'codex-app') {
|
|
346
|
+
return {
|
|
347
|
+
executable: 'codex',
|
|
348
|
+
args: ['app', wtPath],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
283
351
|
if (tool.bundleId) {
|
|
284
352
|
return {
|
|
285
353
|
executable: '/usr/bin/open',
|
|
@@ -292,10 +360,14 @@ export function buildOpenToolLaunchCommand(tool, wtPath) {
|
|
|
292
360
|
};
|
|
293
361
|
}
|
|
294
362
|
export async function runOpenActions(actions, wtPath, options = {}) {
|
|
363
|
+
let openedTerminal = false;
|
|
295
364
|
for (const action of actions) {
|
|
296
365
|
if (action.type === 'tool' && action.tool) {
|
|
297
366
|
log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(action.tool.name)}...`);
|
|
298
367
|
await launchOpenTool(action.tool, wtPath);
|
|
368
|
+
if (action.tool.kind === 'terminal') {
|
|
369
|
+
openedTerminal = true;
|
|
370
|
+
}
|
|
299
371
|
}
|
|
300
372
|
}
|
|
301
373
|
const shellAction = actions.find(action => action.type === 'other-command');
|
|
@@ -303,7 +375,7 @@ export async function runOpenActions(actions, wtPath, options = {}) {
|
|
|
303
375
|
await enterCommand(wtPath, { exec: shellAction.command });
|
|
304
376
|
return;
|
|
305
377
|
}
|
|
306
|
-
if (options.enter !== false) {
|
|
378
|
+
if (options.enter !== false && !openedTerminal) {
|
|
307
379
|
await enterCommand(wtPath);
|
|
308
380
|
}
|
|
309
381
|
}
|
|
@@ -312,13 +384,14 @@ export async function openCommand(wtName, options = {}) {
|
|
|
312
384
|
await getRepoRoot();
|
|
313
385
|
const worktrees = await listWorktrees();
|
|
314
386
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
387
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
315
388
|
if (worktrees.length === 0) {
|
|
316
389
|
log.info('No worktrees found.');
|
|
317
390
|
return;
|
|
318
391
|
}
|
|
319
392
|
let targetWt;
|
|
320
393
|
if (wtName) {
|
|
321
|
-
targetWt = resolveWorktreeByName(worktrees, wtName);
|
|
394
|
+
targetWt = resolveWorktreeByName(worktrees, wtName, managedRoot);
|
|
322
395
|
if (!targetWt) {
|
|
323
396
|
log.error(`Worktree "${wtName}" not found.`);
|
|
324
397
|
return;
|
|
@@ -328,9 +401,9 @@ export async function openCommand(wtName, options = {}) {
|
|
|
328
401
|
ensureAutocompletePrompt();
|
|
329
402
|
const terminalColumns = process.stdout.columns || 100;
|
|
330
403
|
const candidates = await Promise.all(worktrees.map(async (wt) => {
|
|
331
|
-
const type = await detectWorktreeType(wt, mainWorktreePath);
|
|
404
|
+
const type = await detectWorktreeType(wt, mainWorktreePath, managedRoot);
|
|
332
405
|
const branchName = getWorktreeBranchName(wt);
|
|
333
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
406
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
334
407
|
const rawType = type.toLowerCase();
|
|
335
408
|
const rawDisplayPath = displayPath.toLowerCase();
|
|
336
409
|
const rawBranchName = branchName.toLowerCase();
|
package/dist/commands/wt/path.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
3
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
3
4
|
import { log } from '../../lib/ui.js';
|
|
4
5
|
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
5
6
|
export async function pathCommand(wtName) {
|
|
6
7
|
try {
|
|
7
8
|
await getRepoRoot();
|
|
8
9
|
const worktrees = await listWorktrees();
|
|
10
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
9
11
|
if (worktrees.length === 0) {
|
|
10
12
|
log.info('No worktrees found.');
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
let targetWt;
|
|
14
16
|
if (wtName) {
|
|
15
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
17
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
16
18
|
if (!targetWt) {
|
|
17
19
|
log.error(`Worktree "${wtName}" not found.`);
|
|
18
20
|
return;
|
|
@@ -22,7 +24,7 @@ export async function pathCommand(wtName) {
|
|
|
22
24
|
// Interactive Selection
|
|
23
25
|
const choices = worktrees.map(wt => {
|
|
24
26
|
const branchName = getWorktreeBranchName(wt);
|
|
25
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
27
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
26
28
|
return {
|
|
27
29
|
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
28
30
|
value: wt
|