faj-cli
Version:
FAJ - A powerful CLI resume builder with AI enhancement and multi-format export
703 lines (698 loc) • 30.8 kB
JavaScript
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
;