@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 +161 -5
- package/dist/src/commands/backup.d.ts +2 -0
- package/dist/src/commands/backup.js +424 -0
- package/dist/src/commands/deploy.js +486 -67
- 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 +966 -72
- 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
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
|
|
15
|
-
openfactu
|
|
16
|
-
openfactu
|
|
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
|
|
23
|
+
### Instalacion
|
|
22
24
|
|
|
23
25
|
| Comando | Descripcion |
|
|
24
26
|
|---------|-------------|
|
|
25
|
-
| `openfactu install [dir]` |
|
|
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,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
|
+
}
|