@schemasentry/cli 0.1.0 → 0.2.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/dist/index.js +776 -97
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { readFile } from "fs/promises";
|
|
6
|
-
import
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
import path4 from "path";
|
|
7
8
|
import { stableStringify as stableStringify2 } from "@schemasentry/core";
|
|
8
9
|
|
|
9
10
|
// src/report.ts
|
|
@@ -11,47 +12,290 @@ import {
|
|
|
11
12
|
stableStringify,
|
|
12
13
|
validateSchema
|
|
13
14
|
} from "@schemasentry/core";
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
// src/coverage.ts
|
|
17
|
+
var buildCoverageResult = (input2, data) => {
|
|
18
|
+
const manifestRoutes = input2.expectedTypesByRoute ?? {};
|
|
16
19
|
const dataRoutes = data.routes ?? {};
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
...
|
|
20
|
+
const derivedRequiredRoutes = Object.entries(manifestRoutes).filter(([, types]) => (types ?? []).length > 0).map(([route]) => route);
|
|
21
|
+
const requiredRoutes = /* @__PURE__ */ new Set([
|
|
22
|
+
...input2.requiredRoutes ?? [],
|
|
23
|
+
...derivedRequiredRoutes
|
|
20
24
|
]);
|
|
21
|
-
const
|
|
25
|
+
const allRoutes = Array.from(
|
|
26
|
+
/* @__PURE__ */ new Set([
|
|
27
|
+
...Object.keys(manifestRoutes),
|
|
28
|
+
...requiredRoutes,
|
|
29
|
+
...Object.keys(dataRoutes)
|
|
30
|
+
])
|
|
31
|
+
).sort();
|
|
32
|
+
const issuesByRoute = {};
|
|
33
|
+
const summary = {
|
|
34
|
+
missingRoutes: 0,
|
|
35
|
+
missingTypes: 0,
|
|
36
|
+
unlistedRoutes: 0
|
|
37
|
+
};
|
|
38
|
+
for (const route of allRoutes) {
|
|
39
|
+
const issues = [];
|
|
22
40
|
const expectedTypes = manifestRoutes[route] ?? [];
|
|
23
41
|
const nodes = dataRoutes[route] ?? [];
|
|
24
42
|
const foundTypes = nodes.map((node) => node["@type"]).filter((type) => typeof type === "string");
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
if (requiredRoutes.has(route) && nodes.length === 0) {
|
|
44
|
+
issues.push({
|
|
45
|
+
path: `routes["${route}"]`,
|
|
46
|
+
message: "No schema blocks found for route",
|
|
47
|
+
severity: "error",
|
|
48
|
+
ruleId: "coverage.missing_route"
|
|
49
|
+
});
|
|
50
|
+
summary.missingRoutes += 1;
|
|
51
|
+
}
|
|
52
|
+
for (const expectedType of expectedTypes) {
|
|
53
|
+
if (!foundTypes.includes(expectedType)) {
|
|
29
54
|
issues.push({
|
|
30
|
-
path: `routes["${route}"]`,
|
|
31
|
-
message:
|
|
55
|
+
path: `routes["${route}"].types`,
|
|
56
|
+
message: `Missing expected schema type '${expectedType}'`,
|
|
32
57
|
severity: "error",
|
|
33
|
-
ruleId: "coverage.
|
|
58
|
+
ruleId: "coverage.missing_type"
|
|
34
59
|
});
|
|
35
|
-
|
|
36
|
-
for (const expectedType of expectedTypes) {
|
|
37
|
-
if (!foundTypes.includes(expectedType)) {
|
|
38
|
-
issues.push({
|
|
39
|
-
path: `routes["${route}"].types`,
|
|
40
|
-
message: `Missing expected schema type '${expectedType}'`,
|
|
41
|
-
severity: "error",
|
|
42
|
-
ruleId: "coverage.missing_type"
|
|
43
|
-
});
|
|
44
|
-
}
|
|
60
|
+
summary.missingTypes += 1;
|
|
45
61
|
}
|
|
46
62
|
}
|
|
47
|
-
if (!manifestRoutes[route] && nodes.length > 0) {
|
|
63
|
+
if (!manifestRoutes[route] && !requiredRoutes.has(route) && nodes.length > 0) {
|
|
48
64
|
issues.push({
|
|
49
65
|
path: `routes["${route}"]`,
|
|
50
66
|
message: "Route has schema but is missing from manifest",
|
|
51
67
|
severity: "warn",
|
|
52
68
|
ruleId: "coverage.unlisted_route"
|
|
53
69
|
});
|
|
70
|
+
summary.unlistedRoutes += 1;
|
|
71
|
+
}
|
|
72
|
+
if (issues.length > 0) {
|
|
73
|
+
issuesByRoute[route] = issues;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
allRoutes,
|
|
78
|
+
issuesByRoute,
|
|
79
|
+
summary
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/report.ts
|
|
84
|
+
var buildReport = (manifest, data, options = {}) => {
|
|
85
|
+
const manifestRoutes = manifest.routes ?? {};
|
|
86
|
+
const dataRoutes = data.routes ?? {};
|
|
87
|
+
const coverage = buildCoverageResult(
|
|
88
|
+
{ expectedTypesByRoute: manifestRoutes },
|
|
89
|
+
data
|
|
90
|
+
);
|
|
91
|
+
const routes = coverage.allRoutes.map((route) => {
|
|
92
|
+
const expectedTypes = manifestRoutes[route] ?? [];
|
|
93
|
+
const nodes = dataRoutes[route] ?? [];
|
|
94
|
+
const foundTypes = nodes.map((node) => node["@type"]).filter((type) => typeof type === "string");
|
|
95
|
+
const validation = validateSchema(nodes, options);
|
|
96
|
+
const issues = [
|
|
97
|
+
...validation.issues,
|
|
98
|
+
...coverage.issuesByRoute[route] ?? []
|
|
99
|
+
];
|
|
100
|
+
const errorCount = issues.filter((issue) => issue.severity === "error").length;
|
|
101
|
+
const warnCount = issues.filter((issue) => issue.severity === "warn").length;
|
|
102
|
+
const score = Math.max(0, validation.score - errorCount * 5 - warnCount * 2);
|
|
103
|
+
return {
|
|
104
|
+
route,
|
|
105
|
+
ok: errorCount === 0,
|
|
106
|
+
score,
|
|
107
|
+
issues,
|
|
108
|
+
expectedTypes,
|
|
109
|
+
foundTypes
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
const summaryErrors = routes.reduce(
|
|
113
|
+
(count, route) => count + route.issues.filter((i) => i.severity === "error").length,
|
|
114
|
+
0
|
|
115
|
+
);
|
|
116
|
+
const summaryWarnings = routes.reduce(
|
|
117
|
+
(count, route) => count + route.issues.filter((i) => i.severity === "warn").length,
|
|
118
|
+
0
|
|
119
|
+
);
|
|
120
|
+
const summaryScore = routes.length === 0 ? 0 : Math.round(
|
|
121
|
+
routes.reduce((total, route) => total + route.score, 0) / routes.length
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
ok: summaryErrors === 0,
|
|
125
|
+
summary: {
|
|
126
|
+
routes: routes.length,
|
|
127
|
+
errors: summaryErrors,
|
|
128
|
+
warnings: summaryWarnings,
|
|
129
|
+
score: summaryScore,
|
|
130
|
+
coverage: coverage.summary
|
|
131
|
+
},
|
|
132
|
+
routes
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// src/init.ts
|
|
137
|
+
import { promises as fs } from "fs";
|
|
138
|
+
import path from "path";
|
|
139
|
+
import { SCHEMA_CONTEXT } from "@schemasentry/core";
|
|
140
|
+
var DEFAULT_ANSWERS = {
|
|
141
|
+
siteName: "Acme Corp",
|
|
142
|
+
siteUrl: "https://acme.com",
|
|
143
|
+
authorName: "Jane Doe"
|
|
144
|
+
};
|
|
145
|
+
var getDefaultAnswers = () => ({ ...DEFAULT_ANSWERS });
|
|
146
|
+
var buildManifest = (scannedRoutes = []) => {
|
|
147
|
+
const routes = {
|
|
148
|
+
"/": ["Organization", "WebSite"],
|
|
149
|
+
"/blog/[slug]": ["Article"]
|
|
150
|
+
};
|
|
151
|
+
for (const route of scannedRoutes) {
|
|
152
|
+
if (!routes[route]) {
|
|
153
|
+
routes[route] = ["WebPage"];
|
|
54
154
|
}
|
|
155
|
+
}
|
|
156
|
+
return { routes };
|
|
157
|
+
};
|
|
158
|
+
var formatDate = (value) => value.toISOString().slice(0, 10);
|
|
159
|
+
var buildData = (answers, options = {}) => {
|
|
160
|
+
const { siteName, siteUrl, authorName } = answers;
|
|
161
|
+
const normalizedSiteUrl = normalizeUrl(siteUrl);
|
|
162
|
+
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
163
|
+
const date = formatDate(today);
|
|
164
|
+
const logoUrl = new URL("/logo.png", normalizedSiteUrl).toString();
|
|
165
|
+
const articleUrl = new URL("/blog/hello-world", normalizedSiteUrl).toString();
|
|
166
|
+
const imageUrl = new URL("/blog/hello-world.png", normalizedSiteUrl).toString();
|
|
167
|
+
const routes = {
|
|
168
|
+
"/": [
|
|
169
|
+
{
|
|
170
|
+
"@context": SCHEMA_CONTEXT,
|
|
171
|
+
"@type": "Organization",
|
|
172
|
+
name: siteName,
|
|
173
|
+
url: normalizedSiteUrl,
|
|
174
|
+
logo: logoUrl,
|
|
175
|
+
description: `Official website of ${siteName}`
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"@context": SCHEMA_CONTEXT,
|
|
179
|
+
"@type": "WebSite",
|
|
180
|
+
name: siteName,
|
|
181
|
+
url: normalizedSiteUrl,
|
|
182
|
+
description: `Learn more about ${siteName}`
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"/blog/[slug]": [
|
|
186
|
+
{
|
|
187
|
+
"@context": SCHEMA_CONTEXT,
|
|
188
|
+
"@type": "Article",
|
|
189
|
+
headline: "Hello World",
|
|
190
|
+
author: {
|
|
191
|
+
"@type": "Person",
|
|
192
|
+
name: authorName
|
|
193
|
+
},
|
|
194
|
+
datePublished: date,
|
|
195
|
+
dateModified: date,
|
|
196
|
+
description: `An introduction to ${siteName}`,
|
|
197
|
+
image: imageUrl,
|
|
198
|
+
url: articleUrl
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
};
|
|
202
|
+
for (const route of options.scannedRoutes ?? []) {
|
|
203
|
+
if (routes[route]) {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
routes[route] = [
|
|
207
|
+
{
|
|
208
|
+
"@context": SCHEMA_CONTEXT,
|
|
209
|
+
"@type": "WebPage",
|
|
210
|
+
name: routeToName(route),
|
|
211
|
+
url: new URL(route, normalizedSiteUrl).toString()
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
}
|
|
215
|
+
return { routes };
|
|
216
|
+
};
|
|
217
|
+
var writeInitFiles = async (options) => {
|
|
218
|
+
const {
|
|
219
|
+
manifestPath,
|
|
220
|
+
dataPath,
|
|
221
|
+
overwriteManifest,
|
|
222
|
+
overwriteData,
|
|
223
|
+
answers,
|
|
224
|
+
today,
|
|
225
|
+
scannedRoutes
|
|
226
|
+
} = options;
|
|
227
|
+
const manifest = buildManifest(scannedRoutes ?? []);
|
|
228
|
+
const data = buildData(answers, { today, scannedRoutes });
|
|
229
|
+
const manifestResult = await writeJsonFile(
|
|
230
|
+
manifestPath,
|
|
231
|
+
manifest,
|
|
232
|
+
overwriteManifest
|
|
233
|
+
);
|
|
234
|
+
const dataResult = await writeJsonFile(dataPath, data, overwriteData);
|
|
235
|
+
return {
|
|
236
|
+
manifest: manifestResult,
|
|
237
|
+
data: dataResult
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
var writeJsonFile = async (filePath, payload, overwrite) => {
|
|
241
|
+
const exists = await fileExists(filePath);
|
|
242
|
+
if (exists && !overwrite) {
|
|
243
|
+
return "skipped";
|
|
244
|
+
}
|
|
245
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
246
|
+
const json = JSON.stringify(payload, null, 2);
|
|
247
|
+
await fs.writeFile(filePath, `${json}
|
|
248
|
+
`, "utf8");
|
|
249
|
+
return exists ? "overwritten" : "created";
|
|
250
|
+
};
|
|
251
|
+
var fileExists = async (filePath) => {
|
|
252
|
+
try {
|
|
253
|
+
await fs.access(filePath);
|
|
254
|
+
return true;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var normalizeUrl = (value) => {
|
|
260
|
+
try {
|
|
261
|
+
return new URL(value).toString();
|
|
262
|
+
} catch {
|
|
263
|
+
return new URL(`https://${value}`).toString();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
var routeToName = (route) => {
|
|
267
|
+
if (route === "/") {
|
|
268
|
+
return "Home";
|
|
269
|
+
}
|
|
270
|
+
const cleaned = route.replace(/\[|\]/g, "").split("/").filter(Boolean).join(" ");
|
|
271
|
+
return cleaned.split(/\s+/).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
// src/audit.ts
|
|
275
|
+
import {
|
|
276
|
+
validateSchema as validateSchema2
|
|
277
|
+
} from "@schemasentry/core";
|
|
278
|
+
var buildAuditReport = (data, options = {}) => {
|
|
279
|
+
const dataRoutes = data.routes ?? {};
|
|
280
|
+
const manifestRoutes = options.manifest?.routes ?? {};
|
|
281
|
+
const coverageEnabled = Boolean(options.manifest || options.requiredRoutes?.length);
|
|
282
|
+
const coverage = coverageEnabled ? buildCoverageResult(
|
|
283
|
+
{
|
|
284
|
+
expectedTypesByRoute: manifestRoutes,
|
|
285
|
+
requiredRoutes: options.requiredRoutes
|
|
286
|
+
},
|
|
287
|
+
data
|
|
288
|
+
) : null;
|
|
289
|
+
const allRoutes = coverageEnabled ? coverage?.allRoutes ?? [] : Object.keys(dataRoutes).sort();
|
|
290
|
+
const routes = allRoutes.map((route) => {
|
|
291
|
+
const expectedTypes = coverageEnabled ? manifestRoutes[route] ?? [] : [];
|
|
292
|
+
const nodes = dataRoutes[route] ?? [];
|
|
293
|
+
const foundTypes = nodes.map((node) => node["@type"]).filter((type) => typeof type === "string");
|
|
294
|
+
const validation = validateSchema2(nodes, options);
|
|
295
|
+
const issues = [
|
|
296
|
+
...validation.issues,
|
|
297
|
+
...coverage?.issuesByRoute[route] ?? []
|
|
298
|
+
];
|
|
55
299
|
const errorCount = issues.filter((issue) => issue.severity === "error").length;
|
|
56
300
|
const warnCount = issues.filter((issue) => issue.severity === "warn").length;
|
|
57
301
|
const score = Math.max(0, validation.score - errorCount * 5 - warnCount * 2);
|
|
@@ -81,15 +325,223 @@ var buildReport = (manifest, data) => {
|
|
|
81
325
|
routes: routes.length,
|
|
82
326
|
errors: summaryErrors,
|
|
83
327
|
warnings: summaryWarnings,
|
|
84
|
-
score: summaryScore
|
|
328
|
+
score: summaryScore,
|
|
329
|
+
...coverageEnabled ? { coverage: coverage?.summary } : {}
|
|
85
330
|
},
|
|
86
331
|
routes
|
|
87
332
|
};
|
|
88
333
|
};
|
|
89
334
|
|
|
335
|
+
// src/summary.ts
|
|
336
|
+
var formatDuration = (durationMs) => {
|
|
337
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) {
|
|
338
|
+
return "0ms";
|
|
339
|
+
}
|
|
340
|
+
if (durationMs < 1e3) {
|
|
341
|
+
return `${Math.round(durationMs)}ms`;
|
|
342
|
+
}
|
|
343
|
+
const seconds = durationMs / 1e3;
|
|
344
|
+
return `${seconds.toFixed(seconds < 10 ? 1 : 0)}s`;
|
|
345
|
+
};
|
|
346
|
+
var formatSummaryLine = (label, stats) => {
|
|
347
|
+
const parts = [
|
|
348
|
+
`Routes: ${stats.routes}`,
|
|
349
|
+
`Errors: ${stats.errors}`,
|
|
350
|
+
`Warnings: ${stats.warnings}`,
|
|
351
|
+
`Score: ${stats.score}`,
|
|
352
|
+
`Duration: ${formatDuration(stats.durationMs)}`
|
|
353
|
+
];
|
|
354
|
+
if (stats.coverage) {
|
|
355
|
+
parts.push(
|
|
356
|
+
`Coverage: missing_routes=${stats.coverage.missingRoutes} missing_types=${stats.coverage.missingTypes} unlisted_routes=${stats.coverage.unlistedRoutes}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
return `${label} | ${parts.join(" | ")}`;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/config.ts
|
|
363
|
+
import { promises as fs2 } from "fs";
|
|
364
|
+
import path2 from "path";
|
|
365
|
+
var ConfigError = class extends Error {
|
|
366
|
+
constructor(code, message, suggestion) {
|
|
367
|
+
super(message);
|
|
368
|
+
this.code = code;
|
|
369
|
+
this.suggestion = suggestion;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var DEFAULT_CONFIG_PATH = "schema-sentry.config.json";
|
|
373
|
+
var loadConfig = async (options) => {
|
|
374
|
+
const cwd = options.cwd ?? process.cwd();
|
|
375
|
+
const explicit = Boolean(options.configPath);
|
|
376
|
+
const resolvedPath = path2.resolve(cwd, options.configPath ?? DEFAULT_CONFIG_PATH);
|
|
377
|
+
const exists = await fileExists2(resolvedPath);
|
|
378
|
+
if (!exists) {
|
|
379
|
+
if (explicit) {
|
|
380
|
+
throw new ConfigError(
|
|
381
|
+
"config.not_found",
|
|
382
|
+
`Config not found at ${resolvedPath}`,
|
|
383
|
+
"Provide a valid path or remove --config."
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
388
|
+
let raw;
|
|
389
|
+
try {
|
|
390
|
+
raw = await fs2.readFile(resolvedPath, "utf8");
|
|
391
|
+
} catch (error) {
|
|
392
|
+
throw new ConfigError(
|
|
393
|
+
"config.read_failed",
|
|
394
|
+
`Failed to read config at ${resolvedPath}`,
|
|
395
|
+
"Check file permissions or re-create the config file."
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
let parsed;
|
|
399
|
+
try {
|
|
400
|
+
parsed = JSON.parse(raw);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw new ConfigError(
|
|
403
|
+
"config.invalid_json",
|
|
404
|
+
"Config is not valid JSON",
|
|
405
|
+
"Check the JSON syntax or regenerate the file."
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
if (!isConfig(parsed)) {
|
|
409
|
+
throw new ConfigError(
|
|
410
|
+
"config.invalid_shape",
|
|
411
|
+
"Config must be a JSON object with optional boolean 'recommended'",
|
|
412
|
+
'Example: { "recommended": false }'
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
return parsed;
|
|
416
|
+
};
|
|
417
|
+
var resolveRecommended = (cliOverride, config) => cliOverride ?? config?.recommended ?? true;
|
|
418
|
+
var isConfig = (value) => {
|
|
419
|
+
if (!value || typeof value !== "object") {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const config = value;
|
|
423
|
+
if (config.recommended !== void 0 && typeof config.recommended !== "boolean") {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
};
|
|
428
|
+
var fileExists2 = async (filePath) => {
|
|
429
|
+
try {
|
|
430
|
+
await fs2.access(filePath);
|
|
431
|
+
return true;
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/routes.ts
|
|
438
|
+
import { promises as fs3 } from "fs";
|
|
439
|
+
import path3 from "path";
|
|
440
|
+
var scanRoutes = async (options) => {
|
|
441
|
+
const rootDir = options.rootDir;
|
|
442
|
+
const routes = /* @__PURE__ */ new Set();
|
|
443
|
+
if (options.includeApp !== false) {
|
|
444
|
+
const appDir = path3.join(rootDir, "app");
|
|
445
|
+
if (await dirExists(appDir)) {
|
|
446
|
+
const files = await walkDir(appDir);
|
|
447
|
+
for (const file of files) {
|
|
448
|
+
if (!isAppPageFile(file)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const route = toAppRoute(appDir, file);
|
|
452
|
+
if (route) {
|
|
453
|
+
routes.add(route);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (options.includePages !== false) {
|
|
459
|
+
const pagesDir = path3.join(rootDir, "pages");
|
|
460
|
+
if (await dirExists(pagesDir)) {
|
|
461
|
+
const files = await walkDir(pagesDir);
|
|
462
|
+
for (const file of files) {
|
|
463
|
+
if (!isPagesFile(file)) {
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const route = toPagesRoute(pagesDir, file);
|
|
467
|
+
if (route) {
|
|
468
|
+
routes.add(route);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return Array.from(routes).sort();
|
|
474
|
+
};
|
|
475
|
+
var isAppPageFile = (filePath) => /\/page\.(t|j)sx?$/.test(filePath) || /\/page\.mdx$/.test(filePath);
|
|
476
|
+
var isPagesFile = (filePath) => /\.(t|j)sx?$/.test(filePath) || /\.mdx$/.test(filePath);
|
|
477
|
+
var toAppRoute = (appDir, filePath) => {
|
|
478
|
+
const relative = path3.relative(appDir, filePath);
|
|
479
|
+
const segments = relative.split(path3.sep);
|
|
480
|
+
if (segments.length === 0) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
segments.pop();
|
|
484
|
+
const routeSegments = segments.filter((segment) => segment.length > 0).filter((segment) => !isGroupSegment(segment)).filter((segment) => !isParallelSegment(segment));
|
|
485
|
+
if (routeSegments.length === 0) {
|
|
486
|
+
return "/";
|
|
487
|
+
}
|
|
488
|
+
return `/${routeSegments.join("/")}`;
|
|
489
|
+
};
|
|
490
|
+
var toPagesRoute = (pagesDir, filePath) => {
|
|
491
|
+
const relative = path3.relative(pagesDir, filePath);
|
|
492
|
+
const segments = relative.split(path3.sep);
|
|
493
|
+
if (segments.length === 0) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
if (segments[0] === "api") {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
const fileName = segments.pop();
|
|
500
|
+
if (!fileName) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const baseName = fileName.replace(/\.[^/.]+$/, "");
|
|
504
|
+
if (baseName.startsWith("_")) {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
const filtered = segments.filter((segment) => segment.length > 0);
|
|
508
|
+
if (baseName !== "index") {
|
|
509
|
+
filtered.push(baseName);
|
|
510
|
+
}
|
|
511
|
+
if (filtered.length === 0) {
|
|
512
|
+
return "/";
|
|
513
|
+
}
|
|
514
|
+
return `/${filtered.join("/")}`;
|
|
515
|
+
};
|
|
516
|
+
var isGroupSegment = (segment) => segment.startsWith("(") && segment.endsWith(")");
|
|
517
|
+
var isParallelSegment = (segment) => segment.startsWith("@");
|
|
518
|
+
var dirExists = async (dirPath) => {
|
|
519
|
+
try {
|
|
520
|
+
const stat = await fs3.stat(dirPath);
|
|
521
|
+
return stat.isDirectory();
|
|
522
|
+
} catch {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
var walkDir = async (dirPath) => {
|
|
527
|
+
const entries = await fs3.readdir(dirPath, { withFileTypes: true });
|
|
528
|
+
const files = [];
|
|
529
|
+
for (const entry of entries) {
|
|
530
|
+
const resolved = path3.join(dirPath, entry.name);
|
|
531
|
+
if (entry.isDirectory()) {
|
|
532
|
+
files.push(...await walkDir(resolved));
|
|
533
|
+
} else if (entry.isFile()) {
|
|
534
|
+
files.push(resolved);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return files;
|
|
538
|
+
};
|
|
539
|
+
|
|
90
540
|
// src/index.ts
|
|
541
|
+
import { createInterface } from "readline/promises";
|
|
542
|
+
import { stdin as input, stdout as output } from "process";
|
|
91
543
|
var program = new Command();
|
|
92
|
-
program.name("schemasentry").description("Schema Sentry CLI").version(
|
|
544
|
+
program.name("schemasentry").description("Schema Sentry CLI").version(resolveCliVersion());
|
|
93
545
|
program.command("validate").description("Validate schema coverage and rules").option(
|
|
94
546
|
"-m, --manifest <path>",
|
|
95
547
|
"Path to manifest JSON",
|
|
@@ -98,23 +550,19 @@ program.command("validate").description("Validate schema coverage and rules").op
|
|
|
98
550
|
"-d, --data <path>",
|
|
99
551
|
"Path to schema data JSON",
|
|
100
552
|
"schema-sentry.data.json"
|
|
101
|
-
).action(async (options) => {
|
|
102
|
-
const
|
|
103
|
-
const
|
|
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) => {
|
|
554
|
+
const start = Date.now();
|
|
555
|
+
const recommended = await resolveRecommendedOption(options.config);
|
|
556
|
+
const manifestPath = path4.resolve(process.cwd(), options.manifest);
|
|
557
|
+
const dataPath = path4.resolve(process.cwd(), options.data);
|
|
104
558
|
let raw;
|
|
105
559
|
try {
|
|
106
560
|
raw = await readFile(manifestPath, "utf8");
|
|
107
561
|
} catch (error) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
{
|
|
113
|
-
code: "manifest.not_found",
|
|
114
|
-
message: `Manifest not found at ${manifestPath}`
|
|
115
|
-
}
|
|
116
|
-
]
|
|
117
|
-
})
|
|
562
|
+
printCliError(
|
|
563
|
+
"manifest.not_found",
|
|
564
|
+
`Manifest not found at ${manifestPath}`,
|
|
565
|
+
"Run `schemasentry init` to generate starter files."
|
|
118
566
|
);
|
|
119
567
|
process.exit(1);
|
|
120
568
|
return;
|
|
@@ -123,31 +571,19 @@ program.command("validate").description("Validate schema coverage and rules").op
|
|
|
123
571
|
try {
|
|
124
572
|
manifest = JSON.parse(raw);
|
|
125
573
|
} catch (error) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
code: "manifest.invalid_json",
|
|
132
|
-
message: "Manifest is not valid JSON"
|
|
133
|
-
}
|
|
134
|
-
]
|
|
135
|
-
})
|
|
574
|
+
printCliError(
|
|
575
|
+
"manifest.invalid_json",
|
|
576
|
+
"Manifest is not valid JSON",
|
|
577
|
+
"Check the JSON syntax or regenerate with `schemasentry init`."
|
|
136
578
|
);
|
|
137
579
|
process.exit(1);
|
|
138
580
|
return;
|
|
139
581
|
}
|
|
140
582
|
if (!isManifest(manifest)) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
code: "manifest.invalid_shape",
|
|
147
|
-
message: "Manifest must contain a 'routes' object with string array values"
|
|
148
|
-
}
|
|
149
|
-
]
|
|
150
|
-
})
|
|
583
|
+
printCliError(
|
|
584
|
+
"manifest.invalid_shape",
|
|
585
|
+
"Manifest must contain a 'routes' object with string array values",
|
|
586
|
+
"Ensure each route maps to an array of schema type names."
|
|
151
587
|
);
|
|
152
588
|
process.exit(1);
|
|
153
589
|
return;
|
|
@@ -156,16 +592,10 @@ program.command("validate").description("Validate schema coverage and rules").op
|
|
|
156
592
|
try {
|
|
157
593
|
dataRaw = await readFile(dataPath, "utf8");
|
|
158
594
|
} catch (error) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
code: "data.not_found",
|
|
165
|
-
message: `Schema data not found at ${dataPath}`
|
|
166
|
-
}
|
|
167
|
-
]
|
|
168
|
-
})
|
|
595
|
+
printCliError(
|
|
596
|
+
"data.not_found",
|
|
597
|
+
`Schema data not found at ${dataPath}`,
|
|
598
|
+
"Run `schemasentry init` to generate starter files."
|
|
169
599
|
);
|
|
170
600
|
process.exit(1);
|
|
171
601
|
return;
|
|
@@ -174,41 +604,153 @@ program.command("validate").description("Validate schema coverage and rules").op
|
|
|
174
604
|
try {
|
|
175
605
|
data = JSON.parse(dataRaw);
|
|
176
606
|
} catch (error) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
{
|
|
182
|
-
code: "data.invalid_json",
|
|
183
|
-
message: "Schema data is not valid JSON"
|
|
184
|
-
}
|
|
185
|
-
]
|
|
186
|
-
})
|
|
607
|
+
printCliError(
|
|
608
|
+
"data.invalid_json",
|
|
609
|
+
"Schema data is not valid JSON",
|
|
610
|
+
"Check the JSON syntax or regenerate with `schemasentry init`."
|
|
187
611
|
);
|
|
188
612
|
process.exit(1);
|
|
189
613
|
return;
|
|
190
614
|
}
|
|
191
615
|
if (!isSchemaData(data)) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
code: "data.invalid_shape",
|
|
198
|
-
message: "Schema data must contain a 'routes' object with array values"
|
|
199
|
-
}
|
|
200
|
-
]
|
|
201
|
-
})
|
|
616
|
+
printCliError(
|
|
617
|
+
"data.invalid_shape",
|
|
618
|
+
"Schema data must contain a 'routes' object with array values",
|
|
619
|
+
"Ensure each route maps to an array of JSON-LD blocks."
|
|
202
620
|
);
|
|
203
621
|
process.exit(1);
|
|
204
622
|
return;
|
|
205
623
|
}
|
|
206
|
-
const report = buildReport(manifest, data);
|
|
624
|
+
const report = buildReport(manifest, data, { recommended });
|
|
207
625
|
console.log(formatReportOutput(report));
|
|
626
|
+
printValidateSummary(report, Date.now() - start);
|
|
208
627
|
process.exit(report.ok ? 0 : 1);
|
|
209
628
|
});
|
|
210
|
-
program.
|
|
211
|
-
|
|
629
|
+
program.command("init").description("Interactive setup wizard").option(
|
|
630
|
+
"-m, --manifest <path>",
|
|
631
|
+
"Path to manifest JSON",
|
|
632
|
+
"schema-sentry.manifest.json"
|
|
633
|
+
).option(
|
|
634
|
+
"-d, --data <path>",
|
|
635
|
+
"Path to schema data JSON",
|
|
636
|
+
"schema-sentry.data.json"
|
|
637
|
+
).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) => {
|
|
638
|
+
const manifestPath = path4.resolve(process.cwd(), options.manifest);
|
|
639
|
+
const dataPath = path4.resolve(process.cwd(), options.data);
|
|
640
|
+
const force = options.force ?? false;
|
|
641
|
+
const useDefaults = options.yes ?? false;
|
|
642
|
+
const answers = useDefaults ? getDefaultAnswers() : await promptAnswers();
|
|
643
|
+
const scannedRoutes = options.scan ? await scanRoutes({ rootDir: path4.resolve(process.cwd(), options.root ?? ".") }) : [];
|
|
644
|
+
if (options.scan && scannedRoutes.length === 0) {
|
|
645
|
+
console.error("No routes found during scan.");
|
|
646
|
+
}
|
|
647
|
+
const [overwriteManifest, overwriteData] = await resolveOverwrites({
|
|
648
|
+
manifestPath,
|
|
649
|
+
dataPath,
|
|
650
|
+
force,
|
|
651
|
+
interactive: !useDefaults
|
|
652
|
+
});
|
|
653
|
+
const result = await writeInitFiles({
|
|
654
|
+
manifestPath,
|
|
655
|
+
dataPath,
|
|
656
|
+
overwriteManifest,
|
|
657
|
+
overwriteData,
|
|
658
|
+
answers,
|
|
659
|
+
scannedRoutes
|
|
660
|
+
});
|
|
661
|
+
printInitSummary({ manifestPath, dataPath, result });
|
|
662
|
+
});
|
|
663
|
+
program.command("audit").description("Analyze schema health and report issues").option(
|
|
664
|
+
"-d, --data <path>",
|
|
665
|
+
"Path to schema data JSON",
|
|
666
|
+
"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) => {
|
|
668
|
+
const start = Date.now();
|
|
669
|
+
const recommended = await resolveRecommendedOption(options.config);
|
|
670
|
+
const dataPath = path4.resolve(process.cwd(), options.data);
|
|
671
|
+
let dataRaw;
|
|
672
|
+
try {
|
|
673
|
+
dataRaw = await readFile(dataPath, "utf8");
|
|
674
|
+
} catch (error) {
|
|
675
|
+
printCliError(
|
|
676
|
+
"data.not_found",
|
|
677
|
+
`Schema data not found at ${dataPath}`,
|
|
678
|
+
"Run `schemasentry init` to generate starter files."
|
|
679
|
+
);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
let data;
|
|
684
|
+
try {
|
|
685
|
+
data = JSON.parse(dataRaw);
|
|
686
|
+
} catch (error) {
|
|
687
|
+
printCliError(
|
|
688
|
+
"data.invalid_json",
|
|
689
|
+
"Schema data is not valid JSON",
|
|
690
|
+
"Check the JSON syntax or regenerate with `schemasentry init`."
|
|
691
|
+
);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (!isSchemaData(data)) {
|
|
696
|
+
printCliError(
|
|
697
|
+
"data.invalid_shape",
|
|
698
|
+
"Schema data must contain a 'routes' object with array values",
|
|
699
|
+
"Ensure each route maps to an array of JSON-LD blocks."
|
|
700
|
+
);
|
|
701
|
+
process.exit(1);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
let manifest;
|
|
705
|
+
if (options.manifest) {
|
|
706
|
+
const manifestPath = path4.resolve(process.cwd(), options.manifest);
|
|
707
|
+
let manifestRaw;
|
|
708
|
+
try {
|
|
709
|
+
manifestRaw = await readFile(manifestPath, "utf8");
|
|
710
|
+
} catch (error) {
|
|
711
|
+
printCliError(
|
|
712
|
+
"manifest.not_found",
|
|
713
|
+
`Manifest not found at ${manifestPath}`,
|
|
714
|
+
"Run `schemasentry init` to generate starter files."
|
|
715
|
+
);
|
|
716
|
+
process.exit(1);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
manifest = JSON.parse(manifestRaw);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
printCliError(
|
|
723
|
+
"manifest.invalid_json",
|
|
724
|
+
"Manifest is not valid JSON",
|
|
725
|
+
"Check the JSON syntax or regenerate with `schemasentry init`."
|
|
726
|
+
);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (!isManifest(manifest)) {
|
|
731
|
+
printCliError(
|
|
732
|
+
"manifest.invalid_shape",
|
|
733
|
+
"Manifest must contain a 'routes' object with string array values",
|
|
734
|
+
"Ensure each route maps to an array of schema type names."
|
|
735
|
+
);
|
|
736
|
+
process.exit(1);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const requiredRoutes = options.scan ? await scanRoutes({ rootDir: path4.resolve(process.cwd(), options.root ?? ".") }) : [];
|
|
741
|
+
if (options.scan && requiredRoutes.length === 0) {
|
|
742
|
+
console.error("No routes found during scan.");
|
|
743
|
+
}
|
|
744
|
+
const report = buildAuditReport(data, {
|
|
745
|
+
recommended,
|
|
746
|
+
manifest,
|
|
747
|
+
requiredRoutes: requiredRoutes.length > 0 ? requiredRoutes : void 0
|
|
748
|
+
});
|
|
749
|
+
console.log(formatReportOutput(report));
|
|
750
|
+
printAuditSummary(report, Boolean(manifest), Date.now() - start);
|
|
751
|
+
process.exit(report.ok ? 0 : 1);
|
|
752
|
+
});
|
|
753
|
+
function isManifest(value) {
|
|
212
754
|
if (!value || typeof value !== "object") {
|
|
213
755
|
return false;
|
|
214
756
|
}
|
|
@@ -222,8 +764,8 @@ var isManifest = (value) => {
|
|
|
222
764
|
}
|
|
223
765
|
}
|
|
224
766
|
return true;
|
|
225
|
-
}
|
|
226
|
-
|
|
767
|
+
}
|
|
768
|
+
function isSchemaData(value) {
|
|
227
769
|
if (!value || typeof value !== "object") {
|
|
228
770
|
return false;
|
|
229
771
|
}
|
|
@@ -237,5 +779,142 @@ var isSchemaData = (value) => {
|
|
|
237
779
|
}
|
|
238
780
|
}
|
|
239
781
|
return true;
|
|
240
|
-
}
|
|
241
|
-
|
|
782
|
+
}
|
|
783
|
+
function formatReportOutput(report) {
|
|
784
|
+
return stableStringify2(report);
|
|
785
|
+
}
|
|
786
|
+
function printCliError(code, message, suggestion) {
|
|
787
|
+
console.error(
|
|
788
|
+
stableStringify2({
|
|
789
|
+
ok: false,
|
|
790
|
+
errors: [
|
|
791
|
+
{
|
|
792
|
+
code,
|
|
793
|
+
message,
|
|
794
|
+
...suggestion !== void 0 ? { suggestion } : {}
|
|
795
|
+
}
|
|
796
|
+
]
|
|
797
|
+
})
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
async function resolveRecommendedOption(configPath) {
|
|
801
|
+
const override = getRecommendedOverride(process.argv);
|
|
802
|
+
try {
|
|
803
|
+
const config = await loadConfig({ configPath });
|
|
804
|
+
return resolveRecommended(override, config);
|
|
805
|
+
} catch (error) {
|
|
806
|
+
if (error instanceof ConfigError) {
|
|
807
|
+
printCliError(error.code, error.message, error.suggestion);
|
|
808
|
+
process.exit(1);
|
|
809
|
+
return true;
|
|
810
|
+
}
|
|
811
|
+
throw error;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
function getRecommendedOverride(argv) {
|
|
815
|
+
if (argv.includes("--recommended")) {
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
if (argv.includes("--no-recommended")) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
return void 0;
|
|
822
|
+
}
|
|
823
|
+
function resolveCliVersion() {
|
|
824
|
+
try {
|
|
825
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
826
|
+
const parsed = JSON.parse(raw);
|
|
827
|
+
return parsed.version ?? "0.0.0";
|
|
828
|
+
} catch {
|
|
829
|
+
return "0.0.0";
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
program.parse();
|
|
833
|
+
function printValidateSummary(report, durationMs) {
|
|
834
|
+
console.error(
|
|
835
|
+
formatSummaryLine("validate", {
|
|
836
|
+
...report.summary,
|
|
837
|
+
durationMs,
|
|
838
|
+
coverage: report.summary.coverage
|
|
839
|
+
})
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
function printAuditSummary(report, coverageEnabled, durationMs) {
|
|
843
|
+
console.error(
|
|
844
|
+
formatSummaryLine("audit", {
|
|
845
|
+
...report.summary,
|
|
846
|
+
durationMs,
|
|
847
|
+
coverage: report.summary.coverage
|
|
848
|
+
})
|
|
849
|
+
);
|
|
850
|
+
if (!coverageEnabled) {
|
|
851
|
+
console.error("Coverage checks skipped (no manifest provided).");
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
async function promptAnswers() {
|
|
855
|
+
const defaults = getDefaultAnswers();
|
|
856
|
+
const rl = createInterface({ input, output });
|
|
857
|
+
try {
|
|
858
|
+
const siteName = await ask(rl, "Site name", defaults.siteName);
|
|
859
|
+
const siteUrl = await ask(rl, "Base URL", defaults.siteUrl);
|
|
860
|
+
const authorName = await ask(rl, "Primary author name", defaults.authorName);
|
|
861
|
+
return { siteName, siteUrl, authorName };
|
|
862
|
+
} finally {
|
|
863
|
+
rl.close();
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
async function ask(rl, question, fallback) {
|
|
867
|
+
const answer = (await rl.question(`${question} (${fallback}): `)).trim();
|
|
868
|
+
return answer.length > 0 ? answer : fallback;
|
|
869
|
+
}
|
|
870
|
+
async function resolveOverwrites(options) {
|
|
871
|
+
const { manifestPath, dataPath, force, interactive } = options;
|
|
872
|
+
if (force) {
|
|
873
|
+
return [true, true];
|
|
874
|
+
}
|
|
875
|
+
const manifestExists = await fileExists(manifestPath);
|
|
876
|
+
const dataExists = await fileExists(dataPath);
|
|
877
|
+
if (!interactive) {
|
|
878
|
+
return [false, false];
|
|
879
|
+
}
|
|
880
|
+
const rl = createInterface({ input, output });
|
|
881
|
+
try {
|
|
882
|
+
const overwriteManifest = manifestExists ? await confirm(rl, `Manifest exists at ${manifestPath}. Overwrite?`, false) : false;
|
|
883
|
+
const overwriteData = dataExists ? await confirm(rl, `Data file exists at ${dataPath}. Overwrite?`, false) : false;
|
|
884
|
+
return [overwriteManifest, overwriteData];
|
|
885
|
+
} finally {
|
|
886
|
+
rl.close();
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function confirm(rl, question, defaultValue) {
|
|
890
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
891
|
+
const answer = (await rl.question(`${question} (${hint}): `)).trim().toLowerCase();
|
|
892
|
+
if (!answer) {
|
|
893
|
+
return defaultValue;
|
|
894
|
+
}
|
|
895
|
+
return answer === "y" || answer === "yes";
|
|
896
|
+
}
|
|
897
|
+
function printInitSummary(options) {
|
|
898
|
+
const { manifestPath, dataPath, result } = options;
|
|
899
|
+
const created = [];
|
|
900
|
+
if (result.manifest !== "skipped") {
|
|
901
|
+
created.push(`${manifestPath} (${result.manifest})`);
|
|
902
|
+
}
|
|
903
|
+
if (result.data !== "skipped") {
|
|
904
|
+
created.push(`${dataPath} (${result.data})`);
|
|
905
|
+
}
|
|
906
|
+
if (created.length > 0) {
|
|
907
|
+
console.log("Schema Sentry init complete.");
|
|
908
|
+
console.log(`Created ${created.length} file(s):`);
|
|
909
|
+
created.forEach((entry) => console.log(`- ${entry}`));
|
|
910
|
+
}
|
|
911
|
+
if (result.manifest === "skipped" || result.data === "skipped") {
|
|
912
|
+
console.log("Some files were skipped. Use --force to overwrite.");
|
|
913
|
+
}
|
|
914
|
+
if (result.manifest === "skipped" && result.data === "skipped") {
|
|
915
|
+
console.log("No files were written.");
|
|
916
|
+
}
|
|
917
|
+
if (created.length > 0) {
|
|
918
|
+
console.log("Next: run `schemasentry validate` to verify your setup.");
|
|
919
|
+
}
|
|
920
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schemasentry/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.
|
|
36
|
+
"@schemasentry/core": "0.2.0"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "tsup src/index.ts --format esm --dts --clean --tsconfig tsconfig.build.json",
|