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 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
- **This tool is NOT intended for:**
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 install -g devtunnel-cli
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 install -g devtunnel-cli`
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.38",
3
+ "version": "3.0.40",
4
4
  "type": "module",
5
- "description": "DevTunnel-CLI (DevTunnel, devtunnel, dev-tunnel) - Development tool for sharing local servers worldwide. Zero config, supports multiple ports. For dev, testing, demos, webhook debugging. Not for production. npm install -g devtunnel-cli. Works with Vite, React, Laravel, HTML, PHP/XAMPP.",
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
- console.log(' 1. Run with sudo: sudo npm install -g devtunnel-cli');
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.38";
20
+ return pkg.version || "3.0.40";
21
21
  }
22
- } catch (err) {}
23
- return "3.0.38";
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 install -g devtunnel-cli">
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 install -g devtunnel-cli">
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 install -g devtunnel-cli
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.38 | Made with ❤︎ for developers worldwide</small></p>
115
+ <p><small>Version 3.0.40 | Made with ❤︎ for developers worldwide</small></p>
116
116
  </body>
117
117
 
118
118
  </html>