portless 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -2
- package/dist/cli.js +714 -85
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -249,6 +249,18 @@ portless trust
|
|
|
249
249
|
|
|
250
250
|
On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and openSUSE (via `update-ca-certificates` or `update-ca-trust`). On Windows, it uses `certutil` to add the CA to the system trust store.
|
|
251
251
|
|
|
252
|
+
## Start at OS startup
|
|
253
|
+
|
|
254
|
+
Install the proxy as an OS startup service so clean HTTPS URLs are available after reboot without starting the proxy from a terminal:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
portless service install
|
|
258
|
+
portless service status
|
|
259
|
+
portless service uninstall
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The service uses portless defaults: HTTPS on port 443 with `.localhost` names. macOS and Linux install a root-owned service so port 443 can bind at boot. Windows installs a Task Scheduler startup task that runs as SYSTEM. Installation and removal may require administrator privileges. `portless clean` automatically removes the service.
|
|
263
|
+
|
|
252
264
|
## LAN mode
|
|
253
265
|
|
|
254
266
|
```bash
|
|
@@ -300,9 +312,11 @@ portless myapp --funnel next dev
|
|
|
300
312
|
# -> https://devbox.yourteam.ts.net (public)
|
|
301
313
|
```
|
|
302
314
|
|
|
315
|
+
Tailscale HTTPS certificates must be enabled before `--tailscale` or `--funnel` can register HTTPS URLs. Funnel must also be enabled for the tailnet and node before `--funnel` can register the public URL. If either setting is missing, portless exits before starting the child process.
|
|
316
|
+
|
|
303
317
|
Set `PORTLESS_TAILSCALE=1` in your shell profile or `.env` to share every app by default. `portless list` shows both local and tailnet URLs. Tailscale serve registrations are cleaned up automatically when the app exits.
|
|
304
318
|
|
|
305
|
-
Requires the Tailscale CLI to be installed and connected (`tailscale up`).
|
|
319
|
+
Requires the Tailscale CLI to be installed and connected (`tailscale up`), with Tailscale HTTPS certificates enabled.
|
|
306
320
|
|
|
307
321
|
## Commands
|
|
308
322
|
|
|
@@ -332,6 +346,11 @@ portless proxy start -p 1355 # Start on a custom port (no sudo)
|
|
|
332
346
|
portless proxy start --foreground # Start in foreground (for debugging)
|
|
333
347
|
portless proxy start --wildcard # Allow unregistered subdomains to fall back to parent
|
|
334
348
|
portless proxy stop # Stop the proxy
|
|
349
|
+
|
|
350
|
+
# OS startup service
|
|
351
|
+
portless service install # Start HTTPS proxy when the OS starts
|
|
352
|
+
portless service status # Show service and proxy status
|
|
353
|
+
portless service uninstall # Remove the startup service
|
|
335
354
|
```
|
|
336
355
|
|
|
337
356
|
### Options
|
|
@@ -378,7 +397,7 @@ PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is a
|
|
|
378
397
|
NODE_EXTRA_CA_CERTS Path to the portless CA (when HTTPS is active)
|
|
379
398
|
```
|
|
380
399
|
|
|
381
|
-
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, `prune`, and `
|
|
400
|
+
> **Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, `prune`, `proxy`, and `service` 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.
|
|
382
401
|
|
|
383
402
|
## Uninstall / reset
|
|
384
403
|
|
package/dist/cli.js
CHANGED
|
@@ -39,9 +39,9 @@ var gray = dim;
|
|
|
39
39
|
var colors_default = { bold, dim, red, green, yellow, blue, cyan, white, gray };
|
|
40
40
|
|
|
41
41
|
// src/cli.ts
|
|
42
|
-
import * as
|
|
43
|
-
import * as
|
|
44
|
-
import { spawn as spawn3, spawnSync as
|
|
42
|
+
import * as fs9 from "fs";
|
|
43
|
+
import * as path9 from "path";
|
|
44
|
+
import { spawn as spawn3, spawnSync as spawnSync4 } from "child_process";
|
|
45
45
|
import { StringDecoder } from "string_decoder";
|
|
46
46
|
|
|
47
47
|
// src/certs.ts
|
|
@@ -757,10 +757,15 @@ function untrustCAWindows(caCertPath) {
|
|
|
757
757
|
// src/tailscale.ts
|
|
758
758
|
import { spawnSync } from "child_process";
|
|
759
759
|
var TAILSCALE_BINARY = "tailscale";
|
|
760
|
+
var TAILSCALE_COMMAND_TIMEOUT_MS = 3e4;
|
|
760
761
|
var PREFERRED_SERVE_PORTS = [443, 8443, 8444, 8445, 8446, 8447, 8448, 8449, 8450];
|
|
761
762
|
var FUNNEL_PORTS = [443, 8443, 1e4];
|
|
762
763
|
function defaultRunner(args) {
|
|
763
|
-
const result = spawnSync(TAILSCALE_BINARY, args, {
|
|
764
|
+
const result = spawnSync(TAILSCALE_BINARY, args, {
|
|
765
|
+
encoding: "utf-8",
|
|
766
|
+
killSignal: "SIGKILL",
|
|
767
|
+
timeout: TAILSCALE_COMMAND_TIMEOUT_MS
|
|
768
|
+
});
|
|
764
769
|
return {
|
|
765
770
|
status: result.status,
|
|
766
771
|
stdout: result.stdout ?? "",
|
|
@@ -812,11 +817,52 @@ function statusToDnsName(status) {
|
|
|
812
817
|
"Could not determine Tailscale node DNS name from `tailscale status --json`. Is Tailscale connected?"
|
|
813
818
|
);
|
|
814
819
|
}
|
|
815
|
-
function
|
|
820
|
+
function isFunnelCapability(value) {
|
|
821
|
+
const normalized = value.toLowerCase();
|
|
822
|
+
return normalized === "funnel" || normalized.endsWith("/funnel");
|
|
823
|
+
}
|
|
824
|
+
function isHttpsCapability(value) {
|
|
825
|
+
const normalized = value.toLowerCase();
|
|
826
|
+
return normalized === "https" || normalized.endsWith("/https");
|
|
827
|
+
}
|
|
828
|
+
function hasCapability(status, predicate) {
|
|
829
|
+
const capabilities = status.Self?.Capabilities;
|
|
830
|
+
if (Array.isArray(capabilities) && capabilities.some(predicate)) {
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
const capMap = status.Self?.CapMap;
|
|
834
|
+
return Boolean(capMap && Object.keys(capMap).some(predicate));
|
|
835
|
+
}
|
|
836
|
+
function hasHttpsCapability(status) {
|
|
837
|
+
return hasCapability(status, isHttpsCapability);
|
|
838
|
+
}
|
|
839
|
+
function hasFunnelCapability(status) {
|
|
840
|
+
return hasCapability(status, isFunnelCapability);
|
|
841
|
+
}
|
|
842
|
+
function throwHttpsNotEnabled() {
|
|
843
|
+
throw new Error(
|
|
844
|
+
"Tailscale HTTPS is not enabled on your tailnet. Enable HTTPS certificates in Tailscale DNS settings, then run portless again."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
function throwFunnelNotEnabled(status) {
|
|
848
|
+
const nodeId = status.Self?.ID;
|
|
849
|
+
const enableUrl = typeof nodeId === "string" && nodeId.length > 0 ? ` Visit https://login.tailscale.com/f/funnel?node=${nodeId} to enable it.` : "";
|
|
850
|
+
throw new Error(
|
|
851
|
+
"Tailscale Funnel is not enabled on your tailnet. Enable Funnel for this node, then run portless again." + enableUrl
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
function ensureTailscaleReady(options = {}) {
|
|
855
|
+
const runner = options.runner ?? defaultRunner;
|
|
816
856
|
runOrThrow(["version"], "check tailscale version", runner);
|
|
817
857
|
const statusResult = runOrThrow(["status", "--json"], "read tailscale status", runner);
|
|
818
858
|
const status = parseStatusJson(statusResult.stdout);
|
|
819
859
|
const dnsName = statusToDnsName(status);
|
|
860
|
+
if (options.requireHttps && !hasHttpsCapability(status)) {
|
|
861
|
+
throwHttpsNotEnabled();
|
|
862
|
+
}
|
|
863
|
+
if (options.requireFunnel && !hasFunnelCapability(status)) {
|
|
864
|
+
throwFunnelNotEnabled(status);
|
|
865
|
+
}
|
|
820
866
|
return {
|
|
821
867
|
dnsName,
|
|
822
868
|
baseUrl: `https://${dnsName}`
|
|
@@ -868,6 +914,16 @@ function isConflictError(stderr, stdout) {
|
|
|
868
914
|
${stdout}`.toLowerCase();
|
|
869
915
|
return text.includes("already in use") || text.includes("already exists") || text.includes("port conflict") || text.includes("address already");
|
|
870
916
|
}
|
|
917
|
+
function isFunnelNotEnabledError(stderr, stdout) {
|
|
918
|
+
const text = `${stderr}
|
|
919
|
+
${stdout}`.toLowerCase();
|
|
920
|
+
return text.includes("funnel is not enabled on your tailnet");
|
|
921
|
+
}
|
|
922
|
+
function formatFunnelNotEnabledError(stderr, stdout) {
|
|
923
|
+
const details = normalizeSpace(`${stderr}
|
|
924
|
+
${stdout}`);
|
|
925
|
+
return "Tailscale Funnel is not enabled on your tailnet. Enable Funnel for this node, then run portless again." + (details ? ` Tailscale said: ${details}` : "");
|
|
926
|
+
}
|
|
871
927
|
var CONFLICT_MESSAGES = {
|
|
872
928
|
serve: "Stop the existing serve or let portless auto-assign a different port.",
|
|
873
929
|
funnel: "Tailscale Funnel supports ports 443, 8443, and 10000."
|
|
@@ -882,9 +938,20 @@ function register(mode, localPort, httpsPort, runner) {
|
|
|
882
938
|
"Tailscale CLI not found. Install Tailscale (https://tailscale.com/download) and ensure `tailscale` is on PATH."
|
|
883
939
|
);
|
|
884
940
|
}
|
|
941
|
+
if (mode === "funnel" && isFunnelNotEnabledError(result.stderr, result.stdout)) {
|
|
942
|
+
throw new Error(formatFunnelNotEnabledError(result.stderr, result.stdout));
|
|
943
|
+
}
|
|
944
|
+
if (mode === "funnel" && errno.code === "ETIMEDOUT") {
|
|
945
|
+
throw new Error(
|
|
946
|
+
"Tailscale Funnel registration timed out. Make sure Funnel is enabled on your tailnet, then run portless again."
|
|
947
|
+
);
|
|
948
|
+
}
|
|
885
949
|
throw new Error(`Failed to register tailscale ${mode}: ${result.error.message}`);
|
|
886
950
|
}
|
|
887
951
|
if (result.status !== 0) {
|
|
952
|
+
if (mode === "funnel" && isFunnelNotEnabledError(result.stderr, result.stdout)) {
|
|
953
|
+
throw new Error(formatFunnelNotEnabledError(result.stderr, result.stdout));
|
|
954
|
+
}
|
|
888
955
|
if (isConflictError(result.stderr, result.stdout)) {
|
|
889
956
|
throw new Error(
|
|
890
957
|
`Tailscale ${mode === "funnel" ? "Funnel " : ""}HTTPS port ${httpsPort} is already in use. ` + CONFLICT_MESSAGES[mode]
|
|
@@ -2524,6 +2591,522 @@ function hasTurboConfig(wsRoot) {
|
|
|
2524
2591
|
}
|
|
2525
2592
|
}
|
|
2526
2593
|
|
|
2594
|
+
// src/service.ts
|
|
2595
|
+
import * as fs8 from "fs";
|
|
2596
|
+
import * as os2 from "os";
|
|
2597
|
+
import * as path8 from "path";
|
|
2598
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
2599
|
+
var SERVICE_PORT = getProtocolPort(true);
|
|
2600
|
+
var SERVICE_LABEL = "sh.portless.proxy";
|
|
2601
|
+
var SYSTEMD_SERVICE = "portless.service";
|
|
2602
|
+
var WINDOWS_TASK_NAME = "Portless Proxy";
|
|
2603
|
+
var INTERNAL_ELEVATED_ENV = "PORTLESS_INTERNAL_SERVICE_ELEVATED";
|
|
2604
|
+
function defaultRunner2(command, args, options) {
|
|
2605
|
+
return spawnSync3(command, args, {
|
|
2606
|
+
encoding: "utf-8",
|
|
2607
|
+
stdio: options?.stdio ?? "pipe"
|
|
2608
|
+
});
|
|
2609
|
+
}
|
|
2610
|
+
function isSupportedPlatform(platform) {
|
|
2611
|
+
return platform === "darwin" || platform === "linux" || platform === "win32";
|
|
2612
|
+
}
|
|
2613
|
+
function xmlEscape(value) {
|
|
2614
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2615
|
+
}
|
|
2616
|
+
function systemdEscape(value) {
|
|
2617
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
2618
|
+
}
|
|
2619
|
+
function windowsQuote(value) {
|
|
2620
|
+
return `"${value.replace(/"/g, '\\"')}"`;
|
|
2621
|
+
}
|
|
2622
|
+
function readPasswdHome(username) {
|
|
2623
|
+
try {
|
|
2624
|
+
const passwd = fs8.readFileSync("/etc/passwd", "utf-8");
|
|
2625
|
+
for (const line of passwd.split("\n")) {
|
|
2626
|
+
const fields = line.split(":");
|
|
2627
|
+
if (fields[0] === username && fields[5]) {
|
|
2628
|
+
return fields[5];
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
} catch {
|
|
2632
|
+
}
|
|
2633
|
+
return null;
|
|
2634
|
+
}
|
|
2635
|
+
function resolveUserContext(platform) {
|
|
2636
|
+
if (platform === "win32") {
|
|
2637
|
+
const home = process.env.USERPROFILE || os2.homedir();
|
|
2638
|
+
return { home, username: process.env.USERNAME };
|
|
2639
|
+
}
|
|
2640
|
+
const sudoUser = process.env.SUDO_USER;
|
|
2641
|
+
const sudoUid = process.env.SUDO_UID;
|
|
2642
|
+
const sudoGid = process.env.SUDO_GID;
|
|
2643
|
+
if (sudoUser && sudoUser !== "root") {
|
|
2644
|
+
const home = process.env.HOME && process.env.HOME !== "/var/root" && process.env.HOME !== "/root" ? process.env.HOME : readPasswdHome(sudoUser) || (platform === "darwin" ? path8.posix.join("/Users", sudoUser) : path8.posix.join("/home", sudoUser));
|
|
2645
|
+
return { home, uid: sudoUid, gid: sudoGid, username: sudoUser };
|
|
2646
|
+
}
|
|
2647
|
+
const userInfo2 = os2.userInfo();
|
|
2648
|
+
return {
|
|
2649
|
+
home: os2.homedir(),
|
|
2650
|
+
uid: process.getuid?.()?.toString(),
|
|
2651
|
+
gid: process.getgid?.()?.toString(),
|
|
2652
|
+
username: userInfo2.username
|
|
2653
|
+
};
|
|
2654
|
+
}
|
|
2655
|
+
function buildProxyCommand(entryScript) {
|
|
2656
|
+
const config = buildProxyStartConfig({
|
|
2657
|
+
useHttps: true,
|
|
2658
|
+
lanMode: false,
|
|
2659
|
+
tld: DEFAULT_TLD,
|
|
2660
|
+
foreground: true,
|
|
2661
|
+
includePort: true,
|
|
2662
|
+
proxyPort: SERVICE_PORT,
|
|
2663
|
+
skipTrust: true
|
|
2664
|
+
});
|
|
2665
|
+
return [entryScript, "proxy", "start", ...config.args];
|
|
2666
|
+
}
|
|
2667
|
+
function buildServiceEnv(ctx) {
|
|
2668
|
+
const env = {
|
|
2669
|
+
PORTLESS_STATE_DIR: ctx.stateDir
|
|
2670
|
+
};
|
|
2671
|
+
if (ctx.platform === "win32") {
|
|
2672
|
+
env.USERPROFILE = ctx.user.home;
|
|
2673
|
+
env.PATH = ctx.pathEnv;
|
|
2674
|
+
} else {
|
|
2675
|
+
env.HOME = ctx.user.home;
|
|
2676
|
+
if (ctx.user.uid) env.SUDO_UID = ctx.user.uid;
|
|
2677
|
+
if (ctx.user.gid) env.SUDO_GID = ctx.user.gid;
|
|
2678
|
+
}
|
|
2679
|
+
return env;
|
|
2680
|
+
}
|
|
2681
|
+
function defaultStateDir(platform, userHome) {
|
|
2682
|
+
return platform === "win32" ? path8.win32.join(userHome, ".portless") : path8.posix.join(userHome, ".portless");
|
|
2683
|
+
}
|
|
2684
|
+
function buildLaunchdPlist(ctx, programArguments) {
|
|
2685
|
+
const env = buildServiceEnv(ctx);
|
|
2686
|
+
const envEntries = Object.entries(env).map(
|
|
2687
|
+
([key, value]) => ` <key>${xmlEscape(key)}</key>
|
|
2688
|
+
<string>${xmlEscape(value)}</string>`
|
|
2689
|
+
).join("\n");
|
|
2690
|
+
const args = programArguments.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
2691
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2692
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2693
|
+
<plist version="1.0">
|
|
2694
|
+
<dict>
|
|
2695
|
+
<key>Label</key>
|
|
2696
|
+
<string>${SERVICE_LABEL}</string>
|
|
2697
|
+
<key>ProgramArguments</key>
|
|
2698
|
+
<array>
|
|
2699
|
+
${args}
|
|
2700
|
+
</array>
|
|
2701
|
+
<key>EnvironmentVariables</key>
|
|
2702
|
+
<dict>
|
|
2703
|
+
${envEntries}
|
|
2704
|
+
</dict>
|
|
2705
|
+
<key>RunAtLoad</key>
|
|
2706
|
+
<true/>
|
|
2707
|
+
<key>KeepAlive</key>
|
|
2708
|
+
<true/>
|
|
2709
|
+
<key>StandardOutPath</key>
|
|
2710
|
+
<string>${xmlEscape(path8.posix.join(ctx.stateDir, "service.log"))}</string>
|
|
2711
|
+
<key>StandardErrorPath</key>
|
|
2712
|
+
<string>${xmlEscape(path8.posix.join(ctx.stateDir, "service.log"))}</string>
|
|
2713
|
+
</dict>
|
|
2714
|
+
</plist>
|
|
2715
|
+
`;
|
|
2716
|
+
}
|
|
2717
|
+
function buildSystemdUnit(ctx, execStart) {
|
|
2718
|
+
const env = buildServiceEnv(ctx);
|
|
2719
|
+
const envLines = Object.entries(env).map(([key, value]) => `Environment=${key}=${systemdEscape(value)}`).join("\n");
|
|
2720
|
+
return `[Unit]
|
|
2721
|
+
Description=Portless HTTPS proxy
|
|
2722
|
+
After=network-online.target
|
|
2723
|
+
Wants=network-online.target
|
|
2724
|
+
|
|
2725
|
+
[Service]
|
|
2726
|
+
Type=simple
|
|
2727
|
+
${envLines}
|
|
2728
|
+
Environment=PATH=${systemdEscape(ctx.pathEnv)}
|
|
2729
|
+
ExecStart=${execStart.map(systemdEscape).join(" ")}
|
|
2730
|
+
Restart=on-failure
|
|
2731
|
+
RestartSec=2
|
|
2732
|
+
KillSignal=SIGTERM
|
|
2733
|
+
TimeoutStopSec=5
|
|
2734
|
+
|
|
2735
|
+
[Install]
|
|
2736
|
+
WantedBy=multi-user.target
|
|
2737
|
+
`;
|
|
2738
|
+
}
|
|
2739
|
+
function buildWindowsScript(ctx, command) {
|
|
2740
|
+
const env = buildServiceEnv(ctx);
|
|
2741
|
+
const setEnv = Object.entries(env).map(([key, value]) => `set "${key}=${value.replace(/"/g, "").replace(/%/g, "%%")}"`).join("\r\n");
|
|
2742
|
+
const proxyCommand = [windowsQuote(ctx.nodePath), ...command.map(windowsQuote)].join(" ");
|
|
2743
|
+
return `@echo off\r
|
|
2744
|
+
${setEnv}\r
|
|
2745
|
+
${proxyCommand}\r
|
|
2746
|
+
`;
|
|
2747
|
+
}
|
|
2748
|
+
function buildServiceSpec(options) {
|
|
2749
|
+
const ctx = {
|
|
2750
|
+
platform: options.platform,
|
|
2751
|
+
nodePath: options.nodePath,
|
|
2752
|
+
entryScript: options.entryScript,
|
|
2753
|
+
stateDir: options.stateDir || defaultStateDir(options.platform, options.userHome),
|
|
2754
|
+
user: {
|
|
2755
|
+
home: options.userHome,
|
|
2756
|
+
uid: options.uid,
|
|
2757
|
+
gid: options.gid,
|
|
2758
|
+
username: options.username
|
|
2759
|
+
},
|
|
2760
|
+
pathEnv: options.pathEnv || "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
|
|
2761
|
+
programData: options.programData || "C:\\ProgramData"
|
|
2762
|
+
};
|
|
2763
|
+
const proxyCommand = buildProxyCommand(ctx.entryScript);
|
|
2764
|
+
if (ctx.platform === "darwin") {
|
|
2765
|
+
const programArguments = [ctx.nodePath, ...proxyCommand];
|
|
2766
|
+
return {
|
|
2767
|
+
platform: "darwin",
|
|
2768
|
+
label: SERVICE_LABEL,
|
|
2769
|
+
plistPath: `/Library/LaunchDaemons/${SERVICE_LABEL}.plist`,
|
|
2770
|
+
plist: buildLaunchdPlist(ctx, programArguments),
|
|
2771
|
+
stateDir: ctx.stateDir,
|
|
2772
|
+
programArguments
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
if (ctx.platform === "linux") {
|
|
2776
|
+
const execStart = [ctx.nodePath, ...proxyCommand];
|
|
2777
|
+
return {
|
|
2778
|
+
platform: "linux",
|
|
2779
|
+
serviceName: SYSTEMD_SERVICE,
|
|
2780
|
+
unitPath: `/etc/systemd/system/${SYSTEMD_SERVICE}`,
|
|
2781
|
+
unit: buildSystemdUnit(ctx, execStart),
|
|
2782
|
+
stateDir: ctx.stateDir,
|
|
2783
|
+
execStart
|
|
2784
|
+
};
|
|
2785
|
+
}
|
|
2786
|
+
const scriptDir = path8.win32.join(ctx.programData, "portless", "service");
|
|
2787
|
+
const scriptPath = path8.win32.join(scriptDir, "portless-service.cmd");
|
|
2788
|
+
const script = buildWindowsScript(ctx, proxyCommand);
|
|
2789
|
+
const taskRun = windowsQuote(scriptPath);
|
|
2790
|
+
return {
|
|
2791
|
+
platform: "win32",
|
|
2792
|
+
taskName: WINDOWS_TASK_NAME,
|
|
2793
|
+
stateDir: ctx.stateDir,
|
|
2794
|
+
scriptDir,
|
|
2795
|
+
scriptPath,
|
|
2796
|
+
script,
|
|
2797
|
+
taskRun,
|
|
2798
|
+
createArgs: [
|
|
2799
|
+
"/Create",
|
|
2800
|
+
"/TN",
|
|
2801
|
+
WINDOWS_TASK_NAME,
|
|
2802
|
+
"/SC",
|
|
2803
|
+
"ONSTART",
|
|
2804
|
+
"/RU",
|
|
2805
|
+
"SYSTEM",
|
|
2806
|
+
"/RL",
|
|
2807
|
+
"HIGHEST",
|
|
2808
|
+
"/TR",
|
|
2809
|
+
taskRun,
|
|
2810
|
+
"/F"
|
|
2811
|
+
],
|
|
2812
|
+
runArgs: ["/Run", "/TN", WINDOWS_TASK_NAME],
|
|
2813
|
+
deleteArgs: ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"],
|
|
2814
|
+
queryArgs: ["/Query", "/TN", WINDOWS_TASK_NAME, "/FO", "LIST", "/V"]
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
function currentServiceSpec(entryScript) {
|
|
2818
|
+
if (!isSupportedPlatform(process.platform)) {
|
|
2819
|
+
throw new Error(`Unsupported platform: ${process.platform}`);
|
|
2820
|
+
}
|
|
2821
|
+
const user = resolveUserContext(process.platform);
|
|
2822
|
+
return buildServiceSpec({
|
|
2823
|
+
platform: process.platform,
|
|
2824
|
+
nodePath: process.execPath,
|
|
2825
|
+
entryScript,
|
|
2826
|
+
userHome: user.home,
|
|
2827
|
+
uid: user.uid,
|
|
2828
|
+
gid: user.gid,
|
|
2829
|
+
username: user.username,
|
|
2830
|
+
stateDir: process.env.PORTLESS_STATE_DIR || defaultStateDir(process.platform, user.home),
|
|
2831
|
+
pathEnv: process.env.PATH,
|
|
2832
|
+
programData: process.env.ProgramData
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
function collectPortlessEnvArgs(env = process.env, omit = /* @__PURE__ */ new Set()) {
|
|
2836
|
+
const envArgs = [];
|
|
2837
|
+
for (const key of Object.keys(env)) {
|
|
2838
|
+
if (key.startsWith("PORTLESS_") && env[key] && !omit.has(key)) {
|
|
2839
|
+
envArgs.push(`${key}=${env[key]}`);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
return envArgs;
|
|
2843
|
+
}
|
|
2844
|
+
function buildElevatedEnvArgs(options) {
|
|
2845
|
+
const extraEnv = options.extraEnv ?? {};
|
|
2846
|
+
const overrideKeys = /* @__PURE__ */ new Set(["PORTLESS_STATE_DIR", ...Object.keys(extraEnv)]);
|
|
2847
|
+
return [
|
|
2848
|
+
"env",
|
|
2849
|
+
...collectPortlessEnvArgs(options.env, overrideKeys),
|
|
2850
|
+
...Object.entries(extraEnv).map(([key, value]) => `${key}=${value}`),
|
|
2851
|
+
`HOME=${options.home}`,
|
|
2852
|
+
`PORTLESS_STATE_DIR=${options.stateDir}`
|
|
2853
|
+
];
|
|
2854
|
+
}
|
|
2855
|
+
function buildServiceUninstallSudoArgs(entryScript, options = {}) {
|
|
2856
|
+
const env = options.env ?? process.env;
|
|
2857
|
+
const home = options.home ?? os2.homedir();
|
|
2858
|
+
const stateDir = options.stateDir ?? env.PORTLESS_STATE_DIR ?? path8.join(home, ".portless");
|
|
2859
|
+
return [
|
|
2860
|
+
...buildElevatedEnvArgs({ home, stateDir, env }),
|
|
2861
|
+
options.nodePath ?? process.execPath,
|
|
2862
|
+
entryScript,
|
|
2863
|
+
"service",
|
|
2864
|
+
"uninstall"
|
|
2865
|
+
];
|
|
2866
|
+
}
|
|
2867
|
+
function requireUnixElevation(args, runner) {
|
|
2868
|
+
if (process.platform !== "darwin" && process.platform !== "linux") return;
|
|
2869
|
+
if ((process.getuid?.() ?? -1) === 0) return;
|
|
2870
|
+
if (process.env[INTERNAL_ELEVATED_ENV] === "1") return;
|
|
2871
|
+
const home = os2.homedir();
|
|
2872
|
+
const stateDir = process.env.PORTLESS_STATE_DIR || path8.join(home, ".portless");
|
|
2873
|
+
const result = runner(
|
|
2874
|
+
"sudo",
|
|
2875
|
+
[
|
|
2876
|
+
...buildElevatedEnvArgs({
|
|
2877
|
+
home,
|
|
2878
|
+
stateDir,
|
|
2879
|
+
extraEnv: { [INTERNAL_ELEVATED_ENV]: "1" }
|
|
2880
|
+
}),
|
|
2881
|
+
process.execPath,
|
|
2882
|
+
args[0],
|
|
2883
|
+
...args.slice(1)
|
|
2884
|
+
],
|
|
2885
|
+
{ stdio: "inherit" }
|
|
2886
|
+
);
|
|
2887
|
+
process.exit(result.status ?? 1);
|
|
2888
|
+
}
|
|
2889
|
+
function runRequired(runner, command, args) {
|
|
2890
|
+
const result = runner(command, args);
|
|
2891
|
+
if (result.status !== 0) {
|
|
2892
|
+
const detail = result.stderr || result.stdout || result.error?.message || `${command} failed`;
|
|
2893
|
+
throw new Error(detail.trim());
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
function runOptional(runner, command, args) {
|
|
2897
|
+
runner(command, args);
|
|
2898
|
+
}
|
|
2899
|
+
function isPermissionError(err) {
|
|
2900
|
+
const code = err && typeof err === "object" && "code" in err ? String(err.code) : "";
|
|
2901
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2902
|
+
return code === "EACCES" || code === "EPERM" || /permission denied|operation not permitted|access is denied/i.test(message);
|
|
2903
|
+
}
|
|
2904
|
+
function stopExistingProxy(entryScript, runner) {
|
|
2905
|
+
runRequired(runner, process.execPath, [
|
|
2906
|
+
entryScript,
|
|
2907
|
+
"proxy",
|
|
2908
|
+
"stop",
|
|
2909
|
+
"--port",
|
|
2910
|
+
SERVICE_PORT.toString()
|
|
2911
|
+
]);
|
|
2912
|
+
}
|
|
2913
|
+
function prepareTrust(stateDir) {
|
|
2914
|
+
fs8.mkdirSync(stateDir, { recursive: true });
|
|
2915
|
+
fixOwnership(stateDir);
|
|
2916
|
+
try {
|
|
2917
|
+
ensureCerts(stateDir);
|
|
2918
|
+
} catch (err) {
|
|
2919
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
2920
|
+
throw new Error(
|
|
2921
|
+
`Failed to generate certificates in ${stateDir}. Ensure OpenSSL is installed.
|
|
2922
|
+
${detail}`
|
|
2923
|
+
);
|
|
2924
|
+
}
|
|
2925
|
+
if (isCATrusted(stateDir)) return;
|
|
2926
|
+
console.log(colors_default.gray("Trusting portless CA for service startup..."));
|
|
2927
|
+
const trustResult = trustCA(stateDir);
|
|
2928
|
+
if (trustResult.trusted) {
|
|
2929
|
+
console.log(colors_default.green("CA added to the system trust store."));
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2932
|
+
console.warn(colors_default.yellow("Could not add the CA to the system trust store."));
|
|
2933
|
+
if (trustResult.error) {
|
|
2934
|
+
console.warn(colors_default.gray(trustResult.error));
|
|
2935
|
+
}
|
|
2936
|
+
console.warn(colors_default.yellow("Run `portless trust` if browsers show certificate warnings."));
|
|
2937
|
+
}
|
|
2938
|
+
async function installService(entryScript, runner) {
|
|
2939
|
+
requireUnixElevation([entryScript, "service", "install"], runner);
|
|
2940
|
+
const spec = currentServiceSpec(entryScript);
|
|
2941
|
+
prepareTrust(spec.stateDir);
|
|
2942
|
+
if (spec.platform === "darwin") {
|
|
2943
|
+
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
2944
|
+
stopExistingProxy(entryScript, runner);
|
|
2945
|
+
fs8.writeFileSync(spec.plistPath, spec.plist);
|
|
2946
|
+
fs8.chmodSync(spec.plistPath, 420);
|
|
2947
|
+
runRequired(runner, "chown", ["root:wheel", spec.plistPath]);
|
|
2948
|
+
runRequired(runner, "launchctl", ["bootstrap", "system", spec.plistPath]);
|
|
2949
|
+
runRequired(runner, "launchctl", ["enable", `system/${spec.label}`]);
|
|
2950
|
+
runRequired(runner, "launchctl", ["kickstart", "-k", `system/${spec.label}`]);
|
|
2951
|
+
} else if (spec.platform === "linux") {
|
|
2952
|
+
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
2953
|
+
stopExistingProxy(entryScript, runner);
|
|
2954
|
+
fs8.writeFileSync(spec.unitPath, spec.unit);
|
|
2955
|
+
fs8.chmodSync(spec.unitPath, 420);
|
|
2956
|
+
runRequired(runner, "systemctl", ["daemon-reload"]);
|
|
2957
|
+
runRequired(runner, "systemctl", ["enable", spec.serviceName]);
|
|
2958
|
+
runRequired(runner, "systemctl", ["restart", spec.serviceName]);
|
|
2959
|
+
} else {
|
|
2960
|
+
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
2961
|
+
stopExistingProxy(entryScript, runner);
|
|
2962
|
+
fs8.mkdirSync(spec.scriptDir, { recursive: true });
|
|
2963
|
+
fs8.writeFileSync(spec.scriptPath, spec.script);
|
|
2964
|
+
runRequired(runner, "schtasks", spec.createArgs);
|
|
2965
|
+
runOptional(runner, "schtasks", spec.runArgs);
|
|
2966
|
+
}
|
|
2967
|
+
console.log(colors_default.green("Portless service installed."));
|
|
2968
|
+
console.log(colors_default.gray(`State directory: ${spec.stateDir}`));
|
|
2969
|
+
}
|
|
2970
|
+
async function uninstallService(entryScript, runner) {
|
|
2971
|
+
requireUnixElevation([entryScript, "service", "uninstall"], runner);
|
|
2972
|
+
const spec = currentServiceSpec(entryScript);
|
|
2973
|
+
if (spec.platform === "darwin") {
|
|
2974
|
+
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
2975
|
+
fs8.rmSync(spec.plistPath, { force: true });
|
|
2976
|
+
} else if (spec.platform === "linux") {
|
|
2977
|
+
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
2978
|
+
fs8.rmSync(spec.unitPath, { force: true });
|
|
2979
|
+
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
2980
|
+
} else {
|
|
2981
|
+
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
2982
|
+
runOptional(runner, "schtasks", spec.deleteArgs);
|
|
2983
|
+
fs8.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
2984
|
+
}
|
|
2985
|
+
console.log(colors_default.green("Portless service uninstalled."));
|
|
2986
|
+
}
|
|
2987
|
+
function tryUninstallService(entryScript, runner = defaultRunner2) {
|
|
2988
|
+
let installed = false;
|
|
2989
|
+
try {
|
|
2990
|
+
const spec = currentServiceSpec(entryScript);
|
|
2991
|
+
if (spec.platform === "darwin") {
|
|
2992
|
+
installed = fs8.existsSync(spec.plistPath);
|
|
2993
|
+
if (!installed) return { removed: false, installed: false };
|
|
2994
|
+
runOptional(runner, "launchctl", ["bootout", "system", spec.plistPath]);
|
|
2995
|
+
fs8.rmSync(spec.plistPath, { force: true });
|
|
2996
|
+
} else if (spec.platform === "linux") {
|
|
2997
|
+
installed = fs8.existsSync(spec.unitPath);
|
|
2998
|
+
if (!installed) return { removed: false, installed: false };
|
|
2999
|
+
runOptional(runner, "systemctl", ["disable", "--now", spec.serviceName]);
|
|
3000
|
+
fs8.rmSync(spec.unitPath, { force: true });
|
|
3001
|
+
runOptional(runner, "systemctl", ["daemon-reload"]);
|
|
3002
|
+
} else {
|
|
3003
|
+
const query = runner("schtasks", ["/Query", "/TN", spec.taskName, "/FO", "LIST"]);
|
|
3004
|
+
installed = query.status === 0;
|
|
3005
|
+
if (!installed) return { removed: false, installed: false };
|
|
3006
|
+
runOptional(runner, "schtasks", ["/End", "/TN", spec.taskName]);
|
|
3007
|
+
runRequired(runner, "schtasks", spec.deleteArgs);
|
|
3008
|
+
fs8.rmSync(spec.scriptDir, { recursive: true, force: true });
|
|
3009
|
+
}
|
|
3010
|
+
return { removed: true, installed: true };
|
|
3011
|
+
} catch (err) {
|
|
3012
|
+
return {
|
|
3013
|
+
removed: false,
|
|
3014
|
+
installed,
|
|
3015
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3016
|
+
needsElevation: installed && isPermissionError(err)
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function getServiceStatus(entryScript, runner) {
|
|
3021
|
+
const spec = currentServiceSpec(entryScript);
|
|
3022
|
+
const proxyRunning = await isProxyRunning(SERVICE_PORT);
|
|
3023
|
+
if (spec.platform === "darwin") {
|
|
3024
|
+
const installed2 = fs8.existsSync(spec.plistPath);
|
|
3025
|
+
const result = runner("launchctl", ["print", `system/${spec.label}`]);
|
|
3026
|
+
const output2 = `${result.stdout || ""}${result.stderr || ""}`;
|
|
3027
|
+
const managerState = result.status === 0 && /state = running|pid = \d+/.test(output2) ? "running" : installed2 ? "installed" : "not installed";
|
|
3028
|
+
return { installed: installed2, managerState, proxyRunning, details: spec.plistPath };
|
|
3029
|
+
}
|
|
3030
|
+
if (spec.platform === "linux") {
|
|
3031
|
+
const enabled2 = runner("systemctl", ["is-enabled", spec.serviceName]);
|
|
3032
|
+
const active = runner("systemctl", ["is-active", spec.serviceName]);
|
|
3033
|
+
const installed2 = enabled2.status === 0 || active.status === 0 || fs8.existsSync(spec.unitPath);
|
|
3034
|
+
const activeText = (active.stdout || "").trim();
|
|
3035
|
+
return {
|
|
3036
|
+
installed: installed2,
|
|
3037
|
+
managerState: active.status === 0 ? activeText || "active" : installed2 ? "installed" : "not installed",
|
|
3038
|
+
proxyRunning,
|
|
3039
|
+
details: spec.unitPath
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
const query = runner("schtasks", spec.queryArgs);
|
|
3043
|
+
const output = `${query.stdout || ""}${query.stderr || ""}`;
|
|
3044
|
+
const installed = query.status === 0;
|
|
3045
|
+
const stateMatch = output.match(/^\s*Status:\s*(.+)$/im);
|
|
3046
|
+
return {
|
|
3047
|
+
installed,
|
|
3048
|
+
managerState: installed ? stateMatch?.[1]?.trim() || "installed" : "not installed",
|
|
3049
|
+
proxyRunning,
|
|
3050
|
+
details: spec.taskName
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
async function printServiceStatus(entryScript, runner) {
|
|
3054
|
+
const spec = currentServiceSpec(entryScript);
|
|
3055
|
+
const status = await getServiceStatus(entryScript, runner);
|
|
3056
|
+
console.log(colors_default.bold("portless service"));
|
|
3057
|
+
console.log(` Manager state: ${status.managerState}`);
|
|
3058
|
+
console.log(` Installed: ${status.installed ? "yes" : "no"}`);
|
|
3059
|
+
console.log(` Proxy on 443: ${status.proxyRunning ? "responding" : "not responding"}`);
|
|
3060
|
+
console.log(` State directory: ${spec.stateDir}`);
|
|
3061
|
+
if (status.details) {
|
|
3062
|
+
console.log(` Service entry: ${status.details}`);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
function printServiceHelp() {
|
|
3066
|
+
console.log(`
|
|
3067
|
+
${colors_default.bold("portless service")} - Start portless automatically when the OS starts.
|
|
3068
|
+
|
|
3069
|
+
${colors_default.bold("Usage:")}
|
|
3070
|
+
${colors_default.cyan("portless service install")} Install and start the HTTPS service on port 443
|
|
3071
|
+
${colors_default.cyan("portless service uninstall")} Stop and remove the startup service
|
|
3072
|
+
${colors_default.cyan("portless service status")} Show service and proxy status
|
|
3073
|
+
|
|
3074
|
+
${colors_default.bold("Notes:")}
|
|
3075
|
+
The service uses the default clean URL mode: HTTPS on port 443.
|
|
3076
|
+
macOS and Linux install a root-owned service so port 443 can bind at boot.
|
|
3077
|
+
Windows installs a Task Scheduler startup task that runs as SYSTEM.
|
|
3078
|
+
`);
|
|
3079
|
+
}
|
|
3080
|
+
async function handleService(args, options) {
|
|
3081
|
+
const action = args[1];
|
|
3082
|
+
const runner = options.runner || defaultRunner2;
|
|
3083
|
+
if (!action || action === "--help" || action === "-h") {
|
|
3084
|
+
printServiceHelp();
|
|
3085
|
+
process.exit(0);
|
|
3086
|
+
}
|
|
3087
|
+
try {
|
|
3088
|
+
if (action === "install") {
|
|
3089
|
+
await installService(options.entryScript, runner);
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
if (action === "uninstall") {
|
|
3093
|
+
await uninstallService(options.entryScript, runner);
|
|
3094
|
+
return;
|
|
3095
|
+
}
|
|
3096
|
+
if (action === "status") {
|
|
3097
|
+
await printServiceStatus(options.entryScript, runner);
|
|
3098
|
+
return;
|
|
3099
|
+
}
|
|
3100
|
+
console.error(colors_default.red(`Error: Unknown service command "${action}".`));
|
|
3101
|
+
printServiceHelp();
|
|
3102
|
+
process.exit(1);
|
|
3103
|
+
} catch (err) {
|
|
3104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3105
|
+
console.error(colors_default.red("Error:"), message);
|
|
3106
|
+
process.exit(1);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
|
|
2527
3110
|
// src/cli.ts
|
|
2528
3111
|
var chalk = colors_default;
|
|
2529
3112
|
var HOSTS_DISPLAY = isWindows ? "hosts file" : "/etc/hosts";
|
|
@@ -2676,16 +3259,16 @@ function getEntryScript() {
|
|
|
2676
3259
|
function isLocallyInstalled() {
|
|
2677
3260
|
let dir = process.cwd();
|
|
2678
3261
|
for (; ; ) {
|
|
2679
|
-
if (
|
|
3262
|
+
if (fs9.existsSync(path9.join(dir, "node_modules", "portless", "package.json"))) {
|
|
2680
3263
|
return true;
|
|
2681
3264
|
}
|
|
2682
|
-
const parent =
|
|
3265
|
+
const parent = path9.dirname(dir);
|
|
2683
3266
|
if (parent === dir) break;
|
|
2684
3267
|
dir = parent;
|
|
2685
3268
|
}
|
|
2686
3269
|
return false;
|
|
2687
3270
|
}
|
|
2688
|
-
function
|
|
3271
|
+
function collectPortlessEnvArgs2() {
|
|
2689
3272
|
const envArgs = [];
|
|
2690
3273
|
for (const key of Object.keys(process.env)) {
|
|
2691
3274
|
if (key.startsWith("PORTLESS_") && process.env[key]) {
|
|
@@ -2697,7 +3280,35 @@ function collectPortlessEnvArgs() {
|
|
|
2697
3280
|
function sudoStop(port) {
|
|
2698
3281
|
const stopArgs = [process.execPath, getEntryScript(), "proxy", "stop", "-p", String(port)];
|
|
2699
3282
|
console.log(colors_default.yellow("Proxy is running as root. Elevating with sudo to stop it..."));
|
|
2700
|
-
const result =
|
|
3283
|
+
const result = spawnSync4("sudo", ["env", ...collectPortlessEnvArgs2(), ...stopArgs], {
|
|
3284
|
+
stdio: "inherit",
|
|
3285
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
3286
|
+
});
|
|
3287
|
+
return result.status === 0;
|
|
3288
|
+
}
|
|
3289
|
+
function runCleanWithSudo(reason) {
|
|
3290
|
+
console.log(colors_default.yellow(`${reason} Requesting sudo...`));
|
|
3291
|
+
const home = process.env.HOME;
|
|
3292
|
+
const result = spawnSync4(
|
|
3293
|
+
"sudo",
|
|
3294
|
+
[
|
|
3295
|
+
"env",
|
|
3296
|
+
...collectPortlessEnvArgs2(),
|
|
3297
|
+
...home ? [`HOME=${home}`] : [],
|
|
3298
|
+
process.execPath,
|
|
3299
|
+
getEntryScript(),
|
|
3300
|
+
"clean"
|
|
3301
|
+
],
|
|
3302
|
+
{
|
|
3303
|
+
stdio: "inherit",
|
|
3304
|
+
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
3305
|
+
}
|
|
3306
|
+
);
|
|
3307
|
+
return result.status === 0;
|
|
3308
|
+
}
|
|
3309
|
+
function runServiceUninstallWithSudo(reason) {
|
|
3310
|
+
console.log(colors_default.yellow(`${reason} Requesting sudo...`));
|
|
3311
|
+
const result = spawnSync4("sudo", buildServiceUninstallSudoArgs(getEntryScript()), {
|
|
2701
3312
|
stdio: "inherit",
|
|
2702
3313
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
2703
3314
|
});
|
|
@@ -2715,11 +3326,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
2715
3326
|
console.warn(chalk.yellow(`LAN mode disabled: ${reason}`));
|
|
2716
3327
|
}
|
|
2717
3328
|
const routesPath = store.getRoutesPath();
|
|
2718
|
-
if (!
|
|
2719
|
-
|
|
3329
|
+
if (!fs9.existsSync(routesPath)) {
|
|
3330
|
+
fs9.writeFileSync(routesPath, "[]", { mode: FILE_MODE });
|
|
2720
3331
|
}
|
|
2721
3332
|
try {
|
|
2722
|
-
|
|
3333
|
+
fs9.chmodSync(routesPath, FILE_MODE);
|
|
2723
3334
|
} catch {
|
|
2724
3335
|
}
|
|
2725
3336
|
fixOwnership(routesPath);
|
|
@@ -2779,7 +3390,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
2779
3390
|
}
|
|
2780
3391
|
};
|
|
2781
3392
|
try {
|
|
2782
|
-
watcher =
|
|
3393
|
+
watcher = fs9.watch(routesPath, () => {
|
|
2783
3394
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
2784
3395
|
debounceTimer = setTimeout(reloadRoutes, DEBOUNCE_MS);
|
|
2785
3396
|
});
|
|
@@ -2829,8 +3440,8 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
2829
3440
|
redirectServer.listen(80);
|
|
2830
3441
|
}
|
|
2831
3442
|
server.listen(proxyPort, () => {
|
|
2832
|
-
|
|
2833
|
-
|
|
3443
|
+
fs9.writeFileSync(store.pidPath, process.pid.toString(), { mode: FILE_MODE });
|
|
3444
|
+
fs9.writeFileSync(store.portFilePath, proxyPort.toString(), { mode: FILE_MODE });
|
|
2834
3445
|
writeTlsMarker(store.dir, isTls);
|
|
2835
3446
|
writeTldFile(store.dir, tld);
|
|
2836
3447
|
writeLanMarker(store.dir, activeLanIp);
|
|
@@ -2846,7 +3457,7 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
2846
3457
|
console.log(chalk.gray("Services are discoverable as <name>.local on your network"));
|
|
2847
3458
|
if (isTls) {
|
|
2848
3459
|
console.log(chalk.yellow("For HTTPS on devices, install the CA certificate:"));
|
|
2849
|
-
console.log(chalk.gray(` ${
|
|
3460
|
+
console.log(chalk.gray(` ${path9.join(store.dir, "ca.pem")}`));
|
|
2850
3461
|
}
|
|
2851
3462
|
if (!lanIpPinned) {
|
|
2852
3463
|
lanMonitor = startLanIpMonitor({
|
|
@@ -2878,11 +3489,11 @@ function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
|
2878
3489
|
redirectServer.close();
|
|
2879
3490
|
}
|
|
2880
3491
|
try {
|
|
2881
|
-
|
|
3492
|
+
fs9.unlinkSync(store.pidPath);
|
|
2882
3493
|
} catch {
|
|
2883
3494
|
}
|
|
2884
3495
|
try {
|
|
2885
|
-
|
|
3496
|
+
fs9.unlinkSync(store.portFilePath);
|
|
2886
3497
|
} catch {
|
|
2887
3498
|
}
|
|
2888
3499
|
writeTlsMarker(store.dir, false);
|
|
@@ -2912,7 +3523,7 @@ function sudoStopOrHint(port) {
|
|
|
2912
3523
|
}
|
|
2913
3524
|
async function stopProxy(store, proxyPort, _tls) {
|
|
2914
3525
|
const pidPath = store.pidPath;
|
|
2915
|
-
if (!
|
|
3526
|
+
if (!fs9.existsSync(pidPath)) {
|
|
2916
3527
|
if (await isProxyRunning(proxyPort)) {
|
|
2917
3528
|
console.log(colors_default.yellow(`PID file is missing but port ${proxyPort} is still in use.`));
|
|
2918
3529
|
const pid = findPidOnPort(proxyPort);
|
|
@@ -2920,7 +3531,7 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
2920
3531
|
try {
|
|
2921
3532
|
process.kill(pid, "SIGTERM");
|
|
2922
3533
|
try {
|
|
2923
|
-
|
|
3534
|
+
fs9.unlinkSync(store.portFilePath);
|
|
2924
3535
|
} catch {
|
|
2925
3536
|
}
|
|
2926
3537
|
writeTlsMarker(store.dir, false);
|
|
@@ -2958,10 +3569,10 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
2958
3569
|
return;
|
|
2959
3570
|
}
|
|
2960
3571
|
try {
|
|
2961
|
-
const pid = parseInt(
|
|
3572
|
+
const pid = parseInt(fs9.readFileSync(pidPath, "utf-8"), 10);
|
|
2962
3573
|
if (isNaN(pid)) {
|
|
2963
3574
|
console.error(colors_default.red("Corrupted PID file. Removing it."));
|
|
2964
|
-
|
|
3575
|
+
fs9.unlinkSync(pidPath);
|
|
2965
3576
|
writeTlsMarker(store.dir, false);
|
|
2966
3577
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2967
3578
|
writeLanMarker(store.dir, null);
|
|
@@ -2975,9 +3586,9 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
2975
3586
|
return;
|
|
2976
3587
|
}
|
|
2977
3588
|
console.log(colors_default.yellow("Proxy process is no longer running. Cleaning up stale files."));
|
|
2978
|
-
|
|
3589
|
+
fs9.unlinkSync(pidPath);
|
|
2979
3590
|
try {
|
|
2980
|
-
|
|
3591
|
+
fs9.unlinkSync(store.portFilePath);
|
|
2981
3592
|
} catch {
|
|
2982
3593
|
}
|
|
2983
3594
|
writeTlsMarker(store.dir, false);
|
|
@@ -2992,16 +3603,16 @@ async function stopProxy(store, proxyPort, _tls) {
|
|
|
2992
3603
|
)
|
|
2993
3604
|
);
|
|
2994
3605
|
console.log(colors_default.yellow("Removing stale PID file."));
|
|
2995
|
-
|
|
3606
|
+
fs9.unlinkSync(pidPath);
|
|
2996
3607
|
writeTlsMarker(store.dir, false);
|
|
2997
3608
|
writeTldFile(store.dir, DEFAULT_TLD);
|
|
2998
3609
|
writeLanMarker(store.dir, null);
|
|
2999
3610
|
return;
|
|
3000
3611
|
}
|
|
3001
3612
|
process.kill(pid, "SIGTERM");
|
|
3002
|
-
|
|
3613
|
+
fs9.unlinkSync(pidPath);
|
|
3003
3614
|
try {
|
|
3004
|
-
|
|
3615
|
+
fs9.unlinkSync(store.portFilePath);
|
|
3005
3616
|
} catch {
|
|
3006
3617
|
}
|
|
3007
3618
|
writeTlsMarker(store.dir, false);
|
|
@@ -3124,7 +3735,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
3124
3735
|
proxyPort: startPort
|
|
3125
3736
|
});
|
|
3126
3737
|
const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
|
|
3127
|
-
const result =
|
|
3738
|
+
const result = spawnSync4(process.execPath, startArgs, {
|
|
3128
3739
|
stdio: "inherit",
|
|
3129
3740
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
3130
3741
|
});
|
|
@@ -3142,10 +3753,10 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
3142
3753
|
if (!discovered) {
|
|
3143
3754
|
console.error(colors_default.red("Failed to start proxy."));
|
|
3144
3755
|
const fallbackDir = resolveStateDir(effectivePort);
|
|
3145
|
-
const logPath =
|
|
3756
|
+
const logPath = path9.join(fallbackDir, "proxy.log");
|
|
3146
3757
|
console.error(colors_default.blue("Try starting it manually:"));
|
|
3147
3758
|
console.error(colors_default.cyan(` ${manualStartCommand}`));
|
|
3148
|
-
if (
|
|
3759
|
+
if (fs9.existsSync(logPath)) {
|
|
3149
3760
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
3150
3761
|
}
|
|
3151
3762
|
process.exit(1);
|
|
@@ -3163,14 +3774,17 @@ portless
|
|
|
3163
3774
|
let tsBaseUrl;
|
|
3164
3775
|
if (wantsTailscale) {
|
|
3165
3776
|
try {
|
|
3166
|
-
const tsReady = ensureTailscaleReady(
|
|
3777
|
+
const tsReady = ensureTailscaleReady({
|
|
3778
|
+
requireFunnel: wantsFunnel,
|
|
3779
|
+
requireHttps: true
|
|
3780
|
+
});
|
|
3167
3781
|
tsBaseUrl = tsReady.baseUrl;
|
|
3168
3782
|
} catch (err) {
|
|
3169
3783
|
const message = err instanceof Error ? err.message : String(err);
|
|
3170
3784
|
console.error(colors_default.red(`Error: ${message}`));
|
|
3171
3785
|
if (message.includes("not found")) {
|
|
3172
3786
|
console.error(colors_default.blue("Install Tailscale: https://tailscale.com/download"));
|
|
3173
|
-
} else {
|
|
3787
|
+
} else if (!message.includes("not enabled on your tailnet")) {
|
|
3174
3788
|
console.error(colors_default.blue("Make sure Tailscale is connected:"));
|
|
3175
3789
|
console.error(colors_default.cyan(" tailscale up"));
|
|
3176
3790
|
}
|
|
@@ -3299,7 +3913,7 @@ portless
|
|
|
3299
3913
|
} catch {
|
|
3300
3914
|
}
|
|
3301
3915
|
}
|
|
3302
|
-
const basename5 =
|
|
3916
|
+
const basename5 = path9.basename(commandArgs[0]);
|
|
3303
3917
|
const isExpo = basename5 === "expo";
|
|
3304
3918
|
const isExpoLan = isExpo && (lanMode || isLanEnvEnabled());
|
|
3305
3919
|
const hostBind = isExpoLan ? void 0 : "127.0.0.1";
|
|
@@ -3309,8 +3923,8 @@ portless
|
|
|
3309
3923
|
injectFrameworkFlags(commandArgs, port);
|
|
3310
3924
|
const caEnv = {};
|
|
3311
3925
|
if (tls2 && !process.env.NODE_EXTRA_CA_CERTS) {
|
|
3312
|
-
const caPath =
|
|
3313
|
-
if (
|
|
3926
|
+
const caPath = path9.join(stateDir, "ca.pem");
|
|
3927
|
+
if (fs9.existsSync(caPath)) {
|
|
3314
3928
|
caEnv.NODE_EXTRA_CA_CERTS = caPath;
|
|
3315
3929
|
}
|
|
3316
3930
|
}
|
|
@@ -3515,6 +4129,7 @@ ${colors_default.bold("Usage:")}
|
|
|
3515
4129
|
${colors_default.cyan("portless <name> <cmd>")} Run with an explicit app name
|
|
3516
4130
|
${colors_default.cyan("portless proxy start")} Start the proxy (HTTPS on port 443, daemon)
|
|
3517
4131
|
${colors_default.cyan("portless proxy stop")} Stop the proxy
|
|
4132
|
+
${colors_default.cyan("portless service install")} Start proxy automatically when the OS starts
|
|
3518
4133
|
${colors_default.cyan("portless get <name>")} Print URL for a service (for cross-service refs)
|
|
3519
4134
|
${colors_default.cyan("portless alias <name> <port>")} Register a static route (e.g. for Docker)
|
|
3520
4135
|
${colors_default.cyan("portless alias --remove <name>")} Remove a static route
|
|
@@ -3532,6 +4147,7 @@ ${colors_default.bold("Examples:")}
|
|
|
3532
4147
|
portless myapp next dev # -> https://myapp.localhost
|
|
3533
4148
|
portless run next dev # -> https://<project>.localhost
|
|
3534
4149
|
portless run next dev # in worktree -> https://<worktree>.<project>.localhost
|
|
4150
|
+
portless service install # Start HTTPS proxy on OS startup
|
|
3535
4151
|
portless get backend # -> https://backend.localhost
|
|
3536
4152
|
portless myapp --tailscale next dev # -> also https://<node>.ts.net (tailnet)
|
|
3537
4153
|
portless myapp --funnel next dev # -> also https://<node>.ts.net (public)
|
|
@@ -3590,7 +4206,9 @@ ${colors_default.bold("Tailscale sharing:")}
|
|
|
3590
4206
|
Each app is root-mounted on its own Tailscale HTTPS port (443, then 8443,
|
|
3591
4207
|
8444, etc.) so no basePath configuration is needed.
|
|
3592
4208
|
Use --funnel to expose your dev server to the public internet via
|
|
3593
|
-
Tailscale Funnel. Requires Tailscale CLI to be installed and connected
|
|
4209
|
+
Tailscale Funnel. Requires Tailscale CLI to be installed and connected,
|
|
4210
|
+
with Tailscale HTTPS certificates enabled. Funnel must also be enabled
|
|
4211
|
+
on your tailnet.
|
|
3594
4212
|
${colors_default.cyan("portless myapp --tailscale next dev")}
|
|
3595
4213
|
${colors_default.cyan("portless myapp --funnel next dev")}
|
|
3596
4214
|
|
|
@@ -3650,20 +4268,20 @@ ${colors_default.bold("Skip portless:")}
|
|
|
3650
4268
|
PORTLESS=0 pnpm dev # Runs command directly without proxy
|
|
3651
4269
|
|
|
3652
4270
|
${colors_default.bold("Reserved names:")}
|
|
3653
|
-
run, get, alias, hosts, list, trust, clean, prune, proxy are subcommands and
|
|
4271
|
+
run, get, alias, hosts, list, trust, clean, prune, proxy, service are subcommands and
|
|
3654
4272
|
cannot be used as app names directly. Use "portless run" to infer the name,
|
|
3655
4273
|
or "portless --name <name>" to force any name including reserved ones.
|
|
3656
4274
|
`);
|
|
3657
4275
|
process.exit(0);
|
|
3658
4276
|
}
|
|
3659
4277
|
function printVersion() {
|
|
3660
|
-
console.log("0.
|
|
4278
|
+
console.log("0.13.0");
|
|
3661
4279
|
process.exit(0);
|
|
3662
4280
|
}
|
|
3663
4281
|
async function handleTrust() {
|
|
3664
4282
|
const { dir } = await discoverState();
|
|
3665
|
-
if (!
|
|
3666
|
-
|
|
4283
|
+
if (!fs9.existsSync(dir)) {
|
|
4284
|
+
fs9.mkdirSync(dir, { recursive: true });
|
|
3667
4285
|
}
|
|
3668
4286
|
const { caGenerated } = ensureCerts(dir);
|
|
3669
4287
|
if (caGenerated) {
|
|
@@ -3675,14 +4293,14 @@ async function handleTrust() {
|
|
|
3675
4293
|
console.log(colors_default.gray("Browsers will now trust portless HTTPS certificates."));
|
|
3676
4294
|
return;
|
|
3677
4295
|
}
|
|
3678
|
-
const
|
|
3679
|
-
if (
|
|
4296
|
+
const isPermissionError2 = result.error?.includes("Permission denied") || result.error?.includes("EACCES");
|
|
4297
|
+
if (isPermissionError2 && !isWindows && process.getuid?.() !== 0) {
|
|
3680
4298
|
console.log(colors_default.yellow("Trusting the CA requires elevated privileges. Requesting sudo..."));
|
|
3681
|
-
const sudoResult =
|
|
4299
|
+
const sudoResult = spawnSync4(
|
|
3682
4300
|
"sudo",
|
|
3683
4301
|
[
|
|
3684
4302
|
"env",
|
|
3685
|
-
...
|
|
4303
|
+
...collectPortlessEnvArgs2(),
|
|
3686
4304
|
`PORTLESS_STATE_DIR=${dir}`,
|
|
3687
4305
|
process.execPath,
|
|
3688
4306
|
getEntryScript(),
|
|
@@ -3704,10 +4322,11 @@ async function handleClean(args) {
|
|
|
3704
4322
|
console.log(`
|
|
3705
4323
|
${colors_default.bold("portless clean")} - Remove portless artifacts from this machine.
|
|
3706
4324
|
|
|
3707
|
-
Stops the proxy if it is running,
|
|
3708
|
-
when it was installed by portless,
|
|
3709
|
-
(~/.portless, the system state
|
|
3710
|
-
and removes the portless block
|
|
4325
|
+
Stops the proxy if it is running, uninstalls the startup service if installed,
|
|
4326
|
+
removes the local CA from the OS trust store when it was installed by portless,
|
|
4327
|
+
deletes known files under state directories (~/.portless, the system state
|
|
4328
|
+
directory, and PORTLESS_STATE_DIR when set), and removes the portless block
|
|
4329
|
+
from ${HOSTS_DISPLAY}.
|
|
3711
4330
|
|
|
3712
4331
|
Only allowlisted filenames under each state directory are deleted. Custom
|
|
3713
4332
|
certificate paths from --cert and --key are never removed.
|
|
@@ -3728,6 +4347,23 @@ ${colors_default.bold("Options:")}
|
|
|
3728
4347
|
console.error(colors_default.cyan(" portless clean --help"));
|
|
3729
4348
|
process.exit(1);
|
|
3730
4349
|
}
|
|
4350
|
+
const serviceResult = tryUninstallService(getEntryScript());
|
|
4351
|
+
if (serviceResult.removed) {
|
|
4352
|
+
console.log(colors_default.green("Removed startup service."));
|
|
4353
|
+
} else if (serviceResult.needsElevation && !isWindows && (process.getuid?.() ?? -1) !== 0) {
|
|
4354
|
+
if (!runServiceUninstallWithSudo("Removing the startup service requires elevated privileges.")) {
|
|
4355
|
+
console.error(colors_default.red("Failed to remove startup service with sudo."));
|
|
4356
|
+
process.exit(1);
|
|
4357
|
+
}
|
|
4358
|
+
} else if (serviceResult.error) {
|
|
4359
|
+
const adminHint = isWindows ? " Run as Administrator and try again." : "";
|
|
4360
|
+
const message = `Could not remove startup service: ${serviceResult.error}${adminHint}`;
|
|
4361
|
+
if (serviceResult.installed) {
|
|
4362
|
+
console.error(colors_default.red(message));
|
|
4363
|
+
process.exit(1);
|
|
4364
|
+
}
|
|
4365
|
+
console.warn(colors_default.yellow(message));
|
|
4366
|
+
}
|
|
3731
4367
|
console.log(colors_default.cyan("Stopping proxy if it is running..."));
|
|
3732
4368
|
const { dir, port, tls: tls2 } = await discoverState();
|
|
3733
4369
|
const store = new RouteStore(dir, {
|
|
@@ -3746,8 +4382,8 @@ ${colors_default.bold("Options:")}
|
|
|
3746
4382
|
}
|
|
3747
4383
|
const stateDirs = collectStateDirsForCleanup();
|
|
3748
4384
|
for (const stateDir of stateDirs) {
|
|
3749
|
-
const caPath =
|
|
3750
|
-
if (!
|
|
4385
|
+
const caPath = path9.join(stateDir, "ca.pem");
|
|
4386
|
+
if (!fs9.existsSync(caPath)) continue;
|
|
3751
4387
|
const wasTrusted = isCATrusted(stateDir);
|
|
3752
4388
|
if (!wasTrusted) continue;
|
|
3753
4389
|
const untrustResult = untrustCA(stateDir);
|
|
@@ -3769,18 +4405,7 @@ Try: sudo portless clean (Linux), or delete the certificate manually.`
|
|
|
3769
4405
|
if (cleanHostsFile()) {
|
|
3770
4406
|
console.log(colors_default.green(`Removed portless entries from ${HOSTS_DISPLAY}.`));
|
|
3771
4407
|
} else if (!isWindows && process.getuid?.() !== 0) {
|
|
3772
|
-
|
|
3773
|
-
colors_default.yellow(`Updating ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
3774
|
-
);
|
|
3775
|
-
const result = spawnSync3(
|
|
3776
|
-
"sudo",
|
|
3777
|
-
["env", ...collectPortlessEnvArgs(), process.execPath, getEntryScript(), "clean"],
|
|
3778
|
-
{
|
|
3779
|
-
stdio: "inherit",
|
|
3780
|
-
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
3781
|
-
}
|
|
3782
|
-
);
|
|
3783
|
-
if (result.status !== 0) {
|
|
4408
|
+
if (!runCleanWithSudo(`Updating ${HOSTS_DISPLAY} requires elevated privileges.`)) {
|
|
3784
4409
|
console.error(colors_default.red(`Failed to update ${HOSTS_DISPLAY}. Run: sudo portless clean`));
|
|
3785
4410
|
process.exit(1);
|
|
3786
4411
|
}
|
|
@@ -4011,9 +4636,9 @@ ${colors_default.bold("Auto-sync:")}
|
|
|
4011
4636
|
`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`
|
|
4012
4637
|
)
|
|
4013
4638
|
);
|
|
4014
|
-
const result =
|
|
4639
|
+
const result = spawnSync4(
|
|
4015
4640
|
"sudo",
|
|
4016
|
-
["env", ...
|
|
4641
|
+
["env", ...collectPortlessEnvArgs2(), process.execPath, getEntryScript(), "hosts", "clean"],
|
|
4017
4642
|
{
|
|
4018
4643
|
stdio: "inherit",
|
|
4019
4644
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
@@ -4064,9 +4689,9 @@ ${colors_default.bold("Usage: portless hosts <command>")}
|
|
|
4064
4689
|
console.log(
|
|
4065
4690
|
colors_default.yellow(`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
4066
4691
|
);
|
|
4067
|
-
const result =
|
|
4692
|
+
const result = spawnSync4(
|
|
4068
4693
|
"sudo",
|
|
4069
|
-
["env", ...
|
|
4694
|
+
["env", ...collectPortlessEnvArgs2(), process.execPath, getEntryScript(), "hosts", "sync"],
|
|
4070
4695
|
{
|
|
4071
4696
|
stdio: "inherit",
|
|
4072
4697
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
@@ -4355,7 +4980,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
4355
4980
|
if (!hasExplicitPort) {
|
|
4356
4981
|
console.log(colors_default.gray(`(To skip sudo, use an unprivileged port: ${fallbackCommand})`));
|
|
4357
4982
|
}
|
|
4358
|
-
const result =
|
|
4983
|
+
const result = spawnSync4("sudo", ["env", ...collectPortlessEnvArgs2(), ...startArgs], {
|
|
4359
4984
|
stdio: "inherit",
|
|
4360
4985
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
4361
4986
|
});
|
|
@@ -4365,8 +4990,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
4365
4990
|
console.log(colors_default.green(`Proxy started on port ${proxyPort}.`));
|
|
4366
4991
|
} else {
|
|
4367
4992
|
console.error(colors_default.red("Proxy process started but is not responding."));
|
|
4368
|
-
const logPath2 =
|
|
4369
|
-
if (
|
|
4993
|
+
const logPath2 = path9.join(resolveStateDir(proxyPort), "proxy.log");
|
|
4994
|
+
if (fs9.existsSync(logPath2)) {
|
|
4370
4995
|
console.error(colors_default.gray(`Logs: ${logPath2}`));
|
|
4371
4996
|
}
|
|
4372
4997
|
}
|
|
@@ -4405,8 +5030,8 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
4405
5030
|
store.ensureDir();
|
|
4406
5031
|
if (customCertPath && customKeyPath) {
|
|
4407
5032
|
try {
|
|
4408
|
-
const cert =
|
|
4409
|
-
const key =
|
|
5033
|
+
const cert = fs9.readFileSync(customCertPath);
|
|
5034
|
+
const key = fs9.readFileSync(customKeyPath);
|
|
4410
5035
|
const certStr = cert.toString("utf-8");
|
|
4411
5036
|
const keyStr = key.toString("utf-8");
|
|
4412
5037
|
if (!certStr.includes("-----BEGIN CERTIFICATE-----")) {
|
|
@@ -4451,9 +5076,9 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
4451
5076
|
console.warn(colors_default.cyan(" portless trust"));
|
|
4452
5077
|
}
|
|
4453
5078
|
}
|
|
4454
|
-
const cert =
|
|
4455
|
-
const key =
|
|
4456
|
-
const ca =
|
|
5079
|
+
const cert = fs9.readFileSync(certs.certPath);
|
|
5080
|
+
const key = fs9.readFileSync(certs.keyPath);
|
|
5081
|
+
const ca = fs9.readFileSync(certs.caPath);
|
|
4457
5082
|
tlsOptions = {
|
|
4458
5083
|
cert,
|
|
4459
5084
|
key,
|
|
@@ -4468,11 +5093,11 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
4468
5093
|
return;
|
|
4469
5094
|
}
|
|
4470
5095
|
store.ensureDir();
|
|
4471
|
-
const logPath =
|
|
4472
|
-
const logFd =
|
|
5096
|
+
const logPath = path9.join(stateDir, "proxy.log");
|
|
5097
|
+
const logFd = fs9.openSync(logPath, "a");
|
|
4473
5098
|
try {
|
|
4474
5099
|
try {
|
|
4475
|
-
|
|
5100
|
+
fs9.chmodSync(logPath, FILE_MODE);
|
|
4476
5101
|
} catch {
|
|
4477
5102
|
}
|
|
4478
5103
|
fixOwnership(logPath);
|
|
@@ -4503,13 +5128,13 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
4503
5128
|
});
|
|
4504
5129
|
child.unref();
|
|
4505
5130
|
} finally {
|
|
4506
|
-
|
|
5131
|
+
fs9.closeSync(logFd);
|
|
4507
5132
|
}
|
|
4508
5133
|
if (!await waitForProxy(proxyPort, void 0, void 0, useHttps)) {
|
|
4509
5134
|
console.error(colors_default.red("Proxy failed to start (timed out waiting for it to listen)."));
|
|
4510
5135
|
console.error(colors_default.blue("Try starting the proxy in the foreground to see the error:"));
|
|
4511
5136
|
console.error(colors_default.cyan(" portless proxy start --foreground"));
|
|
4512
|
-
if (
|
|
5137
|
+
if (fs9.existsSync(logPath)) {
|
|
4513
5138
|
console.error(colors_default.gray(`Logs: ${logPath}`));
|
|
4514
5139
|
}
|
|
4515
5140
|
process.exit(1);
|
|
@@ -4661,8 +5286,8 @@ async function spawnProxiedApp(app, stateDir, proxyPort, tls2, tld, exitCodes) {
|
|
|
4661
5286
|
PORTLESS_URL: url
|
|
4662
5287
|
};
|
|
4663
5288
|
if (tls2) {
|
|
4664
|
-
const caPath =
|
|
4665
|
-
if (
|
|
5289
|
+
const caPath = path9.join(stateDir, "ca.pem");
|
|
5290
|
+
if (fs9.existsSync(caPath)) {
|
|
4666
5291
|
env.NODE_EXTRA_CA_CERTS = caPath;
|
|
4667
5292
|
}
|
|
4668
5293
|
}
|
|
@@ -4744,7 +5369,7 @@ async function handleDefaultMulti(wsRoot, globalScript, extraArgs = []) {
|
|
|
4744
5369
|
}
|
|
4745
5370
|
const apps = [];
|
|
4746
5371
|
for (const pkg of packages) {
|
|
4747
|
-
const rel =
|
|
5372
|
+
const rel = path9.relative(wsRoot, pkg.dir).replace(/\\/g, "/");
|
|
4748
5373
|
const rootOverride = loaded ? resolveAppConfig(loaded.config, loaded.configDir, pkg.dir) : null;
|
|
4749
5374
|
let pkgConfig;
|
|
4750
5375
|
try {
|
|
@@ -4852,8 +5477,8 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
|
|
|
4852
5477
|
PORTLESS_URL: url
|
|
4853
5478
|
};
|
|
4854
5479
|
if (tls2) {
|
|
4855
|
-
const caPath =
|
|
4856
|
-
if (
|
|
5480
|
+
const caPath = path9.join(stateDir, "ca.pem");
|
|
5481
|
+
if (fs9.existsSync(caPath)) {
|
|
4857
5482
|
entry.NODE_EXTRA_CA_CERTS = caPath;
|
|
4858
5483
|
}
|
|
4859
5484
|
}
|
|
@@ -5166,7 +5791,7 @@ async function main() {
|
|
|
5166
5791
|
args.shift();
|
|
5167
5792
|
}
|
|
5168
5793
|
const skipPortless = process.env.PORTLESS === "0" || process.env.PORTLESS === "false" || process.env.PORTLESS === "skip";
|
|
5169
|
-
if (skipPortless && (isRunCommand || args.length === 0 || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean")) {
|
|
5794
|
+
if (skipPortless && (isRunCommand || args.length === 0 || args.length >= 2 && args[0] !== "proxy" && args[0] !== "clean" && args[0] !== "service")) {
|
|
5170
5795
|
const parsed = isRunCommand ? parseRunArgs(args) : parseAppArgs(args);
|
|
5171
5796
|
let commandArgs = parsed.commandArgs;
|
|
5172
5797
|
if (commandArgs.length === 0 && (isRunCommand || args.length === 0)) {
|
|
@@ -5230,6 +5855,10 @@ async function main() {
|
|
|
5230
5855
|
await handleProxy(args);
|
|
5231
5856
|
return;
|
|
5232
5857
|
}
|
|
5858
|
+
if (args[0] === "service") {
|
|
5859
|
+
await handleService(args, { entryScript: getEntryScript() });
|
|
5860
|
+
return;
|
|
5861
|
+
}
|
|
5233
5862
|
}
|
|
5234
5863
|
if (isRunCommand) {
|
|
5235
5864
|
await handleRunMode(args, globalScript);
|