contract-drift-detection 0.1.0 → 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## 0.1.2 - 2026-03-16
6
+
7
+ ### Added
8
+
9
+ - Root command now accepts serve options directly for faster first-run usage.
10
+ - New runtime diagnostics endpoint: `GET /__routes`.
11
+ - Proxy robustness test coverage for auth header forwarding and diagnostics route.
12
+
13
+ ### Improved
14
+
15
+ - Startup output now prints a clear banner with mode/spec/db/routes information.
16
+ - Drift alerts are more prominent and actionable in terminal output.
17
+ - Drift validation now skips non-2xx proxy responses to reduce false/noisy alerts.
18
+ - Proxy forwarding now preserves incoming request headers more faithfully.
19
+
20
+ ## 0.1.1 - 2026-03-16
21
+
22
+ ### Added
23
+
24
+ - New `serve` onboarding modes:
25
+ - `--spec-url <url>` to fetch remote OpenAPI specs
26
+ - `--discover <backend-url>` to auto-discover OpenAPI endpoints
27
+ - New `quickstart` command to generate starter contract and run instantly.
28
+ - `init --template rest-crud|none` for template-driven setup.
29
+ - Local cache for remote/discovered specs under `.cdd/`.
30
+ - New onboarding tests covering local, remote, and discovery-based spec resolution.
31
+
32
+ ### Improved
33
+
34
+ - README now includes no-friction first-run flows for users without an existing local spec file.
35
+
36
+ ## 0.1.0 - 2026-03-16
37
+
38
+ ### Added
39
+
40
+ - Initial public release with stateful OpenAPI mock server.
41
+ - CRUD route generation from OpenAPI 3.x documents.
42
+ - `x-mock-state` workflow action support.
43
+ - Proxy mode with contract drift detection.
44
+ - Browser CORS support and frontend/backend demo harness.
package/README.md CHANGED
@@ -39,9 +39,11 @@ This project combines both and adds a third layer: drift detection against a liv
39
39
  ### Use directly with `npx` (no install)
40
40
 
41
41
  ```bash
42
- npx contract-drift-detection@latest serve --spec ./openapi.yaml --port 4010
42
+ npx contract-drift-detection@latest --spec ./openapi.yaml --port 4010
43
43
  ```
44
44
 
45
+ (`serve` subcommand is optional; root command accepts serve options.)
46
+
45
47
  ### Install globally
46
48
 
47
49
  ```bash
@@ -64,23 +66,41 @@ npm run dev
64
66
 
65
67
  ## 30-second quick start
66
68
 
67
- Run the included example API:
69
+ No spec file yet? Start instantly:
70
+
71
+ ```bash
72
+ npx contract-drift-detection@latest quickstart
73
+ ```
74
+
75
+ Already have an OpenAPI file? Start with local spec:
68
76
 
69
77
  ```bash
70
- npx tsx src/cli.ts serve --spec ./examples/helpdesk.openapi.yaml --port 4010
78
+ npx contract-drift-detection@latest serve --spec ./openapi.yaml --port 4010
71
79
  ```
72
80
 
73
- Create a starter config file in any project:
81
+ Create a starter config + starter OpenAPI file in current directory:
74
82
 
75
83
  ```bash
76
- npx tsx src/cli.ts init
84
+ npx contract-drift-detection@latest init
85
+ ```
86
+
87
+ If your backend already exposes OpenAPI, avoid writing spec manually:
88
+
89
+ ```bash
90
+ npx contract-drift-detection@latest serve --discover http://localhost:8080 --port 4010
91
+ ```
92
+
93
+ Or use direct remote spec URL:
94
+
95
+ ```bash
96
+ npx contract-drift-detection@latest serve --spec-url http://localhost:8080/openapi.json --port 4010
77
97
  ```
78
98
 
79
99
  Start in drift detection mode against a real backend:
80
100
 
81
101
  ```bash
82
- npx tsx src/cli.ts serve \
83
- --spec ./examples/helpdesk.openapi.yaml \
102
+ npx contract-drift-detection@latest serve \
103
+ --discover http://localhost:8080 \
84
104
  --port 4010 \
