@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-
|
|
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:
|
|
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 ?
|
|
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
|
})
|