thinkncollab-shell 0.0.10 → 0.0.12
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 +5 -2
- package/src/commands/tasks/complete.js +563 -43
- package/src/commands/tasks/create-task.js +2 -2
- package/src/commands/tasks/task.js +162 -65
- package/src/commands/tasks/tasks.js +4 -3
- package/src/core/api-client.js +1 -1
- package/src/core/shell.js +337 -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,271 @@ 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: 'notification',
|
|
351
|
+
level: d.type,
|
|
352
|
+
title: d.title,
|
|
353
|
+
message: d.message,
|
|
354
|
+
taskTitle: d.taskTitle,
|
|
355
|
+
assignedBy: d.assignedBy,
|
|
356
|
+
});
|
|
320
357
|
});
|
|
321
|
-
});
|
|
322
358
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
359
|
+
this.ws.on('error', (e) => {
|
|
360
|
+
console.log(chalk.red(`⚠️ WebSocket error: ${e.message}`));
|
|
361
|
+
this.pushNotification({ type: 'notification', level: 'error', title: 'WebSocket Error', message: e.message });
|
|
362
|
+
});
|
|
363
|
+
}
|
|
328
364
|
|
|
329
365
|
// ─── Start ────────────────────────────────────────────────────────────────
|
|
330
366
|
|
|
@@ -345,7 +381,7 @@ setupWebSocketHandlers() {
|
|
|
345
381
|
|
|
346
382
|
// If no .tncproject but autoConnect is set, connect WS anyway
|
|
347
383
|
if (!booted && this.config.websocket.autoConnect) {
|
|
348
|
-
await this.ws.connect().catch(() => {});
|
|
384
|
+
await this.ws.connect().catch(() => { });
|
|
349
385
|
}
|
|
350
386
|
|
|
351
387
|
const { input } = require('@inquirer/prompts');
|
|
@@ -390,7 +426,8 @@ setupWebSocketHandlers() {
|
|
|
390
426
|
try {
|
|
391
427
|
result = await this.api._request('POST', '/config/verify-config', {
|
|
392
428
|
fileBase64: cfg.fileBase64,
|
|
393
|
-
roomId:
|
|
429
|
+
roomId: cfg.roomId,
|
|
430
|
+
machineId: os.hostname(),
|
|
394
431
|
});
|
|
395
432
|
} catch (err) {
|
|
396
433
|
console.log(chalk.red(`❌ tncproject auth failed: ${err.message}`));
|
|
@@ -401,11 +438,12 @@ setupWebSocketHandlers() {
|
|
|
401
438
|
// Save session exactly like manual login
|
|
402
439
|
this.api._saveSession({
|
|
403
440
|
token: result.shellToken,
|
|
441
|
+
shellSessionId: result.shellSessionId || null,
|
|
404
442
|
user: {
|
|
405
|
-
_id:
|
|
406
|
-
userId:
|
|
407
|
-
email:
|
|
408
|
-
name:
|
|
443
|
+
_id: result._id.toString(),
|
|
444
|
+
userId: result._id.toString(),
|
|
445
|
+
email: result.email,
|
|
446
|
+
name: result.name,
|
|
409
447
|
userType: 'User',
|
|
410
448
|
},
|
|
411
449
|
timestamp: new Date().toISOString(),
|
|
@@ -434,44 +472,44 @@ setupWebSocketHandlers() {
|
|
|
434
472
|
return true;
|
|
435
473
|
}
|
|
436
474
|
|
|
437
|
-
async execute(input) {
|
|
438
|
-
|
|
475
|
+
async execute(input) {
|
|
476
|
+
if (!input.trim()) return;
|
|
439
477
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
478
|
+
const args = this.parseCommand(input);
|
|
479
|
+
const cmdName = args[0].toLowerCase();
|
|
480
|
+
const cmdArgs = args.slice(1);
|
|
443
481
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
482
|
+
// Skip sensitive commands
|
|
483
|
+
const skipLog = ['login', 'logout', 'set', 'unset'];
|
|
484
|
+
if (!skipLog.includes(cmdName)) {
|
|
485
|
+
await this.api.logEvent('command', null, { cmd: cmdName });
|
|
486
|
+
}
|
|
449
487
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
488
|
+
if (this.commands.has(cmdName)) {
|
|
489
|
+
try { await this.commands.get(cmdName).handler(cmdArgs, this); }
|
|
490
|
+
catch (err) { console.log(chalk.red(`Error: ${err.message}`)); }
|
|
491
|
+
process.stdout.write('\r\n');
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (this.commandRegistry) {
|
|
496
|
+
const handled = await this.commandRegistry.execute(input, this);
|
|
497
|
+
if (handled) {
|
|
498
|
+
process.stdout.write('\r\n');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
456
502
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
503
|
+
const blockedOnWindows = ['open', 'which', 'grep', 'touch', 'cat'];
|
|
504
|
+
if (process.platform === 'win32' && blockedOnWindows.includes(cmdName)) {
|
|
505
|
+
console.log(chalk.red(`'${cmdName}' is not a thinknsh command.`));
|
|
506
|
+
console.log(chalk.dim(` Type 'help' to see available commands.`));
|
|
460
507
|
process.stdout.write('\r\n');
|
|
461
508
|
return;
|
|
462
509
|
}
|
|
463
|
-
}
|
|
464
510
|
|
|
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;
|
|
511
|
+
await this.executeSystemCommand(input);
|
|
471
512
|
}
|
|
472
|
-
|
|
473
|
-
await this.executeSystemCommand(input);
|
|
474
|
-
}
|
|
475
513
|
// ─── Prompt ───────────────────────────────────────────────────────────────
|
|
476
514
|
|
|
477
515
|
getPromptText() {
|
|
@@ -481,7 +519,7 @@ async execute(input) {
|
|
|
481
519
|
if (this.ws.isConnected?.()) p += '● ';
|
|
482
520
|
|
|
483
521
|
let displayPath = this.currentDir;
|
|
484
|
-
const homeDir
|
|
522
|
+
const homeDir = os.homedir();
|
|
485
523
|
|
|
486
524
|
if (process.platform === 'win32') {
|
|
487
525
|
if (displayPath.toLowerCase() === homeDir.toLowerCase()) displayPath = '~';
|
|
@@ -495,7 +533,7 @@ async execute(input) {
|
|
|
495
533
|
return `${this.username}@thinknsh:${displayPath} $`;
|
|
496
534
|
}
|
|
497
535
|
|
|
498
|
-
getPrompt()
|
|
536
|
+
getPrompt() { return this.getPromptText(); }
|
|
499
537
|
printMessage(msg) { console.log(msg); }
|
|
500
538
|
|
|
501
539
|
// ─── Built-ins ────────────────────────────────────────────────────────────
|
|
@@ -503,28 +541,28 @@ async execute(input) {
|
|
|
503
541
|
registerBuiltinCommands() {
|
|
504
542
|
const r = (name, handler, desc) => this.commands.set(name, { handler, description: desc });
|
|
505
543
|
|
|
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('~',
|
|
544
|
+
r('help', this.helpCommand.bind(this), 'Show available commands');
|
|
545
|
+
r('exit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
546
|
+
r('quit', () => { this.cleanup(); process.exit(0); }, 'Exit the shell');
|
|
547
|
+
r('clear', () => console.clear(), 'Clear screen');
|
|
548
|
+
r('history', this.historyCommand.bind(this), 'Show command history');
|
|
549
|
+
r('cd', this.cdCommand.bind(this), 'Change directory');
|
|
550
|
+
r('pwd', () => console.log(process.cwd()), 'Print working directory');
|
|
551
|
+
r('ls', this.lsCommand.bind(this), 'List files');
|
|
552
|
+
r('connect', this.connectCommand.bind(this), 'Connect to WebSocket');
|
|
553
|
+
r('disconnect', () => this.ws.disconnect(), 'Disconnect WebSocket');
|
|
554
|
+
r('status', this.statusCommand.bind(this), 'Show connection status');
|
|
555
|
+
r('join', this.joinCommand.bind(this), 'Join a room');
|
|
556
|
+
r('leave', () => this.ws.leaveRoom?.(), 'Leave current room');
|
|
557
|
+
r('rooms', this.roomsCommand.bind(this), 'List rooms');
|
|
558
|
+
r('say', this.sayCommand.bind(this), 'Send a message');
|
|
559
|
+
r('msg', this.sayCommand.bind(this), 'Send a message');
|
|
560
|
+
r('set', this.setCommand.bind(this), 'Set a variable');
|
|
561
|
+
r('unset', this.unsetCommand.bind(this), 'Unset a variable');
|
|
562
|
+
|
|
563
|
+
this.addAlias('ll', 'ls -la');
|
|
564
|
+
this.addAlias('..', 'cd ..');
|
|
565
|
+
this.addAlias('~', 'cd ~');
|
|
528
566
|
}
|
|
529
567
|
|
|
530
568
|
registerCommand(name, handler, description = '') { this.commands.set(name, { handler, description }); }
|
|
@@ -543,7 +581,7 @@ async execute(input) {
|
|
|
543
581
|
const target = args.join(' ').trim() || os.homedir();
|
|
544
582
|
try {
|
|
545
583
|
let newPath;
|
|
546
|
-
if (target === '~')
|
|
584
|
+
if (target === '~') newPath = os.homedir();
|
|
547
585
|
else if (target === '..') newPath = path.dirname(this.currentDir);
|
|
548
586
|
else newPath = path.resolve(this.currentDir, target);
|
|
549
587
|
newPath = path.normalize(newPath);
|
|
@@ -557,8 +595,8 @@ async execute(input) {
|
|
|
557
595
|
|
|
558
596
|
async lsCommand(args) {
|
|
559
597
|
try {
|
|
560
|
-
const files
|
|
561
|
-
const showAll
|
|
598
|
+
const files = await fs.readdir(this.currentDir);
|
|
599
|
+
const showAll = args.includes('-a');
|
|
562
600
|
const filtered = showAll ? files : files.filter(f => !f.startsWith('.'));
|
|
563
601
|
const fileList = await Promise.all(filtered.map(async (file) => {
|
|
564
602
|
const stat = await fs.stat(path.join(this.currentDir, file));
|
|
@@ -567,7 +605,7 @@ async execute(input) {
|
|
|
567
605
|
fileList.forEach(f => f.isDir ? console.log(chalk.blue(f.name + '/')) : console.log(f.name));
|
|
568
606
|
} catch (err) { console.log(chalk.red(`ls: ${err.message}`)); }
|
|
569
607
|
}
|
|
570
|
-
|
|
608
|
+
// tncproject auth failed: Could not read .tncproject — re-download it from the dashboard
|
|
571
609
|
async connectCommand() {
|
|
572
610
|
console.log(chalk.cyan('🔌 Connecting...'));
|
|
573
611
|
try { await this.ws.connect(); }
|
|
@@ -647,13 +685,13 @@ async execute(input) {
|
|
|
647
685
|
executeSystemCommand(command) {
|
|
648
686
|
return new Promise((resolve) => {
|
|
649
687
|
const child = spawn(command, [], {
|
|
650
|
-
cwd:
|
|
688
|
+
cwd: this.currentDir,
|
|
651
689
|
shell: true,
|
|
652
690
|
stdio: 'inherit'
|
|
653
691
|
});
|
|
654
692
|
child.on('close', (code) => {
|
|
655
693
|
// Update currentDir in case command changed it (cd won't but others might) http://localhost:3001
|
|
656
|
-
try { this.currentDir = process.cwd(); } catch {}
|
|
694
|
+
try { this.currentDir = process.cwd(); } catch { }
|
|
657
695
|
process.stdout.write('\r\n');
|
|
658
696
|
resolve();
|
|
659
697
|
});
|
|
@@ -665,7 +703,7 @@ async execute(input) {
|
|
|
665
703
|
}
|
|
666
704
|
|
|
667
705
|
showWelcome() {
|
|
668
|
-
try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch {}
|
|
706
|
+
try { const figlet = require('figlet'); console.log(chalk.cyan(figlet.textSync('ThinkNCollab', { font: 'Standard' }))); } catch { }
|
|
669
707
|
console.log(chalk.cyan(`
|
|
670
708
|
╔══════════════════════════════════════════════════════╗
|
|
671
709
|
║ ThinkNCollab Shell v0.0.10 ║
|
|
@@ -677,19 +715,19 @@ async execute(input) {
|
|
|
677
715
|
try {
|
|
678
716
|
if (fs.existsSync(this.config.historyFile))
|
|
679
717
|
this.history = fs.readFileSync(this.config.historyFile, 'utf8').split('\n').filter(l => l.trim());
|
|
680
|
-
} catch {}
|
|
718
|
+
} catch { }
|
|
681
719
|
}
|
|
682
720
|
|
|
683
721
|
saveHistory() {
|
|
684
|
-
try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch {}
|
|
722
|
+
try { fs.writeFileSync(this.config.historyFile, this.history.slice(-this.config.maxHistory).join('\n')); } catch { }
|
|
685
723
|
}
|
|
686
724
|
|
|
687
|
-
cleanup() {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
725
|
+
cleanup() {
|
|
726
|
+
this.api.logout(); // session end + heartbeat stop + lastSeen update
|
|
727
|
+
if (this.tcpServer) { try { this.tcpServer.close(); } catch { } }
|
|
728
|
+
if (this.ws) this.ws.disconnect();
|
|
729
|
+
this.saveHistory();
|
|
730
|
+
}
|
|
693
731
|
}
|
|
694
732
|
|
|
695
733
|
module.exports = ThinkNCollabShell;
|