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 +9 -2
- package/lib/commands/tunnel.js +54 -4
- package/lib/commands/tunnels.js +114 -0
- package/package.json +1 -1
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
|
-
.
|
|
85
|
-
|
|
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')
|
package/lib/commands/tunnel.js
CHANGED
|
@@ -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:
|
|
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
|
-
}
|
|
254
|
-
|
|
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
|
-
|
|
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
|
+
}
|