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/bin/thinknsh.js CHANGED
@@ -46,9 +46,9 @@ if (args.includes('--version') || args.includes('-v')) {
46
46
  }
47
47
 
48
48
  const shell = new ThinkNCollabShell({
49
- serverUrl: process.env.THINKNCOLLAB_SERVER || 'https://thinkncollab.com', // ← /thinknsh hata
50
- apiUrl: process.env.THINKNCOLLAB_API_URL || 'https://thinkncollab.com',
51
- wsUrl: process.env.THINKNCOLLAB_WS_URL || 'https://thinkncollab.com',
49
+ serverUrl: process.env.THINKNCOLLAB_SERVER || 'http://localhost:3001', // ← /thinknsh hata
50
+ apiUrl: process.env.THINKNCOLLAB_API_URL || 'http://localhost:3001',
51
+ wsUrl: process.env.THINKNCOLLAB_WS_URL || 'http://localhost:3001',
52
52
  autoConnect: process.env.THINKNCOLLAB_AUTO_CONNECT === 'true'
53
53
  });
54
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thinkncollab-shell",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "ThinkNCollab Custom Shell with WebSocket collaboration",
5
5
  "main": "src/core/shell.js",
6
6
  "bin": {
@@ -1,74 +1,74 @@
1
- const chalk = require('chalk');
2
- const fs = require('fs-extra');
3
- const path = require('path');
1
+ /**
2
+ * commands/auth/login.js
3
+ */
4
+
5
+ const chalk = require('chalk');
6
+ const inquirer = require('inquirer');
4
7
 
5
8
  module.exports = {
6
- name: 'login',
7
- description: 'Login to ThinkNCollab',
8
- aliases: ['signin'],
9
+ name: 'login',
10
+ description: 'Login to ThinkNCollab',
11
+ aliases: ['signin'],
9
12
  requiresAuth: false,
10
13
 
11
14
  async execute(args, shell) {
12
- let email, password;
13
-
14
- try {
15
- if (args.length === 0) {
16
- const { input, password: pwdFn } = require('@inquirer/prompts');
17
- email = await input({ message: 'Email:' });
18
- password = await pwdFn({ message: 'Password:', mask: '*' });
19
-
20
- } else if (args.length >= 2) {
21
- email = args[0];
22
- password = args[1];
23
- } else {
24
- console.log(chalk.red('❌ Usage: login <email> <password>'));
25
- console.log(chalk.dim(' or: login (interactive)'));
26
- return;
27
- }
28
-
29
- if (!email || !password) {
30
- console.log(chalk.red('❌ Email and password required'));
31
- return;
32
- }
15
+ // Already logged in?
16
+ if (shell.api.isAuthenticated()) {
17
+ const u = shell.api.getUser();
18
+ console.log(chalk.green(`✅ Already logged in as ${u.name} (${u.email})`));
19
+ console.log(chalk.dim(' Run "logout" first to switch accounts.'));
20
+ return;
21
+ }
33
22
 
34
- console.log(chalk.cyan('🔐 Logging in...'));
23
+ // Collect credentials
24
+ let email = args[0];
25
+ let password = args[1];
26
+
27
+ if (!email || !password) {
28
+ const answers = await inquirer.prompt([
29
+ !email && { type: 'input', name: 'email', message: 'Email:', validate: v => v.includes('@') || 'Enter a valid email' },
30
+ !password && { type: 'password', name: 'password', message: 'Password:', mask: '*', validate: v => v.length >= 1 || 'Password required' },
31
+ ].filter(Boolean));
32
+ email = email || answers.email;
33
+ password = password || answers.password;
34
+ }
35
35
 
36
- if (!shell.api) { console.log(chalk.red('❌ API not initialized')); return; }
36
+ console.log(chalk.blue('🔐 Logging in...'));
37
37
 
38
+ try {
38
39
  const result = await shell.api.login(email, password);
39
40
 
40
- await fs.writeJson(path.join(shell.config.configDir, 'session.json'), {
41
- user: result.user,
42
- token: result.token,
43
- timestamp: new Date().toISOString()
44
- });
45
-
46
- console.log(chalk.green(`✅ Welcome, ${result.user.name || result.user.email}!`));
47
-
48
- // Auto-connect to WebSocket + join personal notification room
49
- try {
50
- const token = result.token;
51
- const userId = result.user._id ;
52
- const name = result.user.name || result.user.email;
53
-
54
- // Set user in websocket manager
55
- shell.ws._token = token;
56
- shell.ws.setUser({ userId, name, userType: 'User' }, token);
57
-
58
- // Connect to backend
59
- await shell.ws.connect(token);
60
-
61
- // Join personal notification room (for task assignments etc.)
62
- shell.ws.joinUserRoom(userId);
63
-
64
- console.log(chalk.green('🔌 Connected to ThinkNCollab server'));
65
- } catch (wsErr) {
66
- console.log(chalk.yellow(`⚠️ Could not connect to server: ${wsErr.message}`));
41
+ // result.user is already normalised by api-client.getUser()
42
+ // shape: { _id, userId, email, name, userType }
43
+ const user = result.user;
44
+
45
+ // ★ Tell the WebSocket manager who the user is + pass the token
46
+ // This must happen BEFORE any join attempt
47
+ shell.ws.setUser(user, result.token);
48
+
49
+ // Connect WebSocket if not already connected
50
+ if (!shell.ws.isConnected()) {
51
+ try {
52
+ await shell.ws.connect(result.token);
53
+ console.log(chalk.dim(' 🔌 Connected to ThinkNCollab server'));
54
+ } catch (wsErr) {
55
+ // Non-fatal — user can still use REST commands
56
+ console.log(chalk.yellow(` ⚠️ WebSocket unavailable: ${wsErr.message}`));
57
+ }
58
+ } else {
59
+ // Already connected — re-announce with new user identity
60
+ shell.ws._announceUser?.();
67
61
  }
68
62
 
63
+ console.log(chalk.green(`✅ Welcome, ${user.name}!`));
64
+ console.log(chalk.dim(` Logged in as ${user.email}`));
65
+
69
66
  } catch (err) {
70
- if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
71
- console.log(chalk.yellow('⚠️ Cancelled'));
67
+ if (err.message.includes('401') || err.message.toLowerCase().includes('invalid') ||
68
+ err.message.toLowerCase().includes('unauthorized')) {
69
+ console.log(chalk.red('❌ Invalid email or password'));
70
+ } else if (err.message.includes('connect') || err.message.includes('ECONNREFUSED')) {
71
+ console.log(chalk.red(`❌ Cannot reach server — is it running?`));
72
72
  } else {
73
73
  console.log(chalk.red(`❌ Login failed: ${err.message}`));
74
74
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * src/commands/auth/register.js
3
+ * Register new account — opens browser with pre-auth session
4
+ */
5
+
6
+ const chalk = require('chalk');
7
+ const { exec } = require('child_process');
8
+
9
+ module.exports = {
10
+ name: 'register',
11
+ description: 'Create a new ThinkNCollab account',
12
+ aliases: ['signup', 'reg'],
13
+ requiresAuth: false,
14
+
15
+ async execute(args, shell) {
16
+ try {
17
+ console.log(chalk.cyan('🌐 Opening registration in browser...'));
18
+
19
+ const baseUrl = shell.api.apiUrl || 'http://localhost:3001';
20
+
21
+ // Generate a CLI source token so backend knows it came from terminal
22
+ const crypto = require('crypto');
23
+ const cliRef = crypto.randomBytes(8).toString('hex');
24
+
25
+ const url = `${baseUrl}/register?ref=cli&cliRef=${cliRef}`;
26
+
27
+ // Open browser cross-platform
28
+ const openCmd = process.platform === 'win32'
29
+ ? `start "" "${url}"`
30
+ : process.platform === 'darwin'
31
+ ? `open "${url}"`
32
+ : `xdg-open "${url}"`;
33
+
34
+ exec(openCmd, (err) => {
35
+ if (err) {
36
+ console.log(chalk.yellow('⚠️ Could not open browser automatically.'));
37
+ console.log(chalk.dim(' Open this URL manually:'));
38
+ console.log(chalk.cyan(` ${url}`));
39
+ }
40
+ });
41
+
42
+ console.log(chalk.green(`✅ Registration page opened!`));
43
+ console.log(chalk.dim(` URL: ${url}`));
44
+ console.log('');
45
+ console.log(chalk.dim(' After registering, come back and run:'));
46
+ console.log(chalk.cyan(' login'));
47
+
48
+ } catch (err) {
49
+ console.log(chalk.red(`❌ Error: ${err.message}`));
50
+ }
51
+ }
52
+ };
@@ -1,47 +1,52 @@
1
- /**
2
- * Whoami Command
3
- *
4
- * Usage: whoami
5
- */
6
-
7
1
  const chalk = require('chalk');
8
2
 
9
3
  module.exports = {
10
4
  name: 'whoami',
11
5
  description: 'Show current user information',
12
6
  aliases: ['user', 'me'],
13
- requiresAuth: true,
14
-
15
- async execute(args, shell) {
16
- const user = shell.api.getUser();
17
- const room = shell.api.getCurrentRoom();
18
-
19
- console.log(chalk.cyan('\n👤 User Information:'));
20
- console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
21
- console.log(` ${chalk.yellow('Email:')} ${user.email}`);
22
- console.log(` ${chalk.yellow('Name:')} ${user.name || 'Not set'}`);
23
- console.log(` ${chalk.yellow('User ID:')} ${user.id || 'N/A'}`);
24
-
25
- if (user.team) {
26
- console.log(` ${chalk.yellow('Team:')} ${user.team}`);
27
- }
28
-
29
- if (user.role) {
30
- console.log(` ${chalk.yellow('Role:')} ${user.role}`);
31
- }
32
-
33
- console.log(` ${chalk.yellow('Status:')} ${shell.api.isConnected() ? chalk.green('Online') : chalk.gray('Offline')}`);
34
-
35
- if (room) {
36
- console.log(` ${chalk.yellow('Room:')} ${chalk.cyan(room.name)} (${room.id})`);
37
- }
38
-
39
- // Show last login if available
40
- if (user.lastLogin) {
41
- const lastLogin = new Date(user.lastLogin).toLocaleString();
42
- console.log(` ${chalk.yellow('Last Login:')} ${lastLogin}`);
43
- }
44
-
45
- console.log();
7
+ requiresAuth: false,
8
+
9
+ async execute(args, shell) {
10
+ const user = shell.api.getUser();
11
+ const room = shell.ws.getCurrentRoom?.();
12
+ const session = shell.api.session;
13
+
14
+ // ✅ Server se last session history fetch karo
15
+ let lastSession = null;
16
+ try {
17
+ const history = await shell.api._request('GET', '/thinknsh/session/history');
18
+ // Current session chhod ke last wali lo
19
+ const prev = history.find(s => s._id !== session?.shellSessionId);
20
+ if (prev) lastSession = prev;
21
+ } catch {}
22
+
23
+ console.log(chalk.cyan('\n👤 User Information:'));
24
+ console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
25
+ console.log(` ${chalk.yellow('Name:')} ${user.name || 'Not set'}`);
26
+ console.log(` ${chalk.yellow('Email:')} ${user.email}`);
27
+ console.log(` ${chalk.yellow('User ID:')} ${user._id || 'N/A'}`);
28
+ console.log(` ${chalk.yellow('Type:')} ${user.userType || 'User'}`);
29
+ console.log(` ${chalk.yellow('Status:')} ${shell.ws.isConnected?.() ? chalk.green('● Online') : chalk.gray('○ Offline')}`);
30
+
31
+ if (room) {
32
+ console.log(` ${chalk.yellow('Room:')} ${chalk.cyan(room)}`);
33
+ }
34
+
35
+ if (session?.timestamp) {
36
+ console.log(` ${chalk.yellow('Login At:')} ${chalk.dim(new Date(session.timestamp).toLocaleString())}`);
37
+ }
38
+
39
+ // ✅ Server se actual logout time
40
+ if (lastSession?.logoutAt) {
41
+ console.log(` ${chalk.yellow('Last Logout:')} ${chalk.dim(new Date(lastSession.logoutAt).toLocaleString())}`);
42
+ } else if (lastSession?.lastSeenAt) {
43
+ console.log(` ${chalk.yellow('Last Seen:')} ${chalk.dim(new Date(lastSession.lastSeenAt).toLocaleString())} ${chalk.red('(crash?)')}`);
46
44
  }
45
+
46
+ if (lastSession?.machineId) {
47
+ console.log(` ${chalk.yellow('Last Machine:')} ${chalk.dim(lastSession.machineId)}`);
48
+ }
49
+
50
+ console.log();
51
+ }
47
52
  };
@@ -1,55 +1,85 @@
1
1
  /**
2
- * Command Registry - Loads and manages all commands
2
+ * src/commands/index.js — Command Registry
3
3
  */
4
-
5
4
  const chalk = require('chalk');
6
5
 
7
6
  class CommandRegistry {
8
7
  constructor(shell) {
9
- this.shell = shell;
10
- this.commands = new Map();
11
- this.aliases = new Map();
8
+ this.shell = shell;
9
+ this.commands = new Map();
10
+ this.aliases = new Map();
12
11
  this.categories = new Map();
13
-
14
12
  this.loadCommands();
15
13
  }
16
14
 
17
15
  loadCommands() {
18
- try {
19
- this.register(require('./auth/login'), 'auth');
20
- this.register(require('./auth/logout'), 'auth');
21
- this.register(require('./auth/whoami'), 'auth');
22
- this.register(require('./team/myteam'), 'team');
23
- this.register(require('./team/teammates'), 'team');
24
- this.register(require('./team/invite'), 'team');
25
- this.register(require('./room/join'), 'room');
26
- this.register(require('./room/leave'), 'room');
27
- this.register(require('./room/rooms'), 'room');
28
- this.register(require('./message/say'), 'message');
29
- this.register(require('./shell/status'), 'shell');
30
- } catch (error) {
31
- console.error(chalk.red(`❌ Failed to load command: ${error.message}`));
32
- console.error(error.stack);
16
+ const load = (path, category) => {
17
+ try {
18
+ const cmd = require(path);
19
+ if (!cmd || !cmd.name) {
20
+ console.error(chalk.red(`❌ BAD COMMAND (no name): ${path}`));
21
+ return;
33
22
  }
23
+ this.register(cmd, category);
34
24
  }
25
+ catch (e) { console.error(chalk.red(`❌ Failed to load ${path}: ${e.message}`)); }
26
+ };
27
+
28
+ // Auth
29
+ load('./auth/login', 'auth');
30
+ load('./auth/logout', 'auth');
31
+ load('./auth/whoami', 'auth');
32
+ load('./auth/register', 'auth');
33
+
34
+ // Team
35
+ load('./team/myteam', 'team');
36
+ load('./team/teammates', 'team');
37
+ load('./team/invite', 'team');
38
+
39
+ // Room
40
+ load('./room/join', 'room');
41
+ load('./room/leave', 'room');
42
+ load('./room/rooms', 'room');
43
+
44
+ // Message
45
+ load('./message/say', 'message');
46
+
47
+ // Shell
48
+ load('./shell/status', 'shell');
49
+ load('./shell/notify', 'shell');
50
+
51
+ // Tasks
52
+ load('./tasks/tasks', 'tasks');
53
+ load('./tasks/task', 'tasks');
54
+ load('./tasks/task-status', 'tasks');
55
+ // load('./tasks/start', 'tasks');
56
+ load('./tasks/complete', 'tasks');
57
+ // load('./tasks/comment', 'tasks');
58
+ load('./tasks/create-task', 'tasks');
59
+
60
+
61
+ // Terminal
62
+ load('./terminal/share', 'terminal');
63
+
64
+
35
65
 
36
- register(command, category) {
37
- this.commands.set(command.name, command);
38
66
 
39
- if (!this.categories.has(category)) {
40
- this.categories.set(category, []);
41
- }
42
- this.categories.get(category).push(command.name);
43
67
 
44
- if (command.aliases) {
45
- command.aliases.forEach(alias => this.aliases.set(alias, command.name));
46
- }
47
68
  }
48
69
 
49
- addAlias(alias, command) {
50
- this.aliases.set(alias, command);
70
+ register(command, category) {
71
+ if (!command || !command.name) {
72
+ console.error(chalk.yellow(`⚠️ Skipping invalid command in category: ${category}`));
73
+ return;
74
+ }
75
+ this.commands.set(command.name, command);
76
+ if (!this.categories.has(category)) this.categories.set(category, []);
77
+ this.categories.get(category).push(command.name);
78
+ if (command.aliases) command.aliases.forEach(a => this.aliases.set(a, command.name));
51
79
  }
52
80
 
81
+ addAlias(alias, command) { this.aliases.set(alias, command); }
82
+
53
83
  getCommand(name) {
54
84
  if (this.aliases.has(name)) name = this.aliases.get(name);
55
85
  return this.commands.get(name);
@@ -65,17 +95,14 @@ class CommandRegistry {
65
95
 
66
96
  try {
67
97
  if (command.requiresAuth && !shell.api?.isAuthenticated()) {
68
- console.log(chalk.red('❌ Please login first: login <email> <password>'));
98
+ console.log(chalk.red('❌ Please login first: login'));
69
99
  return true;
70
100
  }
71
-
72
- if (command.requiresRoom && !shell.api?.getCurrentRoom()) {
101
+ if (command.requiresRoom && !shell.ws?.getCurrentRoom()) {
73
102
  console.log(chalk.red('❌ Please join a room first: join <room-id>'));
74
103
  return true;
75
104
  }
76
-
77
105
  await command.execute(cmdArgs, shell);
78
-
79
106
  } catch (error) {
80
107
  console.log(chalk.red(`❌ Command error: ${error.message}`));
81
108
  }
@@ -86,17 +113,16 @@ class CommandRegistry {
86
113
  getHelpText() {
87
114
  let help = chalk.cyan('\n📚 Available Commands:\n');
88
115
  help += chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n');
89
-
90
116
  for (const [category, commands] of this.categories) {
91
117
  help += chalk.yellow(`${category.toUpperCase()}:\n`);
92
118
  commands.forEach(cmdName => {
93
- const cmd = this.commands.get(cmdName);
94
- const aliases = cmd.aliases ? chalk.dim(` (${cmd.aliases.join(', ')})`) : '';
95
- help += ` ${chalk.green(cmdName.padEnd(12))}${aliases} - ${cmd.description}\n`;
96
- });
119
+ const cmd = this.commands.get(cmdName);
120
+ if (!cmd || !cmdName) return; // ← ye line add karo
121
+ const aliases = cmd.aliases ? chalk.dim(` (${cmd.aliases.join(', ')})`) : '';
122
+ help += ` ${chalk.green(cmdName.padEnd(14))}${aliases} - ${cmd.description || ''}\n`;
123
+ });
97
124
  help += '\n';
98
125
  }
99
-
100
126
  return help;
101
127
  }
102
128
  }
@@ -1,68 +1,142 @@
1
1
  /**
2
2
  * Join Command
3
- *
4
- * Usage: join <room-id>
5
- * join <room-name>
3
+ *
4
+ * Usage: join <room-id> [password]
5
+ *
6
+ * Flow:
7
+ * 1. REST call → shell.api.joinRoom() — validates membership + fetches room info
8
+ * 2. WebSocket → thinknsh:join — registers socket in the room on the server
9
+ * 3. Wait for → thinknsh:joined — server confirms + sends message history
6
10
  */
7
11
 
8
12
  const chalk = require('chalk');
9
13
 
14
+ // How long to wait for thinknsh:joined after emitting thinknsh:join (ms)
15
+ const JOIN_TIMEOUT_MS = 8000;
16
+
10
17
  module.exports = {
11
- name: 'join',
12
- description: 'Join a collaboration room',
13
- aliases: ['enter', 'goto'],
18
+ name: 'join',
19
+ description: 'Join a collaboration room',
20
+ aliases: ['enter', 'goto'],
14
21
  requiresAuth: true,
15
-
22
+
16
23
  async execute(args, shell) {
17
24
  if (args.length === 0) {
18
25
  console.log(chalk.red('❌ Usage: join <room-id> [password]'));
19
26
  return;
20
27
  }
21
-
22
- const roomId = args[0];
28
+
29
+ const roomId = args[0];
23
30
  const password = args[1];
24
-
25
- console.log(chalk.cyan(`🔗 Joining room: ${roomId}...`));
26
-
27
- try {
28
- const result = await shell.api.joinRoom(roomId, password);
29
-
30
- console.log(chalk.green(`✅ Joined ${result.room.name}`));
31
-
32
- // Show room info
33
- console.log(chalk.dim(` 📝 ${result.room.description || 'No description'}`));
34
- console.log(chalk.dim(` 👥 ${result.participants.length} participants`));
35
-
36
- // Show recent messages
37
- if (result.recentMessages && result.recentMessages.length > 0) {
38
- console.log(chalk.cyan('\n📜 Recent messages:'));
39
- result.recentMessages.slice(-3).forEach(msg => {
40
- const time = new Date(msg.timestamp).toLocaleTimeString();
41
- console.log(chalk.dim(` [${time}] ${msg.username}: ${msg.message}`));
42
- });
43
- }
44
-
45
- // Show participants
46
- if (result.participants.length > 0) {
47
- console.log(chalk.cyan('\n👥 In room:'));
48
- const online = result.participants.filter(p => p.online);
49
- online.forEach(p => {
50
- const you = p.email === shell.api.getUser().email ? chalk.green(' (you)') : '';
51
- console.log(` ${chalk.green('●')} ${p.name || p.email}${you}`);
52
- });
31
+
32
+ // ── Step 1: auto-connect WebSocket if needed ──────────────────────
33
+ if (!shell.ws.isConnected()) {
34
+ console.log(chalk.dim(' Connecting...'));
35
+ try {
36
+ await shell.ws.connect();
37
+ } catch (err) {
38
+ console.log(chalk.red(`❌ WebSocket connect failed: ${err.message}`));
39
+ return;
53
40
  }
54
-
55
- console.log(chalk.dim('\n💡 Type "say <message>" to chat'));
56
-
41
+ }
42
+
43
+ console.log(chalk.cyan(`🔗 Joining room ${chalk.bold(roomId)}...`));
44
+
45
+ // ── Step 2: REST call — validates membership, returns room meta ───
46
+ let result;
47
+ try {
48
+ result = await shell.api.joinRoom(roomId, password);
57
49
  } catch (error) {
58
- if (error.message.includes('password')) {
59
- console.log(chalk.red(`❌ Room requires password: join ${roomId} <password>`));
60
- } else if (error.message.includes('not found')) {
50
+ if (error.message?.toLowerCase().includes('password')) {
51
+ console.log(chalk.red(`❌ Room requires a password: join ${roomId} <password>`));
52
+ } else if (error.message?.toLowerCase().includes('not found')) {
61
53
  console.log(chalk.red(`❌ Room not found: ${roomId}`));
62
- console.log(chalk.dim('💡 Type "rooms" to see available rooms'));
54
+ console.log(chalk.dim(' Type "rooms" to see available rooms'));
55
+ } else if (error.message?.toLowerCase().includes('not a member') ||
56
+ error.message?.toLowerCase().includes('unauthorized')) {
57
+ console.log(chalk.red(`❌ You are not a member of this room`));
63
58
  } else {
64
59
  console.log(chalk.red(`❌ Failed to join: ${error.message}`));
65
60
  }
61
+ return;
66
62
  }
63
+
64
+ const room = result.room || result;
65
+ const user = shell.api.getUser?.() || {};
66
+
67
+ // ── Step 3: emit thinknsh:join and wait for thinknsh:joined ──────
68
+ await new Promise((resolve) => {
69
+ const timer = setTimeout(() => {
70
+ shell.ws.socket?.off('thinknsh:joined', onJoined);
71
+ shell.ws.socket?.off('thinknsh:error', onError);
72
+ console.log(chalk.red('❌ Join timeout — server did not confirm. Try again.'));
73
+ resolve();
74
+ }, JOIN_TIMEOUT_MS);
75
+
76
+ function onJoined(data) {
77
+ clearTimeout(timer);
78
+ shell.ws.socket?.off('thinknsh:error', onError);
79
+
80
+ if (!data.success) {
81
+ console.log(chalk.red(`❌ Join rejected: ${data.message || 'unknown error'}`));
82
+ resolve();
83
+ return;
84
+ }
85
+
86
+ // ── Success output ────────────────────────────────────────
87
+ console.log(chalk.green(`✅ Joined ${chalk.bold(room.name || roomId)}`));
88
+
89
+ if (room.description)
90
+ console.log(chalk.dim(` 📝 ${room.description}`));
91
+
92
+ const participants = result.participants || [];
93
+ if (participants.length)
94
+ console.log(chalk.dim(` 👥 ${participants.length} participant(s)`));
95
+
96
+ // Recent message history from thinknsh:joined payload
97
+ const history = data.history || [];
98
+ if (history.length) {
99
+ console.log(chalk.cyan('\n📜 Recent messages:'));
100
+ history.slice(-5).forEach(msg => {
101
+ const time = new Date(msg.timestamp).toLocaleTimeString();
102
+ const isYou = msg.userId === user._id?.toString() || msg.userId === user.id?.toString();
103
+ const who = isYou ? chalk.green('you') : chalk.cyan(msg.name || 'Unknown');
104
+ const src = msg.source === 'shell' ? chalk.dim('[shell]') : '';
105
+ console.log(` ${chalk.dim(`[${time}]`)} ${who}${src}: ${msg.message}`);
106
+ });
107
+ }
108
+
109
+ // Online participants
110
+ const online = participants.filter(p => p.online);
111
+ if (online.length) {
112
+ console.log(chalk.cyan('\n👥 Online:'));
113
+ online.forEach(p => {
114
+ const isYou = p.email === user.email ? chalk.green(' (you)') : '';
115
+ console.log(` ${chalk.green('●')} ${p.name || p.email}${isYou}`);
116
+ });
117
+ }
118
+
119
+ console.log(chalk.dim('\n💡 say <message> to chat | leave to exit\n'));
120
+ resolve();
121
+ }
122
+
123
+ function onError(data) {
124
+ clearTimeout(timer);
125
+ shell.ws.socket?.off('thinknsh:joined', onJoined);
126
+ console.log(chalk.red(`❌ ${data.message || 'Join error'}`));
127
+ resolve();
128
+ }
129
+
130
+ shell.ws.socket.once('thinknsh:joined', onJoined);
131
+ shell.ws.socket.once('thinknsh:error', onError);
132
+
133
+ // Emit the WebSocket join event
134
+ shell.ws.socket.emit('thinknsh:join', {
135
+ roomId,
136
+ userId: user._id?.toString() || user.id?.toString(),
137
+ name: user.name || user.username || 'Unknown',
138
+ userType: user.model || user.role || 'User',
139
+ });
140
+ });
67
141
  }
68
142
  };