numerology/generate-report.ts
rpriven fd6e171586
Add comprehensive numerology calculator with 10 specialized tools
- Core calculations (Life Path, Expression, Soul Urge, Birthday)
- Advanced numbers (Maturity, Personality, Hidden Passion, Karmic Lessons)
- Timing cycles and optimal days finder
- Compatibility analysis and name optimizer
- Telos integration for personal development
- Professional PDF report generation
- Profile management system
- Security fix: Add .claude/ to .gitignore

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-01 14:00:15 -06:00

597 lines
25 KiB
TypeScript
Executable file

#!/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 <id> [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-<name>.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<string, string> = {
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<number, string> = {
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<number, string> = {
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<number, string> = {
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<string, Date[]> = {};
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}`);