portless 0.5.1 → 0.6.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 +109 -115
- package/dist/{chunk-P3DHZHEZ.js → chunk-AB3HUERH.js} +106 -19
- package/dist/cli.js +252 -62
- package/dist/index.d.ts +12 -9
- package/dist/index.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,87 +1,100 @@
|
|
|
1
1
|
# portless
|
|
2
2
|
|
|
3
|
-
Replace port numbers with stable, named .localhost URLs. For humans and agents.
|
|
3
|
+
Replace port numbers with stable, named .localhost URLs for local development. For humans and agents.
|
|
4
4
|
|
|
5
5
|
```diff
|
|
6
|
-
- "dev": "next dev"
|
|
7
|
-
+ "dev": "portless run next dev"
|
|
6
|
+
- "dev": "next dev" # http://localhost:3000
|
|
7
|
+
+ "dev": "portless run next dev" # https://myapp.localhost
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Install
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
# Install
|
|
14
13
|
npm install -g portless
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
> Install globally. Do not add as a project dependency or run via npx.
|
|
17
|
+
|
|
18
|
+
## Run your app
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
```bash
|
|
21
|
+
# Enable HTTPS (one-time setup, auto-generates certs)
|
|
22
|
+
portless proxy start --https
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
portless myapp next dev
|
|
25
|
+
# -> https://myapp.localhost
|
|
26
|
+
|
|
27
|
+
# Without --https, runs on port 1355
|
|
21
28
|
portless myapp next dev
|
|
22
29
|
# -> http://myapp.localhost:1355
|
|
23
30
|
```
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
## Why
|
|
32
|
+
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), portless auto-injects `--port` and `--host` flags.
|
|
28
33
|
|
|
29
|
-
|
|
34
|
+
## Use in package.json
|
|
30
35
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- **Sharing URLs with teammates** -- "what port is that on?" becomes a Slack question
|
|
39
|
-
- **Browser history is useless** -- your history for `localhost:3000` is a jumble of unrelated projects
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "portless run next dev"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
## Subdomains
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
Organize services with subdomains:
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
|
-
# Auto-infer name from package.json / git / directory
|
|
47
|
-
portless run next dev
|
|
48
|
-
# -> http://<project>.localhost:1355
|
|
49
|
-
|
|
50
|
-
# Explicit name
|
|
51
|
-
portless myapp next dev
|
|
52
|
-
# -> http://myapp.localhost:1355
|
|
53
|
-
|
|
54
|
-
# Subdomains
|
|
55
49
|
portless api.myapp pnpm start
|
|
56
50
|
# -> http://api.myapp.localhost:1355
|
|
57
51
|
|
|
58
52
|
portless docs.myapp next dev
|
|
59
53
|
# -> http://docs.myapp.localhost:1355
|
|
54
|
+
```
|
|
55
|
+
|
|
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).
|
|
57
|
+
|
|
58
|
+
## Git Worktrees
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
#
|
|
60
|
+
`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
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# Main worktree -- no prefix
|
|
64
|
+
portless run next dev # -> http://myapp.localhost:1355
|
|
65
|
+
|
|
66
|
+
# Linked worktree on branch "fix-ui"
|
|
67
|
+
portless run next dev # -> http://fix-ui.myapp.localhost:1355
|
|
65
68
|
```
|
|
66
69
|
|
|
67
|
-
|
|
70
|
+
Use `--name` to override the inferred base name while keeping the worktree prefix:
|
|
68
71
|
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
"scripts": {
|
|
72
|
-
"dev": "portless run next dev"
|
|
73
|
-
}
|
|
74
|
-
}
|
|
72
|
+
```bash
|
|
73
|
+
portless run --name myapp next dev # -> http://fix-ui.myapp.localhost:1355
|
|
75
74
|
```
|
|
76
75
|
|
|
77
|
-
|
|
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`.
|
|
77
|
+
|
|
78
|
+
## Custom TLD
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
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
|
+
|
|
82
|
+
```bash
|
|
83
|
+
sudo portless proxy start --https --tld test
|
|
84
|
+
portless myapp next dev
|
|
85
|
+
# -> https://myapp.test
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The proxy auto-syncs `/etc/hosts` for custom TLDs when started with sudo, so `.test` domains resolve correctly.
|
|
89
|
+
|
|
90
|
+
Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflicts with mDNS/Bonjour) and `.dev` (Google-owned, forces HTTPS via HSTS).
|
|
91
|
+
|
|
92
|
+
## How it works
|
|
80
93
|
|
|
81
94
|
```mermaid
|
|
82
95
|
flowchart TD
|
|
83
96
|
Browser["Browser\nmyapp.localhost:1355"]
|
|
84
|
-
Proxy["portless proxy
|
|
97
|
+
Proxy["portless proxy\n(port 1355)"]
|
|
85
98
|
App1[":4123\nmyapp"]
|
|
86
99
|
App2[":4567\napi"]
|
|
87
100
|
|
|
@@ -94,8 +107,6 @@ flowchart TD
|
|
|
94
107
|
2. **Run apps** -- `portless <name> <command>` assigns a free port and registers with the proxy
|
|
95
108
|
3. **Access via URL** -- `http://<name>.localhost:1355` routes through the proxy to your app
|
|
96
109
|
|
|
97
|
-
Apps are assigned a random port (4000-4999) via the `PORT` and `HOST` environment variables. Most frameworks (Next.js, Express, Nuxt, etc.) respect these automatically. For frameworks that ignore `PORT` (Vite, Astro, React Router, Angular, Expo, React Native), portless auto-injects the correct `--port` and `--host` flags.
|
|
98
|
-
|
|
99
110
|
## HTTP/2 + HTTPS
|
|
100
111
|
|
|
101
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.
|
|
@@ -123,7 +134,7 @@ On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and
|
|
|
123
134
|
## Commands
|
|
124
135
|
|
|
125
136
|
```bash
|
|
126
|
-
portless run <cmd> [args...]
|
|
137
|
+
portless run [--name <name>] <cmd> [args...] # Infer name (or override with --name), run through proxy
|
|
127
138
|
portless <name> <cmd> [args...] # Run app at http://<name>.localhost:1355
|
|
128
139
|
portless alias <name> <port> # Register a static route (e.g. for Docker)
|
|
129
140
|
portless alias <name> <port> --force # Overwrite an existing route
|
|
@@ -135,7 +146,6 @@ portless hosts clean # Remove portless entries from /etc/hosts
|
|
|
135
146
|
|
|
136
147
|
# Disable portless (run command directly)
|
|
137
148
|
PORTLESS=0 pnpm dev # Bypasses proxy, uses default port
|
|
138
|
-
# Also accepts PORTLESS=skip
|
|
139
149
|
|
|
140
150
|
# Proxy control
|
|
141
151
|
portless proxy start # Start the proxy (port 1355, daemon)
|
|
@@ -143,64 +153,42 @@ portless proxy start --https # Start with HTTP/2 + TLS
|
|
|
143
153
|
portless proxy start -p 80 # Start on port 80 (requires sudo)
|
|
144
154
|
portless proxy start --foreground # Start in foreground (for debugging)
|
|
145
155
|
portless proxy stop # Stop the proxy
|
|
146
|
-
|
|
147
|
-
# Options
|
|
148
|
-
-p, --port <number> # Port for the proxy (default: 1355)
|
|
149
|
-
# Ports < 1024 require sudo
|
|
150
|
-
--https # Enable HTTP/2 + TLS with auto-generated certs
|
|
151
|
-
--cert <path> # Use a custom TLS certificate (implies --https)
|
|
152
|
-
--key <path> # Use a custom TLS private key (implies --https)
|
|
153
|
-
--no-tls # Disable HTTPS (overrides PORTLESS_HTTPS)
|
|
154
|
-
--foreground # Run proxy in foreground instead of daemon
|
|
155
|
-
--app-port <number> # Use a fixed port for the app (skip auto-assignment)
|
|
156
|
-
--force # Override a route registered by another process
|
|
157
|
-
--name <name> # Use <name> as the app name (bypasses subcommand dispatch)
|
|
158
|
-
-- # Stop flag parsing; everything after is passed to the child
|
|
159
|
-
|
|
160
|
-
# Injected into child processes
|
|
161
|
-
PORT # Ephemeral port the child should listen on
|
|
162
|
-
HOST # Always 127.0.0.1
|
|
163
|
-
PORTLESS_URL # Public URL (e.g. http://myapp.localhost:1355)
|
|
164
|
-
|
|
165
|
-
# Configuration
|
|
166
|
-
PORTLESS_PORT=<number> # Override the default proxy port
|
|
167
|
-
PORTLESS_APP_PORT=<number> # Use a fixed port for the app (same as --app-port)
|
|
168
|
-
PORTLESS_HTTPS=1|true # Always enable HTTPS
|
|
169
|
-
PORTLESS_SYNC_HOSTS=1 # Auto-sync /etc/hosts when routes change
|
|
170
|
-
PORTLESS_STATE_DIR=<path> # Override the state directory
|
|
171
|
-
|
|
172
|
-
# Info
|
|
173
|
-
portless --help # Show help
|
|
174
|
-
portless run --help # Show help for a specific subcommand
|
|
175
|
-
portless --version # Show version
|
|
176
156
|
```
|
|
177
157
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
## State Directory
|
|
181
|
-
|
|
182
|
-
Portless stores its state (routes, PID file, port file) in a directory that depends on the proxy port:
|
|
158
|
+
### Options
|
|
183
159
|
|
|
184
|
-
|
|
185
|
-
-
|
|
160
|
+
```
|
|
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)
|
|
166
|
+
--foreground Run proxy in foreground instead of daemon
|
|
167
|
+
--tld <tld> Use a custom TLD instead of .localhost (e.g. test)
|
|
168
|
+
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
169
|
+
--force Override a route registered by another process
|
|
170
|
+
--name <name> Use <name> as the app name
|
|
171
|
+
```
|
|
186
172
|
|
|
187
|
-
|
|
173
|
+
### Environment variables
|
|
188
174
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
175
|
+
```
|
|
176
|
+
# Configuration
|
|
177
|
+
PORTLESS_PORT=<number> Override the default proxy port
|
|
178
|
+
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
179
|
+
PORTLESS_HTTPS=1 Always enable HTTPS
|
|
180
|
+
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
|
|
181
|
+
PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (auto-enabled for custom TLDs)
|
|
182
|
+
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
192
183
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
pnpm test:coverage # Run tests with coverage
|
|
198
|
-
pnpm test:watch # Run tests in watch mode
|
|
199
|
-
pnpm lint # Lint all packages
|
|
200
|
-
pnpm typecheck # Type-check all packages
|
|
201
|
-
pnpm format # Format all files with Prettier
|
|
184
|
+
# Injected into child processes
|
|
185
|
+
PORT Ephemeral port the child should listen on
|
|
186
|
+
HOST Always 127.0.0.1
|
|
187
|
+
PORTLESS_URL Public URL (e.g. https://myapp.localhost)
|
|
202
188
|
```
|
|
203
189
|
|
|
190
|
+
> **Reserved names:** `run`, `alias`, `hosts`, `list`, `trust`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name from your project, or `portless --name <name> <cmd>` to force any name including reserved ones.
|
|
191
|
+
|
|
204
192
|
## Safari / DNS
|
|
205
193
|
|
|
206
194
|
`.localhost` subdomains auto-resolve to `127.0.0.1` in Chrome, Firefox, and Edge. Safari relies on the system DNS resolver, which may not handle `.localhost` subdomains on all configurations.
|
|
@@ -208,23 +196,15 @@ pnpm format # Format all files with Prettier
|
|
|
208
196
|
If Safari can't find your `.localhost` URL:
|
|
209
197
|
|
|
210
198
|
```bash
|
|
211
|
-
# Add current routes to /etc/hosts
|
|
212
|
-
sudo portless hosts
|
|
213
|
-
|
|
214
|
-
# Clean up later
|
|
215
|
-
sudo portless hosts clean
|
|
199
|
+
sudo portless hosts sync # Add current routes to /etc/hosts
|
|
200
|
+
sudo portless hosts clean # Clean up later
|
|
216
201
|
```
|
|
217
202
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
export PORTLESS_SYNC_HOSTS=1
|
|
222
|
-
sudo portless proxy start
|
|
223
|
-
```
|
|
203
|
+
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`.
|
|
224
204
|
|
|
225
205
|
## Proxying Between Portless Apps
|
|
226
206
|
|
|
227
|
-
If your frontend dev server (e.g. Vite, webpack) proxies API requests to another portless app, make sure the proxy rewrites the `Host` header. Without this,
|
|
207
|
+
If your frontend dev server (e.g. Vite, webpack) proxies API requests to another portless app, make sure the proxy rewrites the `Host` header. Without this, portless routes the request back to the frontend in an infinite loop.
|
|
228
208
|
|
|
229
209
|
**Vite** (`vite.config.ts`):
|
|
230
210
|
|
|
@@ -233,7 +213,7 @@ server: {
|
|
|
233
213
|
proxy: {
|
|
234
214
|
"/api": {
|
|
235
215
|
target: "http://api.myapp.localhost:1355",
|
|
236
|
-
changeOrigin: true,
|
|
216
|
+
changeOrigin: true,
|
|
237
217
|
ws: true,
|
|
238
218
|
},
|
|
239
219
|
},
|
|
@@ -247,13 +227,27 @@ devServer: {
|
|
|
247
227
|
proxy: [{
|
|
248
228
|
context: ["/api"],
|
|
249
229
|
target: "http://api.myapp.localhost:1355",
|
|
250
|
-
changeOrigin: true,
|
|
230
|
+
changeOrigin: true,
|
|
251
231
|
}],
|
|
252
232
|
}
|
|
253
233
|
```
|
|
254
234
|
|
|
255
235
|
Portless detects this misconfiguration and responds with `508 Loop Detected` along with a message pointing to this fix.
|
|
256
236
|
|
|
237
|
+
## Development
|
|
238
|
+
|
|
239
|
+
This repo is a pnpm workspace monorepo using [Turborepo](https://turbo.build). The publishable package lives in `packages/portless/`.
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
pnpm install # Install all dependencies
|
|
243
|
+
pnpm build # Build all packages
|
|
244
|
+
pnpm test # Run tests
|
|
245
|
+
pnpm test:coverage # Run tests with coverage
|
|
246
|
+
pnpm lint # Lint all packages
|
|
247
|
+
pnpm typecheck # Type-check all packages
|
|
248
|
+
pnpm format # Format all files with Prettier
|
|
249
|
+
```
|
|
250
|
+
|
|
257
251
|
## Requirements
|
|
258
252
|
|
|
259
253
|
- Node.js 20+
|
|
@@ -22,15 +22,19 @@ function formatUrl(hostname, proxyPort, tls = false) {
|
|
|
22
22
|
const defaultPort = tls ? 443 : 80;
|
|
23
23
|
return proxyPort === defaultPort ? `${proto}://${hostname}` : `${proto}://${hostname}:${proxyPort}`;
|
|
24
24
|
}
|
|
25
|
-
function parseHostname(input) {
|
|
25
|
+
function parseHostname(input, tld = "localhost") {
|
|
26
|
+
const suffix = `.${tld}`;
|
|
26
27
|
let hostname = input.trim().replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
|
|
27
|
-
if (
|
|
28
|
+
if (tld !== "localhost" && hostname.endsWith(".localhost")) {
|
|
29
|
+
hostname = hostname.slice(0, -".localhost".length);
|
|
30
|
+
}
|
|
31
|
+
if (!hostname || hostname === suffix) {
|
|
28
32
|
throw new Error("Hostname cannot be empty");
|
|
29
33
|
}
|
|
30
|
-
if (!hostname.endsWith(
|
|
31
|
-
hostname = `${hostname}
|
|
34
|
+
if (!hostname.endsWith(suffix)) {
|
|
35
|
+
hostname = `${hostname}${suffix}`;
|
|
32
36
|
}
|
|
33
|
-
const name = hostname.
|
|
37
|
+
const name = hostname.slice(0, -suffix.length);
|
|
34
38
|
if (name.includes("..")) {
|
|
35
39
|
throw new Error(`Invalid hostname "${name}": consecutive dots are not allowed`);
|
|
36
40
|
}
|
|
@@ -39,6 +43,14 @@ function parseHostname(input) {
|
|
|
39
43
|
`Invalid hostname "${name}": must contain only lowercase letters, digits, hyphens, and dots`
|
|
40
44
|
);
|
|
41
45
|
}
|
|
46
|
+
const labels = name.split(".");
|
|
47
|
+
for (const label of labels) {
|
|
48
|
+
if (label.length > 63) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Invalid hostname "${name}": label "${label}" exceeds 63-character DNS limit`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
42
54
|
return hostname;
|
|
43
55
|
}
|
|
44
56
|
|
|
@@ -291,7 +303,14 @@ function findRoute(routes, host) {
|
|
|
291
303
|
return routes.find((r) => r.hostname === host) || routes.find((r) => host.endsWith("." + r.hostname));
|
|
292
304
|
}
|
|
293
305
|
function createProxyServer(options) {
|
|
294
|
-
const {
|
|
306
|
+
const {
|
|
307
|
+
getRoutes,
|
|
308
|
+
proxyPort,
|
|
309
|
+
tld = "localhost",
|
|
310
|
+
onError = (msg) => console.error(msg),
|
|
311
|
+
tls
|
|
312
|
+
} = options;
|
|
313
|
+
const tldSuffix = `.${tld}`;
|
|
295
314
|
const isTls = !!tls;
|
|
296
315
|
const handleRequest = (req, res) => {
|
|
297
316
|
res.setHeader(PORTLESS_HEADER, "1");
|
|
@@ -314,7 +333,7 @@ function createProxyServer(options) {
|
|
|
314
333
|
"Loop Detected",
|
|
315
334
|
`<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: {
|
|
316
335
|
"/api": {
|
|
317
|
-
target: "http://<backend>
|
|
336
|
+
target: "http://<backend>${escapeHtml(tldSuffix)}:<port>",
|
|
318
337
|
changeOrigin: true,
|
|
319
338
|
},
|
|
320
339
|
}</pre></div></div>`
|
|
@@ -325,13 +344,15 @@ function createProxyServer(options) {
|
|
|
325
344
|
const route = findRoute(routes, host);
|
|
326
345
|
if (!route) {
|
|
327
346
|
const safeHost = escapeHtml(host);
|
|
328
|
-
const
|
|
347
|
+
const strippedHost = host.endsWith(tldSuffix) ? host.slice(0, -tldSuffix.length) : host;
|
|
348
|
+
const safeSuggestion = escapeHtml(strippedHost);
|
|
349
|
+
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>';
|
|
329
350
|
res.writeHead(404, { "Content-Type": "text/html" });
|
|
330
351
|
res.end(
|
|
331
352
|
renderPage(
|
|
332
353
|
404,
|
|
333
354
|
"Not Found",
|
|
334
|
-
`<div class="content"><p class="desc">No app registered for <strong>${safeHost}</strong></p>${routesList}<div class="section"><div class="terminal"><span class="prompt">$ </span>portless ${
|
|
355
|
+
`<div class="content"><p class="desc">No app registered for <strong>${safeHost}</strong></p>${routesList}<div class="section"><div class="terminal"><span class="prompt">$ </span>portless ${safeSuggestion} your-command</div></div></div>`
|
|
335
356
|
)
|
|
336
357
|
);
|
|
337
358
|
return;
|
|
@@ -587,7 +608,7 @@ function getManagedHostnames() {
|
|
|
587
608
|
return parts.length >= 2 ? parts[1] : "";
|
|
588
609
|
}).filter(Boolean);
|
|
589
610
|
}
|
|
590
|
-
function
|
|
611
|
+
function checkHostResolution(hostname) {
|
|
591
612
|
return new Promise((resolve) => {
|
|
592
613
|
dns.lookup(hostname, { family: 4 }, (err, address) => {
|
|
593
614
|
if (err) {
|
|
@@ -667,6 +688,54 @@ function writeTlsMarker(dir, enabled) {
|
|
|
667
688
|
}
|
|
668
689
|
}
|
|
669
690
|
}
|
|
691
|
+
var DEFAULT_TLD = "localhost";
|
|
692
|
+
var RISKY_TLDS = /* @__PURE__ */ new Map([
|
|
693
|
+
["local", "conflicts with mDNS/Bonjour on macOS"],
|
|
694
|
+
["dev", "Google-owned; browsers force HTTPS via preloaded HSTS"],
|
|
695
|
+
["com", "public TLD -- DNS requests will leak to the internet"],
|
|
696
|
+
["org", "public TLD -- DNS requests will leak to the internet"],
|
|
697
|
+
["net", "public TLD -- DNS requests will leak to the internet"],
|
|
698
|
+
["io", "public TLD -- DNS requests will leak to the internet"],
|
|
699
|
+
["app", "public TLD -- DNS requests will leak to the internet"],
|
|
700
|
+
["edu", "public TLD -- DNS requests will leak to the internet"],
|
|
701
|
+
["gov", "public TLD -- DNS requests will leak to the internet"],
|
|
702
|
+
["mil", "public TLD -- DNS requests will leak to the internet"],
|
|
703
|
+
["int", "public TLD -- DNS requests will leak to the internet"]
|
|
704
|
+
]);
|
|
705
|
+
function validateTld(tld) {
|
|
706
|
+
if (!tld) return "TLD cannot be empty";
|
|
707
|
+
if (!/^[a-z0-9]+$/.test(tld)) {
|
|
708
|
+
return `Invalid TLD "${tld}": must contain only lowercase letters and digits`;
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
var TLD_FILE = "proxy.tld";
|
|
713
|
+
function readTldFromDir(dir) {
|
|
714
|
+
try {
|
|
715
|
+
const raw = fs3.readFileSync(path.join(dir, TLD_FILE), "utf-8").trim();
|
|
716
|
+
return raw || DEFAULT_TLD;
|
|
717
|
+
} catch {
|
|
718
|
+
return DEFAULT_TLD;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function writeTldFile(dir, tld) {
|
|
722
|
+
const filePath = path.join(dir, TLD_FILE);
|
|
723
|
+
if (tld === DEFAULT_TLD) {
|
|
724
|
+
try {
|
|
725
|
+
fs3.unlinkSync(filePath);
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
} else {
|
|
729
|
+
fs3.writeFileSync(filePath, tld, { mode: 420 });
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
function getDefaultTld() {
|
|
733
|
+
const val = process.env.PORTLESS_TLD?.trim().toLowerCase();
|
|
734
|
+
if (!val) return DEFAULT_TLD;
|
|
735
|
+
const err = validateTld(val);
|
|
736
|
+
if (err) throw new Error(`PORTLESS_TLD: ${err}`);
|
|
737
|
+
return val;
|
|
738
|
+
}
|
|
670
739
|
function isHttpsEnvEnabled() {
|
|
671
740
|
const val = process.env.PORTLESS_HTTPS;
|
|
672
741
|
return val === "1" || val === "true";
|
|
@@ -676,24 +745,36 @@ async function discoverState() {
|
|
|
676
745
|
const dir = process.env.PORTLESS_STATE_DIR;
|
|
677
746
|
const port = readPortFromDir(dir) ?? getDefaultPort();
|
|
678
747
|
const tls = readTlsMarker(dir);
|
|
679
|
-
|
|
748
|
+
const tld = readTldFromDir(dir);
|
|
749
|
+
return { dir, port, tls, tld };
|
|
680
750
|
}
|
|
681
751
|
const userPort = readPortFromDir(USER_STATE_DIR);
|
|
682
752
|
if (userPort !== null) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
753
|
+
if (await isProxyRunning(userPort)) {
|
|
754
|
+
const tls = readTlsMarker(USER_STATE_DIR);
|
|
755
|
+
const tld = readTldFromDir(USER_STATE_DIR);
|
|
756
|
+
return { dir: USER_STATE_DIR, port: userPort, tls, tld };
|
|
686
757
|
}
|
|
687
758
|
}
|
|
688
759
|
const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
|
|
689
760
|
if (systemPort !== null) {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
761
|
+
if (await isProxyRunning(systemPort)) {
|
|
762
|
+
const tls = readTlsMarker(SYSTEM_STATE_DIR);
|
|
763
|
+
const tld = readTldFromDir(SYSTEM_STATE_DIR);
|
|
764
|
+
return { dir: SYSTEM_STATE_DIR, port: systemPort, tls, tld };
|
|
693
765
|
}
|
|
694
766
|
}
|
|
695
767
|
const defaultPort = getDefaultPort();
|
|
696
|
-
|
|
768
|
+
const probePorts = /* @__PURE__ */ new Set([defaultPort, 443, 80]);
|
|
769
|
+
for (const port of probePorts) {
|
|
770
|
+
if (await isProxyRunning(port)) {
|
|
771
|
+
const dir = resolveStateDir(port);
|
|
772
|
+
const tls = readTlsMarker(dir);
|
|
773
|
+
const tld = readTldFromDir(dir);
|
|
774
|
+
return { dir, port, tls, tld };
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return { dir: resolveStateDir(defaultPort), port: defaultPort, tls: false, tld: getDefaultTld() };
|
|
697
778
|
}
|
|
698
779
|
async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
|
|
699
780
|
if (minPort > maxPort) {
|
|
@@ -1067,12 +1148,18 @@ export {
|
|
|
1067
1148
|
syncHostsFile,
|
|
1068
1149
|
cleanHostsFile,
|
|
1069
1150
|
getManagedHostnames,
|
|
1070
|
-
|
|
1151
|
+
checkHostResolution,
|
|
1071
1152
|
PRIVILEGED_PORT_THRESHOLD,
|
|
1072
1153
|
getDefaultPort,
|
|
1073
1154
|
resolveStateDir,
|
|
1074
1155
|
readTlsMarker,
|
|
1075
1156
|
writeTlsMarker,
|
|
1157
|
+
DEFAULT_TLD,
|
|
1158
|
+
RISKY_TLDS,
|
|
1159
|
+
validateTld,
|
|
1160
|
+
readTldFromDir,
|
|
1161
|
+
writeTldFile,
|
|
1162
|
+
getDefaultTld,
|
|
1076
1163
|
isHttpsEnvEnabled,
|
|
1077
1164
|
discoverState,
|
|
1078
1165
|
findFreePort,
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_TLD,
|
|
3
4
|
FILE_MODE,
|
|
4
5
|
PRIVILEGED_PORT_THRESHOLD,
|
|
6
|
+
RISKY_TLDS,
|
|
5
7
|
RouteConflictError,
|
|
6
8
|
RouteStore,
|
|
7
9
|
cleanHostsFile,
|
|
@@ -12,19 +14,23 @@ import {
|
|
|
12
14
|
fixOwnership,
|
|
13
15
|
formatUrl,
|
|
14
16
|
getDefaultPort,
|
|
17
|
+
getDefaultTld,
|
|
15
18
|
injectFrameworkFlags,
|
|
16
19
|
isErrnoException,
|
|
17
20
|
isHttpsEnvEnabled,
|
|
18
21
|
isProxyRunning,
|
|
19
22
|
parseHostname,
|
|
20
23
|
prompt,
|
|
24
|
+
readTldFromDir,
|
|
21
25
|
readTlsMarker,
|
|
22
26
|
resolveStateDir,
|
|
23
27
|
spawnCommand,
|
|
24
28
|
syncHostsFile,
|
|
29
|
+
validateTld,
|
|
25
30
|
waitForProxy,
|
|
31
|
+
writeTldFile,
|
|
26
32
|
writeTlsMarker
|
|
27
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-AB3HUERH.js";
|
|
28
34
|
|
|
29
35
|
// src/cli.ts
|
|
30
36
|
import chalk from "chalk";
|
|
@@ -218,19 +224,24 @@ function isCATrusted(stateDir) {
|
|
|
218
224
|
}
|
|
219
225
|
function isCATrustedMacOS(caCertPath) {
|
|
220
226
|
try {
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
228
|
+
const sudoUser = process.env.SUDO_USER;
|
|
229
|
+
if (isRoot && sudoUser) {
|
|
230
|
+
execFileSync(
|
|
231
|
+
"sudo",
|
|
232
|
+
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
233
|
+
{
|
|
234
|
+
stdio: "pipe",
|
|
235
|
+
timeout: 5e3
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
} else {
|
|
239
|
+
execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
240
|
+
stdio: "pipe",
|
|
241
|
+
timeout: 5e3
|
|
242
|
+
});
|
|
232
243
|
}
|
|
233
|
-
return
|
|
244
|
+
return true;
|
|
234
245
|
} catch {
|
|
235
246
|
return false;
|
|
236
247
|
}
|
|
@@ -364,12 +375,12 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
364
375
|
fixOwnership(keyPath, certPath);
|
|
365
376
|
return { certPath, keyPath };
|
|
366
377
|
}
|
|
367
|
-
function createSNICallback(stateDir, defaultCert, defaultKey) {
|
|
378
|
+
function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost") {
|
|
368
379
|
const cache = /* @__PURE__ */ new Map();
|
|
369
380
|
const pending = /* @__PURE__ */ new Map();
|
|
370
381
|
const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
|
|
371
382
|
return (servername, cb) => {
|
|
372
|
-
if (servername ===
|
|
383
|
+
if (servername === tld) {
|
|
373
384
|
cb(null, defaultCtx);
|
|
374
385
|
return;
|
|
375
386
|
}
|
|
@@ -422,12 +433,29 @@ function trustCA(stateDir) {
|
|
|
422
433
|
}
|
|
423
434
|
try {
|
|
424
435
|
if (process.platform === "darwin") {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
436
|
+
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
437
|
+
if (isRoot) {
|
|
438
|
+
execFileSync(
|
|
439
|
+
"security",
|
|
440
|
+
[
|
|
441
|
+
"add-trusted-cert",
|
|
442
|
+
"-d",
|
|
443
|
+
"-r",
|
|
444
|
+
"trustRoot",
|
|
445
|
+
"-k",
|
|
446
|
+
"/Library/Keychains/System.keychain",
|
|
447
|
+
caCertPath
|
|
448
|
+
],
|
|
449
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
450
|
+
);
|
|
451
|
+
} else {
|
|
452
|
+
const keychain = loginKeychainPath();
|
|
453
|
+
execFileSync(
|
|
454
|
+
"security",
|
|
455
|
+
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
456
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
457
|
+
);
|
|
458
|
+
}
|
|
431
459
|
return { trusted: true };
|
|
432
460
|
} else if (process.platform === "linux") {
|
|
433
461
|
const config = getLinuxCATrustConfig();
|
|
@@ -453,11 +481,21 @@ function trustCA(stateDir) {
|
|
|
453
481
|
}
|
|
454
482
|
|
|
455
483
|
// src/auto.ts
|
|
484
|
+
import { createHash } from "crypto";
|
|
456
485
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
457
486
|
import * as fs2 from "fs";
|
|
458
487
|
import * as path2 from "path";
|
|
488
|
+
var MAX_DNS_LABEL_LENGTH = 63;
|
|
489
|
+
function truncateLabel(label) {
|
|
490
|
+
if (label.length <= MAX_DNS_LABEL_LENGTH) return label;
|
|
491
|
+
const hash = createHash("sha256").update(label).digest("hex").slice(0, 6);
|
|
492
|
+
const maxPrefixLength = MAX_DNS_LABEL_LENGTH - 7;
|
|
493
|
+
const prefix = label.slice(0, maxPrefixLength).replace(/-+$/, "");
|
|
494
|
+
return `${prefix}-${hash}`;
|
|
495
|
+
}
|
|
459
496
|
function sanitizeForHostname(name) {
|
|
460
|
-
|
|
497
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
498
|
+
return truncateLabel(sanitized);
|
|
461
499
|
}
|
|
462
500
|
function inferProjectName(cwd = process.cwd()) {
|
|
463
501
|
const pkgResult = findPackageJsonName(cwd);
|
|
@@ -602,7 +640,7 @@ var DEBOUNCE_MS = 100;
|
|
|
602
640
|
var POLL_INTERVAL_MS = 3e3;
|
|
603
641
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
604
642
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
605
|
-
function startProxyServer(store, proxyPort, tlsOptions) {
|
|
643
|
+
function startProxyServer(store, proxyPort, tld, tlsOptions) {
|
|
606
644
|
store.ensureDir();
|
|
607
645
|
const isTls = !!tlsOptions;
|
|
608
646
|
const routesPath = store.getRoutesPath();
|
|
@@ -618,7 +656,8 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
618
656
|
let debounceTimer = null;
|
|
619
657
|
let watcher = null;
|
|
620
658
|
let pollingInterval = null;
|
|
621
|
-
const
|
|
659
|
+
const syncVal = process.env.PORTLESS_SYNC_HOSTS;
|
|
660
|
+
const autoSyncHosts = syncVal === "1" || syncVal === "true" || tld !== DEFAULT_TLD && syncVal !== "0" && syncVal !== "false";
|
|
622
661
|
const reloadRoutes = () => {
|
|
623
662
|
try {
|
|
624
663
|
cachedRoutes = store.loadRoutes();
|
|
@@ -643,6 +682,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
643
682
|
const server = createProxyServer({
|
|
644
683
|
getRoutes: () => cachedRoutes,
|
|
645
684
|
proxyPort,
|
|
685
|
+
tld,
|
|
646
686
|
onError: (msg) => console.error(chalk.red(msg)),
|
|
647
687
|
tls: tlsOptions
|
|
648
688
|
});
|
|
@@ -668,9 +708,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
668
708
|
fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
669
709
|
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
670
710
|
writeTlsMarker(store.dir, isTls);
|
|
711
|
+
writeTldFile(store.dir, tld);
|
|
671
712
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
672
713
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
673
|
-
|
|
714
|
+
const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
|
|
715
|
+
console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}`));
|
|
674
716
|
});
|
|
675
717
|
let exiting = false;
|
|
676
718
|
const cleanup = () => {
|
|
@@ -690,6 +732,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
690
732
|
} catch {
|
|
691
733
|
}
|
|
692
734
|
writeTlsMarker(store.dir, false);
|
|
735
|
+
writeTldFile(store.dir, DEFAULT_TLD);
|
|
693
736
|
if (autoSyncHosts) cleanHostsFile();
|
|
694
737
|
server.close(() => process.exit(0));
|
|
695
738
|
setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
|
|
@@ -699,12 +742,12 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
699
742
|
console.log(chalk.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
700
743
|
console.log(chalk.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
701
744
|
}
|
|
702
|
-
async function stopProxy(store, proxyPort,
|
|
745
|
+
async function stopProxy(store, proxyPort, _tls) {
|
|
703
746
|
const pidPath = store.pidPath;
|
|
704
747
|
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
705
748
|
const sudoHint = needsSudo ? "sudo " : "";
|
|
706
749
|
if (!fs3.existsSync(pidPath)) {
|
|
707
|
-
if (await isProxyRunning(proxyPort
|
|
750
|
+
if (await isProxyRunning(proxyPort)) {
|
|
708
751
|
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
709
752
|
const pid = findPidOnPort(proxyPort);
|
|
710
753
|
if (pid !== null) {
|
|
@@ -759,7 +802,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
759
802
|
}
|
|
760
803
|
return;
|
|
761
804
|
}
|
|
762
|
-
if (!await isProxyRunning(proxyPort
|
|
805
|
+
if (!await isProxyRunning(proxyPort)) {
|
|
763
806
|
console.log(
|
|
764
807
|
chalk.yellow(
|
|
765
808
|
`PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
|
|
@@ -806,8 +849,22 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
806
849
|
}
|
|
807
850
|
console.log();
|
|
808
851
|
}
|
|
809
|
-
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force, autoInfo, desiredPort) {
|
|
810
|
-
const hostname = parseHostname(name);
|
|
852
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
|
|
853
|
+
const hostname = parseHostname(name, tld);
|
|
854
|
+
let envTld;
|
|
855
|
+
try {
|
|
856
|
+
envTld = getDefaultTld();
|
|
857
|
+
} catch (err) {
|
|
858
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
859
|
+
process.exit(1);
|
|
860
|
+
}
|
|
861
|
+
if (envTld !== DEFAULT_TLD && envTld !== tld) {
|
|
862
|
+
console.warn(
|
|
863
|
+
chalk.yellow(
|
|
864
|
+
`Warning: PORTLESS_TLD=${envTld} but the running proxy uses .${tld}. Using .${tld}.`
|
|
865
|
+
)
|
|
866
|
+
);
|
|
867
|
+
}
|
|
811
868
|
console.log(chalk.blue.bold(`
|
|
812
869
|
portless
|
|
813
870
|
`));
|
|
@@ -845,6 +902,7 @@ portless
|
|
|
845
902
|
console.log(chalk.yellow("Starting proxy (requires sudo)..."));
|
|
846
903
|
const startArgs = [process.execPath, process.argv[1], "proxy", "start"];
|
|
847
904
|
if (wantHttps) startArgs.push("--https");
|
|
905
|
+
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
848
906
|
const result = spawnSync("sudo", startArgs, {
|
|
849
907
|
stdio: "inherit",
|
|
850
908
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
@@ -859,6 +917,7 @@ portless
|
|
|
859
917
|
console.log(chalk.yellow("Starting proxy..."));
|
|
860
918
|
const startArgs = [process.argv[1], "proxy", "start"];
|
|
861
919
|
if (wantHttps) startArgs.push("--https");
|
|
920
|
+
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
862
921
|
const result = spawnSync(process.execPath, startArgs, {
|
|
863
922
|
stdio: "inherit",
|
|
864
923
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
@@ -871,6 +930,7 @@ portless
|
|
|
871
930
|
}
|
|
872
931
|
}
|
|
873
932
|
const autoTls = readTlsMarker(stateDir);
|
|
933
|
+
tld = readTldFromDir(stateDir);
|
|
874
934
|
if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
|
|
875
935
|
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
876
936
|
const logPath = path3.join(stateDir, "proxy.log");
|
|
@@ -918,7 +978,7 @@ portless
|
|
|
918
978
|
PORT: port.toString(),
|
|
919
979
|
HOST: "127.0.0.1",
|
|
920
980
|
PORTLESS_URL: finalUrl,
|
|
921
|
-
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS:
|
|
981
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}`
|
|
922
982
|
},
|
|
923
983
|
onCleanup: () => {
|
|
924
984
|
try {
|
|
@@ -953,6 +1013,7 @@ function appPortFromEnv() {
|
|
|
953
1013
|
function parseRunArgs(args) {
|
|
954
1014
|
let force = false;
|
|
955
1015
|
let appPort;
|
|
1016
|
+
let name;
|
|
956
1017
|
let i = 0;
|
|
957
1018
|
while (i < args.length && args[i].startsWith("-")) {
|
|
958
1019
|
if (args[i] === "--") {
|
|
@@ -966,6 +1027,7 @@ ${chalk.bold("Usage:")}
|
|
|
966
1027
|
${chalk.cyan("portless run [options] <command...>")}
|
|
967
1028
|
|
|
968
1029
|
${chalk.bold("Options:")}
|
|
1030
|
+
--name <name> Override the inferred base name (worktree prefix still applies)
|
|
969
1031
|
--force Override an existing route registered by another process
|
|
970
1032
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
971
1033
|
--help, -h Show this help
|
|
@@ -975,11 +1037,13 @@ ${chalk.bold("Name inference (in order):")}
|
|
|
975
1037
|
2. Git repo root directory name
|
|
976
1038
|
3. Current directory basename
|
|
977
1039
|
|
|
1040
|
+
Use --name to override the inferred name while keeping worktree prefixes.
|
|
978
1041
|
In git worktrees, the branch name is prepended as a subdomain prefix
|
|
979
1042
|
(e.g. feature-auth.myapp.localhost).
|
|
980
1043
|
|
|
981
1044
|
${chalk.bold("Examples:")}
|
|
982
1045
|
portless run next dev # -> http://<project>.localhost:1355
|
|
1046
|
+
portless run --name myapp next dev # -> http://myapp.localhost:1355
|
|
983
1047
|
portless run vite dev # -> http://<project>.localhost:1355
|
|
984
1048
|
portless run --app-port 3000 pnpm start
|
|
985
1049
|
`);
|
|
@@ -989,15 +1053,23 @@ ${chalk.bold("Examples:")}
|
|
|
989
1053
|
} else if (args[i] === "--app-port") {
|
|
990
1054
|
i++;
|
|
991
1055
|
appPort = parseAppPort(args[i]);
|
|
1056
|
+
} else if (args[i] === "--name") {
|
|
1057
|
+
i++;
|
|
1058
|
+
if (!args[i] || args[i].startsWith("-")) {
|
|
1059
|
+
console.error(chalk.red("Error: --name requires a name value."));
|
|
1060
|
+
console.error(chalk.cyan(" portless run --name <name> <command...>"));
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
}
|
|
1063
|
+
name = args[i];
|
|
992
1064
|
} else {
|
|
993
1065
|
console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
|
|
994
|
-
console.error(chalk.blue("Known flags: --force, --app-port, --help"));
|
|
1066
|
+
console.error(chalk.blue("Known flags: --name, --force, --app-port, --help"));
|
|
995
1067
|
process.exit(1);
|
|
996
1068
|
}
|
|
997
1069
|
i++;
|
|
998
1070
|
}
|
|
999
1071
|
if (!appPort) appPort = appPortFromEnv();
|
|
1000
|
-
return { force, appPort, commandArgs: args.slice(i) };
|
|
1072
|
+
return { force, appPort, name, commandArgs: args.slice(i) };
|
|
1001
1073
|
}
|
|
1002
1074
|
function parseAppArgs(args) {
|
|
1003
1075
|
let force = false;
|
|
@@ -1058,6 +1130,7 @@ ${chalk.bold("Usage:")}
|
|
|
1058
1130
|
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
1059
1131
|
${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
1060
1132
|
${chalk.cyan("portless run <cmd>")} Infer name from project, run through proxy
|
|
1133
|
+
${chalk.cyan("portless get <name>")} Print URL for a service (for cross-service refs)
|
|
1061
1134
|
${chalk.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
|
|
1062
1135
|
${chalk.cyan("portless alias --remove <name>")} Remove a static route
|
|
1063
1136
|
${chalk.cyan("portless list")} Show active routes
|
|
@@ -1073,6 +1146,7 @@ ${chalk.bold("Examples:")}
|
|
|
1073
1146
|
portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
|
|
1074
1147
|
portless run next dev # -> http://<project>.localhost:1355
|
|
1075
1148
|
portless run next dev # in worktree -> http://<worktree>.<project>.localhost:1355
|
|
1149
|
+
portless get backend # -> http://backend.localhost:1355 (for cross-service refs)
|
|
1076
1150
|
# Wildcard subdomains: tenant.myapp.localhost also routes to myapp
|
|
1077
1151
|
|
|
1078
1152
|
${chalk.bold("In package.json:")}
|
|
@@ -1097,7 +1171,7 @@ ${chalk.bold("HTTP/2 + HTTPS:")}
|
|
|
1097
1171
|
system trust store. No browser warnings. No sudo required on macOS.
|
|
1098
1172
|
|
|
1099
1173
|
${chalk.bold("Options:")}
|
|
1100
|
-
run <cmd>
|
|
1174
|
+
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
1101
1175
|
Adds worktree prefix in git worktrees
|
|
1102
1176
|
-p, --port <number> Port for the proxy to listen on (default: 1355)
|
|
1103
1177
|
Ports < 1024 require sudo
|
|
@@ -1106,6 +1180,7 @@ ${chalk.bold("Options:")}
|
|
|
1106
1180
|
--key <path> Use a custom TLS private key (implies --https)
|
|
1107
1181
|
--no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
|
|
1108
1182
|
--foreground Run proxy in foreground (for debugging)
|
|
1183
|
+
--tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
|
|
1109
1184
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
1110
1185
|
--force Override an existing route registered by another process
|
|
1111
1186
|
--name <name> Use <name> as the app name (bypasses subcommand dispatch)
|
|
@@ -1114,10 +1189,11 @@ ${chalk.bold("Options:")}
|
|
|
1114
1189
|
${chalk.bold("Environment variables:")}
|
|
1115
1190
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
1116
1191
|
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1117
|
-
PORTLESS_HTTPS=1
|
|
1118
|
-
|
|
1192
|
+
PORTLESS_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
|
|
1193
|
+
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1194
|
+
PORTLESS_SYNC_HOSTS=1 Auto-sync /etc/hosts (auto-enabled for custom TLDs)
|
|
1119
1195
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
1120
|
-
PORTLESS=0
|
|
1196
|
+
PORTLESS=0 Run command directly without proxy
|
|
1121
1197
|
|
|
1122
1198
|
${chalk.bold("Child process environment:")}
|
|
1123
1199
|
PORT Ephemeral port the child should listen on
|
|
@@ -1127,26 +1203,24 @@ ${chalk.bold("Child process environment:")}
|
|
|
1127
1203
|
${chalk.bold("Safari / DNS:")}
|
|
1128
1204
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1129
1205
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1130
|
-
|
|
1206
|
+
Auto-syncs /etc/hosts for custom TLDs (e.g. --tld test). For .localhost,
|
|
1207
|
+
set PORTLESS_SYNC_HOSTS=1 to enable. To manually sync:
|
|
1131
1208
|
${chalk.cyan("sudo portless hosts sync")}
|
|
1132
|
-
|
|
1209
|
+
Clean up later with:
|
|
1133
1210
|
${chalk.cyan("sudo portless hosts clean")}
|
|
1134
|
-
To auto-sync whenever routes change, set PORTLESS_SYNC_HOSTS=1 and
|
|
1135
|
-
start the proxy with sudo.
|
|
1136
1211
|
|
|
1137
1212
|
${chalk.bold("Skip portless:")}
|
|
1138
1213
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
1139
|
-
PORTLESS=skip pnpm dev # Same as above
|
|
1140
1214
|
|
|
1141
1215
|
${chalk.bold("Reserved names:")}
|
|
1142
|
-
run, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
1143
|
-
used as app names directly. Use "portless run" to infer the name,
|
|
1144
|
-
"portless --name <name>" to force any name including reserved ones.
|
|
1216
|
+
run, get, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
1217
|
+
be used as app names directly. Use "portless run" to infer the name,
|
|
1218
|
+
or "portless --name <name>" to force any name including reserved ones.
|
|
1145
1219
|
`);
|
|
1146
1220
|
process.exit(0);
|
|
1147
1221
|
}
|
|
1148
1222
|
function printVersion() {
|
|
1149
|
-
console.log("0.
|
|
1223
|
+
console.log("0.6.0");
|
|
1150
1224
|
process.exit(0);
|
|
1151
1225
|
}
|
|
1152
1226
|
async function handleTrust() {
|
|
@@ -1171,6 +1245,60 @@ async function handleList() {
|
|
|
1171
1245
|
});
|
|
1172
1246
|
listRoutes(store, port, tls2);
|
|
1173
1247
|
}
|
|
1248
|
+
async function handleGet(args) {
|
|
1249
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
1250
|
+
console.log(`
|
|
1251
|
+
${chalk.bold("portless get")} - Print the URL for a service.
|
|
1252
|
+
|
|
1253
|
+
${chalk.bold("Usage:")}
|
|
1254
|
+
${chalk.cyan("portless get <name>")}
|
|
1255
|
+
|
|
1256
|
+
Constructs the URL using the same hostname and worktree logic as
|
|
1257
|
+
"portless run", then prints it to stdout. Useful for wiring services
|
|
1258
|
+
together:
|
|
1259
|
+
|
|
1260
|
+
BACKEND_URL=$(portless get backend)
|
|
1261
|
+
|
|
1262
|
+
${chalk.bold("Options:")}
|
|
1263
|
+
--no-worktree Skip worktree prefix detection
|
|
1264
|
+
--help, -h Show this help
|
|
1265
|
+
|
|
1266
|
+
${chalk.bold("Examples:")}
|
|
1267
|
+
portless get backend # -> http://backend.localhost:1355
|
|
1268
|
+
portless get backend # in worktree -> http://auth.backend.localhost:1355
|
|
1269
|
+
portless get backend --no-worktree # -> http://backend.localhost:1355 (skip worktree)
|
|
1270
|
+
`);
|
|
1271
|
+
process.exit(0);
|
|
1272
|
+
}
|
|
1273
|
+
let skipWorktree = false;
|
|
1274
|
+
const positional = [];
|
|
1275
|
+
for (let i = 1; i < args.length; i++) {
|
|
1276
|
+
if (args[i] === "--no-worktree") {
|
|
1277
|
+
skipWorktree = true;
|
|
1278
|
+
} else if (args[i].startsWith("-")) {
|
|
1279
|
+
console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
|
|
1280
|
+
console.error(chalk.blue("Known flags: --no-worktree, --help"));
|
|
1281
|
+
process.exit(1);
|
|
1282
|
+
} else {
|
|
1283
|
+
positional.push(args[i]);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (positional.length === 0) {
|
|
1287
|
+
console.error(chalk.red("Error: Missing service name."));
|
|
1288
|
+
console.error(chalk.blue("Usage:"));
|
|
1289
|
+
console.error(chalk.cyan(" portless get <name>"));
|
|
1290
|
+
console.error(chalk.blue("Example:"));
|
|
1291
|
+
console.error(chalk.cyan(" portless get backend"));
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
}
|
|
1294
|
+
const name = positional[0];
|
|
1295
|
+
const worktree = skipWorktree ? null : detectWorktreePrefix();
|
|
1296
|
+
const effectiveName = worktree ? `${worktree.prefix}.${name}` : name;
|
|
1297
|
+
const { port, tls: tls2, tld } = await discoverState();
|
|
1298
|
+
const hostname = parseHostname(effectiveName, tld);
|
|
1299
|
+
const url = formatUrl(hostname, port, tls2);
|
|
1300
|
+
process.stdout.write(url + "\n");
|
|
1301
|
+
}
|
|
1174
1302
|
async function handleAlias(args) {
|
|
1175
1303
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
1176
1304
|
console.log(`
|
|
@@ -1188,7 +1316,7 @@ ${chalk.bold("Examples:")}
|
|
|
1188
1316
|
`);
|
|
1189
1317
|
process.exit(0);
|
|
1190
1318
|
}
|
|
1191
|
-
const { dir } = await discoverState();
|
|
1319
|
+
const { dir, tld } = await discoverState();
|
|
1192
1320
|
const store = new RouteStore(dir, {
|
|
1193
1321
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1194
1322
|
});
|
|
@@ -1199,7 +1327,7 @@ ${chalk.bold("Examples:")}
|
|
|
1199
1327
|
console.error(chalk.cyan(" portless alias --remove <name>"));
|
|
1200
1328
|
process.exit(1);
|
|
1201
1329
|
}
|
|
1202
|
-
const hostname2 = parseHostname(aliasName2);
|
|
1330
|
+
const hostname2 = parseHostname(aliasName2, tld);
|
|
1203
1331
|
const routes = store.loadRoutes();
|
|
1204
1332
|
const existing = routes.find((r) => r.hostname === hostname2 && r.pid === 0);
|
|
1205
1333
|
if (!existing) {
|
|
@@ -1221,7 +1349,7 @@ ${chalk.bold("Examples:")}
|
|
|
1221
1349
|
console.error(chalk.cyan(" portless alias my-postgres 5432"));
|
|
1222
1350
|
process.exit(1);
|
|
1223
1351
|
}
|
|
1224
|
-
const hostname = parseHostname(aliasName);
|
|
1352
|
+
const hostname = parseHostname(aliasName, tld);
|
|
1225
1353
|
const port = parseInt(aliasPort, 10);
|
|
1226
1354
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1227
1355
|
console.error(chalk.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
|
|
@@ -1229,7 +1357,7 @@ ${chalk.bold("Examples:")}
|
|
|
1229
1357
|
}
|
|
1230
1358
|
const force = args.includes("--force");
|
|
1231
1359
|
store.addRoute(hostname, port, 0, force);
|
|
1232
|
-
console.log(chalk.green(`Alias registered: ${hostname} ->
|
|
1360
|
+
console.log(chalk.green(`Alias registered: ${hostname} -> 127.0.0.1:${port}`));
|
|
1233
1361
|
}
|
|
1234
1362
|
async function handleHosts(args) {
|
|
1235
1363
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
@@ -1244,8 +1372,8 @@ ${chalk.bold("Usage:")}
|
|
|
1244
1372
|
${chalk.cyan("sudo portless hosts clean")} Remove portless entries from /etc/hosts
|
|
1245
1373
|
|
|
1246
1374
|
${chalk.bold("Auto-sync:")}
|
|
1247
|
-
|
|
1248
|
-
|
|
1375
|
+
Auto-enabled for custom TLDs (e.g. --tld test). For .localhost, set
|
|
1376
|
+
PORTLESS_SYNC_HOSTS=1 to enable. Disable with PORTLESS_SYNC_HOSTS=0.
|
|
1249
1377
|
`);
|
|
1250
1378
|
process.exit(0);
|
|
1251
1379
|
}
|
|
@@ -1315,6 +1443,7 @@ ${chalk.bold("Usage:")}
|
|
|
1315
1443
|
${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS
|
|
1316
1444
|
${chalk.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1317
1445
|
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
1446
|
+
${chalk.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
1318
1447
|
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
1319
1448
|
`);
|
|
1320
1449
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
@@ -1363,6 +1492,39 @@ ${chalk.bold("Usage:")}
|
|
|
1363
1492
|
console.error(chalk.red("Error: --cert and --key must be used together."));
|
|
1364
1493
|
process.exit(1);
|
|
1365
1494
|
}
|
|
1495
|
+
let tld;
|
|
1496
|
+
try {
|
|
1497
|
+
tld = getDefaultTld();
|
|
1498
|
+
} catch (err) {
|
|
1499
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
}
|
|
1502
|
+
const tldIdx = args.indexOf("--tld");
|
|
1503
|
+
if (tldIdx !== -1) {
|
|
1504
|
+
const tldValue = args[tldIdx + 1];
|
|
1505
|
+
if (!tldValue || tldValue.startsWith("-")) {
|
|
1506
|
+
console.error(chalk.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
|
|
1507
|
+
process.exit(1);
|
|
1508
|
+
}
|
|
1509
|
+
tld = tldValue.trim().toLowerCase();
|
|
1510
|
+
const tldErr = validateTld(tld);
|
|
1511
|
+
if (tldErr) {
|
|
1512
|
+
console.error(chalk.red(`Error: ${tldErr}`));
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
const riskyReason = RISKY_TLDS.get(tld);
|
|
1517
|
+
if (riskyReason) {
|
|
1518
|
+
console.warn(chalk.yellow(`Warning: .${tld} -- ${riskyReason}`));
|
|
1519
|
+
}
|
|
1520
|
+
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
1521
|
+
if (tld !== DEFAULT_TLD && syncDisabled) {
|
|
1522
|
+
console.warn(
|
|
1523
|
+
chalk.yellow(`Warning: .${tld} domains require /etc/hosts entries to resolve to 127.0.0.1.`)
|
|
1524
|
+
);
|
|
1525
|
+
console.warn(chalk.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
1526
|
+
console.warn(chalk.cyan(" sudo portless hosts sync"));
|
|
1527
|
+
}
|
|
1366
1528
|
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1367
1529
|
const stateDir = resolveStateDir(proxyPort);
|
|
1368
1530
|
const store = new RouteStore(stateDir, {
|
|
@@ -1374,8 +1536,13 @@ ${chalk.bold("Usage:")}
|
|
|
1374
1536
|
}
|
|
1375
1537
|
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1376
1538
|
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
1539
|
+
const portFlag = proxyPort !== getDefaultPort() ? ` -p ${proxyPort}` : "";
|
|
1377
1540
|
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1378
|
-
console.log(
|
|
1541
|
+
console.log(
|
|
1542
|
+
chalk.blue(
|
|
1543
|
+
`To restart: ${sudoPrefix}portless proxy stop${portFlag} && ${sudoPrefix}portless proxy start${portFlag}`
|
|
1544
|
+
)
|
|
1545
|
+
);
|
|
1379
1546
|
return;
|
|
1380
1547
|
}
|
|
1381
1548
|
if (proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
@@ -1440,13 +1607,13 @@ ${chalk.bold("Usage:")}
|
|
|
1440
1607
|
tlsOptions = {
|
|
1441
1608
|
cert,
|
|
1442
1609
|
key,
|
|
1443
|
-
SNICallback: createSNICallback(stateDir, cert, key)
|
|
1610
|
+
SNICallback: createSNICallback(stateDir, cert, key, tld)
|
|
1444
1611
|
};
|
|
1445
1612
|
}
|
|
1446
1613
|
}
|
|
1447
1614
|
if (isForeground) {
|
|
1448
1615
|
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
1449
|
-
startProxyServer(store, proxyPort, tlsOptions);
|
|
1616
|
+
startProxyServer(store, proxyPort, tld, tlsOptions);
|
|
1450
1617
|
return;
|
|
1451
1618
|
}
|
|
1452
1619
|
store.ensureDir();
|
|
@@ -1469,6 +1636,9 @@ ${chalk.bold("Usage:")}
|
|
|
1469
1636
|
daemonArgs.push("--https");
|
|
1470
1637
|
}
|
|
1471
1638
|
}
|
|
1639
|
+
if (tld !== DEFAULT_TLD) {
|
|
1640
|
+
daemonArgs.push("--tld", tld);
|
|
1641
|
+
}
|
|
1472
1642
|
const child = spawn(process.execPath, daemonArgs, {
|
|
1473
1643
|
detached: true,
|
|
1474
1644
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -1501,10 +1671,24 @@ async function handleRunMode(args) {
|
|
|
1501
1671
|
console.error(chalk.cyan(" portless run next dev"));
|
|
1502
1672
|
process.exit(1);
|
|
1503
1673
|
}
|
|
1504
|
-
|
|
1674
|
+
let baseName;
|
|
1675
|
+
let nameSource;
|
|
1676
|
+
if (parsed.name) {
|
|
1677
|
+
const sanitized = sanitizeForHostname(parsed.name);
|
|
1678
|
+
if (!sanitized) {
|
|
1679
|
+
console.error(chalk.red(`Error: --name value "${parsed.name}" produces an empty hostname.`));
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
}
|
|
1682
|
+
baseName = sanitized;
|
|
1683
|
+
nameSource = "--name flag";
|
|
1684
|
+
} else {
|
|
1685
|
+
const inferred = inferProjectName();
|
|
1686
|
+
baseName = inferred.name;
|
|
1687
|
+
nameSource = inferred.source;
|
|
1688
|
+
}
|
|
1505
1689
|
const worktree = detectWorktreePrefix();
|
|
1506
|
-
const effectiveName = worktree ? `${worktree.prefix}.${
|
|
1507
|
-
const { dir, port, tls: tls2 } = await discoverState();
|
|
1690
|
+
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
1691
|
+
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
1508
1692
|
const store = new RouteStore(dir, {
|
|
1509
1693
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1510
1694
|
});
|
|
@@ -1515,8 +1699,9 @@ async function handleRunMode(args) {
|
|
|
1515
1699
|
effectiveName,
|
|
1516
1700
|
parsed.commandArgs,
|
|
1517
1701
|
tls2,
|
|
1702
|
+
tld,
|
|
1518
1703
|
parsed.force,
|
|
1519
|
-
{ nameSource
|
|
1704
|
+
{ nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
1520
1705
|
parsed.appPort
|
|
1521
1706
|
);
|
|
1522
1707
|
}
|
|
@@ -1530,7 +1715,7 @@ async function handleNamedMode(args) {
|
|
|
1530
1715
|
console.error(chalk.cyan(" portless myapp next dev"));
|
|
1531
1716
|
process.exit(1);
|
|
1532
1717
|
}
|
|
1533
|
-
const { dir, port, tls: tls2 } = await discoverState();
|
|
1718
|
+
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
1534
1719
|
const store = new RouteStore(dir, {
|
|
1535
1720
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1536
1721
|
});
|
|
@@ -1541,6 +1726,7 @@ async function handleNamedMode(args) {
|
|
|
1541
1726
|
parsed.name,
|
|
1542
1727
|
parsed.commandArgs,
|
|
1543
1728
|
tls2,
|
|
1729
|
+
tld,
|
|
1544
1730
|
parsed.force,
|
|
1545
1731
|
void 0,
|
|
1546
1732
|
parsed.appPort
|
|
@@ -1571,7 +1757,7 @@ async function main() {
|
|
|
1571
1757
|
console.error(chalk.cyan(" portless --name <name> <command...>"));
|
|
1572
1758
|
process.exit(1);
|
|
1573
1759
|
}
|
|
1574
|
-
const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
1760
|
+
const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
1575
1761
|
if (skipPortless2) {
|
|
1576
1762
|
const { commandArgs } = parseAppArgs(args);
|
|
1577
1763
|
if (commandArgs.length === 0) {
|
|
@@ -1588,7 +1774,7 @@ async function main() {
|
|
|
1588
1774
|
if (isRunCommand) {
|
|
1589
1775
|
args.shift();
|
|
1590
1776
|
}
|
|
1591
|
-
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
1777
|
+
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
1592
1778
|
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
|
|
1593
1779
|
const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
1594
1780
|
if (commandArgs.length === 0) {
|
|
@@ -1615,6 +1801,10 @@ async function main() {
|
|
|
1615
1801
|
await handleList();
|
|
1616
1802
|
return;
|
|
1617
1803
|
}
|
|
1804
|
+
if (args[0] === "get") {
|
|
1805
|
+
await handleGet(args);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1618
1808
|
if (args[0] === "alias") {
|
|
1619
1809
|
await handleAlias(args);
|
|
1620
1810
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ interface ProxyServerOptions {
|
|
|
12
12
|
getRoutes: () => RouteInfo[];
|
|
13
13
|
/** The port the proxy is listening on (used to build correct URLs). */
|
|
14
14
|
proxyPort: number;
|
|
15
|
+
/** TLD suffix used for hostnames (default: "localhost"). */
|
|
16
|
+
tld?: string;
|
|
15
17
|
/** Optional error logger; defaults to console.error. */
|
|
16
18
|
onError?: (message: string) => void;
|
|
17
19
|
/** When provided, enables HTTP/2 over TLS (HTTPS). */
|
|
@@ -109,15 +111,16 @@ declare function isErrnoException(err: unknown): err is NodeJS.ErrnoException;
|
|
|
109
111
|
*/
|
|
110
112
|
declare function escapeHtml(str: string): string;
|
|
111
113
|
/**
|
|
112
|
-
* Format a
|
|
113
|
-
* (80 for HTTP, 443 for HTTPS).
|
|
114
|
+
* Format a URL for the given hostname. Omits the port when it matches the
|
|
115
|
+
* protocol default (80 for HTTP, 443 for HTTPS).
|
|
114
116
|
*/
|
|
115
117
|
declare function formatUrl(hostname: string, proxyPort: number, tls?: boolean): string;
|
|
116
118
|
/**
|
|
117
|
-
* Parse and normalize a hostname input for use as a
|
|
118
|
-
* Strips protocol prefixes, validates characters, and
|
|
119
|
+
* Parse and normalize a hostname input for use as a subdomain of the
|
|
120
|
+
* configured TLD. Strips protocol prefixes, validates characters, and
|
|
121
|
+
* appends the TLD suffix if needed.
|
|
119
122
|
*/
|
|
120
|
-
declare function parseHostname(input: string): string;
|
|
123
|
+
declare function parseHostname(input: string, tld?: string): string;
|
|
121
124
|
|
|
122
125
|
/**
|
|
123
126
|
* Extract the portless-managed block from /etc/hosts content.
|
|
@@ -150,9 +153,9 @@ declare function cleanHostsFile(): boolean;
|
|
|
150
153
|
*/
|
|
151
154
|
declare function getManagedHostnames(): string[];
|
|
152
155
|
/**
|
|
153
|
-
* Check whether a
|
|
154
|
-
*
|
|
156
|
+
* Check whether a hostname resolves to 127.0.0.1 via the system DNS resolver.
|
|
157
|
+
* Returns true if resolution works, false otherwise.
|
|
155
158
|
*/
|
|
156
|
-
declare function
|
|
159
|
+
declare function checkHostResolution(hostname: string): Promise<boolean>;
|
|
157
160
|
|
|
158
|
-
export { DIR_MODE, FILE_MODE, PORTLESS_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, buildBlock,
|
|
161
|
+
export { DIR_MODE, FILE_MODE, PORTLESS_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, buildBlock, checkHostResolution, cleanHostsFile, createProxyServer, escapeHtml, extractManagedBlock, fixOwnership, formatUrl, getManagedHostnames, isErrnoException, parseHostname, removeBlock, syncHostsFile };
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
SYSTEM_DIR_MODE,
|
|
8
8
|
SYSTEM_FILE_MODE,
|
|
9
9
|
buildBlock,
|
|
10
|
-
|
|
10
|
+
checkHostResolution,
|
|
11
11
|
cleanHostsFile,
|
|
12
12
|
createProxyServer,
|
|
13
13
|
escapeHtml,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
parseHostname,
|
|
20
20
|
removeBlock,
|
|
21
21
|
syncHostsFile
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-AB3HUERH.js";
|
|
23
23
|
export {
|
|
24
24
|
DIR_MODE,
|
|
25
25
|
FILE_MODE,
|
|
@@ -29,7 +29,7 @@ export {
|
|
|
29
29
|
SYSTEM_DIR_MODE,
|
|
30
30
|
SYSTEM_FILE_MODE,
|
|
31
31
|
buildBlock,
|
|
32
|
-
|
|
32
|
+
checkHostResolution,
|
|
33
33
|
cleanHostsFile,
|
|
34
34
|
createProxyServer,
|
|
35
35
|
escapeHtml,
|