portless 0.4.1 → 0.4.2

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/cli.js CHANGED
@@ -1,19 +1,34 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FILE_MODE,
4
- PORTLESS_HEADER,
4
+ PRIVILEGED_PORT_THRESHOLD,
5
+ RouteConflictError,
5
6
  RouteStore,
6
7
  createProxyServer,
8
+ discoverState,
9
+ findFreePort,
10
+ findPidOnPort,
11
+ fixOwnership,
7
12
  formatUrl,
13
+ getDefaultPort,
14
+ injectFrameworkFlags,
8
15
  isErrnoException,
9
- parseHostname
10
- } from "./chunk-VRBD6YAY.js";
16
+ isHttpsEnvEnabled,
17
+ isProxyRunning,
18
+ parseHostname,
19
+ prompt,
20
+ readTlsMarker,
21
+ resolveStateDir,
22
+ spawnCommand,
23
+ waitForProxy,
24
+ writeTlsMarker
25
+ } from "./chunk-JMRTQAVX.js";
11
26
 
12
27
  // src/cli.ts
13
28
  import chalk from "chalk";
14
- import * as fs3 from "fs";
15
- import * as path3 from "path";
16
- import { spawn as spawn2, spawnSync } from "child_process";
29
+ import * as fs2 from "fs";
30
+ import * as path2 from "path";
31
+ import { spawn, spawnSync } from "child_process";
17
32
 
18
33
  // src/certs.ts
19
34
  import * as fs from "fs";
@@ -23,17 +38,6 @@ import * as tls from "tls";
23
38
  import { execFile as execFileCb, execFileSync } from "child_process";
24
39
  import { promisify } from "util";
25
40
  var CA_VALIDITY_DAYS = 3650;
26
- function fixOwnership(...paths) {
27
- const uid = process.env.SUDO_UID;
28
- const gid = process.env.SUDO_GID;
29
- if (!uid || process.getuid?.() !== 0) return;
30
- for (const p of paths) {
31
- try {
32
- fs.chownSync(p, parseInt(uid, 10), parseInt(gid || uid, 10));
33
- } catch {
34
- }
35
- }
36
- }
37
41
  var SERVER_VALIDITY_DAYS = 365;
38
42
  var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
39
43
  var CA_COMMON_NAME = "portless Local CA";
@@ -60,6 +64,17 @@ function isCertValid(certPath) {
60
64
  return false;
61
65
  }
62
66
  }
