thinkncollab-shell 0.0.4 → 0.0.6

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 || 'https://thinkncollab.com',
40
39
  autoConnect: options.autoConnect || false
41
40
  },
42
41
  ...options
@@ -50,7 +49,7 @@ 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
 
@@ -62,8 +61,12 @@ class ThinkNCollabShell extends EventEmitter {
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,18 +136,50 @@ class ThinkNCollabShell extends EventEmitter {
159
136
  }
160
137
  }
161
138
 
162
- // ─── WebSocket → push to notification window ──────────────────────────────
139
+ // ─── WebSocket → Shell + TCP bridge ──────────────────────────────────────
163
140
 
164
141
  setupWebSocketHandlers() {
165
- this.ws.on('connected', (d) => this.pushNotification({ type: 'connected', socketId: d.socketId }));
142
+ this.ws.on('connected', (d) => this.pushNotification({ type: 'connected', socketId: d.socketId }));
166
143
  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 }));
144
+ this.ws.on('reconnected', () => this.pushNotification({ type: 'connected', socketId: 'reconnected' }));
145
+
146
+ this.ws.on('message', (d) => {
147
+ const myId = this.api.getUser()?._id;
148
+ if (myId && d.userId === myId) return;
149
+ this.pushNotification({ type: 'message', from: d.username, text: d.message });
150
+ });
151
+
152
+ this.ws.on('messageHistory', (messages) => {
153
+ if (!messages?.length) return;
154
+ console.log(chalk.dim('\n ─── Room History ───────────────────────────────'));
155
+ messages.forEach(msg => {
156
+ const from = msg.name || msg.username || msg.sender?.name || 'Unknown';
157
+ const text = msg.message || msg.content || msg.text || '';
158
+ const ts = msg.timestamp || msg.createdAt;
159
+ const timeStr = ts ? chalk.dim(`[${new Date(ts).toLocaleTimeString()}]`) : '';
160
+ console.log(` ${timeStr} ${chalk.cyan(from + ':')} ${chalk.white(text)}`);
161
+ });
162
+ console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
163
+ });
164
+
165
+ this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
166
+ this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
167
+
168
+ this.ws.on('notification', (d) => {
169
+ this.pushNotification({
170
+ type: 'notification',
171
+ level: d.type,
172
+ title: d.title,
173
+ message: d.message,
174
+ taskTitle: d.taskTitle,
175
+ assignedBy: d.assignedBy,
176
+ });
177
+ });
178
+
179
+ this.ws.on('error', (e) => {
180
+ console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
181
+ this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
182
+ });
174
183
  }
175
184
 
176
185
  // ─── Start ────────────────────────────────────────────────────────────────
@@ -178,90 +187,149 @@ class ThinkNCollabShell extends EventEmitter {
178
187
  async start() {
179
188
  this.showWelcome();
180
189
 
181
- // 1. Start TCP server for notification IPC
182
190
  await this.startTCPServer();
183
191
  console.log(chalk.dim(` Notification server ready on port ${this.notifyPort}`));
184
192
 
185
- // 2. Spawn notification window
186
193
  this.spawnNotificationWindow();
187
194
  console.log(chalk.dim(' Notification window opening...\n'));
188
195
 
189
196
  this.running = true;
197
+ process.on('SIGINT', () => console.log(chalk.yellow('\nUse "exit" to quit')));
190
198
 
191
- process.on('SIGINT', () => {
192
- console.log(chalk.yellow('\nUse "exit" to quit'));
193
- });
199
+ // Try .tncproject auto-boot first
200
+ const booted = await this._bootFromProjectConfig();
194
201
 
195
- if (this.config.websocket.autoConnect) {
196
- await this.ws.connect();
202
+ // If no .tncproject but autoConnect is set, connect WS anyway
203
+ if (!booted && this.config.websocket.autoConnect) {
204
+ await this.ws.connect().catch(() => {});
197
205
  }
198
206
 
199
207
  const { input } = require('@inquirer/prompts');
200
208
 
201
209
  while (this.running) {
202
210
  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
-
211
+ const line = await input({ message: this.getPromptText(), required: false }).catch(() => null);
212
+ if (line === null) { console.log(chalk.yellow('Use "exit" to quit')); continue; }
213
213
  const trimmed = line.trim();
214
214
  if (!trimmed) continue;
215
-
216
215
  this.history.push(trimmed);
217
216
  this.saveHistory();
218
217
  await this.execute(trimmed);
219
-
220
218
  } catch (err) {
221
- if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
219
+ if (err.name === 'ExitPromptError' || err.message?.includes('force closed'))
222
220
  console.log(chalk.yellow('Use "exit" to quit'));
223
- } else {
221
+ else
224
222
  console.log(chalk.red(`Shell error: ${err.message}`));
225
- }
226
223
  }
227
224
  }
228
225
  }
