/ utils / getWorktreePaths.ts
getWorktreePaths.ts
 1  import { sep } from 'path'
 2  import { logEvent } from '../services/analytics/index.js'
 3  import { execFileNoThrowWithCwd } from './execFileNoThrow.js'
 4  import { gitExe } from './git.js'
 5  
 6  /**
 7   * Returns the paths of all worktrees for the current git repository.
 8   * If git is not available, not in a git repo, or only has one worktree,
 9   * returns an empty array.
10   *
11   * This version includes analytics tracking and uses the CLI's gitExe()
12   * resolver. For a portable version without CLI deps, use
13   * getWorktreePathsPortable().
14   *
15   * @param cwd Directory to run the command from
16   * @returns Array of absolute worktree paths
17   */
18  export async function getWorktreePaths(cwd: string): Promise<string[]> {
19    const startTime = Date.now()
20  
21    const { stdout, code } = await execFileNoThrowWithCwd(
22      gitExe(),
23      ['worktree', 'list', '--porcelain'],
24      {
25        cwd,
26        preserveOutputOnError: false,
27      },
28    )
29  
30    const durationMs = Date.now() - startTime
31  
32    if (code !== 0) {
33      logEvent('tengu_worktree_detection', {
34        duration_ms: durationMs,
35        worktree_count: 0,
36        success: false,
37      })
38      return []
39    }
40  
41    // Parse porcelain output - lines starting with "worktree " contain paths
42    // Example:
43    // worktree /Users/foo/repo
44    // HEAD abc123
45    // branch refs/heads/main
46    //
47    // worktree /Users/foo/repo-wt1
48    // HEAD def456
49    // branch refs/heads/feature
50    const worktreePaths = stdout
51      .split('\n')
52      .filter(line => line.startsWith('worktree '))
53      .map(line => line.slice('worktree '.length).normalize('NFC'))
54  
55    logEvent('tengu_worktree_detection', {
56      duration_ms: durationMs,
57      worktree_count: worktreePaths.length,
58      success: true,
59    })
60  
61    // Sort worktrees: current worktree first, then alphabetically
62    const currentWorktree = worktreePaths.find(
63      path => cwd === path || cwd.startsWith(path + sep),
64    )
65    const otherWorktrees = worktreePaths
66      .filter(path => path !== currentWorktree)
67      .sort((a, b) => a.localeCompare(b))
68  
69    return currentWorktree ? [currentWorktree, ...otherWorktrees] : otherWorktrees
70  }