portless 0.13.1 → 0.14.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 +18 -0
- package/dist/{chunk-3WLVQXFE.js → chunk-OZM4AEYL.js} +22 -4
- package/dist/cli.js +341 -34
- package/dist/index.d.ts +10 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -323,6 +323,20 @@ Set `PORTLESS_TAILSCALE=1` in your shell profile or `.env` to share every app by
|
|
|
323
323
|
|
|
324
324
|
Requires the Tailscale CLI to be installed and connected (`tailscale up`), with Tailscale HTTPS certificates enabled.
|
|
325
325
|
|
|
326
|
+
## ngrok sharing
|
|
327
|
+
|
|
328
|
+
Expose your dev server to the public internet with [ngrok](https://ngrok.com):
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
portless myapp --ngrok next dev
|
|
332
|
+
# -> https://myapp.localhost (local)
|
|
333
|
+
# -> https://abc123.ngrok.app (public)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Set `PORTLESS_NGROK=1` in your shell profile or `.env` to enable ngrok by default when portless runs an app. `portless list` shows both local and ngrok URLs. The ngrok tunnel is cleaned up automatically when the app exits.
|
|
337
|
+
|
|
338
|
+
Requires the ngrok CLI to be installed and authenticated. If ngrok reports an authentication error, run `ngrok config add-authtoken <token>` and try again.
|
|
339
|
+
|
|
326
340
|
## Commands
|
|
327
341
|
|
|
328
342
|
```bash
|
|
@@ -378,6 +392,7 @@ portless service uninstall # Remove the startup service
|
|
|
378
392
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
379
393
|
--tailscale Share the app on your Tailscale network (tailnet)
|
|
380
394
|
--funnel Share the app publicly via Tailscale Funnel
|
|
395
|
+
--ngrok Share the app publicly via ngrok
|
|
381
396
|
--force Kill the existing process and take over its route
|
|
382
397
|
--name <name> Use <name> as the app name
|
|
383
398
|
```
|
|
@@ -396,6 +411,7 @@ PORTLESS_WILDCARD=1 Allow unregistered subdomains to fall back to p
|
|
|
396
411
|
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of /etc/hosts (on by default)
|
|
397
412
|
PORTLESS_TAILSCALE=1 Share apps on your Tailscale network (same as --tailscale)
|
|
398
413
|
PORTLESS_FUNNEL=1 Share apps publicly via Tailscale Funnel (same as --funnel)
|
|
414
|
+
PORTLESS_NGROK=1 Share apps publicly via ngrok (same as --ngrok)
|
|
399
415
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
400
416
|
|
|
401
417
|
# Injected into child processes
|
|
@@ -403,6 +419,7 @@ PORT Ephemeral port the child should listen on
|
|
|
403
419
|
HOST Usually 127.0.0.1 (omitted for Expo in LAN mode)
|
|
404
420
|
PORTLESS_URL Public URL (e.g. https://myapp.localhost)
|
|
405
421
|
PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is active)
|
|
422
|
+
PORTLESS_NGROK_URL ngrok URL of the app (when --ngrok is active)
|
|
406
423
|
NODE_EXTRA_CA_CERTS Path to the portless CA (when HTTPS is active)
|
|
407
424
|
```
|
|
408
425
|
|
|
@@ -486,3 +503,4 @@ pnpm format # Format all files with Prettier
|
|
|
486
503
|
- Node.js 24+
|
|
487
504
|
- macOS, Linux, or Windows
|
|
488
505
|
- Tailscale CLI (optional, for `--tailscale` and `--funnel`)
|
|
506
|
+
- ngrok CLI (optional, for `--ngrok`)
|
|
@@ -920,10 +920,28 @@ var RouteStore = class _RouteStore {
|
|
|
920
920
|
const routes = this.loadRoutes(true);
|
|
921
921
|
const route = routes.find((r) => r.hostname === hostname);
|
|
922
922
|
if (!route) return;
|
|
923
|
-
if (
|
|
924
|
-
|
|
925
|
-
route.
|
|
926
|
-
|
|
923
|
+
if ("tailscaleUrl" in fields) {
|
|
924
|
+
if (fields.tailscaleUrl === null) delete route.tailscaleUrl;
|
|
925
|
+
else if (fields.tailscaleUrl !== void 0) route.tailscaleUrl = fields.tailscaleUrl;
|
|
926
|
+
}
|
|
927
|
+
if ("tailscaleHttpsPort" in fields) {
|
|
928
|
+
if (fields.tailscaleHttpsPort === null) delete route.tailscaleHttpsPort;
|
|
929
|
+
else if (fields.tailscaleHttpsPort !== void 0)
|
|
930
|
+
route.tailscaleHttpsPort = fields.tailscaleHttpsPort;
|
|
931
|
+
}
|
|
932
|
+
if ("tailscaleFunnel" in fields) {
|
|
933
|
+
if (fields.tailscaleFunnel === null) delete route.tailscaleFunnel;
|
|
934
|
+
else if (fields.tailscaleFunnel !== void 0)
|
|
935
|
+
route.tailscaleFunnel = fields.tailscaleFunnel;
|
|
936
|
+
}
|
|
937
|
+
if ("ngrokUrl" in fields) {
|
|
938
|
+
if (fields.ngrokUrl === null) delete route.ngrokUrl;
|
|
939
|
+
else if (fields.ngrokUrl !== void 0) route.ngrokUrl = fields.ngrokUrl;
|
|
940
|
+
}
|
|
941
|
+
if ("ngrokPid" in fields) {
|
|
942
|
+
if (fields.ngrokPid === null) delete route.ngrokPid;
|
|
943
|
+
else if (fields.ngrokPid !== void 0) route.ngrokPid = fields.ngrokPid;
|
|
944
|
+
}
|
|
927
945
|
this.saveRoutes(routes);
|
|
928
946
|
} finally {
|
|
929
947
|
this.releaseLock();
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
parseHostname,
|
|
14
14
|
shouldAutoSyncHosts,
|
|
15
15
|
syncHostsFile
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-OZM4AEYL.js";
|
|
17
17
|
|
|
18
18
|
// src/colors.ts
|
|
19
19
|
function supportsColor() {
|
|
@@ -41,7 +41,7 @@ var colors_default = { bold, dim, red, green, yellow, blue, cyan, white, gray };
|
|
|
41
41
|
// src/cli.ts
|
|
42
42
|
import * as fs9 from "fs";
|
|
43
43
|
import * as path9 from "path";
|
|
44
|
-
import { spawn as
|
|
44
|
+
import { spawn as spawn4, spawnSync as spawnSync5 } from "child_process";
|
|
45
45
|
import { StringDecoder } from "string_decoder";
|
|
46
46
|
|
|
47
47
|
// src/certs.ts
|
|
@@ -999,6 +999,178 @@ function formatTailscaleUrl(baseUrl, httpsPort) {
|
|
|
999
999
|
return `${trimmed}:${httpsPort}`;
|
|
1000
1000
|
}
|
|
1001
1001
|
|
|
1002
|
+
// src/ngrok.ts
|
|
1003
|
+
import { spawn, spawnSync as spawnSync2 } from "child_process";
|
|
1004
|
+
var NGROK_BINARY = "ngrok";
|
|
1005
|
+
var NGROK_START_TIMEOUT_MS = 3e4;
|
|
1006
|
+
var NGROK_COMMAND_TIMEOUT_MS = 1e4;
|
|
1007
|
+
var OUTPUT_BUFFER_LIMIT = 16384;
|
|
1008
|
+
function defaultSpawner(args) {
|
|
1009
|
+
return spawn(NGROK_BINARY, args, {
|
|
1010
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1011
|
+
windowsHide: true
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
function defaultRunner2(args) {
|
|
1015
|
+
const result = spawnSync2(NGROK_BINARY, args, {
|
|
1016
|
+
encoding: "utf-8",
|
|
1017
|
+
killSignal: "SIGKILL",
|
|
1018
|
+
timeout: NGROK_COMMAND_TIMEOUT_MS
|
|
1019
|
+
});
|
|
1020
|
+
return {
|
|
1021
|
+
status: result.status,
|
|
1022
|
+
stdout: result.stdout ?? "",
|
|
1023
|
+
stderr: result.stderr ?? "",
|
|
1024
|
+
...result.error ? { error: result.error } : {}
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
function normalizeSpace2(value) {
|
|
1028
|
+
return value.trim().replace(/\s+/g, " ");
|
|
1029
|
+
}
|
|
1030
|
+
function formatSpawnError(error) {
|
|
1031
|
+
const errno = error;
|
|
1032
|
+
if (errno.code === "ENOENT") {
|
|
1033
|
+
return new Error(
|
|
1034
|
+
"ngrok CLI not found. Install ngrok (https://ngrok.com/download) and ensure `ngrok` is on PATH."
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
return new Error(`Failed to start ngrok: ${error.message}`);
|
|
1038
|
+
}
|
|
1039
|
+
function formatOutputError(output) {
|
|
1040
|
+
const details = normalizeSpace2(output);
|
|
1041
|
+
const lower = details.toLowerCase();
|
|
1042
|
+
if (lower.includes("authtoken") || lower.includes("authentication") || lower.includes("not logged in")) {
|
|
1043
|
+
return new Error(
|
|
1044
|
+
"ngrok could not start because authentication is not configured. Run `ngrok config add-authtoken <token>`, then run portless again."
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
return new Error(
|
|
1048
|
+
`Failed to start ngrok tunnel: ${details || "ngrok exited before printing a public URL"}`
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
function ensureNgrokAvailable(runner = defaultRunner2) {
|
|
1052
|
+
const result = runner(["version"]);
|
|
1053
|
+
if (result.error) {
|
|
1054
|
+
throw formatSpawnError(result.error);
|
|
1055
|
+
}
|
|
1056
|
+
if (result.status !== 0) {
|
|
1057
|
+
const details = normalizeSpace2(result.stderr || result.stdout);
|
|
1058
|
+
throw new Error(`Failed to check ngrok version: ${details || "unknown ngrok error"}`);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function cleanUrl(value) {
|
|
1062
|
+
return value.replace(/[),.]+$/g, "");
|
|
1063
|
+
}
|
|
1064
|
+
function extractNgrokUrl(output) {
|
|
1065
|
+
const urlMatches = output.matchAll(/https:\/\/[^\s"'<>]+/g);
|
|
1066
|
+
for (const match of urlMatches) {
|
|
1067
|
+
const raw = match[0];
|
|
1068
|
+
const matchIndex = match.index ?? 0;
|
|
1069
|
+
const before = output.slice(Math.max(0, matchIndex - 80), matchIndex).toLowerCase();
|
|
1070
|
+
const looksLikeTunnel = before.includes("forwarding") || before.includes("url=") || before.includes('"url"') || before.includes("started tunnel");
|
|
1071
|
+
if (!looksLikeTunnel) continue;
|
|
1072
|
+
const candidate = cleanUrl(raw);
|
|
1073
|
+
try {
|
|
1074
|
+
const parsed = new URL(candidate);
|
|
1075
|
+
if (parsed.hostname === "ngrok.com" || parsed.hostname.endsWith(".ngrok.com")) {
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
return parsed.toString().replace(/\/$/, "");
|
|
1079
|
+
} catch {
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
return null;
|
|
1084
|
+
}
|
|
1085
|
+
function buildNgrokArgs(localPort, hostHeader = "rewrite") {
|
|
1086
|
+
return [
|
|
1087
|
+
"http",
|
|
1088
|
+
"--log=stdout",
|
|
1089
|
+
"--log-format=logfmt",
|
|
1090
|
+
`--host-header=${hostHeader}`,
|
|
1091
|
+
`http://127.0.0.1:${localPort}`
|
|
1092
|
+
];
|
|
1093
|
+
}
|
|
1094
|
+
function startNgrok(localPort, options = {}) {
|
|
1095
|
+
const spawner = options.spawner ?? defaultSpawner;
|
|
1096
|
+
const timeoutMs = options.timeoutMs ?? NGROK_START_TIMEOUT_MS;
|
|
1097
|
+
const args = buildNgrokArgs(localPort, options.hostHeader);
|
|
1098
|
+
let child;
|
|
1099
|
+
try {
|
|
1100
|
+
child = spawner(args);
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
return Promise.reject(formatSpawnError(err instanceof Error ? err : new Error(String(err))));
|
|
1103
|
+
}
|
|
1104
|
+
return new Promise((resolve4, reject) => {
|
|
1105
|
+
let settled = false;
|
|
1106
|
+
let started = false;
|
|
1107
|
+
let output = "";
|
|
1108
|
+
const settle = (fn) => {
|
|
1109
|
+
if (settled) return;
|
|
1110
|
+
settled = true;
|
|
1111
|
+
clearTimeout(timer);
|
|
1112
|
+
fn();
|
|
1113
|
+
};
|
|
1114
|
+
const appendOutput = (chunk) => {
|
|
1115
|
+
if (settled) return;
|
|
1116
|
+
output += chunk.toString();
|
|
1117
|
+
if (output.length > OUTPUT_BUFFER_LIMIT) {
|
|
1118
|
+
output = output.slice(-OUTPUT_BUFFER_LIMIT);
|
|
1119
|
+
}
|
|
1120
|
+
const url = extractNgrokUrl(output);
|
|
1121
|
+
if (url) {
|
|
1122
|
+
settle(() => {
|
|
1123
|
+
started = true;
|
|
1124
|
+
resolve4({ url, pid: child.pid, child });
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
};
|
|
1128
|
+
const timer = setTimeout(() => {
|
|
1129
|
+
try {
|
|
1130
|
+
child.kill("SIGTERM");
|
|
1131
|
+
} catch {
|
|
1132
|
+
}
|
|
1133
|
+
settle(
|
|
1134
|
+
() => reject(
|
|
1135
|
+
new Error(
|
|
1136
|
+
"Timed out waiting for ngrok to print a public URL. Check that ngrok is authenticated and can connect."
|
|
1137
|
+
)
|
|
1138
|
+
)
|
|
1139
|
+
);
|
|
1140
|
+
}, timeoutMs);
|
|
1141
|
+
child.stdout?.on("data", appendOutput);
|
|
1142
|
+
child.stderr?.on("data", appendOutput);
|
|
1143
|
+
child.on("error", (err) => {
|
|
1144
|
+
settle(() => reject(formatSpawnError(err)));
|
|
1145
|
+
});
|
|
1146
|
+
child.on("exit", (code, signal) => {
|
|
1147
|
+
if (settled) {
|
|
1148
|
+
if (started) options.onExit?.(code, signal);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
settle(() => {
|
|
1152
|
+
const suffix = signal ? ` (signal ${signal})` : code !== null ? ` (exit ${code})` : "";
|
|
1153
|
+
const error = formatOutputError(output);
|
|
1154
|
+
reject(new Error(`${error.message}${suffix}`));
|
|
1155
|
+
});
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
function stopNgrokProcess(child) {
|
|
1160
|
+
if (!child) return;
|
|
1161
|
+
try {
|
|
1162
|
+
child.kill("SIGTERM");
|
|
1163
|
+
} catch {
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function stopNgrok(route) {
|
|
1167
|
+
if (!route.ngrokPid) return;
|
|
1168
|
+
try {
|
|
1169
|
+
process.kill(route.ngrokPid, "SIGTERM");
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1002
1174
|
// src/auto.ts
|
|
1003
1175
|
import { createHash as createHash2 } from "crypto";
|
|
1004
1176
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
@@ -1181,7 +1353,7 @@ import * as net from "net";
|
|
|
1181
1353
|
import * as os from "os";
|
|
1182
1354
|
import * as path3 from "path";
|
|
1183
1355
|
import * as readline from "readline";
|
|
1184
|
-
import { execSync, spawn } from "child_process";
|
|
1356
|
+
import { execSync, spawn as spawn2 } from "child_process";
|
|
1185
1357
|
var isWindows = process.platform === "win32";
|
|
1186
1358
|
var FALLBACK_PROXY_PORT = 1355;
|
|
1187
1359
|
var PRIVILEGED_PORT_THRESHOLD = 1024;
|
|
@@ -1718,10 +1890,10 @@ function spawnCommand(commandArgs, options) {
|
|
|
1718
1890
|
}
|
|
1719
1891
|
}
|
|
1720
1892
|
}
|
|
1721
|
-
const child = isWindows ?
|
|
1893
|
+
const child = isWindows ? spawn2("cmd.exe", ["/d", "/s", "/c", commandArgs.join(" ")], {
|
|
1722
1894
|
stdio: "inherit",
|
|
1723
1895
|
env
|
|
1724
|
-
}) :
|
|
1896
|
+
}) : spawn2("/bin/sh", ["-c", commandArgs.map(shellEscape).join(" ")], {
|
|
1725
1897
|
stdio: "inherit",
|
|
1726
1898
|
env,
|
|
1727
1899
|
detached: true
|
|
@@ -1867,7 +2039,7 @@ function removePortlessStateFiles(dir) {
|
|
|
1867
2039
|
}
|
|
1868
2040
|
|
|
1869
2041
|
// src/mdns.ts
|
|
1870
|
-
import { spawn as
|
|
2042
|
+
import { spawn as spawn3, spawnSync as spawnSync3 } from "child_process";
|
|
1871
2043
|
|
|
1872
2044
|
// src/lan-ip.ts
|
|
1873
2045
|
import { createSocket } from "dgram";
|
|
@@ -1983,7 +2155,7 @@ function getMdnsPublisher() {
|
|
|
1983
2155
|
return null;
|
|
1984
2156
|
}
|
|
1985
2157
|
function hasCommand(command, probeArgs) {
|
|
1986
|
-
const result =
|
|
2158
|
+
const result = spawnSync3(command, probeArgs, {
|
|
1987
2159
|
stdio: "ignore",
|
|
1988
2160
|
timeout: 1e3,
|
|
1989
2161
|
windowsHide: true
|
|
@@ -2042,7 +2214,7 @@ function publish(hostname, port, ip, onError) {
|
|
|
2042
2214
|
if (!publisher) {
|
|
2043
2215
|
return;
|
|
2044
2216
|
}
|
|
2045
|
-
const child =
|
|
2217
|
+
const child = spawn3(publisher.command, publisher.buildArgs(fqdn, name, port, ip), {
|
|
2046
2218
|
stdio: "ignore",
|
|
2047
2219
|
detached: false
|
|
2048
2220
|
});
|
|
@@ -2595,7 +2767,7 @@ function hasTurboConfig(wsRoot) {
|
|
|
2595
2767
|
import * as fs8 from "fs";
|
|
2596
2768
|
import * as os2 from "os";
|
|
2597
2769
|
import * as path8 from "path";
|
|
2598
|
-
import { spawnSync as
|
|
2770
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
2599
2771
|
var DEFAULT_SERVICE_PORT = getProtocolPort(true);
|
|
2600
2772
|
var SERVICE_LABEL = "sh.portless.proxy";
|
|
2601
2773
|
var SYSTEMD_SERVICE = "portless.service";
|
|
@@ -2614,8 +2786,8 @@ var DEFAULT_SERVICE_CONFIG = {
|
|
|
2614
2786
|
useWildcard: false,
|
|
2615
2787
|
extraEnv: {}
|
|
2616
2788
|
};
|
|
2617
|
-
function
|
|
2618
|
-
return
|
|
2789
|
+
function defaultRunner3(command, args, options) {
|
|
2790
|
+
return spawnSync4(command, args, {
|
|
2619
2791
|
encoding: "utf-8",
|
|
2620
2792
|
stdio: options?.stdio ?? "pipe"
|
|
2621
2793
|
});
|
|
@@ -3337,7 +3509,7 @@ async function uninstallService(entryScript, runner) {
|
|
|
3337
3509
|
}
|
|
3338
3510
|
console.log(colors_default.green("Portless service uninstalled."));
|
|
3339
3511
|
}
|
|
3340
|
-
function tryUninstallService(entryScript, runner =
|
|
3512
|
+
function tryUninstallService(entryScript, runner = defaultRunner3) {
|
|
3341
3513
|
let installed = false;
|
|
3342
3514
|
try {
|
|
3343
3515
|
const spec = currentServiceSpec(entryScript);
|
|
@@ -3465,7 +3637,7 @@ ${colors_default.bold("Notes:")}
|
|
|
3465
3637
|
}
|
|
3466
3638
|
async function handleService(args, options) {
|
|
3467
3639
|
const action = args[1];
|
|
3468
|
-
const runner = options.runner ||
|
|
3640
|
+
const runner = options.runner || defaultRunner3;
|
|
3469
3641
|
if (!action || action === "--help" || action === "-h") {
|
|
3470
3642
|
printServiceHelp();
|
|
3471
3643
|
process.exit(0);
|
|
@@ -3666,7 +3838,7 @@ function collectPortlessEnvArgs2() {
|
|
|
3666
3838
|
function sudoStop(port) {
|
|
3667
3839
|
const stopArgs = [process.execPath, getEntryScript(), "proxy", "stop", "-p", String(port)];
|
|
3668
3840
|
console.log(colors_default.yellow("Proxy is running as root. Elevating with sudo to stop it..."));
|
|
3669
|
-
const result =
|
|
3841
|
+
const result = spawnSync5("sudo", ["env", ...collectPortlessEnvArgs2(), ...stopArgs], {
|
|
3670
3842
|
stdio: "inherit",
|
|
3671
3843
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
3672
3844
|
});
|
|
@@ -3675,7 +3847,7 @@ function sudoStop(port) {
|
|
|
3675
3847
|
function runCleanWithSudo(reason) {
|
|
3676
3848
|
console.log(colors_default.yellow(`${reason} Requesting sudo...`));
|
|
3677
3849
|
const home = process.env.HOME;
|
|
3678
|
-
const result =
|
|
3850
|
+
const result = spawnSync5(
|
|
3679
3851
|
"sudo",
|
|
3680
3852
|
[
|
|
3681
3853
|
"env",
|
|
@@ -3694,12 +3866,20 @@ function runCleanWithSudo(reason) {
|
|
|
3694
3866
|
}
|
|
3695
3867
|
function runServiceUninstallWithSudo(reason) {
|
|
3696
3868
|
console.log(colors_default.yellow(`${reason} Requesting sudo...`));
|
|
3697
|
-
const result =
|
|
3869
|
+
const result = spawnSync5("sudo", buildServiceUninstallSudoArgs(getEntryScript()), {
|
|
3698
3870
|
stdio: "inherit",
|
|
3699
3871
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
3700
3872
|
});
|
|
3701
3873
|
return result.status === 0;
|
|
3702
3874
|
}
|
|
3875
|
+
function isEnabledEnv(value) {
|
|
3876
|
+
return value === "1" || value === "true";
|
|
3877
|
+
}
|
|
3878
|
+
function formatProcessExitSuffix(code, signal) {
|
|
3879
|
+
if (signal) return ` (signal ${signal})`;
|
|
3880
|
+
if (code !== null) return ` (exit ${code})`;
|
|
3881
|
+
return "";
|
|
3882
|
+
}
|
|
3703
3883
|
function startProxyServer(store, proxyPort, tld, tlsOptions, lanIp, strict) {
|
|
3704
3884
|
store.ensureDir();
|
|
3705
3885
|
const isTls = !!tlsOptions;
|
|
@@ -4038,6 +4218,9 @@ function listRoutes(store, proxyPort, tls2) {
|
|
|
4038
4218
|
const tsLabel = route.tailscaleFunnel ? "funnel" : "tailscale";
|
|
4039
4219
|
console.log(` ${colors_default.gray(tsLabel + ":")} ${colors_default.green(route.tailscaleUrl)}`);
|
|
4040
4220
|
}
|
|
4221
|
+
if (route.ngrokUrl) {
|
|
4222
|
+
console.log(` ${colors_default.gray("ngrok:")} ${colors_default.green(route.ngrokUrl)}`);
|
|
4223
|
+
}
|
|
4041
4224
|
}
|
|
4042
4225
|
console.log();
|
|
4043
4226
|
}
|
|
@@ -4121,7 +4304,7 @@ async function ensureProxyRunning(proxyPort, tls2, desired) {
|
|
|
4121
4304
|
proxyPort: startPort
|
|
4122
4305
|
});
|
|
4123
4306
|
const startArgs = [getEntryScript(), "proxy", "start", ...proxyStartConfig.args];
|
|
4124
|
-
const result =
|
|
4307
|
+
const result = spawnSync5(process.execPath, startArgs, {
|
|
4125
4308
|
stdio: "inherit",
|
|
4126
4309
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
4127
4310
|
});
|
|
@@ -4155,8 +4338,9 @@ async function runApp(initialStore, proxyPort, stateDir, name, commandArgs, tls2
|
|
|
4155
4338
|
console.log(chalk.blue.bold(`
|
|
4156
4339
|
portless
|
|
4157
4340
|
`));
|
|
4158
|
-
const wantsFunnel = process.env.PORTLESS_FUNNEL
|
|
4159
|
-
const wantsTailscale = wantsFunnel || process.env.PORTLESS_TAILSCALE
|
|
4341
|
+
const wantsFunnel = isEnabledEnv(process.env.PORTLESS_FUNNEL);
|
|
4342
|
+
const wantsTailscale = wantsFunnel || isEnabledEnv(process.env.PORTLESS_TAILSCALE);
|
|
4343
|
+
const wantsNgrok = isEnabledEnv(process.env.PORTLESS_NGROK);
|
|
4160
4344
|
let tsBaseUrl;
|
|
4161
4345
|
if (wantsTailscale) {
|
|
4162
4346
|
try {
|
|
@@ -4177,6 +4361,18 @@ portless
|
|
|
4177
4361
|
process.exit(1);
|
|
4178
4362
|
}
|
|
4179
4363
|
}
|
|
4364
|
+
if (wantsNgrok) {
|
|
4365
|
+
try {
|
|
4366
|
+
ensureNgrokAvailable();
|
|
4367
|
+
} catch (err) {
|
|
4368
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4369
|
+
console.error(colors_default.red(`Error: ${message}`));
|
|
4370
|
+
if (message.includes("not found")) {
|
|
4371
|
+
console.error(colors_default.blue("Install ngrok: https://ngrok.com/download"));
|
|
4372
|
+
}
|
|
4373
|
+
process.exit(1);
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4180
4376
|
let desired;
|
|
4181
4377
|
try {
|
|
4182
4378
|
desired = resolveProxyDesiredState(lanMode);
|
|
@@ -4262,6 +4458,36 @@ portless
|
|
|
4262
4458
|
}
|
|
4263
4459
|
let tailscaleHttpsPort;
|
|
4264
4460
|
let tailscaleUrl;
|
|
4461
|
+
let ngrokUrl;
|
|
4462
|
+
let ngrokProcess;
|
|
4463
|
+
let stoppingNgrok = false;
|
|
4464
|
+
let ngrokRouteReady = false;
|
|
4465
|
+
let ngrokExitHandled = false;
|
|
4466
|
+
let pendingNgrokExit;
|
|
4467
|
+
const handleNgrokExit = (code, signal) => {
|
|
4468
|
+
if (stoppingNgrok || ngrokExitHandled) return;
|
|
4469
|
+
if (!ngrokRouteReady) {
|
|
4470
|
+
pendingNgrokExit = { code, signal };
|
|
4471
|
+
return;
|
|
4472
|
+
}
|
|
4473
|
+
ngrokExitHandled = true;
|
|
4474
|
+
ngrokUrl = void 0;
|
|
4475
|
+
console.warn(
|
|
4476
|
+
colors_default.yellow(
|
|
4477
|
+
`Warning: ngrok tunnel for ${hostname} stopped${formatProcessExitSuffix(
|
|
4478
|
+
code,
|
|
4479
|
+
signal
|
|
4480
|
+
)}. Removing its public URL from the route list.`
|
|
4481
|
+
)
|
|
4482
|
+
);
|
|
4483
|
+
try {
|
|
4484
|
+
store.updateRoute(hostname, {
|
|
4485
|
+
ngrokUrl: null,
|
|
4486
|
+
ngrokPid: null
|
|
4487
|
+
});
|
|
4488
|
+
} catch {
|
|
4489
|
+
}
|
|
4490
|
+
};
|
|
4265
4491
|
if (wantsTailscale && tsBaseUrl) {
|
|
4266
4492
|
const maxAttempts = 3;
|
|
4267
4493
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -4299,6 +4525,51 @@ portless
|
|
|
4299
4525
|
} catch {
|
|
4300
4526
|
}
|
|
4301
4527
|
}
|
|
4528
|
+
if (wantsNgrok) {
|
|
4529
|
+
try {
|
|
4530
|
+
ngrokProcess = await startNgrok(port, {
|
|
4531
|
+
hostHeader: hostname,
|
|
4532
|
+
onExit: handleNgrokExit
|
|
4533
|
+
});
|
|
4534
|
+
ngrokUrl = ngrokProcess.url;
|
|
4535
|
+
console.log(chalk.green(` ngrok -> ${ngrokUrl}`));
|
|
4536
|
+
console.log(chalk.gray(" (accessible from the public internet via ngrok)\n"));
|
|
4537
|
+
try {
|
|
4538
|
+
store.updateRoute(hostname, {
|
|
4539
|
+
ngrokUrl,
|
|
4540
|
+
ngrokPid: ngrokProcess.pid
|
|
4541
|
+
});
|
|
4542
|
+
} catch {
|
|
4543
|
+
} finally {
|
|
4544
|
+
ngrokRouteReady = true;
|
|
4545
|
+
if (pendingNgrokExit) {
|
|
4546
|
+
handleNgrokExit(pendingNgrokExit.code, pendingNgrokExit.signal);
|
|
4547
|
+
pendingNgrokExit = void 0;
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
} catch (err) {
|
|
4551
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4552
|
+
console.error(colors_default.red(`Error: ${message}`));
|
|
4553
|
+
if (message.includes("not found")) {
|
|
4554
|
+
console.error(colors_default.blue("Install ngrok: https://ngrok.com/download"));
|
|
4555
|
+
} else if (message.includes("authentication")) {
|
|
4556
|
+
console.error(colors_default.blue("Configure ngrok authentication:"));
|
|
4557
|
+
console.error(colors_default.cyan(" ngrok config add-authtoken <token>"));
|
|
4558
|
+
}
|
|
4559
|
+
try {
|
|
4560
|
+
unregisterTailscale({
|
|
4561
|
+
tailscaleHttpsPort,
|
|
4562
|
+
tailscaleFunnel: wantsFunnel || void 0
|
|
4563
|
+
});
|
|
4564
|
+
} catch {
|
|
4565
|
+
}
|
|
4566
|
+
try {
|
|
4567
|
+
store.removeRoute(hostname);
|
|
4568
|
+
} catch {
|
|
4569
|
+
}
|
|
4570
|
+
process.exit(1);
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4302
4573
|
const basename5 = path9.basename(commandArgs[0]);
|
|
4303
4574
|
const isExpo = basename5 === "expo";
|
|
4304
4575
|
const isExpoLan = isExpo && (lanMode || isLanEnvEnabled());
|
|
@@ -4333,9 +4604,12 @@ portless
|
|
|
4333
4604
|
// own LAN discovery natively.
|
|
4334
4605
|
...lanMode ? { PORTLESS_LAN: "1" } : {},
|
|
4335
4606
|
...tailscaleUrl ? { PORTLESS_TAILSCALE_URL: tailscaleUrl } : {},
|
|
4607
|
+
...ngrokUrl ? { PORTLESS_NGROK_URL: ngrokUrl } : {},
|
|
4336
4608
|
...caEnv
|
|
4337
4609
|
},
|
|
4338
4610
|
onCleanup: () => {
|
|
4611
|
+
stoppingNgrok = true;
|
|
4612
|
+
stopNgrokProcess(ngrokProcess?.child);
|
|
4339
4613
|
try {
|
|
4340
4614
|
unregisterTailscale({
|
|
4341
4615
|
tailscaleHttpsPort,
|
|
@@ -4372,7 +4646,7 @@ function appPortFromEnv() {
|
|
|
4372
4646
|
}
|
|
4373
4647
|
return port;
|
|
4374
4648
|
}
|
|
4375
|
-
function
|
|
4649
|
+
function applySharingFlag(flag) {
|
|
4376
4650
|
if (flag === "--tailscale") {
|
|
4377
4651
|
process.env.PORTLESS_TAILSCALE = "1";
|
|
4378
4652
|
return true;
|
|
@@ -4382,6 +4656,10 @@ function applyTailscaleFlag(flag) {
|
|
|
4382
4656
|
process.env.PORTLESS_TAILSCALE = "1";
|
|
4383
4657
|
return true;
|
|
4384
4658
|
}
|
|
4659
|
+
if (flag === "--ngrok") {
|
|
4660
|
+
process.env.PORTLESS_NGROK = "1";
|
|
4661
|
+
return true;
|
|
4662
|
+
}
|
|
4385
4663
|
return false;
|
|
4386
4664
|
}
|
|
4387
4665
|
function parseRunArgs(args) {
|
|
@@ -4407,6 +4685,9 @@ ${colors_default.bold("Options:")}
|
|
|
4407
4685
|
--name <name> Override the inferred base name (worktree prefix still applies)
|
|
4408
4686
|
--force Kill the existing process and take over its route
|
|
4409
4687
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
4688
|
+
--tailscale Share the app on your Tailscale network (tailnet)
|
|
4689
|
+
--funnel Share the app publicly via Tailscale Funnel
|
|
4690
|
+
--ngrok Share the app publicly via ngrok
|
|
4410
4691
|
--help, -h Show this help
|
|
4411
4692
|
|
|
4412
4693
|
${colors_default.bold("Name inference (in order):")}
|
|
@@ -4440,11 +4721,13 @@ ${colors_default.bold("Examples:")}
|
|
|
4440
4721
|
process.exit(1);
|
|
4441
4722
|
}
|
|
4442
4723
|
name = args[i];
|
|
4443
|
-
} else if (
|
|
4724
|
+
} else if (applySharingFlag(args[i])) {
|
|
4444
4725
|
} else {
|
|
4445
4726
|
console.error(colors_default.red(`Error: Unknown flag "${args[i]}".`));
|
|
4446
4727
|
console.error(
|
|
4447
|
-
colors_default.blue(
|
|
4728
|
+
colors_default.blue(
|
|
4729
|
+
"Known flags: --name, --force, --app-port, --tailscale, --funnel, --ngrok, --help"
|
|
4730
|
+
)
|
|
4448
4731
|
);
|
|
4449
4732
|
process.exit(1);
|
|
4450
4733
|
}
|
|
@@ -4466,10 +4749,12 @@ function parseAppArgs(args) {
|
|
|
4466
4749
|
} else if (args[i] === "--app-port") {
|
|
4467
4750
|
i++;
|
|
4468
4751
|
appPort = parseAppPort(args[i]);
|
|
4469
|
-
} else if (
|
|
4752
|
+
} else if (applySharingFlag(args[i])) {
|
|
4470
4753
|
} else {
|
|
4471
4754
|
console.error(colors_default.red(`Error: Unknown flag "${args[i]}".`));
|
|
4472
|
-
console.error(
|
|
4755
|
+
console.error(
|
|
4756
|
+
colors_default.blue("Known flags: --force, --app-port, --tailscale, --funnel, --ngrok")
|
|
4757
|
+
);
|
|
4473
4758
|
process.exit(1);
|
|
4474
4759
|
}
|
|
4475
4760
|
i++;
|
|
@@ -4485,10 +4770,12 @@ function parseAppArgs(args) {
|
|
|
4485
4770
|
} else if (args[i] === "--app-port") {
|
|
4486
4771
|
i++;
|
|
4487
4772
|
appPort = parseAppPort(args[i]);
|
|
4488
|
-
} else if (
|
|
4773
|
+
} else if (applySharingFlag(args[i])) {
|
|
4489
4774
|
} else {
|
|
4490
4775
|
console.error(colors_default.red(`Error: Unknown flag "${args[i]}".`));
|
|
4491
|
-
console.error(
|
|
4776
|
+
console.error(
|
|
4777
|
+
colors_default.blue("Known flags: --force, --app-port, --tailscale, --funnel, --ngrok")
|
|
4778
|
+
);
|
|
4492
4779
|
process.exit(1);
|
|
4493
4780
|
}
|
|
4494
4781
|
i++;
|
|
@@ -4542,6 +4829,7 @@ ${colors_default.bold("Examples:")}
|
|
|
4542
4829
|
portless get backend # -> https://backend.localhost
|
|
4543
4830
|
portless myapp --tailscale next dev # -> also https://<node>.ts.net (tailnet)
|
|
4544
4831
|
portless myapp --funnel next dev # -> also https://<node>.ts.net (public)
|
|
4832
|
+
portless myapp --ngrok next dev # -> also https://<random>.ngrok.app (public)
|
|
4545
4833
|
|
|
4546
4834
|
${colors_default.bold("Configuration (portless.json):")}
|
|
4547
4835
|
Optional. Portless works out of the box by running the "dev" script
|
|
@@ -4603,6 +4891,11 @@ ${colors_default.bold("Tailscale sharing:")}
|
|
|
4603
4891
|
${colors_default.cyan("portless myapp --tailscale next dev")}
|
|
4604
4892
|
${colors_default.cyan("portless myapp --funnel next dev")}
|
|
4605
4893
|
|
|
4894
|
+
${colors_default.bold("ngrok sharing:")}
|
|
4895
|
+
Use --ngrok to expose your dev server to the public internet with ngrok.
|
|
4896
|
+
Requires the ngrok CLI to be installed and authenticated.
|
|
4897
|
+
${colors_default.cyan("portless myapp --ngrok next dev")}
|
|
4898
|
+
|
|
4606
4899
|
${colors_default.bold("Options:")}
|
|
4607
4900
|
run [--name <name>] <cmd> Infer project name (or override with --name)
|
|
4608
4901
|
Adds worktree prefix in git worktrees
|
|
@@ -4622,6 +4915,7 @@ ${colors_default.bold("Options:")}
|
|
|
4622
4915
|
--app-port <number> Use a fixed port for the app (skip auto-assignment)
|
|
4623
4916
|
--tailscale Share the app on your Tailscale network (tailnet)
|
|
4624
4917
|
--funnel Share the app publicly via Tailscale Funnel
|
|
4918
|
+
--ngrok Share the app publicly via ngrok
|
|
4625
4919
|
--force Kill the existing process and take over its route
|
|
4626
4920
|
--name <name> Use <name> as the app name (bypasses subcommand dispatch)
|
|
4627
4921
|
-- Stop flag parsing; everything after is passed to the child
|
|
@@ -4637,6 +4931,7 @@ ${colors_default.bold("Environment variables:")}
|
|
|
4637
4931
|
PORTLESS_SYNC_HOSTS=0 Disable auto-sync of ${HOSTS_DISPLAY} (on by default)
|
|
4638
4932
|
PORTLESS_TAILSCALE=1 Share apps on your Tailscale network (same as --tailscale)
|
|
4639
4933
|
PORTLESS_FUNNEL=1 Share apps publicly via Tailscale Funnel (same as --funnel)
|
|
4934
|
+
PORTLESS_NGROK=1 Share apps publicly via ngrok (same as --ngrok)
|
|
4640
4935
|
PORTLESS_STATE_DIR=<path> Override the state directory
|
|
4641
4936
|
PORTLESS=0 Run command directly without proxy
|
|
4642
4937
|
|
|
@@ -4646,6 +4941,7 @@ ${colors_default.bold("Child process environment:")}
|
|
|
4646
4941
|
PORTLESS_URL Public URL of the app (e.g. https://myapp.localhost)
|
|
4647
4942
|
PORTLESS_LAN Set to 1 when proxy is in LAN mode
|
|
4648
4943
|
PORTLESS_TAILSCALE_URL Tailscale URL of the app (when --tailscale is active)
|
|
4944
|
+
PORTLESS_NGROK_URL ngrok URL of the app (when --ngrok is active)
|
|
4649
4945
|
NODE_EXTRA_CA_CERTS Path to the portless CA (set when HTTPS is active)
|
|
4650
4946
|
|
|
4651
4947
|
${colors_default.bold("Safari / DNS:")}
|
|
@@ -4668,7 +4964,7 @@ ${colors_default.bold("Reserved names:")}
|
|
|
4668
4964
|
process.exit(0);
|
|
4669
4965
|
}
|
|
4670
4966
|
function printVersion() {
|
|
4671
|
-
console.log("0.
|
|
4967
|
+
console.log("0.14.0");
|
|
4672
4968
|
process.exit(0);
|
|
4673
4969
|
}
|
|
4674
4970
|
async function handleTrust() {
|
|
@@ -4689,7 +4985,7 @@ async function handleTrust() {
|
|
|
4689
4985
|
const isPermissionError2 = result.error?.includes("Permission denied") || result.error?.includes("EACCES");
|
|
4690
4986
|
if (isPermissionError2 && !isWindows && process.getuid?.() !== 0) {
|
|
4691
4987
|
console.log(colors_default.yellow("Trusting the CA requires elevated privileges. Requesting sudo..."));
|
|
4692
|
-
const sudoResult =
|
|
4988
|
+
const sudoResult = spawnSync5(
|
|
4693
4989
|
"sudo",
|
|
4694
4990
|
[
|
|
4695
4991
|
"env",
|
|
@@ -4772,6 +5068,10 @@ ${colors_default.bold("Options:")}
|
|
|
4772
5068
|
} catch {
|
|
4773
5069
|
}
|
|
4774
5070
|
}
|
|
5071
|
+
if (route.ngrokPid) {
|
|
5072
|
+
stopNgrok(route);
|
|
5073
|
+
console.log(colors_default.green(`Stopped ngrok tunnel for ${route.hostname}.`));
|
|
5074
|
+
}
|
|
4775
5075
|
}
|
|
4776
5076
|
const stateDirs = collectStateDirsForCleanup();
|
|
4777
5077
|
for (const stateDir of stateDirs) {
|
|
@@ -4851,6 +5151,10 @@ ${colors_default.bold("Options:")}
|
|
|
4851
5151
|
} catch {
|
|
4852
5152
|
}
|
|
4853
5153
|
}
|
|
5154
|
+
if (route.ngrokPid) {
|
|
5155
|
+
stopNgrok(route);
|
|
5156
|
+
console.log(` ${route.hostname} - stopped ngrok tunnel`);
|
|
5157
|
+
}
|
|
4854
5158
|
}
|
|
4855
5159
|
let killed = 0;
|
|
4856
5160
|
for (const route of stale) {
|
|
@@ -5029,7 +5333,7 @@ ${colors_default.bold("Auto-sync:")}
|
|
|
5029
5333
|
`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`
|
|
5030
5334
|
)
|
|
5031
5335
|
);
|
|
5032
|
-
const result =
|
|
5336
|
+
const result = spawnSync5(
|
|
5033
5337
|
"sudo",
|
|
5034
5338
|
["env", ...collectPortlessEnvArgs2(), process.execPath, getEntryScript(), "hosts", "clean"],
|
|
5035
5339
|
{
|
|
@@ -5082,7 +5386,7 @@ ${colors_default.bold("Usage: portless hosts <command>")}
|
|
|
5082
5386
|
console.log(
|
|
5083
5387
|
colors_default.yellow(`Writing to ${HOSTS_DISPLAY} requires elevated privileges. Requesting sudo...`)
|
|
5084
5388
|
);
|
|
5085
|
-
const result =
|
|
5389
|
+
const result = spawnSync5(
|
|
5086
5390
|
"sudo",
|
|
5087
5391
|
["env", ...collectPortlessEnvArgs2(), process.execPath, getEntryScript(), "hosts", "sync"],
|
|
5088
5392
|
{
|
|
@@ -5373,7 +5677,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5373
5677
|
if (!hasExplicitPort) {
|
|
5374
5678
|
console.log(colors_default.gray(`(To skip sudo, use an unprivileged port: ${fallbackCommand})`));
|
|
5375
5679
|
}
|
|
5376
|
-
const result =
|
|
5680
|
+
const result = spawnSync5("sudo", ["env", ...collectPortlessEnvArgs2(), ...startArgs], {
|
|
5377
5681
|
stdio: "inherit",
|
|
5378
5682
|
timeout: SUDO_SPAWN_TIMEOUT_MS
|
|
5379
5683
|
});
|
|
@@ -5513,7 +5817,7 @@ ${colors_default.bold("LAN mode (--lan):")}
|
|
|
5513
5817
|
skipTrust: true
|
|
5514
5818
|
}).args
|
|
5515
5819
|
];
|
|
5516
|
-
const child =
|
|
5820
|
+
const child = spawn4(process.execPath, daemonArgs, {
|
|
5517
5821
|
detached: true,
|
|
5518
5822
|
stdio: ["ignore", logFd, logFd],
|
|
5519
5823
|
env: process.env,
|
|
@@ -5619,7 +5923,7 @@ async function handleDefaultSingle(cwd, scriptName, appConfig) {
|
|
|
5619
5923
|
);
|
|
5620
5924
|
}
|
|
5621
5925
|
function spawnChildProcess(commandArgs, env, cwd) {
|
|
5622
|
-
return
|
|
5926
|
+
return spawn4(commandArgs[0], commandArgs.slice(1), {
|
|
5623
5927
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5624
5928
|
env,
|
|
5625
5929
|
cwd,
|
|
@@ -5890,7 +6194,7 @@ async function runWithTurbo(wsRoot, stateDir, proxyPort, tls2, tld, scriptName,
|
|
|
5890
6194
|
const pm = detectPackageManager(wsRoot);
|
|
5891
6195
|
const useRootScript = hasScript(scriptName, wsRoot);
|
|
5892
6196
|
const turboArgs = useRootScript ? [pm, "run", scriptName, ...extraArgs] : pm === "npm" ? ["npx", "turbo", "run", scriptName, ...extraArgs] : pm === "bun" ? ["bunx", "turbo", "run", scriptName, ...extraArgs] : [pm, "exec", "turbo", "run", scriptName, ...extraArgs];
|
|
5893
|
-
const turboChild =
|
|
6197
|
+
const turboChild = spawn4(turboArgs[0], turboArgs.slice(1), {
|
|
5894
6198
|
stdio: "inherit",
|
|
5895
6199
|
cwd: wsRoot,
|
|
5896
6200
|
env: {
|
|
@@ -6153,6 +6457,9 @@ async function main() {
|
|
|
6153
6457
|
process.env.PORTLESS_FUNNEL = "1";
|
|
6154
6458
|
process.env.PORTLESS_TAILSCALE = "1";
|
|
6155
6459
|
}
|
|
6460
|
+
if (stripGlobalFlag("--ngrok", false)) {
|
|
6461
|
+
process.env.PORTLESS_NGROK = "1";
|
|
6462
|
+
}
|
|
6156
6463
|
const scriptResult = stripGlobalFlag("--script", true);
|
|
6157
6464
|
if (scriptResult === false) {
|
|
6158
6465
|
console.error(colors_default.red("Error: --script requires a script name."));
|
package/dist/index.d.ts
CHANGED
|
@@ -64,7 +64,16 @@ interface RouteMapping extends RouteInfo {
|
|
|
64
64
|
tailscaleUrl?: string;
|
|
65
65
|
tailscaleHttpsPort?: number;
|
|
66
66
|
tailscaleFunnel?: boolean;
|
|
67
|
+
ngrokUrl?: string;
|
|
68
|
+
ngrokPid?: number;
|
|
67
69
|
}
|
|
70
|
+
type RouteMetadataPatch = {
|
|
71
|
+
tailscaleUrl?: string | null;
|
|
72
|
+
tailscaleHttpsPort?: number | null;
|
|
73
|
+
tailscaleFunnel?: boolean | null;
|
|
74
|
+
ngrokUrl?: string | null;
|
|
75
|
+
ngrokPid?: number | null;
|
|
76
|
+
};
|
|
68
77
|
/**
|
|
69
78
|
* Thrown when a route is already registered by a live process and --force was
|
|
70
79
|
* not specified. With --force, the existing process is killed instead.
|
|
@@ -126,7 +135,7 @@ declare class RouteStore {
|
|
|
126
135
|
* Update metadata on an existing route entry. Only provided fields are
|
|
127
136
|
* merged; the route must already exist (matched by hostname).
|
|
128
137
|
*/
|
|
129
|
-
updateRoute(hostname: string, fields:
|
|
138
|
+
updateRoute(hostname: string, fields: RouteMetadataPatch): void;
|
|
130
139
|
removeRoute(hostname: string): void;
|
|
131
140
|
}
|
|
132
141
|
|
package/dist/index.js
CHANGED