novatec-cli 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/prompts.js CHANGED
@@ -1,273 +1,166 @@
1
1
  import inquirer from 'inquirer';
2
2
  import chalk from 'chalk';
3
+ import fse from 'fs-extra';
4
+ import path from 'path';
5
+ import boxen from 'boxen';
6
+
7
+ // ── PRESETS ───────────────────────────────────────────────────────────────────
8
+ export const PRESETS = {
9
+ 'react-express': { frontend: 'react', backend: 'express', db: 'postgres', typescript: false },
10
+ 'next-nestjs': { frontend: 'next', backend: 'nestjs', db: 'postgres', typescript: true },
11
+ 'vue-fastapi': { frontend: 'vue', backend: 'fastapi', db: 'postgres', typescript: false },
12
+ 'nuxt-express': { frontend: 'nuxt', backend: 'express', db: 'mongo', typescript: false },
13
+ 'astro-hono': { frontend: 'astro', backend: 'hono', db: 'sqlite', typescript: true },
14
+ };
3
15
 
4
16
  // ── FRONTEND FRAMEWORKS ───────────────────────────────────────────────────────
5
17
  export const FRONTEND_CHOICES = [
6
- { name: 'React (Vite) — SPA rápida con React 18', value: 'react' },
7
- { name: '🔺 Next.js React SSR/SSG/App Router', value: 'next' },
8
- { name: '💚 Vue 3 — Vite + Composition API', value: 'vue' },
9
- { name: '🔷 Nuxt 3 — Vue con SSR/SSG', value: 'nuxt' },
10
- { name: '🚀 Astro — MPA ultra-rápido', value: 'astro' },
11
- { name: '🔥 SvelteKit — Svelte con SSR/SSG', value: 'svelte' },
12
- { name: '💎 SolidJS Reactividad sin Virtual DOM', value: 'solid' },
13
- { name: '🅰 Angular — Framework empresarial', value: 'angular' },
14
- { name: '🌐 Remix — Full-stack React con loaders', value: 'remix' },
15
- { name: 'Qwik — Resumability, carga instantánea', value: 'qwik' },
18
+ { name: 'React Vite + React 18', value: 'react' },
19
+ { name: 'Next.js — SSR/SSG App Router', value: 'next' },
20
+ { name: 'Vue 3 — Vite + Composition API', value: 'vue' },
21
+ { name: 'Nuxt 3 — Vue con SSR/SSG', value: 'nuxt' },
22
+ { name: 'Astro — MPA ultra-rápido', value: 'astro' },
23
+ { name: 'SvelteKit — Svelte con SSR/SSG', value: 'svelte' },
24
+ { name: 'SolidJS Sin Virtual DOM', value: 'solid' },
25
+ { name: 'Angular — Framework empresarial', value: 'angular' },
26
+ { name: 'Remix — Full-stack React', value: 'remix' },
27
+ { name: 'Qwik — Resumability', value: 'qwik' },
16
28
  ];
17
29
 
18
30
  // ── BACKEND FRAMEWORKS ────────────────────────────────────────────────────────
19
31
  export const BACKEND_CHOICES = [
20
- { name: '🟢 Express — Minimalista (Node.js)', value: 'express' },
21
- { name: '🔵 NestJS Arquitectura modular', value: 'nestjs' },
22
- { name: 'Fastify — El más rápido (Node)', value: 'fastify' },
23
- { name: '🔥 Hono — Edge-ready ligero', value: 'hono' },
24
- { name: '🐍 FastAPI — Async Python alto rend', value: 'fastapi' },
25
- { name: '🌶 Django — Baterías incluidas', value: 'django' },
26
- { name: '🍶 Flask — Micro-framework Python', value: 'flask' },
27
- { name: 'Spring Boot — Empresarial (Java)', value: 'spring' },
28
- { name: '🦕 Deno/Oak Seguro (TypeScript)', value: 'deno' },
29
- { name: '🐹 Go/Gin — Rendimiento extremo', value: 'gin' },
32
+ { name: 'Express — Minimalista (Node.js)', value: 'express' },
33
+ { name: 'NestJS Modular TypeScript', value: 'nestjs' },
34
+ { name: 'Fastify — El más rápido (Node)', value: 'fastify' },
35
+ { name: 'Hono — Edge-ready ligero', value: 'hono' },
36
+ { name: 'FastAPI — Async Python', value: 'fastapi' },
37
+ { name: 'Django — Baterías incluidas', value: 'django' },
38
+ { name: 'Flask — Micro-framework Python', value: 'flask' },
39
+ { name: 'Spring Boot — Empresarial Java', value: 'spring' },
40
+ { name: 'Deno/Oak TypeScript seguro', value: 'deno' },
41
+ { name: 'Go/Gin — Rendimiento extremo', value: 'gin' },
42
+ ];
43
+
44
+ // ── BASES DE DATOS ────────────────────────────────────────────────────────────
45
+ export const DB_CHOICES = [
46
+ { name: 'PostgreSQL — Relacional robusto', value: 'postgres' },
47
+ { name: 'MySQL — Relacional popular', value: 'mysql' },
48
+ { name: 'MongoDB — NoSQL documentos', value: 'mongo' },
49
+ { name: 'SQLite — Embebido ligero', value: 'sqlite' },
50
+ { name: 'Ninguna — Sin base de datos', value: 'none' },
30
51
  ];
