Initial commit
This commit is contained in:
930
skills/claude-code-marketplace/scripts/marketplace-manager.js
Normal file
930
skills/claude-code-marketplace/scripts/marketplace-manager.js
Normal file
@@ -0,0 +1,930 @@
|
||||
#!/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;
|
||||
Reference in New Issue
Block a user