thinkncollab-shell 0.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,974 @@
1
+ /**
2
+ * ThinkNCollab Shell Core
3
+ * Main shell class with WebSocket integration
4
+ */
5
+
6
+ const readline = require('readline');
7
+ const path = require('path');
8
+ const fs = require('fs-extra');
9
+ const os = require('os');
10
+ const chalk = require('chalk');
11
+ const { exec, spawn } = require('child_process');
12
+ const WebSocketManager = require('./websocket');
13
+ const EventEmitter = require('events');
14
+
15
+ class ThinkNCollabShell extends EventEmitter {
16
+ constructor(options = {}) {
17
+ super();
18
+
19
+ // Configuration
20
+ this.config = {
21
+ historyFile: path.join(os.homedir(), '.thinknsh_history'),
22
+ configDir: path.join(os.homedir(), '.thinkncollab'),
23
+ maxHistory: 1000,
24
+ prompt: options.prompt || 'thinknsh> ',
25
+ websocket: {
26
+ serverUrl: options.serverUrl || 'https://api.thinkncollab.com',
27
+ autoConnect: options.autoConnect || false
28
+ },
29
+ ...options
30
+ };
31
+
32
+ // Ensure config directory exists
33
+ fs.ensureDirSync(this.config.configDir);
34
+
35
+ // Initialize components
36
+ this.ws = new WebSocketManager(this.config.websocket);
37
+ this.rl = null;
38
+ this.currentDir = process.cwd();
39
+ this.username = os.userInfo().username;
40
+ this.hostname = os.hostname();
41
+ this.history = [];
42
+ this.commands = new Map();
43
+ this.aliases = new Map();
44
+ this.variables = new Map();
45
+ this.isTyping = false;
46
+ this.typingTimeout = null;
47
+
48
+ // Load history
49
+ this.loadHistory();
50
+
51
+ // Register built-in commands
52
+ this.registerBuiltinCommands();
53
+
54
+ // Setup WebSocket event handlers
55
+ this.setupWebSocketHandlers();
56
+ }
57
+
58
+ /**
59
+ * Setup WebSocket event handlers
60
+ */
61
+ setupWebSocketHandlers() {
62
+ this.ws.on('connected', (data) => {
63
+ this.printMessage(chalk.green(`✅ Connected to ThinkNCollab server (ID: ${data.socketId})`));
64
+ this.emit('ws:connected', data);
65
+ });
66
+
67
+ this.ws.on('disconnected', (data) => {
68
+ this.printMessage(chalk.yellow(`⚠️ Disconnected: ${data.reason}`));
69
+ this.emit('ws:disconnected', data);
70
+ });
71
+
72
+ this.ws.on('reconnected', (data) => {
73
+ this.printMessage(chalk.green(`✅ Reconnected after ${data.attemptNumber} attempts`));
74
+ this.emit('ws:reconnected', data);
75
+ });
76
+
77
+ this.ws.on('message', (data) => {
78
+ this.handleIncomingMessage(data);
79
+ });
80
+
81
+ this.ws.on('notification', (data) => {
82
+ this.handleNotification(data);
83
+ });
84
+
85
+ this.ws.on('userJoined', (data) => {
86
+ this.printMessage(chalk.cyan(`👤 ${data.username} joined the room`));
87
+ this.emit('user:joined', data);
88
+ });
89
+
90
+ this.ws.on('userLeft', (data) => {
91
+ this.printMessage(chalk.yellow(`👤 ${data.username} left the room`));
92
+ this.emit('user:left', data);
93
+ });
94
+
95
+ this.ws.on('userTyping', (data) => {
96
+ if (data.isTyping) {
97
+ this.showTypingIndicator(data.username);
98
+ } else {
99
+ this.hideTypingIndicator(data.username);
100
+ }
101
+ });
102
+
103
+ this.ws.on('terminalOutput', (data) => {
104
+ this.printMessage(chalk.dim(`[${data.username}] $ ${data.command}`));
105
+ this.printMessage(chalk.gray(data.output));
106
+ });
107
+
108
+ this.ws.on('error', (error) => {
109
+ this.printMessage(chalk.red(`❌ WebSocket error: ${error.message}`));
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Start the shell
115
+ */
116
+ async start() {
117
+ this.showWelcome();
118
+
119
+ this.rl = readline.createInterface({
120
+ input: process.stdin,
121
+ output: process.stdout,
122
+ prompt: this.getPrompt(),
123
+ completer: this.completer.bind(this)
124
+ });
125
+
126
+ // Handle input
127
+ this.rl.on('line', async (line) => {
128
+ const input = line.trim();
129
+
130
+ if (input) {
131
+ this.history.push(input);
132
+ this.saveHistory();
133
+ await this.execute(input);
134
+ }
135
+
136
+ this.rl.setPrompt(this.getPrompt());
137
+ this.rl.prompt();
138
+ });
139
+
140
+ // Handle exit
141
+ this.rl.on('close', () => {
142
+ this.cleanup();
143
+ process.exit(0);
144
+ });
145
+
146
+ // Handle signals
147
+ process.on('SIGINT', () => {
148
+ this.handleInterrupt();
149
+ });
150
+
151
+ this.rl.prompt();
152
+
153
+ // Auto-connect if configured
154
+ if (this.config.websocket.autoConnect) {
155
+ await this.ws.connect();
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Execute a command
161
+ */
162
+ async execute(input) {
163
+ if (!input.trim()) return;
164
+
165
+ // Parse command with quote support
166
+ const args = this.parseCommand(input);
167
+ const command = args[0].toLowerCase();
168
+ const commandArgs = args.slice(1);
169
+
170
+ // Add to history
171
+ this.history.push(input);
172
+ this.saveHistory();
173
+
174
+ // Handle typing indicator
175
+ this.handleTyping();
176
+
177
+ // Check for variable assignment (e.g., VAR=value)
178
+ if (command.includes('=') && args.length === 1) {
179
+ this.setVariable(command);
180
+ return;
181
+ }
182
+
183
+ // Check for alias
184
+ if (this.aliases.has(command)) {
185
+ const aliasCmd = this.aliases.get(command);
186
+ // Preserve original args when expanding alias
187
+ return this.execute(aliasCmd + (commandArgs.length ? ' ' + commandArgs.join(' ') : ''));
188
+ }
189
+
190
+ // Check built-in commands
191
+ if (this.commands.has(command)) {
192
+ try {
193
+ const cmd = this.commands.get(command);
194
+ const result = await cmd.handler(commandArgs, this);
195
+ if (result !== undefined) {
196
+ console.log(result);
197
+ }
198
+ } catch (error) {
199
+ console.log(chalk.red(`Error: ${error.message}`));
200
+ }
201
+ return;
202
+ }
203
+
204
+ // Execute system command
205
+ await this.executeSystemCommand(input);
206
+ }
207
+ /**
208
+ * Register built-in commands
209
+ */
210
+ registerBuiltinCommands() {
211
+ // Core shell commands
212
+ this.registerCommand('help', this.helpCommand.bind(this), 'Show this help');
213
+ this.registerCommand('exit', () => process.exit(0), 'Exit the shell');
214
+ this.registerCommand('quit', () => process.exit(0), 'Exit the shell');
215
+ this.registerCommand('clear', () => console.clear(), 'Clear the screen');
216
+ this.registerCommand('history', this.historyCommand.bind(this), 'Show command history');
217
+
218
+ // Directory commands
219
+ this.registerCommand('cd', this.cdCommand.bind(this), 'Change directory');
220
+ this.registerCommand('pwd', () => process.cwd(), 'Print working directory');
221
+ this.registerCommand('ls', this.lsCommand.bind(this), 'List files');
222
+
223
+ // WebSocket commands
224
+ this.registerCommand('connect', this.connectCommand.bind(this), 'Connect to WebSocket server');
225
+ this.registerCommand('disconnect', () => this.ws.disconnect(), 'Disconnect from WebSocket');
226
+ this.registerCommand('status', this.statusCommand.bind(this), 'Show connection status');
227
+
228
+ // Room commands
229
+ this.registerCommand('join', this.joinCommand.bind(this), 'Join a collaboration room');
230
+ this.registerCommand('leave', () => this.ws.leaveRoom(), 'Leave current room');
231
+ this.registerCommand('rooms', this.roomsCommand.bind(this), 'List available rooms');
232
+ this.registerCommand('users', this.usersCommand.bind(this), 'List users in room');
233
+
234
+ // Message commands
235
+ this.registerCommand('say', this.sayCommand.bind(this), 'Send a message to the room');
236
+ this.registerCommand('msg', this.sayCommand.bind(this), 'Send a message (alias)');
237
+
238
+ // Notification commands
239
+ this.registerCommand('notify', this.notifyCommand.bind(this), 'Send a notification');
240
+
241
+ // Alias commands
242
+ this.registerCommand('alias', this.aliasCommand.bind(this), 'Create an alias');
243
+ this.registerCommand('unalias', this.unaliasCommand.bind(this), 'Remove an alias');
244
+
245
+ // Variable commands
246
+ this.registerCommand('set', this.setCommand.bind(this), 'Set a variable');
247
+ this.registerCommand('unset', this.unsetCommand.bind(this), 'Unset a variable');
248
+
249
+ // Add aliases
250
+ this.addAlias('ll', 'ls -la');
251
+ this.addAlias('..', 'cd ..');
252
+ this.addAlias('...', 'cd ../..');
253
+ this.addAlias('~', 'cd ~');
254
+ }
255
+
256
+ /**
257
+ * Register a command
258
+ */
259
+ registerCommand(name, handler, description = '') {
260
+ this.commands.set(name, { handler, description });
261
+ }
262
+
263
+ /**
264
+ * Add an alias
265
+ */
266
+ addAlias(alias, command) {
267
+ this.aliases.set(alias, command);
268
+ }
269
+
270
+ /**
271
+ * Get formatted prompt
272
+ */
273
+ getPrompt() {
274
+ let prompt = '';
275
+
276
+ // Add room indicator if in a room
277
+ const room = this.ws.getCurrentRoom();
278
+ if (room) {
279
+ prompt += chalk.magenta(`[${room}] `);
280
+ }
281
+
282
+ // Add connection indicator
283
+ if (this.ws.isConnected()) {
284
+ prompt += chalk.green('● ');
285
+ }
286
+
287
+ // FIX: Better path display for Windows
288
+ let displayPath = this.currentDir;
289
+ const homeDir = os.homedir();
290
+
291
+ if (process.platform === 'win32') {
292
+ // Windows: Show clean path without C:\Users\username
293
+ if (displayPath.toLowerCase() === homeDir.toLowerCase()) {
294
+ displayPath = '~'; // At home directory
295
+ } else if (displayPath.toLowerCase().startsWith(homeDir.toLowerCase())) {
296
+ // In subdirectory of home
297
+ const relativePath = displayPath.slice(homeDir.length);
298
+ displayPath = '~' + relativePath.replace(/\\/g, '/');
299
+ } else {
300
+ // Outside home, show full path with forward slashes for consistency
301
+ displayPath = displayPath.replace(/\\/g, '/');
302
+ }
303
+ } else {
304
+ // Unix: Use ~ for home
305
+ displayPath = displayPath.replace(homeDir, '~');
306
+ }
307
+
308
+ prompt += chalk.cyan(`${this.username}@thinknsh`) + ':';
309
+ prompt += chalk.blue(displayPath) + ' ';
310
+
311
+ // Add prompt symbol
312
+ prompt += chalk.green('$ ');
313
+
314
+ return prompt;
315
+ }
316
+
317
+ /**
318
+ * Parse command with quotes
319
+ */
320
+ /**
321
+ * Parse command with support for quoted strings and escaped spaces
322
+ */
323
+ parseCommand(input) {
324
+ const args = [];
325
+ let current = '';
326
+ let inQuotes = false;
327
+ let quoteChar = '';
328
+ let escapeNext = false;
329
+
330
+ for (let i = 0; i < input.length; i++) {
331
+ const char = input[i];
332
+
333
+ // Handle escape character
334
+ if (char === '\\' && !escapeNext && !inQuotes) {
335
+ escapeNext = true;
336
+ continue;
337
+ }
338
+
339
+ if (escapeNext) {
340
+ current += char;
341
+ escapeNext = false;
342
+ continue;
343
+ }
344
+
345
+ // Handle quotes
346
+ if ((char === '"' || char === "'") && !inQuotes) {
347
+ inQuotes = true;
348
+ quoteChar = char;
349
+ continue;
350
+ } else if (char === quoteChar && inQuotes) {
351
+ inQuotes = false;
352
+ quoteChar = '';
353
+ continue;
354
+ }
355
+
356
+ // Handle spaces (argument separator)
357
+ if (char === ' ' && !inQuotes) {
358
+ if (current) {
359
+ args.push(current);
360
+ current = '';
361
+ }
362
+ continue;
363
+ }
364
+
365
+ // Add character to current argument
366
+ current += char;
367
+ }
368
+
369
+ // Add last argument
370
+ if (current) {
371
+ args.push(current);
372
+ }
373
+
374
+ return args;
375
+ }
376
+
377
+ /**
378
+ * Execute system command
379
+ */
380
+ executeSystemCommand(command) {
381
+ return new Promise((resolve) => {
382
+ const child = spawn(command, [], {
383
+ cwd: this.currentDir,
384
+ shell: true,
385
+ stdio: 'inherit',
386
+ env: {
387
+ ...process.env,
388
+ THINKNCOLLAB_SHELL: '1'
389
+ }
390
+ });
391
+
392
+ child.on('close', (code) => {
393
+ if (code !== 0) {
394
+ // Command failed, but we don't show error for normal commands
395
+ }
396
+ resolve();
397
+ });
398
+
399
+ child.on('error', (error) => {
400
+ console.log(chalk.red(`Command not found: ${command}`));
401
+ resolve();
402
+ });
403
+ });
404
+ }
405
+
406
+ /**
407
+ * Command handlers
408
+ */
409
+ async helpCommand(args) {
410
+ console.log(chalk.cyan('\n📚 Available Commands:'));
411
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
412
+
413
+ const categories = {
414
+ 'Core': ['help', 'exit', 'quit', 'clear', 'history'],
415
+ 'Filesystem': ['cd', 'pwd', 'ls'],
416
+ 'WebSocket': ['connect', 'disconnect', 'status'],
417
+ 'Rooms': ['join', 'leave', 'rooms', 'users'],
418
+ 'Messages': ['say', 'msg'],
419
+ 'Notifications': ['notify'],
420
+ 'Aliases': ['alias', 'unalias'],
421
+ 'Variables': ['set', 'unset']
422
+ };
423
+
424
+ for (const [category, cmds] of Object.entries(categories)) {
425
+ console.log(chalk.yellow(`\n${category}:`));
426
+ cmds.forEach(cmd => {
427
+ const command = this.commands.get(cmd);
428
+ if (command) {
429
+ console.log(` ${chalk.green(cmd.padEnd(12))} - ${command.description}`);
430
+ }
431
+ });
432
+ }
433
+
434
+ console.log(chalk.dim('\nAny other command is passed to your system shell\n'));
435
+ }
436
+
437
+ async cdCommand(args) {
438
+ // Join all args to handle paths with spaces
439
+ const target = args.join(' ').trim() || os.homedir();
440
+
441
+ if (!target) {
442
+ // No args, go home
443
+ try {
444
+ process.chdir(os.homedir());
445
+ this.currentDir = process.cwd();
446
+ } catch (error) {
447
+ console.log(chalk.red(`cd: ${error.message}`));
448
+ }
449
+ return;
450
+ }
451
+
452
+ try {
453
+ let newPath;
454
+
455
+ // Handle special cases
456
+ if (target === '~' || target === '~/' || target === '~\\') {
457
+ newPath = os.homedir();
458
+ } else if (target.startsWith('~/') || target.startsWith('~\\')) {
459
+ // Handle ~/path with possible spaces
460
+ const rest = target.slice(2);
461
+ newPath = path.join(os.homedir(), rest);
462
+ } else if (target === '..') {
463
+ newPath = path.dirname(this.currentDir);
464
+ } else if (target === '.') {
465
+ newPath = this.currentDir;
466
+ } else if (target === '-') {
467
+ // Go to previous directory
468
+ newPath = this.previousDir || this.currentDir;
469
+ } else {
470
+ // Handle Windows paths with spaces
471
+ if (process.platform === 'win32') {
472
+ // Check if it's an absolute path (with drive letter)
473
+ if (target.match(/^[a-zA-Z]:[\\/]/) || target.match(/^[a-zA-Z]:$/)) {
474
+ newPath = target;
475
+ } else {
476
+ // Relative path - resolve from current directory
477
+ newPath = path.resolve(this.currentDir, target);
478
+ }
479
+ } else {
480
+ newPath = path.resolve(this.currentDir, target);
481
+ }
482
+ }
483
+
484
+ // Normalize path (fixes any // or \/ issues)
485
+ newPath = path.normalize(newPath);
486
+
487
+ // Check if directory exists
488
+ await fs.access(newPath);
489
+ const stats = await fs.stat(newPath);
490
+
491
+ if (!stats.isDirectory()) {
492
+ console.log(chalk.red(`cd: ${target}: Not a directory`));
493
+ return;
494
+ }
495
+
496
+ // Store previous directory
497
+ this.previousDir = this.currentDir;
498
+
499
+ // Change directory
500
+ process.chdir(newPath);
501
+ this.currentDir = process.cwd();
502
+
503
+ } catch (error) {
504
+ console.log(chalk.red(`cd: ${target}: No such directory`));
505
+ }
506
+ }
507
+
508
+ async lsCommand(args) {
509
+ try {
510
+ const files = await fs.readdir(this.currentDir);
511
+ const showAll = args.includes('-a') || args.includes('--all');
512
+
513
+ const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
514
+
515
+ // Get file stats
516
+ const fileList = await Promise.all(
517
+ filtered.map(async (file) => {
518
+ const stat = await fs.stat(path.join(this.currentDir, file));
519
+ return {
520
+ name: file,
521
+ isDir: stat.isDirectory(),
522
+ size: stat.size,
523
+ mode: stat.mode
524
+ };
525
+ })
526
+ );
527
+
528
+ // Colorize output
529
+ fileList.forEach(file => {
530
+ if (file.isDir) {
531
+ console.log(chalk.blue(file.name + '/'));
532
+ } else if (file.mode & 0o111) {
533
+ console.log(chalk.green(file.name + '*'));
534
+ } else {
535
+ console.log(file.name);
536
+ }
537
+ });
538
+ } catch (error) {
539
+ console.log(chalk.red(`ls: ${error.message}`));
540
+ }
541
+ }
542
+
543
+ async connectCommand(args) {
544
+ const serverUrl = args[0] || this.config.websocket.serverUrl;
545
+
546
+ console.log(chalk.cyan(`🔌 Connecting to ${serverUrl}...`));
547
+
548
+ try {
549
+ await this.ws.connect();
550
+ } catch (error) {
551
+ console.log(chalk.red(`❌ Connection failed: ${error.message}`));
552
+ }
553
+ }
554
+
555
+ async statusCommand() {
556
+ console.log(chalk.cyan('\n📊 Status:'));
557
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
558
+
559
+ const connected = this.ws.isConnected();
560
+ console.log(`WebSocket: ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
561
+
562
+ if (connected) {
563
+ console.log(`Socket ID: ${chalk.dim(this.ws.getSocketId())}`);
564
+ console.log(`Server: ${chalk.dim(this.ws.config.serverUrl)}`);
565
+ }
566
+
567
+ const room = this.ws.getCurrentRoom();
568
+ console.log(`Room: ${room ? chalk.green(room) : chalk.yellow('None')}`);
569
+
570
+ console.log(`Current Directory: ${chalk.blue(this.currentDir)}`);
571
+ console.log(`History: ${chalk.yellow(this.history.length)} commands`);
572
+ console.log(`Commands: ${chalk.green(this.commands.size)} registered`);
573
+ console.log(`Aliases: ${chalk.green(this.aliases.size)} defined`);
574
+ }
575
+
576
+ async joinCommand(args) {
577
+ const roomId = args[0];
578
+
579
+ if (!roomId) {
580
+ console.log(chalk.red('Usage: join <room-id> [password]'));
581
+ return;
582
+ }
583
+
584
+ const password = args[1];
585
+
586
+ if (!this.ws.isConnected()) {
587
+ console.log(chalk.yellow('Not connected. Connecting first...'));
588
+ await this.ws.connect();
589
+ }
590
+
591
+ console.log(chalk.cyan(`🔗 Joining room: ${roomId}...`));
592
+
593
+ try {
594
+ const result = await this.ws.joinRoom(roomId, password);
595
+ console.log(chalk.green(`✅ Joined room: ${result.roomName || roomId}`));
596
+ console.log(chalk.dim(`Participants: ${result.participantCount || '?'}`));
597
+ } catch (error) {
598
+ console.log(chalk.red(`❌ Failed to join: ${error.message}`));
599
+ }
600
+ }
601
+
602
+ async roomsCommand() {
603
+ if (!this.ws.isConnected()) {
604
+ console.log(chalk.yellow('Not connected to WebSocket server'));
605
+ return;
606
+ }
607
+
608
+ console.log(chalk.cyan('\n📋 Available Rooms:'));
609
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
610
+
611
+ // Request rooms list
612
+ this.ws.socket.emit('rooms:list');
613
+
614
+ this.ws.socket.once('rooms:list', (data) => {
615
+ if (data.rooms && data.rooms.length > 0) {
616
+ data.rooms.forEach(room => {
617
+ const current = room.id === this.ws.getCurrentRoom() ? chalk.green(' (current)') : '';
618
+ console.log(` ${chalk.cyan(room.name || room.id)}${current}`);
619
+ if (room.description) {
620
+ console.log(` ${chalk.dim(room.description)}`);
621
+ }
622
+ console.log(` ${chalk.yellow('👥 ' + (room.participantCount || 0))} participants`);
623
+ });
624
+ } else {
625
+ console.log(' No rooms available');
626
+ }
627
+ });
628
+ }
629
+
630
+ async usersCommand() {
631
+ if (!this.ws.isConnected()) {
632
+ console.log(chalk.yellow('Not connected'));
633
+ return;
634
+ }
635
+
636
+ const room = this.ws.getCurrentRoom();
637
+ if (!room) {
638
+ console.log(chalk.yellow('Not in a room'));
639
+ return;
640
+ }
641
+
642
+ console.log(chalk.cyan(`\n👥 Users in ${room}:`));
643
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
644
+
645
+ try {
646
+ const participants = await this.ws.getParticipants();
647
+ participants.forEach(user => {
648
+ const typing = user.isTyping ? chalk.green(' (typing...)') : '';
649
+ console.log(` ${chalk.green('●')} ${user.username}${typing}`);
650
+ if (user.status) {
651
+ console.log(` ${chalk.dim(user.status)}`);
652
+ }
653
+ });
654
+ } catch (error) {
655
+ console.log(chalk.red(`Failed to get users: ${error.message}`));
656
+ }
657
+ }
658
+
659
+ async sayCommand(args) {
660
+ const message = args.join(' ');
661
+
662
+ if (!message) {
663
+ console.log(chalk.red('Usage: say <message>'));
664
+ return;
665
+ }
666
+
667
+ try {
668
+ await this.ws.sendMessage(message);
669
+
670
+ // Clear typing indicator
671
+ if (this.isTyping) {
672
+ this.ws.sendTyping(false);
673
+ this.isTyping = false;
674
+ clearTimeout(this.typingTimeout);
675
+ }
676
+
677
+ // Echo locally
678
+ console.log(chalk.green(`You: ${message}`));
679
+ } catch (error) {
680
+ console.log(chalk.red(`Failed to send: ${error.message}`));
681
+ }
682
+ }
683
+
684
+ async notifyCommand(args) {
685
+ const type = args[0] || 'info';
686
+ const message = args.slice(1).join(' ');
687
+
688
+ if (!message) {
689
+ console.log(chalk.red('Usage: notify <type> <message>'));
690
+ console.log(chalk.dim('Types: info, success, warning, error'));
691
+ return;
692
+ }
693
+
694
+ try {
695
+ await this.ws.sendNotification({
696
+ type,
697
+ title: 'Shell Notification',
698
+ message
699
+ });
700
+
701
+ console.log(chalk.green('✅ Notification sent'));
702
+ } catch (error) {
703
+ console.log(chalk.red(`Failed to send notification: ${error.message}`));
704
+ }
705
+ }
706
+
707
+ async aliasCommand(args) {
708
+ if (args.length === 0) {
709
+ // List aliases
710
+ console.log(chalk.cyan('\n🔧 Current Aliases:'));
711
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
712
+
713
+ if (this.aliases.size === 0) {
714
+ console.log(' No aliases defined');
715
+ } else {
716
+ for (const [alias, cmd] of this.aliases) {
717
+ console.log(` ${chalk.green(alias.padEnd(12))} → ${chalk.dim(cmd)}`);
718
+ }
719
+ }
720
+ return;
721
+ }
722
+
723
+ if (args.length < 2) {
724
+ console.log(chalk.red('Usage: alias <name> <command>'));
725
+ return;
726
+ }
727
+
728
+ const name = args[0];
729
+ const command = args.slice(1).join(' ');
730
+
731
+ this.addAlias(name, command);
732
+ console.log(chalk.green(`✅ Alias created: ${name} → ${command}`));
733
+ }
734
+
735
+ async unaliasCommand(args) {
736
+ if (args.length === 0) {
737
+ console.log(chalk.red('Usage: unalias <name>'));
738
+ return;
739
+ }
740
+
741
+ const name = args[0];
742
+
743
+ if (this.aliases.delete(name)) {
744
+ console.log(chalk.green(`✅ Alias removed: ${name}`));
745
+ } else {
746
+ console.log(chalk.red(`Alias not found: ${name}`));
747
+ }
748
+ }
749
+
750
+ async setCommand(args) {
751
+ if (args.length === 0) {
752
+ // List variables
753
+ console.log(chalk.cyan('\n📋 Variables:'));
754
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
755
+
756
+ if (this.variables.size === 0) {
757
+ console.log(' No variables set');
758
+ } else {
759
+ for (const [key, value] of this.variables) {
760
+ console.log(` ${chalk.green(key.padEnd(12))} = ${chalk.yellow(JSON.stringify(value))}`);
761
+ }
762
+ }
763
+ return;
764
+ }
765
+
766
+ if (args.length < 2) {
767
+ console.log(chalk.red('Usage: set <name> <value>'));
768
+ return;
769
+ }
770
+
771
+ const name = args[0];
772
+ const value = args.slice(1).join(' ');
773
+
774
+ this.variables.set(name, value);
775
+ console.log(chalk.green(`✅ Variable set: ${name} = ${value}`));
776
+ }
777
+
778
+ async unsetCommand(args) {
779
+ if (args.length === 0) {
780
+ console.log(chalk.red('Usage: unset <name>'));
781
+ return;
782
+ }
783
+
784
+ const name = args[0];
785
+
786
+ if (this.variables.delete(name)) {
787
+ console.log(chalk.green(`✅ Variable removed: ${name}`));
788
+ } else {
789
+ console.log(chalk.red(`Variable not found: ${name}`));
790
+ }
791
+ }
792
+
793
+ setVariable(assignment) {
794
+ const parts = assignment.split('=');
795
+ const name = parts[0].trim();
796
+ const value = parts.slice(1).join('=').trim();
797
+
798
+ // Remove quotes if present
799
+ const cleanValue = value.replace(/^["']|["']$/g, '');
800
+
801
+ this.variables.set(name, cleanValue);
802
+ console.log(chalk.dim(`Variable set: ${name}=${cleanValue}`));
803
+ }
804
+
805
+ historyCommand() {
806
+ this.history.forEach((cmd, i) => {
807
+ console.log(`${(i + 1).toString().padStart(4)} ${cmd}`);
808
+ });
809
+ }
810
+
811
+ /**
812
+ * Handle incoming messages
813
+ */
814
+ handleIncomingMessage(data) {
815
+ // Clear current line
816
+ readline.clearLine(process.stdout, 0);
817
+ readline.cursorTo(process.stdout, 0);
818
+
819
+ // Show message
820
+ const prefix = data.username === this.username ? 'You' : data.username;
821
+ console.log(`${chalk.cyan(`[${prefix}]`)} ${data.message}`);
822
+
823
+ // Redraw prompt
824
+ this.rl.prompt(true);
825
+ }
826
+
827
+ /**
828
+ * Handle notifications
829
+ */
830
+ handleNotification(data) {
831
+ readline.clearLine(process.stdout, 0);
832
+ readline.cursorTo(process.stdout, 0);
833
+
834
+ let color = chalk.blue;
835
+ switch (data.type) {
836
+ case 'success': color = chalk.green; break;
837
+ case 'warning': color = chalk.yellow; break;
838
+ case 'error': color = chalk.red; break;
839
+ default: color = chalk.blue;
840
+ }
841
+
842
+ console.log(color(`📢 ${data.title}: ${data.message}`));
843
+ this.rl.prompt(true);
844
+ }
845
+
846
+ /**
847
+ * Show typing indicator
848
+ */
849
+ showTypingIndicator(username) {
850
+ readline.clearLine(process.stdout, 0);
851
+ readline.cursorTo(process.stdout, 0);
852
+ console.log(chalk.dim(`👤 ${username} is typing...`));
853
+ this.rl.prompt(true);
854
+ }
855
+
856
+ /**
857
+ * Hide typing indicator
858
+ */
859
+ hideTypingIndicator(username) {
860
+ // Just redraw prompt
861
+ this.rl.prompt(true);
862
+ }
863
+
864
+ /**
865
+ * Handle typing detection
866
+ */
867
+ handleTyping() {
868
+ if (!this.isTyping && this.ws.getCurrentRoom()) {
869
+ this.isTyping = true;
870
+ this.ws.sendTyping(true);
871
+ }
872
+
873
+ clearTimeout(this.typingTimeout);
874
+ this.typingTimeout = setTimeout(() => {
875
+ if (this.isTyping) {
876
+ this.isTyping = false;
877
+ this.ws.sendTyping(false);
878
+ }
879
+ }, 2000);
880
+ }
881
+
882
+ /**
883
+ * Print message without breaking prompt
884
+ */
885
+ printMessage(message) {
886
+ readline.clearLine(process.stdout, 0);
887
+ readline.cursorTo(process.stdout, 0);
888
+ console.log(message);
889
+ this.rl.prompt(true);
890
+ }
891
+
892
+ /**
893
+ * Show welcome message
894
+ */
895
+ showWelcome() {
896
+ const figlet = require('figlet');
897
+
898
+ console.log(chalk.cyan(figlet.textSync('ThinkNCollab', {
899
+ font: 'Standard',
900
+ horizontalLayout: 'default',
901
+ verticalLayout: 'default'
902
+ })));
903
+
904
+ console.log(chalk.cyan(`
905
+ ╔══════════════════════════════════════════════════════╗
906
+ ║ ThinkNCollab Shell v1.0.0 ║
907
+ ║ Type 'help' for commands ║
908
+ ╚══════════════════════════════════════════════════════╝
909
+ `));
910
+ }
911
+
912
+ /**
913
+ * Command completer
914
+ */
915
+ completer(line) {
916
+ const commands = Array.from(this.commands.keys());
917
+ const aliases = Array.from(this.aliases.keys());
918
+ const all = [...commands, ...aliases];
919
+
920
+ const hits = all.filter(cmd => cmd.startsWith(line));
921
+ return [hits.length ? hits : all, line];
922
+ }
923
+
924
+ /**
925
+ * Handle interrupt (Ctrl+C)
926
+ */
927
+ handleInterrupt() {
928
+ if (this.isTyping) {
929
+ this.isTyping = false;
930
+ this.ws.sendTyping(false);
931
+ clearTimeout(this.typingTimeout);
932
+ }
933
+
934
+ console.log(chalk.yellow('\nUse "exit" to quit, or press Ctrl+C again'));
935
+ }
936
+
937
+ /**
938
+ * Load command history
939
+ */
940
+ loadHistory() {
941
+ try {
942
+ if (fs.existsSync(this.config.historyFile)) {
943
+ const data = fs.readFileSync(this.config.historyFile, 'utf8');
944
+ this.history = data.split('\n').filter(line => line.trim());
945
+ }
946
+ } catch (error) {
947
+ // Ignore
948
+ }
949
+ }
950
+
951
+ /**
952
+ * Save command history
953
+ */
954
+ saveHistory() {
955
+ try {
956
+ const toSave = this.history.slice(-this.config.maxHistory);
957
+ fs.writeFileSync(this.config.historyFile, toSave.join('\n'));
958
+ } catch (error) {
959
+ // Ignore
960
+ }
961
+ }
962
+
963
+ /**
964
+ * Cleanup before exit
965
+ */
966
+ cleanup() {
967
+ if (this.ws) {
968
+ this.ws.disconnect();
969
+ }
970
+ this.saveHistory();
971
+ }
972
+ }
973
+
974
+ module.exports = ThinkNCollabShell;