planrails 0.1.0 → 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/CHANGELOG.md +13 -0
- package/package.json +4 -2
- package/src/hooks/install.mjs +13 -7
- package/src/init.mjs +8 -2
- package/src/plan/lib/claude-md.mjs +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
- `npx planrails update` now also rewrites the plans block in CLAUDE.md from the plans on disk, so a marker or wording written by an older version is replaced on upgrade. `test/claude-md.test.mjs`.
|
|
6
|
+
- Release checks around `npm publish` (`scripts/release-check.mjs`, wired as `prepublishOnly` and `postpublish`): before uploading, refuse a version the registry already has, a missing CHANGELOG entry, a dirty tree or an unpushed HEAD; after uploading, wait until the registry serves the version. Why: a web-authenticated publish is staged and finalizes about a minute later; in that window a second publish fails with E409 and an install with ETARGET, which is how 0.1.1's release went. CONTRIBUTING.md § Release. `test/release-check.test.mjs`.
|
|
7
|
+
|
|
8
|
+
## 0.1.1 — 2026-09-12
|
|
9
|
+
|
|
10
|
+
Fixed, found while moving a real project from a vendored copy to the package:
|
|
11
|
+
|
|
12
|
+
- `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.
|
|
13
|
+
- 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`.
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
3
16
|
## 0.1.0 — 2026-09-12
|
|
4
17
|
|
|
5
18
|
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.
|
|
3
|
+
"version": "0.1.2",
|
|
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": {
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"selftest": "node bin/planrails.mjs selftest && node src/hooks/selftest.mjs",
|
|
23
23
|
"acceptance": "node scripts/acceptance.mjs",
|
|
24
24
|
"trial": "node scripts/trial/run.mjs",
|
|
25
|
-
"prepack": "npm test"
|
|
25
|
+
"prepack": "npm test",
|
|
26
|
+
"prepublishOnly": "node scripts/release-check.mjs before",
|
|
27
|
+
"postpublish": "node scripts/release-check.mjs after"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"zod": "^4.4.3"
|
package/src/hooks/install.mjs
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
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 && !
|
|
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
|
|
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" });
|
|
@@ -130,7 +130,7 @@ export async function init({ dir = null, force = false, noInstall = false, withN
|
|
|
130
130
|
return problems ? 1 : 0;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** Refresh the guide, the skill
|
|
133
|
+
/** Refresh the guide, the skill, the hook entries and the CLAUDE.md plans block to this version. Plans are never touched. */
|
|
134
134
|
export async function update({ dir = null, log = console.log } = {}) {
|
|
135
135
|
if (dir) process.env.PLAN_PROJECT_ROOT = resolve(dir);
|
|
136
136
|
const root = projectRoot();
|
|
@@ -138,6 +138,12 @@ export async function update({ dir = null, log = console.log } = {}) {
|
|
|
138
138
|
const guideDest = join(root, "docs", "PLANNING_GUIDE.md");
|
|
139
139
|
if (!existsSync(guideDest) || readFileSync(guideDest, "utf8") !== readFileSync(guideSrc, "utf8")) { mkdirSync(join(root, "docs"), { recursive: true }); copyFileSync(guideSrc, guideDest); log("refreshed docs/PLANNING_GUIDE.md"); }
|
|
140
140
|
else log("docs/PLANNING_GUIDE.md is current");
|
|
141
|
+
// The plans block, rewritten from the plans on disk: a marker or wording written by an older version is replaced.
|
|
142
|
+
try {
|
|
143
|
+
const { activePlanIds, loadPlanCheap } = await import("./plan/lib/store.mjs");
|
|
144
|
+
const active = activePlanIds().map((id) => ({ id, title: loadPlanCheap(id).state?.title || id }));
|
|
145
|
+
if (existsSync(claudeMdPath())) log(writeBlock(claudeMdPath(), active) ? "refreshed the plans block in CLAUDE.md" : "the plans block in CLAUDE.md is current");
|
|
146
|
+
} catch (e) { log(`could not refresh the plans block in CLAUDE.md: ${e.message}`); }
|
|
141
147
|
const { install, status } = await import("./hooks/install.mjs");
|
|
142
148
|
install({});
|
|
143
149
|
const { problems } = status({ print: false });
|
|
@@ -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
|
|
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(
|
|
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(
|
|
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) {
|