thinkncollab-shell 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +392 -16
- package/package.json +1 -1
- package/src/commands/auth/login.js +58 -58
- package/src/commands/auth/register.js +52 -0
- package/src/commands/index.js +62 -43
- package/src/commands/room/join.js +119 -45
- package/src/commands/shell/notify.js +88 -0
- package/src/commands/tasks/complete.js +47 -0
- package/src/commands/tasks/create-task.js +101 -0
- package/src/commands/tasks/suggestTask.js +0 -0
- package/src/commands/tasks/task.js +75 -0
- package/src/commands/tasks/tasks.js +74 -0
- package/src/commands/terminal/share.js +252 -0
- package/src/core/api-client.js +104 -50
- package/src/core/notification-window.js +2 -1
- package/src/core/project-config.js +100 -0
- package/src/core/shell.js +222 -210
- package/src/core/websocket.js +166 -101
package/src/core/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 || 'https://thinkncollab.com
|
|
38
|
+
serverUrl: options.serverUrl || 'https://thinkncollab.com',
|
|
40
39
|
autoConnect: options.autoConnect || false
|
|
41
40
|
},
|
|
42
41
|
...options
|
|
@@ -50,7 +49,7 @@ 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
|
|
|
@@ -62,8 +61,12 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
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,18 +136,50 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
159
136
|
}
|
|
160
137
|
}
|
|
161
138
|
|
|
162
|
-
// ─── WebSocket →
|
|
139
|
+
// ─── WebSocket → Shell + TCP bridge ──────────────────────────────────────
|
|
163
140
|
|
|
164
141
|
setupWebSocketHandlers() {
|
|
165
|
-
this.ws.on('connected', (d) => this.pushNotification({ type: 'connected',
|
|
142
|
+
this.ws.on('connected', (d) => this.pushNotification({ type: 'connected', socketId: d.socketId }));
|
|
166
143
|
this.ws.on('disconnected', (d) => this.pushNotification({ type: 'disconnected', reason: d.reason }));
|
|
167
|
-
this.ws.on('reconnected', (
|
|
168
|
-
|
|
169
|
-
this.ws.on('
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
144
|
+
this.ws.on('reconnected', () => this.pushNotification({ type: 'connected', socketId: 'reconnected' }));
|
|
145
|
+
|
|
146
|
+
this.ws.on('message', (d) => {
|
|
147
|
+
const myId = this.api.getUser()?._id;
|
|
148
|
+
if (myId && d.userId === myId) return;
|
|
149
|
+
this.pushNotification({ type: 'message', from: d.username, text: d.message });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
this.ws.on('messageHistory', (messages) => {
|
|
153
|
+
if (!messages?.length) return;
|
|
154
|
+
console.log(chalk.dim('\n ─── Room History ───────────────────────────────'));
|
|
155
|
+
messages.forEach(msg => {
|
|
156
|
+
const from = msg.name || msg.username || msg.sender?.name || 'Unknown';
|
|
157
|
+
const text = msg.message || msg.content || msg.text || '';
|
|
158
|
+
const ts = msg.timestamp || msg.createdAt;
|
|
159
|
+
const timeStr = ts ? chalk.dim(`[${new Date(ts).toLocaleTimeString()}]`) : '';
|
|
160
|
+
console.log(` ${timeStr} ${chalk.cyan(from + ':')} ${chalk.white(text)}`);
|
|
161
|
+
});
|
|
162
|
+
console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
|
|
166
|
+
this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
|
|
167
|
+
|
|
168
|
+
this.ws.on('notification', (d) => {
|
|
169
|
+
this.pushNotification({
|
|
170
|
+
type: 'notification',
|
|
171
|
+
level: d.type,
|
|
172
|
+
title: d.title,
|
|
173
|
+
message: d.message,
|
|
174
|
+
taskTitle: d.taskTitle,
|
|
175
|
+
assignedBy: d.assignedBy,
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.ws.on('error', (e) => {
|
|
180
|
+
console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
|
|
181
|
+
this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
|
|
182
|
+
});
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
// ─── Start ────────────────────────────────────────────────────────────────
|
|
@@ -178,90 +187,149 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
178
187
|
async start() {
|
|
179
188
|
this.showWelcome();
|
|
180
189
|
|
|
181
|
-
// 1. Start TCP server for notification IPC
|
|
182
190
|
await this.startTCPServer();
|
|
183
191
|
console.log(chalk.dim(` Notification server ready on port ${this.notifyPort}`));
|
|
184
192
|
|
|
185
|
-
// 2. Spawn notification window
|
|
186
193
|
this.spawnNotificationWindow();
|
|
187
194
|
console.log(chalk.dim(' Notification window opening...\n'));
|
|
188
195
|
|
|
189
196
|
this.running = true;
|
|
197
|
+
process.on('SIGINT', () => console.log(chalk.yellow('\nUse "exit" to quit')));
|
|
190
198
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
199
|
+
// Try .tncproject auto-boot first
|
|
200
|
+
const booted = await this._bootFromProjectConfig();
|
|
194
201
|
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
// If no .tncproject but autoConnect is set, connect WS anyway
|
|
203
|
+
if (!booted && this.config.websocket.autoConnect) {
|
|
204
|
+
await this.ws.connect().catch(() => {});
|
|
197
205
|
}
|
|
198
206
|
|
|
199
207
|
const { input } = require('@inquirer/prompts');
|
|
200
208
|
|
|
201
209
|
while (this.running) {
|
|
202
210
|
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
|
-
|
|
211
|
+
const line = await input({ message: this.getPromptText(), required: false }).catch(() => null);
|
|
212
|
+
if (line === null) { console.log(chalk.yellow('Use "exit" to quit')); continue; }
|
|
213
213
|
const trimmed = line.trim();
|
|
214
214
|
if (!trimmed) continue;
|
|
215
|
-
|
|
216
215
|
this.history.push(trimmed);
|
|
217
216
|
this.saveHistory();
|
|
218
217
|
await this.execute(trimmed);
|
|
219
|
-
|
|
220
218
|
} catch (err) {
|
|
221
|
-
if (err.name === 'ExitPromptError' || err.message?.includes('force closed'))
|
|
219
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed'))
|
|
222
220
|
console.log(chalk.yellow('Use "exit" to quit'));
|
|
223
|
-
|
|
221
|
+
else
|
|
224
222
|
console.log(chalk.red(`Shell error: ${err.message}`));
|
|
225
|
-
}
|
|
226
223
|
}
|
|
227
224
|
}
|
|
228
225
|
}
|
|
229
226
|
|
|
230
|
-
|
|
227
|
+
// ─── Boot from .tncproject ────────────────────────────────────────────────
|
|
231
228
|
|
|
232
|
-
|
|
229
|
+
async _bootFromProjectConfig() {
|
|
230
|
+
if (!hasProjectConfig()) return false;
|
|
231
|
+
|
|
232
|
+
let cfg;
|
|
233
|
+
try {
|
|
234
|
+
cfg = loadProjectConfig();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.log(chalk.yellow(`⚠️ .tncproject: ${err.message}\n`));
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
233
239
|
|
|
234
|
-
|
|
235
|
-
|
|
240
|
+
console.log(chalk.dim(` 📄 .tncproject found`));
|
|
241
|
+
console.log(chalk.dim(` Room : ${cfg.roomId}`));
|
|
242
|
+
console.log(chalk.dim(` File : ${cfg.filePath}\n`));
|
|
236
243
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
244
|
+
// Send raw encrypted file to server — server decrypts, returns shellToken
|
|
245
|
+
let result;
|
|
246
|
+
try {
|
|
247
|
+
result = await this.api._request('POST', '/config/verify-config', {
|
|
248
|
+
fileBase64: cfg.fileBase64,
|
|
249
|
+
roomId: cfg.roomId,
|
|
250
|
+
});
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.log(chalk.red(`❌ .tncproject auth failed: ${err.message}`));
|
|
253
|
+
console.log(chalk.dim(' Falling back to manual login.\n'));
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
240
256
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
// Save session exactly like manual login
|
|
258
|
+
this.api._saveSession({
|
|
259
|
+
token: result.shellToken,
|
|
260
|
+
user: {
|
|
261
|
+
_id: result._id.toString(),
|
|
262
|
+
userId: result._id.toString(),
|
|
263
|
+
email: result.email,
|
|
264
|
+
name: result.name,
|
|
265
|
+
userType: 'User',
|
|
266
|
+
},
|
|
267
|
+
timestamp: new Date().toISOString(),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
this.ws.setUser(this.api.getUser(), result.shellToken);
|
|
271
|
+
|
|
272
|
+
// Connect WebSocket
|
|
273
|
+
try {
|
|
274
|
+
await this.ws.connect(result.shellToken);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.log(chalk.yellow(`⚠️ WebSocket: ${err.message}`));
|
|
277
|
+
console.log(chalk.green(`✅ ${result.name} logged in (offline mode)\n`));
|
|
278
|
+
return true;
|
|
250
279
|
}
|
|
251
280
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
281
|
+
// Auto-join the project room
|
|
282
|
+
try {
|
|
283
|
+
await this.ws.joinRoom(result.roomId.toString());
|
|
284
|
+
console.log(chalk.green(`✅ ${result.name} → room "${result.roomName || result.roomId}"\n`));
|
|
285
|
+
} catch (err) {
|
|
286
|
+
console.log(chalk.yellow(`⚠️ Could not auto-join room: ${err.message}`));
|
|
287
|
+
console.log(chalk.dim(' Use "join <room-id>" manually.\n'));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ─── Execute ──────────────────────────────────────────────────────────────
|
|
294
|
+
// shell.js — execute() mein ye change karo
|
|
295
|
+
async execute(input) {
|
|
296
|
+
if (!input.trim()) return;
|
|
297
|
+
|
|
298
|
+
const args = this.parseCommand(input);
|
|
299
|
+
const cmdName = args[0].toLowerCase();
|
|
300
|
+
const cmdArgs = args.slice(1);
|
|
301
|
+
|
|
302
|
+
if (this.commands.has(cmdName)) {
|
|
303
|
+
try { await this.commands.get(cmdName).handler(cmdArgs, this); }
|
|
304
|
+
catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
|
|
305
|
+
process.stdout.write('\r\n');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (this.commandRegistry) {
|
|
310
|
+
const handled = await this.commandRegistry.execute(input, this);
|
|
311
|
+
if (handled) {
|
|
312
|
+
process.stdout.write('\r\n');
|
|
313
|
+
return;
|
|
255
314
|
}
|
|
315
|
+
}
|
|
256
316
|
|
|
257
|
-
|
|
317
|
+
// ★ Windows pe known-useless OS commands block karo
|
|
318
|
+
const blockedOnWindows = [ 'open', 'which', 'grep', 'touch', 'cat'];
|
|
319
|
+
if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
|
|
320
|
+
console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
|
|
321
|
+
console.log(chalk.dim(` Type 'help' to see available commands.`));
|
|
322
|
+
process.stdout.write('\r\n');
|
|
323
|
+
return;
|
|
258
324
|
}
|
|
259
325
|
|
|
326
|
+
await this.executeSystemCommand(input);
|
|
327
|
+
}
|
|
260
328
|
// ─── Prompt ───────────────────────────────────────────────────────────────
|
|
261
329
|
|
|
262
330
|
getPromptText() {
|
|
263
331
|
let p = '';
|
|
264
|
-
const room
|
|
332
|
+
const room = this.ws.getCurrentRoom?.();
|
|
265
333
|
if (room) p += `[${room}] `;
|
|
266
334
|
if (this.ws.isConnected?.()) p += '● ';
|
|
267
335
|
|
|
@@ -277,43 +345,38 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
277
345
|
displayPath = displayPath.replace(homeDir, '~');
|
|
278
346
|
}
|
|
279
347
|
|
|
280
|
-
|
|
281
|
-
return p;
|
|
348
|
+
return `${this.username}@thinknsh:${displayPath} $`;
|
|
282
349
|
}
|
|
283
350
|
|
|
284
|
-
getPrompt()
|
|
285
|
-
|
|
351
|
+
getPrompt() { return this.getPromptText(); }
|
|
286
352
|
printMessage(msg) { console.log(msg); }
|
|
287
353
|
|
|
288
354
|
// ─── Built-ins ────────────────────────────────────────────────────────────
|
|
289
355
|
|
|
290
356
|
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');
|
|
357
|
+
const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
|
|
358
|
+
|
|
359
|
+
r('help', this.helpCommand.bind(this), 'Show available commands');
|
|
360
|
+
r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
361
|
+
r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
362
|
+
r('clear', () => console.clear(), 'Clear screen');
|
|
363
|
+
r('history', this.historyCommand.bind(this), 'Show command history');
|
|
364
|
+
r('cd', this.cdCommand.bind(this), 'Change directory');
|
|
365
|
+
r('pwd', () => console.log(process.cwd()), 'Print working directory');
|
|
366
|
+
r('ls', this.lsCommand.bind(this), 'List files');
|
|
367
|
+
r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
|
|
368
|
+
r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
|
|
369
|
+
r('status', this.statusCommand.bind(this), 'Show connection status');
|
|
370
|
+
r('join', this.joinCommand.bind(this), 'Join a room');
|
|
371
|
+
r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
|
|
372
|
+
r('rooms', this.roomsCommand.bind(this), 'List rooms');
|
|
373
|
+
r('say', this.sayCommand.bind(this), 'Send a message');
|
|
374
|
+
r('msg', this.sayCommand.bind(this), 'Send a message');
|
|
375
|
+
r('set', this.setCommand.bind(this), 'Set a variable');
|
|
376
|
+
r('unset', this.unsetCommand.bind(this), 'Unset a variable');
|
|
313
377
|
|
|
314
378
|
this.addAlias('ll', 'ls -la');
|
|
315
379
|
this.addAlias('..', 'cd ..');
|
|
316
|
-
this.addAlias('...', 'cd ../..');
|
|
317
380
|
this.addAlias('~', 'cd ~');
|
|
318
381
|
}
|
|
319
382
|
|
|
@@ -321,26 +384,8 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
321
384
|
addAlias(alias, command) { this.aliases.set(alias, command); }
|
|
322
385
|
|
|
323
386
|
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'));
|
|
387
|
+
if (this.commandRegistry) console.log(this.commandRegistry.getHelpText());
|
|
388
|
+
console.log(chalk.dim(' Any unrecognised command runs in your system shell\n'));
|
|
344
389
|
}
|
|
345
390
|
|
|
346
391
|
historyCommand() {
|
|
@@ -351,11 +396,8 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
351
396
|
const target = args.join(' ').trim() || os.homedir();
|
|
352
397
|
try {
|
|
353
398
|
let newPath;
|
|
354
|
-
if (target === '~'
|
|
355
|
-
else if (target.startsWith('~/') || target.startsWith('~\\')) newPath = path.join(os.homedir(), target.slice(2));
|
|
399
|
+
if (target === '~') newPath = os.homedir();
|
|
356
400
|
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
401
|
else newPath = path.resolve(this.currentDir, target);
|
|
360
402
|
newPath = path.normalize(newPath);
|
|
361
403
|
const stats = await fs.stat(newPath);
|
|
@@ -369,22 +411,18 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
369
411
|
async lsCommand(args) {
|
|
370
412
|
try {
|
|
371
413
|
const files = await fs.readdir(this.currentDir);
|
|
372
|
-
const showAll = args.includes('-a')
|
|
414
|
+
const showAll = args.includes('-a');
|
|
373
415
|
const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
|
|
374
416
|
const fileList = await Promise.all(filtered.map(async (file) => {
|
|
375
417
|
const stat = await fs.stat(path.join(this.currentDir, file));
|
|
376
|
-
return { name: file, isDir: stat.isDirectory()
|
|
418
|
+
return { name: file, isDir: stat.isDirectory() };
|
|
377
419
|
}));
|
|
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
|
-
});
|
|
420
|
+
fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
|
|
383
421
|
} catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
|
|
384
422
|
}
|
|
385
423
|
|
|
386
|
-
async connectCommand(
|
|
387
|
-
console.log(chalk.cyan(
|
|
424
|
+
async connectCommand() {
|
|
425
|
+
console.log(chalk.cyan('🔌 Connecting...'));
|
|
388
426
|
try { await this.ws.connect(); }
|
|
389
427
|
catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
|
|
390
428
|
}
|
|
@@ -392,22 +430,21 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
392
430
|
async statusCommand() {
|
|
393
431
|
const connected = this.ws.isConnected();
|
|
394
432
|
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
|
-
}
|
|
433
|
+
console.log(` WebSocket : ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
|
|
434
|
+
if (connected) console.log(` Socket ID : ${chalk.dim(this.ws.getSocketId())}`);
|
|
400
435
|
const room = this.ws.getCurrentRoom();
|
|
401
|
-
console.log(`Room:
|
|
402
|
-
console.log(`
|
|
436
|
+
console.log(` Room : ${room ? chalk.green(room) : chalk.yellow('None')}`);
|
|
437
|
+
console.log(` Auth : ${this.api.isAuthenticated() ? chalk.green('✓ Logged in') : chalk.yellow('Not logged in')}`);
|
|
438
|
+
const user = this.api.getUser();
|
|
439
|
+
if (user) console.log(` User : ${chalk.dim(user.name || user.email)}`);
|
|
440
|
+
console.log('');
|
|
403
441
|
}
|
|
404
442
|
|
|
405
443
|
async joinCommand(args) {
|
|
406
|
-
if (!args[0]) { console.log(chalk.red('Usage: join <room-id>
|
|
444
|
+
if (!args[0]) { console.log(chalk.red('Usage: join <room-id>')); return; }
|
|
407
445
|
if (!this.ws.isConnected()) { console.log(chalk.yellow('Connecting...')); await this.ws.connect(); }
|
|
408
|
-
console.log(chalk.cyan(`🔗 Joining: ${args[0]}...`));
|
|
409
446
|
try {
|
|
410
|
-
const result = await this.ws.joinRoom(args[0]
|
|
447
|
+
const result = await this.ws.joinRoom(args[0]);
|
|
411
448
|
console.log(chalk.green(`✅ Joined: ${result.roomName || args[0]}`));
|
|
412
449
|
} catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
|
|
413
450
|
}
|
|
@@ -417,61 +454,30 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
417
454
|
this.ws.socket.emit('rooms:list');
|
|
418
455
|
this.ws.socket.once('rooms:list', (data) => {
|
|
419
456
|
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
|
|
457
|
+
else console.log(' No rooms');
|
|
421
458
|
});
|
|
422
459
|
}
|
|
423
460
|
|
|
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
461
|
async sayCommand(args) {
|
|
435
462
|
const message = args.join(' ');
|
|
436
463
|
if (!message) { console.log(chalk.red('Usage: say <message>')); return; }
|
|
437
464
|
try {
|
|
438
465
|
await this.ws.sendMessage(message);
|
|
466
|
+
const displayName = this.api.getUser()?.name || this.username;
|
|
439
467
|
console.log(chalk.green(`You: ${message}`));
|
|
468
|
+
this.pushNotification({ type: 'message', from: `You (${displayName})`, text: message });
|
|
440
469
|
} catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
|
|
441
470
|
}
|
|
442
471
|
|
|
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
472
|
async setCommand(args) {
|
|
466
473
|
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
474
|
this.variables.set(args[0], args.slice(1).join(' '));
|
|
469
|
-
console.log(chalk.green(
|
|
475
|
+
console.log(chalk.green(`${args[0]} = ${args.slice(1).join(' ')}`));
|
|
470
476
|
}
|
|
471
477
|
|
|
472
478
|
async unsetCommand(args) {
|
|
473
|
-
if (!args[0])
|
|
474
|
-
this.variables.delete(args[0])
|
|
479
|
+
if (!args[0]) return;
|
|
480
|
+
this.variables.delete(args[0]);
|
|
475
481
|
}
|
|
476
482
|
|
|
477
483
|
getSessionPath() { return path.join(this.config.configDir, 'session.json'); }
|
|
@@ -494,22 +500,28 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
494
500
|
executeSystemCommand(command) {
|
|
495
501
|
return new Promise((resolve) => {
|
|
496
502
|
const child = spawn(command, [], {
|
|
497
|
-
cwd:
|
|
498
|
-
|
|
503
|
+
cwd: this.currentDir,
|
|
504
|
+
shell: true,
|
|
505
|
+
stdio: 'inherit'
|
|
506
|
+
});
|
|
507
|
+
child.on('close', (code) => {
|
|
508
|
+
// Update currentDir in case command changed it (cd won't but others might)
|
|
509
|
+
try { this.currentDir = process.cwd(); } catch {}
|
|
510
|
+
process.stdout.write('\r\n');
|
|
511
|
+
resolve();
|
|
512
|
+
});
|
|
513
|
+
child.on('error', () => {
|
|
514
|
+
console.log(chalk.red(`Command not found: ${command}`));
|
|
515
|
+
resolve();
|
|
499
516
|
});
|
|
500
|
-
child.on('close', () => resolve());
|
|
501
|
-
child.on('error', () => { console.log(chalk.red(`Command not found: ${command}`)); resolve(); });
|
|
502
517
|
});
|
|
503
518
|
}
|
|
504
519
|
|
|
505
520
|
showWelcome() {
|
|
506
|
-
try {
|
|
507
|
-
const figlet = require('figlet');
|
|
508
|
-
console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' })));
|
|
509
|
-
} catch {}
|
|
521
|
+
try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
|
|
510
522
|
console.log(chalk.cyan(`
|
|
511
523
|
╔══════════════════════════════════════════════════════╗
|
|
512
|
-
║ ThinkNCollab Shell v0.0.
|
|
524
|
+
║ ThinkNCollab Shell v0.0.5 ║
|
|
513
525
|
║ Type 'help' for commands ║
|
|
514
526
|
╚══════════════════════════════════════════════════════╝`));
|
|
515
527
|
}
|