@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.
package/README.md CHANGED
@@ -11,20 +11,71 @@ npm i -g @openfactu/cli
11
11
  ## Inicio rapido
12
12
 
13
13
  ```bash
14
- openfactu install # Descarga e instala OpenFactu
15
- openfactu deploy # Configura acceso externo
16
- openfactu setup # Configuracion inicial de BD
14
+ openfactu install # Wizard interactivo completo
15
+ openfactu install:quick # Instalacion rapida (non-interactive)
16
+ openfactu deploy # Configura acceso externo
17
+ openfactu setup # Configuracion inicial de BD
18
+ openfactu doctor # Diagnostico del entorno
17
19
  ```
18
20
 
19
21
  ## Comandos
20
22
 
21
- ### Instalacion y actualizacion
23
+ ### Instalacion
22
24
 
23
25
  | Comando | Descripcion |
24
26
  |---------|-------------|
25
- | `openfactu install [dir]` | Descarga desde releases de GitHub con Docker |
27
+ | `openfactu install [dir]` | Wizard completo: version, modo, Docker, contraseña BD, monitoreo (selección de servicios), servicio |
28
+ | `openfactu install:quick` | Instalacion rapida non-interactive |
29
+ | `openfactu install:script` | Generar script shell standalone para instalar sin CLI |
26
30
  | `openfactu update` | Actualiza sin perder datos |
27
31
  | `openfactu update:check` | Comprueba si hay versiones nuevas |
32
+ | `openfactu uninstall` | Desinstalacion limpia con backup opcional |
33
+
34
+ #### Opciones de `install`
35
+
36
+ ```bash
37
+ openfactu install --tag v1.0.0 # Version especifica
38
+ openfactu install --branch develop # Desde branch
39
+ openfactu install --mode full # Modo: full, docker, minimal, download
40
+ openfactu install --generate-env # No preguntar: genera credenciales aleatorias
41
+ openfactu install --monitoring # Monitoreo: set basico (pgAdmin, Grafana, Prometheus, Portainer)
42
+ openfactu install --with-analytics # Monitoreo: set completo (+ Loki, Promtail, cAdvisor, Node Exporter)
43
+ openfactu install --service # Instalar como servicio systemd
44
+ openfactu install -y # Non-interactive, acepta defaults
45
+ openfactu install --no-preflight # Saltar chequeos previos
46
+ ```
47
+
48
+ > **Credenciales:** en modo interactivo, `install` pide la **contraseña de PostgreSQL**
49
+ > (pulsa Enter para generar una segura) y escribe un `.env` completo **antes** del primer
50
+ > arranque, de modo que Postgres se inicializa con ella. `deploy` reutiliza esa contraseña;
51
+ > cambiarla luego no afecta a un volumen ya creado.
52
+ >
53
+ > **Monitoreo:** si activas monitoreo en modo interactivo, eliges con un **checkbox** qué
54
+ > servicios instalar (pgAdmin, Grafana, Prometheus, Loki, Promtail, cAdvisor, Node Exporter,
55
+ > Portainer, Alertmanager). Con `--monitoring`/`--with-analytics` se usan sets predefinidos.
56
+
57
+ #### Modos de instalacion
58
+
59
+ - **full** — Build + up + setup de BD + health checks
60
+ - **docker** — Build + up (sin setup de BD)
61
+ - **minimal** — Solo up, sin build
62
+ - **download** — Solo descarga el codigo
63
+
64
+ ### Servicio systemd
65
+
66
+ | Comando | Descripcion |
67
+ |---------|-------------|
68
+ | `openfactu service install` | Instalar como servicio systemd con auto-start |
69
+ | `openfactu service start` | Iniciar servicio |
70
+ | `openfactu service stop` | Detener servicio |
71
+ | `openfactu service restart` | Reiniciar servicio |
72
+ | `openfactu service status` | Ver estado |
73
+ | `openfactu service logs` | Ver logs del servicio |
74
+ | `openfactu service uninstall` | Remover servicio |
75
+
76
+ ```bash
77
+ openfactu service install --restart always --healthcheck --with-monitoring
78
+ ```
28
79
 
