portless 0.8.0 → 0.9.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 +42 -50
- package/dist/{chunk-KKXL2CMI.js → chunk-5BR7NCNI.js} +66 -30
- package/dist/cli.js +353 -199
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_TLD,
|
|
4
|
+
FALLBACK_PROXY_PORT,
|
|
4
5
|
FILE_MODE,
|
|
5
6
|
PRIVILEGED_PORT_THRESHOLD,
|
|
6
7
|
RISKY_TLDS,
|
|
7
8
|
RouteConflictError,
|
|
8
9
|
RouteStore,
|
|
10
|
+
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
11
|
+
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
9
12
|
cleanHostsFile,
|
|
13
|
+
createHttpRedirectServer,
|
|
10
14
|
createProxyServer,
|
|
11
15
|
discoverState,
|
|
12
16
|
findFreePort,
|
|
@@ -15,16 +19,15 @@ import {
|
|
|
15
19
|
formatUrl,
|
|
16
20
|
getDefaultPort,
|
|
17
21
|
getDefaultTld,
|
|
22
|
+
getProtocolPort,
|
|
18
23
|
injectFrameworkFlags,
|
|
19
24
|
isErrnoException,
|
|
20
|
-
|
|
25
|
+
isHttpsEnvDisabled,
|
|
21
26
|
isProxyRunning,
|
|
22
27
|
isWildcardEnvEnabled,
|
|
23
28
|
isWindows,
|
|
24
29
|
parseHostname,
|
|
25
30
|
prompt,
|
|
26
|
-
readTldFromDir,
|
|
27
|
-
readTlsMarker,
|
|
28
31
|
resolveStateDir,
|
|
29
32
|
spawnCommand,
|
|
30
33
|
syncHostsFile,
|
|
@@ -32,7 +35,7 @@ import {
|
|
|
32
35
|
waitForProxy,
|
|
33
36
|
writeTldFile,
|
|
34
37
|
writeTlsMarker
|
|
35
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-5BR7NCNI.js";
|
|
36
39
|
|
|
37
40
|
// src/colors.ts
|
|
38
41
|
function supportsColor() {
|
|
@@ -538,7 +541,7 @@ function trustCA(stateDir) {
|
|
|
538
541
|
if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES")) {
|
|
539
542
|
return {
|
|
540
543
|
trusted: false,
|
|
541
|
-
error: "Permission denied. Try:
|
|
544
|
+
error: "Permission denied. Try: portless trust"
|
|
542
545
|
};
|
|
543
546
|
}
|
|
544
547
|
return { trusted: false, error: message };
|
|
@@ -721,11 +724,35 @@ function readBranchFromHead(gitdir) {
|
|
|
721
724
|
|
|
722
725
|
// src/cli.ts
|
|
723
726
|
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
724
|
-
var SUDO_PREFIX = isWindows ? "" : "sudo ";
|
|
725
727
|
var DEBOUNCE_MS = 100;
|
|
726
728
|
var POLL_INTERVAL_MS = 3e3;
|
|
727
729
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
728
730
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
731
|
+
function getEntryScript() {
|
|
732
|
+
const script = process.argv[1];
|
|
733
|
+
if (!script) {
|
|
734
|
+
throw new Error("Cannot determine portless entry script (process.argv[1] is undefined)");
|
|
735
|
+
}
|
|
736
|
+
return script;
|
|
737
|
+
}
|
|
738
|
+
function collectPortlessEnvArgs() {
|
|
739
|
+
const envArgs = [];
|
|
740
|
+
for (const key of Object.keys(process.env)) {
|
|
741
|
+
if (key.startsWith("PORTLESS_") && process.env[key]) {
|
|
742
|
+
envArgs.push(`${key}=${process.env[key]}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return envArgs;
|
|
746
|
+
}
|
|
747
|
+
function sudoStop(port) {
|
|
748
|
+
const stopArgs = [process.execPath, getEntryScript(), "proxy", "stop", "-p", String(port)];
|
|
749
|
+
console.log(colors_default.yellow("Proxy is running as root. Elevating with sudo to stop it..."));
|
|
750
|
+
const result = spawnSync("sudo", ["env", ...collectPortlessEnvArgs(), ...stopArgs], {
|
|
751
|
+
stdio: "inherit",
|
|
752
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
753
|
+
});
|
|
754
|
+
return result.status === 0;
|
|
755
|
+
}
|
|
729
756
|
function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
730
757
|
store.ensureDir();
|
|
731
758
|
const isTls = !!tlsOptions;
|
|
@@ -786,15 +813,22 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
786
813
|
);
|
|
787
814
|
} else if (err.code === "EACCES") {
|
|
788
815
|
console.error(colors_default.red(`Permission denied for port ${proxyPort}.`));
|
|
789
|
-
console.error(colors_default.blue("
|
|
790
|
-
console.error(colors_default.cyan("
|
|
791
|
-
console.error(colors_default.blue("Or use a non-privileged port (no sudo needed):"));
|
|
792
|
-
console.error(colors_default.cyan(" portless proxy start"));
|
|
816
|
+
console.error(colors_default.blue("Use an unprivileged port (no sudo needed):"));
|
|
817
|
+
console.error(colors_default.cyan(" portless proxy start -p 1355"));
|
|
793
818
|
} else {
|
|
794
819
|
console.error(colors_default.red(`Proxy error: ${err.message}`));
|
|
795
820
|
}
|
|
821
|
+
if (redirectServer) redirectServer.close();
|
|
796
822
|
process.exit(1);
|
|
797
823
|
});
|
|
824
|
+
let redirectServer = null;
|
|
825
|
+
if (isTls && proxyPort !== 80) {
|
|
826
|
+
redirectServer = createHttpRedirectServer(proxyPort);
|
|
827
|
+
redirectServer.on("error", () => {
|
|
828
|
+
redirectServer = null;
|
|
829
|
+
});
|
|
830
|
+
redirectServer.listen(80);
|
|
831
|
+
}
|
|
798
832
|
server.listen(proxyPort, () => {
|
|
799
833
|
fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
800
834
|
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
@@ -807,6 +841,9 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
807
841
|
console.log(
|
|
808
842
|
colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
|
|
809
843
|
);
|
|
844
|
+
if (redirectServer) {
|
|
845
|
+
console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
|
|
846
|
+
}
|
|
810
847
|
});
|
|
811
848
|
let exiting = false;
|
|
812
849
|
const cleanup = () => {
|
|
@@ -817,6 +854,9 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
817
854
|
if (watcher) {
|
|
818
855
|
watcher.close();
|
|
819
856
|
}
|
|
857
|
+
if (redirectServer) {
|
|
858
|
+
redirectServer.close();
|
|
859
|
+
}
|
|
820
860
|
try {
|
|
821
861
|
fs3.unlinkSync(store.pidPath);
|
|
822
862
|
} catch {
|
|
@@ -836,10 +876,21 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
836
876
|
console.log(colors_default.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
837
877
|
console.log(colors_default.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
838
878
|
}
|
|
879
|
+
function sudoStopOrHint(port) {
|
|
880
|
+
if (!isWindows) {
|
|
881
|
+
if (!sudoStop(port)) {
|
|
882
|
+
console.error(colors_default.red("Failed to stop proxy with sudo."));
|
|
883
|
+
console.error(colors_default.blue("Try manually:"));
|
|
884
|
+
console.error(colors_default.cyan(` portless proxy stop -p ${port}`));
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
console.error(colors_default.red("Permission denied. The proxy was started with elevated privileges."));
|
|
888
|
+
console.error(colors_default.blue("Stop it with:"));
|
|
889
|
+
console.error(colors_default.cyan(" Run portless proxy stop as Administrator"));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
839
892
|
async function stopProxy(store, proxyPort, _tls) {
|
|
840
893
|
const pidPath = store.pidPath;
|
|
841
|
-
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
842
|
-
const sudoHint = needsSudo ? "sudo " : "";
|
|
843
894
|
if (!fs3.existsSync(pidPath)) {
|
|
844
895
|
if (await isProxyRunning(proxyPort)) {
|
|
845
896
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
@@ -854,15 +905,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
854
905
|
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
855
906
|
} catch (err) {
|
|
856
907
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
857
|
-
|
|
858
|
-
colors_default.red("Permission denied. The proxy was started with elevated privileges.")
|
|
859
|
-
);
|
|
860
|
-
console.error(colors_default.blue("Stop it with:"));
|
|
861
|
-
console.error(
|
|
862
|
-
colors_default.cyan(
|
|
863
|
-
isWindows ? " Run portless proxy stop as Administrator" : " sudo portless proxy stop"
|
|
864
|
-
)
|
|
865
|
-
);
|
|
908
|
+
sudoStopOrHint(proxyPort);
|
|
866
909
|
} else {
|
|
867
910
|
const message = err instanceof Error ? err.message : String(err);
|
|
868
911
|
console.error(colors_default.red(`Failed to stop proxy: ${message}`));
|
|
@@ -875,9 +918,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
875
918
|
}
|
|
876
919
|
}
|
|
877
920
|
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
878
|
-
|
|
879
|
-
console.error(colors_default.blue("Try stopping with sudo:"));
|
|
880
|
-
console.error(colors_default.cyan(" sudo portless proxy stop"));
|
|
921
|
+
sudoStopOrHint(proxyPort);
|
|
881
922
|
} else {
|
|
882
923
|
console.error(colors_default.red(`Could not identify the process on port ${proxyPort}.`));
|
|
883
924
|
console.error(colors_default.blue("Try manually:"));
|
|
@@ -901,7 +942,11 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
901
942
|
}
|
|
902
943
|
try {
|
|
903
944
|
process.kill(pid, 0);
|
|
904
|
-
} catch {
|
|
945
|
+
} catch (err) {
|
|
946
|
+
if (isErrnoException(err) && err.code === "EPERM") {
|
|
947
|
+
sudoStopOrHint(proxyPort);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
905
950
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
906
951
|
fs3.unlinkSync(pidPath);
|
|
907
952
|
try {
|
|
@@ -929,11 +974,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
929
974
|
console.log(colors_default.green("Proxy stopped."));
|
|
930
975
|
} catch (err) {
|
|
931
976
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
932
|
-
|
|
933
|
-
colors_default.red("Permission denied. The proxy was started with elevated privileges.")
|
|
934
|
-
);
|
|
935
|
-
console.error(colors_default.blue("Stop it with:"));
|
|
936
|
-
console.error(colors_default.cyan(` ${sudoHint}portless proxy stop`));
|
|
977
|
+
sudoStopOrHint(proxyPort);
|
|
937
978
|
} else {
|
|
938
979
|
const message = err instanceof Error ? err.message : String(err);
|
|
939
980
|
console.error(colors_default.red(`Failed to stop proxy: ${message}`));
|
|
@@ -963,8 +1004,8 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
963
1004
|
}
|
|
964
1005
|
console.log();
|
|
965
1006
|
}
|
|
966
|
-
async function runApp(
|
|
967
|
-
|
|
1007
|
+
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
|
|
1008
|
+
let store = initialStore;
|
|
968
1009
|
let envTld;
|
|
969
1010
|
try {
|
|
970
1011
|
envTld = getDefaultTld();
|
|
@@ -982,7 +1023,7 @@ async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, tld,
|
|
|
982
1023
|
console.log(colors_default.blue.bold(`
|
|
983
1024
|
portless
|
|
984
1025
|
`));
|
|
985
|
-
console.log(colors_default.gray(`-- ${
|
|
1026
|
+
console.log(colors_default.gray(`-- ${parseHostname(name, tld)} (auto-resolves to 127.0.0.1)`));
|
|
986
1027
|
if (autoInfo) {
|
|
987
1028
|
const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
|
|
988
1029
|
console.log(colors_default.gray(`-- Name "${baseName}" (from ${autoInfo.nameSource})`));
|
|
@@ -991,18 +1032,22 @@ portless
|
|
|
991
1032
|
}
|
|
992
1033
|
}
|
|
993
1034
|
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
994
|
-
const
|
|
1035
|
+
const wantTls = !isHttpsEnvDisabled();
|
|
1036
|
+
const defaultPort = getDefaultPort(wantTls);
|
|
995
1037
|
const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1038
|
+
if (needsSudo && !process.stdin.isTTY) {
|
|
1039
|
+
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
1040
|
+
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
1041
|
+
console.error(colors_default.cyan(" portless proxy start"));
|
|
1042
|
+
console.error(
|
|
1043
|
+
colors_default.blue(
|
|
1044
|
+
`Option 2: use an unprivileged port (no sudo needed, URLs will include :${FALLBACK_PROXY_PORT}):`
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1047
|
+
console.error(colors_default.cyan(` portless proxy start -p ${FALLBACK_PROXY_PORT}`));
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
if (needsSudo && process.stdin.isTTY) {
|
|
1006
1051
|
const answer = await prompt(colors_default.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
1007
1052
|
if (answer === "n" || answer === "no") {
|
|
1008
1053
|
console.log(colors_default.gray("Cancelled."));
|
|
@@ -1013,57 +1058,50 @@ portless
|
|
|
1013
1058
|
spawnCommand(commandArgs);
|
|
1014
1059
|
return;
|
|
1015
1060
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
console.log(colors_default.yellow("Starting proxy..."));
|
|
1034
|
-
const startArgs = [process.argv[1], "proxy", "start"];
|
|
1035
|
-
if (wantHttps) startArgs.push("--https");
|
|
1036
|
-
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
1037
|
-
const result = spawnSync(process.execPath, startArgs, {
|
|
1038
|
-
stdio: "inherit",
|
|
1039
|
-
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1040
|
-
});
|
|
1041
|
-
if (result.status !== 0) {
|
|
1042
|
-
if (!await isProxyRunning(proxyPort)) {
|
|
1043
|
-
console.error(colors_default.red("Failed to start proxy."));
|
|
1044
|
-
console.error(colors_default.blue("Try starting it manually:"));
|
|
1045
|
-
console.error(colors_default.cyan(" portless proxy start"));
|
|
1046
|
-
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
console.log(colors_default.yellow("Starting proxy..."));
|
|
1063
|
+
const startArgs = [getEntryScript(), "proxy", "start"];
|
|
1064
|
+
if (!wantTls) startArgs.push("--no-tls");
|
|
1065
|
+
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
1066
|
+
const result = spawnSync(process.execPath, startArgs, {
|
|
1067
|
+
stdio: "inherit",
|
|
1068
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1069
|
+
});
|
|
1070
|
+
let discovered = null;
|
|
1071
|
+
if (result.status === 0) {
|
|
1072
|
+
for (let i = 0; i < WAIT_FOR_PROXY_MAX_ATTEMPTS; i++) {
|
|
1073
|
+
await new Promise((r) => setTimeout(r, WAIT_FOR_PROXY_INTERVAL_MS));
|
|
1074
|
+
const state = await discoverState();
|
|
1075
|
+
if (await isProxyRunning(state.port)) {
|
|
1076
|
+
discovered = state;
|
|
1077
|
+
break;
|
|
1047
1078
|
}
|
|
1048
1079
|
}
|
|
1049
1080
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
console.error(colors_default.
|
|
1056
|
-
console.error(colors_default.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
|
|
1081
|
+
if (!discovered) {
|
|
1082
|
+
console.error(colors_default.red("Failed to start proxy."));
|
|
1083
|
+
const fallbackDir = resolveStateDir(getDefaultPort(wantTls));
|
|
1084
|
+
const logPath = path3.join(fallbackDir, "proxy.log");
|
|
1085
|
+
console.error(colors_default.blue("Try starting it manually:"));
|
|
1086
|
+
console.error(colors_default.cyan(" portless proxy start"));
|
|
1057
1087
|
if (fs3.existsSync(logPath)) {
|
|
1058
1088
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1059
1089
|
}
|
|
1060
1090
|
process.exit(1);
|
|
1091
|
+
return;
|
|
1061
1092
|
}
|
|
1062
|
-
|
|
1093
|
+
proxyPort = discovered.port;
|
|
1094
|
+
stateDir = discovered.dir;
|
|
1095
|
+
tld = discovered.tld;
|
|
1096
|
+
tls2 = discovered.tls;
|
|
1097
|
+
store = new RouteStore(stateDir, {
|
|
1098
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1099
|
+
});
|
|
1063
1100
|
console.log(colors_default.green("Proxy started in background"));
|
|
1064
1101
|
} else {
|
|
1065
1102
|
console.log(colors_default.gray("-- Proxy is running"));
|
|
1066
1103
|
}
|
|
1104
|
+
const hostname = parseHostname(name, tld);
|
|
1067
1105
|
const port = desiredPort ?? await findFreePort();
|
|
1068
1106
|
if (desiredPort) {
|
|
1069
1107
|
console.log(colors_default.green(`-- Using port ${port} (fixed)`));
|
|
@@ -1160,9 +1198,9 @@ ${colors_default.bold("Name inference (in order):")}
|
|
|
1160
1198
|
(e.g. feature-auth.myapp.localhost).
|
|
1161
1199
|
|
|
1162
1200
|
${colors_default.bold("Examples:")}
|
|
1163
|
-
portless run next dev # ->
|
|
1164
|
-
portless run --name myapp next dev # ->
|
|
1165
|
-
portless run vite dev # ->
|
|
1201
|
+
portless run next dev # -> https://<project>.localhost
|
|
1202
|
+
portless run --name myapp next dev # -> https://myapp.localhost
|
|
1203
|
+
portless run vite dev # -> https://<project>.localhost
|
|
1166
1204
|
portless run --app-port 3000 pnpm start
|
|
1167
1205
|
`);
|
|
1168
1206
|
process.exit(0);
|
|
@@ -1242,9 +1280,9 @@ ${colors_default.bold("Install:")}
|
|
|
1242
1280
|
Do NOT add portless as a project dependency.
|
|
1243
1281
|
|
|
1244
1282
|
${colors_default.bold("Usage:")}
|
|
1245
|
-
${colors_default.cyan("portless proxy start")} Start the proxy (
|
|
1246
|
-
${colors_default.cyan("portless proxy start --
|
|
1247
|
-
${colors_default.cyan("portless proxy start -p
|
|
1283
|
+
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
1284
|
+
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
1285
|
+
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1248
1286
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
1249
1287
|
${colors_default.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
1250
1288
|
${colors_default.cyan("portless run <cmd>")} Infer name from project, run through proxy
|
|
@@ -1257,14 +1295,14 @@ ${colors_default.bold("Usage:")}
|
|
|
1257
1295
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1258
1296
|
|
|
1259
1297
|
${colors_default.bold("Examples:")}
|
|
1260
|
-
portless proxy start # Start proxy on port
|
|
1261
|
-
portless proxy start --
|
|
1262
|
-
portless myapp next dev # ->
|
|
1263
|
-
portless myapp vite dev # ->
|
|
1264
|
-
portless api.myapp pnpm start # ->
|
|
1265
|
-
portless run next dev # ->
|
|
1266
|
-
portless run next dev # in worktree ->
|
|
1267
|
-
portless get backend # ->
|
|
1298
|
+
portless proxy start # Start HTTPS proxy on port 443
|
|
1299
|
+
portless proxy start --no-tls # Start HTTP proxy on port 80
|
|
1300
|
+
portless myapp next dev # -> https://myapp.localhost
|
|
1301
|
+
portless myapp vite dev # -> https://myapp.localhost
|
|
1302
|
+
portless api.myapp pnpm start # -> https://api.myapp.localhost
|
|
1303
|
+
portless run next dev # -> https://<project>.localhost
|
|
1304
|
+
portless run next dev # in worktree -> https://<worktree>.<project>.localhost
|
|
1305
|
+
portless get backend # -> https://backend.localhost (for cross-service refs)
|
|
1268
1306
|
# Wildcard subdomains: tenant.myapp.localhost also routes to myapp
|
|
1269
1307
|
|
|
1270
1308
|
${colors_default.bold("In package.json:")}
|
|
@@ -1275,28 +1313,28 @@ ${colors_default.bold("In package.json:")}
|
|
|
1275
1313
|
}
|
|
1276
1314
|
|
|
1277
1315
|
${colors_default.bold("How it works:")}
|
|
1278
|
-
1. Start the proxy once (
|
|
1316
|
+
1. Start the proxy once (HTTPS on port 443 by default, auto-elevates with sudo)
|
|
1279
1317
|
2. Run your apps - they auto-start the proxy and register automatically
|
|
1280
1318
|
(apps get a random port in the 4000-4999 range via PORT)
|
|
1281
|
-
3. Access via
|
|
1319
|
+
3. Access via https://<name>.localhost
|
|
1282
1320
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
1283
1321
|
5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular,
|
|
1284
1322
|
Expo, React Native) get --port and --host flags injected automatically
|
|
1285
1323
|
|
|
1286
|
-
${colors_default.bold("HTTP/2 + HTTPS:")}
|
|
1287
|
-
|
|
1324
|
+
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
1325
|
+
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
1288
1326
|
On first use, portless generates a local CA and adds it to your
|
|
1289
|
-
system trust store. No browser warnings.
|
|
1327
|
+
system trust store. No browser warnings. Disable with --no-tls.
|
|
1290
1328
|
|
|
1291
1329
|
${colors_default.bold("Options:")}
|
|
1292
1330
|
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
1293
1331
|
Adds worktree prefix in git worktrees
|
|
1294
|
-
-p, --port <number> Port for the proxy
|
|
1295
|
-
|
|
1296
|
-
--
|
|
1297
|
-
--
|
|
1298
|
-
--
|
|
1299
|
-
--
|
|
1332
|
+
-p, --port <number> Port for the proxy (default: 443, or 80 with --no-tls)
|
|
1333
|
+
Standard ports auto-elevate with sudo on macOS/Linux
|
|
1334
|
+
--no-tls Disable HTTPS (use plain HTTP on port 80)
|
|
1335
|
+
--https Enable HTTPS (default, accepted for compatibility)
|
|
1336
|
+
--cert <path> Use a custom TLS certificate
|
|
1337
|
+
--key <path> Use a custom TLS private key
|
|
1300
1338
|
--foreground Run proxy in foreground (for debugging)
|
|
1301
1339
|
--tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
|
|
1302
1340
|
--wildcard Allow unregistered subdomains to fall back to parent route
|
|
@@ -1308,7 +1346,7 @@ ${colors_default.bold("Options:")}
|
|
|
1308
1346
|
${colors_default.bold("Environment variables:")}
|
|
1309
1347
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
1310
1348
|
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1311
|
-
PORTLESS_HTTPS
|
|
1349
|
+
PORTLESS_HTTPS HTTPS on by default; set to 0 to disable (same as --no-tls)
|
|
1312
1350
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1313
1351
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
1314
1352
|
PORTLESS_SYNC_HOSTS=1 Auto-sync ${HOSTS_DISPLAY} (auto-enabled for custom TLDs)
|
|
@@ -1318,16 +1356,16 @@ ${colors_default.bold("Environment variables:")}
|
|
|
1318
1356
|
${colors_default.bold("Child process environment:")}
|
|
1319
1357
|
PORT Ephemeral port the child should listen on
|
|
1320
1358
|
HOST Always 127.0.0.1
|
|
1321
|
-
PORTLESS_URL Public URL of the app (e.g.
|
|
1359
|
+
PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
|
|
1322
1360
|
|
|
1323
1361
|
${colors_default.bold("Safari / DNS:")}
|
|
1324
1362
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1325
1363
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1326
1364
|
Auto-syncs ${HOSTS_DISPLAY} for custom TLDs (e.g. --tld test). For .localhost,
|
|
1327
1365
|
set PORTLESS_SYNC_HOSTS=1 to enable. To manually sync:
|
|
1328
|
-
${colors_default.cyan(
|
|
1366
|
+
${colors_default.cyan("portless hosts sync")}
|
|
1329
1367
|
Clean up later with:
|
|
1330
|
-
${colors_default.cyan(
|
|
1368
|
+
${colors_default.cyan("portless hosts clean")}
|
|
1331
1369
|
|
|
1332
1370
|
${colors_default.bold("Skip portless:")}
|
|
1333
1371
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
@@ -1340,7 +1378,7 @@ ${colors_default.bold("Reserved names:")}
|
|
|
1340
1378
|
process.exit(0);
|
|
1341
1379
|
}
|
|
1342
1380
|
function printVersion() {
|
|
1343
|
-
console.log("0.
|
|
1381
|
+
console.log("0.9.0");
|
|
1344
1382
|
process.exit(0);
|
|
1345
1383
|
}
|
|
1346
1384
|
async function handleTrust() {
|
|
@@ -1349,14 +1387,20 @@ async function handleTrust() {
|
|
|
1349
1387
|
if (result.trusted) {
|
|
1350
1388
|
console.log(colors_default.green("Local CA added to system trust store."));
|
|
1351
1389
|
console.log(colors_default.gray("Browsers will now trust portless HTTPS certificates."));
|
|
1352
|
-
|
|
1353
|
-
console.error(colors_default.red(`Failed to trust CA: ${result.error}`));
|
|
1354
|
-
if (result.error?.includes("sudo")) {
|
|
1355
|
-
console.error(colors_default.blue("Run with sudo:"));
|
|
1356
|
-
console.error(colors_default.cyan(" sudo portless trust"));
|
|
1357
|
-
}
|
|
1358
|
-
process.exit(1);
|
|
1390
|
+
return;
|
|
1359
1391
|
}
|
|
1392
|
+
const isPermissionError = result.error?.includes("Permission denied") || result.error?.includes("EACCES");
|
|
1393
|
+
if (isPermissionError && !isWindows && process.getuid?.() !== 0) {
|
|
1394
|
+
console.log(colors_default.yellow("Trusting the CA requires elevated privileges. Requesting sudo..."));
|
|
1395
|
+
const sudoResult = spawnSync("sudo", [process.execPath, getEntryScript(), "trust"], {
|
|
1396
|
+
stdio: "inherit",
|
|
1397
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1398
|
+
});
|
|
1399
|
+
if (sudoResult.status === 0) return;
|
|
1400
|
+
console.error(colors_default.red("sudo elevation also failed."));
|
|
1401
|
+
}
|
|
1402
|
+
console.error(colors_default.red(`Failed to trust CA: ${result.error}`));
|
|
1403
|
+
process.exit(1);
|
|
1360
1404
|
}
|
|
1361
1405
|
async function handleList() {
|
|
1362
1406
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
@@ -1384,9 +1428,9 @@ ${colors_default.bold("Options:")}
|
|
|
1384
1428
|
--help, -h Show this help
|
|
1385
1429
|
|
|
1386
1430
|
${colors_default.bold("Examples:")}
|
|
1387
|
-
portless get backend # ->
|
|
1388
|
-
portless get backend # in worktree ->
|
|
1389
|
-
portless get backend --no-worktree # ->
|
|
1431
|
+
portless get backend # -> https://backend.localhost
|
|
1432
|
+
portless get backend # in worktree -> https://auth.backend.localhost
|
|
1433
|
+
portless get backend --no-worktree # -> https://backend.localhost (skip worktree)
|
|
1390
1434
|
`);
|
|
1391
1435
|
process.exit(0);
|
|
1392
1436
|
}
|
|
@@ -1430,8 +1474,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1430
1474
|
${colors_default.cyan("portless alias <name> <port> --force")} Override existing route
|
|
1431
1475
|
|
|
1432
1476
|
${colors_default.bold("Examples:")}
|
|
1433
|
-
portless alias my-postgres 5432 # ->
|
|
1434
|
-
portless alias redis 6379 # ->
|
|
1477
|
+
portless alias my-postgres 5432 # -> https://my-postgres.localhost
|
|
1478
|
+
portless alias redis 6379 # -> https://redis.localhost
|
|
1435
1479
|
portless alias --remove my-postgres # Remove the alias
|
|
1436
1480
|
`);
|
|
1437
1481
|
process.exit(0);
|
|
@@ -1488,8 +1532,8 @@ Safari relies on the system DNS resolver, which may not handle .localhost
|
|
|
1488
1532
|
subdomains. This command adds entries to ${HOSTS_DISPLAY} as a workaround.
|
|
1489
1533
|
|
|
1490
1534
|
${colors_default.bold("Usage:")}
|
|
1491
|
-
${colors_default.cyan(
|
|
1492
|
-
${colors_default.cyan(
|
|
1535
|
+
${colors_default.cyan("portless hosts sync")} Add current routes to ${HOSTS_DISPLAY}
|
|
1536
|
+
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1493
1537
|
|
|
1494
1538
|
${colors_default.bold("Auto-sync:")}
|
|
1495
1539
|
Auto-enabled for custom TLDs (e.g. --tld test). For .localhost, set
|
|
@@ -1500,33 +1544,44 @@ ${colors_default.bold("Auto-sync:")}
|
|
|
1500
1544
|
if (args[1] === "clean") {
|
|
1501
1545
|
if (cleanHostsFile()) {
|
|
1502
1546
|
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (!isWindows && process.getuid?.() !== 0) {
|
|
1550
|
+
console.log(
|
|
1551
|
+
colors_default.yellow(
|
|
1552
|
+
`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`
|
|
1507
1553
|
)
|
|
1508
1554
|
);
|
|
1509
|
-
|
|
1510
|
-
|
|
1555
|
+
const result = spawnSync(
|
|
1556
|
+
"sudo",
|
|
1557
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "clean"],
|
|
1558
|
+
{
|
|
1559
|
+
stdio: "inherit",
|
|
1560
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1563
|
+
if (result.status === 0) return;
|
|
1511
1564
|
}
|
|
1565
|
+
console.error(
|
|
1566
|
+
colors_default.red(`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`)
|
|
1567
|
+
);
|
|
1568
|
+
process.exit(1);
|
|
1512
1569
|
return;
|
|
1513
1570
|
}
|
|
1514
1571
|
if (!args[1]) {
|
|
1515
1572
|
console.log(`
|
|
1516
1573
|
${colors_default.bold("Usage: portless hosts <command>")}
|
|
1517
1574
|
|
|
1518
|
-
${colors_default.cyan(
|
|
1519
|
-
${colors_default.cyan(
|
|
1575
|
+
${colors_default.cyan("portless hosts sync")} Add current routes to ${HOSTS_DISPLAY}
|
|
1576
|
+
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1520
1577
|
`);
|
|
1521
1578
|
process.exit(0);
|
|
1522
1579
|
}
|
|
1523
1580
|
if (args[1] !== "sync") {
|
|
1524
1581
|
console.error(colors_default.red(`Error: Unknown hosts subcommand "${args[1]}".`));
|
|
1525
1582
|
console.error(colors_default.blue("Usage:"));
|
|
1526
|
-
console.error(
|
|
1527
|
-
|
|
1528
|
-
);
|
|
1529
|
-
console.error(colors_default.cyan(` ${SUDO_PREFIX}portless hosts clean # Remove portless entries`));
|
|
1583
|
+
console.error(colors_default.cyan(` portless hosts sync # Add routes to ${HOSTS_DISPLAY}`));
|
|
1584
|
+
console.error(colors_default.cyan(" portless hosts clean # Remove portless entries"));
|
|
1530
1585
|
process.exit(1);
|
|
1531
1586
|
}
|
|
1532
1587
|
const { dir } = await discoverState();
|
|
@@ -1544,23 +1599,53 @@ ${colors_default.bold("Usage: portless hosts <command>")}
|
|
|
1544
1599
|
for (const h of hostnames) {
|
|
1545
1600
|
console.log(colors_default.cyan(` 127.0.0.1 ${h}`));
|
|
1546
1601
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
)
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (!isWindows && process.getuid?.() !== 0) {
|
|
1605
|
+
console.log(
|
|
1606
|
+
colors_default.yellow(`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
1552
1607
|
);
|
|
1553
|
-
|
|
1554
|
-
|
|
1608
|
+
const result = spawnSync(
|
|
1609
|
+
"sudo",
|
|
1610
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "sync"],
|
|
1611
|
+
{
|
|
1612
|
+
stdio: "inherit",
|
|
1613
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
if (result.status === 0) return;
|
|
1555
1617
|
}
|
|
1618
|
+
console.error(
|
|
1619
|
+
colors_default.red(`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`)
|
|
1620
|
+
);
|
|
1621
|
+
process.exit(1);
|
|
1556
1622
|
}
|
|
1557
1623
|
async function handleProxy(args) {
|
|
1558
1624
|
if (args[1] === "stop") {
|
|
1559
|
-
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1625
|
+
let explicitPort;
|
|
1626
|
+
const portIdx = args.indexOf("--port") !== -1 ? args.indexOf("--port") : args.indexOf("-p");
|
|
1627
|
+
if (portIdx !== -1) {
|
|
1628
|
+
const portValue = args[portIdx + 1];
|
|
1629
|
+
if (portValue && !portValue.startsWith("-")) {
|
|
1630
|
+
const parsed = parseInt(portValue, 10);
|
|
1631
|
+
if (!isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
|
|
1632
|
+
explicitPort = parsed;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (explicitPort !== void 0) {
|
|
1637
|
+
const dir = resolveStateDir(explicitPort);
|
|
1638
|
+
const store2 = new RouteStore(dir, {
|
|
1639
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1640
|
+
});
|
|
1641
|
+
await stopProxy(store2, explicitPort, false);
|
|
1642
|
+
} else {
|
|
1643
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1644
|
+
const store2 = new RouteStore(dir, {
|
|
1645
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1646
|
+
});
|
|
1647
|
+
await stopProxy(store2, port, tls2);
|
|
1648
|
+
}
|
|
1564
1649
|
return;
|
|
1565
1650
|
}
|
|
1566
1651
|
const isProxyHelp = args[1] === "--help" || args[1] === "-h";
|
|
@@ -1569,10 +1654,10 @@ async function handleProxy(args) {
|
|
|
1569
1654
|
${colors_default.bold("portless proxy")} - Manage the portless proxy server.
|
|
1570
1655
|
|
|
1571
1656
|
${colors_default.bold("Usage:")}
|
|
1572
|
-
${colors_default.cyan("portless proxy start")} Start the proxy (daemon)
|
|
1573
|
-
${colors_default.cyan("portless proxy start --
|
|
1657
|
+
${colors_default.cyan("portless proxy start")} Start the HTTPS proxy on port 443 (daemon)
|
|
1658
|
+
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
1574
1659
|
${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1575
|
-
${colors_default.cyan("portless proxy start -p
|
|
1660
|
+
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1576
1661
|
${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
1577
1662
|
${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
|
|
1578
1663
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
@@ -1580,27 +1665,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1580
1665
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
1581
1666
|
}
|
|
1582
1667
|
const isForeground = args.includes("--foreground");
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
1586
|
-
if (portFlagIndex !== -1) {
|
|
1587
|
-
const portValue = args[portFlagIndex + 1];
|
|
1588
|
-
if (!portValue || portValue.startsWith("-")) {
|
|
1589
|
-
console.error(colors_default.red("Error: --port / -p requires a port number."));
|
|
1590
|
-
console.error(colors_default.blue("Usage:"));
|
|
1591
|
-
console.error(colors_default.cyan(" portless proxy start -p 8080"));
|
|
1592
|
-
process.exit(1);
|
|
1593
|
-
}
|
|
1594
|
-
proxyPort = parseInt(portValue, 10);
|
|
1595
|
-
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
1596
|
-
console.error(colors_default.red(`Error: Invalid port number: ${portValue}`));
|
|
1597
|
-
console.error(colors_default.blue("Port must be between 1 and 65535."));
|
|
1598
|
-
process.exit(1);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
const hasNoTls = args.includes("--no-tls");
|
|
1602
|
-
const hasHttpsFlag = args.includes("--https");
|
|
1603
|
-
const wantHttps = !hasNoTls && (hasHttpsFlag || isHttpsEnvEnabled());
|
|
1668
|
+
const hasNoTls = args.includes("--no-tls") || isHttpsEnvDisabled();
|
|
1669
|
+
const wantHttps = !hasNoTls;
|
|
1604
1670
|
let customCertPath = null;
|
|
1605
1671
|
let customKeyPath = null;
|
|
1606
1672
|
const certIdx = args.indexOf("--cert");
|
|
@@ -1623,6 +1689,27 @@ ${colors_default.bold("Usage:")}
|
|
|
1623
1689
|
console.error(colors_default.red("Error: --cert and --key must be used together."));
|
|
1624
1690
|
process.exit(1);
|
|
1625
1691
|
}
|
|
1692
|
+
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1693
|
+
let hasExplicitPort = false;
|
|
1694
|
+
let proxyPort = getDefaultPort(useHttps);
|
|
1695
|
+
let portFlagIndex = args.indexOf("--port");
|
|
1696
|
+
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
1697
|
+
if (portFlagIndex !== -1) {
|
|
1698
|
+
const portValue = args[portFlagIndex + 1];
|
|
1699
|
+
if (!portValue || portValue.startsWith("-")) {
|
|
1700
|
+
console.error(colors_default.red("Error: --port / -p requires a port number."));
|
|
1701
|
+
console.error(colors_default.blue("Usage:"));
|
|
1702
|
+
console.error(colors_default.cyan(" portless proxy start -p 8080"));
|
|
1703
|
+
process.exit(1);
|
|
1704
|
+
}
|
|
1705
|
+
proxyPort = parseInt(portValue, 10);
|
|
1706
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
1707
|
+
console.error(colors_default.red(`Error: Invalid port number: ${portValue}`));
|
|
1708
|
+
console.error(colors_default.blue("Port must be between 1 and 65535."));
|
|
1709
|
+
process.exit(1);
|
|
1710
|
+
}
|
|
1711
|
+
hasExplicitPort = true;
|
|
1712
|
+
}
|
|
1626
1713
|
let tld;
|
|
1627
1714
|
try {
|
|
1628
1715
|
tld = getDefaultTld();
|
|
@@ -1646,7 +1733,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1646
1733
|
}
|
|
1647
1734
|
const riskyReason = RISKY_TLDS.get(tld);
|
|
1648
1735
|
if (riskyReason) {
|
|
1649
|
-
console.warn(colors_default.yellow(`Warning: .${tld}
|
|
1736
|
+
console.warn(colors_default.yellow(`Warning: .${tld}: ${riskyReason}`));
|
|
1650
1737
|
}
|
|
1651
1738
|
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
1652
1739
|
if (tld !== DEFAULT_TLD && syncDisabled) {
|
|
@@ -1656,36 +1743,96 @@ ${colors_default.bold("Usage:")}
|
|
|
1656
1743
|
)
|
|
1657
1744
|
);
|
|
1658
1745
|
console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
1659
|
-
console.warn(colors_default.cyan(
|
|
1746
|
+
console.warn(colors_default.cyan(" portless hosts sync"));
|
|
1660
1747
|
}
|
|
1661
1748
|
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
const store = new RouteStore(stateDir, {
|
|
1749
|
+
let stateDir = resolveStateDir(proxyPort);
|
|
1750
|
+
let store = new RouteStore(stateDir, {
|
|
1665
1751
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1666
1752
|
});
|
|
1667
1753
|
if (await isProxyRunning(proxyPort)) {
|
|
1668
1754
|
if (isForeground) {
|
|
1669
1755
|
return;
|
|
1670
1756
|
}
|
|
1671
|
-
const
|
|
1672
|
-
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
1673
|
-
const portFlag = proxyPort !== getDefaultPort() ? ` -p ${proxyPort}` : "";
|
|
1757
|
+
const portFlag = proxyPort !== getProtocolPort(useHttps) ? ` -p ${proxyPort}` : "";
|
|
1674
1758
|
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1675
1759
|
console.log(
|
|
1676
|
-
colors_default.blue(
|
|
1677
|
-
`To restart: ${sudoPrefix}portless proxy stop${portFlag} && ${sudoPrefix}portless proxy start${portFlag}`
|
|
1678
|
-
)
|
|
1760
|
+
colors_default.blue(`To restart: portless proxy stop${portFlag} && portless proxy start${portFlag}`)
|
|
1679
1761
|
);
|
|
1680
1762
|
return;
|
|
1681
1763
|
}
|
|
1682
1764
|
if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1765
|
+
const baseArgs = [
|
|
1766
|
+
process.execPath,
|
|
1767
|
+
getEntryScript(),
|
|
1768
|
+
"proxy",
|
|
1769
|
+
"start",
|
|
1770
|
+
"-p",
|
|
1771
|
+
String(proxyPort)
|
|
1772
|
+
];
|
|
1773
|
+
const optionalFlags = [];
|
|
1774
|
+
if (hasNoTls) optionalFlags.push("--no-tls");
|
|
1775
|
+
if (tld !== DEFAULT_TLD) optionalFlags.push("--tld", tld);
|
|
1776
|
+
if (useWildcard) optionalFlags.push("--wildcard");
|
|
1777
|
+
if (isForeground) optionalFlags.push("--foreground");
|
|
1778
|
+
if (customCertPath && customKeyPath)
|
|
1779
|
+
optionalFlags.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1780
|
+
const startArgs = [...baseArgs, ...optionalFlags];
|
|
1781
|
+
const extraFlags = optionalFlags.map((a) => ` ${a}`).join("");
|
|
1782
|
+
console.log(
|
|
1783
|
+
colors_default.yellow(`Port ${proxyPort} requires elevated privileges. Requesting sudo...`)
|
|
1784
|
+
);
|
|
1785
|
+
if (!hasExplicitPort) {
|
|
1786
|
+
console.log(
|
|
1787
|
+
colors_default.gray(
|
|
1788
|
+
`(To skip sudo, use an unprivileged port: portless proxy start -p ${FALLBACK_PROXY_PORT}${extraFlags})`
|
|
1789
|
+
)
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
const result = spawnSync("sudo", ["env", ...collectPortlessEnvArgs(), ...startArgs], {
|
|
1793
|
+
stdio: "inherit",
|
|
1794
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1795
|
+
});
|
|
1796
|
+
if (result.status === 0) {
|
|
1797
|
+
if (!isForeground) {
|
|
1798
|
+
if (await waitForProxy(proxyPort)) {
|
|
1799
|
+
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
1800
|
+
} else {
|
|
1801
|
+
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
1802
|
+
const logPath2 = path3.join(resolveStateDir(proxyPort), "proxy.log");
|
|
1803
|
+
if (fs3.existsSync(logPath2)) {
|
|
1804
|
+
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (result.signal) {
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
if (!hasExplicitPort) {
|
|
1814
|
+
proxyPort = FALLBACK_PROXY_PORT;
|
|
1815
|
+
console.log(colors_default.yellow(`Falling back to port ${proxyPort}.`));
|
|
1816
|
+
console.log(
|
|
1817
|
+
colors_default.blue(`For clean URLs without port numbers, re-run and accept the sudo prompt:`)
|
|
1818
|
+
);
|
|
1819
|
+
console.log(colors_default.cyan(` portless proxy start${extraFlags}`));
|
|
1820
|
+
if (await isProxyRunning(proxyPort)) {
|
|
1821
|
+
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
stateDir = resolveStateDir(proxyPort);
|
|
1825
|
+
store = new RouteStore(stateDir, {
|
|
1826
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1827
|
+
});
|
|
1828
|
+
} else {
|
|
1829
|
+
console.error(
|
|
1830
|
+
colors_default.red(`Error: Port ${proxyPort} requires elevated privileges and sudo failed.`)
|
|
1831
|
+
);
|
|
1832
|
+
console.error(colors_default.blue("Try again (portless will prompt for sudo):"));
|
|
1833
|
+
console.error(colors_default.cyan(` portless proxy start -p ${proxyPort}${extraFlags}`));
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1689
1836
|
}
|
|
1690
1837
|
let tlsOptions;
|
|
1691
1838
|
if (useHttps) {
|
|
@@ -1703,7 +1850,9 @@ ${colors_default.bold("Usage:")}
|
|
|
1703
1850
|
}
|
|
1704
1851
|
if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
|
|
1705
1852
|
console.error(colors_default.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
|
|
1706
|
-
console.error(
|
|
1853
|
+
console.error(
|
|
1854
|
+
colors_default.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----")
|
|
1855
|
+
);
|
|
1707
1856
|
process.exit(1);
|
|
1708
1857
|
}
|
|
1709
1858
|
tlsOptions = { cert, key };
|
|
@@ -1759,16 +1908,22 @@ ${colors_default.bold("Usage:")}
|
|
|
1759
1908
|
} catch {
|
|
1760
1909
|
}
|
|
1761
1910
|
fixOwnership(logPath);
|
|
1762
|
-
const daemonArgs = [
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1911
|
+
const daemonArgs = [
|
|
1912
|
+
getEntryScript(),
|
|
1913
|
+
"proxy",
|
|
1914
|
+
"start",
|
|
1915
|
+
"--foreground",
|
|
1916
|
+
"--port",
|
|
1917
|
+
proxyPort.toString()
|
|
1918
|
+
];
|
|
1766
1919
|
if (useHttps) {
|
|
1767
1920
|
if (customCertPath && customKeyPath) {
|
|
1768
1921
|
daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1769
1922
|
} else {
|
|
1770
1923
|
daemonArgs.push("--https");
|
|
1771
1924
|
}
|
|
1925
|
+
} else {
|
|
1926
|
+
daemonArgs.push("--no-tls");
|
|
1772
1927
|
}
|
|
1773
1928
|
if (tld !== DEFAULT_TLD) {
|
|
1774
1929
|
daemonArgs.push("--tld", tld);
|
|
@@ -1789,8 +1944,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1789
1944
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1790
1945
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1791
1946
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
1792
|
-
|
|
1793
|
-
console.error(colors_default.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
|
|
1947
|
+
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
1794
1948
|
if (fs3.existsSync(logPath)) {
|
|
1795
1949
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1796
1950
|
}
|