devtunnel-cli 3.0.38 → 3.0.40
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 +9 -93
- package/package.json +6 -4
- package/src/core/setup-cloudflared.js +37 -37
- package/src/core/start.js +65 -61
- package/src/utils/pages/index.html +4 -4
package/README.md
CHANGED
|
@@ -14,10 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
DevTunnel-CLI is designed for **DEVELOPMENT**, **TESTING**, **DEMOS**, and **WEBHOOK DEBUGGING**. It provides fast, frictionless access to your local dev servers from anywhere.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
- Production environments
|
|
19
|
-
- Long-lived public services
|
|
20
|
-
- Hosting production traffic
|
|
17
|
+
DevTunnel-CLI is optimized for development workflows — rapid sharing, demos, and debugging. For production-facing use, consider managed or enterprise tunnel solutions that provide governance, identity, and long-lived endpoints.
|
|
21
18
|
|
|
22
19
|
DevTunnel-CLI is built for developers who need instant, temporary public URLs to share work-in-progress, test on mobile devices, demo features to clients, or debug webhooks from third-party services.
|
|
23
20
|
|
|
@@ -28,16 +25,19 @@ DevTunnel-CLI is built for developers who need instant, temporary public URLs to
|
|
|
28
25
|
### Step-by-Step Guide
|
|
29
26
|
|
|
30
27
|
**1. Install DevTunnel (one-time setup):**
|
|
28
|
+
|
|
31
29
|
```bash
|
|
32
|
-
npm
|
|
30
|
+
npm i -g devtunnel-cli
|
|
33
31
|
```
|
|
34
32
|
|
|
35
33
|
**2. Navigate to your project directory:**
|
|
34
|
+
|
|
36
35
|
```bash
|
|
37
36
|
cd your-project
|
|
38
37
|
```
|
|
39
38
|
|
|
40
39
|
**3. Have your app running (in one terminal):**
|
|
40
|
+
|
|
41
41
|
```bash
|
|
42
42
|
npm run dev
|
|
43
43
|
# OR php artisan serve (Laravel)
|
|
@@ -46,6 +46,7 @@ npm run dev
|
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
**4. Run DevTunnel (in another terminal, same directory):**
|
|
49
|
+
|
|
49
50
|
```bash
|
|
50
51
|
cd your-project # Same directory as your project
|
|
51
52
|
devtunnel-cli # Auto-detects project type and port!
|
|
@@ -63,7 +64,7 @@ devtunnel-cli # Auto-detects project type and port!
|
|
|
63
64
|
- 🌍 **Cross-Platform** - Windows, macOS, Linux
|
|
64
65
|
- 🚀 **Any Framework** - Node, React, Laravel, plain HTML, PHP/XAMPP
|
|
65
66
|
- 📄 **HTML** - Default port 5500; built-in static server if none running
|
|
66
|
-
- 🐘 **PHP/XAMPP** - Port 80; supports htdocs subfolders (e.g. http://localhost/YourProject
|
|
67
|
+
- 🐘 **PHP/XAMPP** - Port 80; supports htdocs subfolders (e.g. <http://localhost/YourProject/>)
|
|
67
68
|
- 🔄 **Multi-Service** - Cloudflare, Ngrok, LocalTunnel fallback
|
|
68
69
|
- 🔌 **Multiple Ports** - DevTunnel-CLI supports multiple ports; auto-detects or lets you choose
|
|
69
70
|
- 📹 **Streaming Support** - Handles video/audio files (with limitations for large files)
|
|
@@ -74,7 +75,7 @@ devtunnel-cli # Auto-detects project type and port!
|
|
|
74
75
|
|
|
75
76
|
**Important:** Run `devtunnel-cli` from the same directory as your project!
|
|
76
77
|
|
|
77
|
-
1. **Install DevTunnel** (one-time): `npm
|
|
78
|
+
1. **Install DevTunnel** (one-time): `npm i -g devtunnel-cli`
|
|
78
79
|
2. **Go to your project**: `cd your-project` (Node, Laravel, HTML, or XAMPP folder)
|
|
79
80
|
3. **Have your app running**: `npm run dev`, `php artisan serve`, or XAMPP. For HTML, optional — DevTunnel can serve it.
|
|
80
81
|
4. **Open a new terminal** in the same project directory
|
|
@@ -82,6 +83,7 @@ devtunnel-cli # Auto-detects project type and port!
|
|
|
82
83
|
6. **Get your public URL** and share it! 🌍
|
|
83
84
|
|
|
84
85
|
**Example (Node):**
|
|
86
|
+
|
|
85
87
|
```bash
|
|
86
88
|
# Terminal 1
|
|
87
89
|
cd my-react-app
|
|
@@ -96,92 +98,6 @@ devtunnel-cli
|
|
|
96
98
|
|
|
97
99
|
---
|
|
98
100
|
|
|
99
|
-
## 🔒 Security & Access Model
|
|
100
|
-
|
|
101
|
-
DevTunnel-CLI intentionally **does NOT use authentication or access control**. This is a deliberate design choice to ensure fast, frictionless development workflows.
|
|
102
|
-
|
|
103
|
-
**How Access Works:**
|
|
104
|
-
- Anyone with the generated temporary URL can access the tunnel until it is stopped
|
|
105
|
-
- No login, password, or identity verification required
|
|
106
|
-
- Access is limited by **possession of the URL only**
|
|
107
|
-
|
|
108
|
-
**Why This Design?**
|
|
109
|
-
- **Speed**: Get public URLs instantly without authentication setup
|
|
110
|
-
- **Simplicity**: Zero configuration — just run and share
|
|
111
|
-
- **Friction-free collaboration**: Share with teammates without managing accounts or permissions
|
|
112
|
-
|
|
113
|
-
**Temporary URL Behavior:**
|
|
114
|
-
- URLs are **short-lived** and **unlisted**
|
|
115
|
-
- URLs are **destroyed** when the tunnel stops
|
|
116
|
-
- New random URLs are generated each time you run DevTunnel-CLI
|
|
117
|
-
- URLs are not indexed by search engines
|
|
118
|
-
|
|
119
|
-
**Best Practices:**
|
|
120
|
-
- Only share URLs with trusted collaborators
|
|
121
|
-
- Stop the tunnel when not in use
|
|
122
|
-
- Never expose sensitive data or production databases through DevTunnel-CLI
|
|
123
|
-
- Use for development and testing only
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
## ⚠️ Limitations
|
|
128
|
-
|
|
129
|
-
DevTunnel-CLI has intentional limitations that make it ideal for development but unsuitable for other use cases:
|
|
130
|
-
|
|
131
|
-
### No Authentication or Access Control
|
|
132
|
-
- **By design**: No identity-based access control
|
|
133
|
-
- **By design**: No user-level permission management
|
|
134
|
-
- **By design**: No password protection or login system
|
|
135
|
-
- Anyone with the URL can access your tunnel
|
|
136
|
-
|
|
137
|
-
### Not Suitable For
|
|
138
|
-
- ❌ Production environments
|
|
139
|
-
- ❌ Long-lived public services
|
|
140
|
-
- ❌ Hosting production traffic
|
|
141
|
-
- ❌ Sensitive data exposure
|
|
142
|
-
- ❌ Public-facing applications
|
|
143
|
-
|
|
144
|
-
### Perfect For
|
|
145
|
-
- ✅ Development and testing
|
|
146
|
-
- ✅ Team collaboration and code reviews
|
|
147
|
-
- ✅ Mobile device testing
|
|
148
|
-
- ✅ Client demos and work-in-progress sharing
|
|
149
|
-
- ✅ Webhook debugging with third-party services
|
|
150
|
-
- ✅ Temporary public access to localhost
|
|
151
|
-
|
|
152
|
-
### File Size & Streaming Limits
|
|
153
|
-
- ✅ Small files (<10MB): Works perfectly
|
|
154
|
-
- ✅ Medium files (10-50MB): Works well, may have slight delays
|
|
155
|
-
- ⚠️ Large files (>50MB): May timeout depending on connection speed
|
|
156
|
-
- ⚠️ Very large files (>100MB): Not recommended for Cloudflare free tier
|
|
157
|
-
|
|
158
|
-
---
|
|
159
|
-
|
|
160
|
-
## 🆚 Comparison: DevTunnel-CLI vs. Enterprise Tunnels
|
|
161
|
-
|
|
162
|
-
DevTunnel-CLI is optimized for **speed and simplicity** rather than governance and authentication.
|
|
163
|
-
|
|
164
|
-
| Feature | DevTunnel-CLI | Enterprise Tunnels (e.g., Microsoft Dev Tunnels) |
|
|
165
|
-
|---------|---------------|--------------------------------------------------|
|
|
166
|
-
| **Setup Time** | Instant (0 config) | Requires account, authentication setup |
|
|
167
|
-
| **Authentication** | None (by design) | User-based auth, SSO, identity management |
|
|
168
|
-
| **Access Control** | URL possession only | Fine-grained permissions, user/group policies |
|
|
169
|
-
| **Use Case** | Development, testing, demos | Enterprise dev, governed access, compliance |
|
|
170
|
-
| **Speed** | Instant sharing | May require approval workflows |
|
|
171
|
-
| **Ideal For** | Solo devs, small teams, fast iteration | Large orgs, regulated industries, prod-like envs |
|
|
172
|
-
|
|
173
|
-
**Choose DevTunnel-CLI when:**
|
|
174
|
-
- You need instant, frictionless sharing
|
|
175
|
-
- You're working on non-sensitive development projects
|
|
176
|
-
- Speed and simplicity are priorities
|
|
177
|
-
|
|
178
|
-
**Choose enterprise tunnels when:**
|
|
179
|
-
- You need identity-based access control
|
|
180
|
-
- You're in a regulated or compliance-heavy environment
|
|
181
|
-
- You need audit logs and governance
|
|
182
|
-
|
|
183
|
-
---
|
|
184
|
-
|
|
185
101
|
## 📖 Documentation
|
|
186
102
|
|
|
187
103
|
- [Features](docs/FEATURES.md)
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devtunnel-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.40",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DevTunnel-CLI
|
|
5
|
+
"description": "DevTunnel-CLI — fast, zero-config tool to share local servers for development, testing, demos, and webhook debugging. npm i -g devtunnel-cli.",
|
|
6
6
|
"main": "src/core/start.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"README.md",
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
"dev": "node src/core/RUN.js",
|
|
17
17
|
"run": "node src/core/RUN.js",
|
|
18
18
|
"tunnel": "node src/core/index.js",
|
|
19
|
-
"sync-version": "node sync-version.js"
|
|
19
|
+
"sync-version": "node sync-version.js",
|
|
20
|
+
"test": "node -e \"console.log('ok')\"",
|
|
21
|
+
"prepublishOnly": "npm test"
|
|
20
22
|
},
|
|
21
23
|
"keywords": [
|
|
22
24
|
"DevTunnel-CLI",
|
|
@@ -81,4 +83,4 @@
|
|
|
81
83
|
"prompts": "^2.4.2"
|
|
82
84
|
},
|
|
83
85
|
"devDependencies": {}
|
|
84
|
-
}
|
|
86
|
+
}
|
|
@@ -69,7 +69,7 @@ async function isAdmin() {
|
|
|
69
69
|
if (process.platform !== 'win32') {
|
|
70
70
|
return process.getuid && process.getuid() === 0;
|
|
71
71
|
}
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
try {
|
|
74
74
|
const { stdout } = await execAsync('net session');
|
|
75
75
|
return stdout.length > 0;
|
|
@@ -84,7 +84,7 @@ function showPermissionSolutions(dirPath) {
|
|
|
84
84
|
console.log(' 1. Run terminal as Administrator (Right-click → Run as administrator)');
|
|
85
85
|
console.log(' 2. DevTunnel will automatically request admin privileges if needed');
|
|
86
86
|
} else {
|
|
87
|
-
|
|
87
|
+
console.log(' 1. Run with sudo: sudo npm i -g devtunnel-cli');
|
|
88
88
|
}
|
|
89
89
|
console.log(' 2. Check if antivirus is blocking file writes');
|
|
90
90
|
console.log(' 3. Check folder permissions for:', dirPath);
|
|
@@ -104,10 +104,10 @@ function downloadFile(url, dest, retryCount = 0) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const tempDest = dest + '.download';
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
// Clean up any existing temp file first
|
|
109
109
|
safeUnlink(tempDest);
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
let file;
|
|
112
112
|
try {
|
|
113
113
|
file = fs.createWriteStream(tempDest);
|
|
@@ -119,9 +119,9 @@ function downloadFile(url, dest, retryCount = 0) {
|
|
|
119
119
|
}
|
|
120
120
|
return;
|
|
121
121
|
}
|
|
122
|
-
|
|
123
|
-
const request = https.get(url, {
|
|
124
|
-
headers: {
|
|
122
|
+
|
|
123
|
+
const request = https.get(url, {
|
|
124
|
+
headers: {
|
|
125
125
|
'User-Agent': 'DevTunnel/3.0',
|
|
126
126
|
'Accept': '*/*'
|
|
127
127
|
},
|
|
@@ -170,9 +170,9 @@ function downloadFile(url, dest, retryCount = 0) {
|
|
|
170
170
|
fs.unlinkSync(dest);
|
|
171
171
|
}
|
|
172
172
|
fs.renameSync(tempDest, dest);
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
console.log('\n✅ Download complete');
|
|
175
|
-
|
|
175
|
+
|
|
176
176
|
// Make executable on Unix-like systems
|
|
177
177
|
if (process.platform !== 'win32') {
|
|
178
178
|
try {
|
|
@@ -183,7 +183,7 @@ function downloadFile(url, dest, retryCount = 0) {
|
|
|
183
183
|
console.log(' Run: chmod +x ' + dest);
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
// Verify file size
|
|
188
188
|
const stats = fs.statSync(dest);
|
|
189
189
|
if (stats.size < 1000000) { // Less than 1MB is suspicious
|
|
@@ -191,7 +191,7 @@ function downloadFile(url, dest, retryCount = 0) {
|
|
|
191
191
|
reject(new Error('Downloaded file is too small (corrupted)'));
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
|
|
195
195
|
resolve();
|
|
196
196
|
} catch (err) {
|
|
197
197
|
reject(new Error(`Cannot finalize download: ${err.message}`));
|
|
@@ -230,30 +230,30 @@ async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
|
230
230
|
for (let urlIndex = 0; urlIndex < urls.length; urlIndex++) {
|
|
231
231
|
const url = urls[urlIndex];
|
|
232
232
|
console.log(`📥 Source: ${urlIndex === 0 ? 'GitHub' : 'Mirror'} (${urlIndex + 1}/${urls.length})`);
|
|
233
|
-
|
|
233
|
+
|
|
234
234
|
for (let retry = 0; retry < maxRetries; retry++) {
|
|
235
235
|
try {
|
|
236
236
|
if (retry > 0) {
|
|
237
237
|
console.log(`🔄 Retry ${retry}/${maxRetries - 1}...`);
|
|
238
238
|
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s before retry
|
|
239
239
|
}
|
|
240
|
-
|
|
240
|
+
|
|
241
241
|
await downloadFile(url, dest, retry);
|
|
242
242
|
return true; // Success!
|
|
243
|
-
|
|
243
|
+
|
|
244
244
|
} catch (err) {
|
|
245
245
|
const isLastRetry = retry === maxRetries - 1;
|
|
246
246
|
const isLastUrl = urlIndex === urls.length - 1;
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
|
|
249
249
|
console.log(`\n❌ Permission Error: ${err.message}`);
|
|
250
|
-
|
|
250
|
+
|
|
251
251
|
if (process.platform === 'win32' && retry === 0) {
|
|
252
252
|
const admin = await isAdmin();
|
|
253
253
|
if (!admin) {
|
|
254
254
|
console.log('\n🔐 Attempting to request administrator privileges...');
|
|
255
255
|
console.log(' Please click "Yes" on the UAC prompt\n');
|
|
256
|
-
|
|
256
|
+
|
|
257
257
|
try {
|
|
258
258
|
const nodePath = process.execPath;
|
|
259
259
|
const scriptPath = process.argv[1];
|
|
@@ -266,16 +266,16 @@ async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
|
266
266
|
stdio: 'inherit',
|
|
267
267
|
shell: false
|
|
268
268
|
});
|
|
269
|
-
|
|
269
|
+
|
|
270
270
|
proc.on('close', () => {
|
|
271
271
|
process.exit(0);
|
|
272
272
|
});
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
proc.on('error', () => {
|
|
275
275
|
console.log('\n⚠️ Could not elevate privileges automatically');
|
|
276
276
|
showPermissionSolutions(path.dirname(dest));
|
|
277
277
|
});
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
return false;
|
|
280
280
|
} catch {
|
|
281
281
|
showPermissionSolutions(path.dirname(dest));
|
|
@@ -283,7 +283,7 @@ async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
|
283
283
|
}
|
|
284
284
|
}
|
|
285
285
|
}
|
|
286
|
-
|
|
286
|
+
|
|
287
287
|
showPermissionSolutions(path.dirname(dest));
|
|
288
288
|
throw err;
|
|
289
289
|
} else if (err.message.includes('ENOTFOUND') || err.message.includes('ECONNREFUSED')) {
|
|
@@ -293,11 +293,11 @@ async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
|
293
293
|
} else {
|
|
294
294
|
console.log(`\n❌ Error: ${err.message}`);
|
|
295
295
|
}
|
|
296
|
-
|
|
296
|
+
|
|
297
297
|
if (isLastRetry && isLastUrl) {
|
|
298
298
|
throw new Error(`All download attempts failed: ${err.message}`);
|
|
299
299
|
}
|
|
300
|
-
|
|
300
|
+
|
|
301
301
|
if (isLastRetry) {
|
|
302
302
|
console.log('💡 Trying alternative source...\n');
|
|
303
303
|
break;
|
|
@@ -305,18 +305,18 @@ async function downloadWithRetry(urls, dest, maxRetries = 3) {
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
}
|
|
308
|
-
|
|
308
|
+
|
|
309
309
|
throw new Error('All download sources failed');
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
export async function setupCloudflared() {
|
|
313
313
|
const platform = process.platform;
|
|
314
314
|
const binaryPath = getBinaryPath();
|
|
315
|
-
|
|
315
|
+
|
|
316
316
|
console.log('\n╔════════════════════════════════════════╗');
|
|
317
317
|
console.log('║ 📦 Cloudflare Setup (First Run) ║');
|
|
318
318
|
console.log('╚════════════════════════════════════════╝\n');
|
|
319
|
-
|
|
319
|
+
|
|
320
320
|
// Check if binary already exists
|
|
321
321
|
if (fs.existsSync(binaryPath)) {
|
|
322
322
|
try {
|
|
@@ -327,7 +327,7 @@ export async function setupCloudflared() {
|
|
|
327
327
|
testProc.on('error', () => resolve(false));
|
|
328
328
|
setTimeout(() => resolve(false), 5000);
|
|
329
329
|
});
|
|
330
|
-
|
|
330
|
+
|
|
331
331
|
if (works) {
|
|
332
332
|
console.log('✅ Cloudflare already installed and working\n');
|
|
333
333
|
return binaryPath;
|
|
@@ -351,13 +351,13 @@ export async function setupCloudflared() {
|
|
|
351
351
|
console.log(`🖥️ Platform: ${getPlatformName()}`);
|
|
352
352
|
console.log(`📍 Install to: ${binaryPath}`);
|
|
353
353
|
console.log(`📊 Size: ~40 MB\n`);
|
|
354
|
-
|
|
354
|
+
|
|
355
355
|
// Check disk space
|
|
356
356
|
if (!hasEnoughDiskSpace()) {
|
|
357
357
|
console.error('❌ ERROR: Not enough disk space (need 50+ MB)\n');
|
|
358
358
|
return null;
|
|
359
359
|
}
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
// Check write permissions
|
|
362
362
|
try {
|
|
363
363
|
const dir = path.dirname(binaryPath);
|
|
@@ -373,12 +373,12 @@ export async function setupCloudflared() {
|
|
|
373
373
|
console.error(` Reason: ${err.message}\n`);
|
|
374
374
|
return null;
|
|
375
375
|
}
|
|
376
|
-
|
|
376
|
+
|
|
377
377
|
console.log('📥 Starting download...\n');
|
|
378
|
-
|
|
378
|
+
|
|
379
379
|
try {
|
|
380
380
|
await downloadWithRetry(urls, binaryPath);
|
|
381
|
-
|
|
381
|
+
|
|
382
382
|
// Final verification
|
|
383
383
|
console.log('\n🔍 Verifying installation...');
|
|
384
384
|
const testProc = spawn(binaryPath, ['--version'], { shell: true, stdio: 'pipe' });
|
|
@@ -387,7 +387,7 @@ export async function setupCloudflared() {
|
|
|
387
387
|
testProc.on('error', () => resolve(false));
|
|
388
388
|
setTimeout(() => resolve(false), 5000);
|
|
389
389
|
});
|
|
390
|
-
|
|
390
|
+
|
|
391
391
|
if (works) {
|
|
392
392
|
console.log('✅ Verification successful!');
|
|
393
393
|
console.log('✅ Cloudflare ready to use\n');
|
|
@@ -397,13 +397,13 @@ export async function setupCloudflared() {
|
|
|
397
397
|
safeUnlink(binaryPath);
|
|
398
398
|
return null;
|
|
399
399
|
}
|
|
400
|
-
|
|
400
|
+
|
|
401
401
|
} catch (err) {
|
|
402
402
|
console.error('\n╔════════════════════════════════════════╗');
|
|
403
403
|
console.error('║ ❌ Installation Failed ║');
|
|
404
404
|
console.error('╚════════════════════════════════════════╝\n');
|
|
405
405
|
console.error(`Reason: ${err.message}\n`);
|
|
406
|
-
|
|
406
|
+
|
|
407
407
|
if (err.message.includes('Permission denied') || err.message.includes('EPERM') || err.message.includes('EACCES')) {
|
|
408
408
|
showPermissionSolutions(path.dirname(binaryPath));
|
|
409
409
|
} else {
|
|
@@ -413,9 +413,9 @@ export async function setupCloudflared() {
|
|
|
413
413
|
console.log(' 3. Try running as administrator');
|
|
414
414
|
console.log(' 4. Install manually: https://github.com/cloudflare/cloudflared/releases\n');
|
|
415
415
|
}
|
|
416
|
-
|
|
416
|
+
|
|
417
417
|
console.log('🔄 DevTunnel will use fallback tunnels (Ngrok/LocalTunnel)\n');
|
|
418
|
-
|
|
418
|
+
|
|
419
419
|
return null;
|
|
420
420
|
}
|
|
421
421
|
}
|
package/src/core/start.js
CHANGED
|
@@ -17,25 +17,25 @@ function getPackageVersion() {
|
|
|
17
17
|
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
18
18
|
if (existsSync(pkgPath)) {
|
|
19
19
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
20
|
-
return pkg.version || "3.0.
|
|
20
|
+
return pkg.version || "3.0.40";
|
|
21
21
|
}
|
|
22
|
-
} catch (err) {}
|
|
23
|
-
return "3.0.
|
|
22
|
+
} catch (err) { }
|
|
23
|
+
return "3.0.40";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Helper to run command
|
|
27
27
|
function runCommand(command, args = [], cwd = process.cwd()) {
|
|
28
28
|
return new Promise((resolve) => {
|
|
29
|
-
const proc = spawn(command, args, {
|
|
30
|
-
shell: true,
|
|
29
|
+
const proc = spawn(command, args, {
|
|
30
|
+
shell: true,
|
|
31
31
|
stdio: "pipe",
|
|
32
32
|
cwd: cwd
|
|
33
33
|
});
|
|
34
34
|
let output = "";
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
proc.stdout?.on("data", (data) => output += data.toString());
|
|
37
37
|
proc.stderr?.on("data", (data) => output += data.toString());
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
proc.on("close", (code) => resolve({ code, output }));
|
|
40
40
|
proc.on("error", () => resolve({ code: 1, output: "" }));
|
|
41
41
|
});
|
|
@@ -51,7 +51,7 @@ async function commandExists(command) {
|
|
|
51
51
|
function checkPortInUse(port) {
|
|
52
52
|
return new Promise((resolve) => {
|
|
53
53
|
const server = http.createServer();
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
server.once('error', (err) => {
|
|
56
56
|
// Port is in use
|
|
57
57
|
if (err.code === 'EADDRINUSE') {
|
|
@@ -60,7 +60,7 @@ function checkPortInUse(port) {
|
|
|
60
60
|
resolve(false);
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
|
-
|
|
63
|
+
|
|
64
64
|
server.listen(port, () => {
|
|
65
65
|
// Port is available (not in use)
|
|
66
66
|
server.once('close', () => resolve(false));
|
|
@@ -79,7 +79,7 @@ async function waitForServerReady(port, timeoutMs = 10000) {
|
|
|
79
79
|
req.on("error", () => resolve(null));
|
|
80
80
|
});
|
|
81
81
|
if (code !== null && code >= 200 && code < 500) return true;
|
|
82
|
-
} catch (err) {}
|
|
82
|
+
} catch (err) { }
|
|
83
83
|
await new Promise((r) => setTimeout(r, 300));
|
|
84
84
|
}
|
|
85
85
|
return false;
|
|
@@ -91,24 +91,24 @@ function detectPortFromPackage(packagePath) {
|
|
|
91
91
|
if (!existsSync(packagePath)) return null;
|
|
92
92
|
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
93
93
|
const scripts = packageJson.scripts || {};
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
// Check for common dev commands
|
|
96
96
|
const devScript = scripts.dev || scripts.start || scripts.serve;
|
|
97
97
|
if (!devScript) return null;
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// Try to extract port from script
|
|
100
100
|
const portMatch = devScript.match(/--port\s+(\d+)|:(\d+)|port[=:](\d+)/i);
|
|
101
101
|
if (portMatch) {
|
|
102
102
|
return parseInt(portMatch[1] || portMatch[2] || portMatch[3]);
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
// Default ports based on framework
|
|
106
106
|
if (devScript.includes('vite')) return 5173;
|
|
107
107
|
if (devScript.includes('next')) return 3000;
|
|
108
108
|
if (devScript.includes('react-scripts')) return 3000;
|
|
109
109
|
if (devScript.includes('webpack')) return 8080;
|
|
110
110
|
if (devScript.includes('express')) return 3000;
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
return null;
|
|
113
113
|
} catch (err) {
|
|
114
114
|
return null;
|
|
@@ -148,7 +148,7 @@ function detectPhpProject(currentDir) {
|
|
|
148
148
|
async function detectRunningDevServer() {
|
|
149
149
|
const commonPorts = [3000, 5173, 5500, 8080, 8000, 80, 5000, 4000, 3001, 5174]; // 80 for XAMPP
|
|
150
150
|
const detected = [];
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
for (const port of commonPorts) {
|
|
153
153
|
const inUse = await checkPortInUse(port);
|
|
154
154
|
if (inUse) {
|
|
@@ -174,7 +174,7 @@ async function detectRunningDevServer() {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
|
-
|
|
177
|
+
|
|
178
178
|
return detected;
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -244,6 +244,8 @@ async function autoDetectProject() {
|
|
|
244
244
|
// ASCII Logo - Compatible with all OS and terminals
|
|
245
245
|
function showLogo() {
|
|
246
246
|
console.log("");
|
|
247
|
+
console.log(" ");
|
|
248
|
+
console.log(" ");
|
|
247
249
|
console.log("8888888b. 88888888888 888 .d8888b. 888 8888888 ");
|
|
248
250
|
console.log('888 "Y88b 888 888 d88P Y88b 888 888 ');
|
|
249
251
|
console.log("888 888 888 888 888 888 888 888 ");
|
|
@@ -262,17 +264,19 @@ async function main() {
|
|
|
262
264
|
// ANSI escape codes for clear screen + cursor to top
|
|
263
265
|
process.stdout.write('\x1B[2J\x1B[0f');
|
|
264
266
|
console.clear(); // Fallback for terminals that don't support ANSI
|
|
265
|
-
|
|
267
|
+
|
|
266
268
|
// Show ASCII logo
|
|
267
269
|
showLogo();
|
|
268
|
-
|
|
270
|
+
|
|
269
271
|
console.log(`DevTunnel v${getPackageVersion()}`);
|
|
270
272
|
console.log("Share your local dev servers worldwide");
|
|
271
273
|
console.log("");
|
|
272
274
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
275
|
+
console.log("");
|
|
273
276
|
console.log("Repository: https://github.com/maiz-an/DevTunnel-CLI");
|
|
274
277
|
console.log("npm Package: https://www.npmjs.com/package/devtunnel-cli");
|
|
275
278
|
console.log("Website: https://devtunnel-cli.vercel.app");
|
|
279
|
+
console.log("");
|
|
276
280
|
console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
|
|
277
281
|
console.log("");
|
|
278
282
|
|
|
@@ -288,12 +292,12 @@ async function main() {
|
|
|
288
292
|
|
|
289
293
|
// Step 2: Check Cloudflare (bundled or system-installed)
|
|
290
294
|
console.log("[2/4] Checking Cloudflare...");
|
|
291
|
-
|
|
295
|
+
|
|
292
296
|
// Import bundled cloudflared helpers
|
|
293
297
|
const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
|
|
294
|
-
|
|
298
|
+
|
|
295
299
|
let cloudflareAvailable = false;
|
|
296
|
-
|
|
300
|
+
|
|
297
301
|
if (hasBundledCloudflared()) {
|
|
298
302
|
console.log("SUCCESS: Using bundled Cloudflare (no install needed)");
|
|
299
303
|
cloudflareAvailable = true;
|
|
@@ -304,10 +308,10 @@ async function main() {
|
|
|
304
308
|
console.log("First time setup - Downloading Cloudflare...");
|
|
305
309
|
console.log("This only happens once (~40MB, 10-30 seconds)");
|
|
306
310
|
console.log("");
|
|
307
|
-
|
|
311
|
+
|
|
308
312
|
try {
|
|
309
313
|
const bundledPath = await setupCloudflared();
|
|
310
|
-
|
|
314
|
+
|
|
311
315
|
if (bundledPath) {
|
|
312
316
|
console.log("SUCCESS: Cloudflare ready to use");
|
|
313
317
|
cloudflareAvailable = true;
|
|
@@ -322,7 +326,7 @@ async function main() {
|
|
|
322
326
|
console.log("");
|
|
323
327
|
}
|
|
324
328
|
}
|
|
325
|
-
|
|
329
|
+
|
|
326
330
|
// Show what's available
|
|
327
331
|
if (!cloudflareAvailable) {
|
|
328
332
|
console.log("DevTunnel has multi-service fallback:");
|
|
@@ -354,20 +358,20 @@ async function main() {
|
|
|
354
358
|
|
|
355
359
|
// Step 4: Auto-detect or select project
|
|
356
360
|
console.log("[4/4] Detecting project...");
|
|
357
|
-
|
|
361
|
+
|
|
358
362
|
let projectPath, projectName, devPort;
|
|
359
|
-
|
|
363
|
+
|
|
360
364
|
// Try to auto-detect project in current directory
|
|
361
365
|
const autoDetected = await autoDetectProject();
|
|
362
|
-
|
|
366
|
+
|
|
363
367
|
if (autoDetected && autoDetected.port) {
|
|
364
368
|
// Auto-detected project with port
|
|
365
369
|
projectPath = autoDetected.path;
|
|
366
370
|
projectName = autoDetected.name;
|
|
367
|
-
|
|
371
|
+
|
|
368
372
|
// Double-check: verify the port is actually in use
|
|
369
373
|
const portInUse = await checkPortInUse(autoDetected.port);
|
|
370
|
-
|
|
374
|
+
|
|
371
375
|
if (!portInUse) {
|
|
372
376
|
// Detected port is not actually running, check for other running servers
|
|
373
377
|
const portSource =
|
|
@@ -380,7 +384,7 @@ async function main() {
|
|
|
380
384
|
: "package.json";
|
|
381
385
|
console.log(`Detected port ${autoDetected.port} (${portSource}), but no server running on that port`);
|
|
382
386
|
console.log("Checking for running dev servers...");
|
|
383
|
-
|
|
387
|
+
|
|
384
388
|
const runningPorts = await detectRunningDevServer();
|
|
385
389
|
if (runningPorts.length > 0) {
|
|
386
390
|
if (runningPorts.length === 1) {
|
|
@@ -394,12 +398,12 @@ async function main() {
|
|
|
394
398
|
message: "Select port:",
|
|
395
399
|
choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
|
|
396
400
|
});
|
|
397
|
-
|
|
401
|
+
|
|
398
402
|
if (!portResponse.port) {
|
|
399
403
|
console.log("ERROR: No port selected");
|
|
400
404
|
process.exit(1);
|
|
401
405
|
}
|
|
402
|
-
|
|
406
|
+
|
|
403
407
|
devPort = portResponse.port;
|
|
404
408
|
}
|
|
405
409
|
} else {
|
|
@@ -411,12 +415,12 @@ async function main() {
|
|
|
411
415
|
// Port is in use, use it
|
|
412
416
|
devPort = autoDetected.port;
|
|
413
417
|
}
|
|
414
|
-
|
|
418
|
+
|
|
415
419
|
console.log(`Detected project: ${projectName}`);
|
|
416
420
|
console.log(`Using port: ${devPort}`);
|
|
417
421
|
console.log(`Using current directory: ${projectPath}`);
|
|
418
422
|
console.log("");
|
|
419
|
-
|
|
423
|
+
|
|
420
424
|
// Confirm with user
|
|
421
425
|
const confirm = await prompts({
|
|
422
426
|
type: "confirm",
|
|
@@ -424,22 +428,22 @@ async function main() {
|
|
|
424
428
|
message: "Use detected project?",
|
|
425
429
|
initial: true
|
|
426
430
|
});
|
|
427
|
-
|
|
431
|
+
|
|
428
432
|
if (!confirm.value) {
|
|
429
433
|
// User wants to select manually
|
|
430
434
|
console.log("");
|
|
431
435
|
console.log("Selecting project manually...");
|
|
432
436
|
console.log("");
|
|
433
|
-
|
|
437
|
+
|
|
434
438
|
const selectedPath = await selectFolder();
|
|
435
439
|
if (!selectedPath || selectedPath.length === 0) {
|
|
436
440
|
console.log("ERROR: No folder selected");
|
|
437
441
|
process.exit(1);
|
|
438
442
|
}
|
|
439
|
-
|
|
443
|
+
|
|
440
444
|
projectPath = selectedPath;
|
|
441
445
|
projectName = basename(selectedPath);
|
|
442
|
-
|
|
446
|
+
|
|
443
447
|
// Try to detect port for selected project (Laravel → 8000, HTML → 5500, Node from package.json)
|
|
444
448
|
const selectedPackagePath = join(selectedPath, "package.json");
|
|
445
449
|
const laravelSelected = detectLaravelProject(selectedPath);
|
|
@@ -449,19 +453,19 @@ async function main() {
|
|
|
449
453
|
: htmlSelected
|
|
450
454
|
? htmlSelected.defaultPort
|
|
451
455
|
: detectPortFromPackage(selectedPackagePath);
|
|
452
|
-
|
|
456
|
+
|
|
453
457
|
const portResponse = await prompts({
|
|
454
458
|
type: "number",
|
|
455
459
|
name: "port",
|
|
456
460
|
message: "Enter your dev server port:",
|
|
457
461
|
initial: detectedPort || 5173
|
|
458
462
|
});
|
|
459
|
-
|
|
463
|
+
|
|
460
464
|
if (!portResponse.port) {
|
|
461
465
|
console.log("ERROR: No port entered");
|
|
462
466
|
process.exit(1);
|
|
463
467
|
}
|
|
464
|
-
|
|
468
|
+
|
|
465
469
|
devPort = portResponse.port;
|
|
466
470
|
} else {
|
|
467
471
|
// User confirmed – let them keep default port or type another (e.g. HTML default 5500, can change)
|
|
@@ -479,16 +483,16 @@ async function main() {
|
|
|
479
483
|
// Project detected but no port
|
|
480
484
|
projectPath = autoDetected.path;
|
|
481
485
|
projectName = autoDetected.name;
|
|
482
|
-
|
|
486
|
+
|
|
483
487
|
console.log(`Detected project: ${projectName}`);
|
|
484
488
|
console.log(`Using current directory: ${projectPath}`);
|
|
485
489
|
console.log("Checking for running dev servers...");
|
|
486
|
-
|
|
490
|
+
|
|
487
491
|
const runningPorts = await detectRunningDevServer();
|
|
488
|
-
|
|
492
|
+
|
|
489
493
|
if (runningPorts.length > 0) {
|
|
490
494
|
console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
|
|
491
|
-
|
|
495
|
+
|
|
492
496
|
if (runningPorts.length === 1) {
|
|
493
497
|
devPort = runningPorts[0];
|
|
494
498
|
console.log(`Using port: ${devPort}`);
|
|
@@ -500,12 +504,12 @@ async function main() {
|
|
|
500
504
|
message: "Select port:",
|
|
501
505
|
choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
|
|
502
506
|
});
|
|
503
|
-
|
|
507
|
+
|
|
504
508
|
if (!portResponse.port) {
|
|
505
509
|
console.log("ERROR: No port selected");
|
|
506
510
|
process.exit(1);
|
|
507
511
|
}
|
|
508
|
-
|
|
512
|
+
|
|
509
513
|
devPort = portResponse.port;
|
|
510
514
|
}
|
|
511
515
|
} else {
|
|
@@ -516,33 +520,33 @@ async function main() {
|
|
|
516
520
|
message: "Enter your dev server port:",
|
|
517
521
|
initial: 5173
|
|
518
522
|
});
|
|
519
|
-
|
|
523
|
+
|
|
520
524
|
if (!portResponse.port) {
|
|
521
525
|
console.log("ERROR: No port entered");
|
|
522
526
|
process.exit(1);
|
|
523
527
|
}
|
|
524
|
-
|
|
528
|
+
|
|
525
529
|
devPort = portResponse.port;
|
|
526
530
|
}
|
|
527
|
-
|
|
531
|
+
|
|
528
532
|
console.log("");
|
|
529
533
|
} else {
|
|
530
534
|
// No auto-detection, use folder picker
|
|
531
535
|
console.log("No project detected in current directory");
|
|
532
536
|
console.log("Opening folder picker...");
|
|
533
537
|
console.log("");
|
|
534
|
-
|
|
538
|
+
|
|
535
539
|
projectPath = await selectFolder();
|
|
536
|
-
|
|
540
|
+
|
|
537
541
|
if (!projectPath || projectPath.length === 0) {
|
|
538
542
|
console.log("ERROR: No folder selected");
|
|
539
543
|
process.exit(1);
|
|
540
544
|
}
|
|
541
|
-
|
|
545
|
+
|
|
542
546
|
projectName = basename(projectPath);
|
|
543
547
|
console.log(`Selected: ${projectPath}`);
|
|
544
548
|
console.log("");
|
|
545
|
-
|
|
549
|
+
|
|
546
550
|
// Try to detect port for selected project (Laravel → 8000, HTML → 5500, PHP → 80, Node from package.json)
|
|
547
551
|
const selectedPackagePath = join(projectPath, "package.json");
|
|
548
552
|
const laravelSelected = detectLaravelProject(projectPath);
|
|
@@ -555,30 +559,30 @@ async function main() {
|
|
|
555
559
|
: phpSelected
|
|
556
560
|
? phpSelected.defaultPort // 80
|
|
557
561
|
: detectPortFromPackage(selectedPackagePath);
|
|
558
|
-
|
|
562
|
+
|
|
559
563
|
// Check for running servers
|
|
560
564
|
const runningPorts = await detectRunningDevServer();
|
|
561
|
-
|
|
565
|
+
|
|
562
566
|
let initialPort = detectedPort || 5173;
|
|
563
567
|
if (runningPorts.length > 0 && !detectedPort) {
|
|
564
568
|
initialPort = runningPorts[0];
|
|
565
569
|
}
|
|
566
|
-
|
|
570
|
+
|
|
567
571
|
const portResponse = await prompts({
|
|
568
572
|
type: "number",
|
|
569
573
|
name: "port",
|
|
570
574
|
message: "Enter your dev server port:",
|
|
571
575
|
initial: initialPort
|
|
572
576
|
});
|
|
573
|
-
|
|
577
|
+
|
|
574
578
|
if (!portResponse.port) {
|
|
575
579
|
console.log("ERROR: No port entered");
|
|
576
580
|
process.exit(1);
|
|
577
581
|
}
|
|
578
|
-
|
|
582
|
+
|
|
579
583
|
devPort = portResponse.port;
|
|
580
584
|
}
|
|
581
|
-
|
|
585
|
+
|
|
582
586
|
console.log("");
|
|
583
587
|
const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
|
|
584
588
|
|
|
@@ -608,7 +612,7 @@ async function main() {
|
|
|
608
612
|
stdio: "pipe",
|
|
609
613
|
shell: false
|
|
610
614
|
});
|
|
611
|
-
staticServerProcess.on("error", () => {});
|
|
615
|
+
staticServerProcess.on("error", () => { });
|
|
612
616
|
const ready = await waitForServerReady(devPort, 10000);
|
|
613
617
|
if (!ready) {
|
|
614
618
|
if (staticServerProcess) staticServerProcess.kill();
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta charset="UTF-8">
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
7
|
<meta name="description"
|
|
8
|
-
content="DevTunnel - Share local dev servers worldwide. Zero configuration tunnel for any framework. Install via npm: npm
|
|
8
|
+
content="DevTunnel - Share local dev servers worldwide. Zero configuration tunnel for any framework. Install via npm: npm i -g devtunnel-cli">
|
|
9
9
|
<meta name="keywords"
|
|
10
10
|
content="dev tunnel, localhost tunnel, cloudflare, ngrok, port forwarding, local development, vite, react, nextjs, laravel, php, html, npm install, devtunnel">
|
|
11
11
|
<meta name="author" content="maiz">
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<link rel="canonical" href="https://devtunnel-cli.vercel.app/">
|
|
14
14
|
<meta property="og:title" content="DevTunnel - Share Local Servers Worldwide">
|
|
15
15
|
<meta property="og:description"
|
|
16
|
-
content="Zero configuration tunnel for any framework. Install via npm: npm
|
|
16
|
+
content="Zero configuration tunnel for any framework. Install via npm: npm i -g devtunnel-cli">
|
|
17
17
|
<meta property="og:url" content="https://devtunnel-cli.vercel.app/">
|
|
18
18
|
<meta property="og:type" content="website">
|
|
19
19
|
<meta property="og:image" content="https://devtunnel-cli.vercel.app/og-image.png">
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
|
|
84
84
|
<h2>⚡ Quick Start</h2>
|
|
85
85
|
<h3>Install via npm (Recommended)</h3>
|
|
86
|
-
<pre><code>npm
|
|
86
|
+
<pre><code>npm i -g devtunnel-cli
|
|
87
87
|
devtunnel-cli</code></pre>
|
|
88
88
|
|
|
89
89
|
<h2>✨ Features</h2>
|
|
@@ -112,7 +112,7 @@ devtunnel-cli</code></pre>
|
|
|
112
112
|
</ul>
|
|
113
113
|
|
|
114
114
|
<hr>
|
|
115
|
-
<p><small>Version 3.0.
|
|
115
|
+
<p><small>Version 3.0.40 | Made with ❤︎ for developers worldwide</small></p>
|
|
116
116
|
</body>
|
|
117
117
|
|
|
118
118
|
</html>
|