29
80
  ### Despliegue
30
81
 
@@ -37,6 +88,61 @@ openfactu setup # Configuracion inicial de BD
37
88
  | `openfactu stop` | Para todos los servicios |
38
89
  | `openfactu restart` | Reinicia sin rebuild |
39
90
 
91
+ ### Monitoreo y Analitica
92
+
93
+ | Comando | Descripcion |
94
+ |---------|-------------|
95
+ | `openfactu monitoring` | Wizard para configurar stack de monitoreo |
96
+ | `openfactu monitoring:generate` | Generar compose sin interaccion |
97
+ | `openfactu monitoring:up` | Levantar servicios de monitoreo |
98
+ | `openfactu monitoring:down` | Parar servicios de monitoreo |
99
+ | `openfactu monitoring:status` | Estado de los servicios |
100
+ | `openfactu monitoring:config` | Cambiar configuracion |
101
+
102
+ #### Servicios disponibles
103
+
104
+ - **pgAdmin** — Gestion de base de datos (puerto 5050)
105
+ - **Grafana** — Dashboards y visualizacion (puerto 3001)
106
+ - **Prometheus** — Metricas y alertas (puerto 9090)
107
+ - **Loki** — Agregacion de logs (puerto 3100)
108
+ - **Promtail** — Envio de logs a Loki
109
+ - **cAdvisor** — Metricas de contenedores Docker (puerto 8081)
110
+ - **Node Exporter** — Metricas del host (puerto 9100)
111
+ - **Portainer** — Gestion de Docker (puerto 9000)
112
+ - **Alertmanager** — Gestion de alertas (puerto 9093)
113
+
114
+ ```bash
115
+ openfactu monitoring --with-analytics # Stack completo de analitica
116
+ openfactu monitoring:generate --analytics # Generar compose con todo
117
+ ```
118
+
119
+ ### Backup y Restore
120
+
121
+ | Comando | Descripcion |
122
+ |---------|-------------|
123
+ | `openfactu backup create` | Crear backup completo |
124
+ | `openfactu backup list` | Listar backups disponibles |
125
+ | `openfactu backup restore` | Restaurar desde backup |
126
+ | `openfactu backup delete` | Eliminar backup |
127
+
128
+ ```bash
129
+ openfactu backup create --name produccion-2024
130
+ openfactu backup create --db-only # Solo base de datos
131
+ openfactu backup restore --name produccion-2024
132
+ openfactu uninstall --keep-backup # Backup antes de desinstalar
133
+ ```
134
+
135
+ ### Diagnostico
136
+
137
+ | Comando | Descripcion |
138
+ |---------|-------------|
139
+ | `openfactu doctor` | Diagnostico completo del entorno |
140
+
141
+ ```bash
142
+ openfactu doctor # Verificacion visual
143
+ openfactu doctor --json # Salida en JSON para scripts
144
+ ```
145
+
40
146
  ### Base de datos
41
147
 
42
148
  | Comando | Descripcion |
@@ -74,6 +180,56 @@ openfactu setup # Configuracion inicial de BD
74
180
  |---------|-------------|
75
181
  | `openfactu version` | Versiones del sistema |
76
182
 
