@danieltmn/openbridge 0.5.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,23 @@ Todos los cambios relevantes de OpenBridge. Formato basado en
4
4
  [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/) y
5
5
  [Versionado Semantico](https://semver.org/lang/es/).
6
6
 
7
+ ## [0.5.2] - 2026-09-14
8
+
9
+ ### Corregido
10
+
11
+ - Diagnostico de puerto ocupado: `status` y `server` muestran que **PID** usa el
12
+ puerto (suele ser otra casa con otro `--dir`) y, si el arranque en segundo plano
13
+ muere, se muestran las ultimas lineas de `server.log`.
14
+ - `store`: reintentos con backoff en el `rename` atomico (EPERM/EACCES/EBUSY en
15
+ Windows) y limpieza del `.tmp` si no se puede.
16
+
17
+ ## [0.5.1] - 2026-09-14
18
+
19
+ ### Corregido
20
+
21
+ - `openbridge join` normaliza la URL del hub a `/api.php` (antes, pasar la base
22
+ o `/chat.php` apuntaba a `/` y el puente se quedaba sin trabajo).
23
+
7
24
  ## [0.5.0] - 2026-09-14
8
25
 
9
26
  ### Agregado
package/README.md CHANGED
@@ -133,7 +133,8 @@ Ejemplo concreto:
133
133
  3. En la **PC 2** (remota):
134
134
  ```bash
135
135
  openbridge join https://tu-url-publica --token <bridgeToken> --id pc2 --name "PC 2"
136
- # guarda la config y arranca el puente; con --no-start solo guarda
136
+ # acepta la base, /chat.php o /api.php; se normaliza a /api.php
137
+ # con --no-start solo guarda la config
137
138
  ```
138
139
  (equivale a `openbridge bridge --api … --token … --id … --name …`, pero
139
140
  persiste la config para no repetir flags)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieltmn/openbridge",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "Tu opencode en el celular, sin hosting: corre la app, el puente y un tunel publico desde tu PC.",
5
5
  "type": "commonjs",
6
6
  "author": "tamnora",
package/src/cli.js CHANGED
@@ -105,6 +105,70 @@ function pidAlive(pid) {
105
105
  try { process.kill(pid, 0); return true; } catch (e) { return false; }
106
106
  }
107
107
 
108
+ // PIDs escuchando en un puerto TCP (netstat en Windows; lsof en el resto). Sirve
109
+ // para explicar un EADDRINUSE: normalmente es OTRA casa (otro --dir) con el
110
+ // mismo puerto por defecto.
111
+ function portOwnerPids(port) {
112
+ try {
113
+ const win = process.platform === 'win32';
114
+ const r = spawnSync(win ? 'netstat' : 'lsof',
115
+ win ? ['-ano', '-p', 'tcp'] : ['-t', '-i', 'tcp:' + port, '-s', 'tcp:listen'],
116
+ { encoding: 'utf8', windowsHide: true, timeout: 15000 });
117
+ if (r.error || !r.stdout) return [];
118
+ const pids = new Set();
119
+ if (!win) {
120
+ for (const n of String(r.stdout).split(/\s+/)) {
121
+ const p = parseInt(n, 10);
122
+ if (p > 0) pids.add(p);
123
+ }
124
+ return [...pids];
125
+ }
126
+ const re = new RegExp(':' + port + '\\s');
127
+ for (const line of String(r.stdout).split(/\r?\n/)) {
128
+ if (!re.test(line) || !/LISTENING/i.test(line)) continue;
129
+ const parts = line.trim().split(/\s+/);
130
+ const pid = parseInt(parts[parts.length - 1], 10);
131
+ if (pid > 0) pids.add(pid);
132
+ }
133
+ return [...pids];
134
+ } catch (e) {
135
+ return [];
136
+ }
137
+ }
138
+
139
+ // Nombre del proceso de un PID (best-effort) para el diagnostico del puerto.
140
+ function describePid(pid) {
141
+ try {
142
+ if (process.platform === 'win32') {
143
+ const r = spawnSync('tasklist', ['/FI', 'PID eq ' + pid, '/FO', 'CSV', '/NH'], { encoding: 'utf8', windowsHide: true, timeout: 8000 });
144
+ const m = String(r.stdout || '').match(/^"([^"]+)"/m);
145
+ return m ? m[1] : '';
146
+ }
147
+ const r = spawnSync('ps', ['-p', String(pid), '-o', 'comm='], { encoding: 'utf8', windowsHide: true, timeout: 8000 });
148
+ return String(r.stdout || '').trim();
149
+ } catch (e) {
150
+ return '';
151
+ }
152
+ }
153
+
154
+ // "PID 123 (node.exe)" para el proceso que ocupa el puerto, o '' si no hay.
155
+ function portHint(port) {
156
+ const pids = portOwnerPids(port);
157
+ if (!pids.length) return '';
158
+ return pids.map((p) => 'PID ' + p + (describePid(p) ? ' (' + describePid(p) + ')' : '')).join(', ');
159
+ }
160
+
161
+ // Ultimas `n` lineas no vacias de un archivo (para mostrar el error real del
162
+ // server cuando arranca en segundo plano y muere).
163
+ function tailLines(file, n) {
164
+ try {
165
+ const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/).filter((l) => l.trim() !== '');
166
+ return lines.slice(-n).join('\n');
167
+ } catch (e) {
168
+ return '';
169
+ }
170
+ }
171
+
108
172
  function runtimePath() { return paths.p('runtime.json'); }
