@danieltmn/openbridge 0.1.0 → 0.3.0

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,27 @@ 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.3.0] - 2026-09-13
8
+
9
+ ### Cambios
10
+
11
+ - web: revertir un archivo puntual desde la vista cambios
12
+ - cli: comando tunnel (estado/cambio de proveedor y dominio fijo)
13
+ - web: plantillas de prompts y boton de revertir cambios
14
+ - web: vista de cambios del proyecto (git status/diff) via el puente
15
+ - release: recupera flags que npm se queda (--yes, --tag, --dry-run, --preid)
16
+
17
+ ## [0.2.0] - 2026-09-13
18
+
19
+ ### Cambios
20
+
21
+ - web: muestra el costo acumulado por sesion y total
22
+ - cli: QR de la URL (init/status/server y comando qr)
23
+ - web: dictado por voz en el compositor (Web Speech API)
24
+ - release: exporta logica de version/changelog y agrega tests
25
+ - release: proceso privado con guard de dueno, semver y dist-tags
26
+ - UI: porcentaje de contexto, boton ver archivos y badge plan; refresco de tokens al responder
27
+
7
28
  ## [0.1.0] - 2026-09-11
8
29
 
9
30
  Primera version publicada. Paquete npm **`@danieltmn/openbridge`** y repo
