@simonfestl/husky-cli 0.5.2 โ 0.6.1
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 +23 -0
- package/dist/commands/interactive/worktrees.d.ts +6 -0
- package/dist/commands/interactive/worktrees.js +354 -0
- package/dist/commands/interactive.js +5 -0
- package/dist/commands/roadmap.js +107 -0
- 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
package/README.md
CHANGED
|
@@ -160,6 +160,8 @@ husky roadmap create "Q1 2025" --description "..."
|
|
|
160
160
|
husky roadmap update <roadmap-id> --name "Updated"
|
|
161
161
|
husky roadmap delete <roadmap-id>
|
|
162
162
|
husky roadmap add-phase <roadmap-id> --name "Phase 1"
|
|
163
|
+
husky roadmap update-phase <roadmap-id> <phase-id> --name "Updated"
|
|
164
|
+
husky roadmap delete-phase <roadmap-id> <phase-id> --force
|
|
163
165
|
husky roadmap add-feature <roadmap-id> --phase <id> --title "Feature"
|
|
164
166
|
husky roadmap list-features <roadmap-id>
|
|
165
167
|
husky roadmap update-feature <roadmap-id> <feature-id> --status done
|
|
@@ -187,6 +189,20 @@ husky vm-config update <config-id> --machine-type e2-standard-2
|
|
|
187
189
|
husky vm-config delete <config-id>
|
|
188
190
|
```
|
|
189
191
|
|
|
192
|
+
### Git Worktree Management
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
husky worktree list # List all worktrees
|
|
196
|
+
husky worktree create <session-name> # Create worktree
|
|
197
|
+
husky worktree info <session-name> # Show details
|
|
198
|
+
husky worktree status [session-name] # Show status
|
|
199
|
+
husky worktree cd <session-name> # Print path
|
|
200
|
+
husky worktree merge <session-name> # Merge to base
|
|
201
|
+
husky worktree remove <session-name> # Remove worktree
|
|
202
|
+
husky worktree branches # List husky/* branches
|
|
203
|
+
husky worktree cleanup # Clean stale worktrees
|
|
204
|
+
```
|
|
205
|
+
|
|
190
206
|
### Settings
|
|
191
207
|
|
|
192
208
|
```bash
|
|
@@ -283,6 +299,13 @@ husky --version
|
|
|
283
299
|
|
|
284
300
|
## Changelog
|
|
285
301
|
|
|
302
|
+
### v0.6.0 (2026-01-06)
|
|
303
|
+
- Added: Git Worktree support for multi-agent isolation
|
|
304
|
+
- Added: `husky worktree` commands (create, list, merge, remove, etc.)
|
|
305
|
+
- Added: MergeLock mechanism for safe concurrent operations
|
|
306
|
+
- Refactored: Interactive mode into modular components
|
|
307
|
+
- Improved: Based on Auto-Claude's worktree architecture
|
|
308
|
+
|
|
286
309
|
### v0.5.0 (2026-01-06)
|
|
287
310
|
- Full Dashboard feature parity (69 new commands)
|
|
288
311
|
- Added: project, workflow, idea, department, vm, jules, process, strategy, settings, vm-config commands
|
|
@@ -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
|
+
}
|
|
@@ -12,6 +12,7 @@ import { julesSessionsMenu } from "./interactive/jules-sessions.js";
|
|
|
12
12
|
import { roadmapsMenu } from "./interactive/roadmaps.js";
|
|
13
13
|
import { strategyMenu } from "./interactive/strategy.js";
|
|
14
14
|
import { changelogMenu } from "./interactive/changelog.js";
|
|
15
|
+
import { worktreesMenu } from "./interactive/worktrees.js";
|
|
15
16
|
import { clearScreen, printHeader, pressEnterToContinue } from "./interactive/utils.js";
|
|
16
17
|
// ============================================
|
|
17
18
|
// MAIN MENU
|
|
@@ -33,6 +34,7 @@ export async function runInteractiveMode() {
|
|
|
33
34
|
{ name: "---", value: "separator2", description: "" },
|
|
34
35
|
{ name: "VM Sessions", value: "vm", description: "Manage VM sessions" },
|
|
35
36
|
{ name: "Jules Sessions", value: "jules", description: "Manage Jules AI sessions" },
|
|
37
|
+
{ name: "Worktrees", value: "worktrees", description: "Manage Git worktrees for agent isolation" },
|
|
36
38
|
{ name: "---", value: "separator3", description: "" },
|
|
37
39
|
{ name: "Business Strategy", value: "strategy", description: "Manage business strategy" },
|
|
38
40
|
{ name: "Changelog", value: "changelog", description: "Generate and manage changelogs" },
|
|
@@ -74,6 +76,9 @@ export async function runInteractiveMode() {
|
|
|
74
76
|
case "jules":
|
|
75
77
|
await julesSessionsMenu();
|
|
76
78
|
break;
|
|
79
|
+
case "worktrees":
|
|
80
|
+
await worktreesMenu();
|
|
81
|
+
break;
|
|
77
82
|
case "strategy":
|
|
78
83
|
await strategyMenu();
|
|
79
84
|
break;
|
package/dist/commands/roadmap.js
CHANGED
|
@@ -152,6 +152,113 @@ roadmapCommand
|
|
|
152
152
|
process.exit(1);
|
|
153
153
|
}
|
|
154
154
|
});
|
|
155
|
+
// husky roadmap update-phase <roadmapId> <phaseId>
|
|
156
|
+
roadmapCommand
|
|
157
|
+
.command("update-phase <roadmapId> <phaseId>")
|
|
158
|
+
.description("Update a roadmap phase")
|
|
159
|
+
.option("-n, --name <name>", "New phase name")
|
|
160
|
+
.option("-d, --description <description>", "New phase description")
|
|
161
|
+
.option("--status <status>", "Phase status (planned, in_progress, completed)")
|
|
162
|
+
.option("--order <order>", "Phase order (0-based)")
|
|
163
|
+
.action(async (roadmapId, phaseId, options) => {
|
|
164
|
+
const config = ensureConfig();
|
|
165
|
+
// Build update payload
|
|
166
|
+
const updateData = { phaseId };
|
|
167
|
+
if (options.name)
|
|
168
|
+
updateData.name = options.name;
|
|
169
|
+
if (options.description)
|
|
170
|
+
updateData.description = options.description;
|
|
171
|
+
if (options.status) {
|
|
172
|
+
const validStatuses = ["planned", "in_progress", "completed"];
|
|
173
|
+
if (!validStatuses.includes(options.status)) {
|
|
174
|
+
console.error(`Error: Invalid status "${options.status}". Must be one of: ${validStatuses.join(", ")}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
updateData.status = options.status;
|
|
178
|
+
}
|
|
179
|
+
if (options.order !== undefined) {
|
|
180
|
+
const order = parseInt(options.order, 10);
|
|
181
|
+
if (isNaN(order) || order < 0) {
|
|
182
|
+
console.error("Error: Order must be a non-negative integer");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
updateData.order = order;
|
|
186
|
+
}
|
|
187
|
+
if (Object.keys(updateData).length === 1) {
|
|
188
|
+
console.error("Error: No update options provided. Use -n/--name, -d/--description, --status, or --order");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const res = await fetch(`${config.apiUrl}/api/roadmaps/${roadmapId}/phases`, {
|
|
193
|
+
method: "PATCH",
|
|
194
|
+
headers: {
|
|
195
|
+
"Content-Type": "application/json",
|
|
196
|
+
...(config.apiKey ? { "x-api-key": config.apiKey } : {}),
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify(updateData),
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok) {
|
|
201
|
+
if (res.status === 404) {
|
|
202
|
+
console.error(`Error: Roadmap or phase not found`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
206
|
+
console.error(`Error: API returned ${res.status}`, errorBody.error || "");
|
|
207
|
+
}
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
const roadmap = await res.json();
|
|
211
|
+
const updatedPhase = roadmap.phases.find((p) => p.id === phaseId);
|
|
212
|
+
console.log(`โ Phase updated successfully`);
|
|
213
|
+
if (updatedPhase) {
|
|
214
|
+
console.log(` Name: ${updatedPhase.name}`);
|
|
215
|
+
console.log(` Status: ${updatedPhase.status}`);
|
|
216
|
+
console.log(` Order: ${updatedPhase.order}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
console.error("Error updating phase:", error);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
// husky roadmap delete-phase <roadmapId> <phaseId>
|
|
225
|
+
roadmapCommand
|
|
226
|
+
.command("delete-phase <roadmapId> <phaseId>")
|
|
227
|
+
.description("Delete a phase from a roadmap (including all its features)")
|
|
228
|
+
.option("--force", "Skip confirmation")
|
|
229
|
+
.action(async (roadmapId, phaseId, options) => {
|
|
230
|
+
const config = ensureConfig();
|
|
231
|
+
if (!options.force) {
|
|
232
|
+
console.log("Warning: This will permanently delete the phase and all its features.");
|
|
233
|
+
console.log("Use --force to confirm deletion.");
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
const url = new URL(`/api/roadmaps/${roadmapId}/phases`, config.apiUrl);
|
|
238
|
+
url.searchParams.set("phaseId", phaseId);
|
|
239
|
+
const res = await fetch(url.toString(), {
|
|
240
|
+
method: "DELETE",
|
|
241
|
+
headers: config.apiKey ? { "x-api-key": config.apiKey } : {},
|
|
242
|
+
});
|
|
243
|
+
if (!res.ok) {
|
|
244
|
+
if (res.status === 404) {
|
|
245
|
+
console.error(`Error: Roadmap or phase not found`);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
249
|
+
console.error(`Error: API returned ${res.status}`, errorBody.error || "");
|
|
250
|
+
}
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const roadmap = await res.json();
|
|
254
|
+
console.log(`โ Phase deleted`);
|
|
255
|
+
console.log(` Remaining phases: ${roadmap.phases.length}`);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.error("Error deleting phase:", error);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
155
262
|
// husky roadmap add-feature <roadmapId> <phaseId> <title>
|
|
156
263
|
roadmapCommand
|
|
157
264
|
.command("add-feature <roadmapId> <phaseId> <title>")
|