faj-cli
Version:
FAJ - A powerful CLI resume builder with AI enhancement and multi-format export
1,071 lines (1,065 loc) โข 169 kB
JavaScript
"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;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.InteractiveCommand = void 0;
const chalk_1 = __importDefault(require("chalk"));
const inquirer_1 = __importDefault(require("inquirer"));
const ora_1 = __importDefault(require("ora"));
const ConfigManager_1 = require("../../core/config/ConfigManager");
const ResumeManager_1 = require("../../core/resume/ResumeManager");
const ExperienceManager_1 = require("../../core/experience/ExperienceManager");
const ProjectAnalyzer_1 = require("../../core/analyzer/ProjectAnalyzer");
const ProjectManager_1 = require("../../core/project/ProjectManager");
const AIManager_1 = require("../../ai/AIManager");
const Logger_1 = require("../../utils/Logger");
const EducationOptions_1 = require("../../utils/EducationOptions");
const MenuHelper_1 = require("../../utils/MenuHelper");
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const os = __importStar(require("os"));
class InteractiveCommand {
logger;
configManager;
resumeManager;
experienceManager;
projectAnalyzer;
projectManager;
aiManager;
constructor() {
this.logger = new Logger_1.Logger('InteractiveCommand');
this.configManager = ConfigManager_1.ConfigManager.getInstance();
this.resumeManager = ResumeManager_1.ResumeManager.getInstance();
this.experienceManager = ExperienceManager_1.ExperienceManager.getInstance();
this.projectAnalyzer = new ProjectAnalyzer_1.ProjectAnalyzer();
this.projectManager = ProjectManager_1.ProjectManager.getInstance();
this.aiManager = AIManager_1.AIManager.getInstance();
}
register(program) {
// Main interactive command - when user just types 'faj'
program
.action(async () => {
await this.start();
});
// Also register specific commands for backward compatibility
// But they all lead to interactive mode
program
.command('add')
.description('Add content interactively')
.action(async () => {
await this.start('add');
});
program
.command('export')
.description('Export resume interactively')
.action(async () => {
await this.start('export');
});
program
.command('config')
.description('Configure settings interactively')
.action(async () => {
await this.start('config');
});
}
async start(directAction) {
try {
// Check if first time user
const isNewUser = !(await this.checkExistingConfig());
if (isNewUser) {
await this.welcomeNewUser();
}
else {
await this.showMainMenu(directAction);
}
}
catch (error) {
this.logger.error('Interactive command failed', error);
console.error(chalk_1.default.red('Error:'), error);
}
}
async checkExistingConfig() {
try {
const configPath = path_1.default.join(os.homedir(), '.faj', 'config.json');
await promises_1.default.access(configPath);
await this.configManager.load();
const profile = await this.configManager.get('profile');
// Check if profile exists and has basic required info
return profile !== null && profile !== undefined && profile.id;
}
catch {
return false;
}
}
async welcomeNewUser() {
console.clear();
console.log(chalk_1.default.cyan.bold('\n๐ Welcome to FAJ / ๆฌข่ฟไฝฟ็จFAJ!\n'));
console.log(chalk_1.default.white('Quick setup - just language and AI / ๅฟซ้่ฎพ็ฝฎ - ไป
้่ฏญ่จๅAI้
็ฝฎ\n'));
// Quick language and AI setup
await this.quickSetup();
// After setup, show main menu
await this.showMainMenu();
}
async quickSetup() {
// Step 1: Language selection - same as in configureLanguages
const { primaryLang } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'primaryLang',
message: 'Select your primary language / ้ๆฉไธป่ฆ่ฏญ่จ:',
choices: [
{ name: 'English', value: 'English' },
{ name: 'Chinese (ไธญๆ)', value: 'Chinese' },
{ name: 'Spanish (Espaรฑol)', value: 'Spanish' },
{ name: 'French (Franรงais)', value: 'French' },
{ name: 'German (Deutsch)', value: 'German' },
{ name: 'Japanese (ๆฅๆฌ่ช)', value: 'Japanese' },
{ name: 'Korean (ํ๊ตญ์ด)', value: 'Korean' },
{ name: 'Portuguese', value: 'Portuguese' },
{ name: 'Russian', value: 'Russian' },
{ name: 'Italian', value: 'Italian' }
],
default: 'Chinese'
}
]);
const isChinese = primaryLang === 'Chinese';
// Step 2: AI Provider configuration - same as in configureAI
const { configureAI } = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'configureAI',
message: isChinese ?
'AIๅฏไปฅๅธฎไฝ ไผๅ็ฎๅๅ
ๅฎน๏ผ็ฐๅจ้
็ฝฎๅ๏ผ' :
'AI can help optimize your resume. Configure now?',
default: true
}
]);
let aiConfig = null;
if (configureAI) {
const { provider } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'provider',
message: isChinese ? 'Choose AI provider / ้ๆฉAIๆไพๅ:' : 'Choose AI provider:',
choices: [
{ name: 'โจ Google Gemini (gemini-2.5-pro)', value: 'gemini' },
{ name: '๐ง OpenAI (gpt-5)', value: 'openai' },
{ name: '๐ DeepSeek (deepseek-reasoner)', value: 'deepseek' },
{ name: isChinese ? '็จๅ้
็ฝฎ / Skip' : 'Skip for now', value: 'skip' },
],
},
]);
if (provider !== 'skip') {
const { apiKey } = await inquirer_1.default.prompt([
{
type: 'password',
name: 'apiKey',
message: isChinese ? `่พๅ
ฅ${provider} APIๅฏ้ฅ:` : `Enter ${provider} API key:`,
mask: '*',
validate: (input) => input.length > 0 || 'API key required'
}
]);
// Save API key - same as in configureAI
await this.configManager.setAIApiKey(provider, apiKey);
// Set default model based on provider
const defaultModels = {
'gemini': 'gemini-2.5-pro',
'openai': 'gpt-5',
'deepseek': 'deepseek-reasoner'
};
aiConfig = {
provider: provider,
apiKeys: {
[provider]: apiKey
},
models: defaultModels // Include models like in configureAI
};
}
}
// Save minimal configuration
const spinner = (0, ora_1.default)(isChinese ? 'ไฟๅญ้
็ฝฎ...' : 'Saving configuration...').start();
try {
// Create minimal profile with languages array (consistent with configureLanguages)
const languages = primaryLang === 'Chinese' ? ['Chinese', 'English'] :
primaryLang === 'English' ? ['English'] :
[primaryLang, 'English'];
const profile = {
id: this.generateId(),
role: 'developer',
name: '',
email: '',
languages: languages, // Primary language first in array
createdAt: new Date(),
updatedAt: new Date(),
};
await this.configManager.set('profile', profile);
if (aiConfig) {
await this.configManager.set('ai', aiConfig);
}
spinner.succeed(isChinese ? '้
็ฝฎไฟๅญๆๅ๏ผ' : 'Configuration saved!');
console.log(chalk_1.default.green(isChinese ? '\nโจ ่ฎพ็ฝฎๅฎๆ๏ผ็ฐๅจๅฏไปฅๅผๅงๅๅปบ็ฎๅไบใ\n' : '\nโจ Setup complete! You can now start creating your resume.\n'));
}
catch (error) {
spinner.fail(isChinese ? '้
็ฝฎไฟๅญๅคฑ่ดฅ' : 'Failed to save configuration');
throw error;
}
}
generateId() {
return Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
async showMainMenu(directAction) {
// If direct action specified, execute it
if (directAction) {
switch (directAction) {
case 'add':
await this.addContent();
return;
case 'export':
await this.exportResume();
return;
case 'config':
await this.configureSettings();
return;
}
}
// Show navigation hints
console.log(chalk_1.default.gray('\n Navigation: โโ Select | Enter Confirm | Ctrl+C Exit\n'));
// Show main menu
const { action } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: '๐ View Resume', value: 'view' },
{ name: 'โ Add Content', value: 'add' },
{ name: 'โ๏ธ Edit Content', value: 'edit' },
{ name: '๐ฏ Tailor Resume to Job', value: 'tailor' },
{ name: '๐ค Export Resume', value: 'export' },
{ name: '๐ง Settings', value: 'settings' },
{ name: '๐ Sync/Refresh', value: 'sync' },
new inquirer_1.default.Separator('โโโโโโโโโโโโโโ'),
{ name: '๐๏ธ Delete Resume', value: 'delete-resume' },
{ name: 'โ Exit', value: 'exit' }
],
loop: false,
pageSize: 15
}
]);
await this.handleMainAction(action);
}
// Commented out - not showing resume summary on startup anymore
// private async showResumeSummary(): Promise<void> {
// try {
// const resume = await this.resumeManager.get();
// if (resume) {
// console.clear();
// console.log(chalk.cyan.bold('\n๐ Your Resume Summary\n'));
//
// // Basic info
// if (resume.basicInfo) {
// console.log(chalk.white(`Name: ${resume.basicInfo.name || 'Not set'}`));
// console.log(chalk.white(`Email: ${resume.basicInfo.email || 'Not set'}`));
// if (resume.basicInfo.location) {
// console.log(chalk.white(`Location: ${resume.basicInfo.location}`));
// }
// }
//
// // Stats
// const expCount = resume.content?.experience?.length || 0;
// const projCount = resume.content?.projects?.length || 0;
// const skillCount = resume.content?.skills?.length || 0;
//
// console.log(chalk.gray('\n๐ Content:'));
// console.log(chalk.gray(` โข ${expCount} work experience${expCount !== 1 ? 's' : ''}`));
// console.log(chalk.gray(` โข ${projCount} project${projCount !== 1 ? 's' : ''}`));
// console.log(chalk.gray(` โข ${skillCount} skill${skillCount !== 1 ? 's' : ''}`));
// console.log();
// }
// } catch {
// // No resume yet
// }
// }
async handleMainAction(action) {
switch (action) {
case 'view':
await this.viewResume();
break;
case 'add':
await this.addContent();
break;
case 'edit':
await this.editContent();
break;
case 'tailor':
await this.tailorResume();
break;
case 'export':
await this.exportResume();
break;
case 'settings':
await this.configureSettings();
break;
case 'sync':
await this.syncResume();
break;
case 'delete-resume':
await this.deleteEntireResume();
break;
case 'exit':
console.log(chalk_1.default.gray('\nGoodbye! ๐\n'));
return;
}
// Automatically return to main menu after action (no confirmation needed)
if (action !== 'exit') {
await this.showMainMenu();
}
}
async viewResume() {
const resume = await this.resumeManager.get();
// Load experiences from ExperienceManager (which has the polished versions)
await this.experienceManager.load();
const experiences = await this.experienceManager.getAll();
if (!resume && experiences.length === 0) {
console.log(chalk_1.default.yellow('\nโ ๏ธ No resume found. Let\'s create one!\n'));
await this.createResume();
return;
}
console.clear();
console.log(chalk_1.default.cyan.bold('\n๐ Your Complete Resume\n'));
// Check if resume is empty
const profile = await this.configManager.get('profile');
const hasContent = profile || experiences.length > 0 ||
(resume?.content?.projects && resume.content.projects.length > 0) ||
(resume?.content?.skills && resume.content.skills.length > 0) ||
resume?.content?.education;
if (!hasContent) {
console.log(chalk_1.default.yellow(' โ ๏ธ Your resume is empty. Use "Add Content" from the main menu to get started!\n'));
}
// Basic Information
if (profile) {
console.log(chalk_1.default.yellow.bold('Basic Information:'));
console.log(` Name: ${profile.name || 'Not set'}`);
console.log(` Email: ${profile.email || 'Not set'}`);
if (profile.phone)
console.log(` Phone: ${profile.phone}`);
if (profile.location)
console.log(` Location: ${profile.location}`);
if (profile.githubUrl)
console.log(` GitHub: ${profile.githubUrl}`);
if (profile.linkedinUrl)
console.log(` LinkedIn: ${profile.linkedinUrl}`);
console.log();
}
// Summary
if (profile?.personalSummary || resume?.content?.summary) {
console.log(chalk_1.default.yellow.bold('Professional Summary:'));
console.log(` ${profile?.personalSummary || resume?.content?.summary}`);
console.log();
}
// Education - Display right after basic info and summary
if (profile?.education) {
// Handle both old (single object) and new (array) formats
const educationArray = Array.isArray(profile.education) ? profile.education : [profile.education];
if (educationArray.length > 0 && educationArray[0]) {
console.log(chalk_1.default.yellow.bold('Education:'));
for (const edu of educationArray) {
console.log(` ${chalk_1.default.cyan(edu.degree)}๏ผ${chalk_1.default.cyan(edu.field)}๏ผ`);
console.log(` ${edu.institution}`);
if (edu.startDate && edu.endDate) {
console.log(` ${edu.startDate} - ${edu.endDate}`);
}
else if (edu.startDate) {
console.log(` ${edu.startDate} - Present`);
}
if (edu.gpa) {
console.log(` GPA: ${edu.gpa}`);
}
if (edu.highlights && edu.highlights.length > 0) {
console.log(` Achievements:`);
for (const highlight of edu.highlights) {
console.log(` โข ${highlight}`);
}
}
console.log();
}
}
}
else if (resume?.content?.education && resume.content.education.length > 0) {
// Also check resume for education
console.log(chalk_1.default.yellow.bold('Education:'));
for (const edu of resume.content.education) {
console.log(` ${chalk_1.default.cyan(edu.degree)}๏ผ${chalk_1.default.cyan(edu.field)}๏ผ`);
console.log(` ${edu.institution}`);
if (edu.startDate && edu.endDate) {
console.log(` ${edu.startDate} - ${edu.endDate}`);
}
if (edu.gpa) {
console.log(` GPA: ${edu.gpa}`);
}
console.log();
}
}
// Work Experience - Use ExperienceManager data (includes polished versions)
if (experiences.length > 0) {
console.log(chalk_1.default.yellow.bold('Work Experience:'));
for (const exp of experiences) {
console.log(` ${chalk_1.default.cyan(exp.title)} at ${chalk_1.default.cyan(exp.company)}`);
console.log(` ${exp.startDate} - ${exp.current ? 'Present' : exp.endDate || 'Present'}`);
if (exp.description) {
console.log(` ${exp.description}`);
}
if (exp.highlights && exp.highlights.length > 0) {
console.log(chalk_1.default.gray('\n Key Achievements:'));
exp.highlights.slice(0, 5).forEach((h) => {
console.log(` โข ${h}`);
});
}
if (exp.technologies && exp.technologies.length > 0) {
console.log(chalk_1.default.gray(`\n Technologies: ${exp.technologies.join(', ')}`));
}
console.log();
}
}
// Projects - Load using ProjectManager
try {
await this.projectManager.load();
const projects = await this.projectManager.getAll();
if (projects && projects.length > 0) {
console.log(chalk_1.default.yellow.bold('Projects:'));
for (const proj of projects) {
console.log(` ${chalk_1.default.cyan(proj.name)}`);
if (proj.role)
console.log(` ${proj.role}`);
if (proj.description) {
console.log(` ${proj.description}`);
}
if (proj.highlights && proj.highlights.length > 0) {
console.log(chalk_1.default.gray('\n Key Achievements:'));
proj.highlights.forEach((h) => {
console.log(` โข ${h}`);
});
}
if (proj.technologies && proj.technologies.length > 0) {
console.log(chalk_1.default.gray(`\n Technologies: ${proj.technologies.join(', ')}`));
}
if (proj.metrics) {
const { filesCount, linesOfCode } = proj.metrics;
if (filesCount || linesOfCode) {
console.log(chalk_1.default.gray(`\n Scale: ${filesCount} files, ${linesOfCode.toLocaleString()} lines of code`));
}
}
console.log();
}
}
}
catch {
// No projects file yet or legacy resume format
if (resume?.content?.projects && resume.content.projects.length > 0) {
console.log(chalk_1.default.yellow.bold('Projects:'));
for (const proj of resume.content.projects) {
console.log(` ${chalk_1.default.cyan(proj.name)}`);
console.log(` ${proj.description}`);
if (proj.technologies?.length > 0) {
console.log(` Tech: ${proj.technologies.join(', ')}`);
}
console.log();
}
}
}
// Skills
if (resume?.content?.skills?.length && resume.content.skills.length > 0) {
console.log(chalk_1.default.yellow.bold('Skills:'));
const skillsByCategory = {};
resume.content.skills.forEach(skill => {
const cat = skill.category || 'other';
if (!skillsByCategory[cat])
skillsByCategory[cat] = [];
skillsByCategory[cat].push(skill.name);
});
Object.entries(skillsByCategory).forEach(([cat, skills]) => {
console.log(` ${cat}: ${skills.join(', ')}`);
});
console.log();
}
else if (profile?.skills && profile.skills.length > 0) {
// Also check profile for skills
console.log(chalk_1.default.yellow.bold('Skills:'));
console.log(` ${profile.skills.join(', ')}`);
console.log();
}
// Wait for user to press Enter before returning to main menu
console.log(chalk_1.default.gray('\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n'));
await inquirer_1.default.prompt([
{
type: 'input',
name: 'continue',
message: chalk_1.default.gray('Press Enter to return to main menu...'),
default: ''
}
]);
}
async addContent() {
while (true) {
console.log(chalk_1.default.gray('\n Navigation: โโ Select | Enter Confirm | Choose "โ Back" to return\n'));
const { contentType } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'contentType',
message: 'What would you like to add?',
choices: [
{ name: '๐ผ Work Experience', value: 'work' },
{ name: '๐ Project (from code)', value: 'project' },
{ name: '๐ Education', value: 'education' },
{ name: '๐ก Skills', value: 'skills' },
{ name: '๐ Summary/Objective', value: 'summary' },
new inquirer_1.default.Separator('โโโโโโโโโโโโโโ'),
{ name: 'โ Back to Main Menu', value: 'back' }
],
loop: false,
pageSize: 10
}
]);
if (contentType === 'back')
return;
switch (contentType) {
case 'work':
await this.addWorkExperience();
break;
case 'project':
await this.addProject();
break;
case 'education':
await this.addEducation();
break;
case 'skills':
await this.addSkills();
break;
case 'summary':
await this.addSummary();
break;
}
}
}
async addWorkExperience() {
console.log(chalk_1.default.cyan.bold('\n๐ผ Add Work Experience\n'));
// Show example
console.log(chalk_1.default.gray('Tip: Describe your role briefly. AI will enhance it!\n'));
console.log(chalk_1.default.gray('Example: "Backend engineer at Google, worked on search algorithms,'));
console.log(chalk_1.default.gray('improved performance by 30%, used Python and Go"\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: 'Your position/title:',
validate: input => input.length > 0 || 'Position is required'
},
{
type: 'input',
name: 'startDate',
message: 'Start date (YYYY-MM):',
default: '2020-01',
validate: input => /^\d{4}-\d{2}$/.test(input) || '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 => /^\d{4}-\d{2}$/.test(input) || 'Use YYYY-MM format'
},
{
type: 'input',
name: 'description',
message: 'Describe your role and achievements:',
validate: input => input.length > 20 || 'Please provide more details (at least 20 characters)'
}
]);
// Extract technologies mentioned
const techKeywords = ['Java', 'Python', 'JavaScript', 'TypeScript', 'Go', 'React', 'Node', 'Docker', 'Kubernetes', 'AWS', 'MySQL', 'MongoDB'];
const technologies = techKeywords.filter(tech => new RegExp(`\\b${tech}\\b`, 'i').test(experience.description));
// If user mentioned numbers, extract as highlights
const highlights = [];
const sentences = experience.description.split(/[.!?]/).filter((s) => s.trim());
sentences.forEach((sentence) => {
if (/\d+/.test(sentence) || /improved|reduced|increased|led|managed/i.test(sentence)) {
highlights.push(sentence.trim());
}
});
// Save experience
await this.experienceManager.load();
const saved = await this.experienceManager.add({
...experience,
technologies,
highlights: highlights.slice(0, 3),
rawDescription: experience.description,
polished: false
});
console.log(chalk_1.default.green(`\nโ Work experience at ${experience.company} saved!\n`));
// Try AI polish if configured
const aiConfig = await this.configManager.get('ai');
if (aiConfig) {
// Ask if they want to optimize for a specific job
console.log(chalk_1.default.cyan('๐ฏ AI Enhancement Option'));
console.log(chalk_1.default.gray('You can optimize this experience for a specific job position.\n'));
const { enhanceChoice } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'enhanceChoice',
message: 'How would you like to enhance this experience?',
choices: [
{ name: '๐ฏ Optimize for specific job (paste JD)', value: 'with-jd' },
{ name: 'โจ General enhancement', value: 'general' },
{ name: 'โญ๏ธ Skip enhancement', value: 'skip' }
]
}
]);
if (enhanceChoice !== 'skip') {
let jobDescription = '';
if (enhanceChoice === 'with-jd') {
console.log(chalk_1.default.yellow('\n๐ Paste the target job description:'));
console.log(chalk_1.default.gray('Type or paste the job description, then type "END" on a new line.\n'));
const lines = [];
const readline = (await Promise.resolve().then(() => __importStar(require('readline')))).createInterface({
input: process.stdin,
output: process.stdout
});
await new Promise((resolve) => {
readline.on('line', (line) => {
if (line.trim().toUpperCase() === 'END') {
readline.close();
resolve();
}
else {
lines.push(line);
}
});
});
jobDescription = lines.join('\n');
}
const spinner = (0, ora_1.default)('AI is enhancing your experience...').start();
try {
await this.aiManager.initialize();
let polished;
if (jobDescription && jobDescription.trim()) {
// Enhance with job description context
polished = await this.experienceManager.polishWithJob(saved.id, experience.description, jobDescription);
}
else {
// General enhancement
polished = await this.experienceManager.polish(saved.id, experience.description);
}
if (polished) {
spinner.succeed(jobDescription ? 'Experience optimized for target job!' : 'AI enhanced your experience!');
console.log(chalk_1.default.cyan('\n๐ Enhanced Version:\n'));
console.log(polished.description);
if (polished.highlights?.length > 0) {
console.log(chalk_1.default.cyan('\n๐ฏ Key Achievements:'));
polished.highlights.forEach(h => console.log(` โข ${h}`));
}
if (jobDescription) {
console.log(chalk_1.default.green('\nโ Your experience has been optimized for the target position'));
console.log(chalk_1.default.gray('The AI emphasized relevant skills matching the job requirements.'));
}
const { accept } = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'accept',
message: 'Use this enhanced version?',
default: true
}
]);
if (!accept) {
// Revert to original
await this.experienceManager.update(saved.id, {
description: experience.description,
polished: false
});
}
}
}
catch (error) {
spinner.fail('AI enhancement failed, keeping original');
}
}
}
}
async addProject() {
console.log(chalk_1.default.cyan.bold('\n๐ Add Project from Code\n'));
const { source } = await inquirer_1.default.prompt([
{
type: 'list',
name: 'source',
message: 'Where is your project?',
choices: [
{ name: '๐ Local folder on this computer', value: 'local' },
{ name: '๐ GitHub repository', value: 'github' },
{ name: 'โ Back', value: 'back' }
]
}
]);
if (source === 'back')
return;
let projectPath;
if (source === 'local') {
const { path: localPath } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'path',
message: 'Project folder 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';
}
}
}
]);
projectPath = localPath;
}
else {
const { url } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'url',
message: 'GitHub repository URL:',
default: 'https://github.com/',
validate: input => {
return /^https:\/\/github\.com\/[\w-]+\/[\w-]+/.test(input) ||
'Please enter a valid GitHub URL (e.g., https://github.com/user/repo)';
}
}
]);
projectPath = url;
}
// Analyze project
const spinner = (0, ora_1.default)('Analyzing project...').start();
try {
const analysis = await this.projectAnalyzer.analyze(projectPath);
spinner.succeed('Project analyzed successfully!');
// Show analysis results
console.log(chalk_1.default.green('\nโ Project Analysis Complete\n'));
console.log(` ${chalk_1.default.cyan('Name:')} ${analysis.name}`);
console.log(` ${chalk_1.default.cyan('Languages:')} ${Object.keys(analysis.languages || {}).join(', ')}`);
console.log(` ${chalk_1.default.cyan('Files:')} ${analysis.metrics?.filesCount || 0}`);
console.log(` ${chalk_1.default.cyan('Lines of code:')} ${analysis.metrics?.linesOfCode || 0}`);
// Ask for additional details
const { description, role } = await inquirer_1.default.prompt([
{
type: 'input',
name: 'description',
message: 'Brief project description (optional):',
default: analysis.description || ''
},
{
type: 'input',
name: 'role',
message: 'Your role in this project:',
default: 'Developer'
}
]);
// Try to enhance with AI if configured
const aiConfig = await this.configManager.get('ai');
let finalDescription = description || analysis.description || `${analysis.name} project`;
let highlights = analysis.highlights || [];
if (aiConfig) {
const { enhance } = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'enhance',
message: 'Would you like AI to enhance the project description?',
default: true
}
]);
if (enhance) {
const spinner = (0, ora_1.default)('AI is generating enhanced project description...').start();
try {
await this.aiManager.initialize();
// Get user's language preference
const profile = await this.configManager.get('profile');
const userLanguages = profile?.languages || ['English'];
const primaryLanguage = userLanguages[0];
// Determine the language to use for AI response
let languageName = 'English';
if (primaryLanguage) {
const langLower = primaryLanguage.toLowerCase();
if (langLower.includes('chinese') || langLower.includes('ไธญๆ') || langLower.includes('mandarin')) {
languageName = 'Chinese';
}
else if (langLower.includes('spanish') || langLower.includes('espaรฑol')) {
languageName = 'Spanish';
}
else if (langLower.includes('french') || langLower.includes('franรงais')) {
languageName = 'French';
}
else if (langLower.includes('german') || langLower.includes('deutsch')) {
languageName = 'German';
}
else if (langLower.includes('japanese') || langLower.includes('ๆฅๆฌ่ช')) {
languageName = 'Japanese';
}
else if (langLower.includes('korean') || langLower.includes('ํ๊ตญ์ด')) {
languageName = 'Korean';
}
}
const prompt = `Based on this ACTUAL project analysis data, generate a professional project description in ${languageName}:
ACTUAL PROJECT DATA (DO NOT INVENT ANY OTHER METRICS):
- Project Name: ${analysis.name}
- Programming Languages Used: ${Object.keys(analysis.languages || {}).join(', ')}
- Language Distribution: ${Object.entries(analysis.languages || {}).map(([lang, pct]) => `${lang}: ${pct}%`).join(', ')}
- Total Files: ${analysis.metrics?.filesCount || 0}
- Total Lines of Code: ${analysis.metrics?.linesOfCode || 0}
- Frameworks: ${analysis.frameworks?.join(', ') || 'None detected'}
- Libraries: ${analysis.libraries?.join(', ') || 'None detected'}
- Complexity: ${analysis.complexity || 'Not determined'}
- Your Role: ${role}
- User's Description: ${description || 'Not provided'}
STRICT RULES:
1. ONLY use the metrics provided above - DO NOT invent any performance numbers, user counts, or other metrics
2. Base descriptions on the actual technologies and code statistics provided
3. Focus on the technical stack and architecture evident from the code analysis
4. Describe what the project does based on the technologies used and user's description
5. DO NOT make up features that aren't evident from the data
Generate:
1. A 2-3 sentence description based on the actual project data
2. 3-5 highlights based on ACTUAL metrics (e.g., "Implemented with ${analysis.metrics?.filesCount} files and ${analysis.metrics?.linesOfCode} lines of ${Object.keys(analysis.languages || {})[0]} code")
3. Technical focus areas based on the detected languages and frameworks
IMPORTANT: Generate ALL content in ${languageName} language.
Use ONLY the data provided - no speculation or invention.
Format as JSON:
{
"description": "...",
"highlights": ["highlight1", "highlight2", ...],
"technologies": ["tech1", "tech2", ...]
}`;
const response = await this.aiManager.processPrompt(prompt);
// Parse AI response
try {
// Clean up the response - remove markdown code blocks if present
let cleanedResponse = response;
if (response.includes('```json')) {
cleanedResponse = response.replace(/```json\s*/g, '').replace(/```\s*/g, '');
}
else if (response.includes('```')) {
cleanedResponse = response.replace(/```\s*/g, '');
}
cleanedResponse = cleanedResponse.trim();
const enhanced = JSON.parse(cleanedResponse);
spinner.succeed('AI enhanced your project description!');
console.log(chalk_1.default.cyan('\n๐ Enhanced Description:\n'));
console.log(enhanced.description);
if (enhanced.highlights && enhanced.highlights.length > 0) {
console.log(chalk_1.default.cyan('\n๐ฏ Technical Achievements:'));
enhanced.highlights.forEach((h) => console.log(` โข ${h}`));
}
if (enhanced.metrics) {
console.log(chalk_1.default.cyan('\n๐ Metrics:'));
console.log(` ${enhanced.metrics}`);
}
const { accept } = await inquirer_1.default.prompt([
{
type: 'confirm',
name: 'accept',
message: 'Use this enhanced version?',
default: true
}
]);
if (accept) {
finalDescription = enhanced.description;
highlights = enhanced.highlights || [];
}
}
catch (parseError) {
// If parsing fails, try to extract meaningful content
spinner.warn('AI response formatting issue, using simple enhancement');
finalDescription = response.substring(0, 500); // Use first part of response
}
}
catch (error) {
spinner.fail('AI enhancement failed, using original description');
}
}
}
// Save project to storage
// Use ProjectManager to save the project
await this.projectManager.load();
const savedProject = await this.projectManager.addFromAnalysis(analysis, {
description: finalDescription,
role,
highlights,
githubUrl: source === 'github' ? projectPath : undefined
});
console.log(chalk_1.default.green('\nโ Project added and saved!\n'));
// If AI enhancement was successful, mark as polished
if (aiConfig && finalDescription !== (description || analysis.description)) {
await this.projectManager.update(savedProject.id, {
polished: true
});
}
}
catch (error) {
spinner.fail(`Failed to analyze project: ${error.message}`);
}
}
async addEducation() {
// Get user's language preference
const profile = await this.configManager.get('profile');
const userLanguages = profile?.languages || ['English'];
const primaryLanguage = userLanguages[0];
const eduStrings = (0, EducationOptions_1.getEducationStrings)(primaryLanguage);
console.log(chalk_1.default.cyan.bold(`\n${eduStrings.addEducationTitle}\n`));
const education = await inquirer_1.default.prompt([
{
type: 'list',
name: 'degree',
message: eduStrings.degreeLevel,
choices: [
{ name: eduStrings.degrees.bachelors, value: eduStrings.degrees.bachelors },
{ name: eduStrings.degrees.masters, value: eduStrings.degrees.masters },
{ name: eduStrings.degrees.phd, value: eduStrings.degrees.phd },
{ name: eduStrings.degrees.associate, value: eduStrings.degrees.associate },
{ name: eduStrings.degrees.certificate, value: eduStrings.degrees.certificate },
{ name: eduStrings.degrees.bootcamp, value: eduStrings.degrees.bootcamp },
{ name: eduStrings.degrees.other, value: eduStrings.degrees.other }
]
},
{
type: 'input',
name: 'field',
message: eduStrings.fieldOfStudy,
default: eduStrings.fieldDefault
},
{
type: 'input',
name: 'institution',
message: eduStrings.institution,
validate: input => input.length > 0 || eduStrings.institutionRequired
},
{
type: 'input',
name: 'startYear',
message: eduStrings.startYear,
default: (new Date().getFullYear() - 4).toString(),
validate: input => {
const year = parseInt(input);
return (!isNaN(year) && year >= 1950 && year <= 2030) || eduStrings.enterValidYear;
}
},
{
type: 'input',
name: 'endYear',
message: eduStrings.graduationYear,
default: new Date().getFullYear().toString(),
validate: input => {
const year = parseInt(input);
return (!isNaN(year) && year >= 1950 && year <= 2030) || eduStrings.enterValidYear;
}
},
{
type: 'input',
name: 'gpa',
message: eduStrings.gpa,
default: '',
filter: input => input.trim() || '',
transformer: input => input || eduStrings.gpaOptional
},
{
type: 'input',
name: 'achievements',
message: eduStrings.achievements,
default: '',
filter: input => input.trim() || '',
transformer: input => input || eduStrings.achievementsHelp
}
]);
// Parse achievements if provided
const achievements = education.achievements
? education.achievements.split(';').map((s) => s.trim()).filter((s) => s)
: [];
// Convert years to dates for consistency
const educationWithDates = {
degree: education.degree,
field: education.field,
institution: education.institution,
startDate: education.startYear,
endDate: education.endYear,
current: false,
gpa: education.gpa ? parseFloat(education.gpa) : undefined,
highlights: achievements.length > 0 ? achievements : undefined
};
// Initialize or migrate education array
if (!profile.education) {
profile.education = [];
}
else if (!Array.isArray(profile.education)) {
// Migrate from old single object format to array
const oldEducation = profile.education;
profile.education = [{
degree: oldEducation.degree,
field: oldEducation.field,
institution: oldEducation.institution,
startDate: oldEducation.startYear || oldEducation.startDate || (oldEducation.graduationYear ? `${parseInt(oldEducation.graduationYear) - 4}` : ''),
endDate: oldEducation.endYear || oldEducation.endDate || oldEducation.graduationYear || '',
current: false,
gpa: oldEducation.gpa ? (typeof oldEducation.gpa === 'string' ? parseFloat(oldEducation.gpa) : oldEducation.gpa) : undefined,
highlights: oldEducation.achievements || oldEducation.highlights || []
}];
}
// Add new education entry to the array
profile.education.push(educationWithDates);
// Sort education by end date (most recent first)
profile.education.sort((a, b) => {
const aEnd = a.endDate || a.startDate;
const bEnd = b.endDate || b.startDate;
return bEnd.localeCompare(aEnd);