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:
@@ -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
100
src/commands/remove.js
Normal 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
75
src/commands/sync.js
Normal 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
158
src/commands/update.js
Normal 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}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user