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