UNPKG

faj-cli

Version:

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

872 lines 37.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.InitCommand = void 0; const inquirer_1 = __importDefault(require("inquirer")); const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); const ConfigManager_1 = require("../../core/config/ConfigManager"); const AIManager_1 = require("../../ai/AIManager"); const Logger_1 = require("../../utils/Logger"); const ExperienceManager_1 = require("../../core/experience/ExperienceManager"); const ProjectAnalyzer_1 = require("../../core/analyzer/ProjectAnalyzer"); const ResumeManager_1 = require("../../core/resume/ResumeManager"); const path_1 = __importDefault(require("path")); const promises_1 = __importDefault(require("fs/promises")); class InitCommand { logger; configManager; aiManager; experienceManager; projectAnalyzer; resumeManager; constructor() { this.logger = new Logger_1.Logger('InitCommand'); this.configManager = ConfigManager_1.ConfigManager.getInstance(); this.aiManager = AIManager_1.AIManager.getInstance(); this.experienceManager = ExperienceManager_1.ExperienceManager.getInstance(); this.projectAnalyzer = new ProjectAnalyzer_1.ProjectAnalyzer(); this.resumeManager = ResumeManager_1.ResumeManager.getInstance(); } register(program) { program .command('init') .description('Complete setup wizard to create your resume') .option('--quick', 'Quick mode with minimal questions') .option('--resume-only', 'Focus only on resume creation') .action(async (options) => { try { await this.execute(options); } catch (error) { this.logger.error('Initialization failed', error); process.exit(1); } }); } async execute(options = {}) { console.log(chalk_1.default.cyan.bold('\n🎯 FAJ Resume Creation Wizard\n')); console.log(chalk_1.default.gray('This wizard will help you create a complete professional resume.\n')); // Check if already initialized const existingConfig = await this.configManager.get('profile'); if (existingConfig && !options.resumeOnly) { const { overwrite } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'overwrite', message: 'FAJ is already initialized. Do you want to create a new resume from scratch?', default: false, }, ]); if (!overwrite) { console.log(chalk_1.default.yellow('Use "faj resume" commands to modify existing resume.')); return; } } // Get user role const { role } = await inquirer_1.default.prompt([ { type: 'list', name: 'role', message: 'Select your role:', choices: [ { name: 'Developer - Build resume from projects', value: 'developer' }, { name: 'Recruiter - Find matching developers', value: 'recruiter' }, ], }, ]); // Get basic profile information const profileAnswers = await inquirer_1.default.prompt([ { type: 'input', name: 'name', message: 'Your name:', validate: (input) => input.length > 0 || 'Name is required', }, { type: 'input', name: 'email', message: 'Your email:', validate: (input) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(input) || 'Please enter a valid email'; }, }, ]); let profile = { id: this.generateId(), role, name: profileAnswers.name, email: profileAnswers.email, createdAt: new Date(), updatedAt: new Date(), }; // Additional questions for developers if (role === 'developer') { const devAnswers = await inquirer_1.default.prompt([ { type: 'input', name: 'phone', message: 'Phone number (optional):', }, { type: 'input', name: 'birthDate', message: 'Birth date (YYYY-MM-DD, optional):', }, { type: 'input', name: 'nationality', message: 'Nationality (optional):', }, { type: 'input', name: 'languages', message: 'Languages you speak (comma-separated, e.g., Chinese,English):', default: 'Chinese,English', }, { type: 'input', name: 'githubUsername', message: 'GitHub username (optional):', }, { type: 'input', name: 'linkedinUrl', message: 'LinkedIn URL (optional):', }, { type: 'input', name: 'experience', message: 'Years of experience:', default: '0', validate: (input) => !isNaN(Number(input)) || 'Please enter a valid number', }, { type: 'input', name: 'location', message: 'Location (city, country):', default: 'Beijing, China', }, ]); // Education information console.log(chalk_1.default.cyan('\n📚 Education Information:\n')); const eduAnswers = await inquirer_1.default.prompt([ { type: 'input', name: 'degree', message: 'Degree (e.g., Bachelor of Science):', default: 'Bachelor of Science', }, { type: 'input', name: 'field', message: 'Field of study:', default: 'Computer Science', }, { type: 'input', name: 'institution', message: 'University/Institution:', validate: (input) => input.length > 0 || 'Institution is required', }, { type: 'input', name: 'graduationYear', message: 'Graduation year:', default: '2020', }, { type: 'input', name: 'gpa', message: 'GPA (optional, e.g., 3.8/4.0):', }, ]); // Career objective and personal summary console.log(chalk_1.default.cyan('\n💼 Career Information:\n')); const careerAnswers = await inquirer_1.default.prompt([ { type: 'input', name: 'careerObjective', message: 'Career objective (1-2 sentences):', default: 'Seeking challenging opportunities to apply my technical skills in building innovative solutions.', }, { type: 'input', name: 'personalSummary', message: 'Brief personal introduction (optional):', }, ]); profile = { ...profile, phone: devAnswers.phone || undefined, birthDate: devAnswers.birthDate || undefined, nationality: devAnswers.nationality || undefined, languages: devAnswers.languages ? devAnswers.languages.split(',').map((l) => l.trim()) : [], githubUsername: devAnswers.githubUsername || undefined, linkedinUrl: devAnswers.linkedinUrl || undefined, experience: Number(devAnswers.experience), location: devAnswers.location || undefined, education: { degree: eduAnswers.degree, field: eduAnswers.field, institution: eduAnswers.institution, graduationYear: eduAnswers.graduationYear, gpa: eduAnswers.gpa || undefined, }, careerObjective: careerAnswers.careerObjective || undefined, personalSummary: careerAnswers.personalSummary || undefined, skills: [], }; } // Additional questions for recruiters if (role === 'recruiter') { const recAnswers = await inquirer_1.default.prompt([ { type: 'input', name: 'company', message: 'Company name:', validate: (input) => input.length > 0 || 'Company name is required', }, { type: 'input', name: 'position', message: 'Your position (optional):', }, { type: 'input', name: 'companyUrl', message: 'Company website (optional):', }, ]); profile = { ...profile, company: recAnswers.company, position: recAnswers.position || undefined, companyUrl: recAnswers.companyUrl || undefined, }; } // AI Provider configuration const { configureAI } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'configureAI', message: 'Would you like to configure an AI provider now?', default: true, }, ]); let aiConfig; if (configureAI) { aiConfig = await this.configureAIProvider(); } // Save configuration const spinner = (0, ora_1.default)('Saving configuration...').start(); try { await this.configManager.set('profile', profile); if (aiConfig) { await this.configManager.set('ai', aiConfig); } spinner.succeed('Configuration saved successfully!'); spinner.succeed('Configuration saved successfully!'); // For developers, continue with full resume creation if (role === 'developer') { console.log(chalk_1.default.green('\n✓ Basic profile created!\n')); // Step 2: Collect Work Experience console.log(chalk_1.default.yellow.bold('\nStep 2: Work Experience')); await this.collectWorkExperiences(options.quick, aiConfig !== null); // Step 3: Analyze Projects console.log(chalk_1.default.yellow.bold('\nStep 3: Project Analysis')); const projects = await this.analyzeProjects(); // Step 4: Generate Resume console.log(chalk_1.default.yellow.bold('\nStep 4: Generate Resume')); await this.generateCompleteResume(profile, projects, aiConfig !== null); // Step 5: Export Options await this.exportOptions(); console.log(chalk_1.default.green.bold('\n✨ Resume creation completed successfully!\n')); console.log(chalk_1.default.cyan('You can now use these commands:')); console.log(' • ' + chalk_1.default.white('faj resume show') + ' - View your resume'); console.log(' • ' + chalk_1.default.white('faj resume export md') + ' - Export as Markdown'); console.log(' • ' + chalk_1.default.white('faj resume export html') + ' - Export as HTML'); console.log(' • ' + chalk_1.default.white('faj experience add') + ' - Add more work experiences'); console.log(' • ' + chalk_1.default.white('faj analyze <path>') + ' - Analyze more projects\n'); } else { console.log(chalk_1.default.green('\n✓ FAJ initialized successfully!\n')); console.log('Next steps:'); console.log(' 1. Post a job: ' + chalk_1.default.cyan('faj post <job.json>')); console.log(' 2. Search developers: ' + chalk_1.default.cyan('faj search')); console.log(' 3. Subscribe to alerts: ' + chalk_1.default.cyan('faj subscribe')); console.log('\nRun ' + chalk_1.default.cyan('faj help') + ' to see all available commands.'); } } catch (error) { spinner.fail('Failed to save configuration'); throw error; } } async configureAIProvider() { const { provider } = await inquirer_1.default.prompt([ { type: 'list', name: 'provider', message: 'Choose AI provider for resume generation:', choices: [ { name: 'OpenAI (GPT-4)', value: 'openai' }, { name: 'Google Gemini', value: 'gemini' }, { name: 'Anthropic Claude', value: 'anthropic' }, { name: 'Use environment variable', value: 'env' }, { name: 'Configure later', value: 'later' }, ], }, ]); if (provider === 'later') { console.log(chalk_1.default.yellow('\nYou can configure AI provider later with: faj config ai')); return null; } if (provider === 'env') { console.log(chalk_1.default.yellow('\nUsing environment variables for AI configuration.')); console.log('Set the following environment variables:'); console.log(' - FAJ_AI_PROVIDER=<provider>'); console.log(' - FAJ_<PROVIDER>_API_KEY=<your-api-key>'); return null; } const { apiKey } = await inquirer_1.default.prompt([ { type: 'password', name: 'apiKey', message: `Enter your ${provider} API key:`, mask: '*', validate: (input) => input.length > 0 || 'API key is required', }, ]); // Test the API key const spinner = (0, ora_1.default)('Testing API key...').start(); try { // Store API key in environment for this session process.env[`FAJ_${provider.toUpperCase()}_API_KEY`] = apiKey; await this.aiManager.initialize(); const isValid = await this.aiManager.testProvider(provider); if (isValid) { spinner.succeed('API key validated successfully!'); const { saveKey } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'saveKey', message: 'Save API key for future use? (stored securely)', default: true, }, ]); if (saveKey) { // In a real implementation, use keytar or similar console.log(chalk_1.default.yellow('Note: Secure storage implementation pending. Using environment variable.')); } return { provider: provider, model: this.getDefaultModel(provider), }; } else { spinner.fail('Invalid API key'); console.log(chalk_1.default.red('The API key appears to be invalid. Please check and try again.')); return this.configureAIProvider(); // Recursive retry } } catch (error) { spinner.fail('API key validation failed'); console.log(chalk_1.default.red('Error: ' + error.message)); const { retry } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'retry', message: 'Would you like to try again?', default: true, }, ]); if (retry) { return this.configureAIProvider(); } return null; } } getDefaultModel(provider) { switch (provider) { case 'openai': return 'gpt-4'; case 'gemini': return 'gemini-pro'; case 'anthropic': return 'claude-3-opus-20240229'; default: return 'default'; } } generateId() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } async collectWorkExperiences(quick = false, aiConfigured = false) { await this.experienceManager.load(); const experiences = []; const polishPromises = []; const { hasExperience } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'hasExperience', message: 'Do you have work experience to add?', default: true } ]); if (!hasExperience) { console.log(chalk_1.default.gray('Skipping work experience...')); return experiences; } let addMore = true; while (addMore) { console.log(chalk_1.default.cyan(`\nWork Experience #${experiences.length + 1}:`)); // Show template example for first experience if (!quick && experiences.length === 0) { console.log(chalk_1.default.gray('\nExample description:')); console.log(chalk_1.default.gray('I worked as a Senior Backend Engineer at Tencent, responsible for')); console.log(chalk_1.default.gray('developing WeChat Pay transaction systems. Led a team of 3 engineers,')); console.log(chalk_1.default.gray('optimized payment gateway reducing response time by 75%.')); console.log(chalk_1.default.gray('Used Java, Spring Boot, MySQL, Redis, Kubernetes.\n')); } const experience = await inquirer_1.default.prompt([ { type: 'input', name: 'company', message: 'Company name:', validate: (input) => input.length > 0 || 'Company name is required' }, { type: 'input', name: 'title', message: 'Job title:', validate: (input) => input.length > 0 || 'Job title is required' }, { type: 'input', name: 'startDate', message: 'Start date (YYYY-MM):', default: '2020-01', validate: (input) => { return /^\d{4}-\d{2}$/.test(input) || 'Please use YYYY-MM format'; } }, { type: 'confirm', name: 'current', message: 'Is this your current position?', default: false }, { type: 'input', name: 'endDate', message: 'End date (YYYY-MM):', when: (answers) => !answers.current, validate: (input) => { return /^\d{4}-\d{2}$/.test(input) || 'Please use YYYY-MM format'; } }, { type: 'input', name: 'description', message: 'Describe your role and achievements (one line, you can add more details later):', validate: (input) => input.length > 10 || 'Please provide a meaningful description' }, { type: 'input', name: 'technologies', message: 'Technologies used (comma-separated):', filter: (input) => input.split(',').map(s => s.trim()).filter(s => s) } ]); // Extract highlights from description const highlights = this.extractHighlights(experience.description); // Save experience const savedExp = await this.experienceManager.add({ ...experience, highlights, rawDescription: experience.description, polished: false }); experiences.push(savedExp); console.log(chalk_1.default.green(`✓ Work experience at ${experience.company} saved`)); // Start AI polish in background if AI is configured if (aiConfigured && !quick) { console.log(chalk_1.default.gray(' 🤖 AI polishing in background...')); const polishPromise = this.experienceManager.polish(savedExp.id, experience.description) .then(polished => { if (polished) { // Update the experience in the array const index = experiences.findIndex(e => e.id === savedExp.id); if (index >= 0) { experiences[index] = polished; } return polished; } return savedExp; }) .catch(error => { this.logger.warn('AI polish failed, using original', error); return savedExp; }); polishPromises.push(polishPromise); } // Ask if want to add more const { more } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'more', message: 'Add another work experience?', default: false } ]); addMore = more; } // Wait for all AI polish operations to complete if (polishPromises.length > 0) { console.log(chalk_1.default.cyan('\n⏳ Waiting for AI to polish experiences...')); const polishedResults = await Promise.all(polishPromises); // Show polish results console.log(chalk_1.default.green('\n✨ AI Polish Complete!\n')); let hasChanges = false; for (let i = 0; i < polishedResults.length; i++) { const polished = polishedResults[i]; if (polished && polished.polished) { hasChanges = true; console.log(chalk_1.default.cyan(`📝 ${polished.title} at ${polished.company}:`)); console.log(chalk_1.default.gray('Enhanced Description:')); console.log(` ${polished.description}\n`); if (polished.highlights && polished.highlights.length > 0) { console.log(chalk_1.default.gray('Key Achievements:')); polished.highlights.slice(0, 3).forEach((h) => { console.log(` • ${h}`); }); console.log(); } } } if (hasChanges) { const { acceptAll } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'acceptAll', message: 'Accept all AI enhancements?', default: true } ]); if (!acceptAll) { // If user doesn't accept, revert to original descriptions for (let i = 0; i < experiences.length; i++) { const original = experiences[i]; if (original.rawDescription) { experiences[i] = { ...original, description: original.rawDescription, polished: false }; } } console.log(chalk_1.default.yellow('Using original descriptions')); } else { console.log(chalk_1.default.green('✓ AI enhancements applied')); } } } return experiences; } extractHighlights(description) { const highlights = []; // Extract sentences with numbers or achievement keywords const sentences = description.split(/[.!?]+/).filter(s => s.trim()); for (const sentence of sentences) { const trimmed = sentence.trim(); // Check for achievement indicators if (/\d+/.test(trimmed) || // Has numbers /led|managed|improved|reduced|increased|built|created|developed|implemented/i.test(trimmed)) { if (trimmed.length > 20 && trimmed.length < 200) { // Reasonable length highlights.push(trimmed); } } } // Return top 5 highlights return highlights.slice(0, 5); } async analyzeProjects() { const { analyzeProjects } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'analyzeProjects', message: 'Do you want to analyze your code projects?', default: true } ]); if (!analyzeProjects) { console.log(chalk_1.default.gray('Skipping project analysis...')); return []; } const projects = []; let addMore = true; while (addMore) { const { projectType } = await inquirer_1.default.prompt([ { type: 'list', name: 'projectType', message: 'Project type:', choices: [ { name: 'Local directory', value: 'local' }, { name: 'GitHub repository', value: 'github' }, { name: 'Skip projects', value: 'skip' } ] } ]); if (projectType === 'skip') { break; } if (projectType === 'local') { const { projectPath } = await inquirer_1.default.prompt([ { type: 'input', name: 'projectPath', message: 'Enter project path:', default: process.cwd(), validate: async (input) => { try { const stats = await promises_1.default.stat(input); return stats.isDirectory() || 'Path must be a directory'; } catch { return 'Path does not exist'; } } } ]); const spinner = (0, ora_1.default)('Analyzing project...').start(); try { const analysis = await this.projectAnalyzer.analyze(projectPath); const projectAnalysis = analysis; projects.push(projectAnalysis); spinner.succeed(`Analyzed ${path_1.default.basename(projectPath)}`); } catch (error) { spinner.fail(`Failed to analyze: ${error.message}`); } } else if (projectType === 'github') { const { githubUrl } = await inquirer_1.default.prompt([ { type: 'input', name: 'githubUrl', message: 'Enter GitHub repository URL:', validate: (input) => { return /^https:\/\/github\.com\/[\w-]+\/[\w-]+/.test(input) || 'Please enter a valid GitHub URL'; } } ]); const spinner = (0, ora_1.default)('Analyzing GitHub repository...').start(); try { const analysis = await this.projectAnalyzer.analyze(githubUrl); const projectAnalysis = analysis; projects.push(projectAnalysis); spinner.succeed(`Analyzed ${githubUrl.split('/').pop()}`); } catch (error) { spinner.fail(`Failed to analyze: ${error.message}`); } } const { more } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'more', message: 'Analyze another project?', default: false } ]); addMore = more; } return projects; } async generateCompleteResume(profile, projects, aiConfigured) { const spinner = (0, ora_1.default)('Generating resume...').start(); try { // Load existing resume if any try { const resumePath = path_1.default.join(process.env.HOME || '', '.faj', 'resume.json'); await promises_1.default.access(resumePath); } catch { // Resume doesn't exist yet, that's fine } if (projects.length > 0 && aiConfigured) { // Generate with AI if projects and AI are available await this.aiManager.initialize(); // The ResumeManager will automatically use experiences from ExperienceManager await this.resumeManager.generateFromProjects(projects); spinner.succeed('Resume generated with AI'); } else { // Create basic resume without AI const experiences = await this.experienceManager.getAll(); const resume = { basicInfo: { name: profile.name, email: profile.email, phone: profile.phone, location: profile.location, birthDate: profile.birthDate, nationality: profile.nationality, languages: profile.languages, githubUrl: profile.githubUsername ? `https://github.com/${profile.githubUsername}` : undefined, linkedinUrl: profile.linkedinUrl, portfolioUrl: undefined }, content: { summary: profile.careerObjective || profile.personalSummary || 'Experienced software developer seeking challenging opportunities', experience: experiences.map((exp) => ({ title: exp.title, company: exp.company, startDate: exp.startDate, endDate: exp.endDate, current: exp.current, description: exp.description, highlights: exp.highlights || [], technologies: exp.technologies || [] })), projects: projects.map((proj) => ({ name: proj.name, description: proj.description || 'Software development project', details: [], techStack: Object.keys(proj.languages || {}) })), skills: this.extractSkillsFromData(experiences, projects), education: profile.education ? [{ degree: profile.education.degree, field: profile.education.field, institution: profile.education.institution, graduationYear: profile.education.graduationYear, gpa: profile.education.gpa }] : [] }, metadata: { version: 1, generatedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), aiProvider: aiConfigured ? (await this.configManager.get('ai'))?.provider : 'none' } }; // Save resume const resumePath = path_1.default.join(process.env.HOME || '', '.faj', 'resume.json'); await promises_1.default.mkdir(path_1.default.dirname(resumePath), { recursive: true }); await promises_1.default.writeFile(resumePath, JSON.stringify(resume, null, 2)); spinner.succeed('Resume created successfully'); } } catch (error) { spinner.fail(`Failed to generate resume: ${error.message}`); throw error; } } extractSkillsFromData(experiences, projects) { const skills = []; const skillSet = new Set(); // Extract from experiences experiences.forEach((exp) => { if (exp.technologies) { exp.technologies.forEach((tech) => { if (!skillSet.has(tech)) { skillSet.add(tech); skills.push({ name: tech, level: 'advanced', category: this.categorizeSkill(tech) }); } }); } }); // Extract from projects projects.forEach((proj) => { if (proj.languages) { Object.keys(proj.languages).forEach((lang) => { if (!skillSet.has(lang)) { skillSet.add(lang); skills.push({ name: lang, level: 'advanced', category: 'language' }); } }); } }); return skills; } categorizeSkill(skill) { const databases = ['MySQL', 'PostgreSQL', 'MongoDB', 'Redis', 'Cassandra']; const frameworks = ['React', 'Vue', 'Angular', 'Spring', 'Django', 'Express']; const tools = ['Docker', 'Kubernetes', 'Git', 'Jenkins', 'Nginx']; if (databases.some(db => skill.toLowerCase().includes(db.toLowerCase()))) { return 'database'; } if (frameworks.some(fw => skill.toLowerCase().includes(fw.toLowerCase()))) { return 'framework'; } if (tools.some(tool => skill.toLowerCase().includes(tool.toLowerCase()))) { return 'tool'; } return 'other'; } async exportOptions() { const { exportNow } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'exportNow', message: 'Do you want to export your resume now?', default: true } ]); if (!exportNow) { return; } const { format } = await inquirer_1.default.prompt([ { type: 'list', name: 'format', message: 'Export format:', choices: [ { name: 'Markdown (.md)', value: 'md' }, { name: 'HTML (.html)', value: 'html' }, { name: 'Skip export', value: 'skip' } ] } ]); if (format === 'skip') { return; } const { filename } = await inquirer_1.default.prompt([ { type: 'input', name: 'filename', message: 'Export filename:', default: `resume.${format}` } ]); const spinner = (0, ora_1.default)(`Exporting to ${filename}...`).start(); try { const content = await this.resumeManager.export(format); await promises_1.default.writeFile(filename, content); spinner.succeed(`Resume exported to ${filename}`); } catch (error) { spinner.fail(`Export failed: ${error.message}`); } } } exports.InitCommand = InitCommand; //# sourceMappingURL=init.js.map