thinkncollab-shell 0.0.1 → 0.0.2

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/src/core/shell.js CHANGED
@@ -1,972 +1,533 @@
1
1
  /**
2
2
  * ThinkNCollab Shell Core
3
- * Main shell class with WebSocket integration
3
+ * Inquirer REPL + TCP IPC for notification window
4
4
  */
5
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');
6
+ const path = require('path');
7
+ const fs = require('fs-extra');
8
+ const os = require('os');
9
+ const chalk = require('chalk');
10
+ const net = require('net');
11
+ const { spawn } = require('child_process');
12
12
  const WebSocketManager = require('./websocket');
13
- const EventEmitter = require('events');
13
+ const EventEmitter = require('events');
14
+ const CommandRegistry = require('../commands');
15
+ const ThinkNCollabAPI = require('./api-client');
16
+
17
+ // Find an available port starting from 7379
18
+ function getAvailablePort(startPort) {
19
+ return new Promise((resolve) => {
20
+ const server = net.createServer();
21
+ server.listen(startPort, '127.0.0.1', () => {
22
+ const port = server.address().port;
23
+ server.close(() => resolve(port));
24
+ });
25
+ server.on('error', () => resolve(getAvailablePort(startPort + 1)));
26
+ });
27
+ }
14
28
 
15
29
  class ThinkNCollabShell extends EventEmitter {
30
+
16
31
  constructor(options = {}) {
17
32
  super();
18
-
19
- // Configuration
33
+
20
34
  this.config = {
21
35
  historyFile: path.join(os.homedir(), '.thinknsh_history'),
22
- configDir: path.join(os.homedir(), '.thinkncollab'),
23
- maxHistory: 1000,
24
- prompt: options.prompt || 'thinknsh> ',
36
+ configDir: path.join(os.homedir(), '.thinkncollab'),
37
+ maxHistory: 1000,
25
38
  websocket: {
26
- serverUrl: options.serverUrl || 'https://api.thinkncollab.com',
39
+ serverUrl: options.serverUrl || 'https://thinkncollab.com/thinknsh',
27
40
  autoConnect: options.autoConnect || false
28
41
  },
29
42
  ...options
30
43
  };
31
-
32
- // Ensure config directory exists
44
+
33
45
  fs.ensureDirSync(this.config.configDir);
34
-
35
- // Initialize components
36
- this.ws = new WebSocketManager(this.config.websocket);
37
- this.rl = null;
46
+
47
+ this.ws = new WebSocketManager(this.config.websocket);
38
48
  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();
49
+ this.username = os.userInfo().username;
50
+ this.history = [];
51
+ this.running = false;
52
+
53
+ // TCP notification clients
54
+ this.notifyClients = new Set();
55
+ this.tcpServer = null;
56
+
57
+ this.api = new ThinkNCollabAPI({
58
+ apiUrl: options.apiUrl || 'https://thinkncollab.com',
59
+ wsUrl: options.wsUrl || 'https://thinkncollab.com'
60
+ });
61
+
62
+ this.commands = new Map();
63
+ this.aliases = new Map();
44
64
  this.variables = new Map();
45
- this.isTyping = false;
65
+ this.isTyping = false;
46
66
  this.typingTimeout = null;
47
-
48
- // Load history
67
+
49
68
  this.loadHistory();
50
-
51
- // Register built-in commands
52
69
  this.registerBuiltinCommands();
53
-
54
- // Setup WebSocket event handlers
70
+ this.commandRegistry = new CommandRegistry(this);
55
71
  this.setupWebSocketHandlers();
56
72
  }
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);
73
+
74
+ // ─── TCP Notification Server ──────────────────────────────────────────────
75
+
76
+ async startTCPServer() {
77
+ this.notifyPort = await getAvailablePort(7379);
78
+
79
+ return new Promise((resolve) => {
80
+ this.tcpServer = net.createServer((socket) => {
81
+ this.notifyClients.add(socket);
82
+ socket.on('close', () => this.notifyClients.delete(socket));
83
+ socket.on('error', () => this.notifyClients.delete(socket));
84
+ });
85
+
86
+ this.tcpServer.listen(this.notifyPort, '127.0.0.1', () => {
87
+ resolve();
88
+ });
89
+
90
+ this.tcpServer.on('error', (err) => {
91
+ console.log(chalk.yellow(`⚠️ Notification server: ${err.message}`));
92
+ resolve();
93
+ });
93
94
  });
94
-
95
- this.ws.on('userTyping', (data) => {
96
- if (data.isTyping) {
97
- this.showTypingIndicator(data.username);
95
+ }
96
+
97
+ // Send notification event to all connected notification windows
98
+ pushNotification(data) {
99
+ if (this.notifyClients.size === 0) return;
100
+ const line = JSON.stringify(data) + '\n';
101
+ for (const client of this.notifyClients) {
102
+ try { client.write(line); } catch {}
103
+ }
104
+ }
105
+
106
+ // ─── Spawn notification window (cross-platform) ───────────────────────────
107
+
108
+ spawnNotificationWindow() {
109
+ // Path to notification-window.js (same dir as this file)
110
+ const notifyScript = path.join(__dirname, 'notification-window.js');
111
+ const env = { ...process.env, THINKNSH_NOTIFY_PORT: String(this.notifyPort) };
112
+
113
+ try {
114
+ if (process.platform === 'win32') {
115
+ const isVSCode = process.env.TERM_PROGRAM === 'vscode';
116
+ if (isVSCode) {
117
+ console.log(chalk.yellow('\n VS Code detected — cannot auto-open new window.'));
118
+ console.log(chalk.dim(' Open a new terminal tab and run:'));
119
+ console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${this.notifyPort}; node "${notifyScript}"\n`));
120
+ } else {
121
+ // Use array args so cmd handles spaces in path correctly
122
+ const nodeExe = process.execPath;
123
+ spawn('cmd.exe', [
124
+ '/c', 'start', 'cmd.exe', '/k',
125
+ `"${nodeExe}" "${notifyScript}"`
126
+ ], { detached: true, stdio: 'ignore', env, shell: false }).unref();
127
+ }
128
+
129
+ } else if (process.platform === 'darwin') {
130
+ // macOS — Terminal.app
131
+ const script = `tell application "Terminal" to do script "THINKNSH_NOTIFY_PORT=${NOTIFY_PORT} node '${notifyScript}'"`;
132
+ spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
133
+
98
134
  } else {
99
- this.hideTypingIndicator(data.username);
135
+ // Linux — try common terminal emulators in order
136
+ const terminals = [
137
+ ['gnome-terminal', ['--', 'node', notifyScript]],
138
+ ['xterm', ['-e', `node "${notifyScript}"`]],
139
+ ['konsole', ['--noclose', '-e', 'node', notifyScript]],
140
+ ['xfce4-terminal', ['-e', `node "${notifyScript}"`]],
141
+ ['x-terminal-emulator', ['-e', `node "${notifyScript}"`]],
142
+ ];
143
+ let spawned = false;
144
+ for (const [cmd, cmdArgs] of terminals) {
145
+ try {
146
+ spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', env }).unref();
147
+ spawned = true;
148
+ break;
149
+ } catch {}
150
+ }
151
+ if (!spawned) {
152
+ console.log(chalk.yellow('⚠️ Could not auto-open notification window.'));
153
+ console.log(chalk.dim(` Run manually: node ${notifyScript}`));
154
+ }
100
155
  }
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
- });
156
+ } catch (err) {
157
+ console.log(chalk.yellow(`⚠️ Could not spawn notification window: ${err.message}`));
158
+ console.log(chalk.dim(` Run manually: node "${notifyScript}"`));
159
+ }
111
160
  }
112
-
113
- /**
114
- * Start the shell
115
- */
161
+
162
+ // ─── WebSocket → push to notification window ──────────────────────────────
163
+
164
+ setupWebSocketHandlers() {
165
+ this.ws.on('connected', (d) => this.pushNotification({ type: 'connected', socketId: d.socketId }));
166
+ this.ws.on('disconnected', (d) => this.pushNotification({ type: 'disconnected', reason: d.reason }));
167
+ this.ws.on('reconnected', (d) => this.pushNotification({ type: 'connected', socketId: 'reconnected' }));
168
+ this.ws.on('message', (d) => this.pushNotification({ type: 'message', from: d.username, text: d.message }));
169
+ this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
170
+ this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
171
+ this.ws.on('userTyping', (d) => this.pushNotification({ type: d.isTyping ? 'typing' : 'stoppedTyping', username: d.username }));
172
+ this.ws.on('notification', (d) => this.pushNotification({ type: 'notification', level: d.type, title: d.title, message: d.message }));
173
+ this.ws.on('error', (e) => this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message }));
174
+ }
175
+
176
+ // ─── Start ────────────────────────────────────────────────────────────────
177
+
116
178
  async start() {
117
179
  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
180
+
181
+ // 1. Start TCP server for notification IPC
182
+ await this.startTCPServer();
183
+ console.log(chalk.dim(` Notification server ready on port ${this.notifyPort}`));
184
+
185
+ // 2. Spawn notification window
186
+ this.spawnNotificationWindow();
187
+ console.log(chalk.dim(' Notification window opening...\n'));
188
+
189
+ this.running = true;
190
+
147
191
  process.on('SIGINT', () => {
148
- this.handleInterrupt();
192
+ console.log(chalk.yellow('\nUse "exit" to quit'));
149
193
  });
150
-
151
- this.rl.prompt();
152
-
153
- // Auto-connect if configured
194
+
154
195
  if (this.config.websocket.autoConnect) {
155
196
  await this.ws.connect();
156
197
  }
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);
198
+
199
+ const { input } = require('@inquirer/prompts');
200
+
201
+ while (this.running) {
202
+ try {
203
+ const line = await input({
204
+ message: this.getPromptText(),
205
+ required: false,
206
+ }).catch(() => null);
207
+
208
+ if (line === null) {
209
+ console.log(chalk.yellow('Use "exit" to quit'));
210
+ continue;
211
+ }
212
+
213
+ const trimmed = line.trim();
214
+ if (!trimmed) continue;
215
+
216
+ this.history.push(trimmed);
217
+ this.saveHistory();
218
+ await this.execute(trimmed);
219
+
220
+ } catch (err) {
221
+ if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
222
+ console.log(chalk.yellow('Use "exit" to quit'));
223
+ } else {
224
+ console.log(chalk.red(`Shell error: ${err.message}`));
225
+ }
197
226
  }
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
227
  }
303
- } else {
304
- // Unix: Use ~ for home
305
- displayPath = displayPath.replace(homeDir, '~');
306
228
  }
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 = '';
229
+
230
+ restoreReadline() {}
231
+
232
+ // ─── Execute ──────────────────────────────────────────────────────────────
233
+
234
+ async execute(input) {
235
+ if (!input.trim()) return;
236
+
237
+ const args = this.parseCommand(input);
238
+ const cmdName = args[0].toLowerCase();
239
+ const cmdArgs = args.slice(1);
240
+
241
+ if (this.commands.has(cmdName)) {
242
+ const cmd = this.commands.get(cmdName);
243
+ try {
244
+ const result = await cmd.handler(cmdArgs, this);
245
+ if (result !== undefined) console.log(result);
246
+ } catch (err) {
247
+ console.log(chalk.red(`Error: ${err.message}`));
361
248
  }
362
- continue;
249
+ return;
250
+ }
251
+
252
+ if (this.commandRegistry) {
253
+ const handled = await this.commandRegistry.execute(input, this);
254
+ if (handled) return;
363
255
  }
364
-
365
- // Add character to current argument
366
- current += char;
256
+
257
+ await this.executeSystemCommand(input);
367
258
  }
368
-
369
- // Add last argument
370
- if (current) {
371
- args.push(current);
259
+
260
+ // ─── Prompt ───────────────────────────────────────────────────────────────
261
+
262
+ getPromptText() {
263
+ let p = '';
264
+ const room = this.ws.getCurrentRoom?.();
265
+ if (room) p += `[${room}] `;
266
+ if (this.ws.isConnected?.()) p += '● ';
267
+
268
+ let displayPath = this.currentDir;
269
+ const homeDir = os.homedir();
270
+
271
+ if (process.platform === 'win32') {
272
+ if (displayPath.toLowerCase() === homeDir.toLowerCase()) displayPath = '~';
273
+ else if (displayPath.toLowerCase().startsWith(homeDir.toLowerCase()))
274
+ displayPath = '~' + displayPath.slice(homeDir.length).replace(/\\/g, '/');
275
+ else displayPath = displayPath.replace(/\\/g, '/');
276
+ } else {
277
+ displayPath = displayPath.replace(homeDir, '~');
278
+ }
279
+
280
+ p += `${this.username}@thinknsh:${displayPath} $`;
281
+ return p;
372
282
  }
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
- });
283
+
284
+ getPrompt() { return this.getPromptText(); }
285
+
286
+ printMessage(msg) { console.log(msg); }
287
+
288
+ // ─── Built-ins ────────────────────────────────────────────────────────────
289
+
290
+ registerBuiltinCommands() {
291
+ this.registerCommand('help', this.helpCommand.bind(this), 'Show available commands');
292
+ this.registerCommand('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
293
+ this.registerCommand('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
294
+ this.registerCommand('clear', () => console.clear(), 'Clear screen');
295
+ this.registerCommand('history', this.historyCommand.bind(this), 'Show command history');
296
+ this.registerCommand('cd', this.cdCommand.bind(this), 'Change directory');
297
+ this.registerCommand('pwd', () => console.log(process.cwd()), 'Print working directory');
298
+ this.registerCommand('ls', this.lsCommand.bind(this), 'List files');
299
+ this.registerCommand('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
300
+ this.registerCommand('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
301
+ this.registerCommand('status', this.statusCommand.bind(this), 'Show status');
302
+ this.registerCommand('join', this.joinCommand.bind(this), 'Join a room');
303
+ this.registerCommand('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
304
+ this.registerCommand('rooms', this.roomsCommand.bind(this), 'List rooms');
305
+ this.registerCommand('users', this.usersCommand.bind(this), 'List users in room');
306
+ this.registerCommand('say', this.sayCommand.bind(this), 'Send a message');
307
+ this.registerCommand('msg', this.sayCommand.bind(this), 'Send a message (alias)');
308
+ this.registerCommand('notify', this.notifyCommand.bind(this), 'Send a notification');
309
+ this.registerCommand('alias', this.aliasCommand.bind(this), 'Create an alias');
310
+ this.registerCommand('unalias', this.unaliasCommand.bind(this), 'Remove an alias');
311
+ this.registerCommand('set', this.setCommand.bind(this), 'Set a variable');
312
+ this.registerCommand('unset', this.unsetCommand.bind(this), 'Unset a variable');
313
+
314
+ this.addAlias('ll', 'ls -la');
315
+ this.addAlias('..', 'cd ..');
316
+ this.addAlias('...', 'cd ../..');
317
+ this.addAlias('~', 'cd ~');
404
318
  }
405
-
406
- /**
407
- * Command handlers
408
- */
409
- async helpCommand(args) {
410
- console.log(chalk.cyan('\n📚 Available Commands:'));
411
- console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
412
-
319
+
320
+ registerCommand(name, handler, description = '') { this.commands.set(name, { handler, description }); }
321
+ addAlias(alias, command) { this.aliases.set(alias, command); }
322
+
323
+ async helpCommand() {
413
324
  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'],
325
+ 'Core': ['help', 'exit', 'quit', 'clear', 'history'],
326
+ 'Filesystem': ['cd', 'pwd', 'ls'],
327
+ 'WebSocket': ['connect', 'disconnect', 'status'],
328
+ 'Rooms': ['join', 'leave', 'rooms', 'users'],
329
+ 'Messages': ['say', 'msg'],
419
330
  'Notifications': ['notify'],
420
- 'Aliases': ['alias', 'unalias'],
421
- 'Variables': ['set', 'unset']
331
+ 'Aliases': ['alias', 'unalias'],
332
+ 'Variables': ['set', 'unset']
422
333
  };
423
-
334
+ console.log(chalk.cyan('\n📚 Available Commands:'));
335
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
424
336
  for (const [category, cmds] of Object.entries(categories)) {
425
337
  console.log(chalk.yellow(`\n${category}:`));
426
338
  cmds.forEach(cmd => {
427
- const command = this.commands.get(cmd);
428
- if (command) {
429
- console.log(` ${chalk.green(cmd.padEnd(12))} - ${command.description}`);
430
- }
339
+ const c = this.commands.get(cmd);
340
+ if (c) console.log(` ${chalk.green(cmd.padEnd(12))} - ${c.description}`);
431
341
  });
432
342
  }
433
-
434
- console.log(chalk.dim('\nAny other command is passed to your system shell\n'));
343
+ console.log(chalk.dim('\nAny other command runs in your system shell\n'));
344
+ }
345
+
346
+ historyCommand() {
347
+ this.history.forEach((cmd, i) => console.log(`${(i + 1).toString().padStart(4)} ${cmd}`));
435
348
  }
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
349
+
350
+ async cdCommand(args) {
351
+ const target = args.join(' ').trim() || os.homedir();
443
352
  try {
444
- process.chdir(os.homedir());
353
+ let newPath;
354
+ if (target === '~' || target === '~/' || target === '~\\') newPath = os.homedir();
355
+ else if (target.startsWith('~/') || target.startsWith('~\\')) newPath = path.join(os.homedir(), target.slice(2));
356
+ else if (target === '..') newPath = path.dirname(this.currentDir);
357
+ else if (target === '-') newPath = this.previousDir || this.currentDir;
358
+ else if (process.platform === 'win32' && target.match(/^[a-zA-Z]:[\\/]/)) newPath = target;
359
+ else newPath = path.resolve(this.currentDir, target);
360
+ newPath = path.normalize(newPath);
361
+ const stats = await fs.stat(newPath);
362
+ if (!stats.isDirectory()) { console.log(chalk.red(`cd: not a directory: ${target}`)); return; }
363
+ this.previousDir = this.currentDir;
364
+ process.chdir(newPath);
445
365
  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`));
366
+ } catch { console.log(chalk.red(`cd: no such directory: ${target}`)); }
505
367
  }
506
- }
507
-
368
+
508
369
  async lsCommand(args) {
509
370
  try {
510
- const files = await fs.readdir(this.currentDir);
511
- const showAll = args.includes('-a') || args.includes('--all');
512
-
371
+ const files = await fs.readdir(this.currentDir);
372
+ const showAll = args.includes('-a') || args.includes('--all');
513
373
  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
- }
374
+ const fileList = await Promise.all(filtered.map(async (file) => {
375
+ const stat = await fs.stat(path.join(this.currentDir, file));
376
+ return { name: file, isDir: stat.isDirectory(), mode: stat.mode };
377
+ }));
378
+ fileList.forEach(f => {
379
+ if (f.isDir) console.log(chalk.blue(f.name + '/'));
380
+ else if (f.mode & 0o111) console.log(chalk.green(f.name + '*'));
381
+ else console.log(f.name);
537
382
  });
538
- } catch (error) {
539
- console.log(chalk.red(`ls: ${error.message}`));
540
- }
383
+ } catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
541
384
  }
