qr-terminal 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 +159 -0
- package/index.js +63 -0
- package/package.json +47 -0
- package/src/braille.js +104 -0
- package/src/destruct.js +123 -0
- package/src/engine.js +286 -0
- package/src/export.js +76 -0
- package/src/formatters.js +44 -0
- package/src/handshake.js +112 -0
- package/src/live-preview.js +199 -0
- package/src/luma.js +58 -0
- package/src/matrix-rain.js +115 -0
- package/src/particle.js +91 -0
- package/src/prompts.js +449 -0
- package/src/shortlink.js +58 -0
- package/src/ui.js +183 -0
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# QR Terminal
|
|
2
|
+
|
|
3
|
+
A cinematic, interactive QR code generator that runs entirely in your terminal.
|
|
4
|
+
|
|
5
|
+
Generate QR codes for URLs, WiFi networks, contacts, and secrets — with live preview, animated rendering, gradient colors, phone handshake, and one-click export.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/Boweii22/QR_Terminal.git
|
|
13
|
+
cd QR_Terminal
|
|
14
|
+
npm install
|
|
15
|
+
npm start
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Install globally (run `qr` from anywhere)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm link
|
|
22
|
+
qr
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Requires:** Node.js 18 or higher
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What It Does
|
|
30
|
+
|
|
31
|
+
Launch it and follow the prompts. You'll pick:
|
|
32
|
+
|
|
33
|
+
1. **What to encode** — URL, WiFi, contact card, or secret text
|
|
34
|
+
2. **Error correction** — how much damage the QR can survive
|
|
35
|
+
3. **Color theme** — 10 gradient options
|
|
36
|
+
4. **Render engine** — how the QR appears on screen
|
|
37
|
+
|
|
38
|
+
Then after it renders, you can copy it, save it as an image, or share it.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Input Types
|
|
43
|
+
|
|
44
|
+
### URL
|
|
45
|
+
Type any web address and watch the QR code build itself in real time as you type. Optionally shorten long URLs via TinyURL to keep the QR small and clean.
|
|
46
|
+
|
|
47
|
+
### WiFi
|
|
48
|
+
Generates a tap-to-join QR code. Point your phone camera at it — no app needed — and it connects automatically.
|
|
49
|
+
|
|
50
|
+
Supports WPA/WPA2, WEP, and open networks.
|
|
51
|
+
|
|
52
|
+
### vCard Contact
|
|
53
|
+
Share your contact info as a QR code. Anyone who scans it gets a prompt to save you directly to their address book.
|
|
54
|
+
|
|
55
|
+
Fields: name, phone, email, organisation, website.
|
|
56
|
+
|
|
57
|
+
### Secret Text
|
|
58
|
+
Encode anything sensitive — API keys, passwords, tokens. Input is masked while you type.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Render Engines
|
|
63
|
+
|
|
64
|
+
### Half-Block (default)
|
|
65
|
+
Uses Unicode characters (`▀`, `▄`, `█`) to fit a full QR code in half the usual vertical space. Renders with a cyan scanline sweep animation followed by a brightness pulse.
|
|
66
|
+
|
|
67
|
+
### Braille — Retina Mode
|
|
68
|
+
Each terminal character cell encodes a 2×4 dot grid, giving 4× the visual resolution of standard terminal QR tools. The QR looks noticeably sharper.
|
|
69
|
+
|
|
70
|
+
### Particle — Transformer Effect
|
|
71
|
+
Every block in the QR starts at a random position on screen and flies into its correct place simultaneously. The code assembles itself out of chaos over about one second.
|
|
72
|
+
|
|
73
|
+
### Matrix Rain
|
|
74
|
+
A cascade of green characters rains down the columns of the QR code before fading out to reveal your chosen color theme underneath.
|
|
75
|
+
|
|
76
|
+
### Device Link — Phone Handshake
|
|
77
|
+
The most interactive mode:
|
|
78
|
+
|
|
79
|
+
1. Scan the QR with your phone
|
|
80
|
+
2. A page opens in your phone's browser
|
|
81
|
+
3. The terminal instantly shows **DEVICE CONNECTED ✓**
|
|
82
|
+
4. Type on your phone and hit transmit — the text appears in your terminal
|
|
83
|
+
|
|
84
|
+
Everything runs over your local network. No internet required, no account needed.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Color Themes
|
|
89
|
+
|
|
90
|
+
| Theme | Style |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Classic | Crisp white — maximum scanner reliability |
|
|
93
|
+
| Retro | Coral red fading to warm amber |
|
|
94
|
+
| Ocean | Deep navy sweeping to electric cyan |
|
|
95
|
+
| Neon | Sine-wave ripple: green · cyan · magenta |
|
|
96
|
+
| Sunset | Hot red through orange to gold |
|
|
97
|
+
| Galaxy | 4-corner blend: purple · blue · pink · teal |
|
|
98
|
+
| Cherry | Radial bloom: soft white centre to deep rose |
|
|
99
|
+
| Matrix | Dark green cascading to electric green |
|
|
100
|
+
| Fire | Blood red through ember to gold |
|
|
101
|
+
| Vaporwave | 4-corner: magenta · cyan · purple · blue |
|
|
102
|
+
|
|
103
|
+
All themes use 24-bit TrueColor applied per character — not per row — so gradients wash diagonally, radially, or in waves across the entire code.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Export Options
|
|
108
|
+
|
|
109
|
+
After any QR renders, choose what to do with it:
|
|
110
|
+
|
|
111
|
+
| Option | What you get |
|
|
112
|
+
|---|---|
|
|
113
|
+
| Copy to clipboard | Plain Unicode characters — paste into Slack, a README, or any terminal |
|
|
114
|
+
| Save as PNG | High-resolution image file (600px) |
|
|
115
|
+
| Save as SVG | Scalable vector — perfect for print or presentations |
|
|
116
|
+
| Save as TXT | Plain text half-block file |
|
|
117
|
+
| Self-destruct share | One-time public link for a file (see below) |
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Self-Destructing File Share
|
|
122
|
+
|
|
123
|
+
Pick **Self-destruct share** from the export menu and point it at any file on your machine:
|
|
124
|
+
|
|
125
|
+
- The file is loaded into memory and hosted via a public tunnel
|
|
126
|
+
- A QR code is generated for the public URL
|
|
127
|
+
- The first person to scan and download it triggers the shutdown
|
|
128
|
+
- The tunnel closes, the server stops, and an ASCII explosion plays in the terminal
|
|
129
|
+
- Anyone who tries the link afterwards gets a `410 Gone` response
|
|
130
|
+
|
|
131
|
+
The file is never written anywhere — it lives in RAM only.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Auto Theme Detection
|
|
136
|
+
|
|
137
|
+
When the tool starts, it silently queries your terminal's actual background color using the OSC 11 escape sequence. If you're on a light-background theme (Solarized Light, GitHub Light, etc.) it detects this automatically and warns you so you can choose a high-contrast color theme.
|
|
138
|
+
|
|
139
|
+
Works with: iTerm2, Windows Terminal, Terminal.app, Kitty, Alacritty, xterm-compatible emulators.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Requirements
|
|
144
|
+
|
|
145
|
+
- Node.js 18+
|
|
146
|
+
- A terminal with 24-bit TrueColor support
|
|
147
|
+
- Windows Terminal ✓
|
|
148
|
+
- iTerm2 ✓
|
|
149
|
+
- Kitty ✓
|
|
150
|
+
- Alacritty ✓
|
|
151
|
+
- VS Code integrated terminal ✓
|
|
152
|
+
- Phone on the same WiFi network (Device Link mode only)
|
|
153
|
+
- Internet connection (TinyURL shortening and self-destruct tunnel only)
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* ╔══════════════════════════════════════════════════════════════════════╗
|
|
4
|
+
* ║ QR TERMINAL v1.0.0 ║
|
|
5
|
+
* ║ High-Density Half-Block QR Code Generator for the Command Line ║
|
|
6
|
+
* ║ ║
|
|
7
|
+
* ║ Engine: Unicode ▀ ▄ █ bitmasking — 50% vertical compression ║
|
|
8
|
+
* ║ Colors: chalk 24-bit TrueColor + multi-stop gradient rendering ║
|
|
9
|
+
* ║ Formats: URL · WiFi · vCard · Secret ║
|
|
10
|
+
* ║ Export: PNG · SVG · TXT · Clipboard ║
|
|
11
|
+
* ╚══════════════════════════════════════════════════════════════════════╝
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { printHeader, log } from './src/ui.js';
|
|
16
|
+
import { runMainFlow } from './src/prompts.js';
|
|
17
|
+
import { detectTerminalBgColor } from './src/luma.js';
|
|
18
|
+
|
|
19
|
+
// Ensure stdout supports TrueColor
|
|
20
|
+
process.env.FORCE_COLOR = process.env.FORCE_COLOR ?? '3';
|
|
21
|
+
|
|
22
|
+
async function main() {
|
|
23
|
+
// ── Splash ──────────────────────────────────────────────────────────────
|
|
24
|
+
process.stdout.write('\x1Bc'); // Full terminal clear (preserves scroll buffer)
|
|
25
|
+
|
|
26
|
+
printHeader();
|
|
27
|
+
|
|
28
|
+
// Auto-detect terminal background before any prompts
|
|
29
|
+
const bgResult = await detectTerminalBgColor();
|
|
30
|
+
if (bgResult?.isLight) {
|
|
31
|
+
log.warn('Light terminal background detected — QR colors auto-adjusted for readability.');
|
|
32
|
+
}
|
|
33
|
+
// Store as global for engine to use
|
|
34
|
+
process.env._QR_LIGHT_BG = bgResult?.isLight ? '1' : '0';
|
|
35
|
+
|
|
36
|
+
// ── Main flow ────────────────────────────────────────────────────────────
|
|
37
|
+
try {
|
|
38
|
+
await runMainFlow();
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (err?.message === 'Cancelled' || err?.name === 'ExitPromptError') {
|
|
41
|
+
console.log('\n' + chalk.dim(' Aborted. Goodbye.') + '\n');
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
log.error('Unexpected error: ' + (err?.message ?? String(err)));
|
|
46
|
+
|
|
47
|
+
if (process.env.DEBUG) {
|
|
48
|
+
console.error(err);
|
|
49
|
+
} else {
|
|
50
|
+
log.info('Set DEBUG=1 to see the full stack trace.');
|
|
51
|
+
}
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Sign-off ─────────────────────────────────────────────────────────────
|
|
56
|
+
console.log(
|
|
57
|
+
chalk.dim(' ─── Made with ') +
|
|
58
|
+
chalk.red('♥') +
|
|
59
|
+
chalk.dim(' using Node.js · qr-terminal ───\n')
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qr-terminal",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cinematic terminal QR code generator — half-block, braille, particle fly-in, matrix rain, device handshake",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"qr-terminal": "./index.js",
|
|
9
|
+
"qr": "./index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node index.js"
|
|
13
|
+
},
|
|
14
|
+
"preferGlobal": true,
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["qr", "qr-code", "terminal", "cli", "tui", "unicode", "half-block", "braille", "gradient", "websocket", "generator"],
|
|
19
|
+
"author": "Boweii22",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "https://github.com/Boweii22/QR_Terminal.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/Boweii22/QR_Terminal#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/Boweii22/QR_Terminal/issues"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.js",
|
|
31
|
+
"src/",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@inquirer/prompts": "^7.0.0",
|
|
36
|
+
"axios": "^1.7.7",
|
|
37
|
+
"boxen": "^7.1.1",
|
|
38
|
+
"chalk": "^5.3.0",
|
|
39
|
+
"clipboardy": "^4.0.0",
|
|
40
|
+
"figlet": "^1.7.0",
|
|
41
|
+
"gradient-string": "^2.0.2",
|
|
42
|
+
"localtunnel": "^2.0.2",
|
|
43
|
+
"ora": "^8.1.0",
|
|
44
|
+
"qrcode": "^1.5.4",
|
|
45
|
+
"ws": "^8.18.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/braille.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Braille-Block Hybrid Renderer
|
|
3
|
+
*
|
|
4
|
+
* Maps 2×4 blocks of QR modules onto Unicode Braille patterns (U+2800–U+28FF).
|
|
5
|
+
* Gives 4× vertical resolution vs standard half-block rendering.
|
|
6
|
+
*
|
|
7
|
+
* Braille dot layout in one terminal cell:
|
|
8
|
+
* col+0 col+1
|
|
9
|
+
* dot1 dot4 (row+0)
|
|
10
|
+
* dot2 dot5 (row+1)
|
|
11
|
+
* dot3 dot6 (row+2)
|
|
12
|
+
* dot7 dot8 (row+3)
|
|
13
|
+
*
|
|
14
|
+
* Bit weights: dot1=1, dot2=2, dot3=4, dot4=8, dot5=16, dot6=32, dot7=64, dot8=128
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import chalk from 'chalk';
|
|
18
|
+
import { GRADIENT_PRESETS, computeColor } from './engine.js';
|
|
19
|
+
|
|
20
|
+
const BRAILLE_BASE = 0x2800;
|
|
21
|
+
|
|
22
|
+
// Map from (leftCol, rightCol) dot rows → bit positions
|
|
23
|
+
const DOT_BITS = [
|
|
24
|
+
[1, 8], // row+0: dot1, dot4
|
|
25
|
+
[2, 16], // row+1: dot2, dot5
|
|
26
|
+
[4, 32], // row+2: dot3, dot6
|
|
27
|
+
[64, 128], // row+3: dot7, dot8
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export function renderBraille(modules, options = {}) {
|
|
31
|
+
const { gradient = 'none', quietZone = 2 } = options;
|
|
32
|
+
const { size, data } = modules;
|
|
33
|
+
const preset = GRADIENT_PRESETS[gradient] ?? GRADIENT_PRESETS.none;
|
|
34
|
+
|
|
35
|
+
const colStart = -quietZone, colEnd = size + quietZone;
|
|
36
|
+
const rowStart = -quietZone, rowEnd = size + quietZone;
|
|
37
|
+
const totalCols = colEnd - colStart;
|
|
38
|
+
const totalRows = rowEnd - rowStart;
|
|
39
|
+
|
|
40
|
+
// Braille: 2 QR cols per char, 4 QR rows per char
|
|
41
|
+
const charCols = Math.ceil(totalCols / 2);
|
|
42
|
+
const charRows = Math.ceil(totalRows / 4);
|
|
43
|
+
|
|
44
|
+
const px = (r, c) => {
|
|
45
|
+
if (r < 0 || r >= size || c < 0 || c >= size) return 0;
|
|
46
|
+
return data[r * size + c] ? 1 : 0;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const output = [];
|
|
50
|
+
|
|
51
|
+
for (let ci = 0; ci < charRows; ci++) {
|
|
52
|
+
const ty = charRows > 1 ? ci / (charRows - 1) : 0;
|
|
53
|
+
let coloredLine = '';
|
|
54
|
+
let runColor = null, runChars = '';
|
|
55
|
+
|
|
56
|
+
const flush = () => {
|
|
57
|
+
if (!runChars) return;
|
|
58
|
+
coloredLine += runColor
|
|
59
|
+
? chalk.rgb(runColor[0], runColor[1], runColor[2])(runChars)
|
|
60
|
+
: runChars;
|
|
61
|
+
runChars = ''; runColor = null;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
for (let cj = 0; cj < charCols; cj++) {
|
|
65
|
+
const tx = charCols > 1 ? cj / (charCols - 1) : 0;
|
|
66
|
+
const baseRow = rowStart + ci * 4;
|
|
67
|
+
const baseCol = colStart + cj * 2;
|
|
68
|
+
|
|
69
|
+
// Build 8-bit braille bitmask from the 2×4 QR module block
|
|
70
|
+
let bitmask = 0;
|
|
71
|
+
let hasDark = false;
|
|
72
|
+
for (let dr = 0; dr < 4; dr++) {
|
|
73
|
+
const [leftBit, rightBit] = DOT_BITS[dr];
|
|
74
|
+
if (px(baseRow + dr, baseCol)) { bitmask |= leftBit; hasDark = true; }
|
|
75
|
+
if (px(baseRow + dr, baseCol + 1)) { bitmask |= rightBit; hasDark = true; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const char = String.fromCodePoint(BRAILLE_BASE + bitmask);
|
|
79
|
+
|
|
80
|
+
if (!hasDark || bitmask === 0) {
|
|
81
|
+
flush();
|
|
82
|
+
coloredLine += ' ';
|
|
83
|
+
} else {
|
|
84
|
+
const color = computeColor(preset, tx, ty);
|
|
85
|
+
if (runColor && colorsClose(runColor, color, 8)) {
|
|
86
|
+
runChars += char;
|
|
87
|
+
} else {
|
|
88
|
+
flush();
|
|
89
|
+
runColor = color;
|
|
90
|
+
runChars = char;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
flush();
|
|
96
|
+
output.push(coloredLine);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return output;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function colorsClose([r1,g1,b1],[r2,g2,b2],t){
|
|
103
|
+
return Math.abs(r1-r2)<t && Math.abs(g1-g2)<t && Math.abs(b1-b2)<t;
|
|
104
|
+
}
|
package/src/destruct.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Destructing Session QR
|
|
3
|
+
*
|
|
4
|
+
* Hosts a file over an HTTP server tunneled via localtunnel.
|
|
5
|
+
* The QR code encodes the public tunnel URL.
|
|
6
|
+
* After the file is downloaded once, the tunnel closes and an ASCII
|
|
7
|
+
* "explosion" animation plays.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import http from 'http';
|
|
11
|
+
import { readFile } from 'fs/promises';
|
|
12
|
+
import { existsSync } from 'fs';
|
|
13
|
+
import { basename } from 'path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a self-destructing file share.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} filePath - Path to the file to share
|
|
19
|
+
* @returns {Promise<{ url, waitForDownload, close }>}
|
|
20
|
+
*/
|
|
21
|
+
export async function createDestructQR(filePath) {
|
|
22
|
+
if (!existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
23
|
+
|
|
24
|
+
const fileBuffer = await readFile(filePath);
|
|
25
|
+
const fileName = basename(filePath);
|
|
26
|
+
const port = 7654 + Math.floor(Math.random() * 200);
|
|
27
|
+
let downloaded = false;
|
|
28
|
+
let downloadResolve;
|
|
29
|
+
const downloadPromise = new Promise(r => { downloadResolve = r; });
|
|
30
|
+
|
|
31
|
+
const server = http.createServer((req, res) => {
|
|
32
|
+
if (req.url === '/favicon.ico') { res.writeHead(404); res.end(); return; }
|
|
33
|
+
|
|
34
|
+
if (downloaded) {
|
|
35
|
+
res.writeHead(410, { 'Content-Type': 'text/html' });
|
|
36
|
+
res.end('<html><body style="background:#0d0d0d;color:#ff0040;font-family:monospace;padding:40px"><h1>☠ FILE DESTROYED</h1><p>This QR code was single-use. The file no longer exists.</p></body></html>');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
downloaded = true;
|
|
41
|
+
res.writeHead(200, {
|
|
42
|
+
'Content-Type': 'application/octet-stream',
|
|
43
|
+
'Content-Disposition': `attachment; filename="${fileName}"`,
|
|
44
|
+
'Content-Length': fileBuffer.length,
|
|
45
|
+
});
|
|
46
|
+
res.end(fileBuffer);
|
|
47
|
+
|
|
48
|
+
// Signal after response is sent
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
downloadResolve();
|
|
51
|
+
server.close();
|
|
52
|
+
}, 500);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await new Promise((res, rej) => server.listen(port, (e) => e ? rej(e) : res()));
|
|
56
|
+
|
|
57
|
+
// Try to set up localtunnel
|
|
58
|
+
let tunnelUrl;
|
|
59
|
+
try {
|
|
60
|
+
const { default: localtunnel } = await import('localtunnel');
|
|
61
|
+
const tunnel = await localtunnel({ port });
|
|
62
|
+
tunnelUrl = tunnel.url;
|
|
63
|
+
|
|
64
|
+
downloadPromise.then(() => tunnel.close()).catch(() => {});
|
|
65
|
+
} catch {
|
|
66
|
+
// Fall back to local IP if localtunnel unavailable
|
|
67
|
+
const { networkInterfaces } = await import('os');
|
|
68
|
+
const nets = networkInterfaces();
|
|
69
|
+
let ip = '127.0.0.1';
|
|
70
|
+
for (const name of Object.keys(nets)) {
|
|
71
|
+
for (const iface of nets[name]) {
|
|
72
|
+
if (iface.family === 'IPv4' && !iface.internal) { ip = iface.address; break; }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
tunnelUrl = `http://${ip}:${port}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
url: tunnelUrl,
|
|
80
|
+
waitForDownload: downloadPromise,
|
|
81
|
+
close: () => server.close(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* ASCII explosion animation. Call after the file is consumed.
|
|
87
|
+
*/
|
|
88
|
+
export async function showExplosion() {
|
|
89
|
+
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
90
|
+
const UP = n => `\x1b[${n}A`;
|
|
91
|
+
|
|
92
|
+
const frames = [
|
|
93
|
+
[' ', ' ☠ ', ' '],
|
|
94
|
+
[' · ', ' ✦☠✦ ', ' · '],
|
|
95
|
+
[' ·✦· ', '✦✦☠✦✦ ', ' ·✦· '],
|
|
96
|
+
['✦·✦·✦ ', '·✦ ✦· ', '✦·✦·✦ '],
|
|
97
|
+
['✧ ✦ ✧ ', ' ✦ ✦ ', '✧ ✦ ✧ '],
|
|
98
|
+
[' ✧ ✧', ' ', ' ✧ ✧'],
|
|
99
|
+
[' ', ' ', ' '],
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const colors = [
|
|
103
|
+
[255, 255, 0],
|
|
104
|
+
[255, 200, 0],
|
|
105
|
+
[255, 140, 0],
|
|
106
|
+
[255, 80, 0],
|
|
107
|
+
[255, 40, 0],
|
|
108
|
+
[200, 20, 0],
|
|
109
|
+
[100, 0, 0],
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
// Print initial blank frame
|
|
113
|
+
for (let i = 0; i < 3; i++) process.stdout.write('\n');
|
|
114
|
+
|
|
115
|
+
for (let f = 0; f < frames.length; f++) {
|
|
116
|
+
const [r, g, b] = colors[f];
|
|
117
|
+
process.stdout.write(UP(3));
|
|
118
|
+
for (const line of frames[f]) {
|
|
119
|
+
process.stdout.write(` \x1b[38;2;${r};${g};${b}m${line.padEnd(20)}\x1b[0m\n`);
|
|
120
|
+
}
|
|
121
|
+
await sleep(80);
|
|
122
|
+
}
|
|
123
|
+
}
|