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 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.** Then add to `package.json`:
107
- ```json
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
@@ -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
- console.log(" 1. Check lanekeeper.config.mjs — integrationBranch/checkCommand were auto-detected;");
145
- console.log(" set productionBranch too if you run a two-stage model.");
146
- console.log(' 2. Add to package.json "scripts": land, sync, promote, preview -> "lanekeeper <name>".');
147
- console.log(" 3. Commit lanekeeper.config.mjs, CLAUDE.md, .claude/settings.json, and .husky/pre-push.");
148
- console.log(" 4. claude --worktree <name> — the agent takes it from there.");
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
- if (isEphemeralNpxCopy(fileURLToPath(import.meta.url))) {
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");
@@ -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
+ };
@@ -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.6",
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",