yggtree 1.4.1 → 1.4.3

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.
@@ -6,14 +6,17 @@ 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
8
  import { generateSandboxName, normalizeSandboxName, writeSandboxMeta } from '../../lib/sandbox.js';
9
- import { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
9
+ import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
10
+ import { detectInstalledOpenTools, launchOpenTool, promptOpenToolSelection, } from './open.js';
10
11
  import { execa } from 'execa';
11
- import { spawn } from 'child_process';
12
12
  import fs from 'fs-extra';
13
13
  export async function createSandboxCommand(options = {}) {
14
14
  try {
15
15
  const repoRoot = await getRepoRoot();
16
16
  log.info(`Repo: ${chalk.dim(repoRoot)}`);
17
+ if (options.enter !== undefined) {
18
+ log.warning('`--enter` / `--no-enter` is deprecated. Use `--open` / `--no-open` instead.');
19
+ }
17
20
  // 1. Auto-detect current branch (no prompt!)
18
21
  const currentBranch = await getCurrentBranch();
19
22
  if (!currentBranch) {
@@ -61,19 +64,12 @@ export async function createSandboxCommand(options = {}) {
61
64
  default: true,
62
65
  when: options.bootstrap === undefined,
63
66
  },
64
- {
65
- type: 'confirm',
66
- name: 'shouldEnter',
67
- message: 'Do you want to enter the sandbox now?',
68
- default: true,
69
- when: options.enter === undefined,
70
- },
71
67
  {
72
68
  type: 'confirm',
73
69
  name: 'shouldOpenTool',
74
- message: 'Open a tool after creation? (IDE or agent CLI)',
70
+ message: 'Open an editor after creation?',
75
71
  default: false,
76
- when: options.exec === undefined,
72
+ when: options.exec === undefined && options.open === undefined && options.enter === undefined,
77
73
  },
78
74
  ]);
79
75
  const requestedSandboxName = options.name !== undefined ? options.name : answers.name || '';
@@ -89,10 +85,14 @@ export async function createSandboxCommand(options = {}) {
89
85
  }
90
86
  log.info(`Sandbox name: ${chalk.yellow(sandboxName)}`);
91
87
  const shouldCarry = options.carry !== undefined ? options.carry : answers.carry;
92
- const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
93
88
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
89
+ const shouldOpenTool = options.open !== undefined
90
+ ? options.open
91
+ : options.enter !== undefined
92
+ ? options.enter
93
+ : Boolean(answers.shouldOpenTool);
94
94
  let selectedTool;
95
- if (options.exec === undefined && answers.shouldOpenTool) {
95
+ if (options.exec === undefined && shouldOpenTool) {
96
96
  const installedTools = await detectInstalledOpenTools();
97
97
  if (installedTools.length === 0) {
98
98
  log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
@@ -101,7 +101,6 @@ export async function createSandboxCommand(options = {}) {
101
101
  selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
102
102
  }
103
103
  }
104
- const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
105
104
  // 4. Detect uncommitted changes before creating worktree
106
105
  let changedFiles = [];
107
106
  let submodulePaths = [];
@@ -183,27 +182,28 @@ export async function createSandboxCommand(options = {}) {
183
182
  spinner.succeed(`Sandbox created: ${chalk.cyan(sandboxName)}`);
184
183
  log.dim('This is a local sandbox - no remote branch will be pushed.');
185
184
  // 8. Bootstrap
185
+ await promptAndCopyEnvFiles(repoRoot, wtPath);
186
186
  if (shouldBootstrap) {
187
187
  await runBootstrap(wtPath, repoRoot);
188
188
  }
189
189
  // 9. Exec command
190
- if (execCommandStr && execCommandStr.trim()) {
191
- log.info(`Executing: ${execCommandStr} in ${ui.path(wtPath)}`);
190
+ if (options.exec && options.exec.trim()) {
191
+ log.info(`Executing: ${options.exec} in ${ui.path(wtPath)}`);
192
192
  try {
193
- await execa(execCommandStr, {
193
+ await execa(options.exec, {
194
194
  cwd: wtPath,
195
195
  stdio: 'inherit',
196
196
  shell: true
197
197
  });
198
198
  }
199
199
  catch (error) {
200
- log.actionableError(error.message, execCommandStr, wtPath, [
201
- `cd ${wtPath} && ${execCommandStr}`,
200
+ log.actionableError(error.message, options.exec, wtPath, [
201
+ `cd ${wtPath} && ${options.exec}`,
202
202
  'Check your command syntax and environment variables'
203
203
  ]);
204
204
  }
205
205
  }
206
- if (selectedTool && !isAgentTool(selectedTool)) {
206
+ else if (selectedTool) {
207
207
  try {
208
208
  log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
209
209
  await launchOpenTool(selectedTool, wtPath);
@@ -216,21 +216,7 @@ export async function createSandboxCommand(options = {}) {
216
216
  log.success('Sandbox ready!');
217
217
  log.info(`Use ${chalk.cyan('yggtree wt apply')} to apply changes to origin.`);
218
218
  log.info(`Use ${chalk.cyan('yggtree wt unapply')} to undo applied changes.`);
219
- if (shouldEnter) {
220
- log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
221
- log.dim('Type "exit" to return to the main terminal.');
222
- const shell = process.env.SHELL || 'zsh';
223
- const child = spawn(shell, [], {
224
- cwd: wtPath,
225
- stdio: 'inherit',
226
- });
227
- child.on('close', () => {
228
- log.info('Exited sub-shell.');
229
- });
230
- }
231
- else {
232
- log.header(`cd "${wtPath}"`);
233
- }
219
+ log.header(`cd "${wtPath}"`);
234
220
  }
235
221
  catch (error) {
236
222
  log.actionableError(error.message, 'yggtree wt create-sandbox');
@@ -1,15 +1,16 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import path from 'path';
4
- import { getRepoRoot, getRepoName, createWorktree, fetchAll, listWorktrees } from '../../lib/git.js';
4
+ import { getRepoName, createWorktree, fetchAll, listWorktrees } 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
8
  import { ensureAutocompletePrompt } from '../../lib/prompt.js';
9
+ import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
10
+ import { ensureRepoContext } from '../../lib/repo-context.js';
9
11
  import { enterCommand } from './enter.js';
10
- import { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
12
+ import { detectInstalledOpenTools, promptOpenActions, resolveOpenToolAction, runOpenActions, } from './open.js';
11
13
  import { execa } from 'execa';
12
- import { spawn } from 'child_process';
13
14
  import fs from 'fs-extra';
14
15
  function toSlug(value) {
15
16
  return value
@@ -17,7 +18,7 @@ function toSlug(value) {
17
18
  .replace(/[\/\\]/g, '-')
18
19
  .replace(/\s+/g, '-');
19
20
  }
20
- async function listBranchCandidates() {
21
+ export async function listBranchCandidates() {
21
22
  const [localRefs, remoteRefs] = await Promise.all([
22
23
  execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/heads']),
23
24
  execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/remotes/origin']),
@@ -33,21 +34,26 @@ async function listBranchCandidates() {
33
34
  .filter(ref => ref !== 'origin/HEAD' && !ref.endsWith('/HEAD'))
34
35
  .map(ref => ref.replace(/^origin\//, ''));
35
36
  const localSet = new Set(localBranches);
36
- const remoteOnly = [...new Set(remoteBranches)].filter(branch => !localSet.has(branch));
37
+ const uniqueRemoteBranches = [...new Set(remoteBranches)];
37
38
  const localCandidates = localBranches.map(branchName => ({
38
39
  branchName,
39
40
  checkoutRef: branchName,
40
41
  createLocalBranch: false,
41
42
  source: 'local',
43
+ attachedBranchName: branchName,
44
+ sourceLabel: 'local',
42
45
  }));
43
- const remoteCandidates = remoteOnly.map(branchName => ({
46
+ const remoteCandidates = uniqueRemoteBranches.map(branchName => ({
44
47
  branchName,
45
48
  checkoutRef: `origin/${branchName}`,
46
- createLocalBranch: true,
49
+ createLocalBranch: !localSet.has(branchName),
47
50
  source: 'remote',
51
+ attachedBranchName: localSet.has(branchName) ? undefined : branchName,
52
+ sourceLabel: localSet.has(branchName) ? 'remote tip, detached' : 'remote, creates local branch',
48
53
  }));
49
54
  return [...localCandidates, ...remoteCandidates]
50
- .sort((a, b) => a.branchName.localeCompare(b.branchName));
55
+ .sort((a, b) => a.branchName.localeCompare(b.branchName) ||
56
+ a.checkoutRef.localeCompare(b.checkoutRef));
51
57
  }
52
58
  function resolveCandidateFromRef(ref, candidates) {
53
59
  const trimmedRef = ref.trim();
@@ -55,14 +61,22 @@ function resolveCandidateFromRef(ref, candidates) {
55
61
  return undefined;
56
62
  if (trimmedRef.startsWith('origin/')) {
57
63
  const branchName = trimmedRef.replace(/^origin\//, '');
58
- return candidates.find(candidate => candidate.branchName === branchName);
64
+ return candidates.find(candidate => candidate.branchName === branchName &&
65
+ candidate.checkoutRef === trimmedRef);
59
66
  }
60
67
  return candidates.find(candidate => candidate.branchName === trimmedRef ||
61
68
  candidate.checkoutRef === trimmedRef);
62
69
  }
70
+ export function findExistingBranchWorktree(worktrees, branchName) {
71
+ if (!branchName)
72
+ return undefined;
73
+ return worktrees.find(wt => wt.branch === branchName &&
74
+ !wt.prunable &&
75
+ fs.pathExistsSync(wt.path));
76
+ }
63
77
  export async function createCommand(options) {
64
78
  try {
65
- const repoRoot = await getRepoRoot();
79
+ const repoRoot = await ensureRepoContext();
66
80
  log.info(`Repo: ${chalk.dim(repoRoot)}`);
67
81
  // 1. Load branches
68
82
  const loadingSpinner = createSpinner('Fetching branches...').start();
@@ -94,10 +108,11 @@ export async function createCommand(options) {
94
108
  source: async (_answers, input = '') => {
95
109
  const term = input.trim().toLowerCase();
96
110
  const filtered = term
97
- ? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term))
111
+ ? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term) ||
112
+ candidate.checkoutRef.toLowerCase().includes(term))
98
113
  : candidates;
99
114
  return filtered.map(candidate => ({
100
- name: `${chalk.yellow(candidate.branchName)} ${chalk.dim(`(${candidate.source})`)}`,
115
+ name: `${chalk.yellow(candidate.checkoutRef)} ${chalk.dim(`(${candidate.sourceLabel})`)}`,
101
116
  value: candidate,
102
117
  }));
103
118
  },
@@ -111,15 +126,55 @@ export async function createCommand(options) {
111
126
  return;
112
127
  }
113
128
  const existingWorktrees = await listWorktrees();
114
- const existingManagedWorktree = existingWorktrees.find(wt => wt.branch === selectedBranch.branchName && wt.path.startsWith(WORKTREES_ROOT));
115
- if (existingManagedWorktree) {
116
- log.info(`Branch ${chalk.cyan(selectedBranch.branchName)} is already active in ${ui.path(existingManagedWorktree.path)}.`);
117
- if (options.enter === false) {
118
- log.header(`cd "${existingManagedWorktree.path}"`);
129
+ const existingBranchWorktree = findExistingBranchWorktree(existingWorktrees, selectedBranch.attachedBranchName);
130
+ const shouldEnterShell = options.enter !== false;
131
+ if (existingBranchWorktree) {
132
+ log.info(`Branch ${chalk.cyan(selectedBranch.branchName)} is already active in ${ui.path(existingBranchWorktree.path)}.`);
133
+ if (options.exec && options.exec.trim()) {
134
+ log.info('Reusing the existing worktree and running the requested command...');
135
+ await enterCommand(existingBranchWorktree.path, { exec: options.exec });
119
136
  return;
120
137
  }
138
+ const shouldOpenExisting = options.tool
139
+ ? true
140
+ : options.open !== undefined
141
+ ? options.open
142
+ : (await inquirer.prompt([
143
+ {
144
+ type: 'confirm',
145
+ name: 'shouldOpenTool',
146
+ message: shouldEnterShell
147
+ ? 'Open anything before entering the existing worktree shell?'
148
+ : 'Open anything in the existing worktree?',
149
+ default: true,
150
+ },
151
+ ])).shouldOpenTool;
152
+ if (!shouldOpenExisting) {
153
+ if (shouldEnterShell) {
154
+ await enterCommand(existingBranchWorktree.path);
155
+ return;
156
+ }
157
+ log.header(`cd "${existingBranchWorktree.path}"`);
158
+ return;
159
+ }
160
+ const installedTools = await detectInstalledOpenTools();
121
161
  log.info('Opening existing worktree instead of creating a duplicate...');
122
- await enterCommand(existingManagedWorktree.path, { exec: options.exec ?? '' });
162
+ const actions = options.tool
163
+ ? [await resolveOpenToolAction(options.tool, installedTools)].filter((action) => Boolean(action))
164
+ : await promptOpenActions(installedTools, {
165
+ allowShellAction: shouldEnterShell,
166
+ message: shouldEnterShell
167
+ ? 'Open anything before entering the existing worktree shell?'
168
+ : 'Open anything in the existing worktree?',
169
+ });
170
+ if (options.tool && actions.length === 0) {
171
+ const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
172
+ log.error(`Tool "${options.tool}" not found.`);
173
+ log.dim(`Detected tool commands: ${available}`);
174
+ return;
175
+ }
176
+ await runOpenActions(actions, existingBranchWorktree.path, { enter: shouldEnterShell });
177
+ log.success('Existing worktree opened.');
123
178
  return;
124
179
  }
125
180
  // 3. Gather remaining inputs
@@ -140,35 +195,45 @@ export async function createCommand(options) {
140
195
  default: true,
141
196
  when: options.bootstrap !== false && options.bootstrap !== true,
142
197
  },
143
- {
144
- type: 'confirm',
145
- name: 'shouldEnter',
146
- message: 'Do you want to enter the new worktree now?',
147
- default: true,
148
- when: options.enter === undefined,
149
- },
150
198
  {
151
199
  type: 'confirm',
152
200
  name: 'shouldOpenTool',
153
- message: 'Open a tool after creation? (IDE or agent CLI)',
201
+ message: shouldEnterShell
202
+ ? 'Open anything before entering the worktree shell?'
203
+ : 'Open anything after creation?',
154
204
  default: false,
155
- when: options.exec === undefined,
205
+ when: options.exec === undefined && options.open === undefined && options.tool === undefined,
156
206
  },
157
207
  ]);
158
208
  const name = options.name || answers.name;
159
- const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
160
209
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
161
- let selectedTool;
162
- if (options.exec === undefined && answers.shouldOpenTool) {
210
+ const shouldOpenTool = options.tool
211
+ ? true
212
+ : options.open !== undefined
213
+ ? options.open
214
+ : Boolean(answers.shouldOpenTool);
215
+ let openActions = [];
216
+ if (options.exec === undefined && shouldOpenTool) {
163
217
  const installedTools = await detectInstalledOpenTools();
164
- if (installedTools.length === 0) {
165
- log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
218
+ if (options.tool) {
219
+ const toolAction = await resolveOpenToolAction(options.tool, installedTools);
220
+ if (!toolAction) {
221
+ const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
222
+ log.error(`Tool "${options.tool}" not found.`);
223
+ log.dim(`Detected tool commands: ${available}`);
224
+ return;
225
+ }
226
+ openActions = [toolAction];
166
227
  }
167
228
  else {
168
- selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
229
+ openActions = await promptOpenActions(installedTools, {
230
+ allowShellAction: shouldEnterShell,
231
+ message: shouldEnterShell
232
+ ? 'Open anything before entering the worktree shell?'
233
+ : 'Open anything after creation?',
234
+ });
169
235
  }
170
236
  }
171
- const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
172
237
  const slug = toSlug(name);
173
238
  const repoName = await getRepoName();
174
239
  const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
@@ -200,52 +265,38 @@ export async function createCommand(options) {
200
265
  ]);
201
266
  return;
202
267
  }
268
+ await promptAndCopyEnvFiles(repoRoot, wtPath);
203
269
  if (shouldBootstrap) {
204
270
  await runBootstrap(wtPath, repoRoot);
205
271
  }
206
272
  // 6. Exec Command
207
- if (execCommandStr && execCommandStr.trim()) {
208
- log.info(`Executing: ${execCommandStr} in ${ui.path(wtPath)}`);
273
+ if (options.exec && options.exec.trim()) {
274
+ log.info(`Executing: ${options.exec} in ${ui.path(wtPath)}`);
209
275
  try {
210
- await execa(execCommandStr, {
276
+ await execa(options.exec, {
211
277
  cwd: wtPath,
212
278
  stdio: 'inherit',
213
279
  shell: true
214
280
  });
215
281
  }
216
282
  catch (error) {
217
- log.actionableError(error.message, execCommandStr, wtPath, [
218
- `cd ${wtPath} && ${execCommandStr}`,
283
+ log.actionableError(error.message, options.exec, wtPath, [
284
+ `cd ${wtPath} && ${options.exec}`,
219
285
  'Check your command syntax and environment variables'
220
286
  ]);
221
287
  }
222
288
  }
223
- if (selectedTool && !isAgentTool(selectedTool)) {
289
+ else {
224
290
  try {
225
- log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
226
- await launchOpenTool(selectedTool, wtPath);
291
+ await runOpenActions(openActions, wtPath, { enter: shouldEnterShell });
227
292
  }
228
293
  catch (error) {
229
- log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
294
+ log.warning(`Could not complete post-checkout action: ${error.message}`);
230
295
  }
231
296
  }
232
297
  // 7. Final Output
233
298
  log.success('Worktree ready!');
234
- if (shouldEnter) {
235
- log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
236
- log.dim('Type "exit" to return to the main terminal.');
237
- const shell = process.env.SHELL || 'zsh';
238
- const child = spawn(shell, [], {
239
- cwd: wtPath,
240
- stdio: 'inherit',
241
- });
242
- child.on('close', () => {
243
- log.info('Exited sub-shell.');
244
- });
245
- }
246
- else {
247
- log.header(`cd "${wtPath}"`);
248
- }
299
+ log.header(`cd "${wtPath}"`);
249
300
  }
250
301
  catch (error) {
251
302
  log.actionableError(error.message, 'yggtree wt worktree-checkout');
@@ -2,8 +2,9 @@ import inquirer from 'inquirer';
2
2
  import { spawn } from 'child_process';
3
3
  import { execa } from 'execa';
4
4
  import chalk from 'chalk';
5
- import { listWorktrees, getRepoRoot } from '../../lib/git.js';
5
+ import { listWorktrees } from '../../lib/git.js';
6
6
  import { log, ui } from '../../lib/ui.js';
7
+ import { ensureRepoContext } from '../../lib/repo-context.js';
7
8
  import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
8
9
  function truncateEnd(value, maxLen) {
9
10
  if (maxLen <= 0)
@@ -36,7 +37,7 @@ function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
36
37
  }
37
38
  export async function enterCommand(wtName, options = {}) {
38
39
  try {
39
- await getRepoRoot();
40
+ await ensureRepoContext();
40
41
  const worktrees = await listWorktrees();
41
42
  const mainWorktreePath = worktrees[0]?.path || '';
42
43
  if (worktrees.length === 0) {
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
- import { listWorktrees, getRepoRoot, isGitClean, getLastActivity } from '../../lib/git.js';
2
+ import { listWorktrees, getRepoRoot, isGitClean, getLastActivity, isGhAvailable, getPrStatusBatch } from '../../lib/git.js';
3
3
  import { log, timeAgo } from '../../lib/ui.js';
4
- import { detectWorktreeType, formatWorktreeType, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
4
+ import { detectWorktreeType, formatWorktreeType, formatPrStatus, 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
@@ -11,10 +11,17 @@ export async function listCommand() {
11
11
  log.info('No worktrees found.');
12
12
  return;
13
13
  }
14
+ // Determine if PR status column should be shown
15
+ const ghReady = await isGhAvailable();
16
+ // Collect branch names for batch PR lookup
17
+ const branches = worktrees.map(wt => getWorktreeBranchName(wt));
18
+ const prStatusMap = ghReady ? await getPrStatusBatch(branches) : new Map();
19
+ const showPr = prStatusMap.size > 0;
14
20
  console.log(chalk.bold('\n Active Worktrees:\n'));
15
- // Header
16
- console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('LAST ACTIVE')} ${chalk.dim('BRANCH')}`);
17
- console.log(chalk.dim(' ' + '-'.repeat(70)));
21
+ // Header — PR column only appears when there's data
22
+ const headerPr = showPr ? `${chalk.dim('PR')} ` : '';
23
+ console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('LAST ACTIVE')} ${headerPr}${chalk.dim('BRANCH')}`);
24
+ console.log(chalk.dim(' ' + '-'.repeat(showPr ? 90 : 70)));
18
25
  const rows = await Promise.all(worktrees.map(async (wt, index) => {
19
26
  const [typeKey, isClean, lastActive] = await Promise.all([
20
27
  detectWorktreeType(wt, mainWorktreePath),
@@ -27,8 +34,12 @@ export async function listCommand() {
27
34
  const stateText = isClean ? chalk.green(stateLabel) : chalk.yellow(stateLabel);
28
35
  const activeLabel = lastActive ? timeAgo(lastActive) : '—';
29
36
  const activeText = chalk.magenta(activeLabel.padEnd(14));
37
+ const prStatus = prStatusMap.get(branchName);
38
+ const prText = showPr
39
+ ? (prStatus ? formatPrStatus(prStatus).padEnd(24) : chalk.dim('—'.padEnd(14)))
40
+ : '';
30
41
  return {
31
- text: ` ${type} ${stateText} ${activeText} ${chalk.yellow(branchName)}`,
42
+ text: ` ${type} ${stateText} ${activeText} ${prText}${chalk.yellow(branchName)}`,
32
43
  sortType: WORKTREE_TYPE_ORDER[typeKey],
33
44
  sortBranch: branchName.toLowerCase(),
34
45
  sortIndex: index,
@@ -39,6 +50,12 @@ export async function listCommand() {
39
50
  a.sortBranch.localeCompare(b.sortBranch) ||
40
51
  a.sortIndex - b.sortIndex)
41
52
  .forEach(row => console.log(row.text));
53
+ if (ghReady && !showPr) {
54
+ console.log(chalk.dim('\n ℹ No open PRs found for any worktree branch.'));
55
+ }
56
+ else if (!ghReady) {
57
+ console.log(chalk.dim('\n ℹ PR status omitted (gh CLI not found). Install GitHub CLI for PR tracking.'));
58
+ }
42
59
  console.log('');
43
60
  }
44
61
  catch (error) {