planrails 0.1.0 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 — 2026-09-12
4
+
5
+ Fixed, found while moving a real project from a vendored copy to the package:
6
+
7
+ - `npx planrails init` on a project that did not yet hold the package wrote the npx cache's absolute path into every hook command (machine-local, and evicted by npm). Hook commands now name `$CLAUDE_PROJECT_DIR/node_modules/planrails/…` whenever the project holds its own copy, whichever copy runs the installer. `test/hooks-path.test.mjs` reproduces it with a copied package.
8
+ - A CLAUDE.md plans block written by an older copy of the system (a different generator name in its marker) was not recognised, so `init` and `activate` added a second block beside it. Any `<!-- plans:begin` marker now counts as the block. `test/claude-md.test.mjs`.
9
+ - Internal: `npm test` lists the test files by name (`scripts/run-tests.mjs`); Node 20 does not expand the glob the 0.1.0 script used, so its CI jobs failed.
10
+
3
11
  ## 0.1.0 — 2026-09-12
4
12
 
5
13
  First release, extracted from the project it was built in after a day of use.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "planrails",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Plans that survive compaction, for Claude Code: gates that prove a task done, conditions answered before it closes, briefs and rules injected by hooks, one fresh session per task.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,10 +15,11 @@
15
15
  * and every teammate's machine runs the copy in THEIR node_modules. If planrails
16
16
  * is not installed under the project's node_modules (a checkout, a global
17
17
  * install), the command falls back to the absolute path of this copy and
18
- * `doctor` says so. Paths are always quoted: an unquoted path with a space runs
18
+ * `doctor` says so. The project's copy wins whenever it exists, whichever copy
19
+ * runs the installer (0.1.0 wrote the npx cache path on a first init). Paths are always quoted: an unquoted path with a space runs
19
20
  * `node /Users/x/My` and silently disables every hook.
20
21
  */
21
- import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, realpathSync, unlinkSync } from "node:fs";
22
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, unlinkSync } from "node:fs";
22
23
  import { join, dirname } from "node:path";
23
24
  import { spawnSync } from "node:child_process";
24
25
  import { fileURLToPath } from "node:url";