183
+ ## Flujos de trabajo
184
+
185
+ ### Instalacion para produccion
186
+
187
+ ```bash
188
+ # 1. Instalacion completa con todo
189
+ openfactu install --mode full --monitoring --with-analytics --service --generate-env
190
+
191
+ # 2. Verificar que todo esta bien
192
+ openfactu doctor
193
+
194
+ # 3. Configurar acceso externo
195
+ openfactu deploy
196
+
197
+ # 4. Configurar base de datos
198
+ openfactu setup
199
+ ```
200
+
201
+ ### Instalacion rapida para desarrollo
202
+
203
+ ```bash
204
+ openfactu install:quick --generate-env
205
+ ```
206
+
207
+ ### Instalar en servidor remoto sin CLI
208
+
209
+ ```bash
210
+ # Generar script en tu maquina
211
+ openfactu install:script --output deploy.sh --monitoring --service
212
+
213
+ # Subir y ejecutar en el servidor
214
+ scp deploy.sh usuario@servidor:~/
215
+ ssh usuario@servidor "chmod +x ~/deploy.sh && ~/deploy.sh"
216
+ ```
217
+
218
+ ### Backup antes de actualizar
219
+
220
+ ```bash
221
+ openfactu backup create --name pre-update
222
+ openfactu update
223
+ # Si algo sale mal:
224
+ openfactu backup restore --name pre-update
225
+ ```
226
+
227
+ ### Instalar como servicio con auto-start
228
+
229
+ ```bash
230
+ openfactu service install --restart always --healthcheck --healthcheck-interval 5min
231
+ ```
232
+
77
233
  ## Desarrollo remoto de plugins
78
234
 
