thinkncollab-shell 0.0.5 → 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/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 +1 -1
- package/src/core/project-config.js +100 -0
- package/src/core/shell.js +222 -210
- package/src/core/websocket.js +135 -148
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/commands/tasks/complete.js — Mark your task assignment as completed
|
|
3
|
+
*/
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'complete',
|
|
8
|
+
description: 'Mark a task as completed',
|
|
9
|
+
aliases: ['done', 'finish'],
|
|
10
|
+
requiresAuth: true,
|
|
11
|
+
|
|
12
|
+
async execute(args, shell) {
|
|
13
|
+
if (!args[0]) {
|
|
14
|
+
console.log(chalk.red('Usage: complete <task-id>'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const taskId = args[0];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await shell.api._request('POST', `/thinknsh/${taskId}/cli/complete`);
|
|
22
|
+
|
|
23
|
+
console.log(chalk.green(`✅ Task completed: "${result.task?.title || taskId}"`));
|
|
24
|
+
|
|
25
|
+
if (result.allCompleted)
|
|
26
|
+
console.log(chalk.green(' 🎉 All assignees have completed this task!'));
|
|
27
|
+
|
|
28
|
+
// Push to notification window
|
|
29
|
+
shell.pushNotification({
|
|
30
|
+
type: 'notification',
|
|
31
|
+
level: 'success',
|
|
32
|
+
title: 'Task Completed',
|
|
33
|
+
message: result.task?.title || taskId,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err.message?.includes('403'))
|
|
38
|
+
console.log(chalk.red('❌ You are not assigned to this task'));
|
|
39
|
+
else if (err.message?.includes('404'))
|
|
40
|
+
console.log(chalk.red('❌ Task not found'));
|
|
41
|
+
else if (err.message?.includes('already'))
|
|
42
|
+
console.log(chalk.yellow('⚠️ Task already completed'));
|
|
43
|
+
else
|
|
44
|
+
console.log(chalk.red(`❌ ${err.message}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/commands/tasks/create-task.js — Create a new task interactively
|
|
3
|
+
*/
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'create-task',
|
|
8
|
+
description: 'Create a new task in current room',
|
|
9
|
+
aliases: ['newtask', 'addtask'],
|
|
10
|
+
requiresAuth: true,
|
|
11
|
+
requiresRoom: true,
|
|
12
|
+
|
|
13
|
+
async execute(args, shell) {
|
|
14
|
+
const { input, select, confirm } = require('@inquirer/prompts');
|
|
15
|
+
const roomId = shell.ws.getCurrentRoom();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Fetch columns of current room's board
|
|
19
|
+
const { columns } = await shell.api._request('GET', `/rooms/${roomId}/cli/columns`);
|
|
20
|
+
if (!columns?.length) {
|
|
21
|
+
console.log(chalk.red('❌ No columns found in this room\'s board'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(chalk.cyan('\n // create-task\n'));
|
|
26
|
+
|
|
27
|
+
const title = await input({
|
|
28
|
+
message: 'Task title:',
|
|
29
|
+
validate: v => v.trim().length >= 3 || 'Min 3 characters'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const description = await input({ message: 'Description (optional):' });
|
|
33
|
+
|
|
34
|
+
const columnId = await select({
|
|
35
|
+
message: 'Column:',
|
|
36
|
+
choices: columns.map(c => ({ name: c.title, value: c._id }))
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const priority = await select({
|
|
40
|
+
message: 'Priority:',
|
|
41
|
+
choices: [
|
|
42
|
+
{ name: 'low', value: 'low' },
|
|
43
|
+
{ name: 'medium', value: 'medium' },
|
|
44
|
+
{ name: 'high', value: 'high' },
|
|
45
|
+
],
|
|
46
|
+
default: 'medium'
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const categories = [
|
|
50
|
+
'Bugs', 'Feature Requests', 'Improvements / Enhancements',
|
|
51
|
+
'Production Critical Issues', 'Security Issues', 'Performance Issues',
|
|
52
|
+
'UI/UX Issues', 'Testing & QA', 'DevOps & Infrastructure',
|
|
53
|
+
'Refactoring & Code Quality', 'Documentation', 'Other'
|
|
54
|
+
];
|
|
55
|
+
const category = await select({
|
|
56
|
+
message: 'Category:',
|
|
57
|
+
choices: categories.map(c => ({ name: c, value: c }))
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const dueDateStr = await input({
|
|
61
|
+
message: 'Due date (YYYY-MM-DD, optional):',
|
|
62
|
+
validate: v => {
|
|
63
|
+
if (!v) return true;
|
|
64
|
+
return !isNaN(Date.parse(v)) || 'Invalid date format';
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const confirmed = await confirm({ message: 'Create task?', default: true });
|
|
69
|
+
if (!confirmed) { console.log(chalk.dim(' Cancelled.')); return; }
|
|
70
|
+
|
|
71
|
+
const payload = {
|
|
72
|
+
title: title.trim(),
|
|
73
|
+
description: description.trim() || undefined,
|
|
74
|
+
columnId,
|
|
75
|
+
priority,
|
|
76
|
+
category: category === 'Other' ? undefined : category,
|
|
77
|
+
dueDate: dueDateStr ? new Date(dueDateStr).toISOString() : undefined,
|
|
78
|
+
source: 'cli',
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const result = await shell.api._request('POST', `/${roomId}/cli/tasks`, payload);
|
|
82
|
+
|
|
83
|
+
console.log(chalk.green(`\n✅ Task created: "${result.task.title}"`));
|
|
84
|
+
console.log(chalk.dim(` ID: ${result.task._id}`));
|
|
85
|
+
console.log(chalk.dim(` Column: ${result.task.columnId?.title || columnId}\n`));
|
|
86
|
+
|
|
87
|
+
shell.pushNotification({
|
|
88
|
+
type: 'notification',
|
|
89
|
+
level: 'success',
|
|
90
|
+
title: 'Task Created',
|
|
91
|
+
message: result.task.title,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
} catch (err) {
|
|
95
|
+
if (err.message?.includes('403'))
|
|
96
|
+
console.log(chalk.red('❌ You are not a member of this room'));
|
|
97
|
+
else
|
|
98
|
+
console.log(chalk.red(`❌ ${err.message}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/commands/tasks/task.js — View single task detail
|
|
3
|
+
*/
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
module.exports = {
|
|
7
|
+
name: 'task',
|
|
8
|
+
description: 'View task details by ID',
|
|
9
|
+
aliases: ['t'],
|
|
10
|
+
requiresAuth: true,
|
|
11
|
+
|
|
12
|
+
async execute(args, shell) {
|
|
13
|
+
if (!args[0]) {
|
|
14
|
+
console.log(chalk.red('Usage: task <task-id>'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const taskId = args[0];
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const task = await shell.api._request('GET', `/thinknsh/${taskId}/cli`);
|
|
22
|
+
|
|
23
|
+
const stColors = {
|
|
24
|
+
pending: chalk.yellow,
|
|
25
|
+
inprogress: chalk.blue,
|
|
26
|
+
review: chalk.magenta,
|
|
27
|
+
completed: chalk.green,
|
|
28
|
+
reopened: chalk.cyan,
|
|
29
|
+
};
|
|
30
|
+
const prColors = { high: chalk.red, medium: chalk.yellow, low: chalk.green };
|
|
31
|
+
const stColor = stColors[task.status] || chalk.white;
|
|
32
|
+
const prColor = prColors[task.priority] || chalk.white;
|
|
33
|
+
|
|
34
|
+
console.log(chalk.cyan('\n ─── Task ────────────────────────────────────────'));
|
|
35
|
+
console.log(` ${chalk.dim('id')} ${chalk.dim(task._id)}`);
|
|
36
|
+
console.log(` ${chalk.dim('title')} ${chalk.bold(task.title)}`);
|
|
37
|
+
console.log(` ${chalk.dim('status')} ${stColor(task.status)}`);
|
|
38
|
+
console.log(` ${chalk.dim('priority')} ${prColor(task.priority)}`);
|
|
39
|
+
if (task.category)
|
|
40
|
+
console.log(` ${chalk.dim('category')} ${chalk.dim(task.category)}`);
|
|
41
|
+
if (task.dueDate)
|
|
42
|
+
console.log(` ${chalk.dim('due')} ${chalk.yellow(new Date(task.dueDate).toLocaleDateString())}`);
|
|
43
|
+
if (task.description) {
|
|
44
|
+
console.log(chalk.dim('\n description:'));
|
|
45
|
+
task.description.split('\n').forEach(l => console.log(` ${chalk.white(l)}`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (task.assignedTo?.length) {
|
|
49
|
+
console.log(chalk.dim('\n assignees:'));
|
|
50
|
+
task.assignedTo.forEach(a => {
|
|
51
|
+
const name = a.userId?.name || 'Unknown';
|
|
52
|
+
const stBadge = a.status === 'completed' ? chalk.green('✓') :
|
|
53
|
+
a.status === 'accepted' ? chalk.blue('●') :
|
|
54
|
+
a.status === 'rejected' ? chalk.red('✗') : chalk.dim('○');
|
|
55
|
+
console.log(` ${stBadge} ${name} ${chalk.dim(`[${a.status}]`)}`);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (task.columnId?.name || task.columnName)
|
|
60
|
+
console.log(`\n ${chalk.dim('column')} ${task.columnId?.name || task.columnName}`);
|
|
61
|
+
if (task.boardName)
|
|
62
|
+
console.log(` ${chalk.dim('board')} ${task.boardName}`);
|
|
63
|
+
|
|
64
|
+
console.log(chalk.cyan(' ─────────────────────────────────────────────────\n'));
|
|
65
|
+
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.message?.includes('403'))
|
|
68
|
+
console.log(chalk.red('❌ Access denied — you are not a member of this room'));
|
|
69
|
+
else if (err.message?.includes('404'))
|
|
70
|
+
console.log(chalk.red('❌ Task not found'));
|
|
71
|
+
else
|
|
72
|
+
console.log(chalk.red(`❌ ${err.message}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
name: 'myTask',
|
|
5
|
+
description: 'My Tasks in the Room',
|
|
6
|
+
aliases: ['mytasks'],
|
|
7
|
+
requiresAuth: true,
|
|
8
|
+
|
|
9
|
+
async execute(args, shell) {
|
|
10
|
+
const user = shell.api.getUser();
|
|
11
|
+
const token = shell.api.getToken();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
if (!user || !token) {
|
|
15
|
+
console.log(chalk.red('❌ Please login first: login'));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// FIX: getCurrentRoom() lives on shell.ws, not shell.api
|
|
20
|
+
const roomId = shell.ws.getCurrentRoom();
|
|
21
|
+
|
|
22
|
+
if (!roomId) {
|
|
23
|
+
console.log(chalk.yellow('⚠️ Please join a room first: join <room-id>'));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!shell.api) {
|
|
28
|
+
console.log(chalk.red('❌ API not initialized'));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(chalk.dim(' Fetching tasks...'));
|
|
33
|
+
|
|
34
|
+
// FIX: pass `roomId` (was passing undefined `roomId` from shell.api.getCurrentRoom())
|
|
35
|
+
const result = await shell.api.mytasks(user, token, roomId);
|
|
36
|
+
|
|
37
|
+
if (!result || !result.tasks || result.tasks.length === 0) {
|
|
38
|
+
console.log(chalk.yellow(' No tasks found in this room.'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(chalk.cyan(`\n📋 Your Tasks in room: ${chalk.white(roomId)}\n`));
|
|
43
|
+
console.log(chalk.dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
44
|
+
|
|
45
|
+
result.tasks.forEach((task, index) => {
|
|
46
|
+
const statusColor = {
|
|
47
|
+
completed: chalk.green,
|
|
48
|
+
in_progress: chalk.yellow,
|
|
49
|
+
pending: chalk.dim,
|
|
50
|
+
}[task.status] || chalk.white;
|
|
51
|
+
|
|
52
|
+
const status = statusColor(`[${task.status || 'pending'}]`);
|
|
53
|
+
const title = chalk.white(task.title || task.name || 'Untitled');
|
|
54
|
+
const due = task.dueDate ? chalk.dim(` — due: ${new Date(task.dueDate).toLocaleDateString()}`) : '';
|
|
55
|
+
|
|
56
|
+
console.log(` ${chalk.dim(`${index + 1}.`)} ${status} ${title}${due}`);
|
|
57
|
+
|
|
58
|
+
if (task.description) {
|
|
59
|
+
console.log(` ${chalk.dim(task.description)}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(chalk.dim('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
64
|
+
console.log(chalk.green(` ✅ ${result.tasks.length} task(s) fetched successfully.\n`));
|
|
65
|
+
|
|
66
|
+
} catch (err) {
|
|
67
|
+
if (err.name === 'ExitPromptError' || err.message?.includes('force closed')) {
|
|
68
|
+
console.log(chalk.yellow('⚠️ Cancelled'));
|
|
69
|
+
} else {
|
|
70
|
+
console.log(chalk.red(`❌ Fetching tasks failed: ${err.message}`));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,252 @@
|
|
|
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
|
+
}
|