portless 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,17 +18,12 @@ npm install -g portless
18
18
  ## Run your app
19
19
 
20
20
  ```bash
21
- # Enable HTTPS (one-time setup, auto-generates certs)
22
- portless proxy start --https
23
-
24
21
  portless myapp next dev
25
22
  # -> https://myapp.localhost
26
-
27
- # Without --https, runs on port 1355
28
- portless myapp next dev
29
- # -> http://myapp.localhost:1355
30
23
  ```
31
24
 
25
+ HTTPS with HTTP/2 is enabled by default. On first run, portless generates a local CA, trusts it, and binds port 443 (auto-elevates with sudo on macOS/Linux). Use `--no-tls` for plain HTTP.
26
+
32
27
  The proxy auto-starts when you run an app. A random port (4000--4999) is assigned via the `PORT` environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore `PORT` (Vite, Astro, React Router, Angular, Expo, React Native), portless auto-injects `--port` and `--host` flags.
33
28
 
34
29
  ## Use in package.json
@@ -47,45 +42,45 @@ Organize services with subdomains:
47
42
 
48
43
  ```bash
49
44
  portless api.myapp pnpm start
50
- # -> http://api.myapp.localhost:1355
45
+ # -> https://api.myapp.localhost
51
46
 
52
47
  portless docs.myapp next dev
53
- # -> http://docs.myapp.localhost:1355
48
+ # -> https://docs.myapp.localhost
54
49
  ```
55
50
 
56
- Wildcard subdomain routing: any subdomain of a registered route routes to that app automatically (e.g. `tenant1.myapp.localhost:1355` routes to the `myapp` app without extra registration).
51
+ By default, only explicitly registered subdomains are routed (strict mode). Use `--wildcard` when starting the proxy to allow any subdomain of a registered route to fall back to that app (e.g. `tenant1.myapp.localhost` routes to the `myapp` app without extra registration).
57
52
 
58
53
  ## Git Worktrees
59
54
 
60
55
  `portless run` automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain so each worktree gets its own URL without any config changes:
61
56
 
62
57
  ```bash
63
- # Main worktree -- no prefix
64
- portless run next dev # -> http://myapp.localhost:1355
58
+ # Main worktree (no prefix)
59
+ portless run next dev # -> https://myapp.localhost
65
60
 
66
61
  # Linked worktree on branch "fix-ui"
67
- portless run next dev # -> http://fix-ui.myapp.localhost:1355
62
+ portless run next dev # -> https://fix-ui.myapp.localhost
68
63
  ```
69
64
 
70
65
  Use `--name` to override the inferred base name while keeping the worktree prefix:
71
66
 
72
67
  ```bash
73
- portless run --name myapp next dev # -> http://fix-ui.myapp.localhost:1355
68
+ portless run --name myapp next dev # -> https://fix-ui.myapp.localhost
74
69
  ```
75
70
 
76
- Put `portless run` in your `package.json` once and it works everywhere -- the main checkout uses the plain name, each worktree gets a unique subdomain. No collisions, no `--force`.
71
+ Put `portless run` in your `package.json` once and it works everywhere. The main checkout uses the plain name, each worktree gets a unique subdomain. No collisions, no `--force`.
77
72
 
78
73
  ## Custom TLD
79
74
 
80
75
  By default, portless uses `.localhost` which auto-resolves to `127.0.0.1` in most browsers. If you prefer a different TLD (e.g. `.test`), use `--tld`:
81
76
 
82
77
  ```bash
83
- sudo portless proxy start --https --tld test
78
+ portless proxy start --tld test
84
79
  portless myapp next dev
85
80
  # -> https://myapp.test
86
81
  ```
87
82
 
88
- The proxy auto-syncs `/etc/hosts` for custom TLDs when started with sudo, so `.test` domains resolve correctly.
83
+ The proxy auto-syncs `/etc/hosts` for custom TLDs, so `.test` domains resolve correctly.
89
84
 
90
85
  Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflicts with mDNS/Bonjour) and `.dev` (Google-owned, forces HTTPS via HSTS).
91
86
 
@@ -93,40 +88,35 @@ Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflic
93
88
 
