entexto-cli 2.1.2 → 2.2.1

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/bin/entexto.js CHANGED
@@ -81,10 +81,17 @@ program
81
81
  program
82
82
  .command('tunnel [target]')
83
83
  .description('Exponer un servidor local en una URL pública de entexto.com')
84
- .action((target) => {
85
- require('../lib/commands/tunnel')({ target: target || '3000' });
84
+ .option('-l, --link <name>', 'Link personalizado: /t/<name> (requiere login)')
85
+ .action((target, opts) => {
86
+ require('../lib/commands/tunnel')({ target: target || '3000', link: opts.link });
86
87
  });
87
88
 
89
+ // ─── entexto tunnels ─────────────────────────────────────────
90
+ program
91
+ .command('tunnels')
92
+ .description('Ver y gestionar tus túneles abiertos')
93
+ .action(require('../lib/commands/tunnels'));
94
+
88
95
  // ─── entexto create ──────────────────────────────────────────
89
96
  program
90
97
  .command('create')
@@ -30,6 +30,14 @@ module.exports = async function tunnel(opts) {
30
30
  const { getConfig } = require('../utils/config');
31
31
  const cfg = getConfig();
32
32
  const baseUrl = (cfg.baseUrl || 'https://entexto.com').replace(/\/$/, '');
33
+ const token = cfg.token || null;
34
+
35
+ // Validar link personalizado
36
+ const customLink = opts.link || null;
37
+ if (customLink && !token) {
38
+ console.error(chalk.red('Los links personalizados requieren iniciar sesión. Ejecuta: entexto login'));
39
+ process.exit(1);
40
+ }
33
41
 
34
42
  // Calcular URL del WebSocket
35
43
  const wsUrl = baseUrl.replace(/^http/, 'ws') + '/tunnel';
@@ -52,15 +60,18 @@ module.exports = async function tunnel(opts) {
52
60
  upgrade: false,
53
61
  reconnection: true,
54
62
  reconnectionDelay: 1000,
55
- reconnectionDelayMax: 3000,
63
+ reconnectionDelayMax: 5000,
56
64
  reconnectionAttempts: Infinity,
57
65
  timeout: 20000,
66
+ // Forzar reconexión incluso cuando el server cierra la conexión
67
+ forceNew: false,
58
68
  });
59
69
 
60
70
  // Mantener el mismo tunnelId entre reconexiones
61
71
  let currentTunnelId = null;
62
72
  let publicUrl = null;
63
73
  let heartbeatTimer = null;
74
+ let reconnectWatchdog = null;
64
75
 
65
76
  function startHeartbeat() {
66
77
  clearInterval(heartbeatTimer);
@@ -70,9 +81,24 @@ module.exports = async function tunnel(opts) {
70
81
  }, 25000);
71
82
  }
72
83
 
84
+ // Watchdog: si tras 15s no estamos conectados ni reconectando, forzar reconexión
85
+ function startWatchdog() {
86
+ clearInterval(reconnectWatchdog);
87
+ reconnectWatchdog = setInterval(() => {
88
+ if (!socket.connected && !socket.io._reconnecting) {
89
+ console.log(chalk.yellow(' ⟳ Watchdog: forzando reconexión...'));
90
+ try { socket.connect(); } catch (_) {}
91
+ }
92
+ }, 15000);
93
+ }
94
+
73
95
  socket.on('connect', () => {
74
96
  const meta = { target: targetUrl };
75
97
  if (currentTunnelId) meta.reclaimId = currentTunnelId; // intentar recuperar el mismo ID
98
+ if (customLink) meta.link = customLink;
99
+ if (token) meta.token = token;
100
+
101
+ startWatchdog();
76
102
 
77
103
  socket.emit('open-tunnel', meta, (res) => {
78
104
  if (!res || !res.ok) {
@@ -247,31 +273,55 @@ module.exports = async function tunnel(opts) {
247
273
  localReq.end();
248
274
  });
249
275
 
276
+ let connectErrorCount = 0;
277
+
250
278
  socket.on('connect_error', (err) => {
279
+ connectErrorCount++;
251
280
  if (spinner.isSpinning) {
252
281
  spinner.fail('No se pudo conectar al servidor: ' + err.message);
253
- } else {
254
- console.error(chalk.red(' Conexión perdida: ' + err.message));
282
+ }
283
+ // Solo mostrar cada 10 errores para no llenar la terminal
284
+ if (connectErrorCount <= 3 || connectErrorCount % 10 === 0) {
285
+ console.error(chalk.yellow(` ⟳ Reintentando conexión... (intento ${connectErrorCount})`));
286
+ }
287
+ // Si socket.io dejó de intentar, forzar reconexión
288
+ if (!socket.io._reconnecting) {
289
+ setTimeout(() => {
290
+ try { socket.connect(); } catch (_) {}
291
+ }, 3000);
255
292
  }
256
293
  });
257
294
 
258
295
  socket.on('disconnect', (reason) => {
259
296
  clearInterval(heartbeatTimer);
260
- console.log(chalk.yellow('\n Desconectado: ' + reason));
297
+ connectErrorCount = 0;
261
298
  if (reason === 'io client disconnect') {
262
299
  // Cierre manual (Ctrl+C)
300
+ clearInterval(reconnectWatchdog);
263
301
  process.exit(0);
264
302
  }
303
+ console.log(chalk.yellow('\n Desconectado: ' + reason));
265
304
  if (publicUrl) {
266
305
  console.log(chalk.gray(' Reintentando... (la URL se mantendrá: ') + chalk.cyan(publicUrl) + chalk.gray(')'));
267
306
  } else {
268
307
  console.log(chalk.gray(' Reintentando...'));
269
308
  }
309
+ // Forzar reconexión si socket.io no lo hace automáticamente
310
+ if (reason === 'transport close' || reason === 'transport error' || reason === 'ping timeout') {
311
+ setTimeout(() => {
312
+ if (!socket.connected && !socket.io._reconnecting) {
313
+ console.log(chalk.yellow(' ⟳ Forzando reconexión...'));
314
+ try { socket.connect(); } catch (_) {}
315
+ }
316
+ }, 2000);
317
+ }
270
318
  });
271
319
 
272
320
  // Ctrl+C
273
321
  process.on('SIGINT', () => {
274
322
  console.log(chalk.gray('\n Cerrando túnel...'));
323
+ clearInterval(heartbeatTimer);
324
+ clearInterval(reconnectWatchdog);
275
325
  socket.disconnect();
276
326
  process.exit(0);
277
327
  });
@@ -0,0 +1,114 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ora = require('ora');
5
+
6
+ module.exports = async function tunnels() {
7
+ const { getConfig } = require('../utils/config');
8
+ const cfg = getConfig();
9
+ const token = cfg.token;
10
+
11
+ if (!token) {
12
+ console.error(chalk.red('Debes iniciar sesión para ver tus túneles. Ejecuta: entexto login'));
13
+ process.exit(1);
14
+ }
15
+
16
+ const baseUrl = (cfg.baseUrl || 'https://entexto.com').replace(/\/$/, '');
17
+ const wsUrl = baseUrl.replace(/^http/, 'ws') + '/tunnel';
18
+
19
+ const spinner = ora('Consultando túneles abiertos...').start();
20
+
21
+ let ioClient;
22
+ try {
23
+ ioClient = require('socket.io-client');
24
+ } catch {
25
+ spinner.fail('Falta socket.io-client. Ejecuta: npm install socket.io-client');
26
+ process.exit(1);
27
+ }
28
+
29
+ const socket = ioClient(wsUrl, {
30
+ transports: ['websocket'],
31
+ upgrade: false,
32
+ reconnection: false,
33
+ timeout: 10000,
34
+ });
35
+
36
+ socket.on('connect', () => {
37
+ socket.emit('list-tunnels', { token }, (res) => {
38
+ if (!res || !res.ok) {
39
+ spinner.fail('Error: ' + (res?.error || 'sin respuesta'));
40
+ socket.disconnect();
41
+ process.exit(1);
42
+ }
43
+
44
+ const list = res.tunnels || [];
45
+ spinner.stop();
46
+
47
+ if (list.length === 0) {
48
+ console.log(chalk.yellow('\n No tienes túneles abiertos.\n'));
49
+ socket.disconnect();
50
+ process.exit(0);
51
+ }
52
+
53
+ console.log(chalk.bold(`\n 🚇 Tus túneles abiertos (${list.length}):\n`));
54
+ list.forEach((t, i) => {
55
+ const status = t.connected
56
+ ? chalk.green('● conectado')
57
+ : chalk.yellow('○ reconectando');
58
+ const url = chalk.cyan.underline(`${baseUrl}/t/${t.id}`);
59
+ console.log(` ${i + 1}. ${url}`);
60
+ console.log(` → ${t.target} ${status}${t.queued ? chalk.gray(` (${t.queued} en cola)`) : ''}`);
61
+ });
62
+ console.log('');
63
+
64
+ // Preguntar si quiere cerrar alguno
65
+ askClose(socket, token, list, baseUrl);
66
+ });
67
+ });
68
+
69
+ socket.on('connect_error', (err) => {
70
+ spinner.fail('No se pudo conectar: ' + err.message);
71
+ process.exit(1);
72
+ });
73
+ };
74
+
75
+ async function askClose(socket, token, list, baseUrl) {
76
+ let inquirer;
77
+ try {
78
+ inquirer = require('inquirer');
79
+ } catch {
80
+ socket.disconnect();
81
+ process.exit(0);
82
+ }
83
+
84
+ const { action } = await inquirer.prompt([{
85
+ type: 'list',
86
+ name: 'action',
87
+ message: '¿Qué deseas hacer?',
88
+ choices: [
89
+ ...list.map((t, i) => ({
90
+ name: `Cerrar /t/${t.id} (→ ${t.target})`,
91
+ value: t.id,
92
+ })),
93
+ { name: 'Salir', value: null },
94
+ ],
95
+ }]);
96
+
97
+ if (!action) {
98
+ socket.disconnect();
99
+ process.exit(0);
100
+ }
101
+
102
+ const chalk = require('chalk');
103
+ const spinner = require('ora')('Cerrando túnel...').start();
104
+
105
+ socket.emit('close-tunnel', { token, tunnelId: action }, (res) => {
106
+ if (res?.ok) {
107
+ spinner.succeed(`Túnel /t/${action} cerrado`);
108
+ } else {
109
+ spinner.fail('Error: ' + (res?.error || 'sin respuesta'));
110
+ }
111
+ socket.disconnect();
112
+ process.exit(0);
113
+ });
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "entexto-cli",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "CLI oficial de Entexto — Crea, deploya y gestiona proyectos, dominios, APIs y Live SDK desde tu terminal",
5
5
  "main": "lib/index.js",
6
6
  "bin": {