109
173
  function readRuntime() {
110
174
  try { return JSON.parse(fs.readFileSync(runtimePath(), 'utf8')); } catch (e) { return null; }
@@ -328,6 +392,12 @@ async function cmdServer(argv) {
328
392
  await printStatus();
329
393
  if (!rt) {
330
394
  console.log('');
395
+ const tail = tailLines(paths.serverLogPath(), 8);
396
+ if (tail) {
397
+ console.log('Ultimas lineas de ' + paths.serverLogPath() + ':');
398
+ console.log(tail);
399
+ console.log('');
400
+ }
331
401
  console.log('No pude confirmar el arranque. Revisa: openbridge logs');
332
402
  }
333
403
  return 0;
@@ -349,6 +419,12 @@ async function cmdServer(argv) {
349
419
  } catch (e) {
350
420
  if (e && e.code === 'EADDRINUSE') {
351
421
  log.error('puerto ' + port + ' ocupado (¿ya corre OpenBridge?). Usa --port <otro> o `openbridge stop`.');
422
+ const hint = portHint(port);
423
+ if (hint) {
424
+ console.error(' lo esta usando: ' + hint);
425
+ console.error(' puede ser otra casa (otro --dir): proba `openbridge stop --dir <esa-casa>`'
426
+ + (process.platform === 'win32' ? ' o `taskkill /PID ' + portOwnerPids(port)[0] + ' /T /F`.' : '.'));
427
+ }
352
428
  return 1;
353
429
  }
354
430
  throw e;
@@ -443,6 +519,14 @@ async function printStatus() {
443
519
  if (paths.exists()) {
444
520
  console.log(' carpeta: ' + paths.baseDir());
445
521
  console.log(' datos : ' + paths.home());
522
+ try {
523
+ const app = config.readApp();
524
+ const hint = portHint(app.port);
525
+ if (hint) {
526
+ console.log(' aviso : el puerto ' + app.port + ' ya lo usa ' + hint);
527
+ console.log(' suele ser otra casa (otro --dir); cerrala con `openbridge stop --dir <esa-casa>` o usa --port <otro>.');
528
+ }
529
+ } catch (e) { /* sin app.json no hay puerto que mirar */ }
446
530
  }
447
531
  return false;
448
532
  }
@@ -624,17 +708,28 @@ async function cmdBridge(argv) {
624
708
  // join: esta PC se suma como puente de un hub (otra PC con `openbridge server`).
625
709
  // Guarda la URL/token/identidad en config.json y arranca el puente.
626
710
  // ---------------------------------------------------------------------------
711
+ // Normaliza la URL del hub a su endpoint de API: acepta la base, /chat.php o
712
+ // /api.php y devuelve siempre algo terminado en /api.php (lo que espera el
713
+ // puente). Sin esto, un `join https://host` apuntaba a / y no al API.
714
+ function hubApiUrl(raw) {
715
+ let u = String(raw || '').trim().replace(/\/+$/, '');
716
+ u = u.replace(/\/chat\.php$/i, '');
717
+ if (!/\/api\.php$/i.test(u)) u += '/api.php';
718
+ return u;
719
+ }
720
+
627
721
  async function cmdJoin(argv) {
628
722
  const { flags, _ } = parseArgs(argv);
629
723
  const url = String(_[0] || flags.api || '').trim();
630
724
  if (!url || !/^https?:\/\//i.test(url)) {
631
725
  console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start]');
632
726
  console.error('Ej.: openbridge join https://mi-pc.trycloudflare.com --token <t> --id pc2 --name "PC oficina"');
727
+ console.error('(acepta la base, /chat.php o /api.php; se normaliza a /api.php)');
633
728
  return 1;
634
729
  }
635
730
  paths.ensureDirs();
636
731
  const cfg = config.readBridge();
637
- cfg.apiUrl = url.replace(/\/+$/, '');
732
+ cfg.apiUrl = hubApiUrl(url);
638
733
  if (typeof flags.token === 'string') cfg.apiToken = flags.token;
639
734
  if (typeof flags.id === 'string') cfg.bridgeId = sanitizeId(flags.id);
640
735
  else if (!cfg.bridgeId) cfg.bridgeId = sanitizeId(os.hostname());
@@ -12,6 +12,29 @@ const fs = require('node:fs/promises');
12
12
 
13
13
  const locks = new Map();
14
14
 
15
+ function delay(ms) {
16
+ return new Promise((resolve) => setTimeout(resolve, ms));
17
+ }
18
+
19
+ // En Windows, rename() puede fallar con EPERM/EACCES si otro proceso (antivirus,
20
+ // indexador) tiene el archivo abierto un instante. Reintentamos con backoff y,
21
+ // si no hay forma, limpiamos el .tmp.
22
+ async function renameWithRetry(from, to, tries = 5) {
23
+ for (let i = 0; ; i++) {
24
+ try {
25
+ await fs.rename(from, to);
26
+ return;
27
+ } catch (e) {
28
+ const transient = e && (e.code === 'EPERM' || e.code === 'EACCES' || e.code === 'EBUSY');
29
+ if (!transient || i >= tries - 1) {
30
+ try { await fs.unlink(from); } catch (e2) { /* nada */ }
31
+ throw e;
32
+ }
33
+ await delay(20 * (i + 1));
34
+ }
35
+ }
36
+ }
37
+
15
38
  function withLock(key, fn) {
16
39
  const prev = locks.get(key) || Promise.resolve();
17
40
  const run = prev.then(fn, fn);
@@ -33,7 +56,7 @@ async function readJson(file, fallback) {
33
56
  async function writeAtomic(file, data) {
34
57
  const tmp = file + '.' + process.pid + '.' + Math.random().toString(36).slice(2) + '.tmp';
35
58
  await fs.writeFile(tmp, JSON.stringify(data, null, 2));
36
- await fs.rename(tmp, file);
59
+ await renameWithRetry(tmp, file);
37
60
  }
38
61
 
39
62
  /**