85
105
  --drift-check http://localhost:8080
86
106
  ```
@@ -92,12 +112,24 @@ Point your frontend API base URL to `http://localhost:4010`.
92
112
  ## CLI
93
113
 
94
114
  ```text
95
- contract-drift-detection serve --spec <path> [--port 4010] [--host 0.0.0.0] [--db .mock-db.json] [--cors-origin *] [--drift-check <url>] [--fallback-to-mock] [--verbose]
96
- contract-drift-detection init [--spec openapi.yaml] [--db .mock-db.json] [--port 4010] [--host 0.0.0.0]
115
+ contract-drift-detection serve [--spec <path> | --spec-url <url> | --discover <backend-url>] [--port 4010] [--host 0.0.0.0] [--db .mock-db.json] [--cors-origin *] [--drift-check <url>] [--fallback-to-mock] [--verbose]
116
+ contract-drift-detection init [--spec openapi.yaml] [--template rest-crud|none] [--db .mock-db.json] [--port 4010] [--host 0.0.0.0]
117
+ contract-drift-detection quickstart [--spec openapi.yaml] [--port 4010] [--host 0.0.0.0] [--db .mock-db.json] [--cors-origin *] [--verbose]
118
+
119
+ # Root command (equivalent to serve)
120
+ contract-drift-detection [--spec <path> | --spec-url <url> | --discover <backend-url>] [--port 4010] ...
97
121
  ```
98
122
 
99
123
  `--cors-origin` defaults to `*` for frictionless browser testing.
100
124
 
125
+ Spec resolution priority in `serve` is:
126
+
127
+ 1. `--spec`
128
+ 2. `--spec-url`
129
+ 3. `--discover`
130
+
131
+ Remote/discovered specs are cached locally under `.cdd/`.
132
+
101
133
  ## Recommended workflows
102
134
 
103
135
  ### A) Frontend-first (mock only)
