@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 +21 -0
- package/README.md +146 -143
- package/package.json +9 -1
- package/src/bridge/bridge.js +85 -1
- package/src/cli.js +92 -2
- package/src/qr.js +224 -0
- package/src/web/assets/app.js +358 -9
- package/src/web/routes.js +3 -0
- package/src/web/templates/chat.html +57 -1
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
|
-
[](https://www.npmjs.com/package/@danieltmn/openbridge)
|
|
4
|
-
[](https://github.com/tamnora/openbridge/actions/workflows/ci.yml)
|
|
5
|
-
[](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
|
-
--
|
|
61
|
-
--
|
|
62
|
-
--
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
| `openbridge
|
|
71
|
-
| `openbridge
|
|
72
|
-
| `openbridge
|
|
73
|
-
| `openbridge
|
|
74
|
-
| `openbridge
|
|
75
|
-
| `openbridge
|
|
76
|
-
| `openbridge
|
|
77
|
-
| `openbridge
|
|
78
|
-
| `openbridge
|
|
79
|
-
| `openbridge
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
En
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
# OpenBridge
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@danieltmn/openbridge)
|
|
4
|
+
[](https://github.com/tamnora/openbridge/actions/workflows/ci.yml)
|
|
5
|
+
[](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.
|
|
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
|
},
|
package/src/bridge/bridge.js
CHANGED
|
@@ -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));
|