/** * packageOutputs.ts * Writes the final output package: * - carousel.json (full CarouselDocument) * - carousel_manifest.json (metadata summary) * - linkedin_caption.txt (3 caption variants with hashtags) */ import fs from 'fs'; import path from 'path'; import type { CarouselDocument, CarouselManifest } from '../schema/carouselSchema'; // --------------------------------------------------------------------------- // Caption generator // --------------------------------------------------------------------------- /** * Generate 3 LinkedIn caption variants for the carousel post. * Each variant has a different tone (professional, story-driven, punchy). */ function generateCaptions(doc: CarouselDocument): string { const { positioningAngle, fitAnalysis, targetRoleProfile, highlights } = doc; const topHighlight = highlights[0]; const metricHint = topHighlight ? topHighlight.text.match(/(\$[\d,]+[KMBkm]?|\d+[%xX])/)?.[0] ?? '' : ''; const role = targetRoleProfile.title; const topAlignment = fitAnalysis.strongestAlignments[0]?.area ?? ''; const headline = positioningAngle.headline; // Role-appropriate hashtags const baseHashtags = ['#OpenToWork', '#CareerCarousel', '#LinkedInCarousel']; const domainTags = targetRoleProfile.domainClues.slice(0, 2).map( (d) => `#${d.replace(/\s+/g, '')}` ); const skillTags = targetRoleProfile.requiredSkills .slice(0, 3) .map((s) => `#${s.replace(/[^a-zA-Z0-9]/g, '')}`); const hashtags = [...baseHashtags, ...domainTags, ...skillTags].join(' '); const variant1 = ` --- Caption Variant 1: Professional / Recruiter-facing --- ${headline} Swipe through to see why I'm a strong fit for ${role} roles — and how my experience maps to what teams actually need. ${topAlignment ? `Strong in: ${topAlignment}.` : ''}${metricHint ? ` Results like ${metricHint}.` : ''} If you're hiring or know someone who is, I'd love to connect. ${hashtags} `.trim(); const variant2 = ` --- Caption Variant 2: Story-driven / Personal --- ${topHighlight ? `"${topHighlight.text.slice(0, 100)}${topHighlight.text.length > 100 ? '…' : ''}"` : 'Real work. Real results.'} That's the kind of work I do. And now I'm looking for the next place to do it — in a ${role} role where I can make a real difference. Swipe to see the full picture → ${hashtags} `.trim(); const variant3 = ` --- Caption Variant 3: Punchy / Short --- ${headline.split('—')[0]?.trim() ?? headline} → ${role} opportunities → ${topAlignment ? `Deep ${topAlignment} background` : 'Strong cross-functional background'} → ${metricHint ? `Proven results (${metricHint})` : 'Proven track record'} Open to the right conversation. DM me or connect. ${hashtags} `.trim(); return [variant1, variant2, variant3].join('\n\n' + '='.repeat(60) + '\n\n'); } // --------------------------------------------------------------------------- // Main packager // --------------------------------------------------------------------------- /** * Write all output package files and return the CarouselManifest. */ export async function packageOutputs( doc: CarouselDocument, outputDir: string, options: { pdfPath?: string; pngPaths?: string[]; mode: string; theme: string; inputResumeFile: string; inputJdFile: string; } ): Promise { fs.mkdirSync(outputDir, { recursive: true }); // 1. Write full document JSON const jsonPath = path.join(outputDir, 'carousel.json'); fs.writeFileSync(jsonPath, JSON.stringify(doc, null, 2), 'utf-8'); console.log(' ✓ Written: carousel.json'); // 2. Write LinkedIn captions const captionPath = path.join(outputDir, 'linkedin_caption.txt'); const captionText = generateCaptions(doc); fs.writeFileSync(captionPath, captionText, 'utf-8'); console.log(' ✓ Written: linkedin_caption.txt'); // 3. Build manifest const manifest: CarouselManifest = { generatedAt: new Date().toISOString(), version: '1.0.0', inputFiles: { resume: options.inputResumeFile, jd: options.inputJdFile, }, outputFiles: { pdf: options.pdfPath ?? path.join(outputDir, 'linkedin_carousel.pdf'), pngs: options.pngPaths ?? [], caption: captionPath, json: jsonPath, manifest: path.join(outputDir, 'carousel_manifest.json'), }, mode: doc.positioningAngle.mode, theme: options.theme, slideCount: doc.slides.length, positioningAngle: { headline: doc.positioningAngle.headline, mode: doc.positioningAngle.mode, compositeScore: doc.positioningAngle.compositeScore, }, topHighlights: doc.highlights.slice(0, 3).map((h) => ({ text: h.text, totalScore: h.totalScore, rationale: h.rationale, })), notableGaps: doc.fitAnalysis.notableGaps, overallFitScore: doc.fitAnalysis.overallFitScore, }; // 4. Write manifest JSON const manifestPath = path.join(outputDir, 'carousel_manifest.json'); fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8'); console.log(' ✓ Written: carousel_manifest.json'); return manifest; }