@danieltmn/openbridge 0.5.1 → 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,16 @@ 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
+
7
17
  ## [0.5.1] - 2026-09-14
8
18
 
9
19
  ### Corregido
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieltmn/openbridge",
3
- "version": "0.5.1",
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
  }
@@ -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
  /**