@esneiderbravo/speclaw 0.1.4 → 0.1.6

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
@@ -29,24 +29,28 @@ One command. No cloud, no LLM, no API keys — <b>everything runs on your machin
29
29
  <br/>
30
30
 
31
31
  > [!TIP]
32
- > **One command sets everything up.** Run `npx @esneiderbravo/speclaw init`, pick the agents you
33
- > use, and speclaw scaffolds the project, indexes your code, and hands your agent
34
- > a ready-to-paste prompt to finish the setup.
32
+ > **Install once, then one command sets everything up.** Install speclaw globally, run
33
+ > `speclaw init`, pick the agents you use, and speclaw scaffolds the project, indexes
34
+ > your code, and hands your agent a ready-to-paste prompt to finish the setup.
35
35
 
36
36
  <br/>
37
37
 
38
38
  ## ◆ Quick start
39
39
 
40
- In your project root:
40
+ Install speclaw globally (once), then run `init` in your project root:
41
41
 
42
42
  ```bash
43
- npx @esneiderbravo/speclaw init
43
+ npm i -g @esneiderbravo/speclaw
44
+ speclaw init
44
45
  ```
45
46
 
46
47
  <p align="center">
47
- <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-quickstart.png" width="800" alt="npx @esneiderbravo/speclaw init">
48
+ <img src="https://raw.githubusercontent.com/esneiderbravo/speclaw/main/brand/terminal-quickstart.png" width="800" alt="speclaw init">
48
49
  </p>
49
50
 
51
+ The `speclaw` command is now available everywhere — run `speclaw index`,
52
+ `speclaw visualize`, or `speclaw lawbook …` directly in any project.
53
+
50
54
  `init` will:
51
55
 
52
56
  1. **Ask which agents you use** (Claude Code, Cursor, Codex, …) — and configure
@@ -57,7 +61,9 @@ npx @esneiderbravo/speclaw init
57
61
  5. Print a prompt to paste into your agent so it fills the constitution with your
58
62
  project's real architecture and conventions.
59
63
 
60
- Works with `npm`, `pnpm` (`pnpm dlx @esneiderbravo/speclaw init`), and `yarn`.
64
+ Prefer not to install globally? A one-off `npx @esneiderbravo/speclaw init` works
65
+ too (also `pnpm dlx` / `yarn dlx`) — but installing globally means you can run the
66
+ `speclaw` commands directly afterwards.
61
67
 
62
68
  <br/>
63
69
 
@@ -118,6 +124,25 @@ still use Compass and the lawbook engine by calling the CLI from its shell.
118
124
 
119
125
  <br/>
120
126
 
127
+ ## ◆ Staying up to date
128
+
129
+ speclaw checks for new releases in the background (at most once a day) and nudges
130
+ you when one lands. To upgrade:
131
+
132
+ ```bash
133
+ speclaw update
134
+ ```
135
+
136
+ `update` upgrades the global package **and** brings the current project up to date
137
+ without a re-init: any new standards, skills, commands, or feature steps are added
138
+ **additively** — your existing files are never touched. It re-applies only the tool
139
+ packs this project already uses.
140
+
141
+ - `speclaw update --check` — report whether an update exists, change nothing.
142
+ - `NO_UPDATE_NOTIFIER=1` — silence the reminder.
143
+
144
+ <br/>
145
+
121
146
  ## ◆ Requirements
122
147
 
123
148
  - **Node.js ≥ 22** — uses the built-in `node:sqlite`.
@@ -11,7 +11,8 @@ import { ui, c, banner, renderProgress, clearProgress } from "../lib/ui.js";
11
11
  const PACK_LABELS = {
12
12
  agents: "dev-agents (backend · frontend · product)",
13
13
  };
