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/README.md +15 -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "worktree-launcher",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "CLI tool for managing git worktrees with AI coding assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"author": "mertdeveci5",
|
|
34
34
|
"license": "MIT",
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"blessed": "^0.1.81",
|
|
37
|
+
"blessed-contrib": "^4.11.0",
|
|
36
38
|
"chalk": "^5.3.0",
|
|
37
39
|
"commander": "^12.1.0",
|
|
38
40
|
"glob": "^10.4.5",
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
"ora": "^8.1.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
45
|
+
"@types/blessed": "^0.1.27",
|
|
43
46
|
"@types/inquirer": "^9.0.7",
|
|
44
47
|
"@types/node": "^20.17.9",
|
|
45
48
|
"tsup": "^8.3.5",
|
package/dist/chunk-KGMGW33P.js
DELETED
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
// src/commands/new.ts
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import ora from "ora";
|
|
4
|
-
import path4 from "path";
|
|
5
|
-
|
|
6
|
-
// src/utils/git.ts
|
|
7
|
-
import { execFile } from "child_process";
|
|
8
|
-
import { promisify } from "util";
|
|
9
|
-
import path from "path";
|
|
10
|
-
var execFileAsync = promisify(execFile);
|
|
11
|
-
async function isGitRepo() {
|
|
12
|
-
try {
|
|
13
|
-
await execFileAsync("git", ["rev-parse", "--git-dir"]);
|
|
14
|
-
return true;
|
|
15
|
-
} catch {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
async function getGitRoot() {
|
|
20
|
-
const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"]);
|
|
21
|
-
return stdout.trim();
|
|
22
|
-
}
|
|
23
|
-
async function branchExists(branchName) {
|
|
24
|
-
try {
|
|
25
|
-
await execFileAsync("git", ["rev-parse", "--verify", branchName]);
|
|
26
|
-
return true;
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
async function remoteBranchExists(branchName) {
|
|
32
|
-
try {
|
|
33
|
-
const { stdout } = await execFileAsync("git", ["branch", "-r"]);
|
|
34
|
-
const remoteBranches = stdout.split("\n").map((b) => b.trim());
|
|
35
|
-
return remoteBranches.some(
|
|
36
|
-
(b) => b === `origin/${branchName}` || b.endsWith(`/${branchName}`)
|
|
37
|
-
);
|
|
38
|
-
} catch {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
async function getDefaultBranch() {
|
|
43
|
-
try {
|
|
44
|
-
const { stdout } = await execFileAsync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"]);
|
|
45
|
-
return stdout.trim().replace("refs/remotes/origin/", "");
|
|
46
|
-
} catch {
|
|
47
|
-
if (await branchExists("main")) return "main";
|
|
48
|
-
if (await branchExists("master")) return "master";
|
|
49
|
-
return "main";
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async function createWorktree(worktreePath, branchName) {
|
|
53
|
-
validateBranchName(branchName);
|
|
54
|
-
const exists = await branchExists(branchName);
|
|
55
|
-
if (exists) {
|
|
56
|
-
await execFileAsync("git", ["worktree", "add", "--", worktreePath, branchName]);
|
|
57
|
-
} else {
|
|
58
|
-
await execFileAsync("git", ["worktree", "add", "-b", branchName, "--", worktreePath]);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
async function listWorktrees() {
|
|
62
|
-
const { stdout } = await execFileAsync("git", ["worktree", "list", "--porcelain"]);
|
|
63
|
-
const worktrees = [];
|
|
64
|
-
let current = {};
|
|
65
|
-
for (const line of stdout.split("\n")) {
|
|
66
|
-
if (line.startsWith("worktree ")) {
|
|
67
|
-
if (current.path) {
|
|
68
|
-
worktrees.push(current);
|
|
69
|
-
}
|
|
70
|
-
current = {
|
|
71
|
-
path: line.substring(9),
|
|
72
|
-
bare: false,
|
|
73
|
-
detached: false
|
|
74
|
-
};
|
|
75
|
-
} else if (line.startsWith("HEAD ")) {
|
|
76
|
-
current.head = line.substring(5);
|
|
77
|
-
} else if (line.startsWith("branch ")) {
|
|
78
|
-
current.branch = line.substring(7).replace("refs/heads/", "");
|
|
79
|
-
} else if (line === "bare") {
|
|
80
|
-
current.bare = true;
|
|
81
|
-
} else if (line === "detached") {
|
|
82
|
-
current.detached = true;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
if (current.path) {
|
|
86
|
-
worktrees.push(current);
|
|
87
|
-
}
|
|
88
|
-
return worktrees;
|
|
89
|
-
}
|
|
90
|
-
async function removeWorktree(worktreePath, force = false) {
|
|
91
|
-
const args = ["worktree", "remove"];
|
|
92
|
-
if (force) args.push("--force");
|
|
93
|
-
args.push(worktreePath);
|
|
94
|
-
await execFileAsync("git", args);
|
|
95
|
-
}
|
|
96
|
-
async function pruneWorktrees() {
|
|
97
|
-
await execFileAsync("git", ["worktree", "prune"]);
|
|
98
|
-
}
|
|
99
|
-
async function isBranchMerged(branchName) {
|
|
100
|
-
try {
|
|
101
|
-
const defaultBranch = await getDefaultBranch();
|
|
102
|
-
const { stdout } = await execFileAsync("git", ["branch", "--merged", defaultBranch]);
|
|
103
|
-
const mergedBranches = stdout.split("\n").map((b) => b.trim().replace("* ", ""));
|
|
104
|
-
return mergedBranches.includes(branchName);
|
|
105
|
-
} catch {
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function validateBranchName(branchName) {
|
|
110
|
-
if (!branchName || branchName.trim() === "") {
|
|
111
|
-
throw new Error("Branch name cannot be empty");
|
|
112
|
-
}
|
|
113
|
-
if (branchName.startsWith("-")) {
|
|
114
|
-
throw new Error("Branch name cannot start with -");
|
|
115
|
-
}
|
|
116
|
-
if (branchName.includes("..")) {
|
|
117
|
-
throw new Error("Branch name cannot contain ..");
|
|
118
|
-
}
|
|
119
|
-
if (branchName.length > 250) {
|
|
120
|
-
throw new Error("Branch name too long (max 250 characters)");
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
function getWorktreePath(mainRepoPath, branchName) {
|
|
124
|
-
validateBranchName(branchName);
|
|
125
|
-
const repoName = path.basename(mainRepoPath);
|
|
126
|
-
const safeBranchName = branchName.replace(/\//g, "-");
|
|
127
|
-
return path.join(path.dirname(mainRepoPath), `${repoName}-${safeBranchName}`);
|
|
128
|
-
}
|
|
129
|
-
async function findWorktree(identifier) {
|
|
130
|
-
const worktrees = await listWorktrees();
|
|
131
|
-
return worktrees.find(
|
|
132
|
-
(wt) => wt.branch === identifier || wt.path === identifier || path.basename(wt.path) === identifier || wt.path.endsWith(identifier)
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
async function pushBranch(branchName, cwd) {
|
|
136
|
-
const args = ["push", "-u", "origin", branchName];
|
|
137
|
-
await execFileAsync("git", args, cwd ? { cwd } : void 0);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// src/utils/env.ts
|
|
141
|
-
import { glob } from "glob";
|
|
142
|
-
import { copyFile } from "fs/promises";
|
|
143
|
-
import path2 from "path";
|
|
144
|
-
async function findEnvFiles(sourceDir) {
|
|
145
|
-
const files = await glob(".env*", {
|
|
146
|
-
cwd: sourceDir,
|
|
147
|
-
dot: true,
|
|
148
|
-
nodir: true
|
|
149
|
-
});
|
|
150
|
-
return files.filter((file) => {
|
|
151
|
-
if (file !== ".env" && !file.startsWith(".env.")) return false;
|
|
152
|
-
if (file.endsWith(".example") || file.endsWith(".sample") || file.endsWith(".template")) return false;
|
|
153
|
-
return true;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
async function copyEnvFiles(sourceDir, destDir) {
|
|
157
|
-
const envFiles = await findEnvFiles(sourceDir);
|
|
158
|
-
const copied = [];
|
|
159
|
-
for (const file of envFiles) {
|
|
160
|
-
try {
|
|
161
|
-
await copyFile(path2.join(sourceDir, file), path2.join(destDir, file));
|
|
162
|
-
copied.push(file);
|
|
163
|
-
} catch (error) {
|
|
164
|
-
console.warn(`Warning: Could not copy ${file}: ${error}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return copied;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// src/utils/launcher.ts
|
|
171
|
-
import { spawn } from "child_process";
|
|
172
|
-
import { access } from "fs/promises";
|
|
173
|
-
import path3 from "path";
|
|
174
|
-
import { constants } from "fs";
|
|
175
|
-
function launchAITool(options) {
|
|
176
|
-
const { cwd, tool } = options;
|
|
177
|
-
spawn(tool, [], {
|
|
178
|
-
cwd,
|
|
179
|
-
stdio: "inherit",
|
|
180
|
-
shell: true
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
async function isToolAvailable(tool) {
|
|
184
|
-
return new Promise((resolve) => {
|
|
185
|
-
const child = spawn("which", [tool]);
|
|
186
|
-
child.on("close", (code) => {
|
|
187
|
-
resolve(code === 0);
|
|
188
|
-
});
|
|
189
|
-
child.on("error", () => {
|
|
190
|
-
resolve(false);
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
async function detectPackageManager(dir) {
|
|
195
|
-
const lockfiles = [
|
|
196
|
-
{ file: "bun.lockb", manager: "bun" },
|
|
197
|
-
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
198
|
-
{ file: "yarn.lock", manager: "yarn" },
|
|
199
|
-
{ file: "package-lock.json", manager: "npm" }
|
|
200
|
-
];
|
|
201
|
-
for (const { file, manager } of lockfiles) {
|
|
202
|
-
try {
|
|
203
|
-
await access(path3.join(dir, file), constants.R_OK);
|
|
204
|
-
return manager;
|
|
205
|
-
} catch {
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
await access(path3.join(dir, "package.json"), constants.R_OK);
|
|
210
|
-
return "npm";
|
|
211
|
-
} catch {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function runInstall(dir, packageManager) {
|
|
216
|
-
return new Promise((resolve, reject) => {
|
|
217
|
-
const child = spawn(packageManager, ["install"], {
|
|
218
|
-
cwd: dir,
|
|
219
|
-
stdio: "inherit"
|
|
220
|
-
});
|
|
221
|
-
child.on("close", (code) => {
|
|
222
|
-
if (code === 0) {
|
|
223
|
-
resolve();
|
|
224
|
-
} else {
|
|
225
|
-
reject(new Error(`${packageManager} install failed with code ${code}`));
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
child.on("error", reject);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// src/ui/selector.ts
|
|
233
|
-
import inquirer from "inquirer";
|
|
234
|
-
var AI_TOOLS = [
|
|
235
|
-
{
|
|
236
|
-
name: "Claude Code",
|
|
237
|
-
value: "claude",
|
|
238
|
-
description: "Anthropic's Claude coding assistant"
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
name: "Codex",
|
|
242
|
-
value: "codex",
|
|
243
|
-
description: "OpenAI's Codex coding assistant"
|
|
244
|
-
}
|
|
245
|
-
];
|
|
246
|
-
async function selectAITool() {
|
|
247
|
-
const { tool } = await inquirer.prompt([
|
|
248
|
-
{
|
|
249
|
-
type: "list",
|
|
250
|
-
name: "tool",
|
|
251
|
-
message: "Select AI coding assistant:",
|
|
252
|
-
choices: AI_TOOLS.map((t) => ({
|
|
253
|
-
name: `${t.name} - ${t.description}`,
|
|
254
|
-
value: t.value,
|
|
255
|
-
short: t.name
|
|
256
|
-
}))
|
|
257
|
-
}
|
|
258
|
-
]);
|
|
259
|
-
return tool;
|
|
260
|
-
}
|
|
261
|
-
async function confirm(message, defaultValue = true) {
|
|
262
|
-
const { confirmed } = await inquirer.prompt([
|
|
263
|
-
{
|
|
264
|
-
type: "confirm",
|
|
265
|
-
name: "confirmed",
|
|
266
|
-
message,
|
|
267
|
-
default: defaultValue
|
|
268
|
-
}
|
|
269
|
-
]);
|
|
270
|
-
return confirmed;
|
|
271
|
-
}
|
|
272
|
-
async function selectMultiple(message, choices) {
|
|
273
|
-
const { selected } = await inquirer.prompt([
|
|
274
|
-
{
|
|
275
|
-
type: "checkbox",
|
|
276
|
-
name: "selected",
|
|
277
|
-
message,
|
|
278
|
-
choices
|
|
279
|
-
}
|
|
280
|
-
]);
|
|
281
|
-
return selected;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/commands/new.ts
|
|
285
|
-
async function newCommand(branchName, options) {
|
|
286
|
-
if (!await isGitRepo()) {
|
|
287
|
-
console.error(chalk.red("Error: Not a git repository"));
|
|
288
|
-
process.exit(1);
|
|
289
|
-
}
|
|
290
|
-
const mainRepoPath = await getGitRoot();
|
|
291
|
-
const repoName = path4.basename(mainRepoPath);
|
|
292
|
-
const worktreePath = getWorktreePath(mainRepoPath, branchName);
|
|
293
|
-
console.log(chalk.cyan(`
|
|
294
|
-
Creating worktree for branch: ${chalk.bold(branchName)}`));
|
|
295
|
-
console.log(chalk.dim(`Repository: ${repoName}`));
|
|
296
|
-
console.log(chalk.dim(`Worktree path: ${worktreePath}
|
|
297
|
-
`));
|
|
298
|
-
const spinner = ora("Creating worktree...").start();
|
|
299
|
-
try {
|
|
300
|
-
await createWorktree(worktreePath, branchName);
|
|
301
|
-
spinner.succeed(chalk.green("Worktree created successfully"));
|
|
302
|
-
} catch (error) {
|
|
303
|
-
spinner.fail(chalk.red("Failed to create worktree"));
|
|
304
|
-
console.error(chalk.red(error.message || error));
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
307
|
-
if (options.push) {
|
|
308
|
-
const pushSpinner = ora("Pushing branch to remote...").start();
|
|
309
|
-
try {
|
|
310
|
-
await pushBranch(branchName, worktreePath);
|
|
311
|
-
pushSpinner.succeed(chalk.green(`Pushed ${branchName} to origin`));
|
|
312
|
-
} catch (error) {
|
|
313
|
-
pushSpinner.fail(chalk.yellow(`Could not push: ${error.message}`));
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const envSpinner = ora("Copying .env files...").start();
|
|
317
|
-
try {
|
|
318
|
-
const copiedFiles = await copyEnvFiles(mainRepoPath, worktreePath);
|
|
319
|
-
if (copiedFiles.length > 0) {
|
|
320
|
-
envSpinner.succeed(chalk.green(`Copied ${copiedFiles.length} env file(s): ${copiedFiles.join(", ")}`));
|
|
321
|
-
} else {
|
|
322
|
-
envSpinner.info(chalk.yellow("No .env files found to copy"));
|
|
323
|
-
}
|
|
324
|
-
} catch (error) {
|
|
325
|
-
envSpinner.warn(chalk.yellow(`Warning: Could not copy env files: ${error.message}`));
|
|
326
|
-
}
|
|
327
|
-
if (options.install) {
|
|
328
|
-
const packageManager = await detectPackageManager(worktreePath);
|
|
329
|
-
if (packageManager) {
|
|
330
|
-
const installSpinner = ora(`Running ${packageManager} install...`).start();
|
|
331
|
-
try {
|
|
332
|
-
await runInstall(worktreePath, packageManager);
|
|
333
|
-
installSpinner.succeed(chalk.green(`${packageManager} install completed`));
|
|
334
|
-
} catch (error) {
|
|
335
|
-
installSpinner.fail(chalk.red(`${packageManager} install failed: ${error.message}`));
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
const packageManager = await detectPackageManager(worktreePath);
|
|
340
|
-
if (packageManager) {
|
|
341
|
-
console.log(chalk.dim(`
|
|
342
|
-
Tip: Run '${packageManager} install' in the worktree, or use 'wt new --install' next time`));
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (options.skipLaunch) {
|
|
346
|
-
console.log(chalk.green(`
|
|
347
|
-
\u2713 Worktree ready at: ${worktreePath}`));
|
|
348
|
-
console.log(chalk.dim(` cd "${worktreePath}"`));
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
console.log("");
|
|
352
|
-
const selectedTool = await selectAITool();
|
|
353
|
-
const toolAvailable = await isToolAvailable(selectedTool);
|
|
354
|
-
if (!toolAvailable) {
|
|
355
|
-
console.error(chalk.red(`
|
|
356
|
-
Error: ${selectedTool} is not installed or not in PATH`));
|
|
357
|
-
console.log(chalk.dim(`Worktree is ready at: ${worktreePath}`));
|
|
358
|
-
console.log(chalk.dim(`You can manually launch your AI tool there.`));
|
|
359
|
-
process.exit(1);
|
|
360
|
-
}
|
|
361
|
-
console.log(chalk.cyan(`
|
|
362
|
-
Launching ${selectedTool} in worktree...`));
|
|
363
|
-
launchAITool({
|
|
364
|
-
cwd: worktreePath,
|
|
365
|
-
tool: selectedTool
|
|
366
|
-
});
|
|
367
|
-
console.log(chalk.green(`
|
|
368
|
-
\u2713 ${selectedTool} launched in: ${worktreePath}`));
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export {
|
|
372
|
-
isGitRepo,
|
|
373
|
-
getGitRoot,
|
|
374
|
-
remoteBranchExists,
|
|
375
|
-
listWorktrees,
|
|
376
|
-
removeWorktree,
|
|
377
|
-
pruneWorktrees,
|
|
378
|
-
isBranchMerged,
|
|
379
|
-
findWorktree,
|
|
380
|
-
launchAITool,
|
|
381
|
-
isToolAvailable,
|
|
382
|
-
selectAITool,
|
|
383
|
-
confirm,
|
|
384
|
-
selectMultiple,
|
|
385
|
-
newCommand
|
|
386
|
-
};
|