@danieltmn/openbridge 0.6.0 → 0.6.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,28 @@ 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.6.2] - 2026-09-14
8
+
9
+ ### Corregido
10
+
11
+ - **Tunel en Windows**: los shims `.cmd` (p. ej. `tmole`) se lanzaban con doble
12
+ cita (`cmd.exe /d /s /c "\"ruta\""`) y fallaban con "no se reconoce como un
13
+ comando". Ahora se usa `windowsVerbatimArguments` y, si `tmole` falla, el
14
+ puente **reintenta con `npx --yes tunnelmole`** (antes solo lo intentaba si
15
+ `tmole` no estaba en el PATH). Timeout del tunel: 85 s.
16
+
17
+ ## [0.6.1] - 2026-09-14
18
+
19
+ ### Agregado
20
+
21
+ - `openbridge bridge --background` (segundo plano; sobrevive al cierre de la
22
+ consola), `--stop` y `--status`. `openbridge pair`/`join` aceptan `--background`.
23
+
24
+ ### Corregido
25
+
26
+ - `autostart` corre solo el puente cuando la PC pertenece a un hub remoto
27
+ (`join`/`pair`), en vez del server local.
28
+
7
29
  ## [0.6.0] - 2026-09-14
8
30
 
9
31
  ### Agregado
package/README.md CHANGED
@@ -89,9 +89,9 @@ iniciar sesión** una vez; los chats, carpetas, túnel y push se conservan.
89
89
  | `openbridge qr` | Muestra la URL (pública o local) como QR para escanear desde el celular |
90
90
  | `openbridge tunnel` | Muestra o cambia el proveedor de túnel y su dominio fijo (`--domain`) |
91
91
  | `openbridge logs` | Logs (`--follow`, `--server`, `--bridge`) |
92
- | `openbridge bridge` | Corre **solo** el puente (`--api --token --id --name`) |
93
- | `openbridge join` | Vincula esta PC como puente de un hub (`<url> --token --id --name`) |
94
- | `openbridge pair` | Empareja esta PC con un **hub PHP** por código (`<url> [--id --name]`) |
92
+ | `openbridge bridge` | Corre **solo** el puente (`--background` = segundo plano, `--stop`, `--status`) |
93
+ | `openbridge join` | Vincula esta PC como puente de un hub (`<url> --token --id --name [--background]`) |
94
+ | `openbridge pair` | Empareja esta PC con un **hub PHP** por código (`<url> [--id --name] [--background]`) |
95
95
  | `openbridge import` | Trae `data/` de OpenConex (`<data-dir> [--force]`) |
96
96
  | `openbridge reset` | Borra chats/datos (`--session <id>`, `--yes`) |
97
97
  | `openbridge autostart` | Arranque automático (`install`/`remove`) |
@@ -162,6 +162,15 @@ igual que OpenConex, y dejar que cada PC corra solo el puente:
162
162
 
163
163
  Guía completa (requisitos, build, subida y emparejamiento): [`docs/DEPLOY-PHP.md`](docs/DEPLOY-PHP.md).
164
164
 
165
+ La PC corre el puente; para que siga tras cerrar la consola:
166
+
167
+ ```bash
168
+ openbridge bridge --background # arranca en segundo plano
169
+ openbridge bridge --status # ¿corre?
170
+ openbridge bridge --stop # detener
171
+ openbridge autostart install # arranca solo al iniciar sesión
172
+ ```
173
+
165
174
  ## Túnel y URL estable
166
175
 
