@schemasentry/cli 0.4.0 → 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.
Files changed (3) hide show
  1. package/README.md +37 -1
  2. package/dist/index.js +308 -20
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -81,6 +81,40 @@ pnpm schemasentry collect \
81
81
  --data ./schema-sentry.data.json
82
82
  ```
83
83
 
84
+ ### `scaffold`
85
+
86
+ Auto-generate schema stubs for routes without schema (dry-run by default):
87
+
88
+ ```bash
89
+ pnpm schemasentry scaffold --manifest ./schema-sentry.manifest.json --data ./schema-sentry.data.json
90
+ ```
91
+
92
+ Preview what would be generated without writing files:
93
+
94
+ ```bash
95
+ pnpm schemasentry scaffold
96
+ ```
97
+
98
+ Apply scaffolded schema to your files:
99
+
100
+ ```bash
101
+ pnpm schemasentry scaffold --write
102
+ ```
103
+
104
+ Skip confirmation prompts:
105
+
106
+ ```bash
107
+ pnpm schemasentry scaffold --write --force
108
+ ```
109
+
110
+ **Pattern-based auto-detection** infers schema types from URL patterns:
111
+ - `/blog/*` → BlogPosting
112
+ - `/products/*` → Product
113
+ - `/faq` → FAQPage
114
+ - `/events/*` → Event
115
+ - `/howto/*` → HowTo
116
+ - and more...
117
+
84
118
  ## Options
85
119
 
86
120
  | Option | Description |
@@ -88,10 +122,12 @@ pnpm schemasentry collect \
88
122
  | `--format json\|html` | Output format |
89
123
  | `--annotations none\|github` | CI annotations |
90
124
  | `-o, --output <path>` | Write output to file |
91
- | `--root <path>` | Root directory to scan for HTML output (`collect`) |
125
+ | `--root <path>` | Root directory to scan (`collect`, `scaffold`) |
92
126
  | `--routes <routes...>` | Collect only specific routes (`collect`) |
93
127
  | `--strict-routes` | Fail when any route passed to `--routes` is missing (`collect`) |
94
128
  | `--check` | Compare collected output with existing data and fail on drift (`collect`) |
129
+ | `--write` | Apply scaffolded changes to files (`scaffold`) |
130
+ | `--force` | Skip confirmation prompts (`scaffold`) |
95
131
  | `--recommended / --no-recommended` | Enable recommended field checks |
96
132
 
97
133
  ## Documentation
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  import { Command } from "commander";
5
5
  import { mkdir, readFile, writeFile } from "fs/promises";
6
6
  import { readFileSync } from "fs";
7
- import path5 from "path";
8
- import { stableStringify as stableStringify3 } from "@schemasentry/core";
7
+ import path6 from "path";
8
+ import { stableStringify as stableStringify4 } from "@schemasentry/core";
9
9
 
10
10
  // src/report.ts
11
11
  import {
@@ -877,6 +877,235 @@ var schemaTypeLabel = (node) => {
877
877
  return typeof type === "string" && type.trim().length > 0 ? type : "(unknown)";
878
878
  };
879
879
 
880
+ // src/scaffold.ts
881
+ import { promises as fs5 } from "fs";
882
+ import path5 from "path";
883
+ import { stableStringify as stableStringify3 } from "@schemasentry/core";
884
+
885
+ // src/patterns.ts
886
+ var DEFAULT_PATTERNS = [
887
+ { pattern: "/blog/*", schemaType: "BlogPosting", priority: 10 },
888
+ { pattern: "/blog", schemaType: "WebPage", priority: 5 },
889
+ { pattern: "/products/*", schemaType: "Product", priority: 10 },
890
+ { pattern: "/product/*", schemaType: "Product", priority: 10 },
891
+ { pattern: "/faq", schemaType: "FAQPage", priority: 10 },
892
+ { pattern: "/faqs", schemaType: "FAQPage", priority: 10 },
893
+ { pattern: "/how-to/*", schemaType: "HowTo", priority: 10 },
894
+ { pattern: "/howto/*", schemaType: "HowTo", priority: 10 },
895
+ { pattern: "/events/*", schemaType: "Event", priority: 10 },
896
+ { pattern: "/event/*", schemaType: "Event", priority: 10 },
897
+ { pattern: "/reviews/*", schemaType: "Review", priority: 10 },
898
+ { pattern: "/review/*", schemaType: "Review", priority: 10 },
899
+ { pattern: "/videos/*", schemaType: "VideoObject", priority: 10 },
900
+ { pattern: "/video/*", schemaType: "VideoObject", priority: 10 },
901
+ { pattern: "/images/*", schemaType: "ImageObject", priority: 10 },
902
+ { pattern: "/image/*", schemaType: "ImageObject", priority: 10 },
903
+ { pattern: "/about", schemaType: "WebPage", priority: 10 },
904
+ { pattern: "/contact", schemaType: "WebPage", priority: 10 },
905
+ { pattern: "/", schemaType: "WebSite", priority: 1 }
906
+ ];
907
+ var matchRouteToPatterns = (route, patterns = DEFAULT_PATTERNS) => {
908
+ const matches = [];
909
+ for (const rule of patterns) {
910
+ if (routeMatchesPattern(route, rule.pattern)) {
911
+ matches.push({
912
+ type: rule.schemaType,
913
+ priority: rule.priority ?? 5
914
+ });
915
+ }
916
+ }
917
+ matches.sort((a, b) => b.priority - a.priority);
918
+ return [...new Set(matches.map((m) => m.type))];
919
+ };
920
+ var routeMatchesPattern = (route, pattern) => {
921
+ if (pattern === route) {
922
+ return true;
923
+ }
924
+ if (pattern.endsWith("/*")) {
925
+ const prefix = pattern.slice(0, -1);
926
+ return route.startsWith(prefix);
927
+ }
928
+ const patternRegex = pattern.replace(/\*/g, "[^/]+").replace(/\?/g, ".");
929
+ const regex = new RegExp(`^${patternRegex}$`);
930
+ return regex.test(route);
931
+ };
932
+ var inferSchemaTypes = (routes, customPatterns) => {
933
+ const patterns = customPatterns ?? DEFAULT_PATTERNS;
934
+ const result = /* @__PURE__ */ new Map();
935
+ for (const route of routes) {
936
+ const types = matchRouteToPatterns(route, patterns);
937
+ if (types.length > 0) {
938
+ result.set(route, types);
939
+ }
940
+ }
941
+ return result;
942
+ };
943
+ var generateManifestEntries = (routes, customPatterns) => {
944
+ const inferred = inferSchemaTypes(routes, customPatterns);
945
+ const entries = {};
946
+ for (const [route, types] of inferred) {
947
+ entries[route] = types;
948
+ }
949
+ return entries;
950
+ };
951
+
952
+ // src/scaffold.ts
953
+ var scaffoldSchema = async (options) => {
954
+ const manifest = await loadManifest(options.manifestPath);
955
+ const data = await loadData(options.dataPath);
956
+ const discoveredRoutes = await scanRoutes({ rootDir: options.rootDir });
957
+ const routesNeedingSchema = discoveredRoutes.filter(
958
+ (route) => !data.routes[route] || data.routes[route].length === 0
959
+ );
960
+ const inferredTypes = inferSchemaTypes(routesNeedingSchema, options.customPatterns);
961
+ const manifestEntries = generateManifestEntries(
962
+ routesNeedingSchema,
963
+ options.customPatterns
964
+ );
965
+ const generatedSchemas = /* @__PURE__ */ new Map();
966
+ for (const [route, types] of inferredTypes) {
967
+ const schemas = types.map((type) => generateSchemaStub(type, route));
968
+ generatedSchemas.set(route, schemas);
969
+ }
970
+ const wouldUpdate = routesNeedingSchema.length > 0;
971
+ return {
972
+ routesToScaffold: routesNeedingSchema,
973
+ generatedSchemas,
974
+ manifestUpdates: manifestEntries,
975
+ wouldUpdate
976
+ };
977
+ };
978
+ var loadManifest = async (manifestPath) => {
979
+ try {
980
+ const raw = await fs5.readFile(manifestPath, "utf8");
981
+ return JSON.parse(raw);
982
+ } catch {
983
+ return { routes: {} };
984
+ }
985
+ };
986
+ var loadData = async (dataPath) => {
987
+ try {
988
+ const raw = await fs5.readFile(dataPath, "utf8");
989
+ return JSON.parse(raw);
990
+ } catch {
991
+ return { routes: {} };
992
+ }
993
+ };
994
+ var generateSchemaStub = (type, route) => {
995
+ const base = {
996
+ "@context": "https://schema.org",
997
+ "@type": type
998
+ };
999
+ switch (type) {
1000
+ case "BlogPosting":
1001
+ return {
1002
+ ...base,
1003
+ headline: "Blog Post Title",
1004
+ author: {
1005
+ "@type": "Person",
1006
+ name: "Author Name"
1007
+ },
1008
+ datePublished: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
1009
+ url: route
1010
+ };
1011
+ case "Product":
1012
+ return {
1013
+ ...base,
1014
+ name: "Product Name",
1015
+ description: "Product description",
1016
+ offers: {
1017
+ "@type": "Offer",
1018
+ price: "0.00",
1019
+ priceCurrency: "USD"
1020
+ }
1021
+ };
1022
+ case "FAQPage":
1023
+ return {
1024
+ ...base,
1025
+ mainEntity: []
1026
+ };
1027
+ case "HowTo":
1028
+ return {
1029
+ ...base,
1030
+ name: "How-To Title",
1031
+ step: []
1032
+ };
1033
+ case "Event":
1034
+ return {
1035
+ ...base,
1036
+ name: "Event Name",
1037
+ startDate: (/* @__PURE__ */ new Date()).toISOString()
1038
+ };
1039
+ case "Organization":
1040
+ return {
1041
+ ...base,
1042
+ name: "Organization Name",
1043
+ url: route
1044
+ };
1045
+ case "WebSite":
1046
+ return {
1047
+ ...base,
1048
+ name: "Website Name",
1049
+ url: route
1050
+ };
1051
+ case "Article":
1052
+ return {
1053
+ ...base,
1054
+ headline: "Article Headline",
1055
+ author: {
1056
+ "@type": "Person",
1057
+ name: "Author Name"
1058
+ },
1059
+ datePublished: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
1060
+ };
1061
+ default:
1062
+ return {
1063
+ ...base,
1064
+ name: `${type} Name`
1065
+ };
1066
+ }
1067
+ };
1068
+ var formatScaffoldPreview = (result) => {
1069
+ if (result.routesToScaffold.length === 0) {
1070
+ return "No routes need schema generation.";
1071
+ }
1072
+ const lines = [
1073
+ `Routes to scaffold: ${result.routesToScaffold.length}`,
1074
+ ""
1075
+ ];
1076
+ for (const route of result.routesToScaffold) {
1077
+ const types = result.manifestUpdates[route] || [];
1078
+ lines.push(` ${route}`);
1079
+ lines.push(` Schema types: ${types.join(", ") || "None detected"}`);
1080
+ }
1081
+ return lines.join("\n");
1082
+ };
1083
+ var applyScaffold = async (result, options) => {
1084
+ if (!result.wouldUpdate) {
1085
+ return;
1086
+ }
1087
+ const manifest = await loadManifest(options.manifestPath);
1088
+ const data = await loadData(options.dataPath);
1089
+ for (const [route, types] of Object.entries(result.manifestUpdates)) {
1090
+ if (!manifest.routes[route]) {
1091
+ manifest.routes[route] = types;
1092
+ }
1093
+ }
1094
+ for (const [route, schemas] of result.generatedSchemas) {
1095
+ if (!data.routes[route]) {
1096
+ data.routes[route] = schemas;
1097
+ }
1098
+ }
1099
+ await fs5.mkdir(path5.dirname(options.manifestPath), { recursive: true });
1100
+ await fs5.mkdir(path5.dirname(options.dataPath), { recursive: true });
1101
+ await fs5.writeFile(
1102
+ options.manifestPath,
1103
+ stableStringify3(manifest),
1104
+ "utf8"
1105
+ );
1106
+ await fs5.writeFile(options.dataPath, stableStringify3(data), "utf8");
1107
+ };
1108
+
880
1109
  // src/index.ts
