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
JavaScript
/**
* 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);
}