@openfactu/cli 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +48 -59
- package/dist/src/commands/deploy.js +130 -2
- package/dist/src/commands/install.js +3 -3
- package/dist/src/commands/plugin.js +344 -0
- package/dist/src/utils/db.js +21 -1
- package/package.json +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Angel Acedo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @openfactu/cli
|
|
2
2
|
|
|
3
|
-
CLI oficial para instalar, gestionar y desplegar [OpenFactu](https://github.com/
|
|
3
|
+
CLI oficial para instalar, gestionar y desplegar [OpenFactu](https://github.com/OpenFactu/platform) — ERP de facturacion open source.
|
|
4
4
|
|
|
5
5
|
## Instalacion
|
|
6
6
|
|
|
@@ -11,17 +11,9 @@ npm i -g @openfactu/cli
|
|
|
11
11
|
## Inicio rapido
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
#
|
|
15
|
-
openfactu
|
|
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
|
|
14
|
+
openfactu install # Descarga e instala OpenFactu
|
|
15
|
+
openfactu deploy # Configura acceso externo
|
|
16
|
+
openfactu setup # Configuracion inicial de BD
|
|
25
17
|
```
|
|
26
18
|
|
|
27
19
|
## Comandos
|
|
@@ -30,81 +22,76 @@ openfactu deploy
|
|
|
30
22
|
|
|
31
23
|
| Comando | Descripcion |
|
|
32
24
|
|---------|-------------|
|
|
33
|
-
| `openfactu install [dir]` | Descarga
|
|
34
|
-
| `openfactu update` | Actualiza
|
|
35
|
-
| `openfactu update:check` | Comprueba si hay versiones nuevas
|
|
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
|
-
```
|
|
25
|
+
| `openfactu install [dir]` | Descarga desde releases de GitHub con Docker |
|
|
26
|
+
| `openfactu update` | Actualiza sin perder datos |
|
|
27
|
+
| `openfactu update:check` | Comprueba si hay versiones nuevas |
|
|
47
28
|
|
|
48
29
|
### Despliegue
|
|
49
30
|
|
|
50
31
|
| Comando | Descripcion |
|
|
51
32
|
|---------|-------------|
|
|
52
|
-
| `openfactu deploy` | Wizard para configurar acceso externo
|
|
53
|
-
| `openfactu deploy:status` |
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
openfactu
|
|
58
|
-
|
|
59
|
-
# Ver estado de los servicios
|
|
60
|
-
openfactu deploy:status
|
|
61
|
-
```
|
|
33
|
+
| `openfactu deploy` | Wizard para configurar acceso externo (LAN/internet) |
|
|
34
|
+
| `openfactu deploy:status` | Estado de los contenedores Docker |
|
|
35
|
+
| `openfactu rebuild` | Reconstruye y reinicia contenedores |
|
|
36
|
+
| `openfactu logs` | Muestra logs de los servicios |
|
|
37
|
+
| `openfactu stop` | Para todos los servicios |
|
|
38
|
+
| `openfactu restart` | Reinicia sin rebuild |
|
|
62
39
|
|
|
63
40
|
### Base de datos
|
|
64
41
|
|
|
65
42
|
| Comando | Descripcion |
|
|
66
43
|
|---------|-------------|
|
|
67
|
-
| `openfactu setup` | Configuracion inicial:
|
|
68
|
-
| `openfactu migrate` | Ejecuta migraciones pendientes
|
|
69
|
-
| `openfactu migrate:status` |
|
|
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
|
-
```
|
|
44
|
+
| `openfactu setup` | Configuracion inicial: BD, admin, primer tenant |
|
|
45
|
+
| `openfactu migrate` | Ejecuta migraciones pendientes |
|
|
46
|
+
| `openfactu migrate:status` | Estado de migraciones por tenant |
|
|
78
47
|
|
|
79
48
|
### Tenants (empresas)
|
|
80
49
|
|
|
81
50
|
| Comando | Descripcion |
|
|
82
51
|
|---------|-------------|
|
|
83
|
-
| `openfactu tenant list` | Lista
|
|
84
|
-
| `openfactu tenant create
|
|
85
|
-
| `openfactu tenant sync
|
|
52
|
+
| `openfactu tenant list` | Lista empresas |
|
|
53
|
+
| `openfactu tenant create` | Crea una empresa nueva |
|
|
54
|
+
| `openfactu tenant sync` | Sincroniza migraciones |
|
|
86
55
|
|
|
87
56
|
### Plugins
|
|
88
57
|
|
|
89
58
|
| Comando | Descripcion |
|
|
90
59
|
|---------|-------------|
|
|
91
|
-
| `openfactu plugin list` | Lista plugins instalados con
|
|
60
|
+
| `openfactu plugin list` | Lista plugins instalados con estado por tenant |
|
|
61
|
+
| `openfactu plugin search` | Busca en el marketplace (interactivo) |
|
|
62
|
+
| `openfactu plugin install <nombre>` | Descarga e instala del marketplace |
|
|
63
|
+
| `openfactu plugin update [nombre]` | Actualiza uno o todos |
|
|
64
|
+
| `openfactu plugin remove <nombre>` | Elimina un plugin |
|
|
65
|
+
| `openfactu plugin link [dir]` | Enlaza un plugin externo (symlink) |
|
|
66
|
+
| `openfactu plugin unlink <nombre>` | Quita el enlace |
|
|
67
|
+
| `openfactu plugin push [dir]` | Sube un plugin a un servidor remoto |
|
|
68
|
+
| `openfactu plugin watch [dir]` | Auto-sync al guardar (desarrollo remoto) |
|
|
69
|
+
| `openfactu plugin dev [nombre]` | Servidor en modo desarrollo con hot reload |
|
|
92
70
|
|
|
93
71
|
### Otros
|
|
94
72
|
|
|
95
73
|
| Comando | Descripcion |
|
|
96
74
|
|---------|-------------|
|
|
97
|
-
| `openfactu version` |
|
|
75
|
+
| `openfactu version` | Versiones del sistema |
|
|
98
76
|
|
|
99
|
-
##
|
|
77
|
+
## Desarrollo remoto de plugins
|
|
100
78
|
|
|
101
|
-
|
|
79
|
+
```bash
|
|
80
|
+
# Desde otro ordenador, sube tu plugin automaticamente al guardar
|
|
81
|
+
openfactu plugin watch \
|
|
82
|
+
--server http://mi-servidor:3000 \
|
|
83
|
+
--client-id ofk_... \
|
|
84
|
+
--client-secret ofs_...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Las dev keys se generan desde la UI del ERP: Plugins > Desarrollo > Generar API Key.
|
|
88
|
+
|
|
89
|
+
## Uso desde cualquier directorio
|
|
102
90
|
|
|
103
91
|
```bash
|
|
104
|
-
# Opcion 1: flag --path
|
|
105
92
|
openfactu --path /ruta/a/openfactu migrate
|
|
106
93
|
|
|
107
|
-
#
|
|
94
|
+
# o con variable de entorno
|
|
108
95
|
export OPENFACTU_HOME=/ruta/a/openfactu
|
|
109
96
|
openfactu migrate
|
|
110
97
|
```
|
|
@@ -113,9 +100,11 @@ openfactu migrate
|
|
|
113
100
|
|
|
114
101
|
- Node.js >= 18
|
|
115
102
|
- Docker Desktop (para instalar y desplegar)
|
|
116
|
-
- Git
|
|
103
|
+
- Git
|
|
117
104
|
|
|
118
105
|
## Links
|
|
119
106
|
|
|
120
|
-
- [GitHub](https://github.com/
|
|
121
|
-
- [
|
|
107
|
+
- [GitHub](https://github.com/OpenFactu/platform)
|
|
108
|
+
- [Documentacion](https://openfactuerp.org)
|
|
109
|
+
- [Marketplace](https://openfactuerp.org/marketplace/)
|
|
110
|
+
- [Reportar problema](https://github.com/OpenFactu/platform/issues)
|
|
@@ -82,6 +82,7 @@ function registerDeployCommand(program) {
|
|
|
82
82
|
let host = 'localhost';
|
|
83
83
|
let serverPort = '3000';
|
|
84
84
|
let webPort = '8080';
|
|
85
|
+
let dbPort = '5432';
|
|
85
86
|
let useSSL = false;
|
|
86
87
|
if (mode === 'lan') {
|
|
87
88
|
const ipChoices = localIPs.map((ip) => ({ name: ip, value: ip }));
|
|
@@ -128,7 +129,7 @@ function registerDeployCommand(program) {
|
|
|
128
129
|
{
|
|
129
130
|
type: 'confirm',
|
|
130
131
|
name: 'ports',
|
|
131
|
-
message: `¿Usar puertos por defecto? (web: 8080, api: 3000)`,
|
|
132
|
+
message: `¿Usar puertos por defecto? (web: 8080, api: 3000, db: 5432)`,
|
|
132
133
|
default: true,
|
|
133
134
|
},
|
|
134
135
|
]);
|
|
@@ -136,9 +137,11 @@ function registerDeployCommand(program) {
|
|
|
136
137
|
const answers = await inquirer_1.default.prompt([
|
|
137
138
|
{ type: 'input', name: 'webPort', message: 'Puerto web:', default: '8080' },
|
|
138
139
|
{ type: 'input', name: 'serverPort', message: 'Puerto API:', default: '3000' },
|
|
140
|
+
{ type: 'input', name: 'dbPort', message: 'Puerto BD (host):', default: '5432' },
|
|
139
141
|
]);
|
|
140
142
|
webPort = answers.webPort;
|
|
141
143
|
serverPort = answers.serverPort;
|
|
144
|
+
dbPort = answers.dbPort;
|
|
142
145
|
}
|
|
143
146
|
// Password de BD
|
|
144
147
|
const { dbPassword } = await inquirer_1.default.prompt([
|
|
@@ -161,6 +164,7 @@ function registerDeployCommand(program) {
|
|
|
161
164
|
logger_1.log.title(' Resumen de configuración');
|
|
162
165
|
logger_1.log.info(`Web: ${chalk_1.default.cyan(webUrl)}`);
|
|
163
166
|
logger_1.log.info(`API: ${chalk_1.default.cyan(apiUrl)}`);
|
|
167
|
+
logger_1.log.info(`BD Puerto: ${chalk_1.default.cyan(dbPort)} ${chalk_1.default.dim('(host)')}`);
|
|
164
168
|
logger_1.log.info(`BD Password: ${chalk_1.default.dim(dbPassword === 'openfactu_pass' ? '(default)' : '****')}`);
|
|
165
169
|
logger_1.log.info(`SSL: ${useSSL ? chalk_1.default.green('Si') : chalk_1.default.dim('No')}`);
|
|
166
170
|
logger_1.log.blank();
|
|
@@ -176,7 +180,7 @@ function registerDeployCommand(program) {
|
|
|
176
180
|
const env = readEnv(envPath);
|
|
177
181
|
env.SERVER_PORT = serverPort;
|
|
178
182
|
env.WEB_PORT = webPort;
|
|
179
|
-
env.DB_PORT =
|
|
183
|
+
env.DB_PORT = dbPort;
|
|
180
184
|
env.POSTGRES_USER = env.POSTGRES_USER || 'openfactu';
|
|
181
185
|
env.POSTGRES_PASSWORD = dbPassword;
|
|
182
186
|
env.POSTGRES_DB = env.POSTGRES_DB || 'openfactudb';
|
|
@@ -365,4 +369,128 @@ networks:
|
|
|
365
369
|
logger_1.log.dim(' ' + err.message);
|
|
366
370
|
}
|
|
367
371
|
});
|
|
372
|
+
// ── openfactu rebuild ──
|
|
373
|
+
program
|
|
374
|
+
.command('rebuild')
|
|
375
|
+
.description('Reconstruye y reinicia los contenedores Docker')
|
|
376
|
+
.option('--service <name>', 'Reconstruir solo un servicio (web, server, db)')
|
|
377
|
+
.option('--no-cache', 'Construir sin cache de Docker')
|
|
378
|
+
.action(async (opts) => {
|
|
379
|
+
try {
|
|
380
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
381
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
382
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
383
|
+
const service = opts.service || '';
|
|
384
|
+
const noCache = opts.cache === false ? ' --no-cache' : '';
|
|
385
|
+
logger_1.log.info(`Usando: ${chalk_1.default.dim(composeFile)}`);
|
|
386
|
+
logger_1.log.blank();
|
|
387
|
+
const buildSpinner = (0, ora_1.default)(`Construyendo${service ? ' ' + service : ' todos los servicios'}...`).start();
|
|
388
|
+
try {
|
|
389
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} build${noCache} ${service}`, {
|
|
390
|
+
cwd: root,
|
|
391
|
+
stdio: 'pipe',
|
|
392
|
+
timeout: 600000,
|
|
393
|
+
});
|
|
394
|
+
buildSpinner.succeed('Build completado');
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
buildSpinner.fail('Error en el build');
|
|
398
|
+
// Mostrar output del error
|
|
399
|
+
const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
|
|
400
|
+
const errorLines = output.split('\n').filter((l) => l.includes('error') || l.includes('Error') || l.includes('>>>'));
|
|
401
|
+
if (errorLines.length > 0) {
|
|
402
|
+
logger_1.log.blank();
|
|
403
|
+
for (const line of errorLines.slice(0, 10)) {
|
|
404
|
+
logger_1.log.error(line.trim());
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const upSpinner = (0, ora_1.default)('Reiniciando servicios...').start();
|
|
410
|
+
try {
|
|
411
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} up -d ${service}`, {
|
|
412
|
+
cwd: root,
|
|
413
|
+
stdio: 'pipe',
|
|
414
|
+
timeout: 60000,
|
|
415
|
+
});
|
|
416
|
+
upSpinner.succeed('Servicios levantados');
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
upSpinner.fail('Error al levantar: ' + err.message);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
logger_1.log.blank();
|
|
423
|
+
logger_1.log.success('Rebuild completado');
|
|
424
|
+
// Mostrar URLs
|
|
425
|
+
const envPath = path_1.default.join(root, '.env');
|
|
426
|
+
const env = readEnv(envPath);
|
|
427
|
+
const host = env.HOST || 'localhost';
|
|
428
|
+
const webPort = env.WEB_PORT || '8080';
|
|
429
|
+
const serverPort = env.SERVER_PORT || '3000';
|
|
430
|
+
logger_1.log.info(`Web: ${chalk_1.default.cyan(`http://${host}:${webPort}`)}`);
|
|
431
|
+
logger_1.log.info(`API: ${chalk_1.default.cyan(`http://${host}:${serverPort}`)}`);
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
logger_1.log.error(err.message);
|
|
435
|
+
process.exitCode = 1;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
// ── openfactu logs ──
|
|
439
|
+
program
|
|
440
|
+
.command('logs')
|
|
441
|
+
.description('Muestra los logs de los servicios Docker')
|
|
442
|
+
.option('--service <name>', 'Logs de un servicio especifico (web, server, db)')
|
|
443
|
+
.option('-n, --lines <number>', 'Numero de lineas', '50')
|
|
444
|
+
.action(async (opts) => {
|
|
445
|
+
try {
|
|
446
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
447
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
448
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
449
|
+
const service = opts.service || '';
|
|
450
|
+
const lines = opts.lines || '50';
|
|
451
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} logs --tail ${lines} ${service}`, {
|
|
452
|
+
cwd: root,
|
|
453
|
+
stdio: 'inherit',
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
catch (err) {
|
|
457
|
+
logger_1.log.error(err.message);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
// ── openfactu stop ──
|
|
461
|
+
program
|
|
462
|
+
.command('stop')
|
|
463
|
+
.description('Para todos los servicios Docker')
|
|
464
|
+
.action(async () => {
|
|
465
|
+
try {
|
|
466
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
467
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
468
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
469
|
+
const spinner = (0, ora_1.default)('Parando servicios...').start();
|
|
470
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} down`, { cwd: root, stdio: 'pipe' });
|
|
471
|
+
spinner.succeed('Servicios parados');
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
logger_1.log.error(err.message);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
// ── openfactu restart ──
|
|
478
|
+
program
|
|
479
|
+
.command('restart')
|
|
480
|
+
.description('Reinicia los servicios Docker (sin rebuild)')
|
|
481
|
+
.option('--service <name>', 'Reiniciar solo un servicio')
|
|
482
|
+
.action(async (opts) => {
|
|
483
|
+
try {
|
|
484
|
+
const root = (0, paths_1.getProjectRoot)();
|
|
485
|
+
const prodCompose = path_1.default.join(root, 'docker-compose.prod.yml');
|
|
486
|
+
const composeFile = fs_1.default.existsSync(prodCompose) ? 'docker-compose.prod.yml' : 'docker-compose.yml';
|
|
487
|
+
const service = opts.service || '';
|
|
488
|
+
const spinner = (0, ora_1.default)('Reiniciando...').start();
|
|
489
|
+
(0, child_process_1.execSync)(`docker compose -f ${composeFile} restart ${service}`, { cwd: root, stdio: 'pipe' });
|
|
490
|
+
spinner.succeed('Servicios reiniciados');
|
|
491
|
+
}
|
|
492
|
+
catch (err) {
|
|
493
|
+
logger_1.log.error(err.message);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
368
496
|
}
|
|
@@ -13,9 +13,9 @@ const os_1 = __importDefault(require("os"));
|
|
|
13
13
|
const fs_1 = __importDefault(require("fs"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const logger_1 = require("../utils/logger");
|
|
16
|
-
const REPO_URL = 'https://github.com/
|
|
17
|
-
const GITHUB_OWNER = '
|
|
18
|
-
const GITHUB_REPO = '
|
|
16
|
+
const REPO_URL = 'https://github.com/OpenFactu/platform.git';
|
|
17
|
+
const GITHUB_OWNER = 'OpenFactu';
|
|
18
|
+
const GITHUB_REPO = 'platform';
|
|
19
19
|
function fetchJSON(url) {
|
|
20
20
|
return new Promise((resolve, reject) => {
|
|
21
21
|
https_1.default.get(url, { headers: { 'User-Agent': 'openfactu-cli' } }, (res) => {
|
|
@@ -345,4 +345,348 @@ function registerPluginCommand(program) {
|
|
|
345
345
|
spinner.fail('Error: ' + err.message);
|
|
346
346
|
}
|
|
347
347
|
});
|
|
348
|
+
// ── openfactu plugin link ──
|
|
349
|
+
plugin
|
|
350
|
+
.command('link [dir]')
|
|
351
|
+
.description('Enlaza un plugin externo a la carpeta de plugins de OpenFactu')
|
|
352
|
+
.action(async (dir) => {
|
|
353
|
+
const pluginsDir = (0, paths_1.getPluginsDir)();
|
|
354
|
+
const sourcePath = path_1.default.resolve(dir || process.cwd());
|
|
355
|
+
// Verificar que el directorio existe
|
|
356
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
357
|
+
logger_1.log.error(`Directorio no encontrado: ${sourcePath}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Verificar que tiene index.ts o index.js
|
|
361
|
+
const hasIndex = fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.ts')) || fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.js'));
|
|
362
|
+
if (!hasIndex) {
|
|
363
|
+
logger_1.log.warn('No se encontro index.ts ni index.js en el directorio');
|
|
364
|
+
logger_1.log.dim(' Asegurate de que es un plugin valido de OpenFactu');
|
|
365
|
+
}
|
|
366
|
+
const pluginName = path_1.default.basename(sourcePath);
|
|
367
|
+
const linkPath = path_1.default.join(pluginsDir, pluginName);
|
|
368
|
+
// Verificar si ya existe
|
|
369
|
+
if (fs_1.default.existsSync(linkPath)) {
|
|
370
|
+
const stat = fs_1.default.lstatSync(linkPath);
|
|
371
|
+
if (stat.isSymbolicLink()) {
|
|
372
|
+
logger_1.log.warn(`El enlace "${pluginName}" ya existe → ${fs_1.default.readlinkSync(linkPath)}`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
logger_1.log.error(`Ya existe un plugin "${pluginName}" (no es un symlink). Eliminalo primero.`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
// Crear directorio de plugins si no existe
|
|
379
|
+
if (!fs_1.default.existsSync(pluginsDir)) {
|
|
380
|
+
fs_1.default.mkdirSync(pluginsDir, { recursive: true });
|
|
381
|
+
}
|
|
382
|
+
// Crear symlink
|
|
383
|
+
try {
|
|
384
|
+
fs_1.default.symlinkSync(sourcePath, linkPath, 'dir');
|
|
385
|
+
logger_1.log.success(`Plugin enlazado: ${chalk_1.default.bold(pluginName)}`);
|
|
386
|
+
logger_1.log.dim(` ${sourcePath} → ${linkPath}`);
|
|
387
|
+
logger_1.log.blank();
|
|
388
|
+
logger_1.log.dim(' Ahora puedes desarrollar el plugin desde su carpeta original.');
|
|
389
|
+
logger_1.log.dim(' Los cambios se detectan automaticamente con el watcher.');
|
|
390
|
+
logger_1.log.dim(' Para arrancar: openfactu plugin dev ' + pluginName);
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
logger_1.log.error('Error al crear enlace: ' + err.message);
|
|
394
|
+
logger_1.log.dim(' En Windows ejecuta como administrador');
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
// ── openfactu plugin unlink ──
|
|
398
|
+
plugin
|
|
399
|
+
.command('unlink <name>')
|
|
400
|
+
.description('Elimina el enlace de un plugin externo')
|
|
401
|
+
.action(async (name) => {
|
|
402
|
+
const linkPath = path_1.default.join((0, paths_1.getPluginsDir)(), name);
|
|
403
|
+
if (!fs_1.default.existsSync(linkPath)) {
|
|
404
|
+
logger_1.log.error(`Plugin "${name}" no encontrado`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const stat = fs_1.default.lstatSync(linkPath);
|
|
408
|
+
if (!stat.isSymbolicLink()) {
|
|
409
|
+
logger_1.log.error(`"${name}" no es un enlace simbolico. Usa 'plugin remove' para eliminarlo.`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
fs_1.default.unlinkSync(linkPath);
|
|
413
|
+
logger_1.log.success(`Enlace "${name}" eliminado`);
|
|
414
|
+
logger_1.log.dim(' El directorio original no se ha tocado.');
|
|
415
|
+
});
|
|
416
|
+
// ── openfactu plugin push ──
|
|
417
|
+
plugin
|
|
418
|
+
.command('push [dir]')
|
|
419
|
+
.description('Sube un plugin a un servidor OpenFactu remoto')
|
|
420
|
+
.requiredOption('-s, --server <url>', 'URL del servidor (ej: http://192.168.1.100:3000)')
|
|
421
|
+
.option('-t, --token <token>', 'Token JWT de admin')
|
|
422
|
+
.option('--client-id <id>', 'Client ID de la dev key (ej: ofk_...)')
|
|
423
|
+
.option('--client-secret <secret>', 'Client Secret de la dev key (ej: ofs_...)')
|
|
424
|
+
.action(async (dir, opts) => {
|
|
425
|
+
// Validar autenticacion
|
|
426
|
+
if (!opts.token && (!opts.clientId || !opts.clientSecret)) {
|
|
427
|
+
logger_1.log.error('Necesitas autenticarte con --token o --client-id + --client-secret');
|
|
428
|
+
logger_1.log.dim(' Genera una dev key desde la UI: Plugins → Desarrollo → Generar API Key');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const sourcePath = path_1.default.resolve(dir || process.cwd());
|
|
432
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
433
|
+
logger_1.log.error(`Directorio no encontrado: ${sourcePath}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const pluginName = path_1.default.basename(sourcePath);
|
|
437
|
+
const hasIndex = fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.ts')) || fs_1.default.existsSync(path_1.default.join(sourcePath, 'index.js'));
|
|
438
|
+
if (!hasIndex) {
|
|
439
|
+
logger_1.log.warn('No se encontro index.ts ni index.js. Seguro que es un plugin?');
|
|
440
|
+
}
|
|
441
|
+
logger_1.log.info(`Plugin: ${chalk_1.default.bold(pluginName)}`);
|
|
442
|
+
logger_1.log.info(`Servidor: ${chalk_1.default.dim(opts.server)}`);
|
|
443
|
+
logger_1.log.blank();
|
|
444
|
+
// Recoger todos los archivos del plugin
|
|
445
|
+
const spinner = (0, ora_1.default)('Leyendo archivos...').start();
|
|
446
|
+
const files = [];
|
|
447
|
+
function readDir(dirPath, basePath) {
|
|
448
|
+
const entries = fs_1.default.readdirSync(dirPath);
|
|
449
|
+
for (const entry of entries) {
|
|
450
|
+
if (entry === 'node_modules' || entry === '.git' || entry === 'dist')
|
|
451
|
+
continue;
|
|
452
|
+
const fullPath = path_1.default.join(dirPath, entry);
|
|
453
|
+
const relativePath = path_1.default.relative(basePath, fullPath);
|
|
454
|
+
const stat = fs_1.default.statSync(fullPath);
|
|
455
|
+
if (stat.isDirectory()) {
|
|
456
|
+
readDir(fullPath, basePath);
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
files.push({
|
|
460
|
+
path: relativePath,
|
|
461
|
+
content: fs_1.default.readFileSync(fullPath).toString('base64'),
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
readDir(sourcePath, sourcePath);
|
|
467
|
+
spinner.succeed(`${files.length} archivo(s) encontrado(s)`);
|
|
468
|
+
// Enviar al servidor
|
|
469
|
+
const pushSpinner = (0, ora_1.default)('Subiendo al servidor...').start();
|
|
470
|
+
try {
|
|
471
|
+
const url = `${opts.server}/api/plugins/${pluginName}/push`;
|
|
472
|
+
const response = await new Promise((resolve, reject) => {
|
|
473
|
+
const data = JSON.stringify({ files });
|
|
474
|
+
const urlObj = new (require('url').URL)(url);
|
|
475
|
+
const http = urlObj.protocol === 'https:' ? require('https') : require('http');
|
|
476
|
+
const req = http.request({
|
|
477
|
+
hostname: urlObj.hostname,
|
|
478
|
+
port: urlObj.port,
|
|
479
|
+
path: urlObj.pathname,
|
|
480
|
+
method: 'POST',
|
|
481
|
+
headers: {
|
|
482
|
+
'Content-Type': 'application/json',
|
|
483
|
+
'Content-Length': Buffer.byteLength(data),
|
|
484
|
+
...(opts.token
|
|
485
|
+
? { 'Authorization': `Bearer ${opts.token}` }
|
|
486
|
+
: { 'X-Client-Id': opts.clientId, 'X-Client-Secret': opts.clientSecret }),
|
|
487
|
+
},
|
|
488
|
+
}, (res) => {
|
|
489
|
+
let body = '';
|
|
490
|
+
res.on('data', (chunk) => body += chunk);
|
|
491
|
+
res.on('end', () => {
|
|
492
|
+
try {
|
|
493
|
+
resolve({ status: res.statusCode, body: JSON.parse(body) });
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
resolve({ status: res.statusCode, body });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
req.on('error', reject);
|
|
501
|
+
req.write(data);
|
|
502
|
+
req.end();
|
|
503
|
+
});
|
|
504
|
+
if (response.status === 200 && response.body?.success) {
|
|
505
|
+
pushSpinner.succeed('Plugin subido correctamente');
|
|
506
|
+
if (response.body.reloaded) {
|
|
507
|
+
logger_1.log.success('Plugin recargado automaticamente en el servidor');
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
logger_1.log.info('Reinicia el servidor remoto para cargar el plugin');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
else if (response.status === 403 || response.status === 401) {
|
|
514
|
+
pushSpinner.fail('No autorizado. Verifica el token de admin.');
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
pushSpinner.fail(`Error: ${response.body?.error || response.status}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
pushSpinner.fail('Error de conexion: ' + err.message);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
// ── openfactu plugin watch ──
|
|
525
|
+
plugin
|
|
526
|
+
.command('watch [dir]')
|
|
527
|
+
.description('Vigila cambios en un plugin y los sube automaticamente al servidor')
|
|
528
|
+
.requiredOption('-s, --server <url>', 'URL del servidor')
|
|
529
|
+
.option('-t, --token <token>', 'Token JWT de admin')
|
|
530
|
+
.option('--client-id <id>', 'Client ID de la dev key')
|
|
531
|
+
.option('--client-secret <secret>', 'Client Secret de la dev key')
|
|
532
|
+
.action(async (dir, opts) => {
|
|
533
|
+
if (!opts.token && (!opts.clientId || !opts.clientSecret)) {
|
|
534
|
+
logger_1.log.error('Necesitas --token o --client-id + --client-secret');
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
const sourcePath = path_1.default.resolve(dir || process.cwd());
|
|
538
|
+
if (!fs_1.default.existsSync(sourcePath)) {
|
|
539
|
+
logger_1.log.error(`Directorio no encontrado: ${sourcePath}`);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const pluginName = path_1.default.basename(sourcePath);
|
|
543
|
+
logger_1.log.info(`Vigilando: ${chalk_1.default.bold(pluginName)}`);
|
|
544
|
+
logger_1.log.info(`Servidor: ${chalk_1.default.dim(opts.server)}`);
|
|
545
|
+
logger_1.log.blank();
|
|
546
|
+
logger_1.log.dim(' Guardando cualquier archivo se subira automaticamente al servidor.');
|
|
547
|
+
logger_1.log.dim(' Ctrl+C para parar.');
|
|
548
|
+
logger_1.log.blank();
|
|
549
|
+
const authHeaders = opts.token
|
|
550
|
+
? { 'Authorization': `Bearer ${opts.token}` }
|
|
551
|
+
: { 'X-Client-Id': opts.clientId, 'X-Client-Secret': opts.clientSecret };
|
|
552
|
+
// Funcion para subir un archivo al servidor
|
|
553
|
+
const pushFile = async (filePath) => {
|
|
554
|
+
// Ignorar archivos temporales del editor
|
|
555
|
+
if (filePath.includes('.tmp') || filePath.endsWith('~') || filePath.includes('.swp') || filePath.includes('.swx'))
|
|
556
|
+
return;
|
|
557
|
+
if (!fs_1.default.existsSync(filePath))
|
|
558
|
+
return;
|
|
559
|
+
const relativePath = path_1.default.relative(sourcePath, filePath);
|
|
560
|
+
const content = fs_1.default.readFileSync(filePath).toString('base64');
|
|
561
|
+
try {
|
|
562
|
+
const data = JSON.stringify({ files: [{ path: relativePath, content }] });
|
|
563
|
+
const urlObj = new (require('url').URL)(`${opts.server}/api/plugins/${pluginName}/push`);
|
|
564
|
+
const http = urlObj.protocol === 'https:' ? require('https') : require('http');
|
|
565
|
+
await new Promise((resolve, reject) => {
|
|
566
|
+
const req = http.request({
|
|
567
|
+
hostname: urlObj.hostname,
|
|
568
|
+
port: urlObj.port,
|
|
569
|
+
path: urlObj.pathname,
|
|
570
|
+
method: 'POST',
|
|
571
|
+
headers: {
|
|
572
|
+
'Content-Type': 'application/json',
|
|
573
|
+
'Content-Length': Buffer.byteLength(data),
|
|
574
|
+
...authHeaders,
|
|
575
|
+
},
|
|
576
|
+
}, (res) => {
|
|
577
|
+
let body = '';
|
|
578
|
+
res.on('data', (chunk) => body += chunk);
|
|
579
|
+
res.on('end', () => {
|
|
580
|
+
if (res.statusCode === 200)
|
|
581
|
+
resolve();
|
|
582
|
+
else
|
|
583
|
+
reject(new Error(body));
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
req.on('error', reject);
|
|
587
|
+
req.write(data);
|
|
588
|
+
req.end();
|
|
589
|
+
});
|
|
590
|
+
logger_1.log.success(`${chalk_1.default.dim(relativePath)} → subido y recargado`);
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
logger_1.log.error(`${relativePath} → ${err.message}`);
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
// Watcher
|
|
597
|
+
const chokidar = require('chokidar');
|
|
598
|
+
const debounceTimers = new Map();
|
|
599
|
+
const watcher = chokidar.watch(sourcePath, {
|
|
600
|
+
ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**'],
|
|
601
|
+
ignoreInitial: true,
|
|
602
|
+
persistent: true,
|
|
603
|
+
});
|
|
604
|
+
const handleChange = (filePath) => {
|
|
605
|
+
const existing = debounceTimers.get(filePath);
|
|
606
|
+
if (existing)
|
|
607
|
+
clearTimeout(existing);
|
|
608
|
+
debounceTimers.set(filePath, setTimeout(() => {
|
|
609
|
+
debounceTimers.delete(filePath);
|
|
610
|
+
pushFile(filePath);
|
|
611
|
+
}, 300));
|
|
612
|
+
};
|
|
613
|
+
watcher.on('change', handleChange);
|
|
614
|
+
watcher.on('add', handleChange);
|
|
615
|
+
// Mantener el proceso vivo
|
|
616
|
+
process.on('SIGINT', () => {
|
|
617
|
+
watcher.close();
|
|
618
|
+
logger_1.log.blank();
|
|
619
|
+
logger_1.log.info('Watch detenido');
|
|
620
|
+
process.exit(0);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
// ── openfactu plugin dev ──
|
|
624
|
+
plugin
|
|
625
|
+
.command('dev [name]')
|
|
626
|
+
.description('Arranca el servidor en modo desarrollo para plugins')
|
|
627
|
+
.action(async (name) => {
|
|
628
|
+
const pluginsDir = (0, paths_1.getPluginsDir)();
|
|
629
|
+
if (name) {
|
|
630
|
+
const pluginPath = path_1.default.join(pluginsDir, name);
|
|
631
|
+
if (!fs_1.default.existsSync(pluginPath)) {
|
|
632
|
+
logger_1.log.error(`Plugin "${name}" no encontrado en ${pluginsDir}`);
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
logger_1.log.info(`Modo desarrollo para plugin: ${chalk_1.default.bold(name)}`);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
logger_1.log.info('Modo desarrollo para todos los plugins');
|
|
639
|
+
}
|
|
640
|
+
logger_1.log.blank();
|
|
641
|
+
logger_1.log.dim(' El servidor recargara los plugins automaticamente al detectar cambios.');
|
|
642
|
+
logger_1.log.dim(' Los componentes UI se actualizan en el browser sin refrescar.');
|
|
643
|
+
logger_1.log.blank();
|
|
644
|
+
const { getProjectRoot } = require('../utils/paths');
|
|
645
|
+
const root = getProjectRoot();
|
|
646
|
+
try {
|
|
647
|
+
const child = require('child_process').spawn('npm', ['run', 'dev:server'], {
|
|
648
|
+
cwd: root,
|
|
649
|
+
env: { ...process.env, NODE_ENV: 'development' },
|
|
650
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
651
|
+
});
|
|
652
|
+
child.stdout.on('data', (data) => {
|
|
653
|
+
const line = data.toString().trim();
|
|
654
|
+
if (!line)
|
|
655
|
+
return;
|
|
656
|
+
// Resaltar logs del plugin
|
|
657
|
+
if (name && line.includes(name)) {
|
|
658
|
+
console.log(chalk_1.default.cyan(line));
|
|
659
|
+
}
|
|
660
|
+
else if (line.includes('[Plugins]') || line.includes('[PluginWatcher]') || line.includes('[DevSocket]') || line.includes('[HookManager]')) {
|
|
661
|
+
console.log(chalk_1.default.yellow(line));
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
console.log(chalk_1.default.dim(line));
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
child.stderr.on('data', (data) => {
|
|
668
|
+
const line = data.toString().trim();
|
|
669
|
+
if (!line)
|
|
670
|
+
return;
|
|
671
|
+
console.log(chalk_1.default.red(line));
|
|
672
|
+
});
|
|
673
|
+
child.on('close', (code) => {
|
|
674
|
+
logger_1.log.blank();
|
|
675
|
+
if (code === 0) {
|
|
676
|
+
logger_1.log.info('Servidor detenido');
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
logger_1.log.error(`Servidor terminado con codigo ${code}`);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
// Capturar Ctrl+C
|
|
683
|
+
process.on('SIGINT', () => {
|
|
684
|
+
child.kill('SIGINT');
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
logger_1.log.error('Error al arrancar: ' + err.message);
|
|
689
|
+
logger_1.log.dim(` Ejecuta manualmente: cd ${root} && npm run dev:server`);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
348
692
|
}
|
package/dist/src/utils/db.js
CHANGED
|
@@ -24,7 +24,27 @@ let db = null;
|
|
|
24
24
|
let _schema = null;
|
|
25
25
|
function getSchema() {
|
|
26
26
|
if (!_schema) {
|
|
27
|
-
|
|
27
|
+
// Registrar ts-node si esta disponible (para leer .ts directamente)
|
|
28
|
+
try {
|
|
29
|
+
require('ts-node').register({ transpileOnly: true, compilerOptions: { module: 'commonjs', esModuleInterop: true } });
|
|
30
|
+
}
|
|
31
|
+
catch { }
|
|
32
|
+
const srcPath = path_1.default.join((0, paths_1.getServerSrcDir)(), 'db/schema');
|
|
33
|
+
const distPath = path_1.default.join((0, paths_1.getServerSrcDir)(), '..', 'dist/db/schema');
|
|
34
|
+
try {
|
|
35
|
+
_schema = require(srcPath);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
try {
|
|
39
|
+
_schema = require(distPath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new Error(`No se pudo cargar el schema de la BD.\n` +
|
|
43
|
+
`Intentado: ${srcPath}\n` +
|
|
44
|
+
`Intentado: ${distPath}\n` +
|
|
45
|
+
`Ejecuta 'npm install' en el directorio de OpenFactu.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
28
48
|
}
|
|
29
49
|
return _schema;
|
|
30
50
|
}
|