peakroute 0.5.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/dist/chunk-OWNUHDR5.js +846 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1145 -0
- package/dist/index.d.ts +132 -0
- package/dist/index.js +34 -0
- package/package.json +67 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
FILE_MODE,
|
|
4
|
+
IS_WINDOWS,
|
|
5
|
+
PRIVILEGED_PORT_THRESHOLD,
|
|
6
|
+
RouteConflictError,
|
|
7
|
+
RouteStore,
|
|
8
|
+
chmodSafe,
|
|
9
|
+
createProxyServer,
|
|
10
|
+
discoverState,
|
|
11
|
+
findFreePort,
|
|
12
|
+
findPidOnPort,
|
|
13
|
+
fixOwnership,
|
|
14
|
+
formatUrl,
|
|
15
|
+
getDefaultPort,
|
|
16
|
+
injectFrameworkFlags,
|
|
17
|
+
isErrnoException,
|
|
18
|
+
isHttpsEnvEnabled,
|
|
19
|
+
isProxyRunning,
|
|
20
|
+
parseHostname,
|
|
21
|
+
prompt,
|
|
22
|
+
readTlsMarker,
|
|
23
|
+
resolveStateDir,
|
|
24
|
+
spawnCommand,
|
|
25
|
+
waitForProxy,
|
|
26
|
+
writeTlsMarker
|
|
27
|
+
} from "./chunk-OWNUHDR5.js";
|
|
28
|
+
|
|
29
|
+
// src/cli.ts
|
|
30
|
+
import chalk from "chalk";
|
|
31
|
+
import * as fs2 from "fs";
|
|
32
|
+
import * as path2 from "path";
|
|
33
|
+
import { spawn, spawnSync } from "child_process";
|
|
34
|
+
|
|
35
|
+
// src/certs.ts
|
|
36
|
+
import * as fs from "fs";
|
|
37
|
+
import * as path from "path";
|
|
38
|
+
import * as crypto from "crypto";
|
|
39
|
+
import * as tls from "tls";
|
|
40
|
+
import { execFile as execFileCb, execFileSync } from "child_process";
|
|
41
|
+
import { promisify } from "util";
|
|
42
|
+
var CA_VALIDITY_DAYS = 3650;
|
|
43
|
+
var SERVER_VALIDITY_DAYS = 365;
|
|
44
|
+
var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
45
|
+
var CA_COMMON_NAME = "peakroute Local CA";
|
|
46
|
+
var OPENSSL_TIMEOUT_MS = 15e3;
|
|
47
|
+
var CA_KEY_FILE = "ca-key.pem";
|
|
48
|
+
var CA_CERT_FILE = "ca.pem";
|
|
49
|
+
var SERVER_KEY_FILE = "server-key.pem";
|
|
50
|
+
var SERVER_CERT_FILE = "server.pem";
|
|
51
|
+
function fileExists(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
fs.accessSync(filePath, fs.constants.R_OK);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function isCertValid(certPath) {
|
|
60
|
+
try {
|
|
61
|
+
const pem = fs.readFileSync(certPath, "utf-8");
|
|
62
|
+
const cert = new crypto.X509Certificate(pem);
|
|
63
|
+
const expiry = new Date(cert.validTo).getTime();
|
|
64
|
+
return Date.now() + EXPIRY_BUFFER_MS < expiry;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isCertSignatureStrong(certPath) {
|
|
70
|
+
try {
|
|
71
|
+
const text = openssl(["x509", "-in", certPath, "-noout", "-text"]);
|
|
72
|
+
const match = text.match(/Signature Algorithm:\s*(\S+)/i);
|
|
73
|
+
if (!match) return false;
|
|
74
|
+
const algo = match[1].toLowerCase();
|
|
75
|
+
return !algo.includes("sha1");
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function openssl(args, options) {
|
|
81
|
+
try {
|
|
82
|
+
return execFileSync("openssl", args, {
|
|
83
|
+
encoding: "utf-8",
|
|
84
|
+
timeout: OPENSSL_TIMEOUT_MS,
|
|
85
|
+
input: options?.input,
|
|
86
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
87
|
+
});
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
90
|
+
throw new Error(
|
|
91
|
+
`openssl failed: ${message}
|
|
92
|
+
|
|
93
|
+
Make sure openssl is installed (ships with macOS and most Linux distributions).`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var execFileAsync = promisify(execFileCb);
|
|
98
|
+
async function opensslAsync(args) {
|
|
99
|
+
try {
|
|
100
|
+
const { stdout } = await execFileAsync("openssl", args, {
|
|
101
|
+
encoding: "utf-8",
|
|
102
|
+
timeout: OPENSSL_TIMEOUT_MS
|
|
103
|
+
});
|
|
104
|
+
return stdout;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
107
|
+
throw new Error(
|
|
108
|
+
`openssl failed: ${message}
|
|
109
|
+
|
|
110
|
+
Make sure openssl is installed (ships with macOS and most Linux distributions).`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function generateCA(stateDir) {
|
|
115
|
+
const keyPath = path.join(stateDir, CA_KEY_FILE);
|
|
116
|
+
const certPath = path.join(stateDir, CA_CERT_FILE);
|
|
117
|
+
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", keyPath]);
|
|
118
|
+
openssl([
|
|
119
|
+
"req",
|
|
120
|
+
"-new",
|
|
121
|
+
"-x509",
|
|
122
|
+
"-sha256",
|
|
123
|
+
"-key",
|
|
124
|
+
keyPath,
|
|
125
|
+
"-out",
|
|
126
|
+
certPath,
|
|
127
|
+
"-days",
|
|
128
|
+
CA_VALIDITY_DAYS.toString(),
|
|
129
|
+
"-subj",
|
|
130
|
+
`/CN=${CA_COMMON_NAME}`,
|
|
131
|
+
"-addext",
|
|
132
|
+
"basicConstraints=critical,CA:TRUE",
|
|
133
|
+
"-addext",
|
|
134
|
+
"keyUsage=critical,keyCertSign,cRLSign"
|
|
135
|
+
]);
|
|
136
|
+
fs.chmodSync(keyPath, 384);
|
|
137
|
+
fs.chmodSync(certPath, 420);
|
|
138
|
+
fixOwnership(keyPath, certPath);
|
|
139
|
+
return { certPath, keyPath };
|
|
140
|
+
}
|
|
141
|
+
function generateServerCert(stateDir) {
|
|
142
|
+
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
143
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
144
|
+
const serverKeyPath = path.join(stateDir, SERVER_KEY_FILE);
|
|
145
|
+
const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
|
|
146
|
+
const csrPath = path.join(stateDir, "server.csr");
|
|
147
|
+
const extPath = path.join(stateDir, "server-ext.cnf");
|
|
148
|
+
openssl(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", serverKeyPath]);
|
|
149
|
+
openssl(["req", "-new", "-key", serverKeyPath, "-out", csrPath, "-subj", "/CN=localhost"]);
|
|
150
|
+
fs.writeFileSync(
|
|
151
|
+
extPath,
|
|
152
|
+
[
|
|
153
|
+
"authorityKeyIdentifier=keyid,issuer",
|
|
154
|
+
"basicConstraints=CA:FALSE",
|
|
155
|
+
"keyUsage=digitalSignature,keyEncipherment",
|
|
156
|
+
"extendedKeyUsage=serverAuth",
|
|
157
|
+
"subjectAltName=DNS:localhost,DNS:*.localhost"
|
|
158
|
+
].join("\n") + "\n"
|
|
159
|
+
);
|
|
160
|
+
openssl([
|
|
161
|
+
"x509",
|
|
162
|
+
"-req",
|
|
163
|
+
"-sha256",
|
|
164
|
+
"-in",
|
|
165
|
+
csrPath,
|
|
166
|
+
"-CA",
|
|
167
|
+
caCertPath,
|
|
168
|
+
"-CAkey",
|
|
169
|
+
caKeyPath,
|
|
170
|
+
"-CAcreateserial",
|
|
171
|
+
"-out",
|
|
172
|
+
serverCertPath,
|
|
173
|
+
"-days",
|
|
174
|
+
SERVER_VALIDITY_DAYS.toString(),
|
|
175
|
+
"-extfile",
|
|
176
|
+
extPath
|
|
177
|
+
]);
|
|
178
|
+
for (const tmp of [csrPath, extPath]) {
|
|
179
|
+
try {
|
|
180
|
+
fs.unlinkSync(tmp);
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
fs.chmodSync(serverKeyPath, 384);
|
|
185
|
+
fs.chmodSync(serverCertPath, 420);
|
|
186
|
+
fixOwnership(serverKeyPath, serverCertPath);
|
|
187
|
+
return { certPath: serverCertPath, keyPath: serverKeyPath };
|
|
188
|
+
}
|
|
189
|
+
function ensureCerts(stateDir) {
|
|
190
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
191
|
+
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
192
|
+
const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
|
|
193
|
+
const serverKeyPath = path.join(stateDir, SERVER_KEY_FILE);
|
|
194
|
+
let caGenerated = false;
|
|
195
|
+
if (!fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath) || !isCertSignatureStrong(caCertPath)) {
|
|
196
|
+
generateCA(stateDir);
|
|
197
|
+
caGenerated = true;
|
|
198
|
+
}
|
|
199
|
+
if (caGenerated || !fileExists(serverCertPath) || !fileExists(serverKeyPath) || !isCertValid(serverCertPath) || !isCertSignatureStrong(serverCertPath)) {
|
|
200
|
+
generateServerCert(stateDir);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
certPath: serverCertPath,
|
|
204
|
+
keyPath: path.join(stateDir, SERVER_KEY_FILE),
|
|
205
|
+
caPath: caCertPath,
|
|
206
|
+
caGenerated
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function isCATrusted(stateDir) {
|
|
210
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
211
|
+
if (!fileExists(caCertPath)) return false;
|
|
212
|
+
if (process.platform === "darwin") {
|
|
213
|
+
return isCATrustedMacOS(caCertPath);
|
|
214
|
+
} else if (process.platform === "linux") {
|
|
215
|
+
return isCATrustedLinux(stateDir);
|
|
216
|
+
} else if (IS_WINDOWS) {
|
|
217
|
+
return isCATrustedWindows(caCertPath);
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
function isCATrustedMacOS(caCertPath) {
|
|
222
|
+
try {
|
|
223
|
+
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
224
|
+
for (const keychain of [loginKeychainPath(), "/Library/Keychains/System.keychain"]) {
|
|
225
|
+
try {
|
|
226
|
+
const result = execFileSync("security", ["find-certificate", "-a", "-Z", keychain], {
|
|
227
|
+
encoding: "utf-8",
|
|
228
|
+
timeout: 5e3,
|
|
229
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
230
|
+
});
|
|
231
|
+
if (result.toLowerCase().includes(fingerprint)) return true;
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function loginKeychainPath() {
|
|
241
|
+
try {
|
|
242
|
+
const result = execFileSync("security", ["default-keychain"], {
|
|
243
|
+
encoding: "utf-8",
|
|
244
|
+
timeout: 5e3
|
|
245
|
+
}).trim();
|
|
246
|
+
const match = result.match(/"(.+)"/);
|
|
247
|
+
if (match) return match[1];
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
const home = process.env.HOME || `/Users/${process.env.USER || "unknown"}`;
|
|
251
|
+
return path.join(home, "Library", "Keychains", "login.keychain-db");
|
|
252
|
+
}
|
|
253
|
+
function isCATrustedLinux(stateDir) {
|
|
254
|
+
const systemCertPath = `/usr/local/share/ca-certificates/peakroute-ca.crt`;
|
|
255
|
+
if (!fileExists(systemCertPath)) return false;
|
|
256
|
+
try {
|
|
257
|
+
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
258
|
+
const installed = fs.readFileSync(systemCertPath, "utf-8").trim();
|
|
259
|
+
return ours === installed;
|
|
260
|
+
} catch {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function isCATrustedWindows(caCertPath) {
|
|
265
|
+
try {
|
|
266
|
+
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
267
|
+
const result = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
268
|
+
encoding: "utf-8",
|
|
269
|
+
timeout: 5e3,
|
|
270
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
271
|
+
});
|
|
272
|
+
return result.toLowerCase().includes(fingerprint);
|
|
273
|
+
} catch {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
var HOST_CERTS_DIR = "host-certs";
|
|
278
|
+
function sanitizeHostForFilename(hostname) {
|
|
279
|
+
return hostname.replace(/\./g, "_").replace(/[^a-z0-9_-]/gi, "");
|
|
280
|
+
}
|
|
281
|
+
async function generateHostCertAsync(stateDir, hostname) {
|
|
282
|
+
const caKeyPath = path.join(stateDir, CA_KEY_FILE);
|
|
283
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
284
|
+
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
285
|
+
if (!fs.existsSync(hostDir)) {
|
|
286
|
+
await fs.promises.mkdir(hostDir, { recursive: true, mode: 493 });
|
|
287
|
+
fixOwnership(hostDir);
|
|
288
|
+
}
|
|
289
|
+
const safeName = sanitizeHostForFilename(hostname);
|
|
290
|
+
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
291
|
+
const certPath = path.join(hostDir, `${safeName}.pem`);
|
|
292
|
+
const csrPath = path.join(hostDir, `${safeName}.csr`);
|
|
293
|
+
const extPath = path.join(hostDir, `${safeName}-ext.cnf`);
|
|
294
|
+
await opensslAsync(["ecparam", "-genkey", "-name", "prime256v1", "-noout", "-out", keyPath]);
|
|
295
|
+
await opensslAsync(["req", "-new", "-key", keyPath, "-out", csrPath, "-subj", `/CN=${hostname}`]);
|
|
296
|
+
const sans = [`DNS:${hostname}`];
|
|
297
|
+
const parts = hostname.split(".");
|
|
298
|
+
if (parts.length >= 2) {
|
|
299
|
+
sans.push(`DNS:*.${parts.slice(1).join(".")}`);
|
|
300
|
+
}
|
|
301
|
+
await fs.promises.writeFile(
|
|
302
|
+
extPath,
|
|
303
|
+
[
|
|
304
|
+
"authorityKeyIdentifier=keyid,issuer",
|
|
305
|
+
"basicConstraints=CA:FALSE",
|
|
306
|
+
"keyUsage=digitalSignature,keyEncipherment",
|
|
307
|
+
"extendedKeyUsage=serverAuth",
|
|
308
|
+
`subjectAltName=${sans.join(",")}`
|
|
309
|
+
].join("\n") + "\n"
|
|
310
|
+
);
|
|
311
|
+
await opensslAsync([
|
|
312
|
+
"x509",
|
|
313
|
+
"-req",
|
|
314
|
+
"-sha256",
|
|
315
|
+
"-in",
|
|
316
|
+
csrPath,
|
|
317
|
+
"-CA",
|
|
318
|
+
caCertPath,
|
|
319
|
+
"-CAkey",
|
|
320
|
+
caKeyPath,
|
|
321
|
+
"-CAcreateserial",
|
|
322
|
+
"-out",
|
|
323
|
+
certPath,
|
|
324
|
+
"-days",
|
|
325
|
+
SERVER_VALIDITY_DAYS.toString(),
|
|
326
|
+
"-extfile",
|
|
327
|
+
extPath
|
|
328
|
+
]);
|
|
329
|
+
for (const tmp of [csrPath, extPath]) {
|
|
330
|
+
try {
|
|
331
|
+
await fs.promises.unlink(tmp);
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
await fs.promises.chmod(keyPath, 384);
|
|
336
|
+
await fs.promises.chmod(certPath, 420);
|
|
337
|
+
fixOwnership(keyPath, certPath);
|
|
338
|
+
return { certPath, keyPath };
|
|
339
|
+
}
|
|
340
|
+
function createSNICallback(stateDir, defaultCert, defaultKey) {
|
|
341
|
+
const cache = /* @__PURE__ */ new Map();
|
|
342
|
+
const pending = /* @__PURE__ */ new Map();
|
|
343
|
+
const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
|
|
344
|
+
return (servername, cb) => {
|
|
345
|
+
if (servername === "localhost") {
|
|
346
|
+
cb(null, defaultCtx);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (cache.has(servername)) {
|
|
350
|
+
cb(null, cache.get(servername));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const safeName = sanitizeHostForFilename(servername);
|
|
354
|
+
const hostDir = path.join(stateDir, HOST_CERTS_DIR);
|
|
355
|
+
const certPath = path.join(hostDir, `${safeName}.pem`);
|
|
356
|
+
const keyPath = path.join(hostDir, `${safeName}-key.pem`);
|
|
357
|
+
if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
|
|
358
|
+
try {
|
|
359
|
+
const ctx = tls.createSecureContext({
|
|
360
|
+
cert: fs.readFileSync(certPath),
|
|
361
|
+
key: fs.readFileSync(keyPath)
|
|
362
|
+
});
|
|
363
|
+
cache.set(servername, ctx);
|
|
364
|
+
cb(null, ctx);
|
|
365
|
+
return;
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (pending.has(servername)) {
|
|
370
|
+
pending.get(servername).then((ctx) => cb(null, ctx)).catch((err) => cb(err instanceof Error ? err : new Error(String(err))));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const promise = generateHostCertAsync(stateDir, servername).then(async (generated) => {
|
|
374
|
+
const [cert, key] = await Promise.all([
|
|
375
|
+
fs.promises.readFile(generated.certPath),
|
|
376
|
+
fs.promises.readFile(generated.keyPath)
|
|
377
|
+
]);
|
|
378
|
+
return tls.createSecureContext({ cert, key });
|
|
379
|
+
});
|
|
380
|
+
pending.set(servername, promise);
|
|
381
|
+
promise.then((ctx) => {
|
|
382
|
+
cache.set(servername, ctx);
|
|
383
|
+
pending.delete(servername);
|
|
384
|
+
cb(null, ctx);
|
|
385
|
+
}).catch((err) => {
|
|
386
|
+
pending.delete(servername);
|
|
387
|
+
cb(err instanceof Error ? err : new Error(String(err)));
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function trustCAWindows(caCertPath) {
|
|
392
|
+
execFileSync("certutil", ["-addstore", "-user", "Root", caCertPath], {
|
|
393
|
+
stdio: "pipe",
|
|
394
|
+
timeout: 3e4
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
function trustCA(stateDir) {
|
|
398
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
399
|
+
if (!fileExists(caCertPath)) {
|
|
400
|
+
return { trusted: false, error: "CA certificate not found. Run with --https first." };
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
if (process.platform === "darwin") {
|
|
404
|
+
const keychain = loginKeychainPath();
|
|
405
|
+
execFileSync(
|
|
406
|
+
"security",
|
|
407
|
+
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
408
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
409
|
+
);
|
|
410
|
+
return { trusted: true };
|
|
411
|
+
} else if (process.platform === "linux") {
|
|
412
|
+
const dest = "/usr/local/share/ca-certificates/peakroute-ca.crt";
|
|
413
|
+
fs.copyFileSync(caCertPath, dest);
|
|
414
|
+
execFileSync("update-ca-certificates", [], { stdio: "pipe", timeout: 3e4 });
|
|
415
|
+
return { trusted: true };
|
|
416
|
+
} else if (IS_WINDOWS) {
|
|
417
|
+
trustCAWindows(caCertPath);
|
|
418
|
+
return { trusted: true };
|
|
419
|
+
}
|
|
420
|
+
return { trusted: false, error: `Unsupported platform: ${process.platform}` };
|
|
421
|
+
} catch (err) {
|
|
422
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
423
|
+
if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES") || message.includes("Access is denied")) {
|
|
424
|
+
return {
|
|
425
|
+
trusted: false,
|
|
426
|
+
error: IS_WINDOWS ? "Permission denied. Try running as Administrator." : "Permission denied. Try: sudo peakroute trust"
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
return { trusted: false, error: message };
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/cli.ts
|
|
434
|
+
var DEBOUNCE_MS = 100;
|
|
435
|
+
var POLL_INTERVAL_MS = 3e3;
|
|
436
|
+
var EXIT_TIMEOUT_MS = 2e3;
|
|
437
|
+
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
438
|
+
function startProxyServer(store, proxyPort, tlsOptions) {
|
|
439
|
+
store.ensureDir();
|
|
440
|
+
const isTls = !!tlsOptions;
|
|
441
|
+
const routesPath = store.getRoutesPath();
|
|
442
|
+
if (!fs2.existsSync(routesPath)) {
|
|
443
|
+
fs2.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
444
|
+
}
|
|
445
|
+
chmodSafe(routesPath, FILE_MODE);
|
|
446
|
+
fixOwnership(routesPath);
|
|
447
|
+
let cachedRoutes = store.loadRoutes();
|
|
448
|
+
let debounceTimer = null;
|
|
449
|
+
let watcher = null;
|
|
450
|
+
let pollingInterval = null;
|
|
451
|
+
const reloadRoutes = () => {
|
|
452
|
+
try {
|
|
453
|
+
cachedRoutes = store.loadRoutes();
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
try {
|
|
458
|
+
watcher = fs2.watch(routesPath, () => {
|
|
459
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
460
|
+
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
461
|
+
});
|
|
462
|
+
} catch {
|
|
463
|
+
console.warn(chalk.yellow("fs.watch unavailable; falling back to polling for route changes"));
|
|
464
|
+
pollingInterval = setInterval(reloadRoutes, POLL_INTERVAL_MS);
|
|
465
|
+
}
|
|
466
|
+
const server = createProxyServer({
|
|
467
|
+
getRoutes: () => cachedRoutes,
|
|
468
|
+
proxyPort,
|
|
469
|
+
onError: (msg) => console.error(chalk.red(msg)),
|
|
470
|
+
tls: tlsOptions
|
|
471
|
+
});
|
|
472
|
+
server.on("error", (err) => {
|
|
473
|
+
if (err.code === "EADDRINUSE") {
|
|
474
|
+
console.error(chalk.red(`Port ${proxyPort} is already in use.`));
|
|
475
|
+
console.error(chalk.blue("Stop the existing proxy first:"));
|
|
476
|
+
console.error(chalk.cyan(" peakroute proxy stop"));
|
|
477
|
+
console.error(chalk.blue("Or check what is using the port:"));
|
|
478
|
+
if (IS_WINDOWS) {
|
|
479
|
+
console.error(chalk.cyan(` netstat -ano | findstr :${proxyPort}`));
|
|
480
|
+
} else {
|
|
481
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
482
|
+
}
|
|
483
|
+
} else if (err.code === "EACCES") {
|
|
484
|
+
console.error(chalk.red(`Permission denied for port ${proxyPort}.`));
|
|
485
|
+
if (IS_WINDOWS) {
|
|
486
|
+
console.error(chalk.blue("Try running as Administrator:"));
|
|
487
|
+
console.error(chalk.cyan(" peakroute proxy start -p 80"));
|
|
488
|
+
} else {
|
|
489
|
+
console.error(chalk.blue("Either run with sudo:"));
|
|
490
|
+
console.error(chalk.cyan(" sudo peakroute proxy start -p 80"));
|
|
491
|
+
console.error(chalk.blue("Or use a non-privileged port (no sudo needed):"));
|
|
492
|
+
console.error(chalk.cyan(" peakroute proxy start"));
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
console.error(chalk.red(`Proxy error: ${err.message}`));
|
|
496
|
+
}
|
|
497
|
+
process.exit(1);
|
|
498
|
+
});
|
|
499
|
+
server.listen(proxyPort, () => {
|
|
500
|
+
fs2.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
501
|
+
fs2.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
502
|
+
writeTlsMarker(store.dir, isTls);
|
|
503
|
+
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
504
|
+
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
505
|
+
console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}`));
|
|
506
|
+
});
|
|
507
|
+
let exiting = false;
|
|
508
|
+
const cleanup = () => {
|
|
509
|
+
if (exiting) return;
|
|
510
|
+
exiting = true;
|
|
511
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
512
|
+
if (pollingInterval) clearInterval(pollingInterval);
|
|
513
|
+
if (watcher) {
|
|
514
|
+
watcher.close();
|
|
515
|
+
}
|
|
516
|
+
try {
|
|
517
|
+
fs2.unlinkSync(store.pidPath);
|
|
518
|
+
} catch {
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
fs2.unlinkSync(store.portFilePath);
|
|
522
|
+
} catch {
|
|
523
|
+
}
|
|
524
|
+
writeTlsMarker(store.dir, false);
|
|
525
|
+
server.close(() => process.exit(0));
|
|
526
|
+
setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
|
|
527
|
+
};
|
|
528
|
+
process.on("SIGINT", cleanup);
|
|
529
|
+
process.on("SIGTERM", cleanup);
|
|
530
|
+
console.log(chalk.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
531
|
+
console.log(chalk.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
532
|
+
}
|
|
533
|
+
async function stopProxy(store, proxyPort, tls2) {
|
|
534
|
+
const pidPath = store.pidPath;
|
|
535
|
+
const needsSudo = !IS_WINDOWS && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
536
|
+
const sudoHint = needsSudo ? "sudo " : "";
|
|
537
|
+
if (!fs2.existsSync(pidPath)) {
|
|
538
|
+
if (await isProxyRunning(proxyPort, tls2)) {
|
|
539
|
+
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
540
|
+
const pid = findPidOnPort(proxyPort);
|
|
541
|
+
if (pid !== null) {
|
|
542
|
+
try {
|
|
543
|
+
process.kill(pid, "SIGTERM");
|
|
544
|
+
try {
|
|
545
|
+
fs2.unlinkSync(store.portFilePath);
|
|
546
|
+
} catch {
|
|
547
|
+
}
|
|
548
|
+
console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (isErrnoException(err) && err.code === "EPERM") {
|
|
551
|
+
console.error(
|
|
552
|
+
chalk.red("Permission denied. The proxy was started with elevated permissions.")
|
|
553
|
+
);
|
|
554
|
+
if (IS_WINDOWS) {
|
|
555
|
+
console.error(chalk.blue("Stop it with Administrator privileges:"));
|
|
556
|
+
console.error(chalk.cyan(" peakroute proxy stop"));
|
|
557
|
+
} else {
|
|
558
|
+
console.error(chalk.blue("Stop it with:"));
|
|
559
|
+
console.error(chalk.cyan(" sudo peakroute proxy stop"));
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
563
|
+
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
564
|
+
console.error(chalk.blue("Check if the process is still running:"));
|
|
565
|
+
if (IS_WINDOWS) {
|
|
566
|
+
console.error(chalk.cyan(` netstat -ano | findstr :${proxyPort}`));
|
|
567
|
+
} else {
|
|
568
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
} else if (!IS_WINDOWS && process.getuid?.() !== 0) {
|
|
573
|
+
console.error(chalk.red("Cannot identify the process. It may be running as root."));
|
|
574
|
+
console.error(chalk.blue("Try stopping with sudo:"));
|
|
575
|
+
console.error(chalk.cyan(" sudo peakroute proxy stop"));
|
|
576
|
+
} else {
|
|
577
|
+
console.error(chalk.red(`Could not identify the process on port ${proxyPort}.`));
|
|
578
|
+
console.error(chalk.blue("Try manually:"));
|
|
579
|
+
if (IS_WINDOWS) {
|
|
580
|
+
console.error(chalk.cyan(` taskkill /F /IM peakroute.exe`));
|
|
581
|
+
} else {
|
|
582
|
+
console.error(chalk.cyan(` sudo kill "$(lsof -ti tcp:${proxyPort})"`));
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
console.log(chalk.yellow("Proxy is not running."));
|
|
587
|
+
}
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const pid = parseInt(fs2.readFileSync(pidPath, "utf-8"), 10);
|
|
592
|
+
if (isNaN(pid)) {
|
|
593
|
+
console.error(chalk.red("Corrupted PID file. Removing it."));
|
|
594
|
+
fs2.unlinkSync(pidPath);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
process.kill(pid, 0);
|
|
599
|
+
} catch {
|
|
600
|
+
console.log(chalk.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
601
|
+
fs2.unlinkSync(pidPath);
|
|
602
|
+
try {
|
|
603
|
+
fs2.unlinkSync(store.portFilePath);
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
609
|
+
console.log(
|
|
610
|
+
chalk.yellow(
|
|
611
|
+
`PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
|
|
612
|
+
)
|
|
613
|
+
);
|
|
614
|
+
console.log(chalk.yellow("Removing stale PID file."));
|
|
615
|
+
fs2.unlinkSync(pidPath);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
process.kill(pid, "SIGTERM");
|
|
619
|
+
fs2.unlinkSync(pidPath);
|
|
620
|
+
try {
|
|
621
|
+
fs2.unlinkSync(store.portFilePath);
|
|
622
|
+
} catch {
|
|
623
|
+
}
|
|
624
|
+
console.log(chalk.green("Proxy stopped."));
|
|
625
|
+
} catch (err) {
|
|
626
|
+
if (isErrnoException(err) && err.code === "EPERM") {
|
|
627
|
+
console.error(
|
|
628
|
+
chalk.red("Permission denied. The proxy was started with elevated permissions.")
|
|
629
|
+
);
|
|
630
|
+
if (IS_WINDOWS) {
|
|
631
|
+
console.error(chalk.blue("Stop it with Administrator privileges:"));
|
|
632
|
+
console.error(chalk.cyan(" peakroute proxy stop"));
|
|
633
|
+
} else {
|
|
634
|
+
console.error(chalk.blue("Stop it with:"));
|
|
635
|
+
console.error(chalk.cyan(` ${sudoHint}peakroute proxy stop`));
|
|
636
|
+
}
|
|
637
|
+
} else {
|
|
638
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
639
|
+
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
640
|
+
console.error(chalk.blue("Check if the process is still running:"));
|
|
641
|
+
if (IS_WINDOWS) {
|
|
642
|
+
console.error(chalk.cyan(` netstat -ano | findstr :${proxyPort}`));
|
|
643
|
+
} else {
|
|
644
|
+
console.error(chalk.cyan(` lsof -ti tcp:${proxyPort}`));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
function listRoutes(store, proxyPort, tls2) {
|
|
650
|
+
const routes = store.loadRoutes();
|
|
651
|
+
if (routes.length === 0) {
|
|
652
|
+
console.log(chalk.yellow("No active routes."));
|
|
653
|
+
console.log(chalk.gray("Start an app with: peakroute <name> <command>"));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
console.log(chalk.blue.bold("\nActive routes:\n"));
|
|
657
|
+
for (const route of routes) {
|
|
658
|
+
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
659
|
+
console.log(
|
|
660
|
+
` ${chalk.cyan(url)} ${chalk.gray("->")} ${chalk.white(`localhost:${route.port}`)} ${chalk.gray(`(pid ${route.pid})`)}`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
console.log();
|
|
664
|
+
}
|
|
665
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force) {
|
|
666
|
+
const hostname = parseHostname(name);
|
|
667
|
+
console.log(chalk.blue.bold(`
|
|
668
|
+
peakroute
|
|
669
|
+
`));
|
|
670
|
+
console.log(chalk.gray(`-- ${hostname} (auto-resolves to 127.0.0.1)`));
|
|
671
|
+
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
672
|
+
const defaultPort = getDefaultPort();
|
|
673
|
+
const needsSudo = defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
674
|
+
const wantHttps = isHttpsEnvEnabled();
|
|
675
|
+
const needsElevated = !IS_WINDOWS && defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
676
|
+
if (needsElevated) {
|
|
677
|
+
if (!process.stdin.isTTY) {
|
|
678
|
+
console.error(chalk.red("Proxy is not running."));
|
|
679
|
+
if (IS_WINDOWS) {
|
|
680
|
+
console.error(chalk.blue("Start the proxy first:"));
|
|
681
|
+
console.error(chalk.cyan(" peakroute proxy start"));
|
|
682
|
+
} else {
|
|
683
|
+
console.error(chalk.blue("Start the proxy first (requires sudo for this port):"));
|
|
684
|
+
console.error(chalk.cyan(" sudo peakroute proxy start -p 80"));
|
|
685
|
+
console.error(chalk.blue("Or use the default port (no sudo needed):"));
|
|
686
|
+
console.error(chalk.cyan(" peakroute proxy start"));
|
|
687
|
+
}
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
const answer = await prompt(chalk.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
691
|
+
if (answer === "n" || answer === "no") {
|
|
692
|
+
console.log(chalk.gray("Cancelled."));
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
if (answer === "s" || answer === "skip") {
|
|
696
|
+
console.log(chalk.gray("Skipping proxy, running command directly...\n"));
|
|
697
|
+
spawnCommand(commandArgs);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
if (IS_WINDOWS) {
|
|
701
|
+
console.log(chalk.yellow("Starting proxy..."));
|
|
702
|
+
const startArgs = [process.argv[1], "proxy", "start"];
|
|
703
|
+
if (wantHttps) startArgs.push("--https");
|
|
704
|
+
const result = spawnSync(process.execPath, startArgs, {
|
|
705
|
+
stdio: "inherit",
|
|
706
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS,
|
|
707
|
+
windowsHide: true
|
|
708
|
+
});
|
|
709
|
+
if (result.status !== 0) {
|
|
710
|
+
console.error(chalk.red("Failed to start proxy."));
|
|
711
|
+
console.error(chalk.blue("Try starting it manually:"));
|
|
712
|
+
console.error(chalk.cyan(" peakroute proxy start"));
|
|
713
|
+
process.exit(1);
|
|
714
|
+
}
|
|
715
|
+
} else {
|
|
716
|
+
console.log(chalk.yellow("Starting proxy (requires sudo)..."));
|
|
717
|
+
const startArgs = [process.execPath, process.argv[1], "proxy", "start"];
|
|
718
|
+
if (wantHttps) startArgs.push("--https");
|
|
719
|
+
const result = spawnSync("sudo", startArgs, {
|
|
720
|
+
stdio: "inherit",
|
|
721
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
722
|
+
});
|
|
723
|
+
if (result.status !== 0) {
|
|
724
|
+
console.error(chalk.red("Failed to start proxy."));
|
|
725
|
+
console.error(chalk.blue("Try starting it manually:"));
|
|
726
|
+
console.error(chalk.cyan(" sudo peakroute proxy start"));
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
console.log(chalk.yellow("Starting proxy..."));
|
|
732
|
+
const startArgs = [process.argv[1], "proxy", "start"];
|
|
733
|
+
if (wantHttps) startArgs.push("--https");
|
|
734
|
+
const result = spawnSync(process.execPath, startArgs, {
|
|
735
|
+
stdio: "inherit",
|
|
736
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
737
|
+
});
|
|
738
|
+
if (result.status !== 0) {
|
|
739
|
+
console.error(chalk.red("Failed to start proxy."));
|
|
740
|
+
console.error(chalk.blue("Try starting it manually:"));
|
|
741
|
+
console.error(chalk.cyan(" peakroute proxy start"));
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const autoTls = readTlsMarker(stateDir);
|
|
746
|
+
if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
|
|
747
|
+
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
748
|
+
const logPath = path2.join(stateDir, "proxy.log");
|
|
749
|
+
console.error(chalk.blue("Try starting the proxy manually to see the error:"));
|
|
750
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}peakroute proxy start`));
|
|
751
|
+
if (fs2.existsSync(logPath)) {
|
|
752
|
+
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
753
|
+
}
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
tls2 = autoTls;
|
|
757
|
+
console.log(chalk.green("Proxy started in background"));
|
|
758
|
+
} else {
|
|
759
|
+
console.log(chalk.gray("-- Proxy is running"));
|
|
760
|
+
}
|
|
761
|
+
const port = await findFreePort();
|
|
762
|
+
console.log(chalk.green(`-- Using port ${port}`));
|
|
763
|
+
try {
|
|
764
|
+
store.addRoute(hostname, port, process.pid, force);
|
|
765
|
+
} catch (err) {
|
|
766
|
+
if (err instanceof RouteConflictError) {
|
|
767
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
throw err;
|
|
771
|
+
}
|
|
772
|
+
const finalUrl = formatUrl(hostname, proxyPort, tls2);
|
|
773
|
+
console.log(chalk.cyan.bold(`
|
|
774
|
+
-> ${finalUrl}
|
|
775
|
+
`));
|
|
776
|
+
injectFrameworkFlags(commandArgs, port);
|
|
777
|
+
console.log(chalk.gray(`Running: PORT=${port} HOST=127.0.0.1 ${commandArgs.join(" ")}
|
|
778
|
+
`));
|
|
779
|
+
spawnCommand(commandArgs, {
|
|
780
|
+
env: {
|
|
781
|
+
...process.env,
|
|
782
|
+
PORT: port.toString(),
|
|
783
|
+
HOST: "127.0.0.1",
|
|
784
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: ".localhost"
|
|
785
|
+
},
|
|
786
|
+
onCleanup: () => {
|
|
787
|
+
try {
|
|
788
|
+
store.removeRoute(hostname);
|
|
789
|
+
} catch {
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
async function main() {
|
|
795
|
+
if (process.stdin.isTTY) {
|
|
796
|
+
process.on("exit", () => {
|
|
797
|
+
try {
|
|
798
|
+
process.stdin.setRawMode(false);
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
const args = process.argv.slice(2);
|
|
804
|
+
const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
|
|
805
|
+
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
806
|
+
if (isNpx || isPnpmDlx) {
|
|
807
|
+
console.error(chalk.red("Error: peakroute should not be run via npx or pnpm dlx."));
|
|
808
|
+
console.error(chalk.blue("Install globally instead:"));
|
|
809
|
+
console.error(chalk.cyan(" npm install -g peakroute"));
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
const skipPortless = process.env.PEAKROUTE === "0" || process.env.PEAKROUTE === "skip";
|
|
813
|
+
if (skipPortless && args.length >= 2 && args[0] !== "proxy") {
|
|
814
|
+
spawnCommand(args.slice(1));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
818
|
+
console.log(`
|
|
819
|
+
${chalk.bold("peakroute")} - Replace port numbers with stable, named .localhost URLs. For humans and agents.
|
|
820
|
+
|
|
821
|
+
Eliminates port conflicts, memorizing port numbers, and cookie/storage
|
|
822
|
+
clashes by giving each dev server a stable .localhost URL.
|
|
823
|
+
|
|
824
|
+
${chalk.bold("Install:")}
|
|
825
|
+
${chalk.cyan("npm install -g peakroute")}
|
|
826
|
+
Do NOT add peakroute as a project dependency.
|
|
827
|
+
|
|
828
|
+
${chalk.bold("Usage:")}
|
|
829
|
+
${chalk.cyan("peakroute proxy start")} Start the proxy (background daemon)
|
|
830
|
+
${chalk.cyan("peakroute proxy start --https")} Start with HTTP/2 + TLS (auto-generates certs)
|
|
831
|
+
${chalk.cyan("peakroute proxy start -p 80")} Start on port 80 (requires sudo)
|
|
832
|
+
${chalk.cyan("peakroute proxy stop")} Stop the proxy
|
|
833
|
+
${chalk.cyan("peakroute <name> <cmd>")} Run your app through the proxy
|
|
834
|
+
${chalk.cyan("peakroute list")} Show active routes
|
|
835
|
+
${chalk.cyan("peakroute trust")} Add local CA to system trust store
|
|
836
|
+
|
|
837
|
+
${chalk.bold("Examples:")}
|
|
838
|
+
peakroute proxy start # Start proxy on port 1355
|
|
839
|
+
peakroute proxy start --https # Start with HTTPS/2 (faster page loads)
|
|
840
|
+
peakroute myapp next dev # -> http://myapp.localhost:1355
|
|
841
|
+
peakroute myapp vite dev # -> http://myapp.localhost:1355
|
|
842
|
+
peakroute api.myapp bun start # -> http://api.myapp.localhost:1355
|
|
843
|
+
|
|
844
|
+
${chalk.bold("In package.json:")}
|
|
845
|
+
{
|
|
846
|
+
"scripts": {
|
|
847
|
+
"dev": "peakroute myapp next dev"
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
${chalk.bold("How it works:")}
|
|
852
|
+
1. Start the proxy once (listens on port 1355 by default, no sudo needed)
|
|
853
|
+
2. Run your apps - they auto-start the proxy and register automatically
|
|
854
|
+
3. Access via http://<name>.localhost:1355
|
|
855
|
+
4. .localhost domains auto-resolve to 127.0.0.1
|
|
856
|
+
5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular) get
|
|
857
|
+
--port and --host flags injected automatically
|
|
858
|
+
|
|
859
|
+
${chalk.bold("HTTP/2 + HTTPS:")}
|
|
860
|
+
Use --https for HTTP/2 multiplexing (faster dev server page loads).
|
|
861
|
+
On first use, peakroute generates a local CA and adds it to your
|
|
862
|
+
system trust store. No browser warnings. No sudo required on macOS.
|
|
863
|
+
|
|
864
|
+
${chalk.bold("Options:")}
|
|
865
|
+
-p, --port <number> Port for the proxy to listen on (default: 1355)
|
|
866
|
+
Ports < 1024 require sudo
|
|
867
|
+
--https Enable HTTP/2 + TLS with auto-generated certs
|
|
868
|
+
--cert <path> Use a custom TLS certificate (implies --https)
|
|
869
|
+
--key <path> Use a custom TLS private key (implies --https)
|
|
870
|
+
--no-tls Disable HTTPS (overrides PEAKROUTE_HTTPS)
|
|
871
|
+
--foreground Run proxy in foreground (for debugging)
|
|
872
|
+
--force Override an existing route registered by another process
|
|
873
|
+
|
|
874
|
+
${chalk.bold("Environment variables:")}
|
|
875
|
+
PEAKROUTE_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
876
|
+
PEAKROUTE_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
|
|
877
|
+
PEAKROUTE_STATE_DIR=<path> Override the state directory
|
|
878
|
+
PEAKROUTE=0 | PEAKROUTE=skip Run command directly without proxy
|
|
879
|
+
|
|
880
|
+
${chalk.bold("Skip peakroute:")}
|
|
881
|
+
PEAKROUTE=0 bun dev # Runs command directly without proxy
|
|
882
|
+
PEAKROUTE=skip bun dev # Same as above
|
|
883
|
+
`);
|
|
884
|
+
process.exit(0);
|
|
885
|
+
}
|
|
886
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
887
|
+
console.log("0.5.0");
|
|
888
|
+
process.exit(0);
|
|
889
|
+
}
|
|
890
|
+
if (args[0] === "trust") {
|
|
891
|
+
const { dir: dir2 } = await discoverState();
|
|
892
|
+
const result = trustCA(dir2);
|
|
893
|
+
if (result.trusted) {
|
|
894
|
+
console.log(chalk.green("Local CA added to system trust store."));
|
|
895
|
+
console.log(chalk.gray("Browsers will now trust peakroute HTTPS certificates."));
|
|
896
|
+
} else {
|
|
897
|
+
console.error(chalk.red(`Failed to trust CA: ${result.error}`));
|
|
898
|
+
if (result.error?.includes("sudo")) {
|
|
899
|
+
console.error(chalk.blue("Run with sudo:"));
|
|
900
|
+
console.error(chalk.cyan(" sudo peakroute trust"));
|
|
901
|
+
} else if (result.error?.includes("Administrator")) {
|
|
902
|
+
console.error(chalk.blue("Run as Administrator:"));
|
|
903
|
+
console.error(chalk.cyan(" peakroute trust"));
|
|
904
|
+
}
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (args[0] === "list") {
|
|
910
|
+
const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
|
|
911
|
+
const store2 = new RouteStore(dir2, {
|
|
912
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
913
|
+
});
|
|
914
|
+
listRoutes(store2, port2, tls3);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
if (args[0] === "proxy") {
|
|
918
|
+
if (args[1] === "stop") {
|
|
919
|
+
const { dir: dir2, port: port2, tls: tls3 } = await discoverState();
|
|
920
|
+
const store3 = new RouteStore(dir2, {
|
|
921
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
922
|
+
});
|
|
923
|
+
await stopProxy(store3, port2, tls3);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (args[1] !== "start") {
|
|
927
|
+
console.log(`
|
|
928
|
+
${chalk.bold("Usage: peakroute proxy <command>")}
|
|
929
|
+
|
|
930
|
+
${chalk.cyan("peakroute proxy start")} Start the proxy (daemon)
|
|
931
|
+
${chalk.cyan("peakroute proxy start --https")} Start with HTTP/2 + TLS
|
|
932
|
+
${chalk.cyan("peakroute proxy start --foreground")} Start in foreground (for debugging)
|
|
933
|
+
${chalk.cyan("peakroute proxy start -p 80")} Start on port 80 (requires sudo)
|
|
934
|
+
${chalk.cyan("peakroute proxy stop")} Stop the proxy
|
|
935
|
+
`);
|
|
936
|
+
process.exit(args[1] ? 1 : 0);
|
|
937
|
+
}
|
|
938
|
+
const isForeground = args.includes("--foreground");
|
|
939
|
+
let proxyPort = getDefaultPort();
|
|
940
|
+
let portFlagIndex = args.indexOf("--port");
|
|
941
|
+
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
942
|
+
if (portFlagIndex !== -1) {
|
|
943
|
+
const portValue = args[portFlagIndex + 1];
|
|
944
|
+
if (!portValue || portValue.startsWith("-")) {
|
|
945
|
+
console.error(chalk.red("Error: --port / -p requires a port number."));
|
|
946
|
+
console.error(chalk.blue("Usage:"));
|
|
947
|
+
console.error(chalk.cyan(" peakroute proxy start -p 8080"));
|
|
948
|
+
process.exit(1);
|
|
949
|
+
}
|
|
950
|
+
proxyPort = parseInt(portValue, 10);
|
|
951
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
952
|
+
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
953
|
+
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
954
|
+
process.exit(1);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
const hasNoTls = args.includes("--no-tls");
|
|
958
|
+
const hasHttpsFlag = args.includes("--https");
|
|
959
|
+
const wantHttps = !hasNoTls && (hasHttpsFlag || isHttpsEnvEnabled());
|
|
960
|
+
let customCertPath = null;
|
|
961
|
+
let customKeyPath = null;
|
|
962
|
+
const certIdx = args.indexOf("--cert");
|
|
963
|
+
if (certIdx !== -1) {
|
|
964
|
+
customCertPath = args[certIdx + 1] || null;
|
|
965
|
+
if (!customCertPath || customCertPath.startsWith("-")) {
|
|
966
|
+
console.error(chalk.red("Error: --cert requires a file path."));
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
const keyIdx = args.indexOf("--key");
|
|
971
|
+
if (keyIdx !== -1) {
|
|
972
|
+
customKeyPath = args[keyIdx + 1] || null;
|
|
973
|
+
if (!customKeyPath || customKeyPath.startsWith("-")) {
|
|
974
|
+
console.error(chalk.red("Error: --key requires a file path."));
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (customCertPath && !customKeyPath || !customCertPath && customKeyPath) {
|
|
979
|
+
console.error(chalk.red("Error: --cert and --key must be used together."));
|
|
980
|
+
process.exit(1);
|
|
981
|
+
}
|
|
982
|
+
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
983
|
+
const stateDir = resolveStateDir(proxyPort);
|
|
984
|
+
const store2 = new RouteStore(stateDir, {
|
|
985
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
986
|
+
});
|
|
987
|
+
if (await isProxyRunning(proxyPort)) {
|
|
988
|
+
if (isForeground) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
992
|
+
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
993
|
+
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
994
|
+
console.log(
|
|
995
|
+
chalk.blue(`To restart: peakroute proxy stop && ${sudoPrefix}peakroute proxy start`)
|
|
996
|
+
);
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const needsRoot = !IS_WINDOWS && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1000
|
+
if (needsRoot && (process.getuid?.() ?? -1) !== 0) {
|
|
1001
|
+
console.error(
|
|
1002
|
+
chalk.red(`Error: Port ${proxyPort} requires ${IS_WINDOWS ? "Administrator" : "sudo"}.`)
|
|
1003
|
+
);
|
|
1004
|
+
if (!IS_WINDOWS) {
|
|
1005
|
+
console.error(chalk.blue("Either run with sudo:"));
|
|
1006
|
+
console.error(chalk.cyan(" sudo peakroute proxy start -p 80"));
|
|
1007
|
+
console.error(chalk.blue("Or use the default port (no sudo needed):"));
|
|
1008
|
+
console.error(chalk.cyan(" peakroute proxy start"));
|
|
1009
|
+
}
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
let tlsOptions;
|
|
1013
|
+
if (useHttps) {
|
|
1014
|
+
store2.ensureDir();
|
|
1015
|
+
if (customCertPath && customKeyPath) {
|
|
1016
|
+
try {
|
|
1017
|
+
const cert = fs2.readFileSync(customCertPath);
|
|
1018
|
+
const key = fs2.readFileSync(customKeyPath);
|
|
1019
|
+
const certStr = cert.toString("utf-8");
|
|
1020
|
+
const keyStr = key.toString("utf-8");
|
|
1021
|
+
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
1022
|
+
console.error(chalk.red(`Error: ${customCertPath} is not a valid PEM certificate.`));
|
|
1023
|
+
console.error(chalk.gray("Expected a file starting with -----BEGIN CERTIFICATE-----"));
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
|
|
1027
|
+
console.error(chalk.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
|
|
1028
|
+
console.error(
|
|
1029
|
+
chalk.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----")
|
|
1030
|
+
);
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
tlsOptions = { cert, key };
|
|
1034
|
+
} catch (err) {
|
|
1035
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1036
|
+
console.error(chalk.red(`Error reading certificate files: ${message}`));
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
} else {
|
|
1040
|
+
console.log(chalk.gray("Ensuring TLS certificates..."));
|
|
1041
|
+
const certs = ensureCerts(stateDir);
|
|
1042
|
+
if (certs.caGenerated) {
|
|
1043
|
+
console.log(chalk.green("Generated local CA certificate."));
|
|
1044
|
+
}
|
|
1045
|
+
if (!isCATrusted(stateDir)) {
|
|
1046
|
+
console.log(chalk.yellow("Adding CA to system trust store..."));
|
|
1047
|
+
const trustResult = trustCA(stateDir);
|
|
1048
|
+
if (trustResult.trusted) {
|
|
1049
|
+
console.log(
|
|
1050
|
+
chalk.green("CA added to system trust store. Browsers will trust peakroute certs.")
|
|
1051
|
+
);
|
|
1052
|
+
} else {
|
|
1053
|
+
console.warn(chalk.yellow("Could not add CA to system trust store."));
|
|
1054
|
+
if (trustResult.error) {
|
|
1055
|
+
console.warn(chalk.gray(trustResult.error));
|
|
1056
|
+
}
|
|
1057
|
+
console.warn(
|
|
1058
|
+
chalk.yellow("Browsers will show certificate warnings. To fix this later, run:")
|
|
1059
|
+
);
|
|
1060
|
+
console.warn(chalk.cyan(" peakroute trust"));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const cert = fs2.readFileSync(certs.certPath);
|
|
1064
|
+
const key = fs2.readFileSync(certs.keyPath);
|
|
1065
|
+
tlsOptions = {
|
|
1066
|
+
cert,
|
|
1067
|
+
key,
|
|
1068
|
+
SNICallback: createSNICallback(stateDir, cert, key)
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (isForeground) {
|
|
1073
|
+
console.log(chalk.blue.bold("\npeakroute proxy\n"));
|
|
1074
|
+
startProxyServer(store2, proxyPort, tlsOptions);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
store2.ensureDir();
|
|
1078
|
+
const logPath = path2.join(stateDir, "proxy.log");
|
|
1079
|
+
const logFd = fs2.openSync(logPath, "a");
|
|
1080
|
+
try {
|
|
1081
|
+
try {
|
|
1082
|
+
fs2.chmodSync(logPath, FILE_MODE);
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
fixOwnership(logPath);
|
|
1086
|
+
const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
|
|
1087
|
+
if (portFlagIndex !== -1) {
|
|
1088
|
+
daemonArgs.push("--port", proxyPort.toString());
|
|
1089
|
+
}
|
|
1090
|
+
if (useHttps) {
|
|
1091
|
+
if (customCertPath && customKeyPath) {
|
|
1092
|
+
daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1093
|
+
} else {
|
|
1094
|
+
daemonArgs.push("--https");
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
const child = spawn(process.execPath, daemonArgs, {
|
|
1098
|
+
detached: true,
|
|
1099
|
+
stdio: ["ignore", logFd, logFd],
|
|
1100
|
+
env: process.env,
|
|
1101
|
+
windowsHide: true
|
|
1102
|
+
// PR #6: Evita popup de console no Windows
|
|
1103
|
+
});
|
|
1104
|
+
child.unref();
|
|
1105
|
+
} finally {
|
|
1106
|
+
fs2.closeSync(logFd);
|
|
1107
|
+
}
|
|
1108
|
+
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1109
|
+
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1110
|
+
console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
|
|
1111
|
+
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1112
|
+
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}peakroute proxy start --foreground`));
|
|
1113
|
+
if (fs2.existsSync(logPath)) {
|
|
1114
|
+
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
1115
|
+
}
|
|
1116
|
+
process.exit(1);
|
|
1117
|
+
}
|
|
1118
|
+
const proto = useHttps ? "HTTPS/2" : "HTTP";
|
|
1119
|
+
console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
const forceIdx = args.indexOf("--force");
|
|
1123
|
+
const force = forceIdx >= 0 && forceIdx <= 1;
|
|
1124
|
+
const appArgs = force ? [...args.slice(0, forceIdx), ...args.slice(forceIdx + 1)] : args;
|
|
1125
|
+
const name = appArgs[0];
|
|
1126
|
+
const commandArgs = appArgs.slice(1);
|
|
1127
|
+
if (commandArgs.length === 0) {
|
|
1128
|
+
console.error(chalk.red("Error: No command provided."));
|
|
1129
|
+
console.error(chalk.blue("Usage:"));
|
|
1130
|
+
console.error(chalk.cyan(" peakroute <name> <command...>"));
|
|
1131
|
+
console.error(chalk.blue("Example:"));
|
|
1132
|
+
console.error(chalk.cyan(" peakroute myapp next dev"));
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1136
|
+
const store = new RouteStore(dir, {
|
|
1137
|
+
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1138
|
+
});
|
|
1139
|
+
await runApp(store, port, dir, name, commandArgs, tls2, force);
|
|
1140
|
+
}
|
|
1141
|
+
main().catch((err) => {
|
|
1142
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1143
|
+
console.error(chalk.red("Error:"), message);
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
});
|