thinkncollab-shell 0.0.5 → 0.0.7

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,6 +1,5 @@
1
1
  /**
2
- * ThinkNCollab Shell Core
3
- * Inquirer REPL + TCP IPC for notification window
2
+ * src/core/shell.js — ThinkNCollab Shell Core
4
3
  */
5
4
 
6
5
  const path = require('path');
@@ -9,12 +8,12 @@ const os = require('os');
9
8
  const chalk = require('chalk');
10
9
  const net = require('net');
11
10
  const { spawn } = require('child_process');
12
- const WebSocketManager = require('./websocket');
13
11
  const EventEmitter = require('events');
12
+ const WebSocketManager = require('./websocket');
14
13
  const CommandRegistry = require('../commands');
15
14
  const ThinkNCollabAPI = require('./api-client');
15
+ const { loadProjectConfig, hasProjectConfig } = require('./project-config');
16
16
 
17
- // Find an available port starting from 7379
18
17
  function getAvailablePort(startPort) {
19
18
  return new Promise((resolve) => {
20
19
  const server = net.createServer();
@@ -36,7 +35,7 @@ class ThinkNCollabShell extends EventEmitter {
36
35
  configDir: path.join(os.homedir(), '.thinkncollab'),
37
36
  maxHistory: 1000,
38
37
  websocket: {
39
- serverUrl: options.serverUrl || 'https://thinkncollab.com/thinknsh',
38
+ serverUrl: options.serverUrl || 'http://localhost:3001',
40
39
  autoConnect: options.autoConnect || false
41
40
  },
42
41
  ...options
@@ -50,20 +49,24 @@ class ThinkNCollabShell extends EventEmitter {
50
49
  this.history = [];
51
50
  this.running = false;
52
51
 
53
- // TCP notification clients
52
+ // TCP notification IPC
54
53
  this.notifyClients = new Set();
55
54
  this.tcpServer = null;
56
55
 
57
56
  this.api = new ThinkNCollabAPI({
58
- apiUrl: options.apiUrl || 'https://thinkncollab.com',
59
- wsUrl: options.wsUrl || 'https://thinkncollab.com'
57
+ apiUrl: options.apiUrl || 'http://localhost:3001',
58
+ wsUrl: options.wsUrl || 'http://localhost:3001'
60
59
  });
61
60
 
62
61
  this.commands = new Map();
63
62
  this.aliases = new Map();
64
63
  this.variables = new Map();
65
- this.isTyping = false;
66
- this.typingTimeout = null;
64
+
65
+ // ★ Restore persisted session on startup
66
+ // Handles both .tncproject boot AND manual login from a previous session
67
+ if (this.api.isAuthenticated()) {
68
+ this.ws.setUser(this.api.getUser(), this.api.getToken());
69
+ }
67
70
 
68
71
  this.loadHistory();
69
72
  this.registerBuiltinCommands();
@@ -75,18 +78,13 @@ class ThinkNCollabShell extends EventEmitter {
75
78
 
76
79
  async startTCPServer() {
77
80
  this.notifyPort = await getAvailablePort(7379);
78
-
79
81
  return new Promise((resolve) => {
80
82
  this.tcpServer = net.createServer((socket) => {
81
83
  this.notifyClients.add(socket);
82
84
  socket.on('close', () => this.notifyClients.delete(socket));
83
85
  socket.on('error', () => this.notifyClients.delete(socket));
84
86
  });
85
-
86
- this.tcpServer.listen(this.notifyPort, '127.0.0.1', () => {
87
- resolve();
88
- });
89
-
87
+ this.tcpServer.listen(this.notifyPort, '127.0.0.1', () => resolve());
90
88
  this.tcpServer.on('error', (err) => {
91
89
  console.log(chalk.yellow(`⚠️ Notification server: ${err.message}`));
92
90
  resolve();
@@ -94,7 +92,6 @@ class ThinkNCollabShell extends EventEmitter {
94
92
  });
95
93
  }
96
94
 
97
- // Send notification event to all connected notification windows
98
95
  pushNotification(data) {
99
96
  if (this.notifyClients.size === 0) return;
100
97
  const line = JSON.stringify(data) + '\n';
@@ -103,10 +100,9 @@ class ThinkNCollabShell extends EventEmitter {
103
100
  }
104
101
  }
105
102
 
106
- // ─── Spawn notification window (cross-platform) ───────────────────────────
103
+ // ─── Spawn notification window ────────────────────────────────────────────
107
104
 
108
105
  spawnNotificationWindow() {
109
- // Path to notification-window.js (same dir as this file)
110
106
  const notifyScript = path.join(__dirname, 'notification-window.js');
111
107
  const env = { ...process.env, THINKNSH_NOTIFY_PORT: String(this.notifyPort) };
112
108
 
@@ -114,43 +110,24 @@ class ThinkNCollabShell extends EventEmitter {
114
110
  if (process.platform === 'win32') {
115
111
  const isVSCode = process.env.TERM_PROGRAM === 'vscode';
116
112
  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:'));
113
+ console.log(chalk.yellow('\n VS Code detected — open a new terminal tab and run:'));
119
114
  console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${this.notifyPort}; node "${notifyScript}"\n`));
120
115
  } else {
121
- // Use array args so cmd handles spaces in path correctly
122
116
  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();
117
+ spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/k', `"${nodeExe}" "${notifyScript}"`],
118
+ { detached: true, stdio: 'ignore', env, shell: false }).unref();
127
119
  }
128
-
129
120
  } 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}'"`;
121
+ const script = `tell application "Terminal" to do script "THINKNSH_NOTIFY_PORT=${this.notifyPort} node '${notifyScript}'"`;
132
122
  spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
133
-
134
123
  } else {
135
- // Linux — try common terminal emulators in order
136
124
  const terminals = [
137
125
  ['gnome-terminal', ['--', 'node', notifyScript]],
138
126
  ['xterm', ['-e', `node "${notifyScript}"`]],
139
127
  ['konsole', ['--noclose', '-e', 'node', notifyScript]],
140
- ['xfce4-terminal', ['-e', `node "${notifyScript}"`]],
141
- ['x-terminal-emulator', ['-e', `node "${notifyScript}"`]],
142
128
  ];
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}`));
129
+ for (const [cmd, args] of terminals) {
130
+ try { spawn(cmd, args, { detached: true, stdio: 'ignore', env }).unref(); break; } catch {}
154
131
  }
155
132
  }
156
133
  } catch (err) {
@@ -159,109 +136,218 @@ class ThinkNCollabShell extends EventEmitter {
159
136
  }
160
137
  }
161
138
 
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
- }
139
+ // ─── WebSocket → Shell + TCP bridge ──────────────────────────────────────
140
+
141
+ setupWebSocketHandlers() {
142
+ this.ws.on('connected', (d) => {
143
+ this.api.logEvent('connected', null, { socketId: d?.socketId });
144
+ this.api.startHeartbeat();
145
+ this.pushNotification({ type: 'connected', socketId: d?.socketId });
146
+ });
147
+
148
+ this.ws.on('disconnected', (d) => {
149
+ this.api.logEvent('disconnected', null, { reason: d?.reason });
150
+ this.api.stopHeartbeat();
151
+ this.pushNotification({ type: 'disconnected', reason: d?.reason });
152
+ });
153
+
154
+ this.ws.on('reconnected', () => {
155
+ this.api.logEvent('reconnected');
156
+ this.api.startHeartbeat();
157
+ this.pushNotification({ type: 'connected', socketId: 'reconnected' });
158
+ });
159
+
160
+ this.ws.on('message', (d) => {
161
+ const myId = this.api.getUser()?._id;
162
+ if (myId && d.userId === myId) return;
163
+ this.pushNotification({ type: 'message', from: d.username, text: d.message });
164
+ });
165
+
166
+ this.ws.on('messageHistory', (messages) => {
167
+ if (!messages?.length) return;
168
+ console.log(chalk.dim('\n ─── Room History ───────────────────────────────'));
169
+ messages.forEach(msg => {
170
+ const from = msg.name || msg.username || msg.sender?.name || 'Unknown';
171
+ const text = msg.message || msg.content || msg.text || '';
172
+ const ts = msg.timestamp || msg.createdAt;
173
+ const timeStr = ts ? chalk.dim(`[${new Date(ts).toLocaleTimeString()}]`) : '';
174
+ console.log(` ${timeStr} ${chalk.cyan(from + ':')} ${chalk.white(text)}`);
175
+ });
176
+ console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
177
+ });
178
+
179
+ this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
180
+ this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
181
+
182
+ // room join/leave events
183
+ this.ws.on('joined', (roomId) => this.api.logEvent('room_join', roomId));
184
+ this.ws.on('left', (roomId) => this.api.logEvent('room_leave', roomId));
185
+
186
+ this.ws.on('notification', (d) => {
187
+ this.pushNotification({
188
+ type: 'notification',
189
+ level: d.type,
190
+ title: d.title,
191
+ message: d.message,
192
+ taskTitle: d.taskTitle,
193
+ assignedBy: d.assignedBy,
194
+ });
195
+ });
196
+
197
+ this.ws.on('error', (e) => {
198
+ console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
199
+ this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
200
+ });
201
+ }
175
202
 
176
203
  // ─── Start ────────────────────────────────────────────────────────────────
177
204
 
178
205
  async start() {
179
206
  this.showWelcome();
180
207
 
181
- // 1. Start TCP server for notification IPC
182
208
  await this.startTCPServer();
183
209
  console.log(chalk.dim(` Notification server ready on port ${this.notifyPort}`));
184
210
 
185
- // 2. Spawn notification window
186
211
  this.spawnNotificationWindow();
187
212
  console.log(chalk.dim(' Notification window opening...\n'));
188
213
 
189
214
  this.running = true;
215
+ process.on('SIGINT', () => console.log(chalk.yellow('\nUse "exit" to quit')));
190
216
 
191
- process.on('SIGINT', () => {
192
- console.log(chalk.yellow('\nUse "exit" to quit'));
193
- });
217
+ // Try .tncproject auto-boot first
218
+ const booted = await this._bootFromProjectConfig();
194
219
 
195
- if (this.config.websocket.autoConnect) {
196
- await this.ws.connect();
220
+ // If no .tncproject but autoConnect is set, connect WS anyway
221
+ if (!booted && this.config.websocket.autoConnect) {
222
+ await this.ws.connect().catch(() => {});
197
223
  }
198
224
 
199
225
  const { input } = require('@inquirer/prompts');
200
226
 
201
227
  while (this.running) {
202
228
  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
-
229
+ const line = await input({ message: this.getPromptText(), required: false }).catch(() => null);
230
+ if (line === null) { console.log(chalk.yellow('Use "exit" to quit')); continue; }
213
231
  const trimmed = line.trim();
214
232
  if (!trimmed) continue;
215
-
216
233
  this.history.push(trimmed);
217
234
  this.saveHistory();
218
235
  await this.execute(trimmed);
219
-
220
236
  } catch (err) {
221
- if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
237
+ if (err.name === 'ExitPromptError' || err.message?.includes('force closed'))
222
238
  console.log(chalk.yellow('Use "exit" to quit'));
223
- } else {
239
+ else
224
240
  console.log(chalk.red(`Shell error: ${err.message}`));
225
- }
226
241
  }
227
242
  }
228
243
  }
229
244
 
230
- restoreReadline() {}
245
+ // ─── Boot from .tncproject ────────────────────────────────────────────────
231
246
 
232
- // ─── Execute ──────────────────────────────────────────────────────────────
247
+ async _bootFromProjectConfig() {
248
+ if (!hasProjectConfig()) return false;
233
249
 
234
- async execute(input) {
235
- if (!input.trim()) return;
250
+ let cfg;
251
+ try {
252
+ cfg = loadProjectConfig();
253
+ } catch (err) {
254
+ console.log(chalk.yellow(`⚠️ tncproject: ${err.message}\n`));
255
+ return false;
256
+ }
236
257
 
237
- const args = this.parseCommand(input);
238
- const cmdName = args[0].toLowerCase();
239
- const cmdArgs = args.slice(1);
258
+ console.log(chalk.dim(` 📄 tncproject found`));
259
+ console.log(chalk.dim(` Room : ${cfg.roomId}`));
260
+ console.log(chalk.dim(` File : ${cfg.filePath}\n`));
240
261
 
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}`));
248
- }
249
- return;
262
+ // Send raw encrypted file to server — server decrypts, returns shellToken
263
+ let result;
264
+ try {
265
+ result = await this.api._request('POST', '/config/verify-config', {
266
+ fileBase64: cfg.fileBase64,
267
+ roomId: cfg.roomId,
268
+ });
269
+ } catch (err) {
270
+ console.log(chalk.red(`❌ tncproject auth failed: ${err.message}`));
271
+ console.log(chalk.dim(' Falling back to manual login.\n'));
272
+ return false;
250
273
  }
251
274
 
252
- if (this.commandRegistry) {
253
- const handled = await this.commandRegistry.execute(input, this);
254
- if (handled) return;
275
+ // Save session exactly like manual login
276
+ this.api._saveSession({
277
+ token: result.shellToken,
278
+ user: {
279
+ _id: result._id.toString(),
280
+ userId: result._id.toString(),
281
+ email: result.email,
282
+ name: result.name,
283
+ userType: 'User',
284
+ },
285
+ timestamp: new Date().toISOString(),
286
+ });
287
+
288
+ this.ws.setUser(this.api.getUser(), result.shellToken);
289
+
290
+ // Connect WebSocket
291
+ try {
292
+ await this.ws.connect(result.shellToken);
293
+ } catch (err) {
294
+ console.log(chalk.yellow(`⚠️ WebSocket: ${err.message}`));
295
+ console.log(chalk.green(`✅ ${result.name} logged in (offline mode)\n`));
296
+ return true;
255
297
  }
256
298
 
257
- await this.executeSystemCommand(input);
299
+ // Auto-join the project room
300
+ try {
301
+ await this.ws.joinRoom(result.roomId.toString());
302
+ console.log(chalk.green(`✅ ${result.name} → room "${result.roomName || result.roomId}"\n`));
303
+ } catch (err) {
304
+ console.log(chalk.yellow(`⚠️ Could not auto-join room: ${err.message}`));
305
+ console.log(chalk.dim(' Use "join <room-id>" manually.\n'));
306
+ }
307
+
308
+ return true;
258
309
  }
259
310
 
311
+ // ─── Execute ──────────────────────────────────────────────────────────────
312
+ // shell.js — execute() mein ye change karo
313
+ async execute(input) {
314
+ if (!input.trim()) return;
315
+
316
+ const args = this.parseCommand(input);
317
+ const cmdName = args[0].toLowerCase();
318
+ const cmdArgs = args.slice(1);
319
+
320
+ if (this.commands.has(cmdName)) {
321
+ try { await this.commands.get(cmdName).handler(cmdArgs, this); }
322
+ catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
323
+ process.stdout.write('\r\n');
324
+ return;
325
+ }
326
+
327
+ if (this.commandRegistry) {
328
+ const handled = await this.commandRegistry.execute(input, this);
329
+ if (handled) {
330
+ process.stdout.write('\r\n');
331
+ return;
332
+ }
333
+ }
334
+
335
+ // ★ Windows pe known-useless OS commands block karo
336
+ const blockedOnWindows = [ 'open', 'which', 'grep', 'touch', 'cat'];
337
+ if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
338
+ console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
339
+ console.log(chalk.dim(` Type 'help' to see available commands.`));
340
+ process.stdout.write('\r\n');
341
+ return;
342
+ }
343
+
344
+ await this.executeSystemCommand(input);
345
+ }
260
346
  // ─── Prompt ───────────────────────────────────────────────────────────────
261
347
 
262
348
  getPromptText() {
263
349
  let p = '';
264
- const room = this.ws.getCurrentRoom?.();
350
+ const room = this.ws.getCurrentRoom?.();
265
351
  if (room) p += `[${room}] `;
266
352
  if (this.ws.isConnected?.()) p += '● ';
267
353
 
@@ -277,43 +363,38 @@ class ThinkNCollabShell extends EventEmitter {
277
363
  displayPath = displayPath.replace(homeDir, '~');
278
364
  }
279
365
 
280
- p += `${this.username}@thinknsh:${displayPath} $`;
281
- return p;
366
+ return `${this.username}@thinknsh:${displayPath} $`;
282
367
  }
283
368
 
284
- getPrompt() { return this.getPromptText(); }
285
-
369
+ getPrompt() { return this.getPromptText(); }
286
370
  printMessage(msg) { console.log(msg); }
287
371
 
288
372
  // ─── Built-ins ────────────────────────────────────────────────────────────
289
373
 
290
374
  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');
375
+ const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
376
+
377
+ r('help', this.helpCommand.bind(this), 'Show available commands');
378
+ r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
379
+ r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
380
+ r('clear', () => console.clear(), 'Clear screen');
381
+ r('history', this.historyCommand.bind(this), 'Show command history');
382
+ r('cd', this.cdCommand.bind(this), 'Change directory');
383
+ r('pwd', () => console.log(process.cwd()), 'Print working directory');
384
+ r('ls', this.lsCommand.bind(this), 'List files');
385
+ r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
386
+ r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
387
+ r('status', this.statusCommand.bind(this), 'Show connection status');
388
+ r('join', this.joinCommand.bind(this), 'Join a room');
389
+ r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
390
+ r('rooms', this.roomsCommand.bind(this), 'List rooms');
391
+ r('say', this.sayCommand.bind(this), 'Send a message');
392
+ r('msg', this.sayCommand.bind(this), 'Send a message');
393
+ r('set', this.setCommand.bind(this), 'Set a variable');
394
+ r('unset', this.unsetCommand.bind(this), 'Unset a variable');
313
395
 
314
396
  this.addAlias('ll', 'ls -la');
315
397
  this.addAlias('..', 'cd ..');
316
- this.addAlias('...', 'cd ../..');
317
398
  this.addAlias('~', 'cd ~');
318
399
  }
319
400
 
@@ -321,26 +402,8 @@ class ThinkNCollabShell extends EventEmitter {
321
402
  addAlias(alias, command) { this.aliases.set(alias, command); }
322
403
 
323
404
  async helpCommand() {
324
- const categories = {
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'],
330
- 'Notifications': ['notify'],
331
- 'Aliases': ['alias', 'unalias'],
332
- 'Variables': ['set', 'unset']
333
- };
334
- console.log(chalk.cyan('\n📚 Available Commands:'));
335
- console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
336
- for (const [category, cmds] of Object.entries(categories)) {
337
- console.log(chalk.yellow(`\n${category}:`));
338
- cmds.forEach(cmd => {
339
- const c = this.commands.get(cmd);
340
- if (c) console.log(` ${chalk.green(cmd.padEnd(12))} - ${c.description}`);
341
- });
342
- }
343
- console.log(chalk.dim('\nAny other command runs in your system shell\n'));
405
+ if (this.commandRegistry) console.log(this.commandRegistry.getHelpText());
406
+ console.log(chalk.dim(' Any unrecognised command runs in your system shell\n'));
344
407
  }
345
408
 
346
409
  historyCommand() {
@@ -351,11 +414,8 @@ class ThinkNCollabShell extends EventEmitter {
351
414
  const target = args.join(' ').trim() || os.homedir();
352
415
  try {
353
416
  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));
417
+ if (target === '~') newPath = os.homedir();
356
418
  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
419
  else newPath = path.resolve(this.currentDir, target);
360
420
  newPath = path.normalize(newPath);
361
421
  const stats = await fs.stat(newPath);
@@ -369,22 +429,18 @@ class ThinkNCollabShell extends EventEmitter {
369
429
  async lsCommand(args) {
370
430
  try {
371
431
  const files = await fs.readdir(this.currentDir);
372
- const showAll = args.includes('-a') || args.includes('--all');
432
+ const showAll = args.includes('-a');
373
433
  const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
374
434
  const fileList = await Promise.all(filtered.map(async (file) => {
375
435
  const stat = await fs.stat(path.join(this.currentDir, file));
376
- return { name: file, isDir: stat.isDirectory(), mode: stat.mode };
436
+ return { name: file, isDir: stat.isDirectory() };
377
437
  }));
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);
382
- });
438
+ fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
383
439
  } catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
384
440
  }
385
441
 
386
- async connectCommand(args) {
387
- console.log(chalk.cyan(`🔌 Connecting...`));
442
+ async connectCommand() {
443
+ console.log(chalk.cyan('🔌 Connecting...'));
388
444
  try { await this.ws.connect(); }
389
445
  catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
390
446
  }
@@ -392,23 +448,22 @@ class ThinkNCollabShell extends EventEmitter {
392
448
  async statusCommand() {
393
449
  const connected = this.ws.isConnected();
394
450
  console.log(chalk.cyan('\n📊 Status:'));
395
- console.log(`WebSocket: ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
396
- if (connected) {
397
- console.log(`Socket ID: ${chalk.dim(this.ws.getSocketId())}`);
398
- console.log(`Server: ${chalk.dim(this.ws.config.serverUrl)}`);
399
- }
451
+ console.log(` WebSocket : ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
452
+ if (connected) console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);
400
453
  const room = this.ws.getCurrentRoom();
401
- console.log(`Room: ${room ? chalk.green(room) : chalk.yellow('None')}`);
402
- console.log(`Directory: ${chalk.blue(this.currentDir)}`);
454
+ console.log(` Room : ${room ? chalk.green(room) : chalk.yellow('None')}`);
455
+ console.log(` Auth : ${this.api.isAuthenticated() ? chalk.green('✓ Logged in') : chalk.yellow('Not logged in')}`);
456
+ const user = this.api.getUser();
457
+ if (user) console.log(` User : ${chalk.dim(user.name || user.email)}`);
458
+ console.log('');
403
459
  }
404
460
 
405
461
  async joinCommand(args) {
406
- if (!args[0]) { console.log(chalk.red('Usage: join <room-id> [password]')); return; }
462
+ if (!args[0]) { console.log(chalk.red('Usage: join <room-id>')); return; }
407
463
  if (!this.ws.isConnected()) { console.log(chalk.yellow('Connecting...')); await this.ws.connect(); }
408
- console.log(chalk.cyan(`🔗 Joining: ${args[0]}...`));
409
464
  try {
410
- const result = await this.ws.joinRoom(args[0], args[1]);
411
- console.log(chalk.green(`Joined: ${result.roomName || args[0]}`));
465
+ const result = await this.ws.joinRoom(args[0]);
466
+ console.log(chalk.green(`✅ Joined: ${result.roomName || args[0]}`));
412
467
  } catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
413
468
  }
414
469
 
@@ -417,61 +472,30 @@ class ThinkNCollabShell extends EventEmitter {
417
472
  this.ws.socket.emit('rooms:list');
418
473
  this.ws.socket.once('rooms:list', (data) => {
419
474
  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');
475
+ else console.log(' No rooms');
421
476
  });
422
477
  }
423
478
 
424
- async usersCommand() {
425
- if (!this.ws.isConnected()) { console.log(chalk.yellow('Not connected')); return; }
426
- const room = this.ws.getCurrentRoom();
427
- if (!room) { console.log(chalk.yellow('Not in a room')); return; }
428
- try {
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}`)); }
432
- }
433
-
434
479
  async sayCommand(args) {
435
480
  const message = args.join(' ');
436
481
  if (!message) { console.log(chalk.red('Usage: say <message>')); return; }
437
482
  try {
438
483
  await this.ws.sendMessage(message);
484
+ const displayName = this.api.getUser()?.name || this.username;
439
485
  console.log(chalk.green(`You: ${message}`));
486
+ this.pushNotification({ type: 'message', from: `You (${displayName})`, text: message });
440
487
  } catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
441
488
  }
442
489
 
443
- async notifyCommand(args) {
444
- const type = args[0] || 'info';
445
- const msg = args.slice(1).join(' ');
446
- if (!msg) { console.log(chalk.red('Usage: notify <type> <message>')); return; }
447
- try {
448
- await this.ws.sendNotification({ type, title: 'Shell Notification', message: msg });
449
- console.log(chalk.green('Notification sent'));
450
- } catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
451
- }
452
-
453
- async aliasCommand(args) {
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(' ')}`));
458
- }
459
-
460
- async unaliasCommand(args) {
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]}`));
463
- }
464
-
465
490
  async setCommand(args) {
466
491
  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
492
  this.variables.set(args[0], args.slice(1).join(' '));
469
493
  console.log(chalk.green(`${args[0]} = ${args.slice(1).join(' ')}`));
470
494
  }
471
495
 
472
496
  async unsetCommand(args) {
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`));
497
+ if (!args[0]) return;
498
+ this.variables.delete(args[0]);
475
499
  }
476
500
 
477
501
  getSessionPath() { return path.join(this.config.configDir, 'session.json'); }
@@ -494,22 +518,28 @@ class ThinkNCollabShell extends EventEmitter {
494
518
  executeSystemCommand(command) {
495
519
  return new Promise((resolve) => {
496
520
  const child = spawn(command, [], {
497
- cwd: this.currentDir, shell: true, stdio: 'inherit',
498
- env: { ...process.env, THINKNCOLLAB_SHELL: '1' }
521
+ cwd: this.currentDir,
522
+ shell: true,
523
+ stdio: 'inherit'
524
+ });
525
+ child.on('close', (code) => {
526
+ // Update currentDir in case command changed it (cd won't but others might)
527
+ try { this.currentDir = process.cwd(); } catch {}
528
+ process.stdout.write('\r\n');
529
+ resolve();
530
+ });
531
+ child.on('error', () => {
532
+ console.log(chalk.red(`Command not found: ${command}`));
533
+ resolve();
499
534
  });
500
- child.on('close', () => resolve());
501
- child.on('error', () => { console.log(chalk.red(`Command not found: ${command}`)); resolve(); });
502
535
  });
503
536
  }
504
537
 
505
538
  showWelcome() {
506
- try {
507
- const figlet = require('figlet');
508
- console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' })));
509
- } catch {}
539
+ try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
510
540
  console.log(chalk.cyan(`
511
541
  ╔══════════════════════════════════════════════════════╗
512
- ║ ThinkNCollab Shell v0.0.05 ║
542
+ ║ ThinkNCollab Shell v0.0.6 ║
513
543
  ║ Type 'help' for commands ║
514
544
  ╚══════════════════════════════════════════════════════╝`));
515
545
  }
@@ -525,11 +555,12 @@ class ThinkNCollabShell extends EventEmitter {
525
555
  try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
526
556
  }
527
557
 
528
- cleanup() {
529
- if (this.tcpServer) { try { this.tcpServer.close(); } catch {} }
530
- if (this.ws) this.ws.disconnect();
531
- this.saveHistory();
532
- }
558
+ cleanup() {
559
+ this.api.logout(); // session end + heartbeat stop + lastSeen update
560
+ if (this.tcpServer) { try { this.tcpServer.close(); } catch {} }
561
+ if (this.ws) this.ws.disconnect();
562
+ this.saveHistory();
563
+ }
533
564
  }
534
565
 
535
566
  module.exports = ThinkNCollabShell;