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.
- package/dist/__tests__/auth.test.js +49 -0
- package/dist/__tests__/watcher.test.js +58 -0
- package/dist/claude/sessionBrowser.js +689 -0
- package/dist/claude/watcher.js +242 -0
- package/dist/config.js +17 -0
- package/dist/files/browser.js +127 -0
- package/dist/files/reader.js +159 -0
- package/dist/files/search.js +124 -0
- package/dist/git/gitHandler.js +95 -0
- package/dist/git/gitService.js +237 -0
- package/dist/git/index.js +8 -0
- package/dist/http/server.js +28 -0
- package/dist/index.js +77 -0
- package/dist/ssh/index.js +9 -0
- package/dist/ssh/sshHandler.js +205 -0
- package/dist/ssh/sshManager.js +329 -0
- package/dist/terminal/index.js +11 -0
- package/dist/terminal/localTerminalHandler.js +144 -0
- package/dist/terminal/localTerminalManager.js +465 -0
- package/dist/terminal/terminalWebSocket.js +128 -0
- package/dist/types.js +2 -0
- package/dist/utils/logger.js +42 -0
- package/dist/websocket/auth.js +18 -0
- package/dist/websocket/server.js +512 -0
- package/package.json +64 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LocalTerminalConnection = exports.localTerminalManager = exports.ZellijTerminalConnection = void 0;
|
|
37
|
+
const pty = __importStar(require("node-pty"));
|
|
38
|
+
const os = __importStar(require("os"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const child_process_1 = require("child_process");
|
|
42
|
+
const logger_1 = require("../utils/logger");
|
|
43
|
+
/**
|
|
44
|
+
* Find the full path to a command
|
|
45
|
+
*/
|
|
46
|
+
function findCommand(cmd) {
|
|
47
|
+
const home = process.env.HOME || '';
|
|
48
|
+
const additionalPaths = [
|
|
49
|
+
path.join(home, '.local', 'bin'),
|
|
50
|
+
path.join(home, '.npm-global', 'bin'),
|
|
51
|
+
path.join(home, 'bin'),
|
|
52
|
+
'/usr/local/bin',
|
|
53
|
+
'/opt/homebrew/bin',
|
|
54
|
+
];
|
|
55
|
+
for (const dir of additionalPaths) {
|
|
56
|
+
const fullPath = path.join(dir, cmd);
|
|
57
|
+
if (fs.existsSync(fullPath)) {
|
|
58
|
+
return fullPath;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Get extended PATH
|
|
65
|
+
*/
|
|
66
|
+
function getExtendedPath() {
|
|
67
|
+
const home = process.env.HOME || '';
|
|
68
|
+
const currentPath = process.env.PATH || '';
|
|
69
|
+
const additionalPaths = [
|
|
70
|
+
path.join(home, '.local', 'bin'),
|
|
71
|
+
path.join(home, '.npm-global', 'bin'),
|
|
72
|
+
path.join(home, 'bin'),
|
|
73
|
+
'/opt/homebrew/bin',
|
|
74
|
+
'/usr/local/bin',
|
|
75
|
+
];
|
|
76
|
+
return [...additionalPaths, currentPath].join(':');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Check if zellij is available
|
|
80
|
+
*/
|
|
81
|
+
function isZellijAvailable() {
|
|
82
|
+
try {
|
|
83
|
+
(0, child_process_1.execSync)('which zellij', { stdio: 'ignore' });
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* List existing zellij sessions
|
|
92
|
+
*/
|
|
93
|
+
function listZellijSessions() {
|
|
94
|
+
try {
|
|
95
|
+
const output = (0, child_process_1.execSync)('zellij list-sessions -s 2>/dev/null || true', {
|
|
96
|
+
encoding: 'utf-8',
|
|
97
|
+
env: { ...process.env, PATH: getExtendedPath() },
|
|
98
|
+
});
|
|
99
|
+
return output.trim().split('\n').filter(s => s.length > 0);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Kill a zellij session
|
|
107
|
+
*/
|
|
108
|
+
function killZellijSession(sessionName) {
|
|
109
|
+
try {
|
|
110
|
+
(0, child_process_1.execSync)(`zellij kill-session ${sessionName} 2>/dev/null || true`, {
|
|
111
|
+
env: { ...process.env, PATH: getExtendedPath() },
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Manages zellij-backed terminal sessions for a client
|
|
121
|
+
*/
|
|
122
|
+
class ZellijTerminalConnection {
|
|
123
|
+
constructor(clientId) {
|
|
124
|
+
this.sessions = new Map();
|
|
125
|
+
this.dataCallbacks = new Map();
|
|
126
|
+
this.closeCallbacks = new Map();
|
|
127
|
+
this.clientId = clientId;
|
|
128
|
+
this.zellijAvailable = isZellijAvailable();
|
|
129
|
+
if (this.zellijAvailable) {
|
|
130
|
+
logger_1.logger.info('Zellij is available, using zellij-backed sessions');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
logger_1.logger.warn('Zellij not found, falling back to plain PTY');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Generate a unique zellij session name
|
|
138
|
+
*/
|
|
139
|
+
generateSessionName(sessionId, type) {
|
|
140
|
+
// Use a prefix to identify our sessions
|
|
141
|
+
return `renote-${type}-${sessionId.replace(/[^a-zA-Z0-9]/g, '-')}`;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Check if a session exists
|
|
145
|
+
*/
|
|
146
|
+
hasTerminal(sessionId) {
|
|
147
|
+
return this.sessions.has(sessionId);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Rebind callbacks for reconnection
|
|
151
|
+
*/
|
|
152
|
+
rebindCallbacks(sessionId, onData, onClose) {
|
|
153
|
+
const session = this.sessions.get(sessionId);
|
|
154
|
+
if (!session) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
this.dataCallbacks.set(sessionId, onData);
|
|
158
|
+
this.closeCallbacks.set(sessionId, onClose);
|
|
159
|
+
// If PTY process exists and is running, we're good
|
|
160
|
+
if (session.ptyProcess) {
|
|
161
|
+
logger_1.logger.info(`Rebound callbacks for session ${sessionId}`);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
// PTY was closed but zellij session might still exist - reattach
|
|
165
|
+
if (this.zellijAvailable) {
|
|
166
|
+
const zellijName = this.generateSessionName(sessionId, session.type);
|
|
167
|
+
const existingSessions = listZellijSessions();
|
|
168
|
+
if (existingSessions.includes(zellijName)) {
|
|
169
|
+
logger_1.logger.info(`Reattaching to existing zellij session ${zellijName}`);
|
|
170
|
+
return this.attachToZellijSession(sessionId, session.type, onData, onClose);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Attach to an existing or new zellij session
|
|
177
|
+
*/
|
|
178
|
+
attachToZellijSession(sessionId, type, onData, onClose, options) {
|
|
179
|
+
const zellijName = this.generateSessionName(sessionId, type);
|
|
180
|
+
const cols = options?.cols || 80;
|
|
181
|
+
const rows = options?.rows || 24;
|
|
182
|
+
const cwd = options?.cwd || process.env.HOME || process.cwd();
|
|
183
|
+
try {
|
|
184
|
+
// zellij attach -c will create if not exists
|
|
185
|
+
const ptyProcess = pty.spawn('zellij', ['attach', '-c', zellijName], {
|
|
186
|
+
name: 'xterm-256color',
|
|
187
|
+
cols,
|
|
188
|
+
rows,
|
|
189
|
+
cwd,
|
|
190
|
+
env: {
|
|
191
|
+
...process.env,
|
|
192
|
+
PATH: getExtendedPath(),
|
|
193
|
+
TERM: 'xterm-256color',
|
|
194
|
+
COLORTERM: 'truecolor',
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
this.setupPtyHandlers(sessionId, ptyProcess, onData, onClose);
|
|
198
|
+
const session = this.sessions.get(sessionId);
|
|
199
|
+
if (session) {
|
|
200
|
+
session.ptyProcess = ptyProcess;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
this.sessions.set(sessionId, {
|
|
204
|
+
name: zellijName,
|
|
205
|
+
type,
|
|
206
|
+
createdAt: Date.now(),
|
|
207
|
+
ptyProcess,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
logger_1.logger.info(`Attached to zellij session ${zellijName} (${cols}x${rows})`);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
logger_1.logger.error(`Failed to attach to zellij session ${zellijName}:`, error);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Setup PTY event handlers
|
|
220
|
+
*/
|
|
221
|
+
setupPtyHandlers(sessionId, ptyProcess, onData, onClose) {
|
|
222
|
+
this.dataCallbacks.set(sessionId, onData);
|
|
223
|
+
this.closeCallbacks.set(sessionId, onClose);
|
|
224
|
+
ptyProcess.onData((data) => {
|
|
225
|
+
const callback = this.dataCallbacks.get(sessionId);
|
|
226
|
+
if (callback) {
|
|
227
|
+
callback(data);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
231
|
+
logger_1.logger.info(`PTY for session ${sessionId} exited with code ${exitCode}, signal ${signal}`);
|
|
232
|
+
const session = this.sessions.get(sessionId);
|
|
233
|
+
if (session) {
|
|
234
|
+
session.ptyProcess = null;
|
|
235
|
+
}
|
|
236
|
+
// Note: Don't remove the session - zellij session is still running
|
|
237
|
+
// Only call close callback if client needs to know
|
|
238
|
+
const callback = this.closeCallbacks.get(sessionId);
|
|
239
|
+
if (callback) {
|
|
240
|
+
callback();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Start a new terminal session
|
|
246
|
+
*/
|
|
247
|
+
startTerminal(sessionId, onData, onClose, options = { type: 'shell' }) {
|
|
248
|
+
// Check if session already exists
|
|
249
|
+
if (this.sessions.has(sessionId)) {
|
|
250
|
+
return this.rebindCallbacks(sessionId, onData, onClose);
|
|
251
|
+
}
|
|
252
|
+
const type = options.type;
|
|
253
|
+
if (this.zellijAvailable) {
|
|
254
|
+
// Create zellij session
|
|
255
|
+
const zellijName = this.generateSessionName(sessionId, type);
|
|
256
|
+
this.sessions.set(sessionId, {
|
|
257
|
+
name: zellijName,
|
|
258
|
+
type,
|
|
259
|
+
createdAt: Date.now(),
|
|
260
|
+
ptyProcess: null,
|
|
261
|
+
});
|
|
262
|
+
// Attach to zellij session
|
|
263
|
+
const attached = this.attachToZellijSession(sessionId, type, onData, onClose, options);
|
|
264
|
+
if (!attached) {
|
|
265
|
+
this.sessions.delete(sessionId);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
// If claude type, run claude command inside zellij
|
|
269
|
+
if (type === 'claude') {
|
|
270
|
+
setTimeout(() => {
|
|
271
|
+
const claudePath = findCommand('claude') || 'claude';
|
|
272
|
+
const args = options.claudeArgs?.join(' ') || '';
|
|
273
|
+
this.writeToTerminal(sessionId, `${claudePath} ${args}\n`);
|
|
274
|
+
}, 500);
|
|
275
|
+
}
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Fallback to plain PTY
|
|
280
|
+
return this.startPlainPty(sessionId, onData, onClose, options);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Fallback: start plain PTY without zellij
|
|
285
|
+
*/
|
|
286
|
+
startPlainPty(sessionId, onData, onClose, options) {
|
|
287
|
+
const cols = options.cols || 80;
|
|
288
|
+
const rows = options.rows || 24;
|
|
289
|
+
const cwd = options.cwd || process.env.HOME || process.cwd();
|
|
290
|
+
let command;
|
|
291
|
+
let args;
|
|
292
|
+
if (options.type === 'claude') {
|
|
293
|
+
const claudePath = findCommand('claude');
|
|
294
|
+
command = claudePath || 'claude';
|
|
295
|
+
args = options.claudeArgs || [];
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
command = process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : 'bash');
|
|
299
|
+
args = [];
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const ptyProcess = pty.spawn(command, args, {
|
|
303
|
+
name: 'xterm-256color',
|
|
304
|
+
cols,
|
|
305
|
+
rows,
|
|
306
|
+
cwd,
|
|
307
|
+
env: {
|
|
308
|
+
...process.env,
|
|
309
|
+
PATH: getExtendedPath(),
|
|
310
|
+
TERM: 'xterm-256color',
|
|
311
|
+
COLORTERM: 'truecolor',
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
this.sessions.set(sessionId, {
|
|
315
|
+
name: sessionId,
|
|
316
|
+
type: options.type,
|
|
317
|
+
createdAt: Date.now(),
|
|
318
|
+
ptyProcess,
|
|
319
|
+
});
|
|
320
|
+
this.setupPtyHandlers(sessionId, ptyProcess, onData, onClose);
|
|
321
|
+
logger_1.logger.info(`Started plain PTY ${options.type} session ${sessionId} (${cols}x${rows})`);
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
logger_1.logger.error(`Failed to start plain PTY ${sessionId}:`, error);
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Write to terminal
|
|
331
|
+
*/
|
|
332
|
+
writeToTerminal(sessionId, data) {
|
|
333
|
+
const session = this.sessions.get(sessionId);
|
|
334
|
+
if (!session || !session.ptyProcess) {
|
|
335
|
+
logger_1.logger.warn(`Terminal ${sessionId} not found or not attached`);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
session.ptyProcess.write(data);
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Resize terminal
|
|
343
|
+
*/
|
|
344
|
+
resizeTerminal(sessionId, cols, rows) {
|
|
345
|
+
const session = this.sessions.get(sessionId);
|
|
346
|
+
if (!session || !session.ptyProcess) {
|
|
347
|
+
logger_1.logger.warn(`Terminal ${sessionId} not found for resize`);
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
session.ptyProcess.resize(cols, rows);
|
|
351
|
+
logger_1.logger.debug(`Resized terminal ${sessionId} to ${cols}x${rows}`);
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Close terminal (detach from zellij, but don't kill the session)
|
|
356
|
+
*/
|
|
357
|
+
closeTerminal(sessionId, killSession = false) {
|
|
358
|
+
const session = this.sessions.get(sessionId);
|
|
359
|
+
if (!session) {
|
|
360
|
+
logger_1.logger.warn(`Terminal ${sessionId} not found for close`);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
// Kill the PTY (detach from zellij)
|
|
364
|
+
if (session.ptyProcess) {
|
|
365
|
+
session.ptyProcess.kill();
|
|
366
|
+
session.ptyProcess = null;
|
|
367
|
+
}
|
|
368
|
+
// Optionally kill the zellij session too
|
|
369
|
+
if (killSession && this.zellijAvailable) {
|
|
370
|
+
killZellijSession(session.name);
|
|
371
|
+
logger_1.logger.info(`Killed zellij session ${session.name}`);
|
|
372
|
+
}
|
|
373
|
+
this.sessions.delete(sessionId);
|
|
374
|
+
this.dataCallbacks.delete(sessionId);
|
|
375
|
+
this.closeCallbacks.delete(sessionId);
|
|
376
|
+
logger_1.logger.info(`Closed terminal ${sessionId}${killSession ? ' (session killed)' : ' (session preserved)'}`);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Get list of active terminals
|
|
381
|
+
*/
|
|
382
|
+
getActiveTerminals() {
|
|
383
|
+
return Array.from(this.sessions.keys());
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Get terminal info
|
|
387
|
+
*/
|
|
388
|
+
getTerminalInfo(sessionId) {
|
|
389
|
+
const session = this.sessions.get(sessionId);
|
|
390
|
+
if (!session)
|
|
391
|
+
return null;
|
|
392
|
+
return {
|
|
393
|
+
type: session.type,
|
|
394
|
+
createdAt: session.createdAt,
|
|
395
|
+
zellijSession: this.zellijAvailable ? session.name : undefined,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Close all terminals
|
|
400
|
+
*/
|
|
401
|
+
closeAll(killSessions = false) {
|
|
402
|
+
for (const [sessionId] of this.sessions) {
|
|
403
|
+
this.closeTerminal(sessionId, killSessions);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* List all zellij sessions managed by this server
|
|
408
|
+
*/
|
|
409
|
+
static listManagedSessions() {
|
|
410
|
+
return listZellijSessions().filter(s => s.startsWith('renote-'));
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Kill a zellij session by sessionId (without needing a connection)
|
|
414
|
+
* Tries both shell and claude session names
|
|
415
|
+
*/
|
|
416
|
+
static killSessionById(sessionId) {
|
|
417
|
+
const sanitized = sessionId.replace(/[^a-zA-Z0-9]/g, '-');
|
|
418
|
+
const shellName = `renote-shell-${sanitized}`;
|
|
419
|
+
const claudeName = `renote-claude-${sanitized}`;
|
|
420
|
+
let killed = false;
|
|
421
|
+
const existingSessions = listZellijSessions();
|
|
422
|
+
if (existingSessions.includes(shellName)) {
|
|
423
|
+
killed = killZellijSession(shellName) || killed;
|
|
424
|
+
}
|
|
425
|
+
if (existingSessions.includes(claudeName)) {
|
|
426
|
+
killed = killZellijSession(claudeName) || killed;
|
|
427
|
+
}
|
|
428
|
+
return killed;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
exports.ZellijTerminalConnection = ZellijTerminalConnection;
|
|
432
|
+
exports.LocalTerminalConnection = ZellijTerminalConnection;
|
|
433
|
+
/**
|
|
434
|
+
* Manager for all client connections
|
|
435
|
+
*/
|
|
436
|
+
class ZellijTerminalManager {
|
|
437
|
+
constructor() {
|
|
438
|
+
this.connections = new Map();
|
|
439
|
+
}
|
|
440
|
+
getConnection(clientId) {
|
|
441
|
+
return this.connections.get(clientId);
|
|
442
|
+
}
|
|
443
|
+
getOrCreateConnection(clientId) {
|
|
444
|
+
let connection = this.connections.get(clientId);
|
|
445
|
+
if (!connection) {
|
|
446
|
+
connection = new ZellijTerminalConnection(clientId);
|
|
447
|
+
this.connections.set(clientId, connection);
|
|
448
|
+
logger_1.logger.info(`Created terminal connection for client ${clientId}`);
|
|
449
|
+
}
|
|
450
|
+
return connection;
|
|
451
|
+
}
|
|
452
|
+
removeConnection(clientId, killSessions = false) {
|
|
453
|
+
const connection = this.connections.get(clientId);
|
|
454
|
+
if (connection) {
|
|
455
|
+
connection.closeAll(killSessions);
|
|
456
|
+
this.connections.delete(clientId);
|
|
457
|
+
logger_1.logger.info(`Removed terminal connection for client ${clientId}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
getConnectionCount() {
|
|
461
|
+
return this.connections.size;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
// Export with the same interface for compatibility
|
|
465
|
+
exports.localTerminalManager = new ZellijTerminalManager();
|
|
@@ -0,0 +1,128 @@
|
|
|
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.terminalWebSocketHandler = exports.TerminalWebSocketHandler = void 0;
|
|
7
|
+
const ws_1 = __importDefault(require("ws"));
|
|
8
|
+
const url_1 = require("url");
|
|
9
|
+
const localTerminalManager_1 = require("./localTerminalManager");
|
|
10
|
+
const auth_1 = require("../websocket/auth");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
class TerminalWebSocketHandler {
|
|
13
|
+
constructor() {
|
|
14
|
+
// Map from WebSocket to sessionId for cleanup
|
|
15
|
+
this.wsToSession = new Map();
|
|
16
|
+
this.authManager = new auth_1.AuthManager();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if a request should be handled by this handler
|
|
20
|
+
*/
|
|
21
|
+
shouldHandle(request) {
|
|
22
|
+
try {
|
|
23
|
+
const url = new url_1.URL(request.url || '', `http://${request.headers.host}`);
|
|
24
|
+
return url.pathname === '/terminal';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Handle a new terminal WebSocket connection
|
|
32
|
+
*/
|
|
33
|
+
handleConnection(ws, request) {
|
|
34
|
+
try {
|
|
35
|
+
const url = new url_1.URL(request.url || '', `http://${request.headers.host}`);
|
|
36
|
+
const token = url.searchParams.get('token') || '';
|
|
37
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
38
|
+
const type = (url.searchParams.get('type') || 'shell');
|
|
39
|
+
const cols = parseInt(url.searchParams.get('cols') || '80', 10);
|
|
40
|
+
const rows = parseInt(url.searchParams.get('rows') || '24', 10);
|
|
41
|
+
// Validate token
|
|
42
|
+
if (!this.authManager.validateToken(token)) {
|
|
43
|
+
logger_1.logger.warn('Terminal WebSocket: invalid token');
|
|
44
|
+
ws.close(4001, 'Invalid token');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (!sessionId) {
|
|
48
|
+
logger_1.logger.warn('Terminal WebSocket: missing sessionId');
|
|
49
|
+
ws.close(4002, 'Missing sessionId');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Generate a unique client ID for this connection
|
|
53
|
+
const clientId = this.authManager.generateClientId();
|
|
54
|
+
this.wsToSession.set(ws, { clientId, sessionId });
|
|
55
|
+
logger_1.logger.info(`Terminal WebSocket connected: clientId=${clientId}, sessionId=${sessionId}, type=${type}, ${cols}x${rows}`);
|
|
56
|
+
const connection = localTerminalManager_1.localTerminalManager.getOrCreateConnection(clientId);
|
|
57
|
+
const options = { type, cols, rows };
|
|
58
|
+
const success = connection.startTerminal(sessionId, (data) => {
|
|
59
|
+
// Send terminal output as text frame
|
|
60
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
61
|
+
ws.send(data);
|
|
62
|
+
}
|
|
63
|
+
}, () => {
|
|
64
|
+
// Terminal closed
|
|
65
|
+
if (ws.readyState === ws_1.default.OPEN) {
|
|
66
|
+
ws.close(1000, 'Terminal closed');
|
|
67
|
+
}
|
|
68
|
+
}, options);
|
|
69
|
+
if (!success) {
|
|
70
|
+
logger_1.logger.error(`Failed to start terminal: sessionId=${sessionId}`);
|
|
71
|
+
ws.close(4003, 'Failed to start terminal');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Handle incoming messages
|
|
75
|
+
ws.on('message', (data, isBinary) => {
|
|
76
|
+
if (isBinary) {
|
|
77
|
+
// Binary frame = control message
|
|
78
|
+
this.handleControlMessage(ws, clientId, sessionId, data);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
// Text frame = terminal input
|
|
82
|
+
connection.writeToTerminal(sessionId, data.toString());
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
ws.on('close', () => {
|
|
86
|
+
logger_1.logger.info(`Terminal WebSocket closed: clientId=${clientId}, sessionId=${sessionId}`);
|
|
87
|
+
// Detach but don't kill the zellij session
|
|
88
|
+
connection.closeTerminal(sessionId, false);
|
|
89
|
+
localTerminalManager_1.localTerminalManager.removeConnection(clientId, false);
|
|
90
|
+
this.wsToSession.delete(ws);
|
|
91
|
+
});
|
|
92
|
+
ws.on('error', (error) => {
|
|
93
|
+
logger_1.logger.error(`Terminal WebSocket error: ${error.message}`);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
logger_1.logger.error('Terminal WebSocket connection error:', error);
|
|
98
|
+
ws.close(4000, 'Connection error');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
handleControlMessage(ws, clientId, sessionId, data) {
|
|
102
|
+
try {
|
|
103
|
+
const message = JSON.parse(data.toString());
|
|
104
|
+
switch (message.type) {
|
|
105
|
+
case 'resize':
|
|
106
|
+
if (message.cols && message.rows) {
|
|
107
|
+
const connection = localTerminalManager_1.localTerminalManager.getConnection(clientId);
|
|
108
|
+
if (connection) {
|
|
109
|
+
connection.resizeTerminal(sessionId, message.cols, message.rows);
|
|
110
|
+
logger_1.logger.debug(`Terminal resized: ${sessionId} -> ${message.cols}x${message.rows}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
case 'ping':
|
|
115
|
+
// Respond with pong as binary frame
|
|
116
|
+
ws.send(Buffer.from(JSON.stringify({ type: 'pong' })));
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
logger_1.logger.warn(`Unknown control message type: ${message.type}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger_1.logger.error('Failed to parse control message:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.TerminalWebSocketHandler = TerminalWebSocketHandler;
|
|
128
|
+
exports.terminalWebSocketHandler = new TerminalWebSocketHandler();
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logger = void 0;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
var LogLevel;
|
|
6
|
+
(function (LogLevel) {
|
|
7
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
8
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
9
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
10
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
11
|
+
})(LogLevel || (LogLevel = {}));
|
|
12
|
+
const LEVEL_MAP = {
|
|
13
|
+
debug: LogLevel.DEBUG,
|
|
14
|
+
info: LogLevel.INFO,
|
|
15
|
+
warn: LogLevel.WARN,
|
|
16
|
+
error: LogLevel.ERROR,
|
|
17
|
+
};
|
|
18
|
+
class Logger {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.level = LEVEL_MAP[config_1.CONFIG.logLevel] || LogLevel.INFO;
|
|
21
|
+
}
|
|
22
|
+
log(level, message, ...args) {
|
|
23
|
+
if (level >= this.level) {
|
|
24
|
+
const timestamp = new Date().toISOString();
|
|
25
|
+
const levelName = LogLevel[level];
|
|
26
|
+
console.log(`[${timestamp}] [${levelName}]`, message, ...args);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
debug(message, ...args) {
|
|
30
|
+
this.log(LogLevel.DEBUG, message, ...args);
|
|
31
|
+
}
|
|
32
|
+
info(message, ...args) {
|
|
33
|
+
this.log(LogLevel.INFO, message, ...args);
|
|
34
|
+
}
|
|
35
|
+
warn(message, ...args) {
|
|
36
|
+
this.log(LogLevel.WARN, message, ...args);
|
|
37
|
+
}
|
|
38
|
+
error(message, ...args) {
|
|
39
|
+
this.log(LogLevel.ERROR, message, ...args);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.logger = new Logger();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthManager = void 0;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
class AuthManager {
|
|
7
|
+
validateToken(token) {
|
|
8
|
+
if (!config_1.CONFIG.authToken) {
|
|
9
|
+
logger_1.logger.warn('No AUTH_TOKEN configured, accepting all connections');
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
return token === config_1.CONFIG.authToken;
|
|
13
|
+
}
|
|
14
|
+
generateClientId() {
|
|
15
|
+
return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.AuthManager = AuthManager;
|