portless 0.8.0 → 0.9.1
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 +51 -51
- package/dist/{chunk-KKXL2CMI.js → chunk-5BR7NCNI.js} +66 -30
- package/dist/cli.js +381 -204
- 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() {
|
|
@@ -488,7 +491,10 @@ function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost")
|
|
|
488
491
|
function trustCA(stateDir) {
|
|
489
492
|
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
490
493
|
if (!fileExists(caCertPath)) {
|
|
491
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
trusted: false,
|
|
496
|
+
error: "CA certificate not found. Run portless trust to generate it."
|
|
497
|
+
};
|
|
492
498
|
}
|
|
493
499
|
try {
|
|
494
500
|
if (process.platform === "darwin") {
|
|
@@ -538,7 +544,7 @@ function trustCA(stateDir) {
|
|
|
538
544
|
if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES")) {
|
|
539
545
|
return {
|
|
540
546
|
trusted: false,
|
|
541
|
-
error: "Permission denied. Try:
|
|
547
|
+
error: "Permission denied. Try: portless trust"
|
|
542
548
|
};
|
|
543
549
|
}
|
|
544
550
|
return { trusted: false, error: message };
|
|
@@ -721,11 +727,47 @@ function readBranchFromHead(gitdir) {
|
|
|
721
727
|
|
|
722
728
|
// src/cli.ts
|
|
723
729
|
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
724
|
-
var SUDO_PREFIX = isWindows ? "" : "sudo ";
|
|
725
730
|
var DEBOUNCE_MS = 100;
|
|
726
731
|
var POLL_INTERVAL_MS = 3e3;
|
|
727
732
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
728
733
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
734
|
+
function getEntryScript() {
|
|
735
|
+
const script = process.argv[1];
|
|
736
|
+
if (!script) {
|
|
737
|
+
throw new Error("Cannot determine portless entry script (process.argv[1] is undefined)");
|
|
738
|
+
}
|
|
739
|
+
return script;
|
|
740
|
+
}
|
|
741
|
+
function isLocallyInstalled() {
|
|
742
|
+
let dir = process.cwd();
|
|
743
|
+
for (; ; ) {
|
|
744
|
+
if (fs3.existsSync(path3.join(dir, "node_modules", "portless", "package.json"))) {
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
const parent = path3.dirname(dir);
|
|
748
|
+
if (parent === dir) break;
|
|
749
|
+
dir = parent;
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
function collectPortlessEnvArgs() {
|
|
754
|
+
const envArgs = [];
|
|
755
|
+
for (const key of Object.keys(process.env)) {
|
|
756
|
+
if (key.startsWith("PORTLESS_") && process.env[key]) {
|
|
757
|
+
envArgs.push(`${key}=${process.env[key]}`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return envArgs;
|
|
761
|
+
}
|
|
762
|
+
function sudoStop(port) {
|
|
763
|
+
const stopArgs = [process.execPath, getEntryScript(), "proxy", "stop", "-p", String(port)];
|
|
764
|
+
console.log(colors_default.yellow("Proxy is running as root. Elevating with sudo to stop it..."));
|
|
765
|
+
const result = spawnSync("sudo", ["env", ...collectPortlessEnvArgs(), ...stopArgs], {
|
|
766
|
+
stdio: "inherit",
|
|
767
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
768
|
+
});
|
|
769
|
+
return result.status === 0;
|
|
770
|
+
}
|
|
729
771
|
function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
730
772
|
store.ensureDir();
|
|
731
773
|
const isTls = !!tlsOptions;
|
|
@@ -786,15 +828,22 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
786
828
|
);
|
|
787
829
|
} else if (err.code === "EACCES") {
|
|
788
830
|
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"));
|
|
831
|
+
console.error(colors_default.blue("Use an unprivileged port (no sudo needed):"));
|
|
832
|
+
console.error(colors_default.cyan(" portless proxy start -p 1355"));
|
|
793
833
|
} else {
|
|
794
834
|
console.error(colors_default.red(`Proxy error: ${err.message}`));
|
|
795
835
|
}
|
|
836
|
+
if (redirectServer) redirectServer.close();
|
|
796
837
|
process.exit(1);
|
|
797
838
|
});
|
|
839
|
+
let redirectServer = null;
|
|
840
|
+
if (isTls && proxyPort !== 80) {
|
|
841
|
+
redirectServer = createHttpRedirectServer(proxyPort);
|
|
842
|
+
redirectServer.on("error", () => {
|
|
843
|
+
redirectServer = null;
|
|
844
|
+
});
|
|
845
|
+
redirectServer.listen(80);
|
|
846
|
+
}
|
|
798
847
|
server.listen(proxyPort, () => {
|
|
799
848
|
fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
800
849
|
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
@@ -807,6 +856,9 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
807
856
|
console.log(
|
|
808
857
|
colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
|
|
809
858
|
);
|
|
859
|
+
if (redirectServer) {
|
|
860
|
+
console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
|
|
861
|
+
}
|
|
810
862
|
});
|
|
811
863
|
let exiting = false;
|
|
812
864
|
const cleanup = () => {
|
|
@@ -817,6 +869,9 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
817
869
|
if (watcher) {
|
|
818
870
|
watcher.close();
|
|
819
871
|
}
|
|
872
|
+
if (redirectServer) {
|
|
873
|
+
redirectServer.close();
|
|
874
|
+
}
|
|
820
875
|
try {
|
|
821
876
|
fs3.unlinkSync(store.pidPath);
|
|
822
877
|
} catch {
|
|
@@ -836,10 +891,21 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
|
836
891
|
console.log(colors_default.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
837
892
|
console.log(colors_default.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
838
893
|
}
|
|
894
|
+
function sudoStopOrHint(port) {
|
|
895
|
+
if (!isWindows) {
|
|
896
|
+
if (!sudoStop(port)) {
|
|
897
|
+
console.error(colors_default.red("Failed to stop proxy with sudo."));
|
|
898
|
+
console.error(colors_default.blue("Try manually:"));
|
|
899
|
+
console.error(colors_default.cyan(` portless proxy stop -p ${port}`));
|
|
900
|
+
}
|
|
901
|
+
} else {
|
|
902
|
+
console.error(colors_default.red("Permission denied. The proxy was started with elevated privileges."));
|
|
903
|
+
console.error(colors_default.blue("Stop it with:"));
|
|
904
|
+
console.error(colors_default.cyan(" Run portless proxy stop as Administrator"));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
839
907
|
async function stopProxy(store, proxyPort, _tls) {
|
|
840
908
|
const pidPath = store.pidPath;
|
|
841
|
-
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
842
|
-
const sudoHint = needsSudo ? "sudo " : "";
|
|
843
909
|
if (!fs3.existsSync(pidPath)) {
|
|
844
910
|
if (await isProxyRunning(proxyPort)) {
|
|
845
911
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
@@ -854,15 +920,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
854
920
|
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
855
921
|
} catch (err) {
|
|
856
922
|
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
|
-
);
|
|
923
|
+
sudoStopOrHint(proxyPort);
|
|
866
924
|
} else {
|
|
867
925
|
const message = err instanceof Error ? err.message : String(err);
|
|
868
926
|
console.error(colors_default.red(`Failed to stop proxy: ${message}`));
|
|
@@ -875,9 +933,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
875
933
|
}
|
|
876
934
|
}
|
|
877
935
|
} 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"));
|
|
936
|
+
sudoStopOrHint(proxyPort);
|
|
881
937
|
} else {
|
|
882
938
|
console.error(colors_default.red(`Could not identify the process on port ${proxyPort}.`));
|
|
883
939
|
console.error(colors_default.blue("Try manually:"));
|
|
@@ -901,7 +957,11 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
901
957
|
}
|
|
902
958
|
try {
|
|
903
959
|
process.kill(pid, 0);
|
|
904
|
-
} catch {
|
|
960
|
+
} catch (err) {
|
|
961
|
+
if (isErrnoException(err) && err.code === "EPERM") {
|
|
962
|
+
sudoStopOrHint(proxyPort);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
905
965
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
906
966
|
fs3.unlinkSync(pidPath);
|
|
907
967
|
try {
|
|
@@ -929,11 +989,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
929
989
|
console.log(colors_default.green("Proxy stopped."));
|
|
930
990
|
} catch (err) {
|
|
931
991
|
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`));
|
|
992
|
+
sudoStopOrHint(proxyPort);
|
|
937
993
|
} else {
|
|
938
994
|
const message = err instanceof Error ? err.message : String(err);
|
|
939
995
|
console.error(colors_default.red(`Failed to stop proxy: ${message}`));
|
|
@@ -963,8 +1019,8 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
963
1019
|
}
|
|
964
1020
|
console.log();
|
|
965
1021
|
}
|
|
966
|
-
async function runApp(
|
|
967
|
-
|
|
1022
|
+
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
|
|
1023
|
+
let store = initialStore;
|
|
968
1024
|
let envTld;
|
|
969
1025
|
try {
|
|
970
1026
|
envTld = getDefaultTld();
|
|
@@ -982,7 +1038,7 @@ async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, tld,
|
|
|
982
1038
|
console.log(colors_default.blue.bold(`
|
|
983
1039
|
portless
|
|
984
1040
|
`));
|
|
985
|
-
console.log(colors_default.gray(`-- ${
|
|
1041
|
+
console.log(colors_default.gray(`-- ${parseHostname(name, tld)} (auto-resolves to 127.0.0.1)`));
|
|
986
1042
|
if (autoInfo) {
|
|
987
1043
|
const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
|
|
988
1044
|
console.log(colors_default.gray(`-- Name "${baseName}" (from ${autoInfo.nameSource})`));
|
|
@@ -991,18 +1047,22 @@ portless
|
|
|
991
1047
|
}
|
|
992
1048
|
}
|
|
993
1049
|
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
994
|
-
const
|
|
1050
|
+
const wantTls = !isHttpsEnvDisabled();
|
|
1051
|
+
const defaultPort = getDefaultPort(wantTls);
|
|
995
1052
|
const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
}
|
|
1053
|
+
if (needsSudo && !process.stdin.isTTY) {
|
|
1054
|
+
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
1055
|
+
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
1056
|
+
console.error(colors_default.cyan(" portless proxy start"));
|
|
1057
|
+
console.error(
|
|
1058
|
+
colors_default.blue(
|
|
1059
|
+
`Option 2: use an unprivileged port (no sudo needed, URLs will include :${FALLBACK_PROXY_PORT}):`
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
1062
|
+
console.error(colors_default.cyan(` portless proxy start -p ${FALLBACK_PROXY_PORT}`));
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
if (needsSudo && process.stdin.isTTY) {
|
|
1006
1066
|
const answer = await prompt(colors_default.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
1007
1067
|
if (answer === "n" || answer === "no") {
|
|
1008
1068
|
console.log(colors_default.gray("Cancelled."));
|
|
@@ -1013,57 +1073,50 @@ portless
|
|
|
1013
1073
|
spawnCommand(commandArgs);
|
|
1014
1074
|
return;
|
|
1015
1075
|
}
|
|
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);
|
|
1076
|
+
}
|
|
1077
|
+
console.log(colors_default.yellow("Starting proxy..."));
|
|
1078
|
+
const startArgs = [getEntryScript(), "proxy", "start"];
|
|
1079
|
+
if (!wantTls) startArgs.push("--no-tls");
|
|
1080
|
+
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
1081
|
+
const result = spawnSync(process.execPath, startArgs, {
|
|
1082
|
+
stdio: "inherit",
|
|
1083
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1084
|
+
});
|
|
1085
|
+
let discovered = null;
|
|
1086
|
+
if (result.status === 0) {
|
|
1087
|
+
for (let i = 0; i < WAIT_FOR_PROXY_MAX_ATTEMPTS; i++) {
|
|
1088
|
+
await new Promise((r) => setTimeout(r, WAIT_FOR_PROXY_INTERVAL_MS));
|
|
1089
|
+
const state = await discoverState();
|
|
1090
|
+
if (await isProxyRunning(state.port)) {
|
|
1091
|
+
discovered = state;
|
|
1092
|
+
break;
|
|
1047
1093
|
}
|
|
1048
1094
|
}
|
|
1049
1095
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
console.error(colors_default.
|
|
1056
|
-
console.error(colors_default.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
|
|
1096
|
+
if (!discovered) {
|
|
1097
|
+
console.error(colors_default.red("Failed to start proxy."));
|
|
1098
|
+
const fallbackDir = resolveStateDir(getDefaultPort(wantTls));
|
|
1099
|
+
const logPath = path3.join(fallbackDir, "proxy.log");
|
|
1100
|
+
console.error(colors_default.blue("Try starting it manually:"));
|
|
1101
|
+
console.error(colors_default.cyan(" portless proxy start"));
|
|
1057
1102
|
if (fs3.existsSync(logPath)) {
|
|
1058
1103
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1059
1104
|
}
|
|
1060
1105
|
process.exit(1);
|
|
1106
|
+
return;
|
|
1061
1107
|
}
|
|
1062
|
-
|
|
1108
|
+
proxyPort = discovered.port;
|
|
1109
|
+
stateDir = discovered.dir;
|
|
1110
|
+
tld = discovered.tld;
|
|
1111
|
+
tls2 = discovered.tls;
|
|
1112
|
+
store = new RouteStore(stateDir, {
|
|
1113
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1114
|
+
});
|
|
1063
1115
|
console.log(colors_default.green("Proxy started in background"));
|
|
1064
1116
|
} else {
|
|
1065
1117
|
console.log(colors_default.gray("-- Proxy is running"));
|
|
1066
1118
|
}
|
|
1119
|
+
const hostname = parseHostname(name, tld);
|
|
1067
1120
|
const port = desiredPort ?? await findFreePort();
|
|
1068
1121
|
if (desiredPort) {
|
|
1069
1122
|
console.log(colors_default.green(`-- Using port ${port} (fixed)`));
|
|
@@ -1160,9 +1213,9 @@ ${colors_default.bold("Name inference (in order):")}
|
|
|
1160
1213
|
(e.g. feature-auth.myapp.localhost).
|
|
1161
1214
|
|
|
1162
1215
|
${colors_default.bold("Examples:")}
|
|
1163
|
-
portless run next dev # ->
|
|
1164
|
-
portless run --name myapp next dev # ->
|
|
1165
|
-
portless run vite dev # ->
|
|
1216
|
+
portless run next dev # -> https://<project>.localhost
|
|
1217
|
+
portless run --name myapp next dev # -> https://myapp.localhost
|
|
1218
|
+
portless run vite dev # -> https://<project>.localhost
|
|
1166
1219
|
portless run --app-port 3000 pnpm start
|
|
1167
1220
|
`);
|
|
1168
1221
|
process.exit(0);
|
|
@@ -1238,13 +1291,13 @@ Eliminates port conflicts, memorizing port numbers, and cookie/storage
|
|
|
1238
1291
|
clashes by giving each dev server a stable .localhost URL.
|
|
1239
1292
|
|
|
1240
1293
|
${colors_default.bold("Install:")}
|
|
1241
|
-
${colors_default.cyan("npm install -g portless")}
|
|
1242
|
-
|
|
1294
|
+
${colors_default.cyan("npm install -g portless")} Global (recommended)
|
|
1295
|
+
${colors_default.cyan("npm install -D portless")} Project dev dependency
|
|
1243
1296
|
|
|
1244
1297
|
${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
|
|
1298
|
+
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
1299
|
+
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
1300
|
+
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1248
1301
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
1249
1302
|
${colors_default.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
1250
1303
|
${colors_default.cyan("portless run <cmd>")} Infer name from project, run through proxy
|
|
@@ -1257,14 +1310,14 @@ ${colors_default.bold("Usage:")}
|
|
|
1257
1310
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1258
1311
|
|
|
1259
1312
|
${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 # ->
|
|
1313
|
+
portless proxy start # Start HTTPS proxy on port 443
|
|
1314
|
+
portless proxy start --no-tls # Start HTTP proxy on port 80
|
|
1315
|
+
portless myapp next dev # -> https://myapp.localhost
|
|
1316
|
+
portless myapp vite dev # -> https://myapp.localhost
|
|
1317
|
+
portless api.myapp pnpm start # -> https://api.myapp.localhost
|
|
1318
|
+
portless run next dev # -> https://<project>.localhost
|
|
1319
|
+
portless run next dev # in worktree -> https://<worktree>.<project>.localhost
|
|
1320
|
+
portless get backend # -> https://backend.localhost (for cross-service refs)
|
|
1268
1321
|
# Wildcard subdomains: tenant.myapp.localhost also routes to myapp
|
|
1269
1322
|
|
|
1270
1323
|
${colors_default.bold("In package.json:")}
|
|
@@ -1275,28 +1328,28 @@ ${colors_default.bold("In package.json:")}
|
|
|
1275
1328
|
}
|
|
1276
1329
|
|
|
1277
1330
|
${colors_default.bold("How it works:")}
|
|
1278
|
-
1. Start the proxy once (
|
|
1331
|
+
1. Start the proxy once (HTTPS on port 443 by default, auto-elevates with sudo)
|
|
1279
1332
|
2. Run your apps - they auto-start the proxy and register automatically
|
|
1280
1333
|
(apps get a random port in the 4000-4999 range via PORT)
|
|
1281
|
-
3. Access via
|
|
1334
|
+
3. Access via https://<name>.localhost
|
|
1282
1335
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
1283
1336
|
5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular,
|
|
1284
1337
|
Expo, React Native) get --port and --host flags injected automatically
|
|
1285
1338
|
|
|
1286
|
-
${colors_default.bold("HTTP/2 + HTTPS:")}
|
|
1287
|
-
|
|
1339
|
+
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
1340
|
+
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
1288
1341
|
On first use, portless generates a local CA and adds it to your
|
|
1289
|
-
system trust store. No browser warnings.
|
|
1342
|
+
system trust store. No browser warnings. Disable with --no-tls.
|
|
1290
1343
|
|
|
1291
1344
|
${colors_default.bold("Options:")}
|
|
1292
1345
|
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
1293
1346
|
Adds worktree prefix in git worktrees
|
|
1294
|
-
-p, --port <number> Port for the proxy
|
|
1295
|
-
|
|
1296
|
-
--
|
|
1297
|
-
--
|
|
1298
|
-
--
|
|
1299
|
-
--
|
|
1347
|
+
-p, --port <number> Port for the proxy (default: 443, or 80 with --no-tls)
|
|
1348
|
+
Standard ports auto-elevate with sudo on macOS/Linux
|
|
1349
|
+
--no-tls Disable HTTPS (use plain HTTP on port 80)
|
|
1350
|
+
--https Enable HTTPS (default, accepted for compatibility)
|
|
1351
|
+
--cert <path> Use a custom TLS certificate
|
|
1352
|
+
--key <path> Use a custom TLS private key
|
|
1300
1353
|
--foreground Run proxy in foreground (for debugging)
|
|
1301
1354
|
--tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
|
|
1302
1355
|
--wildcard Allow unregistered subdomains to fall back to parent route
|
|
@@ -1308,7 +1361,7 @@ ${colors_default.bold("Options:")}
|
|
|
1308
1361
|
${colors_default.bold("Environment variables:")}
|
|
1309
1362
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
1310
1363
|
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1311
|
-
PORTLESS_HTTPS
|
|
1364
|
+
PORTLESS_HTTPS HTTPS on by default; set to 0 to disable (same as --no-tls)
|
|
1312
1365
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1313
1366
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
1314
1367
|
PORTLESS_SYNC_HOSTS=1 Auto-sync ${HOSTS_DISPLAY} (auto-enabled for custom TLDs)
|
|
@@ -1318,16 +1371,16 @@ ${colors_default.bold("Environment variables:")}
|
|
|
1318
1371
|
${colors_default.bold("Child process environment:")}
|
|
1319
1372
|
PORT Ephemeral port the child should listen on
|
|
1320
1373
|
HOST Always 127.0.0.1
|
|
1321
|
-
PORTLESS_URL Public URL of the app (e.g.
|
|
1374
|
+
PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
|
|
1322
1375
|
|
|
1323
1376
|
${colors_default.bold("Safari / DNS:")}
|
|
1324
1377
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1325
1378
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1326
1379
|
Auto-syncs ${HOSTS_DISPLAY} for custom TLDs (e.g. --tld test). For .localhost,
|
|
1327
1380
|
set PORTLESS_SYNC_HOSTS=1 to enable. To manually sync:
|
|
1328
|
-
${colors_default.cyan(
|
|
1381
|
+
${colors_default.cyan("portless hosts sync")}
|
|
1329
1382
|
Clean up later with:
|
|
1330
|
-
${colors_default.cyan(
|
|
1383
|
+
${colors_default.cyan("portless hosts clean")}
|
|
1331
1384
|
|
|
1332
1385
|
${colors_default.bold("Skip portless:")}
|
|
1333
1386
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
@@ -1340,23 +1393,36 @@ ${colors_default.bold("Reserved names:")}
|
|
|
1340
1393
|
process.exit(0);
|
|
1341
1394
|
}
|
|
1342
1395
|
function printVersion() {
|
|
1343
|
-
console.log("0.
|
|
1396
|
+
console.log("0.9.1");
|
|
1344
1397
|
process.exit(0);
|
|
1345
1398
|
}
|
|
1346
1399
|
async function handleTrust() {
|
|
1347
1400
|
const { dir } = await discoverState();
|
|
1401
|
+
if (!fs3.existsSync(dir)) {
|
|
1402
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1403
|
+
}
|
|
1404
|
+
const { caGenerated } = ensureCerts(dir);
|
|
1405
|
+
if (caGenerated) {
|
|
1406
|
+
console.log(colors_default.gray("Generated local CA certificate."));
|
|
1407
|
+
}
|
|
1348
1408
|
const result = trustCA(dir);
|
|
1349
1409
|
if (result.trusted) {
|
|
1350
1410
|
console.log(colors_default.green("Local CA added to system trust store."));
|
|
1351
1411
|
console.log(colors_default.gray("Browsers will now trust portless HTTPS certificates."));
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
const isPermissionError = result.error?.includes("Permission denied") || result.error?.includes("EACCES");
|
|
1415
|
+
if (isPermissionError && !isWindows && process.getuid?.() !== 0) {
|
|
1416
|
+
console.log(colors_default.yellow("Trusting the CA requires elevated privileges. Requesting sudo..."));
|
|
1417
|
+
const sudoResult = spawnSync("sudo", [process.execPath, getEntryScript(), "trust"], {
|
|
1418
|
+
stdio: "inherit",
|
|
1419
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1420
|
+
});
|
|
1421
|
+
if (sudoResult.status === 0) return;
|
|
1422
|
+
console.error(colors_default.red("sudo elevation also failed."));
|
|
1359
1423
|
}
|
|
1424
|
+
console.error(colors_default.red(`Failed to trust CA: ${result.error}`));
|
|
1425
|
+
process.exit(1);
|
|
1360
1426
|
}
|
|
1361
1427
|
async function handleList() {
|
|
1362
1428
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
@@ -1384,9 +1450,9 @@ ${colors_default.bold("Options:")}
|
|
|
1384
1450
|
--help, -h Show this help
|
|
1385
1451
|
|
|
1386
1452
|
${colors_default.bold("Examples:")}
|
|
1387
|
-
portless get backend # ->
|
|
1388
|
-
portless get backend # in worktree ->
|
|
1389
|
-
portless get backend --no-worktree # ->
|
|
1453
|
+
portless get backend # -> https://backend.localhost
|
|
1454
|
+
portless get backend # in worktree -> https://auth.backend.localhost
|
|
1455
|
+
portless get backend --no-worktree # -> https://backend.localhost (skip worktree)
|
|
1390
1456
|
`);
|
|
1391
1457
|
process.exit(0);
|
|
1392
1458
|
}
|
|
@@ -1430,8 +1496,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1430
1496
|
${colors_default.cyan("portless alias <name> <port> --force")} Override existing route
|
|
1431
1497
|
|
|
1432
1498
|
${colors_default.bold("Examples:")}
|
|
1433
|
-
portless alias my-postgres 5432 # ->
|
|
1434
|
-
portless alias redis 6379 # ->
|
|
1499
|
+
portless alias my-postgres 5432 # -> https://my-postgres.localhost
|
|
1500
|
+
portless alias redis 6379 # -> https://redis.localhost
|
|
1435
1501
|
portless alias --remove my-postgres # Remove the alias
|
|
1436
1502
|
`);
|
|
1437
1503
|
process.exit(0);
|
|
@@ -1488,8 +1554,8 @@ Safari relies on the system DNS resolver, which may not handle .localhost
|
|
|
1488
1554
|
subdomains. This command adds entries to ${HOSTS_DISPLAY} as a workaround.
|
|
1489
1555
|
|
|
1490
1556
|
${colors_default.bold("Usage:")}
|
|
1491
|
-
${colors_default.cyan(
|
|
1492
|
-
${colors_default.cyan(
|
|
1557
|
+
${colors_default.cyan("portless hosts sync")} Add current routes to ${HOSTS_DISPLAY}
|
|
1558
|
+
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1493
1559
|
|
|
1494
1560
|
${colors_default.bold("Auto-sync:")}
|
|
1495
1561
|
Auto-enabled for custom TLDs (e.g. --tld test). For .localhost, set
|
|
@@ -1500,33 +1566,44 @@ ${colors_default.bold("Auto-sync:")}
|
|
|
1500
1566
|
if (args[1] === "clean") {
|
|
1501
1567
|
if (cleanHostsFile()) {
|
|
1502
1568
|
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (!isWindows && process.getuid?.() !== 0) {
|
|
1572
|
+
console.log(
|
|
1573
|
+
colors_default.yellow(
|
|
1574
|
+
`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`
|
|
1507
1575
|
)
|
|
1508
1576
|
);
|
|
1509
|
-
|
|
1510
|
-
|
|
1577
|
+
const result = spawnSync(
|
|
1578
|
+
"sudo",
|
|
1579
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "clean"],
|
|
1580
|
+
{
|
|
1581
|
+
stdio: "inherit",
|
|
1582
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1583
|
+
}
|
|
1584
|
+
);
|
|
1585
|
+
if (result.status === 0) return;
|
|
1511
1586
|
}
|
|
1587
|
+
console.error(
|
|
1588
|
+
colors_default.red(`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`)
|
|
1589
|
+
);
|
|
1590
|
+
process.exit(1);
|
|
1512
1591
|
return;
|
|
1513
1592
|
}
|
|
1514
1593
|
if (!args[1]) {
|
|
1515
1594
|
console.log(`
|
|
1516
1595
|
${colors_default.bold("Usage: portless hosts <command>")}
|
|
1517
1596
|
|
|
1518
|
-
${colors_default.cyan(
|
|
1519
|
-
${colors_default.cyan(
|
|
1597
|
+
${colors_default.cyan("portless hosts sync")} Add current routes to ${HOSTS_DISPLAY}
|
|
1598
|
+
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1520
1599
|
`);
|
|
1521
1600
|
process.exit(0);
|
|
1522
1601
|
}
|
|
1523
1602
|
if (args[1] !== "sync") {
|
|
1524
1603
|
console.error(colors_default.red(`Error: Unknown hosts subcommand "${args[1]}".`));
|
|
1525
1604
|
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`));
|
|
1605
|
+
console.error(colors_default.cyan(` portless hosts sync # Add routes to ${HOSTS_DISPLAY}`));
|
|
1606
|
+
console.error(colors_default.cyan(" portless hosts clean # Remove portless entries"));
|
|
1530
1607
|
process.exit(1);
|
|
1531
1608
|
}
|
|
1532
1609
|
const { dir } = await discoverState();
|
|
@@ -1544,23 +1621,53 @@ ${colors_default.bold("Usage: portless hosts <command>")}
|
|
|
1544
1621
|
for (const h of hostnames) {
|
|
1545
1622
|
console.log(colors_default.cyan(` 127.0.0.1 ${h}`));
|
|
1546
1623
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
)
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
if (!isWindows && process.getuid?.() !== 0) {
|
|
1627
|
+
console.log(
|
|
1628
|
+
colors_default.yellow(`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
1552
1629
|
);
|
|
1553
|
-
|
|
1554
|
-
|
|
1630
|
+
const result = spawnSync(
|
|
1631
|
+
"sudo",
|
|
1632
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "sync"],
|
|
1633
|
+
{
|
|
1634
|
+
stdio: "inherit",
|
|
1635
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1636
|
+
}
|
|
1637
|
+
);
|
|
1638
|
+
if (result.status === 0) return;
|
|
1555
1639
|
}
|
|
1640
|
+
console.error(
|
|
1641
|
+
colors_default.red(`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`)
|
|
1642
|
+
);
|
|
1643
|
+
process.exit(1);
|
|
1556
1644
|
}
|
|
1557
1645
|
async function handleProxy(args) {
|
|
1558
1646
|
if (args[1] === "stop") {
|
|
1559
|
-
|
|
1560
|
-
const
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1647
|
+
let explicitPort;
|
|
1648
|
+
const portIdx = args.indexOf("--port") !== -1 ? args.indexOf("--port") : args.indexOf("-p");
|
|
1649
|
+
if (portIdx !== -1) {
|
|
1650
|
+
const portValue = args[portIdx + 1];
|
|
1651
|
+
if (portValue && !portValue.startsWith("-")) {
|
|
1652
|
+
const parsed = parseInt(portValue, 10);
|
|
1653
|
+
if (!isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
|
|
1654
|
+
explicitPort = parsed;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (explicitPort !== void 0) {
|
|
1659
|
+
const dir = resolveStateDir(explicitPort);
|
|
1660
|
+
const store2 = new RouteStore(dir, {
|
|
1661
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1662
|
+
});
|
|
1663
|
+
await stopProxy(store2, explicitPort, false);
|
|
1664
|
+
} else {
|
|
1665
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1666
|
+
const store2 = new RouteStore(dir, {
|
|
1667
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1668
|
+
});
|
|
1669
|
+
await stopProxy(store2, port, tls2);
|
|
1670
|
+
}
|
|
1564
1671
|
return;
|
|
1565
1672
|
}
|
|
1566
1673
|
const isProxyHelp = args[1] === "--help" || args[1] === "-h";
|
|
@@ -1569,10 +1676,10 @@ async function handleProxy(args) {
|
|
|
1569
1676
|
${colors_default.bold("portless proxy")} - Manage the portless proxy server.
|
|
1570
1677
|
|
|
1571
1678
|
${colors_default.bold("Usage:")}
|
|
1572
|
-
${colors_default.cyan("portless proxy start")} Start the proxy (daemon)
|
|
1573
|
-
${colors_default.cyan("portless proxy start --
|
|
1679
|
+
${colors_default.cyan("portless proxy start")} Start the HTTPS proxy on port 443 (daemon)
|
|
1680
|
+
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
1574
1681
|
${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1575
|
-
${colors_default.cyan("portless proxy start -p
|
|
1682
|
+
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1576
1683
|
${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
1577
1684
|
${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
|
|
1578
1685
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
@@ -1580,27 +1687,8 @@ ${colors_default.bold("Usage:")}
|
|
|
1580
1687
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
1581
1688
|
}
|
|
1582
1689
|
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());
|
|
1690
|
+
const hasNoTls = args.includes("--no-tls") || isHttpsEnvDisabled();
|
|
1691
|
+
const wantHttps = !hasNoTls;
|
|
1604
1692
|
let customCertPath = null;
|
|
1605
1693
|
let customKeyPath = null;
|
|
1606
1694
|
const certIdx = args.indexOf("--cert");
|
|
@@ -1623,6 +1711,27 @@ ${colors_default.bold("Usage:")}
|
|
|
1623
1711
|
console.error(colors_default.red("Error: --cert and --key must be used together."));
|
|
1624
1712
|
process.exit(1);
|
|
1625
1713
|
}
|
|
1714
|
+
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1715
|
+
let hasExplicitPort = false;
|
|
1716
|
+
let proxyPort = getDefaultPort(useHttps);
|
|
1717
|
+
let portFlagIndex = args.indexOf("--port");
|
|
1718
|
+
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
1719
|
+
if (portFlagIndex !== -1) {
|
|
1720
|
+
const portValue = args[portFlagIndex + 1];
|
|
1721
|
+
if (!portValue || portValue.startsWith("-")) {
|
|
1722
|
+
console.error(colors_default.red("Error: --port / -p requires a port number."));
|
|
1723
|
+
console.error(colors_default.blue("Usage:"));
|
|
1724
|
+
console.error(colors_default.cyan(" portless proxy start -p 8080"));
|
|
1725
|
+
process.exit(1);
|
|
1726
|
+
}
|
|
1727
|
+
proxyPort = parseInt(portValue, 10);
|
|
1728
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
1729
|
+
console.error(colors_default.red(`Error: Invalid port number: ${portValue}`));
|
|
1730
|
+
console.error(colors_default.blue("Port must be between 1 and 65535."));
|
|
1731
|
+
process.exit(1);
|
|
1732
|
+
}
|
|
1733
|
+
hasExplicitPort = true;
|
|
1734
|
+
}
|
|
1626
1735
|
let tld;
|
|
1627
1736
|
try {
|
|
1628
1737
|
tld = getDefaultTld();
|
|
@@ -1646,7 +1755,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1646
1755
|
}
|
|
1647
1756
|
const riskyReason = RISKY_TLDS.get(tld);
|
|
1648
1757
|
if (riskyReason) {
|
|
1649
|
-
console.warn(colors_default.yellow(`Warning: .${tld}
|
|
1758
|
+
console.warn(colors_default.yellow(`Warning: .${tld}: ${riskyReason}`));
|
|
1650
1759
|
}
|
|
1651
1760
|
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
1652
1761
|
if (tld !== DEFAULT_TLD && syncDisabled) {
|
|
@@ -1656,36 +1765,96 @@ ${colors_default.bold("Usage:")}
|
|
|
1656
1765
|
)
|
|
1657
1766
|
);
|
|
1658
1767
|
console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
1659
|
-
console.warn(colors_default.cyan(
|
|
1768
|
+
console.warn(colors_default.cyan(" portless hosts sync"));
|
|
1660
1769
|
}
|
|
1661
1770
|
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
const store = new RouteStore(stateDir, {
|
|
1771
|
+
let stateDir = resolveStateDir(proxyPort);
|
|
1772
|
+
let store = new RouteStore(stateDir, {
|
|
1665
1773
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1666
1774
|
});
|
|
1667
1775
|
if (await isProxyRunning(proxyPort)) {
|
|
1668
1776
|
if (isForeground) {
|
|
1669
1777
|
return;
|
|
1670
1778
|
}
|
|
1671
|
-
const
|
|
1672
|
-
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
1673
|
-
const portFlag = proxyPort !== getDefaultPort() ? ` -p ${proxyPort}` : "";
|
|
1779
|
+
const portFlag = proxyPort !== getProtocolPort(useHttps) ? ` -p ${proxyPort}` : "";
|
|
1674
1780
|
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1675
1781
|
console.log(
|
|
1676
|
-
colors_default.blue(
|
|
1677
|
-
`To restart: ${sudoPrefix}portless proxy stop${portFlag} && ${sudoPrefix}portless proxy start${portFlag}`
|
|
1678
|
-
)
|
|
1782
|
+
colors_default.blue(`To restart: portless proxy stop${portFlag} && portless proxy start${portFlag}`)
|
|
1679
1783
|
);
|
|
1680
1784
|
return;
|
|
1681
1785
|
}
|
|
1682
1786
|
if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1787
|
+
const baseArgs = [
|
|
1788
|
+
process.execPath,
|
|
1789
|
+
getEntryScript(),
|
|
1790
|
+
"proxy",
|
|
1791
|
+
"start",
|
|
1792
|
+
"-p",
|
|
1793
|
+
String(proxyPort)
|
|
1794
|
+
];
|
|
1795
|
+
const optionalFlags = [];
|
|
1796
|
+
if (hasNoTls) optionalFlags.push("--no-tls");
|
|
1797
|
+
if (tld !== DEFAULT_TLD) optionalFlags.push("--tld", tld);
|
|
1798
|
+
if (useWildcard) optionalFlags.push("--wildcard");
|
|
1799
|
+
if (isForeground) optionalFlags.push("--foreground");
|
|
1800
|
+
if (customCertPath && customKeyPath)
|
|
1801
|
+
optionalFlags.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1802
|
+
const startArgs = [...baseArgs, ...optionalFlags];
|
|
1803
|
+
const extraFlags = optionalFlags.map((a) => ` ${a}`).join("");
|
|
1804
|
+
console.log(
|
|
1805
|
+
colors_default.yellow(`Port ${proxyPort} requires elevated privileges. Requesting sudo...`)
|
|
1806
|
+
);
|
|
1807
|
+
if (!hasExplicitPort) {
|
|
1808
|
+
console.log(
|
|
1809
|
+
colors_default.gray(
|
|
1810
|
+
`(To skip sudo, use an unprivileged port: portless proxy start -p ${FALLBACK_PROXY_PORT}${extraFlags})`
|
|
1811
|
+
)
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
const result = spawnSync("sudo", ["env", ...collectPortlessEnvArgs(), ...startArgs], {
|
|
1815
|
+
stdio: "inherit",
|
|
1816
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1817
|
+
});
|
|
1818
|
+
if (result.status === 0) {
|
|
1819
|
+
if (!isForeground) {
|
|
1820
|
+
if (await waitForProxy(proxyPort)) {
|
|
1821
|
+
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
1822
|
+
} else {
|
|
1823
|
+
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
1824
|
+
const logPath2 = path3.join(resolveStateDir(proxyPort), "proxy.log");
|
|
1825
|
+
if (fs3.existsSync(logPath2)) {
|
|
1826
|
+
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (result.signal) {
|
|
1833
|
+
process.exit(1);
|
|
1834
|
+
}
|
|
1835
|
+
if (!hasExplicitPort) {
|
|
1836
|
+
proxyPort = FALLBACK_PROXY_PORT;
|
|
1837
|
+
console.log(colors_default.yellow(`Falling back to port ${proxyPort}.`));
|
|
1838
|
+
console.log(
|
|
1839
|
+
colors_default.blue(`For clean URLs without port numbers, re-run and accept the sudo prompt:`)
|
|
1840
|
+
);
|
|
1841
|
+
console.log(colors_default.cyan(` portless proxy start${extraFlags}`));
|
|
1842
|
+
if (await isProxyRunning(proxyPort)) {
|
|
1843
|
+
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
stateDir = resolveStateDir(proxyPort);
|
|
1847
|
+
store = new RouteStore(stateDir, {
|
|
1848
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1849
|
+
});
|
|
1850
|
+
} else {
|
|
1851
|
+
console.error(
|
|
1852
|
+
colors_default.red(`Error: Port ${proxyPort} requires elevated privileges and sudo failed.`)
|
|
1853
|
+
);
|
|
1854
|
+
console.error(colors_default.blue("Try again (portless will prompt for sudo):"));
|
|
1855
|
+
console.error(colors_default.cyan(` portless proxy start -p ${proxyPort}${extraFlags}`));
|
|
1856
|
+
process.exit(1);
|
|
1857
|
+
}
|
|
1689
1858
|
}
|
|
1690
1859
|
let tlsOptions;
|
|
1691
1860
|
if (useHttps) {
|
|
@@ -1703,7 +1872,9 @@ ${colors_default.bold("Usage:")}
|
|
|
1703
1872
|
}
|
|
1704
1873
|
if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
|
|
1705
1874
|
console.error(colors_default.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
|
|
1706
|
-
console.error(
|
|
1875
|
+
console.error(
|
|
1876
|
+
colors_default.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----")
|
|
1877
|
+
);
|
|
1707
1878
|
process.exit(1);
|
|
1708
1879
|
}
|
|
1709
1880
|
tlsOptions = { cert, key };
|
|
@@ -1759,16 +1930,22 @@ ${colors_default.bold("Usage:")}
|
|
|
1759
1930
|
} catch {
|
|
1760
1931
|
}
|
|
1761
1932
|
fixOwnership(logPath);
|
|
1762
|
-
const daemonArgs = [
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1933
|
+
const daemonArgs = [
|
|
1934
|
+
getEntryScript(),
|
|
1935
|
+
"proxy",
|
|
1936
|
+
"start",
|
|
1937
|
+
"--foreground",
|
|
1938
|
+
"--port",
|
|
1939
|
+
proxyPort.toString()
|
|
1940
|
+
];
|
|
1766
1941
|
if (useHttps) {
|
|
1767
1942
|
if (customCertPath && customKeyPath) {
|
|
1768
1943
|
daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1769
1944
|
} else {
|
|
1770
1945
|
daemonArgs.push("--https");
|
|
1771
1946
|
}
|
|
1947
|
+
} else {
|
|
1948
|
+
daemonArgs.push("--no-tls");
|
|
1772
1949
|
}
|
|
1773
1950
|
if (tld !== DEFAULT_TLD) {
|
|
1774
1951
|
daemonArgs.push("--tld", tld);
|
|
@@ -1789,8 +1966,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1789
1966
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1790
1967
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1791
1968
|
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`));
|
|
1969
|
+
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
1794
1970
|
if (fs3.existsSync(logPath)) {
|
|
1795
1971
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1796
1972
|
}
|
|
@@ -1878,10 +2054,11 @@ async function main() {
|
|
|
1878
2054
|
const args = process.argv.slice(2);
|
|
1879
2055
|
const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
|
|
1880
2056
|
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
1881
|
-
if (isNpx || isPnpmDlx) {
|
|
2057
|
+
if ((isNpx || isPnpmDlx) && !isLocallyInstalled()) {
|
|
1882
2058
|
console.error(colors_default.red("Error: portless should not be run via npx or pnpm dlx."));
|
|
1883
|
-
console.error(colors_default.blue("Install globally
|
|
2059
|
+
console.error(colors_default.blue("Install globally or as a project dependency:"));
|
|
1884
2060
|
console.error(colors_default.cyan(" npm install -g portless"));
|
|
2061
|
+
console.error(colors_default.cyan(" npm install -D portless"));
|
|
1885
2062
|
process.exit(1);
|
|
1886
2063
|
}
|
|
1887
2064
|
if (args[0] === "--name") {
|