@@ -149,6 +181,7 @@ Values inside `assign` and `set` can reference request input with `{{body.field}
149
181
 
150
182
  - `GET /__health` returns a simple health payload.
151
183
  - `GET /__spec` returns the dereferenced OpenAPI document currently in memory.
184
+ - `GET /__routes` returns generated route diagnostics (method/path/operation).
152
185
 
153
186
  ## Browser/CORS notes
154
187
 
package/dist/cli.js CHANGED
@@ -4,22 +4,257 @@
4
4
  import process from "process";
5
5
 
6
6
  // src/config.ts
7
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
8
+ import path2 from "path";
9
+ import { Command } from "commander";
10
+
11
+ // src/onboarding.ts
7
12
  import { mkdir, writeFile } from "fs/promises";
8
13
  import path from "path";
9
- import { Command } from "commander";
14
+ var DISCOVERY_PATHS = [
15
+ "/openapi.json",
16
+ "/openapi.yaml",
17
+ "/swagger.json",
18
+ "/v3/api-docs",
19
+ "/api-docs-json"
20
+ ];
21
+ var STARTER_SPEC = `openapi: 3.0.3
22
+ info:
23
+ title: Contract Drift Detection Starter API
24
+ version: 1.0.0
25
+ paths:
26
+ /users:
27
+ get:
28
+ responses:
29
+ '200':
30
+ description: ok
31
+ content:
32
+ application/json:
33
+ schema:
34
+ type: array
35
+ items:
36
+ $ref: '#/components/schemas/User'
37
+ post:
38
+ requestBody:
39
+ required: true
40
+ content:
41
+ application/json:
42
+ schema:
43
+ $ref: '#/components/schemas/UserCreateInput'
44
+ responses:
45
+ '201':
46
+ description: created
47
+ content:
48
+ application/json:
49
+ schema:
50
+ $ref: '#/components/schemas/User'
51
+ /users/{id}:
52
+ get:
53
+ parameters:
54
+ - $ref: '#/components/parameters/UserId'
55
+ responses:
56
+ '200':
57
+ description: ok
58
+ content:
59
+ application/json:
60
+ schema:
61
+ $ref: '#/components/schemas/User'
62
+ patch:
63
+ parameters:
64
+ - $ref: '#/components/parameters/UserId'
65
+ requestBody:
66
+ required: true
67
+ content:
68
+ application/json:
69
+ schema:
70
+ $ref: '#/components/schemas/UserPatchInput'
71
+ responses:
72
+ '200':
73
+ description: updated
74
+ content:
75
+ application/json:
76
+ schema:
77
+ $ref: '#/components/schemas/User'
78
+ delete:
79
+ parameters:
80
+ - $ref: '#/components/parameters/UserId'
81
+ responses:
82
+ '204':
83
+ description: deleted
84
+ /tickets:
85
+ get:
86
+ responses:
87
+ '200':
88
+ description: ok
89
+ content:
90
+ application/json:
91
+ schema:
92
+ type: array
93
+ items:
94
+ $ref: '#/components/schemas/Ticket'
95
+ /tickets/{id}/resolve:
96
+ post:
97
+ parameters:
98
+ - $ref: '#/components/parameters/TicketId'
99
+ x-mock-state:
100
+ action: update
101
+ target: tickets
102
+ find_by: id
103
+ set:
104
+ status: resolved
105
+ responses:
106
+ '200':
107
+ description: ok
108
+ content:
109
+ application/json:
110
+ schema:
111
+ $ref: '#/components/schemas/Ticket'
112
+ components:
113
+ parameters:
114
+ UserId:
115
+ name: id
116
+ in: path
117
+ required: true
118
+ schema:
119
+ type: integer
120
+ TicketId:
121
+ name: id
122
+ in: path
123
+ required: true
124
+ schema:
125
+ type: integer
126
+ schemas:
127
+ User:
128
+ type: object
129
+ required: [id, name, email]
130
+ properties:
131
+ id:
132
+ type: integer
133
+ name:
134
+ type: string
135
+ email:
136
+ type: string
137
+ format: email
138
+ UserCreateInput:
139
+ type: object
140
+ required: [name, email]
141
+ properties:
142
+ name:
143
+ type: string
144
+ email:
145
+ type: string
146
+ format: email
147
+ UserPatchInput:
148
+ type: object
149
+ properties:
150
+ name:
151
+ type: string
152
+ email:
153
+ type: string
154
+ format: email
155
+ Ticket:
156
+ type: object
157
+ required: [id, title, status]
158
+ properties:
159
+ id:
160
+ type: integer
161
+ title:
162
+ type: string
163
+ status:
164
+ type: string
165
+ enum: [open, resolved]
166
+ `;
167
+ function normalizeBaseUrl(value) {
168
+ return value.endsWith("/") ? value.slice(0, -1) : value;
169
+ }
170
+ function inferSpecExtension(contentType, content) {
171
+ const normalized = contentType.toLowerCase();
172
+ if (normalized.includes("json")) {
173
+ return "json";
174
+ }
175
+ const trimmed = content.trim();
176
+ return trimmed.startsWith("{") || trimmed.startsWith("[") ? "json" : "yaml";
177
+ }
178
+ async function cacheSpecContent(cwd, specContent, extension, filePrefix) {
179
+ const cacheDir = path.join(cwd, ".cdd");
180
+ await mkdir(cacheDir, { recursive: true });
181
+ const filePath = path.join(cacheDir, `${filePrefix}.${extension}`);
182
+ await writeFile(filePath, specContent, "utf8");
183
+ return filePath;
184
+ }
185
+ async function cacheSpecFromUrl(cwd, specUrl) {
186
+ const response = await fetch(specUrl);
187
+ if (!response.ok) {
188
+ throw new Error(`Failed to download OpenAPI spec from ${specUrl}: ${response.status} ${response.statusText}`);
189
+ }
190
+ const content = await response.text();
191
+ const extension = inferSpecExtension(response.headers.get("content-type") ?? "", content);
192
+ return cacheSpecContent(cwd, content, extension, "openapi.cached");
193
+ }
194
+ async function discoverSpecUrl(backendBaseUrl) {
195
+ const baseUrl = normalizeBaseUrl(backendBaseUrl);
196
+ for (const candidatePath of DISCOVERY_PATHS) {
197
+ const candidateUrl = `${baseUrl}${candidatePath}`;
198
+ try {
199
+ const response = await fetch(candidateUrl, { method: "GET" });
200
+ if (!response.ok) {
201
+ continue;
202
+ }
203
+ const body = await response.text();
204
+ const maybeSpec = body.includes("openapi") || body.includes("swagger");
205
+ if (!maybeSpec) {
206
+ continue;
207
+ }
208
+ return candidateUrl;
209
+ } catch {
210
+ continue;
211
+ }
212
+ }
213
+ throw new Error(
214
+ `Could not discover an OpenAPI spec under ${baseUrl}. Tried: ${DISCOVERY_PATHS.join(", ")}`
215
+ );
216
+ }
217
+ async function writeStarterSpec(cwd, specPath) {
218
+ const resolvedPath = path.isAbsolute(specPath) ? specPath : path.join(cwd, specPath);
219
+ await mkdir(path.dirname(resolvedPath), { recursive: true });
220
+ await writeFile(resolvedPath, STARTER_SPEC, "utf8");
221
+ return resolvedPath;
222
+ }
223
+
224
+ // src/config.ts
225
+ function resolvePathFromCwd(cwd, value) {
226
+ return path2.isAbsolute(value) ? value : path2.join(cwd, value);
227
+ }
228
+ function applyServeOptions(command) {
229
+ 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);
230
+ }
10
231
  function createCli() {
11
232
  const program = new Command();
12
233
  program.name("contract-drift-detection").description("Stateful OpenAPI mock server with contract drift detection").version("0.1.0");
13
- program.command("serve").description("Start the mock engine").requiredOption("--spec <path>", "Path to an OpenAPI 3.x file").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);
14
- program.command("init").description("Create a starter config for the current workspace").option("--spec <path>", "Default OpenAPI path", "openapi.yaml").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");
234
+ applyServeOptions(program);
235
+ applyServeOptions(program.command("serve").description("Start the mock engine"));
236
+ 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");
237
+ program.command("quickstart").description("Generate a starter OpenAPI file and start the server immediately").option("--spec <path>", "Starter OpenAPI path", "openapi.yaml").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("--verbose", "Enable verbose logging", false);
15
238
  return program;
16
239
  }
17
- function toServeConfig(options) {
240
+ async function resolveServeConfig(cwd, options) {
241
+ let resolvedSpecPath;
242
+ if (options.spec) {
243
+ resolvedSpecPath = resolvePathFromCwd(cwd, String(options.spec));
244
+ } else if (options.specUrl) {
245
+ resolvedSpecPath = await cacheSpecFromUrl(cwd, String(options.specUrl));
246
+ } else if (options.discover) {
247
+ const discoveredSpecUrl = await discoverSpecUrl(String(options.discover));
248
+ resolvedSpecPath = await cacheSpecFromUrl(cwd, discoveredSpecUrl);
249
+ }
250
+ if (!resolvedSpecPath) {
251
+ throw new Error("Provide one of --spec, --spec-url, or --discover");
252
+ }
18
253
  return {
19
- specPath: String(options.spec),
254
+ specPath: resolvedSpecPath,
20
255
  port: Number(options.port ?? 4010),
21
256
  host: String(options.host ?? "0.0.0.0"),
22
- dbPath: String(options.db ?? ".mock-db.json"),
257
+ dbPath: resolvePathFromCwd(cwd, String(options.db ?? ".mock-db.json")),
23
258
  corsOrigin: String(options.corsOrigin ?? "*"),
24
259
  driftCheckTarget: options.driftCheck ? String(options.driftCheck) : void 0,
25
260
  fallbackToMockOnProxyError: Boolean(options.fallbackToMock),
@@ -27,9 +262,12 @@ function toServeConfig(options) {
27
262
  };
28
263
  }
29
264
  async function writeStarterConfig(cwd, initConfig) {
30
- const targetPath = path.join(cwd, "contract-drift.config.json");
31
- await mkdir(cwd, { recursive: true });
32
- await writeFile(
265
+ if (initConfig.template === "rest-crud") {
266
+ await writeStarterSpec(cwd, initConfig.spec);
267
+ }
268
+ const targetPath = path2.join(cwd, "contract-drift.config.json");
269
+ await mkdir2(cwd, { recursive: true });
270
+ await writeFile2(
33
271
  targetPath,
34
272
  `${JSON.stringify(
35
273
  {
@@ -48,12 +286,12 @@ async function writeStarterConfig(cwd, initConfig) {
48
286
  }
49
287
 
50
288
  // src/server.ts
51
- import path4 from "path";
289
+ import path5 from "path";
52
290
  import Fastify from "fastify";
53
291
  import cors from "@fastify/cors";
54
292
 
55
293
  // src/utils.ts
56
- import path2 from "path";
294
+ import path3 from "path";
57
295
  function isSchemaObject(schema) {
58
296
  return Boolean(schema) && !("$ref" in schema);
59
297
  }
@@ -73,7 +311,7 @@ function singularize(value) {
73
311
  return value;
74
312
  }
75
313
  function resolveFile(baseDir, filePath) {
76
- return path2.isAbsolute(filePath) ? filePath : path2.join(baseDir, filePath);
314
+ return path3.isAbsolute(filePath) ? filePath : path3.join(baseDir, filePath);
77
315
  }
78
316
  function getOperationId(method, openApiPath, operation) {
79
317
  if (operation.operationId) {
@@ -215,7 +453,7 @@ var DriftDetector = class {
215
453
  addFormats(this.ajv);
216
454
  }
217
455
  ajv = new Ajv({ allErrors: true, strict: false });
218
- validate(method, path5, statusCode, schema, body) {
456
+ validate(method, path6, statusCode, schema, body) {
219
457
  if (!schema || body === void 0 || body === null) {
220
458
  return null;
221
459
  }
@@ -226,14 +464,16 @@ var DriftDetector = class {
226
464
  }
227
465
  const issue = {
228
466
  method: method.toUpperCase(),
229
- path: path5,
467
+ path: path6,
230
468
  statusCode,
231
- message: `Drift detected for ${method.toUpperCase()} ${path5} (${statusCode})`,
469
+ message: `Drift detected for ${method.toUpperCase()} ${path6} (${statusCode})`,
232
470
  errors: formatErrors(validate.errors)
233
471
  };
234
- this.logger.error(
235
- `${pc.red("DRIFT DETECTED")} ${issue.method} ${issue.path} -> ${issue.errors.join("; ")}`
236
- );
472
+ const errorLines = issue.errors.map((entry) => ` \u2022 ${entry}`).join("\n");
473
+ this.logger.error([
474
+ `${pc.bgRed(pc.black(" \u{1F6A8} DRIFT DETECTED "))} ${pc.bold(issue.method)} ${issue.path} (${statusCode})`,
475
+ errorLines
476
+ ].join("\n"));
237
477
  return issue;
238
478
  }
239
479
  };
@@ -383,8 +623,8 @@ async function loadOpenApiDocument(specPath) {
383
623
  }
384
624
 
385
625
  // src/state-store.ts
386
- import { mkdir as mkdir2, readFile, writeFile as writeFile2 } from "fs/promises";
387
- import path3 from "path";
626
+ import { mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
627
+ import path4 from "path";
388
628
  function normalizeDatabase(input) {
389
629
  return {
390
630
  collections: input?.collections ?? {},
@@ -397,7 +637,7 @@ var JsonStateStore = class {
397
637
  this.filePath = filePath;
398
638
  }
399
639
  async initialize(seedCollections) {
400
- await mkdir2(path3.dirname(this.filePath), { recursive: true });
640
+ await mkdir3(path4.dirname(this.filePath), { recursive: true });
401
641
  try {
402
642
  const existing = await this.read();
403
643
  let changed = false;
@@ -428,7 +668,7 @@ var JsonStateStore = class {
428
668
  return normalizeDatabase(JSON.parse(raw));
429
669
  }
430
670
  async write(database) {
431
- await writeFile2(this.filePath, `${JSON.stringify(database, null, 2)}
671
+ await writeFile3(this.filePath, `${JSON.stringify(database, null, 2)}
432
672
  `, "utf8");
433
673
  }
434
674
  async withDatabase(updater) {
@@ -440,8 +680,8 @@ var JsonStateStore = class {
440
680
  };
441
681
 
442
682
  // src/proxy.ts
443
- async function proxyRequest(targetBaseUrl, path5, init) {
444
- const response = await fetch(new URL(path5, targetBaseUrl), init);
683
+ async function proxyRequest(targetBaseUrl, path6, init) {
684
+ const response = await fetch(new URL(path6, targetBaseUrl), init);
445
685
  const contentType = readContentType(response.headers);
446
686
  let body;
447
687
  let rawBody;
@@ -463,6 +703,32 @@ async function proxyRequest(targetBaseUrl, path5, init) {
463
703
  }
464
704
 
465
705
  // src/server.ts
706
+ function toProxyHeaders(inputHeaders) {
707
+ const passthrough = ["host", "content-length", "connection"];
708
+ const headers = {};
709
+ for (const [key, value] of Object.entries(inputHeaders)) {
710
+ if (passthrough.includes(key)) {
711
+ continue;
712
+ }
713
+ if (value === void 0) {
714
+ continue;
715
+ }
716
+ headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
717
+ }
718
+ return headers;
719
+ }
720
+ function toProxyBody(request) {
721
+ if (request.method === "GET" || request.method === "HEAD") {
722
+ return void 0;
723
+ }
724
+ if (request.body === void 0 || request.body === null) {
725
+ return void 0;
726
+ }
727
+ if (typeof request.body === "string" || request.body instanceof Uint8Array) {
728
+ return request.body;
729
+ }
730
+ return JSON.stringify(request.body);
731
+ }
466
732
  function sendResponse(reply, statusCode, body) {
467
733
  if (statusCode === 204) {
468
734
  return reply.code(204).send();
@@ -609,25 +875,28 @@ async function handleMockRoute(store, route, request) {
609
875
  });
610
876
  }
611
877
  async function handleProxyRoute(config, route, detector, request) {
612
- const rawBody = request.body ? JSON.stringify(request.body) : void 0;
613
878
  const targetBaseUrl = config.driftCheckTarget;
614
879
  if (!targetBaseUrl) {
615
880
  throw new Error("Proxy target is not configured");
616
881
  }
882
+ const headers = toProxyHeaders(request.headers);
883
+ if (!headers["content-type"] && request.body !== void 0) {
884
+ headers["content-type"] = "application/json";
885
+ }
617
886
  const result = await proxyRequest(targetBaseUrl, request.url, {
618
887
  method: request.method,
619
- headers: {
620
- "content-type": request.headers["content-type"] ?? "application/json"
621
- },
622
- body: rawBody
888
+ headers,
889
+ body: toProxyBody(request)
623
890
  });
624
- detector.validate(
625
- route.method,
626
- route.path,
627
- result.statusCode,
628
- route.successResponse?.schema,
629
- result.body
630
- );
891
+ if (result.statusCode >= 200 && result.statusCode < 300) {
892
+ detector.validate(
893
+ route.method,
894
+ route.path,
895
+ result.statusCode,
896
+ route.successResponse?.schema,
897
+ result.body
898
+ );
899
+ }
631
900
  return {
632
901
  statusCode: result.statusCode,
633
902
  headers: Object.fromEntries(result.headers.entries()),
@@ -664,6 +933,16 @@ async function registerRoutes(app, document, config, store) {
664
933
  }
665
934
  });
666
935
  }
936
+ app.get(
937
+ "/__routes",
938
+ async () => routes.map((route) => ({
939
+ method: route.method.toUpperCase(),
940
+ path: route.fastifyPath,
941
+ operationId: route.operationId,
942
+ resource: route.resourceName,
943
+ hasDsl: Boolean(route.operation["x-mock-state"])
944
+ }))
945
+ );
667
946
  }
668
947
  async function createServer(config) {
669
948
  const app = Fastify({
@@ -679,7 +958,7 @@ async function createServer(config) {
679
958
  });
680
959
  const document = await loadOpenApiDocument(config.specPath);
681
960
  const seedCollections = inferSeedCollections(document);
682
- const dbPath = resolveFile(path4.dirname(config.specPath), config.dbPath);
961
+ const dbPath = resolveFile(path5.dirname(config.specPath), config.dbPath);
683
962
  const store = new JsonStateStore(dbPath);
684
963
  await store.initialize(seedCollections);
685
964
  app.get("/__health", async () => ({ status: "ok" }));
@@ -689,16 +968,39 @@ async function createServer(config) {
689
968
  }
690
969
 
691
970
  // src/cli.ts
971
+ function renderStartupBanner(config) {
972
+ const lines = [
973
+ `\u{1F680} Contract Drift Detection running at http://${config.host}:${config.port}`,
974
+ `- Spec: ${config.specPath}`,
975
+ `- DB: ${config.dbPath}`,
976
+ `- Mode: ${config.driftCheckTarget ? `proxy + drift-check (${config.driftCheckTarget})` : "stateful mock"}`,
977
+ "- Health: GET /__health",
978
+ "- Routes: GET /__routes"
979
+ ];
980
+ return `${lines.join("\n")}
981
+ `;
982
+ }
692
983
  async function main() {
693
984
  const cli = createCli();
694
985
  const serveCommand = cli.commands.find((command) => command.name() === "serve");
695
986
  const initCommand = cli.commands.find((command) => command.name() === "init");
696
- serveCommand?.action(async function() {
697
- const config = toServeConfig(this.opts());
987
+ const quickstartCommand = cli.commands.find((command) => command.name() === "quickstart");
988
+ const startServer = async (rawOptions) => {
989
+ const config = await resolveServeConfig(process.cwd(), rawOptions);
698
990
  const server = await createServer(config);
699
991
  await server.listen({ port: config.port, host: config.host });
700
- process.stdout.write(`Mock engine listening on http://${config.host}:${config.port}
701
- `);
992
+ process.stdout.write(renderStartupBanner(config));
993
+ };
994
+ serveCommand?.action(async function() {
995
+ await startServer(this.opts());
996
+ });
997
+ cli.action(async () => {
998
+ const options = cli.opts();
999
+ if (!options.spec && !options.specUrl && !options.discover) {
1000
+ cli.outputHelp();
1001
+ return;
1002
+ }
1003
+ await startServer(options);
702
1004
  });
703
1005
  initCommand?.action(async function() {
704
1006
  const options = this.opts();
@@ -706,11 +1008,35 @@ async function main() {
706
1008
  spec: String(options.spec),
707
1009
  db: String(options.db),
708
1010
  host: String(options.host),
709
- port: Number(options.port)
1011
+ port: Number(options.port),
1012
+ template: options.template === "none" ? "none" : "rest-crud"
710
1013
  });
711
1014
  process.stdout.write(`Created ${targetPath}
712
1015
  `);
713
1016
  });
1017
+ quickstartCommand?.action(async function() {
1018
+ const options = this.opts();
1019
+ const cwd = process.cwd();
1020
+ const specPath = String(options.spec ?? "openapi.yaml");
1021
+ await writeStarterConfig(cwd, {
1022
+ spec: specPath,
1023
+ db: String(options.db ?? ".mock-db.json"),
1024
+ host: String(options.host ?? "0.0.0.0"),
1025
+ port: Number(options.port ?? 4010),
1026
+ template: "rest-crud"
1027
+ });
1028
+ const config = await resolveServeConfig(cwd, {
1029
+ spec: specPath,
1030
+ db: String(options.db ?? ".mock-db.json"),
1031
+ host: String(options.host ?? "0.0.0.0"),
1032
+ port: String(options.port ?? 4010),
1033
+ corsOrigin: String(options.corsOrigin ?? "*"),
1034
+ verbose: Boolean(options.verbose)
1035
+ });
1036
+ const server = await createServer(config);
1037
+ await server.listen({ port: config.port, host: config.host });
1038
+ process.stdout.write(renderStartupBanner(config));
1039
+ });
714
1040
  await cli.parseAsync(process.argv);
715
1041
  }
716
1042
  await main().catch((error) => {