worktree-launcher 1.2.1 → 1.4.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 +24 -8
- package/dist/index.js +982 -243
- package/package.json +4 -1
- package/dist/chunk-KGMGW33P.js +0 -386
- package/dist/new-DHGI74HT.js +0 -6
package/dist/index.js
CHANGED
|
@@ -1,82 +1,444 @@
|
|
|
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/
|
|
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
|
-
|
|
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 getCurrentBranch() {
|
|
48
|
+
try {
|
|
49
|
+
const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
50
|
+
return stdout.trim();
|
|
51
|
+
} catch {
|
|
52
|
+
return "HEAD";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function getDefaultBranch() {
|
|
56
|
+
try {
|
|
57
|
+
const { stdout } = await execFileAsync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
58
|
+
return stdout.trim().replace("refs/remotes/origin/", "");
|
|
59
|
+
} catch {
|
|
60
|
+
if (await branchExists("main")) return "main";
|
|
61
|
+
if (await branchExists("master")) return "master";
|
|
62
|
+
return "main";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function createWorktree(worktreePath, branchName) {
|
|
66
|
+
validateBranchName(branchName);
|
|
67
|
+
const exists = await branchExists(branchName);
|
|
68
|
+
if (exists) {
|
|
69
|
+
await execFileAsync("git", ["worktree", "add", "--", worktreePath, branchName]);
|
|
70
|
+
} else {
|
|
71
|
+
await execFileAsync("git", ["worktree", "add", "-b", branchName, "--", worktreePath]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function listWorktrees() {
|
|
75
|
+
const { stdout } = await execFileAsync("git", ["worktree", "list", "--porcelain"]);
|
|
76
|
+
const worktrees2 = [];
|
|
77
|
+
let current = {};
|
|
78
|
+
for (const line of stdout.split("\n")) {
|
|
79
|
+
if (line.startsWith("worktree ")) {
|
|
80
|
+
if (current.path) {
|
|
81
|
+
worktrees2.push(current);
|
|
82
|
+
}
|
|
83
|
+
current = {
|
|
84
|
+
path: line.substring(9),
|
|
85
|
+
bare: false,
|
|
86
|
+
detached: false
|
|
87
|
+
};
|
|
88
|
+
} else if (line.startsWith("HEAD ")) {
|
|
89
|
+
current.head = line.substring(5);
|
|
90
|
+
} else if (line.startsWith("branch ")) {
|
|
91
|
+
current.branch = line.substring(7).replace("refs/heads/", "");
|
|
92
|
+
} else if (line === "bare") {
|
|
93
|
+
current.bare = true;
|
|
94
|
+
} else if (line === "detached") {
|
|
95
|
+
current.detached = true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (current.path) {
|
|
99
|
+
worktrees2.push(current);
|
|
100
|
+
}
|
|
101
|
+
return worktrees2;
|
|
102
|
+
}
|
|
103
|
+
async function removeWorktree(worktreePath, force = false) {
|
|
104
|
+
const args = ["worktree", "remove"];
|
|
105
|
+
if (force) args.push("--force");
|
|
106
|
+
args.push(worktreePath);
|
|
107
|
+
await execFileAsync("git", args);
|
|
108
|
+
}
|
|
109
|
+
async function pruneWorktrees() {
|
|
110
|
+
await execFileAsync("git", ["worktree", "prune"]);
|
|
111
|
+
}
|
|
112
|
+
async function isBranchMerged(branchName) {
|
|
113
|
+
try {
|
|
114
|
+
const defaultBranch2 = await getDefaultBranch();
|
|
115
|
+
const { stdout } = await execFileAsync("git", ["branch", "--merged", defaultBranch2]);
|
|
116
|
+
const mergedBranches = stdout.split("\n").map((b) => b.trim().replace("* ", ""));
|
|
117
|
+
return mergedBranches.includes(branchName);
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function validateBranchName(branchName) {
|
|
123
|
+
if (!branchName || branchName.trim() === "") {
|
|
124
|
+
throw new Error("Branch name cannot be empty");
|
|
125
|
+
}
|
|
126
|
+
if (branchName.startsWith("-")) {
|
|
127
|
+
throw new Error("Branch name cannot start with -");
|
|
128
|
+
}
|
|
129
|
+
if (branchName.includes("..")) {
|
|
130
|
+
throw new Error("Branch name cannot contain ..");
|
|
131
|
+
}
|
|
132
|
+
if (branchName.length > 250) {
|
|
133
|
+
throw new Error("Branch name too long (max 250 characters)");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function getWorktreePath(mainRepoPath2, branchName) {
|
|
137
|
+
validateBranchName(branchName);
|
|
138
|
+
const repoName = path.basename(mainRepoPath2);
|
|
139
|
+
const safeBranchName = branchName.replace(/\//g, "-");
|
|
140
|
+
return path.join(path.dirname(mainRepoPath2), `${repoName}-${safeBranchName}`);
|
|
141
|
+
}
|
|
142
|
+
async function findWorktree(identifier) {
|
|
143
|
+
const worktrees2 = await listWorktrees();
|
|
144
|
+
return worktrees2.find(
|
|
145
|
+
(wt) => wt.branch === identifier || wt.path === identifier || path.basename(wt.path) === identifier || wt.path.endsWith(identifier)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
async function pushBranch(branchName, cwd) {
|
|
149
|
+
const args = ["push", "-u", "origin", branchName];
|
|
150
|
+
await execFileAsync("git", args, cwd ? { cwd } : void 0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/utils/env.ts
|
|
154
|
+
import { glob } from "glob";
|
|
155
|
+
import { copyFile } from "fs/promises";
|
|
156
|
+
import path2 from "path";
|
|
157
|
+
async function findEnvFiles(sourceDir) {
|
|
158
|
+
const files = await glob(".env*", {
|
|
159
|
+
cwd: sourceDir,
|
|
160
|
+
dot: true,
|
|
161
|
+
nodir: true
|
|
162
|
+
});
|
|
163
|
+
return files.filter((file) => {
|
|
164
|
+
if (file !== ".env" && !file.startsWith(".env.")) return false;
|
|
165
|
+
if (file.endsWith(".example") || file.endsWith(".sample") || file.endsWith(".template")) return false;
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function copyEnvFiles(sourceDir, destDir) {
|
|
170
|
+
const envFiles = await findEnvFiles(sourceDir);
|
|
171
|
+
const copied = [];
|
|
172
|
+
for (const file of envFiles) {
|
|
173
|
+
try {
|
|
174
|
+
await copyFile(path2.join(sourceDir, file), path2.join(destDir, file));
|
|
175
|
+
copied.push(file);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn(`Warning: Could not copy ${file}: ${error}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return copied;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/utils/launcher.ts
|
|
184
|
+
import { spawn } from "child_process";
|
|
185
|
+
import { access } from "fs/promises";
|
|
186
|
+
import path3 from "path";
|
|
187
|
+
import { constants } from "fs";
|
|
188
|
+
function launchAITool(options) {
|
|
189
|
+
const { cwd, tool } = options;
|
|
190
|
+
spawn(tool, [], {
|
|
191
|
+
cwd,
|
|
192
|
+
stdio: "inherit",
|
|
193
|
+
shell: true
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function isToolAvailable(tool) {
|
|
197
|
+
return new Promise((resolve) => {
|
|
198
|
+
const child = spawn("which", [tool]);
|
|
199
|
+
child.on("close", (code) => {
|
|
200
|
+
resolve(code === 0);
|
|
201
|
+
});
|
|
202
|
+
child.on("error", () => {
|
|
203
|
+
resolve(false);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function detectPackageManager(dir) {
|
|
208
|
+
const lockfiles = [
|
|
209
|
+
{ file: "bun.lockb", manager: "bun" },
|
|
210
|
+
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
211
|
+
{ file: "yarn.lock", manager: "yarn" },
|
|
212
|
+
{ file: "package-lock.json", manager: "npm" }
|
|
213
|
+
];
|
|
214
|
+
for (const { file, manager } of lockfiles) {
|
|
215
|
+
try {
|
|
216
|
+
await access(path3.join(dir, file), constants.R_OK);
|
|
217
|
+
return manager;
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
await access(path3.join(dir, "package.json"), constants.R_OK);
|
|
223
|
+
return "npm";
|
|
224
|
+
} catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function runInstall(dir, packageManager) {
|
|
229
|
+
return new Promise((resolve, reject) => {
|
|
230
|
+
const child = spawn(packageManager, ["install"], {
|
|
231
|
+
cwd: dir,
|
|
232
|
+
stdio: "inherit"
|
|
233
|
+
});
|
|
234
|
+
child.on("close", (code) => {
|
|
235
|
+
if (code === 0) {
|
|
236
|
+
resolve();
|
|
237
|
+
} else {
|
|
238
|
+
reject(new Error(`${packageManager} install failed with code ${code}`));
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
child.on("error", reject);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/ui/selector.ts
|
|
246
|
+
import inquirer from "inquirer";
|
|
247
|
+
var AI_TOOLS = [
|
|
248
|
+
{
|
|
249
|
+
name: "Claude Code",
|
|
250
|
+
value: "claude",
|
|
251
|
+
description: "Anthropic's Claude coding assistant"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: "Codex",
|
|
255
|
+
value: "codex",
|
|
256
|
+
description: "OpenAI's Codex coding assistant"
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
async function selectAITool() {
|
|
260
|
+
const { tool } = await inquirer.prompt([
|
|
261
|
+
{
|
|
262
|
+
type: "list",
|
|
263
|
+
name: "tool",
|
|
264
|
+
message: "Select AI coding assistant:",
|
|
265
|
+
choices: AI_TOOLS.map((t) => ({
|
|
266
|
+
name: `${t.name} - ${t.description}`,
|
|
267
|
+
value: t.value,
|
|
268
|
+
short: t.name
|
|
269
|
+
}))
|
|
270
|
+
}
|
|
271
|
+
]);
|
|
272
|
+
return tool;
|
|
273
|
+
}
|
|
274
|
+
async function confirm(message, defaultValue = true) {
|
|
275
|
+
const { confirmed } = await inquirer.prompt([
|
|
276
|
+
{
|
|
277
|
+
type: "confirm",
|
|
278
|
+
name: "confirmed",
|
|
279
|
+
message,
|
|
280
|
+
default: defaultValue
|
|
281
|
+
}
|
|
282
|
+
]);
|
|
283
|
+
return confirmed;
|
|
284
|
+
}
|
|
285
|
+
async function selectMultiple(message, choices) {
|
|
286
|
+
const { selected } = await inquirer.prompt([
|
|
287
|
+
{
|
|
288
|
+
type: "checkbox",
|
|
289
|
+
name: "selected",
|
|
290
|
+
message,
|
|
291
|
+
choices
|
|
292
|
+
}
|
|
293
|
+
]);
|
|
294
|
+
return selected;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/commands/new.ts
|
|
298
|
+
async function newCommand(branchName, options) {
|
|
26
299
|
if (!await isGitRepo()) {
|
|
27
300
|
console.error(chalk.red("Error: Not a git repository"));
|
|
28
301
|
process.exit(1);
|
|
29
302
|
}
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
303
|
+
const mainRepoPath2 = await getGitRoot();
|
|
304
|
+
const repoName = path4.basename(mainRepoPath2);
|
|
305
|
+
const worktreePath = getWorktreePath(mainRepoPath2, branchName);
|
|
306
|
+
console.log(chalk.cyan(`
|
|
307
|
+
Creating worktree for branch: ${chalk.bold(branchName)}`));
|
|
308
|
+
console.log(chalk.dim(`Repository: ${repoName}`));
|
|
309
|
+
console.log(chalk.dim(`Worktree path: ${worktreePath}
|
|
310
|
+
`));
|
|
311
|
+
const spinner = ora("Creating worktree...").start();
|
|
312
|
+
try {
|
|
313
|
+
await createWorktree(worktreePath, branchName);
|
|
314
|
+
spinner.succeed(chalk.green("Worktree created successfully"));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
spinner.fail(chalk.red("Failed to create worktree"));
|
|
317
|
+
console.error(chalk.red(error.message || error));
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
if (options.push) {
|
|
321
|
+
const pushSpinner = ora("Pushing branch to remote...").start();
|
|
322
|
+
try {
|
|
323
|
+
await pushBranch(branchName, worktreePath);
|
|
324
|
+
pushSpinner.succeed(chalk.green(`Pushed ${branchName} to origin`));
|
|
325
|
+
} catch (error) {
|
|
326
|
+
pushSpinner.fail(chalk.yellow(`Could not push: ${error.message}`));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const envSpinner = ora("Copying .env files...").start();
|
|
330
|
+
try {
|
|
331
|
+
const copiedFiles = await copyEnvFiles(mainRepoPath2, worktreePath);
|
|
332
|
+
if (copiedFiles.length > 0) {
|
|
333
|
+
envSpinner.succeed(chalk.green(`Copied ${copiedFiles.length} env file(s): ${copiedFiles.join(", ")}`));
|
|
334
|
+
} else {
|
|
335
|
+
envSpinner.info(chalk.yellow("No .env files found to copy"));
|
|
336
|
+
}
|
|
337
|
+
} catch (error) {
|
|
338
|
+
envSpinner.warn(chalk.yellow(`Warning: Could not copy env files: ${error.message}`));
|
|
339
|
+
}
|
|
340
|
+
if (options.install) {
|
|
341
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
342
|
+
if (packageManager) {
|
|
343
|
+
const installSpinner = ora(`Running ${packageManager} install...`).start();
|
|
344
|
+
try {
|
|
345
|
+
await runInstall(worktreePath, packageManager);
|
|
346
|
+
installSpinner.succeed(chalk.green(`${packageManager} install completed`));
|
|
347
|
+
} catch (error) {
|
|
348
|
+
installSpinner.fail(chalk.red(`${packageManager} install failed: ${error.message}`));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} else {
|
|
352
|
+
const packageManager = await detectPackageManager(worktreePath);
|
|
353
|
+
if (packageManager) {
|
|
354
|
+
console.log(chalk.dim(`
|
|
355
|
+
Tip: Run '${packageManager} install' in the worktree, or use 'wt new --install' next time`));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (options.skipLaunch) {
|
|
359
|
+
console.log(chalk.green(`
|
|
360
|
+
\u2713 Worktree ready at: ${worktreePath}`));
|
|
361
|
+
console.log(chalk.dim(` cd "${worktreePath}"`));
|
|
34
362
|
return;
|
|
35
363
|
}
|
|
364
|
+
console.log("");
|
|
365
|
+
const selectedTool = await selectAITool();
|
|
366
|
+
const toolAvailable = await isToolAvailable(selectedTool);
|
|
367
|
+
if (!toolAvailable) {
|
|
368
|
+
console.error(chalk.red(`
|
|
369
|
+
Error: ${selectedTool} is not installed or not in PATH`));
|
|
370
|
+
console.log(chalk.dim(`Worktree is ready at: ${worktreePath}`));
|
|
371
|
+
console.log(chalk.dim(`You can manually launch your AI tool there.`));
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
36
374
|
console.log(chalk.cyan(`
|
|
37
|
-
|
|
375
|
+
Launching ${selectedTool} in worktree...`));
|
|
376
|
+
launchAITool({
|
|
377
|
+
cwd: worktreePath,
|
|
378
|
+
tool: selectedTool
|
|
379
|
+
});
|
|
380
|
+
console.log(chalk.green(`
|
|
381
|
+
\u2713 ${selectedTool} launched in: ${worktreePath}`));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/commands/list.ts
|
|
385
|
+
import chalk2 from "chalk";
|
|
386
|
+
import path5 from "path";
|
|
387
|
+
async function listCommand() {
|
|
388
|
+
if (!await isGitRepo()) {
|
|
389
|
+
console.error(chalk2.red("Error: Not a git repository"));
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
const mainRepoPath2 = await getGitRoot();
|
|
393
|
+
const worktrees2 = await listWorktrees();
|
|
394
|
+
if (worktrees2.length === 0) {
|
|
395
|
+
console.log(chalk2.yellow("No worktrees found"));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
console.log(chalk2.cyan(`
|
|
399
|
+
Worktrees for: ${chalk2.bold(path5.basename(mainRepoPath2))}
|
|
38
400
|
`));
|
|
39
401
|
console.log(
|
|
40
|
-
|
|
402
|
+
chalk2.dim("\u2500".repeat(100))
|
|
41
403
|
);
|
|
42
404
|
console.log(
|
|
43
|
-
|
|
405
|
+
chalk2.bold(padEnd("Path", 50)) + chalk2.bold(padEnd("Branch", 25)) + chalk2.bold("Status")
|
|
44
406
|
);
|
|
45
407
|
console.log(
|
|
46
|
-
|
|
408
|
+
chalk2.dim("\u2500".repeat(100))
|
|
47
409
|
);
|
|
48
|
-
for (const wt of
|
|
49
|
-
const isMain = wt.path ===
|
|
410
|
+
for (const wt of worktrees2) {
|
|
411
|
+
const isMain = wt.path === mainRepoPath2;
|
|
50
412
|
const status = await getWorktreeStatus(wt.branch, wt.detached, isMain);
|
|
51
413
|
const displayPath = shortenPath(wt.path, 48);
|
|
52
|
-
const displayBranch = wt.detached ?
|
|
414
|
+
const displayBranch = wt.detached ? chalk2.yellow("(detached)") : wt.branch || "N/A";
|
|
53
415
|
console.log(
|
|
54
|
-
padEnd(isMain ?
|
|
416
|
+
padEnd(isMain ? chalk2.bold(displayPath) : displayPath, 50) + padEnd(displayBranch, 25) + status
|
|
55
417
|
);
|
|
56
418
|
}
|
|
57
|
-
console.log(
|
|
58
|
-
console.log(
|
|
59
|
-
Total: ${
|
|
419
|
+
console.log(chalk2.dim("\u2500".repeat(100)));
|
|
420
|
+
console.log(chalk2.dim(`
|
|
421
|
+
Total: ${worktrees2.length} worktree(s)`));
|
|
60
422
|
}
|
|
61
423
|
async function getWorktreeStatus(branch, detached, isMain) {
|
|
62
424
|
if (isMain) {
|
|
63
|
-
return
|
|
425
|
+
return chalk2.blue("main");
|
|
64
426
|
}
|
|
65
427
|
if (detached) {
|
|
66
|
-
return
|
|
428
|
+
return chalk2.yellow("detached");
|
|
67
429
|
}
|
|
68
430
|
if (!branch) {
|
|
69
|
-
return
|
|
431
|
+
return chalk2.dim("unknown");
|
|
70
432
|
}
|
|
71
433
|
const existsOnRemote = await remoteBranchExists(branch);
|
|
72
434
|
const isMerged = await isBranchMerged(branch);
|
|
73
435
|
if (isMerged) {
|
|
74
|
-
return
|
|
436
|
+
return chalk2.green("merged") + chalk2.dim(" (can clean)");
|
|
75
437
|
}
|
|
76
438
|
if (!existsOnRemote) {
|
|
77
|
-
return
|
|
439
|
+
return chalk2.yellow("local only");
|
|
78
440
|
}
|
|
79
|
-
return
|
|
441
|
+
return chalk2.green("active");
|
|
80
442
|
}
|
|
81
443
|
function padEnd(str, length) {
|
|
82
444
|
const visibleLength = str.replace(/\x1B\[[0-9;]*m/g, "").length;
|
|
@@ -85,12 +447,12 @@ function padEnd(str, length) {
|
|
|
85
447
|
}
|
|
86
448
|
function shortenPath(p, maxLength) {
|
|
87
449
|
if (p.length <= maxLength) return p;
|
|
88
|
-
const parts = p.split(
|
|
450
|
+
const parts = p.split(path5.sep);
|
|
89
451
|
let result = parts[parts.length - 1];
|
|
90
452
|
for (let i = parts.length - 2; i >= 0; i--) {
|
|
91
|
-
const newResult =
|
|
453
|
+
const newResult = path5.join(parts[i], result);
|
|
92
454
|
if (newResult.length > maxLength - 3) {
|
|
93
|
-
return "..." +
|
|
455
|
+
return "..." + path5.sep + result;
|
|
94
456
|
}
|
|
95
457
|
result = newResult;
|
|
96
458
|
}
|
|
@@ -98,23 +460,23 @@ function shortenPath(p, maxLength) {
|
|
|
98
460
|
}
|
|
99
461
|
|
|
100
462
|
// src/commands/clean.ts
|
|
101
|
-
import
|
|
102
|
-
import
|
|
103
|
-
import
|
|
463
|
+
import chalk3 from "chalk";
|
|
464
|
+
import ora2 from "ora";
|
|
465
|
+
import path6 from "path";
|
|
104
466
|
async function cleanCommand() {
|
|
105
467
|
if (!await isGitRepo()) {
|
|
106
|
-
console.error(
|
|
468
|
+
console.error(chalk3.red("Error: Not a git repository"));
|
|
107
469
|
process.exit(1);
|
|
108
470
|
}
|
|
109
|
-
const
|
|
110
|
-
const pruneSpinner =
|
|
471
|
+
const mainRepoPath2 = await getGitRoot();
|
|
472
|
+
const pruneSpinner = ora2("Pruning stale references...").start();
|
|
111
473
|
await pruneWorktrees();
|
|
112
474
|
pruneSpinner.succeed("Pruned stale references");
|
|
113
|
-
const
|
|
114
|
-
const spinner =
|
|
475
|
+
const worktrees2 = await listWorktrees();
|
|
476
|
+
const spinner = ora2("Checking worktree status...").start();
|
|
115
477
|
const staleWorktrees = [];
|
|
116
|
-
for (const wt of
|
|
117
|
-
if (wt.path ===
|
|
478
|
+
for (const wt of worktrees2) {
|
|
479
|
+
if (wt.path === mainRepoPath2) continue;
|
|
118
480
|
if (wt.detached || wt.bare || !wt.branch) continue;
|
|
119
481
|
const merged = await isBranchMerged(wt.branch);
|
|
120
482
|
if (merged) {
|
|
@@ -128,16 +490,16 @@ async function cleanCommand() {
|
|
|
128
490
|
}
|
|
129
491
|
spinner.stop();
|
|
130
492
|
if (staleWorktrees.length === 0) {
|
|
131
|
-
console.log(
|
|
493
|
+
console.log(chalk3.green("\n\u2713 No stale worktrees found"));
|
|
132
494
|
return;
|
|
133
495
|
}
|
|
134
|
-
console.log(
|
|
496
|
+
console.log(chalk3.yellow(`
|
|
135
497
|
Found ${staleWorktrees.length} potentially stale worktree(s):
|
|
136
498
|
`));
|
|
137
499
|
const choices = staleWorktrees.map((wt) => {
|
|
138
|
-
const reasonText = wt.reason === "merged" ?
|
|
500
|
+
const reasonText = wt.reason === "merged" ? chalk3.green("merged") : chalk3.yellow("local only");
|
|
139
501
|
return {
|
|
140
|
-
name: `${
|
|
502
|
+
name: `${path6.basename(wt.path)} (${wt.branch}) - ${reasonText}`,
|
|
141
503
|
value: wt,
|
|
142
504
|
checked: wt.reason === "merged"
|
|
143
505
|
// Pre-select merged branches
|
|
@@ -148,7 +510,7 @@ Found ${staleWorktrees.length} potentially stale worktree(s):
|
|
|
148
510
|
choices
|
|
149
511
|
);
|
|
150
512
|
if (selected.length === 0) {
|
|
151
|
-
console.log(
|
|
513
|
+
console.log(chalk3.yellow("\nNo worktrees selected for removal"));
|
|
152
514
|
return;
|
|
153
515
|
}
|
|
154
516
|
const confirmed = await confirm(
|
|
@@ -156,275 +518,652 @@ Found ${staleWorktrees.length} potentially stale worktree(s):
|
|
|
156
518
|
true
|
|
157
519
|
);
|
|
158
520
|
if (!confirmed) {
|
|
159
|
-
console.log(
|
|
521
|
+
console.log(chalk3.yellow("Cancelled"));
|
|
160
522
|
return;
|
|
161
523
|
}
|
|
162
524
|
console.log("");
|
|
163
525
|
let removed = 0;
|
|
164
526
|
let failed = 0;
|
|
165
527
|
for (const wt of selected) {
|
|
166
|
-
const removeSpinner =
|
|
528
|
+
const removeSpinner = ora2(`Removing ${path6.basename(wt.path)}...`).start();
|
|
167
529
|
try {
|
|
168
530
|
await removeWorktree(wt.path, false);
|
|
169
|
-
removeSpinner.succeed(
|
|
531
|
+
removeSpinner.succeed(chalk3.green(`Removed ${path6.basename(wt.path)}`));
|
|
170
532
|
removed++;
|
|
171
533
|
} catch (error) {
|
|
172
534
|
try {
|
|
173
535
|
await removeWorktree(wt.path, true);
|
|
174
|
-
removeSpinner.succeed(
|
|
536
|
+
removeSpinner.succeed(chalk3.green(`Removed ${path6.basename(wt.path)} (forced)`));
|
|
175
537
|
removed++;
|
|
176
538
|
} catch (forceError) {
|
|
177
|
-
removeSpinner.fail(
|
|
539
|
+
removeSpinner.fail(chalk3.red(`Failed to remove ${path6.basename(wt.path)}: ${forceError.message}`));
|
|
178
540
|
failed++;
|
|
179
541
|
}
|
|
180
542
|
}
|
|
181
543
|
}
|
|
182
544
|
console.log("");
|
|
183
545
|
if (removed > 0) {
|
|
184
|
-
console.log(
|
|
546
|
+
console.log(chalk3.green(`\u2713 Removed ${removed} worktree(s)`));
|
|
185
547
|
}
|
|
186
548
|
if (failed > 0) {
|
|
187
|
-
console.log(
|
|
549
|
+
console.log(chalk3.red(`\u2717 Failed to remove ${failed} worktree(s)`));
|
|
188
550
|
}
|
|
189
551
|
}
|
|
190
552
|
|
|
191
553
|
// src/commands/remove.ts
|
|
192
|
-
import
|
|
193
|
-
import
|
|
194
|
-
import
|
|
554
|
+
import chalk4 from "chalk";
|
|
555
|
+
import ora3 from "ora";
|
|
556
|
+
import path7 from "path";
|
|
195
557
|
async function removeCommand(identifier, options) {
|
|
196
558
|
if (!await isGitRepo()) {
|
|
197
|
-
console.error(
|
|
559
|
+
console.error(chalk4.red("Error: Not a git repository"));
|
|
198
560
|
process.exit(1);
|
|
199
561
|
}
|
|
200
|
-
const
|
|
201
|
-
const spinner =
|
|
562
|
+
const mainRepoPath2 = await getGitRoot();
|
|
563
|
+
const spinner = ora3("Finding worktree...").start();
|
|
202
564
|
const worktree = await findWorktree(identifier);
|
|
203
565
|
if (!worktree) {
|
|
204
|
-
spinner.fail(
|
|
205
|
-
console.log(
|
|
566
|
+
spinner.fail(chalk4.red(`Worktree not found: ${identifier}`));
|
|
567
|
+
console.log(chalk4.dim('\nTip: Run "wt list" to see available worktrees'));
|
|
206
568
|
process.exit(1);
|
|
207
569
|
}
|
|
208
570
|
spinner.stop();
|
|
209
|
-
if (worktree.path ===
|
|
210
|
-
console.error(
|
|
571
|
+
if (worktree.path === mainRepoPath2) {
|
|
572
|
+
console.error(chalk4.red("\nError: Cannot remove the main worktree"));
|
|
211
573
|
process.exit(1);
|
|
212
574
|
}
|
|
213
|
-
console.log(
|
|
214
|
-
console.log(
|
|
215
|
-
console.log(
|
|
575
|
+
console.log(chalk4.cyan("\nWorktree to remove:"));
|
|
576
|
+
console.log(chalk4.dim(` Path: ${worktree.path}`));
|
|
577
|
+
console.log(chalk4.dim(` Branch: ${worktree.branch || "(detached)"}`));
|
|
216
578
|
if (!options.force) {
|
|
217
579
|
const confirmed = await confirm("\nRemove this worktree?", false);
|
|
218
580
|
if (!confirmed) {
|
|
219
|
-
console.log(
|
|
581
|
+
console.log(chalk4.yellow("Cancelled"));
|
|
220
582
|
return;
|
|
221
583
|
}
|
|
222
584
|
}
|
|
223
|
-
const removeSpinner =
|
|
585
|
+
const removeSpinner = ora3("Removing worktree...").start();
|
|
224
586
|
try {
|
|
225
587
|
await removeWorktree(worktree.path, false);
|
|
226
|
-
removeSpinner.succeed(
|
|
588
|
+
removeSpinner.succeed(chalk4.green(`Removed worktree: ${path7.basename(worktree.path)}`));
|
|
227
589
|
} catch (error) {
|
|
228
590
|
if (options.force) {
|
|
229
591
|
try {
|
|
230
592
|
await removeWorktree(worktree.path, true);
|
|
231
|
-
removeSpinner.succeed(
|
|
593
|
+
removeSpinner.succeed(chalk4.green(`Removed worktree (forced): ${path7.basename(worktree.path)}`));
|
|
232
594
|
} catch (forceError) {
|
|
233
|
-
removeSpinner.fail(
|
|
595
|
+
removeSpinner.fail(chalk4.red(`Failed to remove worktree: ${forceError.message}`));
|
|
234
596
|
process.exit(1);
|
|
235
597
|
}
|
|
236
598
|
} else {
|
|
237
|
-
removeSpinner.fail(
|
|
238
|
-
console.log(
|
|
599
|
+
removeSpinner.fail(chalk4.red(`Failed to remove worktree: ${error.message}`));
|
|
600
|
+
console.log(chalk4.dim("\nTip: Use --force to force removal"));
|
|
239
601
|
process.exit(1);
|
|
240
602
|
}
|
|
241
603
|
}
|
|
242
604
|
}
|
|
243
605
|
|
|
244
606
|
// src/commands/interactive.ts
|
|
245
|
-
import
|
|
246
|
-
import
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
var
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (!onRemote) return chalk4.yellow("local");
|
|
258
|
-
return chalk4.green("active");
|
|
259
|
-
}
|
|
607
|
+
import blessed from "blessed";
|
|
608
|
+
import path8 from "path";
|
|
609
|
+
var screen;
|
|
610
|
+
var worktreeList;
|
|
611
|
+
var statusBar;
|
|
612
|
+
var helpBar;
|
|
613
|
+
var headerBox;
|
|
614
|
+
var mainRepoPath;
|
|
615
|
+
var currentBranch;
|
|
616
|
+
var defaultBranch;
|
|
617
|
+
var worktrees = [];
|
|
618
|
+
var selectedIndex = 0;
|
|
260
619
|
async function interactiveCommand() {
|
|
261
620
|
if (!await isGitRepo()) {
|
|
262
|
-
console.error(
|
|
621
|
+
console.error("Error: Not a git repository");
|
|
263
622
|
process.exit(1);
|
|
264
623
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
`
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
624
|
+
mainRepoPath = await getGitRoot();
|
|
625
|
+
currentBranch = await getCurrentBranch();
|
|
626
|
+
defaultBranch = await getDefaultBranch();
|
|
627
|
+
const repoName = path8.basename(mainRepoPath);
|
|
628
|
+
screen = blessed.screen({
|
|
629
|
+
smartCSR: true,
|
|
630
|
+
title: `wt - ${repoName}`
|
|
631
|
+
});
|
|
632
|
+
headerBox = blessed.box({
|
|
633
|
+
parent: screen,
|
|
634
|
+
top: 0,
|
|
635
|
+
left: 0,
|
|
636
|
+
width: "100%",
|
|
637
|
+
height: 1,
|
|
638
|
+
content: ` ${repoName} (${currentBranch})`,
|
|
639
|
+
style: {
|
|
640
|
+
fg: "white",
|
|
641
|
+
bg: "blue",
|
|
642
|
+
bold: true
|
|
284
643
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
644
|
+
});
|
|
645
|
+
worktreeList = blessed.list({
|
|
646
|
+
parent: screen,
|
|
647
|
+
top: 1,
|
|
648
|
+
left: 0,
|
|
649
|
+
width: "100%",
|
|
650
|
+
height: "100%-3",
|
|
651
|
+
keys: true,
|
|
652
|
+
vi: true,
|
|
653
|
+
mouse: true,
|
|
654
|
+
style: {
|
|
655
|
+
selected: {
|
|
656
|
+
bg: "blue",
|
|
657
|
+
fg: "white"
|
|
658
|
+
},
|
|
659
|
+
item: {
|
|
660
|
+
fg: "white"
|
|
295
661
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
662
|
+
},
|
|
663
|
+
scrollbar: {
|
|
664
|
+
ch: " ",
|
|
665
|
+
style: { bg: "grey" }
|
|
300
666
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
667
|
+
});
|
|
668
|
+
statusBar = blessed.box({
|
|
669
|
+
parent: screen,
|
|
670
|
+
bottom: 1,
|
|
671
|
+
left: 0,
|
|
672
|
+
width: "100%",
|
|
673
|
+
height: 1,
|
|
674
|
+
content: "",
|
|
675
|
+
style: {
|
|
676
|
+
fg: "yellow",
|
|
677
|
+
bg: "black"
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
helpBar = blessed.box({
|
|
681
|
+
parent: screen,
|
|
682
|
+
bottom: 0,
|
|
683
|
+
left: 0,
|
|
684
|
+
width: "100%",
|
|
685
|
+
height: 1,
|
|
686
|
+
content: " [n]ew [d]elete [c]laude [x]codex [Enter]cd [q]uit",
|
|
687
|
+
style: {
|
|
688
|
+
fg: "black",
|
|
689
|
+
bg: "white"
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
await refreshWorktrees();
|
|
693
|
+
worktreeList.on("select", (_item, index) => {
|
|
694
|
+
selectedIndex = index;
|
|
695
|
+
showPath();
|
|
696
|
+
});
|
|
697
|
+
screen.key(["q", "C-c"], () => {
|
|
698
|
+
screen.destroy();
|
|
699
|
+
process.exit(0);
|
|
700
|
+
});
|
|
701
|
+
screen.key(["n"], () => {
|
|
702
|
+
startCreationWizard();
|
|
703
|
+
});
|
|
704
|
+
screen.key(["d"], async () => {
|
|
705
|
+
await deleteSelected();
|
|
706
|
+
});
|
|
707
|
+
screen.key(["c"], async () => {
|
|
708
|
+
await launchAI("claude");
|
|
709
|
+
});
|
|
710
|
+
screen.key(["x"], async () => {
|
|
711
|
+
await launchAI("codex");
|
|
712
|
+
});
|
|
713
|
+
screen.key(["enter"], () => {
|
|
714
|
+
const wt = worktrees[selectedIndex];
|
|
715
|
+
if (wt) {
|
|
716
|
+
screen.destroy();
|
|
717
|
+
console.log(`
|
|
718
|
+
cd "${wt.path}"
|
|
719
|
+
`);
|
|
720
|
+
process.exit(0);
|
|
304
721
|
}
|
|
305
|
-
|
|
722
|
+
});
|
|
723
|
+
screen.key(["r"], async () => {
|
|
724
|
+
await refreshWorktrees();
|
|
725
|
+
});
|
|
726
|
+
worktreeList.focus();
|
|
727
|
+
screen.render();
|
|
728
|
+
}
|
|
729
|
+
async function refreshWorktrees() {
|
|
730
|
+
setStatus("Loading...");
|
|
731
|
+
worktrees = await listWorktrees();
|
|
732
|
+
const items = worktrees.map((wt) => {
|
|
733
|
+
const isMain = wt.path === mainRepoPath;
|
|
734
|
+
const dirName = path8.basename(wt.path);
|
|
735
|
+
const branch = wt.branch || "(detached)";
|
|
736
|
+
const status = isMain ? "[main]" : "";
|
|
737
|
+
return ` ${dirName.padEnd(40)} ${branch.padEnd(25)} ${status}`;
|
|
738
|
+
});
|
|
739
|
+
worktreeList.setItems(items);
|
|
740
|
+
worktreeList.select(selectedIndex);
|
|
741
|
+
showPath();
|
|
742
|
+
screen.render();
|
|
743
|
+
}
|
|
744
|
+
function showPath() {
|
|
745
|
+
const wt = worktrees[selectedIndex];
|
|
746
|
+
if (wt) {
|
|
747
|
+
setStatus(wt.path);
|
|
748
|
+
} else {
|
|
749
|
+
setStatus("");
|
|
306
750
|
}
|
|
307
751
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
752
|
+
function setStatus(msg) {
|
|
753
|
+
statusBar.setContent(` ${msg}`);
|
|
754
|
+
screen.render();
|
|
755
|
+
}
|
|
756
|
+
function startCreationWizard() {
|
|
757
|
+
const state = {
|
|
758
|
+
branchName: "",
|
|
759
|
+
baseBranch: "current",
|
|
760
|
+
copyEnv: true,
|
|
761
|
+
pushToRemote: false,
|
|
762
|
+
aiTool: "claude"
|
|
763
|
+
};
|
|
764
|
+
askBranchName(state);
|
|
765
|
+
}
|
|
766
|
+
function askBranchName(state) {
|
|
767
|
+
const form = blessed.box({
|
|
768
|
+
parent: screen,
|
|
769
|
+
top: "center",
|
|
770
|
+
left: "center",
|
|
771
|
+
width: 60,
|
|
772
|
+
height: 12,
|
|
773
|
+
border: { type: "line" },
|
|
774
|
+
style: {
|
|
775
|
+
fg: "white",
|
|
776
|
+
bg: "black",
|
|
777
|
+
border: { fg: "blue" }
|
|
778
|
+
},
|
|
779
|
+
label: " New Worktree "
|
|
780
|
+
});
|
|
781
|
+
blessed.text({
|
|
782
|
+
parent: form,
|
|
783
|
+
top: 1,
|
|
784
|
+
left: 2,
|
|
785
|
+
content: `Repository: ${path8.basename(mainRepoPath)}`,
|
|
786
|
+
style: { fg: "cyan" }
|
|
787
|
+
});
|
|
788
|
+
blessed.text({
|
|
789
|
+
parent: form,
|
|
790
|
+
top: 2,
|
|
791
|
+
left: 2,
|
|
792
|
+
content: `Current branch: ${currentBranch}`,
|
|
793
|
+
style: { fg: "grey" }
|
|
794
|
+
});
|
|
795
|
+
blessed.text({
|
|
796
|
+
parent: form,
|
|
797
|
+
top: 4,
|
|
798
|
+
left: 2,
|
|
799
|
+
content: "Branch name:",
|
|
800
|
+
style: { fg: "white" }
|
|
801
|
+
});
|
|
802
|
+
const input = blessed.textbox({
|
|
803
|
+
parent: form,
|
|
804
|
+
top: 5,
|
|
805
|
+
left: 2,
|
|
806
|
+
width: 54,
|
|
807
|
+
height: 1,
|
|
808
|
+
style: {
|
|
809
|
+
fg: "white",
|
|
810
|
+
bg: "grey"
|
|
811
|
+
},
|
|
812
|
+
inputOnFocus: true
|
|
813
|
+
});
|
|
814
|
+
blessed.text({
|
|
815
|
+
parent: form,
|
|
816
|
+
top: 7,
|
|
817
|
+
left: 2,
|
|
818
|
+
content: "[Enter] next [Esc] cancel",
|
|
819
|
+
style: { fg: "grey" }
|
|
820
|
+
});
|
|
821
|
+
input.focus();
|
|
822
|
+
screen.render();
|
|
823
|
+
input.on("submit", (value) => {
|
|
824
|
+
if (!value || !value.trim()) {
|
|
825
|
+
form.destroy();
|
|
826
|
+
screen.render();
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
try {
|
|
830
|
+
validateBranchName(value.trim());
|
|
831
|
+
state.branchName = value.trim();
|
|
832
|
+
form.destroy();
|
|
833
|
+
askBaseBranch(state);
|
|
834
|
+
} catch (e) {
|
|
835
|
+
setStatus(`Error: ${e.message}`);
|
|
836
|
+
input.focus();
|
|
837
|
+
screen.render();
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
input.on("cancel", () => {
|
|
841
|
+
form.destroy();
|
|
842
|
+
screen.render();
|
|
843
|
+
});
|
|
844
|
+
input.readInput();
|
|
845
|
+
}
|
|
846
|
+
function askBaseBranch(state) {
|
|
847
|
+
const form = blessed.box({
|
|
848
|
+
parent: screen,
|
|
849
|
+
top: "center",
|
|
850
|
+
left: "center",
|
|
851
|
+
width: 50,
|
|
852
|
+
height: 10,
|
|
853
|
+
border: { type: "line" },
|
|
854
|
+
style: {
|
|
855
|
+
fg: "white",
|
|
856
|
+
bg: "black",
|
|
857
|
+
border: { fg: "blue" }
|
|
858
|
+
},
|
|
859
|
+
label: " Base Branch "
|
|
860
|
+
});
|
|
861
|
+
blessed.text({
|
|
862
|
+
parent: form,
|
|
863
|
+
top: 1,
|
|
864
|
+
left: 2,
|
|
865
|
+
content: "Create worktree from:",
|
|
866
|
+
style: { fg: "white" }
|
|
867
|
+
});
|
|
868
|
+
const list = blessed.list({
|
|
869
|
+
parent: form,
|
|
870
|
+
top: 3,
|
|
871
|
+
left: 2,
|
|
872
|
+
width: 44,
|
|
873
|
+
height: 3,
|
|
874
|
+
keys: true,
|
|
875
|
+
vi: true,
|
|
876
|
+
style: {
|
|
877
|
+
selected: { bg: "blue", fg: "white" },
|
|
878
|
+
item: { fg: "white" }
|
|
879
|
+
},
|
|
880
|
+
items: [
|
|
881
|
+
` Current branch (${currentBranch})`,
|
|
882
|
+
` Default branch (${defaultBranch})`
|
|
883
|
+
]
|
|
884
|
+
});
|
|
885
|
+
blessed.text({
|
|
886
|
+
parent: form,
|
|
887
|
+
top: 7,
|
|
888
|
+
left: 2,
|
|
889
|
+
content: "[Enter] select [Esc] cancel",
|
|
890
|
+
style: { fg: "grey" }
|
|
891
|
+
});
|
|
892
|
+
list.focus();
|
|
893
|
+
screen.render();
|
|
894
|
+
list.on("select", (_item, index) => {
|
|
895
|
+
state.baseBranch = index === 0 ? "current" : "default";
|
|
896
|
+
form.destroy();
|
|
897
|
+
askCopyEnv(state);
|
|
898
|
+
});
|
|
899
|
+
list.key(["escape"], () => {
|
|
900
|
+
form.destroy();
|
|
901
|
+
screen.render();
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
function askCopyEnv(state) {
|
|
905
|
+
const form = blessed.box({
|
|
906
|
+
parent: screen,
|
|
907
|
+
top: "center",
|
|
908
|
+
left: "center",
|
|
909
|
+
width: 40,
|
|
910
|
+
height: 8,
|
|
911
|
+
border: { type: "line" },
|
|
912
|
+
style: {
|
|
913
|
+
fg: "white",
|
|
914
|
+
bg: "black",
|
|
915
|
+
border: { fg: "blue" }
|
|
916
|
+
},
|
|
917
|
+
label: " Environment Files "
|
|
918
|
+
});
|
|
919
|
+
blessed.text({
|
|
920
|
+
parent: form,
|
|
921
|
+
top: 1,
|
|
922
|
+
left: 2,
|
|
923
|
+
content: "Copy .env files to worktree?",
|
|
924
|
+
style: { fg: "white" }
|
|
925
|
+
});
|
|
926
|
+
const list = blessed.list({
|
|
927
|
+
parent: form,
|
|
928
|
+
top: 3,
|
|
929
|
+
left: 2,
|
|
930
|
+
width: 34,
|
|
931
|
+
height: 2,
|
|
932
|
+
keys: true,
|
|
933
|
+
vi: true,
|
|
934
|
+
style: {
|
|
935
|
+
selected: { bg: "blue", fg: "white" },
|
|
936
|
+
item: { fg: "white" }
|
|
937
|
+
},
|
|
938
|
+
items: [" Yes (recommended)", " No"]
|
|
939
|
+
});
|
|
940
|
+
list.focus();
|
|
941
|
+
screen.render();
|
|
942
|
+
list.on("select", (_item, index) => {
|
|
943
|
+
state.copyEnv = index === 0;
|
|
944
|
+
form.destroy();
|
|
945
|
+
askPushToRemote(state);
|
|
946
|
+
});
|
|
947
|
+
list.key(["escape"], () => {
|
|
948
|
+
form.destroy();
|
|
949
|
+
screen.render();
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
function askPushToRemote(state) {
|
|
953
|
+
const form = blessed.box({
|
|
954
|
+
parent: screen,
|
|
955
|
+
top: "center",
|
|
956
|
+
left: "center",
|
|
957
|
+
width: 45,
|
|
958
|
+
height: 8,
|
|
959
|
+
border: { type: "line" },
|
|
960
|
+
style: {
|
|
961
|
+
fg: "white",
|
|
962
|
+
bg: "black",
|
|
963
|
+
border: { fg: "blue" }
|
|
964
|
+
},
|
|
965
|
+
label: " Push to Remote "
|
|
966
|
+
});
|
|
967
|
+
blessed.text({
|
|
968
|
+
parent: form,
|
|
969
|
+
top: 1,
|
|
970
|
+
left: 2,
|
|
971
|
+
content: "Push branch to GitHub immediately?",
|
|
972
|
+
style: { fg: "white" }
|
|
973
|
+
});
|
|
974
|
+
const list = blessed.list({
|
|
975
|
+
parent: form,
|
|
976
|
+
top: 3,
|
|
977
|
+
left: 2,
|
|
978
|
+
width: 39,
|
|
979
|
+
height: 2,
|
|
980
|
+
keys: true,
|
|
981
|
+
vi: true,
|
|
982
|
+
style: {
|
|
983
|
+
selected: { bg: "blue", fg: "white" },
|
|
984
|
+
item: { fg: "white" }
|
|
985
|
+
},
|
|
986
|
+
items: [" No (push later)", " Yes (visible on GitHub now)"]
|
|
987
|
+
});
|
|
988
|
+
list.focus();
|
|
989
|
+
screen.render();
|
|
990
|
+
list.on("select", (_item, index) => {
|
|
991
|
+
state.pushToRemote = index === 1;
|
|
992
|
+
form.destroy();
|
|
993
|
+
askAITool(state);
|
|
994
|
+
});
|
|
995
|
+
list.key(["escape"], () => {
|
|
996
|
+
form.destroy();
|
|
997
|
+
screen.render();
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
function askAITool(state) {
|
|
1001
|
+
const form = blessed.box({
|
|
1002
|
+
parent: screen,
|
|
1003
|
+
top: "center",
|
|
1004
|
+
left: "center",
|
|
1005
|
+
width: 40,
|
|
1006
|
+
height: 10,
|
|
1007
|
+
border: { type: "line" },
|
|
1008
|
+
style: {
|
|
1009
|
+
fg: "white",
|
|
1010
|
+
bg: "black",
|
|
1011
|
+
border: { fg: "blue" }
|
|
1012
|
+
},
|
|
1013
|
+
label: " Launch AI Tool "
|
|
1014
|
+
});
|
|
1015
|
+
blessed.text({
|
|
1016
|
+
parent: form,
|
|
1017
|
+
top: 1,
|
|
1018
|
+
left: 2,
|
|
1019
|
+
content: "Which AI assistant to launch?",
|
|
1020
|
+
style: { fg: "white" }
|
|
1021
|
+
});
|
|
1022
|
+
const list = blessed.list({
|
|
1023
|
+
parent: form,
|
|
1024
|
+
top: 3,
|
|
1025
|
+
left: 2,
|
|
1026
|
+
width: 34,
|
|
1027
|
+
height: 3,
|
|
1028
|
+
keys: true,
|
|
1029
|
+
vi: true,
|
|
1030
|
+
style: {
|
|
1031
|
+
selected: { bg: "blue", fg: "white" },
|
|
1032
|
+
item: { fg: "white" }
|
|
1033
|
+
},
|
|
1034
|
+
items: [" Claude Code", " Codex", " Skip (just create worktree)"]
|
|
1035
|
+
});
|
|
1036
|
+
list.focus();
|
|
1037
|
+
screen.render();
|
|
1038
|
+
list.on("select", (_item, index) => {
|
|
1039
|
+
state.aiTool = index === 0 ? "claude" : index === 1 ? "codex" : "skip";
|
|
1040
|
+
form.destroy();
|
|
1041
|
+
executeCreation(state);
|
|
1042
|
+
});
|
|
1043
|
+
list.key(["escape"], () => {
|
|
1044
|
+
form.destroy();
|
|
1045
|
+
screen.render();
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
async function executeCreation(state) {
|
|
1049
|
+
const { branchName, baseBranch, copyEnv, pushToRemote, aiTool } = state;
|
|
1050
|
+
setStatus(`Creating ${branchName}...`);
|
|
1051
|
+
try {
|
|
1052
|
+
if (baseBranch === "default" && currentBranch !== defaultBranch) {
|
|
1053
|
+
const worktreePath = getWorktreePath(mainRepoPath, branchName);
|
|
1054
|
+
const { execFile: execFile2 } = await import("child_process");
|
|
1055
|
+
const { promisify: promisify2 } = await import("util");
|
|
1056
|
+
const execFileAsync2 = promisify2(execFile2);
|
|
1057
|
+
await execFileAsync2("git", ["worktree", "add", "-b", branchName, "--", worktreePath, defaultBranch]);
|
|
1058
|
+
if (copyEnv) {
|
|
1059
|
+
await copyEnvFiles(mainRepoPath, worktreePath);
|
|
1060
|
+
}
|
|
1061
|
+
if (pushToRemote) {
|
|
1062
|
+
setStatus(`Pushing ${branchName}...`);
|
|
1063
|
+
await pushBranch(branchName, worktreePath);
|
|
1064
|
+
}
|
|
1065
|
+
await refreshWorktrees();
|
|
1066
|
+
setStatus(`Created ${branchName}`);
|
|
1067
|
+
if (aiTool !== "skip") {
|
|
1068
|
+
await launchInWorktree(worktreePath, aiTool);
|
|
1069
|
+
}
|
|
1070
|
+
} else {
|
|
1071
|
+
const worktreePath = getWorktreePath(mainRepoPath, branchName);
|
|
1072
|
+
await createWorktree(worktreePath, branchName);
|
|
1073
|
+
if (copyEnv) {
|
|
1074
|
+
await copyEnvFiles(mainRepoPath, worktreePath);
|
|
1075
|
+
}
|
|
1076
|
+
if (pushToRemote) {
|
|
1077
|
+
setStatus(`Pushing ${branchName}...`);
|
|
1078
|
+
await pushBranch(branchName, worktreePath);
|
|
1079
|
+
}
|
|
1080
|
+
await refreshWorktrees();
|
|
1081
|
+
setStatus(`Created ${branchName}`);
|
|
1082
|
+
if (aiTool !== "skip") {
|
|
1083
|
+
await launchInWorktree(worktreePath, aiTool);
|
|
319
1084
|
}
|
|
320
1085
|
}
|
|
321
|
-
|
|
322
|
-
|
|
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" });
|
|
1086
|
+
} catch (e) {
|
|
1087
|
+
setStatus(`Error: ${e.message}`);
|
|
335
1088
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
`)
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
1089
|
+
}
|
|
1090
|
+
async function launchInWorktree(worktreePath, tool) {
|
|
1091
|
+
const available = await isToolAvailable(tool);
|
|
1092
|
+
if (!available) {
|
|
1093
|
+
setStatus(`${tool} is not installed`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
setStatus(`Launching ${tool}...`);
|
|
1097
|
+
screen.destroy();
|
|
1098
|
+
launchAITool({ cwd: worktreePath, tool });
|
|
1099
|
+
console.log(`
|
|
1100
|
+
${tool} launched in: ${worktreePath}
|
|
1101
|
+
`);
|
|
1102
|
+
process.exit(0);
|
|
1103
|
+
}
|
|
1104
|
+
async function deleteSelected() {
|
|
1105
|
+
const wt = worktrees[selectedIndex];
|
|
1106
|
+
if (!wt) return;
|
|
1107
|
+
if (wt.path === mainRepoPath) {
|
|
1108
|
+
setStatus("Cannot delete main worktree");
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const dirName = path8.basename(wt.path);
|
|
1112
|
+
const confirm2 = blessed.question({
|
|
1113
|
+
parent: screen,
|
|
1114
|
+
top: "center",
|
|
1115
|
+
left: "center",
|
|
1116
|
+
width: 40,
|
|
1117
|
+
height: 5,
|
|
1118
|
+
border: { type: "line" },
|
|
1119
|
+
style: {
|
|
1120
|
+
fg: "white",
|
|
1121
|
+
bg: "black",
|
|
1122
|
+
border: { fg: "red" }
|
|
347
1123
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
console.log(chalk4.cyan(` cd "${wt.path}"
|
|
354
|
-
`));
|
|
1124
|
+
});
|
|
1125
|
+
confirm2.ask(`Delete ${dirName}?`, async (err, yes) => {
|
|
1126
|
+
confirm2.destroy();
|
|
1127
|
+
if (yes) {
|
|
1128
|
+
setStatus(`Deleting ${dirName}...`);
|
|
355
1129
|
try {
|
|
356
|
-
await
|
|
357
|
-
|
|
1130
|
+
await removeWorktree(wt.path, false);
|
|
1131
|
+
setStatus(`Deleted ${dirName}`);
|
|
358
1132
|
} 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
1133
|
try {
|
|
391
|
-
await removeWorktree(wt.path,
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
}
|
|
1134
|
+
await removeWorktree(wt.path, true);
|
|
1135
|
+
setStatus(`Deleted ${dirName} (forced)`);
|
|
1136
|
+
} catch (e) {
|
|
1137
|
+
setStatus(`Error: ${e.message}`);
|
|
406
1138
|
}
|
|
407
|
-
await pause();
|
|
408
1139
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
async function pause() {
|
|
416
|
-
await inquirer.prompt([
|
|
417
|
-
{
|
|
418
|
-
type: "input",
|
|
419
|
-
name: "continue",
|
|
420
|
-
message: "Press Enter to continue..."
|
|
1140
|
+
if (selectedIndex > 0) selectedIndex--;
|
|
1141
|
+
await refreshWorktrees();
|
|
1142
|
+
} else {
|
|
1143
|
+
await refreshWorktrees();
|
|
421
1144
|
}
|
|
422
|
-
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
async function launchAI(tool) {
|
|
1148
|
+
const wt = worktrees[selectedIndex];
|
|
1149
|
+
if (!wt) return;
|
|
1150
|
+
const available = await isToolAvailable(tool);
|
|
1151
|
+
if (!available) {
|
|
1152
|
+
setStatus(`${tool} is not installed`);
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
setStatus(`Launching ${tool}...`);
|
|
1156
|
+
screen.destroy();
|
|
1157
|
+
launchAITool({ cwd: wt.path, tool });
|
|
1158
|
+
console.log(`
|
|
1159
|
+
${tool} launched in: ${path8.basename(wt.path)}
|
|
1160
|
+
`);
|
|
1161
|
+
process.exit(0);
|
|
423
1162
|
}
|
|
424
1163
|
|
|
425
1164
|
// src/index.ts
|
|
426
1165
|
var program = new Command();
|
|
427
|
-
program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.
|
|
1166
|
+
program.name("wt").description("CLI tool to streamline git worktrees with AI coding assistants").version("1.4.0").action(async () => {
|
|
428
1167
|
await interactiveCommand();
|
|
429
1168
|
});
|
|
430
1169
|
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) => {
|