package/README.md CHANGED
@@ -1,143 +1,146 @@
1
- # OpenBridge
2
-
3
- [![npm](https://img.shields.io/npm/v/@danieltmn/openbridge.svg)](https://www.npmjs.com/package/@danieltmn/openbridge)
4
- [![CI](https://github.com/tamnora/openbridge/actions/workflows/ci.yml/badge.svg)](https://github.com/tamnora/openbridge/actions/workflows/ci.yml)
5
- [![license](https://img.shields.io/npm/l/@danieltmn/openbridge.svg)](LICENSE)
6
-
7
- Tu **opencode** en el celular, **sin hosting**: corre la app, el puente y un
8
- túnel público desde tu PC. El celular entra por la URL del túnel y vos manejás
9
- opencode (tus carpetas, modelos y chats) desde donde estés.
10
-
11
- > Proyecto nuevo e independiente, portado a **Node** desde OpenConex (PHP).
12
- > Ya no hace falta subir nada a un hosting ni depender de uno.
13
-
14
- ## Cómo funciona
15
-
16
- ```
17
- [Celular] ──► URL del túnel ──► App Node en tu PC ──► .openbridge/data/ (JSON)
18
-
19
- └──► bridge (Node) ──► opencode run
20
- ```
21
-
22
- - **Un solo runtime: Node 18+.** La app web, la API, los datos y el puente viven
23
- en el mismo proceso/hogar.
24
- - **opencode** es el único requisito externo (el motor que responde).
25
- - **Túnel** con TunnelMole (gratis, sin cuenta), ngrok o Cloudflare para exponer
26
- la app a internet.
27
-
28
- ## Requisitos
29
-
30
- - [Node.js](https://nodejs.org) 18 o superior.
31
- - `opencode` instalado y autenticado: `npm i -g opencode-ai`.
32
-
33
- ## Instalación
34
-
35
- ```bash
36
- # opción 1: sin instalar nada (npx)
37
- npx @danieltmn/openbridge init # asistente: workspace, contraseña, túnel, puerto
38
- npx @danieltmn/openbridge server # arranca en segundo plano y muestra el estado/URL
39
-
40
- # opción 2: instalar global
41
- npm i -g @danieltmn/openbridge
42
- openbridge init
43
- openbridge server
44
- ```
45
-
46
- Al arrancar (queda en segundo plano) imprime el **estado con la URL pública**
47
- (`https://….tunnelmole.net/chat.php`): abríla desde el celular y logueate con el
48
- usuario (`admin`) y la contraseña que elegiste en `init`. Para correrlo en
49
- primer plano usá `openbridge server --stream`; para detenerlo, `openbridge stop`.
50
-
51
- ### Opciones de `init`
52
-
53
- ```
54
- --workspace <ruta> carpeta de trabajo (donde están tus proyectos)
55
- --name "PC 1" nombre visible de esta computadora
56
- --id pc1 identificador corto (default: derivado del nombre)
57
- --port 8799 puerto local
58
- --password <clave> contraseña de acceso
59
- --tunnel <prov> tunnelmole | ngrok | cloudflare | none
60
- --yes sin preguntas (usa defaults)
61
- --force reconfigura aunque ya exista
62
- --dir <ruta> casa portable (default: directorio actual)
63
- ```
64
-
65
- ## Comandos
66
-
67
- | Comando | Qué hace |
68
- |---|---|
69
- | `openbridge init` | Configura la casa (workspace, contraseña, túnel, puerto) |
70
- | `openbridge passwd` | Cambia la contraseña de acceso (detiene el server si corre) |
71
- | `openbridge server` | Arranca app + puente + túnel en **segundo plano** y muestra el estado (`--stream` = primer plano) |
72
- | `openbridge stop` | Detiene el server y su árbol de procesos |
73
- | `openbridge status` | Estado, URL, puente en línea y chats |
74
- | `openbridge logs` | Logs (`--follow`, `--server`, `--bridge`) |
75
- | `openbridge bridge` | Corre **solo** el puente (`--api --token --id --name`) |
76
- | `openbridge import` | Trae `data/` de OpenConex (`<data-dir> [--force]`) |
77
- | `openbridge reset` | Borra chats/datos (`--session <id>`, `--yes`) |
78
- | `openbridge autostart` | Arranque automático (`install`/`remove`) |
79
- | `openbridge doctor` | Verifica Node, opencode, configuración y puerto |
80
-
81
- ## Casa portable
82
-
83
- Todo vive dentro de **`.openbridge/`** en el directorio donde corrés `init`
84
- (o `--dir` / `OPENBRIDGE_HOME`):
85
-
86
- ```
87
- .openbridge/
88
- config.json config del puente (apiUrl, token, workspace, bridgeId…)
89
- app.json config de la app (usuario, contraseña, VAPID, puerto, túnel)
90
- folders.json lista blanca de carpetas que se ven desde el celular
91
- data/ sesiones, mensajes, catálogo, registro de puentes, push
92
- logs/ server.log y bridge.log
93
- ```
94
-
95
- Movés la carpeta a donde quieras y sigue funcionando (es portable). Si venías
96
- de una versión anterior con los archivos sueltos, se **migran solos** a
97
- `.openbridge/` la primera vez que corras un comando.
98
-
99
- ## Varias computadoras
100
-
101
- El hub (la PC con la app) puede atender **varias PCs** a la vez:
102
-
103
- - En la PC "hub": `openbridge init` + `openbridge server` (queda la app + túnel).
104
- - En otra PC: `openbridge init` y luego `openbridge bridge --api <url-del-hub>
105
- --token <token> --id pc2 --name "PC 2"` (sin editar `config.json` a mano).
106
-
107
- En el sidebar de la web elegís qué PC usar y ves sus proyectos y chats.
108
-
109
- ## Túnel y URL estable
110
-
111
- Proveedores soportados (`--tunnel`): `tunnelmole` (gratis, sin cuenta),
112
- `ngrok` (cuenta + authtoken; admite dominio fijo con `--domain` en
113
- `app.json`) y `cloudflare` (quick tunnel de `cloudflared`). Con URL
114
- **aleatoria**, la suscripción push no persiste entre reinicios (el push está
115
- atado al origen). Para PWA/push estables conviene ngrok con dominio fijo o
116
- Cloudflare con dominio propio.
117
-
118
- ## Avisos push
119
-
120
- Las claves VAPID se generan solas en `init`. Con URL de túnel **aleatoria**, la
121
- suscripción push no persiste entre reinicios (el push está atado al origen). Para
122
- una URL fija hace falta un proveedor con dominio estable (ngrok/Cloudflare).
123
-
124
- ## Seguridad
125
-
126
- - La app escucha solo en `127.0.0.1`; el túnel la expone.
127
- - Login con contraseña (scrypt) + CSRF y rate limit (5 intentos); token del
128
- puente autogenerado.
129
- - El túnel es **público mientras corre**: detenelo (`openbridge stop`) cuando no
130
- lo uses.
131
-
132
- ## Estado
133
-
134
- Proyecto en desarrollo. Ya funciona: `init`, `server` (segundo plano; `--stream`
135
- en primer plano), `stop`, `status`, `logs` (`--follow`), `bridge` (con flags),
136
- `passwd`, `import`, `reset`, `autostart`, `doctor`, login con rate limit, API
137
- completa, SSE, catálogo por PC, Web Push y túnel (TunnelMole/ngrok/cloudflare).
138
- Tests en `npm test`. Pendiente: re-suscripción push al cambiar la URL y más
139
- cobertura end-to-end.
140
-
141
- ## Licencia
142
-
143
- MIT.
1
+ # OpenBridge
2
+
3
+ [![npm](https://img.shields.io/npm/v/@danieltmn/openbridge.svg)](https://www.npmjs.com/package/@danieltmn/openbridge)
4
+ [![CI](https://github.com/tamnora/openbridge/actions/workflows/ci.yml/badge.svg)](https://github.com/tamnora/openbridge/actions/workflows/ci.yml)
5
+ [![license](https://img.shields.io/npm/l/@danieltmn/openbridge.svg)](LICENSE)
6
+
7
+ Tu **opencode** en el celular, **sin hosting**: corre la app, el puente y un
8
+ túnel público desde tu PC. El celular entra por la URL del túnel y vos manejás
9
+ opencode (tus carpetas, modelos y chats) desde donde estés.
10
+
11
+ > Proyecto nuevo e independiente, portado a **Node** desde OpenConex (PHP).
12
+ > Ya no hace falta subir nada a un hosting ni depender de uno.
13
+
14
+ ## Cómo funciona
15
+
16
+ ```
17
+ [Celular] ──► URL del túnel ──► App Node en tu PC ──► .openbridge/data/ (JSON)
18
+
19
+ └──► bridge (Node) ──► opencode run
20
+ ```
21
+
22
+ - **Un solo runtime: Node 18+.** La app web, la API, los datos y el puente viven
23
+ en el mismo proceso/hogar.
24
+ - **opencode** es el único requisito externo (el motor que responde).
25
+ - **Túnel** con TunnelMole (gratis, sin cuenta), ngrok o Cloudflare para exponer
26
+ la app a internet.
27
+
28
+ ## Requisitos
29
+
30
+ - [Node.js](https://nodejs.org) 18 o superior.
31
+ - `opencode` instalado y autenticado: `npm i -g opencode-ai`.
32
+
33
+ ## Instalación
34
+
35
+ ```bash
36
+ # opción 1: sin instalar nada (npx)
37
+ npx @danieltmn/openbridge init # asistente: workspace, contraseña, túnel, puerto
38
+ npx @danieltmn/openbridge server # arranca en segundo plano y muestra el estado/URL
39
+
40
+ # opción 2: instalar global
41
+ npm i -g @danieltmn/openbridge
42
+ openbridge init
43
+ openbridge server
44
+ ```
45
+
46
+ Al arrancar (queda en segundo plano) imprime el **estado con la URL pública**
47
+ (`https://….tunnelmole.net/chat.php`): abríla desde el celular y logueate con el
48
+ usuario (`admin`) y la contraseña que elegiste en `init`. Para correrlo en
49
+ primer plano usá `openbridge server --stream`; para detenerlo, `openbridge stop`.
50
+
51
+ ### Opciones de `init`
52
+
53
+ ```
54
+ --workspace <ruta> carpeta de trabajo (donde están tus proyectos)
55
+ --name "PC 1" nombre visible de esta computadora
56
+ --id pc1 identificador corto (default: derivado del nombre)
57
+ --port 8799 puerto local
58
+ --password <clave> contraseña de acceso
59
+ --tunnel <prov> tunnelmole | ngrok | cloudflare | none
60
+ --domain <host> dominio fijo del túnel (ngrok)
61
+ --yes sin preguntas (usa defaults)
62
+ --force reconfigura aunque ya exista
63
+ --dir <ruta> casa portable (default: directorio actual)
64
+ ```
65
+
66
+ ## Comandos
67
+
68
+ | Comando | Qué hace |
69
+ |---|---|
70
+ | `openbridge init` | Configura la casa (workspace, contraseña, túnel, puerto) |
71
+ | `openbridge passwd` | Cambia la contraseña de acceso (detiene el server si corre) |
72
+ | `openbridge server` | Arranca app + puente + túnel en **segundo plano** y muestra el estado (`--stream` = primer plano) |
73
+ | `openbridge stop` | Detiene el server y su árbol de procesos |
74
+ | `openbridge status` | Estado, URL, puente en línea y chats |
75
+ | `openbridge qr` | Muestra la URL (pública o local) como QR para escanear desde el celular |
76
+ | `openbridge tunnel` | Muestra o cambia el proveedor de túnel y su dominio fijo (`--domain`) |
77
+ | `openbridge logs` | Logs (`--follow`, `--server`, `--bridge`) |
78
+ | `openbridge bridge` | Corre **solo** el puente (`--api --token --id --name`) |
79
+ | `openbridge import` | Trae `data/` de OpenConex (`<data-dir> [--force]`) |
80
+ | `openbridge reset` | Borra chats/datos (`--session <id>`, `--yes`) |
81
+ | `openbridge autostart` | Arranque automático (`install`/`remove`) |
82
+ | `openbridge doctor` | Verifica Node, opencode, configuración y puerto |
83
+
84
+ ## Casa portable
85
+
86
+ Todo vive dentro de **`.openbridge/`** en el directorio donde corrés `init`
87
+ (o `--dir` / `OPENBRIDGE_HOME`):
88
+
89
+ ```
90
+ .openbridge/
91
+ config.json config del puente (apiUrl, token, workspace, bridgeId…)
92
+ app.json config de la app (usuario, contraseña, VAPID, puerto, túnel)
93
+ folders.json lista blanca de carpetas que se ven desde el celular
94
+ data/ sesiones, mensajes, catálogo, registro de puentes, push
95
+ logs/ server.log y bridge.log
96
+ ```
97
+
98
+ Movés la carpeta a donde quieras y sigue funcionando (es portable). Si venías
99
+ de una versión anterior con los archivos sueltos, se **migran solos** a
100
+ `.openbridge/` la primera vez que corras un comando.
101
+
102
+ ## Varias computadoras
103
+
104
+ El hub (la PC con la app) puede atender **varias PCs** a la vez:
105
+
106
+ - En la PC "hub": `openbridge init` + `openbridge server` (queda la app + túnel).
107
+ - En otra PC: `openbridge init` y luego `openbridge bridge --api <url-del-hub>
108
+ --token <token> --id pc2 --name "PC 2"` (sin editar `config.json` a mano).
109
+
110
+ En el sidebar de la web elegís qué PC usar y ves sus proyectos y chats.
111
+
112
+ ## Túnel y URL estable
113
+
114
+ Proveedores soportados (`--tunnel`): `tunnelmole` (gratis, sin cuenta),
115
+ `ngrok` (cuenta + authtoken; admite dominio fijo con `--domain` en
116
+ `app.json`) y `cloudflare` (quick tunnel de `cloudflared`). Con URL
117
+ **aleatoria**, la suscripción push no persiste entre reinicios (el push está
118
+ atado al origen). Para PWA/push estables conviene ngrok con dominio fijo o
119
+ Cloudflare con dominio propio.
120
+
121
+ ## Avisos push
122
+
123
+ Las claves VAPID se generan solas en `init`. Con URL de túnel **aleatoria**, la
124
+ suscripción push no persiste entre reinicios (el push está atado al origen). Para
125
+ una URL fija hace falta un proveedor con dominio estable (ngrok/Cloudflare).
126
+
127
+ ## Seguridad
128
+
129
+ - La app escucha solo en `127.0.0.1`; el túnel la expone.
130
+ - Login con contraseña (scrypt) + CSRF y rate limit (5 intentos); token del
131
+ puente autogenerado.
132
+ - El túnel es **público mientras corre**: detenelo (`openbridge stop`) cuando no
133
+ lo uses.
134
+
135
+ ## Estado
136
+
137
+ Proyecto en desarrollo. Ya funciona: `init`, `server` (segundo plano; `--stream`
138
+ en primer plano), `stop`, `status`, `logs` (`--follow`), `bridge` (con flags),
139
+ `passwd`, `import`, `reset`, `autostart`, `doctor`, login con rate limit, API
140
+ completa, SSE, catálogo por PC, Web Push y túnel (TunnelMole/ngrok/cloudflare).
141
+ Tests en `npm test`. Pendiente: re-suscripción push al cambiar la URL y más
142
+ cobertura end-to-end.
143
+
144
+ ## Licencia
145
+
146
+ MIT.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieltmn/openbridge",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
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",
@@ -43,8 +43,16 @@
43
43
  "scripts": {
44
44
  "start": "node bin/openbridge.js server",
45
45
  "test": "node --test",
46
+ "release": "node scripts/release.mjs",
47
+ "release:dry": "node scripts/release.mjs --dry-run",
46
48
  "prepublishOnly": "npm test"
47
49
  },
50
+ "release": {
51
+ "npmOwner": "danieltmn",
52
+ "gitRemote": "tamnora/openbridge",
53
+ "gitOwner": "tamnora",
54
+ "branch": "master"
55
+ },
48
56
  "publishConfig": {
49
57
  "access": "public"
50
58
  },
@@ -12,7 +12,7 @@
12
12
  */
13
13
  'use strict';
14
14
 
15
- const { spawn } = require('node:child_process');
15
+ const { spawn, spawnSync } = require('node:child_process');
16
16
  const fs = require('node:fs');
17
17
  const os = require('node:os');
18
18
  const path = require('node:path');
@@ -1549,6 +1549,77 @@ async function handleFsCommand(cmd) {
1549
1549
  }
1550
1550
  }
1551
1551
 
1552
+ // ---------------------------------------------------------------------------
1553
+ // Cambios del proyecto (git status / git diff) para revisar lo que tocó el
1554
+ // agente desde el celular. Corre git en la carpeta de la sesión (validada
1555
+ // dentro del workspace, igual que proc_start).
1556
+ // ---------------------------------------------------------------------------
1557
+ function parseGitStatus(text) {
1558
+ const files = [];
1559
+ let branch = '';
1560
+ for (const line of String(text).split('\n')) {
1561
+ if (line === '') continue;
1562
+ if (line.startsWith('## ')) { branch = line.slice(3).trim(); continue; }
1563
+ const status = line.slice(0, 2).trim() || '?';
1564
+ let p = line.slice(3);
1565
+ const arrow = p.indexOf(' -> ');
1566
+ if (arrow >= 0) p = p.slice(arrow + 4);
1567
+ files.push({ status, path: p });
1568
+ }
1569
+ return { branch, files };
1570
+ }
1571
+
1572
+ function gitIn(folder, args, timeout = 30000) {
1573
+ const r = spawnSync('git', ['-C', folder].concat(args), {
1574
+ encoding: 'utf8',
1575
+ timeout,
1576
+ windowsHide: true,
1577
+ maxBuffer: 16 * 1024 * 1024,
1578
+ });
1579
+ if (r.error) throw new Error(r.error.message || 'no se pudo ejecutar git');
1580
+ const text = String(r.stdout || '');
1581
+ return { code: r.status, text, err: String(r.stderr || '').trim() };
1582
+ }
1583
+
1584
+ async function handleGitCommand(cmd) {
1585
+ let folder;
1586
+ try { folder = procResolveFolder(cmd.args && cmd.args[0]); }
1587
+ catch (e) {
1588
+ await api('command_done', { id: cmd.id, ok: false, text: '', error: e.message }, cmd._t);
1589
+ return;
1590
+ }
1591
+ try {
1592
+ if (cmd.name === 'git_status') {
1593
+ const r = gitIn(folder, ['status', '--porcelain=v1', '-b', '--untracked-files=all']);
1594
+ if (r.code !== 0) throw new Error(r.err || 'no es un repositorio git');
1595
+ await api('command_done', { id: cmd.id, ok: true, text: JSON.stringify(parseGitStatus(r.text)), error: '' }, cmd._t);
1596
+ } else if (cmd.name === 'git_checkout') {
1597
+ // Revierte cambios de archivos RASTREADOS dentro de la carpeta de la
1598
+ // sesion. Sin 2do argumento revierte todo (`.`); con argumento,
1599
+ // solo ese archivo relativo (sin `..` ni rutas absolutas).
1600
+ let target = (cmd.args && cmd.args.length > 1) ? String(cmd.args[1]) : '.';
1601
+ if (target !== '.') {
1602
+ if (path.isAbsolute(target) || target.split(/[\\/]/).includes('..')) {
1603
+ throw new Error('ruta invalida');
1604
+ }
1605
+ }
1606
+ const r = gitIn(folder, ['checkout', '--', target]);
1607
+ if (r.code !== 0) throw new Error(r.err || 'no se pudo revertir');
1608
+ await api('command_done', { id: cmd.id, ok: true, text: JSON.stringify({ reverted: target }), error: '' }, cmd._t);
1609
+ } else {
1610
+ // diff HEAD: cambios preparados + sin preparar. `fs_result` admite
1611
+ // respuestas mas grandes que command_done (8000 car.).
1612
+ const r = gitIn(folder, ['--no-pager', 'diff', 'HEAD', '--no-color', '--no-ext-diff', '--no-renames']);
1613
+ if (r.code !== 0) throw new Error(r.err || 'no es un repositorio git');
1614
+ await api('fs_result', { id: cmd.id, ok: true, text: JSON.stringify({ diff: r.text }), error: '' }, cmd._t);
1615
+ }
1616
+ log('git ' + cmd.name + ' ok en ' + folder);
1617
+ } catch (e) {
1618
+ await api('command_done', { id: cmd.id, ok: false, text: '', error: e.message }, cmd._t);
1619
+ log('git ' + cmd.name + ' error: ' + e.message);
1620
+ }
1621
+ }
1622
+
1552
1623
  // ---------------------------------------------------------------------------
1553
1624
  // Procesa comandos read-only encolados por la web.
1554
1625
  // ---------------------------------------------------------------------------
@@ -2044,6 +2115,11 @@ async function handleCommand(cmd) {
2044
2115
  await handleFsCommand(cmd);
2045
2116
  return;
2046
2117
  }
2118
+ // Cambios git del proyecto (status/diff/revertir).
2119
+ if (cmd.name === 'git_status' || cmd.name === 'git_diff' || cmd.name === 'git_checkout') {
2120
+ await handleGitCommand(cmd);
2121
+ return;
2122
+ }
2047
2123
  // Túneles también son del puente (procesos de esta PC).
2048
2124
  if (cmd.name === 'tunnel_start') { await tunnelStart(cmd); return; }
2049
2125
  if (cmd.name === 'tunnel_stop') { await tunnelStop(cmd); return; }
@@ -2194,6 +2270,14 @@ async function tick(opts) {
2194
2270
  } catch (e) {
2195
2271
  log('error al publicar la respuesta #' + m.id + ': ' + e.message);
2196
2272
  }
2273
+ // Refresca tokens/costo enseguida: el barrido solo corre cada 15 min.
2274
+ if (r.opencodeSession && m._t) {
2275
+ try {
2276
+ await refreshTokens(path.resolve(m.session.folder || ''), { id: r.opencodeSession }, m._t);
2277
+ } catch (e) {
2278
+ log('aviso: no pude refrescar tokens de ' + r.opencodeSession + ': ' + e.message);
2279
+ }
2280
+ }
2197
2281
  }
2198
2282
  for (const f of folders) {
2199
2283
  await handleFolderRequest(f);
package/src/cli.js CHANGED
@@ -10,6 +10,7 @@ const config = require('./config');
10
10
  const store = require('./store');
11
11
  const auth = require('./auth');
12
12
  const push = require('./push');
13
+ const qr = require('./qr');
13
14
  const web = require('./web/server');
14
15
  const tunnel = require('./tunnel');
15
16
  const log = require('./log');
@@ -117,6 +118,15 @@ function clearRuntime() {
117
118
  }
118
119
  }
119
120
 
121
+ // Muestra la URL como QR en la consola (para abrirla en el celular).
122
+ // Devuelve false si la URL es demasiado larga para el QR.
123
+ function printQr(url) {
124
+ const art = qr.qrTerminal(url, 2);
125
+ if (!art) return false;
126
+ console.log(art);
127
+ return true;
128
+ }
129
+
120
130
  // Detiene el server en ejecucion (si lo hay) matando server + hijos. Devuelve
121
131
  // true si habia uno. Se usa antes de reconfigurar para no dejar un proceso con
122
132
  // la config vieja en memoria (p.ej. la contrasena).
@@ -234,7 +244,7 @@ async function cmdInit(argv) {
234
244
  csrfSecret,
235
245
  bridgeToken,
236
246
  vapid,
237
- tunnel: { provider, domain: '' },
247
+ tunnel: { provider, domain: flags.domain ? String(flags.domain) : '' },
238
248
  };
239
249
  config.writeBridge(bridgeCfg);
240
250
  config.writeApp(appCfg);
@@ -255,6 +265,9 @@ async function cmdInit(argv) {
255
265
  console.log(' carpetas : ' + folders.length + ' en folders.json');
256
266
  if (generated) console.log(' -> guarda esta contrasena: no se vuelve a mostrar.');
257
267
  console.log('');
268
+ console.log(' QR local (misma PC):');
269
+ printQr('http://127.0.0.1:' + port + '/chat.php');
270
+ console.log('');
258
271
  console.log('Siguiente paso: openbridge server');
259
272
  return 0;
260
273
  }
@@ -439,6 +452,12 @@ async function printStatus() {
439
452
  const sessions = await store.sessionsListFull();
440
453
  console.log(' chats : ' + sessions.length);
441
454
  } catch (e) { /* sin datos todavia */ }
455
+ const target = rt.publicUrl || rt.localUrl;
456
+ if (target) {
457
+ console.log('');
458
+ console.log(rt.publicUrl ? ' escanea para abrir en el celular:' : ' QR local (misma PC):');
459
+ if (!printQr(target)) console.log(' (URL demasiado larga para el QR; usa el link de arriba)');
460
+ }
442
461
  return true;
443
462
  }
444
463
 
@@ -447,6 +466,73 @@ async function cmdStatus() {
447
466
  return 0;
448
467
  }
449
468
 
469
+ // Muestra la URL como QR para escanear desde el celular.
470
+ async function cmdQr() {
471
+ const rt = readRuntime();
472
+ if (rt && pidAlive(rt.pid) && (rt.publicUrl || rt.localUrl)) {
473
+ const target = rt.publicUrl || rt.localUrl;
474
+ console.log((rt.publicUrl ? 'URL publica' : 'URL local') + ': ' + target);
475
+ console.log('');
476
+ if (!printQr(target)) { console.log('URL demasiado larga para el QR.'); return 1; }
477
+ return 0;
478
+ }
479
+ if (!paths.exists()) {
480
+ console.error('No hay configuracion. Corre primero: openbridge init');
481
+ return 1;
482
+ }
483
+ const app = config.readApp();
484
+ const url = 'http://' + (app.host || '127.0.0.1') + ':' + (app.port || 8799) + '/chat.php';
485
+ console.log('URL local (el server no esta corriendo): ' + url);
486
+ console.log('');
487
+ if (!printQr(url)) { console.log('URL demasiado larga para el QR.'); return 1; }
488
+ return 0;
489
+ }
490
+
491
+ // Muestra o cambia el proveedor de tunel (y su dominio fijo) sin reconfigurar
492
+ // todo. Cambiar el proveedor requiere reiniciar el server.
493
+ async function cmdTunnel(argv) {
494
+ const { flags, _ } = parseArgs(argv);
495
+ if (!paths.exists()) {
496
+ console.error('No hay configuracion. Corre primero: openbridge init');
497
+ return 1;
498
+ }
499
+ const app = config.readApp();
500
+ const cur = app.tunnel || { provider: 'tunnelmole', domain: '' };
501
+ const arg = String(_[0] || '').toLowerCase();
502
+
503
+ if (arg === '' || arg === 'status' || arg === 'show') {
504
+ const rt = readRuntime();
505
+ const running = rt && pidAlive(rt.pid);
506
+ console.log('proveedor : ' + (cur.provider || 'tunnelmole'));
507
+ console.log('dominio : ' + (cur.domain || '(aleatorio)'));
508
+ console.log('estado : ' + (running ? 'corriendo' : 'detenido'));
509
+ if (running && rt.publicUrl) console.log('publico : ' + rt.publicUrl);
510
+ return 0;
511
+ }
512
+
513
+ let provider = arg;
514
+ if (provider === 'cloudflared') provider = 'cloudflare';
515
+ if (!['tunnelmole', 'ngrok', 'cloudflare', 'none'].includes(provider)) {
516
+ console.error('Proveedor desconocido: ' + arg + ' (usar tunnelmole|ngrok|cloudflare|none)');
517
+ return 1;
518
+ }
519
+ const domain = flags.domain !== undefined ? String(flags.domain).trim() : String(cur.domain || '');
520
+ app.tunnel = { provider, domain };
521
+ config.writeApp(app);
522
+ console.log('Tunel configurado: ' + provider + (domain ? ' (dominio ' + domain + ')' : ''));
523
+ if (provider === 'ngrok' && !domain) {
524
+ console.log('aviso: ngrok sin dominio fijo da URL aleatoria; pasá --domain <sub.ngrok.app> para una estable.');
525
+ }
526
+ if (provider === 'cloudflare' && domain) {
527
+ console.log('aviso: el quick tunnel de cloudflared ignora el dominio (URL aleatoria).');
528
+ }
529
+ const rt = readRuntime();
530
+ if (rt && pidAlive(rt.pid)) {
531
+ console.log('Hay un server corriendo: reinicialo para aplicar (openbridge stop && openbridge server).');
532
+ }
533
+ return 0;
534
+ }
535
+
450
536
  async function cmdLogs(argv) {
451
537
  const { flags } = parseArgs(argv);
452
538
  const n = parseInt(flags.n || '40', 10) || 40;
@@ -743,6 +829,8 @@ function usage() {
743
829
  console.log(' passwd Cambia la contrasena de acceso (--password <clave>)');
744
830
  console.log(' stop Detiene el server en segundo plano');
745
831
  console.log(' status Estado del server, puente y chats');
832
+ console.log(' qr Muestra la URL (publica o local) como QR para el celular');
833
+ console.log(' tunnel Muestra o cambia el proveedor de tunel (tunnelmole|ngrok|cloudflare|none) [--domain]');
746
834
  console.log(' logs Ultimas lineas de los logs (--follow --server --bridge)');
747
835
  console.log(' bridge Corre solo el puente (--api --token --id --name)');
748
836
  console.log(' import Trae data/ de OpenConex (<data-dir> [--force])');
@@ -751,7 +839,7 @@ function usage() {
751
839
  console.log(' doctor Verifica Node, opencode, configuracion y puerto');
752
840
  console.log('');
753
841
  console.log('Opciones comunes: --dir <ruta> (casa portable; default: directorio actual)');
754
- console.log('init: --workspace --name --id --port --password --tunnel --yes --force');
842
+ console.log('init: --workspace --name --id --port --password --tunnel --domain --yes --force');
755
843
  console.log('server: --port --no-tunnel --stream (sin --stream corre en segundo plano)');
756
844
  }
757
845
 
@@ -773,6 +861,8 @@ async function main(argv) {
773
861
  case 'server': case 'start': return cmdServer(args.slice(1));
774
862
  case 'stop': return cmdStop();
775
863
  case 'status': return cmdStatus();
864
+ case 'qr': return cmdQr();
865
+ case 'tunnel': return cmdTunnel(args.slice(1));
776
866
  case 'logs': return cmdLogs(args.slice(1));
777
867
  case 'bridge': return cmdBridge(args.slice(1));
778
868
  case 'import': return cmdImport(args.slice(1));