UNPKG

faj-cli

Version:

FAJ - A powerful CLI resume builder with AI enhancement and multi-format export

703 lines (698 loc) 30.8 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ResumeManager = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const os = __importStar(require("os")); const Logger_1 = require("../../utils/Logger"); const AIManager_1 = require("../../ai/AIManager"); const ConfigManager_1 = require("../config/ConfigManager"); const ExperienceManager_1 = require("../experience/ExperienceManager"); const ResumeTemplates_1 = require("../../templates/ResumeTemplates"); const CompactResumeTemplates_1 = require("../../templates/CompactResumeTemplates"); const SectionTitles_1 = require("../../utils/SectionTitles"); const OpenResumePDFGenerator_1 = require("../pdf/OpenResumePDFGenerator"); class ResumeManager { static instance; logger; resumePath; currentResume = null; aiManager; configManager; constructor() { this.logger = new Logger_1.Logger('ResumeManager'); this.resumePath = path.join(os.homedir(), '.faj', 'resume.json'); this.aiManager = AIManager_1.AIManager.getInstance(); this.configManager = ConfigManager_1.ConfigManager.getInstance(); } static getInstance() { if (!ResumeManager.instance) { ResumeManager.instance = new ResumeManager(); } return ResumeManager.instance; } async loadOrCreate() { try { await this.ensureResumeDir(); if (await this.resumeExists()) { const data = await fs.readFile(this.resumePath, 'utf-8'); this.currentResume = JSON.parse(data); this.logger.info('Resume loaded successfully'); } else { this.currentResume = await this.createEmptyResume(); await this.save(); this.logger.info('Created new resume'); } return this.currentResume; } catch (error) { this.logger.error('Failed to load resume', error); throw new Error('Failed to load resume'); } } async generateFromProjects(projects) { this.logger.info(`Generating resume from ${projects.length} projects`); const profile = await this.configManager.get('profile'); if (!profile) { throw new Error('Developer profile not configured. Run "faj init" first.'); } // Load user's real work experiences const experienceManager = ExperienceManager_1.ExperienceManager.getInstance(); await experienceManager.load(); const userExperiences = await experienceManager.getAll(); // Debug: Log profile to see what's being passed this.logger.debug('Profile loaded for resume generation:', { name: profile.name, email: profile.email, phone: profile.phone, location: profile.location, languages: profile.languages, education: profile.education }); this.logger.info(`Including ${userExperiences.length} real work experiences`); try { // Initialize AI manager await this.aiManager.initialize(); // Create enhanced profile with real experiences const enhancedProfile = { ...profile, realExperiences: userExperiences }; // Generate resume using AI with real experiences const generatedResume = await this.aiManager.generateResume(projects, enhancedProfile); // Merge with existing resume if exists if (this.currentResume) { generatedResume.version = this.currentResume.version + 1; generatedResume.id = this.currentResume.id; // Keep same ID } this.currentResume = generatedResume; await this.save(); this.logger.success('Resume generated successfully'); return this.currentResume; } catch (error) { this.logger.error('Failed to generate resume', error); throw new Error(`Failed to generate resume: ${error.message}`); } } async update(changes) { if (!this.currentResume) { await this.loadOrCreate(); } try { // Use AI to intelligently update the resume await this.aiManager.initialize(); const updatedResume = await this.aiManager.updateResume(this.currentResume, changes); this.currentResume = updatedResume; await this.save(); this.logger.success('Resume updated successfully'); return this.currentResume; } catch (error) { this.logger.error('Failed to update resume', error); throw new Error(`Failed to update resume: ${error.message}`); } } async get() { if (!this.currentResume) { await this.loadOrCreate(); } return this.currentResume; } async export(format, themeName) { if (!this.currentResume) { await this.loadOrCreate(); } switch (format) { case 'json': return JSON.stringify(this.currentResume, null, 2); case 'md': return await this.exportMarkdown(); case 'html': return await this.exportHTML(themeName); case 'html-compact': return await this.exportCompactHTML(themeName); case 'pdf': return await this.exportPDF(themeName); default: throw new Error(`Unsupported export format: ${format}`); } } async exportMarkdown() { if (!this.currentResume) { throw new Error('No resume to export'); } const resume = this.currentResume; // Load profile data const profile = await this.configManager.get('profile'); // Ensure basicInfo has languages from profile if (!resume.basicInfo) { resume.basicInfo = { name: profile?.name || '', email: profile?.email || '', phone: profile?.phone || '', location: profile?.location || '', languages: profile?.languages || [] }; } else if (!resume.basicInfo.languages && profile?.languages) { resume.basicInfo.languages = profile.languages; } const titles = (0, SectionTitles_1.getSectionTitles)(resume.basicInfo?.languages); // Load education from profile if not already in resume if (!resume.content.education || resume.content.education.length === 0) { if (profile?.education) { // Handle both old (single object) and new (array) formats const eduArray = Array.isArray(profile.education) ? profile.education : [profile.education]; if (eduArray.length > 0 && eduArray[0]) { resume.content.education = eduArray .filter((edu) => edu && edu.degree && edu.field && edu.institution) .map((edu) => ({ degree: edu.degree, field: edu.field, institution: edu.institution, location: edu.location, startDate: edu.startDate || edu.startYear || '', endDate: edu.endDate || edu.endYear || edu.graduationYear || '', current: edu.current || false, gpa: typeof edu.gpa === 'string' ? parseFloat(edu.gpa) : edu.gpa, highlights: edu.highlights || edu.achievements || [] })); } } } // Load projects from projects.json if not already in resume if (!resume.content.projects || resume.content.projects.length === 0) { try { const projectsPath = path.join(process.env.HOME || '', '.faj', 'projects.json'); const projectsData = await fs.readFile(projectsPath, 'utf-8'); const { projects } = JSON.parse(projectsData); if (projects && projects.length > 0) { resume.content.projects = projects; } } catch { // No projects file exists } } let md = `# Resume\n\n`; // Basic Information if (resume.basicInfo) { md += `## Basic Information\n\n`; md += `**Name:** ${resume.basicInfo.name}\n`; md += `**Email:** ${resume.basicInfo.email}\n`; if (resume.basicInfo.phone) md += `**Phone:** ${resume.basicInfo.phone}\n`; if (resume.basicInfo.location) md += `**Location:** ${resume.basicInfo.location}\n`; if (resume.basicInfo.birthDate) md += `**Birth Date:** ${resume.basicInfo.birthDate}\n`; if (resume.basicInfo.nationality) md += `**Nationality:** ${resume.basicInfo.nationality}\n`; if (resume.basicInfo.languages?.length) { md += `**Languages:** ${resume.basicInfo.languages.join(', ')}\n`; } if (resume.basicInfo.githubUrl) md += `**GitHub:** [${resume.basicInfo.githubUrl}](${resume.basicInfo.githubUrl})\n`; if (resume.basicInfo.linkedinUrl) md += `**LinkedIn:** [${resume.basicInfo.linkedinUrl}](${resume.basicInfo.linkedinUrl})\n`; if (resume.basicInfo.portfolioUrl) md += `**Portfolio:** [${resume.basicInfo.portfolioUrl}](${resume.basicInfo.portfolioUrl})\n`; md += '\n'; } // Professional Summary section removed // Education (moved to second position, after Basic Info) if (resume.content.education.length > 0) { md += `## ${titles.education}\n\n`; for (const edu of resume.content.education) { md += `### ${edu.degree}${edu.field})\n`; md += `${edu.institution}`; if (edu.location) md += `, ${edu.location}`; md += `\n`; md += `*${edu.startDate} - ${edu.current ? 'Present' : edu.endDate}*\n\n`; if (edu.gpa) { md += `**GPA:** ${edu.gpa}\n\n`; } if (edu.highlights && edu.highlights.length > 0) { md += `**Highlights:**\n`; md += edu.highlights.map(h => `- ${h}`).join('\n'); md += '\n\n'; } } } // Work Experience (moved to third position) if (resume.content.experience.length > 0) { md += `## ${titles.workExperience}\n\n`; for (const exp of resume.content.experience) { md += `### ${exp.title}`; if (exp.company) md += ` at ${exp.company}`; md += `\n`; md += `*${exp.startDate} - ${exp.current ? 'Present' : exp.endDate}*\n\n`; md += `${exp.description}\n\n`; if (exp.highlights.length > 0) { md += `**Key Achievements:**\n`; md += exp.highlights.map(h => `- ${h}`).join('\n'); md += '\n\n'; } if (exp.technologies.length > 0) { md += `**Technologies:** ${exp.technologies.join(', ')}\n\n`; } } } // Projects (moved to fourth position) if (resume.content.projects.length > 0) { md += `## Project Experience\n\n`; for (const project of resume.content.projects) { md += `### ${project.name}\n`; md += `${project.description}\n\n`; if (project.role) { md += `**Role:** ${project.role}\n\n`; } if (project.highlights.length > 0) { md += `**Key Features & Achievements:**\n`; md += project.highlights.map(h => `- ${h}`).join('\n'); md += '\n\n'; } if (project.technologies.length > 0) { md += `**Tech Stack:** ${project.technologies.join(', ')}\n\n`; } if (project.url || project.githubUrl) { md += `**Links:** `; if (project.url) md += `[Website](${project.url}) `; if (project.githubUrl) md += `[GitHub](${project.githubUrl})`; md += '\n\n'; } } } // Technical Skills (moved to fifth position) if (resume.content.skills.length > 0) { md += `## ${titles.technicalSkills}\n\n`; const skillsByCategory = this.groupSkillsByCategory(resume.content.skills); for (const [category, skills] of Object.entries(skillsByCategory)) { const categoryLabel = this.getCategoryLabel(category); md += `### ${categoryLabel}\n`; md += skills.map(s => `- ${s.name} (${s.level})`).join('\n'); md += '\n\n'; } } // Certifications if (resume.content.certifications && resume.content.certifications.length > 0) { md += `## Certifications\n\n`; for (const cert of resume.content.certifications) { md += `- **${cert.name}** by ${cert.issuer} (${cert.issueDate})`; if (cert.expiryDate) md += ` - Expires: ${cert.expiryDate}`; if (cert.url) md += ` [View](${cert.url})`; md += '\n'; } } return md; } async tailorToJob(jobDescription) { if (!this.currentResume) { await this.loadOrCreate(); } const resume = this.currentResume; try { await this.aiManager.initialize(); // Tailor experiences to job description const experienceManager = ExperienceManager_1.ExperienceManager.getInstance(); await experienceManager.load(); for (const experience of resume.content.experience || []) { // Find matching experience in manager const allExperiences = await experienceManager.getAll(); const matchingExp = allExperiences.find(e => e.company === experience.company && e.title === experience.title); if (matchingExp) { const tailored = await experienceManager.tailorToJob(matchingExp.id, jobDescription); if (tailored) { // Update resume with tailored content experience.description = tailored.description; experience.highlights = tailored.highlights; experience.technologies = tailored.technologies; } } } // Tailor projects to job description const { ProjectManager } = await Promise.resolve().then(() => __importStar(require('../project/ProjectManager'))); const projectManager = ProjectManager.getInstance(); await projectManager.load(); for (const project of resume.content.projects || []) { // Find matching project in manager const allProjects = await projectManager.getAll(); const matchingProj = allProjects.find(p => p.name === project.name); if (matchingProj) { const tailored = await projectManager.tailorToJob(matchingProj.id, jobDescription); if (tailored) { // Update resume with tailored content project.description = tailored.description; project.highlights = tailored.highlights; project.technologies = tailored.technologies; } } } // Tailor summary to job description const profile = await this.configManager.get('profile'); const prompt = ` Tailor this professional summary to match the job description while maintaining truthfulness: JOB DESCRIPTION: ${jobDescription} CURRENT SUMMARY: ${resume.content.summary} CANDIDATE PROFILE: - Name: ${profile?.name} - Years of Experience: ${profile?.experience || 0} - Skills: ${profile?.skills?.join(', ') || 'Not specified'} - Languages: ${profile?.languages?.join(', ')} RULES: 1. Keep the summary concise (2-3 sentences) 2. Emphasize relevant skills and experience that match the job 3. Use keywords from the job description where they truthfully apply 4. Maintain factual accuracy - do not invent experience 5. Keep a professional tone Generate the tailored summary in ${profile?.languages?.[0] || 'English'}.`; const tailoredSummary = await this.aiManager.processPrompt(prompt); resume.content.summary = tailoredSummary.trim(); // Update resume metadata with tailoring info resume.metadata.lastTailored = new Date(); resume.metadata.tailoredFor = jobDescription.substring(0, 100) + '...'; // Save updated resume this.currentResume = resume; await this.save(); this.logger.success('Resume tailored to job description successfully'); return resume; } catch (error) { this.logger.error('Failed to tailor resume', error); throw error; } } async exportPDF(themeName) { if (!this.currentResume) { throw new Error('No resume to export'); } const resume = this.currentResume; // Load profile data const profile = await this.configManager.get('profile'); // Ensure basicInfo has languages from profile if (!resume.basicInfo) { resume.basicInfo = { name: profile?.name || '', email: profile?.email || '', phone: profile?.phone || '', location: profile?.location || '', languages: profile?.languages || [] }; } else if (!resume.basicInfo.languages && profile?.languages) { resume.basicInfo.languages = profile.languages; } // Load education from profile if not already in resume if (!resume.content.education || resume.content.education.length === 0) { if (profile?.education) { // Handle both old (single object) and new (array) formats const eduArray = Array.isArray(profile.education) ? profile.education : [profile.education]; if (eduArray.length > 0 && eduArray[0]) { resume.content.education = eduArray .filter((edu) => edu && edu.degree && edu.field && edu.institution) .map((edu) => ({ degree: edu.degree, field: edu.field, institution: edu.institution, location: edu.location, startDate: edu.startDate || edu.startYear || '', endDate: edu.endDate || edu.endYear || edu.graduationYear || '', current: edu.current || false, gpa: typeof edu.gpa === 'string' ? parseFloat(edu.gpa) : edu.gpa, highlights: edu.highlights || edu.achievements || [] })); } } } // Load projects from projects.json if not already in resume if (!resume.content.projects || resume.content.projects.length === 0) { try { const projectsPath = path.join(process.env.HOME || '', '.faj', 'projects.json'); const projectsData = await fs.readFile(projectsPath, 'utf-8'); const { projects } = JSON.parse(projectsData); if (projects && projects.length > 0) { resume.content.projects = projects; } } catch { // No projects file exists } } // Use OpenResumePDFGenerator with local TTF fonts (like Open-Resume) const pdfGenerator = new OpenResumePDFGenerator_1.OpenResumePDFGenerator(); try { const pdfBuffer = await pdfGenerator.generatePDF(resume, themeName || 'modern'); return pdfBuffer.toString('base64'); } catch (error) { this.logger.error('Failed to generate PDF', error); throw new Error('Failed to generate PDF'); } } async exportHTML(themeName) { if (!this.currentResume) { throw new Error('No resume to export'); } // Load profile data const profile = await this.configManager.get('profile'); // Ensure basicInfo has languages from profile if (!this.currentResume.basicInfo) { this.currentResume.basicInfo = { name: profile?.name || '', email: profile?.email || '', phone: profile?.phone || '', location: profile?.location || '', languages: profile?.languages || [] }; } else if (!this.currentResume.basicInfo.languages && profile?.languages) { this.currentResume.basicInfo.languages = profile.languages; } // Load education from profile if not already in resume if (!this.currentResume.content.education || this.currentResume.content.education.length === 0) { if (profile?.education) { // Handle both old (single object) and new (array) formats const eduArray = Array.isArray(profile.education) ? profile.education : [profile.education]; if (eduArray.length > 0 && eduArray[0]) { this.currentResume.content.education = eduArray .filter((edu) => edu && edu.degree && edu.field && edu.institution) .map((edu) => ({ degree: edu.degree, field: edu.field, institution: edu.institution, location: edu.location, startDate: edu.startDate || edu.startYear || '', endDate: edu.endDate || edu.endYear || edu.graduationYear || '', current: edu.current || false, gpa: typeof edu.gpa === 'string' ? parseFloat(edu.gpa) : edu.gpa, highlights: edu.highlights || edu.achievements || [] })); } } } // Load projects from projects.json if not already in resume if (!this.currentResume.content.projects || this.currentResume.content.projects.length === 0) { try { const projectsPath = path.join(process.env.HOME || '', '.faj', 'projects.json'); const projectsData = await fs.readFile(projectsPath, 'utf-8'); const { projects } = JSON.parse(projectsData); if (projects && projects.length > 0) { this.currentResume.content.projects = projects; } } catch { // No projects file exists } } return (0, ResumeTemplates_1.generateHTMLResume)(this.currentResume, themeName || 'modern'); } async exportCompactHTML(themeName) { if (!this.currentResume) { throw new Error('No resume to export'); } // Load profile data const profile = await this.configManager.get('profile'); // Ensure basicInfo has languages from profile if (!this.currentResume.basicInfo) { this.currentResume.basicInfo = { name: profile?.name || '', email: profile?.email || '', phone: profile?.phone || '', location: profile?.location || '', languages: profile?.languages || [] }; } else if (!this.currentResume.basicInfo.languages && profile?.languages) { this.currentResume.basicInfo.languages = profile.languages; } // Load education from profile if not already in resume if (!this.currentResume.content.education || this.currentResume.content.education.length === 0) { if (profile?.education) { // Handle both old (single object) and new (array) formats const eduArray = Array.isArray(profile.education) ? profile.education : [profile.education]; if (eduArray.length > 0 && eduArray[0]) { this.currentResume.content.education = eduArray .filter((edu) => edu && edu.degree && edu.field && edu.institution) .map((edu) => ({ degree: edu.degree, field: edu.field, institution: edu.institution, location: edu.location, startDate: edu.startDate || edu.startYear || '', endDate: edu.endDate || edu.endYear || edu.graduationYear || '', current: edu.current || false, gpa: typeof edu.gpa === 'string' ? parseFloat(edu.gpa) : edu.gpa, highlights: edu.highlights || edu.achievements || [] })); } } } // Load projects from projects.json if not already in resume if (!this.currentResume.content.projects || this.currentResume.content.projects.length === 0) { try { const projectsPath = path.join(process.env.HOME || '', '.faj', 'projects.json'); const projectsData = await fs.readFile(projectsPath, 'utf-8'); const { projects } = JSON.parse(projectsData); if (projects && projects.length > 0) { this.currentResume.content.projects = projects; } } catch { // No projects file exists } } return (0, CompactResumeTemplates_1.generateCompactHTMLResume)(this.currentResume, themeName || 'modern'); } getAvailableThemes() { return Object.keys(ResumeTemplates_1.themes).map(key => ({ name: key, description: ResumeTemplates_1.themes[key].description })); } groupSkillsByCategory(skills) { const grouped = {}; for (const skill of skills) { const category = skill.category || 'other'; if (!grouped[category]) { grouped[category] = []; } grouped[category].push(skill); } return grouped; } getCategoryLabel(category) { const labels = { 'language': 'Programming Languages', 'framework': 'Frameworks', 'tool': 'Tools & Technologies', 'database': 'Databases', 'other': 'Other Skills' }; return labels[category] || category; } async save() { if (!this.currentResume) { throw new Error('No resume to save'); } try { await this.ensureResumeDir(); await fs.writeFile(this.resumePath, JSON.stringify(this.currentResume, null, 2), 'utf-8'); this.logger.debug('Resume saved to disk'); } catch (error) { this.logger.error('Failed to save resume', error); throw new Error('Failed to save resume'); } } async ensureResumeDir() { const dir = path.dirname(this.resumePath); try { await fs.access(dir); } catch { await fs.mkdir(dir, { recursive: true }); } } async resumeExists() { try { await fs.access(this.resumePath); return true; } catch { return false; } } async createEmptyResume() { const profile = await this.configManager.get('profile'); const developerId = profile?.id || 'unknown'; return { id: this.generateId(), developerId, version: 1, createdAt: new Date(), updatedAt: new Date(), aiProvider: 'gemini', content: { summary: '', skills: [], experience: [], projects: [], education: [], }, metadata: { hash: '', published: false, }, }; } generateId() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } } exports.ResumeManager = ResumeManager; //# sourceMappingURL=ResumeManager.js.map