portless 0.2.1 → 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 +343 -160
- package/dist/index.d.ts +17 -3
- package/dist/index.js +9 -1
- package/package.json +19 -18
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
|
`));
|
|
@@ -367,6 +486,14 @@ portless
|
|
|
367
486
|
}
|
|
368
487
|
async function main() {
|
|
369
488
|
const args = process.argv.slice(2);
|
|
489
|
+
const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
|
|
490
|
+
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
491
|
+
if (isNpx || isPnpmDlx) {
|
|
492
|
+
console.error(chalk.red("Error: portless should not be run via npx or pnpm dlx."));
|
|
493
|
+
console.error(chalk.blue("Install globally instead:"));
|
|
494
|
+
console.error(chalk.cyan(" npm install -g portless"));
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
370
497
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
371
498
|
if (skipPortless && args.length >= 2 && args[0] !== "proxy") {
|
|
372
499
|
spawnCommand(args.slice(1));
|
|
@@ -379,17 +506,21 @@ ${chalk.bold("portless")} - Replace port numbers with stable, named .localhost U
|
|
|
379
506
|
Eliminates port conflicts, memorizing port numbers, and cookie/storage
|
|
380
507
|
clashes by giving each dev server a stable .localhost URL.
|
|
381
508
|
|
|
509
|
+
${chalk.bold("Install:")}
|
|
510
|
+
${chalk.cyan("npm install -g portless")}
|
|
511
|
+
Do NOT add portless as a project dependency.
|
|
512
|
+
|
|
382
513
|
${chalk.bold("Usage:")}
|
|
383
|
-
${chalk.cyan("
|
|
384
|
-
${chalk.cyan("
|
|
385
|
-
${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
|
|
386
517
|
${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
387
518
|
${chalk.cyan("portless list")} Show active routes
|
|
388
519
|
|
|
389
520
|
${chalk.bold("Examples:")}
|
|
390
|
-
|
|
391
|
-
portless myapp next dev
|
|
392
|
-
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
|
|
393
524
|
|
|
394
525
|
${chalk.bold("In package.json:")}
|
|
395
526
|
{
|
|
@@ -399,14 +530,20 @@ ${chalk.bold("In package.json:")}
|
|
|
399
530
|
}
|
|
400
531
|
|
|
401
532
|
${chalk.bold("How it works:")}
|
|
402
|
-
1. Start the proxy once
|
|
403
|
-
2. Run your apps - they register automatically
|
|
404
|
-
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
|
|
405
536
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
406
537
|
|
|
407
538
|
${chalk.bold("Options:")}
|
|
408
|
-
--port <number>
|
|
409
|
-
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
|
|
410
547
|
|
|
411
548
|
${chalk.bold("Skip portless:")}
|
|
412
549
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
@@ -415,88 +552,134 @@ ${chalk.bold("Skip portless:")}
|
|
|
415
552
|
process.exit(0);
|
|
416
553
|
}
|
|
417
554
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
418
|
-
console.log("0.
|
|
555
|
+
console.log("0.3.0");
|
|
419
556
|
process.exit(0);
|
|
420
557
|
}
|
|
421
558
|
if (args[0] === "list") {
|
|
422
|
-
|
|
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);
|
|
423
564
|
return;
|
|
424
565
|
}
|
|
425
566
|
if (args[0] === "proxy") {
|
|
426
567
|
if (args[1] === "stop") {
|
|
427
|
-
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);
|
|
428
573
|
return;
|
|
429
574
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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");
|
|
433
590
|
if (portFlagIndex !== -1) {
|
|
434
591
|
const portValue = args[portFlagIndex + 1];
|
|
435
592
|
if (!portValue || portValue.startsWith("-")) {
|
|
436
|
-
console.error(chalk.red("Error: --port requires a port number"));
|
|
437
|
-
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"));
|
|
438
596
|
process.exit(1);
|
|
439
597
|
}
|
|
440
598
|
proxyPort = parseInt(portValue, 10);
|
|
441
599
|
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
442
600
|
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
443
|
-
console.
|
|
601
|
+
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
444
602
|
process.exit(1);
|
|
445
603
|
}
|
|
446
604
|
}
|
|
605
|
+
const stateDir = resolveStateDir(proxyPort);
|
|
606
|
+
const store2 = new RouteStore(stateDir, {
|
|
607
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
608
|
+
});
|
|
447
609
|
if (await isProxyRunning(proxyPort)) {
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
console.log(chalk.blue("To restart: portless proxy stop && sudo portless proxy"));
|
|
610
|
+
if (isForeground) {
|
|
611
|
+
return;
|
|
451
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
|
+
);
|
|
452
619
|
return;
|
|
453
620
|
}
|
|
454
|
-
if (proxyPort <
|
|
455
|
-
console.error(chalk.red(`Error:
|
|
456
|
-
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"));
|
|
457
627
|
process.exit(1);
|
|
458
628
|
}
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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 {
|
|
463
638
|
try {
|
|
464
|
-
|
|
639
|
+
fs2.chmodSync(logPath, FILE_MODE);
|
|
465
640
|
} catch {
|
|
466
641
|
}
|
|
467
|
-
const daemonArgs = [process.argv[1], "proxy"];
|
|
642
|
+
const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
|
|
468
643
|
if (portFlagIndex !== -1) {
|
|
469
644
|
daemonArgs.push("--port", proxyPort.toString());
|
|
470
645
|
}
|
|
471
|
-
const child =
|
|
646
|
+
const child = spawn2(process.execPath, daemonArgs, {
|
|
472
647
|
detached: true,
|
|
473
648
|
stdio: ["ignore", logFd, logFd],
|
|
474
649
|
env: process.env
|
|
475
650
|
});
|
|
476
651
|
child.unref();
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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}`));
|
|
484
662
|
}
|
|
485
|
-
|
|
663
|
+
process.exit(1);
|
|
486
664
|
}
|
|
487
|
-
console.log(chalk.
|
|
488
|
-
startProxyServer(proxyPort);
|
|
665
|
+
console.log(chalk.green(`Proxy started on port ${proxyPort}`));
|
|
489
666
|
return;
|
|
490
667
|
}
|
|
491
668
|
const name = args[0];
|
|
492
669
|
const commandArgs = args.slice(1);
|
|
493
670
|
if (commandArgs.length === 0) {
|
|
494
|
-
console.error(chalk.red("Error: No command provided"));
|
|
495
|
-
console.
|
|
496
|
-
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"));
|
|
497
676
|
process.exit(1);
|
|
498
677
|
}
|
|
499
|
-
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);
|
|
500
683
|
}
|
|
501
684
|
main().catch((err) => {
|
|
502
685
|
const message = err instanceof Error ? err.message : String(err);
|