portless 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/dist/cli.js +112 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ flowchart TD
|
|
|
80
80
|
Proxy --> App2
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
1. **Start the proxy** -- runs on port 80 in the background (requires sudo once)
|
|
83
|
+
1. **Start the proxy** -- runs on port 80 by default in the background (requires sudo once)
|
|
84
84
|
2. **Run apps** -- `portless <name> <command>` assigns a free port and registers with the proxy
|
|
85
85
|
3. **Access via URL** -- `http://<name>.localhost` routes through the proxy to your app
|
|
86
86
|
|
|
@@ -97,9 +97,14 @@ PORTLESS=0 pnpm dev # Bypasses proxy, uses default port
|
|
|
97
97
|
# Also accepts PORTLESS=skip
|
|
98
98
|
|
|
99
99
|
# Proxy control
|
|
100
|
-
sudo portless proxy # Start the proxy
|
|
100
|
+
sudo portless proxy # Start the proxy (port 80)
|
|
101
|
+
sudo portless proxy --port 8080 # Start the proxy on a custom port
|
|
101
102
|
sudo portless proxy stop # Stop the proxy
|
|
102
103
|
|
|
104
|
+
# Options
|
|
105
|
+
--port <number> # Port for the proxy (default: 80)
|
|
106
|
+
# Ports >= 1024 do not require sudo
|
|
107
|
+
|
|
103
108
|
# Info
|
|
104
109
|
portless --help # Show help
|
|
105
110
|
portless --version # Show version
|
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import chalk from "chalk";
|
|
|
11
11
|
import * as fs from "fs";
|
|
12
12
|
import * as path from "path";
|
|
13
13
|
import * as readline from "readline";
|
|
14
|
-
import { spawn, spawnSync } from "child_process";
|
|
14
|
+
import { execSync, spawn, spawnSync } from "child_process";
|
|
15
15
|
|
|
16
16
|
// src/cli-utils.ts
|
|
17
17
|
import * as net from "net";
|
|
@@ -71,13 +71,37 @@ function prompt(question) {
|
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
73
|
var PORTLESS_DIR = "/tmp/portless";
|
|
74
|
+
var PROXY_PORT_PATH = path.join(PORTLESS_DIR, "proxy.port");
|
|
75
|
+
var DEFAULT_PROXY_PORT = 80;
|
|
74
76
|
var store = new RouteStore(PORTLESS_DIR, {
|
|
75
77
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
76
78
|
});
|
|
77
|
-
|
|
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
|
+
function findPidOnPort(port) {
|
|
89
|
+
try {
|
|
90
|
+
const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
timeout: 5e3
|
|
93
|
+
});
|
|
94
|
+
const pid = parseInt(output.trim().split("\n")[0], 10);
|
|
95
|
+
return isNaN(pid) ? null : pid;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function waitForProxy(proxyPort, maxAttempts = 20, intervalMs = 250) {
|
|
101
|
+
const port = proxyPort ?? readProxyPort();
|
|
78
102
|
for (let i = 0; i < maxAttempts; i++) {
|
|
79
103
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
80
|
-
if (await isProxyRunning()) {
|
|
104
|
+
if (await isProxyRunning(port)) {
|
|
81
105
|
return true;
|
|
82
106
|
}
|
|
83
107
|
}
|
|
@@ -115,7 +139,7 @@ function spawnCommand(commandArgs, options) {
|
|
|
115
139
|
process.exit(code ?? 1);
|
|
116
140
|
});
|
|
117
141
|
}
|
|
118
|
-
function startProxyServer() {
|
|
142
|
+
function startProxyServer(proxyPort) {
|
|
119
143
|
store.ensureDir();
|
|
120
144
|
const routesPath = store.getRoutesPath();
|
|
121
145
|
if (!fs.existsSync(routesPath)) {
|
|
@@ -149,7 +173,7 @@ function startProxyServer() {
|
|
|
149
173
|
});
|
|
150
174
|
server.on("error", (err) => {
|
|
151
175
|
if (err.code === "EADDRINUSE") {
|
|
152
|
-
console.error(chalk.red(
|
|
176
|
+
console.error(chalk.red(`Port ${proxyPort} is already in use`));
|
|
153
177
|
} else if (err.code === "EACCES") {
|
|
154
178
|
console.error(chalk.red("Permission denied. Use: sudo portless proxy"));
|
|
155
179
|
} else {
|
|
@@ -157,9 +181,10 @@ function startProxyServer() {
|
|
|
157
181
|
}
|
|
158
182
|
process.exit(1);
|
|
159
183
|
});
|
|
160
|
-
server.listen(
|
|
184
|
+
server.listen(proxyPort, () => {
|
|
161
185
|
fs.writeFileSync(store.pidPath, process.pid.toString(), { mode: 420 });
|
|
162
|
-
|
|
186
|
+
fs.writeFileSync(PROXY_PORT_PATH, proxyPort.toString(), { mode: 420 });
|
|
187
|
+
console.log(chalk.green(`HTTP proxy listening on port ${proxyPort}`));
|
|
163
188
|
});
|
|
164
189
|
let exiting = false;
|
|
165
190
|
const cleanup = () => {
|
|
@@ -172,6 +197,10 @@ function startProxyServer() {
|
|
|
172
197
|
fs.unlinkSync(store.pidPath);
|
|
173
198
|
} catch {
|
|
174
199
|
}
|
|
200
|
+
try {
|
|
201
|
+
fs.unlinkSync(PROXY_PORT_PATH);
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
175
204
|
server.close(() => process.exit(0));
|
|
176
205
|
setTimeout(() => process.exit(0), 2e3).unref();
|
|
177
206
|
};
|
|
@@ -182,8 +211,38 @@ function startProxyServer() {
|
|
|
182
211
|
}
|
|
183
212
|
async function stopProxy() {
|
|
184
213
|
const pidPath = store.pidPath;
|
|
214
|
+
const proxyPort = readProxyPort();
|
|
185
215
|
if (!fs.existsSync(pidPath)) {
|
|
186
|
-
|
|
216
|
+
if (await isProxyRunning(proxyPort)) {
|
|
217
|
+
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
218
|
+
const pid = findPidOnPort(proxyPort);
|
|
219
|
+
if (pid !== null) {
|
|
220
|
+
try {
|
|
221
|
+
process.kill(pid, "SIGTERM");
|
|
222
|
+
try {
|
|
223
|
+
fs.unlinkSync(PROXY_PORT_PATH);
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (isErrnoException(err) && err.code === "EPERM") {
|
|
229
|
+
console.error(chalk.red("Permission denied. The proxy runs as root."));
|
|
230
|
+
console.log(chalk.blue("Use: sudo portless proxy stop"));
|
|
231
|
+
} else {
|
|
232
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
233
|
+
console.error(chalk.red("Failed to stop proxy:"), message);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else if (process.getuid?.() !== 0) {
|
|
237
|
+
console.error(chalk.red("Permission denied. The proxy likely runs as root."));
|
|
238
|
+
console.log(chalk.blue("Use: sudo portless proxy stop"));
|
|
239
|
+
} else {
|
|
240
|
+
console.error(chalk.red(`Could not identify the process on port ${proxyPort}.`));
|
|
241
|
+
console.log(chalk.blue(`Try: sudo kill "$(lsof -ti tcp:${proxyPort})"`));
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
console.log(chalk.yellow("Proxy is not running."));
|
|
245
|
+
}
|
|
187
246
|
return;
|
|
188
247
|
}
|
|
189
248
|
try {
|
|
@@ -200,10 +259,10 @@ async function stopProxy() {
|
|
|
200
259
|
fs.unlinkSync(pidPath);
|
|
201
260
|
return;
|
|
202
261
|
}
|
|
203
|
-
if (!await isProxyRunning()) {
|
|
262
|
+
if (!await isProxyRunning(proxyPort)) {
|
|
204
263
|
console.log(
|
|
205
264
|
chalk.yellow(
|
|
206
|
-
|
|
265
|
+
`PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
|
|
207
266
|
)
|
|
208
267
|
);
|
|
209
268
|
console.log(chalk.yellow("Removing stale PID file."));
|
|
@@ -212,6 +271,10 @@ async function stopProxy() {
|
|
|
212
271
|
}
|
|
213
272
|
process.kill(pid, "SIGTERM");
|
|
214
273
|
fs.unlinkSync(pidPath);
|
|
274
|
+
try {
|
|
275
|
+
fs.unlinkSync(PROXY_PORT_PATH);
|
|
276
|
+
} catch {
|
|
277
|
+
}
|
|
215
278
|
console.log(chalk.green("Proxy stopped."));
|
|
216
279
|
} catch (err) {
|
|
217
280
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
@@ -240,11 +303,12 @@ function listRoutes() {
|
|
|
240
303
|
}
|
|
241
304
|
async function runApp(name, commandArgs) {
|
|
242
305
|
const hostname = parseHostname(name);
|
|
306
|
+
const proxyPort = readProxyPort();
|
|
243
307
|
console.log(chalk.blue.bold(`
|
|
244
308
|
portless
|
|
245
309
|
`));
|
|
246
310
|
console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
|
|
247
|
-
if (!await isProxyRunning()) {
|
|
311
|
+
if (!await isProxyRunning(proxyPort)) {
|
|
248
312
|
if (process.stdin.isTTY) {
|
|
249
313
|
const answer = await prompt(chalk.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
250
314
|
if (answer === "n" || answer === "no") {
|
|
@@ -316,10 +380,11 @@ Eliminates port conflicts, memorizing port numbers, and cookie/storage
|
|
|
316
380
|
clashes by giving each dev server a stable .localhost URL.
|
|
317
381
|
|
|
318
382
|
${chalk.bold("Usage:")}
|
|
319
|
-
${chalk.cyan("sudo portless proxy")}
|
|
320
|
-
${chalk.cyan("sudo portless proxy
|
|
321
|
-
${chalk.cyan("portless
|
|
322
|
-
${chalk.cyan("portless
|
|
383
|
+
${chalk.cyan("sudo portless proxy")} Start the proxy (run once, keep open)
|
|
384
|
+
${chalk.cyan("sudo portless proxy --port 8080")} Start the proxy on a custom port
|
|
385
|
+
${chalk.cyan("sudo portless proxy stop")} Stop the proxy
|
|
386
|
+
${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
387
|
+
${chalk.cyan("portless list")} Show active routes
|
|
323
388
|
|
|
324
389
|
${chalk.bold("Examples:")}
|
|
325
390
|
sudo portless proxy # Start proxy in terminal 1
|
|
@@ -334,11 +399,15 @@ ${chalk.bold("In package.json:")}
|
|
|
334
399
|
}
|
|
335
400
|
|
|
336
401
|
${chalk.bold("How it works:")}
|
|
337
|
-
1. Start the proxy once with sudo (listens on port 80)
|
|
402
|
+
1. Start the proxy once with sudo (listens on port 80 by default)
|
|
338
403
|
2. Run your apps - they register automatically
|
|
339
404
|
3. Access via http://<name>.localhost
|
|
340
405
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
341
406
|
|
|
407
|
+
${chalk.bold("Options:")}
|
|
408
|
+
--port <number> Port for the proxy to listen on (default: 80)
|
|
409
|
+
Ports >= 1024 do not require sudo
|
|
410
|
+
|
|
342
411
|
${chalk.bold("Skip portless:")}
|
|
343
412
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
344
413
|
PORTLESS=skip pnpm dev # Same as above
|
|
@@ -346,7 +415,7 @@ ${chalk.bold("Skip portless:")}
|
|
|
346
415
|
process.exit(0);
|
|
347
416
|
}
|
|
348
417
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
349
|
-
console.log("0.1
|
|
418
|
+
console.log("0.2.1");
|
|
350
419
|
process.exit(0);
|
|
351
420
|
}
|
|
352
421
|
if (args[0] === "list") {
|
|
@@ -359,15 +428,31 @@ ${chalk.bold("Skip portless:")}
|
|
|
359
428
|
return;
|
|
360
429
|
}
|
|
361
430
|
const isDaemon = args.includes("--daemon");
|
|
362
|
-
|
|
431
|
+
let proxyPort = DEFAULT_PROXY_PORT;
|
|
432
|
+
const portFlagIndex = args.indexOf("--port");
|
|
433
|
+
if (portFlagIndex !== -1) {
|
|
434
|
+
const portValue = args[portFlagIndex + 1];
|
|
435
|
+
if (!portValue || portValue.startsWith("-")) {
|
|
436
|
+
console.error(chalk.red("Error: --port requires a port number"));
|
|
437
|
+
console.log(chalk.blue("Usage: portless proxy --port 8080"));
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
proxyPort = parseInt(portValue, 10);
|
|
441
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
442
|
+
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
443
|
+
console.log(chalk.blue("Port must be between 1 and 65535"));
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (await isProxyRunning(proxyPort)) {
|
|
363
448
|
if (!isDaemon) {
|
|
364
449
|
console.log(chalk.yellow("Proxy is already running."));
|
|
365
450
|
console.log(chalk.blue("To restart: portless proxy stop && sudo portless proxy"));
|
|
366
451
|
}
|
|
367
452
|
return;
|
|
368
453
|
}
|
|
369
|
-
if (process.getuid() !== 0) {
|
|
370
|
-
console.error(chalk.red(
|
|
454
|
+
if (proxyPort < 1024 && process.getuid() !== 0) {
|
|
455
|
+
console.error(chalk.red(`Error: Proxy requires sudo for port ${proxyPort}`));
|
|
371
456
|
console.log(chalk.blue("Usage: sudo portless proxy"));
|
|
372
457
|
process.exit(1);
|
|
373
458
|
}
|
|
@@ -379,14 +464,18 @@ ${chalk.bold("Skip portless:")}
|
|
|
379
464
|
fs.chmodSync(logPath, 420);
|
|
380
465
|
} catch {
|
|
381
466
|
}
|
|
382
|
-
const
|
|
467
|
+
const daemonArgs = [process.argv[1], "proxy"];
|
|
468
|
+
if (portFlagIndex !== -1) {
|
|
469
|
+
daemonArgs.push("--port", proxyPort.toString());
|
|
470
|
+
}
|
|
471
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
383
472
|
detached: true,
|
|
384
473
|
stdio: ["ignore", logFd, logFd],
|
|
385
474
|
env: process.env
|
|
386
475
|
});
|
|
387
476
|
child.unref();
|
|
388
477
|
fs.closeSync(logFd);
|
|
389
|
-
if (!await waitForProxy()) {
|
|
478
|
+
if (!await waitForProxy(proxyPort)) {
|
|
390
479
|
console.error(chalk.red("Proxy failed to start"));
|
|
391
480
|
if (fs.existsSync(logPath)) {
|
|
392
481
|
console.log(chalk.gray(`Check logs: ${logPath}`));
|
|
@@ -396,7 +485,7 @@ ${chalk.bold("Skip portless:")}
|
|
|
396
485
|
return;
|
|
397
486
|
}
|
|
398
487
|
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
399
|
-
startProxyServer();
|
|
488
|
+
startProxyServer(proxyPort);
|
|
400
489
|
return;
|
|
401
490
|
}
|
|
402
491
|
const name = args[0];
|