229
226
 
230
- restoreReadline() {}
227
+ // ─── Boot from .tncproject ────────────────────────────────────────────────
231
228
 
232
- // ─── Execute ──────────────────────────────────────────────────────────────
229
+ async _bootFromProjectConfig() {
230
+ if (!hasProjectConfig()) return false;
231
+
232
+ let cfg;
233
+ try {
234
+ cfg = loadProjectConfig();
235
+ } catch (err) {
236
+ console.log(chalk.yellow(`⚠️ .tncproject: ${err.message}\n`));
237
+ return false;
238
+ }
233
239
 
234
- async execute(input) {
235
- if (!input.trim()) return;
240
+ console.log(chalk.dim(` 📄 .tncproject found`));
241
+ console.log(chalk.dim(` Room : ${cfg.roomId}`));
242
+ console.log(chalk.dim(` File : ${cfg.filePath}\n`));
236
243
 
237
- const args = this.parseCommand(input);
238
- const cmdName = args[0].toLowerCase();
239
- const cmdArgs = args.slice(1);
244
+ // Send raw encrypted file to server — server decrypts, returns shellToken
245
+ let result;
246
+ try {
247
+ result = await this.api._request('POST', '/config/verify-config', {
248
+ fileBase64: cfg.fileBase64,
249
+ roomId: cfg.roomId,
250
+ });
251
+ } catch (err) {
252
+ console.log(chalk.red(`❌ .tncproject auth failed: ${err.message}`));
253
+ console.log(chalk.dim(' Falling back to manual login.\n'));
254
+ return false;
255
+ }
240
256
 
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;
257
+ // Save session exactly like manual login
258
+ this.api._saveSession({
259
+ token: result.shellToken,
260
+ user: {
261
+ _id: result._id.toString(),
262
+ userId: result._id.toString(),
263
+ email: result.email,
264
+ name: result.name,
265
+ userType: 'User',
266
+ },
267
+ timestamp: new Date().toISOString(),
268
+ });
269
+
270
+ this.ws.setUser(this.api.getUser(), result.shellToken);
271
+
272
+ // Connect WebSocket
273
+ try {
274
+ await this.ws.connect(result.shellToken);
275
+ } catch (err) {
276
+ console.log(chalk.yellow(`⚠️ WebSocket: ${err.message}`));
277
+ console.log(chalk.green(`✅ ${result.name} logged in (offline mode)\n`));
278
+ return true;
250
279
  }
251
280
 
252
- if (this.commandRegistry) {
253
- const handled = await this.commandRegistry.execute(input, this);
254
- if (handled) return;
281
+ // Auto-join the project room
282
+ try {
283
+ await this.ws.joinRoom(result.roomId.toString());
284
+ console.log(chalk.green(`✅ ${result.name} → room "${result.roomName || result.roomId}"\n`));
285
+ } catch (err) {
286
+ console.log(chalk.yellow(`⚠️ Could not auto-join room: ${err.message}`));
287
+ console.log(chalk.dim(' Use "join <room-id>" manually.\n'));
288
+ }
289
+
290
+ return true;
291
+ }
292
+
293
+ // ─── Execute ──────────────────────────────────────────────────────────────
294
+ // shell.js — execute() mein ye change karo
295
+ async execute(input) {
296
+ if (!input.trim()) return;
297
+
298
+ const args = this.parseCommand(input);
299
+ const cmdName = args[0].toLowerCase();
300
+ const cmdArgs = args.slice(1);
301
+
302
+ if (this.commands.has(cmdName)) {
303
+ try { await this.commands.get(cmdName).handler(cmdArgs, this); }
304
+ catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
305
+ process.stdout.write('\r\n');
306
+ return;
307
+ }
308
+
309
+ if (this.commandRegistry) {
310
+ const handled = await this.commandRegistry.execute(input, this);
311
+ if (handled) {
312
+ process.stdout.write('\r\n');
313
+ return;
255
314
  }
315
+ }
256
316
 
257
- await this.executeSystemCommand(input);
317
+ // ★ Windows pe known-useless OS commands block karo
318
+ const blockedOnWindows = [ 'open', 'which', 'grep', 'touch', 'cat'];
319
+ if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
320
+ console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
321
+ console.log(chalk.dim(` Type 'help' to see available commands.`));
322
+ process.stdout.write('\r\n');
323
+ return;
258
324
  }
259
325
 
326
+ await this.executeSystemCommand(input);
327
+ }
260
328
  // ─── Prompt ───────────────────────────────────────────────────────────────
261
329
 
262
330
  getPromptText() {
263
331
  let p = '';
264
- const room = this.ws.getCurrentRoom?.();
332
+ const room = this.ws.getCurrentRoom?.();
265
333
  if (room) p += `[${room}] `;
266
334
  if (this.ws.isConnected?.()) p += '● ';
267
335
 
@@ -277,43 +345,38 @@ class ThinkNCollabShell extends EventEmitter {
277
345
  displayPath = displayPath.replace(homeDir, '~');
278
346
  }
279
347
 
280
- p += `${this.username}@thinknsh:${displayPath} $`;
281
- return p;
348
+ return `${this.username}@thinknsh:${displayPath} $`;
282
349
  }
283
350
 
284
- getPrompt() { return this.getPromptText(); }
285
-
351
+ getPrompt() { return this.getPromptText(); }
286
352
  printMessage(msg) { console.log(msg); }
287
353
 
288
354
  // ─── Built-ins ────────────────────────────────────────────────────────────
289
355
 
290
356
  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');
357
+ const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
358
+
359
+ r('help', this.helpCommand.bind(this), 'Show available commands');
360
+ r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
361
+ r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
362
+ r('clear', () => console.clear(), 'Clear screen');
363
+ r('history', this.historyCommand.bind(this), 'Show command history');
364
+ r('cd', this.cdCommand.bind(this), 'Change directory');
365
+ r('pwd', () => console.log(process.cwd()), 'Print working directory');
366
+ r('ls', this.lsCommand.bind(this), 'List files');
367
+ r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
368
+ r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
369
+ r('status', this.statusCommand.bind(this), 'Show connection status');
370
+ r('join', this.joinCommand.bind(this), 'Join a room');
371
+ r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
372
+ r('rooms', this.roomsCommand.bind(this), 'List rooms');
373
+ r('say', this.sayCommand.bind(this), 'Send a message');
374
+ r('msg', this.sayCommand.bind(this), 'Send a message');
375
+ r('set', this.setCommand.bind(this), 'Set a variable');
376
+ r('unset', this.unsetCommand.bind(this), 'Unset a variable');
313
377
 
314
378
  this.addAlias('ll', 'ls -la');
315
379
  this.addAlias('..', 'cd ..');
316
- this.addAlias('...', 'cd ../..');
317
380
  this.addAlias('~', 'cd ~');
318
381
  }