167
176
  Proveedores (`--tunnel` o `openbridge tunnel <prov>`): `tunnelmole` (gratis, sin
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieltmn/openbridge",
3
- "version": "0.6.0",
3
+ "version": "0.6.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",
@@ -1700,10 +1700,24 @@ function tunnelSpawn(port, onLine, onFail, onUrls, triedNpx = false) {
1700
1700
  return;
1701
1701
  }
1702
1702
  const args = (spec.pre || []).concat(triedNpx ? ['--yes', 'tunnelmole', String(port)] : [String(port)]);
1703
+ let settled = false;
1704
+ // Si `tmole` existe pero falla (shim roto, sin PATH, etc.), reintentamos con
1705
+ // `npx --yes tunnelmole` en vez de darnos por vencidos.
1706
+ const retryNpx = () => {
1707
+ if (settled) return;
1708
+ settled = true;
1709
+ log('túnel: tmole falló, reintentando con npx tunnelmole…');
1710
+ tunnelSpawn(port, onLine, onFail, onUrls, true);
1711
+ };
1703
1712
  let proc;
1704
1713
  try {
1705
- proc = spawn(spec.bin, args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
1714
+ proc = spawn(spec.bin, args, {
1715
+ stdio: ['ignore', 'pipe', 'pipe'],
1716
+ windowsHide: true,
1717
+ windowsVerbatimArguments: !!spec.verbatim,
1718
+ });
1706
1719
  } catch (e) {
1720
+ if (!triedNpx) { retryNpx(); return; }
1707
1721
  onFail('no se pudo iniciar tunnelmole (' + e.message + '). Instalalo con: npm i -g tunnelmole');
1708
1722
  return;
1709
1723
  }
@@ -1711,14 +1725,17 @@ function tunnelSpawn(port, onLine, onFail, onUrls, triedNpx = false) {
1711
1725
  const feed = (d) => {
1712
1726
  buf += String(d);
1713
1727
  const urls = parseTunnelUrls(buf);
1714
- if (urls.https || urls.http) onUrls(urls);
1728
+ if (urls.https || urls.http) { settled = true; onUrls(urls); }
1715
1729
  };
1716
1730
  proc.stdout.on('data', feed);
1717
1731
  proc.stderr.on('data', feed);
1718
1732
  proc.on('error', (e) => {
1733
+ if (!triedNpx) { retryNpx(); return; }
1719
1734
  onFail('no se pudo iniciar tunnelmole (' + e.message + '). Instalalo con: npm i -g tunnelmole');
1720
1735
  });
1721
1736
  proc.on('exit', (code) => {
1737
+ if (settled) return;
1738
+ if (!triedNpx) { retryNpx(); return; }
1722
1739
  onFail('tmole terminó (código ' + code + ')' + (buf ? ': ' + buf.slice(0, 160).trim() : ''));
1723
1740
  });
1724
1741
  onLine(proc);
@@ -1756,10 +1773,10 @@ async function tunnelStart(cmd) {
1756
1773
  };
1757
1774
  entry = { port, proc: null, urls: { https: '', http: '' }, startedAt: Date.now() };
1758
1775
  tunnels.set(port, entry);
1759
- // 60 s: con el fallback de npx, la primera vez descarga tunnelmole.
1776
+ // 85 s: con el fallback de npx, la primera vez descarga tunnelmole.
1760
1777
  timer = setTimeout(() => {
1761
- finish(false, 'tunnelmole no devolvió URLs (60 s). ¿Está instalado? npm i -g tunnelmole');
1762
- }, 60000);
1778
+ finish(false, 'tunnelmole no devolvió URLs (85 s). ¿Está instalado? npm i -g tunnelmole');
1779
+ }, 85000);
1763
1780
  tunnelSpawn(
1764
1781
  port,
1765
1782
  (proc) => { entry.proc = proc; },
@@ -1913,7 +1930,9 @@ function procResolveBin(token, allowSet) {
1913
1930
  const found = procFindOnPath(bare);
1914
1931
  if (!found) throw new Error('no se encontró "' + token + '" en el PATH');
1915
1932
  if (found.ext.toLowerCase() === '.exe') return { bin: found.p, pre: [] };
1916
- return { bin: 'cmd.exe', pre: ['/d', '/s', '/c', '"' + found.p + '"'] };
1933
+ // Shim .cmd/.bat: se ejecuta con cmd.exe. `verbatim` evita que Node vuelva a
1934
+ // citar el argumento (que ya viene entre comillas) y cmd reciba "\"ruta\"".
1935
+ return { bin: 'cmd.exe', pre: ['/d', '/s', '/c', '"' + found.p + '"'], verbatim: true };
1917
1936
  }
1918
1937
 
1919
1938
  async function procDone(cmd, ok, payload, error) {
@@ -2000,6 +2019,7 @@ async function procStart(cmd) {
2000
2019
  stdio: ['ignore', 'pipe', 'pipe'],
2001
2020
  env: process.env,
2002
2021
  windowsHide: true,
2022
+ windowsVerbatimArguments: !!spec.verbatim,
2003
2023
  });
2004
2024
  } catch (e) {
2005
2025
  await procDone(cmd, false, null, 'no se pudo iniciar: ' + e.message);
package/src/cli.js CHANGED
@@ -682,12 +682,46 @@ async function cmdLogs(argv) {
682
682
  // ---------------------------------------------------------------------------
683
683
  // bridge (solo el puente, para modo hub remoto)
684
684
  // ---------------------------------------------------------------------------
685
+ function bridgePid() {
686
+ try {
687
+ return parseInt(fs.readFileSync(paths.bridgeLockPath(), 'utf8'), 10) || 0;
688
+ } catch (e) {
689
+ return 0;
690
+ }
691
+ }
692
+
685
693
  async function cmdBridge(argv) {
686
694
  const { flags } = parseArgs(argv);
687
695
  if (!fs.existsSync(paths.configPath())) {
688
- console.error('No hay configuracion de puente en ' + paths.home() + '. Corre primero: openbridge join <url> o openbridge init');
696
+ console.error('No hay configuracion de puente en ' + paths.home() + '. Corre primero: openbridge join <url> o openbridge pair <url>');
689
697
  return 1;
690
698
  }
699
+
700
+ if (flags.stop) {
701
+ const pid = bridgePid();
702
+ if (!pid || !pidAlive(pid)) {
703
+ try { fs.unlinkSync(paths.bridgeLockPath()); } catch (e) { /* nada */ }
704
+ console.log('El puente no esta corriendo.');
705
+ return 0;
706
+ }
707
+ killPid(pid);
708
+ try { fs.unlinkSync(paths.bridgeLockPath()); } catch (e) { /* nada */ }
709
+ console.log('Puente detenido (pid ' + pid + ').');
710
+ return 0;
711
+ }
712
+
713
+ const cfg = config.readBridge();
714
+ if (flags.status) {
715
+ const pid = bridgePid();
716
+ const live = pid && pidAlive(pid);
717
+ console.log('Puente: ' + (live ? 'corriendo (pid ' + pid + ')' : 'detenido'));
718
+ console.log(' hub : ' + (cfg.apiUrl || '(sin configurar)'));
719
+ console.log(' id : ' + (cfg.bridgeId || '(sin id)'));
720
+ console.log(' nombre : ' + (cfg.bridgeName || ''));
721
+ console.log(' log : ' + paths.bridgeLogPath());
722
+ return 0;
723
+ }
724
+
691
725
  // Permite apuntar a otro hub sin editar config.json a mano.
692
726
  const env = { ...process.env, OPENBRIDGE_HOME: paths.baseDir() };
693
727
  if (flags.api) env.OPENBRIDGE_API_URL = String(flags.api);
@@ -695,6 +729,33 @@ async function cmdBridge(argv) {
695
729
  if (flags.id) env.OPENBRIDGE_BRIDGE_ID = String(flags.id);
696
730
  if (flags.name) env.OPENBRIDGE_BRIDGE_NAME = String(flags.name);
697
731
  const bridgePath = path.join(__dirname, 'bridge', 'bridge.js');
732
+
733
+ // Segundo plano: detached + stdio ignorado; sobrevive al cierre de la consola.
734
+ if (flags.background || flags.bg || flags.detach || flags.d) {
735
+ const existing = bridgePid();
736
+ if (existing && pidAlive(existing)) {
737
+ console.log('El puente ya corre en segundo plano (pid ' + existing + ').');
738
+ console.log(' estado: openbridge bridge --status · detener: openbridge bridge --stop');
739
+ return 0;
740
+ }
741
+ const child = spawn(process.execPath, [bridgePath], {
742
+ cwd: paths.home(),
743
+ env,
744
+ stdio: 'ignore',
745
+ windowsHide: true,
746
+ detached: true,
747
+ });
748
+ child.unref();
749
+ await new Promise((r) => setTimeout(r, 900));
750
+ const pid = bridgePid() || child.pid;
751
+ console.log('Puente en segundo plano (pid ' + pid + ').');
752
+ console.log(' hub : ' + (cfg.apiUrl || '(sin configurar)'));
753
+ console.log(' log : ' + paths.bridgeLogPath());
754
+ console.log(' estado : openbridge bridge --status');
755
+ console.log(' detener: openbridge bridge --stop');
756
+ return 0;
757
+ }
758
+
698
759
  const child = spawn(process.execPath, [bridgePath], {
699
760
  cwd: paths.home(),
700
761
  env,
@@ -718,11 +779,16 @@ function hubApiUrl(raw) {
718
779
  return u;
719
780
  }
720
781
 
782
+ // Reenvia `--background` a `openbridge bridge` cuando join/pair lo piden.
783
+ function bridgeStartArgs(flags) {
784
+ return (flags.background || flags.bg || flags.detach || flags.d) ? ['--background'] : [];
785
+ }
786
+
721
787
  async function cmdJoin(argv) {
722
788
  const { flags, _ } = parseArgs(argv);
723
789
  const url = String(_[0] || flags.api || '').trim();
724
790
  if (!url || !/^https?:\/\//i.test(url)) {
725
- console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start]');
791
+ console.error('Uso: openbridge join <url-del-hub> [--token <t>] [--id <pc>] [--name "<nombre>"] [--no-start] [--background]');
726
792
  console.error('Ej.: openbridge join https://mi-pc.trycloudflare.com --token <t> --id pc2 --name "PC oficina"');
727
793
  console.error('(acepta la base, /chat.php o /api.php; se normaliza a /api.php)');
728
794
  return 1;
@@ -747,7 +813,7 @@ async function cmdJoin(argv) {
747
813
  }
748
814
  console.log('');
749
815
  console.log('Arrancando el puente (Ctrl+C para salir)...');
750
- return cmdBridge([]);
816
+ return cmdBridge(bridgeStartArgs(flags));
751
817
  }
752
818
 
753
819
  // ---------------------------------------------------------------------------
@@ -771,7 +837,7 @@ async function cmdPair(argv) {
771
837
  const { flags, _ } = parseArgs(argv);
772
838
  const url = String(_[0] || flags.hub || flags.api || '').trim();
773
839
  if (!/^https?:\/\//i.test(url)) {
774
- console.error('Uso: openbridge pair <url-del-hub> [--id <pc>] [--name "<nombre>"] [--no-start]');
840
+ console.error('Uso: openbridge pair <url-del-hub> [--id <pc>] [--name "<nombre>"] [--no-start] [--background]');
775
841
  console.error('Ej.: openbridge pair https://openbridge.tamnora.com --name "PC 1"');
776
842
  return 1;
777
843
  }
@@ -842,7 +908,7 @@ async function cmdPair(argv) {
842
908
  return 0;
843
909
  }
844
910
  console.log('Arrancando el puente (Ctrl+C para salir)...');
845
- return cmdBridge([]);
911
+ return cmdBridge(bridgeStartArgs(flags));
846
912
  }
847
913
 
848
914
  // ---------------------------------------------------------------------------
@@ -1062,12 +1128,18 @@ async function cmdAutostart(argv) {
1062
1128
  }
1063
1129
  try { fs.mkdirSync(path.dirname(target), { recursive: true }); } catch (e) { /* nada */ }
1064
1130
  const home = paths.baseDir();
1131
+ // Si esta PC es un puente de un hub remoto (join/pair), el autostart corre
1132
+ // solo el puente; si es el hub local, corre el server.
1133
+ let acfg = { apiUrl: '' };
1134
+ try { acfg = config.readBridge(); } catch (e) { /* nada */ }
1135
+ const remote = /^https?:\/\//i.test(acfg.apiUrl || '') && !/127\.0\.0\.1|localhost/i.test(acfg.apiUrl);
1136
+ const runArgs = remote ? 'bridge' : 'server --stream --no-tunnel';
1065
1137
  if (process.platform === 'win32') {
1066
1138
  const ps = [
1067
1139
  '$ws = New-Object -ComObject WScript.Shell;',
1068
1140
  '$sc = $ws.CreateShortcut(' + JSON.stringify(target) + ');',
1069
1141
  '$sc.TargetPath = ' + JSON.stringify(node) + ';',
1070
- '$sc.Arguments = ' + JSON.stringify('"' + bin + '" server --stream --no-tunnel') + ';',
1142
+ '$sc.Arguments = ' + JSON.stringify('"' + bin + '" ' + runArgs) + ';',
1071
1143
  '$sc.WorkingDirectory = ' + JSON.stringify(home) + ';',
1072
1144
  '$sc.WindowStyle = 7;',
1073
1145
  '$sc.Save();',
@@ -1080,7 +1152,8 @@ async function cmdAutostart(argv) {
1080
1152
  + '<plist version="1.0"><dict>'
1081
1153
  + '<key>Label</key><string>net.openbridge.server</string>'
1082
1154
  + '<key>ProgramArguments</key><array>'
1083
- + '<string>' + node + '</string><string>' + bin + '</string><string>server</string><string>--stream</string><string>--no-tunnel</string>'
1155
+ + '<string>' + node + '</string><string>' + bin + '</string>'
1156
+ + runArgs.split(' ').map((a) => '<string>' + a + '</string>').join('')
1084
1157
  + '</array>'
1085
1158
  + '<key>WorkingDirectory</key><string>' + home + '</string>'
1086
1159
  + '<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>'
@@ -1089,7 +1162,7 @@ async function cmdAutostart(argv) {
1089
1162
  } else {
1090
1163
  const unit = '[Unit]\nDescription=OpenBridge\nAfter=network-online.target\n\n'
1091
1164
  + '[Service]\nType=simple\n'
1092
- + 'ExecStart=' + node + ' ' + bin + ' server --stream --no-tunnel\n'
1165
+ + 'ExecStart=' + node + ' ' + bin + ' ' + runArgs + '\n'
1093
1166
  + 'WorkingDirectory=' + home + '\nRestart=on-failure\n\n'
1094
1167
  + '[Install]\nWantedBy=default.target\n';
1095
1168
  fs.writeFileSync(target, unit);
@@ -1097,7 +1170,7 @@ async function cmdAutostart(argv) {
1097
1170
  spawnSync('systemctl', ['--user', 'enable', '--now', 'openbridge.service'], { windowsHide: true });
1098
1171
  }
1099
1172
  console.log('autostart instalado: ' + target);
1100
- console.log(' (arranca `openbridge server --stream --no-tunnel` en ' + home + ')');
1173
+ console.log(' (arranca `openbridge ' + runArgs + '` en ' + home + ')');
1101
1174
  return 0;
1102
1175
  }
1103
1176
 
@@ -1202,7 +1275,7 @@ function usage() {
1202
1275
  console.log(' qr Muestra la URL (publica o local) como QR para el celular');
1203
1276
  console.log(' tunnel Muestra o cambia el proveedor de tunel (tunnelmole|ngrok|cloudflare|none) [--domain]');
1204
1277
  console.log(' logs Ultimas lineas de los logs (--follow --server --bridge)');
1205
- console.log(' bridge Corre solo el puente (--api --token --id --name)');
1278
+ console.log(' bridge Corre solo el puente (--background | --stop | --status)');
1206
1279
  console.log(' join Vincula esta PC como puente de un hub (<url> --token --id --name)');
1207
1280
  console.log(' pair Empareja esta PC con un hub PHP por codigo (<url> [--id --name])');
1208
1281
  console.log(' import Trae data/ de OpenConex (<data-dir> [--force])');