diff --git a/src/application/console/commandRegistry.ts b/src/application/console/commandRegistry.ts new file mode 100644 index 0000000..54e3f04 --- /dev/null +++ b/src/application/console/commandRegistry.ts @@ -0,0 +1,59 @@ +import * as fs from 'fs' +import * as path from 'path' +import Logger, { LoggerType } from '#application/logger' +import { getAppPath } from '#application/storage' +import { Command } from '#application/types' + +export class CommandRegistry { + private readonly commands: Map = new Map() + private readonly logger = Logger.type(LoggerType.COMMAND) + + public getCommand(name: string): Command | undefined { + return this.commands.get(name) + } + + public async loadCommands(): Promise { + const directory = getAppPath('commands') + this.logger.info(`Loading commands from: ${directory}`) + + try { + const files = await fs.promises.readdir(directory, { withFileTypes: true }) + await Promise.all( + files + .filter(file => this.isValidCommandFile(file)) + .map(file => this.loadCommandFile(file)) + ) + } catch (error) { + this.logger.error(`Failed to read commands directory: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private isValidCommandFile(file: fs.Dirent): boolean { + return file.isFile() && (file.name.endsWith('.ts') || file.name.endsWith('.js')) + } + + private async loadCommandFile(file: fs.Dirent): Promise { + const fullPath = getAppPath('commands', file.name) + const commandName = path.basename(file.name, path.extname(file.name)) + + try { + const module = await import(fullPath) + if (typeof module.default !== 'function') { + this.logger.warn(`Unrecognized export in ${file.name}`) + return + } + + this.registerCommand(commandName, module.default) + } catch (error) { + this.logger.error(`Error loading command ${file.name}: ${error instanceof Error ? error.message : String(error)}`) + } + } + + private registerCommand(name: string, CommandClass: Command): void { + if (this.commands.has(name)) { + this.logger.warn(`Command '${name}' is already registered. Overwriting...`) + } + this.commands.set(name, CommandClass) + this.logger.info(`Registered command: ${name}`) + } +} \ No newline at end of file diff --git a/src/application/console/consolePrompt.ts b/src/application/console/consolePrompt.ts new file mode 100644 index 0000000..129b20c --- /dev/null +++ b/src/application/console/consolePrompt.ts @@ -0,0 +1,33 @@ +import * as readline from 'readline' + +export class ConsolePrompt { + private readonly rl: readline.Interface + private isClosed: boolean = false + + constructor(private readonly commandHandler: (command: string) => void) { + this.rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + + this.rl.on('close', () => { + this.isClosed = true + }) + } + + public start(): void { + if (this.isClosed) return + this.promptCommand() + } + + public close(): void { + this.rl.close() + } + + private promptCommand(): void { + this.rl.question('> ', (command: string) => { + this.commandHandler(command) + this.promptCommand() + }) + } +} \ No newline at end of file diff --git a/src/application/console/logReader.ts b/src/application/console/logReader.ts new file mode 100644 index 0000000..e48b23b --- /dev/null +++ b/src/application/console/logReader.ts @@ -0,0 +1,76 @@ +import * as fs from 'fs' +import * as path from 'path' +import Logger, { LoggerType } from '#application/logger' + +export class LogReader { + private logger = Logger.type(LoggerType.CONSOLE) + private watchers: fs.FSWatcher[] = [] + private readonly logsDirectory: string + + constructor(rootPath: string) { + this.logsDirectory = path.join(rootPath, 'logs') + } + + public start(): void { + this.logger.info('Starting log reader...') + this.watchLogs() + } + + public stop(): void { + this.watchers.forEach(watcher => watcher.close()) + this.watchers = [] + } + + private watchLogs(): void { + // Watch directory for new files + const directoryWatcher = fs.watch(this.logsDirectory, (_, filename) => { + if (filename?.endsWith('.log')) { + this.watchLogFile(filename) + } + }) + this.watchers.push(directoryWatcher) + + // Watch existing files + try { + fs.readdirSync(this.logsDirectory) + .filter(file => file.endsWith('.log')) + .forEach(file => this.watchLogFile(file)) + } catch (error) { + this.logger.error(`Error reading logs directory: ${error}`) + } + } + + private watchLogFile(filename: string): void { + const filePath = path.join(this.logsDirectory, filename) + let currentPosition = fs.existsSync(filePath) ? fs.statSync(filePath).size : 0 + + const watcher = fs.watch(filePath, () => { + try { + const stat = fs.statSync(filePath) + const newPosition = stat.size + + if (newPosition < currentPosition) { + currentPosition = 0 + } + + if (newPosition > currentPosition) { + const stream = fs.createReadStream(filePath, { + start: currentPosition, + end: newPosition + }) + + stream.on('data', (data) => { + process.stdout.write('\r' + `[${filename}]\n${data}`) + process.stdout.write('\n> ') + }) + + currentPosition = newPosition + } + } catch { + watcher.close() + } + }) + + this.watchers.push(watcher) + } +} \ No newline at end of file diff --git a/src/application/logger.ts b/src/application/logger.ts index 2202d5c..e7817b5 100644 --- a/src/application/logger.ts +++ b/src/application/logger.ts @@ -8,7 +8,8 @@ export enum LoggerType { QUEUE = 'queue', COMMAND = 'command', REPOSITORY = 'repository', - ENTITY = 'entity' + ENTITY = 'entity', + CONSOLE = 'console' } class Logger { diff --git a/src/application/types.ts b/src/application/types.ts index 2ebd239..3c9e6c7 100644 --- a/src/application/types.ts +++ b/src/application/types.ts @@ -1,4 +1,4 @@ -import { Socket } from 'socket.io' +import { Server, Socket } from 'socket.io' import { Character } from '#entities/character' import { ZoneEventTile } from '#entities/zoneEventTile' @@ -51,6 +51,12 @@ export type WorldSettings = { fogDensity: number } +export interface Command { + new (io: Server): { + execute(args: string[]): Promise + } +} + // export type TCharacter = Socket & { // user?: User // character?: Character diff --git a/src/managers/commandManager.ts b/src/managers/commandManager.ts deleted file mode 100644 index 4119935..0000000 --- a/src/managers/commandManager.ts +++ /dev/null @@ -1,107 +0,0 @@ -import * as fs from 'fs' -import * as path from 'path' -import * as readline from 'readline' - -import { Server } from 'socket.io' - -import Logger, { LoggerType } from '#application/logger' -import { getAppPath } from '#application/storage' - -class CommandManager { - private logger = Logger.type(LoggerType.COMMAND) - private commands: Map = new Map() - private rl: readline.Interface - private io: Server | null = null - private rlClosed: boolean = false - - constructor() { - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }) - - this.rl.on('close', () => { - this.rlClosed = true - }) - } - - public async boot(io: Server) { - this.io = io - await this.loadCommands() - this.logger.info('Command manager loaded') - this.startPrompt() - } - - private startPrompt() { - if (this.rlClosed) return - - this.rl.question('> ', (command: string) => { - this.processCommand(command) - this.startPrompt() - }) - } - - private async processCommand(command: string): Promise { - const [cmd, ...args] = command.trim().split(' ') - if (this.commands.has(cmd)) { - const CommandClass = this.commands.get(cmd) - const commandInstance = new CommandClass(this.io as Server) - await commandInstance.execute(args) - } else { - this.handleUnknownCommand(cmd) - } - } - - private handleUnknownCommand(command: string) { - switch (command) { - case 'exit': - this.rl.close() - break - default: - console.error(`Unknown command: ${command}`) - break - } - } - - private async loadCommands() { - const directory = getAppPath('commands') - this.logger.info(`Loading commands from: ${directory}`) - - try { - const files = await fs.promises.readdir(directory, { withFileTypes: true }) - - for (const file of files) { - if (!file.isFile() || (!file.name.endsWith('.ts') && !file.name.endsWith('.js'))) { - continue - } - - const fullPath = getAppPath('commands', file.name) - const commandName = path.basename(file.name, path.extname(file.name)) - - try { - const module = await import(fullPath) - if (typeof module.default !== 'function') { - this.logger.warn(`Unrecognized export in ${file.name}`) - continue - } - - this.registerCommand(commandName, module.default) - } catch (error) { - this.logger.error(`Error loading command ${file.name}: ${error instanceof Error ? error.message : String(error)}`) - } - } - } catch (error) { - this.logger.error(`Failed to read commands directory: ${error instanceof Error ? error.message : String(error)}`) - } - } - - private registerCommand(name: string, CommandClass: any) { - if (this.commands.has(name)) { - this.logger.warn(`Command '${name}' is already registered. Overwriting...`) - } - this.commands.set(name, CommandClass) - this.logger.info(`Registered command: ${name}`) - } -} - -export default new CommandManager() diff --git a/src/managers/consoleManager.ts b/src/managers/consoleManager.ts new file mode 100644 index 0000000..4d908e5 --- /dev/null +++ b/src/managers/consoleManager.ts @@ -0,0 +1,56 @@ +import { Server } from 'socket.io' +import { CommandRegistry } from '#application/console/commandRegistry' +import { ConsolePrompt } from '#application/console/consolePrompt' +import { LogReader } from '#application/console/logReader' +import Logger, { LoggerType } from '#application/logger' + +export class ConsoleManager { + private readonly logger = Logger.type(LoggerType.COMMAND) + private readonly registry: CommandRegistry + private readonly prompt: ConsolePrompt + private readonly logReader: LogReader + private io: Server | null = null + + constructor() { + this.registry = new CommandRegistry() + this.prompt = new ConsolePrompt( + (command: string) => this.processCommand(command) + ) + + this.logReader = new LogReader(process.cwd()) + } + + public async boot(io: Server): Promise { + this.io = io + + await this.registry.loadCommands() + this.logReader.start() + this.prompt.start() + + this.logger.info('Console manager loaded') + } + + private async processCommand(commandLine: string): Promise { + const [cmd, ...args] = commandLine.trim().split(' ') + + if (cmd === 'exit') { + this.prompt.close() + return + } + + const CommandClass = this.registry.getCommand(cmd) + if (!CommandClass) { + console.error(`Unknown command: ${cmd}`) + return + } + + try { + const commandInstance = new CommandClass(this.io as Server) + await commandInstance.execute(args) + } catch (error) { + this.logger.error(`Error executing command ${cmd}: ${error instanceof Error ? error.message : String(error)}`) + } + } +} + +export default new ConsoleManager() \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 52fe6a0..f713b80 100644 --- a/src/server.ts +++ b/src/server.ts @@ -11,7 +11,7 @@ import Logger, { LoggerType } from '#application/logger' import { getAppPath } from '#application/storage' import { TSocket } from '#application/types' import { HttpRouter } from '#http/router' -import CommandManager from '#managers/commandManager' +import ConsoleManager from '#managers/consoleManager' import DateManager from '#managers/dateManager' import QueueManager from '#managers/queueManager' import UserManager from '#managers/userManager' @@ -49,8 +49,6 @@ export class Server { * Start the server */ public async start() { - // Read log file and print to console for debugging - // Connect to database try { await Database.initialize() @@ -86,8 +84,8 @@ export class Server { // Load zoneEditor manager await ZoneManager.boot() - // Load command manager - await CommandManager.boot(this.io) + // Load console manager + await ConsoleManager.boot(this.io) // Listen for socket connections this.io.on('connection', this.handleConnection.bind(this))