termbridge 0.3.5 → 0.3.6

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/dist/bin.js CHANGED
@@ -920,21 +920,21 @@ var require_react_development = __commonJS({
920
920
  );
921
921
  actScopeDepth = prevActScopeDepth;
922
922
  }
923
- function recursivelyFlushAsyncActWork(returnValue, resolve4, reject) {
923
+ function recursivelyFlushAsyncActWork(returnValue, resolve5, reject) {
924
924
  var queue = ReactSharedInternals.actQueue;
925
925
  if (null !== queue)
926
926
  if (0 !== queue.length)
927
927
  try {
928
928
  flushActQueue(queue);
929
929
  enqueueTask(function() {
930
- return recursivelyFlushAsyncActWork(returnValue, resolve4, reject);
930
+ return recursivelyFlushAsyncActWork(returnValue, resolve5, reject);
931
931
  });
932
932
  return;
933
933
  } catch (error) {
934
934
  ReactSharedInternals.thrownErrors.push(error);
935
935
  }
936
936
  else ReactSharedInternals.actQueue = null;
937
- 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve4(returnValue);
937
+ 0 < ReactSharedInternals.thrownErrors.length ? (queue = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, reject(queue)) : resolve5(returnValue);
938
938
  }
939
939
  function flushActQueue(queue) {
940
940
  if (!isFlushing) {
@@ -1121,7 +1121,7 @@ var require_react_development = __commonJS({
1121
1121
  ));
1122
1122
  });
1123
1123
  return {
1124
- then: function(resolve4, reject) {
1124
+ then: function(resolve5, reject) {
1125
1125
  didAwaitActCall = true;
1126
1126
  thenable.then(
1127
1127
  function(returnValue) {
@@ -1131,7 +1131,7 @@ var require_react_development = __commonJS({
1131
1131
  flushActQueue(queue), enqueueTask(function() {
1132
1132
  return recursivelyFlushAsyncActWork(
1133
1133
  returnValue,
1134
- resolve4,
1134
+ resolve5,
1135
1135
  reject
1136
1136
  );
1137
1137
  });
@@ -1145,7 +1145,7 @@ var require_react_development = __commonJS({
1145
1145
  ReactSharedInternals.thrownErrors.length = 0;
1146
1146
  reject(_thrownError);
1147
1147
  }
1148
- } else resolve4(returnValue);
1148
+ } else resolve5(returnValue);
1149
1149
  },
1150
1150
  function(error) {
1151
1151
  popActScope(prevActQueue, prevActScopeDepth);
@@ -1167,15 +1167,15 @@ var require_react_development = __commonJS({
1167
1167
  if (0 < ReactSharedInternals.thrownErrors.length)
1168
1168
  throw callback = aggregateErrors(ReactSharedInternals.thrownErrors), ReactSharedInternals.thrownErrors.length = 0, callback;
1169
1169
  return {
1170
- then: function(resolve4, reject) {
1170
+ then: function(resolve5, reject) {
1171
1171
  didAwaitActCall = true;
1172
1172
  0 === prevActScopeDepth ? (ReactSharedInternals.actQueue = queue, enqueueTask(function() {
1173
1173
  return recursivelyFlushAsyncActWork(
1174
1174
  returnValue$jscomp$0,
1175
- resolve4,
1175
+ resolve5,
1176
1176
  reject
1177
1177
  );
1178
- })) : resolve4(returnValue$jscomp$0);
1178
+ })) : resolve5(returnValue$jscomp$0);
1179
1179
  }
1180
1180
  };
1181
1181
  };
@@ -1749,6 +1749,10 @@ var require_jsx_runtime = __commonJS({
1749
1749
  }
1750
1750
  });
1751
1751
 
1752
+ // src/bin.ts
1753
+ import { resolve as resolve4 } from "path";
1754
+ import { config as loadEnv } from "dotenv";
1755
+
1752
1756
  // src/cli/run.ts
1753
1757
  import qrcode2 from "qrcode-terminal";
1754
1758
 
@@ -1820,12 +1824,16 @@ var parseArgs = (argv) => {
1820
1824
  options.noQr = true;
1821
1825
  continue;
1822
1826
  }
1827
+ if (current === "--no-tunnel") {
1828
+ options.tunnel = "none";
1829
+ continue;
1830
+ }
1823
1831
  if (current === "--tunnel") {
1824
1832
  const tunnel = args.shift();
1825
- if (tunnel !== "cloudflare") {
1833
+ if (tunnel !== "cloudflare" && tunnel !== "none") {
1826
1834
  throw new Error("unsupported tunnel provider");
1827
1835
  }
1828
- options.tunnel = "cloudflare";
1836
+ options.tunnel = tunnel;
1829
1837
  continue;
1830
1838
  }
1831
1839
  if (current === "--tunnel-token") {
@@ -1844,6 +1852,74 @@ var parseArgs = (argv) => {
1844
1852
  options.tunnelUrl = url;
1845
1853
  continue;
1846
1854
  }
1855
+ if (current === "--public-url") {
1856
+ const url = args.shift();
1857
+ if (!url) {
1858
+ throw new Error("missing public url");
1859
+ }
1860
+ options.publicUrl = url;
1861
+ continue;
1862
+ }
1863
+ if (current === "--backend") {
1864
+ const backend = args.shift();
1865
+ if (!backend) {
1866
+ throw new Error("missing backend");
1867
+ }
1868
+ if (backend !== "tmux" && backend !== "daytona") {
1869
+ throw new Error("invalid backend");
1870
+ }
1871
+ options.backend = backend;
1872
+ continue;
1873
+ }
1874
+ if (current === "--daytona-repo") {
1875
+ const repo = args.shift();
1876
+ if (!repo) {
1877
+ throw new Error("missing daytona repo");
1878
+ }
1879
+ options.daytonaRepo = repo;
1880
+ continue;
1881
+ }
1882
+ if (current === "--daytona-branch") {
1883
+ const branch = args.shift();
1884
+ if (!branch) {
1885
+ throw new Error("missing daytona branch");
1886
+ }
1887
+ options.daytonaBranch = branch;
1888
+ continue;
1889
+ }
1890
+ if (current === "--daytona-path") {
1891
+ const path = args.shift();
1892
+ if (!path) {
1893
+ throw new Error("missing daytona path");
1894
+ }
1895
+ options.daytonaPath = path;
1896
+ continue;
1897
+ }
1898
+ if (current === "--daytona-name") {
1899
+ const name = args.shift();
1900
+ if (!name) {
1901
+ throw new Error("missing daytona name");
1902
+ }
1903
+ options.daytonaSandboxName = name;
1904
+ continue;
1905
+ }
1906
+ if (current === "--daytona-preview-port") {
1907
+ const port = parseNumber(args.shift());
1908
+ if (!port || port <= 0) {
1909
+ throw new Error("missing daytona preview port");
1910
+ }
1911
+ options.daytonaPreviewPort = port;
1912
+ continue;
1913
+ }
1914
+ if (current === "--daytona-public") {
1915
+ options.daytonaPublic = true;
1916
+ continue;
1917
+ }
1918
+ if (current === "--daytona-direct") {
1919
+ options.daytonaDirect = true;
1920
+ options.tunnel = "none";
1921
+ continue;
1922
+ }
1847
1923
  throw new Error(`unknown option: ${current}`);
1848
1924
  }
1849
1925
  return { command, options };
@@ -1860,10 +1936,20 @@ Options:
1860
1936
  --proxy <port> Proxy a local dev server (e.g., Vite) through termbridge
1861
1937
  --tunnel-token <t> Use a named Cloudflare Tunnel token (requires --port)
1862
1938
  --tunnel-url <url> Public URL for a named tunnel (required with --tunnel-token)
1939
+ --public-url <url> Public URL when tunnel is disabled
1863
1940
  --session <name> Use a specific tmux session name
1864
1941
  --kill-on-exit Kill the tmux session when the CLI exits
1865
1942
  --no-qr Disable QR code output
1866
- --tunnel <provider> Tunnel provider (cloudflare)
1943
+ --no-tunnel Disable tunnel (requires --public-url or TERMBRIDGE_PUBLIC_URL)
1944
+ --backend <name> Terminal backend (tmux | daytona)
1945
+ --daytona-repo <u> Git repo to clone into Daytona
1946
+ --daytona-branch <b> Git branch to checkout in Daytona
1947
+ --daytona-path <p> Repo directory inside the sandbox
1948
+ --daytona-name <n> Daytona sandbox name
1949
+ --daytona-preview-port <p> Preview port to expose from Daytona
1950
+ --daytona-public Make the Daytona sandbox preview public
1951
+ --daytona-direct Run the server inside the Daytona sandbox (no tunnel)
1952
+ --tunnel <provider> Tunnel provider (cloudflare | none)
1867
1953
  -h, --help Show this help message
1868
1954
  `;
1869
1955
 
@@ -1874,6 +1960,7 @@ import qrcode from "qrcode-terminal";
1874
1960
  // src/cli/start.ts
1875
1961
  import { resolve as resolve3, dirname as dirname2 } from "path";
1876
1962
  import { existsSync } from "fs";
1963
+ import { writeFile } from "fs/promises";
1877
1964
  import { fileURLToPath } from "url";
1878
1965
 
1879
1966
  // ../packages/terminal/src/index.ts
@@ -1902,6 +1989,7 @@ var defaultDeps = {
1902
1989
  env: process.env,
1903
1990
  defaultCols: 80,
1904
1991
  defaultRows: 24,
1992
+ defaultCwd: void 0,
1905
1993
  _skipSpawnHelperCheck: false
1906
1994
  };
1907
1995
  var ensureSpawnHelperExecutable = () => {
@@ -1939,8 +2027,12 @@ var createTmuxBackend = (deps = {}) => {
1939
2027
  if (existing) {
1940
2028
  return existing.session;
1941
2029
  }
2030
+ const newSessionArgs = ["new-session", "-d", "-s", name];
2031
+ if (runtime.defaultCwd) {
2032
+ newSessionArgs.push("-c", runtime.defaultCwd);
2033
+ }
1942
2034
  try {
1943
- await runTmux(["new-session", "-d", "-s", name]);
2035
+ await runTmux(newSessionArgs);
1944
2036
  } catch (error) {
1945
2037
  try {
1946
2038
  await runTmux(["has-session", "-t", name]);
@@ -2130,7 +2222,7 @@ var createCloudflaredProvider = (deps = {}) => {
2130
2222
  let stdoutCarry = "";
2131
2223
  let stderrCarry = "";
2132
2224
  const errorLines = [];
2133
- return new Promise((resolve4, reject) => {
2225
+ return new Promise((resolve5, reject) => {
2134
2226
  let resolved = false;
2135
2227
  let resolveTimer = null;
2136
2228
  const resolveOnce = (url) => {
@@ -2142,7 +2234,7 @@ var createCloudflaredProvider = (deps = {}) => {
2142
2234
  clearTimeout(resolveTimer);
2143
2235
  resolveTimer = null;
2144
2236
  }
2145
- resolve4({ publicUrl: url });
2237
+ resolve5({ publicUrl: url });
2146
2238
  };
2147
2239
  const handleLine = (line) => {
2148
2240
  const cleaned = normalizeLine(line);
@@ -2364,6 +2456,7 @@ var createTerminalRegistry = () => {
2364
2456
 
2365
2457
  // src/server/server.ts
2366
2458
  import { createServer as createHttpServer, request as httpRequest } from "http";
2459
+ import { request as httpsRequest } from "https";
2367
2460
  import { randomBytes as randomBytes3 } from "crypto";
2368
2461
  import { WebSocketServer, WebSocket as WsWebSocket } from "ws";
2369
2462
 
@@ -2508,13 +2601,33 @@ var createAppServer = (deps) => {
2508
2601
  const staticHandler = createStaticHandler(deps.uiDistPath, "/__tb/app");
2509
2602
  const wss = new WebSocketServer({ noServer: true });
2510
2603
  const connectionInfo = /* @__PURE__ */ new WeakMap();
2604
+ const hasProxy = typeof deps.proxyPort === "number" || deps.devProxyUrl !== void 0;
2605
+ const resolveProxyUrl = (targetPath, search) => {
2606
+ if (typeof deps.proxyPort === "number") {
2607
+ return new URL(`http://localhost:${deps.proxyPort}${targetPath}${search}`);
2608
+ }
2609
+ if (deps.devProxyUrl) {
2610
+ try {
2611
+ return new URL(`${targetPath}${search}`, deps.devProxyUrl);
2612
+ } catch {
2613
+ return null;
2614
+ }
2615
+ }
2616
+ return null;
2617
+ };
2511
2618
  const proxyRequest = (request, response, targetPath, search) => {
2512
- const targetUrl = `http://localhost:${deps.proxyPort}${targetPath}${search}`;
2513
- const proxyHeaders = { ...request.headers };
2619
+ const targetUrl = resolveProxyUrl(targetPath, search);
2620
+ if (!targetUrl) {
2621
+ response.statusCode = 502;
2622
+ response.end("proxy error");
2623
+ return;
2624
+ }
2625
+ const proxyHeaders = { ...request.headers, ...deps.devProxyHeaders ?? {} };
2514
2626
  delete proxyHeaders.cookie;
2515
2627
  delete proxyHeaders.host;
2516
- proxyHeaders.host = `localhost:${deps.proxyPort}`;
2517
- const proxyReq = httpRequest(
2628
+ proxyHeaders.host = targetUrl.host;
2629
+ const requestImpl = targetUrl.protocol === "https:" ? httpsRequest : httpRequest;
2630
+ const proxyReq = requestImpl(
2518
2631
  targetUrl,
2519
2632
  { method: request.method, headers: proxyHeaders },
2520
2633
  (proxyRes) => {
@@ -2535,7 +2648,7 @@ var createAppServer = (deps) => {
2535
2648
  response.end("ok");
2536
2649
  return;
2537
2650
  }
2538
- if (request.method === "GET" && url.pathname === "/" && !deps.proxyPort) {
2651
+ if (request.method === "GET" && url.pathname === "/" && !hasProxy) {
2539
2652
  response.statusCode = 302;
2540
2653
  response.setHeader("Location", "/__tb/app");
2541
2654
  response.end();
@@ -2616,13 +2729,14 @@ var createAppServer = (deps) => {
2616
2729
  }
2617
2730
  jsonResponse(response, 200, {
2618
2731
  proxyPort: deps.proxyPort ?? null,
2619
- devProxyUrl: deps.devProxyUrl ?? null
2732
+ devProxyUrl: deps.devProxyUrl ?? null,
2733
+ hideTerminalSwitcher: Boolean(deps.hideTerminalSwitcher)
2620
2734
  });
2621
2735
  return;
2622
2736
  }
2623
2737
  const handled = await staticHandler(request, response);
2624
2738
  if (!handled) {
2625
- if (deps.proxyPort && deps.auth.getSessionFromRequest(request)) {
2739
+ if (hasProxy && deps.auth.getSessionFromRequest(request)) {
2626
2740
  proxyRequest(request, response, url.pathname, url.search);
2627
2741
  return;
2628
2742
  }
@@ -2633,9 +2747,22 @@ var createAppServer = (deps) => {
2633
2747
  server.on("upgrade", (request, socket, head) => {
2634
2748
  const url = new URL(request.url, `http://${request.headers.host}`);
2635
2749
  if (!url.pathname.startsWith("/__tb/ws/terminal/")) {
2636
- if (deps.proxyPort && deps.auth.getSessionFromRequest(request)) {
2637
- const targetUrl = `ws://localhost:${deps.proxyPort}${url.pathname}${url.search}`;
2638
- const proxyWs = new WsWebSocket(targetUrl);
2750
+ if (hasProxy && deps.auth.getSessionFromRequest(request)) {
2751
+ let baseUrl = null;
2752
+ try {
2753
+ baseUrl = typeof deps.proxyPort === "number" ? new URL(`http://localhost:${deps.proxyPort}`) : new URL(deps.devProxyUrl);
2754
+ } catch {
2755
+ socket.destroy();
2756
+ return;
2757
+ }
2758
+ const wsProtocol = baseUrl.protocol === "https:" ? "wss:" : "ws:";
2759
+ const targetUrl = new URL(`${url.pathname}${url.search}`, baseUrl);
2760
+ targetUrl.protocol = wsProtocol;
2761
+ const proxyHeaders = { ...request.headers, ...deps.devProxyHeaders ?? {} };
2762
+ delete proxyHeaders.cookie;
2763
+ delete proxyHeaders.host;
2764
+ proxyHeaders.host = targetUrl.host;
2765
+ const proxyWs = new WsWebSocket(targetUrl.toString(), { headers: proxyHeaders });
2639
2766
  proxyWs.on("open", () => {
2640
2767
  wss.handleUpgrade(request, socket, head, (clientWs) => {
2641
2768
  clientWs.on("message", (data) => proxyWs.send(data));
@@ -2721,10 +2848,10 @@ var createAppServer = (deps) => {
2721
2848
  unsubscribe();
2722
2849
  });
2723
2850
  });
2724
- const listen = (port) => new Promise((resolve4, _reject) => {
2851
+ const listen = (port) => new Promise((resolve5, _reject) => {
2725
2852
  server.listen(port, "127.0.0.1", () => {
2726
2853
  const address = server.address();
2727
- resolve4({
2854
+ resolve5({
2728
2855
  port: address.port,
2729
2856
  close: async () => {
2730
2857
  await new Promise((closeResolve) => server.close(() => closeResolve()));
@@ -2736,6 +2863,459 @@ var createAppServer = (deps) => {
2736
2863
  return { listen };
2737
2864
  };
2738
2865
 
2866
+ // src/daytona/daytona-backend.ts
2867
+ import { randomBytes as randomBytes4 } from "crypto";
2868
+ import { Daytona } from "@daytonaio/sdk";
2869
+ var controlKeyMap2 = {
2870
+ ctrl_c: "",
2871
+ esc: "\x1B",
2872
+ tab: " ",
2873
+ up: "\x1B[A",
2874
+ down: "\x1B[B",
2875
+ left: "\x1B[D",
2876
+ right: "\x1B[C"
2877
+ };
2878
+ var noopLogger = {
2879
+ info: () => void 0,
2880
+ warn: () => void 0,
2881
+ error: () => void 0
2882
+ };
2883
+ var deriveRepoPath = (repoUrl) => {
2884
+ const trimmed = repoUrl.replace(/\/$/, "");
2885
+ const last = trimmed.split("/").pop();
2886
+ if (!last) {
2887
+ return "repo";
2888
+ }
2889
+ return last.endsWith(".git") ? last.slice(0, -4) : last;
2890
+ };
2891
+ var createDaytonaBackend = (options) => {
2892
+ const logger = options.logger ?? noopLogger;
2893
+ const sessions = /* @__PURE__ */ new Map();
2894
+ const daytona = new Daytona({
2895
+ apiKey: options.apiKey,
2896
+ apiUrl: options.apiUrl,
2897
+ target: options.target
2898
+ });
2899
+ let sandboxRef = null;
2900
+ let sandboxInit = null;
2901
+ const ensureSandbox = async () => {
2902
+ if (!sandboxInit) {
2903
+ sandboxInit = (async () => {
2904
+ try {
2905
+ const name = options.sandboxName ?? `termbridge-${randomBytes4(4).toString("hex")}`;
2906
+ logger.info(`Daytona: creating sandbox ${name}`);
2907
+ const sandbox = await daytona.create({ name, public: options.public });
2908
+ await sandbox.start();
2909
+ const repoPath = options.repoPath ?? deriveRepoPath(options.repoUrl);
2910
+ logger.info(`Daytona: cloning ${options.repoUrl}`);
2911
+ await sandbox.git.clone(
2912
+ options.repoUrl,
2913
+ repoPath,
2914
+ options.repoBranch,
2915
+ void 0,
2916
+ options.gitUsername,
2917
+ options.gitPassword
2918
+ );
2919
+ sandboxRef = sandbox;
2920
+ logger.info(`Daytona: repo ready at ${repoPath}`);
2921
+ return { sandbox, repoPath };
2922
+ } catch (error) {
2923
+ const message = error instanceof Error ? error.message : "unknown error";
2924
+ logger.error(`Daytona: sandbox init failed (${message})`);
2925
+ throw error;
2926
+ }
2927
+ })();
2928
+ }
2929
+ return sandboxInit;
2930
+ };
2931
+ const ensurePty = async (entry) => {
2932
+ if (entry.handle) {
2933
+ return entry.handle;
2934
+ }
2935
+ const { sandbox, repoPath } = await ensureSandbox();
2936
+ const handle = await sandbox.process.createPty({
2937
+ id: entry.session.name,
2938
+ cwd: repoPath,
2939
+ cols: entry.cols,
2940
+ rows: entry.rows,
2941
+ envs: {
2942
+ TERM: "xterm-256color",
2943
+ COLORTERM: "truecolor"
2944
+ },
2945
+ onData: (data) => {
2946
+ if (!sessions.has(entry.session.name)) {
2947
+ return;
2948
+ }
2949
+ const text = Buffer.from(data).toString("utf8");
2950
+ if (!text) {
2951
+ return;
2952
+ }
2953
+ for (const subscriber of entry.subscribers) {
2954
+ subscriber(text);
2955
+ }
2956
+ }
2957
+ });
2958
+ await handle.waitForConnection();
2959
+ entry.handle = handle;
2960
+ return handle;
2961
+ };
2962
+ const createSession = async (name) => {
2963
+ const existing = sessions.get(name);
2964
+ if (existing) {
2965
+ return existing.session;
2966
+ }
2967
+ const entry = {
2968
+ session: { name, createdAt: /* @__PURE__ */ new Date() },
2969
+ handle: null,
2970
+ subscribers: /* @__PURE__ */ new Set(),
2971
+ cols: 80,
2972
+ rows: 24
2973
+ };
2974
+ sessions.set(name, entry);
2975
+ await ensurePty(entry);
2976
+ return entry.session;
2977
+ };
2978
+ const write = async (sessionName, data) => {
2979
+ if (!data) {
2980
+ return;
2981
+ }
2982
+ const entry = sessions.get(sessionName);
2983
+ if (!entry) {
2984
+ return;
2985
+ }
2986
+ const handle = await ensurePty(entry);
2987
+ await handle.sendInput(data);
2988
+ };
2989
+ const resize = async (sessionName, cols, rows) => {
2990
+ const entry = sessions.get(sessionName);
2991
+ if (!entry) {
2992
+ return;
2993
+ }
2994
+ entry.cols = cols;
2995
+ entry.rows = rows;
2996
+ if (!entry.handle) {
2997
+ return;
2998
+ }
2999
+ await entry.handle.resize(cols, rows);
3000
+ };
3001
+ const sendControl = async (sessionName, key) => {
3002
+ const entry = sessions.get(sessionName);
3003
+ if (!entry) {
3004
+ return;
3005
+ }
3006
+ const handle = await ensurePty(entry);
3007
+ const controlSequence = controlKeyMap2[key];
3008
+ await handle.sendInput(controlSequence);
3009
+ };
3010
+ const scroll = async (_sessionName, _mode, _amount) => {
3011
+ return;
3012
+ };
3013
+ const onOutput = (sessionName, callback) => {
3014
+ const entry = sessions.get(sessionName);
3015
+ if (!entry) {
3016
+ return () => void 0;
3017
+ }
3018
+ entry.subscribers.add(callback);
3019
+ return () => {
3020
+ entry.subscribers.delete(callback);
3021
+ };
3022
+ };
3023
+ const closeSession = async (sessionName) => {
3024
+ const entry = sessions.get(sessionName);
3025
+ if (!entry) {
3026
+ return;
3027
+ }
3028
+ sessions.delete(sessionName);
3029
+ if (entry.handle) {
3030
+ try {
3031
+ await entry.handle.kill();
3032
+ } catch {
3033
+ }
3034
+ try {
3035
+ await entry.handle.disconnect();
3036
+ } catch {
3037
+ }
3038
+ }
3039
+ };
3040
+ const shutdown = async () => {
3041
+ const names = Array.from(sessions.keys());
3042
+ await Promise.all(names.map((name) => closeSession(name)));
3043
+ if (!sandboxRef) {
3044
+ return;
3045
+ }
3046
+ try {
3047
+ await sandboxRef.stop();
3048
+ } catch (error) {
3049
+ const message = error instanceof Error ? error.message : "unknown error";
3050
+ logger.warn(`Daytona: stop failed (${message})`);
3051
+ }
3052
+ if (options.deleteOnExit) {
3053
+ try {
3054
+ await daytona.delete(sandboxRef);
3055
+ } catch (error) {
3056
+ const message = error instanceof Error ? error.message : "unknown error";
3057
+ logger.warn(`Daytona: delete failed (${message})`);
3058
+ }
3059
+ }
3060
+ };
3061
+ return {
3062
+ createSession,
3063
+ write,
3064
+ resize,
3065
+ sendControl,
3066
+ scroll,
3067
+ onOutput,
3068
+ closeSession,
3069
+ shutdown,
3070
+ getPreviewUrl: async (port) => {
3071
+ const { sandbox } = await ensureSandbox();
3072
+ try {
3073
+ const preview = await sandbox.getPreviewLink(port);
3074
+ if (!preview.url) {
3075
+ return null;
3076
+ }
3077
+ const headers = {};
3078
+ if (preview.token) {
3079
+ headers["x-daytona-preview-token"] = preview.token;
3080
+ }
3081
+ headers["x-daytona-skip-preview-warning"] = "true";
3082
+ return { url: preview.url, headers };
3083
+ } catch (error) {
3084
+ const message = error instanceof Error ? error.message : "unknown error";
3085
+ logger.warn(`Daytona: preview failed (${message})`);
3086
+ return null;
3087
+ }
3088
+ }
3089
+ };
3090
+ };
3091
+
3092
+ // src/daytona/daytona-direct.ts
3093
+ import { randomBytes as randomBytes5 } from "crypto";
3094
+ import { Daytona as Daytona2 } from "@daytonaio/sdk";
3095
+ var noopLogger2 = {
3096
+ info: () => void 0,
3097
+ warn: () => void 0,
3098
+ error: () => void 0
3099
+ };
3100
+ var deriveRepoPath2 = (repoUrl) => {
3101
+ const trimmed = repoUrl.replace(/\/$/, "");
3102
+ const last = trimmed.split("/").pop();
3103
+ if (!last) {
3104
+ return "repo";
3105
+ }
3106
+ return last.endsWith(".git") ? last.slice(0, -4) : last;
3107
+ };
3108
+ var normalizePublicUrl = (value) => {
3109
+ const trimmed = value.trim();
3110
+ if (!trimmed) {
3111
+ throw new Error("missing public url");
3112
+ }
3113
+ let parsed;
3114
+ try {
3115
+ parsed = new URL(trimmed);
3116
+ } catch {
3117
+ throw new Error("invalid public url");
3118
+ }
3119
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3120
+ throw new Error("invalid public url");
3121
+ }
3122
+ return trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
3123
+ };
3124
+ var delay = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
3125
+ var ensureTmux = async (sandbox, logger) => {
3126
+ const check = await sandbox.process.executeCommand("command -v tmux");
3127
+ if (check.exitCode === 0) {
3128
+ return;
3129
+ }
3130
+ logger.info("Daytona: installing tmux");
3131
+ const installScript = [
3132
+ "set -e",
3133
+ 'SUDO=""',
3134
+ 'if command -v sudo >/dev/null 2>&1; then SUDO="sudo"; fi',
3135
+ "if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update -y && $SUDO apt-get install -y tmux;",
3136
+ "elif command -v apk >/dev/null 2>&1; then $SUDO apk add --no-cache tmux;",
3137
+ "elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y tmux;",
3138
+ "elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y tmux;",
3139
+ "else echo 'tmux install failed: no supported package manager'; exit 1; fi"
3140
+ ].join(" ");
3141
+ const result = await sandbox.process.executeCommand(installScript);
3142
+ if (result.exitCode !== 0) {
3143
+ throw new Error("tmux install failed");
3144
+ }
3145
+ };
3146
+ var resolvePreviewUrl = async (sandbox, port, logger) => {
3147
+ const preview = await sandbox.getPreviewLink(port);
3148
+ if (!preview.url) {
3149
+ throw new Error("Daytona preview url unavailable");
3150
+ }
3151
+ if (preview.token) {
3152
+ try {
3153
+ const signed = await sandbox.getSignedPreviewUrl(port, 60 * 60 * 24);
3154
+ if (signed.url) {
3155
+ return signed.url;
3156
+ }
3157
+ } catch (error) {
3158
+ const message = error instanceof Error ? error.message : "unknown error";
3159
+ logger.warn(`Daytona: signed preview url failed (${message})`);
3160
+ }
3161
+ }
3162
+ return preview.url;
3163
+ };
3164
+ var readShareUrl = async (sandbox, path, timeoutMs) => {
3165
+ const deadline = Date.now() + timeoutMs;
3166
+ while (Date.now() < deadline) {
3167
+ try {
3168
+ const contents = await sandbox.fs.downloadFile(path);
3169
+ const text = contents.toString("utf8").trim();
3170
+ if (text) {
3171
+ return text.split(/\s+/)[0];
3172
+ }
3173
+ } catch {
3174
+ }
3175
+ await delay(500);
3176
+ }
3177
+ throw new Error("share url unavailable");
3178
+ };
3179
+ var parseShareUrl = (shareUrl) => {
3180
+ const trimmed = shareUrl.trim();
3181
+ const marker = "/__tb/s/";
3182
+ const index = trimmed.indexOf(marker);
3183
+ if (index === -1) {
3184
+ throw new Error("invalid share url");
3185
+ }
3186
+ const publicUrl = trimmed.slice(0, index);
3187
+ const token = trimmed.slice(index + marker.length);
3188
+ if (!publicUrl || !token) {
3189
+ throw new Error("invalid share url");
3190
+ }
3191
+ return { publicUrl, token };
3192
+ };
3193
+ var buildEnv = (values) => Object.fromEntries(
3194
+ Object.entries(values).filter(([, value]) => typeof value === "string" && value.length > 0)
3195
+ );
3196
+ var createDaytonaSandboxServerProvider = (options = {}) => {
3197
+ const baseLogger = options.logger ?? noopLogger2;
3198
+ const daytona = new Daytona2({
3199
+ apiKey: options.apiKey,
3200
+ apiUrl: options.apiUrl,
3201
+ target: options.target
3202
+ });
3203
+ return {
3204
+ start: async (startOptions) => {
3205
+ const logger = startOptions.logger ?? baseLogger;
3206
+ let sandboxRef = null;
3207
+ try {
3208
+ const name = startOptions.sandboxName ?? `termbridge-${randomBytes5(4).toString("hex")}`;
3209
+ logger.info(`Daytona: creating sandbox ${name}`);
3210
+ const sandbox = await daytona.create({ name, public: startOptions.public });
3211
+ sandboxRef = sandbox;
3212
+ await sandbox.start();
3213
+ const repoPath = startOptions.repoPath ?? deriveRepoPath2(startOptions.repoUrl);
3214
+ logger.info(`Daytona: cloning ${startOptions.repoUrl}`);
3215
+ await sandbox.git.clone(
3216
+ startOptions.repoUrl,
3217
+ repoPath,
3218
+ startOptions.repoBranch,
3219
+ void 0,
3220
+ startOptions.gitUsername,
3221
+ startOptions.gitPassword
3222
+ );
3223
+ const workDir = await sandbox.getWorkDir();
3224
+ const repoDir = repoPath.startsWith("/") ? repoPath : workDir ? `${workDir.replace(/\/$/, "")}/${repoPath}` : repoPath;
3225
+ await ensureTmux(sandbox, logger);
3226
+ const publicUrl = normalizePublicUrl(
3227
+ await resolvePreviewUrl(sandbox, startOptions.serverPort, logger)
3228
+ );
3229
+ const runId = randomBytes5(4).toString("hex");
3230
+ const shareFile = `/tmp/termbridge-share-${runId}.txt`;
3231
+ const pidFile = `/tmp/termbridge-${runId}.pid`;
3232
+ const logFile = `/tmp/termbridge-${runId}.log`;
3233
+ const args = [
3234
+ "npx",
3235
+ "termbridge",
3236
+ "start",
3237
+ "--port",
3238
+ String(startOptions.serverPort),
3239
+ "--no-qr",
3240
+ "--tunnel",
3241
+ "none"
3242
+ ];
3243
+ if (startOptions.proxyPort) {
3244
+ args.push("--proxy", String(startOptions.proxyPort));
3245
+ }
3246
+ if (startOptions.sessionName) {
3247
+ args.push("--session", startOptions.sessionName);
3248
+ }
3249
+ if (startOptions.killOnExit) {
3250
+ args.push("--kill-on-exit");
3251
+ }
3252
+ const env = buildEnv({
3253
+ TERMBRIDGE_BACKEND: "tmux",
3254
+ TERMBRIDGE_PUBLIC_URL: publicUrl,
3255
+ TERMBRIDGE_SHARE_FILE: shareFile,
3256
+ TERMBRIDGE_TMUX_CWD: repoDir,
3257
+ TERMBRIDGE_HIDE_TERMINAL_SWITCHER: startOptions.hideTerminalSwitcher ? "1" : void 0
3258
+ });
3259
+ const startCommand2 = `nohup ${args.join(" ")} > ${logFile} 2>&1 & echo $! > ${pidFile}`;
3260
+ await sandbox.process.executeCommand(startCommand2, repoDir, env);
3261
+ logger.info("Daytona: waiting for share url");
3262
+ const shareUrl = await readShareUrl(sandbox, shareFile, 9e4);
3263
+ const parsed = parseShareUrl(shareUrl);
3264
+ const stop = async () => {
3265
+ try {
3266
+ const pidBuffer = await sandbox.fs.downloadFile(pidFile);
3267
+ const pid = Number.parseInt(pidBuffer.toString("utf8").trim(), 10);
3268
+ if (Number.isFinite(pid)) {
3269
+ await sandbox.process.executeCommand(`kill ${pid}`);
3270
+ }
3271
+ } catch {
3272
+ }
3273
+ try {
3274
+ await sandbox.stop();
3275
+ } catch (error) {
3276
+ const message = error instanceof Error ? error.message : "unknown error";
3277
+ logger.warn(`Daytona: stop failed (${message})`);
3278
+ }
3279
+ if (startOptions.deleteOnExit) {
3280
+ try {
3281
+ await daytona.delete(sandbox);
3282
+ } catch (error) {
3283
+ const message = error instanceof Error ? error.message : "unknown error";
3284
+ logger.warn(`Daytona: delete failed (${message})`);
3285
+ }
3286
+ }
3287
+ };
3288
+ return {
3289
+ localUrl: parsed.publicUrl,
3290
+ publicUrl: parsed.publicUrl,
3291
+ token: parsed.token,
3292
+ stop
3293
+ };
3294
+ } catch (error) {
3295
+ const message = error instanceof Error ? error.message : "unknown error";
3296
+ logger.error(`Daytona: sandbox start failed (${message})`);
3297
+ if (sandboxRef) {
3298
+ try {
3299
+ await sandboxRef.stop();
3300
+ } catch (stopError) {
3301
+ const message2 = stopError instanceof Error ? stopError.message : "unknown error";
3302
+ baseLogger.warn(`Daytona: stop failed (${message2})`);
3303
+ }
3304
+ if (startOptions.deleteOnExit) {
3305
+ try {
3306
+ await daytona.delete(sandboxRef);
3307
+ } catch (deleteError) {
3308
+ const message2 = deleteError instanceof Error ? deleteError.message : "unknown error";
3309
+ baseLogger.warn(`Daytona: delete failed (${message2})`);
3310
+ }
3311
+ }
3312
+ }
3313
+ throw error;
3314
+ }
3315
+ }
3316
+ };
3317
+ };
3318
+
2739
3319
  // src/cli/start.ts
2740
3320
  var resolveUiDistPath = () => {
2741
3321
  const currentDir = dirname2(fileURLToPath(import.meta.url));
@@ -2767,7 +3347,38 @@ var parseSessionCount = (value) => {
2767
3347
  }
2768
3348
  return parsed;
2769
3349
  };
2770
- var normalizePublicUrl = (value) => {
3350
+ var parseBoolean = (value) => {
3351
+ if (!value) {
3352
+ return false;
3353
+ }
3354
+ const normalized = value.trim().toLowerCase();
3355
+ return normalized === "1" || normalized === "true" || normalized === "yes";
3356
+ };
3357
+ var parseOptionalNumber = (value) => {
3358
+ if (!value) {
3359
+ return void 0;
3360
+ }
3361
+ const parsed = Number.parseInt(value, 10);
3362
+ return Number.isFinite(parsed) ? parsed : void 0;
3363
+ };
3364
+ var resolveBackendMode = (value) => {
3365
+ if (!value) {
3366
+ return "tmux";
3367
+ }
3368
+ if (value === "tmux" || value === "daytona") {
3369
+ return value;
3370
+ }
3371
+ throw new Error("invalid backend");
3372
+ };
3373
+ var deriveRepoPath3 = (repoUrl) => {
3374
+ const trimmed = repoUrl.replace(/\/$/, "");
3375
+ const last = trimmed.split("/").pop();
3376
+ if (!last) {
3377
+ return "repo";
3378
+ }
3379
+ return last.endsWith(".git") ? last.slice(0, -4) : last;
3380
+ };
3381
+ var normalizePublicUrl2 = (value) => {
2771
3382
  let parsed;
2772
3383
  try {
2773
3384
  parsed = new URL(value);
@@ -2779,6 +3390,38 @@ var normalizePublicUrl = (value) => {
2779
3390
  }
2780
3391
  return value.endsWith("/") ? value.slice(0, -1) : value;
2781
3392
  };
3393
+ var normalizeExternalUrl = (value) => {
3394
+ let parsed;
3395
+ try {
3396
+ parsed = new URL(value);
3397
+ } catch {
3398
+ throw new Error("invalid public url");
3399
+ }
3400
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
3401
+ throw new Error("invalid public url");
3402
+ }
3403
+ return value.endsWith("/") ? value.slice(0, -1) : value;
3404
+ };
3405
+ var resolveTunnelMode = (value) => {
3406
+ if (!value) {
3407
+ return "cloudflare";
3408
+ }
3409
+ if (value === "cloudflare" || value === "none") {
3410
+ return value;
3411
+ }
3412
+ throw new Error("invalid tunnel provider");
3413
+ };
3414
+ var maybeWriteShareFile = async (path, url, logger) => {
3415
+ if (!path) {
3416
+ return;
3417
+ }
3418
+ try {
3419
+ await writeFile(path, url, "utf8");
3420
+ } catch (error) {
3421
+ const message = error instanceof Error ? error.message : "unknown error";
3422
+ logger.warn(`Share file write failed (${message})`);
3423
+ }
3424
+ };
2782
3425
  var startCommand = async (options, deps = {}) => {
2783
3426
  const logger = deps.logger ?? createDefaultLogger();
2784
3427
  const processRef = deps.process ?? process;
@@ -2790,17 +3433,106 @@ var startCommand = async (options, deps = {}) => {
2790
3433
  sessionMaxMs: 8 * 60 * 6e4,
2791
3434
  cookieSecure: !insecureCookie
2792
3435
  })))();
2793
- const terminalBackend = (deps.createTerminalBackend ?? (() => createTmuxBackend()))();
3436
+ const backendMode = resolveBackendMode(options.backend ?? env.TERMBRIDGE_BACKEND);
3437
+ const daytonaDirect = options.daytonaDirect ?? parseBoolean(env.TERMBRIDGE_DAYTONA_DIRECT);
3438
+ const publicUrlOverride = options.publicUrl ?? env.TERMBRIDGE_PUBLIC_URL;
3439
+ const hideTerminalSwitcher = parseBoolean(env.TERMBRIDGE_HIDE_TERMINAL_SWITCHER);
3440
+ const tmuxCwd = env.TERMBRIDGE_TMUX_CWD;
3441
+ const daytonaRepo = options.daytonaRepo ?? env.TERMBRIDGE_DAYTONA_REPO ?? "https://github.com/inline0/termbridge-test-app.git";
3442
+ const daytonaPreviewPort = options.daytonaPreviewPort ?? parseOptionalNumber(env.TERMBRIDGE_DAYTONA_PREVIEW_PORT);
3443
+ const daytonaPublic = options.daytonaPublic ?? parseBoolean(env.TERMBRIDGE_DAYTONA_PUBLIC);
3444
+ const daytonaDeleteOnExit = parseBoolean(env.TERMBRIDGE_DAYTONA_DELETE_ON_EXIT);
3445
+ const daytonaConfig = {
3446
+ apiKey: env.DAYTONA_API_KEY,
3447
+ apiUrl: env.DAYTONA_API_URL,
3448
+ target: env.DAYTONA_TARGET,
3449
+ repoUrl: daytonaRepo,
3450
+ repoBranch: options.daytonaBranch ?? env.TERMBRIDGE_DAYTONA_BRANCH,
3451
+ repoPath: options.daytonaPath ?? env.TERMBRIDGE_DAYTONA_PATH ?? deriveRepoPath3(daytonaRepo),
3452
+ sandboxName: options.daytonaSandboxName ?? env.TERMBRIDGE_DAYTONA_NAME,
3453
+ public: daytonaPublic,
3454
+ deleteOnExit: daytonaDeleteOnExit,
3455
+ gitUsername: env.TERMBRIDGE_DAYTONA_GIT_USERNAME,
3456
+ gitPassword: env.TERMBRIDGE_DAYTONA_GIT_PASSWORD ?? env.TERMBRIDGE_DAYTONA_GIT_TOKEN,
3457
+ logger
3458
+ };
3459
+ if (backendMode === "daytona" && daytonaDirect) {
3460
+ const serverPort = options.port ?? parseOptionalNumber(env.TERMBRIDGE_DAYTONA_SERVER_PORT) ?? 8080;
3461
+ const proxyPort = options.proxy ?? daytonaPreviewPort;
3462
+ const sandboxProvider = (deps.createSandboxProvider ?? createDaytonaSandboxServerProvider)({
3463
+ apiKey: daytonaConfig.apiKey,
3464
+ apiUrl: daytonaConfig.apiUrl,
3465
+ target: daytonaConfig.target,
3466
+ logger
3467
+ });
3468
+ const result = await sandboxProvider.start({
3469
+ repoUrl: daytonaConfig.repoUrl,
3470
+ repoBranch: daytonaConfig.repoBranch,
3471
+ repoPath: daytonaConfig.repoPath,
3472
+ sandboxName: daytonaConfig.sandboxName,
3473
+ public: daytonaConfig.public,
3474
+ deleteOnExit: daytonaConfig.deleteOnExit,
3475
+ gitUsername: daytonaConfig.gitUsername,
3476
+ gitPassword: daytonaConfig.gitPassword,
3477
+ serverPort,
3478
+ proxyPort,
3479
+ sessionName: options.session,
3480
+ killOnExit: options.killOnExit,
3481
+ hideTerminalSwitcher: true,
3482
+ logger
3483
+ });
3484
+ const redeemUrl2 = `${result.publicUrl}/__tb/s/${result.token}`;
3485
+ logger.info(`Public URL: ${result.publicUrl}`);
3486
+ logger.info(`Share URL: ${redeemUrl2}`);
3487
+ if (!options.noQr && deps.qr) {
3488
+ deps.qr.generate(redeemUrl2, { small: true });
3489
+ } else if (!options.noQr) {
3490
+ logger.warn("QR output unavailable");
3491
+ }
3492
+ const shutdown2 = () => {
3493
+ void result.stop();
3494
+ };
3495
+ processRef.on("SIGINT", shutdown2);
3496
+ processRef.on("SIGTERM", shutdown2);
3497
+ return result;
3498
+ }
3499
+ const tunnelMode = resolveTunnelMode(env.TERMBRIDGE_TUNNEL ?? options.tunnel);
3500
+ const terminalBackend = backendMode === "daytona" ? (deps.createDaytonaBackend ?? createDaytonaBackend)(daytonaConfig) : (deps.createTerminalBackend ?? (() => createTmuxBackend({ defaultCwd: tmuxCwd })))();
2794
3501
  const terminalRegistry = (deps.createTerminalRegistry ?? (() => createTerminalRegistry()))();
2795
- const tunnelProvider = (deps.createTunnelProvider ?? (() => createCloudflaredProvider()))();
3502
+ const tunnelProvider = tunnelMode === "cloudflare" ? (deps.createTunnelProvider ?? (() => createCloudflaredProvider()))() : null;
2796
3503
  const tunnelTokenRaw = options.tunnelToken ?? env.TERMBRIDGE_TUNNEL_TOKEN;
2797
3504
  const tunnelToken = tunnelTokenRaw?.trim() || void 0;
2798
3505
  const tunnelUrlRaw = options.tunnelUrl ?? env.TERMBRIDGE_TUNNEL_URL;
2799
- const tunnelUrl = tunnelToken && tunnelUrlRaw ? normalizePublicUrl(tunnelUrlRaw) : void 0;
2800
- if (tunnelToken && !options.port) {
3506
+ const tunnelUrl = tunnelToken && tunnelUrlRaw ? normalizePublicUrl2(tunnelUrlRaw) : void 0;
3507
+ let devProxyUrl = options.devProxyUrl;
3508
+ let devProxyHeaders;
3509
+ let publicUrl = "";
3510
+ if (tunnelMode === "none") {
3511
+ if (!publicUrlOverride) {
3512
+ throw new Error("public url required when tunnel disabled");
3513
+ }
3514
+ if (tunnelToken || tunnelUrlRaw) {
3515
+ throw new Error("tunnel token/url not supported when tunnel disabled");
3516
+ }
3517
+ publicUrl = normalizeExternalUrl(publicUrlOverride);
3518
+ }
3519
+ if (!devProxyUrl && backendMode === "daytona" && daytonaPreviewPort) {
3520
+ const previewInfo = await terminalBackend.getPreviewUrl?.(daytonaPreviewPort);
3521
+ if (previewInfo) {
3522
+ if (typeof previewInfo === "string") {
3523
+ devProxyUrl = previewInfo;
3524
+ } else {
3525
+ devProxyUrl = previewInfo.url;
3526
+ devProxyHeaders = previewInfo.headers;
3527
+ }
3528
+ } else {
3529
+ logger.warn("Daytona: preview URL unavailable");
3530
+ }
3531
+ }
3532
+ if (tunnelMode === "cloudflare" && tunnelToken && !options.port) {
2801
3533
  throw new Error("port required when using tunnel token");
2802
3534
  }
2803
- if (tunnelToken && !tunnelUrl) {
3535
+ if (tunnelMode === "cloudflare" && tunnelToken && !tunnelUrl) {
2804
3536
  throw new Error("tunnel url required when using tunnel token");
2805
3537
  }
2806
3538
  const wsLimiter = createRateLimiter({ limit: 30, windowMs: 6e4 });
@@ -2816,50 +3548,68 @@ var startCommand = async (options, deps = {}) => {
2816
3548
  terminalRegistry,
2817
3549
  terminalBackend,
2818
3550
  proxyPort: options.proxy,
2819
- devProxyUrl: options.devProxyUrl
3551
+ devProxyUrl,
3552
+ devProxyHeaders,
3553
+ hideTerminalSwitcher
2820
3554
  });
2821
3555
  const started = await server.listen(options.port ?? 0);
2822
3556
  const localUrl = `http://127.0.0.1:${started.port}`;
2823
3557
  const sessionName = options.session ?? `termbridge-${started.port}`;
2824
3558
  const sessionCount = parseSessionCount(env.TERMBRIDGE_SESSIONS);
2825
3559
  const createdSessions = [];
3560
+ const terminalSource = backendMode === "daytona" ? "daytona" : "tmux";
2826
3561
  for (let index = 0; index < sessionCount; index += 1) {
2827
3562
  const suffix = index === 0 ? "" : `-${index + 1}`;
2828
3563
  const nextName = `${sessionName}${suffix}`;
2829
3564
  const session = await terminalBackend.createSession(nextName);
2830
- terminalRegistry.add(session.name, session.name, "tmux");
3565
+ terminalRegistry.add(session.name, session.name, terminalSource);
2831
3566
  createdSessions.push(session.name);
2832
3567
  }
2833
3568
  const { token } = auth.issueToken();
2834
- let publicUrl = "";
2835
- try {
2836
- const result = await tunnelProvider.start(localUrl, {
2837
- token: tunnelToken,
2838
- publicUrl: tunnelUrl
2839
- });
2840
- publicUrl = result.publicUrl;
2841
- } catch (error) {
2842
- const message = error instanceof Error ? error.message : "unknown error";
2843
- logger.error(`Tunnel failed: ${message}`);
2844
- await started.close();
2845
- throw error;
3569
+ if (tunnelMode === "cloudflare") {
3570
+ if (!tunnelProvider) {
3571
+ throw new Error("tunnel provider unavailable");
3572
+ }
3573
+ try {
3574
+ const result = await tunnelProvider.start(localUrl, {
3575
+ token: tunnelToken,
3576
+ publicUrl: tunnelUrl
3577
+ });
3578
+ publicUrl = result.publicUrl;
3579
+ } catch (error) {
3580
+ const message = error instanceof Error ? error.message : "unknown error";
3581
+ logger.error(`Tunnel failed: ${message}`);
3582
+ await started.close();
3583
+ throw error;
3584
+ }
2846
3585
  }
2847
3586
  const redeemUrl = `${publicUrl}/__tb/s/${token}`;
3587
+ await maybeWriteShareFile(env.TERMBRIDGE_SHARE_FILE, redeemUrl, logger);
2848
3588
  logger.info(`Local server: ${localUrl}`);
2849
- logger.info(`Tunnel URL: ${redeemUrl}`);
3589
+ if (tunnelMode === "cloudflare") {
3590
+ logger.info(`Tunnel URL: ${redeemUrl}`);
3591
+ } else {
3592
+ logger.info(`Public URL: ${publicUrl}`);
3593
+ logger.info(`Share URL: ${redeemUrl}`);
3594
+ }
2850
3595
  if (!options.noQr && deps.qr) {
2851
3596
  deps.qr.generate(redeemUrl, { small: true });
2852
3597
  } else if (!options.noQr) {
2853
3598
  logger.warn("QR output unavailable");
2854
3599
  }
2855
3600
  const stop = async () => {
2856
- await tunnelProvider.stop();
3601
+ if (tunnelProvider) {
3602
+ await tunnelProvider.stop();
3603
+ }
2857
3604
  await started.close();
2858
3605
  if (options.killOnExit) {
2859
3606
  for (const name of createdSessions) {
2860
3607
  await terminalBackend.closeSession(name);
2861
3608
  }
2862
3609
  }
3610
+ if (terminalBackend.shutdown) {
3611
+ await terminalBackend.shutdown();
3612
+ }
2863
3613
  };
2864
3614
  const shutdown = () => {
2865
3615
  void stop();
@@ -2929,8 +3679,8 @@ var buildSessionName = (options, localUrl) => {
2929
3679
  }
2930
3680
  };
2931
3681
  var buildRedeemUrl = (result) => `${result.publicUrl}/__tb/s/${result.token}`;
2932
- var generateQr = async (text) => new Promise((resolve4) => {
2933
- qrcode.generate(text, { small: true }, (output) => resolve4(output));
3682
+ var generateQr = async (text) => new Promise((resolve5) => {
3683
+ qrcode.generate(text, { small: true }, (output) => resolve5(output));
2934
3684
  });
2935
3685
  var runInkCli = async (options, deps = {}) => {
2936
3686
  const processRef = deps.process ?? process;
@@ -3015,6 +3765,9 @@ var runCli = async (argv, deps = {}) => {
3015
3765
  };
3016
3766
 
3017
3767
  // src/bin.ts
3768
+ if (process.env.NODE_ENV !== "test") {
3769
+ loadEnv({ path: resolve4(process.cwd(), ".env") });
3770
+ }
3018
3771
  var main = async (argv = process.argv.slice(2), proc = process) => {
3019
3772
  const exitCode = await runCli(argv, { process: proc, stdout: proc.stdout, stderr: proc.stderr });
3020
3773
  proc.exitCode = exitCode;