UNPKG

virtue-cli

Version:

Personal character development CLI tool for tracking virtues and philosophical alignment

303 lines 13.3 kB
import { intro, outro, select, text, log, confirm } from '@clack/prompts'; import { calculateAlignment, calculateCoherence, formatDate, formatDateDisplay } from '../utils/helpers.js'; import picocolors from 'picocolors'; import { writeFileSync } from 'fs'; import { join } from 'path'; export class ExportManager { db; constructor(db) { this.db = db; } async exportMenu(profile) { intro(picocolors.cyan('📤 Export Data')); try { const options = await this.gatherExportOptions(); const exportData = await this.gatherExportData(profile, options); const filename = await this.generateExport(profile, exportData, options); outro(picocolors.green(`✅ Data exported to: ${filename}`)); } catch (error) { if (error === 'cancelled') { outro(picocolors.gray('Export cancelled')); } else { log.error(`Export failed: ${error}`); outro(picocolors.red('Export failed')); } } } async gatherExportOptions() { const format = await select({ message: 'Select export format:', options: [ { value: 'markdown', label: '📝 Markdown', hint: 'Human-readable, great for notes apps' }, { value: 'json', label: '📊 JSON', hint: 'Machine-readable, for data analysis' }, { value: 'csv', label: '📋 CSV', hint: 'Spreadsheet format' } ] }); const period = await select({ message: 'Select time period:', options: [ { value: 'all', label: '📅 All time', hint: 'Complete history' }, { value: '1year', label: '📆 Last year', hint: 'Past 365 days' }, { value: '90days', label: '🗓️ Last 90 days', hint: 'Recent quarter' }, { value: '30days', label: '📋 Last 30 days', hint: 'Recent month' } ] }); const includeAnalytics = await confirm({ message: 'Include analytics and insights?', initialValue: true }); if (typeof includeAnalytics === 'symbol') throw 'cancelled'; const includeNotes = await confirm({ message: 'Include personal notes and reflections?', initialValue: true }); if (typeof includeNotes === 'symbol') throw 'cancelled'; const customFilename = await text({ message: 'Custom filename (optional):', placeholder: 'Leave empty for auto-generated name' }); if (typeof customFilename === 'symbol') throw 'cancelled'; return { format, period, includeAnalytics, includeNotes, filename: customFilename || undefined }; } async gatherExportData(profile, options) { let cutoffDate; if (options.period !== 'all') { cutoffDate = new Date(); const days = options.period === '30days' ? 30 : options.period === '90days' ? 90 : 365; cutoffDate.setDate(cutoffDate.getDate() - days); } const rawEntries = this.db.getEntriesForProfile(profile.id, cutoffDate); const entries = this.parseEntries(rawEntries); const virtues = this.db.getVirtuesForProfile(profile.id); let analytics = null; if (options.includeAnalytics && entries.length > 0) { analytics = this.calculateAnalytics(entries, virtues); } return { profile, entries: options.includeNotes ? entries : entries.map(e => ({ ...e, notes: null })), virtues, analytics, exportDate: new Date(), period: options.period }; } parseEntries(rawEntries) { return rawEntries.map(entry => ({ ...entry, virtue_scores: typeof entry.virtue_scores === 'string' ? JSON.parse(entry.virtue_scores) : entry.virtue_scores })); } calculateAnalytics(entries, virtues) { const totalEntries = entries.length; // Calculate average coherence const coherences = entries.map(entry => { const scores = {}; entry.virtue_scores.forEach((vs) => { scores[vs.virtue_id] = { want: vs.want_score, pull: vs.pull_score }; }); return calculateCoherence(scores); }); const averageCoherence = coherences.reduce((sum, val) => sum + val, 0) / coherences.length; // Calculate virtue performance const virtuePerformance = virtues.map(virtue => { const virtueEntries = entries.filter(entry => entry.virtue_scores.some((vs) => vs.virtue_id === virtue.id)); const alignments = virtueEntries.map(entry => { const score = entry.virtue_scores.find((vs) => vs.virtue_id === virtue.id); return score ? calculateAlignment(score.want_score, score.pull_score) : 0; }).filter(alignment => alignment > 0); const avgAlignment = alignments.length > 0 ? alignments.reduce((sum, val) => sum + val, 0) / alignments.length : 0; return { virtue: virtue.name, averageAlignment: avgAlignment, totalEntries: virtueEntries.length }; }); // Calculate streaks const { currentStreak, longestStreak } = this.calculateStreaks(entries); return { totalEntries, averageCoherence, virtuePerformance: virtuePerformance.sort((a, b) => b.averageAlignment - a.averageAlignment), currentStreak, longestStreak, dateRange: { start: entries[entries.length - 1]?.date, end: entries[0]?.date } }; } calculateStreaks(entries) { if (entries.length === 0) return { currentStreak: 0, longestStreak: 0 }; const sortedEntries = entries.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); let currentStreak = 0; let longestStreak = 0; let tempStreak = 0; const today = new Date(); let checkDate = new Date(today); // Calculate current streak for (const entry of sortedEntries) { const entryDate = new Date(entry.date); const daysDiff = Math.floor((checkDate.getTime() - entryDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysDiff === 0 || daysDiff === 1) { currentStreak++; checkDate = entryDate; } else { break; } } // Calculate longest streak for (let i = 0; i < sortedEntries.length; i++) { tempStreak = 1; for (let j = i + 1; j < sortedEntries.length; j++) { const currentDate = new Date(sortedEntries[j - 1].date); const nextDate = new Date(sortedEntries[j].date); const daysDiff = Math.floor((currentDate.getTime() - nextDate.getTime()) / (1000 * 60 * 60 * 24)); if (daysDiff <= 1) { tempStreak++; } else { break; } } longestStreak = Math.max(longestStreak, tempStreak); } return { currentStreak, longestStreak }; } async generateExport(profile, data, options) { const timestamp = new Date().toISOString().split('T')[0]; const baseFilename = options.filename || `virtue-cli-${profile.name.toLowerCase().replace(/\s+/g, '-')}-${options.period}-${timestamp}`; let content; let extension; switch (options.format) { case 'markdown': content = this.generateMarkdown({ ...data, profile, exportDate: new Date(), period: options.period }, options); extension = '.md'; break; case 'json': content = JSON.stringify(data, null, 2); extension = '.json'; break; case 'csv': content = this.generateCSV(data); extension = '.csv'; break; } const filename = baseFilename + extension; const filepath = join(process.cwd(), filename); writeFileSync(filepath, content, 'utf-8'); return filename; } generateMarkdown(data, options) { const { profile, entries, virtues, analytics, exportDate, period } = data; let md = `# Virtue Tracker Export\n\n`; md += `**Profile:** ${profile.name}\n`; md += `**Period:** ${period === 'all' ? 'All time' : period}\n`; md += `**Exported:** ${formatDateDisplay(exportDate)}\n`; md += `**Total Entries:** ${entries.length}\n\n`; // Analytics section if (analytics) { md += `## 📊 Analytics Summary\n\n`; md += `- **Average Coherence:** ${(analytics.averageCoherence * 100).toFixed(1)}%\n`; md += `- **Current Streak:** ${analytics.currentStreak} days\n`; md += `- **Longest Streak:** ${analytics.longestStreak} days\n`; if (analytics.dateRange.start && analytics.dateRange.end) { md += `- **Date Range:** ${formatDate(new Date(analytics.dateRange.start))} to ${formatDate(new Date(analytics.dateRange.end))}\n`; } md += `\n### Virtue Performance\n\n`; analytics.virtuePerformance.forEach((vp, index) => { const rank = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}.`; md += `${rank} **${vp.virtue}**: ${(vp.averageAlignment * 100).toFixed(1)}% (${vp.totalEntries} entries)\n`; }); md += `\n`; } // Virtues section md += `## 🎯 Personal Virtues\n\n`; virtues.forEach((virtue) => { md += `### ${virtue.emoji} ${virtue.name}\n`; md += `${virtue.definition}\n\n`; }); // Entries section if (entries.length > 0) { md += `## 📝 Daily Entries\n\n`; entries.reverse().forEach((entry) => { md += `### ${formatDateDisplay(new Date(entry.date))}\n\n`; // Virtue scores entry.virtue_scores.forEach((vs) => { const virtue = virtues.find((v) => v.id === vs.virtue_id); if (virtue) { const alignment = calculateAlignment(vs.want_score, vs.pull_score); md += `- **${virtue.name}**: Want ${vs.want_score}/10, Pull ${vs.pull_score}/10 (${(alignment * 100).toFixed(1)}% aligned)\n`; } }); // Overall coherence const scores = {}; entry.virtue_scores.forEach((vs) => { scores[vs.virtue_id] = { want: vs.want_score, pull: vs.pull_score }; }); const coherence = calculateCoherence(scores); md += `\n**Overall Coherence:** ${(coherence * 100).toFixed(1)}%\n`; if (entry.notes && options.includeNotes) { md += `\n**Notes:** ${entry.notes}\n`; } md += `\n---\n\n`; }); } md += `## 📋 Export Information\n\n`; md += `This export was generated by Virtue Tracker CLI on ${formatDateDisplay(exportDate)}.\n`; md += `Profile: ${profile.name} | Period: ${period} | Format: Markdown\n\n`; md += `*Keep tracking your virtues and building character!* 🌟\n`; return md; } generateCSV(data) { const { entries, virtues } = data; // Header let csv = 'Date,Overall_Coherence,Completion_Time_Seconds,Notes'; virtues.forEach((virtue) => { csv += `,${virtue.name}_Want,${virtue.name}_Pull,${virtue.name}_Alignment`; }); csv += '\n'; // Data rows entries.forEach((entry) => { const scores = {}; entry.virtue_scores.forEach((vs) => { scores[vs.virtue_id] = { want: vs.want_score, pull: vs.pull_score }; }); const coherence = calculateCoherence(scores); csv += `${entry.date},${coherence.toFixed(4)},${entry.completion_time_seconds || ''},`; csv += `"${(entry.notes || '').replace(/"/g, '""')}"`; virtues.forEach((virtue) => { const score = entry.virtue_scores.find((vs) => vs.virtue_id === virtue.id); if (score) { const alignment = calculateAlignment(score.want_score, score.pull_score); csv += `,${score.want_score},${score.pull_score},${alignment.toFixed(4)}`; } else { csv += ',,,'; } }); csv += '\n'; }); return csv; } } //# sourceMappingURL=ExportManager.js.map