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/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
- isHttpsEnvEnabled,
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-KKXL2CMI.js";
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 { trusted: false, error: "CA certificate not found. Run with --https first." };
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: sudo portless trust"
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("Either run with sudo:"));
790
- console.error(colors_default.cyan(" sudo portless proxy start -p 80"));
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
- console.error(
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
- console.error(colors_default.red("Cannot identify the process. It may be running as root."));
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
- console.error(
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(store, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
967
- const hostname = parseHostname(name, tld);
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(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
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 defaultPort = getDefaultPort();
1050
+ const wantTls = !isHttpsEnvDisabled();
1051
+ const defaultPort = getDefaultPort(wantTls);
995
1052
  const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
996
- const wantHttps = isHttpsEnvEnabled();
997
- if (needsSudo) {
998
- if (!process.stdin.isTTY) {
999
- console.error(colors_default.red("Proxy is not running."));
1000
- console.error(colors_default.blue("Start the proxy first (requires sudo for this port):"));
1001
- console.error(colors_default.cyan(" sudo portless proxy start -p 80"));
1002
- console.error(colors_default.blue("Or use the default port (no sudo needed):"));
1003
- console.error(colors_default.cyan(" portless proxy start"));
1004
- process.exit(1);
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
- console.log(colors_default.yellow("Starting proxy (requires sudo)..."));
1017
- const startArgs = [process.execPath, process.argv[1], "proxy", "start"];
1018
- if (wantHttps) startArgs.push("--https");
1019
- if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
1020
- const result = spawnSync("sudo", startArgs, {
1021
- stdio: "inherit",
1022
- timeout: SUDO_SPAWN_TIMEOUT_MS
1023
- });
1024
- if (result.status !== 0) {
1025
- if (!await isProxyRunning(proxyPort)) {
1026
- console.error(colors_default.red("Failed to start proxy."));
1027
- console.error(colors_default.blue("Try starting it manually:"));
1028
- console.error(colors_default.cyan(" sudo portless proxy start"));
1029
- process.exit(1);
1030
- }
1031
- }
1032
- } else {
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
- const autoTls = readTlsMarker(stateDir);
1051
- tld = readTldFromDir(stateDir);
1052
- if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
1053
- console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
1054
- const logPath = path3.join(stateDir, "proxy.log");
1055
- console.error(colors_default.blue("Try starting the proxy manually to see the error:"));
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
- tls2 = autoTls;
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 # -> http://<project>.localhost:1355
1164
- portless run --name myapp next dev # -> http://myapp.localhost:1355
1165
- portless run vite dev # -> http://<project>.localhost:1355
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
- Do NOT add portless as a project dependency.
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 (background daemon)
1246
- ${colors_default.cyan("portless proxy start --https")} Start with HTTP/2 + TLS (auto-generates certs)
1247
- ${colors_default.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
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 1355
1261
- portless proxy start --https # Start with HTTPS/2 (faster page loads)
1262
- portless myapp next dev # -> http://myapp.localhost:1355
1263
- portless myapp vite dev # -> http://myapp.localhost:1355
1264
- portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
1265
- portless run next dev # -> http://<project>.localhost:1355
1266
- portless run next dev # in worktree -> http://<worktree>.<project>.localhost:1355
1267
- portless get backend # -> http://backend.localhost:1355 (for cross-service refs)
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 (listens on port 1355 by default, no sudo needed)
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 http://<name>.localhost:1355
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
- Use --https for HTTP/2 multiplexing (faster dev server page loads).
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. No sudo required on macOS.
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 to listen on (default: 1355)
1295
- Ports < 1024 require sudo
1296
- --https Enable HTTP/2 + TLS with auto-generated certs
1297
- --cert <path> Use a custom TLS certificate (implies --https)
1298
- --key <path> Use a custom TLS private key (implies --https)
1299
- --no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
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=1 Always enable HTTPS (set in .bashrc / .zshrc)
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. http://myapp.localhost:1355)
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(`${SUDO_PREFIX}portless hosts sync`)}
1381
+ ${colors_default.cyan("portless hosts sync")}
1329
1382
  Clean up later with:
1330
- ${colors_default.cyan(`${SUDO_PREFIX}portless hosts clean`)}
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.8.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
- } else {
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);
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 # -> http://backend.localhost:1355
1388
- portless get backend # in worktree -> http://auth.backend.localhost:1355
1389
- portless get backend --no-worktree # -> http://backend.localhost:1355 (skip 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 # -> http://my-postgres.localhost:1355
1434
- portless alias redis 6379 # -> http://redis.localhost:1355
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(`${SUDO_PREFIX}portless hosts sync`)} Add current routes to ${HOSTS_DISPLAY}
1492
- ${colors_default.cyan(`${SUDO_PREFIX}portless hosts clean`)} Remove portless entries from ${HOSTS_DISPLAY}
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
- } else {
1504
- console.error(
1505
- colors_default.red(
1506
- `Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : " (requires sudo)."}`
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
- console.error(colors_default.cyan(` ${SUDO_PREFIX}portless hosts clean`));
1510
- process.exit(1);
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(`${SUDO_PREFIX}portless hosts sync`)} Add current routes to ${HOSTS_DISPLAY}
1519
- ${colors_default.cyan(`${SUDO_PREFIX}portless hosts clean`)} Remove portless entries from ${HOSTS_DISPLAY}
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
- colors_default.cyan(` ${SUDO_PREFIX}portless hosts sync # Add routes to ${HOSTS_DISPLAY}`)
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
- } else {
1548
- console.error(
1549
- colors_default.red(
1550
- `Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : " (requires sudo)."}`
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
- console.error(colors_default.cyan(` ${SUDO_PREFIX}portless hosts sync`));
1554
- process.exit(1);
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
- const { dir, port, tls: tls2 } = await discoverState();
1560
- const store2 = new RouteStore(dir, {
1561
- onWarning: (msg) => console.warn(colors_default.yellow(msg))
1562
- });
1563
- await stopProxy(store2, port, tls2);
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 --https")} Start with HTTP/2 + TLS
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 80")} Start on port 80 (requires sudo)
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
- let proxyPort = getDefaultPort();
1584
- let portFlagIndex = args.indexOf("--port");
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} -- ${riskyReason}`));
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(` ${SUDO_PREFIX}portless hosts sync`));
1768
+ console.warn(colors_default.cyan(" portless hosts sync"));
1660
1769
  }
1661
1770
  const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
1662
- const useHttps = wantHttps || !!(customCertPath && customKeyPath);
1663
- const stateDir = resolveStateDir(proxyPort);
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 needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
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
- console.error(colors_default.red(`Error: Port ${proxyPort} requires sudo.`));
1684
- console.error(colors_default.blue("Either run with sudo:"));
1685
- console.error(colors_default.cyan(" sudo portless proxy start -p 80"));
1686
- console.error(colors_default.blue("Or use the default port (no sudo needed):"));
1687
- console.error(colors_default.cyan(" portless proxy start"));
1688
- process.exit(1);
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(colors_default.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----"));
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 = [process.argv[1], "proxy", "start", "--foreground"];
1763
- if (portFlagIndex !== -1) {
1764
- daemonArgs.push("--port", proxyPort.toString());
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
- const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
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 instead:"));
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") {