termbridge 0.3.1 → 0.3.3
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 +1 -0
- package/dist/bin.js +140 -11
- package/dist/bin.js.map +1 -1
- package/package.json +2 -2
- package/ui/dist/assets/{index-B6CAkCwJ.css → index-DXNjQhk1.css} +1 -1
- package/ui/dist/assets/index-DhXz4snW.js +103 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CI7cgRhu.js +0 -103
package/README.md
CHANGED
package/dist/bin.js
CHANGED
|
@@ -1828,6 +1828,22 @@ var parseArgs = (argv) => {
|
|
|
1828
1828
|
options.tunnel = "cloudflare";
|
|
1829
1829
|
continue;
|
|
1830
1830
|
}
|
|
1831
|
+
if (current === "--tunnel-token") {
|
|
1832
|
+
const token = args.shift();
|
|
1833
|
+
if (!token) {
|
|
1834
|
+
throw new Error("missing tunnel token");
|
|
1835
|
+
}
|
|
1836
|
+
options.tunnelToken = token;
|
|
1837
|
+
continue;
|
|
1838
|
+
}
|
|
1839
|
+
if (current === "--tunnel-url") {
|
|
1840
|
+
const url = args.shift();
|
|
1841
|
+
if (!url) {
|
|
1842
|
+
throw new Error("missing tunnel url");
|
|
1843
|
+
}
|
|
1844
|
+
options.tunnelUrl = url;
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1831
1847
|
throw new Error(`unknown option: ${current}`);
|
|
1832
1848
|
}
|
|
1833
1849
|
return { command, options };
|
|
@@ -1842,6 +1858,8 @@ Usage:
|
|
|
1842
1858
|
Options:
|
|
1843
1859
|
--port <port> Bind the local server to a fixed port
|
|
1844
1860
|
--proxy <port> Proxy a local dev server (e.g., Vite) through termbridge
|
|
1861
|
+
--tunnel-token <t> Use a named Cloudflare Tunnel token (requires --port)
|
|
1862
|
+
--tunnel-url <url> Public URL for a named tunnel (required with --tunnel-token)
|
|
1845
1863
|
--session <name> Use a specific tmux session name
|
|
1846
1864
|
--kill-on-exit Kill the tmux session when the CLI exits
|
|
1847
1865
|
--no-qr Disable QR code output
|
|
@@ -1910,6 +1928,12 @@ var createTmuxBackend = (deps = {}) => {
|
|
|
1910
1928
|
const runtime = { ...defaultDeps, ...deps };
|
|
1911
1929
|
const sessions = /* @__PURE__ */ new Map();
|
|
1912
1930
|
const runTmux = async (args) => runtime.execFile("tmux", args);
|
|
1931
|
+
const setSessionOption = async (name, option, value) => {
|
|
1932
|
+
try {
|
|
1933
|
+
await runTmux(["set-option", "-t", name, option, value]);
|
|
1934
|
+
} catch {
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1913
1937
|
const createSession = async (name) => {
|
|
1914
1938
|
const existing = sessions.get(name);
|
|
1915
1939
|
if (existing) {
|
|
@@ -1924,10 +1948,12 @@ var createTmuxBackend = (deps = {}) => {
|
|
|
1924
1948
|
throw error;
|
|
1925
1949
|
}
|
|
1926
1950
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1951
|
+
await setSessionOption(name, "status", "off");
|
|
1952
|
+
await setSessionOption(name, "status-left", "");
|
|
1953
|
+
await setSessionOption(name, "status-right", "");
|
|
1954
|
+
await setSessionOption(name, "status-style", "bg=default,fg=default");
|
|
1955
|
+
await setSessionOption(name, "message-style", "bg=default,fg=default");
|
|
1956
|
+
await setSessionOption(name, "message-command-style", "bg=default,fg=default");
|
|
1931
1957
|
const session = { name, createdAt: /* @__PURE__ */ new Date() };
|
|
1932
1958
|
sessions.set(name, {
|
|
1933
1959
|
session,
|
|
@@ -2000,6 +2026,29 @@ var createTmuxBackend = (deps = {}) => {
|
|
|
2000
2026
|
const controlSequence = controlKeyMap[key];
|
|
2001
2027
|
ensurePty(entry).write(controlSequence);
|
|
2002
2028
|
};
|
|
2029
|
+
const scroll = async (sessionName, mode, amount) => {
|
|
2030
|
+
if (!Number.isFinite(amount) || amount === 0) {
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
if (!sessions.has(sessionName)) {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
const direction = amount < 0 ? "up" : "down";
|
|
2037
|
+
const command = mode === "pages" ? `page-${direction}` : `scroll-${direction}`;
|
|
2038
|
+
const steps = Math.min(50, Math.abs(Math.trunc(amount)));
|
|
2039
|
+
try {
|
|
2040
|
+
await runTmux(["copy-mode", "-e", "-t", sessionName]);
|
|
2041
|
+
} catch {
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
for (let index = 0; index < steps; index += 1) {
|
|
2045
|
+
try {
|
|
2046
|
+
await runTmux(["send-keys", "-t", sessionName, "-X", command]);
|
|
2047
|
+
} catch {
|
|
2048
|
+
break;
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
};
|
|
2003
2052
|
const resize = async (sessionName, cols, rows) => {
|
|
2004
2053
|
const entry = sessions.get(sessionName);
|
|
2005
2054
|
if (!entry) {
|
|
@@ -2042,6 +2091,7 @@ var createTmuxBackend = (deps = {}) => {
|
|
|
2042
2091
|
write,
|
|
2043
2092
|
resize,
|
|
2044
2093
|
sendControl,
|
|
2094
|
+
scroll,
|
|
2045
2095
|
onOutput,
|
|
2046
2096
|
closeSession
|
|
2047
2097
|
};
|
|
@@ -2062,36 +2112,83 @@ var readLines = (data, carry, onLine) => {
|
|
|
2062
2112
|
}
|
|
2063
2113
|
return remainder;
|
|
2064
2114
|
};
|
|
2115
|
+
var normalizeLine = (line) => line.trim();
|
|
2065
2116
|
var createCloudflaredProvider = (deps = {}) => {
|
|
2066
2117
|
const spawn2 = deps.spawn ?? spawnCallback;
|
|
2067
2118
|
let child = null;
|
|
2068
|
-
const start = (localUrl) => {
|
|
2119
|
+
const start = (localUrl, options = {}) => {
|
|
2069
2120
|
if (child) {
|
|
2070
2121
|
return Promise.reject(new Error("cloudflared already running"));
|
|
2071
2122
|
}
|
|
2072
|
-
|
|
2123
|
+
const token = options.token?.trim();
|
|
2124
|
+
const publicUrl = options.publicUrl?.trim();
|
|
2125
|
+
if (token && !publicUrl) {
|
|
2126
|
+
return Promise.reject(new Error("tunnel public URL required when using tunnel token"));
|
|
2127
|
+
}
|
|
2128
|
+
const args = token ? ["tunnel", "run", "--token", token] : ["tunnel", "--url", localUrl];
|
|
2129
|
+
child = spawn2("cloudflared", args);
|
|
2073
2130
|
let stdoutCarry = "";
|
|
2074
2131
|
let stderrCarry = "";
|
|
2132
|
+
const errorLines = [];
|
|
2075
2133
|
return new Promise((resolve4, reject) => {
|
|
2134
|
+
let resolved = false;
|
|
2135
|
+
let resolveTimer = null;
|
|
2136
|
+
const resolveOnce = (url) => {
|
|
2137
|
+
if (resolved) {
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
resolved = true;
|
|
2141
|
+
if (resolveTimer) {
|
|
2142
|
+
clearTimeout(resolveTimer);
|
|
2143
|
+
resolveTimer = null;
|
|
2144
|
+
}
|
|
2145
|
+
resolve4({ publicUrl: url });
|
|
2146
|
+
};
|
|
2076
2147
|
const handleLine = (line) => {
|
|
2077
|
-
const
|
|
2148
|
+
const cleaned = normalizeLine(line);
|
|
2149
|
+
if (!cleaned) {
|
|
2150
|
+
return;
|
|
2151
|
+
}
|
|
2152
|
+
options.log?.(cleaned, "stdout");
|
|
2153
|
+
const url = parseCloudflaredUrl(cleaned);
|
|
2078
2154
|
if (url) {
|
|
2079
|
-
|
|
2155
|
+
resolveOnce(url);
|
|
2080
2156
|
}
|
|
2081
2157
|
};
|
|
2082
2158
|
const handleOutput = (data, isStdout) => {
|
|
2083
2159
|
if (isStdout) {
|
|
2084
2160
|
stdoutCarry = readLines(data.toString(), stdoutCarry, handleLine);
|
|
2085
2161
|
} else {
|
|
2086
|
-
stderrCarry = readLines(data.toString(), stderrCarry,
|
|
2162
|
+
stderrCarry = readLines(data.toString(), stderrCarry, (line) => {
|
|
2163
|
+
const cleaned = normalizeLine(line);
|
|
2164
|
+
if (!cleaned) {
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
options.log?.(cleaned, "stderr");
|
|
2168
|
+
errorLines.push(cleaned);
|
|
2169
|
+
if (errorLines.length > 6) {
|
|
2170
|
+
errorLines.shift();
|
|
2171
|
+
}
|
|
2172
|
+
const url = parseCloudflaredUrl(cleaned);
|
|
2173
|
+
if (url) {
|
|
2174
|
+
resolveOnce(url);
|
|
2175
|
+
}
|
|
2176
|
+
});
|
|
2087
2177
|
}
|
|
2088
2178
|
};
|
|
2089
2179
|
child?.stdout.on("data", (data) => handleOutput(data, true));
|
|
2090
2180
|
child?.stderr.on("data", (data) => handleOutput(data, false));
|
|
2091
2181
|
child?.once("error", (error) => reject(error));
|
|
2092
2182
|
child?.once("exit", (code) => {
|
|
2093
|
-
|
|
2183
|
+
if (resolved) {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
const suffix = errorLines.length > 0 ? `: ${errorLines.join(" | ")}` : "";
|
|
2187
|
+
reject(new Error(`cloudflared exited (${code ?? "unknown"})${suffix}`));
|
|
2094
2188
|
});
|
|
2189
|
+
if (token && publicUrl) {
|
|
2190
|
+
resolveTimer = setTimeout(() => resolveOnce(publicUrl), 1200);
|
|
2191
|
+
}
|
|
2095
2192
|
});
|
|
2096
2193
|
};
|
|
2097
2194
|
const stop = async () => {
|
|
@@ -2395,6 +2492,9 @@ var parseClientMessage = (payload) => {
|
|
|
2395
2492
|
if (parsed.type === "control" && allowedControlKeys.has(parsed.key)) {
|
|
2396
2493
|
return { ok: true, message: parsed };
|
|
2397
2494
|
}
|
|
2495
|
+
if (parsed.type === "scroll" && (parsed.mode === "lines" || parsed.mode === "pages") && typeof parsed.amount === "number" && Number.isFinite(parsed.amount)) {
|
|
2496
|
+
return { ok: true, message: parsed };
|
|
2497
|
+
}
|
|
2398
2498
|
return { ok: false, error: "invalid" };
|
|
2399
2499
|
} catch {
|
|
2400
2500
|
return { ok: false, error: "invalid" };
|
|
@@ -2611,6 +2711,10 @@ var createAppServer = (deps) => {
|
|
|
2611
2711
|
void deps.terminalBackend.resize(info.sessionName, message.cols, message.rows);
|
|
2612
2712
|
return;
|
|
2613
2713
|
}
|
|
2714
|
+
if (message.type === "scroll") {
|
|
2715
|
+
void deps.terminalBackend.scroll(info.sessionName, message.mode, message.amount);
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2614
2718
|
void deps.terminalBackend.sendControl(info.sessionName, message.key);
|
|
2615
2719
|
});
|
|
2616
2720
|
socket.on("close", () => {
|
|
@@ -2663,6 +2767,18 @@ var parseSessionCount = (value) => {
|
|
|
2663
2767
|
}
|
|
2664
2768
|
return parsed;
|
|
2665
2769
|
};
|
|
2770
|
+
var normalizePublicUrl = (value) => {
|
|
2771
|
+
let parsed;
|
|
2772
|
+
try {
|
|
2773
|
+
parsed = new URL(value);
|
|
2774
|
+
} catch {
|
|
2775
|
+
throw new Error("invalid tunnel url");
|
|
2776
|
+
}
|
|
2777
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2778
|
+
throw new Error("invalid tunnel url");
|
|
2779
|
+
}
|
|
2780
|
+
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
2781
|
+
};
|
|
2666
2782
|
var startCommand = async (options, deps = {}) => {
|
|
2667
2783
|
const logger = deps.logger ?? createDefaultLogger();
|
|
2668
2784
|
const processRef = deps.process ?? process;
|
|
@@ -2677,6 +2793,16 @@ var startCommand = async (options, deps = {}) => {
|
|
|
2677
2793
|
const terminalBackend = (deps.createTerminalBackend ?? (() => createTmuxBackend()))();
|
|
2678
2794
|
const terminalRegistry = (deps.createTerminalRegistry ?? (() => createTerminalRegistry()))();
|
|
2679
2795
|
const tunnelProvider = (deps.createTunnelProvider ?? (() => createCloudflaredProvider()))();
|
|
2796
|
+
const tunnelTokenRaw = options.tunnelToken ?? env.TERMBRIDGE_TUNNEL_TOKEN;
|
|
2797
|
+
const tunnelToken = tunnelTokenRaw?.trim() || void 0;
|
|
2798
|
+
const tunnelUrlRaw = options.tunnelUrl ?? env.TERMBRIDGE_TUNNEL_URL;
|
|
2799
|
+
const tunnelUrl = tunnelToken && tunnelUrlRaw ? normalizePublicUrl(tunnelUrlRaw) : void 0;
|
|
2800
|
+
if (tunnelToken && !options.port) {
|
|
2801
|
+
throw new Error("port required when using tunnel token");
|
|
2802
|
+
}
|
|
2803
|
+
if (tunnelToken && !tunnelUrl) {
|
|
2804
|
+
throw new Error("tunnel url required when using tunnel token");
|
|
2805
|
+
}
|
|
2680
2806
|
const wsLimiter = createRateLimiter({ limit: 30, windowMs: 6e4 });
|
|
2681
2807
|
const serverFactory = deps.createServer ?? ((serverDeps) => createAppServer({
|
|
2682
2808
|
...serverDeps,
|
|
@@ -2707,7 +2833,10 @@ var startCommand = async (options, deps = {}) => {
|
|
|
2707
2833
|
const { token } = auth.issueToken();
|
|
2708
2834
|
let publicUrl = "";
|
|
2709
2835
|
try {
|
|
2710
|
-
const result = await tunnelProvider.start(localUrl
|
|
2836
|
+
const result = await tunnelProvider.start(localUrl, {
|
|
2837
|
+
token: tunnelToken,
|
|
2838
|
+
publicUrl: tunnelUrl
|
|
2839
|
+
});
|
|
2711
2840
|
publicUrl = result.publicUrl;
|
|
2712
2841
|
} catch (error) {
|
|
2713
2842
|
const message = error instanceof Error ? error.message : "unknown error";
|