devtunnel-cli 3.1.2 → 3.1.4

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,675 +1,762 @@
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.1.1";
21
- }
22
- } catch (err) { }
23
- return "3.1.1";
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
- // Poll until server at port responds (for HTML built-in static server)
73
- async function waitForServerReady(port, timeoutMs = 10000) {
74
- const start = Date.now();
75
- while (Date.now() - start < timeoutMs) {
76
- try {
77
- const code = await new Promise((resolve) => {
78
- const req = http.get(`http://127.0.0.1:${port}`, { timeout: 2000 }, (res) => resolve(res.statusCode));
79
- req.on("error", () => resolve(null));
80
- });
81
- if (code !== null && code >= 200 && code < 500) return true;
82
- } catch (err) { }
83
- await new Promise((r) => setTimeout(r, 300));
84
- }
85
- return false;
86
- }
87
-
88
- // Detect port from package.json
89
- function detectPortFromPackage(packagePath) {
90
- try {
91
- if (!existsSync(packagePath)) return null;
92
- const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
93
- const scripts = packageJson.scripts || {};
94
-
95
- // Check for common dev commands
96
- const devScript = scripts.dev || scripts.start || scripts.serve;
97
- if (!devScript) return null;
98
-
99
- // Try to extract port from script
100
- const portMatch = devScript.match(/--port\s+(\d+)|:(\d+)|port[=:](\d+)/i);
101
- if (portMatch) {
102
- return parseInt(portMatch[1] || portMatch[2] || portMatch[3]);
103
- }
104
-
105
- // Default ports based on framework
106
- if (devScript.includes('vite')) return 5173;
107
- if (devScript.includes('next')) return 3000;
108
- if (devScript.includes('react-scripts')) return 3000;
109
- if (devScript.includes('webpack')) return 8080;
110
- if (devScript.includes('express')) return 3000;
111
-
112
- return null;
113
- } catch (err) {
114
- return null;
115
- }
116
- }
117
-
118
- // Detect Laravel/PHP project (composer.json + artisan)
119
- function detectLaravelProject(currentDir) {
120
- const composerPath = join(currentDir, "composer.json");
121
- const artisanPath = join(currentDir, "artisan");
122
- if (!existsSync(composerPath) || !existsSync(artisanPath)) return null;
123
- try {
124
- const composerJson = JSON.parse(readFileSync(composerPath, "utf8"));
125
- const projectName = (composerJson.name && composerJson.name.replace(/^laravel\//i, "")) || basename(currentDir);
126
- return { name: projectName, defaultPort: 8000 }; // php artisan serve
127
- } catch (err) {
128
- return null;
129
- }
130
- }
131
-
132
- // Detect plain HTML project (index.html in root)
133
- function detectHtmlProject(currentDir) {
134
- const indexPath = join(currentDir, "index.html");
135
- if (!existsSync(indexPath)) return null;
136
- return { name: basename(currentDir), defaultPort: 5500 }; // Live Server default; matches VS Code
137
- }
138
-
139
- // Detect PHP/XAMPP project (index.php in root, not Laravel)
140
- function detectPhpProject(currentDir) {
141
- if (detectLaravelProject(currentDir)) return null; // Laravel has its own flow
142
- const indexPhp = join(currentDir, "index.php");
143
- if (!existsSync(indexPhp)) return null;
144
- return { name: basename(currentDir), defaultPort: 80 }; // XAMPP/Apache default
145
- }
146
-
147
- // Check common ports for running dev servers (includes Laravel 8000, XAMPP/Live Server 8080/5500)
148
- async function detectRunningDevServer() {
149
- const commonPorts = [3000, 5173, 5500, 8080, 8000, 80, 5000, 4000, 3001, 5174]; // 80 for XAMPP
150
- const detected = [];
151
-
152
- for (const port of commonPorts) {
153
- const inUse = await checkPortInUse(port);
154
- if (inUse) {
155
- // Try to verify it's actually a dev server by making a request
156
- try {
157
- const response = await new Promise((resolve) => {
158
- const req = http.get(`http://localhost:${port}`, { timeout: 2000 }, (res) => {
159
- resolve(res.statusCode);
160
- });
161
- req.on('error', () => resolve(null));
162
- req.on('timeout', () => {
163
- req.destroy();
164
- resolve(null);
165
- });
166
- });
167
- // If we get any HTTP response, it's likely a dev server
168
- if (response !== null) {
169
- detected.push(port);
170
- }
171
- } catch (err) {
172
- // Port is in use, add it anyway (might be a dev server)
173
- detected.push(port);
174
- }
175
- }
176
- }
177
-
178
- return detected;
179
- }
180
-
181
- // Auto-detect project in current directory (Laravel/PHP first, then Node/npm, then HTML)
182
- async function autoDetectProject() {
183
- const currentDir = process.cwd();
184
- const packagePath = join(currentDir, "package.json");
185
- const runningPorts = await detectRunningDevServer();
186
-
187
- // 1) Laravel/PHP (composer.json + artisan) — default port 8000 (php artisan serve)
188
- const laravel = detectLaravelProject(currentDir);
189
- if (laravel) {
190
- const detectedPort = runningPorts.length > 0 ? runningPorts[0] : laravel.defaultPort;
191
- return {
192
- path: currentDir,
193
- name: laravel.name,
194
- port: detectedPort,
195
- projectType: "laravel"
196
- };
197
- }
198
-
199
- // 2) Node/npm (package.json)
200
- if (existsSync(packagePath)) {
201
- try {
202
- const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
203
- const projectName = packageJson.name || basename(currentDir);
204
- const detectedPort =
205
- runningPorts.length > 0 ? runningPorts[0] : detectPortFromPackage(packagePath);
206
- return {
207
- path: currentDir,
208
- name: projectName,
209
- port: detectedPort,
210
- projectType: "node"
211
- };
212
- } catch (err) {
213
- // fall through to HTML check
214
- }
215
- }
216
-
217
- // 3) Plain HTML (index.html) — default port 5500 (Live Server), else built-in static server
218
- const html = detectHtmlProject(currentDir);
219
- if (html) {
220
- const detectedPort = runningPorts.length > 0 ? runningPorts[0] : html.defaultPort;
221
- return {
222
- path: currentDir,
223
- name: html.name,
224
- port: detectedPort,
225
- projectType: "html"
226
- };
227
- }
228
-
229
- // 4) PHP/XAMPP (index.php) — default port 80 (Apache), e.g. http://localhost/PeopleQ/
230
- const php = detectPhpProject(currentDir);
231
- if (php) {
232
- const detectedPort = runningPorts.length > 0 ? runningPorts[0] : php.defaultPort;
233
- return {
234
- path: currentDir,
235
- name: php.name,
236
- port: detectedPort,
237
- projectType: "php"
238
- };
239
- }
240
-
241
- return null;
242
- }
243
-
244
- // ASCII Logo - Compatible with all OS and terminals
245
- function showLogo() {
246
- console.log("");
247
- console.log(" ");
248
- console.log(" ");
249
- console.log("8888888b. 88888888888 888 .d8888b. 888 8888888 ");
250
- console.log('888 "Y88b 888 888 d88P Y88b 888 888 ');
251
- console.log("888 888 888 888 888 888 888 888 ");
252
- console.log("888 888 .d88b. 888 888 888 888 888 88888b. 88888b. .d88b. 888 888 888 888 ");
253
- console.log('888 888 d8P Y8b 888 888 888 888 888 888 "88b 888 "88b d8P Y8b 888 888 888 888 ');
254
- console.log("888 888 88888888 Y88 88P 888 888 888 888 888 888 888 88888888 888 888888 888 888 888 888 ");
255
- console.log("888 .d88P Y8b. Y8bd8P 888 Y88b 888 888 888 888 888 Y8b. 888 Y88b d88P 888 888 ");
256
- console.log('8888888P" "Y8888 Y88P 888 "Y88888 888 888 888 888 "Y8888 888 "Y8888P" 88888888 8888888 ');
257
- console.log(" ");
258
- console.log(" ");
259
- console.log("");
260
- }
261
-
262
- async function main() {
263
- // Clear screen - works on Windows, macOS, Linux
264
- // ANSI escape codes for clear screen + cursor to top
265
- process.stdout.write('\x1B[2J\x1B[0f');
266
- console.clear(); // Fallback for terminals that don't support ANSI
267
-
268
- // Show ASCII logo
269
- showLogo();
270
-
271
- console.log(`DevTunnel v${getPackageVersion()}`);
272
- console.log("Share your local dev servers worldwide");
273
- console.log("");
274
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
275
- console.log("");
276
- console.log("Repository: https://github.com/maiz-an/DevTunnel-CLI");
277
- console.log("npm Package: https://www.npmjs.com/package/devtunnel-cli");
278
- console.log("Website: https://devtunnel-cli.vercel.app");
279
- console.log("");
280
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
281
- console.log("");
282
-
283
- // Step 1: Check Node.js
284
- console.log("[1/4] Checking Node.js...");
285
- if (!await commandExists("node")) {
286
- console.log("ERROR: Node.js not found!");
287
- console.log("Install from: https://nodejs.org/");
288
- process.exit(1);
289
- }
290
- console.log("SUCCESS: Node.js installed");
291
- console.log("");
292
-
293
- // Step 2: Check Cloudflare (bundled or system-installed)
294
- console.log("[2/4] Checking Cloudflare...");
295
-
296
- // Import bundled cloudflared helpers
297
- const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
298
-
299
- let cloudflareAvailable = false;
300
-
301
- if (hasBundledCloudflared()) {
302
- console.log("SUCCESS: Using bundled Cloudflare (no install needed)");
303
- cloudflareAvailable = true;
304
- } else if (await commandExists("cloudflared")) {
305
- console.log("SUCCESS: Cloudflare installed on system");
306
- cloudflareAvailable = true;
307
- } else {
308
- console.log("First time setup - Downloading Cloudflare...");
309
- console.log("This only happens once (~40MB, 10-30 seconds)");
310
- console.log("");
311
-
312
- try {
313
- const bundledPath = await setupCloudflared();
314
-
315
- if (bundledPath) {
316
- console.log("SUCCESS: Cloudflare ready to use");
317
- cloudflareAvailable = true;
318
- } else {
319
- console.log("Could not download Cloudflare");
320
- console.log("Will use alternative tunnel services");
321
- console.log("");
322
- }
323
- } catch (err) {
324
- console.log(`Setup error: ${err.message}`);
325
- console.log("Will use alternative tunnel services");
326
- console.log("");
327
- }
328
- }
329
-
330
- // Show what's available
331
- if (!cloudflareAvailable) {
332
- console.log("DevTunnel has multi-service fallback:");
333
- console.log(" Cloudflare (fastest, no password)");
334
- console.log(" Ngrok (fast alternative)");
335
- console.log(" LocalTunnel (backup option)");
336
- console.log("");
337
- }
338
-
339
- // Step 3: Check dependencies
340
- console.log("[3/4] Checking dependencies...");
341
- const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
342
- if (!existsSync(nodeModulesPath)) {
343
- console.log("Installing dependencies...");
344
- console.log("");
345
- // Run npm install in the project root directory
346
- const result = await runCommand("npm", ["install"], PROJECT_ROOT);
347
- if (result.code !== 0) {
348
- console.log("");
349
- console.log("ERROR: npm install failed");
350
- process.exit(1);
351
- }
352
- console.log("");
353
- console.log("SUCCESS: Dependencies installed");
354
- } else {
355
- console.log("SUCCESS: Dependencies already installed");
356
- }
357
- console.log("");
358
-
359
- // Step 4: Auto-detect or select project
360
- console.log("[4/4] Detecting project...");
361
-
362
- let projectPath, projectName, devPort;
363
-
364
- // Try to auto-detect project in current directory
365
- const autoDetected = await autoDetectProject();
366
-
367
- if (autoDetected && autoDetected.port) {
368
- // Auto-detected project with port
369
- projectPath = autoDetected.path;
370
- projectName = autoDetected.name;
371
-
372
- // Double-check: verify the port is actually in use
373
- const portInUse = await checkPortInUse(autoDetected.port);
374
-
375
- if (!portInUse) {
376
- // Detected port is not actually running, check for other running servers
377
- const portSource =
378
- autoDetected.projectType === "laravel"
379
- ? "Laravel (php artisan serve)"
380
- : autoDetected.projectType === "html"
381
- ? "HTML project"
382
- : autoDetected.projectType === "php"
383
- ? "PHP/XAMPP"
384
- : "package.json";
385
- console.log(`Detected port ${autoDetected.port} (${portSource}), but no server running on that port`);
386
- console.log("Checking for running dev servers...");
387
-
388
- const runningPorts = await detectRunningDevServer();
389
- if (runningPorts.length > 0) {
390
- if (runningPorts.length === 1) {
391
- devPort = runningPorts[0];
392
- console.log(`Found running dev server on port: ${devPort}`);
393
- } else {
394
- console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
395
- const portResponse = await prompts({
396
- type: "select",
397
- name: "port",
398
- message: "Select port:",
399
- choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
400
- });
401
-
402
- if (!portResponse.port) {
403
- console.log("ERROR: No port selected");
404
- process.exit(1);
405
- }
406
-
407
- devPort = portResponse.port;
408
- }
409
- } else {
410
- // No running servers, use detected port (user might start it later)
411
- devPort = autoDetected.port;
412
- console.log(`Using detected port: ${devPort} (make sure dev server is running)`);
413
- }
414
- } else {
415
- // Port is in use, use it
416
- devPort = autoDetected.port;
417
- }
418
-
419
- console.log(`Detected project: ${projectName}`);
420
- console.log(`Using port: ${devPort}`);
421
- console.log(`Using current directory: ${projectPath}`);
422
- console.log("");
423
-
424
- // Confirm with user
425
- const confirm = await prompts({
426
- type: "confirm",
427
- name: "value",
428
- message: "Use detected project?",
429
- initial: true
430
- });
431
-
432
- if (!confirm.value) {
433
- // User wants to select manually
434
- console.log("");
435
- console.log("Selecting project manually...");
436
- console.log("");
437
-
438
- const selectedPath = await selectFolder();
439
- if (!selectedPath || selectedPath.length === 0) {
440
- console.log("ERROR: No folder selected");
441
- process.exit(1);
442
- }
443
-
444
- projectPath = selectedPath;
445
- projectName = basename(selectedPath);
446
-
447
- // Try to detect port for selected project (Laravel → 8000, HTML → 5500, Node from package.json)
448
- const selectedPackagePath = join(selectedPath, "package.json");
449
- const laravelSelected = detectLaravelProject(selectedPath);
450
- const htmlSelected = detectHtmlProject(selectedPath);
451
- const detectedPort = laravelSelected
452
- ? laravelSelected.defaultPort
453
- : htmlSelected
454
- ? htmlSelected.defaultPort
455
- : detectPortFromPackage(selectedPackagePath);
456
-
457
- const portResponse = await prompts({
458
- type: "number",
459
- name: "port",
460
- message: "Enter your dev server port:",
461
- initial: detectedPort || 5173
462
- });
463
-
464
- if (!portResponse.port) {
465
- console.log("ERROR: No port entered");
466
- process.exit(1);
467
- }
468
-
469
- devPort = portResponse.port;
470
- } else {
471
- // User confirmed let them keep default port or type another (e.g. HTML default 5500, can change)
472
- const portPrompt = await prompts({
473
- type: "number",
474
- name: "port",
475
- message: "Dev server port (press Enter for default):",
476
- initial: devPort
477
- });
478
- if (portPrompt.port != null && portPrompt.port > 0) {
479
- devPort = portPrompt.port;
480
- }
481
- }
482
- } else if (autoDetected && !autoDetected.port) {
483
- // Project detected but no port
484
- projectPath = autoDetected.path;
485
- projectName = autoDetected.name;
486
-
487
- console.log(`Detected project: ${projectName}`);
488
- console.log(`Using current directory: ${projectPath}`);
489
- console.log("Checking for running dev servers...");
490
-
491
- const runningPorts = await detectRunningDevServer();
492
-
493
- if (runningPorts.length > 0) {
494
- console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
495
-
496
- if (runningPorts.length === 1) {
497
- devPort = runningPorts[0];
498
- console.log(`Using port: ${devPort}`);
499
- } else {
500
- // Multiple ports detected, let user choose
501
- const portResponse = await prompts({
502
- type: "select",
503
- name: "port",
504
- message: "Select port:",
505
- choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
506
- });
507
-
508
- if (!portResponse.port) {
509
- console.log("ERROR: No port selected");
510
- process.exit(1);
511
- }
512
-
513
- devPort = portResponse.port;
514
- }
515
- } else {
516
- // No running server, ask for port
517
- const portResponse = await prompts({
518
- type: "number",
519
- name: "port",
520
- message: "Enter your dev server port:",
521
- initial: 5173
522
- });
523
-
524
- if (!portResponse.port) {
525
- console.log("ERROR: No port entered");
526
- process.exit(1);
527
- }
528
-
529
- devPort = portResponse.port;
530
- }
531
-
532
- console.log("");
533
- } else {
534
- // No auto-detection, use folder picker
535
- console.log("No project detected in current directory");
536
- console.log("Opening folder picker...");
537
- console.log("");
538
-
539
- projectPath = await selectFolder();
540
-
541
- if (!projectPath || projectPath.length === 0) {
542
- console.log("ERROR: No folder selected");
543
- process.exit(1);
544
- }
545
-
546
- projectName = basename(projectPath);
547
- console.log(`Selected: ${projectPath}`);
548
- console.log("");
549
-
550
- // Try to detect port for selected project (Laravel 8000, HTML → 5500, PHP → 80, Node from package.json)
551
- const selectedPackagePath = join(projectPath, "package.json");
552
- const laravelSelected = detectLaravelProject(projectPath);
553
- const htmlSelected = detectHtmlProject(projectPath);
554
- const phpSelected = detectPhpProject(projectPath);
555
- let detectedPort = laravelSelected
556
- ? laravelSelected.defaultPort
557
- : htmlSelected
558
- ? htmlSelected.defaultPort // 5500
559
- : phpSelected
560
- ? phpSelected.defaultPort // 80
561
- : detectPortFromPackage(selectedPackagePath);
562
-
563
- // Check for running servers
564
- const runningPorts = await detectRunningDevServer();
565
-
566
- let initialPort = detectedPort || 5173;
567
- if (runningPorts.length > 0 && !detectedPort) {
568
- initialPort = runningPorts[0];
569
- }
570
-
571
- const portResponse = await prompts({
572
- type: "number",
573
- name: "port",
574
- message: "Enter your dev server port:",
575
- initial: initialPort
576
- });
577
-
578
- if (!portResponse.port) {
579
- console.log("ERROR: No port entered");
580
- process.exit(1);
581
- }
582
-
583
- devPort = portResponse.port;
584
- }
585
-
586
- console.log("");
587
- const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
588
-
589
- // XAMPP subfolder (e.g. htdocs/PeopleQ → http://localhost/PeopleQ/) — proxy rewrites path
590
- const isPhpXamppSubfolder =
591
- devPort === 80 &&
592
- (projectPath.toLowerCase().includes("htdocs") || projectPath.toLowerCase().includes("www"));
593
- const basePath = isPhpXamppSubfolder ? "/" + basename(projectPath) : "";
594
-
595
- console.log("");
596
- console.log("Configuration:");
597
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
598
- console.log(`Project: ${projectName}`);
599
- console.log(`Dev Server: localhost:${devPort}${basePath || ""}`);
600
- console.log(`Proxy Port: ${proxyPort}`);
601
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
602
- console.log("");
603
-
604
- // For HTML projects with no server running: start built-in static server and confirm it works
605
- let staticServerProcess = null;
606
- const isHtmlProject = !!detectHtmlProject(projectPath);
607
- const portInUseNow = await checkPortInUse(devPort);
608
- if (isHtmlProject && !portInUseNow) {
609
- console.log("Starting built-in static server for HTML project...");
610
- const staticServerPath = join(__dirname, "static-server.js");
611
- staticServerProcess = spawn("node", [staticServerPath, projectPath, devPort.toString()], {
612
- stdio: "pipe",
613
- shell: false
614
- });
615
- staticServerProcess.on("error", () => { });
616
- const ready = await waitForServerReady(devPort, 10000);
617
- if (!ready) {
618
- if (staticServerProcess) staticServerProcess.kill();
619
- console.log("");
620
- console.log("ERROR: Built-in static server did not start in time. Check that port " + devPort + " is free.");
621
- process.exit(1);
622
- }
623
- console.log("Static server ready at http://localhost:" + devPort);
624
- console.log("");
625
- }
626
-
627
- // Start proxy server
628
- console.log("Starting services...");
629
- console.log("");
630
- const proxyPath = join(__dirname, "proxy-server.js");
631
- const proxyArgs = [proxyPath, devPort.toString(), proxyPort.toString(), projectName];
632
- if (basePath) proxyArgs.push(basePath);
633
- const proxyProcess = spawn("node", proxyArgs, {
634
- stdio: "inherit",
635
- shell: false
636
- });
637
-
638
- // Wait for proxy to start
639
- await new Promise(resolve => setTimeout(resolve, 2000));
640
-
641
- // Run main tunnel app (connects to proxy port)
642
- // Use shell: false to properly handle paths with spaces
643
- const indexPath = join(__dirname, "index.js");
644
- const tunnelProcess = spawn("node", [indexPath, proxyPort.toString(), projectName, projectPath], {
645
- stdio: "inherit",
646
- shell: false
647
- });
648
-
649
- // Handle cleanup
650
- const cleanup = () => {
651
- console.log("\nShutting down...");
652
- if (staticServerProcess) staticServerProcess.kill();
653
- proxyProcess.kill();
654
- tunnelProcess.kill();
655
- process.exit(0);
656
- };
657
-
658
- tunnelProcess.on("close", (code) => {
659
- cleanup();
660
- });
661
-
662
- proxyProcess.on("close", () => {
663
- cleanup();
664
- });
665
-
666
- // Handle Ctrl+C
667
- process.on("SIGINT", cleanup);
668
- process.on("SIGTERM", cleanup);
669
- }
670
-
671
- // Run
672
- main().catch((error) => {
673
- console.error("\nERROR:", error.message);
674
- process.exit(1);
675
- });
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.1.1";
21
+ }
22
+ } catch (err) { }
23
+ return "3.1.1";
24
+ }
25
+
26
+ // Helper to run command (cross-platform)
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, signal) => resolve({ code: code ?? (signal ? 1 : 0), output }));
40
+ proc.on("error", (err) => resolve({ code: 1, output: "", error: err }));
41
+ });
42
+ }
43
+
44
+ // Check if command exists (Windows: where, macOS/Linux: which)
45
+ async function commandExists(command) {
46
+ const isWin = process.platform === "win32";
47
+ const result = await runCommand(isWin ? "where" : "which", [command]);
48
+ return result.code === 0 && (result.output || "").trim().length > 0;
49
+ }
50
+
51
+ // Check if a port is in use (dev server running)
52
+ function checkPortInUse(port) {
53
+ return new Promise((resolve) => {
54
+ const server = http.createServer();
55
+
56
+ server.once('error', (err) => {
57
+ // Port is in use
58
+ if (err.code === 'EADDRINUSE') {
59
+ resolve(true);
60
+ } else {
61
+ resolve(false);
62
+ }
63
+ });
64
+
65
+ server.listen(port, () => {
66
+ // Port is available (not in use)
67
+ server.once('close', () => resolve(false));
68
+ server.close();
69
+ });
70
+ });
71
+ }
72
+
73
+ // Poll until server at port responds (for HTML built-in static server)
74
+ async function waitForServerReady(port, timeoutMs = 10000) {
75
+ const start = Date.now();
76
+ while (Date.now() - start < timeoutMs) {
77
+ try {
78
+ const code = await new Promise((resolve) => {
79
+ const req = http.get(`http://127.0.0.1:${port}`, { timeout: 2000 }, (res) => resolve(res.statusCode));
80
+ req.on("error", () => resolve(null));
81
+ });
82
+ if (code !== null && code >= 200 && code < 500) return true;
83
+ } catch (err) { }
84
+ await new Promise((r) => setTimeout(r, 300));
85
+ }
86
+ return false;
87
+ }
88
+
89
+ // Detect port from package.json (parse --port / PORT= from script first, then vite.config, then framework defaults)
90
+ function detectPortFromPackage(packagePath) {
91
+ try {
92
+ if (!existsSync(packagePath)) return null;
93
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
94
+ const scripts = packageJson.scripts || {};
95
+
96
+ const devScript = scripts.dev || scripts.start || scripts.serve;
97
+ if (!devScript) return null;
98
+
99
+ // Explicit --port or --port= in script (Vite, Next, etc.) takes priority
100
+ const explicitPort = devScript.match(/(?:--port[\s=]+|--port=)(\d+)/i);
101
+ if (explicitPort) {
102
+ const p = parseInt(explicitPort[1], 10);
103
+ if (p >= 1 && p <= 65535) return p;
104
+ }
105
+
106
+ // PORT=3000 or port=3000 at start of script (e.g. "PORT=3000 vite")
107
+ const envPort = devScript.match(/(?:^|\s)(?:PORT|port)[\s=]+(\d+)/i);
108
+ if (envPort) {
109
+ const p = parseInt(envPort[1], 10);
110
+ if (p >= 1 && p <= 65535) return p;
111
+ }
112
+
113
+ // Vite: try vite.config.js/ts server.port
114
+ const dir = dirname(packagePath);
115
+ const vitePort = detectViteConfigPort(dir);
116
+ if (vitePort != null) return vitePort;
117
+
118
+ // Default ports by framework
119
+ if (devScript.includes('vite')) return 5173;
120
+ if (devScript.includes('next')) return 3000;
121
+ if (devScript.includes('react-scripts')) return 3000;
122
+ if (devScript.includes('webpack')) return 8080;
123
+ if (devScript.includes('express')) return 3000;
124
+
125
+ return null;
126
+ } catch (err) {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ // Read server.port from vite.config.js or vite.config.ts (simple regex, no eval)
132
+ function detectViteConfigPort(dir) {
133
+ for (const name of ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.cjs']) {
134
+ const path = join(dir, name);
135
+ if (!existsSync(path)) continue;
136
+ try {
137
+ const content = readFileSync(path, 'utf8');
138
+ const m = content.match(/server\s*:\s*\{[^}]*port\s*:\s*(\d+)/s);
139
+ if (m) {
140
+ const p = parseInt(m[1], 10);
141
+ if (p >= 1 && p <= 65535) return p;
142
+ }
143
+ } catch (_) { }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ // True if project has a Node dev server (vite, next, etc.) — don't start our static server
149
+ function hasNodeDevServer(projectPath) {
150
+ // Vite: presence of vite.config.* means dev server project (never use built-in static server)
151
+ if (existsSync(join(projectPath, 'vite.config.js')) || existsSync(join(projectPath, 'vite.config.ts')) ||
152
+ existsSync(join(projectPath, 'vite.config.mjs')) || existsSync(join(projectPath, 'vite.config.cjs'))) {
153
+ return true;
154
+ }
155
+ const packagePath = join(projectPath, 'package.json');
156
+ if (!existsSync(packagePath)) return false;
157
+ try {
158
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
159
+ const scripts = packageJson.scripts || {};
160
+ const devScript = (scripts.dev || scripts.start || scripts.serve || '').toLowerCase();
161
+ return /vite|next|react-scripts|webpack|nuxt|svelte|quasar|express/.test(devScript);
162
+ } catch {
163
+ return false;
164
+ }
165
+ }
166
+
167
+ // Detect Laravel/PHP project (composer.json + artisan)
168
+ function detectLaravelProject(currentDir) {
169
+ const composerPath = join(currentDir, "composer.json");
170
+ const artisanPath = join(currentDir, "artisan");
171
+ if (!existsSync(composerPath) || !existsSync(artisanPath)) return null;
172
+ try {
173
+ const composerJson = JSON.parse(readFileSync(composerPath, "utf8"));
174
+ const projectName = (composerJson.name && composerJson.name.replace(/^laravel\//i, "")) || basename(currentDir);
175
+ return { name: projectName, defaultPort: 8000 }; // php artisan serve
176
+ } catch (err) {
177
+ return null;
178
+ }
179
+ }
180
+
181
+ // Detect plain HTML project (index.html in root)
182
+ function detectHtmlProject(currentDir) {
183
+ const indexPath = join(currentDir, "index.html");
184
+ if (!existsSync(indexPath)) return null;
185
+ return { name: basename(currentDir), defaultPort: 5500 }; // Live Server default; matches VS Code
186
+ }
187
+
188
+ // Detect PHP/XAMPP project (index.php in root, not Laravel)
189
+ function detectPhpProject(currentDir) {
190
+ if (detectLaravelProject(currentDir)) return null; // Laravel has its own flow
191
+ const indexPhp = join(currentDir, "index.php");
192
+ if (!existsSync(indexPhp)) return null;
193
+ return { name: basename(currentDir), defaultPort: 80 }; // XAMPP/Apache default
194
+ }
195
+
196
+ // HTTP GET with timeout; returns status code or null (robust for all OSes)
197
+ function httpGetStatus(port, timeoutMs = 4000) {
198
+ return new Promise((resolve) => {
199
+ const req = http.get(`http://127.0.0.1:${port}`, { timeout: timeoutMs }, (res) => {
200
+ res.on('data', () => { });
201
+ res.on('end', () => resolve(res.statusCode));
202
+ res.on('error', () => resolve(null));
203
+ });
204
+ req.on('error', () => resolve(null));
205
+ req.on('timeout', () => {
206
+ req.destroy();
207
+ resolve(null);
208
+ });
209
+ if (req.setTimeout) req.setTimeout(timeoutMs);
210
+ });
211
+ }
212
+
213
+ // Check common ports for running dev servers (3000/5173 first — Vite/Next often use these)
214
+ async function detectRunningDevServer() {
215
+ const commonPorts = [3000, 5173, 5500, 8080, 8000, 80, 5000, 4000, 3001, 5174];
216
+ const detected = [];
217
+ const requestTimeoutMs = 4000;
218
+
219
+ for (const port of commonPorts) {
220
+ const inUse = await checkPortInUse(port);
221
+ if (!inUse) continue;
222
+ try {
223
+ const status = await Promise.race([
224
+ httpGetStatus(port, requestTimeoutMs),
225
+ new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), requestTimeoutMs + 500))
226
+ ]).catch(() => null);
227
+ // Any HTTP response (2xx/3xx/4xx/5xx) means a server is there
228
+ if (status != null && status >= 0) {
229
+ detected.push(port);
230
+ }
231
+ } catch {
232
+ // Port in use but HTTP failed still list it (user may have started server just now)
233
+ detected.push(port);
234
+ }
235
+ }
236
+
237
+ return detected;
238
+ }
239
+
240
+ // Auto-detect project in current directory (Laravel/PHP first, then Node/npm, then HTML)
241
+ async function autoDetectProject() {
242
+ const currentDir = process.cwd();
243
+ const packagePath = join(currentDir, "package.json");
244
+ const runningPorts = await detectRunningDevServer();
245
+
246
+ // 1) Laravel/PHP (composer.json + artisan) — default port 8000 (php artisan serve)
247
+ const laravel = detectLaravelProject(currentDir);
248
+ if (laravel) {
249
+ const detectedPort = runningPorts.length > 0 ? runningPorts[0] : laravel.defaultPort;
250
+ return {
251
+ path: currentDir,
252
+ name: laravel.name,
253
+ port: detectedPort,
254
+ projectType: "laravel"
255
+ };
256
+ }
257
+
258
+ // 2) Node/npm (package.json)
259
+ if (existsSync(packagePath)) {
260
+ try {
261
+ const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
262
+ const projectName = packageJson.name || basename(currentDir);
263
+ const detectedPort =
264
+ runningPorts.length > 0 ? runningPorts[0] : detectPortFromPackage(packagePath);
265
+ return {
266
+ path: currentDir,
267
+ name: projectName,
268
+ port: detectedPort,
269
+ projectType: "node"
270
+ };
271
+ } catch (err) {
272
+ // fall through to HTML check
273
+ }
274
+ }
275
+
276
+ // 3) Plain HTML (index.html) — default port 5500 (Live Server), else built-in static server
277
+ const html = detectHtmlProject(currentDir);
278
+ if (html) {
279
+ const detectedPort = runningPorts.length > 0 ? runningPorts[0] : html.defaultPort;
280
+ return {
281
+ path: currentDir,
282
+ name: html.name,
283
+ port: detectedPort,
284
+ projectType: "html"
285
+ };
286
+ }
287
+
288
+ // 4) PHP/XAMPP (index.php) — default port 80 (Apache), e.g. http://localhost/PeopleQ/
289
+ const php = detectPhpProject(currentDir);
290
+ if (php) {
291
+ const detectedPort = runningPorts.length > 0 ? runningPorts[0] : php.defaultPort;
292
+ return {
293
+ path: currentDir,
294
+ name: php.name,
295
+ port: detectedPort,
296
+ projectType: "php"
297
+ };
298
+ }
299
+
300
+ return null;
301
+ }
302
+
303
+ // ASCII Logo - Compatible with all OS and terminals
304
+ function showLogo() {
305
+ console.log("");
306
+ console.log(" ");
307
+ console.log(" ");
308
+ console.log("8888888b. 88888888888 888 .d8888b. 888 8888888 ");
309
+ console.log('888 "Y88b 888 888 d88P Y88b 888 888 ');
310
+ console.log("888 888 888 888 888 888 888 888 ");
311
+ console.log("888 888 .d88b. 888 888 888 888 888 88888b. 88888b. .d88b. 888 888 888 888 ");
312
+ console.log('888 888 d8P Y8b 888 888 888 888 888 888 "88b 888 "88b d8P Y8b 888 888 888 888 ');
313
+ console.log("888 888 88888888 Y88 88P 888 888 888 888 888 888 888 88888888 888 888888 888 888 888 888 ");
314
+ console.log("888 .d88P Y8b. Y8bd8P 888 Y88b 888 888 888 888 888 Y8b. 888 Y88b d88P 888 888 ");
315
+ console.log('8888888P" "Y8888 Y88P 888 "Y88888 888 888 888 888 "Y8888 888 "Y8888P" 88888888 8888888 ');
316
+ console.log(" ");
317
+ console.log(" ");
318
+ console.log("");
319
+ }
320
+
321
+ async function main() {
322
+ // Clear screen - works on Windows, macOS, Linux
323
+ // ANSI escape codes for clear screen + cursor to top
324
+ process.stdout.write('\x1B[2J\x1B[0f');
325
+ console.clear(); // Fallback for terminals that don't support ANSI
326
+
327
+ // Show ASCII logo
328
+ showLogo();
329
+
330
+ console.log(`DevTunnel v${getPackageVersion()}`);
331
+ console.log("Share your local dev servers worldwide");
332
+ console.log("");
333
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
334
+ console.log("");
335
+ console.log("Repository: https://github.com/maiz-an/DevTunnel-CLI");
336
+ console.log("npm Package: https://www.npmjs.com/package/devtunnel-cli");
337
+ console.log("Website: https://devtunnel-cli.vercel.app");
338
+ console.log("");
339
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
340
+ console.log("");
341
+
342
+ // Step 1: Check Node.js (version and availability)
343
+ console.log("[1/4] Checking Node.js...");
344
+ const minNodeMajor = 16;
345
+ const currentMajor = parseInt(process.version.slice(1).split(".")[0], 10);
346
+ if (isNaN(currentMajor) || currentMajor < minNodeMajor) {
347
+ console.log("ERROR: Node.js " + minNodeMajor + "+ required. Current:", process.version);
348
+ console.log("Install from: https://nodejs.org/");
349
+ process.exit(1);
350
+ }
351
+ if (!await commandExists("node")) {
352
+ console.log("ERROR: Node.js not found in PATH.");
353
+ console.log("Install from: https://nodejs.org/");
354
+ process.exit(1);
355
+ }
356
+ console.log("SUCCESS: Node.js " + process.version + " installed");
357
+ console.log("");
358
+
359
+ // Step 2: Check Cloudflare (bundled or system-installed)
360
+ console.log("[2/4] Checking Cloudflare...");
361
+
362
+ // Import bundled cloudflared helpers
363
+ const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
364
+
365
+ let cloudflareAvailable = false;
366
+
367
+ if (hasBundledCloudflared()) {
368
+ console.log("SUCCESS: Using bundled Cloudflare (no install needed)");
369
+ cloudflareAvailable = true;
370
+ } else if (await commandExists("cloudflared")) {
371
+ console.log("SUCCESS: Cloudflare installed on system");
372
+ cloudflareAvailable = true;
373
+ } else {
374
+ console.log("First time setup - Downloading Cloudflare...");
375
+ console.log("This only happens once (~40MB, 10-30 seconds)");
376
+ console.log("");
377
+
378
+ try {
379
+ const bundledPath = await setupCloudflared();
380
+
381
+ if (bundledPath) {
382
+ console.log("SUCCESS: Cloudflare ready to use");
383
+ cloudflareAvailable = true;
384
+ } else {
385
+ console.log("Could not download Cloudflare");
386
+ console.log("Will use alternative tunnel services");
387
+ console.log("");
388
+ }
389
+ } catch (err) {
390
+ console.log(`Setup error: ${err.message}`);
391
+ console.log("Will use alternative tunnel services");
392
+ console.log("");
393
+ }
394
+ }
395
+
396
+ // Show what's available
397
+ if (!cloudflareAvailable) {
398
+ console.log("DevTunnel has multi-service fallback:");
399
+ console.log(" Cloudflare (fastest, no password)");
400
+ console.log(" Ngrok (fast alternative)");
401
+ console.log(" LocalTunnel (backup option)");
402
+ console.log("");
403
+ }
404
+
405
+ // Step 3: Check dependencies
406
+ console.log("[3/4] Checking dependencies...");
407
+ const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
408
+ if (!existsSync(nodeModulesPath)) {
409
+ const hasNpm = await commandExists("npm");
410
+ if (!hasNpm) {
411
+ console.log("ERROR: npm not found. Node.js/npm is required.");
412
+ console.log("Install from: https://nodejs.org/");
413
+ process.exit(1);
414
+ }
415
+ console.log("Installing dependencies...");
416
+ console.log("");
417
+ // Run npm install in the project root directory
418
+ const result = await runCommand("npm", ["install"], PROJECT_ROOT);
419
+ if (result.code !== 0) {
420
+ console.log("");
421
+ console.log("ERROR: npm install failed" + (result.error ? ": " + result.error.message : ""));
422
+ process.exit(1);
423
+ }
424
+ console.log("");
425
+ console.log("SUCCESS: Dependencies installed");
426
+ } else {
427
+ console.log("SUCCESS: Dependencies already installed");
428
+ }
429
+ console.log("");
430
+
431
+ // Step 4: Auto-detect or select project
432
+ console.log("[4/4] Detecting project...");
433
+
434
+ let projectPath, projectName, devPort;
435
+
436
+ // Try to auto-detect project in current directory
437
+ const autoDetected = await autoDetectProject();
438
+
439
+ if (autoDetected && autoDetected.port) {
440
+ // Auto-detected project with port
441
+ projectPath = autoDetected.path;
442
+ projectName = autoDetected.name;
443
+
444
+ // Double-check: verify the port is actually in use
445
+ const portInUse = await checkPortInUse(autoDetected.port);
446
+
447
+ if (!portInUse) {
448
+ // Detected port is not actually running, check for other running servers
449
+ const portSource =
450
+ autoDetected.projectType === "laravel"
451
+ ? "Laravel (php artisan serve)"
452
+ : autoDetected.projectType === "html"
453
+ ? "HTML project"
454
+ : autoDetected.projectType === "php"
455
+ ? "PHP/XAMPP"
456
+ : "package.json";
457
+ console.log(`Detected port ${autoDetected.port} (${portSource}), but no server running on that port`);
458
+ console.log("Checking for running dev servers...");
459
+
460
+ const runningPorts = await detectRunningDevServer();
461
+ if (runningPorts.length > 0) {
462
+ if (runningPorts.length === 1) {
463
+ devPort = runningPorts[0];
464
+ console.log(`Found running dev server on port: ${devPort}`);
465
+ } else {
466
+ console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
467
+ const portResponse = await prompts({
468
+ type: "select",
469
+ name: "port",
470
+ message: "Select port:",
471
+ choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
472
+ });
473
+
474
+ if (!portResponse.port) {
475
+ console.log("ERROR: No port selected");
476
+ process.exit(1);
477
+ }
478
+
479
+ devPort = portResponse.port;
480
+ }
481
+ } else {
482
+ // No running servers, use detected port (user might start it later)
483
+ devPort = autoDetected.port;
484
+ console.log(`Using detected port: ${devPort} (make sure dev server is running)`);
485
+ }
486
+ } else {
487
+ // Port is in use, use it
488
+ devPort = autoDetected.port;
489
+ }
490
+
491
+ console.log(`Detected project: ${projectName}`);
492
+ console.log(`Using port: ${devPort}`);
493
+ console.log(`Using current directory: ${projectPath}`);
494
+ console.log("");
495
+
496
+ // Confirm with user
497
+ const confirm = await prompts({
498
+ type: "confirm",
499
+ name: "value",
500
+ message: "Use detected project?",
501
+ initial: true
502
+ });
503
+
504
+ if (!confirm.value) {
505
+ // User wants to select manually
506
+ console.log("");
507
+ console.log("Selecting project manually...");
508
+ console.log("");
509
+
510
+ const selectedPath = await selectFolder();
511
+ if (!selectedPath || selectedPath.length === 0) {
512
+ console.log("ERROR: No folder selected");
513
+ process.exit(1);
514
+ }
515
+
516
+ projectPath = selectedPath;
517
+ projectName = basename(selectedPath);
518
+
519
+ // Try to detect port for selected project (Laravel → 8000, HTML → 5500, Node from package.json)
520
+ const selectedPackagePath = join(selectedPath, "package.json");
521
+ const laravelSelected = detectLaravelProject(selectedPath);
522
+ const htmlSelected = detectHtmlProject(selectedPath);
523
+ const detectedPort = laravelSelected
524
+ ? laravelSelected.defaultPort
525
+ : htmlSelected
526
+ ? htmlSelected.defaultPort
527
+ : detectPortFromPackage(selectedPackagePath);
528
+
529
+ const portResponse = await prompts({
530
+ type: "number",
531
+ name: "port",
532
+ message: "Enter your dev server port:",
533
+ initial: detectedPort || 5173
534
+ });
535
+
536
+ if (!portResponse.port) {
537
+ console.log("ERROR: No port entered");
538
+ process.exit(1);
539
+ }
540
+
541
+ devPort = portResponse.port;
542
+ } else {
543
+ // User confirmed – let them keep default port or type another (e.g. HTML default 5500, can change)
544
+ const portPrompt = await prompts({
545
+ type: "number",
546
+ name: "port",
547
+ message: "Dev server port (press Enter for default):",
548
+ initial: devPort
549
+ });
550
+ if (portPrompt.port != null && portPrompt.port > 0) {
551
+ devPort = portPrompt.port;
552
+ }
553
+ }
554
+ } else if (autoDetected && !autoDetected.port) {
555
+ // Project detected but no port
556
+ projectPath = autoDetected.path;
557
+ projectName = autoDetected.name;
558
+
559
+ console.log(`Detected project: ${projectName}`);
560
+ console.log(`Using current directory: ${projectPath}`);
561
+ console.log("Checking for running dev servers...");
562
+
563
+ const runningPorts = await detectRunningDevServer();
564
+
565
+ if (runningPorts.length > 0) {
566
+ console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
567
+
568
+ if (runningPorts.length === 1) {
569
+ devPort = runningPorts[0];
570
+ console.log(`Using port: ${devPort}`);
571
+ } else {
572
+ // Multiple ports detected, let user choose
573
+ const portResponse = await prompts({
574
+ type: "select",
575
+ name: "port",
576
+ message: "Select port:",
577
+ choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
578
+ });
579
+
580
+ if (!portResponse.port) {
581
+ console.log("ERROR: No port selected");
582
+ process.exit(1);
583
+ }
584
+
585
+ devPort = portResponse.port;
586
+ }
587
+ } else {
588
+ // No running server, ask for port
589
+ const portResponse = await prompts({
590
+ type: "number",
591
+ name: "port",
592
+ message: "Enter your dev server port:",
593
+ initial: 5173
594
+ });
595
+
596
+ if (!portResponse.port) {
597
+ console.log("ERROR: No port entered");
598
+ process.exit(1);
599
+ }
600
+
601
+ devPort = portResponse.port;
602
+ }
603
+
604
+ console.log("");
605
+ } else {
606
+ // No auto-detection, use folder picker
607
+ console.log("No project detected in current directory");
608
+ console.log("Opening folder picker...");
609
+ console.log("");
610
+
611
+ projectPath = await selectFolder();
612
+
613
+ if (!projectPath || projectPath.length === 0) {
614
+ console.log("ERROR: No folder selected");
615
+ process.exit(1);
616
+ }
617
+
618
+ projectName = basename(projectPath);
619
+ console.log(`Selected: ${projectPath}`);
620
+ console.log("");
621
+
622
+ // Try to detect port for selected project (Laravel → 8000, HTML → 5500, PHP → 80, Node from package.json)
623
+ const selectedPackagePath = join(projectPath, "package.json");
624
+ const laravelSelected = detectLaravelProject(projectPath);
625
+ const htmlSelected = detectHtmlProject(projectPath);
626
+ const phpSelected = detectPhpProject(projectPath);
627
+ let detectedPort = laravelSelected
628
+ ? laravelSelected.defaultPort
629
+ : htmlSelected
630
+ ? htmlSelected.defaultPort // 5500
631
+ : phpSelected
632
+ ? phpSelected.defaultPort // 80
633
+ : detectPortFromPackage(selectedPackagePath);
634
+
635
+ // Check for running servers
636
+ const runningPorts = await detectRunningDevServer();
637
+
638
+ let initialPort = detectedPort || 5173;
639
+ if (runningPorts.length > 0 && !detectedPort) {
640
+ initialPort = runningPorts[0];
641
+ }
642
+
643
+ const portResponse = await prompts({
644
+ type: "number",
645
+ name: "port",
646
+ message: "Enter your dev server port:",
647
+ initial: initialPort
648
+ });
649
+
650
+ if (!portResponse.port) {
651
+ console.log("ERROR: No port entered");
652
+ process.exit(1);
653
+ }
654
+
655
+ devPort = portResponse.port;
656
+ }
657
+
658
+ console.log("");
659
+ const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
660
+
661
+ // XAMPP subfolder (e.g. htdocs/PeopleQ → http://localhost/PeopleQ/) — proxy rewrites path
662
+ const isPhpXamppSubfolder =
663
+ devPort === 80 &&
664
+ (projectPath.toLowerCase().includes("htdocs") || projectPath.toLowerCase().includes("www"));
665
+ const basePath = isPhpXamppSubfolder ? "/" + basename(projectPath) : "";
666
+
667
+ console.log("");
668
+ console.log("Configuration:");
669
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
670
+ console.log(`Project: ${projectName}`);
671
+ console.log(`Dev Server: localhost:${devPort}${basePath || ""}`);
672
+ console.log(`Proxy Port: ${proxyPort}`);
673
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
674
+ console.log("");
675
+
676
+ // Only start built-in static server for plain HTML (no Vite/Next/etc) when port is free
677
+ let staticServerProcess = null;
678
+ const isHtmlProject = !!detectHtmlProject(projectPath);
679
+ const hasDevServer = hasNodeDevServer(projectPath);
680
+ const portInUseNow = await checkPortInUse(devPort);
681
+ if (isHtmlProject && !hasDevServer && !portInUseNow) {
682
+ console.log("Starting built-in static server for HTML project...");
683
+ const staticServerPath = join(__dirname, "static-server.js");
684
+ const nodeBin = process.execPath;
685
+ staticServerProcess = spawn(nodeBin, [staticServerPath, projectPath, devPort.toString()], {
686
+ stdio: "pipe",
687
+ shell: false
688
+ });
689
+ staticServerProcess.on("error", (err) => {
690
+ console.error("ERROR: Could not start static server:", err.code === "ENOENT" ? "Node not found" : err.message);
691
+ });
692
+ const ready = await waitForServerReady(devPort, 10000);
693
+ if (!ready) {
694
+ if (staticServerProcess) staticServerProcess.kill();
695
+ console.log("");
696
+ console.log("ERROR: Built-in static server did not start in time. Check that port " + devPort + " is free.");
697
+ process.exit(1);
698
+ }
699
+ console.log("Static server ready at http://localhost:" + devPort);
700
+ console.log("");
701
+ }
702
+
703
+ // Start proxy server
704
+ console.log("Starting services...");
705
+ console.log("");
706
+ const nodeBin = process.execPath;
707
+ const proxyPath = join(__dirname, "proxy-server.js");
708
+ const proxyArgs = [proxyPath, devPort.toString(), proxyPort.toString(), projectName];
709
+ if (basePath) proxyArgs.push(basePath);
710
+ const proxyProcess = spawn(nodeBin, proxyArgs, {
711
+ stdio: "inherit",
712
+ shell: false
713
+ });
714
+
715
+ const onChildError = (name, err) => {
716
+ console.error("\nERROR: " + name + " failed to start:", err.code === "ENOENT" ? "Node not found. Install from https://nodejs.org" : err.message);
717
+ process.exit(1);
718
+ };
719
+ proxyProcess.on("error", (err) => onChildError("Proxy server", err));
720
+
721
+ // Wait for proxy to start
722
+ await new Promise(resolve => setTimeout(resolve, 2000));
723
+
724
+ // Run main tunnel app (connects to proxy port)
725
+ const indexPath = join(__dirname, "index.js");
726
+ const tunnelProcess = spawn(nodeBin, [indexPath, proxyPort.toString(), projectName, projectPath], {
727
+ stdio: "inherit",
728
+ shell: false
729
+ });
730
+ tunnelProcess.on("error", (err) => onChildError("Tunnel service", err));
731
+
732
+ // Handle cleanup
733
+ const cleanup = () => {
734
+ console.log("\nShutting down...");
735
+ if (staticServerProcess) try { staticServerProcess.kill(); } catch (_) { }
736
+ try { proxyProcess.kill(); } catch (_) { }
737
+ try { tunnelProcess.kill(); } catch (_) { }
738
+ process.exit(0);
739
+ };
740
+
741
+ tunnelProcess.on("close", (code) => {
742
+ cleanup();
743
+ });
744
+
745
+ proxyProcess.on("close", () => {
746
+ cleanup();
747
+ });
748
+
749
+ // Handle Ctrl+C
750
+ process.on("SIGINT", cleanup);
751
+ process.on("SIGTERM", cleanup);
752
+ }
753
+
754
+ // Run
755
+ main().catch((error) => {
756
+ const msg = error && error.message ? error.message : String(error);
757
+ console.error("\nERROR:", msg);
758
+ if (error && error.code === "ENOENT") {
759
+ console.error(" A required command or file was not found. Check that Node.js is installed: https://nodejs.org");
760
+ }
761
+ process.exit(1);
762
+ });