test-spintax.js
1 #!/usr/bin/env node 2 3 /** 4 * CLI tool for testing spintax templates 5 * Usage: node scripts/test-spintax.js <template_id> [--count N] [--kwd keyword] [--validate] 6 */ 7 8 import { readFile } from 'fs/promises'; 9 import { fileURLToPath } from 'url'; 10 import { dirname, join } from 'path'; 11 import { 12 loadAndSpinTemplate, 13 generateVariations, 14 countPossibleVariations, 15 validateSpintax, 16 testSpintax, 17 } from '../src/utils/spintax.js'; 18 19 const __dirname = dirname(fileURLToPath(import.meta.url)); 20 21 // Parse command line arguments 22 const args = process.argv.slice(2); 23 const templateId = args[0]; 24 const flags = { 25 count: parseInt(args.find((_, i) => args[i - 1] === '--count')) || 10, 26 kwd: args.find((_, i) => args[i - 1] === '--kwd') || 'plumbing', 27 keyword: args.find((_, i) => args[i - 1] === '--keyword') || 'plumbing', 28 firstname: args.find((_, i) => args[i - 1] === '--firstname') || 'Alice', 29 domain: args.find((_, i) => args[i - 1] === '--domain') || 'example.com', 30 grade: args.find((_, i) => args[i - 1] === '--grade') || 'C-', 31 score: args.find((_, i) => args[i - 1] === '--score') || '75', 32 reasoning: 33 args.find((_, i) => args[i - 1] === '--reasoning') || 'weak call-to-action above the fold', 34 evidence: 35 args.find((_, i) => args[i - 1] === '--evidence') || 36 'no phone number visible without scrolling', 37 primary_weakness: 38 args.find((_, i) => args[i - 1] === '--primary_weakness') || 'unclear value proposition', 39 impact: args.find((_, i) => args[i - 1] === '--impact') || '30', 40 industry: args.find((_, i) => args[i - 1] === '--industry') || 'home services', 41 sender_id: args.find((_, i) => args[i - 1] === '--sender_id') || 'Mike', 42 validate: args.includes('--validate'), 43 list: args.includes('--list'), 44 all: args.includes('--all'), 45 }; 46 47 async function loadTemplates() { 48 const templatesDir = join(__dirname, '../data/templates'); 49 const allTemplates = []; 50 51 // Load templates from all country/channel files 52 const countries = ['US', 'AU']; 53 const channels = ['email', 'sms']; 54 55 for (const country of countries) { 56 for (const channel of channels) { 57 try { 58 const filePath = join(templatesDir, country, `${channel}.json`); 59 const json = await readFile(filePath, 'utf-8'); 60 const data = JSON.parse(json); 61 allTemplates.push(...data.templates); 62 } catch (err) { 63 // Skip missing files 64 } 65 } 66 } 67 68 return allTemplates; 69 } 70 71 function printHeader(title) { 72 console.log(`\n${'='.repeat(80)}`); 73 console.log(title); 74 console.log('='.repeat(80)); 75 } 76 77 function printSection(title) { 78 console.log(`\n${'-'.repeat(80)}`); 79 console.log(title); 80 console.log('-'.repeat(80)); 81 } 82 83 async function listTemplates() { 84 const templates = await loadTemplates(); 85 86 printHeader('Available Templates'); 87 88 templates.forEach(t => { 89 console.log(`\n${t.id}`); 90 console.log(` Channel: ${t.channel}`); 91 console.log(` Author: ${t.author}`); 92 console.log(` Tone: ${t.tone}`); 93 console.log(` Approach: ${t.approach}`); 94 95 if (t.subject_spintax) { 96 const subjectVariations = countPossibleVariations(t.subject_spintax); 97 console.log(` Subject variations: ${subjectVariations}`); 98 } 99 100 const bodyVariations = countPossibleVariations(t.body_spintax); 101 console.log(` Body variations: ${bodyVariations}`); 102 103 const total = t.subject_spintax 104 ? countPossibleVariations(t.subject_spintax) * bodyVariations 105 : bodyVariations; 106 console.log(` Total combinations: ${total.toLocaleString()}`); 107 }); 108 109 console.log(`\n\nTotal templates: ${templates.length}\n`); 110 } 111 112 async function validateTemplate(templateId) { 113 const templates = await loadTemplates(); 114 const template = templates.find(t => t.id === templateId); 115 116 if (!template) { 117 console.error(`Template not found: ${templateId}`); 118 process.exit(1); 119 } 120 121 printHeader(`Validating Template: ${templateId}`); 122 123 if (template.subject_spintax) { 124 printSection('Subject Line Validation'); 125 const subjectValidation = validateSpintax(template.subject_spintax); 126 127 if (subjectValidation.valid) { 128 console.log('✅ Subject line spintax is valid'); 129 } else { 130 console.log('❌ Subject line spintax has errors:'); 131 subjectValidation.errors.forEach(err => console.log(` - ${err}`)); 132 } 133 134 console.log(`\nPossible variations: ${countPossibleVariations(template.subject_spintax)}`); 135 } 136 137 printSection('Body Validation'); 138 const bodyValidation = validateSpintax(template.body_spintax); 139 140 if (bodyValidation.valid) { 141 console.log('✅ Body spintax is valid'); 142 } else { 143 console.log('❌ Body spintax has errors:'); 144 bodyValidation.errors.forEach(err => console.log(` - ${err}`)); 145 } 146 147 console.log(`\nPossible variations: ${countPossibleVariations(template.body_spintax)}`); 148 149 const totalVariations = template.subject_spintax 150 ? countPossibleVariations(template.subject_spintax) * 151 countPossibleVariations(template.body_spintax) 152 : countPossibleVariations(template.body_spintax); 153 154 console.log(`\nTotal unique emails possible: ${totalVariations.toLocaleString()}`); 155 } 156 157 async function testTemplate(templateId, count, replacements) { 158 const templates = await loadTemplates(); 159 const template = templates.find(t => t.id === templateId); 160 161 if (!template) { 162 console.error(`Template not found: ${templateId}`); 163 console.log('\nUse --list to see available templates'); 164 process.exit(1); 165 } 166 167 printHeader(`Testing Template: ${templateId}`); 168 169 console.log(`Channel: ${template.channel}`); 170 console.log(`Author: ${template.author}`); 171 console.log(`Tone: ${template.tone}`); 172 console.log(`Approach: ${template.approach}`); 173 174 console.log('\nReplacements:'); 175 Object.entries(replacements).forEach(([key, value]) => { 176 if (value) console.log(` [${key}] = ${value}`); 177 }); 178 179 if (template.subject_spintax) { 180 printSection(`Subject Line Variations (${count} samples)`); 181 182 const subjectVariations = generateVariations(template.subject_spintax, count); 183 184 subjectVariations.forEach((variation, i) => { 185 let replaced = variation; 186 for (const [key, value] of Object.entries(replacements)) { 187 replaced = replaced.replaceAll(`[${key}]`, value); 188 } 189 console.log(`${i + 1}. ${replaced}`); 190 }); 191 192 const totalSubjectVariations = countPossibleVariations(template.subject_spintax); 193 console.log(`\nTotal possible subject variations: ${totalSubjectVariations}`); 194 } 195 196 printSection(`Body Variations (${count} samples)`); 197 198 const bodyVariations = generateVariations(template.body_spintax, count); 199 200 bodyVariations.forEach((variation, i) => { 201 let replaced = variation; 202 for (const [key, value] of Object.entries(replacements)) { 203 replaced = replaced.replaceAll(`[${key}]`, value); 204 } 205 206 console.log(`\n--- Variation ${i + 1} ---`); 207 console.log(replaced); 208 }); 209 210 const totalBodyVariations = countPossibleVariations(template.body_spintax); 211 console.log(`\nTotal possible body variations: ${totalBodyVariations}`); 212 213 const totalVariations = template.subject_spintax 214 ? countPossibleVariations(template.subject_spintax) * totalBodyVariations 215 : totalBodyVariations; 216 217 printSection('Statistics'); 218 console.log(`Total unique emails possible: ${totalVariations.toLocaleString()}`); 219 console.log(`Samples generated: ${count}`); 220 console.log( 221 `Uniqueness guaranteed: ${count <= totalVariations ? 'Yes' : 'No (not enough combinations)'}` 222 ); 223 } 224 225 async function testAllTemplates(count, replacements) { 226 const templates = await loadTemplates(); 227 228 printHeader('Testing All Templates'); 229 230 for (const template of templates) { 231 printSection(`${template.id} (${template.channel}, ${template.tone})`); 232 233 // Generate one sample 234 const result = await loadAndSpinTemplate(template.id, replacements); 235 236 if (result.subject) { 237 console.log(`Subject: ${result.subject}`); 238 } 239 240 console.log('\nBody:'); 241 console.log(result.body); 242 243 const totalVariations = template.subject_spintax 244 ? countPossibleVariations(template.subject_spintax) * 245 countPossibleVariations(template.body_spintax) 246 : countPossibleVariations(template.body_spintax); 247 248 console.log(`\nTotal variations: ${totalVariations.toLocaleString()}`); 249 } 250 } 251 252 // Main execution 253 (async function main() { 254 try { 255 if (flags.list) { 256 await listTemplates(); 257 return; 258 } 259 260 if (flags.all) { 261 await testAllTemplates(flags.count, { 262 kwd: flags.kwd, 263 keyword: flags.keyword, 264 firstname: flags.firstname, 265 domain: flags.domain, 266 grade: flags.grade, 267 score: flags.score, 268 reasoning: flags.reasoning, 269 evidence: flags.evidence, 270 primary_weakness: flags.primary_weakness, 271 impact: flags.impact, 272 industry: flags.industry, 273 sender_id: flags.sender_id, 274 }); 275 return; 276 } 277 278 if (!templateId) { 279 console.log('Usage: node scripts/test-spintax.js <template_id> [options]'); 280 console.log('\nOptions:'); 281 console.log(' --count N Generate N variations (default: 10)'); 282 console.log(' --kwd KEYWORD Business keyword (default: plumbing)'); 283 console.log(' --keyword KEYWORD Alternative to --kwd'); 284 console.log(' --firstname NAME Contact first name (default: Alice)'); 285 console.log(' --domain DOMAIN Website domain (default: example.com)'); 286 console.log(' --grade GRADE Conversion grade (default: C-)'); 287 console.log(' --score SCORE Numeric score 0-100 (default: 75)'); 288 console.log(' --reasoning TEXT Why the score is what it is'); 289 console.log(' --evidence TEXT Specific examples from the site'); 290 console.log(' --primary_weakness TEXT Main conversion issue'); 291 console.log(' --impact PERCENT Percentage impact estimate (default: 30)'); 292 console.log(' --industry INDUSTRY Business industry (default: home services)'); 293 console.log(' --sender_id NAME SMS sender identification (default: Mike)'); 294 console.log(' --validate Validate spintax syntax only'); 295 console.log(' --list List all available templates'); 296 console.log(' --all Test all templates'); 297 console.log('\nExamples:'); 298 console.log(' node scripts/test-spintax.js --list'); 299 console.log(' node scripts/test-spintax.js email_pauljames_01'); 300 console.log(' node scripts/test-spintax.js email_dad_03 --count 20 --kwd landscaping'); 301 console.log(' node scripts/test-spintax.js email_001 --grade B- --impact 25'); 302 console.log(' node scripts/test-spintax.js email_dad_05 --validate'); 303 console.log(' node scripts/test-spintax.js --all --kwd electrician'); 304 process.exit(1); 305 } 306 307 if (flags.validate) { 308 await validateTemplate(templateId); 309 } else { 310 await testTemplate(templateId, flags.count, { 311 kwd: flags.kwd, 312 keyword: flags.keyword, 313 firstname: flags.firstname, 314 domain: flags.domain, 315 grade: flags.grade, 316 score: flags.score, 317 reasoning: flags.reasoning, 318 evidence: flags.evidence, 319 primary_weakness: flags.primary_weakness, 320 impact: flags.impact, 321 industry: flags.industry, 322 sender_id: flags.sender_id, 323 }); 324 } 325 } catch (error) { 326 console.error('Error:', error.message); 327 process.exit(1); 328 } 329 })();