881
1110
  import { createInterface } from "readline/promises";
882
1111
  import { stdin as input, stdout as output } from "process";
@@ -895,8 +1124,8 @@ program.command("validate").description("Validate schema coverage and rules").op
895
1124
  const format = resolveOutputFormat(options.format);
896
1125
  const annotationsMode = resolveAnnotationsMode(options.annotations);
897
1126
  const recommended = await resolveRecommendedOption(options.config);
898
- const manifestPath = path5.resolve(process.cwd(), options.manifest);
899
- const dataPath = path5.resolve(process.cwd(), options.data);
1127
+ const manifestPath = path6.resolve(process.cwd(), options.manifest);
1128
+ const dataPath = path6.resolve(process.cwd(), options.data);
900
1129
  let raw;
901
1130
  try {
902
1131
  raw = await readFile(manifestPath, "utf8");
@@ -983,12 +1212,12 @@ program.command("init").description("Interactive setup wizard").option(
983
1212
  "Path to schema data JSON",
984
1213
  "schema-sentry.data.json"
985
1214
  ).option("-y, --yes", "Use defaults and skip prompts").option("-f, --force", "Overwrite existing files").option("--scan", "Scan the filesystem for routes and add WebPage entries").option("--root <path>", "Project root for scanning", ".").action(async (options) => {
986
- const manifestPath = path5.resolve(process.cwd(), options.manifest);
987
- const dataPath = path5.resolve(process.cwd(), options.data);
1215
+ const manifestPath = path6.resolve(process.cwd(), options.manifest);
1216
+ const dataPath = path6.resolve(process.cwd(), options.data);
988
1217
  const force = options.force ?? false;
989
1218
  const useDefaults = options.yes ?? false;
990
1219
  const answers = useDefaults ? getDefaultAnswers() : await promptAnswers();
991
- const scannedRoutes = options.scan ? await scanRoutes({ rootDir: path5.resolve(process.cwd(), options.root ?? ".") }) : [];
1220
+ const scannedRoutes = options.scan ? await scanRoutes({ rootDir: path6.resolve(process.cwd(), options.root ?? ".") }) : [];
992
1221
  if (options.scan && scannedRoutes.length === 0) {
993
1222
  console.error("No routes found during scan.");
994
1223
  }
@@ -1017,7 +1246,7 @@ program.command("audit").description("Analyze schema health and report issues").
1017
1246
  const format = resolveOutputFormat(options.format);
1018
1247
  const annotationsMode = resolveAnnotationsMode(options.annotations);
1019
1248
  const recommended = await resolveRecommendedOption(options.config);
1020
- const dataPath = path5.resolve(process.cwd(), options.data);
1249
+ const dataPath = path6.resolve(process.cwd(), options.data);
1021
1250
  let dataRaw;
1022
1251
  try {
1023
1252
  dataRaw = await readFile(dataPath, "utf8");
@@ -1053,7 +1282,7 @@ program.command("audit").description("Analyze schema health and report issues").
1053
1282
  }
1054
1283
  let manifest;
1055
1284
  if (options.manifest) {
1056
- const manifestPath = path5.resolve(process.cwd(), options.manifest);
1285
+ const manifestPath = path6.resolve(process.cwd(), options.manifest);
1057
1286
  let manifestRaw;
1058
1287
  try {
1059
1288
  manifestRaw = await readFile(manifestPath, "utf8");
@@ -1087,7 +1316,7 @@ program.command("audit").description("Analyze schema health and report issues").
1087
1316
  return;
1088
1317
  }
1089
1318
  }
1090
- const requiredRoutes = options.scan ? await scanRoutes({ rootDir: path5.resolve(process.cwd(), options.root ?? ".") }) : [];
1319
+ const requiredRoutes = options.scan ? await scanRoutes({ rootDir: path6.resolve(process.cwd(), options.root ?? ".") }) : [];
1091
1320
  if (options.scan && requiredRoutes.length === 0) {
1092
1321
  console.error("No routes found during scan.");
1093
1322
  }
@@ -1113,7 +1342,7 @@ program.command("collect").description("Collect JSON-LD blocks from built HTML o
1113
1342
  ).action(async (options) => {
1114
1343
  const start = Date.now();
1115
1344
  const format = resolveCollectOutputFormat(options.format);
1116
- const rootDir = path5.resolve(process.cwd(), options.root ?? ".");
1345
+ const rootDir = path6.resolve(process.cwd(), options.root ?? ".");
1117
1346
  const check = options.check ?? false;
1118
1347
  const requestedRoutes = normalizeRouteFilter(options.routes ?? []);
1119
1348
  const strictRoutes = options.strictRoutes ?? false;
@@ -1150,7 +1379,7 @@ program.command("collect").description("Collect JSON-LD blocks from built HTML o
1150
1379
  }
1151
1380
  let driftDetected = false;
1152
1381
  if (check) {
1153
- const existingPath = path5.resolve(process.cwd(), options.data);
1382
+ const existingPath = path6.resolve(process.cwd(), options.data);
1154
1383
  let existingRaw;
1155
1384
  try {
1156
1385
  existingRaw = await readFile(existingPath, "utf8");
@@ -1195,9 +1424,9 @@ program.command("collect").description("Collect JSON-LD blocks from built HTML o
1195
1424
  }
1196
1425
  const content = formatCollectOutput(collected.data, format);
1197
1426
  if (options.output) {
1198
- const resolvedPath = path5.resolve(process.cwd(), options.output);
1427
+ const resolvedPath = path6.resolve(process.cwd(), options.output);
1199
1428
  try {
1200
- await mkdir(path5.dirname(resolvedPath), { recursive: true });
1429
+ await mkdir(path6.dirname(resolvedPath), { recursive: true });
1201
1430
  await writeFile(resolvedPath, `${content}
1202
1431
  `, "utf8");
1203
1432
  console.error(`Collected data written to ${resolvedPath}`);
@@ -1225,6 +1454,65 @@ program.command("collect").description("Collect JSON-LD blocks from built HTML o
1225
1454
  });
1226
1455
  process.exit(driftDetected ? 1 : 0);
1227
1456
  });
1457
+ program.command("scaffold").description("Generate schema stubs for routes without schema (dry-run by default)").option(
1458
+ "-m, --manifest <path>",
1459
+ "Path to manifest JSON",
1460
+ "schema-sentry.manifest.json"
1461
+ ).option(
1462
+ "-d, --data <path>",
1463
+ "Path to schema data JSON",
1464
+ "schema-sentry.data.json"
1465
+ ).option("--root <path>", "Project root for scanning", ".").option("--write", "Apply changes (default is dry-run)").option("-f, --force", "Skip confirmation prompts").action(async (options) => {
1466
+ const start = Date.now();
1467
+ const manifestPath = path6.resolve(process.cwd(), options.manifest);
1468
+ const dataPath = path6.resolve(process.cwd(), options.data);
1469
+ const rootDir = path6.resolve(process.cwd(), options.root ?? ".");
1470
+ const dryRun = !(options.write ?? false);
1471
+ const force = options.force ?? false;
1472
+ const result = await scaffoldSchema({
1473
+ manifestPath,
1474
+ dataPath,
1475
+ rootDir,
1476
+ dryRun,
1477
+ force
1478
+ });
1479
+ console.error(formatScaffoldPreview(result));
1480
+ if (!result.wouldUpdate) {
1481
+ process.exit(0);
1482
+ return;
1483
+ }
1484
+ if (dryRun) {
1485
+ console.error("\nDry run complete. Use --write to apply changes.");
1486
+ process.exit(0);
1487
+ return;
1488
+ }
1489
+ if (!force) {
1490
+ console.error("\nScaffolding will update:");
1491
+ console.error(` - ${manifestPath}`);
1492
+ console.error(` - ${dataPath}`);
1493
+ console.error("\nUse --force to skip this confirmation.");
1494
+ }
1495
+ try {
1496
+ await applyScaffold(result, {
1497
+ manifestPath,
1498
+ dataPath,
1499
+ rootDir,
1500
+ dryRun,
1501
+ force
1502
+ });
1503
+ console.error(`
1504
+ Scaffold complete in ${Date.now() - start}ms`);
1505
+ process.exit(0);
1506
+ } catch (error) {
1507
+ const message = error instanceof Error ? error.message : "Unknown error";
1508
+ printCliError(
1509
+ "scaffold.apply_failed",
1510
+ `Failed to apply scaffold: ${message}`,
1511
+ "Check file permissions or disk space."
1512
+ );
1513
+ process.exit(1);
1514
+ }
1515
+ });
1228
1516
  function isManifest(value) {
1229
1517
  if (!value || typeof value !== "object") {
1230
1518
  return false;
@@ -1298,13 +1586,13 @@ function formatReportOutput(report, format, title) {
1298
1586
  if (format === "html") {
1299
1587
  return renderHtmlReport(report, { title });
1300
1588
  }
1301
- return stableStringify3(report);
1589
+ return stableStringify4(report);
1302
1590
  }
1303
1591
  function formatCollectOutput(data, format) {
1304
1592
  if (format === "json") {
1305
- return stableStringify3(data);
1593
+ return stableStringify4(data);
1306
1594
  }
1307
- return stableStringify3(data);
1595
+ return stableStringify4(data);
1308
1596
  }
1309
1597
  async function emitReport(options) {
1310
1598
  const { report, format, outputPath, title } = options;
@@ -1313,9 +1601,9 @@ async function emitReport(options) {
1313
1601
  console.log(content);
1314
1602
  return;
1315
1603
  }
1316
- const resolvedPath = path5.resolve(process.cwd(), outputPath);
1604
+ const resolvedPath = path6.resolve(process.cwd(), outputPath);
1317
1605
  try {
1318
- await mkdir(path5.dirname(resolvedPath), { recursive: true });
1606
+ await mkdir(path6.dirname(resolvedPath), { recursive: true });
1319
1607
  await writeFile(resolvedPath, content, "utf8");
1320
1608
  console.error(`Report written to ${resolvedPath}`);
1321
1609
  } catch (error) {
@@ -1335,7 +1623,7 @@ function emitAnnotations(report, mode, commandLabel) {
1335
1623
  }
1336
1624
  function printCliError(code, message, suggestion) {
1337
1625
  console.error(
1338
- stableStringify3({
1626
+ stableStringify4({
1339
1627
  ok: false,
1340
1628
  errors: [
1341
1629
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schemasentry/cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for Schema Sentry validation and reporting.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "commander": "^12.0.0",
36
- "@schemasentry/core": "0.4.0"
36
+ "@schemasentry/core": "0.5.0"
37
37
  },
38
38
  "scripts": {
39
39
  "build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.build.json",