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.
@@ -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();