542
-
385
+
543
386
  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
- }
387
+ console.log(chalk.cyan(`🔌 Connecting...`));
388
+ try { await this.ws.connect(); }
389
+ catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
553
390
  }
554
-
391
+
555
392
  async statusCommand() {
556
- console.log(chalk.cyan('\n📊 Status:'));
557
- console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
558
-
559
393
  const connected = this.ws.isConnected();
394
+ console.log(chalk.cyan('\n📊 Status:'));
560
395
  console.log(`WebSocket: ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
561
-
562
396
  if (connected) {
563
397
  console.log(`Socket ID: ${chalk.dim(this.ws.getSocketId())}`);
564
- console.log(`Server: ${chalk.dim(this.ws.config.serverUrl)}`);
398
+ console.log(`Server: ${chalk.dim(this.ws.config.serverUrl)}`);
565
399
  }
566
-
567
400
  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`);
401
+ console.log(`Room: ${room ? chalk.green(room) : chalk.yellow('None')}`);
402
+ console.log(`Directory: ${chalk.blue(this.currentDir)}`);
574
403
  }
575
-
404
+
576
405
  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
-
406
+ if (!args[0]) { console.log(chalk.red('Usage: join <room-id> [password]')); return; }
407
+ if (!this.ws.isConnected()) { console.log(chalk.yellow('Connecting...')); await this.ws.connect(); }
408
+ console.log(chalk.cyan(`🔗 Joining: ${args[0]}...`));
593
409
  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
