diff --git a/main.ts b/main.ts index 636dd94..a0eefaf 100755 --- a/main.ts +++ b/main.ts @@ -12,11 +12,12 @@ import { analyzeSwagger } from './src/swagger/analyzer'; import { generateCode, organizeFiles, addDtoImports } from './src/generators/dto.generator'; import { generateCleanArchitecture, - extractTagsFromAnalysis + extractTagsWithOperations } 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 { askApiKeysForTags, askSelectionFilter } from './src/utils/prompt'; +import type { SelectionFilter } from './src/types'; import type { CliOptions } from './src/types'; import packageJson from './package.json'; @@ -40,6 +41,7 @@ program ) .option('--skip-install', 'No instalar dependencias') .option('--dry-run', 'Simular sin generar archivos') + .option('-s, --select-endpoints', 'Seleccionar interactivamente qué tags y endpoints generar') .parse(process.argv); const options = program.opts(); @@ -88,8 +90,19 @@ async function main(): Promise { createDirectoryStructure(options.output); + // ── SELECTION: tags and endpoints ───────────────────────────────────────── + const tagSummaries = extractTagsWithOperations(analysis); + let selectionFilter: SelectionFilter = {}; + + if (options.selectEndpoints) { + selectionFilter = await askSelectionFilter(tagSummaries); + } + + const selectedTags = options.selectEndpoints + ? Object.keys(selectionFilter) + : tagSummaries.map((t) => t.tag); + // ── ENVIRONMENT API KEY SELECTION ────────────────────────────────────────── - const tags = extractTagsFromAnalysis(analysis); const envFile = findEnvironmentFile(process.cwd()); let apiKeys: ReturnType = []; @@ -112,13 +125,13 @@ async function main(): Promise { ); } - const tagApiKeyMap = await askApiKeysForTags(tags, apiKeys); + const 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); + generateCleanArchitecture(analysis, options.output, options.templates, tagApiKeyMap, selectionFilter); cleanup(tempDir); const report = generateReport(options.output, analysis); diff --git a/package.json b/package.json index 561b9f4..8b98857 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@blas/openapi-clean-arch-generator", - "version": "1.0.0", + "version": "1.0.1", "description": "Generador de código Angular con Clean Architecture desde OpenAPI/Swagger", "main": "dist/main.js", "bin": { diff --git a/src/generators/clean-arch.generator.ts b/src/generators/clean-arch.generator.ts index 8347087..50726c3 100644 --- a/src/generators/clean-arch.generator.ts +++ b/src/generators/clean-arch.generator.ts @@ -9,6 +9,8 @@ import type { OpenApiSchema, OpenApiOperation, TagOperation, + TagSummary, + SelectionFilter, GeneratedCount } from '../types'; @@ -34,12 +36,35 @@ export function extractTagsFromAnalysis(analysis: SwaggerAnalysis): string[] { return tags; } +/** + * Extracts all tags with their operations summary for the interactive selection screen. + */ +export function extractTagsWithOperations(analysis: SwaggerAnalysis): TagSummary[] { + const map = new Map(); + Object.entries(analysis.paths).forEach(([pathKey, pathObj]) => { + Object.entries(pathObj as Record).forEach(([method, opRaw]) => { + const op = opRaw as OpenApiOperation; + if (!op.tags?.length) return; + const tag = op.tags[0]; + if (!map.has(tag)) map.set(tag, { tag, operations: [] }); + map.get(tag)!.operations.push({ + nickname: op.operationId || `${method}${pathKey.replace(/\//g, '_')}`, + method: method.toUpperCase(), + path: pathKey, + summary: op.summary || '' + }); + }); + }); + return [...map.values()]; +} + /** Genera todos los artefactos de Clean Architecture (modelos, mappers, repos, use cases, providers) usando Mustache. */ export function generateCleanArchitecture( analysis: SwaggerAnalysis, outputDir: string, templatesDir: string, - tagApiKeyMap: Record = {} + tagApiKeyMap: Record = {}, + selectionFilter: SelectionFilter = {} ): GeneratedCount { logStep('Generando artefactos de Clean Architecture usando Mustache...'); const generatedCount: GeneratedCount = { @@ -221,6 +246,17 @@ export function generateCleanArchitecture( }); }); + if (Object.keys(selectionFilter).length > 0) { + Object.keys(tagsMap).forEach((tag) => { + if (!selectionFilter[tag]) { + delete tagsMap[tag]; + } else { + tagsMap[tag] = tagsMap[tag].filter((op) => selectionFilter[tag].includes(op.nickname)); + if (tagsMap[tag].length === 0) delete tagsMap[tag]; + } + }); + } + // Generar por cada Tag Object.keys(tagsMap).forEach((tag) => { const returnImports: { classname: string; classFilename: string; classVarName: string }[] = []; diff --git a/src/types/cli.types.ts b/src/types/cli.types.ts index be76cb3..53ea263 100644 --- a/src/types/cli.types.ts +++ b/src/types/cli.types.ts @@ -8,4 +8,5 @@ export interface CliOptions { templates: string; skipInstall?: boolean; dryRun?: boolean; + selectEndpoints?: boolean; } diff --git a/src/types/openapi.types.ts b/src/types/openapi.types.ts index 1ff7a85..c5cf082 100644 --- a/src/types/openapi.types.ts +++ b/src/types/openapi.types.ts @@ -1,3 +1,26 @@ +/** + * Resumen de un endpoint para mostrar en la pantalla de selección interactiva. + */ +export interface OperationSummary { + nickname: string; + method: string; + path: string; + summary: string; +} + +/** + * Tag con sus endpoints resumidos, para la pantalla de selección interactiva. + */ +export interface TagSummary { + tag: string; + operations: OperationSummary[]; +} + +/** + * Mapa de filtro de selección: tag → array de nicknames de operaciones seleccionadas. + */ +export type SelectionFilter = Record; + /** * Representación simplificada de un schema de componente OpenAPI. * Se utiliza para generar modelos (entidades) y mappers. diff --git a/src/utils/prompt.ts b/src/utils/prompt.ts index e570ddd..7a75d9a 100644 --- a/src/utils/prompt.ts +++ b/src/utils/prompt.ts @@ -1,6 +1,7 @@ import prompts from 'prompts'; import { ApiKeyInfo } from './environment-finder'; import { colors } from './logger'; +import type { TagSummary, SelectionFilter } from '../types'; function clearScreen(): void { process.stdout.write('\x1Bc'); @@ -26,6 +27,70 @@ function printSummary(tags: string[], result: Record): void { console.log(` ${'─'.repeat(54)}\n`); } +/** + * Interactively asks the user which tags and endpoints to generate. + * Returns a SelectionFilter map of tag → selected operation nicknames. + */ +export async function askSelectionFilter(tagSummaries: TagSummary[]): Promise { + if (tagSummaries.length === 0) return {}; + + clearScreen(); + console.log(`\n ${colors.bright}📋 Selección de tags y endpoints${colors.reset}`); + console.log(` ${'─'.repeat(54)}\n`); + + // Step 1: select tags + const tagResponse = await prompts({ + type: 'multiselect', + name: 'tags', + message: 'Tags a generar', + choices: tagSummaries.map((t) => ({ + title: `${colors.bright}${t.tag}${colors.reset} ${colors.cyan}(${t.operations.length} endpoint${t.operations.length !== 1 ? 's' : ''})${colors.reset}`, + value: t.tag, + selected: true + })), + min: 1, + hint: 'Espacio para marcar/desmarcar, Enter para confirmar' + }); + + if (!tagResponse.tags?.length) process.exit(0); + + const selectedTags: string[] = tagResponse.tags; + const filter: SelectionFilter = {}; + + // Step 2: for each selected tag, select endpoints + for (let i = 0; i < selectedTags.length; i++) { + const tag = selectedTags[i]; + const summary = tagSummaries.find((t) => t.tag === tag)!; + + clearScreen(); + console.log( + `\n ${colors.bright}📋 Endpoints a generar${colors.reset} [${colors.cyan}${i + 1}${colors.reset} de ${colors.cyan}${selectedTags.length}${colors.reset}]` + ); + console.log(` ${'─'.repeat(54)}\n`); + + const opResponse = await prompts({ + type: 'multiselect', + name: 'ops', + message: `Tag ${colors.bright}${tag}${colors.reset}`, + choices: summary.operations.map((op) => ({ + title: + `${colors.bright}${op.method.padEnd(6)}${colors.reset} ${op.path}` + + (op.summary ? ` ${colors.cyan}${op.summary}${colors.reset}` : ''), + value: op.nickname, + selected: true + })), + min: 1, + hint: 'Espacio para marcar/desmarcar, Enter para confirmar' + }); + + if (!opResponse.ops?.length) process.exit(0); + + filter[tag] = opResponse.ops; + } + + return filter; +} + /** * Interactively asks the user which environment API key to use for each tag, * using arrow-key selection. The last option always allows typing manually.