79
235
  ```bash
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerBackupCommand(program: Command): void;
@@ -0,0 +1,424 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerBackupCommand = registerBackupCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const child_process_1 = require("child_process");
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const path_1 = __importDefault(require("path"));
13
+ const os_1 = __importDefault(require("os"));
14
+ const logger_1 = require("../utils/logger");
15
+ const paths_1 = require("../utils/paths");
16
+ const helpers_1 = require("../utils/helpers");
17
+ function getBackupDir() {
18
+ return path_1.default.join(os_1.default.homedir(), 'openfactu-backups');
19
+ }
20
+ function listBackups() {
21
+ const backupDir = getBackupDir();
22
+ if (!fs_1.default.existsSync(backupDir))
23
+ return [];
24
+ const entries = fs_1.default.readdirSync(backupDir, { withFileTypes: true });
25
+ const backups = [];
26
+ for (const entry of entries) {
27
+ if (entry.isDirectory()) {
28
+ const fullPath = path_1.default.join(backupDir, entry.name);
29
+ const stat = fs_1.default.statSync(fullPath);
30
+ const size = (0, child_process_1.execSync)(`du -sh "${fullPath}" 2>/dev/null | cut -f1`, { stdio: 'pipe' }).toString().trim();
31
+ backups.push({
32
+ name: entry.name,
33
+ path: fullPath,
34
+ date: stat.mtime.toLocaleString('es-ES'),
35
+ size,
36
+ });
37
+ }
38
+ }
39
+ return backups.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
40
+ }
41
+ function registerBackupCommand(program) {
42
+ const backupCmd = program
43
+ .command('backup')
44
+ .description('Backup y restore de OpenFactu');
45
+ backupCmd
46
+ .command('create')
47
+ .description('Crear backup completo')
48
+ .option('--name <name>', 'Nombre del backup')
49
+ .option('--db-only', 'Solo backup de base de datos')
50
+ .option('--config-only', 'Solo backup de configuracion')
51
+ .option('--path <path>', 'Ruta del proyecto')
52
+ .option('--output <dir>', 'Directorio de salida')
53
+ .action(async (opts) => {
54
+ console.log();
55
+ console.log(chalk_1.default.bold.white(' OpenFactu — Crear Backup'));
56
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
57
+ console.log();
58
+ try {
59
+ const root = opts.path || (0, paths_1.getProjectRoot)();
60
+ const dockerCmd = (0, helpers_1.getDockerComposeCommand)();
61
+ const backupName = opts.name || `backup-${(0, helpers_1.timestamp)()}`;
62
+ const outputDir = opts.output || getBackupDir();
63
+ const backupPath = path_1.default.join(outputDir, backupName);
64
+ (0, helpers_1.ensureDir)(outputDir);
65
+ (0, helpers_1.ensureDir)(backupPath);
66
+ const dbOnly = opts.dbOnly || false;
67
+ const configOnly = opts.configOnly || false;
68
+ // Database backup
69
+ if (!configOnly) {
70
+ const dbSpinner = (0, ora_1.default)('Backup de base de datos...').start();
71
+ try {
72
+ const dbBackupPath = path_1.default.join(backupPath, 'database.sql');
73
+ const dbContainer = (0, child_process_1.execSync)(`${dockerCmd} ps --format '{{.Names}}' | grep db || true`, {
74
+ cwd: root,
75
+ stdio: 'pipe',
76
+ }).toString().trim();
77
+ if (dbContainer) {
78
+ (0, child_process_1.execSync)(`${dockerCmd} exec -T ${dbContainer} pg_dump -U openfactu openfactudb > "${dbBackupPath}"`, { cwd: root, stdio: 'pipe', timeout: 300000 });
79
+ const dbSize = fs_1.default.statSync(dbBackupPath).size;
80
+ dbSpinner.succeed(`BD backup: ${(dbSize / 1024 / 1024).toFixed(2)}MB`);
81
+ }
82
+ else {
83
+ dbSpinner.warn('Contenedor de BD no encontrado');
84
+ }
85
+ }
86
+ catch (err) {
87
+ dbSpinner.fail('Error en backup de BD: ' + err.message);
88
+ }
89
+ }
90
+ // Config backup
91
+ if (!dbOnly) {
92
+ const configSpinner = (0, ora_1.default)('Backup de configuracion...').start();
93
+ try {
94
+ const configFiles = [
95
+ '.env',
96
+ 'docker-compose.yml',
97
+ 'docker-compose.prod.yml',
98
+ 'docker-compose.prod.monitoring.yml',
99
+ 'docker-compose.monitoring.yml',
100
+ ];
101
+ for (const file of configFiles) {
102
+ const src = path_1.default.join(root, file);
103
+ if (fs_1.default.existsSync(src)) {
104
+ fs_1.default.copyFileSync(src, path_1.default.join(backupPath, file));
105
+ }
106
+ }
107
+ // Monitoring configs
108
+ const monitoringDir = path_1.default.join(root, 'monitoring');
109
+ if (fs_1.default.existsSync(monitoringDir)) {
110
+ const dest = path_1.default.join(backupPath, 'monitoring');
111
+ (0, child_process_1.execSync)(`cp -r "${monitoringDir}" "${dest}"`, { stdio: 'pipe' });
112
+ }
113
+ configSpinner.succeed('Configuracion respaldada');
114
+ }
115
+ catch (err) {
116
+ configSpinner.fail('Error en backup de config: ' + err.message);
117
+ }
118
+ }
119
+ // Storage backup (optional, can be large)
120
+ if (!dbOnly && !configOnly) {
121
+ const { backupStorage } = await inquirer_1.default.prompt([
122
+ {
123
+ type: 'confirm',
124
+ name: 'backupStorage',
125
+ message: 'Incluir storage (archivos, documentos)? Puede ser grande',
126
+ default: false,
127
+ },
128
+ ]);
129
+ if (backupStorage) {
130
+ const storageSpinner = (0, ora_1.default)('Backup de storage...').start();
131
+ try {
132
+ const storageSrc = path_1.default.join(root, 'storage');
133
+ if (fs_1.default.existsSync(storageSrc)) {
134
+ const storageDest = path_1.default.join(backupPath, 'storage');
135
+ (0, child_process_1.execSync)(`cp -r "${storageSrc}" "${storageDest}"`, { stdio: 'pipe', timeout: 300000 });
136
+ const size = (0, child_process_1.execSync)(`du -sh "${storageDest}" | cut -f1`, { stdio: 'pipe' }).toString().trim();
137
+ storageSpinner.succeed(`Storage backup: ${size}`);
138
+ }
139
+ else {
140
+ storageSpinner.warn('Directorio storage no encontrado');
141
+ }
142
+ }
143
+ catch (err) {
144
+ storageSpinner.fail('Error en backup de storage');
145
+ }
146
+ }
147
+ }
148
+ // Create manifest
149
+ const manifest = {
150
+ name: backupName,
151
+ date: new Date().toISOString(),
152
+ project: root,
153
+ dbOnly,
154
+ configOnly,
155
+ files: fs_1.default.readdirSync(backupPath),
156
+ };
157
+ fs_1.default.writeFileSync(path_1.default.join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2));
158
+ const totalSize = (0, child_process_1.execSync)(`du -sh "${backupPath}" | cut -f1`, { stdio: 'pipe' }).toString().trim();
159
+ logger_1.log.blank();
160
+ console.log(chalk_1.default.bold.green(' Backup completado'));
161
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
162
+ logger_1.log.info(`Nombre: ${chalk_1.default.cyan(backupName)}`);
163
+ logger_1.log.info(`Ubicacion: ${chalk_1.default.cyan(backupPath)}`);
164
+ logger_1.log.info(`Tamano: ${chalk_1.default.cyan(totalSize)}`);
165
+ logger_1.log.blank();
166
+ }
167
+ catch (err) {
168
+ logger_1.log.error(err.message);
169
+ process.exitCode = 1;
170
+ }
171
+ });
172
+ backupCmd
173
+ .command('list')
174
+ .description('Listar backups disponibles')
175
+ .action(async () => {
176
+ try {
177
+ const backups = listBackups();
178
+ if (backups.length === 0) {
179
+ logger_1.log.info('No hay backups disponibles');
180
+ return;
181
+ }
182
+ console.log();
183
+ console.log(chalk_1.default.bold.white(' OpenFactu — Backups'));
184
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
185
+ console.log();
186
+ for (const backup of backups) {
187
+ console.log(` ${chalk_1.default.cyan(backup.name)}`);
188
+ console.log(` ${chalk_1.default.dim('Fecha:')} ${backup.date}`);
189
+ console.log(` ${chalk_1.default.dim('Tamano:')} ${backup.size}`);
190
+ console.log(` ${chalk_1.default.dim('Ruta:')} ${chalk_1.default.dim(backup.path)}`);
191
+ console.log();
192
+ }
193
+ }
194
+ catch (err) {
195
+ logger_1.log.error(err.message);
196
+ }
197
+ });
198
+ backupCmd
199
+ .command('restore')
200
+ .description('Restaurar desde un backup')
201
+ .option('--name <name>', 'Nombre del backup a restaurar')
202
+ .option('--path <path>', 'Ruta del proyecto destino')
203
+ .option('--db-only', 'Solo restaurar base de datos')
204
+ .option('--config-only', 'Solo restaurar configuracion')
205
+ .option('--force', 'No preguntar confirmacion')
206
+ .action(async (opts) => {
207
+ console.log();
208
+ console.log(chalk_1.default.bold.white(' OpenFactu — Restaurar Backup'));
209
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
210
+ console.log();
211
+ try {
212
+ const backups = listBackups();
213
+ if (backups.length === 0) {
214
+ logger_1.log.error('No hay backups disponibles');
215
+ return;
216
+ }
217
+ let backupPath;
218
+ if (opts.name) {
219
+ const backup = backups.find(b => b.name === opts.name);
220
+ if (!backup) {
221
+ logger_1.log.error(`Backup no encontrado: ${opts.name}`);
222
+ return;
223
+ }
224
+ backupPath = backup.path;
225
+ }
226
+ else {
227
+ const choices = backups.map(b => ({
228
+ name: `${chalk_1.default.cyan(b.name)} ${chalk_1.default.dim(`(${b.date}, ${b.size})`)}`,
229
+ value: b.path,
230
+ }));
231
+ const { selected } = await inquirer_1.default.prompt([
232
+ {
233
+ type: 'list',
234
+ name: 'selected',
235
+ message: 'Selecciona el backup a restaurar:',
236
+ choices,
237
+ },
238
+ ]);
239
+ backupPath = selected;
240
+ }
241
+ const root = opts.path || (0, paths_1.getProjectRoot)();
242
+ const dockerCmd = (0, helpers_1.getDockerComposeCommand)();
243
+ const dbOnly = opts.dbOnly || false;
244
+ const configOnly = opts.configOnly || false;
245
+ if (!opts.force) {
246
+ logger_1.log.info(`Backup: ${chalk_1.default.cyan(backupPath)}`);
247
+ logger_1.log.info(`Destino: ${chalk_1.default.cyan(root)}`);
248
+ logger_1.log.blank();
249
+ const { confirm } = await inquirer_1.default.prompt([
250
+ {
251
+ type: 'confirm',
252
+ name: 'confirm',
253
+ message: 'Restaurar backup? Esto puede sobrescribir datos existentes',
254
+ default: false,
255
+ },
256
+ ]);
257
+ if (!confirm)
258
+ return;
259
+ }
260
+ // Restore database
261
+ if (!configOnly) {
262
+ const dbBackup = path_1.default.join(backupPath, 'database.sql');
263
+ if (fs_1.default.existsSync(dbBackup)) {
264
+ const dbSpinner = (0, ora_1.default)('Restaurando base de datos...').start();
265
+ try {
266
+ const dbContainer = (0, child_process_1.execSync)(`${dockerCmd} ps --format '{{.Names}}' | grep db || true`, {
267
+ cwd: root,
268
+ stdio: 'pipe',
269
+ }).toString().trim();
270
+ if (dbContainer) {
271
+ (0, child_process_1.execSync)(`${dockerCmd} exec -T ${dbContainer} psql -U openfactu openfactudb < "${dbBackup}"`, { cwd: root, stdio: 'pipe', timeout: 300000 });
272
+ dbSpinner.succeed('Base de datos restaurada');
273
+ }
274
+ else {
275
+ dbSpinner.warn('Contenedor de BD no encontrado');
276
+ }
277
+ }
278
+ catch (err) {
279
+ dbSpinner.fail('Error al restaurar BD');
280
+ }
281
+ }
282
+ }
283
+ // Restore config
284
+ if (!dbOnly) {
285
+ const configSpinner = (0, ora_1.default)('Restaurando configuracion...').start();
286
+ try {
287
+ const configFiles = [
288
+ '.env',
289
+ 'docker-compose.yml',
290
+ 'docker-compose.prod.yml',
291
+ 'docker-compose.prod.monitoring.yml',
292
+ 'docker-compose.monitoring.yml',
293
+ ];
294
+ for (const file of configFiles) {
295
+ const src = path_1.default.join(backupPath, file);
296
+ if (fs_1.default.existsSync(src)) {
297
+ fs_1.default.copyFileSync(src, path_1.default.join(root, file));
298
+ }
299
+ }
300
+ const monitoringSrc = path_1.default.join(backupPath, 'monitoring');
301
+ if (fs_1.default.existsSync(monitoringSrc)) {
302
+ const monitoringDest = path_1.default.join(root, 'monitoring');
303
+ (0, child_process_1.execSync)(`cp -r "${monitoringSrc}" "${monitoringDest}"`, { stdio: 'pipe' });
304
+ }
305
+ configSpinner.succeed('Configuracion restaurada');
306
+ }
307
+ catch (err) {
308
+ configSpinner.fail('Error al restaurar config');
309
+ }
310
+ }
311
+ // Restore storage
312
+ const storageSrc = path_1.default.join(backupPath, 'storage');
313
+ if (fs_1.default.existsSync(storageSrc)) {
314
+ const { restoreStorage } = await inquirer_1.default.prompt([
315
+ {
316
+ type: 'confirm',
317
+ name: 'restoreStorage',
318
+ message: 'Restaurar storage?',
319
+ default: false,
320
+ },
321
+ ]);
322
+ if (restoreStorage) {
323
+ const storageSpinner = (0, ora_1.default)('Restaurando storage...').start();
324
+ try {
325
+ const storageDest = path_1.default.join(root, 'storage');
326
+ (0, child_process_1.execSync)(`cp -r "${storageSrc}"/* "${storageDest}/" 2>/dev/null || cp -r "${storageSrc}" "${storageDest}"`, {
327
+ stdio: 'pipe',
328
+ timeout: 300000,
329
+ });
330
+ storageSpinner.succeed('Storage restaurado');
331
+ }
332
+ catch {
333
+ storageSpinner.fail('Error al restaurar storage');
334
+ }
335
+ }
336
+ }
337
+ logger_1.log.blank();
338
+ console.log(chalk_1.default.bold.green(' Restauracion completada'));
339
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
340
+ logger_1.log.dim(' Reinicia los servicios con: openfactu restart');
341
+ logger_1.log.blank();
342
+ }
343
+ catch (err) {
344
+ logger_1.log.error(err.message);
345
+ process.exitCode = 1;
346
+ }
347
+ });
348
+ backupCmd
349
+ .command('delete')
350
+ .description('Eliminar un backup')
351
+ .option('--name <name>', 'Nombre del backup')
352
+ .option('--all', 'Eliminar todos los backups')
353
+ .option('--force', 'No preguntar confirmacion')
354
+ .action(async (opts) => {
355
+ try {
356
+ const backups = listBackups();
357
+ if (backups.length === 0) {
358
+ logger_1.log.info('No hay backups para eliminar');
359
+ return;
360
+ }
361
+ if (opts.all) {
362
+ if (!opts.force) {
363
+ const { confirm } = await inquirer_1.default.prompt([
364
+ {
365
+ type: 'confirm',
366
+ name: 'confirm',
367
+ message: `Eliminar ${backups.length} backups?`,
368
+ default: false,
369
+ },
370
+ ]);
371
+ if (!confirm)
372
+ return;
373
+ }
374
+ const spinner = (0, ora_1.default)('Eliminando todos los backups...').start();
375
+ (0, child_process_1.execSync)(`rm -rf "${getBackupDir()}"`, { stdio: 'pipe' });
376
+ spinner.succeed('Todos los backups eliminados');
377
+ }
378
+ else {
379
+ let backupPath;
380
+ if (opts.name) {
381
+ const backup = backups.find(b => b.name === opts.name);
382
+ if (!backup) {
383
+ logger_1.log.error(`Backup no encontrado: ${opts.name}`);
384
+ return;
385
+ }
386
+ backupPath = backup.path;
387
+ }
388
+ else {
389
+ const choices = backups.map(b => ({
390
+ name: `${chalk_1.default.cyan(b.name)} ${chalk_1.default.dim(`(${b.date}, ${b.size})`)}`,
391
+ value: b.path,
392
+ }));
393
+ const { selected } = await inquirer_1.default.prompt([
394
+ {
395
+ type: 'list',
396
+ name: 'selected',
397
+ message: 'Selecciona el backup a eliminar:',
398
+ choices,
399
+ },
400
+ ]);
401
+ backupPath = selected;
402
+ }
403
+ if (!opts.force) {
404
+ const { confirm } = await inquirer_1.default.prompt([
405
+ {
406
+ type: 'confirm',
407
+ name: 'confirm',
408
+ message: `Eliminar backup ${path_1.default.basename(backupPath)}?`,
409
+ default: false,
410
+ },
411
+ ]);
412
+ if (!confirm)
413
+ return;
414
+ }
415
+ const spinner = (0, ora_1.default)('Eliminando backup...').start();
416
+ (0, child_process_1.execSync)(`rm -rf "${backupPath}"`, { stdio: 'pipe' });
417
+ spinner.succeed('Backup eliminado');
418
+ }
419
+ }
420
+ catch (err) {
421
+ logger_1.log.error(err.message);
422
+ }
423
+ });
424
+ }