@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.
@@ -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>