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/Readme.md +392 -16
- package/package.json +1 -1
- package/src/commands/auth/login.js +58 -58
- package/src/commands/auth/register.js +52 -0
- package/src/commands/index.js +62 -43
- package/src/commands/room/join.js +119 -45
- package/src/commands/shell/notify.js +88 -0
- package/src/commands/tasks/complete.js +47 -0
- package/src/commands/tasks/create-task.js +101 -0
- package/src/commands/tasks/suggestTask.js +0 -0
- package/src/commands/tasks/task.js +75 -0
- package/src/commands/tasks/tasks.js +74 -0
- package/src/commands/terminal/share.js +252 -0
- package/src/core/api-client.js +104 -50
- package/src/core/notification-window.js +2 -1
- package/src/core/project-config.js +100 -0
- package/src/core/shell.js +222 -210
- package/src/core/websocket.js +166 -101
package/src/core/websocket.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WebSocket Manager for ThinkNCollab Shell
|
|
3
|
-
* Uses thinknsh: prefixed events for terminal-specific communication
|
|
2
|
+
* src/core/websocket.js — WebSocket Manager for ThinkNCollab Shell
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
const { io }
|
|
5
|
+
const { io } = require('socket.io-client');
|
|
7
6
|
const EventEmitter = require('events');
|
|
8
7
|
|
|
9
8
|
class WebSocketManager extends EventEmitter {
|
|
9
|
+
|
|
10
10
|
constructor(config = {}) {
|
|
11
11
|
super();
|
|
12
|
-
|
|
13
12
|
this.config = {
|
|
14
13
|
serverUrl: config.serverUrl || 'https://thinkncollab.com',
|
|
15
14
|
reconnectAttempts: config.reconnectAttempts || 5,
|
|
@@ -19,10 +18,13 @@ class WebSocketManager extends EventEmitter {
|
|
|
19
18
|
|
|
20
19
|
this.socket = null;
|
|
21
20
|
this.connected = false;
|
|
22
|
-
this.currentUser = null;
|
|
21
|
+
this.currentUser = null; // always { userId, name, userType }
|
|
23
22
|
this.roomId = null;
|
|
24
23
|
this.reconnectCount = 0;
|
|
25
24
|
this.messageQueue = [];
|
|
25
|
+
this._authToken = null;
|
|
26
|
+
this._handlersSetup = false;
|
|
27
|
+
this._serverReady = false;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
// ─── Connect ──────────────────────────────────────────────────────────────
|
|
@@ -30,31 +32,28 @@ class WebSocketManager extends EventEmitter {
|
|
|
30
32
|
connect(authToken = null) {
|
|
31
33
|
return new Promise((resolve, reject) => {
|
|
32
34
|
try {
|
|
33
|
-
|
|
35
|
+
if (authToken) this._authToken = authToken;
|
|
36
|
+
|
|
34
37
|
this.socket = io(this.config.serverUrl + '/thinknsh', {
|
|
35
38
|
transports: ['websocket', 'polling'],
|
|
36
39
|
reconnection: true,
|
|
37
40
|
reconnectionAttempts: this.config.reconnectAttempts,
|
|
38
41
|
reconnectionDelay: this.config.reconnectDelay,
|
|
39
|
-
auth:
|
|
42
|
+
auth: this._authToken ? { token: this._authToken } : {},
|
|
40
43
|
});
|
|
41
44
|
|
|
42
|
-
const timeout = setTimeout(() =>
|
|
45
|
+
const timeout = setTimeout(() => {
|
|
46
|
+
reject(new Error('Connection timeout'));
|
|
47
|
+
}, 10000);
|
|
43
48
|
|
|
44
49
|
this.socket.on('connect', () => {
|
|
45
50
|
clearTimeout(timeout);
|
|
46
51
|
this.connected = true;
|
|
47
52
|
this.reconnectCount = 0;
|
|
48
|
-
this._authToken = authToken;
|
|
49
53
|
|
|
50
|
-
// Announce
|
|
54
|
+
// Announce user immediately on connect if we have one
|
|
51
55
|
if (this.currentUser) {
|
|
52
|
-
|
|
53
|
-
this.socket.emit('thinknsh:connected', {
|
|
54
|
-
userId: this.currentUser.userId,
|
|
55
|
-
userName: this.currentUser.name,
|
|
56
|
-
shellToken: authToken,
|
|
57
|
-
});
|
|
56
|
+
this._announceUser();
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
this.processPendingMessages();
|
|
@@ -76,16 +75,18 @@ class WebSocketManager extends EventEmitter {
|
|
|
76
75
|
this.emit('disconnected', { reason });
|
|
77
76
|
});
|
|
78
77
|
|
|
79
|
-
this.socket.on('reconnect', (
|
|
78
|
+
this.socket.on('reconnect', () => {
|
|
80
79
|
this.connected = true;
|
|
81
|
-
this.emit('reconnected', {
|
|
82
|
-
|
|
83
|
-
if (this.
|
|
84
|
-
|
|
85
|
-
}
|
|
80
|
+
this.emit('reconnected', {});
|
|
81
|
+
|
|
82
|
+
if (this.currentUser) this._announceUser();
|
|
83
|
+
if (this.roomId && this.currentUser) this._emitJoin(this.roomId);
|
|
86
84
|
});
|
|
87
85
|
|
|
88
|
-
this.
|
|
86
|
+
if (!this._handlersSetup) {
|
|
87
|
+
this.setupEventHandlers();
|
|
88
|
+
this._handlersSetup = true;
|
|
89
|
+
}
|
|
89
90
|
|
|
90
91
|
} catch (error) {
|
|
91
92
|
reject(error);
|
|
@@ -93,33 +94,68 @@ class WebSocketManager extends EventEmitter {
|
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
// ─── thinknsh:
|
|
97
|
+
// ─── Announce user to server (thinknsh:connected) ─────────────────────────
|
|
98
|
+
|
|
99
|
+
_announceUser() {
|
|
100
|
+
if (!this.socket || !this.currentUser) return;
|
|
101
|
+
this.socket.emit('thinknsh:connected', {
|
|
102
|
+
userId: this.currentUser.userId,
|
|
103
|
+
userName: this.currentUser.name,
|
|
104
|
+
shellToken: this._authToken,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Set user — called right after login ──────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Accepts any of these shapes and normalises to { userId, name, userType }:
|
|
112
|
+
* { _id, userId, name, email, userType } ← from api-client.getUser()
|
|
113
|
+
* { userId, name, userType } ← legacy
|
|
114
|
+
*/
|
|
115
|
+
setUser(user, token = null) {
|
|
116
|
+
if (!user) return;
|
|
117
|
+
|
|
118
|
+
if (token) this._authToken = token;
|
|
119
|
+
|
|
120
|
+
// Normalise — support both _id and userId
|
|
121
|
+
const id = (user.userId || user._id || '').toString();
|
|
122
|
+
this.currentUser = {
|
|
123
|
+
userId: id,
|
|
124
|
+
name: user.name || user.email || 'Unknown',
|
|
125
|
+
userType: user.userType || user.model || user.role || 'User',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// If already connected, announce immediately
|
|
129
|
+
if (this.connected && this.socket) {
|
|
130
|
+
this._announceUser();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Event Handlers ───────────────────────────────────────────────────────
|
|
97
135
|
|
|
98
|
-
setupEventHandlers(
|
|
136
|
+
setupEventHandlers() {
|
|
99
137
|
|
|
100
|
-
// Terminal ready confirmed by server
|
|
101
138
|
this.socket.on('thinknsh:ready', (data) => {
|
|
139
|
+
this._serverReady = true;
|
|
102
140
|
this.emit('ready', data);
|
|
103
141
|
});
|
|
104
142
|
|
|
105
|
-
// Room join confirmed
|
|
106
143
|
this.socket.on('thinknsh:joined', (data) => {
|
|
107
144
|
this.roomId = data.roomId;
|
|
145
|
+
if (data.history?.length) this.emit('messageHistory', data.history);
|
|
108
146
|
this.emit('roomJoined', data);
|
|
109
147
|
});
|
|
110
148
|
|
|
111
|
-
// Incoming message from terminal or web
|
|
112
149
|
this.socket.on('thinknsh:message', (data) => {
|
|
113
150
|
this.emit('message', {
|
|
114
151
|
username: data.name,
|
|
115
152
|
userId: data.userId,
|
|
116
153
|
message: data.message,
|
|
117
|
-
source:
|
|
154
|
+
source: 'shell',
|
|
118
155
|
timestamp: data.timestamp,
|
|
119
156
|
});
|
|
120
157
|
});
|
|
121
158
|
|
|
122
|
-
// Also listen to standard chat-message (from web users)
|
|
123
159
|
this.socket.on('chat-message', (data) => {
|
|
124
160
|
this.emit('message', {
|
|
125
161
|
username: data.name,
|
|
@@ -130,60 +166,55 @@ class WebSocketManager extends EventEmitter {
|
|
|
130
166
|
});
|
|
131
167
|
});
|
|
132
168
|
|
|
133
|
-
// User joined from terminal
|
|
134
169
|
this.socket.on('thinknsh:userJoined', (data) => {
|
|
135
170
|
this.emit('userJoined', { username: data.name, source: 'shell' });
|
|
136
171
|
});
|
|
137
172
|
|
|
138
|
-
|
|
173
|
+
this.socket.on('thinknsh:userLeft', (data) => {
|
|
174
|
+
this.emit('userLeft', { username: data.name, source: 'shell' });
|
|
175
|
+
});
|
|
176
|
+
|
|
139
177
|
this.socket.on('user-joined', (name) => {
|
|
140
178
|
this.emit('userJoined', { username: name, source: 'web' });
|
|
141
179
|
});
|
|
142
180
|
|
|
143
|
-
// User left
|
|
144
181
|
this.socket.on('user-left', (name) => {
|
|
145
|
-
this.emit('userLeft', { username: name });
|
|
182
|
+
this.emit('userLeft', { username: name, source: 'web' });
|
|
146
183
|
});
|
|
147
184
|
|
|
148
|
-
// User list updated
|
|
149
185
|
this.socket.on('user-list', (users) => {
|
|
150
186
|
this.emit('userList', { users });
|
|
151
187
|
});
|
|
152
188
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.emit('notification', {
|
|
156
|
-
type: notification.type || 'info',
|
|
157
|
-
title: notification.type || 'Notification',
|
|
158
|
-
message: notification.message || '',
|
|
159
|
-
meta: notification.meta || {},
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// Terminal status (online/offline) — website sends this
|
|
164
|
-
this.socket.on('thinknsh:status', (data) => {
|
|
165
|
-
this.emit('shellStatus', data);
|
|
189
|
+
this.socket.on('thinknsh:history', (data) => {
|
|
190
|
+
this.emit('messageHistory', data.messages?.length ? data.messages : []);
|
|
166
191
|
});
|
|
167
192
|
|
|
168
|
-
// ── new-notification from NotificationEngine → forward to TCP window ──
|
|
169
193
|
this.socket.on('new-notification', (data) => {
|
|
170
|
-
// This goes to shell.js pushNotification → TCP → notification window
|
|
171
194
|
this.emit('notification', {
|
|
172
|
-
type:
|
|
173
|
-
level:
|
|
174
|
-
title:
|
|
175
|
-
message:
|
|
195
|
+
type: data.type || 'info',
|
|
196
|
+
level: data.type || 'info',
|
|
197
|
+
title: data.title || 'Notification',
|
|
198
|
+
message: data.message || '',
|
|
199
|
+
meta: data.meta || {},
|
|
176
200
|
taskTitle: data.taskTitle || data.meta?.taskTitle || '',
|
|
177
201
|
assignedBy: data.assignedBy || data.meta?.assignedBy || '',
|
|
178
202
|
});
|
|
179
203
|
});
|
|
180
204
|
|
|
181
|
-
|
|
205
|
+
this.socket.on('room:activity', (data) => {
|
|
206
|
+
this.emit('roomActivity', data);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
this.socket.on('thinknsh:status', (data) => {
|
|
210
|
+
this.emit('shellStatus', data);
|
|
211
|
+
});
|
|
212
|
+
|
|
182
213
|
this.socket.on('thinknsh:notifications', (data) => {
|
|
183
214
|
this.emit('notificationList', data.notifications);
|
|
184
215
|
});
|
|
185
216
|
|
|
186
|
-
//
|
|
217
|
+
// Server errors — emit as 'serverError' so shell.js can handle separately
|
|
187
218
|
this.socket.on('thinknsh:error', (data) => {
|
|
188
219
|
this.emit('serverError', data);
|
|
189
220
|
});
|
|
@@ -193,44 +224,64 @@ class WebSocketManager extends EventEmitter {
|
|
|
193
224
|
});
|
|
194
225
|
}
|
|
195
226
|
|
|
196
|
-
// ─── Set user (call after login) ──────────────────────────────────────────
|
|
197
|
-
|
|
198
|
-
setUser(user, token = null) {
|
|
199
|
-
this.currentUser = user;
|
|
200
|
-
// If already connected, announce now
|
|
201
|
-
if (this.connected && this.socket) {
|
|
202
|
-
this.socket.emit('thinknsh:connected', {
|
|
203
|
-
userId: user.userId,
|
|
204
|
-
shellToken: token,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
227
|
// ─── Join room ────────────────────────────────────────────────────────────
|
|
210
228
|
|
|
211
229
|
joinRoom(roomId) {
|
|
212
230
|
return new Promise((resolve, reject) => {
|
|
213
|
-
if (!this.connected || !this.socket)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
231
|
+
if (!this.connected || !this.socket)
|
|
232
|
+
return reject(new Error('Not connected to server'));
|
|
233
|
+
if (!this.currentUser)
|
|
234
|
+
return reject(new Error('Not logged in — run: login'));
|
|
235
|
+
|
|
236
|
+
const timeout = setTimeout(() => {
|
|
237
|
+
this.socket.off('thinknsh:joined', onJoined);
|
|
238
|
+
this.socket.off('thinknsh:error', onError);
|
|
239
|
+
this.socket.off('thinknsh:ready', onReady);
|
|
240
|
+
reject(new Error('Join timeout'));
|
|
241
|
+
}, 8000);
|
|
242
|
+
|
|
243
|
+
const onJoined = (data) => {
|
|
219
244
|
clearTimeout(timeout);
|
|
245
|
+
this.socket.off('thinknsh:error', onError);
|
|
220
246
|
this.roomId = data.roomId;
|
|
221
247
|
resolve(data);
|
|
222
|
-
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// onReady declared at top so onError can reference it regardless of branch
|
|
251
|
+
let onReady = null;
|
|
223
252
|
|
|
224
|
-
|
|
253
|
+
const onError = (data) => {
|
|
254
|
+
clearTimeout(timeout);
|
|
255
|
+
this.socket.off('thinknsh:joined', onJoined);
|
|
256
|
+
if (onReady) this.socket.off('thinknsh:ready', onReady);
|
|
257
|
+
reject(new Error(data.message || 'Join error'));
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const doJoin = () => {
|
|
261
|
+
this.socket.once('thinknsh:joined', onJoined);
|
|
262
|
+
this.socket.once('thinknsh:error', onError);
|
|
263
|
+
this._emitJoin(roomId);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
if (this._serverReady) {
|
|
267
|
+
doJoin();
|
|
268
|
+
} else {
|
|
269
|
+
onReady = () => {
|
|
270
|
+
this._serverReady = true;
|
|
271
|
+
doJoin();
|
|
272
|
+
};
|
|
273
|
+
this.socket.once('thinknsh:ready', onReady);
|
|
274
|
+
}
|
|
225
275
|
});
|
|
226
276
|
}
|
|
227
277
|
|
|
228
278
|
_emitJoin(roomId) {
|
|
279
|
+
if (!this.socket || !this.currentUser) return;
|
|
229
280
|
this.socket.emit('thinknsh:join', {
|
|
230
281
|
roomId,
|
|
231
282
|
userId: this.currentUser.userId,
|
|
232
283
|
name: this.currentUser.name,
|
|
233
|
-
userType: this.currentUser.userType
|
|
284
|
+
userType: this.currentUser.userType,
|
|
234
285
|
});
|
|
235
286
|
}
|
|
236
287
|
|
|
@@ -258,20 +309,43 @@ class WebSocketManager extends EventEmitter {
|
|
|
258
309
|
roomId: this.roomId,
|
|
259
310
|
userId: this.currentUser.userId,
|
|
260
311
|
name: this.currentUser.name,
|
|
261
|
-
userType: this.currentUser.userType
|
|
312
|
+
userType: this.currentUser.userType,
|
|
262
313
|
message,
|
|
263
314
|
});
|
|
264
315
|
|
|
265
316
|
return Promise.resolve({ success: true });
|
|
266
317
|
}
|
|
267
318
|
|
|
268
|
-
// ───
|
|
319
|
+
// ─── History ──────────────────────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
getHistory(limit = 30, skip = 0) {
|
|
322
|
+
if (!this.connected || !this.socket || !this.roomId) return;
|
|
323
|
+
this.socket.emit('thinknsh:getHistory', { roomId: this.roomId, limit, skip });
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ─── Notifications ────────────────────────────────────────────────────────
|
|
269
327
|
|
|
270
328
|
getNotifications() {
|
|
271
329
|
if (!this.connected || !this.currentUser) return;
|
|
272
330
|
this.socket.emit('thinknsh:getNotifications', { userId: this.currentUser.userId });
|
|
273
331
|
}
|
|
274
332
|
|
|
333
|
+
sendNotification(notification, target = 'room') {
|
|
334
|
+
if (!this.connected || !this.socket)
|
|
335
|
+
return Promise.reject(new Error('Not connected'));
|
|
336
|
+
|
|
337
|
+
this.socket.emit('notification:send', {
|
|
338
|
+
type: notification.type || 'info',
|
|
339
|
+
title: notification.title || 'Notification',
|
|
340
|
+
message: notification.message,
|
|
341
|
+
data: notification.data || {},
|
|
342
|
+
target: target === 'room' ? this.roomId : target,
|
|
343
|
+
timestamp: new Date().toISOString()
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return Promise.resolve({ success: true });
|
|
347
|
+
}
|
|
348
|
+
|
|
275
349
|
markNotificationsRead(notificationIds = []) {
|
|
276
350
|
if (!this.connected || !this.currentUser) return;
|
|
277
351
|
this.socket.emit('thinknsh:markRead', {
|
|
@@ -282,23 +356,8 @@ class WebSocketManager extends EventEmitter {
|
|
|
282
356
|
|
|
283
357
|
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
284
358
|
|
|
285
|
-
joinUserRoom(userId) {
|
|
286
|
-
if (!this.socket || !userId) return;
|
|
287
|
-
this.socket.emit('thinknsh:connected', {
|
|
288
|
-
userId,
|
|
289
|
-
userName: this.currentUser?.name || '',
|
|
290
|
-
shellToken: this._token || null,
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
sendTyping() {} // add later if needed
|
|
295
|
-
|
|
296
|
-
ping() {
|
|
297
|
-
if (this.connected && this.socket) this.socket.emit('ping');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
359
|
processPendingMessages() {
|
|
301
|
-
if (this.messageQueue.length
|
|
360
|
+
if (this.messageQueue.length && this.roomId && this.currentUser) {
|
|
302
361
|
this.messageQueue.forEach(msg => this.sendMessage(msg));
|
|
303
362
|
this.messageQueue = [];
|
|
304
363
|
}
|
|
@@ -306,18 +365,24 @@ class WebSocketManager extends EventEmitter {
|
|
|
306
365
|
|
|
307
366
|
disconnect() {
|
|
308
367
|
if (this.socket) {
|
|
368
|
+
this.socket.removeAllListeners();
|
|
309
369
|
this.socket.disconnect();
|
|
310
|
-
this.socket
|
|
311
|
-
this.connected
|
|
312
|
-
this.currentUser
|
|
313
|
-
this.roomId
|
|
370
|
+
this.socket = null;
|
|
371
|
+
this.connected = false;
|
|
372
|
+
this.currentUser = null;
|
|
373
|
+
this.roomId = null;
|
|
374
|
+
this._handlersSetup = false;
|
|
375
|
+
this._serverReady = false;
|
|
376
|
+
this._serverReady = false;
|
|
314
377
|
}
|
|
315
378
|
}
|
|
316
379
|
|
|
317
380
|
isConnected() { return this.connected && !!this.socket?.connected; }
|
|
318
381
|
getCurrentRoom() { return this.roomId; }
|
|
319
382
|
getSocketId() { return this.socket?.id || null; }
|
|
320
|
-
getParticipants()
|
|
383
|
+
getParticipants(){ return Promise.resolve([]); }
|
|
384
|
+
sendTyping() {}
|
|
385
|
+
ping() { if (this.connected && this.socket) this.socket.emit('ping'); }
|
|
321
386
|
}
|
|
322
387
|
|
|
323
388
|
module.exports = WebSocketManager;
|