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/README.md +40 -0
- package/dist/bin.js +804 -51
- package/dist/bin.js.map +1 -1
- package/package.json +3 -1
- package/ui/dist/assets/index-D_YqfdN5.css +1 -0
- package/ui/dist/assets/{index-DxHbJ65v.js → index-Xxwcc0Iu.js} +18 -18
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BjtQGpfN.css +0 -1
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,
|
|
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,
|
|
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)) :
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
1175
|
+
resolve5,
|
|
1176
1176
|
reject
|
|
1177
1177
|
);
|
|
1178
|
-
})) :
|
|
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 =
|
|
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
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
2513
|
-
|
|
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 =
|
|
2517
|
-
const
|
|
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 === "/" && !
|
|
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 (
|
|
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 (
|
|
2637
|
-
|
|
2638
|
-
|
|
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((
|
|
2851
|
+
const listen = (port) => new Promise((resolve5, _reject) => {
|
|
2725
2852
|
server.listen(port, "127.0.0.1", () => {
|
|
2726
2853
|
const address = server.address();
|
|
2727
|
-
|
|
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
|
|
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
|
|
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 ?
|
|
2800
|
-
|
|
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
|
|
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,
|
|
3565
|
+
terminalRegistry.add(session.name, session.name, terminalSource);
|
|
2831
3566
|
createdSessions.push(session.name);
|
|
2832
3567
|
}
|
|
2833
3568
|
const { token } = auth.issueToken();
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
2933
|
-
qrcode.generate(text, { small: true }, (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;
|