67
+ function isCertSignatureStrong(certPath) {
68
+ try {
69
+ const text = openssl(["x509", "-in", certPath, "-noout", "-text"]);
70
+ const match = text.match(/Signature Algorithm:\s*(\S+)/i);
71
+ if (!match) return false;
72
+ const algo = match[1].toLowerCase();
73
+ return !algo.includes("sha1");
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
63
78
  function openssl(args, options) {
64
79
  try {
65
80
  return execFileSync("openssl", args, {
@@ -102,6 +117,7 @@ function generateCA(stateDir) {
102
117
  "req",
103
118
  "-new",
104
119
  "-x509",
120
+ "-sha256",
105
121
  "-key",
106
122
  keyPath,
107
123
  "-out",
@@ -142,6 +158,7 @@ function generateServerCert(stateDir) {
142
158
  openssl([
143
159
  "x509",
144
160
  "-req",
161
+ "-sha256",
145
162
  "-in",
146
163
  csrPath,
147
164
  "-CA",
@@ -171,12 +188,13 @@ function ensureCerts(stateDir) {
171
188
  const caCertPath = path.join(stateDir, CA_CERT_FILE);
172
189
  const caKeyPath = path.join(stateDir, CA_KEY_FILE);
173
190
  const serverCertPath = path.join(stateDir, SERVER_CERT_FILE);
191
+ const serverKeyPath = path.join(stateDir, SERVER_KEY_FILE);
174
192
  let caGenerated = false;
175
- if (!fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath)) {
193
+ if (!fileExists(caCertPath) || !fileExists(caKeyPath) || !isCertValid(caCertPath) || !isCertSignatureStrong(caCertPath)) {
176
194
  generateCA(stateDir);
177
195
  caGenerated = true;
178
196
  }
179
- if (caGenerated || !fileExists(serverCertPath) || !isCertValid(serverCertPath)) {
197
+ if (caGenerated || !fileExists(serverCertPath) || !fileExists(serverKeyPath) || !isCertValid(serverCertPath) || !isCertSignatureStrong(serverCertPath)) {
180
198
  generateServerCert(stateDir);
181
199
  }
182
200
  return {
@@ -276,6 +294,7 @@ async function generateHostCertAsync(stateDir, hostname) {
276
294
  await opensslAsync([
277
295
  "x509",
278
296
  "-req",
297
+ "-sha256",
279
298
  "-in",
280
299
  csrPath,
281
300
  "-CA",
@@ -301,16 +320,12 @@ async function generateHostCertAsync(stateDir, hostname) {
301
320
  fixOwnership(keyPath, certPath);
302
321
  return { certPath, keyPath };
303
322
  }
304
- function isSimpleLocalhostSubdomain(hostname) {
305
- const parts = hostname.split(".");
306
- return parts.length === 2 && parts[1] === "localhost";
307
- }
308
323
  function createSNICallback(stateDir, defaultCert, defaultKey) {
309
324
  const cache = /* @__PURE__ */ new Map();
310
325
  const pending = /* @__PURE__ */ new Map();
311
326
  const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
312
327
  return (servername, cb) => {
313
- if (servername === "localhost" || isSimpleLocalhostSubdomain(servername)) {
328
+ if (servername === "localhost") {
314
329
  cb(null, defaultCtx);
315
330
  return;
316
331
  }
@@ -322,7 +337,7 @@ function createSNICallback(stateDir, defaultCert, defaultKey) {
322
337
  const hostDir = path.join(stateDir, HOST_CERTS_DIR);
323
338
  const certPath = path.join(hostDir, `${safeName}.pem`);
324
339
  const keyPath = path.join(hostDir, `${safeName}-key.pem`);
325
- if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath)) {
340
+ if (fileExists(certPath) && fileExists(keyPath) && isCertValid(certPath) && isCertSignatureStrong(certPath)) {
326
341
  try {
327
342
  const ctx = tls.createSecureContext({
328
343
  cert: fs.readFileSync(certPath),
@@ -389,252 +404,6 @@ function trustCA(stateDir) {
389
404
  }
390
405
  }
391
406
 
392
- // src/cli-utils.ts
393
- import * as fs2 from "fs";
394
- import * as http from "http";
395
- import * as https from "https";
396
- import * as net from "net";
397
- import * as os from "os";
398
- import * as path2 from "path";
399
- import * as readline from "readline";
400
- import { execSync, spawn } from "child_process";
401
- var DEFAULT_PROXY_PORT = 1355;
402
- var PRIVILEGED_PORT_THRESHOLD = 1024;
403
- var SYSTEM_STATE_DIR = "/tmp/portless";
404
- var USER_STATE_DIR = path2.join(os.homedir(), ".portless");
405
- var MIN_APP_PORT = 4e3;
406
- var MAX_APP_PORT = 4999;
407
- var RANDOM_PORT_ATTEMPTS = 50;
408
- var SOCKET_TIMEOUT_MS = 500;
409
- var LSOF_TIMEOUT_MS = 5e3;
410
- var WAIT_FOR_PROXY_MAX_ATTEMPTS = 20;
411
- var WAIT_FOR_PROXY_INTERVAL_MS = 250;
412
- var SIGNAL_CODES = {
413
- SIGHUP: 1,
414
- SIGINT: 2,
415
- SIGQUIT: 3,
416
- SIGABRT: 6,
417
- SIGKILL: 9,
418
- SIGTERM: 15
419
- };
420
- function getDefaultPort() {
421
- const envPort = process.env.PORTLESS_PORT;
422
- if (envPort) {
423
- const port = parseInt(envPort, 10);
424
- if (!isNaN(port) && port >= 1 && port <= 65535) return port;
425
- }
426
- return DEFAULT_PROXY_PORT;
427
- }
428
- function resolveStateDir(port) {
429
- if (process.env.PORTLESS_STATE_DIR) return process.env.PORTLESS_STATE_DIR;
430
- return port < PRIVILEGED_PORT_THRESHOLD ? SYSTEM_STATE_DIR : USER_STATE_DIR;
431
- }
432
- function readPortFromDir(dir) {
433
- try {
434
- const raw = fs2.readFileSync(path2.join(dir, "proxy.port"), "utf-8").trim();
435
- const port = parseInt(raw, 10);
436
- return isNaN(port) ? null : port;
437
- } catch {
438
- return null;
439
- }
440
- }
441
- var TLS_MARKER_FILE = "proxy.tls";
442
- function readTlsMarker(dir) {
443
- try {
444
- return fs2.existsSync(path2.join(dir, TLS_MARKER_FILE));
445
- } catch {
446
- return false;
447
- }
448
- }
449
- function writeTlsMarker(dir, enabled) {
450
- const markerPath = path2.join(dir, TLS_MARKER_FILE);
451
- if (enabled) {
452
- fs2.writeFileSync(markerPath, "1", { mode: 420 });
453
- } else {
454
- try {
455
- fs2.unlinkSync(markerPath);
456
- } catch {
457
- }
458
- }
459
- }
460
- function isHttpsEnvEnabled() {
461
- const val = process.env.PORTLESS_HTTPS;
462
- return val === "1" || val === "true";
463
- }
464
- async function discoverState() {
465
- if (process.env.PORTLESS_STATE_DIR) {
466
- const dir = process.env.PORTLESS_STATE_DIR;
467
- const port = readPortFromDir(dir) ?? getDefaultPort();
468
- const tls2 = readTlsMarker(dir);
469
- return { dir, port, tls: tls2 };
470
- }
471
- const userPort = readPortFromDir(USER_STATE_DIR);
472
- if (userPort !== null) {
473
- const tls2 = readTlsMarker(USER_STATE_DIR);
474
- if (await isProxyRunning(userPort, tls2)) {
475
- return { dir: USER_STATE_DIR, port: userPort, tls: tls2 };
476
- }
477
- }
478
- const systemPort = readPortFromDir(SYSTEM_STATE_DIR);
479
- if (systemPort !== null) {
480
- const tls2 = readTlsMarker(SYSTEM_STATE_DIR);
481
- if (await isProxyRunning(systemPort, tls2)) {
482
- return { dir: SYSTEM_STATE_DIR, port: systemPort, tls: tls2 };
483
- }
484
- }
485
- const defaultPort = getDefaultPort();
486
- return { dir: resolveStateDir(defaultPort), port: defaultPort, tls: false };
487
- }
488
- async function findFreePort(minPort = MIN_APP_PORT, maxPort = MAX_APP_PORT) {
489
- if (minPort > maxPort) {
490
- throw new Error(`minPort (${minPort}) must be <= maxPort (${maxPort})`);
491
- }
492
- const tryPort = (port) => {
493
- return new Promise((resolve) => {
494
- const server = net.createServer();
495
- server.listen(port, () => {
496
- server.close(() => resolve(true));
497
- });
498
- server.on("error", () => resolve(false));
499
- });
500
- };
501
- for (let i = 0; i < RANDOM_PORT_ATTEMPTS; i++) {
502
- const port = minPort + Math.floor(Math.random() * (maxPort - minPort + 1));
503
- if (await tryPort(port)) {
504
- return port;
505
- }
506
- }
507
- for (let port = minPort; port <= maxPort; port++) {
508
- if (await tryPort(port)) {
509
- return port;
510
- }
511
- }
512
- throw new Error(`No free port found in range ${minPort}-${maxPort}`);
513
- }
514
- function isProxyRunning(port, tls2 = false) {
515
- return new Promise((resolve) => {
516
- const requestFn = tls2 ? https.request : http.request;
517
- const req = requestFn(
518
- {
519
- hostname: "127.0.0.1",
520
- port,
521
- path: "/",
522
- method: "HEAD",
523
- timeout: SOCKET_TIMEOUT_MS,
524
- ...tls2 ? { rejectUnauthorized: false } : {}
525
- },
526
- (res) => {
527
- res.resume();
528
- resolve(res.headers[PORTLESS_HEADER.toLowerCase()] === "1");
529
- }
530
- );
531
- req.on("error", () => resolve(false));
532
- req.on("timeout", () => {
533
- req.destroy();
534
- resolve(false);
535
- });
536
- req.end();
537
- });
538
- }
539
- function findPidOnPort(port) {
540
- try {
541
- const output = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
542
- encoding: "utf-8",
543
- timeout: LSOF_TIMEOUT_MS
544
- });
545
- const pid = parseInt(output.trim().split("\n")[0], 10);
546
- return isNaN(pid) ? null : pid;
547
- } catch {
548
- return null;
549
- }
550
- }
551
- async function waitForProxy(port, maxAttempts = WAIT_FOR_PROXY_MAX_ATTEMPTS, intervalMs = WAIT_FOR_PROXY_INTERVAL_MS, tls2 = false) {
552
- for (let i = 0; i < maxAttempts; i++) {
553
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
554
- if (await isProxyRunning(port, tls2)) {
555
- return true;
556
- }
557
- }
558
- return false;
559
- }
560
- function spawnCommand(commandArgs, options) {
561
- const child = spawn(commandArgs[0], commandArgs.slice(1), {
562
- stdio: "inherit",
563
- env: options?.env
564
- });
565
- let exiting = false;
566
- const cleanup = () => {
567
- process.removeListener("SIGINT", onSigInt);
568
- process.removeListener("SIGTERM", onSigTerm);
569
- options?.onCleanup?.();
570
- };
571
- const handleSignal = (signal) => {
572
- if (exiting) return;
573
- exiting = true;
574
- child.kill(signal);
575
- cleanup();
576
- process.exit(128 + (SIGNAL_CODES[signal] || 15));
577
- };
578
- const onSigInt = () => handleSignal("SIGINT");
579
- const onSigTerm = () => handleSignal("SIGTERM");
580
- process.on("SIGINT", onSigInt);
581
- process.on("SIGTERM", onSigTerm);
582
- child.on("error", (err) => {
583
- if (exiting) return;
584
- exiting = true;
585
- console.error(`Failed to run command: ${err.message}`);
586
- if (err.code === "ENOENT") {
587
- console.error(`Is "${commandArgs[0]}" installed and in your PATH?`);
588
- }
589
- cleanup();
590
- process.exit(1);
591
- });
592
- child.on("exit", (code, signal) => {
593
- if (exiting) return;
594
- exiting = true;
595
- cleanup();
596
- if (signal) {
597
- process.exit(128 + (SIGNAL_CODES[signal] || 15));
598
- }
599
- process.exit(code ?? 1);
600
- });
601
- }
602
- var FRAMEWORKS_NEEDING_PORT = {
603
- vite: { strictPort: true },
604
- "react-router": { strictPort: true },
605
- astro: { strictPort: false },
606
- ng: { strictPort: false }
607
- };
608
- function injectFrameworkFlags(commandArgs, port) {
609
- const cmd = commandArgs[0];
610
- if (!cmd) return;
611
- const basename2 = path2.basename(cmd);
612
- const framework = FRAMEWORKS_NEEDING_PORT[basename2];
613
- if (!framework) return;
614
- if (!commandArgs.includes("--port")) {
615
- commandArgs.push("--port", port.toString());
616
- if (framework.strictPort) {
617
- commandArgs.push("--strictPort");
618
- }
619
- }
620
- if (!commandArgs.includes("--host")) {
621
- commandArgs.push("--host", "127.0.0.1");
622
- }
623
- }
624
- function prompt(question) {
625
- const rl = readline.createInterface({
626
- input: process.stdin,
627
- output: process.stdout
628
- });
629
- return new Promise((resolve) => {
630
- rl.on("close", () => resolve(""));
631
- rl.question(question, (answer) => {
632
- rl.close();
633
- resolve(answer.trim().toLowerCase());
634
- });
635
- });
636
- }
637
-
638
407
  // src/cli.ts
639
408
  var DEBOUNCE_MS = 100;
640
409
  var POLL_INTERVAL_MS = 3e3;
@@ -644,13 +413,14 @@ function startProxyServer(store, proxyPort, tlsOptions) {
644
413
  store.ensureDir();
645
414
  const isTls = !!tlsOptions;
646
415
  const routesPath = store.getRoutesPath();
647
- if (!fs3.existsSync(routesPath)) {
648
- fs3.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
416
+ if (!fs2.existsSync(routesPath)) {
417
+ fs2.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
649
418
  }
650
419
  try {
651
- fs3.chmodSync(routesPath, FILE_MODE);
420
+ fs2.chmodSync(routesPath, FILE_MODE);
652
421
  } catch {
653
422
  }
423
+ fixOwnership(routesPath);
654
424
  let cachedRoutes = store.loadRoutes();
655
425
  let debounceTimer = null;
656
426
  let watcher = null;
@@ -662,7 +432,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
662
432
  }
663
433
  };
664
434
  try {
665
- watcher = fs3.watch(routesPath, () => {
435
+ watcher = fs2.watch(routesPath, () => {
666
436
  if (debounceTimer) clearTimeout(debounceTimer);
667
437
  debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
668
438
  });
@@ -695,9 +465,10 @@ function startProxyServer(store, proxyPort, tlsOptions) {
695
465
  process.exit(1);
696
466
  });
697
467
  server.listen(proxyPort, () => {
698
- fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
699
- fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
468
+ fs2.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
469
+ fs2.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
700
470
  writeTlsMarker(store.dir, isTls);
471
+ fixOwnership(store.dir, store.pidPath, store.portFilePath);
701
472
  const proto = isTls ? "HTTPS/2" : "HTTP";
702
473
  console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}`));
703
474
  });
@@ -711,11 +482,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
711
482
  watcher.close();
712
483
  }
713
484
  try {
714
- fs3.unlinkSync(store.pidPath);
485
+ fs2.unlinkSync(store.pidPath);
715
486
  } catch {
716
487
  }
717
488
  try {
718
- fs3.unlinkSync(store.portFilePath);
489
+ fs2.unlinkSync(store.portFilePath);
719
490
  } catch {
720
491
  }
721
492
  writeTlsMarker(store.dir, false);
@@ -731,7 +502,7 @@ async function stopProxy(store, proxyPort, tls2) {
731
502
  const pidPath = store.pidPath;
732
503
  const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
733
504
  const sudoHint = needsSudo ? "sudo " : "";
734
- if (!fs3.existsSync(pidPath)) {
505
+ if (!fs2.existsSync(pidPath)) {
735
506
  if (await isProxyRunning(proxyPort, tls2)) {
736
507
  console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
737
508
  const pid = findPidOnPort(proxyPort);
@@ -739,7 +510,7 @@ async function stopProxy(store, proxyPort, tls2) {
739
510
  try {
740
511
  process.kill(pid, "SIGTERM");
741
512
  try {
742
- fs3.unlinkSync(store.portFilePath);
513
+ fs2.unlinkSync(store.portFilePath);
743
514
  } catch {
744
515
  }
745
516
  console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
@@ -770,19 +541,19 @@ async function stopProxy(store, proxyPort, tls2) {
770
541
  return;
771
542
  }
772
543
  try {
773
- const pid = parseInt(fs3.readFileSync(pidPath, "utf-8"), 10);
544
+ const pid = parseInt(fs2.readFileSync(pidPath, "utf-8"), 10);
774
545
  if (isNaN(pid)) {
775
546
  console.error(chalk.red("Corrupted PID file. Removing it."));
776
- fs3.unlinkSync(pidPath);
547
+ fs2.unlinkSync(pidPath);
777
548
  return;
778
549
  }
779
550
  try {
780
551
  process.kill(pid, 0);
781
552
  } catch {
782
553
  console.log(chalk.yellow("Proxy process is no longer running. Cleaning up stale files."));
783
- fs3.unlinkSync(pidPath);
554
+ fs2.unlinkSync(pidPath);
784
555
  try {
785
- fs3.unlinkSync(store.portFilePath);
556
+ fs2.unlinkSync(store.portFilePath);
786
557
  } catch {
787
558
  }
788
559
  return;
@@ -794,13 +565,13 @@ async function stopProxy(store, proxyPort, tls2) {
794
565
  )
795
566
  );
796
567
  console.log(chalk.yellow("Removing stale PID file."));
797
- fs3.unlinkSync(pidPath);
568
+ fs2.unlinkSync(pidPath);
798
569
  return;
799
570
  }
800
571
  process.kill(pid, "SIGTERM");
801
- fs3.unlinkSync(pidPath);
572
+ fs2.unlinkSync(pidPath);
802
573
  try {
803
- fs3.unlinkSync(store.portFilePath);
574
+ fs2.unlinkSync(store.portFilePath);
804
575
  } catch {
805
576
  }
806
577
  console.log(chalk.green("Proxy stopped."));
@@ -833,7 +604,7 @@ function listRoutes(store, proxyPort, tls2) {
833
604
  }
834
605
  console.log();
835
606
  }
836
- async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2) {
607
+ async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force) {
837
608
  const hostname = parseHostname(name);
838
609
  console.log(chalk.blue.bold(`
839
610
  portless
@@ -893,10 +664,10 @@ portless
893
664
  const autoTls = readTlsMarker(stateDir);
894
665
  if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
895
666
  console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
896
- const logPath = path3.join(stateDir, "proxy.log");
667
+ const logPath = path2.join(stateDir, "proxy.log");
897
668
  console.error(chalk.blue("Try starting the proxy manually to see the error:"));
898
669
  console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
899
- if (fs3.existsSync(logPath)) {
670
+ if (fs2.existsSync(logPath)) {
900
671
  console.error(chalk.gray(`Logs: ${logPath}`));
901
672
  }
902
673
  process.exit(1);
@@ -908,7 +679,15 @@ portless
908
679
  }
909
680
  const port = await findFreePort();
910
681
  console.log(chalk.green(`-- Using port ${port}`));
911
- store.addRoute(hostname, port, process.pid);
682
+ try {
683
+ store.addRoute(hostname, port, process.pid, force);
684
+ } catch (err) {
685
+ if (err instanceof RouteConflictError) {
686
+ console.error(chalk.red(`Error: ${err.message}`));
687
+ process.exit(1);
688
+ }
689
+ throw err;
690
+ }
912
691
  const finalUrl = formatUrl(hostname, proxyPort, tls2);
913
692
  console.log(chalk.cyan.bold(`
914
693
  -> ${finalUrl}
@@ -932,6 +711,14 @@ portless
932
711
  });
933
712
  }
934
713
  async function main() {
714
+ if (process.stdin.isTTY) {
715
+ process.on("exit", () => {
716
+ try {
717
+ process.stdin.setRawMode(false);
718
+ } catch {
719
+ }
720
+ });
721
+ }
935
722
  const args = process.argv.slice(2);
936
723
  const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
937
724
  const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
@@ -1001,6 +788,7 @@ ${chalk.bold("Options:")}
1001
788
  --key <path> Use a custom TLS private key (implies --https)
1002
789
  --no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
1003
790
  --foreground Run proxy in foreground (for debugging)
791
+ --force Override an existing route registered by another process
1004
792
 
1005
793
  ${chalk.bold("Environment variables:")}
1006
794
  PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
@@ -1015,7 +803,7 @@ ${chalk.bold("Skip portless:")}
1015
803
  process.exit(0);
1016
804
  }
1017
805
  if (args[0] === "--version" || args[0] === "-v") {
1018
- console.log("0.4.1");
806
+ console.log("0.4.2");
1019
807
  process.exit(0);
1020
808
  }
1021
809
  if (args[0] === "trust") {
@@ -1137,8 +925,8 @@ ${chalk.bold("Usage: portless proxy <command>")}
1137
925
  store2.ensureDir();
1138
926
  if (customCertPath && customKeyPath) {
1139
927
  try {
1140
- const cert = fs3.readFileSync(customCertPath);
1141
- const key = fs3.readFileSync(customKeyPath);
928
+ const cert = fs2.readFileSync(customCertPath);
929
+ const key = fs2.readFileSync(customKeyPath);
1142
930
  const certStr = cert.toString("utf-8");
1143
931
  const keyStr = key.toString("utf-8");
1144
932
  if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
@@ -1183,8 +971,8 @@ ${chalk.bold("Usage: portless proxy <command>")}
1183
971
  console.warn(chalk.cyan(" portless trust"));
1184
972
  }
1185
973
  }
1186
- const cert = fs3.readFileSync(certs.certPath);
1187
- const key = fs3.readFileSync(certs.keyPath);
974
+ const cert = fs2.readFileSync(certs.certPath);
975
+ const key = fs2.readFileSync(certs.keyPath);
1188
976
  tlsOptions = {
1189
977
  cert,
1190
978
  key,
@@ -1198,13 +986,14 @@ ${chalk.bold("Usage: portless proxy <command>")}
1198
986
  return;
1199
987
  }
1200
988
  store2.ensureDir();
1201
- const logPath = path3.join(stateDir, "proxy.log");
1202
- const logFd = fs3.openSync(logPath, "a");
989
+ const logPath = path2.join(stateDir, "proxy.log");
990
+ const logFd = fs2.openSync(logPath, "a");
1203
991
  try {
1204
992
  try {
1205
- fs3.chmodSync(logPath, FILE_MODE);
993
+ fs2.chmodSync(logPath, FILE_MODE);
1206
994
  } catch {
1207
995
  }
996
+ fixOwnership(logPath);
1208
997
  const daemonArgs = [process.argv[1], "proxy", "start", "--foreground"];
1209
998
  if (portFlagIndex !== -1) {
1210
999
  daemonArgs.push("--port", proxyPort.toString());
@@ -1216,21 +1005,21 @@ ${chalk.bold("Usage: portless proxy <command>")}
1216
1005
  daemonArgs.push("--https");
1217
1006
  }
1218
1007
  }
1219
- const child = spawn2(process.execPath, daemonArgs, {
1008
+ const child = spawn(process.execPath, daemonArgs, {
1220
1009
  detached: true,
1221
1010
  stdio: ["ignore", logFd, logFd],
1222
1011
  env: process.env
1223
1012
  });
1224
1013
  child.unref();
1225
1014
  } finally {
1226
- fs3.closeSync(logFd);
1015
+ fs2.closeSync(logFd);
1227
1016
  }
1228
1017
  if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
1229
1018
  console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
1230
1019
  console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
1231
1020
  const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
1232
1021
  console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
1233
- if (fs3.existsSync(logPath)) {
1022
+ if (fs2.existsSync(logPath)) {
1234
1023
  console.error(chalk.gray(`Logs: ${logPath}`));
1235
1024
  }
1236
1025
  process.exit(1);
@@ -1239,8 +1028,11 @@ ${chalk.bold("Usage: portless proxy <command>")}
1239
1028
  console.log(chalk.green(`${proto} proxy started on port ${proxyPort}`));
1240
1029
  return;
1241
1030
  }
1242
- const name = args[0];
1243
- const commandArgs = args.slice(1);
1031
+ const forceIdx = args.indexOf("--force");
1032
+ const force = forceIdx >= 0 && forceIdx <= 1;
1033
+ const appArgs = force ? [...args.slice(0, forceIdx), ...args.slice(forceIdx + 1)] : args;
1034
+ const name = appArgs[0];
1035
+ const commandArgs = appArgs.slice(1);
1244
1036
  if (commandArgs.length === 0) {
1245
1037
  console.error(chalk.red("Error: No command provided."));
1246
1038
  console.error(chalk.blue("Usage:"));
@@ -1253,7 +1045,7 @@ ${chalk.bold("Usage: portless proxy <command>")}
1253
1045
  const store = new RouteStore(dir, {
1254
1046
  onWarning: (msg) => console.warn(chalk.yellow(msg))
1255
1047
  });
1256
- await runApp(store, port, dir, name, commandArgs, tls2);
1048
+ await runApp(store, port, dir, name, commandArgs, tls2, force);
1257
1049
  }
1258
1050
  main().catch((err) => {
1259
1051
  const message = err instanceof Error ? err.message : String(err);