peakroute 0.5.7 → 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-IVO6GF7V.js → chunk-IJJU4JLF.js} +242 -14
- package/dist/cli.js +188 -11
- 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,23 +686,91 @@ 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"]);
|
|
694
|
+
function detectFrameworkFromPackageJson(scriptName) {
|
|
695
|
+
try {
|
|
696
|
+
const pkgPath = path2.join(process.cwd(), "package.json");
|
|
697
|
+
if (!fs2.existsSync(pkgPath)) return null;
|
|
698
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
699
|
+
const script = pkg.scripts?.[scriptName];
|
|
700
|
+
if (!script) return null;
|
|
701
|
+
for (const [framework] of Object.entries(FRAMEWORKS_NEEDING_PORT)) {
|
|
702
|
+
const regex = new RegExp(
|
|
703
|
+
`(?:^|[\\s;&|/]|cross-env(?:\\s+[^\\s]+)*\\s+)${framework}(?=\\s|$)`,
|
|
704
|
+
"i"
|
|
705
|
+
);
|
|
706
|
+
if (regex.test(script)) {
|
|
707
|
+
return framework;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
function extractScriptName(commandArgs, basename2) {
|
|
715
|
+
const runIndex = commandArgs.indexOf("run");
|
|
716
|
+
if (runIndex !== -1 && runIndex + 1 < commandArgs.length) {
|
|
717
|
+
return commandArgs[runIndex + 1];
|
|
718
|
+
}
|
|
719
|
+
if (basename2 === "yarn" || basename2 === "bun" || basename2 === "pnpm") {
|
|
720
|
+
const scriptName = commandArgs[1];
|
|
721
|
+
if (scriptName && !scriptName.startsWith("-")) {
|
|
722
|
+
return scriptName;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
function resolveFramework(commandArgs) {
|
|
637
728
|
const cmd = commandArgs[0];
|
|
638
|
-
if (!cmd) return;
|
|
729
|
+
if (!cmd) return null;
|
|
639
730
|
const basename2 = path2.basename(cmd);
|
|
640
|
-
const
|
|
641
|
-
if (
|
|
731
|
+
const directFramework = FRAMEWORKS_NEEDING_PORT[basename2];
|
|
732
|
+
if (directFramework) {
|
|
733
|
+
return { name: basename2, ...directFramework };
|
|
734
|
+
}
|
|
735
|
+
if (!PACKAGE_MANAGERS.has(basename2)) return null;
|
|
736
|
+
const scriptName = extractScriptName(commandArgs, basename2);
|
|
737
|
+
if (!scriptName) return null;
|
|
738
|
+
const detectedFramework = detectFrameworkFromPackageJson(scriptName);
|
|
739
|
+
if (!detectedFramework) return null;
|
|
740
|
+
return { name: detectedFramework, ...FRAMEWORKS_NEEDING_PORT[detectedFramework] };
|
|
741
|
+
}
|
|
742
|
+
function injectPortAndHostFlags(commandArgs, port, strictPort, framework) {
|
|
642
743
|
if (!commandArgs.includes("--port")) {
|
|
643
744
|
commandArgs.push("--port", port.toString());
|
|
644
|
-
if (
|
|
745
|
+
if (strictPort) {
|
|
645
746
|
commandArgs.push("--strictPort");
|
|
646
747
|
}
|
|
647
748
|
}
|
|
648
749
|
if (!commandArgs.includes("--host")) {
|
|
649
750
|
commandArgs.push("--host", "127.0.0.1");
|
|
650
751
|
}
|
|
752
|
+
if (framework === "react-native") {
|
|
753
|
+
process.env.RCT_METRO_PORT = port.toString();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
function injectFrameworkFlags(commandArgs, port, manualFramework) {
|
|
757
|
+
if (manualFramework === "force") {
|
|
758
|
+
injectPortAndHostFlags(commandArgs, port, false);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (manualFramework && FRAMEWORKS_NEEDING_PORT[manualFramework]) {
|
|
762
|
+
injectPortAndHostFlags(
|
|
763
|
+
commandArgs,
|
|
764
|
+
port,
|
|
765
|
+
FRAMEWORKS_NEEDING_PORT[manualFramework].strictPort,
|
|
766
|
+
manualFramework
|
|
767
|
+
);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
const framework = resolveFramework(commandArgs);
|
|
771
|
+
if (framework) {
|
|
772
|
+
injectPortAndHostFlags(commandArgs, port, framework.strictPort, framework.name);
|
|
773
|
+
}
|
|
651
774
|
}
|
|
652
775
|
function prompt(question) {
|
|
653
776
|
const rl = readline.createInterface({
|
|
@@ -662,6 +785,83 @@ function prompt(question) {
|
|
|
662
785
|
});
|
|
663
786
|
});
|
|
664
787
|
}
|
|
788
|
+
var UPDATE_CHECK_CACHE_MS = 24 * 60 * 60 * 1e3;
|
|
789
|
+
function getUpdateCachePath() {
|
|
790
|
+
return path2.join(USER_STATE_DIR, ".update-check.json");
|
|
791
|
+
}
|
|
792
|
+
function compareVersions(v1, v2) {
|
|
793
|
+
const parts1 = v1.split(".").map(Number);
|
|
794
|
+
const parts2 = v2.split(".").map(Number);
|
|
795
|
+
const maxLen = Math.max(parts1.length, parts2.length);
|
|
796
|
+
for (let i = 0; i < maxLen; i++) {
|
|
797
|
+
const a = parts1[i] || 0;
|
|
798
|
+
const b = parts2[i] || 0;
|
|
799
|
+
if (a < b) return -1;
|
|
800
|
+
if (a > b) return 1;
|
|
801
|
+
}
|
|
802
|
+
return 0;
|
|
803
|
+
}
|
|
804
|
+
async function checkForUpdate(currentVersion) {
|
|
805
|
+
if (process.env.PEAKROUTE_NO_UPDATE_CHECK) {
|
|
806
|
+
return null;
|
|
807
|
+
}
|
|
808
|
+
const cachePath = getUpdateCachePath();
|
|
809
|
+
try {
|
|
810
|
+
const cache = JSON.parse(fs2.readFileSync(cachePath, "utf-8"));
|
|
811
|
+
if (Date.now() - cache.lastCheck < UPDATE_CHECK_CACHE_MS) {
|
|
812
|
+
if (compareVersions(cache.version, currentVersion) > 0) {
|
|
813
|
+
return cache.version;
|
|
814
|
+
}
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
} catch {
|
|
818
|
+
}
|
|
819
|
+
return new Promise((resolve) => {
|
|
820
|
+
const req = https.get(
|
|
821
|
+
"https://registry.npmjs.org/peakroute/latest",
|
|
822
|
+
{
|
|
823
|
+
timeout: 3e3,
|
|
824
|
+
headers: {
|
|
825
|
+
Accept: "application/json"
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
(res) => {
|
|
829
|
+
let data = "";
|
|
830
|
+
res.on("data", (chunk) => {
|
|
831
|
+
data += chunk;
|
|
832
|
+
});
|
|
833
|
+
res.on("end", () => {
|
|
834
|
+
try {
|
|
835
|
+
const response = JSON.parse(data);
|
|
836
|
+
const latestVersion = response.version;
|
|
837
|
+
const cache = {
|
|
838
|
+
version: latestVersion,
|
|
839
|
+
lastCheck: Date.now()
|
|
840
|
+
};
|
|
841
|
+
try {
|
|
842
|
+
fs2.mkdirSync(USER_STATE_DIR, { recursive: true });
|
|
843
|
+
fs2.writeFileSync(cachePath, JSON.stringify(cache), { mode: 420 });
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
if (compareVersions(latestVersion, currentVersion) > 0) {
|
|
847
|
+
resolve(latestVersion);
|
|
848
|
+
} else {
|
|
849
|
+
resolve(null);
|
|
850
|
+
}
|
|
851
|
+
} catch {
|
|
852
|
+
resolve(null);
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
);
|
|
857
|
+
req.on("error", () => resolve(null));
|
|
858
|
+
req.on("timeout", () => {
|
|
859
|
+
req.destroy();
|
|
860
|
+
resolve(null);
|
|
861
|
+
});
|
|
862
|
+
req.setTimeout(3e3);
|
|
863
|
+
});
|
|
864
|
+
}
|
|
665
865
|
|
|
666
866
|
// src/routes.ts
|
|
667
867
|
import * as fs3 from "fs";
|
|
@@ -775,6 +975,9 @@ var RouteStore = class _RouteStore {
|
|
|
775
975
|
* is no longer alive. Stale-route cleanup is only persisted when the caller
|
|
776
976
|
* already holds the lock (i.e. inside addRoute/removeRoute) to avoid
|
|
777
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.
|
|
778
981
|
*/
|
|
779
982
|
loadRoutes(persistCleanup = false) {
|
|
780
983
|
if (!fs3.existsSync(this.routesPath)) {
|
|
@@ -794,7 +997,7 @@ var RouteStore = class _RouteStore {
|
|
|
794
997
|
return [];
|
|
795
998
|
}
|
|
796
999
|
const routes = parsed.filter(isValidRoute);
|
|
797
|
-
const alive = routes.filter((r) => this.isProcessAlive(r.pid));
|
|
1000
|
+
const alive = routes.filter((r) => r.pid === 0 || this.isProcessAlive(r.pid));
|
|
798
1001
|
if (persistCleanup && alive.length !== routes.length) {
|
|
799
1002
|
try {
|
|
800
1003
|
fs3.writeFileSync(this.routesPath, JSON.stringify(alive, null, 2), {
|
|
@@ -842,6 +1045,28 @@ var RouteStore = class _RouteStore {
|
|
|
842
1045
|
this.releaseLock();
|
|
843
1046
|
}
|
|
844
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
|
+
}
|
|
845
1070
|
};
|
|
846
1071
|
|
|
847
1072
|
export {
|
|
@@ -853,6 +1078,8 @@ export {
|
|
|
853
1078
|
isErrnoException,
|
|
854
1079
|
escapeHtml,
|
|
855
1080
|
formatUrl,
|
|
1081
|
+
detectGitWorktree,
|
|
1082
|
+
sanitizeBranchName,
|
|
856
1083
|
parseHostname,
|
|
857
1084
|
PEAKROUTE_HEADER,
|
|
858
1085
|
createProxyServer,
|
|
@@ -869,6 +1096,7 @@ export {
|
|
|
869
1096
|
spawnCommand,
|
|
870
1097
|
injectFrameworkFlags,
|
|
871
1098
|
prompt,
|
|
1099
|
+
checkForUpdate,
|
|
872
1100
|
FILE_MODE,
|
|
873
1101
|
DIR_MODE,
|
|
874
1102
|
SYSTEM_DIR_MODE,
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
PRIVILEGED_PORT_THRESHOLD,
|
|
6
6
|
RouteConflictError,
|
|
7
7
|
RouteStore,
|
|
8
|
+
checkForUpdate,
|
|
8
9
|
chmodSafe,
|
|
9
10
|
createProxyServer,
|
|
10
11
|
discoverState,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
spawnCommand,
|
|
25
26
|
waitForProxy,
|
|
26
27
|
writeTlsMarker
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-IJJU4JLF.js";
|
|
28
29
|
|
|
29
30
|
// src/cli.ts
|
|
30
31
|
import chalk from "chalk";
|
|
@@ -250,8 +251,80 @@ function loginKeychainPath() {
|
|
|
250
251
|
const home = process.env.HOME || `/Users/${process.env.USER || "unknown"}`;
|
|
251
252
|
return path.join(home, "Library", "Keychains", "login.keychain-db");
|
|
252
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
|
+
}
|
|
253
324
|
function isCATrustedLinux(stateDir) {
|
|
254
|
-
const
|
|
325
|
+
const distro = detectLinuxDistro();
|
|
326
|
+
const config = getLinuxCertConfig(distro);
|
|
327
|
+
const systemCertPath = path.join(config.certDir, config.certFilename);
|
|
255
328
|
if (!fileExists(systemCertPath)) return false;
|
|
256
329
|
try {
|
|
257
330
|
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
@@ -409,9 +482,17 @@ function trustCA(stateDir) {
|
|
|
409
482
|
);
|
|
410
483
|
return { trusted: true };
|
|
411
484
|
} else if (process.platform === "linux") {
|
|
412
|
-
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
|
+
}
|
|
413
491
|
fs.copyFileSync(caCertPath, dest);
|
|
414
|
-
execFileSync(
|
|
492
|
+
execFileSync(config.updateCommand[0], config.updateCommand.slice(1), {
|
|
493
|
+
stdio: "pipe",
|
|
494
|
+
timeout: 3e4
|
|
495
|
+
});
|
|
415
496
|
return { trusted: true };
|
|
416
497
|
} else if (IS_WINDOWS) {
|
|
417
498
|
trustCAWindows(caCertPath);
|
|
@@ -670,13 +751,14 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
670
751
|
console.log(chalk.blue.bold("\nActive routes:\n"));
|
|
671
752
|
for (const route of routes) {
|
|
672
753
|
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
754
|
+
const externalIndicator = route.pid === 0 ? chalk.gray(" [external]") : "";
|
|
673
755
|
console.log(
|
|
674
|
-
` ${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}`
|
|
675
757
|
);
|
|
676
758
|
}
|
|
677
759
|
console.log();
|
|
678
760
|
}
|
|
679
|
-
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force) {
|
|
761
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force, inject, preferredAppPort) {
|
|
680
762
|
const hostname = parseHostname(name);
|
|
681
763
|
console.log(chalk.blue.bold(`
|
|
682
764
|
peakroute
|
|
@@ -772,7 +854,7 @@ peakroute
|
|
|
772
854
|
} else {
|
|
773
855
|
console.log(chalk.gray("-- Proxy is running"));
|
|
774
856
|
}
|
|
775
|
-
const port = await findFreePort();
|
|
857
|
+
const port = await findFreePort(void 0, void 0, preferredAppPort);
|
|
776
858
|
console.log(chalk.green(`-- Using port ${port}`));
|
|
777
859
|
try {
|
|
778
860
|
store.addRoute(hostname, port, process.pid, force);
|
|
@@ -787,7 +869,7 @@ peakroute
|
|
|
787
869
|
console.log(chalk.cyan.bold(`
|
|
788
870
|
-> ${finalUrl}
|
|
789
871
|
`));
|
|
790
|
-
injectFrameworkFlags(commandArgs, port);
|
|
872
|
+
injectFrameworkFlags(commandArgs, port, inject ? "force" : void 0);
|
|
791
873
|
console.log(chalk.gray(`Running: PORT=${port} HOST=127.0.0.1 ${commandArgs.join(" ")}
|
|
792
874
|
`));
|
|
793
875
|
spawnCommand(commandArgs, {
|
|
@@ -795,6 +877,7 @@ peakroute
|
|
|
795
877
|
...process.env,
|
|
796
878
|
PORT: port.toString(),
|
|
797
879
|
HOST: "127.0.0.1",
|
|
880
|
+
PEAKROUTE_URL: finalUrl,
|
|
798
881
|
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: ".localhost"
|
|
799
882
|
},
|
|
800
883
|
onCleanup: () => {
|
|
@@ -806,6 +889,7 @@ peakroute
|
|
|
806
889
|
});
|
|
807
890
|
}
|
|
808
891
|
async function main() {
|
|
892
|
+
const updatePromise = checkForUpdate("0.6.0");
|
|
809
893
|
if (process.stdin.isTTY) {
|
|
810
894
|
process.on("exit", () => {
|
|
811
895
|
try {
|
|
@@ -815,6 +899,15 @@ async function main() {
|
|
|
815
899
|
});
|
|
816
900
|
}
|
|
817
901
|
const args = process.argv.slice(2);
|
|
902
|
+
const newerVersion = await updatePromise;
|
|
903
|
+
if (newerVersion) {
|
|
904
|
+
console.log(
|
|
905
|
+
chalk.yellow(`
|
|
906
|
+
\u2192 Update available: ${chalk.bold(newerVersion)} (current: ${"0.6.0"})`)
|
|
907
|
+
);
|
|
908
|
+
console.log(chalk.gray(` Run: npm install -g peakroute
|
|
909
|
+
`));
|
|
910
|
+
}
|
|
818
911
|
const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
|
|
819
912
|
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
820
913
|
if (isNpx || isPnpmDlx) {
|
|
@@ -847,6 +940,8 @@ ${chalk.bold("Usage:")}
|
|
|
847
940
|
${chalk.cyan("peakroute <name> <cmd>")} Run your app through the proxy
|
|
848
941
|
${chalk.cyan("peakroute list")} Show active routes
|
|
849
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
|
|
850
945
|
|
|
851
946
|
${chalk.bold("Examples:")}
|
|
852
947
|
peakroute proxy start # Start proxy on port 1355
|
|
@@ -884,9 +979,12 @@ ${chalk.bold("Options:")}
|
|
|
884
979
|
--no-tls Disable HTTPS (overrides PEAKROUTE_HTTPS)
|
|
885
980
|
--foreground Run proxy in foreground (for debugging)
|
|
886
981
|
--force Override an existing route registered by another process
|
|
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)
|
|
887
984
|
|
|
888
985
|
${chalk.bold("Environment variables:")}
|
|
889
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)
|
|
890
988
|
PEAKROUTE_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
|
|
891
989
|
PEAKROUTE_STATE_DIR=<path> Override the state directory
|
|
892
990
|
PEAKROUTE=0 | PEAKROUTE=skip Run command directly without proxy
|
|
@@ -898,7 +996,7 @@ ${chalk.bold("Skip peakroute:")}
|
|
|
898
996
|
process.exit(0);
|
|
899
997
|
}
|
|
900
998
|
if (args[0] === "--version" || args[0] === "-v") {
|
|
901
|
-
console.log("0.
|
|
999
|
+
console.log("0.6.0");
|
|
902
1000
|
process.exit(0);
|
|
903
1001
|
}
|
|
904
1002
|
if (args[0] === "trust") {
|
|
@@ -928,6 +1026,54 @@ ${chalk.bold("Skip peakroute:")}
|
|
|
928
1026
|
listRoutes(store2, port2, tls3);
|
|
929
1027
|
return;
|
|
930
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
|
+
}
|
|
931
1077
|
if (args[0] === "proxy") {
|
|
932
1078
|
if (args[1] === "stop") {
|
|
933
1079
|
const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
|
|
@@ -1133,9 +1279,40 @@ ${chalk.bold("Usage: peakroute proxy <command>")}
|
|
|
1133
1279
|
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
1134
1280
|
return;
|
|
1135
1281
|
}
|
|
1282
|
+
const parsedFlags = /* @__PURE__ */ new Set();
|
|
1136
1283
|
const forceIdx = args.indexOf("--force");
|
|
1137
1284
|
const force = forceIdx >= 0 && forceIdx <= 1;
|
|
1138
|
-
|
|
1285
|
+
if (force) parsedFlags.add(forceIdx);
|
|
1286
|
+
const injectIdx = args.indexOf("--inject");
|
|
1287
|
+
const injectFlag = injectIdx >= 0 && injectIdx <= 1;
|
|
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));
|
|
1139
1316
|
const name = appArgs[0];
|
|
1140
1317
|
const commandArgs = appArgs.slice(1);
|
|
1141
1318
|
if (commandArgs.length === 0) {
|
|
@@ -1150,7 +1327,7 @@ ${chalk.bold("Usage: peakroute proxy <command>")}
|
|
|
1150
1327
|
const store = new RouteStore(dir, {
|
|
1151
1328
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1152
1329
|
});
|
|
1153
|
-
await runApp(store, port, dir, name, commandArgs, tls2, force);
|
|
1330
|
+
await runApp(store, port, dir, name, commandArgs, tls2, force, injectFlag, preferredAppPort);
|
|
1154
1331
|
}
|
|
1155
1332
|
main().catch((err) => {
|
|
1156
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
|
};
|