rulebookguard 0.4.0 → 0.5.0

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
@@ -13,7 +13,7 @@ It is language-agnostic: rules are Markdown, and tests in any language link to t
13
13
 
14
14
  ## Install
15
15
 
16
- Requires Node.js 22.23.4 or later (the `site:generate` and `site:run` commands build with Astro 7, which needs it).
16
+ Requires Node.js 22 or later.
17
17
 
18
18
  ```sh
19
19
  npx rulebookguard lint
@@ -262,6 +262,14 @@ The rulebook is found from `--project` (default: the current directory). A comma
262
262
 
263
263
  ### `site:generate [--out <dir>] [--base <path>]`
264
264
 
265
+ The site commands build with Astro, which lives in a separate package so that installing the linter does not download it. Install it next to `rulebookguard` (it requires Node.js 22.23.4 or later):
266
+
267
+ ```sh
268
+ npm install --save-dev rulebookguard @rulebookguard/site
269
+ ```
270
+
271
+ Without it, the site commands fail, exit 1, with `The site commands need the @rulebookguard/site package. Install it next to rulebookguard: npm install --save-dev @rulebookguard/site`. One-off, without installing: `npx -p rulebookguard -p @rulebookguard/site rulebookguard site:run`.
272
+
265
273
  Builds a read-only static site of the rulebook with [Astro Starlight](https://starlight.astro.build) into `--out` (default `site`, relative to the current directory) and prints that path. Open it from disk or publish it as is, for example on GitHub Pages; pass `--base` when it is served from a sub-path (`--base /rules`).
266
274
 
267
275
  ```sh
package/dist/cli.mjs CHANGED
@@ -80,7 +80,7 @@ function render(path, rules, text) {
80
80
  return parts.join("\n\n");
81
81
  }
82
82
  // package.json
83
- var version = "0.4.0";
83
+ var version = "0.5.0";
84
84
  // src/mcp/server.ts
