publish-release.cjs•15.4 kB
#!/usr/bin/env node
/**
 * Desktop Commander - Complete Release Publishing Script
 * 
 * This script handles the entire release process:
 * 1. Version bump
 * 2. Build project and MCPB bundle
 * 3. Commit and tag
 * 4. Publish to NPM
 * 5. Publish to MCP Registry
 * 6. Verify publications
 */
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Colors for output
const colors = {
    reset: '\x1b[0m',
    red: '\x1b[31m',
    green: '\x1b[32m',
    yellow: '\x1b[33m',
    blue: '\x1b[34m',
};
// Helper functions for colored output
function printStep(message) {
    console.log(`${colors.blue}==>${colors.reset} ${message}`);
}
function printSuccess(message) {
    console.log(`${colors.green}✓${colors.reset} ${message}`);
}
function printError(message) {
    console.error(`${colors.red}✗${colors.reset} ${message}`);
}
function printWarning(message) {
    console.log(`${colors.yellow}⚠${colors.reset} ${message}`);
}
// Execute command with error handling
function exec(command, options = {}) {
    try {
        return execSync(command, { 
            encoding: 'utf8', 
            stdio: options.silent ? 'pipe' : 'inherit',
            ...options 
        });
    } catch (error) {
        if (options.ignoreError) {
            return options.silent ? '' : null;
        }
        throw error;
    }
}
// Execute command silently and return output
function execSilent(command, options = {}) {
    return exec(command, { silent: true, ...options });
}
// Parse command line arguments
function parseArgs() {
    const args = process.argv.slice(2);
    const options = {
        bumpType: 'patch',
        skipTests: false,
        dryRun: false,
        help: false,
        skipBump: false,
        skipBuild: false,
        skipMcpb: false,
        skipGit: false,
        skipNpm: false,
    };
    for (const arg of args) {
        switch (arg) {
            case '--minor':
                options.bumpType = 'minor';
                break;
            case '--major':
                options.bumpType = 'major';
                break;
            case '--skip-tests':
                options.skipTests = true;
                break;
            case '--skip-bump':
                options.skipBump = true;
                break;
            case '--skip-build':
                options.skipBuild = true;
                break;
            case '--skip-mcpb':
                options.skipMcpb = true;
                break;
            case '--skip-git':
                options.skipGit = true;
                break;
            case '--skip-npm':
                options.skipNpm = true;
                break;
            case '--mcp-only':
                // Skip everything except MCP Registry publish
                options.skipBump = true;
                options.skipBuild = true;
                options.skipMcpb = true;
                options.skipGit = true;
                options.skipNpm = true;
                break;
            case '--dry-run':
                options.dryRun = true;
                break;
            case '--help':
            case '-h':
                options.help = true;
                break;
            default:
                printError(`Unknown option: ${arg}`);
                console.log("Run 'node scripts/publish-release.js --help' for usage information.");
                process.exit(1);
        }
    }
    return options;
}
// Show help message
function showHelp() {
    console.log('Usage: node scripts/publish-release.cjs [OPTIONS]');
    console.log('');
    console.log('Options:');
    console.log('  --minor       Bump minor version (default: patch)');
    console.log('  --major       Bump major version (default: patch)');
    console.log('  --skip-tests  Skip running tests');
    console.log('  --skip-bump   Skip version bumping');
    console.log('  --skip-build  Skip building (if tests also skipped)');
    console.log('  --skip-mcpb   Skip building MCPB bundle');
    console.log('  --skip-git    Skip git commit and tag');
    console.log('  --skip-npm    Skip NPM publishing');
    console.log('  --mcp-only    Only publish to MCP Registry (skip all other steps)');
    console.log('  --dry-run     Simulate the release without publishing');
    console.log('  --help, -h    Show this help message');
    console.log('');
    console.log('Examples:');
    console.log('  node scripts/publish-release.cjs              # Patch release (0.2.16 -> 0.2.17)');
    console.log('  node scripts/publish-release.cjs --minor      # Minor release (0.2.16 -> 0.3.0)');
    console.log('  node scripts/publish-release.cjs --major      # Major release (0.2.16 -> 1.0.0)');
    console.log('  node scripts/publish-release.cjs --dry-run    # Test without publishing');
    console.log('  node scripts/publish-release.cjs --mcp-only   # Only publish to MCP Registry');
}
// Main release function
async function publishRelease() {
    const options = parseArgs();
    if (options.help) {
        showHelp();
        return;
    }
    // Check if we're in the right directory
    const packageJsonPath = path.join(process.cwd(), 'package.json');
    if (!fs.existsSync(packageJsonPath)) {
        printError('package.json not found. Please run this script from the project root.');
        process.exit(1);
    }
    console.log('');
    console.log('╔══════════════════════════════════════════════════════════╗');
    console.log('║         Desktop Commander Release Publisher             ║');
    console.log('╚══════════════════════════════════════════════════════════╝');
    console.log('');
    // Get current version
    const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
    const currentVersion = packageJson.version;
    printStep(`Current version: ${currentVersion}`);
    printStep(`Bump type: ${options.bumpType}`);
    if (options.dryRun) {
        printWarning('DRY RUN MODE - No changes will be published');
        console.log('');
    }
    try {
        let newVersion = currentVersion;
        
        // Step 1: Bump version
        if (!options.skipBump) {
            printStep('Step 1/6: Bumping version...');
            const bumpCommand = options.bumpType === 'minor' ? 'npm run bump:minor' :
                               options.bumpType === 'major' ? 'npm run bump:major' :
                               'npm run bump';
            exec(bumpCommand);
            const newPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
            newVersion = newPackageJson.version;
            printSuccess(`Version bumped: ${currentVersion} → ${newVersion}`);
            console.log('');
        } else {
            printWarning('Step 1/6: Version bump skipped');
            console.log('');
        }
        console.log('');
        // Step 2: Run tests (unless skipped) - tests also build the project
        if (!options.skipTests && !options.skipBuild) {
            printStep('Step 2/6: Running tests (includes build)...');
            exec('npm test');
            printSuccess('All tests passed');
        } else if (!options.skipBuild) {
            printWarning('Step 2/6: Tests skipped - building project...');
            exec('npm run build');
            printSuccess('Project built successfully');
        } else {
            printWarning('Step 2/6: Tests and build skipped');
        }
        console.log('');
        // Step 3: Build MCPB bundle
        if (!options.skipMcpb) {
            printStep('Step 3/6: Building MCPB bundle...');
            exec('npm run build:mcpb');
            printSuccess('MCPB bundle created');
        } else {
            printWarning('Step 3/6: MCPB bundle build skipped');
        }
        console.log('');
        // Step 4: Commit and tag
        if (!options.skipGit) {
            printStep('Step 4/6: Creating git commit and tag...');
        
            // Check if there are changes to commit
            const gitStatus = execSilent('git status --porcelain', { ignoreError: true });
            const hasChanges = gitStatus.includes('package.json') || 
                              gitStatus.includes('server.json') || 
                              gitStatus.includes('src/version.ts');
            if (!hasChanges) {
                printWarning('No changes to commit (version files already committed)');
            } else {
                exec('git add package.json server.json src/version.ts');
                
                const commitMsg = `Release v${newVersion}
Automated release commit with version bump from ${currentVersion} to ${newVersion}`;
                if (options.dryRun) {
                    printWarning(`Would commit: ${commitMsg.split('\n')[0]}`);
                } else {
                    exec(`git commit -m "${commitMsg}"`);
                    printSuccess('Changes committed');
                }
            }
            // Create and push tag
            const tagName = `v${newVersion}`;
            
            if (options.dryRun) {
                printWarning(`Would create tag: ${tagName}`);
                printWarning(`Would push to origin: main and ${tagName}`);
            } else {
                exec(`git tag ${tagName}`);
                exec('git push origin main');
                exec(`git push origin ${tagName}`);
                printSuccess(`Tag ${tagName} created and pushed`);
            }
        } else {
            printWarning('Step 4/6: Git commit and tag skipped');
        }
        console.log('');
        // Step 5: Publish to NPM
        if (!options.skipNpm) {
            printStep('Step 5/6: Publishing to NPM...');
            
            // Check NPM authentication
            const npmUser = execSilent('npm whoami', { ignoreError: true }).trim();
            if (!npmUser) {
                printError('Not logged into NPM. Please run "npm login" first.');
                process.exit(1);
            }
            printSuccess(`NPM user: ${npmUser}`);
            if (options.dryRun) {
                printWarning('Would publish to NPM: npm publish');
                printWarning('Skipping NPM publish (dry run)');
            } else {
                exec('npm publish');
                printSuccess('Published to NPM');
                
                // Verify NPM publication
                await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds
                const npmVersion = execSilent('npm view @wonderwhy-er/desktop-commander version', { ignoreError: true }).trim();
                if (npmVersion === newVersion) {
                    printSuccess(`NPM publication verified: v${npmVersion}`);
                } else {
                    printWarning(`NPM version mismatch: expected ${newVersion}, got ${npmVersion} (may take a moment to propagate)`);
                }
            }
        } else {
            printWarning('Step 5/6: NPM publish skipped');
        }
        console.log('');
        // Step 6: Publish to MCP Registry
        printStep('Step 6/6: Publishing to MCP Registry...');
        
        // Check if mcp-publisher is installed
        const hasMcpPublisher = execSilent('which mcp-publisher', { ignoreError: true });
        if (!hasMcpPublisher) {
            printError('mcp-publisher not found. Install it with: brew install mcp-publisher');
            printError('Or check your PATH if already installed.');
            process.exit(1);
        }
        if (options.dryRun) {
            printWarning('Would publish to MCP Registry: mcp-publisher publish');
            printWarning('Skipping MCP Registry publish (dry run)');
        } else {
            try {
                exec('mcp-publisher publish');
                printSuccess('Published to MCP Registry');
                
                // Verify MCP Registry publication
                await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds
                try {
                    const mcpResponse = execSilent('curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.wonderwhy-er/desktop-commander"');
                    const mcpData = JSON.parse(mcpResponse);
                    const mcpVersion = mcpData.servers?.[0]?.version || 'unknown';
                    
                    if (mcpVersion === newVersion) {
                        printSuccess(`MCP Registry publication verified: v${mcpVersion}`);
                    } else {
                        printWarning(`MCP Registry version: ${mcpVersion} (expected ${newVersion}, may take a moment to propagate)`);
                    }
                } catch (error) {
                    printWarning('Could not verify MCP Registry publication');
                }
            } catch (error) {
                printError('MCP Registry publish failed!');
                if (error.message.includes('401') || error.message.includes('expired')) {
                    printError('Authentication token expired. Please run: mcp-publisher login github');
                } else if (error.message.includes('422')) {
                    printError('Validation error in server.json. Check the error message above for details.');
                }
                throw error;
            }
        }
        console.log('');
        // Success summary
        console.log('╔══════════════════════════════════════════════════════════╗');
        console.log('║                  🎉 Release Complete! 🎉                 ║');
        console.log('╚══════════════════════════════════════════════════════════╝');
        console.log('');
        printSuccess(`Version: ${newVersion}`);
        printSuccess('NPM: https://www.npmjs.com/package/@wonderwhy-er/desktop-commander');
        printSuccess('MCP Registry: https://registry.modelcontextprotocol.io/');
        printSuccess(`GitHub Tag: https://github.com/wonderwhy-er/DesktopCommanderMCP/releases/tag/${tagName}`);
        console.log('');
        console.log('Next steps:');
        console.log(`  1. Create GitHub release at: https://github.com/wonderwhy-er/DesktopCommanderMCP/releases/new?tag=${tagName}`);
        console.log('  2. Add release notes with features and fixes');
        console.log('  3. Announce on Discord');
        console.log('');
        if (options.dryRun) {
            console.log('');
            printWarning('This was a DRY RUN - no changes were published');
            printWarning('Run without --dry-run to perform the actual release');
            console.log('');
        }
    } catch (error) {
        console.log('');
        printError('Release failed!');
        printError(error.message);
        process.exit(1);
    }
}
// Run the script
publishRelease().catch(error => {
    printError('Unexpected error:');
    console.error(error);
    process.exit(1);
});