thinkncollab-shell 0.0.9 → 0.0.12

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,271 @@ 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: 'notification',
351
+ level: d.type,
352
+ title: d.title,
353
+ message: d.message,
354
+ taskTitle: d.taskTitle,
355
+ assignedBy: d.assignedBy,
356
+ });
320
357
  });
321
- });
322
358
 
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
- }
359
+ this.ws.on('error', (e) => {
360
+ console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
361
+ this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
362
+ });
363
+ }
328
364
 
329
365
  // ─── Start ────────────────────────────────────────────────────────────────
330
366
 
@@ -345,7 +381,7 @@ setupWebSocketHandlers() {
345
381
 
346
382
  // If no .tncproject but autoConnect is set, connect WS anyway
347
383
  if (!booted && this.config.websocket.autoConnect) {
348
- await this.ws.connect().catch(() => {});
384
+ await this.ws.connect().catch(() => { });
349
385
  }
350
386
 
351
387
  const { input } = require('@inquirer/prompts');
@@ -390,7 +426,8 @@ setupWebSocketHandlers() {
390
426
  try {
391
427
  result = await this.api._request('POST', '/config/verify-config', {
392
428
  fileBase64: cfg.fileBase64,
393
- roomId: cfg.roomId,
429
+ roomId: cfg.roomId,
430
+ machineId: os.hostname(),
394
431
  });
395
432
  } catch (err) {
396
433
  console.log(chalk.red(`❌ tncproject auth failed: ${err.message}`));
@@ -401,11 +438,12 @@ setupWebSocketHandlers() {
401
438
  // Save session exactly like manual login
402
439
  this.api._saveSession({
403
440
  token: result.shellToken,
441
+ shellSessionId: result.shellSessionId || null,
404
442
  user: {
405
- _id: result._id.toString(),
406
- userId: result._id.toString(),
407
- email: result.email,
408
- name: result.name,
443
+ _id: result._id.toString(),
444
+ userId: result._id.toString(),
445
+ email: result.email,
446
+ name: result.name,
409
447
  userType: 'User',
410
448
  },
411
449
  timestamp: new Date().toISOString(),
@@ -434,44 +472,44 @@ setupWebSocketHandlers() {
434
472
  return true;
435
473
  }
436
474
 
437
- async execute(input) {
438
- if (!input.trim()) return;
475
+ async execute(input) {
476
+ if (!input.trim()) return;
439
477
 
440
- const args = this.parseCommand(input);
441
- const cmdName = args[0].toLowerCase();
442
- const cmdArgs = args.slice(1);
478
+ const args = this.parseCommand(input);
479
+ const cmdName = args[0].toLowerCase();
480
+ const cmdArgs = args.slice(1);
443
481
 
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
- }
482
+ // Skip sensitive commands
483
+ const skipLog = ['login', 'logout', 'set', 'unset'];
484
+ if (!skipLog.includes(cmdName)) {
485
+ await this.api.logEvent('command', null, { cmd: cmdName });
486
+ }
449
487
 
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
- }
488
+ if (this.commands.has(cmdName)) {
489
+ try { await this.commands.get(cmdName).handler(cmdArgs, this); }
490
+ catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
491
+ process.stdout.write('\r\n');
492
+ return;
493
+ }
494
+
495
+ if (this.commandRegistry) {
496
+ const handled = await this.commandRegistry.execute(input, this);
497
+ if (handled) {
498
+ process.stdout.write('\r\n');
499
+ return;
500
+ }
501
+ }
456
502
 
457
- if (this.commandRegistry) {
458
- const handled = await this.commandRegistry.execute(input, this);
459
- if (handled) {
503
+ const blockedOnWindows = ['open', 'which', 'grep', 'touch', 'cat'];
504
+ if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
505
+ console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
506
+ console.log(chalk.dim(` Type 'help' to see available commands.`));
460
507
  process.stdout.write('\r\n');
461
508
  return;
462
509
  }
463
- }
464
510
 
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;
511
+ await this.executeSystemCommand(input);
471
512
  }
472
-
473
- await this.executeSystemCommand(input);
474
- }
475
513
  // ─── Prompt ───────────────────────────────────────────────────────────────
476
514
 
477
515
  getPromptText() {
@@ -481,7 +519,7 @@ async execute(input) {
481
519
  if (this.ws.isConnected?.()) p += '● ';
482
520
 
483
521
  let displayPath = this.currentDir;
484
- const homeDir = os.homedir();
522
+ const homeDir = os.homedir();
485
523
 
486
524
  if (process.platform === 'win32') {
487
525
  if (displayPath.toLowerCase() === homeDir.toLowerCase()) displayPath = '~';
@@ -495,7 +533,7 @@ async execute(input) {
495
533
  return `${this.username}@thinknsh:${displayPath} $`;
496
534
  }
497
535
 
498
- getPrompt() { return this.getPromptText(); }
536
+ getPrompt() { return this.getPromptText(); }
499
537
  printMessage(msg) { console.log(msg); }
500
538
 
501
539
  // ─── Built-ins ────────────────────────────────────────────────────────────
@@ -503,28 +541,28 @@ async execute(input) {
503
541
  registerBuiltinCommands() {
504
542
  const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
505
543
 
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 ~');
544
+ r('help', this.helpCommand.bind(this), 'Show available commands');
545
+ r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
546
+ r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
547
+ r('clear', () => console.clear(), 'Clear screen');
548
+ r('history', this.historyCommand.bind(this), 'Show command history');
549
+ r('cd', this.cdCommand.bind(this), 'Change directory');
550
+ r('pwd', () => console.log(process.cwd()), 'Print working directory');
551
+ r('ls', this.lsCommand.bind(this), 'List files');
552
+ r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
553
+ r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
554
+ r('status', this.statusCommand.bind(this), 'Show connection status');
555
+ r('join', this.joinCommand.bind(this), 'Join a room');
556
+ r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
557
+ r('rooms', this.roomsCommand.bind(this), 'List rooms');
558
+ r('say', this.sayCommand.bind(this), 'Send a message');
559
+ r('msg', this.sayCommand.bind(this), 'Send a message');
560
+ r('set', this.setCommand.bind(this), 'Set a variable');
561
+ r('unset', this.unsetCommand.bind(this), 'Unset a variable');
562
+
563
+ this.addAlias('ll', 'ls -la');
564
+ this.addAlias('..', 'cd ..');
565
+ this.addAlias('~', 'cd ~');
528
566
  }
529
567
 
530
568
  registerCommand(name, handler, description = '') { this.commands.set(name, { handler, description }); }
@@ -543,7 +581,7 @@ async execute(input) {
543
581
  const target = args.join(' ').trim() || os.homedir();
544
582
  try {
545
583
  let newPath;
546
- if (target === '~') newPath = os.homedir();
584
+ if (target === '~') newPath = os.homedir();
547
585
  else if (target === '..') newPath = path.dirname(this.currentDir);
548
586
  else newPath = path.resolve(this.currentDir, target);
549
587
  newPath = path.normalize(newPath);
@@ -557,8 +595,8 @@ async execute(input) {
557
595
 
558
596
  async lsCommand(args) {
559
597
  try {
560
- const files = await fs.readdir(this.currentDir);
561
- const showAll = args.includes('-a');
598
+ const files = await fs.readdir(this.currentDir);
599
+ const showAll = args.includes('-a');
562
600
  const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
563
601
  const fileList = await Promise.all(filtered.map(async (file) => {
564
602
  const stat = await fs.stat(path.join(this.currentDir, file));
@@ -567,7 +605,7 @@ async execute(input) {
567
605
  fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
568
606
  } catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
569
607
  }
570
-
608
+ // tncproject auth failed: Could not read .tncproject — re-download it from the dashboard
571
609
  async connectCommand() {
572
610
  console.log(chalk.cyan('🔌 Connecting...'));
573
611
  try { await this.ws.connect(); }
@@ -647,13 +685,13 @@ async execute(input) {
647
685
  executeSystemCommand(command) {
648
686
  return new Promise((resolve) => {
649
687
  const child = spawn(command, [], {
650
- cwd: this.currentDir,
688
+ cwd: this.currentDir,
651
689
  shell: true,
652
690
  stdio: 'inherit'
653
691
  });
654
692
  child.on('close', (code) => {
655
- // Update currentDir in case command changed it (cd won't but others might) https://thinkncollab.com
656
- try { this.currentDir = process.cwd(); } catch {}
693
+ // Update currentDir in case command changed it (cd won't but others might) http://localhost:3001
694
+ try { this.currentDir = process.cwd(); } catch { }
657
695
  process.stdout.write('\r\n');
658
696
  resolve();
659
697
  });
@@ -665,10 +703,10 @@ async execute(input) {
665
703
  }
666
704
 
667
705
  showWelcome() {
668
- try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
706
+ try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch { }
669
707
  console.log(chalk.cyan(`
670
708
  ╔══════════════════════════════════════════════════════╗
671
- ║ ThinkNCollab Shell v0.0.08 ║
709
+ ║ ThinkNCollab Shell v0.0.10 ║
672
710
  ║ Type 'help' for commands ║
673
711
  ╚══════════════════════════════════════════════════════╝`));
674
712
  }
@@ -677,19 +715,19 @@ async execute(input) {
677
715
  try {
678
716
  if (fs.existsSync(this.config.historyFile))
679
717
  this.history = fs.readFileSync(this.config.historyFile, 'utf8').split('\n').filter(l => l.trim());
680
- } catch {}
718
+ } catch { }
681
719
  }
682
720
 
683
721
  saveHistory() {
684
- try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
722
+ try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch { }
685
723
  }
686
724
 
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
- }
725
+ cleanup() {
726
+ this.api.logout(); // session end + heartbeat stop + lastSeen update
727
+ if (this.tcpServer) { try { this.tcpServer.close(); } catch { } }
728
+ if (this.ws) this.ws.disconnect();
729
+ this.saveHistory();
730
+ }
693
731
  }
694
732
 
695
733
  module.exports = ThinkNCollabShell;