lanekeeper 0.1.6 → 0.1.8
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 +4 -10
- package/dist/bin/lanekeeper.js +37 -6
- package/dist/hooks/worktree-create.d.ts +1 -0
- package/dist/hooks/worktree-create.js +22 -3
- package/dist/lib/wire-hooks.d.ts +13 -0
- package/dist/lib/wire-hooks.js +39 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,17 +102,11 @@ This does the whole setup, not just the config file:
|
|
|
102
102
|
- **`.husky/pre-push`** — created or appended to, *if* you already have
|
|
103
103
|
Husky. If you don't, `init` tells you so instead of silently writing to
|
|
104
104
|
the untracked, not-shared-with-your-team `.git/hooks/pre-push`.
|
|
105
|
+
- **`package.json` scripts** — `land`, `sync`, `promote`, `preview`, and
|
|
106
|
+
`preview:restore` added, skipping any you've already defined yourself.
|
|
105
107
|
|
|
106
|
-
**Commit everything it wrote
|
|
107
|
-
|
|
108
|
-
"scripts": {
|
|
109
|
-
"land": "lanekeeper land",
|
|
110
|
-
"sync": "lanekeeper sync",
|
|
111
|
-
"promote": "lanekeeper promote",
|
|
112
|
-
"preview": "lanekeeper preview",
|
|
113
|
-
"preview:restore": "lanekeeper preview --restore"
|
|
114
|
-
}
|
|
115
|
-
```
|
|
108
|
+
**Commit everything it wrote**, then you're running. Two steps, not a setup
|
|
109
|
+
guide.
|
|
116
110
|
|
|
117
111
|
If `init` couldn't detect a `checkCommand` (no matching script in
|
|
118
112
|
package.json), every push is **blocked** until you set one — see 🧰 What's
|
package/dist/bin/lanekeeper.js
CHANGED
|
@@ -16,7 +16,7 @@ import { runWorktreeCreateHook } from "../hooks/worktree-create.js";
|
|
|
16
16
|
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
|
-
import { wireClaudeSettings, wireHuskyPrePush, ensureHooksPath } from "../lib/wire-hooks.js";
|
|
19
|
+
import { wireClaudeSettings, wireHuskyPrePush, ensureHooksPath, wirePackageJsonScripts } from "../lib/wire-hooks.js";
|
|
20
20
|
import { resolveMainCheckout } from "../lib/main-checkout.js";
|
|
21
21
|
import { pruneLandedLanes } from "../lib/prune-lanes.js";
|
|
22
22
|
const [, , command, ...rest] = process.argv;
|
|
@@ -34,10 +34,15 @@ async function init() {
|
|
|
34
34
|
}
|
|
35
35
|
const { writeFileSync, readFileSync: read, existsSync, appendFileSync } = await import("node:fs");
|
|
36
36
|
const { join } = await import("node:path");
|
|
37
|
+
// What actually got newly written or modified this run — only these need
|
|
38
|
+
// committing. If everything below was already wired from a previous run,
|
|
39
|
+
// there's nothing new to tell you to commit.
|
|
40
|
+
const writtenFiles = [];
|
|
37
41
|
if (hasConfig(root)) {
|
|
38
42
|
console.log("lanekeeper init: lanekeeper.config.mjs already exists — leaving it alone.");
|
|
39
43
|
}
|
|
40
44
|
else {
|
|
45
|
+
writtenFiles.push("lanekeeper.config.mjs");
|
|
41
46
|
const detectedBranch = detectCurrentBranch(root);
|
|
42
47
|
const detectedCheck = detectCheckCommand(root);
|
|
43
48
|
const generated = {
|
|
@@ -80,10 +85,12 @@ export default ${JSON.stringify(generated, null, 2)};
|
|
|
80
85
|
if (!existsSync(claudeMdPath)) {
|
|
81
86
|
writeFileSync(claudeMdPath, `# Project instructions for Claude Code\n\n${snippet}`);
|
|
82
87
|
console.log(`lanekeeper init: wrote ${claudeMdPath}`);
|
|
88
|
+
writtenFiles.push("CLAUDE.md");
|
|
83
89
|
}
|
|
84
90
|
else if (!read(claudeMdPath, "utf8").includes(MARKER)) {
|
|
85
91
|
appendFileSync(claudeMdPath, `\n${snippet}`);
|
|
86
92
|
console.log(`lanekeeper init: appended the LaneKeeper workflow section to ${claudeMdPath}`);
|
|
93
|
+
writtenFiles.push("CLAUDE.md");
|
|
87
94
|
}
|
|
88
95
|
else {
|
|
89
96
|
console.log(`lanekeeper init: ${claudeMdPath} already has the LaneKeeper workflow section — leaving it alone.`);
|
|
@@ -92,9 +99,11 @@ export default ${JSON.stringify(generated, null, 2)};
|
|
|
92
99
|
switch (claudeSettingsResult) {
|
|
93
100
|
case "created":
|
|
94
101
|
console.log(`lanekeeper init: wrote ${join(root, ".claude", "settings.json")} with the WorktreeCreate hook.`);
|
|
102
|
+
writtenFiles.push(".claude/settings.json");
|
|
95
103
|
break;
|
|
96
104
|
case "merged":
|
|
97
105
|
console.log(`lanekeeper init: added the WorktreeCreate hook to your existing ${join(root, ".claude", "settings.json")}.`);
|
|
106
|
+
writtenFiles.push(".claude/settings.json");
|
|
98
107
|
break;
|
|
99
108
|
case "already-wired":
|
|
100
109
|
console.log("lanekeeper init: .claude/settings.json already has the WorktreeCreate hook — leaving it alone.");
|
|
@@ -108,9 +117,11 @@ export default ${JSON.stringify(generated, null, 2)};
|
|
|
108
117
|
switch (prePushResult) {
|
|
109
118
|
case "created":
|
|
110
119
|
console.log("lanekeeper init: wrote .husky/pre-push.");
|
|
120
|
+
writtenFiles.push(".husky/pre-push");
|
|
111
121
|
break;
|
|
112
122
|
case "merged":
|
|
113
123
|
console.log("lanekeeper init: appended LaneKeeper's checks to your existing .husky/pre-push.");
|
|
124
|
+
writtenFiles.push(".husky/pre-push");
|
|
114
125
|
break;
|
|
115
126
|
case "already-wired":
|
|
116
127
|
console.log("lanekeeper init: .husky/pre-push already wired — leaving it alone.");
|
|
@@ -139,13 +150,33 @@ export default ${JSON.stringify(generated, null, 2)};
|
|
|
139
150
|
break;
|
|
140
151
|
}
|
|
141
152
|
}
|
|
153
|
+
const scriptsResult = wirePackageJsonScripts(root);
|
|
154
|
+
switch (scriptsResult.result) {
|
|
155
|
+
case "added":
|
|
156
|
+
console.log(`lanekeeper init: added "${scriptsResult.added.join('", "')}" to package.json scripts.`);
|
|
157
|
+
writtenFiles.push("package.json");
|
|
158
|
+
break;
|
|
159
|
+
case "already-wired":
|
|
160
|
+
console.log("lanekeeper init: package.json already has all five scripts — leaving them alone.");
|
|
161
|
+
break;
|
|
162
|
+
case "no-package-json":
|
|
163
|
+
console.log("lanekeeper init: no package.json found — scripts NOT wired automatically.");
|
|
164
|
+
console.log(' Add "land"/"sync"/"promote"/"preview"/"preview:restore" -> "lanekeeper <name>" yourself.');
|
|
165
|
+
break;
|
|
166
|
+
case "unparseable":
|
|
167
|
+
console.log("lanekeeper init: package.json exists but isn't valid JSON — left untouched. Wire the scripts manually.");
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
142
170
|
console.log("");
|
|
143
171
|
console.log("Next steps:");
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
if (writtenFiles.length > 0) {
|
|
173
|
+
console.log(` 1. Commit what it wrote — ${writtenFiles.join(", ")}.`);
|
|
174
|
+
console.log(" 2. claude --worktree <name> — the agent takes it from there.");
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(" 1. claude --worktree <name> — the agent takes it from there.");
|
|
178
|
+
console.log(" (everything was already wired — nothing new to commit)");
|
|
179
|
+
}
|
|
149
180
|
}
|
|
150
181
|
async function main() {
|
|
151
182
|
switch (command) {
|
|
@@ -11,4 +11,5 @@ export declare function createLane(mainTop: string, cfg: LaneKeeperConfig): {
|
|
|
11
11
|
lane: number;
|
|
12
12
|
};
|
|
13
13
|
export declare function isEphemeralNpxCopy(selfPath: string): boolean;
|
|
14
|
+
export declare function expectsLocalInstall(mainTop: string): boolean;
|
|
14
15
|
export declare function runWorktreeCreateHook(): Promise<void>;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* automatically — nothing to release by hand.
|
|
25
25
|
*/
|
|
26
26
|
import { execFileSync } from "node:child_process";
|
|
27
|
-
import { existsSync, mkdirSync, symlinkSync } from "node:fs";
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, symlinkSync } from "node:fs";
|
|
28
28
|
import { dirname, join, basename, sep } from "node:path";
|
|
29
29
|
import { fileURLToPath } from "node:url";
|
|
30
30
|
import { loadConfig, hasConfig, DEFAULTS } from "../lib/config.js";
|
|
@@ -140,6 +140,25 @@ export function createLane(mainTop, cfg) {
|
|
|
140
140
|
export function isEphemeralNpxCopy(selfPath) {
|
|
141
141
|
return selfPath.includes(`${sep}_npx${sep}`);
|
|
142
142
|
}
|
|
143
|
+
// The guard above only makes sense for a host project that's an npm project
|
|
144
|
+
// with lanekeeper as a real dependency — hola, say. A non-Node host repo
|
|
145
|
+
// (a Haskell/Lua/Rust/whatever project with no package.json at all) has
|
|
146
|
+
// nowhere to install lanekeeper INTO; npx's ephemeral cache is the only way
|
|
147
|
+
// it can ever run lanekeeper commands, not a fallback masking a broken
|
|
148
|
+
// local install. Only expect a local install — and therefore only treat
|
|
149
|
+
// ephemeral execution as suspicious — when the host's own package.json
|
|
150
|
+
// actually lists lanekeeper as a dependency. No package.json, or one that
|
|
151
|
+
// doesn't mention lanekeeper: ephemeral execution is completely normal.
|
|
152
|
+
export function expectsLocalInstall(mainTop) {
|
|
153
|
+
let pkg;
|
|
154
|
+
try {
|
|
155
|
+
pkg = JSON.parse(readFileSync(join(mainTop, "package.json"), "utf8"));
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return false; // no package.json, or unreadable/invalid — nothing "expected" to be there
|
|
159
|
+
}
|
|
160
|
+
return Boolean(pkg.dependencies?.lanekeeper || pkg.devDependencies?.lanekeeper);
|
|
161
|
+
}
|
|
143
162
|
async function readStdin() {
|
|
144
163
|
const chunks = [];
|
|
145
164
|
for await (const chunk of process.stdin)
|
|
@@ -156,11 +175,11 @@ export async function runWorktreeCreateHook() {
|
|
|
156
175
|
}
|
|
157
176
|
const fromCwd = input.cwd ?? process.cwd();
|
|
158
177
|
try {
|
|
159
|
-
|
|
178
|
+
const mainTop = resolveMainCheckout(fromCwd);
|
|
179
|
+
if (expectsLocalInstall(mainTop) && isEphemeralNpxCopy(fileURLToPath(import.meta.url))) {
|
|
160
180
|
throw new Error("running from npx's ephemeral cache, not this project's own installed dependency — " +
|
|
161
181
|
"node_modules is missing or broken. Run `npm install` in the main checkout and try again.");
|
|
162
182
|
}
|
|
163
|
-
const mainTop = resolveMainCheckout(fromCwd);
|
|
164
183
|
const cfg = hasConfig(mainTop) ? await loadConfig(mainTop) : { ...DEFAULTS };
|
|
165
184
|
const { wt } = createLane(mainTop, cfg);
|
|
166
185
|
process.stdout.write(wt + "\n");
|
package/dist/lib/wire-hooks.d.ts
CHANGED
|
@@ -24,3 +24,16 @@ export type HooksPathResult = "set" | "already-set" | "custom-path";
|
|
|
24
24
|
* husky's own next real install corrects it to `.husky/_`.
|
|
25
25
|
*/
|
|
26
26
|
export declare function ensureHooksPath(root: string): HooksPathResult;
|
|
27
|
+
export type ScriptsWireResult = "added" | "already-wired" | "unparseable" | "no-package-json";
|
|
28
|
+
/**
|
|
29
|
+
* The last "copy this yourself" step `init` used to leave on the table:
|
|
30
|
+
* Quickstart told you to hand-add five scripts to package.json instead of
|
|
31
|
+
* just adding them. Same additive/idempotent contract as the rest of this
|
|
32
|
+
* file — only ever fills in scripts that don't exist yet, never overwrites
|
|
33
|
+
* one you've customized (e.g. if `land` already runs something of yours
|
|
34
|
+
* first), and does nothing if all five are already there.
|
|
35
|
+
*/
|
|
36
|
+
export declare function wirePackageJsonScripts(root: string): {
|
|
37
|
+
result: ScriptsWireResult;
|
|
38
|
+
added: string[];
|
|
39
|
+
};
|
package/dist/lib/wire-hooks.js
CHANGED
|
@@ -15,6 +15,13 @@ import { dirname, join } from "node:path";
|
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
const HOOK_COMMAND = "npx lanekeeper hook worktree-create";
|
|
17
17
|
const PRE_PUSH_MARKER = "lanekeeper check-push";
|
|
18
|
+
const PACKAGE_SCRIPTS = {
|
|
19
|
+
land: "lanekeeper land",
|
|
20
|
+
sync: "lanekeeper sync",
|
|
21
|
+
promote: "lanekeeper promote",
|
|
22
|
+
preview: "lanekeeper preview",
|
|
23
|
+
"preview:restore": "lanekeeper preview --restore",
|
|
24
|
+
};
|
|
18
25
|
export function wireClaudeSettings(root) {
|
|
19
26
|
const dir = join(root, ".claude");
|
|
20
27
|
const path = join(dir, "settings.json");
|
|
@@ -121,3 +128,35 @@ export function ensureHooksPath(root) {
|
|
|
121
128
|
execFileSync("git", ["config", "core.hooksPath", ".husky"], { cwd: root });
|
|
122
129
|
return "set";
|
|
123
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* The last "copy this yourself" step `init` used to leave on the table:
|
|
133
|
+
* Quickstart told you to hand-add five scripts to package.json instead of
|
|
134
|
+
* just adding them. Same additive/idempotent contract as the rest of this
|
|
135
|
+
* file — only ever fills in scripts that don't exist yet, never overwrites
|
|
136
|
+
* one you've customized (e.g. if `land` already runs something of yours
|
|
137
|
+
* first), and does nothing if all five are already there.
|
|
138
|
+
*/
|
|
139
|
+
export function wirePackageJsonScripts(root) {
|
|
140
|
+
const path = join(root, "package.json");
|
|
141
|
+
if (!existsSync(path))
|
|
142
|
+
return { result: "no-package-json", added: [] };
|
|
143
|
+
let pkg;
|
|
144
|
+
try {
|
|
145
|
+
pkg = JSON.parse(readFileSync(path, "utf8"));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return { result: "unparseable", added: [] };
|
|
149
|
+
}
|
|
150
|
+
pkg.scripts ??= {};
|
|
151
|
+
const added = [];
|
|
152
|
+
for (const [name, command] of Object.entries(PACKAGE_SCRIPTS)) {
|
|
153
|
+
if (!(name in pkg.scripts)) {
|
|
154
|
+
pkg.scripts[name] = command;
|
|
155
|
+
added.push(name);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (added.length === 0)
|
|
159
|
+
return { result: "already-wired", added: [] };
|
|
160
|
+
writeFileSync(path, JSON.stringify(pkg, null, 2) + "\n");
|
|
161
|
+
return { result: "added", added };
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lanekeeper",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
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",
|