UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

577 lines (473 loc) 21.7 kB
/** * Optimizely Project JavaScript Sync Tool * * This script provides two main features: * 1. INITIALIZATION: Adds comment delimiters to existing projects to prepare them for syncing * 2. SYNCHRONIZATION: Updates shared JavaScript code across multiple Optimizely projects while preserving project-specific code * * The script uses comment delimiters to identify shared code: * - Shared code is wrapped between // OPTIMIZELY_SHARED_CODE_START and // OPTIMIZELY_SHARED_CODE_END */ // Configuration - Update these values before running const config = { // Required: Your Optimizely API token apiToken: 'YOUR_OPTIMIZELY_API_TOKEN', // API endpoint (shouldn't need to change) baseUrl: 'https://api.optimizely.com/v2', // Delimiters to identify shared code sections (can customize if needed) sharedCodeStartDelimiter: '// OPTIMIZELY_SHARED_CODE_START', sharedCodeEndDelimiter: '// OPTIMIZELY_SHARED_CODE_END', // Initial placeholder text when initializing projects initialSharedCodePlaceholder: '// Shared code managed by automation - will be populated by update script', // Optional: Filter projects by name pattern (e.g., only sync projects containing "production") projectFilter: null, // Set to a regex pattern like /production/i or null to process all projects // NEW: Skip archived projects during sync skipArchivedProjects: true, // Set to true to skip archived projects // NEW: Specific project IDs to sync (empty array = all projects) targetProjectIds: [], // e.g., ['12345', '67890'] to sync only these projects // NEW: Enable detailed comparison logging for debugging verboseComparison: false, // Set to true to debug comparison issues // For SYNC mode, set ONE of these: referenceProjectId: null, // ID of the reference project to extract shared code from sharedCodeContent: null, // Direct string content to use as shared code // Set to true to only log what would happen without making actual update calls (for testing) dryRun: true }; // Helper function to make API requests async function makeApiRequest(endpoint, method = 'GET', data = null) { const url = `${config.baseUrl}${endpoint}`; const headers = { 'Authorization': `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' }; const options = { method, headers }; if (data && (method === 'POST' || method === 'PATCH' || method === 'PUT')) { options.body = JSON.stringify(data); } try { console.log(`Making API call: ${method} ${url}`); if (config.dryRun && (method === 'POST' || method === 'PATCH' || method === 'PUT')) { console.log(`DRY RUN: Would make ${method} request to ${url} with data:`, data); return { dryRun: true, success: true }; } const response = await fetch(url, options); if (!response.ok) { const errorText = await response.text(); throw new Error(`API request failed: ${response.status} ${response.statusText} - ${errorText}`); } // Check if the response has content const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return await response.json(); } else { return await response.text(); } } catch (error) { console.error('API request error:', error); throw error; } } // Get all projects async function listProjects(includeArchived = false) { console.log('Fetching projects...'); let projects = await makeApiRequest('/projects'); // Filter out archived projects if configured if (config.skipArchivedProjects && !includeArchived) { const originalCount = projects.length; projects = projects.filter(project => { // Check various possible fields for archived status return !project.archived && project.status !== 'archived'; }); if (originalCount !== projects.length) { console.log(`Filtered out ${originalCount - projects.length} archived projects.`); } } // Apply name filter if configured if (config.projectFilter) { const beforeFilterCount = projects.length; projects = projects.filter(project => config.projectFilter.test(project.name)); console.log(`Found ${projects.length} projects matching name filter out of ${beforeFilterCount} non-archived projects.`); } // Apply ID filter if configured if (config.targetProjectIds && config.targetProjectIds.length > 0) { const beforeFilterCount = projects.length; projects = projects.filter(project => config.targetProjectIds.includes(project.id.toString())); console.log(`Found ${projects.length} projects matching ID filter out of ${beforeFilterCount} projects.`); } console.log(`Total projects to process: ${projects.length}`); return projects; } // Get project details by ID async function getProject(projectId) { console.log(`Fetching project ${projectId}...`); return await makeApiRequest(`/projects/${projectId}`); } // Update project with new JavaScript async function updateProject(projectId, projectData) { console.log(`Updating project ${projectId}...`); return await makeApiRequest(`/projects/${projectId}`, 'PATCH', projectData); } // Helper function to escape special characters in regex function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } // Helper function to normalize strings for comparison function normalizeString(str) { if (!str) return ''; // Normalize line endings (CRLF -> LF) let normalized = str.replace(/\r\n/g, '\n'); // Trim trailing whitespace from each line normalized = normalized.split('\n').map(line => line.trimEnd()).join('\n'); // Trim leading/trailing whitespace from entire string normalized = normalized.trim(); return normalized; } // Extract shared code sections using delimiters function extractCodeSections(javascript) { // If javascript is null or undefined, return empty if (!javascript) { return { sharedCodeSections: [], hasSharedCodeDelimiters: false }; } const sharedCodeRegex = new RegExp( `${escapeRegExp(config.sharedCodeStartDelimiter)}([\\s\\S]*?)${escapeRegExp(config.sharedCodeEndDelimiter)}`, 'g' ); const sharedCodeMatches = [...(javascript.matchAll(sharedCodeRegex) || [])]; // Extract all shared code sections const sharedCodeSections = sharedCodeMatches.map(match => ({ fullMatch: match[0], content: match[1], startIndex: match.index, endIndex: match.index + match[0].length })); // Check if delimiters exist const hasSharedCodeDelimiters = sharedCodeMatches.length > 0; return { sharedCodeSections, hasSharedCodeDelimiters }; } // Initialize project JavaScript with shared code markers function initializeProjectJavaScript(projectJavaScript) { // If the JavaScript is null or undefined, initialize with an empty string projectJavaScript = projectJavaScript || ''; // Check if markers already exist const { hasSharedCodeDelimiters } = extractCodeSections(projectJavaScript); if (hasSharedCodeDelimiters) { console.log('Project already has shared code markers. No initialization needed.'); return null; } // Construct the new JS content with markers wrapping an empty shared block, // followed by the original content const newJavaScriptContent = config.sharedCodeStartDelimiter + '\n' + config.initialSharedCodePlaceholder + '\n' + config.sharedCodeEndDelimiter + // Add separation only if original JS exists (projectJavaScript.trim() ? '\n\n// --- Original Project Specific Code Below ---\n' + projectJavaScript : ''); return newJavaScriptContent; } // Update a project's JavaScript with new shared code function updateProjectJavaScript(projectJavaScript, latestSharedCode) { const { sharedCodeSections, hasSharedCodeDelimiters } = extractCodeSections(projectJavaScript); if (!hasSharedCodeDelimiters) { console.warn('No shared code sections found in project JavaScript. Project needs initialization first.'); return null; } // Replace each shared code section with the latest shared code let newJavaScript = projectJavaScript; let offset = 0; for (const section of sharedCodeSections) { const updatedSection = `${config.sharedCodeStartDelimiter} ${latestSharedCode} ${config.sharedCodeEndDelimiter}`; const startIndex = section.startIndex + offset; const endIndex = section.endIndex + offset; newJavaScript = newJavaScript.substring(0, startIndex) + updatedSection + newJavaScript.substring(endIndex); // Update offset for subsequent replacements offset += updatedSection.length - (endIndex - startIndex); } return newJavaScript; } // Get shared code from reference project or direct content async function getLatestSharedCode() { // First check if direct content is provided if (config.sharedCodeContent) { console.log('Using provided shared code content.'); return config.sharedCodeContent; } // Then check if reference project ID is provided else if (config.referenceProjectId) { console.log(`Getting shared code from reference project: ${config.referenceProjectId}`); const referenceProject = await getProject(config.referenceProjectId); if (!referenceProject.javascript) { throw new Error(`Reference project ${config.referenceProjectId} does not have any JavaScript.`); } const { sharedCodeSections, hasSharedCodeDelimiters } = extractCodeSections(referenceProject.javascript); if (!hasSharedCodeDelimiters) { throw new Error(`Reference project ${config.referenceProjectId} does not have shared code delimiters.`); } // Take the first shared code section if (sharedCodeSections.length > 0) { return sharedCodeSections[0].content; } else { throw new Error(`No shared code found in reference project ${config.referenceProjectId}.`); } } else { throw new Error('Neither sharedCodeContent nor referenceProjectId is configured. One is required for sync.'); } } // Main function to initialize projects async function initializeProjects(targetProjectIds = []) { try { console.log('Starting Optimizely Project JavaScript Initialization...'); if (config.dryRun) { console.warn('\n*** DRY RUN MODE ENABLED: No actual updates will be performed. ***\n'); } if (!config.apiToken || config.apiToken === 'YOUR_OPTIMIZELY_API_TOKEN') { throw new Error('API Token is not set. Please configure apiToken in the config object.'); } // Get all projects or use provided list let projectIdsToProcess = targetProjectIds; if (!projectIdsToProcess || projectIdsToProcess.length === 0) { console.log('Fetching all projects...'); const projects = await listProjects(); projectIdsToProcess = projects.map(p => p.id); console.log(`Found ${projectIdsToProcess.length} projects.`); } else { console.log(`Processing specific projects: ${projectIdsToProcess.join(', ')}`); } // Process each project let successCount = 0; let skipCount = 0; let failCount = 0; for (const projectId of projectIdsToProcess) { try { console.log(`\n--- Processing Project ID: ${projectId} ---`); // Get project details const projectDetails = await getProject(projectId); // Initialize project JavaScript if needed const updatedJavaScript = initializeProjectJavaScript(projectDetails.javascript); // Skip if already initialized if (updatedJavaScript === null) { console.log(`⏭️ Project ${projectId} - ${projectDetails.name} already has markers. Skipping.`); skipCount++; continue; } // Update the project await updateProject(projectId, { javascript: updatedJavaScript }); console.log(`✅ Project ${projectId} - ${projectDetails.name} initialized successfully`); successCount++; } catch (error) { console.error(`❌ Error processing project ${projectId}:`, error); failCount++; } } console.log('\nInitialization Summary:'); console.log(`Total projects: ${projectIdsToProcess.length}`); console.log(`Successfully initialized: ${successCount}`); console.log(`Already initialized (skipped): ${skipCount}`); console.log(`Failed: ${failCount}`); } catch (error) { console.error('An error occurred during initialization:', error); } } // Main function to sync projects async function syncProjects(targetProjectIds = []) { try { console.log('Starting Optimizely Project JavaScript Sync...'); if (config.dryRun) { console.warn('\n*** DRY RUN MODE ENABLED: No actual updates will be performed. ***\n'); } if (!config.apiToken || config.apiToken === 'YOUR_OPTIMIZELY_API_TOKEN') { throw new Error('API Token is not set. Please configure apiToken in the config object.'); } // Get projects - either specific ones or all based on filters let projects; if (targetProjectIds && targetProjectIds.length > 0) { // Override config with command line project IDs const originalTargetIds = config.targetProjectIds; config.targetProjectIds = targetProjectIds; projects = await listProjects(); config.targetProjectIds = originalTargetIds; // Restore original config } else { projects = await listProjects(); } if (projects.length === 0) { console.log('No projects found to sync based on current filters.'); return; } // Get the latest shared code const latestSharedCode = await getLatestSharedCode(); console.log('Latest shared code retrieved successfully.'); // Process each project let successCount = 0; let notInitializedCount = 0; let noChangeCount = 0; let failCount = 0; let archivedSkipCount = 0; for (const project of projects) { try { console.log(`\nProcessing project: ${project.id} - ${project.name}`); // Get full project details const projectDetails = await getProject(project.id); // Double-check archived status with full details if (config.skipArchivedProjects && (projectDetails.archived || projectDetails.status === 'archived')) { console.log(`⏭️ Skipping archived project ${project.id} - ${project.name}`); archivedSkipCount++; continue; } // Skip projects without JavaScript if (!projectDetails.javascript) { console.log(`⏭️ Skipping project ${project.id} - No JavaScript found`); continue; } // Update the project's JavaScript const updatedJavaScript = updateProjectJavaScript(projectDetails.javascript, latestSharedCode); // Skip if project is not initialized if (updatedJavaScript === null) { console.log(`⚠️ Project ${project.id} - ${project.name} is not initialized with markers. Run initialization first.`); notInitializedCount++; continue; } // Normalize both strings for comparison const normalizedOriginal = normalizeString(projectDetails.javascript); const normalizedUpdated = normalizeString(updatedJavaScript); // Log verbose comparison info if enabled if (config.verboseComparison) { console.log(`Comparison details for project ${project.id}:`); console.log(` Original length: ${projectDetails.javascript.length} -> Normalized: ${normalizedOriginal.length}`); console.log(` Updated length: ${updatedJavaScript.length} -> Normalized: ${normalizedUpdated.length}`); console.log(` Strings equal: ${normalizedOriginal === normalizedUpdated}`); } // Only update if the normalized JavaScript has changed if (normalizedOriginal !== normalizedUpdated) { await updateProject(project.id, { javascript: updatedJavaScript }); console.log(`✅ Project ${project.id} - ${project.name} updated successfully`); successCount++; } else { console.log(`⏭️ Project ${project.id} - ${project.name} already has the latest shared code`); noChangeCount++; } } catch (error) { console.error(`❌ Error processing project ${project.id} - ${project.name}:`, error); failCount++; } } console.log('\n========================================'); console.log('Sync Summary:'); console.log(`Total projects processed: ${projects.length}`); console.log(`Successfully updated: ${successCount}`); console.log(`No changes needed: ${noChangeCount}`); console.log(`Not initialized (skipped): ${notInitializedCount}`); if (archivedSkipCount > 0) { console.log(`Archived projects (skipped): ${archivedSkipCount}`); } console.log(`Failed: ${failCount}`); console.log('========================================\n'); } catch (error) { console.error('An error occurred during sync:', error); } } // Function to handle command line arguments and run the appropriate mode function runScript() { // Default mode is sync let mode = 'sync'; let targetProjectIds = []; // Simple command line args parser const args = process.argv.slice(2); for (let i = 0; i < args.length; i++) { const arg = args[i]; if (arg === '--init' || arg === '--initialize') { mode = 'initialize'; } else if (arg === '--sync') { mode = 'sync'; } else if (arg === '--token' && i + 1 < args.length) { config.apiToken = args[i + 1]; i++; // Skip the next arg which is the token value } else if (arg === '--ref' && i + 1 < args.length) { config.referenceProjectId = args[i + 1]; i++; // Skip the next arg which is the project ID } else if (arg === '--dry-run') { config.dryRun = true; } else if (arg === '--live') { config.dryRun = false; } else if (arg === '--project' && i + 1 < args.length) { targetProjectIds.push(args[i + 1]); i++; // Skip the next arg which is the project ID } else if (arg === '--include-archived') { config.skipArchivedProjects = false; } else if (arg === '--skip-archived') { config.skipArchivedProjects = true; } else if (arg === '--verbose-compare') { config.verboseComparison = true; } else if (arg === '--help') { showHelp(); return; } } // Run the appropriate mode if (mode === 'initialize') { initializeProjects(targetProjectIds); } else if (mode === 'sync') { syncProjects(targetProjectIds); // Pass project IDs to sync as well } } // Help text for command line usage function showHelp() { console.log(` Optimizely Project JavaScript Sync Tool Usage: node optimizely-sync.js [options] Options: --help Show this help text --init Run in initialization mode (add delimiters to projects) --sync Run in sync mode (update shared code across projects) --token <token> Set the Optimizely API token --ref <projectId> Set the reference project ID for sync mode --project <id> Target specific project ID (can use multiple times for both init and sync) --dry-run Run without making any actual updates (default) --live Make actual updates to projects --include-archived Include archived projects (default: skip archived) --skip-archived Skip archived projects (default behavior) --verbose-compare Show detailed comparison info for debugging Examples: # Initialize all projects (dry run) node optimizely-sync.js --init --token YOUR_API_TOKEN # Initialize specific projects (live run) node optimizely-sync.js --init --token YOUR_API_TOKEN --project 12345 --project 67890 --live # Sync shared code from reference project to all non-archived projects (dry run) node optimizely-sync.js --sync --token YOUR_API_TOKEN --ref 12345 # Sync shared code to specific projects only (live run) node optimizely-sync.js --sync --token YOUR_API_TOKEN --ref 12345 --project 11111 --project 22222 --live # Sync including archived projects (live run) node optimizely-sync.js --sync --token YOUR_API_TOKEN --ref 12345 --include-archived --live # Debug comparison issues node optimizely-sync.js --sync --token YOUR_API_TOKEN --ref 12345 --verbose-compare `); } // Check if being run directly (not imported) if (typeof require !== 'undefined' && require.main === module) { runScript(); } // Export functions for modular usage if (typeof module !== 'undefined' && module.exports) { module.exports = { initializeProjects, syncProjects, listProjects, getProject, updateProject, extractCodeSections, updateProjectJavaScript, initializeProjectJavaScript, getLatestSharedCode, normalizeString, config }; }