WIP: Implement a plugin system to allow users to execute private code #2357
							
								
								
									
										22
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.eslintignore
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
# Build output
 | 
			
		||||
build/
 | 
			
		||||
 | 
			
		||||
# Node modules
 | 
			
		||||
node_modules/
 | 
			
		||||
 | 
			
		||||
# Logs
 | 
			
		||||
logs/
 | 
			
		||||
 | 
			
		||||
# Environment files
 | 
			
		||||
.env
 | 
			
		||||
.env.local
 | 
			
		||||
.env.*.local
 | 
			
		||||
 | 
			
		||||
# OS generated files
 | 
			
		||||
.DS_Store
 | 
			
		||||
.DS_Store?
 | 
			
		||||
._*
 | 
			
		||||
.Spotlight-V100
 | 
			
		||||
.Trashes
 | 
			
		||||
ehthumbs.db
 | 
			
		||||
Thumbs.db
 | 
			
		||||
							
								
								
									
										12
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								package.json
									
									
									
									
									
								
							@ -5,10 +5,10 @@
 | 
			
		||||
  "main": "index.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
 | 
			
		||||
    "build": "tsgo --sourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:tsc": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
 | 
			
		||||
    "build:dev": "tsgo --sourceMap",
 | 
			
		||||
    "build:dev:tsc": "tsc --incremental --sourceMap",
 | 
			
		||||
    "build": "node scripts/plugin-discovery.js && tsgo --sourceMap && ncp static/webui build/static/webui && ncp plugins build/plugins",
 | 
			
		||||
    "build:tsc": "node scripts/plugin-discovery.js && tsc --incremental --sourceMap && ncp static/webui build/static/webui && ncp plugins build/plugins",
 | 
			
		||||
    "build:dev": "node scripts/plugin-discovery.js && tsgo --sourceMap",
 | 
			
		||||
    "build:dev:tsc": "node scripts/plugin-discovery.js && tsc --incremental --sourceMap",
 | 
			
		||||
    "build-and-start": "npm run build && npm run start",
 | 
			
		||||
    "build-and-start:bun": "npm run verify && npm run bun-run",
 | 
			
		||||
    "dev": "node scripts/dev.js",
 | 
			
		||||
@ -19,7 +19,9 @@
 | 
			
		||||
    "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
 | 
			
		||||
    "lint:fix": "eslint --fix --ext .ts .",
 | 
			
		||||
    "prettier": "prettier --write .",
 | 
			
		||||
    "update-translations": "cd scripts && node update-translations.js"
 | 
			
		||||
    "update-translations": "cd scripts && node update-translations.js",
 | 
			
		||||
    "new-plugin-ts": "node scripts/new-plugin-ts.js",
 | 
			
		||||
    "new-plugin-js": "node scripts/new-plugin-js.js"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "GNU",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								plugins/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								plugins/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,5 @@
 | 
			
		||||
*
 | 
			
		||||
 | 
			
		||||
!.gitignore
 | 
			
		||||
!ExamplePlugin/
 | 
			
		||||
