feat: add sync, update, and remove commands

- sync: scans plugins and updates registry.json
- update: checks and updates plugins to match registry
- remove: removes installed plugin with --yes and --remove-deps options
- registry.json stored at project root with plugin metadata and _sourcePath
This commit is contained in:
synt-xerror
2026-04-09 01:19:33 -03:00
parent b0d0bbfa5b
commit 649c7bb05a
4 changed files with 355 additions and 0 deletions

View File

@@ -6,6 +6,9 @@ import { initCommand } from '../src/commands/init.js';
import { installCommand } from '../src/commands/install.js'; import { installCommand } from '../src/commands/install.js';
import { listCommand } from '../src/commands/list.js'; import { listCommand } from '../src/commands/list.js';
import { validateCommand } from '../src/commands/validate.js'; import { validateCommand } from '../src/commands/validate.js';
import { syncCommand } from '../src/commands/sync.js';
import { updateCommand } from '../src/commands/update.js';
import { removeCommand } from '../src/commands/remove.js';
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const pkg = require('../package.json'); const pkg = require('../package.json');
@@ -28,6 +31,14 @@ program
.option('-g, --global', 'Install to global registry') .option('-g, --global', 'Install to global registry')
.action(installCommand); .action(installCommand);
program
.command('remove <plugin>')
.alias('rm')
.description('Remove an installed plugin')
.option('-y, --yes', 'Skip confirmation prompt')
.option('--remove-deps', 'Also remove npm dependencies')
.action(removeCommand);
program program
.command('list') .command('list')
.alias('ls') .alias('ls')
@@ -35,6 +46,17 @@ program
.option('-a, --all', 'Include disabled plugins') .option('-a, --all', 'Include disabled plugins')
.action(listCommand); .action(listCommand);
program
.command('sync')
.description('Sync registry.json with installed plugins')
.action(syncCommand);
program
.command('update [plugin]')
.description('Update plugins to match registry versions')
.option('-a, --all', 'Update all plugins (default if no name given)')
.action(updateCommand);
program program
.command('validate [path]') .command('validate [path]')
.description('Validate manyplug.json syntax') .description('Validate manyplug.json syntax')

100
src/commands/remove.js Normal file
View File

@@ -0,0 +1,100 @@
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import { execSync } from 'child_process';
const PLUGINS_DIR = path.resolve('src/plugins');
const REGISTRY_PATH = path.resolve('registry.json');
/**
* Remove an installed plugin
* @param {string} pluginName - plugin name to remove
* @param {object} options - command options
*/
export async function removeCommand(pluginName, options) {
if (!pluginName) {
console.error(chalk.red('❌ Please specify a plugin name'));
console.log(chalk.gray(' Usage: manyplug remove <plugin-name>'));
process.exit(1);
}
const pluginDir = path.join(PLUGINS_DIR, pluginName);
const manifestPath = path.join(pluginDir, 'manyplug.json');
// Check if plugin exists
if (!await fs.pathExists(manifestPath)) {
console.error(chalk.red(`❌ Plugin "${pluginName}" is not installed`));
process.exit(1);
}
// Read manifest to get info before removing
let manifest;
try {
manifest = await fs.readJson(manifestPath);
} catch (err) {
manifest = { name: pluginName, version: '?' };
}
// Confirm removal unless --yes flag
if (!options.yes) {
console.log(chalk.yellow(`⚠️ You are about to remove:`));
console.log(chalk.white(` ${manifest.name} v${manifest.version}`));
if (manifest.description) {
console.log(chalk.gray(` ${manifest.description}`));
}
console.log(chalk.yellow('\nThis action cannot be undone.\n'));
console.log(chalk.gray('Use --yes to skip this confirmation'));
process.exit(1);
}
console.log(chalk.blue(`Removing "${pluginName}"...`));
// Track dependencies to possibly remove
const deps = manifest.dependencies || {};
const hasDeps = Object.keys(deps).length > 0;
try {
// Remove plugin directory
await fs.remove(pluginDir);
console.log(chalk.green(`✅ Removed "${pluginName}"`));
// Update registry if exists
if (await fs.pathExists(REGISTRY_PATH)) {
try {
const registry = await fs.readJson(REGISTRY_PATH);
if (registry.plugins && registry.plugins[pluginName]) {
delete registry.plugins[pluginName];
registry.lastUpdated = new Date().toISOString();
await fs.writeJson(REGISTRY_PATH, registry, { spaces: 2 });
console.log(chalk.gray(' Updated registry'));
}
} catch (err) {
// Ignore registry update errors
}
}
// Warning about orphaned dependencies
if (hasDeps && !options.removeDeps) {
console.log(chalk.yellow('\n⚠ Plugin had npm dependencies:'));
for (const [dep, version] of Object.entries(deps)) {
console.log(chalk.gray(` - ${dep}@${version}`));
}
console.log(chalk.gray('Run with --remove-deps to also uninstall these'));
}
// Remove npm dependencies if requested
if (hasDeps && options.removeDeps) {
console.log(chalk.blue('\nRemoving npm dependencies...'));
const depList = Object.keys(deps).join(' ');
try {
execSync(`npm uninstall ${depList}`, { cwd: process.cwd(), stdio: 'pipe' });
console.log(chalk.green('✅ Dependencies removed'));
} catch (err) {
console.warn(chalk.yellow(`⚠️ Could not remove dependencies: ${err.message}`));
}
}
} catch (err) {
console.error(chalk.red(`❌ Failed to remove plugin: ${err.message}`));
process.exit(1);
}
}