- }
410
+ const result = await this.ws.joinRoom(args[0], args[1]);
411
+ console.log(chalk.green(`✅ Joined: ${result.roomName || args[0]}`));
412
+ } catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
600
413
  }
601
-
414
+
602
415
  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
416
+ if (!this.ws.isConnected()) { console.log(chalk.yellow('Not connected')); return; }
612
417
  this.ws.socket.emit('rooms:list');
613
-
614
418
  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
- }
419
+ if (data.rooms?.length) data.rooms.forEach(r => console.log(` ${chalk.cyan(r.name || r.id)} — 👥 ${r.participantCount || 0}`));
420
+ else console.log(' No rooms available');
627
421
  });
628
422
  }
629
-
423
+
630
424
  async usersCommand() {
631
- if (!this.ws.isConnected()) {
632
- console.log(chalk.yellow('Not connected'));
633
- return;
634
- }
635
-
425
+ if (!this.ws.isConnected()) { console.log(chalk.yellow('Not connected')); return; }
636
426
  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
-
427
+ if (!room) { console.log(chalk.yellow('Not in a room')); return; }
645
428
  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
- }
429
+ const users = await this.ws.getParticipants();
430
+ users.forEach(u => console.log(` ${chalk.green('●')} ${u.username}${u.isTyping ? chalk.dim(' (typing...)') : ''}`));
431
+ } catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
657
432
  }
