import { parseLinkedInCSV } from "./parsers/csv-parser.js"; import { getAnalyticsRoot, ensureDirectories, saveBatch, loadAllPosts, } from "./utils/storage.js"; import { detectAlerts } from "./utils/alerts.js"; import { mean, standardDeviation } from "./utils/stats.js"; import { generateWeeklyReport, getCurrentISOWeek } from "./reports/weekly.js"; import { generateHeatmap } from "./reports/heatmap.js"; import { generateMonthlyReport } from "./reports/monthly.js"; import { join } from "node:path"; import { existsSync } from "node:fs"; import type { PostMetrics } from "./models/types.js"; const args = process.argv.slice(2); const command = args[0]; function parseOption(args: string[], flag: string): string | undefined { const idx = args.indexOf(flag); return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined; } function printUsage() { console.log(` LinkedIn Analytics CLI Usage: node build/cli.js import Import a CSV export node build/cli.js report [--week W] Generate weekly report node build/cli.js report --month YYYY-MM Generate monthly report with MoM comparison node build/cli.js trends [--period P] [--metric M] Show trends and alerts node build/cli.js heatmap Day-of-week performance matrix Options: --week W ISO week (e.g., 2026-W05), defaults to current week --period P Time period: "week" | "month" | "quarter" | "all" (default: "month") --metric M Metric to analyze: "impressions" | "reactions" | "comments" | "shares" | "clicks" | "engagementRate" (default: "impressions") Examples: node build/cli.js import linkedin-export-2026-01-20.csv node build/cli.js report --week 2026-W04 node build/cli.js trends --period quarter --metric engagementRate `); } async function handleImport(root: string, args: string[]) { const filename = args[1]; if (!filename) { console.error("Error: Missing filename argument"); console.error("Usage: node build/cli.js import "); process.exit(1); } const fullPath = join(root, "exports", filename); if (!existsSync(fullPath)) { console.error(`Error: File not found: ${fullPath}`); console.error(`\nMake sure the CSV file is placed in: ${join(root, "exports")}`); process.exit(1); } console.log(`Importing ${filename}...`); try { const batch = parseLinkedInCSV(fullPath, filename); const savedFilename = saveBatch(root, batch); console.log("\nImport successful!"); console.log("─────────────────────────────────────"); console.log(`Posts imported: ${batch.postCount}`); console.log(`Date range: ${batch.dateRange.from} to ${batch.dateRange.to}`); console.log(`Batch ID: ${batch.batchId}`); console.log(`Saved to: posts/${savedFilename}`); // Run alert detection on imported posts const alerts = detectAlerts(batch.posts, "impressions"); if (alerts.length > 0) { console.log("\nImmediate alerts detected:"); console.log("─────────────────────────────────────"); for (const alert of alerts.slice(0, 5)) { const icon = alert.severity === "critical" ? "🔴" : alert.severity === "warning" ? "⚠️" : "ℹ️"; console.log(`${icon} [${alert.severity.toUpperCase()}] ${alert.message}`); } if (alerts.length > 5) { console.log(`\n... and ${alerts.length - 5} more alerts`); } } else { console.log("\nNo anomalies detected in imported data."); } } catch (err) { console.error(`Error parsing CSV: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } } async function handleReport(root: string, args: string[]) { const monthOption = parseOption(args, "--month"); if (monthOption) { return handleMonthlyReport(root, monthOption); } const weekOption = parseOption(args, "--week"); const week = weekOption || getCurrentISOWeek(); console.log(`Generating weekly report for ${week}...`); try { const report = generateWeeklyReport(root, week); console.log("\nWeekly Report"); console.log("═════════════════════════════════════"); console.log(`Week: ${report.week}`); console.log(`Generated at: ${new Date(report.generatedAt).toLocaleString()}`); console.log(); console.log("Summary"); console.log("─────────────────────────────────────"); console.log(`Total posts: ${report.summary.totalPosts}`); console.log(`Total impressions: ${report.summary.totalImpressions.toLocaleString()}`); console.log(`Total reactions: ${report.summary.totalReactions.toLocaleString()}`); console.log(`Total comments: ${report.summary.totalComments.toLocaleString()}`); console.log(`Total shares: ${report.summary.totalShares.toLocaleString()}`); console.log(`Total clicks: ${report.summary.totalClicks.toLocaleString()}`); console.log(`Avg engagement: ${report.summary.avgEngagementRate.toFixed(2)}%`); console.log(`Avg impressions: ${Math.round(report.summary.avgImpressionsPerPost).toLocaleString()} per post`); console.log(); if (report.topPerformers.length > 0) { console.log("Top Performers"); console.log("─────────────────────────────────────"); for (const post of report.topPerformers.slice(0, 5)) { const title = post.title.length > 50 ? post.title.substring(0, 47) + "..." : post.title; console.log(`• ${title}`); console.log(` ${post.metrics.impressions.toLocaleString()} impressions | ${post.metrics.engagementRate.toFixed(2)}% engagement | ${post.publishedDate}`); } console.log(); } if (report.underperformers.length > 0) { console.log("Underperformers"); console.log("─────────────────────────────────────"); for (const post of report.underperformers.slice(0, 3)) { const title = post.title.length > 50 ? post.title.substring(0, 47) + "..." : post.title; console.log(`• ${title}`); console.log(` ${post.metrics.impressions.toLocaleString()} impressions | ${post.metrics.engagementRate.toFixed(2)}% engagement | ${post.publishedDate}`); } console.log(); } console.log("Trends"); console.log("─────────────────────────────────────"); console.log(`Impressions trend: ${report.trends.impressionsTrend.toUpperCase()} (${report.trends.percentChange.impressions > 0 ? "+" : ""}${report.trends.percentChange.impressions.toFixed(1)}%)`); console.log(`Engagement trend: ${report.trends.engagementTrend.toUpperCase()} (${report.trends.percentChange.engagement > 0 ? "+" : ""}${report.trends.percentChange.engagement.toFixed(1)}%)`); console.log(`Compared to: ${report.trends.comparedTo}`); console.log(); if (report.alerts.length > 0) { console.log("Alerts"); console.log("─────────────────────────────────────"); for (const alert of report.alerts) { const icon = alert.severity === "critical" ? "🔴" : alert.severity === "warning" ? "⚠️" : "ℹ️"; console.log(`${icon} [${alert.severity.toUpperCase()}] ${alert.message}`); } console.log(); } console.log(`Report saved to: weekly-reports/${week}.json`); } catch (err) { console.error(`Error generating report: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } } /** * Type guard to check if a string is a valid PostMetrics key */ function isPostMetric(value: string): value is keyof PostMetrics { const validMetrics: (keyof PostMetrics)[] = [ "impressions", "reactions", "comments", "shares", "clicks", "engagementRate", ]; return validMetrics.includes(value as keyof PostMetrics); } async function handleTrends(root: string, args: string[]) { const periodOption = parseOption(args, "--period") || "month"; const metricOption = parseOption(args, "--metric") || "impressions"; const validPeriods = ["week", "month", "quarter", "all"]; if (!validPeriods.includes(periodOption)) { console.error(`Error: Invalid period "${periodOption}". Must be one of: ${validPeriods.join(", ")}`); process.exit(1); } if (!isPostMetric(metricOption)) { const validMetrics: (keyof PostMetrics)[] = [ "impressions", "reactions", "comments", "shares", "clicks", "engagementRate", ]; console.error(`Error: Invalid metric "${metricOption}". Must be one of: ${validMetrics.join(", ")}`); process.exit(1); } const period = periodOption as "week" | "month" | "quarter" | "all"; const metric = metricOption; console.log(`Analyzing trends for ${metric} over ${period}...`); try { const allPosts = loadAllPosts(root); if (allPosts.length === 0) { console.error("Error: No posts found. Import some data first."); process.exit(1); } // Calculate date range based on period const now = new Date(); let fromDate = new Date(0); // Beginning of time for "all" if (period === "week") { fromDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); } else if (period === "month") { fromDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); } else if (period === "quarter") { fromDate = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); } const fromDateStr = fromDate.toISOString().split("T")[0]; // Filter posts by period const filteredPosts = allPosts.filter( (post) => post.publishedDate >= fromDateStr ); if (filteredPosts.length === 0) { console.error(`Error: No posts found in the ${period} period.`); process.exit(1); } // Calculate statistics const values = filteredPosts.map((post) => post.metrics[metric]); const avg = mean(values); const stdDev = standardDeviation(values); const min = Math.min(...values); const max = Math.max(...values); console.log("\nTrend Analysis"); console.log("═════════════════════════════════════"); console.log(`Metric: ${metric}`); console.log(`Period: ${period}`); console.log(`Posts analyzed: ${filteredPosts.length}`); console.log(`Date range: ${filteredPosts[filteredPosts.length - 1].publishedDate} to ${filteredPosts[0].publishedDate}`); console.log(); console.log("Statistics"); console.log("─────────────────────────────────────"); console.log(`Mean: ${avg.toFixed(2)}`); console.log(`Std deviation: ${stdDev.toFixed(2)}`); console.log(`Min: ${min.toFixed(2)}`); console.log(`Max: ${max.toFixed(2)}`); console.log(); // Generate alerts const alerts = detectAlerts(filteredPosts, metric); if (alerts.length > 0) { console.log("Alerts"); console.log("─────────────────────────────────────"); for (const alert of alerts) { const icon = alert.severity === "critical" ? "🔴" : alert.severity === "warning" ? "⚠️" : "ℹ️"; console.log(`${icon} [${alert.severity.toUpperCase()}] ${alert.message}`); } } else { console.log("No anomalies detected in this period."); } } catch (err) { console.error(`Error analyzing trends: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } } async function handleMonthlyReport(root: string, month: string) { console.log(`Generating monthly report for ${month}...`); try { const report = generateMonthlyReport(root, month); console.log("\nMonthly Report"); console.log("═════════════════════════════════════"); console.log(`Month: ${report.month}`); console.log(`Generated at: ${new Date(report.generatedAt).toLocaleString()}`); console.log(); console.log("Summary"); console.log("─────────────────────────────────────"); const s = report.summary; const fmtDelta = (val: number | null, suffix = "%") => val !== null ? ` (${val > 0 ? "+" : ""}${val}${suffix})` : ""; console.log(`Posts: ${s.totalPosts}${fmtDelta(report.trends.percentChange.postCount)}`); console.log(`Impressions: ${s.totalImpressions.toLocaleString()}${fmtDelta(report.trends.percentChange.impressions)}`); console.log(`Avg per post: ${s.avgImpressionsPerPost.toLocaleString()}`); console.log(`Avg engagement: ${s.avgEngagementRate.toFixed(2)}%${fmtDelta(report.trends.percentChange.engagement)}`); console.log(`Reactions: ${s.totalReactions.toLocaleString()}`); console.log(`Comments: ${s.totalComments.toLocaleString()}`); console.log(`Shares: ${s.totalShares.toLocaleString()}`); console.log(`Clicks: ${s.totalClicks.toLocaleString()}`); console.log(); if (report.byWeek.length > 0) { console.log("Week Breakdown"); console.log("─────────────────────────────────────"); for (const w of report.byWeek) { console.log(`${w.week}: ${w.postCount} posts | ${w.avgImpressions.toLocaleString()} avg impr | ${w.avgEngagementRate.toFixed(1)}% eng`); } console.log(); } if (report.topPerformers.length > 0) { console.log("Top Performers"); console.log("─────────────────────────────────────"); for (const post of report.topPerformers.slice(0, 5)) { const title = post.title.length > 50 ? post.title.substring(0, 47) + "..." : post.title; console.log(`• ${title}`); console.log(` ${post.metrics.impressions.toLocaleString()} impressions | ${post.metrics.engagementRate.toFixed(2)}% eng | ${post.publishedDate}`); } console.log(); } if (report.trends.comparedTo) { console.log(`Compared to: ${report.trends.comparedTo}`); } else { console.log("No previous month data for comparison."); } console.log(); if (report.alerts.length > 0) { console.log("Alerts"); console.log("─────────────────────────────────────"); for (const alert of report.alerts) { const icon = alert.severity === "critical" ? "🔴" : alert.severity === "warning" ? "⚠️" : "ℹ️"; console.log(`${icon} [${alert.severity.toUpperCase()}] ${alert.message}`); } console.log(); } console.log(`Report saved to: monthly-reports/${month}.json`); } catch (err) { console.error(`Error generating monthly report: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } } async function handleHeatmap(root: string) { console.log("Generating day-of-week heatmap..."); try { const allPosts = loadAllPosts(root); if (allPosts.length === 0) { console.error("Error: No posts found. Import some data first."); process.exit(1); } const report = generateHeatmap(allPosts); console.log("\nDay-of-Week Performance Heatmap"); console.log("═════════════════════════════════════"); console.log(`Posts analyzed: ${report.postsAnalyzed}`); console.log(`Date range: ${report.dateRange.from} to ${report.dateRange.to}`); console.log(); // Print table header const days = report.byDayOfWeek.map(d => d.dayName.slice(0, 3).padStart(7)); console.log(` ${days.join("")}`); console.log(` ${"───────".repeat(7)}`); // Posts row const postCounts = report.byDayOfWeek.map(d => String(d.postCount).padStart(7)); console.log(`Posts: ${postCounts.join("")}`); // Impressions row const impressions = report.byDayOfWeek.map(d => d.postCount > 0 ? d.avgImpressions.toLocaleString().padStart(7) : " -" ); console.log(`Impr: ${impressions.join("")}`); // Engagement rate row const engRates = report.byDayOfWeek.map(d => d.postCount > 0 ? `${d.avgEngagementRate.toFixed(1)}%`.padStart(7) : " -" ); console.log(`Eng: ${engRates.join("")}`); console.log(); console.log(`Best day for impressions: ${report.bestDayImpressions}`); console.log(`Best day for engagement: ${report.bestDayEngagement}`); console.log("\nNote: LinkedIn CSV exports do not include publish time."); console.log("This heatmap shows day-of-week only."); } catch (err) { console.error(`Error generating heatmap: ${err instanceof Error ? err.message : String(err)}`); process.exit(1); } } async function main() { const root = getAnalyticsRoot(); ensureDirectories(root); switch (command) { case "import": await handleImport(root, args); break; case "report": await handleReport(root, args); break; case "trends": await handleTrends(root, args); break; case "heatmap": await handleHeatmap(root); break; default: printUsage(); process.exit(command ? 1 : 0); } } main().catch((err) => { console.error("Fatal error:", err instanceof Error ? err.message : String(err)); process.exit(1); });