worktree-launcher 1.2.0 → 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/README.md +22 -8
- package/dist/index.js +682 -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,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/
|
|
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 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
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
394
|
+
chalk2.dim("\u2500".repeat(100))
|
|
41
395
|
);
|
|
42
396
|
console.log(
|
|
43
|
-
|
|
397
|
+
chalk2.bold(padEnd("Path", 50)) + chalk2.bold(padEnd("Branch", 25)) + chalk2.bold("Status")
|
|
44
398
|
);
|
|
45
399
|
console.log(
|
|
46
|
-
|
|
400
|
+
chalk2.dim("\u2500".repeat(100))
|
|
47
401
|
);
|
|
48
|
-
for (const wt of
|
|
49
|
-
const isMain = wt.path ===
|
|
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 ?
|
|
406
|
+
const displayBranch = wt.detached ? chalk2.yellow("(detached)") : wt.branch || "N/A";
|
|
53
407
|
console.log(
|
|
54
|
-
padEnd(isMain ?
|
|
408
|
+
padEnd(isMain ? chalk2.bold(displayPath) : displayPath, 50) + padEnd(displayBranch, 25) + status
|
|
55
409
|
);
|
|
56
410
|
}
|
|
57
|
-
console.log(
|
|
58
|
-
console.log(
|
|
59
|
-
Total: ${
|
|
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
|
|
417
|
+
return chalk2.blue("main");
|
|
64
418
|
}
|
|
65
419
|
if (detached) {
|
|
66
|
-
return
|
|
420
|
+
return chalk2.yellow("detached");
|
|
67
421
|
}
|
|
68
422
|
if (!branch) {
|
|
69
|
-
return
|
|
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
|
|
428
|
+
return chalk2.green("merged") + chalk2.dim(" (can clean)");
|
|
75
429
|
}
|
|
76
430
|
if (!existsOnRemote) {
|
|
77
|
-
return
|
|
431
|
+
return chalk2.yellow("local only");
|
|
78
432
|
}
|
|
79
|
-
return
|
|
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(
|
|
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 =
|
|
445
|
+
const newResult = path5.join(parts[i], result);
|
|
92
446
|
if (newResult.length > maxLength - 3) {
|
|
93
|
-
return "..." +
|
|
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
|
|
102
|
-
import
|
|
103
|
-
import
|
|
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(
|
|
460
|
+
console.error(chalk3.red("Error: Not a git repository"));
|
|
107
461
|
process.exit(1);
|
|
108
462
|
}
|
|
109
|
-
const
|
|
110
|
-
const pruneSpinner =
|
|
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
|
|
114
|
-
const spinner =
|
|
467
|
+
const worktrees2 = await listWorktrees();
|
|
468
|
+
const spinner = ora2("Checking worktree status...").start();
|
|
115
469
|
const staleWorktrees = [];
|
|
116
|
-
for (const wt of
|
|
117
|
-
if (wt.path ===
|
|
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(
|
|
485
|
+
console.log(chalk3.green("\n\u2713 No stale worktrees found"));
|
|
132
486
|
return;
|
|
133
487
|
}
|
|
134
|
-
console.log(
|
|
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" ?
|
|
492
|
+
const reasonText = wt.reason === "merged" ? chalk3.green("merged") : chalk3.yellow("local only");
|
|
139
493
|
return {
|
|
140
|
-
name: `${
|
|
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(
|
|
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(
|
|
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 =
|
|
520
|
+
const removeSpinner = ora2(`Removing ${path6.basename(wt.path)}...`).start();
|
|
167
521
|
try {
|
|
168
522
|
await removeWorktree(wt.path, false);
|
|
169
|
-
removeSpinner.succeed(
|
|
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(
|
|
528
|
+
removeSpinner.succeed(chalk3.green(`Removed ${path6.basename(wt.path)} (forced)`));
|
|
175
529
|
removed++;
|
|
176
530
|
} catch (forceError) {
|
|
177
|
-
removeSpinner.fail(
|
|
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(
|
|
538
|
+
console.log(chalk3.green(`\u2713 Removed ${removed} worktree(s)`));
|
|
185
539
|
}
|
|
186
540
|
if (failed > 0) {
|
|
187
|
-
console.log(
|
|
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
|
|
193
|
-
import
|
|
194
|
-
import
|
|
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(
|
|
551
|
+
console.error(chalk4.red("Error: Not a git repository"));
|
|
198
552
|
process.exit(1);
|
|
199
553
|
}
|
|
200
|
-
const
|
|
201
|
-
const spinner =
|
|
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(
|
|
205
|
-
console.log(
|
|
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 ===
|
|
210
|
-
console.error(
|
|
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(
|
|
214
|
-
console.log(
|
|
215
|
-
console.log(
|
|
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(
|
|
573
|
+
console.log(chalk4.yellow("Cancelled"));
|
|
220
574
|
return;
|
|
221
575
|
}
|
|
222
576
|
}
|
|
223
|
-
const removeSpinner =
|
|
577
|
+
const removeSpinner = ora3("Removing worktree...").start();
|
|
224
578
|
try {
|
|
225
579
|
await removeWorktree(worktree.path, false);
|
|
226
|
-
removeSpinner.succeed(
|
|
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(
|
|
585
|
+
removeSpinner.succeed(chalk4.green(`Removed worktree (forced): ${path7.basename(worktree.path)}`));
|
|
232
586
|
} catch (forceError) {
|
|
233
|
-
removeSpinner.fail(
|
|
587
|
+
removeSpinner.fail(chalk4.red(`Failed to remove worktree: ${forceError.message}`));
|
|
234
588
|
process.exit(1);
|
|
235
589
|
}
|
|
236
590
|
} else {
|
|
237
|
-
removeSpinner.fail(
|
|
238
|
-
console.log(
|
|
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
|
|
246
|
-
import
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
var
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
610
|
+
console.error("Error: Not a git repository");
|
|
263
611
|
process.exit(1);
|
|
264
612
|
}
|
|
265
|
-
|
|
266
|
-
const repoName =
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
649
|
+
},
|
|
650
|
+
scrollbar: {
|
|
651
|
+
ch: " ",
|
|
652
|
+
style: { bg: "grey" }
|
|
300
653
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
357
|
-
|
|
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,
|
|
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
|
-
}
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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.
|
|
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) => {
|