75
src/commands/sync.js Normal file
View File

@@ -0,0 +1,75 @@
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
const PLUGINS_DIR = path.resolve('src/plugins');
const REGISTRY_PATH = path.resolve('registry.json');
/**
* Sync registry.json with installed plugins.
* Scans all plugins in src/plugins/ and updates registry.json
*/
export async function syncCommand() {
console.log(chalk.blue('Syncing registry...'));
// Load existing registry or create new one
let registry = { lastUpdated: new Date().toISOString(), plugins: {} };
if (await fs.pathExists(REGISTRY_PATH)) {
try {
registry = await fs.readJson(REGISTRY_PATH);
} catch (err) {
console.warn(chalk.yellow('⚠️ Could not read existing registry, creating new one'));
}
}
// Scan plugins directory
if (!await fs.pathExists(PLUGINS_DIR)) {
console.warn(chalk.yellow('⚠️ No plugins directory found'));
await fs.writeJson(REGISTRY_PATH, registry, { spaces: 2 });
console.log(chalk.green(`✅ Registry updated at ${path.relative(process.cwd(), REGISTRY_PATH)}`));
return;
}
const entries = await fs.readdir(PLUGINS_DIR, { withFileTypes: true });
let updated = 0;
let added = 0;
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const manifestPath = path.join(PLUGINS_DIR, entry.name, 'manyplug.json');
if (!await fs.pathExists(manifestPath)) continue;
try {
const manifest = await fs.readJson(manifestPath);
const pluginName = manifest.name || entry.name;
// Check if plugin exists in registry and version changed
const existing = registry.plugins[pluginName];
if (!existing) {
added++;
} else if (existing.version !== manifest.version) {
updated++;
}
registry.plugins[pluginName] = {
...manifest,
_syncedAt: new Date().toISOString(),
_sourcePath: path.join('src/plugins', entry.name)
};
} catch (err) {
console.warn(chalk.yellow(`⚠️ Failed to read ${entry.name}: ${err.message}`));
}
}
// Update timestamp
registry.lastUpdated = new Date().toISOString();
await fs.writeJson(REGISTRY_PATH, registry, { spaces: 2 });
console.log(chalk.green(`✅ Registry synced`));
console.log(chalk.gray(` Added: ${added}`));
console.log(chalk.gray(` Updated: ${updated}`));
console.log(chalk.gray(` Total plugins: ${Object.keys(registry.plugins).length}`));
}

158
src/commands/update.js Normal file
View File

