353 lines
9.8 KiB
JavaScript
353 lines
9.8 KiB
JavaScript
#!/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;
|