658
-
433
+
659
434
  async sayCommand(args) {
660
435
  const message = args.join(' ');
661
-
662
- if (!message) {
663
- console.log(chalk.red('Usage: say <message>'));
664
- return;
665
- }
666
-
436
+ if (!message) { console.log(chalk.red('Usage: say <message>')); return; }
667
437
  try {
668
438
  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
439
  console.log(chalk.green(`You: ${message}`));
679
- } catch (error) {
680
- console.log(chalk.red(`Failed to send: ${error.message}`));
681
- }
440
+ } catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
682
441
  }
683
-
442
+
684
443
  async notifyCommand(args) {
685
444
  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
-
445
+ const msg = args.slice(1).join(' ');
446
+ if (!msg) { console.log(chalk.red('Usage: notify <type> <message>')); return; }
694
447
  try {
695
- await this.ws.sendNotification({
696
- type,
697
- title: 'Shell Notification',
698
- message
699
- });
700
-
448
+ await this.ws.sendNotification({ type, title: 'Shell Notification', message: msg });
701
449
  console.log(chalk.green('✅ Notification sent'));
702
- } catch (error) {
703
- console.log(chalk.red(`Failed to send notification: ${error.message}`));
704
- }
450
+ } catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
705
451
  }
706
-
452
+
707
453
  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}`));
454
+ if (!args.length) { for (const [a, c] of this.aliases) console.log(` ${chalk.green(a.padEnd(12))} → ${chalk.dim(c)}`); return; }
455
+ if (args.length < 2) { console.log(chalk.red('Usage: alias <n> <cmd>')); return; }
456
+ this.addAlias(args[0], args.slice(1).join(' '));
457
+ console.log(chalk.green(`✅ ${args[0]} → ${args.slice(1).join(' ')}`));
733
458
  }
734
-
459
+
735
460
  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
- }
461
+ if (!args[0]) { console.log(chalk.red('Usage: unalias <n>')); return; }
462
+ this.aliases.delete(args[0]) ? console.log(chalk.green(`✅ Removed: ${args[0]}`)) : console.log(chalk.red(`Not found: ${args[0]}`));
748
463
  }
749
-
464
+
750
465
  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}`));