85
85
  function createRulesServer(projectDir) {
86
86
  const server = new McpServer({
@@ -334,6 +334,7 @@ Commands:
334
334
  site:run [--out <dir>] [--base <path>] [--port <n>]
335
335
  build the site, then serve it on 127.0.0.1 until Ctrl+C;
336
336
  --port: port to serve on (default: 4321)
337
+ site commands need @rulebookguard/site installed
337
338
 
338
339
  Exit codes: 0 success, 1 failure (lint: any error), 2 usage error.
339
340
  `;
@@ -440,7 +441,7 @@ async function dispatch(argv, io) {
440
441
  return 0;
441
442
  }
442
443
  if (name === "site" || name === "site:generate" || name === "site:run") {
443
- const { siteCommand } = await import("./site-BgH5724T.mjs");
444
+ const { siteCommand } = await import("./site-BiiOPVEe.mjs");
444
445
  await siteCommand(io, {
445
446
  out: values.out,
446
447
  base: values.base,
@@ -1,12 +1,9 @@
1
1
  import { a as CliError, c as openCatalog, d as scanTestTags, g as expandGlobs, n as lintCatalog, s as gitFallbackWarning, u as writeLines, y as runGit } from "./lint-C1yUeLOq.mjs";
2
- import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
3
- import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
- import { execFile } from "node:child_process";
5
- import { promisify } from "node:util";
6
- import { randomUUID } from "node:crypto";
7
- import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import { readFile, readdir, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
5
  import { createServer } from "node:http";
9
- import { fileURLToPath } from "node:url";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
7
  // src/site/data.ts
11
8
  const FIELD = "";
12
9
  async function siteData(catalog, onGitFallback) {
@@ -396,7 +393,7 @@ function demote(markdown) {
396
393
  }).join("\n");
397
394
  }
398
395
  // src/commands/site.ts
399
- const execFileAsync = promisify(execFile);
396
+ const SITE_PACKAGE = "@rulebookguard/site";
400
397
  /** Marks an output directory as written by `site`, so a rebuild may replace it. */
401
398
  const MARKER = ".rulebookguard-site";
402
399
  /**
@@ -408,11 +405,12 @@ async function siteCommand(io, options, until = untilInterrupted()) {
408
405
  const fromCwd = relative(io.cwd, out);
409
406
  const shown = fromCwd === "" ? "." : fromCwd.startsWith("..") || isAbsolute(fromCwd) ? out : fromCwd;
410
407
  await assertReplaceable(out, shown);
408
+ const site = await importSitePackage([catalog.root, dirname(fileURLToPath(import.meta.url))]);
411
409
  const language = catalog.config.language ?? "en";
412
410
  const base = normalizeBase(options.base ?? "/");
413
411
  const server = options.serve ? await listen(out, base, options.port ?? 4321) : void 0;
414
412
  try {
415
- await buildSite(catalog, out, language, base, io);
413
+ await buildSite(site, catalog, out, language, base, io);
416
414
  } catch (error) {
417
415
  server?.close();
418
416
  throw error;
@@ -427,7 +425,21 @@ async function siteCommand(io, options, until = untilInterrupted()) {
427
425
  server.closeAllConnections();
428
426
  });
429
427
  }
430
- async function buildSite(catalog, out, language, base, io) {
428
+ /** Imports the site builder from the first of `from` that resolves it. */
429
+ async function importSitePackage(from) {
430
+ for (const directory of from) {
431
+ let entry;
432
+ try {
433
+ entry = createRequire(join(directory, "package.json")).resolve(SITE_PACKAGE);
434
+ } catch (error) {
435
+ if (error.code === "MODULE_NOT_FOUND") continue;
436
+ throw error;
437
+ }
438
+ return await import(pathToFileURL(entry).href);
439
+ }
440
+ throw new CliError(`The site commands need the ${SITE_PACKAGE} package. Install it next to rulebookguard: npm install --save-dev ${SITE_PACKAGE}`);
441
+ }
442
+ async function buildSite(site, catalog, out, language, base, io) {
431
443
  const files = sitePages(await siteData(catalog, gitFallbackWarning(io)), {
432
444
  language,
433
445
  base
@@ -448,25 +460,12 @@ async function buildSite(catalog, out, language, base, io) {
448
460
  }))
449
461
  ];
450
462
  files.set("astro.config.mjs", astroConfig(text.title, language, base, sidebar));
451
- const work = join(siteWorkRoot(), randomUUID());
452
463
  try {
453
- for (const [path, content] of files) {
454
- await mkdir(dirname(join(work, path)), { recursive: true });
455
- await writeFile(join(work, path), content);
456
- }
457
- await buildWithAstro(work);
458
- await rm(out, {
459
- recursive: true,
460
- force: true
461
- });
462
- await cp(join(work, "dist"), out, { recursive: true });
463
- await writeFile(join(out, MARKER), "");
464
- } finally {
465
- await rm(work, {
466
- recursive: true,
467
- force: true
468
- });
464
+ await site.buildSite(files, out);
465
+ } catch (error) {
466
+ throw error instanceof site.SiteBuildError ? new CliError(error.message) : error;
469
467
  }
468
+ await writeFile(join(out, MARKER), "");
470
469
  }
471
470
  function untilInterrupted() {
472
471
  return new Promise((stop) => {
@@ -541,25 +540,6 @@ async function sendNotFound(out, response) {
541
540
  const page = await readFile(join(out, "404.html")).catch(() => "Not found");
542
541
  response.writeHead(404, { "content-type": CONTENT_TYPES[".html"] }).end(page);
543
542
  }
544
- /**
545
- * Starlight injects its routes as package specifiers that Astro resolves from the project root, so the generated
546
- * project must live inside the node_modules directory that holds rulebookguard's own dependencies.
547
- */
548
- function siteWorkRoot() {
549
- let directory = dirname(fileURLToPath(import.meta.url));
550
- while (!isOwnPackageRoot(directory)) {
551
- const parent = dirname(directory);
552
- if (parent === directory) throw new Error("rulebookguard package root not found");
553
- directory = parent;
554
- }
555
- const parent = dirname(directory);
556
- const modules = basename(parent) === "node_modules" ? parent : join(directory, "node_modules");
557
- return join(modules, ".rulebookguard-site");
558
- }
559
- function isOwnPackageRoot(directory) {
560
- const file = join(directory, "package.json");
561
- return existsSync(file) && JSON.parse(readFileSync(file, "utf8")).name === "rulebookguard";
562
- }
563
543
  async function assertReplaceable(out, shown) {
564
544
  let entries;
565
545
  try {
@@ -570,31 +550,6 @@ async function assertReplaceable(out, shown) {
570
550
  }
571
551
  if (entries.length > 0 && !entries.includes(MARKER)) throw new CliError(`${shown} is not empty and was not written by "rulebookguard site"; choose another --out or empty it first.`);
572
552
  }
573
- async function buildWithAstro(work) {
574
- const astro = join(dirname(siteWorkRoot()), "astro");
575
- const { bin } = JSON.parse(readFileSync(join(astro, "package.json"), "utf8"));
576
- try {
577
- await execFileAsync(process.execPath, [
578
- join(astro, bin.astro),
579
- "build",
580
- "--silent"
581
- ], {
582
- cwd: work,
583
- env: {
584
- ...process.env,
585
- ASTRO_TELEMETRY_DISABLED: "1"
586
- },
587
- maxBuffer: 67108864
588
- });
589
- } catch (error) {
590
- const { stderr, stdout } = error;
591
- throw new CliError(`The site build failed:\n${astroFailureDetail(`${stderr ?? ""}${stdout ?? ""}`)}`);
592
- }
593
- }
594
- /** The first lines of Astro's error output, without its stack trace. */
595
- function astroFailureDetail(output) {
596
- return output.split("\n").filter((line) => !/^\s*at /.test(line) && !/^\s*Stack trace:/.test(line)).filter((line) => line.trim() !== "").slice(0, 12).join("\n");
597
- }
598
553
  function astroConfig(title, language, base, sidebar) {
599
554
  return `import starlight from '@astrojs/starlight';
600
555
  import { defineConfig } from 'astro/config';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulebookguard",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Keep business rules as the single source of truth, bound to code and tests.",
5
5
  "homepage": "https://github.com/JuanCJR/rulebookguard#readme",
6
6
  "bugs": {
@@ -28,23 +28,29 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "@astrojs/markdown-remark": "^7.3.2",
32
- "@astrojs/starlight": "^0.42.6",
33
31
  "@modelcontextprotocol/server": "^2.3.1",
34
32
  "ajv": "^8.20.0",
35
- "astro": "^7.3.8",
36
33
  "picomatch": "^4.0.7",
37
34
  "yaml": "^2.9.1",
38
35
  "zod": "^4.6.7"
39
36
  },
40
37
  "devDependencies": {
41
38
  "@modelcontextprotocol/client": "2.3.1",
39
+ "@rulebookguard/site": "0.5.0",
42
40
  "@types/node": "^20.19.43",
43
41
  "@types/picomatch": "^4.0.3",
44
42
  "typescript": "^7.0.2",
45
43
  "vite-plus": "^1.4.0"
46
44
  },
45
+ "peerDependencies": {
46
+ "@rulebookguard/site": "^0.5.0"
47
+ },
48
+ "peerDependenciesMeta": {
49
+ "@rulebookguard/site": {
50
+ "optional": true
51
+ }
52
+ },
47
53
  "engines": {
48
- "node": ">=22.23.4"
54
+ "node": ">=22"
49
55
  }
50
56
  }