@openfactu/cli 0.0.2 → 0.0.4

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 ADDED
@@ -0,0 +1,121 @@
1
+ # @openfactu/cli
2
+
3
+ CLI oficial para instalar, gestionar y desplegar [OpenFactu](https://github.com/AngelAcedo12/OpenFactu) -- ERP de facturación open source.
4
+
5
+ ## Instalacion
6
+
7
+ ```bash
8
+ npm i -g @openfactu/cli
9
+ ```
10
+
11
+ ## Inicio rapido
12
+
13
+ ```bash
14
+ # Descargar e instalar OpenFactu (te deja elegir version)
15
+ openfactu install
16
+
17
+ # Configurar base de datos y usuario admin
18
+ openfactu setup
19
+
20
+ # Aplicar migraciones
21
+ openfactu migrate
22
+
23
+ # Desplegar en red local o internet
24
+ openfactu deploy
25
+ ```
26
+
27
+ ## Comandos
28
+
29
+ ### Instalacion y actualizacion
30
+
31
+ | Comando | Descripcion |
32
+ |---------|-------------|
33
+ | `openfactu install [dir]` | Descarga e instala OpenFactu. Muestra las releases de GitHub para elegir version. Soporta Docker en Windows/Mac/Linux. |
34
+ | `openfactu update` | Actualiza a la ultima version desde GitHub sin perder datos (plugins, storage, .env se preservan). |
35
+ | `openfactu update:check` | Comprueba si hay versiones nuevas disponibles. |
36
+
37
+ ```bash
38
+ # Instalar una release especifica
39
+ openfactu install ./mi-erp --tag v1.2.0
40
+
41
+ # Instalar desde una branch
42
+ openfactu install ./mi-erp --branch develop
43
+
44
+ # Actualizar la instalacion actual
45
+ openfactu update
46
+ ```
47
+
48
+ ### Despliegue
49
+
50
+ | Comando | Descripcion |
51
+ |---------|-------------|
52
+ | `openfactu deploy` | Wizard para configurar acceso externo: red local, dominio publico o localhost. Genera `docker-compose.prod.yml`. |
53
+ | `openfactu deploy:status` | Muestra el estado de los contenedores Docker y las URLs de acceso. |
54
+
55
+ ```bash
56
+ # Configurar para que sea accesible en la red
57
+ openfactu deploy
58
+
59
+ # Ver estado de los servicios
60
+ openfactu deploy:status
61
+ ```
62
+
63
+ ### Base de datos
64
+
65
+ | Comando | Descripcion |
66
+ |---------|-------------|
67
+ | `openfactu setup` | Configuracion inicial: verifica BD, crea admin, primer tenant. |
68
+ | `openfactu migrate` | Ejecuta migraciones pendientes en todos los tenants. |
69
+ | `openfactu migrate:status` | Muestra tabla con estado de migraciones por tenant. |
70
+
71
+ ```bash
72
+ # Migrar solo un tenant especifico
73
+ openfactu migrate --tenant "Mi Empresa"
74
+
75
+ # Ver que migraciones faltan
76
+ openfactu migrate:status
77
+ ```
78
+
79
+ ### Tenants (empresas)
80
+
81
+ | Comando | Descripcion |
82
+ |---------|-------------|
83
+ | `openfactu tenant list` | Lista todas las empresas. |
84
+ | `openfactu tenant create [nombre]` | Crea una empresa nueva con schema y migraciones. |
85
+ | `openfactu tenant sync [nombre]` | Sincroniza migraciones de un tenant o todos. |
86
+
87
+ ### Plugins
88
+
89
+ | Comando | Descripcion |
90
+ |---------|-------------|
91
+ | `openfactu plugin list` | Lista plugins instalados con su estado por tenant. |
92
+
93
+ ### Otros
94
+
95
+ | Comando | Descripcion |
96
+ |---------|-------------|
97
+ | `openfactu version` | Muestra versiones del CLI, server, web y Node. |
98
+
99
+ ## Uso desde cualquier directorio
100
+
101
+ El CLI detecta automaticamente la instalacion de OpenFactu. Si no estas dentro del proyecto:
102
+
103
+ ```bash
104
+ # Opcion 1: flag --path
105
+ openfactu --path /ruta/a/openfactu migrate
106
+
107
+ # Opcion 2: variable de entorno
108
+ export OPENFACTU_HOME=/ruta/a/openfactu
109
+ openfactu migrate
110
+ ```
111
+
112
+ ## Requisitos
113
+
114
+ - Node.js >= 18
115
+ - Docker Desktop (para instalar y desplegar)
116
+ - Git (para descargar releases)
117
+
118
+ ## Links
119
+
120
+ - [GitHub](https://github.com/AngelAcedo12/OpenFactu)
121
+ - [Reportar un problema](https://github.com/AngelAcedo12/OpenFactu/issues)
File without changes
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerDeployCommand(program: Command): void;
@@ -0,0 +1,370 @@
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.registerDeployCommand = registerDeployCommand;
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
+ function getLocalIPs() {
17
+ const interfaces = os_1.default.networkInterfaces();
18
+ const ips = [];
19
+ for (const iface of Object.values(interfaces)) {
20
+ if (!iface)
21
+ continue;
22
+ for (const info of iface) {
23
+ if (info.family === 'IPv4' && !info.internal) {
24
+ ips.push(info.address);
25
+ }
26
+ }
27
+ }
28
+ return ips;
29
+ }
30
+ function readEnv(envPath) {
31
+ const env = {};
32
+ if (!fs_1.default.existsSync(envPath))
33
+ return env;
34
+ const lines = fs_1.default.readFileSync(envPath, 'utf-8').split('\n');
35
+ for (const line of lines) {
36
+ const trimmed = line.trim();
37
+ if (!trimmed || trimmed.startsWith('#'))
38
+ continue;
39
+ const eqIdx = trimmed.indexOf('=');
40
+ if (eqIdx > 0) {
41
+ env[trimmed.substring(0, eqIdx).trim()] = trimmed.substring(eqIdx + 1).trim();
42
+ }
43
+ }
44
+ return env;
45
+ }
46
+ function writeEnv(envPath, env) {
47
+ const lines = [];
48
+ for (const [key, value] of Object.entries(env)) {
49
+ lines.push(`${key}=${value}`);
50
+ }
51
+ fs_1.default.writeFileSync(envPath, lines.join('\n') + '\n');
52
+ }
53
+ function registerDeployCommand(program) {
54
+ program
55
+ .command('deploy')
56
+ .description('Configura OpenFactu para producción (acceso externo)')
57
+ .action(async () => {
58
+ console.log();
59
+ console.log(chalk_1.default.bold.white(' OpenFactu — Configurar Despliegue'));
60
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
61
+ console.log();
62
+ try {
63
+ const root = (0, paths_1.getProjectRoot)();
64
+ const envPath = path_1.default.join(root, '.env');
65
+ const composePath = path_1.default.join(root, 'docker-compose.yml');
66
+ // 1. Detectar IPs locales
67
+ const localIPs = getLocalIPs();
68
+ logger_1.log.info(`IPs detectadas: ${localIPs.join(', ') || 'ninguna'}`);
69
+ // 2. Preguntar configuración
70
+ const { mode } = await inquirer_1.default.prompt([
71
+ {
72
+ type: 'list',
73
+ name: 'mode',
74
+ message: 'Tipo de despliegue:',
75
+ choices: [
76
+ { name: `${chalk_1.default.green('Red local')} ${chalk_1.default.dim('— accesible desde otros equipos en tu red')}`, value: 'lan' },
77
+ { name: `${chalk_1.default.cyan('Dominio/IP pública')} ${chalk_1.default.dim('— accesible desde internet')}`, value: 'public' },
78
+ { name: `${chalk_1.default.dim('Solo localhost')} ${chalk_1.default.dim('— solo este equipo')}`, value: 'localhost' },
79
+ ],
80
+ },
81
+ ]);
82
+ let host = 'localhost';
83
+ let serverPort = '3000';
84
+ let webPort = '8080';
85
+ let useSSL = false;
86
+ if (mode === 'lan') {
87
+ const ipChoices = localIPs.map((ip) => ({ name: ip, value: ip }));
88
+ ipChoices.push({ name: 'Otra (escribir manualmente)', value: '__custom__' });
89
+ const { selectedIP } = await inquirer_1.default.prompt([
90
+ {
91
+ type: 'list',
92
+ name: 'selectedIP',
93
+ message: 'IP de la máquina en la red:',
94
+ choices: ipChoices,
95
+ },
96
+ ]);
97
+ if (selectedIP === '__custom__') {
98
+ const { customIP } = await inquirer_1.default.prompt([
99
+ { type: 'input', name: 'customIP', message: 'IP:' },
100
+ ]);
101
+ host = customIP;
102
+ }
103
+ else {
104
+ host = selectedIP;
105
+ }
106
+ }
107
+ else if (mode === 'public') {
108
+ const { domain } = await inquirer_1.default.prompt([
109
+ {
110
+ type: 'input',
111
+ name: 'domain',
112
+ message: 'Dominio o IP pública (ej: erp.miempresa.com):',
113
+ },
114
+ ]);
115
+ host = domain;
116
+ const { ssl } = await inquirer_1.default.prompt([
117
+ {
118
+ type: 'confirm',
119
+ name: 'ssl',
120
+ message: '¿Usar HTTPS (SSL)?',
121
+ default: true,
122
+ },
123
+ ]);
124
+ useSSL = ssl;
125
+ }
126
+ // Puertos
127
+ const { ports } = await inquirer_1.default.prompt([
128
+ {
129
+ type: 'confirm',
130
+ name: 'ports',
131
+ message: `¿Usar puertos por defecto? (web: 8080, api: 3000)`,
132
+ default: true,
133
+ },
134
+ ]);
135
+ if (!ports) {
136
+ const answers = await inquirer_1.default.prompt([
137
+ { type: 'input', name: 'webPort', message: 'Puerto web:', default: '8080' },
138
+ { type: 'input', name: 'serverPort', message: 'Puerto API:', default: '3000' },
139
+ ]);
140
+ webPort = answers.webPort;
141
+ serverPort = answers.serverPort;
142
+ }
143
+ // Password de BD
144
+ const { dbPassword } = await inquirer_1.default.prompt([
145
+ {
146
+ type: 'input',
147
+ name: 'dbPassword',
148
+ message: 'Password de PostgreSQL:',
149
+ default: 'openfactu_pass',
150
+ },
151
+ ]);
152
+ // 3. Construir configuración
153
+ const protocol = useSSL ? 'https' : 'http';
154
+ const webUrl = webPort === '80' || webPort === '443'
155
+ ? `${protocol}://${host}`
156
+ : `${protocol}://${host}:${webPort}`;
157
+ const apiUrl = serverPort === '80' || serverPort === '443'
158
+ ? `${protocol}://${host}`
159
+ : `${protocol}://${host}:${serverPort}`;
160
+ logger_1.log.blank();
161
+ logger_1.log.title(' Resumen de configuración');
162
+ logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
163
+ logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
164
+ logger_1.log.info(`BD Password: ${chalk_1.default.dim(dbPassword === 'openfactu_pass' ? '(default)' : '****')}`);
165
+ logger_1.log.info(`SSL: ${useSSL ? chalk_1.default.green('Si') : chalk_1.default.dim('No')}`);
166
+ logger_1.log.blank();
167
+ const { confirm } = await inquirer_1.default.prompt([
168
+ { type: 'confirm', name: 'confirm', message: 'Aplicar configuración?', default: true },
169
+ ]);
170
+ if (!confirm) {
171
+ logger_1.log.info('Cancelado');
172
+ return;
173
+ }
174
+ // 4. Escribir .env
175
+ const envSpinner = (0, ora_1.default)('Configurando .env...').start();
176
+ const env = readEnv(envPath);
177
+ env.SERVER_PORT = serverPort;
178
+ env.WEB_PORT = webPort;
179
+ env.DB_PORT = env.DB_PORT || '5432';
180
+ env.POSTGRES_USER = env.POSTGRES_USER || 'openfactu';
181
+ env.POSTGRES_PASSWORD = dbPassword;
182
+ env.POSTGRES_DB = env.POSTGRES_DB || 'openfactudb';
183
+ env.DATABASE_URL = `postgresql://${env.POSTGRES_USER}:${dbPassword}@db:5432/${env.POSTGRES_DB}`;
184
+ env.VITE_API_URL = apiUrl;
185
+ env.HOST = host;
186
+ env.CORS_ORIGIN = webUrl;
187
+ writeEnv(envPath, env);
188
+ envSpinner.succeed('.env configurado');
189
+ // 5. Generar docker-compose.prod.yml con bind a 0.0.0.0
190
+ const prodComposePath = path_1.default.join(root, 'docker-compose.prod.yml');
191
+ const prodSpinner = (0, ora_1.default)('Generando docker-compose.prod.yml...').start();
192
+ let composeContent = `version: '3.8'
193
+
194
+ services:
195
+ web:
196
+ build:
197
+ context: .
198
+ dockerfile: apps/web/Dockerfile
199
+ args:
200
+ VITE_API_URL: "${apiUrl}"
201
+ ports:
202
+ - "0.0.0.0:${webPort}:80"
203
+ environment:
204
+ VITE_API_URL: "${apiUrl}"
205
+ depends_on:
206
+ - server
207
+ restart: unless-stopped
208
+ networks:
209
+ - openfactu_net
210
+
211
+ server:
212
+ build:
213
+ context: .
214
+ dockerfile: apps/server/Dockerfile
215
+ ports:
216
+ - "0.0.0.0:${serverPort}:3000"
217
+ env_file:
218
+ - .env
219
+ volumes:
220
+ - ./plugins:/app/plugins
221
+ - ./storage:/app/storage
222
+ depends_on:
223
+ - db
224
+ environment:
225
+ - DATABASE_URL=postgresql://\${POSTGRES_USER:-openfactu}:\${POSTGRES_PASSWORD:-openfactu_pass}@db:5432/\${POSTGRES_DB:-openfactudb}
226
+ - CORS_ORIGIN=${webUrl}
227
+ - NODE_ENV=production
228
+ restart: unless-stopped
229
+ networks:
230
+ - openfactu_net
231
+
232
+ db:
233
+ image: postgres:15-alpine
234
+ environment:
235
+ POSTGRES_USER: \${POSTGRES_USER:-openfactu}
236
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-openfactu_pass}
237
+ POSTGRES_DB: \${POSTGRES_DB:-openfactudb}
238
+ ports:
239
+ - "127.0.0.1:\${DB_PORT:-5432}:5432"
240
+ volumes:
241
+ - ./storage/db_data:/var/lib/postgresql/data
242
+ restart: unless-stopped
243
+ networks:
244
+ - openfactu_net
245
+
246
+ networks:
247
+ openfactu_net:
248
+ driver: bridge
249
+ `;
250
+ // Si SSL, añadir nginx reverse proxy
251
+ if (useSSL) {
252
+ composeContent += `
253
+ # Para SSL, configura un reverse proxy (nginx, traefik, caddy) delante.
254
+ # Ejemplo con Caddy (descomentar):
255
+ #
256
+ # caddy:
257
+ # image: caddy:2-alpine
258
+ # ports:
259
+ # - "0.0.0.0:80:80"
260
+ # - "0.0.0.0:443:443"
261
+ # volumes:
262
+ # - ./Caddyfile:/etc/caddy/Caddyfile
263
+ # - caddy_data:/data
264
+ # depends_on:
265
+ # - web
266
+ # - server
267
+ # networks:
268
+ # - openfactu_net
269
+ #
270
+ # volumes:
271
+ # caddy_data:
272
+ #
273
+ # Caddyfile:
274
+ # ${host} {
275
+ # handle /api/* {
276
+ # reverse_proxy server:3000
277
+ # }
278
+ # handle {
279
+ # reverse_proxy web:80
280
+ # }
281
+ # }
282
+ `;
283
+ }
284
+ fs_1.default.writeFileSync(prodComposePath, composeContent);
285
+ prodSpinner.succeed('docker-compose.prod.yml generado');
286
+ // 6. Preguntar si levantar
287
+ logger_1.log.blank();
288
+ const { start } = await inquirer_1.default.prompt([
289
+ {
290
+ type: 'confirm',
291
+ name: 'start',
292
+ message: '¿Levantar los servicios ahora?',
293
+ default: true,
294
+ },
295
+ ]);
296
+ if (start) {
297
+ const startSpinner = (0, ora_1.default)('Construyendo y levantando servicios...').start();
298
+ try {
299
+ (0, child_process_1.execSync)('docker compose -f docker-compose.prod.yml up -d --build', {
300
+ cwd: root,
301
+ stdio: 'pipe',
302
+ timeout: 300000,
303
+ });
304
+ startSpinner.succeed('Servicios levantados');
305
+ }
306
+ catch (err) {
307
+ startSpinner.fail('Error: ' + err.message);
308
+ logger_1.log.dim(' Ejecuta manualmente:');
309
+ logger_1.log.dim(` cd ${root} && docker compose -f docker-compose.prod.yml up -d --build`);
310
+ }
311
+ }
312
+ logger_1.log.blank();
313
+ console.log(chalk_1.default.bold.green(' Despliegue configurado'));
314
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
315
+ console.log(` ${chalk_1.default.dim('Web:')} ${chalk_1.default.cyan(webUrl)}`);
316
+ console.log(` ${chalk_1.default.dim('API:')} ${chalk_1.default.cyan(apiUrl)}`);
317
+ logger_1.log.blank();
318
+ if (mode === 'lan') {
319
+ logger_1.log.info('Accede desde otros equipos de la red con la URL de arriba');
320
+ }
321
+ else if (mode === 'public') {
322
+ logger_1.log.info('Asegúrate de que los puertos estén abiertos en el firewall');
323
+ if (useSSL) {
324
+ logger_1.log.info('Configura el reverse proxy (Caddy/Nginx) para SSL');
325
+ }
326
+ }
327
+ logger_1.log.blank();
328
+ logger_1.log.dim(' Comandos útiles:');
329
+ logger_1.log.dim(` docker compose -f docker-compose.prod.yml logs -f — Ver logs`);
330
+ logger_1.log.dim(` docker compose -f docker-compose.prod.yml down — Parar`);
331
+ logger_1.log.dim(` docker compose -f docker-compose.prod.yml restart — Reiniciar`);
332
+ logger_1.log.blank();
333
+ }
334
+ catch (err) {
335
+ logger_1.log.error(err.message);
336
+ process.exitCode = 1;
337
+ }
338
+ });
339
+ // ── openfactu deploy:status ──
340
+ program
341
+ .command('deploy:status')
342
+ .description('Muestra el estado de los servicios Docker')
343
+ .action(async () => {
344
+ try {
345
+ const root = (0, paths_1.getProjectRoot)();
346
+ const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
347
+ const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
348
+ logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
349
+ logger_1.log.blank();
350
+ const output = (0, child_process_1.execSync)(`docker compose -f ${composeFile} ps`, {
351
+ cwd: root,
352
+ }).toString();
353
+ console.log(output);
354
+ // Mostrar URLs
355
+ const envPath = path_1.default.join(root, '.env');
356
+ const env = readEnv(envPath);
357
+ const host = env.HOST || 'localhost';
358
+ const webPort = env.WEB_PORT || '8080';
359
+ const serverPort = env.SERVER_PORT || '3000';
360
+ const protocol = env.VITE_API_URL?.startsWith('https') ? 'https' : 'http';
361
+ logger_1.log.blank();
362
+ logger_1.log.info(`Web: ${chalk_1.default.cyan(`${protocol}://${host}:${webPort}`)}`);
363
+ logger_1.log.info(`API: ${chalk_1.default.cyan(`${protocol}://${host}:${serverPort}`)}`);
364
+ }
365
+ catch (err) {
366
+ logger_1.log.error('Docker no disponible o servicios no levantados');
367
+ logger_1.log.dim(' ' + err.message);
368
+ }
369
+ });
370
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerInstallCommand(program: Command): void;
@@ -0,0 +1,325 @@
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.registerInstallCommand = registerInstallCommand;
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 https_1 = __importDefault(require("https"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const logger_1 = require("../utils/logger");
16
+ const REPO_URL = 'https://github.com/AngelAcedo12/OpenFactu.git';
17
+ const GITHUB_OWNER = 'AngelAcedo12';
18
+ const GITHUB_REPO = 'OpenFactu';
19
+ function fetchJSON(url) {
20
+ return new Promise((resolve, reject) => {
21
+ https_1.default.get(url, { headers: { 'User-Agent': 'openfactu-cli' } }, (res) => {
22
+ let data = '';
23
+ res.on('data', (chunk) => (data += chunk));
24
+ res.on('end', () => {
25
+ try {
26
+ resolve(JSON.parse(data));
27
+ }
28
+ catch {
29
+ reject(new Error('Respuesta no es JSON'));
30
+ }
31
+ });
32
+ }).on('error', reject);
33
+ });
34
+ }
35
+ async function getGithubReleases() {
36
+ try {
37
+ const data = await fetchJSON(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/releases`);
38
+ if (!Array.isArray(data))
39
+ return [];
40
+ return data.filter((r) => !r.draft);
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ }
46
+ function getAvailableBranches() {
47
+ try {
48
+ const output = (0, child_process_1.execSync)(`git ls-remote --heads ${REPO_URL}`, {
49
+ stdio: ['pipe', 'pipe', 'pipe'],
50
+ timeout: 15000,
51
+ }).toString().trim();
52
+ if (!output)
53
+ return [];
54
+ return output
55
+ .split('\n')
56
+ .map((line) => {
57
+ const match = line.match(/refs\/heads\/(.+)$/);
58
+ return match ? match[1] : null;
59
+ })
60
+ .filter((b) => b !== null);
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ }
66
+ function checkDocker() {
67
+ try {
68
+ (0, child_process_1.execSync)('docker --version', { stdio: 'pipe' });
69
+ (0, child_process_1.execSync)('docker compose version', { stdio: 'pipe' });
70
+ return true;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ function registerInstallCommand(program) {
77
+ program
78
+ .command('install [directory]')
79
+ .description('Descarga e instala OpenFactu en un directorio')
80
+ .option('-t, --tag <tag>', 'Versión/tag específico (ej: v1.0.0)')
81
+ .option('-b, --branch <branch>', 'Branch específica (default: main)')
82
+ .option('--repo <url>', 'URL del repositorio', REPO_URL)
83
+ .option('--skip-deps', 'No instalar dependencias (npm install)')
84
+ .action(async (directory, opts) => {
85
+ console.log();
86
+ console.log(chalk_1.default.bold.white(' OpenFactu — Instalación'));
87
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
88
+ console.log();
89
+ try {
90
+ const repoUrl = opts.repo;
91
+ // 1. Obtener releases de GitHub
92
+ const fetchSpinner = (0, ora_1.default)('Consultando releases en GitHub...').start();
93
+ const releases = await getGithubReleases();
94
+ const branches = getAvailableBranches();
95
+ fetchSpinner.succeed(`${releases.length} release(s), ${branches.length} branches disponibles`);
96
+ // 2. Elegir versión
97
+ let ref;
98
+ if (opts.tag) {
99
+ ref = opts.tag;
100
+ }
101
+ else if (opts.branch) {
102
+ ref = opts.branch;
103
+ }
104
+ else {
105
+ // Menú interactivo
106
+ const choices = [];
107
+ // Releases estables primero
108
+ const stable = releases.filter((r) => !r.prerelease);
109
+ const prerelease = releases.filter((r) => r.prerelease);
110
+ for (const rel of stable.slice(0, 10)) {
111
+ const date = new Date(rel.published_at).toLocaleDateString('es-ES');
112
+ choices.push({
113
+ name: `${chalk_1.default.green(rel.tag_name)} ${chalk_1.default.white(rel.name || '')} ${chalk_1.default.dim(`(${date})`)}`,
114
+ value: rel.tag_name,
115
+ });
116
+ }
117
+ if (prerelease.length > 0) {
118
+ choices.push(new inquirer_1.default.Separator(chalk_1.default.dim('── Pre-releases ──')));
119
+ for (const rel of prerelease.slice(0, 5)) {
120
+ const date = new Date(rel.published_at).toLocaleDateString('es-ES');
121
+ choices.push({
122
+ name: `${chalk_1.default.yellow(rel.tag_name)} ${chalk_1.default.white(rel.name || '')} ${chalk_1.default.dim(`(${date}) pre-release`)}`,
123
+ value: rel.tag_name,
124
+ });
125
+ }
126
+ }
127
+ if (branches.length > 0) {
128
+ choices.push(new inquirer_1.default.Separator(chalk_1.default.dim('── Branches ──')));
129
+ for (const branch of branches) {
130
+ const label = branch === 'main' ? chalk_1.default.dim('(última versión)') : '';
131
+ choices.push({
132
+ name: `${chalk_1.default.cyan(branch)} ${label}`,
133
+ value: branch,
134
+ });
135
+ }
136
+ }
137
+ if (choices.length === 0) {
138
+ choices.push({ name: 'main (default)', value: 'main' });
139
+ }
140
+ const { selected } = await inquirer_1.default.prompt([
141
+ {
142
+ type: 'list',
143
+ name: 'selected',
144
+ message: 'Selecciona la versión a instalar:',
145
+ choices,
146
+ pageSize: 15,
147
+ },
148
+ ]);
149
+ ref = selected;
150
+ }
151
+ logger_1.log.info(`Versión seleccionada: ${chalk_1.default.cyan(ref)}`);
152
+ // 3. Directorio destino
153
+ let targetDir = directory;
154
+ if (!targetDir) {
155
+ const { dir } = await inquirer_1.default.prompt([
156
+ {
157
+ type: 'input',
158
+ name: 'dir',
159
+ message: 'Directorio de instalación:',
160
+ default: path_1.default.join(os_1.default.homedir(), 'openfactu'),
161
+ },
162
+ ]);
163
+ targetDir = dir;
164
+ }
165
+ targetDir = path_1.default.resolve(targetDir);
166
+ // Verificar si el directorio ya existe
167
+ if (fs_1.default.existsSync(targetDir)) {
168
+ const contents = fs_1.default.readdirSync(targetDir);
169
+ if (contents.length > 0) {
170
+ const { overwrite } = await inquirer_1.default.prompt([
171
+ {
172
+ type: 'confirm',
173
+ name: 'overwrite',
174
+ message: `El directorio ${targetDir} no está vacío. ¿Continuar?`,
175
+ default: false,
176
+ },
177
+ ]);
178
+ if (!overwrite) {
179
+ logger_1.log.info('Instalación cancelada');
180
+ return;
181
+ }
182
+ }
183
+ }
184
+ logger_1.log.info(`Directorio: ${chalk_1.default.dim(targetDir)}`);
185
+ logger_1.log.blank();
186
+ // 4. Crear directorio si no existe (con sudo si hace falta)
187
+ if (!fs_1.default.existsSync(targetDir)) {
188
+ try {
189
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
190
+ }
191
+ catch (mkdirErr) {
192
+ if (mkdirErr.code === 'EACCES') {
193
+ logger_1.log.warn('Sin permisos. Creando directorio con sudo...');
194
+ try {
195
+ const user = process.env.USER || process.env.USERNAME || 'root';
196
+ (0, child_process_1.execSync)(`sudo mkdir -p "${targetDir}" && sudo chown -R ${user}:${user} "${targetDir}"`, {
197
+ stdio: 'inherit',
198
+ });
199
+ }
200
+ catch {
201
+ logger_1.log.error(`No se pudo crear ${targetDir}. Ejecuta con sudo o elige otro directorio.`);
202
+ return;
203
+ }
204
+ }
205
+ else {
206
+ throw mkdirErr;
207
+ }
208
+ }
209
+ }
210
+ // 5. Clonar repositorio
211
+ const cloneSpinner = (0, ora_1.default)('Descargando OpenFactu...').start();
212
+ const isTag = releases.some((r) => r.tag_name === ref);
213
+ const cloneCmd = isTag
214
+ ? `git clone --depth 1 --branch ${ref} ${repoUrl} "${targetDir}"`
215
+ : `git clone --branch ${ref} ${repoUrl} "${targetDir}"`;
216
+ try {
217
+ (0, child_process_1.execSync)(cloneCmd, { stdio: 'pipe', timeout: 120000 });
218
+ cloneSpinner.succeed('Código descargado');
219
+ }
220
+ catch (err) {
221
+ // Fallback: clonar todo y checkout
222
+ try {
223
+ cloneSpinner.text = 'Descargando (método alternativo)...';
224
+ (0, child_process_1.execSync)(`git clone ${repoUrl} "${targetDir}"`, { stdio: 'pipe', timeout: 180000 });
225
+ (0, child_process_1.execSync)(`git checkout ${ref}`, { cwd: targetDir, stdio: 'pipe' });
226
+ cloneSpinner.succeed('Código descargado');
227
+ }
228
+ catch (err2) {
229
+ cloneSpinner.fail('Error al descargar: ' + err2.message);
230
+ return;
231
+ }
232
+ }
233
+ // 6. Copiar .env.example a .env
234
+ const envExample = path_1.default.join(targetDir, '.env.example');
235
+ const envFile = path_1.default.join(targetDir, '.env');
236
+ if (fs_1.default.existsSync(envExample) && !fs_1.default.existsSync(envFile)) {
237
+ fs_1.default.copyFileSync(envExample, envFile);
238
+ logger_1.log.success('Archivo .env creado desde .env.example');
239
+ }
240
+ // 7. Preguntar modo de instalación
241
+ const hasDocker = checkDocker();
242
+ if (!hasDocker) {
243
+ logger_1.log.warn('Docker no detectado. OpenFactu requiere Docker para funcionar.');
244
+ logger_1.log.dim(' Instala Docker Desktop: https://docs.docker.com/get-docker/');
245
+ logger_1.log.blank();
246
+ }
247
+ const { installMode } = await inquirer_1.default.prompt([
248
+ {
249
+ type: 'list',
250
+ name: 'installMode',
251
+ message: 'Modo de instalación:',
252
+ choices: [
253
+ ...(hasDocker ? [{
254
+ name: `${chalk_1.default.green('Docker')} ${chalk_1.default.dim('— recomendado, funciona en Windows/Mac/Linux')}`,
255
+ value: 'docker',
256
+ }] : []),
257
+ {
258
+ name: `${chalk_1.default.dim('Solo descargar')} ${chalk_1.default.dim('— instalar dependencias manualmente después')}`,
259
+ value: 'none',
260
+ },
261
+ ],
262
+ },
263
+ ]);
264
+ if (installMode === 'docker') {
265
+ // Docker: build + up
266
+ const dockerSpinner = (0, ora_1.default)('Construyendo contenedores Docker...').start();
267
+ try {
268
+ (0, child_process_1.execSync)('docker compose build', { cwd: targetDir, stdio: 'pipe', timeout: 300000 });
269
+ dockerSpinner.succeed('Contenedores construidos');
270
+ const { startNow } = await inquirer_1.default.prompt([
271
+ { type: 'confirm', name: 'startNow', message: '¿Arrancar los servicios?', default: true },
272
+ ]);
273
+ if (startNow) {
274
+ const upSpinner = (0, ora_1.default)('Levantando servicios...').start();
275
+ (0, child_process_1.execSync)('docker compose up -d', { cwd: targetDir, stdio: 'pipe', timeout: 120000 });
276
+ upSpinner.succeed('Servicios levantados');
277
+ }
278
+ }
279
+ catch (err) {
280
+ dockerSpinner.fail('Error Docker: ' + err.message);
281
+ logger_1.log.dim(` Ejecuta manualmente: cd ${targetDir} && docker compose up -d`);
282
+ }
283
+ }
284
+ // 7. Resumen
285
+ const installedPkg = path_1.default.join(targetDir, 'package.json');
286
+ let installedVersion = '?';
287
+ if (fs_1.default.existsSync(installedPkg)) {
288
+ try {
289
+ const pkg = JSON.parse(fs_1.default.readFileSync(installedPkg, 'utf-8'));
290
+ installedVersion = pkg.version || '?';
291
+ }
292
+ catch { }
293
+ }
294
+ let installedCommit = '?';
295
+ try {
296
+ installedCommit = (0, child_process_1.execSync)('git rev-parse --short HEAD', { cwd: targetDir }).toString().trim();
297
+ }
298
+ catch { }
299
+ logger_1.log.blank();
300
+ console.log(chalk_1.default.bold.green(' Instalación completada'));
301
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
302
+ console.log(` ${chalk_1.default.dim('Versión:')} ${chalk_1.default.cyan(installedVersion)}`);
303
+ console.log(` ${chalk_1.default.dim('Ref:')} ${chalk_1.default.cyan(ref)}`);
304
+ console.log(` ${chalk_1.default.dim('Commit:')} ${chalk_1.default.cyan(installedCommit)}`);
305
+ console.log(` ${chalk_1.default.dim('Directorio:')} ${chalk_1.default.white(targetDir)}`);
306
+ console.log(` ${chalk_1.default.dim('Modo:')} ${chalk_1.default.white(installMode)}`);
307
+ logger_1.log.blank();
308
+ logger_1.log.dim(' Próximos pasos:');
309
+ logger_1.log.dim(` cd ${targetDir}`);
310
+ if (installMode === 'docker') {
311
+ logger_1.log.dim(' openfactu deploy — Configurar acceso externo');
312
+ logger_1.log.dim(' openfactu deploy:status — Ver estado de servicios');
313
+ }
314
+ else {
315
+ logger_1.log.dim(' docker compose up -d — Levantar con Docker');
316
+ logger_1.log.dim(' openfactu deploy — Configurar acceso externo');
317
+ }
318
+ logger_1.log.blank();
319
+ }
320
+ catch (err) {
321
+ logger_1.log.error(err.message);
322
+ process.exitCode = 1;
323
+ }
324
+ });
325
+ }
@@ -11,11 +11,12 @@ const path_1 = __importDefault(require("path"));
11
11
  const fs_1 = __importDefault(require("fs"));
12
12
  const db_1 = require("../utils/db");
13
13
  const logger_1 = require("../utils/logger");
14
- const MIGRATIONS_DIR = require('../utils/paths').getMigrationsDir();
14
+ const paths_1 = require("../utils/paths");
15
+ function getMigrDir() { return (0, paths_1.getMigrationsDir)(); }
15
16
  function getMigrationFiles() {
16
- if (!fs_1.default.existsSync(MIGRATIONS_DIR))
17
+ if (!fs_1.default.existsSync(getMigrDir()))
17
18
  return [];
18
- return fs_1.default.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort();
19
+ return fs_1.default.readdirSync(getMigrDir()).filter((f) => f.endsWith('.sql')).sort();
19
20
  }
20
21
  async function getAppliedMigrations(tenantDb, schemaName) {
21
22
  try {
@@ -28,7 +29,7 @@ async function getAppliedMigrations(tenantDb, schemaName) {
28
29
  }
29
30
  async function applyMigration(tenantDb, schemaName, file) {
30
31
  const migrationId = file.replace('.sql', '');
31
- const filePath = path_1.default.join(MIGRATIONS_DIR, file);
32
+ const filePath = path_1.default.join(getMigrDir(), file);
32
33
  let rawSql = fs_1.default.readFileSync(filePath, 'utf8');
33
34
  const processedSql = rawSql.replace(/{{schema}}/g, schemaName);
34
35
  const statements = processedSql
@@ -12,7 +12,6 @@ const path_1 = __importDefault(require("path"));
12
12
  const db_1 = require("../utils/db");
13
13
  const logger_1 = require("../utils/logger");
14
14
  const paths_1 = require("../utils/paths");
15
- const PLUGINS_DIR = (0, paths_1.getPluginsDir)();
16
15
  function registerPluginCommand(program) {
17
16
  const plugin = program
18
17
  .command('plugin')
@@ -26,8 +25,8 @@ function registerPluginCommand(program) {
26
25
  try {
27
26
  // Leer plugins del filesystem
28
27
  const installed = [];
29
- if (fs_1.default.existsSync(PLUGINS_DIR)) {
30
- const dirs = fs_1.default.readdirSync(PLUGINS_DIR).filter((d) => fs_1.default.statSync(path_1.default.join(PLUGINS_DIR, d)).isDirectory());
28
+ if (fs_1.default.existsSync((0, paths_1.getPluginsDir)())) {
29
+ const dirs = fs_1.default.readdirSync((0, paths_1.getPluginsDir)()).filter((d) => fs_1.default.statSync(path_1.default.join((0, paths_1.getPluginsDir)(), d)).isDirectory());
31
30
  installed.push(...dirs);
32
31
  }
33
32
  if (installed.length === 0) {
@@ -38,7 +37,7 @@ function registerPluginCommand(program) {
38
37
  const publicDb = (0, db_1.getPublicDb)();
39
38
  let tenantPlugins = [];
40
39
  try {
41
- tenantPlugins = await publicDb.select().from(db_1.schema.tenantPlugins);
40
+ tenantPlugins = await publicDb.select().from((0, db_1.schema)().tenantPlugins);
42
41
  }
43
42
  catch {
44
43
  // Tabla puede no existir aún
@@ -55,7 +54,7 @@ function registerPluginCommand(program) {
55
54
  style: { head: [], border: ['dim'] },
56
55
  });
57
56
  for (const pluginId of installed) {
58
- const manifestPath = path_1.default.join(PLUGINS_DIR, pluginId, 'manifest.json');
57
+ const manifestPath = path_1.default.join((0, paths_1.getPluginsDir)(), pluginId, 'manifest.json');
59
58
  const hasManifest = fs_1.default.existsSync(manifestPath);
60
59
  let manifest = null;
61
60
  if (hasManifest) {
@@ -50,8 +50,8 @@ function registerSetupCommand(program) {
50
50
  const adminSpinner = (0, ora_1.default)('Verificando usuario administrador...').start();
51
51
  const [existingAdmin] = await publicDb
52
52
  .select()
53
- .from(db_1.schema.globalUsers)
54
- .where((0, db_1.eq)(db_1.schema.globalUsers.username, 'admin'));
53
+ .from((0, db_1.schema)().globalUsers)
54
+ .where((0, db_1.eq)((0, db_1.schema)().globalUsers.username, 'admin'));
55
55
  if (existingAdmin) {
56
56
  adminSpinner.succeed('Usuario admin ya existe');
57
57
  }
@@ -67,7 +67,7 @@ function registerSetupCommand(program) {
67
67
  },
68
68
  ]);
69
69
  const hashedPassword = await bcrypt_1.default.hash(adminPassword, 10);
70
- await publicDb.insert(db_1.schema.globalUsers).values({
70
+ await publicDb.insert((0, db_1.schema)().globalUsers).values({
71
71
  id: crypto_1.default.randomUUID(),
72
72
  email: 'admin@openfactu.com',
73
73
  username: 'admin',
@@ -77,7 +77,7 @@ function registerSetupCommand(program) {
77
77
  adminSpinner.succeed('Usuario admin creado (admin@openfactu.com)');
78
78
  }
79
79
  // 4. Verificar/crear primer tenant
80
- const tenants = await publicDb.select().from(db_1.schema.tenants);
80
+ const tenants = await publicDb.select().from((0, db_1.schema)().tenants);
81
81
  if (tenants.length > 0) {
82
82
  logger_1.log.success(`${tenants.length} tenant(s) ya existen`);
83
83
  }
@@ -106,7 +106,7 @@ function registerSetupCommand(program) {
106
106
  .replace(/^_|_$/g, '');
107
107
  const tenantSpinner = (0, ora_1.default)(`Creando empresa "${tenantName}"...`).start();
108
108
  const tenantId = crypto_1.default.randomUUID();
109
- await publicDb.insert(db_1.schema.tenants).values({
109
+ await publicDb.insert((0, db_1.schema)().tenants).values({
110
110
  id: tenantId,
111
111
  name: tenantName,
112
112
  schemaName,
@@ -118,10 +118,10 @@ function registerSetupCommand(program) {
118
118
  // Asignar admin al tenant
119
119
  const [admin] = await publicDb
120
120
  .select()
121
- .from(db_1.schema.globalUsers)
122
- .where((0, db_1.eq)(db_1.schema.globalUsers.username, 'admin'));
121
+ .from((0, db_1.schema)().globalUsers)
122
+ .where((0, db_1.eq)((0, db_1.schema)().globalUsers.username, 'admin'));
123
123
  if (admin) {
124
- await publicDb.insert(db_1.schema.userTenantMemberships).values({
124
+ await publicDb.insert((0, db_1.schema)().userTenantMemberships).values({
125
125
  id: crypto_1.default.randomUUID(),
126
126
  userId: admin.id,
127
127
  tenantId,
@@ -80,15 +80,15 @@ function registerTenantCommand(program) {
80
80
  // Verificar que no exista
81
81
  const [existing] = await publicDb
82
82
  .select()
83
- .from(db_1.schema.tenants)
84
- .where((0, db_1.eq)(db_1.schema.tenants.name, tenantName));
83
+ .from((0, db_1.schema)().tenants)
84
+ .where((0, db_1.eq)((0, db_1.schema)().tenants.name, tenantName));
85
85
  if (existing) {
86
86
  spinner.warn(`El tenant "${tenantName}" ya existe (${existing.id})`);
87
87
  return;
88
88
  }
89
89
  // Crear registro en la tabla Tenant
90
90
  const tenantId = crypto_1.default.randomUUID();
91
- await publicDb.insert(db_1.schema.tenants).values({
91
+ await publicDb.insert((0, db_1.schema)().tenants).values({
92
92
  id: tenantId,
93
93
  name: tenantName,
94
94
  schemaName,
@@ -12,12 +12,12 @@ const fs_1 = __importDefault(require("fs"));
12
12
  const path_1 = __importDefault(require("path"));
13
13
  const logger_1 = require("../utils/logger");
14
14
  const paths_1 = require("../utils/paths");
15
- const ROOT_DIR = (0, paths_1.getProjectRoot)();
15
+ function ROOT_DIR() { return (0, paths_1.getProjectRoot)(); }
16
16
  const BACKUP_DIRS = ['storage', 'plugins', '.env'];
17
17
  const SAFE_DIRS = ['storage', 'plugins', 'node_modules', '.env', '.git'];
18
18
  function getCurrentVersion() {
19
19
  try {
20
- const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(ROOT_DIR, 'package.json'), 'utf-8'));
20
+ const pkg = JSON.parse(fs_1.default.readFileSync(path_1.default.join(ROOT_DIR(), 'package.json'), 'utf-8'));
21
21
  return pkg.version || '0.0.0';
22
22
  }
23
23
  catch {
@@ -26,7 +26,7 @@ function getCurrentVersion() {
26
26
  }
27
27
  function getCurrentBranch() {
28
28
  try {
29
- return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { cwd: ROOT_DIR }).toString().trim();
29
+ return (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { cwd: ROOT_DIR() }).toString().trim();
30
30
  }
31
31
  catch {
32
32
  return 'unknown';
@@ -34,7 +34,7 @@ function getCurrentBranch() {
34
34
  }
35
35
  function getCurrentCommit() {
36
36
  try {
37
- return (0, child_process_1.execSync)('git rev-parse --short HEAD', { cwd: ROOT_DIR }).toString().trim();
37
+ return (0, child_process_1.execSync)('git rev-parse --short HEAD', { cwd: ROOT_DIR() }).toString().trim();
38
38
  }
39
39
  catch {
40
40
  return 'unknown';
@@ -42,7 +42,7 @@ function getCurrentCommit() {
42
42
  }
43
43
  function hasUncommittedChanges() {
44
44
  try {
45
- const output = (0, child_process_1.execSync)('git status --porcelain', { cwd: ROOT_DIR }).toString().trim();
45
+ const output = (0, child_process_1.execSync)('git status --porcelain', { cwd: ROOT_DIR() }).toString().trim();
46
46
  return output.length > 0;
47
47
  }
48
48
  catch {
@@ -51,8 +51,8 @@ function hasUncommittedChanges() {
51
51
  }
52
52
  function getRemoteTags() {
53
53
  try {
54
- (0, child_process_1.execSync)('git fetch --tags', { cwd: ROOT_DIR, stdio: 'pipe' });
55
- const output = (0, child_process_1.execSync)('git tag --list "v*" --sort=-version:refname', { cwd: ROOT_DIR }).toString().trim();
54
+ (0, child_process_1.execSync)('git fetch --tags', { cwd: ROOT_DIR(), stdio: 'pipe' });
55
+ const output = (0, child_process_1.execSync)('git tag --list "v*" --sort=-version:refname', { cwd: ROOT_DIR() }).toString().trim();
56
56
  return output ? output.split('\n') : [];
57
57
  }
58
58
  catch {
@@ -61,8 +61,8 @@ function getRemoteTags() {
61
61
  }
62
62
  function getRemoteLatestCommit(branch) {
63
63
  try {
64
- (0, child_process_1.execSync)('git fetch origin', { cwd: ROOT_DIR, stdio: 'pipe' });
65
- return (0, child_process_1.execSync)(`git rev-parse --short origin/${branch}`, { cwd: ROOT_DIR }).toString().trim();
64
+ (0, child_process_1.execSync)('git fetch origin', { cwd: ROOT_DIR(), stdio: 'pipe' });
65
+ return (0, child_process_1.execSync)(`git rev-parse --short origin/${branch}`, { cwd: ROOT_DIR() }).toString().trim();
66
66
  }
67
67
  catch {
68
68
  return 'unknown';
@@ -109,7 +109,7 @@ function registerUpdateCommand(program) {
109
109
  // Guardar cambios locales
110
110
  const stashSpinner = (0, ora_1.default)('Guardando cambios locales (git stash)...').start();
111
111
  try {
112
- (0, child_process_1.execSync)('git stash push -m "openfactu-cli-update-backup"', { cwd: ROOT_DIR, stdio: 'pipe' });
112
+ (0, child_process_1.execSync)('git stash push -m "openfactu-cli-update-backup"', { cwd: ROOT_DIR(), stdio: 'pipe' });
113
113
  stashSpinner.succeed('Cambios locales guardados en stash');
114
114
  }
115
115
  catch (err) {
@@ -119,7 +119,7 @@ function registerUpdateCommand(program) {
119
119
  // 2. Fetch remoto
120
120
  const fetchSpinner = (0, ora_1.default)('Descargando información del repositorio...').start();
121
121
  try {
122
- (0, child_process_1.execSync)('git fetch --all --tags', { cwd: ROOT_DIR, stdio: 'pipe' });
122
+ (0, child_process_1.execSync)('git fetch --all --tags', { cwd: ROOT_DIR(), stdio: 'pipe' });
123
123
  fetchSpinner.succeed('Repositorio actualizado');
124
124
  }
125
125
  catch (err) {
@@ -142,10 +142,10 @@ function registerUpdateCommand(program) {
142
142
  }
143
143
  // Mostrar commits pendientes
144
144
  try {
145
- const behindCount = (0, child_process_1.execSync)(`git rev-list --count HEAD..origin/${branch}`, { cwd: ROOT_DIR }).toString().trim();
145
+ const behindCount = (0, child_process_1.execSync)(`git rev-list --count HEAD..origin/${branch}`, { cwd: ROOT_DIR() }).toString().trim();
146
146
  logger_1.log.info(`Commits nuevos disponibles: ${chalk_1.default.yellow(behindCount)}`);
147
147
  // Mostrar resumen de cambios
148
- const changelog = (0, child_process_1.execSync)(`git log --oneline HEAD..origin/${branch} --max-count=10`, { cwd: ROOT_DIR }).toString().trim();
148
+ const changelog = (0, child_process_1.execSync)(`git log --oneline HEAD..origin/${branch} --max-count=10`, { cwd: ROOT_DIR() }).toString().trim();
149
149
  if (changelog) {
150
150
  logger_1.log.blank();
151
151
  logger_1.log.dim(' Cambios recientes:');
@@ -177,7 +177,7 @@ function registerUpdateCommand(program) {
177
177
  const backupSpinner = (0, ora_1.default)('Verificando archivos protegidos...').start();
178
178
  const protectedFiles = [];
179
179
  for (const item of BACKUP_DIRS) {
180
- const itemPath = path_1.default.join(ROOT_DIR, item);
180
+ const itemPath = path_1.default.join(ROOT_DIR(), item);
181
181
  if (fs_1.default.existsSync(itemPath)) {
182
182
  protectedFiles.push(item);
183
183
  }
@@ -187,11 +187,11 @@ function registerUpdateCommand(program) {
187
187
  const updateSpinner = (0, ora_1.default)('Aplicando actualización...').start();
188
188
  try {
189
189
  if (opts.tag) {
190
- (0, child_process_1.execSync)(`git checkout ${opts.tag}`, { cwd: ROOT_DIR, stdio: 'pipe' });
190
+ (0, child_process_1.execSync)(`git checkout ${opts.tag}`, { cwd: ROOT_DIR(), stdio: 'pipe' });
191
191
  }
192
192
  else {
193
193
  const branch = opts.branch || currentBranch || 'main';
194
- (0, child_process_1.execSync)(`git pull origin ${branch} --ff-only`, { cwd: ROOT_DIR, stdio: 'pipe' });
194
+ (0, child_process_1.execSync)(`git pull origin ${branch} --ff-only`, { cwd: ROOT_DIR(), stdio: 'pipe' });
195
195
  }
196
196
  updateSpinner.succeed('Código actualizado');
197
197
  }
@@ -200,7 +200,7 @@ function registerUpdateCommand(program) {
200
200
  logger_1.log.warn('Intentando merge...');
201
201
  try {
202
202
  const branch = opts.branch || currentBranch || 'main';
203
- (0, child_process_1.execSync)(`git pull origin ${branch}`, { cwd: ROOT_DIR, stdio: 'pipe' });
203
+ (0, child_process_1.execSync)(`git pull origin ${branch}`, { cwd: ROOT_DIR(), stdio: 'pipe' });
204
204
  logger_1.log.success('Merge completado');
205
205
  }
206
206
  catch (mergeErr) {
@@ -213,7 +213,7 @@ function registerUpdateCommand(program) {
213
213
  // 7. Instalar dependencias
214
214
  const depsSpinner = (0, ora_1.default)('Instalando dependencias...').start();
215
215
  try {
216
- (0, child_process_1.execSync)('npm install', { cwd: ROOT_DIR, stdio: 'pipe', timeout: 120000 });
216
+ (0, child_process_1.execSync)('npm install', { cwd: ROOT_DIR(), stdio: 'pipe', timeout: 120000 });
217
217
  depsSpinner.succeed('Dependencias instaladas');
218
218
  }
219
219
  catch (err) {
@@ -248,7 +248,7 @@ function registerUpdateCommand(program) {
248
248
  const currentVersion = getCurrentVersion();
249
249
  const currentBranch = getCurrentBranch();
250
250
  const currentCommit = getCurrentCommit();
251
- (0, child_process_1.execSync)('git fetch --all --tags', { cwd: ROOT_DIR, stdio: 'pipe' });
251
+ (0, child_process_1.execSync)('git fetch --all --tags', { cwd: ROOT_DIR(), stdio: 'pipe' });
252
252
  const remoteCommit = getRemoteLatestCommit(currentBranch);
253
253
  const tags = getRemoteTags();
254
254
  spinner.stop();
@@ -258,7 +258,7 @@ function registerUpdateCommand(program) {
258
258
  logger_1.log.success('Estás en la última versión');
259
259
  }
260
260
  else {
261
- const behindCount = (0, child_process_1.execSync)(`git rev-list --count HEAD..origin/${currentBranch}`, { cwd: ROOT_DIR }).toString().trim();
261
+ const behindCount = (0, child_process_1.execSync)(`git rev-list --count HEAD..origin/${currentBranch}`, { cwd: ROOT_DIR() }).toString().trim();
262
262
  logger_1.log.warn(`Hay ${chalk_1.default.yellow(behindCount)} commit(s) nuevos disponibles`);
263
263
  logger_1.log.dim(` Ejecuta: openfactu update`);
264
264
  }
package/dist/src/index.js CHANGED
@@ -8,17 +8,21 @@ const tenant_1 = require("./commands/tenant");
8
8
  const plugin_1 = require("./commands/plugin");
9
9
  const setup_1 = require("./commands/setup");
10
10
  const update_1 = require("./commands/update");
11
+ const install_1 = require("./commands/install");
12
+ const deploy_1 = require("./commands/deploy");
11
13
  function createCLI() {
12
14
  const program = new commander_1.Command();
13
15
  program
14
16
  .name('openfactu')
15
17
  .description('CLI para gestionar OpenFactu')
16
- .version('0.1.0');
18
+ .version('0.0.4');
17
19
  (0, version_1.registerVersionCommand)(program);
18
20
  (0, migrate_1.registerMigrateCommand)(program);
19
21
  (0, tenant_1.registerTenantCommand)(program);
20
22
  (0, plugin_1.registerPluginCommand)(program);
21
23
  (0, setup_1.registerSetupCommand)(program);
22
24
  (0, update_1.registerUpdateCommand)(program);
25
+ (0, install_1.registerInstallCommand)(program);
26
+ (0, deploy_1.registerDeployCommand)(program);
23
27
  return program;
24
28
  }
@@ -1,6 +1,6 @@
1
1
  import { Pool } from 'pg';
2
2
  import { eq, sql } from 'drizzle-orm';
3
- declare const schema: any;
3
+ declare function getSchema(): any;
4
4
  export declare function getPublicDb(): any;
5
5
  export declare function getTenantDb(schemaName: string): import("drizzle-orm/node-postgres").NodePgDatabase<any> & {
6
6
  $client: Pool;
@@ -9,4 +9,5 @@ export declare function getAllTenants(): Promise<any>;
9
9
  export declare function getTenantByName(name: string): Promise<any>;
10
10
  export declare function testConnection(): Promise<boolean>;
11
11
  export declare function disconnect(): Promise<void>;
12
- export { schema, sql, eq };
12
+ export { sql, eq };
13
+ export { getSchema as schema };
@@ -1,49 +1,59 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.eq = exports.sql = exports.schema = void 0;
6
+ exports.eq = exports.sql = void 0;
4
7
  exports.getPublicDb = getPublicDb;
5
8
  exports.getTenantDb = getTenantDb;
6
9
  exports.getAllTenants = getAllTenants;
7
10
  exports.getTenantByName = getTenantByName;
8
11
  exports.testConnection = testConnection;
9
12
  exports.disconnect = disconnect;
13
+ exports.schema = getSchema;
10
14
  const node_postgres_1 = require("drizzle-orm/node-postgres");
11
15
  const pg_1 = require("pg");
12
16
  const drizzle_orm_1 = require("drizzle-orm");
13
17
  Object.defineProperty(exports, "eq", { enumerable: true, get: function () { return drizzle_orm_1.eq; } });
14
18
  Object.defineProperty(exports, "sql", { enumerable: true, get: function () { return drizzle_orm_1.sql; } });
19
+ const path_1 = __importDefault(require("path"));
15
20
  const config_1 = require("./config");
16
- // Importar schema de forma dinámica según la ruta del proyecto
17
21
  const paths_1 = require("./paths");
18
- const schemaPath = require('path').join((0, paths_1.getServerSrcDir)(), 'db/schema');
19
- const schema = require(schemaPath);
20
- exports.schema = schema;
21
22
  let pool = null;
22
23
  let db = null;
24
+ let _schema = null;
25
+ function getSchema() {
26
+ if (!_schema) {
27
+ _schema = require(path_1.default.join((0, paths_1.getServerSrcDir)(), 'db/schema'));
28
+ }
29
+ return _schema;
30
+ }
23
31
  function getPublicDb() {
24
32
  if (db)
25
33
  return db;
26
34
  const config = (0, config_1.loadConfig)();
27
35
  pool = new pg_1.Pool({ connectionString: config.databaseUrl });
28
- db = (0, node_postgres_1.drizzle)(pool, { schema });
36
+ db = (0, node_postgres_1.drizzle)(pool, { schema: getSchema() });
29
37
  return db;
30
38
  }
31
39
  function getTenantDb(schemaName) {
32
40
  const config = (0, config_1.loadConfig)();
33
41
  const url = `${config.databaseUrl}${config.databaseUrl.includes('?') ? '&' : '?'}options=-csearch_path%3D${schemaName}%2Cpublic`;
34
42
  const tenantPool = new pg_1.Pool({ connectionString: url });
35
- return (0, node_postgres_1.drizzle)(tenantPool, { schema });
43
+ return (0, node_postgres_1.drizzle)(tenantPool, { schema: getSchema() });
36
44
  }
37
45
  async function getAllTenants() {
46
+ const s = getSchema();
38
47
  const publicDb = getPublicDb();
39
- return publicDb.select().from(schema.tenants);
48
+ return publicDb.select().from(s.tenants);
40
49
  }
41
50
  async function getTenantByName(name) {
51
+ const s = getSchema();
42
52
  const publicDb = getPublicDb();
43
53
  const [tenant] = await publicDb
44
54
  .select()
45
- .from(schema.tenants)
46
- .where((0, drizzle_orm_1.eq)(schema.tenants.name, name));
55
+ .from(s.tenants)
56
+ .where((0, drizzle_orm_1.eq)(s.tenants.name, name));
47
57
  return tenant || null;
48
58
  }
49
59
  async function testConnection() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openfactu/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "CLI para gestionar OpenFactu: migraciones, tenants, plugins y setup",
5
5
  "main": "./dist/src/index.js",
6
6
  "types": "./dist/src/index.d.ts",