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.
@@ -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 ~/.yggtree)?',
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
@@ -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
@@ -0,0 +1,7 @@
1
+ import { createSandboxCommand } from './create-sandbox.js';
2
+ export async function handoffCommand(options = {}) {
3
+ await createSandboxCommand({
4
+ ...options,
5
+ carry: true,
6
+ });
7
+ }
@@ -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
  ]);
@@ -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.bundleId
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 resolveWorktreeByName(worktrees, wtName) {
141
- return findWorktreeByName(worktrees, wtName);
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 ideChoices = installedTools
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: 10,
246
+ pageSize: OPEN_ACTION_PAGE_SIZE,
191
247
  },
192
248
  ]);
193
249
  return selectedTool;
194
250
  }
195
- export function buildOpenActionsFromSelection(selectedValues, installedTools, otherCommand) {
196
- if (selectedValues.includes(NO_OPEN_ACTION))
251
+ export function buildOpenActionsFromSelection(selectedValue, installedTools, otherCommand) {
252
+ if (selectedValue === NO_OPEN_ACTION)
197
253
  return [];
198
- const actions = selectedValues
199
- .filter(value => value.startsWith('tool:'))
200
- .map(value => installedTools.find(tool => `tool:${tool.id}` === value))
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
- return actions;
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 true;
261
+ return [];
213
262
  }
214
- export async function promptOpenActions(installedTools, options = {}) {
215
- const allowShellAction = options.allowShellAction ?? true;
216
- const choices = [];
217
- if (installedTools.length > 0) {
218
- const editorTools = installedTools.filter(tool => tool.kind === 'editor');
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
- const { selectedValues } = await inquirer.prompt([
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: 'checkbox',
253
- name: 'selectedValues',
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: 10,
258
- validate: validateOpenActionSelection,
290
+ pageSize: OPEN_ACTION_PAGE_SIZE,
259
291
  },
260
292
  ]);
261
293
  let otherCommand;
262
- if (selectedValues.includes(OTHER_COMMAND_ACTION)) {
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(selectedValues, installedTools, otherCommand);
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();
@@ -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