portless 0.7.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -50
- package/dist/{chunk-ROBZDJST.js → chunk-5BR7NCNI.js} +88 -37
- package/dist/cli.js +599 -392
- package/dist/index.d.ts +12 -1
- package/dist/index.js +3 -1
- package/package.json +13 -15
- package/LICENSE +0 -201
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_TLD,
|
|
4
|
+
FALLBACK_PROXY_PORT,
|
|
4
5
|
FILE_MODE,
|
|
5
6
|
PRIVILEGED_PORT_THRESHOLD,
|
|
6
7
|
RISKY_TLDS,
|
|
7
8
|
RouteConflictError,
|
|
8
9
|
RouteStore,
|
|
10
|
+
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
11
|
+
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
9
12
|
cleanHostsFile,
|
|
13
|
+
createHttpRedirectServer,
|
|
10
14
|
createProxyServer,
|
|
11
15
|
discoverState,
|
|
12
16
|
findFreePort,
|
|
@@ -15,15 +19,15 @@ import {
|
|
|
15
19
|
formatUrl,
|
|
16
20
|
getDefaultPort,
|
|
17
21
|
getDefaultTld,
|
|
22
|
+
getProtocolPort,
|
|
18
23
|
injectFrameworkFlags,
|
|
19
24
|
isErrnoException,
|
|
20
|
-
|
|
25
|
+
isHttpsEnvDisabled,
|
|
21
26
|
isProxyRunning,
|
|
27
|
+
isWildcardEnvEnabled,
|
|
22
28
|
isWindows,
|
|
23
29
|
parseHostname,
|
|
24
30
|
prompt,
|
|
25
|
-
readTldFromDir,
|
|
26
|
-
readTlsMarker,
|
|
27
31
|
resolveStateDir,
|
|
28
32
|
spawnCommand,
|
|
29
33
|
syncHostsFile,
|
|
@@ -31,10 +35,34 @@ import {
|
|
|
31
35
|
waitForProxy,
|
|
32
36
|
writeTldFile,
|
|
33
37
|
writeTlsMarker
|
|
34
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-5BR7NCNI.js";
|
|
39
|
+
|
|
40
|
+
// src/colors.ts
|
|
41
|
+
function supportsColor() {
|
|
42
|
+
if ("NO_COLOR" in process.env) return false;
|
|
43
|
+
if ("FORCE_COLOR" in process.env) return true;
|
|
44
|
+
return !!(process.stdout.isTTY || process.stderr.isTTY);
|
|
45
|
+
}
|
|
46
|
+
var enabled = supportsColor();
|
|
47
|
+
var wrap = (open, close) => {
|
|
48
|
+
if (!enabled) return (s) => s;
|
|
49
|
+
return (s) => `\x1B[${open}m${s}\x1B[${close}m`;
|
|
50
|
+
};
|
|
51
|
+
var bold = wrap("1", "22");
|
|
52
|
+
var red = wrap("31", "39");
|
|
53
|
+
var green = wrap("32", "39");
|
|
54
|
+
var yellow = wrap("33", "39");
|
|
55
|
+
var blue = Object.assign(wrap("34", "39"), {
|
|
56
|
+
bold: enabled ? (s) => `\x1B[34;1m${s}\x1B[22;39m` : (s) => s
|
|
57
|
+
});
|
|
58
|
+
var cyan = Object.assign(wrap("36", "39"), {
|
|
59
|
+
bold: enabled ? (s) => `\x1B[36;1m${s}\x1B[22;39m` : (s) => s
|
|
60
|
+
});
|
|
61
|
+
var white = wrap("37", "39");
|
|
62
|
+
var gray = wrap("90", "39");
|
|
63
|
+
var colors_default = { bold, red, green, yellow, blue, cyan, white, gray };
|
|
35
64
|
|
|
36
65
|
// src/cli.ts
|
|
37
|
-
import chalk from "chalk";
|
|
38
66
|
import * as fs3 from "fs";
|
|
39
67
|
import * as path3 from "path";
|
|
40
68
|
import { spawn, spawnSync } from "child_process";
|
|
@@ -164,6 +192,13 @@ function generateServerCert(stateDir) {
|
|
|
164
192
|
"subjectAltName=DNS:localhost,DNS:*.localhost"
|
|
165
193
|
].join("\n") + "\n"
|
|
166
194
|
);
|
|
195
|
+
const srlPath = path.join(stateDir, "ca.srl");
|
|
196
|
+
if (!fileExists(srlPath)) {
|
|
197
|
+
fs.writeFileSync(
|
|
198
|
+
srlPath,
|
|
199
|
+
crypto.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
167
202
|
openssl([
|
|
168
203
|
"x509",
|
|
169
204
|
"-req",
|
|
@@ -174,7 +209,8 @@ function generateServerCert(stateDir) {
|
|
|
174
209
|
caCertPath,
|
|
175
210
|
"-CAkey",
|
|
176
211
|
caKeyPath,
|
|
177
|
-
"-
|
|
212
|
+
"-CAserial",
|
|
213
|
+
srlPath,
|
|
178
214
|
"-out",
|
|
179
215
|
serverCertPath,
|
|
180
216
|
"-days",
|
|
@@ -364,6 +400,13 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
364
400
|
`subjectAltName=${sans.join(",")}`
|
|
365
401
|
].join("\n") + "\n"
|
|
366
402
|
);
|
|
403
|
+
const srlPath = path.join(stateDir, "ca.srl");
|
|
404
|
+
if (!fs.existsSync(srlPath)) {
|
|
405
|
+
await fs.promises.writeFile(
|
|
406
|
+
srlPath,
|
|
407
|
+
crypto.randomUUID().replace(/-/g, "").slice(0, 16).toUpperCase() + "\n"
|
|
408
|
+
);
|
|
409
|
+
}
|
|
367
410
|
await opensslAsync([
|
|
368
411
|
"x509",
|
|
369
412
|
"-req",
|
|
@@ -374,7 +417,8 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
374
417
|
caCertPath,
|
|
375
418
|
"-CAkey",
|
|
376
419
|
caKeyPath,
|
|
377
|
-
"-
|
|
420
|
+
"-CAserial",
|
|
421
|
+
srlPath,
|
|
378
422
|
"-out",
|
|
379
423
|
certPath,
|
|
380
424
|
"-days",
|
|
@@ -497,7 +541,7 @@ function trustCA(stateDir) {
|
|
|
497
541
|
if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES")) {
|
|
498
542
|
return {
|
|
499
543
|
trusted: false,
|
|
500
|
-
error: "Permission denied. Try:
|
|
544
|
+
error: "Permission denied. Try: portless trust"
|
|
501
545
|
};
|
|
502
546
|
}
|
|
503
547
|
return { trusted: false, error: message };
|
|
@@ -680,12 +724,36 @@ function readBranchFromHead(gitdir) {
|
|
|
680
724
|
|
|
681
725
|
// src/cli.ts
|
|
682
726
|
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
683
|
-
var SUDO_PREFIX = isWindows ? "" : "sudo ";
|
|
684
727
|
var DEBOUNCE_MS = 100;
|
|
685
728
|
var POLL_INTERVAL_MS = 3e3;
|
|
686
729
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
687
730
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
688
|
-
function
|
|
731
|
+
function getEntryScript() {
|
|
732
|
+
const script = process.argv[1];
|
|
733
|
+
if (!script) {
|
|
734
|
+
throw new Error("Cannot determine portless entry script (process.argv[1] is undefined)");
|
|
735
|
+
}
|
|
736
|
+
return script;
|
|
737
|
+
}
|
|
738
|
+
function collectPortlessEnvArgs() {
|
|
739
|
+
const envArgs = [];
|
|
740
|
+
for (const key of Object.keys(process.env)) {
|
|
741
|
+
if (key.startsWith("PORTLESS_") && process.env[key]) {
|
|
742
|
+
envArgs.push(`${key}=${process.env[key]}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
return envArgs;
|
|
746
|
+
}
|
|
747
|
+
function sudoStop(port) {
|
|
748
|
+
const stopArgs = [process.execPath, getEntryScript(), "proxy", "stop", "-p", String(port)];
|
|
749
|
+
console.log(colors_default.yellow("Proxy is running as root. Elevating with sudo to stop it..."));
|
|
750
|
+
const result = spawnSync("sudo", ["env", ...collectPortlessEnvArgs(), ...stopArgs], {
|
|
751
|
+
stdio: "inherit",
|
|
752
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
753
|
+
});
|
|
754
|
+
return result.status === 0;
|
|
755
|
+
}
|
|
756
|
+
function startProxyServer(store, proxyPort, tld, tlsOptions, strict) {
|
|
689
757
|
store.ensureDir();
|
|
690
758
|
const isTls = !!tlsOptions;
|
|
691
759
|
const routesPath = store.getRoutesPath();
|
|
@@ -718,7 +786,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions) {
|
|
|
718
786
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
719
787
|
});
|
|
720
788
|
} catch {
|
|
721
|
-
console.warn(
|
|
789
|
+
console.warn(colors_default.yellow("fs.watch unavailable; falling back to polling for route changes"));
|
|
722
790
|
pollingInterval = setInterval(reloadRoutes, POLL_INTERVAL_MS);
|
|
723
791
|
}
|
|
724
792
|
if (autoSyncHosts) {
|
|
@@ -728,31 +796,39 @@ function startProxyServer(store, proxyPort, tld, tlsOptions) {
|
|
|
728
796
|
getRoutes: () => cachedRoutes,
|
|
729
797
|
proxyPort,
|
|
730
798
|
tld,
|
|
731
|
-
|
|
799
|
+
strict,
|
|
800
|
+
onError: (msg) => console.error(colors_default.red(msg)),
|
|
732
801
|
tls: tlsOptions
|
|
733
802
|
});
|
|
734
803
|
server.on("error", (err) => {
|
|
735
804
|
if (err.code === "EADDRINUSE") {
|
|
736
|
-
console.error(
|
|
737
|
-
console.error(
|
|
738
|
-
console.error(
|
|
739
|
-
console.error(
|
|
805
|
+
console.error(colors_default.red(`Port ${proxyPort} is already in use.`));
|
|
806
|
+
console.error(colors_default.blue("Stop the existing proxy first:"));
|
|
807
|
+
console.error(colors_default.cyan(" portless proxy stop"));
|
|
808
|
+
console.error(colors_default.blue("Or check what is using the port:"));
|
|
740
809
|
console.error(
|
|
741
|
-
|
|
810
|
+
colors_default.cyan(
|
|
742
811
|
isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
|
|
743
812
|
)
|
|
744
813
|
);
|
|
745
814
|
} else if (err.code === "EACCES") {
|
|
746
|
-
console.error(
|
|
747
|
-
console.error(
|
|
748
|
-
console.error(
|
|
749
|
-
console.error(chalk.blue("Or use a non-privileged port (no sudo needed):"));
|
|
750
|
-
console.error(chalk.cyan(" portless proxy start"));
|
|
815
|
+
console.error(colors_default.red(`Permission denied for port ${proxyPort}.`));
|
|
816
|
+
console.error(colors_default.blue("Use an unprivileged port (no sudo needed):"));
|
|
817
|
+
console.error(colors_default.cyan(" portless proxy start -p 1355"));
|
|
751
818
|
} else {
|
|
752
|
-
console.error(
|
|
819
|
+
console.error(colors_default.red(`Proxy error: ${err.message}`));
|
|
753
820
|
}
|
|
821
|
+
if (redirectServer) redirectServer.close();
|
|
754
822
|
process.exit(1);
|
|
755
823
|
});
|
|
824
|
+
let redirectServer = null;
|
|
825
|
+
if (isTls && proxyPort !== 80) {
|
|
826
|
+
redirectServer = createHttpRedirectServer(proxyPort);
|
|
827
|
+
redirectServer.on("error", () => {
|
|
828
|
+
redirectServer = null;
|
|
829
|
+
});
|
|
830
|
+
redirectServer.listen(80);
|
|
831
|
+
}
|
|
756
832
|
server.listen(proxyPort, () => {
|
|
757
833
|
fs3.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
758
834
|
fs3.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
@@ -761,7 +837,13 @@ function startProxyServer(store, proxyPort, tld, tlsOptions) {
|
|
|
761
837
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
762
838
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
763
839
|
const tldLabel = tld !== DEFAULT_TLD ? ` (TLD: .${tld})` : "";
|
|
764
|
-
|
|
840
|
+
const modeLabel = strict === false ? " (wildcard)" : "";
|
|
841
|
+
console.log(
|
|
842
|
+
colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
|
|
843
|
+
);
|
|
844
|
+
if (redirectServer) {
|
|
845
|
+
console.log(colors_default.green("HTTP-to-HTTPS redirect listening on port 80"));
|
|
846
|
+
}
|
|
765
847
|
});
|
|
766
848
|
let exiting = false;
|
|
767
849
|
const cleanup = () => {
|
|
@@ -772,6 +854,9 @@ function startProxyServer(store, proxyPort, tld, tlsOptions) {
|
|
|
772
854
|
if (watcher) {
|
|
773
855
|
watcher.close();
|
|
774
856
|
}
|
|
857
|
+
if (redirectServer) {
|
|
858
|
+
redirectServer.close();
|
|
859
|
+
}
|
|
775
860
|
try {
|
|
776
861
|
fs3.unlinkSync(store.pidPath);
|
|
777
862
|
} catch {
|
|
@@ -788,16 +873,27 @@ function startProxyServer(store, proxyPort, tld, tlsOptions) {
|
|
|
788
873
|
};
|
|
789
874
|
process.on("SIGINT", cleanup);
|
|
790
875
|
process.on("SIGTERM", cleanup);
|
|
791
|
-
console.log(
|
|
792
|
-
console.log(
|
|
876
|
+
console.log(colors_default.cyan("\nProxy is running. Press Ctrl+C to stop.\n"));
|
|
877
|
+
console.log(colors_default.gray(`Routes file: ${store.getRoutesPath()}`));
|
|
878
|
+
}
|
|
879
|
+
function sudoStopOrHint(port) {
|
|
880
|
+
if (!isWindows) {
|
|
881
|
+
if (!sudoStop(port)) {
|
|
882
|
+
console.error(colors_default.red("Failed to stop proxy with sudo."));
|
|
883
|
+
console.error(colors_default.blue("Try manually:"));
|
|
884
|
+
console.error(colors_default.cyan(` portless proxy stop -p ${port}`));
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
console.error(colors_default.red("Permission denied. The proxy was started with elevated privileges."));
|
|
888
|
+
console.error(colors_default.blue("Stop it with:"));
|
|
889
|
+
console.error(colors_default.cyan(" Run portless proxy stop as Administrator"));
|
|
890
|
+
}
|
|
793
891
|
}
|
|
794
892
|
async function stopProxy(store, proxyPort, _tls) {
|
|
795
893
|
const pidPath = store.pidPath;
|
|
796
|
-
const needsSudo = !isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD;
|
|
797
|
-
const sudoHint = needsSudo ? "sudo " : "";
|
|
798
894
|
if (!fs3.existsSync(pidPath)) {
|
|
799
895
|
if (await isProxyRunning(proxyPort)) {
|
|
800
|
-
console.log(
|
|
896
|
+
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
801
897
|
const pid = findPidOnPort(proxyPort);
|
|
802
898
|
if (pid !== null) {
|
|
803
899
|
try {
|
|
@@ -806,58 +902,52 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
806
902
|
fs3.unlinkSync(store.portFilePath);
|
|
807
903
|
} catch {
|
|
808
904
|
}
|
|
809
|
-
console.log(
|
|
905
|
+
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
810
906
|
} catch (err) {
|
|
811
907
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
812
|
-
|
|
813
|
-
chalk.red("Permission denied. The proxy was started with elevated privileges.")
|
|
814
|
-
);
|
|
815
|
-
console.error(chalk.blue("Stop it with:"));
|
|
816
|
-
console.error(
|
|
817
|
-
chalk.cyan(
|
|
818
|
-
isWindows ? " Run portless proxy stop as Administrator" : " sudo portless proxy stop"
|
|
819
|
-
)
|
|
820
|
-
);
|
|
908
|
+
sudoStopOrHint(proxyPort);
|
|
821
909
|
} else {
|
|
822
910
|
const message = err instanceof Error ? err.message : String(err);
|
|
823
|
-
console.error(
|
|
824
|
-
console.error(
|
|
911
|
+
console.error(colors_default.red(`Failed to stop proxy: ${message}`));
|
|
912
|
+
console.error(colors_default.blue("Check if the process is still running:"));
|
|
825
913
|
console.error(
|
|
826
|
-
|
|
914
|
+
colors_default.cyan(
|
|
827
915
|
isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
|
|
828
916
|
)
|
|
829
917
|
);
|
|
830
918
|
}
|
|
831
919
|
}
|
|
832
920
|
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
833
|
-
|
|
834
|
-
console.error(chalk.blue("Try stopping with sudo:"));
|
|
835
|
-
console.error(chalk.cyan(" sudo portless proxy stop"));
|
|
921
|
+
sudoStopOrHint(proxyPort);
|
|
836
922
|
} else {
|
|
837
|
-
console.error(
|
|
838
|
-
console.error(
|
|
923
|
+
console.error(colors_default.red(`Could not identify the process on port ${proxyPort}.`));
|
|
924
|
+
console.error(colors_default.blue("Try manually:"));
|
|
839
925
|
console.error(
|
|
840
|
-
|
|
926
|
+
colors_default.cyan(
|
|
841
927
|
isWindows ? " taskkill /F /PID <pid>" : ` sudo kill "$(lsof -ti tcp:${proxyPort})"`
|
|
842
928
|
)
|
|
843
929
|
);
|
|
844
930
|
}
|
|
845
931
|
} else {
|
|
846
|
-
console.log(
|
|
932
|
+
console.log(colors_default.yellow("Proxy is not running."));
|
|
847
933
|
}
|
|
848
934
|
return;
|
|
849
935
|
}
|
|
850
936
|
try {
|
|
851
937
|
const pid = parseInt(fs3.readFileSync(pidPath, "utf-8"), 10);
|
|
852
938
|
if (isNaN(pid)) {
|
|
853
|
-
console.error(
|
|
939
|
+
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
854
940
|
fs3.unlinkSync(pidPath);
|
|
855
941
|
return;
|
|
856
942
|
}
|
|
857
943
|
try {
|
|
858
944
|
process.kill(pid, 0);
|
|
859
|
-
} catch {
|
|
860
|
-
|
|
945
|
+
} catch (err) {
|
|
946
|
+
if (isErrnoException(err) && err.code === "EPERM") {
|
|
947
|
+
sudoStopOrHint(proxyPort);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
861
951
|
fs3.unlinkSync(pidPath);
|
|
862
952
|
try {
|
|
863
953
|
fs3.unlinkSync(store.portFilePath);
|
|
@@ -867,11 +957,11 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
867
957
|
}
|
|
868
958
|
if (!await isProxyRunning(proxyPort)) {
|
|
869
959
|
console.log(
|
|
870
|
-
|
|
960
|
+
colors_default.yellow(
|
|
871
961
|
`PID file exists but port ${proxyPort} is not listening. The PID may have been recycled.`
|
|
872
962
|
)
|
|
873
963
|
);
|
|
874
|
-
console.log(
|
|
964
|
+
console.log(colors_default.yellow("Removing stale PID file."));
|
|
875
965
|
fs3.unlinkSync(pidPath);
|
|
876
966
|
return;
|
|
877
967
|
}
|
|
@@ -881,20 +971,16 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
881
971
|
fs3.unlinkSync(store.portFilePath);
|
|
882
972
|
} catch {
|
|
883
973
|
}
|
|
884
|
-
console.log(
|
|
974
|
+
console.log(colors_default.green("Proxy stopped."));
|
|
885
975
|
} catch (err) {
|
|
886
976
|
if (isErrnoException(err) && err.code === "EPERM") {
|
|
887
|
-
|
|
888
|
-
chalk.red("Permission denied. The proxy was started with elevated privileges.")
|
|
889
|
-
);
|
|
890
|
-
console.error(chalk.blue("Stop it with:"));
|
|
891
|
-
console.error(chalk.cyan(` ${sudoHint}portless proxy stop`));
|
|
977
|
+
sudoStopOrHint(proxyPort);
|
|
892
978
|
} else {
|
|
893
979
|
const message = err instanceof Error ? err.message : String(err);
|
|
894
|
-
console.error(
|
|
895
|
-
console.error(
|
|
980
|
+
console.error(colors_default.red(`Failed to stop proxy: ${message}`));
|
|
981
|
+
console.error(colors_default.blue("Check if the process is still running:"));
|
|
896
982
|
console.error(
|
|
897
|
-
|
|
983
|
+
colors_default.cyan(
|
|
898
984
|
isWindows ? ` netstat -ano | findstr :${proxyPort}` : ` lsof -ti tcp:${proxyPort}`
|
|
899
985
|
)
|
|
900
986
|
);
|
|
@@ -904,143 +990,140 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
904
990
|
function listRoutes(store, proxyPort, tls2) {
|
|
905
991
|
const routes = store.loadRoutes();
|
|
906
992
|
if (routes.length === 0) {
|
|
907
|
-
console.log(
|
|
908
|
-
console.log(
|
|
993
|
+
console.log(colors_default.yellow("No active routes."));
|
|
994
|
+
console.log(colors_default.gray("Start an app with: portless <name> <command>"));
|
|
909
995
|
return;
|
|
910
996
|
}
|
|
911
|
-
console.log(
|
|
997
|
+
console.log(colors_default.blue.bold("\nActive routes:\n"));
|
|
912
998
|
for (const route of routes) {
|
|
913
999
|
const url = formatUrl(route.hostname, proxyPort, tls2);
|
|
914
1000
|
const label = route.pid === 0 ? "(alias)" : `(pid ${route.pid})`;
|
|
915
1001
|
console.log(
|
|
916
|
-
` ${
|
|
1002
|
+
` ${colors_default.cyan(url)} ${colors_default.gray("->")} ${colors_default.white(`localhost:${route.port}`)} ${colors_default.gray(label)}`
|
|
917
1003
|
);
|
|
918
1004
|
}
|
|
919
1005
|
console.log();
|
|
920
1006
|
}
|
|
921
|
-
async function runApp(
|
|
922
|
-
|
|
1007
|
+
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tld, force, autoInfo, desiredPort) {
|
|
1008
|
+
let store = initialStore;
|
|
923
1009
|
let envTld;
|
|
924
1010
|
try {
|
|
925
1011
|
envTld = getDefaultTld();
|
|
926
1012
|
} catch (err) {
|
|
927
|
-
console.error(
|
|
1013
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
928
1014
|
process.exit(1);
|
|
929
1015
|
}
|
|
930
1016
|
if (envTld !== DEFAULT_TLD && envTld !== tld) {
|
|
931
1017
|
console.warn(
|
|
932
|
-
|
|
1018
|
+
colors_default.yellow(
|
|
933
1019
|
`Warning: PORTLESS_TLD=${envTld} but the running proxy uses .${tld}. Using .${tld}.`
|
|
934
1020
|
)
|
|
935
1021
|
);
|
|
936
1022
|
}
|
|
937
|
-
console.log(
|
|
1023
|
+
console.log(colors_default.blue.bold(`
|
|
938
1024
|
portless
|
|
939
1025
|
`));
|
|
940
|
-
console.log(
|
|
1026
|
+
console.log(colors_default.gray(`-- ${parseHostname(name, tld)} (auto-resolves to 127.0.0.1)`));
|
|
941
1027
|
if (autoInfo) {
|
|
942
1028
|
const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
|
|
943
|
-
console.log(
|
|
1029
|
+
console.log(colors_default.gray(`-- Name "${baseName}" (from ${autoInfo.nameSource})`));
|
|
944
1030
|
if (autoInfo.prefix) {
|
|
945
|
-
console.log(
|
|
1031
|
+
console.log(colors_default.gray(`-- Prefix "${autoInfo.prefix}" (from ${autoInfo.prefixSource})`));
|
|
946
1032
|
}
|
|
947
1033
|
}
|
|
948
1034
|
if (!await isProxyRunning(proxyPort, tls2)) {
|
|
949
|
-
const
|
|
1035
|
+
const wantTls = !isHttpsEnvDisabled();
|
|
1036
|
+
const defaultPort = getDefaultPort(wantTls);
|
|
950
1037
|
const needsSudo = !isWindows && defaultPort < PRIVILEGED_PORT_THRESHOLD;
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
|
|
1038
|
+
if (needsSudo && !process.stdin.isTTY) {
|
|
1039
|
+
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
1040
|
+
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
1041
|
+
console.error(colors_default.cyan(" portless proxy start"));
|
|
1042
|
+
console.error(
|
|
1043
|
+
colors_default.blue(
|
|
1044
|
+
`Option 2: use an unprivileged port (no sudo needed, URLs will include :${FALLBACK_PROXY_PORT}):`
|
|
1045
|
+
)
|
|
1046
|
+
);
|
|
1047
|
+
console.error(colors_default.cyan(` portless proxy start -p ${FALLBACK_PROXY_PORT}`));
|
|
1048
|
+
process.exit(1);
|
|
1049
|
+
}
|
|
1050
|
+
if (needsSudo && process.stdin.isTTY) {
|
|
1051
|
+
const answer = await prompt(colors_default.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
962
1052
|
if (answer === "n" || answer === "no") {
|
|
963
|
-
console.log(
|
|
1053
|
+
console.log(colors_default.gray("Cancelled."));
|
|
964
1054
|
process.exit(0);
|
|
965
1055
|
}
|
|
966
1056
|
if (answer === "s" || answer === "skip") {
|
|
967
|
-
console.log(
|
|
1057
|
+
console.log(colors_default.gray("Skipping proxy, running command directly...\n"));
|
|
968
1058
|
spawnCommand(commandArgs);
|
|
969
1059
|
return;
|
|
970
1060
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
console.log(chalk.yellow("Starting proxy..."));
|
|
989
|
-
const startArgs = [process.argv[1], "proxy", "start"];
|
|
990
|
-
if (wantHttps) startArgs.push("--https");
|
|
991
|
-
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
992
|
-
const result = spawnSync(process.execPath, startArgs, {
|
|
993
|
-
stdio: "inherit",
|
|
994
|
-
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
995
|
-
});
|
|
996
|
-
if (result.status !== 0) {
|
|
997
|
-
if (!await isProxyRunning(proxyPort)) {
|
|
998
|
-
console.error(chalk.red("Failed to start proxy."));
|
|
999
|
-
console.error(chalk.blue("Try starting it manually:"));
|
|
1000
|
-
console.error(chalk.cyan(" portless proxy start"));
|
|
1001
|
-
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
console.log(colors_default.yellow("Starting proxy..."));
|
|
1063
|
+
const startArgs = [getEntryScript(), "proxy", "start"];
|
|
1064
|
+
if (!wantTls) startArgs.push("--no-tls");
|
|
1065
|
+
if (tld !== DEFAULT_TLD) startArgs.push("--tld", tld);
|
|
1066
|
+
const result = spawnSync(process.execPath, startArgs, {
|
|
1067
|
+
stdio: "inherit",
|
|
1068
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1069
|
+
});
|
|
1070
|
+
let discovered = null;
|
|
1071
|
+
if (result.status === 0) {
|
|
1072
|
+
for (let i = 0; i < WAIT_FOR_PROXY_MAX_ATTEMPTS; i++) {
|
|
1073
|
+
await new Promise((r) => setTimeout(r, WAIT_FOR_PROXY_INTERVAL_MS));
|
|
1074
|
+
const state = await discoverState();
|
|
1075
|
+
if (await isProxyRunning(state.port)) {
|
|
1076
|
+
discovered = state;
|
|
1077
|
+
break;
|
|
1002
1078
|
}
|
|
1003
1079
|
}
|
|
1004
1080
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
console.error(
|
|
1011
|
-
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start`));
|
|
1081
|
+
if (!discovered) {
|
|
1082
|
+
console.error(colors_default.red("Failed to start proxy."));
|
|
1083
|
+
const fallbackDir = resolveStateDir(getDefaultPort(wantTls));
|
|
1084
|
+
const logPath = path3.join(fallbackDir, "proxy.log");
|
|
1085
|
+
console.error(colors_default.blue("Try starting it manually:"));
|
|
1086
|
+
console.error(colors_default.cyan(" portless proxy start"));
|
|
1012
1087
|
if (fs3.existsSync(logPath)) {
|
|
1013
|
-
console.error(
|
|
1088
|
+
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1014
1089
|
}
|
|
1015
1090
|
process.exit(1);
|
|
1091
|
+
return;
|
|
1016
1092
|
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1093
|
+
proxyPort = discovered.port;
|
|
1094
|
+
stateDir = discovered.dir;
|
|
1095
|
+
tld = discovered.tld;
|
|
1096
|
+
tls2 = discovered.tls;
|
|
1097
|
+
store = new RouteStore(stateDir, {
|
|
1098
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1099
|
+
});
|
|
1100
|
+
console.log(colors_default.green("Proxy started in background"));
|
|
1019
1101
|
} else {
|
|
1020
|
-
console.log(
|
|
1102
|
+
console.log(colors_default.gray("-- Proxy is running"));
|
|
1021
1103
|
}
|
|
1104
|
+
const hostname = parseHostname(name, tld);
|
|
1022
1105
|
const port = desiredPort ?? await findFreePort();
|
|
1023
1106
|
if (desiredPort) {
|
|
1024
|
-
console.log(
|
|
1107
|
+
console.log(colors_default.green(`-- Using port ${port} (fixed)`));
|
|
1025
1108
|
} else {
|
|
1026
|
-
console.log(
|
|
1109
|
+
console.log(colors_default.green(`-- Using port ${port}`));
|
|
1027
1110
|
}
|
|
1028
1111
|
try {
|
|
1029
1112
|
store.addRoute(hostname, port, process.pid, force);
|
|
1030
1113
|
} catch (err) {
|
|
1031
1114
|
if (err instanceof RouteConflictError) {
|
|
1032
|
-
console.error(
|
|
1115
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
1033
1116
|
process.exit(1);
|
|
1034
1117
|
}
|
|
1035
1118
|
throw err;
|
|
1036
1119
|
}
|
|
1037
1120
|
const finalUrl = formatUrl(hostname, proxyPort, tls2);
|
|
1038
|
-
console.log(
|
|
1121
|
+
console.log(colors_default.cyan.bold(`
|
|
1039
1122
|
-> ${finalUrl}
|
|
1040
1123
|
`));
|
|
1041
1124
|
injectFrameworkFlags(commandArgs, port);
|
|
1042
1125
|
console.log(
|
|
1043
|
-
|
|
1126
|
+
colors_default.gray(
|
|
1044
1127
|
`Running: PORT=${port} HOST=127.0.0.1 PORTLESS_URL=${finalUrl} ${commandArgs.join(" ")}
|
|
1045
1128
|
`
|
|
1046
1129
|
)
|
|
@@ -1063,12 +1146,12 @@ portless
|
|
|
1063
1146
|
}
|
|
1064
1147
|
function parseAppPort(value) {
|
|
1065
1148
|
if (!value || value.startsWith("--")) {
|
|
1066
|
-
console.error(
|
|
1149
|
+
console.error(colors_default.red("Error: --app-port requires a port number."));
|
|
1067
1150
|
process.exit(1);
|
|
1068
1151
|
}
|
|
1069
1152
|
const port = parseInt(value, 10);
|
|
1070
1153
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1071
|
-
console.error(
|
|
1154
|
+
console.error(colors_default.red(`Error: Invalid app port "${value}". Must be 1-65535.`));
|
|
1072
1155
|
process.exit(1);
|
|
1073
1156
|
}
|
|
1074
1157
|
return port;
|
|
@@ -1078,7 +1161,7 @@ function appPortFromEnv() {
|
|
|
1078
1161
|
if (!envVal) return void 0;
|
|
1079
1162
|
const port = parseInt(envVal, 10);
|
|
1080
1163
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1081
|
-
console.error(
|
|
1164
|
+
console.error(colors_default.red(`Error: Invalid PORTLESS_APP_PORT="${envVal}". Must be 1-65535.`));
|
|
1082
1165
|
process.exit(1);
|
|
1083
1166
|
}
|
|
1084
1167
|
return port;
|
|
@@ -1094,18 +1177,18 @@ function parseRunArgs(args) {
|
|
|
1094
1177
|
break;
|
|
1095
1178
|
} else if (args[i] === "--help" || args[i] === "-h") {
|
|
1096
1179
|
console.log(`
|
|
1097
|
-
${
|
|
1180
|
+
${colors_default.bold("portless run")} - Infer project name and run through the proxy.
|
|
1098
1181
|
|
|
1099
|
-
${
|
|
1100
|
-
${
|
|
1182
|
+
${colors_default.bold("Usage:")}
|
|
1183
|
+
${colors_default.cyan("portless run [options] <command...>")}
|
|
1101
1184
|
|
|
1102
|
-
${
|
|
1185
|
+
${colors_default.bold("Options:")}
|
|
1103
1186
|
--name <name> Override the inferred base name (worktree prefix still applies)
|
|
1104
1187
|
--force Override an existing route registered by another process
|
|
1105
1188
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
1106
1189
|
--help, -h Show this help
|
|
1107
1190
|
|
|
1108
|
-
${
|
|
1191
|
+
${colors_default.bold("Name inference (in order):")}
|
|
1109
1192
|
1. package.json "name" field (walks up directories)
|
|
1110
1193
|
2. Git repo root directory name
|
|
1111
1194
|
3. Current directory basename
|
|
@@ -1114,10 +1197,10 @@ ${chalk.bold("Name inference (in order):")}
|
|
|
1114
1197
|
In git worktrees, the branch name is prepended as a subdomain prefix
|
|
1115
1198
|
(e.g. feature-auth.myapp.localhost).
|
|
1116
1199
|
|
|
1117
|
-
${
|
|
1118
|
-
portless run next dev # ->
|
|
1119
|
-
portless run --name myapp next dev # ->
|
|
1120
|
-
portless run vite dev # ->
|
|
1200
|
+
${colors_default.bold("Examples:")}
|
|
1201
|
+
portless run next dev # -> https://<project>.localhost
|
|
1202
|
+
portless run --name myapp next dev # -> https://myapp.localhost
|
|
1203
|
+
portless run vite dev # -> https://<project>.localhost
|
|
1121
1204
|
portless run --app-port 3000 pnpm start
|
|
1122
1205
|
`);
|
|
1123
1206
|
process.exit(0);
|
|
@@ -1129,14 +1212,14 @@ ${chalk.bold("Examples:")}
|
|
|
1129
1212
|
} else if (args[i] === "--name") {
|
|
1130
1213
|
i++;
|
|
1131
1214
|
if (!args[i] || args[i].startsWith("-")) {
|
|
1132
|
-
console.error(
|
|
1133
|
-
console.error(
|
|
1215
|
+
console.error(colors_default.red("Error: --name requires a name value."));
|
|
1216
|
+
console.error(colors_default.cyan(" portless run --name <name> <command...>"));
|
|
1134
1217
|
process.exit(1);
|
|
1135
1218
|
}
|
|
1136
1219
|
name = args[i];
|
|
1137
1220
|
} else {
|
|
1138
|
-
console.error(
|
|
1139
|
-
console.error(
|
|
1221
|
+
console.error(colors_default.red(`Error: Unknown flag "${args[i]}".`));
|
|
1222
|
+
console.error(colors_default.blue("Known flags: --name, --force, --app-port, --help"));
|
|
1140
1223
|
process.exit(1);
|
|
1141
1224
|
}
|
|
1142
1225
|
i++;
|
|
@@ -1158,8 +1241,8 @@ function parseAppArgs(args) {
|
|
|
1158
1241
|
i++;
|
|
1159
1242
|
appPort = parseAppPort(args[i]);
|
|
1160
1243
|
} else {
|
|
1161
|
-
console.error(
|
|
1162
|
-
console.error(
|
|
1244
|
+
console.error(colors_default.red(`Error: Unknown flag "${args[i]}".`));
|
|
1245
|
+
console.error(colors_default.blue("Known flags: --force, --app-port"));
|
|
1163
1246
|
process.exit(1);
|
|
1164
1247
|
}
|
|
1165
1248
|
i++;
|
|
@@ -1176,8 +1259,8 @@ function parseAppArgs(args) {
|
|
|
1176
1259
|
i++;
|
|
1177
1260
|
appPort = parseAppPort(args[i]);
|
|
1178
1261
|
} else {
|
|
1179
|
-
console.error(
|
|
1180
|
-
console.error(
|
|
1262
|
+
console.error(colors_default.red(`Error: Unknown flag "${args[i]}".`));
|
|
1263
|
+
console.error(colors_default.blue("Known flags: --force, --app-port"));
|
|
1181
1264
|
process.exit(1);
|
|
1182
1265
|
}
|
|
1183
1266
|
i++;
|
|
@@ -1187,105 +1270,107 @@ function parseAppArgs(args) {
|
|
|
1187
1270
|
}
|
|
1188
1271
|
function printHelp() {
|
|
1189
1272
|
console.log(`
|
|
1190
|
-
${
|
|
1273
|
+
${colors_default.bold("portless")} - Replace port numbers with stable, named .localhost URLs. For humans and agents.
|
|
1191
1274
|
|
|
1192
1275
|
Eliminates port conflicts, memorizing port numbers, and cookie/storage
|
|
1193
1276
|
clashes by giving each dev server a stable .localhost URL.
|
|
1194
1277
|
|
|
1195
|
-
${
|
|
1196
|
-
${
|
|
1278
|
+
${colors_default.bold("Install:")}
|
|
1279
|
+
${colors_default.cyan("npm install -g portless")}
|
|
1197
1280
|
Do NOT add portless as a project dependency.
|
|
1198
1281
|
|
|
1199
|
-
${
|
|
1200
|
-
${
|
|
1201
|
-
${
|
|
1202
|
-
${
|
|
1203
|
-
${
|
|
1204
|
-
${
|
|
1205
|
-
${
|
|
1206
|
-
${
|
|
1207
|
-
${
|
|
1208
|
-
${
|
|
1209
|
-
${
|
|
1210
|
-
${
|
|
1211
|
-
${
|
|
1212
|
-
${
|
|
1282
|
+
${colors_default.bold("Usage:")}
|
|
1283
|
+
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
1284
|
+
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
1285
|
+
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1286
|
+
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
1287
|
+
${colors_default.cyan("portless <name> <cmd>")} Run your app through the proxy
|
|
1288
|
+
${colors_default.cyan("portless run <cmd>")} Infer name from project, run through proxy
|
|
1289
|
+
${colors_default.cyan("portless get <name>")} Print URL for a service (for cross-service refs)
|
|
1290
|
+
${colors_default.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
|
|
1291
|
+
${colors_default.cyan("portless alias --remove <name>")} Remove a static route
|
|
1292
|
+
${colors_default.cyan("portless list")} Show active routes
|
|
1293
|
+
${colors_default.cyan("portless trust")} Add local CA to system trust store
|
|
1294
|
+
${colors_default.cyan("portless hosts sync")} Add routes to ${HOSTS_DISPLAY} (fixes Safari)
|
|
1295
|
+
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1213
1296
|
|
|
1214
|
-
${
|
|
1215
|
-
portless proxy start # Start proxy on port
|
|
1216
|
-
portless proxy start --
|
|
1217
|
-
portless myapp next dev # ->
|
|
1218
|
-
portless myapp vite dev # ->
|
|
1219
|
-
portless api.myapp pnpm start # ->
|
|
1220
|
-
portless run next dev # ->
|
|
1221
|
-
portless run next dev # in worktree ->
|
|
1222
|
-
portless get backend # ->
|
|
1297
|
+
${colors_default.bold("Examples:")}
|
|
1298
|
+
portless proxy start # Start HTTPS proxy on port 443
|
|
1299
|
+
portless proxy start --no-tls # Start HTTP proxy on port 80
|
|
1300
|
+
portless myapp next dev # -> https://myapp.localhost
|
|
1301
|
+
portless myapp vite dev # -> https://myapp.localhost
|
|
1302
|
+
portless api.myapp pnpm start # -> https://api.myapp.localhost
|
|
1303
|
+
portless run next dev # -> https://<project>.localhost
|
|
1304
|
+
portless run next dev # in worktree -> https://<worktree>.<project>.localhost
|
|
1305
|
+
portless get backend # -> https://backend.localhost (for cross-service refs)
|
|
1223
1306
|
# Wildcard subdomains: tenant.myapp.localhost also routes to myapp
|
|
1224
1307
|
|
|
1225
|
-
${
|
|
1308
|
+
${colors_default.bold("In package.json:")}
|
|
1226
1309
|
{
|
|
1227
1310
|
"scripts": {
|
|
1228
1311
|
"dev": "portless run next dev"
|
|
1229
1312
|
}
|
|
1230
1313
|
}
|
|
1231
1314
|
|
|
1232
|
-
${
|
|
1233
|
-
1. Start the proxy once (
|
|
1315
|
+
${colors_default.bold("How it works:")}
|
|
1316
|
+
1. Start the proxy once (HTTPS on port 443 by default, auto-elevates with sudo)
|
|
1234
1317
|
2. Run your apps - they auto-start the proxy and register automatically
|
|
1235
1318
|
(apps get a random port in the 4000-4999 range via PORT)
|
|
1236
|
-
3. Access via
|
|
1319
|
+
3. Access via https://<name>.localhost
|
|
1237
1320
|
4. .localhost domains auto-resolve to 127.0.0.1
|
|
1238
1321
|
5. Frameworks that ignore PORT (Vite, Astro, React Router, Angular,
|
|
1239
1322
|
Expo, React Native) get --port and --host flags injected automatically
|
|
1240
1323
|
|
|
1241
|
-
${
|
|
1242
|
-
|
|
1324
|
+
${colors_default.bold("HTTP/2 + HTTPS (default):")}
|
|
1325
|
+
HTTPS with HTTP/2 multiplexing is enabled by default (faster page loads).
|
|
1243
1326
|
On first use, portless generates a local CA and adds it to your
|
|
1244
|
-
system trust store. No browser warnings.
|
|
1327
|
+
system trust store. No browser warnings. Disable with --no-tls.
|
|
1245
1328
|
|
|
1246
|
-
${
|
|
1329
|
+
${colors_default.bold("Options:")}
|
|
1247
1330
|
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
1248
1331
|
Adds worktree prefix in git worktrees
|
|
1249
|
-
-p, --port <number> Port for the proxy
|
|
1250
|
-
|
|
1251
|
-
--
|
|
1252
|
-
--
|
|
1253
|
-
--
|
|
1254
|
-
--
|
|
1332
|
+
-p, --port <number> Port for the proxy (default: 443, or 80 with --no-tls)
|
|
1333
|
+
Standard ports auto-elevate with sudo on macOS/Linux
|
|
1334
|
+
--no-tls Disable HTTPS (use plain HTTP on port 80)
|
|
1335
|
+
--https Enable HTTPS (default, accepted for compatibility)
|
|
1336
|
+
--cert <path> Use a custom TLS certificate
|
|
1337
|
+
--key <path> Use a custom TLS private key
|
|
1255
1338
|
--foreground Run proxy in foreground (for debugging)
|
|
1256
1339
|
--tld <tld> Use a custom TLD instead of .localhost (e.g. test, dev)
|
|
1340
|
+
--wildcard Allow unregistered subdomains to fall back to parent route
|
|
1257
1341
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
1258
1342
|
--force Override an existing route registered by another process
|
|
1259
1343
|
--name <name> Use <name> as the app name (bypasses subcommand dispatch)
|
|
1260
1344
|
-- Stop flag parsing; everything after is passed to the child
|
|
1261
1345
|
|
|
1262
|
-
${
|
|
1346
|
+
${colors_default.bold("Environment variables:")}
|
|
1263
1347
|
PORTLESS_PORT=<number> Override the default proxy port (e.g. in .bashrc)
|
|
1264
1348
|
PORTLESS_APP_PORT=<number> Use a fixed port for the app (same as --app-port)
|
|
1265
|
-
PORTLESS_HTTPS
|
|
1349
|
+
PORTLESS_HTTPS HTTPS on by default; set to 0 to disable (same as --no-tls)
|
|
1266
1350
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1351
|
+
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
1267
1352
|
PORTLESS_SYNC_HOSTS=1 Auto-sync ${HOSTS_DISPLAY} (auto-enabled for custom TLDs)
|
|
1268
1353
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
1269
1354
|
PORTLESS=0 Run command directly without proxy
|
|
1270
1355
|
|
|
1271
|
-
${
|
|
1356
|
+
${colors_default.bold("Child process environment:")}
|
|
1272
1357
|
PORT Ephemeral port the child should listen on
|
|
1273
1358
|
HOST Always 127.0.0.1
|
|
1274
|
-
PORTLESS_URL Public URL of the app (e.g.
|
|
1359
|
+
PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
|
|
1275
1360
|
|
|
1276
|
-
${
|
|
1361
|
+
${colors_default.bold("Safari / DNS:")}
|
|
1277
1362
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1278
1363
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1279
1364
|
Auto-syncs ${HOSTS_DISPLAY} for custom TLDs (e.g. --tld test). For .localhost,
|
|
1280
1365
|
set PORTLESS_SYNC_HOSTS=1 to enable. To manually sync:
|
|
1281
|
-
${
|
|
1366
|
+
${colors_default.cyan("portless hosts sync")}
|
|
1282
1367
|
Clean up later with:
|
|
1283
|
-
${
|
|
1368
|
+
${colors_default.cyan("portless hosts clean")}
|
|
1284
1369
|
|
|
1285
|
-
${
|
|
1370
|
+
${colors_default.bold("Skip portless:")}
|
|
1286
1371
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
1287
1372
|
|
|
1288
|
-
${
|
|
1373
|
+
${colors_default.bold("Reserved names:")}
|
|
1289
1374
|
run, get, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
1290
1375
|
be used as app names directly. Use "portless run" to infer the name,
|
|
1291
1376
|
or "portless --name <name>" to force any name including reserved ones.
|
|
@@ -1293,38 +1378,44 @@ ${chalk.bold("Reserved names:")}
|
|
|
1293
1378
|
process.exit(0);
|
|
1294
1379
|
}
|
|
1295
1380
|
function printVersion() {
|
|
1296
|
-
console.log("0.
|
|
1381
|
+
console.log("0.9.0");
|
|
1297
1382
|
process.exit(0);
|
|
1298
1383
|
}
|
|
1299
1384
|
async function handleTrust() {
|
|
1300
1385
|
const { dir } = await discoverState();
|
|
1301
1386
|
const result = trustCA(dir);
|
|
1302
1387
|
if (result.trusted) {
|
|
1303
|
-
console.log(
|
|
1304
|
-
console.log(
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1388
|
+
console.log(colors_default.green("Local CA added to system trust store."));
|
|
1389
|
+
console.log(colors_default.gray("Browsers will now trust portless HTTPS certificates."));
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
const isPermissionError = result.error?.includes("Permission denied") || result.error?.includes("EACCES");
|
|
1393
|
+
if (isPermissionError && !isWindows && process.getuid?.() !== 0) {
|
|
1394
|
+
console.log(colors_default.yellow("Trusting the CA requires elevated privileges. Requesting sudo..."));
|
|
1395
|
+
const sudoResult = spawnSync("sudo", [process.execPath, getEntryScript(), "trust"], {
|
|
1396
|
+
stdio: "inherit",
|
|
1397
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1398
|
+
});
|
|
1399
|
+
if (sudoResult.status === 0) return;
|
|
1400
|
+
console.error(colors_default.red("sudo elevation also failed."));
|
|
1312
1401
|
}
|
|
1402
|
+
console.error(colors_default.red(`Failed to trust CA: ${result.error}`));
|
|
1403
|
+
process.exit(1);
|
|
1313
1404
|
}
|
|
1314
1405
|
async function handleList() {
|
|
1315
1406
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
1316
1407
|
const store = new RouteStore(dir, {
|
|
1317
|
-
onWarning: (msg) => console.warn(
|
|
1408
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1318
1409
|
});
|
|
1319
1410
|
listRoutes(store, port, tls2);
|
|
1320
1411
|
}
|
|
1321
1412
|
async function handleGet(args) {
|
|
1322
1413
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
1323
1414
|
console.log(`
|
|
1324
|
-
${
|
|
1415
|
+
${colors_default.bold("portless get")} - Print the URL for a service.
|
|
1325
1416
|
|
|
1326
|
-
${
|
|
1327
|
-
${
|
|
1417
|
+
${colors_default.bold("Usage:")}
|
|
1418
|
+
${colors_default.cyan("portless get <name>")}
|
|
1328
1419
|
|
|
1329
1420
|
Constructs the URL using the same hostname and worktree logic as
|
|
1330
1421
|
"portless run", then prints it to stdout. Useful for wiring services
|
|
@@ -1332,14 +1423,14 @@ together:
|
|
|
1332
1423
|
|
|
1333
1424
|
BACKEND_URL=$(portless get backend)
|
|
1334
1425
|
|
|
1335
|
-
${
|
|
1426
|
+
${colors_default.bold("Options:")}
|
|
1336
1427
|
--no-worktree Skip worktree prefix detection
|
|
1337
1428
|
--help, -h Show this help
|
|
1338
1429
|
|
|
1339
|
-
${
|
|
1340
|
-
portless get backend # ->
|
|
1341
|
-
portless get backend # in worktree ->
|
|
1342
|
-
portless get backend --no-worktree # ->
|
|
1430
|
+
${colors_default.bold("Examples:")}
|
|
1431
|
+
portless get backend # -> https://backend.localhost
|
|
1432
|
+
portless get backend # in worktree -> https://auth.backend.localhost
|
|
1433
|
+
portless get backend --no-worktree # -> https://backend.localhost (skip worktree)
|
|
1343
1434
|
`);
|
|
1344
1435
|
process.exit(0);
|
|
1345
1436
|
}
|
|
@@ -1349,19 +1440,19 @@ ${chalk.bold("Examples:")}
|
|
|
1349
1440
|
if (args[i] === "--no-worktree") {
|
|
1350
1441
|
skipWorktree = true;
|
|
1351
1442
|
} else if (args[i].startsWith("-")) {
|
|
1352
|
-
console.error(
|
|
1353
|
-
console.error(
|
|
1443
|
+
console.error(colors_default.red(`Error: Unknown flag "${args[i]}".`));
|
|
1444
|
+
console.error(colors_default.blue("Known flags: --no-worktree, --help"));
|
|
1354
1445
|
process.exit(1);
|
|
1355
1446
|
} else {
|
|
1356
1447
|
positional.push(args[i]);
|
|
1357
1448
|
}
|
|
1358
1449
|
}
|
|
1359
1450
|
if (positional.length === 0) {
|
|
1360
|
-
console.error(
|
|
1361
|
-
console.error(
|
|
1362
|
-
console.error(
|
|
1363
|
-
console.error(
|
|
1364
|
-
console.error(
|
|
1451
|
+
console.error(colors_default.red("Error: Missing service name."));
|
|
1452
|
+
console.error(colors_default.blue("Usage:"));
|
|
1453
|
+
console.error(colors_default.cyan(" portless get <name>"));
|
|
1454
|
+
console.error(colors_default.blue("Example:"));
|
|
1455
|
+
console.error(colors_default.cyan(" portless get backend"));
|
|
1365
1456
|
process.exit(1);
|
|
1366
1457
|
}
|
|
1367
1458
|
const name = positional[0];
|
|
@@ -1375,76 +1466,76 @@ ${chalk.bold("Examples:")}
|
|
|
1375
1466
|
async function handleAlias(args) {
|
|
1376
1467
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
1377
1468
|
console.log(`
|
|
1378
|
-
${
|
|
1469
|
+
${colors_default.bold("portless alias")} - Register a static route for services not managed by portless.
|
|
1379
1470
|
|
|
1380
|
-
${
|
|
1381
|
-
${
|
|
1382
|
-
${
|
|
1383
|
-
${
|
|
1471
|
+
${colors_default.bold("Usage:")}
|
|
1472
|
+
${colors_default.cyan("portless alias <name> <port>")} Register a route
|
|
1473
|
+
${colors_default.cyan("portless alias --remove <name>")} Remove a route
|
|
1474
|
+
${colors_default.cyan("portless alias <name> <port> --force")} Override existing route
|
|
1384
1475
|
|
|
1385
|
-
${
|
|
1386
|
-
portless alias my-postgres 5432 # ->
|
|
1387
|
-
portless alias redis 6379 # ->
|
|
1476
|
+
${colors_default.bold("Examples:")}
|
|
1477
|
+
portless alias my-postgres 5432 # -> https://my-postgres.localhost
|
|
1478
|
+
portless alias redis 6379 # -> https://redis.localhost
|
|
1388
1479
|
portless alias --remove my-postgres # Remove the alias
|
|
1389
1480
|
`);
|
|
1390
1481
|
process.exit(0);
|
|
1391
1482
|
}
|
|
1392
1483
|
const { dir, tld } = await discoverState();
|
|
1393
1484
|
const store = new RouteStore(dir, {
|
|
1394
|
-
onWarning: (msg) => console.warn(
|
|
1485
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1395
1486
|
});
|
|
1396
1487
|
if (args[1] === "--remove") {
|
|
1397
1488
|
const aliasName2 = args[2];
|
|
1398
1489
|
if (!aliasName2) {
|
|
1399
|
-
console.error(
|
|
1400
|
-
console.error(
|
|
1490
|
+
console.error(colors_default.red("Error: No alias name provided."));
|
|
1491
|
+
console.error(colors_default.cyan(" portless alias --remove <name>"));
|
|
1401
1492
|
process.exit(1);
|
|
1402
1493
|
}
|
|
1403
1494
|
const hostname2 = parseHostname(aliasName2, tld);
|
|
1404
1495
|
const routes = store.loadRoutes();
|
|
1405
1496
|
const existing = routes.find((r) => r.hostname === hostname2 && r.pid === 0);
|
|
1406
1497
|
if (!existing) {
|
|
1407
|
-
console.error(
|
|
1498
|
+
console.error(colors_default.red(`Error: No alias found for "${hostname2}".`));
|
|
1408
1499
|
process.exit(1);
|
|
1409
1500
|
}
|
|
1410
1501
|
store.removeRoute(hostname2);
|
|
1411
|
-
console.log(
|
|
1502
|
+
console.log(colors_default.green(`Removed alias: ${hostname2}`));
|
|
1412
1503
|
return;
|
|
1413
1504
|
}
|
|
1414
1505
|
const aliasName = args[1];
|
|
1415
1506
|
const aliasPort = args[2];
|
|
1416
1507
|
if (!aliasName || !aliasPort) {
|
|
1417
|
-
console.error(
|
|
1418
|
-
console.error(
|
|
1419
|
-
console.error(
|
|
1420
|
-
console.error(
|
|
1421
|
-
console.error(
|
|
1422
|
-
console.error(
|
|
1508
|
+
console.error(colors_default.red("Error: Missing arguments."));
|
|
1509
|
+
console.error(colors_default.blue("Usage:"));
|
|
1510
|
+
console.error(colors_default.cyan(" portless alias <name> <port>"));
|
|
1511
|
+
console.error(colors_default.cyan(" portless alias --remove <name>"));
|
|
1512
|
+
console.error(colors_default.blue("Example:"));
|
|
1513
|
+
console.error(colors_default.cyan(" portless alias my-postgres 5432"));
|
|
1423
1514
|
process.exit(1);
|
|
1424
1515
|
}
|
|
1425
1516
|
const hostname = parseHostname(aliasName, tld);
|
|
1426
1517
|
const port = parseInt(aliasPort, 10);
|
|
1427
1518
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
1428
|
-
console.error(
|
|
1519
|
+
console.error(colors_default.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
|
|
1429
1520
|
process.exit(1);
|
|
1430
1521
|
}
|
|
1431
1522
|
const force = args.includes("--force");
|
|
1432
1523
|
store.addRoute(hostname, port, 0, force);
|
|
1433
|
-
console.log(
|
|
1524
|
+
console.log(colors_default.green(`Alias registered: ${hostname} -> 127.0.0.1:${port}`));
|
|
1434
1525
|
}
|
|
1435
1526
|
async function handleHosts(args) {
|
|
1436
1527
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
1437
1528
|
console.log(`
|
|
1438
|
-
${
|
|
1529
|
+
${colors_default.bold("portless hosts")} - Manage ${HOSTS_DISPLAY} entries for .localhost subdomains.
|
|
1439
1530
|
|
|
1440
1531
|
Safari relies on the system DNS resolver, which may not handle .localhost
|
|
1441
1532
|
subdomains. This command adds entries to ${HOSTS_DISPLAY} as a workaround.
|
|
1442
1533
|
|
|
1443
|
-
${
|
|
1444
|
-
${
|
|
1445
|
-
${
|
|
1534
|
+
${colors_default.bold("Usage:")}
|
|
1535
|
+
${colors_default.cyan("portless hosts sync")} Add current routes to ${HOSTS_DISPLAY}
|
|
1536
|
+
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1446
1537
|
|
|
1447
|
-
${
|
|
1538
|
+
${colors_default.bold("Auto-sync:")}
|
|
1448
1539
|
Auto-enabled for custom TLDs (e.g. --tld test). For .localhost, set
|
|
1449
1540
|
PORTLESS_SYNC_HOSTS=1 to enable. Disable with PORTLESS_SYNC_HOSTS=0.
|
|
1450
1541
|
`);
|
|
@@ -1452,114 +1543,137 @@ ${chalk.bold("Auto-sync:")}
|
|
|
1452
1543
|
}
|
|
1453
1544
|
if (args[1] === "clean") {
|
|
1454
1545
|
if (cleanHostsFile()) {
|
|
1455
|
-
console.log(
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1546
|
+
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (!isWindows && process.getuid?.() !== 0) {
|
|
1550
|
+
console.log(
|
|
1551
|
+
colors_default.yellow(
|
|
1552
|
+
`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`
|
|
1460
1553
|
)
|
|
1461
1554
|
);
|
|
1462
|
-
|
|
1463
|
-
|
|
1555
|
+
const result = spawnSync(
|
|
1556
|
+
"sudo",
|
|
1557
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "clean"],
|
|
1558
|
+
{
|
|
1559
|
+
stdio: "inherit",
|
|
1560
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1561
|
+
}
|
|
1562
|
+
);
|
|
1563
|
+
if (result.status === 0) return;
|
|
1464
1564
|
}
|
|
1565
|
+
console.error(
|
|
1566
|
+
colors_default.red(`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`)
|
|
1567
|
+
);
|
|
1568
|
+
process.exit(1);
|
|
1465
1569
|
return;
|
|
1466
1570
|
}
|
|
1467
1571
|
if (!args[1]) {
|
|
1468
1572
|
console.log(`
|
|
1469
|
-
${
|
|
1573
|
+
${colors_default.bold("Usage: portless hosts <command>")}
|
|
1470
1574
|
|
|
1471
|
-
${
|
|
1472
|
-
${
|
|
1575
|
+
${colors_default.cyan("portless hosts sync")} Add current routes to ${HOSTS_DISPLAY}
|
|
1576
|
+
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1473
1577
|
`);
|
|
1474
1578
|
process.exit(0);
|
|
1475
1579
|
}
|
|
1476
1580
|
if (args[1] !== "sync") {
|
|
1477
|
-
console.error(
|
|
1478
|
-
console.error(
|
|
1479
|
-
console.error(
|
|
1480
|
-
|
|
1481
|
-
);
|
|
1482
|
-
console.error(chalk.cyan(` ${SUDO_PREFIX}portless hosts clean # Remove portless entries`));
|
|
1581
|
+
console.error(colors_default.red(`Error: Unknown hosts subcommand "${args[1]}".`));
|
|
1582
|
+
console.error(colors_default.blue("Usage:"));
|
|
1583
|
+
console.error(colors_default.cyan(` portless hosts sync # Add routes to ${HOSTS_DISPLAY}`));
|
|
1584
|
+
console.error(colors_default.cyan(" portless hosts clean # Remove portless entries"));
|
|
1483
1585
|
process.exit(1);
|
|
1484
1586
|
}
|
|
1485
1587
|
const { dir } = await discoverState();
|
|
1486
1588
|
const store = new RouteStore(dir, {
|
|
1487
|
-
onWarning: (msg) => console.warn(
|
|
1589
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1488
1590
|
});
|
|
1489
1591
|
const routes = store.loadRoutes();
|
|
1490
1592
|
if (routes.length === 0) {
|
|
1491
|
-
console.log(
|
|
1593
|
+
console.log(colors_default.yellow("No active routes to sync."));
|
|
1492
1594
|
return;
|
|
1493
1595
|
}
|
|
1494
1596
|
const hostnames = routes.map((r) => r.hostname);
|
|
1495
1597
|
if (syncHostsFile(hostnames)) {
|
|
1496
|
-
console.log(
|
|
1598
|
+
console.log(colors_default.green(`Synced ${hostnames.length} hostname(s) to ${HOSTS_DISPLAY}:`));
|
|
1497
1599
|
for (const h of hostnames) {
|
|
1498
|
-
console.log(
|
|
1600
|
+
console.log(colors_default.cyan(` 127.0.0.1 ${h}`));
|
|
1499
1601
|
}
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
)
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (!isWindows && process.getuid?.() !== 0) {
|
|
1605
|
+
console.log(
|
|
1606
|
+
colors_default.yellow(`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
1505
1607
|
);
|
|
1506
|
-
|
|
1507
|
-
|
|
1608
|
+
const result = spawnSync(
|
|
1609
|
+
"sudo",
|
|
1610
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "hosts", "sync"],
|
|
1611
|
+
{
|
|
1612
|
+
stdio: "inherit",
|
|
1613
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1614
|
+
}
|
|
1615
|
+
);
|
|
1616
|
+
if (result.status === 0) return;
|
|
1508
1617
|
}
|
|
1618
|
+
console.error(
|
|
1619
|
+
colors_default.red(`Failed to update ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`)
|
|
1620
|
+
);
|
|
1621
|
+
process.exit(1);
|
|
1509
1622
|
}
|
|
1510
1623
|
async function handleProxy(args) {
|
|
1511
1624
|
if (args[1] === "stop") {
|
|
1512
|
-
|
|
1513
|
-
const
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1625
|
+
let explicitPort;
|
|
1626
|
+
const portIdx = args.indexOf("--port") !== -1 ? args.indexOf("--port") : args.indexOf("-p");
|
|
1627
|
+
if (portIdx !== -1) {
|
|
1628
|
+
const portValue = args[portIdx + 1];
|
|
1629
|
+
if (portValue && !portValue.startsWith("-")) {
|
|
1630
|
+
const parsed = parseInt(portValue, 10);
|
|
1631
|
+
if (!isNaN(parsed) && parsed >= 1 && parsed <= 65535) {
|
|
1632
|
+
explicitPort = parsed;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (explicitPort !== void 0) {
|
|
1637
|
+
const dir = resolveStateDir(explicitPort);
|
|
1638
|
+
const store2 = new RouteStore(dir, {
|
|
1639
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1640
|
+
});
|
|
1641
|
+
await stopProxy(store2, explicitPort, false);
|
|
1642
|
+
} else {
|
|
1643
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
1644
|
+
const store2 = new RouteStore(dir, {
|
|
1645
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1646
|
+
});
|
|
1647
|
+
await stopProxy(store2, port, tls2);
|
|
1648
|
+
}
|
|
1517
1649
|
return;
|
|
1518
1650
|
}
|
|
1519
1651
|
const isProxyHelp = args[1] === "--help" || args[1] === "-h";
|
|
1520
1652
|
if (isProxyHelp || args[1] !== "start") {
|
|
1521
1653
|
console.log(`
|
|
1522
|
-
${
|
|
1654
|
+
${colors_default.bold("portless proxy")} - Manage the portless proxy server.
|
|
1523
1655
|
|
|
1524
|
-
${
|
|
1525
|
-
${
|
|
1526
|
-
${
|
|
1527
|
-
${
|
|
1528
|
-
${
|
|
1529
|
-
${
|
|
1530
|
-
${
|
|
1656
|
+
${colors_default.bold("Usage:")}
|
|
1657
|
+
${colors_default.cyan("portless proxy start")} Start the HTTPS proxy on port 443 (daemon)
|
|
1658
|
+
${colors_default.cyan("portless proxy start --no-tls")} Start without HTTPS (port 80)
|
|
1659
|
+
${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
1660
|
+
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
1661
|
+
${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
1662
|
+
${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
|
|
1663
|
+
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
1531
1664
|
`);
|
|
1532
1665
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
1533
1666
|
}
|
|
1534
1667
|
const isForeground = args.includes("--foreground");
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
1538
|
-
if (portFlagIndex !== -1) {
|
|
1539
|
-
const portValue = args[portFlagIndex + 1];
|
|
1540
|
-
if (!portValue || portValue.startsWith("-")) {
|
|
1541
|
-
console.error(chalk.red("Error: --port / -p requires a port number."));
|
|
1542
|
-
console.error(chalk.blue("Usage:"));
|
|
1543
|
-
console.error(chalk.cyan(" portless proxy start -p 8080"));
|
|
1544
|
-
process.exit(1);
|
|
1545
|
-
}
|
|
1546
|
-
proxyPort = parseInt(portValue, 10);
|
|
1547
|
-
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
1548
|
-
console.error(chalk.red(`Error: Invalid port number: ${portValue}`));
|
|
1549
|
-
console.error(chalk.blue("Port must be between 1 and 65535."));
|
|
1550
|
-
process.exit(1);
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
const hasNoTls = args.includes("--no-tls");
|
|
1554
|
-
const hasHttpsFlag = args.includes("--https");
|
|
1555
|
-
const wantHttps = !hasNoTls && (hasHttpsFlag || isHttpsEnvEnabled());
|
|
1668
|
+
const hasNoTls = args.includes("--no-tls") || isHttpsEnvDisabled();
|
|
1669
|
+
const wantHttps = !hasNoTls;
|
|
1556
1670
|
let customCertPath = null;
|
|
1557
1671
|
let customKeyPath = null;
|
|
1558
1672
|
const certIdx = args.indexOf("--cert");
|
|
1559
1673
|
if (certIdx !== -1) {
|
|
1560
1674
|
customCertPath = args[certIdx + 1] || null;
|
|
1561
1675
|
if (!customCertPath || customCertPath.startsWith("-")) {
|
|
1562
|
-
console.error(
|
|
1676
|
+
console.error(colors_default.red("Error: --cert requires a file path."));
|
|
1563
1677
|
process.exit(1);
|
|
1564
1678
|
}
|
|
1565
1679
|
}
|
|
@@ -1567,76 +1681,158 @@ ${chalk.bold("Usage:")}
|
|
|
1567
1681
|
if (keyIdx !== -1) {
|
|
1568
1682
|
customKeyPath = args[keyIdx + 1] || null;
|
|
1569
1683
|
if (!customKeyPath || customKeyPath.startsWith("-")) {
|
|
1570
|
-
console.error(
|
|
1684
|
+
console.error(colors_default.red("Error: --key requires a file path."));
|
|
1571
1685
|
process.exit(1);
|
|
1572
1686
|
}
|
|
1573
1687
|
}
|
|
1574
1688
|
if (customCertPath && !customKeyPath || !customCertPath && customKeyPath) {
|
|
1575
|
-
console.error(
|
|
1689
|
+
console.error(colors_default.red("Error: --cert and --key must be used together."));
|
|
1576
1690
|
process.exit(1);
|
|
1577
1691
|
}
|
|
1692
|
+
const useHttps = wantHttps || !!(customCertPath && customKeyPath);
|
|
1693
|
+
let hasExplicitPort = false;
|
|
1694
|
+
let proxyPort = getDefaultPort(useHttps);
|
|
1695
|
+
let portFlagIndex = args.indexOf("--port");
|
|
1696
|
+
if (portFlagIndex === -1) portFlagIndex = args.indexOf("-p");
|
|
1697
|
+
if (portFlagIndex !== -1) {
|
|
1698
|
+
const portValue = args[portFlagIndex + 1];
|
|
1699
|
+
if (!portValue || portValue.startsWith("-")) {
|
|
1700
|
+
console.error(colors_default.red("Error: --port / -p requires a port number."));
|
|
1701
|
+
console.error(colors_default.blue("Usage:"));
|
|
1702
|
+
console.error(colors_default.cyan(" portless proxy start -p 8080"));
|
|
1703
|
+
process.exit(1);
|
|
1704
|
+
}
|
|
1705
|
+
proxyPort = parseInt(portValue, 10);
|
|
1706
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
1707
|
+
console.error(colors_default.red(`Error: Invalid port number: ${portValue}`));
|
|
1708
|
+
console.error(colors_default.blue("Port must be between 1 and 65535."));
|
|
1709
|
+
process.exit(1);
|
|
1710
|
+
}
|
|
1711
|
+
hasExplicitPort = true;
|
|
1712
|
+
}
|
|
1578
1713
|
let tld;
|
|
1579
1714
|
try {
|
|
1580
1715
|
tld = getDefaultTld();
|
|
1581
1716
|
} catch (err) {
|
|
1582
|
-
console.error(
|
|
1717
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
1583
1718
|
process.exit(1);
|
|
1584
1719
|
}
|
|
1585
1720
|
const tldIdx = args.indexOf("--tld");
|
|
1586
1721
|
if (tldIdx !== -1) {
|
|
1587
1722
|
const tldValue = args[tldIdx + 1];
|
|
1588
1723
|
if (!tldValue || tldValue.startsWith("-")) {
|
|
1589
|
-
console.error(
|
|
1724
|
+
console.error(colors_default.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
|
|
1590
1725
|
process.exit(1);
|
|
1591
1726
|
}
|
|
1592
1727
|
tld = tldValue.trim().toLowerCase();
|
|
1593
1728
|
const tldErr = validateTld(tld);
|
|
1594
1729
|
if (tldErr) {
|
|
1595
|
-
console.error(
|
|
1730
|
+
console.error(colors_default.red(`Error: ${tldErr}`));
|
|
1596
1731
|
process.exit(1);
|
|
1597
1732
|
}
|
|
1598
1733
|
}
|
|
1599
1734
|
const riskyReason = RISKY_TLDS.get(tld);
|
|
1600
1735
|
if (riskyReason) {
|
|
1601
|
-
console.warn(
|
|
1736
|
+
console.warn(colors_default.yellow(`Warning: .${tld}: ${riskyReason}`));
|
|
1602
1737
|
}
|
|
1603
1738
|
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
1604
1739
|
if (tld !== DEFAULT_TLD && syncDisabled) {
|
|
1605
1740
|
console.warn(
|
|
1606
|
-
|
|
1741
|
+
colors_default.yellow(
|
|
1607
1742
|
`Warning: .${tld} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
|
|
1608
1743
|
)
|
|
1609
1744
|
);
|
|
1610
|
-
console.warn(
|
|
1611
|
-
console.warn(
|
|
1745
|
+
console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
1746
|
+
console.warn(colors_default.cyan(" portless hosts sync"));
|
|
1612
1747
|
}
|
|
1613
|
-
const
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
onWarning: (msg) => console.warn(
|
|
1748
|
+
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
1749
|
+
let stateDir = resolveStateDir(proxyPort);
|
|
1750
|
+
let store = new RouteStore(stateDir, {
|
|
1751
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1617
1752
|
});
|
|
1618
1753
|
if (await isProxyRunning(proxyPort)) {
|
|
1619
1754
|
if (isForeground) {
|
|
1620
1755
|
return;
|
|
1621
1756
|
}
|
|
1622
|
-
const
|
|
1623
|
-
|
|
1624
|
-
const portFlag = proxyPort !== getDefaultPort() ? ` -p ${proxyPort}` : "";
|
|
1625
|
-
console.log(chalk.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1757
|
+
const portFlag = proxyPort !== getProtocolPort(useHttps) ? ` -p ${proxyPort}` : "";
|
|
1758
|
+
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1626
1759
|
console.log(
|
|
1627
|
-
|
|
1628
|
-
`To restart: ${sudoPrefix}portless proxy stop${portFlag} && ${sudoPrefix}portless proxy start${portFlag}`
|
|
1629
|
-
)
|
|
1760
|
+
colors_default.blue(`To restart: portless proxy stop${portFlag} && portless proxy start${portFlag}`)
|
|
1630
1761
|
);
|
|
1631
1762
|
return;
|
|
1632
1763
|
}
|
|
1633
1764
|
if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1765
|
+
const baseArgs = [
|
|
1766
|
+
process.execPath,
|
|
1767
|
+
getEntryScript(),
|
|
1768
|
+
"proxy",
|
|
1769
|
+
"start",
|
|
1770
|
+
"-p",
|
|
1771
|
+
String(proxyPort)
|
|
1772
|
+
];
|
|
1773
|
+
const optionalFlags = [];
|
|
1774
|
+
if (hasNoTls) optionalFlags.push("--no-tls");
|
|
1775
|
+
if (tld !== DEFAULT_TLD) optionalFlags.push("--tld", tld);
|
|
1776
|
+
if (useWildcard) optionalFlags.push("--wildcard");
|
|
1777
|
+
if (isForeground) optionalFlags.push("--foreground");
|
|
1778
|
+
if (customCertPath && customKeyPath)
|
|
1779
|
+
optionalFlags.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1780
|
+
const startArgs = [...baseArgs, ...optionalFlags];
|
|
1781
|
+
const extraFlags = optionalFlags.map((a) => ` ${a}`).join("");
|
|
1782
|
+
console.log(
|
|
1783
|
+
colors_default.yellow(`Port ${proxyPort} requires elevated privileges. Requesting sudo...`)
|
|
1784
|
+
);
|
|
1785
|
+
if (!hasExplicitPort) {
|
|
1786
|
+
console.log(
|
|
1787
|
+
colors_default.gray(
|
|
1788
|
+
`(To skip sudo, use an unprivileged port: portless proxy start -p ${FALLBACK_PROXY_PORT}${extraFlags})`
|
|
1789
|
+
)
|
|
1790
|
+
);
|
|
1791
|
+
}
|
|
1792
|
+
const result = spawnSync("sudo", ["env", ...collectPortlessEnvArgs(), ...startArgs], {
|
|
1793
|
+
stdio: "inherit",
|
|
1794
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
1795
|
+
});
|
|
1796
|
+
if (result.status === 0) {
|
|
1797
|
+
if (!isForeground) {
|
|
1798
|
+
if (await waitForProxy(proxyPort)) {
|
|
1799
|
+
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
1800
|
+
} else {
|
|
1801
|
+
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
1802
|
+
const logPath2 = path3.join(resolveStateDir(proxyPort), "proxy.log");
|
|
1803
|
+
if (fs3.existsSync(logPath2)) {
|
|
1804
|
+
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (result.signal) {
|
|
1811
|
+
process.exit(1);
|
|
1812
|
+
}
|
|
1813
|
+
if (!hasExplicitPort) {
|
|
1814
|
+
proxyPort = FALLBACK_PROXY_PORT;
|
|
1815
|
+
console.log(colors_default.yellow(`Falling back to port ${proxyPort}.`));
|
|
1816
|
+
console.log(
|
|
1817
|
+
colors_default.blue(`For clean URLs without port numbers, re-run and accept the sudo prompt:`)
|
|
1818
|
+
);
|
|
1819
|
+
console.log(colors_default.cyan(` portless proxy start${extraFlags}`));
|
|
1820
|
+
if (await isProxyRunning(proxyPort)) {
|
|
1821
|
+
console.log(colors_default.yellow(`Proxy is already running on port ${proxyPort}.`));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
stateDir = resolveStateDir(proxyPort);
|
|
1825
|
+
store = new RouteStore(stateDir, {
|
|
1826
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1827
|
+
});
|
|
1828
|
+
} else {
|
|
1829
|
+
console.error(
|
|
1830
|
+
colors_default.red(`Error: Port ${proxyPort} requires elevated privileges and sudo failed.`)
|
|
1831
|
+
);
|
|
1832
|
+
console.error(colors_default.blue("Try again (portless will prompt for sudo):"));
|
|
1833
|
+
console.error(colors_default.cyan(` portless proxy start -p ${proxyPort}${extraFlags}`));
|
|
1834
|
+
process.exit(1);
|
|
1835
|
+
}
|
|
1640
1836
|
}
|
|
1641
1837
|
let tlsOptions;
|
|
1642
1838
|
if (useHttps) {
|
|
@@ -1648,43 +1844,45 @@ ${chalk.bold("Usage:")}
|
|
|
1648
1844
|
const certStr = cert.toString("utf-8");
|
|
1649
1845
|
const keyStr = key.toString("utf-8");
|
|
1650
1846
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
1651
|
-
console.error(
|
|
1652
|
-
console.error(
|
|
1847
|
+
console.error(colors_default.red(`Error: ${customCertPath} is not a valid PEM certificate.`));
|
|
1848
|
+
console.error(colors_default.gray("Expected a file starting with -----BEGIN CERTIFICATE-----"));
|
|
1653
1849
|
process.exit(1);
|
|
1654
1850
|
}
|
|
1655
1851
|
if (!keyStr.match(/-----BEGIN [\w\s]*PRIVATE KEY-----/)) {
|
|
1656
|
-
console.error(
|
|
1657
|
-
console.error(
|
|
1852
|
+
console.error(colors_default.red(`Error: ${customKeyPath} is not a valid PEM private key.`));
|
|
1853
|
+
console.error(
|
|
1854
|
+
colors_default.gray("Expected a file starting with -----BEGIN ...PRIVATE KEY-----")
|
|
1855
|
+
);
|
|
1658
1856
|
process.exit(1);
|
|
1659
1857
|
}
|
|
1660
1858
|
tlsOptions = { cert, key };
|
|
1661
1859
|
} catch (err) {
|
|
1662
1860
|
const message = err instanceof Error ? err.message : String(err);
|
|
1663
|
-
console.error(
|
|
1861
|
+
console.error(colors_default.red(`Error reading certificate files: ${message}`));
|
|
1664
1862
|
process.exit(1);
|
|
1665
1863
|
}
|
|
1666
1864
|
} else {
|
|
1667
|
-
console.log(
|
|
1865
|
+
console.log(colors_default.gray("Ensuring TLS certificates..."));
|
|
1668
1866
|
const certs = ensureCerts(stateDir);
|
|
1669
1867
|
if (certs.caGenerated) {
|
|
1670
|
-
console.log(
|
|
1868
|
+
console.log(colors_default.green("Generated local CA certificate."));
|
|
1671
1869
|
}
|
|
1672
1870
|
if (!isCATrusted(stateDir)) {
|
|
1673
|
-
console.log(
|
|
1871
|
+
console.log(colors_default.yellow("Adding CA to system trust store..."));
|
|
1674
1872
|
const trustResult = trustCA(stateDir);
|
|
1675
1873
|
if (trustResult.trusted) {
|
|
1676
1874
|
console.log(
|
|
1677
|
-
|
|
1875
|
+
colors_default.green("CA added to system trust store. Browsers will trust portless certs.")
|
|
1678
1876
|
);
|
|
1679
1877
|
} else {
|
|
1680
|
-
console.warn(
|
|
1878
|
+
console.warn(colors_default.yellow("Could not add CA to system trust store."));
|
|
1681
1879
|
if (trustResult.error) {
|
|
1682
|
-
console.warn(
|
|
1880
|
+
console.warn(colors_default.gray(trustResult.error));
|
|
1683
1881
|
}
|
|
1684
1882
|
console.warn(
|
|
1685
|
-
|
|
1883
|
+
colors_default.yellow("Browsers will show certificate warnings. To fix this later, run:")
|
|
1686
1884
|
);
|
|
1687
|
-
console.warn(
|
|
1885
|
+
console.warn(colors_default.cyan(" portless trust"));
|
|
1688
1886
|
}
|
|
1689
1887
|
}
|
|
1690
1888
|
const cert = fs3.readFileSync(certs.certPath);
|
|
@@ -1697,8 +1895,8 @@ ${chalk.bold("Usage:")}
|
|
|
1697
1895
|
}
|
|
1698
1896
|
}
|
|
1699
1897
|
if (isForeground) {
|
|
1700
|
-
console.log(
|
|
1701
|
-
startProxyServer(store, proxyPort, tld, tlsOptions);
|
|
1898
|
+
console.log(colors_default.blue.bold("\nportless proxy\n"));
|
|
1899
|
+
startProxyServer(store, proxyPort, tld, tlsOptions, useWildcard ? false : void 0);
|
|
1702
1900
|
return;
|
|
1703
1901
|
}
|
|
1704
1902
|
store.ensureDir();
|
|
@@ -1710,20 +1908,29 @@ ${chalk.bold("Usage:")}
|
|
|
1710
1908
|
} catch {
|
|
1711
1909
|
}
|
|
1712
1910
|
fixOwnership(logPath);
|
|
1713
|
-
const daemonArgs = [
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1911
|
+
const daemonArgs = [
|
|
1912
|
+
getEntryScript(),
|
|
1913
|
+
"proxy",
|
|
1914
|
+
"start",
|
|
1915
|
+
"--foreground",
|
|
1916
|
+
"--port",
|
|
1917
|
+
proxyPort.toString()
|
|
1918
|
+
];
|
|
1717
1919
|
if (useHttps) {
|
|
1718
1920
|
if (customCertPath && customKeyPath) {
|
|
1719
1921
|
daemonArgs.push("--cert", customCertPath, "--key", customKeyPath);
|
|
1720
1922
|
} else {
|
|
1721
1923
|
daemonArgs.push("--https");
|
|
1722
1924
|
}
|
|
1925
|
+
} else {
|
|
1926
|
+
daemonArgs.push("--no-tls");
|
|
1723
1927
|
}
|
|
1724
1928
|
if (tld !== DEFAULT_TLD) {
|
|
1725
1929
|
daemonArgs.push("--tld", tld);
|
|
1726
1930
|
}
|
|
1931
|
+
if (useWildcard) {
|
|
1932
|
+
daemonArgs.push("--wildcard");
|
|
1933
|
+
}
|
|
1727
1934
|
const child = spawn(process.execPath, daemonArgs, {
|
|
1728
1935
|
detached: true,
|
|
1729
1936
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -1735,32 +1942,31 @@ ${chalk.bold("Usage:")}
|
|
|
1735
1942
|
fs3.closeSync(logFd);
|
|
1736
1943
|
}
|
|
1737
1944
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
1738
|
-
console.error(
|
|
1739
|
-
console.error(
|
|
1740
|
-
|
|
1741
|
-
console.error(chalk.cyan(` ${needsSudo ? "sudo " : ""}portless proxy start --foreground`));
|
|
1945
|
+
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
1946
|
+
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
1947
|
+
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
1742
1948
|
if (fs3.existsSync(logPath)) {
|
|
1743
|
-
console.error(
|
|
1949
|
+
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1744
1950
|
}
|
|
1745
1951
|
process.exit(1);
|
|
1746
1952
|
}
|
|
1747
1953
|
const proto = useHttps ? "HTTPS/2" : "HTTP";
|
|
1748
|
-
console.log(
|
|
1954
|
+
console.log(colors_default.green(`${proto} proxy started on port ${proxyPort}`));
|
|
1749
1955
|
}
|
|
1750
1956
|
async function handleRunMode(args) {
|
|
1751
1957
|
const parsed = parseRunArgs(args);
|
|
1752
1958
|
if (parsed.commandArgs.length === 0) {
|
|
1753
|
-
console.error(
|
|
1754
|
-
console.error(
|
|
1755
|
-
console.error(
|
|
1756
|
-
console.error(
|
|
1757
|
-
console.error(
|
|
1959
|
+
console.error(colors_default.red("Error: No command provided."));
|
|
1960
|
+
console.error(colors_default.blue("Usage:"));
|
|
1961
|
+
console.error(colors_default.cyan(" portless run <command...>"));
|
|
1962
|
+
console.error(colors_default.blue("Example:"));
|
|
1963
|
+
console.error(colors_default.cyan(" portless run next dev"));
|
|
1758
1964
|
process.exit(1);
|
|
1759
1965
|
}
|
|
1760
1966
|
let baseName;
|
|
1761
1967
|
let nameSource;
|
|
1762
1968
|
if (parsed.name) {
|
|
1763
|
-
baseName = parsed.name;
|
|
1969
|
+
baseName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
1764
1970
|
nameSource = "--name flag";
|
|
1765
1971
|
} else {
|
|
1766
1972
|
const inferred = inferProjectName();
|
|
@@ -1771,7 +1977,7 @@ async function handleRunMode(args) {
|
|
|
1771
1977
|
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
1772
1978
|
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
1773
1979
|
const store = new RouteStore(dir, {
|
|
1774
|
-
onWarning: (msg) => console.warn(
|
|
1980
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1775
1981
|
});
|
|
1776
1982
|
await runApp(
|
|
1777
1983
|
store,
|
|
@@ -1789,22 +1995,23 @@ async function handleRunMode(args) {
|
|
|
1789
1995
|
async function handleNamedMode(args) {
|
|
1790
1996
|
const parsed = parseAppArgs(args);
|
|
1791
1997
|
if (parsed.commandArgs.length === 0) {
|
|
1792
|
-
console.error(
|
|
1793
|
-
console.error(
|
|
1794
|
-
console.error(
|
|
1795
|
-
console.error(
|
|
1796
|
-
console.error(
|
|
1998
|
+
console.error(colors_default.red("Error: No command provided."));
|
|
1999
|
+
console.error(colors_default.blue("Usage:"));
|
|
2000
|
+
console.error(colors_default.cyan(" portless <name> <command...>"));
|
|
2001
|
+
console.error(colors_default.blue("Example:"));
|
|
2002
|
+
console.error(colors_default.cyan(" portless myapp next dev"));
|
|
1797
2003
|
process.exit(1);
|
|
1798
2004
|
}
|
|
2005
|
+
const safeName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
1799
2006
|
const { dir, port, tls: tls2, tld } = await discoverState();
|
|
1800
2007
|
const store = new RouteStore(dir, {
|
|
1801
|
-
onWarning: (msg) => console.warn(
|
|
2008
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
1802
2009
|
});
|
|
1803
2010
|
await runApp(
|
|
1804
2011
|
store,
|
|
1805
2012
|
port,
|
|
1806
2013
|
dir,
|
|
1807
|
-
|
|
2014
|
+
safeName,
|
|
1808
2015
|
parsed.commandArgs,
|
|
1809
2016
|
tls2,
|
|
1810
2017
|
tld,
|
|
@@ -1826,23 +2033,23 @@ async function main() {
|
|
|
1826
2033
|
const isNpx = process.env.npm_command === "exec" && !process.env.npm_lifecycle_event;
|
|
1827
2034
|
const isPnpmDlx = !!process.env.PNPM_SCRIPT_SRC_DIR && !process.env.npm_lifecycle_event;
|
|
1828
2035
|
if (isNpx || isPnpmDlx) {
|
|
1829
|
-
console.error(
|
|
1830
|
-
console.error(
|
|
1831
|
-
console.error(
|
|
2036
|
+
console.error(colors_default.red("Error: portless should not be run via npx or pnpm dlx."));
|
|
2037
|
+
console.error(colors_default.blue("Install globally instead:"));
|
|
2038
|
+
console.error(colors_default.cyan(" npm install -g portless"));
|
|
1832
2039
|
process.exit(1);
|
|
1833
2040
|
}
|
|
1834
2041
|
if (args[0] === "--name") {
|
|
1835
2042
|
args.shift();
|
|
1836
2043
|
if (!args[0]) {
|
|
1837
|
-
console.error(
|
|
1838
|
-
console.error(
|
|
2044
|
+
console.error(colors_default.red("Error: --name requires an app name."));
|
|
2045
|
+
console.error(colors_default.cyan(" portless --name <name> <command...>"));
|
|
1839
2046
|
process.exit(1);
|
|
1840
2047
|
}
|
|
1841
2048
|
const skipPortless2 = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
1842
2049
|
if (skipPortless2) {
|
|
1843
2050
|
const { commandArgs } = parseAppArgs(args);
|
|
1844
2051
|
if (commandArgs.length === 0) {
|
|
1845
|
-
console.error(
|
|
2052
|
+
console.error(colors_default.red("Error: No command provided."));
|
|
1846
2053
|
process.exit(1);
|
|
1847
2054
|
}
|
|
1848
2055
|
spawnCommand(commandArgs);
|
|
@@ -1859,7 +2066,7 @@ async function main() {
|
|
|
1859
2066
|
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
|
|
1860
2067
|
const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
1861
2068
|
if (commandArgs.length === 0) {
|
|
1862
|
-
console.error(
|
|
2069
|
+
console.error(colors_default.red("Error: No command provided."));
|
|
1863
2070
|
process.exit(1);
|
|
1864
2071
|
}
|
|
1865
2072
|
spawnCommand(commandArgs);
|
|
@@ -1907,6 +2114,6 @@ async function main() {
|
|
|
1907
2114
|
}
|
|
1908
2115
|
main().catch((err) => {
|
|
1909
2116
|
const message = err instanceof Error ? err.message : String(err);
|
|
1910
|
-
console.error(
|
|
2117
|
+
console.error(colors_default.red("Error:"), message);
|
|
1911
2118
|
process.exit(1);
|
|
1912
2119
|
});
|