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 CHANGED
@@ -16,6 +16,7 @@ npx termbridge
16
16
  ```
17
17
 
18
18
  Scan the QR code and open the URL on your phone. The CLI stays running while the tunnel is active.
19
+ Use the action tray in the UI for quick navigation, including line + page scroll jumps.
19
20
 
20
21
  ## Documentation
21
22
 
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
- try {
1928
- await runTmux(["set-option", "-t", name, "status", "off"]);
1929
- } catch {
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
- child = spawn2("cloudflared", ["tunnel", "--url", localUrl]);
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 url = parseCloudflaredUrl(line);
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
- resolve4({ publicUrl: url });
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, handleLine);
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
- reject(new Error(`cloudflared exited (${code ?? "unknown"})`));
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";