@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.
- package/README.md +70 -0
- package/dist/index.js +195 -6
- 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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|