Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:39:56 +08:00
commit e862bbb341
40 changed files with 10597 additions and 0 deletions

View File

@@ -0,0 +1,352 @@
#!/usr/bin/env node
/**
* Marketplace Deployment Script
* Handles deployment of marketplace plugins and updates
*/
const fs = require('fs');
const path = require('path');
const { spawn } = require('child_process');
class MarketplaceDeployer {
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
*/
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);
});
});
}
/**
* Validate marketplace before deployment
*/
async validateBeforeDeploy(marketplacePath) {
this.log('Validating marketplace before deployment...');
const validatorPath = path.join(__dirname, 'validate.js');
try {
await this.executeCommand('node', [validatorPath, '--verbose'], marketplacePath);
this.log('Marketplace validation passed');
return true;
} catch (error) {
this.log(`Marketplace validation failed: ${error.message}`, 'error');
if (!this.force) {
throw new Error('Deployment aborted due to validation failures. Use --force to override.');
}
this.log('Proceeding with deployment despite validation failures (force mode)', 'warn');
return false;
}
}
/**
* Get current version from marketplace configuration
*/
getCurrentVersion(marketplacePath) {
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
if (!fs.existsSync(configPath)) {
throw new Error('Marketplace configuration not found');
}
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return config.version;
}
/**
* Increment version based on release type
*/
incrementVersion(version, type = 'patch') {
const parts = version.split('.');
if (parts.length !== 3) {
throw new Error(`Invalid version format: ${version}`);
}
const [major, minor, patch] = parts.map(p => parseInt(p, 10));
switch (type) {
case 'major':
return `${major + 1}.0.0`;
case 'minor':
return `${major}.${minor + 1}.0`;
case 'patch':
return `${major}.${minor}.${patch + 1}`;
default:
throw new Error(`Invalid release type: ${type}`);
}
}
/**
* Update marketplace version
*/
updateVersion(marketplacePath, newVersion) {
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
config.version = newVersion;
config.updated = new Date().toISOString();
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
this.log(`Updated marketplace version to: ${newVersion}`);
}
/**
* Create git tag for release
*/
async createGitTag(marketplacePath, version) {
this.log(`Creating git tag: v${version}`);
if (!this.dryRun) {
try {
await this.executeCommand('git', ['add', '.'], marketplacePath);
await this.executeCommand('git', ['commit', '-m', `Release v${version}`], marketplacePath);
await this.executeCommand(
'git',
['tag', `-a`, `v${version}`, '-m', `Release v${version}`],
marketplacePath
);
this.log(`Git tag v${version} created successfully`);
} catch (error) {
this.log(`Failed to create git tag: ${error.message}`, 'error');
throw error;
}
} else {
this.log(`[DRY RUN] Would create git tag: v${version}`);
}
}
/**
* Push to remote repository
*/
async pushToRemote(marketplacePath, version) {
this.log('Pushing to remote repository...');
if (!this.dryRun) {
try {
await this.executeCommand('git', ['push', 'origin', 'main'], marketplacePath);
await this.executeCommand('git', ['push', 'origin', `v${version}`], marketplacePath);
this.log('Successfully pushed to remote repository');
} catch (error) {
this.log(`Failed to push to remote: ${error.message}`, 'error');
throw error;
}
} else {
this.log('[DRY RUN] Would push to remote repository');
}
}
/**
* Generate release notes
*/
generateReleaseNotes(marketplacePath, version) {
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const notes = [
`# Release ${version}`,
'',
`## Changes`,
'',
`### Marketplace`,
`- Updated marketplace configuration`,
`- Version bump to ${version}`,
'',
`### Plugins`,
`${config.plugins.length} plugins included`,
'',
`### Skills`,
`${config.skills.length} skills included`,
'',
`## Installation`,
'```bash',
`/plugin marketplace add [repository-url]`,
'```',
'',
`## Verification`,
'```bash',
`/plugin marketplace list`,
`/plugin install [plugin-name]@[marketplace-name]`,
'```',
'',
`---`,
`*Released on ${new Date().toISOString().split('T')[0]}*`,
];
const notesPath = path.join(marketplacePath, 'RELEASE_NOTES.md');
fs.writeFileSync(notesPath, notes.join('\n'));
this.log(`Release notes generated: ${notesPath}`);
return notesPath;
}
/**
* Deploy marketplace
*/
async deploy(marketplacePath = './', options = {}) {
const releaseType = options.type || 'patch';
const skipValidation = options.skipValidation || false;
const skipGit = options.skipGit || false;
console.log(`Starting marketplace deployment for: ${marketplacePath}`);
console.log(`Release type: ${releaseType}`);
console.log('='.repeat(50));
try {
// Validate marketplace unless skipped
if (!skipValidation) {
await this.validateBeforeDeploy(marketplacePath);
}
// Get current version
const currentVersion = this.getCurrentVersion(marketplacePath);
this.log(`Current version: ${currentVersion}`);
// Calculate new version
const newVersion = this.incrementVersion(currentVersion, releaseType);
this.log(`New version: ${newVersion}`);
// Update version in configuration
this.updateVersion(marketplacePath, newVersion);
// Generate release notes
const notesPath = this.generateReleaseNotes(marketplacePath, newVersion);
// Git operations unless skipped
if (!skipGit) {
await this.createGitTag(marketplacePath, newVersion);
await this.pushToRemote(marketplacePath, newVersion);
}
console.log('='.repeat(50));
console.log('✅ Deployment completed successfully');
console.log(`Version: ${newVersion}`);
console.log(`Release notes: ${notesPath}`);
if (!skipGit) {
console.log('Git tag created and pushed');
}
return {
success: true,
version: newVersion,
notesPath,
skipped: { validation: skipValidation, git: skipGit },
};
} catch (error) {
console.error('❌ Deployment failed:', error.message);
throw error;
}
}
/**
* Deploy individual plugin
*/
async deployPlugin(pluginPath, options = {}) {
this.log(`Deploying plugin: ${pluginPath}`);
// Validate plugin structure
const pluginJsonPath = path.join(pluginPath, '.claude-plugin', 'plugin.json');
if (!fs.existsSync(pluginJsonPath)) {
throw new Error('Plugin configuration not found');
}
const pluginConfig = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
this.log(`Plugin: ${pluginConfig.name} v${pluginConfig.version}`);
// Implementation would depend on deployment target
// This is a placeholder for actual plugin deployment logic
if (!this.dryRun) {
// Add actual plugin deployment logic here
// Could involve:
// - Publishing to npm registry
// - Creating GitHub release
// - Uploading to plugin registry
// etc.
}
this.log(`Plugin deployment completed: ${pluginConfig.name}`);
return { success: true, plugin: pluginConfig.name };
}
}
// 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'),
type: 'patch',
skipValidation: args.includes('--skip-validation'),
skipGit: args.includes('--skip-git'),
};
// Parse release type
const typeIndex = args.findIndex(arg => arg.startsWith('--type='));
if (typeIndex !== -1) {
options.type = args[typeIndex].split('=')[1];
}
const marketplacePath = args.find(arg => !arg.startsWith('--')) || './';
const deployer = new MarketplaceDeployer(options);
deployer
.deploy(marketplacePath, options)
.then(result => {
console.log('\nDeployment summary:', result);
})
.catch(error => {
console.error('\nDeployment failed:', error.message);
process.exit(1);
});
}
module.exports = MarketplaceDeployer;

