@vaibhavjha/qrfy 1.0.8 → 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 +55 -4
- package/bin/qrfy.js +56 -12
- package/lib/qr.js +41 -1
- package/lib/tunnel.js +53 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,12 +125,56 @@ Now `npm run dev` gives you a QR code every time. Your whole team gets it for fr
|
|
|
125
125
|
|
|
126
126
|
---
|
|
127
127
|
|
|
128
|
+
## Share From Anywhere (Cloudflare Tunnel)
|
|
129
|
+
|
|
130
|
+
Same WiFi not an option? Pass `-t` (or `--tunnel`) and QRfy will expose your dev server through a free [Cloudflare quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/) — a fresh `*.trycloudflare.com` URL, reachable over cellular, from a coworker's laptop, or a stakeholder on the other side of the world.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
qrfy -t next dev
|
|
134
|
+
qrfy --tunnel npm run dev
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
You'll get a QR for the **public URL** instead of the LAN one. No Cloudflare account needed.
|
|
138
|
+
|
|
139
|
+
**One-time setup** — install `cloudflared`:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# macOS
|
|
143
|
+
brew install cloudflared
|
|
144
|
+
|
|
145
|
+
# Windows
|
|
146
|
+
winget install --id Cloudflare.cloudflared
|
|
147
|
+
|
|
148
|
+
# Linux
|
|
149
|
+
# https://pkg.cloudflare.com/index.html
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
> The tunnel URL is fresh every run and tears down when you Ctrl+C. Don't share it for anything you wouldn't put on the open internet.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## QR for Anything
|
|
157
|
+
|
|
158
|
+
Need a QR code for a URL, a WiFi password, or any piece of text? Use `-q`:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
qrfy -q https://example.com
|
|
162
|
+
qrfy -q "hello world"
|
|
163
|
+
qrfy -q "WIFI:T:WPA;S:MyNetwork;P:mypassword;;"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
No dev server, no port detection — just a scannable QR code in your terminal.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
128
170
|
## CLI Options
|
|
129
171
|
|
|
130
172
|
```bash
|
|
131
|
-
qrfy <command>
|
|
132
|
-
qrfy
|
|
133
|
-
qrfy
|
|
173
|
+
qrfy <command> # Wrap a dev server
|
|
174
|
+
qrfy -t <command> # Wrap + expose via Cloudflare tunnel
|
|
175
|
+
qrfy -q <text|url...> # QR code for any text or URL
|
|
176
|
+
qrfy --help, -h # Show help
|
|
177
|
+
qrfy --version, -v # Show version
|
|
134
178
|
```
|
|
135
179
|
|
|
136
180
|
---
|
|
@@ -174,7 +218,7 @@ qrfy --version, -v # Show version
|
|
|
174
218
|
| | Requirement |
|
|
175
219
|
|---|---|
|
|
176
220
|
| **Runtime** | Node.js >= 14 |
|
|
177
|
-
| **Network** | Same WiFi |
|
|
221
|
+
| **Network** | Same WiFi (or `cloudflared` installed for `-t`) |
|
|
178
222
|
| **OS** | macOS, Linux, Windows |
|
|
179
223
|
|
|
180
224
|
---
|
|
@@ -197,6 +241,13 @@ The port wasn't detected from your server's output. Make sure your dev server pr
|
|
|
197
241
|
|
|
198
242
|
</details>
|
|
199
243
|
|
|
244
|
+
<details>
|
|
245
|
+
<summary><b>"cloudflared is not installed" when using <code>-t</code></b></summary>
|
|
246
|
+
|
|
247
|
+
Tunnel mode shells out to Cloudflare's `cloudflared` binary. Install it with the command for your OS shown in the [Share From Anywhere](#share-from-anywhere-cloudflare-tunnel) section, then retry.
|
|
248
|
+
|
|
249
|
+
</details>
|
|
250
|
+
|
|
200
251
|
<details>
|
|
201
252
|
<summary><b>Command not found after install</b></summary>
|
|
202
253
|
|
package/bin/qrfy.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { getLocalIP } = require("../lib/network");
|
|
4
|
-
const { displayQR } = require("../lib/qr");
|
|
4
|
+
const { displayQR, displayStaticQR, displayTunnelQR } = require("../lib/qr");
|
|
5
5
|
const { runServer } = require("../lib/runner");
|
|
6
|
+
const { hasCloudflared, printInstallHint, startTunnel } = require("../lib/tunnel");
|
|
6
7
|
|
|
7
8
|
const { version } = require("../package.json");
|
|
8
9
|
|
|
@@ -18,31 +19,74 @@ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
|
18
19
|
console.log(` \x1b[1mqrfy\x1b[0m v${version} - Scan your dev server instantly`);
|
|
19
20
|
console.log("");
|
|
20
21
|
console.log(" \x1b[1mUsage:\x1b[0m");
|
|
21
|
-
console.log(" qrfy <command> [args...]");
|
|
22
|
+
console.log(" qrfy <command> [args...] Wrap a dev server");
|
|
23
|
+
console.log(" qrfy -t <command> [args...] Wrap + expose via Cloudflare tunnel");
|
|
24
|
+
console.log(" qrfy -q <text|url...> QR code for any text or URL");
|
|
22
25
|
console.log("");
|
|
23
26
|
console.log(" \x1b[1mExamples:\x1b[0m");
|
|
24
27
|
console.log(" qrfy next dev");
|
|
25
28
|
console.log(" qrfy vite");
|
|
26
29
|
console.log(" qrfy npm run dev");
|
|
30
|
+
console.log(" qrfy --tunnel next dev");
|
|
31
|
+
console.log(" qrfy -q https://example.com");
|
|
32
|
+
console.log(" qrfy -q \"hello world\"");
|
|
27
33
|
console.log("");
|
|
28
34
|
console.log(" \x1b[1mOptions:\x1b[0m");
|
|
35
|
+
console.log(" -t, --tunnel Expose via a Cloudflare quick tunnel (needs cloudflared)");
|
|
36
|
+
console.log(" -q, --qr Generate QR for any text or URL");
|
|
29
37
|
console.log(" -h, --help Show this help message");
|
|
30
38
|
console.log(" -v, --version Show version number");
|
|
31
39
|
console.log("");
|
|
32
40
|
process.exit(0);
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
|
|
43
|
+
// Static QR: explicit QR mode
|
|
44
|
+
if (args[0] === "--qr" || args[0] === "-q") {
|
|
45
|
+
const content = args.slice(1).join(" ");
|
|
46
|
+
if (!content) {
|
|
47
|
+
console.error("\x1b[31m QRfy: -q requires content\x1b[0m");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
displayStaticQR(content);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tunnel = args.includes("--tunnel") || args.includes("-t");
|
|
55
|
+
const serverArgs = args.filter((a) => a !== "--tunnel" && a !== "-t");
|
|
36
56
|
|
|
37
|
-
if (
|
|
38
|
-
console.error(
|
|
39
|
-
"\x1b[31m QRfy: No network connection found. Connect to WiFi and try again.\x1b[0m"
|
|
40
|
-
);
|
|
57
|
+
if (serverArgs.length === 0) {
|
|
58
|
+
console.error("\x1b[31m QRfy: --tunnel requires a command to wrap (e.g. qrfy -t next dev)\x1b[0m");
|
|
41
59
|
process.exit(1);
|
|
42
60
|
}
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
62
|
+
if (tunnel) {
|
|
63
|
+
if (!hasCloudflared()) {
|
|
64
|
+
printInstallHint();
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
runServer(serverArgs, (port) => {
|
|
69
|
+
const tunnelChild = startTunnel(port, (publicUrl) => {
|
|
70
|
+
displayTunnelQR(publicUrl);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
process.on("SIGINT", () => tunnelChild.kill("SIGINT"));
|
|
74
|
+
process.on("SIGTERM", () => tunnelChild.kill("SIGTERM"));
|
|
75
|
+
process.on("exit", () => tunnelChild.kill("SIGTERM"));
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
const ip = getLocalIP();
|
|
79
|
+
|
|
80
|
+
if (!ip) {
|
|
81
|
+
console.error(
|
|
82
|
+
"\x1b[31m QRfy: No network connection found. Connect to WiFi and try again.\x1b[0m"
|
|
83
|
+
);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
runServer(serverArgs, (port) => {
|
|
88
|
+
const localUrl = `http://localhost:${port}`;
|
|
89
|
+
const mobileUrl = `http://${ip}:${port}`;
|
|
90
|
+
displayQR(localUrl, mobileUrl);
|
|
91
|
+
});
|
|
92
|
+
}
|
package/lib/qr.js
CHANGED
|
@@ -23,4 +23,44 @@ function displayQR(localUrl, mobileUrl) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
function displayTunnelQR(publicUrl) {
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log("\x1b[1m\x1b[34m QRfy tunnel ready\x1b[0m");
|
|
29
|
+
console.log("");
|
|
30
|
+
console.log(` Public: \x1b[32m${publicUrl}\x1b[0m`);
|
|
31
|
+
console.log("");
|
|
32
|
+
console.log(" Scan to open from anywhere:");
|
|
33
|
+
console.log("");
|
|
34
|
+
qrcode.generate(publicUrl, { small: true }, (code) => {
|
|
35
|
+
const indented = code
|
|
36
|
+
.split("\n")
|
|
37
|
+
.map((line) => " " + line)
|
|
38
|
+
.join("\n");
|
|
39
|
+
console.log(indented);
|
|
40
|
+
console.log("");
|
|
41
|
+
console.log(
|
|
42
|
+
" \x1b[33mFresh URL per run — reachable over cellular and remote networks\x1b[0m"
|
|
43
|
+
);
|
|
44
|
+
console.log("");
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function displayStaticQR(content) {
|
|
49
|
+
console.log("");
|
|
50
|
+
console.log("\x1b[1m\x1b[34m QRfy\x1b[0m");
|
|
51
|
+
console.log("");
|
|
52
|
+
console.log(` Content: \x1b[32m${content}\x1b[0m`);
|
|
53
|
+
console.log("");
|
|
54
|
+
console.log(" Scan with your phone:");
|
|
55
|
+
console.log("");
|
|
56
|
+
qrcode.generate(content, { small: true }, (code) => {
|
|
57
|
+
const indented = code
|
|
58
|
+
.split("\n")
|
|
59
|
+
.map((line) => " " + line)
|
|
60
|
+
.join("\n");
|
|
61
|
+
console.log(indented);
|
|
62
|
+
console.log("");
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { displayQR, displayStaticQR, displayTunnelQR };
|
package/lib/tunnel.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const { spawn, spawnSync } = require("child_process");
|
|
2
|
+
|
|
3
|
+
const TUNNEL_URL_REGEX = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/i;
|
|
4
|
+
|
|
5
|
+
function hasCloudflared() {
|
|
6
|
+
const probe = spawnSync(
|
|
7
|
+
process.platform === "win32" ? "where" : "which",
|
|
8
|
+
["cloudflared"],
|
|
9
|
+
{ stdio: "ignore" }
|
|
10
|
+
);
|
|
11
|
+
return probe.status === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function printInstallHint() {
|
|
15
|
+
console.error("");
|
|
16
|
+
console.error("\x1b[31m QRfy: cloudflared is not installed.\x1b[0m");
|
|
17
|
+
console.error("");
|
|
18
|
+
console.error(" Install it, then re-run with --tunnel:");
|
|
19
|
+
console.error(" macOS: \x1b[36mbrew install cloudflared\x1b[0m");
|
|
20
|
+
console.error(" Windows: \x1b[36mwinget install --id Cloudflare.cloudflared\x1b[0m");
|
|
21
|
+
console.error(" Linux: \x1b[36mhttps://pkg.cloudflare.com/index.html\x1b[0m");
|
|
22
|
+
console.error("");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function startTunnel(port, onUrl) {
|
|
26
|
+
const child = spawn(
|
|
27
|
+
"cloudflared",
|
|
28
|
+
["tunnel", "--no-autoupdate", "--url", `http://localhost:${port}`],
|
|
29
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
let resolved = false;
|
|
33
|
+
|
|
34
|
+
function scan(buf) {
|
|
35
|
+
if (resolved) return;
|
|
36
|
+
const match = buf.toString().match(TUNNEL_URL_REGEX);
|
|
37
|
+
if (match) {
|
|
38
|
+
resolved = true;
|
|
39
|
+
onUrl(match[0]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
child.stdout.on("data", scan);
|
|
44
|
+
child.stderr.on("data", scan);
|
|
45
|
+
|
|
46
|
+
child.on("error", (err) => {
|
|
47
|
+
console.error(`\x1b[31m QRfy tunnel error: ${err.message}\x1b[0m`);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return child;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { hasCloudflared, printInstallHint, startTunnel };
|