466
+ if (!args.length) { for (const [k, v] of this.variables) console.log(` ${chalk.green(k.padEnd(12))} = ${v}`); return; }
467
+ if (args.length < 2) { console.log(chalk.red('Usage: set <n> <value>')); return; }
468
+ this.variables.set(args[0], args.slice(1).join(' '));
469
+ console.log(chalk.green(`✅ ${args[0]} = ${args.slice(1).join(' ')}`));
776
470
  }
777
-
471
+
778
472
  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
- }
473
+ if (!args[0]) { console.log(chalk.red('Usage: unset <n>')); return; }
474
+ this.variables.delete(args[0]) ? console.log(chalk.green(`✅ Removed`)) : console.log(chalk.red(`Not found`));
791
475
  }
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);
476
+
477
+ getSessionPath() { return path.join(this.config.configDir, 'session.json'); }
478
+
479
+ parseCommand(input) {
480
+ const args = [];
481
+ let current = '', inQuotes = false, quoteChar = '', escapeNext = false;
482
+ for (const char of input) {
483
+ if (char === '\\' && !escapeNext && !inQuotes) { escapeNext = true; continue; }
484
+ if (escapeNext) { current += char; escapeNext = false; continue; }
485
+ if ((char === '"' || char === "'") && !inQuotes) { inQuotes = true; quoteChar = char; continue; }
486
+ if (char === quoteChar && inQuotes) { inQuotes = false; quoteChar = ''; continue; }
487
+ if (char === ' ' && !inQuotes) { if (current) { args.push(current); current = ''; } continue; }
488
+ current += char;
871
489
  }
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);
490
+ if (current) args.push(current);
491
+ return args;
880
492
  }
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);
493
+
494
+ executeSystemCommand(command) {
495
+ return new Promise((resolve) => {
496
+ const child = spawn(command, [], {
497
+ cwd: this.currentDir, shell: true, stdio: 'inherit',
498
+ env: { ...process.env, THINKNCOLLAB_SHELL: '1' }
499
+ });
500
+ child.on('close', () => resolve());
501
+ child.on('error', () => { console.log(chalk.red(`Command not found: ${command}`)); resolve(); });
502
+ });
890
503
  }
