thinkncollab-shell 0.0.1 → 0.0.3
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/bin/thinknsh.js +3 -2
- package/package.json +8 -5
- package/src/commands/auth/login.js +77 -0
- package/src/commands/auth/logout.js +32 -0
- package/src/commands/auth/whoami.js +47 -0
- package/src/commands/index.js +104 -0
- package/src/commands/message/say.js +43 -0
- package/src/commands/room/join.js +68 -0
- package/src/commands/room/leave.js +33 -0
- package/src/commands/room/rooms.js +66 -0
- package/src/commands/shell/status.js +73 -0
- package/src/commands/team/invite.js +46 -0
- package/src/commands/team/myteam.js +78 -0
- package/src/commands/team/teammates.js +52 -0
- package/src/core/api-client.js +156 -0
- package/src/core/notification-window.js +135 -0
- package/src/core/shell.js +416 -855
- package/src/core/websocket.js +230 -292
package/src/core/shell.js
CHANGED
|
@@ -1,972 +1,533 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ThinkNCollab Shell Core
|
|
3
|
-
*
|
|
3
|
+
* Inquirer REPL + TCP IPC for notification window
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const {
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const net = require('net');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
12
|
const WebSocketManager = require('./websocket');
|
|
13
|
-
const EventEmitter
|
|
13
|
+
const EventEmitter = require('events');
|
|
14
|
+
const CommandRegistry = require('../commands');
|
|
15
|
+
const ThinkNCollabAPI = require('./api-client');
|
|
16
|
+
|
|
17
|
+
// Find an available port starting from 7379
|
|
18
|
+
function getAvailablePort(startPort) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const server = net.createServer();
|
|
21
|
+
server.listen(startPort, '127.0.0.1', () => {
|
|
22
|
+
const port = server.address().port;
|
|
23
|
+
server.close(() => resolve(port));
|
|
24
|
+
});
|
|
25
|
+
server.on('error', () => resolve(getAvailablePort(startPort + 1)));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
14
28
|
|
|
15
29
|
class ThinkNCollabShell extends EventEmitter {
|
|
30
|
+
|
|
16
31
|
constructor(options = {}) {
|
|
17
32
|
super();
|
|
18
|
-
|
|
19
|
-
// Configuration
|
|
33
|
+
|
|
20
34
|
this.config = {
|
|
21
35
|
historyFile: path.join(os.homedir(), '.thinknsh_history'),
|
|
22
|
-
configDir:
|
|
23
|
-
maxHistory:
|
|
24
|
-
prompt: options.prompt || 'thinknsh> ',
|
|
36
|
+
configDir: path.join(os.homedir(), '.thinkncollab'),
|
|
37
|
+
maxHistory: 1000,
|
|
25
38
|
websocket: {
|
|
26
|
-
serverUrl:
|
|
39
|
+
serverUrl: options.serverUrl || 'https://thinkncollab.com/thinknsh',
|
|
27
40
|
autoConnect: options.autoConnect || false
|
|
28
41
|
},
|
|
29
42
|
...options
|
|
30
43
|
};
|
|
31
|
-
|
|
32
|
-
// Ensure config directory exists
|
|
44
|
+
|
|
33
45
|
fs.ensureDirSync(this.config.configDir);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
this.ws = new WebSocketManager(this.config.websocket);
|
|
37
|
-
this.rl = null;
|
|
46
|
+
|
|
47
|
+
this.ws = new WebSocketManager(this.config.websocket);
|
|
38
48
|
this.currentDir = process.cwd();
|
|
39
|
-
this.username
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
this.username = os.userInfo().username;
|
|
50
|
+
this.history = [];
|
|
51
|
+
this.running = false;
|
|
52
|
+
|
|
53
|
+
// TCP notification clients
|
|
54
|
+
this.notifyClients = new Set();
|
|
55
|
+
this.tcpServer = null;
|
|
56
|
+
|
|
57
|
+
this.api = new ThinkNCollabAPI({
|
|
58
|
+
apiUrl: options.apiUrl || 'https://thinkncollab.com',
|
|
59
|
+
wsUrl: options.wsUrl || 'https://thinkncollab.com'
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
this.commands = new Map();
|
|
63
|
+
this.aliases = new Map();
|
|
44
64
|
this.variables = new Map();
|
|
45
|
-
this.isTyping
|
|
65
|
+
this.isTyping = false;
|
|
46
66
|
this.typingTimeout = null;
|
|
47
|
-
|
|
48
|
-
// Load history
|
|
67
|
+
|
|
49
68
|
this.loadHistory();
|
|
50
|
-
|
|
51
|
-
// Register built-in commands
|
|
52
69
|
this.registerBuiltinCommands();
|
|
53
|
-
|
|
54
|
-
// Setup WebSocket event handlers
|
|
70
|
+
this.commandRegistry = new CommandRegistry(this);
|
|
55
71
|
this.setupWebSocketHandlers();
|
|
56
72
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.handleIncomingMessage(data);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
this.ws.on('notification', (data) => {
|
|
82
|
-
this.handleNotification(data);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
this.ws.on('userJoined', (data) => {
|
|
86
|
-
this.printMessage(chalk.cyan(`👤 ${data.username} joined the room`));
|
|
87
|
-
this.emit('user:joined', data);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
this.ws.on('userLeft', (data) => {
|
|
91
|
-
this.printMessage(chalk.yellow(`👤 ${data.username} left the room`));
|
|
92
|
-
this.emit('user:left', data);
|
|
73
|
+
|
|
74
|
+
// ─── TCP Notification Server ──────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
async startTCPServer() {
|
|
77
|
+
this.notifyPort = await getAvailablePort(7379);
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve) => {
|
|
80
|
+
this.tcpServer = net.createServer((socket) => {
|
|
81
|
+
this.notifyClients.add(socket);
|
|
82
|
+
socket.on('close', () => this.notifyClients.delete(socket));
|
|
83
|
+
socket.on('error', () => this.notifyClients.delete(socket));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
this.tcpServer.listen(this.notifyPort, '127.0.0.1', () => {
|
|
87
|
+
resolve();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.tcpServer.on('error', (err) => {
|
|
91
|
+
console.log(chalk.yellow(`⚠️ Notification server: ${err.message}`));
|
|
92
|
+
resolve();
|
|
93
|
+
});
|
|
93
94
|
});
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Send notification event to all connected notification windows
|
|
98
|
+
pushNotification(data) {
|
|
99
|
+
if (this.notifyClients.size === 0) return;
|
|
100
|
+
const line = JSON.stringify(data) + '\n';
|
|
101
|
+
for (const client of this.notifyClients) {
|
|
102
|
+
try { client.write(line); } catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Spawn notification window (cross-platform) ───────────────────────────
|
|
107
|
+
|
|
108
|
+
spawnNotificationWindow() {
|
|
109
|
+
// Path to notification-window.js (same dir as this file)
|
|
110
|
+
const notifyScript = path.join(__dirname, 'notification-window.js');
|
|
111
|
+
const env = { ...process.env, THINKNSH_NOTIFY_PORT: String(this.notifyPort) };
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
if (process.platform === 'win32') {
|
|
115
|
+
const isVSCode = process.env.TERM_PROGRAM === 'vscode';
|
|
116
|
+
if (isVSCode) {
|
|
117
|
+
console.log(chalk.yellow('\n VS Code detected — cannot auto-open new window.'));
|
|
118
|
+
console.log(chalk.dim(' Open a new terminal tab and run:'));
|
|
119
|
+
console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${this.notifyPort}; node "${notifyScript}"\n`));
|
|
120
|
+
} else {
|
|
121
|
+
// Use array args so cmd handles spaces in path correctly
|
|
122
|
+
const nodeExe = process.execPath;
|
|
123
|
+
spawn('cmd.exe', [
|
|
124
|
+
'/c', 'start', 'cmd.exe', '/k',
|
|
125
|
+
`"${nodeExe}" "${notifyScript}"`
|
|
126
|
+
], { detached: true, stdio: 'ignore', env, shell: false }).unref();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
} else if (process.platform === 'darwin') {
|
|
130
|
+
// macOS — Terminal.app
|
|
131
|
+
const script = `tell application "Terminal" to do script "THINKNSH_NOTIFY_PORT=${NOTIFY_PORT} node '${notifyScript}'"`;
|
|
132
|
+
spawn('osascript', ['-e', script], { detached: true, stdio: 'ignore' }).unref();
|
|
133
|
+
|
|
98
134
|
} else {
|
|
99
|
-
|
|
135
|
+
// Linux — try common terminal emulators in order
|
|
136
|
+
const terminals = [
|
|
137
|
+
['gnome-terminal', ['--', 'node', notifyScript]],
|
|
138
|
+
['xterm', ['-e', `node "${notifyScript}"`]],
|
|
139
|
+
['konsole', ['--noclose', '-e', 'node', notifyScript]],
|
|
140
|
+
['xfce4-terminal', ['-e', `node "${notifyScript}"`]],
|
|
141
|
+
['x-terminal-emulator', ['-e', `node "${notifyScript}"`]],
|
|
142
|
+
];
|
|
143
|
+
let spawned = false;
|
|
144
|
+
for (const [cmd, cmdArgs] of terminals) {
|
|
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}`));
|
|
154
|
+
}
|
|
100
155
|
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
this.printMessage(chalk.gray(data.output));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
this.ws.on('error', (error) => {
|
|
109
|
-
this.printMessage(chalk.red(`❌ WebSocket error: ${error.message}`));
|
|
110
|
-
});
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.log(chalk.yellow(`⚠️ Could not spawn notification window: ${err.message}`));
|
|
158
|
+
console.log(chalk.dim(` Run manually: node "${notifyScript}"`));
|
|
159
|
+
}
|
|
111
160
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
161
|
+
|
|
162
|
+
// ─── WebSocket → push to notification window ──────────────────────────────
|
|
163
|
+
|
|
164
|
+
setupWebSocketHandlers() {
|
|
165
|
+
this.ws.on('connected', (d) => this.pushNotification({ type: 'connected', socketId: d.socketId }));
|
|
166
|
+
this.ws.on('disconnected', (d) => this.pushNotification({ type: 'disconnected', reason: d.reason }));
|
|
167
|
+
this.ws.on('reconnected', (d) => this.pushNotification({ type: 'connected', socketId: 'reconnected' }));
|
|
168
|
+
this.ws.on('message', (d) => this.pushNotification({ type: 'message', from: d.username, text: d.message }));
|
|
169
|
+
this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
|
|
170
|
+
this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
|
|
171
|
+
this.ws.on('userTyping', (d) => this.pushNotification({ type: d.isTyping ? 'typing' : 'stoppedTyping', username: d.username }));
|
|
172
|
+
this.ws.on('notification', (d) => this.pushNotification({ type: 'notification', level: d.type, title: d.title, message: d.message }));
|
|
173
|
+
this.ws.on('error', (e) => this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message }));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Start ────────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
116
178
|
async start() {
|
|
117
179
|
this.showWelcome();
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
this.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (input) {
|
|
131
|
-
this.history.push(input);
|
|
132
|
-
this.saveHistory();
|
|
133
|
-
await this.execute(input);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
this.rl.setPrompt(this.getPrompt());
|
|
137
|
-
this.rl.prompt();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Handle exit
|
|
141
|
-
this.rl.on('close', () => {
|
|
142
|
-
this.cleanup();
|
|
143
|
-
process.exit(0);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
// Handle signals
|
|
180
|
+
|
|
181
|
+
// 1. Start TCP server for notification IPC
|
|
182
|
+
await this.startTCPServer();
|
|
183
|
+
console.log(chalk.dim(` Notification server ready on port ${this.notifyPort}`));
|
|
184
|
+
|
|
185
|
+
// 2. Spawn notification window
|
|
186
|
+
this.spawnNotificationWindow();
|
|
187
|
+
console.log(chalk.dim(' Notification window opening...\n'));
|
|
188
|
+
|
|
189
|
+
this.running = true;
|
|
190
|
+
|
|
147
191
|
process.on('SIGINT', () => {
|
|
148
|
-
|
|
192
|
+
console.log(chalk.yellow('\nUse "exit" to quit'));
|
|
149
193
|
});
|
|
150
|
-
|
|
151
|
-
this.rl.prompt();
|
|
152
|
-
|
|
153
|
-
// Auto-connect if configured
|
|
194
|
+
|
|
154
195
|
if (this.config.websocket.autoConnect) {
|
|
155
196
|
await this.ws.connect();
|
|
156
197
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const aliasCmd = this.aliases.get(command);
|
|
186
|
-
// Preserve original args when expanding alias
|
|
187
|
-
return this.execute(aliasCmd + (commandArgs.length ? ' ' + commandArgs.join(' ') : ''));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Check built-in commands
|
|
191
|
-
if (this.commands.has(command)) {
|
|
192
|
-
try {
|
|
193
|
-
const cmd = this.commands.get(command);
|
|
194
|
-
const result = await cmd.handler(commandArgs, this);
|
|
195
|
-
if (result !== undefined) {
|
|
196
|
-
console.log(result);
|
|
198
|
+
|
|
199
|
+
const { input } = require('@inquirer/prompts');
|
|
200
|
+
|
|
201
|
+
while (this.running) {
|
|
202
|
+
try {
|
|
203
|
+
const line = await input({
|
|
204
|
+
message: this.getPromptText(),
|
|
205
|
+
required: false,
|
|
206
|
+
}).catch(() => null);
|
|
207
|
+
|
|
208
|
+
if (line === null) {
|
|
209
|
+
console.log(chalk.yellow('Use "exit" to quit'));
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const trimmed = line.trim();
|
|
214
|
+
if (!trimmed) continue;
|
|
215
|
+
|
|
216
|
+
this.history.push(trimmed);
|
|
217
|
+
this.saveHistory();
|
|
218
|
+
await this.execute(trimmed);
|
|
219
|
+
|
|
220
|
+
} catch (err) {
|
|
221
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
|
|
222
|
+
console.log(chalk.yellow('Use "exit" to quit'));
|
|
223
|
+
} else {
|
|
224
|
+
console.log(chalk.red(`Shell error: ${err.message}`));
|
|
225
|
+
}
|
|
197
226
|
}
|
|
198
|
-
} catch (error) {
|
|
199
|
-
console.log(chalk.red(`Error: ${error.message}`));
|
|
200
|
-
}
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Execute system command
|
|
205
|
-
await this.executeSystemCommand(input);
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Register built-in commands
|
|
209
|
-
*/
|
|
210
|
-
registerBuiltinCommands() {
|
|
211
|
-
// Core shell commands
|
|
212
|
-
this.registerCommand('help', this.helpCommand.bind(this), 'Show this help');
|
|
213
|
-
this.registerCommand('exit', () => process.exit(0), 'Exit the shell');
|
|
214
|
-
this.registerCommand('quit', () => process.exit(0), 'Exit the shell');
|
|
215
|
-
this.registerCommand('clear', () => console.clear(), 'Clear the screen');
|
|
216
|
-
this.registerCommand('history', this.historyCommand.bind(this), 'Show command history');
|
|
217
|
-
|
|
218
|
-
// Directory commands
|
|
219
|
-
this.registerCommand('cd', this.cdCommand.bind(this), 'Change directory');
|
|
220
|
-
this.registerCommand('pwd', () => process.cwd(), 'Print working directory');
|
|
221
|
-
this.registerCommand('ls', this.lsCommand.bind(this), 'List files');
|
|
222
|
-
|
|
223
|
-
// WebSocket commands
|
|
224
|
-
this.registerCommand('connect', this.connectCommand.bind(this), 'Connect to WebSocket server');
|
|
225
|
-
this.registerCommand('disconnect', () => this.ws.disconnect(), 'Disconnect from WebSocket');
|
|
226
|
-
this.registerCommand('status', this.statusCommand.bind(this), 'Show connection status');
|
|
227
|
-
|
|
228
|
-
// Room commands
|
|
229
|
-
this.registerCommand('join', this.joinCommand.bind(this), 'Join a collaboration room');
|
|
230
|
-
this.registerCommand('leave', () => this.ws.leaveRoom(), 'Leave current room');
|
|
231
|
-
this.registerCommand('rooms', this.roomsCommand.bind(this), 'List available rooms');
|
|
232
|
-
this.registerCommand('users', this.usersCommand.bind(this), 'List users in room');
|
|
233
|
-
|
|
234
|
-
// Message commands
|
|
235
|
-
this.registerCommand('say', this.sayCommand.bind(this), 'Send a message to the room');
|
|
236
|
-
this.registerCommand('msg', this.sayCommand.bind(this), 'Send a message (alias)');
|
|
237
|
-
|
|
238
|
-
// Notification commands
|
|
239
|
-
this.registerCommand('notify', this.notifyCommand.bind(this), 'Send a notification');
|
|
240
|
-
|
|
241
|
-
// Alias commands
|
|
242
|
-
this.registerCommand('alias', this.aliasCommand.bind(this), 'Create an alias');
|
|
243
|
-
this.registerCommand('unalias', this.unaliasCommand.bind(this), 'Remove an alias');
|
|
244
|
-
|
|
245
|
-
// Variable commands
|
|
246
|
-
this.registerCommand('set', this.setCommand.bind(this), 'Set a variable');
|
|
247
|
-
this.registerCommand('unset', this.unsetCommand.bind(this), 'Unset a variable');
|
|
248
|
-
|
|
249
|
-
// Add aliases
|
|
250
|
-
this.addAlias('ll', 'ls -la');
|
|
251
|
-
this.addAlias('..', 'cd ..');
|
|
252
|
-
this.addAlias('...', 'cd ../..');
|
|
253
|
-
this.addAlias('~', 'cd ~');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Register a command
|
|
258
|
-
*/
|
|
259
|
-
registerCommand(name, handler, description = '') {
|
|
260
|
-
this.commands.set(name, { handler, description });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Add an alias
|
|
265
|
-
*/
|
|
266
|
-
addAlias(alias, command) {
|
|
267
|
-
this.aliases.set(alias, command);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get formatted prompt
|
|
272
|
-
*/
|
|
273
|
-
getPrompt() {
|
|
274
|
-
let prompt = '';
|
|
275
|
-
|
|
276
|
-
// Add room indicator if in a room
|
|
277
|
-
const room = this.ws.getCurrentRoom();
|
|
278
|
-
if (room) {
|
|
279
|
-
prompt += chalk.magenta(`[${room}] `);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Add connection indicator
|
|
283
|
-
if (this.ws.isConnected()) {
|
|
284
|
-
prompt += chalk.green('● ');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// FIX: Better path display for Windows
|
|
288
|
-
let displayPath = this.currentDir;
|
|
289
|
-
const homeDir = os.homedir();
|
|
290
|
-
|
|
291
|
-
if (process.platform === 'win32') {
|
|
292
|
-
// Windows: Show clean path without C:\Users\username
|
|
293
|
-
if (displayPath.toLowerCase() === homeDir.toLowerCase()) {
|
|
294
|
-
displayPath = '~'; // At home directory
|
|
295
|
-
} else if (displayPath.toLowerCase().startsWith(homeDir.toLowerCase())) {
|
|
296
|
-
// In subdirectory of home
|
|
297
|
-
const relativePath = displayPath.slice(homeDir.length);
|
|
298
|
-
displayPath = '~' + relativePath.replace(/\\/g, '/');
|
|
299
|
-
} else {
|
|
300
|
-
// Outside home, show full path with forward slashes for consistency
|
|
301
|
-
displayPath = displayPath.replace(/\\/g, '/');
|
|
302
227
|
}
|
|
303
|
-
} else {
|
|
304
|
-
// Unix: Use ~ for home
|
|
305
|
-
displayPath = displayPath.replace(homeDir, '~');
|
|
306
228
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
let inQuotes = false;
|
|
327
|
-
let quoteChar = '';
|
|
328
|
-
let escapeNext = false;
|
|
329
|
-
|
|
330
|
-
for (let i = 0; i < input.length; i++) {
|
|
331
|
-
const char = input[i];
|
|
332
|
-
|
|
333
|
-
// Handle escape character
|
|
334
|
-
if (char === '\\' && !escapeNext && !inQuotes) {
|
|
335
|
-
escapeNext = true;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
if (escapeNext) {
|
|
340
|
-
current += char;
|
|
341
|
-
escapeNext = false;
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Handle quotes
|
|
346
|
-
if ((char === '"' || char === "'") && !inQuotes) {
|
|
347
|
-
inQuotes = true;
|
|
348
|
-
quoteChar = char;
|
|
349
|
-
continue;
|
|
350
|
-
} else if (char === quoteChar && inQuotes) {
|
|
351
|
-
inQuotes = false;
|
|
352
|
-
quoteChar = '';
|
|
353
|
-
continue;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Handle spaces (argument separator)
|
|
357
|
-
if (char === ' ' && !inQuotes) {
|
|
358
|
-
if (current) {
|
|
359
|
-
args.push(current);
|
|
360
|
-
current = '';
|
|
229
|
+
|
|
230
|
+
restoreReadline() {}
|
|
231
|
+
|
|
232
|
+
// ─── Execute ──────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
async execute(input) {
|
|
235
|
+
if (!input.trim()) return;
|
|
236
|
+
|
|
237
|
+
const args = this.parseCommand(input);
|
|
238
|
+
const cmdName = args[0].toLowerCase();
|
|
239
|
+
const cmdArgs = args.slice(1);
|
|
240
|
+
|
|
241
|
+
if (this.commands.has(cmdName)) {
|
|
242
|
+
const cmd = this.commands.get(cmdName);
|
|
243
|
+
try {
|
|
244
|
+
const result = await cmd.handler(cmdArgs, this);
|
|
245
|
+
if (result !== undefined) console.log(result);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.log(chalk.red(`Error: ${err.message}`));
|
|
361
248
|
}
|
|
362
|
-
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (this.commandRegistry) {
|
|
253
|
+
const handled = await this.commandRegistry.execute(input, this);
|
|
254
|
+
if (handled) return;
|
|
363
255
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
current += char;
|
|
256
|
+
|
|
257
|
+
await this.executeSystemCommand(input);
|
|
367
258
|
}
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
259
|
+
|
|
260
|
+
// ─── Prompt ───────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
getPromptText() {
|
|
263
|
+
let p = '';
|
|
264
|
+
const room = this.ws.getCurrentRoom?.();
|
|
265
|
+
if (room) p += `[${room}] `;
|
|
266
|
+
if (this.ws.isConnected?.()) p += '● ';
|
|
267
|
+
|
|
268
|
+
let displayPath = this.currentDir;
|
|
269
|
+
const homeDir = os.homedir();
|
|
270
|
+
|
|
271
|
+
if (process.platform === 'win32') {
|
|
272
|
+
if (displayPath.toLowerCase() === homeDir.toLowerCase()) displayPath = '~';
|
|
273
|
+
else if (displayPath.toLowerCase().startsWith(homeDir.toLowerCase()))
|
|
274
|
+
displayPath = '~' + displayPath.slice(homeDir.length).replace(/\\/g, '/');
|
|
275
|
+
else displayPath = displayPath.replace(/\\/g, '/');
|
|
276
|
+
} else {
|
|
277
|
+
displayPath = displayPath.replace(homeDir, '~');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
p += `${this.username}@thinknsh:${displayPath} $`;
|
|
281
|
+
return p;
|
|
372
282
|
}
|
|
373
|
-
|
|
374
|
-
return
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
283
|
+
|
|
284
|
+
getPrompt() { return this.getPromptText(); }
|
|
285
|
+
|
|
286
|
+
printMessage(msg) { console.log(msg); }
|
|
287
|
+
|
|
288
|
+
// ─── Built-ins ────────────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
registerBuiltinCommands() {
|
|
291
|
+
this.registerCommand('help', this.helpCommand.bind(this), 'Show available commands');
|
|
292
|
+
this.registerCommand('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
293
|
+
this.registerCommand('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
294
|
+
this.registerCommand('clear', () => console.clear(), 'Clear screen');
|
|
295
|
+
this.registerCommand('history', this.historyCommand.bind(this), 'Show command history');
|
|
296
|
+
this.registerCommand('cd', this.cdCommand.bind(this), 'Change directory');
|
|
297
|
+
this.registerCommand('pwd', () => console.log(process.cwd()), 'Print working directory');
|
|
298
|
+
this.registerCommand('ls', this.lsCommand.bind(this), 'List files');
|
|
299
|
+
this.registerCommand('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
|
|
300
|
+
this.registerCommand('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
|
|
301
|
+
this.registerCommand('status', this.statusCommand.bind(this), 'Show status');
|
|
302
|
+
this.registerCommand('join', this.joinCommand.bind(this), 'Join a room');
|
|
303
|
+
this.registerCommand('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
|
|
304
|
+
this.registerCommand('rooms', this.roomsCommand.bind(this), 'List rooms');
|
|
305
|
+
this.registerCommand('users', this.usersCommand.bind(this), 'List users in room');
|
|
306
|
+
this.registerCommand('say', this.sayCommand.bind(this), 'Send a message');
|
|
307
|
+
this.registerCommand('msg', this.sayCommand.bind(this), 'Send a message (alias)');
|
|
308
|
+
this.registerCommand('notify', this.notifyCommand.bind(this), 'Send a notification');
|
|
309
|
+
this.registerCommand('alias', this.aliasCommand.bind(this), 'Create an alias');
|
|
310
|
+
this.registerCommand('unalias', this.unaliasCommand.bind(this), 'Remove an alias');
|
|
311
|
+
this.registerCommand('set', this.setCommand.bind(this), 'Set a variable');
|
|
312
|
+
this.registerCommand('unset', this.unsetCommand.bind(this), 'Unset a variable');
|
|
313
|
+
|
|
314
|
+
this.addAlias('ll', 'ls -la');
|
|
315
|
+
this.addAlias('..', 'cd ..');
|
|
316
|
+
this.addAlias('...', 'cd ../..');
|
|
317
|
+
this.addAlias('~', 'cd ~');
|
|
404
318
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
async helpCommand(
|
|
410
|
-
console.log(chalk.cyan('\n📚 Available Commands:'));
|
|
411
|
-
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
412
|
-
|
|
319
|
+
|
|
320
|
+
registerCommand(name, handler, description = '') { this.commands.set(name, { handler, description }); }
|
|
321
|
+
addAlias(alias, command) { this.aliases.set(alias, command); }
|
|
322
|
+
|
|
323
|
+
async helpCommand() {
|
|
413
324
|
const categories = {
|
|
414
|
-
'Core':
|
|
415
|
-
'Filesystem':
|
|
416
|
-
'WebSocket':
|
|
417
|
-
'Rooms':
|
|
418
|
-
'Messages':
|
|
325
|
+
'Core': ['help', 'exit', 'quit', 'clear', 'history'],
|
|
326
|
+
'Filesystem': ['cd', 'pwd', 'ls'],
|
|
327
|
+
'WebSocket': ['connect', 'disconnect', 'status'],
|
|
328
|
+
'Rooms': ['join', 'leave', 'rooms', 'users'],
|
|
329
|
+
'Messages': ['say', 'msg'],
|
|
419
330
|
'Notifications': ['notify'],
|
|
420
|
-
'Aliases':
|
|
421
|
-
'Variables':
|
|
331
|
+
'Aliases': ['alias', 'unalias'],
|
|
332
|
+
'Variables': ['set', 'unset']
|
|
422
333
|
};
|
|
423
|
-
|
|
334
|
+
console.log(chalk.cyan('\n📚 Available Commands:'));
|
|
335
|
+
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
424
336
|
for (const [category, cmds] of Object.entries(categories)) {
|
|
425
337
|
console.log(chalk.yellow(`\n${category}:`));
|
|
426
338
|
cmds.forEach(cmd => {
|
|
427
|
-
const
|
|
428
|
-
if (
|
|
429
|
-
console.log(` ${chalk.green(cmd.padEnd(12))} - ${command.description}`);
|
|
430
|
-
}
|
|
339
|
+
const c = this.commands.get(cmd);
|
|
340
|
+
if (c) console.log(` ${chalk.green(cmd.padEnd(12))} - ${c.description}`);
|
|
431
341
|
});
|
|
432
342
|
}
|
|
433
|
-
|
|
434
|
-
|
|
343
|
+
console.log(chalk.dim('\nAny other command runs in your system shell\n'));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
historyCommand() {
|
|
347
|
+
this.history.forEach((cmd, i) => console.log(`${(i + 1).toString().padStart(4)} ${cmd}`));
|
|
435
348
|
}
|
|
436
|
-
|
|
437
|
-
async cdCommand(args) {
|
|
438
|
-
|
|
439
|
-
const target = args.join(' ').trim() || os.homedir();
|
|
440
|
-
|
|
441
|
-
if (!target) {
|
|
442
|
-
// No args, go home
|
|
349
|
+
|
|
350
|
+
async cdCommand(args) {
|
|
351
|
+
const target = args.join(' ').trim() || os.homedir();
|
|
443
352
|
try {
|
|
444
|
-
|
|
353
|
+
let newPath;
|
|
354
|
+
if (target === '~' || target === '~/' || target === '~\\') newPath = os.homedir();
|
|
355
|
+
else if (target.startsWith('~/') || target.startsWith('~\\')) newPath = path.join(os.homedir(), target.slice(2));
|
|
356
|
+
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
|
+
else newPath = path.resolve(this.currentDir, target);
|
|
360
|
+
newPath = path.normalize(newPath);
|
|
361
|
+
const stats = await fs.stat(newPath);
|
|
362
|
+
if (!stats.isDirectory()) { console.log(chalk.red(`cd: not a directory: ${target}`)); return; }
|
|
363
|
+
this.previousDir = this.currentDir;
|
|
364
|
+
process.chdir(newPath);
|
|
445
365
|
this.currentDir = process.cwd();
|
|
446
|
-
} catch (
|
|
447
|
-
console.log(chalk.red(`cd: ${error.message}`));
|
|
448
|
-
}
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
let newPath;
|
|
454
|
-
|
|
455
|
-
// Handle special cases
|
|
456
|
-
if (target === '~' || target === '~/' || target === '~\\') {
|
|
457
|
-
newPath = os.homedir();
|
|
458
|
-
} else if (target.startsWith('~/') || target.startsWith('~\\')) {
|
|
459
|
-
// Handle ~/path with possible spaces
|
|
460
|
-
const rest = target.slice(2);
|
|
461
|
-
newPath = path.join(os.homedir(), rest);
|
|
462
|
-
} else if (target === '..') {
|
|
463
|
-
newPath = path.dirname(this.currentDir);
|
|
464
|
-
} else if (target === '.') {
|
|
465
|
-
newPath = this.currentDir;
|
|
466
|
-
} else if (target === '-') {
|
|
467
|
-
// Go to previous directory
|
|
468
|
-
newPath = this.previousDir || this.currentDir;
|
|
469
|
-
} else {
|
|
470
|
-
// Handle Windows paths with spaces
|
|
471
|
-
if (process.platform === 'win32') {
|
|
472
|
-
// Check if it's an absolute path (with drive letter)
|
|
473
|
-
if (target.match(/^[a-zA-Z]:[\\/]/) || target.match(/^[a-zA-Z]:$/)) {
|
|
474
|
-
newPath = target;
|
|
475
|
-
} else {
|
|
476
|
-
// Relative path - resolve from current directory
|
|
477
|
-
newPath = path.resolve(this.currentDir, target);
|
|
478
|
-
}
|
|
479
|
-
} else {
|
|
480
|
-
newPath = path.resolve(this.currentDir, target);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
// Normalize path (fixes any // or \/ issues)
|
|
485
|
-
newPath = path.normalize(newPath);
|
|
486
|
-
|
|
487
|
-
// Check if directory exists
|
|
488
|
-
await fs.access(newPath);
|
|
489
|
-
const stats = await fs.stat(newPath);
|
|
490
|
-
|
|
491
|
-
if (!stats.isDirectory()) {
|
|
492
|
-
console.log(chalk.red(`cd: ${target}: Not a directory`));
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Store previous directory
|
|
497
|
-
this.previousDir = this.currentDir;
|
|
498
|
-
|
|
499
|
-
// Change directory
|
|
500
|
-
process.chdir(newPath);
|
|
501
|
-
this.currentDir = process.cwd();
|
|
502
|
-
|
|
503
|
-
} catch (error) {
|
|
504
|
-
console.log(chalk.red(`cd: ${target}: No such directory`));
|
|
366
|
+
} catch { console.log(chalk.red(`cd: no such directory: ${target}`)); }
|
|
505
367
|
}
|
|
506
|
-
|
|
507
|
-
|
|
368
|
+
|
|
508
369
|
async lsCommand(args) {
|
|
509
370
|
try {
|
|
510
|
-
const files
|
|
511
|
-
const showAll
|
|
512
|
-
|
|
371
|
+
const files = await fs.readdir(this.currentDir);
|
|
372
|
+
const showAll = args.includes('-a') || args.includes('--all');
|
|
513
373
|
const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
size: stat.size,
|
|
523
|
-
mode: stat.mode
|
|
524
|
-
};
|
|
525
|
-
})
|
|
526
|
-
);
|
|
527
|
-
|
|
528
|
-
// Colorize output
|
|
529
|
-
fileList.forEach(file => {
|
|
530
|
-
if (file.isDir) {
|
|
531
|
-
console.log(chalk.blue(file.name + '/'));
|
|
532
|
-
} else if (file.mode & 0o111) {
|
|
533
|
-
console.log(chalk.green(file.name + '*'));
|
|
534
|
-
} else {
|
|
535
|
-
console.log(file.name);
|
|
536
|
-
}
|
|
374
|
+
const fileList = await Promise.all(filtered.map(async (file) => {
|
|
375
|
+
const stat = await fs.stat(path.join(this.currentDir, file));
|
|
376
|
+
return { name: file, isDir: stat.isDirectory(), mode: stat.mode };
|
|
377
|
+
}));
|
|
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);
|
|
537
382
|
});
|
|
538
|
-
} catch (
|
|
539
|
-
console.log(chalk.red(`ls: ${error.message}`));
|
|
540
|
-
}
|
|
383
|
+
} catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
|
|
541
384
|
}
|
|
542
|
-
|
|
385
|
+
|
|
543
386
|
async connectCommand(args) {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
console.log(chalk.
|
|
547
|
-
|
|
548
|
-
try {
|
|
549
|
-
await this.ws.connect();
|
|
550
|
-
} catch (error) {
|
|
551
|
-
console.log(chalk.red(`❌ Connection failed: ${error.message}`));
|
|
552
|
-
}
|
|
387
|
+
console.log(chalk.cyan(`🔌 Connecting...`));
|
|
388
|
+
try { await this.ws.connect(); }
|
|
389
|
+
catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
|
|
553
390
|
}
|
|
554
|
-
|
|
391
|
+
|
|
555
392
|
async statusCommand() {
|
|
556
|
-
console.log(chalk.cyan('\n📊 Status:'));
|
|
557
|
-
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
558
|
-
|
|
559
393
|
const connected = this.ws.isConnected();
|
|
394
|
+
console.log(chalk.cyan('\n📊 Status:'));
|
|
560
395
|
console.log(`WebSocket: ${connected ? chalk.green('✓ Connected') : chalk.red('✗ Disconnected')}`);
|
|
561
|
-
|
|
562
396
|
if (connected) {
|
|
563
397
|
console.log(`Socket ID: ${chalk.dim(this.ws.getSocketId())}`);
|
|
564
|
-
console.log(`Server:
|
|
398
|
+
console.log(`Server: ${chalk.dim(this.ws.config.serverUrl)}`);
|
|
565
399
|
}
|
|
566
|
-
|
|
567
400
|
const room = this.ws.getCurrentRoom();
|
|
568
|
-
console.log(`Room:
|
|
569
|
-
|
|
570
|
-
console.log(`Current Directory: ${chalk.blue(this.currentDir)}`);
|
|
571
|
-
console.log(`History: ${chalk.yellow(this.history.length)} commands`);
|
|
572
|
-
console.log(`Commands: ${chalk.green(this.commands.size)} registered`);
|
|
573
|
-
console.log(`Aliases: ${chalk.green(this.aliases.size)} defined`);
|
|
401
|
+
console.log(`Room: ${room ? chalk.green(room) : chalk.yellow('None')}`);
|
|
402
|
+
console.log(`Directory: ${chalk.blue(this.currentDir)}`);
|
|
574
403
|
}
|
|
575
|
-
|
|
404
|
+
|
|
576
405
|
async joinCommand(args) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
console.log(chalk.red('Usage: join <room-id> [password]'));
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const password = args[1];
|
|
585
|
-
|
|
586
|
-
if (!this.ws.isConnected()) {
|
|
587
|
-
console.log(chalk.yellow('Not connected. Connecting first...'));
|
|
588
|
-
await this.ws.connect();
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
console.log(chalk.cyan(`🔗 Joining room: ${roomId}...`));
|
|
592
|
-
|
|
406
|
+
if (!args[0]) { console.log(chalk.red('Usage: join <room-id> [password]')); return; }
|
|
407
|
+
if (!this.ws.isConnected()) { console.log(chalk.yellow('Connecting...')); await this.ws.connect(); }
|
|
408
|
+
console.log(chalk.cyan(`🔗 Joining: ${args[0]}...`));
|
|
593
409
|
try {
|
|
594
|
-
const result = await this.ws.joinRoom(
|
|
595
|
-
console.log(chalk.green(`✅ Joined
|
|
596
|
-
|
|
597
|
-
} catch (error) {
|
|
598
|
-
console.log(chalk.red(`❌ Failed to join: ${error.message}`));
|
|
599
|
-
}
|
|
410
|
+
const result = await this.ws.joinRoom(args[0], args[1]);
|
|
411
|
+
console.log(chalk.green(`✅ Joined: ${result.roomName || args[0]}`));
|
|
412
|
+
} catch (err) { console.log(chalk.red(`❌ ${err.message}`)); }
|
|
600
413
|
}
|
|
601
|
-
|
|
414
|
+
|
|
602
415
|
async roomsCommand() {
|
|
603
|
-
if (!this.ws.isConnected()) {
|
|
604
|
-
console.log(chalk.yellow('Not connected to WebSocket server'));
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
console.log(chalk.cyan('\n📋 Available Rooms:'));
|
|
609
|
-
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
610
|
-
|
|
611
|
-
// Request rooms list
|
|
416
|
+
if (!this.ws.isConnected()) { console.log(chalk.yellow('Not connected')); return; }
|
|
612
417
|
this.ws.socket.emit('rooms:list');
|
|
613
|
-
|
|
614
418
|
this.ws.socket.once('rooms:list', (data) => {
|
|
615
|
-
if (data.rooms
|
|
616
|
-
|
|
617
|
-
const current = room.id === this.ws.getCurrentRoom() ? chalk.green(' (current)') : '';
|
|
618
|
-
console.log(` ${chalk.cyan(room.name || room.id)}${current}`);
|
|
619
|
-
if (room.description) {
|
|
620
|
-
console.log(` ${chalk.dim(room.description)}`);
|
|
621
|
-
}
|
|
622
|
-
console.log(` ${chalk.yellow('👥 ' + (room.participantCount || 0))} participants`);
|
|
623
|
-
});
|
|
624
|
-
} else {
|
|
625
|
-
console.log(' No rooms available');
|
|
626
|
-
}
|
|
419
|
+
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 available');
|
|
627
421
|
});
|
|
628
422
|
}
|
|
629
|
-
|
|
423
|
+
|
|
630
424
|
async usersCommand() {
|
|
631
|
-
if (!this.ws.isConnected()) {
|
|
632
|
-
console.log(chalk.yellow('Not connected'));
|
|
633
|
-
return;
|
|
634
|
-
}
|
|
635
|
-
|
|
425
|
+
if (!this.ws.isConnected()) { console.log(chalk.yellow('Not connected')); return; }
|
|
636
426
|
const room = this.ws.getCurrentRoom();
|
|
637
|
-
if (!room) {
|
|
638
|
-
console.log(chalk.yellow('Not in a room'));
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
console.log(chalk.cyan(`\n👥 Users in ${room}:`));
|
|
643
|
-
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
644
|
-
|
|
427
|
+
if (!room) { console.log(chalk.yellow('Not in a room')); return; }
|
|
645
428
|
try {
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
console.log(` ${chalk.green('●')} ${user.username}${typing}`);
|
|
650
|
-
if (user.status) {
|
|
651
|
-
console.log(` ${chalk.dim(user.status)}`);
|
|
652
|
-
}
|
|
653
|
-
});
|
|
654
|
-
} catch (error) {
|
|
655
|
-
console.log(chalk.red(`Failed to get users: ${error.message}`));
|
|
656
|
-
}
|
|
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}`)); }
|
|
657
432
|
}
|
|
658
|
-
|
|
433
|
+
|
|
659
434
|
async sayCommand(args) {
|
|
660
435
|
const message = args.join(' ');
|
|
661
|
-
|
|
662
|
-
if (!message) {
|
|
663
|
-
console.log(chalk.red('Usage: say <message>'));
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
436
|
+
if (!message) { console.log(chalk.red('Usage: say <message>')); return; }
|
|
667
437
|
try {
|
|
668
438
|
await this.ws.sendMessage(message);
|
|
669
|
-
|
|
670
|
-
// Clear typing indicator
|
|
671
|
-
if (this.isTyping) {
|
|
672
|
-
this.ws.sendTyping(false);
|
|
673
|
-
this.isTyping = false;
|
|
674
|
-
clearTimeout(this.typingTimeout);
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// Echo locally
|
|
678
439
|
console.log(chalk.green(`You: ${message}`));
|
|
679
|
-
} catch (
|
|
680
|
-
console.log(chalk.red(`Failed to send: ${error.message}`));
|
|
681
|
-
}
|
|
440
|
+
} catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
|
|
682
441
|
}
|
|
683
|
-
|
|
442
|
+
|
|
684
443
|
async notifyCommand(args) {
|
|
685
444
|
const type = args[0] || 'info';
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
if (!message) {
|
|
689
|
-
console.log(chalk.red('Usage: notify <type> <message>'));
|
|
690
|
-
console.log(chalk.dim('Types: info, success, warning, error'));
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
|
|
445
|
+
const msg = args.slice(1).join(' ');
|
|
446
|
+
if (!msg) { console.log(chalk.red('Usage: notify <type> <message>')); return; }
|
|
694
447
|
try {
|
|
695
|
-
await this.ws.sendNotification({
|
|
696
|
-
type,
|
|
697
|
-
title: 'Shell Notification',
|
|
698
|
-
message
|
|
699
|
-
});
|
|
700
|
-
|
|
448
|
+
await this.ws.sendNotification({ type, title: 'Shell Notification', message: msg });
|
|
701
449
|
console.log(chalk.green('✅ Notification sent'));
|
|
702
|
-
} catch (
|
|
703
|
-
console.log(chalk.red(`Failed to send notification: ${error.message}`));
|
|
704
|
-
}
|
|
450
|
+
} catch (err) { console.log(chalk.red(`Failed: ${err.message}`)); }
|
|
705
451
|
}
|
|
706
|
-
|
|
452
|
+
|
|
707
453
|
async aliasCommand(args) {
|
|
708
|
-
if (args.length
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (this.aliases.size === 0) {
|
|
714
|
-
console.log(' No aliases defined');
|
|
715
|
-
} else {
|
|
716
|
-
for (const [alias, cmd] of this.aliases) {
|
|
717
|
-
console.log(` ${chalk.green(alias.padEnd(12))} → ${chalk.dim(cmd)}`);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
return;
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
if (args.length < 2) {
|
|
724
|
-
console.log(chalk.red('Usage: alias <name> <command>'));
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const name = args[0];
|
|
729
|
-
const command = args.slice(1).join(' ');
|
|
730
|
-
|
|
731
|
-
this.addAlias(name, command);
|
|
732
|
-
console.log(chalk.green(`✅ Alias created: ${name} → ${command}`));
|
|
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(' ')}`));
|
|
733
458
|
}
|
|
734
|
-
|
|
459
|
+
|
|
735
460
|
async unaliasCommand(args) {
|
|
736
|
-
if (args.
|
|
737
|
-
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
const name = args[0];
|
|
742
|
-
|
|
743
|
-
if (this.aliases.delete(name)) {
|
|
744
|
-
console.log(chalk.green(`✅ Alias removed: ${name}`));
|
|
745
|
-
} else {
|
|
746
|
-
console.log(chalk.red(`Alias not found: ${name}`));
|
|
747
|
-
}
|
|
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]}`));
|
|
748
463
|
}
|
|
749
|
-
|
|
464
|
+
|
|
750
465
|
async setCommand(args) {
|
|
751
|
-
if (args.length
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (this.variables.size === 0) {
|
|
757
|
-
console.log(' No variables set');
|
|
758
|
-
} else {
|
|
759
|
-
for (const [key, value] of this.variables) {
|
|
760
|
-
console.log(` ${chalk.green(key.padEnd(12))} = ${chalk.yellow(JSON.stringify(value))}`);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
if (args.length < 2) {
|
|
767
|
-
console.log(chalk.red('Usage: set <name> <value>'));
|
|
768
|
-
return;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const name = args[0];
|
|
772
|
-
const value = args.slice(1).join(' ');
|
|
773
|
-
|
|
774
|
-
this.variables.set(name, value);
|
|
775
|
-
console.log(chalk.green(`✅ Variable set: ${name} = ${value}`));
|
|
466
|
+
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
|
+
this.variables.set(args[0], args.slice(1).join(' '));
|
|
469
|
+
console.log(chalk.green(`✅ ${args[0]} = ${args.slice(1).join(' ')}`));
|
|
776
470
|
}
|
|
777
|
-
|
|
471
|
+
|
|
778
472
|
async unsetCommand(args) {
|
|
779
|
-
if (args.
|
|
780
|
-
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const name = args[0];
|
|
785
|
-
|
|
786
|
-
if (this.variables.delete(name)) {
|
|
787
|
-
console.log(chalk.green(`✅ Variable removed: ${name}`));
|
|
788
|
-
} else {
|
|
789
|
-
console.log(chalk.red(`Variable not found: ${name}`));
|
|
790
|
-
}
|
|
473
|
+
if (!args[0]) { console.log(chalk.red('Usage: unset <n>')); return; }
|
|
474
|
+
this.variables.delete(args[0]) ? console.log(chalk.green(`✅ Removed`)) : console.log(chalk.red(`Not found`));
|
|
791
475
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
historyCommand() {
|
|
806
|
-
this.history.forEach((cmd, i) => {
|
|
807
|
-
console.log(`${(i + 1).toString().padStart(4)} ${cmd}`);
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
/**
|
|
812
|
-
* Handle incoming messages
|
|
813
|
-
*/
|
|
814
|
-
handleIncomingMessage(data) {
|
|
815
|
-
// Clear current line
|
|
816
|
-
readline.clearLine(process.stdout, 0);
|
|
817
|
-
readline.cursorTo(process.stdout, 0);
|
|
818
|
-
|
|
819
|
-
// Show message
|
|
820
|
-
const prefix = data.username === this.username ? 'You' : data.username;
|
|
821
|
-
console.log(`${chalk.cyan(`[${prefix}]`)} ${data.message}`);
|
|
822
|
-
|
|
823
|
-
// Redraw prompt
|
|
824
|
-
this.rl.prompt(true);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
/**
|
|
828
|
-
* Handle notifications
|
|
829
|
-
*/
|
|
830
|
-
handleNotification(data) {
|
|
831
|
-
readline.clearLine(process.stdout, 0);
|
|
832
|
-
readline.cursorTo(process.stdout, 0);
|
|
833
|
-
|
|
834
|
-
let color = chalk.blue;
|
|
835
|
-
switch (data.type) {
|
|
836
|
-
case 'success': color = chalk.green; break;
|
|
837
|
-
case 'warning': color = chalk.yellow; break;
|
|
838
|
-
case 'error': color = chalk.red; break;
|
|
839
|
-
default: color = chalk.blue;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
console.log(color(`📢 ${data.title}: ${data.message}`));
|
|
843
|
-
this.rl.prompt(true);
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Show typing indicator
|
|
848
|
-
*/
|
|
849
|
-
showTypingIndicator(username) {
|
|
850
|
-
readline.clearLine(process.stdout, 0);
|
|
851
|
-
readline.cursorTo(process.stdout, 0);
|
|
852
|
-
console.log(chalk.dim(`👤 ${username} is typing...`));
|
|
853
|
-
this.rl.prompt(true);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
/**
|
|
857
|
-
* Hide typing indicator
|
|
858
|
-
*/
|
|
859
|
-
hideTypingIndicator(username) {
|
|
860
|
-
// Just redraw prompt
|
|
861
|
-
this.rl.prompt(true);
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* Handle typing detection
|
|
866
|
-
*/
|
|
867
|
-
handleTyping() {
|
|
868
|
-
if (!this.isTyping && this.ws.getCurrentRoom()) {
|
|
869
|
-
this.isTyping = true;
|
|
870
|
-
this.ws.sendTyping(true);
|
|
476
|
+
|
|
477
|
+
getSessionPath() { return path.join(this.config.configDir, 'session.json'); }
|
|
478
|
+
|
|
479
|
+
parseCommand(input) {
|
|
480
|
+
const args = [];
|
|
481
|
+
let current = '', inQuotes = false, quoteChar = '', escapeNext = false;
|
|
482
|
+
for (const char of input) {
|
|
483
|
+
if (char === '\\' && !escapeNext && !inQuotes) { escapeNext = true; continue; }
|
|
484
|
+
if (escapeNext) { current += char; escapeNext = false; continue; }
|
|
485
|
+
if ((char === '"' || char === "'") && !inQuotes) { inQuotes = true; quoteChar = char; continue; }
|
|
486
|
+
if (char === quoteChar && inQuotes) { inQuotes = false; quoteChar = ''; continue; }
|
|
487
|
+
if (char === ' ' && !inQuotes) { if (current) { args.push(current); current = ''; } continue; }
|
|
488
|
+
current += char;
|
|
871
489
|
}
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
this.typingTimeout = setTimeout(() => {
|
|
875
|
-
if (this.isTyping) {
|
|
876
|
-
this.isTyping = false;
|
|
877
|
-
this.ws.sendTyping(false);
|
|
878
|
-
}
|
|
879
|
-
}, 2000);
|
|
490
|
+
if (current) args.push(current);
|
|
491
|
+
return args;
|
|
880
492
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
493
|
+
|
|
494
|
+
executeSystemCommand(command) {
|
|
495
|
+
return new Promise((resolve) => {
|
|
496
|
+
const child = spawn(command, [], {
|
|
497
|
+
cwd: this.currentDir, shell: true, stdio: 'inherit',
|
|
498
|
+
env: { ...process.env, THINKNCOLLAB_SHELL: '1' }
|
|
499
|
+
});
|
|
500
|
+
child.on('close', () => resolve());
|
|
501
|
+
child.on('error', () => { console.log(chalk.red(`Command not found: ${command}`)); resolve(); });
|
|
502
|
+
});
|
|
890
503
|
}
|
|
891
|
-
|
|
892
|
-
/**
|
|
893
|
-
* Show welcome message
|
|
894
|
-
*/
|
|
504
|
+
|
|
895
505
|
showWelcome() {
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
horizontalLayout: 'default',
|
|
901
|
-
verticalLayout: 'default'
|
|
902
|
-
})));
|
|
903
|
-
|
|
506
|
+
try {
|
|
507
|
+
const figlet = require('figlet');
|
|
508
|
+
console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' })));
|
|
509
|
+
} catch {}
|
|
904
510
|
console.log(chalk.cyan(`
|
|
905
511
|
╔══════════════════════════════════════════════════════╗
|
|
906
|
-
║ ThinkNCollab Shell
|
|
907
|
-
║ Type 'help' for commands
|
|
908
|
-
|
|
909
|
-
`));
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Command completer
|
|
914
|
-
*/
|
|
915
|
-
completer(line) {
|
|
916
|
-
const commands = Array.from(this.commands.keys());
|
|
917
|
-
const aliases = Array.from(this.aliases.keys());
|
|
918
|
-
const all = [...commands, ...aliases];
|
|
919
|
-
|
|
920
|
-
const hits = all.filter(cmd => cmd.startsWith(line));
|
|
921
|
-
return [hits.length ? hits : all, line];
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Handle interrupt (Ctrl+C)
|
|
926
|
-
*/
|
|
927
|
-
handleInterrupt() {
|
|
928
|
-
if (this.isTyping) {
|
|
929
|
-
this.isTyping = false;
|
|
930
|
-
this.ws.sendTyping(false);
|
|
931
|
-
clearTimeout(this.typingTimeout);
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
console.log(chalk.yellow('\nUse "exit" to quit, or press Ctrl+C again'));
|
|
512
|
+
║ ThinkNCollab Shell v0.0.03 ║
|
|
513
|
+
║ Type 'help' for commands ║
|
|
514
|
+
╚══════════════════════════════════════════════════════╝`));
|
|
935
515
|
}
|
|
936
|
-
|
|
937
|
-
/**
|
|
938
|
-
* Load command history
|
|
939
|
-
*/
|
|
516
|
+
|
|
940
517
|
loadHistory() {
|
|
941
518
|
try {
|
|
942
|
-
if (fs.existsSync(this.config.historyFile))
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
} catch (error) {
|
|
947
|
-
// Ignore
|
|
948
|
-
}
|
|
519
|
+
if (fs.existsSync(this.config.historyFile))
|
|
520
|
+
this.history = fs.readFileSync(this.config.historyFile, 'utf8').split('\n').filter(l => l.trim());
|
|
521
|
+
} catch {}
|
|
949
522
|
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Save command history
|
|
953
|
-
*/
|
|
523
|
+
|
|
954
524
|
saveHistory() {
|
|
955
|
-
try {
|
|
956
|
-
const toSave = this.history.slice(-this.config.maxHistory);
|
|
957
|
-
fs.writeFileSync(this.config.historyFile, toSave.join('\n'));
|
|
958
|
-
} catch (error) {
|
|
959
|
-
// Ignore
|
|
960
|
-
}
|
|
525
|
+
try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
|
|
961
526
|
}
|
|
962
|
-
|
|
963
|
-
/**
|
|
964
|
-
* Cleanup before exit
|
|
965
|
-
*/
|
|
527
|
+
|
|
966
528
|
cleanup() {
|
|
967
|
-
if (this.
|
|
968
|
-
|
|
969
|
-
}
|
|
529
|
+
if (this.tcpServer) { try { this.tcpServer.close(); } catch {} }
|
|
530
|
+
if (this.ws) this.ws.disconnect();
|
|
970
531
|
this.saveHistory();
|
|
971
532
|
}
|
|
972
533
|
}
|