@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.
@@ -0,0 +1,106 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { loadOpenApi } from "../openapi/loader.js";
4
+ const METHODS = [
5
+ "get", "post", "put", "patch", "delete", "head", "options"
6
+ ];
7
+ function pascalCase(value) {
8
+ return value
9
+ .replace(/[^a-zA-Z0-9]+(.)?/g, (_, c) => c ? c.toUpperCase() : "")
10
+ .replace(/^[a-z]/, c => c.toUpperCase());
11
+ }
12
+ function methodName(path, method, operationId) {
13
+ if (operationId)
14
+ return operationId;
15
+ const parts = path
16
+ .split("/")
17
+ .filter(Boolean)
18
+ .map(part => part.startsWith("{") ? `By${pascalCase(part.slice(1, -1))}` : pascalCase(part));
19
+ return `${method}${parts.join("") || "Root"}`;
20
+ }
21
+ function schemaType(refOrSchema) {
22
+ if (!refOrSchema || typeof refOrSchema !== "object")
23
+ return "unknown";
24
+ const schema = refOrSchema;
25
+ if (typeof schema.$ref === "string") {
26
+ const match = schema.$ref.match(/^#\/components\/schemas\/(.+)$/);
27
+ if (match)
28
+ return match[1];
29
+ }
30
+ if (typeof schema.type === "string") {
31
+ switch (schema.type) {
32
+ case "string": return "string";
33
+ case "integer":
34
+ case "number": return "number";
35
+ case "boolean": return "boolean";
36
+ case "array": return `${schemaType(schema.items)}[]`;
37
+ case "object": return "Record<string, unknown>";
38
+ }
39
+ }
40
+ return "unknown";
41
+ }
42
+ function responseType(operation) {
43
+ const responses = operation.responses;
44
+ if (!responses)
45
+ return "unknown";
46
+ const preferred = ["200", "201", "202", "204", "default"];
47
+ const status = preferred.find(code => responses[code]) ?? Object.keys(responses)[0];
48
+ if (!status)
49
+ return "unknown";
50
+ const response = responses[status];
51
+ if (status === "204")
52
+ return "void";
53
+ const content = response.content;
54
+ if (!content)
55
+ return "void";
56
+ const media = content["application/json"] ?? content[Object.keys(content)[0]];
57
+ const schema = media?.schema;
58
+ return schemaType(schema);
59
+ }
60
+ function parameterType(parameter) {
61
+ return schemaType(parameter.schema);
62
+ }
63
+ export async function generateApi(inputPath, outputPath, typesImport = "./types.js") {
64
+ const document = await loadOpenApi(inputPath);
65
+ const paths = document.paths ?? {};
66
+ const lines = [
67
+ "/* eslint-disable */",
68
+ "// Generated by api-contract. Do not edit manually.",
69
+ `import type * as ApiTypes from "${typesImport}";`,
70
+ "",
71
+ "export interface ApiMethods {"
72
+ ];
73
+ for (const [path, rawPathItem] of Object.entries(paths)) {
74
+ if (!rawPathItem || typeof rawPathItem !== "object")
75
+ continue;
76
+ const pathItem = rawPathItem;
77
+ const pathParameters = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];
78
+ for (const method of METHODS) {
79
+ const rawOperation = pathItem[method];
80
+ if (!rawOperation || typeof rawOperation !== "object")
81
+ continue;
82
+ const operation = rawOperation;
83
+ const name = methodName(path, method, typeof operation.operationId === "string" ? operation.operationId : undefined);
84
+ const parameters = [
85
+ ...pathParameters,
86
+ ...(Array.isArray(operation.parameters) ? operation.parameters : [])
87
+ ];
88
+ const args = parameters
89
+ .filter(p => typeof p === "object")
90
+ .map(p => {
91
+ const param = p;
92
+ const paramName = String(param.name ?? "param");
93
+ const required = Boolean(param.required);
94
+ const type = parameterType(param);
95
+ return `${paramName}${required ? "" : "?"}: ${type}`;
96
+ });
97
+ const result = responseType(operation);
98
+ lines.push(` ${name}(${args.join(", ")}): Promise<${result}>;`);
99
+ }
100
+ }
101
+ lines.push("}", "");
102
+ lines.push("// ApiTypes is imported to keep the generated contract connected to the generated OpenAPI types.");
103
+ lines.push("export type OpenApiTypes = typeof ApiTypes;");
104
+ await mkdir(dirname(outputPath), { recursive: true });
105
+ await writeFile(outputPath, lines.join("\n"), "utf8");
106
+ }
@@ -0,0 +1 @@
1
+ export declare function generateTypes(inputPath: string, outputPath: string): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import openapiTS, { astToString } from "openapi-typescript";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { loadOpenApi } from "../openapi/loader.js";
5
+ export async function generateTypes(inputPath, outputPath) {
6
+ const document = await loadOpenApi(inputPath);
7
+ const ast = await openapiTS(document, {
8
+ alphabetize: true,
9
+ enum: true
10
+ });
11
+ const output = astToString(ast);
12
+ await mkdir(dirname(outputPath), { recursive: true });
13
+ await writeFile(outputPath, output, "utf8");
14
+ }
@@ -0,0 +1,2 @@
1
+ export type OpenApiDocument = Record<string, unknown>;
2
+ export declare function loadOpenApi(filePath: string): Promise<OpenApiDocument>;
@@ -0,0 +1,4 @@
1
+ import SwaggerParser from "@apidevtools/swagger-parser";
2
+ export async function loadOpenApi(filePath) {
3
+ return SwaggerParser.validate(filePath);
4
+ }
@@ -0,0 +1,161 @@
1
+ export type ChangeType = "ENDPOINT_ADDED" | "ENDPOINT_REMOVED" | "ENDPOINT_MODIFIED" | "PARAMETER_ADDED" | "PARAMETER_REMOVED" | "PARAMETER_TYPE_CHANGED" | "PARAMETER_REQUIRED_CHANGED" | "SCHEMA_ADDED" | "SCHEMA_REMOVED" | "SCHEMA_TYPE_CHANGED" | "PROPERTY_ADDED" | "PROPERTY_REMOVED" | "PROPERTY_TYPE_CHANGED" | "PROPERTY_REQUIRED_CHANGED" | "ENUM_VALUE_ADDED" | "ENUM_VALUE_REMOVED" | "REQUEST_BODY_ADDED" | "REQUEST_BODY_REMOVED" | "REQUEST_BODY_CHANGED" | "RESPONSE_ADDED" | "RESPONSE_REMOVED" | "RESPONSE_CHANGED";
2
+ export interface DiffSummary {
3
+ added: number;
4
+ removed: number;
5
+ modified: number;
6
+ breaking: number;
7
+ }
8
+ export interface EndpointChangeBase {
9
+ location: string;
10
+ path: string;
11
+ method: string;
12
+ detail: string;
13
+ breaking: boolean;
14
+ }
15
+ export interface EndpointAddedChange extends EndpointChangeBase {
16
+ type: "ENDPOINT_ADDED";
17
+ }
18
+ export interface EndpointRemovedChange extends EndpointChangeBase {
19
+ type: "ENDPOINT_REMOVED";
20
+ }
21
+ export interface EndpointModifiedChange extends EndpointChangeBase {
22
+ type: "ENDPOINT_MODIFIED";
23
+ from: unknown;
24
+ to: unknown;
25
+ }
26
+ export interface ParameterChangeBase extends EndpointChangeBase {
27
+ parameter: string;
28
+ }
29
+ export interface ParameterAddedChange extends ParameterChangeBase {
30
+ type: "PARAMETER_ADDED";
31
+ to: {
32
+ required: boolean;
33
+ };
34
+ }
35
+ export interface ParameterRemovedChange extends ParameterChangeBase {
36
+ type: "PARAMETER_REMOVED";
37
+ from: {
38
+ required: boolean;
39
+ };
40
+ }
41
+ export interface ParameterTypeChangedChange extends ParameterChangeBase {
42
+ type: "PARAMETER_TYPE_CHANGED";
43
+ from: string;
44
+ to: string;
45
+ }
46
+ export interface ParameterRequiredChangedChange extends ParameterChangeBase {
47
+ type: "PARAMETER_REQUIRED_CHANGED";
48
+ from: "required" | "optional";
49
+ to: "required" | "optional";
50
+ }
51
+ export interface SchemaAddedChange {
52
+ type: "SCHEMA_ADDED";
53
+ schema: string;
54
+ location: string;
55
+ detail: string;
56
+ breaking: boolean;
57
+ }
58
+ export interface SchemaRemovedChange {
59
+ type: "SCHEMA_REMOVED";
60
+ schema: string;
61
+ location: string;
62
+ detail: string;
63
+ breaking: boolean;
64
+ }
65
+ export interface SchemaTypeChangedChange {
66
+ type: "SCHEMA_TYPE_CHANGED";
67
+ schema: string;
68
+ location: string;
69
+ from: string;
70
+ to: string;
71
+ detail: string;
72
+ breaking: boolean;
73
+ }
74
+ export interface PropertyChangeBase {
75
+ schema: string;
76
+ property: string;
77
+ location: string;
78
+ detail: string;
79
+ breaking: boolean;
80
+ }
81
+ export interface PropertyAddedChange extends PropertyChangeBase {
82
+ type: "PROPERTY_ADDED";
83
+ required: boolean;
84
+ }
85
+ export interface PropertyRemovedChange extends PropertyChangeBase {
86
+ type: "PROPERTY_REMOVED";
87
+ from: "required" | "optional";
88
+ }
89
+ export interface PropertyTypeChangedChange extends PropertyChangeBase {
90
+ type: "PROPERTY_TYPE_CHANGED";
91
+ from: string;
92
+ to: string;
93
+ }
94
+ export interface PropertyRequiredChangedChange extends PropertyChangeBase {
95
+ type: "PROPERTY_REQUIRED_CHANGED";
96
+ from: "required" | "optional";
97
+ to: "required" | "optional";
98
+ }
99
+ export interface EnumValueAddedChange {
100
+ type: "ENUM_VALUE_ADDED";
101
+ schema: string;
102
+ location: string;
103
+ value: unknown;
104
+ detail: string;
105
+ breaking: boolean;
106
+ }
107
+ export interface EnumValueRemovedChange {
108
+ type: "ENUM_VALUE_REMOVED";
109
+ schema: string;
110
+ location: string;
111
+ from: unknown;
112
+ detail: string;
113
+ breaking: boolean;
114
+ }
115
+ export interface RequestBodyChangeBase {
116
+ path: string;
117
+ method: string;
118
+ location: string;
119
+ detail: string;
120
+ breaking: boolean;
121
+ }
122
+ export interface RequestBodyAddedChange extends RequestBodyChangeBase {
123
+ type: "REQUEST_BODY_ADDED";
124
+ to: string;
125
+ }
126
+ export interface RequestBodyRemovedChange extends RequestBodyChangeBase {
127
+ type: "REQUEST_BODY_REMOVED";
128
+ from: string;
129
+ }
130
+ export interface RequestBodyChangedChange extends RequestBodyChangeBase {
131
+ type: "REQUEST_BODY_CHANGED";
132
+ from: string;
133
+ to: string;
134
+ }
135
+ export interface ResponseChangeBase {
136
+ path: string;
137
+ method: string;
138
+ status: string;
139
+ location: string;
140
+ detail: string;
141
+ breaking: boolean;
142
+ }
143
+ export interface ResponseAddedChange extends ResponseChangeBase {
144
+ type: "RESPONSE_ADDED";
145
+ to: string;
146
+ }
147
+ export interface ResponseRemovedChange extends ResponseChangeBase {
148
+ type: "RESPONSE_REMOVED";
149
+ from: string;
150
+ }
151
+ export interface ResponseChangedChange extends ResponseChangeBase {
152
+ type: "RESPONSE_CHANGED";
153
+ from: string;
154
+ to: string;
155
+ }
156
+ export type ContractChange = EndpointAddedChange | EndpointRemovedChange | EndpointModifiedChange | ParameterAddedChange | ParameterRemovedChange | ParameterTypeChangedChange | ParameterRequiredChangedChange | SchemaAddedChange | SchemaRemovedChange | SchemaTypeChangedChange | PropertyAddedChange | PropertyRemovedChange | PropertyTypeChangedChange | PropertyRequiredChangedChange | EnumValueAddedChange | EnumValueRemovedChange | RequestBodyAddedChange | RequestBodyRemovedChange | RequestBodyChangedChange | ResponseAddedChange | ResponseRemovedChange | ResponseChangedChange;
157
+ export interface DiffResult {
158
+ version: "1";
159
+ summary: DiffSummary;
160
+ changes: ContractChange[];
161
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,161 @@
1
+ ---
2
+
3
+ name: API Contract Generate
4
+ description: Generate TypeScript API contract files from an OpenAPI specification using the api-contract CLI.
5
+ -------------------------------------------------------------------------------------------------------------
6
+
7
+ # API Contract Generate Agent
8
+
9
+ You generate TypeScript API contract files from an OpenAPI specification using the `api-contract` CLI.
10
+
11
+ ## Responsibility
12
+
13
+ Your job is to:
14
+
15
+ 1. Identify the user's OpenAPI input.
16
+ 2. Identify the user's requested output directory, if provided.
17
+ 3. Run the deterministic `api-contract generate` command.
18
+ 4. Verify the generated files.
19
+ 5. Report the result.
20
+
21
+ The `api-contract` CLI owns the actual generation logic.
22
+
23
+ Do not manually generate TypeScript from OpenAPI.
24
+
25
+ ---
26
+
27
+ ## Input
28
+
29
+ The user may provide:
30
+
31
+ * an attached OpenAPI file
32
+ * a repository OpenAPI file
33
+ * an explicit OpenAPI path
34
+
35
+ Example:
36
+
37
+ > Generate types for this openapi.yaml file and output to `src/api/generated-types`.
38
+
39
+ Use the supplied OpenAPI file.
40
+
41
+ ---
42
+
43
+ ## Output Directory
44
+
45
+ If the user specifies an output directory:
46
+
47
+ ```text
48
+ <user-specified-directory>
49
+ ```
50
+
51
+ use it exactly.
52
+
53
+ Example:
54
+
55
+ ```text
56
+ src/api/generated-types
57
+ ```
58
+
59
+ Run:
60
+
61
+ ```bash
62
+ api-contract generate <openapi-file> --output src/api/generated-types
63
+ ```
64
+
65
+ If no output directory is specified, invoke the CLI without an output option:
66
+
67
+ ```bash
68
+ api-contract generate <openapi-file>
69
+ ```
70
+
71
+ The CLI resolves the default output relative to the current working directory:
72
+
73
+ ```text
74
+ api-contract/generated/
75
+ ├── types.ts
76
+ └── api.ts
77
+ ```
78
+
79
+ Do not manually add `--output api-contract/generated` in this case.
80
+
81
+ ---
82
+
83
+ ## Execution
84
+
85
+ When the user specifies an output directory, use:
86
+
87
+ ```bash
88
+ api-contract generate <openapi-file> --output <output-directory>
89
+ ```
90
+
91
+ When no output directory is specified, use:
92
+
93
+ ```bash
94
+ api-contract generate <openapi-file>
95
+ ```
96
+
97
+ The CLI is responsible for:
98
+
99
+ * OpenAPI parsing
100
+ * validation
101
+ * TypeScript generation
102
+ * writing generated files
103
+
104
+ Do not reproduce or modify this logic in the agent.
105
+
106
+ ---
107
+
108
+ ## Verification
109
+
110
+ After generation, verify that the output contains:
111
+
112
+ ```text
113
+ types.ts
114
+ api.ts
115
+ ```
116
+
117
+ Confirm that the generated files contain TypeScript output corresponding to the supplied OpenAPI specification.
118
+
119
+ Do not modify generated files manually.
120
+
121
+ ---
122
+
123
+ ## Modification Policy
124
+
125
+ This agent is allowed to create or replace generated contract files because generation is its explicit purpose.
126
+
127
+ It must not:
128
+
129
+ * modify handwritten application code
130
+ * modify unrelated files
131
+ * silently choose a different output directory
132
+ * manually edit generated output
133
+
134
+ If the user explicitly requests application-code integration after generation, that should be treated as a separate task.
135
+
136
+ ---
137
+
138
+ ## Response
139
+
140
+ Return:
141
+
142
+ # API Contract Generation
143
+
144
+ ## Input
145
+
146
+ * OpenAPI: `<path>`
147
+
148
+ ## Output
149
+
150
+ * Directory: `<path>`
151
+
152
+ ## Generated
153
+
154
+ * `<path>/types.ts`
155
+ * `<path>/api.ts`
156
+
157
+ ## Result
158
+
159
+ Briefly state whether generation succeeded.
160
+
161
+ Keep the response concise.
@@ -0,0 +1,172 @@
1
+ ---
2
+
3
+ description: Generate TypeScript API contract files from an OpenAPI specification
4
+ agent: agent
5
+ ------------
6
+
7
+ # API Contract Generate
8
+
9
+ Generate TypeScript API contract files from an OpenAPI specification using the `api-contract` CLI.
10
+
11
+ The user may provide:
12
+
13
+ * an OpenAPI file
14
+ * an output directory
15
+
16
+ ## Core Principle
17
+
18
+ The `api-contract` CLI performs the actual OpenAPI-to-TypeScript generation.
19
+
20
+ The agent is responsible for:
21
+
22
+ * identifying the OpenAPI input
23
+ * determining the requested output location
24
+ * invoking `api-contract generate`
25
+ * verifying the generated files
26
+ * reporting the result
27
+
28
+ Do not manually generate TypeScript from the OpenAPI specification.
29
+
30
+ ---
31
+
32
+ ## Input
33
+
34
+ The user may provide an OpenAPI file and an optional output directory.
35
+
36
+ Example:
37
+
38
+ ```text
39
+ Generate types for this openapi.yaml file and output to src/api/generated-types.
40
+ ```
41
+
42
+ The OpenAPI file may be:
43
+
44
+ * a repository file
45
+ * an attached file
46
+ * an explicitly provided path
47
+
48
+ ---
49
+
50
+ ## Output Location
51
+
52
+ ### User-specified output
53
+
54
+ If the user specifies an output directory, use that directory exactly.
55
+
56
+ Example:
57
+
58
+ ```text
59
+ src/api/generated-types
60
+ ```
61
+
62
+ Run:
63
+
64
+ ```bash
65
+ api-contract generate <openapi-file> --output src/api/generated-types
66
+ ```
67
+
68
+ Do not substitute another directory unless the requested location is invalid or inaccessible.
69
+
70
+ ### Default output
71
+
72
+ If the user does not specify an output directory, invoke the CLI without an output option:
73
+
74
+ ```bash
75
+ api-contract generate <openapi-file>
76
+ ```
77
+
78
+ The CLI resolves the default output relative to the current working directory:
79
+
80
+ ```text
81
+ api-contract/generated/
82
+ ├── types.ts
83
+ └── api.ts
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Workflow
89
+
90
+ ### 1. Identify the OpenAPI file
91
+
92
+ Determine the OpenAPI specification from the user's request.
93
+
94
+ If the user attached a file, use that file.
95
+
96
+ If the user provides a repository path, use that path.
97
+
98
+ If multiple possible OpenAPI files exist and the intended file cannot be determined, ask the user rather than guessing.
99
+
100
+ ### 2. Determine the output directory
101
+
102
+ Use the user-specified output directory when provided.
103
+
104
+ Otherwise let the CLI determine the default output location by omitting `--output`.
105
+
106
+ ### 3. Generate
107
+
108
+ If the user specified an output directory, run:
109
+
110
+ ```bash
111
+ api-contract generate <openapi-file> --output <output-directory>
112
+ ```
113
+
114
+ Otherwise run:
115
+
116
+ ```bash
117
+ api-contract generate <openapi-file>
118
+ ```
119
+
120
+ Do not manually reproduce the generation logic.
121
+
122
+ ### 4. Verify
123
+
124
+ After generation, verify that the expected files exist:
125
+
126
+ ```text
127
+ types.ts
128
+ api.ts
129
+ ```
130
+
131
+ Inspect the generated output enough to confirm that it contains TypeScript generated from the supplied OpenAPI specification.
132
+
133
+ ### 5. Handle existing output
134
+
135
+ If the output directory already contains generated files, the CLI may replace them.
136
+
137
+ Do not modify unrelated files.
138
+
139
+ Do not modify handwritten application code.
140
+
141
+ ### 6. Report
142
+
143
+ Provide:
144
+
145
+ #### Generation Summary
146
+
147
+ * OpenAPI input
148
+ * output directory
149
+ * command executed
150
+ * generated files
151
+
152
+ #### Result
153
+
154
+ Confirm whether generation succeeded.
155
+
156
+ #### Generated Files
157
+
158
+ List the generated files using repository-relative paths where possible.
159
+
160
+ ---
161
+
162
+ ## Important Rules
163
+
164
+ * Always use `api-contract generate`.
165
+ * Never manually generate the TypeScript output.
166
+ * Respect a user-specified output directory exactly.
167
+ * Let the CLI default to `api-contract/generated/` relative to the current working directory when no output directory is provided.
168
+ * Do not modify application code.
169
+ * Do not modify unrelated files.
170
+ * Do not introduce an LLM/API dependency into `api-contract`.
171
+ * If the OpenAPI input cannot be identified, ask the user.
172
+ * If the requested output location cannot be used, explain the problem instead of silently choosing another location.