891
-
892
- /**
893
- * Show welcome message
894
- */
504
+
895
505
  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
-
506
+ try {
507
+ const figlet = require('figlet');
508
+ console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' })));
509
+ } catch {}
904
510
  console.log(chalk.cyan(`
905
511
  ╔══════════════════════════════════════════════════════╗
906
- ║ ThinkNCollab Shell v1.0.0 ║
512
+ ║ ThinkNCollab Shell v0.0.02 ║
907
513
  ║ 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'));
514
+ ╚══════════════════════════════════════════════════════╝`));
935
515
  }
936
-
937
- /**
938
- * Load command history
939
- */
516
+
940
517
  loadHistory() {
941
518
  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
- }
519
+ if (fs.existsSync(this.config.historyFile))
520
+ this.history = fs.readFileSync(this.config.historyFile, 'utf8').split('\n').filter(l => l.trim());
521
+ } catch {}
949
522
  }
950
-
951
- /**
952
- * Save command history
953
- */
523
+
954
524
  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
- }
525
+ try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
961
526
  }
962
-
963
- /**
964
- * Cleanup before exit
965
- */
527
+
966
528
  cleanup() {
967
- if (this.ws) {
968
- this.ws.disconnect();
969
- }
529
+ if (this.tcpServer) { try { this.tcpServer.close(); } catch {} }
530
+ if (this.ws) this.ws.disconnect();
970
531
  this.saveHistory();
971
532
  }
972
533
  }