portless 0.14.0 → 0.15.1
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 +23 -7
- package/dist/{chunk-OZM4AEYL.js → chunk-SD2PIWJU.js} +52 -8
- package/dist/cli.js +729 -169
- package/dist/index.d.ts +17 -2
- package/dist/index.js +5 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -4,16 +4,20 @@ import {
|
|
|
4
4
|
PORTLESS_HEADER,
|
|
5
5
|
RouteConflictError,
|
|
6
6
|
RouteStore,
|
|
7
|
+
checkHostResolution,
|
|
7
8
|
cleanHostsFile,
|
|
8
9
|
createHttpRedirectServer,
|
|
9
10
|
createProxyServer,
|
|
10
11
|
fixOwnership,
|
|
11
12
|
formatUrl,
|
|
13
|
+
getManagedHostnames,
|
|
12
14
|
isErrnoException,
|
|
15
|
+
isProcessAlive,
|
|
13
16
|
parseHostname,
|
|
17
|
+
parseHostnames,
|
|
14
18
|
shouldAutoSyncHosts,
|
|
15
19
|
syncHostsFile
|
|
16
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-SD2PIWJU.js";
|
|
17
21
|
|
|
18
22
|
// src/colors.ts
|
|
19
23
|
function supportsColor() {
|
|
@@ -498,15 +502,16 @@ async function generateHostCertAsync(stateDir, hostname) {
|
|
|
498
502
|
fixOwnership(keyPath, certPath);
|
|
499
503
|
return { certPath, keyPath };
|
|
500
504
|
}
|
|
501
|
-
function createSNICallback(stateDir, defaultCert, defaultKey,
|
|
505
|
+
function createSNICallback(stateDir, defaultCert, defaultKey, tlds = "localhost", caCert) {
|
|
502
506
|
const cache = /* @__PURE__ */ new Map();
|
|
503
507
|
const pending = /* @__PURE__ */ new Map();
|
|
508
|
+
const configuredTlds = Array.isArray(tlds) ? tlds : [tlds];
|
|
504
509
|
const defaultCtx = tls.createSecureContext({
|
|
505
510
|
cert: caCert ? Buffer.concat([defaultCert, caCert]) : defaultCert,
|
|
506
511
|
key: defaultKey
|
|
507
512
|
});
|
|
508
513
|
return (servername, cb) => {
|
|
509
|
-
if (servername ===
|
|
514
|
+
if (servername === "localhost" && configuredTlds.includes("localhost")) {
|
|
510
515
|
cb(null, defaultCtx);
|
|
511
516
|
return;
|
|
512
517
|
}
|
|
@@ -1503,6 +1508,7 @@ function readPortFromDir(dir) {
|
|
|
1503
1508
|
}
|
|
1504
1509
|
}
|
|
1505
1510
|
var TLS_MARKER_FILE = "proxy.tls";
|
|
1511
|
+
var CUSTOM_CERT_MARKER_FILE = "proxy.custom-cert";
|
|
1506
1512
|
function readTlsMarker(dir) {
|
|
1507
1513
|
try {
|
|
1508
1514
|
return fs3.existsSync(path3.join(dir, TLS_MARKER_FILE));
|
|
@@ -1521,6 +1527,24 @@ function writeTlsMarker(dir, enabled2) {
|
|
|
1521
1527
|
}
|
|
1522
1528
|
}
|
|
1523
1529
|
}
|
|
1530
|
+
function readCustomCertMarker(dir) {
|
|
1531
|
+
try {
|
|
1532
|
+
return fs3.existsSync(path3.join(dir, CUSTOM_CERT_MARKER_FILE));
|
|
1533
|
+
} catch {
|
|
1534
|
+
return false;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function writeCustomCertMarker(dir, enabled2) {
|
|
1538
|
+
const markerPath = path3.join(dir, CUSTOM_CERT_MARKER_FILE);
|
|
1539
|
+
if (enabled2) {
|
|
1540
|
+
fs3.writeFileSync(markerPath, "1", { mode: 420 });
|
|
1541
|
+
} else {
|
|
1542
|
+
try {
|
|
1543
|
+
fs3.unlinkSync(markerPath);
|
|
1544
|
+
} catch {
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1524
1548
|
var LAN_MARKER_FILE = "proxy.lan";
|
|
1525
1549
|
function readLanMarker(dir) {
|
|
1526
1550
|
try {
|
|
@@ -1562,8 +1586,25 @@ function validateTld(tld) {
|
|
|
1562
1586
|
}
|
|
1563
1587
|
return null;
|
|
1564
1588
|
}
|
|
1589
|
+
function parseTldList(value, source = "TLD") {
|
|
1590
|
+
const trimmed = value.trim();
|
|
1591
|
+
if (!trimmed) return [];
|
|
1592
|
+
const tlds = [];
|
|
1593
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1594
|
+
for (const rawPart of trimmed.split(",")) {
|
|
1595
|
+
const tld = rawPart.trim().toLowerCase();
|
|
1596
|
+
const err = validateTld(tld);
|
|
1597
|
+
if (err) throw new Error(source === "TLD" ? err : `${source}: ${err}`);
|
|
1598
|
+
if (!seen.has(tld)) {
|
|
1599
|
+
seen.add(tld);
|
|
1600
|
+
tlds.push(tld);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return tlds;
|
|
1604
|
+
}
|
|
1565
1605
|
var TLD_FILE = "proxy.tld";
|
|
1566
|
-
|
|
1606
|
+
var TLDS_FILE = "proxy.tlds";
|
|
1607
|
+
function readLegacyTldFromDir(dir) {
|
|
1567
1608
|
try {
|
|
1568
1609
|
const raw = fs3.readFileSync(path3.join(dir, TLD_FILE), "utf-8").trim();
|
|
1569
1610
|
return raw || DEFAULT_TLD;
|
|
@@ -1571,23 +1612,43 @@ function readTldFromDir(dir) {
|
|
|
1571
1612
|
return DEFAULT_TLD;
|
|
1572
1613
|
}
|
|
1573
1614
|
}
|
|
1574
|
-
function
|
|
1575
|
-
|
|
1576
|
-
|
|
1615
|
+
function readTldsFromDir(dir) {
|
|
1616
|
+
try {
|
|
1617
|
+
const raw = fs3.readFileSync(path3.join(dir, TLDS_FILE), "utf-8").trim();
|
|
1618
|
+
const parsed = raw.startsWith("[") ? JSON.parse(raw) : raw.split(/\r?\n/).flatMap((line) => line.split(",")).map((line) => line.trim()).filter(Boolean);
|
|
1619
|
+
if (!Array.isArray(parsed)) return [readLegacyTldFromDir(dir)];
|
|
1620
|
+
const tlds = parsed.flatMap((value) => typeof value === "string" ? parseTldList(value) : []);
|
|
1621
|
+
return tlds.length > 0 ? [...new Set(tlds)] : [DEFAULT_TLD];
|
|
1622
|
+
} catch {
|
|
1623
|
+
return [readLegacyTldFromDir(dir)];
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
function writeTldsFile(dir, tlds) {
|
|
1627
|
+
const uniqueTlds = [...new Set(tlds)];
|
|
1628
|
+
const tldsPath = path3.join(dir, TLDS_FILE);
|
|
1629
|
+
const tldPath = path3.join(dir, TLD_FILE);
|
|
1630
|
+
if (uniqueTlds.length === 1 && uniqueTlds[0] === DEFAULT_TLD) {
|
|
1631
|
+
try {
|
|
1632
|
+
fs3.unlinkSync(tldsPath);
|
|
1633
|
+
} catch {
|
|
1634
|
+
}
|
|
1577
1635
|
try {
|
|
1578
|
-
fs3.unlinkSync(
|
|
1636
|
+
fs3.unlinkSync(tldPath);
|
|
1579
1637
|
} catch {
|
|
1580
1638
|
}
|
|
1581
1639
|
} else {
|
|
1582
|
-
fs3.writeFileSync(
|
|
1640
|
+
fs3.writeFileSync(tldsPath, uniqueTlds.join("\n") + "\n", { mode: 420 });
|
|
1641
|
+
fs3.writeFileSync(tldPath, uniqueTlds[0] ?? DEFAULT_TLD, { mode: 420 });
|
|
1583
1642
|
}
|
|
1584
1643
|
}
|
|
1585
|
-
function
|
|
1644
|
+
function writeTldFile(dir, tld) {
|
|
1645
|
+
writeTldsFile(dir, [tld]);
|
|
1646
|
+
}
|
|
1647
|
+
function getDefaultTlds() {
|
|
1586
1648
|
const val = process.env.PORTLESS_TLD?.trim().toLowerCase();
|
|
1587
|
-
if (!val) return DEFAULT_TLD;
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
return val;
|
|
1649
|
+
if (!val) return [DEFAULT_TLD];
|
|
1650
|
+
const tlds = parseTldList(val, "PORTLESS_TLD");
|
|
1651
|
+
return tlds.length > 0 ? tlds : [DEFAULT_TLD];
|
|
1591
1652
|
}
|
|
1592
1653
|
function isHttpsEnvDisabled() {
|
|
1593
1654
|
const val = process.env.PORTLESS_HTTPS;
|
|
@@ -1606,14 +1667,17 @@ function readPersistedProxyState() {
|
|
|
1606
1667
|
const port = readPortFromDir(dir);
|
|
1607
1668
|
if (port !== null) {
|
|
1608
1669
|
const tls2 = readTlsMarker(dir);
|
|
1609
|
-
const
|
|
1670
|
+
const tlds = readTldsFromDir(dir);
|
|
1671
|
+
const tld = tlds[0] ?? DEFAULT_TLD;
|
|
1610
1672
|
const lanIp = readLanMarker(dir);
|
|
1611
|
-
return { port, tls: tls2, tld, lanMode: lanIp !== null ||
|
|
1673
|
+
return { port, tls: tls2, tld, tlds, lanMode: lanIp !== null || tlds.includes("local") };
|
|
1612
1674
|
}
|
|
1613
1675
|
return null;
|
|
1614
1676
|
}
|
|
1615
1677
|
function buildProxyStartConfig(options) {
|
|
1616
|
-
const
|
|
1678
|
+
const requestedTlds = options.tlds && options.tlds.length > 0 ? [...options.tlds] : [options.tld];
|
|
1679
|
+
const effectiveTlds = options.lanMode ? ["local"] : [...new Set(requestedTlds)];
|
|
1680
|
+
const effectiveTld = effectiveTlds[0] ?? DEFAULT_TLD;
|
|
1617
1681
|
const args = [];
|
|
1618
1682
|
if (options.foreground) {
|
|
1619
1683
|
args.push("--foreground");
|
|
@@ -1639,8 +1703,10 @@ function buildProxyStartConfig(options) {
|
|
|
1639
1703
|
args.push(INTERNAL_LAN_IP_FLAG, options.lanIp);
|
|
1640
1704
|
}
|
|
1641
1705
|
}
|
|
1642
|
-
} else if (effectiveTld !== DEFAULT_TLD) {
|
|
1643
|
-
|
|
1706
|
+
} else if (effectiveTlds.length > 1 || effectiveTld !== DEFAULT_TLD) {
|
|
1707
|
+
for (const tld of effectiveTlds) {
|
|
1708
|
+
args.push("--tld", tld);
|
|
1709
|
+
}
|
|
1644
1710
|
}
|
|
1645
1711
|
if (options.useWildcard) {
|
|
1646
1712
|
args.push("--wildcard");
|
|
@@ -1648,7 +1714,7 @@ function buildProxyStartConfig(options) {
|
|
|
1648
1714
|
if (options.skipTrust) {
|
|
1649
1715
|
args.push("--skip-trust");
|
|
1650
1716
|
}
|
|
1651
|
-
return { effectiveTld, args };
|
|
1717
|
+
return { effectiveTld, effectiveTlds, args };
|
|
1652
1718
|
}
|
|
1653
1719
|
async function discoverState() {
|
|
1654
1720
|
if (process.env.PORTLESS_STATE_DIR) {
|
|
@@ -1657,14 +1723,25 @@ async function discoverState() {
|
|
|
1657
1723
|
const lanIp = readLanMarker(dir2);
|
|
1658
1724
|
if (await isProxyRunning(port) || await isPortListening(port)) {
|
|
1659
1725
|
const tls2 = readTlsMarker(dir2);
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1726
|
+
const tlds3 = readTldsFromDir(dir2);
|
|
1727
|
+
const tld = tlds3[0] ?? DEFAULT_TLD;
|
|
1728
|
+
return {
|
|
1729
|
+
dir: dir2,
|
|
1730
|
+
port,
|
|
1731
|
+
tls: tls2,
|
|
1732
|
+
tld,
|
|
1733
|
+
tlds: tlds3,
|
|
1734
|
+
lanMode: lanIp !== null || tlds3.includes("local"),
|
|
1735
|
+
lanIp
|
|
1736
|
+
};
|
|
1662
1737
|
}
|
|
1738
|
+
const tlds2 = readTldsFromDir(dir2);
|
|
1663
1739
|
return {
|
|
1664
1740
|
dir: dir2,
|
|
1665
1741
|
port,
|
|
1666
1742
|
tls: readTlsMarker(dir2),
|
|
1667
|
-
tld:
|
|
1743
|
+
tld: tlds2[0] ?? DEFAULT_TLD,
|
|
1744
|
+
tlds: tlds2,
|
|
1668
1745
|
lanMode: lanIp !== null,
|
|
1669
1746
|
lanIp: null
|
|
1670
1747
|
};
|
|
@@ -1673,14 +1750,16 @@ async function discoverState() {
|
|
|
1673
1750
|
if (userPort !== null) {
|
|
1674
1751
|
if (await isProxyRunning(userPort)) {
|
|
1675
1752
|
const tls2 = readTlsMarker(USER_STATE_DIR);
|
|
1676
|
-
const
|
|
1753
|
+
const tlds2 = readTldsFromDir(USER_STATE_DIR);
|
|
1754
|
+
const tld = tlds2[0] ?? DEFAULT_TLD;
|
|
1677
1755
|
const lanIp = readLanMarker(USER_STATE_DIR);
|
|
1678
1756
|
return {
|
|
1679
1757
|
dir: USER_STATE_DIR,
|
|
1680
1758
|
port: userPort,
|
|
1681
1759
|
tls: tls2,
|
|
1682
1760
|
tld,
|
|
1683
|
-
|
|
1761
|
+
tlds: tlds2,
|
|
1762
|
+
lanMode: lanIp !== null || tlds2.includes("local"),
|
|
1684
1763
|
lanIp
|
|
1685
1764
|
};
|
|
1686
1765
|
}
|
|
@@ -1689,14 +1768,16 @@ async function discoverState() {
|
|
|
1689
1768
|
if (legacyPort !== null) {
|
|
1690
1769
|
if (await isProxyRunning(legacyPort)) {
|
|
1691
1770
|
const tls2 = readTlsMarker(LEGACY_SYSTEM_STATE_DIR);
|
|
1692
|
-
const
|
|
1771
|
+
const tlds2 = readTldsFromDir(LEGACY_SYSTEM_STATE_DIR);
|
|
1772
|
+
const tld = tlds2[0] ?? DEFAULT_TLD;
|
|
1693
1773
|
const lanIp = readLanMarker(LEGACY_SYSTEM_STATE_DIR);
|
|
1694
1774
|
return {
|
|
1695
1775
|
dir: LEGACY_SYSTEM_STATE_DIR,
|
|
1696
1776
|
port: legacyPort,
|
|
1697
1777
|
tls: tls2,
|
|
1698
1778
|
tld,
|
|
1699
|
-
|
|
1779
|
+
tlds: tlds2,
|
|
1780
|
+
lanMode: lanIp !== null || tlds2.includes("local"),
|
|
1700
1781
|
lanIp
|
|
1701
1782
|
};
|
|
1702
1783
|
}
|
|
@@ -1708,17 +1789,28 @@ async function discoverState() {
|
|
|
1708
1789
|
const dir2 = resolveStateDir(port);
|
|
1709
1790
|
const markerTls = readTlsMarker(dir2);
|
|
1710
1791
|
const tls2 = markerTls || port === getProtocolPort(true);
|
|
1711
|
-
const
|
|
1792
|
+
const tlds2 = readTldsFromDir(dir2);
|
|
1793
|
+
const tld = tlds2[0] ?? DEFAULT_TLD;
|
|
1712
1794
|
const lanIp = readLanMarker(dir2);
|
|
1713
|
-
return {
|
|
1795
|
+
return {
|
|
1796
|
+
dir: dir2,
|
|
1797
|
+
port,
|
|
1798
|
+
tls: tls2,
|
|
1799
|
+
tld,
|
|
1800
|
+
tlds: tlds2,
|
|
1801
|
+
lanMode: lanIp !== null || tlds2.includes("local"),
|
|
1802
|
+
lanIp
|
|
1803
|
+
};
|
|
1714
1804
|
}
|
|
1715
1805
|
}
|
|
1716
1806
|
const dir = resolveStateDir(configuredPort);
|
|
1807
|
+
const tlds = readTldsFromDir(dir);
|
|
1717
1808
|
return {
|
|
1718
1809
|
dir,
|
|
1719
1810
|
port: configuredPort,
|
|
1720
1811
|
tls: readTlsMarker(dir),
|
|
1721
|
-
tld:
|
|
1812
|
+
tld: tlds[0] ?? DEFAULT_TLD,
|
|
1813
|
+
tlds,
|
|
1722
1814
|
lanMode: readLanMarker(dir) !== null,
|
|
1723
1815
|
lanIp: null
|
|
1724
1816
|
};
|
|
@@ -2001,7 +2093,9 @@ var PORTLESS_STATE_FILES = [
|
|
|
2001
2093
|
"proxy.port",
|
|
2002
2094
|
"proxy.log",
|
|
2003
2095
|
"proxy.tls",
|
|
2096
|
+
"proxy.custom-cert",
|
|
2004
2097
|
"proxy.tld",
|
|
2098
|
+
"proxy.tlds",
|
|
2005
2099
|
"proxy.lan",
|
|
2006
2100
|
"ca-key.pem",
|
|
2007
2101
|
"ca.pem",
|
|
@@ -2774,6 +2868,15 @@ var SYSTEMD_SERVICE = "portless.service";
|
|
|
2774
2868
|
var WINDOWS_TASK_NAME = "Portless Proxy";
|
|
2775
2869
|
var INTERNAL_ELEVATED_ENV = "PORTLESS_INTERNAL_SERVICE_ELEVATED";
|
|
2776
2870
|
var SERVICE_ENV_KEYS = /* @__PURE__ */ new Set(["PORTLESS_SYNC_HOSTS"]);
|
|
2871
|
+
function normalizeTlds(tlds) {
|
|
2872
|
+
return [...new Set(tlds.length > 0 ? tlds : [DEFAULT_TLD])];
|
|
2873
|
+
}
|
|
2874
|
+
function primaryTld(tlds) {
|
|
2875
|
+
return tlds[0] ?? DEFAULT_TLD;
|
|
2876
|
+
}
|
|
2877
|
+
function formatTldList(tlds) {
|
|
2878
|
+
return tlds.map((tld) => `.${tld}`).join(", ");
|
|
2879
|
+
}
|
|
2777
2880
|
var DEFAULT_SERVICE_CONFIG = {
|
|
2778
2881
|
proxyPort: DEFAULT_SERVICE_PORT,
|
|
2779
2882
|
useHttps: true,
|
|
@@ -2783,6 +2886,7 @@ var DEFAULT_SERVICE_CONFIG = {
|
|
|
2783
2886
|
lanIp: null,
|
|
2784
2887
|
lanIpExplicit: false,
|
|
2785
2888
|
tld: DEFAULT_TLD,
|
|
2889
|
+
tlds: [DEFAULT_TLD],
|
|
2786
2890
|
useWildcard: false,
|
|
2787
2891
|
extraEnv: {}
|
|
2788
2892
|
};
|
|
@@ -2869,10 +2973,8 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
|
|
|
2869
2973
|
config.lanIpExplicit = true;
|
|
2870
2974
|
}
|
|
2871
2975
|
if (env.PORTLESS_TLD) {
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
if (err) throw new Error(`PORTLESS_TLD: ${err}`);
|
|
2875
|
-
config.tld = tld;
|
|
2976
|
+
config.tlds = normalizeTlds(parseTldList(env.PORTLESS_TLD, "PORTLESS_TLD"));
|
|
2977
|
+
config.tld = primaryTld(config.tlds);
|
|
2876
2978
|
}
|
|
2877
2979
|
const envWildcard = parseBooleanEnv(env.PORTLESS_WILDCARD);
|
|
2878
2980
|
if (envWildcard !== null) {
|
|
@@ -2884,6 +2986,7 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
|
|
|
2884
2986
|
config.proxyPort = getProtocolPort(config.useHttps);
|
|
2885
2987
|
}
|
|
2886
2988
|
const tokens = args[0] === "service" ? args.slice(2) : args;
|
|
2989
|
+
let tldFlagSeen = false;
|
|
2887
2990
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
2888
2991
|
const token = tokens[i];
|
|
2889
2992
|
switch (token) {
|
|
@@ -2908,10 +3011,10 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
|
|
|
2908
3011
|
i += 1;
|
|
2909
3012
|
break;
|
|
2910
3013
|
case "--tld": {
|
|
2911
|
-
const
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
3014
|
+
const tlds = parseTldList(getFlagValue(tokens, i, token));
|
|
3015
|
+
config.tlds = normalizeTlds([...tldFlagSeen ? config.tlds : [], ...tlds]);
|
|
3016
|
+
config.tld = primaryTld(config.tlds);
|
|
3017
|
+
tldFlagSeen = true;
|
|
2915
3018
|
i += 1;
|
|
2916
3019
|
break;
|
|
2917
3020
|
}
|
|
@@ -2951,6 +3054,9 @@ function parseServiceInstallConfig(args, env = process.env, options = {}) {
|
|
|
2951
3054
|
if (!config.lanMode) {
|
|
2952
3055
|
config.lanIp = null;
|
|
2953
3056
|
config.lanIpExplicit = false;
|
|
3057
|
+
} else {
|
|
3058
|
+
config.tlds = ["local"];
|
|
3059
|
+
config.tld = "local";
|
|
2954
3060
|
}
|
|
2955
3061
|
return config;
|
|
2956
3062
|
}
|
|
@@ -2996,6 +3102,7 @@ function buildProxyCommand(entryScript, serviceConfig) {
|
|
|
2996
3102
|
lanIp: serviceConfig.lanIp,
|
|
2997
3103
|
lanIpExplicit: serviceConfig.lanIpExplicit,
|
|
2998
3104
|
tld: serviceConfig.tld,
|
|
3105
|
+
tlds: serviceConfig.tlds,
|
|
2999
3106
|
useWildcard: serviceConfig.useWildcard,
|
|
3000
3107
|
foreground: true,
|
|
3001
3108
|
includePort: true,
|
|
@@ -3018,8 +3125,8 @@ function buildServiceEnv(ctx) {
|
|
|
3018
3125
|
}
|
|
3019
3126
|
if (ctx.config.lanMode) {
|
|
3020
3127
|
env.PORTLESS_TLD = "local";
|
|
3021
|
-
} else if (ctx.config.tld !== DEFAULT_TLD) {
|
|
3022
|
-
env.PORTLESS_TLD = ctx.config.
|
|
3128
|
+
} else if (ctx.config.tlds.length > 1 || ctx.config.tld !== DEFAULT_TLD) {
|
|
3129
|
+
env.PORTLESS_TLD = ctx.config.tlds.join(",");
|
|
3023
3130
|
}
|
|
3024
3131
|
if (ctx.platform === "win32") {
|
|
3025
3132
|
env.USERPROFILE = ctx.user.home;
|
|
@@ -3104,6 +3211,10 @@ function buildServiceSpec(options) {
|
|
|
3104
3211
|
...options.installConfig,
|
|
3105
3212
|
extraEnv: options.installConfig?.extraEnv ?? {}
|
|
3106
3213
|
};
|
|
3214
|
+
installConfig.tlds = installConfig.lanMode ? ["local"] : normalizeTlds(
|
|
3215
|
+
options.installConfig?.tlds ?? (options.installConfig?.tld ? [options.installConfig.tld] : installConfig.tlds)
|
|
3216
|
+
);
|
|
3217
|
+
installConfig.tld = primaryTld(installConfig.tlds);
|
|
3107
3218
|
const stateDir = options.stateDir || installConfig.stateDir || defaultStateDir(options.platform, options.userHome);
|
|
3108
3219
|
const normalizedConfig = {
|
|
3109
3220
|
...installConfig,
|
|
@@ -3594,7 +3705,7 @@ async function printServiceStatus(entryScript, runner) {
|
|
|
3594
3705
|
` Proxy on ${config.proxyPort}: ${status.proxyRunning ? "responding" : "not responding"}`
|
|
3595
3706
|
);
|
|
3596
3707
|
console.log(` HTTPS: ${config.useHttps ? "yes" : "no"}`);
|
|
3597
|
-
console.log(`
|
|
3708
|
+
console.log(` TLDs: ${config.lanMode ? ".local" : formatTldList(config.tlds)}`);
|
|
3598
3709
|
console.log(` LAN mode: ${config.lanMode ? "yes" : "no"}`);
|
|
3599
3710
|
if (config.lanIpExplicit && config.lanIp) {
|
|
3600
3711
|
console.log(` LAN IP: ${config.lanIp}`);
|
|
@@ -3622,7 +3733,7 @@ ${colors_default.bold("Install options:")}
|
|
|
3622
3733
|
--https Enable HTTPS
|
|
3623
3734
|
--lan Enable LAN mode
|
|
3624
3735
|
--ip <address> Pin a specific LAN IP
|
|
3625
|
-
--tld <tld> Use a custom TLD outside LAN mode
|
|
3736
|
+
--tld <tld> Use a custom TLD outside LAN mode, repeatable
|
|
3626
3737
|
--wildcard Allow subdomain fallback
|
|
3627
3738
|
--cert <path> Use a custom TLS certificate
|
|
3628
3739
|
--key <path> Use a custom TLS private key
|
|
@@ -3672,7 +3783,17 @@ var DEBOUNCE_MS = 100;
|
|
|
3672
3783
|
var POLL_INTERVAL_MS = 3e3;
|
|
3673
3784
|
var EXIT_TIMEOUT_MS = 2e3;
|
|
3674
3785
|
var SUDO_SPAWN_TIMEOUT_MS = 3e4;
|
|
3675
|
-
function
|
|
3786
|
+
function normalizeTlds2(tlds) {
|
|
3787
|
+
return [...new Set(tlds.length > 0 ? tlds : [DEFAULT_TLD])];
|
|
3788
|
+
}
|
|
3789
|
+
function primaryTld2(tlds) {
|
|
3790
|
+
return tlds[0] ?? DEFAULT_TLD;
|
|
3791
|
+
}
|
|
3792
|
+
function formatTldList2(tlds) {
|
|
3793
|
+
return tlds.map((tld) => `.${tld}`).join(", ");
|
|
3794
|
+
}
|
|
3795
|
+
function defaultProxyConfig(tlds, useHttps, lanMode) {
|
|
3796
|
+
const effectiveTlds = lanMode ? ["local"] : normalizeTlds2(tlds);
|
|
3676
3797
|
return {
|
|
3677
3798
|
useHttps,
|
|
3678
3799
|
customCertPath: null,
|
|
@@ -3680,13 +3801,14 @@ function defaultProxyConfig(tld, useHttps, lanMode) {
|
|
|
3680
3801
|
lanMode,
|
|
3681
3802
|
lanIp: null,
|
|
3682
3803
|
lanIpExplicit: false,
|
|
3683
|
-
tld:
|
|
3804
|
+
tld: primaryTld2(effectiveTlds),
|
|
3805
|
+
tlds: effectiveTlds,
|
|
3684
3806
|
useWildcard: false
|
|
3685
3807
|
};
|
|
3686
3808
|
}
|
|
3687
3809
|
function resolveProxyConfig(options) {
|
|
3688
3810
|
const config = defaultProxyConfig(
|
|
3689
|
-
options.
|
|
3811
|
+
options.defaultTlds,
|
|
3690
3812
|
options.useHttps,
|
|
3691
3813
|
options.explicit.lanMode ? options.lanMode : options.persistedLanMode
|
|
3692
3814
|
);
|
|
@@ -3707,8 +3829,9 @@ function resolveProxyConfig(options) {
|
|
|
3707
3829
|
if (!options.lanMode) {
|
|
3708
3830
|
config.lanIp = null;
|
|
3709
3831
|
config.lanIpExplicit = false;
|
|
3710
|
-
if (!options.explicit.
|
|
3711
|
-
config.
|
|
3832
|
+
if (!options.explicit.tlds) {
|
|
3833
|
+
config.tlds = normalizeTlds2(options.defaultTlds);
|
|
3834
|
+
config.tld = primaryTld2(config.tlds);
|
|
3712
3835
|
}
|
|
3713
3836
|
}
|
|
3714
3837
|
}
|
|
@@ -3717,8 +3840,9 @@ function resolveProxyConfig(options) {
|
|
|
3717
3840
|
config.lanIp = options.lanIp;
|
|
3718
3841
|
config.lanIpExplicit = true;
|
|
3719
3842
|
}
|
|
3720
|
-
if (options.explicit.
|
|
3721
|
-
config.
|
|
3843
|
+
if (options.explicit.tlds) {
|
|
3844
|
+
config.tlds = normalizeTlds2(options.tlds);
|
|
3845
|
+
config.tld = primaryTld2(config.tlds);
|
|
3722
3846
|
}
|
|
3723
3847
|
if (options.explicit.useWildcard) {
|
|
3724
3848
|
config.useWildcard = options.useWildcard;
|
|
@@ -3728,6 +3852,7 @@ function resolveProxyConfig(options) {
|
|
|
3728
3852
|
config.lanIpExplicit = false;
|
|
3729
3853
|
}
|
|
3730
3854
|
if (config.lanMode) {
|
|
3855
|
+
config.tlds = ["local"];
|
|
3731
3856
|
config.tld = "local";
|
|
3732
3857
|
if (!config.lanIpExplicit) {
|
|
3733
3858
|
config.lanIp = null;
|
|
@@ -3741,15 +3866,17 @@ function resolveProxyConfig(options) {
|
|
|
3741
3866
|
}
|
|
3742
3867
|
function readCurrentProxyConfig(dir) {
|
|
3743
3868
|
const lanIp = readLanMarker(dir);
|
|
3744
|
-
const
|
|
3869
|
+
const tlds = readTldsFromDir(dir);
|
|
3870
|
+
const tld = primaryTld2(tlds);
|
|
3745
3871
|
return {
|
|
3746
3872
|
useHttps: readTlsMarker(dir),
|
|
3747
3873
|
customCertPath: null,
|
|
3748
3874
|
customKeyPath: null,
|
|
3749
|
-
lanMode: lanIp !== null ||
|
|
3875
|
+
lanMode: lanIp !== null || tlds.includes("local"),
|
|
3750
3876
|
lanIp,
|
|
3751
3877
|
lanIpExplicit: false,
|
|
3752
3878
|
tld,
|
|
3879
|
+
tlds,
|
|
3753
3880
|
useWildcard: false
|
|
3754
3881
|
};
|
|
3755
3882
|
}
|
|
@@ -3770,9 +3897,9 @@ function getProxyConfigMismatchMessages(desiredConfig, actualConfig, explicit) {
|
|
|
3770
3897
|
desiredConfig.useHttps ? "requested HTTPS, but the running proxy is using HTTP" : "requested HTTP, but the running proxy is using HTTPS"
|
|
3771
3898
|
);
|
|
3772
3899
|
}
|
|
3773
|
-
if (explicit.
|
|
3900
|
+
if (explicit.tlds && (desiredConfig.tlds.length !== actualConfig.tlds.length || desiredConfig.tlds.some((tld, index) => tld !== actualConfig.tlds[index]))) {
|
|
3774
3901
|
messages.push(
|
|
3775
|
-
`requested
|
|
3902
|
+
`requested ${formatTldList2(desiredConfig.tlds)}, but the running proxy is using ${formatTldList2(actualConfig.tlds)}`
|
|
3776
3903
|
);
|
|
3777
3904
|
}
|
|
3778
3905
|
return messages;
|
|
@@ -3787,6 +3914,7 @@ function formatProxyStartCommand(proxyPort, config) {
|
|
|
3787
3914
|
lanIp: config.lanIpExplicit ? config.lanIp : null,
|
|
3788
3915
|
lanIpExplicit: config.lanIpExplicit,
|
|
3789
3916
|
tld: config.tld,
|
|
3917
|
+
tlds: config.tlds,
|
|
3790
3918
|
useWildcard: config.useWildcard,
|
|
3791
3919
|
includePort: proxyPort !== getDefaultPort(config.useHttps),
|
|
3792
3920
|
proxyPort
|
|
@@ -3880,7 +4008,46 @@ function formatProcessExitSuffix(code, signal) {
|
|
|
3880
4008
|
if (code !== null) return ` (exit ${code})`;
|
|
3881
4009
|
return "";
|
|
3882
4010
|
}
|
|
3883
|
-
function
|
|
4011
|
+
function buildHostnames(name, tlds) {
|
|
4012
|
+
return parseHostnames(name, normalizeTlds2(tlds));
|
|
4013
|
+
}
|
|
4014
|
+
function formatUrls(hostnames, proxyPort, tls2) {
|
|
4015
|
+
return hostnames.map((hostname) => formatUrl(hostname, proxyPort, tls2));
|
|
4016
|
+
}
|
|
4017
|
+
function formatViteAllowedHosts(tlds) {
|
|
4018
|
+
return tlds.map((configuredTld) => `.${configuredTld}`).join(",");
|
|
4019
|
+
}
|
|
4020
|
+
function addRoutes(store, hostnames, port, pid, force = false) {
|
|
4021
|
+
const registered = [];
|
|
4022
|
+
const killedPids = [];
|
|
4023
|
+
try {
|
|
4024
|
+
for (const hostname of hostnames) {
|
|
4025
|
+
const killedPid = store.addRoute(hostname, port, pid, force);
|
|
4026
|
+
registered.push(hostname);
|
|
4027
|
+
if (killedPid !== void 0) {
|
|
4028
|
+
killedPids.push(killedPid);
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
} catch (err) {
|
|
4032
|
+
for (const hostname of registered) {
|
|
4033
|
+
try {
|
|
4034
|
+
store.removeRoute(hostname, pid);
|
|
4035
|
+
} catch {
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
throw err;
|
|
4039
|
+
}
|
|
4040
|
+
return [...new Set(killedPids)];
|
|
4041
|
+
}
|
|
4042
|
+
function removeRoutes(store, hostnames, ownerPid) {
|
|
4043
|
+
for (const hostname of hostnames) {
|
|
4044
|
+
try {
|
|
4045
|
+
store.removeRoute(hostname, ownerPid);
|
|
4046
|
+
} catch {
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
function startProxyServer(store, proxyPort, tld, tlds, tlsOptions, lanIp, strict, customCert = false) {
|
|
3884
4051
|
store.ensureDir();
|
|
3885
4052
|
const isTls = !!tlsOptions;
|
|
3886
4053
|
const mdnsSupport = isMdnsSupported();
|
|
@@ -3972,6 +4139,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
3972
4139
|
getRoutes: () => cachedRoutes,
|
|
3973
4140
|
proxyPort,
|
|
3974
4141
|
tld,
|
|
4142
|
+
tlds,
|
|
3975
4143
|
strict,
|
|
3976
4144
|
onError: (msg) => console.error(colors_default.red(msg)),
|
|
3977
4145
|
tls: tlsOptions
|
|
@@ -4009,11 +4177,12 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
4009
4177
|
fs9.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
4010
4178
|
fs9.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
4011
4179
|
writeTlsMarker(store.dir, isTls);
|
|
4012
|
-
|
|
4180
|
+
writeCustomCertMarker(store.dir, isTls && customCert);
|
|
4181
|
+
writeTldsFile(store.dir, tlds);
|
|
4013
4182
|
writeLanMarker(store.dir, activeLanIp);
|
|
4014
4183
|
fixOwnership(store.dir, store.pidPath, store.portFilePath);
|
|
4015
4184
|
const proto = isTls ? "HTTPS/2" : "HTTP";
|
|
4016
|
-
const tldLabel = tld !== DEFAULT_TLD ? ` (
|
|
4185
|
+
const tldLabel = tlds.length > 1 || tld !== DEFAULT_TLD ? ` (TLDs: ${formatTldList2(tlds)})` : "";
|
|
4017
4186
|
const modeLabel = strict === false ? " (wildcard)" : "";
|
|
4018
4187
|
console.log(
|
|
4019
4188
|
colors_default.green(`${proto} proxy listening on port ${proxyPort}${tldLabel}${modeLabel}`)
|
|
@@ -4021,7 +4190,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
4021
4190
|
if (activeLanIp) {
|
|
4022
4191
|
console.log(chalk.green(`LAN mode: ${activeLanIp}`));
|
|
4023
4192
|
console.log(chalk.gray("Services are discoverable as <name>.local on your network"));
|
|
4024
|
-
if (isTls) {
|
|
4193
|
+
if (isTls && !customCert) {
|
|
4025
4194
|
console.log(chalk.yellow("For HTTPS on devices, install the CA certificate:"));
|
|
4026
4195
|
console.log(chalk.gray(` ${path9.join(store.dir, "ca.pem")}`));
|
|
4027
4196
|
}
|
|
@@ -4063,6 +4232,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
4063
4232
|
} catch {
|
|
4064
4233
|
}
|
|
4065
4234
|
writeTlsMarker(store.dir, false);
|
|
4235
|
+
writeCustomCertMarker(store.dir, false);
|
|
4066
4236
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
4067
4237
|
writeLanMarker(store.dir, null);
|
|
4068
4238
|
if (autoSyncHosts) cleanHostsFile();
|
|
@@ -4101,6 +4271,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4101
4271
|
} catch {
|
|
4102
4272
|
}
|
|
4103
4273
|
writeTlsMarker(store.dir, false);
|
|
4274
|
+
writeCustomCertMarker(store.dir, false);
|
|
4104
4275
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
4105
4276
|
writeLanMarker(store.dir, null);
|
|
4106
4277
|
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
@@ -4140,6 +4311,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4140
4311
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
4141
4312
|
fs9.unlinkSync(pidPath);
|
|
4142
4313
|
writeTlsMarker(store.dir, false);
|
|
4314
|
+
writeCustomCertMarker(store.dir, false);
|
|
4143
4315
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
4144
4316
|
writeLanMarker(store.dir, null);
|
|
4145
4317
|
return;
|
|
@@ -4158,6 +4330,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4158
4330
|
} catch {
|
|
4159
4331
|
}
|
|
4160
4332
|
writeTlsMarker(store.dir, false);
|
|
4333
|
+
writeCustomCertMarker(store.dir, false);
|
|
4161
4334
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
4162
4335
|
writeLanMarker(store.dir, null);
|
|
4163
4336
|
return;
|
|
@@ -4171,6 +4344,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4171
4344
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
4172
4345
|
fs9.unlinkSync(pidPath);
|
|
4173
4346
|
writeTlsMarker(store.dir, false);
|
|
4347
|
+
writeCustomCertMarker(store.dir, false);
|
|
4174
4348
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
4175
4349
|
writeLanMarker(store.dir, null);
|
|
4176
4350
|
return;
|
|
@@ -4182,6 +4356,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
4182
4356
|
} catch {
|
|
4183
4357
|
}
|
|
4184
4358
|
writeTlsMarker(store.dir, false);
|
|
4359
|
+
writeCustomCertMarker(store.dir, false);
|
|
4185
4360
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
4186
4361
|
writeLanMarker(store.dir, null);
|
|
4187
4362
|
console.log(colors_default.green("Proxy stopped."));
|
|
@@ -4225,28 +4400,29 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
4225
4400
|
console.log();
|
|
4226
4401
|
}
|
|
4227
4402
|
function resolveProxyDesiredState(lanMode) {
|
|
4228
|
-
const
|
|
4403
|
+
const envTlds = getDefaultTlds();
|
|
4404
|
+
const envTld = primaryTld2(envTlds);
|
|
4229
4405
|
const explicit = {
|
|
4230
4406
|
useHttps: process.env.PORTLESS_HTTPS !== void 0,
|
|
4231
4407
|
customCert: false,
|
|
4232
4408
|
lanMode: process.env.PORTLESS_LAN !== void 0,
|
|
4233
4409
|
lanIp: process.env.PORTLESS_LAN_IP !== void 0,
|
|
4234
|
-
|
|
4410
|
+
tlds: process.env.PORTLESS_TLD !== void 0,
|
|
4235
4411
|
useWildcard: process.env.PORTLESS_WILDCARD !== void 0
|
|
4236
4412
|
};
|
|
4237
4413
|
const desiredConfig = resolveProxyConfig({
|
|
4238
4414
|
persistedLanMode: lanMode,
|
|
4239
4415
|
explicit,
|
|
4240
|
-
|
|
4416
|
+
defaultTlds: envTlds,
|
|
4241
4417
|
useHttps: !isHttpsEnvDisabled(),
|
|
4242
4418
|
customCertPath: null,
|
|
4243
4419
|
customKeyPath: null,
|
|
4244
4420
|
lanMode: isLanEnvEnabled(),
|
|
4245
4421
|
lanIp: process.env.PORTLESS_LAN_IP || null,
|
|
4246
|
-
|
|
4422
|
+
tlds: envTlds,
|
|
4247
4423
|
useWildcard: isWildcardEnvEnabled()
|
|
4248
4424
|
});
|
|
4249
|
-
return { explicit, desiredConfig, envTld };
|
|
4425
|
+
return { explicit, desiredConfig, envTld, envTlds };
|
|
4250
4426
|
}
|
|
4251
4427
|
async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
4252
4428
|
const { explicit, desiredConfig } = desired;
|
|
@@ -4262,8 +4438,9 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
4262
4438
|
if (!explicit.useHttps && persisted.tls !== desiredConfig.useHttps) {
|
|
4263
4439
|
startConfig.useHttps = persisted.tls;
|
|
4264
4440
|
}
|
|
4265
|
-
if (!explicit.
|
|
4266
|
-
startConfig.
|
|
4441
|
+
if (!explicit.tlds && (persisted.tlds.length !== desiredConfig.tlds.length || persisted.tlds.some((tld, index) => tld !== desiredConfig.tlds[index]))) {
|
|
4442
|
+
startConfig.tlds = persisted.tlds;
|
|
4443
|
+
startConfig.tld = primaryTld2(persisted.tlds);
|
|
4267
4444
|
}
|
|
4268
4445
|
if (!explicit.lanMode && persisted.lanMode !== desiredConfig.lanMode) {
|
|
4269
4446
|
startConfig.lanMode = persisted.lanMode;
|
|
@@ -4299,6 +4476,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
4299
4476
|
lanIp: startConfig.lanIpExplicit ? startConfig.lanIp : null,
|
|
4300
4477
|
lanIpExplicit: startConfig.lanIpExplicit,
|
|
4301
4478
|
tld: startConfig.tld,
|
|
4479
|
+
tlds: startConfig.tlds,
|
|
4302
4480
|
useWildcard: startConfig.useWildcard,
|
|
4303
4481
|
includePort: startPort !== void 0,
|
|
4304
4482
|
proxyPort: startPort
|
|
@@ -4333,7 +4511,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
4333
4511
|
}
|
|
4334
4512
|
return { started: true, state: discovered };
|
|
4335
4513
|
}
|
|
4336
|
-
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2,
|
|
4514
|
+
async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2, tlds, force, autoInfo, desiredPort, lanMode = false, lanIp) {
|
|
4337
4515
|
let store = initialStore;
|
|
4338
4516
|
console.log(chalk.blue.bold(`
|
|
4339
4517
|
portless
|
|
@@ -4380,12 +4558,12 @@ portless
|
|
|
4380
4558
|
console.error(colors_default.red(`Error: ${err.message}`));
|
|
4381
4559
|
process.exit(1);
|
|
4382
4560
|
}
|
|
4383
|
-
|
|
4561
|
+
buildHostnames(name, tlds);
|
|
4384
4562
|
const ensureResult = await ensureProxyRunning(proxyPort, tls2, desired);
|
|
4385
4563
|
if (ensureResult.started) {
|
|
4386
4564
|
proxyPort = ensureResult.state.port;
|
|
4387
4565
|
stateDir = ensureResult.state.dir;
|
|
4388
|
-
|
|
4566
|
+
tlds = ensureResult.state.tlds;
|
|
4389
4567
|
tls2 = ensureResult.state.tls;
|
|
4390
4568
|
lanMode = ensureResult.state.lanMode;
|
|
4391
4569
|
lanIp = ensureResult.state.lanIp;
|
|
@@ -4407,20 +4585,22 @@ portless
|
|
|
4407
4585
|
}
|
|
4408
4586
|
lanMode = runningConfig.lanMode;
|
|
4409
4587
|
lanIp = runningConfig.lanIp;
|
|
4588
|
+
tlds = runningConfig.tlds;
|
|
4410
4589
|
console.log(chalk.gray("-- Proxy is running"));
|
|
4411
4590
|
}
|
|
4412
|
-
const
|
|
4413
|
-
|
|
4591
|
+
const hostnames = buildHostnames(name, tlds);
|
|
4592
|
+
const hostname = hostnames[0];
|
|
4593
|
+
if (desired.explicit.tlds && (desired.envTlds.some((envConfiguredTld, index) => envConfiguredTld !== tlds[index]) || desired.envTlds.length !== tlds.length)) {
|
|
4414
4594
|
console.warn(
|
|
4415
4595
|
chalk.yellow(
|
|
4416
|
-
`Warning: PORTLESS_TLD=${desired.
|
|
4596
|
+
`Warning: PORTLESS_TLD=${desired.envTlds.join(",")} but the running proxy uses ${formatTldList2(tlds)}.`
|
|
4417
4597
|
)
|
|
4418
4598
|
);
|
|
4419
4599
|
}
|
|
4420
4600
|
if (lanIp) {
|
|
4421
|
-
console.log(chalk.gray(`-- ${
|
|
4601
|
+
console.log(chalk.gray(`-- ${hostnames.join(", ")} (LAN: ${lanIp})`));
|
|
4422
4602
|
} else {
|
|
4423
|
-
console.log(chalk.gray(`-- ${
|
|
4603
|
+
console.log(chalk.gray(`-- ${hostnames.join(", ")} (auto-resolves to 127.0.0.1)`));
|
|
4424
4604
|
}
|
|
4425
4605
|
if (autoInfo) {
|
|
4426
4606
|
const baseName = autoInfo.prefix ? name.slice(autoInfo.prefix.length + 1) : name;
|
|
@@ -4435,9 +4615,9 @@ portless
|
|
|
4435
4615
|
} else {
|
|
4436
4616
|
console.log(colors_default.green(`-- Using port ${port}`));
|
|
4437
4617
|
}
|
|
4438
|
-
let
|
|
4618
|
+
let killedPids = [];
|
|
4439
4619
|
try {
|
|
4440
|
-
|
|
4620
|
+
killedPids = addRoutes(store, hostnames, port, process.pid, force);
|
|
4441
4621
|
} catch (err) {
|
|
4442
4622
|
if (err instanceof RouteConflictError) {
|
|
4443
4623
|
console.error(colors_default.red(`Error: ${err.message}`));
|
|
@@ -4445,13 +4625,20 @@ portless
|
|
|
4445
4625
|
}
|
|
4446
4626
|
throw err;
|
|
4447
4627
|
}
|
|
4448
|
-
if (
|
|
4449
|
-
console.log(colors_default.yellow(`Killed existing process
|
|
4628
|
+
if (killedPids.length > 0) {
|
|
4629
|
+
console.log(colors_default.yellow(`Killed existing process(es): ${killedPids.join(", ")}`));
|
|
4450
4630
|
}
|
|
4451
4631
|
const finalUrl = formatUrl(hostname, proxyPort, tls2);
|
|
4632
|
+
const allUrls = formatUrls(hostnames, proxyPort, tls2);
|
|
4452
4633
|
console.log(chalk.cyan.bold(`
|
|
4453
4634
|
-> ${finalUrl}
|
|
4454
4635
|
`));
|
|
4636
|
+
for (const extraUrl of allUrls.slice(1)) {
|
|
4637
|
+
console.log(chalk.cyan(` also -> ${extraUrl}`));
|
|
4638
|
+
}
|
|
4639
|
+
if (allUrls.length > 1) {
|
|
4640
|
+
console.log("");
|
|
4641
|
+
}
|
|
4455
4642
|
if (lanIp) {
|
|
4456
4643
|
console.log(chalk.green(` LAN -> ${finalUrl}`));
|
|
4457
4644
|
console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
|
|
@@ -4564,7 +4751,7 @@ portless
|
|
|
4564
4751
|
} catch {
|
|
4565
4752
|
}
|
|
4566
4753
|
try {
|
|
4567
|
-
store.
|
|
4754
|
+
removeRoutes(store, hostnames, process.pid);
|
|
4568
4755
|
} catch {
|
|
4569
4756
|
}
|
|
4570
4757
|
process.exit(1);
|
|
@@ -4598,7 +4785,7 @@ portless
|
|
|
4598
4785
|
PORT: port.toString(),
|
|
4599
4786
|
...hostBind ? { HOST: hostBind } : {},
|
|
4600
4787
|
PORTLESS_URL: finalUrl,
|
|
4601
|
-
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS:
|
|
4788
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds),
|
|
4602
4789
|
// Note: EXPO_PACKAGER_PROXY_URL is not used — expo-dev-client removed
|
|
4603
4790
|
// baked-in pinging, making this env var ineffective. Expo handles its
|
|
4604
4791
|
// own LAN discovery natively.
|
|
@@ -4618,7 +4805,7 @@ portless
|
|
|
4618
4805
|
} catch {
|
|
4619
4806
|
}
|
|
4620
4807
|
try {
|
|
4621
|
-
store.
|
|
4808
|
+
removeRoutes(store, hostnames, process.pid);
|
|
4622
4809
|
} catch {
|
|
4623
4810
|
}
|
|
4624
4811
|
}
|
|
@@ -4803,13 +4990,14 @@ ${colors_default.bold("Usage:")}
|
|
|
4803
4990
|
${colors_default.cyan("portless run")} Same as above
|
|
4804
4991
|
${colors_default.cyan("portless run <cmd>")} Run a command through the proxy
|
|
4805
4992
|
${colors_default.cyan("portless <name> <cmd>")} Run with an explicit app name
|
|
4806
|
-
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
4993
|
+
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon); rarely needed since it auto-starts on first run
|
|
4807
4994
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
4808
4995
|
${colors_default.cyan("portless service install")} Start proxy automatically when the OS starts
|
|
4809
4996
|
${colors_default.cyan("portless get <name>")} Print URL for a service (for cross-service refs)
|
|
4810
4997
|
${colors_default.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
|
|
4811
4998
|
${colors_default.cyan("portless alias --remove <name>")} Remove a static route
|
|
4812
4999
|
${colors_default.cyan("portless list")} Show active routes
|
|
5000
|
+
${colors_default.cyan("portless doctor")} Check local portless health
|
|
4813
5001
|
${colors_default.cyan("portless trust")} Add local CA to system trust store
|
|
4814
5002
|
${colors_default.cyan("portless clean")} Remove portless state, trust entry, and hosts block
|
|
4815
5003
|
${colors_default.cyan("portless prune")} Kill orphaned dev servers from crashed sessions
|
|
@@ -4827,6 +5015,7 @@ ${colors_default.bold("Examples:")}
|
|
|
4827
5015
|
portless service install --lan # Persist LAN mode in the startup service
|
|
4828
5016
|
portless service install --wildcard # Persist wildcard routing in the startup service
|
|
4829
5017
|
portless get backend # -> https://backend.localhost
|
|
5018
|
+
portless doctor # Check proxy, routes, DNS, and CA trust
|
|
4830
5019
|
portless myapp --tailscale next dev # -> also https://<node>.ts.net (tailnet)
|
|
4831
5020
|
portless myapp --funnel next dev # -> also https://<node>.ts.net (public)
|
|
4832
5021
|
portless myapp --ngrok next dev # -> also https://<random>.ngrok.app (public)
|
|
@@ -4909,7 +5098,7 @@ ${colors_default.bold("Options:")}
|
|
|
4909
5098
|
--cert <path> Use a custom TLS certificate
|
|
4910
5099
|
--key <path> Use a custom TLS private key
|
|
4911
5100
|
--foreground Run proxy in foreground (for debugging)
|
|
4912
|
-
--tld <tld> Use a custom TLD instead of .localhost
|
|
5101
|
+
--tld <tld> Use a custom TLD instead of .localhost; repeat for more
|
|
4913
5102
|
--wildcard Allow unregistered subdomains to fall back to parent route
|
|
4914
5103
|
--state-dir <path> Use a custom state directory with service install
|
|
4915
5104
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
@@ -4926,7 +5115,7 @@ ${colors_default.bold("Environment variables:")}
|
|
|
4926
5115
|
PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
|
|
4927
5116
|
PORTLESS_LAN=1 Enable LAN mode when set to 1 (set in .bashrc / .zshrc)
|
|
4928
5117
|
PORTLESS_LAN_IP=<address> Pin a specific LAN IP for LAN mode
|
|
4929
|
-
PORTLESS_TLD=<tld>
|
|
5118
|
+
PORTLESS_TLD=<tld>[,<tld>] Use one or more TLDs (e.g. localhost,test)
|
|
4930
5119
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
4931
5120
|
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of ${HOSTS_DISPLAY} (on by default)
|
|
4932
5121
|
PORTLESS_TAILSCALE=1 Share apps on your Tailscale network (same as --tailscale)
|
|
@@ -4938,7 +5127,7 @@ ${colors_default.bold("Environment variables:")}
|
|
|
4938
5127
|
${colors_default.bold("Child process environment:")}
|
|
4939
5128
|
PORT Ephemeral port the child should listen on
|
|
4940
5129
|
HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
|
|
4941
|
-
PORTLESS_URL
|
|
5130
|
+
PORTLESS_URL Primary public URL of the app
|
|
4942
5131
|
PORTLESS_LAN Set to 1 when proxy is in LAN mode
|
|
4943
5132
|
PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is active)
|
|
4944
5133
|
PORTLESS_NGROK_URL ngrok URL of the app (when --ngrok is active)
|
|
@@ -4957,14 +5146,14 @@ ${colors_default.bold("Skip portless:")}
|
|
|
4957
5146
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
4958
5147
|
|
|
4959
5148
|
${colors_default.bold("Reserved names:")}
|
|
4960
|
-
run, get, alias, hosts, list, trust, clean, prune, proxy, service are subcommands and
|
|
5149
|
+
run, get, alias, hosts, list, doctor, trust, clean, prune, proxy, service are subcommands and
|
|
4961
5150
|
cannot be used as app names directly. Use "portless run" to infer the name,
|
|
4962
5151
|
or "portless --name <name>" to force any name including reserved ones.
|
|
4963
5152
|
`);
|
|
4964
5153
|
process.exit(0);
|
|
4965
5154
|
}
|
|
4966
5155
|
function printVersion() {
|
|
4967
|
-
console.log("0.
|
|
5156
|
+
console.log("0.15.1");
|
|
4968
5157
|
process.exit(0);
|
|
4969
5158
|
}
|
|
4970
5159
|
async function handleTrust() {
|
|
@@ -5239,8 +5428,8 @@ ${colors_default.bold("Examples:")}
|
|
|
5239
5428
|
const name = positional[0];
|
|
5240
5429
|
const worktree = skipWorktree ? null : detectWorktreePrefix();
|
|
5241
5430
|
const effectiveName = worktree ? `${worktree.prefix}.${name}` : name;
|
|
5242
|
-
const { port, tls: tls2,
|
|
5243
|
-
const hostname =
|
|
5431
|
+
const { port, tls: tls2, tlds } = await discoverState();
|
|
5432
|
+
const hostname = buildHostnames(effectiveName, tlds)[0];
|
|
5244
5433
|
const url = formatUrl(hostname, port, tls2);
|
|
5245
5434
|
process.stdout.write(url + "\n");
|
|
5246
5435
|
}
|
|
@@ -5261,7 +5450,7 @@ ${colors_default.bold("Examples:")}
|
|
|
5261
5450
|
`);
|
|
5262
5451
|
process.exit(0);
|
|
5263
5452
|
}
|
|
5264
|
-
const { dir,
|
|
5453
|
+
const { dir, tlds } = await discoverState();
|
|
5265
5454
|
const store = new RouteStore(dir, {
|
|
5266
5455
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
5267
5456
|
});
|
|
@@ -5272,15 +5461,15 @@ ${colors_default.bold("Examples:")}
|
|
|
5272
5461
|
console.error(colors_default.cyan(" portless alias --remove <name>"));
|
|
5273
5462
|
process.exit(1);
|
|
5274
5463
|
}
|
|
5275
|
-
const
|
|
5464
|
+
const hostnames2 = buildHostnames(aliasName2, tlds);
|
|
5276
5465
|
const routes = store.loadRoutes();
|
|
5277
|
-
const existing = routes.find((r) => r.hostname
|
|
5466
|
+
const existing = routes.find((r) => hostnames2.includes(r.hostname) && r.pid === 0);
|
|
5278
5467
|
if (!existing) {
|
|
5279
|
-
console.error(colors_default.red(`Error: No alias found for "${
|
|
5468
|
+
console.error(colors_default.red(`Error: No alias found for "${hostnames2.join(", ")}".`));
|
|
5280
5469
|
process.exit(1);
|
|
5281
5470
|
}
|
|
5282
|
-
store
|
|
5283
|
-
console.log(colors_default.green(`Removed alias: ${
|
|
5471
|
+
removeRoutes(store, hostnames2);
|
|
5472
|
+
console.log(colors_default.green(`Removed alias: ${hostnames2.join(", ")}`));
|
|
5284
5473
|
return;
|
|
5285
5474
|
}
|
|
5286
5475
|
const aliasName = args[1];
|
|
@@ -5294,15 +5483,15 @@ ${colors_default.bold("Examples:")}
|
|
|
5294
5483
|
console.error(colors_default.cyan(" portless alias my-postgres 5432"));
|
|
5295
5484
|
process.exit(1);
|
|
5296
5485
|
}
|
|
5297
|
-
const
|
|
5486
|
+
const hostnames = buildHostnames(aliasName, tlds);
|
|
5298
5487
|
const port = parseInt(aliasPort, 10);
|
|
5299
5488
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
5300
5489
|
console.error(colors_default.red(`Error: Invalid port "${aliasPort}". Must be 1-65535.`));
|
|
5301
5490
|
process.exit(1);
|
|
5302
5491
|
}
|
|
5303
5492
|
const force = args.includes("--force");
|
|
5304
|
-
store
|
|
5305
|
-
console.log(colors_default.green(`Alias registered: ${
|
|
5493
|
+
addRoutes(store, hostnames, port, 0, force);
|
|
5494
|
+
console.log(colors_default.green(`Alias registered: ${hostnames.join(", ")} -> 127.0.0.1:${port}`));
|
|
5306
5495
|
}
|
|
5307
5496
|
async function handleHosts(args) {
|
|
5308
5497
|
if (args[1] === "--help" || args[1] === "-h") {
|
|
@@ -5401,6 +5590,358 @@ ${colors_default.bold("Usage: portless hosts <command>")}
|
|
|
5401
5590
|
);
|
|
5402
5591
|
process.exit(1);
|
|
5403
5592
|
}
|
|
5593
|
+
function colorDoctorStatus(status) {
|
|
5594
|
+
if (status === "fail") return colors_default.red;
|
|
5595
|
+
if (status === "warn") return colors_default.yellow;
|
|
5596
|
+
if (status === "ok") return colors_default.green;
|
|
5597
|
+
return colors_default.gray;
|
|
5598
|
+
}
|
|
5599
|
+
function printDoctorFinding(finding) {
|
|
5600
|
+
const status = finding.status.padEnd(5);
|
|
5601
|
+
console.log(`${colorDoctorStatus(finding.status)(status)} ${finding.message}`);
|
|
5602
|
+
if (finding.hint) {
|
|
5603
|
+
console.log(colors_default.gray(` ${finding.hint}`));
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
function isProcessAliveForDoctor(pid) {
|
|
5607
|
+
if (pid <= 0) return true;
|
|
5608
|
+
return isProcessAlive(pid);
|
|
5609
|
+
}
|
|
5610
|
+
function checkPathWritable(targetPath) {
|
|
5611
|
+
try {
|
|
5612
|
+
fs9.accessSync(targetPath, fs9.constants.W_OK);
|
|
5613
|
+
return true;
|
|
5614
|
+
} catch {
|
|
5615
|
+
return false;
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
function findExistingAncestor(targetPath) {
|
|
5619
|
+
let current = targetPath;
|
|
5620
|
+
for (; ; ) {
|
|
5621
|
+
if (fs9.existsSync(current)) return current;
|
|
5622
|
+
const parent = path9.dirname(current);
|
|
5623
|
+
if (parent === current) return null;
|
|
5624
|
+
current = parent;
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
function checkCommandAvailable(command, args) {
|
|
5628
|
+
const result = spawnSync5(command, args, {
|
|
5629
|
+
stdio: "ignore",
|
|
5630
|
+
timeout: 3e3,
|
|
5631
|
+
windowsHide: true
|
|
5632
|
+
});
|
|
5633
|
+
return !result.error && (result.status === 0 || result.status === null);
|
|
5634
|
+
}
|
|
5635
|
+
function pluralize(count, singular, plural = `${singular}s`) {
|
|
5636
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
5637
|
+
}
|
|
5638
|
+
function isValidTcpPort(port) {
|
|
5639
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
5640
|
+
}
|
|
5641
|
+
function doctorProxyStartHint(proxyPort, tls2) {
|
|
5642
|
+
const defaultPort = getDefaultPort(tls2);
|
|
5643
|
+
const portArgs = proxyPort === defaultPort ? "" : ` -p ${proxyPort}`;
|
|
5644
|
+
const tlsArgs = tls2 ? "" : " --no-tls";
|
|
5645
|
+
return `Run: portless proxy start${portArgs}${tlsArgs}`;
|
|
5646
|
+
}
|
|
5647
|
+
async function handleDoctor(args) {
|
|
5648
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
5649
|
+
console.log(`
|
|
5650
|
+
${colors_default.bold("portless doctor")} - Check local portless health and print suggested fixes.
|
|
5651
|
+
|
|
5652
|
+
${colors_default.bold("Usage:")}
|
|
5653
|
+
${colors_default.cyan("portless doctor")}
|
|
5654
|
+
|
|
5655
|
+
Checks Node.js, the state directory, proxy liveness, route entries, HTTPS CA
|
|
5656
|
+
trust, hostname resolution, and LAN mode prerequisites. It does not start,
|
|
5657
|
+
stop, clean, prune, trust, or modify portless state.
|
|
5658
|
+
|
|
5659
|
+
${colors_default.bold("Options:")}
|
|
5660
|
+
--help, -h Show this help
|
|
5661
|
+
`);
|
|
5662
|
+
process.exit(0);
|
|
5663
|
+
}
|
|
5664
|
+
if (args.length > 1) {
|
|
5665
|
+
console.error(colors_default.red(`Error: Unknown argument "${args[1]}".`));
|
|
5666
|
+
console.error(colors_default.cyan(" portless doctor --help"));
|
|
5667
|
+
process.exit(1);
|
|
5668
|
+
}
|
|
5669
|
+
const findings = [];
|
|
5670
|
+
const add = (status, message, hint) => {
|
|
5671
|
+
findings.push({ status, message, hint });
|
|
5672
|
+
};
|
|
5673
|
+
let state;
|
|
5674
|
+
try {
|
|
5675
|
+
state = await discoverState();
|
|
5676
|
+
} catch (err) {
|
|
5677
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5678
|
+
add("fail", `Could not discover portless state: ${message}`);
|
|
5679
|
+
state = {
|
|
5680
|
+
dir: resolveStateDir(),
|
|
5681
|
+
port: getDefaultPort(!isHttpsEnvDisabled()),
|
|
5682
|
+
tls: !isHttpsEnvDisabled(),
|
|
5683
|
+
tld: DEFAULT_TLD,
|
|
5684
|
+
tlds: [DEFAULT_TLD],
|
|
5685
|
+
lanMode: isLanEnvEnabled(),
|
|
5686
|
+
lanIp: null
|
|
5687
|
+
};
|
|
5688
|
+
}
|
|
5689
|
+
const store = new RouteStore(state.dir, {
|
|
5690
|
+
onWarning: (msg) => add("warn", msg)
|
|
5691
|
+
});
|
|
5692
|
+
const hasPortFile = fs9.existsSync(store.portFilePath);
|
|
5693
|
+
const configuredTls = hasPortFile ? state.tls : !isHttpsEnvDisabled();
|
|
5694
|
+
const configuredPort = hasPortFile ? state.port : getDefaultPort(configuredTls);
|
|
5695
|
+
const initialProxyRunning = await isProxyRunning(state.port, state.tls);
|
|
5696
|
+
const probePort = initialProxyRunning || hasPortFile ? state.port : configuredPort;
|
|
5697
|
+
const probeTls = initialProxyRunning || hasPortFile ? state.tls : configuredTls;
|
|
5698
|
+
const proxyRunning = initialProxyRunning && probePort === state.port ? true : await isProxyRunning(probePort, probeTls);
|
|
5699
|
+
const portListening = proxyRunning ? true : await isPortListening(probePort);
|
|
5700
|
+
const proxyPort = proxyRunning || portListening || hasPortFile ? probePort : configuredPort;
|
|
5701
|
+
const proxyTls = proxyRunning || portListening || hasPortFile ? probeTls : configuredTls;
|
|
5702
|
+
const currentProxyStateIsHttp = (proxyRunning || portListening || hasPortFile) && !proxyTls;
|
|
5703
|
+
const proxyUsesCustomCert = proxyTls && readCustomCertMarker(state.dir);
|
|
5704
|
+
const stateExists = fs9.existsSync(state.dir);
|
|
5705
|
+
console.log(colors_default.blue.bold("\nportless doctor\n"));
|
|
5706
|
+
console.log(`Version: ${"0.15.1"}`);
|
|
5707
|
+
console.log(`Node.js: ${process.versions.node}`);
|
|
5708
|
+
console.log(`Platform: ${process.platform} ${process.arch}`);
|
|
5709
|
+
console.log(`State dir: ${state.dir}`);
|
|
5710
|
+
console.log(`Proxy target: ${formatUrl("127.0.0.1", proxyPort, proxyTls)}`);
|
|
5711
|
+
console.log(
|
|
5712
|
+
`Mode: ${proxyTls ? "HTTPS" : "HTTP"}, ${formatTldList2(state.tlds)}${state.lanMode ? ", LAN" : ""}`
|
|
5713
|
+
);
|
|
5714
|
+
console.log("");
|
|
5715
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
|
5716
|
+
if (nodeMajor >= 24) {
|
|
5717
|
+
add("ok", `Node.js ${process.versions.node} satisfies portless requirements.`);
|
|
5718
|
+
} else {
|
|
5719
|
+
add("fail", `Node.js ${process.versions.node} is unsupported.`, "Install Node.js 24 or newer.");
|
|
5720
|
+
}
|
|
5721
|
+
if (stateExists) {
|
|
5722
|
+
try {
|
|
5723
|
+
const stat = fs9.statSync(state.dir);
|
|
5724
|
+
if (!stat.isDirectory()) {
|
|
5725
|
+
add("fail", `State path exists but is not a directory: ${state.dir}`);
|
|
5726
|
+
} else if (checkPathWritable(state.dir)) {
|
|
5727
|
+
add("ok", `State directory is writable: ${state.dir}`);
|
|
5728
|
+
} else {
|
|
5729
|
+
add("fail", `State directory is not writable: ${state.dir}`);
|
|
5730
|
+
}
|
|
5731
|
+
} catch (err) {
|
|
5732
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5733
|
+
add("fail", `Could not inspect state directory: ${message}`);
|
|
5734
|
+
}
|
|
5735
|
+
} else {
|
|
5736
|
+
const ancestor = findExistingAncestor(path9.dirname(state.dir));
|
|
5737
|
+
if (!ancestor) {
|
|
5738
|
+
add(
|
|
5739
|
+
"fail",
|
|
5740
|
+
`State directory does not exist and no writable ancestor was found: ${state.dir}`
|
|
5741
|
+
);
|
|
5742
|
+
} else {
|
|
5743
|
+
const ancestorStat = fs9.statSync(ancestor);
|
|
5744
|
+
if (!ancestorStat.isDirectory()) {
|
|
5745
|
+
add("fail", `State directory does not exist and ancestor is not a directory: ${ancestor}`);
|
|
5746
|
+
} else if (checkPathWritable(ancestor)) {
|
|
5747
|
+
add("info", `State directory has not been created yet: ${state.dir}`);
|
|
5748
|
+
} else {
|
|
5749
|
+
add("fail", `State directory does not exist and ancestor is not writable: ${ancestor}`);
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
if (proxyRunning) {
|
|
5754
|
+
add("ok", `Proxy is responding on port ${proxyPort}.`);
|
|
5755
|
+
} else if (portListening) {
|
|
5756
|
+
const pid = findPidOnPort(proxyPort);
|
|
5757
|
+
add(
|
|
5758
|
+
"fail",
|
|
5759
|
+
`Port ${proxyPort} is in use, but it is not a portless proxy.`,
|
|
5760
|
+
pid ? `Process on port: PID ${pid}` : "Could not identify the process holding the port."
|
|
5761
|
+
);
|
|
5762
|
+
} else {
|
|
5763
|
+
add(
|
|
5764
|
+
"warn",
|
|
5765
|
+
`Proxy is not running on port ${proxyPort}.`,
|
|
5766
|
+
doctorProxyStartHint(proxyPort, proxyTls)
|
|
5767
|
+
);
|
|
5768
|
+
}
|
|
5769
|
+
if (fs9.existsSync(store.pidPath)) {
|
|
5770
|
+
try {
|
|
5771
|
+
const rawPid = fs9.readFileSync(store.pidPath, "utf-8").trim();
|
|
5772
|
+
const pid = parseInt(rawPid, 10);
|
|
5773
|
+
if (isNaN(pid) || pid <= 0) {
|
|
5774
|
+
add("fail", `Proxy PID file is invalid: ${store.pidPath}`);
|
|
5775
|
+
} else if (!isProcessAliveForDoctor(pid)) {
|
|
5776
|
+
add("warn", `Proxy PID file is stale: ${pid}`, "Run: portless proxy stop");
|
|
5777
|
+
} else if (!proxyRunning) {
|
|
5778
|
+
add(
|
|
5779
|
+
"warn",
|
|
5780
|
+
`Proxy PID file points to PID ${pid}, but no portless proxy is responding on port ${proxyPort}.`,
|
|
5781
|
+
"Run: portless proxy stop"
|
|
5782
|
+
);
|
|
5783
|
+
} else {
|
|
5784
|
+
const portPid = findPidOnPort(proxyPort);
|
|
5785
|
+
if (portPid !== null && portPid !== pid) {
|
|
5786
|
+
add(
|
|
5787
|
+
"warn",
|
|
5788
|
+
`Proxy PID file points to PID ${pid}, but port ${proxyPort} is owned by PID ${portPid}.`,
|
|
5789
|
+
"Run: portless proxy stop"
|
|
5790
|
+
);
|
|
5791
|
+
} else {
|
|
5792
|
+
add("ok", `Proxy PID file points to the responding proxy process: ${pid}`);
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
} catch (err) {
|
|
5796
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5797
|
+
add("fail", `Could not read proxy PID file: ${message}`);
|
|
5798
|
+
}
|
|
5799
|
+
} else if (proxyRunning) {
|
|
5800
|
+
add("warn", `Proxy is running but the PID file is missing: ${store.pidPath}`);
|
|
5801
|
+
}
|
|
5802
|
+
if (proxyUsesCustomCert) {
|
|
5803
|
+
add("ok", "Proxy is configured with a custom TLS certificate.");
|
|
5804
|
+
} else if (proxyTls || !currentProxyStateIsHttp && !isHttpsEnvDisabled()) {
|
|
5805
|
+
if (checkCommandAvailable("openssl", ["version"])) {
|
|
5806
|
+
add("ok", "OpenSSL is available for certificate generation.");
|
|
5807
|
+
} else {
|
|
5808
|
+
add(
|
|
5809
|
+
"fail",
|
|
5810
|
+
"OpenSSL is not available on PATH.",
|
|
5811
|
+
isWindows ? "Install OpenSSL or add Git for Windows OpenSSL to PATH." : "Install OpenSSL with your system package manager."
|
|
5812
|
+
);
|
|
5813
|
+
}
|
|
5814
|
+
} else {
|
|
5815
|
+
add("info", "HTTPS is disabled, so OpenSSL is not required for this run.");
|
|
5816
|
+
}
|
|
5817
|
+
if (proxyTls && proxyUsesCustomCert) {
|
|
5818
|
+
add("info", "Generated local CA is not required for custom TLS certificates.");
|
|
5819
|
+
} else if (proxyTls) {
|
|
5820
|
+
const caPath = path9.join(state.dir, "ca.pem");
|
|
5821
|
+
if (fs9.existsSync(caPath)) {
|
|
5822
|
+
if (isCATrusted(state.dir)) {
|
|
5823
|
+
add("ok", "Local CA is trusted by the OS trust store.");
|
|
5824
|
+
} else {
|
|
5825
|
+
add(
|
|
5826
|
+
"warn",
|
|
5827
|
+
"Local CA exists but is not trusted by the OS trust store.",
|
|
5828
|
+
"Run: portless trust"
|
|
5829
|
+
);
|
|
5830
|
+
}
|
|
5831
|
+
} else if (proxyRunning) {
|
|
5832
|
+
add("warn", `Generated CA file is missing: ${caPath}`);
|
|
5833
|
+
} else {
|
|
5834
|
+
add("info", "Local CA has not been generated yet.");
|
|
5835
|
+
}
|
|
5836
|
+
} else {
|
|
5837
|
+
add("info", "HTTPS is disabled for the current proxy state.");
|
|
5838
|
+
}
|
|
5839
|
+
let rawRoutes = [];
|
|
5840
|
+
try {
|
|
5841
|
+
rawRoutes = store.loadRoutesRaw();
|
|
5842
|
+
} catch (err) {
|
|
5843
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5844
|
+
add("fail", `Could not read routes: ${message}`);
|
|
5845
|
+
}
|
|
5846
|
+
const liveRoutes = rawRoutes.filter(
|
|
5847
|
+
(route) => route.pid === 0 || isProcessAliveForDoctor(route.pid)
|
|
5848
|
+
);
|
|
5849
|
+
const staleRoutes = rawRoutes.filter(
|
|
5850
|
+
(route) => route.pid !== 0 && !isProcessAliveForDoctor(route.pid)
|
|
5851
|
+
);
|
|
5852
|
+
if (rawRoutes.length === 0) {
|
|
5853
|
+
add("info", "No routes are registered.");
|
|
5854
|
+
} else if (staleRoutes.length === 0) {
|
|
5855
|
+
add("ok", `Routes: ${pluralize(liveRoutes.length, "active route")}.`);
|
|
5856
|
+
} else {
|
|
5857
|
+
add(
|
|
5858
|
+
"warn",
|
|
5859
|
+
`Routes: ${pluralize(liveRoutes.length, "active route")}, ${pluralize(staleRoutes.length, "stale route")}.`,
|
|
5860
|
+
"Run: portless prune"
|
|
5861
|
+
);
|
|
5862
|
+
}
|
|
5863
|
+
for (const route of staleRoutes.slice(0, 5)) {
|
|
5864
|
+
add("warn", `Stale route ${route.hostname} is owned by exited PID ${route.pid}.`);
|
|
5865
|
+
}
|
|
5866
|
+
if (staleRoutes.length > 5) {
|
|
5867
|
+
add("warn", `${staleRoutes.length - 5} additional stale routes hidden.`);
|
|
5868
|
+
}
|
|
5869
|
+
const routePortChecks = await Promise.all(
|
|
5870
|
+
liveRoutes.map(async (route) => {
|
|
5871
|
+
const validPort = isValidTcpPort(route.port);
|
|
5872
|
+
return {
|
|
5873
|
+
route,
|
|
5874
|
+
invalidPort: !validPort,
|
|
5875
|
+
listening: validPort ? await isPortListening(route.port) : false
|
|
5876
|
+
};
|
|
5877
|
+
})
|
|
5878
|
+
);
|
|
5879
|
+
for (const { route, invalidPort, listening } of routePortChecks) {
|
|
5880
|
+
if (invalidPort) {
|
|
5881
|
+
add(
|
|
5882
|
+
"warn",
|
|
5883
|
+
`Route ${route.hostname} has invalid port ${route.port}.`,
|
|
5884
|
+
route.pid === 0 ? "Remove or recreate the alias." : "Run: portless prune"
|
|
5885
|
+
);
|
|
5886
|
+
continue;
|
|
5887
|
+
}
|
|
5888
|
+
if (listening) continue;
|
|
5889
|
+
add(
|
|
5890
|
+
"warn",
|
|
5891
|
+
`Route ${route.hostname} points to port ${route.port}, but nothing is listening there.`,
|
|
5892
|
+
route.pid === 0 ? "Remove the alias or start that service." : "The app may still be starting."
|
|
5893
|
+
);
|
|
5894
|
+
}
|
|
5895
|
+
if (state.lanMode || isLanEnvEnabled()) {
|
|
5896
|
+
const mdns = isMdnsSupported();
|
|
5897
|
+
if (mdns.supported) {
|
|
5898
|
+
add("ok", "mDNS publishing support is available for LAN mode.");
|
|
5899
|
+
} else {
|
|
5900
|
+
add("fail", `LAN mode is enabled but mDNS publishing is unavailable: ${mdns.reason}`);
|
|
5901
|
+
}
|
|
5902
|
+
if (state.lanIp) {
|
|
5903
|
+
add("ok", `LAN IP is recorded: ${state.lanIp}`);
|
|
5904
|
+
} else {
|
|
5905
|
+
add("warn", "LAN mode is enabled but no LAN IP is recorded.");
|
|
5906
|
+
}
|
|
5907
|
+
} else if (liveRoutes.length > 0) {
|
|
5908
|
+
const managedHosts = new Set(getManagedHostnames());
|
|
5909
|
+
const resolutionChecks = await Promise.all(
|
|
5910
|
+
liveRoutes.map(async (route) => ({
|
|
5911
|
+
hostname: route.hostname,
|
|
5912
|
+
resolves: await checkHostResolution(route.hostname),
|
|
5913
|
+
managed: managedHosts.has(route.hostname)
|
|
5914
|
+
}))
|
|
5915
|
+
);
|
|
5916
|
+
const unresolved = resolutionChecks.filter((result) => !result.resolves);
|
|
5917
|
+
if (unresolved.length === 0) {
|
|
5918
|
+
add("ok", "Registered hostnames resolve through the system resolver.");
|
|
5919
|
+
} else {
|
|
5920
|
+
add(
|
|
5921
|
+
"warn",
|
|
5922
|
+
`${pluralize(unresolved.length, "hostname")} did not resolve through the system resolver.`,
|
|
5923
|
+
"Run: portless hosts sync"
|
|
5924
|
+
);
|
|
5925
|
+
for (const result of unresolved.slice(0, 5)) {
|
|
5926
|
+
const hostState = result.managed ? "present in hosts block" : "missing from hosts block";
|
|
5927
|
+
add("warn", `${result.hostname} is ${hostState}.`);
|
|
5928
|
+
}
|
|
5929
|
+
}
|
|
5930
|
+
}
|
|
5931
|
+
for (const finding of findings) {
|
|
5932
|
+
printDoctorFinding(finding);
|
|
5933
|
+
}
|
|
5934
|
+
const failures = findings.filter((finding) => finding.status === "fail").length;
|
|
5935
|
+
const warnings = findings.filter((finding) => finding.status === "warn").length;
|
|
5936
|
+
console.log("");
|
|
5937
|
+
if (failures > 0) {
|
|
5938
|
+
console.log(
|
|
5939
|
+
colors_default.red(`Summary: ${pluralize(failures, "failure")}, ${pluralize(warnings, "warning")}.`)
|
|
5940
|
+
);
|
|
5941
|
+
process.exit(1);
|
|
5942
|
+
}
|
|
5943
|
+
console.log(colors_default.green(`Summary: 0 failures, ${pluralize(warnings, "warning")}.`));
|
|
5944
|
+
}
|
|
5404
5945
|
async function handleProxy(args) {
|
|
5405
5946
|
if (args[1] === "stop") {
|
|
5406
5947
|
let explicitPort;
|
|
@@ -5441,6 +5982,7 @@ ${colors_default.bold("Usage:")}
|
|
|
5441
5982
|
${colors_default.cyan("portless proxy start --foreground")} Start in foreground (for debugging)
|
|
5442
5983
|
${colors_default.cyan("portless proxy start -p 1355")} Start on a custom port (no sudo)
|
|
5443
5984
|
${colors_default.cyan("portless proxy start --tld test")} Use .test instead of .localhost
|
|
5985
|
+
${colors_default.cyan("portless proxy start --tld localhost --tld test")} Serve both TLDs
|
|
5444
5986
|
${colors_default.cyan("portless proxy start --wildcard")} Allow unregistered subdomains to fall back to parent
|
|
5445
5987
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
5446
5988
|
|
|
@@ -5502,34 +6044,41 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5502
6044
|
}
|
|
5503
6045
|
hasExplicitPort = true;
|
|
5504
6046
|
}
|
|
5505
|
-
let
|
|
6047
|
+
let tlds;
|
|
5506
6048
|
try {
|
|
5507
|
-
|
|
6049
|
+
tlds = getDefaultTlds();
|
|
5508
6050
|
} catch (err) {
|
|
5509
6051
|
console.error(colors_default.red(`Error: ${err.message}`));
|
|
5510
6052
|
process.exit(1);
|
|
5511
6053
|
}
|
|
5512
|
-
const
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
6054
|
+
const tldFlagValues = [];
|
|
6055
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
6056
|
+
if (args[i] === "--tld") {
|
|
6057
|
+
const tldValue = args[i + 1];
|
|
6058
|
+
if (!tldValue || tldValue.startsWith("-")) {
|
|
6059
|
+
console.error(colors_default.red("Error: --tld requires a TLD value (e.g. test, localhost)."));
|
|
6060
|
+
process.exit(1);
|
|
6061
|
+
}
|
|
6062
|
+
tldFlagValues.push(tldValue);
|
|
6063
|
+
i += 1;
|
|
5518
6064
|
}
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
6065
|
+
}
|
|
6066
|
+
if (tldFlagValues.length > 0) {
|
|
6067
|
+
try {
|
|
6068
|
+
tlds = normalizeTlds2(tldFlagValues.flatMap((value) => parseTldList(value)));
|
|
6069
|
+
} catch (err) {
|
|
6070
|
+
console.error(colors_default.red(`Error: ${err.message}`));
|
|
5523
6071
|
process.exit(1);
|
|
5524
6072
|
}
|
|
5525
6073
|
}
|
|
6074
|
+
let tld = primaryTld2(tlds);
|
|
5526
6075
|
const useWildcard = args.includes("--wildcard") || isWildcardEnvEnabled();
|
|
5527
6076
|
const explicit = {
|
|
5528
6077
|
useHttps: hasHttpsFlag || hasNoTls || customCertPath !== null || customKeyPath !== null || process.env.PORTLESS_HTTPS !== void 0,
|
|
5529
6078
|
customCert: customCertPath !== null || customKeyPath !== null,
|
|
5530
6079
|
lanMode: process.env.PORTLESS_LAN !== void 0,
|
|
5531
6080
|
lanIp: process.env.PORTLESS_LAN_IP !== void 0,
|
|
5532
|
-
|
|
6081
|
+
tlds: tldFlagValues.length > 0 || process.env.PORTLESS_TLD !== void 0,
|
|
5533
6082
|
useWildcard: args.includes("--wildcard") || process.env.PORTLESS_WILDCARD !== void 0
|
|
5534
6083
|
};
|
|
5535
6084
|
let stateDir = resolveStateDir(proxyPort);
|
|
@@ -5547,13 +6096,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5547
6096
|
const desiredConfig = resolveProxyConfig({
|
|
5548
6097
|
persistedLanMode,
|
|
5549
6098
|
explicit,
|
|
5550
|
-
|
|
6099
|
+
defaultTlds: getDefaultTlds(),
|
|
5551
6100
|
useHttps: wantHttps || !!(customCertPath && customKeyPath),
|
|
5552
6101
|
customCertPath,
|
|
5553
6102
|
customKeyPath,
|
|
5554
6103
|
lanMode: isLanEnvEnabled(),
|
|
5555
6104
|
lanIp: process.env.PORTLESS_LAN_IP || null,
|
|
5556
|
-
|
|
6105
|
+
tlds,
|
|
5557
6106
|
useWildcard
|
|
5558
6107
|
});
|
|
5559
6108
|
const lanMode = desiredConfig.lanMode;
|
|
@@ -5561,31 +6110,35 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5561
6110
|
customCertPath = desiredConfig.customCertPath;
|
|
5562
6111
|
customKeyPath = desiredConfig.customKeyPath;
|
|
5563
6112
|
tld = desiredConfig.tld;
|
|
6113
|
+
tlds = desiredConfig.tlds;
|
|
5564
6114
|
const desiredWildcard = desiredConfig.useWildcard;
|
|
5565
6115
|
let lanIp = desiredConfig.lanIpExplicit ? desiredConfig.lanIp : null;
|
|
5566
6116
|
if (!hasExplicitPort && runningPort === null) {
|
|
5567
6117
|
proxyPort = getDefaultPort(useHttps);
|
|
5568
6118
|
stateDir = resolveStateDir(proxyPort);
|
|
5569
6119
|
}
|
|
5570
|
-
if (lanMode &&
|
|
5571
|
-
const
|
|
5572
|
-
if (
|
|
6120
|
+
if (lanMode && tldFlagValues.length > 0) {
|
|
6121
|
+
const userTlds = tldFlagValues.join(", ");
|
|
6122
|
+
if (userTlds !== "local") {
|
|
5573
6123
|
console.warn(
|
|
5574
6124
|
chalk.yellow(
|
|
5575
|
-
`Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${
|
|
6125
|
+
`Warning: --lan forces .local TLD (mDNS requirement). Ignoring --tld ${userTlds}.`
|
|
5576
6126
|
)
|
|
5577
6127
|
);
|
|
5578
6128
|
}
|
|
5579
6129
|
}
|
|
5580
|
-
const
|
|
5581
|
-
|
|
5582
|
-
|
|
6130
|
+
for (const configuredTld of tlds) {
|
|
6131
|
+
const riskyReason = RISKY_TLDS.get(configuredTld);
|
|
6132
|
+
if (riskyReason && !lanMode) {
|
|
6133
|
+
console.warn(colors_default.yellow(`Warning: .${configuredTld}: ${riskyReason}`));
|
|
6134
|
+
}
|
|
5583
6135
|
}
|
|
5584
6136
|
const syncDisabled = process.env.PORTLESS_SYNC_HOSTS === "0" || process.env.PORTLESS_SYNC_HOSTS === "false";
|
|
5585
|
-
|
|
6137
|
+
const nonDefaultTlds = tlds.filter((configuredTld) => configuredTld !== DEFAULT_TLD);
|
|
6138
|
+
if (nonDefaultTlds.length > 0 && !lanMode && syncDisabled) {
|
|
5586
6139
|
console.warn(
|
|
5587
6140
|
colors_default.yellow(
|
|
5588
|
-
`Warning:
|
|
6141
|
+
`Warning: ${formatTldList2(nonDefaultTlds)} domains require ${HOSTS_DISPLAY} entries to resolve to 127.0.0.1.`
|
|
5589
6142
|
)
|
|
5590
6143
|
);
|
|
5591
6144
|
console.warn(colors_default.yellow("Hosts sync is disabled. To add entries manually, run:"));
|
|
@@ -5647,6 +6200,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5647
6200
|
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
5648
6201
|
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
5649
6202
|
tld,
|
|
6203
|
+
tlds,
|
|
5650
6204
|
useWildcard: desiredWildcard
|
|
5651
6205
|
};
|
|
5652
6206
|
if (!isWindows && proxyPort < PRIVILEGED_PORT_THRESHOLD && (process.getuid?.() ?? -1) !== 0) {
|
|
@@ -5663,6 +6217,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5663
6217
|
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
5664
6218
|
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
5665
6219
|
tld,
|
|
6220
|
+
tlds,
|
|
5666
6221
|
useWildcard: desiredWildcard,
|
|
5667
6222
|
foreground: isForeground,
|
|
5668
6223
|
includePort: true,
|
|
@@ -5780,13 +6335,22 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5780
6335
|
cert,
|
|
5781
6336
|
key,
|
|
5782
6337
|
ca,
|
|
5783
|
-
SNICallback: createSNICallback(stateDir, cert, key,
|
|
6338
|
+
SNICallback: createSNICallback(stateDir, cert, key, tlds, ca)
|
|
5784
6339
|
};
|
|
5785
6340
|
}
|
|
5786
6341
|
}
|
|
5787
6342
|
if (isForeground) {
|
|
5788
6343
|
console.log(chalk.blue.bold("\nportless proxy\n"));
|
|
5789
|
-
startProxyServer(
|
|
6344
|
+
startProxyServer(
|
|
6345
|
+
store,
|
|
6346
|
+
proxyPort,
|
|
6347
|
+
tld,
|
|
6348
|
+
tlds,
|
|
6349
|
+
tlsOptions,
|
|
6350
|
+
lanIp,
|
|
6351
|
+
desiredWildcard ? false : void 0,
|
|
6352
|
+
!!(customCertPath && customKeyPath)
|
|
6353
|
+
);
|
|
5790
6354
|
return;
|
|
5791
6355
|
}
|
|
5792
6356
|
store.ensureDir();
|
|
@@ -5810,6 +6374,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5810
6374
|
lanIp: desiredConfig.lanIpExplicit ? lanIp : null,
|
|
5811
6375
|
lanIpExplicit: desiredConfig.lanIpExplicit,
|
|
5812
6376
|
tld,
|
|
6377
|
+
tlds,
|
|
5813
6378
|
useWildcard: desiredWildcard,
|
|
5814
6379
|
foreground: true,
|
|
5815
6380
|
includePort: true,
|
|
@@ -5903,7 +6468,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
|
|
|
5903
6468
|
}
|
|
5904
6469
|
const worktree = detectWorktreePrefix(cwd);
|
|
5905
6470
|
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
5906
|
-
const { dir, port, tls: tls2,
|
|
6471
|
+
const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
|
|
5907
6472
|
const store = new RouteStore(dir, {
|
|
5908
6473
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
5909
6474
|
});
|
|
@@ -5914,7 +6479,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
|
|
|
5914
6479
|
effectiveName,
|
|
5915
6480
|
resolved,
|
|
5916
6481
|
tls2,
|
|
5917
|
-
|
|
6482
|
+
tlds,
|
|
5918
6483
|
false,
|
|
5919
6484
|
{ nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
5920
6485
|
appConfig?.appPort,
|
|
@@ -5954,13 +6519,13 @@ function pipeOutput(child, prefix) {
|
|
|
5954
6519
|
prefixStream(child.stdout, process.stdout, prefix);
|
|
5955
6520
|
prefixStream(child.stderr, process.stderr, prefix);
|
|
5956
6521
|
}
|
|
5957
|
-
async function spawnProxiedApp(app, stateDir, proxyPort, tls2,
|
|
6522
|
+
async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tlds, exitCodes) {
|
|
5958
6523
|
const usesPortless = app.commandArgs[0] === "portless";
|
|
5959
6524
|
const pkgEnv = { ...process.env };
|
|
5960
6525
|
pkgEnv.PATH = augmentedPath(pkgEnv, app.pkg.dir);
|
|
5961
6526
|
let env;
|
|
5962
6527
|
let store = null;
|
|
5963
|
-
let
|
|
6528
|
+
let hostnames = [];
|
|
5964
6529
|
let displayUrl;
|
|
5965
6530
|
if (usesPortless) {
|
|
5966
6531
|
env = pkgEnv;
|
|
@@ -5970,17 +6535,17 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
|
|
|
5970
6535
|
onWarning: (msg) => console.warn(colors_default.yellow(`[${app.name}] ${msg}`))
|
|
5971
6536
|
});
|
|
5972
6537
|
const appPort = app.appPort ?? await findFreePort();
|
|
5973
|
-
|
|
5974
|
-
const
|
|
5975
|
-
const url =
|
|
6538
|
+
hostnames = buildHostnames(app.name, tlds);
|
|
6539
|
+
const urls = formatUrls(hostnames, proxyPort, tls2);
|
|
6540
|
+
const url = urls[0];
|
|
5976
6541
|
displayUrl = url;
|
|
5977
|
-
|
|
5978
|
-
store.addRoute(hostname, appPort, process.pid);
|
|
6542
|
+
addRoutes(store, hostnames, appPort, process.pid);
|
|
5979
6543
|
env = {
|
|
5980
6544
|
...pkgEnv,
|
|
5981
6545
|
PORT: String(appPort),
|
|
5982
6546
|
HOST: "127.0.0.1",
|
|
5983
|
-
PORTLESS_URL: url
|
|
6547
|
+
PORTLESS_URL: url,
|
|
6548
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds)
|
|
5984
6549
|
};
|
|
5985
6550
|
if (tls2) {
|
|
5986
6551
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
@@ -5992,7 +6557,7 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
|
|
|
5992
6557
|
const child = spawnChildProcess(app.commandArgs, env, app.pkg.dir);
|
|
5993
6558
|
pipeOutput(child, chalk.cyan(`[${app.name}]`));
|
|
5994
6559
|
const capturedStore = store;
|
|
5995
|
-
const
|
|
6560
|
+
const capturedHostnames = hostnames;
|
|
5996
6561
|
child.on("exit", (code, signal) => {
|
|
5997
6562
|
exitCodes.set(app.name, code);
|
|
5998
6563
|
if (code !== 0 && code !== null) {
|
|
@@ -6000,14 +6565,11 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
|
|
|
6000
6565
|
} else if (signal) {
|
|
6001
6566
|
console.error(colors_default.yellow(`[${app.name}] killed by ${signal}`));
|
|
6002
6567
|
}
|
|
6003
|
-
if (capturedStore &&
|
|
6004
|
-
|
|
6005
|
-
capturedStore.removeRoute(capturedHostname);
|
|
6006
|
-
} catch {
|
|
6007
|
-
}
|
|
6568
|
+
if (capturedStore && capturedHostnames.length > 0) {
|
|
6569
|
+
removeRoutes(capturedStore, capturedHostnames, process.pid);
|
|
6008
6570
|
}
|
|
6009
6571
|
});
|
|
6010
|
-
const route = store &&
|
|
6572
|
+
const route = store && hostnames.length > 0 ? { store, hostnames } : null;
|
|
6011
6573
|
return { child, displayUrl, route };
|
|
6012
6574
|
}
|
|
6013
6575
|
function spawnTaskApp(app, exitCodes) {
|
|
@@ -6118,7 +6680,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
|
|
|
6118
6680
|
console.log(chalk.blue.bold(`
|
|
6119
6681
|
portless
|
|
6120
6682
|
`));
|
|
6121
|
-
let { dir, port, tls: tls2,
|
|
6683
|
+
let { dir, port, tls: tls2, tlds } = await discoverState();
|
|
6122
6684
|
if (proxiedApps.length > 0) {
|
|
6123
6685
|
let multiDesired;
|
|
6124
6686
|
try {
|
|
@@ -6132,9 +6694,9 @@ portless
|
|
|
6132
6694
|
dir = ensureResult.state.dir;
|
|
6133
6695
|
port = ensureResult.state.port;
|
|
6134
6696
|
tls2 = ensureResult.state.tls;
|
|
6135
|
-
|
|
6697
|
+
tlds = ensureResult.state.tlds;
|
|
6136
6698
|
} else {
|
|
6137
|
-
({ dir, port, tls: tls2,
|
|
6699
|
+
({ dir, port, tls: tls2, tlds } = await discoverState());
|
|
6138
6700
|
}
|
|
6139
6701
|
if (tls2 && !isCATrusted(dir)) {
|
|
6140
6702
|
await handleTrust();
|
|
@@ -6142,12 +6704,12 @@ portless
|
|
|
6142
6704
|
}
|
|
6143
6705
|
const useTurbo = loaded?.config.turbo !== false && hasTurboConfig(wsRoot);
|
|
6144
6706
|
if (useTurbo) {
|
|
6145
|
-
await runWithTurbo(wsRoot, dir, port, tls2,
|
|
6707
|
+
await runWithTurbo(wsRoot, dir, port, tls2, tlds, scriptName, proxiedApps, taskApps, extraArgs);
|
|
6146
6708
|
} else {
|
|
6147
|
-
await runWithDirectSpawn(dir, port, tls2,
|
|
6709
|
+
await runWithDirectSpawn(dir, port, tls2, tlds, proxiedApps, taskApps);
|
|
6148
6710
|
}
|
|
6149
6711
|
}
|
|
6150
|
-
async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2,
|
|
6712
|
+
async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tlds, scriptName, proxiedApps, taskApps, extraArgs = []) {
|
|
6151
6713
|
const store = new RouteStore(stateDir, {
|
|
6152
6714
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
6153
6715
|
});
|
|
@@ -6161,17 +6723,17 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
|
|
|
6161
6723
|
continue;
|
|
6162
6724
|
}
|
|
6163
6725
|
const appPort = app.appPort ?? await findFreePort();
|
|
6164
|
-
const
|
|
6165
|
-
const
|
|
6166
|
-
const url =
|
|
6726
|
+
const hostnames = buildHostnames(app.name, tlds);
|
|
6727
|
+
const urls = formatUrls(hostnames, proxyPort, tls2);
|
|
6728
|
+
const url = urls[0];
|
|
6167
6729
|
appUrls.push({ label: app.label, url });
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
routes.push({ hostname });
|
|
6730
|
+
addRoutes(store, hostnames, appPort, process.pid);
|
|
6731
|
+
routes.push({ hostnames });
|
|
6171
6732
|
const entry = {
|
|
6172
6733
|
PORT: String(appPort),
|
|
6173
6734
|
HOST: "127.0.0.1",
|
|
6174
|
-
PORTLESS_URL: url
|
|
6735
|
+
PORTLESS_URL: url,
|
|
6736
|
+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: formatViteAllowedHosts(tlds)
|
|
6175
6737
|
};
|
|
6176
6738
|
if (tls2) {
|
|
6177
6739
|
const caPath = path9.join(stateDir, "ca.pem");
|
|
@@ -6214,11 +6776,8 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
|
|
|
6214
6776
|
killTree(turboChild, "SIGKILL");
|
|
6215
6777
|
}
|
|
6216
6778
|
}, SIGKILL_TIMEOUT_MS).unref();
|
|
6217
|
-
for (const {
|
|
6218
|
-
|
|
6219
|
-
store.removeRoute(hostname);
|
|
6220
|
-
} catch {
|
|
6221
|
-
}
|
|
6779
|
+
for (const { hostnames } of routes) {
|
|
6780
|
+
removeRoutes(store, hostnames, process.pid);
|
|
6222
6781
|
}
|
|
6223
6782
|
removeManifest();
|
|
6224
6783
|
};
|
|
@@ -6232,7 +6791,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
|
|
|
6232
6791
|
process.exit(exitCode);
|
|
6233
6792
|
}
|
|
6234
6793
|
}
|
|
6235
|
-
async function runWithDirectSpawn(stateDir, proxyPort, tls2,
|
|
6794
|
+
async function runWithDirectSpawn(stateDir, proxyPort, tls2, tlds, proxiedApps, taskApps) {
|
|
6236
6795
|
const children = [];
|
|
6237
6796
|
const exitCodes = /* @__PURE__ */ new Map();
|
|
6238
6797
|
const appUrls = [];
|
|
@@ -6243,7 +6802,7 @@ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, t
|
|
|
6243
6802
|
stateDir,
|
|
6244
6803
|
proxyPort,
|
|
6245
6804
|
tls2,
|
|
6246
|
-
|
|
6805
|
+
tlds,
|
|
6247
6806
|
exitCodes
|
|
6248
6807
|
);
|
|
6249
6808
|
children.push(child);
|
|
@@ -6278,11 +6837,8 @@ async function runWithDirectSpawn(stateDir, proxyPort, tls2, tld, proxiedApps, t
|
|
|
6278
6837
|
}
|
|
6279
6838
|
}
|
|
6280
6839
|
}, SIGKILL_TIMEOUT_MS).unref();
|
|
6281
|
-
for (const { store,
|
|
6282
|
-
|
|
6283
|
-
store.removeRoute(hostname);
|
|
6284
|
-
} catch {
|
|
6285
|
-
}
|
|
6840
|
+
for (const { store, hostnames } of routeEntries) {
|
|
6841
|
+
removeRoutes(store, hostnames, process.pid);
|
|
6286
6842
|
}
|
|
6287
6843
|
};
|
|
6288
6844
|
process.on("SIGINT", cleanup);
|
|
@@ -6341,7 +6897,7 @@ async function handleRunMode(args, globalScript) {
|
|
|
6341
6897
|
}
|
|
6342
6898
|
const worktree = detectWorktreePrefix();
|
|
6343
6899
|
const effectiveName = worktree ? `${worktree.prefix}.${baseName}` : baseName;
|
|
6344
|
-
const { dir, port, tls: tls2,
|
|
6900
|
+
const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
|
|
6345
6901
|
const store = new RouteStore(dir, {
|
|
6346
6902
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
6347
6903
|
});
|
|
@@ -6352,7 +6908,7 @@ async function handleRunMode(args, globalScript) {
|
|
|
6352
6908
|
effectiveName,
|
|
6353
6909
|
parsed.commandArgs,
|
|
6354
6910
|
tls2,
|
|
6355
|
-
|
|
6911
|
+
tlds,
|
|
6356
6912
|
parsed.force,
|
|
6357
6913
|
{ nameSource, prefix: worktree?.prefix, prefixSource: worktree?.source },
|
|
6358
6914
|
parsed.appPort,
|
|
@@ -6378,7 +6934,7 @@ async function handleNamedMode(args) {
|
|
|
6378
6934
|
}
|
|
6379
6935
|
const safeName = parsed.name.split(".").map((label) => truncateLabel(label)).join(".");
|
|
6380
6936
|
parseHostname(safeName, DEFAULT_TLD);
|
|
6381
|
-
const { dir, port, tls: tls2,
|
|
6937
|
+
const { dir, port, tls: tls2, tlds, lanMode, lanIp } = await discoverState();
|
|
6382
6938
|
const store = new RouteStore(dir, {
|
|
6383
6939
|
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
6384
6940
|
});
|
|
@@ -6389,7 +6945,7 @@ async function handleNamedMode(args) {
|
|
|
6389
6945
|
safeName,
|
|
6390
6946
|
parsed.commandArgs,
|
|
6391
6947
|
tls2,
|
|
6392
|
-
|
|
6948
|
+
tlds,
|
|
6393
6949
|
parsed.force,
|
|
6394
6950
|
void 0,
|
|
6395
6951
|
parsed.appPort,
|
|
@@ -6492,7 +7048,7 @@ async function main() {
|
|
|
6492
7048
|
args.shift();
|
|
6493
7049
|
}
|
|
6494
7050
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
6495
|
-
if (skipPortless && (isRunCommand || args.length === 0 || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean" && args[0] !== "service")) {
|
|
7051
|
+
if (skipPortless && (isRunCommand || args.length === 0 || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean" && args[0] !== "doctor" && args[0] !== "service")) {
|
|
6496
7052
|
const parsed = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
6497
7053
|
let commandArgs = parsed.commandArgs;
|
|
6498
7054
|
if (commandArgs.length === 0 && (isRunCommand || args.length === 0)) {
|
|
@@ -6540,6 +7096,10 @@ async function main() {
|
|
|
6540
7096
|
await handleList();
|
|
6541
7097
|
return;
|
|
6542
7098
|
}
|
|
7099
|
+
if (args[0] === "doctor") {
|
|
7100
|
+
await handleDoctor(args);
|
|
7101
|
+
return;
|
|
7102
|
+
}
|
|
6543
7103
|
if (args[0] === "get") {
|
|
6544
7104
|
await handleGet(args);
|
|
6545
7105
|
return;
|