View File

@@ -0,0 +1,391 @@
#!/usr/bin/env node
/**
* Marketplace Validation Script
* Validates marketplace structure, configuration, and content
*/
const fs = require('fs');
const path = require('path');
class MarketplaceValidator {
constructor() {
this.errors = [];
this.warnings = [];
this.info = [];
}
log(message, type = 'info') {
this[type].push(message);
}
/**
* Validate marketplace directory structure
*/
validateStructure(marketplacePath) {
console.log('Validating marketplace structure...');
const requiredDirs = ['.claude-plugin'];
const requiredFiles = ['.claude-plugin/marketplace.json'];
const optionalDirs = ['plugins', 'skills', 'docs', 'tests', 'scripts', 'examples'];
// Check required directories
requiredDirs.forEach(dir => {
const dirPath = path.join(marketplacePath, dir);
if (!fs.existsSync(dirPath)) {
this.log(`Required directory missing: ${dir}`, 'errors');
} else {
this.log(`Required directory found: ${dir}`, 'info');
}
});
// Check required files
requiredFiles.forEach(file => {
const filePath = path.join(marketplacePath, file);
if (!fs.existsSync(filePath)) {
this.log(`Required file missing: ${file}`, 'errors');
} else {
this.log(`Required file found: ${file}`, 'info');
}
});
// Check optional directories
optionalDirs.forEach(dir => {
const dirPath = path.join(marketplacePath, dir);
if (fs.existsSync(dirPath)) {
this.log(`Optional directory found: ${dir}`, 'info');
}
});
}
/**
* Validate marketplace configuration
*/
validateConfiguration(marketplacePath) {
console.log('Validating marketplace configuration...');
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
if (!fs.existsSync(configPath)) {
this.log('Marketplace configuration file missing', 'errors');
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]) {
this.log(`Required configuration field missing: ${field}`, 'errors');
} else {
this.log(`Required field found: ${field}`, 'info');
}
});
// Validate version format
if (config.version && !this.isValidVersion(config.version)) {
this.log(`Invalid version format: ${config.version}`, 'errors');
}
// Validate plugins array
if (config.plugins) {
if (!Array.isArray(config.plugins)) {
this.log('Plugins field must be an array', 'errors');
} else {
this.log(`Found ${config.plugins.length} plugins in configuration`, 'info');
}
}
// Validate skills array
if (config.skills) {
if (!Array.isArray(config.skills)) {
this.log('Skills field must be an array', 'errors');
} else {
this.log(`Found ${config.skills.length} skills in configuration`, 'info');
}
}
// Validate owner object
if (config.owner) {
const ownerFields = ['name', 'email'];
ownerFields.forEach(field => {
if (config.owner[field]) {
this.log(`Owner ${field} found: ${config.owner[field]}`, 'info');
}
});
}
this.log('Configuration file structure validated', 'info');
} catch (error) {
this.log(`Invalid JSON in configuration file: ${error.message}`, 'errors');
}
}
/**
* Validate plugins
*/
validatePlugins(marketplacePath) {
console.log('Validating plugins...');
const configPath = path.join(marketplacePath, '.claude-plugin', 'marketplace.json');
if (!fs.existsSync(configPath)) {
this.log('Cannot validate plugins - configuration file missing', 'warnings');
return;
}
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
if (!config.plugins || config.plugins.length === 0) {
this.log('No plugins configured in marketplace', 'warnings');
return;
}
const pluginsDir = path.join(marketplacePath, 'plugins');
if (!fs.existsSync(pluginsDir)) {
this.log('Plugins directory not found', 'warnings');
return;
}
for (const plugin of config.plugins) {
this.validatePlugin(plugin, pluginsDir);
}
} catch (error) {
this.log(`Error validating plugins: ${error.message}`, 'errors');
}
}
/**
* Validate individual plugin
*/
validatePlugin(plugin, pluginsDir) {
if (!plugin.name) {
this.log('Plugin found without name in configuration', 'errors');
return;
}
const pluginPath = path.join(pluginsDir, plugin.name);
if (!fs.existsSync(pluginPath)) {
this.log(`Plugin directory not found: ${plugin.name}`, 'warnings');
return;
}
const pluginJsonPath = path.join(pluginPath, '.claude-plugin', 'plugin.json');
if (!fs.existsSync(pluginJsonPath)) {
this.log(`Plugin configuration missing: ${plugin.name}/plugin.json`, 'errors');
return;
}
try {
const pluginConfig = JSON.parse(fs.readFileSync(pluginJsonPath, 'utf8'));
// Validate plugin structure
const requiredFields = ['name', 'version', 'description'];
requiredFields.forEach(field => {
if (!pluginConfig[field]) {
this.log(`Plugin ${plugin.name}: Required field missing: ${field}`, 'errors');
}
});
this.log(`Plugin validated: ${plugin.name}`, 'info');
} catch (error) {
this.log(`Plugin ${plugin.name}: Invalid configuration - ${error.message}`, 'errors');
}
}
/**
* Validate skills
*/
validateSkills(marketplacePath) {
console.log('Validating skills...');
const skillsDir = path.join(marketplacePath, 'skills');
if (!fs.existsSync(skillsDir)) {
this.log('Skills directory not found', 'warnings');
return;
}
try {
const skills = fs
.readdirSync(skillsDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
if (skills.length === 0) {
this.log('No skills found in marketplace', 'warnings');
return;
}
for (const skill of skills) {
this.validateSkill(path.join(skillsDir, skill), skill);
}
} catch (error) {
this.log(`Error validating skills: ${error.message}`, 'errors');
}
}
/**
* Validate individual skill
*/
validateSkill(skillPath, skillName) {
const skillMdPath = path.join(skillPath, 'SKILL.md');
if (!fs.existsSync(skillMdPath)) {
this.log(`Skill SKILL.md missing: ${skillName}`, 'errors');
return;
}
try {
const content = fs.readFileSync(skillMdPath, 'utf8');
// Check for required frontmatter
if (!content.includes('---')) {
this.log(`Skill ${skillName}: Missing frontmatter`, 'errors');
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) {
this.log(`Skill ${skillName}: Missing name in frontmatter`, 'errors');
}
if (!frontmatter.description) {
this.log(`Skill ${skillName}: Missing description in frontmatter`, 'warnings');
}
} catch (error) {
this.log(`Skill ${skillName}: Invalid frontmatter format`, 'errors');
}
}
this.log(`Skill validated: ${skillName}`, 'info');
} catch (error) {
this.log(`Skill ${skillName}: Error reading file - ${error.message}`, 'errors');
}
}
/**
* Validate documentation
*/
validateDocumentation(marketplacePath) {
console.log('Validating documentation...');
const docsDir = path.join(marketplacePath, 'docs');
if (!fs.existsSync(docsDir)) {
this.log('Documentation directory not found', 'warnings');
return;
}
const requiredDocs = ['README.md'];
const recommendedDocs = ['GUIDE.md', 'CONTRIBUTING.md'];
// Check required documentation
requiredDocs.forEach(doc => {
const docPath = path.join(marketplacePath, doc);
if (fs.existsSync(docPath)) {
this.log(`Required documentation found: ${doc}`, 'info');
} else {
this.log(`Required documentation missing: ${doc}`, 'errors');
}
});
// Check recommended documentation
recommendedDocs.forEach(doc => {
const docPath = path.join(docsDir, doc);
if (fs.existsSync(docPath)) {
this.log(`Recommended documentation found: ${doc}`, 'info');
} else {
this.log(`Recommended documentation missing: ${doc}`, 'warnings');
}
});
}
/**
* Check if version string is valid
*/
isValidVersion(version) {
return /^\d+\.\d+\.\d+(-.*)?$/.test(version);
}
/**
* Run complete validation
*/
async validate(marketplacePath = './') {
console.log(`Starting marketplace validation for: ${marketplacePath}`);
console.log('='.repeat(50));
// Check if marketplace exists
if (!fs.existsSync(marketplacePath)) {
console.error('Error: Marketplace directory does not exist');
process.exit(1);
}
// Run all validations
this.validateStructure(marketplacePath);
this.validateConfiguration(marketplacePath);
this.validatePlugins(marketplacePath);
this.validateSkills(marketplacePath);
this.validateDocumentation(marketplacePath);
// Report results
console.log('='.repeat(50));
console.log('Validation Results:');
console.log(`Errors: ${this.errors.length}`);
console.log(`Warnings: ${this.warnings.length}`);
console.log(`Info: ${this.info.length}`);
if (this.errors.length > 0) {
console.log('\nErrors:');
this.errors.forEach(error => console.log(`${error}`));
}
if (this.warnings.length > 0) {
console.log('\nWarnings:');
this.warnings.forEach(warning => console.log(` ⚠️ ${warning}`));
}
if (this.verbose && this.info.length > 0) {
console.log('\nInfo:');
this.info.forEach(info => console.log(` ${info}`));
}
// Exit with appropriate code
if (this.errors.length > 0) {
console.log('\n❌ Validation failed with errors');
process.exit(1);
} else if (this.warnings.length > 0) {
console.log('\n⚠ Validation completed with warnings');
process.exit(2);
} else {
console.log('\n✅ Validation passed successfully');
process.exit(0);
}
}
}
// CLI interface
if (require.main === module) {
const args = process.argv.slice(2);
const marketplacePath = args[0] || './';
const validator = new MarketplaceValidator();
// Check for verbose flag
validator.verbose = args.includes('--verbose');
validator.validate(marketplacePath).catch(error => {
console.error('Validation error:', error.message);
process.exit(1);
});
}
module.exports = MarketplaceValidator;