numerology/numerology.ts
2026-01-04 02:23:56 -07:00

698 lines
24 KiB
TypeScript
Executable file

#!/usr/bin/env bun
/**
* Numerology Calculator - TypeScript Edition
*
* Calculates core numerology numbers:
* - Life Path: Your life's purpose and journey
* - Expression: Your natural talents and abilities
* - Soul Urge: Your inner desires and motivations
* - Birthday: Special gifts from your birth day
*
* Usage:
* bun numerology.ts --name "John Doe" --birthdate "5/13/1982"
* bun numerology.ts --name "Jane Smith" --birthdate "11/22/1990" --format markdown
* bun numerology.ts --name "Bob Jones" --birthdate "3/14/1975" --format json
*/
import { lifePath, expression, soulUrge, birthday, type NumberMeaning } from './meanings';
import { calculateCycles, type CycleCalculations } from './cycles';
import { personalYear, personalMonth, personalDay, universalYear } from './cycle-meanings';
import { calculateCoreNumbers, calculateAdditionalNumbers, type AdditionalNumbers } from './core-calculator';
import {
maturityMeanings,
personalityMeanings,
hiddenPassionMeanings,
karmicLessonMeanings,
balanceMeanings
} from './additional-meanings';
import { loadProfile } from './profile-manager';
const MASTER_NUMBERS = [11, 22, 33];
interface NumerologyResult {
name: string;
birthdate: string;
lifePath: number;
expression: number;
soulUrge: number;
birthday: number;
calculations: {
month: number;
day: number;
year: number;
nameBreakdown: Record<string, number>;
vowelBreakdown: Record<string, number>;
};
}
/**
* Reduces a number to single digit or returns master number
*/
function reduce(n: number, context: string = "", silent: boolean = false): number {
if (MASTER_NUMBERS.includes(n)) {
if (context && !silent) {
console.error(`✨ Master number found in ${context}: ${n}`);
}
return n;
}
const total = String(n).split('').reduce((sum, digit) => sum + parseInt(digit), 0);
return total < 10 ? total : reduce(total, context);
}
/**
* Converts name to numerology value
*/
function nameToNumber(name: string): number {
const cleaned = name.toLowerCase().replace(/[^a-z]/g, '');
let total = 0;
for (const char of cleaned) {
const value = char.charCodeAt(0) - 96; // a=1, b=2, etc.
total += value < 10 ? value : reduce(value);
}
return total;
}
/**
* Checks for master number before further reduction
*/
function masterCheck(total: number, context: string = "", silent: boolean = false): number {
if (MASTER_NUMBERS.includes(total)) {
if (context && !silent) {
console.error(`✨ Master number found in ${context}: ${total}`);
}
return total;
}
const reduced = String(total).split('').reduce((sum, digit) => sum + parseInt(digit), 0);
return reduced < 10 ? reduced : reduce(reduced);
}
/**
* Extracts vowels from name
*/
function extractVowels(name: string): string {
return name.toLowerCase().replace(/[^aeiou]/g, '');
}
/**
* Parse birthdate string (mm/dd/yyyy)
*/
function parseBirthdate(birthdate: string): { month: number; day: number; year: number } {
const parts = birthdate.split('/');
if (parts.length !== 3) {
throw new Error('Invalid birthdate format. Use mm/dd/yyyy (e.g., 5/13/1982)');
}
return {
month: parseInt(parts[0]),
day: parseInt(parts[1]),
year: parseInt(parts[2])
};
}
/**
* Calculate all numerology numbers
*/
function calculate(name: string, birthdate: string, silent: boolean = false): NumerologyResult {
// Parse name
const nameParts = name.trim().split(/\s+/);
if (nameParts.length < 2) {
throw new Error('Please provide at least first and last name');
}
// Parse birthdate
const { month, day, year } = parseBirthdate(birthdate);
// Life Path (from birthdate)
const monthReduced = reduce(month, "Month", silent);
const dayReduced = reduce(day, "Day", silent);
const yearReduced = reduce(year, "Year", silent);
const birthDateSum = monthReduced + dayReduced + yearReduced;
const lifePathNum = reduce(birthDateSum, "Life Path", silent);
// Birthday (from day of birth)
const birthdayNum = reduce(day, "Birthday", silent);
// Expression (from full name)
const nameBreakdown: Record<string, number> = {};
let expressionTotal = 0;
// Use only first and last name if more than 4 parts
const namesToUse = nameParts.length > 4
? [nameParts[0], nameParts[nameParts.length - 1]]
: nameParts;
for (const part of namesToUse) {
const value = nameToNumber(part);
const checked = masterCheck(value, `Name: ${part}`, silent);
nameBreakdown[part] = checked;
expressionTotal += checked;
}
const expressionNum = masterCheck(reduce(expressionTotal, "", silent), "Expression", silent);
// Soul Urge (from vowels in name)
const vowelBreakdown: Record<string, number> = {};
let soulUrgeTotal = 0;
for (const part of nameParts) {
const vowels = extractVowels(part);
if (vowels) {
const value = nameToNumber(vowels);
const checked = masterCheck(value, `Vowels in ${part}`, silent);
vowelBreakdown[part] = checked;
soulUrgeTotal += checked;
}
}
const soulUrgeNum = masterCheck(reduce(soulUrgeTotal, "", silent), "Soul Urge", silent);
return {
name,
birthdate,
lifePath: lifePathNum,
expression: expressionNum,
soulUrge: soulUrgeNum,
birthday: birthdayNum,
calculations: {
month: monthReduced,
day: dayReduced,
year: yearReduced,
nameBreakdown,
vowelBreakdown
}
};
}
/**
* Format output as terminal text (with colors)
*/
function formatTerminal(result: NumerologyResult, detailed: boolean = false): string {
const CYAN = '\x1b[36m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const BLUE = '\x1b[34m';
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
let output = `\n${CYAN}${BOLD}╔═══════════════════════════════════════╗${RESET}\n`;
output += `${CYAN}${BOLD}║ NUMEROLOGY CHART ║${RESET}\n`;
output += `${CYAN}${BOLD}╚═══════════════════════════════════════╝${RESET}\n\n`;
output += `${BOLD}Name:${RESET} ${result.name}\n`;
output += `${BOLD}Birthdate:${RESET} ${result.birthdate}\n\n`;
output += `${GREEN}${BOLD}Core Numbers:${RESET}\n`;
output += `${GREEN} 1. Life Path:${RESET} ${result.lifePath}\n`;
output += `${GREEN} 2. Expression:${RESET} ${result.expression}\n`;
output += `${GREEN} 3. Soul Urge:${RESET} ${result.soulUrge}\n`;
output += `${GREEN} 4. Birthday:${RESET} ${result.birthday}\n`;
if (detailed) {
output += `\n${BLUE}${BOLD}═══════════════════════════════════════${RESET}\n`;
output += `${BLUE}${BOLD}DETAILED MEANINGS${RESET}\n`;
output += `${BLUE}${BOLD}═══════════════════════════════════════${RESET}\n\n`;
// Life Path
const lpMeaning = lifePath[result.lifePath];
output += `${YELLOW}${BOLD}Life Path ${result.lifePath}:${RESET} ${lpMeaning.keywords.join(', ')}\n`;
output += `${lpMeaning.description}\n\n`;
output += `${BOLD}Strengths:${RESET} ${lpMeaning.strengths.join(', ')}\n`;
output += `${BOLD}Challenges:${RESET} ${lpMeaning.challenges.join(', ')}\n`;
if (lpMeaning.lifePurpose) {
output += `${BOLD}Life Purpose:${RESET} ${lpMeaning.lifePurpose}\n`;
}
output += '\n';
// Expression
const exMeaning = expression[result.expression];
output += `${YELLOW}${BOLD}Expression ${result.expression}:${RESET} ${exMeaning.keywords.join(', ')}\n`;
output += `${exMeaning.description}\n\n`;
// Soul Urge
const suMeaning = soulUrge[result.soulUrge];
output += `${YELLOW}${BOLD}Soul Urge ${result.soulUrge}:${RESET} ${suMeaning.keywords.join(', ')}\n`;
output += `${suMeaning.description}\n\n`;
// Birthday
const bdMeaning = birthday[result.birthday];
output += `${YELLOW}${BOLD}Birthday ${result.birthday}:${RESET} ${bdMeaning.keywords.join(', ')}\n`;
output += `${bdMeaning.description}\n`;
}
return output;
}
/**
* Format output as markdown
*/
function formatMarkdown(result: NumerologyResult): string {
let output = `# Numerology Chart\n\n`;
output += `**Name:** ${result.name} \n`;
output += `**Birthdate:** ${result.birthdate}\n\n`;
output += `## Core Numbers\n\n`;
output += `1. **Life Path:** ${result.lifePath}\n`;
output += `2. **Expression:** ${result.expression}\n`;
output += `3. **Soul Urge:** ${result.soulUrge}\n`;
output += `4. **Birthday:** ${result.birthday}\n\n`;
output += `---\n\n`;
output += `## Detailed Meanings\n\n`;
// Life Path
const lpMeaning = lifePath[result.lifePath];
output += `### Life Path ${result.lifePath}: ${lpMeaning.keywords.join(', ')}\n\n`;
output += `${lpMeaning.description}\n\n`;
output += `**Strengths:** ${lpMeaning.strengths.join(', ')}\n\n`;
output += `**Challenges:** ${lpMeaning.challenges.join(', ')}\n\n`;
if (lpMeaning.lifePurpose) {
output += `**Life Purpose:** ${lpMeaning.lifePurpose}\n\n`;
}
if (lpMeaning.careerPaths) {
output += `**Career Paths:** ${lpMeaning.careerPaths.join(', ')}\n\n`;
}
if (lpMeaning.relationships) {
output += `**Relationships:** ${lpMeaning.relationships}\n\n`;
}
if (lpMeaning.spiritualLesson) {
output += `**Spiritual Lesson:** ${lpMeaning.spiritualLesson}\n\n`;
}
// Expression
const exMeaning = expression[result.expression];
output += `### Expression ${result.expression}: ${exMeaning.keywords.join(', ')}\n\n`;
output += `${exMeaning.description}\n\n`;
output += `**Strengths:** ${exMeaning.strengths.join(', ')}\n\n`;
output += `**Challenges:** ${exMeaning.challenges.join(', ')}\n\n`;
// Soul Urge
const suMeaning = soulUrge[result.soulUrge];
output += `### Soul Urge ${result.soulUrge}: ${suMeaning.keywords.join(', ')}\n\n`;
output += `${suMeaning.description}\n\n`;
output += `**Strengths:** ${suMeaning.strengths.join(', ')}\n\n`;
output += `**Challenges:** ${suMeaning.challenges.join(', ')}\n\n`;
// Birthday
const bdMeaning = birthday[result.birthday];
output += `### Birthday ${result.birthday}: ${bdMeaning.keywords.join(', ')}\n\n`;
output += `${bdMeaning.description}\n\n`;
output += `**Strengths:** ${bdMeaning.strengths.join(', ')}\n\n`;
output += `**Challenges:** ${bdMeaning.challenges.join(', ')}\n\n`;
return output;
}
/**
* Format output as JSON
*/
function formatJson(result: NumerologyResult): string {
const output = {
...result,
meanings: {
lifePath: lifePath[result.lifePath],
expression: expression[result.expression],
soulUrge: soulUrge[result.soulUrge],
birthday: birthday[result.birthday]
}
};
return JSON.stringify(output, null, 2);
}
/**
* Format cycles output for terminal
*/
function formatCyclesTerminal(cycles: CycleCalculations): string {
const MAGENTA = '\x1b[35m';
const CYAN = '\x1b[36m';
const YELLOW = '\x1b[33m';
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
let output = `\n${MAGENTA}${BOLD}═══════════════════════════════════════${RESET}\n`;
output += `${MAGENTA}${BOLD}TIMING CYCLES${RESET}\n`;
output += `${MAGENTA}${BOLD}═══════════════════════════════════════${RESET}\n\n`;
output += `${BOLD}Date:${RESET} ${cycles.targetDate}\n\n`;
// Universal Cycles
output += `${CYAN}${BOLD}Universal Energy (Global):${RESET}\n`;
output += ` Year: ${cycles.universal.year} ${universalYear[cycles.universal.year]}\n`;
output += ` Month: ${cycles.universal.month}\n`;
output += ` Day: ${cycles.universal.day}\n\n`;
// Personal Cycles
output += `${CYAN}${BOLD}Personal Energy (Your Rhythm):${RESET}\n`;
output += ` Year: ${cycles.personal.year}\n`;
output += ` Month: ${cycles.personal.month}\n`;
output += ` Day: ${cycles.personal.day} ${personalDay[cycles.personal.day]}\n\n`;
// Personal Year meaning
const pyMeaning = personalYear[cycles.personal.year];
output += `${YELLOW}${BOLD}Personal Year ${cycles.personal.year}: ${pyMeaning.keywords.join(', ')}${RESET}\n`;
output += `${pyMeaning.theme}\n\n`;
output += `${BOLD}Best For:${RESET} ${pyMeaning.bestFor.slice(0, 3).join(', ')}\n`;
output += `${BOLD}Avoid:${RESET} ${pyMeaning.avoidDoing.slice(0, 2).join(', ')}\n\n`;
// Personal Month meaning
const pmMeaning = personalMonth[cycles.personal.month];
output += `${YELLOW}${BOLD}Personal Month ${cycles.personal.month}: ${pmMeaning.keywords.join(', ')}${RESET}\n`;
output += `${pmMeaning.theme}\n`;
return output;
}
/**
* Format cycles output for markdown
*/
function formatCyclesMarkdown(cycles: CycleCalculations): string {
let output = `\n---\n\n# Timing Cycles\n\n`;
output += `**Date:** ${cycles.targetDate}\n\n`;
output += `## Universal Energy (Global)\n\n`;
output += `- **Year:** ${cycles.universal.year}\n`;
output += ` - ${universalYear[cycles.universal.year]}\n`;
output += `- **Month:** ${cycles.universal.month}\n`;
output += `- **Day:** ${cycles.universal.day}\n\n`;
output += `## Personal Energy (Your Rhythm)\n\n`;
output += `- **Year:** ${cycles.personal.year}\n`;
output += `- **Month:** ${cycles.personal.month}\n`;
output += `- **Day:** ${cycles.personal.day}\n`;
output += ` - ${personalDay[cycles.personal.day]}\n\n`;
// Personal Year meaning
const pyMeaning = personalYear[cycles.personal.year];
output += `## Personal Year ${cycles.personal.year}\n\n`;
output += `> *${pyMeaning.keywords.join(' • ')}*\n\n`;
output += `**Theme:** ${pyMeaning.theme}\n\n`;
output += `### Opportunities\n`;
pyMeaning.opportunities.forEach(o => output += `- ${o}\n`);
output += `\n### Challenges\n`;
pyMeaning.challenges.forEach(c => output += `- ${c}\n`);
output += `\n**Advice:** ${pyMeaning.advice}\n\n`;
output += `**Best For:** ${pyMeaning.bestFor.join(', ')}\n\n`;
output += `**Avoid Doing:** ${pyMeaning.avoidDoing.join(', ')}\n\n`;
// Personal Month meaning
const pmMeaning = personalMonth[cycles.personal.month];
output += `## Personal Month ${cycles.personal.month}\n\n`;
output += `> *${pmMeaning.keywords.join(' • ')}*\n\n`;
output += `**Theme:** ${pmMeaning.theme}\n\n`;
output += `**Advice:** ${pmMeaning.advice}\n\n`;
return output;
}
/**
* Format advanced numbers for terminal output
*/
function formatAdvancedTerminal(additional: AdditionalNumbers, detailed: boolean = false): string {
const PURPLE = '\x1b[35m';
const GREEN = '\x1b[32m';
const YELLOW = '\x1b[33m';
const RED = '\x1b[31m';
const RESET = '\x1b[0m';
const BOLD = '\x1b[1m';
let output = `\n${PURPLE}${BOLD}═══════════════════════════════════════${RESET}\n`;
output += `${PURPLE}${BOLD}ADVANCED NUMBERS${RESET}\n`;
output += `${PURPLE}${BOLD}═══════════════════════════════════════${RESET}\n\n`;
// Maturity
const maturityMeaning = maturityMeanings[additional.maturity];
output += `${GREEN} 5. Maturity:${RESET} ${additional.maturity} (${maturityMeaning.keywords.slice(0, 3).join(', ')})\n`;
if (detailed) {
output += ` ${maturityMeaning.description}\n`;
}
// Personality
const personalityMeaning = personalityMeanings[additional.personality];
output += `${GREEN} 6. Personality:${RESET} ${additional.personality} (${personalityMeaning.keywords.slice(0, 3).join(', ')})\n`;
if (detailed) {
output += ` ${personalityMeaning.description}\n`;
}
// Hidden Passion
if (additional.hiddenPassion) {
const hiddenPassionMeaning = hiddenPassionMeanings[additional.hiddenPassion];
output += `${GREEN} 7. Hidden Passion:${RESET} ${additional.hiddenPassion} (${hiddenPassionMeaning.keywords.slice(0, 2).join(', ')})\n`;
if (detailed) {
output += ` ${hiddenPassionMeaning.description}\n`;
}
}
// Balance
const balanceMeaning = balanceMeanings[additional.balance];
output += `${GREEN} 8. Balance:${RESET} ${additional.balance} (${balanceMeaning.keywords.slice(0, 2).join(', ')})\n`;
if (detailed) {
output += ` ${balanceMeaning.description}\n`;
}
// Karmic Lessons
if (additional.karmicLessons.length > 0) {
output += `${RED} 9. Karmic Lessons:${RESET} ${additional.karmicLessons.join(', ')}\n`;
if (detailed) {
output += ` Areas to develop: ${additional.karmicLessons.map(n => karmicLessonMeanings[n].keywords[0]).join(', ')}\n`;
}
} else {
output += `${GREEN} 9. Karmic Lessons:${RESET} None (All numbers 1-9 present)\n`;
}
output += '\n';
return output;
}
/**
* Router function - delegates to individual scripts
*/
async function routeToScript(scriptName: string, args: string[]): Promise<void> {
const scriptPath = new URL(`./${scriptName}`, import.meta.url).pathname;
const proc = Bun.spawn(['bun', scriptPath, ...args], {
stdout: 'inherit',
stderr: 'inherit',
stdin: 'inherit',
});
const exitCode = await proc.exited;
process.exit(exitCode);
}
/**
* Main CLI handler
*/
async function main() {
const args = process.argv.slice(2);
// Check for routing commands first (before parsing other args)
for (let i = 0; i < args.length; i++) {
// Week Cycles (handle both -c and combined flags like -cd)
if (args[i] === '--cycles-week' || args[i] === '-c' || args[i].startsWith('-c')) {
// For combined flags like -cd, expand them properly
let scriptArgs = args.filter((_, idx) => idx !== i);
// If it's a combined flag (like -cd), extract the additional flags
if (args[i].startsWith('-c') && args[i].length > 2) {
const extraFlags = args[i].slice(2).split('').map(f => `-${f}`);
scriptArgs = [...extraFlags, ...scriptArgs];
}
await routeToScript('cycles-week.ts', scriptArgs);
return;
}
// Year Ahead
if (args[i] === '--year-ahead' || args[i] === '-y') {
const scriptArgs = args.filter((_, idx) => idx !== i);
await routeToScript('year-ahead.ts', scriptArgs);
return;
}
// Optimal Days
if (args[i] === '--optimal-days') {
const scriptArgs = args.filter((_, idx) => idx !== i);
await routeToScript('optimal-days.ts', scriptArgs);
return;
}
// Pinnacles
if (args[i] === '--pinnacles') {
const scriptArgs = args.filter((_, idx) => idx !== i);
await routeToScript('pinnacles.ts', scriptArgs);
return;
}
// Name Optimizer
if (args[i] === '--name-optimizer') {
const scriptArgs = args.filter((_, idx) => idx !== i);
await routeToScript('name-optimizer.ts', scriptArgs);
return;
}
// Compatibility
if (args[i] === '--compatibility') {
const scriptArgs = args.filter((_, idx) => idx !== i);
await routeToScript('compatibility.ts', scriptArgs);
return;
}
}
// No routing command found - proceed with core numerology calculation
// Parse arguments
let name = '';
let birthdate = '';
let profileId = '';
let format: 'terminal' | 'markdown' | 'json' = 'terminal';
let detailed = false;
let showCycles = false;
let cycleDate = '';
let showAdvanced = false;
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] === '--format' && args[i + 1]) {
const fmt = args[i + 1].toLowerCase();
if (fmt === 'terminal' || fmt === 'markdown' || fmt === 'json') {
format = fmt;
}
i++;
} else if (args[i] === '--detailed' || args[i] === '-d') {
detailed = true;
} else if (args[i] === '--date' && args[i + 1]) {
cycleDate = args[i + 1];
i++;
} else if (args[i] === '--advanced' || args[i] === '-a') {
showAdvanced = true;
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(`
Numerology Calculator - Unified CLI
CORE NUMBERS:
numerology -p <profile> Core numerology numbers
numerology -n "Name" -b "mm/dd/yyyy" Core numbers with name/birthdate
Options:
-p, --profile ID Use saved profile
-n, --name NAME Full name (required if no profile)
-b, --birthdate DATE Birthdate in mm/dd/yyyy format (required if no profile)
-d, --detailed Show detailed meanings
-a, --advanced Show advanced numbers (Maturity, Personality, etc.)
--format FORMAT Output format: terminal, markdown, json
--date DATE Date for cycle calculation (mm/dd/yyyy)
SPECIALIZED TOOLS:
-c, --cycles-week Weekly personal day cycles
-y, --year-ahead Full year month-by-month overview
--optimal-days Find best days for specific activities
--pinnacles Life stages & 9-year pinnacle cycles
--name-optimizer Find powerful name variations
--compatibility Compare two people's charts
Examples:
# Core numbers
numerology -p rob
numerology -p rob -d -a
numerology -n "John Doe" -b "5/13/1982"
# Specialized tools
numerology -c -p rob # This week's cycles
numerology -y -p rob # Year ahead
numerology --pinnacles -p rob # Life pinnacles
numerology --optimal-days -p rob --day 1 # Find all Personal Day 1s
numerology --compatibility --p1 "John:1/1/1980" --p2 "Jane:2/2/1985"
For detailed help on each tool, use: numerology <tool> --help
`);
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;
}
// Validate inputs
if (!name || !birthdate) {
console.error('Error: Either --profile or both --name and --birthdate are required');
console.error('Use --help for usage information');
process.exit(1);
}
try {
// Calculate (silent mode for JSON to avoid console pollution)
const silent = format === 'json';
const result = calculate(name, birthdate, silent);
// Calculate advanced numbers if requested
let advancedNumbers: AdditionalNumbers | undefined;
if (showAdvanced) {
const coreNumbers = calculateCoreNumbers(name, birthdate);
advancedNumbers = calculateAdditionalNumbers(name, coreNumbers);
}
// Calculate cycles if requested
let cycles: CycleCalculations | undefined;
if (showCycles) {
cycles = calculateCycles(birthdate, cycleDate || undefined);
}
// Format and output
let output: string;
switch (format) {
case 'markdown':
output = formatMarkdown(result);
if (cycles) {
output += formatCyclesMarkdown(cycles);
}
break;
case 'json':
output = formatJson(result);
break;
default:
output = formatTerminal(result, detailed);
if (advancedNumbers) {
output += formatAdvancedTerminal(advancedNumbers, detailed);
}
if (cycles) {
output += formatCyclesTerminal(cycles);
}
}
console.log(output);
} catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
}
// Run if called directly
if (import.meta.main) {
await main();
}
export { calculate, formatTerminal, formatMarkdown, formatJson, type NumerologyResult };