thinkncollab-shell 0.0.19 → 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 -482
  199. package/src/events/notifications.js +0 -184
  200. package/src/utils/logger.js +0 -159
  201. package/src/utils/terminalCrypto.js +0 -47
package/src/core/shell.js DELETED
@@ -1,734 +0,0 @@
1
- /**
2
- * src/core/shell.js — ThinkNCollab Shell Core sound
3
- */
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');
10
- const { spawn } = require('child_process');
11
- const EventEmitter = require('events');
12
- const WebSocketManager = require('./websocket');
13
- const CommandRegistry = require('../commands');
14
- const ThinkNCollabAPI = require('./api-client');
15
- const { loadProjectConfig, hasProjectConfig } = require('./project-config');
16
-
17
- function getAvailablePort(startPort) {
18
- return new Promise((resolve) => {
19
- const server = net.createServer();
20
- server.listen(startPort, '127.0.0.1', () => {
21
- const port = server.address().port;
22
- server.close(() => resolve(port));
23
- });
24
- server.on('error', () => resolve(getAvailablePort(startPort + 1)));
25
- });
26
- }
27
-
28
- class ThinkNCollabShell extends EventEmitter {
29
-
30
- constructor(options = {}) {
31
- super();
32
-
33
- this.config = {
34
- historyFile: path.join(os.homedir(), '.thinknsh_history'),
35
- configDir: path.join(os.homedir(), '.thinkncollab'),
36
- maxHistory: 1000,
37
- websocket: {
38
- serverUrl: options.serverUrl || 'https://thinkncollab.com',
39
- autoConnect: options.autoConnect || false
40
- },
41
- ...options
42
- };
43
-
44
- fs.ensureDirSync(this.config.configDir);
45
-
46
- this.ws = new WebSocketManager({ ...this.config.websocket, _shellRef: this });
47
- this.currentDir = process.cwd();
48
- this.username = os.userInfo().username;
49
- this.history = [];
50
- this.running = false;
51
-
52
- // TCP notification IPC
53
- this.notifyClients = new Set();
54
- this.tcpServer = null;
55
-
56
- this.api = new ThinkNCollabAPI({
57
- apiUrl: options.apiUrl || 'https://thinkncollab.com',
58
- wsUrl: options.wsUrl || 'https://thinkncollab.com'
59
- });
60
-
61
- this.commands = new Map();
62
- this.aliases = new Map();
63
- this.variables = new Map();
64
-
65
- // Restore persisted session on startup
66
- // Handles both .tncproject boot AND manual login from a previous session
67
- if (this.api.isAuthenticated()) {
68
- this.ws.setUser(this.api.getUser(), this.api.getToken());
69
- }
70
-
71
- this.loadHistory();
72
- this.registerBuiltinCommands();
73
- this.commandRegistry = new CommandRegistry(this);
74
- this.setupWebSocketHandlers();
75
- }
76
-
77
- // ─── TCP Notification Server ──────────────────────────────────────────────
78
-
79
- async startTCPServer() {
80
- this.notifyPort = await getAvailablePort(7379);
81
- return new Promise((resolve) => {
82
- this.tcpServer = net.createServer((socket) => {
83
- this.notifyClients.add(socket);
84
- socket.on('close', () => this.notifyClients.delete(socket));
85
- socket.on('error', () => this.notifyClients.delete(socket));
86
- });
87
- this.tcpServer.listen(this.notifyPort, '127.0.0.1', () => resolve());
88
- this.tcpServer.on('error', (err) => {
89
- console.log(chalk.yellow(`āš ļø Notification server: ${err.message}`));
90
- resolve();
91
- });
92
- });
93
- }
94
-
95
- pushNotification(data) {
96
- if (this.notifyClients.size === 0) return;
97
- const line = JSON.stringify(data) + '\n';
98
- for (const client of this.notifyClients) {
99
- try { client.write(line); } catch { }
100
- }
101
- }
102
-
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();
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
- `;
222
- try {
223
- spawn('osascript', ['-e', terminalScript], {
224
- detached: true, stdio: 'ignore'
225
- }).unref();
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();
270
- }
271
-
272
- } catch (err) {
273
- fallback();
274
- }
275
- }
276
-
277
- // ─── WebSocket → Shell + TCP bridge ──────────────────────────────────────
278
-
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
- });
285
-
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
- });
291
-
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
- });
321
-
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
- });
327
-
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'));
339
- });
340
-
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
- });
358
- });
359
-
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
- }
365
-
366
- // ─── Start ────────────────────────────────────────────────────────────────
367
-
368
- async start() {
369
- this.showWelcome();
370
-
371
- await this.startTCPServer();
372
- console.log(chalk.dim(` Notification server ready on port ${this.notifyPort}`));
373
-
374
- this.spawnNotificationWindow();
375
- console.log(chalk.dim(' Notification window opening...\n'));
376
-
377
- this.running = true;
378
- process.on('SIGINT', () => console.log(chalk.yellow('\nUse "exit" to quit')));
379
-
380
- // Try .tncproject auto-boot first
381
- const booted = await this._bootFromProjectConfig();
382
-
383
- // If no .tncproject but autoConnect is set, connect WS anyway
384
- if (!booted && this.config.websocket.autoConnect) {
385
- await this.ws.connect().catch(() => { });
386
- }
387
-
388
- const { input } = require('@inquirer/prompts');
389
-
390
- while (this.running) {
391
- try {
392
- const line = await input({ message: this.getPromptText(), required: false }).catch(() => null);
393
- if (line === null) { console.log(chalk.yellow('Use "exit" to quit')); continue; }
394
- const trimmed = line.trim();
395
- if (!trimmed) continue;
396
- this.history.push(trimmed);
397
- this.saveHistory();
398
- await this.execute(trimmed);
399
- } catch (err) {
400
- if (err.name === 'ExitPromptError' || err.message?.includes('force closed'))
401
- console.log(chalk.yellow('Use "exit" to quit'));
402
- else
403
- console.log(chalk.red(`Shell error: ${err.message}`));
404
- }
405
- }
406
- }
407
-
408
- // ─── Boot from .tncproject ────────────────────────────────────────────────
409
-
410
- async _bootFromProjectConfig() {
411
- if (!hasProjectConfig()) return false;
412
-
413
- let cfg;
414
- try {
415
- cfg = loadProjectConfig();
416
- } catch (err) {
417
- console.log(chalk.yellow(`āš ļø tncproject: ${err.message}\n`));
418
- return false;
419
- }
420
-
421
- console.log(chalk.dim(` šŸ“„ tncproject found`));
422
- console.log(chalk.dim(` Room : ${cfg.roomId}`));
423
- console.log(chalk.dim(` File : ${cfg.filePath}\n`));
424
-
425
- // Send raw encrypted file to server — server decrypts, returns shellToken
426
- let result;
427
- try {
428
- result = await this.api._request('POST', '/config/verify-config', {
429
- fileBase64: cfg.fileBase64,
430
- roomId: cfg.roomId,
431
- machineId: os.hostname(),
432
- });
433
- } catch (err) {
434
- console.log(chalk.red(`āŒ tncproject auth failed: ${err.message}`));
435
- console.log(chalk.dim(' Falling back to manual login.\n'));
436
- return false;
437
- }
438
-
439
- // Save session exactly like manual login
440
- this.api._saveSession({
441
- token: result.shellToken,
442
- shellSessionId: result.shellSessionId || null,
443
- user: {
444
- _id: result._id.toString(),
445
- userId: result._id.toString(),
446
- email: result.email,
447
- name: result.name,
448
- userType: 'User',
449
- },
450
- timestamp: new Date().toISOString(),
451
- });
452
-
453
- this.ws.setUser(this.api.getUser(), result.shellToken);
454
-
455
- // Connect WebSocket
456
- try {
457
- await this.ws.connect(result.shellToken);
458
- } catch (err) {
459
- console.log(chalk.yellow(`āš ļø WebSocket: ${err.message}`));
460
- console.log(chalk.green(`āœ… ${result.name} logged in (offline mode)\n`));
461
- return true;
462
- }
463
-
464
- // Auto-join the project room
465
- try {
466
- await this.ws.joinRoom(result.roomId.toString());
467
- console.log(chalk.green(`āœ… ${result.name} → room "${result.roomName || result.roomId}"\n`));
468
- } catch (err) {
469
- console.log(chalk.yellow(`āš ļø Could not auto-join room: ${err.message}`));
470
- console.log(chalk.dim(' Use "join <room-id>" manually.\n'));
471
- }
472
-
473
- return true;
474
- }
475
-
476
- async execute(input) {
477
- if (!input.trim()) return;
478
-
479
- const args = this.parseCommand(input);
480
- const cmdName = args[0].toLowerCase();
481
- const cmdArgs = args.slice(1);
482
-
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
- }
488
-
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
- }
503
-
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.`));
508
- process.stdout.write('\r\n');
509
- return;
510
- }
511
-
512
- await this.executeSystemCommand(input);
513
- }
514
- // ─── Prompt ───────────────────────────────────────────────────────────────
515
-
516
- getPromptText() {
517
- let p = '';
518
- const room = this.ws.getCurrentRoom?.();
519
- if (room) p += `[${room}] `;
520
- if (this.ws.isConnected?.()) p += 'ā— ';
521
-
522
- let displayPath = this.currentDir;
523
- const homeDir = os.homedir();
524
-
525
- if (process.platform === 'win32') {
526
- if (displayPath.toLowerCase() === homeDir.toLowerCase()) displayPath = '~';
527
- else if (displayPath.toLowerCase().startsWith(homeDir.toLowerCase()))
528
- displayPath = '~' + displayPath.slice(homeDir.length).replace(/\\/g, '/');
529
- else displayPath = displayPath.replace(/\\/g, '/');
530
- } else {
531
- displayPath = displayPath.replace(homeDir, '~');
532
- }
533
-
534
- return `${this.username}@thinknsh:${displayPath} $`;
535
- }
536
-
537
- getPrompt() { return this.getPromptText(); }
538
- printMessage(msg) { console.log(msg); }
539
-
540
- // ─── Built-ins ────────────────────────────────────────────────────────────
541
-
542
- registerBuiltinCommands() {
543
- const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
544
-
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 ~');
567
- }
568
-
569
- registerCommand(name, handler, description = '') { this.commands.set(name, { handler, description }); }
570
- addAlias(alias, command) { this.aliases.set(alias, command); }
571
-
572
- async helpCommand() {
573
- if (this.commandRegistry) console.log(this.commandRegistry.getHelpText());
574
- console.log(chalk.dim(' Any unrecognised command runs in your system shell\n'));
575
- }
576
-
577
- historyCommand() {
578
- this.history.forEach((cmd, i) => console.log(`${(i + 1).toString().padStart(4)} ${cmd}`));
579
- }
580
-
581
- async cdCommand(args) {
582
- const target = args.join(' ').trim() || os.homedir();
583
- try {
584
- let newPath;
585
- if (target === '~') newPath = os.homedir();
586
- else if (target === '..') newPath = path.dirname(this.currentDir);
587
- else newPath = path.resolve(this.currentDir, target);
588
- newPath = path.normalize(newPath);
589
- const stats = await fs.stat(newPath);
590
- if (!stats.isDirectory()) { console.log(chalk.red(`cd: not a directory: ${target}`)); return; }
591
- this.previousDir = this.currentDir;
592
- process.chdir(newPath);
593
- this.currentDir = process.cwd();
594
- } catch { console.log(chalk.red(`cd: no such directory: ${target}`)); }
595
- }
596
-
597
- async lsCommand(args) {
598
- try {
599
- const files = await fs.readdir(this.currentDir);
600
- const showAll = args.includes('-a');
601
- const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
602
- const fileList = await Promise.all(filtered.map(async (file) => {
603
- const stat = await fs.stat(path.join(this.currentDir, file));
604
- return { name: file, isDir: stat.isDirectory() };
605
- }));
606
- fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
607
- } catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
608
- }
609
- // tncproject auth failed: Could not read .tncproject — re-download it from the dashboard
610
- async connectCommand() {
611
- console.log(chalk.cyan('šŸ”Œ Connecting...'));
612
- try { await this.ws.connect(); }
613
- catch (err) { console.log(chalk.red(`āŒ ${err.message}`)); }
614
- }
615
-
616
- async statusCommand() {
617
- const connected = this.ws.isConnected();
618
- console.log(chalk.cyan('\nšŸ“Š Status:'));
619
- console.log(` WebSocket : ${connected ? chalk.green('āœ“ Connected') : chalk.red('āœ— Disconnected')}`);
620
- if (connected) console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);
621
- const room = this.ws.getCurrentRoom();
622
- console.log(` Room : ${room ? chalk.green(room) : chalk.yellow('None')}`);
623
- console.log(` Auth : ${this.api.isAuthenticated() ? chalk.green('āœ“ Logged in') : chalk.yellow('Not logged in')}`);
624
- const user = this.api.getUser();
625
- if (user) console.log(` User : ${chalk.dim(user.name || user.email)}`);
626
- console.log('');
627
- }
628
-
629
- async joinCommand(args) {
630
- if (!args[0]) { console.log(chalk.red('Usage: join <room-id>')); return; }
631
- if (!this.ws.isConnected()) { console.log(chalk.yellow('Connecting...')); await this.ws.connect(); }
632
- try {
633
- const result = await this.ws.joinRoom(args[0]);
634
- console.log(chalk.green(`āœ… Joined: ${result.roomName || args[0]}`));
635
- } catch (err) { console.log(chalk.red(`āŒ ${err.message}`)); }
636
- }
637
-
638
- async roomsCommand() {
639
- if (!this.ws.isConnected()) { console.log(chalk.yellow('Not connected')); return; }
640
- this.ws.socket.emit('rooms:list');
641
- this.ws.socket.once('rooms:list', (data) => {
642
- if (data.rooms?.length) data.rooms.forEach(r => console.log(` ${chalk.cyan(r.name || r.id)} — šŸ‘„ ${r.participantCount || 0}`));
643
- else console.log(' No rooms');
644
- });
645
- }
646
-
647
- async sayCommand(args) {
648
- const message = args.join(' ');
649
- if (!message) { console.log(chalk.red('Usage: say <message>')); return; }
650
- try {
651
- await this.ws.sendMessage(message);
652
- const displayName = this.api.getUser()?.name || this.username;
653
- console.log(chalk.green(`You: ${message}`));
654
- this.pushNotification({ type: 'message', from: `You (${displayName})`, text: message });
655
- } catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
656
- }
657
-
658
- async setCommand(args) {
659
- if (!args.length) { for (const [k, v] of this.variables) console.log(` ${chalk.green(k.padEnd(12))} = ${v}`); return; }
660
- this.variables.set(args[0], args.slice(1).join(' '));
661
- console.log(chalk.green(`${args[0]} = ${args.slice(1).join(' ')}`));
662
- }
663
-
664
- async unsetCommand(args) {
665
- if (!args[0]) return;
666
- this.variables.delete(args[0]);
667
- }
668
-
669
- getSessionPath() { return path.join(this.config.configDir, 'session.json'); }
670
-
671
- parseCommand(input) {
672
- const args = [];
673
- let current = '', inQuotes = false, quoteChar = '', escapeNext = false;
674
- for (const char of input) {
675
- if (char === '\\' && !escapeNext && !inQuotes) { escapeNext = true; continue; }
676
- if (escapeNext) { current += char; escapeNext = false; continue; }
677
- if ((char === '"' || char === "'") && !inQuotes) { inQuotes = true; quoteChar = char; continue; }
678
- if (char === quoteChar && inQuotes) { inQuotes = false; quoteChar = ''; continue; }
679
- if (char === ' ' && !inQuotes) { if (current) { args.push(current); current = ''; } continue; }
680
- current += char;
681
- }
682
- if (current) args.push(current);
683
- return args;
684
- }
685
-
686
- executeSystemCommand(command) {
687
- return new Promise((resolve) => {
688
- const child = spawn(command, [], {
689
- cwd: this.currentDir,
690
- shell: true,
691
- stdio: 'inherit'
692
- });
693
- child.on('close', (code) => {
694
- // Update currentDir in case command changed it (cd won't but others might) http://localhost:3001
695
- try { this.currentDir = process.cwd(); } catch { }
696
- process.stdout.write('\r\n');
697
- resolve();
698
- });
699
- child.on('error', () => {
700
- console.log(chalk.red(`Command not found: ${command}`));
701
- resolve();
702
- });
703
- });
704
- }
705
-
706
- showWelcome() {
707
- try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch { }
708
- console.log(chalk.cyan(`
709
- ╔══════════════════════════════════════════════════════╗
710
- ā•‘ ThinkNCollab Shell v0.0.10 ā•‘
711
- ā•‘ Type 'help' for commands ā•‘
712
- ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•`));
713
- }
714
-
715
- loadHistory() {
716
- try {
717
- if (fs.existsSync(this.config.historyFile))
718
- this.history = fs.readFileSync(this.config.historyFile, 'utf8').split('\n').filter(l => l.trim());
719
- } catch { }
720
- }
721
-
722
- saveHistory() {
723
- try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch { }
724
- }
725
-
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
- }
732
- }
733
-
734
- module.exports = ThinkNCollabShell;