faj-cli
Version:
FAJ - A powerful CLI resume builder with AI enhancement and multi-format export
625 lines ⢠26.3 kB
JavaScript
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
;