renote-server 1.0.1

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.
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitHandler = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const gitService_1 = require("./gitService");
6
+ /**
7
+ * Handles Git-related WebSocket messages.
8
+ * Routes git_* messages to GitService methods.
9
+ */
10
+ class GitHandler {
11
+ constructor(sendFn) {
12
+ this.sendFn = sendFn;
13
+ }
14
+ canHandle(messageType) {
15
+ return messageType.startsWith('git_');
16
+ }
17
+ async handle(ws, clientId, message) {
18
+ switch (message.type) {
19
+ case 'git_status':
20
+ await this.handleStatus(ws, message);
21
+ break;
22
+ case 'git_file_diff':
23
+ await this.handleFileDiff(ws, message);
24
+ break;
25
+ case 'git_check_repo':
26
+ await this.handleCheckRepo(ws, message);
27
+ break;
28
+ default:
29
+ logger_1.logger.warn(`Unknown git message type: ${message.type}`);
30
+ }
31
+ }
32
+ async handleCheckRepo(ws, message) {
33
+ try {
34
+ const workingDir = message.path || process.cwd();
35
+ const isGitRepo = await gitService_1.gitService.isGitRepo(workingDir);
36
+ this.sendFn(ws, {
37
+ type: 'git_check_repo_response',
38
+ data: { isGitRepo },
39
+ });
40
+ }
41
+ catch (error) {
42
+ logger_1.logger.error('Git check repo error:', error);
43
+ this.sendFn(ws, {
44
+ type: 'git_check_repo_response',
45
+ data: { isGitRepo: false },
46
+ error: error.message,
47
+ });
48
+ }
49
+ }
50
+ async handleStatus(ws, message) {
51
+ try {
52
+ const workingDir = message.path || process.cwd();
53
+ const files = await gitService_1.gitService.getStatus(workingDir);
54
+ this.sendFn(ws, {
55
+ type: 'git_status_response',
56
+ data: { files },
57
+ });
58
+ }
59
+ catch (error) {
60
+ logger_1.logger.error('Git status error:', error);
61
+ this.sendFn(ws, {
62
+ type: 'error',
63
+ error: `Failed to get git status: ${error.message}`,
64
+ });
65
+ }
66
+ }
67
+ async handleFileDiff(ws, message) {
68
+ try {
69
+ const workingDir = message.path || process.cwd();
70
+ const { filePath, staged } = message;
71
+ logger_1.logger.info(`Git diff request: filePath=${filePath}, staged=${staged}, workingDir=${workingDir}`);
72
+ if (!filePath) {
73
+ this.sendFn(ws, {
74
+ type: 'error',
75
+ error: 'File path is required for git diff',
76
+ });
77
+ return;
78
+ }
79
+ const diff = await gitService_1.gitService.getFileDiff(workingDir, filePath, staged);
80
+ logger_1.logger.info(`Git diff result: ${diff.length} bytes`);
81
+ this.sendFn(ws, {
82
+ type: 'git_file_diff_response',
83
+ data: { filePath, diff },
84
+ });
85
+ }
86
+ catch (error) {
87
+ logger_1.logger.error('Git diff error:', error);
88
+ this.sendFn(ws, {
89
+ type: 'error',
90
+ error: `Failed to get diff: ${error.message}`,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ exports.GitHandler = GitHandler;
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.gitService = exports.GitService = void 0;
7
+ const child_process_1 = require("child_process");
8
+ const util_1 = require("util");
9
+ const promises_1 = require("fs/promises");
10
+ const path_1 = __importDefault(require("path"));
11
+ const logger_1 = require("../utils/logger");
12
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
13
+ /**
14
+ * Git service for executing git commands safely.
15
+ */
16
+ class GitService {
17
+ /**
18
+ * Check if a directory is inside a git repository.
19
+ */
20
+ async isGitRepo(workingDir) {
21
+ try {
22
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
23
+ cwd: workingDir,
24
+ });
25
+ return stdout.trim() === 'true';
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ /**
32
+ * Get the root directory of the git repository.
33
+ */
34
+ async getRepoRoot(workingDir) {
35
+ try {
36
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--show-toplevel'], {
37
+ cwd: workingDir,
38
+ });
39
+ return stdout.trim();
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ /**
46
+ * Get git status for all changed files.
47
+ * Uses `git status --porcelain` for machine-parseable output.
48
+ */
49
+ async getStatus(workingDir) {
50
+ try {
51
+ // Use repo root so paths in output are consistent
52
+ const repoRoot = await this.getRepoRoot(workingDir);
53
+ const cwd = repoRoot || workingDir;
54
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain', '-uall'], { cwd, maxBuffer: 10 * 1024 * 1024 });
55
+ if (!stdout.trim()) {
56
+ return [];
57
+ }
58
+ const files = [];
59
+ const lines = stdout.split('\n').filter(line => line.length > 0);
60
+ for (const line of lines) {
61
+ const parsed = this.parseStatusLine(line);
62
+ if (parsed) {
63
+ files.push(parsed);
64
+ }
65
+ }
66
+ return files;
67
+ }
68
+ catch (error) {
69
+ logger_1.logger.error('Failed to get git status:', error);
70
+ throw error;
71
+ }
72
+ }
73
+ /**
74
+ * Parse a single line of git status --porcelain output.
75
+ * Format: XY PATH or XY ORIG_PATH -> PATH (for renames)
76
+ */
77
+ parseStatusLine(line) {
78
+ if (line.length < 3)
79
+ return null;
80
+ const indexStatus = line[0];
81
+ const workTreeStatus = line[1];
82
+ const filePath = line.slice(3);
83
+ // Handle renames: "R old -> new"
84
+ if (indexStatus === 'R' || workTreeStatus === 'R') {
85
+ const parts = filePath.split(' -> ');
86
+ if (parts.length === 2) {
87
+ return {
88
+ path: parts[1],
89
+ status: 'renamed',
90
+ staged: indexStatus === 'R',
91
+ oldPath: parts[0],
92
+ };
93
+ }
94
+ }
95
+ // Determine status based on index and work tree status
96
+ let status;
97
+ let staged = false;
98
+ // Prioritize staged changes for display
99
+ if (indexStatus === 'A') {
100
+ status = 'added';
101
+ staged = true;
102
+ }
103
+ else if (indexStatus === 'D') {
104
+ status = 'deleted';
105
+ staged = true;
106
+ }
107
+ else if (indexStatus === 'M') {
108
+ status = 'modified';
109
+ staged = true;
110
+ }
111
+ else if (workTreeStatus === 'M') {
112
+ status = 'modified';
113
+ staged = false;
114
+ }
115
+ else if (workTreeStatus === 'D') {
116
+ status = 'deleted';
117
+ staged = false;
118
+ }
119
+ else if (workTreeStatus === '?' || indexStatus === '?') {
120
+ status = 'untracked';
121
+ staged = false;
122
+ }
123
+ else if (workTreeStatus === 'A') {
124
+ status = 'added';
125
+ staged = false;
126
+ }
127
+ else {
128
+ // Fallback
129
+ status = 'modified';
130
+ staged = indexStatus !== ' ' && indexStatus !== '?';
131
+ }
132
+ return { path: filePath, status, staged };
133
+ }
134
+ /**
135
+ * Get diff for a specific file.
136
+ * Returns unified diff format.
137
+ *
138
+ * Strategy:
139
+ * - Untracked files (??) → generate synthetic diff from file content
140
+ * - New files (A) → use git diff --cached (compare with empty)
141
+ * - Other files → use git diff HEAD (shows all changes vs last commit)
142
+ */
143
+ async getFileDiff(workingDir, filePath, staged = false) {
144
+ try {
145
+ // Get the repo root to ensure correct path handling
146
+ const repoRoot = await this.getRepoRoot(workingDir);
147
+ const cwd = repoRoot || workingDir;
148
+ logger_1.logger.info(`getFileDiff: cwd=${cwd}, filePath=${filePath}`);
149
+ // Get the actual status of this file
150
+ const { stdout: statusOut } = await execFileAsync('git', ['status', '--porcelain', '--', filePath], { cwd });
151
+ const statusLine = statusOut.trim().split('\n')[0] || '';
152
+ const indexStatus = statusLine[0] || ' ';
153
+ const workTreeStatus = statusLine[1] || ' ';
154
+ logger_1.logger.info(`getFileDiff: status line="${statusLine}", index=${indexStatus}, worktree=${workTreeStatus}`);
155
+ // Case 1: Untracked file - generate diff from file content
156
+ if (indexStatus === '?' || workTreeStatus === '?') {
157
+ logger_1.logger.info('getFileDiff: untracked file, generating synthetic diff');
158
+ return this.getUntrackedFileDiff(cwd, filePath);
159
+ }
160
+ // Case 2: Newly added file (only in index, not in HEAD)
161
+ if (indexStatus === 'A') {
162
+ logger_1.logger.info('getFileDiff: new file, using --cached');
163
+ const { stdout } = await execFileAsync('git', ['diff', '--cached', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
164
+ if (stdout.trim()) {
165
+ return stdout;
166
+ }
167
+ // Fallback: if --cached returns empty (shouldn't happen), show file content
168
+ return this.getUntrackedFileDiff(cwd, filePath);
169
+ }
170
+ // Case 3: Modified/Deleted files - use git diff HEAD to show all changes
171
+ // This captures both staged and unstaged changes relative to last commit
172
+ logger_1.logger.info('getFileDiff: tracked file, using diff HEAD');
173
+ const { stdout: headDiff } = await execFileAsync('git', ['diff', 'HEAD', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
174
+ if (headDiff.trim()) {
175
+ logger_1.logger.info(`getFileDiff: HEAD diff length=${headDiff.length}`);
176
+ return headDiff;
177
+ }
178
+ // Fallback: try regular diff (in case HEAD doesn't exist)
179
+ logger_1.logger.info('getFileDiff: HEAD diff empty, trying regular diff');
180
+ const { stdout: regularDiff } = await execFileAsync('git', ['diff', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
181
+ if (regularDiff.trim()) {
182
+ return regularDiff;
183
+ }
184
+ // Last resort: try --cached
185
+ logger_1.logger.info('getFileDiff: regular diff empty, trying --cached');
186
+ const { stdout: cachedDiff } = await execFileAsync('git', ['diff', '--cached', '--', filePath], { cwd, maxBuffer: 10 * 1024 * 1024 });
187
+ return cachedDiff;
188
+ }
189
+ catch (error) {
190
+ logger_1.logger.error('Failed to get file diff:', error);
191
+ throw error;
192
+ }
193
+ }
194
+ /**
195
+ * Generate a diff-like output for an untracked file.
196
+ */
197
+ async getUntrackedFileDiff(workingDir, filePath) {
198
+ try {
199
+ const fullPath = path_1.default.join(workingDir, filePath);
200
+ // Check if file is binary
201
+ const isBinary = await this.isBinaryFile(fullPath);
202
+ if (isBinary) {
203
+ return `diff --git a/${filePath} b/${filePath}\nnew file mode 100644\nBinary file`;
204
+ }
205
+ // Read file content
206
+ const content = await (0, promises_1.readFile)(fullPath, 'utf-8');
207
+ const lines = content.split('\n');
208
+ const diffLines = [
209
+ `diff --git a/${filePath} b/${filePath}`,
210
+ 'new file mode 100644',
211
+ '--- /dev/null',
212
+ `+++ b/${filePath}`,
213
+ `@@ -0,0 +1,${lines.length} @@`,
214
+ ...lines.map(line => `+${line}`),
215
+ ];
216
+ return diffLines.join('\n');
217
+ }
218
+ catch (error) {
219
+ logger_1.logger.error('Failed to generate untracked file diff:', error);
220
+ throw error;
221
+ }
222
+ }
223
+ /**
224
+ * Check if a file is binary by looking at the first few bytes.
225
+ */
226
+ async isBinaryFile(filePath) {
227
+ try {
228
+ const { stdout } = await execFileAsync('file', ['--mime', filePath]);
229
+ return stdout.includes('charset=binary');
230
+ }
231
+ catch {
232
+ return false;
233
+ }
234
+ }
235
+ }
236
+ exports.GitService = GitService;
237
+ exports.gitService = new GitService();
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GitHandler = exports.gitService = exports.GitService = void 0;
4
+ var gitService_1 = require("./gitService");
5
+ Object.defineProperty(exports, "GitService", { enumerable: true, get: function () { return gitService_1.GitService; } });
6
+ Object.defineProperty(exports, "gitService", { enumerable: true, get: function () { return gitService_1.gitService; } });
7
+ var gitHandler_1 = require("./gitHandler");
8
+ Object.defineProperty(exports, "GitHandler", { enumerable: true, get: function () { return gitHandler_1.GitHandler; } });
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createHttpServer = createHttpServer;
7
+ const express_1 = __importDefault(require("express"));
8
+ const config_1 = require("../config");
9
+ const logger_1 = require("../utils/logger");
10
+ function createHttpServer() {
11
+ const app = (0, express_1.default)();
12
+ app.get('/health', (_req, res) => {
13
+ res.json({ status: 'ok', timestamp: Date.now() });
14
+ });
15
+ app.get('/token', (req, res) => {
16
+ const authHeader = req.headers.authorization;
17
+ if (authHeader !== `Bearer ${config_1.CONFIG.authToken}`) {
18
+ res.status(401).json({ error: 'Unauthorized' });
19
+ return;
20
+ }
21
+ res.json({ token: config_1.CONFIG.authToken });
22
+ });
23
+ const httpPort = config_1.CONFIG.port + 1;
24
+ app.listen(httpPort, () => {
25
+ logger_1.logger.info(`HTTP server running on port ${httpPort}`);
26
+ });
27
+ return app;
28
+ }
package/dist/index.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const server_1 = require("./websocket/server");
5
+ const watcher_1 = require("./claude/watcher");
6
+ const server_2 = require("./http/server");
7
+ const logger_1 = require("./utils/logger");
8
+ class RemoteDevServer {
9
+ constructor() {
10
+ this.wsServer = new server_1.WebSocketServer();
11
+ this.claudeWatcher = new watcher_1.ClaudeWatcher();
12
+ (0, server_2.createHttpServer)();
13
+ this.setupClaudeWatcher();
14
+ }
15
+ setupClaudeWatcher() {
16
+ this.claudeWatcher.on('user_input', (data) => {
17
+ this.wsServer.broadcast({
18
+ type: 'claude_user_input',
19
+ data
20
+ });
21
+ });
22
+ this.claudeWatcher.on('assistant_message', (data) => {
23
+ this.wsServer.broadcast({
24
+ type: 'claude_assistant_message',
25
+ data
26
+ });
27
+ });
28
+ this.claudeWatcher.on('tool_call', (data) => {
29
+ this.wsServer.broadcast({
30
+ type: 'claude_tool_call',
31
+ data
32
+ });
33
+ });
34
+ this.claudeWatcher.on('tool_result', (data) => {
35
+ this.wsServer.broadcast({
36
+ type: 'claude_tool_result',
37
+ data
38
+ });
39
+ });
40
+ this.claudeWatcher.on('file_change', (data) => {
41
+ this.wsServer.broadcast({
42
+ type: 'claude_file_change',
43
+ data
44
+ });
45
+ });
46
+ this.claudeWatcher.on('progress', (data) => {
47
+ this.wsServer.broadcast({
48
+ type: 'claude_progress',
49
+ data
50
+ });
51
+ });
52
+ }
53
+ async start() {
54
+ logger_1.logger.info('Starting Remote Dev Server');
55
+ await this.claudeWatcher.start();
56
+ logger_1.logger.info('Server ready');
57
+ }
58
+ stop() {
59
+ logger_1.logger.info('Stopping Remote Dev Server');
60
+ this.claudeWatcher.stop();
61
+ }
62
+ }
63
+ const server = new RemoteDevServer();
64
+ server.start().catch((error) => {
65
+ logger_1.logger.error('Failed to start server:', error);
66
+ process.exit(1);
67
+ });
68
+ process.on('SIGINT', () => {
69
+ logger_1.logger.info('Received SIGINT, shutting down');
70
+ server.stop();
71
+ process.exit(0);
72
+ });
73
+ process.on('SIGTERM', () => {
74
+ logger_1.logger.info('Received SIGTERM, shutting down');
75
+ server.stop();
76
+ process.exit(0);
77
+ });
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SSHHandler = exports.sshManager = exports.SSHConnection = exports.SSHManager = void 0;
4
+ var sshManager_1 = require("./sshManager");
5
+ Object.defineProperty(exports, "SSHManager", { enumerable: true, get: function () { return sshManager_1.SSHManager; } });
6
+ Object.defineProperty(exports, "SSHConnection", { enumerable: true, get: function () { return sshManager_1.SSHConnection; } });
7
+ Object.defineProperty(exports, "sshManager", { enumerable: true, get: function () { return sshManager_1.sshManager; } });
8
+ var sshHandler_1 = require("./sshHandler");
9
+ Object.defineProperty(exports, "SSHHandler", { enumerable: true, get: function () { return sshHandler_1.SSHHandler; } });
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SSHHandler = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const sshManager_1 = require("./sshManager");
6
+ /**
7
+ * Handles SSH-related WebSocket messages.
8
+ * Routes ssh_* messages to the appropriate SSHManager methods.
9
+ * Supports multiple shell sessions per client via sessionId.
10
+ */
11
+ class SSHHandler {
12
+ constructor(sendFn) {
13
+ this.sendFn = sendFn;
14
+ }
15
+ /**
16
+ * Check if this handler should process the message.
17
+ */
18
+ canHandle(messageType) {
19
+ return messageType.startsWith('ssh_');
20
+ }
21
+ /**
22
+ * Process an SSH message.
23
+ */
24
+ async handle(ws, clientId, message) {
25
+ switch (message.type) {
26
+ case 'ssh_connect':
27
+ await this.handleConnect(ws, clientId, message);
28
+ break;
29
+ case 'ssh_start_shell':
30
+ await this.handleStartShell(ws, clientId, message);
31
+ break;
32
+ case 'ssh_input':
33
+ this.handleInput(clientId, message);
34
+ break;
35
+ case 'ssh_resize':
36
+ this.handleResize(clientId, message);
37
+ break;
38
+ case 'ssh_disconnect':
39
+ this.handleDisconnect(ws, clientId);
40
+ break;
41
+ case 'ssh_close_shell':
42
+ this.handleCloseShell(ws, clientId, message);
43
+ break;
44
+ case 'ssh_list_shells':
45
+ this.handleListShells(ws, clientId);
46
+ break;
47
+ case 'ssh_port_forward':
48
+ await this.handlePortForward(ws, clientId, message);
49
+ break;
50
+ case 'ssh_stop_port_forward':
51
+ this.handleStopPortForward(ws, clientId, message);
52
+ break;
53
+ default:
54
+ logger_1.logger.warn(`Unknown SSH message type: ${message.type}`);
55
+ }
56
+ }
57
+ async handleConnect(ws, clientId, message) {
58
+ try {
59
+ const { host, port, username, privateKey, password } = message.data;
60
+ logger_1.logger.info(`SSH connect request: host=${host}, port=${port}, username=${username}, hasPassword=${!!password}, hasPrivateKey=${!!privateKey}`);
61
+ const connection = sshManager_1.sshManager.getConnection(clientId);
62
+ await connection.connect({ host, port, username, privateKey, password });
63
+ this.sendFn(ws, {
64
+ type: 'ssh_connect_response',
65
+ data: { success: true },
66
+ });
67
+ }
68
+ catch (error) {
69
+ logger_1.logger.error('SSH connect error:', error);
70
+ this.sendFn(ws, {
71
+ type: 'ssh_connect_response',
72
+ data: { success: false, message: error.message },
73
+ });
74
+ }
75
+ }
76
+ async handleStartShell(ws, clientId, message) {
77
+ try {
78
+ const connection = sshManager_1.sshManager.getConnection(clientId);
79
+ if (!connection.isConnected()) {
80
+ this.sendFn(ws, {
81
+ type: 'ssh_status',
82
+ data: { status: 'error', message: 'SSH not connected' },
83
+ });
84
+ return;
85
+ }
86
+ const sessionId = message.data?.sessionId || 'default';
87
+ const cols = message.data?.cols || 80;
88
+ const rows = message.data?.rows || 24;
89
+ await connection.startShell(sessionId, (data) => {
90
+ this.sendFn(ws, { type: 'ssh_output', data: { sessionId, output: data } });
91
+ }, () => {
92
+ this.sendFn(ws, {
93
+ type: 'ssh_shell_closed',
94
+ data: { sessionId },
95
+ });
96
+ }, cols, rows);
97
+ this.sendFn(ws, {
98
+ type: 'ssh_shell_started',
99
+ data: { sessionId },
100
+ });
101
+ }
102
+ catch (error) {
103
+ logger_1.logger.error('SSH start shell error:', error);
104
+ this.sendFn(ws, {
105
+ type: 'ssh_status',
106
+ data: { status: 'error', message: error.message },
107
+ });
108
+ }
109
+ }
110
+ handleInput(clientId, message) {
111
+ const connection = sshManager_1.sshManager.getConnection(clientId);
112
+ if (connection.isConnected()) {
113
+ const sessionId = message.data?.sessionId || 'default';
114
+ const input = message.data?.input || message.data;
115
+ // Try new API first, fall back to legacy
116
+ if (!connection.writeToShell(sessionId, typeof input === 'string' ? input : input.input)) {
117
+ // Legacy fallback
118
+ connection.write(typeof input === 'string' ? input : input.input);
119
+ }
120
+ }
121
+ }
122
+ handleResize(clientId, message) {
123
+ const connection = sshManager_1.sshManager.getConnection(clientId);
124
+ if (connection.isConnected()) {
125
+ const sessionId = message.data?.sessionId;
126
+ const cols = message.data.cols;
127
+ const rows = message.data.rows;
128
+ if (sessionId) {
129
+ connection.resizeShell(sessionId, cols, rows);
130
+ }
131
+ else {
132
+ // Legacy fallback
133
+ connection.resize(cols, rows);
134
+ }
135
+ }
136
+ }
137
+ handleCloseShell(ws, clientId, message) {
138
+ const connection = sshManager_1.sshManager.getConnection(clientId);
139
+ const sessionId = message.data?.sessionId;
140
+ if (sessionId && connection.isConnected()) {
141
+ const closed = connection.closeShell(sessionId);
142
+ this.sendFn(ws, {
143
+ type: 'ssh_shell_closed',
144
+ data: { sessionId, success: closed },
145
+ });
146
+ }
147
+ }
148
+ handleListShells(ws, clientId) {
149
+ const connection = sshManager_1.sshManager.getConnection(clientId);
150
+ const shells = connection.isConnected() ? connection.getActiveShells() : [];
151
+ this.sendFn(ws, {
152
+ type: 'ssh_list_shells_response',
153
+ data: { shells },
154
+ });
155
+ }
156
+ handleDisconnect(ws, clientId) {
157
+ sshManager_1.sshManager.removeConnection(clientId);
158
+ this.sendFn(ws, {
159
+ type: 'ssh_status',
160
+ data: { status: 'disconnected', message: 'Disconnected' },
161
+ });
162
+ }
163
+ async handlePortForward(ws, clientId, message) {
164
+ try {
165
+ const { localPort, remoteHost, remotePort } = message.data;
166
+ const connection = sshManager_1.sshManager.getConnection(clientId);
167
+ if (!connection.isConnected()) {
168
+ this.sendFn(ws, {
169
+ type: 'ssh_port_forward_response',
170
+ data: { success: false, localPort, message: 'SSH not connected' },
171
+ });
172
+ return;
173
+ }
174
+ await connection.setupPortForward({ localPort, remoteHost, remotePort });
175
+ this.sendFn(ws, {
176
+ type: 'ssh_port_forward_response',
177
+ data: { success: true, localPort },
178
+ });
179
+ }
180
+ catch (error) {
181
+ logger_1.logger.error('SSH port forward error:', error);
182
+ this.sendFn(ws, {
183
+ type: 'ssh_port_forward_response',
184
+ data: { success: false, localPort: message.data.localPort, message: error.message },
185
+ });
186
+ }
187
+ }
188
+ handleStopPortForward(ws, clientId, message) {
189
+ const connection = sshManager_1.sshManager.getConnection(clientId);
190
+ if (connection.isConnected()) {
191
+ connection.stopPortForward(message.data.localPort);
192
+ }
193
+ this.sendFn(ws, {
194
+ type: 'ssh_port_forward_response',
195
+ data: { success: true, localPort: message.data.localPort, message: 'Port forward stopped' },
196
+ });
197
+ }
198
+ /**
199
+ * Clean up SSH resources for a disconnected client.
200
+ */
201
+ cleanup(clientId) {
202
+ sshManager_1.sshManager.removeConnection(clientId);
203
+ }
204
+ }
205
+ exports.SSHHandler = SSHHandler;