@schemasentry/cli 0.2.0 → 0.3.2

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.
Files changed (3) hide show
  1. package/README.md +70 -0
  2. package/dist/index.js +195 -6
  3. package/package.json +3 -2
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @schemasentry/cli
2
+
3
+ CLI for Schema Sentry validation and reporting.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add -D @schemasentry/cli
9
+ npm install -D @schemasentry/cli
10
+ ```
11
+
12
+ ## Commands
13
+
14
+ ### `init`
15
+
16
+ Generate starter manifest and data files:
17
+
18
+ ```bash
19
+ pnpm schemasentry init
20
+ ```
21
+
22
+ With route scanning:
23
+
24
+ ```bash
25
+ pnpm schemasentry init --scan
26
+ ```
27
+
28
+ ### `validate`
29
+
30
+ Check schema coverage and validation:
31
+
32
+ ```bash
33
+ pnpm schemasentry validate --manifest ./schema-sentry.manifest.json --data ./schema-sentry.data.json
34
+ ```
35
+
36
+ With GitHub annotations:
37
+
38
+ ```bash
39
+ pnpm schemasentry validate --annotations github
40
+ ```
41
+
42
+ ### `audit`
43
+
44
+ Analyze site-wide schema health:
45
+
46
+ ```bash
47
+ pnpm schemasentry audit --data ./schema-sentry.data.json --manifest ./schema-sentry.manifest.json
48
+ ```
49
+
50
+ With HTML report:
51
+
52
+ ```bash
53
+ pnpm schemasentry audit \
54
+ --data ./schema-sentry.data.json \
55
+ --format html \
56
+ --output ./report.html
57
+ ```
58
+
59
+ ## Options
60
+
61
+ | Option | Description |
62
+ |--------|-------------|
63
+ | `--format json\|html` | Output format |
64
+ | `--annotations none\|github` | CI annotations |
65
+ | `-o, --output <path>` | Write output to file |
66
+ | `--recommended / --no-recommended` | Enable recommended field checks |
67
+
68
+ ## Documentation
69
+
70
+ See [Schema Sentry Docs](https://github.com/arindamdawn/schema-sentry#readme)
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import { readFile } from "fs/promises";
5
+ import { mkdir, readFile, writeFile } from "fs/promises";
6
6
  import { readFileSync } from "fs";
7
7
  import path4 from "path";
8
8
  import { stableStringify as stableStringify2 } from "@schemasentry/core";
@@ -537,6 +537,123 @@ var walkDir = async (dirPath) => {
537
537
  return files;
538
538
  };
539
539
 
540
+ // src/html.ts
541
+ var escapeHtml = (value) => String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
542
+ var renderRouteIssues = (route) => {
543
+ if (route.issues.length === 0) {
544
+ return '<p class="muted">No issues.</p>';
545
+ }
546
+ const items = route.issues.map((issue) => {
547
+ const severityClass = issue.severity === "error" ? "sev-error" : "sev-warn";
548
+ return `<li class="${severityClass}">
549
+ <span class="sev">${escapeHtml(issue.severity.toUpperCase())}</span>
550
+ <code>${escapeHtml(issue.ruleId)}</code>
551
+ <span>${escapeHtml(issue.message)}</span>
552
+ <small>${escapeHtml(issue.path)}</small>
553
+ </li>`;
554
+ }).join("");
555
+ return `<ul class="issues">${items}</ul>`;
556
+ };
557
+ var renderRoute = (route) => {
558
+ const statusClass = route.ok ? "ok" : "fail";
559
+ const expected = route.expectedTypes.length ? route.expectedTypes.join(", ") : "(none)";
560
+ const found = route.foundTypes.length ? route.foundTypes.join(", ") : "(none)";
561
+ return `<section class="route">
562
+ <header>
563
+ <h3>${escapeHtml(route.route)}</h3>
564
+ <span class="badge ${statusClass}">${route.ok ? "OK" : "FAIL"}</span>
565
+ </header>
566
+ <p><strong>Score:</strong> ${route.score}</p>
567
+ <p><strong>Expected types:</strong> ${escapeHtml(expected)}</p>
568
+ <p><strong>Found types:</strong> ${escapeHtml(found)}</p>
569
+ ${renderRouteIssues(route)}
570
+ </section>`;
571
+ };
572
+ var renderCoverage = (report) => {
573
+ if (!report.summary.coverage) {
574
+ return "<li><strong>Coverage:</strong> not enabled</li>";
575
+ }
576
+ const coverage = report.summary.coverage;
577
+ return `<li><strong>Coverage:</strong> missing_routes=${coverage.missingRoutes} missing_types=${coverage.missingTypes} unlisted_routes=${coverage.unlistedRoutes}</li>`;
578
+ };
579
+ var renderHtmlReport = (report, options) => {
580
+ const title = escapeHtml(options.title);
581
+ const generatedAt = escapeHtml(
582
+ (options.generatedAt ?? /* @__PURE__ */ new Date()).toISOString()
583
+ );
584
+ const routes = report.routes.map(renderRoute).join("");
585
+ return `<!doctype html>
586
+ <html lang="en">
587
+ <head>
588
+ <meta charset="utf-8" />
589
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
590
+ <title>${title}</title>
591
+ <style>
592
+ :root { color-scheme: light dark; }
593
+ body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, sans-serif; margin: 24px; line-height: 1.45; }
594
+ h1, h2, h3 { margin: 0 0 8px; }
595
+ .muted { color: #666; }
596
+ .summary, .routes { margin-top: 20px; }
597
+ .route { border: 1px solid #ddd; border-radius: 8px; padding: 12px; margin: 12px 0; }
598
+ .route header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
599
+ .badge { padding: 2px 8px; border-radius: 999px; font-size: 12px; font-weight: 600; }
600
+ .badge.ok { background: #dcfce7; color: #166534; }
601
+ .badge.fail { background: #fee2e2; color: #991b1b; }
602
+ .issues { margin: 10px 0 0; padding-left: 16px; }
603
+ .issues li { margin: 8px 0; display: grid; gap: 2px; }
604
+ .sev { font-weight: 700; }
605
+ .sev-error .sev { color: #991b1b; }
606
+ .sev-warn .sev { color: #92400e; }
607
+ code { background: #f5f5f5; padding: 1px 5px; border-radius: 4px; }
608
+ </style>
609
+ </head>
610
+ <body>
611
+ <h1>${title}</h1>
612
+ <p class="muted">Generated at ${generatedAt}</p>
613
+
614
+ <section class="summary">
615
+ <h2>Summary</h2>
616
+ <ul>
617
+ <li><strong>OK:</strong> ${report.ok}</li>
618
+ <li><strong>Routes:</strong> ${report.summary.routes}</li>
619
+ <li><strong>Errors:</strong> ${report.summary.errors}</li>
620
+ <li><strong>Warnings:</strong> ${report.summary.warnings}</li>
621
+ <li><strong>Score:</strong> ${report.summary.score}</li>
622
+ ${renderCoverage(report)}
623
+ </ul>
624
+ </section>
625
+
626
+ <section class="routes">
627
+ <h2>Routes</h2>
628
+ ${routes}
629
+ </section>
630
+ </body>
631
+ </html>`;
632
+ };
633
+
634
+ // src/annotations.ts
635
+ var escapeCommandValue = (value) => value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
636
+ var escapeCommandProperty = (value) => escapeCommandValue(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
637
+ var formatIssueMessage = (route, issue) => `[${route}] ${issue.ruleId}: ${issue.message} (${issue.path})`;
638
+ var buildGitHubAnnotationLines = (report, commandLabel) => {
639
+ const title = escapeCommandProperty(`Schema Sentry ${commandLabel}`);
640
+ const lines = [];
641
+ for (const route of report.routes) {
642
+ for (const issue of route.issues) {
643
+ const level = issue.severity === "error" ? "error" : "warning";
644
+ const message = escapeCommandValue(formatIssueMessage(route.route, issue));
645
+ lines.push(`::${level} title=${title}::${message}`);
646
+ }
647
+ }
648
+ return lines;
649
+ };
650
+ var emitGitHubAnnotations = (report, commandLabel) => {
651
+ const lines = buildGitHubAnnotationLines(report, commandLabel);
652
+ for (const line of lines) {
653
+ console.error(line);
654
+ }
655
+ };
656
+
540
657
  // src/index.ts
541
658
  import { createInterface } from "readline/promises";
542
659
  import { stdin as input, stdout as output } from "process";
@@ -550,8 +667,10 @@ program.command("validate").description("Validate schema coverage and rules").op
550
667
  "-d, --data <path>",
551
668
  "Path to schema data JSON",
552
669
  "schema-sentry.data.json"
553
- ).option("-c, --config <path>", "Path to config JSON").option("--recommended", "Enable recommended field checks").option("--no-recommended", "Disable recommended field checks").action(async (options) => {
670
+ ).option("-c, --config <path>", "Path to config JSON").option("--format <format>", "Report format (json|html)", "json").option("--annotations <provider>", "Emit CI annotations (none|github)", "none").option("-o, --output <path>", "Write report output to file").option("--recommended", "Enable recommended field checks").option("--no-recommended", "Disable recommended field checks").action(async (options) => {
554
671
  const start = Date.now();
672
+ const format = resolveOutputFormat(options.format);
673
+ const annotationsMode = resolveAnnotationsMode(options.annotations);
555
674
  const recommended = await resolveRecommendedOption(options.config);
556
675
  const manifestPath = path4.resolve(process.cwd(), options.manifest);
557
676
  const dataPath = path4.resolve(process.cwd(), options.data);
@@ -622,7 +741,13 @@ program.command("validate").description("Validate schema coverage and rules").op
622
741
  return;
623
742
  }
624
743
  const report = buildReport(manifest, data, { recommended });
625
- console.log(formatReportOutput(report));
744
+ await emitReport({
745
+ report,
746
+ format,
747
+ outputPath: options.output,
748
+ title: "Schema Sentry Validate Report"
749
+ });
750
+ emitAnnotations(report, annotationsMode, "validate");
626
751
  printValidateSummary(report, Date.now() - start);
627
752
  process.exit(report.ok ? 0 : 1);
628
753
  });
@@ -664,8 +789,10 @@ program.command("audit").description("Analyze schema health and report issues").
664
789
  "-d, --data <path>",
665
790
  "Path to schema data JSON",
666
791
  "schema-sentry.data.json"
667
- ).option("-m, --manifest <path>", "Path to manifest JSON (optional)").option("--scan", "Scan the filesystem for routes").option("--root <path>", "Project root for scanning", ".").option("-c, --config <path>", "Path to config JSON").option("--recommended", "Enable recommended field checks").option("--no-recommended", "Disable recommended field checks").action(async (options) => {
792
+ ).option("-m, --manifest <path>", "Path to manifest JSON (optional)").option("--scan", "Scan the filesystem for routes").option("--root <path>", "Project root for scanning", ".").option("-c, --config <path>", "Path to config JSON").option("--format <format>", "Report format (json|html)", "json").option("--annotations <provider>", "Emit CI annotations (none|github)", "none").option("-o, --output <path>", "Write report output to file").option("--recommended", "Enable recommended field checks").option("--no-recommended", "Disable recommended field checks").action(async (options) => {
668
793
  const start = Date.now();
794
+ const format = resolveOutputFormat(options.format);
795
+ const annotationsMode = resolveAnnotationsMode(options.annotations);
669
796
  const recommended = await resolveRecommendedOption(options.config);
670
797
  const dataPath = path4.resolve(process.cwd(), options.data);
671
798
  let dataRaw;
@@ -746,7 +873,13 @@ program.command("audit").description("Analyze schema health and report issues").
746
873
  manifest,
747
874
  requiredRoutes: requiredRoutes.length > 0 ? requiredRoutes : void 0
748
875
  });
749
- console.log(formatReportOutput(report));
876
+ await emitReport({
877
+ report,
878
+ format,
879
+ outputPath: options.output,
880
+ title: "Schema Sentry Audit Report"
881
+ });
882
+ emitAnnotations(report, annotationsMode, "audit");
750
883
  printAuditSummary(report, Boolean(manifest), Date.now() - start);
751
884
  process.exit(report.ok ? 0 : 1);
752
885
  });
@@ -780,9 +913,65 @@ function isSchemaData(value) {
780
913
  }
781
914
  return true;
782
915
  }
783
- function formatReportOutput(report) {
916
+ function resolveOutputFormat(value) {
917
+ const format = (value ?? "json").trim().toLowerCase();
918
+ if (format === "json" || format === "html") {
919
+ return format;
920
+ }
921
+ printCliError(
922
+ "output.invalid_format",
923
+ `Unsupported report format '${value ?? ""}'`,
924
+ "Use --format json or --format html."
925
+ );
926
+ process.exit(1);
927
+ return "json";
928
+ }
929
+ function resolveAnnotationsMode(value) {
930
+ const mode = (value ?? "none").trim().toLowerCase();
931
+ if (mode === "none" || mode === "github") {
932
+ return mode;
933
+ }
934
+ printCliError(
935
+ "annotations.invalid_provider",
936
+ `Unsupported annotations provider '${value ?? ""}'`,
937
+ "Use --annotations none or --annotations github."
938
+ );
939
+ process.exit(1);
940
+ return "none";
941
+ }
942
+ function formatReportOutput(report, format, title) {
943
+ if (format === "html") {
944
+ return renderHtmlReport(report, { title });
945
+ }
784
946
  return stableStringify2(report);
785
947
  }
948
+ async function emitReport(options) {
949
+ const { report, format, outputPath, title } = options;
950
+ const content = formatReportOutput(report, format, title);
951
+ if (!outputPath) {
952
+ console.log(content);
953
+ return;
954
+ }
955
+ const resolvedPath = path4.resolve(process.cwd(), outputPath);
956
+ try {
957
+ await mkdir(path4.dirname(resolvedPath), { recursive: true });
958
+ await writeFile(resolvedPath, content, "utf8");
959
+ console.error(`Report written to ${resolvedPath}`);
960
+ } catch (error) {
961
+ const reason = error instanceof Error && error.message.length > 0 ? error.message : "Unknown file system error";
962
+ printCliError(
963
+ "output.write_failed",
964
+ `Could not write report to ${resolvedPath}: ${reason}`
965
+ );
966
+ process.exit(1);
967
+ }
968
+ }
969
+ function emitAnnotations(report, mode, commandLabel) {
970
+ if (mode !== "github") {
971
+ return;
972
+ }
973
+ emitGitHubAnnotations(report, commandLabel);
974
+ }
786
975
  function printCliError(code, message, suggestion) {
787
976
  console.error(
788
977
  stableStringify2({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schemasentry/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for Schema Sentry validation and reporting.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,10 +33,11 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "commander": "^12.0.0",
36
- "@schemasentry/core": "0.2.0"
36
+ "@schemasentry/core": "0.3.2"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.build.json",
40
+ "benchmark:200": "node ./scripts/benchmark-200-routes.mjs",
40
41
  "lint": "echo \"lint not configured\" && exit 0",
41
42
  "test": "vitest run"
42
43
  }