novatec-cli 2.0.0 → 3.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/bin/index.js +109 -169
- package/lib/add.js +122 -0
- package/lib/create.js +220 -310
- package/lib/doctor.js +167 -0
- package/lib/generators/business.js +262 -0
- package/lib/generators/deploy.js +166 -0
- package/lib/generators/docker.js +97 -0
- package/lib/generators/security.js +132 -0
- package/lib/prompts.js +159 -188
- package/package.json +1 -1
package/lib/create.js
CHANGED
|
@@ -1,378 +1,288 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fse from 'fs-extra';
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import boxen from 'boxen';
|
|
5
|
+
import { spawnSync } 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 {
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const bar = chalk.white('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
|
|
20
|
-
process.stdout.write(`\r [${bar}] ${chalk.white(pct + '%')} ${chalk.gray(label)} `);
|
|
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
|
+
import { isInstalled, runCmd } from './utils.js';
|
|
14
|
+
|
|
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))} `);
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
// ── MAIN ──────────────────────────────────────────────────────────────────────
|
|
24
21
|
export async function runCreate(nameArg, options) {
|
|
25
|
-
const
|
|
22
|
+
const t0 = Date.now();
|
|
26
23
|
const config = await askProjectOptions(nameArg, options);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
// Asegurar carpeta
|
|
30
|
-
await fse.ensureDir(projectPath);
|
|
24
|
+
const root = path.resolve(process.cwd(), config.name);
|
|
25
|
+
await fse.ensureDir(root);
|
|
31
26
|
|
|
32
|
-
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
33
|
-
const isNode = !isPython && config.backend !== 'spring' && config.backend !== 'gin';
|
|
34
|
-
|
|
35
|
-
// Calcular pasos totales
|
|
36
27
|
const extras = config.extras || [];
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
extras.includes('
|
|
44
|
-
extras.includes('
|
|
45
|
-
extras.includes('
|
|
46
|
-
extras.includes('
|
|
47
|
-
extras.includes('
|
|
48
|
-
extras.includes('
|
|
49
|
-
extras.includes('
|
|
50
|
-
extras.includes('
|
|
51
|
-
|
|
52
|
-
config.
|
|
28
|
+
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
29
|
+
const pm = config.pm || 'npm';
|
|
30
|
+
|
|
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',
|
|
53
44
|
].filter(Boolean);
|
|
54
45
|
|
|
55
46
|
console.log();
|
|
56
47
|
let step = 0;
|
|
57
|
-
|
|
58
|
-
const tick = (label) => {
|
|
59
|
-
step++;
|
|
60
|
-
progressBar(step, steps.length, label);
|
|
61
|
-
};
|
|
48
|
+
const tick = (label) => { step++; bar(step, steps.length, label); };
|
|
62
49
|
|
|
63
50
|
// 1. Frontend
|
|
64
51
|
tick('Generando frontend...');
|
|
65
|
-
await generateFrontend(config,
|
|
52
|
+
await generateFrontend(config, root);
|
|
53
|
+
await connectFrontendToBackend(config, root);
|
|
66
54
|
|
|
67
|
-
// 2. Backend
|
|
55
|
+
// 2. Backend con arquitectura
|
|
68
56
|
tick('Generando backend...');
|
|
69
|
-
await generateBackend(config,
|
|
57
|
+
await generateBackend(config, root);
|
|
58
|
+
await applyArchitecture(config, root);
|
|
70
59
|
|
|
71
|
-
// 3. README
|
|
72
|
-
tick('
|
|
73
|
-
await
|
|
60
|
+
// 3. README
|
|
61
|
+
tick('README...');
|
|
62
|
+
await generateReadme(config, root);
|
|
74
63
|
|
|
75
|
-
// 4.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
tick('
|
|
82
|
-
await
|
|
83
|
-
}
|
|
84
|
-
if (extras.includes('eslint')) {
|
|
85
|
-
tick('ESLint + Prettier...');
|
|
86
|
-
await setupEslint(config, projectPath);
|
|
64
|
+
// 4. .env
|
|
65
|
+
tick('.env files...');
|
|
66
|
+
await generateEnv(config, root);
|
|
67
|
+
|
|
68
|
+
// 5. Business mode
|
|
69
|
+
if (config.mode === 'business') {
|
|
70
|
+
tick('Business mode...');
|
|
71
|
+
await generateBusiness(config, root);
|
|
87
72
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
73
|
+
|
|
74
|
+
// 6. Docker
|
|
75
|
+
if (extras.includes('docker')) {
|
|
76
|
+
tick('Docker...');
|
|
77
|
+
await generateDocker(config, root);
|
|
91
78
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
|
|
80
|
+
// 7. Seguridad
|
|
81
|
+
if (extras.includes('security')) {
|
|
82
|
+
tick('Seguridad...');
|
|
83
|
+
await generateSecurity(config, root);
|
|
95
84
|
}
|
|
85
|
+
|
|
86
|
+
// 8. GitHub Actions
|
|
96
87
|
if (extras.includes('ci')) {
|
|
97
88
|
tick('GitHub Actions...');
|
|
98
|
-
await
|
|
89
|
+
await generateDeploy(config, root);
|
|
99
90
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
91
|
+
|
|
92
|
+
// 9. ESLint
|
|
93
|
+
if (extras.includes('eslint')) {
|
|
94
|
+
tick('ESLint...');
|
|
95
|
+
await setupEslint(root);
|
|
103
96
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
|
|
98
|
+
// 10. Testing
|
|
99
|
+
if (extras.includes('testing')) {
|
|
100
|
+
tick('Testing...');
|
|
101
|
+
await setupTesting(root);
|
|
107
102
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
|
|
104
|
+
// 11. Swagger (ya incluido en security/backend, solo notificar)
|
|
105
|
+
if (extras.includes('swagger')) {
|
|
106
|
+
tick('Swagger...');
|
|
107
|
+
// generado en generateSecurity
|
|
111
108
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
|
|
110
|
+
// 12. Logs
|
|
111
|
+
if (extras.includes('logs')) {
|
|
112
|
+
tick('Logs...');
|
|
113
|
+
await setupLogs(config, root);
|
|
116
114
|
}
|
|
117
115
|
|
|
118
|
-
//
|
|
119
|
-
if (
|
|
120
|
-
tick('
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
spawnSync('npm', ['install', ...config.frontendDeps], { cwd: frontendPath, shell: true, stdio: 'pipe' });
|
|
124
|
-
} catch {}
|
|
116
|
+
// 13. SEO
|
|
117
|
+
if (extras.includes('seo')) {
|
|
118
|
+
tick('SEO...');
|
|
119
|
+
await setupSEO(config, root);
|
|
125
120
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
|
|
122
|
+
// 14. Git
|
|
123
|
+
if (extras.includes('git') && isInstalled('git')) {
|
|
124
|
+
tick('Git init...');
|
|
129
125
|
try {
|
|
130
|
-
spawnSync('
|
|
126
|
+
spawnSync('git', ['init'], { cwd: root, shell: true, stdio: 'pipe' });
|
|
127
|
+
await fse.writeFile(path.join(root, '.gitignore'), GITIGNORE);
|
|
131
128
|
} catch {}
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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 {}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
140
142
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
express: 'npm run dev', nestjs: 'npm run start:dev', fastify: 'npm run dev',
|
|
144
|
-
hono: 'npm run dev', fastapi: 'uvicorn main:app --reload', django: 'python manage.py runserver',
|
|
145
|
-
flask: 'python run.py', spring: 'mvn spring-boot:run', deno: 'deno task dev', gin: 'go run main.go',
|
|
146
|
-
};
|
|
143
|
+
bar(steps.length, steps.length, 'Completado');
|
|
144
|
+
console.log('\n');
|
|
147
145
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
)
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
|
|
146
|
+
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
|
+
|
|
149
|
+
console.log(boxen(
|
|
150
|
+
chalk.bold.white(' ✔ PROYECTO LISTO — NovaTec CLI\n\n') +
|
|
151
|
+
chalk.gray(' Nombre │ ') + chalk.white(config.name) + '\n' +
|
|
152
|
+
chalk.gray(' Modo │ ') + chalk.white(config.mode || 'estándar') + '\n' +
|
|
153
|
+
chalk.gray(' Frontend │ ') + chalk.white(config.frontend) + (config.typescript ? chalk.gray(' + TypeScript') : '') + '\n' +
|
|
154
|
+
chalk.gray(' Backend │ ') + chalk.white(config.backend) + '\n' +
|
|
155
|
+
chalk.gray(' Base datos │ ') + chalk.white(config.db || 'ninguna') + '\n' +
|
|
156
|
+
chalk.gray(' Arquitectura │ ') + chalk.white(config.architecture || 'mvc') + '\n' +
|
|
157
|
+
chalk.gray(' Tiempo │ ') + chalk.white(elapsed + 's') + '\n\n' +
|
|
158
|
+
chalk.gray(' ─────────────────────────────────────────────\n\n') +
|
|
159
|
+
chalk.white(' # Frontend\n') +
|
|
160
|
+
chalk.gray(` cd ${config.name}/frontend\n`) +
|
|
161
|
+
chalk.gray(` ${pm} ${pm === 'yarn' ? '' : 'run '}dev\n\n`) +
|
|
162
|
+
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') : ''),
|
|
166
|
+
{ padding: 1, margin: { top: 1, left: 2 }, borderStyle: 'round', borderColor: 'white', dimBorder: true }
|
|
167
|
+
));
|
|
168
|
+
|
|
169
|
+
// Abrir VS Code
|
|
164
170
|
if (isInstalled('code')) {
|
|
165
|
-
try {
|
|
166
|
-
|
|
167
|
-
console.log(' ' + chalk.gray('VS Code abierto automáticamente ✔'));
|
|
168
|
-
} catch {}
|
|
171
|
+
try { spawnSync('code', [root], { shell: true, stdio: 'ignore', detached: true }); } catch {}
|
|
172
|
+
console.log(' ' + chalk.gray('VS Code abierto ✔'));
|
|
169
173
|
}
|
|
170
|
-
|
|
171
174
|
console.log();
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
// ──
|
|
175
|
-
async function
|
|
176
|
-
const
|
|
177
|
-
const dbSection = config.db && config.db !== 'none'
|
|
178
|
-
? `\n## Base de datos\n\n**${config.db}** — Configura la conexión en \`backend/.env\`\n` : '';
|
|
179
|
-
|
|
180
|
-
const content = `# ${config.name}
|
|
181
|
-
|
|
182
|
-
> Proyecto generado con [NOVATEC CLI](https://www.npmjs.com/package/novatec-cli)
|
|
183
|
-
|
|
184
|
-
## Stack
|
|
185
|
-
|
|
186
|
-
| Capa | Tecnología |
|
|
187
|
-
|------|-----------|
|
|
188
|
-
| Frontend | **${config.frontend}**${ts} |
|
|
189
|
-
| Backend | **${config.backend}**${ts} |
|
|
190
|
-
| Base de datos | **${config.db || 'No configurada'}** |
|
|
191
|
-
${dbSection}
|
|
192
|
-
## Inicio rápido
|
|
193
|
-
|
|
194
|
-
\`\`\`bash
|
|
195
|
-
# Frontend
|
|
196
|
-
cd ${config.name}/frontend
|
|
197
|
-
npm install
|
|
198
|
-
npm run dev
|
|
199
|
-
|
|
200
|
-
# Backend
|
|
201
|
-
cd ${config.name}/backend
|
|
202
|
-
${['fastapi','django','flask'].includes(config.backend) ? 'pip install -r requirements.txt\n' : 'npm install\n'}\
|
|
203
|
-
${config.backend === 'fastapi' ? 'uvicorn main:app --reload' :
|
|
204
|
-
config.backend === 'django' ? 'python manage.py runserver' :
|
|
205
|
-
config.backend === 'flask' ? 'python run.py' :
|
|
206
|
-
config.backend === 'spring' ? 'mvn spring-boot:run' :
|
|
207
|
-
config.backend === 'gin' ? 'go run main.go' : 'npm run dev'}
|
|
208
|
-
\`\`\`
|
|
209
|
-
${config.extras?.includes('docker') ? '\n## Docker\n\n```bash\ndocker-compose up --build\n```\n' : ''}
|
|
210
|
-
## Generado con
|
|
211
|
-
|
|
212
|
-
\`\`\`
|
|
213
|
-
novatec create ${config.name}
|
|
214
|
-
\`\`\`
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
*Generado el ${new Date().toLocaleDateString('es-MX')} con NOVATEC CLI*
|
|
218
|
-
`;
|
|
219
|
-
await fse.writeFile(path.join(projectPath, 'README.md'), content);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// ── AUTH JWT ──────────────────────────────────────────────────────────────────
|
|
223
|
-
async function setupAuth(config, projectPath) {
|
|
224
|
-
const backendPath = path.join(projectPath, 'backend');
|
|
177
|
+
// ── CONEXIÓN FE → BE ──────────────────────────────────────────────────────────
|
|
178
|
+
async function connectFrontendToBackend(config, root) {
|
|
179
|
+
const feDir = path.join(root, 'frontend');
|
|
225
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
|
+
}
|
|
226
202
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
203
|
+
// ── ARQUITECTURA ──────────────────────────────────────────────────────────────
|
|
204
|
+
async function applyArchitecture(config, root) {
|
|
205
|
+
const beDir = path.join(root, 'backend', 'src');
|
|
206
|
+
const arch = config.architecture || 'mvc';
|
|
231
207
|
|
|
232
|
-
const
|
|
208
|
+
const dirs = {
|
|
209
|
+
mvc: ['controllers', 'models', 'routes', 'middlewares', 'services'],
|
|
210
|
+
hexagonal: ['domain/entities', 'domain/ports', 'application/usecases', 'infrastructure/adapters', 'infrastructure/repositories', 'interfaces/http'],
|
|
211
|
+
clean: ['domain/entities', 'domain/usecases', 'data/repositories', 'data/datasources', 'presentation/controllers', 'presentation/routes'],
|
|
212
|
+
};
|
|
233
213
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
req.user = jwt.verify(token, process.env.JWT_SECRET || 'change_me');
|
|
239
|
-
next();
|
|
240
|
-
} catch {
|
|
241
|
-
res.status(401).json({ message: 'Token inválido' });
|
|
214
|
+
for (const dir of (dirs[arch] || dirs.mvc)) {
|
|
215
|
+
await fse.ensureDir(path.join(beDir, dir));
|
|
216
|
+
await fse.writeFile(path.join(beDir, dir, '.gitkeep'), '');
|
|
242
217
|
}
|
|
243
218
|
}
|
|
244
|
-
`;
|
|
245
|
-
const authRoutes = `import { Router } from 'express';
|
|
246
|
-
import jwt from 'jsonwebtoken';
|
|
247
|
-
import bcrypt from 'bcryptjs';
|
|
248
|
-
|
|
249
|
-
const router = Router();
|
|
250
|
-
|
|
251
|
-
// POST /api/auth/register
|
|
252
|
-
router.post('/register', async (req, res) => {
|
|
253
|
-
const { email, password } = req.body;
|
|
254
|
-
const hash = await bcrypt.hash(password, 10);
|
|
255
|
-
// TODO: guardar usuario en BD
|
|
256
|
-
const token = jwt.sign({ email }, process.env.JWT_SECRET || 'change_me', { expiresIn: '7d' });
|
|
257
|
-
res.json({ token });
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// POST /api/auth/login
|
|
261
|
-
router.post('/login', async (req, res) => {
|
|
262
|
-
const { email, password } = req.body;
|
|
263
|
-
// TODO: buscar usuario en BD y verificar hash
|
|
264
|
-
const token = jwt.sign({ email }, process.env.JWT_SECRET || 'change_me', { expiresIn: '7d' });
|
|
265
|
-
res.json({ token });
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
export default router;
|
|
269
|
-
`;
|
|
270
|
-
|
|
271
|
-
await fse.writeFile(path.join(authDir, 'middleware.js'), middleware);
|
|
272
|
-
await fse.writeFile(path.join(authDir, 'routes.js'), authRoutes);
|
|
273
|
-
|
|
274
|
-
// Instalar deps de auth
|
|
275
|
-
try {
|
|
276
|
-
spawnSync('npm', ['install', 'jsonwebtoken', 'bcryptjs'], { cwd: backendPath, shell: true, stdio: 'pipe' });
|
|
277
|
-
} catch {}
|
|
278
|
-
}
|
|
279
219
|
|
|
280
|
-
// ──
|
|
281
|
-
async function
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await fse.writeFile(
|
|
303
|
-
|
|
304
|
-
await fse.ensureDir(path.join(frontendPath, 'src'));
|
|
305
|
-
await fse.writeFile(cssPath, tailwindImports);
|
|
220
|
+
// ── ENV ───────────────────────────────────────────────────────────────────────
|
|
221
|
+
async function generateEnv(config, root) {
|
|
222
|
+
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
223
|
+
const dbUrl = {
|
|
224
|
+
postgres: 'postgresql://user:password@localhost:5432/mydb',
|
|
225
|
+
mysql: 'mysql://user:password@localhost:3306/mydb',
|
|
226
|
+
mongo: 'mongodb://localhost:27017/mydb',
|
|
227
|
+
sqlite: 'file:./dev.db',
|
|
228
|
+
supabase: config.supabaseUrl || 'https://your-project.supabase.co',
|
|
229
|
+
}[config.db] || '';
|
|
230
|
+
|
|
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` : '');
|
|
233
|
+
|
|
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' : '');
|
|
238
|
+
|
|
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);
|
|
306
244
|
}
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
spawnSync('npm', ['install', '-D', 'tailwindcss', 'postcss', 'autoprefixer'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
|
|
310
|
-
} catch {}
|
|
311
245
|
}
|
|
312
246
|
|
|
313
|
-
// ──
|
|
314
|
-
async function
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
await fse.writeFile(path.join(
|
|
318
|
-
try { spawnSync('npm', ['install', '-D', 'eslint', 'prettier'], { cwd: frontendPath, shell: true, stdio: 'pipe' }); } catch {}
|
|
247
|
+
// ── README ────────────────────────────────────────────────────────────────────
|
|
248
|
+
async function generateReadme(config, root) {
|
|
249
|
+
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`;
|
|
251
|
+
await fse.writeFile(path.join(root, 'README.md'), content);
|
|
319
252
|
}
|
|
320
253
|
|
|
321
|
-
// ──
|
|
322
|
-
async function
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
spawnSync('npx', ['husky', 'init'], { cwd: frontendPath, shell: true, stdio: 'pipe' });
|
|
327
|
-
await fse.ensureDir(path.join(frontendPath, '.husky'));
|
|
328
|
-
await fse.writeFile(path.join(frontendPath, '.husky', 'pre-commit'), '#!/bin/sh\nnpx lint-staged\n');
|
|
329
|
-
} catch {}
|
|
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));
|
|
330
259
|
}
|
|
331
260
|
|
|
332
|
-
// ──
|
|
333
|
-
async function setupTesting(
|
|
334
|
-
const
|
|
335
|
-
const testDir = path.join(frontendPath, 'src', '__tests__');
|
|
261
|
+
// ── TESTING ───────────────────────────────────────────────────────────────────
|
|
262
|
+
async function setupTesting(root) {
|
|
263
|
+
const testDir = path.join(root, 'frontend', 'src', '__tests__');
|
|
336
264
|
await fse.ensureDir(testDir);
|
|
337
|
-
await fse.writeFile(path.join(testDir, 'app.test.js'), `import { describe, it, expect } from 'vitest';\
|
|
338
|
-
try { spawnSync('npm', ['install', '-D', 'vitest'], { cwd: frontendPath, shell: true, stdio: 'pipe' }); } catch {}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// ── GitHub Actions ────────────────────────────────────────────────────────────
|
|
342
|
-
async function setupCI(config, projectPath) {
|
|
343
|
-
const ciDir = path.join(projectPath, '.github', 'workflows');
|
|
344
|
-
await fse.ensureDir(ciDir);
|
|
345
|
-
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
346
|
-
await fse.writeFile(path.join(ciDir, 'ci.yml'), `name: CI\non:\n push:\n branches: [main]\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n - run: cd frontend && npm ci && npm run build\n${isPython ? '' : ' - run: cd backend && npm ci\n'}`);
|
|
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`);
|
|
347
266
|
}
|
|
348
267
|
|
|
349
|
-
// ──
|
|
350
|
-
async function
|
|
268
|
+
// ── LOGS ──────────────────────────────────────────────────────────────────────
|
|
269
|
+
async function setupLogs(config, root) {
|
|
351
270
|
const isPython = ['fastapi', 'django', 'flask'].includes(config.backend);
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
await fse.
|
|
358
|
-
|
|
359
|
-
);
|
|
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'), '');
|
|
360
278
|
}
|
|
361
279
|
|
|
362
|
-
// ──
|
|
363
|
-
async function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
config.db === 'mongo' ? 'mongodb://localhost:27017/mydb' :
|
|
369
|
-
config.db === 'sqlite' ? 'file:./dev.db' : '';
|
|
370
|
-
if (!isPython) {
|
|
371
|
-
await fse.writeFile(path.join(projectPath, 'backend', '.env.example'),
|
|
372
|
-
`PORT=3001\nNODE_ENV=development\nJWT_SECRET=change_me_in_production\n${dbUrl ? 'DATABASE_URL=' + dbUrl + '\n' : ''}`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
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`);
|
|
375
286
|
}
|
|
376
287
|
|
|
377
|
-
const
|
|
378
|
-
const GITIGNORE = `node_modules/\ndist/\n.next/\n.nuxt/\n.svelte-kit/\n.env\n*.log\n__pycache__/\n.venv/\n*.pyc\ntarget/\n*.class\n.DS_Store\n`;
|
|
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`;
|