@dutodev/cli 0.7.0 → 1.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.
- package/README.md +116 -116
- package/dist/{chunk-74IJFYGT.js → chunk-76YCOMXH.js} +8 -7
- package/dist/{fullscreen-YK365XSO.js → fullscreen-ECEH3R2V.js} +54 -53
- package/dist/index.js +364 -254
- package/package.json +3 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
C,
|
|
4
|
+
MAX_NAME,
|
|
4
5
|
TRUNCATED_NOTE,
|
|
5
6
|
bodyNote,
|
|
6
7
|
fit,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
statusColor,
|
|
13
14
|
tunnelColor,
|
|
14
15
|
validateName
|
|
15
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-76YCOMXH.js";
|
|
16
17
|
|
|
17
18
|
// src/ui/fullscreen.tsx
|
|
18
19
|
import { render } from "ink";
|
|
@@ -40,9 +41,9 @@ function Section({ title, headers, body, truncated, note: note2 }) {
|
|
|
40
41
|
v
|
|
41
42
|
] }, k)),
|
|
42
43
|
entries.length > MAX_HEADERS && /* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
|
|
43
|
-
"\u2026
|
|
44
|
+
"\u2026 ",
|
|
44
45
|
entries.length - MAX_HEADERS,
|
|
45
|
-
"
|
|
46
|
+
" more headers"
|
|
46
47
|
] }),
|
|
47
48
|
body === null ? /* @__PURE__ */ jsx(Text, { color: C.muted, children: note2 ?? "" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
48
49
|
lines.slice(0, MAX_BODY_LINES).map((l, i) => /* @__PURE__ */ jsx(Text, { wrap: "truncate-end", children: l }, i)),
|
|
@@ -70,15 +71,15 @@ function Detail({ record }) {
|
|
|
70
71
|
] })
|
|
71
72
|
] }),
|
|
72
73
|
record.replayOf !== void 0 && /* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
|
|
73
|
-
"\u21BB
|
|
74
|
+
"\u21BB replay of #",
|
|
74
75
|
record.replayOf
|
|
75
76
|
] }),
|
|
76
77
|
record.error && /* @__PURE__ */ jsx(Text, { color: C.err, children: record.error }),
|
|
77
|
-
/* @__PURE__ */ jsx(Section, { title: "
|
|
78
|
-
/* @__PURE__ */ jsx(Section, { title: "
|
|
78
|
+
/* @__PURE__ */ jsx(Section, { title: "Request", headers: record.reqHeaders, body: record.reqBody, truncated: record.reqTruncated, note: bodyNote(record, "req") }),
|
|
79
|
+
/* @__PURE__ */ jsx(Section, { title: "Response", headers: record.resHeaders, body: record.resBody, truncated: record.resTruncated, note: bodyNote(record, "res") }),
|
|
79
80
|
/* @__PURE__ */ jsxs(Text, { color: C.muted, children: [
|
|
80
81
|
"\n",
|
|
81
|
-
"r
|
|
82
|
+
"r replay \xB7 esc back to list"
|
|
82
83
|
] })
|
|
83
84
|
] });
|
|
84
85
|
}
|
|
@@ -101,16 +102,16 @@ import { Box as Box2, Text as Text3 } from "ink";
|
|
|
101
102
|
|
|
102
103
|
// src/ui/qr.ts
|
|
103
104
|
import qrcode from "qrcode-generator";
|
|
104
|
-
var
|
|
105
|
+
var MARGIN = 2;
|
|
105
106
|
function qrLines(text) {
|
|
106
107
|
const qr = qrcode(0, "L");
|
|
107
108
|
qr.addData(text);
|
|
108
109
|
qr.make();
|
|
109
110
|
const n = qr.getModuleCount();
|
|
110
|
-
const size = n +
|
|
111
|
+
const size = n + MARGIN * 2;
|
|
111
112
|
const dark = (r, c) => {
|
|
112
|
-
const rr = r -
|
|
113
|
-
const cc = c -
|
|
113
|
+
const rr = r - MARGIN;
|
|
114
|
+
const cc = c - MARGIN;
|
|
114
115
|
return rr >= 0 && cc >= 0 && rr < n && cc < n && qr.isDark(rr, cc);
|
|
115
116
|
};
|
|
116
117
|
const lines = [];
|
|
@@ -137,7 +138,7 @@ function QrView({ tunnel, link, now }) {
|
|
|
137
138
|
const left = link.expiresAt === null ? null : link.expiresAt - now;
|
|
138
139
|
const expired = left !== null && Math.ceil(left / 1e3) <= 0;
|
|
139
140
|
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
|
|
140
|
-
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: "
|
|
141
|
+
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: "Open on your phone" }),
|
|
141
142
|
/* @__PURE__ */ jsxs3(Box2, { children: [
|
|
142
143
|
/* @__PURE__ */ jsx3(Box2, { flexDirection: "column", marginRight: 3, children: lines.map((l, i) => /* @__PURE__ */ jsx3(Text3, { backgroundColor: "white", color: "black", children: l }, i)) }),
|
|
143
144
|
/* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
|
|
@@ -151,14 +152,14 @@ function QrView({ tunnel, link, now }) {
|
|
|
151
152
|
] }),
|
|
152
153
|
left !== null && /* @__PURE__ */ jsxs3(Fragment2, { children: [
|
|
153
154
|
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
154
|
-
/* @__PURE__ */ jsx3(Text3, { children: "\u{1F512}
|
|
155
|
-
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: "
|
|
156
|
-
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: "
|
|
155
|
+
/* @__PURE__ */ jsx3(Text3, { children: "\u{1F512} This tunnel has a password." }),
|
|
156
|
+
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: "The QR signs you in, no password to type." }),
|
|
157
|
+
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: "Valid for 5 minutes, one device only." }),
|
|
157
158
|
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
158
|
-
expired ? /* @__PURE__ */ jsx3(Text3, { color: C.warn, children: "
|
|
159
|
+
expired ? /* @__PURE__ */ jsx3(Text3, { color: C.warn, children: "Expired. Press g to generate another." }) : /* @__PURE__ */ jsx3(Text3, { color: C.signal, children: `Expires in ${countdown(left)}` })
|
|
159
160
|
] }),
|
|
160
161
|
/* @__PURE__ */ jsx3(Text3, { children: " " }),
|
|
161
|
-
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: left !== null ? "g
|
|
162
|
+
/* @__PURE__ */ jsx3(Text3, { color: C.muted, children: left !== null ? "g new code \xB7 esc back" : "esc back" })
|
|
162
163
|
] })
|
|
163
164
|
] })
|
|
164
165
|
] });
|
|
@@ -171,7 +172,7 @@ function accessLabel(a) {
|
|
|
171
172
|
if (!a) return null;
|
|
172
173
|
if (a.kind === "user") return { text: a.user, color: C.muted };
|
|
173
174
|
if (a.kind === "allow") return { text: a.reason, color: C.muted };
|
|
174
|
-
return { text: "
|
|
175
|
+
return { text: "no password", color: C.warn };
|
|
175
176
|
}
|
|
176
177
|
function RequestList(props) {
|
|
177
178
|
const { records, selected, focused, height, width } = props;
|
|
@@ -179,14 +180,14 @@ function RequestList(props) {
|
|
|
179
180
|
const visible = records.slice(start, start + height);
|
|
180
181
|
const accessW = Math.max(0, ...visible.map((r) => (accessLabel(r.access)?.text.length ?? -1) + 1));
|
|
181
182
|
const pathW = Math.max(10, width - 54 - accessW);
|
|
182
|
-
const title = props.filter ? `
|
|
183
|
+
const title = props.filter ? `REQUESTS \xB7 filter: ${props.filter} ` : "REQUESTS ";
|
|
183
184
|
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", children: [
|
|
184
185
|
/* @__PURE__ */ jsx4(Text4, { color: C.muted, children: title + "\u2500".repeat(Math.max(0, width - title.length)) }),
|
|
185
|
-
records.length === 0 ? /* @__PURE__ */ jsx4(Text4, { color: C.muted, children: props.filter ? "
|
|
186
|
+
records.length === 0 ? /* @__PURE__ */ jsx4(Text4, { color: C.muted, children: props.filter ? "No requests match that filter." : "No requests yet. They show up here as they arrive." }) : visible.map((r, i) => {
|
|
186
187
|
const sel = focused && start + i === selected;
|
|
187
188
|
const method = r.websocket ? "WS" : r.method;
|
|
188
189
|
const status = r.status === null ? "\u2026" : String(r.status);
|
|
189
|
-
const tail = r.websocket && r.status === 101 ? "
|
|
190
|
+
const tail = r.websocket && r.status === 101 ? "open" : r.durationMs === null ? "" : formatDuration(r.durationMs);
|
|
190
191
|
const acc = accessLabel(r.access);
|
|
191
192
|
return /* @__PURE__ */ jsxs4(Text4, { inverse: sel, wrap: "truncate-end", children: [
|
|
192
193
|
/* @__PURE__ */ jsx4(Text4, { color: C.muted, children: formatTime(r.startedAt) }),
|
|
@@ -212,10 +213,10 @@ function RequestList(props) {
|
|
|
212
213
|
import { Box as Box4, Text as Text5 } from "ink";
|
|
213
214
|
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
214
215
|
function note(t) {
|
|
215
|
-
if (t.status === "failed") return { text: t.error ?? "
|
|
216
|
-
if (t.status === "no-listener") return { text: `
|
|
217
|
-
if (t.status === "opening") return { text: "
|
|
218
|
-
if (t.adHoc) return { text: "
|
|
216
|
+
if (t.status === "failed") return { text: t.error ?? "failed", color: C.err };
|
|
217
|
+
if (t.status === "no-listener") return { text: `nothing on port ${t.port}`, color: C.warn };
|
|
218
|
+
if (t.status === "opening") return { text: "opening\u2026", color: C.muted };
|
|
219
|
+
if (t.adHoc) return { text: "this session only", color: C.muted };
|
|
219
220
|
return null;
|
|
220
221
|
}
|
|
221
222
|
function dot(t) {
|
|
@@ -233,7 +234,7 @@ function TunnelTable({ tunnels, selected, focused }) {
|
|
|
233
234
|
return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
|
|
234
235
|
/* @__PURE__ */ jsxs5(Text5, { color: C.muted, children: [
|
|
235
236
|
" ",
|
|
236
|
-
fit("
|
|
237
|
+
fit("TUNNEL", nameW),
|
|
237
238
|
" ",
|
|
238
239
|
fit("URL", urlW),
|
|
239
240
|
anyAuth ? " " : "",
|
|
@@ -242,7 +243,7 @@ function TunnelTable({ tunnels, selected, focused }) {
|
|
|
242
243
|
" ",
|
|
243
244
|
"REQ".padStart(4),
|
|
244
245
|
" ",
|
|
245
|
-
"
|
|
246
|
+
"ERR".padStart(5)
|
|
246
247
|
] }),
|
|
247
248
|
tunnels.map((t, i) => {
|
|
248
249
|
const d = dot(t);
|
|
@@ -273,15 +274,15 @@ function TunnelTable({ tunnels, selected, focused }) {
|
|
|
273
274
|
// src/ui/App.tsx
|
|
274
275
|
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
275
276
|
var KEYS = [
|
|
276
|
-
["n", "
|
|
277
|
-
["c", "
|
|
278
|
-
["o", "
|
|
279
|
-
["i", "
|
|
280
|
-
["m", "QR
|
|
281
|
-
["f", "
|
|
282
|
-
["\u21B5", "
|
|
283
|
-
["tab", "
|
|
284
|
-
["q", "
|
|
277
|
+
["n", "new tunnel"],
|
|
278
|
+
["c", "copy URL"],
|
|
279
|
+
["o", "open in browser"],
|
|
280
|
+
["i", "inspector"],
|
|
281
|
+
["m", "QR for phone"],
|
|
282
|
+
["f", "filter"],
|
|
283
|
+
["\u21B5", "details"],
|
|
284
|
+
["tab", "focus"],
|
|
285
|
+
["q", "quit"]
|
|
285
286
|
];
|
|
286
287
|
function App({ session, version, system, onExit, inspector = null, inspectorError }) {
|
|
287
288
|
const subState = useCallback((fn) => session.subscribe(fn), [session]);
|
|
@@ -296,7 +297,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
|
|
|
296
297
|
const [focus, setFocus] = useState("requests");
|
|
297
298
|
const [selReq, setSelReq] = useState(0);
|
|
298
299
|
const [selTun, setSelTun] = useState(0);
|
|
299
|
-
const [message, setMessage] = useState(inspectorError ? { text: `
|
|
300
|
+
const [message, setMessage] = useState(inspectorError ? { text: `Inspector failed to start: ${inspectorError}`, tone: "warn" } : null);
|
|
300
301
|
const [now, setNow] = useState(() => Date.now());
|
|
301
302
|
const quitting = useRef(false);
|
|
302
303
|
const records = useMemo(() => {
|
|
@@ -332,36 +333,36 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
|
|
|
332
333
|
async function quit() {
|
|
333
334
|
if (quitting.current) return;
|
|
334
335
|
quitting.current = true;
|
|
335
|
-
setMessage({ text: "
|
|
336
|
+
setMessage({ text: "Closing tunnels\u2026", tone: "muted" });
|
|
336
337
|
await session.stop();
|
|
337
338
|
onExit();
|
|
338
339
|
}
|
|
339
340
|
async function openInspector() {
|
|
340
|
-
if (!inspector) return setMessage({ text: "
|
|
341
|
+
if (!inspector) return setMessage({ text: "Inspector is off (--no-inspect).", tone: "warn" });
|
|
341
342
|
const ok = await system.open(inspector.openUrl);
|
|
342
|
-
setMessage(ok ? { text: "
|
|
343
|
+
setMessage(ok ? { text: "Opening the inspector", tone: "signal" } : { text: "Could not open the browser.", tone: "warn" });
|
|
343
344
|
}
|
|
344
345
|
async function act(kind) {
|
|
345
346
|
const t = currentTunnel();
|
|
346
|
-
if (!t?.url) return setMessage({ text: "
|
|
347
|
+
if (!t?.url) return setMessage({ text: "That tunnel has no URL yet.", tone: "warn" });
|
|
347
348
|
if (kind === "copy") {
|
|
348
349
|
const ok = await system.copy(t.url);
|
|
349
|
-
setMessage(ok ? { text: `URL
|
|
350
|
+
setMessage(ok ? { text: `URL copied: ${t.url}`, tone: "signal" } : { text: "Could not copy \u2014 select the URL in the table", tone: "warn" });
|
|
350
351
|
} else {
|
|
351
352
|
const ok = await system.open(t.url);
|
|
352
|
-
setMessage(ok ? { text: `
|
|
353
|
+
setMessage(ok ? { text: `Opening ${t.url}`, tone: "signal" } : { text: "Could not open the browser", tone: "warn" });
|
|
353
354
|
}
|
|
354
355
|
}
|
|
355
356
|
function showQr() {
|
|
356
357
|
const t = currentTunnel();
|
|
357
358
|
const link = t ? session.qrLink(t.key) : null;
|
|
358
|
-
if (!t || !link) return setMessage({ text: "
|
|
359
|
+
if (!t || !link) return setMessage({ text: "That tunnel has no URL yet.", tone: "warn" });
|
|
359
360
|
setNow(Date.now());
|
|
360
361
|
setMode({ kind: "qr", key: t.key, link });
|
|
361
362
|
}
|
|
362
363
|
function submitPort(value) {
|
|
363
364
|
const port = Number(value.trim());
|
|
364
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) return setMessage({ text: `
|
|
365
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return setMessage({ text: `Invalid port: ${value || "(empty)"} (use 1\u201365535)`, tone: "err" });
|
|
365
366
|
setMode({ kind: "name", value: "", port });
|
|
366
367
|
}
|
|
367
368
|
function submitName(value, port) {
|
|
@@ -369,12 +370,12 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
|
|
|
369
370
|
const name = raw ? normalizeName(raw) : null;
|
|
370
371
|
if (name !== null) {
|
|
371
372
|
const v = validateName(name);
|
|
372
|
-
if (!v.ok) return setMessage({ text: v.reason === "reserved" ? `
|
|
373
|
+
if (!v.ok) return setMessage({ text: v.reason === "reserved" ? `"${raw}" is reserved. Pick another name.` : `Invalid name. Use only lowercase letters and digits (no hyphens), up to ${MAX_NAME} characters.`, tone: "err" });
|
|
373
374
|
}
|
|
374
375
|
setMode({ kind: "list" });
|
|
375
376
|
void session.add({ name, port });
|
|
376
377
|
setMessage(
|
|
377
|
-
name ? { text: `
|
|
378
|
+
name ? { text: `To keep it, add to duto.yml: ${name}: ${port}`, tone: "muted" } : { text: `Opening localhost:${port} for this session only`, tone: "muted" }
|
|
378
379
|
);
|
|
379
380
|
}
|
|
380
381
|
useInput((input, key) => {
|
|
@@ -433,7 +434,7 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
|
|
|
433
434
|
if (input === "n") return setMode({ kind: "port", value: "" });
|
|
434
435
|
});
|
|
435
436
|
const live = state.tunnels.filter((t) => t.status === "live").length;
|
|
436
|
-
const conn = state.connection === "online" ? { text: "
|
|
437
|
+
const conn = state.connection === "online" ? { text: "connected \u25CF", color: C.signal } : state.connection === "connecting" ? { text: "connecting\u2026", color: C.muted } : { text: "reconnecting\u2026", color: C.warn };
|
|
437
438
|
const listHeight = Math.max(3, rows - state.tunnels.length - (inspector ? 10 : 9));
|
|
438
439
|
const detailRecord = mode.kind === "detail" ? all.find((r) => r.id === mode.id) : void 0;
|
|
439
440
|
const qrTunnel = mode.kind === "qr" ? state.tunnels.find((t) => t.key === mode.key) : void 0;
|
|
@@ -454,20 +455,20 @@ function App({ session, version, system, onExit, inspector = null, inspectorErro
|
|
|
454
455
|
live,
|
|
455
456
|
"/",
|
|
456
457
|
state.tunnels.length,
|
|
457
|
-
"
|
|
458
|
+
" live"
|
|
458
459
|
] })
|
|
459
460
|
] }),
|
|
460
461
|
/* @__PURE__ */ jsx6(Text6, { color: conn.color, children: conn.text })
|
|
461
462
|
] }),
|
|
462
463
|
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
463
464
|
/* @__PURE__ */ jsx6(TunnelTable, { tunnels: state.tunnels, selected: selTun, focused: focus === "tunnels" }),
|
|
464
|
-
inspector && /* @__PURE__ */ jsx6(Text6, { color: C.muted, children: `
|
|
465
|
+
inspector && /* @__PURE__ */ jsx6(Text6, { color: C.muted, children: `inspector ${inspector.url}` }),
|
|
465
466
|
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
466
467
|
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 }),
|
|
467
468
|
/* @__PURE__ */ jsx6(Text6, { children: " " }),
|
|
468
|
-
mode.kind === "filter" && /* @__PURE__ */ jsx6(Prompt, { label: "
|
|
469
|
-
mode.kind === "port" && /* @__PURE__ */ jsx6(Prompt, { label: "
|
|
470
|
-
mode.kind === "name" && /* @__PURE__ */ jsx6(Prompt, { label: `
|
|
469
|
+
mode.kind === "filter" && /* @__PURE__ */ jsx6(Prompt, { label: "Filter (tunnel, method, path or status):", value: mode.value, hint: "\u21B5 apply \xB7 esc cancel" }),
|
|
470
|
+
mode.kind === "port" && /* @__PURE__ */ jsx6(Prompt, { label: "New tunnel \u2014 local port:", value: mode.value, hint: "\u21B5 continue \xB7 esc cancel" }),
|
|
471
|
+
mode.kind === "name" && /* @__PURE__ */ jsx6(Prompt, { label: `Name for localhost:${mode.port} (optional):`, value: mode.value, hint: "\u21B5 open \xB7 esc cancel" }),
|
|
471
472
|
mode.kind !== "filter" && mode.kind !== "port" && mode.kind !== "name" && /* @__PURE__ */ jsx6(Text6, { color: message ? C[message.tone] : C.muted, children: message?.text ?? " " }),
|
|
472
473
|
/* @__PURE__ */ jsx6(Text6, { wrap: "truncate-end", children: KEYS.map(([k, label]) => /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
473
474
|
/* @__PURE__ */ jsx6(Text6, { inverse: true, color: C.signal, children: ` ${k} ` }),
|