devtunnel-cli 3.0.30 → 3.0.32

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,671 +1,671 @@
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.30";
21
- }
22
- } catch (err) {}
23
- return "3.0.30";
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(" ▒███ ▒▒███ ██████ █████ █████▒ ▒███ ▒ █████ ████ ████████ ████████ ██████ ▒███ ");
250
- console.log(" ▒███ ▒███ ███▒▒███▒▒███ ▒▒███ ▒███ ▒▒███ ▒███ ▒▒███▒▒███ ▒▒███▒▒███ ███▒▒███ ▒███ ");
251
- console.log(" ▒███ ▒███▒███████ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███████ ▒███ ");
252
- console.log(" ▒███ ███ ▒███▒▒▒ ▒▒███ ███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███ ▒███▒▒▒ ▒███ ");
253
- console.log(" ██████████ ▒▒██████ ▒▒█████ █████ ▒▒████████ ████ █████ ████ █████▒▒██████ █████");
254
- console.log("▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒ ▒▒▒▒▒ ");
255
- console.log(" ");
256
- console.log(" ");
257
- console.log("");
258
- }
259
-
260
- async function main() {
261
- // Clear screen - works on Windows, macOS, Linux
262
- // ANSI escape codes for clear screen + cursor to top
263
- process.stdout.write('\x1B[2J\x1B[0f');
264
- console.clear(); // Fallback for terminals that don't support ANSI
265
-
266
- // Show ASCII logo
267
- showLogo();
268
-
269
- console.log(`DevTunnel v${getPackageVersion()}`);
270
- console.log("Share your local dev servers worldwide");
271
- console.log("");
272
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
273
- console.log("Repository: https://github.com/maiz-an/DevTunnel-CLI");
274
- console.log("npm Package: https://www.npmjs.com/package/devtunnel-cli");
275
- console.log("Website: https://devtunnel-cli.vercel.app");
276
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
277
- console.log("");
278
-
279
- // Step 1: Check Node.js
280
- console.log("[1/4] Checking Node.js...");
281
- if (!await commandExists("node")) {
282
- console.log("ERROR: Node.js not found!");
283
- console.log("Install from: https://nodejs.org/");
284
- process.exit(1);
285
- }
286
- console.log("SUCCESS: Node.js installed");
287
- console.log("");
288
-
289
- // Step 2: Check Cloudflare (bundled or system-installed)
290
- console.log("[2/4] Checking Cloudflare...");
291
-
292
- // Import bundled cloudflared helpers
293
- const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
294
-
295
- let cloudflareAvailable = false;
296
-
297
- if (hasBundledCloudflared()) {
298
- console.log("SUCCESS: Using bundled Cloudflare (no install needed)");
299
- cloudflareAvailable = true;
300
- } else if (await commandExists("cloudflared")) {
301
- console.log("SUCCESS: Cloudflare installed on system");
302
- cloudflareAvailable = true;
303
- } else {
304
- console.log("First time setup - Downloading Cloudflare...");
305
- console.log("This only happens once (~40MB, 10-30 seconds)");
306
- console.log("");
307
-
308
- try {
309
- const bundledPath = await setupCloudflared();
310
-
311
- if (bundledPath) {
312
- console.log("SUCCESS: Cloudflare ready to use");
313
- cloudflareAvailable = true;
314
- } else {
315
- console.log("Could not download Cloudflare");
316
- console.log("Will use alternative tunnel services");
317
- console.log("");
318
- }
319
- } catch (err) {
320
- console.log(`Setup error: ${err.message}`);
321
- console.log("Will use alternative tunnel services");
322
- console.log("");
323
- }
324
- }
325
-
326
- // Show what's available
327
- if (!cloudflareAvailable) {
328
- console.log("DevTunnel has multi-service fallback:");
329
- console.log(" Cloudflare (fastest, no password)");
330
- console.log(" Ngrok (fast alternative)");
331
- console.log(" LocalTunnel (backup option)");
332
- console.log("");
333
- }
334
-
335
- // Step 3: Check dependencies
336
- console.log("[3/4] Checking dependencies...");
337
- const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
338
- if (!existsSync(nodeModulesPath)) {
339
- console.log("Installing dependencies...");
340
- console.log("");
341
- // Run npm install in the project root directory
342
- const result = await runCommand("npm", ["install"], PROJECT_ROOT);
343
- if (result.code !== 0) {
344
- console.log("");
345
- console.log("ERROR: npm install failed");
346
- process.exit(1);
347
- }
348
- console.log("");
349
- console.log("SUCCESS: Dependencies installed");
350
- } else {
351
- console.log("SUCCESS: Dependencies already installed");
352
- }
353
- console.log("");
354
-
355
- // Step 4: Auto-detect or select project
356
- console.log("[4/4] Detecting project...");
357
-
358
- let projectPath, projectName, devPort;
359
-
360
- // Try to auto-detect project in current directory
361
- const autoDetected = await autoDetectProject();
362
-
363
- if (autoDetected && autoDetected.port) {
364
- // Auto-detected project with port
365
- projectPath = autoDetected.path;
366
- projectName = autoDetected.name;
367
-
368
- // Double-check: verify the port is actually in use
369
- const portInUse = await checkPortInUse(autoDetected.port);
370
-
371
- if (!portInUse) {
372
- // Detected port is not actually running, check for other running servers
373
- const portSource =
374
- autoDetected.projectType === "laravel"
375
- ? "Laravel (php artisan serve)"
376
- : autoDetected.projectType === "html"
377
- ? "HTML project"
378
- : autoDetected.projectType === "php"
379
- ? "PHP/XAMPP"
380
- : "package.json";
381
- console.log(`Detected port ${autoDetected.port} (${portSource}), but no server running on that port`);
382
- console.log("Checking for running dev servers...");
383
-
384
- const runningPorts = await detectRunningDevServer();
385
- if (runningPorts.length > 0) {
386
- if (runningPorts.length === 1) {
387
- devPort = runningPorts[0];
388
- console.log(`Found running dev server on port: ${devPort}`);
389
- } else {
390
- console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
391
- const portResponse = await prompts({
392
- type: "select",
393
- name: "port",
394
- message: "Select port:",
395
- choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
396
- });
397
-
398
- if (!portResponse.port) {
399
- console.log("ERROR: No port selected");
400
- process.exit(1);
401
- }
402
-
403
- devPort = portResponse.port;
404
- }
405
- } else {
406
- // No running servers, use detected port (user might start it later)
407
- devPort = autoDetected.port;
408
- console.log(`Using detected port: ${devPort} (make sure dev server is running)`);
409
- }
410
- } else {
411
- // Port is in use, use it
412
- devPort = autoDetected.port;
413
- }
414
-
415
- console.log(`Detected project: ${projectName}`);
416
- console.log(`Using port: ${devPort}`);
417
- console.log(`Using current directory: ${projectPath}`);
418
- console.log("");
419
-
420
- // Confirm with user
421
- const confirm = await prompts({
422
- type: "confirm",
423
- name: "value",
424
- message: "Use detected project?",
425
- initial: true
426
- });
427
-
428
- if (!confirm.value) {
429
- // User wants to select manually
430
- console.log("");
431
- console.log("Selecting project manually...");
432
- console.log("");
433
-
434
- const selectedPath = await selectFolder();
435
- if (!selectedPath || selectedPath.length === 0) {
436
- console.log("ERROR: No folder selected");
437
- process.exit(1);
438
- }
439
-
440
- projectPath = selectedPath;
441
- projectName = basename(selectedPath);
442
-
443
- // Try to detect port for selected project (Laravel → 8000, HTML → 5500, Node from package.json)
444
- const selectedPackagePath = join(selectedPath, "package.json");
445
- const laravelSelected = detectLaravelProject(selectedPath);
446
- const htmlSelected = detectHtmlProject(selectedPath);
447
- const detectedPort = laravelSelected
448
- ? laravelSelected.defaultPort
449
- : htmlSelected
450
- ? htmlSelected.defaultPort
451
- : detectPortFromPackage(selectedPackagePath);
452
-
453
- const portResponse = await prompts({
454
- type: "number",
455
- name: "port",
456
- message: "Enter your dev server port:",
457
- initial: detectedPort || 5173
458
- });
459
-
460
- if (!portResponse.port) {
461
- console.log("ERROR: No port entered");
462
- process.exit(1);
463
- }
464
-
465
- devPort = portResponse.port;
466
- } else {
467
- // User confirmed – let them keep default port or type another (e.g. HTML default 5500, can change)
468
- const portPrompt = await prompts({
469
- type: "number",
470
- name: "port",
471
- message: "Dev server port (press Enter for default):",
472
- initial: devPort
473
- });
474
- if (portPrompt.port != null && portPrompt.port > 0) {
475
- devPort = portPrompt.port;
476
- }
477
- }
478
- } else if (autoDetected && !autoDetected.port) {
479
- // Project detected but no port
480
- projectPath = autoDetected.path;
481
- projectName = autoDetected.name;
482
-
483
- console.log(`Detected project: ${projectName}`);
484
- console.log(`Using current directory: ${projectPath}`);
485
- console.log("Checking for running dev servers...");
486
-
487
- const runningPorts = await detectRunningDevServer();
488
-
489
- if (runningPorts.length > 0) {
490
- console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
491
-
492
- if (runningPorts.length === 1) {
493
- devPort = runningPorts[0];
494
- console.log(`Using port: ${devPort}`);
495
- } else {
496
- // Multiple ports detected, let user choose
497
- const portResponse = await prompts({
498
- type: "select",
499
- name: "port",
500
- message: "Select port:",
501
- choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
502
- });
503
-
504
- if (!portResponse.port) {
505
- console.log("ERROR: No port selected");
506
- process.exit(1);
507
- }
508
-
509
- devPort = portResponse.port;
510
- }
511
- } else {
512
- // No running server, ask for port
513
- const portResponse = await prompts({
514
- type: "number",
515
- name: "port",
516
- message: "Enter your dev server port:",
517
- initial: 5173
518
- });
519
-
520
- if (!portResponse.port) {
521
- console.log("ERROR: No port entered");
522
- process.exit(1);
523
- }
524
-
525
- devPort = portResponse.port;
526
- }
527
-
528
- console.log("");
529
- } else {
530
- // No auto-detection, use folder picker
531
- console.log("No project detected in current directory");
532
- console.log("Opening folder picker...");
533
- console.log("");
534
-
535
- projectPath = await selectFolder();
536
-
537
- if (!projectPath || projectPath.length === 0) {
538
- console.log("ERROR: No folder selected");
539
- process.exit(1);
540
- }
541
-
542
- projectName = basename(projectPath);
543
- console.log(`Selected: ${projectPath}`);
544
- console.log("");
545
-
546
- // Try to detect port for selected project (Laravel → 8000, HTML → 5500, PHP → 80, Node from package.json)
547
- const selectedPackagePath = join(projectPath, "package.json");
548
- const laravelSelected = detectLaravelProject(projectPath);
549
- const htmlSelected = detectHtmlProject(projectPath);
550
- const phpSelected = detectPhpProject(projectPath);
551
- let detectedPort = laravelSelected
552
- ? laravelSelected.defaultPort
553
- : htmlSelected
554
- ? htmlSelected.defaultPort // 5500
555
- : phpSelected
556
- ? phpSelected.defaultPort // 80
557
- : detectPortFromPackage(selectedPackagePath);
558
-
559
- // Check for running servers
560
- const runningPorts = await detectRunningDevServer();
561
-
562
- let initialPort = detectedPort || 5173;
563
- if (runningPorts.length > 0 && !detectedPort) {
564
- initialPort = runningPorts[0];
565
- }
566
-
567
- const portResponse = await prompts({
568
- type: "number",
569
- name: "port",
570
- message: "Enter your dev server port:",
571
- initial: initialPort
572
- });
573
-
574
- if (!portResponse.port) {
575
- console.log("ERROR: No port entered");
576
- process.exit(1);
577
- }
578
-
579
- devPort = portResponse.port;
580
- }
581
-
582
- console.log("");
583
- const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
584
-
585
- // XAMPP subfolder (e.g. htdocs/PeopleQ → http://localhost/PeopleQ/) — proxy rewrites path
586
- const isPhpXamppSubfolder =
587
- devPort === 80 &&
588
- (projectPath.toLowerCase().includes("htdocs") || projectPath.toLowerCase().includes("www"));
589
- const basePath = isPhpXamppSubfolder ? "/" + basename(projectPath) : "";
590
-
591
- console.log("");
592
- console.log("Configuration:");
593
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
594
- console.log(`Project: ${projectName}`);
595
- console.log(`Dev Server: localhost:${devPort}${basePath || ""}`);
596
- console.log(`Proxy Port: ${proxyPort}`);
597
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
598
- console.log("");
599
-
600
- // For HTML projects with no server running: start built-in static server and confirm it works
601
- let staticServerProcess = null;
602
- const isHtmlProject = !!detectHtmlProject(projectPath);
603
- const portInUseNow = await checkPortInUse(devPort);
604
- if (isHtmlProject && !portInUseNow) {
605
- console.log("Starting built-in static server for HTML project...");
606
- const staticServerPath = join(__dirname, "static-server.js");
607
- staticServerProcess = spawn("node", [staticServerPath, projectPath, devPort.toString()], {
608
- stdio: "pipe",
609
- shell: false
610
- });
611
- staticServerProcess.on("error", () => {});
612
- const ready = await waitForServerReady(devPort, 10000);
613
- if (!ready) {
614
- if (staticServerProcess) staticServerProcess.kill();
615
- console.log("");
616
- console.log("ERROR: Built-in static server did not start in time. Check that port " + devPort + " is free.");
617
- process.exit(1);
618
- }
619
- console.log("Static server ready at http://localhost:" + devPort);
620
- console.log("");
621
- }
622
-
623
- // Start proxy server
624
- console.log("Starting services...");
625
- console.log("");
626
- const proxyPath = join(__dirname, "proxy-server.js");
627
- const proxyArgs = [proxyPath, devPort.toString(), proxyPort.toString(), projectName];
628
- if (basePath) proxyArgs.push(basePath);
629
- const proxyProcess = spawn("node", proxyArgs, {
630
- stdio: "inherit",
631
- shell: false
632
- });
633
-
634
- // Wait for proxy to start
635
- await new Promise(resolve => setTimeout(resolve, 2000));
636
-
637
- // Run main tunnel app (connects to proxy port)
638
- // Use shell: false to properly handle paths with spaces
639
- const indexPath = join(__dirname, "index.js");
640
- const tunnelProcess = spawn("node", [indexPath, proxyPort.toString(), projectName, projectPath], {
641
- stdio: "inherit",
642
- shell: false
643
- });
644
-
645
- // Handle cleanup
646
- const cleanup = () => {
647
- console.log("\nShutting down...");
648
- if (staticServerProcess) staticServerProcess.kill();
649
- proxyProcess.kill();
650
- tunnelProcess.kill();
651
- process.exit(0);
652
- };
653
-
654
- tunnelProcess.on("close", (code) => {
655
- cleanup();
656
- });
657
-
658
- proxyProcess.on("close", () => {
659
- cleanup();
660
- });
661
-
662
- // Handle Ctrl+C
663
- process.on("SIGINT", cleanup);
664
- process.on("SIGTERM", cleanup);
665
- }
666
-
667
- // Run
668
- main().catch((error) => {
669
- console.error("\nERROR:", error.message);
670
- process.exit(1);
671
- });
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.32";
21
+ }
22
+ } catch (err) {}
23
+ return "3.0.32";
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("8888888b. 88888888888 888 .d8888b. 888 8888888 ");
248
+ console.log('888 "Y88b 888 888 d88P Y88b 888 888 ');
249
+ console.log("888 888 888 888 888 888 888 888 ");
250
+ console.log("888 888 .d88b. 888 888 888 888 888 88888b. 88888b. .d88b. 888 888 888 888 ");
251
+ console.log('888 888 d8P Y8b 888 888 888 888 888 888 "88b 888 "88b d8P Y8b 888 888 888 888 ');
252
+ console.log("888 888 88888888 Y88 88P 888 888 888 888 888 888 888 88888888 888 888888 888 888 888 888 ");
253
+ console.log("888 .d88P Y8b. Y8bd8P 888 Y88b 888 888 888 888 888 Y8b. 888 Y88b d88P 888 888 ");
254
+ console.log('8888888P" "Y8888 Y88P 888 "Y88888 888 888 888 888 "Y8888 888 "Y8888P" 88888888 8888888 ');
255
+ console.log(" ");
256
+ console.log(" ");
257
+ console.log("");
258
+ }
259
+
260
+ async function main() {
261
+ // Clear screen - works on Windows, macOS, Linux
262
+ // ANSI escape codes for clear screen + cursor to top
263
+ process.stdout.write('\x1B[2J\x1B[0f');
264
+ console.clear(); // Fallback for terminals that don't support ANSI
265
+
266
+ // Show ASCII logo
267
+ showLogo();
268
+
269
+ console.log(`DevTunnel v${getPackageVersion()}`);
270
+ console.log("Share your local dev servers worldwide");
271
+ console.log("");
272
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
273
+ console.log("Repository: https://github.com/maiz-an/DevTunnel-CLI");
274
+ console.log("npm Package: https://www.npmjs.com/package/devtunnel-cli");
275
+ console.log("Website: https://www.devtunnel-cli.mzieos.com");
276
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
277
+ console.log("");
278
+
279
+ // Step 1: Check Node.js
280
+ console.log("[1/4] Checking Node.js...");
281
+ if (!await commandExists("node")) {
282
+ console.log("ERROR: Node.js not found!");
283
+ console.log("Install from: https://nodejs.org/");
284
+ process.exit(1);
285
+ }
286
+ console.log("SUCCESS: Node.js installed");
287
+ console.log("");
288
+
289
+ // Step 2: Check Cloudflare (bundled or system-installed)
290
+ console.log("[2/4] Checking Cloudflare...");
291
+
292
+ // Import bundled cloudflared helpers
293
+ const { setupCloudflared, hasBundledCloudflared } = await import("./setup-cloudflared.js");
294
+
295
+ let cloudflareAvailable = false;
296
+
297
+ if (hasBundledCloudflared()) {
298
+ console.log("SUCCESS: Using bundled Cloudflare (no install needed)");
299
+ cloudflareAvailable = true;
300
+ } else if (await commandExists("cloudflared")) {
301
+ console.log("SUCCESS: Cloudflare installed on system");
302
+ cloudflareAvailable = true;
303
+ } else {
304
+ console.log("First time setup - Downloading Cloudflare...");
305
+ console.log("This only happens once (~40MB, 10-30 seconds)");
306
+ console.log("");
307
+
308
+ try {
309
+ const bundledPath = await setupCloudflared();
310
+
311
+ if (bundledPath) {
312
+ console.log("SUCCESS: Cloudflare ready to use");
313
+ cloudflareAvailable = true;
314
+ } else {
315
+ console.log("Could not download Cloudflare");
316
+ console.log("Will use alternative tunnel services");
317
+ console.log("");
318
+ }
319
+ } catch (err) {
320
+ console.log(`Setup error: ${err.message}`);
321
+ console.log("Will use alternative tunnel services");
322
+ console.log("");
323
+ }
324
+ }
325
+
326
+ // Show what's available
327
+ if (!cloudflareAvailable) {
328
+ console.log("DevTunnel has multi-service fallback:");
329
+ console.log(" Cloudflare (fastest, no password)");
330
+ console.log(" Ngrok (fast alternative)");
331
+ console.log(" LocalTunnel (backup option)");
332
+ console.log("");
333
+ }
334
+
335
+ // Step 3: Check dependencies
336
+ console.log("[3/4] Checking dependencies...");
337
+ const nodeModulesPath = join(PROJECT_ROOT, "node_modules");
338
+ if (!existsSync(nodeModulesPath)) {
339
+ console.log("Installing dependencies...");
340
+ console.log("");
341
+ // Run npm install in the project root directory
342
+ const result = await runCommand("npm", ["install"], PROJECT_ROOT);
343
+ if (result.code !== 0) {
344
+ console.log("");
345
+ console.log("ERROR: npm install failed");
346
+ process.exit(1);
347
+ }
348
+ console.log("");
349
+ console.log("SUCCESS: Dependencies installed");
350
+ } else {
351
+ console.log("SUCCESS: Dependencies already installed");
352
+ }
353
+ console.log("");
354
+
355
+ // Step 4: Auto-detect or select project
356
+ console.log("[4/4] Detecting project...");
357
+
358
+ let projectPath, projectName, devPort;
359
+
360
+ // Try to auto-detect project in current directory
361
+ const autoDetected = await autoDetectProject();
362
+
363
+ if (autoDetected && autoDetected.port) {
364
+ // Auto-detected project with port
365
+ projectPath = autoDetected.path;
366
+ projectName = autoDetected.name;
367
+
368
+ // Double-check: verify the port is actually in use
369
+ const portInUse = await checkPortInUse(autoDetected.port);
370
+
371
+ if (!portInUse) {
372
+ // Detected port is not actually running, check for other running servers
373
+ const portSource =
374
+ autoDetected.projectType === "laravel"
375
+ ? "Laravel (php artisan serve)"
376
+ : autoDetected.projectType === "html"
377
+ ? "HTML project"
378
+ : autoDetected.projectType === "php"
379
+ ? "PHP/XAMPP"
380
+ : "package.json";
381
+ console.log(`Detected port ${autoDetected.port} (${portSource}), but no server running on that port`);
382
+ console.log("Checking for running dev servers...");
383
+
384
+ const runningPorts = await detectRunningDevServer();
385
+ if (runningPorts.length > 0) {
386
+ if (runningPorts.length === 1) {
387
+ devPort = runningPorts[0];
388
+ console.log(`Found running dev server on port: ${devPort}`);
389
+ } else {
390
+ console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
391
+ const portResponse = await prompts({
392
+ type: "select",
393
+ name: "port",
394
+ message: "Select port:",
395
+ choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
396
+ });
397
+
398
+ if (!portResponse.port) {
399
+ console.log("ERROR: No port selected");
400
+ process.exit(1);
401
+ }
402
+
403
+ devPort = portResponse.port;
404
+ }
405
+ } else {
406
+ // No running servers, use detected port (user might start it later)
407
+ devPort = autoDetected.port;
408
+ console.log(`Using detected port: ${devPort} (make sure dev server is running)`);
409
+ }
410
+ } else {
411
+ // Port is in use, use it
412
+ devPort = autoDetected.port;
413
+ }
414
+
415
+ console.log(`Detected project: ${projectName}`);
416
+ console.log(`Using port: ${devPort}`);
417
+ console.log(`Using current directory: ${projectPath}`);
418
+ console.log("");
419
+
420
+ // Confirm with user
421
+ const confirm = await prompts({
422
+ type: "confirm",
423
+ name: "value",
424
+ message: "Use detected project?",
425
+ initial: true
426
+ });
427
+
428
+ if (!confirm.value) {
429
+ // User wants to select manually
430
+ console.log("");
431
+ console.log("Selecting project manually...");
432
+ console.log("");
433
+
434
+ const selectedPath = await selectFolder();
435
+ if (!selectedPath || selectedPath.length === 0) {
436
+ console.log("ERROR: No folder selected");
437
+ process.exit(1);
438
+ }
439
+
440
+ projectPath = selectedPath;
441
+ projectName = basename(selectedPath);
442
+
443
+ // Try to detect port for selected project (Laravel → 8000, HTML → 5500, Node from package.json)
444
+ const selectedPackagePath = join(selectedPath, "package.json");
445
+ const laravelSelected = detectLaravelProject(selectedPath);
446
+ const htmlSelected = detectHtmlProject(selectedPath);
447
+ const detectedPort = laravelSelected
448
+ ? laravelSelected.defaultPort
449
+ : htmlSelected
450
+ ? htmlSelected.defaultPort
451
+ : detectPortFromPackage(selectedPackagePath);
452
+
453
+ const portResponse = await prompts({
454
+ type: "number",
455
+ name: "port",
456
+ message: "Enter your dev server port:",
457
+ initial: detectedPort || 5173
458
+ });
459
+
460
+ if (!portResponse.port) {
461
+ console.log("ERROR: No port entered");
462
+ process.exit(1);
463
+ }
464
+
465
+ devPort = portResponse.port;
466
+ } else {
467
+ // User confirmed – let them keep default port or type another (e.g. HTML default 5500, can change)
468
+ const portPrompt = await prompts({
469
+ type: "number",
470
+ name: "port",
471
+ message: "Dev server port (press Enter for default):",
472
+ initial: devPort
473
+ });
474
+ if (portPrompt.port != null && portPrompt.port > 0) {
475
+ devPort = portPrompt.port;
476
+ }
477
+ }
478
+ } else if (autoDetected && !autoDetected.port) {
479
+ // Project detected but no port
480
+ projectPath = autoDetected.path;
481
+ projectName = autoDetected.name;
482
+
483
+ console.log(`Detected project: ${projectName}`);
484
+ console.log(`Using current directory: ${projectPath}`);
485
+ console.log("Checking for running dev servers...");
486
+
487
+ const runningPorts = await detectRunningDevServer();
488
+
489
+ if (runningPorts.length > 0) {
490
+ console.log(`Found ${runningPorts.length} running dev server(s) on port(s): ${runningPorts.join(', ')}`);
491
+
492
+ if (runningPorts.length === 1) {
493
+ devPort = runningPorts[0];
494
+ console.log(`Using port: ${devPort}`);
495
+ } else {
496
+ // Multiple ports detected, let user choose
497
+ const portResponse = await prompts({
498
+ type: "select",
499
+ name: "port",
500
+ message: "Select port:",
501
+ choices: runningPorts.map(p => ({ title: `Port ${p}`, value: p }))
502
+ });
503
+
504
+ if (!portResponse.port) {
505
+ console.log("ERROR: No port selected");
506
+ process.exit(1);
507
+ }
508
+
509
+ devPort = portResponse.port;
510
+ }
511
+ } else {
512
+ // No running server, ask for port
513
+ const portResponse = await prompts({
514
+ type: "number",
515
+ name: "port",
516
+ message: "Enter your dev server port:",
517
+ initial: 5173
518
+ });
519
+
520
+ if (!portResponse.port) {
521
+ console.log("ERROR: No port entered");
522
+ process.exit(1);
523
+ }
524
+
525
+ devPort = portResponse.port;
526
+ }
527
+
528
+ console.log("");
529
+ } else {
530
+ // No auto-detection, use folder picker
531
+ console.log("No project detected in current directory");
532
+ console.log("Opening folder picker...");
533
+ console.log("");
534
+
535
+ projectPath = await selectFolder();
536
+
537
+ if (!projectPath || projectPath.length === 0) {
538
+ console.log("ERROR: No folder selected");
539
+ process.exit(1);
540
+ }
541
+
542
+ projectName = basename(projectPath);
543
+ console.log(`Selected: ${projectPath}`);
544
+ console.log("");
545
+
546
+ // Try to detect port for selected project (Laravel → 8000, HTML → 5500, PHP → 80, Node from package.json)
547
+ const selectedPackagePath = join(projectPath, "package.json");
548
+ const laravelSelected = detectLaravelProject(projectPath);
549
+ const htmlSelected = detectHtmlProject(projectPath);
550
+ const phpSelected = detectPhpProject(projectPath);
551
+ let detectedPort = laravelSelected
552
+ ? laravelSelected.defaultPort
553
+ : htmlSelected
554
+ ? htmlSelected.defaultPort // 5500
555
+ : phpSelected
556
+ ? phpSelected.defaultPort // 80
557
+ : detectPortFromPackage(selectedPackagePath);
558
+
559
+ // Check for running servers
560
+ const runningPorts = await detectRunningDevServer();
561
+
562
+ let initialPort = detectedPort || 5173;
563
+ if (runningPorts.length > 0 && !detectedPort) {
564
+ initialPort = runningPorts[0];
565
+ }
566
+
567
+ const portResponse = await prompts({
568
+ type: "number",
569
+ name: "port",
570
+ message: "Enter your dev server port:",
571
+ initial: initialPort
572
+ });
573
+
574
+ if (!portResponse.port) {
575
+ console.log("ERROR: No port entered");
576
+ process.exit(1);
577
+ }
578
+
579
+ devPort = portResponse.port;
580
+ }
581
+
582
+ console.log("");
583
+ const proxyPort = devPort + 1000; // Use port 1000 higher for proxy
584
+
585
+ // XAMPP subfolder (e.g. htdocs/PeopleQ → http://localhost/PeopleQ/) — proxy rewrites path
586
+ const isPhpXamppSubfolder =
587
+ devPort === 80 &&
588
+ (projectPath.toLowerCase().includes("htdocs") || projectPath.toLowerCase().includes("www"));
589
+ const basePath = isPhpXamppSubfolder ? "/" + basename(projectPath) : "";
590
+
591
+ console.log("");
592
+ console.log("Configuration:");
593
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
594
+ console.log(`Project: ${projectName}`);
595
+ console.log(`Dev Server: localhost:${devPort}${basePath || ""}`);
596
+ console.log(`Proxy Port: ${proxyPort}`);
597
+ console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
598
+ console.log("");
599
+
600
+ // For HTML projects with no server running: start built-in static server and confirm it works
601
+ let staticServerProcess = null;
602
+ const isHtmlProject = !!detectHtmlProject(projectPath);
603
+ const portInUseNow = await checkPortInUse(devPort);
604
+ if (isHtmlProject && !portInUseNow) {
605
+ console.log("Starting built-in static server for HTML project...");
606
+ const staticServerPath = join(__dirname, "static-server.js");
607
+ staticServerProcess = spawn("node", [staticServerPath, projectPath, devPort.toString()], {
608
+ stdio: "pipe",
609
+ shell: false
610
+ });
611
+ staticServerProcess.on("error", () => {});
612
+ const ready = await waitForServerReady(devPort, 10000);
613
+ if (!ready) {
614
+ if (staticServerProcess) staticServerProcess.kill();
615
+ console.log("");
616
+ console.log("ERROR: Built-in static server did not start in time. Check that port " + devPort + " is free.");
617
+ process.exit(1);
618
+ }
619
+ console.log("Static server ready at http://localhost:" + devPort);
620
+ console.log("");
621
+ }
622
+
623
+ // Start proxy server
624
+ console.log("Starting services...");
625
+ console.log("");
626
+ const proxyPath = join(__dirname, "proxy-server.js");
627
+ const proxyArgs = [proxyPath, devPort.toString(), proxyPort.toString(), projectName];
628
+ if (basePath) proxyArgs.push(basePath);
629
+ const proxyProcess = spawn("node", proxyArgs, {
630
+ stdio: "inherit",
631
+ shell: false
632
+ });
633
+
634
+ // Wait for proxy to start
635
+ await new Promise(resolve => setTimeout(resolve, 2000));
636
+
637
+ // Run main tunnel app (connects to proxy port)
638
+ // Use shell: false to properly handle paths with spaces
639
+ const indexPath = join(__dirname, "index.js");
640
+ const tunnelProcess = spawn("node", [indexPath, proxyPort.toString(), projectName, projectPath], {
641
+ stdio: "inherit",
642
+ shell: false
643
+ });
644
+
645
+ // Handle cleanup
646
+ const cleanup = () => {
647
+ console.log("\nShutting down...");
648
+ if (staticServerProcess) staticServerProcess.kill();
649
+ proxyProcess.kill();
650
+ tunnelProcess.kill();
651
+ process.exit(0);
652
+ };
653
+
654
+ tunnelProcess.on("close", (code) => {
655
+ cleanup();
656
+ });
657
+
658
+ proxyProcess.on("close", () => {
659
+ cleanup();
660
+ });
661
+
662
+ // Handle Ctrl+C
663
+ process.on("SIGINT", cleanup);
664
+ process.on("SIGTERM", cleanup);
665
+ }
666
+
667
+ // Run
668
+ main().catch((error) => {
669
+ console.error("\nERROR:", error.message);
670
+ process.exit(1);
671
+ });