@@ -0,0 +1,158 @@
import fs from 'fs-extra';
import path from 'path';
import chalk from 'chalk';
import { execSync } from 'child_process';
const PLUGINS_DIR = path.resolve('src/plugins');
const REGISTRY_PATH = path.resolve('registry.json');
/**
* Check and update plugins to match registry versions
* @param {string} pluginName - specific plugin to update, or undefined for all
* @param {object} options - command options
*/
export async function updateCommand(pluginName, options) {
// Check if registry exists
if (!await fs.pathExists(REGISTRY_PATH)) {
console.error(chalk.red('❌ registry.json not found. Run "manyplug sync" first.'));
process.exit(1);
}
const registry = await fs.readJson(REGISTRY_PATH);
if (pluginName) {
// Update specific plugin
await updatePlugin(pluginName, registry.plugins[pluginName]);
} else {
// Update all plugins
const plugins = Object.keys(registry.plugins);
if (plugins.length === 0) {
console.log(chalk.gray('No plugins in registry'));
return;
}
console.log(chalk.blue(`Checking ${plugins.length} plugin(s)...\n`));
let updated = 0;
let upToDate = 0;
let notInstalled = 0;
let errors = 0;
for (const name of plugins) {
const result = await updatePlugin(name, registry.plugins[name], { silent: true });
if (result === 'updated') updated++;
else if (result === 'upToDate') upToDate++;
else if (result === 'notInstalled') notInstalled++;
else errors++;
}
console.log(chalk.bold('\nUpdate Summary:'));
console.log(chalk.green(` Updated: ${updated}`));
console.log(chalk.gray(` Up to date: ${upToDate}`));
if (notInstalled > 0) console.log(chalk.yellow(` Not installed: ${notInstalled}`));
if (errors > 0) console.log(chalk.red(` Errors: ${errors}`));
}
}
async function updatePlugin(name, registryEntry, { silent = false } = {}) {
if (!registryEntry) {
if (!silent) console.error(chalk.red(`❌ "${name}" not found in registry`));
return 'error';
}
const pluginDir = path.join(PLUGINS_DIR, name);
const manifestPath = path.join(pluginDir, 'manyplug.json');
// Check if plugin is installed
if (!await fs.pathExists(manifestPath)) {
// Plugin exists in registry but not installed - try to install it
if (registryEntry._sourcePath) {
if (!silent) console.log(chalk.blue(`Installing "${name}" from registry...`));
await installFromSource(name, registryEntry);
return 'updated';
}
if (!silent) console.log(chalk.yellow(`⚠️ "${name}" not installed (run "manyplug install ${name}")`));
return 'notInstalled';
}
// Read installed manifest
const installed = await fs.readJson(manifestPath);
// Compare versions
if (installed.version === registryEntry.version) {
if (!silent) console.log(chalk.gray(` ${name}: ${installed.version} (up to date)`));
return 'upToDate';
}
// Version differs - update
if (!silent) console.log(chalk.blue(`Updating "${name}"...`));
console.log(chalk.gray(` ${installed.version}${registryEntry.version}`));
// If we have a source path in registry, re-copy from there
if (registryEntry._sourcePath && await fs.pathExists(registryEntry._sourcePath)) {
// Backup current
const backupDir = `${pluginDir}.backup-${Date.now()}`;
await fs.copy(pluginDir, backupDir);
try {
// Remove old version
await fs.remove(pluginDir);
// Copy new version
await fs.copy(registryEntry._sourcePath, pluginDir);
// Install dependencies
if (registryEntry.dependencies && Object.keys(registryEntry.dependencies).length > 0) {
await installNpmDeps(registryEntry.dependencies);
}
if (!silent) console.log(chalk.green(` ✅ Updated to ${registryEntry.version}`));
return 'updated';
} catch (err) {
// Restore backup on error
console.error(chalk.red(` ❌ Update failed: ${err.message}`));
console.log(chalk.yellow(' Restoring backup...'));
await fs.copy(backupDir, pluginDir);
await fs.remove(backupDir);
return 'error';
}
}
// No source path - just update the version in manifest (metadata update)
await fs.writeJson(manifestPath, { ...installed, version: registryEntry.version }, { spaces: 2 });
if (!silent) console.log(chalk.yellow(` ⚠️ Updated version only (no source available)`));
return 'updated';
}
async function installFromSource(name, registryEntry) {
const sourcePath = registryEntry._sourcePath;
const targetDir = path.join(PLUGINS_DIR, name);
if (!await fs.pathExists(sourcePath)) {
console.error(chalk.red(` ❌ Source not found: ${sourcePath}`));
return;
}
await fs.ensureDir(PLUGINS_DIR);
await fs.copy(sourcePath, targetDir);
if (registryEntry.dependencies && Object.keys(registryEntry.dependencies).length > 0) {
await installNpmDeps(registryEntry.dependencies);
}
console.log(chalk.green(` ✅ Installed "${name}" v${registryEntry.version}`));
}
async function installNpmDeps(dependencies) {
const deps = Object.entries(dependencies)
.map(([name, version]) => `${name}@${version === '*' ? 'latest' : version}`)
.join(' ');
if (!deps) return;
try {
execSync(`npm install ${deps}`, { cwd: process.cwd(), stdio: 'pipe' });
} catch (err) {
console.warn(chalk.yellow(` ⚠️ Failed to install dependencies: ${err.message}`));
}
}