worktree-launcher 1.2.1 → 1.3.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/dist/index.js CHANGED
@@ -1,82 +1,436 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- confirm,
4
- findWorktree,
5
- getGitRoot,
6
- isBranchMerged,
7
- isGitRepo,
8
- isToolAvailable,
9
- launchAITool,
10
- listWorktrees,
11
- newCommand,
12
- pruneWorktrees,
13
- remoteBranchExists,
14
- removeWorktree,
15
- selectAITool,
16
- selectMultiple
17
- } from "./chunk-KGMGW33P.js";
18
2
 
19
3
  // src/index.ts
20
4
  import { Command } from "commander";
21
5
 
22
- // src/commands/list.ts
6
+ // src/commands/new.ts
23
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";
24
14
  import path from "path";
25
- async function listCommand() {
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 worktrees2 = [];
69
+ let current = {};
70
+ for (const line of stdout.split("\n")) {
71
+ if (line.startsWith("worktree ")) {
72
+ if (current.path) {
73
+ worktrees2.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
+ worktrees2.push(current);
92
+ }
93
+ return worktrees2;
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(mainRepoPath2, branchName) {
129
+ validateBranchName(branchName);
130
+ const repoName = path.basename(mainRepoPath2);
131
+ const safeBranchName = branchName.replace(/\//g, "-");
132
+ return path.join(path.dirname(mainRepoPath2), `${repoName}-${safeBranchName}`);
133
+ }
134
+ async function findWorktree(identifier) {
135
+ const worktrees2 = await listWorktrees();
136
+ return worktrees2.find(
137
+ (wt) => wt.branch === identifier || wt.path === identifier || path.basename(wt.path) === identifier || wt.path.endsWith(identifier)
138
+ );
139
+ }
140
+ async function pushBranch(branchName, cwd) {
141
+ const args = ["push", "-u", "origin", branchName];
142
+ await execFileAsync("git", args, cwd ? { cwd } : void 0);
143
+ }
144
+
145
+ // src/utils/env.ts
146
+ import { glob } from "glob";
147
+ import { copyFile } from "fs/promises";
148
+ import path2 from "path";
149
+ async function findEnvFiles(sourceDir) {
150
+ const files = await glob(".env*", {
151
+ cwd: sourceDir,
152
+ dot: true,
153
+ nodir: true
154
+ });
155
+ return files.filter((file) => {
156
+ if (file !== ".env" && !file.startsWith(".env.")) return false;
157
+ if (file.endsWith(".example") || file.endsWith(".sample") || file.endsWith(".template")) return false;
158
+ return true;
159
+ });
160
+ }
161
+ async function copyEnvFiles(sourceDir, destDir) {
162
+ const envFiles = await findEnvFiles(sourceDir);
163
+ const copied = [];
164
+ for (const file of envFiles) {
165
+ try {
166
+ await copyFile(path2.join(sourceDir, file), path2.join(destDir, file));
167
+ copied.push(file);
168
+ } catch (error) {
169
+ console.warn(`Warning: Could not copy ${file}: ${error}`);
170
+ }
171
+ }
172
+ return copied;
173
+ }
174
+
175
+ // src/utils/launcher.ts
176
+ import { spawn } from "child_process";
177
+ import { access } from "fs/promises";
178
+ import path3 from "path";
179
+ import { constants } from "fs";
180
+ function launchAITool(options) {
181
+ const { cwd, tool } = options;
182
+ spawn(tool, [], {
183
+ cwd,
184
+ stdio: "inherit",
185
+ shell: true
186
+ });
187
+ }
188
+ async function isToolAvailable(tool) {
189
+ return new Promise((resolve) => {
190
+ const child = spawn("which", [tool]);
191
+ child.on("close", (code) => {
192
+ resolve(code === 0);
193
+ });
194
+ child.on("error", () => {
195
+ resolve(false);
196
+ });
197
+ });
198
+ }
199
+ async function detectPackageManager(dir) {
200
+ const lockfiles = [
201
+ { file: "bun.lockb", manager: "bun" },
202
+ { file: "pnpm-lock.yaml", manager: "pnpm" },
203
+ { file: "yarn.lock", manager: "yarn" },
204
+ { file: "package-lock.json", manager: "npm" }
205
+ ];
206
+ for (const { file, manager } of lockfiles) {
207
+ try {
208
+ await access(path3.join(dir, file), constants.R_OK);
209
+ return manager;
210
+ } catch {
211
+ }
212
+ }
213
+ try {
214
+ await access(path3.join(dir, "package.json"), constants.R_OK);
215
+ return "npm";
216
+ } catch {
217
+ return null;
218
+ }
219
+ }
220
+ function runInstall(dir, packageManager) {
221
+ return new Promise((resolve, reject) => {
222
+ const child = spawn(packageManager, ["install"], {
223
+ cwd: dir,
224
+ stdio: "inherit"
225
+ });
226
+ child.on("close", (code) => {
227
+ if (code === 0) {
228
+ resolve();
229
+ } else {
230
+ reject(new Error(`${packageManager} install failed with code ${code}`));
231
+ }
232
+ });
233
+ child.on("error", reject);
234
+ });
235
+ }
236
+
237
+ // src/ui/selector.ts
238
+ import inquirer from "inquirer";
239
+ var AI_TOOLS = [
240
+ {
241
+ name: "Claude Code",
242
+ value: "claude",
243
+ description: "Anthropic's Claude coding assistant"
244
+ },
245
+ {
246
+ name: "Codex",
247
+ value: "codex",
248
+ description: "OpenAI's Codex coding assistant"
249
+ }
250
+ ];
251
+ async function selectAITool() {
252
+ const { tool } = await inquirer.prompt([
253
+ {
254
+ type: "list",
255
+ name: "tool",
256
+ message: "Select AI coding assistant:",
257
+ choices: AI_TOOLS.map((t) => ({
258
+ name: `${t.name} - ${t.description}`,
259
+ value: t.value,
260
+ short: t.name
261
+ }))
262
+ }
263
+ ]);
264
+ return tool;
265
+ }
266
+ async function confirm(message, defaultValue = true) {
267
+ const { confirmed } = await inquirer.prompt([
268
+ {
269
+ type: "confirm",
270
+ name: "confirmed",
271
+ message,
272
+ default: defaultValue
273
+ }
274
+ ]);
275
+ return confirmed;
276
+ }
277
+ async function selectMultiple(message, choices) {
278
+ const { selected } = await inquirer.prompt([
279
+ {
280
+ type: "checkbox",
281
+ name: "selected",
282
+ message,
283
+ choices
284
+ }
285
+ ]);
286
+ return selected;
287
+ }
288
+
289
+ // src/commands/new.ts
290
+ async function newCommand(branchName, options) {
26
291
  if (!await isGitRepo()) {
27
292
  console.error(chalk.red("Error: Not a git repository"));
28
293
  process.exit(1);
29
294
  }
30
- const mainRepoPath = await getGitRoot();
31
- const worktrees = await listWorktrees();
32
- if (worktrees.length === 0) {
33
- console.log(chalk.yellow("No worktrees found"));
295
+ const mainRepoPath2 = await getGitRoot();
296
+ const repoName = path4.basename(mainRepoPath2);
297
+ const worktreePath = getWorktreePath(mainRepoPath2, branchName);
298
+ console.log(chalk.cyan(`
299
+ Creating worktree for branch: ${chalk.bold(branchName)}`));
300
+ console.log(chalk.dim(`Repository: ${repoName}`));
301
+ console.log(chalk.dim(`Worktree path: ${worktreePath}
302
+ `));
303
+ const spinner = ora("Creating worktree...").start();
304
+ try {
305
+ await createWorktree(worktreePath, branchName);
306
+ spinner.succeed(chalk.green("Worktree created successfully"));
307
+ } catch (error) {
308
+ spinner.fail(chalk.red("Failed to create worktree"));
309
+ console.error(chalk.red(error.message || error));
310
+ process.exit(1);
311
+ }
312
+ if (options.push) {
313
+ const pushSpinner = ora("Pushing branch to remote...").start();
314
+ try {
315
+ await pushBranch(branchName, worktreePath);
316
+ pushSpinner.succeed(chalk.green(`Pushed ${branchName} to origin`));
317
+ } catch (error) {
318
+ pushSpinner.fail(chalk.yellow(`Could not push: ${error.message}`));
319
+ }
320
+ }
321
+ const envSpinner = ora("Copying .env files...").start();
322
+ try {
323
+ const copiedFiles = await copyEnvFiles(mainRepoPath2, worktreePath);
324
+ if (copiedFiles.length > 0) {
325
+ envSpinner.succeed(chalk.green(`Copied ${copiedFiles.length} env file(s): ${copiedFiles.join(", ")}`));
326
+ } else {
327
+ envSpinner.info(chalk.yellow("No .env files found to copy"));
328
+ }
329
+ } catch (error) {
330
+ envSpinner.warn(chalk.yellow(`Warning: Could not copy env files: ${error.message}`));
331
+ }
332
+ if (options.install) {
333
+ const packageManager = await detectPackageManager(worktreePath);
334
+ if (packageManager) {
335
+ const installSpinner = ora(`Running ${packageManager} install...`).start();
336
+ try {
337
+ await runInstall(worktreePath, packageManager);
338
+ installSpinner.succeed(chalk.green(`${packageManager} install completed`));
339
+ } catch (error) {
340
+ installSpinner.fail(chalk.red(`${packageManager} install failed: ${error.message}`));
341
+ }
342
+ }
343
+ } else {
344
+ const packageManager = await detectPackageManager(worktreePath);
345
+ if (packageManager) {
346
+ console.log(chalk.dim(`
347
+ Tip: Run '${packageManager} install' in the worktree, or use 'wt new --install' next time`));
348
+ }
349
+ }
350
+ if (options.skipLaunch) {
351
+ console.log(chalk.green(`
352
+ \u2713 Worktree ready at: ${worktreePath}`));
353
+ console.log(chalk.dim(` cd "${worktreePath}"`));
34
354
  return;
35
355
  }
356
+ console.log("");
357
+ const selectedTool = await selectAITool();
358
+ const toolAvailable = await isToolAvailable(selectedTool);
359
+ if (!toolAvailable) {
360
+ console.error(chalk.red(`
361
+ Error: ${selectedTool} is not installed or not in PATH`));
362
+ console.log(chalk.dim(`Worktree is ready at: ${worktreePath}`));
363
+ console.log(chalk.dim(`You can manually launch your AI tool there.`));
364
+ process.exit(1);
365
+ }
36
366
  console.log(chalk.cyan(`
37
- Worktrees for: ${chalk.bold(path.basename(mainRepoPath))}
367
+ Launching ${selectedTool} in worktree...`));
368
+ launchAITool({
369
+ cwd: worktreePath,
370
+ tool: selectedTool
371
+ });
372
+ console.log(chalk.green(`
373
+ \u2713 ${selectedTool} launched in: ${worktreePath}`));
374
+ }
375
+
376
+ // src/commands/list.ts
377
+ import chalk2 from "chalk";
378
+ import path5 from "path";
379
+ async function listCommand() {
380
+ if (!await isGitRepo()) {
381
+ console.error(chalk2.red("Error: Not a git repository"));
382
+ process.exit(1);
383
+ }
384
+ const mainRepoPath2 = await getGitRoot();
385
+ const worktrees2 = await listWorktrees();
386
+ if (worktrees2.length === 0) {
387
+ console.log(chalk2.yellow("No worktrees found"));
388
+ return;
389
+ }
390
+ console.log(chalk2.cyan(`
391
+ Worktrees for: ${chalk2.bold(path5.basename(mainRepoPath2))}
38
392
  `));
39
393
  console.log(
40
- chalk.dim("\u2500".repeat(100))
394
+ chalk2.dim("\u2500".repeat(100))
41
395
  );
42
396
  console.log(
43
- chalk.bold(padEnd("Path", 50)) + chalk.bold(padEnd("Branch", 25)) + chalk.bold("Status")
397
+ chalk2.bold(padEnd("Path", 50)) + chalk2.bold(padEnd("Branch", 25)) + chalk2.bold("Status")
44
398
  );
45
399
  console.log(
46
- chalk.dim("\u2500".repeat(100))
400
+ chalk2.dim("\u2500".repeat(100))
47
401
  );
48
- for (const wt of worktrees) {
49
- const isMain = wt.path === mainRepoPath;
402
+ for (const wt of worktrees2) {
403
+ const isMain = wt.path === mainRepoPath2;
50
404
  const status = await getWorktreeStatus(wt.branch, wt.detached, isMain);
51
405
  const displayPath = shortenPath(wt.path, 48);
52
- const displayBranch = wt.detached ? chalk.yellow("(detached)") : wt.branch || "N/A";
406
+ const displayBranch = wt.detached ? chalk2.yellow("(detached)") : wt.branch || "N/A";
53
407
  console.log(
54
- padEnd(isMain ? chalk.bold(displayPath) : displayPath, 50) + padEnd(displayBranch, 25) + status
408
+ padEnd(isMain ? chalk2.bold(displayPath) : displayPath, 50) + padEnd(displayBranch, 25) + status
55
409
  );
56
410
  }
57
- console.log(chalk.dim("\u2500".repeat(100)));
58
- console.log(chalk.dim(`
59
- Total: ${worktrees.length} worktree(s)`));
411
+ console.log(chalk2.dim("\u2500".repeat(100)));
412
+ console.log(chalk2.dim(`
413
+ Total: ${worktrees2.length} worktree(s)`));
60
414
  }
61
415
  async function getWorktreeStatus(branch, detached, isMain) {
62
416
  if (isMain) {
63
- return chalk.blue("main");
417
+ return chalk2.blue("main");
64
418
  }
65
419
  if (detached) {
66
- return chalk.yellow("detached");
420
+ return chalk2.yellow("detached");
67
421
  }
68
422
  if (!branch) {
69
- return chalk.dim("unknown");
423
+ return chalk2.dim("unknown");
70
424
  }
71
425
  const existsOnRemote = await remoteBranchExists(branch);
72
426
  const isMerged = await isBranchMerged(branch);
73
427
  if (isMerged) {
74
- return chalk.green("merged") + chalk.dim(" (can clean)");
428
+ return chalk2.green("merged") + chalk2.dim(" (can clean)");
75
429
  }
76
430
  if (!existsOnRemote) {
77
- return chalk.yellow("local only");
431
+ return chalk2.yellow("local only");
78
432
  }
79
- return chalk.green("active");
433
+ return chalk2.green("active");
80
434
  }
81
435
  function padEnd(str, length) {
82
436
  const visibleLength = str.replace(/\x1B\[[0-9;]*m/g, "").length;
@@ -85,12 +439,12 @@ function padEnd(str, length) {
85
439
  }
86
440
  function shortenPath(p, maxLength) {
87
441
  if (p.length <= maxLength) return p;
88
- const parts = p.split(path.sep);
442
+ const parts = p.split(path5.sep);
89
443
  let result = parts[parts.length - 1];
90
444
  for (let i = parts.length - 2; i >= 0; i--) {
91
- const newResult = path.join(parts[i], result);
445
+ const newResult = path5.join(parts[i], result);
92
446
  if (newResult.length > maxLength - 3) {
93
- return "..." + path.sep + result;
447
+ return "..." + path5.sep + result;
94
448
  }
95
449
  result = newResult;
96
450
  }
@@ -98,23 +452,23 @@ function shortenPath(p, maxLength) {
98
452
  }
99
453
 
100
454
  // src/commands/clean.ts
101
- import chalk2 from "chalk";
102
- import ora from "ora";
103
- import path2 from "path";
455
+ import chalk3 from "chalk";
456
+ import ora2 from "ora";
457
+ import path6 from "path";
104
458
  async function cleanCommand() {
105
459
  if (!await isGitRepo()) {
106
- console.error(chalk2.red("Error: Not a git repository"));
460
+ console.error(chalk3.red("Error: Not a git repository"));
107
461
  process.exit(1);
108
462
  }
109
- const mainRepoPath = await getGitRoot();
110
- const pruneSpinner = ora("Pruning stale references...").start();
463
+ const mainRepoPath2 = await getGitRoot();
464
+ const pruneSpinner = ora2("Pruning stale references...").start();
111
465
  await pruneWorktrees();
112
466
  pruneSpinner.succeed("Pruned stale references");
113
- const worktrees = await listWorktrees();
114
- const spinner = ora("Checking worktree status...").start();
467
+ const worktrees2 = await listWorktrees();
468
+ const spinner = ora2("Checking worktree status...").start();
115
469
  const staleWorktrees = [];
116
- for (const wt of worktrees) {
117
- if (wt.path === mainRepoPath) continue;
470
+ for (const wt of worktrees2) {
471
+ if (wt.path === mainRepoPath2) continue;
118
472
  if (wt.detached || wt.bare || !wt.branch) continue;
119
473
  const merged = await isBranchMerged(wt.branch);
120
474
  if (merged) {
@@ -128,16 +482,16 @@ async function cleanCommand() {
128
482
  }
129
483
  spinner.stop();
130
484
  if (staleWorktrees.length === 0) {
131
- console.log(chalk2.green("\n\u2713 No stale worktrees found"));
485
+ console.log(chalk3.green("\n\u2713 No stale worktrees found"));
132
486
  return;
133
487
  }
134
- console.log(chalk2.yellow(`
488
+ console.log(chalk3.yellow(`
135
489
  Found ${staleWorktrees.length} potentially stale worktree(s):
136
490
  `));
137
491
  const choices = staleWorktrees.map((wt) => {
138
- const reasonText = wt.reason === "merged" ? chalk2.green("merged") : chalk2.yellow("local only");
492
+ const reasonText = wt.reason === "merged" ? chalk3.green("merged") : chalk3.yellow("local only");
139
493
  return {
140
- name: `${path2.basename(wt.path)} (${wt.branch}) - ${reasonText}`,
494
+ name: `${path6.basename(wt.path)} (${wt.branch}) - ${reasonText}`,
141
495
  value: wt,
142
496
  checked: wt.reason === "merged"
143
497
  // Pre-select merged branches
@@ -148,7 +502,7 @@ Found ${staleWorktrees.length} potentially stale worktree(s):
148
502
  choices
149
503
  );
150
504
  if (selected.length === 0) {
151
- console.log(chalk2.yellow("\nNo worktrees selected for removal"));
505
+ console.log(chalk3.yellow("\nNo worktrees selected for removal"));
152
506
  return;
153
507
  }
154
508
  const confirmed = await confirm(
@@ -156,275 +510,360 @@ Found ${staleWorktrees.length} potentially stale worktree(s):
156
510
  true
157
511
  );
158
512
  if (!confirmed) {
159
- console.log(chalk2.yellow("Cancelled"));
513
+ console.log(chalk3.yellow("Cancelled"));
160
514
  return;
161
515
  }
162
516
  console.log("");
163
517
  let removed = 0;
164
518
  let failed = 0;
165
519
  for (const wt of selected) {
166
- const removeSpinner = ora(`Removing ${path2.basename(wt.path)}...`).start();
520
+ const removeSpinner = ora2(`Removing ${path6.basename(wt.path)}...`).start();
167
521
  try {
168
522
  await removeWorktree(wt.path, false);
169
- removeSpinner.succeed(chalk2.green(`Removed ${path2.basename(wt.path)}`));
523
+ removeSpinner.succeed(chalk3.green(`Removed ${path6.basename(wt.path)}`));
170
524
  removed++;
171
525
  } catch (error) {
172
526
  try {
173
527
  await removeWorktree(wt.path, true);
174
- removeSpinner.succeed(chalk2.green(`Removed ${path2.basename(wt.path)} (forced)`));
528
+ removeSpinner.succeed(chalk3.green(`Removed ${path6.basename(wt.path)} (forced)`));
175
529
  removed++;
176
530
  } catch (forceError) {
177
- removeSpinner.fail(chalk2.red(`Failed to remove ${path2.basename(wt.path)}: ${forceError.message}`));
531
+ removeSpinner.fail(chalk3.red(`Failed to remove ${path6.basename(wt.path)}: ${forceError.message}`));
178
532
  failed++;
179
533
  }
180
534
  }
181
535
  }
182
536
  console.log("");
183
537
  if (removed > 0) {
184
- console.log(chalk2.green(`\u2713 Removed ${removed} worktree(s)`));
538
+ console.log(chalk3.green(`\u2713 Removed ${removed} worktree(s)`));
185
539
  }
186
540
  if (failed > 0) {
187
- console.log(chalk2.red(`\u2717 Failed to remove ${failed} worktree(s)`));
541
+ console.log(chalk3.red(`\u2717 Failed to remove ${failed} worktree(s)`));
188
542
  }
189
543
  }
190
544
 
191
545
  // src/commands/remove.ts
192
- import chalk3 from "chalk";
193
- import ora2 from "ora";
194
- import path3 from "path";
546
+ import chalk4 from "chalk";
547
+ import ora3 from "ora";
548
+ import path7 from "path";
195
549
  async function removeCommand(identifier, options) {
196
550
  if (!await isGitRepo()) {
197
- console.error(chalk3.red("Error: Not a git repository"));
551
+ console.error(chalk4.red("Error: Not a git repository"));
198
552
  process.exit(1);
199
553
  }
200
- const mainRepoPath = await getGitRoot();
201
- const spinner = ora2("Finding worktree...").start();
554
+ const mainRepoPath2 = await getGitRoot();
555
+ const spinner = ora3("Finding worktree...").start();
202
556
  const worktree = await findWorktree(identifier);
203
557
  if (!worktree) {
204
- spinner.fail(chalk3.red(`Worktree not found: ${identifier}`));
205
- console.log(chalk3.dim('\nTip: Run "wt list" to see available worktrees'));
558
+ spinner.fail(chalk4.red(`Worktree not found: ${identifier}`));
559
+ console.log(chalk4.dim('\nTip: Run "wt list" to see available worktrees'));
206
560
  process.exit(1);
207
561
  }
208
562
  spinner.stop();
209
- if (worktree.path === mainRepoPath) {
210
- console.error(chalk3.red("\nError: Cannot remove the main worktree"));
563
+ if (worktree.path === mainRepoPath2) {
564
+ console.error(chalk4.red("\nError: Cannot remove the main worktree"));
211
565
  process.exit(1);
212
566
  }
213
- console.log(chalk3.cyan("\nWorktree to remove:"));
214
- console.log(chalk3.dim(` Path: ${worktree.path}`));
215
- console.log(chalk3.dim(` Branch: ${worktree.branch || "(detached)"}`));
567
+ console.log(chalk4.cyan("\nWorktree to remove:"));
568
+ console.log(chalk4.dim(` Path: ${worktree.path}`));
569
+ console.log(chalk4.dim(` Branch: ${worktree.branch || "(detached)"}`));
216
570
  if (!options.force) {
217
571
  const confirmed = await confirm("\nRemove this worktree?", false);
218
572
  if (!confirmed) {
219
- console.log(chalk3.yellow("Cancelled"));
573
+ console.log(chalk4.yellow("Cancelled"));
220
574
  return;
221
575
  }
222
576
  }
223
- const removeSpinner = ora2("Removing worktree...").start();
577
+ const removeSpinner = ora3("Removing worktree...").start();
224
578
  try {
225
579
  await removeWorktree(worktree.path, false);
226
- removeSpinner.succeed(chalk3.green(`Removed worktree: ${path3.basename(worktree.path)}`));
580
+ removeSpinner.succeed(chalk4.green(`Removed worktree: ${path7.basename(worktree.path)}`));
227
581
  } catch (error) {
228
582
  if (options.force) {
229
583
  try {
230
584
  await removeWorktree(worktree.path, true);
231
- removeSpinner.succeed(chalk3.green(`Removed worktree (forced): ${path3.basename(worktree.path)}`));
585
+ removeSpinner.succeed(chalk4.green(`Removed worktree (forced): ${path7.basename(worktree.path)}`));
232
586
  } catch (forceError) {
233
- removeSpinner.fail(chalk3.red(`Failed to remove worktree: ${forceError.message}`));
587
+ removeSpinner.fail(chalk4.red(`Failed to remove worktree: ${forceError.message}`));
234
588
  process.exit(1);
235
589
  }
236
590
  } else {
237
- removeSpinner.fail(chalk3.red(`Failed to remove worktree: ${error.message}`));
238
- console.log(chalk3.dim("\nTip: Use --force to force removal"));
591
+ removeSpinner.fail(chalk4.red(`Failed to remove worktree: ${error.message}`));
592
+ console.log(chalk4.dim("\nTip: Use --force to force removal"));
239
593
  process.exit(1);
240
594
  }
241
595
  }
242
596
  }
243
597
 
244
598
  // src/commands/interactive.ts
245
- import chalk4 from "chalk";
246
- import inquirer from "inquirer";
247
- import path4 from "path";
248
- import { execFile } from "child_process";
249
- import { promisify } from "util";
250
- var execFileAsync = promisify(execFile);
251
- async function getWorktreeStatus2(branch, isMain) {
252
- if (isMain) return chalk4.blue("main");
253
- if (!branch) return chalk4.dim("detached");
254
- const merged = await isBranchMerged(branch);
255
- if (merged) return chalk4.green("merged");
256
- const onRemote = await remoteBranchExists(branch);
257
- if (!onRemote) return chalk4.yellow("local");
258
- return chalk4.green("active");
259
- }
599
+ import blessed from "blessed";
600
+ import path8 from "path";
601
+ var screen;
602
+ var worktreeList;
603
+ var statusBar;
604
+ var helpBar;
605
+ var mainRepoPath;
606
+ var worktrees = [];
607
+ var selectedIndex = 0;
260
608
  async function interactiveCommand() {
261
609
  if (!await isGitRepo()) {
262
- console.error(chalk4.red("Error: Not a git repository"));
610
+ console.error("Error: Not a git repository");
263
611
  process.exit(1);
264
612
  }
265
- const mainRepoPath = await getGitRoot();
266
- const repoName = path4.basename(mainRepoPath);
267
- while (true) {
268
- console.clear();
269
- console.log(chalk4.cyan.bold(`
270
- Worktrees: ${repoName}
271
- `));
272
- const worktrees = await listWorktrees();
273
- const choices = [];
274
- for (const wt of worktrees) {
275
- const isMain = wt.path === mainRepoPath;
276
- const status = await getWorktreeStatus2(wt.branch, isMain);
277
- const branchDisplay = wt.branch || "(detached)";
278
- const dirName = path4.basename(wt.path);
279
- choices.push({
280
- name: ` ${dirName.padEnd(35)} ${branchDisplay.padEnd(25)} ${status}`,
281
- value: wt,
282
- short: dirName
283
- });
613
+ mainRepoPath = await getGitRoot();
614
+ const repoName = path8.basename(mainRepoPath);
615
+ screen = blessed.screen({
616
+ smartCSR: true,
617
+ title: `wt - ${repoName}`
618
+ });
619
+ blessed.box({
620
+ parent: screen,
621
+ top: 0,
622
+ left: 0,
623
+ width: "100%",
624
+ height: 1,
625
+ content: ` Worktrees: ${repoName}`,
626
+ style: {
627
+ fg: "white",
628
+ bg: "blue",
629
+ bold: true
284
630
  }
285
- choices.push({ name: chalk4.dim("\u2500".repeat(70)), value: "quit", short: "" });
286
- choices.push({ name: chalk4.green(" + Create new worktree"), value: "new", short: "new" });
287
- choices.push({ name: chalk4.dim(" q Quit"), value: "quit", short: "quit" });
288
- const { selected } = await inquirer.prompt([
289
- {
290
- type: "list",
291
- name: "selected",
292
- message: "Select a worktree:",
293
- choices,
294
- pageSize: 15
631
+ });
632
+ worktreeList = blessed.list({
633
+ parent: screen,
634
+ top: 1,
635
+ left: 0,
636
+ width: "100%",
637
+ height: "100%-3",
638
+ keys: true,
639
+ vi: true,
640
+ mouse: true,
641
+ style: {
642
+ selected: {
643
+ bg: "blue",
644
+ fg: "white"
645
+ },
646
+ item: {
647
+ fg: "white"
295
648
  }
296
- ]);
297
- if (selected === "quit") {
298
- console.log(chalk4.dim("\nGoodbye.\n"));
299
- break;
649
+ },
650
+ scrollbar: {
651
+ ch: " ",
652
+ style: { bg: "grey" }
300
653
  }
301
- if (selected === "new") {
302
- await handleNewWorktree(mainRepoPath);
303
- continue;
654
+ });
655
+ statusBar = blessed.box({
656
+ parent: screen,
657
+ bottom: 1,
658
+ left: 0,
659
+ width: "100%",
660
+ height: 1,
661
+ content: "",
662
+ style: {
663
+ fg: "yellow",
664
+ bg: "black"
665
+ }
666
+ });
667
+ helpBar = blessed.box({
668
+ parent: screen,
669
+ bottom: 0,
670
+ left: 0,
671
+ width: "100%",
672
+ height: 1,
673
+ content: " [n]ew [d]elete [c]laude [x]codex [p]ush [Enter]cd [q]uit",
674
+ style: {
675
+ fg: "black",
676
+ bg: "white"
304
677
  }
305
- await handleWorktreeActions(selected, mainRepoPath);
678
+ });
679
+ await refreshWorktrees();
680
+ worktreeList.on("select", (_item, index) => {
681
+ selectedIndex = index;
682
+ showPath();
683
+ });
684
+ screen.key(["q", "C-c"], () => {
685
+ screen.destroy();
686
+ process.exit(0);
687
+ });
688
+ screen.key(["n"], async () => {
689
+ await createNewWorktree();
690
+ });
691
+ screen.key(["d"], async () => {
692
+ await deleteSelected();
693
+ });
694
+ screen.key(["c"], async () => {
695
+ await launchAI("claude");
696
+ });
697
+ screen.key(["x"], async () => {
698
+ await launchAI("codex");
699
+ });
700
+ screen.key(["p"], async () => {
701
+ await pushSelected();
702
+ });
703
+ screen.key(["enter"], () => {
704
+ const wt = worktrees[selectedIndex];
705
+ if (wt) {
706
+ screen.destroy();
707
+ console.log(`
708
+ cd "${wt.path}"
709
+ `);
710
+ process.exit(0);
711
+ }
712
+ });
713
+ screen.key(["r"], async () => {
714
+ await refreshWorktrees();
715
+ });
716
+ worktreeList.focus();
717
+ screen.render();
718
+ }
719
+ async function refreshWorktrees() {
720
+ setStatus("Loading...");
721
+ worktrees = await listWorktrees();
722
+ const items = worktrees.map((wt) => {
723
+ const isMain = wt.path === mainRepoPath;
724
+ const dirName = path8.basename(wt.path);
725
+ const branch = wt.branch || "(detached)";
726
+ const status = isMain ? "[main]" : "";
727
+ return ` ${dirName.padEnd(40)} ${branch.padEnd(25)} ${status}`;
728
+ });
729
+ worktreeList.setItems(items);
730
+ worktreeList.select(selectedIndex);
731
+ showPath();
732
+ screen.render();
733
+ }
734
+ function showPath() {
735
+ const wt = worktrees[selectedIndex];
736
+ if (wt) {
737
+ setStatus(wt.path);
738
+ } else {
739
+ setStatus("");
306
740
  }
307
741
  }
308
- async function handleNewWorktree(mainRepoPath) {
309
- const { branchName } = await inquirer.prompt([
310
- {
311
- type: "input",
312
- name: "branchName",
313
- message: "Branch name:",
314
- validate: (input) => {
315
- if (!input.trim()) return "Branch name required";
316
- if (input.startsWith("-")) return "Cannot start with -";
317
- if (input.includes("..")) return "Cannot contain ..";
318
- return true;
319
- }
742
+ function setStatus(msg) {
743
+ statusBar.setContent(` ${msg}`);
744
+ screen.render();
745
+ }
746
+ async function createNewWorktree() {
747
+ const input = blessed.textbox({
748
+ parent: screen,
749
+ top: "center",
750
+ left: "center",
751
+ width: 50,
752
+ height: 3,
753
+ border: { type: "line" },
754
+ style: {
755
+ fg: "white",
756
+ bg: "black",
757
+ border: { fg: "blue" }
758
+ },
759
+ label: " Branch name ",
760
+ inputOnFocus: true
761
+ });
762
+ input.focus();
763
+ screen.render();
764
+ input.on("submit", async (value) => {
765
+ input.destroy();
766
+ if (!value || !value.trim()) {
767
+ await refreshWorktrees();
768
+ return;
320
769
  }
321
- ]);
322
- if (!branchName.trim()) return;
323
- const { newCommand: newCommand2 } = await import("./new-DHGI74HT.js");
324
- await newCommand2(branchName.trim(), { skipLaunch: false });
325
- }
326
- async function handleWorktreeActions(wt, mainRepoPath) {
327
- const isMain = wt.path === mainRepoPath;
328
- const dirName = path4.basename(wt.path);
329
- const actions = [
330
- { name: " Open in terminal (cd)", value: "cd" },
331
- { name: " Launch AI assistant", value: "launch" }
332
- ];
333
- if (!isMain) {
334
- actions.push({ name: chalk4.red(" Delete worktree"), value: "delete" });
770
+ const branchName = value.trim();
771
+ try {
772
+ validateBranchName(branchName);
773
+ } catch (e) {
774
+ setStatus(`Error: ${e.message}`);
775
+ return;
776
+ }
777
+ setStatus(`Creating ${branchName}...`);
778
+ try {
779
+ const worktreePath = getWorktreePath(mainRepoPath, branchName);
780
+ await createWorktree(worktreePath, branchName);
781
+ await copyEnvFiles(mainRepoPath, worktreePath);
782
+ setStatus(`Created ${branchName}`);
783
+ await refreshWorktrees();
784
+ } catch (e) {
785
+ setStatus(`Error: ${e.message}`);
786
+ }
787
+ });
788
+ input.on("cancel", () => {
789
+ input.destroy();
790
+ refreshWorktrees();
791
+ });
792
+ input.readInput();
793
+ }
794
+ async function deleteSelected() {
795
+ const wt = worktrees[selectedIndex];
796
+ if (!wt) return;
797
+ if (wt.path === mainRepoPath) {
798
+ setStatus("Cannot delete main worktree");
799
+ return;
335
800
  }
336
- actions.push({ name: chalk4.dim(" Back"), value: "back" });
337
- console.log(chalk4.cyan(`
338
- ${dirName}`));
339
- console.log(chalk4.dim(` ${wt.path}
340
- `));
341
- const { action } = await inquirer.prompt([
342
- {
343
- type: "list",
344
- name: "action",
345
- message: "Action:",
346
- choices: actions
801
+ const dirName = path8.basename(wt.path);
802
+ const confirm2 = blessed.question({
803
+ parent: screen,
804
+ top: "center",
805
+ left: "center",
806
+ width: 40,
807
+ height: 5,
808
+ border: { type: "line" },
809
+ style: {
810
+ fg: "white",
811
+ bg: "black",
812
+ border: { fg: "red" }
347
813
  }
348
- ]);
349
- switch (action) {
350
- case "cd":
351
- console.log(chalk4.green(`
352
- To open this worktree, run:`));
353
- console.log(chalk4.cyan(` cd "${wt.path}"
354
- `));
814
+ });
815
+ confirm2.ask(`Delete ${dirName}?`, async (err, yes) => {
816
+ confirm2.destroy();
817
+ if (yes) {
818
+ setStatus(`Deleting ${dirName}...`);
355
819
  try {
356
- await execFileAsync("pbcopy", [], { input: wt.path });
357
- console.log(chalk4.dim("(Path copied to clipboard)\n"));
820
+ await removeWorktree(wt.path, false);
821
+ setStatus(`Deleted ${dirName}`);
358
822
  } catch {
359
- }
360
- await pause();
361
- break;
362
- case "launch":
363
- const tool = await selectAITool();
364
- const available = await isToolAvailable(tool);
365
- if (!available) {
366
- console.log(chalk4.red(`
367
- ${tool} is not installed or not in PATH
368
- `));
369
- await pause();
370
- break;
371
- }
372
- console.log(chalk4.cyan(`
373
- Launching ${tool} in ${dirName}...`));
374
- launchAITool({ cwd: wt.path, tool });
375
- console.log(chalk4.green(`
376
- ${tool} launched.
377
- `));
378
- await pause();
379
- break;
380
- case "delete":
381
- const { confirm: confirm2 } = await inquirer.prompt([
382
- {
383
- type: "confirm",
384
- name: "confirm",
385
- message: `Delete ${dirName}?`,
386
- default: false
387
- }
388
- ]);
389
- if (confirm2) {
390
823
  try {
391
- await removeWorktree(wt.path, false);
392
- console.log(chalk4.green(`
393
- Deleted ${dirName}
394
- `));
395
- } catch {
396
- try {
397
- await removeWorktree(wt.path, true);
398
- console.log(chalk4.green(`
399
- Deleted ${dirName} (forced)
400
- `));
401
- } catch (e) {
402
- console.log(chalk4.red(`
403
- Failed to delete: ${e.message}
404
- `));
405
- }
824
+ await removeWorktree(wt.path, true);
825
+ setStatus(`Deleted ${dirName} (forced)`);
826
+ } catch (e) {
827
+ setStatus(`Error: ${e.message}`);
406
828
  }
407
- await pause();
408
829
  }
409
- break;
410
- case "back":
411
- default:
412
- break;
830
+ if (selectedIndex > 0) selectedIndex--;
831
+ await refreshWorktrees();
832
+ } else {
833
+ await refreshWorktrees();
834
+ }
835
+ });
836
+ }
837
+ async function launchAI(tool) {
838
+ const wt = worktrees[selectedIndex];
839
+ if (!wt) return;
840
+ const available = await isToolAvailable(tool);
841
+ if (!available) {
842
+ setStatus(`${tool} is not installed`);
843
+ return;
413
844
  }
845
+ setStatus(`Launching ${tool}...`);
846
+ launchAITool({ cwd: wt.path, tool });
847
+ setStatus(`${tool} launched in ${path8.basename(wt.path)}`);
414
848
  }
415
- async function pause() {
416
- await inquirer.prompt([
417
- {
418
- type: "input",
419
- name: "continue",
420
- message: "Press Enter to continue..."
421
- }
422
- ]);
849
+ async function pushSelected() {
850
+ const wt = worktrees[selectedIndex];
851
+ if (!wt || !wt.branch) {
852
+ setStatus("No branch to push");
853
+ return;
854
+ }
855
+ setStatus(`Pushing ${wt.branch}...`);
856
+ try {
857
+ await pushBranch(wt.branch, wt.path);
858
+ setStatus(`Pushed ${wt.branch} to origin`);
859
+ } catch (e) {
860
+ setStatus(`Error: ${e.message}`);
861
+ }
423
862
  }
424
863
 
425
864
  // src/index.ts
426
865
  var program = new Command();
427
- program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.2.1").action(async () => {
866
+ program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.3.0").action(async () => {
428
867
  await interactiveCommand();
429
868
  });
430
869
  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").option("-p, --push", "Push branch to remote (visible on GitHub)").action(async (branchName, options) => {