94
89
  ```mermaid
95
90
  flowchart TD
96
- Browser["Browser<br>myapp.localhost:1355"]
97
- Proxy["portless proxy<br>(port 1355)"]
91
+ Browser["Browser<br>myapp.localhost"]
92
+ Proxy["portless proxy<br>(port 80 or 443)"]
98
93
  App1[":4123<br>myapp"]
99
94
  App2[":4567<br>api"]
100
95
 
101
- Browser -->|port 1355| Proxy
96
+ Browser --> Proxy
102
97
  Proxy --> App1
103
98
  Proxy --> App2
104
99
  ```
105
100
 
106
- 1. **Start the proxy** -- auto-starts when you run an app, or start explicitly with `portless proxy start`
107
- 2. **Run apps** -- `portless <name> <command>` assigns a free port and registers with the proxy
108
- 3. **Access via URL** -- `http://<name>.localhost:1355` routes through the proxy to your app
101
+ 1. **Start the proxy**: auto-starts when you run an app, or start explicitly with `portless proxy start`
102
+ 2. **Run apps**: `portless <name> <command>` assigns a free port and registers with the proxy
103
+ 3. **Access via URL**: `https://<name>.localhost` routes through the proxy to your app
109
104
 
110
105
  ## HTTP/2 + HTTPS
111
106
 
112
- Enable HTTP/2 for faster dev server page loads. Browsers limit HTTP/1.1 to 6 connections per host, which bottlenecks dev servers that serve many unbundled files (Vite, Nuxt, etc.). HTTP/2 multiplexes all requests over a single connection.
113
-
114
- ```bash
115
- # Start with HTTPS/2 -- generates certs and trusts them automatically
116
- portless proxy start --https
117
-
118
- # First run prompts for sudo once to add the CA to your system trust store.
119
- # After that, no prompts. No browser warnings.
107
+ HTTPS with HTTP/2 is enabled by default. Browsers limit HTTP/1.1 to 6 connections per host, which bottlenecks dev servers that serve many unbundled files (Vite, Nuxt, etc.). HTTP/2 multiplexes all requests over a single connection.
120
108
 
121
- # Make it permanent (add to .bashrc / .zshrc)
122
- export PORTLESS_HTTPS=1
123
- portless proxy start # HTTPS by default now
109
+ On first run, portless generates a local CA and adds it to your system trust store. No browser warnings. No manual setup.
124
110
 
111
+ ```bash
125
112
  # Use your own certs (e.g., from mkcert)
126
113
  portless proxy start --cert ./cert.pem --key ./key.pem
127
114
 
128
- # If you skipped sudo on first run, trust the CA later
129
- sudo portless trust
115
+ # Disable HTTPS (plain HTTP on port 80)
116
+ portless proxy start --no-tls
117
+
118
+ # If you skipped the trust prompt on first run, trust the CA later
119
+ portless trust
130
120
  ```
131
121
 
132
122
  On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and openSUSE (via `update-ca-certificates` or `update-ca-trust`). On Windows, it uses `certutil` to add the CA to the system trust store.
@@ -135,7 +125,7 @@ On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and
135
125
 
136
126
  ```bash
137
127
  portless run [--name <name>] <cmd> [args...] # Infer name (or override with --name), run through proxy
138
- portless <name> <cmd> [args...] # Run app at http://<name>.localhost:1355
128
+ portless <name> <cmd> [args...] # Run app at https://<name>.localhost
139
129
  portless alias <name> <port> # Register a static route (e.g. for Docker)
140
130
  portless alias <name> <port> --force # Overwrite an existing route
141
131
  portless alias --remove <name> # Remove a static route
@@ -148,23 +138,25 @@ portless hosts clean # Remove portless entries from /etc/hosts
148
138
  PORTLESS=0 pnpm dev # Bypasses proxy, uses default port
149
139
 
150
140
  # Proxy control
151
- portless proxy start # Start the proxy (port 1355, daemon)
152
- portless proxy start --https # Start with HTTP/2 + TLS
153
- portless proxy start -p 80 # Start on port 80 (requires sudo)
141
+ portless proxy start # Start the HTTPS proxy (port 443, daemon)
142
+ portless proxy start --no-tls # Start without HTTPS (port 80)
143
+ portless proxy start -p 1355 # Start on a custom port (no sudo)
154
144
  portless proxy start --foreground # Start in foreground (for debugging)
