thinkncollab-shell 0.0.5 → 0.0.7
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 +322 -179
- package/bin/thinknsh.js +3 -3
- package/package.json +1 -1
- package/src/commands/auth/login.js +58 -58
- package/src/commands/auth/register.js +52 -0
- package/src/commands/auth/whoami.js +44 -39
- package/src/commands/index.js +69 -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-status.js +61 -0
- package/src/commands/tasks/task.js +75 -0
- package/src/commands/tasks/tasks.js +183 -0
- package/src/commands/terminal/share.js +252 -0
- package/src/core/api-client.js +156 -66
- package/src/core/notification-window.js +1 -1
- package/src/core/project-config.js +100 -0
- package/src/core/shell.js +252 -221
- package/src/core/websocket.js +136 -149
package/src/core/shell.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ThinkNCollab Shell Core
|
|
3
|
-
* Inquirer REPL + TCP IPC for notification window
|
|
2
|
+
* src/core/shell.js — ThinkNCollab Shell Core
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const path = require('path');
|
|
@@ -9,12 +8,12 @@ const os = require('os');
|
|
|
9
8
|
const chalk = require('chalk');
|
|
10
9
|
const net = require('net');
|
|
11
10
|
const { spawn } = require('child_process');
|
|
12
|
-
const WebSocketManager = require('./websocket');
|
|
13
11
|
const EventEmitter = require('events');
|
|
12
|
+
const WebSocketManager = require('./websocket');
|
|
14
13
|
const CommandRegistry = require('../commands');
|
|
15
14
|
const ThinkNCollabAPI = require('./api-client');
|
|
15
|
+
const { loadProjectConfig, hasProjectConfig } = require('./project-config');
|
|
16
16
|
|
|
17
|
-
// Find an available port starting from 7379
|
|
18
17
|
function getAvailablePort(startPort) {
|
|
19
18
|
return new Promise((resolve) => {
|
|
20
19
|
const server = net.createServer();
|
|
@@ -36,7 +35,7 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
36
35
|
configDir: path.join(os.homedir(), '.thinkncollab'),
|
|
37
36
|
maxHistory: 1000,
|
|
38
37
|
websocket: {
|
|
39
|
-
serverUrl: options.serverUrl || '
|
|
38
|
+
serverUrl: options.serverUrl || 'http://localhost:3001',
|
|
40
39
|
autoConnect: options.autoConnect || false
|
|
41
40
|
},
|
|
42
41
|
...options
|
|
@@ -50,20 +49,24 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
50
49
|
this.history = [];
|
|
51
50
|
this.running = false;
|
|
52
51
|
|
|
53
|
-
// TCP notification
|
|
52
|
+
// TCP notification IPC
|
|
54
53
|
this.notifyClients = new Set();
|
|
55
54
|
this.tcpServer = null;
|
|
56
55
|
|
|
57
56
|
this.api = new ThinkNCollabAPI({
|
|
58
|
-
apiUrl: options.apiUrl || '
|
|
59
|
-
wsUrl: options.wsUrl || '
|
|
57
|
+
apiUrl: options.apiUrl || 'http://localhost:3001',
|
|
58
|
+
wsUrl: options.wsUrl || 'http://localhost:3001'
|
|
60
59
|
});
|
|
61
60
|
|
|
62
61
|
this.commands = new Map();
|
|
63
62
|
this.aliases = new Map();
|
|
64
63
|
this.variables = new Map();
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
}
|
|
67
70
|
|
|
68
71
|
this.loadHistory();
|
|
69
72
|
this.registerBuiltinCommands();
|
|
@@ -75,18 +78,13 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
75
78
|
|
|
76
79
|
async startTCPServer() {
|
|
77
80
|
this.notifyPort = await getAvailablePort(7379);
|
|
78
|
-
|
|
79
81
|
return new Promise((resolve) => {
|
|
80
82
|
this.tcpServer = net.createServer((socket) => {
|
|
81
83
|
this.notifyClients.add(socket);
|
|
82
84
|
socket.on('close', () => this.notifyClients.delete(socket));
|
|
83
85
|
socket.on('error', () => this.notifyClients.delete(socket));
|
|
84
86
|
});
|
|
85
|
-
|
|
86
|
-
this.tcpServer.listen(this.notifyPort, '127.0.0.1', () => {
|
|
87
|
-
resolve();
|
|
88
|
-
});
|
|
89
|
-
|
|
87
|
+
this.tcpServer.listen(this.notifyPort, '127.0.0.1', () => resolve());
|
|
90
88
|
this.tcpServer.on('error', (err) => {
|
|
91
89
|
console.log(chalk.yellow(`⚠️ Notification server: ${err.message}`));
|
|
92
90
|
resolve();
|
|
@@ -94,7 +92,6 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
94
92
|
});
|
|
95
93
|
}
|
|
96
94
|
|
|
97
|
-
// Send notification event to all connected notification windows
|
|
98
95
|
pushNotification(data) {
|
|
99
96
|
if (this.notifyClients.size === 0) return;
|
|
100
97
|
const line = JSON.stringify(data) + '\n';
|
|
@@ -103,10 +100,9 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
103
100
|
}
|
|
104
101
|
}
|
|
105
102
|
|
|
106
|
-
// ─── Spawn notification window
|
|
103
|
+
// ─── Spawn notification window ────────────────────────────────────────────
|
|
107
104
|
|
|
108
105
|
spawnNotificationWindow() {
|
|
109
|
-
// Path to notification-window.js (same dir as this file)
|
|
110
106
|
const notifyScript = path.join(__dirname, 'notification-window.js');
|
|
111
107
|
const env = { ...process.env, THINKNSH_NOTIFY_PORT: String(this.notifyPort) };
|
|
112
108
|
|
|
@@ -114,43 +110,24 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
114
110
|
if (process.platform === 'win32') {
|
|
115
111
|
const isVSCode = process.env.TERM_PROGRAM === 'vscode';
|
|
116
112
|
if (isVSCode) {
|
|
117
|
-
console.log(chalk.yellow('\n VS Code detected —
|
|
118
|
-
console.log(chalk.dim(' Open a new terminal tab and run:'));
|
|
113
|
+
console.log(chalk.yellow('\n VS Code detected — open a new terminal tab and run:'));
|
|
119
114
|
console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${this.notifyPort}; node "${notifyScript}"\n`));
|
|
120
115
|
} else {
|
|
121
|
-
// Use array args so cmd handles spaces in path correctly
|
|
122
116
|
const nodeExe = process.execPath;
|
|
123
|
-
spawn('cmd.exe', [
|
|
124
|
-
|
|
125
|
-
`"${nodeExe}" "${notifyScript}"`
|
|
126
|
-
], { detached: true, stdio: 'ignore', env, shell: false }).unref();
|
|
117
|
+
spawn('cmd.exe', ['/c', 'start', 'cmd.exe', '/k', `"${nodeExe}" "${notifyScript}"`],
|
|
118
|
+
{ detached: true, stdio: 'ignore', env, shell: false }).unref();
|
|
127
119
|
}
|
|
128
|
-
|
|
129
120
|
} else if (process.platform === 'darwin') {
|
|
130
|
-
|
|
131
|
-
const script = `tell application "Terminal" to do script "THINKNSH_NOTIFY_PORT=${NOTIFY_PORT} node '${notifyScript}'"`;
|
|
121
|
+
const script = `tell application "Terminal" to do script "THINKNSH_NOTIFY_PORT=${this.notifyPort} node '${notifyScript}'"`;
|
|
132
122
|
spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
133
|
-
|
|
134
123
|
} else {
|
|
135
|
-
// Linux — try common terminal emulators in order
|
|
136
124
|
const terminals = [
|
|
137
125
|
['gnome-terminal', ['--', 'node', notifyScript]],
|
|
138
126
|
['xterm', ['-e', `node "${notifyScript}"`]],
|
|
139
127
|
['konsole', ['--noclose', '-e', 'node', notifyScript]],
|
|
140
|
-
['xfce4-terminal', ['-e', `node "${notifyScript}"`]],
|
|
141
|
-
['x-terminal-emulator', ['-e', `node "${notifyScript}"`]],
|
|
142
128
|
];
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
spawn(cmd, cmdArgs, { detached: true, stdio: 'ignore', env }).unref();
|
|
147
|
-
spawned = true;
|
|
148
|
-
break;
|
|
149
|
-
} catch {}
|
|
150
|
-
}
|
|
151
|
-
if (!spawned) {
|
|
152
|
-
console.log(chalk.yellow('⚠️ Could not auto-open notification window.'));
|
|
153
|
-
console.log(chalk.dim(` Run manually: node ${notifyScript}`));
|
|
129
|
+
for (const [cmd, args] of terminals) {
|
|
130
|
+
try { spawn(cmd, args, { detached: true, stdio: 'ignore', env }).unref(); break; } catch {}
|
|
154
131
|
}
|
|
155
132
|
}
|
|
156
133
|
} catch (err) {
|
|
@@ -159,109 +136,218 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
159
136
|
}
|
|
160
137
|
}
|
|
161
138
|
|
|
162
|
-
// ─── WebSocket →
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.
|
|
167
|
-
this.
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
174
|
-
|
|
139
|
+
// ─── WebSocket → Shell + TCP bridge ──────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
setupWebSocketHandlers() {
|
|
142
|
+
this.ws.on('connected', (d) => {
|
|
143
|
+
this.api.logEvent('connected', null, { socketId: d?.socketId });
|
|
144
|
+
this.api.startHeartbeat();
|
|
145
|
+
this.pushNotification({ type: 'connected', socketId: d?.socketId });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.ws.on('disconnected', (d) => {
|
|
149
|
+
this.api.logEvent('disconnected', null, { reason: d?.reason });
|
|
150
|
+
this.api.stopHeartbeat();
|
|
151
|
+
this.pushNotification({ type: 'disconnected', reason: d?.reason });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.ws.on('reconnected', () => {
|
|
155
|
+
this.api.logEvent('reconnected');
|
|
156
|
+
this.api.startHeartbeat();
|
|
157
|
+
this.pushNotification({ type: 'connected', socketId: 'reconnected' });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
this.ws.on('message', (d) => {
|
|
161
|
+
const myId = this.api.getUser()?._id;
|
|
162
|
+
if (myId && d.userId === myId) return;
|
|
163
|
+
this.pushNotification({ type: 'message', from: d.username, text: d.message });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this.ws.on('messageHistory', (messages) => {
|
|
167
|
+
if (!messages?.length) return;
|
|
168
|
+
console.log(chalk.dim('\n ─── Room History ───────────────────────────────'));
|
|
169
|
+
messages.forEach(msg => {
|
|
170
|
+
const from = msg.name || msg.username || msg.sender?.name || 'Unknown';
|
|
171
|
+
const text = msg.message || msg.content || msg.text || '';
|
|
172
|
+
const ts = msg.timestamp || msg.createdAt;
|
|
173
|
+
const timeStr = ts ? chalk.dim(`[${new Date(ts).toLocaleTimeString()}]`) : '';
|
|
174
|
+
console.log(` ${timeStr} ${chalk.cyan(from + ':')} ${chalk.white(text)}`);
|
|
175
|
+
});
|
|
176
|
+
console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
|
|
180
|
+
this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
|
|
181
|
+
|
|
182
|
+
// room join/leave events
|
|
183
|
+
this.ws.on('joined', (roomId) => this.api.logEvent('room_join', roomId));
|
|
184
|
+
this.ws.on('left', (roomId) => this.api.logEvent('room_leave', roomId));
|
|
185
|
+
|
|
186
|
+
this.ws.on('notification', (d) => {
|
|
187
|
+
this.pushNotification({
|
|
188
|
+
type: 'notification',
|
|
189
|
+
level: d.type,
|
|
190
|
+
title: d.title,
|
|
191
|
+
message: d.message,
|
|
192
|
+
taskTitle: d.taskTitle,
|
|
193
|
+
assignedBy: d.assignedBy,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
this.ws.on('error', (e) => {
|
|
198
|
+
console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
|
|
199
|
+
this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
|
|
200
|
+
});
|
|
201
|
+
}
|
|
175
202
|
|
|
176
203
|
// ─── Start ────────────────────────────────────────────────────────────────
|
|
177
204
|
|
|
178
205
|
async start() {
|
|
179
206
|
this.showWelcome();
|
|
180
207
|
|
|
181
|
-
// 1. Start TCP server for notification IPC
|
|
182
208
|
await this.startTCPServer();
|
|
183
209
|
console.log(chalk.dim(` Notification server ready on port ${this.notifyPort}`));
|
|
184
210
|
|
|
185
|
-
// 2. Spawn notification window
|
|
186
211
|
this.spawnNotificationWindow();
|
|
187
212
|
console.log(chalk.dim(' Notification window opening...\n'));
|
|
188
213
|
|
|
189
214
|
this.running = true;
|
|
215
|
+
process.on('SIGINT', () => console.log(chalk.yellow('\nUse "exit" to quit')));
|
|
190
216
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
217
|
+
// Try .tncproject auto-boot first
|
|
218
|
+
const booted = await this._bootFromProjectConfig();
|
|
194
219
|
|
|
195
|
-
|
|
196
|
-
|
|
220
|
+
// If no .tncproject but autoConnect is set, connect WS anyway
|
|
221
|
+
if (!booted && this.config.websocket.autoConnect) {
|
|
222
|
+
await this.ws.connect().catch(() => {});
|
|
197
223
|
}
|
|
198
224
|
|
|
199
225
|
const { input } = require('@inquirer/prompts');
|
|
200
226
|
|
|
201
227
|
while (this.running) {
|
|
202
228
|
try {
|
|
203
|
-
const line = await input({
|
|
204
|
-
|
|
205
|
-
required: false,
|
|
206
|
-
}).catch(() => null);
|
|
207
|
-
|
|
208
|
-
if (line === null) {
|
|
209
|
-
console.log(chalk.yellow('Use "exit" to quit'));
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
|
|
229
|
+
const line = await input({ message: this.getPromptText(), required: false }).catch(() => null);
|
|
230
|
+
if (line === null) { console.log(chalk.yellow('Use "exit" to quit')); continue; }
|
|
213
231
|
const trimmed = line.trim();
|
|
214
232
|
if (!trimmed) continue;
|
|
215
|
-
|
|
216
233
|
this.history.push(trimmed);
|
|
217
234
|
this.saveHistory();
|
|
218
235
|
await this.execute(trimmed);
|
|
219
|
-
|
|
220
236
|
} catch (err) {
|
|
221
|
-
if (err.name === 'ExitPromptError' || err.message?.includes('force closed'))
|
|
237
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed'))
|
|
222
238
|
console.log(chalk.yellow('Use "exit" to quit'));
|
|
223
|
-
|
|
239
|
+
else
|
|
224
240
|
console.log(chalk.red(`Shell error: ${err.message}`));
|
|
225
|
-
}
|
|
226
241
|
}
|
|
227
242
|
}
|
|
228
243
|
}
|
|
229
244
|
|
|
230
|
-
|
|
245
|
+
// ─── Boot from .tncproject ────────────────────────────────────────────────
|
|
231
246
|
|
|
232
|
-
|
|
247
|
+
async _bootFromProjectConfig() {
|
|
248
|
+
if (!hasProjectConfig()) return false;
|
|
233
249
|
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
let cfg;
|
|
251
|
+
try {
|
|
252
|
+
cfg = loadProjectConfig();
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.log(chalk.yellow(`⚠️ tncproject: ${err.message}\n`));
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
236
257
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
258
|
+
console.log(chalk.dim(` 📄 tncproject found`));
|
|
259
|
+
console.log(chalk.dim(` Room : ${cfg.roomId}`));
|
|
260
|
+
console.log(chalk.dim(` File : ${cfg.filePath}\n`));
|
|
240
261
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
262
|
+
// Send raw encrypted file to server — server decrypts, returns shellToken
|
|
263
|
+
let result;
|
|
264
|
+
try {
|
|
265
|
+
result = await this.api._request('POST', '/config/verify-config', {
|
|
266
|
+
fileBase64: cfg.fileBase64,
|
|
267
|
+
roomId: cfg.roomId,
|
|
268
|
+
});
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.log(chalk.red(`❌ tncproject auth failed: ${err.message}`));
|
|
271
|
+
console.log(chalk.dim(' Falling back to manual login.\n'));
|
|
272
|
+
return false;
|
|
250
273
|
}
|
|
251
274
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
275
|
+
// Save session exactly like manual login
|
|
276
|
+
this.api._saveSession({
|
|
277
|
+
token: result.shellToken,
|
|
278
|
+
user: {
|
|
279
|
+
_id: result._id.toString(),
|
|
280
|
+
userId: result._id.toString(),
|
|
281
|
+
email: result.email,
|
|
282
|
+
name: result.name,
|
|
283
|
+
userType: 'User',
|
|
284
|
+
},
|
|
285
|
+
timestamp: new Date().toISOString(),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
this.ws.setUser(this.api.getUser(), result.shellToken);
|
|
289
|
+
|
|
290
|
+
// Connect WebSocket
|
|
291
|
+
try {
|
|
292
|
+
await this.ws.connect(result.shellToken);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
console.log(chalk.yellow(`⚠️ WebSocket: ${err.message}`));
|
|
295
|
+
console.log(chalk.green(`✅ ${result.name} logged in (offline mode)\n`));
|
|
296
|
+
return true;
|
|
255
297
|
}
|
|
256
298
|
|
|
257
|
-
|
|
299
|
+
// Auto-join the project room
|
|
300
|
+
try {
|
|
301
|
+
await this.ws.joinRoom(result.roomId.toString());
|
|
302
|
+
console.log(chalk.green(`✅ ${result.name} → room "${result.roomName || result.roomId}"\n`));
|
|
303
|
+
} catch (err) {
|
|
304
|
+
console.log(chalk.yellow(`⚠️ Could not auto-join room: ${err.message}`));
|
|
305
|
+
console.log(chalk.dim(' Use "join <room-id>" manually.\n'));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return true;
|
|
258
309
|
}
|
|
259
310
|
|
|
311
|
+
// ─── Execute ──────────────────────────────────────────────────────────────
|
|
312
|
+
// shell.js — execute() mein ye change karo
|
|
313
|
+
async execute(input) {
|
|
314
|
+
if (!input.trim()) return;
|
|
315
|
+
|
|
316
|
+
const args = this.parseCommand(input);
|
|
317
|
+
const cmdName = args[0].toLowerCase();
|
|
318
|
+
const cmdArgs = args.slice(1);
|
|
319
|
+
|
|
320
|
+
if (this.commands.has(cmdName)) {
|
|
321
|
+
try { await this.commands.get(cmdName).handler(cmdArgs, this); }
|
|
322
|
+
catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
|
|
323
|
+
process.stdout.write('\r\n');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (this.commandRegistry) {
|
|
328
|
+
const handled = await this.commandRegistry.execute(input, this);
|
|
329
|
+
if (handled) {
|
|
330
|
+
process.stdout.write('\r\n');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ★ Windows pe known-useless OS commands block karo
|
|
336
|
+
const blockedOnWindows = [ 'open', 'which', 'grep', 'touch', 'cat'];
|
|
337
|
+
if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
|
|
338
|
+
console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
|
|
339
|
+
console.log(chalk.dim(` Type 'help' to see available commands.`));
|
|
340
|
+
process.stdout.write('\r\n');
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
await this.executeSystemCommand(input);
|
|
345
|
+
}
|
|
260
346
|
// ─── Prompt ───────────────────────────────────────────────────────────────
|
|
261
347
|
|
|
262
348
|
getPromptText() {
|
|
263
349
|
let p = '';
|
|
264
|
-
const room
|
|
350
|
+
const room = this.ws.getCurrentRoom?.();
|
|
265
351
|
if (room) p += `[${room}] `;
|
|
266
352
|
if (this.ws.isConnected?.()) p += '● ';
|
|
267
353
|
|
|
@@ -277,43 +363,38 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
277
363
|
displayPath = displayPath.replace(homeDir, '~');
|
|
278
364
|
}
|
|
279
365
|
|
|
280
|
-
|
|
281
|
-
return p;
|
|
366
|
+
return `${this.username}@thinknsh:${displayPath} $`;
|
|
282
367
|
}
|
|
283
368
|
|
|
284
|
-
getPrompt()
|
|
285
|
-
|
|
369
|
+
getPrompt() { return this.getPromptText(); }
|
|
286
370
|
printMessage(msg) { console.log(msg); }
|
|
287
371
|
|
|
288
372
|
// ─── Built-ins ────────────────────────────────────────────────────────────
|
|
289
373
|
|
|
290
374
|
registerBuiltinCommands() {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
this.registerCommand('set', this.setCommand.bind(this), 'Set a variable');
|
|
312
|
-
this.registerCommand('unset', this.unsetCommand.bind(this), 'Unset a variable');
|
|
375
|
+
const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
|
|
376
|
+
|
|
377
|
+
r('help', this.helpCommand.bind(this), 'Show available commands');
|
|
378
|
+
r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
379
|
+
r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
380
|
+
r('clear', () => console.clear(), 'Clear screen');
|
|
381
|
+
r('history', this.historyCommand.bind(this), 'Show command history');
|
|
382
|
+
r('cd', this.cdCommand.bind(this), 'Change directory');
|
|
383
|
+
r('pwd', () => console.log(process.cwd()), 'Print working directory');
|
|
384
|
+
r('ls', this.lsCommand.bind(this), 'List files');
|
|
385
|
+
r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
|
|
386
|
+
r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
|
|
387
|
+
r('status', this.statusCommand.bind(this), 'Show connection status');
|
|
388
|
+
r('join', this.joinCommand.bind(this), 'Join a room');
|
|
389
|
+
r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
|
|
390
|
+
r('rooms', this.roomsCommand.bind(this), 'List rooms');
|
|
391
|
+
r('say', this.sayCommand.bind(this), 'Send a message');
|
|
392
|
+
r('msg', this.sayCommand.bind(this), 'Send a message');
|
|
393
|
+
r('set', this.setCommand.bind(this), 'Set a variable');
|
|
394
|
+
r('unset', this.unsetCommand.bind(this), 'Unset a variable');
|
|
313
395
|
|
|
314
396
|
this.addAlias('ll', 'ls -la');
|
|
315
397
|
this.addAlias('..', 'cd ..');
|
|
316
|
-
this.addAlias('...', 'cd ../..');
|
|
317
398
|
this.addAlias('~', 'cd ~');
|
|
318
399
|
}
|
|
319
400
|
|
|
@@ -321,26 +402,8 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
321
402
|
addAlias(alias, command) { this.aliases.set(alias, command); }
|
|
322
403
|
|
|
323
404
|
async helpCommand() {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
'Filesystem': ['cd', 'pwd', 'ls'],
|
|
327
|
-
'WebSocket': ['connect', 'disconnect', 'status'],
|
|
328
|
-
'Rooms': ['join', 'leave', 'rooms', 'users'],
|
|
329
|
-
'Messages': ['say', 'msg'],
|
|
330
|
-
'Notifications': ['notify'],
|
|
331
|
-
'Aliases': ['alias', 'unalias'],
|
|
332
|
-
'Variables': ['set', 'unset']
|
|
333
|
-
};
|
|
334
|
-
console.log(chalk.cyan('\n📚 Available Commands:'));
|
|
335
|
-
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
336
|
-
for (const [category, cmds] of Object.entries(categories)) {
|
|
337
|
-
console.log(chalk.yellow(`\n${category}:`));
|
|
338
|
-
cmds.forEach(cmd => {
|
|
339
|
-
const c = this.commands.get(cmd);
|
|
340
|
-
if (c) console.log(` ${chalk.green(cmd.padEnd(12))} - ${c.description}`);
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
console.log(chalk.dim('\nAny other command runs in your system shell\n'));
|
|
405
|
+
if (this.commandRegistry) console.log(this.commandRegistry.getHelpText());
|
|
406
|
+
console.log(chalk.dim(' Any unrecognised command runs in your system shell\n'));
|
|
344
407
|
}
|
|
345
408
|
|
|
346
409
|
historyCommand() {
|
|
@@ -351,11 +414,8 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
351
414
|
const target = args.join(' ').trim() || os.homedir();
|
|
352
415
|
try {
|
|
353
416
|
let newPath;
|
|
354
|
-
if (target === '~'
|
|
355
|
-
else if (target.startsWith('~/') || target.startsWith('~\\')) newPath = path.join(os.homedir(), target.slice(2));
|
|
417
|
+
if (target === '~') newPath = os.homedir();
|
|
356
418
|
else if (target === '..') newPath = path.dirname(this.currentDir);
|
|
357
|
-
else if (target === '-') newPath = this.previousDir || this.currentDir;
|
|
358
|
-
else if (process.platform === 'win32' && target.match(/^[a-zA-Z]:[\\/]/)) newPath = target;
|
|
359
419
|
else newPath = path.resolve(this.currentDir, target);
|
|
360
420
|
newPath = path.normalize(newPath);
|
|
361
421
|
const stats = await fs.stat(newPath);
|
|
@@ -369,22 +429,18 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
369
429
|
async lsCommand(args) {
|
|
370
430
|
try {
|
|
371
431
|
const files = await fs.readdir(this.currentDir);
|
|
372
|
-
const showAll = args.includes('-a')
|
|
432
|
+
const showAll = args.includes('-a');
|
|
373
433
|
const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
|
|
374
434
|
const fileList = await Promise.all(filtered.map(async (file) => {
|
|
375
435
|
const stat = await fs.stat(path.join(this.currentDir, file));
|
|
376
|
-
return { name: file, isDir: stat.isDirectory()
|
|
436
|
+
return { name: file, isDir: stat.isDirectory() };
|
|
377
437
|
}));
|
|
378
|
-
fileList.forEach(f =>
|
|
379
|
-
if (f.isDir) console.log(chalk.blue(f.name + '/'));
|
|
380
|
-
else if (f.mode & 0o111) console.log(chalk.green(f.name + '*'));
|
|
381
|
-
else console.log(f.name);
|
|
382
|
-
});
|
|
438
|
+
fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
|
|
383
439
|
} catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
|
|
384
440
|
}
|
|
385
441
|
|
|
386
|
-
async connectCommand(
|
|
387
|
-
console.log(chalk.cyan(
|
|
442
|
+
async connectCommand() {
|
|
443
|
+
console.log(chalk.cyan('🔌 Connecting...'));
|
|
388
444
|
try { await this.ws.connect(); }
|
|
389
445
|
catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
|
|
390
446
|
}
|
|
@@ -392,23 +448,22 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
392
448
|
async statusCommand() {
|
|
393
449
|
const connected = this.ws.isConnected();
|
|
394
450
|
console.log(chalk.cyan('\n📊 Status:'));
|
|
395
|
-
console.log(`WebSocket: ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
|
|
396
|
-
if (connected) {
|
|
397
|
-
console.log(`Socket ID: ${chalk.dim(this.ws.getSocketId())}`);
|
|
398
|
-
console.log(`Server: ${chalk.dim(this.ws.config.serverUrl)}`);
|
|
399
|
-
}
|
|
451
|
+
console.log(` WebSocket : ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
|
|
452
|
+
if (connected) console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);
|
|
400
453
|
const room = this.ws.getCurrentRoom();
|
|
401
|
-
console.log(`Room:
|
|
402
|
-
console.log(`
|
|
454
|
+
console.log(` Room : ${room ? chalk.green(room) : chalk.yellow('None')}`);
|
|
455
|
+
console.log(` Auth : ${this.api.isAuthenticated() ? chalk.green('✓ Logged in') : chalk.yellow('Not logged in')}`);
|
|
456
|
+
const user = this.api.getUser();
|
|
457
|
+
if (user) console.log(` User : ${chalk.dim(user.name || user.email)}`);
|
|
458
|
+
console.log('');
|
|
403
459
|
}
|
|
404
460
|
|
|
405
461
|
async joinCommand(args) {
|
|
406
|
-
if (!args[0]) { console.log(chalk.red('Usage: join <room-id>
|
|
462
|
+
if (!args[0]) { console.log(chalk.red('Usage: join <room-id>')); return; }
|
|
407
463
|
if (!this.ws.isConnected()) { console.log(chalk.yellow('Connecting...')); await this.ws.connect(); }
|
|
408
|
-
console.log(chalk.cyan(`🔗 Joining: ${args[0]}...`));
|
|
409
464
|
try {
|
|
410
|
-
const result = await this.ws.joinRoom(args[0]
|
|
411
|
-
console.log(chalk.green(
|
|
465
|
+
const result = await this.ws.joinRoom(args[0]);
|
|
466
|
+
console.log(chalk.green(`✅ Joined: ${result.roomName || args[0]}`));
|
|
412
467
|
} catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
|
|
413
468
|
}
|
|
414
469
|
|
|
@@ -417,61 +472,30 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
417
472
|
this.ws.socket.emit('rooms:list');
|
|
418
473
|
this.ws.socket.once('rooms:list', (data) => {
|
|
419
474
|
if (data.rooms?.length) data.rooms.forEach(r => console.log(` ${chalk.cyan(r.name || r.id)} — 👥 ${r.participantCount || 0}`));
|
|
420
|
-
else console.log(' No rooms
|
|
475
|
+
else console.log(' No rooms');
|
|
421
476
|
});
|
|
422
477
|
}
|
|
423
478
|
|
|
424
|
-
async usersCommand() {
|
|
425
|
-
if (!this.ws.isConnected()) { console.log(chalk.yellow('Not connected')); return; }
|
|
426
|
-
const room = this.ws.getCurrentRoom();
|
|
427
|
-
if (!room) { console.log(chalk.yellow('Not in a room')); return; }
|
|
428
|
-
try {
|
|
429
|
-
const users = await this.ws.getParticipants();
|
|
430
|
-
users.forEach(u => console.log(` ${chalk.green('●')} ${u.username}${u.isTyping ? chalk.dim(' (typing...)') : ''}`));
|
|
431
|
-
} catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
|
|
432
|
-
}
|
|
433
|
-
|
|
434
479
|
async sayCommand(args) {
|
|
435
480
|
const message = args.join(' ');
|
|
436
481
|
if (!message) { console.log(chalk.red('Usage: say <message>')); return; }
|
|
437
482
|
try {
|
|
438
483
|
await this.ws.sendMessage(message);
|
|
484
|
+
const displayName = this.api.getUser()?.name || this.username;
|
|
439
485
|
console.log(chalk.green(`You: ${message}`));
|
|
486
|
+
this.pushNotification({ type: 'message', from: `You (${displayName})`, text: message });
|
|
440
487
|
} catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
|
|
441
488
|
}
|
|
442
489
|
|
|
443
|
-
async notifyCommand(args) {
|
|
444
|
-
const type = args[0] || 'info';
|
|
445
|
-
const msg = args.slice(1).join(' ');
|
|
446
|
-
if (!msg) { console.log(chalk.red('Usage: notify <type> <message>')); return; }
|
|
447
|
-
try {
|
|
448
|
-
await this.ws.sendNotification({ type, title: 'Shell Notification', message: msg });
|
|
449
|
-
console.log(chalk.green('Notification sent'));
|
|
450
|
-
} catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async aliasCommand(args) {
|
|
454
|
-
if (!args.length) { for (const [a, c] of this.aliases) console.log(` ${chalk.green(a.padEnd(12))} → ${chalk.dim(c)}`); return; }
|
|
455
|
-
if (args.length < 2) { console.log(chalk.red('Usage: alias <n> <cmd>')); return; }
|
|
456
|
-
this.addAlias(args[0], args.slice(1).join(' '));
|
|
457
|
-
console.log(chalk.green(`${args[0]} → ${args.slice(1).join(' ')}`));
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
async unaliasCommand(args) {
|
|
461
|
-
if (!args[0]) { console.log(chalk.red('Usage: unalias <n>')); return; }
|
|
462
|
-
this.aliases.delete(args[0]) ? console.log(chalk.green(`Removed: ${args[0]}`)) : console.log(chalk.red(`Not found: ${args[0]}`));
|
|
463
|
-
}
|
|
464
|
-
|
|
465
490
|
async setCommand(args) {
|
|
466
491
|
if (!args.length) { for (const [k, v] of this.variables) console.log(` ${chalk.green(k.padEnd(12))} = ${v}`); return; }
|
|
467
|
-
if (args.length < 2) { console.log(chalk.red('Usage: set <n> <value>')); return; }
|
|
468
492
|
this.variables.set(args[0], args.slice(1).join(' '));
|
|
469
493
|
console.log(chalk.green(`${args[0]} = ${args.slice(1).join(' ')}`));
|
|
470
494
|
}
|
|
471
495
|
|
|
472
496
|
async unsetCommand(args) {
|
|
473
|
-
if (!args[0])
|
|
474
|
-
this.variables.delete(args[0])
|
|
497
|
+
if (!args[0]) return;
|
|
498
|
+
this.variables.delete(args[0]);
|
|
475
499
|
}
|
|
476
500
|
|
|
477
501
|
getSessionPath() { return path.join(this.config.configDir, 'session.json'); }
|
|
@@ -494,22 +518,28 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
494
518
|
executeSystemCommand(command) {
|
|
495
519
|
return new Promise((resolve) => {
|
|
496
520
|
const child = spawn(command, [], {
|
|
497
|
-
cwd:
|
|
498
|
-
|
|
521
|
+
cwd: this.currentDir,
|
|
522
|
+
shell: true,
|
|
523
|
+
stdio: 'inherit'
|
|
524
|
+
});
|
|
525
|
+
child.on('close', (code) => {
|
|
526
|
+
// Update currentDir in case command changed it (cd won't but others might)
|
|
527
|
+
try { this.currentDir = process.cwd(); } catch {}
|
|
528
|
+
process.stdout.write('\r\n');
|
|
529
|
+
resolve();
|
|
530
|
+
});
|
|
531
|
+
child.on('error', () => {
|
|
532
|
+
console.log(chalk.red(`Command not found: ${command}`));
|
|
533
|
+
resolve();
|
|
499
534
|
});
|
|
500
|
-
child.on('close', () => resolve());
|
|
501
|
-
child.on('error', () => { console.log(chalk.red(`Command not found: ${command}`)); resolve(); });
|
|
502
535
|
});
|
|
503
536
|
}
|
|
504
537
|
|
|
505
538
|
showWelcome() {
|
|
506
|
-
try {
|
|
507
|
-
const figlet = require('figlet');
|
|
508
|
-
console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' })));
|
|
509
|
-
} catch {}
|
|
539
|
+
try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
|
|
510
540
|
console.log(chalk.cyan(`
|
|
511
541
|
╔══════════════════════════════════════════════════════╗
|
|
512
|
-
║ ThinkNCollab Shell v0.0.
|
|
542
|
+
║ ThinkNCollab Shell v0.0.6 ║
|
|
513
543
|
║ Type 'help' for commands ║
|
|
514
544
|
╚══════════════════════════════════════════════════════╝`));
|
|
515
545
|
}
|
|
@@ -525,11 +555,12 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
525
555
|
try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
|
|
526
556
|
}
|
|
527
557
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
558
|
+
cleanup() {
|
|
559
|
+
this.api.logout(); // session end + heartbeat stop + lastSeen update
|
|
560
|
+
if (this.tcpServer) { try { this.tcpServer.close(); } catch {} }
|
|
561
|
+
if (this.ws) this.ws.disconnect();
|
|
562
|
+
this.saveHistory();
|
|
563
|
+
}
|
|
533
564
|
}
|
|
534
565
|
|
|
535
566
|
module.exports = ThinkNCollabShell;
|