31
52
 
32
- // ── DEPENDENCIAS POR FRONTEND ─────────────────────────────────────────────────
53
+ // ── DEPENDENCIAS FRONTEND ─────────────────────────────────────────────────────
33
54
  export const FRONTEND_DEPS = {
34
- react: [
35
- { name: 'axios — HTTP client', value: 'axios' },
36
- { name: 'react-router-dom — Enrutamiento SPA', value: 'react-router-dom' },
37
- { name: 'zustand — Estado global ligero', value: 'zustand' },
38
- { name: 'jotai — Estado atómico', value: 'jotai' },
39
- { name: '@tanstack/react-query — Server state / fetching', value: '@tanstack/react-query' },
40
- { name: 'tailwindcss — CSS utility-first', value: 'tailwindcss' },
41
- { name: 'shadcn/ui — Componentes accesibles', value: '@shadcn/ui' },
42
- { name: 'framer-motion — Animaciones fluidas', value: 'framer-motion' },
43
- { name: 'react-hook-form — Formularios performantes', value: 'react-hook-form' },
44
- { name: 'zod — Validación de esquemas', value: 'zod' },
45
- { name: 'date-fns — Manejo de fechas', value: 'date-fns' },
46
- { name: 'recharts — Gráficas y dashboards', value: 'recharts' },
47
- { name: 'i18next — Internacionalización', value: 'i18next' },
48
- { name: 'dotenv — Variables de entorno', value: 'dotenv' },
49
- ],
50
- next: [
51
- { name: 'axios — HTTP client', value: 'axios' },
52
- { name: 'next-auth — Autenticación completa', value: 'next-auth' },
53
- { name: 'zustand — Estado global ligero', value: 'zustand' },
54
- { name: 'tailwindcss — CSS utility-first (incluido)', value: 'tailwindcss' },
55
- { name: 'prisma — ORM moderno', value: 'prisma' },
56
- { name: '@tanstack/react-query — Server state', value: '@tanstack/react-query' },
57
- { name: 'zod — Validación de esquemas', value: 'zod' },
58
- { name: 'react-hook-form — Formularios performantes', value: 'react-hook-form' },
59
- { name: 'shadcn/ui — Componentes accesibles', value: '@shadcn/ui' },
60
- { name: 'stripe — Pagos online', value: 'stripe' },
61
- { name: 'resend — Emails transaccionales', value: 'resend' },
62
- { name: 'uploadthing — Subida de archivos', value: 'uploadthing' },
63
- { name: 'date-fns — Manejo de fechas', value: 'date-fns' },
64
- ],
65
- vue: [
66
- { name: 'axios — HTTP client', value: 'axios' },
67
- { name: 'vue-router — Enrutamiento oficial', value: 'vue-router' },
68
- { name: 'pinia — Estado global oficial', value: 'pinia' },
69
- { name: 'tailwindcss — CSS utility-first', value: 'tailwindcss' },
70
- { name: 'vee-validate — Validación de formularios', value: 'vee-validate' },
71
- { name: 'zod — Validación de esquemas', value: 'zod' },
72
- { name: 'vueuse — Composables utilitarios', value: '@vueuse/core' },
73
- { name: 'naive-ui — Componentes UI', value: 'naive-ui' },
74
- { name: 'date-fns — Manejo de fechas', value: 'date-fns' },
75
- { name: 'i18n — Internacionalización', value: 'vue-i18n' },
76
- ],
77
- nuxt: [
78
- { name: 'axios / ofetch — HTTP client', value: 'ofetch' },
79
- { name: '@pinia/nuxt — Estado global', value: '@pinia/nuxt' },
80
- { name: '@nuxtjs/tailwindcss — Tailwind integrado', value: '@nuxtjs/tailwindcss' },
81
- { name: '@nuxt/image — Optimización de imágenes', value: '@nuxt/image' },
82
- { name: 'nuxt-auth-utils — Autenticación', value: 'nuxt-auth-utils' },
83
- { name: 'zod — Validación', value: 'zod' },
84
- { name: '@vueuse/nuxt — Composables', value: '@vueuse/nuxt' },
85
- ],
86
- astro: [
87
- { name: 'tailwindcss — CSS utility-first', value: '@astrojs/tailwind' },
88
- { name: '@astrojs/react — Integración React', value: '@astrojs/react' },
89
- { name: '@astrojs/vue — Integración Vue', value: '@astrojs/vue' },
90
- { name: '@astrojs/mdx — Soporte MDX', value: '@astrojs/mdx' },
91
- { name: '@astrojs/image — Optimización imágenes', value: '@astrojs/image' },
92
- { name: 'nanostores — Estado global ligero', value: 'nanostores' },
93
- ],
94
- svelte: [
95
- { name: 'axios — HTTP client', value: 'axios' },
96
- { name: 'tailwindcss — CSS utility-first', value: 'tailwindcss' },
97
- { name: 'zod — Validación', value: 'zod' },
98
- { name: 'svelte-query — Server state', value: '@tanstack/svelte-query' },
99
- { name: 'skeleton — UI components', value: '@skeletonlabs/skeleton' },
100
- { name: 'lucia — Autenticación', value: 'lucia' },
101
- ],
102
- solid: [
103
- { name: 'axios — HTTP client', value: 'axios' },
104
- { name: '@solidjs/router — Enrutamiento', value: '@solidjs/router' },
105
- { name: 'tailwindcss — CSS utility-first', value: 'tailwindcss' },
106
- { name: 'solid-query — Server state', value: '@tanstack/solid-query' },
107
- { name: 'zod — Validación', value: 'zod' },
108
- ],
109
- angular: [
110
- { name: 'axios — HTTP client', value: 'axios' },
111
- { name: '@angular/material — Material Design UI', value: '@angular/material' },
112
- { name: 'tailwindcss — CSS utility-first', value: 'tailwindcss' },
113
- { name: 'ngrx/store — Estado global Redux-like', value: '@ngrx/store' },
114
- { name: 'zod — Validación', value: 'zod' },
115
- { name: 'date-fns — Manejo de fechas', value: 'date-fns' },
116
- ],
117
- remix: [
118
- { name: 'tailwindcss — CSS utility-first', value: 'tailwindcss' },
119
- { name: 'zod — Validación', value: 'zod' },
120
- { name: 'prisma — ORM moderno', value: 'prisma' },
121
- { name: 'react-hook-form — Formularios', value: 'react-hook-form' },
122
- { name: 'date-fns — Fechas', value: 'date-fns' },
123
- ],
124
- qwik: [
125
- { name: 'tailwindcss — CSS utility-first', value: 'tailwindcss' },
126
- { name: 'zod — Validación', value: 'zod' },
127
- { name: 'axios — HTTP client', value: 'axios' },
128
- ],
55
+ react: ['axios','react-router-dom','zustand','@tanstack/react-query','tailwindcss','framer-motion','react-hook-form','zod','date-fns','recharts'],
56
+ next: ['axios','next-auth','zustand','tailwindcss','prisma','@tanstack/react-query','zod','react-hook-form','stripe','date-fns'],
57
+ vue: ['axios','vue-router','pinia','tailwindcss','vee-validate','zod','@vueuse/core','date-fns','vue-i18n'],
58
+ nuxt: ['ofetch','@pinia/nuxt','@nuxtjs/tailwindcss','@nuxt/image','nuxt-auth-utils','zod','@vueuse/nuxt'],
59
+ astro: ['@astrojs/tailwind','@astrojs/react','@astrojs/mdx','nanostores'],
60
+ svelte: ['axios','tailwindcss','zod','@tanstack/svelte-query','@skeletonlabs/skeleton'],
61
+ solid: ['axios','@solidjs/router','tailwindcss','@tanstack/solid-query','zod'],
62
+ angular: ['axios','@angular/material','tailwindcss','@ngrx/store','zod','date-fns'],
63
+ remix: ['tailwindcss','zod','prisma','react-hook-form','date-fns'],
64
+ qwik: ['tailwindcss','zod','axios'],
129
65
  };
