@n42/cli 0.3.8 → 0.3.10
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/dist/assets/copy-light.svg +69 -0
- package/dist/assets/curl-modal.js +577 -0
- package/dist/assets/discover.html.template +2 -0
- package/dist/assets/discover.js +67 -0
- package/dist/assets/download-light.svg +69 -0
- package/dist/assets/open-external-light.svg +69 -0
- package/dist/assets/terminal-dark.css +139 -0
- package/dist/assets/terminal-light.css +146 -0
- package/dist/assets/terminal.js +777 -0
- package/dist/n42 +1 -1
- package/package.json +1 -1
- package/src/assets/copy-light.svg +69 -0
- package/src/assets/discover.html.template +2 -0
- package/src/assets/discover.js +67 -0
- package/src/assets/download-light.svg +69 -0
- package/src/assets/open-external-light.svg +69 -0
- package/src/assets/terminal-dark.css +139 -0
- package/src/assets/terminal-light.css +146 -0
- package/src/assets/terminal.js +777 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
|
+
|
|
4
|
+
<svg
|
|
5
|
+
width="800px"
|
|
6
|
+
height="800px"
|
|
7
|
+
viewBox="0 0 512 512"
|
|
8
|
+
version="1.1"
|
|
9
|
+
id="svg8"
|
|
10
|
+
sodipodi:docname="copy-light.svg"
|
|
11
|
+
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
|
12
|
+
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
13
|
+
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
14
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
15
|
+
xmlns:svg="http://www.w3.org/2000/svg"
|
|
16
|
+
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
17
|
+
xmlns:cc="http://creativecommons.org/ns#"
|
|
18
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
|
19
|
+
<sodipodi:namedview
|
|
20
|
+
id="namedview432"
|
|
21
|
+
pagecolor="#ffffff"
|
|
22
|
+
bordercolor="#000000"
|
|
23
|
+
borderopacity="0.25"
|
|
24
|
+
inkscape:showpageshadow="2"
|
|
25
|
+
inkscape:pageopacity="0.0"
|
|
26
|
+
inkscape:pagecheckerboard="0"
|
|
27
|
+
inkscape:deskcolor="#d1d1d1"
|
|
28
|
+
showgrid="false"
|
|
29
|
+
inkscape:zoom="1.28375"
|
|
30
|
+
inkscape:cx="366.1149"
|
|
31
|
+
inkscape:cy="400.77897"
|
|
32
|
+
inkscape:window-width="3440"
|
|
33
|
+
inkscape:window-height="1371"
|
|
34
|
+
inkscape:window-x="0"
|
|
35
|
+
inkscape:window-y="32"
|
|
36
|
+
inkscape:window-maximized="1"
|
|
37
|
+
inkscape:current-layer="svg8" />
|
|
38
|
+
<defs
|
|
39
|
+
id="defs12" />
|
|
40
|
+
<title
|
|
41
|
+
id="title2">copy</title>
|
|
42
|
+
<g
|
|
43
|
+
id="Page-1"
|
|
44
|
+
stroke="none"
|
|
45
|
+
stroke-width="1"
|
|
46
|
+
fill="none"
|
|
47
|
+
fill-rule="evenodd"
|
|
48
|
+
transform="translate(0,10.666668)">
|
|
49
|
+
<g
|
|
50
|
+
id="copy"
|
|
51
|
+
fill="#000000"
|
|
52
|
+
transform="translate(85.333333,42.666667)"
|
|
53
|
+
style="fill:#6b7280;fill-opacity:1">
|
|
54
|
+
<path
|
|
55
|
+
d="M 341.33333,85.333333 V 405.33333 H 85.333333 V 85.333333 Z M 298.66667,128 H 128 v 234.66667 h 170.66667 z m -64,-128 V 42.666667 H 42.666667 V 298.66667 H 0 V 0 Z"
|
|
56
|
+
id="path4"
|
|
57
|
+
style="fill:#6b7280;fill-opacity:1" />
|
|
58
|
+
</g>
|
|
59
|
+
</g>
|
|
60
|
+
<metadata
|
|
61
|
+
id="metadata877">
|
|
62
|
+
<rdf:RDF>
|
|
63
|
+
<cc:Work
|
|
64
|
+
rdf:about="">
|
|
65
|
+
<dc:title>copy</dc:title>
|
|
66
|
+
</cc:Work>
|
|
67
|
+
</rdf:RDF>
|
|
68
|
+
</metadata>
|
|
69
|
+
</svg>
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
class CurlModal {
|
|
2
|
+
constructor(opts = {}) {
|
|
3
|
+
this.opts = {
|
|
4
|
+
title: "Request",
|
|
5
|
+
brand: "curl",
|
|
6
|
+
...opts,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
this._active = false;
|
|
10
|
+
this._controller = null;
|
|
11
|
+
this._lastFocused = null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async openAndRun(req) {
|
|
15
|
+
this.open();
|
|
16
|
+
try {
|
|
17
|
+
await this.run(req);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// errors already printed by run(); rethrow if you want
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
open() {
|
|
24
|
+
if (!this.overlay) {
|
|
25
|
+
this._ensureDom();
|
|
26
|
+
}
|
|
27
|
+
if (this._active) return;
|
|
28
|
+
this._active = true;
|
|
29
|
+
|
|
30
|
+
this._lastFocused = document.activeElement;
|
|
31
|
+
|
|
32
|
+
this.overlay.classList.remove("hidden");
|
|
33
|
+
this.dialog.classList.remove("hidden");
|
|
34
|
+
|
|
35
|
+
// Initial focus: close button (per common modal patterns)
|
|
36
|
+
this.btnClose.focus();
|
|
37
|
+
|
|
38
|
+
// prevent background scroll
|
|
39
|
+
this._prevOverflow = document.documentElement.style.overflow;
|
|
40
|
+
document.documentElement.style.overflow = "hidden";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
close() {
|
|
44
|
+
if (!this._active) return;
|
|
45
|
+
this._active = false;
|
|
46
|
+
|
|
47
|
+
this.abort("Modal closed");
|
|
48
|
+
|
|
49
|
+
this.overlay.classList.add("hidden");
|
|
50
|
+
this.dialog.classList.add("hidden");
|
|
51
|
+
|
|
52
|
+
document.documentElement.style.overflow = this._prevOverflow || "";
|
|
53
|
+
|
|
54
|
+
// restore focus to trigger
|
|
55
|
+
if (this._lastFocused && typeof this._lastFocused.focus === "function") {
|
|
56
|
+
this._lastFocused.focus();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
abort(reason = "Aborted") {
|
|
61
|
+
if (this._controller) {
|
|
62
|
+
try { this._controller.abort(reason); } catch {}
|
|
63
|
+
}
|
|
64
|
+
this._controller = null;
|
|
65
|
+
this._setBusy(false);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async run(req) {
|
|
69
|
+
const normalized = this._normalizeReq(req);
|
|
70
|
+
this._resetTerminal();
|
|
71
|
+
|
|
72
|
+
this._printLine(`${this.opts.brand} ${this._toCurlArgs(normalized)}`);
|
|
73
|
+
this._printLine("");
|
|
74
|
+
|
|
75
|
+
this._setBusy(true);
|
|
76
|
+
|
|
77
|
+
const t0 = performance.now();
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
this._controller = controller;
|
|
80
|
+
|
|
81
|
+
// Timeout: use AbortSignal.timeout() if available; else manual.
|
|
82
|
+
let timeoutId = null;
|
|
83
|
+
const timeoutMs = normalized.timeoutMs;
|
|
84
|
+
|
|
85
|
+
let signal = controller.signal;
|
|
86
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" && timeoutMs > 0) {
|
|
87
|
+
// combine signals: abort either manual or timeout
|
|
88
|
+
signal = this._anySignal([controller.signal, AbortSignal.timeout(timeoutMs)]);
|
|
89
|
+
} else if (timeoutMs > 0) {
|
|
90
|
+
timeoutId = setTimeout(() => controller.abort("Timeout"), timeoutMs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const fetchInit = {
|
|
95
|
+
method: normalized.method,
|
|
96
|
+
headers: normalized.headers,
|
|
97
|
+
body: normalized.body,
|
|
98
|
+
signal,
|
|
99
|
+
credentials: normalized.credentials,
|
|
100
|
+
redirect: normalized.redirect,
|
|
101
|
+
cache: "no-store",
|
|
102
|
+
mode: normalized.mode,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
this._printLine(`> ${normalized.method} ${normalized.url}`);
|
|
106
|
+
for (const [k, v] of Object.entries(normalized.headers || {})) {
|
|
107
|
+
this._printLine(`> ${k}: ${v}`);
|
|
108
|
+
}
|
|
109
|
+
if (normalized.body) this._printLine(`> (body ${this._byteLen(normalized.body)} bytes)`);
|
|
110
|
+
this._printLine("");
|
|
111
|
+
|
|
112
|
+
const res = await fetch(normalized.url, fetchInit);
|
|
113
|
+
|
|
114
|
+
const dt = (performance.now() - t0);
|
|
115
|
+
this._printLine(`< HTTP ${res.status} ${res.statusText} (${dt.toFixed(0)} ms)`);
|
|
116
|
+
|
|
117
|
+
res.headers.forEach((v, k) => this._printLine(`< ${k}: ${v}`));
|
|
118
|
+
this._printLine("");
|
|
119
|
+
|
|
120
|
+
const { text, truncated, bytesRead } = await this._readTextWithLimit(res, normalized.maxBytes);
|
|
121
|
+
if (truncated) {
|
|
122
|
+
this._printLine(`... [truncated after ${bytesRead} bytes (maxBytes=${normalized.maxBytes})] ...`);
|
|
123
|
+
this._printLine("");
|
|
124
|
+
}
|
|
125
|
+
this._printRaw(text);
|
|
126
|
+
|
|
127
|
+
this._printLine("");
|
|
128
|
+
this._printLine(`[done]`);
|
|
129
|
+
this._setBusy(false);
|
|
130
|
+
return { status: res.status, headers: res.headers, body: text, truncated };
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
const name = err?.name || "Error";
|
|
134
|
+
const msg = err?.message || String(err);
|
|
135
|
+
|
|
136
|
+
//this._printLine("");
|
|
137
|
+
this._printLine(`[${name}] ${msg}`);
|
|
138
|
+
this._setBusy(false);
|
|
139
|
+
|
|
140
|
+
return { status: 0, headers: new Headers(), body: "", error: msg };
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
144
|
+
this._controller = null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------- private ----------------
|
|
149
|
+
|
|
150
|
+
_normalizeReq(req) {
|
|
151
|
+
if (!req || typeof req.url !== "string") throw new Error("Missing url");
|
|
152
|
+
|
|
153
|
+
const method = (req.method || "GET").toUpperCase();
|
|
154
|
+
const headers = { ...(req.headers || {}) };
|
|
155
|
+
|
|
156
|
+
// If body is object, default JSON
|
|
157
|
+
let body = req.body ?? null;
|
|
158
|
+
if (body && typeof body === "object" && !(body instanceof FormData) && !(body instanceof Blob)) {
|
|
159
|
+
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
160
|
+
headers["Content-Type"] = "application/json";
|
|
161
|
+
}
|
|
162
|
+
body = JSON.stringify(body);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
url: req.url,
|
|
167
|
+
method,
|
|
168
|
+
headers,
|
|
169
|
+
body,
|
|
170
|
+
timeoutMs: Number.isFinite(req.timeoutMs) ? req.timeoutMs : 15000,
|
|
171
|
+
maxBytes: Number.isFinite(req.maxBytes) ? req.maxBytes : 500_000,
|
|
172
|
+
credentials: req.credentials || "omit", // set "include" if you need cookies
|
|
173
|
+
redirect: req.redirect || "follow",
|
|
174
|
+
mode: req.mode || "cors",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_ensureDom() {
|
|
179
|
+
if (this.overlay) return;
|
|
180
|
+
|
|
181
|
+
const textColor = this.opts.textColor;
|
|
182
|
+
const terminalTextColor = this.opts.terminalTextColor;
|
|
183
|
+
|
|
184
|
+
// Styles (namespaced)
|
|
185
|
+
const style = document.createElement("style");
|
|
186
|
+
style.textContent = `
|
|
187
|
+
.cm-overlay {
|
|
188
|
+
position: fixed; inset: 0;
|
|
189
|
+
background: rgba(0,0,0,.55);
|
|
190
|
+
display: flex;
|
|
191
|
+
align-items: center;
|
|
192
|
+
justify-content: center;
|
|
193
|
+
padding: 24px;
|
|
194
|
+
z-index: 2147483647;
|
|
195
|
+
}
|
|
196
|
+
.cm-overlay.hidden {
|
|
197
|
+
display: none;
|
|
198
|
+
}
|
|
199
|
+
.cm-dialog {
|
|
200
|
+
width: min(980px, 100%);
|
|
201
|
+
height: min(720px, 100%);
|
|
202
|
+
background: #0b0f14;
|
|
203
|
+
color: ${textColor};
|
|
204
|
+
border: 1px solid rgba(255,255,255,.12);
|
|
205
|
+
border-radius: 12px;
|
|
206
|
+
box-shadow: 0 18px 60px rgba(0,0,0,.55);
|
|
207
|
+
display: flex;
|
|
208
|
+
flex-direction: column;
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
211
|
+
}
|
|
212
|
+
.cm-display.hidden {
|
|
213
|
+
display: none;
|
|
214
|
+
}
|
|
215
|
+
.cm-head {
|
|
216
|
+
display: flex; align-items: center; gap: 12px;
|
|
217
|
+
padding: 10px 10px 10px 14px;
|
|
218
|
+
border-bottom: 1px solid rgba(255,255,255,.10);
|
|
219
|
+
background: rgba(255,255,255,.03);
|
|
220
|
+
}
|
|
221
|
+
.cm-title {
|
|
222
|
+
font-weight: 600;
|
|
223
|
+
font-size: 13px;
|
|
224
|
+
opacity: .95;
|
|
225
|
+
}
|
|
226
|
+
.cm-spacer { flex: 1; }
|
|
227
|
+
.cm-btn {
|
|
228
|
+
appearance: none;
|
|
229
|
+
border: 1px solid rgba(255,255,255,.18);
|
|
230
|
+
background: rgba(255,255,255,0);
|
|
231
|
+
color: inherit;
|
|
232
|
+
padding: 6px 10px;
|
|
233
|
+
border-radius: 6px;
|
|
234
|
+
font-size: 12px;
|
|
235
|
+
cursor: pointer;
|
|
236
|
+
}
|
|
237
|
+
.cm-btn:hover {
|
|
238
|
+
border: 1px solid rgba(255,255,255,.38);
|
|
239
|
+
}
|
|
240
|
+
.cm-btn:disabled { opacity: .55; cursor: not-allowed; }
|
|
241
|
+
.cm-body {
|
|
242
|
+
flex: 1;
|
|
243
|
+
overflow: auto;
|
|
244
|
+
padding: 12px;
|
|
245
|
+
|
|
246
|
+
scrollbar-color: #323232 #0b0f14;
|
|
247
|
+
scrollbar-width: thin;
|
|
248
|
+
}
|
|
249
|
+
.cm-body::-webkit-scrollbar {
|
|
250
|
+
width: 10px;
|
|
251
|
+
}
|
|
252
|
+
.cm-body::-webkit-scrollbar-track {
|
|
253
|
+
background: #0b0f14;
|
|
254
|
+
}
|
|
255
|
+
.cm-body::-webkit-scrollbar-thumb {
|
|
256
|
+
background: #323232;
|
|
257
|
+
border-radius: 6px;
|
|
258
|
+
}
|
|
259
|
+
.cm-body::-webkit-scrollbar-thumb:hover {
|
|
260
|
+
background: #656565;
|
|
261
|
+
}
|
|
262
|
+
.cm-term {
|
|
263
|
+
white-space: pre-wrap;
|
|
264
|
+
word-break: break-word;
|
|
265
|
+
color: ${terminalTextColor};
|
|
266
|
+
line-height: 1.35;
|
|
267
|
+
font-size: 12px;
|
|
268
|
+
}
|
|
269
|
+
.cm-foot {
|
|
270
|
+
padding: 10px 12px;
|
|
271
|
+
border-top: 1px solid rgba(255,255,255,.10);
|
|
272
|
+
display: flex; align-items: center; gap: 10px;
|
|
273
|
+
background: rgba(255,255,255,.02);
|
|
274
|
+
}
|
|
275
|
+
.cm-status { font-size: 12px; opacity: .85; }
|
|
276
|
+
.cm-kbd {
|
|
277
|
+
font-size: 11px; opacity: .7;
|
|
278
|
+
border: 1px solid rgba(255,255,255,.16);
|
|
279
|
+
padding: 2px 6px;
|
|
280
|
+
border-radius: 6px;
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
document.head.appendChild(style);
|
|
284
|
+
|
|
285
|
+
// Overlay
|
|
286
|
+
const overlay = document.createElement("div");
|
|
287
|
+
overlay.className = "cm-overlay";
|
|
288
|
+
overlay.hidden = true;
|
|
289
|
+
|
|
290
|
+
// Dialog
|
|
291
|
+
const dialog = document.createElement("div");
|
|
292
|
+
dialog.className = "cm-dialog";
|
|
293
|
+
dialog.hidden = true;
|
|
294
|
+
dialog.setAttribute("role", "dialog");
|
|
295
|
+
dialog.setAttribute("aria-modal", "true"); // per aria-modal guidance
|
|
296
|
+
dialog.setAttribute("aria-label", this.opts.title);
|
|
297
|
+
|
|
298
|
+
// Header
|
|
299
|
+
const head = document.createElement("div");
|
|
300
|
+
head.className = "cm-head";
|
|
301
|
+
|
|
302
|
+
const title = document.createElement("div");
|
|
303
|
+
title.className = "cm-title";
|
|
304
|
+
title.textContent = this.opts.title;
|
|
305
|
+
|
|
306
|
+
const spacer = document.createElement("div");
|
|
307
|
+
spacer.className = "cm-spacer";
|
|
308
|
+
|
|
309
|
+
const btnCopy = document.createElement("button");
|
|
310
|
+
btnCopy.className = "cm-btn";
|
|
311
|
+
btnCopy.type = "button";
|
|
312
|
+
btnCopy.textContent = "Copy";
|
|
313
|
+
btnCopy.addEventListener("click", () => this._copy());
|
|
314
|
+
|
|
315
|
+
const btnDownload = document.createElement("button");
|
|
316
|
+
btnDownload.className = "cm-btn";
|
|
317
|
+
btnDownload.type = "button";
|
|
318
|
+
btnDownload.textContent = "Download";
|
|
319
|
+
btnDownload.addEventListener("click", () => this._download());
|
|
320
|
+
|
|
321
|
+
const btnAbort = document.createElement("button");
|
|
322
|
+
btnAbort.className = "cm-btn";
|
|
323
|
+
btnAbort.type = "button";
|
|
324
|
+
btnAbort.textContent = "Cancel";
|
|
325
|
+
btnAbort.addEventListener("click", () => this.abort("User canceled"));
|
|
326
|
+
|
|
327
|
+
const btnClose = document.createElement("button");
|
|
328
|
+
btnClose.className = "cm-btn";
|
|
329
|
+
btnClose.type = "button";
|
|
330
|
+
btnClose.textContent = "Close";
|
|
331
|
+
btnClose.addEventListener("click", () => this.close());
|
|
332
|
+
|
|
333
|
+
head.append(title, spacer, btnCopy, btnDownload, btnClose);
|
|
334
|
+
|
|
335
|
+
// Body
|
|
336
|
+
const body = document.createElement("div");
|
|
337
|
+
body.className = "cm-body";
|
|
338
|
+
|
|
339
|
+
const term = document.createElement("div");
|
|
340
|
+
term.className = "cm-term";
|
|
341
|
+
term.setAttribute("role", "log");
|
|
342
|
+
term.setAttribute("aria-live", "polite");
|
|
343
|
+
|
|
344
|
+
body.appendChild(term);
|
|
345
|
+
|
|
346
|
+
// Footer
|
|
347
|
+
const foot = document.createElement("div");
|
|
348
|
+
foot.className = "cm-foot";
|
|
349
|
+
|
|
350
|
+
const status = document.createElement("div");
|
|
351
|
+
status.className = "cm-status";
|
|
352
|
+
status.textContent = "Ready";
|
|
353
|
+
|
|
354
|
+
const hint = document.createElement("div");
|
|
355
|
+
hint.className = "cm-spacer";
|
|
356
|
+
hint.textContent = "";
|
|
357
|
+
|
|
358
|
+
const kbdEsc = document.createElement("span");
|
|
359
|
+
kbdEsc.className = "cm-kbd";
|
|
360
|
+
kbdEsc.textContent = "ESC";
|
|
361
|
+
|
|
362
|
+
const hint2 = document.createElement("div");
|
|
363
|
+
hint2.style.opacity = ".75";
|
|
364
|
+
hint2.style.fontSize = "12px";
|
|
365
|
+
hint2.textContent = "to close";
|
|
366
|
+
|
|
367
|
+
foot.append(status, hint, kbdEsc, hint2);
|
|
368
|
+
|
|
369
|
+
dialog.append(head, body, foot);
|
|
370
|
+
overlay.appendChild(dialog);
|
|
371
|
+
document.body.appendChild(overlay);
|
|
372
|
+
|
|
373
|
+
// Store refs
|
|
374
|
+
this.overlay = overlay;
|
|
375
|
+
this.dialog = dialog;
|
|
376
|
+
this.term = term;
|
|
377
|
+
this.statusEl = status;
|
|
378
|
+
|
|
379
|
+
this.btnCopy = btnCopy;
|
|
380
|
+
this.btnDownload = btnDownload;
|
|
381
|
+
this.btnAbort = btnAbort;
|
|
382
|
+
this.btnClose = btnClose;
|
|
383
|
+
|
|
384
|
+
// Close when clicking outside (optional)
|
|
385
|
+
overlay.addEventListener("mousedown", (e) => {
|
|
386
|
+
if (e.target === overlay) this.close();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Key handling + focus trap
|
|
390
|
+
document.addEventListener("keydown", (e) => {
|
|
391
|
+
if (!this._active) return;
|
|
392
|
+
|
|
393
|
+
if (e.key === "Escape") {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
this.close();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (e.key === "Tab") {
|
|
400
|
+
this._trapTab(e);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
_trapTab(e) {
|
|
406
|
+
// Basic focus trap: keep tabbing inside dialog controls.
|
|
407
|
+
const focusables = this.dialog.querySelectorAll(
|
|
408
|
+
'button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])'
|
|
409
|
+
);
|
|
410
|
+
const list = Array.from(focusables).filter(el => !el.disabled && el.offsetParent !== null);
|
|
411
|
+
if (list.length === 0) return;
|
|
412
|
+
|
|
413
|
+
const first = list[0];
|
|
414
|
+
const last = list[list.length - 1];
|
|
415
|
+
const active = document.activeElement;
|
|
416
|
+
|
|
417
|
+
if (e.shiftKey && active === first) {
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
last.focus();
|
|
420
|
+
} else if (!e.shiftKey && active === last) {
|
|
421
|
+
e.preventDefault();
|
|
422
|
+
first.focus();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
_setBusy(isBusy) {
|
|
427
|
+
this.btnAbort.disabled = !isBusy;
|
|
428
|
+
this.statusEl.textContent = isBusy ? "Running…" : "Ready";
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
_resetTerminal() {
|
|
432
|
+
this.term.textContent = "";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
_printLine(s) {
|
|
436
|
+
this.term.textContent += `${s}\n`;
|
|
437
|
+
this._scrollToBottom();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
_printRaw(s) {
|
|
441
|
+
this.term.textContent += s;
|
|
442
|
+
if (!s.endsWith("\n")) this.term.textContent += "\n";
|
|
443
|
+
this._scrollToBottom();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
_scrollToBottom() {
|
|
447
|
+
const body = this.term.parentElement;
|
|
448
|
+
body.scrollTop = body.scrollHeight;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async _copy() {
|
|
452
|
+
const text = this.term.textContent || "";
|
|
453
|
+
try {
|
|
454
|
+
await navigator.clipboard.writeText(text);
|
|
455
|
+
this.statusEl.textContent = "Copied";
|
|
456
|
+
} catch {
|
|
457
|
+
// fallback
|
|
458
|
+
const ta = document.createElement("textarea");
|
|
459
|
+
ta.value = text;
|
|
460
|
+
ta.style.position = "fixed";
|
|
461
|
+
ta.style.left = "-9999px";
|
|
462
|
+
document.body.appendChild(ta);
|
|
463
|
+
ta.select();
|
|
464
|
+
try { document.execCommand("copy"); this.statusEl.textContent = "Copied"; }
|
|
465
|
+
catch { this.statusEl.textContent = "Copy failed"; }
|
|
466
|
+
ta.remove();
|
|
467
|
+
}
|
|
468
|
+
setTimeout(() => (this.statusEl.textContent = "Ready"), 900);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
_download() {
|
|
472
|
+
const text = this.term.textContent || "";
|
|
473
|
+
const blob = new Blob([text], { type: "text/plain;charset=utf-8" });
|
|
474
|
+
const url = URL.createObjectURL(blob);
|
|
475
|
+
const a = document.createElement("a");
|
|
476
|
+
a.href = url;
|
|
477
|
+
a.download = `curl-output-${new Date().toISOString().slice(0,19).replace(/[:T]/g,"-")}.txt`;
|
|
478
|
+
document.body.appendChild(a);
|
|
479
|
+
a.click();
|
|
480
|
+
a.remove();
|
|
481
|
+
URL.revokeObjectURL(url);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
_toCurlArgs(req) {
|
|
485
|
+
// Presentational: escapes minimally for display (not execution-safe for all shells).
|
|
486
|
+
const parts = [];
|
|
487
|
+
parts.push(`-X ${req.method}`);
|
|
488
|
+
for (const [k, v] of Object.entries(req.headers || {})) {
|
|
489
|
+
parts.push(`-H ${this._shQuote(`${k}: ${v}`)}`);
|
|
490
|
+
}
|
|
491
|
+
if (req.body) parts.push(`--data ${this._shQuote(typeof req.body === "string" ? req.body : String(req.body))}`);
|
|
492
|
+
parts.push(this._shQuote(req.url));
|
|
493
|
+
return parts.join(" ");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
_shQuote(s) {
|
|
497
|
+
// single-quote shell style: ' -> '"'"'
|
|
498
|
+
const str = String(s);
|
|
499
|
+
return `'${str.replace(/'/g, `'"'"'`)}'`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
_byteLen(s) {
|
|
503
|
+
if (typeof s !== "string") s = String(s);
|
|
504
|
+
return new TextEncoder().encode(s).length;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async _readTextWithLimit(res, maxBytes) {
|
|
508
|
+
// Reads response body safely with byte limit.
|
|
509
|
+
// If no stream support, fall back to res.text() then truncate.
|
|
510
|
+
const bytesLimit = Math.max(0, maxBytes | 0);
|
|
511
|
+
|
|
512
|
+
if (!res.body || !res.body.getReader) {
|
|
513
|
+
const full = await res.text();
|
|
514
|
+
const enc = new TextEncoder().encode(full);
|
|
515
|
+
if (enc.length <= bytesLimit) return { text: full, truncated: false, bytesRead: enc.length };
|
|
516
|
+
// truncate bytes -> decode
|
|
517
|
+
const cut = enc.slice(0, bytesLimit);
|
|
518
|
+
return { text: new TextDecoder().decode(cut), truncated: true, bytesRead: bytesLimit };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const reader = res.body.getReader();
|
|
522
|
+
const chunks = [];
|
|
523
|
+
let bytesRead = 0;
|
|
524
|
+
let truncated = false;
|
|
525
|
+
|
|
526
|
+
while (true) {
|
|
527
|
+
const { value, done } = await reader.read();
|
|
528
|
+
if (done) break;
|
|
529
|
+
if (!value) continue;
|
|
530
|
+
|
|
531
|
+
if (bytesRead + value.byteLength > bytesLimit) {
|
|
532
|
+
const allowed = bytesLimit - bytesRead;
|
|
533
|
+
if (allowed > 0) chunks.push(value.slice(0, allowed));
|
|
534
|
+
truncated = true;
|
|
535
|
+
bytesRead = bytesLimit;
|
|
536
|
+
try { reader.cancel("maxBytes reached"); } catch {}
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
chunks.push(value);
|
|
541
|
+
bytesRead += value.byteLength;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const merged = this._concatU8(chunks, bytesRead);
|
|
545
|
+
const text = new TextDecoder().decode(merged);
|
|
546
|
+
return { text, truncated, bytesRead };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
_concatU8(chunks, total) {
|
|
550
|
+
const out = new Uint8Array(total);
|
|
551
|
+
let offset = 0;
|
|
552
|
+
for (const c of chunks) {
|
|
553
|
+
out.set(c, offset);
|
|
554
|
+
offset += c.byteLength;
|
|
555
|
+
if (offset >= total) break;
|
|
556
|
+
}
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
_anySignal(signals) {
|
|
561
|
+
// Combine multiple AbortSignals into one.
|
|
562
|
+
const controller = new AbortController();
|
|
563
|
+
const onAbort = (sig) => {
|
|
564
|
+
if (controller.signal.aborted) return;
|
|
565
|
+
controller.abort(sig.reason || "Aborted");
|
|
566
|
+
};
|
|
567
|
+
for (const sig of signals) {
|
|
568
|
+
if (!sig) continue;
|
|
569
|
+
if (sig.aborted) {
|
|
570
|
+
onAbort(sig);
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
sig.addEventListener("abort", () => onAbort(sig), { once: true });
|
|
574
|
+
}
|
|
575
|
+
return controller.signal;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<meta charset="utf-8" />
|
|
5
5
|
<title>Node42</title>
|
|
6
6
|
<link rel="stylesheet" href="../../assets/wrapper-light.css">
|
|
7
|
+
<link rel="stylesheet" href="../../assets/terminal-light.css">
|
|
7
8
|
</head>
|
|
8
9
|
<body>
|
|
9
10
|
<div id="app-header">
|
|
@@ -46,5 +47,6 @@
|
|
|
46
47
|
</div>
|
|
47
48
|
<script src="../../assets/discover.js" defer></script>
|
|
48
49
|
<script src="../../assets/wrapper.js" defer></script>
|
|
50
|
+
<script src="../../assets/terminal.js"></script>
|
|
49
51
|
</body>
|
|
50
52
|
</html>
|