!ExamplePlugin/**
 | 
			
		||||
							
								
								
									
										32
									
								
								plugins/ExamplePlugin/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								plugins/ExamplePlugin/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
import { IPlugin } from "@/src/types/pluginTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
 | 
			
		||||
export default class ExamplePlugin implements IPlugin {
 | 
			
		||||
    public name = "ExamplePlugin";
 | 
			
		||||
    public version = "1.0.0";
 | 
			
		||||
    public description = "Example plugin for the server";
 | 
			
		||||
    public author = "Your Name";
 | 
			
		||||
 | 
			
		||||
    async initialize(): Promise<void> {
 | 
			
		||||
        logger.info(`[${this.name}] Plugin initialized successfully!`);
 | 
			
		||||
 | 
			
		||||
        // Add your plugin initialization logic here
 | 
			
		||||
        // For example:
 | 
			
		||||
        // - Register new routes
 | 
			
		||||
        // - Add new API endpoints
 | 
			
		||||
        // - Set up event listeners
 | 
			
		||||
        // - Connect to external services
 | 
			
		||||
        await Promise.resolve(); // Simulate async operation if needed
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async cleanup(): Promise<void> {
 | 
			
		||||
        logger.info(`[${this.name}] Plugin cleanup completed`);
 | 
			
		||||
 | 
			
		||||
        // Add your cleanup logic here
 | 
			
		||||
        // For example:
 | 
			
		||||
        // - Close database connections
 | 
			
		||||
        // - Clear timers/intervals
 | 
			
		||||
        // - Remove event listeners
 | 
			
		||||
        await Promise.resolve(); // Simulate async operation if needed
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								plugins/ExamplePlugin/plugin.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								plugins/ExamplePlugin/plugin.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ExamplePlugin",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "Example plugin for the server",
 | 
			
		||||
  "author": "Your Name",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "dependencies": [],
 | 
			
		||||
  "config": {
 | 
			
		||||
    "enabled": false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										238
									
								
								scripts/create-plugin-core.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								scripts/create-plugin-core.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,238 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin Generator Script
 | 
			
		||||
 * Creates new plugin templates for both TypeScript and JavaScript
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const fs = require("fs");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
 | 
			
		||||
class PluginGenerator {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.pluginsDir = path.resolve("plugins");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create a new plugin
 | 
			
		||||
     */
 | 
			
		||||
    createPlugin(name, language = "ts") {
 | 
			
		||||
        if (!name) {
 | 
			
		||||
            console.error("❌ Plugin name is required!");
 | 
			
		||||
            console.log("Usage: node create-plugin.js <plugin-name> [ts|js]");
 | 
			
		||||
            process.exit(1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!["ts", "js"].includes(language)) {
 | 
			
		||||
            console.error("❌ Language must be 'ts' or 'js'");
 | 
			
		||||
            process.exit(1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const pluginDir = path.join(this.pluginsDir, name);
 | 
			
		||||
 | 
			
		||||
        // Check if plugin already exists
 | 
			
		||||
        if (fs.existsSync(pluginDir)) {
 | 
			
		||||
            console.error(`❌ Plugin '${name}' already exists!`);
 | 
			
		||||
            process.exit(1);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log(`🚀 Creating ${language.toUpperCase()} plugin: ${name}`);
 | 
			
		||||
 | 
			
		||||
        // Create plugin directory
 | 
			
		||||
        this.createDirectory(pluginDir);
 | 
			
		||||
 | 
			
		||||
        // Create plugin files
 | 
			
		||||
        this.createPluginManifest(pluginDir, name);
 | 
			
		||||
 | 
			
		||||
        if (language === "ts") {
 | 
			
		||||
            this.createTypeScriptPlugin(pluginDir, name);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.createJavaScriptPlugin(pluginDir, name);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log(`✅ Plugin '${name}' created successfully!`);
 | 
			
		||||
        console.log(`📁 Location: ${pluginDir}`);
 | 
			
		||||
        console.log(`🔧 Next steps:`);
 | 
			
		||||
        console.log(`   1. Edit ${path.join(pluginDir, `index.${language}`)} to implement your plugin logic`);
 | 
			
		||||
        console.log(`   2. Update ${path.join(pluginDir, "plugin.json")} if needed`);
 | 
			
		||||
        console.log(`   3. Run 'npm run build' to build the plugin`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create directory if it doesn't exist
 | 
			
		||||
     */
 | 
			
		||||
    createDirectory(dirPath) {
 | 
			
		||||
        if (!fs.existsSync(dirPath)) {
 | 
			
		||||
            fs.mkdirSync(dirPath, { recursive: true });
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create plugin manifest file
 | 
			
		||||
     */
 | 
			
		||||
    createPluginManifest(pluginDir, name) {
 | 
			
		||||
        const manifest = {
 | 
			
		||||
            name: name,
 | 
			
		||||
            version: "1.0.0",
 | 
			
		||||
            description: `${name} plugin for Warframe Emulator`,
 | 
			
		||||
            main: "index.js",
 | 
			
		||||
            author: "Your Name",
 | 
			
		||||
            license: "GNU",
 | 
			
		||||
            config: {
 | 
			
		||||
                enabled: true
 | 
			
		||||
            },
 | 
			
		||||
            dependencies: {},
 | 
			
		||||
            tags: ["custom"]
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const manifestPath = path.join(pluginDir, "plugin.json");
 | 
			
		||||
        fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
 | 
			
		||||
        console.log(`📝 Created: plugin.json`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create TypeScript plugin template
 | 
			
		||||
     */
 | 
			
		||||
    createTypeScriptPlugin(pluginDir, name) {
 | 
			
		||||
        const template = `import { IPlugin } from "@/src/types/pluginTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
 | 
			
		||||
export default class ${name} implements IPlugin {
 | 
			
		||||
    public name = "${name}";
 | 
			
		||||
    public version = "1.0.0";
 | 
			
		||||
    public description = "${name} plugin for Warframe Emulator";
 | 
			
		||||
 | 
			
		||||
    async initialize(): Promise<void> {
 | 
			
		||||
        logger.info(\`[\${this.name}] Plugin initialized successfully!\`);
 | 
			
		||||
 | 
			
		||||
        // Add your plugin initialization logic here
 | 
			
		||||
        // For example:
 | 
			
		||||
        // - Register new routes
 | 
			
		||||
        // - Add new API endpoints
 | 
			
		||||
        // - Set up event listeners
 | 
			
		||||
        // - Connect to external services
 | 
			
		||||
 | 
			
		||||
        await Promise.resolve(); // Remove this line and add your actual logic
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async cleanup(): Promise<void> {
 | 
			
		||||
        logger.info(\`[\${this.name}] Plugin cleanup completed\`);
 | 
			
		||||
 | 
			
		||||
        // Add your cleanup logic here
 | 
			
		||||
        // For example:
 | 
			
		||||
        // - Close database connections
 | 
			
		||||
        // - Clear timers/intervals
 | 
			
		||||
        // - Remove event listeners
 | 
			
		||||
        // - Cleanup resources
 | 
			
		||||
 | 
			
		||||
        await Promise.resolve(); // Remove this line and add your actual logic
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add your custom methods here
 | 
			
		||||
    // Example:
 | 
			
		||||
    // public async customMethod(): Promise<void> {
 | 
			
		||||
    //     logger.info(\`[\${this.name}] Custom method called\`);
 | 
			
		||||
    // }
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const pluginPath = path.join(pluginDir, "index.ts");
 | 
			
		||||
        fs.writeFileSync(pluginPath, template);
 | 
			
		||||
        console.log(`📝 Created: index.ts`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Create JavaScript plugin template
 | 
			
		||||
     */
 | 
			
		||||
    createJavaScriptPlugin(pluginDir, name) {
 | 
			
		||||
        const template = `/**
 | 
			
		||||
 * ${name} Plugin
 | 
			
		||||
 * ${name} plugin for Warframe Emulator
 | 
			
		||||
 */
 | 
			
		||||
import {logger} from "../../src/utils/logger.js";
 | 
			
		||||
 | 
			
		||||
class ${name} {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.name = "${name}";
 | 
			
		||||
        this.version = "1.0.0";
 | 
			
		||||
        this.description = "${name} plugin for Warframe Emulator";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async initialize() {
 | 
			
		||||
        logger.info(\`[\${this.name}] Plugin initialized successfully!\`);
 | 
			
		||||
 | 
			
		||||
        // Add your plugin initialization logic here
 | 
			
		||||
        // For example:
 | 
			
		||||
        // - Register new routes
 | 
			
		||||
        // - Add new API endpoints
 | 
			
		||||
        // - Set up event listeners
 | 
			
		||||
        // - Connect to external services
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(); // Remove this line and add your actual logic
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async cleanup() {
 | 
			
		||||
        logger.info(\`[\${this.name}] Plugin cleanup completed\`);
 | 
			
		||||
 | 
			
		||||
        // Add your cleanup logic here
 | 
			
		||||
        // For example:
 | 
			
		||||
        // - Close database connections
 | 
			
		||||
        // - Clear timers/intervals
 | 
			
		||||
        // - Remove event listeners
 | 
			
		||||
        // - Cleanup resources
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(); // Remove this line and add your actual logic
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add your custom methods here
 | 
			
		||||
    // Example:
 | 
			
		||||
    // async customMethod() {
 | 
			
		||||
    //     console.log(\`[\${this.name}] Custom method called\`);
 | 
			
		||||
    // }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ${name};
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
        const pluginPath = path.join(pluginDir, "index.js");
 | 
			
		||||
        fs.writeFileSync(pluginPath, template);
 | 
			
		||||
        console.log(`📝 Created: index.js`);
 | 
			
		||||
 | 
			
		||||
        // Create package.json for ES module support
 | 
			
		||||
        const packageJson = {
 | 
			
		||||
            type: "module"
 | 
			
		||||
        };
 | 
			
		||||
        const packagePath = path.join(pluginDir, "package.json");
 | 
			
		||||
        fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
 | 
			
		||||
        console.log(`📝 Created: package.json`);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Parse command line arguments
 | 
			
		||||
const args = process.argv.slice(2);
 | 
			
		||||
const pluginName = args[0];
 | 
			
		||||
const language = args[1] || "ts";
 | 
			
		||||
 | 
			
		||||
// Validate plugin name
 | 
			
		||||
if (!pluginName) {
 | 
			
		||||
    console.error("❌ Plugin name is required!");
 | 
			
		||||
    console.log("Usage:");
 | 
			
		||||
    console.log("  node scripts/create-plugin.js <plugin-name> [ts|js]");
 | 
			
		||||
    console.log("");
 | 
			
		||||
    console.log("Examples:");
 | 
			
		||||
    console.log("  node scripts/create-plugin.js MyAwesomePlugin ts");
 | 
			
		||||
    console.log("  node scripts/create-plugin.js MyJSPlugin js");
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Validate plugin name format
 | 
			
		||||
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(pluginName)) {
 | 
			
		||||
    console.error("❌ Plugin name must start with a letter and contain only letters, numbers, and underscores!");
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create the plugin
 | 
			
		||||
if (require.main === module) {
 | 
			
		||||
    const generator = new PluginGenerator();
 | 
			
		||||
    generator.createPlugin(pluginName, language);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = { PluginGenerator };
 | 
			
		||||
							
								
								
									
										19
									
								
								scripts/new-plugin-js.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								scripts/new-plugin-js.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
/**
 | 
			
		||||
 * JavaScript Plugin Generator Wrapper
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const { PluginGenerator } = require("./create-plugin-core");
 | 
			
		||||
 | 
			
		||||
// Get plugin name from command line arguments
 | 
			
		||||
const pluginName = process.argv[2];
 | 
			
		||||
 | 
			
		||||
if (!pluginName) {
 | 
			
		||||
    console.error("❌ Plugin name is required!");
 | 
			
		||||
    console.log("Usage: npm run new-plugin-js <plugin-name>");
 | 
			
		||||
    console.log("Example: npm run new-plugin-js MyJSPlugin");
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create JavaScript plugin
 | 
			
		||||
const generator = new PluginGenerator();
 | 
			
		||||
generator.createPlugin(pluginName, "js");
 | 
			
		||||
							
								
								
									
										19
									
								
								scripts/new-plugin-ts.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								scripts/new-plugin-ts.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
/**
 | 
			
		||||
 * TypeScript Plugin Generator Wrapper
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const { PluginGenerator } = require("./create-plugin-core");
 | 
			
		||||
 | 
			
		||||
// Get plugin name from command line arguments
 | 
			
		||||
const pluginName = process.argv[2];
 | 
			
		||||
 | 
			
		||||
if (!pluginName) {
 | 
			
		||||
    console.error("❌ Plugin name is required!");
 | 
			
		||||
    console.log("Usage: npm run new-plugin-ts <plugin-name>");
 | 
			
		||||
    console.log("Example: npm run new-plugin-ts MyAwesomePlugin");
 | 
			
		||||
    process.exit(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Create TypeScript plugin
 | 
			
		||||
const generator = new PluginGenerator();
 | 
			
		||||
generator.createPlugin(pluginName, "ts");
 | 
			
		||||
							
								
								
									
										128
									
								
								scripts/plugin-discovery.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								scripts/plugin-discovery.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,128 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Plugin Discovery Script
 | 
			
		||||
 * This script runs during build time to discover all plugins and generate a registry
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const fs = require("fs");
 | 
			
		||||
const path = require("path");
 | 
			
		||||
 | 
			
		||||
class PluginDiscovery {
 | 
			
		||||
    constructor(pluginsDir = "plugins", outputPath = "build/plugin-registry.json") {
 | 
			
		||||
        this.pluginsDir = path.resolve(pluginsDir);
 | 
			
		||||
        this.outputPath = path.resolve(outputPath);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Discover all plugins in the plugins directory
 | 
			
		||||
     */
 | 
			
		||||
    discoverPlugins() {
 | 
			
		||||
        const registry = {
 | 
			
		||||
            plugins: [],
 | 
			
		||||
            manifest: {},
 | 
			
		||||
            buildTime: new Date().toISOString()
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        console.log(`🔍 Discovering plugins in: ${this.pluginsDir}`);
 | 
			
		||||
 | 
			
		||||
        if (!fs.existsSync(this.pluginsDir)) {
 | 
			
		||||
            console.log("⚠️  Plugins directory not found, creating empty registry");
 | 
			
		||||
            return registry;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const pluginDirs = fs
 | 
			
		||||
            .readdirSync(this.pluginsDir, { withFileTypes: true })
 | 
			
		||||
            .filter(dirent => dirent.isDirectory())
 | 
			
		||||
            .map(dirent => dirent.name);
 | 
			
		||||
 | 
			
		||||
        for (const pluginDir of pluginDirs) {
 | 
			
		||||
            const pluginPath = path.join(this.pluginsDir, pluginDir);
 | 
			
		||||
            const manifest = this.loadPluginManifest(pluginPath);
 | 
			
		||||
 | 
			
		||||
            if (manifest) {
 | 
			
		||||
                registry.plugins.push(pluginPath);
 | 
			
		||||
                registry.manifest[manifest.name] = manifest;
 | 
			
		||||
                console.log(`✅ Found plugin: ${manifest.name} v${manifest.version}`);
 | 
			
		||||
            } else {
 | 
			
		||||
                console.log(`❌ Invalid plugin: ${pluginDir} (missing or invalid plugin.json)`);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        console.log(`📦 Discovered ${registry.plugins.length} plugins`);
 | 
			
		||||
        return registry;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load and validate plugin manifest
 | 
			
		||||
     */
 | 
			
		||||
    loadPluginManifest(pluginPath) {
 | 
			
		||||
        try {
 | 
			
		||||
            const manifestPath = path.join(pluginPath, "plugin.json");
 | 
			
		||||
 | 
			
		||||
            if (!fs.existsSync(manifestPath)) {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const manifestContent = fs.readFileSync(manifestPath, "utf-8");
 | 
			
		||||
            const manifest = JSON.parse(manifestContent);
 | 
			
		||||
 | 
			
		||||
            // Validate required fields
 | 
			
		||||
            if (!manifest.name || !manifest.version || !manifest.main) {
 | 
			
		||||
                console.log(`⚠️  Invalid manifest in ${pluginPath}: missing required fields (name, version, main)`);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Check if main file exists
 | 
			
		||||
            const mainFile = path.join(pluginPath, manifest.main);
 | 
			
		||||
            const mainTsFile = mainFile.replace(/\.js$/, ".ts");
 | 
			
		||||
 | 
			
		||||
            if (!fs.existsSync(mainFile) && !fs.existsSync(mainTsFile)) {
 | 
			
		||||
                console.log(`⚠️  Main file not found: ${mainFile} or ${mainTsFile}`);
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return manifest;
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.log(`❌ Failed to load manifest from ${pluginPath}:`, error.message);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Save the plugin registry to file
 | 
			
		||||
     */
 | 
			
		||||
    saveRegistry(registry) {
 | 
			
		||||
        try {
 | 
			
		||||
            // Ensure output directory exists
 | 
			
		||||
            const outputDir = path.dirname(this.outputPath);
 | 
			
		||||
            if (!fs.existsSync(outputDir)) {
 | 
			
		||||
                fs.mkdirSync(outputDir, { recursive: true });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fs.writeFileSync(this.outputPath, JSON.stringify(registry, null, 2));
 | 
			
		||||
            console.log(`💾 Plugin registry saved to: ${this.outputPath}`);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            console.error(`❌ Failed to save plugin registry:`, error.message);
 | 
			
		||||
            process.exit(1);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Main execution function
 | 
			
		||||
     */
 | 
			
		||||
    run() {
 | 
			
		||||
        console.log("🚀 Starting plugin discovery...");
 | 
			
		||||
 | 
			
		||||
        const registry = this.discoverPlugins();
 | 
			
		||||
        this.saveRegistry(registry);
 | 
			
		||||
 | 
			
		||||
        console.log("✨ Plugin discovery completed!");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Run the plugin discovery if this script is executed directly
 | 
			
		||||
if (require.main === module) {
 | 
			
		||||
    const discovery = new PluginDiscovery();
 | 
			
		||||
    discovery.run();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = { PluginDiscovery };
 | 
			
		||||
							
								
								
									
										22
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								src/index.ts
									
									
									
									
									
								
							@ -23,18 +23,38 @@ import { startWebServer } from "./services/webService";
 | 
			
		||||
 | 
			
		||||
import { syncConfigWithDatabase, validateConfig } from "@/src/services/configWatcherService";
 | 
			
		||||
import { updateWorldStateCollections } from "./services/worldStateService";
 | 
			
		||||
import { pluginManager } from "@/src/managers/pluginManager";
 | 
			
		||||
 | 
			
		||||
// Patch JSON.stringify to work flawlessly with Bigints.
 | 
			
		||||
JSON.stringify = JSONStringify;
 | 
			
		||||
 | 
			
		||||
validateConfig();
 | 
			
		||||
 | 
			
		||||
// Handle graceful shutdown
 | 
			
		||||
process.on("SIGINT", () => {
 | 
			
		||||
    logger.info("Received SIGINT, starting graceful shutdown...");
 | 
			
		||||
    void pluginManager.cleanup().then(() => {
 | 
			
		||||
        process.exit(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
process.on("SIGTERM", () => {
 | 
			
		||||
    logger.info("Received SIGTERM, starting graceful shutdown...");
 | 
			
		||||
    void pluginManager.cleanup().then(() => {
 | 
			
		||||
        process.exit(0);
 | 
			
		||||
    });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
mongoose
 | 
			
		||||
    .connect(config.mongodbUrl)
 | 
			
		||||
    .then(() => {
 | 
			
		||||
    .then(async () => {
 | 
			
		||||
        logger.info("Connected to MongoDB");
 | 
			
		||||
        syncConfigWithDatabase();
 | 
			
		||||
 | 
			
		||||
        // Initialize plugins before starting the web server
 | 
			
		||||
        logger.info("Loading plugins...");
 | 
			
		||||
        await pluginManager.loadPlugins();
 | 
			
		||||
 | 
			
		||||
        startWebServer();
 | 
			
		||||
 | 
			
		||||
        void updateWorldStateCollections();
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										193
									
								
								src/managers/pluginManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								src/managers/pluginManager.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,193 @@
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { IPlugin, PluginInfo, PluginManifest } from "@/src/types/pluginTypes";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { pathToFileURL } from "url";
 | 
			
		||||
 | 
			
		||||
interface PluginRegistry {
 | 
			
		||||
    plugins: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface PluginModule {
 | 
			
		||||
    default?:
 | 
			
		||||
        | (new () => IPlugin)
 | 
			
		||||
        | {
 | 
			
		||||
              default?: new () => IPlugin;
 | 
			
		||||
          };
 | 
			
		||||
    [key: string]: unknown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface PluginConfig {
 | 
			
		||||
    enabled?: boolean;
 | 
			
		||||
    [key: string]: unknown;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class PluginManager {
 | 
			
		||||
    private plugins: Map<string, IPlugin> = new Map();
 | 
			
		||||
    private pluginInfos: Map<string, PluginInfo> = new Map();
 | 
			
		||||
    private pluginsDir: string;
 | 
			
		||||
 | 
			
		||||
    constructor(pluginsDir: string = "plugins") {
 | 
			
		||||
        this.pluginsDir = path.resolve(pluginsDir);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load all plugins from the plugins directory
 | 
			
		||||
     */
 | 
			
		||||
    async loadPlugins(): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            // Load plugin registry generated at build time
 | 
			
		||||
            const pluginRegistryPath = path.resolve("build/plugin-registry.json");
 | 
			
		||||
            if (!fs.existsSync(pluginRegistryPath)) {
 | 
			
		||||
                logger.info("No plugin registry found, skipping plugin loading");
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const pluginRegistry = JSON.parse(fs.readFileSync(pluginRegistryPath, "utf-8")) as PluginRegistry;
 | 
			
		||||
            logger.info(`Found ${pluginRegistry.plugins.length} plugins in registry`);
 | 
			
		||||
 | 
			
		||||
            for (const pluginPath of pluginRegistry.plugins) {
 | 
			
		||||
                await this.loadPlugin(pluginPath);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            logger.info(`Successfully loaded ${this.plugins.size} plugins`);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            logger.error("Failed to load plugins:", error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Load a single plugin
 | 
			
		||||
     */
 | 
			
		||||
    private async loadPlugin(pluginPath: string): Promise<void> {
 | 
			
		||||
        try {
 | 
			
		||||
            const absolutePath = path.resolve(pluginPath);
 | 
			
		||||
            const manifestPath = path.join(absolutePath, "plugin.json");
 | 
			
		||||
 | 
			
		||||
            if (!fs.existsSync(manifestPath)) {
 | 
			
		||||
                logger.warn(`Plugin manifest not found: ${manifestPath}`);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as PluginManifest;
 | 
			
		||||
 | 
			
		||||
            // Check if plugin is enabled from config
 | 
			
		||||
            const pluginConfig = manifest.config as PluginConfig | undefined;
 | 
			
		||||
            const pluginInfo: PluginInfo = {
 | 
			
		||||
                manifest,
 | 
			
		||||
                path: absolutePath,
 | 
			
		||||
                enabled: pluginConfig?.enabled ?? true
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            if (!pluginInfo.enabled) {
 | 
			
		||||
                logger.info(`Plugin ${manifest.name} is disabled, skipping`);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Load the plugin module from build directory
 | 
			
		||||
            const pluginName = path.basename(absolutePath);
 | 
			
		||||
            const buildPluginPath = path.resolve("build/plugins", pluginName);
 | 
			
		||||
            const mainFile = path.join(buildPluginPath, manifest.main || "index.js");
 | 
			
		||||
            if (!fs.existsSync(mainFile)) {
 | 
			
		||||
                logger.error(`Plugin main file not found: ${mainFile}`);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const pluginModule = (await import(pathToFileURL(mainFile).href)) as PluginModule;
 | 
			
		||||
 | 
			
		||||
            let PluginClass: (new () => IPlugin) | undefined;
 | 
			
		||||
 | 
			
		||||
            if (typeof pluginModule.default === "function") {
 | 
			
		||||
                PluginClass = pluginModule.default;
 | 
			
		||||
            } else if (typeof pluginModule.default === "object" && pluginModule.default.default) {
 | 
			
		||||
                PluginClass = pluginModule.default.default;
 | 
			
		||||
            } else if (pluginModule[manifest.name]) {
 | 
			
		||||
                PluginClass = pluginModule[manifest.name] as new () => IPlugin;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!PluginClass) {
 | 
			
		||||
                logger.error(`Plugin class not found in ${mainFile}`);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const plugin = new PluginClass();
 | 
			
		||||
 | 
			
		||||
            // Validate plugin interface
 | 
			
		||||
            if (!this.validatePlugin(plugin)) {
 | 
			
		||||
                logger.error(`Plugin ${manifest.name} does not implement required interface`);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Initialize the plugin
 | 
			
		||||
            await Promise.resolve(plugin.initialize());
 | 
			
		||||
 | 
			
		||||
            this.plugins.set(manifest.name, plugin);
 | 
			
		||||
            this.pluginInfos.set(manifest.name, pluginInfo);
 | 
			
		||||
 | 
			
		||||
            logger.info(`Loaded plugin: ${manifest.name} v${manifest.version}`);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            logger.error(`Failed to load plugin from ${pluginPath}:`, error);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Validate that a plugin implements the required interface
 | 
			
		||||
     */
 | 
			
		||||
    private validatePlugin(plugin: unknown): plugin is IPlugin {
 | 
			
		||||
        return (
 | 
			
		||||
            plugin != null &&
 | 
			
		||||
            typeof plugin === "object" &&
 | 
			
		||||
            "name" in plugin &&
 | 
			
		||||
            "version" in plugin &&
 | 
			
		||||
            "initialize" in plugin &&
 | 
			
		||||
            typeof (plugin as Record<string, unknown>).name === "string" &&
 | 
			
		||||
            typeof (plugin as Record<string, unknown>).version === "string" &&
 | 
			
		||||
            typeof (plugin as Record<string, unknown>).initialize === "function"
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get a plugin by name
 | 
			
		||||
     */
 | 
			
		||||
    getPlugin(name: string): IPlugin | undefined {
 | 
			
		||||
        return this.plugins.get(name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get all loaded plugins
 | 
			
		||||
     */
 | 
			
		||||
    getAllPlugins(): Map<string, IPlugin> {
 | 
			
		||||
        return new Map(this.plugins);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Get plugin info
 | 
			
		||||
     */
 | 
			
		||||
    getPluginInfo(name: string): PluginInfo | undefined {
 | 
			
		||||
        return this.pluginInfos.get(name);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Cleanup all plugins
 | 
			
		||||
     */
 | 
			
		||||
    async cleanup(): Promise<void> {
 | 
			
		||||
        logger.info("Cleaning up plugins...");
 | 
			
		||||
 | 
			
		||||
        for (const [name, plugin] of this.plugins) {
 | 
			
		||||
            try {
 | 
			
		||||
                if (plugin.cleanup) {
 | 
			
		||||
                    await Promise.resolve(plugin.cleanup());
 | 
			
		||||
                }
 | 
			
		||||
                logger.info(`Cleaned up plugin: ${name}`);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                logger.error(`Failed to cleanup plugin ${name}:`, error);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.plugins.clear();
 | 
			
		||||
        this.pluginInfos.clear();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Global plugin manager instance
 | 
			
		||||
export const pluginManager = new PluginManager();
 | 
			
		||||
							
								
								
									
										52
									
								
								src/types/pluginTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/types/pluginTypes.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
export interface IPlugin {
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugin name
 | 
			
		||||
     */
 | 
			
		||||
    name: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugin version
 | 
			
		||||
     */
 | 
			
		||||
    version: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugin description
 | 
			
		||||
     */
 | 
			
		||||
    description?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugin author
 | 
			
		||||
     */
 | 
			
		||||
    author?: string;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Initialize the plugin
 | 
			
		||||
     */
 | 
			
		||||
    initialize(): Promise<void> | void;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Cleanup plugin resources
 | 
			
		||||
     */
 | 
			
		||||
    cleanup?(): Promise<void> | void;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Plugin configuration
 | 
			
		||||
     */
 | 
			
		||||
    config?: Record<string, unknown>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PluginManifest {
 | 
			
		||||
    name: string;
 | 
			
		||||
    version: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
    author?: string;
 | 
			
		||||
    main: string;
 | 
			
		||||
    dependencies?: string[];
 | 
			
		||||
    config?: Record<string, unknown>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PluginInfo {
 | 
			
		||||
    manifest: PluginManifest;
 | 
			
		||||
    path: string;
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user