portless 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -7
- package/dist/{chunk-WXEE5QH6.js → chunk-EZJWUTUA.js} +53 -8
- package/dist/cli.js +389 -78
- package/dist/index.d.ts +6 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,11 @@ portless myapp next dev
|
|
|
32
32
|
|
|
33
33
|
HTTPS with HTTP/2 is enabled by default. On first run, portless generates a local CA, trusts it, and binds port 443 (auto-elevates with sudo on macOS/Linux). Use `--no-tls` for plain HTTP.
|
|
34
34
|
|
|
35
|
-
The proxy auto-starts when you run an app. A random port (4000
|
|
35
|
+
The proxy auto-starts when you run an app. A random port (4000-4999) is assigned via the `PORT` environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore `PORT` (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag.
|
|
36
|
+
|
|
37
|
+
When auto-starting, portless reuses the configuration (port, TLS, TLD) from the most recent proxy run, so a restart or reboot does not silently revert to defaults. Explicit env vars (`PORTLESS_PORT`, `PORTLESS_HTTPS`, etc.) always take priority.
|
|
38
|
+
|
|
39
|
+
In non-interactive environments (no TTY, or `CI=1`), portless exits with a descriptive error instead of prompting, so task runners like turborepo and CI scripts fail early with a clear message.
|
|
36
40
|
|
|
37
41
|
## Use in package.json
|
|
38
42
|
|
|
@@ -88,7 +92,7 @@ portless myapp next dev
|
|
|
88
92
|
# -> https://myapp.test
|
|
89
93
|
```
|
|
90
94
|
|
|
91
|
-
The proxy auto-syncs `/etc/hosts` for
|
|
95
|
+
The proxy auto-syncs `/etc/hosts` for route hostnames (including `.test`), so those domains resolve on your machine.
|
|
92
96
|
|
|
93
97
|
Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflicts with mDNS/Bonjour) and `.dev` (Google-owned, forces HTTPS via HSTS).
|
|
94
98
|
|
|
@@ -139,7 +143,7 @@ portless proxy start --lan --ip 192.168.1.42
|
|
|
139
143
|
|
|
140
144
|
`--lan` switches the proxy to mDNS discovery: services are advertised as `<name>.local` and reachable from any device on the same network. Portless auto-detects your LAN IP and follows Wi-Fi/IP changes automatically, but you can pin another address with `--ip <address>` or by exporting `PORTLESS_LAN_IP`. Set `PORTLESS_LAN=1` in your shell (0/1 boolean) to make LAN mode the default whenever the proxy starts.
|
|
141
145
|
|
|
142
|
-
Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode.
|
|
146
|
+
Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start it again, it stays in LAN mode. All proxy settings (port, TLS, TLD, LAN) are persisted and reused on auto-start unless overridden by explicit flags or env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first.
|
|
143
147
|
|
|
144
148
|
LAN mode depends on the system mDNS tools that portless already spawns: macOS ships with `dns-sd`, while Linux uses `avahi-publish-address` from `avahi-utils` (install via `sudo apt install avahi-utils` or your distro’s equivalent). If the command is missing or your network isn’t reachable, `portless proxy start --lan` prints the relevant error and exits.
|
|
145
149
|
|
|
@@ -166,6 +170,7 @@ portless alias <name> <port> --force # Overwrite an existing route
|
|
|
166
170
|
portless alias --remove <name> # Remove a static route
|
|
167
171
|
portless list # Show active routes
|
|
168
172
|
portless trust # Add local CA to system trust store
|
|
173
|
+
portless clean # Remove state, CA trust entry, and hosts block
|
|
169
174
|
portless hosts sync # Add routes to /etc/hosts (fixes Safari)
|
|
170
175
|
portless hosts clean # Remove portless entries from /etc/hosts
|
|
171
176
|
|
|
@@ -210,16 +215,27 @@ PORTLESS_HTTPS=0 Disable HTTPS (same as --no-tls)
|
|
|
210
215
|
PORTLESS_LAN=1 Enable LAN mode when set to 1 (auto-detects LAN IP)
|
|
211
216
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test; default: localhost)
|
|
212
217
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
213
|
-
PORTLESS_SYNC_HOSTS=
|
|
218
|
+
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of /etc/hosts (on by default)
|
|
214
219
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
215
220
|
|
|
216
221
|
# Injected into child processes
|
|
217
222
|
PORT Ephemeral port the child should listen on
|
|
218
223
|
HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
|
|
219
224
|
PORTLESS_URL Public URL (e.g. https://myapp.localhost)
|
|
225
|
+
NODE_EXTRA_CA_CERTS Path to the portless CA (when HTTPS is active)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name from your project, or `portless --name <name> <cmd>` to force any name including reserved ones.
|
|
229
|
+
|
|
230
|
+
## Uninstall / reset
|
|
231
|
+
|
|
232
|
+
To remove portless data from your machine (proxy state under `~/.portless` and the system state directory, the local CA from the OS trust store when portless installed it, and the portless block in `/etc/hosts`):
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
portless clean
|
|
220
236
|
```
|
|
221
237
|
|
|
222
|
-
|
|
238
|
+
macOS/Linux may prompt for `sudo`. Custom certificate paths passed with `--cert` and `--key` are not deleted.
|
|
223
239
|
|
|
224
240
|
## Safari / DNS
|
|
225
241
|
|
|
@@ -232,7 +248,7 @@ portless hosts sync # Add current routes to /etc/hosts
|
|
|
232
248
|
portless hosts clean # Clean up later
|
|
233
249
|
```
|
|
234
250
|
|
|
235
|
-
Auto-syncs `/etc/hosts` for custom TLDs
|
|
251
|
+
Auto-syncs `/etc/hosts` for route hostnames by default (`.localhost`, custom TLDs, LAN `.local`). Set `PORTLESS_SYNC_HOSTS=0` to disable.
|
|
236
252
|
|
|
237
253
|
## Proxying Between Portless Apps
|
|
238
254
|
|
|
@@ -264,7 +280,7 @@ devServer: {
|
|
|
264
280
|
}
|
|
265
281
|
```
|
|
266
282
|
|
|
267
|
-
|
|
283
|
+
Portless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem` (or `~/.portless/ca.pem` when the proxy runs on a non-privileged port like 1355). Alternatively, use `--no-tls` for plain HTTP.
|
|
268
284
|
|
|
269
285
|
Portless detects this misconfiguration and responds with `508 Loop Detected` along with a message pointing to this fix.
|
|
270
286
|
|
|
@@ -392,8 +392,10 @@ function createProxyServer(options) {
|
|
|
392
392
|
proxyRes.on("error", () => {
|
|
393
393
|
if (!res.headersSent) {
|
|
394
394
|
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
395
|
+
res.end();
|
|
396
|
+
} else {
|
|
397
|
+
res.destroy();
|
|
395
398
|
}
|
|
396
|
-
res.end();
|
|
397
399
|
});
|
|
398
400
|
proxyRes.pipe(res);
|
|
399
401
|
}
|
|
@@ -516,9 +518,19 @@ function createProxyServer(options) {
|
|
|
516
518
|
cert: tls.ca ? Buffer.concat([tls.cert, tls.ca]) : tls.cert,
|
|
517
519
|
key: tls.key,
|
|
518
520
|
allowHTTP1: true,
|
|
521
|
+
// Tolerate high rates of RST_STREAM from browsers during HMR and
|
|
522
|
+
// page navigations. Without this, Node sends GOAWAY INTERNAL_ERROR
|
|
523
|
+
// after ~1000 cumulative stream resets and kills the session,
|
|
524
|
+
// surfacing as ERR_HTTP2_PROTOCOL_ERROR in Chrome. Available in
|
|
525
|
+
// Node 22.11+; silently ignored on older versions.
|
|
526
|
+
...{ streamResetBurst: 1e4, streamResetRate: 100 },
|
|
519
527
|
...tls.SNICallback ? { SNICallback: tls.SNICallback } : {}
|
|
520
528
|
});
|
|
529
|
+
h2Server.on("sessionError", () => {
|
|
530
|
+
});
|
|
521
531
|
h2Server.on("request", (req, res) => {
|
|
532
|
+
req.stream?.on("error", () => {
|
|
533
|
+
});
|
|
522
534
|
handleRequest(req, res);
|
|
523
535
|
});
|
|
524
536
|
h2Server.on("upgrade", (req, socket, head) => {
|
|
@@ -614,6 +626,9 @@ function buildBlock(hostnames) {
|
|
|
614
626
|
${entries}
|
|
615
627
|
${MARKER_END}`;
|
|
616
628
|
}
|
|
629
|
+
function shouldAutoSyncHosts(syncVal) {
|
|
630
|
+
return syncVal !== "0" && syncVal !== "false";
|
|
631
|
+
}
|
|
617
632
|
function syncHostsFile(hostnames) {
|
|
618
633
|
try {
|
|
619
634
|
const content = readHostsFile();
|
|
@@ -898,6 +913,24 @@ function isLanEnvEnabled() {
|
|
|
898
913
|
const val = process.env.PORTLESS_LAN;
|
|
899
914
|
return val === "1" || val === "true";
|
|
900
915
|
}
|
|
916
|
+
function readPersistedProxyState() {
|
|
917
|
+
const dirs = [];
|
|
918
|
+
if (process.env.PORTLESS_STATE_DIR) {
|
|
919
|
+
dirs.push(process.env.PORTLESS_STATE_DIR);
|
|
920
|
+
} else {
|
|
921
|
+
dirs.push(USER_STATE_DIR, SYSTEM_STATE_DIR);
|
|
922
|
+
}
|
|
923
|
+
for (const dir of dirs) {
|
|
924
|
+
const port = readPortFromDir(dir);
|
|
925
|
+
if (port !== null) {
|
|
926
|
+
const tls = readTlsMarker(dir);
|
|
927
|
+
const tld = readTldFromDir(dir);
|
|
928
|
+
const lanIp = readLanMarker(dir);
|
|
929
|
+
return { port, tls, tld, lanMode: lanIp !== null || tld === "local" };
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
901
934
|
function buildProxyStartConfig(options) {
|
|
902
935
|
const effectiveTld = options.lanMode ? "local" : options.tld;
|
|
903
936
|
const args = [];
|
|
@@ -931,6 +964,9 @@ function buildProxyStartConfig(options) {
|
|
|
931
964
|
if (options.useWildcard) {
|
|
932
965
|
args.push("--wildcard");
|
|
933
966
|
}
|
|
967
|
+
if (options.skipTrust) {
|
|
968
|
+
args.push("--skip-trust");
|
|
969
|
+
}
|
|
934
970
|
return { effectiveTld, args };
|
|
935
971
|
}
|
|
936
972
|
async function discoverState() {
|
|
@@ -999,8 +1035,8 @@ async function discoverState() {
|
|
|
999
1035
|
return {
|
|
1000
1036
|
dir,
|
|
1001
1037
|
port: configuredPort,
|
|
1002
|
-
tls:
|
|
1003
|
-
tld:
|
|
1038
|
+
tls: readTlsMarker(dir),
|
|
1039
|
+
tld: readTldFromDir(dir),
|
|
1004
1040
|
lanMode: readLanMarker(dir) !== null,
|
|
1005
1041
|
lanIp: null
|
|
1006
1042
|
};
|
|
@@ -1269,8 +1305,9 @@ function prompt(question) {
|
|
|
1269
1305
|
import * as fs4 from "fs";
|
|
1270
1306
|
import * as path3 from "path";
|
|
1271
1307
|
var STALE_LOCK_THRESHOLD_MS = 1e4;
|
|
1272
|
-
var
|
|
1273
|
-
var
|
|
1308
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
1309
|
+
var LOCK_RETRY_BASE_MS = 10;
|
|
1310
|
+
var LOCK_RETRY_CAP_MS = 500;
|
|
1274
1311
|
var FILE_MODE = 420;
|
|
1275
1312
|
var DIR_MODE = 493;
|
|
1276
1313
|
var SYSTEM_DIR_MODE = 1023;
|
|
@@ -1334,8 +1371,10 @@ var RouteStore = class _RouteStore {
|
|
|
1334
1371
|
syncSleep(ms) {
|
|
1335
1372
|
Atomics.wait(_RouteStore.sleepBuffer, 0, 0, ms);
|
|
1336
1373
|
}
|
|
1337
|
-
acquireLock(
|
|
1338
|
-
|
|
1374
|
+
acquireLock() {
|
|
1375
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
1376
|
+
let delay = LOCK_RETRY_BASE_MS;
|
|
1377
|
+
while (Date.now() < deadline) {
|
|
1339
1378
|
try {
|
|
1340
1379
|
fs4.mkdirSync(this.lockPath);
|
|
1341
1380
|
return true;
|
|
@@ -1350,7 +1389,9 @@ var RouteStore = class _RouteStore {
|
|
|
1350
1389
|
} catch {
|
|
1351
1390
|
continue;
|
|
1352
1391
|
}
|
|
1353
|
-
|
|
1392
|
+
const jitter = Math.floor(Math.random() * delay);
|
|
1393
|
+
this.syncSleep(delay + jitter);
|
|
1394
|
+
delay = Math.min(delay * 2, LOCK_RETRY_CAP_MS);
|
|
1354
1395
|
} else {
|
|
1355
1396
|
return false;
|
|
1356
1397
|
}
|
|
@@ -1475,6 +1516,7 @@ export {
|
|
|
1475
1516
|
extractManagedBlock,
|
|
1476
1517
|
removeBlock,
|
|
1477
1518
|
buildBlock,
|
|
1519
|
+
shouldAutoSyncHosts,
|
|
1478
1520
|
syncHostsFile,
|
|
1479
1521
|
cleanHostsFile,
|
|
1480
1522
|
getManagedHostnames,
|
|
@@ -1484,6 +1526,8 @@ export {
|
|
|
1484
1526
|
PRIVILEGED_PORT_THRESHOLD,
|
|
1485
1527
|
INTERNAL_LAN_IP_ENV,
|
|
1486
1528
|
INTERNAL_LAN_IP_FLAG,
|
|
1529
|
+
SYSTEM_STATE_DIR,
|
|
1530
|
+
USER_STATE_DIR,
|
|
1487
1531
|
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
1488
1532
|
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
1489
1533
|
getDefaultPort,
|
|
@@ -1501,6 +1545,7 @@ export {
|
|
|
1501
1545
|
isHttpsEnvDisabled,
|
|
1502
1546
|
isWildcardEnvEnabled,
|
|
1503
1547
|
isLanEnvEnabled,
|
|
1548
|
+
readPersistedProxyState,
|
|
1504
1549
|
buildProxyStartConfig,
|
|
1505
1550
|
discoverState,
|
|
1506
1551
|
findFreePort,
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
RISKY_TLDS,
|
|
10
10
|
RouteConflictError,
|
|
11
11
|
RouteStore,
|
|
12
|
+
SYSTEM_STATE_DIR,
|
|
13
|
+
USER_STATE_DIR,
|
|
12
14
|
WAIT_FOR_PROXY_INTERVAL_MS,
|
|
13
15
|
WAIT_FOR_PROXY_MAX_ATTEMPTS,
|
|
14
16
|
buildProxyStartConfig,
|
|
@@ -33,9 +35,11 @@ import {
|
|
|
33
35
|
parseHostname,
|
|
34
36
|
prompt,
|
|
35
37
|
readLanMarker,
|
|
38
|
+
readPersistedProxyState,
|
|
36
39
|
readTldFromDir,
|
|
37
40
|
readTlsMarker,
|
|
38
41
|
resolveStateDir,
|
|
42
|
+
shouldAutoSyncHosts,
|
|
39
43
|
spawnCommand,
|
|
40
44
|
syncHostsFile,
|
|
41
45
|
validateTld,
|
|
@@ -43,7 +47,7 @@ import {
|
|
|
43
47
|
writeLanMarker,
|
|
44
48
|
writeTldFile,
|
|
45
49
|
writeTlsMarker
|
|
46
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-EZJWUTUA.js";
|
|
47
51
|
|
|
48
52
|
// src/colors.ts
|
|
49
53
|
function supportsColor() {
|
|
@@ -71,8 +75,8 @@ var gray = wrap("90", "39");
|
|
|
71
75
|
var colors_default = { bold, red, green, yellow, blue, cyan, white, gray };
|
|
72
76
|
|
|
73
77
|
// src/cli.ts
|
|
74
|
-
import * as
|
|
75
|
-
import * as
|
|
78
|
+
import * as fs4 from "fs";
|
|
79
|
+
import * as path4 from "path";
|
|
76
80
|
import { spawn as spawn2, spawnSync as spawnSync2 } from "child_process";
|
|
77
81
|
|
|
78
82
|
// src/certs.ts
|
|
@@ -87,6 +91,9 @@ var SERVER_VALIDITY_DAYS = 365;
|
|
|
87
91
|
var EXPIRY_BUFFER_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
88
92
|
var CA_COMMON_NAME = "portless Local CA";
|
|
89
93
|
var OPENSSL_TIMEOUT_MS = 15e3;
|
|
94
|
+
var MACOS_SECURITY_TIMEOUT_MS = 15e3;
|
|
95
|
+
var MACOS_SECURITY_AUTH_TIMEOUT_MS = 12e4;
|
|
96
|
+
var MACOS_SECURITY_ROOT_TIMEOUT_MS = 6e4;
|
|
90
97
|
var CA_KEY_FILE = "ca-key.pem";
|
|
91
98
|
var CA_CERT_FILE = "ca.pem";
|
|
92
99
|
var SERVER_KEY_FILE = "server-key.pem";
|
|
@@ -335,13 +342,13 @@ function isCATrustedMacOS(caCertPath) {
|
|
|
335
342
|
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
336
343
|
{
|
|
337
344
|
stdio: "pipe",
|
|
338
|
-
timeout:
|
|
345
|
+
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
339
346
|
}
|
|
340
347
|
);
|
|
341
348
|
} else {
|
|
342
349
|
execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
343
350
|
stdio: "pipe",
|
|
344
|
-
timeout:
|
|
351
|
+
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
345
352
|
});
|
|
346
353
|
}
|
|
347
354
|
return true;
|
|
@@ -353,7 +360,7 @@ function loginKeychainPath() {
|
|
|
353
360
|
try {
|
|
354
361
|
const result = execFileSync("security", ["default-keychain"], {
|
|
355
362
|
encoding: "utf-8",
|
|
356
|
-
timeout:
|
|
363
|
+
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
357
364
|
}).trim();
|
|
358
365
|
const match = result.match(/"(.+)"/);
|
|
359
366
|
if (match) return match[1];
|
|
@@ -569,14 +576,14 @@ function trustCA(stateDir) {
|
|
|
569
576
|
"/Library/Keychains/System.keychain",
|
|
570
577
|
caCertPath
|
|
571
578
|
],
|
|
572
|
-
{ stdio: "pipe", timeout:
|
|
579
|
+
{ stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS }
|
|
573
580
|
);
|
|
574
581
|
} else {
|
|
575
582
|
const keychain = loginKeychainPath();
|
|
576
583
|
execFileSync(
|
|
577
584
|
"security",
|
|
578
585
|
["add-trusted-cert", "-r", "trustRoot", "-k", keychain, caCertPath],
|
|
579
|
-
{ stdio: "pipe", timeout:
|
|
586
|
+
{ stdio: "pipe", timeout: MACOS_SECURITY_AUTH_TIMEOUT_MS }
|
|
580
587
|
);
|
|
581
588
|
}
|
|
582
589
|
return { trusted: true };
|
|
@@ -599,6 +606,10 @@ function trustCA(stateDir) {
|
|
|
599
606
|
return { trusted: false, error: `Unsupported platform: ${process.platform}` };
|
|
600
607
|
} catch (err) {
|
|
601
608
|
const message = err instanceof Error ? err.message : String(err);
|
|
609
|
+
if (message.includes("ETIMEDOUT")) {
|
|
610
|
+
const hint = process.platform === "darwin" ? "The macOS security command timed out. This can happen when the Keychain Services daemon is unresponsive or a system authorization dialog was not dismissed in time. Try restarting Keychain Access (or run: sudo killall securityd) and then: portless trust" : "The trust command timed out. Try: portless trust";
|
|
611
|
+
return { trusted: false, error: hint };
|
|
612
|
+
}
|
|
602
613
|
if (message.includes("authorization") || message.includes("permission") || message.includes("EACCES")) {
|
|
603
614
|
return {
|
|
604
615
|
trusted: false,
|
|
@@ -608,6 +619,129 @@ function trustCA(stateDir) {
|
|
|
608
619
|
return { trusted: false, error: message };
|
|
609
620
|
}
|
|
610
621
|
}
|
|
622
|
+
function untrustCA(stateDir) {
|
|
623
|
+
const caCertPath = path.join(stateDir, CA_CERT_FILE);
|
|
624
|
+
if (!fileExists(caCertPath)) {
|
|
625
|
+
return { removed: true };
|
|
626
|
+
}
|
|
627
|
+
if (!isCATrusted(stateDir)) {
|
|
628
|
+
return { removed: true };
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
if (process.platform === "darwin") {
|
|
632
|
+
return untrustCAMacOS(caCertPath);
|
|
633
|
+
}
|
|
634
|
+
if (process.platform === "linux") {
|
|
635
|
+
return untrustCALinux(stateDir);
|
|
636
|
+
}
|
|
637
|
+
if (process.platform === "win32") {
|
|
638
|
+
return untrustCAWindows(caCertPath);
|
|
639
|
+
}
|
|
640
|
+
return { removed: false, error: `Unsupported platform: ${process.platform}` };
|
|
641
|
+
} catch (err) {
|
|
642
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
643
|
+
return { removed: false, error: message };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function untrustCAMacOS(caCertPath) {
|
|
647
|
+
const errors = [];
|
|
648
|
+
const tryExec = (args) => {
|
|
649
|
+
try {
|
|
650
|
+
execFileSync("security", args, { stdio: "pipe", timeout: MACOS_SECURITY_ROOT_TIMEOUT_MS });
|
|
651
|
+
return true;
|
|
652
|
+
} catch (err) {
|
|
653
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
654
|
+
errors.push(message);
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
if (tryExec(["remove-trusted-cert", caCertPath])) {
|
|
659
|
+
return isCATrustedMacOSAfterAttempt(caCertPath) ? { removed: false, error: errors.join("; ") || "Trust entry may still be present" } : { removed: true };
|
|
660
|
+
}
|
|
661
|
+
const login = loginKeychainPath();
|
|
662
|
+
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, login]);
|
|
663
|
+
tryExec(["delete-certificate", "-c", CA_COMMON_NAME, "/Library/Keychains/System.keychain"]);
|
|
664
|
+
return isCATrustedMacOSAfterAttempt(caCertPath) ? { removed: false, error: errors.join("; ") || "Could not remove CA from keychain (try sudo)" } : { removed: true };
|
|
665
|
+
}
|
|
666
|
+
function isCATrustedMacOSAfterAttempt(caCertPath) {
|
|
667
|
+
try {
|
|
668
|
+
const isRoot = (process.getuid?.() ?? -1) === 0;
|
|
669
|
+
const sudoUser = process.env.SUDO_USER;
|
|
670
|
+
if (isRoot && sudoUser) {
|
|
671
|
+
execFileSync(
|
|
672
|
+
"sudo",
|
|
673
|
+
["-u", sudoUser, "security", "verify-cert", "-c", caCertPath, "-L", "-p", "ssl"],
|
|
674
|
+
{ stdio: "pipe", timeout: MACOS_SECURITY_TIMEOUT_MS }
|
|
675
|
+
);
|
|
676
|
+
} else {
|
|
677
|
+
execFileSync("security", ["verify-cert", "-c", caCertPath, "-L", "-p", "ssl"], {
|
|
678
|
+
stdio: "pipe",
|
|
679
|
+
timeout: MACOS_SECURITY_TIMEOUT_MS
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
} catch {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function untrustCALinux(stateDir) {
|
|
688
|
+
const errors = [];
|
|
689
|
+
let deletedAny = false;
|
|
690
|
+
for (const config of Object.values(LINUX_CA_TRUST_CONFIGS)) {
|
|
691
|
+
const dest = path.join(config.certDir, "portless-ca.crt");
|
|
692
|
+
try {
|
|
693
|
+
if (fileExists(dest)) {
|
|
694
|
+
const ours = fs.readFileSync(path.join(stateDir, CA_CERT_FILE), "utf-8").trim();
|
|
695
|
+
const installed = fs.readFileSync(dest, "utf-8").trim();
|
|
696
|
+
if (ours === installed) {
|
|
697
|
+
fs.unlinkSync(dest);
|
|
698
|
+
deletedAny = true;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} catch (err) {
|
|
702
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (deletedAny) {
|
|
706
|
+
try {
|
|
707
|
+
const config = getLinuxCATrustConfig();
|
|
708
|
+
execFileSync(config.updateCommand, [], { stdio: "pipe", timeout: 3e4 });
|
|
709
|
+
} catch (err) {
|
|
710
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (isCATrusted(stateDir)) {
|
|
714
|
+
return {
|
|
715
|
+
removed: false,
|
|
716
|
+
error: errors.join("; ") || "CA still trusted (remove portless-ca.crt and run the distro CA update command, often with sudo)"
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
return { removed: true };
|
|
720
|
+
}
|
|
721
|
+
function untrustCAWindows(caCertPath) {
|
|
722
|
+
try {
|
|
723
|
+
const fingerprint = openssl(["x509", "-in", caCertPath, "-noout", "-fingerprint", "-sha1"]).trim().replace(/^.*=/, "").replace(/:/g, "").toLowerCase();
|
|
724
|
+
const storeListing = execFileSync("certutil", ["-store", "-user", "Root"], {
|
|
725
|
+
encoding: "utf-8",
|
|
726
|
+
timeout: 1e4,
|
|
727
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
728
|
+
});
|
|
729
|
+
const normalized = storeListing.replace(/\s/g, "").toLowerCase();
|
|
730
|
+
if (!normalized.includes(fingerprint)) {
|
|
731
|
+
return { removed: true };
|
|
732
|
+
}
|
|
733
|
+
execFileSync("certutil", ["-delstore", "-user", "Root", "portless Local CA"], {
|
|
734
|
+
stdio: "pipe",
|
|
735
|
+
timeout: 3e4
|
|
736
|
+
});
|
|
737
|
+
if (isCATrustedWindows(caCertPath)) {
|
|
738
|
+
return { removed: false, error: "certutil could not remove the portless CA from Root" };
|
|
739
|
+
}
|
|
740
|
+
return { removed: true };
|
|
741
|
+
} catch (err) {
|
|
742
|
+
return { removed: false, error: err instanceof Error ? err.message : String(err) };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
611
745
|
|
|
612
746
|
// src/auto.ts
|
|
613
747
|
import { createHash } from "crypto";
|
|
@@ -783,6 +917,53 @@ function readBranchFromHead(gitdir) {
|
|
|
783
917
|
}
|
|
784
918
|
}
|
|
785
919
|
|
|
920
|
+
// src/clean-utils.ts
|
|
921
|
+
import * as fs3 from "fs";
|
|
922
|
+
import * as path3 from "path";
|
|
923
|
+
var PORTLESS_STATE_FILES = [
|
|
924
|
+
"routes.json",
|
|
925
|
+
"routes.lock",
|
|
926
|
+
"proxy.pid",
|
|
927
|
+
"proxy.port",
|
|
928
|
+
"proxy.log",
|
|
929
|
+
"proxy.tls",
|
|
930
|
+
"proxy.tld",
|
|
931
|
+
"proxy.lan",
|
|
932
|
+
"ca-key.pem",
|
|
933
|
+
"ca.pem",
|
|
934
|
+
"server-key.pem",
|
|
935
|
+
"server.pem",
|
|
936
|
+
"server.csr",
|
|
937
|
+
"server-ext.cnf",
|
|
938
|
+
"ca.srl"
|
|
939
|
+
];
|
|
940
|
+
var HOST_CERTS_DIR2 = "host-certs";
|
|
941
|
+
function collectStateDirsForCleanup() {
|
|
942
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
943
|
+
const add = (d) => {
|
|
944
|
+
const trimmed = d?.trim();
|
|
945
|
+
if (!trimmed) return;
|
|
946
|
+
const resolved = path3.resolve(trimmed);
|
|
947
|
+
if (fs3.existsSync(resolved)) dirs.add(resolved);
|
|
948
|
+
};
|
|
949
|
+
add(USER_STATE_DIR);
|
|
950
|
+
add(SYSTEM_STATE_DIR);
|
|
951
|
+
add(process.env.PORTLESS_STATE_DIR);
|
|
952
|
+
return [...dirs];
|
|
953
|
+
}
|
|
954
|
+
function removePortlessStateFiles(dir) {
|
|
955
|
+
for (const f of PORTLESS_STATE_FILES) {
|
|
956
|
+
try {
|
|
957
|
+
fs3.unlinkSync(path3.join(dir, f));
|
|
958
|
+
} catch {
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
try {
|
|
962
|
+
fs3.rmSync(path3.join(dir, HOST_CERTS_DIR2), { recursive: true, force: true });
|
|
963
|
+
} catch {
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
786
967
|
// src/mdns.ts
|
|
787
968
|
import { spawn, spawnSync } from "child_process";
|
|
788
969
|
|
|
@@ -815,7 +996,7 @@ function isInternalInterface(iname, macStr, internal) {
|
|
|
815
996
|
return false;
|
|
816
997
|
}
|
|
817
998
|
function probeDefaultRouteIPv4() {
|
|
818
|
-
return new Promise((
|
|
999
|
+
return new Promise((resolve3, reject) => {
|
|
819
1000
|
const socket = createSocket({ type: "udp4", reuseAddr: true });
|
|
820
1001
|
socket.on("error", (error) => {
|
|
821
1002
|
socket.close();
|
|
@@ -827,7 +1008,7 @@ function probeDefaultRouteIPv4() {
|
|
|
827
1008
|
socket.close();
|
|
828
1009
|
socket.unref();
|
|
829
1010
|
if (addr && "address" in addr && addr.address && addr.address !== NO_ROUTE_IP) {
|
|
830
|
-
|
|
1011
|
+
resolve3(addr.address);
|
|
831
1012
|
} else {
|
|
832
1013
|
reject(new Error("No route to host"));
|
|
833
1014
|
}
|
|
@@ -1138,10 +1319,10 @@ function getEntryScript() {
|
|
|
1138
1319
|
function isLocallyInstalled() {
|
|
1139
1320
|
let dir = process.cwd();
|
|
1140
1321
|
for (; ; ) {
|
|
1141
|
-
if (
|
|
1322
|
+
if (fs4.existsSync(path4.join(dir, "node_modules", "portless", "package.json"))) {
|
|
1142
1323
|
return true;
|
|
1143
1324
|
}
|
|
1144
|
-
const parent =
|
|
1325
|
+
const parent = path4.dirname(dir);
|
|
1145
1326
|
if (parent === dir) break;
|
|
1146
1327
|
dir = parent;
|
|
1147
1328
|
}
|
|
@@ -1177,11 +1358,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1177
1358
|
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
1178
1359
|
}
|
|
1179
1360
|
const routesPath = store.getRoutesPath();
|
|
1180
|
-
if (!
|
|
1181
|
-
|
|
1361
|
+
if (!fs4.existsSync(routesPath)) {
|
|
1362
|
+
fs4.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
1182
1363
|
}
|
|
1183
1364
|
try {
|
|
1184
|
-
|
|
1365
|
+
fs4.chmodSync(routesPath, FILE_MODE);
|
|
1185
1366
|
} catch {
|
|
1186
1367
|
}
|
|
1187
1368
|
fixOwnership(routesPath);
|
|
@@ -1189,8 +1370,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1189
1370
|
let debounceTimer = null;
|
|
1190
1371
|
let watcher = null;
|
|
1191
1372
|
let pollingInterval = null;
|
|
1192
|
-
const
|
|
1193
|
-
const autoSyncHosts = syncVal === "1" || syncVal === "true" || tld !== DEFAULT_TLD && !activeLanIp && syncVal !== "0" && syncVal !== "false";
|
|
1373
|
+
const autoSyncHosts = shouldAutoSyncHosts(process.env.PORTLESS_SYNC_HOSTS);
|
|
1194
1374
|
const onMdnsError = (msg) => console.warn(chalk.yellow(msg));
|
|
1195
1375
|
const publishCachedRoutes = () => {
|
|
1196
1376
|
if (!activeLanIp) return;
|
|
@@ -1242,7 +1422,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1242
1422
|
}
|
|
1243
1423
|
};
|
|
1244
1424
|
try {
|
|
1245
|
-
watcher =
|
|
1425
|
+
watcher = fs4.watch(routesPath, () => {
|
|
1246
1426
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1247
1427
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
1248
1428
|
});
|
|
@@ -1292,8 +1472,8 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1292
1472
|
redirectServer.listen(80);
|
|
1293
1473
|
}
|
|
1294
1474
|
server.listen(proxyPort, () => {
|
|
1295
|
-
|
|
1296
|
-
|
|
1475
|
+
fs4.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
1476
|
+
fs4.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
1297
1477
|
writeTlsMarker(store.dir, isTls);
|
|
1298
1478
|
writeTldFile(store.dir, tld);
|
|
1299
1479
|
writeLanMarker(store.dir, activeLanIp);
|
|
@@ -1309,7 +1489,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1309
1489
|
console.log(chalk.gray("Services are discoverable as <name>.local on your network"));
|
|
1310
1490
|
if (isTls) {
|
|
1311
1491
|
console.log(chalk.yellow("For HTTPS on devices, install the CA certificate:"));
|
|
1312
|
-
console.log(chalk.gray(` ${
|
|
1492
|
+
console.log(chalk.gray(` ${path4.join(store.dir, "ca.pem")}`));
|
|
1313
1493
|
}
|
|
1314
1494
|
if (!lanIpPinned) {
|
|
1315
1495
|
lanMonitor = startLanIpMonitor({
|
|
@@ -1341,11 +1521,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
1341
1521
|
redirectServer.close();
|
|
1342
1522
|
}
|
|
1343
1523
|
try {
|
|
1344
|
-
|
|
1524
|
+
fs4.unlinkSync(store.pidPath);
|
|
1345
1525
|
} catch {
|
|
1346
1526
|
}
|
|
1347
1527
|
try {
|
|
1348
|
-
|
|
1528
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1349
1529
|
} catch {
|
|
1350
1530
|
}
|
|
1351
1531
|
writeTlsMarker(store.dir, false);
|
|
@@ -1374,7 +1554,7 @@ function sudoStopOrHint(port) {
|
|
|
1374
1554
|
}
|
|
1375
1555
|
async function stopProxy(store, proxyPort, _tls) {
|
|
1376
1556
|
const pidPath = store.pidPath;
|
|
1377
|
-
if (!
|
|
1557
|
+
if (!fs4.existsSync(pidPath)) {
|
|
1378
1558
|
if (await isProxyRunning(proxyPort)) {
|
|
1379
1559
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
1380
1560
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -1382,7 +1562,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1382
1562
|
try {
|
|
1383
1563
|
process.kill(pid, "SIGTERM");
|
|
1384
1564
|
try {
|
|
1385
|
-
|
|
1565
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1386
1566
|
} catch {
|
|
1387
1567
|
}
|
|
1388
1568
|
console.log(colors_default.green(`Killed process ${pid}. Proxy stopped.`));
|
|
@@ -1417,10 +1597,10 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1417
1597
|
return;
|
|
1418
1598
|
}
|
|
1419
1599
|
try {
|
|
1420
|
-
const pid = parseInt(
|
|
1600
|
+
const pid = parseInt(fs4.readFileSync(pidPath, "utf-8"), 10);
|
|
1421
1601
|
if (isNaN(pid)) {
|
|
1422
1602
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
1423
|
-
|
|
1603
|
+
fs4.unlinkSync(pidPath);
|
|
1424
1604
|
return;
|
|
1425
1605
|
}
|
|
1426
1606
|
try {
|
|
@@ -1431,9 +1611,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1431
1611
|
return;
|
|
1432
1612
|
}
|
|
1433
1613
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
1434
|
-
|
|
1614
|
+
fs4.unlinkSync(pidPath);
|
|
1435
1615
|
try {
|
|
1436
|
-
|
|
1616
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1437
1617
|
} catch {
|
|
1438
1618
|
}
|
|
1439
1619
|
return;
|
|
@@ -1445,13 +1625,13 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
1445
1625
|
)
|
|
1446
1626
|
);
|
|
1447
1627
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
1448
|
-
|
|
1628
|
+
fs4.unlinkSync(pidPath);
|
|
1449
1629
|
return;
|
|
1450
1630
|
}
|
|
1451
1631
|
process.kill(pid, "SIGTERM");
|
|
1452
|
-
|
|
1632
|
+
fs4.unlinkSync(pidPath);
|
|
1453
1633
|
try {
|
|
1454
|
-
|
|
1634
|
+
fs4.unlinkSync(store.portFilePath);
|
|
1455
1635
|
} catch {
|
|
1456
1636
|
}
|
|
1457
1637
|
console.log(colors_default.green("Proxy stopped."));
|
|
@@ -1523,11 +1703,30 @@ portless
|
|
|
1523
1703
|
const proxyResponsive = await isProxyRunning(proxyPort, tls2);
|
|
1524
1704
|
const proxyListeningFromStateDir = !!process.env.PORTLESS_STATE_DIR && await isPortListening(proxyPort);
|
|
1525
1705
|
if (!proxyResponsive && !proxyListeningFromStateDir) {
|
|
1526
|
-
const
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1706
|
+
const persisted = readPersistedProxyState();
|
|
1707
|
+
const startConfig = { ...desiredConfig };
|
|
1708
|
+
let startPort;
|
|
1709
|
+
if (persisted) {
|
|
1710
|
+
if (!explicit.useHttps && persisted.tls !== desiredConfig.useHttps) {
|
|
1711
|
+
startConfig.useHttps = persisted.tls;
|
|
1712
|
+
}
|
|
1713
|
+
if (!explicit.tld && persisted.tld !== desiredConfig.tld) {
|
|
1714
|
+
startConfig.tld = persisted.tld;
|
|
1715
|
+
}
|
|
1716
|
+
if (!explicit.lanMode && persisted.lanMode !== desiredConfig.lanMode) {
|
|
1717
|
+
startConfig.lanMode = persisted.lanMode;
|
|
1718
|
+
}
|
|
1719
|
+
const envPort = getDefaultPort(startConfig.useHttps);
|
|
1720
|
+
if (persisted.port !== envPort) {
|
|
1721
|
+
startPort = persisted.port;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const effectivePort = startPort ?? getDefaultPort(startConfig.useHttps);
|
|
1725
|
+
const needsSudo = !isWindows && effectivePort < PRIVILEGED_PORT_THRESHOLD;
|
|
1726
|
+
const manualStartCommand = formatProxyStartCommand(effectivePort, startConfig);
|
|
1727
|
+
const fallbackStartCommand = formatProxyStartCommand(FALLBACK_PROXY_PORT, startConfig);
|
|
1728
|
+
const isInteractive = !!process.stdin.isTTY && !process.env.CI;
|
|
1729
|
+
if (needsSudo && !isInteractive) {
|
|
1531
1730
|
console.error(colors_default.red("Proxy is not running and no TTY is available for sudo."));
|
|
1532
1731
|
console.error(colors_default.blue("Option 1: start the proxy in a terminal (will prompt for sudo):"));
|
|
1533
1732
|
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
@@ -1539,7 +1738,7 @@ portless
|
|
|
1539
1738
|
console.error(colors_default.cyan(` ${fallbackStartCommand}`));
|
|
1540
1739
|
process.exit(1);
|
|
1541
1740
|
}
|
|
1542
|
-
if (needsSudo
|
|
1741
|
+
if (needsSudo) {
|
|
1543
1742
|
const answer = await prompt(colors_default.yellow("Proxy not running. Start it? [Y/n/skip] "));
|
|
1544
1743
|
if (answer === "n" || answer === "no") {
|
|
1545
1744
|
console.log(colors_default.gray("Cancelled."));
|
|
@@ -1551,16 +1750,26 @@ portless
|
|
|
1551
1750
|
return;
|
|
1552
1751
|
}
|
|
1553
1752
|
}
|
|
1554
|
-
|
|
1753
|
+
if (persisted && startPort !== void 0) {
|
|
1754
|
+
console.log(
|
|
1755
|
+
colors_default.yellow(
|
|
1756
|
+
`Starting proxy with previous configuration (port ${startPort}, ${startConfig.useHttps ? "HTTPS" : "HTTP"})...`
|
|
1757
|
+
)
|
|
1758
|
+
);
|
|
1759
|
+
} else {
|
|
1760
|
+
console.log(colors_default.yellow("Starting proxy..."));
|
|
1761
|
+
}
|
|
1555
1762
|
const proxyStartConfig = buildProxyStartConfig({
|
|
1556
|
-
useHttps:
|
|
1557
|
-
customCertPath:
|
|
1558
|
-
customKeyPath:
|
|
1559
|
-
lanMode:
|
|
1560
|
-
lanIp:
|
|
1561
|
-
lanIpExplicit:
|
|
1562
|
-
tld:
|
|
1563
|
-
useWildcard:
|
|
1763
|
+
useHttps: startConfig.useHttps,
|
|
1764
|
+
customCertPath: startConfig.customCertPath,
|
|
1765
|
+
customKeyPath: startConfig.customKeyPath,
|
|
1766
|
+
lanMode: startConfig.lanMode,
|
|
1767
|
+
lanIp: startConfig.lanIpExplicit ? startConfig.lanIp : null,
|
|
1768
|
+
lanIpExplicit: startConfig.lanIpExplicit,
|
|
1769
|
+
tld: startConfig.tld,
|
|
1770
|
+
useWildcard: startConfig.useWildcard,
|
|
1771
|
+
includePort: startPort !== void 0,
|
|
1772
|
+
proxyPort: startPort
|
|
1564
1773
|
});
|
|
1565
1774
|
const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
|
|
1566
1775
|
const result = spawnSync2(process.execPath, startArgs, {
|
|
@@ -1580,11 +1789,11 @@ portless
|
|
|
1580
1789
|
}
|
|
1581
1790
|
if (!discovered) {
|
|
1582
1791
|
console.error(colors_default.red("Failed to start proxy."));
|
|
1583
|
-
const fallbackDir = resolveStateDir(
|
|
1584
|
-
const logPath =
|
|
1792
|
+
const fallbackDir = resolveStateDir(effectivePort);
|
|
1793
|
+
const logPath = path4.join(fallbackDir, "proxy.log");
|
|
1585
1794
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
1586
1795
|
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
1587
|
-
if (
|
|
1796
|
+
if (fs4.existsSync(logPath)) {
|
|
1588
1797
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
1589
1798
|
}
|
|
1590
1799
|
process.exit(1);
|
|
@@ -1657,7 +1866,7 @@ portless
|
|
|
1657
1866
|
console.log(chalk.green(` LAN -> ${finalUrl}`));
|
|
1658
1867
|
console.log(chalk.gray(" (accessible from other devices on the same WiFi network)\n"));
|
|
1659
1868
|
}
|
|
1660
|
-
const basename3 =
|
|
1869
|
+
const basename3 = path4.basename(commandArgs[0]);
|
|
1661
1870
|
const isExpo = basename3 === "expo";
|
|
1662
1871
|
const isExpoLan = isExpo && (lanMode || isLanEnvEnabled());
|
|
1663
1872
|
const hostBind = isExpoLan ? void 0 : "127.0.0.1";
|
|
@@ -1665,9 +1874,17 @@ portless
|
|
|
1665
1874
|
process.env.PORTLESS_LAN = "1";
|
|
1666
1875
|
}
|
|
1667
1876
|
injectFrameworkFlags(commandArgs, port);
|
|
1877
|
+
const caEnv = {};
|
|
1878
|
+
if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
|
|
1879
|
+
const caPath = path4.join(stateDir, "ca.pem");
|
|
1880
|
+
if (fs4.existsSync(caPath)) {
|
|
1881
|
+
caEnv.NODE_EXTRA_CA_CERTS = caPath;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
const caFragment = caEnv.NODE_EXTRA_CA_CERTS ? ` NODE_EXTRA_CA_CERTS="${caEnv.NODE_EXTRA_CA_CERTS}"` : "";
|
|
1668
1885
|
console.log(
|
|
1669
1886
|
chalk.gray(
|
|
1670
|
-
`Running: PORT=${port}${hostBind ? ` HOST=${hostBind}` : ""} PORTLESS_URL=${finalUrl} ${commandArgs.join(" ")}
|
|
1887
|
+
`Running: PORT=${port}${hostBind ? ` HOST=${hostBind}` : ""} PORTLESS_URL=${finalUrl}${caFragment} ${commandArgs.join(" ")}
|
|
1671
1888
|
`
|
|
1672
1889
|
)
|
|
1673
1890
|
);
|
|
@@ -1681,7 +1898,8 @@ portless
|
|
|
1681
1898
|
// Note: EXPO_PACKAGER_PROXY_URL is not used — expo-dev-client removed
|
|
1682
1899
|
// baked-in pinging, making this env var ineffective. Expo handles its
|
|
1683
1900
|
// own LAN discovery natively.
|
|
1684
|
-
...lanMode ? { PORTLESS_LAN: "1" } : {}
|
|
1901
|
+
...lanMode ? { PORTLESS_LAN: "1" } : {},
|
|
1902
|
+
...caEnv
|
|
1685
1903
|
},
|
|
1686
1904
|
onCleanup: () => {
|
|
1687
1905
|
try {
|
|
@@ -1839,6 +2057,7 @@ ${colors_default.bold("Usage:")}
|
|
|
1839
2057
|
${colors_default.cyan("portless alias --remove <name>")} Remove a static route
|
|
1840
2058
|
${colors_default.cyan("portless list")} Show active routes
|
|
1841
2059
|
${colors_default.cyan("portless trust")} Add local CA to system trust store
|
|
2060
|
+
${colors_default.cyan("portless clean")} Remove portless state, trust entry, and hosts block
|
|
1842
2061
|
${colors_default.cyan("portless hosts sync")} Add routes to ${HOSTS_DISPLAY} (fixes Safari)
|
|
1843
2062
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
1844
2063
|
|
|
@@ -1882,7 +2101,8 @@ ${colors_default.bold("LAN mode:")}
|
|
|
1882
2101
|
Expo keeps Metro's default LAN host behavior in this mode.
|
|
1883
2102
|
Auto-detected LAN IPs follow network changes automatically.
|
|
1884
2103
|
Stopped LAN proxies keep LAN mode for the next start via proxy.lan.
|
|
1885
|
-
|
|
2104
|
+
All proxy settings are persisted and reused on auto-start unless
|
|
2105
|
+
overridden by explicit flags or env vars.
|
|
1886
2106
|
Use PORTLESS_LAN=0 for one start to switch back to .localhost mode.
|
|
1887
2107
|
If a proxy is already running with different explicit LAN/TLS/TLD settings,
|
|
1888
2108
|
stop it first.
|
|
@@ -1916,7 +2136,7 @@ ${colors_default.bold("Environment variables:")}
|
|
|
1916
2136
|
PORTLESS_LAN=1 Enable LAN mode when set to 1 (set in .bashrc / .zshrc)
|
|
1917
2137
|
PORTLESS_TLD=<tld> Use a custom TLD (e.g. test, dev; default: localhost)
|
|
1918
2138
|
PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to parent route
|
|
1919
|
-
PORTLESS_SYNC_HOSTS=
|
|
2139
|
+
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of ${HOSTS_DISPLAY} (on by default)
|
|
1920
2140
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
1921
2141
|
PORTLESS=0 Run command directly without proxy
|
|
1922
2142
|
|
|
@@ -1925,12 +2145,13 @@ ${colors_default.bold("Child process environment:")}
|
|
|
1925
2145
|
HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
|
|
1926
2146
|
PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
|
|
1927
2147
|
PORTLESS_LAN Set to 1 when proxy is in LAN mode
|
|
2148
|
+
NODE_EXTRA_CA_CERTS Path to the portless CA (set when HTTPS is active)
|
|
1928
2149
|
|
|
1929
2150
|
${colors_default.bold("Safari / DNS:")}
|
|
1930
2151
|
.localhost subdomains auto-resolve in Chrome, Firefox, and Edge.
|
|
1931
2152
|
Safari relies on the system DNS resolver, which may not handle them.
|
|
1932
|
-
Auto-syncs ${HOSTS_DISPLAY} for
|
|
1933
|
-
|
|
2153
|
+
Auto-syncs ${HOSTS_DISPLAY} for route hostnames by default (including .localhost,
|
|
2154
|
+
custom TLDs, and LAN .local). Set PORTLESS_SYNC_HOSTS=0 to disable. To manually sync:
|
|
1934
2155
|
${colors_default.cyan("portless hosts sync")}
|
|
1935
2156
|
Clean up later with:
|
|
1936
2157
|
${colors_default.cyan("portless hosts clean")}
|
|
@@ -1939,20 +2160,20 @@ ${colors_default.bold("Skip portless:")}
|
|
|
1939
2160
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
1940
2161
|
|
|
1941
2162
|
${colors_default.bold("Reserved names:")}
|
|
1942
|
-
run, get, alias, hosts, list, trust, proxy are subcommands and cannot
|
|
2163
|
+
run, get, alias, hosts, list, trust, clean, proxy are subcommands and cannot
|
|
1943
2164
|
be used as app names directly. Use "portless run" to infer the name,
|
|
1944
2165
|
or "portless --name <name>" to force any name including reserved ones.
|
|
1945
2166
|
`);
|
|
1946
2167
|
process.exit(0);
|
|
1947
2168
|
}
|
|
1948
2169
|
function printVersion() {
|
|
1949
|
-
console.log("0.10.
|
|
2170
|
+
console.log("0.10.2");
|
|
1950
2171
|
process.exit(0);
|
|
1951
2172
|
}
|
|
1952
2173
|
async function handleTrust() {
|
|
1953
2174
|
const { dir } = await discoverState();
|
|
1954
|
-
if (!
|
|
1955
|
-
|
|
2175
|
+
if (!fs4.existsSync(dir)) {
|
|
2176
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1956
2177
|
}
|
|
1957
2178
|
const { caGenerated } = ensureCerts(dir);
|
|
1958
2179
|
if (caGenerated) {
|
|
@@ -1988,6 +2209,90 @@ async function handleTrust() {
|
|
|
1988
2209
|
console.error(colors_default.red(`Failed to trust CA: ${result.error}`));
|
|
1989
2210
|
process.exit(1);
|
|
1990
2211
|
}
|
|
2212
|
+
async function handleClean(args) {
|
|
2213
|
+
if (args[1] === "--help" || args[1] === "-h") {
|
|
2214
|
+
console.log(`
|
|
2215
|
+
${colors_default.bold("portless clean")} - Remove portless artifacts from this machine.
|
|
2216
|
+
|
|
2217
|
+
Stops the proxy if it is running, removes the local CA from the OS trust store
|
|
2218
|
+
when it was installed by portless, deletes known files under state directories
|
|
2219
|
+
(~/.portless, the system state directory, and PORTLESS_STATE_DIR when set),
|
|
2220
|
+
and removes the portless block from ${HOSTS_DISPLAY}.
|
|
2221
|
+
|
|
2222
|
+
Only allowlisted filenames under each state directory are deleted. Custom
|
|
2223
|
+
certificate paths from --cert and --key are never removed.
|
|
2224
|
+
|
|
2225
|
+
macOS/Linux may prompt for sudo when the proxy, trust store, or ${HOSTS_DISPLAY}
|
|
2226
|
+
require elevated privileges. On Windows, run as Administrator if needed.
|
|
2227
|
+
|
|
2228
|
+
${colors_default.bold("Usage:")}
|
|
2229
|
+
${colors_default.cyan("portless clean")}
|
|
2230
|
+
|
|
2231
|
+
${colors_default.bold("Options:")}
|
|
2232
|
+
--help, -h Show this help
|
|
2233
|
+
`);
|
|
2234
|
+
process.exit(0);
|
|
2235
|
+
}
|
|
2236
|
+
if (args.length > 1) {
|
|
2237
|
+
console.error(colors_default.red(`Error: Unknown argument "${args[1]}".`));
|
|
2238
|
+
console.error(colors_default.cyan(" portless clean --help"));
|
|
2239
|
+
process.exit(1);
|
|
2240
|
+
}
|
|
2241
|
+
console.log(colors_default.cyan("Stopping proxy if it is running..."));
|
|
2242
|
+
const { dir, port, tls: tls2 } = await discoverState();
|
|
2243
|
+
const store = new RouteStore(dir, {
|
|
2244
|
+
onWarning: (msg) => console.warn(colors_default.yellow(msg))
|
|
2245
|
+
});
|
|
2246
|
+
await stopProxy(store, port, tls2);
|
|
2247
|
+
const stateDirs = collectStateDirsForCleanup();
|
|
2248
|
+
for (const stateDir of stateDirs) {
|
|
2249
|
+
const caPath = path4.join(stateDir, "ca.pem");
|
|
2250
|
+
if (!fs4.existsSync(caPath)) continue;
|
|
2251
|
+
const wasTrusted = isCATrusted(stateDir);
|
|
2252
|
+
if (!wasTrusted) continue;
|
|
2253
|
+
const untrustResult = untrustCA(stateDir);
|
|
2254
|
+
if (untrustResult.removed) {
|
|
2255
|
+
console.log(colors_default.green("Removed local CA from the system trust store."));
|
|
2256
|
+
} else if (untrustResult.error) {
|
|
2257
|
+
console.warn(
|
|
2258
|
+
colors_default.yellow(
|
|
2259
|
+
`Could not remove CA from trust store: ${untrustResult.error}
|
|
2260
|
+
Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
2261
|
+
)
|
|
2262
|
+
);
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
for (const stateDir of stateDirs) {
|
|
2266
|
+
removePortlessStateFiles(stateDir);
|
|
2267
|
+
}
|
|
2268
|
+
console.log(colors_default.green("Removed portless state files from known state directories."));
|
|
2269
|
+
if (cleanHostsFile()) {
|
|
2270
|
+
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
2271
|
+
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
2272
|
+
console.log(
|
|
2273
|
+
colors_default.yellow(`Updating ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
2274
|
+
);
|
|
2275
|
+
const result = spawnSync2(
|
|
2276
|
+
"sudo",
|
|
2277
|
+
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "clean"],
|
|
2278
|
+
{
|
|
2279
|
+
stdio: "inherit",
|
|
2280
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
2281
|
+
}
|
|
2282
|
+
);
|
|
2283
|
+
if (result.status !== 0) {
|
|
2284
|
+
console.error(colors_default.red(`Failed to update ${HOSTS_DISPLAY}. Run: sudo portless clean`));
|
|
2285
|
+
process.exit(1);
|
|
2286
|
+
}
|
|
2287
|
+
} else {
|
|
2288
|
+
console.warn(
|
|
2289
|
+
colors_default.yellow(
|
|
2290
|
+
`Could not remove portless entries from ${HOSTS_DISPLAY}${isWindows ? " (run as Administrator)." : "."}`
|
|
2291
|
+
)
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
console.log(colors_default.green("Clean finished."));
|
|
2295
|
+
}
|
|
1991
2296
|
async function handleList() {
|
|
1992
2297
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
1993
2298
|
const store = new RouteStore(dir, {
|
|
@@ -2122,8 +2427,8 @@ ${colors_default.bold("Usage:")}
|
|
|
2122
2427
|
${colors_default.cyan("portless hosts clean")} Remove portless entries from ${HOSTS_DISPLAY}
|
|
2123
2428
|
|
|
2124
2429
|
${colors_default.bold("Auto-sync:")}
|
|
2125
|
-
|
|
2126
|
-
PORTLESS_SYNC_HOSTS=
|
|
2430
|
+
The proxy updates ${HOSTS_DISPLAY} for route hostnames by default. Disable with
|
|
2431
|
+
PORTLESS_SYNC_HOSTS=0.
|
|
2127
2432
|
`);
|
|
2128
2433
|
process.exit(0);
|
|
2129
2434
|
}
|
|
@@ -2260,6 +2565,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2260
2565
|
process.exit(isProxyHelp || !args[1] ? 0 : 1);
|
|
2261
2566
|
}
|
|
2262
2567
|
const isForeground = args.includes("--foreground");
|
|
2568
|
+
const skipTrust = args.includes("--skip-trust");
|
|
2263
2569
|
const hasHttpsFlag = args.includes("--https");
|
|
2264
2570
|
const hasNoTls = args.includes("--no-tls") || isHttpsEnvDisabled();
|
|
2265
2571
|
const wantHttps = !hasNoTls;
|
|
@@ -2491,8 +2797,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2491
2797
|
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
2492
2798
|
} else {
|
|
2493
2799
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
2494
|
-
const logPath2 =
|
|
2495
|
-
if (
|
|
2800
|
+
const logPath2 = path4.join(resolveStateDir(proxyPort), "proxy.log");
|
|
2801
|
+
if (fs4.existsSync(logPath2)) {
|
|
2496
2802
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
2497
2803
|
}
|
|
2498
2804
|
}
|
|
@@ -2531,8 +2837,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2531
2837
|
store.ensureDir();
|
|
2532
2838
|
if (customCertPath && customKeyPath) {
|
|
2533
2839
|
try {
|
|
2534
|
-
const cert =
|
|
2535
|
-
const key =
|
|
2840
|
+
const cert = fs4.readFileSync(customCertPath);
|
|
2841
|
+
const key = fs4.readFileSync(customKeyPath);
|
|
2536
2842
|
const certStr = cert.toString("utf-8");
|
|
2537
2843
|
const keyStr = key.toString("utf-8");
|
|
2538
2844
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -2559,7 +2865,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2559
2865
|
if (certs.caGenerated) {
|
|
2560
2866
|
console.log(colors_default.green("Generated local CA certificate."));
|
|
2561
2867
|
}
|
|
2562
|
-
if (!isCATrusted(stateDir)) {
|
|
2868
|
+
if (!skipTrust && !isCATrusted(stateDir)) {
|
|
2563
2869
|
console.log(colors_default.yellow("Adding CA to system trust store..."));
|
|
2564
2870
|
const trustResult = trustCA(stateDir);
|
|
2565
2871
|
if (trustResult.trusted) {
|
|
@@ -2577,9 +2883,9 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2577
2883
|
console.warn(colors_default.cyan(" portless trust"));
|
|
2578
2884
|
}
|
|
2579
2885
|
}
|
|
2580
|
-
const cert =
|
|
2581
|
-
const key =
|
|
2582
|
-
const ca =
|
|
2886
|
+
const cert = fs4.readFileSync(certs.certPath);
|
|
2887
|
+
const key = fs4.readFileSync(certs.keyPath);
|
|
2888
|
+
const ca = fs4.readFileSync(certs.caPath);
|
|
2583
2889
|
tlsOptions = {
|
|
2584
2890
|
cert,
|
|
2585
2891
|
key,
|
|
@@ -2594,11 +2900,11 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2594
2900
|
return;
|
|
2595
2901
|
}
|
|
2596
2902
|
store.ensureDir();
|
|
2597
|
-
const logPath =
|
|
2598
|
-
const logFd =
|
|
2903
|
+
const logPath = path4.join(stateDir, "proxy.log");
|
|
2904
|
+
const logFd = fs4.openSync(logPath, "a");
|
|
2599
2905
|
try {
|
|
2600
2906
|
try {
|
|
2601
|
-
|
|
2907
|
+
fs4.chmodSync(logPath, FILE_MODE);
|
|
2602
2908
|
} catch {
|
|
2603
2909
|
}
|
|
2604
2910
|
fixOwnership(logPath);
|
|
@@ -2617,7 +2923,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2617
2923
|
useWildcard: desiredWildcard,
|
|
2618
2924
|
foreground: true,
|
|
2619
2925
|
includePort: true,
|
|
2620
|
-
proxyPort
|
|
2926
|
+
proxyPort,
|
|
2927
|
+
skipTrust: true
|
|
2621
2928
|
}).args
|
|
2622
2929
|
];
|
|
2623
2930
|
const child = spawn2(process.execPath, daemonArgs, {
|
|
@@ -2628,13 +2935,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
2628
2935
|
});
|
|
2629
2936
|
child.unref();
|
|
2630
2937
|
} finally {
|
|
2631
|
-
|
|
2938
|
+
fs4.closeSync(logFd);
|
|
2632
2939
|
}
|
|
2633
2940
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
2634
2941
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
2635
2942
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
2636
2943
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
2637
|
-
if (
|
|
2944
|
+
if (fs4.existsSync(logPath)) {
|
|
2638
2945
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
2639
2946
|
}
|
|
2640
2947
|
process.exit(1);
|
|
@@ -2795,7 +3102,7 @@ async function main() {
|
|
|
2795
3102
|
args.shift();
|
|
2796
3103
|
}
|
|
2797
3104
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
2798
|
-
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy")) {
|
|
3105
|
+
if (skipPortless && (isRunCommand || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean")) {
|
|
2799
3106
|
const { commandArgs } = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
2800
3107
|
if (commandArgs.length === 0) {
|
|
2801
3108
|
console.error(colors_default.red("Error: No command provided."));
|
|
@@ -2817,6 +3124,10 @@ async function main() {
|
|
|
2817
3124
|
await handleTrust();
|
|
2818
3125
|
return;
|
|
2819
3126
|
}
|
|
3127
|
+
if (args[0] === "clean") {
|
|
3128
|
+
await handleClean(args);
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
2820
3131
|
if (args[0] === "list") {
|
|
2821
3132
|
await handleList();
|
|
2822
3133
|
return;
|
package/dist/index.d.ts
CHANGED
|
@@ -157,6 +157,11 @@ declare function removeBlock(content: string): string;
|
|
|
157
157
|
* Build a portless-managed block for the given hostnames.
|
|
158
158
|
*/
|
|
159
159
|
declare function buildBlock(hostnames: string[]): string;
|
|
160
|
+
/**
|
|
161
|
+
* Whether the proxy should write route hostnames to the hosts file.
|
|
162
|
+
* Disabled only when `PORTLESS_SYNC_HOSTS` is `0` or `false` (opt-out).
|
|
163
|
+
*/
|
|
164
|
+
declare function shouldAutoSyncHosts(syncVal: string | undefined): boolean;
|
|
160
165
|
/**
|
|
161
166
|
* Sync /etc/hosts to include entries for all given hostnames.
|
|
162
167
|
* Replaces any existing portless-managed block. Requires root access.
|
|
@@ -178,4 +183,4 @@ declare function getManagedHostnames(): string[];
|
|
|
178
183
|
*/
|
|
179
184
|
declare function checkHostResolution(hostname: string): Promise<boolean>;
|
|
180
185
|
|
|
181
|
-
export { DIR_MODE, FILE_MODE, PORTLESS_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, buildBlock, checkHostResolution, cleanHostsFile, createHttpRedirectServer, createProxyServer, escapeHtml, extractManagedBlock, fixOwnership, formatUrl, getManagedHostnames, isErrnoException, parseHostname, removeBlock, syncHostsFile };
|
|
186
|
+
export { DIR_MODE, FILE_MODE, PORTLESS_HEADER, type ProxyServer, type ProxyServerOptions, RouteConflictError, type RouteInfo, type RouteMapping, RouteStore, SYSTEM_DIR_MODE, SYSTEM_FILE_MODE, buildBlock, checkHostResolution, cleanHostsFile, createHttpRedirectServer, createProxyServer, escapeHtml, extractManagedBlock, fixOwnership, formatUrl, getManagedHostnames, isErrnoException, parseHostname, removeBlock, shouldAutoSyncHosts, syncHostsFile };
|
package/dist/index.js
CHANGED
|
@@ -19,8 +19,9 @@ import {
|
|
|
19
19
|
isErrnoException,
|
|
20
20
|
parseHostname,
|
|
21
21
|
removeBlock,
|
|
22
|
+
shouldAutoSyncHosts,
|
|
22
23
|
syncHostsFile
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-EZJWUTUA.js";
|
|
24
25
|
export {
|
|
25
26
|
DIR_MODE,
|
|
26
27
|
FILE_MODE,
|
|
@@ -42,5 +43,6 @@ export {
|
|
|
42
43
|
isErrnoException,
|
|
43
44
|
parseHostname,
|
|
44
45
|
removeBlock,
|
|
46
|
+
shouldAutoSyncHosts,
|
|
45
47
|
syncHostsFile
|
|
46
48
|
};
|