devtunnel-cli 3.0.16 → 3.0.18

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/src/core/start.js CHANGED
@@ -1,573 +1,602 @@
1
- import { spawn } from "child_process";
2
- import { existsSync, readFileSync } from "fs";
3
- import { join, dirname, basename } from "path";
4
- import { fileURLToPath } from "url";
5
- import http from "http";
6
- import prompts from "prompts";
7
- import { selectFolder } from "../utils/folder-picker.js";
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = dirname(__filename);
11
-
12
- // Get project root directory dynamically (two levels up from src/core/)
13
- const PROJECT_ROOT = dirname(dirname(__dirname));
14
-
15
- // Helper to run command
16
- function runCommand(command, args = [], cwd = process.cwd()) {
17
- return new Promise((resolve) => {
18
- const proc = spawn(command, args, {
19
- shell: true,
20
- stdio: "pipe",
21
- cwd: cwd
22
- });
23
- let output = "";
24
-
25
- proc.stdout?.on("data", (data) => output += data.toString());
26
- proc.stderr?.on("data", (data) => output += data.toString());
27
-
28
- proc.on("close", (code) => resolve({ code, output }));
29
- proc.on("error", () => resolve({ code: 1, output: "" }));
30
- });
31
- }
32
-
33
- // Check if command exists
34
- async function commandExists(command) {
35
- const result = await runCommand("where", [command]);
36
- return result.code === 0;
37
- }
38
-
39
- // Check if a port is in use (dev server running)
40
- function checkPortInUse(port) {
41
- return new Promise((resolve) => {
42
- const server = http.createServer();
43
-
44
- server.once('error', (err) => {
45
- // Port is in use
46
- if (err.code === 'EADDRINUSE') {
47
- resolve(true);
48
- } else {
49
- resolve(false);
50
- }
51
- });
52
-
53
- server.listen(port, () => {
54
- // Port is available (not in use)
55
- server.once('close', () => resolve(false));
56
- server.close();
57
- });
58
- });
59
- }
60
-
61
- // Detect port from package.json
62
- function detectPortFromPackage(packagePath) {
63
- try {
64
- if (!existsSync(packagePath)) return null;
65
- const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
66
- const scripts = packageJson.scripts || {};
67
-
68
- // Check for common dev commands
69
- const devScript = scripts.dev || scripts.start || scripts.serve;
70
- if (!devScript) return null;
71
-
72
- // Try to extract port from script
73
- const portMatch = devScript.match(/--port\s+(\d+)|:(\d+)|port[=:](\d+)/i);
74
- if (portMatch) {
75
- return parseInt(portMatch[1] || portMatch[2] || portMatch[3]);
76
- }
77
-
78
- // Default ports based on framework
79
- if (devScript.includes('vite')) return 5173;
80
- if (devScript.includes('next')) return 3000;
81
- if (devScript.includes('react-scripts')) return 3000;
82
- if (devScript.includes('webpack')) return 8080;
83
- if (devScript.includes('express')) return 3000;
84
-
85
- return null;
86
- } catch (err) {
87
- return null;
88
- }
89
- }
90
-
91
- // Detect Laravel/PHP project (composer.json + artisan)
92
- function detectLaravelProject(currentDir) {
93
- const composerPath = join(currentDir, "composer.json");
94
- const artisanPath = join(currentDir, "artisan");
95
- if (!existsSync(composerPath) || !existsSync(artisanPath)) return null;
96
- try {
97
- const composerJson = JSON.parse(readFileSync(composerPath, "utf8"));
98
- const projectName = (composerJson.name && composerJson.name.replace(/^laravel\//i, "")) || basename(currentDir);
99
- return { name: projectName, defaultPort: 8000 }; // php artisan serve
100
- } catch (err) {
101
- return null;
102
- }
103
- }
104
-
105
- // Detect plain HTML project (index.html in root)
106
- function detectHtmlProject(currentDir) {
107
- const indexPath = join(currentDir, "index.html");
108
- if (!existsSync(indexPath)) return null;
109
- return { name: basename(currentDir), defaultPort: 8080 }; // common for HTML/Live Server/XAMPP
110
- }
111
-
112
- // Check common ports for running dev servers (includes Laravel 8000, XAMPP/Live Server 8080/5500)
113
- async function detectRunningDevServer() {
114
- const commonPorts = [3000, 5173, 8080, 8000, 5000, 4000, 5500, 3001, 5174];
115
- const detected = [];
116
-
117
- for (const port of commonPorts) {
118
- const inUse = await checkPortInUse(port);
119
- if (inUse) {
120
- // Try to verify it's actually a dev server by making a request
121
- try {
122
- const response = await new Promise((resolve) => {
123
- const req = http.get(`http://localhost:${port}`, { timeout: 2000 }, (res) => {
124
- resolve(res.statusCode);
125
- });
126
- req.on('error', () => resolve(null));
127
- req.on('timeout', () => {
128
- req.destroy();
129
- resolve(null);
130
- });
131
- });
132
- // If we get any HTTP response, it's likely a dev server
133
- if (response !== null) {
134
- detected.push(port);
135
- }
136
- } catch (err) {
137
- // Port is in use, add it anyway (might be a dev server)
138
- detected.push(port);
139
- }
140
- }
141
- }
142
-
143
- return detected;
144
- }
145
-
146
- // Auto-detect project in current directory (Laravel/PHP first, then Node/npm, then HTML)
147
- async function autoDetectProject() {
148
- const currentDir = process.cwd();
149
- const packagePath = join(currentDir, "package.json");
150
- const runningPorts = await detectRunningDevServer();
151
-
152
- // 1) Laravel/PHP (composer.json + artisan) — default port 8000 (php artisan serve)
153
- const laravel = detectLaravelProject(currentDir);
154
- if (laravel) {
155
- const detectedPort = runningPorts.length > 0 ? runningPorts[0] : laravel.defaultPort;
156
- return {
157
- path: currentDir,
158
- name: laravel.name,
159
- port: detectedPort,
160
- projectType: "laravel"
161
- };
162
- }
163
-
164
- // 2) Node/npm (package.json)
165
- if (existsSync(packagePath)) {
166
- try {
167
- const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
168
- const projectName = packageJson.name || basename(currentDir);
169
- const detectedPort =
170
- runningPorts.length > 0 ? runningPorts[0] : detectPortFromPackage(packagePath);
171
- return {
172
- path: currentDir,
173
- name: projectName,
174
- port: detectedPort,
175
- projectType: "node"
176
- };
177
- } catch (err) {
178
- // fall through to HTML check
179
- }
180
- }
181
-
182
- // 3) Plain HTML (index.html) — default port 8080 (Live Server, XAMPP, etc.)
183
- const html = detectHtmlProject(currentDir);
184
- if (html) {
185
- const detectedPort = runningPorts.length > 0 ? runningPorts[0] : html.defaultPort;
186
- return {
187
- path: currentDir,
188
- name: html.name,
189
- port: detectedPort,
190
- projectType: "html"
191
- };
192
- }
193
-
194
- return null;
195
- }
196
-
197
- // ASCII Logo - Compatible with all OS and terminals
198
- function showLogo() {
199
- console.log("");
200
- console.log(" ██████████ ███████████ ████ ");
201
- console.log("▒▒███▒▒▒▒███ ▒█▒▒▒███▒▒▒█ ▒▒███ ");
202
- console.log(" ▒███ ▒▒███ ██████ █████ █████▒ ▒███ ▒ █████ ████ ████████ ████████ ██████ ▒███ ");
203
- console.log(" ▒███ ▒███ ███▒▒███▒▒███ ▒▒███ ▒███ ▒▒███ ▒███ ▒▒███▒▒███ ▒▒███▒▒███ ███▒▒███ ▒███ ");
204
- console.log(" ▒███ ▒███▒███████ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███████ ▒███ ");
205
- console.log(" ▒███ ███ ▒███▒▒▒ ▒▒███ ███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███▒▒▒ ▒███ ");
206
- console.log(" ██████████ ▒▒██████ ▒▒█████ █████ ▒▒████████ ████ █████ ████ █████▒▒██████ █████");
207
- console.log("▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ");
208
- console.log(" ");
209
- console.log(" ");
210
- console.log("");
211
- }
212
-
213
- async function main() {
214
- // Clear screen - works on Windows, macOS, Linux
215
- // ANSI escape codes for clear screen + cursor to top
216
- process.stdout.write('\x1B[2J\x1B[0f');
217
- console.clear(); // Fallback for terminals that don't support ANSI
218
-
219
- // Show ASCII logo
220
- showLogo();
221
-
222
- console.log("DevTunnel v3.0.15");
223
- console.log("Share your local dev servers worldwide");
224
- console.log("");
225
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
226
- console.log("Repository: https://github.com/maiz-an/DevTunnel");
227
- console.log("npm Package: https://www.npmjs.com/package/devtunnel");
228
- console.log("Website: https://devtunnel.vercel.app");
229
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
230
- console.log("");
231
-
232
- // Step 1: Check Node.js
233
- console.log("[1/4] Checking Node.js...");
234
- if (!await commandExists("node")) {
235
- console.log("ERROR: Node.js not found!");
236
- console.log("Install from: https://nodejs.org/");
237
- process.exit(1);
238
- }
239
- console.log("SUCCESS: Node.js installed");
240
- console.log("");
241
-
242
- // Step 2: Check Cloudflare (bundled or system-installed)
243
- console.log("[2/4] Checking Cloudflare...");
244
-
245
- // Import bundled cloudflared helpers
246
- const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
247
-
248
- let cloudflareAvailable = false;
249
-
250
- if (hasBundledCloudflared()) {
251
- console.log("SUCCESS: Using bundled Cloudflare (no install needed)");
252
- cloudflareAvailable = true;
253
- } else if (await commandExists("cloudflared")) {
254
- console.log("SUCCESS: Cloudflare installed on system");
255
- cloudflareAvailable = true;
256
- } else {
257
- console.log("First time setup - Downloading Cloudflare...");
258
- console.log("This only happens once (~40MB, 10-30 seconds)");
259
- console.log("");
260
-
261
- try {
262
- const bundledPath = await setupCloudflared();
263
-
264
- if (bundledPath) {
265
- console.log("SUCCESS: Cloudflare ready to use");
266
- cloudflareAvailable = true;
267
- } else {
268
- console.log("Could not download Cloudflare");
269
- console.log("Will use alternative tunnel services");
270
- console.log("");
271
- }
272
- } catch (err) {
273
- console.log(`Setup error: ${err.message}`);
274
- console.log("Will use alternative tunnel services");
275
- console.log("");
276
- }
277
- }
278
-
279
- // Show what's available
280
- if (!cloudflareAvailable) {
281
- console.log("DevTunnel has multi-service fallback:");
282
- console.log(" Cloudflare (fastest, no password)");
283
- console.log(" Ngrok (fast alternative)");
284
- console.log(" LocalTunnel (backup option)");
285
- console.log("");
286
- }
287
-
288
- // Step 3: Check dependencies
289
- console.log("[3/4] Checking dependencies...");
290
- const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
291
- if (!existsSync(nodeModulesPath)) {
292
- console.log("Installing dependencies...");
293
- console.log("");
294
- // Run npm install in the project root directory
295
- const result = await runCommand("npm", ["install"], PROJECT_ROOT);
296
- if (result.code !== 0) {
297
- console.log("");
298
- console.log("ERROR: npm install failed");
299
- process.exit(1);
300
- }
301
- console.log("");
302
- console.log("SUCCESS: Dependencies installed");
303
- } else {
304
- console.log("SUCCESS: Dependencies already installed");
305
- }
306
- console.log("");
307
-
308
- // Step 4: Auto-detect or select project
309
- console.log("[4/4] Detecting project...");
310
-
311
- let projectPath, projectName, devPort;
312
-
313
- // Try to auto-detect project in current directory
314
- const autoDetected = await autoDetectProject();
315
-
316
- if (autoDetected && autoDetected.port) {
317
- // Auto-detected project with port
318
- projectPath = autoDetected.path;
319
- projectName = autoDetected.name;
320
-
321
- // Double-check: verify the port is actually in use
322
- const portInUse = await checkPortInUse(autoDetected.port);
323
-
324
- if (!portInUse) {
325
- // Detected port is not actually running, check for other running servers
326
- const portSource =
327
- autoDetected.projectType === "laravel"
328
- ? "Laravel (php artisan serve)"
329
- : autoDetected.projectType === "html"
330
- ? "HTML project"
331
- : "package.json";
332
- console.log(`Detected port ${autoDetected.port} (${portSource}), but no server running on that port`);
333
- console.log("Checking for running dev servers...");
334
-
335
- const runningPorts = await detectRunningDevServer();
336
- if (runningPorts.length > 0) {
337
- if (runningPorts.length === 1) {
338
- devPort = runningPorts[0];
339
- console.log(`Found running dev server on port: ${devPort}`);
340
- } else {
341
- console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
342
- const portResponse = await prompts({
343
- type: "select",
344
- name: "port",
345
- message: "Select port:",
346
- choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
347
- });
348
-
349
- if (!portResponse.port) {
350
- console.log("ERROR: No port selected");
351
- process.exit(1);
352
- }
353
-
354
- devPort = portResponse.port;
355
- }
356
- } else {
357
- // No running servers, use detected port (user might start it later)
358
- devPort = autoDetected.port;
359
- console.log(`Using detected port: ${devPort} (make sure dev server is running)`);
360
- }
361
- } else {
362
- // Port is in use, use it
363
- devPort = autoDetected.port;
364
- }
365
-
366
- console.log(`Detected project: ${projectName}`);
367
- console.log(`Using port: ${devPort}`);
368
- console.log(`Using current directory: ${projectPath}`);
369
- console.log("");
370
-
371
- // Confirm with user
372
- const confirm = await prompts({
373
- type: "confirm",
374
- name: "value",
375
- message: "Use detected project?",
376
- initial: true
377
- });
378
-
379
- if (!confirm.value) {
380
- // User wants to select manually
381
- console.log("");
382
- console.log("Selecting project manually...");
383
- console.log("");
384
-
385
- const selectedPath = await selectFolder();
386
- if (!selectedPath || selectedPath.length === 0) {
387
- console.log("ERROR: No folder selected");
388
- process.exit(1);
389
- }
390
-
391
- projectPath = selectedPath;
392
- projectName = basename(selectedPath);
393
-
394
- // Try to detect port for selected project (Laravel → 8000, Node from package.json, else 5173)
395
- const selectedPackagePath = join(selectedPath, "package.json");
396
- const laravelSelected = detectLaravelProject(selectedPath);
397
- const detectedPort = laravelSelected
398
- ? laravelSelected.defaultPort
399
- : detectPortFromPackage(selectedPackagePath);
400
-
401
- const portResponse = await prompts({
402
- type: "number",
403
- name: "port",
404
- message: "Enter your dev server port:",
405
- initial: detectedPort || 5173
406
- });
407
-
408
- if (!portResponse.port) {
409
- console.log("ERROR: No port entered");
410
- process.exit(1);
411
- }
412
-
413
- devPort = portResponse.port;
414
- }
415
- } else if (autoDetected && !autoDetected.port) {
416
- // Project detected but no port
417
- projectPath = autoDetected.path;
418
- projectName = autoDetected.name;
419
-
420
- console.log(`Detected project: ${projectName}`);
421
- console.log(`Using current directory: ${projectPath}`);
422
- console.log("Checking for running dev servers...");
423
-
424
- const runningPorts = await detectRunningDevServer();
425
-
426
- if (runningPorts.length > 0) {
427
- console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
428
-
429
- if (runningPorts.length === 1) {
430
- devPort = runningPorts[0];
431
- console.log(`Using port: ${devPort}`);
432
- } else {
433
- // Multiple ports detected, let user choose
434
- const portResponse = await prompts({
435
- type: "select",
436
- name: "port",
437
- message: "Select port:",
438
- choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
439
- });
440
-
441
- if (!portResponse.port) {
442
- console.log("ERROR: No port selected");
443
- process.exit(1);
444
- }
445
-
446
- devPort = portResponse.port;
447
- }
448
- } else {
449
- // No running server, ask for port
450
- const portResponse = await prompts({
451
- type: "number",
452
- name: "port",
453
- message: "Enter your dev server port:",
454
- initial: 5173
455
- });
456
-
457
- if (!portResponse.port) {
458
- console.log("ERROR: No port entered");
459
- process.exit(1);
460
- }
461
-
462
- devPort = portResponse.port;
463
- }
464
-
465
- console.log("");
466
- } else {
467
- // No auto-detection, use folder picker
468
- console.log("No project detected in current directory");
469
- console.log("Opening folder picker...");
470
- console.log("");
471
-
472
- projectPath = await selectFolder();
473
-
474
- if (!projectPath || projectPath.length === 0) {
475
- console.log("ERROR: No folder selected");
476
- process.exit(1);
477
- }
478
-
479
- projectName = basename(projectPath);
480
- console.log(`Selected: ${projectPath}`);
481
- console.log("");
482
-
483
- // Try to detect port for selected project (Laravel → 8000, HTML → 8080, Node from package.json)
484
- const selectedPackagePath = join(projectPath, "package.json");
485
- const laravelSelected = detectLaravelProject(projectPath);
486
- const htmlSelected = detectHtmlProject(projectPath);
487
- let detectedPort = laravelSelected
488
- ? laravelSelected.defaultPort
489
- : htmlSelected
490
- ? htmlSelected.defaultPort
491
- : detectPortFromPackage(selectedPackagePath);
492
-
493
- // Check for running servers
494
- const runningPorts = await detectRunningDevServer();
495
-
496
- let initialPort = detectedPort || 5173;
497
- if (runningPorts.length > 0 && !detectedPort) {
498
- initialPort = runningPorts[0];
499
- }
500
-
501
- const portResponse = await prompts({
502
- type: "number",
503
- name: "port",
504
- message: "Enter your dev server port:",
505
- initial: initialPort
506
- });
507
-
508
- if (!portResponse.port) {
509
- console.log("ERROR: No port entered");
510
- process.exit(1);
511
- }
512
-
513
- devPort = portResponse.port;
514
- }
515
-
516
- console.log("");
517
- const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
518
-
519
- console.log("");
520
- console.log("Configuration:");
521
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
522
- console.log(`Project: ${projectName}`);
523
- console.log(`Dev Server: localhost:${devPort}`);
524
- console.log(`Proxy Port: ${proxyPort}`);
525
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
526
- console.log("");
527
-
528
- // Start proxy server
529
- console.log("Starting services...");
530
- console.log("");
531
- const proxyPath = join(__dirname, "proxy-server.js");
532
- const proxyProcess = spawn("node", [proxyPath, devPort.toString(), proxyPort.toString(), projectName], {
533
- stdio: "inherit",
534
- shell: false
535
- });
536
-
537
- // Wait for proxy to start
538
- await new Promise(resolve => setTimeout(resolve, 2000));
539
-
540
- // Run main tunnel app (connects to proxy port)
541
- // Use shell: false to properly handle paths with spaces
542
- const indexPath = join(__dirname, "index.js");
543
- const tunnelProcess = spawn("node", [indexPath, proxyPort.toString(), projectName, projectPath], {
544
- stdio: "inherit",
545
- shell: false
546
- });
547
-
548
- // Handle cleanup
549
- const cleanup = () => {
550
- console.log("\nShutting down...");
551
- proxyProcess.kill();
552
- tunnelProcess.kill();
553
- process.exit(0);
554
- };
555
-
556
- tunnelProcess.on("close", (code) => {
557
- cleanup();
558
- });
559
-
560
- proxyProcess.on("close", () => {
561
- cleanup();
562
- });
563
-
564
- // Handle Ctrl+C
565
- process.on("SIGINT", cleanup);
566
- process.on("SIGTERM", cleanup);
567
- }
568
-
569
- // Run
570
- main().catch((error) => {
571
- console.error("\nERROR:", error.message);
572
- process.exit(1);
573
- });
1
+ import { spawn } from "child_process";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { join, dirname, basename } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import http from "http";
6
+ import prompts from "prompts";
7
+ import { selectFolder } from "../utils/folder-picker.js";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ // Get project root directory dynamically (two levels up from src/core/)
13
+ const PROJECT_ROOT = dirname(dirname(__dirname));
14
+
15
+ function getPackageVersion() {
16
+ try {
17
+ const pkgPath = join(PROJECT_ROOT, "package.json");
18
+ if (existsSync(pkgPath)) {
19
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
20
+ return pkg.version || "3.0.17";
21
+ }
22
+ } catch (err) {}
23
+ return "3.0.17";
24
+ }
25
+
26
+ // Helper to run command
27
+ function runCommand(command, args = [], cwd = process.cwd()) {
28
+ return new Promise((resolve) => {
29
+ const proc = spawn(command, args, {
30
+ shell: true,
31
+ stdio: "pipe",
32
+ cwd: cwd
33
+ });
34
+ let output = "";
35
+
36
+ proc.stdout?.on("data", (data) => output += data.toString());
37
+ proc.stderr?.on("data", (data) => output += data.toString());
38
+
39
+ proc.on("close", (code) => resolve({ code, output }));
40
+ proc.on("error", () => resolve({ code: 1, output: "" }));
41
+ });
42
+ }
43
+
44
+ // Check if command exists
45
+ async function commandExists(command) {
46
+ const result = await runCommand("where", [command]);
47
+ return result.code === 0;
48
+ }
49
+
50
+ // Check if a port is in use (dev server running)
51
+ function checkPortInUse(port) {
52
+ return new Promise((resolve) => {
53
+ const server = http.createServer();
54
+
55
+ server.once('error', (err) => {
56
+ // Port is in use
57
+ if (err.code === 'EADDRINUSE') {
58
+ resolve(true);
59
+ } else {
60
+ resolve(false);
61
+ }
62
+ });
63
+
64
+ server.listen(port, () => {
65
+ // Port is available (not in use)
66
+ server.once('close', () => resolve(false));
67
+ server.close();
68
+ });
69
+ });
70
+ }
71
+
72
+ // Detect port from package.json
73
+ function detectPortFromPackage(packagePath) {
74
+ try {
75
+ if (!existsSync(packagePath)) return null;
76
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
77
+ const scripts = packageJson.scripts || {};
78
+
79
+ // Check for common dev commands
80
+ const devScript = scripts.dev || scripts.start || scripts.serve;
81
+ if (!devScript) return null;
82
+
83
+ // Try to extract port from script
84
+ const portMatch = devScript.match(/--port\s+(\d+)|:(\d+)|port[=:](\d+)/i);
85
+ if (portMatch) {
86
+ return parseInt(portMatch[1] || portMatch[2] || portMatch[3]);
87
+ }
88
+
89
+ // Default ports based on framework
90
+ if (devScript.includes('vite')) return 5173;
91
+ if (devScript.includes('next')) return 3000;
92
+ if (devScript.includes('react-scripts')) return 3000;
93
+ if (devScript.includes('webpack')) return 8080;
94
+ if (devScript.includes('express')) return 3000;
95
+
96
+ return null;
97
+ } catch (err) {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ // Detect Laravel/PHP project (composer.json + artisan)
103
+ function detectLaravelProject(currentDir) {
104
+ const composerPath = join(currentDir, "composer.json");
105
+ const artisanPath = join(currentDir, "artisan");
106
+ if (!existsSync(composerPath) || !existsSync(artisanPath)) return null;
107
+ try {
108
+ const composerJson = JSON.parse(readFileSync(composerPath, "utf8"));
109
+ const projectName = (composerJson.name && composerJson.name.replace(/^laravel\//i, "")) || basename(currentDir);
110
+ return { name: projectName, defaultPort: 8000 }; // php artisan serve
111
+ } catch (err) {
112
+ return null;
113
+ }
114
+ }
115
+
116
+ // Detect plain HTML project (index.html in root)
117
+ function detectHtmlProject(currentDir) {
118
+ const indexPath = join(currentDir, "index.html");
119
+ if (!existsSync(indexPath)) return null;
120
+ return { name: basename(currentDir), defaultPort: 8080 }; // common for HTML/Live Server/XAMPP
121
+ }
122
+
123
+ // Check common ports for running dev servers (includes Laravel 8000, XAMPP/Live Server 8080/5500)
124
+ async function detectRunningDevServer() {
125
+ const commonPorts = [3000, 5173, 8080, 8000, 5000, 4000, 5500, 3001, 5174];
126
+ const detected = [];
127
+
128
+ for (const port of commonPorts) {
129
+ const inUse = await checkPortInUse(port);
130
+ if (inUse) {
131
+ // Try to verify it's actually a dev server by making a request
132
+ try {
133
+ const response = await new Promise((resolve) => {
134
+ const req = http.get(`http://localhost:${port}`, { timeout: 2000 }, (res) => {
135
+ resolve(res.statusCode);
136
+ });
137
+ req.on('error', () => resolve(null));
138
+ req.on('timeout', () => {
139
+ req.destroy();
140
+ resolve(null);
141
+ });
142
+ });
143
+ // If we get any HTTP response, it's likely a dev server
144
+ if (response !== null) {
145
+ detected.push(port);
146
+ }
147
+ } catch (err) {
148
+ // Port is in use, add it anyway (might be a dev server)
149
+ detected.push(port);
150
+ }
151
+ }
152
+ }
153
+
154
+ return detected;
155
+ }
156
+
157
+ // Auto-detect project in current directory (Laravel/PHP first, then Node/npm, then HTML)
158
+ async function autoDetectProject() {
159
+ const currentDir = process.cwd();
160
+ const packagePath = join(currentDir, "package.json");
161
+ const runningPorts = await detectRunningDevServer();
162
+
163
+ // 1) Laravel/PHP (composer.json + artisan) — default port 8000 (php artisan serve)
164
+ const laravel = detectLaravelProject(currentDir);
165
+ if (laravel) {
166
+ const detectedPort = runningPorts.length > 0 ? runningPorts[0] : laravel.defaultPort;
167
+ return {
168
+ path: currentDir,
169
+ name: laravel.name,
170
+ port: detectedPort,
171
+ projectType: "laravel"
172
+ };
173
+ }
174
+
175
+ // 2) Node/npm (package.json)
176
+ if (existsSync(packagePath)) {
177
+ try {
178
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
179
+ const projectName = packageJson.name || basename(currentDir);
180
+ const detectedPort =
181
+ runningPorts.length > 0 ? runningPorts[0] : detectPortFromPackage(packagePath);
182
+ return {
183
+ path: currentDir,
184
+ name: projectName,
185
+ port: detectedPort,
186
+ projectType: "node"
187
+ };
188
+ } catch (err) {
189
+ // fall through to HTML check
190
+ }
191
+ }
192
+
193
+ // 3) Plain HTML (index.html) — default port 8080 (Live Server, XAMPP, etc.)
194
+ const html = detectHtmlProject(currentDir);
195
+ if (html) {
196
+ const detectedPort = runningPorts.length > 0 ? runningPorts[0] : html.defaultPort;
197
+ return {
198
+ path: currentDir,
199
+ name: html.name,
200
+ port: detectedPort,
201
+ projectType: "html"
202
+ };
203
+ }
204
+
205
+ return null;
206
+ }
207
+
208
+ // ASCII Logo - Compatible with all OS and terminals
209
+ function showLogo() {
210
+ console.log("");
211
+ console.log(" ██████████ ███████████ ████ ");
212
+ console.log("▒▒███▒▒▒▒███ ▒█▒▒▒███▒▒▒█ ▒▒███ ");
213
+ console.log(" ▒███ ▒▒███ ██████ █████ █████▒ ▒███ ▒ █████ ████ ████████ ████████ ██████ ▒███ ");
214
+ console.log(" ▒███ ▒███ ███▒▒███▒▒███ ▒▒███ ▒███ ▒▒███ ▒███ ▒▒███▒▒███ ▒▒███▒▒███ ███▒▒███ ▒███ ");
215
+ console.log(" ▒███ ▒███▒███████ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███████ ▒███ ");
216
+ console.log(" ▒███ ███ ▒███▒▒▒ ▒▒███ ███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███▒▒▒ ▒███ ");
217
+ console.log(" ██████████ ▒▒██████ ▒▒█████ █████ ▒▒████████ ████ █████ ████ █████▒▒██████ █████");
218
+ console.log("▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ");
219
+ console.log(" ");
220
+ console.log(" ");
221
+ console.log("");
222
+ }
223
+
224
+ async function main() {
225
+ // Clear screen - works on Windows, macOS, Linux
226
+ // ANSI escape codes for clear screen + cursor to top
227
+ process.stdout.write('\x1B[2J\x1B[0f');
228
+ console.clear(); // Fallback for terminals that don't support ANSI
229
+
230
+ // Show ASCII logo
231
+ showLogo();
232
+
233
+ console.log(`DevTunnel v${getPackageVersion()}`);
234
+ console.log("Share your local dev servers worldwide");
235
+ console.log("");
236
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
237
+ console.log("Repository: https://github.com/maiz-an/DevTunnel");
238
+ console.log("npm Package: https://www.npmjs.com/package/devtunnel");
239
+ console.log("Website: https://devtunnel.vercel.app");
240
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
241
+ console.log("");
242
+
243
+ // Step 1: Check Node.js
244
+ console.log("[1/4] Checking Node.js...");
245
+ if (!await commandExists("node")) {
246
+ console.log("ERROR: Node.js not found!");
247
+ console.log("Install from: https://nodejs.org/");
248
+ process.exit(1);
249
+ }
250
+ console.log("SUCCESS: Node.js installed");
251
+ console.log("");
252
+
253
+ // Step 2: Check Cloudflare (bundled or system-installed)
254
+ console.log("[2/4] Checking Cloudflare...");
255
+
256
+ // Import bundled cloudflared helpers
257
+ const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
258
+
259
+ let cloudflareAvailable = false;
260
+
261
+ if (hasBundledCloudflared()) {
262
+ console.log("SUCCESS: Using bundled Cloudflare (no install needed)");
263
+ cloudflareAvailable = true;
264
+ } else if (await commandExists("cloudflared")) {
265
+ console.log("SUCCESS: Cloudflare installed on system");
266
+ cloudflareAvailable = true;
267
+ } else {
268
+ console.log("First time setup - Downloading Cloudflare...");
269
+ console.log("This only happens once (~40MB, 10-30 seconds)");
270
+ console.log("");
271
+
272
+ try {
273
+ const bundledPath = await setupCloudflared();
274
+
275
+ if (bundledPath) {
276
+ console.log("SUCCESS: Cloudflare ready to use");
277
+ cloudflareAvailable = true;
278
+ } else {
279
+ console.log("Could not download Cloudflare");
280
+ console.log("Will use alternative tunnel services");
281
+ console.log("");
282
+ }
283
+ } catch (err) {
284
+ console.log(`Setup error: ${err.message}`);
285
+ console.log("Will use alternative tunnel services");
286
+ console.log("");
287
+ }
288
+ }
289
+
290
+ // Show what's available
291
+ if (!cloudflareAvailable) {
292
+ console.log("DevTunnel has multi-service fallback:");
293
+ console.log(" Cloudflare (fastest, no password)");
294
+ console.log(" Ngrok (fast alternative)");
295
+ console.log(" LocalTunnel (backup option)");
296
+ console.log("");
297
+ }
298
+
299
+ // Step 3: Check dependencies
300
+ console.log("[3/4] Checking dependencies...");
301
+ const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
302
+ if (!existsSync(nodeModulesPath)) {
303
+ console.log("Installing dependencies...");
304
+ console.log("");
305
+ // Run npm install in the project root directory
306
+ const result = await runCommand("npm", ["install"], PROJECT_ROOT);
307
+ if (result.code !== 0) {
308
+ console.log("");
309
+ console.log("ERROR: npm install failed");
310
+ process.exit(1);
311
+ }
312
+ console.log("");
313
+ console.log("SUCCESS: Dependencies installed");
314
+ } else {
315
+ console.log("SUCCESS: Dependencies already installed");
316
+ }
317
+ console.log("");
318
+
319
+ // Step 4: Auto-detect or select project
320
+ console.log("[4/4] Detecting project...");
321
+
322
+ let projectPath, projectName, devPort;
323
+
324
+ // Try to auto-detect project in current directory
325
+ const autoDetected = await autoDetectProject();
326
+
327
+ if (autoDetected && autoDetected.port) {
328
+ // Auto-detected project with port
329
+ projectPath = autoDetected.path;
330
+ projectName = autoDetected.name;
331
+
332
+ // Double-check: verify the port is actually in use
333
+ const portInUse = await checkPortInUse(autoDetected.port);
334
+
335
+ if (!portInUse) {
336
+ // Detected port is not actually running, check for other running servers
337
+ const portSource =
338
+ autoDetected.projectType === "laravel"
339
+ ? "Laravel (php artisan serve)"
340
+ : autoDetected.projectType === "html"
341
+ ? "HTML project"
342
+ : "package.json";
343
+ console.log(`Detected port ${autoDetected.port} (${portSource}), but no server running on that port`);
344
+ console.log("Checking for running dev servers...");
345
+
346
+ const runningPorts = await detectRunningDevServer();
347
+ if (runningPorts.length > 0) {
348
+ if (runningPorts.length === 1) {
349
+ devPort = runningPorts[0];
350
+ console.log(`Found running dev server on port: ${devPort}`);
351
+ } else {
352
+ console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
353
+ const portResponse = await prompts({
354
+ type: "select",
355
+ name: "port",
356
+ message: "Select port:",
357
+ choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
358
+ });
359
+
360
+ if (!portResponse.port) {
361
+ console.log("ERROR: No port selected");
362
+ process.exit(1);
363
+ }
364
+
365
+ devPort = portResponse.port;
366
+ }
367
+ } else {
368
+ // No running servers, use detected port (user might start it later)
369
+ devPort = autoDetected.port;
370
+ console.log(`Using detected port: ${devPort} (make sure dev server is running)`);
371
+ }
372
+ } else {
373
+ // Port is in use, use it
374
+ devPort = autoDetected.port;
375
+ }
376
+
377
+ console.log(`Detected project: ${projectName}`);
378
+ console.log(`Using port: ${devPort}`);
379
+ console.log(`Using current directory: ${projectPath}`);
380
+ console.log("");
381
+
382
+ // Confirm with user
383
+ const confirm = await prompts({
384
+ type: "confirm",
385
+ name: "value",
386
+ message: "Use detected project?",
387
+ initial: true
388
+ });
389
+
390
+ if (!confirm.value) {
391
+ // User wants to select manually
392
+ console.log("");
393
+ console.log("Selecting project manually...");
394
+ console.log("");
395
+
396
+ const selectedPath = await selectFolder();
397
+ if (!selectedPath || selectedPath.length === 0) {
398
+ console.log("ERROR: No folder selected");
399
+ process.exit(1);
400
+ }
401
+
402
+ projectPath = selectedPath;
403
+ projectName = basename(selectedPath);
404
+
405
+ // Try to detect port for selected project (Laravel → 8000, Node from package.json, else 5173)
406
+ const selectedPackagePath = join(selectedPath, "package.json");
407
+ const laravelSelected = detectLaravelProject(selectedPath);
408
+ const detectedPort = laravelSelected
409
+ ? laravelSelected.defaultPort
410
+ : detectPortFromPackage(selectedPackagePath);
411
+
412
+ const portResponse = await prompts({
413
+ type: "number",
414
+ name: "port",
415
+ message: "Enter your dev server port:",
416
+ initial: detectedPort || 5173
417
+ });
418
+
419
+ if (!portResponse.port) {
420
+ console.log("ERROR: No port entered");
421
+ process.exit(1);
422
+ }
423
+
424
+ devPort = portResponse.port;
425
+ }
426
+ } else if (autoDetected && !autoDetected.port) {
427
+ // Project detected but no port
428
+ projectPath = autoDetected.path;
429
+ projectName = autoDetected.name;
430
+
431
+ console.log(`Detected project: ${projectName}`);
432
+ console.log(`Using current directory: ${projectPath}`);
433
+ console.log("Checking for running dev servers...");
434
+
435
+ const runningPorts = await detectRunningDevServer();
436
+
437
+ if (runningPorts.length > 0) {
438
+ console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
439
+
440
+ if (runningPorts.length === 1) {
441
+ devPort = runningPorts[0];
442
+ console.log(`Using port: ${devPort}`);
443
+ } else {
444
+ // Multiple ports detected, let user choose
445
+ const portResponse = await prompts({
446
+ type: "select",
447
+ name: "port",
448
+ message: "Select port:",
449
+ choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
450
+ });
451
+
452
+ if (!portResponse.port) {
453
+ console.log("ERROR: No port selected");
454
+ process.exit(1);
455
+ }
456
+
457
+ devPort = portResponse.port;
458
+ }
459
+ } else {
460
+ // No running server, ask for port
461
+ const portResponse = await prompts({
462
+ type: "number",
463
+ name: "port",
464
+ message: "Enter your dev server port:",
465
+ initial: 5173
466
+ });
467
+
468
+ if (!portResponse.port) {
469
+ console.log("ERROR: No port entered");
470
+ process.exit(1);
471
+ }
472
+
473
+ devPort = portResponse.port;
474
+ }
475
+
476
+ console.log("");
477
+ } else {
478
+ // No auto-detection, use folder picker
479
+ console.log("No project detected in current directory");
480
+ console.log("Opening folder picker...");
481
+ console.log("");
482
+
483
+ projectPath = await selectFolder();
484
+
485
+ if (!projectPath || projectPath.length === 0) {
486
+ console.log("ERROR: No folder selected");
487
+ process.exit(1);
488
+ }
489
+
490
+ projectName = basename(projectPath);
491
+ console.log(`Selected: ${projectPath}`);
492
+ console.log("");
493
+
494
+ // Try to detect port for selected project (Laravel → 8000, HTML → 8080, Node from package.json)
495
+ const selectedPackagePath = join(projectPath, "package.json");
496
+ const laravelSelected = detectLaravelProject(projectPath);
497
+ const htmlSelected = detectHtmlProject(projectPath);
498
+ let detectedPort = laravelSelected
499
+ ? laravelSelected.defaultPort
500
+ : htmlSelected
501
+ ? htmlSelected.defaultPort
502
+ : detectPortFromPackage(selectedPackagePath);
503
+
504
+ // Check for running servers
505
+ const runningPorts = await detectRunningDevServer();
506
+
507
+ let initialPort = detectedPort || 5173;
508
+ if (runningPorts.length > 0 && !detectedPort) {
509
+ initialPort = runningPorts[0];
510
+ }
511
+
512
+ const portResponse = await prompts({
513
+ type: "number",
514
+ name: "port",
515
+ message: "Enter your dev server port:",
516
+ initial: initialPort
517
+ });
518
+
519
+ if (!portResponse.port) {
520
+ console.log("ERROR: No port entered");
521
+ process.exit(1);
522
+ }
523
+
524
+ devPort = portResponse.port;
525
+ }
526
+
527
+ console.log("");
528
+ const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
529
+
530
+ console.log("");
531
+ console.log("Configuration:");
532
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
533
+ console.log(`Project: ${projectName}`);
534
+ console.log(`Dev Server: localhost:${devPort}`);
535
+ console.log(`Proxy Port: ${proxyPort}`);
536
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
537
+ console.log("");
538
+
539
+ // For HTML projects with no server running: start built-in static server
540
+ let staticServerProcess = null;
541
+ const isHtmlProject = !!detectHtmlProject(projectPath);
542
+ const portInUseNow = await checkPortInUse(devPort);
543
+ if (isHtmlProject && !portInUseNow) {
544
+ console.log("Starting built-in static server for HTML project...");
545
+ const staticServerPath = join(__dirname, "static-server.js");
546
+ staticServerProcess = spawn("node", [staticServerPath, projectPath, devPort.toString()], {
547
+ stdio: "pipe",
548
+ shell: false
549
+ });
550
+ staticServerProcess.on("error", () => {});
551
+ await new Promise((r) => setTimeout(r, 1200));
552
+ console.log(`Static server running at http://localhost:${devPort}`);
553
+ console.log("");
554
+ }
555
+
556
+ // Start proxy server
557
+ console.log("Starting services...");
558
+ console.log("");
559
+ const proxyPath = join(__dirname, "proxy-server.js");
560
+ const proxyProcess = spawn("node", [proxyPath, devPort.toString(), proxyPort.toString(), projectName], {
561
+ stdio: "inherit",
562
+ shell: false
563
+ });
564
+
565
+ // Wait for proxy to start
566
+ await new Promise(resolve => setTimeout(resolve, 2000));
567
+
568
+ // Run main tunnel app (connects to proxy port)
569
+ // Use shell: false to properly handle paths with spaces
570
+ const indexPath = join(__dirname, "index.js");
571
+ const tunnelProcess = spawn("node", [indexPath, proxyPort.toString(), projectName, projectPath], {
572
+ stdio: "inherit",
573
+ shell: false
574
+ });
575
+
576
+ // Handle cleanup
577
+ const cleanup = () => {
578
+ console.log("\nShutting down...");
579
+ if (staticServerProcess) staticServerProcess.kill();
580
+ proxyProcess.kill();
581
+ tunnelProcess.kill();
582
+ process.exit(0);
583
+ };
584
+
585
+ tunnelProcess.on("close", (code) => {
586
+ cleanup();
587
+ });
588
+
589
+ proxyProcess.on("close", () => {
590
+ cleanup();
591
+ });
592
+
593
+ // Handle Ctrl+C
594
+ process.on("SIGINT", cleanup);
595
+ process.on("SIGTERM", cleanup);
596
+ }
597
+
598
+ // Run
599
+ main().catch((error) => {
600
+ console.error("\nERROR:", error.message);
601
+ process.exit(1);
602
+ });