|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +/** |
| 4 | + * Desktop Commander - Complete Release Publishing Script |
| 5 | + * |
| 6 | + * This script handles the entire release process: |
| 7 | + * 1. Version bump |
| 8 | + * 2. Build project and MCPB bundle |
| 9 | + * 3. Commit and tag |
| 10 | + * 4. Publish to NPM |
| 11 | + * 5. Publish to MCP Registry |
| 12 | + * 6. Verify publications |
| 13 | + */ |
| 14 | + |
| 15 | +const { execSync } = require('child_process'); |
| 16 | +const fs = require('fs'); |
| 17 | +const path = require('path'); |
| 18 | + |
| 19 | +// Colors for output |
| 20 | +const colors = { |
| 21 | + reset: '\x1b[0m', |
| 22 | + red: '\x1b[31m', |
| 23 | + green: '\x1b[32m', |
| 24 | + yellow: '\x1b[33m', |
| 25 | + blue: '\x1b[34m', |
| 26 | +}; |
| 27 | + |
| 28 | +// Helper functions for colored output |
| 29 | +function printStep(message) { |
| 30 | + console.log(`${colors.blue}==>${colors.reset} ${message}`); |
| 31 | +} |
| 32 | + |
| 33 | +function printSuccess(message) { |
| 34 | + console.log(`${colors.green}✓${colors.reset} ${message}`); |
| 35 | +} |
| 36 | + |
| 37 | +function printError(message) { |
| 38 | + console.error(`${colors.red}✗${colors.reset} ${message}`); |
| 39 | +} |
| 40 | + |
| 41 | +function printWarning(message) { |
| 42 | + console.log(`${colors.yellow}⚠${colors.reset} ${message}`); |
| 43 | +} |
| 44 | + |
| 45 | +// Execute command with error handling |
| 46 | +function exec(command, options = {}) { |
| 47 | + try { |
| 48 | + return execSync(command, { |
| 49 | + encoding: 'utf8', |
| 50 | + stdio: options.silent ? 'pipe' : 'inherit', |
| 51 | + ...options |
| 52 | + }); |
| 53 | + } catch (error) { |
| 54 | + if (options.ignoreError) { |
| 55 | + return options.silent ? '' : null; |
| 56 | + } |
| 57 | + throw error; |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +// Execute command silently and return output |
| 62 | +function execSilent(command, options = {}) { |
| 63 | + return exec(command, { silent: true, ...options }); |
| 64 | +} |
| 65 | + |
| 66 | +// Parse command line arguments |
| 67 | +function parseArgs() { |
| 68 | + const args = process.argv.slice(2); |
| 69 | + const options = { |
| 70 | + bumpType: 'patch', |
| 71 | + skipTests: false, |
| 72 | + dryRun: false, |
| 73 | + help: false, |
| 74 | + }; |
| 75 | + |
| 76 | + for (const arg of args) { |
| 77 | + switch (arg) { |
| 78 | + case '--minor': |
| 79 | + options.bumpType = 'minor'; |
| 80 | + break; |
| 81 | + case '--major': |
| 82 | + options.bumpType = 'major'; |
| 83 | + break; |
| 84 | + case '--skip-tests': |
| 85 | + options.skipTests = true; |
| 86 | + break; |
| 87 | + case '--dry-run': |
| 88 | + options.dryRun = true; |
| 89 | + break; |
| 90 | + case '--help': |
| 91 | + case '-h': |
| 92 | + options.help = true; |
| 93 | + break; |
| 94 | + default: |
| 95 | + printError(`Unknown option: ${arg}`); |
| 96 | + console.log("Run 'node scripts/publish-release.js --help' for usage information."); |
| 97 | + process.exit(1); |
| 98 | + } |
| 99 | + } |
| 100 | + |
| 101 | + return options; |
| 102 | +} |
| 103 | + |
| 104 | +// Show help message |
| 105 | +function showHelp() { |
| 106 | + console.log('Usage: node scripts/publish-release.cjs [OPTIONS]'); |
| 107 | + console.log(''); |
| 108 | + console.log('Options:'); |
| 109 | + console.log(' --minor Bump minor version (default: patch)'); |
| 110 | + console.log(' --major Bump major version (default: patch)'); |
| 111 | + console.log(' --skip-tests Skip running tests'); |
| 112 | + console.log(' --dry-run Simulate the release without publishing'); |
| 113 | + console.log(' --help, -h Show this help message'); |
| 114 | + console.log(''); |
| 115 | + console.log('Examples:'); |
| 116 | + console.log(' node scripts/publish-release.cjs # Patch release (0.2.16 -> 0.2.17)'); |
| 117 | + console.log(' node scripts/publish-release.cjs --minor # Minor release (0.2.16 -> 0.3.0)'); |
| 118 | + console.log(' node scripts/publish-release.cjs --major # Major release (0.2.16 -> 1.0.0)'); |
| 119 | + console.log(' node scripts/publish-release.cjs --dry-run # Test without publishing'); |
| 120 | +} |
| 121 | + |
| 122 | +// Main release function |
| 123 | +async function publishRelease() { |
| 124 | + const options = parseArgs(); |
| 125 | + |
| 126 | + if (options.help) { |
| 127 | + showHelp(); |
| 128 | + return; |
| 129 | + } |
| 130 | + |
| 131 | + // Check if we're in the right directory |
| 132 | + const packageJsonPath = path.join(process.cwd(), 'package.json'); |
| 133 | + if (!fs.existsSync(packageJsonPath)) { |
| 134 | + printError('package.json not found. Please run this script from the project root.'); |
| 135 | + process.exit(1); |
| 136 | + } |
| 137 | + |
| 138 | + console.log(''); |
| 139 | + console.log('╔══════════════════════════════════════════════════════════╗'); |
| 140 | + console.log('║ Desktop Commander Release Publisher ║'); |
| 141 | + console.log('╚══════════════════════════════════════════════════════════╝'); |
| 142 | + console.log(''); |
| 143 | + |
| 144 | + // Get current version |
| 145 | + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); |
| 146 | + const currentVersion = packageJson.version; |
| 147 | + printStep(`Current version: ${currentVersion}`); |
| 148 | + printStep(`Bump type: ${options.bumpType}`); |
| 149 | + |
| 150 | + if (options.dryRun) { |
| 151 | + printWarning('DRY RUN MODE - No changes will be published'); |
| 152 | + console.log(''); |
| 153 | + } |
| 154 | + |
| 155 | + try { |
| 156 | + // Step 1: Bump version |
| 157 | + printStep('Step 1/7: Bumping version...'); |
| 158 | + const bumpCommand = options.bumpType === 'minor' ? 'npm run bump:minor' : |
| 159 | + options.bumpType === 'major' ? 'npm run bump:major' : |
| 160 | + 'npm run bump'; |
| 161 | + exec(bumpCommand); |
| 162 | + |
| 163 | + const newPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); |
| 164 | + const newVersion = newPackageJson.version; |
| 165 | + printSuccess(`Version bumped: ${currentVersion} → ${newVersion}`); |
| 166 | + console.log(''); |
| 167 | + |
| 168 | + // Step 2: Build project |
| 169 | + printStep('Step 2/7: Building project...'); |
| 170 | + exec('npm run build'); |
| 171 | + printSuccess('Project built successfully'); |
| 172 | + console.log(''); |
| 173 | + |
| 174 | + // Step 3: Run tests (unless skipped) |
| 175 | + if (!options.skipTests) { |
| 176 | + printStep('Step 3/7: Running tests...'); |
| 177 | + exec('npm test'); |
| 178 | + printSuccess('All tests passed'); |
| 179 | + } else { |
| 180 | + printWarning('Step 3/7: Tests skipped'); |
| 181 | + } |
| 182 | + console.log(''); |
| 183 | + |
| 184 | + // Step 4: Build MCPB bundle |
| 185 | + printStep('Step 4/7: Building MCPB bundle...'); |
| 186 | + exec('npm run build:mcpb'); |
| 187 | + printSuccess('MCPB bundle created'); |
| 188 | + console.log(''); |
| 189 | + |
| 190 | + // Step 5: Commit and tag |
| 191 | + printStep('Step 5/7: Creating git commit and tag...'); |
| 192 | + |
| 193 | + // Check if there are changes to commit |
| 194 | + const gitStatus = execSilent('git status --porcelain', { ignoreError: true }); |
| 195 | + const hasChanges = gitStatus.includes('package.json') || |
| 196 | + gitStatus.includes('server.json') || |
| 197 | + gitStatus.includes('src/version.ts'); |
| 198 | + |
| 199 | + if (!hasChanges) { |
| 200 | + printWarning('No changes to commit (version files already committed)'); |
| 201 | + } else { |
| 202 | + exec('git add package.json server.json src/version.ts'); |
| 203 | + |
| 204 | + const commitMsg = `Release v${newVersion} |
| 205 | +
|
| 206 | +Automated release commit with version bump from ${currentVersion} to ${newVersion}`; |
| 207 | + |
| 208 | + if (options.dryRun) { |
| 209 | + printWarning(`Would commit: ${commitMsg.split('\n')[0]}`); |
| 210 | + } else { |
| 211 | + exec(`git commit -m "${commitMsg}"`); |
| 212 | + printSuccess('Changes committed'); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + // Create and push tag |
| 217 | + const tagName = `v${newVersion}`; |
| 218 | + |
| 219 | + if (options.dryRun) { |
| 220 | + printWarning(`Would create tag: ${tagName}`); |
| 221 | + printWarning(`Would push to origin: main and ${tagName}`); |
| 222 | + } else { |
| 223 | + exec(`git tag ${tagName}`); |
| 224 | + exec('git push origin main'); |
| 225 | + exec(`git push origin ${tagName}`); |
| 226 | + printSuccess(`Tag ${tagName} created and pushed`); |
| 227 | + } |
| 228 | + console.log(''); |
| 229 | + |
| 230 | + // Step 6: Publish to NPM |
| 231 | + printStep('Step 6/7: Publishing to NPM...'); |
| 232 | + |
| 233 | + // Check NPM authentication |
| 234 | + const npmUser = execSilent('npm whoami', { ignoreError: true }).trim(); |
| 235 | + if (!npmUser) { |
| 236 | + printError('Not logged into NPM. Please run "npm login" first.'); |
| 237 | + process.exit(1); |
| 238 | + } |
| 239 | + printSuccess(`NPM user: ${npmUser}`); |
| 240 | + |
| 241 | + if (options.dryRun) { |
| 242 | + printWarning('Would publish to NPM: npm publish'); |
| 243 | + printWarning('Skipping NPM publish (dry run)'); |
| 244 | + } else { |
| 245 | + exec('npm publish'); |
| 246 | + printSuccess('Published to NPM'); |
| 247 | + |
| 248 | + // Verify NPM publication |
| 249 | + await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds |
| 250 | + const npmVersion = execSilent('npm view @wonderwhy-er/desktop-commander version', { ignoreError: true }).trim(); |
| 251 | + if (npmVersion === newVersion) { |
| 252 | + printSuccess(`NPM publication verified: v${npmVersion}`); |
| 253 | + } else { |
| 254 | + printWarning(`NPM version mismatch: expected ${newVersion}, got ${npmVersion} (may take a moment to propagate)`); |
| 255 | + } |
| 256 | + } |
| 257 | + console.log(''); |
| 258 | + |
| 259 | + // Step 7: Publish to MCP Registry |
| 260 | + printStep('Step 7/7: Publishing to MCP Registry...'); |
| 261 | + |
| 262 | + // Check if mcp-publisher is installed |
| 263 | + const hasMcpPublisher = execSilent('mcp-publisher --version', { ignoreError: true }); |
| 264 | + if (!hasMcpPublisher) { |
| 265 | + printError('mcp-publisher not found. Install it with: brew install mcp-publisher'); |
| 266 | + process.exit(1); |
| 267 | + } |
| 268 | + |
| 269 | + if (options.dryRun) { |
| 270 | + printWarning('Would publish to MCP Registry: mcp-publisher publish'); |
| 271 | + printWarning('Skipping MCP Registry publish (dry run)'); |
| 272 | + } else { |
| 273 | + exec('mcp-publisher publish'); |
| 274 | + printSuccess('Published to MCP Registry'); |
| 275 | + |
| 276 | + // Verify MCP Registry publication |
| 277 | + await new Promise(resolve => setTimeout(resolve, 3000)); // Wait 3 seconds |
| 278 | + try { |
| 279 | + const mcpResponse = execSilent('curl -s "https://registry.modelcontextprotocol.io/v0/servers?search=io.github.wonderwhy-er/desktop-commander"'); |
| 280 | + const mcpData = JSON.parse(mcpResponse); |
| 281 | + const mcpVersion = mcpData.servers?.[0]?.version || 'unknown'; |
| 282 | + |
| 283 | + if (mcpVersion === newVersion) { |
| 284 | + printSuccess(`MCP Registry publication verified: v${mcpVersion}`); |
| 285 | + } else { |
| 286 | + printWarning(`MCP Registry version: ${mcpVersion} (expected ${newVersion}, may take a moment to propagate)`); |
| 287 | + } |
| 288 | + } catch (error) { |
| 289 | + printWarning('Could not verify MCP Registry publication'); |
| 290 | + } |
| 291 | + } |
| 292 | + console.log(''); |
| 293 | + |
| 294 | + // Success summary |
| 295 | + console.log('╔══════════════════════════════════════════════════════════╗'); |
| 296 | + console.log('║ 🎉 Release Complete! 🎉 ║'); |
| 297 | + console.log('╚══════════════════════════════════════════════════════════╝'); |
| 298 | + console.log(''); |
| 299 | + printSuccess(`Version: ${newVersion}`); |
| 300 | + printSuccess('NPM: https://www.npmjs.com/package/@wonderwhy-er/desktop-commander'); |
| 301 | + printSuccess('MCP Registry: https://registry.modelcontextprotocol.io/'); |
| 302 | + printSuccess(`GitHub Tag: https://github.com/wonderwhy-er/DesktopCommanderMCP/releases/tag/${tagName}`); |
| 303 | + console.log(''); |
| 304 | + console.log('Next steps:'); |
| 305 | + console.log(` 1. Create GitHub release at: https://github.com/wonderwhy-er/DesktopCommanderMCP/releases/new?tag=${tagName}`); |
| 306 | + console.log(' 2. Add release notes with features and fixes'); |
| 307 | + console.log(' 3. Announce on Discord'); |
| 308 | + console.log(''); |
| 309 | + |
| 310 | + if (options.dryRun) { |
| 311 | + console.log(''); |
| 312 | + printWarning('This was a DRY RUN - no changes were published'); |
| 313 | + printWarning('Run without --dry-run to perform the actual release'); |
| 314 | + console.log(''); |
| 315 | + } |
| 316 | + |
| 317 | + } catch (error) { |
| 318 | + console.log(''); |
| 319 | + printError('Release failed!'); |
| 320 | + printError(error.message); |
| 321 | + process.exit(1); |
| 322 | + } |
| 323 | +} |
| 324 | + |
| 325 | +// Run the script |
| 326 | +publishRelease().catch(error => { |
| 327 | + printError('Unexpected error:'); |
| 328 | + console.error(error); |
| 329 | + process.exit(1); |
| 330 | +}); |
0 commit comments