portless 0.2.2 → 0.3.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 +45 -30
- package/dist/{chunk-SE7KL62V.js → chunk-Y5OVKUR4.js} +122 -30
- package/dist/cli.js +333 -161
- package/dist/index.d.ts +17 -3
- package/dist/index.js +9 -1
- package/package.json +2 -4
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Replace port numbers with stable, named .localhost URLs. For humans and agents.
|
|
|
4
4
|
|
|
5
5
|
```diff
|
|
6
6
|
- "dev": "next dev" # http://localhost:3000
|
|
7
|
-
+ "dev": "portless myapp next dev" # http://myapp.localhost
|
|
7
|
+
+ "dev": "portless myapp next dev" # http://myapp.localhost:1355
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## Quick Start
|
|
@@ -13,29 +13,29 @@ Replace port numbers with stable, named .localhost URLs. For humans and agents.
|
|
|
13
13
|
# Install
|
|
14
14
|
npm install -g portless
|
|
15
15
|
|
|
16
|
-
# Start the proxy (once,
|
|
17
|
-
|
|
16
|
+
# Start the proxy (once, no sudo needed)
|
|
17
|
+
portless proxy start
|
|
18
18
|
|
|
19
|
-
# Run your app
|
|
19
|
+
# Run your app (auto-starts the proxy if needed)
|
|
20
20
|
portless myapp next dev
|
|
21
|
-
#
|
|
21
|
+
# -> http://myapp.localhost:1355
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
>
|
|
24
|
+
> The proxy auto-starts when you run an app. You can also start it explicitly with `portless proxy start`.
|
|
25
25
|
|
|
26
26
|
## Why
|
|
27
27
|
|
|
28
28
|
Local dev with port numbers is fragile:
|
|
29
29
|
|
|
30
|
-
- **Port conflicts**
|
|
31
|
-
- **Memorizing ports**
|
|
32
|
-
- **Refreshing shows the wrong app**
|
|
33
|
-
- **Monorepo multiplier**
|
|
34
|
-
- **Agents test the wrong port**
|
|
35
|
-
- **Cookie and storage clashes**
|
|
36
|
-
- **Hardcoded ports in config**
|
|
37
|
-
- **Sharing URLs with teammates**
|
|
38
|
-
- **Browser history is useless**
|
|
30
|
+
- **Port conflicts** -- two projects default to the same port and you get `EADDRINUSE`
|
|
31
|
+
- **Memorizing ports** -- was the API on 3001 or 8080?
|
|
32
|
+
- **Refreshing shows the wrong app** -- stop one server, start another on the same port, and your open tab now shows something completely different
|
|
33
|
+
- **Monorepo multiplier** -- every problem above scales with each service in the repo
|
|
34
|
+
- **Agents test the wrong port** -- AI coding agents guess or hardcode the wrong port, especially in monorepos
|
|
35
|
+
- **Cookie and storage clashes** -- cookies set on `localhost` bleed across apps on different ports; localStorage is lost when ports shift
|
|
36
|
+
- **Hardcoded ports in config** -- CORS allowlists, OAuth redirect URIs, and `.env` files all break when ports change
|
|
37
|
+
- **Sharing URLs with teammates** -- "what port is that on?" becomes a Slack question
|
|
38
|
+
- **Browser history is useless** -- your history for `localhost:3000` is a jumble of unrelated projects
|
|
39
39
|
|
|
40
40
|
Portless fixes all of this by giving each dev server a stable, named `.localhost` URL that both humans and agents can rely on.
|
|
41
41
|
|
|
@@ -44,14 +44,14 @@ Portless fixes all of this by giving each dev server a stable, named `.localhost
|
|
|
44
44
|
```bash
|
|
45
45
|
# Basic
|
|
46
46
|
portless myapp next dev
|
|
47
|
-
#
|
|
47
|
+
# -> http://myapp.localhost:1355
|
|
48
48
|
|
|
49
49
|
# Subdomains
|
|
50
50
|
portless api.myapp pnpm start
|
|
51
|
-
#
|
|
51
|
+
# -> http://api.myapp.localhost:1355
|
|
52
52
|
|
|
53
53
|
portless docs.myapp next dev
|
|
54
|
-
#
|
|
54
|
+
# -> http://docs.myapp.localhost:1355
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
### In package.json
|
|
@@ -64,32 +64,32 @@ portless docs.myapp next dev
|
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
The proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`.
|
|
68
68
|
|
|
69
69
|
## How It Works
|
|
70
70
|
|
|
71
71
|
```mermaid
|
|
72
72
|
flowchart TD
|
|
73
|
-
Browser["Browser\nmyapp.localhost"]
|
|
74
|
-
Proxy["portless proxy\n(port
|
|
73
|
+
Browser["Browser\nmyapp.localhost:1355"]
|
|
74
|
+
Proxy["portless proxy\n(port 1355)"]
|
|
75
75
|
App1[":4123\nmyapp"]
|
|
76
76
|
App2[":4567\napi"]
|
|
77
77
|
|
|
78
|
-
Browser -->|port
|
|
78
|
+
Browser -->|port 1355| Proxy
|
|
79
79
|
Proxy --> App1
|
|
80
80
|
Proxy --> App2
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
1. **Start the proxy** --
|
|
83
|
+
1. **Start the proxy** -- auto-starts when you run an app, or start explicitly with `portless proxy start`
|
|
84
84
|
2. **Run apps** -- `portless <name> <command>` assigns a free port and registers with the proxy
|
|
85
|
-
3. **Access via URL** -- `http://<name>.localhost` routes through the proxy to your app
|
|
85
|
+
3. **Access via URL** -- `http://<name>.localhost:1355` routes through the proxy to your app
|
|
86
86
|
|
|
87
87
|
Apps are assigned a random port (4000-4999) via the `PORT` environment variable. Most frameworks (Next.js, Vite, etc.) respect this automatically.
|
|
88
88
|
|
|
89
89
|
## Commands
|
|
90
90
|
|
|
91
91
|
```bash
|
|
92
|
-
portless <name> <cmd> [args...] # Run app at http://<name>.localhost
|
|
92
|
+
portless <name> <cmd> [args...] # Run app at http://<name>.localhost:1355
|
|
93
93
|
portless list # Show active routes
|
|
94
94
|
|
|
95
95
|
# Disable portless (run command directly)
|
|
@@ -97,19 +97,34 @@ PORTLESS=0 pnpm dev # Bypasses proxy, uses default port
|
|
|
97
97
|
# Also accepts PORTLESS=skip
|
|
98
98
|
|
|
99
99
|
# Proxy control
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
portless proxy start # Start the proxy (port 1355, daemon)
|
|
101
|
+
portless proxy start -p 80 # Start on port 80 (requires sudo)
|
|
102
|
+
portless proxy start --foreground # Start in foreground (for debugging)
|
|
103
|
+
portless proxy stop # Stop the proxy
|
|
103
104
|
|
|
104
105
|
# Options
|
|
105
|
-
--port <number>
|
|
106
|
-
# Ports
|
|
106
|
+
-p, --port <number> # Port for the proxy (default: 1355)
|
|
107
|
+
# Ports < 1024 require sudo
|
|
108
|
+
--foreground # Run proxy in foreground instead of daemon
|
|
109
|
+
|
|
110
|
+
# Environment variables
|
|
111
|
+
PORTLESS_PORT=<number> # Override the default proxy port
|
|
112
|
+
PORTLESS_STATE_DIR=<path> # Override the state directory
|
|
107
113
|
|
|
108
114
|
# Info
|
|
109
115
|
portless --help # Show help
|
|
110
116
|
portless --version # Show version
|
|
111
117
|
```
|
|
112
118
|
|
|
119
|
+
## State Directory
|
|
120
|
+
|
|
121
|
+
Portless stores its state (routes, PID file, port file) in a directory that depends on the proxy port:
|
|
122
|
+
|
|
123
|
+
- **Port < 1024** (sudo required): `/tmp/portless` -- shared between root and user processes
|
|
124
|
+
- **Port >= 1024** (no sudo): `~/.portless` -- user-scoped, no root involvement
|
|
125
|
+
|
|
126
|
+
Override with the `PORTLESS_STATE_DIR` environment variable if needed.
|
|
127
|
+
|
|
113
128
|
## Requirements
|
|
114
129
|
|
|
115
130
|
- Node.js 20+
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// src/utils.ts
|
|
2
2
|
function isErrnoException(err) {
|
|
3
|
-
return err instanceof Error && "code" in err;
|
|
3
|
+
return err instanceof Error && "code" in err && typeof err.code === "string";
|
|
4
4
|
}
|
|
5
5
|
function escapeHtml(str) {
|
|
6
6
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7
7
|
}
|
|
8
|
+
function formatUrl(hostname, proxyPort) {
|
|
9
|
+
return proxyPort === 80 ? `http://${hostname}` : `http://${hostname}:${proxyPort}`;
|
|
10
|
+
}
|
|
8
11
|
function parseHostname(input) {
|
|
9
12
|
let hostname = input.trim().replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
|
|
10
13
|
if (!hostname || hostname === ".localhost") {
|
|
@@ -27,23 +30,22 @@ function parseHostname(input) {
|
|
|
27
30
|
|
|
28
31
|
// src/proxy.ts
|
|
29
32
|
import * as http from "http";
|
|
30
|
-
|
|
33
|
+
var PORTLESS_HEADER = "X-Portless";
|
|
34
|
+
function buildForwardedHeaders(req) {
|
|
35
|
+
const headers = {};
|
|
36
|
+
const remoteAddress = req.socket.remoteAddress || "127.0.0.1";
|
|
37
|
+
const proto = "http";
|
|
38
|
+
const hostHeader = req.headers.host || "";
|
|
39
|
+
headers["x-forwarded-for"] = req.headers["x-forwarded-for"] ? `${req.headers["x-forwarded-for"]}, ${remoteAddress}` : remoteAddress;
|
|
40
|
+
headers["x-forwarded-proto"] = req.headers["x-forwarded-proto"] || proto;
|
|
41
|
+
headers["x-forwarded-host"] = req.headers["x-forwarded-host"] || hostHeader;
|
|
42
|
+
headers["x-forwarded-port"] = req.headers["x-forwarded-port"] || hostHeader.split(":")[1] || "80";
|
|
43
|
+
return headers;
|
|
44
|
+
}
|
|
31
45
|
function createProxyServer(options) {
|
|
32
|
-
const { getRoutes, onError = (msg) => console.error(msg) } = options;
|
|
33
|
-
const proxy = httpProxy.createProxyServer({});
|
|
34
|
-
proxy.on("error", (err, req, res) => {
|
|
35
|
-
onError(`Proxy error for ${req.headers.host}: ${err.message}`);
|
|
36
|
-
if (res && "writeHead" in res) {
|
|
37
|
-
const serverRes = res;
|
|
38
|
-
if (!serverRes.headersSent) {
|
|
39
|
-
const errWithCode = err;
|
|
40
|
-
const message = errWithCode.code === "ECONNREFUSED" ? "Bad Gateway: the target app is not responding. It may have crashed." : "Bad Gateway: the target app may not be running.";
|
|
41
|
-
serverRes.writeHead(502, { "Content-Type": "text/plain" });
|
|
42
|
-
serverRes.end(message);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
+
const { getRoutes, proxyPort, onError = (msg) => console.error(msg) } = options;
|
|
46
47
|
const handleRequest = (req, res) => {
|
|
48
|
+
res.setHeader(PORTLESS_HEADER, "1");
|
|
47
49
|
const routes = getRoutes();
|
|
48
50
|
const host = (req.headers.host || "").split(":")[0];
|
|
49
51
|
if (!host) {
|
|
@@ -64,7 +66,7 @@ function createProxyServer(options) {
|
|
|
64
66
|
${routes.length > 0 ? `
|
|
65
67
|
<h2>Active apps:</h2>
|
|
66
68
|
<ul>
|
|
67
|
-
${routes.map((r) => `<li><a href="
|
|
69
|
+
${routes.map((r) => `<li><a href="${escapeHtml(formatUrl(r.hostname, proxyPort))}">${escapeHtml(r.hostname)}</a> - localhost:${escapeHtml(String(r.port))}</li>`).join("")}
|
|
68
70
|
</ul>
|
|
69
71
|
` : "<p><em>No apps running.</em></p>"}
|
|
70
72
|
<p>Start an app with: <code>portless ${safeHost.replace(".localhost", "")} your-command</code></p>
|
|
@@ -73,10 +75,44 @@ function createProxyServer(options) {
|
|
|
73
75
|
`);
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
const forwardedHeaders = buildForwardedHeaders(req);
|
|
79
|
+
const proxyReqHeaders = { ...req.headers };
|
|
80
|
+
for (const [key, value] of Object.entries(forwardedHeaders)) {
|
|
81
|
+
proxyReqHeaders[key] = value;
|
|
82
|
+
}
|
|
83
|
+
const proxyReq = http.request(
|
|
84
|
+
{
|
|
85
|
+
hostname: "127.0.0.1",
|
|
86
|
+
port: route.port,
|
|
87
|
+
path: req.url,
|
|
88
|
+
method: req.method,
|
|
89
|
+
headers: proxyReqHeaders
|
|
90
|
+
},
|
|
91
|
+
(proxyRes) => {
|
|
92
|
+
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
93
|
+
proxyRes.pipe(res);
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
proxyReq.on("error", (err) => {
|
|
97
|
+
onError(`Proxy error for ${req.headers.host}: ${err.message}`);
|
|
98
|
+
if (!res.headersSent) {
|
|
99
|
+
const errWithCode = err;
|
|
100
|
+
const message = errWithCode.code === "ECONNREFUSED" ? "Bad Gateway: the target app is not responding. It may have crashed." : "Bad Gateway: the target app may not be running.";
|
|
101
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
102
|
+
res.end(message);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
res.on("close", () => {
|
|
106
|
+
if (!proxyReq.destroyed) {
|
|
107
|
+
proxyReq.destroy();
|
|
108
|
+
}
|
|
79
109
|
});
|
|
110
|
+
req.on("error", () => {
|
|
111
|
+
if (!proxyReq.destroyed) {
|
|
112
|
+
proxyReq.destroy();
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
req.pipe(proxyReq);
|
|
80
116
|
};
|
|
81
117
|
const handleUpgrade = (req, socket, head) => {
|
|
82
118
|
const routes = getRoutes();
|
|
@@ -86,10 +122,56 @@ function createProxyServer(options) {
|
|
|
86
122
|
socket.destroy();
|
|
87
123
|
return;
|
|
88
124
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
125
|
+
const forwardedHeaders = buildForwardedHeaders(req);
|
|
126
|
+
const proxyReqHeaders = { ...req.headers };
|
|
127
|
+
for (const [key, value] of Object.entries(forwardedHeaders)) {
|
|
128
|
+
proxyReqHeaders[key] = value;
|
|
129
|
+
}
|
|
130
|
+
const proxyReq = http.request({
|
|
131
|
+
hostname: "127.0.0.1",
|
|
132
|
+
port: route.port,
|
|
133
|
+
path: req.url,
|
|
134
|
+
method: req.method,
|
|
135
|
+
headers: proxyReqHeaders
|
|
92
136
|
});
|
|
137
|
+
proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
|
|
138
|
+
let response = `HTTP/1.1 101 Switching Protocols\r
|
|
139
|
+
`;
|
|
140
|
+
for (let i = 0; i < proxyRes.rawHeaders.length; i += 2) {
|
|
141
|
+
response += `${proxyRes.rawHeaders[i]}: ${proxyRes.rawHeaders[i + 1]}\r
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
response += "\r\n";
|
|
145
|
+
socket.write(response);
|
|
146
|
+
if (proxyHead.length > 0) {
|
|
147
|
+
socket.write(proxyHead);
|
|
148
|
+
}
|
|
149
|
+
proxySocket.pipe(socket);
|
|
150
|
+
socket.pipe(proxySocket);
|
|
151
|
+
proxySocket.on("error", () => socket.destroy());
|
|
152
|
+
socket.on("error", () => proxySocket.destroy());
|
|
153
|
+
});
|
|
154
|
+
proxyReq.on("error", (err) => {
|
|
155
|
+
onError(`WebSocket proxy error for ${req.headers.host}: ${err.message}`);
|
|
156
|
+
socket.destroy();
|
|
157
|
+
});
|
|
158
|
+
proxyReq.on("response", (res) => {
|
|
159
|
+
if (!socket.destroyed) {
|
|
160
|
+
let response = `HTTP/1.1 ${res.statusCode} ${res.statusMessage}\r
|
|
161
|
+
`;
|
|
162
|
+
for (let i = 0; i < res.rawHeaders.length; i += 2) {
|
|
163
|
+
response += `${res.rawHeaders[i]}: ${res.rawHeaders[i + 1]}\r
|
|
164
|
+
`;
|
|
165
|
+
}
|
|
166
|
+
response += "\r\n";
|
|
167
|
+
socket.write(response);
|
|
168
|
+
res.pipe(socket);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (head.length > 0) {
|
|
172
|
+
proxyReq.write(head);
|
|
173
|
+
}
|
|
174
|
+
proxyReq.end();
|
|
93
175
|
};
|
|
94
176
|
const httpServer = http.createServer(handleRequest);
|
|
95
177
|
httpServer.on("upgrade", handleUpgrade);
|
|
@@ -99,6 +181,11 @@ function createProxyServer(options) {
|
|
|
99
181
|
// src/routes.ts
|
|
100
182
|
import * as fs from "fs";
|
|
101
183
|
import * as path from "path";
|
|
184
|
+
var STALE_LOCK_THRESHOLD_MS = 1e4;
|
|
185
|
+
var LOCK_MAX_RETRIES = 20;
|
|
186
|
+
var LOCK_RETRY_DELAY_MS = 50;
|
|
187
|
+
var FILE_MODE = 420;
|
|
188
|
+
var DIR_MODE = 493;
|
|
102
189
|
function isValidRoute(value) {
|
|
103
190
|
return typeof value === "object" && value !== null && typeof value.hostname === "string" && typeof value.port === "number" && typeof value.pid === "number";
|
|
104
191
|
}
|
|
@@ -107,20 +194,22 @@ var RouteStore = class _RouteStore {
|
|
|
107
194
|
routesPath;
|
|
108
195
|
lockPath;
|
|
109
196
|
pidPath;
|
|
197
|
+
portFilePath;
|
|
110
198
|
onWarning;
|
|
111
199
|
constructor(dir, options) {
|
|
112
200
|
this.dir = dir;
|
|
113
201
|
this.routesPath = path.join(dir, "routes.json");
|
|
114
202
|
this.lockPath = path.join(dir, "routes.lock");
|
|
115
203
|
this.pidPath = path.join(dir, "proxy.pid");
|
|
204
|
+
this.portFilePath = path.join(dir, "proxy.port");
|
|
116
205
|
this.onWarning = options?.onWarning;
|
|
117
206
|
}
|
|
118
207
|
ensureDir() {
|
|
119
208
|
if (!fs.existsSync(this.dir)) {
|
|
120
|
-
fs.mkdirSync(this.dir, { recursive: true, mode:
|
|
209
|
+
fs.mkdirSync(this.dir, { recursive: true, mode: DIR_MODE });
|
|
121
210
|
}
|
|
122
211
|
try {
|
|
123
|
-
fs.chmodSync(this.dir,
|
|
212
|
+
fs.chmodSync(this.dir, DIR_MODE);
|
|
124
213
|
} catch {
|
|
125
214
|
}
|
|
126
215
|
}
|
|
@@ -132,7 +221,7 @@ var RouteStore = class _RouteStore {
|
|
|
132
221
|
syncSleep(ms) {
|
|
133
222
|
Atomics.wait(_RouteStore.sleepBuffer, 0, 0, ms);
|
|
134
223
|
}
|
|
135
|
-
acquireLock(maxRetries =
|
|
224
|
+
acquireLock(maxRetries = LOCK_MAX_RETRIES, retryDelayMs = LOCK_RETRY_DELAY_MS) {
|
|
136
225
|
for (let i = 0; i < maxRetries; i++) {
|
|
137
226
|
try {
|
|
138
227
|
fs.mkdirSync(this.lockPath);
|
|
@@ -141,7 +230,7 @@ var RouteStore = class _RouteStore {
|
|
|
141
230
|
if (isErrnoException(err) && err.code === "EEXIST") {
|
|
142
231
|
try {
|
|
143
232
|
const stat = fs.statSync(this.lockPath);
|
|
144
|
-
if (Date.now() - stat.mtimeMs >
|
|
233
|
+
if (Date.now() - stat.mtimeMs > STALE_LOCK_THRESHOLD_MS) {
|
|
145
234
|
fs.rmSync(this.lockPath, { recursive: true });
|
|
146
235
|
continue;
|
|
147
236
|
}
|
|
@@ -198,7 +287,7 @@ var RouteStore = class _RouteStore {
|
|
|
198
287
|
const alive = routes.filter((r) => this.isProcessAlive(r.pid));
|
|
199
288
|
if (persistCleanup && alive.length !== routes.length) {
|
|
200
289
|
try {
|
|
201
|
-
fs.writeFileSync(this.routesPath, JSON.stringify(alive, null, 2), { mode:
|
|
290
|
+
fs.writeFileSync(this.routesPath, JSON.stringify(alive, null, 2), { mode: FILE_MODE });
|
|
202
291
|
} catch {
|
|
203
292
|
}
|
|
204
293
|
}
|
|
@@ -208,8 +297,7 @@ var RouteStore = class _RouteStore {
|
|
|
208
297
|
}
|
|
209
298
|
}
|
|
210
299
|
saveRoutes(routes) {
|
|
211
|
-
this.
|
|
212
|
-
fs.writeFileSync(this.routesPath, JSON.stringify(routes, null, 2), { mode: 420 });
|
|
300
|
+
fs.writeFileSync(this.routesPath, JSON.stringify(routes, null, 2), { mode: FILE_MODE });
|
|
213
301
|
}
|
|
214
302
|
addRoute(hostname, port, pid) {
|
|
215
303
|
this.ensureDir();
|
|
@@ -241,7 +329,11 @@ var RouteStore = class _RouteStore {
|
|
|
241
329
|
export {
|
|
242
330
|
isErrnoException,
|
|
243
331
|
escapeHtml,
|
|
332
|
+
formatUrl,
|
|
244
333
|
parseHostname,
|
|
334
|
+
PORTLESS_HEADER,
|
|
245
335
|
createProxyServer,
|
|
336
|
+
FILE_MODE,
|
|
337
|
+
DIR_MODE,
|
|
246
338
|
RouteStore
|
|
247
339
|
};
|
package/dist/cli.js
CHANGED
|
@@ -1,21 +1,89 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
FILE_MODE,
|
|
4
|
+
PORTLESS_HEADER,
|
|
3
5
|
RouteStore,
|
|
4
6
|
createProxyServer,
|
|
7
|
+
formatUrl,
|
|
5
8
|
isErrnoException,
|
|
6
9
|
parseHostname
|
|
7
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-Y5OVKUR4.js";
|
|
8
11
|
|
|
9
12
|
// src/cli.ts
|
|
10
13
|
import chalk from "chalk";
|
|
11
|
-
import * as
|
|
12
|
-
import * as
|
|
13
|
-
import
|
|
14
|
-
import { execSync, spawn, spawnSync } from "child_process";
|
|
14
|
+
import * as fs2 from "fs";
|
|
15
|
+
import * as path2 from "path";
|
|
16
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
15
17
|
|
|
16
18
|
// src/cli-utils.ts
|
|
19
|
+
import * as fs from "fs";
|
|
20
|
+
import * as http from "http";
|
|
17
21
|
import * as net from "net";
|
|
18
|
-
|
|
22
|
+
import * as os from "os";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
import * as readline from "readline";
|
|
25
|
+
import { execSync, spawn } from "child_process";
|
|
26
|
+
var DEFAULT_PROXY_PORT = 1355;
|
|
27
|
+
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
28
|
+
var SYSTEM_STATE_DIR = "/tmp/portless";
|
|
29
|
+
var USER_STATE_DIR = path.join(os.homedir(), ".portless");
|
|
30
|
+
var MIN_APP_PORT = 4e3;
|
|
31
|
+
var MAX_APP_PORT = 4999;
|
|
32
|
+
var RANDOM_PORT_ATTEMPTS = 50;
|
|
33
|
+
var SOCKET_TIMEOUT_MS = 500;
|
|
34
|
+
var LSOF_TIMEOUT_MS = 5e3;
|
|
35
|
+
var WAIT_FOR_PROXY_MAX_ATTEMPTS = 20;
|
|
36
|
+
var WAIT_FOR_PROXY_INTERVAL_MS = 250;
|
|
37
|
+
var SIGNAL_CODES = {
|
|
38
|
+
SIGHUP: 1,
|
|
39
|
+
SIGINT: 2,
|
|
40
|
+
SIGQUIT: 3,
|
|
41
|
+
SIGABRT: 6,
|
|
42
|
+
SIGKILL: 9,
|
|
43
|
+
SIGTERM: 15
|
|
44
|
+
};
|
|
45
|
+
function getDefaultPort() {
|
|
46
|
+
const envPort = process.env.PORTLESS_PORT;
|
|
47
|
+
if (envPort) {
|
|
48
|
+
const port = parseInt(envPort, 10);
|
|
49
|
+
if (!isNaN(port) && port >= 1 && port <= 65535) return port;
|
|
50
|
+
}
|
|
51
|
+
return DEFAULT_PROXY_PORT;
|
|
52
|
+
}
|
|
53
|
+
function resolveStateDir(port) {
|
|
54
|
+
if (process.env.PORTLESS_STATE_DIR) return process.env.PORTLESS_STATE_DIR;
|
|
55
|
+
return port < PRIVILEGED_PORT_THRESHOLD ? SYSTEM_STATE_DIR : USER_STATE_DIR;
|
|
56
|
+
}
|
|
57
|
+
function readPortFromDir(dir) {
|
|
58
|
+
try {
|
|
59
|
+
const raw = fs.readFileSync(path.join(dir, "proxy.port"), "utf-8").trim();
|
|
60
|
+
const port = parseInt(raw, 10);
|
|
61
|
+
return isNaN(port) ? null : port;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function discoverState() {
|
|
67
|
+
if (process.env.PORTLESS_STATE_DIR) {
|
|
68
|
+
const dir = process.env.PORTLESS_STATE_DIR;
|
|
69
|
+
const port = readPortFromDir(dir) ?? getDefaultPort();
|
|
70
|
+
return { dir, port };
|
|
71
|
+
}
|
|
72
|
+
const userPort = readPortFromDir(USER_STATE_DIR);
|
|
73
|
+
if (userPort !== null && await isProxyRunning(userPort)) {
|
|
74
|
+
return { dir: USER_STATE_DIR, port: userPort };
|
|
75
|
+
}
|
|
76
|
+
const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
|
|
77
|
+
if (systemPort !== null && await isProxyRunning(systemPort)) {
|
|
78
|
+
return { dir: SYSTEM_STATE_DIR, port: systemPort };
|
|
79
|
+
}
|
|
80
|
+
const defaultPort = getDefaultPort();
|
|
81
|
+
return { dir: resolveStateDir(defaultPort), port: defaultPort };
|
|
82
|
+
}
|
|
83
|
+
async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
|
|
84
|
+
if (minPort > maxPort) {
|
|
85
|
+
throw new Error(`minPort (${minPort}) must be <= maxPort (${maxPort})`);
|
|
86
|
+
}
|
|
19
87
|
const tryPort = (port) => {
|
|
20
88
|
return new Promise((resolve) => {
|
|
21
89
|
const server = net.createServer();
|
|
@@ -25,7 +93,7 @@ async function findFreePort(minPort = 4e3, maxPort = 4999) {
|
|
|
25
93
|
server.on("error", () => resolve(false));
|
|
26
94
|
});
|
|
27
95
|
};
|
|
28
|
-
for (let i = 0; i <
|
|
96
|
+
for (let i = 0; i < RANDOM_PORT_ATTEMPTS; i++) {
|
|
29
97
|
const port = minPort + Math.floor(Math.random() * (maxPort - minPort + 1));
|
|
30
98
|
if (await tryPort(port)) {
|
|
31
99
|
return port;
|
|
@@ -38,58 +106,34 @@ async function findFreePort(minPort = 4e3, maxPort = 4999) {
|
|
|
38
106
|
}
|
|
39
107
|
throw new Error(`No free port found in range ${minPort}-${maxPort}`);
|
|
40
108
|
}
|
|
41
|
-
function isProxyRunning(port
|
|
109
|
+
function isProxyRunning(port) {
|
|
42
110
|
return new Promise((resolve) => {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
111
|
+
const req = http.request(
|
|
112
|
+
{
|
|
113
|
+
hostname: "127.0.0.1",
|
|
114
|
+
port,
|
|
115
|
+
path: "/",
|
|
116
|
+
method: "HEAD",
|
|
117
|
+
timeout: SOCKET_TIMEOUT_MS
|
|
118
|
+
},
|
|
119
|
+
(res) => {
|
|
120
|
+
res.resume();
|
|
121
|
+
resolve(res.headers[PORTLESS_HEADER.toLowerCase()] === "1");
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
req.on("error", () => resolve(false));
|
|
125
|
+
req.on("timeout", () => {
|
|
126
|
+
req.destroy();
|
|
52
127
|
resolve(false);
|
|
53
128
|
});
|
|
54
|
-
|
|
129
|
+
req.end();
|
|
55
130
|
});
|
|
56
131
|
}
|
|
57
|
-
|
|
58
|
-
// src/cli.ts
|
|
59
|
-
var SIGNAL_CODES = { SIGINT: 2, SIGTERM: 15 };
|
|
60
|
-
function prompt(question) {
|
|
61
|
-
const rl = readline.createInterface({
|
|
62
|
-
input: process.stdin,
|
|
63
|
-
output: process.stdout
|
|
64
|
-
});
|
|
65
|
-
return new Promise((resolve) => {
|
|
66
|
-
rl.on("close", () => resolve(""));
|
|
67
|
-
rl.question(question, (answer) => {
|
|
68
|
-
rl.close();
|
|
69
|
-
resolve(answer.trim().toLowerCase());
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
var PORTLESS_DIR = "/tmp/portless";
|
|
74
|
-
var PROXY_PORT_PATH = path.join(PORTLESS_DIR, "proxy.port");
|
|
75
|
-
var DEFAULT_PROXY_PORT = 80;
|
|
76
|
-
var store = new RouteStore(PORTLESS_DIR, {
|
|
77
|
-
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
78
|
-
});
|
|
79
|
-
function readProxyPort() {
|
|
80
|
-
try {
|
|
81
|
-
const raw = fs.readFileSync(PROXY_PORT_PATH, "utf-8").trim();
|
|
82
|
-
const port = parseInt(raw, 10);
|
|
83
|
-
return isNaN(port) ? DEFAULT_PROXY_PORT : port;
|
|
84
|
-
} catch {
|
|
85
|
-
return DEFAULT_PROXY_PORT;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
132
|
function findPidOnPort(port) {
|
|
89
133
|
try {
|
|
90
134
|
const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
91
135
|
encoding: "utf-8",
|
|
92
|
-
timeout:
|
|
136
|
+
timeout: LSOF_TIMEOUT_MS
|
|
93
137
|
});
|
|
94
138
|
const pid = parseInt(output.trim().split("\n")[0], 10);
|
|
95
139
|
return isNaN(pid) ? null : pid;
|
|
@@ -97,8 +141,7 @@ function findPidOnPort(port) {
|
|
|
97
141
|
return null;
|
|
98
142
|
}
|
|
99
143
|
}
|
|
100
|
-
async function waitForProxy(
|
|
101
|
-
const port = proxyPort ?? readProxyPort();
|
|
144
|
+
async function waitForProxy(port, maxAttempts = WAIT_FOR_PROXY_MAX_ATTEMPTS, intervalMs = WAIT_FOR_PROXY_INTERVAL_MS) {
|
|
102
145
|
for (let i = 0; i < maxAttempts; i++) {
|
|
103
146
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
104
147
|
if (await isProxyRunning(port)) {
|
|
@@ -113,45 +156,75 @@ function spawnCommand(commandArgs, options) {
|
|
|
113
156
|
env: options?.env
|
|
114
157
|
});
|
|
115
158
|
let exiting = false;
|
|
159
|
+
const cleanup = () => {
|
|
160
|
+
process.removeListener("SIGINT", onSigInt);
|
|
161
|
+
process.removeListener("SIGTERM", onSigTerm);
|
|
162
|
+
options?.onCleanup?.();
|
|
163
|
+
};
|
|
116
164
|
const handleSignal = (signal) => {
|
|
117
165
|
if (exiting) return;
|
|
118
166
|
exiting = true;
|
|
119
167
|
child.kill(signal);
|
|
120
|
-
|
|
168
|
+
cleanup();
|
|
121
169
|
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
122
170
|
};
|
|
123
|
-
|
|
124
|
-
|
|
171
|
+
const onSigInt = () => handleSignal("SIGINT");
|
|
172
|
+
const onSigTerm = () => handleSignal("SIGTERM");
|
|
173
|
+
process.on("SIGINT", onSigInt);
|
|
174
|
+
process.on("SIGTERM", onSigTerm);
|
|
125
175
|
child.on("error", (err) => {
|
|
126
176
|
if (exiting) return;
|
|
127
177
|
exiting = true;
|
|
128
|
-
console.error(
|
|
129
|
-
|
|
178
|
+
console.error(`Failed to run command: ${err.message}`);
|
|
179
|
+
if (err.code === "ENOENT") {
|
|
180
|
+
console.error(`Is "${commandArgs[0]}" installed and in your PATH?`);
|
|
181
|
+
}
|
|
182
|
+
cleanup();
|
|
130
183
|
process.exit(1);
|
|
131
184
|
});
|
|
132
185
|
child.on("exit", (code, signal) => {
|
|
133
186
|
if (exiting) return;
|
|
134
187
|
exiting = true;
|
|
135
|
-
|
|
188
|
+
cleanup();
|
|
136
189
|
if (signal) {
|
|
137
|
-
process.exit(128 + (SIGNAL_CODES[signal] ||
|
|
190
|
+
process.exit(128 + (SIGNAL_CODES[signal] || 15));
|
|
138
191
|
}
|
|
139
192
|
process.exit(code ?? 1);
|
|
140
193
|
});
|
|
141
194
|
}
|
|
142
|
-
function
|
|
195
|
+
function prompt(question) {
|
|
196
|
+
const rl = readline.createInterface({
|
|
197
|
+
input: process.stdin,
|
|
198
|
+
output: process.stdout
|
|
199
|
+
});
|
|
200
|
+
return new Promise((resolve) => {
|
|
201
|
+
rl.on("close", () => resolve(""));
|
|
202
|
+
rl.question(question, (answer) => {
|
|
203
|
+
rl.close();
|
|
204
|
+
resolve(answer.trim().toLowerCase());
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/cli.ts
|
|
210
|
+
var DEBOUNCE_MS = 100;
|
|
211
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
212
|
+
var EXIT_TIMEOUT_MS = 2e3;
|
|
213
|
+
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
214
|
+
function startProxyServer(store, proxyPort) {
|
|
143
215
|
store.ensureDir();
|
|
144
216
|
const routesPath = store.getRoutesPath();
|
|
145
|
-
if (!
|
|
146
|
-
|
|
217
|
+
if (!fs2.existsSync(routesPath)) {
|
|
218
|
+
fs2.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
147
219
|
}
|
|
148
220
|
try {
|
|
149
|
-
|
|
221
|
+
fs2.chmodSync(routesPath, FILE_MODE);
|
|
150
222
|
} catch {
|
|
151
223
|
}
|
|
152
224
|
let cachedRoutes = store.loadRoutes();
|
|
153
225
|
let debounceTimer = null;
|
|
154
226
|
let watcher = null;
|
|
227
|
+
let pollingInterval = null;
|
|
155
228
|
const reloadRoutes = () => {
|
|
156
229
|
try {
|
|
157
230
|
cachedRoutes = store.loadRoutes();
|
|
@@ -159,60 +232,72 @@ function startProxyServer(proxyPort) {
|
|
|
159
232
|
}
|
|
160
233
|
};
|
|
161
234
|
try {
|
|
162
|
-
watcher =
|
|
235
|
+
watcher = fs2.watch(routesPath, () => {
|
|
163
236
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
164
|
-
debounceTimer = setTimeout(reloadRoutes,
|
|
237
|
+
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
165
238
|
});
|
|
166
239
|
} catch {
|
|
167
240
|
console.warn(chalk.yellow("fs.watch unavailable; falling back to polling for route changes"));
|
|
168
|
-
setInterval(reloadRoutes,
|
|
241
|
+
pollingInterval = setInterval(reloadRoutes, POLL_INTERVAL_MS);
|
|
169
242
|
}
|
|
170
243
|
const server = createProxyServer({
|
|
171
244
|
getRoutes: () => cachedRoutes,
|
|
245
|
+
proxyPort,
|
|
172
246
|
onError: (msg) => console.error(chalk.red(msg))
|
|
173
247
|
});
|
|
174
248
|
server.on("error", (err) => {
|
|
175
249
|
if (err.code === "EADDRINUSE") {
|
|
176
|
-
console.error(chalk.red(`Port ${proxyPort} is already in use
|
|
250
|
+
console.error(chalk.red(`Port ${proxyPort} is already in use.`));
|
|
251
|
+
console.error(chalk.blue("Stop the existing proxy first:"));
|
|
252
|
+
console.error(chalk.cyan(" portless proxy stop"));
|
|
253
|
+
console.error(chalk.blue("Or check what is using the port:"));
|
|
254
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
177
255
|
} else if (err.code === "EACCES") {
|
|
178
|
-
console.error(chalk.red(
|
|
256
|
+
console.error(chalk.red(`Permission denied for port ${proxyPort}.`));
|
|
257
|
+
console.error(chalk.blue("Either run with sudo:"));
|
|
258
|
+
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
259
|
+
console.error(chalk.blue("Or use a non-privileged port (no sudo needed):"));
|
|
260
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
179
261
|
} else {
|
|
180
262
|
console.error(chalk.red(`Proxy error: ${err.message}`));
|
|
181
263
|
}
|
|
182
264
|
process.exit(1);
|
|
183
265
|
});
|
|
184
266
|
server.listen(proxyPort, () => {
|
|
185
|
-
|
|
186
|
-
|
|
267
|
+
fs2.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
268
|
+
fs2.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
187
269
|
console.log(chalk.green(`HTTP proxy listening on port ${proxyPort}`));
|
|
188
270
|
});
|
|
189
271
|
let exiting = false;
|
|
190
272
|
const cleanup = () => {
|
|
191
273
|
if (exiting) return;
|
|
192
274
|
exiting = true;
|
|
275
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
276
|
+
if (pollingInterval) clearInterval(pollingInterval);
|
|
193
277
|
if (watcher) {
|
|
194
278
|
watcher.close();
|
|
195
279
|
}
|
|
196
280
|
try {
|
|
197
|
-
|
|
281
|
+
fs2.unlinkSync(store.pidPath);
|
|
198
282
|
} catch {
|
|
199
283
|
}
|
|
200
284
|
try {
|
|
201
|
-
|
|
285
|
+
fs2.unlinkSync(store.portFilePath);
|
|
202
286
|
} catch {
|
|
203
287
|
}
|
|
204
288
|
server.close(() => process.exit(0));
|
|
205
|
-
setTimeout(() => process.exit(0),
|
|
289
|
+
setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
|
|
206
290
|
};
|
|
207
291
|
process.on("SIGINT", cleanup);
|
|
208
292
|
process.on("SIGTERM", cleanup);
|
|
209
293
|
console.log(chalk.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
210
294
|
console.log(chalk.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
211
295
|
}
|
|
212
|
-
async function stopProxy() {
|
|
296
|
+
async function stopProxy(store, proxyPort) {
|
|
213
297
|
const pidPath = store.pidPath;
|
|
214
|
-
const
|
|
215
|
-
|
|
298
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
299
|
+
const sudoHint = needsSudo ? "sudo " : "";
|
|
300
|
+
if (!fs2.existsSync(pidPath)) {
|
|
216
301
|
if (await isProxyRunning(proxyPort)) {
|
|
217
302
|
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
218
303
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -220,25 +305,30 @@ async function stopProxy() {
|
|
|
220
305
|
try {
|
|
221
306
|
process.kill(pid, "SIGTERM");
|
|
222
307
|
try {
|
|
223
|
-
|
|
308
|
+
fs2.unlinkSync(store.portFilePath);
|
|
224
309
|
} catch {
|
|
225
310
|
}
|
|
226
311
|
console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
|
|
227
312
|
} catch (err) {
|
|
228
313
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
229
|
-
console.error(chalk.red("Permission denied. The proxy
|
|
230
|
-
console.
|
|
314
|
+
console.error(chalk.red("Permission denied. The proxy was started with sudo."));
|
|
315
|
+
console.error(chalk.blue("Stop it with:"));
|
|
316
|
+
console.error(chalk.cyan(" sudo portless proxy stop"));
|
|
231
317
|
} else {
|
|
232
318
|
const message = err instanceof Error ? err.message : String(err);
|
|
233
|
-
console.error(chalk.red(
|
|
319
|
+
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
320
|
+
console.error(chalk.blue("Check if the process is still running:"));
|
|
321
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
234
322
|
}
|
|
235
323
|
}
|
|
236
324
|
} else if (process.getuid?.() !== 0) {
|
|
237
|
-
console.error(chalk.red("
|
|
238
|
-
console.
|
|
325
|
+
console.error(chalk.red("Cannot identify the process. It may be running as root."));
|
|
326
|
+
console.error(chalk.blue("Try stopping with sudo:"));
|
|
327
|
+
console.error(chalk.cyan(" sudo portless proxy stop"));
|
|
239
328
|
} else {
|
|
240
329
|
console.error(chalk.red(`Could not identify the process on port ${proxyPort}.`));
|
|
241
|
-
console.
|
|
330
|
+
console.error(chalk.blue("Try manually:"));
|
|
331
|
+
console.error(chalk.cyan(` sudo kill "$(lsof -ti tcp:${proxyPort})"`));
|
|
242
332
|
}
|
|
243
333
|
} else {
|
|
244
334
|
console.log(chalk.yellow("Proxy is not running."));
|
|
@@ -246,17 +336,21 @@ async function stopProxy() {
|
|
|
246
336
|
return;
|
|
247
337
|
}
|
|
248
338
|
try {
|
|
249
|
-
const pid = parseInt(
|
|
339
|
+
const pid = parseInt(fs2.readFileSync(pidPath, "utf-8"), 10);
|
|
250
340
|
if (isNaN(pid)) {
|
|
251
341
|
console.error(chalk.red("Corrupted PID file. Removing it."));
|
|
252
|
-
|
|
342
|
+
fs2.unlinkSync(pidPath);
|
|
253
343
|
return;
|
|
254
344
|
}
|
|
255
345
|
try {
|
|
256
346
|
process.kill(pid, 0);
|
|
257
347
|
} catch {
|
|
258
|
-
console.log(chalk.yellow("Proxy process is no longer running. Cleaning up."));
|
|
259
|
-
|
|
348
|
+
console.log(chalk.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
349
|
+
fs2.unlinkSync(pidPath);
|
|
350
|
+
try {
|
|
351
|
+
fs2.unlinkSync(store.portFilePath);
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
260
354
|
return;
|
|
261
355
|
}
|
|
262
356
|
if (!await isProxyRunning(proxyPort)) {
|
|
@@ -266,27 +360,30 @@ async function stopProxy() {
|
|
|
266
360
|
)
|
|
267
361
|
);
|
|
268
362
|
console.log(chalk.yellow("Removing stale PID file."));
|
|
269
|
-
|
|
363
|
+
fs2.unlinkSync(pidPath);
|
|
270
364
|
return;
|
|
271
365
|
}
|
|
272
366
|
process.kill(pid, "SIGTERM");
|
|
273
|
-
|
|
367
|
+
fs2.unlinkSync(pidPath);
|
|
274
368
|
try {
|
|
275
|
-
|
|
369
|
+
fs2.unlinkSync(store.portFilePath);
|
|
276
370
|
} catch {
|
|
277
371
|
}
|
|
278
372
|
console.log(chalk.green("Proxy stopped."));
|
|
279
373
|
} catch (err) {
|
|
280
374
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
281
|
-
console.error(chalk.red("Permission denied. The proxy
|
|
282
|
-
console.
|
|
375
|
+
console.error(chalk.red("Permission denied. The proxy was started with sudo."));
|
|
376
|
+
console.error(chalk.blue("Stop it with:"));
|
|
377
|
+
console.error(chalk.cyan(` ${sudoHint}portless proxy stop`));
|
|
283
378
|
} else {
|
|
284
379
|
const message = err instanceof Error ? err.message : String(err);
|
|
285
|
-
console.error(chalk.red(
|
|
380
|
+
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
381
|
+
console.error(chalk.blue("Check if the process is still running:"));
|
|
382
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
286
383
|
}
|
|
287
384
|
}
|
|
288
385
|
}
|
|
289
|
-
function listRoutes() {
|
|
386
|
+
function listRoutes(store, proxyPort) {
|
|
290
387
|
const routes = store.loadRoutes();
|
|
291
388
|
if (routes.length === 0) {
|
|
292
389
|
console.log(chalk.yellow("No active routes."));
|
|
@@ -295,21 +392,32 @@ function listRoutes() {
|
|
|
295
392
|
}
|
|
296
393
|
console.log(chalk.blue.bold("\nActive routes:\n"));
|
|
297
394
|
for (const route of routes) {
|
|
395
|
+
const url = formatUrl(route.hostname, proxyPort);
|
|
298
396
|
console.log(
|
|
299
|
-
` ${chalk.cyan(
|
|
397
|
+
` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(`(pid ${route.pid})`)}`
|
|
300
398
|
);
|
|
301
399
|
}
|
|
302
400
|
console.log();
|
|
303
401
|
}
|
|
304
|
-
async function runApp(name, commandArgs) {
|
|
402
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs) {
|
|
305
403
|
const hostname = parseHostname(name);
|
|
306
|
-
const
|
|
404
|
+
const appUrl = formatUrl(hostname, proxyPort);
|
|
307
405
|
console.log(chalk.blue.bold(`
|
|
308
406
|
portless
|
|
309
407
|
`));
|
|
310
408
|
console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
|
|
311
409
|
if (!await isProxyRunning(proxyPort)) {
|
|
312
|
-
|
|
410
|
+
const defaultPort = getDefaultPort();
|
|
411
|
+
const needsSudo = defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
412
|
+
if (needsSudo) {
|
|
413
|
+
if (!process.stdin.isTTY) {
|
|
414
|
+
console.error(chalk.red("Proxy is not running."));
|
|
415
|
+
console.error(chalk.blue("Start the proxy first (requires sudo for this port):"));
|
|
416
|
+
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
417
|
+
console.error(chalk.blue("Or use the default port (no sudo needed):"));
|
|
418
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
313
421
|
const answer = await prompt(chalk.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
314
422
|
if (answer === "n" || answer === "no") {
|
|
315
423
|
console.log(chalk.gray("Cancelled."));
|
|
@@ -321,29 +429,40 @@ portless
|
|
|
321
429
|
return;
|
|
322
430
|
}
|
|
323
431
|
console.log(chalk.yellow("Starting proxy (requires sudo)..."));
|
|
324
|
-
const result = spawnSync("sudo", [process.execPath, process.argv[1], "proxy", "
|
|
432
|
+
const result = spawnSync("sudo", [process.execPath, process.argv[1], "proxy", "start"], {
|
|
325
433
|
stdio: "inherit",
|
|
326
|
-
timeout:
|
|
434
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
327
435
|
});
|
|
328
436
|
if (result.status !== 0) {
|
|
329
|
-
console.
|
|
437
|
+
console.error(chalk.red("Failed to start proxy."));
|
|
438
|
+
console.error(chalk.blue("Try starting it manually:"));
|
|
439
|
+
console.error(chalk.cyan(" sudo portless proxy start"));
|
|
330
440
|
process.exit(1);
|
|
331
441
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
442
|
+
} else {
|
|
443
|
+
console.log(chalk.yellow("Starting proxy..."));
|
|
444
|
+
const result = spawnSync(process.execPath, [process.argv[1], "proxy", "start"], {
|
|
445
|
+
stdio: "inherit",
|
|
446
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
447
|
+
});
|
|
448
|
+
if (result.status !== 0) {
|
|
449
|
+
console.error(chalk.red("Failed to start proxy."));
|
|
450
|
+
console.error(chalk.blue("Try starting it manually:"));
|
|
451
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
338
452
|
process.exit(1);
|
|
339
453
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
console.
|
|
343
|
-
|
|
344
|
-
console.
|
|
454
|
+
}
|
|
455
|
+
if (!await waitForProxy(defaultPort)) {
|
|
456
|
+
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
457
|
+
const logPath = path2.join(stateDir, "proxy.log");
|
|
458
|
+
console.error(chalk.blue("Try starting the proxy manually to see the error:"));
|
|
459
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
|
|
460
|
+
if (fs2.existsSync(logPath)) {
|
|
461
|
+
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
462
|
+
}
|
|
345
463
|
process.exit(1);
|
|
346
464
|
}
|
|
465
|
+
console.log(chalk.green("Proxy started in background"));
|
|
347
466
|
} else {
|
|
348
467
|
console.log(chalk.gray("-- Proxy is running"));
|
|
349
468
|
}
|
|
@@ -351,7 +470,7 @@ portless
|
|
|
351
470
|
console.log(chalk.green(`-- Using port ${port}`));
|
|
352
471
|
store.addRoute(hostname, port, process.pid);
|
|
353
472
|
console.log(chalk.cyan.bold(`
|
|
354
|
-
->
|
|
473
|
+
-> ${appUrl}
|
|
355
474
|
`));
|
|
356
475
|
console.log(chalk.gray(`Running: PORT=${port} ${commandArgs.join(" ")}
|
|
357
476
|
`));
|
|
@@ -371,7 +490,8 @@ async function main() {
|
|
|
371
490
|
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
372
491
|
if (isNpx || isPnpmDlx) {
|
|
373
492
|
console.error(chalk.red("Error: portless should not be run via npx or pnpm dlx."));
|
|
374
|
-
console.
|
|
493
|
+
console.error(chalk.blue("Install globally instead:"));
|
|
494
|
+
console.error(chalk.cyan(" npm install -g portless"));
|
|
375
495
|
process.exit(1);
|
|
376
496
|
}
|
|
377
497
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
@@ -391,16 +511,16 @@ ${chalk.bold("Install:")}
|
|
|
391
511
|
Do NOT add portless as a project dependency.
|
|
392
512
|
|
|
393
513
|
${chalk.bold("Usage:")}
|
|
394
|
-
${chalk.cyan("
|
|
395
|
-
${chalk.cyan("
|
|
396
|
-
${chalk.cyan("
|
|
514
|
+
${chalk.cyan("portless proxy start")} Start the proxy (background daemon)
|
|
515
|
+
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
516
|
+
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
397
517
|
${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
398
518
|
${chalk.cyan("portless list")} Show active routes
|
|
399
519
|
|
|
400
520
|
${chalk.bold("Examples:")}
|
|
401
|
-
|
|
402
|
-
portless myapp next dev
|
|
403
|
-
portless api.myapp pnpm start
|
|
521
|
+
portless proxy start # Start proxy on port 1355
|
|
522
|
+
portless myapp next dev # -> http://myapp.localhost:1355
|
|
523
|
+
portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
|
|
404
524
|
|
|
405
525
|
${chalk.bold("In package.json:")}
|
|
406
526
|
{
|
|
@@ -410,14 +530,20 @@ ${chalk.bold("In package.json:")}
|
|
|
410
530
|
}
|
|
411
531
|
|
|
412
532
|
${chalk.bold("How it works:")}
|
|
413
|
-
1. Start the proxy once
|
|
414
|
-
2. Run your apps - they register automatically
|
|
415
|
-
3. Access via http://<name>.localhost
|
|
533
|
+
1. Start the proxy once (listens on port 1355 by default, no sudo needed)
|
|
534
|
+
2. Run your apps - they auto-start the proxy and register automatically
|
|
535
|
+
3. Access via http://<name>.localhost:1355
|
|
416
536
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
417
537
|
|
|
418
538
|
${chalk.bold("Options:")}
|
|
419
|
-
--port <number>
|
|
420
|
-
Ports
|
|
539
|
+
-p, --port <number> Port for the proxy to listen on (default: 1355)
|
|
540
|
+
Ports < 1024 require sudo
|
|
541
|
+
--foreground Run proxy in foreground (for debugging)
|
|
542
|
+
|
|
543
|
+
${chalk.bold("Environment variables:")}
|
|
544
|
+
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
545
|
+
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
546
|
+
PORTLESS=0 | PORTLESS=skip Run command directly without proxy
|
|
421
547
|
|
|
422
548
|
${chalk.bold("Skip portless:")}
|
|
423
549
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
@@ -426,88 +552,134 @@ ${chalk.bold("Skip portless:")}
|
|
|
426
552
|
process.exit(0);
|
|
427
553
|
}
|
|
428
554
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
429
|
-
console.log("0.
|
|
555
|
+
console.log("0.3.0");
|
|
430
556
|
process.exit(0);
|
|
431
557
|
}
|
|
432
558
|
if (args[0] === "list") {
|
|
433
|
-
|
|
559
|
+
const { dir: dir2, port: port2 } = await discoverState();
|
|
560
|
+
const store2 = new RouteStore(dir2, {
|
|
561
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
562
|
+
});
|
|
563
|
+
listRoutes(store2, port2);
|
|
434
564
|
return;
|
|
435
565
|
}
|
|
436
566
|
if (args[0] === "proxy") {
|
|
437
567
|
if (args[1] === "stop") {
|
|
438
|
-
await
|
|
568
|
+
const { dir: dir2, port: port2 } = await discoverState();
|
|
569
|
+
const store3 = new RouteStore(dir2, {
|
|
570
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
571
|
+
});
|
|
572
|
+
await stopProxy(store3, port2);
|
|
439
573
|
return;
|
|
440
574
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
575
|
+
if (args[1] !== "start") {
|
|
576
|
+
console.log(`
|
|
577
|
+
${chalk.bold("Usage: portless proxy <command>")}
|
|
578
|
+
|
|
579
|
+
${chalk.cyan("portless proxy start")} Start the proxy (daemon)
|
|
580
|
+
${chalk.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
581
|
+
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
582
|
+
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
583
|
+
`);
|
|
584
|
+
process.exit(args[1] ? 1 : 0);
|
|
585
|
+
}
|
|
586
|
+
const isForeground = args.includes("--foreground");
|
|
587
|
+
let proxyPort = getDefaultPort();
|
|
588
|
+
let portFlagIndex = args.indexOf("--port");
|
|
589
|
+
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
444
590
|
if (portFlagIndex !== -1) {
|
|
445
591
|
const portValue = args[portFlagIndex + 1];
|
|
446
592
|
if (!portValue || portValue.startsWith("-")) {
|
|
447
|
-
console.error(chalk.red("Error: --port requires a port number"));
|
|
448
|
-
console.
|
|
593
|
+
console.error(chalk.red("Error: --port / -p requires a port number."));
|
|
594
|
+
console.error(chalk.blue("Usage:"));
|
|
595
|
+
console.error(chalk.cyan(" portless proxy start -p 8080"));
|
|
449
596
|
process.exit(1);
|
|
450
597
|
}
|
|
451
598
|
proxyPort = parseInt(portValue, 10);
|
|
452
599
|
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
453
600
|
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
454
|
-
console.
|
|
601
|
+
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
455
602
|
process.exit(1);
|
|
456
603
|
}
|
|
457
604
|
}
|
|
605
|
+
const stateDir = resolveStateDir(proxyPort);
|
|
606
|
+
const store2 = new RouteStore(stateDir, {
|
|
607
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
608
|
+
});
|
|
458
609
|
if (await isProxyRunning(proxyPort)) {
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
console.log(chalk.blue("To restart: portless proxy stop && sudo portless proxy"));
|
|
610
|
+
if (isForeground) {
|
|
611
|
+
return;
|
|
462
612
|
}
|
|
613
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
614
|
+
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
615
|
+
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
616
|
+
console.log(
|
|
617
|
+
chalk.blue(`To restart: portless proxy stop && ${sudoPrefix}portless proxy start`)
|
|
618
|
+
);
|
|
463
619
|
return;
|
|
464
620
|
}
|
|
465
|
-
if (proxyPort <
|
|
466
|
-
console.error(chalk.red(`Error:
|
|
467
|
-
console.
|
|
621
|
+
if (proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
622
|
+
console.error(chalk.red(`Error: Port ${proxyPort} requires sudo.`));
|
|
623
|
+
console.error(chalk.blue("Either run with sudo:"));
|
|
624
|
+
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
625
|
+
console.error(chalk.blue("Or use the default port (no sudo needed):"));
|
|
626
|
+
console.error(chalk.cyan(" portless proxy start"));
|
|
468
627
|
process.exit(1);
|
|
469
628
|
}
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
629
|
+
if (isForeground) {
|
|
630
|
+
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
631
|
+
startProxyServer(store2, proxyPort);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
store2.ensureDir();
|
|
635
|
+
const logPath = path2.join(stateDir, "proxy.log");
|
|
636
|
+
const logFd = fs2.openSync(logPath, "a");
|
|
637
|
+
try {
|
|
474
638
|
try {
|
|
475
|
-
|
|
639
|
+
fs2.chmodSync(logPath, FILE_MODE);
|
|
476
640
|
} catch {
|
|
477
641
|
}
|
|
478
|
-
const daemonArgs = [process.argv[1], "proxy"];
|
|
642
|
+
const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
|
|
479
643
|
if (portFlagIndex !== -1) {
|
|
480
644
|
daemonArgs.push("--port", proxyPort.toString());
|
|
481
645
|
}
|
|
482
|
-
const child =
|
|
646
|
+
const child = spawn2(process.execPath, daemonArgs, {
|
|
483
647
|
detached: true,
|
|
484
648
|
stdio: ["ignore", logFd, logFd],
|
|
485
649
|
env: process.env
|
|
486
650
|
});
|
|
487
651
|
child.unref();
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
652
|
+
} finally {
|
|
653
|
+
fs2.closeSync(logFd);
|
|
654
|
+
}
|
|
655
|
+
if (!await waitForProxy(proxyPort)) {
|
|
656
|
+
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
657
|
+
console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
|
|
658
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
659
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
|
|
660
|
+
if (fs2.existsSync(logPath)) {
|
|
661
|
+
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
495
662
|
}
|
|
496
|
-
|
|
663
|
+
process.exit(1);
|
|
497
664
|
}
|
|
498
|
-
console.log(chalk.
|
|
499
|
-
startProxyServer(proxyPort);
|
|
665
|
+
console.log(chalk.green(`Proxy started on port ${proxyPort}`));
|
|
500
666
|
return;
|
|
501
667
|
}
|
|
502
668
|
const name = args[0];
|
|
503
669
|
const commandArgs = args.slice(1);
|
|
504
670
|
if (commandArgs.length === 0) {
|
|
505
|
-
console.error(chalk.red("Error: No command provided"));
|
|
506
|
-
console.
|
|
507
|
-
console.
|
|
671
|
+
console.error(chalk.red("Error: No command provided."));
|
|
672
|
+
console.error(chalk.blue("Usage:"));
|
|
673
|
+
console.error(chalk.cyan(" portless <name> <command...>"));
|
|
674
|
+
console.error(chalk.blue("Example:"));
|
|
675
|
+
console.error(chalk.cyan(" portless myapp next dev"));
|
|
508
676
|
process.exit(1);
|
|
509
677
|
}
|
|
510
|
-
await
|
|
678
|
+
const { dir, port } = await discoverState();
|
|
679
|
+
const store = new RouteStore(dir, {
|
|
680
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
681
|
+
});
|
|
682
|
+
await runApp(store, port, dir, name, commandArgs);
|
|
511
683
|
}
|
|
512
684
|
main().catch((err) => {
|
|
513
685
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/index.d.ts
CHANGED
|
@@ -8,18 +8,27 @@ interface RouteInfo {
|
|
|
8
8
|
interface ProxyServerOptions {
|
|
9
9
|
/** Called on each request to get the current route table. */
|
|
10
10
|
getRoutes: () => RouteInfo[];
|
|
11
|
+
/** The port the proxy is listening on (used to build correct URLs). */
|
|
12
|
+
proxyPort: number;
|
|
11
13
|
/** Optional error logger; defaults to console.error. */
|
|
12
14
|
onError?: (message: string) => void;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/** Response header used to identify a portless proxy (for health checks). */
|
|
18
|
+
declare const PORTLESS_HEADER = "X-Portless";
|
|
15
19
|
/**
|
|
16
20
|
* Create an HTTP proxy server that routes requests based on the Host header.
|
|
17
21
|
*
|
|
22
|
+
* Uses Node's built-in http module for proxying (no external dependencies).
|
|
18
23
|
* The `getRoutes` callback is invoked on every request so callers can provide
|
|
19
24
|
* either a static list or a live-updating one.
|
|
20
25
|
*/
|
|
21
26
|
declare function createProxyServer(options: ProxyServerOptions): http.Server;
|
|
22
27
|
|
|
28
|
+
/** File permission mode for route and state files. */
|
|
29
|
+
declare const FILE_MODE = 420;
|
|
30
|
+
/** Directory permission mode for the state directory. */
|
|
31
|
+
declare const DIR_MODE = 493;
|
|
23
32
|
interface RouteMapping extends RouteInfo {
|
|
24
33
|
pid: number;
|
|
25
34
|
}
|
|
@@ -32,6 +41,7 @@ declare class RouteStore {
|
|
|
32
41
|
private readonly routesPath;
|
|
33
42
|
private readonly lockPath;
|
|
34
43
|
readonly pidPath: string;
|
|
44
|
+
readonly portFilePath: string;
|
|
35
45
|
private readonly onWarning;
|
|
36
46
|
constructor(dir: string, options?: {
|
|
37
47
|
onWarning?: (message: string) => void;
|
|
@@ -40,8 +50,8 @@ declare class RouteStore {
|
|
|
40
50
|
getRoutesPath(): string;
|
|
41
51
|
private static readonly sleepBuffer;
|
|
42
52
|
private syncSleep;
|
|
43
|
-
acquireLock
|
|
44
|
-
releaseLock
|
|
53
|
+
private acquireLock;
|
|
54
|
+
private releaseLock;
|
|
45
55
|
private isProcessAlive;
|
|
46
56
|
/**
|
|
47
57
|
* Load routes from disk, filtering out stale entries whose owning process
|
|
@@ -61,10 +71,14 @@ declare function isErrnoException(err: unknown): err is NodeJS.ErrnoException;
|
|
|
61
71
|
* Escape HTML special characters to prevent XSS.
|
|
62
72
|
*/
|
|
63
73
|
declare function escapeHtml(str: string): string;
|
|
74
|
+
/**
|
|
75
|
+
* Format a .localhost URL, including the port only when it is not 80 (standard HTTP).
|
|
76
|
+
*/
|
|
77
|
+
declare function formatUrl(hostname: string, proxyPort: number): string;
|
|
64
78
|
/**
|
|
65
79
|
* Parse and normalize a hostname input for use as a .localhost subdomain.
|
|
66
80
|
* Strips protocol prefixes, validates characters, and appends .localhost if needed.
|
|
67
81
|
*/
|
|
68
82
|
declare function parseHostname(input: string): string;
|
|
69
83
|
|
|
70
|
-
export { type ProxyServerOptions, type RouteInfo, type RouteMapping, RouteStore, createProxyServer, escapeHtml, isErrnoException, parseHostname };
|
|
84
|
+
export { DIR_MODE, FILE_MODE, PORTLESS_HEADER, type ProxyServerOptions, type RouteInfo, type RouteMapping, RouteStore, createProxyServer, escapeHtml, formatUrl, isErrnoException, parseHostname };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DIR_MODE,
|
|
3
|
+
FILE_MODE,
|
|
4
|
+
PORTLESS_HEADER,
|
|
2
5
|
RouteStore,
|
|
3
6
|
createProxyServer,
|
|
4
7
|
escapeHtml,
|
|
8
|
+
formatUrl,
|
|
5
9
|
isErrnoException,
|
|
6
10
|
parseHostname
|
|
7
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-Y5OVKUR4.js";
|
|
8
12
|
export {
|
|
13
|
+
DIR_MODE,
|
|
14
|
+
FILE_MODE,
|
|
15
|
+
PORTLESS_HEADER,
|
|
9
16
|
RouteStore,
|
|
10
17
|
createProxyServer,
|
|
11
18
|
escapeHtml,
|
|
19
|
+
formatUrl,
|
|
12
20
|
isErrnoException,
|
|
13
21
|
parseHostname
|
|
14
22
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Replace port numbers with stable, named .localhost URLs. For humans and agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -57,12 +57,10 @@
|
|
|
57
57
|
"url": "https://github.com/vercel-labs/portless/issues"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"chalk": "^5.3.0"
|
|
61
|
-
"http-proxy": "^1.18.1"
|
|
60
|
+
"chalk": "^5.3.0"
|
|
62
61
|
},
|
|
63
62
|
"devDependencies": {
|
|
64
63
|
"@eslint/js": "^9.39.2",
|
|
65
|
-
"@types/http-proxy": "^1.17.14",
|
|
66
64
|
"@types/node": "^20.11.0",
|
|
67
65
|
"@vitest/coverage-v8": "^4.0.18",
|
|
68
66
|
"eslint": "^9.39.2",
|