lanekeeper 0.1.1 → 0.1.2
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 +7 -0
- package/dist/bin/lanekeeper.js +26 -0
- package/dist/lib/prune-lanes.js +36 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ locally instead of in someone else's billed cloud. 💸
|
|
|
60
60
|
| `lanekeeper promote` | Ships the integration branch to production. **Human-only** — never in an agent's instructions, never automated. |
|
|
61
61
|
| `lanekeeper preview` | Instantly mirrors a lane's live working tree — uncommitted changes included — onto the main checkout, so you can look at it without a build. |
|
|
62
62
|
| `lanekeeper port` | Prints a lane's dev-server port, derived from its own directory name. |
|
|
63
|
+
| `lanekeeper prune` | Removes already-landed sibling lane worktrees on demand — `land` already does this automatically, this is for "clean these up right now" instead of waiting for the next lane to land something. |
|
|
63
64
|
|
|
64
65
|
Plus 🔒 a pre-push hook that makes `land` non-optional: a direct `git push`
|
|
65
66
|
straight to the integration branch gets bounced, full stop. Not a lint
|
|
@@ -259,6 +260,12 @@ Things a sharp reader should already know before they ask:
|
|
|
259
260
|
CLAUDE.md tells it to resolve the conflict itself and re-run `land`, the
|
|
260
261
|
same way it'd fix any other bug — `checkCommand` still gates the result
|
|
261
262
|
either way, so a bad resolution gets caught there.
|
|
263
|
+
- **Auto-pruning checks for a live process, via `lsof`.** A merged branch
|
|
264
|
+
alone isn't enough to prune a lane — a brand-new, zero-commit lane is
|
|
265
|
+
*trivially* "merged" too (nothing's diverged yet), so pruning also
|
|
266
|
+
refuses to touch any worktree with a live process's cwd inside it right
|
|
267
|
+
now. That check needs `lsof` on PATH; if it's missing, pruning fails
|
|
268
|
+
closed (treats liveness as unknown, never removes) rather than guessing.
|
|
262
269
|
|
|
263
270
|
## 🧬 Where this came from
|
|
264
271
|
|
package/dist/bin/lanekeeper.js
CHANGED
|
@@ -17,6 +17,8 @@ import { lanePort } from "../lib/lane-port.js";
|
|
|
17
17
|
import { claudeMdSnippet, MARKER } from "../lib/claude-md-snippet.js";
|
|
18
18
|
import { detectCheckCommand, runCheckCommand } from "../lib/check-command.js";
|
|
19
19
|
import { wireClaudeSettings, wireHuskyPrePush, ensureHooksPath } from "../lib/wire-hooks.js";
|
|
20
|
+
import { resolveMainCheckout } from "../lib/main-checkout.js";
|
|
21
|
+
import { pruneLandedLanes } from "../lib/prune-lanes.js";
|
|
20
22
|
const [, , command, ...rest] = process.argv;
|
|
21
23
|
async function readStdin() {
|
|
22
24
|
const chunks = [];
|
|
@@ -180,6 +182,30 @@ async function main() {
|
|
|
180
182
|
case "promote":
|
|
181
183
|
process.exit(await promote());
|
|
182
184
|
return;
|
|
185
|
+
case "prune": {
|
|
186
|
+
// `land` already does this automatically as a side effect of every
|
|
187
|
+
// successful landing — this is the on-demand escape hatch for
|
|
188
|
+
// "clean these up right now" instead of waiting for the next lane to
|
|
189
|
+
// land something. Same safety rules apply: only already-merged
|
|
190
|
+
// branches, never anything with a live process inside, never the
|
|
191
|
+
// worktree this command itself is running from.
|
|
192
|
+
const root = findRepoRoot();
|
|
193
|
+
if (!root || !hasConfig(root)) {
|
|
194
|
+
console.error("lanekeeper prune: no lanekeeper.config found — nothing to do.");
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
const cfg = await loadConfig(root);
|
|
198
|
+
const mainTop = resolveMainCheckout(process.cwd());
|
|
199
|
+
const pruned = pruneLandedLanes(mainTop, cfg, process.cwd());
|
|
200
|
+
if (pruned.length === 0) {
|
|
201
|
+
console.log("lanekeeper prune: nothing to clean up — no already-landed sibling lanes found.");
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
const names = pruned.map((p) => p.split("/").pop()).join(", ");
|
|
205
|
+
console.log(`lanekeeper prune: removed ${pruned.length} already-landed lane${pruned.length === 1 ? "" : "s"}: ${names}`);
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
183
209
|
case "preview":
|
|
184
210
|
return runPreview(rest);
|
|
185
211
|
case "build-lock": {
|
package/dist/lib/prune-lanes.js
CHANGED
|
@@ -14,7 +14,17 @@
|
|
|
14
14
|
* fast-forwarded yet at the exact moment this runs. That's the literal
|
|
15
15
|
* definition of "nothing to lose": the work already made it upstream
|
|
16
16
|
* under its own name.
|
|
17
|
-
* 2.
|
|
17
|
+
* 2. Never touch a worktree with a LIVE process currently working in it
|
|
18
|
+
* (checked via `lsof -a -d cwd`, see below) — this is NOT redundant
|
|
19
|
+
* with the ancestor check. A brand-new lane that hasn't diverged yet
|
|
20
|
+
* is *trivially* an ancestor of upstream (its tip IS a commit already
|
|
21
|
+
* on the integration branch, just because nothing's been committed
|
|
22
|
+
* there yet) — structurally identical, in the git graph alone, to a
|
|
23
|
+
* lane whose own real work already landed. Only a liveness check can
|
|
24
|
+
* tell "someone's about to start working here" apart from "this is
|
|
25
|
+
* truly done." Confirmed live: a fresh, zero-commit lane got swept by
|
|
26
|
+
* another lane's land before its own first commit.
|
|
27
|
+
* 3. `git worktree remove` (no `--force`) refuses on its own if the
|
|
18
28
|
* worktree has uncommitted changes — dirty work is never discarded
|
|
19
29
|
* just because its branch happens to be merged.
|
|
20
30
|
* Deleting the now-redundant local branch (`git branch -d`, never `-D`) is a
|
|
@@ -24,9 +34,31 @@
|
|
|
24
34
|
* That failure doesn't undo the (already-safe) worktree removal or keep it
|
|
25
35
|
* out of the returned list; it just leaves a harmless leftover branch ref.
|
|
26
36
|
*/
|
|
27
|
-
import { execFileSync } from "node:child_process";
|
|
37
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
28
38
|
import { realpathSync } from "node:fs";
|
|
29
39
|
import { basename, dirname } from "node:path";
|
|
40
|
+
/**
|
|
41
|
+
* Is any live process's current working directory inside `dir` right now?
|
|
42
|
+
* `lsof`'s own exit code is NOT reliable for this — it returns non-zero
|
|
43
|
+
* both when nothing matches AND when it merely fails to inspect some
|
|
44
|
+
* unrelated process it lacks permission for (common, e.g. root-owned system
|
|
45
|
+
* daemons), even while correctly printing real matches. The trustworthy
|
|
46
|
+
* signal is whether stdout has any content at all: genuinely idle
|
|
47
|
+
* directories produce completely empty output; a live process's cwd entry
|
|
48
|
+
* always prints a real row. `-a` ANDs the two filters together (lsof ORs by
|
|
49
|
+
* default) so this only matches things actually inside `dir`, not every
|
|
50
|
+
* process on the system.
|
|
51
|
+
*
|
|
52
|
+
* If `lsof` isn't available at all, this fails CLOSED — treats liveness as
|
|
53
|
+
* unknown/possible rather than confirmed-safe, so an unverifiable state
|
|
54
|
+
* never gets treated the same as a verified-idle one.
|
|
55
|
+
*/
|
|
56
|
+
function hasLiveProcessInside(dir) {
|
|
57
|
+
const result = spawnSync("lsof", ["-a", "-d", "cwd", "+D", dir], { encoding: "utf8" });
|
|
58
|
+
if (result.error)
|
|
59
|
+
return true; // lsof missing/unspawnable — can't verify, assume in use
|
|
60
|
+
return result.stdout.trim().length > 0;
|
|
61
|
+
}
|
|
30
62
|
// Best-effort realpath — a "current worktree" that's already gone (or was
|
|
31
63
|
// never real to begin with) shouldn't crash the sweep over one path.
|
|
32
64
|
function existsRealpath(path) {
|
|
@@ -100,6 +132,8 @@ export function pruneLandedLanes(mainTop, cfg, currentWorktree) {
|
|
|
100
132
|
catch {
|
|
101
133
|
continue; // not safe to touch — leave this lane exactly as it is
|
|
102
134
|
}
|
|
135
|
+
if (hasLiveProcessInside(wt))
|
|
136
|
+
continue; // someone's actively in here — never touch it
|
|
103
137
|
try {
|
|
104
138
|
execFileSync("git", ["worktree", "remove", wt], { cwd: mainTop, stdio: "ignore" });
|
|
105
139
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanekeeper",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "The local, zero-cost merge queue for parallel Claude Code agents. Plugs into Claude Code's native worktree isolation; one build at a time, one landing at a time, zero races.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|