portless 0.5.2 → 0.7.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/LICENSE +201 -0
- package/README.md +104 -130
- package/dist/{chunk-P3DHZHEZ.js → chunk-Y6FWHU6F.js} +158 -39
- package/dist/cli.js +360 -95
- package/dist/index.d.ts +14 -10
- package/dist/index.js +3 -3
- package/package.json +14 -14
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEFAULT_TLD,
|
|
3
4
|
FILE_MODE,
|
|
4
5
|
PRIVILEGED_PORT_THRESHOLD,
|
|
6
|
+
RISKY_TLDS,
|
|
5
7
|
RouteConflictError,
|
|
6
8
|
RouteStore,
|
|
7
9
|
cleanHostsFile,
|
|
@@ -12,19 +14,24 @@ import {
|
|
|
12
14
|
fixOwnership,
|
|
13
15
|
formatUrl,
|
|
14
16
|
getDefaultPort,
|
|
17
|
+
getDefaultTld,
|
|
15
18
|
injectFrameworkFlags,
|
|
16
19
|
isErrnoException,
|
|
17
20
|
isHttpsEnvEnabled,
|
|
18
21
|
isProxyRunning,
|
|
22
|
+
isWindows,
|
|
19
23
|
parseHostname,
|
|
20
24
|
prompt,
|
|
25
|
+
readTldFromDir,
|
|
21
26
|
readTlsMarker,
|
|
22
27
|
resolveStateDir,
|
|
23
28
|
spawnCommand,
|
|
24
29
|
syncHostsFile,
|
|
30
|
+
validateTld,
|
|
25
31
|
waitForProxy,
|
|
32
|
+
writeTldFile,
|
|
26
33
|
writeTlsMarker
|
|
27
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-Y6FWHU6F.js";
|
|
28
35
|
|
|
29
36
|
// src/cli.ts
|
|
30
37
|
import chalk from "chalk";
|
|
@@ -213,24 +220,44 @@ function isCATrusted(stateDir) {
|
|
|
213
220
|
return isCATrustedMacOS(caCertPath);
|
|
214
221
|
} else if (process.platform === "linux") {
|
|
215
222
|
return isCATrustedLinux(stateDir);
|
|
223
|
+
} else if (process.platform === "win32") {
|
|
224
|
+
return isCATrustedWindows(caCertPath);
|
|
216
225
|
}
|
|
217
226
|
return false;
|
|
218
227
|
}
|
|
219
|
-
function
|
|
228
|
+
function isCATrustedWindows(caCertPath) {
|
|
220
229
|
try {
|
|
221
230
|
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (result.toLowerCase().includes(fingerprint)) return true;
|
|
230
|
-
} catch {
|
|
231
|
-
}
|
|
232
|
-
}
|
|
231
|
+
const result = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
232
|
+
encoding: "utf-8",
|
|
233
|
+
timeout: 1e4,
|
|
234
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
235
|
+
});
|
|
236
|
+
return result.replace(/\s/g, "").toLowerCase().includes(fingerprint);
|
|
237
|
+
} catch {
|
|
233
238
|
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function isCATrustedMacOS(caCertPath) {
|
|
242
|
+
try {
|
|
243
|
+
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
244
|
+
const sudoUser = process.env.SUDO_USER;
|
|
245
|
+
if (isRoot && sudoUser) {
|
|
246
|
+
execFileSync(
|
|
247
|
+
"sudo",
|
|
248
|
+
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
249
|
+
{
|
|
250
|
+
stdio: "pipe",
|
|
251
|
+
timeout: 5e3
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
} else {
|
|
255
|
+
execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
256
|
+
stdio: "pipe",
|
|
257
|
+
timeout: 5e3
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
234
261
|
} catch {
|
|
235
262
|
return false;
|
|
236
263
|
}
|
|
@@ -364,12 +391,12 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
364
391
|
fixOwnership(keyPath, certPath);
|
|
365
392
|
return { certPath, keyPath };
|
|
366
393
|
}
|
|
367
|
-
function createSNICallback(stateDir, defaultCert, defaultKey) {
|
|
394
|
+
function createSNICallback(stateDir, defaultCert, defaultKey, tld = "localhost") {
|
|
368
395
|
const cache = /* @__PURE__ */ new Map();
|
|
369
396
|
const pending = /* @__PURE__ */ new Map();
|
|
370
397
|
const defaultCtx = tls.createSecureContext({ cert: defaultCert, key: defaultKey });
|
|
371
398
|
return (servername, cb) => {
|
|
372
|
-
if (servername ===
|
|
399
|
+
if (servername === tld) {
|
|
373
400
|
cb(null, defaultCtx);
|
|
374
401
|
return;
|
|
375
402
|
}
|
|
@@ -422,12 +449,29 @@ function trustCA(stateDir) {
|
|
|
422
449
|
}
|
|
423
450
|
try {
|
|
424
451
|
if (process.platform === "darwin") {
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
452
|
+
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
453
|
+
if (isRoot) {
|
|
454
|
+
execFileSync(
|
|
455
|
+
"security",
|
|
456
|
+
[
|
|
457
|
+
"add-trusted-cert",
|
|
458
|
+
"-d",
|
|
459
|
+
"-r",
|
|
460
|
+
"trustRoot",
|
|
461
|
+
"-k",
|
|
462
|
+
"/Library/Keychains/System.keychain",
|
|
463
|
+
caCertPath
|
|
464
|
+
],
|
|
465
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
466
|
+
);
|
|
467
|
+
} else {
|
|
468
|
+
const keychain = loginKeychainPath();
|
|
469
|
+
execFileSync(
|
|
470
|
+
"security",
|
|
471
|
+
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
472
|
+
{ stdio: "pipe", timeout: 3e4 }
|
|
473
|
+
);
|
|
474
|
+
}
|
|
431
475
|
return { trusted: true };
|
|
432
476
|
} else if (process.platform === "linux") {
|
|
433
477
|
const config = getLinuxCATrustConfig();
|
|
@@ -438,6 +482,12 @@ function trustCA(stateDir) {
|
|
|
438
482
|
fs.copyFileSync(caCertPath, dest);
|
|
439
483
|
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
440
484
|
return { trusted: true };
|
|
485
|
+
} else if (process.platform === "win32") {
|
|
486
|
+
execFileSync("certutil", ["-addstore", "-user", "Root", caCertPath], {
|
|
487
|
+
stdio: "pipe",
|
|
488
|
+
timeout: 3e4
|
|
489
|
+
});
|
|
490
|
+
return { trusted: true };
|
|
441
491
|
}
|
|
442
492
|
return { trusted: false, error: `Unsupported platform: ${process.platform}` };
|
|
443
493
|
} catch (err) {
|
|
@@ -453,11 +503,21 @@ function trustCA(stateDir) {
|
|
|
453
503
|
}
|
|
454
504
|
|
|
455
505
|
// src/auto.ts
|
|
506
|
+
import { createHash } from "crypto";
|
|
456
507
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
457
508
|
import * as fs2 from "fs";
|
|
458
509
|
import * as path2 from "path";
|
|
510
|
+
var MAX_DNS_LABEL_LENGTH = 63;
|
|
511
|
+
function truncateLabel(label) {
|
|
512
|
+
if (label.length <= MAX_DNS_LABEL_LENGTH) return label;
|
|
513
|
+
const hash = createHash("sha256").update(label).digest("hex").slice(0, 6);
|
|
514
|
+
const maxPrefixLength = MAX_DNS_LABEL_LENGTH - 7;
|
|
515
|
+
const prefix = label.slice(0, maxPrefixLength).replace(/-+$/, "");
|
|
516
|
+
return `${prefix}-${hash}`;
|
|
517
|
+
}
|
|
459
518
|
function sanitizeForHostname(name) {
|
|
460
|
-
|
|
519
|
+
const sanitized = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-+|-+$/g, "");
|
|
520
|
+
return truncateLabel(sanitized);
|
|
461
521
|
}
|
|
462
522
|
function inferProjectName(cwd = process.cwd()) {
|
|
463
523
|
const pkgResult = findPackageJsonName(cwd);
|
|
@@ -546,6 +606,25 @@ function detectWorktreeViaCli(cwd) {
|
|
|
546
606
|
});
|
|
547
607
|
const worktreeCount = listOutput.split("\n").filter((l) => l.startsWith("worktree ")).length;
|
|
548
608
|
if (worktreeCount <= 1) return null;
|
|
609
|
+
const gitDir = path2.resolve(
|
|
610
|
+
cwd,
|
|
611
|
+
execFileSync2("git", ["rev-parse", "--git-dir"], {
|
|
612
|
+
cwd,
|
|
613
|
+
encoding: "utf-8",
|
|
614
|
+
timeout: 5e3,
|
|
615
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
616
|
+
}).trim()
|
|
617
|
+
);
|
|
618
|
+
const gitCommonDir = path2.resolve(
|
|
619
|
+
cwd,
|
|
620
|
+
execFileSync2("git", ["rev-parse", "--git-common-dir"], {
|
|
621
|
+
cwd,
|
|
622
|
+
encoding: "utf-8",
|
|
623
|
+
timeout: 5e3,
|
|
624
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
625
|
+
}).trim()
|
|
626
|
+
);
|
|
627
|
+
if (gitDir === gitCommonDir) return null;
|
|
549
628
|
const branch = execFileSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
550
629
|
cwd,
|
|
551
630
|
encoding: "utf-8",
|
|
@@ -573,7 +652,7 @@ function detectWorktreeViaFilesystem(startDir) {
|
|
|
573
652
|
const match = content.match(/^gitdir:\s*(.+)$/);
|
|
574
653
|
if (!match) return null;
|
|
575
654
|
const gitdir = match[1];
|
|
576
|
-
if (!gitdir.match(
|
|
655
|
+
if (!gitdir.match(/[/\\]worktrees[/\\][^/\\]+$/)) return null;
|
|
577
656
|
const branch = readBranchFromHead(path2.resolve(dir, gitdir));
|
|
578
657
|
const prefix = branchToPrefix(branch ?? "");
|
|
579
658
|
if (!prefix) return null;
|
|
@@ -598,11 +677,13 @@ function readBranchFromHead(gitdir) {
|
|
|
598
677
|
}
|
|
599
678
|
|
|
600
679
|
// src/cli.ts
|
|
680
|
+
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
681
|
+
var SUDO_PREFIX = isWindows ? "" : "sudo ";
|
|
601
682
|
var DEBOUNCE_MS = 100;
|
|
602
683
|
var POLL_INTERVAL_MS = 3e3;
|
|
603
684
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
604
685
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
605
|
-
function startProxyServer(store, proxyPort, tlsOptions) {
|
|
686
|
+
function startProxyServer(store, proxyPort, tld, tlsOptions) {
|
|
606
687
|
store.ensureDir();
|
|
607
688
|
const isTls = !!tlsOptions;
|
|
608
689
|
const routesPath = store.getRoutesPath();
|
|
@@ -618,7 +699,8 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
618
699
|
let debounceTimer = null;
|
|
619
700
|
let watcher = null;
|
|
620
701
|
let pollingInterval = null;
|
|
621
|
-
const
|
|
702
|
+
const syncVal = process.env.PORTLESS_SYNC_HOSTS;
|
|
703
|
+
const autoSyncHosts = syncVal === "1" || syncVal === "true" || tld !== DEFAULT_TLD && syncVal !== "0" && syncVal !== "false";
|
|
622
704
|
const reloadRoutes = () => {
|
|
623
705
|
try {
|
|
624
706
|
cachedRoutes = store.loadRoutes();
|
|
@@ -643,6 +725,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
643
725
|
const server = createProxyServer({
|
|
644
726
|
getRoutes: () => cachedRoutes,
|
|
645
727
|
proxyPort,
|
|
728
|
+
tld,
|
|
646
729
|
onError: (msg) => console.error(chalk.red(msg)),
|
|
647
730
|
tls: tlsOptions
|
|
648
731
|
});
|
|
@@ -652,7 +735,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
652
735
|
console.error(chalk.blue("Stop the existing proxy first:"));
|
|
653
736
|
console.error(chalk.cyan(" portless proxy stop"));
|
|
654
737
|
console.error(chalk.blue("Or check what is using the port:"));
|
|
655
|
-
console.error(
|
|
738
|
+
console.error(
|
|
739
|
+
chalk.cyan(
|
|
740
|
+
isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
|
|
741
|
+
)
|
|
742
|
+
);
|
|
656
743
|
} else if (err.code === "EACCES") {
|
|
657
744
|
console.error(chalk.red(`Permission denied for port ${proxyPort}.`));
|
|
658
745
|
console.error(chalk.blue("Either run with sudo:"));
|
|
@@ -668,9 +755,11 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
668
755
|
fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
669
756
|
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
670
757
|
writeTlsMarker(store.dir, isTls);
|
|
758
|
+
writeTldFile(store.dir, tld);
|
|
671
759
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
672
760
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
673
|
-
|
|
761
|
+
const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
|
|
762
|
+
console.log(chalk.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}`));
|
|
674
763
|
});
|
|
675
764
|
let exiting = false;
|
|
676
765
|
const cleanup = () => {
|
|
@@ -690,6 +779,7 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
690
779
|
} catch {
|
|
691
780
|
}
|
|
692
781
|
writeTlsMarker(store.dir, false);
|
|
782
|
+
writeTldFile(store.dir, DEFAULT_TLD);
|
|
693
783
|
if (autoSyncHosts) cleanHostsFile();
|
|
694
784
|
server.close(() => process.exit(0));
|
|
695
785
|
setTimeout(() => process.exit(0), EXIT_TIMEOUT_MS).unref();
|
|
@@ -699,12 +789,12 @@ function startProxyServer(store, proxyPort, tlsOptions) {
|
|
|
699
789
|
console.log(chalk.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
700
790
|
console.log(chalk.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
701
791
|
}
|
|
702
|
-
async function stopProxy(store, proxyPort,
|
|
792
|
+
async function stopProxy(store, proxyPort, _tls) {
|
|
703
793
|
const pidPath = store.pidPath;
|
|
704
|
-
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
794
|
+
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
705
795
|
const sudoHint = needsSudo ? "sudo " : "";
|
|
706
796
|
if (!fs3.existsSync(pidPath)) {
|
|
707
|
-
if (await isProxyRunning(proxyPort
|
|
797
|
+
if (await isProxyRunning(proxyPort)) {
|
|
708
798
|
console.log(chalk.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
709
799
|
const pid = findPidOnPort(proxyPort);
|
|
710
800
|
if (pid !== null) {
|
|
@@ -717,24 +807,38 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
717
807
|
console.log(chalk.green(`Killed process ${pid}. Proxy stopped.`));
|
|
718
808
|
} catch (err) {
|
|
719
809
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
720
|
-
console.error(
|
|
810
|
+
console.error(
|
|
811
|
+
chalk.red("Permission denied. The proxy was started with elevated privileges.")
|
|
812
|
+
);
|
|
721
813
|
console.error(chalk.blue("Stop it with:"));
|
|
722
|
-
console.error(
|
|
814
|
+
console.error(
|
|
815
|
+
chalk.cyan(
|
|
816
|
+
isWindows ? " Run portless proxy stop as Administrator" : " sudo portless proxy stop"
|
|
817
|
+
)
|
|
818
|
+
);
|
|
723
819
|
} else {
|
|
724
820
|
const message = err instanceof Error ? err.message : String(err);
|
|
725
821
|
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
726
822
|
console.error(chalk.blue("Check if the process is still running:"));
|
|
727
|
-
console.error(
|
|
823
|
+
console.error(
|
|
824
|
+
chalk.cyan(
|
|
825
|
+
isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
|
|
826
|
+
)
|
|
827
|
+
);
|
|
728
828
|
}
|
|
729
829
|
}
|
|
730
|
-
} else if (process.getuid?.() !== 0) {
|
|
830
|
+
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
731
831
|
console.error(chalk.red("Cannot identify the process. It may be running as root."));
|
|
732
832
|
console.error(chalk.blue("Try stopping with sudo:"));
|
|
733
833
|
console.error(chalk.cyan(" sudo portless proxy stop"));
|
|
734
834
|
} else {
|
|
735
835
|
console.error(chalk.red(`Could not identify the process on port ${proxyPort}.`));
|
|
736
836
|
console.error(chalk.blue("Try manually:"));
|
|
737
|
-
console.error(
|
|
837
|
+
console.error(
|
|
838
|
+
chalk.cyan(
|
|
839
|
+
isWindows ? " taskkill /F /PID <pid>" : ` sudo kill "$(lsof -ti tcp:${proxyPort})"`
|
|
840
|
+
)
|
|
841
|
+
);
|
|
738
842
|
}
|
|
739
843
|
} else {
|
|
740
844
|
console.log(chalk.yellow("Proxy is not running."));
|
|
@@ -759,7 +863,7 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
759
863
|
}
|
|
760
864
|
return;
|
|
761
865
|
}
|
|
762
|
-
if (!await isProxyRunning(proxyPort
|
|
866
|
+
if (!await isProxyRunning(proxyPort)) {
|
|
763
867
|
console.log(
|
|
764
868
|
chalk.yellow(
|
|
765
869
|
`PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
|
|
@@ -778,14 +882,20 @@ async function stopProxy(store, proxyPort, tls2) {
|
|
|
778
882
|
console.log(chalk.green("Proxy stopped."));
|
|
779
883
|
} catch (err) {
|
|
780
884
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
781
|
-
console.error(
|
|
885
|
+
console.error(
|
|
886
|
+
chalk.red("Permission denied. The proxy was started with elevated privileges.")
|
|
887
|
+
);
|
|
782
888
|
console.error(chalk.blue("Stop it with:"));
|
|
783
889
|
console.error(chalk.cyan(` ${sudoHint}portless proxy stop`));
|
|
784
890
|
} else {
|
|
785
891
|
const message = err instanceof Error ? err.message : String(err);
|
|
786
892
|
console.error(chalk.red(`Failed to stop proxy: ${message}`));
|
|
787
893
|
console.error(chalk.blue("Check if the process is still running:"));
|
|
788
|
-
console.error(
|
|
894
|
+
console.error(
|
|
895
|
+
chalk.cyan(
|
|
896
|
+
isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
|
|
897
|
+
)
|
|
898
|
+
);
|
|
789
899
|
}
|
|
790
900
|
}
|
|
791
901
|
}
|
|
@@ -806,8 +916,22 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
806
916
|
}
|
|
807
917
|
console.log();
|
|
808
918
|
}
|
|
809
|
-
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, force, autoInfo, desiredPort) {
|
|
810
|
-
const hostname = parseHostname(name);
|
|
919
|
+
async function runApp(store, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
|
|
920
|
+
const hostname = parseHostname(name, tld);
|
|
921
|
+
let envTld;
|
|
922
|
+
try {
|
|
923
|
+
envTld = getDefaultTld();
|
|
924
|
+
} catch (err) {
|
|
925
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
926
|
+
process.exit(1);
|
|
927
|
+
}
|
|
928
|
+
if (envTld !== DEFAULT_TLD && envTld !== tld) {
|
|
929
|
+
console.warn(
|
|
930
|
+
chalk.yellow(
|
|
931
|
+
`Warning: PORTLESS_TLD=${envTld} but the running proxy uses .${tld}. Using .${tld}.`
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
}
|
|
811
935
|
console.log(chalk.blue.bold(`
|
|
812
936
|
portless
|
|
813
937
|
`));
|
|
@@ -821,7 +945,7 @@ portless
|
|
|
821
945
|
}
|
|
822
946
|
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
823
947
|
const defaultPort = getDefaultPort();
|
|
824
|
-
const needsSudo = defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
948
|
+
const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
825
949
|
const wantHttps = isHttpsEnvEnabled();
|
|
826
950
|
if (needsSudo) {
|
|
827
951
|
if (!process.stdin.isTTY) {
|
|
@@ -845,6 +969,7 @@ portless
|
|
|
845
969
|
console.log(chalk.yellow("Starting proxy (requires sudo)..."));
|
|
846
970
|
const startArgs = [process.execPath, process.argv[1], "proxy", "start"];
|
|
847
971
|
if (wantHttps) startArgs.push("--https");
|
|
972
|
+
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
848
973
|
const result = spawnSync("sudo", startArgs, {
|
|
849
974
|
stdio: "inherit",
|
|
850
975
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
@@ -859,6 +984,7 @@ portless
|
|
|
859
984
|
console.log(chalk.yellow("Starting proxy..."));
|
|
860
985
|
const startArgs = [process.argv[1], "proxy", "start"];
|
|
861
986
|
if (wantHttps) startArgs.push("--https");
|
|
987
|
+
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
862
988
|
const result = spawnSync(process.execPath, startArgs, {
|
|
863
989
|
stdio: "inherit",
|
|
864
990
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
@@ -871,6 +997,7 @@ portless
|
|
|
871
997
|
}
|
|
872
998
|
}
|
|
873
999
|
const autoTls = readTlsMarker(stateDir);
|
|
1000
|
+
tld = readTldFromDir(stateDir);
|
|
874
1001
|
if (!await waitForProxy(defaultPort, void 0, void 0, autoTls)) {
|
|
875
1002
|
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
876
1003
|
const logPath = path3.join(stateDir, "proxy.log");
|
|
@@ -918,7 +1045,7 @@ portless
|
|
|
918
1045
|
PORT: port.toString(),
|
|
919
1046
|
HOST: "127.0.0.1",
|
|
920
1047
|
PORTLESS_URL: finalUrl,
|
|
921
|
-
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS:
|
|
1048
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: `.${tld}`
|
|
922
1049
|
},
|
|
923
1050
|
onCleanup: () => {
|
|
924
1051
|
try {
|
|
@@ -953,6 +1080,7 @@ function appPortFromEnv() {
|
|
|
953
1080
|
function parseRunArgs(args) {
|
|
954
1081
|
let force = false;
|
|
955
1082
|
let appPort;
|
|
1083
|
+
let name;
|
|
956
1084
|
let i = 0;
|
|
957
1085
|
while (i < args.length && args[i].startsWith("-")) {
|
|
958
1086
|
if (args[i] === "--") {
|
|
@@ -966,6 +1094,7 @@ ${chalk.bold("Usage:")}
|
|
|
966
1094
|
${chalk.cyan("portless run [options] <command...>")}
|
|
967
1095
|
|
|
968
1096
|
${chalk.bold("Options:")}
|
|
1097
|
+
--name <name> Override the inferred base name (worktree prefix still applies)
|
|
969
1098
|
--force Override an existing route registered by another process
|
|
970
1099
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
971
1100
|
--help, -h Show this help
|
|
@@ -975,11 +1104,13 @@ ${chalk.bold("Name inference (in order):")}
|
|
|
975
1104
|
2. Git repo root directory name
|
|
976
1105
|
3. Current directory basename
|
|
977
1106
|
|
|
1107
|
+
Use --name to override the inferred name while keeping worktree prefixes.
|
|
978
1108
|
In git worktrees, the branch name is prepended as a subdomain prefix
|
|
979
1109
|
(e.g. feature-auth.myapp.localhost).
|
|
980
1110
|
|
|
981
1111
|
${chalk.bold("Examples:")}
|
|
982
1112
|
portless run next dev # -> http://<project>.localhost:1355
|
|
1113
|
+
portless run --name myapp next dev # -> http://myapp.localhost:1355
|
|
983
1114
|
portless run vite dev # -> http://<project>.localhost:1355
|
|
984
1115
|
portless run --app-port 3000 pnpm start
|
|
985
1116
|
`);
|
|
@@ -989,15 +1120,23 @@ ${chalk.bold("Examples:")}
|
|
|
989
1120
|
} else if (args[i] === "--app-port") {
|
|
990
1121
|
i++;
|
|
991
1122
|
appPort = parseAppPort(args[i]);
|
|
1123
|
+
} else if (args[i] === "--name") {
|
|
1124
|
+
i++;
|
|
1125
|
+
if (!args[i] || args[i].startsWith("-")) {
|
|
1126
|
+
console.error(chalk.red("Error: --name requires a name value."));
|
|
1127
|
+
console.error(chalk.cyan(" portless run --name <name> <command...>"));
|
|
1128
|
+
process.exit(1);
|
|
1129
|
+
}
|
|
1130
|
+
name = args[i];
|
|
992
1131
|
} else {
|
|
993
1132
|
console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
|
|
994
|
-
console.error(chalk.blue("Known flags: --force, --app-port, --help"));
|
|
1133
|
+
console.error(chalk.blue("Known flags: --name, --force, --app-port, --help"));
|
|
995
1134
|
process.exit(1);
|
|
996
1135
|
}
|
|
997
1136
|
i++;
|
|
998
1137
|
}
|
|
999
1138
|
if (!appPort) appPort = appPortFromEnv();
|
|
1000
|
-
return { force, appPort, commandArgs: args.slice(i) };
|
|
1139
|
+
return { force, appPort, name, commandArgs: args.slice(i) };
|
|
1001
1140
|
}
|
|
1002
1141
|
function parseAppArgs(args) {
|
|
1003
1142
|
let force = false;
|
|
@@ -1058,12 +1197,13 @@ ${chalk.bold("Usage:")}
|
|
|
1058
1197
|
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
1059
1198
|
${chalk.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
1060
1199
|
${chalk.cyan("portless run <cmd>")} Infer name from project, run through proxy
|
|
1200
|
+
${chalk.cyan("portless get <name>")} Print URL for a service (for cross-service refs)
|
|
1061
1201
|
${chalk.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
|
|
1062
1202
|
${chalk.cyan("portless alias --remove <name>")} Remove a static route
|
|
1063
1203
|
${chalk.cyan("portless list")} Show active routes
|
|
1064
1204
|
${chalk.cyan("portless trust")} Add local CA to system trust store
|
|
1065
|
-
${chalk.cyan("portless hosts sync")} Add routes to
|
|
1066
|
-
${chalk.cyan("portless hosts clean")} Remove portless entries from
|
|
1205
|
+
${chalk.cyan("portless hosts sync")} Add routes to ${HOSTS_DISPLAY} (fixes Safari)
|
|
1206
|
+
${chalk.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1067
1207
|
|
|
1068
1208
|
${chalk.bold("Examples:")}
|
|
1069
1209
|
portless proxy start # Start proxy on port 1355
|
|
@@ -1073,6 +1213,7 @@ ${chalk.bold("Examples:")}
|
|
|
1073
1213
|
portless api.myapp pnpm start # -> http://api.myapp.localhost:1355
|
|
1074
1214
|
portless run next dev # -> http://<project>.localhost:1355
|
|
1075
1215
|
portless run next dev # in worktree -> http://<worktree>.<project>.localhost:1355
|
|
1216
|
+
portless get backend # -> http://backend.localhost:1355 (for cross-service refs)
|
|
1076
1217
|
# Wildcard subdomains: tenant.myapp.localhost also routes to myapp
|
|
1077
1218
|
|
|
1078
1219
|
${chalk.bold("In package.json:")}
|
|
@@ -1097,7 +1238,7 @@ ${chalk.bold("HTTP/2 + HTTPS:")}
|
|
|
1097
1238
|
system trust store. No browser warnings. No sudo required on macOS.
|
|
1098
1239
|
|
|
1099
1240
|
${chalk.bold("Options:")}
|
|
1100
|
-
run <cmd>
|
|
1241
|
+
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
1101
1242
|
Adds worktree prefix in git worktrees
|
|
1102
1243
|
-p, --port <number> Port for the proxy to listen on (default: 1355)
|
|
1103
1244
|
Ports < 1024 require sudo
|
|
@@ -1106,6 +1247,7 @@ ${chalk.bold("Options:")}
|
|
|
1106
1247
|
--key <path> Use a custom TLS private key (implies --https)
|
|
1107
1248
|
--no-tls Disable HTTPS (overrides PORTLESS_HTTPS)
|
|
1108
1249
|
--foreground Run proxy in foreground (for debugging)
|
|
1250
|
+
--tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
|
|
1109
1251
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
1110
1252
|
--force Override an existing route registered by another process
|
|
1111
1253
|
--name <name> Use <name> as the app name (bypasses subcommand dispatch)
|
|
@@ -1114,10 +1256,11 @@ ${chalk.bold("Options:")}
|
|
|
1114
1256
|
${chalk.bold("Environment variables:")}
|
|
1115
1257
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
1116
1258
|
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1117
|
-
PORTLESS_HTTPS=1
|
|
1118
|
-
|
|
1259
|
+
PORTLESS_HTTPS=1 Always enable HTTPS (set in .bashrc / .zshrc)
|
|
1260
|
+
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1261
|
+
PORTLESS_SYNC_HOSTS=1 Auto-sync ${HOSTS_DISPLAY} (auto-enabled for custom TLDs)
|
|
1119
1262
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
1120
|
-
PORTLESS=0
|
|
1263
|
+
PORTLESS=0 Run command directly without proxy
|
|
1121
1264
|
|
|
1122
1265
|
${chalk.bold("Child process environment:")}
|
|
1123
1266
|
PORT Ephemeral port the child should listen on
|
|
@@ -1127,26 +1270,24 @@ ${chalk.bold("Child process environment:")}
|
|
|
1127
1270
|
${chalk.bold("Safari / DNS:")}
|
|
1128
1271
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1129
1272
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
start the proxy with sudo.
|
|
1273
|
+
Auto-syncs ${HOSTS_DISPLAY} for custom TLDs (e.g. --tld test). For .localhost,
|
|
1274
|
+
set PORTLESS_SYNC_HOSTS=1 to enable. To manually sync:
|
|
1275
|
+
${chalk.cyan(`${SUDO_PREFIX}portless hosts sync`)}
|
|
1276
|
+
Clean up later with:
|
|
1277
|
+
${chalk.cyan(`${SUDO_PREFIX}portless hosts clean`)}
|
|
1136
1278
|
|
|
1137
1279
|
${chalk.bold("Skip portless:")}
|
|
1138
1280
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
1139
|
-
PORTLESS=skip pnpm dev # Same as above
|
|
1140
1281
|
|
|
1141
1282
|
${chalk.bold("Reserved names:")}
|
|
1142
|
-
run, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
1143
|
-
used as app names directly. Use "portless run" to infer the name,
|
|
1144
|
-
"portless --name <name>" to force any name including reserved ones.
|
|
1283
|
+
run, get, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
1284
|
+
be used as app names directly. Use "portless run" to infer the name,
|
|
1285
|
+
or "portless --name <name>" to force any name including reserved ones.
|
|
1145
1286
|
`);
|
|
1146
1287
|
process.exit(0);
|
|
1147
1288
|
}
|
|
1148
1289
|
function printVersion() {
|
|
1149
|
-
console.log("0.
|
|
1290
|
+
console.log("0.7.0");
|
|
1150
1291
|
process.exit(0);
|
|
1151
1292
|
}
|
|
1152
1293
|
async function handleTrust() {
|
|
@@ -1171,6 +1312,60 @@ async function handleList() {
|
|
|
1171
1312
|
});
|
|
1172
1313
|
listRoutes(store, port, tls2);
|
|
1173
1314
|
}
|
|
1315
|
+
async function handleGet(args) {
|
|
1316
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
1317
|
+
console.log(`
|
|
1318
|
+
${chalk.bold("portless get")} - Print the URL for a service.
|
|
1319
|
+
|
|
1320
|
+
${chalk.bold("Usage:")}
|
|
1321
|
+
${chalk.cyan("portless get <name>")}
|
|
1322
|
+
|
|
1323
|
+
Constructs the URL using the same hostname and worktree logic as
|
|
1324
|
+
"portless run", then prints it to stdout. Useful for wiring services
|
|
1325
|
+
together:
|
|
1326
|
+
|
|
1327
|
+
BACKEND_URL=$(portless get backend)
|
|
1328
|
+
|
|
1329
|
+
${chalk.bold("Options:")}
|
|
1330
|
+
--no-worktree Skip worktree prefix detection
|
|
1331
|
+
--help, -h Show this help
|
|
1332
|
+
|
|
1333
|
+
${chalk.bold("Examples:")}
|
|
1334
|
+
portless get backend # -> http://backend.localhost:1355
|
|
1335
|
+
portless get backend # in worktree -> http://auth.backend.localhost:1355
|
|
1336
|
+
portless get backend --no-worktree # -> http://backend.localhost:1355 (skip worktree)
|
|
1337
|
+
`);
|
|
1338
|
+
process.exit(0);
|
|
1339
|
+
}
|
|
1340
|
+
let skipWorktree = false;
|
|
1341
|
+
const positional = [];
|
|
1342
|
+
for (let i = 1; i < args.length; i++) {
|
|
1343
|
+
if (args[i] === "--no-worktree") {
|
|
1344
|
+
skipWorktree = true;
|
|
1345
|
+
} else if (args[i].startsWith("-")) {
|
|
1346
|
+
console.error(chalk.red(`Error: Unknown flag "${args[i]}".`));
|
|
1347
|
+
console.error(chalk.blue("Known flags: --no-worktree, --help"));
|
|
1348
|
+
process.exit(1);
|
|
1349
|
+
} else {
|
|
1350
|
+
positional.push(args[i]);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (positional.length === 0) {
|
|
1354
|
+
console.error(chalk.red("Error: Missing service name."));
|
|
1355
|
+
console.error(chalk.blue("Usage:"));
|
|
1356
|
+
console.error(chalk.cyan(" portless get <name>"));
|
|
1357
|
+
console.error(chalk.blue("Example:"));
|
|
1358
|
+
console.error(chalk.cyan(" portless get backend"));
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
const name = positional[0];
|
|
1362
|
+
const worktree = skipWorktree ? null : detectWorktreePrefix();
|
|
1363
|
+
const effectiveName = worktree ? `${worktree.prefix}.${name}` : name;
|
|
1364
|
+
const { port, tls: tls2, tld } = await discoverState();
|
|
1365
|
+
const hostname = parseHostname(effectiveName, tld);
|
|
1366
|
+
const url = formatUrl(hostname, port, tls2);
|
|
1367
|
+
process.stdout.write(url + "\n");
|
|
1368
|
+
}
|
|
1174
1369
|
async function handleAlias(args) {
|
|
1175
1370
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
1176
1371
|
console.log(`
|
|
@@ -1188,7 +1383,7 @@ ${chalk.bold("Examples:")}
|
|
|
1188
1383
|
`);
|
|
1189
1384
|
process.exit(0);
|
|
1190
1385
|
}
|
|
1191
|
-
const { dir } = await discoverState();
|
|
1386
|
+
const { dir, tld } = await discoverState();
|
|
1192
1387
|
const store = new RouteStore(dir, {
|
|
1193
1388
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1194
1389
|
});
|
|
@@ -1199,7 +1394,7 @@ ${chalk.bold("Examples:")}
|
|
|
1199
1394
|
console.error(chalk.cyan(" portless alias --remove <name>"));
|
|
1200
1395
|
process.exit(1);
|
|
1201
1396
|
}
|
|
1202
|
-
const hostname2 = parseHostname(aliasName2);
|
|
1397
|
+
const hostname2 = parseHostname(aliasName2, tld);
|
|
1203
1398
|
const routes = store.loadRoutes();
|
|
1204
1399
|
const existing = routes.find((r) => r.hostname === hostname2 && r.pid === 0);
|
|
1205
1400
|
if (!existing) {
|
|
@@ -1221,7 +1416,7 @@ ${chalk.bold("Examples:")}
|
|
|
1221
1416
|
console.error(chalk.cyan(" portless alias my-postgres 5432"));
|
|
1222
1417
|
process.exit(1);
|
|
1223
1418
|
}
|
|
1224
|
-
const hostname = parseHostname(aliasName);
|
|
1419
|
+
const hostname = parseHostname(aliasName, tld);
|
|
1225
1420
|
const port = parseInt(aliasPort, 10);
|
|
1226
1421
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1227
1422
|
console.error(chalk.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
|
|
@@ -1229,32 +1424,36 @@ ${chalk.bold("Examples:")}
|
|
|
1229
1424
|
}
|
|
1230
1425
|
const force = args.includes("--force");
|
|
1231
1426
|
store.addRoute(hostname, port, 0, force);
|
|
1232
|
-
console.log(chalk.green(`Alias registered: ${hostname} ->
|
|
1427
|
+
console.log(chalk.green(`Alias registered: ${hostname} -> 127.0.0.1:${port}`));
|
|
1233
1428
|
}
|
|
1234
1429
|
async function handleHosts(args) {
|
|
1235
1430
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
1236
1431
|
console.log(`
|
|
1237
|
-
${chalk.bold("portless hosts")} - Manage
|
|
1432
|
+
${chalk.bold("portless hosts")} - Manage ${HOSTS_DISPLAY} entries for .localhost subdomains.
|
|
1238
1433
|
|
|
1239
1434
|
Safari relies on the system DNS resolver, which may not handle .localhost
|
|
1240
|
-
subdomains. This command adds entries to
|
|
1435
|
+
subdomains. This command adds entries to ${HOSTS_DISPLAY} as a workaround.
|
|
1241
1436
|
|
|
1242
1437
|
${chalk.bold("Usage:")}
|
|
1243
|
-
${chalk.cyan(
|
|
1244
|
-
${chalk.cyan(
|
|
1438
|
+
${chalk.cyan(`${SUDO_PREFIX}portless hosts sync`)} Add current routes to ${HOSTS_DISPLAY}
|
|
1439
|
+
${chalk.cyan(`${SUDO_PREFIX}portless hosts clean`)} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1245
1440
|
|
|
1246
1441
|
${chalk.bold("Auto-sync:")}
|
|
1247
|
-
|
|
1248
|
-
|
|
1442
|
+
Auto-enabled for custom TLDs (e.g. --tld test). For .localhost, set
|
|
1443
|
+
PORTLESS_SYNC_HOSTS=1 to enable. Disable with PORTLESS_SYNC_HOSTS=0.
|
|
1249
1444
|
`);
|
|
1250
1445
|
process.exit(0);
|
|
1251
1446
|
}
|
|
1252
1447
|
if (args[1] === "clean") {
|
|
1253
1448
|
if (cleanHostsFile()) {
|
|
1254
|
-
console.log(chalk.green(
|
|
1449
|
+
console.log(chalk.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
1255
1450
|
} else {
|
|
1256
|
-
console.error(
|
|
1257
|
-
|
|
1451
|
+
console.error(
|
|
1452
|
+
chalk.red(
|
|
1453
|
+
`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : " (requires sudo)."}`
|
|
1454
|
+
)
|
|
1455
|
+
);
|
|
1456
|
+
console.error(chalk.cyan(` ${SUDO_PREFIX}portless hosts clean`));
|
|
1258
1457
|
process.exit(1);
|
|
1259
1458
|
}
|
|
1260
1459
|
return;
|
|
@@ -1263,16 +1462,18 @@ ${chalk.bold("Auto-sync:")}
|
|
|
1263
1462
|
console.log(`
|
|
1264
1463
|
${chalk.bold("Usage: portless hosts <command>")}
|
|
1265
1464
|
|
|
1266
|
-
${chalk.cyan(
|
|
1267
|
-
${chalk.cyan(
|
|
1465
|
+
${chalk.cyan(`${SUDO_PREFIX}portless hosts sync`)} Add current routes to ${HOSTS_DISPLAY}
|
|
1466
|
+
${chalk.cyan(`${SUDO_PREFIX}portless hosts clean`)} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1268
1467
|
`);
|
|
1269
1468
|
process.exit(0);
|
|
1270
1469
|
}
|
|
1271
1470
|
if (args[1] !== "sync") {
|
|
1272
1471
|
console.error(chalk.red(`Error: Unknown hosts subcommand "${args[1]}".`));
|
|
1273
1472
|
console.error(chalk.blue("Usage:"));
|
|
1274
|
-
console.error(
|
|
1275
|
-
|
|
1473
|
+
console.error(
|
|
1474
|
+
chalk.cyan(` ${SUDO_PREFIX}portless hosts sync # Add routes to ${HOSTS_DISPLAY}`)
|
|
1475
|
+
);
|
|
1476
|
+
console.error(chalk.cyan(` ${SUDO_PREFIX}portless hosts clean # Remove portless entries`));
|
|
1276
1477
|
process.exit(1);
|
|
1277
1478
|
}
|
|
1278
1479
|
const { dir } = await discoverState();
|
|
@@ -1286,13 +1487,17 @@ ${chalk.bold("Usage: portless hosts <command>")}
|
|
|
1286
1487
|
}
|
|
1287
1488
|
const hostnames = routes.map((r) => r.hostname);
|
|
1288
1489
|
if (syncHostsFile(hostnames)) {
|
|
1289
|
-
console.log(chalk.green(`Synced ${hostnames.length} hostname(s) to
|
|
1490
|
+
console.log(chalk.green(`Synced ${hostnames.length} hostname(s) to ${HOSTS_DISPLAY}:`));
|
|
1290
1491
|
for (const h of hostnames) {
|
|
1291
1492
|
console.log(chalk.cyan(` 127.0.0.1 ${h}`));
|
|
1292
1493
|
}
|
|
1293
1494
|
} else {
|
|
1294
|
-
console.error(
|
|
1295
|
-
|
|
1495
|
+
console.error(
|
|
1496
|
+
chalk.red(
|
|
1497
|
+
`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : " (requires sudo)."}`
|
|
1498
|
+
)
|
|
1499
|
+
);
|
|
1500
|
+
console.error(chalk.cyan(` ${SUDO_PREFIX}portless hosts sync`));
|
|
1296
1501
|
process.exit(1);
|
|
1297
1502
|
}
|
|
1298
1503
|
}
|
|
@@ -1315,6 +1520,7 @@ ${chalk.bold("Usage:")}
|
|
|
1315
1520
|
${chalk.cyan("portless proxy start --https")} Start with HTTP/2 + TLS
|
|
1316
1521
|
${chalk.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1317
1522
|
${chalk.cyan("portless proxy start -p 80")} Start on port 80 (requires sudo)
|
|
1523
|
+
${chalk.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
1318
1524
|
${chalk.cyan("portless proxy stop")} Stop the proxy
|
|
1319
1525
|
`);
|
|
1320
1526
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
@@ -1363,6 +1569,41 @@ ${chalk.bold("Usage:")}
|
|
|
1363
1569
|
console.error(chalk.red("Error: --cert and --key must be used together."));
|
|
1364
1570
|
process.exit(1);
|
|
1365
1571
|
}
|
|
1572
|
+
let tld;
|
|
1573
|
+
try {
|
|
1574
|
+
tld = getDefaultTld();
|
|
1575
|
+
} catch (err) {
|
|
1576
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
1577
|
+
process.exit(1);
|
|
1578
|
+
}
|
|
1579
|
+
const tldIdx = args.indexOf("--tld");
|
|
1580
|
+
if (tldIdx !== -1) {
|
|
1581
|
+
const tldValue = args[tldIdx + 1];
|
|
1582
|
+
if (!tldValue || tldValue.startsWith("-")) {
|
|
1583
|
+
console.error(chalk.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
|
|
1584
|
+
process.exit(1);
|
|
1585
|
+
}
|
|
1586
|
+
tld = tldValue.trim().toLowerCase();
|
|
1587
|
+
const tldErr = validateTld(tld);
|
|
1588
|
+
if (tldErr) {
|
|
1589
|
+
console.error(chalk.red(`Error: ${tldErr}`));
|
|
1590
|
+
process.exit(1);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const riskyReason = RISKY_TLDS.get(tld);
|
|
1594
|
+
if (riskyReason) {
|
|
1595
|
+
console.warn(chalk.yellow(`Warning: .${tld} -- ${riskyReason}`));
|
|
1596
|
+
}
|
|
1597
|
+
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
1598
|
+
if (tld !== DEFAULT_TLD && syncDisabled) {
|
|
1599
|
+
console.warn(
|
|
1600
|
+
chalk.yellow(
|
|
1601
|
+
`Warning: .${tld} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
|
|
1602
|
+
)
|
|
1603
|
+
);
|
|
1604
|
+
console.warn(chalk.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
1605
|
+
console.warn(chalk.cyan(` ${SUDO_PREFIX}portless hosts sync`));
|
|
1606
|
+
}
|
|
1366
1607
|
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1367
1608
|
const stateDir = resolveStateDir(proxyPort);
|
|
1368
1609
|
const store = new RouteStore(stateDir, {
|
|
@@ -1372,13 +1613,18 @@ ${chalk.bold("Usage:")}
|
|
|
1372
1613
|
if (isForeground) {
|
|
1373
1614
|
return;
|
|
1374
1615
|
}
|
|
1375
|
-
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1616
|
+
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1376
1617
|
const sudoPrefix = needsSudo ? "sudo " : "";
|
|
1618
|
+
const portFlag = proxyPort !== getDefaultPort() ? ` -p ${proxyPort}` : "";
|
|
1377
1619
|
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1378
|
-
console.log(
|
|
1620
|
+
console.log(
|
|
1621
|
+
chalk.blue(
|
|
1622
|
+
`To restart: ${sudoPrefix}portless proxy stop${portFlag} && ${sudoPrefix}portless proxy start${portFlag}`
|
|
1623
|
+
)
|
|
1624
|
+
);
|
|
1379
1625
|
return;
|
|
1380
1626
|
}
|
|
1381
|
-
if (proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1627
|
+
if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1382
1628
|
console.error(chalk.red(`Error: Port ${proxyPort} requires sudo.`));
|
|
1383
1629
|
console.error(chalk.blue("Either run with sudo:"));
|
|
1384
1630
|
console.error(chalk.cyan(" sudo portless proxy start -p 80"));
|
|
@@ -1440,13 +1686,13 @@ ${chalk.bold("Usage:")}
|
|
|
1440
1686
|
tlsOptions = {
|
|
1441
1687
|
cert,
|
|
1442
1688
|
key,
|
|
1443
|
-
SNICallback: createSNICallback(stateDir, cert, key)
|
|
1689
|
+
SNICallback: createSNICallback(stateDir, cert, key, tld)
|
|
1444
1690
|
};
|
|
1445
1691
|
}
|
|
1446
1692
|
}
|
|
1447
1693
|
if (isForeground) {
|
|
1448
1694
|
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
1449
|
-
startProxyServer(store, proxyPort, tlsOptions);
|
|
1695
|
+
startProxyServer(store, proxyPort, tld, tlsOptions);
|
|
1450
1696
|
return;
|
|
1451
1697
|
}
|
|
1452
1698
|
store.ensureDir();
|
|
@@ -1469,10 +1715,14 @@ ${chalk.bold("Usage:")}
|
|
|
1469
1715
|
daemonArgs.push("--https");
|
|
1470
1716
|
}
|
|
1471
1717
|
}
|
|
1718
|
+
if (tld !== DEFAULT_TLD) {
|
|
1719
|
+
daemonArgs.push("--tld", tld);
|
|
1720
|
+
}
|
|
1472
1721
|
const child = spawn(process.execPath, daemonArgs, {
|
|
1473
1722
|
detached: true,
|
|
1474
1723
|
stdio: ["ignore", logFd, logFd],
|
|
1475
|
-
env: process.env
|
|
1724
|
+
env: process.env,
|
|
1725
|
+
windowsHide: true
|
|
1476
1726
|
});
|
|
1477
1727
|
child.unref();
|
|
1478
1728
|
} finally {
|
|
@@ -1481,7 +1731,7 @@ ${chalk.bold("Usage:")}
|
|
|
1481
1731
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1482
1732
|
console.error(chalk.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1483
1733
|
console.error(chalk.blue("Try starting the proxy in the foreground to see the error:"));
|
|
1484
|
-
const needsSudo = proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1734
|
+
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
1485
1735
|
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
|
|
1486
1736
|
if (fs3.existsSync(logPath)) {
|
|
1487
1737
|
console.error(chalk.gray(`Logs: ${logPath}`));
|
|
@@ -1501,10 +1751,19 @@ async function handleRunMode(args) {
|
|
|
1501
1751
|
console.error(chalk.cyan(" portless run next dev"));
|
|
1502
1752
|
process.exit(1);
|
|
1503
1753
|
}
|
|
1504
|
-
|
|
1754
|
+
let baseName;
|
|
1755
|
+
let nameSource;
|
|
1756
|
+
if (parsed.name) {
|
|
1757
|
+
baseName = parsed.name;
|
|
1758
|
+
nameSource = "--name flag";
|
|
1759
|
+
} else {
|
|
1760
|
+
const inferred = inferProjectName();
|
|
1761
|
+
baseName = inferred.name;
|
|
1762
|
+
nameSource = inferred.source;
|
|
1763
|
+
}
|
|
1505
1764
|
const worktree = detectWorktreePrefix();
|
|
1506
|
-
const effectiveName = worktree ? `${worktree.prefix}.${
|
|
1507
|
-
const { dir, port, tls: tls2 } = await discoverState();
|
|
1765
|
+
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
1766
|
+
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
1508
1767
|
const store = new RouteStore(dir, {
|
|
1509
1768
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1510
1769
|
});
|
|
@@ -1515,8 +1774,9 @@ async function handleRunMode(args) {
|
|
|
1515
1774
|
effectiveName,
|
|
1516
1775
|
parsed.commandArgs,
|
|
1517
1776
|
tls2,
|
|
1777
|
+
tld,
|
|
1518
1778
|
parsed.force,
|
|
1519
|
-
{ nameSource
|
|
1779
|
+
{ nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
1520
1780
|
parsed.appPort
|
|
1521
1781
|
);
|
|
1522
1782
|
}
|
|
@@ -1530,7 +1790,7 @@ async function handleNamedMode(args) {
|
|
|
1530
1790
|
console.error(chalk.cyan(" portless myapp next dev"));
|
|
1531
1791
|
process.exit(1);
|
|
1532
1792
|
}
|
|
1533
|
-
const { dir, port, tls: tls2 } = await discoverState();
|
|
1793
|
+
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
1534
1794
|
const store = new RouteStore(dir, {
|
|
1535
1795
|
onWarning: (msg) => console.warn(chalk.yellow(msg))
|
|
1536
1796
|
});
|
|
@@ -1541,6 +1801,7 @@ async function handleNamedMode(args) {
|
|
|
1541
1801
|
parsed.name,
|
|
1542
1802
|
parsed.commandArgs,
|
|
1543
1803
|
tls2,
|
|
1804
|
+
tld,
|
|
1544
1805
|
parsed.force,
|
|
1545
1806
|
void 0,
|
|
1546
1807
|
parsed.appPort
|
|
@@ -1571,7 +1832,7 @@ async function main() {
|
|
|
1571
1832
|
console.error(chalk.cyan(" portless --name <name> <command...>"));
|
|
1572
1833
|
process.exit(1);
|
|
1573
1834
|
}
|
|
1574
|
-
const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
1835
|
+
const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
1575
1836
|
if (skipPortless2) {
|
|
1576
1837
|
const { commandArgs } = parseAppArgs(args);
|
|
1577
1838
|
if (commandArgs.length === 0) {
|
|
@@ -1588,7 +1849,7 @@ async function main() {
|
|
|
1588
1849
|
if (isRunCommand) {
|
|
1589
1850
|
args.shift();
|
|
1590
1851
|
}
|
|
1591
|
-
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "skip";
|
|
1852
|
+
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
1592
1853
|
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
|
|
1593
1854
|
const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
1594
1855
|
if (commandArgs.length === 0) {
|
|
@@ -1615,6 +1876,10 @@ async function main() {
|
|
|
1615
1876
|
await handleList();
|
|
1616
1877
|
return;
|
|
1617
1878
|
}
|
|
1879
|
+
if (args[0] === "get") {
|
|
1880
|
+
await handleGet(args);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1618
1883
|
if (args[0] === "alias") {
|
|
1619
1884
|
await handleAlias(args);
|
|
1620
1885
|
return;
|