319
382
 
@@ -321,26 +384,8 @@ class ThinkNCollabShell extends EventEmitter {
321
384
  addAlias(alias, command) { this.aliases.set(alias, command); }
322
385
 
323
386
  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'));
387
+ if (this.commandRegistry) console.log(this.commandRegistry.getHelpText());
388
+ console.log(chalk.dim(' Any unrecognised command runs in your system shell\n'));
344
389
  }
345
390
 
346
391
  historyCommand() {
@@ -351,11 +396,8 @@ class ThinkNCollabShell extends EventEmitter {
351
396
  const target = args.join(' ').trim() || os.homedir();
352
397
  try {
353
398
  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));
399
+ if (target === '~') newPath = os.homedir();
356
400
  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
401
  else newPath = path.resolve(this.currentDir, target);
360
402
  newPath = path.normalize(newPath);
361
403
  const stats = await fs.stat(newPath);
@@ -369,22 +411,18 @@ class ThinkNCollabShell extends EventEmitter {
369
411
  async lsCommand(args) {
370
412
  try {
371
413
  const files = await fs.readdir(this.currentDir);
372
- const showAll = args.includes('-a') || args.includes('--all');
414
+ const showAll = args.includes('-a');
373
415
  const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
374
416
  const fileList = await Promise.all(filtered.map(async (file) => {
375
417
  const stat = await fs.stat(path.join(this.currentDir, file));
376
- return { name: file, isDir: stat.isDirectory(), mode: stat.mode };
418
+ return { name: file, isDir: stat.isDirectory() };
377
419
  }));
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
- });
420
+ fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
383
421
  } catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
384
422
  }
385
423
 
