@dutodev/cli 0.6.0 → 0.7.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 CHANGED
@@ -146,6 +146,7 @@ Com os túneis no ar, o terminal mostra cada túnel (URL, destino local, requisi
146
146
  | `f` | Filtra por túnel, método, caminho ou status |
147
147
  | `↵` | Detalhes da requisição: cabeçalhos e o começo dos corpos |
148
148
  | `r` | No detalhe, reenvia a requisição ao serviço local |
149
+ | `i` | Abre o inspetor no navegador |
149
150
  | `tab`, `↑↓` | Alterna entre túneis e requisições e move a seleção |
150
151
  | `q` ou Ctrl+C | Fecha os túneis e sai |
151
152
 
@@ -157,6 +158,26 @@ Se o túnel está no ar mas nada responde na porta local, quem abrir o endereço
157
158
 
158
159
  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.
159
160
 
161
+ ## Inspetor no navegador
162
+
163
+ Junto com a sessão, a CLI sobe um inspetor local: uma página no navegador com as requisições chegando ao vivo, o JSON formatado e botões para copiar e reenviar. Ele é mais confortável que o terminal para corpos grandes.
164
+
165
+ - **Como abrir.** Na tela cheia, a tecla `i` abre o navegador já com o endereço certo, e a tela mostra `inspetor http://127.0.0.1:<porta>`. No modo `plain` (pipe, CI, `--no-ui`), a CLI imprime `inspetor: <url>?t=<token>`; copie esse endereço inteiro.
166
+ - **Porta.** O padrão é `127.0.0.1:4141`. Se estiver ocupada, a CLI tenta as seguintes até a 4161 e, por fim, qualquer porta livre. O inspetor escuta só em `127.0.0.1`: não é acessível por outras máquinas da rede.
167
+ - **Token.** O endereço leva um token da sessão (`?t=...`). Ele existe para que outros sites abertos no seu navegador não consigam ler nem reenviar as suas requisições. Por isso a tela cheia não exibe o token; use a tecla `i`.
168
+ - **Desligar.** `--no-inspect` não sobe o inspetor. No modo `plain` o endereço com token vai para a saída; se ela vai para um log compartilhado, use `--no-inspect`.
169
+ - **Nada vai para o disco.** As requisições ficam só na memória da sessão e somem quando ela termina. Funciona sem internet.
170
+
171
+ A página tem a lista à esquerda e o detalhe à direita, com as abas **Corpo**, **Cabeçalhos**, **Resposta** e **Raw**. O JSON aparece reindentado, exatamente como chegou, com os erros de sintaxe destacados.
172
+
173
+ | Ação | Como |
174
+ |---|---|
175
+ | Copiar o corpo | **Copiar corpo** copia o corpo da aba ativa |
176
+ | Copiar como `curl` | **Copiar como curl**; em túnel com senha, o comando leva `-H 'X-Duto-Auth: <usuário>:SENHA'` e você troca `SENHA` pela senha |
177
+ | Reenviar | **↻ Reenviar** ou a tecla `r`, com as mesmas regras do terminal |
178
+ | Filtrar | `/` foca o filtro |
179
+ | Navegar | `↑` e `↓` movem a seleção |
180
+
160
181
  ## Comandos
161
182
 
162
183
  | Comando | O que faz |
@@ -176,6 +197,7 @@ Fora de um terminal interativo (pipe, CI) ou com `--no-ui`, a CLI imprime um log
176
197
  | `--auth <usuario:senha>` | Pede usuário e senha para abrir o endereço (repetível) |
177
198
  | `--permitir <faixa>` | Com `--auth`: IP, faixa ou nome (`stripe`, `github`) que entra sem senha (repetível) |
178
199
  | `--no-ui` / `--ui` | Desliga / força a tela cheia |
200
+ | `--no-inspect` | Não sobe o inspetor no navegador (a tecla `i` passa a mostrar "Inspetor desligado (--no-inspect).") |
179
201
 
180
202
  ## Configuração
181
203
 
@@ -38,6 +38,37 @@ function validateName(name, max = MAX_NAME) {
38
38
  return { ok: true };
39
39
  }
40
40
 
41
+ // src/ui/theme.ts
42
+ var C = {
43
+ signal: "#2dd4bf",
44
+ muted: "#8a8f8b",
45
+ strong: "#f3f4f1",
46
+ ok: "#4ade80",
47
+ warn: "#facc15",
48
+ err: "#f87171"
49
+ };
50
+ var TUNNEL_COLORS = ["#93c5fd", "#c4b5fd", "#facc15", "#4ade80", "#f9a8d4", "#fdba74", "#67e8f9"];
51
+ function tunnelColor(index) {
52
+ return TUNNEL_COLORS[index % TUNNEL_COLORS.length];
53
+ }
54
+ function fit(text, width) {
55
+ if (width <= 0) return "";
56
+ return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}\u2026` : text.padEnd(width);
57
+ }
58
+
59
+ // src/session/bodyNote.ts
60
+ var TRUNCATED_NOTE = "Corpo cortado em 64 KB.";
61
+ function bodyNote(r, side) {
62
+ if (side === "req") {
63
+ if (r.reqBodyBytes === 0) return "Sem corpo.";
64
+ if (r.reqBody === null && !r.reqTruncated) return "Corpo bin\xE1rio; n\xE3o \xE9 guardado.";
65
+ return null;
66
+ }
67
+ if (r.bytes === 0) return "Sem corpo.";
68
+ if (r.resBody === null) return "Corpo bin\xE1rio; n\xE3o \xE9 guardado.";
69
+ return null;
70
+ }
71
+
41
72
  // src/ui/format.ts
42
73
  function statusColor(status) {
43
74
  if (status === null) return "muted";
@@ -66,6 +97,11 @@ function localLabel(t) {
66
97
  export {
67
98
  normalizeName,
68
99
  validateName,
100
+ C,
101
+ tunnelColor,
102
+ fit,
103
+ TRUNCATED_NOTE,
104
+ bodyNote,
69
105
  statusColor,
70
106
  formatDuration,
71
107
  formatBytes,
@@ -1,13 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ C,
4
+ TRUNCATED_NOTE,
5
+ bodyNote,
6
+ fit,
3
7
  formatBytes,
4
8
  formatDuration,
5
9
  formatTime,
6
10
  localLabel,
7
11
  normalizeName,
8
12
  statusColor,
13
+ tunnelColor,
9
14
  validateName
10
- } from "./chunk-26EQC6HB.js";
15
+ } from "./chunk-74IJFYGT.js";
11
16
 
12
17
  // src/ui/fullscreen.tsx
13
18
  import { render } from "ink";
@@ -18,30 +23,10 @@ import { Box as Box5, Text as Text6, useInput, useStdout } from "ink";
18
23
 
19
24
  // src/ui/Detail.tsx
20
25
  import { Box, Text } from "ink";
21
-
22
- // src/ui/theme.ts
23
- var C = {
24
- signal: "#2dd4bf",
25
- muted: "#8a8f8b",
26
- strong: "#f3f4f1",
27
- ok: "#4ade80",
28
- warn: "#facc15",
29
- err: "#f87171"
30
- };
31
- var TUNNEL_COLORS = ["#93c5fd", "#c4b5fd", "#facc15", "#4ade80", "#f9a8d4", "#fdba74", "#67e8f9"];
32
- function tunnelColor(index) {
33
- return TUNNEL_COLORS[index % TUNNEL_COLORS.length];
34
- }
35
- function fit(text, width) {
36
- if (width <= 0) return "";
37
- return text.length > width ? `${text.slice(0, Math.max(0, width - 1))}\u2026` : text.padEnd(width);
38
- }
39
-
40
- // src/ui/Detail.tsx
41
26
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
42
27
  var MAX_HEADERS = 12;
43
28
  var MAX_BODY_LINES = 8;
44
- function Section({ title, headers, body, truncated }) {
29
+ function Section({ title, headers, body, truncated, note: note2 }) {
45
30
  const entries = Object.entries(headers);
46
31
  const lines = body ? body.split("\n") : [];
47
32
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
@@ -59,9 +44,9 @@ function Section({ title, headers, body, truncated }) {
59
44
  entries.length - MAX_HEADERS,
60
45
  " cabe\xE7alhos"
61
46
  ] }),
62
- body === null ? /* @__PURE__ */ jsx(Text, { color: C.muted, children: "(corpo bin\xE1rio ou n\xE3o capturado)" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
47
+ body === null ? /* @__PURE__ */ jsx(Text, { color: C.muted, children: note2 ?? "" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
63
48
  lines.slice(0, MAX_BODY_LINES).map((l, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: l }, i)),
64
- (lines.length > MAX_BODY_LINES || truncated) && /* @__PURE__ */ jsx(Text, { color: C.muted, children: truncated ? "\u2026 cortado em 64 KB" : "\u2026" })
49
+ (lines.length > MAX_BODY_LINES || truncated) && /* @__PURE__ */ jsx(Text, { color: C.muted, children: truncated ? TRUNCATED_NOTE : "\u2026" })
65
50
  ] })
66
51
  ] });
67
52
  }
@@ -89,8 +74,8 @@ function Detail({ record }) {
89
74
  record.replayOf
90
75
  ] }),
91
76
  record.error && /* @__PURE__ */ jsx(Text, { color: C.err, children: record.error }),
92
- /* @__PURE__ */ jsx(Section, { title: "Requisi\xE7\xE3o", headers: record.reqHeaders, body: record.reqBody, truncated: record.reqTruncated }),
93
- /* @__PURE__ */ jsx(Section, { title: "Resposta", headers: record.resHeaders, body: record.resBody, truncated: record.resTruncated }),
77
+ /* @__PURE__ */ jsx(Section, { title: "Requisi\xE7\xE3o", headers: record.reqHeaders, body: record.reqBody, truncated: record.reqTruncated, note: bodyNote(record, "req") }),
78
+ /* @__PURE__ */ jsx(Section, { title: "Resposta", headers: record.resHeaders, body: record.resBody, truncated: record.resTruncated, note: bodyNote(record, "res") }),
94
79
  /* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
95
80
  "\n",
96
81
  "r reenvia \xB7 esc volta para a lista"
@@ -291,13 +276,14 @@ var KEYS = [
291
276
  ["n", "novo t\xFAnel"],
292
277
  ["c", "copiar URL"],
293
278
  ["o", "abrir no navegador"],
279
+ ["i", "inspetor"],
294
280
  ["m", "QR no celular"],
295
281
  ["f", "filtrar"],
296
282
  ["\u21B5", "detalhes"],
297
283
  ["tab", "foco"],
298
284
  ["q", "sair"]
299
285
  ];
300
- function App({ session, version, system, onExit }) {
286
+ function App({ session, version, system, onExit, inspector = null, inspectorError }) {
301
287
  const subState = useCallback((fn) => session.subscribe(fn), [session]);
302
288
  const subRecords = useCallback((fn) => session.records.subscribe(fn), [session]);
303
289
  const state = useSyncExternalStore(subState, () => session.getState());
@@ -310,7 +296,7 @@ function App({ session, version, system, onExit }) {
310
296
  const [focus, setFocus] = useState("requests");
311
297
  const [selReq, setSelReq] = useState(0);
312
298
  const [selTun, setSelTun] = useState(0);
313
- const [message, setMessage] = useState(null);
299
+ const [message, setMessage] = useState(inspectorError ? { text: `Inspetor n\xE3o subiu: ${inspectorError}`, tone: "warn" } : null);
314
300
  const [now, setNow] = useState(() => Date.now());
315
301
  const quitting = useRef(false);
316
302
  const records = useMemo(() => {
@@ -350,6 +336,11 @@ function App({ session, version, system, onExit }) {
350
336
  await session.stop();
351
337
  onExit();
352
338
  }
339
+ async function openInspector() {
340
+ if (!inspector) return setMessage({ text: "Inspetor desligado (--no-inspect).", tone: "warn" });
341
+ const ok = await system.open(inspector.openUrl);
342
+ setMessage(ok ? { text: "Abrindo o inspetor", tone: "signal" } : { text: "N\xE3o consegui abrir o navegador.", tone: "warn" });
343
+ }
353
344
  async function act(kind) {
354
345
  const t = currentTunnel();
355
346
  if (!t?.url) return setMessage({ text: "Esse t\xFAnel ainda n\xE3o tem URL.", tone: "warn" });
@@ -436,13 +427,14 @@ function App({ session, version, system, onExit }) {
436
427
  if (key.escape && filter) return setFilter("");
437
428
  if (input === "c") return void act("copy");
438
429
  if (input === "o") return void act("open");
430
+ if (input === "i") return void openInspector();
439
431
  if (input === "m") return showQr();
440
432
  if (input === "f") return setMode({ kind: "filter", value: filter });
441
433
  if (input === "n") return setMode({ kind: "port", value: "" });
442
434
  });
443
435
  const live = state.tunnels.filter((t) => t.status === "live").length;
444
436
  const conn = state.connection === "online" ? { text: "conectado \u25CF", color: C.signal } : state.connection === "connecting" ? { text: "conectando\u2026", color: C.muted } : { text: "reconectando\u2026", color: C.warn };
445
- const listHeight = Math.max(3, rows - state.tunnels.length - 9);
437
+ const listHeight = Math.max(3, rows - state.tunnels.length - (inspector ? 10 : 9));
446
438
  const detailRecord = mode.kind === "detail" ? all.find((r) => r.id === mode.id) : void 0;
447
439
  const qrTunnel = mode.kind === "qr" ? state.tunnels.find((t) => t.key === mode.key) : void 0;
448
440
  return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", width, children: [
@@ -469,6 +461,7 @@ function App({ session, version, system, onExit }) {
469
461
  ] }),
470
462
  /* @__PURE__ */ jsx6(Text6, { children: " " }),
471
463
  /* @__PURE__ */ jsx6(TunnelTable, { tunnels: state.tunnels, selected: selTun, focused: focus === "tunnels" }),
464
+ inspector && /* @__PURE__ */ jsx6(Text6, { color: C.muted, children: `inspetor ${inspector.url}` }),
472
465
  /* @__PURE__ */ jsx6(Text6, { children: " " }),
473
466
  mode.kind === "detail" && detailRecord ? /* @__PURE__ */ jsx6(Detail, { record: detailRecord }) : mode.kind === "qr" && qrTunnel ? /* @__PURE__ */ jsx6(QrView, { tunnel: qrTunnel, link: mode.link, now }) : /* @__PURE__ */ jsx6(RequestList, { records, tunnelIndex, selected: selReq, focused: focus === "requests", height: listHeight, width, filter }),
474
467
  /* @__PURE__ */ jsx6(Text6, { children: " " }),
@@ -554,7 +547,7 @@ var realSystem = {
554
547
 
555
548
  // src/ui/fullscreen.tsx
556
549
  import { jsx as jsx7 } from "react/jsx-runtime";
557
- function runFullscreen(session, specs, version) {
550
+ function runFullscreen(session, specs, version, inspector, inspectorError) {
558
551
  return withFullscreen(
559
552
  process.stdout,
560
553
  (signal) => new Promise((resolve) => {
@@ -565,7 +558,7 @@ function runFullscreen(session, specs, version) {
565
558
  app.unmount();
566
559
  resolve(code);
567
560
  };
568
- const app = render(/* @__PURE__ */ jsx7(App, { session, version, system: realSystem, onExit: () => finish(0) }), { exitOnCtrlC: false });
561
+ const app = render(/* @__PURE__ */ jsx7(App, { session, version, system: realSystem, inspector, inspectorError, onExit: () => finish(0) }), { exitOnCtrlC: false });
569
562
  signal.addEventListener("abort", () => void session.stop().then(() => finish(0)), { once: true });
570
563
  session.start(specs).catch(() => void 0);
571
564
  })