@jasonfutch/worktree-manager 1.0.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.
@@ -0,0 +1,57 @@
1
+ import type { Worktree, CreateWorktreeOptions } from '../types.js';
2
+ export declare class GitWorktree {
3
+ private repoPath;
4
+ constructor(repoPath: string);
5
+ /**
6
+ * Check if path is a valid git repository
7
+ */
8
+ static isGitRepo(dirPath: string): boolean;
9
+ /**
10
+ * Get the root of the git repository
11
+ */
12
+ static getRepoRoot(dirPath: string): string | null;
13
+ /**
14
+ * List all worktrees for this repository
15
+ */
16
+ list(): Promise<Worktree[]>;
17
+ /**
18
+ * Create a new worktree
19
+ */
20
+ create(options: CreateWorktreeOptions): Promise<Worktree>;
21
+ /**
22
+ * Remove a worktree
23
+ */
24
+ remove(worktreePath: string, force?: boolean): Promise<void>;
25
+ /**
26
+ * Prune worktrees (clean up stale entries)
27
+ */
28
+ prune(): Promise<void>;
29
+ /**
30
+ * Delete a branch
31
+ * @param branch - Branch name to delete
32
+ * @param force - If true, use -D (force delete) instead of -d
33
+ */
34
+ deleteBranch(branch: string, force?: boolean): Promise<void>;
35
+ /**
36
+ * Check if a branch exists
37
+ * @returns true if branch exists, false otherwise
38
+ * @note This method returns false on any error (including invalid branch names)
39
+ */
40
+ private branchExists;
41
+ /**
42
+ * Get list of all local branches
43
+ * @returns Array of branch names, empty array on error
44
+ * @note This method returns empty array on any error
45
+ */
46
+ getBranches(): Promise<string[]>;
47
+ /**
48
+ * Get current branch name
49
+ * @returns Current branch name, or 'unknown' on error
50
+ * @note This method returns 'unknown' on any error
51
+ */
52
+ getCurrentBranch(): Promise<string>;
53
+ /**
54
+ * Get repo name
55
+ */
56
+ getRepoName(): string;
57
+ }
@@ -0,0 +1,272 @@
1
+ import { execSync, exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import { WorktreeNotFoundError, InvalidBranchError, InvalidPathError, parseGitError, } from '../errors.js';
6
+ import { isPathSafe, isValidBranchName, escapeShellArg } from '../utils/shell.js';
7
+ import { GIT } from '../constants.js';
8
+ const execAsync = promisify(exec);
9
+ export class GitWorktree {
10
+ repoPath;
11
+ constructor(repoPath) {
12
+ this.repoPath = repoPath;
13
+ }
14
+ /**
15
+ * Check if path is a valid git repository
16
+ */
17
+ static isGitRepo(dirPath) {
18
+ try {
19
+ execSync('git rev-parse --git-dir', { cwd: dirPath, stdio: 'pipe' });
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * Get the root of the git repository
28
+ */
29
+ static getRepoRoot(dirPath) {
30
+ try {
31
+ const result = execSync('git rev-parse --show-toplevel', {
32
+ cwd: dirPath,
33
+ encoding: 'utf-8',
34
+ stdio: ['pipe', 'pipe', 'pipe']
35
+ });
36
+ return result.trim();
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /**
43
+ * List all worktrees for this repository
44
+ */
45
+ async list() {
46
+ const command = 'git worktree list --porcelain';
47
+ try {
48
+ const { stdout } = await execAsync(command, {
49
+ cwd: this.repoPath
50
+ });
51
+ const worktrees = [];
52
+ const entries = stdout.trim().split('\n\n');
53
+ for (const entry of entries) {
54
+ if (!entry.trim())
55
+ continue;
56
+ const lines = entry.split('\n');
57
+ const worktree = {
58
+ isMain: false,
59
+ isBare: false,
60
+ isLocked: false,
61
+ prunable: false,
62
+ };
63
+ for (const line of lines) {
64
+ if (line.startsWith('worktree ')) {
65
+ worktree.path = line.substring(9);
66
+ worktree.name = path.basename(worktree.path);
67
+ }
68
+ else if (line.startsWith('HEAD ')) {
69
+ // Extract SHA safely - handle variable length commits
70
+ const sha = line.substring(5);
71
+ worktree.commit = sha.substring(0, GIT.SHORT_SHA_LENGTH);
72
+ }
73
+ else if (line.startsWith('branch ')) {
74
+ // Parse branch reference more robustly
75
+ const ref = line.substring(7);
76
+ worktree.branch = ref.replace(/^refs\/heads\//, '');
77
+ }
78
+ else if (line === 'bare') {
79
+ worktree.isBare = true;
80
+ }
81
+ else if (line === 'locked') {
82
+ worktree.isLocked = true;
83
+ }
84
+ else if (line.startsWith('locked ')) {
85
+ worktree.isLocked = true;
86
+ worktree.lockedReason = line.substring(7);
87
+ }
88
+ else if (line === 'prunable') {
89
+ worktree.prunable = true;
90
+ }
91
+ else if (line === 'detached') {
92
+ worktree.branch = 'HEAD (detached)';
93
+ }
94
+ }
95
+ // Check if this is the main worktree
96
+ if (worktree.path === this.repoPath) {
97
+ worktree.isMain = true;
98
+ }
99
+ if (worktree.path && worktree.commit) {
100
+ worktrees.push(worktree);
101
+ }
102
+ }
103
+ return worktrees;
104
+ }
105
+ catch (error) {
106
+ throw parseGitError(error, command);
107
+ }
108
+ }
109
+ /**
110
+ * Create a new worktree
111
+ */
112
+ async create(options) {
113
+ const { branch, baseBranch = GIT.DEFAULT_BASE_BRANCH, path: customPath } = options;
114
+ // Validate branch name
115
+ if (!isValidBranchName(branch)) {
116
+ throw new InvalidBranchError(branch, 'Branch names cannot contain spaces, .., or special characters');
117
+ }
118
+ // Generate worktree path if not provided
119
+ const worktreePath = customPath || path.join(path.dirname(this.repoPath), GIT.WORKTREES_DIR, branch.replace(/\//g, '-'));
120
+ // Validate path doesn't contain traversal attempts
121
+ const repoParent = path.dirname(this.repoPath);
122
+ if (!isPathSafe(worktreePath, repoParent)) {
123
+ throw new InvalidPathError(worktreePath, 'Path contains invalid characters or traversal attempts');
124
+ }
125
+ // Ensure parent directory exists
126
+ const parentDir = path.dirname(worktreePath);
127
+ if (!fs.existsSync(parentDir)) {
128
+ fs.mkdirSync(parentDir, { recursive: true });
129
+ }
130
+ try {
131
+ // Check if branch exists
132
+ const branchExists = await this.branchExists(branch);
133
+ // Use escaped arguments for safety
134
+ const escapedPath = escapeShellArg(worktreePath);
135
+ const escapedBranch = escapeShellArg(branch);
136
+ const escapedBase = escapeShellArg(baseBranch);
137
+ let command;
138
+ if (branchExists) {
139
+ // Checkout existing branch
140
+ command = `git worktree add ${escapedPath} ${escapedBranch}`;
141
+ }
142
+ else {
143
+ // Create new branch from base
144
+ command = `git worktree add -b ${escapedBranch} ${escapedPath} ${escapedBase}`;
145
+ }
146
+ await execAsync(command, { cwd: this.repoPath });
147
+ // Get the created worktree info
148
+ const worktrees = await this.list();
149
+ const created = worktrees.find(wt => wt.path === worktreePath);
150
+ if (!created) {
151
+ throw new WorktreeNotFoundError(worktreePath);
152
+ }
153
+ return created;
154
+ }
155
+ catch (error) {
156
+ if (error instanceof WorktreeNotFoundError) {
157
+ throw error;
158
+ }
159
+ throw parseGitError(error, `git worktree add ${branch}`);
160
+ }
161
+ }
162
+ /**
163
+ * Remove a worktree
164
+ */
165
+ async remove(worktreePath, force = false) {
166
+ // Validate path
167
+ if (!isPathSafe(worktreePath)) {
168
+ throw new InvalidPathError(worktreePath, 'Path contains invalid characters');
169
+ }
170
+ const escapedPath = escapeShellArg(worktreePath);
171
+ const forceFlag = force ? '--force' : '';
172
+ const command = `git worktree remove ${forceFlag} ${escapedPath}`;
173
+ try {
174
+ await execAsync(command, { cwd: this.repoPath });
175
+ }
176
+ catch (error) {
177
+ const gitError = parseGitError(error, command);
178
+ // Check if it's a not-found error
179
+ if (gitError.stderr.includes('is not a working tree')) {
180
+ throw new WorktreeNotFoundError(worktreePath);
181
+ }
182
+ throw gitError;
183
+ }
184
+ }
185
+ /**
186
+ * Prune worktrees (clean up stale entries)
187
+ */
188
+ async prune() {
189
+ const command = 'git worktree prune';
190
+ try {
191
+ await execAsync(command, { cwd: this.repoPath });
192
+ }
193
+ catch (error) {
194
+ throw parseGitError(error, command);
195
+ }
196
+ }
197
+ /**
198
+ * Delete a branch
199
+ * @param branch - Branch name to delete
200
+ * @param force - If true, use -D (force delete) instead of -d
201
+ */
202
+ async deleteBranch(branch, force = false) {
203
+ if (!isValidBranchName(branch)) {
204
+ throw new InvalidBranchError(branch, 'Invalid branch name');
205
+ }
206
+ const escapedBranch = escapeShellArg(branch);
207
+ const flag = force ? '-D' : '-d';
208
+ const command = `git branch ${flag} ${escapedBranch}`;
209
+ try {
210
+ await execAsync(command, { cwd: this.repoPath });
211
+ }
212
+ catch (error) {
213
+ throw parseGitError(error, command);
214
+ }
215
+ }
216
+ /**
217
+ * Check if a branch exists
218
+ * @returns true if branch exists, false otherwise
219
+ * @note This method returns false on any error (including invalid branch names)
220
+ */
221
+ async branchExists(branch) {
222
+ try {
223
+ const escapedBranch = escapeShellArg(branch);
224
+ await execAsync(`git rev-parse --verify ${escapedBranch}`, {
225
+ cwd: this.repoPath
226
+ });
227
+ return true;
228
+ }
229
+ catch {
230
+ return false;
231
+ }
232
+ }
233
+ /**
234
+ * Get list of all local branches
235
+ * @returns Array of branch names, empty array on error
236
+ * @note This method returns empty array on any error
237
+ */
238
+ async getBranches() {
239
+ try {
240
+ const { stdout } = await execAsync('git branch -a --format="%(refname:short)"', {
241
+ cwd: this.repoPath
242
+ });
243
+ return stdout.trim().split('\n').filter(b => b && !b.startsWith('origin/'));
244
+ }
245
+ catch {
246
+ // Return empty array on error - caller should handle empty results
247
+ return [];
248
+ }
249
+ }
250
+ /**
251
+ * Get current branch name
252
+ * @returns Current branch name, or 'unknown' on error
253
+ * @note This method returns 'unknown' on any error
254
+ */
255
+ async getCurrentBranch() {
256
+ try {
257
+ const { stdout } = await execAsync('git branch --show-current', {
258
+ cwd: this.repoPath
259
+ });
260
+ return stdout.trim();
261
+ }
262
+ catch {
263
+ return 'unknown';
264
+ }
265
+ }
266
+ /**
267
+ * Get repo name
268
+ */
269
+ getRepoName() {
270
+ return path.basename(this.repoPath);
271
+ }
272
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,336 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { GitWorktree } from './git/worktree.js';
4
+ import { WorktreeManagerTUI } from './tui/app.js';
5
+ import { runStartupChecks } from './utils/checks.js';
6
+ import { openEditor, openTerminal, launchAI, validEditors, validAITools } from './utils/launch.js';
7
+ import chalk from 'chalk';
8
+ import path from 'path';
9
+ import fs from 'fs';
10
+ import readline from 'readline';
11
+ import { GIT } from './constants.js';
12
+ import { VERSION, PACKAGE_NAME, pkg } from './version.js';
13
+ import updateNotifier from 'update-notifier';
14
+ import { execSync } from 'child_process';
15
+ // Check dependencies before anything else
16
+ runStartupChecks();
17
+ // Check for updates (runs in background, notifies if update available)
18
+ const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 * 60 * 60 * 24 }); // Check daily
19
+ notifier.notify({ isGlobal: true });
20
+ // Helper to prompt user for input
21
+ function promptUser(question) {
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout
25
+ });
26
+ return new Promise((resolve) => {
27
+ rl.question(question, (answer) => {
28
+ rl.close();
29
+ resolve(answer.trim());
30
+ });
31
+ });
32
+ }
33
+ // Helper to check if a branch is protected
34
+ function isProtectedBranch(branch) {
35
+ const normalizedBranch = branch.toLowerCase();
36
+ return GIT.PROTECTED_BRANCHES.some(protectedBranch => normalizedBranch === protectedBranch.toLowerCase());
37
+ }
38
+ const program = new Command();
39
+ program
40
+ .name('worktree-manager')
41
+ .description('Terminal app for managing git worktrees with AI assistance')
42
+ .version(VERSION)
43
+ .argument('[path]', 'Path to git repository', '.')
44
+ .action(async (repoPath) => {
45
+ const resolvedPath = path.resolve(repoPath);
46
+ // Validate path exists
47
+ if (!fs.existsSync(resolvedPath)) {
48
+ console.error(chalk.red(`Error: Path does not exist: ${resolvedPath}`));
49
+ process.exit(1);
50
+ }
51
+ // Validate it's a git repo
52
+ if (!GitWorktree.isGitRepo(resolvedPath)) {
53
+ console.error(chalk.red(`Error: Not a git repository: ${resolvedPath}`));
54
+ process.exit(1);
55
+ }
56
+ // Get the repo root (in case user passed a subdirectory)
57
+ const repoRoot = GitWorktree.getRepoRoot(resolvedPath);
58
+ if (!repoRoot) {
59
+ console.error(chalk.red(`Error: Could not determine git repository root`));
60
+ process.exit(1);
61
+ }
62
+ // Launch TUI
63
+ const tui = new WorktreeManagerTUI(repoRoot);
64
+ await tui.start();
65
+ });
66
+ // Subcommand: list worktrees (non-interactive)
67
+ program
68
+ .command('list')
69
+ .description('List all worktrees (non-interactive)')
70
+ .argument('[path]', 'Path to git repository', '.')
71
+ .action(async (repoPath) => {
72
+ const resolvedPath = path.resolve(repoPath);
73
+ const repoRoot = GitWorktree.getRepoRoot(resolvedPath);
74
+ if (!repoRoot) {
75
+ console.error(chalk.red('Not a git repository'));
76
+ process.exit(1);
77
+ }
78
+ const git = new GitWorktree(repoRoot);
79
+ const worktrees = await git.list();
80
+ console.log(chalk.cyan.bold('\n Git Worktrees\n'));
81
+ for (const wt of worktrees) {
82
+ const mainBadge = wt.isMain ? chalk.green(' [main]') : '';
83
+ const lockBadge = wt.isLocked ? chalk.red(' [locked]') : '';
84
+ console.log(` ${chalk.yellow('●')} ${chalk.bold(wt.branch)}${mainBadge}${lockBadge}`);
85
+ console.log(` ${chalk.gray('Path:')} ${wt.path}`);
86
+ console.log(` ${chalk.gray('Commit:')} ${wt.commit}\n`);
87
+ }
88
+ });
89
+ // Subcommand: create worktree
90
+ program
91
+ .command('create <branch>')
92
+ .description('Create a new worktree')
93
+ .argument('[path]', 'Path to git repository', '.')
94
+ .option('-b, --base <branch>', 'Base branch to create from', 'main')
95
+ .option('-p, --path <path>', 'Custom path for the worktree')
96
+ .action(async (branch, repoPath, options) => {
97
+ const resolvedPath = path.resolve(repoPath || '.');
98
+ const repoRoot = GitWorktree.getRepoRoot(resolvedPath);
99
+ if (!repoRoot) {
100
+ console.error(chalk.red('Not a git repository'));
101
+ process.exit(1);
102
+ }
103
+ const git = new GitWorktree(repoRoot);
104
+ console.log(chalk.cyan(`Creating worktree for branch: ${branch}...`));
105
+ try {
106
+ const wt = await git.create({
107
+ branch,
108
+ baseBranch: options.base,
109
+ path: options.path
110
+ });
111
+ console.log(chalk.green(`✓ Created worktree: ${wt.path}`));
112
+ }
113
+ catch (error) {
114
+ console.error(chalk.red(`Error: ${error}`));
115
+ process.exit(1);
116
+ }
117
+ });
118
+ // Subcommand: remove worktree
119
+ program
120
+ .command('remove <branch-or-path>')
121
+ .description('Remove a worktree')
122
+ .argument('[repo-path]', 'Path to git repository', '.')
123
+ .option('-f, --force', 'Force removal even with local changes')
124
+ .action(async (branchOrPath, repoPath, options) => {
125
+ const resolvedPath = path.resolve(repoPath || '.');
126
+ const repoRoot = GitWorktree.getRepoRoot(resolvedPath);
127
+ if (!repoRoot) {
128
+ console.error(chalk.red('Not a git repository'));
129
+ process.exit(1);
130
+ }
131
+ const git = new GitWorktree(repoRoot);
132
+ const worktrees = await git.list();
133
+ // Find worktree by branch name or path
134
+ const wt = worktrees.find(w => w.branch === branchOrPath ||
135
+ w.path === branchOrPath ||
136
+ w.path.endsWith(branchOrPath));
137
+ if (!wt) {
138
+ console.error(chalk.red(`Worktree not found: ${branchOrPath}`));
139
+ process.exit(1);
140
+ }
141
+ if (wt.isMain) {
142
+ console.error(chalk.red('Cannot remove main worktree'));
143
+ process.exit(1);
144
+ }
145
+ const branchName = wt.branch;
146
+ console.log(chalk.yellow(`Removing worktree: ${branchName}...`));
147
+ try {
148
+ await git.remove(wt.path, options.force);
149
+ console.log(chalk.green(`✓ Removed worktree: ${branchName}`));
150
+ // Ask about branch deletion (only for non-protected branches)
151
+ if (!isProtectedBranch(branchName)) {
152
+ const answer = await promptUser(chalk.yellow(`\nAlso delete branch "${branchName}"? Type "yes" to confirm (or press Enter to skip): `));
153
+ if (answer === 'yes') {
154
+ console.log(chalk.yellow(`Deleting branch: ${branchName}...`));
155
+ try {
156
+ await git.deleteBranch(branchName, true);
157
+ console.log(chalk.green(`✓ Deleted branch: ${branchName}`));
158
+ }
159
+ catch (branchError) {
160
+ console.error(chalk.yellow(`⚠ Branch deletion failed: ${branchError}`));
161
+ }
162
+ }
163
+ else {
164
+ console.log(chalk.gray(`Branch "${branchName}" kept.`));
165
+ }
166
+ }
167
+ }
168
+ catch (error) {
169
+ console.error(chalk.red(`Error: ${error}`));
170
+ process.exit(1);
171
+ }
172
+ });
173
+ // Helper to find worktree by branch or path
174
+ async function findWorktree(branchOrPath, repoPath) {
175
+ const resolvedPath = path.resolve(repoPath || '.');
176
+ const repoRoot = GitWorktree.getRepoRoot(resolvedPath);
177
+ if (!repoRoot) {
178
+ console.error(chalk.red('Not a git repository'));
179
+ process.exit(1);
180
+ }
181
+ const git = new GitWorktree(repoRoot);
182
+ const worktrees = await git.list();
183
+ const wt = worktrees.find(w => w.branch === branchOrPath ||
184
+ w.path === branchOrPath ||
185
+ w.path.endsWith(branchOrPath) ||
186
+ w.name === branchOrPath);
187
+ if (!wt) {
188
+ console.error(chalk.red(`Worktree not found: ${branchOrPath}`));
189
+ process.exit(1);
190
+ }
191
+ return wt;
192
+ }
193
+ // Subcommand: open worktree in editor
194
+ program
195
+ .command('open <branch-or-path>')
196
+ .description('Open a worktree in an editor')
197
+ .argument('[repo-path]', 'Path to git repository', '.')
198
+ .option('-e, --editor <editor>', `Editor to use (${validEditors.join(', ')})`, 'code')
199
+ .action(async (branchOrPath, repoPath, options) => {
200
+ const editor = options.editor;
201
+ if (!validEditors.includes(editor)) {
202
+ console.error(chalk.red(`Invalid editor: ${editor}`));
203
+ console.error(chalk.gray(`Valid options: ${validEditors.join(', ')}`));
204
+ process.exit(1);
205
+ }
206
+ const wt = await findWorktree(branchOrPath, repoPath);
207
+ console.log(chalk.cyan(`Opening ${wt.branch} in ${editor}...`));
208
+ openEditor(wt.path, editor);
209
+ });
210
+ // Subcommand: open terminal in worktree
211
+ program
212
+ .command('terminal <branch-or-path>')
213
+ .alias('term')
214
+ .description('Open a terminal in a worktree')
215
+ .argument('[repo-path]', 'Path to git repository', '.')
216
+ .action(async (branchOrPath, repoPath) => {
217
+ const wt = await findWorktree(branchOrPath, repoPath);
218
+ console.log(chalk.cyan(`Opening terminal in ${wt.branch}...`));
219
+ openTerminal(wt.path);
220
+ });
221
+ // Subcommand: launch AI tool in worktree
222
+ program
223
+ .command('ai <branch-or-path>')
224
+ .description('Launch an AI tool in a worktree')
225
+ .argument('[repo-path]', 'Path to git repository', '.')
226
+ .option('-t, --tool <tool>', `AI tool to use (${validAITools.join(', ')})`, 'claude')
227
+ .action(async (branchOrPath, repoPath, options) => {
228
+ const tool = options.tool;
229
+ if (!validAITools.includes(tool)) {
230
+ console.error(chalk.red(`Invalid AI tool: ${tool}`));
231
+ console.error(chalk.gray(`Valid options: ${validAITools.join(', ')}`));
232
+ process.exit(1);
233
+ }
234
+ const wt = await findWorktree(branchOrPath, repoPath);
235
+ console.log(chalk.cyan(`Launching ${tool} in ${wt.branch}...`));
236
+ launchAI(wt.path, tool);
237
+ });
238
+ // Subcommand: help with examples
239
+ program
240
+ .command('help')
241
+ .description('Show detailed help and examples')
242
+ .action(() => {
243
+ console.log(`
244
+ ${chalk.cyan.bold('Worktree Manager')} - Terminal app for managing git worktrees
245
+
246
+ ${chalk.yellow.bold('QUICK START')}
247
+ ${chalk.gray('Launch TUI in current repo:')}
248
+ $ ${chalk.green('wtm')}
249
+
250
+ ${chalk.gray('Launch TUI for specific repo:')}
251
+ $ ${chalk.green('wtm /path/to/repo')}
252
+
253
+ ${chalk.yellow.bold('CLI COMMANDS')}
254
+
255
+ ${chalk.cyan('wtm list')} ${chalk.gray('[path]')}
256
+ List all worktrees
257
+ $ wtm list
258
+ $ wtm list /path/to/repo
259
+
260
+ ${chalk.cyan('wtm create')} ${chalk.white('<branch>')} ${chalk.gray('[path] [-b base] [-p path]')}
261
+ Create a new worktree
262
+ $ wtm create feature/login
263
+ $ wtm create feature/api -b develop
264
+ $ wtm create bugfix/issue-42 -p /custom/path
265
+
266
+ ${chalk.cyan('wtm remove')} ${chalk.white('<branch>')} ${chalk.gray('[path] [-f]')}
267
+ Remove a worktree
268
+ $ wtm remove feature/login
269
+ $ wtm remove feature/old --force
270
+
271
+ ${chalk.cyan('wtm open')} ${chalk.white('<branch>')} ${chalk.gray('[path] [-e editor]')}
272
+ Open worktree in editor
273
+ $ wtm open feature/login
274
+ $ wtm open main -e cursor
275
+ ${chalk.gray('Editors: code, cursor, zed, webstorm, subl, nvim')}
276
+
277
+ ${chalk.cyan('wtm terminal')} ${chalk.white('<branch>')} ${chalk.gray('[path]')}
278
+ Open terminal in worktree
279
+ $ wtm terminal feature/login
280
+ $ wtm term main
281
+
282
+ ${chalk.cyan('wtm ai')} ${chalk.white('<branch>')} ${chalk.gray('[path] [-t tool]')}
283
+ Launch AI tool in worktree
284
+ $ wtm ai feature/login
285
+ $ wtm ai main -t gemini
286
+ ${chalk.gray('Tools: claude, gemini, codex')}
287
+
288
+ ${chalk.cyan('wtm update')}
289
+ Update to the latest version
290
+ $ wtm update
291
+
292
+ ${chalk.yellow.bold('TUI KEYBINDINGS')}
293
+ ${chalk.cyan('↑/k, ↓/j')} Navigate worktrees
294
+ ${chalk.cyan('Enter')} Show details
295
+ ${chalk.cyan('n')} Create new worktree
296
+ ${chalk.cyan('d')} Delete worktree
297
+ ${chalk.cyan('e')} Open in editor
298
+ ${chalk.cyan('t')} Open terminal
299
+ ${chalk.cyan('a')} Launch AI tool
300
+ ${chalk.cyan('r')} Refresh list
301
+ ${chalk.cyan('?')} Show help
302
+ ${chalk.cyan('q')} Quit
303
+ `);
304
+ });
305
+ // Subcommand: update to latest version
306
+ program
307
+ .command('update')
308
+ .description('Update to the latest version')
309
+ .action(() => {
310
+ console.log(chalk.cyan(`\nCurrent version: ${VERSION}`));
311
+ console.log(chalk.gray('Checking for updates...\n'));
312
+ try {
313
+ // Check npm for latest version
314
+ const latestVersion = execSync(`npm view ${PACKAGE_NAME} version`, { encoding: 'utf-8' }).trim();
315
+ if (latestVersion === VERSION) {
316
+ console.log(chalk.green(`✓ Already on the latest version (${VERSION})`));
317
+ return;
318
+ }
319
+ console.log(chalk.yellow(`New version available: ${latestVersion}`));
320
+ console.log(chalk.gray(`Updating ${PACKAGE_NAME}...\n`));
321
+ // Run the update
322
+ execSync(`npm install -g ${PACKAGE_NAME}@latest`, { stdio: 'inherit' });
323
+ console.log(chalk.green(`\n✓ Successfully updated to ${latestVersion}`));
324
+ }
325
+ catch (error) {
326
+ if (error instanceof Error && error.message.includes('npm view')) {
327
+ console.error(chalk.red('Error: Could not check for updates. Package may not be published yet.'));
328
+ }
329
+ else {
330
+ console.error(chalk.red(`Error updating: ${error instanceof Error ? error.message : error}`));
331
+ console.log(chalk.gray(`\nTry manually: npm install -g ${PACKAGE_NAME}@latest`));
332
+ }
333
+ process.exit(1);
334
+ }
335
+ });
336
+ program.parse();