@neurolint/cli
Version:
NeuroLint CLI for React/Next.js modernization with advanced 6-layer orchestration and intelligent AST transformations
545 lines (439 loc) • 14 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const { PluginSystem } = require('./plugin-system');
const chalk = require('chalk');
/**
* Plugin Manager for NeuroLint CLI
* Handles plugin installation, removal, and management
*/
class PluginManager {
constructor(config = {}) {
this.pluginSystem = new PluginSystem();
this.registryUrl = config.registryUrl || 'https://registry.neurolint.dev/plugins';
this.pluginDir = config.pluginDir || path.join(process.cwd(), '.neurolint', 'plugins');
this.configFile = path.join(process.cwd(), '.neurolint', 'plugins.json');
this.installedPlugins = new Map();
}
/**
* Initialize plugin manager
*/
async initialize() {
await fs.ensureDir(this.pluginDir);
await this.loadInstalledPlugins();
await this.pluginSystem.initialize({
pluginDir: this.pluginDir
});
}
/**
* Install plugin from various sources
*/
async installPlugin(source, options = {}) {
console.log(chalk.blue(`Installing plugin: ${source}`));
let plugin;
let pluginId;
try {
if (source.startsWith('http') || source.startsWith('https')) {
// Install from URL
plugin = await this.installFromUrl(source);
} else if (source.includes('/') || source.includes('\\')) {
// Install from local path
plugin = await this.installFromPath(source);
} else if (source.startsWith('@') || source.includes('/')) {
// Install from npm package
plugin = await this.installFromNpm(source);
} else {
// Install from NeuroLint registry
plugin = await this.installFromRegistry(source);
}
// Register plugin with the system
pluginId = await this.pluginSystem.registerPlugin(plugin);
// Save plugin metadata
await this.savePluginMetadata(pluginId, {
source,
installedAt: new Date().toISOString(),
version: plugin.version,
enabled: true,
...options
});
console.log(chalk.green(`Plugin installed successfully: ${plugin.name} v${plugin.version}`));
return { pluginId, plugin };
} catch (error) {
console.error(chalk.red(`Plugin installation failed: ${error.message}`));
throw error;
}
}
/**
* Install plugin from URL
*/
async installFromUrl(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch plugin from ${url}: ${response.statusText}`);
}
const pluginCode = await response.text();
// Create temporary file and require it
const tempFile = path.join(this.pluginDir, `temp_${Date.now()}.js`);
await fs.writeFile(tempFile, pluginCode);
try {
const plugin = require(tempFile);
// Move to permanent location
const pluginFile = path.join(this.pluginDir, `${plugin.name}.js`);
await fs.move(tempFile, pluginFile);
return plugin;
} catch (error) {
await fs.remove(tempFile);
throw error;
}
}
/**
* Install plugin from local path
*/
async installFromPath(localPath) {
const absolutePath = path.resolve(localPath);
if (!(await fs.pathExists(absolutePath))) {
throw new Error(`Plugin file not found: ${absolutePath}`);
}
const plugin = require(absolutePath);
// Copy to plugin directory
const pluginFile = path.join(this.pluginDir, `${plugin.name}.js`);
await fs.copy(absolutePath, pluginFile);
return plugin;
}
/**
* Install plugin from npm package
*/
async installFromNpm(packageName) {
// This would require npm installation - simplified for now
throw new Error('NPM package installation not yet implemented. Use local path or URL.');
}
/**
* Install plugin from NeuroLint registry
*/
async installFromRegistry(pluginName) {
const registryUrl = `${this.registryUrl}/${pluginName}`;
try {
const response = await fetch(registryUrl);
if (!response.ok) {
throw new Error(`Plugin not found in registry: ${pluginName}`);
}
const pluginInfo = await response.json();
// Download plugin from registry
const downloadUrl = pluginInfo.downloadUrl;
return await this.installFromUrl(downloadUrl);
} catch (error) {
throw new Error(`Failed to install from registry: ${error.message}`);
}
}
/**
* Remove plugin
*/
async removePlugin(pluginName) {
console.log(chalk.blue(`Removing plugin: ${pluginName}`));
try {
// Find plugin by name
const pluginId = this.findPluginByName(pluginName);
if (!pluginId) {
throw new Error(`Plugin not found: ${pluginName}`);
}
// Remove from plugin system
this.pluginSystem.removePlugin(pluginId);
// Remove plugin file
const pluginFile = path.join(this.pluginDir, `${pluginName}.js`);
if (await fs.pathExists(pluginFile)) {
await fs.remove(pluginFile);
}
// Remove metadata
this.installedPlugins.delete(pluginId);
await this.saveInstalledPlugins();
console.log(chalk.green(`Plugin removed successfully: ${pluginName}`));
} catch (error) {
console.error(chalk.red(`Plugin removal failed: ${error.message}`));
throw error;
}
}
/**
* List installed plugins
*/
listPlugins() {
const plugins = this.pluginSystem.listPlugins();
return plugins.map(plugin => {
const metadata = this.installedPlugins.get(plugin.id) || {};
return {
...plugin,
source: metadata.source || 'unknown',
installedAt: metadata.installedAt || 'unknown',
enabled: metadata.enabled !== false
};
});
}
/**
* Enable/disable plugin
*/
async togglePlugin(pluginName, enabled) {
const pluginId = this.findPluginByName(pluginName);
if (!pluginId) {
throw new Error(`Plugin not found: ${pluginName}`);
}
const metadata = this.installedPlugins.get(pluginId) || {};
metadata.enabled = enabled;
this.installedPlugins.set(pluginId, metadata);
await this.saveInstalledPlugins();
console.log(chalk.green(`Plugin ${pluginName} ${enabled ? 'enabled' : 'disabled'}`));
}
/**
* Update plugin
*/
async updatePlugin(pluginName) {
console.log(chalk.blue(`Updating plugin: ${pluginName}`));
try {
const pluginId = this.findPluginByName(pluginName);
if (!pluginId) {
throw new Error(`Plugin not found: ${pluginName}`);
}
const metadata = this.installedPlugins.get(pluginId);
if (!metadata || !metadata.source) {
throw new Error(`Cannot update plugin: missing source information`);
}
// Remove old version
await this.removePlugin(pluginName);
// Install new version
await this.installPlugin(metadata.source, { update: true });
console.log(chalk.green(`Plugin updated successfully: ${pluginName}`));
} catch (error) {
console.error(chalk.red(`Plugin update failed: ${error.message}`));
throw error;
}
}
/**
* Search registry for plugins
*/
async searchPlugins(query) {
try {
const searchUrl = `${this.registryUrl}/search?q=${encodeURIComponent(query)}`;
const response = await fetch(searchUrl);
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
const results = await response.json();
return results.plugins || [];
} catch (error) {
console.warn(chalk.yellow(`Search failed: ${error.message}`));
return [];
}
}
/**
* Create plugin template
*/
async createPluginTemplate(pluginName, options = {}) {
const pluginDir = path.join(process.cwd(), pluginName);
if (await fs.pathExists(pluginDir)) {
throw new Error(`Directory already exists: ${pluginDir}`);
}
await fs.ensureDir(pluginDir);
const template = this.generatePluginTemplate(pluginName, options);
// Write main plugin file
await fs.writeFile(path.join(pluginDir, 'index.js'), template.main);
// Write package.json
await fs.writeFile(path.join(pluginDir, 'package.json'), JSON.stringify(template.packageJson, null, 2));
// Write README
await fs.writeFile(path.join(pluginDir, 'README.md'), template.readme);
// Write test file
await fs.writeFile(path.join(pluginDir, 'test.js'), template.test);
console.log(chalk.green(`Plugin template created: ${pluginDir}`));
console.log(chalk.gray(`Next steps:`));
console.log(chalk.gray(` cd ${pluginName}`));
console.log(chalk.gray(` # Edit index.js to implement your plugin`));
console.log(chalk.gray(` neurolint plugin install .`));
}
/**
* Generate plugin template
*/
generatePluginTemplate(pluginName, options) {
const className = pluginName.charAt(0).toUpperCase() + pluginName.slice(1).replace(/-/g, '');
const main = `/**
* ${pluginName} - NeuroLint Plugin
* ${options.description || 'Custom NeuroLint plugin'}
*/
module.exports = {
name: '${pluginName}',
version: '1.0.0',
description: '${options.description || 'Custom NeuroLint plugin'}',
author: '${options.author || 'Your Name'}',
// Plugin initialization
initialize(context) {
context.log('${className} plugin initialized');
},
// Plugin cleanup
cleanup() {
// Cleanup resources if needed
},
// Hook implementations
hooks: {
'before-analysis': {
priority: 0,
handler: async (context) => {
// Called before code analysis starts
return { success: true };
}
},
'after-analysis': {
priority: 0,
handler: async (context) => {
// Called after code analysis completes
return { success: true };
}
}
},
// Custom transformers
transformers: {
'${pluginName}-transform': {
layer: 3, // Target layer (1-6)
priority: 0,
handler: async (ast, context) => {
// Implement your AST transformation here
const transformations = [];
// Example transformation
// traverse(ast, {
// FunctionDeclaration(path) {
// // Your transformation logic
// }
// });
return transformations;
}
}
},
// Custom validators
validators: {
'${pluginName}-validator': {
priority: 0,
handler: async (code, context) => {
// Implement your code validation here
const issues = [];
// Example validation
if (code.includes('// TODO')) {
issues.push({
type: 'todo-comment',
severity: 'info',
message: 'TODO comment found',
line: 1 // Calculate actual line number
});
}
return issues;
}
}
}
};
`;
const packageJson = {
name: pluginName,
version: '1.0.0',
description: options.description || 'Custom NeuroLint plugin',
main: 'index.js',
scripts: {
test: 'node test.js'
},
keywords: ['neurolint', 'plugin', 'code-analysis'],
author: options.author || 'Your Name',
license: 'MIT',
peerDependencies: {
'@babel/parser': '^7.0.0',
'@babel/traverse': '^7.0.0',
'@babel/types': '^7.0.0'
}
};
const readme = `# ${pluginName}
${options.description || 'Custom NeuroLint plugin'}
## Installation
\`\`\`bash
neurolint plugin install .
\`\`\`
## Usage
This plugin will be automatically loaded by NeuroLint when installed.
## Configuration
Add plugin configuration to your \`.neurolintrc.json\`:
\`\`\`json
{
"plugins": {
"${pluginName}": {
"enabled": true,
"options": {
// Plugin-specific options
}
}
}
}
\`\`\`
## Development
1. Clone this repository
2. Make your changes to \`index.js\`
3. Test with \`npm test\`
4. Install locally with \`neurolint plugin install .\`
## License
MIT
`;
const test = `/**
* Basic test for ${pluginName} plugin
*/
const plugin = require('./index.js');
// Test plugin structure
console.log('Testing plugin structure...');
if (!plugin.name) {
throw new Error('Plugin must have a name');
}
if (!plugin.version) {
throw new Error('Plugin must have a version');
}
console.log(\`Plugin \${plugin.name} v\${plugin.version} structure is valid\`);
// Test plugin initialization
if (plugin.initialize) {
const mockContext = {
log: (msg) => console.log(\`[Mock] \${msg}\`),
warn: (msg) => console.warn(\`[Mock] \${msg}\`),
pluginId: 'test-plugin'
};
plugin.initialize(mockContext);
console.log('Plugin initialization test passed');
}
console.log('All tests passed!');
`;
return {
main,
packageJson,
readme,
test
};
}
/**
* Helper methods
*/
findPluginByName(name) {
const plugins = this.pluginSystem.listPlugins();
const plugin = plugins.find(p => p.name === name);
return plugin ? plugin.id : null;
}
async loadInstalledPlugins() {
try {
if (await fs.pathExists(this.configFile)) {
const config = await fs.readJson(this.configFile);
this.installedPlugins = new Map(Object.entries(config.plugins || {}));
}
} catch (error) {
console.warn('Failed to load plugin configuration:', error.message);
}
}
async saveInstalledPlugins() {
try {
const config = {
plugins: Object.fromEntries(this.installedPlugins)
};
await fs.writeJson(this.configFile, config, { spaces: 2 });
} catch (error) {
console.warn('Failed to save plugin configuration:', error.message);
}
}
async savePluginMetadata(pluginId, metadata) {
this.installedPlugins.set(pluginId, metadata);
await this.saveInstalledPlugins();
}
}
module.exports = { PluginManager };