@simonfestl/husky-cli 0.5.2 → 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/interactive/worktrees.d.ts +6 -0
- package/dist/commands/interactive/worktrees.js +354 -0
- package/dist/commands/interactive.js +5 -0
- package/dist/commands/worktree.d.ts +2 -0
- package/dist/commands/worktree.js +404 -0
- package/dist/index.js +2 -0
- 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,404 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { WorktreeManager } from "../lib/worktree.js";
|
|
4
|
+
import { MergeLock, withMergeLock } from "../lib/merge-lock.js";
|
|
5
|
+
export const worktreeCommand = new Command("worktree")
|
|
6
|
+
.description("Manage Git worktrees for isolated agent workspaces");
|
|
7
|
+
/**
|
|
8
|
+
* Get the project directory (current working directory or specified).
|
|
9
|
+
*/
|
|
10
|
+
function getProjectDir(options) {
|
|
11
|
+
return options.project ? path.resolve(options.project) : process.cwd();
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Get a WorktreeManager instance.
|
|
15
|
+
*/
|
|
16
|
+
function getManager(options) {
|
|
17
|
+
const projectDir = getProjectDir(options);
|
|
18
|
+
return new WorktreeManager(projectDir, options.baseBranch);
|
|
19
|
+
}
|
|
20
|
+
// husky worktree create <session-name>
|
|
21
|
+
worktreeCommand
|
|
22
|
+
.command("create <session-name>")
|
|
23
|
+
.description("Create a new worktree for a session")
|
|
24
|
+
.option("-b, --base-branch <branch>", "Base branch to create from (default: main/master)")
|
|
25
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
26
|
+
.option("--json", "Output as JSON")
|
|
27
|
+
.action(async (sessionName, options) => {
|
|
28
|
+
try {
|
|
29
|
+
const manager = getManager(options);
|
|
30
|
+
const info = manager.createWorktree(sessionName);
|
|
31
|
+
if (options.json) {
|
|
32
|
+
console.log(JSON.stringify(info, null, 2));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
console.log(`\nWorktree created successfully!`);
|
|
36
|
+
console.log(` Session: ${info.sessionName}`);
|
|
37
|
+
console.log(` Branch: ${info.branch}`);
|
|
38
|
+
console.log(` Path: ${info.path}`);
|
|
39
|
+
console.log(`\nTo work in this worktree:`);
|
|
40
|
+
console.log(` cd ${info.path}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error("Error creating worktree:", error instanceof Error ? error.message : error);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// husky worktree list
|
|
49
|
+
worktreeCommand
|
|
50
|
+
.command("list")
|
|
51
|
+
.description("List all worktrees")
|
|
52
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
53
|
+
.option("--json", "Output as JSON")
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
try {
|
|
56
|
+
const manager = getManager(options);
|
|
57
|
+
const worktrees = manager.listWorktrees();
|
|
58
|
+
if (options.json) {
|
|
59
|
+
console.log(JSON.stringify({ worktrees, baseBranch: manager.getBaseBranch() }, null, 2));
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
printWorktreeList(worktrees, manager.getBaseBranch());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
console.error("Error listing worktrees:", error instanceof Error ? error.message : error);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
// husky worktree info <session-name>
|
|
71
|
+
worktreeCommand
|
|
72
|
+
.command("info <session-name>")
|
|
73
|
+
.description("Get detailed info about a worktree")
|
|
74
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
75
|
+
.option("--json", "Output as JSON")
|
|
76
|
+
.action(async (sessionName, options) => {
|
|
77
|
+
try {
|
|
78
|
+
const manager = getManager(options);
|
|
79
|
+
const info = manager.getWorktree(sessionName);
|
|
80
|
+
if (!info) {
|
|
81
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
const changedFiles = manager.getChangedFiles(sessionName);
|
|
85
|
+
const hasUncommitted = manager.hasUncommittedChanges(sessionName);
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify({ ...info, changedFiles, hasUncommittedChanges: hasUncommitted }, null, 2));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
printWorktreeDetail(info, changedFiles, hasUncommitted);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
console.error("Error getting worktree info:", error instanceof Error ? error.message : error);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// husky worktree cd <session-name>
|
|
99
|
+
worktreeCommand
|
|
100
|
+
.command("cd <session-name>")
|
|
101
|
+
.description("Print the path to a worktree (use with: cd $(husky worktree cd <name>))")
|
|
102
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
103
|
+
.action(async (sessionName, options) => {
|
|
104
|
+
try {
|
|
105
|
+
const manager = getManager(options);
|
|
106
|
+
const info = manager.getWorktree(sessionName);
|
|
107
|
+
if (!info) {
|
|
108
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
// Just print the path so it can be used with cd
|
|
112
|
+
console.log(info.path);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
// husky worktree status [session-name]
|
|
120
|
+
worktreeCommand
|
|
121
|
+
.command("status [session-name]")
|
|
122
|
+
.description("Show status of worktree(s)")
|
|
123
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
124
|
+
.option("--json", "Output as JSON")
|
|
125
|
+
.action(async (sessionName, options) => {
|
|
126
|
+
try {
|
|
127
|
+
const manager = getManager(options);
|
|
128
|
+
if (sessionName) {
|
|
129
|
+
// Single worktree status
|
|
130
|
+
const info = manager.getWorktree(sessionName);
|
|
131
|
+
if (!info) {
|
|
132
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
const changedFiles = manager.getChangedFiles(sessionName);
|
|
136
|
+
const hasUncommitted = manager.hasUncommittedChanges(sessionName);
|
|
137
|
+
if (options.json) {
|
|
138
|
+
console.log(JSON.stringify({ sessionName, changedFiles, hasUncommittedChanges: hasUncommitted, stats: info.stats }, null, 2));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
printWorktreeStatus(info, changedFiles, hasUncommitted);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// All worktrees status
|
|
146
|
+
const worktrees = manager.listWorktrees();
|
|
147
|
+
const statuses = worktrees.map((wt) => ({
|
|
148
|
+
...wt,
|
|
149
|
+
changedFiles: manager.getChangedFiles(wt.sessionName),
|
|
150
|
+
hasUncommittedChanges: manager.hasUncommittedChanges(wt.sessionName),
|
|
151
|
+
}));
|
|
152
|
+
if (options.json) {
|
|
153
|
+
console.log(JSON.stringify({ worktrees: statuses }, null, 2));
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
for (const status of statuses) {
|
|
157
|
+
printWorktreeStatus(status, status.changedFiles, status.hasUncommittedChanges);
|
|
158
|
+
console.log("");
|
|
159
|
+
}
|
|
160
|
+
if (statuses.length === 0) {
|
|
161
|
+
console.log("No worktrees found.");
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.error("Error getting status:", error instanceof Error ? error.message : error);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// husky worktree remove <session-name>
|
|
172
|
+
worktreeCommand
|
|
173
|
+
.command("remove <session-name>")
|
|
174
|
+
.description("Remove a worktree")
|
|
175
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
176
|
+
.option("--delete-branch", "Also delete the associated branch")
|
|
177
|
+
.option("--force", "Force removal even with uncommitted changes")
|
|
178
|
+
.option("--json", "Output as JSON")
|
|
179
|
+
.action(async (sessionName, options) => {
|
|
180
|
+
try {
|
|
181
|
+
const manager = getManager(options);
|
|
182
|
+
const info = manager.getWorktree(sessionName);
|
|
183
|
+
if (!info) {
|
|
184
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
// Check for uncommitted changes unless --force
|
|
188
|
+
if (!options.force && manager.hasUncommittedChanges(sessionName)) {
|
|
189
|
+
console.error(`Error: Worktree has uncommitted changes.`);
|
|
190
|
+
console.error(`Use --force to remove anyway, or commit/stash changes first.`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
manager.removeWorktree(sessionName, options.deleteBranch);
|
|
194
|
+
if (options.json) {
|
|
195
|
+
console.log(JSON.stringify({ removed: true, sessionName, branchDeleted: !!options.deleteBranch }, null, 2));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
console.log(`Worktree removed: ${sessionName}`);
|
|
199
|
+
if (options.deleteBranch) {
|
|
200
|
+
console.log(`Branch deleted: ${info.branch}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error("Error removing worktree:", error instanceof Error ? error.message : error);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// husky worktree merge <session-name>
|
|
210
|
+
worktreeCommand
|
|
211
|
+
.command("merge <session-name>")
|
|
212
|
+
.description("Merge a worktree branch back to base")
|
|
213
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
214
|
+
.option("--no-commit", "Stage changes but don't commit")
|
|
215
|
+
.option("--delete-after", "Remove worktree and branch after successful merge")
|
|
216
|
+
.option("-m, --message <message>", "Custom merge commit message")
|
|
217
|
+
.option("--json", "Output as JSON")
|
|
218
|
+
.action(async (sessionName, options) => {
|
|
219
|
+
try {
|
|
220
|
+
const projectDir = getProjectDir(options);
|
|
221
|
+
const manager = getManager(options);
|
|
222
|
+
const info = manager.getWorktree(sessionName);
|
|
223
|
+
if (!info) {
|
|
224
|
+
console.error(`Error: No worktree found for session: ${sessionName}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
// Use merge lock
|
|
228
|
+
const success = await withMergeLock(projectDir, sessionName, async () => {
|
|
229
|
+
return manager.mergeWorktree(sessionName, {
|
|
230
|
+
noCommit: options.noCommit,
|
|
231
|
+
deleteAfter: options.deleteAfter,
|
|
232
|
+
message: options.message,
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
if (options.json) {
|
|
236
|
+
console.log(JSON.stringify({ success, sessionName, branch: info.branch, baseBranch: info.baseBranch }, null, 2));
|
|
237
|
+
}
|
|
238
|
+
if (!success) {
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
console.error("Error merging worktree:", error instanceof Error ? error.message : error);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
// husky worktree cleanup
|
|
248
|
+
worktreeCommand
|
|
249
|
+
.command("cleanup")
|
|
250
|
+
.description("Clean up stale worktrees and locks")
|
|
251
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
252
|
+
.option("--all", "Remove ALL worktrees (with confirmation)")
|
|
253
|
+
.option("--force", "Skip confirmation for --all")
|
|
254
|
+
.option("--json", "Output as JSON")
|
|
255
|
+
.action(async (options) => {
|
|
256
|
+
try {
|
|
257
|
+
const projectDir = getProjectDir(options);
|
|
258
|
+
const manager = getManager(options);
|
|
259
|
+
if (options.all) {
|
|
260
|
+
const worktrees = manager.listWorktrees();
|
|
261
|
+
if (worktrees.length === 0) {
|
|
262
|
+
console.log("No worktrees to clean up.");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!options.force) {
|
|
266
|
+
console.log(`This will remove ${worktrees.length} worktree(s) and their branches:`);
|
|
267
|
+
for (const wt of worktrees) {
|
|
268
|
+
console.log(` - ${wt.sessionName} (${wt.branch})`);
|
|
269
|
+
}
|
|
270
|
+
console.log("\nUse --force to confirm, or remove worktrees individually.");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
manager.cleanupAll();
|
|
274
|
+
if (options.json) {
|
|
275
|
+
console.log(JSON.stringify({ cleaned: worktrees.length, type: "all" }, null, 2));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
console.log(`Removed ${worktrees.length} worktree(s)`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
// Just cleanup stale
|
|
283
|
+
manager.cleanupStale();
|
|
284
|
+
const staleLocks = MergeLock.cleanupStale(projectDir);
|
|
285
|
+
if (options.json) {
|
|
286
|
+
console.log(JSON.stringify({ staleLocksRemoved: staleLocks, type: "stale" }, null, 2));
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.log("Cleaned up stale worktrees and locks");
|
|
290
|
+
if (staleLocks > 0) {
|
|
291
|
+
console.log(` Removed ${staleLocks} stale lock(s)`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
console.error("Error during cleanup:", error instanceof Error ? error.message : error);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
// husky worktree branches
|
|
302
|
+
worktreeCommand
|
|
303
|
+
.command("branches")
|
|
304
|
+
.description("List all husky/* branches")
|
|
305
|
+
.option("-p, --project <path>", "Project directory (default: current directory)")
|
|
306
|
+
.option("--json", "Output as JSON")
|
|
307
|
+
.action(async (options) => {
|
|
308
|
+
try {
|
|
309
|
+
const manager = getManager(options);
|
|
310
|
+
const branches = manager.listBranches();
|
|
311
|
+
const worktrees = manager.listWorktrees();
|
|
312
|
+
const worktreeSessionNames = new Set(worktrees.map((w) => w.sessionName));
|
|
313
|
+
if (options.json) {
|
|
314
|
+
console.log(JSON.stringify({
|
|
315
|
+
branches: branches.map((b) => ({
|
|
316
|
+
name: b,
|
|
317
|
+
hasWorktree: worktreeSessionNames.has(b.replace("husky/", "")),
|
|
318
|
+
})),
|
|
319
|
+
}, null, 2));
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
console.log("\n HUSKY BRANCHES");
|
|
323
|
+
console.log(" " + "-".repeat(50));
|
|
324
|
+
if (branches.length === 0) {
|
|
325
|
+
console.log(" No husky/* branches found.");
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
for (const branch of branches) {
|
|
329
|
+
const sessionName = branch.replace("husky/", "");
|
|
330
|
+
const hasWorktree = worktreeSessionNames.has(sessionName);
|
|
331
|
+
const marker = hasWorktree ? " [worktree]" : "";
|
|
332
|
+
console.log(` ${branch}${marker}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
console.log("");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.error("Error listing branches:", error instanceof Error ? error.message : error);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
// Print helpers
|
|
344
|
+
function printWorktreeList(worktrees, baseBranch) {
|
|
345
|
+
console.log(`\n Base branch: ${baseBranch}`);
|
|
346
|
+
console.log(" " + "-".repeat(80));
|
|
347
|
+
if (worktrees.length === 0) {
|
|
348
|
+
console.log(" No worktrees found.");
|
|
349
|
+
console.log(" Create one with: husky worktree create <session-name>");
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
console.log(` ${"SESSION".padEnd(20)} ${"BRANCH".padEnd(25)} ${"COMMITS".padEnd(8)} ${"CHANGES"}`);
|
|
353
|
+
console.log(" " + "-".repeat(80));
|
|
354
|
+
for (const wt of worktrees) {
|
|
355
|
+
const changes = `+${wt.stats.additions}/-${wt.stats.deletions} (${wt.stats.filesChanged} files)`;
|
|
356
|
+
console.log(` ${wt.sessionName.padEnd(20)} ${wt.branch.padEnd(25)} ${String(wt.stats.commitCount).padEnd(8)} ${changes}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
console.log("");
|
|
360
|
+
}
|
|
361
|
+
function printWorktreeDetail(info, changedFiles, hasUncommitted) {
|
|
362
|
+
console.log(`\n Worktree: ${info.sessionName}`);
|
|
363
|
+
console.log(" " + "-".repeat(60));
|
|
364
|
+
console.log(` Path: ${info.path}`);
|
|
365
|
+
console.log(` Branch: ${info.branch}`);
|
|
366
|
+
console.log(` Base: ${info.baseBranch}`);
|
|
367
|
+
console.log(` Active: ${info.isActive ? "Yes" : "No"}`);
|
|
368
|
+
console.log(`\n Statistics:`);
|
|
369
|
+
console.log(` Commits: ${info.stats.commitCount}`);
|
|
370
|
+
console.log(` Files: ${info.stats.filesChanged}`);
|
|
371
|
+
console.log(` Added: +${info.stats.additions}`);
|
|
372
|
+
console.log(` Removed: -${info.stats.deletions}`);
|
|
373
|
+
if (hasUncommitted) {
|
|
374
|
+
console.log(`\n ⚠ Has uncommitted changes`);
|
|
375
|
+
}
|
|
376
|
+
if (changedFiles.length > 0) {
|
|
377
|
+
console.log(`\n Changed files:`);
|
|
378
|
+
const maxFiles = 10;
|
|
379
|
+
for (const file of changedFiles.slice(0, maxFiles)) {
|
|
380
|
+
const statusLabel = file.status === "A" ? "[new]" : file.status === "D" ? "[del]" : "[mod]";
|
|
381
|
+
console.log(` ${statusLabel.padEnd(6)} ${file.file}`);
|
|
382
|
+
}
|
|
383
|
+
if (changedFiles.length > maxFiles) {
|
|
384
|
+
console.log(` ... and ${changedFiles.length - maxFiles} more`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
console.log("");
|
|
388
|
+
}
|
|
389
|
+
function printWorktreeStatus(info, changedFiles, hasUncommitted) {
|
|
390
|
+
const statusIcon = hasUncommitted ? "●" : "○";
|
|
391
|
+
const changes = `+${info.stats.additions}/-${info.stats.deletions}`;
|
|
392
|
+
console.log(`${statusIcon} ${info.sessionName}`);
|
|
393
|
+
console.log(` Branch: ${info.branch}`);
|
|
394
|
+
console.log(` Commits: ${info.stats.commitCount} | Files: ${info.stats.filesChanged} | ${changes}`);
|
|
395
|
+
if (hasUncommitted) {
|
|
396
|
+
console.log(` ⚠ Uncommitted changes`);
|
|
397
|
+
}
|
|
398
|
+
if (changedFiles.length > 0 && changedFiles.length <= 5) {
|
|
399
|
+
for (const file of changedFiles) {
|
|
400
|
+
const statusLabel = file.status === "A" ? "+" : file.status === "D" ? "-" : "~";
|
|
401
|
+
console.log(` ${statusLabel} ${file.file}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ import { processCommand } from "./commands/process.js";
|
|
|
17
17
|
import { settingsCommand } from "./commands/settings.js";
|
|
18
18
|
import { strategyCommand } from "./commands/strategy.js";
|
|
19
19
|
import { completionCommand } from "./commands/completion.js";
|
|
20
|
+
import { worktreeCommand } from "./commands/worktree.js";
|
|
20
21
|
import { runInteractiveMode } from "./commands/interactive.js";
|
|
21
22
|
const program = new Command();
|
|
22
23
|
program
|
|
@@ -40,6 +41,7 @@ program.addCommand(processCommand);
|
|
|
40
41
|
program.addCommand(settingsCommand);
|
|
41
42
|
program.addCommand(strategyCommand);
|
|
42
43
|
program.addCommand(completionCommand);
|
|
44
|
+
program.addCommand(worktreeCommand);
|
|
43
45
|
// Check if no command was provided - run interactive mode
|
|
44
46
|
if (process.argv.length <= 2) {
|
|
45
47
|
runInteractiveMode();
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge Lock Mechanism for Husky Worktrees
|
|
3
|
+
*
|
|
4
|
+
* Provides file-based locking to prevent concurrent merges on the same session.
|
|
5
|
+
* Uses atomic file creation with PID tracking for stale lock detection.
|
|
6
|
+
*
|
|
7
|
+
* Based on Auto-Claude's lock mechanism.
|
|
8
|
+
*/
|
|
9
|
+
export declare class MergeLockError extends Error {
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
interface LockInfo {
|
|
13
|
+
pid: number;
|
|
14
|
+
timestamp: number;
|
|
15
|
+
sessionName: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class MergeLock {
|
|
18
|
+
private projectDir;
|
|
19
|
+
private sessionName;
|
|
20
|
+
private lockFile;
|
|
21
|
+
private lockDir;
|
|
22
|
+
private isHeldByMe;
|
|
23
|
+
static readonly DEFAULT_TIMEOUT = 30000;
|
|
24
|
+
static readonly POLL_INTERVAL = 500;
|
|
25
|
+
constructor(projectDir: string, sessionName: string);
|
|
26
|
+
/**
|
|
27
|
+
* Acquire the merge lock.
|
|
28
|
+
* Returns true if lock acquired, false if timeout.
|
|
29
|
+
*/
|
|
30
|
+
acquire(timeout?: number): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Try to acquire the lock atomically.
|
|
33
|
+
*/
|
|
34
|
+
private tryAcquire;
|
|
35
|
+
/**
|
|
36
|
+
* Check if the lock is stale (held by a dead process).
|
|
37
|
+
*/
|
|
38
|
+
private isStale;
|
|
39
|
+
/**
|
|
40
|
+
* Check if a process exists.
|
|
41
|
+
*/
|
|
42
|
+
private processExists;
|
|
43
|
+
/**
|
|
44
|
+
* Release the lock.
|
|
45
|
+
*/
|
|
46
|
+
release(): void;
|
|
47
|
+
/**
|
|
48
|
+
* Force release the lock (for stale lock cleanup).
|
|
49
|
+
*/
|
|
50
|
+
private forceRelease;
|
|
51
|
+
/**
|
|
52
|
+
* Check if this lock instance holds the lock.
|
|
53
|
+
*/
|
|
54
|
+
isHeld(): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Check if the lock file exists (held by any process).
|
|
57
|
+
*/
|
|
58
|
+
isLocked(): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Get information about the current lock holder.
|
|
61
|
+
*/
|
|
62
|
+
getLockInfo(): LockInfo | null;
|
|
63
|
+
/**
|
|
64
|
+
* Sleep helper.
|
|
65
|
+
*/
|
|
66
|
+
private sleep;
|
|
67
|
+
/**
|
|
68
|
+
* Cleanup all stale locks in a project.
|
|
69
|
+
*/
|
|
70
|
+
static cleanupStale(projectDir: string): number;
|
|
71
|
+
/**
|
|
72
|
+
* List all active locks in a project.
|
|
73
|
+
*/
|
|
74
|
+
static listLocks(projectDir: string): Array<{
|
|
75
|
+
sessionName: string;
|
|
76
|
+
info: LockInfo;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Helper function to use MergeLock with async/await cleanup.
|
|
81
|
+
*/
|
|
82
|
+
export declare function withMergeLock<T>(projectDir: string, sessionName: string, fn: () => Promise<T>, timeout?: number): Promise<T>;
|
|
83
|
+
export {};
|