contract-drift-detection 0.1.4 → 0.1.5
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/CHANGELOG.md +18 -0
- package/dist/cli.js +218 -43
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +157 -24
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.1.5 - 2026-03-16
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- New drift reporting outputs via `--reporter pretty|json|junit`.
|
|
10
|
+
- Optional report destination via `--report-file <path>`.
|
|
11
|
+
- Strict drift mode via `--strict` to enforce `additionalProperties: false` behavior during validation.
|
|
12
|
+
- Path-based drift ignore controls via repeated `--drift-ignore <path>` and `.driftignore` support.
|
|
13
|
+
- New diagnostics endpoint: `GET /__drift` for in-memory drift issue visibility.
|
|
14
|
+
- New unit test coverage for drift analyzer strict/ignore behavior.
|
|
15
|
+
- New CLI e2e coverage for discover failure UX.
|
|
16
|
+
|
|
17
|
+
### Improved
|
|
18
|
+
|
|
19
|
+
- CI defaults now support fail-fast drift behavior with `--fail-on-drift`.
|
|
20
|
+
- Drift validation pipeline now supports machine-readable reporting for CI gates.
|
|
21
|
+
- Server-level drift callback flow for automated fail/report handling.
|
|
22
|
+
|
|
5
23
|
## 0.1.4 - 2026-03-16
|
|
6
24
|
|
|
7
25
|
### Improved
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import
|
|
4
|
+
import process2 from "process";
|
|
5
5
|
|
|
6
6
|
// src/config.ts
|
|
7
|
-
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
7
|
+
import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
|
|
8
8
|
import path2 from "path";
|
|
9
9
|
import { Command } from "commander";
|
|
10
10
|
|
|
@@ -238,11 +238,26 @@ function resolvePathFromCwd(cwd, value) {
|
|
|
238
238
|
return path2.isAbsolute(value) ? value : path2.join(cwd, value);
|
|
239
239
|
}
|
|
240
240
|
function applyServeOptions(command) {
|
|
241
|
-
return command.option("--spec <path>", "Path to an OpenAPI 3.x file").option("--spec-url <url>", "Remote URL to an OpenAPI 3.x file").option("--discover <backend-url>", "Discover OpenAPI from a backend URL and cache it locally").option("--port <port>", "Port to bind the server", "4010").option("--host <host>", "Host to bind the server", "0.0.0.0").option("--db <path>", "JSON database path", ".mock-db.json").option("--cors-origin <origin>", "CORS origin (default is *)", "*").option("--drift-check <url>", "Forward traffic to a real backend and validate responses").option("--fallback-to-mock", "Fallback to mock responses when proxying fails", false).option("--verbose", "Enable verbose logging", false);
|
|
241
|
+
return command.option("--spec <path>", "Path to an OpenAPI 3.x file").option("--spec-url <url>", "Remote URL to an OpenAPI 3.x file").option("--discover <backend-url>", "Discover OpenAPI from a backend URL and cache it locally").option("--port <port>", "Port to bind the server", "4010").option("--host <host>", "Host to bind the server", "0.0.0.0").option("--db <path>", "JSON database path", ".mock-db.json").option("--cors-origin <origin>", "CORS origin (default is *)", "*").option("--drift-check <url>", "Forward traffic to a real backend and validate responses").option("--strict", "Treat additional fields as drift", false).option("--drift-ignore <paths>", "Comma-separated JSON pointer paths to ignore during drift checks").option("--reporter <type>", "Drift reporter: pretty|json|junit", "pretty").option("--report-file <path>", "Output file path for json/junit reporters").option("--fail-on-drift", "Exit with code 1 when drift is detected (enabled by default in CI)", false).option("--fallback-to-mock", "Fallback to mock responses when proxying fails", false).option("--verbose", "Enable verbose logging", false);
|
|
242
|
+
}
|
|
243
|
+
function parseCsvOption(value) {
|
|
244
|
+
if (!value || typeof value !== "string") {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
248
|
+
}
|
|
249
|
+
async function loadDriftIgnoreFromFile(cwd) {
|
|
250
|
+
const ignoreFilePath = path2.join(cwd, ".driftignore");
|
|
251
|
+
try {
|
|
252
|
+
const raw = await readFile(ignoreFilePath, "utf8");
|
|
253
|
+
return raw.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line && !line.startsWith("#"));
|
|
254
|
+
} catch {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
242
257
|
}
|
|
243
258
|
function createCli() {
|
|
244
259
|
const program = new Command();
|
|
245
|
-
program.name("contract-drift-detection").description("Stateful OpenAPI mock server with contract drift detection").version("0.1.
|
|
260
|
+
program.name("contract-drift-detection").description("Stateful OpenAPI mock server with contract drift detection").version("0.1.5");
|
|
246
261
|
applyServeOptions(program);
|
|
247
262
|
applyServeOptions(program.command("serve").description("Start the mock engine"));
|
|
248
263
|
program.command("init").description("Create a starter config for the current workspace").option("--spec <path>", "Default OpenAPI path", "openapi.yaml").option("--template <name>", "Template to generate (rest-crud | none)", "rest-crud").option("--db <path>", "Default JSON database path", ".mock-db.json").option("--port <port>", "Default port", "4010").option("--host <host>", "Default host", "0.0.0.0");
|
|
@@ -262,6 +277,13 @@ async function resolveServeConfig(cwd, options) {
|
|
|
262
277
|
if (!resolvedSpecPath) {
|
|
263
278
|
throw new Error("Provide one of --spec, --spec-url, or --discover");
|
|
264
279
|
}
|
|
280
|
+
const ignoreFromCli = parseCsvOption(options.driftIgnore);
|
|
281
|
+
const ignoreFromFile = await loadDriftIgnoreFromFile(cwd);
|
|
282
|
+
const mergedIgnore = Array.from(/* @__PURE__ */ new Set([...ignoreFromFile, ...ignoreFromCli]));
|
|
283
|
+
const reporter = String(options.reporter ?? "pretty");
|
|
284
|
+
if (!["pretty", "json", "junit"].includes(reporter)) {
|
|
285
|
+
throw new Error(`Invalid reporter '${reporter}'. Use one of: pretty, json, junit`);
|
|
286
|
+
}
|
|
265
287
|
return {
|
|
266
288
|
specPath: resolvedSpecPath,
|
|
267
289
|
port: Number(options.port ?? 4010),
|
|
@@ -269,6 +291,11 @@ async function resolveServeConfig(cwd, options) {
|
|
|
269
291
|
dbPath: resolvePathFromCwd(cwd, String(options.db ?? ".mock-db.json")),
|
|
270
292
|
corsOrigin: String(options.corsOrigin ?? "*"),
|
|
271
293
|
driftCheckTarget: options.driftCheck ? String(options.driftCheck) : void 0,
|
|
294
|
+
strictDrift: Boolean(options.strict),
|
|
295
|
+
driftIgnorePaths: mergedIgnore,
|
|
296
|
+
reporter,
|
|
297
|
+
reportFile: options.reportFile ? resolvePathFromCwd(cwd, String(options.reportFile)) : void 0,
|
|
298
|
+
failOnDrift: Boolean(options.failOnDrift) || process.env.CI === "true",
|
|
272
299
|
fallbackToMockOnProxyError: Boolean(options.fallbackToMock),
|
|
273
300
|
verbose: Boolean(options.verbose)
|
|
274
301
|
};
|
|
@@ -298,7 +325,7 @@ async function writeStarterConfig(cwd, initConfig) {
|
|
|
298
325
|
}
|
|
299
326
|
|
|
300
327
|
// src/server.ts
|
|
301
|
-
import
|
|
328
|
+
import path6 from "path";
|
|
302
329
|
import Fastify from "fastify";
|
|
303
330
|
import cors from "@fastify/cors";
|
|
304
331
|
|
|
@@ -453,33 +480,103 @@ function applyDslMutation(extension, request, collection, defaultIdKey) {
|
|
|
453
480
|
import Ajv from "ajv";
|
|
454
481
|
import addFormats from "ajv-formats";
|
|
455
482
|
import pc from "picocolors";
|
|
483
|
+
function deepClone2(value) {
|
|
484
|
+
return JSON.parse(JSON.stringify(value));
|
|
485
|
+
}
|
|
486
|
+
function pathMatchesIgnoreRule(instancePath, rule) {
|
|
487
|
+
if (!rule) {
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
const pathSegments = instancePath.split("/").filter(Boolean);
|
|
491
|
+
const ruleSegments = rule.split("/").filter(Boolean);
|
|
492
|
+
if (ruleSegments.length > pathSegments.length) {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
return ruleSegments.every((segment, index) => segment === "*" || segment === pathSegments[index]);
|
|
496
|
+
}
|
|
497
|
+
function applyStrictAdditionalProperties(schema) {
|
|
498
|
+
const output = deepClone2(schema);
|
|
499
|
+
const walk = (candidate) => {
|
|
500
|
+
if (candidate.type === "object" || candidate.properties) {
|
|
501
|
+
if (candidate.additionalProperties === void 0) {
|
|
502
|
+
candidate.additionalProperties = false;
|
|
503
|
+
}
|
|
504
|
+
for (const value of Object.values(candidate.properties ?? {})) {
|
|
505
|
+
if (value && !("$ref" in value)) {
|
|
506
|
+
walk(value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (candidate.type === "array" && candidate.items && !("$ref" in candidate.items)) {
|
|
511
|
+
walk(candidate.items);
|
|
512
|
+
}
|
|
513
|
+
for (const entry of candidate.allOf ?? []) {
|
|
514
|
+
if (!("$ref" in entry)) {
|
|
515
|
+
walk(entry);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
for (const entry of candidate.oneOf ?? []) {
|
|
519
|
+
if (!("$ref" in entry)) {
|
|
520
|
+
walk(entry);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
for (const entry of candidate.anyOf ?? []) {
|
|
524
|
+
if (!("$ref" in entry)) {
|
|
525
|
+
walk(entry);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
walk(output);
|
|
530
|
+
return output;
|
|
531
|
+
}
|
|
532
|
+
function buildAjvSchema(schema, strictMode) {
|
|
533
|
+
return strictMode ? applyStrictAdditionalProperties(schema) : schema;
|
|
534
|
+
}
|
|
456
535
|
function formatErrors(errors) {
|
|
457
536
|
return (errors ?? []).map((error) => {
|
|
458
537
|
const location = error.instancePath || "/";
|
|
459
538
|
return `${location} ${error.message ?? "failed validation"}`.trim();
|
|
460
539
|
});
|
|
461
540
|
}
|
|
541
|
+
function analyzeDrift(ajv, schema, body, options) {
|
|
542
|
+
const normalizedSchema = buildAjvSchema(schema, options.strictMode);
|
|
543
|
+
const validate = ajv.compile(normalizedSchema);
|
|
544
|
+
const valid = validate(body);
|
|
545
|
+
if (valid) {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
const filteredErrors = (validate.errors ?? []).filter(
|
|
549
|
+
(entry) => !options.ignorePaths.some((rule) => pathMatchesIgnoreRule(entry.instancePath || "/", rule))
|
|
550
|
+
);
|
|
551
|
+
return formatErrors(filteredErrors);
|
|
552
|
+
}
|
|
462
553
|
var DriftDetector = class {
|
|
463
|
-
constructor(logger) {
|
|
554
|
+
constructor(logger, options) {
|
|
464
555
|
this.logger = logger;
|
|
465
556
|
addFormats(this.ajv);
|
|
557
|
+
this.strictMode = options?.strictMode ?? false;
|
|
558
|
+
this.ignorePaths = options?.ignorePaths ?? [];
|
|
466
559
|
}
|
|
467
560
|
ajv = new Ajv({ allErrors: true, strict: false });
|
|
468
|
-
|
|
561
|
+
strictMode;
|
|
562
|
+
ignorePaths;
|
|
563
|
+
validate(method, path7, statusCode, schema, body) {
|
|
469
564
|
if (!schema || body === void 0 || body === null) {
|
|
470
565
|
return null;
|
|
471
566
|
}
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
567
|
+
const errors = analyzeDrift(this.ajv, schema, body, {
|
|
568
|
+
strictMode: this.strictMode,
|
|
569
|
+
ignorePaths: this.ignorePaths
|
|
570
|
+
});
|
|
571
|
+
if (!errors.length) {
|
|
475
572
|
return null;
|
|
476
573
|
}
|
|
477
574
|
const issue = {
|
|
478
575
|
method: method.toUpperCase(),
|
|
479
|
-
path:
|
|
576
|
+
path: path7,
|
|
480
577
|
statusCode,
|
|
481
|
-
message: `Drift detected for ${method.toUpperCase()} ${
|
|
482
|
-
errors
|
|
578
|
+
message: `Drift detected for ${method.toUpperCase()} ${path7} (${statusCode})`,
|
|
579
|
+
errors
|
|
483
580
|
};
|
|
484
581
|
const errorLines = issue.errors.map((entry) => ` \u2022 ${entry}`).join("\n");
|
|
485
582
|
this.logger.error([
|
|
@@ -635,7 +732,7 @@ async function loadOpenApiDocument(specPath) {
|
|
|
635
732
|
}
|
|
636
733
|
|
|
637
734
|
// src/state-store.ts
|
|
638
|
-
import { mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
|
|
735
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
639
736
|
import path4 from "path";
|
|
640
737
|
function normalizeDatabase(input) {
|
|
641
738
|
return {
|
|
@@ -676,7 +773,7 @@ var JsonStateStore = class {
|
|
|
676
773
|
}
|
|
677
774
|
}
|
|
678
775
|
async read() {
|
|
679
|
-
const raw = await
|
|
776
|
+
const raw = await readFile2(this.filePath, "utf8");
|
|
680
777
|
return normalizeDatabase(JSON.parse(raw));
|
|
681
778
|
}
|
|
682
779
|
async write(database) {
|
|
@@ -692,8 +789,8 @@ var JsonStateStore = class {
|
|
|
692
789
|
};
|
|
693
790
|
|
|
694
791
|
// src/proxy.ts
|
|
695
|
-
async function proxyRequest(targetBaseUrl,
|
|
696
|
-
const response = await fetch(new URL(
|
|
792
|
+
async function proxyRequest(targetBaseUrl, path7, init) {
|
|
793
|
+
const response = await fetch(new URL(path7, targetBaseUrl), init);
|
|
697
794
|
const contentType = readContentType(response.headers);
|
|
698
795
|
let body;
|
|
699
796
|
let rawBody;
|
|
@@ -714,6 +811,56 @@ async function proxyRequest(targetBaseUrl, path6, init) {
|
|
|
714
811
|
};
|
|
715
812
|
}
|
|
716
813
|
|
|
814
|
+
// src/drift-reporter.ts
|
|
815
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
816
|
+
import path5 from "path";
|
|
817
|
+
function escapeXml(value) {
|
|
818
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
819
|
+
}
|
|
820
|
+
function toJunit(issues) {
|
|
821
|
+
const testCases = issues.map((issue, index) => {
|
|
822
|
+
const name = `${issue.method} ${issue.path} (${issue.statusCode}) #${index + 1}`;
|
|
823
|
+
const failure = escapeXml(issue.errors.join("; "));
|
|
824
|
+
return ` <testcase classname="contract-drift-detection" name="${escapeXml(name)}">
|
|
825
|
+
<failure message="drift detected">${failure}</failure>
|
|
826
|
+
</testcase>`;
|
|
827
|
+
}).join("\n");
|
|
828
|
+
return [
|
|
829
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
830
|
+
`<testsuite name="contract-drift-detection" tests="${issues.length}" failures="${issues.length}">`,
|
|
831
|
+
testCases,
|
|
832
|
+
"</testsuite>",
|
|
833
|
+
""
|
|
834
|
+
].join("\n");
|
|
835
|
+
}
|
|
836
|
+
function toJson(issues) {
|
|
837
|
+
const report = {
|
|
838
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
839
|
+
summary: {
|
|
840
|
+
total: issues.length
|
|
841
|
+
},
|
|
842
|
+
issues
|
|
843
|
+
};
|
|
844
|
+
return `${JSON.stringify(report, null, 2)}
|
|
845
|
+
`;
|
|
846
|
+
}
|
|
847
|
+
function defaultReportPath(config) {
|
|
848
|
+
if (config.reportFile) {
|
|
849
|
+
return config.reportFile;
|
|
850
|
+
}
|
|
851
|
+
return config.reporter === "junit" ? "cdd-drift-report.xml" : "cdd-drift-report.json";
|
|
852
|
+
}
|
|
853
|
+
async function writeDriftReport(config, issues) {
|
|
854
|
+
if (config.reporter === "pretty") {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
const filePath = defaultReportPath(config);
|
|
858
|
+
const content = config.reporter === "junit" ? toJunit(issues) : toJson(issues);
|
|
859
|
+
await mkdir4(path5.dirname(filePath), { recursive: true });
|
|
860
|
+
await writeFile4(filePath, content, "utf8");
|
|
861
|
+
return filePath;
|
|
862
|
+
}
|
|
863
|
+
|
|
717
864
|
// src/server.ts
|
|
718
865
|
function toProxyHeaders(inputHeaders) {
|
|
719
866
|
const passthrough = ["host", "content-length", "connection"];
|
|
@@ -886,7 +1033,7 @@ async function handleMockRoute(store, route, request) {
|
|
|
886
1033
|
}
|
|
887
1034
|
});
|
|
888
1035
|
}
|
|
889
|
-
async function handleProxyRoute(config,
|
|
1036
|
+
async function handleProxyRoute(config, request) {
|
|
890
1037
|
const targetBaseUrl = config.driftCheckTarget;
|
|
891
1038
|
if (!targetBaseUrl) {
|
|
892
1039
|
throw new Error("Proxy target is not configured");
|
|
@@ -900,15 +1047,6 @@ async function handleProxyRoute(config, route, detector, request) {
|
|
|
900
1047
|
headers,
|
|
901
1048
|
body: toProxyBody(request)
|
|
902
1049
|
});
|
|
903
|
-
if (result.statusCode >= 200 && result.statusCode < 300) {
|
|
904
|
-
detector.validate(
|
|
905
|
-
route.method,
|
|
906
|
-
route.path,
|
|
907
|
-
result.statusCode,
|
|
908
|
-
route.successResponse?.schema,
|
|
909
|
-
result.body
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
1050
|
return {
|
|
913
1051
|
statusCode: result.statusCode,
|
|
914
1052
|
headers: Object.fromEntries(result.headers.entries()),
|
|
@@ -917,7 +1055,11 @@ async function handleProxyRoute(config, route, detector, request) {
|
|
|
917
1055
|
}
|
|
918
1056
|
async function registerRoutes(app, document, config, store) {
|
|
919
1057
|
const routes = buildRouteContexts(document);
|
|
920
|
-
const detector = new DriftDetector(app.log
|
|
1058
|
+
const detector = new DriftDetector(app.log, {
|
|
1059
|
+
strictMode: config.strictDrift,
|
|
1060
|
+
ignorePaths: config.driftIgnorePaths
|
|
1061
|
+
});
|
|
1062
|
+
const driftIssues = [];
|
|
921
1063
|
for (const route of routes) {
|
|
922
1064
|
app.route({
|
|
923
1065
|
method: route.method.toUpperCase(),
|
|
@@ -925,7 +1067,21 @@ async function registerRoutes(app, document, config, store) {
|
|
|
925
1067
|
handler: async (request, reply) => {
|
|
926
1068
|
if (config.driftCheckTarget) {
|
|
927
1069
|
try {
|
|
928
|
-
const proxied = await handleProxyRoute(config,
|
|
1070
|
+
const proxied = await handleProxyRoute(config, request);
|
|
1071
|
+
if (proxied.statusCode >= 200 && proxied.statusCode < 300 && proxied.body !== void 0) {
|
|
1072
|
+
const issue = detector.validate(
|
|
1073
|
+
route.method,
|
|
1074
|
+
route.path,
|
|
1075
|
+
proxied.statusCode,
|
|
1076
|
+
route.successResponse?.schema,
|
|
1077
|
+
proxied.body
|
|
1078
|
+
);
|
|
1079
|
+
if (issue) {
|
|
1080
|
+
driftIssues.push(issue);
|
|
1081
|
+
await writeDriftReport(config, driftIssues);
|
|
1082
|
+
await config.onDriftDetected?.(issue);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
929
1085
|
for (const [headerName, headerValue] of Object.entries(proxied.headers)) {
|
|
930
1086
|
if (headerName.toLowerCase() === "content-length") {
|
|
931
1087
|
continue;
|
|
@@ -955,6 +1111,10 @@ async function registerRoutes(app, document, config, store) {
|
|
|
955
1111
|
hasDsl: Boolean(route.operation["x-mock-state"])
|
|
956
1112
|
}))
|
|
957
1113
|
);
|
|
1114
|
+
app.get("/__drift", async () => ({
|
|
1115
|
+
total: driftIssues.length,
|
|
1116
|
+
issues: driftIssues
|
|
1117
|
+
}));
|
|
958
1118
|
}
|
|
959
1119
|
async function createServer(config) {
|
|
960
1120
|
const app = Fastify({
|
|
@@ -970,7 +1130,7 @@ async function createServer(config) {
|
|
|
970
1130
|
});
|
|
971
1131
|
const document = await loadOpenApiDocument(config.specPath);
|
|
972
1132
|
const seedCollections = inferSeedCollections(document);
|
|
973
|
-
const dbPath = resolveFile(
|
|
1133
|
+
const dbPath = resolveFile(path6.dirname(config.specPath), config.dbPath);
|
|
974
1134
|
const store = new JsonStateStore(dbPath);
|
|
975
1135
|
await store.initialize(seedCollections);
|
|
976
1136
|
app.get("/__health", async () => ({ status: "ok" }));
|
|
@@ -986,8 +1146,12 @@ function renderStartupBanner(config) {
|
|
|
986
1146
|
`- Spec: ${config.specPath}`,
|
|
987
1147
|
`- DB: ${config.dbPath}`,
|
|
988
1148
|
`- Mode: ${config.driftCheckTarget ? `proxy + drift-check (${config.driftCheckTarget})` : "stateful mock"}`,
|
|
1149
|
+
`- Drift policy: strict=${config.strictDrift ? "on" : "off"}, fail-on-drift=${config.failOnDrift ? "on" : "off"}`,
|
|
1150
|
+
`- Reporter: ${config.reporter}${config.reportFile ? ` (${config.reportFile})` : ""}`,
|
|
1151
|
+
`- Ignore rules: ${config.driftIgnorePaths.length ? config.driftIgnorePaths.join(", ") : "none"}`,
|
|
989
1152
|
"- Health: GET /__health",
|
|
990
|
-
"- Routes: GET /__routes"
|
|
1153
|
+
"- Routes: GET /__routes",
|
|
1154
|
+
"- Drift: GET /__drift"
|
|
991
1155
|
];
|
|
992
1156
|
return `${lines.join("\n")}
|
|
993
1157
|
`;
|
|
@@ -998,10 +1162,21 @@ async function main() {
|
|
|
998
1162
|
const initCommand = cli.commands.find((command) => command.name() === "init");
|
|
999
1163
|
const quickstartCommand = cli.commands.find((command) => command.name() === "quickstart");
|
|
1000
1164
|
const startServer = async (rawOptions) => {
|
|
1001
|
-
|
|
1165
|
+
let hasFailedOnDrift = false;
|
|
1166
|
+
const config = await resolveServeConfig(process2.cwd(), rawOptions);
|
|
1167
|
+
config.onDriftDetected = async () => {
|
|
1168
|
+
if (!config.failOnDrift || hasFailedOnDrift) {
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
hasFailedOnDrift = true;
|
|
1172
|
+
process2.stderr.write("Failing process because drift was detected (--fail-on-drift enabled).\n");
|
|
1173
|
+
setTimeout(() => {
|
|
1174
|
+
process2.exit(1);
|
|
1175
|
+
}, 25);
|
|
1176
|
+
};
|
|
1002
1177
|
const server = await createServer(config);
|
|
1003
1178
|
await server.listen({ port: config.port, host: config.host });
|
|
1004
|
-
|
|
1179
|
+
process2.stdout.write(renderStartupBanner(config));
|
|
1005
1180
|
};
|
|
1006
1181
|
serveCommand?.action(async function() {
|
|
1007
1182
|
await startServer(this.opts());
|
|
@@ -1016,19 +1191,19 @@ async function main() {
|
|
|
1016
1191
|
});
|
|
1017
1192
|
initCommand?.action(async function() {
|
|
1018
1193
|
const options = this.opts();
|
|
1019
|
-
const targetPath = await writeStarterConfig(
|
|
1194
|
+
const targetPath = await writeStarterConfig(process2.cwd(), {
|
|
1020
1195
|
spec: String(options.spec),
|
|
1021
1196
|
db: String(options.db),
|
|
1022
1197
|
host: String(options.host),
|
|
1023
1198
|
port: Number(options.port),
|
|
1024
1199
|
template: options.template === "none" ? "none" : "rest-crud"
|
|
1025
1200
|
});
|
|
1026
|
-
|
|
1201
|
+
process2.stdout.write(`Created ${targetPath}
|
|
1027
1202
|
`);
|
|
1028
1203
|
});
|
|
1029
1204
|
quickstartCommand?.action(async function() {
|
|
1030
1205
|
const options = this.opts();
|
|
1031
|
-
const cwd =
|
|
1206
|
+
const cwd = process2.cwd();
|
|
1032
1207
|
const specPath = String(options.spec ?? "openapi.yaml");
|
|
1033
1208
|
await writeStarterConfig(cwd, {
|
|
1034
1209
|
spec: specPath,
|
|
@@ -1047,22 +1222,22 @@ async function main() {
|
|
|
1047
1222
|
});
|
|
1048
1223
|
const server = await createServer(config);
|
|
1049
1224
|
await server.listen({ port: config.port, host: config.host });
|
|
1050
|
-
|
|
1225
|
+
process2.stdout.write(renderStartupBanner(config));
|
|
1051
1226
|
});
|
|
1052
|
-
await cli.parseAsync(
|
|
1227
|
+
await cli.parseAsync(process2.argv);
|
|
1053
1228
|
}
|
|
1054
1229
|
await main().catch((error) => {
|
|
1055
1230
|
if (error instanceof Error) {
|
|
1056
|
-
|
|
1231
|
+
process2.stderr.write(`Error: ${error.message}
|
|
1057
1232
|
`);
|
|
1058
|
-
if (
|
|
1059
|
-
|
|
1233
|
+
if (process2.env.CDD_SHOW_STACK === "1") {
|
|
1234
|
+
process2.stderr.write(`${error.stack}
|
|
1060
1235
|
`);
|
|
1061
1236
|
}
|
|
1062
1237
|
} else {
|
|
1063
|
-
|
|
1238
|
+
process2.stderr.write(`${String(error)}
|
|
1064
1239
|
`);
|
|
1065
1240
|
}
|
|
1066
|
-
|
|
1241
|
+
process2.exitCode = 1;
|
|
1067
1242
|
});
|
|
1068
1243
|
//# sourceMappingURL=cli.js.map
|