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.
- package/Readme.md +26 -0
- package/bin/thinknsh.js +68 -0
- package/package.json +42 -0
- package/src/core/shell.js +974 -0
- package/src/core/websocket.js +385 -0
- package/src/events/notifications.js +198 -0
- package/src/utils/logger.js +169 -0
|
@@ -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;
|