130
66
 
131
- // ── DEPENDENCIAS POR BACKEND ──────────────────────────────────────────────────
67
+ // ── DEPENDENCIAS BACKEND ──────────────────────────────────────────────────────
132
68
  export const BACKEND_DEPS = {
133
- express: [
134
- { name: 'cors — Habilitar CORS', value: 'cors' },
135
- { name: 'dotenv — Variables de entorno', value: 'dotenv' },
136
- { name: 'mongoose — ODM para MongoDB', value: 'mongoose' },
137
- { name: 'prisma — ORM moderno (SQL)', value: 'prisma' },
138
- { name: 'jsonwebtoken — Autenticación JWT', value: 'jsonwebtoken' },
139
- { name: 'bcryptjs — Hash de contraseñas', value: 'bcryptjs' },
140
- { name: 'express-validator — Validación de inputs', value: 'express-validator' },
141
- { name: 'multer — Subida de archivos', value: 'multer' },
142
- { name: 'helmet — Seguridad HTTP headers', value: 'helmet' },
143
- { name: 'morgan — Logger HTTP', value: 'morgan' },
144
- { name: 'rate-limiter — Limitar peticiones', value: 'express-rate-limit' },
145
- { name: 'swagger-ui — Documentación API', value: 'swagger-ui-express' },
146
- { name: 'socket.io — WebSockets en tiempo real', value: 'socket.io' },
147
- { name: 'bull — Colas de trabajo', value: 'bull' },
148
- { name: 'nodemailer — Envío de emails', value: 'nodemailer' },
149
- { name: 'nodemon — Auto-reload en dev', value: 'nodemon' },
150
- ],
151
- nestjs: [
152
- { name: 'prisma — ORM moderno (SQL)', value: 'prisma' },
153
- { name: '@nestjs/jwt — Módulo JWT oficial', value: '@nestjs/jwt' },
154
- { name: '@nestjs/config — Variables de entorno', value: '@nestjs/config' },
155
- { name: 'class-validator — Validación de DTOs', value: 'class-validator' },
156
- { name: 'class-transformer — Transformación de objetos', value: 'class-transformer' },
157
- { name: 'mongoose — ODM para MongoDB', value: 'mongoose' },
158
- { name: '@nestjs/swagger — Documentación API', value: '@nestjs/swagger' },
159
- { name: '@nestjs/websockets — WebSockets', value: '@nestjs/websockets' },
160
- { name: '@nestjs/bull — Colas de trabajo', value: '@nestjs/bull' },
161
- { name: '@nestjs/mailer — Envío de emails', value: '@nestjs-modules/mailer' },
162
- { name: 'passport — Autenticación estrategias', value: '@nestjs/passport' },
163
- ],
164
- fastify: [
165
- { name: '@fastify/cors — CORS', value: '@fastify/cors' },
166
- { name: '@fastify/jwt — JWT', value: '@fastify/jwt' },
167
- { name: '@fastify/swagger — Documentación API', value: '@fastify/swagger' },
168
- { name: '@fastify/multipart — Subida de archivos', value: '@fastify/multipart' },
169
- { name: '@fastify/rate-limit — Rate limiting', value: '@fastify/rate-limit' },
170
- { name: 'prisma — ORM moderno', value: 'prisma' },
171
- { name: 'mongoose — ODM MongoDB', value: 'mongoose' },
172
- { name: 'dotenv — Variables de entorno', value: 'dotenv' },
173
- { name: 'nodemon — Auto-reload', value: 'nodemon' },
174
- ],
175
- hono: [
176
- { name: 'hono/jwt — JWT middleware', value: '@hono/jwt' },
177
- { name: 'zod — Validación', value: 'zod' },
178
- { name: 'drizzle-orm — ORM ligero', value: 'drizzle-orm' },
179
- { name: 'prisma — ORM moderno', value: 'prisma' },
180
- { name: 'dotenv — Variables de entorno', value: 'dotenv' },
181
- ],
182
- fastapi: [
183
- { name: 'sqlalchemy — ORM para Python', value: 'sqlalchemy' },
184
- { name: 'alembic — Migraciones de BD', value: 'alembic' },
185
- { name: 'python-jose — JWT para Python', value: 'python-jose[cryptography]' },
186
- { name: 'passlib — Hash de contraseñas', value: 'passlib[bcrypt]' },
187
- { name: 'python-dotenv — Variables de entorno', value: 'python-dotenv' },
188
- { name: 'httpx — HTTP client async', value: 'httpx' },
189
- { name: 'celery — Colas de tareas', value: 'celery' },
190
- { name: 'redis — Cache / broker', value: 'redis' },
191
- { name: 'pydantic-settings — Config tipada', value: 'pydantic-settings' },
192
- { name: 'tortoise-orm — ORM async', value: 'tortoise-orm' },
193
- { name: 'aiofiles — Archivos async', value: 'aiofiles' },
194
- ],
195
- django: [
196
- { name: 'djangorestframework — API REST', value: 'djangorestframework' },
197
- { name: 'django-cors-headers — CORS', value: 'django-cors-headers' },
198
- { name: 'djangorestframework-simplejwt — JWT', value: 'djangorestframework-simplejwt' },
199
- { name: 'celery — Colas de tareas', value: 'celery' },
200
- { name: 'redis — Cache / broker', value: 'redis' },
201
- { name: 'pillow — Procesamiento de imágenes', value: 'Pillow' },
202
- { name: 'python-dotenv — Variables de entorno', value: 'python-dotenv' },
203
- { name: 'psycopg2 — PostgreSQL driver', value: 'psycopg2-binary' },
204
- { name: 'drf-spectacular — Documentación OpenAPI', value: 'drf-spectacular' },
205
- ],
206
- flask: [
207
- { name: 'flask-restful — API REST', value: 'flask-restful' },
208
- { name: 'flask-sqlalchemy — ORM integrado', value: 'flask-sqlalchemy' },
209
- { name: 'flask-jwt-extended — JWT', value: 'flask-jwt-extended' },
210
- { name: 'flask-cors — CORS', value: 'flask-cors' },
211
- { name: 'flask-migrate — Migraciones', value: 'flask-migrate' },
212
- { name: 'marshmallow — Serialización', value: 'marshmallow' },
213
- { name: 'python-dotenv — Variables de entorno', value: 'python-dotenv' },
214
- { name: 'celery — Colas de tareas', value: 'celery' },
215
- ],
216
- spring: [
217
- { name: 'spring-data-jpa — ORM / JPA', value: 'spring-data-jpa' },
218
- { name: 'spring-security — Seguridad y auth', value: 'spring-security' },
219
- { name: 'spring-web — REST controllers', value: 'spring-web' },
220
- { name: 'lombok — Reduce boilerplate', value: 'lombok' },
221
- { name: 'postgresql-driver — Driver PostgreSQL', value: 'postgresql' },
222
- { name: 'h2-database — BD en memoria (dev)', value: 'h2' },
223
- ],
224
- deno: [
225
- { name: 'oak/cors — CORS middleware', value: 'cors' },
226
- { name: 'djwt — JWT para Deno', value: 'djwt' },
227
- { name: 'zod — Validación', value: 'zod' },
228
- ],
229
- gin: [
230
- { name: 'gorm — ORM para Go', value: 'gorm' },
231
- { name: 'jwt-go — JWT', value: 'jwt-go' },
232
- { name: 'godotenv — Variables de entorno', value: 'godotenv' },
233
- { name: 'validator — Validación de structs', value: 'validator' },
234
- ],
69
+ express: ['cors','dotenv','mongoose','prisma','jsonwebtoken','bcryptjs','express-validator','multer','helmet','morgan','express-rate-limit','swagger-ui-express','socket.io','nodemailer','nodemon'],
70
+ nestjs: ['prisma','@nestjs/jwt','@nestjs/config','class-validator','class-transformer','mongoose','@nestjs/swagger','@nestjs/websockets','@nestjs/bull','@nestjs/passport'],
71
+ fastify: ['@fastify/cors','@fastify/jwt','@fastify/swagger','@fastify/multipart','@fastify/rate-limit','prisma','mongoose','dotenv','nodemon'],
72
+ hono: ['zod','drizzle-orm','prisma','dotenv'],
73
+ fastapi: ['sqlalchemy','alembic','python-jose[cryptography]','passlib[bcrypt]','python-dotenv','httpx','celery','redis','pydantic-settings'],
74
+ django: ['djangorestframework','django-cors-headers','djangorestframework-simplejwt','celery','redis','Pillow','python-dotenv','psycopg2-binary','drf-spectacular'],
75
+ flask: ['flask-restful','flask-sqlalchemy','flask-jwt-extended','flask-cors','flask-migrate','marshmallow','python-dotenv','celery'],
76
+ spring: ['spring-data-jpa','spring-security','spring-web','lombok','postgresql','h2'],
77
+ deno: ['cors','djwt','zod'],
78
+ gin: ['gorm','jwt-go','godotenv','validator'],
235
79
  };
