931 lines
25 KiB
JavaScript
931 lines
25 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Claude Code Marketplace Manager
|
|
* Comprehensive tool for creating, managing, and maintaining Claude Code Marketplaces
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { spawn } = require('child_process');
|
|
|
|
class MarketplaceManager {
|
|
constructor(options = {}) {
|
|
this.verbose = options.verbose || false;
|
|
this.dryRun = options.dryRun || false;
|
|
this.force = options.force || false;
|
|
}
|
|
|
|
log(message, level = 'info') {
|
|
if (this.verbose || level === 'error') {
|
|
const timestamp = new Date().toISOString();
|
|
console.log(`[${timestamp}] [${level.toUpperCase()}] ${message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute command safely using spawn
|
|
*/
|
|
async executeCommand(command, args = [], cwd = process.cwd()) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
cwd,
|
|
stdio: this.verbose ? 'inherit' : 'pipe',
|
|
});
|
|
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
if (!this.verbose) {
|
|
child.stdout?.on('data', data => {
|
|
stdout += data.toString();
|
|
});
|
|
|
|
child.stderr?.on('data', data => {
|
|
stderr += data.toString();
|
|
});
|
|
}
|
|
|
|
child.on('close', code => {
|
|
if (code === 0) {
|
|
resolve({ stdout, stderr, code });
|
|
} else {
|
|
reject(new Error(`Command failed with code ${code}: ${stderr}`));
|
|
}
|
|
});
|
|
|
|
child.on('error', error => {
|
|
reject(error);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create a new marketplace from template
|
|
*/
|
|
async createMarketplace(name, options = {}) {
|
|
const template = options.template || 'standard';
|
|
const targetPath = options.path || `./${name}`;
|
|
|
|
this.log(`Creating marketplace '${name}' at ${targetPath} using ${template} template`);
|
|
|
|
if (!this.dryRun) {
|
|
// Create directory structure
|
|
this.createDirectoryStructure(targetPath);
|
|
|
|
// Generate configuration files
|
|
await this.generateMarketplaceConfig(targetPath, name, template);
|
|
|
|
// Copy template files
|
|
await this.copyTemplateFiles(targetPath, template);
|
|
|
|
// Initialize git repository
|
|
await this.initializeGitRepository(targetPath);
|
|
|
|
// Validate created marketplace
|
|
if (options.autoValidate !== false) {
|
|
await this.validateMarketplace(targetPath);
|
|
}
|
|
}
|
|
|
|
this.log(`Marketplace '${name}' created successfully`);
|
|
return { success: true, path: targetPath, template };
|
|
}
|
|
|
|
/**
|
|
* Create marketplace directory structure
|
|
*/
|
|
createDirectoryStructure(basePath) {
|
|
const directories = [
|
|
'.claude-plugin',
|
|
'plugins',
|
|
'skills',
|
|
'docs',
|
|
'tests',
|
|
'scripts',
|
|
'examples',
|
|
];
|
|
|
|
directories.forEach(dir => {
|
|
const fullPath = path.join(basePath, dir);
|
|
this.log(`Creating directory: ${fullPath}`);
|
|
if (!this.dryRun) {
|
|
fs.mkdirSync(fullPath, { recursive: true });
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Generate marketplace configuration
|
|
*/
|
|
async generateMarketplaceConfig(basePath, name, template) {
|
|
const configPath = path.join(basePath, '.claude-plugin', 'marketplace.json');
|
|
|
|
const config = {
|
|
name: name,
|
|
version: '1.0.0',
|
|
description: `${name} - Claude Code Marketplace`,
|
|
owner: {
|
|
name: 'Marketplace Owner',
|
|
email: 'owner@example.com',
|
|
},
|
|
repository: {
|
|
type: 'git',
|
|
url: `https://github.com/owner/${name}.git`,
|
|
},
|
|
license: 'MIT',
|
|
plugins: [],
|
|
skills: [],
|
|
template: template,
|
|
created: new Date().toISOString(),
|
|
validation: this.getValidationConfig(template),
|
|
};
|
|
|
|
this.log(`Generating marketplace configuration: ${configPath}`);
|
|
if (!this.dryRun) {
|
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get validation configuration based on template type
|
|
*/
|
|
getValidationConfig(template) {
|
|
const configs = {
|
|
standard: {
|
|
level: 'standard',
|
|
strict: false,
|
|
checks: ['structure', 'metadata', 'plugins'],
|
|
},
|
|
enterprise: {
|
|
level: 'enterprise',
|
|
strict: true,
|
|
checks: ['structure', 'metadata', 'plugins', 'security', 'compliance'],
|
|
},
|
|
community: {
|
|
level: 'community',
|
|
strict: false,
|
|
checks: ['structure', 'metadata', 'plugins', 'documentation'],
|
|
},
|
|
minimal: {
|
|
level: 'minimal',
|
|
strict: false,
|
|
checks: ['structure', 'metadata'],
|
|
},
|
|
};
|
|
|
|
return configs[template] || configs.standard;
|
|
}
|
|
|
|
/**
|
|
* Copy template files
|
|
*/
|
|
async copyTemplateFiles(targetPath, template) {
|
|
const templateDir = path.join(__dirname, '..', 'templates', template);
|
|
|
|
if (!fs.existsSync(templateDir)) {
|
|
this.log(`Template directory not found: ${templateDir}`, 'warn');
|
|
return;
|
|
}
|
|
|
|
const files = this.getTemplateFiles(template);
|
|
|
|
for (const file of files) {
|
|
const sourcePath = path.join(templateDir, file);
|
|
const targetFilePath = path.join(targetPath, file);
|
|
|
|
this.log(`Copying template file: ${file}`);
|
|
if (!this.dryRun && fs.existsSync(sourcePath)) {
|
|
// Ensure target directory exists
|
|
const targetDir = path.dirname(targetFilePath);
|
|
if (!fs.existsSync(targetDir)) {
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
}
|
|
|
|
const content = fs.readFileSync(sourcePath, 'utf8');
|
|
fs.writeFileSync(targetFilePath, content);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get template file list
|
|
*/
|
|
getTemplateFiles(template) {
|
|
const files = {
|
|
standard: [
|
|
'README.md',
|
|
'.claude-plugin/plugin.json',
|
|
'docs/GUIDE.md',
|
|
'scripts/validate.js',
|
|
'scripts/deploy.js',
|
|
],
|
|
enterprise: [
|
|
'README.md',
|
|
'.claude-plugin/plugin.json',
|
|
'docs/ENTERPRISE.md',
|
|
'docs/SECURITY.md',
|
|
'scripts/validate.js',
|
|
'scripts/deploy.js',
|
|
'scripts/security-scan.js',
|
|
'tests/compliance.test.js',
|
|
],
|
|
community: [
|
|
'README.md',
|
|
'.claude-plugin/plugin.json',
|
|
'docs/CONTRIBUTING.md',
|
|
'docs/COMMUNITY.md',
|
|
'scripts/validate.js',
|
|
'scripts/deploy.js',
|
|
],
|
|
minimal: ['README.md', '.claude-plugin/plugin.json', 'scripts/validate.js'],
|
|
};
|
|
|
|
return files[template] || files.standard;
|
|
}
|
|
|
|
/**
|
|
* Initialize git repository
|
|
*/
|
|
async initializeGitRepository(basePath) {
|
|
this.log(`Initializing git repository in: ${basePath}`);
|
|
|
|
if (!this.dryRun) {
|
|
try {
|
|
await this.executeCommand('git', ['init'], basePath);
|
|
await this.executeCommand('git', ['add', '.'], basePath);
|
|
await this.executeCommand(
|
|
'git',
|
|
['commit', '-m', 'Initial marketplace structure'],
|
|
basePath
|
|
);
|
|
} catch (error) {
|
|
this.log(`Git initialization failed: ${error.message}`, 'warn');
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate marketplace structure and configuration
|
|
*/
|
|
async validateMarketplace(marketplacePath) {
|
|
this.log(`Validating marketplace at: ${marketplacePath}`);
|
|
|
|
const results = {
|
|
success: true,
|
|
errors: [],
|
|
warnings: [],
|
|
info: [],
|
|
};
|
|
|
|
// Check if marketplace exists
|
|
if (!fs.existsSync(marketplacePath)) {
|
|
results.errors.push('Marketplace directory does not exist');
|
|
results.success = false;
|
|
return results;
|
|
}
|
|
|
|
// Validate directory structure
|
|
this.validateDirectoryStructure(marketplacePath, results);
|
|
|
|
// Validate configuration files
|
|
await this.validateConfiguration(marketplacePath, results);
|
|
|
|
// Validate plugins
|
|
await this.validatePlugins(marketplacePath, results);
|
|
|
|
// Validate skills
|
|
await this.validateSkills(marketplacePath, results);
|
|
|
|
// Report results
|
|
this.reportValidationResults(results);
|
|
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Validate marketplace directory structure
|
|
*/
|
|
validateDirectoryStructure(marketplacePath, results) {
|
|
const requiredDirs = ['.claude-plugin'];
|
|
const optionalDirs = ['plugins', 'skills', 'docs', 'tests', 'scripts', 'examples'];
|
|
|
|
requiredDirs.forEach(dir => {
|
|
const dirPath = path.join(marketplacePath, dir);
|
|
if (!fs.existsSync(dirPath)) {
|
|
results.errors.push(`Required directory missing: ${dir}`);
|
|
} else {
|
|
results.info.push(`Required directory found: ${dir}`);
|
|
}
|
|
});
|
|
|
|
optionalDirs.forEach(dir => {
|
|
const dirPath = path.join(marketplacePath, dir);
|
|
if (fs.existsSync(dirPath)) {
|
|
results.info.push(`Optional directory found: ${dir}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Validate marketplace configuration
|
|
*/
|
|
async validateConfiguration(marketplacePath, results) {
|
|
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
|
|
|
|
if (!fs.existsSync(configPath)) {
|
|
results.errors.push('Marketplace configuration file missing: marketplace.json');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
|
|
// Validate required fields
|
|
const requiredFields = ['name', 'version', 'description', 'owner'];
|
|
requiredFields.forEach(field => {
|
|
if (!config[field]) {
|
|
results.errors.push(`Required configuration field missing: ${field}`);
|
|
}
|
|
});
|
|
|
|
// Validate version format
|
|
if (config.version && !this.isValidVersion(config.version)) {
|
|
results.errors.push(`Invalid version format: ${config.version}`);
|
|
}
|
|
|
|
// Validate plugins array
|
|
if (config.plugins && !Array.isArray(config.plugins)) {
|
|
results.errors.push('Plugins field must be an array');
|
|
}
|
|
|
|
results.info.push('Configuration file validated successfully');
|
|
} catch (error) {
|
|
results.errors.push(`Invalid JSON in configuration file: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate plugins in marketplace
|
|
*/
|
|
async validatePlugins(marketplacePath, results) {
|
|
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
|
|
|
|
if (!fs.existsSync(configPath)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
|
|
if (!config.plugins || config.plugins.length === 0) {
|
|
results.warnings.push('No plugins configured in marketplace');
|
|
return;
|
|
}
|
|
|
|
const pluginsDir = path.join(marketplacePath, 'plugins');
|
|
|
|
for (const plugin of config.plugins) {
|
|
if (!plugin.name) {
|
|
results.errors.push('Plugin found without name in configuration');
|
|
continue;
|
|
}
|
|
|
|
const pluginPath = path.join(pluginsDir, plugin.name);
|
|
if (fs.existsSync(pluginPath)) {
|
|
await this.validatePlugin(pluginPath, plugin, results);
|
|
} else {
|
|
results.warnings.push(`Plugin directory not found: ${plugin.name}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
results.errors.push(`Error validating plugins: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate individual plugin
|
|
*/
|
|
async validatePlugin(pluginPath, pluginConfig, results) {
|
|
const pluginJsonPath = path.join(pluginPath, '.claude-plugin', 'plugin.json');
|
|
|
|
if (!fs.existsSync(pluginJsonPath)) {
|
|
results.errors.push(`Plugin configuration missing: ${pluginConfig.name}/plugin.json`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const pluginConfig = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
|
|
|
|
// Validate plugin structure
|
|
const requiredFields = ['name', 'version', 'description'];
|
|
requiredFields.forEach(field => {
|
|
if (!pluginConfig[field]) {
|
|
results.errors.push(`Plugin ${pluginConfig.name}: Required field missing: ${field}`);
|
|
}
|
|
});
|
|
|
|
results.info.push(`Plugin validated: ${pluginConfig.name}`);
|
|
} catch (error) {
|
|
results.errors.push(`Plugin ${pluginConfig.name}: Invalid configuration - ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate skills in marketplace
|
|
*/
|
|
async validateSkills(marketplacePath, results) {
|
|
const skillsDir = path.join(marketplacePath, 'skills');
|
|
|
|
if (!fs.existsSync(skillsDir)) {
|
|
results.warnings.push('Skills directory not found');
|
|
return;
|
|
}
|
|
|
|
const skills = fs
|
|
.readdirSync(skillsDir, { withFileTypes: true })
|
|
.filter(dirent => dirent.isDirectory())
|
|
.map(dirent => dirent.name);
|
|
|
|
if (skills.length === 0) {
|
|
results.warnings.push('No skills found in marketplace');
|
|
return;
|
|
}
|
|
|
|
for (const skill of skills) {
|
|
await this.validateSkill(path.join(skillsDir, skill), skill, results);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate individual skill
|
|
*/
|
|
async validateSkill(skillPath, skillName, results) {
|
|
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
|
|
if (!fs.existsSync(skillMdPath)) {
|
|
results.errors.push(`Skill SKILL.md missing: ${skillName}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
|
|
// Check for required frontmatter
|
|
if (!content.includes('---')) {
|
|
results.errors.push(`Skill ${skillName}: Missing frontmatter`);
|
|
return;
|
|
}
|
|
|
|
// Extract and validate frontmatter
|
|
const frontmatterMatch = content.match(/^---\n(.*?)\n---/s);
|
|
if (frontmatterMatch) {
|
|
try {
|
|
const frontmatter = JSON.parse(frontmatterMatch[1].replace(/(\w+):/g, '"$1":'));
|
|
|
|
if (!frontmatter.name) {
|
|
results.errors.push(`Skill ${skillName}: Missing name in frontmatter`);
|
|
}
|
|
|
|
if (!frontmatter.description) {
|
|
results.warnings.push(`Skill ${skillName}: Missing description in frontmatter`);
|
|
}
|
|
} catch (error) {
|
|
results.errors.push(`Skill ${skillName}: Invalid frontmatter format`);
|
|
}
|
|
}
|
|
|
|
results.info.push(`Skill validated: ${skillName}`);
|
|
} catch (error) {
|
|
results.errors.push(`Skill ${skillName}: Error reading file - ${error.message}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deploy marketplace plugins
|
|
*/
|
|
async deployMarketplace(marketplacePath, options = {}) {
|
|
this.log(`Deploying marketplace from: ${marketplacePath}`);
|
|
|
|
const validation = await this.validateMarketplace(marketplacePath);
|
|
|
|
if (!validation.success && !this.force) {
|
|
throw new Error('Marketplace validation failed. Use --force to deploy anyway.');
|
|
}
|
|
|
|
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
|
|
const results = {
|
|
deployed: [],
|
|
failed: [],
|
|
skipped: [],
|
|
};
|
|
|
|
for (const plugin of config.plugins) {
|
|
try {
|
|
await this.deployPlugin(plugin, marketplacePath, options);
|
|
results.deployed.push(plugin.name);
|
|
this.log(`Deployed plugin: ${plugin.name}`);
|
|
} catch (error) {
|
|
results.failed.push({ name: plugin.name, error: error.message });
|
|
this.log(`Failed to deploy plugin ${plugin.name}: ${error.message}`, 'error');
|
|
}
|
|
}
|
|
|
|
this.reportDeploymentResults(results);
|
|
return results;
|
|
}
|
|
|
|
/**
|
|
* Deploy individual plugin
|
|
*/
|
|
async deployPlugin(plugin, marketplacePath, options) {
|
|
// Implementation would depend on deployment target
|
|
// This is a placeholder for actual deployment logic
|
|
this.log(`Deploying plugin: ${plugin.name} (version: ${plugin.version})`);
|
|
|
|
if (!this.dryRun) {
|
|
// Add actual deployment logic here
|
|
// Could involve:
|
|
// - Publishing to GitHub
|
|
// - Uploading to registry
|
|
// - Deploying to server
|
|
// etc.
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze marketplace health
|
|
*/
|
|
async analyzeMarketplaceHealth(marketplacePath) {
|
|
this.log(`Analyzing marketplace health: ${marketplacePath}`);
|
|
|
|
const analysis = {
|
|
overall_score: 0,
|
|
structure_score: 0,
|
|
configuration_score: 0,
|
|
plugin_score: 0,
|
|
documentation_score: 0,
|
|
recommendations: [],
|
|
metrics: {},
|
|
};
|
|
|
|
// Structure analysis
|
|
analysis.structure_score = this.analyzeStructure(marketplacePath);
|
|
|
|
// Configuration analysis
|
|
analysis.configuration_score = await this.analyzeConfiguration(marketplacePath);
|
|
|
|
// Plugin analysis
|
|
analysis.plugin_score = await this.analyzePlugins(marketplacePath);
|
|
|
|
// Documentation analysis
|
|
analysis.documentation_score = await this.analyzeDocumentation(marketplacePath);
|
|
|
|
// Calculate overall score
|
|
analysis.overall_score = Math.round(
|
|
(analysis.structure_score +
|
|
analysis.configuration_score +
|
|
analysis.plugin_score +
|
|
analysis.documentation_score) /
|
|
4
|
|
);
|
|
|
|
// Generate recommendations
|
|
analysis.recommendations = this.generateRecommendations(analysis);
|
|
|
|
// Generate metrics
|
|
analysis.metrics = await this.generateMetrics(marketplacePath);
|
|
|
|
this.reportHealthAnalysis(analysis);
|
|
return analysis;
|
|
}
|
|
|
|
/**
|
|
* Analyze marketplace structure
|
|
*/
|
|
analyzeStructure(marketplacePath) {
|
|
let score = 0;
|
|
const maxScore = 100;
|
|
|
|
const requiredDirs = ['.claude-plugin'];
|
|
const optionalDirs = ['plugins', 'skills', 'docs', 'tests', 'scripts'];
|
|
|
|
// Check required directories (40 points)
|
|
requiredDirs.forEach(dir => {
|
|
if (fs.existsSync(path.join(marketplacePath, dir))) {
|
|
score += 40 / requiredDirs.length;
|
|
}
|
|
});
|
|
|
|
// Check optional directories (60 points)
|
|
optionalDirs.forEach(dir => {
|
|
if (fs.existsSync(path.join(marketplacePath, dir))) {
|
|
score += 60 / optionalDirs.length;
|
|
}
|
|
});
|
|
|
|
return Math.round(score);
|
|
}
|
|
|
|
/**
|
|
* Analyze configuration
|
|
*/
|
|
async analyzeConfiguration(marketplacePath) {
|
|
let score = 0;
|
|
|
|
try {
|
|
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
|
|
// Check required fields (50 points)
|
|
const requiredFields = ['name', 'version', 'description', 'owner', 'license'];
|
|
requiredFields.forEach(field => {
|
|
if (config[field]) score += 50 / requiredFields.length;
|
|
});
|
|
|
|
// Check optional but recommended fields (30 points)
|
|
const recommendedFields = ['repository', 'keywords', 'homepage'];
|
|
recommendedFields.forEach(field => {
|
|
if (config[field]) score += 30 / recommendedFields.length;
|
|
});
|
|
|
|
// Check plugins array (20 points)
|
|
if (config.plugins && Array.isArray(config.plugins) && config.plugins.length > 0) {
|
|
score += 20;
|
|
}
|
|
} catch (error) {
|
|
score = 0;
|
|
}
|
|
|
|
return Math.round(score);
|
|
}
|
|
|
|
/**
|
|
* Analyze plugins
|
|
*/
|
|
async analyzePlugins(marketplacePath) {
|
|
let score = 0;
|
|
|
|
try {
|
|
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
|
|
if (!config.plugins || config.plugins.length === 0) {
|
|
return 0;
|
|
}
|
|
|
|
const pluginsDir = path.join(marketplacePath, 'plugins');
|
|
let validPlugins = 0;
|
|
|
|
for (const plugin of config.plugins) {
|
|
const pluginPath = path.join(pluginsDir, plugin.name);
|
|
if (fs.existsSync(pluginPath)) {
|
|
validPlugins++;
|
|
}
|
|
}
|
|
|
|
score = (validPlugins / config.plugins.length) * 100;
|
|
} catch (error) {
|
|
score = 0;
|
|
}
|
|
|
|
return Math.round(score);
|
|
}
|
|
|
|
/**
|
|
* Analyze documentation
|
|
*/
|
|
async analyzeDocumentation(marketplacePath) {
|
|
let score = 0;
|
|
|
|
const docsDir = path.join(marketplacePath, 'docs');
|
|
|
|
if (!fs.existsSync(docsDir)) {
|
|
return 20; // Basic README only
|
|
}
|
|
|
|
const docFiles = fs.readdirSync(docsDir);
|
|
|
|
// Check for key documentation files
|
|
const keyDocs = ['README.md', 'GUIDE.md', 'CONTRIBUTING.md'];
|
|
keyDocs.forEach(doc => {
|
|
if (docFiles.includes(doc)) {
|
|
score += 80 / keyDocs.length;
|
|
}
|
|
});
|
|
|
|
// Bonus for additional documentation
|
|
if (docFiles.length > keyDocs.length) {
|
|
score += 20;
|
|
}
|
|
|
|
return Math.round(Math.min(score, 100));
|
|
}
|
|
|
|
/**
|
|
* Generate recommendations
|
|
*/
|
|
generateRecommendations(analysis) {
|
|
const recommendations = [];
|
|
|
|
if (analysis.structure_score < 80) {
|
|
recommendations.push('Improve directory structure by adding missing recommended directories');
|
|
}
|
|
|
|
if (analysis.configuration_score < 80) {
|
|
recommendations.push(
|
|
'Enhance marketplace configuration with additional metadata and information'
|
|
);
|
|
}
|
|
|
|
if (analysis.plugin_score < 80) {
|
|
recommendations.push('Add more plugins or fix existing plugin issues');
|
|
}
|
|
|
|
if (analysis.documentation_score < 80) {
|
|
recommendations.push('Improve documentation by adding comprehensive guides and examples');
|
|
}
|
|
|
|
if (analysis.overall_score >= 90) {
|
|
recommendations.push('Excellent marketplace health! Consider sharing with the community');
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
|
|
/**
|
|
* Generate marketplace metrics
|
|
*/
|
|
async generateMetrics(marketplacePath) {
|
|
const metrics = {};
|
|
|
|
try {
|
|
// Count plugins
|
|
const pluginsDir = path.join(marketplacePath, 'plugins');
|
|
if (fs.existsSync(pluginsDir)) {
|
|
metrics.plugin_count = fs.readdirSync(pluginsDir).length;
|
|
}
|
|
|
|
// Count skills
|
|
const skillsDir = path.join(marketplacePath, 'skills');
|
|
if (fs.existsSync(skillsDir)) {
|
|
metrics.skill_count = fs.readdirSync(skillsDir).length;
|
|
}
|
|
|
|
// Calculate total size
|
|
metrics.total_size = this.calculateDirectorySize(marketplacePath);
|
|
|
|
// Get last modified
|
|
const stats = fs.statSync(marketplacePath);
|
|
metrics.last_modified = stats.mtime.toISOString();
|
|
} catch (error) {
|
|
this.log(`Error generating metrics: ${error.message}`, 'warn');
|
|
}
|
|
|
|
return metrics;
|
|
}
|
|
|
|
/**
|
|
* Calculate directory size
|
|
*/
|
|
calculateDirectorySize(dirPath) {
|
|
let totalSize = 0;
|
|
|
|
try {
|
|
const files = fs.readdirSync(dirPath);
|
|
|
|
for (const file of files) {
|
|
const filePath = path.join(dirPath, file);
|
|
const stats = fs.statSync(filePath);
|
|
|
|
if (stats.isDirectory()) {
|
|
totalSize += this.calculateDirectorySize(filePath);
|
|
} else {
|
|
totalSize += stats.size;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
this.log(`Error calculating size for ${dirPath}: ${error.message}`, 'warn');
|
|
}
|
|
|
|
return totalSize;
|
|
}
|
|
|
|
/**
|
|
* Report validation results
|
|
*/
|
|
reportValidationResults(results) {
|
|
this.log(`Validation completed: ${results.success ? 'SUCCESS' : 'FAILED'}`);
|
|
|
|
if (results.errors.length > 0) {
|
|
this.log(`Errors found: ${results.errors.length}`, 'error');
|
|
results.errors.forEach(error => this.log(` - ${error}`, 'error'));
|
|
}
|
|
|
|
if (results.warnings.length > 0) {
|
|
this.log(`Warnings found: ${results.warnings.length}`, 'warn');
|
|
results.warnings.forEach(warning => this.log(` - ${warning}`, 'warn'));
|
|
}
|
|
|
|
if (results.info.length > 0 && this.verbose) {
|
|
this.log(`Info messages: ${results.info.length}`);
|
|
results.info.forEach(info => this.log(` - ${info}`));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report deployment results
|
|
*/
|
|
reportDeploymentResults(results) {
|
|
this.log(`Deployment completed`);
|
|
this.log(`Deployed: ${results.deployed.length}`);
|
|
this.log(`Failed: ${results.failed.length}`);
|
|
this.log(`Skipped: ${results.skipped.length}`);
|
|
|
|
if (results.failed.length > 0) {
|
|
results.failed.forEach(failure => {
|
|
this.log(`Failed to deploy ${failure.name}: ${failure.error}`, 'error');
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Report health analysis
|
|
*/
|
|
reportHealthAnalysis(analysis) {
|
|
this.log(`Marketplace Health Analysis`);
|
|
this.log(`Overall Score: ${analysis.overall_score}/100`);
|
|
this.log(`Structure: ${analysis.structure_score}/100`);
|
|
this.log(`Configuration: ${analysis.configuration_score}/100`);
|
|
this.log(`Plugins: ${analysis.plugin_score}/100`);
|
|
this.log(`Documentation: ${analysis.documentation_score}/100`);
|
|
|
|
if (analysis.recommendations.length > 0) {
|
|
this.log(`Recommendations:`);
|
|
analysis.recommendations.forEach(rec => this.log(` - ${rec}`));
|
|
}
|
|
|
|
this.log(`Metrics:`, 'info');
|
|
Object.entries(analysis.metrics).forEach(([key, value]) => {
|
|
this.log(` ${key}: ${value}`, 'info');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if version string is valid
|
|
*/
|
|
isValidVersion(version) {
|
|
return /^\d+\.\d+\.\d+(-.*)?$/.test(version);
|
|
}
|
|
}
|
|
|
|
// CLI interface
|
|
if (require.main === module) {
|
|
const args = process.argv.slice(2);
|
|
const options = {
|
|
verbose: args.includes('--verbose'),
|
|
dryRun: args.includes('--dry-run'),
|
|
force: args.includes('--force'),
|
|
};
|
|
|
|
const manager = new MarketplaceManager(options);
|
|
|
|
// Parse command
|
|
const command = args[0];
|
|
|
|
switch (command) {
|
|
case 'create':
|
|
const name = args[1];
|
|
if (!name) {
|
|
console.error('Error: Marketplace name required');
|
|
process.exit(1);
|
|
}
|
|
manager.createMarketplace(name, options);
|
|
break;
|
|
|
|
case 'validate':
|
|
const marketplacePath = args[1] || './';
|
|
manager.validateMarketplace(marketplacePath);
|
|
break;
|
|
|
|
case 'deploy':
|
|
const deployPath = args[1] || './';
|
|
manager.deployMarketplace(deployPath, options);
|
|
break;
|
|
|
|
case 'analyze':
|
|
const analyzePath = args[1] || './';
|
|
manager.analyzeMarketplaceHealth(analyzePath);
|
|
break;
|
|
|
|
default:
|
|
console.log('Usage: node marketplace-manager.js <command> [options]');
|
|
console.log('Commands: create, validate, deploy, analyze');
|
|
console.log('Options: --verbose, --dry-run, --force');
|
|
}
|
|
}
|
|
|
|
module.exports = MarketplaceManager;
|