@arpixel/api-contract 0.1.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/cli.js ADDED
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { generateCommand } from "./commands/generate.js";
4
+ import { diffCommand } from "./commands/diff.js";
5
+ import { initCommand } from "./commands/init.js";
6
+ const program = new Command();
7
+ program
8
+ .name("api-contract")
9
+ .description("Understand OpenAPI contract changes and generate frontend-ready TypeScript.")
10
+ .version("0.1.0");
11
+ program
12
+ .command("generate")
13
+ .description("Generate TypeScript types and API method signatures from an OpenAPI file.")
14
+ .argument("<openapi-file>", "Path to OpenAPI YAML or JSON file")
15
+ .option("-o, --output <directory>", "Output directory")
16
+ .action(async (file, options) => {
17
+ try {
18
+ await generateCommand(file, options.output);
19
+ }
20
+ catch (error) {
21
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
22
+ process.exitCode = 1;
23
+ }
24
+ });
25
+ program
26
+ .command("diff")
27
+ .description("Compare two OpenAPI contracts.")
28
+ .argument("<old-file>", "Previous OpenAPI YAML or JSON file")
29
+ .argument("<new-file>", "New OpenAPI YAML or JSON file")
30
+ .option("-f, --format <format>", "Output format: text or json", "text")
31
+ .action(async (oldFile, newFile, options) => {
32
+ try {
33
+ if (options.format !== "text" && options.format !== "json") {
34
+ throw new Error(`Unsupported format "${options.format}". Use "text" or "json".`);
35
+ }
36
+ await diffCommand(oldFile, newFile, options.format);
37
+ }
38
+ catch (error) {
39
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
40
+ process.exitCode = 1;
41
+ }
42
+ });
43
+ program
44
+ .command("init")
45
+ .description("Install optional API Contract integrations.")
46
+ .option("--host <host>", "Integration host")
47
+ .action(async (options) => {
48
+ try {
49
+ await initCommand(options.host);
50
+ }
51
+ catch (error) {
52
+ console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
53
+ process.exitCode = 1;
54
+ }
55
+ });
56
+ await program.parseAsync();
@@ -0,0 +1 @@
1
+ export declare function diffCommand(oldPath: string, newPath: string, format: "text" | "json"): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import { loadOpenApi } from "../openapi/loader.js";
2
+ import { compareContracts } from "../diff/comparator.js";
3
+ import { formatDiff } from "../diff/reporter.js";
4
+ export async function diffCommand(oldPath, newPath, format) {
5
+ const [oldDocument, newDocument] = await Promise.all([
6
+ loadOpenApi(oldPath),
7
+ loadOpenApi(newPath)
8
+ ]);
9
+ const result = compareContracts(oldDocument, newDocument);
10
+ if (format === "json") {
11
+ console.log(JSON.stringify(result, null, 2));
12
+ return;
13
+ }
14
+ console.log(formatDiff(result));
15
+ }
@@ -0,0 +1 @@
1
+ export declare function generateCommand(inputPath: string, outputDir?: string): Promise<void>;
@@ -0,0 +1,19 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { generateApi } from "../generator/api.js";
4
+ import { generateTypes } from "../generator/typescript.js";
5
+ export async function generateCommand(inputPath, outputDir) {
6
+ const input = resolve(inputPath);
7
+ const output = outputDir
8
+ ? resolve(outputDir)
9
+ : resolve(process.cwd(), "api-contract/generated");
10
+ await mkdir(output, { recursive: true });
11
+ const typesPath = `${output}/types.ts`;
12
+ const apiPath = `${output}/api.ts`;
13
+ await generateTypes(input, typesPath);
14
+ await generateApi(input, apiPath);
15
+ console.log("Generated TypeScript API contract.");
16
+ console.log("\nOutput:");
17
+ console.log(` ${typesPath}`);
18
+ console.log(` ${apiPath}`);
19
+ }
@@ -0,0 +1 @@
1
+ export declare function initCommand(host: string | undefined, homeDirectory?: string): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { copyFile, mkdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ const integrationDirectory = join(dirname(fileURLToPath(import.meta.url)), "../../integrations/vscode");
6
+ const promptFiles = [
7
+ "api-contract-generate.prompt.md",
8
+ "api-contract-impact.prompt.md"
9
+ ];
10
+ const agentFiles = [
11
+ "api-contract-generate.agent.md",
12
+ "api-contract-impact.agent.md"
13
+ ];
14
+ export async function initCommand(host, homeDirectory = homedir()) {
15
+ if (!host) {
16
+ throw new Error('A host is required. Supported hosts: "vscode".');
17
+ }
18
+ if (host !== "vscode") {
19
+ throw new Error(`Unsupported host "${host}". Supported hosts: "vscode".`);
20
+ }
21
+ const promptDirectory = join(homeDirectory, ".api-contract", "prompts");
22
+ const agentDirectory = join(homeDirectory, ".copilot", "agents");
23
+ await mkdir(promptDirectory, { recursive: true });
24
+ await mkdir(agentDirectory, { recursive: true });
25
+ await Promise.all([
26
+ ...promptFiles.map(file => copyFile(join(integrationDirectory, file), join(promptDirectory, file))),
27
+ ...agentFiles.map(file => copyFile(join(integrationDirectory, file), join(agentDirectory, file)))
28
+ ]);
29
+ console.log("API Contract VS Code integration installed.");
30
+ console.log(`\nPrompts:\n ~/.api-contract/prompts/${promptFiles.join("\n ~/.api-contract/prompts/")}`);
31
+ console.log(`\nAgents:\n ~/.copilot/agents/${agentFiles.join("\n ~/.copilot/agents/")}`);
32
+ console.log(`\nPrompt usage options:\n\n1. Repository-local\n Copy prompts from:\n ~/.api-contract/prompts/\n to:\n .github/prompts/\n\n2. Global\n Add this directory to VS Code's chat.promptFilesLocations:\n ~/.api-contract/prompts\n\nThe API Contract agents are installed globally and can be selected from VS Code Chat.`);
33
+ }
@@ -0,0 +1,3 @@
1
+ import type { OpenApiDocument } from "../openapi/loader.js";
2
+ import type { DiffResult } from "../types/diff.js";
3
+ export declare function compareContracts(oldDocument: OpenApiDocument, newDocument: OpenApiDocument): DiffResult;
@@ -0,0 +1,416 @@
1
+ const METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
2
+ function asRecord(value) {
3
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
4
+ return undefined;
5
+ }
6
+ return value;
7
+ }
8
+ function asStringArray(value) {
9
+ if (!Array.isArray(value)) {
10
+ return [];
11
+ }
12
+ return value.filter((item) => typeof item === "string");
13
+ }
14
+ function schemaType(schema) {
15
+ const value = asRecord(schema);
16
+ if (!value)
17
+ return "unknown";
18
+ if (typeof value.$ref === "string")
19
+ return value.$ref;
20
+ if (value.type === "array")
21
+ return `array<${schemaType(value.items)}>`;
22
+ if (typeof value.type === "string")
23
+ return String(value.type);
24
+ if (Array.isArray(value.oneOf))
25
+ return `oneOf(${value.oneOf.map(item => schemaType(item)).join("|")})`;
26
+ if (Array.isArray(value.allOf))
27
+ return `allOf(${value.allOf.map(item => schemaType(item)).join("&")})`;
28
+ return "object";
29
+ }
30
+ function schemaLocation(name) {
31
+ return `components.schemas.${name}`;
32
+ }
33
+ function propertyLocation(name, property) {
34
+ return `${schemaLocation(name)}.properties.${property}`;
35
+ }
36
+ function operationLocation(path, method) {
37
+ return `paths.${path}.${method}`;
38
+ }
39
+ function changeLocation(path, method, suffix) {
40
+ return `${operationLocation(path, method)}.${suffix}`;
41
+ }
42
+ function compareOperationDetails(path, method, oldOperation, newOperation, changes) {
43
+ const oldParameters = Array.isArray(oldOperation.parameters)
44
+ ? oldOperation.parameters
45
+ .map(item => asRecord(item))
46
+ .filter((item) => Boolean(item))
47
+ : [];
48
+ const newParameters = Array.isArray(newOperation.parameters)
49
+ ? newOperation.parameters
50
+ .map(item => asRecord(item))
51
+ .filter((item) => Boolean(item))
52
+ : [];
53
+ const parameterKey = (parameter) => `${String(parameter.in ?? "unknown")}.${String(parameter.name ?? "unknown")}`;
54
+ const oldParameterMap = new Map();
55
+ for (const parameter of oldParameters) {
56
+ oldParameterMap.set(parameterKey(parameter), parameter);
57
+ }
58
+ const newParameterMap = new Map();
59
+ for (const parameter of newParameters) {
60
+ newParameterMap.set(parameterKey(parameter), parameter);
61
+ }
62
+ for (const key of new Set([...oldParameterMap.keys(), ...newParameterMap.keys()])) {
63
+ const oldParameter = oldParameterMap.get(key);
64
+ const newParameter = newParameterMap.get(key);
65
+ const location = changeLocation(path, method, `parameters.${key}`);
66
+ const parameterName = typeof newParameter?.name === "string"
67
+ ? newParameter.name
68
+ : typeof oldParameter?.name === "string"
69
+ ? oldParameter.name
70
+ : key;
71
+ if (!oldParameter) {
72
+ const required = Boolean(newParameter?.required);
73
+ changes.push({
74
+ type: "PARAMETER_ADDED",
75
+ path,
76
+ method: method.toUpperCase(),
77
+ parameter: parameterName,
78
+ location,
79
+ to: { required },
80
+ detail: `${method.toUpperCase()} ${path} parameter ${parameterName} was added${required ? " as required" : ""}.`,
81
+ breaking: required
82
+ });
83
+ continue;
84
+ }
85
+ if (!newParameter) {
86
+ changes.push({
87
+ type: "PARAMETER_REMOVED",
88
+ path,
89
+ method: method.toUpperCase(),
90
+ parameter: parameterName,
91
+ location,
92
+ from: { required: Boolean(oldParameter.required) },
93
+ detail: `${method.toUpperCase()} ${path} parameter ${parameterName} was removed.`,
94
+ breaking: true
95
+ });
96
+ continue;
97
+ }
98
+ const oldType = schemaType(oldParameter.schema);
99
+ const newType = schemaType(newParameter.schema);
100
+ if (oldType !== newType) {
101
+ changes.push({
102
+ type: "PARAMETER_TYPE_CHANGED",
103
+ path,
104
+ method: method.toUpperCase(),
105
+ parameter: parameterName,
106
+ location,
107
+ from: oldType,
108
+ to: newType,
109
+ detail: `${method.toUpperCase()} ${path} parameter ${parameterName} type changed from ${oldType} to ${newType}.`,
110
+ breaking: true
111
+ });
112
+ }
113
+ if (Boolean(oldParameter.required) !== Boolean(newParameter.required)) {
114
+ const from = Boolean(oldParameter.required) ? "required" : "optional";
115
+ const to = Boolean(newParameter.required) ? "required" : "optional";
116
+ changes.push({
117
+ type: "PARAMETER_REQUIRED_CHANGED",
118
+ path,
119
+ method: method.toUpperCase(),
120
+ parameter: parameterName,
121
+ location,
122
+ from,
123
+ to,
124
+ detail: `${method.toUpperCase()} ${path} parameter ${parameterName} changed from ${from} to ${to}.`,
125
+ breaking: !Boolean(oldParameter.required) && Boolean(newParameter.required)
126
+ });
127
+ }
128
+ }
129
+ const oldResponses = asRecord(oldOperation.responses) ?? {};
130
+ const newResponses = asRecord(newOperation.responses) ?? {};
131
+ for (const status of new Set([...Object.keys(oldResponses), ...Object.keys(newResponses)])) {
132
+ const oldResponse = asRecord(oldResponses[status]);
133
+ const newResponse = asRecord(newResponses[status]);
134
+ const location = changeLocation(path, method, `responses.${status}`);
135
+ const oldResponseContent = asRecord(oldResponse?.content);
136
+ const newResponseContent = asRecord(newResponse?.content);
137
+ const oldResponseJson = asRecord(oldResponseContent?.["application/json"]);
138
+ const newResponseJson = asRecord(newResponseContent?.["application/json"]);
139
+ const oldResponseType = schemaType(oldResponseJson?.schema);
140
+ const newResponseType = schemaType(newResponseJson?.schema);
141
+ if (!oldResponse) {
142
+ changes.push({
143
+ type: "RESPONSE_ADDED",
144
+ path,
145
+ method: method.toUpperCase(),
146
+ status,
147
+ location,
148
+ to: newResponseType,
149
+ detail: `${method.toUpperCase()} ${path} response ${status} was added.`,
150
+ breaking: false
151
+ });
152
+ }
153
+ else if (!newResponse) {
154
+ changes.push({
155
+ type: "RESPONSE_REMOVED",
156
+ path,
157
+ method: method.toUpperCase(),
158
+ status,
159
+ location,
160
+ from: oldResponseType,
161
+ detail: `${method.toUpperCase()} ${path} response ${status} was removed.`,
162
+ breaking: true
163
+ });
164
+ }
165
+ else if (oldResponseType !== newResponseType) {
166
+ changes.push({
167
+ type: "RESPONSE_CHANGED",
168
+ path,
169
+ method: method.toUpperCase(),
170
+ status,
171
+ location,
172
+ from: oldResponseType,
173
+ to: newResponseType,
174
+ detail: `${method.toUpperCase()} ${path} response ${status} changed from ${oldResponseType} to ${newResponseType}.`,
175
+ breaking: true
176
+ });
177
+ }
178
+ }
179
+ const oldRequestBody = asRecord(oldOperation.requestBody);
180
+ const newRequestBody = asRecord(newOperation.requestBody);
181
+ const requestBodyLocation = changeLocation(path, method, "requestBody");
182
+ const oldRequestBodyContent = asRecord(oldRequestBody?.content);
183
+ const newRequestBodyContent = asRecord(newRequestBody?.content);
184
+ const oldRequestBodyJson = asRecord(oldRequestBodyContent?.["application/json"]);
185
+ const newRequestBodyJson = asRecord(newRequestBodyContent?.["application/json"]);
186
+ const oldRequestBodyType = schemaType(oldRequestBodyJson?.schema);
187
+ const newRequestBodyType = schemaType(newRequestBodyJson?.schema);
188
+ if (!oldRequestBody && newRequestBody) {
189
+ changes.push({
190
+ type: "REQUEST_BODY_ADDED",
191
+ path,
192
+ method: method.toUpperCase(),
193
+ location: requestBodyLocation,
194
+ to: newRequestBodyType,
195
+ detail: `${method.toUpperCase()} ${path} request body was added.`,
196
+ breaking: Boolean(newRequestBody.required)
197
+ });
198
+ }
199
+ else if (oldRequestBody && !newRequestBody) {
200
+ changes.push({
201
+ type: "REQUEST_BODY_REMOVED",
202
+ path,
203
+ method: method.toUpperCase(),
204
+ location: requestBodyLocation,
205
+ from: oldRequestBodyType,
206
+ detail: `${method.toUpperCase()} ${path} request body was removed.`,
207
+ breaking: true
208
+ });
209
+ }
210
+ else if (oldRequestBody && newRequestBody && oldRequestBodyType !== newRequestBodyType) {
211
+ changes.push({
212
+ type: "REQUEST_BODY_CHANGED",
213
+ path,
214
+ method: method.toUpperCase(),
215
+ location: requestBodyLocation,
216
+ from: oldRequestBodyType,
217
+ to: newRequestBodyType,
218
+ detail: `${method.toUpperCase()} ${path} request body changed from ${oldRequestBodyType} to ${newRequestBodyType}.`,
219
+ breaking: true
220
+ });
221
+ }
222
+ }
223
+ function compareSchemas(oldSchemas, newSchemas, changes) {
224
+ const names = new Set([...Object.keys(oldSchemas), ...Object.keys(newSchemas)]);
225
+ for (const name of names) {
226
+ const oldSchema = oldSchemas[name];
227
+ const newSchema = newSchemas[name];
228
+ if (!oldSchema) {
229
+ changes.push({
230
+ type: "SCHEMA_ADDED",
231
+ schema: name,
232
+ location: schemaLocation(name),
233
+ detail: `Schema ${name} was added.`,
234
+ breaking: false
235
+ });
236
+ continue;
237
+ }
238
+ if (!newSchema) {
239
+ changes.push({
240
+ type: "SCHEMA_REMOVED",
241
+ schema: name,
242
+ location: schemaLocation(name),
243
+ detail: `Schema ${name} was removed.`,
244
+ breaking: true
245
+ });
246
+ continue;
247
+ }
248
+ const oldProps = asRecord(oldSchema.properties) ?? {};
249
+ const newProps = asRecord(newSchema.properties) ?? {};
250
+ const oldRequired = new Set(asStringArray(oldSchema.required));
251
+ const newRequired = new Set(asStringArray(newSchema.required));
252
+ const props = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);
253
+ for (const property of props) {
254
+ const oldProp = oldProps[property];
255
+ const newProp = newProps[property];
256
+ if (!oldProp) {
257
+ const required = newRequired.has(property);
258
+ changes.push({
259
+ type: "PROPERTY_ADDED",
260
+ schema: name,
261
+ property,
262
+ location: propertyLocation(name, property),
263
+ required,
264
+ detail: `${name}.${property} was added${required ? " as required" : ""}.`,
265
+ breaking: required
266
+ });
267
+ continue;
268
+ }
269
+ if (!newProp) {
270
+ changes.push({
271
+ type: "PROPERTY_REMOVED",
272
+ schema: name,
273
+ property,
274
+ location: propertyLocation(name, property),
275
+ from: oldRequired.has(property) ? "required" : "optional",
276
+ detail: `${name}.${property} was removed.`,
277
+ breaking: true
278
+ });
279
+ continue;
280
+ }
281
+ const oldType = schemaType(oldProp);
282
+ const newType = schemaType(newProp);
283
+ if (oldType !== newType) {
284
+ changes.push({
285
+ type: "PROPERTY_TYPE_CHANGED",
286
+ schema: name,
287
+ property,
288
+ location: propertyLocation(name, property),
289
+ from: oldType,
290
+ to: newType,
291
+ detail: `${name}.${property}: ${oldType} → ${newType}.`,
292
+ breaking: true
293
+ });
294
+ }
295
+ if (!oldRequired.has(property) && newRequired.has(property)) {
296
+ changes.push({
297
+ type: "PROPERTY_REQUIRED_CHANGED",
298
+ schema: name,
299
+ property,
300
+ location: propertyLocation(name, property),
301
+ from: "optional",
302
+ to: "required",
303
+ detail: `${name}.${property} changed from optional to required.`,
304
+ breaking: true
305
+ });
306
+ }
307
+ }
308
+ const oldEnum = asStringArray(oldSchema.enum);
309
+ const newEnum = asStringArray(newSchema.enum);
310
+ for (const value of oldEnum) {
311
+ if (!newEnum.includes(value)) {
312
+ changes.push({
313
+ type: "ENUM_VALUE_REMOVED",
314
+ schema: name,
315
+ location: `${schemaLocation(name)}.enum`,
316
+ from: value,
317
+ detail: `${name} enum value ${JSON.stringify(value)} was removed.`,
318
+ breaking: true
319
+ });
320
+ }
321
+ }
322
+ for (const value of newEnum) {
323
+ if (!oldEnum.includes(value)) {
324
+ changes.push({
325
+ type: "ENUM_VALUE_ADDED",
326
+ schema: name,
327
+ location: `${schemaLocation(name)}.enum`,
328
+ value,
329
+ detail: `${name} enum value ${JSON.stringify(value)} was added.`,
330
+ breaking: false
331
+ });
332
+ }
333
+ }
334
+ }
335
+ }
336
+ export function compareContracts(oldDocument, newDocument) {
337
+ const changes = [];
338
+ const oldPaths = oldDocument.paths ?? {};
339
+ const newPaths = newDocument.paths ?? {};
340
+ const paths = new Set([...Object.keys(oldPaths), ...Object.keys(newPaths)]);
341
+ for (const path of paths) {
342
+ const oldPath = oldPaths[path];
343
+ const newPath = newPaths[path];
344
+ if (!oldPath) {
345
+ for (const method of METHODS) {
346
+ if (newPath?.[method]) {
347
+ changes.push({
348
+ type: "ENDPOINT_ADDED",
349
+ path,
350
+ method: method.toUpperCase(),
351
+ location: operationLocation(path, method),
352
+ detail: `${method.toUpperCase()} ${path} was added.`,
353
+ breaking: false
354
+ });
355
+ }
356
+ }
357
+ continue;
358
+ }
359
+ if (!newPath) {
360
+ for (const method of METHODS) {
361
+ if (oldPath?.[method]) {
362
+ changes.push({
363
+ type: "ENDPOINT_REMOVED",
364
+ path,
365
+ method: method.toUpperCase(),
366
+ location: operationLocation(path, method),
367
+ detail: `${method.toUpperCase()} ${path} was removed.`,
368
+ breaking: true
369
+ });
370
+ }
371
+ }
372
+ continue;
373
+ }
374
+ for (const method of METHODS) {
375
+ const oldOperation = asRecord(oldPath?.[method]);
376
+ const newOperation = asRecord(newPath?.[method]);
377
+ if (!oldOperation && newOperation) {
378
+ changes.push({
379
+ type: "ENDPOINT_ADDED",
380
+ path,
381
+ method: method.toUpperCase(),
382
+ location: operationLocation(path, method),
383
+ detail: `${method.toUpperCase()} ${path} was added.`,
384
+ breaking: false
385
+ });
386
+ }
387
+ else if (oldOperation && !newOperation) {
388
+ changes.push({
389
+ type: "ENDPOINT_REMOVED",
390
+ path,
391
+ method: method.toUpperCase(),
392
+ location: operationLocation(path, method),
393
+ detail: `${method.toUpperCase()} ${path} was removed.`,
394
+ breaking: true
395
+ });
396
+ }
397
+ else if (oldOperation && newOperation) {
398
+ compareOperationDetails(path, method, oldOperation, newOperation, changes);
399
+ }
400
+ }
401
+ }
402
+ const oldComponents = asRecord(oldDocument.components) ?? {};
403
+ const newComponents = asRecord(newDocument.components) ?? {};
404
+ const oldSchemas = (asRecord(oldComponents.schemas) ?? {});
405
+ const newSchemas = (asRecord(newComponents.schemas) ?? {});
406
+ compareSchemas(oldSchemas, newSchemas, changes);
407
+ const added = changes.filter(c => c.type.endsWith("_ADDED")).length;
408
+ const removed = changes.filter(c => c.type.endsWith("_REMOVED")).length;
409
+ const modified = changes.filter(c => c.type.endsWith("_CHANGED") || c.type === "REQUEST_BODY_CHANGED" || c.type === "RESPONSE_CHANGED").length;
410
+ const breaking = changes.filter(c => c.breaking).length;
411
+ return {
412
+ version: "1",
413
+ summary: { added, removed, modified, breaking },
414
+ changes
415
+ };
416
+ }
@@ -0,0 +1,2 @@
1
+ import type { DiffResult } from "../types/diff.js";
2
+ export declare function formatDiff(result: DiffResult): string;
@@ -0,0 +1,32 @@
1
+ export function formatDiff(result) {
2
+ const lines = [
3
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
4
+ "API CONTRACT DIFF",
5
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
6
+ "",
7
+ "Summary",
8
+ ` Added: ${result.summary.added}`,
9
+ ` Removed: ${result.summary.removed}`,
10
+ ` Modified: ${result.summary.modified}`,
11
+ ` Breaking: ${result.summary.breaking}`,
12
+ ""
13
+ ];
14
+ const breaking = result.changes.filter(c => c.breaking);
15
+ const nonBreaking = result.changes.filter(c => !c.breaking);
16
+ if (breaking.length) {
17
+ lines.push("BREAKING CHANGES", "──────────────────────────────────────", "");
18
+ for (const change of breaking) {
19
+ lines.push(`⚠ ${change.detail}`, "");
20
+ }
21
+ }
22
+ if (nonBreaking.length) {
23
+ lines.push("NON-BREAKING CHANGES", "──────────────────────────────────────", "");
24
+ for (const change of nonBreaking) {
25
+ lines.push(`+ ${change.detail}`, "");
26
+ }
27
+ }
28
+ if (!result.changes.length) {
29
+ lines.push("No contract changes detected.", "");
30
+ }
31
+ return lines.join("\n");
32
+ }
@@ -0,0 +1 @@
1
+ export declare function generateApi(inputPath: string, outputPath: string, typesImport?: string): Promise<void>;