pinggy 0.3.0 → 0.3.2

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.
Files changed (47) hide show
  1. package/.github/workflows/publish-binaries.yml +50 -54
  2. package/Makefile +2 -2
  3. package/dist/index.cjs +1060 -902
  4. package/dist/index.d.cts +1 -1
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +1039 -129
  7. package/ent.plist +14 -0
  8. package/package.json +31 -14
  9. package/scripts/pre_pkg_processing.js +74 -0
  10. package/src/cli/buildConfig.ts +10 -13
  11. package/src/cli/{starCli.tsx → starCli.ts} +51 -80
  12. package/src/index.ts +0 -6
  13. package/src/remote_management/remoteManagement.ts +0 -3
  14. package/src/remote_management/remote_schema.ts +2 -2
  15. package/src/tui/blessed/TunnelTui.ts +298 -0
  16. package/src/tui/blessed/components/DisplayUpdaters.ts +118 -0
  17. package/src/tui/blessed/components/KeyBindings.ts +134 -0
  18. package/src/tui/blessed/components/Modals.ts +216 -0
  19. package/src/tui/blessed/components/UIComponents.ts +306 -0
  20. package/src/tui/blessed/components/index.ts +4 -0
  21. package/src/tui/blessed/headerFetcher.ts +35 -0
  22. package/src/tui/blessed/index.ts +2 -0
  23. package/src/tui/blessed/qrCodeGenerator.ts +20 -0
  24. package/src/tui/blessed/webDebuggerConnection.ts +100 -0
  25. package/src/tui/{hooks → ink/hooks}/useReqResHeaders.ts +1 -1
  26. package/src/tui/{hooks → ink/hooks}/useWebDebugger.ts +2 -2
  27. package/src/tui/{index.tsx → ink/index.tsx} +12 -2
  28. package/src/tui/spinner/spinner.ts +64 -0
  29. package/src/tunnel_manager/TunnelManager.ts +9 -10
  30. package/src/types.ts +1 -1
  31. package/src/utils/printer.ts +15 -21
  32. package/src/utils/util.ts +5 -0
  33. package/tsconfig.json +1 -1
  34. package/dist/tui-AZUFY7T2.js +0 -584
  35. package/src/utils/esmOnlyPackageLoader.ts +0 -29
  36. /package/src/tui/{asciArt.ts → ink/asciArt.ts} +0 -0
  37. /package/src/tui/{hooks → ink/hooks}/useQrCodes.ts +0 -0
  38. /package/src/tui/{hooks → ink/hooks}/useTerminalSize.ts +0 -0
  39. /package/src/tui/{hooks → ink/hooks}/useTerminalStats.ts +0 -0
  40. /package/src/tui/{layout → ink/layout}/Borders.tsx +0 -0
  41. /package/src/tui/{layout → ink/layout}/Container.tsx +0 -0
  42. /package/src/tui/{sections → ink/sections}/DebuggerDetailModal.tsx +0 -0
  43. /package/src/tui/{sections → ink/sections}/KeyBindings.tsx +0 -0
  44. /package/src/tui/{sections → ink/sections}/QrCodeSection.tsx +0 -0
  45. /package/src/tui/{sections → ink/sections}/StatsSection.tsx +0 -0
  46. /package/src/tui/{sections → ink/sections}/URLsSection.tsx +0 -0
  47. /package/src/tui/{utils → ink/utils}/utils.ts +0 -0
