peakroute 0.5.8 → 0.6.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 +39 -0
- package/dist/{chunk-EWW65DJW.js → chunk-IJJU4JLF.js} +104 -12
- package/dist/cli.js +175 -12
- package/dist/index.d.ts +21 -1
- package/dist/index.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,8 @@ sudo peakroute trust
|
|
|
123
123
|
peakroute <name> <cmd> [args...] # Run app at http://<name>.localhost:1355
|
|
124
124
|
peakroute list # Show active routes
|
|
125
125
|
peakroute trust # Add local CA to system trust store
|
|
126
|
+
peakroute alias <host> <port> # Register external service (e.g., Docker)
|
|
127
|
+
peakroute alias remove <host> # Remove an external route
|
|
126
128
|
|
|
127
129
|
# Disable peakroute (run command directly)
|
|
128
130
|
PEAKROUTE=0 bun dev # Bypasses proxy, uses default port
|
|
@@ -164,6 +166,43 @@ Peakroute stores its state (routes, PID file, port file) in a directory that dep
|
|
|
164
166
|
|
|
165
167
|
Override with the `PEAKROUTE_STATE_DIR` environment variable if needed.
|
|
166
168
|
|
|
169
|
+
## External Services (Docker, etc.)
|
|
170
|
+
|
|
171
|
+
Use the `peakroute alias` command to register routes for services not spawned by peakroute, such as Docker containers or other external processes:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Register a Docker container running on port 3000
|
|
175
|
+
peakroute alias mydocker.localhost 3000
|
|
176
|
+
|
|
177
|
+
# Now access it at http://mydocker.localhost:1355
|
|
178
|
+
|
|
179
|
+
# Remove the alias when done
|
|
180
|
+
peakroute alias remove mydocker.localhost
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Aliases are marked with `[external]` in the route list and are never cleaned up as "stale" since they don't have an associated process PID.
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
peakroute list
|
|
187
|
+
# -> http://mydocker.localhost:1355 -> localhost:3000 (pid 0) [external]
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Git Worktree Support
|
|
191
|
+
|
|
192
|
+
Peakroute automatically detects when you're running inside a [Git Worktree](https://git-scm.com/docs/git-worktree) and prepends the branch name as a subdomain prefix. This gives each worktree a unique URL without any configuration changes.
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# In a worktree on branch feat/login
|
|
196
|
+
peakroute myapp next dev
|
|
197
|
+
# -> http://feat-login.myapp.localhost:1355
|
|
198
|
+
|
|
199
|
+
# In a worktree on branch fix/auth-bug
|
|
200
|
+
peakroute api next dev
|
|
201
|
+
# -> http://fix-auth-bug.api.localhost:1355
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The branch name is sanitized for use as a hostname (slashes become hyphens, invalid characters are removed).
|
|
205
|
+
|
|
167
206
|
## Development
|
|
168
207
|
|
|
169
208
|
This repo is a Bun workspace monorepo using [Turborepo](https://turbo.build). The publishable package lives in `packages/peakroute/`.
|
|
@@ -10,6 +10,7 @@ var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
|
10
10
|
|
|
11
11
|
// src/utils.ts
|
|
12
12
|
import * as fs from "fs";
|
|
13
|
+
import { execSync } from "child_process";
|
|
13
14
|
function chmodSafe(path4, mode) {
|
|
14
15
|
if (IS_WINDOWS) return;
|
|
15
16
|
try {
|
|
@@ -47,12 +48,39 @@ function formatUrl(hostname, proxyPort, tls = false) {
|
|
|
47
48
|
const defaultPort = tls ? 443 : 80;
|
|
48
49
|
return proxyPort === defaultPort ? `${proto}://${hostname}` : `${proto}://${hostname}:${proxyPort}`;
|
|
49
50
|
}
|
|
51
|
+
function detectGitWorktree() {
|
|
52
|
+
try {
|
|
53
|
+
const gitDir = execSync("git rev-parse --git-dir", {
|
|
54
|
+
encoding: "utf-8",
|
|
55
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
56
|
+
}).trim();
|
|
57
|
+
const isWorktree = gitDir.includes("/worktrees/") || gitDir.includes("\\worktrees\\");
|
|
58
|
+
if (!isWorktree) return null;
|
|
59
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
62
|
+
}).trim();
|
|
63
|
+
return branch || null;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function sanitizeBranchName(branch) {
|
|
69
|
+
return branch.replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "");
|
|
70
|
+
}
|
|
50
71
|
function parseHostname(input) {
|
|
51
72
|
let hostname = input.trim().replace(/^https?:\/\//, "").split("/")[0].toLowerCase();
|
|
52
73
|
if (!hostname || hostname === ".localhost") {
|
|
53
74
|
throw new Error("Hostname cannot be empty");
|
|
54
75
|
}
|
|
55
|
-
if (
|
|
76
|
+
if (hostname.endsWith(".localhost")) {
|
|
77
|
+
return hostname;
|
|
78
|
+
}
|
|
79
|
+
const branch = detectGitWorktree();
|
|
80
|
+
if (branch) {
|
|
81
|
+
const sanitized = sanitizeBranchName(branch);
|
|
82
|
+
hostname = `${sanitized}.${hostname}.localhost`;
|
|
83
|
+
} else {
|
|
56
84
|
hostname = `${hostname}.localhost`;
|
|
57
85
|
}
|
|
58
86
|
const name = hostname.replace(/\.localhost$/, "");
|
|
@@ -71,6 +99,18 @@ function parseHostname(input) {
|
|
|
71
99
|
import * as http from "http";
|
|
72
100
|
import * as http2 from "http2";
|
|
73
101
|
import * as net from "net";
|
|
102
|
+
function findRoute(routes, host) {
|
|
103
|
+
const exact = routes.find((r) => r.hostname === host);
|
|
104
|
+
if (exact) return exact;
|
|
105
|
+
const parts = host.split(".");
|
|
106
|
+
while (parts.length > 2) {
|
|
107
|
+
parts.shift();
|
|
108
|
+
const wildcard = parts.join(".");
|
|
109
|
+
const match = routes.find((r) => r.hostname === wildcard);
|
|
110
|
+
if (match) return match;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
74
114
|
var PEAKROUTE_HEADER = "X-Peakroute";
|
|
75
115
|
var HOP_BY_HOP_HEADERS = /* @__PURE__ */ new Set([
|
|
76
116
|
"connection",
|
|
@@ -134,7 +174,7 @@ Fix: add changeOrigin: true to your proxy config, e.g.:
|
|
|
134
174
|
);
|
|
135
175
|
return;
|
|
136
176
|
}
|
|
137
|
-
const route = routes
|
|
177
|
+
const route = findRoute(routes, host);
|
|
138
178
|
if (!route) {
|
|
139
179
|
const safeHost = escapeHtml(host);
|
|
140
180
|
res.writeHead(404, { "Content-Type": "text/html" });
|
|
@@ -221,7 +261,7 @@ Fix: add changeOrigin: true to your proxy config, e.g.:
|
|
|
221
261
|
}
|
|
222
262
|
const routes = getRoutes();
|
|
223
263
|
const host = getRequestHost(req).split(":")[0];
|
|
224
|
-
const route = routes
|
|
264
|
+
const route = findRoute(routes, host);
|
|
225
265
|
if (!route) {
|
|
226
266
|
socket.destroy();
|
|
227
267
|
return;
|
|
@@ -333,7 +373,7 @@ import * as https from "https";
|
|
|
333
373
|
import * as net2 from "net";
|
|
334
374
|
import * as path2 from "path";
|
|
335
375
|
import * as readline from "readline";
|
|
336
|
-
import { execSync, spawn } from "child_process";
|
|
376
|
+
import { execSync as execSync2, spawn } from "child_process";
|
|
337
377
|
var DEFAULT_PROXY_PORT = 1355;
|
|
338
378
|
var MIN_APP_PORT = 4e3;
|
|
339
379
|
var MAX_APP_PORT = 4999;
|
|
@@ -419,7 +459,22 @@ async function discoverState() {
|
|
|
419
459
|
const defaultPort = getDefaultPort();
|
|
420
460
|
return { dir: resolveStateDir(defaultPort), port: defaultPort, tls: false };
|
|
421
461
|
}
|
|
422
|
-
|
|
462
|
+
function isPortAvailable(port) {
|
|
463
|
+
return new Promise((resolve) => {
|
|
464
|
+
const server = net2.createServer();
|
|
465
|
+
server.listen(port, () => {
|
|
466
|
+
server.close(() => resolve(true));
|
|
467
|
+
});
|
|
468
|
+
server.on("error", () => resolve(false));
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT, preferredPort) {
|
|
472
|
+
if (preferredPort !== void 0) {
|
|
473
|
+
if (await isPortAvailable(preferredPort)) {
|
|
474
|
+
return preferredPort;
|
|
475
|
+
}
|
|
476
|
+
throw new Error(`Port ${preferredPort} is already in use`);
|
|
477
|
+
}
|
|
423
478
|
if (minPort > maxPort) {
|
|
424
479
|
throw new Error(`minPort (${minPort}) must be <= maxPort (${maxPort})`);
|
|
425
480
|
}
|
|
@@ -489,7 +544,7 @@ function parsePidFromNetstat(output, port) {
|
|
|
489
544
|
function findPidOnPort(port) {
|
|
490
545
|
if (IS_WINDOWS) {
|
|
491
546
|
try {
|
|
492
|
-
const output =
|
|
547
|
+
const output = execSync2(`netstat -ano -p tcp`, {
|
|
493
548
|
encoding: "utf-8",
|
|
494
549
|
timeout: LSOF_TIMEOUT_MS
|
|
495
550
|
});
|
|
@@ -499,7 +554,7 @@ function findPidOnPort(port) {
|
|
|
499
554
|
}
|
|
500
555
|
}
|
|
501
556
|
try {
|
|
502
|
-
const output =
|
|
557
|
+
const output = execSync2(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
503
558
|
encoding: "utf-8",
|
|
504
559
|
timeout: LSOF_TIMEOUT_MS
|
|
505
560
|
});
|
|
@@ -631,7 +686,9 @@ var FRAMEWORKS_NEEDING_PORT = {
|
|
|
631
686
|
vite: { strictPort: true },
|
|
632
687
|
"react-router": { strictPort: true },
|
|
633
688
|
astro: { strictPort: false },
|
|
634
|
-
ng: { strictPort: false }
|
|
689
|
+
ng: { strictPort: false },
|
|
690
|
+
expo: { strictPort: false },
|
|
691
|
+
"react-native": { strictPort: false }
|
|
635
692
|
};
|
|
636
693
|
var PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "yarn", "pnpm", "bun"]);
|
|
637
694
|
function detectFrameworkFromPackageJson(scriptName) {
|
|
@@ -682,7 +739,7 @@ function resolveFramework(commandArgs) {
|
|
|
682
739
|
if (!detectedFramework) return null;
|
|
683
740
|
return { name: detectedFramework, ...FRAMEWORKS_NEEDING_PORT[detectedFramework] };
|
|
684
741
|
}
|
|
685
|
-
function injectPortAndHostFlags(commandArgs, port, strictPort) {
|
|
742
|
+
function injectPortAndHostFlags(commandArgs, port, strictPort, framework) {
|
|
686
743
|
if (!commandArgs.includes("--port")) {
|
|
687
744
|
commandArgs.push("--port", port.toString());
|
|
688
745
|
if (strictPort) {
|
|
@@ -692,6 +749,9 @@ function injectPortAndHostFlags(commandArgs, port, strictPort) {
|
|
|
692
749
|
if (!commandArgs.includes("--host")) {
|
|
693
750
|
commandArgs.push("--host", "127.0.0.1");
|
|
694
751
|
}
|
|
752
|
+
if (framework === "react-native") {
|
|
753
|
+
process.env.RCT_METRO_PORT = port.toString();
|
|
754
|
+
}
|
|
695
755
|
}
|
|
696
756
|
function injectFrameworkFlags(commandArgs, port, manualFramework) {
|
|
697
757
|
if (manualFramework === "force") {
|
|
@@ -699,12 +759,17 @@ function injectFrameworkFlags(commandArgs, port, manualFramework) {
|
|
|
699
759
|
return;
|
|
700
760
|
}
|
|
701
761
|
if (manualFramework && FRAMEWORKS_NEEDING_PORT[manualFramework]) {
|
|
702
|
-
injectPortAndHostFlags(
|
|
762
|
+
injectPortAndHostFlags(
|
|
763
|
+
commandArgs,
|
|
764
|
+
port,
|
|
765
|
+
FRAMEWORKS_NEEDING_PORT[manualFramework].strictPort,
|
|
766
|
+
manualFramework
|
|
767
|
+
);
|
|
703
768
|
return;
|
|
704
769
|
}
|
|
705
770
|
const framework = resolveFramework(commandArgs);
|
|
706
771
|
if (framework) {
|
|
707
|
-
injectPortAndHostFlags(commandArgs, port, framework.strictPort);
|
|
772
|
+
injectPortAndHostFlags(commandArgs, port, framework.strictPort, framework.name);
|
|
708
773
|
}
|
|
709
774
|
}
|
|
710
775
|
function prompt(question) {
|
|
@@ -910,6 +975,9 @@ var RouteStore = class _RouteStore {
|
|
|
910
975
|
* is no longer alive. Stale-route cleanup is only persisted when the caller
|
|
911
976
|
* already holds the lock (i.e. inside addRoute/removeRoute) to avoid
|
|
912
977
|
* unprotected concurrent writes.
|
|
978
|
+
*
|
|
979
|
+
* Note: Routes with pid=0 are considered external (e.g., Docker containers)
|
|
980
|
+
* and are never filtered out as stale.
|
|
913
981
|
*/
|
|
914
982
|
loadRoutes(persistCleanup = false) {
|
|
915
983
|
if (!fs3.existsSync(this.routesPath)) {
|
|
@@ -929,7 +997,7 @@ var RouteStore = class _RouteStore {
|
|
|
929
997
|
return [];
|
|
930
998
|
}
|
|
931
999
|
const routes = parsed.filter(isValidRoute);
|
|
932
|
-
const alive = routes.filter((r) => this.isProcessAlive(r.pid));
|
|
1000
|
+
const alive = routes.filter((r) => r.pid === 0 || this.isProcessAlive(r.pid));
|
|
933
1001
|
if (persistCleanup && alive.length !== routes.length) {
|
|
934
1002
|
try {
|
|
935
1003
|
fs3.writeFileSync(this.routesPath, JSON.stringify(alive, null, 2), {
|
|
@@ -977,6 +1045,28 @@ var RouteStore = class _RouteStore {
|
|
|
977
1045
|
this.releaseLock();
|
|
978
1046
|
}
|
|
979
1047
|
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Register an alias for an external service (e.g., Docker container).
|
|
1050
|
+
* Uses pid=0 as a sentinel to indicate the route is not managed by
|
|
1051
|
+
* a child process and should never be cleaned up as "stale".
|
|
1052
|
+
*/
|
|
1053
|
+
addAlias(hostname, port) {
|
|
1054
|
+
this.ensureDir();
|
|
1055
|
+
if (!this.acquireLock()) {
|
|
1056
|
+
throw new Error("Failed to acquire route lock");
|
|
1057
|
+
}
|
|
1058
|
+
try {
|
|
1059
|
+
const routes = this.loadRoutes(true);
|
|
1060
|
+
const existing = routes.find((r) => r.hostname === hostname);
|
|
1061
|
+
if (existing) {
|
|
1062
|
+
throw new Error(`Hostname "${hostname}" is already registered`);
|
|
1063
|
+
}
|
|
1064
|
+
routes.push({ hostname, port, pid: 0 });
|
|
1065
|
+
this.saveRoutes(routes);
|
|
1066
|
+
} finally {
|
|
1067
|
+
this.releaseLock();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
980
1070
|
};
|
|
981
1071
|
|
|
982
1072
|
export {
|
|
@@ -988,6 +1078,8 @@ export {
|
|
|
988
1078
|
isErrnoException,
|
|
989
1079
|
escapeHtml,
|
|
990
1080
|
formatUrl,
|
|
1081
|
+
detectGitWorktree,
|
|
1082
|
+
sanitizeBranchName,
|
|
991
1083
|
parseHostname,
|
|
992
1084
|
PEAKROUTE_HEADER,
|
|
993
1085
|
createProxyServer,
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
spawnCommand,
|
|
26
26
|
waitForProxy,
|
|
27
27
|
writeTlsMarker
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-IJJU4JLF.js";
|
|
29
29
|
|
|
30
30
|
// src/cli.ts
|
|
31
31
|
import chalk from "chalk";
|
|
@@ -251,8 +251,80 @@ function loginKeychainPath() {
|
|
|
251
251
|
const home = process.env.HOME || `/Users/${process.env.USER || "unknown"}`;
|
|
252
252
|
return path.join(home, "Library", "Keychains", "login.keychain-db");
|
|
253
253
|
}
|
|
254
|
+
function detectLinuxDistro() {
|
|
255
|
+
try {
|
|
256
|
+
const osRelease = fs.readFileSync("/etc/os-release", "utf-8");
|
|
257
|
+
const idMatch = osRelease.match(/^ID=(.+)$/m);
|
|
258
|
+
const idLikeMatch = osRelease.match(/^ID_LIKE=(.+)$/m);
|
|
259
|
+
const id = idMatch?.[1]?.toLowerCase().replace(/"/g, "");
|
|
260
|
+
const idLike = idLikeMatch?.[1]?.toLowerCase().replace(/"/g, "");
|
|
261
|
+
if (id) {
|
|
262
|
+
if (id === "debian" || id === "ubuntu" || id === "linuxmint" || id === "pop") {
|
|
263
|
+
return "debian";
|
|
264
|
+
}
|
|
265
|
+
if (id === "fedora" || id === "rhel" || id === "centos" || id === "rocky" || id === "almalinux") {
|
|
266
|
+
return "fedora";
|
|
267
|
+
}
|
|
268
|
+
if (id === "arch" || id === "manjaro" || id === "endeavouros") {
|
|
269
|
+
return "arch";
|
|
270
|
+
}
|
|
271
|
+
if (id === "opensuse" || id === "opensuse-leap" || id === "opensuse-tumbleweed") {
|
|
272
|
+
return "opensuse";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (idLike) {
|
|
276
|
+
if (idLike.includes("debian")) return "debian";
|
|
277
|
+
if (idLike.includes("fedora") || idLike.includes("rhel")) return "fedora";
|
|
278
|
+
if (idLike.includes("arch")) return "arch";
|
|
279
|
+
if (idLike.includes("suse")) return "opensuse";
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
execFileSync("which", ["update-ca-certificates"], { stdio: "pipe" });
|
|
285
|
+
return "debian";
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
execFileSync("which", ["update-ca-trust"], { stdio: "pipe" });
|
|
290
|
+
return "fedora";
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
return "unknown";
|
|
294
|
+
}
|
|
295
|
+
function getLinuxCertConfig(distro) {
|
|
296
|
+
switch (distro) {
|
|
297
|
+
case "fedora":
|
|
298
|
+
return {
|
|
299
|
+
certDir: "/etc/pki/ca-trust/source/anchors",
|
|
300
|
+
certFilename: "peakroute-ca.crt",
|
|
301
|
+
updateCommand: ["update-ca-trust", "extract"]
|
|
302
|
+
};
|
|
303
|
+
case "arch":
|
|
304
|
+
return {
|
|
305
|
+
certDir: "/etc/ca-certificates/trust-source/anchors",
|
|
306
|
+
certFilename: "peakroute-ca.crt",
|
|
307
|
+
updateCommand: ["update-ca-trust"]
|
|
308
|
+
};
|
|
309
|
+
case "opensuse":
|
|
310
|
+
return {
|
|
311
|
+
certDir: "/etc/pki/trust/anchors",
|
|
312
|
+
certFilename: "peakroute-ca.crt",
|
|
313
|
+
updateCommand: ["update-ca-certificates"]
|
|
314
|
+
};
|
|
315
|
+
case "debian":
|
|
316
|
+
default:
|
|
317
|
+
return {
|
|
318
|
+
certDir: "/usr/local/share/ca-certificates",
|
|
319
|
+
certFilename: "peakroute-ca.crt",
|
|
320
|
+
updateCommand: ["update-ca-certificates"]
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
254
324
|
function isCATrustedLinux(stateDir) {
|
|
255
|
-
const
|
|
325
|
+
const distro = detectLinuxDistro();
|
|
326
|
+
const config = getLinuxCertConfig(distro);
|
|
327
|
+
const systemCertPath = path.join(config.certDir, config.certFilename);
|
|
256
328
|
if (!fileExists(systemCertPath)) return false;
|
|
257
329
|
try {
|
|
258
330
|
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
@@ -410,9 +482,17 @@ function trustCA(stateDir) {
|
|
|
410
482
|
);
|
|
411
483
|
return { trusted: true };
|
|
412
484
|
} else if (process.platform === "linux") {
|
|
413
|
-
const
|
|
485
|
+
const distro = detectLinuxDistro();
|
|
486
|
+
const config = getLinuxCertConfig(distro);
|
|
487
|
+
const dest = path.join(config.certDir, config.certFilename);
|
|
488
|
+
if (!fs.existsSync(config.certDir)) {
|
|
489
|
+
fs.mkdirSync(config.certDir, { recursive: true });
|
|
490
|
+
}
|
|
414
491
|
fs.copyFileSync(caCertPath, dest);
|
|
415
|
-
execFileSync(
|
|
492
|
+
execFileSync(config.updateCommand[0], config.updateCommand.slice(1), {
|
|
493
|
+
stdio: "pipe",
|
|
494
|
+
timeout: 3e4
|
|
495
|
+
});
|
|
416
496
|
return { trusted: true };
|
|
417
497
|
} else if (IS_WINDOWS) {
|
|
418
498
|
trustCAWindows(caCertPath);
|
|
@@ -671,13 +751,14 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
671
751
|
console.log(chalk.blue.bold("\nActive routes:\n"));
|
|
672
752
|
for (const route of routes) {
|
|
673
753
|
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
754
|
+
const externalIndicator = route.pid === 0 ? chalk.gray(" [external]") : "";
|
|
674
755
|
console.log(
|
|
675
|
-
` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(`(pid ${route.pid})`)}`
|
|
756
|
+
` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(`(pid ${route.pid})`)}${externalIndicator}`
|
|
676
757
|
);
|
|
677
758
|
}
|
|
678
759
|
console.log();
|
|
679
760
|
}
|
|
680
|
-
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force, inject) {
|
|
761
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force, inject, preferredAppPort) {
|
|
681
762
|
const hostname = parseHostname(name);
|
|
682
763
|
console.log(chalk.blue.bold(`
|
|
683
764
|
peakroute
|
|
@@ -773,7 +854,7 @@ peakroute
|
|
|
773
854
|
} else {
|
|
774
855
|
console.log(chalk.gray("-- Proxy is running"));
|
|
775
856
|
}
|
|
776
|
-
const port = await findFreePort();
|
|
857
|
+
const port = await findFreePort(void 0, void 0, preferredAppPort);
|
|
777
858
|
console.log(chalk.green(`-- Using port ${port}`));
|
|
778
859
|
try {
|
|
779
860
|
store.addRoute(hostname, port, process.pid, force);
|
|
@@ -796,6 +877,7 @@ peakroute
|
|
|
796
877
|
...process.env,
|
|
797
878
|
PORT: port.toString(),
|
|
798
879
|
HOST: "127.0.0.1",
|
|
880
|
+
PEAKROUTE_URL: finalUrl,
|
|
799
881
|
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: ".localhost"
|
|
800
882
|
},
|
|
801
883
|
onCleanup: () => {
|
|
@@ -807,7 +889,7 @@ peakroute
|
|
|
807
889
|
});
|
|
808
890
|
}
|
|
809
891
|
async function main() {
|
|
810
|
-
const updatePromise = checkForUpdate("0.
|
|
892
|
+
const updatePromise = checkForUpdate("0.6.0");
|
|
811
893
|
if (process.stdin.isTTY) {
|
|
812
894
|
process.on("exit", () => {
|
|
813
895
|
try {
|
|
@@ -821,7 +903,7 @@ async function main() {
|
|
|
821
903
|
if (newerVersion) {
|
|
822
904
|
console.log(
|
|
823
905
|
chalk.yellow(`
|
|
824
|
-
\u2192 Update available: ${chalk.bold(newerVersion)} (current: ${"0.
|
|
906
|
+
\u2192 Update available: ${chalk.bold(newerVersion)} (current: ${"0.6.0"})`)
|
|
825
907
|
);
|
|
826
908
|
console.log(chalk.gray(` Run: npm install -g peakroute
|
|
827
909
|
`));
|
|
@@ -858,6 +940,8 @@ ${chalk.bold("Usage:")}
|
|
|
858
940
|
${chalk.cyan("peakroute <name> <cmd>")} Run your app through the proxy
|
|
859
941
|
${chalk.cyan("peakroute list")} Show active routes
|
|
860
942
|
${chalk.cyan("peakroute trust")} Add local CA to system trust store
|
|
943
|
+
${chalk.cyan("peakroute alias <host> <port>")} Register a route for external services (e.g. Docker)
|
|
944
|
+
${chalk.cyan("peakroute alias remove <host>")} Remove an external route
|
|
861
945
|
|
|
862
946
|
${chalk.bold("Examples:")}
|
|
863
947
|
peakroute proxy start # Start proxy on port 1355
|
|
@@ -896,9 +980,11 @@ ${chalk.bold("Options:")}
|
|
|
896
980
|
--foreground Run proxy in foreground (for debugging)
|
|
897
981
|
--force Override an existing route registered by another process
|
|
898
982
|
--inject Force injection of --port and --host flags (when auto-detection fails)
|
|
983
|
+
--app-port <number> Use a specific port for the app (instead of auto-finding one)
|
|
899
984
|
|
|
900
985
|
${chalk.bold("Environment variables:")}
|
|
901
986
|
PEAKROUTE_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
987
|
+
PEAKROUTE_APP_PORT=<number> Use a specific port for the app (alternative to --app-port)
|
|
902
988
|
PEAKROUTE_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
|
|
903
989
|
PEAKROUTE_STATE_DIR=<path> Override the state directory
|
|
904
990
|
PEAKROUTE=0 | PEAKROUTE=skip Run command directly without proxy
|
|
@@ -910,7 +996,7 @@ ${chalk.bold("Skip peakroute:")}
|
|
|
910
996
|
process.exit(0);
|
|
911
997
|
}
|
|
912
998
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
913
|
-
console.log("0.
|
|
999
|
+
console.log("0.6.0");
|
|
914
1000
|
process.exit(0);
|
|
915
1001
|
}
|
|
916
1002
|
if (args[0] === "trust") {
|
|
@@ -940,6 +1026,54 @@ ${chalk.bold("Skip peakroute:")}
|
|
|
940
1026
|
listRoutes(store2, port2, tls3);
|
|
941
1027
|
return;
|
|
942
1028
|
}
|
|
1029
|
+
if (args[0] === "alias") {
|
|
1030
|
+
const { dir: dir2 } = await discoverState();
|
|
1031
|
+
const store2 = new RouteStore(dir2, {
|
|
1032
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1033
|
+
});
|
|
1034
|
+
if (args[1] === "remove") {
|
|
1035
|
+
const hostname2 = args[2];
|
|
1036
|
+
if (!hostname2) {
|
|
1037
|
+
console.error(chalk.red("Error: Missing hostname."));
|
|
1038
|
+
console.error(chalk.blue("Usage:"));
|
|
1039
|
+
console.error(chalk.cyan(" peakroute alias remove <hostname>"));
|
|
1040
|
+
process.exit(1);
|
|
1041
|
+
}
|
|
1042
|
+
try {
|
|
1043
|
+
store2.removeRoute(parseHostname(hostname2));
|
|
1044
|
+
console.log(chalk.green(`Alias removed: ${hostname2}`));
|
|
1045
|
+
} catch (err) {
|
|
1046
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1047
|
+
console.error(chalk.red(`Failed to remove alias: ${message}`));
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const hostname = args[1];
|
|
1053
|
+
const portStr = args[2];
|
|
1054
|
+
if (!hostname || !portStr) {
|
|
1055
|
+
console.error(chalk.red("Error: Missing arguments."));
|
|
1056
|
+
console.error(chalk.blue("Usage:"));
|
|
1057
|
+
console.error(chalk.cyan(" peakroute alias <hostname> <port>"));
|
|
1058
|
+
console.error(chalk.cyan(" peakroute alias remove <hostname>"));
|
|
1059
|
+
process.exit(1);
|
|
1060
|
+
}
|
|
1061
|
+
const portNum = parseInt(portStr, 10);
|
|
1062
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
1063
|
+
console.error(chalk.red(`Error: Invalid port number: ${portStr}`));
|
|
1064
|
+
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
1065
|
+
process.exit(1);
|
|
1066
|
+
}
|
|
1067
|
+
try {
|
|
1068
|
+
store2.addAlias(parseHostname(hostname), portNum);
|
|
1069
|
+
console.log(chalk.green(`Alias registered: ${hostname} -> localhost:${portNum}`));
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1072
|
+
console.error(chalk.red(`Failed to register alias: ${message}`));
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
943
1077
|
if (args[0] === "proxy") {
|
|
944
1078
|
if (args[1] === "stop") {
|
|
945
1079
|
const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
|
|
@@ -1145,11 +1279,40 @@ ${chalk.bold("Usage: peakroute proxy <command>")}
|
|
|
1145
1279
|
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
1146
1280
|
return;
|
|
1147
1281
|
}
|
|
1282
|
+
const parsedFlags = /* @__PURE__ */ new Set();
|
|
1148
1283
|
const forceIdx = args.indexOf("--force");
|
|
1149
1284
|
const force = forceIdx >= 0 && forceIdx <= 1;
|
|
1285
|
+
if (force) parsedFlags.add(forceIdx);
|
|
1150
1286
|
const injectIdx = args.indexOf("--inject");
|
|
1151
1287
|
const injectFlag = injectIdx >= 0 && injectIdx <= 1;
|
|
1152
|
-
|
|
1288
|
+
if (injectFlag) parsedFlags.add(injectIdx);
|
|
1289
|
+
let preferredAppPort;
|
|
1290
|
+
const appPortIdx = args.indexOf("--app-port");
|
|
1291
|
+
if (appPortIdx >= 0 && appPortIdx <= 1) {
|
|
1292
|
+
const portValue = args[appPortIdx + 1];
|
|
1293
|
+
if (!portValue || portValue.startsWith("-")) {
|
|
1294
|
+
console.error(chalk.red("Error: --app-port requires a port number."));
|
|
1295
|
+
console.error(chalk.blue("Usage:"));
|
|
1296
|
+
console.error(chalk.cyan(" peakroute myapp --app-port 3000 next dev"));
|
|
1297
|
+
process.exit(1);
|
|
1298
|
+
}
|
|
1299
|
+
const parsedPort = parseInt(portValue, 10);
|
|
1300
|
+
if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
1301
|
+
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
1302
|
+
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
preferredAppPort = parsedPort;
|
|
1306
|
+
parsedFlags.add(appPortIdx);
|
|
1307
|
+
parsedFlags.add(appPortIdx + 1);
|
|
1308
|
+
}
|
|
1309
|
+
if (preferredAppPort === void 0 && process.env.PEAKROUTE_APP_PORT) {
|
|
1310
|
+
const envPort = parseInt(process.env.PEAKROUTE_APP_PORT, 10);
|
|
1311
|
+
if (!isNaN(envPort) && envPort >= 1 && envPort <= 65535) {
|
|
1312
|
+
preferredAppPort = envPort;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
const appArgs = args.filter((_, i) => !parsedFlags.has(i));
|
|
1153
1316
|
const name = appArgs[0];
|
|
1154
1317
|
const commandArgs = appArgs.slice(1);
|
|
1155
1318
|
if (commandArgs.length === 0) {
|
|
@@ -1164,7 +1327,7 @@ ${chalk.bold("Usage: peakroute proxy <command>")}
|
|
|
1164
1327
|
const store = new RouteStore(dir, {
|
|
1165
1328
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1166
1329
|
});
|
|
1167
|
-
await runApp(store, port, dir, name, commandArgs, tls2, force, injectFlag);
|
|
1330
|
+
await runApp(store, port, dir, name, commandArgs, tls2, force, injectFlag, preferredAppPort);
|
|
1168
1331
|
}
|
|
1169
1332
|
main().catch((err) => {
|
|
1170
1333
|
const message = err instanceof Error ? err.message : String(err);
|
package/dist/index.d.ts
CHANGED
|
@@ -90,11 +90,20 @@ declare class RouteStore {
|
|
|
90
90
|
* is no longer alive. Stale-route cleanup is only persisted when the caller
|
|
91
91
|
* already holds the lock (i.e. inside addRoute/removeRoute) to avoid
|
|
92
92
|
* unprotected concurrent writes.
|
|
93
|
+
*
|
|
94
|
+
* Note: Routes with pid=0 are considered external (e.g., Docker containers)
|
|
95
|
+
* and are never filtered out as stale.
|
|
93
96
|
*/
|
|
94
97
|
loadRoutes(persistCleanup?: boolean): RouteMapping[];
|
|
95
98
|
private saveRoutes;
|
|
96
99
|
addRoute(hostname: string, port: number, pid: number, force?: boolean): void;
|
|
97
100
|
removeRoute(hostname: string): void;
|
|
101
|
+
/**
|
|
102
|
+
* Register an alias for an external service (e.g., Docker container).
|
|
103
|
+
* Uses pid=0 as a sentinel to indicate the route is not managed by
|
|
104
|
+
* a child process and should never be cleaned up as "stale".
|
|
105
|
+
*/
|
|
106
|
+
addAlias(hostname: string, port: number): void;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
/**
|
|
@@ -123,10 +132,21 @@ declare function escapeHtml(str: string): string;
|
|
|
123
132
|
* (80 for HTTP, 443 for HTTPS).
|
|
124
133
|
*/
|
|
125
134
|
declare function formatUrl(hostname: string, proxyPort: number, tls?: boolean): string;
|
|
135
|
+
/**
|
|
136
|
+
* Detect if we're in a git worktree and return the branch name.
|
|
137
|
+
* Returns null if not in a worktree or if git is not available.
|
|
138
|
+
*/
|
|
139
|
+
declare function detectGitWorktree(): string | null;
|
|
140
|
+
/**
|
|
141
|
+
* Sanitize branch name for use in hostname.
|
|
142
|
+
* Replaces / with - and removes invalid characters.
|
|
143
|
+
*/
|
|
144
|
+
declare function sanitizeBranchName(branch: string): string;
|
|
126
145
|
/**
|
|
127
146
|
* Parse and normalize a hostname input for use as a .localhost subdomain.
|
|
128
147
|
* Strips protocol prefixes, validates characters, and appends .localhost if needed.
|
|
148
|
+
* When in a git worktree, prepends the branch name as a subdomain prefix.
|
|
129
149
|
*/
|
|
130
150
|
declare function parseHostname(input: string): string;
|
|
131
151
|
|
|
132
|
-
export { DIR_MODE, FILE_MODE, PEAKROUTE_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, chmodSafe, chmodSafeAsync, createProxyServer, escapeHtml, fixOwnership, formatUrl, isErrnoException, parseHostname };
|
|
152
|
+
export { DIR_MODE, FILE_MODE, PEAKROUTE_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, chmodSafe, chmodSafeAsync, createProxyServer, detectGitWorktree, escapeHtml, fixOwnership, formatUrl, isErrnoException, parseHostname, sanitizeBranchName };
|
package/dist/index.js
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
chmodSafe,
|
|
10
10
|
chmodSafeAsync,
|
|
11
11
|
createProxyServer,
|
|
12
|
+
detectGitWorktree,
|
|
12
13
|
escapeHtml,
|
|
13
14
|
fixOwnership,
|
|
14
15
|
formatUrl,
|
|
15
16
|
isErrnoException,
|
|
16
|
-
parseHostname
|
|
17
|
-
|
|
17
|
+
parseHostname,
|
|
18
|
+
sanitizeBranchName
|
|
19
|
+
} from "./chunk-IJJU4JLF.js";
|
|
18
20
|
export {
|
|
19
21
|
DIR_MODE,
|
|
20
22
|
FILE_MODE,
|
|
@@ -26,9 +28,11 @@ export {
|
|
|
26
28
|
chmodSafe,
|
|
27
29
|
chmodSafeAsync,
|
|
28
30
|
createProxyServer,
|
|
31
|
+
detectGitWorktree,
|
|
29
32
|
escapeHtml,
|
|
30
33
|
fixOwnership,
|
|
31
34
|
formatUrl,
|
|
32
35
|
isErrnoException,
|
|
33
|
-
parseHostname
|
|
36
|
+
parseHostname,
|
|
37
|
+
sanitizeBranchName
|
|
34
38
|
};
|