pinggy 0.3.4 → 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 +1 -1
- package/dist/chunk-65R2GMKQ.js +2101 -0
- package/dist/index.cjs +1814 -1362
- package/dist/index.d.cts +616 -0
- package/dist/index.d.ts +616 -0
- package/dist/index.js +38 -55
- package/dist/{main-CZY6GID4.js → main-2QDG7PWL.js} +229 -1726
- package/package.json +3 -4
- package/.github/workflows/npm-publish-github-packages.yml +0 -34
- package/.github/workflows/publish-binaries.yml +0 -223
- package/Makefile +0 -4
- package/caxa_build.js +0 -24
- package/dist/chunk-T5ESYDJY.js +0 -121
- package/ent.plist +0 -14
- package/jest.config.js +0 -19
- package/src/_tests_/build_config.test.ts +0 -91
- package/src/cli/buildConfig.ts +0 -475
- package/src/cli/defaults.ts +0 -20
- package/src/cli/extendedOptions.ts +0 -153
- package/src/cli/help.ts +0 -43
- package/src/cli/options.ts +0 -50
- package/src/cli/starCli.ts +0 -229
- package/src/index.ts +0 -30
- package/src/logger.ts +0 -138
- package/src/main.ts +0 -87
- package/src/remote_management/handler.ts +0 -244
- package/src/remote_management/remoteManagement.ts +0 -226
- package/src/remote_management/remote_schema.ts +0 -176
- package/src/remote_management/websocket_handlers.ts +0 -180
- package/src/tui/blessed/TunnelTui.ts +0 -340
- package/src/tui/blessed/components/DisplayUpdaters.ts +0 -189
- package/src/tui/blessed/components/KeyBindings.ts +0 -236
- package/src/tui/blessed/components/Modals.ts +0 -302
- package/src/tui/blessed/components/UIComponents.ts +0 -306
- package/src/tui/blessed/components/index.ts +0 -4
- package/src/tui/blessed/config.ts +0 -53
- package/src/tui/blessed/headerFetcher.ts +0 -42
- package/src/tui/blessed/index.ts +0 -2
- package/src/tui/blessed/qrCodeGenerator.ts +0 -20
- package/src/tui/blessed/webDebuggerConnection.ts +0 -128
- package/src/tui/ink/asciArt.ts +0 -7
- package/src/tui/ink/hooks/useQrCodes.ts +0 -27
- package/src/tui/ink/hooks/useReqResHeaders.ts +0 -27
- package/src/tui/ink/hooks/useTerminalSize.ts +0 -26
- package/src/tui/ink/hooks/useTerminalStats.ts +0 -24
- package/src/tui/ink/hooks/useWebDebugger.ts +0 -98
- package/src/tui/ink/index.tsx +0 -243
- package/src/tui/ink/layout/Borders.tsx +0 -15
- package/src/tui/ink/layout/Container.tsx +0 -15
- package/src/tui/ink/sections/DebuggerDetailModal.tsx +0 -53
- package/src/tui/ink/sections/KeyBindings.tsx +0 -58
- package/src/tui/ink/sections/QrCodeSection.tsx +0 -28
- package/src/tui/ink/sections/StatsSection.tsx +0 -20
- package/src/tui/ink/sections/URLsSection.tsx +0 -53
- package/src/tui/ink/utils/utils.ts +0 -35
- package/src/tui/spinner/spinner.ts +0 -64
- package/src/tunnel_manager/TunnelManager.ts +0 -1212
- package/src/types.ts +0 -255
- package/src/utils/FileServer.ts +0 -112
- package/src/utils/detect_vc_redist_on_windows.ts +0 -167
- package/src/utils/getFreePort.ts +0 -41
- package/src/utils/htmlTemplates.ts +0 -146
- package/src/utils/parseArgs.ts +0 -79
- package/src/utils/printer.ts +0 -81
- package/src/utils/util.ts +0 -18
- package/src/workers/file_serve_worker.ts +0 -33
- package/tsconfig.json +0 -17
- package/tsup.config.ts +0 -12
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import WebSocket from "ws";
|
|
2
|
-
import { logger } from "../logger.js";
|
|
3
|
-
import { ErrorCode, NewErrorResponseObject, ResponseObj, ErrorResponse, isErrorResponse, NewResponseObject } from "../types.js";
|
|
4
|
-
import { TunnelOperations, TunnelResponse } from "./handler.js";
|
|
5
|
-
import { GetSchema, RestartSchema, StartSchema, StopSchema, UpdateConfigSchema } from "./remote_schema.js";
|
|
6
|
-
import z from "zod";
|
|
7
|
-
import CLIPrinter from "../utils/printer.js";
|
|
8
|
-
|
|
9
|
-
export interface ConnectionStatus {
|
|
10
|
-
success: boolean;
|
|
11
|
-
error_code?: number;
|
|
12
|
-
error_msg?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface WebSocketRequest {
|
|
16
|
-
requestid: string;
|
|
17
|
-
command: string;
|
|
18
|
-
data?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
type CommandName = "start" | "stop" | "get" | "restart" | "updateconfig" | "list";
|
|
22
|
-
|
|
23
|
-
export class WebSocketCommandHandler {
|
|
24
|
-
private tunnelHandler = new TunnelOperations();
|
|
25
|
-
|
|
26
|
-
private safeParse(text?: string): unknown {
|
|
27
|
-
if (!text) return undefined;
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(text);
|
|
30
|
-
} catch (e) {
|
|
31
|
-
logger.warn("Invalid JSON payload", { error: String(e), text });
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
private sendResponse(ws: WebSocket, resp: ResponseObj) {
|
|
37
|
-
const payload = {
|
|
38
|
-
...resp,
|
|
39
|
-
response: Buffer.from(resp.response || []).toString("base64"),
|
|
40
|
-
};
|
|
41
|
-
ws.send(JSON.stringify(payload));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private sendError(ws: WebSocket, req: Partial<WebSocketRequest>, message: string, code = ErrorCode.InternalServerError) {
|
|
45
|
-
const resp = NewErrorResponseObject({ code, message });
|
|
46
|
-
resp.command = req.command || "";
|
|
47
|
-
resp.requestid = req.requestid || "";
|
|
48
|
-
this.sendResponse(ws, resp);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private async handleStartReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
|
|
52
|
-
const dc = StartSchema.parse(raw);
|
|
53
|
-
CLIPrinter.info("Starting tunnel with config name: " + dc.tunnelConfig.configname);
|
|
54
|
-
const result = await this.tunnelHandler.handleStart(dc.tunnelConfig);
|
|
55
|
-
return this.wrapResponse(result, req);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
private async handleStopReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
|
|
59
|
-
const dc = StopSchema.parse(raw);
|
|
60
|
-
CLIPrinter.info("Stopping tunnel with ID: " + dc.tunnelID);
|
|
61
|
-
const result = await this.tunnelHandler.handleStop(dc.tunnelID);
|
|
62
|
-
return this.wrapResponse(result, req);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private async handleGetReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
|
|
66
|
-
const dc = GetSchema.parse(raw);
|
|
67
|
-
const result = await this.tunnelHandler.handleGet(dc.tunnelID);
|
|
68
|
-
return this.wrapResponse(result, req);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private async handleRestartReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
|
|
72
|
-
const dc = RestartSchema.parse(raw);
|
|
73
|
-
const result = await this.tunnelHandler.handleRestart(dc.tunnelID);
|
|
74
|
-
return this.wrapResponse(result, req);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
private async handleUpdateConfigReq(req: WebSocketRequest, raw: unknown): Promise<ResponseObj> {
|
|
78
|
-
const dc = UpdateConfigSchema.parse(raw);
|
|
79
|
-
const result = await this.tunnelHandler.handleUpdateConfig(dc.tunnelConfig);
|
|
80
|
-
return this.wrapResponse(result, req);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private async handleListReq(req: WebSocketRequest): Promise<ResponseObj> {
|
|
84
|
-
const result = await this.tunnelHandler.handleList();
|
|
85
|
-
return this.wrapResponse(result, req);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private wrapResponse(result: ResponseObj | ErrorResponse | TunnelResponse | TunnelResponse[], req: WebSocketRequest): ResponseObj {
|
|
89
|
-
if (isErrorResponse(result)) {
|
|
90
|
-
const errResp = NewErrorResponseObject(result);
|
|
91
|
-
errResp.command = req.command;
|
|
92
|
-
errResp.requestid = req.requestid;
|
|
93
|
-
return errResp;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Temporary workaround to remove allowPreflight from response
|
|
97
|
-
const finalResult = JSON.parse(JSON.stringify(result));
|
|
98
|
-
if (Array.isArray(finalResult)) {
|
|
99
|
-
finalResult.forEach(item => {
|
|
100
|
-
if (item?.tunnelconfig) {
|
|
101
|
-
delete item.tunnelconfig.allowPreflight;
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
} else if (finalResult?.tunnelconfig) {
|
|
105
|
-
delete finalResult.tunnelconfig.allowPreflight;
|
|
106
|
-
}
|
|
107
|
-
const respObj = NewResponseObject(finalResult);
|
|
108
|
-
respObj.command = req.command;
|
|
109
|
-
respObj.requestid = req.requestid;
|
|
110
|
-
return respObj;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async handle(ws: WebSocket, req: WebSocketRequest) {
|
|
114
|
-
const cmd = (req.command || "").toLowerCase() as CommandName | string;
|
|
115
|
-
const raw = this.safeParse(req.data);
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
let response: ResponseObj;
|
|
119
|
-
switch (cmd as CommandName) {
|
|
120
|
-
case "start": {
|
|
121
|
-
response = await this.handleStartReq(req, raw);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case "stop": {
|
|
125
|
-
response = await this.handleStopReq(req, raw);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
case "get": {
|
|
129
|
-
response = await this.handleGetReq(req, raw);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case "restart": {
|
|
133
|
-
response = await this.handleRestartReq(req, raw);
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
case "updateconfig": {
|
|
137
|
-
response = await this.handleUpdateConfigReq(req, raw);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
case "list": {
|
|
141
|
-
response = await this.handleListReq(req);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
144
|
-
default:
|
|
145
|
-
if (typeof req.command === 'string') {
|
|
146
|
-
logger.warn("Unknown command", { command: req.command });
|
|
147
|
-
}
|
|
148
|
-
return this.sendError(ws, req, "Invalid command");
|
|
149
|
-
}
|
|
150
|
-
logger.debug("Sending response", { command: response.command, requestid: response.requestid });
|
|
151
|
-
this.sendResponse(ws, response);
|
|
152
|
-
} catch (e: any) {
|
|
153
|
-
if (e instanceof z.ZodError) {
|
|
154
|
-
logger.warn("Validation failed", { cmd, issues: e.issues });
|
|
155
|
-
return this.sendError(ws, req, "Invalid request data", ErrorCode.InvalidBodyFormatError);
|
|
156
|
-
}
|
|
157
|
-
logger.error("Error handling command", { cmd, error: String(e) });
|
|
158
|
-
return this.sendError(ws, req, e?.message || "Internal error");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// // Returns true if connection status is OK else sends logs and returns false
|
|
164
|
-
export function handleConnectionStatusMessage(firstMessage: WebSocket.Data): boolean {
|
|
165
|
-
try {
|
|
166
|
-
const text = typeof firstMessage === 'string' ? firstMessage : firstMessage.toString();
|
|
167
|
-
const cs = JSON.parse(text) as ConnectionStatus;
|
|
168
|
-
if (!cs.success) {
|
|
169
|
-
const msg = cs.error_msg || "Connection failed";
|
|
170
|
-
CLIPrinter.warn(`Connection failed: ${msg}`);
|
|
171
|
-
logger.warn("Remote management connection failed", { error_code: cs.error_code, error_msg: msg });
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
return true;
|
|
175
|
-
} catch (e) {
|
|
176
|
-
logger.warn("Failed to parse connection status message", { error: String(e) });
|
|
177
|
-
// If parsing fails, assume connection is okay
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
@@ -1,340 +0,0 @@
|
|
|
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 = -1; // -1 means no selection
|
|
53
|
-
private selectedRequestKey: number | null = null; // Track selected request by key
|
|
54
|
-
private qrCodes: string[] = [];
|
|
55
|
-
private stats: TunnelUsageType = {
|
|
56
|
-
elapsedTime: 0,
|
|
57
|
-
numLiveConnections: 0,
|
|
58
|
-
numTotalConnections: 0,
|
|
59
|
-
numTotalReqBytes: 0,
|
|
60
|
-
numTotalResBytes: 0,
|
|
61
|
-
numTotalTxBytes: 0,
|
|
62
|
-
};
|
|
63
|
-
private pairs: ReqResPair[] = [];
|
|
64
|
-
private webDebuggerConnection: WebDebuggerConnection | null = null;
|
|
65
|
-
|
|
66
|
-
// UI Elements
|
|
67
|
-
private uiElements!: UIElements;
|
|
68
|
-
private modalManager: ModalManager = {
|
|
69
|
-
detailModal: null,
|
|
70
|
-
keyBindingsModal: null,
|
|
71
|
-
disconnectModal: null,
|
|
72
|
-
inDetailView: false,
|
|
73
|
-
keyBindingView: false,
|
|
74
|
-
inDisconnectView: false,
|
|
75
|
-
loadingBox: null,
|
|
76
|
-
loadingView: false,
|
|
77
|
-
fetchAbortController: null,
|
|
78
|
-
};
|
|
79
|
-
private tunnelInstance?: ManagedTunnel
|
|
80
|
-
|
|
81
|
-
private exitPromiseResolve: (() => void) | null = null;
|
|
82
|
-
private exitPromise: Promise<void>;
|
|
83
|
-
|
|
84
|
-
constructor(props: TunnelAppProps) {
|
|
85
|
-
this.urls = props.urls;
|
|
86
|
-
this.greet = props.greet || "";
|
|
87
|
-
this.tunnelConfig = props.tunnelConfig;
|
|
88
|
-
this.disconnectInfo = props.disconnectInfo;
|
|
89
|
-
if(props.tunnelInstance){
|
|
90
|
-
this.tunnelInstance=props.tunnelInstance
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
this.exitPromise = new Promise((resolve) => {
|
|
94
|
-
this.exitPromiseResolve = resolve;
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
this.screen = blessed.screen({
|
|
98
|
-
smartCSR: true,
|
|
99
|
-
title: "Pinggy Tunnel",
|
|
100
|
-
fullUnicode: true,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
this.setupStatsListener();
|
|
104
|
-
this.setupWebDebugger();
|
|
105
|
-
this.generateQrCodes();
|
|
106
|
-
this.createUI();
|
|
107
|
-
this.setupKeyBindings();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
private setupStatsListener() {
|
|
111
|
-
globalThis.__PINGGY_TUNNEL_STATS__ = (newStats: TunnelUsageType) => {
|
|
112
|
-
this.stats = { ...newStats };
|
|
113
|
-
this.updateStatsDisplay();
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private clearSelection() {
|
|
118
|
-
this.selectedIndex = -1;
|
|
119
|
-
this.selectedRequestKey = null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private setupWebDebugger() {
|
|
123
|
-
if (this.tunnelConfig?.webDebugger) {
|
|
124
|
-
this.webDebuggerConnection = createWebDebuggerConnection(
|
|
125
|
-
this.tunnelConfig.webDebugger,
|
|
126
|
-
(pairs) => {
|
|
127
|
-
this.pairs = pairs;
|
|
128
|
-
|
|
129
|
-
// If there's a selected request key, find its new index
|
|
130
|
-
if (this.selectedRequestKey !== null) {
|
|
131
|
-
const newIndex = pairs.findIndex(
|
|
132
|
-
(pair) => pair.request?.key === this.selectedRequestKey
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
if (newIndex !== -1) {
|
|
136
|
-
// Request still exists, update index
|
|
137
|
-
this.selectedIndex = newIndex;
|
|
138
|
-
} else {
|
|
139
|
-
// Request no longer exists, clear selection
|
|
140
|
-
this.clearSelection();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
this.updateRequestsDisplay();
|
|
145
|
-
}
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private async generateQrCodes() {
|
|
151
|
-
if (this.tunnelConfig?.qrCode && this.urls.length > 0) {
|
|
152
|
-
this.qrCodes = await createQrCodes(this.urls);
|
|
153
|
-
this.updateQrCodeDisplay();
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Create the UI based on terminal size
|
|
158
|
-
private createUI() {
|
|
159
|
-
this.buildUI();
|
|
160
|
-
|
|
161
|
-
this.screen.on("resize", () => {
|
|
162
|
-
this.handleResize();
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private buildUI() {
|
|
167
|
-
const width = this.screen.width as number;
|
|
168
|
-
|
|
169
|
-
if (width < MIN_WIDTH_WARNING) {
|
|
170
|
-
this.uiElements = {
|
|
171
|
-
mainContainer: createWarningUI(this.screen),
|
|
172
|
-
urlsBox: null as any,
|
|
173
|
-
statsBox: null as any,
|
|
174
|
-
requestsBox: null as any,
|
|
175
|
-
footerBox: null as any,
|
|
176
|
-
warningBox: createWarningUI(this.screen),
|
|
177
|
-
};
|
|
178
|
-
this.screen.render();
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (width < SIMPLE_LAYOUT_THRESHOLD) {
|
|
183
|
-
this.uiElements = createSimpleUI(this.screen, this.urls, this.greet);
|
|
184
|
-
} else {
|
|
185
|
-
this.uiElements = createFullUI(this.screen, this.urls, this.greet, this.tunnelConfig);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
this.refreshDisplays();
|
|
189
|
-
this.screen.render();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
private refreshDisplays() {
|
|
193
|
-
this.updateUrlsDisplay();
|
|
194
|
-
this.updateStatsDisplay();
|
|
195
|
-
this.updateRequestsDisplay();
|
|
196
|
-
this.updateQrCodeDisplay();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private updateUrlsDisplay() {
|
|
200
|
-
updateUrlsDisplay(
|
|
201
|
-
this.uiElements?.urlsBox,
|
|
202
|
-
this.screen,
|
|
203
|
-
this.urls,
|
|
204
|
-
this.currentQrIndex
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
private updateStatsDisplay() {
|
|
209
|
-
updateStatsDisplay(
|
|
210
|
-
this.uiElements?.statsBox,
|
|
211
|
-
this.screen,
|
|
212
|
-
this.stats
|
|
213
|
-
);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
private updateRequestsDisplay() {
|
|
217
|
-
const result = updateRequestsDisplay(
|
|
218
|
-
this.uiElements?.requestsBox,
|
|
219
|
-
this.screen,
|
|
220
|
-
this.pairs,
|
|
221
|
-
this.selectedIndex
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
// Update selectedIndex if it was adjusted due to trimming
|
|
225
|
-
if (result.adjustedSelectedIndex !== this.selectedIndex) {
|
|
226
|
-
if (result.adjustedSelectedIndex === -1) {
|
|
227
|
-
// Selection was cleared due to trimming
|
|
228
|
-
this.clearSelection();
|
|
229
|
-
} else {
|
|
230
|
-
// Update to new index
|
|
231
|
-
this.selectedIndex = result.adjustedSelectedIndex;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Update pairs if they were trimmed (different reference means trimming occurred)
|
|
236
|
-
if (result.trimmedPairs !== this.pairs) {
|
|
237
|
-
this.pairs = result.trimmedPairs;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private updateQrCodeDisplay() {
|
|
242
|
-
updateQrCodeDisplay(
|
|
243
|
-
this.uiElements?.qrCodeBox,
|
|
244
|
-
this.screen,
|
|
245
|
-
this.qrCodes,
|
|
246
|
-
this.urls,
|
|
247
|
-
this.currentQrIndex
|
|
248
|
-
);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
private setupKeyBindings() {
|
|
252
|
-
const self = this;
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
// Create a state object with getters to always get current values
|
|
256
|
-
const state: KeyBindingsState = {
|
|
257
|
-
get currentQrIndex() { return self.currentQrIndex; },
|
|
258
|
-
set currentQrIndex(value: number) { self.currentQrIndex = value; },
|
|
259
|
-
get selectedIndex() { return self.selectedIndex; },
|
|
260
|
-
set selectedIndex(value: number) { self.selectedIndex = value; },
|
|
261
|
-
get pairs() { return self.pairs; },
|
|
262
|
-
get urls() { return self.urls; },
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
const callbacks: KeyBindingsCallbacks = {
|
|
266
|
-
onQrIndexChange: (index: number) => {
|
|
267
|
-
self.currentQrIndex = index;
|
|
268
|
-
},
|
|
269
|
-
onSelectedIndexChange: (index: number, requestKey: number | null) => {
|
|
270
|
-
self.selectedIndex = index;
|
|
271
|
-
self.selectedRequestKey = requestKey;
|
|
272
|
-
},
|
|
273
|
-
onDestroy: () => self.destroy(),
|
|
274
|
-
updateUrlsDisplay: () => self.updateUrlsDisplay(),
|
|
275
|
-
updateQrCodeDisplay: () => self.updateQrCodeDisplay(),
|
|
276
|
-
updateRequestsDisplay: () => self.updateRequestsDisplay(),
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
setupKeyBindings(
|
|
280
|
-
this.screen,
|
|
281
|
-
this.modalManager,
|
|
282
|
-
state,
|
|
283
|
-
callbacks,
|
|
284
|
-
this.tunnelConfig,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private handleResize() {
|
|
289
|
-
// Destroy current UI and recreate based on new size
|
|
290
|
-
this.screen.children.forEach((child) => child.destroy());
|
|
291
|
-
this.buildUI();
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
public updateDisconnectInfo(info: TunnelAppProps["disconnectInfo"]) {
|
|
295
|
-
this.disconnectInfo = info;
|
|
296
|
-
if (info?.disconnected) {
|
|
297
|
-
const message = info.error
|
|
298
|
-
? `Error: ${info.error}\nTunnel will be closed.`
|
|
299
|
-
: info.messages?.join('\n') || 'Disconnect request received. Tunnel will be closed.';
|
|
300
|
-
|
|
301
|
-
showDisconnectModal(
|
|
302
|
-
this.screen,
|
|
303
|
-
this.modalManager,
|
|
304
|
-
message,
|
|
305
|
-
() => this.destroy()
|
|
306
|
-
);
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
public start() {
|
|
311
|
-
this.screen.render();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
public waitUntilExit(): Promise<void> {
|
|
315
|
-
return this.exitPromise;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
public destroy() {
|
|
319
|
-
// Stop the tunnel first
|
|
320
|
-
if (this.tunnelInstance?.tunnelid) {
|
|
321
|
-
const manager = TunnelManager.getInstance();
|
|
322
|
-
manager.stopTunnel(this.tunnelInstance.tunnelid);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Cleanup
|
|
326
|
-
delete globalThis.__PINGGY_TUNNEL_STATS__;
|
|
327
|
-
|
|
328
|
-
if (this.webDebuggerConnection) {
|
|
329
|
-
this.webDebuggerConnection.close();
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
this.screen.destroy();
|
|
333
|
-
|
|
334
|
-
if (this.exitPromiseResolve) {
|
|
335
|
-
this.exitPromiseResolve();
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
export default TunnelTui;
|
|
@@ -1,189 +0,0 @@
|
|
|
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
|
-
import { getTuiConfig } from "../config.js";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Updates the URLs display box
|
|
9
|
-
*/
|
|
10
|
-
export function updateUrlsDisplay(
|
|
11
|
-
urlsBox: blessed.Widgets.BoxElement | undefined,
|
|
12
|
-
screen: blessed.Widgets.Screen,
|
|
13
|
-
urls: string[],
|
|
14
|
-
currentQrIndex: number
|
|
15
|
-
): void {
|
|
16
|
-
if (!urlsBox) return;
|
|
17
|
-
|
|
18
|
-
let content = "{green-fg}{bold}Public URLs{/bold}{/green-fg}\n";
|
|
19
|
-
urls.forEach((url, index) => {
|
|
20
|
-
const isSelected = index === currentQrIndex;
|
|
21
|
-
const prefix = isSelected ? "→ " : "• ";
|
|
22
|
-
const color = isSelected ? "yellow" : "magenta";
|
|
23
|
-
|
|
24
|
-
if (isSelected) {
|
|
25
|
-
content += `{bold}{${color}-fg}${prefix}${url}{/${color}-fg}{/bold}\n`;
|
|
26
|
-
} else {
|
|
27
|
-
content += `{${color}-fg}${prefix}${url}{/${color}-fg}\n`;
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
urlsBox.setContent(content);
|
|
32
|
-
screen.render();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Updates the stats display box
|
|
37
|
-
*/
|
|
38
|
-
export function updateStatsDisplay(
|
|
39
|
-
statsBox: blessed.Widgets.BoxElement | undefined,
|
|
40
|
-
screen: blessed.Widgets.Screen,
|
|
41
|
-
stats: TunnelUsageType
|
|
42
|
-
): void {
|
|
43
|
-
if (!statsBox) return;
|
|
44
|
-
|
|
45
|
-
const content = `{green-fg}{bold}Live Stats{/bold}{/green-fg}
|
|
46
|
-
Elapsed: ${stats.elapsedTime}s
|
|
47
|
-
Live Connections: ${stats.numLiveConnections}
|
|
48
|
-
Total Connections: ${stats.numTotalConnections}
|
|
49
|
-
Request: ${getBytesInt(stats.numTotalReqBytes)}
|
|
50
|
-
Response: ${getBytesInt(stats.numTotalResBytes)}
|
|
51
|
-
Total Transfer: ${getBytesInt(stats.numTotalTxBytes)}`;
|
|
52
|
-
|
|
53
|
-
statsBox.setContent(content);
|
|
54
|
-
statsBox.style = { ...statsBox.style };
|
|
55
|
-
(statsBox as any).parseContent();
|
|
56
|
-
screen.render();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Updates the requests display box
|
|
61
|
-
* This function displays HTTP requests with reversed order (latest at top):
|
|
62
|
-
* - Limits the total pairs to maxRequestPairs (configurable)
|
|
63
|
-
* - Shows latest requests at top when no selection (selectedIndex = -1)
|
|
64
|
-
* - Ensures the selected item is always visible when there is selection
|
|
65
|
-
* - selectedIndex -1 means no selection, viewport shows top (latest requests)
|
|
66
|
-
*/
|
|
67
|
-
export function updateRequestsDisplay(
|
|
68
|
-
requestsBox: blessed.Widgets.BoxElement | undefined,
|
|
69
|
-
screen: blessed.Widgets.Screen,
|
|
70
|
-
pairs: ReqResPair[],
|
|
71
|
-
selectedIndex: number
|
|
72
|
-
): { adjustedSelectedIndex: number; trimmedPairs: ReqResPair[] } {
|
|
73
|
-
const config = getTuiConfig();
|
|
74
|
-
const { maxRequestPairs, visibleRequestCount, viewportScrollMargin } = config;
|
|
75
|
-
|
|
76
|
-
if (!requestsBox) {
|
|
77
|
-
return { adjustedSelectedIndex: selectedIndex, trimmedPairs: pairs };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// pairs array (latest first)
|
|
81
|
-
let allPairs = pairs;
|
|
82
|
-
let trimmedPairs = pairs;
|
|
83
|
-
|
|
84
|
-
if (allPairs.length > maxRequestPairs) {
|
|
85
|
-
// Keep only the first maxRequestPairs (which are the latest ones)
|
|
86
|
-
allPairs = allPairs.slice(0, maxRequestPairs);
|
|
87
|
-
trimmedPairs = allPairs;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const totalPairs = allPairs.length;
|
|
91
|
-
|
|
92
|
-
// Adjust selectedIndex if it's now out of bounds due to trimming
|
|
93
|
-
// If the selected item was trimmed, clear the selection
|
|
94
|
-
let adjustedSelectedIndex = selectedIndex;
|
|
95
|
-
if (adjustedSelectedIndex >= totalPairs) {
|
|
96
|
-
adjustedSelectedIndex = -1;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Calculate viewport window
|
|
100
|
-
let viewportStart: number;
|
|
101
|
-
|
|
102
|
-
if (totalPairs <= visibleRequestCount) {
|
|
103
|
-
// All pairs fit in the viewport
|
|
104
|
-
viewportStart = 0;
|
|
105
|
-
} else if (adjustedSelectedIndex === -1) {
|
|
106
|
-
// No selection: show latest requests (top of the list)
|
|
107
|
-
viewportStart = 0;
|
|
108
|
-
} else {
|
|
109
|
-
// Has selection: ensure selector is visible
|
|
110
|
-
viewportStart = 0;
|
|
111
|
-
|
|
112
|
-
// If selector would be below the visible area, scroll down
|
|
113
|
-
if (adjustedSelectedIndex >= visibleRequestCount - viewportScrollMargin) {
|
|
114
|
-
viewportStart = Math.min(
|
|
115
|
-
totalPairs - visibleRequestCount,
|
|
116
|
-
adjustedSelectedIndex - viewportScrollMargin
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// If selector would be above the visible area, scroll up
|
|
121
|
-
if (adjustedSelectedIndex < viewportStart + viewportScrollMargin) {
|
|
122
|
-
viewportStart = Math.max(0, adjustedSelectedIndex - viewportScrollMargin);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const viewportEnd = Math.min(viewportStart + visibleRequestCount, totalPairs);
|
|
127
|
-
const visiblePairs = allPairs.slice(viewportStart, viewportEnd);
|
|
128
|
-
|
|
129
|
-
let content = "{yellow-fg}HTTP Requests:{/yellow-fg}";
|
|
130
|
-
|
|
131
|
-
// Show scroll indicator if there are items above the viewport
|
|
132
|
-
if (viewportStart > 0) {
|
|
133
|
-
content += ` {gray-fg}↑ ${viewportStart} more{/gray-fg}`;
|
|
134
|
-
}
|
|
135
|
-
content += "\n";
|
|
136
|
-
|
|
137
|
-
visiblePairs.forEach((pair, i) => {
|
|
138
|
-
const globalIndex = viewportStart + i;
|
|
139
|
-
const isSelected = adjustedSelectedIndex !== -1 && adjustedSelectedIndex === globalIndex;
|
|
140
|
-
const prefix = isSelected ? "> " : " ";
|
|
141
|
-
const method = pair.request?.method || "";
|
|
142
|
-
const uri = pair.request?.uri || "";
|
|
143
|
-
const status = pair.response?.status || "";
|
|
144
|
-
const statusColor = getStatusColor(String(status));
|
|
145
|
-
|
|
146
|
-
if (isSelected) {
|
|
147
|
-
content += `{cyan-fg}${prefix}${method} ${status} ${uri}{/cyan-fg}\n`;
|
|
148
|
-
} else if (pair.response) {
|
|
149
|
-
content += `{${statusColor}-fg}${prefix}${method} ${status} ${uri}{/${statusColor}-fg}\n`;
|
|
150
|
-
} else {
|
|
151
|
-
content += `${prefix}${method} ...${uri}\n`;
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
// Show scroll indicator if there are items below the viewport
|
|
156
|
-
const itemsBelow = totalPairs - viewportEnd;
|
|
157
|
-
if (itemsBelow > 0) {
|
|
158
|
-
content += `{gray-fg} ↓ ${itemsBelow} more{/gray-fg}\n`;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
requestsBox.setContent(content);
|
|
163
|
-
screen.render();
|
|
164
|
-
|
|
165
|
-
return { adjustedSelectedIndex, trimmedPairs };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Updates the QR code display box
|
|
170
|
-
*/
|
|
171
|
-
export function updateQrCodeDisplay(
|
|
172
|
-
qrCodeBox: blessed.Widgets.BoxElement | undefined,
|
|
173
|
-
screen: blessed.Widgets.Screen,
|
|
174
|
-
qrCodes: string[],
|
|
175
|
-
urls: string[],
|
|
176
|
-
currentQrIndex: number
|
|
177
|
-
): void {
|
|
178
|
-
if (!qrCodeBox || qrCodes.length === 0) return;
|
|
179
|
-
|
|
180
|
-
let content = `{green-fg}{bold}QR Code ${currentQrIndex + 1}/${urls.length}{/bold}{/green-fg}\n`;
|
|
181
|
-
if (urls.length > 1) {
|
|
182
|
-
content += "\n{yellow-fg}← → to switch QR codes{/yellow-fg}\n";
|
|
183
|
-
}
|
|
184
|
-
content += qrCodes[currentQrIndex] || "";
|
|
185
|
-
qrCodeBox.setContent(content);
|
|
186
|
-
qrCodeBox.style = { ...qrCodeBox.style };
|
|
187
|
-
(qrCodeBox as any).parseContent();
|
|
188
|
-
screen.render();
|
|
189
|
-
}
|