UNPKG

accs-cli

Version:

ACCS CLI — Full-featured developer tool for scaffolding, running, building, and managing multi-language projects

449 lines (369 loc) 12.2 kB
/** * Plugin system command */ import path from 'path'; import chalk from 'chalk'; import inquirer from 'inquirer'; import { logger } from '../utils/logger.js'; import { FileUtils } from '../utils/file-utils.js'; import { configManager } from '../config/config-manager.js'; export function pluginCommand(program) { program .command('plugin') .argument('<action>', 'Plugin action: add, remove, list, or create') .argument('[name]', 'Plugin name') .option('-g, --global', 'Install plugin globally') .option('-v, --verbose', 'Verbose output') .description('Manage ACCS plugins') .action(async (action, name, options) => { try { await handlePluginAction(action, name, options); } catch (error) { logger.error('Plugin operation failed:', error.message); process.exit(1); } }); } async function handlePluginAction(action, name, options) { switch (action.toLowerCase()) { case 'add': case 'install': if (!name) { throw new Error('Plugin name is required for installation'); } await addPlugin(name, options); break; case 'remove': case 'uninstall': if (!name) { throw new Error('Plugin name is required for removal'); } await removePlugin(name, options); break; case 'list': case 'ls': await listPlugins(options); break; case 'create': await createPlugin(name, options); break; case 'info': if (!name) { throw new Error('Plugin name is required for info'); } await showPluginInfo(name); break; default: throw new Error(`Unknown plugin action: ${action}. Use: add, remove, list, or create`); } } async function addPlugin(name, options) { logger.info(`Installing plugin: ${chalk.cyan(name)}`); // Validate plugin name if (!isValidPluginName(name)) { throw new Error('Invalid plugin name. Use format: accs-plugin-name or @scope/accs-plugin-name'); } const projectRoot = FileUtils.getProjectRoot(); // Check if it's a local plugin file const isLocalPlugin = name.endsWith('.js') || FileUtils.exists(path.resolve(name)); if (isLocalPlugin) { await installLocalPlugin(name, options); } else { await installNpmPlugin(name, projectRoot, options); } // Update config const installedPlugins = configManager.get('plugins', []); if (!installedPlugins.includes(name)) { installedPlugins.push(name); configManager.set('plugins', installedPlugins); } logger.success(`Plugin "${name}" installed successfully`); } async function installNpmPlugin(name, projectRoot, options) { // Add plugin prefix if not present const pluginName = name.startsWith('accs-plugin-') || name.startsWith('@') ? name : `accs-plugin-${name}`; logger.startSpinner(`Installing ${pluginName} from npm...`); try { const { execa } = await import('execa'); const installCmd = options.global ? ['install', '-g'] : ['install', '--save-dev']; await execa('npm', [...installCmd, pluginName], { cwd: projectRoot, stdio: options.verbose ? 'inherit' : 'pipe' }); logger.succeedSpinner(`Installed ${pluginName} from npm`); } catch (error) { logger.failSpinner(`Failed to install ${pluginName}`); throw new Error(`npm install failed: ${error.message}`); } } async function installLocalPlugin(name) { const pluginPath = path.resolve(name); if (!FileUtils.exists(pluginPath)) { throw new Error(`Local plugin file not found: ${name}`); } // Validate plugin structure try { const plugin = await import(pluginPath); if (!plugin.default && !plugin.name) { throw new Error('Plugin must export a name or default export'); } logger.success(`Local plugin validated: ${pluginPath}`); } catch (error) { throw new Error(`Invalid plugin file: ${error.message}`); } } async function removePlugin(name, options) { logger.info(`Removing plugin: ${chalk.cyan(name)}`); const installedPlugins = configManager.get('plugins', []); if (!installedPlugins.includes(name)) { logger.warn(`Plugin "${name}" is not installed`); return; } // Remove from npm if it's an npm plugin const isNpmPlugin = !name.endsWith('.js') && !FileUtils.exists(path.resolve(name)); if (isNpmPlugin) { const projectRoot = FileUtils.getProjectRoot(); const pluginName = name.startsWith('accs-plugin-') || name.startsWith('@') ? name : `accs-plugin-${name}`; try { const { execa } = await import('execa'); await execa('npm', ['uninstall', pluginName], { cwd: projectRoot, stdio: options.verbose ? 'inherit' : 'pipe' }); logger.success(`Uninstalled ${pluginName} from npm`); } catch (error) { logger.warn(`Failed to uninstall from npm: ${error.message}`); } } // Remove from config const updatedPlugins = installedPlugins.filter(p => p !== name); configManager.set('plugins', updatedPlugins); logger.success(`Plugin "${name}" removed successfully`); } async function listPlugins(options) { const installedPlugins = configManager.get('plugins', []); if (installedPlugins.length === 0) { logger.info('No plugins installed'); logger.info(`Install plugins with: ${chalk.cyan('accs plugin add <plugin-name>')}`); return; } logger.section(`Installed Plugins (${installedPlugins.length})`); for (const pluginName of installedPlugins) { await displayPluginInfo(pluginName, options); } logger.separator(); logger.info('Plugin commands:'); logger.info(` ${chalk.cyan('accs plugin add <name>')} - Install a plugin`); logger.info(` ${chalk.cyan('accs plugin remove <name>')} - Remove a plugin`); logger.info(` ${chalk.cyan('accs plugin create [name]')} - Create a new plugin`); } async function displayPluginInfo(pluginName, options) { try { // Try to load plugin info const isLocalPlugin = pluginName.endsWith('.js') || FileUtils.exists(path.resolve(pluginName)); let plugin = null; if (isLocalPlugin) { plugin = await import(path.resolve(pluginName)); } else { // Try to require npm plugin const npmPluginName = pluginName.startsWith('accs-plugin-') || pluginName.startsWith('@') ? pluginName : `accs-plugin-${pluginName}`; try { plugin = await import(npmPluginName); } catch (error) { logger.warn(`Could not load plugin info for ${pluginName}`); console.log(` ${chalk.yellow('○')} ${pluginName} ${chalk.gray('(info unavailable)')}`); return; } } const pluginInfo = plugin.default || plugin; const name = pluginInfo.name || pluginName; const version = pluginInfo.version || '1.0.0'; const description = pluginInfo.description || 'No description available'; console.log(` ${chalk.green('●')} ${chalk.bold(name)} ${chalk.gray(`v${version}`)}`); console.log(` ${description}`); if (options.verbose && pluginInfo.commands) { console.log(` Commands: ${Object.keys(pluginInfo.commands).join(', ')}`); } } catch (error) { console.log(` ${chalk.red('○')} ${pluginName} ${chalk.gray('(error loading)')}`); if (options.verbose) { console.log(` ${chalk.red(error.message)}`); } } } async function createPlugin(name, options) { let pluginName = name; if (!pluginName) { const { name: inputName } = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Plugin name:', default: 'my-plugin', validate: (input) => { if (!input.trim()) return 'Plugin name is required'; if (!/^[a-z0-9-]+$/.test(input)) { return 'Plugin name can only contain lowercase letters, numbers, and hyphens'; } return true; } } ]); pluginName = inputName; } const { description, author, commands } = await inquirer.prompt([ { type: 'input', name: 'description', message: 'Plugin description:', default: `A custom ACCS plugin` }, { type: 'input', name: 'author', message: 'Author:', default: 'Anonymous' }, { type: 'input', name: 'commands', message: 'Command names (comma separated):', default: 'hello', filter: (input) => input.split(',').map(cmd => cmd.trim()).filter(Boolean) } ]); const pluginDir = path.join(process.cwd(), `accs-plugin-${pluginName}`); if (FileUtils.exists(pluginDir)) { const { overwrite } = await inquirer.prompt([ { type: 'confirm', name: 'overwrite', message: `Directory "${pluginName}" already exists. Overwrite?`, default: false } ]); if (!overwrite) { logger.info('Plugin creation cancelled'); return; } await FileUtils.remove(pluginDir); } await createPluginStructure(pluginDir, pluginName, { description, author, commands }, options); logger.success(`Plugin "${pluginName}" created successfully!`); logger.info(`Directory: ${chalk.cyan(pluginDir)}`); logger.info(`Next steps:`); logger.info(` cd accs-plugin-${pluginName}`); logger.info(` npm install`); logger.info(` accs plugin add ./index.js`); } async function createPluginStructure(pluginDir, name, info) { // Create directory await FileUtils.createDir(pluginDir); // Package.json const packageJson = { name: `accs-plugin-${name}`, version: '1.0.0', description: info.description, main: 'index.js', keywords: ['accs', 'accs-plugin', 'cli'], author: info.author, license: 'MIT', peerDependencies: { 'accs-cli': '^1.0.0' } }; await FileUtils.writeJson(path.join(pluginDir, 'package.json'), packageJson); // Main plugin file const mainContent = generatePluginCode(name, info); await import('fs').then(fs => fs.promises.writeFile(path.join(pluginDir, 'index.js'), mainContent, 'utf8') ); // README const readmeContent = generatePluginReadme(name, info); await import('fs').then(fs => fs.promises.writeFile(path.join(pluginDir, 'README.md'), readmeContent, 'utf8') ); } function generatePluginCode(name, info) { const commandsCode = info.commands.map(cmd => ` ${cmd}: { description: 'Example ${cmd} command', action: async (args, options) => { console.log('Hello from ${cmd} command!'); console.log('Args:', args); console.log('Options:', options); } }`).join(','); return `/** * ACCS Plugin: ${name} * ${info.description} */ export default { name: '${name}', version: '1.0.0', description: '${info.description}', author: '${info.author}', // Plugin commands commands: {${commandsCode} }, // Plugin lifecycle hooks hooks: { beforeInit: async (context) => { console.log('Plugin ${name} loaded!'); }, afterBuild: async (context) => { // Called after build completes } }, // Plugin configuration config: { enabled: true, options: { // Plugin-specific options } } }; `; } function generatePluginReadme(name, info) { return `# ACCS Plugin: ${name} ${info.description} ## Installation \`\`\`bash npm install accs-plugin-${name} accs plugin add accs-plugin-${name} \`\`\` ## Commands ${info.commands.map(cmd => `- \`accs ${cmd}\` - Example ${cmd} command`).join('\n')} ## Development \`\`\`bash # Link for local development accs plugin add ./index.js # Remove plugin accs plugin remove ${name} \`\`\` ## Author ${info.author} `; } async function showPluginInfo(name) { logger.info(`Plugin information: ${chalk.cyan(name)}`); await displayPluginInfo(name, { verbose: true }); } function isValidPluginName(name) { // Allow local files if (name.endsWith('.js') || name.includes('/') || name.includes('\\')) { return true; } // Allow npm packages if (name.startsWith('@')) { return /^@[a-z0-9-~][a-z0-9-._~]*\/[a-z0-9-~][a-z0-9-._~]*$/.test(name); } return /^[a-z0-9-]+$/.test(name); }