Twan07 commited on
Commit
6b825ee
·
verified ·
1 Parent(s): 196144d

Upload 13 files

Browse files
exocore-web/src/console.ts ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { spawn, ChildProcess } from "child_process";
6
+ import { WebSocketServer, WebSocket, RawData } from 'ws';
7
+ import http from 'http';
8
+ import express from 'express';
9
+
10
+ const LOG_DIR: string = path.resolve(__dirname, "../models/data");
11
+ const LOG_FILE: string = path.join(LOG_DIR, "logs.txt");
12
+
13
+ let processInstance: ChildProcess | null = null;
14
+ let isRunning: boolean = false;
15
+ let isAwaitingInput: boolean = false;
16
+
17
+ const INPUT_PROMPT_STRING = "__NEEDS_INPUT__";
18
+
19
+ interface ApiType {
20
+ clearLogFile: () => void;
21
+ broadcast: (msg: string | Buffer, wss?: WebSocketServer, isError?: boolean, silent?: boolean) => void;
22
+ parseExocoreRun: (filePath: string) => { exportCommands: string[]; runCommand: string | null };
23
+ executeCommand: (
24
+ command: string,
25
+ cwd: string,
26
+ onCloseCallback: (outcome: number | string | Error) => void,
27
+ wss?: WebSocketServer
28
+ ) => ChildProcess | null;
29
+ runCommandsSequentially: (
30
+ commands: string[],
31
+ cwd: string,
32
+ onSequenceDone: (success: boolean) => void,
33
+ wss?: WebSocketServer,
34
+ index?: number
35
+ ) => void;
36
+ start: (wss?: WebSocketServer, args?: string) => void;
37
+ stop: (wss?: WebSocketServer) => void;
38
+ restart: (wss?: WebSocketServer, args?: string) => void;
39
+ status: () => "running" | "stopped";
40
+ }
41
+
42
+ const api: ApiType = {
43
+ clearLogFile(): void {
44
+ if (!fs.existsSync(LOG_DIR)) {
45
+ fs.mkdirSync(LOG_DIR, { recursive: true });
46
+ }
47
+ fs.writeFileSync(LOG_FILE, "");
48
+ },
49
+
50
+ broadcast(msg: string | Buffer, wss?: WebSocketServer, isError: boolean = false, silent: boolean = false): void {
51
+ const data = msg.toString();
52
+ if (!fs.existsSync(LOG_DIR)) {
53
+ fs.mkdirSync(LOG_DIR, { recursive: true });
54
+ }
55
+ if (!data.includes(INPUT_PROMPT_STRING)) {
56
+ fs.appendFileSync(LOG_FILE, `${data}`);
57
+ }
58
+
59
+ if (!silent && wss && wss.clients) {
60
+ wss.clients.forEach((client: WebSocket) => {
61
+ if (client.readyState === WebSocket.OPEN) {
62
+ if (data.includes(INPUT_PROMPT_STRING)) {
63
+ const cleanData = data.replace(INPUT_PROMPT_STRING, "").trim();
64
+ isAwaitingInput = true;
65
+ client.send(JSON.stringify({ type: 'INPUT_REQUIRED', payload: cleanData }));
66
+ } else {
67
+ client.send(data);
68
+ }
69
+ }
70
+ });
71
+ }
72
+ if (isError) {
73
+ console.error(`BROADCAST_ERROR_LOG: ${data.trim()}`);
74
+ }
75
+ },
76
+
77
+ parseExocoreRun(filePath: string): { exportCommands: string[]; runCommand: string | null } {
78
+ const raw: string = fs.readFileSync(filePath, "utf8");
79
+ const exportMatch: RegExpMatchArray | null = raw.match(/export\s*=\s*{([\s\S]*?)}/);
80
+ const functionMatch: RegExpMatchArray | null = raw.match(/function\s*=\s*{([\s\S]*?)}/);
81
+ const exportCommands: string[] = [];
82
+
83
+ if (exportMatch && exportMatch[1]) {
84
+ const lines: string[] = exportMatch[1].split(";");
85
+ for (let line of lines) {
86
+ const matchResult: RegExpMatchArray | null = line.match(/["'](.+?)["']/);
87
+ if (matchResult && typeof matchResult[1] === 'string' && matchResult[1].trim() !== '') {
88
+ exportCommands.push(matchResult[1]);
89
+ }
90
+ }
91
+ }
92
+
93
+ let runCommand: string | null = null;
94
+ if (functionMatch && functionMatch[1]) {
95
+ const runMatch: RegExpMatchArray | null = functionMatch[1].match(/run\s*=\s*["'](.+?)["']/);
96
+ if (runMatch && typeof runMatch[1] === 'string' && runMatch[1].trim() !== '') {
97
+ runCommand = runMatch[1];
98
+ }
99
+ }
100
+ return { exportCommands, runCommand };
101
+ },
102
+
103
+ executeCommand(
104
+ command: string,
105
+ cwd: string,
106
+ onCloseCallback: (outcome: number | string | Error) => void,
107
+ wss?: WebSocketServer
108
+ ): ChildProcess | null {
109
+ let proc: ChildProcess | null = null;
110
+ try {
111
+ proc = spawn(command, {
112
+ cwd,
113
+ shell: true,
114
+ env: { ...process.env, FORCE_COLOR: "1", LANG: "en_US.UTF-8" },
115
+ });
116
+ } catch (rawErr: unknown) {
117
+ let errMsg = "Unknown spawn error";
118
+ if (rawErr instanceof Error) errMsg = rawErr.message;
119
+ else if (typeof rawErr === 'string') errMsg = rawErr;
120
+ api.broadcast(`\x1b[31m❌ Spawn error for command "${command}": ${errMsg}\x1b[0m`, wss, true, false);
121
+ if (typeof onCloseCallback === 'function') {
122
+ if (rawErr instanceof Error) onCloseCallback(rawErr);
123
+ else onCloseCallback(new Error(String(rawErr)));
124
+ }
125
+ return null;
126
+ }
127
+
128
+ if (proc.stdout) {
129
+ proc.stdout.on("data", (data: Buffer | string) => api.broadcast(data, wss, false, false));
130
+ }
131
+ if (proc.stderr) {
132
+ proc.stderr.on("data", (data: Buffer | string) => api.broadcast(`\x1b[31m${data.toString()}\x1b[0m`, wss, true, false));
133
+ }
134
+
135
+ proc.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
136
+ if (typeof onCloseCallback === 'function') {
137
+ if (code === null) onCloseCallback(signal || 'signaled');
138
+ else onCloseCallback(code);
139
+ }
140
+ });
141
+
142
+ proc.on("error", (err: Error) => {
143
+ api.broadcast(`\x1b[31m❌ Command execution error for "${command}": ${err.message}\x1b[0m`, wss, true, false);
144
+ if (typeof onCloseCallback === 'function') onCloseCallback(err);
145
+ });
146
+ return proc;
147
+ },
148
+
149
+ runCommandsSequentially(
150
+ commands: string[],
151
+ cwd: string,
152
+ onSequenceDone: (success: boolean) => void,
153
+ wss?: WebSocketServer,
154
+ index: number = 0
155
+ ): void {
156
+ if (index >= commands.length) {
157
+ if (typeof onSequenceDone === 'function') onSequenceDone(true);
158
+ return;
159
+ }
160
+ const cmd = commands[index];
161
+
162
+ if (typeof cmd !== 'string' || cmd.trim() === "") {
163
+ api.broadcast(`\x1b[31m❌ Invalid or empty setup command at index ${index}.\x1b[0m`, wss, true, false);
164
+ if (typeof onSequenceDone === 'function') onSequenceDone(false);
165
+ return;
166
+ }
167
+
168
+ const proc = api.executeCommand(cmd, cwd, (outcome: number | string | Error) => {
169
+ const benignSignal = (typeof outcome === 'string' && (outcome.toUpperCase() === 'SIGTERM' || outcome.toUpperCase() === 'SIGKILL'));
170
+ if (outcome !== 0 && (typeof outcome === 'number' || outcome instanceof Error || (typeof outcome === 'string' && !benignSignal))) {
171
+ const errorMessage = outcome instanceof Error ? outcome.message : String(outcome);
172
+ api.broadcast(`\x1b[31m❌ Setup command "${cmd}" failed: ${errorMessage}\x1b[0m`, wss, true, false);
173
+ if (typeof onSequenceDone === 'function') onSequenceDone(false);
174
+ return;
175
+ }
176
+ api.runCommandsSequentially(commands, cwd, onSequenceDone, wss, index + 1);
177
+ }, wss
178
+ );
179
+
180
+ if (!proc) {
181
+ api.broadcast(`\x1b[31m❌ Failed to initiate setup command "${cmd}".\x1b[0m`, wss, true, false);
182
+ if (typeof onSequenceDone === 'function') onSequenceDone(false);
183
+ }
184
+ },
185
+
186
+ start(wss?: WebSocketServer, args?: string): void {
187
+ if (isRunning) {
188
+ api.broadcast("\x1b[33m⚠️ Process is already running.\x1b[0m", wss, true, false);
189
+ return;
190
+ }
191
+ api.clearLogFile();
192
+ api.broadcast(`\x1b[36m[SYSTEM] Starting process...\x1b[0m`, wss, false, true);
193
+ isAwaitingInput = false;
194
+
195
+ let projectPath: string | undefined;
196
+
197
+ try {
198
+ const configJsonPath: string = path.resolve(process.cwd(), 'config.json');
199
+ if (fs.existsSync(configJsonPath)) {
200
+ const configRaw: string = fs.readFileSync(configJsonPath, 'utf-8');
201
+ if (configRaw.trim() !== "") {
202
+ const configData: any = JSON.parse(configRaw);
203
+ if (configData && typeof configData.project === 'string' && configData.project.trim() !== "") {
204
+ const configJsonDir: string = path.dirname(configJsonPath);
205
+ const resolvedPath: string = path.resolve(configJsonDir, configData.project);
206
+ if (fs.existsSync(resolvedPath) && fs.statSync(resolvedPath).isDirectory()) {
207
+ projectPath = resolvedPath;
208
+ } else {
209
+ api.broadcast(`\x1b[31m❌ Error: Project path from config.json is invalid: ${resolvedPath}\x1b[0m`, wss, true, false);
210
+ return;
211
+ }
212
+ } else {
213
+ api.broadcast(`\x1b[31m❌ Error: 'project' key in config.json is missing or invalid.\x1b[0m`, wss, true, false);
214
+ return;
215
+ }
216
+ } else {
217
+ api.broadcast(`\x1b[31m❌ Error: config.json is empty.\x1b[0m`, wss, true, false);
218
+ return;
219
+ }
220
+ } else {
221
+ api.broadcast(`\x1b[31m❌ Error: Configuration file not found at ${configJsonPath}.\x1b[0m`, wss, true, false);
222
+ return;
223
+ }
224
+ } catch (rawErr: unknown) {
225
+ api.broadcast(`\x1b[31m❌ Error reading or parsing project configuration.\x1b[0m`, wss, true, false);
226
+ return;
227
+ }
228
+
229
+ if (typeof projectPath !== 'string') {
230
+ api.broadcast(`\x1b[31m❌ Project path could not be determined.\x1b[0m`, wss, true, false);
231
+ return;
232
+ }
233
+
234
+ const currentProjectPath: string = projectPath;
235
+ const exocoreRunPath: string = path.join(currentProjectPath, "exocore.run");
236
+
237
+ if (!fs.existsSync(exocoreRunPath)) {
238
+ api.broadcast(`\x1b[31m❌ Missing exocore.run file in ${currentProjectPath}.\x1b[0m`, wss, true, false);
239
+ return;
240
+ }
241
+
242
+ const { exportCommands, runCommand } = api.parseExocoreRun(exocoreRunPath);
243
+
244
+ if (!runCommand) {
245
+ api.broadcast("\x1b[31m❌ Missing run command in exocore.run.\x1b[0m", wss, true, false);
246
+ return;
247
+ }
248
+
249
+ const currentRunCommand: string = args ? `${runCommand} ${args}` : runCommand;
250
+
251
+ api.runCommandsSequentially([...exportCommands], currentProjectPath, (setupSuccess: boolean) => {
252
+ if (!setupSuccess) {
253
+ api.broadcast(`\x1b[31m❌ Setup commands failed. Main process will not start.\x1b[0m`, wss, true, false);
254
+ return;
255
+ }
256
+
257
+ api.broadcast(`\x1b[36m[SYSTEM] Executing: ${currentRunCommand}\x1b[0m`, wss, false, true);
258
+
259
+ let spawnedProc: ChildProcess | null = null;
260
+ try {
261
+ spawnedProc = spawn(currentRunCommand, {
262
+ cwd: currentProjectPath,
263
+ shell: true,
264
+ detached: true,
265
+ env: { ...process.env, FORCE_COLOR: "1", LANG: "en_US.UTF-8" },
266
+ });
267
+ } catch (rawErr: unknown) {
268
+ api.broadcast(`\x1b[31m❌ Failed to spawn main process.\x1b[0m`, wss, true, false);
269
+ return;
270
+ }
271
+
272
+ if (!spawnedProc || typeof spawnedProc.pid !== 'number') {
273
+ api.broadcast(`\x1b[31m❌ Failed to get process handle.\x1b[0m`, wss, true, false);
274
+ return;
275
+ }
276
+
277
+ processInstance = spawnedProc;
278
+ isRunning = true;
279
+ api.broadcast(`\x1b[32m[SYSTEM] Process started with PID: ${processInstance.pid}\x1b[0m`, wss, false, true);
280
+
281
+ if (processInstance.stdout) {
282
+ processInstance.stdout.on("data", (data: Buffer | string) => api.broadcast(data, wss, false, false));
283
+ }
284
+ if (processInstance.stderr) {
285
+ processInstance.stderr.on("data", (data: Buffer | string) =>
286
+ api.broadcast(`\x1b[31m${data.toString()}\x1b[0m`, wss, true, false)
287
+ );
288
+ }
289
+
290
+ processInstance.on("close", (code: number | null, signal: NodeJS.Signals | null) => {
291
+ isRunning = false;
292
+ processInstance = null;
293
+ isAwaitingInput = false;
294
+ const exitReason = signal ? `signal ${signal}` : `code ${code}`;
295
+ api.broadcast(`\x1b[33m[SYSTEM] Process exited with ${exitReason}. It will not be restarted automatically.\x1b[0m`, wss, false, false);
296
+ });
297
+
298
+ processInstance.on("error", (err: Error) => {
299
+ api.broadcast(`\x1b[31m❌ Error with main process: ${err.message}\x1b[0m`, wss, true, false);
300
+ isRunning = false;
301
+ processInstance = null;
302
+ isAwaitingInput = false;
303
+ });
304
+ }, wss);
305
+ },
306
+
307
+ stop(wss?: WebSocketServer): void {
308
+ if (!processInstance || typeof processInstance.pid !== 'number') {
309
+ api.broadcast("\x1b[33m⚠️ No active process to stop.\x1b[0m", wss, true, false);
310
+ if (isRunning) {
311
+ isRunning = false;
312
+ processInstance = null;
313
+ }
314
+ return;
315
+ }
316
+
317
+ if (!isRunning) {
318
+ api.broadcast("\x1b[33m⚠️ Process is already stopped.\x1b[0m", wss, true, false);
319
+ return;
320
+ }
321
+
322
+ const pidToStop: number = processInstance.pid;
323
+ api.broadcast(`\x1b[36m[SYSTEM] Stopping process group PID: ${pidToStop}...\x1b[0m`, wss, false, true);
324
+
325
+ try {
326
+ process.kill(-pidToStop, "SIGTERM");
327
+
328
+ setTimeout(() => {
329
+ if (processInstance && processInstance.pid === pidToStop && !processInstance.killed) {
330
+ api.broadcast(`\x1b[33m[SYSTEM] ⚠️ Process ${pidToStop} unresponsive, sending SIGKILL.\x1b[0m`, wss, true, false);
331
+ try {
332
+ process.kill(-pidToStop, "SIGKILL");
333
+ } catch (rawKillErr: unknown) {
334
+ const err = rawKillErr as NodeJS.ErrnoException;
335
+ if (err.code !== 'ESRCH') {
336
+ api.broadcast(`\x1b[31m[SYSTEM] ❌ Error sending SIGKILL to PID ${pidToStop}: ${err.message}\x1b[0m`, wss, true, false);
337
+ }
338
+ } finally {
339
+ if (processInstance && processInstance.pid === pidToStop) {
340
+ isRunning = false;
341
+ processInstance = null;
342
+ }
343
+ }
344
+ }
345
+ }, 3000);
346
+ } catch (rawTermErr: unknown) {
347
+ const err = rawTermErr as NodeJS.ErrnoException;
348
+ if (err.code !== 'ESRCH') {
349
+ api.broadcast(`\x1b[31m❌ Error sending SIGTERM: ${err.message}\x1b[0m`, wss, true, false);
350
+ }
351
+ isRunning = false;
352
+ processInstance = null;
353
+ }
354
+ },
355
+
356
+ restart(wss?: WebSocketServer, args?: string): void {
357
+ api.broadcast(`\x1b[36m[SYSTEM] Restarting process...\x1b[0m`, wss, false, true);
358
+ api.stop(wss);
359
+ setTimeout(() => {
360
+ api.start(wss, args);
361
+ }, 1000);
362
+ },
363
+
364
+ status(): "running" | "stopped" {
365
+ if (isRunning && processInstance && !processInstance.killed) {
366
+ try {
367
+ process.kill(processInstance.pid!, 0);
368
+ return "running";
369
+ } catch (e) {
370
+ isRunning = false;
371
+ processInstance = null;
372
+ return "stopped";
373
+ }
374
+ }
375
+ return "stopped";
376
+ },
377
+ };
378
+
379
+ export interface RouteHandlerParamsSuperset {
380
+ app?: express.Application;
381
+ req: express.Request;
382
+ res: express.Response;
383
+ wss?: WebSocketServer;
384
+ wssConsole?: WebSocketServer;
385
+ Shellwss?: WebSocketServer;
386
+ server?: http.Server;
387
+ }
388
+
389
+ export interface ExpressRouteModule {
390
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
391
+ path: string;
392
+ install: (params: Partial<RouteHandlerParamsSuperset>) => void;
393
+ }
394
+
395
+ type StartStopRestartParams = Pick<RouteHandlerParamsSuperset, 'req' | 'res' | 'wssConsole'>;
396
+ type ConsoleStatusParams = Pick<RouteHandlerParamsSuperset, 'res'>;
397
+
398
+ export const modules: Array<{
399
+ method: "get" | "post";
400
+ path: string;
401
+ install: (params: any) => void;
402
+ }> = [
403
+ {
404
+ method: "post",
405
+ path: "/start",
406
+ install: ({ req, res, wssConsole }: StartStopRestartParams) => {
407
+ if (!wssConsole) return res.status(500).send("Console WebSocket server not available.");
408
+ api.start(wssConsole, req.body?.args);
409
+ res.send(`Process start initiated.`);
410
+ },
411
+ },
412
+ {
413
+ method: "post",
414
+ path: "/stop",
415
+ install: ({ res, wssConsole }: StartStopRestartParams) => {
416
+ if (!wssConsole) return res.status(500).send("Console WebSocket server not available.");
417
+ api.stop(wssConsole);
418
+ res.send(`Process stop initiated.`);
419
+ },
420
+ },
421
+ {
422
+ method: "post",
423
+ path: "/restart",
424
+ install: ({ req, res, wssConsole }: StartStopRestartParams) => {
425
+ if (!wssConsole) return res.status(500).send("Console WebSocket server not available.");
426
+ api.restart(wssConsole, req.body?.args);
427
+ res.send(`Process restart initiated.`);
428
+ },
429
+ },
430
+ {
431
+ method: "get",
432
+ path: "/console/status",
433
+ install: ({ res }: ConsoleStatusParams) => {
434
+ res.send(api.status());
435
+ },
436
+ },
437
+ ];
438
+
439
+ export function setupConsoleWS(wssConsole: WebSocketServer): void {
440
+ wssConsole.on("connection", (ws: WebSocket) => {
441
+ console.log("Console WebSocket client connected");
442
+ try {
443
+ const logContent: string = fs.existsSync(LOG_FILE) ? fs.readFileSync(LOG_FILE, "utf8") : "\x1b[33mℹ️ No previous logs.\x1b[0m";
444
+ ws.send(logContent);
445
+ } catch (err) {
446
+ ws.send("\x1b[31mError reading past logs.\x1b[0m");
447
+ }
448
+
449
+ ws.on("message", (rawMessage: RawData) => {
450
+ try {
451
+ const message = JSON.parse(rawMessage.toString());
452
+
453
+ if (message.type === 'STDIN_INPUT' && typeof message.payload === 'string') {
454
+ if (processInstance && isRunning && isAwaitingInput && processInstance.stdin && processInstance.stdin.writable) {
455
+ processInstance.stdin.write(message.payload + '\n');
456
+ isAwaitingInput = false;
457
+ } else {
458
+ api.broadcast(`\x1b[33m[SYSTEM-WARN] Process not running or not awaiting input.\x1b[0m`, wssConsole, true, false);
459
+ }
460
+ }
461
+ } catch (e) {
462
+ console.log(`Received non-command message from client: ${rawMessage.toString()}`);
463
+ }
464
+ });
465
+
466
+ ws.on("close", () => console.log("Console WebSocket client disconnected"));
467
+ ws.on("error", (error: Error) => console.error("Console WebSocket client error:", error));
468
+ });
469
+ }
exocore-web/src/file-manager.ts ADDED
@@ -0,0 +1,730 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import archiver, { ArchiverError } from 'archiver';
4
+ import multer, { Multer } from 'multer';
5
+ import extract from 'extract-zip';
6
+ import yauzl, { Options as YauzlOptions, ZipFile as YauzlZipFile, Entry as YauzlEntry } from 'yauzl';
7
+ import { Request, Response, Application, RequestHandler } from 'express';
8
+
9
+ const configJsonPath: string = path.resolve(__dirname, '../config.json');
10
+ const UPLOAD_TEMP_DIR: string = path.resolve(__dirname, '../upload_temp');
11
+
12
+ let currentProjectPathFromConfig: string = '';
13
+ let currentBaseDir: string = '';
14
+
15
+ async function pathExists(p: string): Promise<boolean> {
16
+ try {
17
+ await fs.promises.access(p, fs.constants.F_OK);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ async function ensureDirExists(dirPath: string): Promise<void> {
25
+ if (dirPath && !(await pathExists(dirPath))) {
26
+ await fs.promises.mkdir(dirPath, { recursive: true });
27
+ }
28
+ }
29
+
30
+ ensureDirExists(UPLOAD_TEMP_DIR).catch(err => {
31
+ console.error("FATAL: Failed to create UPLOAD_TEMP_DIR on startup:", err);
32
+ });
33
+
34
+ interface AppConfig {
35
+ project?: string;
36
+ [key: string]: any;
37
+ }
38
+
39
+ function updatePathsFromConfig(): boolean {
40
+ const oldRawProjectPath = currentProjectPathFromConfig;
41
+ const oldBaseDir = currentBaseDir;
42
+ const wasPreviouslyValid = oldBaseDir !== '';
43
+
44
+ let newRawProjectPathCandidate: string = '';
45
+ let newResolvedBaseDirCandidate: string = '';
46
+ let isNowValidConfig: boolean = false;
47
+
48
+ try {
49
+ if (!fs.existsSync(configJsonPath)) {
50
+ newRawProjectPathCandidate = '';
51
+ newResolvedBaseDirCandidate = '';
52
+ isNowValidConfig = false;
53
+ } else {
54
+ const rawConfigData: string = fs.readFileSync(configJsonPath, 'utf-8');
55
+ if (rawConfigData.trim() === '') {
56
+ newRawProjectPathCandidate = '';
57
+ newResolvedBaseDirCandidate = '';
58
+ isNowValidConfig = false;
59
+ } else {
60
+ const parsedConfig: AppConfig = JSON.parse(rawConfigData);
61
+ if (parsedConfig && typeof parsedConfig.project === 'string') {
62
+ newRawProjectPathCandidate = parsedConfig.project;
63
+ const trimmedProjectPath: string = parsedConfig.project.trim();
64
+ if (trimmedProjectPath !== '') {
65
+ const configJsonDir: string = path.dirname(configJsonPath);
66
+ newResolvedBaseDirCandidate = path.resolve(configJsonDir, trimmedProjectPath);
67
+ isNowValidConfig = true;
68
+ } else {
69
+ newResolvedBaseDirCandidate = '';
70
+ isNowValidConfig = false;
71
+ }
72
+ } else {
73
+ newRawProjectPathCandidate = '';
74
+ newResolvedBaseDirCandidate = '';
75
+ isNowValidConfig = false;
76
+ }
77
+ }
78
+ }
79
+ } catch (error: any) {
80
+ const errMsg = error instanceof Error ? error.message : String(error);
81
+ console.error(`[ConfigMonitor] ERROR reading/parsing ${configJsonPath}: ${errMsg}`);
82
+ newRawProjectPathCandidate = '';
83
+ newResolvedBaseDirCandidate = '';
84
+ isNowValidConfig = false;
85
+ }
86
+
87
+ currentProjectPathFromConfig = newRawProjectPathCandidate;
88
+ currentBaseDir = isNowValidConfig ? newResolvedBaseDirCandidate : '';
89
+
90
+ if (isNowValidConfig) {
91
+ if (!wasPreviouslyValid) {
92
+ console.log(`[ConfigMonitor] PROJECT LOADED: Project path "${currentProjectPathFromConfig.trim()}" configured. Base directory: ${currentBaseDir}`);
93
+ } else if (currentBaseDir !== oldBaseDir) {
94
+ console.log(`[ConfigMonitor] PROJECT CHANGED: Project path updated to "${currentProjectPathFromConfig.trim()}". New base directory: ${currentBaseDir}`);
95
+ }
96
+ } else {
97
+ if (wasPreviouslyValid) {
98
+ console.warn(`[ConfigMonitor] PROJECT UNCONFIGURED: Project path "${oldRawProjectPath}" (was base: "${oldBaseDir}") is now invalid, removed, or config error. Waiting for configuration.`);
99
+ }
100
+ }
101
+ return isNowValidConfig;
102
+ }
103
+
104
+ function startConfigMonitor(): void {
105
+ console.log(`[ConfigMonitor] Initializing: Monitoring ${configJsonPath} for project configuration.`);
106
+ const initiallyValid = updatePathsFromConfig();
107
+
108
+ if (!initiallyValid) {
109
+ console.warn(`[ConfigMonitor] Initial Status: No valid project path found in ${configJsonPath}. Waiting for configuration.`);
110
+ }
111
+
112
+ setInterval(() => {
113
+ updatePathsFromConfig();
114
+ }, 1000);
115
+ }
116
+
117
+ startConfigMonitor();
118
+
119
+ const upload: Multer = multer({ dest: UPLOAD_TEMP_DIR });
120
+
121
+ function runMulterMiddleware(req: Request, res: Response, multerMiddleware: RequestHandler): Promise<void> {
122
+ return new Promise<void>((resolve, reject) => {
123
+ multerMiddleware(req, res, (err: any) => {
124
+ if (err) {
125
+ return reject(err);
126
+ }
127
+ resolve();
128
+ });
129
+ });
130
+ }
131
+
132
+ async function safePathResolve(relativePathInput: string | undefined | null, checkExists: boolean = false): Promise<string> {
133
+ if (!currentBaseDir) {
134
+ console.error("[safePathResolve] Error: Attempted to operate while project base directory is not configured.");
135
+ throw new Error('Project base directory is not configured. Please check config.json and ensure the "project" key is set to a valid path.');
136
+ }
137
+
138
+ const activeBaseDir: string = path.resolve(currentBaseDir);
139
+
140
+ if (relativePathInput === '') {
141
+ await ensureDirExists(activeBaseDir);
142
+ if (checkExists && !(await pathExists(activeBaseDir))) {
143
+ throw new Error(`Base project directory not found: ${activeBaseDir}`);
144
+ }
145
+ return activeBaseDir;
146
+ }
147
+
148
+ if (typeof relativePathInput !== 'string' || relativePathInput.trim() === '') {
149
+ throw new Error('Invalid path provided: path is empty or not a string (and not an intentional empty string for base directory).');
150
+ }
151
+
152
+ const trimmedRelativePath: string = relativePathInput.trim();
153
+ const resolvedPath: string = path.resolve(activeBaseDir, trimmedRelativePath);
154
+
155
+ if (!resolvedPath.startsWith(activeBaseDir + path.sep) && resolvedPath !== activeBaseDir) {
156
+ console.error(`[safePathResolve] Unsafe path attempt: Resolved "${resolvedPath}" is outside base "${activeBaseDir}" from relative "${trimmedRelativePath}"`);
157
+ throw new Error('Unsafe path: Access denied due to path traversal attempt.');
158
+ }
159
+
160
+ if (checkExists && !(await pathExists(resolvedPath))) {
161
+ const displayPath: string = path.relative(activeBaseDir, resolvedPath) || trimmedRelativePath;
162
+ console.warn(`[safePathResolve] Path not found: "${displayPath}" (resolved from "${trimmedRelativePath}" within base "${activeBaseDir}")`);
163
+ throw new Error(`Path not found: ${displayPath}`);
164
+ }
165
+ return resolvedPath;
166
+ }
167
+
168
+ interface FileManagerRouteParams {
169
+ req: Request;
170
+ res: Response;
171
+ app?: Application;
172
+ }
173
+
174
+ type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
175
+
176
+ interface FileManagerExpressRouteModule {
177
+ method: HttpMethod;
178
+ path: string;
179
+ install: (params: FileManagerRouteParams) => Promise<void> | void;
180
+ }
181
+
182
+ export const modules: FileManagerExpressRouteModule[] = [
183
+ {
184
+ method: 'post',
185
+ path: '/file/list', // <--- Here it is
186
+ install: async ({ req, res }: FileManagerRouteParams) => {
187
+ try {
188
+ const dirToList: string = await safePathResolve('');
189
+
190
+ const itemNames: string[] = await fs.promises.readdir(dirToList);
191
+ const items = await Promise.all(itemNames.map(async (name) => {
192
+ const fullPath: string = path.join(dirToList, name);
193
+ const stat: fs.Stats = await fs.promises.stat(fullPath);
194
+ return { name, isDir: stat.isDirectory() };
195
+ }));
196
+ res.json(items);
197
+ } catch (e: any) {
198
+ const errMsg: string = e instanceof Error ? e.message : String(e);
199
+ console.error('Error in /file/list:', errMsg, e);
200
+ if (errMsg.includes('Project base directory is not configured')) {
201
+ res.status(503).send(errMsg);
202
+ } else if (e instanceof Error && e.message.startsWith('Path not found')) {
203
+ res.status(404).send("Root project directory not found or accessible.");
204
+ } else {
205
+ res.status(500).send(errMsg);
206
+ }
207
+ }
208
+ },
209
+ },
210
+
211
+ {
212
+ method: 'post',
213
+ path: '/file/list',
214
+ install: async ({ req, res }: FileManagerRouteParams) => {
215
+ try {
216
+ const dirToList: string = await safePathResolve('');
217
+
218
+ const itemNames: string[] = await fs.promises.readdir(dirToList);
219
+ const items = await Promise.all(itemNames.map(async (name) => {
220
+ const fullPath: string = path.join(dirToList, name);
221
+ const stat: fs.Stats = await fs.promises.stat(fullPath);
222
+ return { name, isDir: stat.isDirectory() };
223
+ }));
224
+ res.json(items);
225
+ } catch (e: any) {
226
+ const errMsg: string = e instanceof Error ? e.message : String(e);
227
+ console.error('Error in /file/list:', errMsg, e);
228
+ if (errMsg.includes('Project base directory is not configured')) {
229
+ res.status(503).send(errMsg);
230
+ } else if (e instanceof Error && e.message.startsWith('Path not found')) {
231
+ res.status(404).send("Root project directory not found or accessible.");
232
+ } else {
233
+ res.status(500).send(errMsg);
234
+ }
235
+ }
236
+ },
237
+ },
238
+ {
239
+ method: 'post',
240
+ path: '/file/open',
241
+ install: async ({ req, res }: FileManagerRouteParams) => {
242
+ try {
243
+ const filePath: string = await safePathResolve(req.body.file, true);
244
+ const content: string = await fs.promises.readFile(filePath, 'utf8');
245
+ res.send(content);
246
+ } catch (e: any) {
247
+ const errMsg: string = e instanceof Error ? e.message : String(e);
248
+ console.error('Error in /file/open:', errMsg, e);
249
+ let statusCode: number = 500;
250
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
251
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
252
+ else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
253
+ res.status(statusCode).send(errMsg);
254
+ }
255
+ },
256
+ },
257
+ {
258
+ method: 'post',
259
+ path: '/file/save',
260
+ install: async ({ req, res }: FileManagerRouteParams) => {
261
+ try {
262
+ const fileRelativePath: string = req.body.file;
263
+ const content: string = req.body.content;
264
+
265
+ if (typeof content !== 'string') {
266
+ res.status(400).send('Invalid content for file save.');
267
+ return;
268
+ }
269
+ const filePath: string = await safePathResolve(fileRelativePath, false);
270
+ const dirForFile: string = path.dirname(filePath);
271
+ await ensureDirExists(dirForFile);
272
+
273
+ await fs.promises.writeFile(filePath, content, 'utf8');
274
+ res.send('File saved');
275
+ } catch (e: any) {
276
+ const errMsg: string = e instanceof Error ? e.message : String(e);
277
+ console.error('Error in /file/save:', errMsg, e);
278
+ let statusCode: number = 500;
279
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
280
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
281
+ res.status(statusCode).send(errMsg);
282
+ }
283
+ },
284
+ },
285
+ {
286
+ method: 'post',
287
+ path: '/file/upload',
288
+ install: async ({ req, res }: FileManagerRouteParams) => {
289
+ try {
290
+ if (!currentBaseDir) {
291
+ throw new Error('Project base directory is not configured. Cannot accept uploads.');
292
+ }
293
+ await runMulterMiddleware(req, res, upload.single('file'));
294
+
295
+ if (!req.file) {
296
+ res.status(400).send('No file uploaded.');
297
+ return;
298
+ }
299
+ const uploadedFile: Express.Multer.File = req.file as Express.Multer.File;
300
+
301
+ const relativeTargetSubfolder: string = typeof req.body.path === 'string' && req.body.path.trim() !== ''
302
+ ? req.body.path.trim()
303
+ : '';
304
+
305
+ const targetDirectory: string = await safePathResolve(relativeTargetSubfolder, false);
306
+
307
+ try {
308
+ const targetStat: fs.Stats = await fs.promises.stat(targetDirectory);
309
+ if (!targetStat.isDirectory()) {
310
+ await fs.promises.unlink(uploadedFile.path);
311
+ res.status(400).send('Target path exists but is not a directory.');
312
+ return;
313
+ }
314
+ } catch (statError: any) {
315
+ if (statError && typeof statError === 'object' && 'code' in statError && statError.code === 'ENOENT') {
316
+ await ensureDirExists(targetDirectory);
317
+ } else {
318
+ await fs.promises.unlink(uploadedFile.path);
319
+ throw statError;
320
+ }
321
+ }
322
+
323
+ const finalDestination: string = path.join(targetDirectory, uploadedFile.originalname);
324
+ if (!finalDestination.startsWith(targetDirectory + path.sep) && finalDestination !== targetDirectory) {
325
+ await fs.promises.unlink(uploadedFile.path);
326
+ throw new Error("Upload failed: constructed final path is outside target directory.");
327
+ }
328
+
329
+ await fs.promises.rename(uploadedFile.path, finalDestination);
330
+ const displayPath: string = path.relative(path.resolve(currentBaseDir), finalDestination).split(path.sep).join('/') || uploadedFile.originalname;
331
+ res.send('File uploaded to ' + displayPath);
332
+
333
+ } catch (e: any) {
334
+ const errMsg: string = e instanceof Error ? e.message : String(e);
335
+ console.error('Error in /file/upload handler:', errMsg, e);
336
+ const currentReqFile = req.file as Express.Multer.File | undefined;
337
+ if (currentReqFile && currentReqFile.path && await pathExists(currentReqFile.path)) {
338
+ await fs.promises.unlink(currentReqFile.path).catch(unlinkErr => console.error("Failed to cleanup temp upload file:", (unlinkErr as Error).message));
339
+ }
340
+ if (!res.headersSent) {
341
+ let statusCode: number = 500;
342
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
343
+ else if (e instanceof multer.MulterError) statusCode = 400;
344
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
345
+ res.status(statusCode).send(errMsg);
346
+ }
347
+ }
348
+ },
349
+ },
350
+ {
351
+ method: 'post',
352
+ path: '/file/download',
353
+ install: async ({ req, res }: FileManagerRouteParams) => {
354
+ try {
355
+ const filePath: string = await safePathResolve(req.body.file, true);
356
+ const stat: fs.Stats = await fs.promises.stat(filePath);
357
+ if (!stat.isFile()) {
358
+ res.status(400).send('Specified path is not a file.');
359
+ return;
360
+ }
361
+ res.download(filePath);
362
+ } catch (e: any) {
363
+ const errMsg: string = e instanceof Error ? e.message : String(e);
364
+ console.error('Error in /file/download:', errMsg, e);
365
+ let statusCode: number = 500;
366
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
367
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
368
+ else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
369
+ res.status(statusCode).send(errMsg);
370
+ }
371
+ },
372
+ },
373
+ {
374
+ method: 'post',
375
+ path: '/file/download-zip',
376
+ install: async ({ req, res }: FileManagerRouteParams) => {
377
+ try {
378
+ const folderRelativePath: string = typeof req.body.folder === 'string' ? req.body.folder.trim() : '';
379
+ const folderToZip: string = await safePathResolve(folderRelativePath, true);
380
+
381
+ const stat: fs.Stats = await fs.promises.stat(folderToZip);
382
+ if (!stat.isDirectory()) {
383
+ res.status(400).send('Specified path is not a directory.');
384
+ return;
385
+ }
386
+
387
+ const zipName: string = (path.basename(folderToZip) || 'archive') + '.zip';
388
+ res.setHeader('Content-Disposition', `attachment; filename="${zipName}"`);
389
+ res.setHeader('Content-Type', 'application/zip');
390
+
391
+ const archive = archiver('zip', { zlib: { level: 9 } });
392
+
393
+ archive.on('warning', (err: ArchiverError) => {
394
+ console.warn('[Archiver Warning]', err.code, err.message);
395
+ });
396
+ archive.on('error', (err: Error) => {
397
+ console.error('[Archiver Error]', err.message, err);
398
+ if (!res.headersSent) {
399
+ res.status(500).send({ error: 'Failed to create zip file: ' + err.message });
400
+ } else if (res.writable && !res.writableEnded) {
401
+ res.end();
402
+ }
403
+ });
404
+ archive.pipe(res);
405
+ archive.directory(folderToZip, false);
406
+ await archive.finalize();
407
+
408
+ } catch (e: any) {
409
+ const errMsg: string = e instanceof Error ? e.message : String(e);
410
+ console.error('Error in /file/download-zip setup:', errMsg, e);
411
+ if (!res.headersSent) {
412
+ let statusCode: number = 500;
413
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
414
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
415
+ else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
416
+ res.status(statusCode).send(errMsg);
417
+ }
418
+ }
419
+ },
420
+ },
421
+ {
422
+ method: 'post',
423
+ path: '/file/unzip',
424
+ install: async ({ req, res }: FileManagerRouteParams) => {
425
+ const {
426
+ zipFilePath: zipFileRelativePath,
427
+ destinationPath: destinationRelativePathInput,
428
+ overwrite
429
+ }: { zipFilePath: string; destinationPath?: string; overwrite?: boolean } = req.body;
430
+
431
+ try {
432
+ if (typeof zipFileRelativePath !== 'string' || zipFileRelativePath.trim() === '') {
433
+ res.status(400).send('zipFilePath is required.'); return;
434
+ }
435
+ const absoluteZipFilePath: string = await safePathResolve(zipFileRelativePath.trim(), true);
436
+ if (!(await fs.promises.stat(absoluteZipFilePath)).isFile()) {
437
+ res.status(400).send(`Specified zipFilePath '${zipFileRelativePath}' is not a file.`); return;
438
+ }
439
+ if (!absoluteZipFilePath.toLowerCase().endsWith('.zip')) {
440
+ console.warn(`[Unzip] Warning: File '${zipFileRelativePath}' may not be a .zip file based on extension. Attempting unzip.`);
441
+ }
442
+
443
+ let absoluteDestinationDir: string;
444
+ const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
445
+
446
+ if (typeof destinationRelativePathInput === 'string' && destinationRelativePathInput.trim() !== '') {
447
+ absoluteDestinationDir = await safePathResolve(destinationRelativePathInput.trim(), false);
448
+ } else {
449
+ absoluteDestinationDir = path.dirname(absoluteZipFilePath);
450
+ if (!absoluteDestinationDir.startsWith(resolvedCurrentBaseDir + path.sep) && absoluteDestinationDir !== resolvedCurrentBaseDir) {
451
+ console.error(`[Unzip] Default destination dir "${absoluteDestinationDir}" is outside base "${resolvedCurrentBaseDir}"`);
452
+ throw new Error('Default extraction path is outside the allowed project directory.');
453
+ }
454
+ }
455
+
456
+ try {
457
+ const destStat: fs.Stats = await fs.promises.stat(absoluteDestinationDir);
458
+ if (!destStat.isDirectory()) {
459
+ res.status(400).send(`Destination path '${path.relative(resolvedCurrentBaseDir, absoluteDestinationDir).split(path.sep).join('/') || '.'}' exists and is not a directory.`); return;
460
+ }
461
+ } catch (statError: any) {
462
+ if (statError && typeof statError === 'object' && 'code' in statError && statError.code === 'ENOENT') {
463
+ } else {
464
+ throw statError;
465
+ }
466
+ }
467
+ await ensureDirExists(absoluteDestinationDir);
468
+
469
+ if (!overwrite) {
470
+ const conflictingEntries: string[] = [];
471
+ const openZipForRead = (filePath: string, options: YauzlOptions): Promise<YauzlZipFile> =>
472
+ new Promise<YauzlZipFile>((resolveMain, rejectMain) => {
473
+ yauzl.open(filePath, options, (err: Error | null, zipfile?: YauzlZipFile) => {
474
+ if (err || !zipfile) rejectMain(err || new Error("Failed to open zipfile. Path: " + filePath));
475
+ else resolveMain(zipfile);
476
+ });
477
+ });
478
+
479
+ let zipfile: YauzlZipFile | null = null;
480
+ try {
481
+ zipfile = await openZipForRead(absoluteZipFilePath, { lazyEntries: true });
482
+ const currentZipfile: YauzlZipFile = zipfile;
483
+ await new Promise<void>((resolveLoop, rejectLoop) => {
484
+ currentZipfile.on('error', rejectLoop);
485
+ currentZipfile.on('end', resolveLoop);
486
+ currentZipfile.on('entry', async (entry: YauzlEntry) => {
487
+ const targetPath: string = path.resolve(absoluteDestinationDir, entry.fileName);
488
+ if (!targetPath.startsWith(absoluteDestinationDir)) {
489
+ console.warn(`[Unzip] Skipped potentially unsafe entry during conflict check: ${entry.fileName} (resolves outside destination)`);
490
+ currentZipfile.readEntry();
491
+ return;
492
+ }
493
+ if (await pathExists(targetPath)) {
494
+ const existingStat: fs.Stats = await fs.promises.stat(targetPath);
495
+ const entryIsDirectory: boolean = entry.fileName.endsWith('/');
496
+ if (existingStat.isDirectory() !== entryIsDirectory) {
497
+ conflictingEntries.push(`${entry.fileName} (type mismatch: existing is ${existingStat.isDirectory() ? 'dir' : 'file'}, zip entry is ${entryIsDirectory ? 'dir' : 'file'})`);
498
+ } else if (!entryIsDirectory) {
499
+ conflictingEntries.push(entry.fileName);
500
+ }
501
+ }
502
+ currentZipfile.readEntry();
503
+ });
504
+ currentZipfile.readEntry();
505
+ });
506
+ } finally {
507
+ zipfile?.close();
508
+ }
509
+
510
+ if (conflictingEntries.length > 0) {
511
+ res.status(409).send({
512
+ message: "Extraction would overwrite or conflict with existing files/directories. Please use 'overwrite: true' in your request to proceed.",
513
+ conflicts: conflictingEntries
514
+ });
515
+ return;
516
+ }
517
+ }
518
+
519
+ await extract(absoluteZipFilePath, { dir: absoluteDestinationDir });
520
+ const displayDestPath: string = path.relative(resolvedCurrentBaseDir, absoluteDestinationDir).split(path.sep).join('/') || '.';
521
+ res.send(`Successfully unzipped '${path.basename(zipFileRelativePath)}' to '${displayDestPath}'`);
522
+
523
+ } catch (e: any) {
524
+ const errMsg: string = e instanceof Error ? e.message : String(e);
525
+ let statusCode: number = 500;
526
+ let clientMsg: string = `Failed to unzip: ${errMsg}`;
527
+
528
+ if (errMsg.includes('Project base directory is not configured')) { statusCode = 503; clientMsg = errMsg; }
529
+ else if (e instanceof Error) {
530
+ if (e.message.startsWith('Unsafe path')) { statusCode = 403; clientMsg = errMsg; }
531
+ else if (e.message.startsWith('Path not found')) { statusCode = 404; clientMsg = errMsg; }
532
+ else if (e.message.includes('is not a directory')) { statusCode = 400; clientMsg = errMsg; }
533
+ else if (errMsg.toLowerCase().includes("invalid") && (errMsg.toLowerCase().includes("zip") || errMsg.toLowerCase().includes("archive"))) {
534
+ statusCode = 400; clientMsg = `Failed to unzip '${zipFileRelativePath || 'file'}': May be corrupted or not a valid ZIP.`;
535
+ } else if (errMsg.includes("yauzl") && (errMsg.includes("Не найден указанный файл") || errMsg.includes("end of central directory record signature not found") || errMsg.includes("Failed to open zipfile"))) {
536
+ statusCode = 400; clientMsg = `Failed to read '${zipFileRelativePath || 'file'}': Invalid or corrupted ZIP file.`;
537
+ } else if (e.message.includes('EEXIST') || e.message.includes('ENOTDIR') || (e.message.includes('entry') && e.message.includes('isDirectory') && e.message.includes('false but is a directory'))) {
538
+ statusCode = 409;
539
+ clientMsg = `Failed to unzip: A conflict occurred during extraction (e.g., a file exists where a directory was expected, or vice-versa). Original error: ${errMsg}`;
540
+ }
541
+ }
542
+ console.error(`Error in /file/unzip (HTTP ${statusCode}):`, errMsg, e);
543
+ if (!res.headersSent) res.status(statusCode).send(clientMsg);
544
+ }
545
+ },
546
+ },
547
+ {
548
+ method: 'post',
549
+ path: '/file/create',
550
+ install: async ({ req, res }: FileManagerRouteParams) => {
551
+ try {
552
+ const fileRelativePath: string = req.body.file;
553
+ if (typeof fileRelativePath !== 'string' || fileRelativePath.trim() === '') {
554
+ res.status(400).send('File path is required.'); return;
555
+ }
556
+ const filePath: string = await safePathResolve(fileRelativePath.trim(), false);
557
+ if (await pathExists(filePath)) {
558
+ res.status(400).send('File or folder already exists at the target path.'); return;
559
+ }
560
+ await ensureDirExists(path.dirname(filePath));
561
+ await fs.promises.writeFile(filePath, '');
562
+ res.send('File created: ' + (path.relative(path.resolve(currentBaseDir), filePath).split(path.sep).join('/') || path.basename(filePath)));
563
+ } catch (e: any) {
564
+ const errMsg: string = e instanceof Error ? e.message : String(e);
565
+ console.error('Error in /file/create:', errMsg, e);
566
+ let statusCode: number = 500;
567
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
568
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
569
+ res.status(statusCode).send(errMsg);
570
+ }
571
+ },
572
+ },
573
+ {
574
+ method: 'post',
575
+ path: '/file/create-folder',
576
+ install: async ({ req, res }: FileManagerRouteParams) => {
577
+ try {
578
+ const folderRelativePath: string = req.body.folder;
579
+ if (typeof folderRelativePath !== 'string' || folderRelativePath.trim() === '') {
580
+ res.status(400).send('Folder path is required.'); return;
581
+ }
582
+ const folderPath: string = await safePathResolve(folderRelativePath.trim(), false);
583
+ if (await pathExists(folderPath)) {
584
+ res.status(400).send('File or folder already exists at the target path.'); return;
585
+ }
586
+ await fs.promises.mkdir(folderPath, { recursive: true });
587
+ res.send('Folder created: ' + (path.relative(path.resolve(currentBaseDir), folderPath).split(path.sep).join('/') || path.basename(folderPath)));
588
+ } catch (e: any) {
589
+ const errMsg: string = e instanceof Error ? e.message : String(e);
590
+ console.error('Error in /file/create-folder:', errMsg, e);
591
+ let statusCode: number = 500;
592
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
593
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
594
+ res.status(statusCode).send(errMsg);
595
+ }
596
+ },
597
+ },
598
+ {
599
+ method: 'post',
600
+ path: '/file/open-folder',
601
+ install: async ({ req, res }: FileManagerRouteParams) => {
602
+ try {
603
+ const relativeFolderPath: string = typeof req.body.folder === 'string' ? req.body.folder.trim() : '';
604
+ const folderPath: string = await safePathResolve(relativeFolderPath, true);
605
+
606
+ if (!(await fs.promises.stat(folderPath)).isDirectory()) {
607
+ res.status(400).send(`Specified path '${relativeFolderPath}' is not a directory.`); return;
608
+ }
609
+ const itemNames: string[] = await fs.promises.readdir(folderPath);
610
+ const items = await Promise.all(itemNames.map(async (name) => {
611
+ const fullPath: string = path.join(folderPath, name);
612
+ const itemStat: fs.Stats = await fs.promises.stat(fullPath);
613
+ return { name, isDir: itemStat.isDirectory(), size: itemStat.size, lastModified: itemStat.mtimeMs };
614
+ }));
615
+
616
+ const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
617
+ const currentDisplayPath: string = path.relative(resolvedCurrentBaseDir, folderPath).split(path.sep).join('/');
618
+
619
+ res.json({
620
+ currentPath: folderPath === resolvedCurrentBaseDir ? '' : currentDisplayPath,
621
+ items,
622
+ });
623
+ } catch (e: any) {
624
+ const errMsg: string = e instanceof Error ? e.message : String(e);
625
+ console.error('Error in /file/open-folder:', errMsg, e);
626
+ let statusCode: number = 500;
627
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
628
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
629
+ else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
630
+ res.status(statusCode).send(errMsg);
631
+ }
632
+ },
633
+ },
634
+ {
635
+ method: 'post',
636
+ path: '/file/rename',
637
+ install: async ({ req, res }: FileManagerRouteParams) => {
638
+ try {
639
+ const { oldPath: oldRelativePath, newPath: newRelativePath }: { oldPath: string; newPath: string } = req.body;
640
+ if (typeof oldRelativePath !== 'string' || !oldRelativePath.trim() ||
641
+ typeof newRelativePath !== 'string' || !newRelativePath.trim()) {
642
+ res.status(400).send('Old and new paths are required and cannot be empty.'); return;
643
+ }
644
+
645
+ const trimmedOldRelativePath: string = oldRelativePath.trim();
646
+ const trimmedNewRelativePath: string = newRelativePath.trim();
647
+
648
+ if (trimmedOldRelativePath === trimmedNewRelativePath) {
649
+ res.status(400).send('Old and new paths cannot be the same.'); return;
650
+ }
651
+
652
+ const oldFullPath: string = await safePathResolve(trimmedOldRelativePath, true);
653
+
654
+ const newName: string = path.basename(trimmedNewRelativePath);
655
+ if (!newName || newName === '.' || newName === '..') {
656
+ res.status(400).send('New name is invalid.'); return;
657
+ }
658
+ const newParentDirRelative: string = path.dirname(trimmedNewRelativePath);
659
+ const newParentDirAbsolute: string = await safePathResolve(newParentDirRelative, false);
660
+ await ensureDirExists(newParentDirAbsolute);
661
+
662
+ const newFullPath: string = path.join(newParentDirAbsolute, newName);
663
+
664
+ const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
665
+ if (!newFullPath.startsWith(resolvedCurrentBaseDir + path.sep) && newFullPath !== resolvedCurrentBaseDir) {
666
+ throw new Error('Unsafe new path: Target is outside the allowed directory.');
667
+ }
668
+
669
+ if (oldFullPath === newFullPath) {
670
+ res.status(400).send('Resolved old and new paths are identical.'); return;
671
+ }
672
+ if (await pathExists(newFullPath)) {
673
+ res.status(400).send(`Item at new path '${trimmedNewRelativePath}' already exists.`); return;
674
+ }
675
+
676
+ await fs.promises.rename(oldFullPath, newFullPath);
677
+ res.send('Renamed successfully to ' + (path.relative(resolvedCurrentBaseDir, newFullPath).split(path.sep).join('/') || newName));
678
+ } catch (e: any) {
679
+ const errMsg: string = e instanceof Error ? e.message : String(e);
680
+ console.error('Error in /file/rename:', errMsg, e);
681
+ let statusCode: number = 500;
682
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
683
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
684
+ else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
685
+ res.status(statusCode).send(errMsg);
686
+ }
687
+ },
688
+ },
689
+ {
690
+ method: 'post',
691
+ path: '/file/delete',
692
+ install: async ({ req, res }: FileManagerRouteParams) => {
693
+ try {
694
+ const itemRelativePath: string = req.body.path;
695
+ if (typeof itemRelativePath !== 'string' || !itemRelativePath.trim()) {
696
+ res.status(400).send('Path for deletion is required.'); return;
697
+ }
698
+ const trimmedItemRelativePath: string = itemRelativePath.trim();
699
+ if (trimmedItemRelativePath === '.' || trimmedItemRelativePath === '/') {
700
+ res.status(400).send('Invalid path for deletion. Cannot delete root.'); return;
701
+ }
702
+
703
+ const itemFullPath: string = await safePathResolve(trimmedItemRelativePath, true);
704
+ const resolvedCurrentBaseDir: string = path.resolve(currentBaseDir);
705
+
706
+ if (itemFullPath === resolvedCurrentBaseDir) {
707
+ res.status(400).send('Cannot delete the root project directory itself.'); return;
708
+ }
709
+
710
+ const stat: fs.Stats = await fs.promises.stat(itemFullPath);
711
+ const displayPath: string = path.relative(resolvedCurrentBaseDir, itemFullPath).split(path.sep).join('/') || trimmedItemRelativePath;
712
+
713
+ if (stat.isDirectory()) {
714
+ await fs.promises.rm(itemFullPath, { recursive: true, force: true });
715
+ } else {
716
+ await fs.promises.unlink(itemFullPath);
717
+ }
718
+ res.send(`${stat.isDirectory() ? 'Folder' : 'File'} deleted: ${displayPath}`);
719
+ } catch (e: any) {
720
+ const errMsg: string = e instanceof Error ? e.message : String(e);
721
+ console.error('Error in /file/delete:', errMsg, e);
722
+ let statusCode: number = 500;
723
+ if (errMsg.includes('Project base directory is not configured')) statusCode = 503;
724
+ else if (e instanceof Error && e.message.startsWith('Unsafe path')) statusCode = 403;
725
+ else if (e instanceof Error && e.message.startsWith('Path not found')) statusCode = 404;
726
+ res.status(statusCode).send(errMsg);
727
+ }
728
+ },
729
+ },
730
+ ];
exocore-web/src/forgot.ts ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import axios, { AxiosResponse } from 'axios';
4
+ import fs from 'fs';
5
+ import fsPromises from 'fs/promises';
6
+ import path from 'path';
7
+ import { Request, Response } from 'express';
8
+
9
+ const ACC_FILE_PATH = path.resolve(__dirname, '../models/data/acc.json');
10
+ const ACC_DIR_PATH = path.dirname(ACC_FILE_PATH);
11
+
12
+ const api = {
13
+ getLink: async (): Promise<string> => {
14
+ const res = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
15
+ if (typeof res.data === 'string' && res.data.startsWith('http')) {
16
+ return res.data;
17
+ }
18
+ if (res.data && typeof res.data === 'object' && typeof res.data.link === 'string') {
19
+ return res.data.link;
20
+ }
21
+ throw new Error('Failed to fetch external link from pastebin');
22
+ },
23
+
24
+ forgotPass: async (req: Request, res: Response) => {
25
+ try {
26
+ const { identifier, action, otpCode, newPass } = req.body;
27
+
28
+ if (!identifier || !action) {
29
+ return res.status(400).json({ error: 'Missing identifier or action' });
30
+ }
31
+
32
+ const link = await api.getLink();
33
+ const apiURL = `${link}/forgot-password`;
34
+
35
+ const body: Record<string, any> = { identifier };
36
+
37
+ if (action === 'SendOtp') {
38
+ body.action = 'sent';
39
+ } else if (action === 'ResetPassword') {
40
+ if (!otpCode || !newPass) {
41
+ return res.status(400).json({ error: 'Missing otpCode or newPass for reset' });
42
+ }
43
+ body.action = 'submit';
44
+ body.otpCode = otpCode;
45
+ body.newPass = newPass;
46
+ } else if (action === 'VerifyOtp') {
47
+ if (!otpCode) {
48
+ return res.status(400).json({ error: 'Missing otpCode for verification' });
49
+ }
50
+ body.action = 'verify';
51
+ body.otpCode = otpCode;
52
+ } else {
53
+ return res.status(400).json({ error: 'Invalid action' });
54
+ }
55
+
56
+ const response: AxiosResponse = await axios.post(apiURL, body);
57
+ const data = response.data;
58
+
59
+ if (data.status === 'success' || data.message) {
60
+ try {
61
+ if (!fs.existsSync(ACC_DIR_PATH)) {
62
+ await fsPromises.mkdir(ACC_DIR_PATH, { recursive: true });
63
+ }
64
+ await fsPromises.writeFile(ACC_FILE_PATH, JSON.stringify(data, null, 2));
65
+ } catch (err) {
66
+ console.error('[ForgotPass Route] Failed to write acc.json:', err);
67
+ }
68
+ return res.json({ success: true, data });
69
+ } else {
70
+ return res.status(400).json({ success: false, error: data.error || 'Unexpected error' });
71
+ }
72
+ } catch (err) {
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ console.error('[ForgotPass Route] Error:', msg);
75
+ return res.status(500).json({ error: 'Server error', details: msg });
76
+ }
77
+ }
78
+ };
79
+
80
+ export const modules = [
81
+ {
82
+ method: 'post',
83
+ path: '/forgotpass',
84
+ install: async ({ req, res }: { req: Request; res: Response }) => {
85
+ return api.forgotPass(req, res);
86
+ },
87
+ },
88
+ ];
exocore-web/src/login.ts ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import axios, { AxiosError, AxiosResponse } from 'axios';
4
+ import fs from 'fs';
5
+ import fsPromises from 'fs/promises';
6
+ import path from 'path';
7
+ import { Request, Response } from 'express';
8
+
9
+ console.log(`[Login Module] src/login.ts file is being evaluated. __dirname is: ${__dirname}`);
10
+
11
+ interface PastebinData {
12
+ link?: string;
13
+ [key: string]: any; // To accommodate if res.data is an object with more fields
14
+ }
15
+
16
+ interface LoginClientRequestBody {
17
+ user: string;
18
+ pass: string;
19
+ }
20
+
21
+ interface LoginApiPayload {
22
+ user: string;
23
+ pass: string;
24
+ }
25
+
26
+ interface LoginApiResponseData {
27
+ token: string;
28
+ userDetails: object; // Consider defining a more specific interface if the structure is known
29
+ message: string;
30
+ status: string; // e.g., "success"
31
+ }
32
+
33
+ function isLoginClientRequestBody(body: any): body is LoginClientRequestBody {
34
+ if (body && typeof body === 'object' &&
35
+ typeof body.user === 'string' && body.user.length > 0 &&
36
+ typeof body.pass === 'string' && body.pass.length > 0) {
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ const ACC_FILE_PATH: string = path.resolve(__dirname, '../models/data/acc.json');
43
+ const ACC_DIR_PATH: string = path.dirname(ACC_FILE_PATH);
44
+
45
+ const api = {
46
+ async getLink(): Promise<string> {
47
+ console.log("[Login API - getLink] Attempting to fetch link...");
48
+ try {
49
+ const res: AxiosResponse<PastebinData | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
50
+ let extractedLink: string | null = null;
51
+
52
+ if (res.data && typeof res.data === 'object' && typeof res.data.link === 'string') {
53
+ extractedLink = res.data.link;
54
+ } else if (typeof res.data === 'string' && res.data.startsWith('http')) {
55
+ extractedLink = res.data;
56
+ }
57
+
58
+ if (!extractedLink) {
59
+ console.error("[Login API - getLink] Invalid/missing link in Pastebin response:", res.data);
60
+ throw new Error('Invalid or missing link in Pastebin response data.');
61
+ }
62
+ console.log("[Login API - getLink] Fetched link:", extractedLink);
63
+ return extractedLink;
64
+ } catch (err: unknown) {
65
+ const error = err instanceof Error ? err : new Error(String(err));
66
+ console.error("[Login API - getLink] Failed to get base link.", error.message, error.cause || error);
67
+ throw new Error('Failed to get base link.', { cause: error });
68
+ }
69
+ },
70
+
71
+ async login(body: LoginApiPayload): Promise<LoginApiResponseData> {
72
+ console.log("[Login API - login] Attempting external API login.");
73
+ try {
74
+ const link = await api.getLink();
75
+ console.log("[Login API - login] External API Link:", link);
76
+ console.log("[Login API - login] Payload to external API:", body);
77
+
78
+ const res: AxiosResponse<LoginApiResponseData> = await axios.post(`${link}/signin`, body);
79
+ console.log("[Login API - login] Response from external API:", res.data);
80
+ return res.data;
81
+ } catch (err: unknown) {
82
+ const error = err instanceof Error ? err : new Error(String(err));
83
+ console.error("[Login API - login] Error with /signin endpoint.", error.message, error.cause || error);
84
+ if (axios.isAxiosError(error) && error.response) {
85
+ console.error("[Login API - login] Axios error data:", error.response.data, "Status:", error.response.status);
86
+ }
87
+ throw new Error('Failed to contact/process signin endpoint.', { cause: error });
88
+ }
89
+ },
90
+ };
91
+
92
+ interface LoginRouteParams {
93
+ req: Request<any, any, unknown>; // Request body is unknown, will use typeguard
94
+ res: Response;
95
+ }
96
+
97
+ interface LoginExpressRouteModule {
98
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
99
+ path: string;
100
+ install: (params: LoginRouteParams) => Promise<void> | void;
101
+ }
102
+
103
+ export const modules: LoginExpressRouteModule[] = [
104
+ {
105
+ method: 'post',
106
+ path: '/login',
107
+ install: async ({ req, res }: LoginRouteParams): Promise<void> => {
108
+ try {
109
+ const requestBody: unknown = req.body;
110
+
111
+ if (isLoginClientRequestBody(requestBody)) {
112
+ const clientData: LoginClientRequestBody = requestBody;
113
+
114
+ const apiLoginPayload: LoginApiPayload = {
115
+ user: clientData.user,
116
+ pass: clientData.pass,
117
+ };
118
+
119
+ const responseData: LoginApiResponseData = await api.login(apiLoginPayload);
120
+
121
+ if (responseData && responseData.status === 'success' && responseData.token) {
122
+ console.log("[Login Route] External login successful.");
123
+ try {
124
+ if (!fs.existsSync(ACC_DIR_PATH)) {
125
+ await fsPromises.mkdir(ACC_DIR_PATH, { recursive: true });
126
+ }
127
+ await fsPromises.writeFile(ACC_FILE_PATH, JSON.stringify(responseData, null, 2));
128
+ } catch (fileErr: unknown) {
129
+ const fError = fileErr instanceof Error ? fileErr : new Error(String(fileErr));
130
+ console.error(`[Login Route] CRITICAL: Failed to write acc.json:`, fError);
131
+ }
132
+ res.json({ success: true, data: responseData });
133
+ } else {
134
+ console.warn("[Login Route] External login failed or unexpected response:", responseData);
135
+ res.status(401).json({ success: false, error: responseData?.message || 'Login failed.' });
136
+ }
137
+ } else {
138
+ console.warn("[Login Route] Validation failed: req.body does not conform to LoginClientRequestBody. Received:", requestBody);
139
+ res.status(400).json({ success: false, error: 'Invalid request format: user and pass are required strings.' });
140
+ return;
141
+ }
142
+ } catch (err: unknown) {
143
+ let error: Error & { cause?: unknown };
144
+ if (err instanceof Error) {
145
+ error = err as Error & { cause?: unknown };
146
+ } else {
147
+ error = new Error(String(err)) as Error & { cause?: unknown };
148
+ }
149
+
150
+ let errorMessage = "An unknown error occurred.";
151
+ let statusCode = 500;
152
+ console.error(`[Login Route] Overall error:`, error.message, error.cause || error);
153
+
154
+ const originalError: unknown = error.cause || error;
155
+
156
+ if (axios.isAxiosError(originalError)) {
157
+ const axiosError = originalError as AxiosError<any>; // Type assertion
158
+ errorMessage = axiosError.message;
159
+ statusCode = axiosError.response?.status || 500;
160
+ const responseDataError = axiosError.response?.data?.error || axiosError.response?.data?.message;
161
+ if (responseDataError) {
162
+ errorMessage = `Login API Error (${statusCode}): ${responseDataError}`;
163
+ }
164
+ } else if (originalError instanceof Error) {
165
+ errorMessage = originalError.message || String(originalError);
166
+ } else {
167
+ errorMessage = String(originalError);
168
+ }
169
+
170
+
171
+ console.error(`[Login Route] Error to client: ${errorMessage}, Status: ${statusCode}`);
172
+ if (!res.headersSent) {
173
+ res.status(statusCode).json({ success: false, error: errorMessage });
174
+ } else {
175
+ console.warn("[Login Route] Headers sent, cannot send error to client.");
176
+ }
177
+ }
178
+ },
179
+ },
180
+ ];
exocore-web/src/logout.ts ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { Request, Response, Application } from 'express';
6
+ import { WebSocketServer } from 'ws';
7
+ import { Server as HttpServer } from 'http';
8
+
9
+ interface LogoutRouteParams {
10
+ req: Request;
11
+ res: Response;
12
+ app?: Application;
13
+ wss?: WebSocketServer;
14
+ wssConsole?: WebSocketServer;
15
+ Shellwss?: WebSocketServer;
16
+ server?: HttpServer;
17
+ }
18
+
19
+ interface LogoutExpressRouteModule {
20
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
21
+ path: string;
22
+ install: (params: Pick<LogoutRouteParams, 'req' | 'res'>) => Promise<void> | void;
23
+ }
24
+
25
+ const ACC_FILE_PATH: string = path.resolve(__dirname, '../models/data/acc.json');
26
+ const ACC_DIR_PATH: string = path.dirname(ACC_FILE_PATH);
27
+
28
+ export const modules: LogoutExpressRouteModule[] = [
29
+ {
30
+ method: 'post',
31
+ path: '/logout',
32
+ install: async ({ req, res }: Pick<LogoutRouteParams, 'req' | 'res'>): Promise<void> => {
33
+ const html: string = `
34
+ <!DOCTYPE html>
35
+ <html lang="en">
36
+ <head>
37
+ <meta charset="UTF-8" />
38
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
39
+ <title>Logout</title>
40
+ <script>
41
+ localStorage.removeItem("exocore-token");
42
+ localStorage.removeItem("exocore-cookies");
43
+ // Optional: Redirect after clearing localStorage
44
+ // window.location.href = '/login'; // Or your desired redirect path
45
+ </script>
46
+ <style>
47
+ body { font-family: monospace, sans-serif; padding: 20px; }
48
+ pre { white-space: pre-wrap; word-wrap: break-word; }
49
+ </style>
50
+ </head>
51
+ <body>
52
+ <p>You have been logged out. localStorage items 'exocore-token' and 'exocore-cookies' have been cleared.</p>
53
+ <pre>{
54
+ "status": "success"
55
+ }</pre>
56
+ </body>
57
+ </html>`;
58
+
59
+ res.setHeader('Content-Type', 'text/html');
60
+ res.send(html);
61
+
62
+ try {
63
+ if (!fs.existsSync(ACC_DIR_PATH)) {
64
+ await fs.promises.mkdir(ACC_DIR_PATH, { recursive: true });
65
+ }
66
+ await fs.promises.writeFile(ACC_FILE_PATH, JSON.stringify({}, null, 2));
67
+ } catch (fileError: unknown) {
68
+ const errMsg = fileError instanceof Error ? fileError.message : String(fileError);
69
+ console.error(`Failed to clear or write acc.json during logout: ${errMsg}`, fileError);
70
+ }
71
+ },
72
+ },
73
+ ];
exocore-web/src/otp.ts ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import axios, { AxiosResponse, AxiosError } from 'axios';
4
+ import { Request, Response, Application } from 'express';
5
+ import { WebSocketServer } from 'ws';
6
+ import { Server as HttpServer } from 'http';
7
+
8
+ interface PastebinResponse {
9
+ link: string;
10
+ }
11
+
12
+ interface OtpRequestBody {
13
+ token: string;
14
+ cookies: Record<string, any> | string;
15
+ action: string;
16
+ otp?: string;
17
+ }
18
+
19
+ interface OtpApiResponseData {
20
+ success: boolean;
21
+ message: string;
22
+ data?: Record<string, any> | any[];
23
+ }
24
+
25
+ const api = {
26
+ async getLink(): Promise<string> {
27
+ try {
28
+ const res: AxiosResponse<PastebinResponse | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
29
+ if (res.data && typeof res.data === 'object' && typeof (res.data as PastebinResponse).link === 'string') {
30
+ return (res.data as PastebinResponse).link;
31
+ } else if (typeof res.data === 'string' && res.data.startsWith('http')) {
32
+ return res.data;
33
+ }
34
+ throw new Error('Invalid or missing link in Pastebin response data.');
35
+ } catch (err: unknown) {
36
+ throw new Error('Failed to fetch API link.', { cause: err instanceof Error ? err : new Error(String(err)) });
37
+ }
38
+ },
39
+
40
+ async performOtpAction(body: OtpRequestBody): Promise<OtpApiResponseData> {
41
+ try {
42
+ const link: string = await api.getLink();
43
+ const { token, cookies, action, otp } = body;
44
+
45
+ const payload: {
46
+ token: string;
47
+ cookies: Record<string, any> | string;
48
+ action: string;
49
+ otp?: string;
50
+ } = {
51
+ token,
52
+ cookies,
53
+ action,
54
+ };
55
+ if (otp) {
56
+ payload.otp = otp;
57
+ }
58
+
59
+ const res: AxiosResponse<OtpApiResponseData> = await axios.post(`${link}/otp`, payload);
60
+ return res.data;
61
+ } catch (err: unknown) {
62
+ throw new Error('Failed to perform OTP action.', { cause: err instanceof Error ? err : new Error(String(err)) });
63
+ }
64
+ },
65
+ };
66
+
67
+ interface OtpRouteParams {
68
+ req: Request<any, any, OtpRequestBody>;
69
+ res: Response;
70
+ app?: Application;
71
+ wss?: WebSocketServer;
72
+ wssConsole?: WebSocketServer;
73
+ Shellwss?: WebSocketServer;
74
+ server?: HttpServer;
75
+ }
76
+
77
+ interface OtpExpressRouteModule {
78
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
79
+ path: string;
80
+ install: (params: OtpRouteParams) => Promise<void> | void;
81
+ }
82
+
83
+ export const modules: OtpExpressRouteModule[] = [
84
+ {
85
+ method: 'post',
86
+ path: '/otp',
87
+ install: async ({ req, res }: OtpRouteParams): Promise<void> => {
88
+ try {
89
+ const { token, cookies, action, otp } = req.body;
90
+
91
+ if (typeof token !== 'string' || !token ||
92
+ (cookies === undefined || cookies === null) ||
93
+ typeof action !== 'string' || !action) {
94
+ res.status(400).json({
95
+ status: 'error',
96
+ message: 'Missing required fields: token, cookies, and action must be provided.',
97
+ });
98
+ return;
99
+ }
100
+
101
+ if (action.toLowerCase() === 'verify' && (typeof otp !== 'string' || !otp)) {
102
+ res.status(400).json({
103
+ status: 'error',
104
+ message: 'OTP is required for verify action.',
105
+ });
106
+ return;
107
+ }
108
+
109
+ let safeCookies: Record<string, any>;
110
+ if (typeof cookies === 'string') {
111
+ try {
112
+ safeCookies = JSON.parse(cookies);
113
+ } catch (parseError: unknown) {
114
+ const errMsg = parseError instanceof Error ? parseError.message : String(parseError);
115
+ res.status(400).json({ status: 'error', message: `Invalid cookies JSON format: ${errMsg}` });
116
+ return;
117
+ }
118
+ } else if (typeof cookies === 'object' && cookies !== null) {
119
+ safeCookies = cookies;
120
+ } else {
121
+ res.status(400).json({ status: 'error', message: 'Cookies must be a JSON string or an object.' });
122
+ return;
123
+ }
124
+
125
+ const responseData: OtpApiResponseData = await api.performOtpAction({ token, cookies: safeCookies, action, otp });
126
+
127
+ res.json({
128
+ status: 'success',
129
+ data: responseData,
130
+ });
131
+ } catch (err: unknown) {
132
+ let errorMessage = "An unknown error occurred in OTP handler.";
133
+ let statusCode = 500;
134
+
135
+ const topLevelError = err instanceof Error ? err : new Error(String(err));
136
+ const originalError: unknown = topLevelError.cause || topLevelError;
137
+
138
+ if (axios.isAxiosError(originalError)) {
139
+ const axiosError = originalError as AxiosError<any>;
140
+ errorMessage = axiosError.message;
141
+ if (axiosError.response) {
142
+ statusCode = axiosError.response.status;
143
+ const responseDataError = axiosError.response.data?.error || axiosError.response.data?.message;
144
+ errorMessage = `OTP API Error (${statusCode}): ${responseDataError || axiosError.message}`;
145
+ console.error(`Axios error response from /otp endpoint: Status ${statusCode}`, axiosError.response.data);
146
+ } else if (axiosError.request) {
147
+ errorMessage = "No response received from OTP service.";
148
+ statusCode = 503;
149
+ console.error("Axios no response error for /otp:", axiosError.request);
150
+ }
151
+ } else if (originalError instanceof Error) {
152
+ errorMessage = originalError.message;
153
+ } else {
154
+ errorMessage = String(originalError);
155
+ }
156
+
157
+ console.error(`Error in /otp route: ${errorMessage}`, topLevelError);
158
+ if (!res.headersSent) {
159
+ res.status(statusCode).json({
160
+ status: 'error',
161
+ message: errorMessage,
162
+ });
163
+ }
164
+ }
165
+ },
166
+ },
167
+ ];
exocore-web/src/panel.ts ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import fsPromises from 'fs/promises';
4
+ import path from 'path';
5
+ import { Request, Response } from 'express';
6
+
7
+ interface LoginClientRequestBody {
8
+ user: string;
9
+ pass: string;
10
+ }
11
+
12
+ function isLoginClientRequestBody(body: any): body is LoginClientRequestBody {
13
+ return (body && typeof body === 'object' &&
14
+ typeof body.user === 'string' && body.user.length > 0 &&
15
+ typeof body.pass === 'string' && body.pass.length > 0);
16
+ }
17
+
18
+ interface LoginRouteParams {
19
+ req: Request<any, any, unknown>;
20
+ res: Response;
21
+ }
22
+
23
+ interface ConfigData {
24
+ user: string;
25
+ pass: string;
26
+ [key: string]: any;
27
+ }
28
+
29
+ interface PanelExpressRouteModule {
30
+ method: 'post';
31
+ path: string;
32
+ install: (params: LoginRouteParams) => Promise<void>;
33
+ }
34
+
35
+ export const modules: PanelExpressRouteModule[] = [
36
+ {
37
+ method: 'post',
38
+ path: '/panel',
39
+ install: async ({ req, res }: LoginRouteParams): Promise<void> => {
40
+ const requestBody: unknown = req.body;
41
+
42
+ // DEBUG: Log the raw request body
43
+ console.log('[Panel Login] Raw request body:', JSON.stringify(requestBody));
44
+
45
+ if (!isLoginClientRequestBody(requestBody)) {
46
+ res.status(400).json({
47
+ message: 'Invalid request payload. "user" and "pass" are required and must be non-empty strings.',
48
+ status: 'error',
49
+ });
50
+ return;
51
+ }
52
+
53
+ const { user: clientUser, pass: clientPass } = requestBody;
54
+
55
+ // DEBUG: Log received client credentials (use quotes to see whitespace)
56
+ console.log(`[Panel Login] Received from client - User: "${clientUser}", Pass: "${clientPass}"`);
57
+
58
+ const relativeConfigPath = '../../config.json';
59
+ let configPath: string;
60
+
61
+ if (typeof __dirname !== 'undefined') {
62
+ configPath = path.resolve(__dirname, relativeConfigPath);
63
+ } else {
64
+ console.warn(
65
+ `[Panel Login] Warning: __dirname is not defined (standard in ES Modules). ` +
66
+ `Resolving config path "${relativeConfigPath}" relative to current working directory: ${process.cwd()}. ` +
67
+ `For robust file-relative paths in ES modules, the standard approach uses import.meta.url.`
68
+ );
69
+ configPath = path.resolve(process.cwd(), relativeConfigPath);
70
+ }
71
+
72
+ // DEBUG: Log the resolved config path
73
+ console.log(`[Panel Login] Attempting to load config from: "${configPath}"`);
74
+
75
+ try {
76
+ const configFileContent: string = await fsPromises.readFile(configPath, 'utf-8');
77
+ const config: ConfigData = JSON.parse(configFileContent);
78
+
79
+ // DEBUG: Log credentials loaded from config.json (use quotes to see whitespace)
80
+ if (config.user && config.pass) {
81
+ console.log(`[Panel Login] Loaded from config.json - User: "${config.user}", Pass: "${config.pass}"`);
82
+ } else {
83
+ console.log('[Panel Login] Loaded from config.json - "user" or "pass" field is missing or not a string.');
84
+ }
85
+
86
+
87
+ if (typeof config.user !== 'string' || typeof config.pass !== 'string') {
88
+ console.error(`[Panel Login] Invalid config.json structure at "${configPath}". "user" and "pass" must be strings.`);
89
+ res.status(500).json({
90
+ message: 'Server configuration error: Invalid config file structure.',
91
+ status: 'error',
92
+ });
93
+ return;
94
+ }
95
+
96
+ if (clientUser === config.user && clientPass === config.pass) {
97
+ console.log('[Panel Login] Credentials MATCHED!'); // DEBUG
98
+ res.status(200).json({
99
+ message: 'Login successful!',
100
+ status: 'success',
101
+ });
102
+ return;
103
+ } else {
104
+ console.log('[Panel Login] Credentials DID NOT MATCH.'); // DEBUG
105
+ console.log(`Comparison details: Client User ("${clientUser}") === Config User ("${config.user}") -> ${clientUser === config.user}`);
106
+ console.log(`Comparison details: Client Pass ("${clientPass}") === Config Pass ("${config.pass}") -> ${clientPass === config.pass}`);
107
+ res.status(401).json({
108
+ message: 'Invalid username or password.',
109
+ status: 'failed',
110
+ });
111
+ return;
112
+ }
113
+ } catch (error: unknown) {
114
+ console.error(`[Panel Login] Error during login process for config path "${configPath}":`, error instanceof Error ? error.message : String(error));
115
+ // ... (rest of your error handling) ...
116
+ if (error instanceof SyntaxError) {
117
+ res.status(500).json({
118
+ message: `Server configuration error: Malformed config file at "${configPath}". Please ensure it is valid JSON.`,
119
+ status: 'error',
120
+ });
121
+ return;
122
+ }
123
+
124
+ if (error instanceof Error && 'code' in error) {
125
+ const errnoError = error as NodeJS.ErrnoException;
126
+ if (errnoError.code === 'ENOENT') {
127
+ res.status(500).json({
128
+ message: `Server configuration error: Config file not found at "${configPath}". Please ensure the path is correct.`,
129
+ status: 'error',
130
+ });
131
+ return;
132
+ }
133
+ }
134
+
135
+ res.status(500).json({
136
+ message: 'An internal server error occurred during login.',
137
+ status: 'error',
138
+ });
139
+ return;
140
+ }
141
+ },
142
+ },
143
+ ];
exocore-web/src/project.ts ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import axios, { AxiosResponse } from 'axios';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import simpleGit, { SimpleGit } from 'simple-git';
7
+ import { Request, Response, Application } from 'express';
8
+ import { WebSocketServer } from 'ws';
9
+ import { Server as HttpServer } from 'http';
10
+
11
+ const TEMPLATE_URL = 'https://raw.githubusercontent.com/ChoruOfficial/global-exocore/main/template.json';
12
+ const configPath = path.resolve(__dirname, '../config.json');
13
+ const git: SimpleGit = simpleGit();
14
+
15
+ interface Template {
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ image: string;
20
+ git: string;
21
+ }
22
+
23
+ interface ConfigData {
24
+ project?: string;
25
+ [key: string]: any;
26
+ }
27
+
28
+ interface CreateProjectOptions {
29
+ templateId?: string;
30
+ gitUrl?: string;
31
+ }
32
+
33
+ async function pathExists(p: string): Promise<boolean> {
34
+ try {
35
+ await fs.promises.access(p);
36
+ return true;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ const api = {
43
+ async getTemplates(): Promise<Template[]> {
44
+ try {
45
+ const res: AxiosResponse<Template[]> = await axios.get(TEMPLATE_URL);
46
+ if (Array.isArray(res.data)) return res.data;
47
+ console.warn('Fetched template.json is not an array.');
48
+ return [];
49
+ } catch (err: unknown) {
50
+ console.error('Failed to fetch templates:', err);
51
+ return [];
52
+ }
53
+ },
54
+
55
+ async updateConfig(projectName: string): Promise<void> {
56
+ let config: ConfigData = {};
57
+ try {
58
+ if (await pathExists(configPath)) {
59
+ const file = await fs.promises.readFile(configPath, 'utf-8');
60
+ config = file.trim() ? JSON.parse(file) : {};
61
+ }
62
+ } catch (err) {
63
+ console.error('Error reading config:', err);
64
+ }
65
+
66
+ config.project = `../${projectName}`;
67
+ try {
68
+ await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2));
69
+ console.log(`Config updated: ${configPath}`);
70
+ } catch (err) {
71
+ console.error('Error writing config:', err);
72
+ }
73
+ },
74
+
75
+ async cloneTemplate(gitUrl: string, targetPath: string): Promise<void> {
76
+ try {
77
+ await git.clone(gitUrl, targetPath);
78
+ console.log('Cloned template successfully.');
79
+ } catch (err) {
80
+ console.error('Git clone failed:', err);
81
+ throw new Error('Git clone failed');
82
+ }
83
+ },
84
+
85
+ async checkProjectStatus(): Promise<{ exists: boolean }> {
86
+ if (!await pathExists(configPath)) return { exists: false };
87
+ try {
88
+ const raw = await fs.promises.readFile(configPath, 'utf-8');
89
+ const config: ConfigData = JSON.parse(raw);
90
+ if (!config.project) return { exists: false };
91
+
92
+ const folder = path.resolve(path.dirname(configPath), config.project);
93
+ return { exists: await pathExists(folder) };
94
+ } catch (err) {
95
+ console.error('Error checking project status:', err);
96
+ return { exists: false };
97
+ }
98
+ },
99
+
100
+ async createProject(projectName: string, { templateId, gitUrl }: CreateProjectOptions): Promise<string> {
101
+ if (!projectName.trim()) throw new Error('Project name is required.');
102
+
103
+ let cloneUrl: string;
104
+
105
+ if (gitUrl && templateId) throw new Error('Provide either a template ID or a Git URL, not both.');
106
+ if (gitUrl) {
107
+ cloneUrl = gitUrl;
108
+ } else if (templateId) {
109
+ const templates = await api.getTemplates();
110
+ const template = templates.find(t => t.id === templateId);
111
+ if (!template) throw new Error(`Template "${templateId}" not found.`);
112
+ cloneUrl = template.git;
113
+ } else {
114
+ throw new Error('You must provide a template ID or Git URL.');
115
+ }
116
+
117
+ const base = path.resolve(__dirname, '../..');
118
+ const target = path.join(base, projectName);
119
+
120
+ if (await pathExists(target)) {
121
+ throw new Error(`Project "${projectName}" already exists.`);
122
+ }
123
+
124
+ await api.cloneTemplate(cloneUrl, target);
125
+ await api.updateConfig(projectName);
126
+ return `Project "${projectName}" created at ${target}`;
127
+ },
128
+ };
129
+
130
+ interface ProjectRouteParams {
131
+ req: Request;
132
+ res: Response;
133
+ app?: Application;
134
+ wss?: WebSocketServer;
135
+ wssConsole?: WebSocketServer;
136
+ Shellwss?: WebSocketServer;
137
+ server?: HttpServer;
138
+ }
139
+
140
+ interface ProjectExpressRouteModule {
141
+ method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head' | 'all';
142
+ path: string;
143
+ install: (params: any) => Promise<void> | void;
144
+ }
145
+
146
+ interface CreateProjectRequestBody {
147
+ name: string;
148
+ template?: string;
149
+ gitUrl?: string;
150
+ }
151
+
152
+ export const modules: ProjectExpressRouteModule[] = [
153
+ {
154
+ method: 'post',
155
+ path: '/templates',
156
+ install: async ({ res }: Pick<ProjectRouteParams, 'res'>) => {
157
+ try {
158
+ const templates = await api.getTemplates();
159
+ res.json(templates);
160
+ } catch (err) {
161
+ console.error('Error in /templates:', err);
162
+ res.status(500).json({ error: 'Failed to load templates.' });
163
+ }
164
+ }
165
+ },
166
+ {
167
+ method: 'post',
168
+ path: '/project',
169
+ install: async ({ req, res }: Pick<ProjectRouteParams, 'req' | 'res'>) => {
170
+ const { name, template, gitUrl } = req.body as CreateProjectRequestBody;
171
+ try {
172
+ const msg = await api.createProject(name, { templateId: template, gitUrl });
173
+ res.status(201).json({ success: true, message: msg, projectPath: name });
174
+ } catch (err) {
175
+ console.error('Project creation failed:', err);
176
+ res.status(400).json({ success: false, error: (err as Error).message });
177
+ }
178
+ }
179
+ },
180
+ {
181
+ method: 'post',
182
+ path: '/project/status',
183
+ install: async ({ res }: Pick<ProjectRouteParams, 'res'>) => {
184
+ try {
185
+ const status = await api.checkProjectStatus();
186
+ res.json(status);
187
+ } catch (err) {
188
+ console.error('Error checking status:', err);
189
+ res.status(500).json({ error: 'Unable to check project status.' });
190
+ }
191
+ }
192
+ }
193
+ ];
exocore-web/src/register.ts ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import axios, { AxiosResponse, AxiosError } from 'axios';
4
+ import { Request, Response } from 'express';
5
+
6
+ console.log(`[Register Module] src/register.ts file is being evaluated. __dirname is: ${__dirname}`);
7
+
8
+ interface PastebinResponse {
9
+ link: string;
10
+ }
11
+
12
+ interface RegisterClientPayload {
13
+ user: string;
14
+ pass: string;
15
+ email: string;
16
+ nickname?: string;
17
+ avatar?: string;
18
+ bio?: string;
19
+ dob?: string;
20
+ cover_photo?: string;
21
+ country?: string;
22
+ timezone?: string;
23
+ }
24
+
25
+ type ExternalApiSignUpPayload = RegisterClientPayload;
26
+
27
+ interface RegisterApiResponseData {
28
+ status: string;
29
+ token?: string;
30
+ cookies?: string;
31
+ avatar?: string;
32
+ cover_photo?: string;
33
+ message?: string;
34
+ }
35
+
36
+ function isRegisterClientPayload(body: any): body is RegisterClientPayload {
37
+ return !!(body && typeof body === 'object' &&
38
+ typeof body.user === 'string' && body.user.length > 0 &&
39
+ typeof body.pass === 'string' && body.pass.length > 0 &&
40
+ typeof body.email === 'string' && body.email.length > 0);
41
+ }
42
+
43
+ function isRegisterApiResponseData(data: any): data is RegisterApiResponseData {
44
+ return !!(data && typeof data === 'object' && typeof data.status === 'string');
45
+ }
46
+
47
+ const api = {
48
+ async getLink(): Promise<string> {
49
+ console.log("[Register API - getLink] Attempting to fetch link...");
50
+ try {
51
+ const res: AxiosResponse<string | PastebinResponse> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
52
+ const responseData = res.data;
53
+ if (responseData && typeof responseData === 'object' && typeof (responseData as PastebinResponse).link === 'string') {
54
+ return (responseData as PastebinResponse).link;
55
+ } else if (typeof responseData === 'string' && responseData.startsWith('http')) {
56
+ return responseData;
57
+ }
58
+ throw new Error('Invalid or missing link in Pastebin response data.');
59
+ } catch (err: unknown) {
60
+ const error = err instanceof Error ? err : new Error(String(err));
61
+ throw new Error('Failed to get base link.', { cause: error });
62
+ }
63
+ },
64
+
65
+ async register(body: ExternalApiSignUpPayload): Promise<RegisterApiResponseData> {
66
+ console.log("[Register API - register] Attempting external API signup, payload:", body);
67
+ try {
68
+ const link: string = await api.getLink();
69
+ const res: AxiosResponse<RegisterApiResponseData> = await axios.post(`${link}/signup`, body);
70
+ console.log("[Register API - register] Response from external API:", res.data);
71
+ return res.data; // Axios automatically parses JSON response
72
+ } catch (err: unknown) {
73
+ const error = err instanceof Error ? err : new Error(String(err));
74
+ if (axios.isAxiosError(error) && error.response) {
75
+ console.error("Axios error data:", error.response.data, "Status:", error.response.status);
76
+ }
77
+ throw new Error('Failed to contact/process signup endpoint.', { cause: error });
78
+ }
79
+ },
80
+ };
81
+
82
+ interface RegisterRouteParams {
83
+ req: Request<any, any, any>; // Body is initially any, refined by type guard
84
+ res: Response;
85
+ }
86
+
87
+ interface RegisterExpressRouteModule {
88
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
89
+ path: string;
90
+ install: (params: RegisterRouteParams) => Promise<void> | void;
91
+ }
92
+
93
+ export const modules: RegisterExpressRouteModule[] = [
94
+ {
95
+ method: 'post',
96
+ path: '/register',
97
+ install: async ({ req, res }: RegisterRouteParams): Promise<void> => {
98
+ console.log(`[Register Route] Handler for ${req.method.toUpperCase()} ${req.path}`);
99
+ const rawClientBody: unknown = req.body;
100
+ console.log("[Register Route] Raw request body from client:", rawClientBody);
101
+
102
+ try {
103
+ if (isRegisterClientPayload(rawClientBody)) {
104
+ const clientData: RegisterClientPayload = rawClientBody; // Type guard refines type
105
+
106
+ const payloadForExternalApi: ExternalApiSignUpPayload = {
107
+ user: clientData.user,
108
+ pass: clientData.pass,
109
+ email: clientData.email,
110
+ };
111
+
112
+ if (clientData.nickname && clientData.nickname.trim() !== "") payloadForExternalApi.nickname = clientData.nickname;
113
+ if (clientData.avatar && clientData.avatar.trim() !== "") payloadForExternalApi.avatar = clientData.avatar;
114
+ if (clientData.bio && clientData.bio.trim() !== "") payloadForExternalApi.bio = clientData.bio;
115
+ if (clientData.dob && clientData.dob.trim() !== "") payloadForExternalApi.dob = clientData.dob;
116
+ if (clientData.cover_photo && clientData.cover_photo.trim() !== "") payloadForExternalApi.cover_photo = clientData.cover_photo;
117
+ if (clientData.country && clientData.country.trim() !== "") payloadForExternalApi.country = clientData.country;
118
+ if (clientData.timezone && clientData.timezone.trim() !== "") payloadForExternalApi.timezone = clientData.timezone;
119
+
120
+ const rawApiResult: RegisterApiResponseData = await api.register(payloadForExternalApi);
121
+
122
+ if (isRegisterApiResponseData(rawApiResult)) { // Guard for external API response structure
123
+ const resultFromExternalApi: RegisterApiResponseData = rawApiResult;
124
+
125
+ if (resultFromExternalApi.status === 'success') {
126
+ console.log("[Register Route] External API reported SUCCESS.");
127
+ res.json({
128
+ success: true,
129
+ message: resultFromExternalApi.message || "Registration successful!",
130
+ data: resultFromExternalApi
131
+ });
132
+ } else {
133
+ console.warn("[Register Route] External API reported FAILURE. Status:", resultFromExternalApi.status, "Message:", resultFromExternalApi.message);
134
+ res.status(400).json({
135
+ success: false,
136
+ error: resultFromExternalApi.message || "Registration failed at external API."
137
+ });
138
+ }
139
+ } else {
140
+ console.error("[Register Route] Unexpected response structure from external API:", rawApiResult);
141
+ res.status(500).json({ success: false, error: "Received an unexpected response from the registration service." });
142
+ }
143
+
144
+ } else {
145
+ console.warn("[Register Route] Validation failed: req.body does not conform to RegisterClientPayload. Received:", rawClientBody);
146
+ res.status(400).json({ success: false, error: 'Invalid registration data: user, pass, and email are required and must be valid non-empty strings.' });
147
+ return;
148
+ }
149
+ } catch (err: unknown) {
150
+ const error = err instanceof Error ? err : new Error(String(err));
151
+ let errorMessage = "An unknown error occurred";
152
+ let statusCode = 500;
153
+ console.error(`[Register Route] Overall error:`, error.message, (error as any).cause || error);
154
+
155
+ const originalError = (error as any).cause || error;
156
+
157
+ if (axios.isAxiosError(originalError)) {
158
+ const axiosError = originalError as AxiosError<any>; // Type assertion
159
+ errorMessage = axiosError.message;
160
+ statusCode = axiosError.response?.status || 503;
161
+ const responseData = axiosError.response?.data;
162
+ const specificApiError = responseData?.error || responseData?.message;
163
+
164
+ if (specificApiError) {
165
+ errorMessage = `Reg API Error (${statusCode}): ${specificApiError}`;
166
+ } else if (axiosError.response) {
167
+ errorMessage = `Reg API Error (${statusCode}): Req failed (status ${statusCode}).`;
168
+ } else if (axiosError.request) {
169
+ errorMessage = `No response from reg service: ${axiosError.message}`;
170
+ }
171
+ } else if (originalError instanceof Error) {
172
+ errorMessage = originalError.message || String(originalError);
173
+ } else {
174
+ errorMessage = String(originalError);
175
+ }
176
+
177
+ console.error(`[Register Route] Error to client: ${errorMessage}, Status: ${statusCode}`);
178
+ if (!res.headersSent) {
179
+ res.status(statusCode).json({ success: false, error: errorMessage });
180
+ } else {
181
+ console.warn("[Register Route] Headers sent, cannot send error.");
182
+ }
183
+ }
184
+ },
185
+ },
186
+ ];
exocore-web/src/shell.ts ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import { spawn, ChildProcess } from 'child_process';
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+ import simpleGit from 'simple-git';
7
+ import { Request, Response } from 'express';
8
+ import { WebSocketServer, WebSocket } from 'ws';
9
+
10
+ let shellProcess: ChildProcess | null = null;
11
+ let currentCwd: string | null = null;
12
+ let projectRootPath: string | null = null;
13
+
14
+ const PROMPT_DELIMITER = '__EXOCORE_SHELL_PROMPT_BOUNDARY__\n';
15
+
16
+ const FORBIDDEN_COMMAND_PATTERNS: RegExp[] = [
17
+ /^cd\s+\.\.(?:\/\s*)?$/,
18
+ /^cd\s+\.\.\/exocore-web\s*$/,
19
+ /^cd\s+\.\.\/src\s*$/
20
+ ];
21
+
22
+ interface WsMessage {
23
+ type: 'log' | 'prompt' | 'system';
24
+ data: string;
25
+ }
26
+
27
+ function isCommandForbidden(command: string): boolean {
28
+ const trimmedCommand = command.trim();
29
+ return FORBIDDEN_COMMAND_PATTERNS.some((pattern) => pattern.test(trimmedCommand));
30
+ }
31
+
32
+ function getFormattedCwdPrompt(cwd: string, root: string): string {
33
+ const rootName = path.basename(root);
34
+ if (cwd.startsWith(root)) {
35
+ const relativePath = path.relative(root, cwd);
36
+ return `/@${rootName}${relativePath ? `/${relativePath}` : ''}$ `;
37
+ }
38
+ return `${cwd}$ `;
39
+ }
40
+
41
+ function broadcastToShellWss(wssInstance: WebSocketServer, message: WsMessage): void {
42
+ if (wssInstance && wssInstance.clients) {
43
+ const messageString = JSON.stringify(message);
44
+ wssInstance.clients.forEach((client: WebSocket) => {
45
+ if (client.readyState === 1) {
46
+ client.send(messageString);
47
+ }
48
+ });
49
+ }
50
+ }
51
+
52
+ export const modules = [
53
+ {
54
+ method: 'post',
55
+ path: '/shell/sent',
56
+ install: async ({ req, res, Shellwss }: { req: Request, res: Response, Shellwss: WebSocketServer }): Promise<void> => {
57
+ const commandFromBody: string | null = req.body && typeof req.body.command === 'string' ? req.body.command : null;
58
+ if (!commandFromBody) {
59
+ res.status(400).send('No command provided.');
60
+ return;
61
+ }
62
+ const trimmedCommand = commandFromBody.trim();
63
+
64
+ if (isCommandForbidden(trimmedCommand)) {
65
+ broadcastToShellWss(Shellwss, { type: 'log', data: '\x1b[31m Access Denied \x1b[0m\n' });
66
+ res.status(403).send('Command execution is restricted.');
67
+ return;
68
+ }
69
+
70
+ if (trimmedCommand.startsWith('exocore git clone ')) {
71
+ const repoUrl: string = trimmedCommand.substring('exocore git clone '.length).trim();
72
+ if (!repoUrl) {
73
+ broadcastToShellWss(Shellwss, { type: 'log', data: '\x1b[31mError: No repository URL provided...\x1b[0m\n' });
74
+ res.status(400).send('No repository URL provided.');
75
+ return;
76
+ }
77
+ const baseDirForPkg = path.resolve(__dirname, '../../src/pkg');
78
+ try {
79
+ await fs.promises.mkdir(baseDirForPkg, { recursive: true });
80
+ const repoName = path.basename(repoUrl, '.git');
81
+ const clonePath = path.join(baseDirForPkg, repoName);
82
+ broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[33mCloning ${repoUrl}...\x1b[0m\n` });
83
+ if (fs.existsSync(clonePath) && fs.readdirSync(clonePath).length > 0) {
84
+ broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[33mDirectory ${clonePath} already exists. Skipping.\x1b[0m\n` });
85
+ res.status(200).send('Clone destination already exists.');
86
+ return;
87
+ }
88
+ const git = simpleGit();
89
+ await git.clone(repoUrl, clonePath);
90
+ broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[32mSuccessfully cloned ${repoUrl}\x1b[0m\n` });
91
+ res.send('Clone command executed successfully.');
92
+ } catch (error: any) {
93
+ const errMsg = error instanceof Error ? error.message : String(error);
94
+ broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[31mFailed to clone: ${errMsg}\x1b[0m\n` });
95
+ res.status(500).send('Clone command failed.');
96
+ }
97
+ return;
98
+ }
99
+
100
+ if (!shellProcess) {
101
+ let projectPath: string;
102
+ let customPkgPathForShellEnv: string | undefined;
103
+
104
+ try {
105
+ const configJsonPath = path.resolve(__dirname, '../config.json');
106
+ const configData = JSON.parse(fs.readFileSync(configJsonPath, 'utf-8'));
107
+ projectPath = path.resolve(path.dirname(configJsonPath), configData.project);
108
+ } catch (e) {
109
+ projectPath = path.resolve(__dirname, '..');
110
+ }
111
+
112
+ customPkgPathForShellEnv = path.resolve(__dirname, '../../src/pkg');
113
+ if (!fs.existsSync(customPkgPathForShellEnv) || !fs.statSync(customPkgPathForShellEnv).isDirectory()) {
114
+ customPkgPathForShellEnv = undefined;
115
+ }
116
+
117
+ projectRootPath = projectPath;
118
+ currentCwd = projectPath;
119
+
120
+ const currentEnv: NodeJS.ProcessEnv = { ...process.env };
121
+ let effectivePath: string | undefined = currentEnv.PATH;
122
+
123
+ if (customPkgPathForShellEnv) {
124
+ effectivePath = `${customPkgPathForShellEnv}:${effectivePath || ''}`;
125
+ }
126
+
127
+ const shellEnv: NodeJS.ProcessEnv = {
128
+ ...currentEnv,
129
+ FORCE_COLOR: '1',
130
+ NPM_CONFIG_COLOR: 'always',
131
+ TERM: 'xterm-256color',
132
+ LANG: 'en_US.UTF-8',
133
+ PATH: effectivePath,
134
+ };
135
+
136
+ shellProcess = spawn('bash', { cwd: projectPath, shell: true, env: shellEnv });
137
+
138
+ let stdoutBuffer = '';
139
+ const handleShellOutput = (data: Buffer | string) => {
140
+ stdoutBuffer += data.toString();
141
+ while (stdoutBuffer.includes(PROMPT_DELIMITER)) {
142
+ const boundaryIndex = stdoutBuffer.indexOf(PROMPT_DELIMITER);
143
+ const chunk = stdoutBuffer.substring(0, boundaryIndex);
144
+ stdoutBuffer = stdoutBuffer.substring(boundaryIndex + PROMPT_DELIMITER.length);
145
+
146
+ const lines = chunk.trim().split('\n');
147
+ const newCwd = lines.pop()?.trim();
148
+ const commandOutput = lines.join('\n');
149
+
150
+ if (commandOutput) {
151
+ broadcastToShellWss(Shellwss, { type: 'log', data: commandOutput + '\n' });
152
+ }
153
+ if (newCwd && projectRootPath && fs.existsSync(newCwd)) {
154
+ currentCwd = newCwd;
155
+ const newPrompt = getFormattedCwdPrompt(currentCwd, projectRootPath);
156
+ broadcastToShellWss(Shellwss, { type: 'prompt', data: newPrompt });
157
+ }
158
+ }
159
+ };
160
+
161
+ shellProcess.stdout?.on('data', handleShellOutput);
162
+ shellProcess.stderr?.on('data', (data) => broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[31m${data.toString()}\x1b[0m` }));
163
+ shellProcess.on('close', (code) => {
164
+ broadcastToShellWss(Shellwss, { type: 'system', data: `\x1b[33mShell exited (code: ${code}).\x1b[0m\n` });
165
+ shellProcess = null;
166
+ currentCwd = null;
167
+ projectRootPath = null;
168
+ });
169
+ shellProcess.on('error', (err) => {
170
+ broadcastToShellWss(Shellwss, { type: 'log', data: `\x1b[31mFailed to start shell: ${err.message}\x1b[0m\n` });
171
+ shellProcess = null;
172
+ });
173
+
174
+ await new Promise(resolve => setTimeout(resolve, 100));
175
+ }
176
+
177
+ if (shellProcess && shellProcess.stdin?.writable) {
178
+ shellProcess.stdin.write(trimmedCommand + '\n');
179
+ shellProcess.stdin.write(`pwd && echo "${PROMPT_DELIMITER.trim()}"\n`);
180
+ res.send('Command sent to shell.');
181
+ } else {
182
+ broadcastToShellWss(Shellwss, { type: 'log', data: '\x1b[31mCannot send command: Shell is not ready.\x1b[0m\n' });
183
+ res.status(503).send('Shell process not available.');
184
+ }
185
+ },
186
+ },
187
+ {
188
+ method: 'post',
189
+ path: '/shell/kill',
190
+ install: ({ res, Shellwss }: { res: Response, Shellwss: WebSocketServer }) => {
191
+ if (shellProcess) {
192
+ shellProcess.kill();
193
+ broadcastToShellWss(Shellwss, { type: 'system', data: '\x1b[33mKill signal sent.\x1b[0m\n' });
194
+ res.send('Kill signal sent.');
195
+ } else {
196
+ broadcastToShellWss(Shellwss, { type: 'system', data: '\x1b[33mNo active shell to kill.\x1b[0m\n' });
197
+ res.status(404).send('No active shell process.');
198
+ }
199
+ },
200
+ },
201
+ ];
202
+
203
+ export function setupShellWS(Shellwss: WebSocketServer): void {
204
+ Shellwss.on('connection', (ws: WebSocket) => {
205
+ ws.send(JSON.stringify({ type: 'system', data: '\x1b[32mWelcome to the interactive shell!\x1b[0m\n' }));
206
+ if (shellProcess && currentCwd && projectRootPath) {
207
+ const prompt = getFormattedCwdPrompt(currentCwd, projectRootPath);
208
+ ws.send(JSON.stringify({ type: 'prompt', data: prompt }));
209
+ } else {
210
+ ws.send(JSON.stringify({ type: 'system', data: '\x1b[33mShell not running. Send command to start.\x1b[0m\n' }));
211
+ }
212
+ ws.on('message', (message: Buffer | string) => {
213
+ ws.send(JSON.stringify({ type: 'system', data: '\x1b[33mPlease send commands through the terminal input, not directly via WebSocket message.\x1b[0m\n' }));
214
+ });
215
+ });
216
+ }
217
+
218
+ export function stopShellProcessOnExit(): void {
219
+ if (shellProcess) {
220
+ shellProcess.kill('SIGTERM');
221
+ }
222
+ }
exocore-web/src/skills.ts ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { Request, Response } from 'express';
6
+
7
+ interface SkillResult {
8
+ name: string;
9
+ skill: string;
10
+ extension: string;
11
+ }
12
+
13
+ interface Config {
14
+ project: string;
15
+ }
16
+
17
+ const languageMap: { [key: string]: string } = {
18
+ "js": "JavaScript",
19
+ "ts": "TypeScript",
20
+ "html": "HTML",
21
+ "css": "CSS",
22
+ "json": "JSON",
23
+ "md": "Markdown",
24
+ "py": "Python",
25
+ "java": "Java",
26
+ "c": "C",
27
+ "cpp": "C++",
28
+ "cs": "C#",
29
+ "go": "Go",
30
+ "rb": "Ruby",
31
+ "php": "PHP",
32
+ "swift": "Swift",
33
+ "kt": "Kotlin",
34
+ "sh": "Shell Script",
35
+ "xml": "XML",
36
+ "yaml": "YAML",
37
+ "yml": "YAML",
38
+ "sql": "SQL",
39
+ "jsx": "React (JavaScript)",
40
+ "tsx": "React (TypeScript)",
41
+ };
42
+
43
+ function walkDir(dirPath: string, counts: Map<string, number>): number {
44
+ let totalFiles = 0;
45
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
46
+ console.warn(`[Skills Analyzer] Directory not found or not a directory: ${dirPath}`);
47
+ return 0;
48
+ }
49
+
50
+ const files = fs.readdirSync(dirPath);
51
+ for (const file of files) {
52
+ const fullPath = path.join(dirPath, file);
53
+ try {
54
+ const stats = fs.statSync(fullPath);
55
+ if (stats.isDirectory()) {
56
+ totalFiles += walkDir(fullPath, counts);
57
+ } else if (stats.isFile()) {
58
+ let ext = path.extname(file);
59
+ if (ext) {
60
+ ext = ext.substring(1).toLowerCase();
61
+ counts.set(ext, (counts.get(ext) || 0) + 1);
62
+ totalFiles++;
63
+ }
64
+ }
65
+ } catch (error: any) {
66
+ console.error(`[Skills Analyzer] Error processing file ${fullPath}:`, error.message);
67
+ }
68
+ }
69
+ return totalFiles;
70
+ }
71
+
72
+ async function analyzeProjectSkills(): Promise<{ project: string; skills: SkillResult[] }> {
73
+ const configPath = path.resolve(__dirname, '..', 'config.json');
74
+ console.log(`[Skills Analyzer] Attempting to read config from: ${configPath}`);
75
+
76
+ let projectRoot: string;
77
+ try {
78
+ const configContent = fs.readFileSync(configPath, 'utf8');
79
+ const config: Config = JSON.parse(configContent);
80
+ if (!config.project) {
81
+ throw new Error("Missing 'project' key in config.json. Please ensure it's defined.");
82
+ }
83
+ projectRoot = path.resolve(path.dirname(configPath), config.project);
84
+ console.log(`[Skills Analyzer] Project path from config: ${projectRoot}`);
85
+ } catch (error: any) {
86
+ console.error(`[Skills Analyzer] Failed to read or parse config.json:`, error.message);
87
+ return { project: "Unknown Project (config error)", skills: [] };
88
+ }
89
+
90
+ const languageCounts = new Map<string, number>();
91
+ const totalFiles = walkDir(projectRoot, languageCounts);
92
+
93
+ if (totalFiles === 0) {
94
+ console.warn(`[Skills Analyzer] No files with recognized extensions found in project directory: ${projectRoot}`);
95
+ return { project: projectRoot, skills: [] };
96
+ }
97
+
98
+ const skills: SkillResult[] = [];
99
+ for (const [ext, count] of languageCounts.entries()) {
100
+ const percentage = ((count / totalFiles) * 100).toFixed(2);
101
+ const name = languageMap[ext] || ext.toUpperCase();
102
+ skills.push({
103
+ name: name.toLowerCase(),
104
+ skill: `${percentage}%`,
105
+ extension: ext,
106
+ });
107
+ }
108
+
109
+ skills.sort((a, b) => parseFloat(b.skill) - parseFloat(a.skill));
110
+
111
+ console.log(`[Skills Analyzer] Analysis complete for project: ${projectRoot}. Found ${totalFiles} files.`);
112
+ return { project: projectRoot, skills: skills };
113
+ }
114
+
115
+ interface LoginRouteParams {
116
+ req: Request<any, any, unknown>;
117
+ res: Response;
118
+ }
119
+
120
+ interface LoginExpressRouteModule {
121
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
122
+ path: string;
123
+ install: (params: LoginRouteParams) => Promise<void> | void;
124
+ }
125
+
126
+ export const modules: LoginExpressRouteModule[] = [
127
+ {
128
+ method: "post",
129
+ path: "/skills",
130
+ install: async ({ req, res }: { req: Request; res: Response }) => {
131
+ try {
132
+ const { project, skills } = await analyzeProjectSkills();
133
+ res.json([{ project: project, skills: skills }]);
134
+ } catch (error: any) {
135
+ console.error("[Skills Route] Error serving skills:", error.message);
136
+ res.status(500).json({ error: "Failed to analyze project skills. Check server logs." });
137
+ }
138
+ },
139
+ },
140
+ ];
exocore-web/src/userinfo.ts ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import axios, { AxiosResponse, AxiosError } from 'axios';
4
+ import { Request, Response, Application } from 'express';
5
+ import { WebSocketServer } from 'ws';
6
+ import { Server as HttpServer } from 'http';
7
+
8
+ interface PastebinResponse {
9
+ link: string;
10
+ }
11
+
12
+ interface UserInfoRequestBody {
13
+ token: string;
14
+ cookies: Record<string, any> | string | undefined;
15
+ }
16
+
17
+ interface UserInfoResponseData {
18
+ userId: string;
19
+ username: string;
20
+ roles: string[];
21
+ [key: string]: any;
22
+ }
23
+
24
+ const api = {
25
+ async getLink(): Promise<string> {
26
+ try {
27
+ const res: AxiosResponse<PastebinResponse | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
28
+ if (res.data && typeof res.data === 'object' && typeof (res.data as PastebinResponse).link === 'string') {
29
+ return (res.data as PastebinResponse).link;
30
+ } else if (typeof res.data === 'string' && res.data.startsWith('http')) {
31
+ return res.data;
32
+ }
33
+ throw new Error('Invalid or missing link in Pastebin response data.');
34
+ } catch (err: unknown) {
35
+ const error = err instanceof Error ? err : new Error(String(err));
36
+ throw new Error('Failed to get base link from Pastebin.', { cause: error });
37
+ }
38
+ },
39
+
40
+ async userinfo(body: UserInfoRequestBody): Promise<UserInfoResponseData> {
41
+ try {
42
+ const link: string = await api.getLink();
43
+ const res: AxiosResponse<UserInfoResponseData> = await axios.post(`${link}/userinfo`, body);
44
+ return res.data;
45
+ } catch (err: unknown) {
46
+ const error = err instanceof Error ? err : new Error(String(err));
47
+ throw new Error('Failed to contact userinfo endpoint.', { cause: error });
48
+ }
49
+ },
50
+ };
51
+
52
+ interface UserInfoRouteParams {
53
+ req: Request<any, any, UserInfoRequestBody>;
54
+ res: Response;
55
+ app?: Application;
56
+ wss?: WebSocketServer;
57
+ wssConsole?: WebSocketServer;
58
+ Shellwss?: WebSocketServer;
59
+ server?: HttpServer;
60
+ }
61
+
62
+ interface UserInfoExpressRouteModule {
63
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
64
+ path: string;
65
+ install: (params: UserInfoRouteParams) => Promise<void> | void;
66
+ }
67
+
68
+ export const modules: UserInfoExpressRouteModule[] = [
69
+ {
70
+ method: 'post',
71
+ path: '/userinfo',
72
+ install: async ({ req, res }: UserInfoRouteParams): Promise<void> => {
73
+ try {
74
+ const { token, cookies: cookiesRaw } = req.body;
75
+
76
+ if (typeof token !== 'string' || !token) {
77
+ res.status(400).json({ error: "Token is required and must be a string." });
78
+ return;
79
+ }
80
+ if (cookiesRaw === undefined || cookiesRaw === null) {
81
+ res.status(400).json({ error: 'Cookies are required.' });
82
+ return;
83
+ }
84
+
85
+ let safeCookies: Record<string, any>;
86
+ if (typeof cookiesRaw === 'string') {
87
+ try {
88
+ safeCookies = JSON.parse(cookiesRaw);
89
+ } catch (parseError: unknown) {
90
+ const errMsg = parseError instanceof Error ? parseError.message : String(parseError);
91
+ res.status(400).json({ error: `Invalid cookies JSON format: ${errMsg}` });
92
+ return;
93
+ }
94
+ } else if (typeof cookiesRaw === 'object' && cookiesRaw !== null) {
95
+ safeCookies = cookiesRaw;
96
+ } else {
97
+ res.status(400).json({ error: 'Cookies must be a JSON string or an object.' });
98
+ return;
99
+ }
100
+
101
+ const data: UserInfoResponseData = await api.userinfo({ token, cookies: safeCookies });
102
+ res.json({ data });
103
+ } catch (err: unknown) {
104
+ let errorMessage = "An unknown error occurred while fetching user info.";
105
+ let statusCode = 500;
106
+
107
+ const topLevelError = err instanceof Error ? err : new Error(String(err));
108
+ const originalError: unknown = topLevelError.cause || topLevelError;
109
+
110
+ if (axios.isAxiosError(originalError)) {
111
+ const axiosError = originalError as AxiosError<any>; // Type assertion
112
+ errorMessage = axiosError.message;
113
+ if (axiosError.response) {
114
+ statusCode = axiosError.response.status;
115
+ const responseDataError = (axiosError.response.data as any)?.error;
116
+ errorMessage = `API Error (${statusCode}): ${responseDataError || axiosError.message}`;
117
+ console.error(`Axios error response from /userinfo endpoint: Status ${statusCode}`, axiosError.response.data);
118
+ } else if (axiosError.request) {
119
+ errorMessage = "No response received from userinfo service.";
120
+ statusCode = 503;
121
+ console.error("Axios no response error for /userinfo:", axiosError.request);
122
+ }
123
+ } else if (originalError instanceof Error) {
124
+ errorMessage = originalError.message;
125
+ } else {
126
+ errorMessage = String(originalError);
127
+ }
128
+
129
+ console.error(`Error in /userinfo route: ${errorMessage}`, topLevelError);
130
+ if (!res.headersSent) {
131
+ res.status(statusCode).json({ error: errorMessage });
132
+ }
133
+ }
134
+ },
135
+ },
136
+ ];
exocore-web/src/userinfoEdit.ts ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // @ts-check
2
+
3
+ import axios, { AxiosResponse, AxiosError } from 'axios';
4
+ import { Request, Response, Application } from 'express';
5
+ import { WebSocketServer } from 'ws';
6
+ import { Server as HttpServer } from 'http';
7
+
8
+ interface PastebinResponse {
9
+ link: string;
10
+ }
11
+
12
+ interface UserInfoEditFunctionParams {
13
+ token: string;
14
+ cookies: Record<string, any> | string | undefined;
15
+ field: string;
16
+ edit: any;
17
+ }
18
+
19
+ interface UserInfoEditApiResponseData {
20
+ status: string;
21
+ updatedFields: Record<string, any>;
22
+ [key: string]: any; // Allow for other potential fields
23
+ }
24
+
25
+ const api = {
26
+ async getLink(): Promise<string> {
27
+ try {
28
+ const res: AxiosResponse<PastebinResponse | string> = await axios.get('https://pastebin.com/raw/YtqNc7Yi');
29
+ if (res.data && typeof res.data === 'object' && typeof (res.data as PastebinResponse).link === 'string') {
30
+ return (res.data as PastebinResponse).link;
31
+ } else if (typeof res.data === 'string' && res.data.startsWith('http')) {
32
+ return res.data;
33
+ }
34
+ throw new Error('Invalid or missing link in Pastebin response data.');
35
+ } catch (err: unknown) {
36
+ const error = err instanceof Error ? err : new Error(String(err));
37
+ throw new Error('Failed to get base link from Pastebin.', { cause: error });
38
+ }
39
+ },
40
+
41
+ async userinfoEdit({ token, cookies, field, edit }: UserInfoEditFunctionParams): Promise<UserInfoEditApiResponseData> {
42
+ try {
43
+ const link: string = await api.getLink();
44
+ const res: AxiosResponse<UserInfoEditApiResponseData> = await axios.post(`${link}/userinfoEdit`, { token, cookies, field, edit });
45
+ return res.data;
46
+ } catch (err: unknown) {
47
+ const error = err instanceof Error ? err : new Error(String(err));
48
+ throw new Error('Failed to contact userinfoEdit endpoint.', { cause: error });
49
+ }
50
+ },
51
+ };
52
+
53
+ interface UserInfoEditRouteHandlerParams {
54
+ req: Request<any, any, UserInfoEditFunctionParams>;
55
+ res: Response;
56
+ app?: Application;
57
+ wss?: WebSocketServer;
58
+ wssConsole?: WebSocketServer;
59
+ Shellwss?: WebSocketServer;
60
+ server?: HttpServer;
61
+ }
62
+
63
+ interface UserInfoEditExpressRouteModule {
64
+ method: "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "all";
65
+ path: string;
66
+ install: (params: UserInfoEditRouteHandlerParams) => Promise<void> | void;
67
+ }
68
+
69
+ export const modules: UserInfoEditExpressRouteModule[] = [
70
+ {
71
+ method: 'post',
72
+ path: '/userinfoEdit',
73
+ install: async ({ req, res }: UserInfoEditRouteHandlerParams): Promise<void> => {
74
+ try {
75
+ const { token, cookies, field, edit } = req.body;
76
+
77
+ if (typeof token !== 'string' || !token) {
78
+ res.status(400).json({ error: 'Token is required and must be a string.' });
79
+ return;
80
+ }
81
+ if (typeof field !== 'string' || !field) {
82
+ res.status(400).json({ error: 'Field to edit is required and must be a string.' });
83
+ return;
84
+ }
85
+ if (edit === undefined) {
86
+ res.status(400).json({ error: 'New value for edit is required.' });
87
+ return;
88
+ }
89
+ if (cookies === undefined || cookies === null) {
90
+ res.status(400).json({ error: 'Cookies are required.' });
91
+ return;
92
+ }
93
+
94
+ let safeCookies: Record<string, any>;
95
+ if (typeof cookies === 'string') {
96
+ try {
97
+ safeCookies = JSON.parse(cookies);
98
+ } catch (parseError: unknown) {
99
+ const errMsg = parseError instanceof Error ? parseError.message : String(parseError);
100
+ res.status(400).json({ error: `Invalid cookies JSON format: ${errMsg}` });
101
+ return;
102
+ }
103
+ } else if (typeof cookies === 'object' && cookies !== null) {
104
+ safeCookies = cookies;
105
+ } else {
106
+ res.status(400).json({ error: 'Cookies must be a JSON string or an object.' });
107
+ return;
108
+ }
109
+
110
+ const data: UserInfoEditApiResponseData = await api.userinfoEdit({ token, cookies: safeCookies, field, edit });
111
+ res.json({ data });
112
+ } catch (err: unknown) {
113
+ let errorMessage = "An unknown error occurred while editing user info.";
114
+ let statusCode = 500;
115
+
116
+ const topLevelError = err instanceof Error ? err : new Error(String(err));
117
+ const originalError: unknown = topLevelError.cause || topLevelError;
118
+
119
+ if (axios.isAxiosError(originalError)) {
120
+ const axiosError = originalError as AxiosError<any>; // Type assertion
121
+ errorMessage = axiosError.message;
122
+ if (axiosError.response) {
123
+ statusCode = axiosError.response.status;
124
+ const responseDataError = (axiosError.response.data as any)?.error;
125
+ errorMessage = `API Error (${statusCode}): ${responseDataError || axiosError.message}`;
126
+ console.error(`Axios error response from /userinfoEdit endpoint: Status ${statusCode}`, axiosError.response.data);
127
+ } else if (axiosError.request) {
128
+ errorMessage = "No response received from userinfoEdit service.";
129
+ statusCode = 503;
130
+ console.error("Axios no response error for /userinfoEdit:", axiosError.request);
131
+ }
132
+ } else if (originalError instanceof Error) {
133
+ errorMessage = originalError.message;
134
+ } else if (typeof originalError === 'string') {
135
+ errorMessage = originalError;
136
+ } else {
137
+ errorMessage = String(originalError);
138
+ }
139
+
140
+ console.error(`Error in /userinfoEdit route: ${errorMessage}`, topLevelError);
141
+ if (!res.headersSent) {
142
+ res.status(statusCode).json({ error: errorMessage });
143
+ }
144
+ }
145
+ },
146
+ },
147
+ ];