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