rulebookguard 0.3.2 → 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 +23 -3
- package/dist/cli.mjs +31 -7
- package/dist/{site-C2qLRXNK.mjs → site-BiiOPVEe.mjs} +116 -67
- package/package.json +11 -5
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
|
|
16
|
+
Requires Node.js 22 or later.
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
npx rulebookguard lint
|
|
@@ -260,15 +260,35 @@ 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
|
+
|
|
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`.
|
|
264
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
|
|
268
|
-
$ rulebookguard site --out public/rules --base /rules
|
|
276
|
+
$ rulebookguard site:generate --out public/rules --base /rules
|
|
269
277
|
public/rules
|
|
270
278
|
```
|
|
271
279
|
|
|
280
|
+
`site` is the same command.
|
|
281
|
+
|
|
282
|
+
### `site:run [--out <dir>] [--base <path>] [--port <n>]`
|
|
283
|
+
|
|
284
|
+
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.
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
$ rulebookguard site:run
|
|
288
|
+
site
|
|
289
|
+
Serving site at http://127.0.0.1:4321/ (Ctrl+C to stop)
|
|
290
|
+
```
|
|
291
|
+
|
|
272
292
|
The site has:
|
|
273
293
|
|
|
274
294
|
- **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.
|
|
83
|
+
var version = "0.5.0";
|
|
84
84
|
// src/mcp/server.ts
|
|
85
85
|
function createRulesServer(projectDir) {
|
|
86
86
|
const server = new McpServer({
|
|
@@ -329,12 +329,20 @@ 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>]
|
|
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)
|
|
337
|
+
site commands need @rulebookguard/site installed
|
|
334
338
|
|
|
335
339
|
Exit codes: 0 success, 1 failure (lint: any error), 2 usage error.
|
|
336
340
|
`;
|
|
337
341
|
const JSON_OPTION = { json: { type: "boolean" } };
|
|
342
|
+
const SITE_OPTIONS = {
|
|
343
|
+
out: { type: "string" },
|
|
344
|
+
base: { type: "string" }
|
|
345
|
+
};
|
|
338
346
|
const COMMANDS = {
|
|
339
347
|
contexts: {
|
|
340
348
|
usage: "rulebookguard contexts [--json]",
|
|
@@ -378,9 +386,17 @@ const COMMANDS = {
|
|
|
378
386
|
},
|
|
379
387
|
site: {
|
|
380
388
|
usage: "rulebookguard site [--out <dir>] [--base <path>]",
|
|
389
|
+
options: SITE_OPTIONS
|
|
390
|
+
},
|
|
391
|
+
"site:generate": {
|
|
392
|
+
usage: "rulebookguard site:generate [--out <dir>] [--base <path>]",
|
|
393
|
+
options: SITE_OPTIONS
|
|
394
|
+
},
|
|
395
|
+
"site:run": {
|
|
396
|
+
usage: "rulebookguard site:run [--out <dir>] [--base <path>] [--port <n>]",
|
|
381
397
|
options: {
|
|
382
|
-
|
|
383
|
-
|
|
398
|
+
...SITE_OPTIONS,
|
|
399
|
+
port: { type: "string" }
|
|
384
400
|
}
|
|
385
401
|
},
|
|
386
402
|
lint: {
|
|
@@ -424,11 +440,13 @@ async function dispatch(argv, io) {
|
|
|
424
440
|
await mcpCommand(io, values.project);
|
|
425
441
|
return 0;
|
|
426
442
|
}
|
|
427
|
-
if (name === "site") {
|
|
428
|
-
const { siteCommand } = await import("./site-
|
|
443
|
+
if (name === "site" || name === "site:generate" || name === "site:run") {
|
|
444
|
+
const { siteCommand } = await import("./site-BiiOPVEe.mjs");
|
|
429
445
|
await siteCommand(io, {
|
|
430
446
|
out: values.out,
|
|
431
|
-
base: values.base
|
|
447
|
+
base: values.base,
|
|
448
|
+
serve: name === "site:run",
|
|
449
|
+
port: sitePort(values.port, spec.usage)
|
|
432
450
|
});
|
|
433
451
|
return 0;
|
|
434
452
|
}
|
|
@@ -461,6 +479,12 @@ async function dispatch(argv, io) {
|
|
|
461
479
|
}
|
|
462
480
|
return 0;
|
|
463
481
|
}
|
|
482
|
+
function sitePort(value, usage) {
|
|
483
|
+
if (value === void 0) return;
|
|
484
|
+
const port = Number(value);
|
|
485
|
+
if (!/^\d+$/.test(value) || port > 65535) throw new CliError(`--port must be a number from 0 to 65535.\nUsage: ${usage}`, 2);
|
|
486
|
+
return port;
|
|
487
|
+
}
|
|
464
488
|
function parse(spec, args) {
|
|
465
489
|
let parsed;
|
|
466
490
|
try {
|
|
@@ -1,11 +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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
-
import { fileURLToPath } from "node:url";
|
|
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";
|
|
5
|
+
import { createServer } from "node:http";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
7
|
// src/site/data.ts
|
|
10
8
|
const FIELD = "";
|
|
11
9
|
async function siteData(catalog, onGitFallback) {
|
|
@@ -395,17 +393,53 @@ function demote(markdown) {
|
|
|
395
393
|
}).join("\n");
|
|
396
394
|
}
|
|
397
395
|
// src/commands/site.ts
|
|
398
|
-
const
|
|
396
|
+
const SITE_PACKAGE = "@rulebookguard/site";
|
|
399
397
|
/** Marks an output directory as written by `site`, so a rebuild may replace it. */
|
|
400
398
|
const MARKER = ".rulebookguard-site";
|
|
401
|
-
|
|
399
|
+
/**
|
|
400
|
+
* Builds the site into `--out`; with `serve`, also serves it on 127.0.0.1 until `until` settles (Ctrl+C by default).
|
|
401
|
+
*/
|
|
402
|
+
async function siteCommand(io, options, until = untilInterrupted()) {
|
|
402
403
|
const catalog = await openCatalog(io);
|
|
403
404
|
const out = resolve(io.cwd, options.out ?? "site");
|
|
404
405
|
const fromCwd = relative(io.cwd, out);
|
|
405
406
|
const shown = fromCwd === "" ? "." : fromCwd.startsWith("..") || isAbsolute(fromCwd) ? out : fromCwd;
|
|
406
407
|
await assertReplaceable(out, shown);
|
|
408
|
+
const site = await importSitePackage([catalog.root, dirname(fileURLToPath(import.meta.url))]);
|
|
407
409
|
const language = catalog.config.language ?? "en";
|
|
408
410
|
const base = normalizeBase(options.base ?? "/");
|
|
411
|
+
const server = options.serve ? await listen(out, base, options.port ?? 4321) : void 0;
|
|
412
|
+
try {
|
|
413
|
+
await buildSite(site, catalog, out, language, base, io);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
server?.close();
|
|
416
|
+
throw error;
|
|
417
|
+
}
|
|
418
|
+
writeLines(io, [shown]);
|
|
419
|
+
if (server === void 0) return;
|
|
420
|
+
const { port } = server.address();
|
|
421
|
+
writeLines(io, [`Serving ${shown} at http://127.0.0.1:${port}${base} (Ctrl+C to stop)`]);
|
|
422
|
+
await until;
|
|
423
|
+
await new Promise((closed) => {
|
|
424
|
+
server.close(() => closed());
|
|
425
|
+
server.closeAllConnections();
|
|
426
|
+
});
|
|
427
|
+
}
|
|
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) {
|
|
409
443
|
const files = sitePages(await siteData(catalog, gitFallbackWarning(io)), {
|
|
410
444
|
language,
|
|
411
445
|
base
|
|
@@ -426,45 +460,85 @@ async function siteCommand(io, options) {
|
|
|
426
460
|
}))
|
|
427
461
|
];
|
|
428
462
|
files.set("astro.config.mjs", astroConfig(text.title, language, base, sidebar));
|
|
429
|
-
const work = join(siteWorkRoot(), randomUUID());
|
|
430
463
|
try {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
464
|
+
await site.buildSite(files, out);
|
|
465
|
+
} catch (error) {
|
|
466
|
+
throw error instanceof site.SiteBuildError ? new CliError(error.message) : error;
|
|
467
|
+
}
|
|
468
|
+
await writeFile(join(out, MARKER), "");
|
|
469
|
+
}
|
|
470
|
+
function untilInterrupted() {
|
|
471
|
+
return new Promise((stop) => {
|
|
472
|
+
process.once("SIGINT", () => stop());
|
|
473
|
+
process.once("SIGTERM", () => stop());
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const CONTENT_TYPES = {
|
|
477
|
+
".html": "text/html; charset=utf-8",
|
|
478
|
+
".css": "text/css; charset=utf-8",
|
|
479
|
+
".js": "text/javascript; charset=utf-8",
|
|
480
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
481
|
+
".json": "application/json; charset=utf-8",
|
|
482
|
+
".txt": "text/plain; charset=utf-8",
|
|
483
|
+
".xml": "application/xml; charset=utf-8",
|
|
484
|
+
".svg": "image/svg+xml",
|
|
485
|
+
".png": "image/png",
|
|
486
|
+
".jpg": "image/jpeg",
|
|
487
|
+
".ico": "image/x-icon",
|
|
488
|
+
".webp": "image/webp",
|
|
489
|
+
".woff2": "font/woff2",
|
|
490
|
+
".wasm": "application/wasm"
|
|
491
|
+
};
|
|
492
|
+
function listen(out, base, port) {
|
|
493
|
+
const server = createServer((request, response) => {
|
|
494
|
+
serveFile(out, base, request, response).catch(() => {
|
|
495
|
+
response.writeHead(500).end();
|
|
439
496
|
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
recursive: true,
|
|
445
|
-
force: true
|
|
497
|
+
});
|
|
498
|
+
return new Promise((ready, fail) => {
|
|
499
|
+
server.once("error", (error) => {
|
|
500
|
+
fail(error.code === "EADDRINUSE" ? new CliError(`Port ${port} is already in use; pass another --port.`) : error);
|
|
446
501
|
});
|
|
447
|
-
|
|
448
|
-
|
|
502
|
+
server.listen(port, "127.0.0.1", () => ready(server));
|
|
503
|
+
});
|
|
449
504
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
505
|
+
async function serveFile(out, base, request, response) {
|
|
506
|
+
const path = decodeURIComponent(new URL(request.url ?? "/", "http://localhost").pathname);
|
|
507
|
+
if (`${path}/` === base) {
|
|
508
|
+
response.writeHead(301, { location: base }).end();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (!path.startsWith(base)) {
|
|
512
|
+
if (path === "/") {
|
|
513
|
+
response.writeHead(301, { location: base }).end();
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
await sendNotFound(out, response);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
let file = resolve(out, `.${sep}${path.slice(base.length)}`);
|
|
520
|
+
if (file !== out && !file.startsWith(`${out}${sep}`)) {
|
|
521
|
+
await sendNotFound(out, response);
|
|
522
|
+
return;
|
|
460
523
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
524
|
+
if ((await stat(file).catch(() => void 0))?.isDirectory()) {
|
|
525
|
+
if (!path.endsWith("/")) {
|
|
526
|
+
response.writeHead(301, { location: `${path}/` }).end();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
file = join(file, "index.html");
|
|
530
|
+
}
|
|
531
|
+
const body = await readFile(file).catch(() => void 0);
|
|
532
|
+
if (body === void 0) {
|
|
533
|
+
await sendNotFound(out, response);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
response.writeHead(200, { "content-type": CONTENT_TYPES[extname(file)] ?? "application/octet-stream" });
|
|
537
|
+
response.end(body);
|
|
464
538
|
}
|
|
465
|
-
function
|
|
466
|
-
const
|
|
467
|
-
|
|
539
|
+
async function sendNotFound(out, response) {
|
|
540
|
+
const page = await readFile(join(out, "404.html")).catch(() => "Not found");
|
|
541
|
+
response.writeHead(404, { "content-type": CONTENT_TYPES[".html"] }).end(page);
|
|
468
542
|
}
|
|
469
543
|
async function assertReplaceable(out, shown) {
|
|
470
544
|
let entries;
|
|
@@ -476,31 +550,6 @@ async function assertReplaceable(out, shown) {
|
|
|
476
550
|
}
|
|
477
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.`);
|
|
478
552
|
}
|
|
479
|
-
async function buildWithAstro(work) {
|
|
480
|
-
const astro = join(dirname(siteWorkRoot()), "astro");
|
|
481
|
-
const { bin } = JSON.parse(readFileSync(join(astro, "package.json"), "utf8"));
|
|
482
|
-
try {
|
|
483
|
-
await execFileAsync(process.execPath, [
|
|
484
|
-
join(astro, bin.astro),
|
|
485
|
-
"build",
|
|
486
|
-
"--silent"
|
|
487
|
-
], {
|
|
488
|
-
cwd: work,
|
|
489
|
-
env: {
|
|
490
|
-
...process.env,
|
|
491
|
-
ASTRO_TELEMETRY_DISABLED: "1"
|
|
492
|
-
},
|
|
493
|
-
maxBuffer: 67108864
|
|
494
|
-
});
|
|
495
|
-
} catch (error) {
|
|
496
|
-
const { stderr, stdout } = error;
|
|
497
|
-
throw new CliError(`The site build failed:\n${astroFailureDetail(`${stderr ?? ""}${stdout ?? ""}`)}`);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
/** The first lines of Astro's error output, without its stack trace. */
|
|
501
|
-
function astroFailureDetail(output) {
|
|
502
|
-
return output.split("\n").filter((line) => !/^\s*at /.test(line) && !/^\s*Stack trace:/.test(line)).filter((line) => line.trim() !== "").slice(0, 12).join("\n");
|
|
503
|
-
}
|
|
504
553
|
function astroConfig(title, language, base, sidebar) {
|
|
505
554
|
return `import starlight from '@astrojs/starlight';
|
|
506
555
|
import { defineConfig } from 'astro/config';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rulebookguard",
|
|
3
|
-
"version": "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
|
|
54
|
+
"node": ">=22"
|
|
49
55
|
}
|
|
50
56
|
}
|