thinkncollab-shell 0.0.20 → 2.0.0-beta.1

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.
Files changed (201) hide show
  1. package/LICENSE +82 -0
  2. package/Readme.md +246 -502
  3. package/dist/bin/thinknsh.js +8 -0
  4. package/dist/src/commands/auth/login.js +7 -0
  5. package/dist/src/commands/auth/logout.js +7 -0
  6. package/dist/src/commands/auth/register.js +7 -0
  7. package/dist/src/commands/auth/whoami.js +7 -0
  8. package/dist/src/commands/dock/bench.js +7 -0
  9. package/dist/src/commands/dock/ci.js +7 -0
  10. package/dist/src/commands/dock/cluster.js +7 -0
  11. package/dist/src/commands/dock/containers.js +7 -0
  12. package/dist/src/commands/dock/dock.js +7 -0
  13. package/dist/src/commands/github/commits.js +7 -0
  14. package/dist/src/commands/github/issue-create.js +7 -0
  15. package/dist/src/commands/github/issues.js +7 -0
  16. package/dist/src/commands/github/pulls.js +7 -0
  17. package/dist/src/commands/github/statusgh.js +7 -0
  18. package/dist/src/commands/index.js +7 -0
  19. package/dist/src/commands/message/say.js +7 -0
  20. package/dist/src/commands/room/join.js +7 -0
  21. package/dist/src/commands/room/leave.js +7 -0
  22. package/dist/src/commands/room/rooms.js +7 -0
  23. package/dist/src/commands/room/sync.js +7 -0
  24. package/dist/src/commands/sandbox/console.js +7 -0
  25. package/dist/src/commands/sandbox/dep.js +7 -0
  26. package/dist/src/commands/sandbox/deploy.js +7 -0
  27. package/dist/src/commands/sandbox/dev.js +7 -0
  28. package/dist/src/commands/sandbox/exec.js +7 -0
  29. package/dist/src/commands/sandbox/freeze.js +7 -0
  30. package/dist/src/commands/sandbox/push.js +7 -0
  31. package/dist/src/commands/sandbox/resume.js +7 -0
  32. package/dist/src/commands/sandbox/sandbox.js +7 -0
  33. package/dist/src/commands/sandbox/share.js +7 -0
  34. package/dist/src/commands/sandbox/snapshot.js +7 -0
  35. package/dist/src/commands/sandbox/sync-deps.js +7 -0
  36. package/dist/src/commands/shell/doctor.js +7 -0
  37. package/dist/src/commands/shell/notifications.js +7 -0
  38. package/dist/src/commands/shell/notify.js +7 -0
  39. package/dist/src/commands/shell/status.js +7 -0
  40. package/dist/src/commands/shell/version.js +7 -0
  41. package/dist/src/commands/tasks/build.js +7 -0
  42. package/dist/src/commands/tasks/complete.js +7 -0
  43. package/dist/src/commands/tasks/create-task.js +7 -0
  44. package/dist/src/commands/tasks/loadtest.js +7 -0
  45. package/dist/src/commands/tasks/task-status.js +7 -0
  46. package/dist/src/commands/tasks/task.js +7 -0
  47. package/dist/src/commands/tasks/tasks.js +7 -0
  48. package/dist/src/commands/team/invite.js +7 -0
  49. package/dist/src/commands/team/myteam.js +7 -0
  50. package/dist/src/commands/team/teammates.js +7 -0
  51. package/dist/src/commands/terminal/share.js +7 -0
  52. package/dist/src/core/api-client.js +7 -0
  53. package/dist/src/core/crypto/e2ee-vault.js +7 -0
  54. package/dist/src/core/dep-store.js +7 -0
  55. package/dist/src/core/dependency-tracker.js +7 -0
  56. package/dist/src/core/geekload.js +7 -0
  57. package/dist/src/core/local-runner.js +7 -0
  58. package/dist/src/core/multi-lang-scanner.js +7 -0
  59. package/dist/src/core/notification-window.js +8 -0
  60. package/dist/src/core/notify-auth.js +7 -0
  61. package/dist/src/core/project-config.js +7 -0
  62. package/dist/src/core/sandbox/arch-shim.js +7 -0
  63. package/dist/src/core/sandbox/capsule-builder.js +7 -0
  64. package/dist/src/core/sandbox/cgroups.js +7 -0
  65. package/dist/src/core/sandbox/container-monitor.js +7 -0
  66. package/dist/src/core/sandbox/crun-manager.js +7 -0
  67. package/dist/src/core/sandbox/engine.js +7 -0
  68. package/dist/src/core/sandbox/image-manager.js +7 -0
  69. package/dist/src/core/sandbox/image-store.js +7 -0
  70. package/dist/src/core/sandbox/index.js +7 -0
  71. package/dist/src/core/sandbox/macos-sandbox.js +7 -0
  72. package/dist/src/core/sandbox/microvm-manager.js +7 -0
  73. package/dist/src/core/sandbox/native-service-manager.js +7 -0
  74. package/dist/src/core/sandbox/network.js +7 -0
  75. package/dist/src/core/sandbox/oci-exporter.js +7 -0
  76. package/dist/src/core/sandbox/oci-puller.js +7 -0
  77. package/dist/src/core/sandbox/oci-runtime-spec.js +7 -0
  78. package/dist/src/core/sandbox/policy-engine.js +7 -0
  79. package/dist/src/core/sandbox/port-proxy-daemon.js +7 -0
  80. package/dist/src/core/sandbox/room-sync-manager.js +7 -0
  81. package/dist/src/core/sandbox/runtime-manager.js +7 -0
  82. package/dist/src/core/sandbox/snapshot-manager.js +7 -0
  83. package/dist/src/core/sandbox/storage.js +7 -0
  84. package/dist/src/core/sandbox/supervisor.js +7 -0
  85. package/dist/src/core/sandbox/tunnel-daemon.js +7 -0
  86. package/dist/src/core/sandbox/tunnel.js +7 -0
  87. package/dist/src/core/sandbox/window-launcher.js +7 -0
  88. package/dist/src/core/sandbox/windows-wsl2.js +7 -0
  89. package/dist/src/core/sandbox-window.js +8 -0
  90. package/dist/src/core/shell.js +7 -0
  91. package/dist/src/core/websocket.js +7 -0
  92. package/dist/src/dock/api/routes.js +7 -0
  93. package/dist/src/dock/api/server.js +7 -0
  94. package/dist/src/dock/api/wire.js +7 -0
  95. package/dist/src/dock/bench/memory.js +7 -0
  96. package/dist/src/dock/bench/runner.js +7 -0
  97. package/dist/src/dock/build/builder.js +7 -0
  98. package/dist/src/dock/build/buildfile.js +7 -0
  99. package/dist/src/dock/build/buildkit.js +7 -0
  100. package/dist/src/dock/build/changes.js +7 -0
  101. package/dist/src/dock/ci/actions.js +7 -0
  102. package/dist/src/dock/ci/runner.js +7 -0
  103. package/dist/src/dock/ci/workflow.js +7 -0
  104. package/dist/src/dock/cli/api.js +7 -0
  105. package/dist/src/dock/cli/args.js +7 -0
  106. package/dist/src/dock/cli/commands.js +7 -0
  107. package/dist/src/dock/cli/compose.js +7 -0
  108. package/dist/src/dock/cli/context.js +7 -0
  109. package/dist/src/dock/cli/files.js +7 -0
  110. package/dist/src/dock/cli/format.js +7 -0
  111. package/dist/src/dock/cli/manage.js +7 -0
  112. package/dist/src/dock/cli/moved.js +7 -0
  113. package/dist/src/dock/cli/services.js +7 -0
  114. package/dist/src/dock/cli/session.js +7 -0
  115. package/dist/src/dock/cli/workspace.js +7 -0
  116. package/dist/src/dock/client.js +7 -0
  117. package/dist/src/dock/compat/cases.js +7 -0
  118. package/dist/src/dock/compat/runner.js +7 -0
  119. package/dist/src/dock/config.js +7 -0
  120. package/dist/src/dock/daemon-main.js +7 -0
  121. package/dist/src/dock/daemon.js +7 -0
  122. package/dist/src/dock/deploy/compose.js +7 -0
  123. package/dist/src/dock/engine.js +7 -0
  124. package/dist/src/dock/image/archive.js +7 -0
  125. package/dist/src/dock/image/auth.js +7 -0
  126. package/dist/src/dock/image/reference.js +7 -0
  127. package/dist/src/dock/image/registry.js +7 -0
  128. package/dist/src/dock/image/tar.js +7 -0
  129. package/dist/src/dock/index.js +7 -0
  130. package/dist/src/dock/linux/host.js +7 -0
  131. package/dist/src/dock/paths.js +7 -0
  132. package/dist/src/dock/runtime/emulation.js +7 -0
  133. package/dist/src/dock/runtime/fsevents.js +7 -0
  134. package/dist/src/dock/runtime/fsops.js +7 -0
  135. package/dist/src/dock/runtime/hostfwd.js +7 -0
  136. package/dist/src/dock/runtime/logs.js +7 -0
  137. package/dist/src/dock/runtime/network.js +7 -0
  138. package/dist/src/dock/runtime/ports.js +7 -0
  139. package/dist/src/dock/runtime/rootfs.js +7 -0
  140. package/dist/src/dock/runtime/seccomp-profile.LICENSE +202 -0
  141. package/dist/src/dock/runtime/seccomp-profile.json +1227 -0
  142. package/dist/src/dock/runtime/seccomp.js +7 -0
  143. package/dist/src/dock/runtime/spec.js +7 -0
  144. package/dist/src/dock/runtime/stats.js +7 -0
  145. package/dist/src/dock/runtime/volumes.js +7 -0
  146. package/dist/src/dock/selftest.js +7 -0
  147. package/dist/src/dock/setup/app.js +7 -0
  148. package/dist/src/dock/setup/autostart.js +7 -0
  149. package/dist/src/dock/setup/index.js +7 -0
  150. package/dist/src/dock/setup/integrate.js +7 -0
  151. package/dist/src/dock/setup/migrate.js +7 -0
  152. package/dist/src/dock/state/store.js +7 -0
  153. package/dist/src/dock/ui/page.js +385 -0
  154. package/dist/src/dock/ui/server.js +7 -0
  155. package/dist/src/dock/version.js +7 -0
  156. package/dist/src/dock/workspace/apps.js +7 -0
  157. package/dist/src/dock/workspace/workspace.js +7 -0
  158. package/dist/src/events/notifications.js +7 -0
  159. package/dist/src/utils/logger.js +7 -0
  160. package/dist/src/utils/terminalCrypto.js +7 -0
  161. package/package.json +41 -16
  162. package/bin/thinknsh.js +0 -71
  163. package/src/commands/auth/login.js +0 -84
  164. package/src/commands/auth/logout.js +0 -32
  165. package/src/commands/auth/register.js +0 -52
  166. package/src/commands/auth/whoami.js +0 -52
  167. package/src/commands/github/commits.js +0 -58
  168. package/src/commands/github/issue-create.js +0 -59
  169. package/src/commands/github/issues.js +0 -57
  170. package/src/commands/github/pulls.js +0 -58
  171. package/src/commands/github/statusgh.js +0 -50
  172. package/src/commands/index.js +0 -136
  173. package/src/commands/message/say.js +0 -49
  174. package/src/commands/room/join.js +0 -142
  175. package/src/commands/room/leave.js +0 -24
  176. package/src/commands/room/rooms.js +0 -66
  177. package/src/commands/shell/notify.js +0 -88
  178. package/src/commands/shell/status.js +0 -73
  179. package/src/commands/shell/version.js +0 -18
  180. package/src/commands/tasks/build.js +0 -190
  181. package/src/commands/tasks/complete.js +0 -940
  182. package/src/commands/tasks/create-task.js +0 -101
  183. package/src/commands/tasks/loadtest.js +0 -101
  184. package/src/commands/tasks/suggestTask.js +0 -0
  185. package/src/commands/tasks/task-status.js +0 -72
  186. package/src/commands/tasks/task.js +0 -182
  187. package/src/commands/tasks/tasks.js +0 -184
  188. package/src/commands/team/invite.js +0 -46
  189. package/src/commands/team/myteam.js +0 -78
  190. package/src/commands/team/teammates.js +0 -52
  191. package/src/commands/terminal/share.js +0 -261
  192. package/src/core/api-client.js +0 -246
  193. package/src/core/geekload.js +0 -524
  194. package/src/core/local-runner.js +0 -861
  195. package/src/core/notification-window.js +0 -165
  196. package/src/core/project-config.js +0 -100
  197. package/src/core/shell.js +0 -734
  198. package/src/core/websocket.js +0 -657
  199. package/src/events/notifications.js +0 -184
  200. package/src/utils/logger.js +0 -159
  201. package/src/utils/terminalCrypto.js +0 -47
