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 +82 -0
- package/bin/README.md +40 -0
- package/package.json +56 -0
- package/src/core/RUN.js +41 -0
- package/src/core/index.js +394 -0
- package/src/core/proxy-server.js +72 -0
- package/src/core/setup-cloudflared.js +334 -0
- package/src/core/start.js +200 -0
- package/src/utils/folder-picker.js +140 -0
- package/src/utils/tunnel-helpers.js +35 -0
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
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](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
|
+
}
|
package/src/core/RUN.js
ADDED
|
@@ -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
|
+
});
|