Skip to main content
Glama

DollhouseMCP

by DollhouseMCP
update-version.mjsโ€ข14 kB
#!/usr/bin/env node /** * Smart Version Update Script * * Updates version numbers throughout the codebase while preserving: * - Version history sections * - Changelog entries * - Example/documentation that shouldn't change * * Usage: * npm run update:version -- 1.6.5 * npm run update:version -- 1.6.5 --notes "Added portfolio sync fix" * npm run update:version -- 1.6.5 --dry-run */ import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; import { glob } from 'glob'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Parse command line arguments const args = process.argv.slice(2); const newVersion = args[0]; const isDryRun = args.includes('--dry-run'); const notesIndex = args.indexOf('--notes'); const releaseNotes = notesIndex !== -1 && args[notesIndex + 1] ? args[notesIndex + 1] : ''; // Security: Validate release notes length to prevent injection attacks if (releaseNotes.length > 1000) { console.error('โŒ Release notes too long (max 1000 characters)'); process.exit(1); } if (!newVersion || !newVersion.match(/^\d+\.\d+\.\d+(-[\w\.]+)?$/)) { console.error('โŒ Please provide a valid semantic version (e.g., 1.6.5 or 1.6.5-beta.1)'); console.error('Usage: npm run update:version -- <version> [--notes "Release notes"] [--dry-run]'); process.exit(1); } // Get current version from package.json const packageJsonPath = path.join(path.dirname(__dirname), 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); const currentVersion = packageJson.version; if (currentVersion === newVersion) { console.log(`โ„น๏ธ Version is already ${newVersion}`); process.exit(0); } // Helper function to escape special regex characters function escapeRegExp(string) { // Escape all special regex characters including backslashes return string.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&'); } console.log(`\n๐Ÿ”„ Updating version from ${currentVersion} to ${newVersion}`); if (isDryRun) { console.log('๐Ÿงช DRY RUN MODE - No files will be changed\n'); } // Configuration for files to update const updateConfigs = [ { name: 'package.json', pattern: /"version":\s*"[^"]+"/, replacement: `"version": "${newVersion}"`, required: true }, { name: 'package-lock.json', updates: [ { // Only update root package version in first occurrence after package name pattern: /("name":\s*"@dollhousemcp\/mcp-server",[\s\S]*?"version":\s*")[^"]+"/, replacement: `$1${newVersion}"`, once: true }, { // Update packages section if it exists pattern: /("packages":\s*{[\s\S]*?"node_modules\/@dollhousemcp\/mcp-server":\s*{[\s\S]*?"version":\s*")[^"]+"/, replacement: `$1${newVersion}"`, once: true } ], required: true }, { name: 'README.md', updates: [ { // Update version badge pattern: /\[Version-v[\d\.]+(-[\w\.]+)?-/g, replacement: `[Version-v${newVersion}-` }, { // Update current version mentions (but not in history/changelog) pattern: new RegExp(`\\bv?${escapeRegExp(currentVersion)}\\b`, 'g'), replacement: (match) => match.startsWith('v') ? `v${newVersion}` : newVersion, // Skip if line contains certain keywords skipLines: /changelog|history|previous|released|was |fixed in|since|before|after|from [\d\.]+ to|migrat|v[\d\.]+ \(/i }, { // Update installation instructions pattern: /@dollhousemcp\/mcp-server@[\d\.]+(-[\w\.]+)?/g, replacement: `@dollhousemcp/mcp-server@${newVersion}` } ] }, { name: 'CHANGELOG.md', updates: [ { // Add new version entry at the top (after the header) pattern: /(# Changelog\n+)/, replacement: (match, p1, offset, string) => { // Check if version already exists if (string.includes(`## [${newVersion}]`)) { console.log(' โญ๏ธ Version already exists in CHANGELOG.md'); return match; // Don't add duplicate } return `${p1}## [${newVersion}] - ${new Date().toISOString().split('T')[0]}${releaseNotes ? `\n\n${releaseNotes}` : '\n\n- Version bump'}\n\n`; }, once: true // Only replace first occurrence } ] }, { name: 'docs/**/*.md', glob: true, updates: [ { // Update version references in documentation pattern: new RegExp(`(?:Version|v)\\s*${escapeRegExp(currentVersion)}\\b`, 'g'), replacement: (match) => match.replace(currentVersion, newVersion), // Skip historical references skipLines: /changelog|history|previous|released|deprecated|legacy|old version|upgrade from|PR #|Issue #|commit|merged/i } ] }, { name: 'docker-compose.yml', pattern: new RegExp(`dollhousemcp/mcp-server:${escapeRegExp(currentVersion)}`, 'g'), replacement: `dollhousemcp/mcp-server:${newVersion}`, optional: true // Don't fail if file doesn't exist }, { name: 'Dockerfile', updates: [ { // Update LABEL version pattern: /LABEL version="[\d\.]+(-[\w\.]+)?"/, replacement: `LABEL version="${newVersion}"` } ], optional: true } ]; // Security: Helper function to validate file paths and prevent directory traversal function validateFilePath(filePath, basePath) { const normalizedPath = path.normalize(filePath); // Check for path traversal attempts if (normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) { throw new Error(`Path traversal detected: ${filePath}`); } const resolved = path.resolve(basePath, normalizedPath); const resolvedBase = path.resolve(basePath); // Ensure the resolved path is within the base path if (!resolved.startsWith(resolvedBase)) { throw new Error(`Path traversal detected: ${filePath}`); } return resolved; } // Helper function to update a single file function updateFile(filePath, config) { try { // Security: Validate path to prevent directory traversal const basePath = path.dirname(__dirname); const fullPath = validateFilePath(filePath, basePath); if (!fs.existsSync(fullPath)) { if (config.createIfMissing && config.defaultContent) { if (!isDryRun) { // Create directory if needed const dir = path.dirname(fullPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(fullPath, config.defaultContent); } console.log(` โœ… Created ${filePath}`); return true; } else if (config.optional) { return false; } else if (config.required) { console.error(` โŒ Required file not found: ${filePath}`); return false; } return false; } // Security: Check file size to prevent DoS attacks const stats = fs.statSync(fullPath); const maxFileSize = 10 * 1024 * 1024; // 10MB limit if (stats.size > maxFileSize) { console.error(` โŒ File too large: ${filePath} (${Math.round(stats.size / 1024 / 1024)}MB, max 10MB)`); return false; } let content = fs.readFileSync(fullPath, 'utf-8'); const originalContent = content; let changesMade = false; if (config.updates) { // Multiple update patterns for this file for (const update of config.updates) { if (update.skipLines) { // Process line by line for selective updates const lines = content.split('\n'); const updatedLines = lines.map(line => { if (update.skipLines.test(line)) { return line; // Skip this line } if (typeof update.replacement === 'function') { return line.replace(update.pattern, update.replacement); } return line.replace(update.pattern, update.replacement); }); content = updatedLines.join('\n'); } else if (update.requireContext) { // Only replace if context matches const lines = content.split('\n'); const updatedLines = lines.map(line => { if (update.requireContext.test(line)) { return line.replace(update.pattern, update.replacement); } return line; }); content = updatedLines.join('\n'); } else if (update.once) { // Replace only first occurrence content = content.replace(update.pattern, update.replacement); } else { // Standard replacement (works for both function and string replacements) content = content.replace(update.pattern, update.replacement); } } } else if (config.pattern) { // Single pattern update if (config.multiple) { content = content.replace(new RegExp(config.pattern, 'g'), config.replacement); } else { content = content.replace(config.pattern, config.replacement); } } changesMade = content !== originalContent; if (changesMade) { if (!isDryRun) { fs.writeFileSync(fullPath, content); } console.log(` โœ… Updated ${filePath}`); return true; } else { console.log(` โญ๏ธ No changes needed in ${filePath}`); return false; } } catch (error) { console.error(` โŒ Error updating ${filePath}: ${error.message}`); return false; } } // Helper function to handle glob patterns async function handleGlobPattern(pattern, config) { const basePath = path.dirname(__dirname); // Security: Validate pattern to prevent directory traversal const normalizedPattern = path.normalize(pattern); if (normalizedPattern.includes('..') || path.isAbsolute(normalizedPattern)) { throw new Error(`Path traversal detected in pattern: ${pattern}`); } const fullPattern = path.join(basePath, normalizedPattern); // Get files matching the pattern const files = await glob(fullPattern); // Security: Limit number of files to prevent DoS if (files.length > 1000) { console.error(`โŒ Too many files matched (${files.length}). Maximum is 1000.`); process.exit(1); } let updated = 0; for (const file of files) { const relativePath = path.relative(basePath, file); if (updateFile(relativePath, config)) { updated++; } } return updated; } // Main update process async function main() { let totalUpdated = 0; let totalErrors = 0; for (const config of updateConfigs) { if (config.glob) { const updated = await handleGlobPattern(config.name, config); totalUpdated += updated; } else { if (updateFile(config.name, config)) { totalUpdated++; } else if (config.required && !fs.existsSync(path.join(path.dirname(__dirname), config.name))) { totalErrors++; } } } console.log('\n' + '='.repeat(60)); if (totalErrors > 0) { console.log(`โŒ Version update failed with ${totalErrors} errors`); process.exit(1); } if (isDryRun) { console.log(`๐Ÿงช DRY RUN COMPLETE: Would update ${totalUpdated} files`); console.log(`Run without --dry-run to apply changes`); } else { console.log(`โœ… Version updated to ${newVersion} in ${totalUpdated} files`); // Update package-lock.json properly using npm console.log('\n๐Ÿ“ฆ Updating package-lock.json with npm...'); try { // Security: Use resolved path for cwd to ensure we're in the right directory const projectRoot = path.resolve(path.dirname(__dirname)); execSync('npm install --package-lock-only', { stdio: 'inherit', cwd: projectRoot }); } catch (error) { console.error('โš ๏ธ Failed to update package-lock.json with npm:', error.message); // Don't continue silently - this is important for version consistency process.exit(1); } // Generate version file if script exists const versionScriptPath = path.join(path.dirname(__dirname), 'scripts/generate-version.js'); if (fs.existsSync(versionScriptPath)) { console.log('\n๐Ÿ—๏ธ Generating version info...'); try { // Security: Use resolved path for cwd const projectRoot = path.resolve(path.dirname(__dirname)); execSync('node scripts/generate-version.js', { stdio: 'inherit', cwd: projectRoot }); } catch (error) { console.error('โš ๏ธ Failed to generate version info:', error.message); // Non-critical, so we can continue } } // Build README files if build script exists const readmeBuildScriptPath = path.join(path.dirname(__dirname), 'scripts/build-readme.js'); if (fs.existsSync(readmeBuildScriptPath)) { console.log('\n๐Ÿ“š Building modular README files...'); try { // Security: Use resolved path for cwd const projectRoot = path.resolve(path.dirname(__dirname)); // Build both NPM and GitHub versions execSync('npm run build:readme', { stdio: 'inherit', cwd: projectRoot }); console.log(' โœ… README files built successfully'); } catch (error) { console.error('โš ๏ธ Failed to build README files:', error.message); // Non-critical, so we can continue but warn console.log(' โ„น๏ธ You may need to run "npm run build:readme" manually'); } } console.log('\n๐Ÿ“ Next steps:'); console.log(` 1. Review the changes: git diff`); console.log(` 2. Update CHANGELOG.md with detailed release notes`); console.log(` 3. Commit: git add -A && git commit -m "chore: bump version to ${newVersion}"`); console.log(` 4. Tag the release: git tag v${newVersion}`); console.log(` 5. Push: git push && git push --tags`); } } // Run the main process main().catch(error => { console.error('โŒ Fatal error:', error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DollhouseMCP/DollhouseMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server