@@ -43,11 +44,16 @@ export const OPTIONAL = {
43
44
  const ALL_FILES = new Set([...MANAGED, ...Object.values(OPTIONAL)].map((m) => m.file));
44
45
  const SKILL_SRC = join(HOOKS_DIR, "..", "plan", "skill", "SKILL.md");
45
46
 
46
- function same(a, b) { try { return realpathSync(a) === realpathSync(b); } catch { return false; } }
47
- /** Portable when the package sits in the project's node_modules; absolute otherwise. */
47
+ /** Does the PROJECT hold its own copy of this hook under node_modules? That copy is what the hook command names. */
48
+ function installedLocally(file) { return existsSync(join(projectRoot(), "node_modules", "planrails", "src", "hooks", file)); }
49
+ /**
50
+ * Portable ($CLAUDE_PROJECT_DIR/node_modules/planrails/…) whenever the project holds its own copy, WHICHEVER copy runs
51
+ * this installer. 0.1.0 compared the running copy with the project's, so `npx planrails init` on a project that did not
52
+ * yet hold the package (the documented first step) wrote the npx cache's absolute path into every hook — machine-local,
53
+ * and evicted by npm. Absolute only when the project has no copy of its own.
54
+ */
48
55
  export function commandFor(m) {
49
- const viaNodeModules = same(packageRoot(), join(projectRoot(), "node_modules", "planrails"));
50
- const path = viaNodeModules ? `$CLAUDE_PROJECT_DIR/node_modules/planrails/src/hooks/${m.file}` : join(HOOKS_DIR, m.file);
56
+ const path = installedLocally(m.file) ? `$CLAUDE_PROJECT_DIR/node_modules/planrails/src/hooks/${m.file}` : join(HOOKS_DIR, m.file);
51
57
  return `${m.runner} "${path}"`;
52
58
  }
53
59
  function entryFor(m) {
@@ -122,7 +128,7 @@ export function install({ dryRun = false, withNeverDelete = false, remove = fals
122
128
  }
123
129
  } else if (!remove) lines.push(`(dry run) would install /plan skill → ${skillDest()}`);
124
130
  if (wanted.some((m) => m.file === "guard-never-delete.sh") && spawnSync("which", ["jq"]).status !== 0) lines.push("warn: jq not found — the never-delete guard needs it (brew install jq / apt install jq)");
125
- if (!remove && !same(packageRoot(), join(projectRoot(), "node_modules", "planrails"))) lines.push(`note: planrails is not installed under ${join(projectRoot(), "node_modules")} — hook commands use the absolute path of this copy (${packageRoot()}), which only works on this machine. Run: npm install --save-dev planrails, then ${cli} hooks install`);
131
+ if (!remove && !installedLocally("plan-session-start.mjs")) lines.push(`note: planrails is not installed under ${join(projectRoot(), "node_modules")} — hook commands use the absolute path of this copy (${packageRoot()}), which only works on this machine. Run: npm install --save-dev planrails, then ${cli} hooks install`);
126
132
  lines.push(remove ? "Restart Claude Code (or run /hooks) so the removal takes effect." : "Restart Claude Code (or run /hooks) so the new hooks load.");
127
133
  process.stdout.write(lines.join("\n") + "\n");
128
134
  return { changed, replaced };
package/src/init.mjs CHANGED
@@ -57,7 +57,7 @@ export async function init({ dir = null, force = false, noInstall = false, withN
57
57
  const installed = existsSync(join(root, "node_modules", "planrails", "package.json"));
58
58
  const declared = Boolean(pkg.devDependencies?.planrails || pkg.dependencies?.planrails);
59
59
  if (installed && declared) note(false, `planrails is a dependency (${readJson(join(root, "node_modules", "planrails", "package.json"), {}).version || "?"})`);
60
- else if (noInstall || dryRun) note(false, `planrails not installed as a dependency (${noInstall ? "--no-install" : "dry run"}); hooks will use this copy's absolute path`);
60
+ else if (noInstall || dryRun) note(false, `planrails not installed as a dependency (${noInstall ? "--no-install" : "dry run"}); hooks name the project's own copy if it has one, else this copy's absolute path`);
61
61
  else {
62
62
  log(`installing planrails@${pkgVersion()} as a devDependency…`);
63
63
  const r = spawnSync("npm", ["install", "--save-dev", `planrails@${pkgVersion()}`], { cwd: root, stdio: "inherit" });
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
15
15
 
16
- export const BEGIN = "<!-- plans:begin — generated by src/plan/plan.mjs; do not hand-edit this block -->";
16
+ export const BEGIN = "<!-- plans:begin — generated by planrails; do not hand-edit this block -->";
17
17
  export const END = "<!-- plans:end -->";
18
18
 
19
19
  export function renderBlock(activePlans) {
@@ -28,13 +28,15 @@ export function renderBlock(activePlans) {
28
28
  return lines.join("\n");
29
29
  }
30
30
 
31
+ /** Any begin marker counts, whatever generator name it carries: an older copy of this system wrote a different one, and a second block must never be added beside it. */
32
+ const BEGIN_PREFIX = "<!-- plans:begin";
31
33
  export function readBlock(md) {
32
- const a = md.indexOf(BEGIN); const b = md.indexOf(END);
34
+ const a = md.indexOf(BEGIN_PREFIX); const b = md.indexOf(END);
33
35
  if (a === -1 || b === -1 || b < a) return null;
34
36
  return md.slice(a, b + END.length);
35
37
  }
36
38
  /** How many blocks the file holds. More than one is a merge accident; validate fails on it. */
37
- export function blockCount(md) { return md.split(BEGIN).length - 1; }
39
+ export function blockCount(md) { return md.split(BEGIN_PREFIX).length - 1; }
38
40
 
39
41
  /** Plan ids currently listed in the block (for the drift check). */
40
42
  export function idsInBlock(md) {