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 ADDED
@@ -0,0 +1,217 @@
1
+ [![npm version](https://img.shields.io/npm/v/peakroute)](https://www.npmjs.com/package/peakroute) [![GitHub](https://img.shields.io/badge/GitHub-repository-black?logo=github)](https://github.com/faladev/peakroute)
2
+ ![Windows](https://img.shields.io/badge/Windows-supported-brightgreen?logo=windows) ![macOS](https://img.shields.io/badge/macOS-supported-black?logo=apple) ![Linux](https://img.shields.io/badge/Linux-supported-yellow?logo=linux)
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 isPowerShell = !!process.env.PSModulePath || process.env.TERM === "xterm-256color";
548
- if (isPowerShell) {
549
- const shellCmd = commandArgs.map((a) => {
550
- if (/[\s"'()$;|&<>@`]/.test(a)) {
551
- return `"${a.replace(/"/g, '`"')}"`;
552
- }
553
- return a;
554
- }).join(" ");
555
- child = spawn("powershell.exe", ["-Command", shellCmd + "; exit $LASTEXITCODE"], {
556
- stdio: "inherit",
557
- env,
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-ENHLKLCJ.js";
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
- process.kill(pid, "SIGTERM");
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
- process.kill(pid, "SIGTERM");
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.2");
901
+ console.log("0.5.4");
888
902
  process.exit(0);
889
903
  }
890
904
  if (args[0] === "trust") {
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  formatUrl,
15
15
  isErrnoException,
16
16
  parseHostname
17
- } from "./chunk-ENHLKLCJ.js";
17
+ } from "./chunk-IVO6GF7V.js";
18
18
  export {
19
19
  DIR_MODE,
20
20
  FILE_MODE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peakroute",
3
- "version": "0.5.2",
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"