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 +15 -3
- package/dist/cli.mjs +30 -7
- package/dist/{site-C2qLRXNK.mjs → site-BgH5724T.mjs} +98 -4
- package/package.json +1 -1
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`
|
|
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.
|
|
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>]
|
|
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
|
-
|
|
383
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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