145
+ portless proxy start --wildcard # Allow unregistered subdomains to fall back to parent
155
146
  portless proxy stop # Stop the proxy
156
147
  ```
157
148
 
158
149
  ### Options
159
150
 
160
151
  ```
161
- -p, --port <number> Port for the proxy (default: 1355)
162
- --https Enable HTTP/2 + TLS with auto-generated certs
163
- --cert <path> Use a custom TLS certificate (implies --https)
164
- --key <path> Use a custom TLS private key (implies --https)
165
- --no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
152
+ -p, --port <number> Port for the proxy (default: 443, or 80 with --no-tls)
153
+ --no-tls Disable HTTPS (use plain HTTP on port 80)
154
+ --https Enable HTTPS (default, accepted for compatibility)
155
+ --cert <path> Use a custom TLS certificate
156
+ --key <path> Use a custom TLS private key
166
157
  --foreground Run proxy in foreground instead of daemon
167
158
  --tld <tld> Use a custom TLD instead of .localhost (e.g. test)
159
+ --wildcard Allow unregistered subdomains to fall back to parent route
168
160
  --app-port <number> Use a fixed port for the app (skip auto-assignment)
169
161
  --force Override a route registered by another process
170
162
  --name <name> Use <name> as the app name
@@ -176,8 +168,9 @@ portless proxy stop # Stop the proxy
176
168
  # Configuration
177
169
  PORTLESS_PORT=<number> Override the default proxy port
178
170
  PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
179
- PORTLESS_HTTPS=1 Always enable HTTPS
171
+ PORTLESS_HTTPS HTTPS on by default; set to 0 to disable (same as --no-tls)
180
172
  PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
173
+ PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
181
174
  PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (auto-enabled for custom TLDs)
182
175
  PORTLESS_STATE_DIR=<path> Override the state directory
183
176
 