236
80
 
237
- // ── EXTRAS GLOBALES ───────────────────────────────────────────────────────────
81
+ // ── EXTRAS ────────────────────────────────────────────────────────────────────
238
82
  export const EXTRAS_CHOICES = [
239
- { name: '🔀 Git init automático', value: 'git', checked: true },
240
- { name: '🔍 ESLint + Prettier (linting/formato)', value: 'eslint' },
241
- { name: '🐶 Husky + lint-staged (pre-commit hooks)', value: 'husky' },
242
- { name: '🧪 Vitest / Jest (testing)', value: 'testing' },
243
- { name: '🐳 docker-compose.yml', value: 'docker' },
244
- { name: '🔒 Archivos .env.example', value: 'env' },
245
- { name: '📋 .editorconfig', value: 'editorconfig' },
246
- { name: '🚀 GitHub Actions CI/CD básico', value: 'ci' },
83
+ { name: 'Git init automático', value: 'git', checked: true },
84
+ { name: 'ESLint + Prettier', value: 'eslint', checked: false },
85
+ { name: 'Husky + lint-staged (pre-commit)', value: 'husky', checked: false },
86
+ { name: 'Vitest (testing)', value: 'testing', checked: false },
87
+ { name: 'Docker + docker-compose', value: 'docker', checked: false },
88
+ { name: 'Archivos .env.example', value: 'env', checked: true },
89
+ { name: '.editorconfig', value: 'editorconfig', checked: false },
90
+ { name: 'GitHub Actions CI/CD', value: 'ci', checked: false },
91
+ { name: 'Auth JWT lista (rutas + middleware)', value: 'auth', checked: false },
92
+ { name: 'Tailwind CSS auto-configurado', value: 'tailwind', checked: false },
247
93
  ];
