portless 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -2
- package/dist/{chunk-EZJWUTUA.js → chunk-6PDLZVDS.js} +19 -693
- package/dist/cli.js +1944 -260
- package/dist/index.d.ts +2 -9
- package/dist/index.js +1 -5
- package/package.json +13 -12
- package/LICENSE +0 -201
package/dist/cli.js
CHANGED
|
@@ -1,53 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
DEFAULT_TLD,
|
|
4
|
-
FALLBACK_PROXY_PORT,
|
|
5
3
|
FILE_MODE,
|
|
6
|
-
|
|
7
|
-
INTERNAL_LAN_IP_FLAG,
|
|
8
|
-
PRIVILEGED_PORT_THRESHOLD,
|
|
9
|
-
RISKY_TLDS,
|
|
4
|
+
PORTLESS_HEADER,
|
|
10
5
|
RouteConflictError,
|
|
11
6
|
RouteStore,
|
|
12
|
-
SYSTEM_STATE_DIR,
|
|
13
|
-
USER_STATE_DIR,
|
|
14
|
-
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
15
|
-
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
16
|
-
buildProxyStartConfig,
|
|
17
7
|
cleanHostsFile,
|
|
18
8
|
createHttpRedirectServer,
|
|
19
9
|
createProxyServer,
|
|
20
|
-
discoverState,
|
|
21
|
-
findFreePort,
|
|
22
|
-
findPidOnPort,
|
|
23
10
|
fixOwnership,
|
|
24
11
|
formatUrl,
|
|
25
|
-
getDefaultPort,
|
|
26
|
-
getDefaultTld,
|
|
27
|
-
injectFrameworkFlags,
|
|
28
12
|
isErrnoException,
|
|
29
|
-
isHttpsEnvDisabled,
|
|
30
|
-
isLanEnvEnabled,
|
|
31
|
-
isPortListening,
|
|
32
|
-
isProxyRunning,
|
|
33
|
-
isWildcardEnvEnabled,
|
|
34
|
-
isWindows,
|
|
35
13
|
parseHostname,
|
|
36
|
-
prompt,
|
|
37
|
-
readLanMarker,
|
|
38
|
-
readPersistedProxyState,
|
|
39
|
-
readTldFromDir,
|
|
40
|
-
readTlsMarker,
|
|
41
|
-
resolveStateDir,
|
|
42
14
|
shouldAutoSyncHosts,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
validateTld,
|
|
46
|
-
waitForProxy,
|
|
47
|
-
writeLanMarker,
|
|
48
|
-
writeTldFile,
|
|
49
|
-
writeTlsMarker
|
|
50
|
-
} from "./chunk-EZJWUTUA.js";
|
|
15
|
+
syncHostsFile
|
|
16
|
+
} from "./chunk-6PDLZVDS.js";
|
|
51
17
|
|
|
52
18
|
// src/colors.ts
|
|
53
19
|
function supportsColor() {
|
|
@@ -60,24 +26,23 @@ var wrap = (open, close) => {
|
|
|
60
26
|
if (!enabled) return (s) => s;
|
|
61
27
|
return (s) => `\x1B[${open}m${s}\x1B[${close}m`;
|
|
62
28
|
};
|
|
29
|
+
var identity = (s) => s;
|
|
63
30
|
var bold = wrap("1", "22");
|
|
31
|
+
var dim = wrap("2", "22");
|
|
64
32
|
var red = wrap("31", "39");
|
|
65
|
-
var green =
|
|
33
|
+
var green = identity;
|
|
66
34
|
var yellow = wrap("33", "39");
|
|
67
|
-
var blue = Object.assign(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
var
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
var white = wrap("37", "39");
|
|
74
|
-
var gray = wrap("90", "39");
|
|
75
|
-
var colors_default = { bold, red, green, yellow, blue, cyan, white, gray };
|
|
35
|
+
var blue = Object.assign(identity, { bold });
|
|
36
|
+
var cyan = Object.assign(identity, { bold });
|
|
37
|
+
var white = identity;
|
|
38
|
+
var gray = dim;
|
|
39
|
+
var colors_default = { bold, dim, red, green, yellow, blue, cyan, white, gray };
|
|
76
40
|
|
|
77
41
|
// src/cli.ts
|
|
78
|
-
import * as
|
|
79
|
-
import * as
|
|
80
|
-
import { spawn as
|
|
42
|
+
import * as fs8 from "fs";
|
|
43
|
+
import * as path8 from "path";
|
|
44
|
+
import { spawn as spawn3, spawnSync as spawnSync2 } from "child_process";
|
|
45
|
+
import { StringDecoder } from "string_decoder";
|
|
81
46
|
|
|
82
47
|
// src/certs.ts
|
|
83
48
|
import * as fs from "fs";
|
|
@@ -98,6 +63,7 @@ var CA_KEY_FILE = "ca-key.pem";
|
|
|
98
63
|
var CA_CERT_FILE = "ca.pem";
|
|
99
64
|
var SERVER_KEY_FILE = "server-key.pem";
|
|
100
65
|
var SERVER_CERT_FILE = "server.pem";
|
|
66
|
+
var CA_TRUST_MARKER = "ca.trusted";
|
|
101
67
|
function fileExists(filePath) {
|
|
102
68
|
try {
|
|
103
69
|
fs.accessSync(filePath, fs.constants.R_OK);
|
|
@@ -106,6 +72,36 @@ function fileExists(filePath) {
|
|
|
106
72
|
return false;
|
|
107
73
|
}
|
|
108
74
|
}
|
|
75
|
+
function caFingerprint(stateDir) {
|
|
76
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
77
|
+
try {
|
|
78
|
+
const pem = fs.readFileSync(caCertPath);
|
|
79
|
+
return crypto.createHash("sha256").update(pem).digest("hex");
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readTrustMarker(stateDir) {
|
|
85
|
+
try {
|
|
86
|
+
const value = fs.readFileSync(path.join(stateDir, CA_TRUST_MARKER), "utf-8").trim();
|
|
87
|
+
return value || null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function writeTrustMarker(stateDir) {
|
|
93
|
+
const fp = caFingerprint(stateDir);
|
|
94
|
+
if (fp) {
|
|
95
|
+
fs.writeFileSync(path.join(stateDir, CA_TRUST_MARKER), fp + "\n");
|
|
96
|
+
fixOwnership(path.join(stateDir, CA_TRUST_MARKER));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function clearTrustMarker(stateDir) {
|
|
100
|
+
try {
|
|
101
|
+
fs.unlinkSync(path.join(stateDir, CA_TRUST_MARKER));
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
109
105
|
var _opensslEnv;
|
|
110
106
|
function getOpensslEnv() {
|
|
111
107
|
if (process.platform !== "win32") return void 0;
|
|
@@ -228,6 +224,7 @@ function generateCA(stateDir) {
|
|
|
228
224
|
fs.chmodSync(keyPath, 384);
|
|
229
225
|
fs.chmodSync(certPath, 420);
|
|
230
226
|
fixOwnership(keyPath, certPath);
|
|
227
|
+
clearTrustMarker(stateDir);
|
|
231
228
|
return { certPath, keyPath };
|
|
232
229
|
}
|
|
233
230
|
function generateServerCert(stateDir) {
|
|
@@ -293,7 +290,8 @@ function ensureCerts(stateDir) {
|
|
|
293
290
|
const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
|
|
294
291
|
const serverKeyPath = path.join(stateDir, SERVER_KEY_FILE);
|
|
295
292
|
let caGenerated = false;
|
|
296
|
-
|
|
293
|
+
const caMissing = !fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath) || !isCertSignatureStrong(caCertPath);
|
|
294
|
+
if (caMissing) {
|
|
297
295
|
generateCA(stateDir);
|
|
298
296
|
caGenerated = true;
|
|
299
297
|
}
|
|
@@ -302,7 +300,7 @@ function ensureCerts(stateDir) {
|
|
|
302
300
|
}
|
|
303
301
|
return {
|
|
304
302
|
certPath: serverCertPath,
|
|
305
|
-
keyPath:
|
|
303
|
+
keyPath: serverKeyPath,
|
|
306
304
|
caPath: caCertPath,
|
|
307
305
|
caGenerated
|
|
308
306
|
};
|
|
@@ -310,6 +308,11 @@ function ensureCerts(stateDir) {
|
|
|
310
308
|
function isCATrusted(stateDir) {
|
|
311
309
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
312
310
|
if (!fileExists(caCertPath)) return false;
|
|
311
|
+
const marker = readTrustMarker(stateDir);
|
|
312
|
+
if (marker) {
|
|
313
|
+
const fp = caFingerprint(stateDir);
|
|
314
|
+
if (fp && marker === fp) return true;
|
|
315
|
+
}
|
|
313
316
|
if (process.platform === "darwin") {
|
|
314
317
|
return isCATrustedMacOS(caCertPath);
|
|
315
318
|
} else if (process.platform === "linux") {
|
|
@@ -586,6 +589,7 @@ function trustCA(stateDir) {
|
|
|
586
589
|
{ stdio: "pipe", timeout: MACOS_SECURITY_AUTH_TIMEOUT_MS }
|
|
587
590
|
);
|
|
588
591
|
}
|
|
592
|
+
writeTrustMarker(stateDir);
|
|
589
593
|
return { trusted: true };
|
|
590
594
|
} else if (process.platform === "linux") {
|
|
591
595
|
const config = getLinuxCATrustConfig();
|
|
@@ -595,12 +599,14 @@ function trustCA(stateDir) {
|
|
|
595
599
|
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
596
600
|
fs.copyFileSync(caCertPath, dest);
|
|
597
601
|
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
602
|
+
writeTrustMarker(stateDir);
|
|
598
603
|
return { trusted: true };
|
|
599
604
|
} else if (process.platform === "win32") {
|
|
600
605
|
execFileSync("certutil", ["-addstore", "-user", "Root", caCertPath], {
|
|
601
606
|
stdio: "pipe",
|
|
602
607
|
timeout: 3e4
|
|
603
608
|
});
|
|
609
|
+
writeTrustMarker(stateDir);
|
|
604
610
|
return { trusted: true };
|
|
605
611
|
}
|
|
606
612
|
return { trusted: false, error: `Unsupported platform: ${process.platform}` };
|
|
@@ -622,22 +628,26 @@ function trustCA(stateDir) {
|
|
|
622
628
|
function untrustCA(stateDir) {
|
|
623
629
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
624
630
|
if (!fileExists(caCertPath)) {
|
|
631
|
+
clearTrustMarker(stateDir);
|
|
625
632
|
return { removed: true };
|
|
626
633
|
}
|
|
627
634
|
if (!isCATrusted(stateDir)) {
|
|
635
|
+
clearTrustMarker(stateDir);
|
|
628
636
|
return { removed: true };
|
|
629
637
|
}
|
|
630
638
|
try {
|
|
639
|
+
let result;
|
|
631
640
|
if (process.platform === "darwin") {
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
641
|
+
result = untrustCAMacOS(caCertPath);
|
|
642
|
+
} else if (process.platform === "linux") {
|
|
643
|
+
result = untrustCALinux(stateDir);
|
|
644
|
+
} else if (process.platform === "win32") {
|
|
645
|
+
result = untrustCAWindows(caCertPath);
|
|
646
|
+
} else {
|
|
647
|
+
result = { removed: false, error: `Unsupported platform: ${process.platform}` };
|
|
639
648
|
}
|
|
640
|
-
|
|
649
|
+
if (result.removed) clearTrustMarker(stateDir);
|
|
650
|
+
return result;
|
|
641
651
|
} catch (err) {
|
|
642
652
|
const message = err instanceof Error ? err.message : String(err);
|
|
643
653
|
return { removed: false, error: message };
|
|
@@ -655,12 +665,13 @@ function untrustCAMacOS(caCertPath) {
|
|
|
655
665
|
return false;
|
|
656
666
|
}
|
|
657
667
|
};
|
|
658
|
-
|
|
659
|
-
|
|
668
|
+
tryExec(["remove-trusted-cert", caCertPath]);
|
|
669
|
+
const keychains = [loginKeychainPath(), "/Library/Keychains/System.keychain"];
|
|
670
|
+
for (const kc of keychains) {
|
|
671
|
+
for (let i = 0; i < 20; i++) {
|
|
672
|
+
if (!tryExec(["delete-certificate", "-c", CA_COMMON_NAME, kc])) break;
|
|
673
|
+
}
|
|
660
674
|
}
|
|
661
|
-
const login = loginKeychainPath();
|
|
662
|
-
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, login]);
|
|
663
|
-
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, "/Library/Keychains/System.keychain"]);
|
|
664
675
|
return isCATrustedMacOSAfterAttempt(caCertPath) ? { removed: false, error: errors.join("; ") || "Could not remove CA from keychain (try sudo)" } : { removed: true };
|
|
665
676
|
}
|
|
666
677
|
function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
@@ -744,14 +755,14 @@ function untrustCAWindows(caCertPath) {
|
|
|
744
755
|
}
|
|
745
756
|
|
|
746
757
|
// src/auto.ts
|
|
747
|
-
import { createHash } from "crypto";
|
|
758
|
+
import { createHash as createHash2 } from "crypto";
|
|
748
759
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
749
760
|
import * as fs2 from "fs";
|
|
750
761
|
import * as path2 from "path";
|
|
751
762
|
var MAX_DNS_LABEL_LENGTH = 63;
|
|
752
763
|
function truncateLabel(label) {
|
|
753
764
|
if (label.length <= MAX_DNS_LABEL_LENGTH) return label;
|
|
754
|
-
const hash =
|
|
765
|
+
const hash = createHash2("sha256").update(label).digest("hex").slice(0, 6);
|
|
755
766
|
const maxPrefixLength = MAX_DNS_LABEL_LENGTH - 7;
|
|
756
767
|
const prefix = label.slice(0, maxPrefixLength).replace(/-+$/, "");
|
|
757
768
|
return `${prefix}-${hash}`;
|
|
@@ -917,9 +928,618 @@ function readBranchFromHead(gitdir) {
|
|
|
917
928
|
}
|
|
918
929
|
}
|
|
919
930
|
|
|
920
|
-
// src/
|
|
931
|
+
// src/cli-utils.ts
|
|
921
932
|
import * as fs3 from "fs";
|
|
933
|
+
import * as http from "http";
|
|
934
|
+
import * as https from "https";
|
|
935
|
+
import * as net from "net";
|
|
936
|
+
import * as os from "os";
|
|
922
937
|
import * as path3 from "path";
|
|
938
|
+
import * as readline from "readline";
|
|
939
|
+
import { execSync, spawn } from "child_process";
|
|
940
|
+
var isWindows = process.platform === "win32";
|
|
941
|
+
var FALLBACK_PROXY_PORT = 1355;
|
|
942
|
+
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
943
|
+
var INTERNAL_LAN_IP_ENV = "PORTLESS_INTERNAL_LAN_IP";
|
|
944
|
+
var INTERNAL_LAN_IP_FLAG = "--lan-ip-auto";
|
|
945
|
+
var LEGACY_SYSTEM_STATE_DIR = isWindows ? path3.join(os.tmpdir(), "portless") : "/tmp/portless";
|
|
946
|
+
var USER_STATE_DIR = path3.join(os.homedir(), ".portless");
|
|
947
|
+
var MIN_APP_PORT = 4e3;
|
|
948
|
+
var MAX_APP_PORT = 4999;
|
|
949
|
+
var RANDOM_PORT_ATTEMPTS = 50;
|
|
950
|
+
var BLOCKED_PORTS = /* @__PURE__ */ new Set([
|
|
951
|
+
0,
|
|
952
|
+
1,
|
|
953
|
+
7,
|
|
954
|
+
9,
|
|
955
|
+
11,
|
|
956
|
+
13,
|
|
957
|
+
15,
|
|
958
|
+
17,
|
|
959
|
+
19,
|
|
960
|
+
20,
|
|
961
|
+
21,
|
|
962
|
+
22,
|
|
963
|
+
23,
|
|
964
|
+
25,
|
|
965
|
+
37,
|
|
966
|
+
42,
|
|
967
|
+
43,
|
|
968
|
+
53,
|
|
969
|
+
69,
|
|
970
|
+
77,
|
|
971
|
+
79,
|
|
972
|
+
87,
|
|
973
|
+
95,
|
|
974
|
+
101,
|
|
975
|
+
102,
|
|
976
|
+
103,
|
|
977
|
+
104,
|
|
978
|
+
109,
|
|
979
|
+
110,
|
|
980
|
+
111,
|
|
981
|
+
113,
|
|
982
|
+
115,
|
|
983
|
+
117,
|
|
984
|
+
119,
|
|
985
|
+
123,
|
|
986
|
+
135,
|
|
987
|
+
137,
|
|
988
|
+
139,
|
|
989
|
+
143,
|
|
990
|
+
161,
|
|
991
|
+
179,
|
|
992
|
+
389,
|
|
993
|
+
427,
|
|
994
|
+
465,
|
|
995
|
+
512,
|
|
996
|
+
513,
|
|
997
|
+
514,
|
|
998
|
+
515,
|
|
999
|
+
526,
|
|
1000
|
+
530,
|
|
1001
|
+
531,
|
|
1002
|
+
532,
|
|
1003
|
+
540,
|
|
1004
|
+
548,
|
|
1005
|
+
554,
|
|
1006
|
+
556,
|
|
1007
|
+
563,
|
|
1008
|
+
587,
|
|
1009
|
+
601,
|
|
1010
|
+
636,
|
|
1011
|
+
989,
|
|
1012
|
+
990,
|
|
1013
|
+
993,
|
|
1014
|
+
995,
|
|
1015
|
+
1719,
|
|
1016
|
+
1720,
|
|
1017
|
+
1723,
|
|
1018
|
+
2049,
|
|
1019
|
+
3659,
|
|
1020
|
+
4045,
|
|
1021
|
+
4190,
|
|
1022
|
+
5060,
|
|
1023
|
+
5061,
|
|
1024
|
+
6e3,
|
|
1025
|
+
6566,
|
|
1026
|
+
6665,
|
|
1027
|
+
6666,
|
|
1028
|
+
6667,
|
|
1029
|
+
6668,
|
|
1030
|
+
6669,
|
|
1031
|
+
6679,
|
|
1032
|
+
6697,
|
|
1033
|
+
10080
|
|
1034
|
+
]);
|
|
1035
|
+
var SOCKET_TIMEOUT_MS = 500;
|
|
1036
|
+
var PID_LOOKUP_TIMEOUT_MS = 5e3;
|
|
1037
|
+
var WAIT_FOR_PROXY_MAX_ATTEMPTS = 20;
|
|
1038
|
+
var WAIT_FOR_PROXY_INTERVAL_MS = 250;
|
|
1039
|
+
var SIGNAL_CODES = {
|
|
1040
|
+
SIGHUP: 1,
|
|
1041
|
+
SIGINT: 2,
|
|
1042
|
+
SIGQUIT: 3,
|
|
1043
|
+
SIGABRT: 6,
|
|
1044
|
+
SIGKILL: 9,
|
|
1045
|
+
SIGTERM: 15
|
|
1046
|
+
};
|
|
1047
|
+
function getProtocolPort(tls2) {
|
|
1048
|
+
return tls2 ? 443 : 80;
|
|
1049
|
+
}
|
|
1050
|
+
function getDefaultPort(tls2) {
|
|
1051
|
+
const envPort = process.env.PORTLESS_PORT;
|
|
1052
|
+
if (envPort) {
|
|
1053
|
+
const port = parseInt(envPort, 10);
|
|
1054
|
+
if (!isNaN(port) && port >= 1 && port <= 65535) return port;
|
|
1055
|
+
}
|
|
1056
|
+
return tls2 === void 0 ? FALLBACK_PROXY_PORT : getProtocolPort(tls2);
|
|
1057
|
+
}
|
|
1058
|
+
function resolveStateDir(_port) {
|
|
1059
|
+
if (process.env.PORTLESS_STATE_DIR) return process.env.PORTLESS_STATE_DIR;
|
|
1060
|
+
return USER_STATE_DIR;
|
|
1061
|
+
}
|
|
1062
|
+
function readPortFromDir(dir) {
|
|
1063
|
+
try {
|
|
1064
|
+
const raw = fs3.readFileSync(path3.join(dir, "proxy.port"), "utf-8").trim();
|
|
1065
|
+
const port = parseInt(raw, 10);
|
|
1066
|
+
return isNaN(port) ? null : port;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
var TLS_MARKER_FILE = "proxy.tls";
|
|
1072
|
+
function readTlsMarker(dir) {
|
|
1073
|
+
try {
|
|
1074
|
+
return fs3.existsSync(path3.join(dir, TLS_MARKER_FILE));
|
|
1075
|
+
} catch {
|
|
1076
|
+
return false;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function writeTlsMarker(dir, enabled2) {
|
|
1080
|
+
const markerPath = path3.join(dir, TLS_MARKER_FILE);
|
|
1081
|
+
if (enabled2) {
|
|
1082
|
+
fs3.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1083
|
+
} else {
|
|
1084
|
+
try {
|
|
1085
|
+
fs3.unlinkSync(markerPath);
|
|
1086
|
+
} catch {
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
var LAN_MARKER_FILE = "proxy.lan";
|
|
1091
|
+
function readLanMarker(dir) {
|
|
1092
|
+
try {
|
|
1093
|
+
const raw = fs3.readFileSync(path3.join(dir, LAN_MARKER_FILE), "utf-8").trim();
|
|
1094
|
+
return raw || null;
|
|
1095
|
+
} catch {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function writeLanMarker(dir, ip) {
|
|
1100
|
+
const markerPath = path3.join(dir, LAN_MARKER_FILE);
|
|
1101
|
+
if (!ip) {
|
|
1102
|
+
try {
|
|
1103
|
+
fs3.unlinkSync(markerPath);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
} else {
|
|
1107
|
+
fs3.writeFileSync(markerPath, ip, { mode: 420 });
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
var DEFAULT_TLD = "localhost";
|
|
1111
|
+
var RISKY_TLDS = /* @__PURE__ */ new Map([
|
|
1112
|
+
["local", "conflicts with mDNS/Bonjour on macOS"],
|
|
1113
|
+
["dev", "Google-owned; browsers force HTTPS via preloaded HSTS"],
|
|
1114
|
+
["com", "public TLD; DNS requests will leak to the internet"],
|
|
1115
|
+
["org", "public TLD; DNS requests will leak to the internet"],
|
|
1116
|
+
["net", "public TLD; DNS requests will leak to the internet"],
|
|
1117
|
+
["io", "public TLD; DNS requests will leak to the internet"],
|
|
1118
|
+
["app", "public TLD; DNS requests will leak to the internet"],
|
|
1119
|
+
["edu", "public TLD; DNS requests will leak to the internet"],
|
|
1120
|
+
["gov", "public TLD; DNS requests will leak to the internet"],
|
|
1121
|
+
["mil", "public TLD; DNS requests will leak to the internet"],
|
|
1122
|
+
["int", "public TLD; DNS requests will leak to the internet"]
|
|
1123
|
+
]);
|
|
1124
|
+
function validateTld(tld) {
|
|
1125
|
+
if (!tld) return "TLD cannot be empty";
|
|
1126
|
+
if (!/^[a-z0-9]+$/.test(tld)) {
|
|
1127
|
+
return `Invalid TLD "${tld}": must contain only lowercase letters and digits`;
|
|
1128
|
+
}
|
|
1129
|
+
return null;
|
|
1130
|
+
}
|
|
1131
|
+
var TLD_FILE = "proxy.tld";
|
|
1132
|
+
function readTldFromDir(dir) {
|
|
1133
|
+
try {
|
|
1134
|
+
const raw = fs3.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
|
|
1135
|
+
return raw || DEFAULT_TLD;
|
|
1136
|
+
} catch {
|
|
1137
|
+
return DEFAULT_TLD;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function writeTldFile(dir, tld) {
|
|
1141
|
+
const filePath = path3.join(dir, TLD_FILE);
|
|
1142
|
+
if (tld === DEFAULT_TLD) {
|
|
1143
|
+
try {
|
|
1144
|
+
fs3.unlinkSync(filePath);
|
|
1145
|
+
} catch {
|
|
1146
|
+
}
|
|
1147
|
+
} else {
|
|
1148
|
+
fs3.writeFileSync(filePath, tld, { mode: 420 });
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
function getDefaultTld() {
|
|
1152
|
+
const val = process.env.PORTLESS_TLD?.trim().toLowerCase();
|
|
1153
|
+
if (!val) return DEFAULT_TLD;
|
|
1154
|
+
const err = validateTld(val);
|
|
1155
|
+
if (err) throw new Error(`PORTLESS_TLD: ${err}`);
|
|
1156
|
+
return val;
|
|
1157
|
+
}
|
|
1158
|
+
function isHttpsEnvDisabled() {
|
|
1159
|
+
const val = process.env.PORTLESS_HTTPS;
|
|
1160
|
+
return val === "0" || val === "false";
|
|
1161
|
+
}
|
|
1162
|
+
function isWildcardEnvEnabled() {
|
|
1163
|
+
const val = process.env.PORTLESS_WILDCARD;
|
|
1164
|
+
return val === "1" || val === "true";
|
|
1165
|
+
}
|
|
1166
|
+
function isLanEnvEnabled() {
|
|
1167
|
+
const val = process.env.PORTLESS_LAN;
|
|
1168
|
+
return val === "1" || val === "true";
|
|
1169
|
+
}
|
|
1170
|
+
function readPersistedProxyState() {
|
|
1171
|
+
const dir = process.env.PORTLESS_STATE_DIR || USER_STATE_DIR;
|
|
1172
|
+
const port = readPortFromDir(dir);
|
|
1173
|
+
if (port !== null) {
|
|
1174
|
+
const tls2 = readTlsMarker(dir);
|
|
1175
|
+
const tld = readTldFromDir(dir);
|
|
1176
|
+
const lanIp = readLanMarker(dir);
|
|
1177
|
+
return { port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local" };
|
|
1178
|
+
}
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
function buildProxyStartConfig(options) {
|
|
1182
|
+
const effectiveTld = options.lanMode ? "local" : options.tld;
|
|
1183
|
+
const args = [];
|
|
1184
|
+
if (options.foreground) {
|
|
1185
|
+
args.push("--foreground");
|
|
1186
|
+
}
|
|
1187
|
+
if (options.includePort && options.proxyPort !== void 0) {
|
|
1188
|
+
args.push("--port", options.proxyPort.toString());
|
|
1189
|
+
}
|
|
1190
|
+
if (options.useHttps) {
|
|
1191
|
+
if (options.customCertPath && options.customKeyPath) {
|
|
1192
|
+
args.push("--cert", options.customCertPath, "--key", options.customKeyPath);
|
|
1193
|
+
} else {
|
|
1194
|
+
args.push("--https");
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
args.push("--no-tls");
|
|
1198
|
+
}
|
|
1199
|
+
if (options.lanMode) {
|
|
1200
|
+
args.push("--lan");
|
|
1201
|
+
if (options.lanIp) {
|
|
1202
|
+
if (options.lanIpExplicit) {
|
|
1203
|
+
args.push("--ip", options.lanIp);
|
|
1204
|
+
} else {
|
|
1205
|
+
args.push(INTERNAL_LAN_IP_FLAG, options.lanIp);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
} else if (effectiveTld !== DEFAULT_TLD) {
|
|
1209
|
+
args.push("--tld", effectiveTld);
|
|
1210
|
+
}
|
|
1211
|
+
if (options.useWildcard) {
|
|
1212
|
+
args.push("--wildcard");
|
|
1213
|
+
}
|
|
1214
|
+
if (options.skipTrust) {
|
|
1215
|
+
args.push("--skip-trust");
|
|
1216
|
+
}
|
|
1217
|
+
return { effectiveTld, args };
|
|
1218
|
+
}
|
|
1219
|
+
async function discoverState() {
|
|
1220
|
+
if (process.env.PORTLESS_STATE_DIR) {
|
|
1221
|
+
const dir2 = process.env.PORTLESS_STATE_DIR;
|
|
1222
|
+
const port = readPortFromDir(dir2) ?? getDefaultPort();
|
|
1223
|
+
const lanIp = readLanMarker(dir2);
|
|
1224
|
+
if (await isProxyRunning(port) || await isPortListening(port)) {
|
|
1225
|
+
const tls2 = readTlsMarker(dir2);
|
|
1226
|
+
const tld = readTldFromDir(dir2);
|
|
1227
|
+
return { dir: dir2, port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local", lanIp };
|
|
1228
|
+
}
|
|
1229
|
+
return {
|
|
1230
|
+
dir: dir2,
|
|
1231
|
+
port,
|
|
1232
|
+
tls: readTlsMarker(dir2),
|
|
1233
|
+
tld: readTldFromDir(dir2),
|
|
1234
|
+
lanMode: lanIp !== null,
|
|
1235
|
+
lanIp: null
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
const userPort = readPortFromDir(USER_STATE_DIR);
|
|
1239
|
+
if (userPort !== null) {
|
|
1240
|
+
if (await isProxyRunning(userPort)) {
|
|
1241
|
+
const tls2 = readTlsMarker(USER_STATE_DIR);
|
|
1242
|
+
const tld = readTldFromDir(USER_STATE_DIR);
|
|
1243
|
+
const lanIp = readLanMarker(USER_STATE_DIR);
|
|
1244
|
+
return {
|
|
1245
|
+
dir: USER_STATE_DIR,
|
|
1246
|
+
port: userPort,
|
|
1247
|
+
tls: tls2,
|
|
1248
|
+
tld,
|
|
1249
|
+
lanMode: lanIp !== null || tld === "local",
|
|
1250
|
+
lanIp
|
|
1251
|
+
};
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const legacyPort = readPortFromDir(LEGACY_SYSTEM_STATE_DIR);
|
|
1255
|
+
if (legacyPort !== null) {
|
|
1256
|
+
if (await isProxyRunning(legacyPort)) {
|
|
1257
|
+
const tls2 = readTlsMarker(LEGACY_SYSTEM_STATE_DIR);
|
|
1258
|
+
const tld = readTldFromDir(LEGACY_SYSTEM_STATE_DIR);
|
|
1259
|
+
const lanIp = readLanMarker(LEGACY_SYSTEM_STATE_DIR);
|
|
1260
|
+
return {
|
|
1261
|
+
dir: LEGACY_SYSTEM_STATE_DIR,
|
|
1262
|
+
port: legacyPort,
|
|
1263
|
+
tls: tls2,
|
|
1264
|
+
tld,
|
|
1265
|
+
lanMode: lanIp !== null || tld === "local",
|
|
1266
|
+
lanIp
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
const configuredPort = getDefaultPort();
|
|
1271
|
+
const probePorts = /* @__PURE__ */ new Set([443, 80, FALLBACK_PROXY_PORT, configuredPort]);
|
|
1272
|
+
for (const port of probePorts) {
|
|
1273
|
+
if (await isProxyRunning(port)) {
|
|
1274
|
+
const dir2 = resolveStateDir(port);
|
|
1275
|
+
const markerTls = readTlsMarker(dir2);
|
|
1276
|
+
const tls2 = markerTls || port === getProtocolPort(true);
|
|
1277
|
+
const tld = readTldFromDir(dir2);
|
|
1278
|
+
const lanIp = readLanMarker(dir2);
|
|
1279
|
+
return { dir: dir2, port, tls: tls2, tld, lanMode: lanIp !== null || tld === "local", lanIp };
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
const dir = resolveStateDir(configuredPort);
|
|
1283
|
+
return {
|
|
1284
|
+
dir,
|
|
1285
|
+
port: configuredPort,
|
|
1286
|
+
tls: readTlsMarker(dir),
|
|
1287
|
+
tld: readTldFromDir(dir),
|
|
1288
|
+
lanMode: readLanMarker(dir) !== null,
|
|
1289
|
+
lanIp: null
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
|
|
1293
|
+
if (minPort > maxPort) {
|
|
1294
|
+
throw new Error(`minPort (${minPort}) must be <= maxPort (${maxPort})`);
|
|
1295
|
+
}
|
|
1296
|
+
const tryPort = (port) => {
|
|
1297
|
+
return new Promise((resolve3) => {
|
|
1298
|
+
const server = net.createServer();
|
|
1299
|
+
server.listen(port, () => {
|
|
1300
|
+
server.close(() => resolve3(true));
|
|
1301
|
+
});
|
|
1302
|
+
server.on("error", () => resolve3(false));
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1305
|
+
for (let i = 0; i < RANDOM_PORT_ATTEMPTS; i++) {
|
|
1306
|
+
const port = minPort + Math.floor(Math.random() * (maxPort - minPort + 1));
|
|
1307
|
+
if (!BLOCKED_PORTS.has(port) && await tryPort(port)) {
|
|
1308
|
+
return port;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
for (let port = minPort; port <= maxPort; port++) {
|
|
1312
|
+
if (!BLOCKED_PORTS.has(port) && await tryPort(port)) {
|
|
1313
|
+
return port;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
throw new Error(`No free port found in range ${minPort}-${maxPort}`);
|
|
1317
|
+
}
|
|
1318
|
+
function isProxyRunning(port, tls2 = false) {
|
|
1319
|
+
return new Promise((resolve3) => {
|
|
1320
|
+
const requestFn = tls2 ? https.request : http.request;
|
|
1321
|
+
const req = requestFn(
|
|
1322
|
+
{
|
|
1323
|
+
hostname: "127.0.0.1",
|
|
1324
|
+
port,
|
|
1325
|
+
path: "/",
|
|
1326
|
+
method: "HEAD",
|
|
1327
|
+
timeout: SOCKET_TIMEOUT_MS,
|
|
1328
|
+
...tls2 ? { rejectUnauthorized: false } : {}
|
|
1329
|
+
},
|
|
1330
|
+
(res) => {
|
|
1331
|
+
res.resume();
|
|
1332
|
+
resolve3(res.headers[PORTLESS_HEADER.toLowerCase()] === "1");
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1335
|
+
req.on("error", () => resolve3(false));
|
|
1336
|
+
req.on("timeout", () => {
|
|
1337
|
+
req.destroy();
|
|
1338
|
+
resolve3(false);
|
|
1339
|
+
});
|
|
1340
|
+
req.end();
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
function isPortListening(port) {
|
|
1344
|
+
return new Promise((resolve3) => {
|
|
1345
|
+
const socket = net.createConnection({ host: "127.0.0.1", port });
|
|
1346
|
+
let settled = false;
|
|
1347
|
+
const finish = (result) => {
|
|
1348
|
+
if (settled) return;
|
|
1349
|
+
settled = true;
|
|
1350
|
+
socket.destroy();
|
|
1351
|
+
resolve3(result);
|
|
1352
|
+
};
|
|
1353
|
+
socket.setTimeout(SOCKET_TIMEOUT_MS);
|
|
1354
|
+
socket.once("connect", () => finish(true));
|
|
1355
|
+
socket.once("error", () => finish(false));
|
|
1356
|
+
socket.once("timeout", () => finish(false));
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
function parsePidFromNetstat(output, port) {
|
|
1360
|
+
for (const line of output.split(/\r?\n/)) {
|
|
1361
|
+
if (!line.includes("LISTENING")) continue;
|
|
1362
|
+
const parts = line.trim().split(/\s+/);
|
|
1363
|
+
if (parts.length < 5) continue;
|
|
1364
|
+
const localAddr = parts[1];
|
|
1365
|
+
const lastColon = localAddr.lastIndexOf(":");
|
|
1366
|
+
if (lastColon === -1) continue;
|
|
1367
|
+
const addrPort = parseInt(localAddr.substring(lastColon + 1), 10);
|
|
1368
|
+
if (addrPort === port) {
|
|
1369
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
1370
|
+
if (!isNaN(pid) && pid > 0) return pid;
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return null;
|
|
1374
|
+
}
|
|
1375
|
+
function findPidOnPort(port) {
|
|
1376
|
+
try {
|
|
1377
|
+
if (isWindows) {
|
|
1378
|
+
const output2 = execSync("netstat -ano -p tcp", {
|
|
1379
|
+
encoding: "utf-8",
|
|
1380
|
+
timeout: PID_LOOKUP_TIMEOUT_MS
|
|
1381
|
+
});
|
|
1382
|
+
return parsePidFromNetstat(output2, port);
|
|
1383
|
+
}
|
|
1384
|
+
const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
1385
|
+
encoding: "utf-8",
|
|
1386
|
+
timeout: PID_LOOKUP_TIMEOUT_MS
|
|
1387
|
+
});
|
|
1388
|
+
const pid = parseInt(output.trim().split("\n")[0], 10);
|
|
1389
|
+
return isNaN(pid) ? null : pid;
|
|
1390
|
+
} catch {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
async function waitForProxy(port, maxAttempts = WAIT_FOR_PROXY_MAX_ATTEMPTS, intervalMs = WAIT_FOR_PROXY_INTERVAL_MS, tls2 = false) {
|
|
1395
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1396
|
+
await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
|
|
1397
|
+
if (await isProxyRunning(port, tls2)) {
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
function shellEscape(arg) {
|
|
1404
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
1405
|
+
}
|
|
1406
|
+
function collectBinPaths(cwd) {
|
|
1407
|
+
const dirs = [];
|
|
1408
|
+
let dir = cwd;
|
|
1409
|
+
for (; ; ) {
|
|
1410
|
+
const bin = path3.join(dir, "node_modules", ".bin");
|
|
1411
|
+
if (fs3.existsSync(bin)) {
|
|
1412
|
+
dirs.push(bin);
|
|
1413
|
+
}
|
|
1414
|
+
const parent = path3.dirname(dir);
|
|
1415
|
+
if (parent === dir) break;
|
|
1416
|
+
dir = parent;
|
|
1417
|
+
}
|
|
1418
|
+
return dirs;
|
|
1419
|
+
}
|
|
1420
|
+
function augmentedPath(env, cwd) {
|
|
1421
|
+
const source = env ?? process.env;
|
|
1422
|
+
const base = source.PATH ?? source.Path ?? "";
|
|
1423
|
+
const bins = collectBinPaths(cwd ?? process.cwd());
|
|
1424
|
+
const nodeBin = path3.dirname(process.execPath);
|
|
1425
|
+
const allBins = [...bins, nodeBin];
|
|
1426
|
+
return allBins.join(path3.delimiter) + path3.delimiter + base;
|
|
1427
|
+
}
|
|
1428
|
+
function spawnCommand(commandArgs, options) {
|
|
1429
|
+
const env = {
|
|
1430
|
+
...options?.env ?? process.env,
|
|
1431
|
+
PATH: augmentedPath(options?.env)
|
|
1432
|
+
};
|
|
1433
|
+
if (isWindows) {
|
|
1434
|
+
for (const key of Object.keys(env)) {
|
|
1435
|
+
if (key !== "PATH" && key.toUpperCase() === "PATH") {
|
|
1436
|
+
delete env[key];
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
const child = isWindows ? spawn("cmd.exe", ["/d", "/s", "/c", commandArgs.join(" ")], {
|
|
1441
|
+
stdio: "inherit",
|
|
1442
|
+
env
|
|
1443
|
+
}) : spawn("/bin/sh", ["-c", commandArgs.map(shellEscape).join(" ")], {
|
|
1444
|
+
stdio: "inherit",
|
|
1445
|
+
env
|
|
1446
|
+
});
|
|
1447
|
+
let exiting = false;
|
|
1448
|
+
const cleanup = () => {
|
|
1449
|
+
process.removeListener("SIGINT", onSigInt);
|
|
1450
|
+
process.removeListener("SIGTERM", onSigTerm);
|
|
1451
|
+
options?.onCleanup?.();
|
|
1452
|
+
};
|
|
1453
|
+
const handleSignal = (signal) => {
|
|
1454
|
+
if (exiting) return;
|
|
1455
|
+
exiting = true;
|
|
1456
|
+
child.kill(signal);
|
|
1457
|
+
cleanup();
|
|
1458
|
+
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
1459
|
+
};
|
|
1460
|
+
const onSigInt = () => handleSignal("SIGINT");
|
|
1461
|
+
const onSigTerm = () => handleSignal("SIGTERM");
|
|
1462
|
+
process.on("SIGINT", onSigInt);
|
|
1463
|
+
process.on("SIGTERM", onSigTerm);
|
|
1464
|
+
child.on("error", (err) => {
|
|
1465
|
+
if (exiting) return;
|
|
1466
|
+
exiting = true;
|
|
1467
|
+
console.error(`Failed to run command: ${err.message}`);
|
|
1468
|
+
if (err.code === "ENOENT") {
|
|
1469
|
+
console.error(`Is "${commandArgs[0]}" installed and in your PATH?`);
|
|
1470
|
+
}
|
|
1471
|
+
cleanup();
|
|
1472
|
+
process.exit(1);
|
|
1473
|
+
});
|
|
1474
|
+
child.on("exit", (code, signal) => {
|
|
1475
|
+
if (exiting) return;
|
|
1476
|
+
exiting = true;
|
|
1477
|
+
cleanup();
|
|
1478
|
+
if (signal) {
|
|
1479
|
+
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
1480
|
+
}
|
|
1481
|
+
process.exit(code ?? 1);
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
var FRAMEWORKS_NEEDING_PORT = {
|
|
1485
|
+
vite: { strictPort: true },
|
|
1486
|
+
vp: { strictPort: true },
|
|
1487
|
+
"react-router": { strictPort: true },
|
|
1488
|
+
rsbuild: { strictPort: false },
|
|
1489
|
+
astro: { strictPort: false },
|
|
1490
|
+
ng: { strictPort: false },
|
|
1491
|
+
"react-native": { strictPort: false },
|
|
1492
|
+
expo: { strictPort: false }
|
|
1493
|
+
};
|
|
1494
|
+
var PACKAGE_RUNNERS = {
|
|
1495
|
+
npx: [],
|
|
1496
|
+
bunx: [],
|
|
1497
|
+
pnpx: [],
|
|
1498
|
+
yarn: ["dlx", "exec"],
|
|
1499
|
+
pnpm: ["dlx", "exec"]
|
|
1500
|
+
};
|
|
1501
|
+
function findFrameworkBasename(commandArgs) {
|
|
1502
|
+
if (commandArgs.length === 0) return null;
|
|
1503
|
+
const first = path3.basename(commandArgs[0]);
|
|
1504
|
+
if (FRAMEWORKS_NEEDING_PORT[first]) return first;
|
|
1505
|
+
const subcommands = PACKAGE_RUNNERS[first];
|
|
1506
|
+
if (!subcommands) return null;
|
|
1507
|
+
let i = 1;
|
|
1508
|
+
if (subcommands.length > 0) {
|
|
1509
|
+
while (i < commandArgs.length && commandArgs[i].startsWith("-")) i++;
|
|
1510
|
+
if (i >= commandArgs.length) return null;
|
|
1511
|
+
if (!subcommands.includes(commandArgs[i])) {
|
|
1512
|
+
const name2 = path3.basename(commandArgs[i]);
|
|
1513
|
+
return FRAMEWORKS_NEEDING_PORT[name2] ? name2 : null;
|
|
1514
|
+
}
|
|
1515
|
+
i++;
|
|
1516
|
+
}
|
|
1517
|
+
while (i < commandArgs.length && commandArgs[i].startsWith("-")) i++;
|
|
1518
|
+
if (i >= commandArgs.length) return null;
|
|
1519
|
+
const name = path3.basename(commandArgs[i]);
|
|
1520
|
+
return FRAMEWORKS_NEEDING_PORT[name] ? name : null;
|
|
1521
|
+
}
|
|
1522
|
+
function injectFrameworkFlags(commandArgs, port) {
|
|
1523
|
+
const basename5 = findFrameworkBasename(commandArgs);
|
|
1524
|
+
if (!basename5) return;
|
|
1525
|
+
const framework = FRAMEWORKS_NEEDING_PORT[basename5];
|
|
1526
|
+
if (!commandArgs.includes("--port")) {
|
|
1527
|
+
commandArgs.push("--port", port.toString());
|
|
1528
|
+
if (framework.strictPort) {
|
|
1529
|
+
commandArgs.push("--strictPort");
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
if (!commandArgs.includes("--host")) {
|
|
1533
|
+
const isExpoLan = basename5 === "expo" && isLanEnvEnabled();
|
|
1534
|
+
if (isExpoLan) return;
|
|
1535
|
+
const hostValue = basename5 === "expo" ? "localhost" : "127.0.0.1";
|
|
1536
|
+
commandArgs.push("--host", hostValue);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// src/clean-utils.ts
|
|
1541
|
+
import * as fs4 from "fs";
|
|
1542
|
+
import * as path4 from "path";
|
|
923
1543
|
var PORTLESS_STATE_FILES = [
|
|
924
1544
|
"routes.json",
|
|
925
1545
|
"routes.lock",
|
|
@@ -943,29 +1563,29 @@ function collectStateDirsForCleanup() {
|
|
|
943
1563
|
const add = (d) => {
|
|
944
1564
|
const trimmed = d?.trim();
|
|
945
1565
|
if (!trimmed) return;
|
|
946
|
-
const resolved =
|
|
947
|
-
if (
|
|
1566
|
+
const resolved = path4.resolve(trimmed);
|
|
1567
|
+
if (fs4.existsSync(resolved)) dirs.add(resolved);
|
|
948
1568
|
};
|
|
949
1569
|
add(USER_STATE_DIR);
|
|
950
|
-
add(
|
|
1570
|
+
add(LEGACY_SYSTEM_STATE_DIR);
|
|
951
1571
|
add(process.env.PORTLESS_STATE_DIR);
|
|
952
1572
|
return [...dirs];
|
|
953
1573
|
}
|
|
954
1574
|
function removePortlessStateFiles(dir) {
|
|
955
1575
|
for (const f of PORTLESS_STATE_FILES) {
|
|
956
1576
|
try {
|
|
957
|
-
|
|
1577
|
+
fs4.unlinkSync(path4.join(dir, f));
|
|
958
1578
|
} catch {
|
|
959
1579
|
}
|
|
960
1580
|
}
|
|
961
1581
|
try {
|
|
962
|
-
|
|
1582
|
+
fs4.rmSync(path4.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
|
|
963
1583
|
} catch {
|
|
964
1584
|
}
|
|
965
1585
|
}
|
|
966
1586
|
|
|
967
1587
|
// src/mdns.ts
|
|
968
|
-
import { spawn, spawnSync } from "child_process";
|
|
1588
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
969
1589
|
|
|
970
1590
|
// src/lan-ip.ts
|
|
971
1591
|
import { createSocket } from "dgram";
|
|
@@ -1140,7 +1760,7 @@ function publish(hostname, port, ip, onError) {
|
|
|
1140
1760
|
if (!publisher) {
|
|
1141
1761
|
return;
|
|
1142
1762
|
}
|
|
1143
|
-
const child =
|
|
1763
|
+
const child = spawn2(publisher.command, publisher.buildArgs(fqdn, name, port, ip), {
|
|
1144
1764
|
stdio: "ignore",
|
|
1145
1765
|
detached: false
|
|
1146
1766
|
});
|
|
@@ -1167,6 +1787,528 @@ function cleanupAll() {
|
|
|
1167
1787
|
activePublishers.clear();
|
|
1168
1788
|
}
|
|
1169
1789
|
|
|
1790
|
+
// src/config.ts
|
|
1791
|
+
import * as fs5 from "fs";
|
|
1792
|
+
import * as path5 from "path";
|
|
1793
|
+
var ConfigValidationError = class extends Error {
|
|
1794
|
+
constructor(message) {
|
|
1795
|
+
super(message);
|
|
1796
|
+
this.name = "ConfigValidationError";
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
var CONFIG_FILENAME = "portless.json";
|
|
1800
|
+
function loadConfig(cwd = process.cwd()) {
|
|
1801
|
+
const configPath = path5.join(cwd, CONFIG_FILENAME);
|
|
1802
|
+
try {
|
|
1803
|
+
const raw = fs5.readFileSync(configPath, "utf-8");
|
|
1804
|
+
const parsed = JSON.parse(raw);
|
|
1805
|
+
validateConfig(parsed, configPath);
|
|
1806
|
+
return { config: parsed, configDir: cwd };
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
if (isErrnoException2(err) && err.code === "ENOENT") {
|
|
1809
|
+
return loadConfigFromPackageJson(cwd);
|
|
1810
|
+
}
|
|
1811
|
+
if (err instanceof SyntaxError) {
|
|
1812
|
+
throw new ConfigValidationError(`Invalid JSON in ${configPath}`);
|
|
1813
|
+
}
|
|
1814
|
+
throw err;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function normalizePortlessValue(value) {
|
|
1818
|
+
if (typeof value === "string") {
|
|
1819
|
+
return value.trim() ? { name: value.trim() } : null;
|
|
1820
|
+
}
|
|
1821
|
+
return value;
|
|
1822
|
+
}
|
|
1823
|
+
function loadConfigFromPackageJson(dir) {
|
|
1824
|
+
const pkgPath = path5.join(dir, "package.json");
|
|
1825
|
+
try {
|
|
1826
|
+
const raw = fs5.readFileSync(pkgPath, "utf-8");
|
|
1827
|
+
const pkg = JSON.parse(raw);
|
|
1828
|
+
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
1829
|
+
const config = normalizePortlessValue(pkg.portless);
|
|
1830
|
+
if (config === null) return null;
|
|
1831
|
+
validateConfig(config, `${pkgPath} "portless"`);
|
|
1832
|
+
return { config, configDir: dir };
|
|
1833
|
+
}
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
if (isErrnoException2(err) && err.code === "ENOENT") return null;
|
|
1836
|
+
if (err instanceof SyntaxError) return null;
|
|
1837
|
+
throw err;
|
|
1838
|
+
}
|
|
1839
|
+
return null;
|
|
1840
|
+
}
|
|
1841
|
+
function loadPackagePortlessConfig(dir) {
|
|
1842
|
+
const pkgPath = path5.join(dir, "package.json");
|
|
1843
|
+
try {
|
|
1844
|
+
const raw = fs5.readFileSync(pkgPath, "utf-8");
|
|
1845
|
+
const pkg = JSON.parse(raw);
|
|
1846
|
+
if (pkg && typeof pkg === "object" && "portless" in pkg) {
|
|
1847
|
+
const config = normalizePortlessValue(pkg.portless);
|
|
1848
|
+
if (config === null) return null;
|
|
1849
|
+
if (typeof config === "object" && !Array.isArray(config)) {
|
|
1850
|
+
validateAppConfig(config, "portless", pkgPath);
|
|
1851
|
+
return config;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
} catch (err) {
|
|
1855
|
+
if (err instanceof ConfigValidationError) throw err;
|
|
1856
|
+
}
|
|
1857
|
+
return null;
|
|
1858
|
+
}
|
|
1859
|
+
function resolveAppConfig(config, configDir, packageDir) {
|
|
1860
|
+
if (config.apps) {
|
|
1861
|
+
const rel = normalizePath(path5.relative(configDir, packageDir));
|
|
1862
|
+
if (rel && !rel.startsWith("..")) {
|
|
1863
|
+
let candidate = rel;
|
|
1864
|
+
while (candidate) {
|
|
1865
|
+
if (config.apps[candidate]) {
|
|
1866
|
+
return config.apps[candidate];
|
|
1867
|
+
}
|
|
1868
|
+
const parent = path5.dirname(candidate);
|
|
1869
|
+
if (parent === "." || parent === candidate) break;
|
|
1870
|
+
candidate = normalizePath(parent);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
return {};
|
|
1874
|
+
}
|
|
1875
|
+
return { name: config.name, script: config.script, appPort: config.appPort, proxy: config.proxy };
|
|
1876
|
+
}
|
|
1877
|
+
function hasScript(scriptName, dir) {
|
|
1878
|
+
const pkgPath = path5.join(dir, "package.json");
|
|
1879
|
+
try {
|
|
1880
|
+
const raw = fs5.readFileSync(pkgPath, "utf-8");
|
|
1881
|
+
const pkg = JSON.parse(raw);
|
|
1882
|
+
return typeof pkg?.scripts?.[scriptName] === "string";
|
|
1883
|
+
} catch {
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
var LOCK_FILES = [
|
|
1888
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
1889
|
+
["yarn.lock", "yarn"],
|
|
1890
|
+
["bun.lockb", "bun"],
|
|
1891
|
+
["bun.lock", "bun"],
|
|
1892
|
+
["package-lock.json", "npm"]
|
|
1893
|
+
];
|
|
1894
|
+
function detectPackageManager(cwd) {
|
|
1895
|
+
let dir = cwd;
|
|
1896
|
+
for (; ; ) {
|
|
1897
|
+
const pkgPath = path5.join(dir, "package.json");
|
|
1898
|
+
try {
|
|
1899
|
+
const raw = fs5.readFileSync(pkgPath, "utf-8");
|
|
1900
|
+
const pkg = JSON.parse(raw);
|
|
1901
|
+
if (typeof pkg.packageManager === "string") {
|
|
1902
|
+
const name = pkg.packageManager.split("@")[0];
|
|
1903
|
+
if (name === "pnpm" || name === "yarn" || name === "bun" || name === "npm") {
|
|
1904
|
+
return name;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
} catch {
|
|
1908
|
+
}
|
|
1909
|
+
for (const [file, pm] of LOCK_FILES) {
|
|
1910
|
+
try {
|
|
1911
|
+
fs5.accessSync(path5.join(dir, file), fs5.constants.F_OK);
|
|
1912
|
+
return pm;
|
|
1913
|
+
} catch {
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
const parent = path5.dirname(dir);
|
|
1917
|
+
if (parent === dir) break;
|
|
1918
|
+
dir = parent;
|
|
1919
|
+
}
|
|
1920
|
+
return "npm";
|
|
1921
|
+
}
|
|
1922
|
+
function resolveScriptCommand(scriptName, packageDir) {
|
|
1923
|
+
if (!hasScript(scriptName, packageDir)) return null;
|
|
1924
|
+
const pm = detectPackageManager(packageDir);
|
|
1925
|
+
return [pm, "run", scriptName];
|
|
1926
|
+
}
|
|
1927
|
+
function splitCommand(command) {
|
|
1928
|
+
const args = [];
|
|
1929
|
+
let current = "";
|
|
1930
|
+
let inSingle = false;
|
|
1931
|
+
let inDouble = false;
|
|
1932
|
+
let escaped = false;
|
|
1933
|
+
for (const ch of command) {
|
|
1934
|
+
if (escaped) {
|
|
1935
|
+
current += ch;
|
|
1936
|
+
escaped = false;
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
if (ch === "\\" && !inSingle) {
|
|
1940
|
+
escaped = true;
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
if (ch === "'" && !inDouble) {
|
|
1944
|
+
inSingle = !inSingle;
|
|
1945
|
+
} else if (ch === '"' && !inSingle) {
|
|
1946
|
+
inDouble = !inDouble;
|
|
1947
|
+
} else if (/\s/.test(ch) && !inSingle && !inDouble) {
|
|
1948
|
+
if (current) {
|
|
1949
|
+
args.push(current);
|
|
1950
|
+
current = "";
|
|
1951
|
+
}
|
|
1952
|
+
} else {
|
|
1953
|
+
current += ch;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (current) args.push(current);
|
|
1957
|
+
return args;
|
|
1958
|
+
}
|
|
1959
|
+
var BUILD_ONLY_COMMANDS = /* @__PURE__ */ new Set([
|
|
1960
|
+
"tsup",
|
|
1961
|
+
"tsc",
|
|
1962
|
+
"esbuild",
|
|
1963
|
+
"rollup",
|
|
1964
|
+
"babel",
|
|
1965
|
+
"swc",
|
|
1966
|
+
"unbuild",
|
|
1967
|
+
"pkgroll",
|
|
1968
|
+
"ncc",
|
|
1969
|
+
"microbundle"
|
|
1970
|
+
]);
|
|
1971
|
+
function isServerCommand(args) {
|
|
1972
|
+
if (args.length === 0) return false;
|
|
1973
|
+
const bin = path5.basename(args[0]);
|
|
1974
|
+
return !BUILD_ONLY_COMMANDS.has(bin);
|
|
1975
|
+
}
|
|
1976
|
+
function normalizePath(p) {
|
|
1977
|
+
return p.replace(/\\/g, "/");
|
|
1978
|
+
}
|
|
1979
|
+
function isErrnoException2(err) {
|
|
1980
|
+
return err instanceof Error && "code" in err;
|
|
1981
|
+
}
|
|
1982
|
+
var KNOWN_TOP_KEYS = /* @__PURE__ */ new Set(["name", "script", "appPort", "proxy", "apps", "turbo"]);
|
|
1983
|
+
var KNOWN_APP_KEYS = /* @__PURE__ */ new Set(["name", "script", "appPort", "proxy"]);
|
|
1984
|
+
function validateConfig(config, configPath) {
|
|
1985
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
1986
|
+
throw new ConfigValidationError(`${configPath} must be a JSON object.`);
|
|
1987
|
+
}
|
|
1988
|
+
const obj = config;
|
|
1989
|
+
if (obj.name !== void 0) {
|
|
1990
|
+
if (typeof obj.name !== "string" || !obj.name.trim()) {
|
|
1991
|
+
throw new ConfigValidationError(`"name" in ${configPath} must be a non-empty string.`);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (obj.script !== void 0) {
|
|
1995
|
+
if (typeof obj.script !== "string" || !obj.script.trim()) {
|
|
1996
|
+
throw new ConfigValidationError(`"script" in ${configPath} must be a non-empty string.`);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
if (obj.appPort !== void 0) {
|
|
2000
|
+
if (typeof obj.appPort !== "number" || !Number.isInteger(obj.appPort) || obj.appPort < 1 || obj.appPort > 65535) {
|
|
2001
|
+
throw new ConfigValidationError(
|
|
2002
|
+
`"appPort" in ${configPath} must be an integer between 1 and 65535.`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
if (obj.proxy !== void 0) {
|
|
2007
|
+
if (typeof obj.proxy !== "boolean") {
|
|
2008
|
+
throw new ConfigValidationError(`"proxy" in ${configPath} must be a boolean.`);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (obj.turbo !== void 0) {
|
|
2012
|
+
if (typeof obj.turbo !== "boolean") {
|
|
2013
|
+
throw new ConfigValidationError(`"turbo" in ${configPath} must be a boolean.`);
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
if (obj.apps !== void 0) {
|
|
2017
|
+
if (typeof obj.apps !== "object" || obj.apps === null || Array.isArray(obj.apps)) {
|
|
2018
|
+
throw new ConfigValidationError(`"apps" in ${configPath} must be an object.`);
|
|
2019
|
+
}
|
|
2020
|
+
for (const [key, value] of Object.entries(obj.apps)) {
|
|
2021
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2022
|
+
throw new ConfigValidationError(`"apps.${key}" in ${configPath} must be an object.`);
|
|
2023
|
+
}
|
|
2024
|
+
validateAppConfig(value, `apps.${key}`, configPath);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
warnUnknownKeys(obj, KNOWN_TOP_KEYS, configPath);
|
|
2028
|
+
}
|
|
2029
|
+
function validateAppConfig(obj, prefix, configPath) {
|
|
2030
|
+
if (obj.name !== void 0) {
|
|
2031
|
+
if (typeof obj.name !== "string" || !obj.name.trim()) {
|
|
2032
|
+
throw new ConfigValidationError(
|
|
2033
|
+
`"${prefix}.name" in ${configPath} must be a non-empty string.`
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
if (obj.script !== void 0) {
|
|
2038
|
+
if (typeof obj.script !== "string" || !obj.script.trim()) {
|
|
2039
|
+
throw new ConfigValidationError(
|
|
2040
|
+
`"${prefix}.script" in ${configPath} must be a non-empty string.`
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
if (obj.appPort !== void 0) {
|
|
2045
|
+
if (typeof obj.appPort !== "number" || !Number.isInteger(obj.appPort) || obj.appPort < 1 || obj.appPort > 65535) {
|
|
2046
|
+
throw new ConfigValidationError(
|
|
2047
|
+
`"${prefix}.appPort" in ${configPath} must be an integer between 1 and 65535.`
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
if (obj.proxy !== void 0) {
|
|
2052
|
+
if (typeof obj.proxy !== "boolean") {
|
|
2053
|
+
throw new ConfigValidationError(`"${prefix}.proxy" in ${configPath} must be a boolean.`);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
warnUnknownKeys(obj, KNOWN_APP_KEYS, configPath, prefix);
|
|
2057
|
+
}
|
|
2058
|
+
function warnUnknownKeys(obj, known, configPath, prefix) {
|
|
2059
|
+
for (const key of Object.keys(obj)) {
|
|
2060
|
+
if (!known.has(key)) {
|
|
2061
|
+
const label = prefix ? `"${prefix}.${key}"` : `"${key}"`;
|
|
2062
|
+
console.warn(
|
|
2063
|
+
`Warning: Unknown key ${label} in ${configPath}. Known keys: ${[...known].join(", ")}`
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
// src/workspace.ts
|
|
2070
|
+
import * as fs6 from "fs";
|
|
2071
|
+
import * as path6 from "path";
|
|
2072
|
+
function findWorkspaceRoot(cwd = process.cwd()) {
|
|
2073
|
+
let dir = cwd;
|
|
2074
|
+
for (; ; ) {
|
|
2075
|
+
try {
|
|
2076
|
+
fs6.accessSync(path6.join(dir, "pnpm-workspace.yaml"), fs6.constants.R_OK);
|
|
2077
|
+
return dir;
|
|
2078
|
+
} catch {
|
|
2079
|
+
}
|
|
2080
|
+
if (readWorkspacesFromPackageJson(dir) !== null) {
|
|
2081
|
+
return dir;
|
|
2082
|
+
}
|
|
2083
|
+
const parent = path6.dirname(dir);
|
|
2084
|
+
if (parent === dir) return null;
|
|
2085
|
+
dir = parent;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
function detectWorkspaceSource(workspaceRoot) {
|
|
2089
|
+
try {
|
|
2090
|
+
fs6.accessSync(path6.join(workspaceRoot, "pnpm-workspace.yaml"), fs6.constants.R_OK);
|
|
2091
|
+
return "pnpm";
|
|
2092
|
+
} catch {
|
|
2093
|
+
}
|
|
2094
|
+
if (readWorkspacesFromPackageJson(workspaceRoot) !== null) {
|
|
2095
|
+
return "package-json";
|
|
2096
|
+
}
|
|
2097
|
+
return null;
|
|
2098
|
+
}
|
|
2099
|
+
function readWorkspacesFromPackageJson(dir) {
|
|
2100
|
+
const pkgPath = path6.join(dir, "package.json");
|
|
2101
|
+
try {
|
|
2102
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2103
|
+
const pkg = JSON.parse(raw);
|
|
2104
|
+
if (!pkg || typeof pkg !== "object") return null;
|
|
2105
|
+
const ws = pkg.workspaces;
|
|
2106
|
+
if (Array.isArray(ws)) {
|
|
2107
|
+
return ws.filter((g) => typeof g === "string");
|
|
2108
|
+
}
|
|
2109
|
+
if (ws && typeof ws === "object" && !Array.isArray(ws) && Array.isArray(ws.packages)) {
|
|
2110
|
+
return ws.packages.filter((g) => typeof g === "string");
|
|
2111
|
+
}
|
|
2112
|
+
} catch {
|
|
2113
|
+
}
|
|
2114
|
+
return null;
|
|
2115
|
+
}
|
|
2116
|
+
function discoverWorkspacePackages(workspaceRoot) {
|
|
2117
|
+
const source = detectWorkspaceSource(workspaceRoot);
|
|
2118
|
+
let globs;
|
|
2119
|
+
if (source === "pnpm") {
|
|
2120
|
+
const wsPath = path6.join(workspaceRoot, "pnpm-workspace.yaml");
|
|
2121
|
+
let content;
|
|
2122
|
+
try {
|
|
2123
|
+
content = fs6.readFileSync(wsPath, "utf-8");
|
|
2124
|
+
} catch {
|
|
2125
|
+
return [];
|
|
2126
|
+
}
|
|
2127
|
+
globs = parsePnpmWorkspaceYaml(content);
|
|
2128
|
+
} else if (source === "package-json") {
|
|
2129
|
+
globs = readWorkspacesFromPackageJson(workspaceRoot) ?? [];
|
|
2130
|
+
} else {
|
|
2131
|
+
return [];
|
|
2132
|
+
}
|
|
2133
|
+
const dirs = expandPackageGlobs(workspaceRoot, globs);
|
|
2134
|
+
const packages = [];
|
|
2135
|
+
for (const dir of dirs) {
|
|
2136
|
+
const pkgPath = path6.join(dir, "package.json");
|
|
2137
|
+
try {
|
|
2138
|
+
const raw = fs6.readFileSync(pkgPath, "utf-8");
|
|
2139
|
+
const pkg = JSON.parse(raw);
|
|
2140
|
+
const rawName = typeof pkg.name === "string" ? pkg.name : null;
|
|
2141
|
+
const scopeMatch = rawName?.match(/^@([^/]+)\//);
|
|
2142
|
+
const scope = scopeMatch ? scopeMatch[1] : null;
|
|
2143
|
+
const name = rawName ? rawName.replace(/^@[^/]+\//, "") : null;
|
|
2144
|
+
const scripts = typeof pkg.scripts === "object" && pkg.scripts !== null ? pkg.scripts : {};
|
|
2145
|
+
packages.push({ dir, name, scope, scripts });
|
|
2146
|
+
} catch {
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return packages;
|
|
2150
|
+
}
|
|
2151
|
+
function parsePnpmWorkspaceYaml(content) {
|
|
2152
|
+
const lines = content.split("\n");
|
|
2153
|
+
const globs = [];
|
|
2154
|
+
let inPackages = false;
|
|
2155
|
+
for (const rawLine of lines) {
|
|
2156
|
+
const line = rawLine.trimEnd();
|
|
2157
|
+
const headerMatch = line.match(/^packages\s*:(.*)/);
|
|
2158
|
+
if (headerMatch) {
|
|
2159
|
+
const rest = headerMatch[1].trim();
|
|
2160
|
+
if (rest.startsWith("[")) {
|
|
2161
|
+
return parseFlowSequence(rest);
|
|
2162
|
+
}
|
|
2163
|
+
inPackages = true;
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
if (inPackages) {
|
|
2167
|
+
if (line.length > 0 && !line.startsWith(" ") && !line.startsWith(" ") && !line.startsWith("-")) {
|
|
2168
|
+
break;
|
|
2169
|
+
}
|
|
2170
|
+
const trimmed = line.trim();
|
|
2171
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
2172
|
+
const match = trimmed.match(/^-\s+['"]?([^'"#]+?)['"]?\s*(?:#.*)?$/);
|
|
2173
|
+
if (match) {
|
|
2174
|
+
const glob = match[1].trim();
|
|
2175
|
+
if (glob) globs.push(glob);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
return globs;
|
|
2180
|
+
}
|
|
2181
|
+
function parseFlowSequence(input) {
|
|
2182
|
+
const inner = input.replace(/^\[/, "").replace(/]\s*$/, "");
|
|
2183
|
+
return inner.split(",").map((s) => s.trim().replace(/^['"]/, "").replace(/['"]$/, "").trim()).filter(Boolean);
|
|
2184
|
+
}
|
|
2185
|
+
function expandPackageGlobs(root, globs) {
|
|
2186
|
+
const included = /* @__PURE__ */ new Set();
|
|
2187
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
2188
|
+
for (const glob of globs) {
|
|
2189
|
+
if (glob.startsWith("!")) {
|
|
2190
|
+
const negated = glob.slice(1);
|
|
2191
|
+
for (const dir of expandSingleGlob(root, negated)) {
|
|
2192
|
+
excluded.add(dir);
|
|
2193
|
+
}
|
|
2194
|
+
} else {
|
|
2195
|
+
for (const dir of expandSingleGlob(root, glob)) {
|
|
2196
|
+
included.add(dir);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
for (const dir of excluded) {
|
|
2201
|
+
included.delete(dir);
|
|
2202
|
+
}
|
|
2203
|
+
return [...included].sort();
|
|
2204
|
+
}
|
|
2205
|
+
function segmentMatches(pattern, name) {
|
|
2206
|
+
if (pattern === "*" || pattern === "**") return true;
|
|
2207
|
+
const starIdx = pattern.indexOf("*");
|
|
2208
|
+
if (starIdx === -1) return pattern === name;
|
|
2209
|
+
const prefix = pattern.slice(0, starIdx);
|
|
2210
|
+
const suffix = pattern.slice(starIdx + 1).replace(/\*+$/, "");
|
|
2211
|
+
return name.startsWith(prefix) && name.endsWith(suffix);
|
|
2212
|
+
}
|
|
2213
|
+
function expandSingleGlob(root, glob) {
|
|
2214
|
+
const segments = glob.split("/");
|
|
2215
|
+
return expandSegments(root, segments);
|
|
2216
|
+
}
|
|
2217
|
+
function expandSegments(base, segments) {
|
|
2218
|
+
if (segments.length === 0) {
|
|
2219
|
+
try {
|
|
2220
|
+
const stat = fs6.statSync(base);
|
|
2221
|
+
if (stat.isDirectory()) return [base];
|
|
2222
|
+
} catch {
|
|
2223
|
+
}
|
|
2224
|
+
return [];
|
|
2225
|
+
}
|
|
2226
|
+
const [current, ...rest] = segments;
|
|
2227
|
+
if (current.includes("*")) {
|
|
2228
|
+
try {
|
|
2229
|
+
const entries = fs6.readdirSync(base, { withFileTypes: true });
|
|
2230
|
+
const matched = entries.filter((e) => e.isDirectory() && segmentMatches(current, e.name));
|
|
2231
|
+
if (rest.length === 0) {
|
|
2232
|
+
return matched.map((e) => path6.join(base, e.name));
|
|
2233
|
+
}
|
|
2234
|
+
const results = [];
|
|
2235
|
+
for (const entry of matched) {
|
|
2236
|
+
results.push(...expandSegments(path6.join(base, entry.name), rest));
|
|
2237
|
+
}
|
|
2238
|
+
return results;
|
|
2239
|
+
} catch {
|
|
2240
|
+
return [];
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return expandSegments(path6.join(base, current), rest);
|
|
2244
|
+
}
|
|
2245
|
+
|
|
2246
|
+
// src/turbo.ts
|
|
2247
|
+
import * as fs7 from "fs";
|
|
2248
|
+
import * as path7 from "path";
|
|
2249
|
+
var LOADER_FILENAME = "turbo-env-loader.cjs";
|
|
2250
|
+
var MANIFEST_FILENAME = "dev-manifest.json";
|
|
2251
|
+
function loaderPath(baseDir = USER_STATE_DIR) {
|
|
2252
|
+
return path7.join(baseDir, LOADER_FILENAME);
|
|
2253
|
+
}
|
|
2254
|
+
function manifestPath(baseDir = USER_STATE_DIR) {
|
|
2255
|
+
return path7.join(baseDir, MANIFEST_FILENAME);
|
|
2256
|
+
}
|
|
2257
|
+
function loaderSource(baseDir = USER_STATE_DIR) {
|
|
2258
|
+
return `"use strict";
|
|
2259
|
+
var fs = require("fs");
|
|
2260
|
+
var path = require("path");
|
|
2261
|
+
var manifestPath = path.join(${JSON.stringify(baseDir)}, "dev-manifest.json");
|
|
2262
|
+
try {
|
|
2263
|
+
var raw = fs.readFileSync(manifestPath, "utf-8");
|
|
2264
|
+
var manifest = JSON.parse(raw);
|
|
2265
|
+
var cwd = process.cwd();
|
|
2266
|
+
var entry = manifest[cwd];
|
|
2267
|
+
if (entry && typeof entry === "object") {
|
|
2268
|
+
var keys = Object.keys(entry);
|
|
2269
|
+
for (var i = 0; i < keys.length; i++) {
|
|
2270
|
+
process.env[keys[i]] = entry[keys[i]];
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
} catch (_) {}
|
|
2274
|
+
`;
|
|
2275
|
+
}
|
|
2276
|
+
function ensureEnvLoader(baseDir = USER_STATE_DIR) {
|
|
2277
|
+
fs7.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2278
|
+
const target = loaderPath(baseDir);
|
|
2279
|
+
const source = loaderSource(baseDir);
|
|
2280
|
+
try {
|
|
2281
|
+
const existing = fs7.readFileSync(target, "utf-8");
|
|
2282
|
+
if (existing === source) return;
|
|
2283
|
+
} catch {
|
|
2284
|
+
}
|
|
2285
|
+
fs7.writeFileSync(target, source, { mode: 420 });
|
|
2286
|
+
}
|
|
2287
|
+
function writeManifest(entries, baseDir = USER_STATE_DIR) {
|
|
2288
|
+
fs7.mkdirSync(baseDir, { recursive: true, mode: 493 });
|
|
2289
|
+
fs7.writeFileSync(manifestPath(baseDir), JSON.stringify(entries, null, 2) + "\n", { mode: 420 });
|
|
2290
|
+
}
|
|
2291
|
+
function removeManifest(baseDir = USER_STATE_DIR) {
|
|
2292
|
+
try {
|
|
2293
|
+
fs7.unlinkSync(manifestPath(baseDir));
|
|
2294
|
+
} catch {
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
function buildNodeOptions(baseDir = USER_STATE_DIR) {
|
|
2298
|
+
const existing = process.env.NODE_OPTIONS || "";
|
|
2299
|
+
const lp = loaderPath(baseDir);
|
|
2300
|
+
const requireFlag = lp.includes(" ") ? `--require "${lp}"` : `--require ${lp}`;
|
|
2301
|
+
return existing ? `${requireFlag} ${existing}` : requireFlag;
|
|
2302
|
+
}
|
|
2303
|
+
function hasTurboConfig(wsRoot) {
|
|
2304
|
+
try {
|
|
2305
|
+
fs7.accessSync(path7.join(wsRoot, "turbo.json"), fs7.constants.R_OK);
|
|
2306
|
+
return true;
|
|
2307
|
+
} catch {
|
|
2308
|
+
return false;
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1170
2312
|
// src/cli.ts
|
|
1171
2313
|
var chalk = colors_default;
|
|
1172
2314
|
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
@@ -1319,10 +2461,10 @@ function getEntryScript() {
|
|
|
1319
2461
|
function isLocallyInstalled() {
|
|
1320
2462
|
let dir = process.cwd();
|
|
1321
2463
|
for (; ; ) {
|
|
1322
|
-
if (
|
|
2464
|
+
if (fs8.existsSync(path8.join(dir, "node_modules", "portless", "package.json"))) {
|
|
1323
2465
|
return true;
|
|
1324
2466
|
}
|
|
1325
|
-
const parent =
|
|
2467
|
+
const parent = path8.dirname(dir);
|
|
1326
2468
|
if (parent === dir) break;
|
|
1327
2469
|
dir = parent;
|
|
1328
2470
|
}
|
|
@@ -1358,11 +2500,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1358
2500
|
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
1359
2501
|
}
|
|
1360
2502
|
const routesPath = store.getRoutesPath();
|
|
1361
|
-
if (!
|
|
1362
|
-
|
|
2503
|
+
if (!fs8.existsSync(routesPath)) {
|
|
2504
|
+
fs8.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
1363
2505
|
}
|
|
1364
2506
|
try {
|
|
1365
|
-
|
|
2507
|
+
fs8.chmodSync(routesPath, FILE_MODE);
|
|
1366
2508
|
} catch {
|
|
1367
2509
|
}
|
|
1368
2510
|
fixOwnership(routesPath);
|
|
@@ -1422,7 +2564,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1422
2564
|
}
|
|
1423
2565
|
};
|
|
1424
2566
|
try {
|
|
1425
|
-
watcher =
|
|
2567
|
+
watcher = fs8.watch(routesPath, () => {
|
|
1426
2568
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1427
2569
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
1428
2570
|
});
|
|
@@ -1472,8 +2614,8 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1472
2614
|
redirectServer.listen(80);
|
|
1473
2615
|
}
|
|
1474
2616
|
server.listen(proxyPort, () => {
|
|
1475
|
-
|
|
1476
|
-
|
|
2617
|
+
fs8.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
2618
|
+
fs8.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
1477
2619
|
writeTlsMarker(store.dir, isTls);
|
|
1478
2620
|
writeTldFile(store.dir, tld);
|
|
1479
2621
|
writeLanMarker(store.dir, activeLanIp);
|
|
@@ -1489,7 +2631,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1489
2631
|
console.log(chalk.gray("Services are discoverable as <name>.local on your network"));
|
|
1490
2632
|
if (isTls) {
|
|
1491
2633
|
console.log(chalk.yellow("For HTTPS on devices, install the CA certificate:"));
|
|
1492
|
-
console.log(chalk.gray(` ${
|
|
2634
|
+
console.log(chalk.gray(` ${path8.join(store.dir, "ca.pem")}`));
|
|
1493
2635
|
}
|
|
1494
2636
|
if (!lanIpPinned) {
|
|
1495
2637
|
lanMonitor = startLanIpMonitor({
|
|
@@ -1521,15 +2663,16 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1521
2663
|
redirectServer.close();
|
|
1522
2664
|
}
|
|
1523
2665
|
try {
|
|
1524
|
-
|
|
2666
|
+
fs8.unlinkSync(store.pidPath);
|
|
1525
2667
|
} catch {
|
|
1526
2668
|
}
|
|
1527
2669
|
try {
|
|
1528
|
-
|
|
2670
|
+
fs8.unlinkSync(store.portFilePath);
|
|
1529
2671
|
} catch {
|
|
1530
2672
|
}
|
|
1531
2673
|
writeTlsMarker(store.dir, false);
|
|
1532
2674
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2675
|
+
writeLanMarker(store.dir, null);
|
|
1533
2676
|
if (autoSyncHosts) cleanHostsFile();
|
|
1534
2677
|
server.close(() => process.exit(0));
|
|
1535
2678
|
setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
|
|
@@ -1554,7 +2697,7 @@ function sudoStopOrHint(port) {
|
|
|
1554
2697
|
}
|
|
1555
2698
|
async function stopProxy(store, proxyPort, _tls) {
|
|
1556
2699
|
const pidPath = store.pidPath;
|
|
1557
|
-
if (!
|
|
2700
|
+
if (!fs8.existsSync(pidPath)) {
|
|
1558
2701
|
if (await isProxyRunning(proxyPort)) {
|
|
1559
2702
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
1560
2703
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -1562,9 +2705,12 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1562
2705
|
try {
|
|
1563
2706
|
process.kill(pid, "SIGTERM");
|
|
1564
2707
|
try {
|
|
1565
|
-
|
|
2708
|
+
fs8.unlinkSync(store.portFilePath);
|
|
1566
2709
|
} catch {
|
|
1567
2710
|
}
|
|
2711
|
+
writeTlsMarker(store.dir, false);
|
|
2712
|
+
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2713
|
+
writeLanMarker(store.dir, null);
|
|
1568
2714
|
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
1569
2715
|
} catch (err) {
|
|
1570
2716
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
@@ -1597,10 +2743,13 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1597
2743
|
return;
|
|
1598
2744
|
}
|
|
1599
2745
|
try {
|
|
1600
|
-
const pid = parseInt(
|
|
2746
|
+
const pid = parseInt(fs8.readFileSync(pidPath, "utf-8"), 10);
|
|
1601
2747
|
if (isNaN(pid)) {
|
|
1602
2748
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
1603
|
-
|
|
2749
|
+
fs8.unlinkSync(pidPath);
|
|
2750
|
+
writeTlsMarker(store.dir, false);
|
|
2751
|
+
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2752
|
+
writeLanMarker(store.dir, null);
|
|
1604
2753
|
return;
|
|
1605
2754
|
}
|
|
1606
2755
|
try {
|
|
@@ -1611,11 +2760,14 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1611
2760
|
return;
|
|
1612
2761
|
}
|
|
1613
2762
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
1614
|
-
|
|
2763
|
+
fs8.unlinkSync(pidPath);
|
|
1615
2764
|
try {
|
|
1616
|
-
|
|
2765
|
+
fs8.unlinkSync(store.portFilePath);
|
|
1617
2766
|
} catch {
|
|
1618
2767
|
}
|
|
2768
|
+
writeTlsMarker(store.dir, false);
|
|
2769
|
+
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2770
|
+
writeLanMarker(store.dir, null);
|
|
1619
2771
|
return;
|
|
1620
2772
|
}
|
|
1621
2773
|
if (!await isProxyRunning(proxyPort)) {
|
|
@@ -1625,15 +2777,21 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1625
2777
|
)
|
|
1626
2778
|
);
|
|
1627
2779
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
1628
|
-
|
|
2780
|
+
fs8.unlinkSync(pidPath);
|
|
2781
|
+
writeTlsMarker(store.dir, false);
|
|
2782
|
+
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2783
|
+
writeLanMarker(store.dir, null);
|
|
1629
2784
|
return;
|
|
1630
2785
|
}
|
|
1631
2786
|
process.kill(pid, "SIGTERM");
|
|
1632
|
-
|
|
2787
|
+
fs8.unlinkSync(pidPath);
|
|
1633
2788
|
try {
|
|
1634
|
-
|
|
2789
|
+
fs8.unlinkSync(store.portFilePath);
|
|
1635
2790
|
} catch {
|
|
1636
2791
|
}
|
|
2792
|
+
writeTlsMarker(store.dir, false);
|
|
2793
|
+
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2794
|
+
writeLanMarker(store.dir, null);
|
|
1637
2795
|
console.log(colors_default.green("Proxy stopped."));
|
|
1638
2796
|
} catch (err) {
|
|
1639
2797
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
@@ -1659,26 +2817,16 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
1659
2817
|
}
|
|
1660
2818
|
console.log(colors_default.blue.bold("\nActive routes:\n"));
|
|
1661
2819
|
for (const route of routes) {
|
|
1662
|
-
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
1663
|
-
const label = route.pid === 0 ? "(alias)" : `(pid ${route.pid})`;
|
|
1664
|
-
console.log(
|
|
1665
|
-
` ${colors_default.cyan(url)} ${colors_default.gray("->")} ${colors_default.white(`localhost:${route.port}`)} ${colors_default.gray(label)}`
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
|
-
console.log();
|
|
1669
|
-
}
|
|
1670
|
-
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort, lanMode = false, lanIp) {
|
|
1671
|
-
let store = initialStore;
|
|
1672
|
-
console.log(chalk.blue.bold(`
|
|
1673
|
-
portless
|
|
1674
|
-
`));
|
|
1675
|
-
let envTld;
|
|
1676
|
-
try {
|
|
1677
|
-
envTld = getDefaultTld();
|
|
1678
|
-
} catch (err) {
|
|
1679
|
-
console.error(colors_default.red(`Error: ${err.message}`));
|
|
1680
|
-
process.exit(1);
|
|
2820
|
+
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
2821
|
+
const label = route.pid === 0 ? "(alias)" : `(pid ${route.pid})`;
|
|
2822
|
+
console.log(
|
|
2823
|
+
` ${colors_default.cyan(url)} ${colors_default.gray("->")} ${colors_default.white(`localhost:${route.port}`)} ${colors_default.gray(label)}`
|
|
2824
|
+
);
|
|
1681
2825
|
}
|
|
2826
|
+
console.log();
|
|
2827
|
+
}
|
|
2828
|
+
function resolveProxyDesiredState(lanMode) {
|
|
2829
|
+
const envTld = getDefaultTld();
|
|
1682
2830
|
const explicit = {
|
|
1683
2831
|
useHttps: process.env.PORTLESS_HTTPS !== void 0,
|
|
1684
2832
|
customCert: false,
|
|
@@ -1699,131 +2847,139 @@ portless
|
|
|
1699
2847
|
tld: envTld,
|
|
1700
2848
|
useWildcard: isWildcardEnvEnabled()
|
|
1701
2849
|
});
|
|
1702
|
-
|
|
2850
|
+
return { explicit, desiredConfig, envTld };
|
|
2851
|
+
}
|
|
2852
|
+
async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
2853
|
+
const { explicit, desiredConfig } = desired;
|
|
1703
2854
|
const proxyResponsive = await isProxyRunning(proxyPort, tls2);
|
|
1704
2855
|
const proxyListeningFromStateDir = !!process.env.PORTLESS_STATE_DIR && await isPortListening(proxyPort);
|
|
1705
|
-
if (
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
startConfig.tld = persisted.tld;
|
|
1715
|
-
}
|
|
1716
|
-
if (!explicit.lanMode && persisted.lanMode !== desiredConfig.lanMode) {
|
|
1717
|
-
startConfig.lanMode = persisted.lanMode;
|
|
1718
|
-
}
|
|
1719
|
-
const envPort = getDefaultPort(startConfig.useHttps);
|
|
1720
|
-
if (persisted.port !== envPort) {
|
|
1721
|
-
startPort = persisted.port;
|
|
1722
|
-
}
|
|
2856
|
+
if (proxyResponsive || proxyListeningFromStateDir) {
|
|
2857
|
+
return { started: false };
|
|
2858
|
+
}
|
|
2859
|
+
const persisted = readPersistedProxyState();
|
|
2860
|
+
const startConfig = { ...desiredConfig };
|
|
2861
|
+
let startPort;
|
|
2862
|
+
if (persisted) {
|
|
2863
|
+
if (!explicit.useHttps && persisted.tls !== desiredConfig.useHttps) {
|
|
2864
|
+
startConfig.useHttps = persisted.tls;
|
|
1723
2865
|
}
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
const manualStartCommand = formatProxyStartCommand(effectivePort, startConfig);
|
|
1727
|
-
const fallbackStartCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, startConfig);
|
|
1728
|
-
const isInteractive = !!process.stdin.isTTY && !process.env.CI;
|
|
1729
|
-
if (needsSudo && !isInteractive) {
|
|
1730
|
-
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
1731
|
-
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
1732
|
-
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
1733
|
-
console.error(
|
|
1734
|
-
colors_default.blue(
|
|
1735
|
-
`Option 2: use an unprivileged port (no sudo needed, URLs will include :${FALLBACK_PROXY_PORT}):`
|
|
1736
|
-
)
|
|
1737
|
-
);
|
|
1738
|
-
console.error(colors_default.cyan(` ${fallbackStartCommand}`));
|
|
1739
|
-
process.exit(1);
|
|
2866
|
+
if (!explicit.tld && persisted.tld !== desiredConfig.tld) {
|
|
2867
|
+
startConfig.tld = persisted.tld;
|
|
1740
2868
|
}
|
|
1741
|
-
if (
|
|
1742
|
-
|
|
1743
|
-
if (answer === "n" || answer === "no") {
|
|
1744
|
-
console.log(colors_default.gray("Cancelled."));
|
|
1745
|
-
process.exit(0);
|
|
1746
|
-
}
|
|
1747
|
-
if (answer === "s" || answer === "skip") {
|
|
1748
|
-
console.log(colors_default.gray("Skipping proxy, running command directly...\n"));
|
|
1749
|
-
spawnCommand(commandArgs);
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
2869
|
+
if (!explicit.lanMode && persisted.lanMode !== desiredConfig.lanMode) {
|
|
2870
|
+
startConfig.lanMode = persisted.lanMode;
|
|
1752
2871
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
`Starting proxy with previous configuration (port ${startPort}, ${startConfig.useHttps ? "HTTPS" : "HTTP"})...`
|
|
1757
|
-
)
|
|
1758
|
-
);
|
|
1759
|
-
} else {
|
|
1760
|
-
console.log(colors_default.yellow("Starting proxy..."));
|
|
1761
|
-
}
|
|
1762
|
-
const proxyStartConfig = buildProxyStartConfig({
|
|
1763
|
-
useHttps: startConfig.useHttps,
|
|
1764
|
-
customCertPath: startConfig.customCertPath,
|
|
1765
|
-
customKeyPath: startConfig.customKeyPath,
|
|
1766
|
-
lanMode: startConfig.lanMode,
|
|
1767
|
-
lanIp: startConfig.lanIpExplicit ? startConfig.lanIp : null,
|
|
1768
|
-
lanIpExplicit: startConfig.lanIpExplicit,
|
|
1769
|
-
tld: startConfig.tld,
|
|
1770
|
-
useWildcard: startConfig.useWildcard,
|
|
1771
|
-
includePort: startPort !== void 0,
|
|
1772
|
-
proxyPort: startPort
|
|
1773
|
-
});
|
|
1774
|
-
const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
|
|
1775
|
-
const result = spawnSync2(process.execPath, startArgs, {
|
|
1776
|
-
stdio: "inherit",
|
|
1777
|
-
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1778
|
-
});
|
|
1779
|
-
let discovered = null;
|
|
1780
|
-
if (!result.signal) {
|
|
1781
|
-
for (let i = 0; i < WAIT_FOR_PROXY_MAX_ATTEMPTS; i++) {
|
|
1782
|
-
await new Promise((r) => setTimeout(r, WAIT_FOR_PROXY_INTERVAL_MS));
|
|
1783
|
-
const state = await discoverState();
|
|
1784
|
-
if (await isProxyRunning(state.port)) {
|
|
1785
|
-
discovered = state;
|
|
1786
|
-
break;
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
2872
|
+
const envPort = getDefaultPort(startConfig.useHttps);
|
|
2873
|
+
if (persisted.port !== envPort) {
|
|
2874
|
+
startPort = persisted.port;
|
|
1789
2875
|
}
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2876
|
+
}
|
|
2877
|
+
const effectivePort = startPort ?? getDefaultPort(startConfig.useHttps);
|
|
2878
|
+
const needsSudo = !isWindows && effectivePort < PRIVILEGED_PORT_THRESHOLD;
|
|
2879
|
+
const manualStartCommand = formatProxyStartCommand(effectivePort, startConfig);
|
|
2880
|
+
const fallbackStartCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, startConfig);
|
|
2881
|
+
const isInteractive = !!process.stdin.isTTY && !process.env.CI;
|
|
2882
|
+
if (needsSudo && !isInteractive) {
|
|
2883
|
+
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
2884
|
+
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
2885
|
+
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
2886
|
+
console.error(
|
|
2887
|
+
colors_default.blue(
|
|
2888
|
+
`Option 2: use an unprivileged port (no sudo needed, URLs will include :${FALLBACK_PROXY_PORT}):`
|
|
2889
|
+
)
|
|
2890
|
+
);
|
|
2891
|
+
console.error(colors_default.cyan(` ${fallbackStartCommand}`));
|
|
2892
|
+
process.exit(1);
|
|
2893
|
+
}
|
|
2894
|
+
console.log(colors_default.gray("Starting proxy..."));
|
|
2895
|
+
const proxyStartConfig = buildProxyStartConfig({
|
|
2896
|
+
useHttps: startConfig.useHttps,
|
|
2897
|
+
customCertPath: startConfig.customCertPath,
|
|
2898
|
+
customKeyPath: startConfig.customKeyPath,
|
|
2899
|
+
lanMode: startConfig.lanMode,
|
|
2900
|
+
lanIp: startConfig.lanIpExplicit ? startConfig.lanIp : null,
|
|
2901
|
+
lanIpExplicit: startConfig.lanIpExplicit,
|
|
2902
|
+
tld: startConfig.tld,
|
|
2903
|
+
useWildcard: startConfig.useWildcard,
|
|
2904
|
+
includePort: startPort !== void 0,
|
|
2905
|
+
proxyPort: startPort
|
|
2906
|
+
});
|
|
2907
|
+
const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
|
|
2908
|
+
const result = spawnSync2(process.execPath, startArgs, {
|
|
2909
|
+
stdio: "inherit",
|
|
2910
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
2911
|
+
});
|
|
2912
|
+
let discovered = null;
|
|
2913
|
+
if (!result.signal) {
|
|
2914
|
+
for (let i = 0; i < WAIT_FOR_PROXY_MAX_ATTEMPTS; i++) {
|
|
2915
|
+
await new Promise((r) => setTimeout(r, WAIT_FOR_PROXY_INTERVAL_MS));
|
|
2916
|
+
const state = await discoverState();
|
|
2917
|
+
if (await isProxyRunning(state.port)) {
|
|
2918
|
+
discovered = state;
|
|
2919
|
+
break;
|
|
1798
2920
|
}
|
|
1799
|
-
process.exit(1);
|
|
1800
|
-
return;
|
|
1801
2921
|
}
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
2922
|
+
}
|
|
2923
|
+
if (!discovered) {
|
|
2924
|
+
console.error(colors_default.red("Failed to start proxy."));
|
|
2925
|
+
const fallbackDir = resolveStateDir(effectivePort);
|
|
2926
|
+
const logPath = path8.join(fallbackDir, "proxy.log");
|
|
2927
|
+
console.error(colors_default.blue("Try starting it manually:"));
|
|
2928
|
+
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
2929
|
+
if (fs8.existsSync(logPath)) {
|
|
2930
|
+
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
2931
|
+
}
|
|
2932
|
+
process.exit(1);
|
|
2933
|
+
return { started: false };
|
|
2934
|
+
}
|
|
2935
|
+
return { started: true, state: discovered };
|
|
2936
|
+
}
|
|
2937
|
+
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort, lanMode = false, lanIp) {
|
|
2938
|
+
let store = initialStore;
|
|
2939
|
+
console.log(chalk.blue.bold(`
|
|
2940
|
+
portless
|
|
2941
|
+
`));
|
|
2942
|
+
let desired;
|
|
2943
|
+
try {
|
|
2944
|
+
desired = resolveProxyDesiredState(lanMode);
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
2947
|
+
process.exit(1);
|
|
2948
|
+
}
|
|
2949
|
+
parseHostname(name, tld);
|
|
2950
|
+
const ensureResult = await ensureProxyRunning(proxyPort, tls2, desired);
|
|
2951
|
+
if (ensureResult.started) {
|
|
2952
|
+
proxyPort = ensureResult.state.port;
|
|
2953
|
+
stateDir = ensureResult.state.dir;
|
|
2954
|
+
tld = ensureResult.state.tld;
|
|
2955
|
+
tls2 = ensureResult.state.tls;
|
|
2956
|
+
lanMode = ensureResult.state.lanMode;
|
|
2957
|
+
lanIp = ensureResult.state.lanIp;
|
|
1808
2958
|
store = new RouteStore(stateDir, {
|
|
1809
2959
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1810
2960
|
});
|
|
1811
|
-
|
|
2961
|
+
if (tls2 && !isCATrusted(stateDir)) {
|
|
2962
|
+
await handleTrust();
|
|
2963
|
+
}
|
|
1812
2964
|
} else {
|
|
1813
2965
|
const runningConfig = readCurrentProxyConfig(stateDir);
|
|
1814
|
-
const mismatchMessages = getProxyConfigMismatchMessages(
|
|
2966
|
+
const mismatchMessages = getProxyConfigMismatchMessages(
|
|
2967
|
+
desired.desiredConfig,
|
|
2968
|
+
runningConfig,
|
|
2969
|
+
desired.explicit
|
|
2970
|
+
);
|
|
1815
2971
|
if (mismatchMessages.length > 0) {
|
|
1816
|
-
printProxyConfigMismatch(proxyPort, desiredConfig, mismatchMessages);
|
|
2972
|
+
printProxyConfigMismatch(proxyPort, desired.desiredConfig, mismatchMessages);
|
|
1817
2973
|
}
|
|
1818
2974
|
lanMode = runningConfig.lanMode;
|
|
1819
2975
|
lanIp = runningConfig.lanIp;
|
|
1820
2976
|
console.log(chalk.gray("-- Proxy is running"));
|
|
1821
2977
|
}
|
|
1822
2978
|
const hostname = parseHostname(name, tld);
|
|
1823
|
-
if (envTld !== DEFAULT_TLD && envTld !== tld) {
|
|
2979
|
+
if (desired.envTld !== DEFAULT_TLD && desired.envTld !== tld) {
|
|
1824
2980
|
console.warn(
|
|
1825
2981
|
chalk.yellow(
|
|
1826
|
-
`Warning: PORTLESS_TLD=${envTld} but the running proxy uses .${tld}. Using .${tld}.`
|
|
2982
|
+
`Warning: PORTLESS_TLD=${desired.envTld} but the running proxy uses .${tld}. Using .${tld}.`
|
|
1827
2983
|
)
|
|
1828
2984
|
);
|
|
1829
2985
|
}
|
|
@@ -1866,8 +3022,8 @@ portless
|
|
|
1866
3022
|
console.log(chalk.green(` LAN -> ${finalUrl}`));
|
|
1867
3023
|
console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
|
|
1868
3024
|
}
|
|
1869
|
-
const
|
|
1870
|
-
const isExpo =
|
|
3025
|
+
const basename5 = path8.basename(commandArgs[0]);
|
|
3026
|
+
const isExpo = basename5 === "expo";
|
|
1871
3027
|
const isExpoLan = isExpo && (lanMode || isLanEnvEnabled());
|
|
1872
3028
|
const hostBind = isExpoLan ? void 0 : "127.0.0.1";
|
|
1873
3029
|
if (lanMode && !process.env.PORTLESS_LAN) {
|
|
@@ -1876,8 +3032,8 @@ portless
|
|
|
1876
3032
|
injectFrameworkFlags(commandArgs, port);
|
|
1877
3033
|
const caEnv = {};
|
|
1878
3034
|
if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
|
|
1879
|
-
const caPath =
|
|
1880
|
-
if (
|
|
3035
|
+
const caPath = path8.join(stateDir, "ca.pem");
|
|
3036
|
+
if (fs8.existsSync(caPath)) {
|
|
1881
3037
|
caEnv.NODE_EXTRA_CA_CERTS = caPath;
|
|
1882
3038
|
}
|
|
1883
3039
|
}
|
|
@@ -1945,7 +3101,10 @@ function parseRunArgs(args) {
|
|
|
1945
3101
|
${colors_default.bold("portless run")} - Infer project name and run through the proxy.
|
|
1946
3102
|
|
|
1947
3103
|
${colors_default.bold("Usage:")}
|
|
1948
|
-
${colors_default.cyan("portless run [options]
|
|
3104
|
+
${colors_default.cyan("portless run [options] [command...]")}
|
|
3105
|
+
|
|
3106
|
+
When no command is given, runs the configured script (default: "dev")
|
|
3107
|
+
from package.json.
|
|
1949
3108
|
|
|
1950
3109
|
${colors_default.bold("Options:")}
|
|
1951
3110
|
--name <name> Override the inferred base name (worktree prefix still applies)
|
|
@@ -1954,15 +3113,17 @@ ${colors_default.bold("Options:")}
|
|
|
1954
3113
|
--help, -h Show this help
|
|
1955
3114
|
|
|
1956
3115
|
${colors_default.bold("Name inference (in order):")}
|
|
1957
|
-
1.
|
|
1958
|
-
2.
|
|
1959
|
-
3.
|
|
3116
|
+
1. portless.json "name" field
|
|
3117
|
+
2. package.json "name" field (walks up directories)
|
|
3118
|
+
3. Git repo root directory name
|
|
3119
|
+
4. Current directory basename
|
|
1960
3120
|
|
|
1961
3121
|
Use --name to override the inferred name while keeping worktree prefixes.
|
|
1962
3122
|
In git worktrees, the branch name is prepended as a subdomain prefix
|
|
1963
3123
|
(e.g. feature-auth.myapp.localhost).
|
|
1964
3124
|
|
|
1965
3125
|
${colors_default.bold("Examples:")}
|
|
3126
|
+
portless run # Run dev script through proxy
|
|
1966
3127
|
portless run next dev # -> https://<project>.localhost
|
|
1967
3128
|
portless run --name myapp next dev # -> https://myapp.localhost
|
|
1968
3129
|
portless run vite dev # -> https://<project>.localhost
|
|
@@ -2045,13 +3206,13 @@ ${colors_default.bold("Install:")}
|
|
|
2045
3206
|
${colors_default.cyan("npm install -D portless")} Project dev dependency
|
|
2046
3207
|
|
|
2047
3208
|
${colors_default.bold("Usage:")}
|
|
3209
|
+
${colors_default.cyan("portless")} Run dev script through proxy
|
|
3210
|
+
${colors_default.cyan("portless")} From monorepo root: run all workspace packages
|
|
3211
|
+
${colors_default.cyan("portless run")} Same as above
|
|
3212
|
+
${colors_default.cyan("portless run <cmd>")} Run a command through the proxy
|
|
3213
|
+
${colors_default.cyan("portless <name> <cmd>")} Run with an explicit app name
|
|
2048
3214
|
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
2049
|
-
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
2050
|
-
${colors_default.cyan("portless proxy start --lan")} Start in LAN mode (mDNS for real device testing)
|
|
2051
|
-
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
2052
3215
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
2053
|
-
${colors_default.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
2054
|
-
${colors_default.cyan("portless run <cmd>")} Infer name from project, run through proxy
|
|
2055
3216
|
${colors_default.cyan("portless get <name>")} Print URL for a service (for cross-service refs)
|
|
2056
3217
|
${colors_default.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
|
|
2057
3218
|
${colors_default.cyan("portless alias --remove <name>")} Remove a static route
|
|
@@ -2062,22 +3223,31 @@ ${colors_default.bold("Usage:")}
|
|
|
2062
3223
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
2063
3224
|
|
|
2064
3225
|
${colors_default.bold("Examples:")}
|
|
2065
|
-
portless
|
|
2066
|
-
portless
|
|
3226
|
+
portless # Run dev script through proxy
|
|
3227
|
+
portless # From monorepo root: start all apps
|
|
3228
|
+
portless --script start # Run "start" script instead of "dev"
|
|
2067
3229
|
portless myapp next dev # -> https://myapp.localhost
|
|
2068
|
-
portless myapp vite dev # -> https://myapp.localhost
|
|
2069
|
-
portless api.myapp pnpm start # -> https://api.myapp.localhost
|
|
2070
3230
|
portless run next dev # -> https://<project>.localhost
|
|
2071
3231
|
portless run next dev # in worktree -> https://<worktree>.<project>.localhost
|
|
2072
|
-
portless get backend
|
|
2073
|
-
|
|
3232
|
+
portless get backend # -> https://backend.localhost
|
|
3233
|
+
|
|
3234
|
+
${colors_default.bold("Configuration (portless.json):")}
|
|
3235
|
+
Optional. Portless works out of the box by running the "dev" script
|
|
3236
|
+
from package.json. Use portless.json to override defaults.
|
|
3237
|
+
|
|
3238
|
+
Override name: { "name": "myapp" }
|
|
3239
|
+
Override script: { "name": "myapp", "script": "start" }
|
|
3240
|
+
Monorepo: { "apps": { "apps/web": { "name": "myapp" } } }
|
|
2074
3241
|
|
|
2075
3242
|
${colors_default.bold("In package.json:")}
|
|
2076
3243
|
{
|
|
2077
3244
|
"scripts": {
|
|
2078
|
-
"dev": "
|
|
3245
|
+
"dev": "next dev"
|
|
2079
3246
|
}
|
|
2080
3247
|
}
|
|
3248
|
+
Then run: portless
|
|
3249
|
+
Or: portless run
|
|
3250
|
+
Or: portless run next dev
|
|
2081
3251
|
|
|
2082
3252
|
${colors_default.bold("How it works:")}
|
|
2083
3253
|
1. Start the proxy once (HTTPS on port 443 by default, auto-elevates with sudo)
|
|
@@ -2113,6 +3283,7 @@ ${colors_default.bold("LAN mode:")}
|
|
|
2113
3283
|
${colors_default.bold("Options:")}
|
|
2114
3284
|
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
2115
3285
|
Adds worktree prefix in git worktrees
|
|
3286
|
+
--script <name> Run a specific package.json script (default: dev)
|
|
2116
3287
|
-p, --port <number> Port for the proxy (default: 443, or 80 with --no-tls)
|
|
2117
3288
|
Standard ports auto-elevate with sudo on macOS/Linux
|
|
2118
3289
|
--no-tls Disable HTTPS (use plain HTTP on port 80)
|
|
@@ -2167,13 +3338,13 @@ ${colors_default.bold("Reserved names:")}
|
|
|
2167
3338
|
process.exit(0);
|
|
2168
3339
|
}
|
|
2169
3340
|
function printVersion() {
|
|
2170
|
-
console.log("0.
|
|
3341
|
+
console.log("0.11.0");
|
|
2171
3342
|
process.exit(0);
|
|
2172
3343
|
}
|
|
2173
3344
|
async function handleTrust() {
|
|
2174
3345
|
const { dir } = await discoverState();
|
|
2175
|
-
if (!
|
|
2176
|
-
|
|
3346
|
+
if (!fs8.existsSync(dir)) {
|
|
3347
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
2177
3348
|
}
|
|
2178
3349
|
const { caGenerated } = ensureCerts(dir);
|
|
2179
3350
|
if (caGenerated) {
|
|
@@ -2246,8 +3417,8 @@ ${colors_default.bold("Options:")}
|
|
|
2246
3417
|
await stopProxy(store, port, tls2);
|
|
2247
3418
|
const stateDirs = collectStateDirsForCleanup();
|
|
2248
3419
|
for (const stateDir of stateDirs) {
|
|
2249
|
-
const caPath =
|
|
2250
|
-
if (!
|
|
3420
|
+
const caPath = path8.join(stateDir, "ca.pem");
|
|
3421
|
+
if (!fs8.existsSync(caPath)) continue;
|
|
2251
3422
|
const wasTrusted = isCATrusted(stateDir);
|
|
2252
3423
|
if (!wasTrusted) continue;
|
|
2253
3424
|
const untrustResult = untrustCA(stateDir);
|
|
@@ -2797,8 +3968,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2797
3968
|
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
2798
3969
|
} else {
|
|
2799
3970
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
2800
|
-
const logPath2 =
|
|
2801
|
-
if (
|
|
3971
|
+
const logPath2 = path8.join(resolveStateDir(proxyPort), "proxy.log");
|
|
3972
|
+
if (fs8.existsSync(logPath2)) {
|
|
2802
3973
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
2803
3974
|
}
|
|
2804
3975
|
}
|
|
@@ -2837,8 +4008,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2837
4008
|
store.ensureDir();
|
|
2838
4009
|
if (customCertPath && customKeyPath) {
|
|
2839
4010
|
try {
|
|
2840
|
-
const cert =
|
|
2841
|
-
const key =
|
|
4011
|
+
const cert = fs8.readFileSync(customCertPath);
|
|
4012
|
+
const key = fs8.readFileSync(customKeyPath);
|
|
2842
4013
|
const certStr = cert.toString("utf-8");
|
|
2843
4014
|
const keyStr = key.toString("utf-8");
|
|
2844
4015
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -2883,9 +4054,9 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2883
4054
|
console.warn(colors_default.cyan(" portless trust"));
|
|
2884
4055
|
}
|
|
2885
4056
|
}
|
|
2886
|
-
const cert =
|
|
2887
|
-
const key =
|
|
2888
|
-
const ca =
|
|
4057
|
+
const cert = fs8.readFileSync(certs.certPath);
|
|
4058
|
+
const key = fs8.readFileSync(certs.keyPath);
|
|
4059
|
+
const ca = fs8.readFileSync(certs.caPath);
|
|
2889
4060
|
tlsOptions = {
|
|
2890
4061
|
cert,
|
|
2891
4062
|
key,
|
|
@@ -2900,11 +4071,11 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2900
4071
|
return;
|
|
2901
4072
|
}
|
|
2902
4073
|
store.ensureDir();
|
|
2903
|
-
const logPath =
|
|
2904
|
-
const logFd =
|
|
4074
|
+
const logPath = path8.join(stateDir, "proxy.log");
|
|
4075
|
+
const logFd = fs8.openSync(logPath, "a");
|
|
2905
4076
|
try {
|
|
2906
4077
|
try {
|
|
2907
|
-
|
|
4078
|
+
fs8.chmodSync(logPath, FILE_MODE);
|
|
2908
4079
|
} catch {
|
|
2909
4080
|
}
|
|
2910
4081
|
fixOwnership(logPath);
|
|
@@ -2927,7 +4098,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2927
4098
|
skipTrust: true
|
|
2928
4099
|
}).args
|
|
2929
4100
|
];
|
|
2930
|
-
const child =
|
|
4101
|
+
const child = spawn3(process.execPath, daemonArgs, {
|
|
2931
4102
|
detached: true,
|
|
2932
4103
|
stdio: ["ignore", logFd, logFd],
|
|
2933
4104
|
env: process.env,
|
|
@@ -2935,13 +4106,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2935
4106
|
});
|
|
2936
4107
|
child.unref();
|
|
2937
4108
|
} finally {
|
|
2938
|
-
|
|
4109
|
+
fs8.closeSync(logFd);
|
|
2939
4110
|
}
|
|
2940
4111
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
2941
4112
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
2942
4113
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
2943
4114
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
2944
|
-
if (
|
|
4115
|
+
if (fs8.existsSync(logPath)) {
|
|
2945
4116
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
2946
4117
|
}
|
|
2947
4118
|
process.exit(1);
|
|
@@ -2953,8 +4124,488 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2953
4124
|
console.log(chalk.gray("Services will be discoverable as <name>.local on your network."));
|
|
2954
4125
|
}
|
|
2955
4126
|
}
|
|
2956
|
-
|
|
4127
|
+
function loadAppConfig(cwd = process.cwd()) {
|
|
4128
|
+
try {
|
|
4129
|
+
const loaded = loadConfig(cwd);
|
|
4130
|
+
if (!loaded) return null;
|
|
4131
|
+
return resolveAppConfig(loaded.config, loaded.configDir, cwd);
|
|
4132
|
+
} catch (err) {
|
|
4133
|
+
if (err instanceof ConfigValidationError) {
|
|
4134
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
4135
|
+
process.exit(1);
|
|
4136
|
+
}
|
|
4137
|
+
throw err;
|
|
4138
|
+
}
|
|
4139
|
+
}
|
|
4140
|
+
async function handleDefaultMode(globalScript, extraArgs = []) {
|
|
4141
|
+
const cwd = process.cwd();
|
|
4142
|
+
const wsRoot = findWorkspaceRoot(cwd);
|
|
4143
|
+
if (wsRoot === cwd) {
|
|
4144
|
+
const packages = discoverWorkspacePackages(cwd);
|
|
4145
|
+
let wsScriptName;
|
|
4146
|
+
try {
|
|
4147
|
+
wsScriptName = globalScript ?? loadConfig(cwd)?.config.script ?? "dev";
|
|
4148
|
+
} catch (err) {
|
|
4149
|
+
if (err instanceof ConfigValidationError) {
|
|
4150
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
4151
|
+
process.exit(1);
|
|
4152
|
+
}
|
|
4153
|
+
throw err;
|
|
4154
|
+
}
|
|
4155
|
+
const hasMatchingPackages = packages.some((p) => p.scripts[wsScriptName]);
|
|
4156
|
+
if (hasMatchingPackages) {
|
|
4157
|
+
await handleDefaultMulti(cwd, globalScript, extraArgs);
|
|
4158
|
+
return true;
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
const appConfig = loadAppConfig(cwd);
|
|
4162
|
+
const scriptName = globalScript ?? appConfig?.script ?? "dev";
|
|
4163
|
+
if (hasScript(scriptName, cwd)) {
|
|
4164
|
+
await handleDefaultSingle(cwd, scriptName, appConfig);
|
|
4165
|
+
return true;
|
|
4166
|
+
}
|
|
4167
|
+
return false;
|
|
4168
|
+
}
|
|
4169
|
+
async function handleDefaultSingle(cwd, scriptName, appConfig) {
|
|
4170
|
+
const resolved = resolveScriptCommand(scriptName, cwd);
|
|
4171
|
+
if (!resolved) {
|
|
4172
|
+
console.error(colors_default.red(`Error: No "${scriptName}" script found in package.json.`));
|
|
4173
|
+
process.exit(1);
|
|
4174
|
+
}
|
|
4175
|
+
let baseName;
|
|
4176
|
+
let nameSource;
|
|
4177
|
+
if (appConfig?.name) {
|
|
4178
|
+
baseName = appConfig.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
4179
|
+
nameSource = "portless.json";
|
|
4180
|
+
} else {
|
|
4181
|
+
const inferred = inferProjectName(cwd);
|
|
4182
|
+
baseName = inferred.name;
|
|
4183
|
+
nameSource = inferred.source;
|
|
4184
|
+
}
|
|
4185
|
+
const worktree = detectWorktreePrefix(cwd);
|
|
4186
|
+
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
4187
|
+
const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
|
|
4188
|
+
const store = new RouteStore(dir, {
|
|
4189
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
4190
|
+
});
|
|
4191
|
+
await runApp(
|
|
4192
|
+
store,
|
|
4193
|
+
port,
|
|
4194
|
+
dir,
|
|
4195
|
+
effectiveName,
|
|
4196
|
+
resolved,
|
|
4197
|
+
tls2,
|
|
4198
|
+
tld,
|
|
4199
|
+
false,
|
|
4200
|
+
{ nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
4201
|
+
appConfig?.appPort,
|
|
4202
|
+
lanMode,
|
|
4203
|
+
lanIp
|
|
4204
|
+
);
|
|
4205
|
+
}
|
|
4206
|
+
function spawnChildProcess(commandArgs, env, cwd) {
|
|
4207
|
+
return spawn3(commandArgs[0], commandArgs.slice(1), {
|
|
4208
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4209
|
+
env,
|
|
4210
|
+
cwd
|
|
4211
|
+
});
|
|
4212
|
+
}
|
|
4213
|
+
function prefixStream(stream, output, prefix) {
|
|
4214
|
+
if (!stream) return;
|
|
4215
|
+
const decoder = new StringDecoder("utf8");
|
|
4216
|
+
let buffer = "";
|
|
4217
|
+
stream.on("data", (data) => {
|
|
4218
|
+
buffer += decoder.write(data);
|
|
4219
|
+
let idx;
|
|
4220
|
+
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
4221
|
+
const line = buffer.slice(0, idx).replace(/\r$/, "");
|
|
4222
|
+
buffer = buffer.slice(idx + 1);
|
|
4223
|
+
output.write(`${prefix} ${line}
|
|
4224
|
+
`);
|
|
4225
|
+
}
|
|
4226
|
+
});
|
|
4227
|
+
stream.on("end", () => {
|
|
4228
|
+
buffer += decoder.end();
|
|
4229
|
+
if (buffer) output.write(`${prefix} ${buffer}
|
|
4230
|
+
`);
|
|
4231
|
+
});
|
|
4232
|
+
}
|
|
4233
|
+
function pipeOutput(child, prefix) {
|
|
4234
|
+
prefixStream(child.stdout, process.stdout, prefix);
|
|
4235
|
+
prefixStream(child.stderr, process.stderr, prefix);
|
|
4236
|
+
}
|
|
4237
|
+
async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
|
|
4238
|
+
const usesPortless = app.commandArgs[0] === "portless";
|
|
4239
|
+
const pkgEnv = { ...process.env };
|
|
4240
|
+
pkgEnv.PATH = augmentedPath(pkgEnv, app.pkg.dir);
|
|
4241
|
+
let env;
|
|
4242
|
+
let store = null;
|
|
4243
|
+
let hostname = null;
|
|
4244
|
+
let displayUrl;
|
|
4245
|
+
if (usesPortless) {
|
|
4246
|
+
env = pkgEnv;
|
|
4247
|
+
displayUrl = "(managed by portless)";
|
|
4248
|
+
} else {
|
|
4249
|
+
store = new RouteStore(stateDir, {
|
|
4250
|
+
onWarning: (msg) => console.warn(colors_default.yellow(`[${app.name}] ${msg}`))
|
|
4251
|
+
});
|
|
4252
|
+
const appPort = app.appPort ?? await findFreePort();
|
|
4253
|
+
const protocol = tls2 ? "https" : "http";
|
|
4254
|
+
const portSuffix = tls2 && proxyPort === 443 || !tls2 && proxyPort === 80 ? "" : `:${proxyPort}`;
|
|
4255
|
+
const url = `${protocol}://${app.name}.${tld}${portSuffix}`;
|
|
4256
|
+
displayUrl = url;
|
|
4257
|
+
hostname = parseHostname(app.name, tld);
|
|
4258
|
+
store.addRoute(hostname, appPort, process.pid);
|
|
4259
|
+
env = {
|
|
4260
|
+
...pkgEnv,
|
|
4261
|
+
PORT: String(appPort),
|
|
4262
|
+
HOST: "127.0.0.1",
|
|
4263
|
+
PORTLESS_URL: url
|
|
4264
|
+
};
|
|
4265
|
+
if (tls2) {
|
|
4266
|
+
const caPath = path8.join(stateDir, "ca.pem");
|
|
4267
|
+
if (fs8.existsSync(caPath)) {
|
|
4268
|
+
env.NODE_EXTRA_CA_CERTS = caPath;
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
const child = spawnChildProcess(app.commandArgs, env, app.pkg.dir);
|
|
4273
|
+
pipeOutput(child, chalk.cyan(`[${app.name}]`));
|
|
4274
|
+
const capturedStore = store;
|
|
4275
|
+
const capturedHostname = hostname;
|
|
4276
|
+
child.on("exit", (code, signal) => {
|
|
4277
|
+
exitCodes.set(app.name, code);
|
|
4278
|
+
if (code !== 0 && code !== null) {
|
|
4279
|
+
console.error(colors_default.red(`[${app.name}] exited with code ${code}`));
|
|
4280
|
+
} else if (signal) {
|
|
4281
|
+
console.error(colors_default.yellow(`[${app.name}] killed by ${signal}`));
|
|
4282
|
+
}
|
|
4283
|
+
if (capturedStore && capturedHostname) {
|
|
4284
|
+
try {
|
|
4285
|
+
capturedStore.removeRoute(capturedHostname);
|
|
4286
|
+
} catch {
|
|
4287
|
+
}
|
|
4288
|
+
}
|
|
4289
|
+
});
|
|
4290
|
+
const route = store && hostname ? { store, hostname } : null;
|
|
4291
|
+
return { child, displayUrl, route };
|
|
4292
|
+
}
|
|
4293
|
+
function spawnTaskApp(app, exitCodes) {
|
|
4294
|
+
const pkgEnv = { ...process.env };
|
|
4295
|
+
pkgEnv.PATH = augmentedPath(pkgEnv, app.pkg.dir);
|
|
4296
|
+
const child = spawnChildProcess(app.commandArgs, pkgEnv, app.pkg.dir);
|
|
4297
|
+
pipeOutput(child, chalk.gray(`[${app.name}]`));
|
|
4298
|
+
child.on("exit", (code, signal) => {
|
|
4299
|
+
exitCodes.set(app.name, code);
|
|
4300
|
+
if (code !== 0 && code !== null) {
|
|
4301
|
+
console.error(colors_default.red(`[${app.name}] exited with code ${code}`));
|
|
4302
|
+
} else if (signal) {
|
|
4303
|
+
console.error(colors_default.yellow(`[${app.name}] killed by ${signal}`));
|
|
4304
|
+
}
|
|
4305
|
+
});
|
|
4306
|
+
return child;
|
|
4307
|
+
}
|
|
4308
|
+
async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
|
|
4309
|
+
let loaded;
|
|
4310
|
+
try {
|
|
4311
|
+
loaded = loadConfig(wsRoot);
|
|
4312
|
+
} catch (err) {
|
|
4313
|
+
if (err instanceof ConfigValidationError) {
|
|
4314
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
4315
|
+
process.exit(1);
|
|
4316
|
+
}
|
|
4317
|
+
throw err;
|
|
4318
|
+
}
|
|
4319
|
+
const packages = discoverWorkspacePackages(wsRoot);
|
|
4320
|
+
if (packages.length === 0) {
|
|
4321
|
+
console.error(colors_default.red("Error: No workspace packages found."));
|
|
4322
|
+
process.exit(1);
|
|
4323
|
+
}
|
|
4324
|
+
const scriptName = globalScript ?? loaded?.config.script ?? "dev";
|
|
4325
|
+
let projectName;
|
|
4326
|
+
if (loaded?.config.name) {
|
|
4327
|
+
projectName = loaded.config.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
4328
|
+
} else {
|
|
4329
|
+
const scopeCounts = /* @__PURE__ */ new Map();
|
|
4330
|
+
for (const p of packages) {
|
|
4331
|
+
if (p.scope) scopeCounts.set(p.scope, (scopeCounts.get(p.scope) ?? 0) + 1);
|
|
4332
|
+
}
|
|
4333
|
+
let commonScope;
|
|
4334
|
+
let maxCount = 0;
|
|
4335
|
+
for (const [scope, count] of scopeCounts) {
|
|
4336
|
+
if (count > maxCount) {
|
|
4337
|
+
commonScope = scope;
|
|
4338
|
+
maxCount = count;
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
if (commonScope) {
|
|
4342
|
+
projectName = sanitizeForHostname(commonScope) || inferProjectName(wsRoot).name;
|
|
4343
|
+
} else {
|
|
4344
|
+
projectName = inferProjectName(wsRoot).name;
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
const apps = [];
|
|
4348
|
+
for (const pkg of packages) {
|
|
4349
|
+
const rel = path8.relative(wsRoot, pkg.dir).replace(/\\/g, "/");
|
|
4350
|
+
const rootOverride = loaded ? resolveAppConfig(loaded.config, loaded.configDir, pkg.dir) : null;
|
|
4351
|
+
let pkgConfig;
|
|
4352
|
+
try {
|
|
4353
|
+
pkgConfig = loadPackagePortlessConfig(pkg.dir);
|
|
4354
|
+
} catch (err) {
|
|
4355
|
+
if (err instanceof ConfigValidationError) {
|
|
4356
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
4357
|
+
process.exit(1);
|
|
4358
|
+
}
|
|
4359
|
+
throw err;
|
|
4360
|
+
}
|
|
4361
|
+
const appOverride = {
|
|
4362
|
+
...Object.fromEntries(Object.entries(rootOverride ?? {}).filter(([, v]) => v !== void 0)),
|
|
4363
|
+
...Object.fromEntries(Object.entries(pkgConfig ?? {}).filter(([, v]) => v !== void 0))
|
|
4364
|
+
};
|
|
4365
|
+
const effectiveScript = appOverride.script ?? scriptName;
|
|
4366
|
+
const scriptValue = pkg.scripts[effectiveScript];
|
|
4367
|
+
if (!scriptValue) continue;
|
|
4368
|
+
const rawScript = splitCommand(scriptValue);
|
|
4369
|
+
if (rawScript.length === 0) continue;
|
|
4370
|
+
const pm = detectPackageManager(pkg.dir);
|
|
4371
|
+
const commandArgs = [pm, "run", effectiveScript];
|
|
4372
|
+
const proxied = appOverride.proxy ?? isServerCommand(rawScript);
|
|
4373
|
+
let name;
|
|
4374
|
+
let label;
|
|
4375
|
+
if (appOverride.name) {
|
|
4376
|
+
name = appOverride.name.split(".").map((l) => truncateLabel(l)).join(".");
|
|
4377
|
+
label = appOverride.name;
|
|
4378
|
+
} else {
|
|
4379
|
+
let pkgLabel;
|
|
4380
|
+
if (pkg.name) {
|
|
4381
|
+
const sanitized = sanitizeForHostname(pkg.name);
|
|
4382
|
+
pkgLabel = sanitized || rel.replace(/\//g, "-");
|
|
4383
|
+
} else {
|
|
4384
|
+
pkgLabel = rel.replace(/\//g, "-");
|
|
4385
|
+
}
|
|
4386
|
+
name = pkgLabel === projectName ? projectName : `${pkgLabel}.${projectName}`;
|
|
4387
|
+
label = pkg.scope ? `@${pkg.scope}/${pkg.name}` : pkg.name ?? rel;
|
|
4388
|
+
}
|
|
4389
|
+
apps.push({ pkg, name, label, commandArgs, appPort: appOverride.appPort, proxied });
|
|
4390
|
+
}
|
|
4391
|
+
if (apps.length === 0) {
|
|
4392
|
+
console.error(colors_default.yellow(`No workspace packages have a "${scriptName}" script.`));
|
|
4393
|
+
process.exit(1);
|
|
4394
|
+
}
|
|
4395
|
+
apps.sort((a, b) => a.label.localeCompare(b.label));
|
|
4396
|
+
const proxiedApps = apps.filter((a) => a.proxied);
|
|
4397
|
+
const taskApps = apps.filter((a) => !a.proxied);
|
|
4398
|
+
console.log(chalk.blue.bold(`
|
|
4399
|
+
portless
|
|
4400
|
+
`));
|
|
4401
|
+
let { dir, port, tls: tls2, tld } = await discoverState();
|
|
4402
|
+
if (proxiedApps.length > 0) {
|
|
4403
|
+
let multiDesired;
|
|
4404
|
+
try {
|
|
4405
|
+
multiDesired = resolveProxyDesiredState(false);
|
|
4406
|
+
} catch (err) {
|
|
4407
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
4408
|
+
process.exit(1);
|
|
4409
|
+
}
|
|
4410
|
+
const ensureResult = await ensureProxyRunning(port, tls2, multiDesired);
|
|
4411
|
+
if (ensureResult.started) {
|
|
4412
|
+
dir = ensureResult.state.dir;
|
|
4413
|
+
port = ensureResult.state.port;
|
|
4414
|
+
tls2 = ensureResult.state.tls;
|
|
4415
|
+
tld = ensureResult.state.tld;
|
|
4416
|
+
} else {
|
|
4417
|
+
({ dir, port, tls: tls2, tld } = await discoverState());
|
|
4418
|
+
}
|
|
4419
|
+
if (tls2 && !isCATrusted(dir)) {
|
|
4420
|
+
await handleTrust();
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
const useTurbo = loaded?.config.turbo !== false && hasTurboConfig(wsRoot);
|
|
4424
|
+
if (useTurbo) {
|
|
4425
|
+
await runWithTurbo(wsRoot, dir, port, tls2, tld, scriptName, proxiedApps, taskApps, extraArgs);
|
|
4426
|
+
} else {
|
|
4427
|
+
await runWithDirectSpawn(dir, port, tls2, tld, proxiedApps, taskApps);
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName, proxiedApps, taskApps, extraArgs = []) {
|
|
4431
|
+
const store = new RouteStore(stateDir, {
|
|
4432
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
4433
|
+
});
|
|
4434
|
+
const manifest = {};
|
|
4435
|
+
const routes = [];
|
|
4436
|
+
const appUrls = [];
|
|
4437
|
+
for (const app of proxiedApps) {
|
|
4438
|
+
const usesPortless = app.commandArgs[0] === "portless";
|
|
4439
|
+
if (usesPortless) {
|
|
4440
|
+
appUrls.push({ label: app.label, url: "(managed by portless)" });
|
|
4441
|
+
continue;
|
|
4442
|
+
}
|
|
4443
|
+
const appPort = app.appPort ?? await findFreePort();
|
|
4444
|
+
const protocol = tls2 ? "https" : "http";
|
|
4445
|
+
const portSuffix = tls2 && proxyPort === 443 || !tls2 && proxyPort === 80 ? "" : `:${proxyPort}`;
|
|
4446
|
+
const url = `${protocol}://${app.name}.${tld}${portSuffix}`;
|
|
4447
|
+
appUrls.push({ label: app.label, url });
|
|
4448
|
+
const hostname = parseHostname(app.name, tld);
|
|
4449
|
+
store.addRoute(hostname, appPort, process.pid);
|
|
4450
|
+
routes.push({ hostname });
|
|
4451
|
+
const entry = {
|
|
4452
|
+
PORT: String(appPort),
|
|
4453
|
+
HOST: "127.0.0.1",
|
|
4454
|
+
PORTLESS_URL: url
|
|
4455
|
+
};
|
|
4456
|
+
if (tls2) {
|
|
4457
|
+
const caPath = path8.join(stateDir, "ca.pem");
|
|
4458
|
+
if (fs8.existsSync(caPath)) {
|
|
4459
|
+
entry.NODE_EXTRA_CA_CERTS = caPath;
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
manifest[app.pkg.dir] = entry;
|
|
4463
|
+
}
|
|
4464
|
+
ensureEnvLoader();
|
|
4465
|
+
writeManifest(manifest);
|
|
4466
|
+
if (appUrls.length > 0) {
|
|
4467
|
+
const maxLabel = Math.max(...appUrls.map((a) => a.label.length));
|
|
4468
|
+
for (const { label, url } of appUrls) {
|
|
4469
|
+
const pad = " ".repeat(maxLabel - label.length);
|
|
4470
|
+
console.log(` ${label}${pad} ${chalk.dim(url)}`);
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
console.log("");
|
|
4474
|
+
const pm = detectPackageManager(wsRoot);
|
|
4475
|
+
const useRootScript = hasScript(scriptName, wsRoot);
|
|
4476
|
+
const turboArgs = useRootScript ? [pm, "run", scriptName, ...extraArgs] : pm === "npm" ? ["npx", "turbo", "run", scriptName, ...extraArgs] : pm === "bun" ? ["bunx", "turbo", "run", scriptName, ...extraArgs] : [pm, "exec", "turbo", "run", scriptName, ...extraArgs];
|
|
4477
|
+
const turboChild = spawn3(turboArgs[0], turboArgs.slice(1), {
|
|
4478
|
+
stdio: "inherit",
|
|
4479
|
+
cwd: wsRoot,
|
|
4480
|
+
env: {
|
|
4481
|
+
...process.env,
|
|
4482
|
+
NODE_OPTIONS: buildNodeOptions()
|
|
4483
|
+
}
|
|
4484
|
+
});
|
|
4485
|
+
const SIGKILL_TIMEOUT_MS = 5e3;
|
|
4486
|
+
let cleanedUp = false;
|
|
4487
|
+
const cleanup = () => {
|
|
4488
|
+
if (cleanedUp) return;
|
|
4489
|
+
cleanedUp = true;
|
|
4490
|
+
try {
|
|
4491
|
+
turboChild.kill("SIGTERM");
|
|
4492
|
+
} catch {
|
|
4493
|
+
}
|
|
4494
|
+
setTimeout(() => {
|
|
4495
|
+
if (turboChild.exitCode === null && !turboChild.killed) {
|
|
4496
|
+
try {
|
|
4497
|
+
turboChild.kill("SIGKILL");
|
|
4498
|
+
} catch {
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
}, SIGKILL_TIMEOUT_MS).unref();
|
|
4502
|
+
for (const { hostname } of routes) {
|
|
4503
|
+
try {
|
|
4504
|
+
store.removeRoute(hostname);
|
|
4505
|
+
} catch {
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
removeManifest();
|
|
4509
|
+
};
|
|
4510
|
+
process.on("SIGINT", cleanup);
|
|
4511
|
+
process.on("SIGTERM", cleanup);
|
|
4512
|
+
const exitCode = await new Promise((resolve3) => {
|
|
4513
|
+
turboChild.on("exit", (code) => resolve3(code));
|
|
4514
|
+
});
|
|
4515
|
+
cleanup();
|
|
4516
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
4517
|
+
process.exit(exitCode);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, taskApps) {
|
|
4521
|
+
const children = [];
|
|
4522
|
+
const exitCodes = /* @__PURE__ */ new Map();
|
|
4523
|
+
const appUrls = [];
|
|
4524
|
+
const routeEntries = [];
|
|
4525
|
+
for (const app of proxiedApps) {
|
|
4526
|
+
const { child, displayUrl, route } = await spawnProxiedApp(
|
|
4527
|
+
app,
|
|
4528
|
+
stateDir,
|
|
4529
|
+
proxyPort,
|
|
4530
|
+
tls2,
|
|
4531
|
+
tld,
|
|
4532
|
+
exitCodes
|
|
4533
|
+
);
|
|
4534
|
+
children.push(child);
|
|
4535
|
+
if (route) routeEntries.push(route);
|
|
4536
|
+
appUrls.push({ label: app.label, url: displayUrl });
|
|
4537
|
+
}
|
|
4538
|
+
const taskLabels = [];
|
|
4539
|
+
for (const app of taskApps) {
|
|
4540
|
+
children.push(spawnTaskApp(app, exitCodes));
|
|
4541
|
+
taskLabels.push(app.label);
|
|
4542
|
+
}
|
|
4543
|
+
if (appUrls.length > 0) {
|
|
4544
|
+
const maxLabel = Math.max(...appUrls.map((a) => a.label.length));
|
|
4545
|
+
for (const { label, url } of appUrls) {
|
|
4546
|
+
const pad = " ".repeat(maxLabel - label.length);
|
|
4547
|
+
console.log(` ${label}${pad} ${chalk.dim(url)}`);
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
console.log("");
|
|
4551
|
+
const SIGKILL_TIMEOUT_MS = 5e3;
|
|
4552
|
+
let cleanedUp = false;
|
|
4553
|
+
const cleanup = () => {
|
|
4554
|
+
if (cleanedUp) return;
|
|
4555
|
+
cleanedUp = true;
|
|
4556
|
+
for (const child of children) {
|
|
4557
|
+
try {
|
|
4558
|
+
child.kill("SIGTERM");
|
|
4559
|
+
} catch {
|
|
4560
|
+
}
|
|
4561
|
+
}
|
|
4562
|
+
setTimeout(() => {
|
|
4563
|
+
for (const child of children) {
|
|
4564
|
+
if (child.exitCode === null && !child.killed) {
|
|
4565
|
+
try {
|
|
4566
|
+
child.kill("SIGKILL");
|
|
4567
|
+
} catch {
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
}, SIGKILL_TIMEOUT_MS).unref();
|
|
4572
|
+
for (const { store, hostname } of routeEntries) {
|
|
4573
|
+
try {
|
|
4574
|
+
store.removeRoute(hostname);
|
|
4575
|
+
} catch {
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
};
|
|
4579
|
+
process.on("SIGINT", cleanup);
|
|
4580
|
+
process.on("SIGTERM", cleanup);
|
|
4581
|
+
await Promise.all(
|
|
4582
|
+
children.map(
|
|
4583
|
+
(child) => new Promise((resolve3) => {
|
|
4584
|
+
child.on("exit", () => resolve3());
|
|
4585
|
+
})
|
|
4586
|
+
)
|
|
4587
|
+
);
|
|
4588
|
+
const failed = [...exitCodes.entries()].filter(([, code]) => code !== 0 && code !== null);
|
|
4589
|
+
if (failed.length > 0) {
|
|
4590
|
+
console.error(
|
|
4591
|
+
colors_default.red(
|
|
4592
|
+
`
|
|
4593
|
+
${failed.length} app${failed.length === 1 ? "" : "s"} exited with errors: ${failed.map(([name, code]) => `${name} (${code})`).join(", ")}`
|
|
4594
|
+
)
|
|
4595
|
+
);
|
|
4596
|
+
process.exit(1);
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
async function handleRunMode(args, globalScript) {
|
|
2957
4600
|
const parsed = parseRunArgs(args);
|
|
4601
|
+
const appConfig = loadAppConfig();
|
|
4602
|
+
if (parsed.commandArgs.length === 0) {
|
|
4603
|
+
const scriptName = globalScript ?? appConfig?.script ?? "dev";
|
|
4604
|
+
const resolved = resolveScriptCommand(scriptName, process.cwd());
|
|
4605
|
+
if (resolved) {
|
|
4606
|
+
parsed.commandArgs = resolved;
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
2958
4609
|
if (parsed.commandArgs.length === 0) {
|
|
2959
4610
|
console.error(colors_default.red("Error: No command provided."));
|
|
2960
4611
|
console.error(colors_default.blue("Usage:"));
|
|
@@ -2968,11 +4619,17 @@ async function handleRunMode(args) {
|
|
|
2968
4619
|
if (parsed.name) {
|
|
2969
4620
|
baseName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
2970
4621
|
nameSource = "--name flag";
|
|
4622
|
+
} else if (appConfig?.name) {
|
|
4623
|
+
baseName = appConfig.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
4624
|
+
nameSource = "portless.json";
|
|
2971
4625
|
} else {
|
|
2972
4626
|
const inferred = inferProjectName();
|
|
2973
4627
|
baseName = inferred.name;
|
|
2974
4628
|
nameSource = inferred.source;
|
|
2975
4629
|
}
|
|
4630
|
+
if (!parsed.appPort && appConfig?.appPort) {
|
|
4631
|
+
parsed.appPort = appConfig.appPort;
|
|
4632
|
+
}
|
|
2976
4633
|
const worktree = detectWorktreePrefix();
|
|
2977
4634
|
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
2978
4635
|
const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
|
|
@@ -3004,6 +4661,12 @@ async function handleNamedMode(args) {
|
|
|
3004
4661
|
console.error(colors_default.cyan(" portless myapp next dev"));
|
|
3005
4662
|
process.exit(1);
|
|
3006
4663
|
}
|
|
4664
|
+
if (!parsed.appPort) {
|
|
4665
|
+
const appConfig = loadAppConfig();
|
|
4666
|
+
if (appConfig?.appPort) {
|
|
4667
|
+
parsed.appPort = appConfig.appPort;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
3007
4670
|
const safeName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
3008
4671
|
const { dir, port, tls: tls2, tld, lanMode, lanIp } = await discoverState();
|
|
3009
4672
|
const store = new RouteStore(dir, {
|
|
@@ -3077,6 +4740,13 @@ async function main() {
|
|
|
3077
4740
|
process.env[INTERNAL_LAN_IP_ENV] = autoIpResult;
|
|
3078
4741
|
process.env.PORTLESS_LAN = "1";
|
|
3079
4742
|
}
|
|
4743
|
+
const scriptResult = stripGlobalFlag("--script", true);
|
|
4744
|
+
if (scriptResult === false) {
|
|
4745
|
+
console.error(colors_default.red("Error: --script requires a script name."));
|
|
4746
|
+
console.error(colors_default.cyan(" portless --script start"));
|
|
4747
|
+
process.exit(1);
|
|
4748
|
+
}
|
|
4749
|
+
const globalScript = typeof scriptResult === "string" ? scriptResult : void 0;
|
|
3080
4750
|
if (args[0] === "--name") {
|
|
3081
4751
|
args.shift();
|
|
3082
4752
|
if (!args[0]) {
|
|
@@ -3102,8 +4772,15 @@ async function main() {
|
|
|
3102
4772
|
args.shift();
|
|
3103
4773
|
}
|
|
3104
4774
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
3105
|
-
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean")) {
|
|
3106
|
-
const
|
|
4775
|
+
if (skipPortless && (isRunCommand || args.length === 0 || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean")) {
|
|
4776
|
+
const parsed = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
4777
|
+
let commandArgs = parsed.commandArgs;
|
|
4778
|
+
if (commandArgs.length === 0 && (isRunCommand || args.length === 0)) {
|
|
4779
|
+
const appConfig = loadAppConfig();
|
|
4780
|
+
const scriptName = globalScript ?? appConfig?.script ?? "dev";
|
|
4781
|
+
const resolved = resolveScriptCommand(scriptName, process.cwd());
|
|
4782
|
+
if (resolved) commandArgs = resolved;
|
|
4783
|
+
}
|
|
3107
4784
|
if (commandArgs.length === 0) {
|
|
3108
4785
|
console.error(colors_default.red("Error: No command provided."));
|
|
3109
4786
|
process.exit(1);
|
|
@@ -3112,7 +4789,14 @@ async function main() {
|
|
|
3112
4789
|
return;
|
|
3113
4790
|
}
|
|
3114
4791
|
if (!isRunCommand) {
|
|
3115
|
-
if (args
|
|
4792
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
4793
|
+
printHelp();
|
|
4794
|
+
return;
|
|
4795
|
+
}
|
|
4796
|
+
if (args.length === 0 || args[0] === "--") {
|
|
4797
|
+
const extraArgs = args[0] === "--" ? args.slice(1) : [];
|
|
4798
|
+
const handled = await handleDefaultMode(globalScript, extraArgs);
|
|
4799
|
+
if (handled) return;
|
|
3116
4800
|
printHelp();
|
|
3117
4801
|
return;
|
|
3118
4802
|
}
|
|
@@ -3150,7 +4834,7 @@ async function main() {
|
|
|
3150
4834
|
}
|
|
3151
4835
|
}
|
|
3152
4836
|
if (isRunCommand) {
|
|
3153
|
-
await handleRunMode(args);
|
|
4837
|
+
await handleRunMode(args, globalScript);
|
|
3154
4838
|
} else {
|
|
3155
4839
|
await handleNamedMode(args);
|
|
3156
4840
|
}
|