@dutodev/cli 0.4.3 → 0.4.5

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 CHANGED
@@ -106,7 +106,7 @@ Com os túneis no ar, o terminal mostra cada túnel (URL, destino local, requisi
106
106
  | `tab`, `↑↓` | Alterna entre túneis e requisições e move a seleção |
107
107
  | `q` ou Ctrl+C | Fecha os túneis e sai |
108
108
 
109
- Se o túnel está no ar mas nada responde na porta local, quem abrir o endereço no navegador vê uma página do Duto que tenta de novo a cada 3 segundos e volta sozinha quando o serviço sobe. Clientes que não são navegador (`curl`, APIs) recebem um 502 com uma mensagem em texto.
109
+ Se o túnel está no ar mas nada responde na porta local, quem abrir o endereço no navegador vê uma página do Duto que volta sozinha quando o serviço sobe. Ela espera por uma conexão só (SSE): a CLI checa a porta na sua máquina e avisa a página assim que o serviço responde, sem recarregar de tempos em tempos. Clientes que não são navegador (`curl`, APIs) recebem a mesma resposta em texto. Em ambos os casos o status é `503` com `Retry-After: 3`. Os caminhos `/__duto/…` são reservados para isso e nunca chegam ao seu app.
110
110
 
111
111
  Fora de um terminal interativo (pipe, CI) ou com `--no-ui`, a CLI imprime um log simples. No Windows a tela cheia vem desligada; use `--ui` para tentar.
112
112
 
package/dist/index.js CHANGED
@@ -368,6 +368,24 @@ var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">":
368
368
  function wantsHtml(accept) {
369
369
  return /\btext\/html\b/i.test(accept ?? "");
370
370
  }
371
+ var WAIT_SCRIPT = `
372
+ <script>
373
+ (function () {
374
+ var delay = 3000;
375
+ function poll() {
376
+ setTimeout(function () {
377
+ fetch(location.href, { method: "HEAD", cache: "no-store" })
378
+ .then(function (r) { if (r.status !== 503) location.reload(); else again(); })
379
+ .catch(again);
380
+ }, delay);
381
+ }
382
+ function again() { delay = Math.min(delay * 2, 30000); poll(); }
383
+ if (!window.EventSource) return poll();
384
+ var fails = 0, es = new EventSource("/__duto/espera");
385
+ es.addEventListener("up", function () { es.close(); location.reload(); });
386
+ es.onerror = function () { if (++fails >= 3) { es.close(); poll(); } };
387
+ })();
388
+ </script>`;
371
389
  function unavailablePage(o) {
372
390
  const tunnel2 = esc(o.tunnel);
373
391
  const local = `localhost:${o.port}`;
@@ -376,7 +394,7 @@ function unavailablePage(o) {
376
394
  <head>
377
395
  <meta charset="utf-8">
378
396
  <meta name="viewport" content="width=device-width, initial-scale=1">
379
- ${o.retry ? '<meta http-equiv="refresh" content="3">\n' : ""}<meta name="robots" content="noindex">
397
+ <meta name="robots" content="noindex">
380
398
  <title>Nada respondendo em ${local} \xB7 duto</title>
381
399
  <style>
382
400
  :root{--bg:#f6f6f3;--fg:#0f1110;--muted:#5f6360;--line:#dcddd8;--card:#fff;--signal:#2dd4bf;--signal-text:#0f766e}
@@ -400,8 +418,8 @@ h1 code{font-size:.86em;font-weight:600}
400
418
  <h1>Nada respondendo em <code>${local}</code></h1>
401
419
  <p>O t\xFAnel <code>${tunnel2}</code> est\xE1 no ar, mas o servi\xE7o local n\xE3o respondeu. Suba o servidor na m\xE1quina que abriu o t\xFAnel.</p>
402
420
  <p>Se voc\xEA s\xF3 recebeu este link, avise quem o enviou.</p>
403
- <div class="retry">${o.retry ? "<b>Tentando de novo a cada 3 s.</b> A p\xE1gina volta sozinha quando o servi\xE7o responder." : "Recarregue a p\xE1gina quando o servi\xE7o estiver no ar."}</div>
404
- </main>
421
+ <div class="retry">${o.retry ? `<b>Aguardando o servi\xE7o.</b> A p\xE1gina volta sozinha quando ele responder.<noscript> Recarregue quando o servi\xE7o estiver no ar.</noscript>` : "Recarregue a p\xE1gina quando o servi\xE7o estiver no ar."}</div>
422
+ </main>${o.retry ? WAIT_SCRIPT : ""}
405
423
  </body>
406
424
  </html>
407
425
  `;
@@ -485,6 +503,38 @@ function rewriteLocation(location, localHost2, publicHost) {
485
503
  function hostOf(req) {
486
504
  return (req.headers.host ?? "").replace(/:\d+$/, "").toLowerCase();
487
505
  }
506
+ var DUTO_PREFIX = "/__duto/";
507
+ var WAIT_PATH = "/__duto/espera";
508
+ var WAIT_MAX_MS = 10 * 6e4;
509
+ async function isUp(port) {
510
+ return await probe("127.0.0.1", port) || await probe("::1", port);
511
+ }
512
+ function waitForService(res, port, checkMs, pingMs) {
513
+ res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-accel-buffering": "no" });
514
+ res.write(": duto\n\n");
515
+ let busy = false;
516
+ const stop = () => {
517
+ clearInterval(check);
518
+ clearInterval(ping);
519
+ clearTimeout(limit);
520
+ };
521
+ const check = setInterval(async () => {
522
+ if (busy) return;
523
+ busy = true;
524
+ const up = await isUp(port);
525
+ busy = false;
526
+ if (up && !res.writableEnded) {
527
+ stop();
528
+ res.end("event: up\ndata: 1\n\n");
529
+ }
530
+ }, checkMs);
531
+ const ping = setInterval(() => res.write(": ping\n\n"), pingMs);
532
+ const limit = setTimeout(() => {
533
+ stop();
534
+ res.end();
535
+ }, WAIT_MAX_MS);
536
+ res.on("close", stop);
537
+ }
488
538
  async function startProxy(o) {
489
539
  const max = o.maxBody ?? MAX_BODY;
490
540
  let nextId = 1;
@@ -495,6 +545,12 @@ async function startProxy(o) {
495
545
  res.end("T\xFAnel n\xE3o encontrado nesta sess\xE3o");
496
546
  return;
497
547
  }
548
+ if ((req.url ?? "").startsWith(DUTO_PREFIX)) {
549
+ if (req.url === WAIT_PATH && req.method === "GET") return waitForService(res, route.port, o.waitCheckMs ?? 1e3, o.waitPingMs ?? 25e3);
550
+ res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
551
+ res.end("Rota reservada do Duto");
552
+ return;
553
+ }
498
554
  const id = nextId++;
499
555
  const started = Date.now();
500
556
  const headers = rewrite(req.headers, route);
@@ -553,10 +609,10 @@ async function startProxy(o) {
553
609
  const html = wantsHtml(req.headers.accept);
554
610
  const msg = html ? unavailablePage({ tunnel: route.tunnel, port: route.port, retry: req.method === "GET" }) : `Nada escutando em localhost:${route.port}. Suba o servidor e tente de novo.`;
555
611
  if (!res.headersSent) {
556
- res.writeHead(502, { "content-type": html ? "text/html; charset=utf-8" : "text/plain; charset=utf-8", "cache-control": "no-store" });
612
+ res.writeHead(503, { "content-type": html ? "text/html; charset=utf-8" : "text/plain; charset=utf-8", "cache-control": "no-store", "retry-after": "3" });
557
613
  res.end(req.method === "HEAD" ? void 0 : msg);
558
614
  } else res.destroy();
559
- o.store.update(id, { status: 502, durationMs: Date.now() - started, bytes: res.headersSent ? 0 : Buffer.byteLength(msg), error: err.message || err.code || err.name, reqBody: reqCap.text() });
615
+ o.store.update(id, { status: 503, durationMs: Date.now() - started, bytes: res.headersSent ? 0 : Buffer.byteLength(msg), error: err.message || err.code || err.name, reqBody: reqCap.text() });
560
616
  });
561
617
  req.on("data", (c) => reqCap.push(c));
562
618
  req.pipe(upstream);
@@ -605,7 +661,7 @@ async function startProxy(o) {
605
661
  client.pipe(target);
606
662
  });
607
663
  const fail3 = (err) => {
608
- o.store.update(id, { status: 502, durationMs: Date.now() - started, error: err.message });
664
+ o.store.update(id, { status: 503, durationMs: Date.now() - started, error: err.message });
609
665
  client.destroy();
610
666
  };
611
667
  target.on("error", fail3);
@@ -957,9 +1013,9 @@ async function runSession(specs, source, flags) {
957
1013
  const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source });
958
1014
  if (wantsUi(flags)) {
959
1015
  const { runFullscreen } = await import("./fullscreen-5437X7MP.js");
960
- process.exit(await runFullscreen(session, specs, "0.4.3"));
1016
+ process.exit(await runFullscreen(session, specs, "0.4.5"));
961
1017
  }
962
- console.log(`${mark()} ${paint("muted", `${"0.4.3"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
1018
+ console.log(`${mark()} ${paint("muted", `${"0.4.5"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
963
1019
  const stopLog = runPlain(session, (l) => console.log(l));
964
1020
  const stopped = new Promise((resolve) => {
965
1021
  const onSignal = () => {
@@ -1046,7 +1102,7 @@ cli.help(
1046
1102
  body: s.body.replace(/^\s*For more info, run any command with the `--help` flag:/m, " Mais detalhes de um comando: duto <comando> --help").replace("Display this message", "Mostra esta ajuda").replace("Display version number", "Mostra a vers\xE3o")
1047
1103
  }))
1048
1104
  );
1049
- cli.version("0.4.3");
1105
+ cli.version("0.4.5");
1050
1106
  try {
1051
1107
  cli.parse(process.argv, { run: false });
1052
1108
  await cli.runMatchedCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dutodev/cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Exponha uma porta local em https://<nome>-<usuário>.duto.dev com um comando.",
5
5
  "license": "MIT",
6
6
  "type": "module",