bhole 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +30 -0
- package/dist/commands/http.d.ts +3 -0
- package/dist/commands/http.d.ts.map +1 -0
- package/dist/commands/http.js +95 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +24 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +40 -0
- package/dist/commands/tcp.d.ts +3 -0
- package/dist/commands/tcp.d.ts.map +1 -0
- package/dist/commands/tcp.js +10 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.d.ts.map +1 -0
- package/dist/commands/whoami.js +38 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +35 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/lib/loginPlain.d.ts +2 -0
- package/dist/lib/loginPlain.d.ts.map +1 -0
- package/dist/lib/loginPlain.js +76 -0
- package/dist/lib/tunnelPlain.d.ts +2 -0
- package/dist/lib/tunnelPlain.d.ts.map +1 -0
- package/dist/lib/tunnelPlain.js +31 -0
- package/dist/lib/version.d.ts +4 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +44 -0
- package/dist/lib/words.d.ts +2 -0
- package/dist/lib/words.d.ts.map +1 -0
- package/dist/lib/words.js +24 -0
- package/dist/tunnel/http.d.ts +23 -0
- package/dist/tunnel/http.d.ts.map +1 -0
- package/dist/tunnel/http.js +267 -0
- package/dist/ui/LoginApp.d.ts +8 -0
- package/dist/ui/LoginApp.d.ts.map +1 -0
- package/dist/ui/LoginApp.js +129 -0
- package/dist/ui/TunnelApp.d.ts +12 -0
- package/dist/ui/TunnelApp.d.ts.map +1 -0
- package/dist/ui/TunnelApp.js +122 -0
- package/package.json +38 -0
- package/src/commands/config.ts +37 -0
- package/src/commands/http.tsx +117 -0
- package/src/commands/tcp.ts +11 -0
- package/src/config.ts +41 -0
- package/src/index.ts +20 -0
- package/src/lib/tunnelPlain.ts +45 -0
- package/src/lib/version.ts +45 -0
- package/src/lib/words.ts +25 -0
- package/src/tunnel/http.ts +315 -0
- package/src/ui/TunnelApp.tsx +252 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import React, { useEffect, useState, useRef } from "react";
|
|
2
|
+
import { render, Text, Box, useInput } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { createHttpTunnel, type TunnelRequestInfo } from "../tunnel/http.js";
|
|
5
|
+
import { getLatestVersion, shouldSuggestUpdate } from "../lib/version.js";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
type TunnelStatus = "connecting" | "registering" | "ready" | "error";
|
|
9
|
+
|
|
10
|
+
interface TunnelAppProps {
|
|
11
|
+
serverUrl: string;
|
|
12
|
+
endpoint: string;
|
|
13
|
+
localPort: number;
|
|
14
|
+
publicUrl: string;
|
|
15
|
+
tunnelDomain?: string;
|
|
16
|
+
authToken?: string;
|
|
17
|
+
onExit: (code: number) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatBytes(n: number): string {
|
|
21
|
+
if (n < 1024) return `${n} B`;
|
|
22
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
23
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function regionDisplay(region: string): string {
|
|
27
|
+
const names: Record<string, string> = {
|
|
28
|
+
ord: "Chicago",
|
|
29
|
+
iad: "Virginia",
|
|
30
|
+
lax: "Los Angeles",
|
|
31
|
+
gru: "São Paulo",
|
|
32
|
+
arn: "Stockholm",
|
|
33
|
+
local: "Local",
|
|
34
|
+
};
|
|
35
|
+
return names[region] ?? region;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function TunnelApp({
|
|
39
|
+
serverUrl,
|
|
40
|
+
endpoint,
|
|
41
|
+
localPort,
|
|
42
|
+
publicUrl,
|
|
43
|
+
tunnelDomain = "localhost",
|
|
44
|
+
authToken,
|
|
45
|
+
onExit,
|
|
46
|
+
}: TunnelAppProps) {
|
|
47
|
+
const [status, setStatus] = useState<TunnelStatus>("connecting");
|
|
48
|
+
const [error, setError] = useState<string | null>(null);
|
|
49
|
+
const [requests, setRequests] = useState(0);
|
|
50
|
+
const [bytesIn, setBytesIn] = useState(0);
|
|
51
|
+
const [bytesOut, setBytesOut] = useState(0);
|
|
52
|
+
const [recentRequests, setRecentRequests] = useState<TunnelRequestInfo[]>([]);
|
|
53
|
+
const [retryCount, setRetryCount] = useState(0);
|
|
54
|
+
const [serviceInfo, setServiceInfo] = useState<{ region: string; version: string } | null>(null);
|
|
55
|
+
const [latency, setLatency] = useState<{ up: number; down: number } | null>(null);
|
|
56
|
+
const [updateAvailable, setUpdateAvailable] = useState<string | null>(null);
|
|
57
|
+
const tunnelStarted = useRef(false);
|
|
58
|
+
const retriesUsed = useRef(0);
|
|
59
|
+
const maxRetries = 2;
|
|
60
|
+
const baseDelayMs = 2000;
|
|
61
|
+
|
|
62
|
+
useInput((input, key) => {
|
|
63
|
+
if (key.ctrl && input === "c") {
|
|
64
|
+
onExit(0);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (tunnelStarted.current) return;
|
|
70
|
+
tunnelStarted.current = true;
|
|
71
|
+
|
|
72
|
+
const wsUrl = serverUrl.replace(/^http/, "ws");
|
|
73
|
+
|
|
74
|
+
const tryConnect = () => {
|
|
75
|
+
setStatus("connecting");
|
|
76
|
+
setError(null);
|
|
77
|
+
if (retriesUsed.current > 0) {
|
|
78
|
+
setRetryCount(retriesUsed.current);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createHttpTunnel(
|
|
82
|
+
{ serverUrl: wsUrl, endpoint, localPort, authToken },
|
|
83
|
+
{
|
|
84
|
+
onOpen: () => setStatus("registering"),
|
|
85
|
+
onReady: () => setStatus("ready"),
|
|
86
|
+
onRequest: (info) => {
|
|
87
|
+
setRequests((n) => n + 1);
|
|
88
|
+
setBytesIn((b) => b + info.bytesIn);
|
|
89
|
+
setBytesOut((b) => b + info.bytesOut);
|
|
90
|
+
setRecentRequests((prev) => [info, ...prev.slice(0, 4)]);
|
|
91
|
+
},
|
|
92
|
+
onError: (err) => {
|
|
93
|
+
setError(err.message);
|
|
94
|
+
setStatus("error");
|
|
95
|
+
if (retriesUsed.current < maxRetries) {
|
|
96
|
+
const attempt = retriesUsed.current + 1;
|
|
97
|
+
retriesUsed.current = attempt;
|
|
98
|
+
const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
|
|
99
|
+
setTimeout(() => tryConnect(), delayMs);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
).catch(() => {
|
|
104
|
+
if (retriesUsed.current < maxRetries) {
|
|
105
|
+
const attempt = retriesUsed.current + 1;
|
|
106
|
+
retriesUsed.current = attempt;
|
|
107
|
+
const delayMs = baseDelayMs * Math.pow(2, attempt - 1);
|
|
108
|
+
setTimeout(() => tryConnect(), delayMs);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
tryConnect();
|
|
114
|
+
}, [serverUrl, endpoint, localPort]);
|
|
115
|
+
|
|
116
|
+
// Fetch account, service info, and measure latency when ready
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (status !== "ready") return;
|
|
119
|
+
|
|
120
|
+
const host = new URL(serverUrl.replace(/^ws/, "http")).host;
|
|
121
|
+
const apiBase = serverUrl.includes("localhost")
|
|
122
|
+
? "http://localhost:8080"
|
|
123
|
+
: `https://${host}`;
|
|
124
|
+
|
|
125
|
+
const measureLatency = () => {
|
|
126
|
+
const start = performance.now();
|
|
127
|
+
fetch(`${apiBase}/api/health`, { cache: "no-store" })
|
|
128
|
+
.then((r) => r.json())
|
|
129
|
+
.then((data) => {
|
|
130
|
+
const total = Math.round(performance.now() - start);
|
|
131
|
+
setServiceInfo({ region: data.region ?? "?", version: data.version ?? "?" });
|
|
132
|
+
setLatency({ up: Math.round(total / 2), down: Math.round(total / 2) });
|
|
133
|
+
})
|
|
134
|
+
.catch(() => {});
|
|
135
|
+
};
|
|
136
|
+
measureLatency();
|
|
137
|
+
const latId = setInterval(measureLatency, 10000);
|
|
138
|
+
|
|
139
|
+
getLatestVersion()
|
|
140
|
+
.then((latest) => {
|
|
141
|
+
if (shouldSuggestUpdate(latest)) setUpdateAvailable(latest!);
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {});
|
|
144
|
+
|
|
145
|
+
return () => clearInterval(latId);
|
|
146
|
+
}, [status, serverUrl, tunnelDomain]);
|
|
147
|
+
|
|
148
|
+
if (status === "connecting" || status === "registering") {
|
|
149
|
+
return (
|
|
150
|
+
<Box flexDirection="column" padding={1}>
|
|
151
|
+
<Box>
|
|
152
|
+
<Text color="cyan">
|
|
153
|
+
<Spinner type="dots" />{" "}
|
|
154
|
+
{status === "connecting" ? "Connecting" : "Registering tunnel"}...
|
|
155
|
+
{retryCount > 0 ? (
|
|
156
|
+
<Text color="dim"> (retry {retryCount}/{maxRetries})</Text>
|
|
157
|
+
) : null}
|
|
158
|
+
</Text>
|
|
159
|
+
</Box>
|
|
160
|
+
</Box>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (status === "error") {
|
|
165
|
+
return (
|
|
166
|
+
<Box flexDirection="column" padding={1}>
|
|
167
|
+
<Box marginBottom={1}>
|
|
168
|
+
<Text bold color="red">
|
|
169
|
+
✗ Tunnel error: {error}
|
|
170
|
+
</Text>
|
|
171
|
+
</Box>
|
|
172
|
+
<Box>
|
|
173
|
+
<Text color="dim">Press Ctrl+C to exit</Text>
|
|
174
|
+
</Box>
|
|
175
|
+
</Box>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Box flexDirection="column" padding={1}>
|
|
181
|
+
<Box>
|
|
182
|
+
<Text color="dim">Status: </Text>
|
|
183
|
+
<Text bold color="green">
|
|
184
|
+
Tunnel active
|
|
185
|
+
</Text>
|
|
186
|
+
</Box>
|
|
187
|
+
{serviceInfo && (
|
|
188
|
+
<Box>
|
|
189
|
+
<Text color="dim">Region: </Text>
|
|
190
|
+
<Text color="cyan">{regionDisplay(serviceInfo.region)}</Text>
|
|
191
|
+
</Box>
|
|
192
|
+
)}
|
|
193
|
+
{latency && (
|
|
194
|
+
<Box>
|
|
195
|
+
<Text color="dim">Latency: </Text>
|
|
196
|
+
<Text color="cyan">{latency.up}ms</Text>
|
|
197
|
+
<Text color="dim"> ↑ </Text>
|
|
198
|
+
<Text color="cyan">{latency.down}ms</Text>
|
|
199
|
+
<Text color="dim"> ↓</Text>
|
|
200
|
+
</Box>
|
|
201
|
+
)}
|
|
202
|
+
<Box>
|
|
203
|
+
<Text color="dim">Forwarding: </Text>
|
|
204
|
+
<Text color="cyan">{publicUrl}</Text>
|
|
205
|
+
<Text color="dim"> → </Text>
|
|
206
|
+
<Text color="green">localhost:{localPort}</Text>
|
|
207
|
+
</Box>
|
|
208
|
+
|
|
209
|
+
<Box>
|
|
210
|
+
<Text color="dim">Usage: </Text>
|
|
211
|
+
<Text color="cyan">↑{formatBytes(bytesIn)}</Text>
|
|
212
|
+
<Text color="dim"> </Text>
|
|
213
|
+
<Text color="cyan">↓{formatBytes(bytesOut)}</Text>
|
|
214
|
+
</Box>
|
|
215
|
+
{recentRequests.length > 0 && (
|
|
216
|
+
<Box flexDirection="column" marginTop={1}>
|
|
217
|
+
<Box>
|
|
218
|
+
<Text color="dim">Requests </Text>
|
|
219
|
+
<Text color="cyan">{requests}</Text>
|
|
220
|
+
</Box>
|
|
221
|
+
{recentRequests.slice(0, 5).map((r, i) => (
|
|
222
|
+
<Box key={i}>
|
|
223
|
+
<Text color="dim">
|
|
224
|
+
[{r.timestamp.toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit" })}]
|
|
225
|
+
</Text>
|
|
226
|
+
<Text color="dim"> </Text>
|
|
227
|
+
<Text color="cyan">{r.method}</Text>
|
|
228
|
+
<Text color="dim"> </Text>
|
|
229
|
+
{r.statusCode != null && (
|
|
230
|
+
<>
|
|
231
|
+
<Text color={r.statusCode >= 400 ? "red" : "green"}>{r.statusCode}</Text>
|
|
232
|
+
<Text color="dim"> </Text>
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
<Text color="yellow">{r.path}</Text>
|
|
236
|
+
</Box>
|
|
237
|
+
))}
|
|
238
|
+
</Box>
|
|
239
|
+
)}
|
|
240
|
+
{updateAvailable && (
|
|
241
|
+
<Box>
|
|
242
|
+
<Text color="yellow">Update available: </Text>
|
|
243
|
+
<Text color="cyan">{updateAvailable}</Text>
|
|
244
|
+
<Text color="dim"> — npm i -g bhole</Text>
|
|
245
|
+
</Box>
|
|
246
|
+
)}
|
|
247
|
+
<Box>
|
|
248
|
+
<Text color="dim">Press Ctrl+C to stop</Text>
|
|
249
|
+
</Box>
|
|
250
|
+
</Box>
|
|
251
|
+
);
|
|
252
|
+
}
|
package/tsconfig.json
ADDED