@@ -0,0 +1,298 @@
1
+ import blessed from "blessed";
2
+ import { FinalConfig, ReqResPair } from "../../types.js";
3
+ import { TunnelUsageType } from "@pinggy/pinggy";
4
+ import { createQrCodes } from "./qrCodeGenerator.js";
5
+ import { createWebDebuggerConnection, WebDebuggerConnection } from "./webDebuggerConnection.js";
6
+ import { ManagedTunnel, TunnelManager } from "../../tunnel_manager/TunnelManager.js";
7
+ import {
8
+ createFullUI,
9
+ createSimpleUI,
10
+ createWarningUI,
11
+ UIElements,
12
+ MIN_WIDTH_WARNING,
13
+ SIMPLE_LAYOUT_THRESHOLD,
14
+ } from "./components/UIComponents.js";
15
+ import {
16
+ updateUrlsDisplay,
17
+ updateStatsDisplay,
18
+ updateRequestsDisplay,
19
+ updateQrCodeDisplay,
20
+ } from "./components/DisplayUpdaters.js";
21
+ import {
22
+ ModalManager,
23
+ showDisconnectModal,
24
+ } from "./components/Modals.js";
25
+ import {
26
+ setupKeyBindings,
27
+ KeyBindingsState,
28
+ KeyBindingsCallbacks,
29
+ } from "./components/KeyBindings.js";
30
+
31
+ interface TunnelAppProps {
32
+ urls: string[];
33
+ greet?: string;
34
+ tunnelConfig?: FinalConfig;
35
+ disconnectInfo?: {
36
+ disconnected: boolean;
37
+ error?: string;
38
+ messages?: string[];
39
+ } | null;
40
+ tunnelInstance?:ManagedTunnel
41
+ }
42
+
43
+ export class TunnelTui {
44
+ private screen: blessed.Widgets.Screen;
45
+ private urls: string[];
46
+ private greet: string;
47
+ private tunnelConfig: FinalConfig | undefined;
48
+ private disconnectInfo: TunnelAppProps["disconnectInfo"];
49
+
50
+ // State
51
+ private currentQrIndex: number = 0;
52
+ private selectedIndex: number = 0;
53
+ private qrCodes: string[] = [];
54
+ private stats: TunnelUsageType = {
55
+ elapsedTime: 0,
56
+ numLiveConnections: 0,
57
+ numTotalConnections: 0,
58
+ numTotalReqBytes: 0,
59
+ numTotalResBytes: 0,
60
+ numTotalTxBytes: 0,
61
+ };
62
+ private pairs: Map<number, ReqResPair> = new Map();
63
+ private webDebuggerConnection: WebDebuggerConnection | null = null;
64
+
65
+ // UI Elements
66
+ private uiElements!: UIElements;
67
+ private modalManager: ModalManager = {
68
+ detailModal: null,
69
+ keyBindingsModal: null,
70
+ disconnectModal: null,
71
+ inDetailView: false,
72
+ keyBindingView: false,
73
+ inDisconnectView: false,
74
+ };
75
+ private tunnelInstance?: ManagedTunnel
76
+
77
+ private exitPromiseResolve: (() => void) | null = null;
78
+ private exitPromise: Promise<void>;
79
+
80
+ constructor(props: TunnelAppProps) {
81
+ this.urls = props.urls;
82
+ this.greet = props.greet || "";
83
+ this.tunnelConfig = props.tunnelConfig;
84
+ this.disconnectInfo = props.disconnectInfo;
85
+ if(props.tunnelInstance){
86
+ this.tunnelInstance=props.tunnelInstance
87
+ }
88
+
89
+ this.exitPromise = new Promise((resolve) => {
90
+ this.exitPromiseResolve = resolve;
91
+ });
92
+
93
+ this.screen = blessed.screen({
94
+ smartCSR: true,
95
+ title: "Pinggy Tunnel",
96
+ fullUnicode: true,
97
+ });
98
+
99
+ this.setupStatsListener();
100
+ this.setupWebDebugger();
101
+ this.generateQrCodes();
102
+ this.createUI();
103
+ this.setupKeyBindings();
104
+ }
105
+
106
+ private setupStatsListener() {
107
+ globalThis.__PINGGY_TUNNEL_STATS__ = (newStats: TunnelUsageType) => {
108
+ this.stats = { ...newStats };
109
+ this.updateStatsDisplay();
110
+ };
111
+ }
112
+
113
+ private setupWebDebugger() {
114
+ if (this.tunnelConfig?.webDebugger) {
115
+ this.webDebuggerConnection = createWebDebuggerConnection(
116
+ this.tunnelConfig.webDebugger,
117
+ (pairs) => {
118
+ this.pairs = pairs;
119
+ this.updateRequestsDisplay();
120
+ }
121
+ );
122
+ }
123
+ }
124
+
125
+ private async generateQrCodes() {
126
+ if (this.tunnelConfig?.qrCode && this.urls.length > 0) {
127
+ this.qrCodes = await createQrCodes(this.urls);
128
+ this.updateQrCodeDisplay();
129
+ }
130
+ }
131
+
132
+ // Create the UI based on terminal size
133
+ private createUI() {
134
+ this.buildUI();
135
+
136
+ this.screen.on("resize", () => {
137
+ this.handleResize();
138
+ });
139
+ }
140
+
141
+ private buildUI() {
142
+ const width = this.screen.width as number;
143
+
144
+ if (width < MIN_WIDTH_WARNING) {
145
+ this.uiElements = {
146
+ mainContainer: createWarningUI(this.screen),
147
+ urlsBox: null as any,
148
+ statsBox: null as any,
149
+ requestsBox: null as any,
150
+ footerBox: null as any,
151
+ warningBox: createWarningUI(this.screen),
152
+ };
153
+ this.screen.render();
154
+ return;
155
+ }
156
+
157
+ if (width < SIMPLE_LAYOUT_THRESHOLD) {
158
+ this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
159
+ } else {
160
+ this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
161
+ }
162
+
163
+ this.refreshDisplays();
164
+ this.screen.render();
165
+ }
166
+
167
+ private refreshDisplays() {
168
+ this.updateUrlsDisplay();
169
+ this.updateStatsDisplay();
170
+ this.updateRequestsDisplay();
171
+ this.updateQrCodeDisplay();
172
+ }
173
+
174
+ private updateUrlsDisplay() {
175
+ updateUrlsDisplay(
176
+ this.uiElements?.urlsBox,
177
+ this.screen,
178
+ this.urls,
179
+ this.currentQrIndex
180
+ );
181
+ }
182
+
183
+ private updateStatsDisplay() {
184
+ updateStatsDisplay(
185
+ this.uiElements?.statsBox,
186
+ this.screen,
187
+ this.stats
188
+ );
189
+ }
190
+
191
+ private updateRequestsDisplay() {
192
+ updateRequestsDisplay(
193
+ this.uiElements?.requestsBox,
194
+ this.screen,
195
+ this.pairs,
196
+ this.selectedIndex
197
+ );
198
+ }
199
+
200
+ private updateQrCodeDisplay() {
201
+ updateQrCodeDisplay(
202
+ this.uiElements?.qrCodeBox,
203
+ this.screen,
204
+ this.qrCodes,
205
+ this.urls,
206
+ this.currentQrIndex
207
+ );
208
+ }
209
+
210
+ private setupKeyBindings() {
211
+ const self = this;
212
+
213
+ // Create a state object with getters to always get current values
214
+ const state: KeyBindingsState = {
215
+ get currentQrIndex() { return self.currentQrIndex; },
216
+ set currentQrIndex(value: number) { self.currentQrIndex = value; },
217
+ get selectedIndex() { return self.selectedIndex; },
218
+ set selectedIndex(value: number) { self.selectedIndex = value; },
219
+ get pairs() { return self.pairs; },
220
+ get urls() { return self.urls; },
221
+ };
222
+
223
+ const callbacks: KeyBindingsCallbacks = {
224
+ onQrIndexChange: (index: number) => {
225
+ self.currentQrIndex = index;
226
+ },
227
+ onSelectedIndexChange: (index: number) => {
228
+ self.selectedIndex = index;
229
+ },
230
+ onDestroy: () => self.destroy(),
231
+ updateUrlsDisplay: () => self.updateUrlsDisplay(),
232
+ updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
233
+ updateRequestsDisplay: () => self.updateRequestsDisplay(),
234
+ };
235
+
236
+ setupKeyBindings(
237
+ this.screen,
238
+ this.modalManager,
239
+ state,
240
+ callbacks,
241
+ this.tunnelConfig,
242
+ this.tunnelInstance
243
+ );
244
+ }
245
+
246
+ private handleResize() {
247
+ // Destroy current UI and recreate based on new size
248
+ this.screen.children.forEach((child) => child.destroy());
249
+ this.buildUI();
250
+ }
251
+
252
+ public updateDisconnectInfo(info: TunnelAppProps["disconnectInfo"]) {
253
+ this.disconnectInfo = info;
254
+ if (info?.disconnected) {
255
+ const message = info.error
256
+ ? `Error: ${info.error}\nTunnel will be closed.`
257
+ : info.messages?.join('\n') || 'Disconnect request received. Tunnel will be closed.';
258
+
259
+ showDisconnectModal(
260
+ this.screen,
261
+ this.modalManager,
262
+ message,
263
+ () => this.destroy()
264
+ );
265
+ }
266
+ }
267
+
268
+ public start() {
269
+ this.screen.render();
270
+ }
271
+
272
+ public waitUntilExit(): Promise<void> {
273
+ return this.exitPromise;
274
+ }
275
+
276
+ public destroy() {
277
+ // Stop the tunnel first
278
+ if (this.tunnelInstance?.tunnelid) {
279
+ const manager = TunnelManager.getInstance();
280
+ manager.stopTunnel(this.tunnelInstance.tunnelid);
281
+ }
282
+
283
+ // Cleanup
284
+ delete globalThis.__PINGGY_TUNNEL_STATS__;
285
+
286
+ if (this.webDebuggerConnection) {
287
+ this.webDebuggerConnection.close();
288
+ }
289
+
290
+ this.screen.destroy();
291
+
292
+ if (this.exitPromiseResolve) {
293
+ this.exitPromiseResolve();
294
+ }
295
+ }
296
+ }
297
+
298
+ export default TunnelTui;
@@ -0,0 +1,118 @@
1
+ import blessed from "blessed";
2
+ import { TunnelUsageType } from "@pinggy/pinggy";
3
+ import { ReqResPair } from "../../../types.js";
4
+ import { getBytesInt, getStatusColor } from "../../ink/utils/utils.js";
5
+
6
+ /**
7
+ * Updates the URLs display box
8
+ */
9
+ export function updateUrlsDisplay(
10
+ urlsBox: blessed.Widgets.BoxElement | undefined,
11
+ screen: blessed.Widgets.Screen,
12
+ urls: string[],
13
+ currentQrIndex: number
14
+ ): void {
15
+ if (!urlsBox) return;
16
+
17
+ let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}\n";
18
+ urls.forEach((url, index) => {
19
+ const isSelected = index === currentQrIndex;
20
+ const prefix = isSelected ? "→ " : "• ";
21
+ const color = isSelected ? "yellow" : "magenta";
22
+
23
+ if (isSelected) {
24
+ content += `{bold}{${color}-fg}${prefix}${url}{/${color}-fg}{/bold}\n`;
25
+ } else {
26
+ content += `{${color}-fg}${prefix}${url}{/${color}-fg}\n`;
27
+ }
28
+ });
29
+
30
+ urlsBox.setContent(content);
31
+ screen.render();
32
+ }
33
+
34
+ /**
35
+ * Updates the stats display box
36
+ */
37
+ export function updateStatsDisplay(
38
+ statsBox: blessed.Widgets.BoxElement | undefined,
39
+ screen: blessed.Widgets.Screen,
40
+ stats: TunnelUsageType
41
+ ): void {
42
+ if (!statsBox) return;
43
+
44
+ const content = `{green-fg}{bold}Live Stats{/bold}{/green-fg}
45
+ Elapsed: ${stats.elapsedTime}s
46
+ Live Connections: ${stats.numLiveConnections}
47
+ Total Connections: ${stats.numTotalConnections}
48
+ Request: ${getBytesInt(stats.numTotalReqBytes)}
49
+ Response: ${getBytesInt(stats.numTotalResBytes)}
50
+ Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
51
+
52
+ statsBox.setContent(content);
53
+ statsBox.style = { ...statsBox.style };
54
+ (statsBox as any).parseContent();
55
+ screen.render();
56
+ }
57
+
58
+ /**
59
+ * Updates the requests display box
60
+ */
61
+ export function updateRequestsDisplay(
62
+ requestsBox: blessed.Widgets.BoxElement | undefined,
63
+ screen: blessed.Widgets.Screen,
64
+ pairs: Map<number, ReqResPair>,
65
+ selectedIndex: number
66
+ ): void {
67
+ if (!requestsBox) return;
68
+
69
+ const allPairs = [...pairs.values()];
70
+ const visiblePairs = allPairs.slice(-10);
71
+ const startIndex = allPairs.length - visiblePairs.length;
72
+
73
+ let content = "{yellow-fg}HTTP Requests:{/yellow-fg}\n";
74
+
75
+ visiblePairs.forEach((pair, i) => {
76
+ const globalIndex = startIndex + i;
77
+ const isSelected = selectedIndex === globalIndex;
78
+ const prefix = isSelected ? "> " : " ";
79
+ const method = pair.request?.method || "";
80
+ const uri = pair.request?.uri || "";
81
+ const status = pair.response?.status || "";
82
+ const statusColor = getStatusColor(String(status));
83
+
84
+ if (isSelected) {
85
+ content += `{cyan-fg}${prefix}${method} ${status} ${uri}{/cyan-fg}\n`;
86
+ } else if (pair.response) {
87
+ content += `{${statusColor}-fg}${prefix}${method} ${status} ${uri}{/${statusColor}-fg}\n`;
88
+ } else {
89
+ content += `${prefix}${method} ...${uri}\n`;
90
+ }
91
+ });
92
+
93
+ requestsBox.setContent(content);
94
+ screen.render();
95
+ }
96
+
97
+ /**
98
+ * Updates the QR code display box
99
+ */
100
+ export function updateQrCodeDisplay(
101
+ qrCodeBox: blessed.Widgets.BoxElement | undefined,
102
+ screen: blessed.Widgets.Screen,
103
+ qrCodes: string[],
104
+ urls: string[],
105
+ currentQrIndex: number
106
+ ): void {
107
+ if (!qrCodeBox || qrCodes.length === 0) return;
108
+
109
+ let content = `{green-fg}{bold}QR Code ${currentQrIndex + 1}/${urls.length}{/bold}{/green-fg}\n`;
110
+ if (urls.length > 1) {
111
+ content += "\n{yellow-fg}← → to switch QR codes{/yellow-fg}\n";
112
+ }
113
+ content += qrCodes[currentQrIndex] || "";
114
+ qrCodeBox.setContent(content);
115
+ qrCodeBox.style = { ...qrCodeBox.style };
116
+ (qrCodeBox as any).parseContent();
117
+ screen.render();
118
+ }
@@ -0,0 +1,134 @@
1
+ import blessed from "blessed";
2
+ import { ReqResPair, FinalConfig } from "../../../types.js";
3
+ import { ManagedTunnel, TunnelManager } from "../../../tunnel_manager/TunnelManager.js";
4
+ import { fetchReqResHeaders } from "../headerFetcher.js";
5
+ import { logger } from "../../../logger.js";
6
+ import { ModalManager, showDetailModal, closeDetailModal, showKeyBindingsModal, closeKeyBindingsModal } from "./Modals.js";
7
+
8
+ export interface KeyBindingsState {
9
+ currentQrIndex: number;
10
+ selectedIndex: number;
11
+ pairs: Map<number, ReqResPair>;
12
+ urls: string[];
13
+ }
14
+
15
+ export interface KeyBindingsCallbacks {
16
+ onQrIndexChange: (index: number) => void;
17
+ onSelectedIndexChange: (index: number) => void;
18
+ onDestroy: () => void;
19
+ updateUrlsDisplay: () => void;
20
+ updateQrCodeDisplay: () => void;
21
+ updateRequestsDisplay: () => void;
22
+ }
23
+
24
+ /**
25
+ * Sets up all key bindings for the TUI
26
+ */
27
+ export function setupKeyBindings(
28
+ screen: blessed.Widgets.Screen,
29
+ modalManager: ModalManager,
30
+ state: KeyBindingsState,
31
+ callbacks: KeyBindingsCallbacks,
32
+ tunnelConfig?: FinalConfig,
33
+ tunnelInstance?: ManagedTunnel
34
+ ): void {
35
+ // Exit on Ctrl+C
36
+ screen.key(["C-c"], () => {
37
+ const manager = TunnelManager.getInstance();
38
+ manager.stopTunnel(tunnelInstance?.tunnelid || "");
39
+ callbacks.onDestroy();
40
+ process.exit(0);
41
+ });
42
+
43
+ // Escape key
44
+ screen.key(["escape"], () => {
45
+ if (modalManager.inDetailView) {
46
+ closeDetailModal(screen, modalManager);
47
+ return;
48
+ }
49
+ if (modalManager.keyBindingView) {
50
+ closeKeyBindingsModal(screen, modalManager);
51
+ return;
52
+ }
53
+ });
54
+
55
+ // Navigation - Up
56
+ screen.key(["up"], () => {
57
+ if (modalManager.inDetailView || modalManager.keyBindingView) return;
58
+ if (state.selectedIndex > 0) {
59
+ callbacks.onSelectedIndexChange(state.selectedIndex - 1);
60
+ callbacks.updateRequestsDisplay();
61
+ }
62
+ });
63
+
64
+ // Navigation - Down
65
+ screen.key(["down"], () => {
66
+ if (modalManager.inDetailView || modalManager.keyBindingView) return;
67
+ const allPairs = [...state.pairs.values()];
68
+ if (state.selectedIndex < allPairs.length - 1) {
69
+ callbacks.onSelectedIndexChange(state.selectedIndex + 1);
70
+ callbacks.updateRequestsDisplay();
71
+ }
72
+ });
73
+
74
+ // Enter to view details
75
+ screen.key(["enter"], async () => {
76
+ if (modalManager.inDetailView || modalManager.keyBindingView) return;
77
+ const allPairs = [...state.pairs.values()];
78
+ const pair = allPairs[state.selectedIndex];
79
+ if (pair?.request?.key !== undefined && pair?.request?.key !== null) {
80
+ try {
81
+ const headers = await fetchReqResHeaders(
82
+ tunnelConfig?.webDebugger || "",
83
+ pair.request.key
84
+ );
85
+ showDetailModal(screen, modalManager, headers.req, headers.res);
86
+ } catch (err) {
87
+ logger.error("Fetch error:", err);
88
+ }
89
+ }
90
+ });
91
+
92
+ // Help toggle
93
+ screen.key(["h"], () => {
94
+ if (modalManager.inDetailView) return;
95
+ if (modalManager.keyBindingView) {
96
+ closeKeyBindingsModal(screen, modalManager);
97
+ } else {
98
+ showKeyBindingsModal(screen, modalManager);
99
+ }
100
+ });
101
+
102
+ // Copy URL
103
+ screen.key(["c"], async () => {
104
+ if (modalManager.inDetailView || modalManager.keyBindingView) return;
105
+ if (state.urls.length > 0) {
106
+ try {
107
+ const clipboardy = await import("clipboardy");
108
+ clipboardy.default.writeSync(state.urls[state.currentQrIndex]);
109
+ } catch (err) {
110
+ logger.error("Failed to copy to clipboard:", err);
111
+ }
112
+ }
113
+ });
114
+
115
+ // QR code navigation - Left
116
+ screen.key(["left"], () => {
117
+ if (modalManager.inDetailView || modalManager.keyBindingView) return;
118
+ if (state.currentQrIndex > 0) {
119
+ callbacks.onQrIndexChange(state.currentQrIndex - 1);
120
+ callbacks.updateUrlsDisplay();
121
+ callbacks.updateQrCodeDisplay();
122
+ }
123
+ });
124
+
125
+ // QR code navigation - Right
126
+ screen.key(["right"], () => {
127
+ if (modalManager.inDetailView || modalManager.keyBindingView) return;
128
+ if (state.currentQrIndex < state.urls.length - 1) {
129
+ callbacks.onQrIndexChange(state.currentQrIndex + 1);
130
+ callbacks.updateUrlsDisplay();
131
+ callbacks.updateQrCodeDisplay();
132
+ }
133
+ });
134
+ }