#!/usr/bin/env node import fs from 'fs-extra'; import mustache from 'mustache'; import path from 'path'; import { program } from 'commander'; import { log, logSuccess, logWarning, logError, logDetail, initGenerationLog, colors } from './src/utils/logger'; import { checkOpenApiGenerator, installOpenApiGenerator } from './src/utils/openapi-generator'; import { createDirectoryStructure, cleanup } from './src/utils/filesystem'; import { analyzeSwagger } from './src/swagger/analyzer'; import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator'; import { generateCleanArchitecture, extractTagsWithOperations, buildTagsMapFromAnalysis, buildSchemaTagMap } from './src/generators/clean-arch.generator'; import { generateReport } from './src/generators/report.generator'; import { lintGeneratedFiles } from './src/generators/lint.generator'; import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder'; import { getExampleMismatches, clearExampleMismatches } from './src/utils/example-validator'; import { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt'; import { loadConfig, generateDefaultConfig, writeConfig, deriveSelectionFilter, deriveTagApiKeyMap } from './src/utils/config'; import type { SelectionFilter, LintResult } from './src/types'; import type { CliOptions } from './src/types'; import packageJson from './package.json'; // Disable HTML escaping so that < and > produce valid TypeScript generic types. (mustache as { escape: (text: string) => string }).escape = function (text: string): string { return text; }; // ── CLI CONFIGURATION ──────────────────────────────────────────────────────── program .name('generate-clean-arch') .description('Angular Clean Architecture code generator from OpenAPI/Swagger') .version(packageJson.version) .option('-i, --input ', 'OpenAPI/Swagger file (yaml or json)', 'swagger.yaml') .option('-o, --output ', 'Output directory', './src/app') .option('-t, --templates ', 'Custom templates directory', path.join(__dirname, 'templates')) .option('--skip-install', 'Skip dependency installation') .option('--dry-run', 'Simulate without generating files') .option('--skip-lint', 'Skip post-generation linting and formatting') .option('-s, --select-endpoints', 'Interactively select which tags and endpoints to generate') .option('-c, --config ', 'Use a JSON configuration file (skips interactive prompts)') .option('--init-config [file]', 'Generate a JSON configuration file instead of generating code') .parse(process.argv); const options = program.opts(); // ── MAIN ORCHESTRATOR ──────────────────────────────────────────────────────── async function main(): Promise { console.log('\n' + '='.repeat(60)); log(' OpenAPI Clean Architecture Generator', 'bright'); log(' Angular + Clean Architecture Code Generator', 'cyan'); console.log('='.repeat(60) + '\n'); const logPath = path.join(process.cwd(), 'generation.log'); initGenerationLog(logPath); // ── CONFIG FILE: override CLI defaults with config values ───────────────── const configFile = options.config; const generationConfig = configFile ? loadConfig(configFile) : undefined; if (generationConfig) { if (generationConfig.input) options.input = generationConfig.input; if (generationConfig.output) options.output = generationConfig.output; if (generationConfig.templates) options.templates = generationConfig.templates; if (generationConfig.skipInstall !== undefined) options.skipInstall = generationConfig.skipInstall; if (generationConfig.skipLint !== undefined) options.skipLint = generationConfig.skipLint; logDetail('config', `Using configuration file: ${configFile}`); } logDetail('config', `Input: ${options.input}`); logDetail('config', `Output: ${options.output}`); logDetail('config', `Templates: ${options.templates}`); if (!fs.existsSync(options.input)) { logError(`File not found: ${options.input}`); process.exit(1); } if (options.dryRun) { logWarning('DRY RUN mode — no files will be generated'); } if (!checkOpenApiGenerator()) { logWarning('OpenAPI Generator CLI not found'); if (!options.skipInstall) { installOpenApiGenerator(); } else { logError( 'Install openapi-generator-cli with: npm install -g @openapitools/openapi-generator-cli' ); process.exit(1); } } else { logSuccess('OpenAPI Generator CLI found'); } const analysis = analyzeSwagger(options.input); const tagSummaries = extractTagsWithOperations(analysis); // ── INIT CONFIG MODE: generate config file and exit ─────────────────────── if (options.initConfig !== undefined) { const envFile = findEnvironmentFile(process.cwd()); let apiKeys: ReturnType = []; if (envFile) { const envContent = fs.readFileSync(envFile, 'utf8'); apiKeys = parseApiKeys(envContent); } const defaultConfig = generateDefaultConfig(analysis, tagSummaries, options, apiKeys); const outputFile = typeof options.initConfig === 'string' ? options.initConfig : 'generation-config.json'; writeConfig(defaultConfig, outputFile); logSuccess(`Configuration file generated: ${outputFile}`); logDetail( 'config', 'Edit the file to customise tags, endpoints and baseUrls, then run with --config' ); return; } if (options.dryRun) { logWarning('Finishing in DRY RUN mode'); return; } createDirectoryStructure(options.output); clearExampleMismatches(); // ── SELECTION: tags and endpoints ───────────────────────────────────────── let selectionFilter: SelectionFilter = {}; let tagApiKeyMap: Record; if (generationConfig) { // Config-driven: derive everything from the JSON file selectionFilter = deriveSelectionFilter(generationConfig); tagApiKeyMap = deriveTagApiKeyMap(generationConfig); logDetail('config', `Tags from config: ${Object.keys(generationConfig.tags).join(', ')}`); Object.entries(tagApiKeyMap).forEach(([tag, key]) => { logDetail('config', `API key for "${tag}": environment.${key}.url`); }); } else { // Interactive mode (original behaviour) if (options.selectEndpoints) { selectionFilter = await askSelectionFilter(tagSummaries); } const selectedTags = options.selectEndpoints ? Object.keys(selectionFilter) : tagSummaries.map((t) => t.tag); // ── ENVIRONMENT API KEY SELECTION ──────────────────────────────────────── const envFile = findEnvironmentFile(process.cwd()); let apiKeys: ReturnType = []; if (envFile) { const envContent = fs.readFileSync(envFile, 'utf8'); apiKeys = parseApiKeys(envContent); logSuccess( `environment.ts found: ${colors.cyan}${path.relative(process.cwd(), envFile)}${colors.reset}` ); if (apiKeys.length === 0) { logWarning('No keys containing "api" found in environment.ts. Will be requested manually.'); } } else { logWarning('No environment.ts found. The key will be requested manually per repository.'); } tagApiKeyMap = await askApiKeysForTags(selectedTags, apiKeys); Object.entries(tagApiKeyMap).forEach(([tag, key]) => { logDetail('config', `API key for "${tag}": environment.${key}.url`); }); } // ────────────────────────────────────────────────────────────────────────── const tempDir = generateCode(options.input, options.templates); // Compute schema→tag map before organizeFiles so DTOs land in the right subfolder const tagsMapForSchema = buildTagsMapFromAnalysis(analysis, selectionFilter); const schemaTagMap = buildSchemaTagMap( (analysis.swagger as { components?: { schemas?: Record } }).components ?.schemas || {}, tagsMapForSchema ); organizeFiles(tempDir, options.output, schemaTagMap); addDtoImports(options.output); generateCleanArchitecture( analysis, options.output, options.templates, tagApiKeyMap, selectionFilter, schemaTagMap ); cleanup(tempDir); // ── EXAMPLE/TYPE MISMATCH WARNINGS ───────────────────────────────────────── const mismatches = getExampleMismatches(); if (mismatches.length > 0) { console.log(''); logWarning(`${mismatches.length} example/type mismatch(es) detected in OpenAPI schemas:`); for (const m of mismatches) { const action = m.action === 'coerced' ? `→ coerced to ${JSON.stringify(m.coercedValue)}` : '→ example ignored, using type default'; logWarning( ` ${m.schemaName}.${m.propertyName}: type '${m.declaredType}' but example is ${m.exampleJsType} (${JSON.stringify(m.exampleValue)}) ${action}` ); logDetail( 'VALIDATE', `${m.schemaName}.${m.propertyName}: declared=${m.declaredType} example=${JSON.stringify(m.exampleValue)} (${m.exampleJsType}) action=${m.action}` ); } } const noLintResult: LintResult = { prettier: { ran: false, filesFormatted: 0 }, eslint: { ran: false, filesFixed: 0 } }; const lintResult = options.skipLint ? noLintResult : lintGeneratedFiles(options.output); const report = generateReport(options.output, analysis, lintResult); console.log('\n' + '='.repeat(60)); log(' ✨ Generation completed successfully', 'green'); console.log('='.repeat(60)); console.log(`\n📊 Summary:`); console.log(` - DTOs generated: ${report.structure.dtos}`); console.log(` - Repositories: ${report.structure.repositories}`); console.log(` - Mappers: ${report.structure.mappers}`); console.log(` - Use Cases: ${report.structure.useCases}`); console.log(` - Providers: ${report.structure.providers}`); console.log(` - Mocks: ${report.structure.mocks}`); if (report.warnings.total > 0) { console.log( `\n ${colors.yellow}⚠️ ${report.warnings.total} example/type mismatch(es) (see above)${colors.reset}` ); } console.log(`\n📁 Files generated in: ${colors.cyan}${options.output}${colors.reset}\n`); } main().catch((error: unknown) => { const err = error as Error; logError(`Fatal error: ${err.message}`); console.error(error); process.exit(1); });