UNPKG

@simonecoelhosfo/optimizely-mcp-server

Version:

Optimizely MCP Server for AI assistants with integrated CLI tools

1,190 lines โ€ข 69.3 kB
/** * Template Generator - Interactive template creation */ import * as fs from 'fs'; import * as path from 'path'; import inquirer from 'inquirer'; import chalk from 'chalk'; import { OrchestrationRulesLayer } from './OrchestrationRulesLayer'; import { TemplateStore } from '../../orchestration/storage/TemplateStore'; export class TemplateGenerator { rulesLayer; constructor() { this.rulesLayer = new OrchestrationRulesLayer(); } /** * Generate a new template */ async generate(options = {}) { const platform = options.platform || 'web'; if (options.pattern) { const template = await this.fromPattern(options.pattern, platform, options.platformExplicit); // Allow customization of pattern-based templates unless explicitly disabled if (options.customize !== false) { return this.customizeTemplate(template, platform); } return template; } if (options.interactive) { return this.interactiveWizard(platform); } // Default: basic template structure return this.createBasicTemplate(platform); } /** * Create from a pattern */ async fromPattern(pattern, platform, platformExplicit) { const patterns = this.getAvailablePatterns(); const selectedPattern = patterns[pattern]; if (!selectedPattern) { throw new Error(`Unknown pattern: ${pattern}. Available: ${Object.keys(patterns).join(', ')}`); } // For platform-agnostic patterns, ask which platform to use if not explicitly set const platformAgnosticPatterns = ['full-stack', 'e-commerce', 'event-tracking', 'audience-segmentation']; // Only ask if platform wasn't explicitly set via --platform flag if (platformAgnosticPatterns.includes(pattern) && !platformExplicit) { try { const { selectedPlatform } = await inquirer.prompt([ { type: 'list', name: 'selectedPlatform', message: 'Which platform are you building for?', choices: [ { name: '๐Ÿšฉ Feature Experimentation (Flags, Rules, Variables)', value: 'feature' }, { name: '๐Ÿงช Web Experimentation (A/B Tests, Campaigns, Pages)', value: 'web' } ], default: platform } ]); platform = selectedPlatform; } catch (error) { // If prompt fails, use the default platform console.log(`Using ${platform} platform...`); } } return selectedPattern.generate(platform); } /** * Interactive wizard - Guided template creation */ async interactiveWizard(platform) { // Skip TTY check - all our environments support TTY // The check was failing incorrectly when run through npm scripts console.log('\n๐Ÿš€ Welcome to the Optimizely Template Creator!\n'); try { // Step 1: Basic template information const templateInfo = await inquirer.prompt([ { type: 'input', name: 'description', message: 'Enter a description for your template:', default: `${platform === 'web' ? 'Web' : 'Feature'} Experimentation Template` }, { type: 'list', name: 'goal', message: 'What would you like to create?', choices: [ { name: '๐Ÿงช A/B Test', value: 'ab-test' }, { name: '๐Ÿšฉ Feature Flag with Rollout', value: 'feature-flag' }, { name: '๐ŸŽฏ Audience-based Experiment', value: 'audience-experiment' }, { name: '๐Ÿ—๏ธ Custom Template', value: 'custom' } ] } ]); // Initialize template const template = { version: '1.0', description: templateInfo.description, parameters: [ { name: 'project_id', type: 'integer', required: true, description: 'The project ID where entities will be created' } ], steps: [] }; // Handle different template goals switch (templateInfo.goal) { case 'ab-test': await this.buildABTestInteractive(template, platform); break; case 'feature-flag': await this.buildFeatureFlagInteractive(template); break; case 'audience-experiment': await this.buildAudienceExperimentInteractive(template, platform); break; case 'custom': await this.buildCustomInteractive(template, platform); break; } // Step: Review and confirmation const { confirmed } = await inquirer.prompt([ { type: 'confirm', name: 'confirmed', message: 'Template created! Would you like to review it?', default: true } ]); if (confirmed) { console.log('\n๐Ÿ“‹ Template Preview:\n'); console.log(JSON.stringify(template, null, 2)); } return template; } catch (error) { console.log('\nโš ๏ธ Interactive mode was interrupted or not available.'); console.log(' Use command-line options to customize:'); console.log(' --pattern <pattern-name>'); console.log(' --name "My Template"'); console.log(' --add-event "Event Name"'); console.log(' --experiment-name "Test Name" (web)'); console.log(' --flag-name "Flag Name" (feature)\n'); // Throw error to prevent saving an empty template throw new Error('Interactive mode interrupted'); } } /** * Customize a template with user input */ async customizeTemplate(template, platform) { try { console.log('\n๐Ÿ“ Customize your template:\n'); const customization = await inquirer.prompt([ { type: 'input', name: 'name', message: 'Template name:', default: template.description || 'My Template' }, { type: 'input', name: 'project_id', message: 'Default project ID (optional):', validate: (input) => { if (!input) return true; return !isNaN(parseInt(input)) || 'Must be a number'; } }, { type: 'confirm', name: 'customize_names', message: 'Would you like to customize entity names?', default: true } ]); // Update template description template.description = customization.name; // Update default project ID if provided if (customization.project_id) { const projectParam = template.parameters.find(p => p.name === 'project_id'); if (projectParam) { projectParam.default = parseInt(customization.project_id); } } // Customize entity names if requested if (customization.customize_names) { console.log('\n๐Ÿท๏ธ Customize entity names (press enter to keep defaults):\n'); for (const step of template.steps) { if (step.template?.inputs?.name) { const currentName = step.template.inputs.name; const entityType = step.id.replace('create_', '').replace(/_/g, ' '); const { newName } = await inquirer.prompt([ { type: 'input', name: 'newName', message: `${entityType}: `, default: currentName } ]); if (newName !== currentName) { step.template.inputs.name = newName; // Update key if it exists if (step.template.inputs.key) { step.template.inputs.key = newName.toLowerCase().replace(/\s+/g, '_'); } } } } } return template; } catch (error) { console.log('\nโš ๏ธ Using default values...'); return template; } } /** * Create a basic template structure */ createBasicTemplate(platform) { const template = { version: '1.0', description: 'Basic orchestration template', parameters: [ { name: 'project_id', type: 'integer', required: true, description: 'The project ID' } ], steps: [] }; // Add a default event to avoid empty template this.addCreateStep(template, 'event', 'create_default_event', { name: 'Default Event', key: 'default_event', description: 'Default tracking event' }); return template; } /** * Add a step to create an entity */ addCreateStep(template, entityType, stepId, inputs = {}) { const step = { id: stepId, name: `Create ${entityType}`, type: 'template', template: { system_template_id: `optimizely/${entityType}/create`, inputs: { project_id: '${project_id}', ...inputs } } }; template.steps.push(step); } /** * Get available patterns */ getAvailablePatterns() { return { // Basic patterns 'ab-test': { name: 'A/B Test', description: 'Create an A/B test with variations', generate: (platform) => this.createABTestTemplate(platform) }, 'feature-flag': { name: 'Feature Flag', description: 'Create a feature flag with rollout rules', generate: () => this.createFeatureFlagTemplate() }, 'campaign': { name: 'Campaign', description: 'Create a campaign with experiments', generate: () => this.createCampaignTemplate() }, // Advanced patterns 'personalization': { name: 'Personalization Campaign', description: 'Multi-experience personalization with audiences', generate: () => this.createPersonalizationTemplate() }, 'campaign-builder': { name: 'Campaign Builder', description: 'Build a custom campaign with multiple experiments interactively', generate: () => this.buildCampaignInteractive() }, 'feature-rollout': { name: 'Progressive Feature Rollout', description: 'Gradual feature rollout with percentage stages', generate: () => this.createProgressiveRolloutTemplate() }, 'targeted-delivery': { name: 'Targeted Delivery', description: 'Single variation delivery without metrics', generate: () => this.createTargetedDeliveryTemplate() }, 'multi-page-test': { name: 'Multi-Page Test', description: 'A/B test across multiple pages', generate: () => this.createMultiPageTestTemplate() }, 'multi-armed-bandit': { name: 'Multi-Armed Bandit', description: 'Auto-optimizing experiment', generate: () => this.createMABTemplate() }, // Complete setups 'full-stack': { name: 'Full Stack Setup', description: 'Complete setup with audiences, events, and experiments', generate: (platform) => this.createFullStackTemplate(platform) }, 'e-commerce': { name: 'E-commerce Optimization', description: 'Complete e-commerce testing setup', generate: () => this.createEcommerceTemplate() }, 'saas-onboarding': { name: 'SaaS Onboarding Flow', description: 'Optimize user onboarding experience', generate: () => this.createSaaSOnboardingTemplate() } }; } /** * Pattern: A/B Test Template */ createABTestTemplate(platform) { const template = this.createBasicTemplate(platform); template.description = 'A/B Test Template'; // Add event this.addCreateStep(template, 'event', 'create_event', { name: 'Conversion Event', key: 'conversion_event', description: 'Track conversions' }); if (platform === 'web') { // Web experiment this.addCreateStep(template, 'experiment', 'create_experiment', { name: 'A/B Test Experiment', key: 'ab_test_experiment', status: 'not_started', type: 'a/b', variations: [ { name: 'Control', key: 'control', weight: 5000 }, { name: 'Treatment', key: 'treatment', weight: 5000 } ], metrics: [ { event_id: '${create_event.id}', scope: 'visitor' } ] }); } else { // Feature experiment (ruleset) this.addCreateStep(template, 'flag', 'create_flag', { name: 'Test Feature Flag', key: 'test_feature_flag', description: 'Feature flag for A/B test', variations: [ { key: 'control', name: 'Control', value: 'false' }, { key: 'treatment', name: 'Treatment', value: 'true' } ] }); this.addCreateStep(template, 'ruleset', 'create_ruleset', { flag_key: '${create_flag.key}', type: 'a/b_test', rules: [ { key: 'ab_test_rule', variations: [ { variation_key: 'control', weight: 5000 }, { variation_key: 'treatment', weight: 5000 } ], metrics: [ { event_key: '${create_event.key}', scope: 'visitor' } ] } ] }); } return template; } /** * Pattern: Feature Flag Template */ createFeatureFlagTemplate() { const template = this.createBasicTemplate('feature'); template.description = 'Feature Flag Template'; this.addCreateStep(template, 'flag', 'create_flag', { name: 'New Feature Flag', key: 'new_feature_flag', description: 'Control feature availability', variations: [ { key: 'off', name: 'Off', value: 'false' }, { key: 'on', name: 'On', value: 'true' } ] }); this.addCreateStep(template, 'ruleset', 'create_rollout', { flag_key: '${create_flag.key}', type: 'rollout', rules: [ { key: 'rollout_rule', percentage_included: 0, deliver: 'off' } ] }); return template; } /** * Pattern: Campaign Template */ createCampaignTemplate() { const template = this.createBasicTemplate('web'); template.description = 'Campaign Template'; this.addCreateStep(template, 'campaign', 'create_campaign', { name: 'Marketing Campaign', holdback: 1000, metrics: { scope: 'session' // Required for campaigns! } }); return template; } /** * Pattern: Full Stack Template */ createFullStackTemplate(platform) { const template = this.createBasicTemplate(platform); template.description = 'Full Stack Setup'; // Create audience this.addCreateStep(template, 'audience', 'create_audience', { name: 'Beta Users', conditions: JSON.stringify([ 'and', ['or', ['custom_attribute', 'plan_type', 'equals', 'beta']] ]) }); // Create events this.addCreateStep(template, 'event', 'create_conversion_event', { name: 'Purchase Complete', key: 'purchase_complete', description: 'Track completed purchases' }); this.addCreateStep(template, 'event', 'create_metric_event', { name: 'Revenue', key: 'revenue', description: 'Track revenue amount' }); // Platform-specific setup if (platform === 'web') { // Web Experimentation: Pages, Experiments, Campaigns this.addCreateStep(template, 'page', 'create_page', { name: 'Checkout Page', edit_url: 'https://example.com/checkout', activation_type: 'immediate', activation_code: 'window.optimizely.push({"type": "page", "pageName": "checkout"});' }); this.addCreateStep(template, 'experiment', 'create_experiment', { name: 'Checkout Flow Test', status: 'not_started', type: 'a/b', audience_conditions: 'everyone', page_ids: ['${create_page.id}'], variations: [ { name: 'Original', weight: 5000 }, { name: 'Simplified', weight: 5000 } ], metrics: [ { event_id: '${create_conversion_event.id}', scope: 'visitor' } ] }); } else { // Feature Experimentation: Flags, Rules, Variables this.addCreateStep(template, 'flag', 'create_feature_flag', { name: 'Checkout Feature', key: 'checkout_feature', description: 'Controls checkout flow experience', variations: [ { key: 'off', name: 'Off', value: 'false' }, { key: 'on', name: 'On', value: 'true' } ] }); // Add variable definitions for configuration this.addCreateStep(template, 'variable_definition', 'create_config_variable', { flag_key: '${create_feature_flag.key}', name: 'checkout_config', type: 'json', default_value: JSON.stringify({ flow_type: 'multi-step', show_progress_bar: true, enable_guest_checkout: false }) }); // Add rollout rules this.addCreateStep(template, 'ruleset', 'create_rollout_rules', { flag_key: '${create_feature_flag.key}', type: 'rollout', rules: [ { key: 'beta_users_rule', audience_conditions: 'audience_ids=${create_audience.id}', percentage_included: 10000, // 100% of beta users deliver: 'on' }, { key: 'general_rollout', percentage_included: 1000, // 10% general rollout deliver: 'on' } ] }); // Add A/B test ruleset this.addCreateStep(template, 'ruleset', 'create_ab_test', { flag_key: '${create_feature_flag.key}', type: 'a/b_test', rules: [ { key: 'checkout_test', variations: [ { variation_key: 'off', weight: 5000 }, { variation_key: 'on', weight: 5000 } ], metrics: [ { event_key: '${create_conversion_event.key}' } ] } ] }); } return template; } /** * Save template to database and optionally to file */ async saveTemplate(cliTemplate, outputPath) { // Generate a proper template ID const templateId = `tpl_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; // Determine directory from env variable or use default const templateDir = process.env.ORCHESTRATION_TEMPLATES_DIR || path.join(process.cwd(), 'orchestration-templates'); // Determine file path let filePath; if (outputPath) { // If output path is absolute, use it as-is // If relative, put it in the templates directory filePath = path.isAbsolute(outputPath) ? outputPath : path.join(templateDir, outputPath); } else { // Default: save in templates directory with template ID as filename filePath = path.join(templateDir, `${templateId}.json`); } // Extract template name from description or file path let templateName = cliTemplate.description || 'Orchestration Template'; if (outputPath && !cliTemplate.description) { const fileName = path.basename(outputPath, '.json'); templateName = fileName.replace(/[-_]/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); } // Convert CLI template to proper OrchestrationTemplate format const orchestrationTemplate = { id: templateId, name: templateName, description: cliTemplate.description, version: cliTemplate.version || '1.0.0', type: 'user', platform: 'both', // Default to both, should be determined from template content author: process.env.USER || 'cli-user', // Convert parameters from array to object format parameters: cliTemplate.parameters.reduce((acc, param) => { acc[param.name] = { type: param.type, description: param.description || '', required: param.required, default: param.default }; return acc; }, {}), // Convert steps - ensure they have the right structure steps: cliTemplate.steps.map(step => ({ id: step.id, name: step.name, type: 'template', template: step.template ? { entity_type: step.template.entity_type || 'unknown', // Required for new format system_template_id: step.template.system_template_id, operation: 'create', // Default to create inputs: step.template.inputs } : undefined })) }; try { // Save to database const templateStore = new TemplateStore(); await templateStore.initialize(); await templateStore.createTemplate(orchestrationTemplate); console.log(`\nโœ… Template saved to database with ID: ${templateId}`); // Also save to file for reference const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } // Save the full template (with database metadata) to file const fullTemplate = { ...orchestrationTemplate, _metadata: { cli_generated: true, file_path: filePath, database_id: templateId } }; fs.writeFileSync(filePath, JSON.stringify(fullTemplate, null, 2)); console.log(`๐Ÿ“„ Template also saved to file: ${filePath}`); return templateId; } catch (error) { console.error('โŒ Failed to save template to database:', error); // Fallback: save to file only const dir = path.dirname(filePath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(filePath, JSON.stringify(cliTemplate, null, 2)); console.log(`โš ๏ธ Template saved to file only: ${filePath}`); console.log(' (Database storage failed - template won\'t be available to MCP tools)'); throw error; } } /** * Build A/B Test template interactively */ async buildABTestInteractive(template, platform) { console.log('\n๐Ÿงช Setting up A/B Test...\n'); // Get test details const testDetails = await inquirer.prompt([ { type: 'input', name: 'testName', message: 'Enter the name for your A/B test:', validate: (input) => input.length > 0 || 'Test name is required' }, { type: 'input', name: 'testKey', message: 'Enter the key (URL-safe identifier):', default: (answers) => answers.testName.toLowerCase().replace(/\s+/g, '_'), validate: (input) => /^[a-z0-9_]+$/.test(input) || 'Key must be lowercase letters, numbers, and underscores only' } ]); // Get variation details const { variationCount } = await inquirer.prompt([ { type: 'number', name: 'variationCount', message: 'How many variations (including control)?', default: 2, validate: (input) => (input !== undefined && input >= 2) || 'Must have at least 2 variations' } ]); const variations = []; let totalWeight = 0; for (let i = 0; i < variationCount; i++) { const isLast = i === variationCount - 1; const remainingWeight = 10000 - totalWeight; const variation = await inquirer.prompt([ { type: 'input', name: 'name', message: `Enter name for variation ${i + 1}:`, default: i === 0 ? 'Control' : `Treatment ${i}` }, { type: 'input', name: 'key', message: `Enter key for variation ${i + 1}:`, default: (answers) => answers.name.toLowerCase().replace(/\s+/g, '_') }, { type: 'number', name: 'weight', message: `Enter traffic allocation (0-${remainingWeight}):`, default: isLast ? remainingWeight : Math.floor(remainingWeight / (variationCount - i)), validate: (input) => { if (input === undefined || input < 0 || input > remainingWeight) { return `Weight must be between 0 and ${remainingWeight}`; } return true; } } ]); totalWeight += variation.weight; variations.push(variation); } // Create event for tracking this.addCreateStep(template, 'event', 'create_conversion_event', { name: `${testDetails.testName} Conversion`, key: `${testDetails.testKey}_conversion`, description: 'Conversion event for A/B test' }); if (platform === 'web') { // Web experiment this.addCreateStep(template, 'experiment', 'create_experiment', { name: testDetails.testName, key: testDetails.testKey, status: 'not_started', type: 'a/b', variations: variations, metrics: [ { event_id: '${create_conversion_event.id}', scope: 'visitor' } ] }); } else { // Feature flag + ruleset for Feature Experimentation this.addCreateStep(template, 'flag', 'create_flag', { name: testDetails.testName, key: testDetails.testKey, description: 'Feature flag for A/B test', variations: variations.map(v => ({ key: v.key, name: v.name, value: v.key === 'control' ? 'false' : 'true' })) }); this.addCreateStep(template, 'ruleset', 'create_ruleset', { flag_key: '${create_flag.key}', type: 'a/b_test', rules: [ { key: `${testDetails.testKey}_rule`, variations: variations.map(v => ({ variation_key: v.key, weight: v.weight })), metrics: [ { event_key: '${create_conversion_event.key}', scope: 'visitor' } ] } ] }); } } /** * Build Feature Flag template interactively */ async buildFeatureFlagInteractive(template) { console.log('\n๐Ÿšฉ Setting up Feature Flag...\n'); const flagDetails = await inquirer.prompt([ { type: 'input', name: 'flagName', message: 'Enter the name for your feature flag:', validate: (input) => input.length > 0 || 'Flag name is required' }, { type: 'input', name: 'flagKey', message: 'Enter the key (URL-safe identifier):', default: (answers) => answers.flagName.toLowerCase().replace(/\s+/g, '_'), validate: (input) => /^[a-z0-9_]+$/.test(input) || 'Key must be lowercase letters, numbers, and underscores only' }, { type: 'input', name: 'description', message: 'Enter a description for the flag:', default: 'Feature flag for controlling feature availability' }, { type: 'number', name: 'initialRollout', message: 'Initial rollout percentage (0-100):', default: 0, validate: (input) => (input !== undefined && input >= 0 && input <= 100) || 'Must be between 0 and 100' } ]); // Create flag this.addCreateStep(template, 'flag', 'create_flag', { name: flagDetails.flagName, key: flagDetails.flagKey, description: flagDetails.description, variations: [ { key: 'off', name: 'Off', value: 'false' }, { key: 'on', name: 'On', value: 'true' } ] }); // Create rollout ruleset this.addCreateStep(template, 'ruleset', 'create_rollout', { flag_key: '${create_flag.key}', type: 'rollout', rules: [ { key: 'rollout_rule', percentage_included: flagDetails.initialRollout * 100, // Convert to basis points deliver: flagDetails.initialRollout > 0 ? 'on' : 'off' } ] }); } /** * Build Audience-based Experiment interactively */ async buildAudienceExperimentInteractive(template, platform) { console.log('\n๐ŸŽฏ Setting up Audience-based Experiment...\n'); // Get audience details const audienceDetails = await inquirer.prompt([ { type: 'input', name: 'audienceName', message: 'Enter the name for your audience:', validate: (input) => input.length > 0 || 'Audience name is required' }, { type: 'list', name: 'conditionType', message: 'What type of audience condition?', choices: [ { name: 'Custom Attribute', value: 'custom_attribute' }, { name: 'Browser Type', value: 'browser_type' }, { name: 'Device Type', value: 'device_type' }, { name: 'Location', value: 'location' } ] } ]); let conditions; switch (audienceDetails.conditionType) { case 'custom_attribute': const attrDetails = await inquirer.prompt([ { type: 'input', name: 'attributeName', message: 'Enter the attribute name:', default: 'plan_type' }, { type: 'input', name: 'attributeValue', message: 'Enter the attribute value:', default: 'premium' } ]); conditions = JSON.stringify([ 'and', ['or', ['custom_attribute', attrDetails.attributeName, 'equals', attrDetails.attributeValue]] ]); break; default: // Simplified for other types conditions = JSON.stringify(['and', ['or', [audienceDetails.conditionType, 'equals', 'value']]]); } // Create audience this.addCreateStep(template, 'audience', 'create_audience', { name: audienceDetails.audienceName, conditions: conditions }); // Now create an experiment using this audience await this.buildABTestInteractive(template, platform); // Update the last experiment/ruleset step to include audience const lastStep = template.steps[template.steps.length - 1]; if (lastStep && lastStep.template) { if (platform === 'web') { lastStep.template.inputs.audience_conditions = 'audience_ids=${create_audience.id}'; } else { // For feature flags, add audience to the rule if (lastStep.template.inputs.rules && lastStep.template.inputs.rules[0]) { lastStep.template.inputs.rules[0].audience_conditions = [ { audience_id: '${create_audience.id}' } ]; } } } } /** * Build custom template interactively */ async buildCustomInteractive(template, platform) { console.log('\n๐Ÿ—๏ธ Building Custom Template...\n'); let addMore = true; while (addMore) { const entityChoices = this.getEntityChoices(platform); const { entityType } = await inquirer.prompt([ { type: 'list', name: 'entityType', message: 'What entity would you like to add?', choices: entityChoices.map(e => ({ name: e.name, value: e.value })) } ]); const { stepId, entityName } = await inquirer.prompt([ { type: 'input', name: 'stepId', message: 'Enter a unique ID for this step:', default: `create_${entityType}`, validate: (input) => { if (!input) return 'Step ID is required'; if (template.steps.some(s => s.id === input)) return 'Step ID must be unique'; return true; } }, { type: 'input', name: 'entityName', message: `Enter the name for the ${entityType}:`, validate: (input) => input.length > 0 || 'Name is required' } ]); // Get required fields for this entity type const entityChoice = entityChoices.find(e => e.value === entityType); const inputs = { name: entityName }; // Add entity-specific fields if (entityType === 'flag' || entityType === 'event') { const { key } = await inquirer.prompt([ { type: 'input', name: 'key', message: 'Enter the key:', default: entityName.toLowerCase().replace(/\s+/g, '_'), validate: (input) => /^[a-z0-9_]+$/.test(input) || 'Key must be lowercase letters, numbers, and underscores only' } ]); inputs.key = key; } // Add page-specific fields if (entityType === 'page') { const { editUrl } = await inquirer.prompt([ { type: 'input', name: 'editUrl', message: 'Enter the page URL:', validate: (input) => input.startsWith('http') || 'Must be a valid URL' } ]); inputs.edit_url = editUrl; } this.addCreateStep(template, entityType, stepId, inputs); const { continueAdding } = await inquirer.prompt([ { type: 'confirm', name: 'continueAdding', message: 'Would you like to add another entity?', default: false } ]); addMore = continueAdding; } } /** * Get entity choices for a platform */ getEntityChoices(platform) { // Web Experimentation specific entities const webEntities = [ { name: '๐Ÿงช Experiment', value: 'experiment', description: 'A/B test or personalization', requiredFields: ['name', 'project_id'] }, { name: '๐ŸŽญ Campaign', value: 'campaign', description: 'Group of experiments', requiredFields: ['name', 'project_id'] }, { name: '๐Ÿ“„ Page', value: 'page', description: 'Page for targeting', requiredFields: ['name', 'project_id', 'edit_url'] }, { name: '๐Ÿ”Œ Extension', value: 'extension', description: 'Custom code extension', requiredFields: ['name', 'project_id', 'implementation'] }, { name: '๐ŸŽฒ Variation', value: 'variation', description: 'Experiment variation', requiredFields: ['name', 'weight'] } ]; // Feature Experimentation specific entities const featureEntities = [ { name: '๐Ÿšฉ Feature Flag', value: 'flag', description: 'Feature toggle with variables', requiredFields: ['name', 'key', 'project_id'] }, { name: '๐Ÿ“‹ Ruleset', value: 'ruleset', description: 'Flag delivery rules (rollout/a-b test)', requiredFields: ['flag_key', 'project_id'] }, { name: '๐Ÿ“ Rule', value: 'rule', description: 'Individual targeting rule', requiredFields: ['name', 'percentage_included'] }, { name: '๐Ÿ”ง Variable Definition', value: 'variable_definition', description: 'Flag variable configuration', requiredFields: ['key', 'type', 'default_value'] }, { name: '๐ŸŒŸ Feature', value: 'feature', description: 'Feature grouping', requiredFields: ['name', 'key'] } ]; // Shared entities (available on both platforms) const sharedEntities = [ { name: '๐ŸŽฏ Audience', value: 'audience', description: 'User segment with conditions', requiredFields: ['name', 'project_id', 'conditions'] }, { name: '๐Ÿ“Š Event', value: 'event', description: 'Tracking event for metrics', requiredFields: ['name', 'key', 'project_id'] }, { name: '๐Ÿท๏ธ Attribute', value: 'attribute', description: 'Custom user attribute', requiredFields: ['name', 'key', 'project_id'] }, { name: '๐Ÿ“ List Attribute', value: 'list_attribute', description: 'List-type custom attribute', requiredFields: ['name', 'key', 'list_type'] }, { name: '๐ŸŒ Environment', value: 'environment', description: 'Project environment', requiredFields: ['name', 'key'] }, { name: '๐Ÿ”” Webhook', value: 'webhook', description: 'Webhook integration', requiredFields: ['name', 'url', 'event_types'] }, { name: '๐Ÿ‘ฅ Group', value: 'group', description: 'User exclusion group', requiredFields: ['name', 'policy'] }, { name: '๐Ÿ‘ค Collaborator', value: 'collaborator', description: 'Project collaborator', requiredFields: ['email', 'role'] }, { name: '๐Ÿข Project', value: 'project', description: 'New project', requiredFields: ['name', 'platform'] }, { name: '๐Ÿ“ˆ Segment', value: 'segment', description: 'Analytics segment', requiredFields: ['name', 'conditions'] } ]; // Advanced/specialized entities const advancedEntities = [ { name: '๐Ÿ”„ Targeted Delivery', value: 'targeted_delivery', description: 'Single variation delivery (no metrics)', requiredFields: ['name', 'audience'] }, { name: '๐ŸŽฐ Multi-Armed Bandit', value: 'mab', description: 'Auto-optimizing experiment', requiredFields: ['name', 'metric'] }, { name: 'โšก Stats Accelerator', value: 'stats_accelerator', description: 'Fast statistical testing', requiredFields: ['name', 'confidence_level'] }, { name: '๐Ÿ” Mutually Exclusive Group', value: 'mutex_group', description: 'Experiment exclusion group', requiredFields: ['name', 'experiments'] } ]; // Return entities based on platform if (platform === 'web') { return [...webEntities, ...sharedEntities, ...advancedEntities.filter(e => ['targeted_delivery', 'mab', 'stats_accelerator', 'mutex_group'].includes(e.value))]; } else if (platform === 'feature') { return [...featureEntities, ...sharedEntities]; } else { // 'both' platform - return all entities return [...webEntities, ...featureEntities, ...sharedEntities, ...advancedEntities]; } } /** * Interactive Campaign Builder */ async buildCampaignInteractive() { const template = this.createBasicTemplate('web'); template.description = 'Custom Campaign with Multiple Experiments'; console.log('\n๐ŸŽฏ Campaign Builder - Create a personalization campaign with custom experiments\n'); // Get campaign details const { campaignName, holdback } = await inquirer.prompt([ { type: 'input', name: 'campaignName', message: 'Campaign name:', default: 'Personalization Campaign' }, { type: 'number', name: 'holdback', message: 'Campaign holdback % (0-10):', default: 0, validate: (input) => (input !== undefined && input >= 0 && input <= 10) || 'Must be between 0 and 10' } ]); // Create campaign step this.addCreateStep(template, 'campaign', 'create_campaign', { name: campaignName, holdback: holdback * 1000 // Convert to basis points }); // Ask about pages const { pageCount } = await inquirer.prompt([ { type: 'number', name: 'pageCount', message: 'How many pages will this campaign run on?', default: 1, validate: (input) => (input !== undefined && input >= 1) || 'At least one page required' } ]); const pageIds = []; for (let i = 0; i < pageCount; i++) { const { pageName, pageUrl } = await inquirer.prompt([ { type: 'input', name: 'pageName', message: `Page ${i + 1} name:`, default: i === 0 ? 'Homepage' : `Page ${i + 1}` }, { type: 'input', name: 'pageUrl', message: `Page ${i + 1} URL:`, default: 'https://example.com' } ]); const pageStepId = `create_page_${i + 1}`; this.addCreateStep(template, 'page', pageStepId, { name: pageName, edit_url: pageUrl }); pageIds.push(`\${${pageStepId}.id}`); } // Build experiments let addMoreExperiments = true; let experimentCount = 0; while (addMoreExperiments) { experimentCount++; console.log(chalk.blue(`\n๐Ÿ“Š Experiment ${experimentCount}:`)); const { experimentName, audienceType } = await inquirer.prompt([ { type: 'input', name: 'experimentName', message: 'Experiment name:', default: `Experience ${experimentCount}` }, { type: 'list', name: 'audienceType', message: 'Target audience:', choices: [ { name: 'Everyone', value: 'everyone' }, { name: 'New visitors', value: 'new' }, { name: 'Returning visitors', value: 'returning' }, { name: 'Mobile users', value: 'mobile' }, { name: 'Desktop users', value: 'desktop' }, { name: 'Custom audience (existing)', value: 'custom' }, { name: 'Create new audience', value: 'create' } ] } ]); let audienceCondition = 'everyone'; if (audienceType === 'create') { const { audienceName, attributeName, operator, value } = await inquirer.prompt([ { type: 'input', name: 'audienceName', message: 'Audience name:', default: `Audience ${experi