@openfactu/cli 0.0.7 → 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.
@@ -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
- .action(async () => {
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(' ────────────────────────────────────'));
@@ -104,6 +126,15 @@ function registerDeployCommand(program) {
104
126
  else {
105
127
  host = selectedIP;
106
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;
107
138
  }
108
139
  else if (mode === 'public') {
109
140
  const { domain } = await inquirer_1.default.prompt([
@@ -124,6 +155,50 @@ function registerDeployCommand(program) {
124
155
  ]);
125
156
  useSSL = ssl;
126
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
+ }
127
202
  // Puertos
128
203
  const { ports } = await inquirer_1.default.prompt([
129
204
  {
@@ -143,29 +218,36 @@ function registerDeployCommand(program) {
143
218
  serverPort = answers.serverPort;
144
219
  dbPort = answers.dbPort;
145
220
  }
146
- // Password de BD
147
- const { dbPassword } = await inquirer_1.default.prompt([
148
- {
149
- type: 'input',
150
- name: 'dbPassword',
151
- message: 'Password de PostgreSQL:',
152
- default: 'openfactu_pass',
153
- },
154
- ]);
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);
155
225
  // 3. Construir configuración
156
- const protocol = useSSL ? 'https' : 'http';
157
- const webUrl = webPort === '80' || webPort === '443'
158
- ? `${protocol}://${host}`
159
- : `${protocol}://${host}:${webPort}`;
160
- const apiUrl = serverPort === '80' || serverPort === '443'
161
- ? `${protocol}://${host}`
162
- : `${protocol}://${host}:${serverPort}`;
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
+ }
163
245
  logger_1.log.blank();
164
246
  logger_1.log.title(' Resumen de configuración');
165
247
  logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
166
248
  logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
167
249
  logger_1.log.info(`BD Puerto: ${chalk_1.default.cyan(dbPort)} ${chalk_1.default.dim('(host)')}`);
168
- logger_1.log.info(`BD Password: ${chalk_1.default.dim(dbPassword === 'openfactu_pass' ? '(default)' : '****')}`);
250
+ logger_1.log.info(`BD Password: ${chalk_1.default.dim('(reutilizada de install)')}`);
169
251
  logger_1.log.info(`SSL: ${useSSL ? chalk_1.default.green('Si') : chalk_1.default.dim('No')}`);
170
252
  logger_1.log.blank();
171
253
  const { confirm } = await inquirer_1.default.prompt([
@@ -184,7 +266,7 @@ function registerDeployCommand(program) {
184
266
  env.POSTGRES_USER = env.POSTGRES_USER || 'openfactu';
185
267
  env.POSTGRES_PASSWORD = dbPassword;
186
268
  env.POSTGRES_DB = env.POSTGRES_DB || 'openfactudb';
187
- 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}`;
188
270
  env.VITE_API_URL = apiUrl;
189
271
  env.HOST = host;
190
272
  env.CORS_ORIGIN = webUrl;
@@ -244,47 +326,164 @@ function registerDeployCommand(program) {
244
326
  restart: unless-stopped
245
327
  networks:
246
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
247
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 += `
248
383
  networks:
249
384
  openfactu_net:
250
385
  driver: bridge
251
386
  `;
252
- // Si SSL, añadir nginx reverse proxy
253
- if (useSSL) {
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 {
254
406
  composeContent += `
255
- # Para SSL, configura un reverse proxy (nginx, traefik, caddy) delante.
256
- # Ejemplo con Caddy (descomentar):
257
- #
258
- # caddy:
259
- # image: caddy:2-alpine
260
- # ports:
261
- # - "0.0.0.0:80:80"
262
- # - "0.0.0.0:443:443"
263
- # volumes:
264
- # - ./Caddyfile:/etc/caddy/Caddyfile
265
- # - caddy_data:/data
266
- # depends_on:
267
- # - web
268
- # - server
269
- # networks:
270
- # - openfactu_net
271
- #
272
- # volumes:
273
- # caddy_data:
274
- #
275
- # Caddyfile:
276
- # ${host} {
277
- # handle /api/* {
278
- # reverse_proxy server:3000
279
- # }
280
- # handle {
281
- # reverse_proxy web:80
282
- # }
283
- # }
407
+ networks:
408
+ openfactu_net:
409
+ driver: bridge
284
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');
285
486
  }
286
- fs_1.default.writeFileSync(prodComposePath, composeContent);
287
- prodSpinner.succeed('docker-compose.prod.yml generado');
288
487
  // 6. Preguntar si levantar
289
488
  logger_1.log.blank();
290
489
  const { start } = await inquirer_1.default.prompt([
@@ -323,7 +522,9 @@ networks:
323
522
  else if (mode === 'public') {
324
523
  logger_1.log.info('Asegúrate de que los puertos estén abiertos en el firewall');
325
524
  if (useSSL) {
326
- logger_1.log.info('Configura el reverse proxy (Caddy/Nginx) para SSL');
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)');
327
528
  }
328
529
  }
329
530
  logger_1.log.blank();
@@ -338,31 +539,223 @@ networks:
338
539
  process.exitCode = 1;
339
540
  }
340
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
+ });
341
727
  // ── openfactu deploy:status ──
342
728
  program
343
729
  .command('deploy:status')
344
730
  .description('Muestra el estado de los servicios Docker')
345
- .action(async () => {
731
+ .option('--with-monitoring', 'Incluir servicios de monitoreo')
732
+ .action(async (opts) => {
346
733
  try {
347
734
  const root = (0, paths_1.getProjectRoot)();
348
735
  const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
349
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);
350
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')}`);
351
742
  logger_1.log.blank();
352
- const output = (0, child_process_1.execSync)(`docker compose -f ${composeFile} ps`, {
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`, {
353
747
  cwd: root,
354
748
  }).toString();
355
749
  console.log(output);
356
- // Mostrar URLs
750
+ // Mostrar URLs realmente configuradas en .env (correctas con o sin SSL/Caddy)
357
751
  const envPath = path_1.default.join(root, '.env');
358
752
  const env = readEnv(envPath);
359
753
  const host = env.HOST || 'localhost';
360
- const webPort = env.WEB_PORT || '8080';
361
- const serverPort = env.SERVER_PORT || '3000';
362
- 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'}`;
363
756
  logger_1.log.blank();
364
- logger_1.log.info(`Web: ${chalk_1.default.cyan(`${protocol}://${host}:${webPort}`)}`);
365
- logger_1.log.info(`API: ${chalk_1.default.cyan(`${protocol}://${host}:${serverPort}`)}`);
757
+ logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
758
+ logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
366
759
  }
367
760
  catch (err) {
368
761
  logger_1.log.error('Docker no disponible o servicios no levantados');
@@ -375,18 +768,26 @@ networks:
375
768
  .description('Reconstruye y reinicia los contenedores Docker')
376
769
  .option('--service <name>', 'Reconstruir solo un servicio (web, server, db)')
377
770
  .option('--no-cache', 'Construir sin cache de Docker')
771
+ .option('--with-monitoring', 'Incluir servicios de monitoreo')
378
772
  .action(async (opts) => {
379
773
  try {
380
774
  const root = (0, paths_1.getProjectRoot)();
381
775
  const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
382
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);
383
779
  const service = opts.service || '';
384
780
  const noCache = opts.cache === false ? ' --no-cache' : '';
385
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')}`);
386
784
  logger_1.log.blank();
785
+ const files = useMonitoring
786
+ ? `-f ${composeFile} -f docker-compose.prod.monitoring.yml`
787
+ : `-f ${composeFile}`;
387
788
  const buildSpinner = (0, ora_1.default)(`Construyendo${service ? ' ' + service : ' todos los servicios'}...`).start();
388
789
  try {
389
- (0, child_process_1.execSync)(`docker compose -f ${composeFile} build${noCache} ${service}`, {
790
+ (0, child_process_1.execSync)(`docker compose ${files} build${noCache} ${service}`, {
390
791
  cwd: root,
391
792
  stdio: 'pipe',
392
793
  timeout: 600000,
@@ -408,7 +809,7 @@ networks:
408
809
  }
409
810
  const upSpinner = (0, ora_1.default)('Reiniciando servicios...').start();
410
811
  try {
411
- (0, child_process_1.execSync)(`docker compose -f ${composeFile} up -d ${service}`, {
812
+ (0, child_process_1.execSync)(`docker compose ${files} up -d ${service}`, {
412
813
  cwd: root,
413
814
  stdio: 'pipe',
414
815
  timeout: 60000,
@@ -441,14 +842,20 @@ networks:
441
842
  .description('Muestra los logs de los servicios Docker')
442
843
  .option('--service <name>', 'Logs de un servicio especifico (web, server, db)')
443
844
  .option('-n, --lines <number>', 'Numero de lineas', '50')
845
+ .option('--with-monitoring', 'Incluir servicios de monitoreo')
444
846
  .action(async (opts) => {
445
847
  try {
446
848
  const root = (0, paths_1.getProjectRoot)();
447
849
  const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
448
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);
449
853
  const service = opts.service || '';
450
854
  const lines = opts.lines || '50';
451
- (0, child_process_1.execSync)(`docker compose -f ${composeFile} logs --tail ${lines} ${service}`, {
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}`, {
452
859
  cwd: root,
453
860
  stdio: 'inherit',
454
861
  });
@@ -461,13 +868,19 @@ networks:
461
868
  program
462
869
  .command('stop')
463
870
  .description('Para todos los servicios Docker')
464
- .action(async () => {
871
+ .option('--with-monitoring', 'Incluir servicios de monitoreo')
872
+ .action(async (opts) => {
465
873
  try {
466
874
  const root = (0, paths_1.getProjectRoot)();
467
875
  const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
468
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}`;
469
882
  const spinner = (0, ora_1.default)('Parando servicios...').start();
470
- (0, child_process_1.execSync)(`docker compose -f ${composeFile} down`, { cwd: root, stdio: 'pipe' });
883
+ (0, child_process_1.execSync)(`docker compose ${files} down`, { cwd: root, stdio: 'pipe' });
471
884
  spinner.succeed('Servicios parados');
472
885
  }
473
886
  catch (err) {
@@ -479,14 +892,20 @@ networks:
479
892
  .command('restart')
480
893
  .description('Reinicia los servicios Docker (sin rebuild)')
481
894
  .option('--service <name>', 'Reiniciar solo un servicio')
895
+ .option('--with-monitoring', 'Incluir servicios de monitoreo')
482
896
  .action(async (opts) => {
483
897
  try {
484
898
  const root = (0, paths_1.getProjectRoot)();
485
899
  const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
486
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}`;
487
906
  const service = opts.service || '';
488
907
  const spinner = (0, ora_1.default)('Reiniciando...').start();
489
- (0, child_process_1.execSync)(`docker compose -f ${composeFile} restart ${service}`, { cwd: root, stdio: 'pipe' });
908
+ (0, child_process_1.execSync)(`docker compose ${files} restart ${service}`, { cwd: root, stdio: 'pipe' });
490
909
  spinner.succeed('Servicios reiniciados');
491
910
  }
492
911
  catch (err) {