- 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>
597 lines
25 KiB
TypeScript
Executable file
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}`);
|