UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

997 lines (857 loc) 33.3 kB
#!/usr/bin/env node /** * OpenAPI/Swagger Parser Utility * This script extracts and organizes endpoint information from OpenAPI spec files * * Usage: * node openapi-parser.js <openapi-file.json> --summary * node openapi-parser.js <openapi-file.json> --endpoints * // ... other commands * node openapi-parser.js <openapi-file.json> --find-property <prop-name> * node openapi-parser.js <openapi-file.json> --find-property <prop-name> --in-schema <SchemaName> */ import fs from 'fs/promises'; class OpenAPIParser { constructor(spec) { this.spec = spec; this.version = spec.openapi || spec.swagger || '2.0'; // Global cache for resolved schemas with better tracking this._resolvedSchemas = null; this._globalCache = new Map(); this._refCache = new Map(); // Global recursion tracking this._globalVisited = new WeakSet(); this._refStack = new Set(); } /** * Get all endpoints grouped by tag or path */ getAllEndpoints() { const endpoints = []; const paths = this.spec.paths || {}; for (const [pathKey, pathItem] of Object.entries(paths)) { for (const [method, operation] of Object.entries(pathItem)) { if (['get', 'post', 'put', 'delete', 'patch', 'options', 'head'].includes(method)) { endpoints.push({ path: pathKey, method: method.toUpperCase(), operationId: operation.operationId, summary: operation.summary, description: operation.description, tags: operation.tags || [], deprecated: operation.deprecated || false }); } } } return endpoints; } /** * Get detailed information about a specific endpoint */ getEndpointDetails(method, path) { const pathItem = this.spec.paths?.[path]; if (!pathItem) return null; const operation = pathItem[method.toLowerCase()]; if (!operation) return null; return { path, method: method.toUpperCase(), operationId: operation.operationId, summary: operation.summary, description: operation.description, tags: operation.tags || [], parameters: this.extractParameters(operation, pathItem), requestBody: this.extractRequestBody(operation), responses: this.extractResponses(operation), security: operation.security || this.spec.security || [], deprecated: operation.deprecated || false }; } /** * Extract parameters (path, query, header, cookie) */ extractParameters(operation, pathItem) { const parameters = [ ...(pathItem.parameters || []), ...(operation.parameters || []) ]; return parameters.map(param => { // Handle $ref if (param.$ref) { param = this.resolveRef(param.$ref); } return { name: param.name, in: param.in, description: param.description, required: param.required || false, deprecated: param.deprecated || false, schema: param.schema || { type: param.type }, example: param.example, examples: param.examples, style: param.style, explode: param.explode }; }); } /** * Extract request body information */ extractRequestBody(operation) { if (!operation.requestBody) return null; let requestBody = operation.requestBody; // Handle $ref if (requestBody.$ref) { requestBody = this.resolveRef(requestBody.$ref); } const content = {}; for (const [mediaType, mediaTypeObj] of Object.entries(requestBody.content || {})) { content[mediaType] = { schema: this.resolveSchema(mediaTypeObj.schema), examples: mediaTypeObj.examples, example: mediaTypeObj.example }; } return { description: requestBody.description, required: requestBody.required || false, content }; } /** * Extract response information */ extractResponses(operation) { const responses = {}; for (const [statusCode, response] of Object.entries(operation.responses || {})) { let resolvedResponse = response; // Handle $ref if (response.$ref) { resolvedResponse = this.resolveRef(response.$ref); } const content = {}; for (const [mediaType, mediaTypeObj] of Object.entries(resolvedResponse.content || {})) { content[mediaType] = { schema: this.resolveSchema(mediaTypeObj.schema), examples: mediaTypeObj.examples, example: mediaTypeObj.example }; } responses[statusCode] = { description: resolvedResponse.description, headers: resolvedResponse.headers, content }; } return responses; } /** * Resolve schema references with robust circular reference protection */ resolveSchema(schema, context = { depth: 0, visited: new Set(), path: '' }) { // Hard limits const MAX_DEPTH = 10; const MAX_ITERATIONS = 1000; if (!schema || context.depth > MAX_DEPTH) { console.warn(`Schema resolution stopped: depth=${context.depth}, path=${context.path}`); return { type: 'object', description: '[Depth limit reached]' }; } // Create a stable identifier for this schema instance const schemaKey = this._createStableSchemaKey(schema, context.path); // Check if we've already processed this exact schema in this context if (context.visited.has(schemaKey)) { return { type: 'object', description: '[Circular reference detected]', $ref: schema.$ref }; } // Check global cache if (this._globalCache.has(schemaKey)) { return this._globalCache.get(schemaKey); } // Add to visited before processing to prevent infinite recursion context.visited.add(schemaKey); try { let resolvedSchema = schema; // Handle $ref with enhanced protection if (schema.$ref) { if (this._refStack.has(schema.$ref)) { console.warn(`Circular $ref detected: ${schema.$ref}`); return { type: 'object', description: '[Circular $ref]', $ref: schema.$ref }; } this._refStack.add(schema.$ref); try { resolvedSchema = this.resolveRef(schema.$ref); } catch (error) { console.warn(`Failed to resolve $ref: ${schema.$ref} - ${error.message}`); return { type: 'object', $ref: schema.$ref, error: 'Failed to resolve' }; } finally { this._refStack.delete(schema.$ref); } } // Build the resolved schema safely const result = { type: resolvedSchema.type, format: resolvedSchema.format, description: resolvedSchema.description, example: resolvedSchema.example, default: resolvedSchema.default, enum: resolvedSchema.enum, nullable: resolvedSchema.nullable, deprecated: resolvedSchema.deprecated }; // Cache early to prevent re-processing this._globalCache.set(schemaKey, result); // Handle different schema types with depth tracking const newContext = { depth: context.depth + 1, visited: new Set(context.visited), path: context.path }; if (resolvedSchema.type === 'object' && resolvedSchema.properties) { result.properties = {}; result.required = resolvedSchema.required || []; const propertyKeys = Object.keys(resolvedSchema.properties).slice(0, 50); // Limit properties for (const propName of propertyKeys) { const propSchema = resolvedSchema.properties[propName]; const propPath = context.path ? `${context.path}.${propName}` : propName; result.properties[propName] = this.resolveSchema(propSchema, { ...newContext, path: propPath }); } if (resolvedSchema.additionalProperties !== undefined) { if (typeof resolvedSchema.additionalProperties === 'object') { result.additionalProperties = this.resolveSchema(resolvedSchema.additionalProperties, { ...newContext, path: `${context.path}.<additional>` }); } else { result.additionalProperties = resolvedSchema.additionalProperties; } } } else if (resolvedSchema.type === 'array' && resolvedSchema.items) { result.items = this.resolveSchema(resolvedSchema.items, { ...newContext, path: `${context.path}[]` }); result.minItems = resolvedSchema.minItems; result.maxItems = resolvedSchema.maxItems; result.uniqueItems = resolvedSchema.uniqueItems; } // Handle string constraints if (resolvedSchema.type === 'string') { result.minLength = resolvedSchema.minLength; result.maxLength = resolvedSchema.maxLength; result.pattern = resolvedSchema.pattern; } // Handle number constraints if (resolvedSchema.type === 'number' || resolvedSchema.type === 'integer') { result.minimum = resolvedSchema.minimum; result.maximum = resolvedSchema.maximum; result.exclusiveMinimum = resolvedSchema.exclusiveMinimum; result.exclusiveMaximum = resolvedSchema.exclusiveMaximum; result.multipleOf = resolvedSchema.multipleOf; } // Handle combined schemas with strict limits if (context.depth < MAX_DEPTH - 2) { if (resolvedSchema.allOf && resolvedSchema.allOf.length <= 10) { result.allOf = resolvedSchema.allOf.map((s, i) => this.resolveSchema(s, { ...newContext, path: `${context.path}.allOf[${i}]` }) ); } if (resolvedSchema.oneOf && resolvedSchema.oneOf.length <= 10) { result.oneOf = resolvedSchema.oneOf.map((s, i) => this.resolveSchema(s, { ...newContext, path: `${context.path}.oneOf[${i}]` }) ); } if (resolvedSchema.anyOf && resolvedSchema.anyOf.length <= 10) { result.anyOf = resolvedSchema.anyOf.map((s, i) => this.resolveSchema(s, { ...newContext, path: `${context.path}.anyOf[${i}]` }) ); } } return result; } catch (error) { console.warn(`Error resolving schema at path ${context.path}:`, error.message); return { type: 'object', error: error.message }; } } /** * Create a stable, unique key for schema caching */ _createStableSchemaKey(schema, path) { if (!schema) return 'null'; // For $ref, use the ref itself as the primary key if (schema.$ref) { return `ref:${schema.$ref}:${path}`; } // Create a deterministic fingerprint const parts = [ schema.type || 'any', schema.title || '', schema.format || '', schema.description ? schema.description.substring(0, 50) : '', Object.keys(schema.properties || {}).length.toString(), schema.items ? 'hasItems' : '', path ]; return parts.join('|'); } /** * Resolve $ref references with better error handling */ resolveRef(ref) { // Check cache first if (this._refCache.has(ref)) { return this._refCache.get(ref); } const parts = ref.split('/'); let current = this.spec; try { for (let i = 1; i < parts.length; i++) { if (!current || typeof current !== 'object') { throw new Error(`Invalid reference path at segment: ${parts[i]}`); } current = current[parts[i]]; if (current === undefined) { throw new Error(`Reference path not found at segment: ${parts[i]}`); } } // Cache the result this._refCache.set(ref, current); return current; } catch (error) { throw new Error(`Cannot resolve reference: ${ref} - ${error.message}`); } } /** * Get all schemas with improved caching */ getAllSchemas() { if (this._resolvedSchemas) { return this._resolvedSchemas; } const schemas = this.spec.components?.schemas || this.spec.definitions || {}; const result = {}; // Process schemas with limits const schemaNames = Object.keys(schemas).slice(0, 100); // Limit total schemas for (const name of schemaNames) { try { result[name] = this.resolveSchema(schemas[name], { depth: 0, visited: new Set(), path: name }); } catch (error) { console.warn(`Failed to resolve schema ${name}:`, error.message); result[name] = { type: 'object', error: error.message }; } } this._resolvedSchemas = result; return result; } /** * Search endpoints by keyword */ searchEndpoints(keyword) { const lowerKeyword = keyword.toLowerCase(); const endpoints = this.getAllEndpoints(); return endpoints.filter(endpoint => { const searchText = `${endpoint.path} ${endpoint.method} ${endpoint.operationId || ''} ${endpoint.summary || ''} ${endpoint.description || ''} ${endpoint.tags.join(' ')}`.toLowerCase(); return searchText.includes(lowerKeyword); }); } /** * Get endpoints by tag */ getEndpointsByTag(tag) { const endpoints = this.getAllEndpoints(); return endpoints.filter(endpoint => endpoint.tags.includes(tag)); } /** * Get a summary of the API */ getAPISummary() { const endpoints = this.getAllEndpoints(); const tags = new Set(); const methods = {}; endpoints.forEach(endpoint => { endpoint.tags.forEach(tag => tags.add(tag)); methods[endpoint.method] = (methods[endpoint.method] || 0) + 1; }); return { title: this.spec.info?.title, version: this.spec.info?.version, description: this.spec.info?.description, servers: this.spec.servers || [{ url: this.spec.host ? `${this.spec.schemes?.[0] || 'http'}://${this.spec.host}${this.spec.basePath || ''}` : 'N/A' }], totalEndpoints: endpoints.length, tags: Array.from(tags), methodCounts: methods, security: this.spec.security || [], externalDocs: this.spec.externalDocs }; } /** * Robust property finder with better recursion protection */ _findPropertyRecursive(currentSchema, propertyName, currentPath, results, context = { depth: 0, visited: new Set(), pathStack: new Set() }) { // Hard limits const MAX_DEPTH = 15; const MAX_RESULTS = 100; const MAX_PATHS = 50; if (context.depth > MAX_DEPTH || results.length > MAX_RESULTS || context.pathStack.size > MAX_PATHS) { return; } if (!currentSchema || typeof currentSchema !== 'object') return; // Create unique identifier for this search context const contextKey = `${currentPath}:${context.depth}:${propertyName}`; if (context.visited.has(contextKey)) { return; } if (context.pathStack.has(currentPath)) { return; // Circular path detected } context.visited.add(contextKey); context.pathStack.add(currentPath); try { // Handle $ref resolution with protection if (currentSchema.$ref) { if (this._refStack.has(currentSchema.$ref)) { return; // Circular reference } this._refStack.add(currentSchema.$ref); try { const resolvedSchema = this.resolveRef(currentSchema.$ref); this._findPropertyRecursive(resolvedSchema, propertyName, currentPath, results, { depth: context.depth + 1, visited: new Set(context.visited), pathStack: new Set(context.pathStack) }); } catch (error) { console.warn(`Failed to resolve $ref in property search: ${currentSchema.$ref}`); } finally { this._refStack.delete(currentSchema.$ref); } return; } // Handle object properties with limits if (currentSchema.type === 'object' && currentSchema.properties) { const propertyKeys = Object.keys(currentSchema.properties).slice(0, 20); // Limit properties per object for (const key of propertyKeys) { const subSchema = currentSchema.properties[key]; const newPath = currentPath ? `${currentPath}.${key}` : key; // Check for property match if (key.toLowerCase() === propertyName.toLowerCase()) { results.push({ path: newPath, validation: this._simplifySchema(subSchema), depth: context.depth }); } // Continue searching deeper with new context if (this._shouldRecurseInto(subSchema) && context.depth < MAX_DEPTH - 2) { this._findPropertyRecursive(subSchema, propertyName, newPath, results, { depth: context.depth + 1, visited: new Set(context.visited), pathStack: new Set(context.pathStack) }); } } } // Handle array items with protection if (currentSchema.type === 'array' && currentSchema.items && context.depth < MAX_DEPTH - 2) { const itemsPath = currentPath ? `${currentPath}[]` : '[]'; if (this._shouldRecurseInto(currentSchema.items)) { this._findPropertyRecursive(currentSchema.items, propertyName, itemsPath, results, { depth: context.depth + 1, visited: new Set(context.visited), pathStack: new Set(context.pathStack) }); } } // Handle combined schemas with strict limits if (context.depth < MAX_DEPTH - 3) { const combinedSchemas = [ ...(currentSchema.allOf || []).slice(0, 5), ...(currentSchema.oneOf || []).slice(0, 5), ...(currentSchema.anyOf || []).slice(0, 5) ]; for (const schema of combinedSchemas) { if (this._shouldRecurseInto(schema)) { this._findPropertyRecursive(schema, propertyName, currentPath, results, { depth: context.depth + 1, visited: new Set(context.visited), pathStack: new Set(context.pathStack) }); } } } // Handle additionalProperties with protection if (currentSchema.additionalProperties && typeof currentSchema.additionalProperties === 'object' && context.depth < MAX_DEPTH - 2) { const additionalPath = currentPath ? `${currentPath}.<additionalProperty>` : '<additionalProperty>'; if (this._shouldRecurseInto(currentSchema.additionalProperties)) { this._findPropertyRecursive(currentSchema.additionalProperties, propertyName, additionalPath, results, { depth: context.depth + 1, visited: new Set(context.visited), pathStack: new Set(context.pathStack) }); } } } catch (error) { console.warn(`Error in property search at path ${currentPath}:`, error.message); } finally { context.pathStack.delete(currentPath); } } /** * Enhanced recursion safety check */ _shouldRecurseInto(schema) { if (!schema || typeof schema !== 'object') return false; // Don't recurse into simple types if (schema.type && ['string', 'number', 'integer', 'boolean', 'null'].includes(schema.type)) { // Unless they have special properties that might contain nested schemas return !!(schema.properties || schema.items || schema.allOf || schema.oneOf || schema.anyOf); } // Check if there's something meaningful to recurse into return !!( schema.$ref || schema.properties || schema.items || schema.allOf || schema.oneOf || schema.anyOf || (schema.additionalProperties && typeof schema.additionalProperties === 'object') ); } /** * Simplify schema for output */ _simplifySchema(schema) { if (!schema) return null; const simplified = { type: schema.type, format: schema.format, description: schema.description ? schema.description.substring(0, 200) : undefined, example: schema.example, default: schema.default, enum: schema.enum, required: schema.required }; // Add validation constraints based on type if (schema.type === 'string') { if (schema.minLength !== undefined) simplified.minLength = schema.minLength; if (schema.maxLength !== undefined) simplified.maxLength = schema.maxLength; if (schema.pattern) simplified.pattern = schema.pattern; } else if (schema.type === 'number' || schema.type === 'integer') { if (schema.minimum !== undefined) simplified.minimum = schema.minimum; if (schema.maximum !== undefined) simplified.maximum = schema.maximum; } else if (schema.type === 'array') { if (schema.minItems !== undefined) simplified.minItems = schema.minItems; if (schema.maxItems !== undefined) simplified.maxItems = schema.maxItems; simplified.items = schema.items?.$ref || schema.items?.type || 'any'; } // Include $ref if present if (schema.$ref) simplified.$ref = schema.$ref; // Remove undefined values Object.keys(simplified).forEach(key => { if (simplified[key] === undefined) { delete simplified[key]; } }); return simplified; } /** * Find property with enhanced protection */ findProperty(propertyName, schemaName = null) { // Clear global state this._refStack.clear(); this._globalVisited = new WeakSet(); const rawSchemas = this.spec.components?.schemas || this.spec.definitions || {}; const results = []; try { if (schemaName) { // Scoped search const foundSchemaKey = Object.keys(rawSchemas).find(k => k.toLowerCase() === schemaName.toLowerCase()); if (!foundSchemaKey) { return { error: `Schema '${schemaName}' not found.` }; } const schemaToSearch = rawSchemas[foundSchemaKey]; this._findPropertyRecursive(schemaToSearch, propertyName, '', results); return results.map(r => ({ ...r, path: `${foundSchemaKey}.${r.path}` })); } else { // Global search with limits const globalResults = {}; const schemaKeys = Object.keys(rawSchemas).slice(0, 50); // Limit schemas to search for (const key of schemaKeys) { const schema = rawSchemas[key]; const schemaResults = []; try { this._findPropertyRecursive(schema, propertyName, '', schemaResults); if (schemaResults.length > 0) { globalResults[key] = schemaResults.map(r => ({ ...r, path: `${key}.${r.path}` })); } } catch (error) { console.warn(`Error searching in schema ${key}:`, error.message); } // Reset state between schemas this._refStack.clear(); } return globalResults; } } catch (error) { console.error('Property search failed:', error.message); return { error: error.message }; } } /** * Test recursion handling with better protection */ testRecursionHandling() { const testResults = { totalSchemas: 0, complexSchemas: [], deepestNesting: 0, circularReferences: [], errors: [], cacheStats: { globalCacheSize: this._globalCache.size, refCacheSize: this._refCache.size } }; try { const schemas = this.spec.components?.schemas || this.spec.definitions || {}; testResults.totalSchemas = Object.keys(schemas).length; // Limit testing to prevent runaway processes const schemasToTest = Object.entries(schemas).slice(0, 20); for (const [name, schema] of schemasToTest) { try { // Clear state before each test this._refStack.clear(); // Test resolution with timeout const startTime = Date.now(); const resolved = this.resolveSchema(schema, { depth: 0, visited: new Set(), path: name }); const endTime = Date.now(); if (endTime - startTime > 5000) { // 5 second timeout testResults.errors.push({ schema: name, error: 'Resolution timeout' }); continue; } // Measure complexity safely const depth = this._measureSchemaDepth(schema, new Set(), 0, 10); // Max depth 10 for testing if (depth > 5) { testResults.complexSchemas.push({ name, depth, hasCircularRef: this._hasCircularReference(schema, [], new Set(), 10) }); } if (depth > testResults.deepestNesting) { testResults.deepestNesting = depth; } // Check for circular references safely if (this._hasCircularReference(schema, [], new Set(), 10)) { testResults.circularReferences.push(name); } } catch (error) { testResults.errors.push({ schema: name, error: error.message }); } } } catch (error) { testResults.errors.push({ general: error.message }); } return testResults; } /** * Measure schema depth with protection */ _measureSchemaDepth(schema, visited, depth, maxDepth) { if (!schema || depth >= maxDepth) return depth; const schemaKey = schema.$ref || JSON.stringify(schema).substring(0, 50); if (visited.has(schemaKey)) return depth; visited.add(schemaKey); let maxFoundDepth = depth; try { if (schema.$ref) { const resolved = this.resolveRef(schema.$ref); maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(resolved, visited, depth + 1, maxDepth)); } if (schema.properties) { const props = Object.values(schema.properties).slice(0, 10); // Limit properties for (const prop of props) { maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(prop, visited, depth + 1, maxDepth)); } } if (schema.items) { maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(schema.items, visited, depth + 1, maxDepth)); } const combined = [ ...(schema.allOf || []).slice(0, 5), ...(schema.oneOf || []).slice(0, 5), ...(schema.anyOf || []).slice(0, 5) ]; for (const subSchema of combined) { maxFoundDepth = Math.max(maxFoundDepth, this._measureSchemaDepth(subSchema, visited, depth + 1, maxDepth)); } } catch (error) { // Ignore errors in depth measurement } return maxFoundDepth; } /** * Check for circular references with protection */ _hasCircularReference(schema, path, visited, maxDepth) { if (!schema || path.length >= maxDepth) return false; const schemaId = schema.$ref || JSON.stringify(schema).substring(0, 50); if (path.includes(schemaId)) return true; if (visited.has(schemaId)) return false; visited.add(schemaId); const newPath = [...path, schemaId]; try { if (schema.$ref) { const resolved = this.resolveRef(schema.$ref); if (this._hasCircularReference(resolved, newPath, visited, maxDepth)) return true; } if (schema.properties) { const props = Object.values(schema.properties).slice(0, 10); for (const prop of props) { if (this._hasCircularReference(prop, newPath, visited, maxDepth)) return true; } } if (schema.items && this._hasCircularReference(schema.items, newPath, visited, maxDepth)) return true; const combined = [ ...(schema.allOf || []).slice(0, 5), ...(schema.oneOf || []).slice(0, 5), ...(schema.anyOf || []).slice(0, 5) ]; for (const subSchema of combined) { if (this._hasCircularReference(subSchema, newPath, visited, maxDepth)) return true; } } catch (error) { // Ignore errors in circular reference detection } return false; } } // CLI Handler async function main() { const args = process.argv.slice(2); if (args.length < 2) { console.log(` Usage: node openapi-parser.js <openapi-file> [command] [options] Commands: --summary Get a high-level summary of the API. --endpoints List all available endpoints. --endpoint <METHOD> <PATH> Get details for a specific endpoint. --schemas List all defined data schemas. --search <keyword> Search endpoints by a keyword. --tag <tag> Get endpoints associated with a specific tag. --find-property <prop-name> Find a property globally across all schemas. --find-property <prop-name> --in-schema <SchemaName> Find a property only within a specific schema. --test-recursion Test the parser for recursion handling and schema complexity. Examples: node openapi-parser.js api.json --summary node openapi-parser.js api.json --find-property status node openapi-parser.js api.json --find-property type --in-schema Experiment node openapi-parser.js api.json --test-recursion `); process.exit(1); } try { const filename = args[0]; const command = args[1]; const content = await fs.readFile(filename, 'utf8'); const spec = JSON.parse(content); const parser = new OpenAPIParser(spec); let result; switch (command) { case '--summary': result = parser.getAPISummary(); break; case '--endpoints': result = parser.getAllEndpoints(); break; case '--endpoint': if (args.length < 4) { console.error('Error: Please provide METHOD and PATH for --endpoint'); process.exit(1); } result = parser.getEndpointDetails(args[2], args[3]); break; case '--schemas': result = parser.getAllSchemas(); break; case '--search': if (args.length < 3) { console.error('Error: Please provide a search keyword for --search'); process.exit(1); } result = parser.searchEndpoints(args[2]); break; case '--tag': if (args.length < 3) { console.error('Error: Please provide a tag name for --tag'); process.exit(1); } result = parser.getEndpointsByTag(args[2]); break; case '--find-property': { if (args.length < 3) { console.error('Error: Please provide a property name for --find-property'); process.exit(1); } const propertyName = args[2]; const schemaFlagIndex = args.indexOf('--in-schema'); if (schemaFlagIndex !== -1 && args.length > schemaFlagIndex + 1) { // Scoped search const schemaName = args[schemaFlagIndex + 1]; result = parser.findProperty(propertyName, schemaName); } else { // Global search result = parser.findProperty(propertyName); } break; } case '--test-recursion': result = parser.testRecursionHandling(); break; default: console.error(`Unknown command: ${command}`); process.exit(1); } console.log(JSON.stringify(result, null, 2)); } catch (error) { // Provide more detailed error information console.error('Error:', error.message); if (error.stack && (error.stack.includes('Maximum call stack size exceeded') || error.message.includes('recursion'))) { console.error('\nRecursion error detected! The parser has built-in protection, but this schema may be extremely complex.'); console.error('Try running with --test-recursion to identify problematic schemas.'); } if (process.env.DEBUG) { console.error('\nStack trace:', error.stack); } process.exit(1); } } // Export for use as a module export { OpenAPIParser }; // Run CLI if called directly if (import.meta.url.endsWith(process.argv[1])) { main(); }