vibe-codex
Version:
CLI tool to install development rules and git hooks with interactive configuration
276 lines (235 loc) • 6.92 kB
JavaScript
/**
* Module loader for vibe-codex
* Dynamically loads and manages rule modules based on configuration
*/
import fs from "fs/promises";
import path from "path";
import { fileURLToPath } from "url";
import logger from "../utils/logger.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export class ModuleLoader {
constructor() {
this.modules = new Map();
this.config = null;
this.projectPath = process.cwd();
}
/**
* Initialize the module loader with project configuration
* @param {string} projectPath - Project root path
* @returns {Promise<void>}
*/
async initialize(projectPath = process.cwd()) {
this.projectPath = projectPath;
// Load configuration
this.config = await this.loadConfiguration();
// Load enabled modules
await this.loadEnabledModules();
}
/**
* Load project configuration
* @returns {Promise<Object>}
*/
async loadConfiguration() {
const configPaths = [
path.join(this.projectPath, ".vibe-codex.json"),
path.join(this.projectPath, "vibe-codex.config.json"),
path.join(this.projectPath, "package.json"),
];
for (const configPath of configPaths) {
try {
const content = await fs.readFile(configPath, "utf-8");
const data = JSON.parse(content);
// Extract vibe-codex config from package.json if needed
if (configPath.endsWith("package.json")) {
if (data["vibe-codex"]) {
return data["vibe-codex"];
}
} else {
return data;
}
} catch (error) {
// Config file not found or invalid, continue to next
}
}
// Return default configuration
return {
version: "1.0.0",
modules: {
core: { enabled: true },
},
};
}
/**
* Load all enabled modules
* @returns {Promise<void>}
*/
async loadEnabledModules() {
const moduleConfig = this.config.modules || {};
// Core module is always loaded
await this.loadModule("core", moduleConfig.core || { enabled: true });
// Load other enabled modules
for (const [moduleName, config] of Object.entries(moduleConfig)) {
if (moduleName !== "core" && config && config.enabled) {
await this.loadModule(moduleName, config);
}
}
// Validate module dependencies
await this.validateDependencies();
}
/**
* Load a specific module
* @param {string} moduleName - Module name
* @param {Object} config - Module configuration
* @returns {Promise<void>}
*/
async loadModule(moduleName, config) {
try {
// Try to load built-in module
const modulePath = path.join(__dirname, moduleName, "index.js");
const module = await import(modulePath);
const instance = module.default || new module[Object.keys(module)[0]]();
// Apply module configuration
if (config.options) {
Object.assign(instance.options, config.options);
}
// Initialize module
await instance.initialize();
this.modules.set(moduleName, instance);
logger.debug(`✅ Loaded module: ${moduleName}`);
} catch (error) {
// Try to load custom module
if (this.config.customModules && this.config.customModules[moduleName]) {
try {
const customPath = path.join(
this.projectPath,
this.config.customModules[moduleName],
);
const module = await import(customPath);
const instance =
module.default || new module[Object.keys(module)[0]]();
await instance.initialize();
this.modules.set(moduleName, instance);
logger.debug(`✅ Loaded custom module: ${moduleName}`);
} catch (customError) {
logger.error(
`❌ Failed to load module ${moduleName}:`,
customError.message,
);
}
} else {
logger.warn(`⚠️ Module ${moduleName} not found`);
}
}
}
/**
* Validate module dependencies
* @returns {Promise<void>}
*/
async validateDependencies() {
const errors = [];
for (const [moduleName, module] of this.modules) {
const validationErrors = module.validateConfig({
modules: Object.fromEntries(
Array.from(this.modules.entries()).map(([name, mod]) => [
name,
{ enabled: true },
]),
),
});
errors.push(...validationErrors);
}
if (errors.length > 0) {
logger.error("❌ Module dependency errors:");
errors.forEach((error) => {
logger.error(` - ${error.module}: ${error.message}`);
});
throw new Error("Module dependency validation failed");
}
}
/**
* Get all rules from enabled modules
* @returns {Array<Object>}
*/
getAllRules() {
const rules = [];
for (const module of this.modules.values()) {
rules.push(...module.getEnabledRules());
}
return rules;
}
/**
* Get rules by level
* @param {number} level - Rule level
* @returns {Array<Object>}
*/
getRulesByLevel(level) {
const rules = [];
for (const module of this.modules.values()) {
rules.push(...module.getRulesByLevel(level));
}
return rules;
}
/**
* Get all hooks for an event
* @param {string} event - Hook event name
* @returns {Array<Function>}
*/
getHooks(event) {
const hooks = [];
for (const module of this.modules.values()) {
hooks.push(...module.getHooks(event));
}
return hooks;
}
/**
* Run all validators
* @param {Object} context - Validation context
* @returns {Promise<Object>}
*/
async runValidators(context) {
const results = {};
for (const [moduleName, module] of this.modules) {
results[moduleName] = {};
for (const [validatorName, validator] of Object.entries(
module.validators,
)) {
try {
results[moduleName][validatorName] = await validator(context);
} catch (error) {
results[moduleName][validatorName] = {
valid: false,
error: error.message,
};
}
}
}
return results;
}
/**
* Get module by name
* @param {string} name - Module name
* @returns {RuleModule|null}
*/
getModule(name) {
return this.modules.get(name) || null;
}
/**
* Get loaded module names
* @returns {Array<string>}
*/
getLoadedModules() {
return Array.from(this.modules.keys());
}
/**
* Save configuration
* @param {Object} config - Configuration to save
* @returns {Promise<void>}
*/
async saveConfiguration(config) {
const configPath = path.join(this.projectPath, ".vibe-codex.json");
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
this.config = config;
}
}
// Export singleton instance
export default new ModuleLoader();