openpets 1.0.11 → 1.0.12
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/data/api.json +3758 -7222
- package/dist/src/core/build-pet.d.ts.map +1 -1
- package/dist/src/core/build-pet.js +7 -0
- package/dist/src/core/build-pet.js.map +1 -1
- package/dist/src/core/cli.js +456 -130
- package/dist/src/core/cli.js.map +1 -1
- package/dist/src/core/ensure-npmignore.d.ts +30 -0
- package/dist/src/core/ensure-npmignore.d.ts.map +1 -0
- package/dist/src/core/ensure-npmignore.js +121 -0
- package/dist/src/core/ensure-npmignore.js.map +1 -0
- package/dist/src/core/index.d.ts +6 -3
- package/dist/src/core/index.d.ts.map +1 -1
- package/dist/src/core/index.js +9 -3
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/core/mcp-generator.d.ts +56 -0
- package/dist/src/core/mcp-generator.d.ts.map +1 -0
- package/dist/src/core/mcp-generator.js +1438 -0
- package/dist/src/core/mcp-generator.js.map +1 -0
- package/dist/src/core/mcp-server.js +0 -0
- package/dist/src/core/openapi-generator.d.ts +59 -0
- package/dist/src/core/openapi-generator.d.ts.map +1 -0
- package/dist/src/core/openapi-generator.js +800 -0
- package/dist/src/core/openapi-generator.js.map +1 -0
- package/dist/src/core/pet-config.d.ts +107 -49
- package/dist/src/core/pet-config.d.ts.map +1 -1
- package/dist/src/core/pet-config.js +6 -4
- package/dist/src/core/pet-config.js.map +1 -1
- package/dist/src/core/pet-downloader.d.ts +16 -0
- package/dist/src/core/pet-downloader.d.ts.map +1 -1
- package/dist/src/core/pet-downloader.js +145 -3
- package/dist/src/core/pet-downloader.js.map +1 -1
- package/dist/src/core/publish-pet.d.ts +29 -0
- package/dist/src/core/publish-pet.d.ts.map +1 -0
- package/dist/src/core/publish-pet.js +372 -0
- package/dist/src/core/publish-pet.js.map +1 -0
- package/dist/src/core/sdk-generator.d.ts +92 -0
- package/dist/src/core/sdk-generator.d.ts.map +1 -0
- package/dist/src/core/sdk-generator.js +567 -0
- package/dist/src/core/sdk-generator.js.map +1 -0
- package/dist/src/core/search-pets.d.ts +5 -0
- package/dist/src/core/search-pets.d.ts.map +1 -1
- package/dist/src/core/search-pets.js +43 -0
- package/dist/src/core/search-pets.js.map +1 -1
- package/dist/src/core/security-scanner.d.ts +49 -0
- package/dist/src/core/security-scanner.d.ts.map +1 -0
- package/dist/src/core/security-scanner.js +255 -0
- package/dist/src/core/security-scanner.js.map +1 -0
- package/dist/src/core/tool-lister.d.ts +61 -0
- package/dist/src/core/tool-lister.d.ts.map +1 -0
- package/dist/src/core/tool-lister.js +333 -0
- package/dist/src/core/tool-lister.js.map +1 -0
- package/dist/src/core/validate-pet.d.ts +2 -0
- package/dist/src/core/validate-pet.d.ts.map +1 -1
- package/dist/src/core/validate-pet.js +93 -1
- package/dist/src/core/validate-pet.js.map +1 -1
- package/dist/src/sdk/plugin-factory.d.ts +86 -0
- package/dist/src/sdk/plugin-factory.d.ts.map +1 -1
- package/dist/src/sdk/plugin-factory.js +450 -53
- package/dist/src/sdk/plugin-factory.js.map +1 -1
- package/dist/src/sdk/prompts-manager.d.ts +6 -0
- package/dist/src/sdk/prompts-manager.d.ts.map +1 -0
- package/dist/src/sdk/prompts-manager.js +162 -0
- package/dist/src/sdk/prompts-manager.js.map +1 -0
- package/package.json +1 -1
- package/dist/src/core/local-cache.d.ts +0 -69
- package/dist/src/core/local-cache.d.ts.map +0 -1
- package/dist/src/core/local-cache.js +0 -212
- package/dist/src/core/local-cache.js.map +0 -1
- package/dist/src/core/plugin-factory.d.ts +0 -58
- package/dist/src/core/plugin-factory.d.ts.map +0 -1
- package/dist/src/core/plugin-factory.js +0 -212
- package/dist/src/core/plugin-factory.js.map +0 -1
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI Tool Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates OpenPets-compatible tool definitions from OpenAPI/Swagger specifications.
|
|
5
|
+
* This is the highest-level way to create tools for API-first products.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* pets generate-openapi --url https://api.example.com/openapi.json
|
|
9
|
+
* pets generate-openapi --file ./openapi.json
|
|
10
|
+
* pets generate-openapi # Uses openapiSpec.url from package.json
|
|
11
|
+
*
|
|
12
|
+
* This tool:
|
|
13
|
+
* 1. Fetches or reads an OpenAPI spec (v3.0 or v3.1)
|
|
14
|
+
* 2. Parses endpoints into tool definitions
|
|
15
|
+
* 3. Generates Zod schemas from JSON Schema
|
|
16
|
+
* 4. Creates an openapi-client.ts file with all tools
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
19
|
+
import { resolve, basename } from "path";
|
|
20
|
+
import { config as loadDotenv } from "dotenv";
|
|
21
|
+
import { createLogger } from "./logger";
|
|
22
|
+
const logger = createLogger("openapi-generator");
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Config Loading
|
|
25
|
+
// ============================================================================
|
|
26
|
+
function loadConfigFromPackageJson(dir) {
|
|
27
|
+
const packageJsonPath = resolve(dir, "package.json");
|
|
28
|
+
if (!existsSync(packageJsonPath)) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
33
|
+
// Check for explicit openapiSpec config
|
|
34
|
+
if (packageJson.openapiSpec) {
|
|
35
|
+
const petName = packageJson.name?.replace(/^@openpets\//, "") || basename(dir);
|
|
36
|
+
return {
|
|
37
|
+
config: packageJson.openapiSpec,
|
|
38
|
+
petName,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Check for openapi-config.json in the pet directory
|
|
42
|
+
const configPath = resolve(dir, "openapi-config.json");
|
|
43
|
+
if (existsSync(configPath)) {
|
|
44
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
45
|
+
const petName = packageJson.name?.replace(/^@openpets\//, "") || basename(dir);
|
|
46
|
+
return {
|
|
47
|
+
config: config,
|
|
48
|
+
petName,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
logger.error(`Error reading package.json: ${error.message}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// OpenAPI Spec Fetching
|
|
60
|
+
// ============================================================================
|
|
61
|
+
async function fetchOpenAPISpec(url) {
|
|
62
|
+
const response = await fetch(url);
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`Failed to fetch OpenAPI spec: ${response.status} ${response.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
return response.json();
|
|
67
|
+
}
|
|
68
|
+
function readOpenAPISpec(filePath) {
|
|
69
|
+
const content = readFileSync(filePath, "utf-8");
|
|
70
|
+
return JSON.parse(content);
|
|
71
|
+
}
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Schema Resolution
|
|
74
|
+
// ============================================================================
|
|
75
|
+
function resolveRef(spec, ref) {
|
|
76
|
+
// Handle $ref like "#/components/schemas/UserLogin"
|
|
77
|
+
const path = ref.replace(/^#\//, "").split("/");
|
|
78
|
+
let current = spec;
|
|
79
|
+
for (const segment of path) {
|
|
80
|
+
current = current[segment];
|
|
81
|
+
if (!current) {
|
|
82
|
+
logger.warn(`Could not resolve ref: ${ref}`);
|
|
83
|
+
return { type: "object" };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return current;
|
|
87
|
+
}
|
|
88
|
+
function resolveSchema(spec, schema) {
|
|
89
|
+
if (!schema)
|
|
90
|
+
return { type: "object" };
|
|
91
|
+
if (schema.$ref) {
|
|
92
|
+
return resolveSchema(spec, resolveRef(spec, schema.$ref));
|
|
93
|
+
}
|
|
94
|
+
// Handle allOf by merging schemas
|
|
95
|
+
if (schema.allOf) {
|
|
96
|
+
const merged = { type: "object", properties: {}, required: [] };
|
|
97
|
+
for (const subSchema of schema.allOf) {
|
|
98
|
+
const resolved = resolveSchema(spec, subSchema);
|
|
99
|
+
if (resolved.properties) {
|
|
100
|
+
merged.properties = { ...merged.properties, ...resolved.properties };
|
|
101
|
+
}
|
|
102
|
+
if (resolved.required) {
|
|
103
|
+
merged.required = [...(merged.required || []), ...resolved.required];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return merged;
|
|
107
|
+
}
|
|
108
|
+
return schema;
|
|
109
|
+
}
|
|
110
|
+
// ============================================================================
|
|
111
|
+
// Zod Schema Generation
|
|
112
|
+
// ============================================================================
|
|
113
|
+
function schemaToZod(spec, schema, depth = 0) {
|
|
114
|
+
if (!schema)
|
|
115
|
+
return "z.any()";
|
|
116
|
+
// Resolve refs
|
|
117
|
+
const resolved = resolveSchema(spec, schema);
|
|
118
|
+
const desc = resolved.description
|
|
119
|
+
? `.describe(${JSON.stringify(resolved.description)})`
|
|
120
|
+
: "";
|
|
121
|
+
// Handle anyOf (nullable/union types)
|
|
122
|
+
if (resolved.anyOf && Array.isArray(resolved.anyOf)) {
|
|
123
|
+
const nonNullTypes = resolved.anyOf.filter(s => s.type !== "null");
|
|
124
|
+
if (nonNullTypes.length === 1) {
|
|
125
|
+
return schemaToZod(spec, { ...nonNullTypes[0], description: resolved.description }, depth);
|
|
126
|
+
}
|
|
127
|
+
// Multiple types - use any with description
|
|
128
|
+
return `z.any()${desc}`;
|
|
129
|
+
}
|
|
130
|
+
// Handle oneOf similarly
|
|
131
|
+
if (resolved.oneOf && Array.isArray(resolved.oneOf)) {
|
|
132
|
+
const types = resolved.oneOf.filter(s => s.type !== "null");
|
|
133
|
+
if (types.length === 1) {
|
|
134
|
+
return schemaToZod(spec, { ...types[0], description: resolved.description }, depth);
|
|
135
|
+
}
|
|
136
|
+
return `z.any()${desc}`;
|
|
137
|
+
}
|
|
138
|
+
switch (resolved.type) {
|
|
139
|
+
case "string":
|
|
140
|
+
if (resolved.enum && resolved.enum.length > 0) {
|
|
141
|
+
const enumVals = resolved.enum.slice(0, 10).map(v => JSON.stringify(v)).join(", ");
|
|
142
|
+
const more = resolved.enum.length > 10 ? ` /* +${resolved.enum.length - 10} more */` : "";
|
|
143
|
+
return `z.enum([${enumVals}${more}])${desc}`;
|
|
144
|
+
}
|
|
145
|
+
if (resolved.format === "email") {
|
|
146
|
+
return `z.string().email()${desc}`;
|
|
147
|
+
}
|
|
148
|
+
if (resolved.format === "date-time") {
|
|
149
|
+
return `z.string()${desc.replace('.describe("', '.describe("ISO 8601 datetime: ')}`;
|
|
150
|
+
}
|
|
151
|
+
return `z.string()${desc}`;
|
|
152
|
+
case "number":
|
|
153
|
+
case "integer":
|
|
154
|
+
return `z.number()${desc}`;
|
|
155
|
+
case "boolean":
|
|
156
|
+
return `z.boolean()${desc}`;
|
|
157
|
+
case "array":
|
|
158
|
+
if (depth > 1) {
|
|
159
|
+
// OpenCode limitation: nested arrays become JSON strings
|
|
160
|
+
const arrayDesc = resolved.description
|
|
161
|
+
? `.describe(${JSON.stringify("JSON array: " + resolved.description)})`
|
|
162
|
+
: '.describe("JSON array")';
|
|
163
|
+
return `z.string()${arrayDesc}`;
|
|
164
|
+
}
|
|
165
|
+
if (resolved.items) {
|
|
166
|
+
const itemsResolved = resolveSchema(spec, resolved.items);
|
|
167
|
+
if (itemsResolved.type === "string") {
|
|
168
|
+
return `z.array(z.string())${desc}`;
|
|
169
|
+
}
|
|
170
|
+
if (itemsResolved.type === "number" || itemsResolved.type === "integer") {
|
|
171
|
+
return `z.array(z.number())${desc}`;
|
|
172
|
+
}
|
|
173
|
+
if (itemsResolved.enum) {
|
|
174
|
+
const enumVals = itemsResolved.enum.slice(0, 10).map((v) => JSON.stringify(v)).join(", ");
|
|
175
|
+
return `z.array(z.enum([${enumVals}]))${desc}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Complex array items - use JSON string
|
|
179
|
+
const arrayDesc = resolved.description
|
|
180
|
+
? `.describe(${JSON.stringify("JSON array: " + resolved.description)})`
|
|
181
|
+
: '.describe("JSON array")';
|
|
182
|
+
return `z.string()${arrayDesc}`;
|
|
183
|
+
case "object":
|
|
184
|
+
if (depth > 0) {
|
|
185
|
+
// Nested objects become JSON strings (OpenCode limitation)
|
|
186
|
+
const objDesc = generateObjectDescription(spec, resolved);
|
|
187
|
+
return `z.string().describe(${JSON.stringify("JSON object: " + objDesc)})`;
|
|
188
|
+
}
|
|
189
|
+
// Top-level objects can be expanded
|
|
190
|
+
if (resolved.properties && Object.keys(resolved.properties).length > 0) {
|
|
191
|
+
const props = Object.entries(resolved.properties).map(([name, propSchema]) => {
|
|
192
|
+
const isRequired = resolved.required?.includes(name);
|
|
193
|
+
let zodType = schemaToZod(spec, propSchema, depth + 1);
|
|
194
|
+
if (!isRequired && !zodType.includes(".optional()")) {
|
|
195
|
+
zodType += ".optional()";
|
|
196
|
+
}
|
|
197
|
+
return ` ${name}: ${zodType}`;
|
|
198
|
+
});
|
|
199
|
+
return `z.object({\n${props.join(",\n")}\n })`;
|
|
200
|
+
}
|
|
201
|
+
return `z.any()${desc}`;
|
|
202
|
+
default:
|
|
203
|
+
return `z.any()${desc}`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function generateObjectDescription(spec, schema) {
|
|
207
|
+
const parts = [];
|
|
208
|
+
if (schema.description) {
|
|
209
|
+
parts.push(schema.description);
|
|
210
|
+
}
|
|
211
|
+
else if (schema.title) {
|
|
212
|
+
parts.push(schema.title);
|
|
213
|
+
}
|
|
214
|
+
if (schema.properties) {
|
|
215
|
+
const required = schema.required || [];
|
|
216
|
+
const propDescs = [];
|
|
217
|
+
for (const [name, propSchema] of Object.entries(schema.properties)) {
|
|
218
|
+
const resolved = resolveSchema(spec, propSchema);
|
|
219
|
+
const isRequired = required.includes(name);
|
|
220
|
+
const reqMarker = isRequired ? " (required)" : "";
|
|
221
|
+
let typeDesc = resolved.type || "any";
|
|
222
|
+
if (resolved.enum) {
|
|
223
|
+
typeDesc = `enum: [${resolved.enum.slice(0, 3).join(", ")}${resolved.enum.length > 3 ? "..." : ""}]`;
|
|
224
|
+
}
|
|
225
|
+
let desc = `${name}${reqMarker}: ${typeDesc}`;
|
|
226
|
+
if (resolved.description) {
|
|
227
|
+
desc += ` - ${resolved.description.substring(0, 50)}${resolved.description.length > 50 ? "..." : ""}`;
|
|
228
|
+
}
|
|
229
|
+
propDescs.push(desc);
|
|
230
|
+
}
|
|
231
|
+
if (propDescs.length > 0) {
|
|
232
|
+
parts.push("Properties: " + propDescs.slice(0, 5).join("; "));
|
|
233
|
+
if (propDescs.length > 5) {
|
|
234
|
+
parts.push(`... and ${propDescs.length - 5} more`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return parts.join(". ") || "JSON object";
|
|
239
|
+
}
|
|
240
|
+
// ============================================================================
|
|
241
|
+
// Tool Generation
|
|
242
|
+
// ============================================================================
|
|
243
|
+
function operationToTool(spec, path, method, operation, pathParams, petName, readOnlyPatterns, coreEndpoints) {
|
|
244
|
+
// Generate tool name from operationId or path
|
|
245
|
+
let toolName;
|
|
246
|
+
if (operation.operationId) {
|
|
247
|
+
// Convert operationId to kebab-case with pet prefix
|
|
248
|
+
toolName = `${petName}-${operation.operationId
|
|
249
|
+
.replace(/([A-Z])/g, "-$1")
|
|
250
|
+
.replace(/_/g, "-")
|
|
251
|
+
.replace(/--+/g, "-")
|
|
252
|
+
.replace(/^-/, "")
|
|
253
|
+
.toLowerCase()}`;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Generate from method + path
|
|
257
|
+
const pathParts = path.replace(/\{[^}]+\}/g, "").split("/").filter(Boolean);
|
|
258
|
+
toolName = `${petName}-${method}-${pathParts.join("-")}`.toLowerCase();
|
|
259
|
+
}
|
|
260
|
+
// Description from summary/description
|
|
261
|
+
const description = operation.summary || operation.description || `${method.toUpperCase()} ${path}`;
|
|
262
|
+
// Combine path-level and operation-level parameters
|
|
263
|
+
const allParams = [...pathParams, ...(operation.parameters || [])];
|
|
264
|
+
// Convert parameters to our format
|
|
265
|
+
const parameters = allParams.map(param => {
|
|
266
|
+
const paramSchema = resolveSchema(spec, param.schema);
|
|
267
|
+
return {
|
|
268
|
+
name: param.name,
|
|
269
|
+
in: param.in,
|
|
270
|
+
required: param.required || param.in === "path",
|
|
271
|
+
schema: schemaToZod(spec, paramSchema),
|
|
272
|
+
description: param.description || paramSchema.description,
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
// Handle request body - expand properties into individual parameters
|
|
276
|
+
let hasRequestBody = false;
|
|
277
|
+
if (operation.requestBody?.content) {
|
|
278
|
+
const jsonContent = operation.requestBody.content["application/json"];
|
|
279
|
+
if (jsonContent?.schema) {
|
|
280
|
+
hasRequestBody = true;
|
|
281
|
+
const bodySchema = resolveSchema(spec, jsonContent.schema);
|
|
282
|
+
const bodyRequired = operation.requestBody.required ?? false;
|
|
283
|
+
// Expand body properties into individual parameters with "body" location
|
|
284
|
+
if (bodySchema.properties && Object.keys(bodySchema.properties).length > 0) {
|
|
285
|
+
const requiredProps = bodySchema.required || [];
|
|
286
|
+
for (const [propName, propSchema] of Object.entries(bodySchema.properties)) {
|
|
287
|
+
const resolvedProp = resolveSchema(spec, propSchema);
|
|
288
|
+
const isRequired = bodyRequired && requiredProps.includes(propName);
|
|
289
|
+
parameters.push({
|
|
290
|
+
name: propName,
|
|
291
|
+
in: "body",
|
|
292
|
+
required: isRequired,
|
|
293
|
+
schema: schemaToZod(spec, resolvedProp, 0),
|
|
294
|
+
description: resolvedProp.description || resolvedProp.title,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// Determine if read-only
|
|
301
|
+
const isReadOnly = method === "get" || method === "head" || method === "options" ||
|
|
302
|
+
readOnlyPatterns.some(pattern => toolName.includes(pattern) || operation.operationId?.includes(pattern));
|
|
303
|
+
// Determine if this is a core tool (always loaded) or requires env var
|
|
304
|
+
const tags = operation.tags || [];
|
|
305
|
+
let isCore = true; // Default to core if no coreEndpoints config
|
|
306
|
+
let loadEnvVar;
|
|
307
|
+
if (coreEndpoints) {
|
|
308
|
+
// Check if this tool matches any core endpoint criteria
|
|
309
|
+
const matchesCoreTag = coreEndpoints.tags?.some(tag => tags.map(t => t.toLowerCase()).includes(tag.toLowerCase()));
|
|
310
|
+
const matchesCorePath = coreEndpoints.paths?.some(pattern => path.toLowerCase().startsWith(pattern.toLowerCase()));
|
|
311
|
+
const matchesCoreOperationId = coreEndpoints.operationIds?.some(opId => operation.operationId?.toLowerCase().includes(opId.toLowerCase()));
|
|
312
|
+
isCore = !!(matchesCoreTag || matchesCorePath || matchesCoreOperationId);
|
|
313
|
+
if (!isCore) {
|
|
314
|
+
// Generate env var name for loading this tool group
|
|
315
|
+
// e.g., AI_HARMONY_LOAD_ADMIN_TOOLS=true
|
|
316
|
+
const tagName = (tags[0] || 'OTHER').replace(/[^a-zA-Z0-9]/g, '_').toUpperCase();
|
|
317
|
+
loadEnvVar = `${petName.toUpperCase().replace(/-/g, '_')}_LOAD_${tagName}_TOOLS`;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return {
|
|
321
|
+
name: toolName,
|
|
322
|
+
method: method.toUpperCase(),
|
|
323
|
+
path,
|
|
324
|
+
description,
|
|
325
|
+
parameters,
|
|
326
|
+
hasRequestBody,
|
|
327
|
+
isReadOnly,
|
|
328
|
+
tags,
|
|
329
|
+
isCore,
|
|
330
|
+
loadEnvVar,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function parseOpenAPISpec(spec, petName, config) {
|
|
334
|
+
const tools = [];
|
|
335
|
+
const readOnlyPatterns = config.readOnlyPatterns || ["list", "get", "search", "check", "status", "health"];
|
|
336
|
+
for (const [path, pathItem] of Object.entries(spec.paths)) {
|
|
337
|
+
const pathParams = pathItem.parameters || [];
|
|
338
|
+
const methods = [
|
|
339
|
+
{ method: "get", operation: pathItem.get },
|
|
340
|
+
{ method: "post", operation: pathItem.post },
|
|
341
|
+
{ method: "put", operation: pathItem.put },
|
|
342
|
+
{ method: "patch", operation: pathItem.patch },
|
|
343
|
+
{ method: "delete", operation: pathItem.delete },
|
|
344
|
+
];
|
|
345
|
+
for (const { method, operation } of methods) {
|
|
346
|
+
if (!operation)
|
|
347
|
+
continue;
|
|
348
|
+
// Skip deprecated operations
|
|
349
|
+
if (operation.deprecated)
|
|
350
|
+
continue;
|
|
351
|
+
// Check include/exclude filters
|
|
352
|
+
if (config.include && config.include.length > 0) {
|
|
353
|
+
const matches = config.include.some(pattern => operation.operationId?.includes(pattern) || path.includes(pattern));
|
|
354
|
+
if (!matches)
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (config.exclude && config.exclude.length > 0) {
|
|
358
|
+
const matches = config.exclude.some(pattern => operation.operationId?.includes(pattern) || path.includes(pattern));
|
|
359
|
+
if (matches)
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const tool = operationToTool(spec, path, method, operation, pathParams, petName, readOnlyPatterns, config.coreEndpoints);
|
|
363
|
+
tools.push(tool);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return tools;
|
|
367
|
+
}
|
|
368
|
+
// ============================================================================
|
|
369
|
+
// File Generation
|
|
370
|
+
// ============================================================================
|
|
371
|
+
/**
|
|
372
|
+
* Check if a property name needs to be quoted in JavaScript
|
|
373
|
+
*/
|
|
374
|
+
function needsQuotes(name) {
|
|
375
|
+
// Property names with special characters need quotes
|
|
376
|
+
return /[^a-zA-Z0-9_$]/.test(name) || /^\d/.test(name);
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get safe property accessor for args object
|
|
380
|
+
*/
|
|
381
|
+
function getPropertyAccessor(name) {
|
|
382
|
+
return needsQuotes(name) ? `args["${name}"]` : `args.${name}`;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Get safe property name for object literals
|
|
386
|
+
*/
|
|
387
|
+
function getPropertyName(name) {
|
|
388
|
+
return needsQuotes(name) ? `"${name}"` : name;
|
|
389
|
+
}
|
|
390
|
+
function generateToolDefinition(tool) {
|
|
391
|
+
// Build schema fields
|
|
392
|
+
const schemaFields = [];
|
|
393
|
+
// Add parameters (path, query, header, body)
|
|
394
|
+
for (const param of tool.parameters) {
|
|
395
|
+
let zodType = param.schema;
|
|
396
|
+
// Add description if not already present
|
|
397
|
+
if (param.description && !zodType.includes(".describe(")) {
|
|
398
|
+
// Insert .describe() before .optional() if present, otherwise append
|
|
399
|
+
if (zodType.includes(".optional()")) {
|
|
400
|
+
zodType = zodType.replace(".optional()", `.describe(${JSON.stringify(param.description)}).optional()`);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
zodType = zodType + `.describe(${JSON.stringify(param.description)})`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
// Add optional if not required and not already optional
|
|
407
|
+
if (!param.required && !zodType.includes(".optional()")) {
|
|
408
|
+
zodType += ".optional()";
|
|
409
|
+
}
|
|
410
|
+
// Use quoted property name if needed
|
|
411
|
+
const propName = getPropertyName(param.name);
|
|
412
|
+
schemaFields.push(` ${propName}: ${zodType}`);
|
|
413
|
+
}
|
|
414
|
+
const schemaBody = schemaFields.length > 0
|
|
415
|
+
? `z.object({\n${schemaFields.join(",\n")}\n })`
|
|
416
|
+
: "z.object({})";
|
|
417
|
+
// Generate path with parameter substitution
|
|
418
|
+
const pathWithParams = tool.path.replace(/\{([^}]+)\}/g, (_, name) => `\${${getPropertyAccessor(name)}}`);
|
|
419
|
+
// Generate execute body
|
|
420
|
+
const pathParams = tool.parameters.filter(p => p.in === "path").map(p => p.name);
|
|
421
|
+
const queryParams = tool.parameters.filter(p => p.in === "query").map(p => p.name);
|
|
422
|
+
const headerParams = tool.parameters.filter(p => p.in === "header").map(p => p.name);
|
|
423
|
+
const bodyParams = tool.parameters.filter(p => p.in === "body").map(p => p.name);
|
|
424
|
+
let executeBody = ` const url = \`\${baseUrl}${pathWithParams}\``;
|
|
425
|
+
// Add query parameters
|
|
426
|
+
if (queryParams.length > 0) {
|
|
427
|
+
executeBody += `
|
|
428
|
+
const params = new URLSearchParams()
|
|
429
|
+
${queryParams.map(p => ` if (${getPropertyAccessor(p)} !== undefined) params.append("${p}", String(${getPropertyAccessor(p)}))`).join("\n")}
|
|
430
|
+
const queryString = params.toString()
|
|
431
|
+
const fullUrl = queryString ? \`\${url}?\${queryString}\` : url`;
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
executeBody += `
|
|
435
|
+
const fullUrl = url`;
|
|
436
|
+
}
|
|
437
|
+
// Build fetch options
|
|
438
|
+
executeBody += `
|
|
439
|
+
|
|
440
|
+
const options: RequestInit = {
|
|
441
|
+
method: "${tool.method}",
|
|
442
|
+
headers: { ...headers }`;
|
|
443
|
+
// Add header parameters
|
|
444
|
+
if (headerParams.length > 0) {
|
|
445
|
+
for (const param of headerParams) {
|
|
446
|
+
executeBody += `,
|
|
447
|
+
...(${getPropertyAccessor(param)} ? { "${param}": ${getPropertyAccessor(param)} } : {})`;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Add body from body parameters
|
|
451
|
+
if (tool.hasRequestBody && bodyParams.length > 0) {
|
|
452
|
+
executeBody += `,
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
${bodyParams.map(p => ` ...(${getPropertyAccessor(p)} !== undefined ? { ${getPropertyName(p)}: ${getPropertyAccessor(p)} } : {})`).join(",\n")}
|
|
455
|
+
})`;
|
|
456
|
+
}
|
|
457
|
+
executeBody += `
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return fetchAPI(fullUrl, options)`;
|
|
461
|
+
return ` {
|
|
462
|
+
name: "${tool.name}",
|
|
463
|
+
description: ${JSON.stringify(tool.description)},
|
|
464
|
+
schema: ${schemaBody},
|
|
465
|
+
async execute(args: any) {
|
|
466
|
+
${executeBody}
|
|
467
|
+
}
|
|
468
|
+
}`;
|
|
469
|
+
}
|
|
470
|
+
function generateOpenAPIClientFile(spec, tools, config, petName) {
|
|
471
|
+
// Separate core tools from loadEnv tools
|
|
472
|
+
const coreTools = tools.filter(t => t.isCore);
|
|
473
|
+
const loadEnvTools = tools.filter(t => !t.isCore);
|
|
474
|
+
// Group loadEnv tools by their env var
|
|
475
|
+
const loadEnvGroups = {};
|
|
476
|
+
for (const tool of loadEnvTools) {
|
|
477
|
+
const envVar = tool.loadEnvVar || 'OTHER';
|
|
478
|
+
if (!loadEnvGroups[envVar])
|
|
479
|
+
loadEnvGroups[envVar] = [];
|
|
480
|
+
loadEnvGroups[envVar].push(tool);
|
|
481
|
+
}
|
|
482
|
+
const coreToolDefinitions = coreTools.map(generateToolDefinition).join(",\n\n");
|
|
483
|
+
// Generate loadEnv tool groups
|
|
484
|
+
const loadEnvGroupDefinitions = [];
|
|
485
|
+
for (const [envVar, groupTools] of Object.entries(loadEnvGroups)) {
|
|
486
|
+
const groupDefs = groupTools.map(generateToolDefinition).join(",\n\n");
|
|
487
|
+
loadEnvGroupDefinitions.push(`
|
|
488
|
+
// ${envVar} tools (${groupTools.length} tools)
|
|
489
|
+
// Enable with: ${envVar}=true
|
|
490
|
+
const ${envVar.toLowerCase().replace(/_/g, '')}Tools: ToolDefinition[] = process.env.${envVar} === 'true' ? [
|
|
491
|
+
${groupDefs}
|
|
492
|
+
] : []
|
|
493
|
+
`);
|
|
494
|
+
}
|
|
495
|
+
// Determine base URL
|
|
496
|
+
// Priority: config.baseUrl > spec.servers[0].url > derived from spec URL
|
|
497
|
+
let baseUrl = config.baseUrl || spec.servers?.[0]?.url || "";
|
|
498
|
+
// If still empty and we have a URL, derive baseUrl from it
|
|
499
|
+
if (!baseUrl && config.url) {
|
|
500
|
+
try {
|
|
501
|
+
const specUrl = new URL(config.url);
|
|
502
|
+
baseUrl = `${specUrl.protocol}//${specUrl.host}`;
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Ignore URL parsing errors
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// Determine auth setup
|
|
509
|
+
const authEnvVar = config.authEnvVar || `${petName.toUpperCase().replace(/-/g, "_")}_API_KEY`;
|
|
510
|
+
const authType = config.authType || "bearer";
|
|
511
|
+
const apiKeyHeader = config.apiKeyHeader || "X-API-Key";
|
|
512
|
+
let authSetup;
|
|
513
|
+
switch (authType) {
|
|
514
|
+
case "apiKey":
|
|
515
|
+
authSetup = `if (apiKey) {
|
|
516
|
+
headers["${apiKeyHeader}"] = apiKey
|
|
517
|
+
}`;
|
|
518
|
+
break;
|
|
519
|
+
case "basic":
|
|
520
|
+
authSetup = `if (apiKey) {
|
|
521
|
+
headers["Authorization"] = \`Basic \${apiKey}\`
|
|
522
|
+
}`;
|
|
523
|
+
break;
|
|
524
|
+
case "none":
|
|
525
|
+
authSetup = "";
|
|
526
|
+
break;
|
|
527
|
+
default: // bearer
|
|
528
|
+
authSetup = `if (apiKey) {
|
|
529
|
+
headers["Authorization"] = \`Bearer \${apiKey}\`
|
|
530
|
+
}`;
|
|
531
|
+
}
|
|
532
|
+
// Count read-only vs write tools
|
|
533
|
+
const readOnlyCount = tools.filter(t => t.isReadOnly).length;
|
|
534
|
+
const writeCount = tools.length - readOnlyCount;
|
|
535
|
+
const coreCount = coreTools.length;
|
|
536
|
+
const loadEnvCount = loadEnvTools.length;
|
|
537
|
+
// Group tools by tag for documentation
|
|
538
|
+
const tagGroups = {};
|
|
539
|
+
for (const tool of tools) {
|
|
540
|
+
const tag = tool.tags[0] || "Other";
|
|
541
|
+
if (!tagGroups[tag])
|
|
542
|
+
tagGroups[tag] = { tools: [], isCore: tool.isCore };
|
|
543
|
+
tagGroups[tag].tools.push(tool.name);
|
|
544
|
+
}
|
|
545
|
+
const tagDocs = Object.entries(tagGroups)
|
|
546
|
+
.map(([tag, info]) => ` * ${tag}: ${info.tools.length} tools ${info.isCore ? '(CORE)' : '(loadEnv)'}`)
|
|
547
|
+
.join("\n");
|
|
548
|
+
// Generate env var documentation for loadEnv groups
|
|
549
|
+
const loadEnvDocs = Object.entries(loadEnvGroups)
|
|
550
|
+
.map(([envVar, groupTools]) => ` * ${envVar}=true → ${groupTools.length} tools`)
|
|
551
|
+
.join("\n");
|
|
552
|
+
// Generate the combined tools array
|
|
553
|
+
const loadEnvVarNames = Object.keys(loadEnvGroups).map(ev => `...${ev.toLowerCase().replace(/_/g, '')}Tools`);
|
|
554
|
+
return `/**
|
|
555
|
+
* Auto-generated OpenAPI client tools
|
|
556
|
+
*
|
|
557
|
+
* Source: ${config.url || config.file || "OpenAPI spec"}
|
|
558
|
+
* API: ${spec.info.title} v${spec.info.version}
|
|
559
|
+
* Generated: ${new Date().toISOString()}
|
|
560
|
+
*
|
|
561
|
+
* Tools by category:
|
|
562
|
+
${tagDocs}
|
|
563
|
+
*
|
|
564
|
+
* Total: ${tools.length} tools (${readOnlyCount} read-only, ${writeCount} write)
|
|
565
|
+
* Core tools: ${coreCount} (always loaded)
|
|
566
|
+
* LoadEnv tools: ${loadEnvCount} (require env var to enable)
|
|
567
|
+
*
|
|
568
|
+
* To enable additional tool groups, set environment variables:
|
|
569
|
+
${loadEnvDocs}
|
|
570
|
+
*
|
|
571
|
+
* DO NOT EDIT MANUALLY - Regenerate with: pets generate-openapi
|
|
572
|
+
*/
|
|
573
|
+
|
|
574
|
+
import { z, type ToolDefinition, createLogger, isReadOnly, filterToolsForReadOnly } from "openpets-sdk"
|
|
575
|
+
|
|
576
|
+
const logger = createLogger("${petName}-client")
|
|
577
|
+
|
|
578
|
+
// Configuration
|
|
579
|
+
const baseUrl = "${baseUrl}"
|
|
580
|
+
const apiKey = process.env.${authEnvVar}
|
|
581
|
+
|
|
582
|
+
// Read-only mode check
|
|
583
|
+
const readOnlyMode = isReadOnly("${petName}")
|
|
584
|
+
|
|
585
|
+
if (readOnlyMode) {
|
|
586
|
+
logger.info("READ-ONLY MODE - write operations disabled")
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Common headers
|
|
590
|
+
const headers: Record<string, string> = {
|
|
591
|
+
"Content-Type": "application/json",
|
|
592
|
+
"Accept": "application/json",
|
|
593
|
+
}
|
|
594
|
+
${authSetup}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Fetch wrapper with error handling
|
|
598
|
+
*/
|
|
599
|
+
async function fetchAPI(url: string, options: RequestInit): Promise<string> {
|
|
600
|
+
try {
|
|
601
|
+
const response = await fetch(url, options)
|
|
602
|
+
|
|
603
|
+
if (!response.ok) {
|
|
604
|
+
const errorText = await response.text()
|
|
605
|
+
return JSON.stringify({
|
|
606
|
+
success: false,
|
|
607
|
+
error: \`HTTP \${response.status}: \${errorText}\`,
|
|
608
|
+
status: response.status
|
|
609
|
+
}, null, 2)
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const contentType = response.headers.get("content-type")
|
|
613
|
+
if (contentType?.includes("application/json")) {
|
|
614
|
+
const data = await response.json()
|
|
615
|
+
return JSON.stringify(data, null, 2)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const text = await response.text()
|
|
619
|
+
return JSON.stringify({ success: true, data: text }, null, 2)
|
|
620
|
+
} catch (error: any) {
|
|
621
|
+
return JSON.stringify({
|
|
622
|
+
success: false,
|
|
623
|
+
error: error.message
|
|
624
|
+
}, null, 2)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Tools marked as write operations (filtered in read-only mode)
|
|
629
|
+
// Based on HTTP method: POST, PUT, PATCH, DELETE are write operations
|
|
630
|
+
const writeToolNames = new Set([
|
|
631
|
+
${tools.filter(t => !t.isReadOnly).map(t => ` "${t.name}"`).join(",\n")}
|
|
632
|
+
])
|
|
633
|
+
|
|
634
|
+
// ============================================================================
|
|
635
|
+
// CORE TOOLS - Always loaded (${coreCount} tools)
|
|
636
|
+
// ============================================================================
|
|
637
|
+
const coreTools: ToolDefinition[] = [
|
|
638
|
+
${coreToolDefinitions}
|
|
639
|
+
]
|
|
640
|
+
|
|
641
|
+
// ============================================================================
|
|
642
|
+
// LOADENV TOOLS - Require environment variable to enable
|
|
643
|
+
// ============================================================================
|
|
644
|
+
${loadEnvGroupDefinitions.join("\n")}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* All tools combined (core + enabled loadEnv groups)
|
|
648
|
+
*/
|
|
649
|
+
const allTools: ToolDefinition[] = [
|
|
650
|
+
...coreTools,
|
|
651
|
+
${loadEnvVarNames.join(",\n ")}
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
// Log which tool groups are enabled
|
|
655
|
+
const enabledGroups: string[] = ['core']
|
|
656
|
+
${Object.keys(loadEnvGroups).map(ev => `if (process.env.${ev} === 'true') enabledGroups.push('${ev}')`).join("\n")}
|
|
657
|
+
logger.info(\`Loaded tool groups: \${enabledGroups.join(', ')} (\${allTools.length} tools total)\`)
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Filter tools by read-only mode using HTTP method classification
|
|
661
|
+
*/
|
|
662
|
+
function filterByReadOnlyMode(tools: ToolDefinition[]): ToolDefinition[] {
|
|
663
|
+
if (!readOnlyMode) return tools
|
|
664
|
+
|
|
665
|
+
const filtered = tools.filter(t => !writeToolNames.has(t.name))
|
|
666
|
+
const excluded = tools.length - filtered.length
|
|
667
|
+
|
|
668
|
+
if (excluded > 0) {
|
|
669
|
+
logger.info(\`Filtered \${excluded} write tools in read-only mode\`)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return filtered
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
export const openAPITools = filterByReadOnlyMode(allTools)
|
|
676
|
+
|
|
677
|
+
export default openAPITools
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
// ============================================================================
|
|
681
|
+
// Main Generator Function
|
|
682
|
+
// ============================================================================
|
|
683
|
+
export async function generateOpenAPITools(options = {}) {
|
|
684
|
+
const cwd = process.cwd();
|
|
685
|
+
const outputFile = options.outputFile || "openapi-client.ts";
|
|
686
|
+
// Load .env files
|
|
687
|
+
loadDotenv({ path: resolve(cwd, ".env") });
|
|
688
|
+
loadDotenv({ path: resolve(cwd, "../.env") });
|
|
689
|
+
loadDotenv({ path: resolve(cwd, "../../.env") });
|
|
690
|
+
// Determine config source
|
|
691
|
+
let config;
|
|
692
|
+
let petName;
|
|
693
|
+
// Check for URL or file from options first
|
|
694
|
+
if (options.url || options.file) {
|
|
695
|
+
const packageJsonPath = resolve(cwd, "package.json");
|
|
696
|
+
if (existsSync(packageJsonPath)) {
|
|
697
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
698
|
+
petName = packageJson.name?.replace(/^@openpets\//, "") || basename(cwd);
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
petName = basename(cwd);
|
|
702
|
+
}
|
|
703
|
+
config = {
|
|
704
|
+
url: options.url,
|
|
705
|
+
file: options.file,
|
|
706
|
+
baseUrl: options.baseUrl,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
// Load from package.json
|
|
711
|
+
const loaded = loadConfigFromPackageJson(cwd);
|
|
712
|
+
if (!loaded) {
|
|
713
|
+
return {
|
|
714
|
+
success: false,
|
|
715
|
+
message: "No OpenAPI configuration found. Use --url or --file options, or add openapiSpec to package.json"
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
config = loaded.config;
|
|
719
|
+
petName = loaded.petName;
|
|
720
|
+
}
|
|
721
|
+
if (!config.url && !config.file) {
|
|
722
|
+
return {
|
|
723
|
+
success: false,
|
|
724
|
+
message: "No OpenAPI spec URL or file specified. Use --url or --file options."
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
if (options.verbose) {
|
|
728
|
+
console.log(`Pet Name: ${petName}`);
|
|
729
|
+
console.log(`Source: ${config.url || config.file}`);
|
|
730
|
+
}
|
|
731
|
+
try {
|
|
732
|
+
// Fetch or read the OpenAPI spec
|
|
733
|
+
let spec;
|
|
734
|
+
if (config.url) {
|
|
735
|
+
if (options.verbose) {
|
|
736
|
+
console.log(`Fetching OpenAPI spec from: ${config.url}`);
|
|
737
|
+
}
|
|
738
|
+
spec = await fetchOpenAPISpec(config.url);
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
const filePath = resolve(cwd, config.file);
|
|
742
|
+
if (options.verbose) {
|
|
743
|
+
console.log(`Reading OpenAPI spec from: ${filePath}`);
|
|
744
|
+
}
|
|
745
|
+
spec = readOpenAPISpec(filePath);
|
|
746
|
+
}
|
|
747
|
+
if (options.verbose) {
|
|
748
|
+
console.log(`API: ${spec.info.title} v${spec.info.version}`);
|
|
749
|
+
console.log(`Paths: ${Object.keys(spec.paths).length}`);
|
|
750
|
+
}
|
|
751
|
+
// Dump spec for debugging
|
|
752
|
+
if (options.dumpSpec) {
|
|
753
|
+
const specFile = resolve(cwd, "openapi-spec-dump.json");
|
|
754
|
+
writeFileSync(specFile, JSON.stringify(spec, null, 2));
|
|
755
|
+
console.log(`Dumped spec to: ${specFile}`);
|
|
756
|
+
}
|
|
757
|
+
// Parse spec into tools
|
|
758
|
+
const tools = parseOpenAPISpec(spec, petName, config);
|
|
759
|
+
if (options.verbose) {
|
|
760
|
+
console.log(`Generated ${tools.length} tools`);
|
|
761
|
+
console.log("\nTools:");
|
|
762
|
+
for (const tool of tools) {
|
|
763
|
+
const marker = tool.isReadOnly ? "read" : "write";
|
|
764
|
+
console.log(` [${marker}] ${tool.name} - ${tool.method} ${tool.path}`);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Generate client file
|
|
768
|
+
const content = generateOpenAPIClientFile(spec, tools, config, petName);
|
|
769
|
+
if (options.dryRun) {
|
|
770
|
+
console.log("\n--- Generated openapi-client.ts ---");
|
|
771
|
+
console.log(content.slice(0, 5000));
|
|
772
|
+
if (content.length > 5000) {
|
|
773
|
+
console.log(`\n... (${content.length - 5000} more characters)`);
|
|
774
|
+
}
|
|
775
|
+
console.log("--- End ---\n");
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
const outputPath = resolve(cwd, outputFile);
|
|
779
|
+
writeFileSync(outputPath, content);
|
|
780
|
+
}
|
|
781
|
+
const readOnlyCount = tools.filter(t => t.isReadOnly).length;
|
|
782
|
+
const writeCount = tools.length - readOnlyCount;
|
|
783
|
+
return {
|
|
784
|
+
success: true,
|
|
785
|
+
message: options.dryRun
|
|
786
|
+
? `Would generate ${tools.length} tools (${readOnlyCount} read-only, ${writeCount} write) to ${outputFile}`
|
|
787
|
+
: `Generated ${tools.length} tools (${readOnlyCount} read-only, ${writeCount} write) to ${outputFile}`,
|
|
788
|
+
toolCount: tools.length,
|
|
789
|
+
outputPath: resolve(cwd, outputFile),
|
|
790
|
+
tools,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
catch (error) {
|
|
794
|
+
return {
|
|
795
|
+
success: false,
|
|
796
|
+
message: `Failed to generate OpenAPI tools: ${error.message}`
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
//# sourceMappingURL=openapi-generator.js.map
|