devtunnel-cli 3.0.39 ā 3.0.41
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 -7
- package/package.json +5 -3
- package/src/core/setup-cloudflared.js +37 -37
- package/src/core/start.js +2 -2
- package/src/utils/pages/index.html +18 -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
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devtunnel-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.41",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DevTunnel-CLI ā fast, zero-config tool to share local servers for development, testing, demos, and webhook debugging. npm
|
|
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",
|
|
@@ -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,10 +17,10 @@ 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.41";
|
|
21
21
|
}
|
|
22
22
|
} catch (err) { }
|
|
23
|
-
return "3.0.
|
|
23
|
+
return "3.0.41";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// Helper to run command
|
|
@@ -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,21 @@ devtunnel-cli</code></pre>
|
|
|
112
112
|
</ul>
|
|
113
113
|
|
|
114
114
|
<hr>
|
|
115
|
-
<p><small>Version 3.0.
|
|
115
|
+
<p><small>Version <span id="pkg-version">3.0.41</span> | Made with ā¤ļø for developers worldwide</small></p>
|
|
116
|
+
|
|
117
|
+
<script>
|
|
118
|
+
(async function () {
|
|
119
|
+
try {
|
|
120
|
+
const res = await fetch('https://registry.npmjs.org/devtunnel-cli');
|
|
121
|
+
if (!res.ok) return;
|
|
122
|
+
const data = await res.json();
|
|
123
|
+
const latest = data && data['dist-tags'] && data['dist-tags'].latest;
|
|
124
|
+
if (latest) document.getElementById('pkg-version').textContent = latest;
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// keep static version if fetch fails
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
</script>
|
|
116
130
|
</body>
|
|
117
131
|
|
|
118
132
|
</html>
|