@dutodev/cli 0.4.2 → 0.4.3

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,6 +106,8 @@ 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.
110
+
109
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.
110
112
 
111
113
  ## Comandos
package/dist/index.js CHANGED
@@ -362,6 +362,52 @@ async function isPortListening(port, timeoutMs = 500) {
362
362
  // src/session/proxy.ts
363
363
  import { Agent, createServer, request } from "http";
364
364
  import { connect as connect2 } from "net";
365
+
366
+ // src/session/errorPage.ts
367
+ var esc = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
368
+ function wantsHtml(accept) {
369
+ return /\btext\/html\b/i.test(accept ?? "");
370
+ }
371
+ function unavailablePage(o) {
372
+ const tunnel2 = esc(o.tunnel);
373
+ const local = `localhost:${o.port}`;
374
+ return `<!doctype html>
375
+ <html lang="pt-BR">
376
+ <head>
377
+ <meta charset="utf-8">
378
+ <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">
380
+ <title>Nada respondendo em ${local} \xB7 duto</title>
381
+ <style>
382
+ :root{--bg:#f6f6f3;--fg:#0f1110;--muted:#5f6360;--line:#dcddd8;--card:#fff;--signal:#2dd4bf;--signal-text:#0f766e}
383
+ @media (prefers-color-scheme:dark){:root{--bg:#0b0d0c;--fg:#e9ebe7;--muted:#8a8f8b;--line:#222624;--card:#111413;--signal-text:#2dd4bf}}
384
+ *{box-sizing:border-box}
385
+ body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px 16px;background:var(--bg);color:var(--fg);font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}
386
+ main{width:100%;max-width:520px;border:1px solid var(--line);border-radius:.3rem;background:var(--card);padding:28px}
387
+ .mark{display:flex;align-items:center;gap:8px;font:600 13px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.02em;color:var(--muted);margin-bottom:22px}
388
+ .dot{width:8px;height:8px;border-radius:50%;border:1.5px solid var(--signal)}
389
+ h1{font-size:21px;line-height:1.3;margin:0 0 10px;font-weight:650;letter-spacing:-.01em}
390
+ p{margin:0 0 12px;color:var(--muted)}
391
+ code{font:13px ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--fg)}
392
+ h1 code{font-size:.86em;font-weight:600}
393
+ .retry{margin-top:20px;padding-top:14px;border-top:1px solid var(--line);font-size:13px;color:var(--muted)}
394
+ .retry b{color:var(--signal-text);font-weight:600}
395
+ </style>
396
+ </head>
397
+ <body>
398
+ <main>
399
+ <div class="mark"><span class="dot"></span>duto \xB7 ${tunnel2}</div>
400
+ <h1>Nada respondendo em <code>${local}</code></h1>
401
+ <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
+ <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>
405
+ </body>
406
+ </html>
407
+ `;
408
+ }
409
+
410
+ // src/session/proxy.ts
365
411
  var TEXT = /^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded|graphql)|[^;]*\+(json|xml))/i;
366
412
  var MAX_BODY = 64 * 1024;
367
413
  var LOCAL_AGENT = new Agent({ keepAlive: false });
@@ -504,10 +550,11 @@ async function startProxy(o) {
504
550
  });
505
551
  upstream.on("error", (err) => {
506
552
  if (res.destroyed && res.writableFinished === false && !res.headersSent) return;
507
- const msg = `Nada escutando em localhost:${route.port}. Suba o servidor e tente de novo.`;
553
+ const html = wantsHtml(req.headers.accept);
554
+ 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.`;
508
555
  if (!res.headersSent) {
509
- res.writeHead(502, { "content-type": "text/plain; charset=utf-8" });
510
- res.end(msg);
556
+ res.writeHead(502, { "content-type": html ? "text/html; charset=utf-8" : "text/plain; charset=utf-8", "cache-control": "no-store" });
557
+ res.end(req.method === "HEAD" ? void 0 : msg);
511
558
  } else res.destroy();
512
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() });
513
560
  });
@@ -910,9 +957,9 @@ async function runSession(specs, source, flags) {
910
957
  const session = new Session({ api, deviceId: config.deviceId, deviceName: hostname(), cloudflaredPath, source });
911
958
  if (wantsUi(flags)) {
912
959
  const { runFullscreen } = await import("./fullscreen-5437X7MP.js");
913
- process.exit(await runFullscreen(session, specs, "0.4.2"));
960
+ process.exit(await runFullscreen(session, specs, "0.4.3"));
914
961
  }
915
- console.log(`${mark()} ${paint("muted", `${"0.4.2"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
962
+ console.log(`${mark()} ${paint("muted", `${"0.4.3"} \xB7 ${source} \xB7 Ctrl+C para encerrar`)}`);
916
963
  const stopLog = runPlain(session, (l) => console.log(l));
917
964
  const stopped = new Promise((resolve) => {
918
965
  const onSignal = () => {
@@ -999,7 +1046,7 @@ cli.help(
999
1046
  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")
1000
1047
  }))
1001
1048
  );
1002
- cli.version("0.4.2");
1049
+ cli.version("0.4.3");
1003
1050
  try {
1004
1051
  cli.parse(process.argv, { run: false });
1005
1052
  await cli.runMatchedCommand();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dutodev/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",