UNPKG

arela

Version:

AI-powered CTO with multi-agent orchestration, code summarization, visual testing (web + mobile) for blazing fast development.

158 lines • 6.75 kB
/** * API Contract Generator - Main Orchestrator */ import path from 'path'; import fs from 'fs-extra'; import pc from 'picocolors'; import { extractEndpoints, detectSlices } from './endpoint-extractor.js'; import { extractCalls } from './call-extractor.js'; import { matchCallsToEndpoints, findUnmatchedCalls, findUnmatchedEndpoints } from './matcher.js'; import { detectDrift } from './drift-detector.js'; import { generateOpenAPISpec, specToYaml, specToJson, } from './openapi-generator.js'; import { groupEndpointsBySlice, groupCallsBySlice, groupMatchesBySlice, groupDriftBySlice, createSliceGroups, } from './slice-grouper.js'; import { displayContractReport } from './reporter.js'; /** * Main function to generate API contracts */ export async function generateContracts(options) { const startTime = Date.now(); console.log(pc.bold(pc.cyan('\nšŸ” Analyzing API contracts...\n'))); // Find Graph DB const graphDbPath = findGraphDb(options.repoPaths); if (!graphDbPath) { throw new Error('Graph database not found. Run "arela ingest" first.'); } console.log(pc.gray(`Using database: ${graphDbPath}\n`)); try { // 1. Extract endpoints and calls console.log(pc.cyan('šŸ“Š Extracting API endpoints and calls...')); const endpoints = extractEndpoints(graphDbPath); const calls = extractCalls(graphDbPath); console.log(pc.green(`āœ… Found ${endpoints.length} endpoints, ${calls.length} calls\n`)); // 2. Match calls to endpoints console.log(pc.cyan('šŸ”— Matching calls to endpoints...')); const matches = matchCallsToEndpoints(calls, endpoints, 0.75); const unmatchedCalls = findUnmatchedCalls(calls, matches); const unmatchedEndpoints = findUnmatchedEndpoints(endpoints, matches); console.log(pc.green(`āœ… Matched ${matches.length} endpoint-call pairs\n`)); // 3. Detect drift console.log(pc.cyan('šŸ” Detecting schema drift...')); const driftIssues = detectDrift(matches, unmatchedCalls, unmatchedEndpoints); console.log(pc.green(`āœ… Found ${driftIssues.length} drift issues\n`)); // 4. Group by slice if requested let specs = []; let sliceGroups = []; if (options.perSlice !== false) { console.log(pc.cyan('šŸ“¦ Grouping contracts by slice...')); const sliceNames = detectSlices(graphDbPath); console.log(pc.green(`āœ… Detected ${sliceNames.length} slices\n`)); // Group endpoints and calls by slice const endpointsBySlice = groupEndpointsBySlice(endpoints); const callsBySlice = groupCallsBySlice(calls); const matchesBySlice = groupMatchesBySlice(matches); const driftBySlice = groupDriftBySlice(driftIssues); // Create slice groups sliceGroups = createSliceGroups(endpointsBySlice, callsBySlice, matchesBySlice, driftBySlice); // Generate specs per slice console.log(pc.cyan('šŸŽØ Generating OpenAPI specs per slice...')); for (const slice of sliceGroups) { const spec = generateOpenAPISpec(slice.name, slice.endpoints); slice.spec = spec; specs.push(spec); } console.log(pc.green(`āœ… Generated ${specs.length} OpenAPI specs\n`)); } else { // Generate single spec for all endpoints console.log(pc.cyan('šŸŽØ Generating OpenAPI spec...')); const spec = generateOpenAPISpec('api', endpoints); specs.push(spec); console.log(pc.green('āœ… Generated OpenAPI spec\n')); } // 5. Save specs console.log(pc.cyan('šŸ’¾ Saving OpenAPI specs...')); await saveSpecs(specs, options); console.log(pc.green(`āœ… Saved to ${options.outputDir || 'openapi/'}\n`)); // 6. Create report const duration = Date.now() - startTime; const report = { totalEndpoints: endpoints.length, totalCalls: calls.length, matchedCount: matches.length, unmatchedCalls, unmatchedEndpoints, driftIssues, slices: sliceGroups, specs, generatedAt: new Date().toISOString(), duration, }; // 7. Display results if (!options.driftOnly) { displayContractReport(report); } else { // Only show drift if (driftIssues.length > 0) { console.log(pc.bold(pc.red(`āŒ Schema Drift Detected (${driftIssues.length}):\n`))); for (const issue of driftIssues.slice(0, 5)) { console.log(` ${issue.message}`); } if (driftIssues.length > 5) { console.log(pc.gray(` ... and ${driftIssues.length - 5} more\n`)); } } else { console.log(pc.green('āœ… No drift issues detected!\n')); } } return report; } catch (error) { console.error(pc.red(`\nāŒ Contract generation failed: ${error.message}`)); throw error; } } /** * Find Graph DB path */ function findGraphDb(repoPaths) { // Try .arela/memory/graph.db in provided repo paths first const searchPaths = []; // Check each provided repo path for (const repoPath of repoPaths) { searchPaths.push(path.join(repoPath, '.arela/memory/graph.db')); } // Fallback to current directory searchPaths.push(path.join(process.cwd(), '.arela/memory/graph.db')); for (const p of searchPaths) { if (fs.existsSync(p)) { return p; } } return null; } /** * Save OpenAPI specs to files */ async function saveSpecs(specs, options) { const outputDir = options.outputDir || path.join(process.cwd(), 'openapi'); await fs.ensureDir(outputDir); const format = options.format || 'yaml'; for (const spec of specs) { const filename = `${spec.info.title.toLowerCase().replace(/\s+/g, '-')}.${format === 'json' ? 'json' : 'yaml'}`; const filepath = path.join(outputDir, filename); const content = format === 'json' ? specToJson(spec) : specToYaml(spec); await fs.writeFile(filepath, content); console.log(pc.gray(` šŸ“ ${filename}`)); } } // Export individual modules for advanced usage export * from './endpoint-extractor.js'; export * from './call-extractor.js'; export * from './matcher.js'; export * from './drift-detector.js'; export * from './openapi-generator.js'; export * from './slice-grouper.js'; export * from './reporter.js'; //# sourceMappingURL=index.js.map