386
- async connectCommand(args) {
387
- console.log(chalk.cyan(`🔌 Connecting...`));
424
+ async connectCommand() {
425
+ console.log(chalk.cyan('🔌 Connecting...'));
388
426
  try { await this.ws.connect(); }
389
427
  catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
390
428
  }
@@ -392,22 +430,21 @@ class ThinkNCollabShell extends EventEmitter {
392
430
  async statusCommand() {
393
431
  const connected = this.ws.isConnected();
394
432
  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
- }
433
+ console.log(` WebSocket : ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
434
+ if (connected) console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);
400
435
  const room = this.ws.getCurrentRoom();
401
- console.log(`Room: ${room ? chalk.green(room) : chalk.yellow('None')}`);
402
- console.log(`Directory: ${chalk.blue(this.currentDir)}`);
436
+ console.log(` Room : ${room ? chalk.green(room) : chalk.yellow('None')}`);
437
+ console.log(` Auth : ${this.api.isAuthenticated() ? chalk.green('✓ Logged in') : chalk.yellow('Not logged in')}`);
438
+ const user = this.api.getUser();
439
+ if (user) console.log(` User : ${chalk.dim(user.name || user.email)}`);
440
+ console.log('');
403
441
  }
404
442
 
405
443
  async joinCommand(args) {
406
- if (!args[0]) { console.log(chalk.red('Usage: join <room-id> [password]')); return; }
444
+ if (!args[0]) { console.log(chalk.red('Usage: join <room-id>')); return; }
407
445
  if (!this.ws.isConnected()) { console.log(chalk.yellow('Connecting...')); await this.ws.connect(); }
408
- console.log(chalk.cyan(`🔗 Joining: ${args[0]}...`));
409
446
  try {
410
- const result = await this.ws.joinRoom(args[0], args[1]);
447
+ const result = await this.ws.joinRoom(args[0]);
411
448
  console.log(chalk.green(`✅ Joined: ${result.roomName || args[0]}`));
412
449
  } catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
413
450
  }
@@ -417,61 +454,30 @@ class ThinkNCollabShell extends EventEmitter {
417
454
  this.ws.socket.emit('rooms:list');
418
455
  this.ws.socket.once('rooms:list', (data) => {
419
456
  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');
457
+ else console.log(' No rooms');
421
458
  });
422
459
  }
423
460
 
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
461
  async sayCommand(args) {
435
462
  const message = args.join(' ');
436
463
  if (!message) { console.log(chalk.red('Usage: say <message>')); return; }
437
464
  try {
438
465
  await this.ws.sendMessage(message);
466
+ const displayName = this.api.getUser()?.name || this.username;
439
467
  console.log(chalk.green(`You: ${message}`));
468
+ this.pushNotification({ type: 'message', from: `You (${displayName})`, text: message });
440
469
  } catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
441
470
  }
442
471
 
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
472
  async setCommand(args) {
466
473
  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
474
  this.variables.set(args[0], args.slice(1).join(' '));
469
- console.log(chalk.green(`✅ ${args[0]} = ${args.slice(1).join(' ')}`));
475
+ console.log(chalk.green(`${args[0]} = ${args.slice(1).join(' ')}`));
470
476
  }
471
477
 
472
478
  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`));
479
+ if (!args[0]) return;
480
+ this.variables.delete(args[0]);
475
481
  }
476
482
 
477
483
  getSessionPath() { return path.join(this.config.configDir, 'session.json'); }
@@ -494,22 +500,28 @@ class ThinkNCollabShell extends EventEmitter {
494
500
  executeSystemCommand(command) {
495
501
  return new Promise((resolve) => {
496
502
  const child = spawn(command, [], {
497
- cwd: this.currentDir, shell: true, stdio: 'inherit',
498
- env: { ...process.env, THINKNCOLLAB_SHELL: '1' }
503
+ cwd: this.currentDir,
504
+ shell: true,
505
+ stdio: 'inherit'
506
+ });
507
+ child.on('close', (code) => {
508
+ // Update currentDir in case command changed it (cd won't but others might)
509
+ try { this.currentDir = process.cwd(); } catch {}
510
+ process.stdout.write('\r\n');
511
+ resolve();
512
+ });
513
+ child.on('error', () => {
514
+ console.log(chalk.red(`Command not found: ${command}`));
515
+ resolve();
499
516
  });
500
- child.on('close', () => resolve());
501
- child.on('error', () => { console.log(chalk.red(`Command not found: ${command}`)); resolve(); });
502
517
  });
503
518
  }
504
519
 
505
520
  showWelcome() {
506
- try {
507
- const figlet = require('figlet');
508
- console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' })));
509
- } catch {}
521
+ try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
510
522
  console.log(chalk.cyan(`
511
523
  ╔══════════════════════════════════════════════════════╗
512
- ║ ThinkNCollab Shell v0.0.04 ║
524
+ ║ ThinkNCollab Shell v0.0.5 ║
513
525
  ║ Type 'help' for commands ║
514
526
  ╚══════════════════════════════════════════════════════╝`));
515
527
  }