UNPKG

faj-cli

Version:

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

625 lines • 26.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WizardCommand = void 0; const chalk_1 = __importDefault(require("chalk")); const ora_1 = __importDefault(require("ora")); const inquirer_1 = __importDefault(require("inquirer")); const Logger_1 = require("../../utils/Logger"); const ConfigManager_1 = require("../../core/config/ConfigManager"); const ExperienceManager_1 = require("../../core/experience/ExperienceManager"); const ProjectAnalyzer_1 = require("../../core/analyzer/ProjectAnalyzer"); const ResumeManager_1 = require("../../core/resume/ResumeManager"); const AIManager_1 = require("../../ai/AIManager"); const path_1 = __importDefault(require("path")); const promises_1 = __importDefault(require("fs/promises")); class WizardCommand { logger; configManager; experienceManager; projectAnalyzer; resumeManager; constructor() { this.logger = new Logger_1.Logger('WizardCommand'); this.configManager = ConfigManager_1.ConfigManager.getInstance(); this.experienceManager = ExperienceManager_1.ExperienceManager.getInstance(); this.projectAnalyzer = new ProjectAnalyzer_1.ProjectAnalyzer(); this.resumeManager = ResumeManager_1.ResumeManager.getInstance(); } register(program) { const wizard = program .command('wizard') .alias('w') .description('Complete resume setup wizard for new users') .option('--quick', 'Quick mode with minimal questions') .action(async (options) => { await this.execute(options); }); wizard .command('resume') .description('Create a complete resume from scratch') .action(async () => { await this.execute({ resumeOnly: true }); }); } async execute(options) { try { 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')); // Step 1: Basic Information console.log(chalk_1.default.yellow.bold('Step 1/6: Personal Information')); const basicInfo = await this.collectBasicInfo(options.quick); // Step 2: Education console.log(chalk_1.default.yellow.bold('\nStep 2/6: Education Background')); const education = await this.collectEducation(options.quick); // Step 3: Work Experience console.log(chalk_1.default.yellow.bold('\nStep 3/6: Work Experience')); const experiences = await this.collectWorkExperiences(options.quick); // Step 4: AI Configuration console.log(chalk_1.default.yellow.bold('\nStep 4/6: AI Configuration')); const aiConfigured = await this.configureAI(); // Step 5: Project Analysis console.log(chalk_1.default.yellow.bold('\nStep 5/6: Project Analysis')); const projects = await this.analyzeProjects(); // Step 6: Generate Resume console.log(chalk_1.default.yellow.bold('\nStep 6/6: Generate Resume')); await this.generateCompleteResume(basicInfo, education, experiences, projects, aiConfigured); // Final: 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'); } catch (error) { this.logger.error('Wizard failed', error); console.error(chalk_1.default.red('āœ– Wizard failed:'), error); process.exit(1); } } async collectBasicInfo(quick = false) { const questions = [ { type: 'input', name: 'name', message: 'Your full name:', validate: (input) => input.length > 0 || 'Name is required' }, { type: 'input', name: 'email', message: 'Email address:', validate: (input) => { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(input) || 'Please enter a valid email'; } }, { type: 'input', name: 'phone', message: 'Phone number:', default: '' }, { type: 'input', name: 'location', message: 'Location (e.g., Beijing, China):', default: '' }, { type: 'input', name: 'githubUrl', message: 'GitHub URL (optional):', default: '', when: !quick }, { type: 'input', name: 'linkedinUrl', message: 'LinkedIn URL (optional):', default: '', when: !quick }, { type: 'input', name: 'languages', message: 'Languages (comma-separated, e.g., Chinese, English):', default: 'Chinese, English', filter: (input) => input.split(',').map(s => s.trim()).filter(s => s) }, { type: 'editor', name: 'careerObjective', message: 'Career objective (press Enter to open editor):', default: 'Seeking challenging opportunities in software development', when: !quick } ]; const answers = await inquirer_1.default.prompt(questions); // Save to config await this.configManager.load(); const profile = { ...this.configManager.getProfile(), role: 'developer', ...answers }; await this.configManager.updateProfile(profile); console.log(chalk_1.default.green('āœ“ Personal information saved')); return answers; } async collectEducation(quick = false) { const questions = [ { type: 'input', name: 'degree', message: 'Degree (e.g., Bachelor, Master):', default: 'Bachelor' }, { 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: new Date().getFullYear().toString(), validate: (input) => { const year = parseInt(input); return (!isNaN(year) && year > 1900 && year <= new Date().getFullYear() + 10) || 'Please enter a valid year'; } }, { type: 'input', name: 'gpa', message: 'GPA (optional):', default: '', when: !quick } ]; const education = await inquirer_1.default.prompt(questions); // Save to config await this.configManager.updateProfile({ ...this.configManager.getProfile(), education }); console.log(chalk_1.default.green('āœ“ Education information saved')); return education; } async collectWorkExperiences(quick = false) { const experiences = []; 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 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: 'editor', name: 'description', message: 'Describe your role and achievements (press Enter to open editor):' }, { type: 'input', name: 'technologies', message: 'Technologies used (comma-separated):', filter: (input) => input.split(',').map(s => s.trim()).filter(s => s) } ]); // Process and 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`)); // 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; } 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 configureAI() { const { hasApiKey } = await inquirer_1.default.prompt([ { type: 'confirm', name: 'hasApiKey', message: 'Do you have an AI API key (OpenAI/Gemini/Anthropic)?', default: true } ]); if (!hasApiKey) { console.log(chalk_1.default.yellow('⚠ AI features will be limited without an API key')); console.log(chalk_1.default.gray('You can configure it later with: faj config ai')); return false; } const { provider } = await inquirer_1.default.prompt([ { type: 'list', name: 'provider', message: 'Select AI provider:', choices: [ { name: 'OpenAI (GPT-4)', value: 'openai' }, { name: 'Google Gemini', value: 'gemini' }, { name: 'Anthropic Claude', value: 'anthropic' } ], default: 'gemini' } ]); 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' } ]); // Save AI configuration await this.configManager.updateAIConfig({ provider, apiKey, model: provider === 'gemini' ? 'gemini-pro' : provider === 'openai' ? 'gpt-4' : 'claude-3-opus' }); console.log(chalk_1.default.green(`āœ“ ${provider} AI configured successfully`)); return true; } 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.analyzeLocal(projectPath); projects.push(analysis); 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.analyzeGitHub(githubUrl); projects.push(analysis); 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(basicInfo, education, experiences, projects, aiConfigured) { const spinner = (0, ora_1.default)('Generating resume...').start(); try { // Load existing resume or create new await this.resumeManager.load(); if (projects.length > 0 && aiConfigured) { // Generate with AI if projects and AI are available const aiManager = AIManager_1.AIManager.getInstance(); await 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 resume = { basicInfo: { name: basicInfo.name, email: basicInfo.email, phone: basicInfo.phone, location: basicInfo.location, githubUrl: basicInfo.githubUrl, linkedinUrl: basicInfo.linkedinUrl, languages: basicInfo.languages }, content: { summary: basicInfo.careerObjective || '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: proj.languages || [] })), skills: this.extractSkillsFromExperiences(experiences, projects), education: [{ degree: education.degree, field: education.field, institution: education.institution, graduationYear: education.graduationYear, gpa: education.gpa }] }, metadata: { version: 1, generatedAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), aiProvider: aiConfigured ? this.configManager.getAIConfig()?.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('Basic resume created'); } } catch (error) { spinner.fail(`Failed to generate resume: ${error.message}`); throw error; } } extractSkillsFromExperiences(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.WizardCommand = WizardCommand; //# sourceMappingURL=wizard.js.map