novatec-cli 3.0.1 → 3.0.3

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/create.js CHANGED
@@ -2,154 +2,99 @@ import path from 'path';
2
2
  import chalk from 'chalk';
3
3
  import fse from 'fs-extra';
4
4
  import boxen from 'boxen';
5
- import { spawnSync } from 'child_process';
5
+ import { execSync } from 'child_process';
6
6
  import { askProjectOptions } from './prompts.js';
7
7
  import { generateFrontend } from './generators/frontend.js';
8
8
  import { generateBackend } from './generators/backend.js';
9
- import { generateBusiness } from './generators/business.js';
10
- import { generateDocker } from './generators/docker.js';
11
- import { generateSecurity } from './generators/security.js';
12
- import { generateDeploy } from './generators/deploy.js';
13
9
  import { isInstalled } from './utils.js';
14
10
 
15
- function bar(cur, tot, label) {
16
- const w = 28, f = Math.round((cur / tot) * w);
17
- const pct = Math.round((cur / tot) * 100);
18
- process.stdout.write(`\r [${'█'.repeat(f)}${'░'.repeat(w - f)}] ${chalk.white(pct + '%')} ${chalk.gray(label.substring(0, 30))} `);
19
- }
20
-
21
11
  export async function runCreate(nameArg, options) {
22
12
  const t0 = Date.now();
23
13
  const config = await askProjectOptions(nameArg, options);
24
14
  const root = path.resolve(process.cwd(), config.name);
25
15
  await fse.ensureDir(root);
26
16
 
27
- const extras = config.extras || [];
28
17
  const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
29
18
  const pm = config.pm || 'npm';
30
19
 
31
- // Calcular pasos
32
- const steps = ['Frontend', 'Backend', 'README', 'Env files',
33
- config.mode === 'business' && 'Business mode',
34
- extras.includes('docker') && 'Docker',
35
- extras.includes('security') && 'Seguridad',
36
- extras.includes('ci') && 'GitHub Actions',
37
- extras.includes('eslint') && 'ESLint',
38
- extras.includes('testing') && 'Testing',
39
- extras.includes('swagger') && 'Swagger',
40
- extras.includes('logs') && 'Logs',
41
- extras.includes('seo') && 'SEO',
42
- extras.includes('git') && 'Git',
43
- config.install && 'Instalando deps',
44
- ].filter(Boolean);
45
-
46
20
  console.log();
47
- let step = 0;
48
- const tick = (label) => { step++; bar(step, steps.length, label); };
49
21
 
50
22
  // 1. Frontend
51
- tick('Generando frontend...');
23
+ console.log(chalk.white(' [1/4] Generando frontend...'));
52
24
  await generateFrontend(config, root);
53
- await connectFrontendToBackend(config, root);
54
25
 
55
- // 2. Backend con arquitectura
56
- tick('Generando backend...');
26
+ // 2. Backend
27
+ console.log(chalk.white(' [2/4] Generando backend...'));
57
28
  await generateBackend(config, root);
58
- await applyArchitecture(config, root);
59
29
 
60
- // 3. README
61
- tick('README...');
62
- await generateReadme(config, root);
30
+ // 3. Arquitectura
31
+ console.log(chalk.white(' [3/4] Aplicando arquitectura...'));
32
+ await applyArchitecture(config, root);
63
33
 
64
- // 4. .env
65
- tick('.env files...');
34
+ // 4. Archivos base (.env, README)
35
+ console.log(chalk.white(' [4/4] Generando archivos base...'));
66
36
  await generateEnv(config, root);
37
+ await generateReadme(config, root);
67
38
 
68
- // 5. Business mode
69
- if (config.mode === 'business') {
70
- tick('Business mode...');
71
- await generateBusiness(config, root);
72
- }
73
-
74
- // 6. Docker
75
- if (extras.includes('docker')) {
76
- tick('Docker...');
77
- await generateDocker(config, root);
78
- }
79
-
80
- // 7. Seguridad
81
- if (extras.includes('security')) {
82
- tick('Seguridad...');
83
- await generateSecurity(config, root);
84
- }
85
-
86
- // 8. GitHub Actions
87
- if (extras.includes('ci')) {
88
- tick('GitHub Actions...');
89
- await generateDeploy(config, root);
90
- }
91
-
92
- // 9. ESLint
93
- if (extras.includes('eslint')) {
94
- tick('ESLint...');
95
- await setupEslint(root);
96
- }
97
-
98
- // 10. Testing
99
- if (extras.includes('testing')) {
100
- tick('Testing...');
101
- await setupTesting(root);
102
- }
103
-
104
- // 11. Swagger (ya incluido en security/backend, solo notificar)
105
- if (extras.includes('swagger')) {
106
- tick('Swagger...');
107
- // generado en generateSecurity
108
- }
109
-
110
- // 12. Logs
111
- if (extras.includes('logs')) {
112
- tick('Logs...');
113
- await setupLogs(config, root);
114
- }
39
+ // 5. Instalar dependencias
40
+ if (config.install) {
41
+ console.log();
42
+ console.log(chalk.white(' Instalando dependencias...'));
43
+ const feDir = path.join(root, 'frontend');
44
+ const beDir = path.join(root, 'backend');
45
+ const installCmd = pm === 'yarn' ? 'yarn' : `${pm} install`;
115
46
 
116
- // 13. SEO
117
- if (extras.includes('seo')) {
118
- tick('SEO...');
119
- await setupSEO(config, root);
120
- }
47
+ if (await fse.pathExists(path.join(feDir, 'package.json'))) {
48
+ try {
49
+ console.log(chalk.gray(` → frontend (${pm} install)`));
50
+ execSync(installCmd, { cwd: feDir, stdio: 'inherit' });
51
+ } catch (e) {
52
+ console.log(chalk.yellow(' ⚠ Error instalando frontend: ' + e.message));
53
+ }
54
+ }
121
55
 
122
- // 14. Git
123
- if (extras.includes('git') && isInstalled('git')) {
124
- tick('Git init...');
125
- try {
126
- spawnSync('git', ['init'], { cwd: root, shell: true, stdio: 'pipe' });
127
- await fse.writeFile(path.join(root, '.gitignore'), GITIGNORE);
128
- } catch {}
129
- }
56
+ if (!isPython && await fse.pathExists(path.join(beDir, 'package.json'))) {
57
+ try {
58
+ console.log(chalk.gray(` → backend (${pm} install)`));
59
+ execSync(installCmd, { cwd: beDir, stdio: 'inherit' });
60
+ } catch (e) {
61
+ console.log(chalk.yellow(' ⚠ Error instalando backend: ' + e.message));
62
+ }
63
+ }
130
64
 
131
- // 15. Instalar deps
132
- if (config.install && isInstalled(pm)) {
133
- tick('Instalando dependencias...');
134
- const fe = path.join(root, 'frontend');
135
- const be = path.join(root, 'backend');
136
- const installCmd = pm === 'yarn' ? 'yarn' : `${pm} install`;
137
- try { spawnSync(installCmd, { cwd: fe, shell: true, stdio: 'pipe' }); } catch {}
138
- if (!isPython) {
139
- try { spawnSync(installCmd, { cwd: be, shell: true, stdio: 'pipe' }); } catch {}
65
+ if (isPython && await fse.pathExists(path.join(beDir, 'requirements.txt'))) {
66
+ if (isInstalled('pip')) {
67
+ try {
68
+ console.log(chalk.gray(' backend (pip install -r requirements.txt)'));
69
+ execSync('pip install -r requirements.txt', { cwd: beDir, stdio: 'inherit' });
70
+ } catch (e) {
71
+ console.log(chalk.yellow(' Error instalando backend Python: ' + e.message));
72
+ }
73
+ } else {
74
+ console.log(chalk.yellow(' ⚠ pip no encontrado. Ejecuta manualmente: pip install -r requirements.txt'));
75
+ }
140
76
  }
141
77
  }
142
78
 
143
- bar(steps.length, steps.length, 'Completado');
144
- console.log('\n');
145
-
146
79
  const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
147
- const beCmd = { express:'npm run dev', nestjs:'npm run start:dev', fastify:'npm run dev', hono:'npm run dev', fastapi:'uvicorn main:app --reload', django:'python manage.py runserver', flask:'python run.py', spring:'mvn spring-boot:run', deno:'deno task dev', gin:'go run main.go' };
148
80
 
81
+ const beCmd = {
82
+ express: 'npm run dev',
83
+ nestjs: 'npm run start:dev',
84
+ fastify: 'npm run dev',
85
+ hono: 'npm run dev',
86
+ fastapi: 'uvicorn main:app --reload',
87
+ django: 'python manage.py runserver',
88
+ flask: 'python run.py',
89
+ spring: 'mvn spring-boot:run',
90
+ deno: 'deno task dev',
91
+ gin: 'go run main.go',
92
+ };
93
+
94
+ console.log();
149
95
  console.log(boxen(
150
96
  chalk.bold.white(' ✔ PROYECTO LISTO — NovaTec CLI\n\n') +
151
97
  chalk.gray(' Nombre │ ') + chalk.white(config.name) + '\n' +
152
- chalk.gray(' Modo │ ') + chalk.white(config.mode || 'estándar') + '\n' +
153
98
  chalk.gray(' Frontend │ ') + chalk.white(config.frontend) + (config.typescript ? chalk.gray(' + TypeScript') : '') + '\n' +
154
99
  chalk.gray(' Backend │ ') + chalk.white(config.backend) + '\n' +
155
100
  chalk.gray(' Base datos │ ') + chalk.white(config.db || 'ninguna') + '\n' +
@@ -157,49 +102,20 @@ export async function runCreate(nameArg, options) {
157
102
  chalk.gray(' Tiempo │ ') + chalk.white(elapsed + 's') + '\n\n' +
158
103
  chalk.gray(' ─────────────────────────────────────────────\n\n') +
159
104
  chalk.white(' # Frontend\n') +
160
- chalk.gray(` cd ${config.name}/frontend\n`) +
161
- chalk.gray(` ${pm} ${pm === 'yarn' ? '' : 'run '}dev\n\n`) +
105
+ chalk.gray(` cd ${config.name}/frontend && ${pm} run dev\n\n`) +
162
106
  chalk.white(' # Backend\n') +
163
- chalk.gray(` cd ${config.name}/backend\n`) +
164
- chalk.gray(` ${beCmd[config.backend] || pm + ' run dev'}\n`) +
165
- (config.mode === 'business' ? '\n' + chalk.gray(' # Dashboard → http://localhost:5173/dashboard\n') + chalk.gray(' # API Docs → http://localhost:3001/api-docs') : ''),
107
+ chalk.gray(` cd ${config.name}/backend && ${beCmd[config.backend] || pm + ' run dev'}`),
166
108
  { padding: 1, margin: { top: 1, left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
167
109
  ));
168
110
 
169
111
  // Abrir VS Code
170
112
  if (isInstalled('code')) {
171
- try { spawnSync('code', [root], { shell: true, stdio: 'ignore', detached: true }); } catch {}
113
+ try { execSync(`code "${root}"`, { stdio: 'ignore' }); } catch {}
172
114
  console.log(' ' + chalk.gray('VS Code abierto ✔'));
173
115
  }
174
116
  console.log();
175
117
  }
176
118
 
177
- // ── CONEXIÓN FE → BE ──────────────────────────────────────────────────────────
178
- async function connectFrontendToBackend(config, root) {
179
- const feDir = path.join(root, 'frontend');
180
- const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
181
- const bePort = isPython ? '8000' : '3001';
182
-
183
- // Archivo de API client
184
- const apiClient = config.db === 'supabase'
185
- ? `import { createClient } from '@supabase/supabase-js';\n\nexport const supabase = createClient(\n import.meta.env.VITE_SUPABASE_URL,\n import.meta.env.VITE_SUPABASE_ANON_KEY\n);\n\nexport const api = {\n get: (url) => fetch(import.meta.env.VITE_API_URL + url).then(r => r.json()),\n post: (url, data) => fetch(import.meta.env.VITE_API_URL + url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json()),\n put: (url, data) => fetch(import.meta.env.VITE_API_URL + url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }).then(r => r.json()),\n delete: (url) => fetch(import.meta.env.VITE_API_URL + url, { method: 'DELETE' }).then(r => r.json()),\n};\n`
186
- : `const BASE = import.meta.env.VITE_API_URL || 'http://localhost:${bePort}';\n\nconst headers = () => ({\n 'Content-Type': 'application/json',\n ...(localStorage.getItem('token') ? { Authorization: \`Bearer \${localStorage.getItem('token')}\` } : {}),\n});\n\nexport const api = {\n get: (url) => fetch(BASE + url, { headers: headers() }).then(r => r.json()),\n post: (url, data) => fetch(BASE + url, { method: 'POST', headers: headers(), body: JSON.stringify(data) }).then(r => r.json()),\n put: (url, data) => fetch(BASE + url, { method: 'PUT', headers: headers(), body: JSON.stringify(data) }).then(r => r.json()),\n delete: (url) => fetch(BASE + url, { method: 'DELETE', headers: headers() }).then(r => r.json()),\n};\n`;
187
-
188
- const srcDir = path.join(feDir, 'src', 'lib');
189
- await fse.ensureDir(srcDir);
190
- await fse.writeFile(path.join(srcDir, 'api.js'), apiClient);
191
-
192
- // Proxy en vite.config si aplica
193
- const viteConfig = path.join(feDir, 'vite.config.js');
194
- if (await fse.pathExists(viteConfig)) {
195
- const content = await fse.readFile(viteConfig, 'utf8');
196
- if (!content.includes('proxy')) {
197
- const proxy = `\n server: {\n proxy: {\n '/api': {\n target: 'http://localhost:${bePort}',\n changeOrigin: true,\n },\n },\n },`;
198
- await fse.writeFile(viteConfig, content.replace('plugins:', proxy + '\n plugins:'));
199
- }
200
- }
201
- }
202
-
203
119
  // ── ARQUITECTURA ──────────────────────────────────────────────────────────────
204
120
  async function applyArchitecture(config, root) {
205
121
  const beDir = path.join(root, 'backend', 'src');
@@ -217,72 +133,67 @@ async function applyArchitecture(config, root) {
217
133
  }
218
134
  }
219
135
 
220
- // ── ENV ───────────────────────────────────────────────────────────────────────
136
+ // ── .ENV ──────────────────────────────────────────────────────────────────────
221
137
  async function generateEnv(config, root) {
222
138
  const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
139
+ const bePort = isPython ? '8000' : '3001';
140
+
223
141
  const dbUrl = {
224
142
  postgres: 'postgresql://user:password@localhost:5432/mydb',
225
143
  mysql: 'mysql://user:password@localhost:3306/mydb',
226
144
  mongo: 'mongodb://localhost:27017/mydb',
227
145
  sqlite: 'file:./dev.db',
228
- supabase: config.supabaseUrl || 'https://your-project.supabase.co',
146
+ supabase: 'https://your-project.supabase.co',
229
147
  }[config.db] || '';
230
148
 
231
- const feEnv = `VITE_API_URL=http://localhost:${isPython ? '8000' : '3001'}\n` +
232
- (config.db === 'supabase' ? `VITE_SUPABASE_URL=${config.supabaseUrl || 'https://your-project.supabase.co'}\nVITE_SUPABASE_ANON_KEY=${config.supabaseKey || 'your-anon-key'}\n` : '');
149
+ const feEnv = `VITE_API_URL=http://localhost:${bePort}\n`;
150
+ const beEnv = isPython
151
+ ? `DATABASE_URL=${dbUrl}\nSECRET_KEY=change_me_in_production\nDEBUG=True\n`
152
+ : `PORT=3001\nNODE_ENV=development\nJWT_SECRET=change_me_in_production\n${dbUrl ? 'DATABASE_URL=' + dbUrl + '\n' : ''}`;
233
153
 
234
- const beEnv = isPython ? `DATABASE_URL=${dbUrl}\nSECRET_KEY=change_me_in_production\nDEBUG=True\n`
235
- : `PORT=3001\nNODE_ENV=development\nJWT_SECRET=change_me_in_production\n${dbUrl ? 'DATABASE_URL=' + dbUrl + '\n' : ''}` +
236
- (config.db === 'supabase' ? `SUPABASE_URL=${config.supabaseUrl || ''}\nSUPABASE_KEY=${config.supabaseKey || ''}\n` : '') +
237
- (config.extras?.includes('stripe') ? 'STRIPE_SECRET_KEY=sk_test_...\nSTRIPE_WEBHOOK_SECRET=whsec_...\n' : '');
154
+ const feDir = path.join(root, 'frontend');
155
+ const beDir = path.join(root, 'backend');
238
156
 
239
- await fse.writeFile(path.join(root, 'frontend', '.env.example'), feEnv);
240
- await fse.writeFile(path.join(root, 'frontend', '.env'), feEnv);
241
- if (!isPython) {
242
- await fse.writeFile(path.join(root, 'backend', '.env.example'), beEnv);
243
- await fse.writeFile(path.join(root, 'backend', '.env'), beEnv);
157
+ if (await fse.pathExists(feDir)) {
158
+ await fse.writeFile(path.join(feDir, '.env'), feEnv);
159
+ await fse.writeFile(path.join(feDir, '.env.example'), feEnv);
160
+ }
161
+ if (await fse.pathExists(beDir)) {
162
+ await fse.writeFile(path.join(beDir, '.env'), beEnv);
163
+ await fse.writeFile(path.join(beDir, '.env.example'), beEnv);
244
164
  }
245
165
  }
246
166
 
247
167
  // ── README ────────────────────────────────────────────────────────────────────
248
168
  async function generateReadme(config, root) {
249
169
  const ts = config.typescript ? ' + TypeScript' : '';
250
- const content = `# ${config.name}\n\n> Generado con [NovaTec CLI](https://www.npmjs.com/package/novatec-cli) — Enterprise Fullstack Generator\n\n## Stack\n\n| Capa | Tecnología |\n|------|------------|\n| Frontend | **${config.frontend}**${ts} |\n| Backend | **${config.backend}** |\n| Base de datos | **${config.db || 'N/A'}** |\n| Arquitectura | **${config.architecture || 'mvc'}** |\n| Modo | **${config.mode || 'estándar'}** |\n\n## Inicio rápido\n\n\`\`\`bash\n# Frontend\ncd ${config.name}/frontend && npm install && npm run dev\n\n# Backend\ncd ${config.name}/backend && npm install && npm run dev\n\`\`\`\n${config.mode === 'business' ? '\n## Rutas del sistema\n\n| Ruta | Descripción |\n|------|-------------|\n| `/` | Landing page |\n| `/login` | Autenticación |\n| `/register` | Registro |\n| `/dashboard` | Panel administrativo |\n| `/api-docs` | Swagger UI |\n' : ''}\n---\n*Generado el ${new Date().toLocaleDateString('es-MX')} con NovaTec CLI v3.0.0*\n`;
170
+ const content =
171
+ `# ${config.name}
172
+
173
+ > Generado con [NovaTec CLI](https://www.npmjs.com/package/novatec-cli)
174
+
175
+ ## Stack
176
+
177
+ | Capa | Tecnología |
178
+ |------|------------|
179
+ | Frontend | **${config.frontend}**${ts} |
180
+ | Backend | **${config.backend}** |
181
+ | Base de datos | **${config.db || 'N/A'}** |
182
+ | Arquitectura | **${config.architecture || 'mvc'}** |
183
+
184
+ ## Inicio rápido
185
+
186
+ \`\`\`bash
187
+ # Frontend
188
+ cd ${config.name}/frontend
189
+ npm install
190
+ npm run dev
191
+
192
+ # Backend
193
+ cd ${config.name}/backend
194
+ npm install
195
+ npm run dev
196
+ \`\`\`
197
+ `;
251
198
  await fse.writeFile(path.join(root, 'README.md'), content);
252
199
  }
253
-
254
- // ── ESLINT ────────────────────────────────────────────────────────────────────
255
- async function setupEslint(root) {
256
- const fe = path.join(root, 'frontend');
257
- await fse.writeFile(path.join(fe, '.eslintrc.json'), JSON.stringify({ env: { browser: true, es2021: true }, extends: ['eslint:recommended'], parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, rules: { 'no-unused-vars': 'warn' } }, null, 2));
258
- await fse.writeFile(path.join(fe, '.prettierrc'), JSON.stringify({ semi: true, singleQuote: true, tabWidth: 2, trailingComma: 'es5' }, null, 2));
259
- }
260
-
261
- // ── TESTING ───────────────────────────────────────────────────────────────────
262
- async function setupTesting(root) {
263
- const testDir = path.join(root, 'frontend', 'src', '__tests__');
264
- await fse.ensureDir(testDir);
265
- await fse.writeFile(path.join(testDir, 'app.test.js'), `import { describe, it, expect } from 'vitest';\ndescribe('App', () => { it('works', () => expect(1 + 1).toBe(2)); });\n`);
266
- }
267
-
268
- // ── LOGS ──────────────────────────────────────────────────────────────────────
269
- async function setupLogs(config, root) {
270
- const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
271
- if (isPython) return;
272
- const loggerFile = `import winston from 'winston';\n\nexport const logger = winston.createLogger({\n level: process.env.LOG_LEVEL || 'info',\n format: winston.format.combine(\n winston.format.timestamp(),\n winston.format.colorize(),\n winston.format.printf(({ timestamp, level, message }) => \`\${timestamp} [\${level}]: \${message}\`)\n ),\n transports: [\n new winston.transports.Console(),\n new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),\n new winston.transports.File({ filename: 'logs/combined.log' }),\n ],\n});\n`;
273
- const logDir = path.join(root, 'backend', 'src', 'utils');
274
- await fse.ensureDir(logDir);
275
- await fse.writeFile(path.join(logDir, 'logger.js'), loggerFile);
276
- await fse.ensureDir(path.join(root, 'backend', 'logs'));
277
- await fse.writeFile(path.join(root, 'backend', 'logs', '.gitkeep'), '');
278
- }
279
-
280
- // ── SEO ───────────────────────────────────────────────────────────────────────
281
- async function setupSEO(config, root) {
282
- const publicDir = path.join(root, 'frontend', 'public');
283
- await fse.ensureDir(publicDir);
284
- await fse.writeFile(path.join(publicDir, 'robots.txt'), `User-agent: *\nAllow: /\nSitemap: https://yourdomain.com/sitemap.xml\n`);
285
- await fse.writeFile(path.join(publicDir, 'sitemap.xml'), `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>https://yourdomain.com/</loc><priority>1.0</priority></url>\n</urlset>\n`);
286
- }
287
-
288
- const GITIGNORE = `node_modules/\ndist/\n.next/\n.nuxt/\n.svelte-kit/\n.env\n*.log\nlogs/\n__pycache__/\n.venv/\n*.pyc\ntarget/\n*.class\n.DS_Store\n`;
package/lib/doctor.js CHANGED
@@ -1,12 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import boxen from 'boxen';
3
- import { spawnSync, execSync } from 'child_process';
4
-
5
- function check(cmd, args = ['--version']) {
6
- const r = spawnSync(cmd, args, { stdio: 'pipe', shell: true, encoding: 'utf8' });
7
- if (r.status !== 0) return null;
8
- return (r.stdout || r.stderr || '').trim().split('\n')[0];
9
- }
3
+ import { execSync } from 'child_process';
10
4
 
11
5
  function getVersion(cmd) {
12
6
  try { return execSync(`${cmd} --version`, { encoding: 'utf8', stdio: 'pipe' }).trim().split('\n')[0]; } catch { return null; }