devtunnel-cli 3.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 ADDED
@@ -0,0 +1,82 @@
1
+ # DevTunnel šŸš€
2
+
3
+ **Share your local dev servers worldwide - Zero config tunnel for any framework**
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+ [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)](https://github.com/maiz-an/DevTunnel)
7
+
8
+ 🌐 **Website:** [devtunnel.vercel.app](https://devtunnel.vercel.app)
9
+
10
+ ---
11
+
12
+ ## ⚔ Quick Start
13
+
14
+ ### Windows
15
+ Double-click `START.bat`
16
+
17
+ ### macOS
18
+ Double-click `START.command`
19
+
20
+ ### Linux
21
+ ```bash
22
+ chmod +x START.sh
23
+ ./START.sh
24
+ ```
25
+
26
+ Or use npm:
27
+ ```bash
28
+ npm start
29
+ ```
30
+
31
+ ---
32
+
33
+ ## ✨ Features
34
+
35
+ - šŸ¤– **Fully Automatic** - Cloudflare bundled, no installation needed
36
+ - šŸŽÆ **Zero Config** - No project changes needed
37
+ - šŸ”— **Smart Proxy** - Bypasses Vite/React restrictions
38
+ - šŸŒ **Cross-Platform** - Windows, macOS, Linux
39
+ - šŸš€ **Any Framework** - Works with all
40
+ - šŸ”„ **Multi-Service** - Cloudflare, Ngrok, LocalTunnel fallback
41
+
42
+ ---
43
+
44
+ ## šŸ’” How to Use
45
+
46
+ 1. Start your dev server: `npm start` or `npm run dev` (whichever your project uses)
47
+ 2. Run DevTunnel (see Quick Start above)
48
+ 3. Select your project folder
49
+ 4. Enter your port (default: 5173 for Vite, 3000 for most others)
50
+ 5. Get your public URL and share it! šŸŒ
51
+
52
+ **Works with any command:** Vite, Create React App, Next.js, Express, NestJS, etc.
53
+
54
+ ---
55
+
56
+ ## šŸ“– Documentation
57
+
58
+ Complete docs in `/docs` folder:
59
+ - [Complete Guide](docs/README.md)
60
+ - [Features](docs/FEATURES.md)
61
+ - [Troubleshooting](docs/TROUBLESHOOTING.md)
62
+ - [GitHub Pages Website](docs/DEPLOY-WEBSITE.md)
63
+
64
+ ---
65
+
66
+ ## šŸ› ļø Requirements
67
+
68
+ - Node.js 16+ (download from [nodejs.org](https://nodejs.org))
69
+ - Internet connection
70
+ - Your dev server running
71
+
72
+ **No other installations needed!** Cloudflare is automatically bundled on first run.
73
+
74
+ ---
75
+
76
+ ## šŸ“„ License
77
+
78
+ MIT License - see [LICENSE](docs/LICENSE)
79
+
80
+ ---
81
+
82
+ **Version 3.0.0** | Made with ā¤ļø for developers worldwide
package/bin/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # Bundled Binaries
2
+
3
+ This folder contains platform-specific binaries that are automatically downloaded on first run.
4
+
5
+ ## Structure
6
+
7
+ ```
8
+ bin/
9
+ ā”œā”€ā”€ win32/
10
+ │ └── cloudflared.exe (Windows binary)
11
+ ā”œā”€ā”€ darwin/
12
+ │ └── cloudflared (macOS binary)
13
+ └── linux/
14
+ └── cloudflared (Linux binary)
15
+ ```
16
+
17
+ ## Automatic Setup
18
+
19
+ When you run DevTunnel for the first time, it will:
20
+ 1. Detect your operating system
21
+ 2. Download the appropriate Cloudflare binary (~40MB)
22
+ 3. Save it to this folder
23
+ 4. Use it for all future runs
24
+
25
+ **No manual installation required!**
26
+
27
+ ## Manual Setup (Optional)
28
+
29
+ If you want to pre-download binaries:
30
+
31
+ ```bash
32
+ node src/core/setup-cloudflared.js
33
+ ```
34
+
35
+ ## Notes
36
+
37
+ - These files are in `.gitignore` (not pushed to GitHub)
38
+ - Downloaded automatically on first run
39
+ - Falls back to system-installed cloudflared if download fails
40
+ - Version: Cloudflare 2024.8.2 (latest stable)
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "devtunnel-cli",
3
+ "version": "3.0.0",
4
+ "type": "module",
5
+ "description": "Share local dev servers worldwide - Zero config tunnel for any framework",
6
+ "main": "src/core/start.js",
7
+ "bin": {
8
+ "devtunnel": "src/core/RUN.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node src/core/RUN.js",
12
+ "dev": "node src/core/RUN.js",
13
+ "run": "node src/core/RUN.js",
14
+ "tunnel": "node src/core/index.js"
15
+ },
16
+ "keywords": [
17
+ "tunnel",
18
+ "devtunnel",
19
+ "cloudflare",
20
+ "ngrok",
21
+ "localtunnel",
22
+ "dev-server",
23
+ "share",
24
+ "public-url",
25
+ "vite",
26
+ "react",
27
+ "nextjs",
28
+ "nestjs",
29
+ "express",
30
+ "backend",
31
+ "frontend",
32
+ "proxy",
33
+ "development",
34
+ "localhost",
35
+ "port-forwarding"
36
+ ],
37
+ "author": "maiz",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/maiz-an/DevTunnel.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/maiz-an/DevTunnel/issues"
45
+ },
46
+ "homepage": "https://devtunnel.vercel.app",
47
+ "engines": {
48
+ "node": ">=16.0.0"
49
+ },
50
+ "dependencies": {
51
+ "http-proxy": "^1.18.1",
52
+ "localtunnel": "^2.0.2",
53
+ "prompts": "^2.4.2"
54
+ },
55
+ "devDependencies": {}
56
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Universal Node.js Launcher - Works on ALL platforms!
4
+ import { spawn } from "child_process";
5
+ import { platform } from "os";
6
+ import { fileURLToPath } from "url";
7
+ import { dirname, join } from "path";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ console.log("\nšŸš€ DevTunnel - Universal Launcher\n");
13
+ console.log(`šŸ“ Platform detected: ${platform()}\n`);
14
+
15
+ // Start the main app
16
+ const startPath = join(__dirname, "src", "core", "start.js");
17
+ const child = spawn("node", [startPath], {
18
+ stdio: "inherit",
19
+ shell: false
20
+ });
21
+
22
+ child.on("error", (error) => {
23
+ console.error("āŒ Error starting app:", error.message);
24
+ process.exit(1);
25
+ });
26
+
27
+ child.on("close", (code) => {
28
+ if (code !== 0) {
29
+ console.log(`\nāš ļø Process exited with code ${code}`);
30
+ }
31
+ process.exit(code || 0);
32
+ });
33
+
34
+ // Handle Ctrl+C
35
+ process.on("SIGINT", () => {
36
+ child.kill("SIGINT");
37
+ });
38
+
39
+ process.on("SIGTERM", () => {
40
+ child.kill("SIGTERM");
41
+ });
@@ -0,0 +1,394 @@
1
+ import { spawn } from "child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join, dirname } from "path";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // Get current directory (needed for ESM)
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Get port and project path from command line arguments
12
+ const PORT = parseInt(process.argv[2]);
13
+ const PROJECT_NAME = process.argv[3] || `Project (Port ${PORT})`;
14
+ const PROJECT_PATH = process.argv[4] || process.cwd();
15
+
16
+ // Import bundled cloudflared helpers
17
+ const { getBinaryPath, hasBundledCloudflared } = await import("./setup-cloudflared.js");
18
+
19
+ // No custom prefix - Cloudflare generates random URLs
20
+
21
+ if (!PORT || isNaN(PORT) || PORT < 1 || PORT > 65535) {
22
+ console.error("āŒ Invalid port number!");
23
+ console.error("Usage: node index.js <port> [projectName]");
24
+ console.error("Example: node index.js 5173");
25
+ process.exit(1);
26
+ }
27
+
28
+ let tunnelProcess;
29
+ let currentTunnelType = null;
30
+
31
+ console.log("╔════════════════════════════════════════════╗");
32
+ console.log("ā•‘ 🌐 DevTunnel v3.0 ā•‘");
33
+ console.log("╠════════════════════════════════════════════╣");
34
+ console.log(`ā•‘ šŸ“¦ ${PROJECT_NAME.padEnd(38)} ā•‘`);
35
+ console.log(`ā•‘ šŸ”Œ Port: ${PORT.toString().padEnd(34)} ā•‘`);
36
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n");
37
+ console.log("šŸ’” Ensure dev server is running on port " + PORT + "\n");
38
+
39
+ // Check if project is Vite and auto-fix config for Cloudflare
40
+ async function fixViteConfigForCloudflare() {
41
+ const viteConfigPath = join(PROJECT_PATH, "vite.config.js");
42
+ const viteConfigTsPath = join(PROJECT_PATH, "vite.config.ts");
43
+
44
+ let configPath = null;
45
+ if (existsSync(viteConfigPath)) configPath = viteConfigPath;
46
+ else if (existsSync(viteConfigTsPath)) configPath = viteConfigTsPath;
47
+
48
+ if (!configPath) return false;
49
+
50
+ try {
51
+ let config = readFileSync(configPath, "utf-8");
52
+
53
+ // Check if already configured for external access
54
+ if (config.includes("host: true") || config.includes("host:true") ||
55
+ config.includes("host: '0.0.0.0'") || config.includes('host: "0.0.0.0"') ||
56
+ config.includes('host:"0.0.0.0"') || config.includes("host:'0.0.0.0'")) {
57
+ console.log("āœ… Vite config already allows external access\n");
58
+ return true;
59
+ }
60
+
61
+ console.log("šŸ“ Auto-fixing Vite config for tunnel access...");
62
+
63
+ let newConfig = config;
64
+
65
+ // Case 1: Has existing server config
66
+ if (config.includes("server:") || config.includes("server :")) {
67
+ // Add host inside existing server block
68
+ newConfig = config.replace(
69
+ /(server\s*:\s*\{)/,
70
+ `$1\n host: true, // Allow tunnel access`
71
+ );
72
+ }
73
+ // Case 2: No server config, add it after defineConfig({
74
+ else if (config.includes("defineConfig")) {
75
+ newConfig = config.replace(
76
+ /defineConfig\(\s*\{/,
77
+ `defineConfig({\n server: {\n host: true, // Allow tunnel access\n },`
78
+ );
79
+ }
80
+ // Case 3: Simple export default object
81
+ else if (config.includes("export default {")) {
82
+ newConfig = config.replace(
83
+ /export\s+default\s+\{/,
84
+ `export default {\n server: {\n host: true, // Allow tunnel access\n },`
85
+ );
86
+ }
87
+
88
+ if (newConfig !== config) {
89
+ // Backup original
90
+ writeFileSync(configPath + ".backup", config);
91
+ writeFileSync(configPath, newConfig);
92
+ console.log("\n" + "=".repeat(60));
93
+ console.log("āœ… VITE CONFIG UPDATED!");
94
+ console.log("=".repeat(60));
95
+ console.log("āš ļø IMPORTANT: You MUST restart your dev server now!");
96
+ console.log(" 1. Stop your dev server (Ctrl+C)");
97
+ console.log(" 2. Run: npm run dev");
98
+ console.log(" 3. Then run this tool again");
99
+ console.log("\nBackup saved: " + configPath + ".backup");
100
+ console.log("=".repeat(60) + "\n");
101
+
102
+ // Wait for user to acknowledge
103
+ console.log("Press Ctrl+C to exit and restart your dev server...\n");
104
+ await new Promise(resolve => setTimeout(resolve, 60000)); // Wait 60 seconds
105
+ return true;
106
+ }
107
+
108
+ console.log("āš ļø Could not auto-fix config. You may need to manually add:\n");
109
+ console.log(" server: { host: true }\n");
110
+ return false;
111
+ } catch (error) {
112
+ console.log("āš ļø Could not read Vite config:", error.message);
113
+ return false;
114
+ }
115
+ }
116
+
117
+ // Get cloudflared command (bundled or system)
118
+ function getCloudflaredCommand() {
119
+ return hasBundledCloudflared() ? getBinaryPath() : "cloudflared";
120
+ }
121
+
122
+ // Tunnel services to try in order (Cloudflare first - no password, fast)
123
+ const TUNNEL_SERVICES = [
124
+ {
125
+ name: "Cloudflare",
126
+ get command() {
127
+ return getCloudflaredCommand();
128
+ },
129
+ args: ["tunnel", "--url", `http://localhost:${PORT}`],
130
+ available: async () => {
131
+ try {
132
+ const cmd = getCloudflaredCommand();
133
+ const result = spawn(cmd, ["--version"], { shell: true, stdio: "pipe" });
134
+ return new Promise((resolve) => {
135
+ result.on("close", (code) => resolve(code === 0));
136
+ result.on("error", () => resolve(false));
137
+ });
138
+ } catch {
139
+ return false;
140
+ }
141
+ },
142
+ needsViteFix: true
143
+ },
144
+ {
145
+ name: "Ngrok",
146
+ command: "ngrok",
147
+ args: ["http", PORT.toString()],
148
+ available: async () => {
149
+ try {
150
+ const result = spawn("ngrok", ["--version"], { shell: true, stdio: "pipe" });
151
+ return new Promise((resolve) => {
152
+ result.on("close", (code) => resolve(code === 0));
153
+ result.on("error", () => resolve(false));
154
+ });
155
+ } catch {
156
+ return false;
157
+ }
158
+ },
159
+ needsViteFix: true
160
+ },
161
+ {
162
+ name: "LocalTunnel",
163
+ command: "node",
164
+ args: [join(__dirname, "tunnel-helpers.js"), "localtunnel", PORT.toString()],
165
+ available: async () => {
166
+ try {
167
+ await import("localtunnel");
168
+ return true;
169
+ } catch {
170
+ return false;
171
+ }
172
+ },
173
+ needsViteFix: false,
174
+ warning: "āš ļø Note: LocalTunnel shows a password page on first visit (uses your public IP)"
175
+ }
176
+ ];
177
+
178
+ // Try each tunnel service
179
+ async function tryTunnelServices() {
180
+ console.log("šŸ” Checking available tunnel services...\n");
181
+
182
+ let hasCloudflare = false;
183
+
184
+ // Check if Cloudflare is available
185
+ for (const service of TUNNEL_SERVICES) {
186
+ if (service.name === "Cloudflare" && await service.available()) {
187
+ hasCloudflare = true;
188
+ break;
189
+ }
190
+ }
191
+
192
+ // Show tip if Cloudflare not installed
193
+ if (!hasCloudflare) {
194
+ console.log("šŸ’” TIP: Install Cloudflare for best experience (no password, fastest)");
195
+ console.log(" → winget install Cloudflare.cloudflared\n");
196
+ }
197
+
198
+ for (const service of TUNNEL_SERVICES) {
199
+ const available = await service.available();
200
+
201
+ if (available) {
202
+ console.log(`āœ… ${service.name} is available`);
203
+
204
+ // Show warning if exists
205
+ if (service.warning) {
206
+ console.log(service.warning);
207
+ }
208
+
209
+ // Skip Vite auto-fix - using proxy server instead
210
+
211
+ console.log(`🌐 Starting ${service.name} tunnel...\n`);
212
+
213
+ currentTunnelType = service.name;
214
+ tunnelProcess = spawn(service.command, service.args, {
215
+ shell: true,
216
+ stdio: "pipe"
217
+ });
218
+
219
+ setupTunnelHandlers(service.name);
220
+
221
+ // Wait a bit to see if tunnel starts successfully
222
+ await new Promise(resolve => setTimeout(resolve, 3000));
223
+
224
+ // Check if process is still running
225
+ if (tunnelProcess && !tunnelProcess.killed) {
226
+ console.log(`\nāœ… Successfully connected via ${service.name}!`);
227
+ if (service.name === "LocalTunnel") {
228
+ console.log("šŸ’” First-time visitors need to enter tunnel password (your public IP)");
229
+ console.log("šŸ’” Get password at: https://loca.lt/mytunnelpassword\n");
230
+ }
231
+ console.log("Press Ctrl+C to stop the tunnel\n");
232
+ return true;
233
+ }
234
+ } else {
235
+ console.log(`āš ļø ${service.name} not available`);
236
+ }
237
+ }
238
+
239
+ console.log("\nāŒ No tunnel services available!");
240
+ console.log("\nšŸ’” Recommended: Install Cloudflare (fastest, no password):");
241
+ console.log(" winget install Cloudflare.cloudflared");
242
+ console.log("\nšŸ’” Or install Ngrok:");
243
+ console.log(" Download from: https://ngrok.com/download");
244
+ console.log("\nšŸ’” LocalTunnel is already installed but may require restart");
245
+ process.exit(1);
246
+ }
247
+
248
+ function setupTunnelHandlers(serviceName) {
249
+ if (!tunnelProcess) return;
250
+
251
+ tunnelProcess.stdout.on("data", (data) => {
252
+ const output = data.toString();
253
+ const lines = output.split("\n");
254
+
255
+ lines.forEach(line => {
256
+ const trimmed = line.trim();
257
+ if (!trimmed) return;
258
+
259
+ // Extract URLs from different services
260
+ if (serviceName === "Cloudflare") {
261
+ // Look for trycloudflare.com URL
262
+ if (trimmed.includes("trycloudflare.com")) {
263
+ // Extract just the URL
264
+ const urlMatch = trimmed.match(/(https?:\/\/[^\s]+trycloudflare\.com[^\s]*)/);
265
+ if (urlMatch) {
266
+ const url = urlMatch[1];
267
+ const minWidth = 60;
268
+ const urlLength = url.length + 4; // 2 spaces on each side + "ā•‘"
269
+ const boxWidth = Math.max(minWidth, urlLength);
270
+
271
+ console.log("\nā•”" + "═".repeat(boxWidth) + "ā•—");
272
+ const headerText = "āœ… PUBLIC URL";
273
+ const headerPadding = boxWidth - headerText.length;
274
+ console.log("ā•‘ " + headerText + " ".repeat(Math.max(0, headerPadding)) + "ā•‘");
275
+ console.log("ā• " + "═".repeat(boxWidth) + "ā•£");
276
+ const urlPadding = boxWidth - url.length;
277
+ console.log("ā•‘ " + url + " ".repeat(Math.max(0, urlPadding)) + "ā•‘");
278
+ console.log("ā• " + "═".repeat(boxWidth) + "ā•£");
279
+ const shareText = "šŸ’” Share this URL with anyone!";
280
+ const sharePadding = boxWidth - shareText.length;
281
+ console.log("ā•‘ " + shareText + " ".repeat(Math.max(0, sharePadding)) + "ā•‘");
282
+ console.log("ā•š" + "═".repeat(boxWidth) + "ā•\n");
283
+ }
284
+ }
285
+ // Show other important messages (but filter out most INF/WRN logs)
286
+ else if (!trimmed.includes("INF") && !trimmed.includes("WRN") && !trimmed.includes("+---")) {
287
+ // Don't show these lines
288
+ }
289
+ } else if (serviceName === "Ngrok") {
290
+ if (trimmed.includes("https://") || trimmed.includes("http://")) {
291
+ const url = trimmed;
292
+ const minWidth = 60;
293
+ const urlLength = url.length + 4;
294
+ const boxWidth = Math.max(minWidth, urlLength);
295
+
296
+ console.log("\nā•”" + "═".repeat(boxWidth) + "ā•—");
297
+ const headerText = "āœ… PUBLIC URL";
298
+ const headerPadding = boxWidth - headerText.length;
299
+ console.log("ā•‘ " + headerText + " ".repeat(Math.max(0, headerPadding)) + "ā•‘");
300
+ console.log("ā• " + "═".repeat(boxWidth) + "ā•£");
301
+ const urlPadding = boxWidth - url.length;
302
+ console.log("ā•‘ " + url + " ".repeat(Math.max(0, urlPadding)) + "ā•‘");
303
+ console.log("ā•š" + "═".repeat(boxWidth) + "ā•\n");
304
+ }
305
+ } else {
306
+ // LocalTunnel or other services
307
+ if (trimmed.includes("your url is:")) {
308
+ const urlMatch = trimmed.match(/https?:\/\/[^\s]+/);
309
+ if (urlMatch) {
310
+ const url = urlMatch[0];
311
+ const minWidth = 60;
312
+ const urlLength = url.length + 4;
313
+ const boxWidth = Math.max(minWidth, urlLength);
314
+
315
+ console.log("\nā•”" + "═".repeat(boxWidth) + "ā•—");
316
+ const headerText = "āœ… PUBLIC URL";
317
+ const headerPadding = boxWidth - headerText.length;
318
+ console.log("ā•‘ " + headerText + " ".repeat(Math.max(0, headerPadding)) + "ā•‘");
319
+ console.log("ā• " + "═".repeat(boxWidth) + "ā•£");
320
+ const urlPadding = boxWidth - url.length;
321
+ console.log("ā•‘ " + url + " ".repeat(Math.max(0, urlPadding)) + "ā•‘");
322
+ console.log("ā•š" + "═".repeat(boxWidth) + "ā•\n");
323
+ }
324
+ }
325
+ }
326
+ });
327
+ });
328
+
329
+ tunnelProcess.stderr.on("data", (data) => {
330
+ const output = data.toString();
331
+
332
+ // For Cloudflare, check if URL is in stderr (sometimes it is)
333
+ if (serviceName === "Cloudflare" && output.includes("trycloudflare.com")) {
334
+ const urlMatch = output.match(/(https?:\/\/[^\s]+trycloudflare\.com[^\s]*)/);
335
+ if (urlMatch) {
336
+ const url = urlMatch[1];
337
+ const minWidth = 60;
338
+ const urlLength = url.length + 4;
339
+ const boxWidth = Math.max(minWidth, urlLength);
340
+
341
+ console.log("\nā•”" + "═".repeat(boxWidth) + "ā•—");
342
+ const headerText = "āœ… PUBLIC URL";
343
+ const headerPadding = boxWidth - headerText.length;
344
+ console.log("ā•‘ " + headerText + " ".repeat(Math.max(0, headerPadding)) + "ā•‘");
345
+ console.log("ā• " + "═".repeat(boxWidth) + "ā•£");
346
+ const urlPadding = boxWidth - url.length;
347
+ console.log("ā•‘ " + url + " ".repeat(Math.max(0, urlPadding)) + "ā•‘");
348
+ console.log("ā•š" + "═".repeat(boxWidth) + "ā•\n");
349
+ }
350
+ }
351
+
352
+ // Only show errors, not info messages
353
+ if (!output.includes("INF") && !output.includes("WRN")) {
354
+ const trimmed = output.trim();
355
+ if (trimmed && !trimmed.includes("originCertPath")) {
356
+ console.error(`āš ļø ${trimmed}`);
357
+ }
358
+ }
359
+ });
360
+
361
+ tunnelProcess.on("error", (error) => {
362
+ console.error(`\nāŒ ${serviceName} error:`, error.message);
363
+ });
364
+
365
+ tunnelProcess.on("exit", (code) => {
366
+ if (code !== 0 && code !== null) {
367
+ console.error(`\nāš ļø ${serviceName} exited with code ${code}`);
368
+ }
369
+ });
370
+ }
371
+
372
+ // Handle cleanup on exit
373
+ function cleanup() {
374
+ console.log("\n\nšŸ›‘ Shutting down tunnel...");
375
+ try {
376
+ if (tunnelProcess) {
377
+ tunnelProcess.kill();
378
+ }
379
+ } catch (e) {
380
+ // Ignore errors during cleanup
381
+ }
382
+ setTimeout(() => {
383
+ process.exit(0);
384
+ }, 500);
385
+ }
386
+
387
+ process.on("SIGINT", cleanup);
388
+ process.on("SIGTERM", cleanup);
389
+
390
+ // Start trying tunnel services
391
+ tryTunnelServices().catch((error) => {
392
+ console.error("Error:", error);
393
+ process.exit(1);
394
+ });
@@ -0,0 +1,72 @@
1
+ import http from "http";
2
+ import httpProxy from "http-proxy";
3
+
4
+ // Get ports from command line
5
+ const TARGET_PORT = parseInt(process.argv[2]); // Your dev server port
6
+ const PROXY_PORT = parseInt(process.argv[3]); // Port for tunnel to connect to
7
+ const PROJECT_NAME = process.argv[4] || "Project";
8
+
9
+ if (!TARGET_PORT || !PROXY_PORT) {
10
+ console.error("Usage: node proxy-server.js <target-port> <proxy-port> [project-name]");
11
+ process.exit(1);
12
+ }
13
+
14
+ // Create proxy
15
+ const proxy = httpProxy.createProxyServer({
16
+ target: `http://localhost:${TARGET_PORT}`,
17
+ changeOrigin: true,
18
+ ws: true, // Enable WebSocket proxying (for HMR)
19
+ xfwd: true
20
+ });
21
+
22
+ // Handle proxy errors
23
+ proxy.on("error", (err, req, res) => {
24
+ console.error("āŒ Proxy error:", err.message);
25
+ if (res.writeHead) {
26
+ res.writeHead(502, { "Content-Type": "text/plain" });
27
+ res.end("Bad Gateway: Could not connect to your dev server.\nMake sure it's running on port " + TARGET_PORT);
28
+ }
29
+ });
30
+
31
+ // Create HTTP server
32
+ const server = http.createServer((req, res) => {
33
+ // Add CORS headers
34
+ res.setHeader("Access-Control-Allow-Origin", "*");
35
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
36
+ res.setHeader("Access-Control-Allow-Headers", "*");
37
+
38
+ if (req.method === "OPTIONS") {
39
+ res.writeHead(200);
40
+ res.end();
41
+ return;
42
+ }
43
+
44
+ // Proxy the request
45
+ proxy.web(req, res);
46
+ });
47
+
48
+ // Handle WebSocket upgrade (for Vite HMR)
49
+ server.on("upgrade", (req, socket, head) => {
50
+ proxy.ws(req, socket, head);
51
+ });
52
+
53
+ // Start server
54
+ server.listen(PROXY_PORT, () => {
55
+ console.log("╔════════════════════════════════════════════╗");
56
+ console.log("ā•‘ šŸ”— DevTunnel Proxy Server ā•‘");
57
+ console.log("╠════════════════════════════════════════════╣");
58
+ console.log(`ā•‘ šŸ“¦ Project: ${PROJECT_NAME.padEnd(28)} ā•‘`);
59
+ console.log(`ā•‘ šŸŽÆ Dev Server: http://localhost:${TARGET_PORT.toString().padEnd(7)} ā•‘`);
60
+ console.log(`ā•‘ šŸ”Œ Proxy Port: ${PROXY_PORT.toString().padEnd(28)} ā•‘`);
61
+ console.log("╠════════════════════════════════════════════╣");
62
+ console.log("ā•‘ āœ… Ready! Tunnel will connect to proxy ā•‘");
63
+ console.log("ā•‘ šŸ’” No config changes needed ā•‘");
64
+ console.log("ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•\n");
65
+ });
66
+
67
+ // Handle shutdown
68
+ process.on("SIGINT", () => {
69
+ console.log("\n\nšŸ›‘ Shutting down proxy...");
70
+ server.close();
71
+ process.exit(0);
72
+ });