@openfactu/cli 0.0.6 → 0.0.8
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/LICENSE +21 -0
- package/README.md +164 -8
- package/dist/src/commands/backup.d.ts +2 -0
- package/dist/src/commands/backup.js +424 -0
- package/dist/src/commands/deploy.js +492 -69
- package/dist/src/commands/doctor.d.ts +2 -0
- package/dist/src/commands/doctor.js +295 -0
- package/dist/src/commands/install-quick.d.ts +2 -0
- package/dist/src/commands/install-quick.js +249 -0
- package/dist/src/commands/install-script.d.ts +2 -0
- package/dist/src/commands/install-script.js +474 -0
- package/dist/src/commands/install.js +969 -75
- package/dist/src/commands/monitoring.d.ts +2 -0
- package/dist/src/commands/monitoring.js +352 -0
- package/dist/src/commands/service.d.ts +2 -0
- package/dist/src/commands/service.js +402 -0
- package/dist/src/commands/setup.js +7 -2
- package/dist/src/commands/sync-ports.d.ts +2 -0
- package/dist/src/commands/sync-ports.js +298 -0
- package/dist/src/commands/uninstall.d.ts +2 -0
- package/dist/src/commands/uninstall.js +189 -0
- package/dist/src/index.js +17 -1
- package/dist/src/utils/config.d.ts +8 -0
- package/dist/src/utils/config.js +25 -1
- package/dist/src/utils/env.d.ts +11 -0
- package/dist/src/utils/env.js +31 -0
- package/dist/src/utils/helpers.d.ts +22 -0
- package/dist/src/utils/helpers.js +244 -0
- package/dist/src/utils/monitoring.d.ts +38 -0
- package/dist/src/utils/monitoring.js +353 -0
- package/dist/src/utils/paths.d.ts +1 -0
- package/dist/src/utils/paths.js +2 -0
- package/package.json +8 -5
|
@@ -13,6 +13,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
13
13
|
const os_1 = __importDefault(require("os"));
|
|
14
14
|
const logger_1 = require("../utils/logger");
|
|
15
15
|
const paths_1 = require("../utils/paths");
|
|
16
|
+
const helpers_1 = require("../utils/helpers");
|
|
16
17
|
function getLocalIPs() {
|
|
17
18
|
const interfaces = os_1.default.networkInterfaces();
|
|
18
19
|
const ips = [];
|
|
@@ -27,6 +28,26 @@ function getLocalIPs() {
|
|
|
27
28
|
}
|
|
28
29
|
return ips;
|
|
29
30
|
}
|
|
31
|
+
function checkPortInUse(port) {
|
|
32
|
+
try {
|
|
33
|
+
const output = (0, child_process_1.execSync)(`lsof -i :${port} -sTCP:LISTEN -t 2>/dev/null | grep -v 'docker-pr' || true`, {
|
|
34
|
+
stdio: 'pipe',
|
|
35
|
+
}).toString().trim();
|
|
36
|
+
if (output) {
|
|
37
|
+
const pid = output.split('\n')[0].trim();
|
|
38
|
+
let processName = 'desconocido';
|
|
39
|
+
try {
|
|
40
|
+
processName = (0, child_process_1.execSync)(`ps -p ${pid} -o comm= 2>/dev/null || echo 'desconocido'`, { stdio: 'pipe' }).toString().trim();
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
return { inUse: true, process: `${processName} (PID: ${pid})` };
|
|
44
|
+
}
|
|
45
|
+
return { inUse: false };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { inUse: false };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
30
51
|
function readEnv(envPath) {
|
|
31
52
|
const env = {};
|
|
32
53
|
if (!fs_1.default.existsSync(envPath))
|
|
@@ -54,7 +75,8 @@ function registerDeployCommand(program) {
|
|
|
54
75
|
program
|
|
55
76
|
.command('deploy')
|
|
56
77
|
.description('Configura OpenFactu para producción (acceso externo)')
|
|
57
|
-
.
|
|
78
|
+
.option('--with-monitoring', 'Incluir stack de monitoreo (Grafana, Prometheus, etc.)')
|
|
79
|
+
.action(async (opts) => {
|
|
58
80
|
console.log();
|
|
59
81
|
console.log(chalk_1.default.bold.white(' OpenFactu — Configurar Despliegue'));
|
|
60
82
|
console.log(chalk_1.default.dim(' ────────────────────────────────────'));
|
|
@@ -82,6 +104,7 @@ function registerDeployCommand(program) {
|
|
|
82
104
|
let host = 'localhost';
|
|
83
105
|
let serverPort = '3000';
|
|
84
106
|
let webPort = '8080';
|
|
107
|
+
let dbPort = '5432';
|
|
85
108
|
let useSSL = false;
|
|
86
109
|
if (mode === 'lan') {
|
|
87
110
|
const ipChoices = localIPs.map((ip) => ({ name: ip, value: ip }));
|
|
@@ -103,6 +126,15 @@ function registerDeployCommand(program) {
|
|
|
103
126
|
else {
|
|
104
127
|
host = selectedIP;
|
|
105
128
|
}
|
|
129
|
+
const { ssl } = await inquirer_1.default.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: 'confirm',
|
|
132
|
+
name: 'ssl',
|
|
133
|
+
message: '¿Usar HTTPS? (certificado auto-firmado para red local)',
|
|
134
|
+
default: false,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
useSSL = ssl;
|
|
106
138
|
}
|
|
107
139
|
else if (mode === 'public') {
|
|
108
140
|
const { domain } = await inquirer_1.default.prompt([
|
|
@@ -123,12 +155,56 @@ function registerDeployCommand(program) {
|
|
|
123
155
|
]);
|
|
124
156
|
useSSL = ssl;
|
|
125
157
|
}
|
|
158
|
+
// Verificar conflictos de puertos si se usa SSL
|
|
159
|
+
let httpPort = '80';
|
|
160
|
+
let httpsPort = '443';
|
|
161
|
+
if (useSSL) {
|
|
162
|
+
const port80 = checkPortInUse(80);
|
|
163
|
+
const port443 = checkPortInUse(443);
|
|
164
|
+
if (port80.inUse || port443.inUse) {
|
|
165
|
+
logger_1.log.blank();
|
|
166
|
+
logger_1.log.warn('Puertos en conflicto detectados:');
|
|
167
|
+
if (port80.inUse)
|
|
168
|
+
logger_1.log.warn(` Puerto 80: ocupado por ${port80.process}`);
|
|
169
|
+
if (port443.inUse)
|
|
170
|
+
logger_1.log.warn(` Puerto 443: ocupado por ${port443.process}`);
|
|
171
|
+
logger_1.log.blank();
|
|
172
|
+
const { portAction } = await inquirer_1.default.prompt([
|
|
173
|
+
{
|
|
174
|
+
type: 'list',
|
|
175
|
+
name: 'portAction',
|
|
176
|
+
message: '¿Cómo resolver el conflicto?',
|
|
177
|
+
choices: [
|
|
178
|
+
{ name: 'Usar puertos alternativos (8080/8443)', value: 'alternate' },
|
|
179
|
+
{ name: 'Especificar puertos personalizados', value: 'custom' },
|
|
180
|
+
{ name: 'Continuar con 80/443 (puede fallar)', value: 'continue' },
|
|
181
|
+
{ name: 'Desactivar HTTPS', value: 'nossl' },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
]);
|
|
185
|
+
if (portAction === 'nossl') {
|
|
186
|
+
useSSL = false;
|
|
187
|
+
}
|
|
188
|
+
else if (portAction === 'alternate') {
|
|
189
|
+
httpPort = '8080';
|
|
190
|
+
httpsPort = '8443';
|
|
191
|
+
}
|
|
192
|
+
else if (portAction === 'custom') {
|
|
193
|
+
const { customHttp, customHttps } = await inquirer_1.default.prompt([
|
|
194
|
+
{ type: 'input', name: 'customHttp', message: 'Puerto HTTP alternativo:', default: '8080' },
|
|
195
|
+
{ type: 'input', name: 'customHttps', message: 'Puerto HTTPS alternativo:', default: '8443' },
|
|
196
|
+
]);
|
|
197
|
+
httpPort = customHttp;
|
|
198
|
+
httpsPort = customHttps;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
126
202
|
// Puertos
|
|
127
203
|
const { ports } = await inquirer_1.default.prompt([
|
|
128
204
|
{
|
|
129
205
|
type: 'confirm',
|
|
130
206
|
name: 'ports',
|
|
131
|
-
message: `¿Usar puertos por defecto? (web: 8080, api: 3000)`,
|
|
207
|
+
message: `¿Usar puertos por defecto? (web: 8080, api: 3000, db: 5432)`,
|
|
132
208
|
default: true,
|
|
133
209
|
},
|
|
134
210
|
]);
|
|
@@ -136,32 +212,42 @@ function registerDeployCommand(program) {
|
|
|
136
212
|
const answers = await inquirer_1.default.prompt([
|
|
137
213
|
{ type: 'input', name: 'webPort', message: 'Puerto web:', default: '8080' },
|
|
138
214
|
{ type: 'input', name: 'serverPort', message: 'Puerto API:', default: '3000' },
|
|
215
|
+
{ type: 'input', name: 'dbPort', message: 'Puerto BD (host):', default: '5432' },
|
|
139
216
|
]);
|
|
140
217
|
webPort = answers.webPort;
|
|
141
218
|
serverPort = answers.serverPort;
|
|
219
|
+
dbPort = answers.dbPort;
|
|
142
220
|
}
|
|
143
|
-
// Password de BD
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
name: 'dbPassword',
|
|
148
|
-
message: 'Password de PostgreSQL:',
|
|
149
|
-
default: 'openfactu_pass',
|
|
150
|
-
},
|
|
151
|
-
]);
|
|
221
|
+
// Password de BD: se reutiliza la fijada en `install` (está en el .env).
|
|
222
|
+
// No se vuelve a preguntar: cambiarla aquí no surte efecto sobre un
|
|
223
|
+
// volumen de Postgres ya inicializado, así que evitamos esa trampa.
|
|
224
|
+
const dbPassword = readEnv(envPath).POSTGRES_PASSWORD || (0, helpers_1.generatePassword)(24);
|
|
152
225
|
// 3. Construir configuración
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
226
|
+
// Con SSL el ÚNICO punto de entrada es Caddy en httpsPort: enruta
|
|
227
|
+
// "/" → web:80 y "/api/*" → server:3000. Por eso las URLs de cara al
|
|
228
|
+
// usuario (web, API, VITE_API_URL, CORS_ORIGIN) deben apuntar a Caddy,
|
|
229
|
+
// no a los puertos HTTP planos web/api (8080/3000).
|
|
230
|
+
let webUrl;
|
|
231
|
+
let apiUrl;
|
|
232
|
+
if (useSSL) {
|
|
233
|
+
const base = httpsPort === '443' ? `https://${host}` : `https://${host}:${httpsPort}`;
|
|
234
|
+
webUrl = base;
|
|
235
|
+
apiUrl = base;
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
webUrl = webPort === '80' || webPort === '443'
|
|
239
|
+
? `http://${host}`
|
|
240
|
+
: `http://${host}:${webPort}`;
|
|
241
|
+
apiUrl = serverPort === '80' || serverPort === '443'
|
|
242
|
+
? `http://${host}`
|
|
243
|
+
: `http://${host}:${serverPort}`;
|
|
244
|
+
}
|
|
160
245
|
logger_1.log.blank();
|
|
161
246
|
logger_1.log.title(' Resumen de configuración');
|
|
162
247
|
logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
|
|
163
248
|
logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
|
|
164
|
-
logger_1.log.info(`BD
|
|
249
|
+
logger_1.log.info(`BD Puerto: ${chalk_1.default.cyan(dbPort)} ${chalk_1.default.dim('(host)')}`);
|
|
250
|
+
logger_1.log.info(`BD Password: ${chalk_1.default.dim('(reutilizada de install)')}`);
|
|
165
251
|
logger_1.log.info(`SSL: ${useSSL ? chalk_1.default.green('Si') : chalk_1.default.dim('No')}`);
|
|
166
252
|
logger_1.log.blank();
|
|
167
253
|
const { confirm } = await inquirer_1.default.prompt([
|
|
@@ -176,11 +262,11 @@ function registerDeployCommand(program) {
|
|
|
176
262
|
const env = readEnv(envPath);
|
|
177
263
|
env.SERVER_PORT = serverPort;
|
|
178
264
|
env.WEB_PORT = webPort;
|
|
179
|
-
env.DB_PORT =
|
|
265
|
+
env.DB_PORT = dbPort;
|
|
180
266
|
env.POSTGRES_USER = env.POSTGRES_USER || 'openfactu';
|
|
181
267
|
env.POSTGRES_PASSWORD = dbPassword;
|
|
182
268
|
env.POSTGRES_DB = env.POSTGRES_DB || 'openfactudb';
|
|
183
|
-
env.DATABASE_URL = `postgresql://${env.POSTGRES_USER}:${dbPassword}@db:5432/${env.POSTGRES_DB}`;
|
|
269
|
+
env.DATABASE_URL = `postgresql://${env.POSTGRES_USER}:${encodeURIComponent(dbPassword)}@db:5432/${env.POSTGRES_DB}`;
|
|
184
270
|
env.VITE_API_URL = apiUrl;
|
|
185
271
|
env.HOST = host;
|
|
186
272
|
env.CORS_ORIGIN = webUrl;
|
|
@@ -240,47 +326,164 @@ function registerDeployCommand(program) {
|
|
|
240
326
|
restart: unless-stopped
|
|
241
327
|
networks:
|
|
242
328
|
- openfactu_net
|
|
329
|
+
`;
|
|
330
|
+
// Si SSL, añadir Caddy reverse proxy con Let's Encrypt (servicio dentro de
|
|
331
|
+
// services:). El bloque top-level networks: se añade SIEMPRE al final.
|
|
332
|
+
if (useSSL) {
|
|
333
|
+
composeContent += `
|
|
334
|
+
caddy:
|
|
335
|
+
image: caddy:2-alpine
|
|
336
|
+
container_name: openfactu-caddy
|
|
337
|
+
ports:
|
|
338
|
+
- "0.0.0.0:${httpPort}:80"
|
|
339
|
+
- "0.0.0.0:${httpsPort}:443"
|
|
340
|
+
volumes:
|
|
341
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
342
|
+
- caddy_data:/data
|
|
343
|
+
- caddy_config:/config
|
|
344
|
+
depends_on:
|
|
345
|
+
- web
|
|
346
|
+
- server
|
|
347
|
+
restart: unless-stopped
|
|
348
|
+
networks:
|
|
349
|
+
- openfactu_net
|
|
243
350
|
|
|
351
|
+
volumes:
|
|
352
|
+
caddy_data:
|
|
353
|
+
caddy_config:
|
|
354
|
+
`;
|
|
355
|
+
// Generar Caddyfile
|
|
356
|
+
const caddyfilePath = path_1.default.join(root, 'Caddyfile');
|
|
357
|
+
const isLAN = mode === 'lan';
|
|
358
|
+
const tlsDirective = isLAN ? 'tls internal' : '';
|
|
359
|
+
const httpRedirect = isLAN ? '' : `
|
|
360
|
+
http://${host} {
|
|
361
|
+
redir https://{host}{uri} permanent
|
|
362
|
+
}
|
|
363
|
+
`;
|
|
364
|
+
const caddyfileContent = `${host} {
|
|
365
|
+
${tlsDirective}
|
|
366
|
+
encode gzip
|
|
367
|
+
|
|
368
|
+
handle /api/* {
|
|
369
|
+
reverse_proxy server:3000
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
handle {
|
|
373
|
+
reverse_proxy web:80
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
log {
|
|
377
|
+
output file /var/log/caddy/access.log
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
${httpRedirect}`;
|
|
381
|
+
fs_1.default.writeFileSync(caddyfilePath, caddyfileContent);
|
|
382
|
+
composeContent += `
|
|
244
383
|
networks:
|
|
245
384
|
openfactu_net:
|
|
246
385
|
driver: bridge
|
|
247
386
|
`;
|
|
248
|
-
|
|
249
|
-
|
|
387
|
+
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
388
|
+
prodSpinner.succeed('docker-compose.prod.yml y Caddyfile generados');
|
|
389
|
+
logger_1.log.blank();
|
|
390
|
+
if (isLAN) {
|
|
391
|
+
logger_1.log.info('Caddy generará certificado auto-firmado para la red local');
|
|
392
|
+
logger_1.log.dim(' Los navegadores mostrarán advertencia de seguridad (es normal)');
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
logger_1.log.info('Caddy obtendrá certificados Let\'s Encrypt automáticamente');
|
|
396
|
+
logger_1.log.dim(' El DNS debe apuntar a este servidor');
|
|
397
|
+
logger_1.log.dim(' El primer request tardará unos segundos mientras se obtiene el certificado');
|
|
398
|
+
}
|
|
399
|
+
if (httpPort !== '80' || httpsPort !== '443') {
|
|
400
|
+
logger_1.log.blank();
|
|
401
|
+
logger_1.log.info(`Puertos alternativos: HTTP=${httpPort}, HTTPS=${httpsPort}`);
|
|
402
|
+
logger_1.log.dim(' Asegúrate de abrir estos puertos en el firewall');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
250
406
|
composeContent += `
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
# caddy:
|
|
255
|
-
# image: caddy:2-alpine
|
|
256
|
-
# ports:
|
|
257
|
-
# - "0.0.0.0:80:80"
|
|
258
|
-
# - "0.0.0.0:443:443"
|
|
259
|
-
# volumes:
|
|
260
|
-
# - ./Caddyfile:/etc/caddy/Caddyfile
|
|
261
|
-
# - caddy_data:/data
|
|
262
|
-
# depends_on:
|
|
263
|
-
# - web
|
|
264
|
-
# - server
|
|
265
|
-
# networks:
|
|
266
|
-
# - openfactu_net
|
|
267
|
-
#
|
|
268
|
-
# volumes:
|
|
269
|
-
# caddy_data:
|
|
270
|
-
#
|
|
271
|
-
# Caddyfile:
|
|
272
|
-
# ${host} {
|
|
273
|
-
# handle /api/* {
|
|
274
|
-
# reverse_proxy server:3000
|
|
275
|
-
# }
|
|
276
|
-
# handle {
|
|
277
|
-
# reverse_proxy web:80
|
|
278
|
-
# }
|
|
279
|
-
# }
|
|
407
|
+
networks:
|
|
408
|
+
openfactu_net:
|
|
409
|
+
driver: bridge
|
|
280
410
|
`;
|
|
411
|
+
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
412
|
+
prodSpinner.succeed('docker-compose.prod.yml generado');
|
|
413
|
+
}
|
|
414
|
+
// Generar docker-compose.prod.monitoring.yml si se pidió monitoreo
|
|
415
|
+
if (opts.withMonitoring) {
|
|
416
|
+
const monSpinner = (0, ora_1.default)('Generando docker-compose.prod.monitoring.yml...').start();
|
|
417
|
+
const monitoringComposePath = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
418
|
+
const monitoringCompose = `services:
|
|
419
|
+
pgadmin:
|
|
420
|
+
image: dpage/pgadmin4:latest
|
|
421
|
+
environment:
|
|
422
|
+
PGADMIN_DEFAULT_EMAIL: \${PGADMIN_EMAIL:-admin@openfactu.local}
|
|
423
|
+
PGADMIN_DEFAULT_PASSWORD: \${PGADMIN_PASSWORD:-admin}
|
|
424
|
+
PGADMIN_CONFIG_SERVER_MODE: 'False'
|
|
425
|
+
ports:
|
|
426
|
+
- "0.0.0.0:\${PGADMIN_PORT:-5050}:80"
|
|
427
|
+
volumes:
|
|
428
|
+
- ./storage/pgadmin_data:/var/lib/pgadmin
|
|
429
|
+
depends_on:
|
|
430
|
+
- db
|
|
431
|
+
restart: unless-stopped
|
|
432
|
+
networks:
|
|
433
|
+
- openfactu_net
|
|
434
|
+
|
|
435
|
+
grafana:
|
|
436
|
+
image: grafana/grafana:latest
|
|
437
|
+
environment:
|
|
438
|
+
- GF_SECURITY_ADMIN_USER=\${GRAFANA_USER:-admin}
|
|
439
|
+
- GF_SECURITY_ADMIN_PASSWORD=\${GRAFANA_PASSWORD:-admin}
|
|
440
|
+
- GF_USERS_ALLOW_SIGN_UP=false
|
|
441
|
+
ports:
|
|
442
|
+
- "0.0.0.0:\${GRAFANA_PORT:-3001}:3000"
|
|
443
|
+
volumes:
|
|
444
|
+
- ./storage/grafana_data:/var/lib/grafana
|
|
445
|
+
depends_on:
|
|
446
|
+
- prometheus
|
|
447
|
+
restart: unless-stopped
|
|
448
|
+
networks:
|
|
449
|
+
- openfactu_net
|
|
450
|
+
|
|
451
|
+
prometheus:
|
|
452
|
+
image: prom/prometheus:latest
|
|
453
|
+
command:
|
|
454
|
+
- '--config.file=/etc/prometheus/prometheus.yml'
|
|
455
|
+
- '--storage.tsdb.path=/prometheus'
|
|
456
|
+
- '--storage.tsdb.retention.time=15d'
|
|
457
|
+
- '--web.enable-lifecycle'
|
|
458
|
+
ports:
|
|
459
|
+
- "0.0.0.0:\${PROMETHEUS_PORT:-9090}:9090"
|
|
460
|
+
volumes:
|
|
461
|
+
- ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
|
462
|
+
- ./storage/prometheus_data:/prometheus
|
|
463
|
+
restart: unless-stopped
|
|
464
|
+
networks:
|
|
465
|
+
- openfactu_net
|
|
466
|
+
|
|
467
|
+
portainer:
|
|
468
|
+
image: portainer/portainer-ce:latest
|
|
469
|
+
command: -H unix:///var/run/docker.sock
|
|
470
|
+
ports:
|
|
471
|
+
- "0.0.0.0:\${PORTAINER_PORT:-9000}:9000"
|
|
472
|
+
volumes:
|
|
473
|
+
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
474
|
+
- ./storage/portainer_data:/data
|
|
475
|
+
restart: unless-stopped
|
|
476
|
+
networks:
|
|
477
|
+
- openfactu_net
|
|
478
|
+
|
|
479
|
+
networks:
|
|
480
|
+
openfactu_net:
|
|
481
|
+
name: openfactu_net
|
|
482
|
+
driver: bridge
|
|
483
|
+
`;
|
|
484
|
+
fs_1.default.writeFileSync(monitoringComposePath, monitoringCompose);
|
|
485
|
+
monSpinner.succeed('docker-compose.prod.monitoring.yml generado');
|
|
281
486
|
}
|
|
282
|
-
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
283
|
-
prodSpinner.succeed('docker-compose.prod.yml generado');
|
|
284
487
|
// 6. Preguntar si levantar
|
|
285
488
|
logger_1.log.blank();
|
|
286
489
|
const { start } = await inquirer_1.default.prompt([
|
|
@@ -319,7 +522,9 @@ networks:
|
|
|
319
522
|
else if (mode === 'public') {
|
|
320
523
|
logger_1.log.info('Asegúrate de que los puertos estén abiertos en el firewall');
|
|
321
524
|
if (useSSL) {
|
|
322
|
-
logger_1.log.info('
|
|
525
|
+
logger_1.log.info('Caddy obtendrá certificado Let\'s Encrypt automáticamente');
|
|
526
|
+
logger_1.log.dim(' El DNS debe apuntar a este servidor para que funcione');
|
|
527
|
+
logger_1.log.dim(' Puertos requeridos: 80 (HTTP) y 443 (HTTPS)');
|
|
323
528
|
}
|
|
324
529
|
}
|
|
325
530
|
logger_1.log.blank();
|
|
@@ -334,31 +539,223 @@ networks:
|
|
|
334
539
|
process.exitCode = 1;
|
|
335
540
|
}
|
|
336
541
|
});
|
|
542
|
+
// ── openfactu deploy:ssl ──
|
|
543
|
+
program
|
|
544
|
+
.command('deploy:ssl')
|
|
545
|
+
.description('Activa HTTPS en un despliegue existente con Caddy')
|
|
546
|
+
.option('--domain <domain>', 'Dominio para el certificado')
|
|
547
|
+
.option('--lan', 'Usar certificado auto-firmado para red local')
|
|
548
|
+
.option('--port <port>', 'Puerto HTTPS (default: 443)', '443')
|
|
549
|
+
.action(async (opts) => {
|
|
550
|
+
console.log();
|
|
551
|
+
console.log(chalk_1.default.bold.white(' OpenFactu — Activar HTTPS'));
|
|
552
|
+
console.log(chalk_1.default.dim(' ────────────────────────────────────'));
|
|
553
|
+
console.log();
|
|
554
|
+
try {
|
|
555
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
556
|
+
const envPath = path_1.default.join(root, '.env');
|
|
557
|
+
const env = readEnv(envPath);
|
|
558
|
+
const host = opts.domain || env.HOST || 'localhost';
|
|
559
|
+
const isLAN = opts.lan || false;
|
|
560
|
+
if (!isLAN && !opts.domain) {
|
|
561
|
+
const { domain } = await inquirer_1.default.prompt([
|
|
562
|
+
{
|
|
563
|
+
type: 'input',
|
|
564
|
+
name: 'domain',
|
|
565
|
+
message: 'Dominio para el certificado Let\'s Encrypt:',
|
|
566
|
+
default: host,
|
|
567
|
+
},
|
|
568
|
+
]);
|
|
569
|
+
opts.domain = domain;
|
|
570
|
+
}
|
|
571
|
+
const finalHost = opts.domain || host;
|
|
572
|
+
let httpsPort = opts.port || '443';
|
|
573
|
+
let httpPort = '80';
|
|
574
|
+
// Verificar conflictos de puertos
|
|
575
|
+
const port80 = checkPortInUse(80);
|
|
576
|
+
const port443 = checkPortInUse(443);
|
|
577
|
+
if (port80.inUse || port443.inUse) {
|
|
578
|
+
logger_1.log.blank();
|
|
579
|
+
logger_1.log.warn('Puertos en conflicto detectados:');
|
|
580
|
+
if (port80.inUse)
|
|
581
|
+
logger_1.log.warn(` Puerto 80: ocupado por ${port80.process}`);
|
|
582
|
+
if (port443.inUse)
|
|
583
|
+
logger_1.log.warn(` Puerto 443: ocupado por ${port443.process}`);
|
|
584
|
+
logger_1.log.blank();
|
|
585
|
+
const { portAction } = await inquirer_1.default.prompt([
|
|
586
|
+
{
|
|
587
|
+
type: 'list',
|
|
588
|
+
name: 'portAction',
|
|
589
|
+
message: '¿Cómo resolver el conflicto?',
|
|
590
|
+
choices: [
|
|
591
|
+
{ name: 'Usar puertos alternativos (8080/8443)', value: 'alternate' },
|
|
592
|
+
{ name: 'Especificar puertos personalizados', value: 'custom' },
|
|
593
|
+
{ name: 'Continuar con 80/443 (puede fallar)', value: 'continue' },
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
]);
|
|
597
|
+
if (portAction === 'alternate') {
|
|
598
|
+
httpPort = '8080';
|
|
599
|
+
httpsPort = '8443';
|
|
600
|
+
}
|
|
601
|
+
else if (portAction === 'custom') {
|
|
602
|
+
const { customHttp, customHttps } = await inquirer_1.default.prompt([
|
|
603
|
+
{ type: 'input', name: 'customHttp', message: 'Puerto HTTP alternativo:', default: '8080' },
|
|
604
|
+
{ type: 'input', name: 'customHttps', message: 'Puerto HTTPS alternativo:', default: '8443' },
|
|
605
|
+
]);
|
|
606
|
+
httpPort = customHttp;
|
|
607
|
+
httpsPort = customHttps;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
// Generar Caddyfile
|
|
611
|
+
const tlsDirective = isLAN ? 'tls internal' : '';
|
|
612
|
+
const httpRedirect = isLAN ? '' : `
|
|
613
|
+
http://${finalHost} {
|
|
614
|
+
redir https://${finalHost}{uri} permanent
|
|
615
|
+
}
|
|
616
|
+
`;
|
|
617
|
+
const caddyfileContent = `${finalHost} {
|
|
618
|
+
${tlsDirective}
|
|
619
|
+
encode gzip
|
|
620
|
+
|
|
621
|
+
handle /api/* {
|
|
622
|
+
reverse_proxy server:3000
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
handle {
|
|
626
|
+
reverse_proxy web:80
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
log {
|
|
630
|
+
output file /var/log/caddy/access.log
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
${httpRedirect}`;
|
|
634
|
+
const caddyfilePath = path_1.default.join(root, 'Caddyfile');
|
|
635
|
+
fs_1.default.writeFileSync(caddyfilePath, caddyfileContent);
|
|
636
|
+
// Actualizar docker-compose.prod.yml para añadir Caddy
|
|
637
|
+
const prodComposePath = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
638
|
+
let composeContent = fs_1.default.existsSync(prodComposePath)
|
|
639
|
+
? fs_1.default.readFileSync(prodComposePath, 'utf-8')
|
|
640
|
+
: '';
|
|
641
|
+
if (!composeContent.includes('caddy:')) {
|
|
642
|
+
const caddyBlock = `
|
|
643
|
+
caddy:
|
|
644
|
+
image: caddy:2-alpine
|
|
645
|
+
container_name: openfactu-caddy
|
|
646
|
+
ports:
|
|
647
|
+
- "0.0.0.0:${httpPort}:80"
|
|
648
|
+
- "0.0.0.0:${httpsPort}:443"
|
|
649
|
+
volumes:
|
|
650
|
+
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
651
|
+
- caddy_data:/data
|
|
652
|
+
- caddy_config:/config
|
|
653
|
+
depends_on:
|
|
654
|
+
- web
|
|
655
|
+
- server
|
|
656
|
+
restart: unless-stopped
|
|
657
|
+
networks:
|
|
658
|
+
- openfactu_net
|
|
659
|
+
`;
|
|
660
|
+
const volumesBlock = `
|
|
661
|
+
volumes:
|
|
662
|
+
caddy_data:
|
|
663
|
+
caddy_config:
|
|
664
|
+
`;
|
|
665
|
+
// Insertar caddy DENTRO de services (antes del bloque top-level
|
|
666
|
+
// "networks:"), no al final, para que no quede anidado bajo networks:.
|
|
667
|
+
if (/\nnetworks:/.test(composeContent)) {
|
|
668
|
+
composeContent = composeContent.replace(/\nnetworks:/, () => `${caddyBlock}${volumesBlock}\nnetworks:`);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
composeContent += caddyBlock + volumesBlock;
|
|
672
|
+
}
|
|
673
|
+
fs_1.default.writeFileSync(prodComposePath, composeContent);
|
|
674
|
+
}
|
|
675
|
+
// Actualizar .env
|
|
676
|
+
const protocol = 'https';
|
|
677
|
+
const webUrl = httpsPort === '443'
|
|
678
|
+
? `${protocol}://${finalHost}`
|
|
679
|
+
: `${protocol}://${finalHost}:${httpsPort}`;
|
|
680
|
+
env.VITE_API_URL = webUrl;
|
|
681
|
+
env.CORS_ORIGIN = webUrl;
|
|
682
|
+
env.HOST = finalHost;
|
|
683
|
+
writeEnv(envPath, env);
|
|
684
|
+
logger_1.log.blank();
|
|
685
|
+
logger_1.log.success('HTTPS configurado');
|
|
686
|
+
logger_1.log.blank();
|
|
687
|
+
logger_1.log.info(`URL: ${chalk_1.default.cyan(webUrl)}`);
|
|
688
|
+
if (httpPort !== '80' || httpsPort !== '443') {
|
|
689
|
+
logger_1.log.info(`Puertos: HTTP=${httpPort}, HTTPS=${httpsPort}`);
|
|
690
|
+
}
|
|
691
|
+
if (isLAN) {
|
|
692
|
+
logger_1.log.dim(' Certificado auto-firmado (los navegadores mostrarán advertencia)');
|
|
693
|
+
}
|
|
694
|
+
else {
|
|
695
|
+
logger_1.log.dim(' Caddy obtendrá certificado Let\'s Encrypt en el primer request');
|
|
696
|
+
logger_1.log.dim(' El DNS debe apuntar a este servidor');
|
|
697
|
+
}
|
|
698
|
+
logger_1.log.blank();
|
|
699
|
+
const { restart } = await inquirer_1.default.prompt([
|
|
700
|
+
{
|
|
701
|
+
type: 'confirm',
|
|
702
|
+
name: 'restart',
|
|
703
|
+
message: '¿Reiniciar servicios para aplicar HTTPS?',
|
|
704
|
+
default: true,
|
|
705
|
+
},
|
|
706
|
+
]);
|
|
707
|
+
if (restart) {
|
|
708
|
+
const spinner = (0, ora_1.default)('Reiniciando servicios...').start();
|
|
709
|
+
try {
|
|
710
|
+
(0, child_process_1.execSync)('docker compose -f docker-compose.prod.yml up -d', {
|
|
711
|
+
cwd: root,
|
|
712
|
+
stdio: 'pipe',
|
|
713
|
+
timeout: 120000,
|
|
714
|
+
});
|
|
715
|
+
spinner.succeed('Servicios reiniciados');
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
spinner.fail('Error: ' + err.message);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch (err) {
|
|
723
|
+
logger_1.log.error(err.message);
|
|
724
|
+
process.exitCode = 1;
|
|
725
|
+
}
|
|
726
|
+
});
|
|
337
727
|
// ── openfactu deploy:status ──
|
|
338
728
|
program
|
|
339
729
|
.command('deploy:status')
|
|
340
730
|
.description('Muestra el estado de los servicios Docker')
|
|
341
|
-
.
|
|
731
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
732
|
+
.action(async (opts) => {
|
|
342
733
|
try {
|
|
343
734
|
const root = (0, paths_1.getProjectRoot)();
|
|
344
735
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
345
736
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
737
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
738
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
346
739
|
logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
|
|
740
|
+
if (useMonitoring)
|
|
741
|
+
logger_1.log.info(`Monitoreo: ${chalk_1.default.dim('docker-compose.prod.monitoring.yml')}`);
|
|
347
742
|
logger_1.log.blank();
|
|
348
|
-
const
|
|
743
|
+
const files = useMonitoring
|
|
744
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
745
|
+
: `-f ${composeFile}`;
|
|
746
|
+
const output = (0, child_process_1.execSync)(`docker compose ${files} ps`, {
|
|
349
747
|
cwd: root,
|
|
350
748
|
}).toString();
|
|
351
749
|
console.log(output);
|
|
352
|
-
// Mostrar URLs
|
|
750
|
+
// Mostrar URLs realmente configuradas en .env (correctas con o sin SSL/Caddy)
|
|
353
751
|
const envPath = path_1.default.join(root, '.env');
|
|
354
752
|
const env = readEnv(envPath);
|
|
355
753
|
const host = env.HOST || 'localhost';
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
const protocol = env.VITE_API_URL?.startsWith('https') ? 'https' : 'http';
|
|
754
|
+
const webUrl = env.CORS_ORIGIN || `http://${host}:${env.WEB_PORT || '8080'}`;
|
|
755
|
+
const apiUrl = env.VITE_API_URL || `http://${host}:${env.SERVER_PORT || '3000'}`;
|
|
359
756
|
logger_1.log.blank();
|
|
360
|
-
logger_1.log.info(`Web: ${chalk_1.default.cyan(
|
|
361
|
-
logger_1.log.info(`API: ${chalk_1.default.cyan(
|
|
757
|
+
logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
|
|
758
|
+
logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
|
|
362
759
|
}
|
|
363
760
|
catch (err) {
|
|
364
761
|
logger_1.log.error('Docker no disponible o servicios no levantados');
|
|
@@ -371,18 +768,26 @@ networks:
|
|
|
371
768
|
.description('Reconstruye y reinicia los contenedores Docker')
|
|
372
769
|
.option('--service <name>', 'Reconstruir solo un servicio (web, server, db)')
|
|
373
770
|
.option('--no-cache', 'Construir sin cache de Docker')
|
|
771
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
374
772
|
.action(async (opts) => {
|
|
375
773
|
try {
|
|
376
774
|
const root = (0, paths_1.getProjectRoot)();
|
|
377
775
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
378
776
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
777
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
778
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
379
779
|
const service = opts.service || '';
|
|
380
780
|
const noCache = opts.cache === false ? ' --no-cache' : '';
|
|
381
781
|
logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
|
|
782
|
+
if (useMonitoring)
|
|
783
|
+
logger_1.log.info(`Monitoreo: ${chalk_1.default.dim('docker-compose.prod.monitoring.yml')}`);
|
|
382
784
|
logger_1.log.blank();
|
|
785
|
+
const files = useMonitoring
|
|
786
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
787
|
+
: `-f ${composeFile}`;
|
|
383
788
|
const buildSpinner = (0, ora_1.default)(`Construyendo${service ? ' ' + service : ' todos los servicios'}...`).start();
|
|
384
789
|
try {
|
|
385
|
-
(0, child_process_1.execSync)(`docker compose
|
|
790
|
+
(0, child_process_1.execSync)(`docker compose ${files} build${noCache} ${service}`, {
|
|
386
791
|
cwd: root,
|
|
387
792
|
stdio: 'pipe',
|
|
388
793
|
timeout: 600000,
|
|
@@ -404,7 +809,7 @@ networks:
|
|
|
404
809
|
}
|
|
405
810
|
const upSpinner = (0, ora_1.default)('Reiniciando servicios...').start();
|
|
406
811
|
try {
|
|
407
|
-
(0, child_process_1.execSync)(`docker compose
|
|
812
|
+
(0, child_process_1.execSync)(`docker compose ${files} up -d ${service}`, {
|
|
408
813
|
cwd: root,
|
|
409
814
|
stdio: 'pipe',
|
|
410
815
|
timeout: 60000,
|
|
@@ -437,14 +842,20 @@ networks:
|
|
|
437
842
|
.description('Muestra los logs de los servicios Docker')
|
|
438
843
|
.option('--service <name>', 'Logs de un servicio especifico (web, server, db)')
|
|
439
844
|
.option('-n, --lines <number>', 'Numero de lineas', '50')
|
|
845
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
440
846
|
.action(async (opts) => {
|
|
441
847
|
try {
|
|
442
848
|
const root = (0, paths_1.getProjectRoot)();
|
|
443
849
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
444
850
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
851
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
852
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
445
853
|
const service = opts.service || '';
|
|
446
854
|
const lines = opts.lines || '50';
|
|
447
|
-
|
|
855
|
+
const files = useMonitoring
|
|
856
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
857
|
+
: `-f ${composeFile}`;
|
|
858
|
+
(0, child_process_1.execSync)(`docker compose ${files} logs --tail ${lines} ${service}`, {
|
|
448
859
|
cwd: root,
|
|
449
860
|
stdio: 'inherit',
|
|
450
861
|
});
|
|
@@ -457,13 +868,19 @@ networks:
|
|
|
457
868
|
program
|
|
458
869
|
.command('stop')
|
|
459
870
|
.description('Para todos los servicios Docker')
|
|
460
|
-
.
|
|
871
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
872
|
+
.action(async (opts) => {
|
|
461
873
|
try {
|
|
462
874
|
const root = (0, paths_1.getProjectRoot)();
|
|
463
875
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
464
876
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
877
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
878
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
879
|
+
const files = useMonitoring
|
|
880
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
881
|
+
: `-f ${composeFile}`;
|
|
465
882
|
const spinner = (0, ora_1.default)('Parando servicios...').start();
|
|
466
|
-
(0, child_process_1.execSync)(`docker compose
|
|
883
|
+
(0, child_process_1.execSync)(`docker compose ${files} down`, { cwd: root, stdio: 'pipe' });
|
|
467
884
|
spinner.succeed('Servicios parados');
|
|
468
885
|
}
|
|
469
886
|
catch (err) {
|
|
@@ -475,14 +892,20 @@ networks:
|
|
|
475
892
|
.command('restart')
|
|
476
893
|
.description('Reinicia los servicios Docker (sin rebuild)')
|
|
477
894
|
.option('--service <name>', 'Reiniciar solo un servicio')
|
|
895
|
+
.option('--with-monitoring', 'Incluir servicios de monitoreo')
|
|
478
896
|
.action(async (opts) => {
|
|
479
897
|
try {
|
|
480
898
|
const root = (0, paths_1.getProjectRoot)();
|
|
481
899
|
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
482
900
|
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
901
|
+
const monitoringCompose = path_1.default.join(root, 'docker-compose.prod.monitoring.yml');
|
|
902
|
+
const useMonitoring = opts.withMonitoring && fs_1.default.existsSync(monitoringCompose);
|
|
903
|
+
const files = useMonitoring
|
|
904
|
+
? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
|
|
905
|
+
: `-f ${composeFile}`;
|
|
483
906
|
const service = opts.service || '';
|
|
484
907
|
const spinner = (0, ora_1.default)('Reiniciando...').start();
|
|
485
|
-
(0, child_process_1.execSync)(`docker compose
|
|
908
|
+
(0, child_process_1.execSync)(`docker compose ${files} restart ${service}`, { cwd: root, stdio: 'pipe' });
|
|
486
909
|
spinner.succeed('Servicios reiniciados');
|
|
487
910
|
}
|
|
488
911
|
catch (err) {
|