@simonfestl/husky-cli 0.5.1 โ 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/config.js +4 -3
- package/dist/commands/idea.js +9 -7
- package/dist/commands/interactive/changelog.d.ts +1 -0
- package/dist/commands/interactive/changelog.js +398 -0
- package/dist/commands/interactive/departments.d.ts +1 -0
- package/dist/commands/interactive/departments.js +242 -0
- package/dist/commands/interactive/ideas.d.ts +1 -0
- package/dist/commands/interactive/ideas.js +311 -0
- package/dist/commands/interactive/jules-sessions.d.ts +1 -0
- package/dist/commands/interactive/jules-sessions.js +460 -0
- package/dist/commands/interactive/processes.d.ts +1 -0
- package/dist/commands/interactive/processes.js +271 -0
- package/dist/commands/interactive/projects.d.ts +1 -0
- package/dist/commands/interactive/projects.js +297 -0
- package/dist/commands/interactive/roadmaps.d.ts +1 -0
- package/dist/commands/interactive/roadmaps.js +650 -0
- package/dist/commands/interactive/strategy.d.ts +1 -0
- package/dist/commands/interactive/strategy.js +790 -0
- package/dist/commands/interactive/tasks.d.ts +1 -0
- package/dist/commands/interactive/tasks.js +415 -0
- package/dist/commands/interactive/utils.d.ts +15 -0
- package/dist/commands/interactive/utils.js +54 -0
- package/dist/commands/interactive/vm-sessions.d.ts +1 -0
- package/dist/commands/interactive/vm-sessions.js +319 -0
- package/dist/commands/interactive/workflows.d.ts +1 -0
- package/dist/commands/interactive/workflows.js +442 -0
- package/dist/commands/interactive/worktrees.d.ts +6 -0
- package/dist/commands/interactive/worktrees.js +354 -0
- package/dist/commands/interactive.js +118 -1208
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +404 -0
- package/dist/index.js +3 -1
- package/dist/lib/merge-lock.d.ts +83 -0
- package/dist/lib/merge-lock.js +242 -0
- package/dist/lib/worktree.d.ts +133 -0
- package/dist/lib/worktree.js +473 -0
- package/package.json +1 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive Mode: Worktrees Module
|
|
3
|
+
*
|
|
4
|
+
* Provides menu-based worktree management for isolated agent workspaces.
|
|
5
|
+
*/
|
|
6
|
+
import { select, input, confirm } from "@inquirer/prompts";
|
|
7
|
+
import { WorktreeManager } from "../../lib/worktree.js";
|
|
8
|
+
import { MergeLock, withMergeLock } from "../../lib/merge-lock.js";
|
|
9
|
+
import { pressEnterToContinue, truncate } from "./utils.js";
|
|
10
|
+
let manager;
|
|
11
|
+
function getManager() {
|
|
12
|
+
if (!manager) {
|
|
13
|
+
manager = new WorktreeManager(process.cwd());
|
|
14
|
+
}
|
|
15
|
+
return manager;
|
|
16
|
+
}
|
|
17
|
+
export async function worktreesMenu() {
|
|
18
|
+
const mgr = getManager();
|
|
19
|
+
const worktrees = mgr.listWorktrees();
|
|
20
|
+
console.log("\n WORKTREES");
|
|
21
|
+
console.log(" " + "-".repeat(50));
|
|
22
|
+
console.log(` Base branch: ${mgr.getBaseBranch()}`);
|
|
23
|
+
console.log(` Active worktrees: ${worktrees.length}`);
|
|
24
|
+
console.log("");
|
|
25
|
+
const menuItems = [
|
|
26
|
+
{ name: "๐ List Worktrees", value: "list" },
|
|
27
|
+
{ name: "โ Create Worktree", value: "create" },
|
|
28
|
+
{ name: "๐ View Worktree Details", value: "view" },
|
|
29
|
+
{ name: "๐ Show Status", value: "status" },
|
|
30
|
+
{ name: "๐ Merge Worktree", value: "merge" },
|
|
31
|
+
{ name: "๐๏ธ Remove Worktree", value: "remove" },
|
|
32
|
+
{ name: "๐งน Cleanup Stale", value: "cleanup" },
|
|
33
|
+
{ name: "๐ฟ List Branches", value: "branches" },
|
|
34
|
+
{ name: "โ Back", value: "back" },
|
|
35
|
+
];
|
|
36
|
+
const choice = await select({
|
|
37
|
+
message: "Worktree Action:",
|
|
38
|
+
choices: menuItems,
|
|
39
|
+
});
|
|
40
|
+
switch (choice) {
|
|
41
|
+
case "list":
|
|
42
|
+
await listWorktrees();
|
|
43
|
+
break;
|
|
44
|
+
case "create":
|
|
45
|
+
await createWorktree();
|
|
46
|
+
break;
|
|
47
|
+
case "view":
|
|
48
|
+
await viewWorktree();
|
|
49
|
+
break;
|
|
50
|
+
case "status":
|
|
51
|
+
await showStatus();
|
|
52
|
+
break;
|
|
53
|
+
case "merge":
|
|
54
|
+
await mergeWorktree();
|
|
55
|
+
break;
|
|
56
|
+
case "remove":
|
|
57
|
+
await removeWorktree();
|
|
58
|
+
break;
|
|
59
|
+
case "cleanup":
|
|
60
|
+
await cleanup();
|
|
61
|
+
break;
|
|
62
|
+
case "branches":
|
|
63
|
+
await listBranches();
|
|
64
|
+
break;
|
|
65
|
+
case "back":
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async function listWorktrees() {
|
|
70
|
+
const mgr = getManager();
|
|
71
|
+
const worktrees = mgr.listWorktrees();
|
|
72
|
+
console.log("\n WORKTREES");
|
|
73
|
+
console.log(" " + "-".repeat(70));
|
|
74
|
+
if (worktrees.length === 0) {
|
|
75
|
+
console.log(" No worktrees found.");
|
|
76
|
+
console.log(" Create one with the 'Create Worktree' option.");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.log(` ${"SESSION".padEnd(20)} ${"BRANCH".padEnd(25)} ${"COMMITS".padEnd(8)} ${"CHANGES"}`);
|
|
80
|
+
console.log(" " + "-".repeat(70));
|
|
81
|
+
for (const wt of worktrees) {
|
|
82
|
+
const changes = `+${wt.stats.additions}/-${wt.stats.deletions} (${wt.stats.filesChanged} files)`;
|
|
83
|
+
console.log(` ${truncate(wt.sessionName, 18).padEnd(20)} ${truncate(wt.branch, 23).padEnd(25)} ${String(wt.stats.commitCount).padEnd(8)} ${changes}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
console.log("");
|
|
87
|
+
await pressEnterToContinue();
|
|
88
|
+
}
|
|
89
|
+
async function createWorktree() {
|
|
90
|
+
const mgr = getManager();
|
|
91
|
+
const sessionName = await input({
|
|
92
|
+
message: "Session name:",
|
|
93
|
+
validate: (val) => {
|
|
94
|
+
if (!val.trim())
|
|
95
|
+
return "Session name is required";
|
|
96
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(val)) {
|
|
97
|
+
return "Session name can only contain letters, numbers, hyphens, and underscores";
|
|
98
|
+
}
|
|
99
|
+
if (mgr.worktreeExists(val)) {
|
|
100
|
+
return "A worktree with this name already exists";
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const baseBranch = await input({
|
|
106
|
+
message: "Base branch (leave empty for default):",
|
|
107
|
+
default: mgr.getBaseBranch(),
|
|
108
|
+
});
|
|
109
|
+
try {
|
|
110
|
+
// Recreate manager with custom base branch if specified
|
|
111
|
+
const actualManager = baseBranch !== mgr.getBaseBranch()
|
|
112
|
+
? new WorktreeManager(process.cwd(), baseBranch)
|
|
113
|
+
: mgr;
|
|
114
|
+
const info = actualManager.createWorktree(sessionName);
|
|
115
|
+
console.log("\n โ Worktree created successfully!");
|
|
116
|
+
console.log(` Session: ${info.sessionName}`);
|
|
117
|
+
console.log(` Branch: ${info.branch}`);
|
|
118
|
+
console.log(` Path: ${info.path}`);
|
|
119
|
+
console.log("\n To work in this worktree:");
|
|
120
|
+
console.log(` cd ${info.path}`);
|
|
121
|
+
console.log("");
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error("\n โ Error creating worktree:", error instanceof Error ? error.message : error);
|
|
125
|
+
}
|
|
126
|
+
await pressEnterToContinue();
|
|
127
|
+
}
|
|
128
|
+
async function selectWorktree(message) {
|
|
129
|
+
const mgr = getManager();
|
|
130
|
+
const worktrees = mgr.listWorktrees();
|
|
131
|
+
if (worktrees.length === 0) {
|
|
132
|
+
console.log("\n No worktrees found.");
|
|
133
|
+
await pressEnterToContinue();
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const choices = worktrees.map((wt) => ({
|
|
137
|
+
name: `${wt.sessionName} (${wt.branch})`,
|
|
138
|
+
value: wt.sessionName,
|
|
139
|
+
}));
|
|
140
|
+
choices.push({ name: "โ Cancel", value: "__cancel__" });
|
|
141
|
+
const sessionName = await select({
|
|
142
|
+
message,
|
|
143
|
+
choices,
|
|
144
|
+
});
|
|
145
|
+
if (sessionName === "__cancel__")
|
|
146
|
+
return null;
|
|
147
|
+
return mgr.getWorktree(sessionName);
|
|
148
|
+
}
|
|
149
|
+
async function viewWorktree() {
|
|
150
|
+
const mgr = getManager();
|
|
151
|
+
const info = await selectWorktree("Select worktree to view:");
|
|
152
|
+
if (!info)
|
|
153
|
+
return;
|
|
154
|
+
const changedFiles = mgr.getChangedFiles(info.sessionName);
|
|
155
|
+
const hasUncommitted = mgr.hasUncommittedChanges(info.sessionName);
|
|
156
|
+
console.log(`\n Worktree: ${info.sessionName}`);
|
|
157
|
+
console.log(" " + "-".repeat(60));
|
|
158
|
+
console.log(` Path: ${info.path}`);
|
|
159
|
+
console.log(` Branch: ${info.branch}`);
|
|
160
|
+
console.log(` Base: ${info.baseBranch}`);
|
|
161
|
+
console.log(` Active: ${info.isActive ? "Yes" : "No"}`);
|
|
162
|
+
console.log(`\n Statistics:`);
|
|
163
|
+
console.log(` Commits: ${info.stats.commitCount}`);
|
|
164
|
+
console.log(` Files: ${info.stats.filesChanged}`);
|
|
165
|
+
console.log(` Added: +${info.stats.additions}`);
|
|
166
|
+
console.log(` Removed: -${info.stats.deletions}`);
|
|
167
|
+
if (hasUncommitted) {
|
|
168
|
+
console.log(`\n โ Has uncommitted changes`);
|
|
169
|
+
}
|
|
170
|
+
if (changedFiles.length > 0) {
|
|
171
|
+
console.log(`\n Changed files:`);
|
|
172
|
+
const maxFiles = 15;
|
|
173
|
+
for (const file of changedFiles.slice(0, maxFiles)) {
|
|
174
|
+
const statusLabel = file.status === "A" ? "[new]" : file.status === "D" ? "[del]" : "[mod]";
|
|
175
|
+
console.log(` ${statusLabel.padEnd(6)} ${file.file}`);
|
|
176
|
+
}
|
|
177
|
+
if (changedFiles.length > maxFiles) {
|
|
178
|
+
console.log(` ... and ${changedFiles.length - maxFiles} more`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.log("");
|
|
182
|
+
await pressEnterToContinue();
|
|
183
|
+
}
|
|
184
|
+
async function showStatus() {
|
|
185
|
+
const mgr = getManager();
|
|
186
|
+
const worktrees = mgr.listWorktrees();
|
|
187
|
+
console.log("\n WORKTREE STATUS");
|
|
188
|
+
console.log(" " + "-".repeat(50));
|
|
189
|
+
if (worktrees.length === 0) {
|
|
190
|
+
console.log(" No worktrees found.");
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
for (const wt of worktrees) {
|
|
194
|
+
const hasUncommitted = mgr.hasUncommittedChanges(wt.sessionName);
|
|
195
|
+
const statusIcon = hasUncommitted ? "โ" : "โ";
|
|
196
|
+
const changes = `+${wt.stats.additions}/-${wt.stats.deletions}`;
|
|
197
|
+
console.log(`\n ${statusIcon} ${wt.sessionName}`);
|
|
198
|
+
console.log(` Branch: ${wt.branch}`);
|
|
199
|
+
console.log(` Commits: ${wt.stats.commitCount} | Files: ${wt.stats.filesChanged} | ${changes}`);
|
|
200
|
+
if (hasUncommitted) {
|
|
201
|
+
console.log(` โ Uncommitted changes`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
console.log("");
|
|
206
|
+
await pressEnterToContinue();
|
|
207
|
+
}
|
|
208
|
+
async function mergeWorktree() {
|
|
209
|
+
const mgr = getManager();
|
|
210
|
+
const worktrees = mgr.listWorktrees();
|
|
211
|
+
// Filter to worktrees with commits
|
|
212
|
+
const mergeableWorktrees = worktrees.filter((wt) => wt.stats.commitCount > 0);
|
|
213
|
+
if (mergeableWorktrees.length === 0) {
|
|
214
|
+
console.log("\n No worktrees have commits to merge.");
|
|
215
|
+
await pressEnterToContinue();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const choices = mergeableWorktrees.map((wt) => ({
|
|
219
|
+
name: `${wt.sessionName} (${wt.stats.commitCount} commits, +${wt.stats.additions}/-${wt.stats.deletions})`,
|
|
220
|
+
value: wt.sessionName,
|
|
221
|
+
}));
|
|
222
|
+
choices.push({ name: "โ Cancel", value: "__cancel__" });
|
|
223
|
+
const sessionName = await select({
|
|
224
|
+
message: "Select worktree to merge:",
|
|
225
|
+
choices,
|
|
226
|
+
});
|
|
227
|
+
if (sessionName === "__cancel__")
|
|
228
|
+
return;
|
|
229
|
+
const noCommit = await confirm({
|
|
230
|
+
message: "Stage only (no commit)?",
|
|
231
|
+
default: false,
|
|
232
|
+
});
|
|
233
|
+
const deleteAfter = await confirm({
|
|
234
|
+
message: "Delete worktree after successful merge?",
|
|
235
|
+
default: false,
|
|
236
|
+
});
|
|
237
|
+
const info = mgr.getWorktree(sessionName);
|
|
238
|
+
if (!info) {
|
|
239
|
+
console.log("\n Worktree not found.");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
console.log(`\n Merging ${info.branch} into ${info.baseBranch}...`);
|
|
244
|
+
const success = await withMergeLock(process.cwd(), sessionName, async () => {
|
|
245
|
+
return mgr.mergeWorktree(sessionName, {
|
|
246
|
+
noCommit,
|
|
247
|
+
deleteAfter,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
if (success) {
|
|
251
|
+
console.log("\n โ Merge successful!");
|
|
252
|
+
if (noCommit) {
|
|
253
|
+
console.log(" Changes are staged. Review and commit when ready.");
|
|
254
|
+
}
|
|
255
|
+
if (deleteAfter) {
|
|
256
|
+
console.log(" Worktree and branch have been removed.");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
console.log("\n โ Merge failed. There may be conflicts.");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.error("\n โ Error during merge:", error instanceof Error ? error.message : error);
|
|
265
|
+
}
|
|
266
|
+
console.log("");
|
|
267
|
+
await pressEnterToContinue();
|
|
268
|
+
}
|
|
269
|
+
async function removeWorktree() {
|
|
270
|
+
const mgr = getManager();
|
|
271
|
+
const worktrees = mgr.listWorktrees();
|
|
272
|
+
if (worktrees.length === 0) {
|
|
273
|
+
console.log("\n No worktrees found.");
|
|
274
|
+
await pressEnterToContinue();
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const choices = worktrees.map((wt) => {
|
|
278
|
+
const hasUncommitted = mgr.hasUncommittedChanges(wt.sessionName);
|
|
279
|
+
const warning = hasUncommitted ? " โ " : "";
|
|
280
|
+
return {
|
|
281
|
+
name: `${wt.sessionName} (${wt.branch})${warning}`,
|
|
282
|
+
value: wt.sessionName,
|
|
283
|
+
};
|
|
284
|
+
});
|
|
285
|
+
choices.push({ name: "โ Cancel", value: "__cancel__" });
|
|
286
|
+
const sessionName = await select({
|
|
287
|
+
message: "Select worktree to remove:",
|
|
288
|
+
choices,
|
|
289
|
+
});
|
|
290
|
+
if (sessionName === "__cancel__")
|
|
291
|
+
return;
|
|
292
|
+
const deleteBranch = await confirm({
|
|
293
|
+
message: "Also delete the branch?",
|
|
294
|
+
default: false,
|
|
295
|
+
});
|
|
296
|
+
const shouldContinue = await confirm({
|
|
297
|
+
message: "Are you sure?",
|
|
298
|
+
default: false,
|
|
299
|
+
});
|
|
300
|
+
if (!shouldContinue) {
|
|
301
|
+
console.log("\n Cancelled.");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const info = mgr.getWorktree(sessionName);
|
|
305
|
+
try {
|
|
306
|
+
mgr.removeWorktree(sessionName, deleteBranch);
|
|
307
|
+
console.log(`\n โ Worktree removed: ${sessionName}`);
|
|
308
|
+
if (deleteBranch && info) {
|
|
309
|
+
console.log(` Branch deleted: ${info.branch}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
console.error("\n โ Error removing worktree:", error instanceof Error ? error.message : error);
|
|
314
|
+
}
|
|
315
|
+
await pressEnterToContinue();
|
|
316
|
+
}
|
|
317
|
+
async function cleanup() {
|
|
318
|
+
const mgr = getManager();
|
|
319
|
+
const projectDir = process.cwd();
|
|
320
|
+
console.log("\n Cleaning up stale worktrees and locks...");
|
|
321
|
+
try {
|
|
322
|
+
mgr.cleanupStale();
|
|
323
|
+
const staleLocks = MergeLock.cleanupStale(projectDir);
|
|
324
|
+
console.log("\n โ Cleanup complete");
|
|
325
|
+
if (staleLocks > 0) {
|
|
326
|
+
console.log(` Removed ${staleLocks} stale lock(s)`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
console.error("\n โ Error during cleanup:", error instanceof Error ? error.message : error);
|
|
331
|
+
}
|
|
332
|
+
await pressEnterToContinue();
|
|
333
|
+
}
|
|
334
|
+
async function listBranches() {
|
|
335
|
+
const mgr = getManager();
|
|
336
|
+
const branches = mgr.listBranches();
|
|
337
|
+
const worktrees = mgr.listWorktrees();
|
|
338
|
+
const worktreeSessionNames = new Set(worktrees.map((w) => w.sessionName));
|
|
339
|
+
console.log("\n HUSKY BRANCHES");
|
|
340
|
+
console.log(" " + "-".repeat(50));
|
|
341
|
+
if (branches.length === 0) {
|
|
342
|
+
console.log(" No husky/* branches found.");
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
for (const branch of branches) {
|
|
346
|
+
const sessionName = branch.replace("husky/", "");
|
|
347
|
+
const hasWorktree = worktreeSessionNames.has(sessionName);
|
|
348
|
+
const marker = hasWorktree ? " [worktree]" : "";
|
|
349
|
+
console.log(` ${branch}${marker}`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
console.log("");
|
|
353
|
+
await pressEnterToContinue();
|
|
354
|
+
}
|