248
94
 
249
95
  // ── FLUJO PRINCIPAL ───────────────────────────────────────────────────────────
250
96
  export async function askProjectOptions(nameArg, options) {
97
+ // Modo preset
98
+ if (options.preset) {
99
+ const preset = PRESETS[options.preset];
100
+ if (!preset) {
101
+ console.log(chalk.red(`\n Preset "${options.preset}" no existe.`));
102
+ console.log(chalk.gray(' Presets disponibles: ' + Object.keys(PRESETS).join(', ') + '\n'));
103
+ process.exit(1);
104
+ }
105
+ const name = nameArg || options.preset + '-app';
106
+ console.log(chalk.gray(`\n Usando preset: ${chalk.white(options.preset)}\n`));
107
+ return { name, ...preset, frontendDeps: [], backendDeps: [], extras: ['git', 'env'] };
108
+ }
109
+
110
+ // Modo --yes (defaults)
111
+ if (options.yes) {
112
+ const name = nameArg || 'my-app';
113
+ await checkFolderConflict(name, true);
114
+ return {
115
+ name,
116
+ frontend: options.frontend || 'react',
117
+ backend: options.backend || 'express',
118
+ db: options.db || 'postgres',
119
+ typescript: options.typescript || false,
120
+ frontendDeps: [],
121
+ backendDeps: [],
122
+ extras: ['git', 'env'],
123
+ };
124
+ }
125
+
251
126
  const answers = {};
252
127
 
128
+ // Nombre
253
129
  if (!nameArg) {
254
130
  const { name } = await inquirer.prompt([{
255
131
  type: 'input',
256
132
  name: 'name',
257
- message: chalk.cyan('📦 Nombre del proyecto:'),
133
+ message: chalk.white('Nombre del proyecto:'),
258
134
  default: 'my-app',
259
- validate: v => /^[a-z0-9-_]+$/i.test(v.trim()) || 'Solo letras, números, guiones y guiones bajos',
135
+ validate: v => {
136
+ if (!/^[a-z0-9-_]+$/i.test(v.trim())) return 'Solo letras, números, guiones y guiones bajos';
137
+ if (v.trim().length < 2) return 'Mínimo 2 caracteres';
138
+ return true;
139
+ },
260
140
  }]);
261
141
  answers.name = name.trim();
262
142
  } else {
263
143
  answers.name = nameArg;
264
144
  }
265
145
 
146
+ // Verificar carpeta existente
147
+ await checkFolderConflict(answers.name, false);
148
+
149
+ // TypeScript
150
+ const { typescript } = await inquirer.prompt([{
151
+ type: 'confirm',
152
+ name: 'typescript',
153
+ message: chalk.white('¿Usar TypeScript?'),
154
+ default: options.typescript || false,
155
+ }]);
156
+ answers.typescript = typescript;
157
+
158
+ // Frontend
266
159
  if (!options.frontend) {
267
160
  const { frontend } = await inquirer.prompt([{
268
161
  type: 'list',
269
162
  name: 'frontend',
270
- message: chalk.cyan('⚛ Elige el framework de Frontend:'),
163
+ message: chalk.white('Framework Frontend:'),
271
164
  choices: FRONTEND_CHOICES,
272
165
  pageSize: 12,
273
166
  }]);
@@ -276,11 +169,12 @@ export async function askProjectOptions(nameArg, options) {
276
169
  answers.frontend = options.frontend;
277
170
  }
278
171
 
172
+ // Backend
279
173
  if (!options.backend) {
280
174
  const { backend } = await inquirer.prompt([{
281
175
  type: 'list',
282
176
  name: 'backend',
283
- message: chalk.cyan('🔧 Elige el framework de Backend:'),
177
+ message: chalk.white('Framework Backend:'),
284
178
  choices: BACKEND_CHOICES,
285
179
  pageSize: 12,
286
180
  }]);
@@ -289,60 +183,93 @@ export async function askProjectOptions(nameArg, options) {
289
183
  answers.backend = options.backend;
290
184
  }
291
185
 
292
- // Dependencias contextuales frontend
293
- console.log(chalk.gray(`\n 💡 Dependencias recomendadas para ${chalk.bold(answers.frontend)}:`));
186
+ // Base de datos
187
+ if (!options.db) {
188
+ const { db } = await inquirer.prompt([{
189
+ type: 'list',
190
+ name: 'db',
191
+ message: chalk.white('Base de datos:'),
192
+ choices: DB_CHOICES,
193
+ pageSize: 6,
194
+ }]);
195
+ answers.db = db;
196
+ } else {
197
+ answers.db = options.db;
198
+ }
199
+
200
+ // Deps frontend
294
201
  const { frontendDeps } = await inquirer.prompt([{
295
202
  type: 'checkbox',
296
203
  name: 'frontendDeps',
297
- message: chalk.cyan('📦 Dependencias para el Frontend (espacio = seleccionar):'),
298
- choices: FRONTEND_DEPS[answers.frontend] || [],
299
- pageSize: 14,
204
+ message: chalk.white(`Dependencias Frontend (${answers.frontend}):`),
205
+ choices: (FRONTEND_DEPS[answers.frontend] || []).map(d => ({ name: d, value: d })),
206
+ pageSize: 12,
300
207
  }]);
301
208
 
302
- // Dependencias contextuales backend
303
- console.log(chalk.gray(`\n 💡 Dependencias recomendadas para ${chalk.bold(answers.backend)}:`));
209
+ // Deps backend
304
210
  const { backendDeps } = await inquirer.prompt([{
305
211
  type: 'checkbox',
306
212
  name: 'backendDeps',
307
- message: chalk.cyan('📦 Dependencias para el Backend (espacio = seleccionar):'),
308
- choices: BACKEND_DEPS[answers.backend] || [],
309
- pageSize: 14,
213
+ message: chalk.white(`Dependencias Backend (${answers.backend}):`),
214
+ choices: (BACKEND_DEPS[answers.backend] || []).map(d => ({ name: d, value: d })),
215
+ pageSize: 12,
310
216
  }]);
311
217
 
312
218
  // Extras
313
219
  const { extras } = await inquirer.prompt([{
314
220
  type: 'checkbox',
315
221
  name: 'extras',
316
- message: chalk.cyan('⚙ Opciones adicionales del proyecto:'),
222
+ message: chalk.white('Extras del proyecto:'),
317
223
  choices: EXTRAS_CHOICES,
318
- pageSize: 10,
224
+ pageSize: 12,
319
225
  }]);
320
226
 
321
227
  // Resumen + confirmación
322
228
  console.log();
323
- console.log(chalk.cyan(' ╔══════════════════════════════════════════════════════╗'));
324
- console.log(chalk.cyan(' ║') + chalk.bold.white(' 📋 RESUMEN DEL PROYECTO ') + chalk.cyan('║'));
325
- console.log(chalk.cyan(' ╠══════════════════════════════════════════════════════╣'));
326
- console.log(chalk.cyan(' ') + chalk.white(` 📦 Nombre : `) + chalk.cyan.bold(answers.name.padEnd(36)) + chalk.cyan(''));
327
- console.log(chalk.cyan(' ') + chalk.white(` ⚛ Frontend : `) + chalk.cyan.bold(answers.frontend.padEnd(36)) + chalk.cyan(''));
328
- console.log(chalk.cyan(' ') + chalk.white(` 🔧 Backend : `) + chalk.cyan.bold(answers.backend.padEnd(36)) + chalk.cyan(''));
329
- console.log(chalk.cyan(' ║') + chalk.white(` 📦 FE deps : `) + chalk.gray((frontendDeps.length ? frontendDeps.length + ' seleccionadas' : 'ninguna').padEnd(36)) + chalk.cyan('║'));
330
- console.log(chalk.cyan(' ║') + chalk.white(` 📦 BE deps : `) + chalk.gray((backendDeps.length ? backendDeps.length + ' seleccionadas' : 'ninguna').padEnd(36)) + chalk.cyan(''));
331
- console.log(chalk.cyan(' ') + chalk.white(` ⚙ Extras : `) + chalk.gray((extras.length ? extras.join(', ') : 'ninguno').substring(0, 36).padEnd(36)) + chalk.cyan(''));
332
- console.log(chalk.cyan(' ╚══════════════════════════════════════════════════════╝'));
229
+ console.log(
230
+ boxen(
231
+ chalk.bold.white(' RESUMEN\n\n') +
232
+ chalk.gray(' Nombre │ ') + chalk.white(answers.name) + '\n' +
233
+ chalk.gray(' Frontend │ ') + chalk.white(answers.frontend) + (answers.typescript ? chalk.gray(' + TypeScript') : '') + '\n' +
234
+ chalk.gray(' Backend │ ') + chalk.white(answers.backend) + '\n' +
235
+ chalk.gray(' Base datos ') + chalk.white(answers.db) + '\n' +
236
+ chalk.gray(' FE deps ') + chalk.white(frontendDeps.length ? frontendDeps.length + ' paquetes' : 'ninguno') + '\n' +
237
+ chalk.gray(' BE deps │ ') + chalk.white(backendDeps.length ? backendDeps.length + ' paquetes' : 'ninguno') + '\n' +
238
+ chalk.gray(' Extras │ ') + chalk.white(extras.length ? extras.join(', ') : 'ninguno'),
239
+ { padding: 1, margin: { left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
240
+ )
241
+ );
333
242
  console.log();
334
243
 
335
244
  const { confirm } = await inquirer.prompt([{
336
245
  type: 'confirm',
337
246
  name: 'confirm',
338
- message: chalk.yellow('¿Crear el proyecto con esta configuración?'),
247
+ message: chalk.white('¿Crear el proyecto?'),
339
248
  default: true,
340
249
  }]);
341
250
 
342
- if (!confirm) {
343
- console.log(chalk.yellow('\n Operación cancelada.\n'));
344
- process.exit(0);
345
- }
251
+ if (!confirm) { console.log(chalk.gray('\n Cancelado.\n')); process.exit(0); }
346
252
 
347
- return { name: answers.name, frontend: answers.frontend, backend: answers.backend, frontendDeps, backendDeps, extras };
253
+ return { ...answers, frontendDeps, backendDeps, extras };
348
254
  }
255
+
256
+ // ── HELPERS ───────────────────────────────────────────────────────────────────
257
+ async function checkFolderConflict(name, autoSkip) {
258
+ const projectPath = path.resolve(process.cwd(), name);
259
+ if (await fse.pathExists(projectPath)) {
260
+ if (autoSkip) {
261
+ console.log(chalk.yellow(`\n La carpeta "${name}" ya existe. Se sobreescribirá.\n`));
262
+ return;
263
+ }
264
+ const { overwrite } = await inquirer.prompt([{
265
+ type: 'confirm',
266
+ name: 'overwrite',
267
+ message: chalk.yellow(`La carpeta "${name}" ya existe. ¿Sobreescribir?`),
268
+ default: false,
269
+ }]);
270
+ if (!overwrite) { console.log(chalk.gray('\n Cancelado.\n')); process.exit(0); }
271
+ await fse.remove(projectPath);
272
+ }
273
+ }
274
+
275
+
package/lib/utils.js CHANGED
@@ -8,33 +8,46 @@ export function isInstalled(cmd) {
8
8
  }
9
9
 
10
10
  export async function checkRequirements() {
11
- const spinner = ora({ text: chalk.cyan('Verificando entorno...'), color: 'cyan' }).start();
11
+ const spinner = ora({
12
+ text: chalk.white('Verificando entorno del sistema...'),
13
+ spinner: 'aesthetic',
14
+ color: 'white',
15
+ }).start();
16
+
12
17
  const missing = [];
13
18
  if (!isInstalled('node')) missing.push('Node.js');
14
19
  if (!isInstalled('npm')) missing.push('npm');
20
+
15
21
  if (missing.length) {
16
22
  spinner.fail(chalk.red('Faltan herramientas: ' + missing.join(', ')));
17
23
  throw new Error('Instala las herramientas faltantes y vuelve a intentarlo.');
18
24
  }
25
+
19
26
  const nodeVer = execSync('node --version', { encoding: 'utf8' }).trim();
20
27
  const npmVer = execSync('npm --version', { encoding: 'utf8' }).trim();
28
+
21
29
  spinner.succeed(
22
- chalk.green('Entorno OK ') +
23
- chalk.gray('') +
24
- chalk.cyan(` Node ${nodeVer} `) +
25
- chalk.gray('│') +
26
- chalk.cyan(` npm v${npmVer}`)
30
+ chalk.white('Entorno listo ') +
31
+ chalk.gray('· Node ') + chalk.bold.white(nodeVer) +
32
+ chalk.gray(' · npm ') + chalk.bold.white('v' + npmVer)
27
33
  );
28
34
  console.log();
29
35
  }
30
36
 
31
37
  export function run(cmd, cwd, label) {
32
- const spinner = ora({ text: chalk.white(label), color: 'cyan', spinner: 'dots2' }).start();
38
+ const spinner = ora({
39
+ text: chalk.white(label),
40
+ spinner: 'dots',
41
+ color: 'white',
42
+ }).start();
43
+
33
44
  const result = spawnSync(cmd, { cwd, shell: true, stdio: 'pipe', encoding: 'utf8' });
45
+
34
46
  if (result.status !== 0) {
35
- spinner.fail(chalk.red(`✖ ${label}`));
47
+ spinner.fail(chalk.red('✖ ' + label));
36
48
  const msg = result.stderr || result.stdout || 'Error desconocido';
37
49
  throw new Error(msg.trim());
38
50
  }
39
- spinner.succeed(chalk.green(`✔ ${label}`));
51
+
52
+ spinner.succeed(chalk.white('✔ ' + label));
40
53
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "novatec-cli",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "🚀 NOVATEC FULLSTACK CLI — Generador profesional de proyectos full stack | React, Next.js, Vue, Express, NestJS, FastAPI y más",
5
5
  "type": "module",
6
6
  "main": "./lib/create.js",
7
7
  "bin": {
8
- "novatec-cli": "bin/index.js"
8
+ "novatec": "bin/index.js"
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node ./bin/index.js create test-project",