peakroute 0.5.2 → 0.5.4
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 +217 -0
- package/dist/{chunk-ENHLKLCJ.js → chunk-IVO6GF7V.js} +39 -21
- package/dist/cli.js +19 -5
- package/dist/index.js +1 -1
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
[](https://www.npmjs.com/package/peakroute) [](https://github.com/faladev/peakroute)
|
|
2
|
+
  
|
|
3
|
+
|
|
4
|
+
# peakroute
|
|
5
|
+
|
|
6
|
+
> [!NOTE]
|
|
7
|
+
> **📌 Fork Notice:** This is a continuation fork of [vercel-labs/portless](https://github.com/vercel-labs/portless). We maintain and extend the project with additional features and platform support.
|
|
8
|
+
|
|
9
|
+
> [!IMPORTANT]
|
|
10
|
+
> **🪟 Windows Support Added!** This fork includes full Windows support alongside macOS and Linux. No platform limitations!
|
|
11
|
+
|
|
12
|
+
Replace port numbers with stable, named .localhost URLs. For humans and agents.
|
|
13
|
+
|
|
14
|
+
```diff
|
|
15
|
+
- "dev": "next dev" # http://localhost:3000
|
|
16
|
+
+ "dev": "peakroute myapp next dev" # http://myapp.localhost:1355
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Install
|
|
23
|
+
npm install -g peakroute
|
|
24
|
+
|
|
25
|
+
# Start the proxy (once, no sudo needed)
|
|
26
|
+
peakroute proxy start
|
|
27
|
+
|
|
28
|
+
# Run your app (auto-starts the proxy if needed)
|
|
29
|
+
peakroute myapp next dev
|
|
30
|
+
# -> http://myapp.localhost:1355
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
> The proxy auto-starts when you run an app. You can also start it explicitly with `peakroute proxy start`.
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
Local dev with port numbers is fragile:
|
|
38
|
+
|
|
39
|
+
- **Port conflicts** -- two projects default to the same port and you get `EADDRINUSE`
|
|
40
|
+
- **Memorizing ports** -- was the API on 3001 or 8080?
|
|
41
|
+
- **Refreshing shows the wrong app** -- stop one server, start another on the same port, and your open tab now shows something completely different
|
|
42
|
+
- **Monorepo multiplier** -- every problem above scales with each service in the repo
|
|
43
|
+
- **Agents test the wrong port** -- AI coding agents guess or hardcode the wrong port, especially in monorepos
|
|
44
|
+
- **Cookie and storage clashes** -- cookies set on `localhost` bleed across apps on different ports; localStorage is lost when ports shift
|
|
45
|
+
- **Hardcoded ports in config** -- CORS allowlists, OAuth redirect URIs, and `.env` files all break when ports change
|
|
46
|
+
- **Sharing URLs with teammates** -- "what port is that on?" becomes a Slack question
|
|
47
|
+
- **Browser history is useless** -- your history for `localhost:3000` is a jumble of unrelated projects
|
|
48
|
+
|
|
49
|
+
Peakroute fixes all of this by giving each dev server a stable, named `.localhost` URL that both humans and agents can rely on.
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Basic
|
|
55
|
+
peakroute myapp next dev
|
|
56
|
+
# -> http://myapp.localhost:1355
|
|
57
|
+
|
|
58
|
+
# Subdomains
|
|
59
|
+
peakroute api.myapp npm start
|
|
60
|
+
# -> http://api.myapp.localhost:1355
|
|
61
|
+
|
|
62
|
+
peakroute docs.myapp next dev
|
|
63
|
+
# -> http://docs.myapp.localhost:1355
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### In package.json
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"scripts": {
|
|
71
|
+
"dev": "peakroute myapp next dev"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The proxy auto-starts when you run an app. Or start it explicitly: `peakroute proxy start`.
|
|
77
|
+
|
|
78
|
+
## How It Works
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
flowchart TD
|
|
82
|
+
Browser["Browser<br/>myapp.localhost:1355"]
|
|
83
|
+
Proxy["peakroute proxy<br/>(port 1355)"]
|
|
84
|
+
App1[":4123<br/>myapp"]
|
|
85
|
+
App2[":4567<br/>api"]
|
|
86
|
+
|
|
87
|
+
Browser -->|port 1355| Proxy
|
|
88
|
+
Proxy --> App1
|
|
89
|
+
Proxy --> App2
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
1. **Start the proxy** -- auto-starts when you run an app, or start explicitly with `peakroute proxy start`
|
|
93
|
+
2. **Run apps** -- `peakroute <name> <command>` assigns a free port and registers with the proxy
|
|
94
|
+
3. **Access via URL** -- `http://<name>.localhost:1355` routes through the proxy to your app
|
|
95
|
+
|
|
96
|
+
Apps are assigned a random port (4000-4999) via the `PORT` and `HOST` environment variables. Most frameworks (Next.js, Express, Nuxt, etc.) respect these automatically. For frameworks that ignore `PORT` (Vite, Astro, React Router, Angular), peakroute auto-injects the correct `--port` and `--host` flags.
|
|
97
|
+
|
|
98
|
+
## HTTP/2 + HTTPS
|
|
99
|
+
|
|
100
|
+
Enable HTTP/2 for faster dev server page loads. Browsers limit HTTP/1.1 to 6 connections per host, which bottlenecks dev servers that serve many unbundled files (Vite, Nuxt, etc.). HTTP/2 multiplexes all requests over a single connection.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Start with HTTPS/2 -- generates certs and trusts them automatically
|
|
104
|
+
peakroute proxy start --https
|
|
105
|
+
|
|
106
|
+
# First run prompts for sudo once to add the CA to your system trust store.
|
|
107
|
+
# After that, no prompts. No browser warnings.
|
|
108
|
+
|
|
109
|
+
# Make it permanent (add to .bashrc / .zshrc)
|
|
110
|
+
export PEAKROUTE_HTTPS=1
|
|
111
|
+
peakroute proxy start # HTTPS by default now
|
|
112
|
+
|
|
113
|
+
# Use your own certs (e.g., from mkcert)
|
|
114
|
+
peakroute proxy start --cert ./cert.pem --key ./key.pem
|
|
115
|
+
|
|
116
|
+
# If you skipped sudo on first run, trust the CA later
|
|
117
|
+
sudo peakroute trust
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Commands
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
peakroute <name> <cmd> [args...] # Run app at http://<name>.localhost:1355
|
|
124
|
+
peakroute list # Show active routes
|
|
125
|
+
peakroute trust # Add local CA to system trust store
|
|
126
|
+
|
|
127
|
+
# Disable peakroute (run command directly)
|
|
128
|
+
PEAKROUTE=0 bun dev # Bypasses proxy, uses default port
|
|
129
|
+
# Also accepts PEAKROUTE=skip
|
|
130
|
+
|
|
131
|
+
# Proxy control
|
|
132
|
+
peakroute proxy start # Start the proxy (port 1355, daemon)
|
|
133
|
+
peakroute proxy start --https # Start with HTTP/2 + TLS
|
|
134
|
+
peakroute proxy start -p 80 # Start on port 80 (requires sudo)
|
|
135
|
+
peakroute proxy start --foreground # Start in foreground (for debugging)
|
|
136
|
+
peakroute proxy stop # Stop the proxy
|
|
137
|
+
|
|
138
|
+
# Options
|
|
139
|
+
-p, --port <number> # Port for the proxy (default: 1355)
|
|
140
|
+
# Ports < 1024 require sudo
|
|
141
|
+
--https # Enable HTTP/2 + TLS with auto-generated certs
|
|
142
|
+
--cert <path> # Use a custom TLS certificate (implies --https)
|
|
143
|
+
--key <path> # Use a custom TLS private key (implies --https)
|
|
144
|
+
--no-tls # Disable HTTPS (overrides PEAKROUTE_HTTPS)
|
|
145
|
+
--foreground # Run proxy in foreground instead of daemon
|
|
146
|
+
--force # Override a route registered by another process
|
|
147
|
+
|
|
148
|
+
# Environment variables
|
|
149
|
+
PEAKROUTE_PORT=<number> # Override the default proxy port
|
|
150
|
+
PEAKROUTE_HTTPS=1 # Always enable HTTPS
|
|
151
|
+
PEAKROUTE_STATE_DIR=<path> # Override the state directory
|
|
152
|
+
|
|
153
|
+
# Info
|
|
154
|
+
peakroute --help # Show help
|
|
155
|
+
peakroute --version # Show version
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## State Directory
|
|
159
|
+
|
|
160
|
+
Peakroute stores its state (routes, PID file, port file) in a directory that depends on the proxy port:
|
|
161
|
+
|
|
162
|
+
- **Port < 1024** (sudo required): `/tmp/peakroute` -- shared between root and user processes
|
|
163
|
+
- **Port >= 1024** (no sudo): `~/.peakroute` -- user-scoped, no root involvement
|
|
164
|
+
|
|
165
|
+
Override with the `PEAKROUTE_STATE_DIR` environment variable if needed.
|
|
166
|
+
|
|
167
|
+
## Development
|
|
168
|
+
|
|
169
|
+
This repo is a Bun workspace monorepo using [Turborepo](https://turbo.build). The publishable package lives in `packages/peakroute/`.
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
bun install # Install all dependencies
|
|
173
|
+
bun run build # Build all packages
|
|
174
|
+
bun run test # Run tests
|
|
175
|
+
bun run test:coverage # Run tests with coverage
|
|
176
|
+
bun run test:watch # Run tests in watch mode
|
|
177
|
+
bun run lint # Lint all packages
|
|
178
|
+
bun run typecheck # Type-check all packages
|
|
179
|
+
bun run format # Format all files with Prettier
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Proxying Between Peakroute Apps
|
|
183
|
+
|
|
184
|
+
If your frontend dev server (e.g. Vite, webpack) proxies API requests to another peakroute app, make sure the proxy rewrites the `Host` header. Without this, the proxy sends the **original** Host header, causing peakroute to route the request back to the frontend in an infinite loop.
|
|
185
|
+
|
|
186
|
+
**Vite** (`vite.config.ts`):
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
server: {
|
|
190
|
+
proxy: {
|
|
191
|
+
"/api": {
|
|
192
|
+
target: "http://api.myapp.localhost:1355",
|
|
193
|
+
changeOrigin: true, // Required: rewrites Host header to match target
|
|
194
|
+
ws: true,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**webpack-dev-server** (`webpack.config.js`):
|
|
201
|
+
|
|
202
|
+
```js
|
|
203
|
+
devServer: {
|
|
204
|
+
proxy: [{
|
|
205
|
+
context: ["/api"],
|
|
206
|
+
target: "http://api.myapp.localhost:1355",
|
|
207
|
+
changeOrigin: true, // Required: rewrites Host header to match target
|
|
208
|
+
}],
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Peakroute detects this misconfiguration and responds with `508 Loop Detected` along with a message pointing to this fix.
|
|
213
|
+
|
|
214
|
+
## Requirements
|
|
215
|
+
|
|
216
|
+
- Node.js 20+
|
|
217
|
+
- macOS, Linux, or Windows
|
|
@@ -538,33 +538,51 @@ function collectBinPaths(cwd) {
|
|
|
538
538
|
function augmentedPath(env) {
|
|
539
539
|
const base = (env ?? process.env).PATH ?? "";
|
|
540
540
|
const bins = collectBinPaths(process.cwd());
|
|
541
|
+
if (IS_WINDOWS) {
|
|
542
|
+
const systemRoot = process.env.SystemRoot ?? "C:\\Windows";
|
|
543
|
+
const essentialPaths = [
|
|
544
|
+
path2.join(systemRoot, "System32"),
|
|
545
|
+
path2.join(systemRoot, "System32", "WindowsPowerShell", "v1.0"),
|
|
546
|
+
path2.join(systemRoot, "System32", "wbem")
|
|
547
|
+
// for WMI tools
|
|
548
|
+
];
|
|
549
|
+
const bunPaths = [
|
|
550
|
+
path2.join(process.env.USERPROFILE ?? "C:\\Users\\" + process.env.USERNAME, ".bun", "bin"),
|
|
551
|
+
path2.join(process.env.LOCALAPPDATA ?? "", "bun", "bin")
|
|
552
|
+
];
|
|
553
|
+
const pathEntries = base.split(path2.delimiter).filter(Boolean);
|
|
554
|
+
const missingSystemPaths = essentialPaths.filter(
|
|
555
|
+
(p) => pathEntries.every((entry) => entry.toLowerCase() !== p.toLowerCase())
|
|
556
|
+
);
|
|
557
|
+
const missingBunPaths = bunPaths.filter(
|
|
558
|
+
(p) => pathEntries.every((entry) => entry.toLowerCase() !== p.toLowerCase())
|
|
559
|
+
);
|
|
560
|
+
const allMissing = [...missingSystemPaths, ...missingBunPaths];
|
|
561
|
+
if (allMissing.length > 0) {
|
|
562
|
+
const newPath = allMissing.join(path2.delimiter) + path2.delimiter + base;
|
|
563
|
+
if (bins.length > 0) {
|
|
564
|
+
return bins.join(path2.delimiter) + path2.delimiter + newPath;
|
|
565
|
+
}
|
|
566
|
+
return newPath;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
541
569
|
return bins.length > 0 ? bins.join(path2.delimiter) + path2.delimiter + base : base;
|
|
542
570
|
}
|
|
543
571
|
function spawnCommand(commandArgs, options) {
|
|
544
572
|
const env = { ...options?.env ?? process.env, PATH: augmentedPath(options?.env) };
|
|
545
573
|
let child;
|
|
546
574
|
if (IS_WINDOWS) {
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
windowsHide: true
|
|
559
|
-
});
|
|
560
|
-
} else {
|
|
561
|
-
child = spawn(commandArgs[0], commandArgs.slice(1), {
|
|
562
|
-
stdio: "inherit",
|
|
563
|
-
env,
|
|
564
|
-
shell: true,
|
|
565
|
-
windowsHide: true
|
|
566
|
-
});
|
|
567
|
-
}
|
|
575
|
+
const shellCmd = commandArgs.map((a) => {
|
|
576
|
+
if (/[\s"&<>|^]/.test(a)) {
|
|
577
|
+
return `"${a.replace(/"/g, '""')}"`;
|
|
578
|
+
}
|
|
579
|
+
return a;
|
|
580
|
+
}).join(" ");
|
|
581
|
+
child = spawn("cmd.exe", ["/c", shellCmd], {
|
|
582
|
+
stdio: "inherit",
|
|
583
|
+
env,
|
|
584
|
+
windowsHide: true
|
|
585
|
+
});
|
|
568
586
|
} else {
|
|
569
587
|
const shellCmd = commandArgs.map(shellEscape).join(" ");
|
|
570
588
|
child = spawn("/bin/sh", ["-c", shellCmd], {
|
package/dist/cli.js
CHANGED
|
@@ -24,13 +24,13 @@ import {
|
|
|
24
24
|
spawnCommand,
|
|
25
25
|
waitForProxy,
|
|
26
26
|
writeTlsMarker
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-IVO6GF7V.js";
|
|
28
28
|
|
|
29
29
|
// src/cli.ts
|
|
30
30
|
import chalk from "chalk";
|
|
31
31
|
import * as fs2 from "fs";
|
|
32
32
|
import * as path2 from "path";
|
|
33
|
-
import { spawn, spawnSync } from "child_process";
|
|
33
|
+
import { spawn, spawnSync, execSync } from "child_process";
|
|
34
34
|
|
|
35
35
|
// src/certs.ts
|
|
36
36
|
import * as fs from "fs";
|
|
@@ -435,6 +435,20 @@ var DEBOUNCE_MS = 100;
|
|
|
435
435
|
var POLL_INTERVAL_MS = 3e3;
|
|
436
436
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
437
437
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
438
|
+
function killProcess(pid, force = false) {
|
|
439
|
+
if (IS_WINDOWS) {
|
|
440
|
+
const args = force ? ["/F", "/T", "/PID", pid.toString()] : ["/T", "/PID", pid.toString()];
|
|
441
|
+
try {
|
|
442
|
+
execSync(`taskkill ${args.join(" ")}`, { windowsHide: true });
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
try {
|
|
447
|
+
process.kill(pid, force ? "SIGKILL" : "SIGTERM");
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
438
452
|
function startProxyServer(store, proxyPort, tlsOptions) {
|
|
439
453
|
store.ensureDir();
|
|
440
454
|
const isTls = !!tlsOptions;
|
|
@@ -540,7 +554,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
540
554
|
const pid = findPidOnPort(proxyPort);
|
|
541
555
|
if (pid !== null) {
|
|
542
556
|
try {
|
|
543
|
-
|
|
557
|
+
killProcess(pid);
|
|
544
558
|
try {
|
|
545
559
|
fs2.unlinkSync(store.portFilePath);
|
|
546
560
|
} catch {
|
|
@@ -615,7 +629,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
615
629
|
fs2.unlinkSync(pidPath);
|
|
616
630
|
return;
|
|
617
631
|
}
|
|
618
|
-
|
|
632
|
+
killProcess(pid);
|
|
619
633
|
fs2.unlinkSync(pidPath);
|
|
620
634
|
try {
|
|
621
635
|
fs2.unlinkSync(store.portFilePath);
|
|
@@ -884,7 +898,7 @@ ${chalk.bold("Skip peakroute:")}
|
|
|
884
898
|
process.exit(0);
|
|
885
899
|
}
|
|
886
900
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
887
|
-
console.log("0.5.
|
|
901
|
+
console.log("0.5.4");
|
|
888
902
|
process.exit(0);
|
|
889
903
|
}
|
|
890
904
|
if (args[0] === "trust") {
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peakroute",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Replace port numbers with stable, named .localhost URLs. For humans and agents. (Formerly portless)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"peakroute": "./dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
21
22
|
"node": ">=20"
|