@@ -196,8 +189,8 @@ PORTLESS_URL Public URL (e.g. https://myapp.localhost)
196
189
  If Safari can't find your `.localhost` URL:
197
190
 
198
191
  ```bash
199
- sudo portless hosts sync # Add current routes to /etc/hosts
200
- sudo portless hosts clean # Clean up later
192
+ portless hosts sync # Add current routes to /etc/hosts
193
+ portless hosts clean # Clean up later
201
194
  ```
202
195
 
203
196
  Auto-syncs `/etc/hosts` for custom TLDs (e.g. `--tld test`). For `.localhost`, set `PORTLESS_SYNC_HOSTS=1` to enable. Disable with `PORTLESS_SYNC_HOSTS=0`.
@@ -212,7 +205,7 @@ If your frontend dev server (e.g. Vite, webpack) proxies API requests to another
212
205
  server: {
213
206
  proxy: {
214
207
  "/api": {
215
- target: "http://api.myapp.localhost:1355",
208
+ target: "https://api.myapp.localhost",
216
209
  changeOrigin: true,
217
210
  ws: true,
218
211
  },
@@ -226,12 +219,14 @@ server: {
226
219
  devServer: {
227
220
  proxy: [{
228
221
  context: ["/api"],
229
- target: "http://api.myapp.localhost:1355",
222
+ target: "https://api.myapp.localhost",
230
223
  changeOrigin: true,
231
224
  }],
232
225
  }
233
226
  ```
234
227
 
228
+ If your tooling doesn't trust the portless CA, point Node.js at it: `NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem` (or `~/.portless/ca.pem` when the proxy runs on a non-privileged port like 1355). Alternatively, use `--no-tls` for plain HTTP.
229
+
235
230
  Portless detects this misconfiguration and responds with `508 Loop Detected` along with a message pointing to this fix.
236
231
 
237
232
  ## Development
@@ -286,6 +286,9 @@ function getRequestHost(req) {
286
286
  if (typeof authority === "string" && authority) return authority;
287
287
  return req.headers.host || "";
288
288
  }
289
+ function isEncrypted(req) {
290
+ return !!req.socket.encrypted;
291
+ }
289
292
  function buildForwardedHeaders(req, tls) {
290
293
  const headers = {};
291
294
  const remoteAddress = req.socket.remoteAddress || "127.0.0.1";
@@ -300,20 +303,21 @@ function buildForwardedHeaders(req, tls) {
300
303
  }
301
304
  var PORTLESS_HOPS_HEADER = "x-portless-hops";
302
305
  var MAX_PROXY_HOPS = 5;
303
- function findRoute(routes, host) {
304
- return routes.find((r) => r.hostname === host) || routes.find((r) => host.endsWith("." + r.hostname));
306
+ function findRoute(routes, host, strict) {
307
+ return routes.find((r) => r.hostname === host) || (strict ? void 0 : routes.find((r) => host.endsWith("." + r.hostname)));
305
308
  }
306
309
  function createProxyServer(options) {
307
310
  const {
308
311
  getRoutes,
309
312
  proxyPort,
310
313
  tld = "localhost",
314
+ strict = true,
311
315
  onError = (msg) => console.error(msg),
312
316
  tls
313
317
  } = options;
314
318
  const tldSuffix = `.${tld}`;
315
- const isTls = !!tls;
316
319
  const handleRequest = (req, res) => {
320
+ const reqTls = isEncrypted(req);
317
321
  res.setHeader(PORTLESS_HEADER, "1");
318
322
  const routes = getRoutes();
319
323
  const host = getRequestHost(req).split(":")[0];
@@ -334,7 +338,7 @@ function createProxyServer(options) {
334
338
  "Loop Detected",
335
339
  `<div class="content"><p class="desc">This request has passed through portless ${hops} times. This usually means a dev server (Vite, webpack, etc.) is proxying requests back through portless without rewriting the Host header.</p><div class="section"><p class="label">Fix: add changeOrigin to your proxy config</p><pre class="terminal">proxy: {
336
340
  "/api": {
337
- target: "http://&lt;backend&gt;${escapeHtml(tldSuffix)}:&lt;port&gt;",
341
+ target: "${reqTls ? "https" : "http"}://&lt;backend&gt;${escapeHtml(tldSuffix)}${reqTls ? "" : ":&lt;port&gt;"}",
338
342
  changeOrigin: true,
339
343
  },
340
344
  }</pre></div></div>`
@@ -342,12 +346,12 @@ function createProxyServer(options) {
342
346
  );
343
347
  return;
344
348
  }
345
- const route = findRoute(routes, host);
349
+ const route = findRoute(routes, host, strict);
346
350
  if (!route) {
347
351
  const safeHost = escapeHtml(host);
348
352
  const strippedHost = host.endsWith(tldSuffix) ? host.slice(0, -tldSuffix.length) : host;
349
353
  const safeSuggestion = escapeHtml(strippedHost);
350
- const routesList = routes.length > 0 ? `<div class="section"><p class="label">Active apps</p><ul class="card">${routes.map((r) => `<li><a href="${escapeHtml(formatUrl(r.hostname, proxyPort, isTls))}" class="card-link"><span class="name">${escapeHtml(r.hostname)}</span><span class="meta"><code class="port">127.0.0.1:${escapeHtml(String(r.port))}</code><span class="arrow">${ARROW_SVG}</span></span></a></li>`).join("")}</ul></div>` : '<p class="empty">No apps running.</p>';
354
+ const routesList = routes.length > 0 ? `<div class="section"><p class="label">Active apps</p><ul class="card">${routes.map((r) => `<li><a href="${escapeHtml(formatUrl(r.hostname, proxyPort, reqTls))}" class="card-link"><span class="name">${escapeHtml(r.hostname)}</span><span class="meta"><code class="port">127.0.0.1:${escapeHtml(String(r.port))}</code><span class="arrow">${ARROW_SVG}</span></span></a></li>`).join("")}</ul></div>` : '<p class="empty">No apps running.</p>';
351
355
  res.writeHead(404, { "Content-Type": "text/html" });
352
356
  res.end(
353
357
  renderPage(
@@ -358,7 +362,7 @@ function createProxyServer(options) {
358
362
  );
359
363
  return;
360
364
  }
361
- const forwardedHeaders = buildForwardedHeaders(req, isTls);
365
+ const forwardedHeaders = buildForwardedHeaders(req, reqTls);
362
366
  const proxyReqHeaders = { ...req.headers };
363
367
  for (const [key, value] of Object.entries(forwardedHeaders)) {
364
368
  proxyReqHeaders[key] = value;
@@ -379,7 +383,7 @@ function createProxyServer(options) {
379
383
  },
380
384
  (proxyRes) => {
381
385
  const responseHeaders = { ...proxyRes.headers };
382
- if (isTls) {
386
+ if (reqTls) {
383
387
  for (const h of HOP_BY_HOP_HEADERS) {
384
388
  delete responseHeaders[h];
385
389
  }
@@ -436,12 +440,12 @@ function createProxyServer(options) {
436
440
  }
437
441
  const routes = getRoutes();
438
442
  const host = getRequestHost(req).split(":")[0];
439
- const route = findRoute(routes, host);
443
+ const route = findRoute(routes, host, strict);
440
444
  if (!route) {
441
445
  socket.destroy();
442
446
  return;
443
447
  }
444
- const forwardedHeaders = buildForwardedHeaders(req, isTls);
448
+ const forwardedHeaders = buildForwardedHeaders(req, isEncrypted(req));
445
449
  const proxyReqHeaders = { ...req.headers };
446
450
  for (const [key, value] of Object.entries(forwardedHeaders)) {
447
451
  proxyReqHeaders[key] = value;
@@ -512,8 +516,19 @@ function createProxyServer(options) {
512
516
  h2Server.on("upgrade", (req, socket, head) => {
513
517
  handleUpgrade(req, socket, head);
514
518
  });
515
- const plainServer = http.createServer(handleRequest);
516
- plainServer.on("upgrade", handleUpgrade);
519
+ const plainServer = http.createServer((req, res) => {
520
+ const host = getRequestHost(req).split(":")[0] || "localhost";
521
+ const location = `https://${host}${proxyPort === 443 ? "" : `:${proxyPort}`}${req.url || "/"}`;
522
+ res.writeHead(302, { Location: location, [PORTLESS_HEADER]: "1" });
523
+ res.end();
524
+ });
525
+ plainServer.on("upgrade", (req, socket) => {
526
+ const host = getRequestHost(req);
527
+ console.warn(
528
+ `[portless] Dropped plain-HTTP WebSocket upgrade for ${host}; use wss:// instead`
529
+ );
530
+ socket.destroy();
531
+ });
517
532
  const wrapper = net.createServer((socket) => {
518
533
  socket.on("error", () => {
519
534
  socket.destroy();
@@ -544,6 +559,15 @@ function createProxyServer(options) {
544
559
  httpServer.on("upgrade", handleUpgrade);
545
560
  return httpServer;
546
561
  }
562
+ function createHttpRedirectServer(httpsPort) {
563
+ return http.createServer((req, res) => {
564
+ const host = (req.headers.host || "localhost").split(":")[0];
565
+ const portSuffix = httpsPort === 443 ? "" : `:${httpsPort}`;
566
+ const location = `https://${host}${portSuffix}${req.url || "/"}`;
567
+ res.writeHead(302, { Location: location, [PORTLESS_HEADER]: "1" });
568
+ res.end();
569
+ });
570
+ }
547
571
 
548
572
  // src/hosts.ts
549
573
  import * as fs2 from "fs";
@@ -636,7 +660,7 @@ import * as path2 from "path";
636
660
  import * as readline from "readline";
637
661
  import { execSync, spawn } from "child_process";
638
662
  var isWindows2 = process.platform === "win32";
639
- var DEFAULT_PROXY_PORT = 1355;
663
+ var FALLBACK_PROXY_PORT = 1355;
640
664
  var PRIVILEGED_PORT_THRESHOLD = 1024;
641
665
  var SYSTEM_STATE_DIR = isWindows2 ? path2.join(os.tmpdir(), "portless") : "/tmp/portless";
642
666
  var USER_STATE_DIR = path2.join(os.homedir(), ".portless");
@@ -655,13 +679,16 @@ var SIGNAL_CODES = {
655
679
  SIGKILL: 9,
656
680
  SIGTERM: 15
657
681
  };
658
- function getDefaultPort() {
682
+ function getProtocolPort(tls) {
683
+ return tls ? 443 : 80;
684
+ }
685
+ function getDefaultPort(tls) {
659
686
  const envPort = process.env.PORTLESS_PORT;
660
687
  if (envPort) {
661
688
  const port = parseInt(envPort, 10);
662
689
  if (!isNaN(port) && port >= 1 && port <= 65535) return port;
663
690
  }
664
- return DEFAULT_PROXY_PORT;
691
+ return tls === void 0 ? FALLBACK_PROXY_PORT : getProtocolPort(tls);
665
692
  }
666
693
  function resolveStateDir(port) {
667
694
  if (process.env.PORTLESS_STATE_DIR) return process.env.PORTLESS_STATE_DIR;
@@ -700,15 +727,15 @@ var DEFAULT_TLD = "localhost";
700
727
  var RISKY_TLDS = /* @__PURE__ */ new Map([
701
728
  ["local", "conflicts with mDNS/Bonjour on macOS"],
702
729
  ["dev", "Google-owned; browsers force HTTPS via preloaded HSTS"],
703
- ["com", "public TLD -- DNS requests will leak to the internet"],
704
- ["org", "public TLD -- DNS requests will leak to the internet"],
705
- ["net", "public TLD -- DNS requests will leak to the internet"],
706
- ["io", "public TLD -- DNS requests will leak to the internet"],
707
- ["app", "public TLD -- DNS requests will leak to the internet"],
708
- ["edu", "public TLD -- DNS requests will leak to the internet"],
709
- ["gov", "public TLD -- DNS requests will leak to the internet"],
710
- ["mil", "public TLD -- DNS requests will leak to the internet"],
711
- ["int", "public TLD -- DNS requests will leak to the internet"]
730
+ ["com", "public TLD; DNS requests will leak to the internet"],
731
+ ["org", "public TLD; DNS requests will leak to the internet"],
732
+ ["net", "public TLD; DNS requests will leak to the internet"],
733
+ ["io", "public TLD; DNS requests will leak to the internet"],
734
+ ["app", "public TLD; DNS requests will leak to the internet"],
735
+ ["edu", "public TLD; DNS requests will leak to the internet"],
736
+ ["gov", "public TLD; DNS requests will leak to the internet"],
737
+ ["mil", "public TLD; DNS requests will leak to the internet"],
738
+ ["int", "public TLD; DNS requests will leak to the internet"]
712
739
  ]);
713
740
  function validateTld(tld) {
714
741
  if (!tld) return "TLD cannot be empty";
@@ -744,8 +771,12 @@ function getDefaultTld() {
744
771
  if (err) throw new Error(`PORTLESS_TLD: ${err}`);
745
772
  return val;
746
773
  }
747
- function isHttpsEnvEnabled() {
774
+ function isHttpsEnvDisabled() {
748
775
  const val = process.env.PORTLESS_HTTPS;
776
+ return val === "0" || val === "false";
777
+ }
778
+ function isWildcardEnvEnabled() {
779
+ const val = process.env.PORTLESS_WILDCARD;
749
780
  return val === "1" || val === "true";
750
781
  }
751
782
  async function discoverState() {
@@ -772,8 +803,8 @@ async function discoverState() {
772
803
  return { dir: SYSTEM_STATE_DIR, port: systemPort, tls, tld };
773
804
  }
774
805
  }
775
- const defaultPort = getDefaultPort();
776
- const probePorts = /* @__PURE__ */ new Set([defaultPort, 443, 80]);
806
+ const configuredPort = getDefaultPort();
807
+ const probePorts = /* @__PURE__ */ new Set([443, 80, FALLBACK_PROXY_PORT, configuredPort]);
777
808
  for (const port of probePorts) {
778
809
  if (await isProxyRunning(port)) {
779
810
  const dir = resolveStateDir(port);
@@ -782,7 +813,12 @@ async function discoverState() {
782
813
  return { dir, port, tls, tld };
783
814
  }
784
815
  }
785
- return { dir: resolveStateDir(defaultPort), port: defaultPort, tls: false, tld: getDefaultTld() };
816
+ return {
817
+ dir: resolveStateDir(configuredPort),
818
+ port: configuredPort,
819
+ tls: false,
820
+ tld: getDefaultTld()
821
+ };
786
822
  }
787
823
  async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
788
824
  if (minPort > maxPort) {
@@ -905,11 +941,20 @@ function augmentedPath(env) {
905
941
  return allBins.join(path2.delimiter) + path2.delimiter + base;
906
942
  }
907
943
  function spawnCommand(commandArgs, options) {
908
- const env = { ...options?.env ?? process.env, PATH: augmentedPath(options?.env) };
909
- const child = isWindows2 ? spawn(commandArgs[0], commandArgs.slice(1), {
944
+ const env = {
945
+ ...options?.env ?? process.env,
946
+ PATH: augmentedPath(options?.env)
947
+ };
948
+ if (isWindows2) {
949
+ for (const key of Object.keys(env)) {
950
+ if (key !== "PATH" && key.toUpperCase() === "PATH") {
951
+ delete env[key];
952
+ }
953
+ }
954
+ }
955
+ const child = isWindows2 ? spawn("cmd.exe", ["/d", "/s", "/c", commandArgs.join(" ")], {
910
956
  stdio: "inherit",
911
- env,
912
- shell: true
957
+ env
913
958
  }) : spawn("/bin/sh", ["-c", commandArgs.map(shellEscape).join(" ")], {
914
959
  stdio: "inherit",
915
960
  env
@@ -1079,7 +1124,8 @@ var RouteStore = class _RouteStore {
1079
1124
  getRoutesPath() {
1080
1125
  return this.routesPath;
1081
1126
  }
1082
- // -- Locking ---------------------------------------------------------------
1127
+ // Locking
1128
+ // ---------------------------------------------------------------------------
1083
1129
  static sleepBuffer = new Int32Array(new SharedArrayBuffer(4));
1084
1130
  syncSleep(ms) {
1085
1131
  Atomics.wait(_RouteStore.sleepBuffer, 0, 0, ms);
@@ -1114,7 +1160,8 @@ var RouteStore = class _RouteStore {
1114
1160
  } catch {
1115
1161
  }
1116
1162
  }
1117
- // -- Route I/O -------------------------------------------------------------
1163
+ // Route I/O
1164
+ // ---------------------------------------------------------------------------
1118
1165
  isProcessAlive(pid) {
1119
1166
  try {
1120
1167
  process.kill(pid, 0);
@@ -1205,6 +1252,7 @@ export {
1205
1252
  parseHostname,
1206
1253
  PORTLESS_HEADER,
1207
1254
  createProxyServer,
1255
+ createHttpRedirectServer,
1208
1256
  extractManagedBlock,
1209
1257
  removeBlock,
1210
1258
  buildBlock,
@@ -1213,18 +1261,21 @@ export {
1213
1261
  getManagedHostnames,
1214
1262
  checkHostResolution,
1215
1263
  isWindows2 as isWindows,
1264
+ FALLBACK_PROXY_PORT,
1216
1265
  PRIVILEGED_PORT_THRESHOLD,
1266
+ WAIT_FOR_PROXY_MAX_ATTEMPTS,
1267
+ WAIT_FOR_PROXY_INTERVAL_MS,
1268
+ getProtocolPort,
1217
1269
  getDefaultPort,
1218
1270
  resolveStateDir,
1219
- readTlsMarker,
1220
1271
  writeTlsMarker,
1221
1272
  DEFAULT_TLD,
1222
1273
  RISKY_TLDS,
1223
1274
  validateTld,
1224
- readTldFromDir,
1225
1275
  writeTldFile,
1226
1276
  getDefaultTld,
1227
- isHttpsEnvEnabled,
1277
+ isHttpsEnvDisabled,
1278
+ isWildcardEnvEnabled,
1228
1279
  discoverState,
1229
1280
  findFreePort,
1230
1281
  isProxyRunning,