14
- function detectProjectName(cwd) {
14
+ /** Best-effort project name: the package.json name (unscoped) or the directory name. */
15
+ export function detectProjectName(cwd) {
15
16
  try {
16
17
  const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf8"));
17
18
  if (pkg.name)
@@ -67,15 +68,24 @@ export async function runInit(flags) {
67
68
  process.exit(1);
68
69
  }
69
70
  const profile = { project_name: projectName };
70
- // 1. Content + chosen agents, with a check per piece installed
71
+ // 1. Content + chosen agents, with a check per piece installed. Re-running
72
+ // init is safe: every writer skips files that already exist, so anything you
73
+ // (or your agent) filled in is preserved — init only adds what's missing.
74
+ const reinit = fs.existsSync(path.join(cwd, "LAWS.md"));
71
75
  ui.step(`Setting up ${c.bold(c.cyan(projectName))}`);
72
- scaffold(cwd, profile, packs, agents);
76
+ if (reinit) {
77
+ ui.info("speclaw is already set up here — your existing files are kept; only missing pieces are added.");
78
+ }
79
+ const report = scaffold(cwd, profile, packs, agents);
73
80
  ui.ok(`Foundation ${c.muted("— LAWS.md + 8 standards + CLAUDE.md/AGENTS.md")}`);
74
81
  ui.ok(`Lawbook workflow ${c.muted("— draft · build · sync · archive · explore")}`);
75
82
  for (const p of packs)
76
83
  ui.ok(`${PACK_LABELS[p] ?? p + " pack"}`);
77
84
  specInit(cwd);
78
85
  ui.ok(`Lawbook workspace ${c.muted("— lawbook/")}`);
86
+ if (reinit && report.skipped.length) {
87
+ ui.info(`${report.written.length} added · ${c.cream(String(report.skipped.length))} preserved untouched`);
88
+ }
79
89
  ui.step("Configuring agents");
80
90
  for (const id of agents)
81
91
  ui.ok(`${agentById(id).label} ${c.muted("— symlinks + MCP")}`);
@@ -0,0 +1,100 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { ui, c } from "../lib/ui.js";
5
+ import { checkForUpdates, isNewer } from "../lib/update-check.js";
6
+ import { pkgName, pkgVersion } from "../../shared/version.js";
7
+ import { scaffold } from "../../modules/foundation/scaffold.js";
8
+ import { detectConfiguredAgents } from "../../shared/agents.js";
9
+ import { readManifest } from "../../shared/manifest.js";
10
+ import { loadPacks } from "../../modules/tools/packs.js";
11
+ import { detectProjectName } from "./init.js";
12
+ const MIGRATIONS = [
13
+ // e.g. { version: "0.3.0", describe: "…", run: (p, r) => { … } }
14
+ ];
15
+ /**
16
+ * Update speclaw and bring the current project up to date without a full re-init:
17
+ * upgrade the global package, then additively apply any new standards, skills,
18
+ * commands, and feature steps this project is missing (existing files untouched).
19
+ *
20
+ * @param flags - `--check` reports without changing anything; `--migrate-only`
21
+ * skips the global upgrade and only applies project changes (used internally
22
+ * after the package is upgraded, so migrations run from the new version).
23
+ */
24
+ export async function runUpdate(flags) {
25
+ const cwd = process.cwd();
26
+ const migrateOnly = Boolean(flags["migrate-only"]);
27
+ const checkOnly = Boolean(flags.check);
28
+ const winShell = process.platform === "win32";
29
+ if (!migrateOnly) {
30
+ ui.step("Checking for updates");
31
+ const { current, latest, updateAvailable } = await checkForUpdates({ force: true });
32
+ if (!latest) {
33
+ ui.warn("Could not reach the npm registry — skipping the version check.");
34
+ }
35
+ else if (updateAvailable) {
36
+ ui.info(`${c.muted(current)} ${c.muted("→")} ${c.cyan(latest)}`);
37
+ if (checkOnly) {
38
+ ui.info(`Run ${ui.code("speclaw update")} to upgrade and apply what's new.`);
39
+ return;
40
+ }
41
+ ui.step(`Updating ${pkgName()} globally`);
42
+ const install = spawnSync("npm", ["install", "-g", `${pkgName()}@latest`], { stdio: "inherit", shell: winShell });
43
+ if (install.status !== 0) {
44
+ ui.err("Global update failed. Try again with elevated permissions (e.g. sudo), or check your npm setup.");
45
+ process.exit(1);
46
+ }
47
+ ui.ok(`Updated to ${latest}`);
48
+ // Re-exec the NEWLY installed binary so migrations run with the new assets
49
+ // and any new feature steps — not this (now-stale) process.
50
+ const re = spawnSync("speclaw", ["update", "--migrate-only"], { stdio: "inherit", shell: winShell });
51
+ if (re.error) {
52
+ ui.warn(`Upgraded — now run ${ui.code("speclaw update --migrate-only")} to apply project changes.`);
53
+ return;
54
+ }
55
+ process.exit(re.status ?? 0);
56
+ }
57
+ else {
58
+ ui.ok(`Already on the latest version (${current}).`);
59
+ if (checkOnly)
60
+ return;
61
+ }
62
+ }
63
+ applyProjectMigrations(cwd);
64
+ }
65
+ /**
66
+ * Additively apply the current version's content and feature steps to a project.
67
+ * No-op with a hint when the directory isn't a speclaw project.
68
+ */
69
+ function applyProjectMigrations(cwd) {
70
+ const initialized = fs.existsSync(path.join(cwd, "ai-specs")) || fs.existsSync(path.join(cwd, "LAWS.md"));
71
+ if (!initialized) {
72
+ ui.step("Project");
73
+ ui.info(`No speclaw project here — run ${ui.code("speclaw init")} to set one up.`);
74
+ return;
75
+ }
76
+ ui.step("Applying what's new to this project");
77
+ const fromVersion = readManifest(cwd)?.version ?? "0.0.0";
78
+ const agents = detectConfiguredAgents(cwd);
79
+ const known = loadPacks();
80
+ const packs = (readManifest(cwd)?.packs ?? []).filter((p) => p in known);
81
+ // scaffold is non-destructive: only missing files are written, and it refreshes
82
+ // the manifest to the current version. Existing files are left exactly as-is.
83
+ const report = scaffold(cwd, { project_name: detectProjectName(cwd) }, packs, agents);
84
+ const newFiles = report.written.filter((w) => !w.includes(".gitignore"));
85
+ if (newFiles.length) {
86
+ for (const w of newFiles)
87
+ ui.ok(c.cream(path.relative(cwd, w.split(" (")[0])));
88
+ ui.info(`${newFiles.length} new file(s) added · ${report.skipped.length} left untouched.`);
89
+ }
90
+ else {
91
+ ui.ok("Content already up to date — nothing new to add.");
92
+ }
93
+ const pending = MIGRATIONS.filter((m) => isNewer(m.version, fromVersion));
94
+ for (const m of pending) {
95
+ ui.step(`Step for ${m.version}: ${m.describe}`);
96
+ m.run(cwd, report);
97
+ }
98
+ ui.plain();
99
+ ui.ok(`On ${c.cyan(pkgVersion())}. No re-init needed.`);
100
+ }
package/dist/cli/index.js CHANGED
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseFlags } from "./lib/args.js";
3
3
  import { ui } from "./lib/ui.js";
4
- const HELP = `speclaw spec-driven, agent-ready projects (foundation + Compass + Spec)
4
+ import { maybeNotifyUpdate } from "./lib/update-check.js";
5
+ const HELP = `speclaw — spec-driven, agent-ready projects (foundation + Compass + Lawbook)
5
6
 
6
7
  Usage: speclaw <command> [options]
7
8
 
9
+ Install globally so the command is always available:
10
+ npm i -g @esneiderbravo/speclaw
11
+
8
12
  Setup
9
13
  init Interactive setup: pick agents, scaffold, index, get the prompt
14
+ update Upgrade speclaw and apply only what's new (no re-init)
10
15
  agent list Show which agents are configured
11
16
  agent add <id> Configure another agent later (symlinks + MCP)
12
17
 
@@ -32,10 +37,8 @@ Other
32
37
  mcp Start the MCP server (used by your agent's config)
33
38
  help Show this help
34
39
  `;
35
- /** Parse argv and dispatch to the matching command handler. */
36
- async function main() {
37
- const [cmd, ...rest] = process.argv.slice(2);
38
- const flags = parseFlags(rest);
40
+ /** Run the handler for a single command. Returns when the command completes. */
41
+ async function dispatch(cmd, flags) {
39
42
  switch (cmd) {
40
43
  case undefined:
41
44
  case "help":
@@ -50,6 +53,8 @@ async function main() {
50
53
  }
51
54
  case "init":
52
55
  return (await import("./commands/init.js")).runInit(flags);
56
+ case "update":
57
+ return (await import("./commands/update.js")).runUpdate(flags);
53
58
  case "agent":
54
59
  return (await import("./commands/agent.js")).runAgent(flags);
55
60
  case "index":
@@ -74,6 +79,13 @@ async function main() {
74
79
  process.exit(1);
75
80
  }
76
81
  }
82
+ /** Parse argv, run the command, then surface an update notice if one is due. */
83
+ async function main() {
84
+ const [cmd, ...rest] = process.argv.slice(2);
85
+ const flags = parseFlags(rest);
86
+ await dispatch(cmd, flags);
87
+ await maybeNotifyUpdate(cmd);
88
+ }
77
89
  main().catch((err) => {
78
90
  ui.err(err.message);
79
91
  process.exit(1);
@@ -0,0 +1,120 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { pkgName, pkgVersion } from "../../shared/version.js";
5
+ import { c } from "./ui.js";
6
+ // A lightweight, best-effort update notifier. The registry is queried at most
7
+ // once a day (result cached under ~/.speclaw/), the lookup is time-boxed, and
8
+ // every failure is swallowed — checking for updates must never slow down or
9
+ // break a command. Notices go to stderr so piped stdout stays clean.
10
+ const TTL_MS = 24 * 60 * 60 * 1000;
11
+ function cacheFile() {
12
+ return path.join(os.homedir(), ".speclaw", "update-check.json");
13
+ }
14
+ function readCache() {
15
+ try {
16
+ const c = JSON.parse(fs.readFileSync(cacheFile(), "utf8"));
17
+ if (typeof c.checkedAt === "number" && typeof c.latest === "string")
18
+ return c;
19
+ }
20
+ catch {
21
+ /* no cache yet */
22
+ }
23
+ return null;
24
+ }
25
+ function writeCache(latest) {
26
+ try {
27
+ const f = cacheFile();
28
+ fs.mkdirSync(path.dirname(f), { recursive: true });
29
+ fs.writeFileSync(f, JSON.stringify({ checkedAt: Date.now(), latest }));
30
+ }
31
+ catch {
32
+ /* cache is an optimization; ignore failures */
33
+ }
34
+ }
35
+ async function fetchLatest(name) {
36
+ const ctrl = new AbortController();
37
+ const timer = setTimeout(() => ctrl.abort(), 2500);
38
+ try {
39
+ const url = `https://registry.npmjs.org/${name.replace("/", "%2F")}/latest`;
40
+ const res = await fetch(url, { signal: ctrl.signal });
41
+ if (!res.ok)
42
+ return null;
43
+ const body = (await res.json());
44
+ return typeof body.version === "string" ? body.version : null;
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ finally {
50
+ clearTimeout(timer);
51
+ }
52
+ }
53
+ /**
54
+ * Compare two dotted versions (ignoring any prerelease suffix).
55
+ *
56
+ * @returns True when `latest` is strictly newer than `current`.
57
+ */
58
+ export function isNewer(latest, current) {
59
+ const parse = (v) => v.split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
60
+ const a = parse(latest);
61
+ const b = parse(current);
62
+ for (let i = 0; i < 3; i++) {
63
+ const x = a[i] ?? 0;
64
+ const y = b[i] ?? 0;
65
+ if (x > y)
66
+ return true;
67
+ if (x < y)
68
+ return false;
69
+ }
70
+ return false;
71
+ }
72
+ /**
73
+ * Resolve the current version and the latest published one, using the daily
74
+ * cache unless `force` is set. Falls back to stale cache when offline.
75
+ *
76
+ * @param opts - `force` bypasses the cache and always queries the registry.
77
+ * @returns The current/latest versions and whether an upgrade is available.
78
+ */
79
+ export async function checkForUpdates(opts = {}) {
80
+ const current = pkgVersion();
81
+ const cache = readCache();
82
+ let latest = null;
83
+ if (!opts.force && cache && Date.now() - cache.checkedAt < TTL_MS) {
84
+ latest = cache.latest;
85
+ }
86
+ else {
87
+ latest = await fetchLatest(pkgName());
88
+ if (latest)
89
+ writeCache(latest);
90
+ else if (cache)
91
+ latest = cache.latest; // offline: use whatever we last knew
92
+ }
93
+ return { current, latest, updateAvailable: !!latest && isNewer(latest, current) };
94
+ }
95
+ /**
96
+ * Print a one-line "update available" notice to stderr when a newer version
97
+ * exists. No-op for the `mcp`/`update`/`help` commands, on non-TTY stderr, or
98
+ * when NO_UPDATE_NOTIFIER / SPECLAW_NO_UPDATE_NOTIFIER is set. Never throws.
99
+ *
100
+ * @param cmd - The command that just ran (used to skip noisy contexts).
101
+ */
102
+ export async function maybeNotifyUpdate(cmd) {
103
+ try {
104
+ if (process.env.NO_UPDATE_NOTIFIER || process.env.SPECLAW_NO_UPDATE_NOTIFIER)
105
+ return;
106
+ if (!process.stderr.isTTY)
107
+ return;
108
+ if (!cmd || ["mcp", "update", "help", "--help", "-h"].includes(cmd))
109
+ return;
110
+ const { current, latest, updateAvailable } = await checkForUpdates();
111
+ if (!updateAvailable || !latest)
112
+ return;
113
+ process.stderr.write("\n" +
114
+ " " + c.amber("⬆ speclaw ") + c.muted(current + " → ") + c.cyan(latest) + c.muted(" available") + "\n" +
115
+ " " + c.muted("run ") + c.cyan("speclaw update") + c.muted(" — upgrades and applies only what's new") + "\n\n");
116
+ }
117
+ catch {
118
+ /* the notifier is best-effort — never let it break a command */
119
+ }
120
+ }
@@ -49,13 +49,53 @@ CREATE TABLE IF NOT EXISTS node_embeddings (
49
49
  );
50
50
  `;
51
51
  /** Schema version stamped into the `meta` table on first creation. */
52
- export const SCHEMA_VERSION = "2";
52
+ export const SCHEMA_VERSION = "3";
53
+ /** The stamped schema version, or null if the db predates versioning / has no meta table. */
54
+ function readSchemaVersion(db) {
55
+ try {
56
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
57
+ return row ? String(row.value) : null;
58
+ }
59
+ catch {
60
+ return null; // meta table doesn't exist yet
61
+ }
62
+ }
63
+ /**
64
+ * Decide whether an existing database is from an incompatible schema and must be
65
+ * rebuilt. A fresh database (no tables) needs no reset — the schema will create
66
+ * them. An existing one is stale if its stamped version differs from the current
67
+ * one, or if the `edges` table is missing a column the current code writes to
68
+ * (guards against past schema changes that weren't version-bumped).
69
+ */
70
+ function isStale(db) {
71
+ const hasEdges = db
72
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'edges'")
73
+ .get();
74
+ if (!hasEdges)
75
+ return false;
76
+ if (readSchemaVersion(db) !== SCHEMA_VERSION)
77
+ return true;
78
+ const cols = db.prepare("PRAGMA table_info(edges)").all().map((c) => c.name);
79
+ return !cols.includes("src_node_id") || !cols.includes("dst_node_id");
80
+ }
81
+ /** Drop every table (children first) so the current schema can be recreated cleanly. */
82
+ function resetSchema(db) {
83
+ db.exec(`
84
+ DROP TABLE IF EXISTS node_embeddings;
85
+ DROP TABLE IF EXISTS edges;
86
+ DROP TABLE IF EXISTS nodes;
87
+ DROP TABLE IF EXISTS files;
88
+ DROP TABLE IF EXISTS meta;
89
+ `);
90
+ }
53
91
  /**
54
92
  * Open (creating if needed) the index database at `<projectPath>/.speclaw/index.db`.
55
93
  *
56
- * Ensures the `.speclaw` directory exists, applies the schema (idempotently),
57
- * enables WAL journaling and foreign keys, and stamps the schema version on a
58
- * fresh database.
94
+ * Ensures the `.speclaw` directory exists, enables WAL journaling and foreign
95
+ * keys, and applies the schema. If an existing database is from an incompatible
96
+ * schema (e.g. after a speclaw upgrade), it is dropped and rebuilt — `.speclaw`
97
+ * is fully regenerable, so the next index just repopulates it. The schema
98
+ * version is stamped on a fresh (or freshly reset) database.
59
99
  *
60
100
  * @param projectPath - Absolute path to the project root.
61
101
  * @returns An open connection to the index database.
@@ -65,6 +105,8 @@ export function openDb(projectPath) {
65
105
  fs.mkdirSync(dir, { recursive: true });
66
106
  const db = new DatabaseSync(path.join(dir, "index.db"));
67
107
  db.exec("PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;");
108
+ if (isStale(db))
109
+ resetSchema(db);
68
110
  db.exec(SCHEMA);
69
111
  const row = db
70
112
  .prepare("SELECT value FROM meta WHERE key = 'schema_version'")
@@ -6,6 +6,8 @@ import { emptyReport, ensureGitignore } from "../../shared/install.js";
6
6
  import { configureAgent } from "../../shared/agents.js";
7
7
  import { installWorkflow } from "../lawbook/register.js";
8
8
  import { installPack, loadPacks } from "../tools/packs.js";
9
+ import { writeManifest } from "../../shared/manifest.js";
10
+ import { pkgVersion } from "../../shared/version.js";
9
11
  const ASSETS = assetsDir(import.meta.url);
10
12
  // Every {{var}} the foundation templates may reference. Ones the agent didn't
11
13
  // provide default to empty so a bare `scaffold` never leaves a raw {{tag}}.
@@ -88,6 +90,9 @@ export function scaffold(projectPath, profile, packNames, agents = []) {
88
90
  ensureGitignore(projectPath, ".speclaw/", "speclaw local code Compass (never commit)", report);
89
91
  for (const id of agents)
90
92
  configureAgent(projectPath, id, report); // only the chosen agents
93
+ // Record what was installed so `speclaw update` can re-apply only these packs
94
+ // (additively) and gate feature migrations by version — no full re-init.
95
+ writeManifest(projectPath, pkgVersion(), packNames);
91
96
  report.nextSteps = [
92
97
  "Run the `lawbook_init` tool to set up the spec-driven workflow (creates lawbook/). No external CLI needed — it's built into speclaw.",
93
98
  "Run the `compass_index` tool to build the local code graph (.speclaw/). No install, no LLM — it's built into speclaw. Re-run it after significant edits.",
@@ -0,0 +1,35 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ function manifestPath(projectPath) {
4
+ return path.join(projectPath, "ai-specs", ".speclaw.json");
5
+ }
6
+ /**
7
+ * Read a project's speclaw manifest.
8
+ *
9
+ * @param projectPath - Project root to read from.
10
+ * @returns The manifest, or null if the project has none (e.g. an older init).
11
+ */
12
+ export function readManifest(projectPath) {
13
+ try {
14
+ const m = JSON.parse(fs.readFileSync(manifestPath(projectPath), "utf8"));
15
+ return { version: String(m.version ?? "0.0.0"), packs: Array.isArray(m.packs) ? m.packs.map(String) : [] };
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ /**
22
+ * Write (or refresh) a project's manifest, recording the current version and the
23
+ * union of previously-installed and newly-installed packs.
24
+ *
25
+ * @param projectPath - Project root to write into.
26
+ * @param version - The speclaw version doing the write.
27
+ * @param packs - Pack names installed in this run.
28
+ */
29
+ export function writeManifest(projectPath, version, packs) {
30
+ const prev = readManifest(projectPath);
31
+ const merged = Array.from(new Set([...(prev?.packs ?? []), ...packs]));
32
+ const p = manifestPath(projectPath);
33
+ fs.mkdirSync(path.dirname(p), { recursive: true });
34
+ fs.writeFileSync(p, JSON.stringify({ version, packs: merged }, null, 2) + "\n");
35
+ }
@@ -0,0 +1,27 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ // The package.json sits at the package root — two levels up from this compiled
5
+ // file (dist/shared/version.js -> dist -> <root>). Read once and cache.
6
+ let cached = null;
7
+ function readPkg() {
8
+ if (cached)
9
+ return cached;
10
+ const pkgPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
11
+ try {
12
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
13
+ cached = { name: String(pkg.name ?? "@esneiderbravo/speclaw"), version: String(pkg.version ?? "0.0.0") };
14
+ }
15
+ catch {
16
+ cached = { name: "@esneiderbravo/speclaw", version: "0.0.0" };
17
+ }
18
+ return cached;
19
+ }
20
+ /** The published package name (e.g. `@esneiderbravo/speclaw`). */
21
+ export function pkgName() {
22
+ return readPkg().name;
23
+ }
24
+ /** The currently installed package version (e.g. `0.1.4`). */
25
+ export function pkgVersion() {
26
+ return readPkg().version;
27
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@esneiderbravo/speclaw",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },