@openfactu/cli 0.0.2

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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ // Registrar ts-node para resolver imports de .ts del server
5
+ try {
6
+ require('ts-node').register({
7
+ transpileOnly: true,
8
+ compilerOptions: { module: 'commonjs', esModuleInterop: true },
9
+ });
10
+ }
11
+ catch { }
12
+ // Opción --path para usar desde cualquier directorio
13
+ const pathIdx = process.argv.indexOf('--path');
14
+ if (pathIdx !== -1 && process.argv[pathIdx + 1]) {
15
+ const { setProjectRoot } = require('../src/utils/paths');
16
+ setProjectRoot(process.argv[pathIdx + 1]);
17
+ // Quitar --path y su valor de argv para que commander no los procese
18
+ process.argv.splice(pathIdx, 2);
19
+ }
20
+ const index_1 = require("../src/index");
21
+ const program = (0, index_1.createCLI)();
22
+ program.option('--path <dir>', 'Ruta a la instalación de OpenFactu');
23
+ program.parse(process.argv);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerMigrateCommand(program: Command): void;
@@ -0,0 +1,160 @@
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.registerMigrateCommand = registerMigrateCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const cli_table3_1 = __importDefault(require("cli-table3"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const db_1 = require("../utils/db");
13
+ const logger_1 = require("../utils/logger");
14
+ const MIGRATIONS_DIR = require('../utils/paths').getMigrationsDir();
15
+ function getMigrationFiles() {
16
+ if (!fs_1.default.existsSync(MIGRATIONS_DIR))
17
+ return [];
18
+ return fs_1.default.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort();
19
+ }
20
+ async function getAppliedMigrations(tenantDb, schemaName) {
21
+ try {
22
+ const result = await tenantDb.execute(db_1.sql.raw(`SELECT id FROM "${schemaName}"."_MigrationHistory" ORDER BY id`));
23
+ return result.rows.map((r) => r.id);
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ }
29
+ async function applyMigration(tenantDb, schemaName, file) {
30
+ const migrationId = file.replace('.sql', '');
31
+ const filePath = path_1.default.join(MIGRATIONS_DIR, file);
32
+ let rawSql = fs_1.default.readFileSync(filePath, 'utf8');
33
+ const processedSql = rawSql.replace(/{{schema}}/g, schemaName);
34
+ const statements = processedSql
35
+ .split(/;(?=(?:[^$]*\$\$[^$]*\$\$)*[^$]*$)/)
36
+ .map((s) => s.trim())
37
+ .filter((s) => s.length > 0);
38
+ for (const statement of statements) {
39
+ await tenantDb.execute(db_1.sql.raw(statement));
40
+ }
41
+ await tenantDb.execute(db_1.sql.raw(`INSERT INTO "${schemaName}"."_MigrationHistory" (id, description) VALUES ('${migrationId}', 'Aplicado desde CLI')`));
42
+ }
43
+ function registerMigrateCommand(program) {
44
+ // ── openfactu migrate ──
45
+ program
46
+ .command('migrate')
47
+ .description('Ejecuta migraciones pendientes en todos los tenants')
48
+ .option('-t, --tenant <name>', 'Migrar solo un tenant específico')
49
+ .action(async (opts) => {
50
+ const spinner = (0, ora_1.default)('Conectando a la base de datos...').start();
51
+ try {
52
+ const allFiles = getMigrationFiles();
53
+ if (allFiles.length === 0) {
54
+ spinner.fail('No se encontraron archivos de migración');
55
+ return;
56
+ }
57
+ let tenants;
58
+ if (opts.tenant) {
59
+ const tenant = await (0, db_1.getTenantByName)(opts.tenant);
60
+ if (!tenant) {
61
+ spinner.fail(`Tenant "${opts.tenant}" no encontrado`);
62
+ return;
63
+ }
64
+ tenants = [tenant];
65
+ }
66
+ else {
67
+ tenants = await (0, db_1.getAllTenants)();
68
+ }
69
+ if (tenants.length === 0) {
70
+ spinner.warn('No hay tenants registrados');
71
+ return;
72
+ }
73
+ spinner.succeed(`${tenants.length} tenant(s) encontrado(s), ${allFiles.length} migraciones disponibles`);
74
+ logger_1.log.blank();
75
+ let totalApplied = 0;
76
+ for (const tenant of tenants) {
77
+ const tenantDb = (0, db_1.getTenantDb)(tenant.schemaName);
78
+ const applied = await getAppliedMigrations(tenantDb, tenant.schemaName);
79
+ const pending = allFiles.filter((f) => !applied.includes(f.replace('.sql', '')));
80
+ if (pending.length === 0) {
81
+ logger_1.log.dim(` ${tenant.name} (${tenant.schemaName}) — al dia`);
82
+ continue;
83
+ }
84
+ logger_1.log.title(` ${tenant.name} (${tenant.schemaName})`);
85
+ for (const file of pending) {
86
+ const migSpinner = (0, ora_1.default)(` Aplicando ${file}...`).start();
87
+ try {
88
+ await applyMigration(tenantDb, tenant.schemaName, file);
89
+ migSpinner.succeed(` ${file}`);
90
+ totalApplied++;
91
+ }
92
+ catch (err) {
93
+ migSpinner.fail(` ${file}: ${err.message}`);
94
+ throw err;
95
+ }
96
+ }
97
+ }
98
+ logger_1.log.blank();
99
+ if (totalApplied > 0) {
100
+ logger_1.log.success(`${totalApplied} migración(es) aplicada(s)`);
101
+ }
102
+ else {
103
+ logger_1.log.success('Todos los tenants están al día');
104
+ }
105
+ }
106
+ catch (err) {
107
+ spinner.fail(err.message);
108
+ process.exitCode = 1;
109
+ }
110
+ finally {
111
+ await (0, db_1.disconnect)();
112
+ }
113
+ });
114
+ // ── openfactu migrate:status ──
115
+ program
116
+ .command('migrate:status')
117
+ .description('Muestra el estado de migraciones por tenant')
118
+ .action(async () => {
119
+ const spinner = (0, ora_1.default)('Leyendo estado de migraciones...').start();
120
+ try {
121
+ const allFiles = getMigrationFiles();
122
+ const tenants = await (0, db_1.getAllTenants)();
123
+ if (tenants.length === 0) {
124
+ spinner.warn('No hay tenants registrados');
125
+ return;
126
+ }
127
+ spinner.succeed(`${tenants.length} tenant(s), ${allFiles.length} migraciones`);
128
+ logger_1.log.blank();
129
+ for (const tenant of tenants) {
130
+ const tenantDb = (0, db_1.getTenantDb)(tenant.schemaName);
131
+ const applied = await getAppliedMigrations(tenantDb, tenant.schemaName);
132
+ const pending = allFiles.filter((f) => !applied.includes(f.replace('.sql', '')));
133
+ const table = new cli_table3_1.default({
134
+ head: [chalk_1.default.white('Migración'), chalk_1.default.white('Estado')],
135
+ colWidths: [45, 15],
136
+ style: { head: [], border: ['dim'] },
137
+ });
138
+ for (const file of allFiles) {
139
+ const migId = file.replace('.sql', '');
140
+ const isApplied = applied.includes(migId);
141
+ table.push([
142
+ migId,
143
+ isApplied ? chalk_1.default.green('✓ Aplicada') : chalk_1.default.yellow('Pendiente'),
144
+ ]);
145
+ }
146
+ console.log(chalk_1.default.bold(`\n ${tenant.name}`) + chalk_1.default.dim(` (${tenant.schemaName})`));
147
+ console.log(chalk_1.default.dim(` ${applied.length} aplicadas, `) +
148
+ (pending.length > 0 ? chalk_1.default.yellow(`${pending.length} pendientes`) : chalk_1.default.green('al día')));
149
+ console.log(table.toString());
150
+ }
151
+ }
152
+ catch (err) {
153
+ spinner.fail(err.message);
154
+ process.exitCode = 1;
155
+ }
156
+ finally {
157
+ await (0, db_1.disconnect)();
158
+ }
159
+ });
160
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerPluginCommand(program: Command): void;
@@ -0,0 +1,95 @@
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.registerPluginCommand = registerPluginCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const cli_table3_1 = __importDefault(require("cli-table3"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const db_1 = require("../utils/db");
13
+ const logger_1 = require("../utils/logger");
14
+ const paths_1 = require("../utils/paths");
15
+ const PLUGINS_DIR = (0, paths_1.getPluginsDir)();
16
+ function registerPluginCommand(program) {
17
+ const plugin = program
18
+ .command('plugin')
19
+ .description('Gestión de plugins');
20
+ // ── openfactu plugin list ──
21
+ plugin
22
+ .command('list')
23
+ .description('Lista plugins instalados')
24
+ .action(async () => {
25
+ const spinner = (0, ora_1.default)('Leyendo plugins...').start();
26
+ try {
27
+ // Leer plugins del filesystem
28
+ 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());
31
+ installed.push(...dirs);
32
+ }
33
+ if (installed.length === 0) {
34
+ spinner.warn('No hay plugins instalados');
35
+ return;
36
+ }
37
+ // Leer estado de activación por tenant
38
+ const publicDb = (0, db_1.getPublicDb)();
39
+ let tenantPlugins = [];
40
+ try {
41
+ tenantPlugins = await publicDb.select().from(db_1.schema.tenantPlugins);
42
+ }
43
+ catch {
44
+ // Tabla puede no existir aún
45
+ }
46
+ const tenants = await (0, db_1.getAllTenants)();
47
+ spinner.succeed(`${installed.length} plugin(s) instalado(s)`);
48
+ logger_1.log.blank();
49
+ const table = new cli_table3_1.default({
50
+ head: [
51
+ chalk_1.default.white('Plugin'),
52
+ chalk_1.default.white('Manifest'),
53
+ ...tenants.map((t) => chalk_1.default.white(t.name)),
54
+ ],
55
+ style: { head: [], border: ['dim'] },
56
+ });
57
+ for (const pluginId of installed) {
58
+ const manifestPath = path_1.default.join(PLUGINS_DIR, pluginId, 'manifest.json');
59
+ const hasManifest = fs_1.default.existsSync(manifestPath);
60
+ let manifest = null;
61
+ if (hasManifest) {
62
+ try {
63
+ manifest = JSON.parse(fs_1.default.readFileSync(manifestPath, 'utf-8'));
64
+ }
65
+ catch { }
66
+ }
67
+ const row = [
68
+ chalk_1.default.bold(manifest?.name || pluginId),
69
+ hasManifest ? chalk_1.default.green('Si') : chalk_1.default.dim('No'),
70
+ ];
71
+ for (const tenant of tenants) {
72
+ const tp = tenantPlugins.find((r) => r.tenantId === tenant.id && r.pluginId === pluginId);
73
+ if (tp?.isActive) {
74
+ row.push(chalk_1.default.green('Activo'));
75
+ }
76
+ else if (tp) {
77
+ row.push(chalk_1.default.dim('Inactivo'));
78
+ }
79
+ else {
80
+ row.push(chalk_1.default.dim('-'));
81
+ }
82
+ }
83
+ table.push(row);
84
+ }
85
+ console.log(table.toString());
86
+ }
87
+ catch (err) {
88
+ spinner.fail(err.message);
89
+ process.exitCode = 1;
90
+ }
91
+ finally {
92
+ await (0, db_1.disconnect)();
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerSetupCommand(program: Command): void;
@@ -0,0 +1,152 @@
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.registerSetupCommand = registerSetupCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const inquirer_1 = __importDefault(require("inquirer"));
10
+ const crypto_1 = __importDefault(require("crypto"));
11
+ const bcrypt_1 = __importDefault(require("bcrypt"));
12
+ const db_1 = require("../utils/db");
13
+ const logger_1 = require("../utils/logger");
14
+ function registerSetupCommand(program) {
15
+ program
16
+ .command('setup')
17
+ .description('Configuración inicial de OpenFactu')
18
+ .action(async () => {
19
+ console.log();
20
+ console.log(chalk_1.default.bold.white(' OpenFactu — Setup Inicial'));
21
+ console.log(chalk_1.default.dim(' ────────────────────────────────────'));
22
+ console.log();
23
+ try {
24
+ // 1. Verificar conexión
25
+ const connSpinner = (0, ora_1.default)('Verificando conexión a la base de datos...').start();
26
+ const connected = await (0, db_1.testConnection)();
27
+ if (!connected) {
28
+ connSpinner.fail('No se pudo conectar a la base de datos');
29
+ logger_1.log.blank();
30
+ logger_1.log.warn('Verifica que DATABASE_URL esté configurado en .env');
31
+ logger_1.log.warn('Ejemplo: DATABASE_URL=postgresql://openfactu:openfactu_pass@localhost:5432/openfactudb');
32
+ return;
33
+ }
34
+ connSpinner.succeed('Conexión a la base de datos establecida');
35
+ const publicDb = (0, db_1.getPublicDb)();
36
+ // 2. Crear tablas del schema público
37
+ const schemaSpinner = (0, ora_1.default)('Verificando schema público...').start();
38
+ try {
39
+ // Verificar si la tabla Tenant existe
40
+ await publicDb.execute(db_1.sql.raw('SELECT 1 FROM "Tenant" LIMIT 1'));
41
+ schemaSpinner.succeed('Schema público OK');
42
+ }
43
+ catch {
44
+ schemaSpinner.text = 'Creando tablas del schema público...';
45
+ // Las tablas se crean con drizzle push — aquí solo verificamos
46
+ schemaSpinner.warn('Schema público necesita inicialización. Ejecuta: npm run db:push:public');
47
+ return;
48
+ }
49
+ // 3. Verificar/crear admin
50
+ const adminSpinner = (0, ora_1.default)('Verificando usuario administrador...').start();
51
+ const [existingAdmin] = await publicDb
52
+ .select()
53
+ .from(db_1.schema.globalUsers)
54
+ .where((0, db_1.eq)(db_1.schema.globalUsers.username, 'admin'));
55
+ if (existingAdmin) {
56
+ adminSpinner.succeed('Usuario admin ya existe');
57
+ }
58
+ else {
59
+ adminSpinner.text = 'Creando usuario administrador...';
60
+ const { adminPassword } = await inquirer_1.default.prompt([
61
+ {
62
+ type: 'password',
63
+ name: 'adminPassword',
64
+ message: 'Password para el usuario admin:',
65
+ default: 'admin123',
66
+ mask: '*',
67
+ },
68
+ ]);
69
+ const hashedPassword = await bcrypt_1.default.hash(adminPassword, 10);
70
+ await publicDb.insert(db_1.schema.globalUsers).values({
71
+ id: crypto_1.default.randomUUID(),
72
+ email: 'admin@openfactu.com',
73
+ username: 'admin',
74
+ password: hashedPassword,
75
+ role: 'SUPERUSER',
76
+ });
77
+ adminSpinner.succeed('Usuario admin creado (admin@openfactu.com)');
78
+ }
79
+ // 4. Verificar/crear primer tenant
80
+ const tenants = await publicDb.select().from(db_1.schema.tenants);
81
+ if (tenants.length > 0) {
82
+ logger_1.log.success(`${tenants.length} tenant(s) ya existen`);
83
+ }
84
+ else {
85
+ logger_1.log.blank();
86
+ const { createTenant } = await inquirer_1.default.prompt([
87
+ {
88
+ type: 'confirm',
89
+ name: 'createTenant',
90
+ message: 'No hay empresas. ¿Crear la primera?',
91
+ default: true,
92
+ },
93
+ ]);
94
+ if (createTenant) {
95
+ const { tenantName } = await inquirer_1.default.prompt([
96
+ {
97
+ type: 'input',
98
+ name: 'tenantName',
99
+ message: 'Nombre de la empresa:',
100
+ default: 'Mi Empresa',
101
+ },
102
+ ]);
103
+ const schemaName = 'tenant_' + tenantName
104
+ .toLowerCase()
105
+ .replace(/[^a-z0-9]+/g, '_')
106
+ .replace(/^_|_$/g, '');
107
+ const tenantSpinner = (0, ora_1.default)(`Creando empresa "${tenantName}"...`).start();
108
+ const tenantId = crypto_1.default.randomUUID();
109
+ await publicDb.insert(db_1.schema.tenants).values({
110
+ id: tenantId,
111
+ name: tenantName,
112
+ schemaName,
113
+ config: JSON.stringify({ createdAt: new Date(), createdBy: 'CLI Setup' }),
114
+ updatedAt: new Date(),
115
+ });
116
+ await publicDb.execute(db_1.sql.raw(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`));
117
+ tenantSpinner.succeed(`Empresa "${tenantName}" creada (${schemaName})`);
118
+ // Asignar admin al tenant
119
+ const [admin] = await publicDb
120
+ .select()
121
+ .from(db_1.schema.globalUsers)
122
+ .where((0, db_1.eq)(db_1.schema.globalUsers.username, 'admin'));
123
+ if (admin) {
124
+ await publicDb.insert(db_1.schema.userTenantMemberships).values({
125
+ id: crypto_1.default.randomUUID(),
126
+ userId: admin.id,
127
+ tenantId,
128
+ role: 'ADMIN',
129
+ });
130
+ logger_1.log.success('Admin asignado a la empresa');
131
+ }
132
+ logger_1.log.info('Ejecuta "openfactu migrate" para aplicar migraciones al nuevo tenant');
133
+ }
134
+ }
135
+ logger_1.log.blank();
136
+ logger_1.log.success(chalk_1.default.bold('Setup completado'));
137
+ logger_1.log.blank();
138
+ logger_1.log.dim(' Próximos pasos:');
139
+ logger_1.log.dim(' openfactu migrate — Aplicar migraciones');
140
+ logger_1.log.dim(' openfactu tenant list — Ver empresas');
141
+ logger_1.log.dim(' openfactu version — Ver versiones');
142
+ logger_1.log.blank();
143
+ }
144
+ catch (err) {
145
+ logger_1.log.error(err.message);
146
+ process.exitCode = 1;
147
+ }
148
+ finally {
149
+ await (0, db_1.disconnect)();
150
+ }
151
+ });
152
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerTenantCommand(program: Command): void;
@@ -0,0 +1,220 @@
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.registerTenantCommand = registerTenantCommand;
7
+ const chalk_1 = __importDefault(require("chalk"));
8
+ const ora_1 = __importDefault(require("ora"));
9
+ const cli_table3_1 = __importDefault(require("cli-table3"));
10
+ const inquirer_1 = __importDefault(require("inquirer"));
11
+ const crypto_1 = __importDefault(require("crypto"));
12
+ const db_1 = require("../utils/db");
13
+ const logger_1 = require("../utils/logger");
14
+ function registerTenantCommand(program) {
15
+ const tenant = program
16
+ .command('tenant')
17
+ .description('Gestión de tenants (empresas)');
18
+ // ── openfactu tenant list ──
19
+ tenant
20
+ .command('list')
21
+ .description('Lista todos los tenants')
22
+ .action(async () => {
23
+ const spinner = (0, ora_1.default)('Cargando tenants...').start();
24
+ try {
25
+ const tenants = await (0, db_1.getAllTenants)();
26
+ if (tenants.length === 0) {
27
+ spinner.warn('No hay tenants registrados');
28
+ return;
29
+ }
30
+ spinner.succeed(`${tenants.length} tenant(s) encontrado(s)`);
31
+ const table = new cli_table3_1.default({
32
+ head: [chalk_1.default.white('Nombre'), chalk_1.default.white('Schema'), chalk_1.default.white('ID'), chalk_1.default.white('Creado')],
33
+ style: { head: [], border: ['dim'] },
34
+ });
35
+ for (const t of tenants) {
36
+ table.push([
37
+ chalk_1.default.bold(t.name),
38
+ chalk_1.default.cyan(t.schemaName),
39
+ chalk_1.default.dim(t.id.substring(0, 8) + '...'),
40
+ t.createdAt ? new Date(t.createdAt).toLocaleDateString('es-ES') : '-',
41
+ ]);
42
+ }
43
+ console.log(table.toString());
44
+ }
45
+ catch (err) {
46
+ spinner.fail(err.message);
47
+ process.exitCode = 1;
48
+ }
49
+ finally {
50
+ await (0, db_1.disconnect)();
51
+ }
52
+ });
53
+ // ── openfactu tenant create ──
54
+ tenant
55
+ .command('create [name]')
56
+ .description('Crea un nuevo tenant')
57
+ .action(async (name) => {
58
+ try {
59
+ let tenantName = name;
60
+ if (!tenantName) {
61
+ const answers = await inquirer_1.default.prompt([
62
+ {
63
+ type: 'input',
64
+ name: 'name',
65
+ message: 'Nombre de la empresa:',
66
+ validate: (v) => v.trim().length > 0 || 'El nombre es obligatorio',
67
+ },
68
+ ]);
69
+ tenantName = answers.name;
70
+ }
71
+ const schemaName = 'tenant_' + tenantName
72
+ .toLowerCase()
73
+ .replace(/[^a-z0-9]+/g, '_')
74
+ .replace(/^_|_$/g, '');
75
+ logger_1.log.info(`Empresa: ${chalk_1.default.bold(tenantName)}`);
76
+ logger_1.log.info(`Schema: ${chalk_1.default.cyan(schemaName)}`);
77
+ logger_1.log.blank();
78
+ const spinner = (0, ora_1.default)('Creando tenant...').start();
79
+ const publicDb = (0, db_1.getPublicDb)();
80
+ // Verificar que no exista
81
+ const [existing] = await publicDb
82
+ .select()
83
+ .from(db_1.schema.tenants)
84
+ .where((0, db_1.eq)(db_1.schema.tenants.name, tenantName));
85
+ if (existing) {
86
+ spinner.warn(`El tenant "${tenantName}" ya existe (${existing.id})`);
87
+ return;
88
+ }
89
+ // Crear registro en la tabla Tenant
90
+ const tenantId = crypto_1.default.randomUUID();
91
+ await publicDb.insert(db_1.schema.tenants).values({
92
+ id: tenantId,
93
+ name: tenantName,
94
+ schemaName,
95
+ config: JSON.stringify({ createdAt: new Date(), createdBy: 'CLI' }),
96
+ updatedAt: new Date(),
97
+ });
98
+ spinner.text = 'Creando schema de PostgreSQL...';
99
+ await publicDb.execute(db_1.sql.raw(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`));
100
+ spinner.text = 'Ejecutando migraciones...';
101
+ // Aplicar migraciones
102
+ const fs = require('fs');
103
+ const path = require('path');
104
+ const MIGRATIONS_DIR = require('../utils/paths').getMigrationsDir();
105
+ if (fs.existsSync(MIGRATIONS_DIR)) {
106
+ const tenantDb = (0, db_1.getTenantDb)(schemaName);
107
+ // Crear tabla de historia
108
+ await tenantDb.execute(db_1.sql.raw(`
109
+ CREATE TABLE IF NOT EXISTS "${schemaName}"."_MigrationHistory" (
110
+ "id" TEXT PRIMARY KEY,
111
+ "description" TEXT,
112
+ "appliedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
113
+ )
114
+ `));
115
+ const files = fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort();
116
+ for (const file of files) {
117
+ spinner.text = `Aplicando ${file}...`;
118
+ const migrationId = file.replace('.sql', '');
119
+ const filePath = path.join(MIGRATIONS_DIR, file);
120
+ let rawSql = fs.readFileSync(filePath, 'utf8');
121
+ const processedSql = rawSql.replace(/{{schema}}/g, schemaName);
122
+ const statements = processedSql
123
+ .split(/;(?=(?:[^$]*\$\$[^$]*\$\$)*[^$]*$)/)
124
+ .map((s) => s.trim())
125
+ .filter((s) => s.length > 0);
126
+ for (const statement of statements) {
127
+ await tenantDb.execute(db_1.sql.raw(statement));
128
+ }
129
+ await tenantDb.execute(db_1.sql.raw(`INSERT INTO "${schemaName}"."_MigrationHistory" (id, description) VALUES ('${migrationId}', 'Creado desde CLI')`));
130
+ }
131
+ }
132
+ spinner.succeed('Tenant creado correctamente');
133
+ logger_1.log.blank();
134
+ logger_1.log.success(`ID: ${chalk_1.default.dim(tenantId)}`);
135
+ logger_1.log.success(`Nombre: ${chalk_1.default.bold(tenantName)}`);
136
+ logger_1.log.success(`Schema: ${chalk_1.default.cyan(schemaName)}`);
137
+ }
138
+ catch (err) {
139
+ logger_1.log.error(err.message);
140
+ process.exitCode = 1;
141
+ }
142
+ finally {
143
+ await (0, db_1.disconnect)();
144
+ }
145
+ });
146
+ // ── openfactu tenant sync ──
147
+ tenant
148
+ .command('sync [name]')
149
+ .description('Sincroniza migraciones de un tenant o todos')
150
+ .action(async (name) => {
151
+ const spinner = (0, ora_1.default)('Sincronizando...').start();
152
+ try {
153
+ let tenants;
154
+ if (name) {
155
+ const t = await (0, db_1.getAllTenants)();
156
+ const found = t.find((x) => x.name.toLowerCase() === name.toLowerCase());
157
+ if (!found) {
158
+ spinner.fail(`Tenant "${name}" no encontrado`);
159
+ return;
160
+ }
161
+ tenants = [found];
162
+ }
163
+ else {
164
+ tenants = await (0, db_1.getAllTenants)();
165
+ }
166
+ spinner.succeed(`Sincronizando ${tenants.length} tenant(s)...`);
167
+ // Usamos el mismo proceso de migración
168
+ const fs = require('fs');
169
+ const path = require('path');
170
+ const MIGRATIONS_DIR = require('../utils/paths').getMigrationsDir();
171
+ const files = fs.existsSync(MIGRATIONS_DIR)
172
+ ? fs.readdirSync(MIGRATIONS_DIR).filter((f) => f.endsWith('.sql')).sort()
173
+ : [];
174
+ for (const tenant of tenants) {
175
+ const tenantDb = (0, db_1.getTenantDb)(tenant.schemaName);
176
+ // Asegurar tabla de historia
177
+ await tenantDb.execute(db_1.sql.raw(`
178
+ CREATE TABLE IF NOT EXISTS "${tenant.schemaName}"."_MigrationHistory" (
179
+ "id" TEXT PRIMARY KEY,
180
+ "description" TEXT,
181
+ "appliedAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
182
+ )
183
+ `));
184
+ const result = await tenantDb.execute(db_1.sql.raw(`SELECT id FROM "${tenant.schemaName}"."_MigrationHistory" ORDER BY id`));
185
+ const applied = result.rows.map((r) => r.id);
186
+ const pending = files.filter((f) => !applied.includes(f.replace('.sql', '')));
187
+ if (pending.length === 0) {
188
+ logger_1.log.dim(` ${tenant.name} — al día`);
189
+ }
190
+ else {
191
+ logger_1.log.info(` ${tenant.name} — ${pending.length} pendiente(s)`);
192
+ for (const file of pending) {
193
+ const migrationId = file.replace('.sql', '');
194
+ const filePath = path.join(MIGRATIONS_DIR, file);
195
+ let rawSql = fs.readFileSync(filePath, 'utf8');
196
+ const processedSql = rawSql.replace(/{{schema}}/g, tenant.schemaName);
197
+ const statements = processedSql
198
+ .split(/;(?=(?:[^$]*\$\$[^$]*\$\$)*[^$]*$)/)
199
+ .map((s) => s.trim())
200
+ .filter((s) => s.length > 0);
201
+ for (const statement of statements) {
202
+ await tenantDb.execute(db_1.sql.raw(statement));
203
+ }
204
+ await tenantDb.execute(db_1.sql.raw(`INSERT INTO "${tenant.schemaName}"."_MigrationHistory" (id, description) VALUES ('${migrationId}', 'Sync CLI')`));
205
+ logger_1.log.success(` ${file}`);
206
+ }
207
+ }
208
+ }
209
+ logger_1.log.blank();
210
+ logger_1.log.success('Sincronización completada');
211
+ }
212
+ catch (err) {
213
+ spinner.fail(err.message);
214
+ process.exitCode = 1;
215
+ }
216
+ finally {
217
+ await (0, db_1.disconnect)();
218
+ }
219
+ });
220
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerUpdateCommand(program: Command): void;