worktree-launcher 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.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # worktree-launcher (wt)
2
+
3
+ CLI tool to streamline git worktrees with AI coding assistants (Claude Code / Codex).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g worktree-launcher
9
+ ```
10
+
11
+ Or for local development:
12
+
13
+ ```bash
14
+ git clone <repo>
15
+ cd worktree-launcher
16
+ npm install
17
+ npm run build
18
+ npm link
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ ### `wt new <branch-name>`
24
+
25
+ Create a new worktree and launch your AI assistant.
26
+
27
+ ```bash
28
+ wt new feature-auth
29
+ ```
30
+
31
+ This will:
32
+ 1. Create a new branch (if it doesn't exist)
33
+ 2. Create a worktree at `../<repo-name>-<branch-name>/`
34
+ 3. Copy all `.env*` files from the main repo
35
+ 4. Show an interactive selector to choose Claude Code or Codex
36
+ 5. Launch the selected AI assistant in the new worktree
37
+
38
+ **Options:**
39
+ - `-i, --install` - Run package manager install after creating worktree
40
+ - `-s, --skip-launch` - Create worktree without launching AI assistant
41
+
42
+ **Examples:**
43
+ ```bash
44
+ wt new feature-auth # Create and launch AI
45
+ wt new feature-auth --install # Also run npm/yarn/pnpm install
46
+ wt new feature-auth --skip-launch # Just create the worktree
47
+ ```
48
+
49
+ ### `wt list` (or `wt ls`)
50
+
51
+ List all worktrees for the current repository.
52
+
53
+ ```bash
54
+ wt list
55
+ ```
56
+
57
+ Shows:
58
+ - Worktree paths
59
+ - Branch names
60
+ - Status (active, merged, local only, detached)
61
+
62
+ ### `wt clean`
63
+
64
+ Interactively remove worktrees for merged or deleted branches.
65
+
66
+ ```bash
67
+ wt clean
68
+ ```
69
+
70
+ This will:
71
+ 1. Find worktrees where the branch was merged or deleted
72
+ 2. Show an interactive checkbox to select which to remove
73
+ 3. Remove selected worktrees
74
+
75
+ ### `wt remove <name>` (or `wt rm`)
76
+
77
+ Remove a specific worktree.
78
+
79
+ ```bash
80
+ wt remove feature-auth
81
+ wt rm feature-auth --force
82
+ ```
83
+
84
+ **Options:**
85
+ - `-f, --force` - Force removal even with uncommitted changes
86
+
87
+ ## Worktree Location
88
+
89
+ Worktrees are created as siblings to your main repository:
90
+
91
+ ```
92
+ /Users/you/code/
93
+ ├── myproject/ # Main repo
94
+ ├── myproject-feature-auth/ # Worktree for feature-auth branch
95
+ └── myproject-fix-bug/ # Worktree for fix-bug branch
96
+ ```
97
+
98
+ ## Env Files
99
+
100
+ The following env files are automatically copied to new worktrees:
101
+ - `.env`
102
+ - `.env.local`
103
+ - `.env.development`
104
+ - `.env.development.local`
105
+ - `.env.test`
106
+ - `.env.test.local`
107
+ - `.env.production`
108
+ - `.env.production.local`
109
+
110
+ Template files (`.env.example`, `.env.sample`) are not copied.
111
+
112
+ ## Requirements
113
+
114
+ - Node.js 18+
115
+ - Git
116
+ - Claude Code or Codex CLI installed and in PATH
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,607 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/new.ts
7
+ import chalk from "chalk";
8
+ import ora from "ora";
9
+ import path4 from "path";
10
+
11
+ // src/utils/git.ts
12
+ import { execFile } from "child_process";
13
+ import { promisify } from "util";
14
+ import path from "path";
15
+ var execFileAsync = promisify(execFile);
16
+ async function isGitRepo() {
17
+ try {
18
+ await execFileAsync("git", ["rev-parse", "--git-dir"]);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+ async function getGitRoot() {
25
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
26
+ return stdout.trim();
27
+ }
28
+ async function branchExists(branchName) {
29
+ try {
30
+ await execFileAsync("git", ["rev-parse", "--verify", branchName]);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ async function remoteBranchExists(branchName) {
37
+ try {
38
+ const { stdout } = await execFileAsync("git", ["branch", "-r"]);
39
+ const remoteBranches = stdout.split("\n").map((b) => b.trim());
40
+ return remoteBranches.some(
41
+ (b) => b === `origin/${branchName}` || b.endsWith(`/${branchName}`)
42
+ );
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ async function getDefaultBranch() {
48
+ try {
49
+ const { stdout } = await execFileAsync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
50
+ return stdout.trim().replace("refs/remotes/origin/", "");
51
+ } catch {
52
+ if (await branchExists("main")) return "main";
53
+ if (await branchExists("master")) return "master";
54
+ return "main";
55
+ }
56
+ }
57
+ async function createWorktree(worktreePath, branchName) {
58
+ validateBranchName(branchName);
59
+ const exists = await branchExists(branchName);
60
+ if (exists) {
61
+ await execFileAsync("git", ["worktree", "add", "--", worktreePath, branchName]);
62
+ } else {
63
+ await execFileAsync("git", ["worktree", "add", "-b", branchName, "--", worktreePath]);
64
+ }
65
+ }
66
+ async function listWorktrees() {
67
+ const { stdout } = await execFileAsync("git", ["worktree", "list", "--porcelain"]);
68
+ const worktrees = [];
69
+ let current = {};
70
+ for (const line of stdout.split("\n")) {
71
+ if (line.startsWith("worktree ")) {
72
+ if (current.path) {
73
+ worktrees.push(current);
74
+ }
75
+ current = {
76
+ path: line.substring(9),
77
+ bare: false,
78
+ detached: false
79
+ };
80
+ } else if (line.startsWith("HEAD ")) {
81
+ current.head = line.substring(5);
82
+ } else if (line.startsWith("branch ")) {
83
+ current.branch = line.substring(7).replace("refs/heads/", "");
84
+ } else if (line === "bare") {
85
+ current.bare = true;
86
+ } else if (line === "detached") {
87
+ current.detached = true;
88
+ }
89
+ }
90
+ if (current.path) {
91
+ worktrees.push(current);
92
+ }
93
+ return worktrees;
94
+ }
95
+ async function removeWorktree(worktreePath, force = false) {
96
+ const args = ["worktree", "remove"];
97
+ if (force) args.push("--force");
98
+ args.push(worktreePath);
99
+ await execFileAsync("git", args);
100
+ }
101
+ async function pruneWorktrees() {
102
+ await execFileAsync("git", ["worktree", "prune"]);
103
+ }
104
+ async function isBranchMerged(branchName) {
105
+ try {
106
+ const defaultBranch = await getDefaultBranch();
107
+ const { stdout } = await execFileAsync("git", ["branch", "--merged", defaultBranch]);
108
+ const mergedBranches = stdout.split("\n").map((b) => b.trim().replace("* ", ""));
109
+ return mergedBranches.includes(branchName);
110
+ } catch {
111
+ return false;
112
+ }
113
+ }
114
+ function validateBranchName(branchName) {
115
+ if (!branchName || branchName.trim() === "") {
116
+ throw new Error("Branch name cannot be empty");
117
+ }
118
+ if (branchName.startsWith("-")) {
119
+ throw new Error("Branch name cannot start with -");
120
+ }
121
+ if (branchName.includes("..")) {
122
+ throw new Error("Branch name cannot contain ..");
123
+ }
124
+ if (branchName.length > 250) {
125
+ throw new Error("Branch name too long (max 250 characters)");
126
+ }
127
+ }
128
+ function getWorktreePath(mainRepoPath, branchName) {
129
+ validateBranchName(branchName);
130
+ const repoName = path.basename(mainRepoPath);
131
+ const safeBranchName = branchName.replace(/\//g, "-");
132
+ return path.join(path.dirname(mainRepoPath), `${repoName}-${safeBranchName}`);
133
+ }
134
+ async function findWorktree(identifier) {
135
+ const worktrees = await listWorktrees();
136
+ return worktrees.find(
137
+ (wt) => wt.branch === identifier || wt.path === identifier || path.basename(wt.path) === identifier || wt.path.endsWith(identifier)
138
+ );
139
+ }
140
+
141
+ // src/utils/env.ts
142
+ import { glob } from "glob";
143
+ import { copyFile } from "fs/promises";
144
+ import path2 from "path";
145
+ async function findEnvFiles(sourceDir) {
146
+ const files = await glob(".env*", {
147
+ cwd: sourceDir,
148
+ dot: true,
149
+ nodir: true
150
+ });
151
+ return files.filter((file) => {
152
+ if (file !== ".env" && !file.startsWith(".env.")) return false;
153
+ if (file.endsWith(".example") || file.endsWith(".sample") || file.endsWith(".template")) return false;
154
+ return true;
155
+ });
156
+ }
157
+ async function copyEnvFiles(sourceDir, destDir) {
158
+ const envFiles = await findEnvFiles(sourceDir);
159
+ const copied = [];
160
+ for (const file of envFiles) {
161
+ try {
162
+ await copyFile(path2.join(sourceDir, file), path2.join(destDir, file));
163
+ copied.push(file);
164
+ } catch (error) {
165
+ console.warn(`Warning: Could not copy ${file}: ${error}`);
166
+ }
167
+ }
168
+ return copied;
169
+ }
170
+
171
+ // src/utils/launcher.ts
172
+ import { spawn } from "child_process";
173
+ import { access } from "fs/promises";
174
+ import path3 from "path";
175
+ import { constants } from "fs";
176
+ function launchAITool(options) {
177
+ const { cwd, tool } = options;
178
+ const child = spawn(tool, [], {
179
+ cwd,
180
+ stdio: "inherit",
181
+ // Detach so the CLI can exit
182
+ detached: true
183
+ });
184
+ child.unref();
185
+ }
186
+ async function isToolAvailable(tool) {
187
+ return new Promise((resolve) => {
188
+ const child = spawn("which", [tool]);
189
+ child.on("close", (code) => {
190
+ resolve(code === 0);
191
+ });
192
+ child.on("error", () => {
193
+ resolve(false);
194
+ });
195
+ });
196
+ }
197
+ async function detectPackageManager(dir) {
198
+ const lockfiles = [
199
+ { file: "bun.lockb", manager: "bun" },
200
+ { file: "pnpm-lock.yaml", manager: "pnpm" },
201
+ { file: "yarn.lock", manager: "yarn" },
202
+ { file: "package-lock.json", manager: "npm" }
203
+ ];
204
+ for (const { file, manager } of lockfiles) {
205
+ try {
206
+ await access(path3.join(dir, file), constants.R_OK);
207
+ return manager;
208
+ } catch {
209
+ }
210
+ }
211
+ try {
212
+ await access(path3.join(dir, "package.json"), constants.R_OK);
213
+ return "npm";
214
+ } catch {
215
+ return null;
216
+ }
217
+ }
218
+ function runInstall(dir, packageManager) {
219
+ return new Promise((resolve, reject) => {
220
+ const child = spawn(packageManager, ["install"], {
221
+ cwd: dir,
222
+ stdio: "inherit"
223
+ });
224
+ child.on("close", (code) => {
225
+ if (code === 0) {
226
+ resolve();
227
+ } else {
228
+ reject(new Error(`${packageManager} install failed with code ${code}`));
229
+ }
230
+ });
231
+ child.on("error", reject);
232
+ });
233
+ }
234
+
235
+ // src/ui/selector.ts
236
+ import inquirer from "inquirer";
237
+ var AI_TOOLS = [
238
+ {
239
+ name: "Claude Code",
240
+ value: "claude",
241
+ description: "Anthropic's Claude coding assistant"
242
+ },
243
+ {
244
+ name: "Codex",
245
+ value: "codex",
246
+ description: "OpenAI's Codex coding assistant"
247
+ }
248
+ ];
249
+ async function selectAITool() {
250
+ const { tool } = await inquirer.prompt([
251
+ {
252
+ type: "list",
253
+ name: "tool",
254
+ message: "Select AI coding assistant:",
255
+ choices: AI_TOOLS.map((t) => ({
256
+ name: `${t.name} - ${t.description}`,
257
+ value: t.value,
258
+ short: t.name
259
+ }))
260
+ }
261
+ ]);
262
+ return tool;
263
+ }
264
+ async function confirm(message, defaultValue = true) {
265
+ const { confirmed } = await inquirer.prompt([
266
+ {
267
+ type: "confirm",
268
+ name: "confirmed",
269
+ message,
270
+ default: defaultValue
271
+ }
272
+ ]);
273
+ return confirmed;
274
+ }
275
+ async function selectMultiple(message, choices) {
276
+ const { selected } = await inquirer.prompt([
277
+ {
278
+ type: "checkbox",
279
+ name: "selected",
280
+ message,
281
+ choices
282
+ }
283
+ ]);
284
+ return selected;
285
+ }
286
+
287
+ // src/commands/new.ts
288
+ async function newCommand(branchName, options) {
289
+ if (!await isGitRepo()) {
290
+ console.error(chalk.red("Error: Not a git repository"));
291
+ process.exit(1);
292
+ }
293
+ const mainRepoPath = await getGitRoot();
294
+ const repoName = path4.basename(mainRepoPath);
295
+ const worktreePath = getWorktreePath(mainRepoPath, branchName);
296
+ console.log(chalk.cyan(`
297
+ Creating worktree for branch: ${chalk.bold(branchName)}`));
298
+ console.log(chalk.dim(`Repository: ${repoName}`));
299
+ console.log(chalk.dim(`Worktree path: ${worktreePath}
300
+ `));
301
+ const spinner = ora("Creating worktree...").start();
302
+ try {
303
+ await createWorktree(worktreePath, branchName);
304
+ spinner.succeed(chalk.green("Worktree created successfully"));
305
+ } catch (error) {
306
+ spinner.fail(chalk.red("Failed to create worktree"));
307
+ console.error(chalk.red(error.message || error));
308
+ process.exit(1);
309
+ }
310
+ const envSpinner = ora("Copying .env files...").start();
311
+ try {
312
+ const copiedFiles = await copyEnvFiles(mainRepoPath, worktreePath);
313
+ if (copiedFiles.length > 0) {
314
+ envSpinner.succeed(chalk.green(`Copied ${copiedFiles.length} env file(s): ${copiedFiles.join(", ")}`));
315
+ } else {
316
+ envSpinner.info(chalk.yellow("No .env files found to copy"));
317
+ }
318
+ } catch (error) {
319
+ envSpinner.warn(chalk.yellow(`Warning: Could not copy env files: ${error.message}`));
320
+ }
321
+ if (options.install) {
322
+ const packageManager = await detectPackageManager(worktreePath);
323
+ if (packageManager) {
324
+ const installSpinner = ora(`Running ${packageManager} install...`).start();
325
+ try {
326
+ await runInstall(worktreePath, packageManager);
327
+ installSpinner.succeed(chalk.green(`${packageManager} install completed`));
328
+ } catch (error) {
329
+ installSpinner.fail(chalk.red(`${packageManager} install failed: ${error.message}`));
330
+ }
331
+ }
332
+ } else {
333
+ const packageManager = await detectPackageManager(worktreePath);
334
+ if (packageManager) {
335
+ console.log(chalk.dim(`
336
+ Tip: Run '${packageManager} install' in the worktree, or use 'wt new --install' next time`));
337
+ }
338
+ }
339
+ if (options.skipLaunch) {
340
+ console.log(chalk.green(`
341
+ \u2713 Worktree ready at: ${worktreePath}`));
342
+ console.log(chalk.dim(` cd "${worktreePath}"`));
343
+ return;
344
+ }
345
+ console.log("");
346
+ const selectedTool = await selectAITool();
347
+ const toolAvailable = await isToolAvailable(selectedTool);
348
+ if (!toolAvailable) {
349
+ console.error(chalk.red(`
350
+ Error: ${selectedTool} is not installed or not in PATH`));
351
+ console.log(chalk.dim(`Worktree is ready at: ${worktreePath}`));
352
+ console.log(chalk.dim(`You can manually launch your AI tool there.`));
353
+ process.exit(1);
354
+ }
355
+ console.log(chalk.cyan(`
356
+ Launching ${selectedTool} in worktree...`));
357
+ launchAITool({
358
+ cwd: worktreePath,
359
+ tool: selectedTool
360
+ });
361
+ console.log(chalk.green(`
362
+ \u2713 ${selectedTool} launched in: ${worktreePath}`));
363
+ }
364
+
365
+ // src/commands/list.ts
366
+ import chalk2 from "chalk";
367
+ import path5 from "path";
368
+ async function listCommand() {
369
+ if (!await isGitRepo()) {
370
+ console.error(chalk2.red("Error: Not a git repository"));
371
+ process.exit(1);
372
+ }
373
+ const mainRepoPath = await getGitRoot();
374
+ const worktrees = await listWorktrees();
375
+ if (worktrees.length === 0) {
376
+ console.log(chalk2.yellow("No worktrees found"));
377
+ return;
378
+ }
379
+ console.log(chalk2.cyan(`
380
+ Worktrees for: ${chalk2.bold(path5.basename(mainRepoPath))}
381
+ `));
382
+ console.log(
383
+ chalk2.dim("\u2500".repeat(100))
384
+ );
385
+ console.log(
386
+ chalk2.bold(padEnd("Path", 50)) + chalk2.bold(padEnd("Branch", 25)) + chalk2.bold("Status")
387
+ );
388
+ console.log(
389
+ chalk2.dim("\u2500".repeat(100))
390
+ );
391
+ for (const wt of worktrees) {
392
+ const isMain = wt.path === mainRepoPath;
393
+ const status = await getWorktreeStatus(wt.branch, wt.detached, isMain);
394
+ const displayPath = shortenPath(wt.path, 48);
395
+ const displayBranch = wt.detached ? chalk2.yellow("(detached)") : wt.branch || "N/A";
396
+ console.log(
397
+ padEnd(isMain ? chalk2.bold(displayPath) : displayPath, 50) + padEnd(displayBranch, 25) + status
398
+ );
399
+ }
400
+ console.log(chalk2.dim("\u2500".repeat(100)));
401
+ console.log(chalk2.dim(`
402
+ Total: ${worktrees.length} worktree(s)`));
403
+ }
404
+ async function getWorktreeStatus(branch, detached, isMain) {
405
+ if (isMain) {
406
+ return chalk2.blue("main");
407
+ }
408
+ if (detached) {
409
+ return chalk2.yellow("detached");
410
+ }
411
+ if (!branch) {
412
+ return chalk2.dim("unknown");
413
+ }
414
+ const existsOnRemote = await remoteBranchExists(branch);
415
+ const isMerged = await isBranchMerged(branch);
416
+ if (isMerged) {
417
+ return chalk2.green("merged") + chalk2.dim(" (can clean)");
418
+ }
419
+ if (!existsOnRemote) {
420
+ return chalk2.yellow("local only");
421
+ }
422
+ return chalk2.green("active");
423
+ }
424
+ function padEnd(str, length) {
425
+ const visibleLength = str.replace(/\x1B\[[0-9;]*m/g, "").length;
426
+ const padding = Math.max(0, length - visibleLength);
427
+ return str + " ".repeat(padding);
428
+ }
429
+ function shortenPath(p, maxLength) {
430
+ if (p.length <= maxLength) return p;
431
+ const parts = p.split(path5.sep);
432
+ let result = parts[parts.length - 1];
433
+ for (let i = parts.length - 2; i >= 0; i--) {
434
+ const newResult = path5.join(parts[i], result);
435
+ if (newResult.length > maxLength - 3) {
436
+ return "..." + path5.sep + result;
437
+ }
438
+ result = newResult;
439
+ }
440
+ return result;
441
+ }
442
+
443
+ // src/commands/clean.ts
444
+ import chalk3 from "chalk";
445
+ import ora2 from "ora";
446
+ import path6 from "path";
447
+ async function cleanCommand() {
448
+ if (!await isGitRepo()) {
449
+ console.error(chalk3.red("Error: Not a git repository"));
450
+ process.exit(1);
451
+ }
452
+ const mainRepoPath = await getGitRoot();
453
+ const pruneSpinner = ora2("Pruning stale references...").start();
454
+ await pruneWorktrees();
455
+ pruneSpinner.succeed("Pruned stale references");
456
+ const worktrees = await listWorktrees();
457
+ const spinner = ora2("Checking worktree status...").start();
458
+ const staleWorktrees = [];
459
+ for (const wt of worktrees) {
460
+ if (wt.path === mainRepoPath) continue;
461
+ if (wt.detached || wt.bare || !wt.branch) continue;
462
+ const merged = await isBranchMerged(wt.branch);
463
+ if (merged) {
464
+ staleWorktrees.push({ ...wt, reason: "merged" });
465
+ continue;
466
+ }
467
+ const existsOnRemote = await remoteBranchExists(wt.branch);
468
+ if (!existsOnRemote) {
469
+ staleWorktrees.push({ ...wt, reason: "local-only" });
470
+ }
471
+ }
472
+ spinner.stop();
473
+ if (staleWorktrees.length === 0) {
474
+ console.log(chalk3.green("\n\u2713 No stale worktrees found"));
475
+ return;
476
+ }
477
+ console.log(chalk3.yellow(`
478
+ Found ${staleWorktrees.length} potentially stale worktree(s):
479
+ `));
480
+ const choices = staleWorktrees.map((wt) => {
481
+ const reasonText = wt.reason === "merged" ? chalk3.green("merged") : chalk3.yellow("local only");
482
+ return {
483
+ name: `${path6.basename(wt.path)} (${wt.branch}) - ${reasonText}`,
484
+ value: wt,
485
+ checked: wt.reason === "merged"
486
+ // Pre-select merged branches
487
+ };
488
+ });
489
+ const selected = await selectMultiple(
490
+ "Select worktrees to remove:",
491
+ choices
492
+ );
493
+ if (selected.length === 0) {
494
+ console.log(chalk3.yellow("\nNo worktrees selected for removal"));
495
+ return;
496
+ }
497
+ const confirmed = await confirm(
498
+ `Remove ${selected.length} worktree(s)?`,
499
+ true
500
+ );
501
+ if (!confirmed) {
502
+ console.log(chalk3.yellow("Cancelled"));
503
+ return;
504
+ }
505
+ console.log("");
506
+ let removed = 0;
507
+ let failed = 0;
508
+ for (const wt of selected) {
509
+ const removeSpinner = ora2(`Removing ${path6.basename(wt.path)}...`).start();
510
+ try {
511
+ await removeWorktree(wt.path, false);
512
+ removeSpinner.succeed(chalk3.green(`Removed ${path6.basename(wt.path)}`));
513
+ removed++;
514
+ } catch (error) {
515
+ try {
516
+ await removeWorktree(wt.path, true);
517
+ removeSpinner.succeed(chalk3.green(`Removed ${path6.basename(wt.path)} (forced)`));
518
+ removed++;
519
+ } catch (forceError) {
520
+ removeSpinner.fail(chalk3.red(`Failed to remove ${path6.basename(wt.path)}: ${forceError.message}`));
521
+ failed++;
522
+ }
523
+ }
524
+ }
525
+ console.log("");
526
+ if (removed > 0) {
527
+ console.log(chalk3.green(`\u2713 Removed ${removed} worktree(s)`));
528
+ }
529
+ if (failed > 0) {
530
+ console.log(chalk3.red(`\u2717 Failed to remove ${failed} worktree(s)`));
531
+ }
532
+ }
533
+
534
+ // src/commands/remove.ts
535
+ import chalk4 from "chalk";
536
+ import ora3 from "ora";
537
+ import path7 from "path";
538
+ async function removeCommand(identifier, options) {
539
+ if (!await isGitRepo()) {
540
+ console.error(chalk4.red("Error: Not a git repository"));
541
+ process.exit(1);
542
+ }
543
+ const mainRepoPath = await getGitRoot();
544
+ const spinner = ora3("Finding worktree...").start();
545
+ const worktree = await findWorktree(identifier);
546
+ if (!worktree) {
547
+ spinner.fail(chalk4.red(`Worktree not found: ${identifier}`));
548
+ console.log(chalk4.dim('\nTip: Run "wt list" to see available worktrees'));
549
+ process.exit(1);
550
+ }
551
+ spinner.stop();
552
+ if (worktree.path === mainRepoPath) {
553
+ console.error(chalk4.red("\nError: Cannot remove the main worktree"));
554
+ process.exit(1);
555
+ }
556
+ console.log(chalk4.cyan("\nWorktree to remove:"));
557
+ console.log(chalk4.dim(` Path: ${worktree.path}`));
558
+ console.log(chalk4.dim(` Branch: ${worktree.branch || "(detached)"}`));
559
+ if (!options.force) {
560
+ const confirmed = await confirm("\nRemove this worktree?", false);
561
+ if (!confirmed) {
562
+ console.log(chalk4.yellow("Cancelled"));
563
+ return;
564
+ }
565
+ }
566
+ const removeSpinner = ora3("Removing worktree...").start();
567
+ try {
568
+ await removeWorktree(worktree.path, false);
569
+ removeSpinner.succeed(chalk4.green(`Removed worktree: ${path7.basename(worktree.path)}`));
570
+ } catch (error) {
571
+ if (options.force) {
572
+ try {
573
+ await removeWorktree(worktree.path, true);
574
+ removeSpinner.succeed(chalk4.green(`Removed worktree (forced): ${path7.basename(worktree.path)}`));
575
+ } catch (forceError) {
576
+ removeSpinner.fail(chalk4.red(`Failed to remove worktree: ${forceError.message}`));
577
+ process.exit(1);
578
+ }
579
+ } else {
580
+ removeSpinner.fail(chalk4.red(`Failed to remove worktree: ${error.message}`));
581
+ console.log(chalk4.dim("\nTip: Use --force to force removal"));
582
+ process.exit(1);
583
+ }
584
+ }
585
+ }
586
+
587
+ // src/index.ts
588
+ var program = new Command();
589
+ program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.0.0");
590
+ program.command("new <branch-name>").description("Create a new worktree and launch AI assistant").option("-i, --install", "Run package manager install after creating worktree").option("-s, --skip-launch", "Create worktree without launching AI assistant").action(async (branchName, options) => {
591
+ await newCommand(branchName, {
592
+ install: options.install,
593
+ skipLaunch: options.skipLaunch
594
+ });
595
+ });
596
+ program.command("list").alias("ls").description("List all worktrees for the current repository").action(async () => {
597
+ await listCommand();
598
+ });
599
+ program.command("clean").description("Remove worktrees for merged or deleted branches").action(async () => {
600
+ await cleanCommand();
601
+ });
602
+ program.command("remove <name>").alias("rm").description("Remove a specific worktree").option("-f, --force", "Force removal even if there are uncommitted changes").action(async (name, options) => {
603
+ await removeCommand(name, {
604
+ force: options.force
605
+ });
606
+ });
607
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "worktree-launcher",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool to streamline git worktrees with AI coding assistants",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "wt": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsup src/index.ts --format esm --dts --clean",
12
+ "dev": "tsup src/index.ts --format esm --watch",
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "eslint src/",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "git",
19
+ "worktree",
20
+ "cli",
21
+ "claude",
22
+ "codex",
23
+ "ai"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "chalk": "^5.3.0",
29
+ "commander": "^12.1.0",
30
+ "glob": "^10.4.5",
31
+ "inquirer": "^9.3.7",
32
+ "ora": "^8.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/inquirer": "^9.0.7",
36
+ "@types/node": "^20.17.9",
37
+ "tsup": "^8.3.5",
38
+ "typescript": "^5.7.2"
39
+ },
40
+ "engines": {
41
+ "node": ">=18.0.0"
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ]
46
+ }