#!/usr/bin/env bun /** * LaTeX PDF Report Generator * * Generates a beautiful, comprehensive numerology report in PDF format. * Uses LaTeX for professional typography and layout. * * Usage: * bun generate-report.ts --name "John Doe" --birthdate "5/13/1982" * bun generate-report.ts --name "Jane Smith" --birthdate "11/22/1990" --output ~/jane-report.pdf */ import { calculateCoreNumbers, calculateAdditionalNumbers } from './core-calculator'; import { calculateCycles, calculateYearCycles, findOptimalDays } from './cycles'; import { lifePath, expression, soulUrge, birthday } from './meanings'; import { personalYear, personalMonth, personalDay } from './cycle-meanings'; import { maturityMeanings, personalityMeanings, hiddenPassionMeanings, karmicLessonMeanings, balanceMeanings } from './additional-meanings'; import { loadProfile } from './profile-manager'; // Parse command line arguments const args = process.argv.slice(2); let name = ''; let birthdate = ''; let outputPath = ''; let profileId = ''; for (let i = 0; i < args.length; i++) { if ((args[i] === '--profile' || args[i] === '-p') && args[i + 1]) { profileId = args[i + 1]; i++; } else if ((args[i] === '--name' || args[i] === '-n') && args[i + 1]) { name = args[i + 1]; i++; } else if ((args[i] === '--birthdate' || args[i] === '-b') && args[i + 1]) { birthdate = args[i + 1]; i++; } else if ((args[i] === '--output' || args[i] === '-o') && args[i + 1]) { outputPath = args[i + 1]; i++; } else if (args[i] === '--help' || args[i] === '-h') { console.log(` LaTeX PDF Report Generator Generate a beautiful, comprehensive numerology report in PDF format. USAGE: bun generate-report.ts --profile [OPTIONS] bun generate-report.ts --name "Your Name" --birthdate "mm/dd/yyyy" [OPTIONS] OPTIONS: -p, --profile ID Use saved profile -n, --name NAME Your full name [required if no profile] -b, --birthdate DATE Your birthdate (mm/dd/yyyy) [required if no profile] -o, --output PATH Output PDF path [default: ./numerology-report-.pdf] -h, --help Show this help message REQUIREMENTS: - pdflatex must be installed (sudo apt install texlive-latex-extra texlive-fonts-extra) EXAMPLES: # Generate report with profile bun generate-report.ts --profile john # Generate report with name/birthdate bun generate-report.ts --name "John Doe" --birthdate "5/13/1982" # Custom output location bun generate-report.ts --profile john --output ~/my-report.pdf OUTPUT: Creates a professional multi-page PDF report including: - Core numbers with detailed interpretations - Advanced numbers (Maturity, Personality, Hidden Passion, etc.) - Current timing cycles - Life pinnacles and challenges - Synthesis and guidance - Calculation appendix `); process.exit(0); } } // Load profile if specified if (profileId) { const profile = loadProfile(profileId); if (!profile) { console.error(`Error: Profile '${profileId}' not found`); console.error('List profiles with: bun profile.ts list'); process.exit(1); } name = profile.name; birthdate = profile.birthdate; console.log(`Using profile '${profileId}': ${name} (${birthdate})`); } if (!name || !birthdate) { console.error('Error: Either --profile or both --name and --birthdate are required'); console.error('Try: bun generate-report.ts --help'); process.exit(1); } // Set default output path if (!outputPath) { const safeName = name.toLowerCase().replace(/[^a-z0-9]/g, '-'); outputPath = `./numerology-report-${safeName}.pdf`; } // Helper functions function escapeLatex(text: string | undefined): string { if (!text) return ''; return text .replace(/\\/g, '\\textbackslash{}') .replace(/[&%$#_{}]/g, '\\$&') .replace(/~/g, '\\textasciitilde{}') .replace(/\^/g, '\\textasciicircum{}'); } function formatList(items: string[] | undefined): string { if (!items || items.length === 0) return ' \\item (None listed)'; return items.map(item => ` \\item ${escapeLatex(item)}`).join('\n'); } function getMasterSymbol(num: number): string { return [11, 22, 33].includes(num) ? '\\textcolor{accent}{\\textbf{(MASTER)}}' : ''; } function reduce(num: number): number { if (num === 11 || num === 22 || num === 33) return num; while (num > 9) { num = num.toString().split('').reduce((sum, digit) => sum + parseInt(digit), 0); if (num === 11 || num === 22 || num === 33) return num; } return num; } // Reduce to single digit (0-9) - for challenge numbers only (no master numbers) function reduceSingleDigit(num: number): number { while (num > 9) { num = num.toString().split('').reduce((sum, digit) => sum + parseInt(digit), 0); } return num; } function calculatePinnacles(birthdate: string, lifePath: number) { const [month, day, year] = birthdate.split('/').map(n => parseInt(n)); const monthReduced = reduce(month); const dayReduced = reduce(day); const yearReduced = reduce(year); // Calculate pinnacle numbers const pinnacle1 = reduce(monthReduced + dayReduced); const pinnacle2 = reduce(dayReduced + yearReduced); const pinnacle3 = reduce(pinnacle1 + pinnacle2); const pinnacle4 = reduce(monthReduced + yearReduced); // Calculate challenge numbers (always reduce to 0-9, no master numbers in challenges) const challenge1 = reduceSingleDigit(Math.abs(monthReduced - dayReduced)); const challenge2 = reduceSingleDigit(Math.abs(dayReduced - yearReduced)); const challenge3 = reduceSingleDigit(Math.abs(challenge1 - challenge2)); const challenge4 = reduceSingleDigit(Math.abs(monthReduced - yearReduced)); // Calculate age ranges const age1End = 36 - lifePath; const age2End = age1End + 9; const age3End = age2End + 9; const currentYear = new Date().getFullYear(); const birthYear = year; const currentAge = currentYear - birthYear; return { pinnacle1: { number: pinnacle1, challenge: challenge1, ageStart: 0, ageEnd: age1End }, pinnacle2: { number: pinnacle2, challenge: challenge2, ageStart: age1End, ageEnd: age2End }, pinnacle3: { number: pinnacle3, challenge: challenge3, ageStart: age2End, ageEnd: age3End }, pinnacle4: { number: pinnacle4, challenge: challenge4, ageStart: age3End, ageEnd: 999 }, currentAge }; } // Calculate all numbers console.log('Calculating numerology...'); const coreNumbers = calculateCoreNumbers(name, birthdate); const additionalNumbers = calculateAdditionalNumbers(name, coreNumbers); const today = new Date(); const cycles = calculateCycles(birthdate, today.toLocaleDateString('en-US')); // Calculate pinnacles const pinnacles = calculatePinnacles(birthdate, coreNumbers.lifePath); // Calculate year-ahead cycles const currentYear = today.getFullYear(); const nextYear = currentYear + 1; const yearAheadCycles = calculateYearCycles(birthdate, nextYear); // Find optimal days for next 3 months - all numbers including master numbers const optimalDays: Array<{date: Date, personalDay: number}> = []; for (let monthOffset = 0; monthOffset < 3; monthOffset++) { const targetDate = new Date(today.getFullYear(), today.getMonth() + monthOffset, 1); const targetYear = targetDate.getFullYear(); const targetMonth = targetDate.getMonth() + 1; // Include all single-digit numbers 1-9 plus master numbers 11, 22, 33 [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33].forEach(desiredDay => { const days = findOptimalDays(birthdate, targetYear, targetMonth, desiredDay); days.forEach(day => { optimalDays.push({ date: new Date(targetYear, targetMonth - 1, day), personalDay: desiredDay }); }); }); } // Get meanings const lifePathMeaning = lifePath[coreNumbers.lifePath]; const expressionMeaning = expression[coreNumbers.expression]; const soulUrgeMeaning = soulUrge[coreNumbers.soulUrge]; const birthdayMeaning = birthday[coreNumbers.birthday]; const maturityMeaning = maturityMeanings[additionalNumbers.maturity]; const personalityMeaning = personalityMeanings[additionalNumbers.personality]; const balanceMeaning = balanceMeanings[additionalNumbers.balance]; const personalYearMeaning = personalYear[cycles.personal.year]; const personalMonthMeaning = personalMonth[cycles.personal.month]; const personalDayMeaningText = personalDay[cycles.personal.day]; // Build template variables const vars: Record = { name: escapeLatex(name), birthdate: escapeLatex(birthdate), date: today.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }), // Core numbers lifePath: coreNumbers.lifePath.toString(), lifePath_master: getMasterSymbol(coreNumbers.lifePath), lifePath_keywords: escapeLatex(lifePathMeaning.keywords.join(', ')), lifePath_description: escapeLatex(lifePathMeaning.description), lifePath_purpose: escapeLatex(lifePathMeaning.lifePurpose), lifePath_strengths: formatList(lifePathMeaning.strengths), lifePath_challenges: formatList(lifePathMeaning.challenges), lifePath_careers: escapeLatex((lifePathMeaning.careerPaths || []).join(', ')), lifePath_relationships: escapeLatex(lifePathMeaning.relationships || ''), lifePath_spiritual: escapeLatex(lifePathMeaning.spiritualLesson || ''), expression: coreNumbers.expression.toString(), expression_master: getMasterSymbol(coreNumbers.expression), expression_keywords: escapeLatex(expressionMeaning.keywords.join(', ')), expression_description: escapeLatex(expressionMeaning.description), expression_purpose: escapeLatex(expressionMeaning.lifePurpose), expression_strengths: formatList(expressionMeaning.strengths), expression_challenges: formatList(expressionMeaning.challenges), expression_careers: escapeLatex((expressionMeaning.careerPaths || []).join(', ')), expression_relationships: escapeLatex(expressionMeaning.relationships || ''), soulUrge: coreNumbers.soulUrge.toString(), soulUrge_master: getMasterSymbol(coreNumbers.soulUrge), soulUrge_keywords: escapeLatex(soulUrgeMeaning.keywords.join(', ')), soulUrge_description: escapeLatex(soulUrgeMeaning.description), soulUrge_purpose: escapeLatex(soulUrgeMeaning.lifePurpose), soulUrge_strengths: formatList(soulUrgeMeaning.strengths), soulUrge_challenges: formatList(soulUrgeMeaning.challenges), birthday: coreNumbers.birthday.toString(), birthday_keywords: escapeLatex(birthdayMeaning.keywords.join(', ')), birthday_description: escapeLatex(birthdayMeaning.description), birthday_purpose: escapeLatex(birthdayMeaning.lifePurpose), birthday_strengths: formatList(birthdayMeaning.strengths), // Advanced numbers maturity: additionalNumbers.maturity.toString(), maturity_master: getMasterSymbol(additionalNumbers.maturity), maturity_description: escapeLatex(maturityMeaning.description), personality: additionalNumbers.personality.toString(), personality_master: getMasterSymbol(additionalNumbers.personality), personality_description: escapeLatex(personalityMeaning.description), balance: additionalNumbers.balance.toString(), balance_description: escapeLatex(balanceMeaning.description), // Timing cycles personalYear: cycles.personal.year.toString(), personalYear_keywords: escapeLatex(personalYearMeaning.keywords.join(', ')), personalYear_description: escapeLatex(personalYearMeaning.description), personalYear_meaning: escapeLatex(personalYearMeaning.theme), personalYear_opportunities: formatList(personalYearMeaning.opportunities || []), personalYear_challenges_list: formatList(personalYearMeaning.challenges || []), currentYear: today.getFullYear().toString(), personalMonth: cycles.personal.month.toString(), personalMonth_description: escapeLatex(personalMonthMeaning.description), personalMonth_focus: escapeLatex(personalMonthMeaning.theme), currentMonth: today.toLocaleDateString('en-US', { month: 'long' }), personalDay: cycles.personal.day.toString(), personalDay_description: escapeLatex(personalDayMeaningText), personalDay_activities: escapeLatex(personalDayMeaningText), // Same as description for simple format today: today.toLocaleDateString('en-US'), universalYear: cycles.universal.year.toString(), universalMonth: cycles.universal.month.toString(), universalDay: cycles.universal.day.toString(), // Calculations (simplified for now) calc_month: birthdate.split('/')[0], calc_day: birthdate.split('/')[1], calc_year: birthdate.split('/')[2], calc_expression_breakdown: `Full name calculation: ${coreNumbers.expression}`, calc_soulUrge_breakdown: `Vowels only calculation: ${coreNumbers.soulUrge}`, calc_personality_breakdown: `${additionalNumbers.personality}`, }; // Hidden Passion section if (additionalNumbers.hiddenPassion) { const hpMeaning = hiddenPassionMeanings[additionalNumbers.hiddenPassion]; vars.hiddenPassion_section = ` \\subsection{Hidden Passion Number: ${additionalNumbers.hiddenPassion} ${getMasterSymbol(additionalNumbers.hiddenPassion)}} \\begin{tcolorbox}[meaningbox] \\textbf{Your Secret Talent:} ${escapeLatex(hpMeaning.description)} \\vspace{0.3cm} This number appears ${additionalNumbers.hiddenPassionCount} times in your name - more than any other number. It represents a hidden talent or deep passion that drives you. \\end{tcolorbox} `; } else { vars.hiddenPassion_section = ` \\subsection{Hidden Passion Number: None} \\begin{tcolorbox}[meaningbox] No single number dominates your name, which means you have a balanced set of talents and interests. \\end{tcolorbox} `; } // Karmic Lessons section if (additionalNumbers.karmicLessons.length > 0) { const lessonsList = additionalNumbers.karmicLessons.map(lesson => { const meaning = karmicLessonMeanings[lesson]; return ` \\textbf{Number ${lesson} - ${escapeLatex(meaning.keywords[0])}:} ${escapeLatex(meaning.description)} `; }).join('\n\n \\vspace{0.3cm}\n'); vars.karmicLessons_section = ` \\subsection{Karmic Lessons: ${additionalNumbers.karmicLessons.join(', ')}} \\begin{tcolorbox}[meaningbox] \\textbf{Areas for Growth:} The following numbers are missing from your name, indicating areas you're here to develop: \\vspace{0.3cm} ${lessonsList} \\end{tcolorbox} `; } else { vars.karmicLessons_section = ` \\subsection{Karmic Lessons: None} \\begin{tcolorbox}[meaningbox] All numbers 1-9 appear in your name, which means you have no specific karmic lessons to learn in this lifetime. You came in with a complete set of tools! \\end{tcolorbox} `; } // Pinnacles section - full implementation const pinnacleNames = ['First Pinnacle', 'Second Pinnacle', 'Third Pinnacle', 'Fourth Pinnacle']; const pinnacleDescriptions: Record = { 1: 'Leadership, independence, new beginnings. This is a time to develop your individuality and take initiative.', 2: 'Cooperation, partnerships, patience. Focus on relationships and diplomacy.', 3: 'Creativity, self-expression, social connections. Time to develop your creative talents.', 4: 'Building, organization, hard work. Establish solid foundations and systems.', 5: 'Change, freedom, adventure. Embrace variety and new experiences.', 6: 'Responsibility, service, family. Focus on home, relationships, and helping others.', 7: 'Introspection, spirituality, wisdom. Develop inner knowledge and expertise.', 8: 'Material success, power, authority. Time to build wealth and recognition.', 9: 'Completion, humanitarianism, wisdom. Let go and serve the greater good.', 11: 'Inspiration, spiritual insight, illumination. Channel higher wisdom and inspire others.', 22: 'Master building, large-scale manifestation. Create lasting legacies that serve humanity.', 33: 'Master teaching, universal love, healing. Teach and heal at the highest level.' }; const challengeDescriptions: Record = { 0: 'Choice - You have many options and must learn to choose wisely.', 1: 'Independence vs. dependence - Balance self-reliance with accepting help.', 2: 'Sensitivity - Overcome timidity and learn to assert yourself tactfully.', 3: 'Self-expression - Overcome self-doubt and express your creativity.', 4: 'Limitation - Work within restrictions and build solid foundations.', 5: 'Change - Learn to handle unexpected changes and maintain focus.', 6: 'Responsibility - Balance giving to others with self-care.', 7: 'Trust - Develop faith and overcome skepticism or isolation.', 8: 'Power - Handle authority and success without becoming controlling.' }; let currentPinnacle = 1; if (pinnacles.currentAge >= pinnacles.pinnacle2.ageStart && pinnacles.currentAge < pinnacles.pinnacle2.ageEnd) currentPinnacle = 2; else if (pinnacles.currentAge >= pinnacles.pinnacle3.ageStart && pinnacles.currentAge < pinnacles.pinnacle3.ageEnd) currentPinnacle = 3; else if (pinnacles.currentAge >= pinnacles.pinnacle4.ageStart) currentPinnacle = 4; const pinnaclesList = [pinnacles.pinnacle1, pinnacles.pinnacle2, pinnacles.pinnacle3, pinnacles.pinnacle4]; vars.pinnacles_content = pinnaclesList.map((p, idx) => { const isCurrent = (idx + 1) === currentPinnacle; const pinnacleNum = p.number; const challengeNum = p.challenge; const ageRange = p.ageEnd === 999 ? `Age ${p.ageStart}+` : `Ages ${p.ageStart}-${p.ageEnd}`; const currentMarker = isCurrent ? ' \\textcolor{accent}{\\textbf{(CURRENT)}}' : ''; return ` \\subsection{${pinnacleNames[idx]}${currentMarker}: ${pinnacleNum} ${getMasterSymbol(pinnacleNum)}} \\textbf{${ageRange}} \\begin{tcolorbox}[meaningbox] \\textbf{Theme:} ${escapeLatex(pinnacleDescriptions[pinnacleNum] || 'Development and growth.')} \\vspace{0.3cm} \\textbf{Challenge Number: ${challengeNum}} ${escapeLatex(challengeDescriptions[challengeNum] || 'Learning and growth opportunity.')} ${isCurrent ? `\\vspace{0.3cm}\n \\textcolor{accent}{\\textbf{You are currently in this pinnacle.}} This is the primary energy shaping your life right now.` : ''} \\end{tcolorbox} `; }).join('\n'); // Optimal Days section const dayMeanings: Record = { 1: 'New beginnings, launches, starting projects', 2: 'Cooperation, partnerships, negotiations', 3: 'Creative work, socializing, presentations', 4: 'Organization, building systems, practical work', 5: 'Networking, trying new things, marketing', 6: 'Family matters, counseling, creating harmony', 7: 'Research, planning, spiritual work', 8: 'Business deals, financial decisions, leadership', 9: 'Completion, letting go, humanitarian work', 11: 'Inspirational work, teaching, intuitive guidance', 22: 'Large-scale projects, master building', 33: 'Healing, teaching with compassion' }; const groupedDays: Record = {}; optimalDays.forEach(day => { const dayNum = day.personalDay; const key = `Day ${dayNum}: ${dayMeanings[dayNum] || 'General activities'}`; if (!groupedDays[key]) groupedDays[key] = []; groupedDays[key].push(day.date); }); // Show all day types (1-9, 11, 22, 33) vars.optimalDays_content = Object.entries(groupedDays).map(([desc, dates]) => { const dateList = dates.slice(0, 5).map(d => d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })).join(', '); const more = dates.length > 5 ? ` (and ${dates.length - 5} more)` : ''; return `\\textbf{${escapeLatex(desc)}}\\\\${dateList}${more}\\\\[0.3cm]`; }).join('\n'); // Year Ahead section const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; const yearAheadPersonalYear = reduce(reduce(parseInt(birthdate.split('/')[0])) + reduce(parseInt(birthdate.split('/')[1])) + reduce(nextYear)); vars.nextYear = nextYear.toString(); vars.yearAhead_content = ` \\begin{tcolorbox}[meaningbox] \\textbf{Your Personal Year ${yearAheadPersonalYear} in ${nextYear}} ${escapeLatex(personalYear[yearAheadPersonalYear]?.theme || 'A year of growth and development.')} \\vspace{0.3cm} \\textbf{Monthly Breakdown:} \\begin{itemize}[leftmargin=1.5cm] ${yearAheadCycles.map(({ month, personalMonth: pmNum }) => { const monthName = monthNames[month - 1]; const pmMeaning = personalMonth[pmNum]; const theme = pmMeaning?.keywords?.[0] || 'Development'; return ` \\item \\textbf{${monthName}:} Personal Month ${pmNum} - ${escapeLatex(theme)}`; }).join('\n')} \\end{itemize} \\vspace{0.3cm} \\textit{Best months for new beginnings: ${yearAheadCycles.filter(c => c.personalMonth === 1).map(c => monthNames[c.month - 1]).join(', ') || 'See your Personal Year theme'}} \\textit{Best months for completion: ${yearAheadCycles.filter(c => c.personalMonth === 9).map(c => monthNames[c.month - 1]).join(', ') || 'See your Personal Year theme'}} \\end{tcolorbox} `; // Master numbers section const masterNumbers = []; if ([11, 22, 33].includes(coreNumbers.lifePath)) masterNumbers.push(`Life Path ${coreNumbers.lifePath}`); if ([11, 22, 33].includes(coreNumbers.expression)) masterNumbers.push(`Expression ${coreNumbers.expression}`); if ([11, 22, 33].includes(coreNumbers.soulUrge)) masterNumbers.push(`Soul Urge ${coreNumbers.soulUrge}`); if ([11, 22, 33].includes(additionalNumbers.maturity)) masterNumbers.push(`Maturity ${additionalNumbers.maturity}`); if ([11, 22, 33].includes(additionalNumbers.personality)) masterNumbers.push(`Personality ${additionalNumbers.personality}`); if (masterNumbers.length > 0) { vars.masterNumbers_section = ` \\begin{tcolorbox}[numberbox] \\textcolor{accent}{\\textbf{✨ You have ${masterNumbers.length} Master Number(s) in your chart!}} \\vspace{0.3cm} ${masterNumbers.map(m => `\\textbf{${escapeLatex(m)}}`).join(' • ')} \\vspace{0.3cm} Master numbers (11, 22, 33) carry heightened spiritual significance and greater responsibility. They represent advanced soul development and the potential for making a significant impact on the world. \\end{tcolorbox} `; } else { vars.masterNumbers_section = ` \\begin{tcolorbox}[meaningbox] You have no master numbers in your chart. This doesn't diminish your potential - it simply means your path is about mastering the single-digit energies with depth and consistency. \\end{tcolorbox} `; } // Synthesis variables (simplified summaries) vars.synthesis_core = escapeLatex(lifePathMeaning.keywords.slice(0, 3).join(', ').toLowerCase()); vars.synthesis_soul = escapeLatex(soulUrgeMeaning.keywords[0].toLowerCase()); vars.synthesis_year = escapeLatex(personalYearMeaning.keywords.slice(0, 2).join(' and ').toLowerCase()); // Fix maturity synthesis to be more verbose const maturityAge = 40; // This could be calculated based on Life Path vars.synthesis_maturity = escapeLatex(`embodies ${maturityMeaning.keywords.slice(0, 2).join(' and ').toLowerCase()}`); // Recommendations (generic for now - could be more personalized) vars.rec_lifePath = escapeLatex(`Focus on the themes of ${lifePathMeaning.keywords.slice(0, 2).join(' and ').toLowerCase()}. This is your primary life purpose.`); vars.rec_expression = escapeLatex(`Use your natural talents in ${expressionMeaning.keywords.slice(0, 2).join(' and ').toLowerCase()} to fulfill your Life Path.`); vars.rec_soulUrge = escapeLatex(`Honor your deep desire for ${soulUrgeMeaning.keywords[0].toLowerCase()}. When you do, you'll feel most alive.`); vars.rec_karmic = additionalNumbers.karmicLessons.length > 0 ? escapeLatex(`Pay special attention to developing ${additionalNumbers.karmicLessons.map(n => karmicLessonMeanings[n].keywords[0].toLowerCase()).join(', ')}. These are growth areas.`) : escapeLatex('You have no karmic lessons, so focus on refining and mastering your existing talents.'); vars.rec_cycles = escapeLatex(`This Personal Year ${cycles.personal.year} is about ${personalYearMeaning.keywords[0].toLowerCase()}. Align your actions with this energy.`); // Read template console.log('Reading LaTeX template...'); const templatePath = './templates/report-template.tex'; const templateFile = Bun.file(templatePath); let template = await templateFile.text(); // Replace all variables console.log('Populating template...'); for (const [key, value] of Object.entries(vars)) { template = template.replace(new RegExp(`\\\\VAR{${key}}`, 'g'), value); } // Write populated template to temp file const tempTexPath = '/tmp/numerology-report.tex'; await Bun.write(tempTexPath, template); console.log(`LaTeX file generated: ${tempTexPath}`); console.log('Compiling to PDF...'); // Compile with pdflatex const proc = Bun.spawn(['pdflatex', '-interaction=nonstopmode', '-output-directory=/tmp', tempTexPath], { stdout: 'pipe', stderr: 'pipe', }); await proc.exited; if (proc.exitCode !== 0) { console.error('Error: pdflatex compilation failed'); console.error('Make sure texlive is installed: sudo apt install texlive-latex-extra texlive-fonts-extra'); process.exit(1); } // Run pdflatex again for table of contents const proc2 = Bun.spawn(['pdflatex', '-interaction=nonstopmode', '-output-directory=/tmp', tempTexPath], { stdout: 'pipe', stderr: 'pipe', }); await proc2.exited; // Move PDF to output location const tempPdfPath = '/tmp/numerology-report.pdf'; await Bun.write(outputPath, Bun.file(tempPdfPath)); console.log(`\n✨ Report generated successfully!`); console.log(`📄 Output: ${outputPath}`); console.log(`\nOpen with: xdg-open ${outputPath}`);