@@ -1,657 +0,0 @@
1
- /*
2
- src/core/websocket.js — WebSocket Manager for ThinkNCollab Shell
3
- */
4
-
5
- const { io } = require('socket.io-client');
6
- const EventEmitter = require('events');
7
-
8
- class WebSocketManager extends EventEmitter {
9
-
10
- constructor(config = {}) {
11
- super();
12
- this.config = {
13
- serverUrl: config.serverUrl || 'https://thinkncollab.com',
14
- reconnectAttempts: config.reconnectAttempts || 5,
15
- reconnectDelay: config.reconnectDelay || 1000,
16
- ...config
17
- };
18
-
19
- this.socket = null;
20
- this.connected = false;
21
- this.currentUser = null; // always { userId, name, userType }
22
- this.roomId = null;
23
- this.reconnectCount = 0;
24
- this.messageQueue = [];
25
- this._authToken = null;
26
- this._handlersSetup = false;
27
- this._serverReady = false;
28
- this._shellRef = config._shellRef || null; // ref to Shell instance for api access
29
- }
30
-
31
- // ─── Connect ──────────────────────────────────────────────────────────────
32
-
33
- connect(authToken = null) {
34
- return new Promise((resolve, reject) => {
35
- try {
36
- if (authToken) this._authToken = authToken;
37
-
38
- this.socket = io(this.config.serverUrl + '/thinknsh', {
39
- transports: ['websocket', 'polling'],
40
- reconnection: true,
41
- reconnectionAttempts: this.config.reconnectAttempts,
42
- reconnectionDelay: this.config.reconnectDelay,
43
- auth: this._authToken ? { token: this._authToken } : {},
44
- });
45
-
46
- const timeout = setTimeout(() => {
47
- reject(new Error('Connection timeout'));
48
- }, 10000);
49
-
50
- this.socket.on('connect', () => {
51
- clearTimeout(timeout);
52
- this.connected = true;
53
- this.reconnectCount = 0;
54
-
55
- // Announce user immediately on connect if we have one
56
- if (this.currentUser) {
57
- this._announceUser();
58
- }
59
-
60
- this.processPendingMessages();
61
- this.emit('connected', { socketId: this.socket.id });
62
- resolve({ success: true, socketId: this.socket.id });
63
- });
64
-
65
- this.socket.on('connect_error', (error) => {
66
- clearTimeout(timeout);
67
- if (this.reconnectCount === 0) {
68
- reject(new Error(`Cannot connect to ${this.config.serverUrl}`));
69
- }
70
- this.reconnectCount++;
71
- this.emit('error', { message: error.message });
72
- });
73
-
74
- this.socket.on('disconnect', (reason) => {
75
- this.connected = false;
76
- this.emit('disconnected', { reason });
77
- });
78
- this.socket.on('task:verdict', (data) => {
79
- this.emit('task:verdict', data);
80
- });
81
-
82
- // Triggered by TNC server when user clicks "run test" on browser (production)
83
- // CLI reads .tnc/workflows/*.yml from process.cwd() (client's project dir) and runs it locally
84
- this.socket.on('task:run-local', async (data) => {
85
- try {
86
- const chalk = require('chalk');
87
- const { runLocal } = require('./local-runner');
88
- const taskId = data.taskId;
89
- const shellRef = this._shellRef;
90
- const api = shellRef ? shellRef.api : null;
91
-
92
- if (!api) {
93
- console.log(chalk.red('\n❌ CLI not authenticated. Run: login\n'));
94
- return;
95
- }
96
-
97
- // Fetch task info (webhookSecret) from TNC API
98
- let task;
99
- try {
100
- task = await api._request('GET', `/thinknsh/${taskId}/cli`);
101
- } catch (e) {
102
- console.log(chalk.red(`\n❌ Could not fetch task info: ${e.message}\n`));
103
- return;
104
- }
105
-
106
- if (!task || !task.webhookSecret) {
107
- console.log(chalk.red('\n❌ Task has no webhookSecret — cannot submit result.\n'));
108
- return;
109
- }
110
-
111
- await runLocal({
112
- taskId,
113
- webhookSecret: task.webhookSecret,
114
- apiUrl: this.config.serverUrl || 'https://thinkncollab.com',
115
- // Pass repo info from TNC task — runner will clone this exact repo
116
- repoUrl: task.repoUrl || task.githubUrl || task.repo_url || null,
117
- repoBranch: task.branch || task.repoBranch || 'main',
118
- workflowFile: task.workflowFile || task.workflow || null,
119
- chalk,
120
- shell: shellRef,
121
- onLog: (logData) => {
122
- // Emit log stream to server via the terminal socket
123
- this.socket.emit('task:log', {
124
- taskId,
125
- boardId: task.boardId,
126
- ...logData
127
- });
128
- }
129
- });
130
-
131
- } catch (err) {
132
- console.error('task:run-local error:', err.message);
133
- }
134
- });
135
-
136
- this.socket.on('reconnect', () => {
137
- this.connected = true;
138
- this.emit('reconnected', {});
139
-
140
- if (this.currentUser) this._announceUser();
141
- if (this.roomId && this.currentUser) this._emitJoin(this.roomId);
142
- });
143
-
144
- if (!this._handlersSetup) {
145
- this.setupEventHandlers();
146
- this._handlersSetup = true;
147
- }
148
-
149
- } catch (error) {
150
- reject(error);
151
- }
152
- });
153
- }
154
-
155
- // ─── Announce user to server (thinknsh:connected) ─────────────────────────
156
-
157
- _announceUser() {
158
- if (!this.socket || !this.currentUser) return;
159
- this.socket.emit('thinknsh:connected', {
160
- userId: this.currentUser.userId,
161
- userName: this.currentUser.name,
162
- shellToken: this._authToken,
163
- });
164
- }
165
-
166
- // ─── Set user — called right after login ──────────────────────────────────
167
-
168
- /**
169
- * Accepts any of these shapes and normalises to { userId, name, userType }:
170
- * { _id, userId, name, email, userType } ← from api-client.getUser()
171
- * { userId, name, userType } ← legacy
172
- */
173
- setUser(user, token = null) {
174
- this._user = user;
175
- if (!user) return;
176
-
177
- if (token) this._authToken = token;
178
-
179
- // Normalise — support both _id and userId
180
- const id = (user.userId || user._id || '').toString();
181
- this.currentUser = {
182
- userId: id,
183
- name: user.name || user.email || 'Unknown',
184
- userType: user.userType || user.model || user.role || 'User',
185
- };
186
-
187
- // If already connected, announce immediately
188
- if (this.connected && this.socket) {
189
- this._announceUser();
190
- }
191
- }
192
-
193
- // ─── Event Handlers ───────────────────────────────────────────────────────
194
-
195
- setupEventHandlers() {
196
-
197
- this.socket.on('thinknsh:ready', (data) => {
198
- this._serverReady = true;
199
- this.emit('ready', data);
200
- });
201
-
202
- this.socket.on('thinknsh:joined', (data) => {
203
- this.roomId = data.roomId;
204
- if (data.history?.length) this.emit('messageHistory', data.history);
205
- this.emit('roomJoined', data);
206
- });
207
-
208
- this.socket.on('thinknsh:message', (data) => {
209
- this.emit('message', {
210
- username: data.name,
211
- userId: data.userId,
212
- message: data.message,
213
- source: 'shell',
214
- timestamp: data.timestamp,
215
- });
216
- });
217
-
218
- this.socket.on('chat-message', (data) => {
219
- this.emit('message', {
220
- username: data.name,
221
- userId: data.userId,
222
- message: data.message,
223
- source: data.source || 'web',
224
- timestamp: data.timestamp,
225
- });
226
- });
227
-
228
- this.socket.on('thinknsh:userJoined', (data) => {
229
- this.emit('userJoined', { username: data.name, source: 'shell' });
230
- });
231
-
232
- this.socket.on('thinknsh:userLeft', (data) => {
233
- this.emit('userLeft', { username: data.name, source: 'shell' });
234
- });
235
-
236
- this.socket.on('user-joined', (name) => {
237
- this.emit('userJoined', { username: name, source: 'web' });
238
- });
239
-
240
- this.socket.on('user-left', (name) => {
241
- this.emit('userLeft', { username: name, source: 'web' });
242
- });
243
-
244
- this.socket.on('user-list', (users) => {
245
- this.emit('userList', { users });
246
- });
247
-
248
- this.socket.on('thinknsh:history', (data) => {
249
- this.emit('messageHistory', data.messages?.length ? data.messages : []);
250
- });
251
-
252
- this.socket.on('new-notification', (data) => {
253
- this.emit('notification', {
254
- type: data.type || 'info',
255
- level: data.type || 'info',
256
- title: data.title || 'Notification',
257
- message: data.message || '',
258
- meta: data.meta || {},
259
- taskTitle: data.taskTitle || data.meta?.taskTitle || '',
260
- assignedBy: data.assignedBy || data.meta?.assignedBy || '',
261
- });
262
- });
263
-
264
- this.socket.on('room:activity', (data) => {
265
- this.emit('roomActivity', data);
266
- });
267
-
268
- this.socket.on('thinknsh:status', (data) => {
269
- this.emit('shellStatus', data);
270
- });
271
-
272
- this.socket.on('thinknsh:notifications', (data) => {
273
- this.emit('notificationList', data.notifications);
274
- });
275
-
276
- // Server errors — emit as 'serverError' so shell.js can handle separately
277
- this.socket.on('thinknsh:error', (data) => {
278
- this.emit('serverError', data);
279
- });
280
-
281
- this.socket.on('error', (data) => {
282
- this.emit('serverError', data);
283
- });
284
-
285
- // Triggered by TNC server when user clicks "Run in Browser" or triggers CI/CD pipeline
286
- const activeRunIds = new Set();
287
- const handleCicdRun = async (data) => {
288
- if (!data || !data.runId) return;
289
- if (activeRunIds.has(data.runId)) return;
290
- activeRunIds.add(data.runId);
291
-
292
- try {
293
- const chalk = require('chalk');
294
- const { runCicdLocal } = require('./local-runner');
295
- const shellRef = this._shellRef;
296
- const api = shellRef ? shellRef.api : null;
297
-
298
- console.log(chalk.cyan(`\n🚀 [thinkncollab-shell] CI/CD execution request received for room: ${data.roomId || this.roomId}`));
299
-
300
- await runCicdLocal({
301
- runId: data.runId,
302
- roomId: data.roomId || this.roomId,
303
- workflowFile: data.workflowFile || 'universal-polyglot-ci.yml',
304
- workflowYaml: data.workflowYaml || '',
305
- localPath: data.localPath || '',
306
- apiUrl: api ? api.baseURL : this.config.serverUrl,
307
- chalk,
308
- api,
309
- socket: this.socket
310
- });
311
- } catch (err) {
312
- console.error('\n❌ Error in CI/CD local execution:', err);
313
- } finally {
314
- setTimeout(() => activeRunIds.delete(data.runId), 30000);
315
- }
316
- };
317
-
318
- this.socket.on('cicd:run-dispatch', handleCicdRun);
319
-
320
- // ── Task Test Dispatch — async local execution ────────────────────────
321
- // Server sends this when GitHub webhook fires and assigned user's shell is active,
322
- // OR when user connects and there are waiting_for_shell TestRuns queued.
323
- // Test runs fully async using spawn — never blocks the shell.
324
- const activeTestRunIds = new Set();
325
-
326
- const handleTestDispatch = async (data) => {
327
- if (!data || !data.runId || !data.taskId) return;
328
- if (activeTestRunIds.has(data.runId)) return;
329
- activeTestRunIds.add(data.runId);
330
-
331
- try {
332
- const chalk = require('chalk');
333
- const { spawn } = require('child_process');
334
- const http = require('http');
335
- const https = require('https');
336
- const crypto = require('crypto');
337
-
338
- const encryptPayload = (dataObj, keySecret) => {
339
- if (!keySecret) return null;
340
- const key = crypto.createHash('sha256').update(keySecret).digest();
341
- const iv = crypto.randomBytes(12);
342
- const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
343
- const json = JSON.stringify(dataObj || {});
344
- const encrypted = Buffer.concat([cipher.update(json, 'utf8'), cipher.final()]);
345
- const tag = cipher.getAuthTag();
346
- return { iv: iv.toString('hex'), tag: tag.toString('hex'), cipherText: encrypted.toString('hex') };
347
- };
348
-
349
- const decryptPayload = (encryptedObj, keySecret) => {
350
- if (!encryptedObj || !encryptedObj.cipherText || !encryptedObj.iv || !encryptedObj.tag) return null;
351
- try {
352
- const key = crypto.createHash('sha256').update(keySecret).digest();
353
- const iv = Buffer.from(encryptedObj.iv, 'hex');
354
- const tag = Buffer.from(encryptedObj.tag, 'hex');
355
- const encryptedText = Buffer.from(encryptedObj.cipherText, 'hex');
356
- const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
357
- decipher.setAuthTag(tag);
358
- const decrypted = Buffer.concat([decipher.update(encryptedText), decipher.final()]);
359
- return JSON.parse(decrypted.toString('utf8'));
360
- } catch (_) { return null; }
361
- };
362
-
363
- const { runId, taskId, runResultToken, encryptedPayload } = data;
364
- let testConfig = data.testConfig;
365
- let commit = data.commit;
366
- let taskTitle = data.taskTitle;
367
-
368
- // Decrypt payload using AES-256-GCM if encrypted
369
- if (encryptedPayload && runResultToken) {
370
- const decrypted = decryptPayload(encryptedPayload, runResultToken);
371
- if (decrypted) {
372
- testConfig = decrypted.testConfig || testConfig;
373
- commit = decrypted.commit || commit;
374
- taskTitle = decrypted.taskTitle || taskTitle;
375
- }
376
- }
377
-
378
- console.log(chalk.cyan(`\n🧪 [TNC TestRunner - AES-256-GCM Secure] Task: "${taskTitle}"`));
379
- console.log(chalk.gray(` Run ID : ${runId}`));
380
- console.log(chalk.gray(` Type : ${testConfig?.type}`));
381
- console.log(chalk.gray(` Commit : ${commit?.sha?.slice(0, 7) || 'N/A'} — ${commit?.message || ''}`));
382
-
383
- let result = { exitCode: 1, stdout: '', stderr: '', status: null, body: null, url: null };
384
-
385
- if (testConfig.type === 'exitCode' || testConfig.type === 'stdout' || testConfig.type === 'regex') {
386
- // ── Async shell command via spawn ─────────────────────────
387
- const command = testConfig.command || testConfig.script;
388
- if (!command) {
389
- result.stderr = 'No command specified in testConfig';
390
- } else {
391
- console.log(chalk.yellow(` Running : ${command}`));
392
- result = await new Promise((resolve) => {
393
- const proc = spawn(command, [], {
394
- shell: true,
395
- cwd: process.cwd(),
396
- timeout: 60000
397
- });
398
-
399
- let stdout = '';
400
- let stderr = '';
401
-
402
- proc.stdout.on('data', (chunk) => {
403
- stdout += chunk.toString();
404
- process.stdout.write(chalk.gray(' │ ') + chunk.toString());
405
- });
406
- proc.stderr.on('data', (chunk) => {
407
- stderr += chunk.toString();
408
- process.stderr.write(chalk.red(' │ ') + chunk.toString());
409
- });
410
-
411
- proc.on('close', (code) => {
412
- resolve({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim(), status: null, body: null, url: null });
413
- });
414
-
415
- proc.on('error', (err) => {
416
- resolve({ exitCode: 1, stdout: '', stderr: err.message, status: null, body: null, url: null });
417
- });
418
- });
419
- console.log(result.exitCode === 0
420
- ? chalk.green(` ✅ Exit 0`)
421
- : chalk.red(` ❌ Exit ${result.exitCode} — ${result.stderr.slice(0, 100)}`));
422
- }
423
-
424
- } else if (testConfig.type === 'http') {
425
- // ── Async HTTP test ───────────────────────────────────────
426
- const endpoint = testConfig.endpoint || testConfig.url;
427
- const method = (testConfig.method || 'GET').toUpperCase();
428
- console.log(chalk.yellow(` Hitting : ${method} ${endpoint}`));
429
-
430
- result = await new Promise((resolve) => {
431
- const lib = endpoint?.startsWith('https') ? https : http;
432
- const url = new URL(endpoint);
433
- const options = { hostname: url.hostname, port: url.port || (url.protocol === 'https:' ? 443 : 80), path: url.pathname + url.search, method };
434
-
435
- const req = lib.request(options, (res) => {
436
- let body = '';
437
- res.on('data', chunk => body += chunk.toString());
438
- res.on('end', () => {
439
- let parsed = body;
440
- try { parsed = JSON.parse(body); } catch (_) {}
441
- console.log(chalk.green(` ✅ HTTP ${res.statusCode}`));
442
- resolve({ exitCode: 0, stdout: body.slice(0, 500), stderr: '', status: res.statusCode, body: parsed, url: endpoint });
443
- });
444
- });
445
-
446
- req.on('error', (err) => {
447
- console.log(chalk.red(` ❌ HTTP error: ${err.message}`));
448
- resolve({ exitCode: 1, stdout: '', stderr: err.message, status: null, body: null, url: endpoint });
449
- });
450
-
451
- req.setTimeout(15000, () => {
452
- req.destroy();
453
- resolve({ exitCode: 1, stdout: '', stderr: 'Request timeout (15s)', status: null, body: null, url: endpoint });
454
- });
455
-
456
- req.end();
457
- });
458
-
459
- } else {
460
- // ── Fallback: run testConfig.command if available ─────────
461
- const command = testConfig.command || testConfig.script;
462
- if (command) {
463
- result = await new Promise((resolve) => {
464
- const proc = spawn(command, [], { shell: true, cwd: process.cwd(), timeout: 60000 });
465
- let stdout = '', stderr = '';
466
- proc.stdout.on('data', c => stdout += c.toString());
467
- proc.stderr.on('data', c => stderr += c.toString());
468
- proc.on('close', code => resolve({ exitCode: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim(), status: null, body: null, url: null }));
469
- proc.on('error', err => resolve({ exitCode: 1, stdout: '', stderr: err.message, status: null, body: null, url: null }));
470
- });
471
- } else {
472
- result.stderr = `Unsupported testConfig type: ${testConfig.type}`;
473
- }
474
- }
475
-
476
- // ── AES-256-GCM Encrypt result back to server ─────────────────
477
- const encryptedResult = runResultToken ? encryptPayload(result, runResultToken) : null;
478
- const resultPayload = { runId, taskId, runResultToken, encryptedResult, ...result };
479
-
480
- // Primary: via socket (instant, zero overhead)
481
- this.socket.emit('thinknsh:test-result', resultPayload);
482
- console.log(chalk.cyan(` 🔒 [AES-256-GCM] Encrypted result dispatched to TNC server.\n`));
483
-
484
- } catch (err) {
485
- console.error('thinknsh:test-dispatch error:', err.message);
486
- } finally {
487
- // Clean up after 60s so same runId can be retried if needed
488
- setTimeout(() => activeTestRunIds.delete(data.runId), 60000);
489
- }
490
- };
491
-
492
- this.socket.on('thinknsh:test-dispatch', handleTestDispatch);
493
- }
494
-
495
-
496
- // ─── Join room ────────────────────────────────────────────────────────────
497
-
498
- joinRoom(roomId) {
499
- return new Promise((resolve, reject) => {
500
- if (!this.connected || !this.socket)
501
- return reject(new Error('Not connected to server'));
502
- if (!this.currentUser)
503
- return reject(new Error('Not logged in — run: login'));
504
-
505
- const timeout = setTimeout(() => {
506
- this.socket.off('thinknsh:joined', onJoined);
507
- this.socket.off('thinknsh:error', onError);
508
- this.socket.off('thinknsh:ready', onReady);
509
- reject(new Error('Join timeout'));
510
- }, 8000);
511
-
512
- const onJoined = (data) => {
513
- clearTimeout(timeout);
514
- this.socket.off('thinknsh:error', onError);
515
- this.roomId = data.roomId;
516
- resolve(data);
517
- };
518
-
519
- // onReady declared at top so onError can reference it regardless of branch
520
- let onReady = null;
521
-
522
- const onError = (data) => {
523
- clearTimeout(timeout);
524
- this.socket.off('thinknsh:joined', onJoined);
525
- if (onReady) this.socket.off('thinknsh:ready', onReady);
526
- reject(new Error(data.message || 'Join error'));
527
- };
528
-
529
- const doJoin = () => {
530
- this.socket.once('thinknsh:joined', onJoined);
531
- this.socket.once('thinknsh:error', onError);
532
- this._emitJoin(roomId);
533
- };
534
-
535
- if (this._serverReady) {
536
- doJoin();
537
- } else {
538
- onReady = () => {
539
- this._serverReady = true;
540
- doJoin();
541
- };
542
- this.socket.once('thinknsh:ready', onReady);
543
- }
544
- });
545
- }
546
-
547
- _emitJoin(roomId) {
548
- if (!this.socket || !this.currentUser) return;
549
- this.socket.emit('thinknsh:join', {
550
- roomId,
551
- userId: this.currentUser.userId,
552
- name: this.currentUser.name,
553
- userType: this.currentUser.userType,
554
- });
555
- }
556
-
557
- // ─── Leave room ───────────────────────────────────────────────────────────
558
-
559
- leaveRoom() {
560
- if (this.socket && this.roomId) {
561
- this.socket.emit('leave-room', { roomId: this.roomId });
562
- this.roomId = null;
563
- }
564
- return Promise.resolve({ success: true });
565
- }
566
-
567
- sendMessage(message, replyTo = null) {
568
- return new Promise((resolve, reject) => {
569
- if (!this.socket?.connected) return reject(new Error('Not connected'));
570
-
571
- const roomId = this.roomId;
572
- if (!roomId) return reject(new Error('Not in any room. Use: join <room-id>'));
573
-
574
- if (!this.currentUser) return reject(new Error('Not authenticated. Run: login'));
575
-
576
- this.socket.emit('thinknsh:message', {
577
- roomId,
578
- userId: this.currentUser.userId,
579
- name: this.currentUser.name,
580
- userType: this.currentUser.userType || 'User',
581
- message,
582
- replyTo: replyTo || null
583
- });
584
-
585
- resolve();
586
- });
587
- }
588
- // ─── History ──────────────────────────────────────────────────────────────
589
-
590
- getHistory(limit = 30, skip = 0) {
591
- if (!this.connected || !this.socket || !this.roomId) return;
592
- this.socket.emit('thinknsh:getHistory', { roomId: this.roomId, limit, skip });
593
- }
594
-
595
- // ─── Notifications ────────────────────────────────────────────────────────
596
-
597
- getNotifications() {
598
- if (!this.connected || !this.currentUser) return;
599
- this.socket.emit('thinknsh:getNotifications', { userId: this.currentUser.userId });
600
- }
601
-
602
- sendNotification(notification, target = 'room') {
603
- if (!this.connected || !this.socket)
604
- return Promise.reject(new Error('Not connected'));
605
-
606
- this.socket.emit('notification:send', {
607
- type: notification.type || 'info',
608
- title: notification.title || 'Notification',
609
- message: notification.message,
610
- data: notification.data || {},
611
- target: target === 'room' ? this.roomId : target,
612
- timestamp: new Date().toISOString()
613
- });
614
-
615
- return Promise.resolve({ success: true });
616
- }
617
-
618
- markNotificationsRead(notificationIds = []) {
619
- if (!this.connected || !this.currentUser) return;
620
- this.socket.emit('thinknsh:markRead', {
621
- userId: this.currentUser.userId,
622
- notificationIds,
623
- });
624
- }
625
-
626
- // ─── Helpers ──────────────────────────────────────────────────────────────
627
-
628
- processPendingMessages() {
629
- if (this.messageQueue.length && this.roomId && this.currentUser) {
630
- this.messageQueue.forEach(msg => this.sendMessage(msg));
631
- this.messageQueue = [];
632
- }
633
- }
634
-
635
- disconnect() {
636
- if (this.socket) {
637
- this.socket.removeAllListeners();
638
- this.socket.disconnect();
639
- this.socket = null;
640
- this.connected = false;
641
- this.currentUser = null;
642
- this.roomId = null;
643
- this._handlersSetup = false;
644
- this._serverReady = false;
645
- this._serverReady = false;
646
- }
647
- }
648
-
649
- isConnected() { return this.connected && !!this.socket?.connected; }
650
- getCurrentRoom() { return this.roomId; }
651
- getSocketId() { return this.socket?.id || null; }
652
- getParticipants(){ return Promise.resolve([]); }
653
- sendTyping() {}
654
- ping() { if (this.connected && this.socket) this.socket.emit('ping'); }
655
- }
656
-
657
- module.exports = WebSocketManager;