rulebookguard 0.3.2 → 0.4.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` command builds with Astro 7, which needs it).
16
+ Requires Node.js 22.23.4 or later (the `site:generate` and `site:run` commands build with Astro 7, which needs it).
17
17
 
18
18
  ```sh
19
19
  npx rulebookguard lint
@@ -260,15 +260,27 @@ An [MCP](https://modelcontextprotocol.io) server over stdio for AI agents, with
260
260
 
261
261
  The rulebook is found from `--project` (default: the current directory). A command that would fail answers a tool error carrying the same message the CLI prints, for example `Unknown rule id "R-NOPE-001". Run "rulebookguard list" to list them.`
262
262
 
263
- ### `site [--out <dir>] [--base <path>]`
263
+ ### `site:generate [--out <dir>] [--base <path>]`
264
264
 
265
265
  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
266
 
267
267
  ```sh
268
- $ rulebookguard site --out public/rules --base /rules
268
+ $ rulebookguard site:generate --out public/rules --base /rules
269
269
  public/rules
270
270
  ```
271
271
 
272
+ `site` is the same command.
273
+
274
+ ### `site:run [--out <dir>] [--base <path>] [--port <n>]`
275
+
276
+ Builds the site like `site:generate`, then serves `--out` on `127.0.0.1` (port 4321, or `--port`) and prints the URL, until Ctrl+C. Unknown paths get the site's 404 page. A taken port fails, exit 1, before building.
277
+
278
+ ```sh
279
+ $ rulebookguard site:run
280
+ site
281
+ Serving site at http://127.0.0.1:4321/ (Ctrl+C to stop)
282
+ ```
283
+
272
284
  The site has:
273
285
 
274
286
  - **Home:** the contexts map, with how many rules of each status every context holds.
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.3.2";
83
+ var version = "0.4.0";
84
84
  // src/mcp/server.ts
85
85
  function createRulesServer(projectDir) {
86
86
  const server = new McpServer({
@@ -329,12 +329,19 @@ Commands:
329
329
  and prints the rules anchored to the edited file
330
330
  mcp [--project <dir>] MCP server over stdio with rules_for and rules_show;
331
331
  --project: directory to find the rulebook from (default: cwd)
332
- site [--out <dir>] [--base <path>] build the read-only static site of the rulebook;
332
+ site:generate [--out <dir>] [--base <path>] build the read-only static site of the rulebook (alias: site);
333
333
  --out: output directory (default: site), --base: URL path
334
+ site:run [--out <dir>] [--base <path>] [--port <n>]
335
+ build the site, then serve it on 127.0.0.1 until Ctrl+C;
336
+ --port: port to serve on (default: 4321)
334
337
 
335
338
  Exit codes: 0 success, 1 failure (lint: any error), 2 usage error.
336
339
  `;
337
340
  const JSON_OPTION = { json: { type: "boolean" } };
341
+ const SITE_OPTIONS = {
342
+ out: { type: "string" },
343
+ base: { type: "string" }
344
+ };
338
345
  const COMMANDS = {
339
346
  contexts: {
340
347
  usage: "rulebookguard contexts [--json]",
@@ -378,9 +385,17 @@ const COMMANDS = {
378
385
  },
379
386
  site: {
380
387
  usage: "rulebookguard site [--out <dir>] [--base <path>]",
388
+ options: SITE_OPTIONS
389
+ },
390
+ "site:generate": {
391
+ usage: "rulebookguard site:generate [--out <dir>] [--base <path>]",
392
+ options: SITE_OPTIONS
393
+ },
394
+ "site:run": {
395
+ usage: "rulebookguard site:run [--out <dir>] [--base <path>] [--port <n>]",
381
396
  options: {
382
- out: { type: "string" },
383
- base: { type: "string" }
397
+ ...SITE_OPTIONS,
398
+ port: { type: "string" }
384
399
  }
385
400
  },
386
401
  lint: {
@@ -424,11 +439,13 @@ async function dispatch(argv, io) {
424
439
  await mcpCommand(io, values.project);
425
440
  return 0;
426
441
  }
427
- if (name === "site") {
428
- const { siteCommand } = await import("./site-C2qLRXNK.mjs");
442
+ if (name === "site" || name === "site:generate" || name === "site:run") {
443
+ const { siteCommand } = await import("./site-BgH5724T.mjs");
429
444
  await siteCommand(io, {
430
445
  out: values.out,
431
- base: values.base
446
+ base: values.base,
447
+ serve: name === "site:run",
448
+ port: sitePort(values.port, spec.usage)
432
449
  });
433
450
  return 0;
434
451
  }
@@ -461,6 +478,12 @@ async function dispatch(argv, io) {
461
478
  }
462
479
  return 0;
463
480
  }
481
+ function sitePort(value, usage) {
482
+ if (value === void 0) return;
483
+ const port = Number(value);
484
+ if (!/^\d+$/.test(value) || port > 65535) throw new CliError(`--port must be a number from 0 to 65535.\nUsage: ${usage}`, 2);
485
+ return port;
486
+ }
464
487
  function parse(spec, args) {
465
488
  let parsed;
466
489
  try {
@@ -1,10 +1,11 @@
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, readdir, rm, writeFile } from "node:fs/promises";
3
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
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
4
  import { execFile } from "node:child_process";
5
5
  import { promisify } from "node:util";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { existsSync, readFileSync } from "node:fs";
8
+ import { createServer } from "node:http";
8
9
  import { fileURLToPath } from "node:url";
9
10
  // src/site/data.ts
10
11
  const FIELD = "";
@@ -398,7 +399,10 @@ function demote(markdown) {
398
399
  const execFileAsync = promisify(execFile);
399
400
  /** Marks an output directory as written by `site`, so a rebuild may replace it. */
400
401
  const MARKER = ".rulebookguard-site";
401
- async function siteCommand(io, options) {
402
+ /**
403
+ * Builds the site into `--out`; with `serve`, also serves it on 127.0.0.1 until `until` settles (Ctrl+C by default).
404
+ */
405
+ async function siteCommand(io, options, until = untilInterrupted()) {
402
406
  const catalog = await openCatalog(io);
403
407
  const out = resolve(io.cwd, options.out ?? "site");
404
408
  const fromCwd = relative(io.cwd, out);
@@ -406,6 +410,24 @@ async function siteCommand(io, options) {
406
410
  await assertReplaceable(out, shown);
407
411
  const language = catalog.config.language ?? "en";
408
412
  const base = normalizeBase(options.base ?? "/");
413
+ const server = options.serve ? await listen(out, base, options.port ?? 4321) : void 0;
414
+ try {
415
+ await buildSite(catalog, out, language, base, io);
416
+ } catch (error) {
417
+ server?.close();
418
+ throw error;
419
+ }
420
+ writeLines(io, [shown]);
421
+ if (server === void 0) return;
422
+ const { port } = server.address();
423
+ writeLines(io, [`Serving ${shown} at http://127.0.0.1:${port}${base} (Ctrl+C to stop)`]);
424
+ await until;
425
+ await new Promise((closed) => {
426
+ server.close(() => closed());
427
+ server.closeAllConnections();
428
+ });
429
+ }
430
+ async function buildSite(catalog, out, language, base, io) {
409
431
  const files = sitePages(await siteData(catalog, gitFallbackWarning(io)), {
410
432
  language,
411
433
  base
@@ -445,7 +467,79 @@ async function siteCommand(io, options) {
445
467
  force: true
446
468
  });
447
469
  }
448
- writeLines(io, [shown]);
470
+ }
471
+ function untilInterrupted() {
472
+ return new Promise((stop) => {
473
+ process.once("SIGINT", () => stop());
474
+ process.once("SIGTERM", () => stop());
475
+ });
476
+ }
477
+ const CONTENT_TYPES = {
478
+ ".html": "text/html; charset=utf-8",
479
+ ".css": "text/css; charset=utf-8",
480
+ ".js": "text/javascript; charset=utf-8",
481
+ ".mjs": "text/javascript; charset=utf-8",
482
+ ".json": "application/json; charset=utf-8",
483
+ ".txt": "text/plain; charset=utf-8",
484
+ ".xml": "application/xml; charset=utf-8",
485
+ ".svg": "image/svg+xml",
486
+ ".png": "image/png",
487
+ ".jpg": "image/jpeg",
488
+ ".ico": "image/x-icon",
489
+ ".webp": "image/webp",
490
+ ".woff2": "font/woff2",
491
+ ".wasm": "application/wasm"
492
+ };
493
+ function listen(out, base, port) {
494
+ const server = createServer((request, response) => {
495
+ serveFile(out, base, request, response).catch(() => {
496
+ response.writeHead(500).end();
497
+ });
498
+ });
499
+ return new Promise((ready, fail) => {
500
+ server.once("error", (error) => {
501
+ fail(error.code === "EADDRINUSE" ? new CliError(`Port ${port} is already in use; pass another --port.`) : error);
502
+ });
503
+ server.listen(port, "127.0.0.1", () => ready(server));
504
+ });
505
+ }
506
+ async function serveFile(out, base, request, response) {
507
+ const path = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
508
+ if (`${path}/` === base) {
509
+ response.writeHead(301, { location: base }).end();
510
+ return;
511
+ }
512
+ if (!path.startsWith(base)) {
513
+ if (path === "/") {
514
+ response.writeHead(301, { location: base }).end();
515
+ return;
516
+ }
517
+ await sendNotFound(out, response);
518
+ return;
519
+ }
520
+ let file = resolve(out, `.${sep}${path.slice(base.length)}`);
521
+ if (file !== out && !file.startsWith(`${out}${sep}`)) {
522
+ await sendNotFound(out, response);
523
+ return;
524
+ }
525
+ if ((await stat(file).catch(() => void 0))?.isDirectory()) {
526
+ if (!path.endsWith("/")) {
527
+ response.writeHead(301, { location: `${path}/` }).end();
528
+ return;
529
+ }
530
+ file = join(file, "index.html");
531
+ }
532
+ const body = await readFile(file).catch(() => void 0);
533
+ if (body === void 0) {
534
+ await sendNotFound(out, response);
535
+ return;
536
+ }
537
+ response.writeHead(200, { "content-type": CONTENT_TYPES[extname(file)] ?? "application/octet-stream" });
538
+ response.end(body);
539
+ }
540
+ async function sendNotFound(out, response) {
541
+ const page = await readFile(join(out, "404.html")).catch(() => "Not found");
542
+ response.writeHead(404, { "content-type": CONTENT_TYPES[".html"] }).end(page);
449
543
  }
450
544
  /**
451
545
  * Starlight injects its routes as package specifiers that Astro resolves from the project root, so the generated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulebookguard",
3
- "version": "0.3.2",
3
+ "version": "0.4.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": {