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.
Files changed (3) hide show
  1. package/README.md +7 -2
  2. package/dist/cli.js +112 -23
  3. 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
- async function waitForProxy(maxAttempts = 20, intervalMs = 250) {
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("Port 80 is already in use"));
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(80, () => {
184
+ server.listen(proxyPort, () => {
161
185
  fs.writeFileSync(store.pidPath, process.pid.toString(), { mode: 420 });
162
- console.log(chalk.green("HTTP proxy listening on port 80"));
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
- console.log(chalk.yellow("Proxy is not running."));
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
- "PID file exists but port 80 is not listening. The PID may have been recycled."
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")} Start the proxy (run once, keep open)
320
- ${chalk.cyan("sudo portless proxy stop")} Stop the proxy
321
- ${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
322
- ${chalk.cyan("portless list")} Show active routes
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.0");
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
- if (await isProxyRunning()) {
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("Error: Proxy requires sudo for port 80"));
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 child = spawn(process.execPath, [process.argv[1], "proxy"], {
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];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portless",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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",