#!/usr/bin/env node import fs from 'fs-extra'; import mustache from 'mustache'; import path from 'path'; import { program } from 'commander'; import { log, logSuccess, logInfo, logWarning, logError, 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 } 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 { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt'; import { loadConfig, generateDefaultConfig, writeConfig, deriveSelectionFilter, deriveTagApiKeyMap } from './src/utils/config'; import type { SelectionFilter } 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'); // ── 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; logInfo(`Using configuration file: ${configFile}`); } if (!fs.existsSync(options.input)) { logError(`File not found: ${options.input}`); process.exit(1); } logInfo(`Input file: ${options.input}`); logInfo(`Output directory: ${options.output}`); logInfo(`Templates: ${options.templates}`); 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}`); logInfo('Edit the file to customise tags, endpoints and baseUrls, then run with --config'); return; } if (options.dryRun) { logInfo('Finishing in DRY RUN mode'); return; } createDirectoryStructure(options.output); // ── 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); logInfo(`Tags from config: ${Object.keys(generationConfig.tags).join(', ')}`); } 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) { logInfo(`Detected API keys: ${apiKeys.map((k) => k.key).join(', ')}`); } else { 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); } // ────────────────────────────────────────────────────────────────────────── const tempDir = generateCode(options.input, options.templates); organizeFiles(tempDir, options.output); addDtoImports(options.output); generateCleanArchitecture( analysis, options.output, options.templates, tagApiKeyMap, selectionFilter ); cleanup(tempDir); if (!options.skipLint) { lintGeneratedFiles(options.output); } const report = generateReport(options.output, analysis); 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}`); 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); });