peezy-cli
Version:
Production-ready CLI for scaffolding modern applications with curated full-stack templates, intelligent migrations, and enterprise security.
312 lines • 10.1 kB
JavaScript
/**
* Plugin Manager v1.0
*
* Manages plugin lifecycle, loading, and execution
*/
import { readFile } from "node:fs/promises";
import { resolve, join } from "node:path";
import { pathToFileURL } from "node:url";
import semver from "semver";
import { PluginError, PluginVersionError, PluginLoadError } from "./types.js";
import { log } from "../../utils/logger.js";
/**
* Plugin Manager - handles plugin discovery, loading, and execution
*/
export class PluginManager {
plugins = new Map();
config;
cliVersion;
constructor(config, cliVersion) {
this.config = config;
this.cliVersion = cliVersion;
}
/**
* Load all configured plugins
*/
async loadPlugins() {
const startTime = Date.now();
let loadedCount = 0;
let errorCount = 0;
for (const [pluginName, pluginConfig] of Object.entries(this.config.plugins)) {
if (!pluginConfig.enabled) {
continue;
}
try {
await this.loadPlugin(pluginName, pluginConfig.config);
loadedCount++;
}
catch (error) {
errorCount++;
log.warn(`Failed to load plugin ${pluginName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
const loadTime = Date.now() - startTime;
log.debug(`Loaded ${loadedCount} plugins in ${loadTime}ms (${errorCount} errors)`);
}
/**
* Load a single plugin
*/
async loadPlugin(pluginName, config) {
const startTime = Date.now();
try {
// Find plugin module
const pluginPath = await this.findPlugin(pluginName);
if (!pluginPath) {
throw new PluginLoadError(pluginName, new Error(`Plugin not found: ${pluginName}`));
}
// Load plugin module
const pluginModule = await import(pathToFileURL(pluginPath).href);
const PluginClass = pluginModule.default;
if (!PluginClass) {
throw new PluginLoadError(pluginName, new Error("Plugin must export a default class"));
}
// Create plugin instance
const plugin = new PluginClass();
// Validate plugin manifest
this.validatePlugin(plugin);
// Check version compatibility
this.checkVersionCompatibility(plugin.manifest);
// Initialize plugin
if (plugin.initialize) {
await plugin.initialize();
}
// Register plugin
const entry = {
manifest: plugin.manifest,
instance: plugin,
enabled: true,
loadTime: Date.now() - startTime,
config,
};
this.plugins.set(pluginName, entry);
log.debug(`Loaded plugin ${pluginName} v${plugin.manifest.version} in ${entry.loadTime}ms`);
}
catch (error) {
if (error instanceof PluginError) {
throw error;
}
throw new PluginLoadError(pluginName, error);
}
}
/**
* Unload a plugin
*/
async unloadPlugin(pluginName) {
const entry = this.plugins.get(pluginName);
if (!entry) {
return;
}
try {
// Dispose plugin
if (entry.instance.dispose) {
await entry.instance.dispose();
}
// Remove from registry
this.plugins.delete(pluginName);
log.debug(`Unloaded plugin ${pluginName}`);
}
catch (error) {
log.warn(`Error unloading plugin ${pluginName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Unload all plugins
*/
async unloadAllPlugins() {
const pluginNames = Array.from(this.plugins.keys());
await Promise.all(pluginNames.map((name) => this.unloadPlugin(name)));
}
/**
* Execute beforeScaffold hooks
*/
async executeBeforeScaffold(context) {
await this.executeHooks("beforeScaffold", context);
}
/**
* Execute templateTransform hooks
*/
async executeTemplateTransform(context) {
await this.executeHooks("templateTransform", context);
}
/**
* Execute afterScaffold hooks
*/
async executeAfterScaffold(context) {
await this.executeHooks("afterScaffold", context);
}
/**
* Execute beforeInstall hooks
*/
async executeBeforeInstall(context) {
await this.executeHooks("beforeInstall", context);
}
/**
* Execute afterInstall hooks
*/
async executeAfterInstall(context) {
await this.executeHooks("afterInstall", context);
}
/**
* Execute configValidation hooks
*/
async executeConfigValidation(config) {
const results = [];
for (const [pluginName, entry] of this.plugins) {
if (!entry.enabled || !entry.instance.hooks.configValidation) {
continue;
}
try {
const result = await entry.instance.hooks.configValidation(config);
results.push(result);
}
catch (error) {
log.warn(`Plugin ${pluginName} config validation failed: ${error instanceof Error ? error.message : String(error)}`);
results.push({
valid: false,
errors: [
`Plugin ${pluginName} validation error: ${error instanceof Error ? error.message : String(error)}`,
],
warnings: [],
});
}
}
return results;
}
/**
* Get loaded plugins
*/
getLoadedPlugins() {
return Array.from(this.plugins.values()).filter((entry) => entry.enabled);
}
/**
* Get plugin by name
*/
getPlugin(name) {
return this.plugins.get(name);
}
/**
* Check if plugin is loaded
*/
isPluginLoaded(name) {
const entry = this.plugins.get(name);
return entry?.enabled ?? false;
}
/**
* Execute hooks of a specific type
*/
async executeHooks(hookName, ...args) {
const promises = [];
for (const [pluginName, entry] of this.plugins) {
if (!entry.enabled) {
continue;
}
const hook = entry.instance.hooks[hookName];
if (!hook) {
continue;
}
promises.push((async () => {
try {
await hook.apply(entry.instance.hooks, args);
}
catch (error) {
log.warn(`Plugin ${pluginName} hook ${hookName} failed: ${error instanceof Error ? error.message : String(error)}`);
}
})());
}
await Promise.all(promises);
}
/**
* Find plugin module path
*/
async findPlugin(pluginName) {
const searchPaths = this.config.searchPaths || [
"node_modules",
join(process.cwd(), "node_modules"),
join(process.env.HOME || "~", ".peezy", "plugins"),
];
for (const searchPath of searchPaths) {
try {
const pluginPath = resolve(searchPath, pluginName);
const packageJsonPath = join(pluginPath, "package.json");
// Check if package.json exists
await readFile(packageJsonPath, "utf8");
// Return main entry point
return join(pluginPath, "index.js");
}
catch {
// Continue searching
}
}
return null;
}
/**
* Validate plugin structure
*/
validatePlugin(plugin) {
if (!plugin.manifest) {
throw new Error("Plugin must have a manifest");
}
if (!plugin.manifest.name) {
throw new Error("Plugin manifest must have a name");
}
if (!plugin.manifest.version) {
throw new Error("Plugin manifest must have a version");
}
if (!semver.valid(plugin.manifest.version)) {
throw new Error(`Plugin version must be valid semver: ${plugin.manifest.version}`);
}
if (!plugin.hooks) {
throw new Error("Plugin must have hooks object");
}
}
/**
* Check version compatibility
*/
checkVersionCompatibility(manifest) {
if (!semver.satisfies(this.cliVersion, manifest.minCliVersion)) {
throw new PluginVersionError(manifest.name, manifest.minCliVersion, this.cliVersion);
}
if (manifest.maxCliVersion &&
!semver.satisfies(this.cliVersion, manifest.maxCliVersion)) {
throw new PluginVersionError(manifest.name, `<=${manifest.maxCliVersion}`, this.cliVersion);
}
}
}
/**
* Default plugin configuration
*/
export const defaultPluginConfig = {
plugins: {},
searchPaths: [
"node_modules",
join(process.cwd(), "node_modules"),
join(process.env.HOME || "~", ".peezy", "plugins"),
],
autoInstall: false,
registries: ["https://registry.npmjs.org"],
};
/**
* Load plugin configuration from file
*/
export async function loadPluginConfig(configPath) {
if (!configPath) {
return defaultPluginConfig;
}
try {
const configContent = await readFile(configPath, "utf8");
const config = JSON.parse(configContent);
return {
...defaultPluginConfig,
...config,
plugins: {
...defaultPluginConfig.plugins,
...config.plugins,
},
};
}
catch (error) {
log.warn(`Failed to load plugin config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
return defaultPluginConfig;
}
}
//# sourceMappingURL=manager.js.map