thinkncollab-shell 0.0.7 ā 0.0.9
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 +12 -10
- package/package.json +1 -1
- package/src/commands/auth/login.js +76 -76
- package/src/commands/auth/logout.js +31 -31
- package/src/commands/auth/register.js +51 -51
- package/src/commands/auth/whoami.js +51 -51
- package/src/commands/index.js +129 -129
- package/src/commands/message/say.js +42 -42
- package/src/commands/room/join.js +141 -141
- package/src/commands/room/leave.js +32 -32
- package/src/commands/room/rooms.js +65 -65
- package/src/commands/shell/notify.js +87 -87
- package/src/commands/shell/status.js +72 -72
- package/src/commands/tasks/complete.js +46 -46
- package/src/commands/tasks/create-task.js +100 -100
- package/src/commands/tasks/task-status.js +60 -60
- package/src/commands/tasks/task.js +74 -74
- package/src/commands/tasks/tasks.js +182 -182
- package/src/commands/team/invite.js +45 -45
- package/src/commands/team/myteam.js +77 -77
- package/src/commands/team/teammates.js +51 -51
- package/src/commands/terminal/share.js +251 -251
- package/src/core/api-client.js +245 -245
- package/src/core/project-config.js +99 -99
- package/src/core/shell.js +168 -39
- package/src/core/websocket.js +3 -3
- package/src/events/notifications.js +15 -28
- package/src/utils/logger.js +10 -20
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const chalk = require('chalk');
|
|
9
|
-
|
|
10
|
-
module.exports = {
|
|
11
|
-
name: 'teammates',
|
|
12
|
-
description: 'List online teammates',
|
|
13
|
-
aliases: ['members', 'online'],
|
|
14
|
-
requiresAuth: true,
|
|
15
|
-
|
|
16
|
-
async execute(args, shell) {
|
|
17
|
-
const filter = args[0] ? args[0].toLowerCase() : null;
|
|
18
|
-
|
|
19
|
-
console.log(chalk.cyan('\nš„ Online Teammates:'));
|
|
20
|
-
console.log(chalk.dim('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const teammates = await shell.api.getTeammates();
|
|
24
|
-
|
|
25
|
-
const online = teammates.filter(t => t.online);
|
|
26
|
-
const offline = teammates.filter(t => !t.online);
|
|
27
|
-
|
|
28
|
-
if (online.length === 0) {
|
|
29
|
-
console.log(chalk.yellow(' No teammates online'));
|
|
30
|
-
} else {
|
|
31
|
-
online
|
|
32
|
-
.filter(t => !filter || t.name.toLowerCase().includes(filter) || t.email.toLowerCase().includes(filter))
|
|
33
|
-
.forEach(t => {
|
|
34
|
-
const typing = t.typing ? chalk.green(' (typing...)') : '';
|
|
35
|
-
const room = t.currentRoom ? chalk.dim(` in ${t.currentRoom}`) : '';
|
|
36
|
-
console.log(` ${chalk.green('ā')} ${chalk.white(t.name || t.email)}${typing}${room}`);
|
|
37
|
-
|
|
38
|
-
if (t.status && t.status !== 'online') {
|
|
39
|
-
console.log(chalk.dim(` ${t.status}`));
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (offline.length > 0 && !filter) {
|
|
45
|
-
console.log(chalk.dim(`\n ... and ${offline.length} offline`));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
} catch (error) {
|
|
49
|
-
console.log(chalk.red(`ā Failed to fetch teammates: ${error.message}`));
|
|
50
|
-
}
|
|
51
|
-
}
|
|
1
|
+
/*
|
|
2
|
+
Teammates Command
|
|
3
|
+
|
|
4
|
+
Usage: teammates
|
|
5
|
+
teammates [name]
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
name: 'teammates',
|
|
12
|
+
description: 'List online teammates',
|
|
13
|
+
aliases: ['members', 'online'],
|
|
14
|
+
requiresAuth: true,
|
|
15
|
+
|
|
16
|
+
async execute(args, shell) {
|
|
17
|
+
const filter = args[0] ? args[0].toLowerCase() : null;
|
|
18
|
+
|
|
19
|
+
console.log(chalk.cyan('\nš„ Online Teammates:'));
|
|
20
|
+
console.log(chalk.dim('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const teammates = await shell.api.getTeammates();
|
|
24
|
+
|
|
25
|
+
const online = teammates.filter(t => t.online);
|
|
26
|
+
const offline = teammates.filter(t => !t.online);
|
|
27
|
+
|
|
28
|
+
if (online.length === 0) {
|
|
29
|
+
console.log(chalk.yellow(' No teammates online'));
|
|
30
|
+
} else {
|
|
31
|
+
online
|
|
32
|
+
.filter(t => !filter || t.name.toLowerCase().includes(filter) || t.email.toLowerCase().includes(filter))
|
|
33
|
+
.forEach(t => {
|
|
34
|
+
const typing = t.typing ? chalk.green(' (typing...)') : '';
|
|
35
|
+
const room = t.currentRoom ? chalk.dim(` in ${t.currentRoom}`) : '';
|
|
36
|
+
console.log(` ${chalk.green('ā')} ${chalk.white(t.name || t.email)}${typing}${room}`);
|
|
37
|
+
|
|
38
|
+
if (t.status && t.status !== 'online') {
|
|
39
|
+
console.log(chalk.dim(` ${t.status}`));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (offline.length > 0 && !filter) {
|
|
45
|
+
console.log(chalk.dim(`\n ... and ${offline.length} offline`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.log(chalk.red(`ā Failed to fetch teammates: ${error.message}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
52
|
};
|
|
@@ -1,252 +1,252 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const chalk = require('chalk');
|
|
14
|
-
|
|
15
|
-
module.exports = {
|
|
16
|
-
name: 'share',
|
|
17
|
-
description: 'Share your terminal with room members',
|
|
18
|
-
aliases: ['terminal-share', 'ts'],
|
|
19
|
-
requiresAuth: true,
|
|
20
|
-
requiresRoom: true,
|
|
21
|
-
|
|
22
|
-
async execute(args, shell) {
|
|
23
|
-
const sub = (args[0] || '').toLowerCase();
|
|
24
|
-
|
|
25
|
-
switch (sub) {
|
|
26
|
-
case 'start': return cmdStart(shell);
|
|
27
|
-
case 'stop': return cmdStop(shell);
|
|
28
|
-
case 'status': return cmdStatus(shell);
|
|
29
|
-
case 'grant': return cmdGrant(args[1], shell);
|
|
30
|
-
case 'revoke': return cmdRevoke(args[1], shell);
|
|
31
|
-
case 'kick': return cmdKick(args[1], shell);
|
|
32
|
-
default:
|
|
33
|
-
printHelp();
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// āā start āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
39
|
-
async function cmdStart(shell) {
|
|
40
|
-
if (shell._shareActive) {
|
|
41
|
-
console.log(chalk.yellow('ā ļø Already sharing. Run "share stop" first.'));
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const roomId = shell.ws.getCurrentRoom();
|
|
46
|
-
if (!roomId) {
|
|
47
|
-
console.log(chalk.red('ā Join a room first: join <room-id>'));
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return new Promise((resolve) => {
|
|
52
|
-
const timeout = setTimeout(() => {
|
|
53
|
-
shell.ws.socket.off('terminal:share:started', onStarted);
|
|
54
|
-
console.log(chalk.red('ā Share start timeout'));
|
|
55
|
-
resolve();
|
|
56
|
-
}, 6000);
|
|
57
|
-
|
|
58
|
-
function onStarted(data) {
|
|
59
|
-
clearTimeout(timeout);
|
|
60
|
-
shell._shareActive = true;
|
|
61
|
-
shell._shareSessionId = data.sessionId;
|
|
62
|
-
|
|
63
|
-
console.log(chalk.green(`\nā
Terminal sharing started`));
|
|
64
|
-
console.log(chalk.dim(` Session : ${data.sessionId}`));
|
|
65
|
-
console.log(chalk.dim(` Room : ${roomId}`));
|
|
66
|
-
console.log(chalk.dim(` URL : /terminal/${data.sessionId}`));
|
|
67
|
-
console.log(chalk.dim(`\n Room members can now view your terminal.`));
|
|
68
|
-
console.log(chalk.dim(` Use "share grant <userId>" to give write access.\n`));
|
|
69
|
-
|
|
70
|
-
// Hook into shell output to stream it
|
|
71
|
-
_hookOutput(shell, data.sessionId);
|
|
72
|
-
|
|
73
|
-
resolve();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
shell.ws.socket.once('terminal:share:started', onStarted);
|
|
77
|
-
shell.ws.socket.emit('terminal:share:start', { roomId });
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// āā stop āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
82
|
-
async function cmdStop(shell) {
|
|
83
|
-
if (!shell._shareActive) {
|
|
84
|
-
console.log(chalk.yellow('ā ļø No active share session.'));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
shell.ws.socket.emit('terminal:share:stop');
|
|
89
|
-
_unhookOutput(shell);
|
|
90
|
-
shell._shareActive = false;
|
|
91
|
-
shell._shareSessionId = null;
|
|
92
|
-
console.log(chalk.green('ā
Terminal sharing stopped.'));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// āā status āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
96
|
-
async function cmdStatus(shell) {
|
|
97
|
-
if (!shell._shareActive || !shell._shareSessionId) {
|
|
98
|
-
console.log(chalk.yellow('// No active share session'));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const mem = shell._shareViewers || {};
|
|
103
|
-
const viewers = Object.values(mem);
|
|
104
|
-
|
|
105
|
-
console.log(chalk.cyan(`\n Terminal Share ā ${shell._shareSessionId}`));
|
|
106
|
-
console.log(chalk.dim(' ' + 'ā'.repeat(44)));
|
|
107
|
-
|
|
108
|
-
if (!viewers.length) {
|
|
109
|
-
console.log(chalk.dim(' No viewers yet.'));
|
|
110
|
-
} else {
|
|
111
|
-
viewers.forEach(v => {
|
|
112
|
-
const access = v.canWrite ? chalk.blue('[write]') : chalk.dim('[read]');
|
|
113
|
-
const req = v.requested ? chalk.yellow(' ā requesting write') : '';
|
|
114
|
-
console.log(` ${chalk.green('ā')} ${chalk.white(v.name)} ${access}${req}`);
|
|
115
|
-
console.log(chalk.dim(` userId: ${v.userId}`));
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
console.log();
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// āā grant āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
122
|
-
async function cmdGrant(userId, shell) {
|
|
123
|
-
if (!userId) { console.log(chalk.red('Usage: share grant <userId>')); return; }
|
|
124
|
-
if (!shell._shareActive) { console.log(chalk.red('ā No active share session')); return; }
|
|
125
|
-
|
|
126
|
-
shell.ws.socket.emit('terminal:grant', {
|
|
127
|
-
sessionId: shell._shareSessionId,
|
|
128
|
-
userId,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// Update local viewer state
|
|
132
|
-
if (shell._shareViewers?.[userId]) {
|
|
133
|
-
shell._shareViewers[userId].canWrite = true;
|
|
134
|
-
shell._shareViewers[userId].requested = false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
console.log(chalk.green(`ā
Write access granted to ${userId}`));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// āā revoke āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
141
|
-
async function cmdRevoke(userId, shell) {
|
|
142
|
-
if (!userId) { console.log(chalk.red('Usage: share revoke <userId>')); return; }
|
|
143
|
-
if (!shell._shareActive) { console.log(chalk.red('ā No active share session')); return; }
|
|
144
|
-
|
|
145
|
-
shell.ws.socket.emit('terminal:revoke', {
|
|
146
|
-
sessionId: shell._shareSessionId,
|
|
147
|
-
userId,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
if (shell._shareViewers?.[userId]) {
|
|
151
|
-
shell._shareViewers[userId].canWrite = false;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
console.log(chalk.green(`ā
Write access revoked from ${userId}`));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// āā kick āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
158
|
-
async function cmdKick(userId, shell) {
|
|
159
|
-
if (!userId) { console.log(chalk.red('Usage: share kick <userId>')); return; }
|
|
160
|
-
if (!shell._shareActive) { console.log(chalk.red('ā No active share session')); return; }
|
|
161
|
-
|
|
162
|
-
shell.ws.socket.emit('terminal:kick', {
|
|
163
|
-
sessionId: shell._shareSessionId,
|
|
164
|
-
userId,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
if (shell._shareViewers) delete shell._shareViewers[userId];
|
|
168
|
-
console.log(chalk.green(`ā
${userId} kicked from session`));
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// āā Output hooking āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
172
|
-
// Intercept process.stdout.write to stream output to viewers
|
|
173
|
-
|
|
174
|
-
let _originalWrite = null;
|
|
175
|
-
|
|
176
|
-
function _hookOutput(shell, sessionId) {
|
|
177
|
-
shell._shareViewers = {};
|
|
178
|
-
_originalWrite = process.stdout.write.bind(process.stdout);
|
|
179
|
-
|
|
180
|
-
// Remove any existing listeners to prevent duplicates on re-share
|
|
181
|
-
shell.ws.socket.off('terminal:viewerJoined');
|
|
182
|
-
shell.ws.socket.off('terminal:viewerLeft');
|
|
183
|
-
shell.ws.socket.off('terminal:writeRequest');
|
|
184
|
-
shell.ws.socket.off('terminal:input');
|
|
185
|
-
|
|
186
|
-
process.stdout.write = function(chunk, encoding, callback) {
|
|
187
|
-
const str = chunk.toString();
|
|
188
|
-
// Send to viewers ā stream everything including ANSI colors
|
|
189
|
-
if (shell.ws.isConnected() && shell._shareActive) {
|
|
190
|
-
shell.ws.socket.emit('terminal:output', {
|
|
191
|
-
sessionId,
|
|
192
|
-
data: str,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
// Still write to own terminal
|
|
196
|
-
return _originalWrite(chunk, encoding, callback);
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
// Listen for viewer events
|
|
200
|
-
shell.ws.socket.on('terminal:viewerJoined', (d) => {
|
|
201
|
-
if (!shell._shareViewers) shell._shareViewers = {};
|
|
202
|
-
shell._shareViewers[d.userId] = { userId: d.userId, name: d.name, canWrite: false, requested: false };
|
|
203
|
-
console.log(chalk.dim(`\n šļø ${d.name} is now viewing your terminal`));
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
shell.ws.socket.on('terminal:viewerLeft', (d) => {
|
|
207
|
-
if (shell._shareViewers) delete shell._shareViewers[d.userId];
|
|
208
|
-
console.log(chalk.dim(`\n šļø ${d.name} stopped viewing`));
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
shell.ws.socket.on('terminal:writeRequest', (d) => {
|
|
212
|
-
if (shell._shareViewers?.[d.userId]) shell._shareViewers[d.userId].requested = true;
|
|
213
|
-
console.log(chalk.yellow(`\n āļø ${d.name} is requesting write access`));
|
|
214
|
-
console.log(chalk.dim(` share grant ${d.userId} or share kick ${d.userId}\n`));
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
shell.ws.socket.on('terminal:input', (d) => {
|
|
218
|
-
// A writer sent input ā execute it in the shell
|
|
219
|
-
console.log(chalk.blue(`\n [${d.fromName}] ā ${d.input}`));
|
|
220
|
-
// The shell will process this as if the user typed it
|
|
221
|
-
shell.execute(d.input.trim());
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function _unhookOutput(shell) {
|
|
226
|
-
if (_originalWrite) {
|
|
227
|
-
process.stdout.write = _originalWrite;
|
|
228
|
-
_originalWrite = null;
|
|
229
|
-
}
|
|
230
|
-
shell.ws.socket.off('terminal:viewerJoined');
|
|
231
|
-
shell.ws.socket.off('terminal:viewerLeft');
|
|
232
|
-
shell.ws.socket.off('terminal:writeRequest');
|
|
233
|
-
shell.ws.socket.off('terminal:input');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
// āā Help āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
237
|
-
function printHelp() {
|
|
238
|
-
console.log(chalk.cyan('\n Terminal Share Commands'));
|
|
239
|
-
console.log(chalk.dim(' ' + 'ā'.repeat(40)));
|
|
240
|
-
const rows = [
|
|
241
|
-
['share start', 'Start sharing your terminal'],
|
|
242
|
-
['share stop', 'Stop sharing'],
|
|
243
|
-
['share status', 'Show viewers and write requests'],
|
|
244
|
-
['share grant <userId>', 'Give write access to a viewer'],
|
|
245
|
-
['share revoke <userId>','Revoke write access'],
|
|
246
|
-
['share kick <userId>', 'Remove a viewer'],
|
|
247
|
-
];
|
|
248
|
-
rows.forEach(([cmd, desc]) => {
|
|
249
|
-
console.log(` ${chalk.green(cmd.padEnd(26))} ${chalk.dim(desc)}`);
|
|
250
|
-
});
|
|
251
|
-
console.log();
|
|
1
|
+
/*
|
|
2
|
+
commands/terminal/share.js
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
share start ā start sharing current terminal
|
|
6
|
+
share stop ā stop sharing
|
|
7
|
+
share status ā show who's viewing / requesting
|
|
8
|
+
share grant <uid> ā grant write to a viewer
|
|
9
|
+
share revoke <uid>ā revoke write
|
|
10
|
+
share kick <uid> ā kick a viewer
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
|
|
15
|
+
module.exports = {
|
|
16
|
+
name: 'share',
|
|
17
|
+
description: 'Share your terminal with room members',
|
|
18
|
+
aliases: ['terminal-share', 'ts'],
|
|
19
|
+
requiresAuth: true,
|
|
20
|
+
requiresRoom: true,
|
|
21
|
+
|
|
22
|
+
async execute(args, shell) {
|
|
23
|
+
const sub = (args[0] || '').toLowerCase();
|
|
24
|
+
|
|
25
|
+
switch (sub) {
|
|
26
|
+
case 'start': return cmdStart(shell);
|
|
27
|
+
case 'stop': return cmdStop(shell);
|
|
28
|
+
case 'status': return cmdStatus(shell);
|
|
29
|
+
case 'grant': return cmdGrant(args[1], shell);
|
|
30
|
+
case 'revoke': return cmdRevoke(args[1], shell);
|
|
31
|
+
case 'kick': return cmdKick(args[1], shell);
|
|
32
|
+
default:
|
|
33
|
+
printHelp();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// āā start āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
39
|
+
async function cmdStart(shell) {
|
|
40
|
+
if (shell._shareActive) {
|
|
41
|
+
console.log(chalk.yellow('ā ļø Already sharing. Run "share stop" first.'));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const roomId = shell.ws.getCurrentRoom();
|
|
46
|
+
if (!roomId) {
|
|
47
|
+
console.log(chalk.red('ā Join a room first: join <room-id>'));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
const timeout = setTimeout(() => {
|
|
53
|
+
shell.ws.socket.off('terminal:share:started', onStarted);
|
|
54
|
+
console.log(chalk.red('ā Share start timeout'));
|
|
55
|
+
resolve();
|
|
56
|
+
}, 6000);
|
|
57
|
+
|
|
58
|
+
function onStarted(data) {
|
|
59
|
+
clearTimeout(timeout);
|
|
60
|
+
shell._shareActive = true;
|
|
61
|
+
shell._shareSessionId = data.sessionId;
|
|
62
|
+
|
|
63
|
+
console.log(chalk.green(`\nā
Terminal sharing started`));
|
|
64
|
+
console.log(chalk.dim(` Session : ${data.sessionId}`));
|
|
65
|
+
console.log(chalk.dim(` Room : ${roomId}`));
|
|
66
|
+
console.log(chalk.dim(` URL : /terminal/${data.sessionId}`));
|
|
67
|
+
console.log(chalk.dim(`\n Room members can now view your terminal.`));
|
|
68
|
+
console.log(chalk.dim(` Use "share grant <userId>" to give write access.\n`));
|
|
69
|
+
|
|
70
|
+
// Hook into shell output to stream it
|
|
71
|
+
_hookOutput(shell, data.sessionId);
|
|
72
|
+
|
|
73
|
+
resolve();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
shell.ws.socket.once('terminal:share:started', onStarted);
|
|
77
|
+
shell.ws.socket.emit('terminal:share:start', { roomId });
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// āā stop āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
82
|
+
async function cmdStop(shell) {
|
|
83
|
+
if (!shell._shareActive) {
|
|
84
|
+
console.log(chalk.yellow('ā ļø No active share session.'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
shell.ws.socket.emit('terminal:share:stop');
|
|
89
|
+
_unhookOutput(shell);
|
|
90
|
+
shell._shareActive = false;
|
|
91
|
+
shell._shareSessionId = null;
|
|
92
|
+
console.log(chalk.green('ā
Terminal sharing stopped.'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// āā status āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
96
|
+
async function cmdStatus(shell) {
|
|
97
|
+
if (!shell._shareActive || !shell._shareSessionId) {
|
|
98
|
+
console.log(chalk.yellow('// No active share session'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const mem = shell._shareViewers || {};
|
|
103
|
+
const viewers = Object.values(mem);
|
|
104
|
+
|
|
105
|
+
console.log(chalk.cyan(`\n Terminal Share ā ${shell._shareSessionId}`));
|
|
106
|
+
console.log(chalk.dim(' ' + 'ā'.repeat(44)));
|
|
107
|
+
|
|
108
|
+
if (!viewers.length) {
|
|
109
|
+
console.log(chalk.dim(' No viewers yet.'));
|
|
110
|
+
} else {
|
|
111
|
+
viewers.forEach(v => {
|
|
112
|
+
const access = v.canWrite ? chalk.blue('[write]') : chalk.dim('[read]');
|
|
113
|
+
const req = v.requested ? chalk.yellow(' ā requesting write') : '';
|
|
114
|
+
console.log(` ${chalk.green('ā')} ${chalk.white(v.name)} ${access}${req}`);
|
|
115
|
+
console.log(chalk.dim(` userId: ${v.userId}`));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
console.log();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// āā grant āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
122
|
+
async function cmdGrant(userId, shell) {
|
|
123
|
+
if (!userId) { console.log(chalk.red('Usage: share grant <userId>')); return; }
|
|
124
|
+
if (!shell._shareActive) { console.log(chalk.red('ā No active share session')); return; }
|
|
125
|
+
|
|
126
|
+
shell.ws.socket.emit('terminal:grant', {
|
|
127
|
+
sessionId: shell._shareSessionId,
|
|
128
|
+
userId,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Update local viewer state
|
|
132
|
+
if (shell._shareViewers?.[userId]) {
|
|
133
|
+
shell._shareViewers[userId].canWrite = true;
|
|
134
|
+
shell._shareViewers[userId].requested = false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(chalk.green(`ā
Write access granted to ${userId}`));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// āā revoke āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
141
|
+
async function cmdRevoke(userId, shell) {
|
|
142
|
+
if (!userId) { console.log(chalk.red('Usage: share revoke <userId>')); return; }
|
|
143
|
+
if (!shell._shareActive) { console.log(chalk.red('ā No active share session')); return; }
|
|
144
|
+
|
|
145
|
+
shell.ws.socket.emit('terminal:revoke', {
|
|
146
|
+
sessionId: shell._shareSessionId,
|
|
147
|
+
userId,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (shell._shareViewers?.[userId]) {
|
|
151
|
+
shell._shareViewers[userId].canWrite = false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log(chalk.green(`ā
Write access revoked from ${userId}`));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// āā kick āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
158
|
+
async function cmdKick(userId, shell) {
|
|
159
|
+
if (!userId) { console.log(chalk.red('Usage: share kick <userId>')); return; }
|
|
160
|
+
if (!shell._shareActive) { console.log(chalk.red('ā No active share session')); return; }
|
|
161
|
+
|
|
162
|
+
shell.ws.socket.emit('terminal:kick', {
|
|
163
|
+
sessionId: shell._shareSessionId,
|
|
164
|
+
userId,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (shell._shareViewers) delete shell._shareViewers[userId];
|
|
168
|
+
console.log(chalk.green(`ā
${userId} kicked from session`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// āā Output hooking āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
172
|
+
// Intercept process.stdout.write to stream output to viewers
|
|
173
|
+
|
|
174
|
+
let _originalWrite = null;
|
|
175
|
+
|
|
176
|
+
function _hookOutput(shell, sessionId) {
|
|
177
|
+
shell._shareViewers = {};
|
|
178
|
+
_originalWrite = process.stdout.write.bind(process.stdout);
|
|
179
|
+
|
|
180
|
+
// Remove any existing listeners to prevent duplicates on re-share
|
|
181
|
+
shell.ws.socket.off('terminal:viewerJoined');
|
|
182
|
+
shell.ws.socket.off('terminal:viewerLeft');
|
|
183
|
+
shell.ws.socket.off('terminal:writeRequest');
|
|
184
|
+
shell.ws.socket.off('terminal:input');
|
|
185
|
+
|
|
186
|
+
process.stdout.write = function(chunk, encoding, callback) {
|
|
187
|
+
const str = chunk.toString();
|
|
188
|
+
// Send to viewers ā stream everything including ANSI colors
|
|
189
|
+
if (shell.ws.isConnected() && shell._shareActive) {
|
|
190
|
+
shell.ws.socket.emit('terminal:output', {
|
|
191
|
+
sessionId,
|
|
192
|
+
data: str,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
// Still write to own terminal
|
|
196
|
+
return _originalWrite(chunk, encoding, callback);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Listen for viewer events
|
|
200
|
+
shell.ws.socket.on('terminal:viewerJoined', (d) => {
|
|
201
|
+
if (!shell._shareViewers) shell._shareViewers = {};
|
|
202
|
+
shell._shareViewers[d.userId] = { userId: d.userId, name: d.name, canWrite: false, requested: false };
|
|
203
|
+
console.log(chalk.dim(`\n šļø ${d.name} is now viewing your terminal`));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
shell.ws.socket.on('terminal:viewerLeft', (d) => {
|
|
207
|
+
if (shell._shareViewers) delete shell._shareViewers[d.userId];
|
|
208
|
+
console.log(chalk.dim(`\n šļø ${d.name} stopped viewing`));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
shell.ws.socket.on('terminal:writeRequest', (d) => {
|
|
212
|
+
if (shell._shareViewers?.[d.userId]) shell._shareViewers[d.userId].requested = true;
|
|
213
|
+
console.log(chalk.yellow(`\n āļø ${d.name} is requesting write access`));
|
|
214
|
+
console.log(chalk.dim(` share grant ${d.userId} or share kick ${d.userId}\n`));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
shell.ws.socket.on('terminal:input', (d) => {
|
|
218
|
+
// A writer sent input ā execute it in the shell
|
|
219
|
+
console.log(chalk.blue(`\n [${d.fromName}] ā ${d.input}`));
|
|
220
|
+
// The shell will process this as if the user typed it
|
|
221
|
+
shell.execute(d.input.trim());
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _unhookOutput(shell) {
|
|
226
|
+
if (_originalWrite) {
|
|
227
|
+
process.stdout.write = _originalWrite;
|
|
228
|
+
_originalWrite = null;
|
|
229
|
+
}
|
|
230
|
+
shell.ws.socket.off('terminal:viewerJoined');
|
|
231
|
+
shell.ws.socket.off('terminal:viewerLeft');
|
|
232
|
+
shell.ws.socket.off('terminal:writeRequest');
|
|
233
|
+
shell.ws.socket.off('terminal:input');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// āā Help āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
237
|
+
function printHelp() {
|
|
238
|
+
console.log(chalk.cyan('\n Terminal Share Commands'));
|
|
239
|
+
console.log(chalk.dim(' ' + 'ā'.repeat(40)));
|
|
240
|
+
const rows = [
|
|
241
|
+
['share start', 'Start sharing your terminal'],
|
|
242
|
+
['share stop', 'Stop sharing'],
|
|
243
|
+
['share status', 'Show viewers and write requests'],
|
|
244
|
+
['share grant <userId>', 'Give write access to a viewer'],
|
|
245
|
+
['share revoke <userId>','Revoke write access'],
|
|
246
|
+
['share kick <userId>', 'Remove a viewer'],
|
|
247
|
+
];
|
|
248
|
+
rows.forEach(([cmd, desc]) => {
|
|
249
|
+
console.log(` ${chalk.green(cmd.padEnd(26))} ${chalk.dim(desc)}`);
|
|
250
|
+
});
|
|
251
|
+
console.log();
|
|
252
252
|
}
|