diff --git a/main.ts b/main.ts index 6379041..b45e938 100755 --- a/main.ts +++ b/main.ts @@ -10,9 +10,12 @@ import { checkOpenApiGenerator, installOpenApiGenerator } from './src/utils/open import { createDirectoryStructure, cleanup } from './src/utils/filesystem'; import { analyzeSwagger } from './src/swagger/analyzer'; import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator'; -import { generateCleanArchitecture } from './src/generators/clean-arch.generator'; +import { generateCleanArchitecture, extractTagsFromAnalysis } from './src/generators/clean-arch.generator'; import { generateReport } from './src/generators/report.generator'; +import { findEnvironmentFile, parseApiKeys } from './src/utils/environment-finder'; +import { askApiKeysForTags } from './src/utils/prompt'; import type { CliOptions } from './src/types'; +import packageJson from './package.json'; // Desactivar escape HTML para que los literales < y > generen tipos genéricos válidos de TS. (mustache as { escape: (text: string) => string }).escape = function (text: string): string { @@ -24,7 +27,7 @@ import type { CliOptions } from './src/types'; program .name('generate-clean-arch') .description('Generador de código Angular con Clean Architecture desde OpenAPI/Swagger') - .version('1.0.0') + .version(packageJson.version) .option('-i, --input ', 'Archivo OpenAPI/Swagger (yaml o json)', 'swagger.yaml') .option('-o, --output ', 'Directorio de salida', './src/app') .option( @@ -82,10 +85,31 @@ async function main(): Promise { createDirectoryStructure(options.output); + // ── ENVIRONMENT API KEY SELECTION ────────────────────────────────────────── + const tags = extractTagsFromAnalysis(analysis); + const envFile = findEnvironmentFile(process.cwd()); + let apiKeys: ReturnType = []; + + if (envFile) { + const envContent = fs.readFileSync(envFile, 'utf8'); + apiKeys = parseApiKeys(envContent); + logSuccess(`environment.ts encontrado: ${colors.cyan}${path.relative(process.cwd(), envFile)}${colors.reset}`); + if (apiKeys.length > 0) { + logInfo(`Claves de API detectadas: ${apiKeys.map((k) => k.key).join(', ')}`); + } else { + logWarning('No se encontraron claves con "api" en environment.ts. Se solicitará manualmente.'); + } + } else { + logWarning('No se encontró environment.ts. Se solicitará la clave manualmente por repositorio.'); + } + + const tagApiKeyMap = await askApiKeysForTags(tags, apiKeys); + // ────────────────────────────────────────────────────────────────────────── + const tempDir = generateCode(options.input, options.templates); organizeFiles(tempDir, options.output); addDtoImports(options.output); - generateCleanArchitecture(analysis, options.output, options.templates); + generateCleanArchitecture(analysis, options.output, options.templates, tagApiKeyMap); cleanup(tempDir); const report = generateReport(options.output, analysis); diff --git a/package-lock.json b/package-lock.json index 30f5482..020f844 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "openapi-clean-arch-generator", + "name": "@blas/openapi-clean-arch-generator", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "openapi-clean-arch-generator", + "name": "@blas/openapi-clean-arch-generator", "version": "1.0.0", "license": "MIT", "dependencies": { @@ -13,10 +13,11 @@ "commander": "^11.1.0", "fs-extra": "^11.2.0", "js-yaml": "^4.1.0", - "mustache": "^4.2.0" + "mustache": "^4.2.0", + "prompts": "^2.4.2" }, "bin": { - "generate-clean-arch": "dist/generate.js" + "generate-clean-arch": "dist/main.js" }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -24,6 +25,7 @@ "@types/js-yaml": "^4.0.9", "@types/mustache": "^4.2.6", "@types/node": "^25.5.0", + "@types/prompts": "^2.4.9", "@typescript-eslint/eslint-plugin": "^8.57.1", "@typescript-eslint/parser": "^8.57.1", "eslint": "^10.1.0", @@ -354,6 +356,17 @@ "undici-types": "~7.18.0" } }, + "node_modules/@types/prompts": { + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", + "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "kleur": "^3.0.3" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.2", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", @@ -1293,6 +1306,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1491,6 +1513,19 @@ "node": ">=6.0.0" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -1537,6 +1572,12 @@ "node": ">=8" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index 764edff..561b9f4 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "commander": "^11.1.0", "fs-extra": "^11.2.0", "js-yaml": "^4.1.0", - "mustache": "^4.2.0" + "mustache": "^4.2.0", + "prompts": "^2.4.2" }, "engines": { "node": ">=14.0.0" @@ -52,6 +53,7 @@ "@types/js-yaml": "^4.0.9", "@types/mustache": "^4.2.6", "@types/node": "^25.5.0", + "@types/prompts": "^2.4.9", "@typescript-eslint/eslint-plugin": "^8.57.1", "@typescript-eslint/parser": "^8.57.1", "eslint": "^10.1.0", diff --git a/src/generators/clean-arch.generator.ts b/src/generators/clean-arch.generator.ts index 17c09f2..8347087 100644 --- a/src/generators/clean-arch.generator.ts +++ b/src/generators/clean-arch.generator.ts @@ -12,11 +12,34 @@ import type { GeneratedCount } from '../types'; +/** + * Extracts the unique tags (in order of appearance) from a SwaggerAnalysis. + * Only endpoints that have at least one tag are considered; the first tag is used. + */ +export function extractTagsFromAnalysis(analysis: SwaggerAnalysis): string[] { + const seen = new Set(); + const tags: string[] = []; + Object.values(analysis.paths).forEach((pathObj) => { + Object.values(pathObj as Record).forEach((opRaw) => { + const op = opRaw as OpenApiOperation; + if (op.tags && op.tags.length > 0) { + const tag = op.tags[0]; + if (!seen.has(tag)) { + seen.add(tag); + tags.push(tag); + } + } + }); + }); + return tags; +} + /** Genera todos los artefactos de Clean Architecture (modelos, mappers, repos, use cases, providers) usando Mustache. */ export function generateCleanArchitecture( analysis: SwaggerAnalysis, outputDir: string, - templatesDir: string + templatesDir: string, + tagApiKeyMap: Record = {} ): GeneratedCount { logStep('Generando artefactos de Clean Architecture usando Mustache...'); const generatedCount: GeneratedCount = { @@ -236,7 +259,9 @@ export function generateCleanArchitecture( // Return-type-only imports — for repo impl (Dto + Entity + Mapper) returnImports, // Param-only imports — for repo impl (Entity only, no Dto/Mapper) - paramImports + paramImports, + // Environment API key for the repository base URL (e.g. "aprovalmApi") + environmentApiKey: tagApiKeyMap[tag] || 'apiUrl' } } ] diff --git a/src/utils/environment-finder.ts b/src/utils/environment-finder.ts new file mode 100644 index 0000000..d40f85b --- /dev/null +++ b/src/utils/environment-finder.ts @@ -0,0 +1,47 @@ +import fs from 'fs-extra'; +import path from 'path'; + +export interface ApiKeyInfo { + key: string; + url?: string; +} + +const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', '.angular', 'coverage', '.cache']); + +export function findEnvironmentFile(dir: string, maxDepth = 8, currentDepth = 0): string | null { + if (currentDepth > maxDepth) return null; + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (SKIP_DIRS.has(entry.name)) continue; + const fullPath = path.join(dir, entry.name); + if (entry.isFile() && entry.name === 'environment.ts') { + return fullPath; + } + if (entry.isDirectory()) { + const found = findEnvironmentFile(fullPath, maxDepth, currentDepth + 1); + if (found) return found; + } + } + } catch {} + return null; +} + +/** + * Parses environment.ts content and returns all top-level keys that contain "api" + * (case-insensitive), along with their `url` value if present. + */ +export function parseApiKeys(content: string): ApiKeyInfo[] { + const result: ApiKeyInfo[] = []; + const keyRegex = /^ {2}(\w*[Aa][Pp][Ii]\w*)\s*:/gm; + let match: RegExpExecArray | null; + + while ((match = keyRegex.exec(content)) !== null) { + const key = match[1]; + const afterKey = content.slice(match.index); + const urlMatch = afterKey.match(/url:\s*['"`]([^'"`\n]+)['"`]/); + result.push({ key, url: urlMatch?.[1] }); + } + + return result; +} diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts new file mode 100644 index 0000000..e570ddd --- /dev/null +++ b/src/utils/prompt.ts @@ -0,0 +1,118 @@ +import prompts from 'prompts'; +import { ApiKeyInfo } from './environment-finder'; +import { colors } from './logger'; + +function clearScreen(): void { + process.stdout.write('\x1Bc'); +} + +function printHeader(current?: number, total?: number): void { + const stepText = + current !== undefined && total !== undefined + ? ` [${colors.cyan}${current}${colors.reset} de ${colors.cyan}${total}${colors.reset}]` + : ''; + console.log(`\n ${colors.bright}🔑 Configuración de URLs base${colors.reset}${stepText}`); + console.log(` ${'─'.repeat(54)}\n`); +} + +function printSummary(tags: string[], result: Record): void { + clearScreen(); + console.log(`\n ${colors.bright}✅ Configuración completada${colors.reset}`); + console.log(` ${'─'.repeat(54)}\n`); + tags.forEach((tag) => { + console.log(` ${colors.bright}${tag}${colors.reset}`); + console.log(` ${colors.cyan}environment.${result[tag]}.url${colors.reset}\n`); + }); + console.log(` ${'─'.repeat(54)}\n`); +} + +/** + * Interactively asks the user which environment API key to use for each tag, + * using arrow-key selection. The last option always allows typing manually. + * Returns a map of tag → environment key (e.g. { "SupplyingMaintenances": "suppliyingMaintenancesApi" }). + */ +export async function askApiKeysForTags( + tags: string[], + apiKeys: ApiKeyInfo[] +): Promise> { + if (tags.length === 0) return {}; + + clearScreen(); + printHeader(); + + const modeResponse = await prompts({ + type: 'select', + name: 'mode', + message: 'URL base para los repositorios', + choices: [ + { title: `${colors.bright}La misma para todos${colors.reset}`, value: 'all' }, + { title: `${colors.bright}Configurar individualmente${colors.reset}`, value: 'individual' } + ], + hint: ' ' + }); + + if (modeResponse.mode === undefined) process.exit(0); + + const result: Record = {}; + + if (modeResponse.mode === 'all') { + clearScreen(); + printHeader(); + const sharedKey = await askApiKeyForTag('todos los repositorios', apiKeys); + tags.forEach((tag) => (result[tag] = sharedKey)); + } else { + for (let i = 0; i < tags.length; i++) { + clearScreen(); + printHeader(i + 1, tags.length); + result[tags[i]] = await askApiKeyForTag(tags[i], apiKeys); + } + } + + printSummary(tags, result); + return result; +} + +async function askApiKeyForTag(tagName: string, apiKeys: ApiKeyInfo[]): Promise { + const MANUAL_VALUE = '__manual__'; + + const choices = [ + ...apiKeys.map((k) => ({ + title: k.url + ? `${colors.bright}${k.key}${colors.reset}\n ${colors.cyan}↳ ${k.url}${colors.reset}` + : `${colors.bright}${k.key}${colors.reset}`, + value: k.key + })), + { + title: `${colors.bright}Escribir manualmente${colors.reset}`, + value: MANUAL_VALUE + } + ]; + + const selectResponse = await prompts({ + type: 'select', + name: 'key', + message: `Repositorio ${colors.bright}${tagName}${colors.reset}`, + choices, + hint: ' ' + }); + + if (selectResponse.key === undefined) process.exit(0); + + if (selectResponse.key !== MANUAL_VALUE) { + return selectResponse.key as string; + } + + console.log(); + + const textResponse = await prompts({ + type: 'text', + name: 'key', + message: `Clave de environment`, + hint: 'ej: aprovalmApi', + validate: (v: string) => v.trim().length > 0 || 'La clave no puede estar vacía' + }); + + if (textResponse.key === undefined) process.exit(0); + + return (textResponse.key as string).trim(); +} diff --git a/templates/api.repository.impl.mustache b/templates/api.repository.impl.mustache index 80bc23b..e40cb8a 100644 --- a/templates/api.repository.impl.mustache +++ b/templates/api.repository.impl.mustache @@ -26,7 +26,7 @@ import { {{classname}} } from '@/entities/models/{{classFilename}}.model'; @Injectable() export class {{classname}}RepositoryImpl extends MRepository implements {{classname}}Repository { constructor() { - super(`${environment.modapApi.url}`); + super(`${environment.{{environmentApiKey}}.url}`); } {{#operation}}