/** * index.ts — job-carousel CLI entry point * * Usage: * npx ts-node src/job-carousel/index.ts \ * --resume ./path/to/resume.txt \ * --jd ./path/to/jd.txt \ * [--mode recruiter|hiring-manager|personal-brand] \ * [--theme clean|dark|bold] \ * [--slides 6|7|8|9|10] \ * [--output ./output/job-carousel] \ * [--no-render] \ * [--timeline] \ * [--show-education auto|always|never] * * Pipeline: * 1. Read resume + JD files * 2. Parse both into structured profiles * 3. Analyze fit * 4. Rank highlights * 5. Choose positioning narrative * 6. Build carousel outline * 7. Write slide copy * 8. Render + export (PDF + PNGs) * 9. Package outputs (JSON + caption + manifest) * 10. Print summary */ import fs from 'fs'; import path from 'path'; import { parseResume } from './parse/resumeParser'; import { parseJobDescription } from './parse/jobParser'; import { analyzeFit } from './analysis/fitAnalysis'; import { rankCandidateHighlights } from './analysis/rankHighlights'; import { choosePositioningAngle } from './analysis/chooseNarrative'; import { buildCarouselOutline } from './generation/buildOutline'; import { writeSlideCopy, type ShowEducation } from './generation/writeSlides'; import { getTheme } from './render/themes'; import { exportToPngs } from './export/exportPng'; import { exportToPdf } from './export/exportPdf'; import { exportToPptx } from './export/exportPptx'; import { exportToSvgs } from './export/exportSvg'; import { packageOutputs } from './export/packageOutputs'; import { validateCarouselDocument, formatValidationReport } from './utils/validation'; import type { CarouselDocument, PositioningMode, } from './schema/carouselSchema'; // --------------------------------------------------------------------------- // CLI argument parser // --------------------------------------------------------------------------- type ExportTarget = 'canva' | 'figma'; interface CliArgs { resume: string; jd: string; mode: PositioningMode | 'auto'; theme: string; slides: number; output: string; noRender: boolean; exportTargets: ExportTarget[]; showTimeline: boolean; showEducation: ShowEducation; } function parseArgs(argv: string[]): CliArgs { const args: CliArgs = { resume: '', jd: '', mode: 'auto', theme: 'clean', slides: 7, output: path.join(process.cwd(), 'output', 'job-carousel'), noRender: false, exportTargets: [], showTimeline: false, showEducation: 'auto', }; for (let i = 2; i < argv.length; i++) { const arg = argv[i]; const next = argv[i + 1]; switch (arg) { case '--resume': args.resume = next; i++; break; case '--jd': args.jd = next; i++; break; case '--mode': args.mode = next as PositioningMode | 'auto'; i++; break; case '--theme': args.theme = next; i++; break; case '--slides': args.slides = parseInt(next, 10); i++; break; case '--output': args.output = next; i++; break; case '--no-render': args.noRender = true; break; case '--timeline': args.showTimeline = true; // When timeline is added, default slide count bumps to 8 if still at 7 if (args.slides === 7) args.slides = 8; break; case '--show-education': args.showEducation = next as ShowEducation; i++; break; case '--export': // Accepts comma-separated targets: --export canva,figma args.exportTargets = next .split(',') .map((t) => t.trim().toLowerCase() as ExportTarget) .filter((t) => ['canva', 'figma'].includes(t)); i++; break; } } return args; } function validateArgs(args: CliArgs): void { if (!args.resume) { console.error('Error: --resume is required. Provide a path to your resume file.'); process.exit(1); } if (!args.jd) { console.error('Error: --jd is required. Provide a path to the job description file.'); process.exit(1); } if (!fs.existsSync(args.resume)) { console.error(`Error: Resume file not found: ${args.resume}`); process.exit(1); } if (!fs.existsSync(args.jd)) { console.error(`Error: JD file not found: ${args.jd}`); process.exit(1); } if (![6, 7, 8, 9, 10].includes(args.slides)) { console.error('Error: --slides must be 6, 7, 8, 9, or 10.'); process.exit(1); } } // --------------------------------------------------------------------------- // Progress logger // --------------------------------------------------------------------------- function step(label: string): void { console.log(`\n[${new Date().toISOString().split('T')[1].split('.')[0]}] ${label}`); } // --------------------------------------------------------------------------- // Summary printer // --------------------------------------------------------------------------- function printSummary( doc: CarouselDocument, outputDir: string, pngPaths: string[], pdfPath: string, skippedRender: boolean, pptxPath?: string, svgPaths?: string[] ): void { const { positioningAngle, fitAnalysis, highlights } = doc; console.log('\n' + '═'.repeat(60)); console.log(' JOB-CAROUSEL GENERATION COMPLETE'); console.log('═'.repeat(60)); console.log('\nšŸ“Œ POSITIONING ANGLE'); console.log(` Headline: "${positioningAngle.headline}"`); console.log(` Mode: ${positioningAngle.mode}`); console.log(` Score: ${positioningAngle.compositeScore}/10`); console.log(` Rationale: ${positioningAngle.rationale}`); console.log('\nšŸ† TOP HIGHLIGHTS SELECTED'); highlights.slice(0, 3).forEach((h, i) => { const label = h.isSoftened ? ' [softened]' : ''; console.log(` ${i + 1}. (${h.totalScore}/10)${label} "${h.text.slice(0, 80)}${h.text.length > 80 ? '…' : ''}"`); console.log(` Source: ${h.source}`); console.log(` ${h.rationale}`); }); console.log('\nšŸ“Š FIT ANALYSIS'); console.log(` Overall Fit Score: ${fitAnalysis.overallFitScore}/10`); console.log(` Best Fit Story: ${fitAnalysis.bestFitStory}`); if (fitAnalysis.notableGaps.length > 0) { console.log('\nāš ļø NOTABLE GAPS (not on resume):'); fitAnalysis.notableGaps.forEach((g) => console.log(` • ${g}`)); } const softenedHighlights = highlights.filter((h) => h.isSoftened); if (softenedHighlights.length > 0) { console.log('\nāœļø SOFTENED CLAIMS (weak evidence → hedged phrasing):'); softenedHighlights.forEach((h) => console.log(` Original: "${h.text.slice(0, 60)}…"\n Softened: "${h.softenedText}"`) ); } console.log('\nšŸ“ OUTPUT FILES'); if (!skippedRender) { console.log(` PDF: ${pdfPath}`); pngPaths.forEach((p, i) => console.log(` Slide ${String(i + 1).padStart(2, '0')}: ${p}`) ); } else { console.log(' Render skipped (--no-render flag). HTML files only.'); } console.log(` JSON: ${path.join(outputDir, 'carousel.json')}`); console.log(` Caption: ${path.join(outputDir, 'linkedin_caption.txt')}`); console.log(` Manifest: ${path.join(outputDir, 'carousel_manifest.json')}`); if (pptxPath) { console.log(` PPTX: ${pptxPath} ← import into Canva`); } if (svgPaths && svgPaths.length > 0) { console.log(` SVGs: ${path.join(outputDir, 'figma_export', '*.svg')} ← drag into Figma`); console.log(` Guide: ${path.join(outputDir, 'figma_export', 'FIGMA_IMPORT_GUIDE.md')}`); } console.log('\n' + '═'.repeat(60) + '\n'); } // --------------------------------------------------------------------------- // Main pipeline // --------------------------------------------------------------------------- async function main(): Promise { const args = parseArgs(process.argv); validateArgs(args); const resumeAbs = path.resolve(args.resume); const jdAbs = path.resolve(args.jd); const outputDir = path.resolve(args.output); fs.mkdirSync(outputDir, { recursive: true }); console.log('\nšŸš€ job-carousel pipeline starting...'); console.log(` Resume: ${resumeAbs}`); console.log(` JD: ${jdAbs}`); console.log(` Mode: ${args.mode}`); console.log(` Theme: ${args.theme}`); console.log(` Slides: ${args.slides}`); if (args.showTimeline) console.log(` Timeline: enabled (education: ${args.showEducation})`); // Step 1: Read files step('Step 1/9 — Reading input files'); const resumeText = fs.readFileSync(resumeAbs, 'utf-8'); const jdText = fs.readFileSync(jdAbs, 'utf-8'); // Step 2: Parse step('Step 2/9 — Parsing resume and job description'); const candidateProfile = parseResume(resumeText); const targetRoleProfile = parseJobDescription(jdText); console.log(` Parsed ${candidateProfile.roles.length} roles, ${candidateProfile.allBullets.length} bullets`); console.log(` Target role: ${targetRoleProfile.title} (${targetRoleProfile.seniority})`); // Step 3: Fit analysis step('Step 3/9 — Analyzing fit'); const fitAnalysis = analyzeFit(candidateProfile, targetRoleProfile); console.log(` Fit score: ${fitAnalysis.overallFitScore}/10`); console.log(` Strong alignments: ${fitAnalysis.strongestAlignments.length}`); console.log(` Notable gaps: ${fitAnalysis.notableGaps.length}`); // Step 4: Rank highlights step('Step 4/9 — Ranking highlights'); const highlights = rankCandidateHighlights(candidateProfile, targetRoleProfile, fitAnalysis); console.log(` ${highlights.length} highlights scored above threshold`); if (highlights.length === 0) { console.warn(' WARNING: No highlights scored above threshold. Carousel will use narrative framing only.'); } // Step 5: Choose narrative step('Step 5/9 — Choosing positioning narrative'); const mode: PositioningMode = args.mode === 'auto' ? fitAnalysis.inferredMode : (args.mode as PositioningMode); const positioningAngle = choosePositioningAngle( candidateProfile, targetRoleProfile, fitAnalysis, mode ); console.log(` Chosen angle: "${positioningAngle.headline}"`); console.log(` Composite score: ${positioningAngle.compositeScore}/10`); // Step 6: Build outline step('Step 6/9 — Building carousel outline'); const outline = buildCarouselOutline(fitAnalysis, highlights, positioningAngle, args.slides, args.showTimeline); console.log(` Outline: ${outline.map((s) => s.templateType).join(' → ')}`); // Step 7: Write slide copy step('Step 7/9 — Writing slide copy'); const slides = writeSlideCopy( outline, candidateProfile, targetRoleProfile, fitAnalysis, positioningAngle, highlights, args.showEducation ); console.log(` ${slides.length} slides written`); // Assemble full document const doc: CarouselDocument = { candidateProfile, targetRoleProfile, fitAnalysis, highlights, positioningAngle, slides, }; // Validate document const validationResults = validateCarouselDocument(doc); const hasErrors = validationResults.some((r) => !r.valid && r.severity === 'error'); if (validationResults.some((r) => !r.valid)) { console.log(formatValidationReport(validationResults)); } if (hasErrors) { console.error('ERROR: Validation found critical errors. See above. Aborting export.'); process.exit(1); } // Step 8: Render and export const theme = getTheme(args.theme); let pngPaths: string[] = []; let pdfPath = path.join(outputDir, 'linkedin_carousel.pdf'); if (!args.noRender) { step('Step 8/9 — Exporting PNGs and PDF'); try { pngPaths = await exportToPngs(slides, theme, outputDir); pdfPath = await exportToPdf(slides, theme, outputDir, pngPaths); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(`\n Export failed: ${msg}`); console.error(' Continuing with JSON/caption output only.\n'); } } else { step('Step 8/9 — Skipping render (--no-render)'); console.log(' PNGs and PDF not generated. carousel.json will contain full slide data.'); } // Step 8b: Optional Canva/Figma exports let pptxPath: string | undefined; let svgPaths: string[] = []; if (args.exportTargets.includes('canva')) { step('Step 8b — Exporting PPTX for Canva'); try { pptxPath = await exportToPptx(slides, theme, outputDir); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(` Canva export failed: ${msg}`); } } if (args.exportTargets.includes('figma')) { step('Step 8b — Exporting SVGs for Figma'); try { svgPaths = exportToSvgs(slides, theme, outputDir); } catch (err) { const msg = err instanceof Error ? err.message : String(err); console.error(` Figma export failed: ${msg}`); } } // Step 9: Package outputs step('Step 9/9 — Packaging outputs'); const manifest = await packageOutputs(doc, outputDir, { pdfPath, pngPaths, mode, theme: args.theme, inputResumeFile: resumeAbs, inputJdFile: jdAbs, }); // Print summary printSummary(doc, outputDir, pngPaths, pdfPath, args.noRender, pptxPath, svgPaths); } // --------------------------------------------------------------------------- // Entry point // --------------------------------------------------------------------------- main().catch((err) => { console.error('\nFatal error:', err instanceof Error ? err.message : err); if (err instanceof Error && err.stack) { console.error(err.stack); } process.exit(1); });