bridgeaibrasil 0.1.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/README.md +153 -0
- package/bin/bridgeai.js +105 -0
- package/package.json +39 -0
- package/scripts/abrir.js +98 -0
- package/scripts/login.js +155 -0
- package/scripts/portas.js +398 -0
- package/scripts/tunnel.js +500 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// A ponta local do túnel da BridgeAI.
|
|
3
|
+
//
|
|
4
|
+
// Abre um `127.0.0.1:55432` na máquina da pessoa e liga cada conexão que chegar
|
|
5
|
+
// nele ao banco do projeto, por dentro do WebSocket do servidor MCP. Para o
|
|
6
|
+
// Prisma, o `psql` e qualquer driver, aquilo é um Postgres comum na máquina.
|
|
7
|
+
//
|
|
8
|
+
// node tunnel.js --dev (todos os projetos)
|
|
9
|
+
// node tunnel.js --app loja-do-joao --environment staging (um ambiente)
|
|
10
|
+
//
|
|
11
|
+
// ⚠️ **`--dev` abre UM túnel para todos os seus projetos**, e é o caminho normal
|
|
12
|
+
// desde 06/09/2026: o ambiente de desenvolvimento é da PESSOA, não do projeto.
|
|
13
|
+
// Dá certo porque o destino é sempre o mesmo servidor e **o Postgres escolhe o
|
|
14
|
+
// database na mensagem de abertura da conexão** — o `DATABASE_URL` de cada
|
|
15
|
+
// projeto aponta para o banco dele, pela mesma porta. Uma porta por projeto era
|
|
16
|
+
// mais um processo para rodar e mais uma coisa para explicar.
|
|
17
|
+
//
|
|
18
|
+
// Por que ele existe: o banco da plataforma só tem endereço interno, e publicar
|
|
19
|
+
// a porta dele seria expor banco de cliente à internet. O detalhe inteiro está
|
|
20
|
+
// em `mcp/src/tunnel.ts`.
|
|
21
|
+
//
|
|
22
|
+
// ⚠️ **A porta padrão NÃO é a 5432, e essa escolha é de segurança de dado.**
|
|
23
|
+
// A 5432 é a porta do Postgres de todo mundo. Numa máquina que já tinha um
|
|
24
|
+
// Postgres em Docker os dois subiam juntos sem reclamar: no Windows, ligar em
|
|
25
|
+
// `127.0.0.1:5432` com um `0.0.0.0:5432` existente é permitido, e o endereço
|
|
26
|
+
// específico vence. O perigo não é a colisão — é o silêncio ao contrário: se o
|
|
27
|
+
// túnel cair enquanto o app roda, a próxima conexão cai no Postgres local, sem
|
|
28
|
+
// erro nenhum, e a migration seguinte vai para o banco errado.
|
|
29
|
+
//
|
|
30
|
+
// Por isso, além da 55432: este script RECUSA subir se já houver alguém
|
|
31
|
+
// atendendo na porta, em vez de confiar no `EADDRINUSE` — que no Windows não
|
|
32
|
+
// acontece. E confere que o outro lado responde antes de dizer "aberto".
|
|
33
|
+
//
|
|
34
|
+
// ⚠️ Zero dependência, de propósito. Este arquivo roda na máquina do usuário,
|
|
35
|
+
// que pode não ter `node_modules` nenhum — e mandar alguém rodar `npm install`
|
|
36
|
+
// antes de conseguir desenvolver é exatamente o degrau que o produto promete
|
|
37
|
+
// tirar. Por isso `WebSocket` global (Node 22) e `node:net`, e nada além disso.
|
|
38
|
+
//
|
|
39
|
+
// ⚠️ Como o `WebSocket` do Node não deixa pôr cabeçalho, o token viaja no
|
|
40
|
+
// subprotocolo — o servidor aceita os dois caminhos. Ver TOKEN_PROTOCOL_PREFIX
|
|
41
|
+
// em `mcp/src/tunnel.ts`.
|
|
42
|
+
|
|
43
|
+
const net = require('node:net');
|
|
44
|
+
const { readFileSync } = require('node:fs');
|
|
45
|
+
const { resolve } = require('node:path');
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* O registro de portas locais (`~/.bridgeai/portas.json`).
|
|
49
|
+
*
|
|
50
|
+
* O túnel se anota nele ao subir e se apaga ao sair. É o processo que mais fica
|
|
51
|
+
* esquecido rodando — e o único cuja porta, tomada por outro, faz o projeto
|
|
52
|
+
* gravar no banco errado sem erro nenhum. Deixar isso para o Claude lembrar de
|
|
53
|
+
* anotar seria confiar numa frase; anotar aqui é um fato.
|
|
54
|
+
*
|
|
55
|
+
* Envolvido em try/catch porque **um registro quebrado não pode impedir alguém
|
|
56
|
+
* de desenvolver** — é a mesma regra de ouro dos hooks deste plugin.
|
|
57
|
+
*/
|
|
58
|
+
let portas = null;
|
|
59
|
+
try {
|
|
60
|
+
portas = require('./portas.js');
|
|
61
|
+
} catch {
|
|
62
|
+
portas = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function anota(fn) {
|
|
66
|
+
try {
|
|
67
|
+
if (portas) fn(portas);
|
|
68
|
+
} catch {
|
|
69
|
+
// silêncio: anotar é conveniência, o túnel é o serviço
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const PADRAO_MCP = 'https://mcp.bridgeaibrasil.com.br';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A porta local. Longe da 5432 de propósito — ver o aviso no topo.
|
|
77
|
+
*
|
|
78
|
+
* Ao mudar este número, mude junto o `porta` do `credentials` em
|
|
79
|
+
* `mcp/src/real-source.ts`: é ele que escreve o `.env` que aponta para cá. Os
|
|
80
|
+
* dois discordarem dá "connection refused" num arquivo que a plataforma gerou.
|
|
81
|
+
*/
|
|
82
|
+
const PORTA_PADRAO = 55432;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A porta local do cache, quando o app tem um. Longe da 6379 pelo mesmo
|
|
86
|
+
* motivo, e tem que casar com `portaCache` no `credentials` de
|
|
87
|
+
* `mcp/src/real-source.ts`.
|
|
88
|
+
*/
|
|
89
|
+
const PORTA_CACHE_PADRAO = 56379;
|
|
90
|
+
|
|
91
|
+
/** Quanto esperar pelo aperto de mão com o servidor antes de desistir. */
|
|
92
|
+
const ESPERA_MS = 10_000;
|
|
93
|
+
|
|
94
|
+
/** O subprotocolo que o servidor exige. Ver PROTOCOL em `mcp/src/tunnel.ts`. */
|
|
95
|
+
const PROTOCOLO = 'bridgeai.tunnel.v1';
|
|
96
|
+
|
|
97
|
+
function args(argv) {
|
|
98
|
+
const out = {};
|
|
99
|
+
for (let i = 0; i < argv.length; i++) {
|
|
100
|
+
const a = argv[i];
|
|
101
|
+
if (a.startsWith('--')) out[a.slice(2)] = argv[i + 1] ?? '';
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Lê uma variável do `.env` do projeto, sem depender de biblioteca.
|
|
108
|
+
*
|
|
109
|
+
* ⚠️ É por aqui que o túnel funciona para quem entrou pelo caminho
|
|
110
|
+
* RECOMENDADO. O login por OAuth (`/mcp` → bridgeai) guarda o acesso dentro do
|
|
111
|
+
* cliente MCP, e este arquivo é um programa separado: ele não alcança aquele
|
|
112
|
+
* token, e o ambiente não tem nenhum. O `dev_credentials` resolve gravando um
|
|
113
|
+
* `BRIDGEAI_TUNNEL_TOKEN` no `.env` — de escopo restrito e curta duração —, e
|
|
114
|
+
* o que falta é alguém lê-lo.
|
|
115
|
+
*
|
|
116
|
+
* Não sobrescreve o ambiente: quem exportou uma variável quis aquilo.
|
|
117
|
+
*/
|
|
118
|
+
function doEnvDoProjeto(chave) {
|
|
119
|
+
for (const nome of ['.env', '.env.local']) {
|
|
120
|
+
let bruto;
|
|
121
|
+
try {
|
|
122
|
+
bruto = readFileSync(resolve(process.cwd(), nome), 'utf8');
|
|
123
|
+
} catch {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const linha of bruto.split(/\r?\n/)) {
|
|
127
|
+
const m = linha.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
|
|
128
|
+
if (!m || m[1] !== chave) continue;
|
|
129
|
+
// Aspas em volta são convenção de arquivo `.env`, não parte do valor.
|
|
130
|
+
return m[2].trim().replace(/^(['"])([\s\S]*)\1$/, '$2');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const opt = args(process.argv.slice(2));
|
|
137
|
+
// `--dev` é o modo conta: sem projeto, um túnel só. Sem `--app` e sem `--dev`
|
|
138
|
+
// também cai nele, porque é o que a pessoa quer em 99% das vezes — e a recusa
|
|
139
|
+
// por falta de `--app` mandava quem tem três projetos abrir três túneis.
|
|
140
|
+
const modoConta = 'dev' in opt || !opt.app;
|
|
141
|
+
const app = modoConta ? null : opt.app;
|
|
142
|
+
const environment = modoConta ? 'dev' : opt.environment || 'dev';
|
|
143
|
+
const porta = Number(opt.port || PORTA_PADRAO);
|
|
144
|
+
const base = (opt.url || process.env.BRIDGEAI_MCP_URL || PADRAO_MCP).replace(/\/+$/, '');
|
|
145
|
+
|
|
146
|
+
// A ordem é do mais explícito para o mais implícito: o que veio no comando
|
|
147
|
+
// ganha de tudo, o ambiente vem depois, e o `.env` é a reserva — que na prática
|
|
148
|
+
// é o caso comum de quem entrou por OAuth.
|
|
149
|
+
const token =
|
|
150
|
+
opt.token ||
|
|
151
|
+
process.env.BRIDGEAI_TOKEN ||
|
|
152
|
+
process.env.BRIDGEAI_TUNNEL_TOKEN ||
|
|
153
|
+
doEnvDoProjeto('BRIDGEAI_TUNNEL_TOKEN');
|
|
154
|
+
|
|
155
|
+
function morre(mensagem) {
|
|
156
|
+
console.error(mensagem);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!token) {
|
|
161
|
+
// ⚠️ Esta mensagem dizia "rode `npm run login`" — um comando do repositório
|
|
162
|
+
// PRIVADO da plataforma, que quem instalou o plugin não tem. Ela mandava a
|
|
163
|
+
// pessoa a um lugar onde ela não consegue chegar.
|
|
164
|
+
//
|
|
165
|
+
// E a causa mais provável de cair aqui não é "não entrei": é ter entrado pelo
|
|
166
|
+
// caminho recomendado. O login por OAuth (`/mcp` → bridgeai) guarda o token
|
|
167
|
+
// dentro do cliente MCP, e este script é um processo separado que só enxerga
|
|
168
|
+
// o ambiente. Enquanto o túnel autenticar por variável, quem entrou por OAuth
|
|
169
|
+
// precisa deste segundo login.
|
|
170
|
+
morre(
|
|
171
|
+
'Faltou o acesso da BridgeAI para abrir o túnel.\n' +
|
|
172
|
+
'\n' +
|
|
173
|
+
'Peça ao seu agente as CREDENCIAIS DE DESENVOLVIMENTO deste projeto.\n' +
|
|
174
|
+
'Ele escreve o .env com tudo o que o túnel precisa, e não há chave\n' +
|
|
175
|
+
'nenhuma para você copiar.\n' +
|
|
176
|
+
'\n' +
|
|
177
|
+
'Rode o túnel de dentro da pasta do projeto — é de lá que ele lê o .env.\n' +
|
|
178
|
+
'\n' +
|
|
179
|
+
'Sem agente à mão: npx bridgeaibrasil login',
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// O cache vem junto quando o `.env` que o `dev_credentials` escreveu diz que
|
|
184
|
+
// vem (`BRIDGEAI_TUNNEL_CACHE`), ou quando alguém pede `--cache`. Sem uma das
|
|
185
|
+
// duas, só o banco — o comportamento de sempre.
|
|
186
|
+
const comCache = 'cache' in opt || doEnvDoProjeto('BRIDGEAI_TUNNEL_CACHE') !== null;
|
|
187
|
+
const portaCache = Number(opt['cache-port'] || PORTA_CACHE_PADRAO);
|
|
188
|
+
|
|
189
|
+
// ⚠️ **O cache é de UM projeto, mesmo no modo conta.** Desde 09/09/2026 cada
|
|
190
|
+
// projeto tem o seu Redis (`app-<id>-cache`), então o túnel de cache precisa
|
|
191
|
+
// saber de qual projeto é — o de banco não precisa, porque o Postgres escolhe
|
|
192
|
+
// o database na abertura da conexão e o servidor é um só. Com `--app` é ele;
|
|
193
|
+
// sem `--app`, é o `BRIDGEAI_APP` que o `dev_credentials` grava no `.env` da
|
|
194
|
+
// pasta de onde este script rodou. Sem nenhum dos dois, o túnel de cache não
|
|
195
|
+
// tem para onde ir, e diz isso em vez de subir uma porta que não leva a nada.
|
|
196
|
+
const appDoCache = app || doEnvDoProjeto('BRIDGEAI_APP');
|
|
197
|
+
|
|
198
|
+
/** A query do pedido. Sem `app` é o modo conta — ver o aviso no topo. */
|
|
199
|
+
const consulta = (target = 'db') =>
|
|
200
|
+
(target === 'cache' && appDoCache
|
|
201
|
+
? `app=${encodeURIComponent(appDoCache)}&`
|
|
202
|
+
: app
|
|
203
|
+
? `app=${encodeURIComponent(app)}&`
|
|
204
|
+
: '') +
|
|
205
|
+
`environment=${encodeURIComponent(environment)}`;
|
|
206
|
+
|
|
207
|
+
const wsUrlPara = (target) =>
|
|
208
|
+
`${base.replace(/^http/, 'ws')}/tunnel?${consulta(target)}&target=${target}`;
|
|
209
|
+
const wsUrl = wsUrlPara('db');
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Pergunta ao servidor por que o túnel não abriria, ANTES de tentar abrir.
|
|
213
|
+
*
|
|
214
|
+
* A API de WebSocket não entrega o código HTTP de um upgrade recusado: 401, 403
|
|
215
|
+
* e 404 chegam aqui como o mesmo erro vazio. Sem esta pergunta, a única coisa
|
|
216
|
+
* que dá para dizer é "não deu" — e quem está do outro lado não programa.
|
|
217
|
+
*/
|
|
218
|
+
async function conferir(target = 'db') {
|
|
219
|
+
const url = `${base}/tunnel/check?${consulta(target)}&target=${target}`;
|
|
220
|
+
let res;
|
|
221
|
+
try {
|
|
222
|
+
res = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
|
|
223
|
+
} catch {
|
|
224
|
+
morre('Não consegui falar com a BridgeAI. Confira sua conexão com a internet.');
|
|
225
|
+
}
|
|
226
|
+
if (res.ok) return;
|
|
227
|
+
|
|
228
|
+
let corpo = {};
|
|
229
|
+
try {
|
|
230
|
+
corpo = await res.json();
|
|
231
|
+
} catch {
|
|
232
|
+
/* resposta sem JSON: cai na frase genérica abaixo */
|
|
233
|
+
}
|
|
234
|
+
morre(corpo.error || `A BridgeAI respondeu ${res.status} e não deu para abrir o túnel.`);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Já tem alguém atendendo nesta porta?
|
|
239
|
+
*
|
|
240
|
+
* Perguntar CONECTANDO, e não confiando no `EADDRINUSE` do `listen`. No Windows
|
|
241
|
+
* o `listen` em `127.0.0.1:<p>` funciona mesmo com um `0.0.0.0:<p>` já de pé —
|
|
242
|
+
* os dois convivem, o endereço específico ganha as conexões novas, e nada
|
|
243
|
+
* reclama. Foi assim que o túnel e um Postgres em Docker subiram lado a lado.
|
|
244
|
+
*
|
|
245
|
+
* Um `connect` bem-sucedido não engana: se algo aceitou a conexão, é esse algo
|
|
246
|
+
* que o `.env` do projeto vai encontrar quando o túnel não estiver de pé.
|
|
247
|
+
*/
|
|
248
|
+
function ocupada(p) {
|
|
249
|
+
return new Promise((resolve) => {
|
|
250
|
+
const s = net.connect({ host: '127.0.0.1', port: p });
|
|
251
|
+
let respondido = false;
|
|
252
|
+
const fim = (r) => {
|
|
253
|
+
if (respondido) return;
|
|
254
|
+
respondido = true;
|
|
255
|
+
s.destroy();
|
|
256
|
+
resolve(r);
|
|
257
|
+
};
|
|
258
|
+
s.setTimeout(1500);
|
|
259
|
+
s.once('connect', () => fim(true));
|
|
260
|
+
s.once('error', () => fim(false)); // recusada é o que se quer: porta livre
|
|
261
|
+
s.once('timeout', () => fim(false));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Abre um WebSocket e fecha, só para ver se o outro lado está mesmo lá.
|
|
267
|
+
*
|
|
268
|
+
* O servidor conecta o banco ANTES de aceitar o upgrade (ver `mcp/src/tunnel.ts`),
|
|
269
|
+
* então um `open` aqui quer dizer que o Postgres da nuvem aceitou uma conexão.
|
|
270
|
+
* Sem isto, "Túnel aberto" queria dizer só "consegui reservar a porta" — e o
|
|
271
|
+
* primeiro a descobrir que o banco não vinha era o app da pessoa, com um erro
|
|
272
|
+
* de driver que não aponta para lugar nenhum.
|
|
273
|
+
*/
|
|
274
|
+
function apresentacao(url = wsUrl) {
|
|
275
|
+
return new Promise((resolve) => {
|
|
276
|
+
let respondido = false;
|
|
277
|
+
const fim = (r) => {
|
|
278
|
+
if (respondido) return;
|
|
279
|
+
respondido = true;
|
|
280
|
+
resolve(r);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
let ws;
|
|
284
|
+
try {
|
|
285
|
+
ws = new WebSocket(url, [PROTOCOLO, `bridgeai.token.${token}`]);
|
|
286
|
+
} catch {
|
|
287
|
+
return fim(false);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const relogio = setTimeout(() => fim(false), ESPERA_MS);
|
|
291
|
+
ws.addEventListener('open', () => {
|
|
292
|
+
clearTimeout(relogio);
|
|
293
|
+
try {
|
|
294
|
+
ws.close();
|
|
295
|
+
} catch {
|
|
296
|
+
/* já fechou sozinho */
|
|
297
|
+
}
|
|
298
|
+
fim(true);
|
|
299
|
+
});
|
|
300
|
+
// Fechar sem ter aberto é recusa. Depois do `open` o `fim` já respondeu, e
|
|
301
|
+
// a segunda chamada não faz nada — daí a guarda.
|
|
302
|
+
ws.addEventListener('error', () => {
|
|
303
|
+
clearTimeout(relogio);
|
|
304
|
+
fim(false);
|
|
305
|
+
});
|
|
306
|
+
ws.addEventListener('close', () => {
|
|
307
|
+
clearTimeout(relogio);
|
|
308
|
+
fim(false);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Liga uma conexão local ao servidor. Uma por conexão, como um TCP de verdade. */
|
|
314
|
+
function atende(local, url = wsUrl) {
|
|
315
|
+
const ws = new WebSocket(url, [PROTOCOLO, `bridgeai.token.${token}`]);
|
|
316
|
+
ws.binaryType = 'arraybuffer';
|
|
317
|
+
|
|
318
|
+
// O que chegar antes de o WebSocket abrir fica aqui. Um driver de banco manda
|
|
319
|
+
// o aperto de mão no primeiro milissegundo, muito antes de o WebSocket estar
|
|
320
|
+
// pronto — sem esta fila, esse primeiro pacote se perderia e a conexão ficaria
|
|
321
|
+
// pendurada sem ninguém entender por quê.
|
|
322
|
+
const fila = [];
|
|
323
|
+
let aberto = false;
|
|
324
|
+
|
|
325
|
+
local.on('data', (d) => {
|
|
326
|
+
if (aberto) ws.send(d);
|
|
327
|
+
else fila.push(d);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
ws.addEventListener('open', () => {
|
|
331
|
+
aberto = true;
|
|
332
|
+
for (const d of fila) ws.send(d);
|
|
333
|
+
fila.length = 0;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
ws.addEventListener('message', (ev) => {
|
|
337
|
+
// Só binário é dado. O servidor não manda texto neste canal — ele conecta o
|
|
338
|
+
// banco ANTES de aceitar o WebSocket, justamente para não precisar avisar
|
|
339
|
+
// nada por aqui.
|
|
340
|
+
if (typeof ev.data === 'string') return;
|
|
341
|
+
local.write(Buffer.from(ev.data));
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const fecha = () => {
|
|
345
|
+
try {
|
|
346
|
+
local.destroy();
|
|
347
|
+
} catch {
|
|
348
|
+
/* já estava fechada */
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
if (ws.readyState <= 1) ws.close();
|
|
352
|
+
} catch {
|
|
353
|
+
/* idem */
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
ws.addEventListener('close', fecha);
|
|
358
|
+
ws.addEventListener('error', fecha);
|
|
359
|
+
local.on('error', fecha);
|
|
360
|
+
local.on('close', fecha);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async function main() {
|
|
364
|
+
// A porta primeiro: é a única conferência que não depende da internet, e é a
|
|
365
|
+
// que protege dado. Continuar com a porta ocupada é aceitar que, no dia em que
|
|
366
|
+
// o túnel cair, o projeto escreva no banco errado sem nenhum erro aparecer.
|
|
367
|
+
if (await ocupada(porta)) {
|
|
368
|
+
morre(
|
|
369
|
+
`Já tem alguma coisa atendendo em 127.0.0.1:${porta}, então o túnel não vai subir aí.\n` +
|
|
370
|
+
'Quase sempre é um Postgres seu — em Docker, ou instalado na máquina.\n' +
|
|
371
|
+
'\n' +
|
|
372
|
+
'Não dá para dividir a porta: se o túnel caísse, seu projeto passaria a\n' +
|
|
373
|
+
'gravar nesse outro banco sem avisar, e uma migration iria para o lugar\n' +
|
|
374
|
+
'errado em silêncio.\n' +
|
|
375
|
+
'\n' +
|
|
376
|
+
`Saídas: desligue o que está usando a ${porta}, ou escolha outra com\n` +
|
|
377
|
+
`--port ${porta + 1} — e mude DATABASE_URL e DB_PORT no .env junto.`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
await conferir();
|
|
382
|
+
|
|
383
|
+
// Só depois de saber que o pedido é legítimo é que vale gastar um WebSocket
|
|
384
|
+
// para ver se o banco responde. Ao contrário, um token vencido apareceria
|
|
385
|
+
// aqui como "o banco não respondeu", que manda a pessoa olhar o lugar errado.
|
|
386
|
+
if (!(await apresentacao())) {
|
|
387
|
+
morre(
|
|
388
|
+
'Consegui falar com a BridgeAI, mas o banco do projeto não respondeu ao abrir o túnel.\n' +
|
|
389
|
+
'Não vou subir a porta local: um túnel aberto sem banco atrás parece que está\n' +
|
|
390
|
+
'funcionando e falha só quando você for usar.\n' +
|
|
391
|
+
'\n' +
|
|
392
|
+
'Tente de novo em alguns segundos. Se continuar, é o banco do projeto que está fora.',
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const servidor = net.createServer(atende);
|
|
397
|
+
|
|
398
|
+
servidor.on('error', (e) => {
|
|
399
|
+
if (e.code === 'EADDRINUSE') {
|
|
400
|
+
// Continua valendo: o `ocupada` acima fecha a janela quase toda, mas dois
|
|
401
|
+
// túneis subindo ao mesmo tempo ainda chegam aqui — e no Linux este é o
|
|
402
|
+
// erro que aparece de verdade.
|
|
403
|
+
morre(
|
|
404
|
+
`A porta ${porta} já está ocupada na sua máquina. ` +
|
|
405
|
+
`Use outra: --port ${porta + 1} (e mude a porta no seu arquivo .env também).`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
morre(`Não consegui abrir a porta ${porta}: ${e.message}`);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Só 127.0.0.1. Escutar em 0.0.0.0 poria o banco do projeto ao alcance de
|
|
412
|
+
// qualquer um na mesma rede — o café, o coworking, o wi-fi do prédio.
|
|
413
|
+
servidor.listen(porta, '127.0.0.1', () => {
|
|
414
|
+
anota((p) =>
|
|
415
|
+
p.anotarAbertura({
|
|
416
|
+
porta,
|
|
417
|
+
oQue: app ? `túnel do banco de ${app} (${environment})` : 'túnel do banco (serve todos os projetos)',
|
|
418
|
+
comando: `node tunnel.js ${app ? `--app ${app}` : '--dev'}`,
|
|
419
|
+
pid: process.pid,
|
|
420
|
+
}),
|
|
421
|
+
);
|
|
422
|
+
console.log(
|
|
423
|
+
`Túnel aberto: 127.0.0.1:${porta} → ` +
|
|
424
|
+
(app ? `banco de ${app} (${environment}).` : 'seu ambiente de desenvolvimento (todos os projetos).'),
|
|
425
|
+
);
|
|
426
|
+
console.log('O banco da nuvem respondeu — a ligação foi conferida, e não só reservada.');
|
|
427
|
+
if (!comCache) console.log('Deixe esta janela aberta enquanto estiver desenvolvendo.');
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (!comCache) return;
|
|
431
|
+
|
|
432
|
+
if (!appDoCache) {
|
|
433
|
+
morre(
|
|
434
|
+
'O .env pede o túnel de cache, mas não diz de qual projeto é o Redis.\n' +
|
|
435
|
+
'Rode de novo com --app <id>, ou peça ao Claude o dev_credentials de novo —\n' +
|
|
436
|
+
'ele grava BRIDGEAI_APP no .env, e é daí que este script lê.',
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// A segunda porta, para o cache. Mesmas três conferências do banco, pela
|
|
441
|
+
// mesma razão: uma porta aberta sem nada atrás parece que funciona e falha
|
|
442
|
+
// só quando o app for usar.
|
|
443
|
+
if (await ocupada(portaCache)) {
|
|
444
|
+
morre(
|
|
445
|
+
`Já tem alguma coisa atendendo em 127.0.0.1:${portaCache}, e é onde o cache do projeto iria.\n` +
|
|
446
|
+
'Quase sempre é um Redis seu. Desligue-o, ou escolha outra porta com\n' +
|
|
447
|
+
`--cache-port ${portaCache + 1} — e mude a porta em REDIS_URL no .env junto.`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
await conferir('cache');
|
|
451
|
+
if (!(await apresentacao(wsUrlPara('cache')))) {
|
|
452
|
+
morre('O banco do projeto respondeu, mas o cache não. Tente de novo em alguns segundos.');
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const servidorCache = net.createServer((local) => atende(local, wsUrlPara('cache')));
|
|
456
|
+
servidorCache.on('error', (e) => morre(`Não consegui abrir a porta ${portaCache} do cache: ${e.message}`));
|
|
457
|
+
servidorCache.listen(portaCache, '127.0.0.1', () => {
|
|
458
|
+
anota((p) =>
|
|
459
|
+
p.anotarAbertura({
|
|
460
|
+
porta: portaCache,
|
|
461
|
+
oQue: `túnel do cache de ${appDoCache} (faixa de desenvolvimento)`,
|
|
462
|
+
comando: `node tunnel.js ${app ? `--app ${app}` : '--dev'}`,
|
|
463
|
+
pid: process.pid,
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
console.log(
|
|
467
|
+
`Túnel aberto: 127.0.0.1:${portaCache} → o Redis de ${appDoCache} ` +
|
|
468
|
+
'(faixa de desenvolvimento, separada da produção).',
|
|
469
|
+
);
|
|
470
|
+
console.log('Deixe esta janela aberta enquanto estiver desenvolvendo.');
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Ao sair, apaga do registro as portas que ESTE processo abriu.
|
|
476
|
+
*
|
|
477
|
+
* `exit` não basta sozinho: um Ctrl+C manda SIGINT, e sem um ouvinte o Node
|
|
478
|
+
* encerra sem passar por aqui — que é justamente como esta janela costuma ser
|
|
479
|
+
* fechada. Por isso os três.
|
|
480
|
+
*
|
|
481
|
+
* E é por isso que o registro nunca é lido como verdade: SIGKILL, queda de
|
|
482
|
+
* energia e reinício não passam por lugar nenhum. Quem sabe se a porta está de
|
|
483
|
+
* pé é quem mede.
|
|
484
|
+
*/
|
|
485
|
+
function despedida() {
|
|
486
|
+
anota((p) => {
|
|
487
|
+
p.anotarFechamento(porta);
|
|
488
|
+
if (comCache) p.anotarFechamento(portaCache);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
process.on('exit', despedida);
|
|
493
|
+
for (const sinal of ['SIGINT', 'SIGTERM']) {
|
|
494
|
+
process.on(sinal, () => {
|
|
495
|
+
despedida();
|
|
496
|
+
process.exit(0);
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
main();
|