thinkncollab-shell 0.0.10 → 0.0.13

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,17 +1,17 @@
1
1
  /**
2
- * src/core/shell.js — ThinkNCollab Shell Core
2
+ * src/core/shell.js — ThinkNCollab Shell Core sound
3
3
  */
4
4
 
5
- const path = require('path');
6
- const fs = require('fs-extra');
7
- const os = require('os');
8
- const chalk = require('chalk');
9
- const net = require('net');
5
+ const path = require('path');
6
+ const fs = require('fs-extra');
7
+ const os = require('os');
8
+ const chalk = require('chalk');
9
+ const net = require('net');
10
10
  const { spawn } = require('child_process');
11
- const EventEmitter = require('events');
11
+ const EventEmitter = require('events');
12
12
  const WebSocketManager = require('./websocket');
13
- const CommandRegistry = require('../commands');
14
- const ThinkNCollabAPI = require('./api-client');
13
+ const CommandRegistry = require('../commands');
14
+ const ThinkNCollabAPI = require('./api-client');
15
15
  const { loadProjectConfig, hasProjectConfig } = require('./project-config');
16
16
 
17
17
  function getAvailablePort(startPort) {
@@ -32,10 +32,10 @@ class ThinkNCollabShell extends EventEmitter {
32
32
 
33
33
  this.config = {
34
34
  historyFile: path.join(os.homedir(), '.thinknsh_history'),
35
- configDir: path.join(os.homedir(), '.thinkncollab'),
36
- maxHistory: 1000,
35
+ configDir: path.join(os.homedir(), '.thinkncollab'),
36
+ maxHistory: 1000,
37
37
  websocket: {
38
- serverUrl: options.serverUrl || 'https://thinkncollab.com',
38
+ serverUrl: options.serverUrl || 'https://thinkncollab.com',
39
39
  autoConnect: options.autoConnect || false
40
40
  },
41
41
  ...options
@@ -43,23 +43,23 @@ class ThinkNCollabShell extends EventEmitter {
43
43
 
44
44
  fs.ensureDirSync(this.config.configDir);
45
45
 
46
- this.ws = new WebSocketManager(this.config.websocket);
46
+ this.ws = new WebSocketManager(this.config.websocket);
47
47
  this.currentDir = process.cwd();
48
- this.username = os.userInfo().username;
49
- this.history = [];
50
- this.running = false;
48
+ this.username = os.userInfo().username;
49
+ this.history = [];
50
+ this.running = false;
51
51
 
52
52
  // TCP notification IPC
53
53
  this.notifyClients = new Set();
54
- this.tcpServer = null;
54
+ this.tcpServer = null;
55
55
 
56
56
  this.api = new ThinkNCollabAPI({
57
57
  apiUrl: options.apiUrl || 'https://thinkncollab.com',
58
- wsUrl: options.wsUrl || 'https://thinkncollab.com'
58
+ wsUrl: options.wsUrl || 'https://thinkncollab.com'
59
59
  });
60
60
 
61
- this.commands = new Map();
62
- this.aliases = new Map();
61
+ this.commands = new Map();
62
+ this.aliases = new Map();
63
63
  this.variables = new Map();
64
64
 
65
65
  // Restore persisted session on startup
@@ -96,235 +96,272 @@ class ThinkNCollabShell extends EventEmitter {
96
96
  if (this.notifyClients.size === 0) return;
97
97
  const line = JSON.stringify(data) + '\n';
98
98
  for (const client of this.notifyClients) {
99
- try { client.write(line); } catch {}
99
+ try { client.write(line); } catch { }
100
100
  }
101
101
  }
102
102
 
103
103
  // ─── Spawn notification window ────────────────────────────────────────────
104
- spawnNotificationWindow() {
105
- const notifyScript = path.join(__dirname, 'notification-window.js');
106
- const port = this.notifyPort;
107
- const env = { ...process.env, THINKNSH_NOTIFY_PORT: String(port) };
108
-
109
- const isWSL = process.platform === 'linux' &&
110
- (() => { try { return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); } catch { return false; } })();
111
-
112
- const fallback = () => {
113
- console.log(chalk.yellow(`\n ⚠️ Could not open notification window.`));
114
- console.log(chalk.dim(` Open a new terminal and run:`));
115
- console.log(chalk.cyan(` THINKNSH_NOTIFY_PORT=${port} node "${notifyScript}"\n`));
116
- };
117
-
118
- try {
119
- // ── WSL ──────────────────────────────────────────────────────────────
120
- if (isWSL) {
121
- const winScript = notifyScript
122
- .replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
123
- .replace(/\//g, '\\');
124
-
125
- const launchWinNode = (winExe) => {
126
- const p = spawn(
127
- `start cmd.exe /k ""${winExe}" "${winScript}""`,
128
- [],
129
- { detached: true, stdio: 'ignore', env, shell: true }
130
- );
131
- p.on('error', fallback);
132
- p.unref();
133
- };
134
-
135
- // 1. Known paths — instant
136
- const known = [
137
- '/mnt/c/Program Files/nodejs/node.exe',
138
- '/mnt/c/Program Files (x86)/nodejs/node.exe',
139
- ];
140
- const found = known.find(p => { try { return fs.existsSync(p); } catch { return false; } });
141
-
142
- if (found) {
143
- const winExe = found
144
- .replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
145
- .replace(/\//g, '\\');
146
- launchWinNode(winExe);
147
- } else {
148
- // 2. Async where node
149
- const child = spawn('cmd.exe', ['/c', 'where', 'node'], {
150
- stdio: ['ignore', 'pipe', 'ignore']
151
- });
152
- let out = '';
153
- child.stdout.on('data', d => out += d);
154
- child.on('close', () => {
155
- const winExe = out.trim().split('\n')[0]?.trim();
156
- if (winExe?.endsWith('.exe')) {
157
- launchWinNode(winExe);
158
- } else {
159
- // 3. Last resort
160
- const p = spawn(
161
- `start cmd.exe /k "wsl.exe -- bash -c 'THINKNSH_NOTIFY_PORT=${port} node \\"${notifyScript}\\"; read'"`,
162
- [],
163
- { detached: true, stdio: 'ignore', shell: true }
164
- );
165
- p.on('error', fallback);
166
- p.unref();
167
- }
168
- });
169
- }
170
-
171
- // ── Windows native ────────────────────────────────────────────────────
172
- } else if (process.platform === 'win32') {
173
- const nodeExe = process.execPath;
174
- const isVSCode = process.env.TERM_PROGRAM === 'vscode';
175
-
176
- if (isVSCode) {
177
- console.log(chalk.yellow('\n VS Code detected — open a new terminal tab and run:'));
178
- console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${port}; node "${notifyScript}"\n`));
179
- } else {
180
- const launchCmd = () => {
181
- // shell:true — Windows quoting issues handle ho jaate hain
182
- const p = spawn(
183
- `start cmd.exe /k ""${nodeExe}" "${notifyScript}""`,
184
- [],
185
- { detached: true, stdio: 'ignore', env, shell: true }
186
- );
187
- p.on('error', fallback);
188
- p.unref();
104
+ spawnNotificationWindow() {
105
+ const notifyScript = path.join(__dirname, 'notification-window.js');
106
+ const port = this.notifyPort;
107
+ const env = { ...process.env, THINKNSH_NOTIFY_PORT: String(port) };
108
+
109
+ const isWSL = process.platform === 'linux' &&
110
+ (() => { try { return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); } catch { return false; } })();
111
+
112
+ const fallback = () => {
113
+ console.log(chalk.yellow(`\n ⚠️ Could not open notification window.`));
114
+ console.log(chalk.dim(` Open a new terminal and run:`));
115
+ console.log(chalk.cyan(` THINKNSH_NOTIFY_PORT=${port} node "${notifyScript}"\n`));
189
116
  };
190
117
 
191
- const wt = spawn('wt.exe', [
192
- 'cmd.exe', '/k', `"${nodeExe}" "${notifyScript}"`
193
- ], { detached: true, stdio: 'ignore', env });
194
- wt.on('error', launchCmd);
195
- wt.unref();
196
- }
197
- // ── macOS ─────────────────────────────────────────────────────────────
198
- } else if (process.platform === 'darwin') {
199
- const nodeExe = process.execPath;
200
- const isVSCode = process.env.TERM_PROGRAM === 'vscode';
201
- if (isVSCode) {
202
- console.log(chalk.yellow(`\n VS Code detected — open a new terminal tab and run:`));
203
- console.log(chalk.cyan(` THINKNSH_NOTIFY_PORT=${port} node "${notifyScript}"\n`));
204
- return;
205
- }
206
- const terminalScript = `
207
- tell application "Terminal"
208
- do script "THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'"
209
- activate
210
- end tell
211
- `;
212
- try {
213
- spawn('osascript', ['-e', terminalScript], {
214
- detached: true, stdio: 'ignore'
215
- }).unref();
216
- } catch {
217
- // iTerm2 fallback
218
- const itermScript = `
219
- tell application "iTerm2"
220
- tell current window
221
- create tab with default profile
222
- tell current session of current tab
223
- write text "THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'"
224
- end tell
225
- end tell
226
- end tell
227
- `;
118
+ try {
119
+ // ── WSL ──────────────────────────────────────────────────────────────
120
+ if (isWSL) {
121
+ const winScript = notifyScript
122
+ .replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
123
+ .replace(/\//g, '\\');
124
+
125
+ const launchWinNode = (winExe) => {
126
+ const p = spawn(
127
+ `start cmd.exe /k ""${winExe}" "${winScript}""`,
128
+ [],
129
+ { detached: true, stdio: 'ignore', env, shell: true }
130
+ );
131
+ p.on('error', fallback);
132
+ p.unref();
133
+ };
134
+
135
+ // 1. Known paths — instant
136
+ const known = [
137
+ '/mnt/c/Program Files/nodejs/node.exe',
138
+ '/mnt/c/Program Files (x86)/nodejs/node.exe',
139
+ ];
140
+ const found = known.find(p => { try { return fs.existsSync(p); } catch { return false; } });
141
+
142
+ if (found) {
143
+ const winExe = found
144
+ .replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
145
+ .replace(/\//g, '\\');
146
+ launchWinNode(winExe);
147
+ } else {
148
+ // 2. Async where node
149
+ const child = spawn('cmd.exe', ['/c', 'where', 'node'], {
150
+ stdio: ['ignore', 'pipe', 'ignore']
151
+ });
152
+ let out = '';
153
+ child.stdout.on('data', d => out += d);
154
+ child.on('close', () => {
155
+ const winExe = out.trim().split('\n')[0]?.trim();
156
+ if (winExe?.endsWith('.exe')) {
157
+ launchWinNode(winExe);
158
+ } else {
159
+ // 3. Last resort
160
+ const p = spawn(
161
+ `start cmd.exe /k "wsl.exe -- bash -c 'THINKNSH_NOTIFY_PORT=${port} node \\"${notifyScript}\\"; read'"`,
162
+ [],
163
+ { detached: true, stdio: 'ignore', shell: true }
164
+ );
165
+ p.on('error', fallback);
166
+ p.unref();
167
+ }
168
+ });
169
+ }
170
+
171
+ // ── Windows native ────────────────────────────────────────────────────
172
+ } else if (process.platform === 'win32') {
173
+ const nodeExe = process.execPath;
174
+ const isVSCode = process.env.TERM_PROGRAM === 'vscode';
175
+
176
+ if (isVSCode) {
177
+ console.log(chalk.yellow('\n VS Code detected — open a new terminal tab and run:'));
178
+ console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${port}; node "${notifyScript}"\n`));
179
+ } else {
180
+ const launchCmd = () => {
181
+ // shell:true — Windows quoting issues handle ho jaate hain
182
+ const p = spawn(
183
+ `start cmd.exe /k ""${nodeExe}" "${notifyScript}""`,
184
+ [],
185
+ { detached: true, stdio: 'ignore', env, shell: true }
186
+ );
187
+ p.on('error', fallback);
188
+ p.unref();
189
+ };
190
+
191
+ const wt = spawn('wt.exe', [
192
+ 'cmd.exe', '/k', `"${nodeExe}" "${notifyScript}"`
193
+ ], { detached: true, stdio: 'ignore', env });
194
+ wt.on('error', launchCmd);
195
+ wt.unref();
196
+ }
197
+ // ── macOS ─────────────────────────────────────────────────────────────
198
+ } else if (process.platform === 'darwin') {
199
+ const nodeExe = process.execPath;
200
+
201
+ // Better VS Code detection - multiple checks
202
+ const isVSCode = process.env.TERM_PROGRAM === 'vscode' ||
203
+ process.env.VSCODE_INJECTION === '1' ||
204
+ process.env.TERM_PROGRAM?.includes('vscode') ||
205
+ process.env.VSCODE_CWD !== undefined;
206
+
207
+ if (isVSCode) {
208
+ console.log(chalk.yellow(`\n ⚠️ VS Code terminal detected`));
209
+ console.log(chalk.dim(` Notification window can't auto-open in VS Code.`));
210
+ console.log(chalk.cyan(` Please open Terminal.app and run:\n`));
211
+ console.log(chalk.white(` THINKNSH_NOTIFY_PORT=${port} node "${notifyScript}"\n`));
212
+ return;
213
+ }
214
+
215
+ // Rest of the macOS code for Terminal.app/iTerm2
216
+ const terminalScript = `
217
+ tell application "Terminal"
218
+ do script "THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'"
219
+ activate
220
+ end tell
221
+ `;
228
222
  try {
229
- spawn('osascript', ['-e', itermScript], {
223
+ spawn('osascript', ['-e', terminalScript], {
230
224
  detached: true, stdio: 'ignore'
231
225
  }).unref();
232
- } catch { fallback(); }
226
+ } catch {
227
+ // iTerm2 fallback
228
+ const itermScript = `
229
+ tell application "iTerm2"
230
+ tell current window
231
+ create tab with default profile
232
+ tell current session of current tab
233
+ write text "THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'"
234
+ end tell
235
+ end tell
236
+ end tell
237
+ `;
238
+ try {
239
+ spawn('osascript', ['-e', itermScript], {
240
+ detached: true, stdio: 'ignore'
241
+ }).unref();
242
+ } catch {
243
+ console.log(chalk.yellow(` ⚠️ Could not open notification window`));
244
+ }
245
+ }
246
+
247
+ // ── Linux native ──────────────────────────────────────────────────────
248
+ } else {
249
+ const nodeExe = process.execPath;
250
+ const bashCmd = `THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'; read`;
251
+ const terminals = [
252
+ ['gnome-terminal', ['--', 'bash', '-c', bashCmd]],
253
+ ['konsole', ['--noclose', '-e', 'bash', '-c', bashCmd]],
254
+ ['xfce4-terminal', ['--hold', '-e', `bash -c "${bashCmd}"`]],
255
+ ['tilix', ['-e', `bash -c "${bashCmd}"`]],
256
+ ['alacritty', ['-e', 'bash', '-c', bashCmd]],
257
+ ['kitty', ['bash', '-c', bashCmd]],
258
+ ['xterm', ['-hold', '-e', `bash -c "${bashCmd}"`]],
259
+ ];
260
+
261
+ let spawned = false;
262
+ for (const [bin, args] of terminals) {
263
+ try {
264
+ spawn(bin, args, { detached: true, stdio: 'ignore', env }).unref();
265
+ spawned = true;
266
+ break;
267
+ } catch { }
268
+ }
269
+ if (!spawned) fallback();
233
270
  }
234
271
 
235
- // ── Linux native ──────────────────────────────────────────────────────
236
- } else {
237
- const nodeExe = process.execPath;
238
- const bashCmd = `THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'; read`;
239
- const terminals = [
240
- ['gnome-terminal', ['--', 'bash', '-c', bashCmd]],
241
- ['konsole', ['--noclose', '-e', 'bash', '-c', bashCmd]],
242
- ['xfce4-terminal', ['--hold', '-e', `bash -c "${bashCmd}"`]],
243
- ['tilix', ['-e', `bash -c "${bashCmd}"`]],
244
- ['alacritty', ['-e', 'bash', '-c', bashCmd]],
245
- ['kitty', ['bash', '-c', bashCmd]],
246
- ['xterm', ['-hold', '-e', `bash -c "${bashCmd}"`]],
247
- ];
248
-
249
- let spawned = false;
250
- for (const [bin, args] of terminals) {
251
- try {
252
- spawn(bin, args, { detached: true, stdio: 'ignore', env }).unref();
253
- spawned = true;
254
- break;
255
- } catch {}
256
- }
257
- if (!spawned) fallback();
272
+ } catch (err) {
273
+ fallback();
258
274
  }
259
-
260
- } catch (err) {
261
- fallback();
262
275
  }
263
- }
264
276
 
265
277
  // ─── WebSocket → Shell + TCP bridge ──────────────────────────────────────
266
278
 
267
- setupWebSocketHandlers() {
268
- this.ws.on('connected', (d) => {
269
- this.api.logEvent('connected', null, { socketId: d?.socketId });
270
- this.api.startHeartbeat();
271
- this.pushNotification({ type: 'connected', socketId: d?.socketId });
272
- });
279
+ setupWebSocketHandlers() {
280
+ this.ws.on('connected', (d) => {
281
+ this.api.logEvent('connected', null, { socketId: d?.socketId });
282
+ this.api.startHeartbeat();
283
+ this.pushNotification({ type: 'connected', socketId: d?.socketId });
284
+ });
273
285
 
274
- this.ws.on('disconnected', (d) => {
275
- this.api.logEvent('disconnected', null, { reason: d?.reason });
276
- this.api.stopHeartbeat();
277
- this.pushNotification({ type: 'disconnected', reason: d?.reason });
278
- });
286
+ this.ws.on('disconnected', (d) => {
287
+ this.api.logEvent('disconnected', null, { reason: d?.reason });
288
+ this.api.stopHeartbeat();
289
+ this.pushNotification({ type: 'disconnected', reason: d?.reason });
290
+ });
279
291
 
280
- this.ws.on('reconnected', () => {
281
- this.api.logEvent('reconnected');
282
- this.api.startHeartbeat();
283
- this.pushNotification({ type: 'connected', socketId: 'reconnected' });
284
- });
292
+ this.ws.on('reconnected', () => {
293
+ this.api.logEvent('reconnected');
294
+ this.api.startHeartbeat();
295
+ this.pushNotification({ type: 'connected', socketId: 'reconnected' });
296
+ });
297
+ this.ws.on('task:verdict', (d) => {
298
+ const passed = d.passed;
299
+ const border = passed ? chalk.green('─'.repeat(52)) : chalk.red('─'.repeat(52));
300
+ const badge = passed ? chalk.bgGreen.black(' PASS ') : chalk.bgRed.white(' FAIL ');
301
+
302
+ console.log('\n' + border);
303
+ console.log(` ${badge} ${chalk.bold(d.title || 'Task')} ${passed ? chalk.green('✅ PASSED') : chalk.red('❌ FAILED')}`);
304
+ console.log(` ${chalk.dim('Reason :')} ${passed ? chalk.green(d.reason) : chalk.red(d.reason)}`);
305
+ if (d.diff) {
306
+ console.log(` ${chalk.dim('Expected:')} ${chalk.cyan(JSON.stringify(d.diff.expected ?? d.diff.conditions))}`);
307
+ console.log(` ${chalk.dim('Actual :')} ${chalk.yellow(JSON.stringify(d.diff.actual))}`);
308
+ }
309
+ console.log(` ${chalk.dim('At :')} ${chalk.dim(new Date(d.at || Date.now()).toLocaleTimeString())}`);
310
+ console.log(border + '\n');
311
+
312
+ this.pushNotification({
313
+ type: 'verdict',
314
+ passed,
315
+ title: d.title,
316
+ reason: d.reason,
317
+ taskId: d.taskId,
318
+ at: d.at
319
+ });
320
+ });
285
321
 
286
- this.ws.on('message', (d) => {
287
- const myId = this.api.getUser()?._id;
288
- if (myId && d.userId === myId) return;
289
- this.pushNotification({ type: 'message', from: d.username, text: d.message });
290
- });
322
+ this.ws.on('message', (d) => {
323
+ const myId = this.api.getUser()?._id;
324
+ if (myId && d.userId === myId) return;
325
+ this.pushNotification({ type: 'message', from: d.username, text: d.message });
326
+ });
291
327
 
292
- this.ws.on('messageHistory', (messages) => {
293
- if (!messages?.length) return;
294
- console.log(chalk.dim('\n ─── Room History ───────────────────────────────'));
295
- messages.forEach(msg => {
296
- const from = msg.name || msg.username || msg.sender?.name || 'Unknown';
297
- const text = msg.message || msg.content || msg.text || '';
298
- const ts = msg.timestamp || msg.createdAt;
299
- const timeStr = ts ? chalk.dim(`[${new Date(ts).toLocaleTimeString()}]`) : '';
300
- console.log(` ${timeStr} ${chalk.cyan(from + ':')} ${chalk.white(text)}`);
328
+ this.ws.on('messageHistory', (messages) => {
329
+ if (!messages?.length) return;
330
+ console.log(chalk.dim('\n ─── Room History ───────────────────────────────'));
331
+ messages.forEach(msg => {
332
+ const from = msg.name || msg.username || msg.sender?.name || 'Unknown';
333
+ const text = msg.message || msg.content || msg.text || '';
334
+ const ts = msg.timestamp || msg.createdAt;
335
+ const timeStr = ts ? chalk.dim(`[${new Date(ts).toLocaleTimeString()}]`) : '';
336
+ console.log(` ${timeStr} ${chalk.cyan(from + ':')} ${chalk.white(text)}`);
337
+ });
338
+ console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
301
339
  });
302
- console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
303
- });
304
340
 
305
- this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
306
- this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
307
-
308
- // room join/leave events
309
- this.ws.on('joined', (roomId) => this.api.logEvent('room_join', roomId));
310
- this.ws.on('left', (roomId) => this.api.logEvent('room_leave', roomId));
311
-
312
- this.ws.on('notification', (d) => {
313
- this.pushNotification({
314
- type: 'notification',
315
- level: d.type,
316
- title: d.title,
317
- message: d.message,
318
- taskTitle: d.taskTitle,
319
- assignedBy: d.assignedBy,
341
+ this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
342
+ this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
343
+
344
+ // room join/leave events
345
+ this.ws.on('joined', (roomId) => this.api.logEvent('room_join', roomId));
346
+ this.ws.on('left', (roomId) => this.api.logEvent('room_leave', roomId));
347
+
348
+ this.ws.on('notification', (d) => {
349
+ this.pushNotification({
350
+ type: d.type || 'notification',
351
+ level: d.level,
352
+ title: d.title,
353
+ message: d.message,
354
+ taskTitle: d.taskTitle,
355
+ assignedBy: d.assignedBy,
356
+ meta: d.meta,
357
+ });
320
358
  });
321
- });
322
359
 
323
- this.ws.on('error', (e) => {
324
- console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
325
- this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
326
- });
327
- }
360
+ this.ws.on('error', (e) => {
361
+ console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
362
+ this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
363
+ });
364
+ }
328
365
 
329
366
  // ─── Start ────────────────────────────────────────────────────────────────
330
367
 
@@ -345,7 +382,7 @@ setupWebSocketHandlers() {
345
382
 
346
383
  // If no .tncproject but autoConnect is set, connect WS anyway
347
384
  if (!booted && this.config.websocket.autoConnect) {
348
- await this.ws.connect().catch(() => {});
385
+ await this.ws.connect().catch(() => { });
349
386
  }
350
387
 
351
388
  const { input } = require('@inquirer/prompts');
@@ -390,7 +427,8 @@ setupWebSocketHandlers() {
390
427
  try {
391
428
  result = await this.api._request('POST', '/config/verify-config', {
392
429
  fileBase64: cfg.fileBase64,
393
- roomId: cfg.roomId,
430
+ roomId: cfg.roomId,
431
+ machineId: os.hostname(),
394
432
  });
395
433
  } catch (err) {
396
434
  console.log(chalk.red(`❌ tncproject auth failed: ${err.message}`));
@@ -401,11 +439,12 @@ setupWebSocketHandlers() {
401
439
  // Save session exactly like manual login
402
440
  this.api._saveSession({
403
441
  token: result.shellToken,
442
+ shellSessionId: result.shellSessionId || null,
404
443
  user: {
405
- _id: result._id.toString(),
406
- userId: result._id.toString(),
407
- email: result.email,
408
- name: result.name,
444
+ _id: result._id.toString(),
445
+ userId: result._id.toString(),
446
+ email: result.email,
447
+ name: result.name,
409
448
  userType: 'User',
410
449
  },
411
450
  timestamp: new Date().toISOString(),
@@ -434,44 +473,44 @@ setupWebSocketHandlers() {
434
473
  return true;
435
474
  }
436
475
 
437
- async execute(input) {
438
- if (!input.trim()) return;
476
+ async execute(input) {
477
+ if (!input.trim()) return;
439
478
 
440
- const args = this.parseCommand(input);
441
- const cmdName = args[0].toLowerCase();
442
- const cmdArgs = args.slice(1);
479
+ const args = this.parseCommand(input);
480
+ const cmdName = args[0].toLowerCase();
481
+ const cmdArgs = args.slice(1);
443
482
 
444
- // Skip sensitive commands
445
- const skipLog = ['login', 'logout', 'set', 'unset'];
446
- if (!skipLog.includes(cmdName)) {
447
- await this.api.logEvent('command', null, { cmd: cmdName });
448
- }
483
+ // Skip sensitive commands
484
+ const skipLog = ['login', 'logout', 'set', 'unset'];
485
+ if (!skipLog.includes(cmdName)) {
486
+ await this.api.logEvent('command', null, { cmd: cmdName });
487
+ }
449
488
 
450
- if (this.commands.has(cmdName)) {
451
- try { await this.commands.get(cmdName).handler(cmdArgs, this); }
452
- catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
453
- process.stdout.write('\r\n');
454
- return;
455
- }
489
+ if (this.commands.has(cmdName)) {
490
+ try { await this.commands.get(cmdName).handler(cmdArgs, this); }
491
+ catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
492
+ process.stdout.write('\r\n');
493
+ return;
494
+ }
495
+
496
+ if (this.commandRegistry) {
497
+ const handled = await this.commandRegistry.execute(input, this);
498
+ if (handled) {
499
+ process.stdout.write('\r\n');
500
+ return;
501
+ }
502
+ }
456
503
 
457
- if (this.commandRegistry) {
458
- const handled = await this.commandRegistry.execute(input, this);
459
- if (handled) {
504
+ const blockedOnWindows = ['open', 'which', 'grep', 'touch', 'cat'];
505
+ if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
506
+ console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
507
+ console.log(chalk.dim(` Type 'help' to see available commands.`));
460
508
  process.stdout.write('\r\n');
461
509
  return;
462
510
  }
463
- }
464
511
 
465
- const blockedOnWindows = ['open', 'which', 'grep', 'touch', 'cat'];
466
- if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
467
- console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
468
- console.log(chalk.dim(` Type 'help' to see available commands.`));
469
- process.stdout.write('\r\n');
470
- return;
512
+ await this.executeSystemCommand(input);
471
513
  }
472
-
473
- await this.executeSystemCommand(input);
474
- }
475
514
  // ─── Prompt ───────────────────────────────────────────────────────────────
476
515
 
477
516
  getPromptText() {
@@ -481,7 +520,7 @@ async execute(input) {
481
520
  if (this.ws.isConnected?.()) p += '● ';
482
521
 
483
522
  let displayPath = this.currentDir;
484
- const homeDir = os.homedir();
523
+ const homeDir = os.homedir();
485
524
 
486
525
  if (process.platform === 'win32') {
487
526
  if (displayPath.toLowerCase() === homeDir.toLowerCase()) displayPath = '~';
@@ -495,7 +534,7 @@ async execute(input) {
495
534
  return `${this.username}@thinknsh:${displayPath} $`;
496
535
  }
497
536
 
498
- getPrompt() { return this.getPromptText(); }
537
+ getPrompt() { return this.getPromptText(); }
499
538
  printMessage(msg) { console.log(msg); }
500
539
 
501
540
  // ─── Built-ins ────────────────────────────────────────────────────────────
@@ -503,28 +542,28 @@ async execute(input) {
503
542
  registerBuiltinCommands() {
504
543
  const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
505
544
 
506
- r('help', this.helpCommand.bind(this), 'Show available commands');
507
- r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
508
- r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
509
- r('clear', () => console.clear(), 'Clear screen');
510
- r('history', this.historyCommand.bind(this), 'Show command history');
511
- r('cd', this.cdCommand.bind(this), 'Change directory');
512
- r('pwd', () => console.log(process.cwd()), 'Print working directory');
513
- r('ls', this.lsCommand.bind(this), 'List files');
514
- r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
515
- r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
516
- r('status', this.statusCommand.bind(this), 'Show connection status');
517
- r('join', this.joinCommand.bind(this), 'Join a room');
518
- r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
519
- r('rooms', this.roomsCommand.bind(this), 'List rooms');
520
- r('say', this.sayCommand.bind(this), 'Send a message');
521
- r('msg', this.sayCommand.bind(this), 'Send a message');
522
- r('set', this.setCommand.bind(this), 'Set a variable');
523
- r('unset', this.unsetCommand.bind(this), 'Unset a variable');
524
-
525
- this.addAlias('ll', 'ls -la');
526
- this.addAlias('..', 'cd ..');
527
- this.addAlias('~', 'cd ~');
545
+ r('help', this.helpCommand.bind(this), 'Show available commands');
546
+ r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
547
+ r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
548
+ r('clear', () => console.clear(), 'Clear screen');
549
+ r('history', this.historyCommand.bind(this), 'Show command history');
550
+ r('cd', this.cdCommand.bind(this), 'Change directory');
551
+ r('pwd', () => console.log(process.cwd()), 'Print working directory');
552
+ r('ls', this.lsCommand.bind(this), 'List files');
553
+ r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
554
+ r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
555
+ r('status', this.statusCommand.bind(this), 'Show connection status');
556
+ r('join', this.joinCommand.bind(this), 'Join a room');
557
+ r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
558
+ r('rooms', this.roomsCommand.bind(this), 'List rooms');
559
+ r('say', this.sayCommand.bind(this), 'Send a message');
560
+ r('msg', this.sayCommand.bind(this), 'Send a message');
561
+ r('set', this.setCommand.bind(this), 'Set a variable');
562
+ r('unset', this.unsetCommand.bind(this), 'Unset a variable');
563
+
564
+ this.addAlias('ll', 'ls -la');
565
+ this.addAlias('..', 'cd ..');
566
+ this.addAlias('~', 'cd ~');
528
567
  }
529
568
 
530
569
  registerCommand(name, handler, description = '') { this.commands.set(name, { handler, description }); }
@@ -543,7 +582,7 @@ async execute(input) {
543
582
  const target = args.join(' ').trim() || os.homedir();
544
583
  try {
545
584
  let newPath;
546
- if (target === '~') newPath = os.homedir();
585
+ if (target === '~') newPath = os.homedir();
547
586
  else if (target === '..') newPath = path.dirname(this.currentDir);
548
587
  else newPath = path.resolve(this.currentDir, target);
549
588
  newPath = path.normalize(newPath);
@@ -557,8 +596,8 @@ async execute(input) {
557
596
 
558
597
  async lsCommand(args) {
559
598
  try {
560
- const files = await fs.readdir(this.currentDir);
561
- const showAll = args.includes('-a');
599
+ const files = await fs.readdir(this.currentDir);
600
+ const showAll = args.includes('-a');
562
601
  const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
563
602
  const fileList = await Promise.all(filtered.map(async (file) => {
564
603
  const stat = await fs.stat(path.join(this.currentDir, file));
@@ -567,7 +606,7 @@ async execute(input) {
567
606
  fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
568
607
  } catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
569
608
  }
570
-
609
+ // tncproject auth failed: Could not read .tncproject — re-download it from the dashboard
571
610
  async connectCommand() {
572
611
  console.log(chalk.cyan('🔌 Connecting...'));
573
612
  try { await this.ws.connect(); }
@@ -647,13 +686,13 @@ async execute(input) {
647
686
  executeSystemCommand(command) {
648
687
  return new Promise((resolve) => {
649
688
  const child = spawn(command, [], {
650
- cwd: this.currentDir,
689
+ cwd: this.currentDir,
651
690
  shell: true,
652
691
  stdio: 'inherit'
653
692
  });
654
693
  child.on('close', (code) => {
655
694
  // Update currentDir in case command changed it (cd won't but others might) http://localhost:3001
656
- try { this.currentDir = process.cwd(); } catch {}
695
+ try { this.currentDir = process.cwd(); } catch { }
657
696
  process.stdout.write('\r\n');
658
697
  resolve();
659
698
  });
@@ -665,7 +704,7 @@ async execute(input) {
665
704
  }
666
705
 
667
706
  showWelcome() {
668
- try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
707
+ try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch { }
669
708
  console.log(chalk.cyan(`
670
709
  ╔══════════════════════════════════════════════════════╗
671
710
  ║ ThinkNCollab Shell v0.0.10 ║
@@ -677,19 +716,19 @@ async execute(input) {
677
716
  try {
678
717
  if (fs.existsSync(this.config.historyFile))
679
718
  this.history = fs.readFileSync(this.config.historyFile, 'utf8').split('\n').filter(l => l.trim());
680
- } catch {}
719
+ } catch { }
681
720
  }
682
721
 
683
722
  saveHistory() {
684
- try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
723
+ try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch { }
685
724
  }
686
725
 
687
- cleanup() {
688
- this.api.logout(); // session end + heartbeat stop + lastSeen update
689
- if (this.tcpServer) { try { this.tcpServer.close(); } catch {} }
690
- if (this.ws) this.ws.disconnect();
691
- this.saveHistory();
692
- }
726
+ cleanup() {
727
+ this.api.logout(); // session end + heartbeat stop + lastSeen update
728
+ if (this.tcpServer) { try { this.tcpServer.close(); } catch { } }
729
+ if (this.ws) this.ws.disconnect();
730
+ this.saveHistory();
731
+ }
693
732
  }
694
733
 
695
734
  module.exports = ThinkNCollabShell;