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/package.json +1 -1
- package/src/core/RUN.js +71 -64
- package/src/core/index.js +384 -374
- package/src/core/start.js +762 -675
- package/src/utils/folder-picker.js +160 -140
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
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
server.close();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
req.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const devScript = scripts.dev || scripts.start || scripts.serve;
|
|
97
|
-
if (!devScript) return null;
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
console.log("
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
console.log("");
|
|
353
|
-
console.log("
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
console.log("");
|
|
358
|
-
|
|
359
|
-
// Step
|
|
360
|
-
console.log("[
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
console.log("
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
console.
|
|
674
|
-
|
|
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
|
+
});
|