thinkncollab-shell 0.0.10 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +32 -0
- package/package.json +5 -2
- package/src/commands/github/commits.js +58 -0
- package/src/commands/github/issue-create.js +67 -0
- package/src/commands/github/issues.js +57 -0
- package/src/commands/github/pulls.js +58 -0
- package/src/commands/github/statusgh.js +50 -0
- package/src/commands/index.js +30 -26
- package/src/commands/room/leave.js +6 -15
- package/src/commands/shell/status.js +5 -5
- package/src/commands/tasks/complete.js +748 -35
- package/src/commands/tasks/create-task.js +2 -2
- package/src/commands/tasks/loadtest.js +73 -0
- package/src/commands/tasks/task-status.js +63 -52
- package/src/commands/tasks/task.js +169 -65
- package/src/commands/tasks/tasks.js +4 -3
- package/src/commands/terminal/share.js +1 -1
- package/src/core/api-client.js +1 -1
- package/src/core/geekload.js +524 -0
- package/src/core/notification-window.js +25 -5
- package/src/core/shell.js +338 -299
- package/src/core/websocket.js +3 -0
- package/src/events/notifications.js +3 -4
package/src/core/shell.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* src/core/shell.js — ThinkNCollab Shell Core
|
|
2
|
+
* src/core/shell.js — ThinkNCollab Shell Core sound
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const path
|
|
6
|
-
const fs
|
|
7
|
-
const os
|
|
8
|
-
const chalk
|
|
9
|
-
const net
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const fs = require('fs-extra');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const net = require('net');
|
|
10
10
|
const { spawn } = require('child_process');
|
|
11
|
-
const EventEmitter
|
|
11
|
+
const EventEmitter = require('events');
|
|
12
12
|
const WebSocketManager = require('./websocket');
|
|
13
|
-
const CommandRegistry
|
|
14
|
-
const ThinkNCollabAPI
|
|
13
|
+
const CommandRegistry = require('../commands');
|
|
14
|
+
const ThinkNCollabAPI = require('./api-client');
|
|
15
15
|
const { loadProjectConfig, hasProjectConfig } = require('./project-config');
|
|
16
16
|
|
|
17
17
|
function getAvailablePort(startPort) {
|
|
@@ -32,10 +32,10 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
32
32
|
|
|
33
33
|
this.config = {
|
|
34
34
|
historyFile: path.join(os.homedir(), '.thinknsh_history'),
|
|
35
|
-
configDir:
|
|
36
|
-
maxHistory:
|
|
35
|
+
configDir: path.join(os.homedir(), '.thinkncollab'),
|
|
36
|
+
maxHistory: 1000,
|
|
37
37
|
websocket: {
|
|
38
|
-
serverUrl:
|
|
38
|
+
serverUrl: options.serverUrl || 'https://thinkncollab.com',
|
|
39
39
|
autoConnect: options.autoConnect || false
|
|
40
40
|
},
|
|
41
41
|
...options
|
|
@@ -43,23 +43,23 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
43
43
|
|
|
44
44
|
fs.ensureDirSync(this.config.configDir);
|
|
45
45
|
|
|
46
|
-
this.ws
|
|
46
|
+
this.ws = new WebSocketManager(this.config.websocket);
|
|
47
47
|
this.currentDir = process.cwd();
|
|
48
|
-
this.username
|
|
49
|
-
this.history
|
|
50
|
-
this.running
|
|
48
|
+
this.username = os.userInfo().username;
|
|
49
|
+
this.history = [];
|
|
50
|
+
this.running = false;
|
|
51
51
|
|
|
52
52
|
// TCP notification IPC
|
|
53
53
|
this.notifyClients = new Set();
|
|
54
|
-
this.tcpServer
|
|
54
|
+
this.tcpServer = null;
|
|
55
55
|
|
|
56
56
|
this.api = new ThinkNCollabAPI({
|
|
57
57
|
apiUrl: options.apiUrl || 'https://thinkncollab.com',
|
|
58
|
-
wsUrl:
|
|
58
|
+
wsUrl: options.wsUrl || 'https://thinkncollab.com'
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
this.commands
|
|
62
|
-
this.aliases
|
|
61
|
+
this.commands = new Map();
|
|
62
|
+
this.aliases = new Map();
|
|
63
63
|
this.variables = new Map();
|
|
64
64
|
|
|
65
65
|
// Restore persisted session on startup
|
|
@@ -96,235 +96,272 @@ class ThinkNCollabShell extends EventEmitter {
|
|
|
96
96
|
if (this.notifyClients.size === 0) return;
|
|
97
97
|
const line = JSON.stringify(data) + '\n';
|
|
98
98
|
for (const client of this.notifyClients) {
|
|
99
|
-
try { client.write(line); } catch {}
|
|
99
|
+
try { client.write(line); } catch { }
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// ─── Spawn notification window ────────────────────────────────────────────
|
|
104
|
-
spawnNotificationWindow() {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
// ── WSL ──────────────────────────────────────────────────────────────
|
|
120
|
-
if (isWSL) {
|
|
121
|
-
const winScript = notifyScript
|
|
122
|
-
.replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
|
|
123
|
-
.replace(/\//g, '\\');
|
|
124
|
-
|
|
125
|
-
const launchWinNode = (winExe) => {
|
|
126
|
-
const p = spawn(
|
|
127
|
-
`start cmd.exe /k ""${winExe}" "${winScript}""`,
|
|
128
|
-
[],
|
|
129
|
-
{ detached: true, stdio: 'ignore', env, shell: true }
|
|
130
|
-
);
|
|
131
|
-
p.on('error', fallback);
|
|
132
|
-
p.unref();
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
// 1. Known paths — instant
|
|
136
|
-
const known = [
|
|
137
|
-
'/mnt/c/Program Files/nodejs/node.exe',
|
|
138
|
-
'/mnt/c/Program Files (x86)/nodejs/node.exe',
|
|
139
|
-
];
|
|
140
|
-
const found = known.find(p => { try { return fs.existsSync(p); } catch { return false; } });
|
|
141
|
-
|
|
142
|
-
if (found) {
|
|
143
|
-
const winExe = found
|
|
144
|
-
.replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
|
|
145
|
-
.replace(/\//g, '\\');
|
|
146
|
-
launchWinNode(winExe);
|
|
147
|
-
} else {
|
|
148
|
-
// 2. Async where node
|
|
149
|
-
const child = spawn('cmd.exe', ['/c', 'where', 'node'], {
|
|
150
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
151
|
-
});
|
|
152
|
-
let out = '';
|
|
153
|
-
child.stdout.on('data', d => out += d);
|
|
154
|
-
child.on('close', () => {
|
|
155
|
-
const winExe = out.trim().split('\n')[0]?.trim();
|
|
156
|
-
if (winExe?.endsWith('.exe')) {
|
|
157
|
-
launchWinNode(winExe);
|
|
158
|
-
} else {
|
|
159
|
-
// 3. Last resort
|
|
160
|
-
const p = spawn(
|
|
161
|
-
`start cmd.exe /k "wsl.exe -- bash -c 'THINKNSH_NOTIFY_PORT=${port} node \\"${notifyScript}\\"; read'"`,
|
|
162
|
-
[],
|
|
163
|
-
{ detached: true, stdio: 'ignore', shell: true }
|
|
164
|
-
);
|
|
165
|
-
p.on('error', fallback);
|
|
166
|
-
p.unref();
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// ── Windows native ────────────────────────────────────────────────────
|
|
172
|
-
} else if (process.platform === 'win32') {
|
|
173
|
-
const nodeExe = process.execPath;
|
|
174
|
-
const isVSCode = process.env.TERM_PROGRAM === 'vscode';
|
|
175
|
-
|
|
176
|
-
if (isVSCode) {
|
|
177
|
-
console.log(chalk.yellow('\n VS Code detected — open a new terminal tab and run:'));
|
|
178
|
-
console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${port}; node "${notifyScript}"\n`));
|
|
179
|
-
} else {
|
|
180
|
-
const launchCmd = () => {
|
|
181
|
-
// shell:true — Windows quoting issues handle ho jaate hain
|
|
182
|
-
const p = spawn(
|
|
183
|
-
`start cmd.exe /k ""${nodeExe}" "${notifyScript}""`,
|
|
184
|
-
[],
|
|
185
|
-
{ detached: true, stdio: 'ignore', env, shell: true }
|
|
186
|
-
);
|
|
187
|
-
p.on('error', fallback);
|
|
188
|
-
p.unref();
|
|
104
|
+
spawnNotificationWindow() {
|
|
105
|
+
const notifyScript = path.join(__dirname, 'notification-window.js');
|
|
106
|
+
const port = this.notifyPort;
|
|
107
|
+
const env = { ...process.env, THINKNSH_NOTIFY_PORT: String(port) };
|
|
108
|
+
|
|
109
|
+
const isWSL = process.platform === 'linux' &&
|
|
110
|
+
(() => { try { return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); } catch { return false; } })();
|
|
111
|
+
|
|
112
|
+
const fallback = () => {
|
|
113
|
+
console.log(chalk.yellow(`\n ⚠️ Could not open notification window.`));
|
|
114
|
+
console.log(chalk.dim(` Open a new terminal and run:`));
|
|
115
|
+
console.log(chalk.cyan(` THINKNSH_NOTIFY_PORT=${port} node "${notifyScript}"\n`));
|
|
189
116
|
};
|
|
190
117
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
118
|
+
try {
|
|
119
|
+
// ── WSL ──────────────────────────────────────────────────────────────
|
|
120
|
+
if (isWSL) {
|
|
121
|
+
const winScript = notifyScript
|
|
122
|
+
.replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
|
|
123
|
+
.replace(/\//g, '\\');
|
|
124
|
+
|
|
125
|
+
const launchWinNode = (winExe) => {
|
|
126
|
+
const p = spawn(
|
|
127
|
+
`start cmd.exe /k ""${winExe}" "${winScript}""`,
|
|
128
|
+
[],
|
|
129
|
+
{ detached: true, stdio: 'ignore', env, shell: true }
|
|
130
|
+
);
|
|
131
|
+
p.on('error', fallback);
|
|
132
|
+
p.unref();
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// 1. Known paths — instant
|
|
136
|
+
const known = [
|
|
137
|
+
'/mnt/c/Program Files/nodejs/node.exe',
|
|
138
|
+
'/mnt/c/Program Files (x86)/nodejs/node.exe',
|
|
139
|
+
];
|
|
140
|
+
const found = known.find(p => { try { return fs.existsSync(p); } catch { return false; } });
|
|
141
|
+
|
|
142
|
+
if (found) {
|
|
143
|
+
const winExe = found
|
|
144
|
+
.replace(/^\/mnt\/([a-z])/, (_, d) => `${d.toUpperCase()}:`)
|
|
145
|
+
.replace(/\//g, '\\');
|
|
146
|
+
launchWinNode(winExe);
|
|
147
|
+
} else {
|
|
148
|
+
// 2. Async where node
|
|
149
|
+
const child = spawn('cmd.exe', ['/c', 'where', 'node'], {
|
|
150
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
151
|
+
});
|
|
152
|
+
let out = '';
|
|
153
|
+
child.stdout.on('data', d => out += d);
|
|
154
|
+
child.on('close', () => {
|
|
155
|
+
const winExe = out.trim().split('\n')[0]?.trim();
|
|
156
|
+
if (winExe?.endsWith('.exe')) {
|
|
157
|
+
launchWinNode(winExe);
|
|
158
|
+
} else {
|
|
159
|
+
// 3. Last resort
|
|
160
|
+
const p = spawn(
|
|
161
|
+
`start cmd.exe /k "wsl.exe -- bash -c 'THINKNSH_NOTIFY_PORT=${port} node \\"${notifyScript}\\"; read'"`,
|
|
162
|
+
[],
|
|
163
|
+
{ detached: true, stdio: 'ignore', shell: true }
|
|
164
|
+
);
|
|
165
|
+
p.on('error', fallback);
|
|
166
|
+
p.unref();
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Windows native ────────────────────────────────────────────────────
|
|
172
|
+
} else if (process.platform === 'win32') {
|
|
173
|
+
const nodeExe = process.execPath;
|
|
174
|
+
const isVSCode = process.env.TERM_PROGRAM === 'vscode';
|
|
175
|
+
|
|
176
|
+
if (isVSCode) {
|
|
177
|
+
console.log(chalk.yellow('\n VS Code detected — open a new terminal tab and run:'));
|
|
178
|
+
console.log(chalk.cyan(` $env:THINKNSH_NOTIFY_PORT=${port}; node "${notifyScript}"\n`));
|
|
179
|
+
} else {
|
|
180
|
+
const launchCmd = () => {
|
|
181
|
+
// shell:true — Windows quoting issues handle ho jaate hain
|
|
182
|
+
const p = spawn(
|
|
183
|
+
`start cmd.exe /k ""${nodeExe}" "${notifyScript}""`,
|
|
184
|
+
[],
|
|
185
|
+
{ detached: true, stdio: 'ignore', env, shell: true }
|
|
186
|
+
);
|
|
187
|
+
p.on('error', fallback);
|
|
188
|
+
p.unref();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const wt = spawn('wt.exe', [
|
|
192
|
+
'cmd.exe', '/k', `"${nodeExe}" "${notifyScript}"`
|
|
193
|
+
], { detached: true, stdio: 'ignore', env });
|
|
194
|
+
wt.on('error', launchCmd);
|
|
195
|
+
wt.unref();
|
|
196
|
+
}
|
|
197
|
+
// ── macOS ─────────────────────────────────────────────────────────────
|
|
198
|
+
} else if (process.platform === 'darwin') {
|
|
199
|
+
const nodeExe = process.execPath;
|
|
200
|
+
|
|
201
|
+
// Better VS Code detection - multiple checks
|
|
202
|
+
const isVSCode = process.env.TERM_PROGRAM === 'vscode' ||
|
|
203
|
+
process.env.VSCODE_INJECTION === '1' ||
|
|
204
|
+
process.env.TERM_PROGRAM?.includes('vscode') ||
|
|
205
|
+
process.env.VSCODE_CWD !== undefined;
|
|
206
|
+
|
|
207
|
+
if (isVSCode) {
|
|
208
|
+
console.log(chalk.yellow(`\n ⚠️ VS Code terminal detected`));
|
|
209
|
+
console.log(chalk.dim(` Notification window can't auto-open in VS Code.`));
|
|
210
|
+
console.log(chalk.cyan(` Please open Terminal.app and run:\n`));
|
|
211
|
+
console.log(chalk.white(` THINKNSH_NOTIFY_PORT=${port} node "${notifyScript}"\n`));
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Rest of the macOS code for Terminal.app/iTerm2
|
|
216
|
+
const terminalScript = `
|
|
217
|
+
tell application "Terminal"
|
|
218
|
+
do script "THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'"
|
|
219
|
+
activate
|
|
220
|
+
end tell
|
|
221
|
+
`;
|
|
228
222
|
try {
|
|
229
|
-
spawn('osascript', ['-e',
|
|
223
|
+
spawn('osascript', ['-e', terminalScript], {
|
|
230
224
|
detached: true, stdio: 'ignore'
|
|
231
225
|
}).unref();
|
|
232
|
-
} catch {
|
|
226
|
+
} catch {
|
|
227
|
+
// iTerm2 fallback
|
|
228
|
+
const itermScript = `
|
|
229
|
+
tell application "iTerm2"
|
|
230
|
+
tell current window
|
|
231
|
+
create tab with default profile
|
|
232
|
+
tell current session of current tab
|
|
233
|
+
write text "THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'"
|
|
234
|
+
end tell
|
|
235
|
+
end tell
|
|
236
|
+
end tell
|
|
237
|
+
`;
|
|
238
|
+
try {
|
|
239
|
+
spawn('osascript', ['-e', itermScript], {
|
|
240
|
+
detached: true, stdio: 'ignore'
|
|
241
|
+
}).unref();
|
|
242
|
+
} catch {
|
|
243
|
+
console.log(chalk.yellow(` ⚠️ Could not open notification window`));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Linux native ──────────────────────────────────────────────────────
|
|
248
|
+
} else {
|
|
249
|
+
const nodeExe = process.execPath;
|
|
250
|
+
const bashCmd = `THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'; read`;
|
|
251
|
+
const terminals = [
|
|
252
|
+
['gnome-terminal', ['--', 'bash', '-c', bashCmd]],
|
|
253
|
+
['konsole', ['--noclose', '-e', 'bash', '-c', bashCmd]],
|
|
254
|
+
['xfce4-terminal', ['--hold', '-e', `bash -c "${bashCmd}"`]],
|
|
255
|
+
['tilix', ['-e', `bash -c "${bashCmd}"`]],
|
|
256
|
+
['alacritty', ['-e', 'bash', '-c', bashCmd]],
|
|
257
|
+
['kitty', ['bash', '-c', bashCmd]],
|
|
258
|
+
['xterm', ['-hold', '-e', `bash -c "${bashCmd}"`]],
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
let spawned = false;
|
|
262
|
+
for (const [bin, args] of terminals) {
|
|
263
|
+
try {
|
|
264
|
+
spawn(bin, args, { detached: true, stdio: 'ignore', env }).unref();
|
|
265
|
+
spawned = true;
|
|
266
|
+
break;
|
|
267
|
+
} catch { }
|
|
268
|
+
}
|
|
269
|
+
if (!spawned) fallback();
|
|
233
270
|
}
|
|
234
271
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const nodeExe = process.execPath;
|
|
238
|
-
const bashCmd = `THINKNSH_NOTIFY_PORT=${port} '${nodeExe}' '${notifyScript}'; read`;
|
|
239
|
-
const terminals = [
|
|
240
|
-
['gnome-terminal', ['--', 'bash', '-c', bashCmd]],
|
|
241
|
-
['konsole', ['--noclose', '-e', 'bash', '-c', bashCmd]],
|
|
242
|
-
['xfce4-terminal', ['--hold', '-e', `bash -c "${bashCmd}"`]],
|
|
243
|
-
['tilix', ['-e', `bash -c "${bashCmd}"`]],
|
|
244
|
-
['alacritty', ['-e', 'bash', '-c', bashCmd]],
|
|
245
|
-
['kitty', ['bash', '-c', bashCmd]],
|
|
246
|
-
['xterm', ['-hold', '-e', `bash -c "${bashCmd}"`]],
|
|
247
|
-
];
|
|
248
|
-
|
|
249
|
-
let spawned = false;
|
|
250
|
-
for (const [bin, args] of terminals) {
|
|
251
|
-
try {
|
|
252
|
-
spawn(bin, args, { detached: true, stdio: 'ignore', env }).unref();
|
|
253
|
-
spawned = true;
|
|
254
|
-
break;
|
|
255
|
-
} catch {}
|
|
256
|
-
}
|
|
257
|
-
if (!spawned) fallback();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
fallback();
|
|
258
274
|
}
|
|
259
|
-
|
|
260
|
-
} catch (err) {
|
|
261
|
-
fallback();
|
|
262
275
|
}
|
|
263
|
-
}
|
|
264
276
|
|
|
265
277
|
// ─── WebSocket → Shell + TCP bridge ──────────────────────────────────────
|
|
266
278
|
|
|
267
|
-
setupWebSocketHandlers() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
279
|
+
setupWebSocketHandlers() {
|
|
280
|
+
this.ws.on('connected', (d) => {
|
|
281
|
+
this.api.logEvent('connected', null, { socketId: d?.socketId });
|
|
282
|
+
this.api.startHeartbeat();
|
|
283
|
+
this.pushNotification({ type: 'connected', socketId: d?.socketId });
|
|
284
|
+
});
|
|
273
285
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
this.ws.on('disconnected', (d) => {
|
|
287
|
+
this.api.logEvent('disconnected', null, { reason: d?.reason });
|
|
288
|
+
this.api.stopHeartbeat();
|
|
289
|
+
this.pushNotification({ type: 'disconnected', reason: d?.reason });
|
|
290
|
+
});
|
|
279
291
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
292
|
+
this.ws.on('reconnected', () => {
|
|
293
|
+
this.api.logEvent('reconnected');
|
|
294
|
+
this.api.startHeartbeat();
|
|
295
|
+
this.pushNotification({ type: 'connected', socketId: 'reconnected' });
|
|
296
|
+
});
|
|
297
|
+
this.ws.on('task:verdict', (d) => {
|
|
298
|
+
const passed = d.passed;
|
|
299
|
+
const border = passed ? chalk.green('─'.repeat(52)) : chalk.red('─'.repeat(52));
|
|
300
|
+
const badge = passed ? chalk.bgGreen.black(' PASS ') : chalk.bgRed.white(' FAIL ');
|
|
301
|
+
|
|
302
|
+
console.log('\n' + border);
|
|
303
|
+
console.log(` ${badge} ${chalk.bold(d.title || 'Task')} ${passed ? chalk.green('✅ PASSED') : chalk.red('❌ FAILED')}`);
|
|
304
|
+
console.log(` ${chalk.dim('Reason :')} ${passed ? chalk.green(d.reason) : chalk.red(d.reason)}`);
|
|
305
|
+
if (d.diff) {
|
|
306
|
+
console.log(` ${chalk.dim('Expected:')} ${chalk.cyan(JSON.stringify(d.diff.expected ?? d.diff.conditions))}`);
|
|
307
|
+
console.log(` ${chalk.dim('Actual :')} ${chalk.yellow(JSON.stringify(d.diff.actual))}`);
|
|
308
|
+
}
|
|
309
|
+
console.log(` ${chalk.dim('At :')} ${chalk.dim(new Date(d.at || Date.now()).toLocaleTimeString())}`);
|
|
310
|
+
console.log(border + '\n');
|
|
311
|
+
|
|
312
|
+
this.pushNotification({
|
|
313
|
+
type: 'verdict',
|
|
314
|
+
passed,
|
|
315
|
+
title: d.title,
|
|
316
|
+
reason: d.reason,
|
|
317
|
+
taskId: d.taskId,
|
|
318
|
+
at: d.at
|
|
319
|
+
});
|
|
320
|
+
});
|
|
285
321
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
322
|
+
this.ws.on('message', (d) => {
|
|
323
|
+
const myId = this.api.getUser()?._id;
|
|
324
|
+
if (myId && d.userId === myId) return;
|
|
325
|
+
this.pushNotification({ type: 'message', from: d.username, text: d.message });
|
|
326
|
+
});
|
|
291
327
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
328
|
+
this.ws.on('messageHistory', (messages) => {
|
|
329
|
+
if (!messages?.length) return;
|
|
330
|
+
console.log(chalk.dim('\n ─── Room History ───────────────────────────────'));
|
|
331
|
+
messages.forEach(msg => {
|
|
332
|
+
const from = msg.name || msg.username || msg.sender?.name || 'Unknown';
|
|
333
|
+
const text = msg.message || msg.content || msg.text || '';
|
|
334
|
+
const ts = msg.timestamp || msg.createdAt;
|
|
335
|
+
const timeStr = ts ? chalk.dim(`[${new Date(ts).toLocaleTimeString()}]`) : '';
|
|
336
|
+
console.log(` ${timeStr} ${chalk.cyan(from + ':')} ${chalk.white(text)}`);
|
|
337
|
+
});
|
|
338
|
+
console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
|
|
301
339
|
});
|
|
302
|
-
console.log(chalk.dim(' ────────────────────────────────────────────────\n'));
|
|
303
|
-
});
|
|
304
340
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
341
|
+
this.ws.on('userJoined', (d) => this.pushNotification({ type: 'userJoined', username: d.username }));
|
|
342
|
+
this.ws.on('userLeft', (d) => this.pushNotification({ type: 'userLeft', username: d.username }));
|
|
343
|
+
|
|
344
|
+
// room join/leave events
|
|
345
|
+
this.ws.on('joined', (roomId) => this.api.logEvent('room_join', roomId));
|
|
346
|
+
this.ws.on('left', (roomId) => this.api.logEvent('room_leave', roomId));
|
|
347
|
+
|
|
348
|
+
this.ws.on('notification', (d) => {
|
|
349
|
+
this.pushNotification({
|
|
350
|
+
type: d.type || 'notification',
|
|
351
|
+
level: d.level,
|
|
352
|
+
title: d.title,
|
|
353
|
+
message: d.message,
|
|
354
|
+
taskTitle: d.taskTitle,
|
|
355
|
+
assignedBy: d.assignedBy,
|
|
356
|
+
meta: d.meta,
|
|
357
|
+
});
|
|
320
358
|
});
|
|
321
|
-
});
|
|
322
359
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
360
|
+
this.ws.on('error', (e) => {
|
|
361
|
+
console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
|
|
362
|
+
this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
|
|
363
|
+
});
|
|
364
|
+
}
|
|
328
365
|
|
|
329
366
|
// ─── Start ────────────────────────────────────────────────────────────────
|
|
330
367
|
|
|
@@ -345,7 +382,7 @@ setupWebSocketHandlers() {
|
|
|
345
382
|
|
|
346
383
|
// If no .tncproject but autoConnect is set, connect WS anyway
|
|
347
384
|
if (!booted && this.config.websocket.autoConnect) {
|
|
348
|
-
await this.ws.connect().catch(() => {});
|
|
385
|
+
await this.ws.connect().catch(() => { });
|
|
349
386
|
}
|
|
350
387
|
|
|
351
388
|
const { input } = require('@inquirer/prompts');
|
|
@@ -390,7 +427,8 @@ setupWebSocketHandlers() {
|
|
|
390
427
|
try {
|
|
391
428
|
result = await this.api._request('POST', '/config/verify-config', {
|
|
392
429
|
fileBase64: cfg.fileBase64,
|
|
393
|
-
roomId:
|
|
430
|
+
roomId: cfg.roomId,
|
|
431
|
+
machineId: os.hostname(),
|
|
394
432
|
});
|
|
395
433
|
} catch (err) {
|
|
396
434
|
console.log(chalk.red(`❌ tncproject auth failed: ${err.message}`));
|
|
@@ -401,11 +439,12 @@ setupWebSocketHandlers() {
|
|
|
401
439
|
// Save session exactly like manual login
|
|
402
440
|
this.api._saveSession({
|
|
403
441
|
token: result.shellToken,
|
|
442
|
+
shellSessionId: result.shellSessionId || null,
|
|
404
443
|
user: {
|
|
405
|
-
_id:
|
|
406
|
-
userId:
|
|
407
|
-
email:
|
|
408
|
-
name:
|
|
444
|
+
_id: result._id.toString(),
|
|
445
|
+
userId: result._id.toString(),
|
|
446
|
+
email: result.email,
|
|
447
|
+
name: result.name,
|
|
409
448
|
userType: 'User',
|
|
410
449
|
},
|
|
411
450
|
timestamp: new Date().toISOString(),
|
|
@@ -434,44 +473,44 @@ setupWebSocketHandlers() {
|
|
|
434
473
|
return true;
|
|
435
474
|
}
|
|
436
475
|
|
|
437
|
-
async execute(input) {
|
|
438
|
-
|
|
476
|
+
async execute(input) {
|
|
477
|
+
if (!input.trim()) return;
|
|
439
478
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
479
|
+
const args = this.parseCommand(input);
|
|
480
|
+
const cmdName = args[0].toLowerCase();
|
|
481
|
+
const cmdArgs = args.slice(1);
|
|
443
482
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
483
|
+
// Skip sensitive commands
|
|
484
|
+
const skipLog = ['login', 'logout', 'set', 'unset'];
|
|
485
|
+
if (!skipLog.includes(cmdName)) {
|
|
486
|
+
await this.api.logEvent('command', null, { cmd: cmdName });
|
|
487
|
+
}
|
|
449
488
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
489
|
+
if (this.commands.has(cmdName)) {
|
|
490
|
+
try { await this.commands.get(cmdName).handler(cmdArgs, this); }
|
|
491
|
+
catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
|
|
492
|
+
process.stdout.write('\r\n');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (this.commandRegistry) {
|
|
497
|
+
const handled = await this.commandRegistry.execute(input, this);
|
|
498
|
+
if (handled) {
|
|
499
|
+
process.stdout.write('\r\n');
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
456
503
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
504
|
+
const blockedOnWindows = ['open', 'which', 'grep', 'touch', 'cat'];
|
|
505
|
+
if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
|
|
506
|
+
console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
|
|
507
|
+
console.log(chalk.dim(` Type 'help' to see available commands.`));
|
|
460
508
|
process.stdout.write('\r\n');
|
|
461
509
|
return;
|
|
462
510
|
}
|
|
463
|
-
}
|
|
464
511
|
|
|
465
|
-
|
|
466
|
-
if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
|
|
467
|
-
console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
|
|
468
|
-
console.log(chalk.dim(` Type 'help' to see available commands.`));
|
|
469
|
-
process.stdout.write('\r\n');
|
|
470
|
-
return;
|
|
512
|
+
await this.executeSystemCommand(input);
|
|
471
513
|
}
|
|
472
|
-
|
|
473
|
-
await this.executeSystemCommand(input);
|
|
474
|
-
}
|
|
475
514
|
// ─── Prompt ───────────────────────────────────────────────────────────────
|
|
476
515
|
|
|
477
516
|
getPromptText() {
|
|
@@ -481,7 +520,7 @@ async execute(input) {
|
|
|
481
520
|
if (this.ws.isConnected?.()) p += '● ';
|
|
482
521
|
|
|
483
522
|
let displayPath = this.currentDir;
|
|
484
|
-
const homeDir
|
|
523
|
+
const homeDir = os.homedir();
|
|
485
524
|
|
|
486
525
|
if (process.platform === 'win32') {
|
|
487
526
|
if (displayPath.toLowerCase() === homeDir.toLowerCase()) displayPath = '~';
|
|
@@ -495,7 +534,7 @@ async execute(input) {
|
|
|
495
534
|
return `${this.username}@thinknsh:${displayPath} $`;
|
|
496
535
|
}
|
|
497
536
|
|
|
498
|
-
getPrompt()
|
|
537
|
+
getPrompt() { return this.getPromptText(); }
|
|
499
538
|
printMessage(msg) { console.log(msg); }
|
|
500
539
|
|
|
501
540
|
// ─── Built-ins ────────────────────────────────────────────────────────────
|
|
@@ -503,28 +542,28 @@ async execute(input) {
|
|
|
503
542
|
registerBuiltinCommands() {
|
|
504
543
|
const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
|
|
505
544
|
|
|
506
|
-
r('help',
|
|
507
|
-
r('exit',
|
|
508
|
-
r('quit',
|
|
509
|
-
r('clear',
|
|
510
|
-
r('history',
|
|
511
|
-
r('cd',
|
|
512
|
-
r('pwd',
|
|
513
|
-
r('ls',
|
|
514
|
-
r('connect',
|
|
515
|
-
r('disconnect', () => this.ws.disconnect(),
|
|
516
|
-
r('status',
|
|
517
|
-
r('join',
|
|
518
|
-
r('leave',
|
|
519
|
-
r('rooms',
|
|
520
|
-
r('say',
|
|
521
|
-
r('msg',
|
|
522
|
-
r('set',
|
|
523
|
-
r('unset',
|
|
524
|
-
|
|
525
|
-
this.addAlias('ll',
|
|
526
|
-
this.addAlias('..',
|
|
527
|
-
this.addAlias('~',
|
|
545
|
+
r('help', this.helpCommand.bind(this), 'Show available commands');
|
|
546
|
+
r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
547
|
+
r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
548
|
+
r('clear', () => console.clear(), 'Clear screen');
|
|
549
|
+
r('history', this.historyCommand.bind(this), 'Show command history');
|
|
550
|
+
r('cd', this.cdCommand.bind(this), 'Change directory');
|
|
551
|
+
r('pwd', () => console.log(process.cwd()), 'Print working directory');
|
|
552
|
+
r('ls', this.lsCommand.bind(this), 'List files');
|
|
553
|
+
r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
|
|
554
|
+
r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
|
|
555
|
+
r('status', this.statusCommand.bind(this), 'Show connection status');
|
|
556
|
+
r('join', this.joinCommand.bind(this), 'Join a room');
|
|
557
|
+
r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
|
|
558
|
+
r('rooms', this.roomsCommand.bind(this), 'List rooms');
|
|
559
|
+
r('say', this.sayCommand.bind(this), 'Send a message');
|
|
560
|
+
r('msg', this.sayCommand.bind(this), 'Send a message');
|
|
561
|
+
r('set', this.setCommand.bind(this), 'Set a variable');
|
|
562
|
+
r('unset', this.unsetCommand.bind(this), 'Unset a variable');
|
|
563
|
+
|
|
564
|
+
this.addAlias('ll', 'ls -la');
|
|
565
|
+
this.addAlias('..', 'cd ..');
|
|
566
|
+
this.addAlias('~', 'cd ~');
|
|
528
567
|
}
|
|
529
568
|
|
|
530
569
|
registerCommand(name, handler, description = '') { this.commands.set(name, { handler, description }); }
|
|
@@ -543,7 +582,7 @@ async execute(input) {
|
|
|
543
582
|
const target = args.join(' ').trim() || os.homedir();
|
|
544
583
|
try {
|
|
545
584
|
let newPath;
|
|
546
|
-
if (target === '~')
|
|
585
|
+
if (target === '~') newPath = os.homedir();
|
|
547
586
|
else if (target === '..') newPath = path.dirname(this.currentDir);
|
|
548
587
|
else newPath = path.resolve(this.currentDir, target);
|
|
549
588
|
newPath = path.normalize(newPath);
|
|
@@ -557,8 +596,8 @@ async execute(input) {
|
|
|
557
596
|
|
|
558
597
|
async lsCommand(args) {
|
|
559
598
|
try {
|
|
560
|
-
const files
|
|
561
|
-
const showAll
|
|
599
|
+
const files = await fs.readdir(this.currentDir);
|
|
600
|
+
const showAll = args.includes('-a');
|
|
562
601
|
const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
|
|
563
602
|
const fileList = await Promise.all(filtered.map(async (file) => {
|
|
564
603
|
const stat = await fs.stat(path.join(this.currentDir, file));
|
|
@@ -567,7 +606,7 @@ async execute(input) {
|
|
|
567
606
|
fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
|
|
568
607
|
} catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
|
|
569
608
|
}
|
|
570
|
-
|
|
609
|
+
// tncproject auth failed: Could not read .tncproject — re-download it from the dashboard
|
|
571
610
|
async connectCommand() {
|
|
572
611
|
console.log(chalk.cyan('🔌 Connecting...'));
|
|
573
612
|
try { await this.ws.connect(); }
|
|
@@ -647,13 +686,13 @@ async execute(input) {
|
|
|
647
686
|
executeSystemCommand(command) {
|
|
648
687
|
return new Promise((resolve) => {
|
|
649
688
|
const child = spawn(command, [], {
|
|
650
|
-
cwd:
|
|
689
|
+
cwd: this.currentDir,
|
|
651
690
|
shell: true,
|
|
652
691
|
stdio: 'inherit'
|
|
653
692
|
});
|
|
654
693
|
child.on('close', (code) => {
|
|
655
694
|
// Update currentDir in case command changed it (cd won't but others might) http://localhost:3001
|
|
656
|
-
try { this.currentDir = process.cwd(); } catch {}
|
|
695
|
+
try { this.currentDir = process.cwd(); } catch { }
|
|
657
696
|
process.stdout.write('\r\n');
|
|
658
697
|
resolve();
|
|
659
698
|
});
|
|
@@ -665,7 +704,7 @@ async execute(input) {
|
|
|
665
704
|
}
|
|
666
705
|
|
|
667
706
|
showWelcome() {
|
|
668
|
-
try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
|
|
707
|
+
try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch { }
|
|
669
708
|
console.log(chalk.cyan(`
|
|
670
709
|
╔══════════════════════════════════════════════════════╗
|
|
671
710
|
║ ThinkNCollab Shell v0.0.10 ║
|
|
@@ -677,19 +716,19 @@ async execute(input) {
|
|
|
677
716
|
try {
|
|
678
717
|
if (fs.existsSync(this.config.historyFile))
|
|
679
718
|
this.history = fs.readFileSync(this.config.historyFile, 'utf8').split('\n').filter(l => l.trim());
|
|
680
|
-
} catch {}
|
|
719
|
+
} catch { }
|
|
681
720
|
}
|
|
682
721
|
|
|
683
722
|
saveHistory() {
|
|
684
|
-
try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
|
|
723
|
+
try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch { }
|
|
685
724
|
}
|
|
686
725
|
|
|
687
|
-
cleanup() {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
726
|
+
cleanup() {
|
|
727
|
+
this.api.logout(); // session end + heartbeat stop + lastSeen update
|
|
728
|
+
if (this.tcpServer) { try { this.tcpServer.close(); } catch { } }
|
|
729
|
+
if (this.ws) this.ws.disconnect();
|
|
730
|
+
this.saveHistory();
|
|
731
|
+
}
|
|
693
732
|
}
|
|
694
733
|
|
|
695
734
|
module.exports = ThinkNCollabShell;
|