UNPKG

virtue-cli

Version:

Personal character development CLI tool for tracking virtues and philosophical alignment

1,061 lines (1,059 loc) โ€ข 62.2 kB
import { intro, outro, text, select, confirm, multiselect, note, log, spinner, isCancel } from '@clack/prompts'; import { sleep } from '../utils/helpers.js'; import { getTheme } from '../utils/themes.js'; import { philosophicalTemplates } from './templates/philosophical-templates.js'; import { ClaudeService } from '../services/ClaudeService.js'; import { DiscoverySessionManager } from '../services/DiscoverySessionManager.js'; import { getCoreVirtues } from './CoreVirtues.js'; import { VirtueVersionService } from '../services/VirtueVersionService.js'; import { VirtueVersionUI } from '../ui/VirtueVersionUI.js'; import { AutoExportManager } from '../services/AutoExportManager.js'; export class ProfileManager { db; theme = getTheme(); claudeService; sessionManager; versionService; versionUI; autoExportManager; constructor(db) { this.db = db; this.claudeService = new ClaudeService(); this.sessionManager = new DiscoverySessionManager(); this.versionService = new VirtueVersionService(db); this.versionUI = new VirtueVersionUI(this.versionService); this.autoExportManager = new AutoExportManager(db); } async createProfile() { intro(this.theme.primary('๐Ÿงญ Create Your Virtue Framework')); // Check for existing discovery sessions const activeSessions = this.sessionManager.listActiveSessions(); let resumeSession = null; if (activeSessions.length > 0) { const resumeChoice = await select({ message: 'We found incomplete discovery sessions. What would you like to do?', options: [ { value: 'resume', label: '๐Ÿ”„ Resume previous session', hint: `${activeSessions.length} session(s) available` }, { value: 'new', label: 'โœจ Start fresh session', hint: 'Previous sessions will be kept' }, { value: 'view', label: '๐Ÿ‘€ View saved sessions', hint: 'See details and manage sessions' } ] }); if (resumeChoice === 'resume') { resumeSession = await this.selectSessionToResume(activeSessions); if (resumeSession) { return await this.resumeProfileCreation(resumeSession); } } else if (resumeChoice === 'view') { await this.manageSavedSessions(activeSessions); // After managing sessions, start fresh } } // Get profile name const name = await text({ message: 'What would you like to name this profile?', placeholder: 'Personal Journey', validate: (value) => { if (!value || value.trim().length === 0) { return 'Profile name is required'; } } }); // Choose approach const approach = await select({ message: 'How would you like to create your virtue framework?', options: [ { value: 'guided', label: '๐Ÿค– AI-Guided Discovery', hint: 'Answer questions to derive personalized virtues' }, { value: 'template', label: '๐Ÿ“š Philosophical Template', hint: 'Start with a traditional framework' }, { value: 'custom', label: 'โœ๏ธ Custom Framework', hint: 'Define your own virtues from scratch' } ] }); let virtues = []; let template = 'custom'; switch (approach) { case 'guided': const session = this.sessionManager.createSession(name); const responses = await this.conductPhilosophicalDiscovery(session); virtues = await this.deriveVirtuesFromResponses(responses); template = 'ai-derived'; this.sessionManager.completeSession(session); break; case 'template': const selectedTemplate = await this.selectPhilosophicalTemplate(); virtues = selectedTemplate.virtues; template = selectedTemplate.name; break; case 'custom': virtues = await this.createCustomVirtues(); break; } // Create profile const profile = await this.db.createProfile({ name: name, philosophical_template: template, ascii_theme: 'default', color_scheme: 'blue', output_directory: null }); // Create virtues for (const virtue of virtues) { await this.db.createVirtue({ ...virtue, profile_id: profile.id }); } // Configure auto export await this.configureInitialExport(profile); outro(this.theme.success(`โœ“ Profile "${profile.name}" created successfully!`)); return profile; } async conductPhilosophicalDiscovery(session) { const responses = session?.responses || []; const currentSession = session || this.sessionManager.createSession('Temporary Session'); // Set up graceful interruption handling const originalHandler = process.listeners('SIGINT'); const gracefulExit = () => { note('โธ๏ธ Discovery paused! Your progress has been saved.\n\n' + `You can resume this session later by selecting "${currentSession.profileName}" ` + 'when creating a new profile.', 'Session Paused'); this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, responses.length); process.exit(0); }; process.removeAllListeners('SIGINT'); process.on('SIGINT', gracefulExit); // Show progress information const progress = this.sessionManager.calculateProgress(currentSession); this.displayProgressInfo(progress); note('๐Ÿ’ญ Comprehensive Virtue Discovery\n\n' + 'We\'ll explore your values through 15-25 thoughtful questions, covering your core beliefs,\n' + 'cultural background, and personal philosophy. This deeper exploration creates a\n' + 'more personalized and meaningful virtue framework with detailed behavioral indicators.\n\n' + '๐Ÿ’พ Your progress is automatically saved - you can pause and resume anytime.', 'About this process'); // Skip initial questions if already answered (resuming session) let questionCount = responses.length || 0; // Question 1: Binary choice (if not already answered) if (questionCount < 1) { const q1 = await select({ message: 'Question 1: Would you rather be known as someone who always tells the truth, or someone who\'s always kind?', options: [ { value: 'truth', label: 'Truth' }, { value: 'kindness', label: 'Kindness' } ] }); if (isCancel(q1)) { this.sessionManager.updateSession(currentSession, [], currentSession.currentPhase, questionCount); return responses; } responses.push({ question: 'Truth vs Kindness', answer: q1 }); questionCount++; this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, questionCount); } // Question 2: Scenario (if not already answered) if (questionCount < 2) { const q2 = await select({ message: 'Question 2: In difficult times, you tend to rely most on:', options: [ { value: 'logic', label: 'Logic and reason' }, { value: 'intuition', label: 'Intuition and feeling' }, { value: 'principles', label: 'Established principles' }, { value: 'relationships', label: 'Support from others' } ] }); if (isCancel(q2)) { this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, questionCount); return responses; } responses.push({ question: 'Decision Making', answer: q2 }); questionCount++; this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, questionCount); } // Question 3: Growth focus (if not already answered) if (questionCount < 3) { const q3 = await select({ message: 'Question 3: What aspect of personal growth excites you most?', options: [ { value: 'intellectual', label: 'Intellectual development' }, { value: 'emotional', label: 'Emotional intelligence' }, { value: 'spiritual', label: 'Spiritual connection' }, { value: 'social', label: 'Social impact' } ] }); if (isCancel(q3)) { this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, questionCount); return responses; } responses.push({ question: 'Growth Focus', answer: q3 }); questionCount++; this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, questionCount); } // Question 4: Character aspect ranking (if not already answered) if (questionCount < 4) { note('Question 4: Now we\'ll rank different aspects of character that matter to you.\n' + 'These represent the building blocks of virtue and help us understand your priorities.', 'Character Aspect Ranking'); const characterAspects = [ // Wisdom aspects 'Critical Thinking', 'Learning Orientation', 'Practical Judgment', // Integrity aspects 'Honesty', 'Authenticity', 'Reliability', // Compassion aspects 'Empathy', 'Kindness', 'Service', // Justice aspects 'Fairness', 'Advocacy', 'Responsibility', // Courage aspects 'Moral Courage', 'Resilience', 'Initiative' ]; const rankedAspects = []; const remainingAspects = [...characterAspects]; note('Please rank these 15 character aspects from most important to least important to you personally.', 'Instructions'); for (let i = 0; i < characterAspects.length; i++) { const choice = await select({ message: `Select your ${i === 0 ? 'highest' : i === characterAspects.length - 1 ? 'lowest' : `${i + 1}${this.getOrdinalSuffix(i + 1)}`} priority aspect:`, options: remainingAspects.map(aspect => ({ value: aspect, label: aspect })) }); if (isCancel(choice)) { this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, questionCount); return responses; } rankedAspects.push(choice); remainingAspects.splice(remainingAspects.indexOf(choice), 1); } responses.push({ question: 'Character Aspect Rankings', answer: rankedAspects }); questionCount++; this.sessionManager.updateSession(currentSession, responses, currentSession.currentPhase, questionCount); } // AI-guided philosophical discovery if Claude is available if (this.claudeService.isConfigured()) { try { note('Now we\'ll dive deeper with AI-guided questions that explore your cultural background,\n' + 'personal definitions, and real-world applications. This comprehensive approach\n' + 'ensures your virtue framework reflects your authentic values and experiences.', 'AI-Guided Comprehensive Discovery'); // Start the philosophical discovery const s = spinner(); s.start(this.theme.primary('๐Ÿค– Analyzing your initial responses...')); const discoveryStart = await this.claudeService.startPhilosophicalDiscovery(responses); s.stop(this.theme.success('โœ“ Analysis complete')); let currentPhase = currentSession.currentPhase || 1; let conversationComplete = false; questionCount = Math.max(questionCount, 4); // Ensure we're at least at question 4 after initial questions // Continue the conversation until complete while (!conversationComplete && questionCount < 25) { // Safety limit of 25 total questions for comprehensive discovery let currentStep; if (discoveryStart.isComplete) { currentStep = null; } else if (questionCount === 4) { currentStep = discoveryStart; } else { // Show spinner while generating next question const questionSpinner = spinner(); questionSpinner.start(this.theme.primary('๐Ÿค– Generating next question...')); currentStep = await this.claudeService.continuePhilosophicalDiscovery(responses, currentPhase); questionSpinner.stop(this.theme.success('โœ“ Question ready')); } if (!currentStep || currentStep.isComplete) { conversationComplete = true; break; } // Display phase information if it changed if (currentStep.phase !== currentPhase) { currentPhase = currentStep.phase; const phaseNames = { 1: "Core Values & Intuitions", 2: "Philosophical Foundations", 3: "Practical Integration" }; note(`Moving to Phase ${currentPhase}: ${phaseNames[currentPhase]}`, 'Discovery Progress'); } // Ask the AI-generated question with progress indicator const answer = await text({ message: `Question ${questionCount + 1}: ${currentStep.nextQuestion}`, placeholder: 'Share your thoughts...', validate: (value) => { if (!value || value.trim().length < 5) { return 'Please provide a thoughtful response (at least 5 characters)'; } } }); if (answer && !isCancel(answer)) { responses.push({ question: currentStep.nextQuestion, answer: answer }); questionCount++; // Save progress after each response this.sessionManager.updateSession(currentSession, responses, currentPhase, questionCount); // Show AI reasoning if in development mode if (process.env.NODE_ENV === 'development' && currentStep.reasoning) { console.log(this.theme.muted(`\n๐Ÿ” AI Reasoning: ${currentStep.reasoning}`)); } } else { // User cancelled, save current progress and end discovery this.sessionManager.updateSession(currentSession, responses, currentPhase, questionCount); break; } } if (conversationComplete) { note(`โœจ Comprehensive virtue discovery complete! Through ${questionCount} thoughtful questions,\n` + `we've explored your cultural background, personal definitions, and core values.\n` + `This deep exploration will create a truly personalized virtue framework.`, 'Discovery Complete'); // Mark session as complete this.sessionManager.updateSession(currentSession, responses, currentPhase, questionCount); } } catch (error) { log.warning('AI-guided discovery failed, continuing with standard assessment...'); console.error('Discovery error:', error); } } // Restore original SIGINT handlers process.removeAllListeners('SIGINT'); originalHandler.forEach(handler => { process.on('SIGINT', handler); }); return responses; } async deriveVirtuesFromResponses(responses) { const s = spinner(); if (this.claudeService.isConfigured()) { s.start(this.theme.primary('๐Ÿค– Personalizing your virtue framework...')); await sleep(1500); try { const virtues = await this.claudeService.deriveVirtuesFromResponses(responses); s.stop(this.theme.success('โœ“ Personalization complete')); // Display AI-personalized virtues console.log('\n' + this.theme.success('โœจ Your Personalized Virtue Framework:')); console.log(this.theme.muted('These 5 core virtues have been customized based on your philosophical responses:\n')); for (const virtue of virtues) { console.log(`${virtue.emoji} ${this.theme.primary(virtue.name)} (Priority: ${(virtue.priority_weight * 100).toFixed(0)}%)`); console.log(this.theme.muted(` ${virtue.definition}`)); // Display character aspects if available if (virtue.sub_dimensions && virtue.sub_dimensions.length > 0) { for (const aspect of virtue.sub_dimensions) { console.log(this.theme.muted(` โ€ข ${this.theme.primary(aspect.name)}: ${aspect.definition}`)); // Display behavioral indicators if available if (aspect.indicators && aspect.indicators.length > 0) { for (const indicator of aspect.indicators) { console.log(this.theme.muted(` - ${indicator}`)); } } } } console.log(''); // Add spacing between virtues } const confirm = await select({ message: 'How does this personalized framework feel to you?', options: [ { value: 'perfect', label: 'โœจ Perfect! Let\'s use this framework' }, { value: 'adjust', label: '๐Ÿ”ง Good, but I\'d like to adjust the definitions' }, { value: 'restart', label: '๐Ÿ”„ Let\'s try a different approach' } ] }); if (confirm === 'adjust') { return await this.editVirtues(virtues); } else if (confirm === 'restart') { return await this.createCustomVirtues(); } return virtues; } catch { s.stop(this.theme.error('โœ— AI personalization failed')); log.warning('Using core virtue framework with default priorities...'); return getCoreVirtues(); } } // Fallback to core virtues when AI not available s.start(this.theme.primary('๐Ÿงฎ Using core virtue framework...')); await sleep(1000); s.stop(this.theme.success('โœ“ Framework loaded')); const coreVirtues = getCoreVirtues(); // Display core virtues console.log('\n' + this.theme.success('โœจ Core Virtue Framework:')); console.log(this.theme.muted('These 5 universal virtues form the foundation of character development:\n')); for (const virtue of coreVirtues) { console.log(`${virtue.emoji} ${this.theme.primary(virtue.name)} (Priority: ${(virtue.priority_weight * 100).toFixed(0)}%)`); console.log(this.theme.muted(` ${virtue.definition}`)); // Display character aspects if available if (virtue.sub_dimensions && virtue.sub_dimensions.length > 0) { for (const aspect of virtue.sub_dimensions) { console.log(this.theme.muted(` โ€ข ${this.theme.primary(aspect.name)}: ${aspect.definition}`)); // Display behavioral indicators if available if (aspect.indicators && aspect.indicators.length > 0) { for (const indicator of aspect.indicators) { console.log(this.theme.muted(` - ${indicator}`)); } } } } console.log(''); // Add spacing between virtues } const confirm = await select({ message: 'Would you like to use this core framework?', options: [ { value: 'perfect', label: 'โœจ Yes, let\'s use this framework' }, { value: 'adjust', label: '๐Ÿ”ง I\'d like to adjust the definitions' }, { value: 'restart', label: '๐Ÿ”„ Let\'s try a different approach' } ] }); if (confirm === 'adjust') { return await this.editVirtues(coreVirtues); } else if (confirm === 'restart') { return await this.createCustomVirtues(); } return coreVirtues; } async selectPhilosophicalTemplate() { let selectedTemplate = null; while (!selectedTemplate) { const action = await select({ message: 'Choose a philosophical tradition to explore:', options: [ ...Object.entries(philosophicalTemplates).map(([key, template]) => ({ value: `view_${key}`, label: `๐Ÿ‘๏ธ ${template.name}`, hint: 'View detailed virtues and definitions' })), { value: 'back', label: 'โฌ…๏ธ Back to main options', hint: 'Choose a different approach' } ] }); if (action === 'back' || isCancel(action)) { throw new Error('Template selection cancelled'); } const templateKey = action.replace('view_', ''); const template = philosophicalTemplates[templateKey]; // Display detailed template information console.log('\n' + this.theme.success(`๐Ÿ“š ${template.name}`)); console.log(this.theme.muted(`${template.description}\n`)); console.log(this.theme.primary('Virtues & Sub-Dimensions:')); for (const virtue of template.virtues) { console.log(`\n${virtue.emoji} ${this.theme.primary(virtue.name)}`); console.log(this.theme.muted(` ${virtue.definition}`)); if (virtue.sub_dimensions && virtue.sub_dimensions.length > 0) { for (const subDim of virtue.sub_dimensions) { console.log(this.theme.muted(` โ€ข ${this.theme.primary(subDim.name)}: ${subDim.definition}`)); // Display behavioral indicators if available if (subDim.indicators && subDim.indicators.length > 0) { for (const indicator of subDim.indicators) { console.log(this.theme.muted(` - ${indicator}`)); } } } } } const decision = await select({ message: 'What would you like to do with this template?', options: [ { value: 'select', label: 'โœ… Select this template', hint: 'Use this philosophical framework' }, { value: 'back', label: 'โฌ…๏ธ View other templates', hint: 'Continue browsing' } ] }); if (decision === 'select') { selectedTemplate = templateKey; } // If 'back', the loop will continue to show the template list again } const selected = philosophicalTemplates[selectedTemplate]; note(`You've selected: ${selected.name}\n\n` + `This framework will guide your virtue development with ${selected.virtues.length} core virtues ` + `and their associated character aspects.`, 'Template Confirmed'); return { name: selectedTemplate, virtues: selected.virtues }; } async createCustomVirtues() { const virtues = []; let addMore = true; while (addMore && virtues.length < 7) { const name = await text({ message: `Virtue ${virtues.length + 1} name:`, placeholder: 'e.g., Intellectual Honesty', validate: (value) => { if (!value || value.trim().length === 0) { return 'Virtue name is required'; } } }); const definition = await text({ message: 'Brief definition:', placeholder: 'e.g., The pursuit of truth through reason and evidence', validate: (value) => { if (!value || value.trim().length === 0) { return 'Definition is required'; } } }); const emoji = await text({ message: 'Choose an emoji for this virtue:', placeholder: '๐ŸŽฏ', initialValue: '๐ŸŽฏ' }); virtues.push({ name: name, definition: definition, priority_weight: 1.0 - (virtues.length * 0.1), template_source: 'custom', emoji: emoji || '๐ŸŽฏ' }); if (virtues.length < 7) { addMore = await confirm({ message: 'Add another virtue?', initialValue: virtues.length < 3 // Encourage at least 3 virtues }); } } return virtues; } async editVirtues(virtues) { const action = await select({ message: 'What would you like to do?', options: [ { value: 'edit', label: 'โœ๏ธ Edit existing virtues' }, { value: 'add', label: 'โž• Add more virtues' }, { value: 'remove', label: 'โž– Remove virtues' }, { value: 'done', label: 'โœ… Done editing' } ] }); switch (action) { case 'edit': const virtueToEdit = await select({ message: 'Which virtue to edit?', options: virtues.map((v, i) => ({ value: i, label: `${v.emoji} ${v.name}` })) }); const idx = virtueToEdit; const newName = await text({ message: 'New name:', initialValue: virtues[idx].name }); const newDefinition = await text({ message: 'New definition:', initialValue: virtues[idx].definition }); virtues[idx].name = newName; virtues[idx].definition = newDefinition; break; case 'add': const newVirtues = await this.createCustomVirtues(); virtues.push(...newVirtues); break; case 'remove': if (virtues.length <= 1) { log.warning('You must have at least one virtue'); break; } const toRemove = await multiselect({ message: 'Select virtues to remove:', options: virtues.map((v, i) => ({ value: i, label: `${v.emoji} ${v.name}` })) }); virtues = virtues.filter((_, i) => !toRemove.includes(i)); break; case 'done': return virtues; } // Recurse if not done if (action === 'done') { return virtues; } else { return await this.editVirtues(virtues); } } async manage(currentProfile) { const profiles = this.db.getAllProfiles(); const action = await select({ message: 'Profile Management', options: [ ...(currentProfile ? [{ value: 'current', label: `๐Ÿ“ Current: ${currentProfile.name}`, hint: 'View current profile' }] : []), { value: 'create', label: 'โž• Create new profile' }, { value: 'switch', label: '๐Ÿ”„ Switch profile', hint: `${profiles.length} profiles available` }, { value: 'update', label: 'โœ๏ธ Update profile', hint: 'Edit profile details' }, ...(currentProfile ? [{ value: 'virtues', label: '๐Ÿ“œ Manage Virtue Versions', hint: 'Version history and refinement' }] : []), ...(currentProfile ? [{ value: 'export', label: '๐Ÿ“„ Configure Auto Export', hint: 'Set up automatic markdown exports' }] : []), { value: 'delete', label: '๐Ÿ—‘๏ธ Delete profile', hint: 'Remove a profile' }, { value: 'back', label: 'โฌ…๏ธ Back to main menu' } ] }); switch (action) { case 'current': await this.viewProfile(currentProfile); break; case 'create': await this.createProfile(); break; case 'switch': await this.switchProfile(); break; case 'update': await this.updateProfile(); break; case 'virtues': await this.manageVirtueVersions(currentProfile); break; case 'export': await this.configureAutoExport(currentProfile); break; case 'delete': await this.deleteProfile(); break; } } async viewProfile(profile) { const virtues = this.db.getVirtuesForProfile(profile.id); const streak = this.db.calculateCurrentStreak(profile.id); const avgCoherence = this.db.calculateAverageCoherence(profile.id); note(`Name: ${profile.name}\n` + `Template: ${profile.philosophical_template}\n` + `Created: ${new Date(profile.created_at).toLocaleDateString()}\n\n` + `Stats:\n` + `โ€ข Current Streak: ${streak} days\n` + `โ€ข Average Coherence: ${(avgCoherence * 100).toFixed(1)}%\n\n` + `Virtues (${virtues.length}):\n` + virtues.map(v => `${v.emoji} ${v.name}`).join('\n'), 'Profile Details'); } async switchProfile() { const profiles = this.db.getAllProfiles(); if (profiles.length === 0) { log.warning('No profiles available. Create one first.'); return; } const selected = await select({ message: 'Select profile:', options: profiles.map(p => ({ value: p.id, label: p.name, hint: `${p.philosophical_template} โ€ข Created ${new Date(p.created_at).toLocaleDateString()}` })) }); // In a real app, we'd update the current profile in the VirtueTracker log.success(`Switched to profile: ${profiles.find(p => p.id === selected)?.name}`); } async updateProfile() { const profiles = this.db.getAllProfiles(); if (profiles.length === 0) { log.warning('No profiles available to update.'); return; } const profileId = await select({ message: 'Select profile to update:', options: profiles.map(p => ({ value: p.id, label: p.name, hint: `${p.philosophical_template} โ€ข Created ${new Date(p.created_at).toLocaleDateString()}` })) }); if (isCancel(profileId)) return; const profile = profiles.find(p => p.id === profileId); if (!profile) return; const updateChoice = await select({ message: 'What would you like to update?', options: [ { value: 'name', label: '๐Ÿ“ Profile name' }, { value: 'template', label: '๐Ÿ›๏ธ Philosophical template' }, { value: 'output', label: '๐Ÿ“ Auto-save directory' }, { value: 'back', label: 'โฌ…๏ธ Cancel' } ] }); if (updateChoice === 'back' || isCancel(updateChoice)) return; switch (updateChoice) { case 'name': const newName = await text({ message: 'Enter new profile name:', placeholder: profile.name, validate: (value) => { if (!value || value.trim().length === 0) { return 'Profile name cannot be empty'; } } }); if (!isCancel(newName)) { this.db.updateProfile(profileId, { name: newName }); log.success(`Profile renamed to: ${newName}`); } break; case 'template': const newTemplate = await select({ message: 'Select new philosophical template:', options: [ { value: 'stoic', label: '๐Ÿ›๏ธ Stoic Philosophy' }, { value: 'aristotelian', label: '๐Ÿ“š Aristotelian Ethics' }, { value: 'buddhist', label: 'โ˜ธ๏ธ Buddhist Path' }, { value: 'christian', label: 'โœ๏ธ Christian Virtues' }, { value: 'confucian', label: 'โ˜ฏ๏ธ Confucian Values' }, { value: 'modern', label: '๐ŸŒŸ Modern Integration' }, { value: 'custom', label: '๐ŸŽจ Custom Framework' } ] }); if (!isCancel(newTemplate)) { this.db.updateProfile(profileId, { philosophical_template: newTemplate }); log.success(`Template updated to: ${newTemplate}`); } break; case 'output': const currentDir = profile.output_directory || 'Not set'; const outputChoice = await select({ message: `Current output directory: ${currentDir}`, options: [ { value: 'set', label: '๐Ÿ“ Set directory' }, { value: 'clear', label: '๐Ÿ—‘๏ธ Clear directory (disable auto-save)' }, { value: 'cancel', label: 'โฌ…๏ธ Cancel' } ] }); if (outputChoice === 'cancel' || isCancel(outputChoice)) break; if (outputChoice === 'set') { const newDir = await text({ message: 'Enter directory path for auto-saving entries:', placeholder: './virtue-entries', validate: (value) => { if (!value || value.trim().length === 0) { return 'Directory path cannot be empty'; } } }); if (!isCancel(newDir)) { this.db.updateProfile(profileId, { output_directory: newDir }); log.success(`Output directory set to: ${newDir}`); log.info('Daily and monthly entries will be automatically saved as markdown files.'); } } else if (outputChoice === 'clear') { this.db.updateProfile(profileId, { output_directory: null }); log.success('Output directory cleared. Auto-save disabled.'); } break; } } async deleteProfile() { const profiles = this.db.getAllProfiles(); if (profiles.length === 0) { log.warning('No profiles available to delete.'); return; } if (profiles.length === 1) { log.warning('Cannot delete the only profile. Create another profile first.'); return; } const profileId = await select({ message: 'Select profile to delete:', options: profiles.map(p => ({ value: p.id, label: p.name, hint: `${p.philosophical_template} โ€ข Created ${new Date(p.created_at).toLocaleDateString()}` })) }); if (isCancel(profileId)) return; const profile = profiles.find(p => p.id === profileId); if (!profile) return; const confirmDelete = await confirm({ message: `Are you sure you want to delete profile "${profile.name}"? This will delete all associated data.` }); if (confirmDelete) { const deleted = this.db.deleteProfile(profileId); if (deleted) { log.success(`Profile "${profile.name}" has been deleted.`); } else { log.error('Failed to delete profile.'); } } } getOrdinalSuffix(num) { const ones = num % 10; const tens = num % 100; if (tens >= 11 && tens <= 13) { return 'th'; } switch (ones) { case 1: return 'st'; case 2: return 'nd'; case 3: return 'rd'; default: return 'th'; } } displayProgressInfo(progress) { const progressBar = this.createProgressBar(progress.percentComplete); note(`๐Ÿ“Š Discovery Progress\n\n` + `${progressBar} ${progress.percentComplete}%\n\n` + `Questions: ${progress.completedQuestions}/${progress.totalQuestions}\n` + `Phase: ${progress.currentPhase}/${progress.totalPhases}\n` + `Estimated time remaining: ${progress.estimatedTimeRemaining} minutes\n\n` + `${progress.canPause ? 'โธ๏ธ You can pause anytime by pressing Ctrl+C' : ''}`, 'Progress Overview'); } createProgressBar(percent, width = 20) { const filled = Math.round((percent / 100) * width); const empty = width - filled; return 'โ–ˆ'.repeat(filled) + 'โ–‘'.repeat(empty); } async selectSessionToResume(sessions) { const sessionOptions = sessions.map(session => { const progress = this.sessionManager.calculateProgress(session); const timeAgo = this.formatTimeAgo(session.lastSaved); return { value: session.id, label: `${session.profileName}`, hint: `${progress.percentComplete}% complete โ€ข ${timeAgo} โ€ข ${progress.estimatedTimeRemaining}min remaining` }; }); sessionOptions.push({ value: 'cancel', label: 'โŒ Cancel', hint: 'Start a new session instead' }); const selectedId = await select({ message: 'Select a session to resume:', options: sessionOptions }); if (selectedId === 'cancel') { return null; } return sessions.find(session => session.id === selectedId) || null; } async manageSavedSessions(sessions) { if (sessions.length === 0) { note('No saved sessions found.', 'Saved Sessions'); return; } const action = await select({ message: 'Manage saved sessions:', options: [ { value: 'view', label: '๐Ÿ‘€ View session details' }, { value: 'delete', label: '๐Ÿ—‘๏ธ Delete sessions' }, { value: 'back', label: 'โฌ…๏ธ Back to profile creation' } ] }); switch (action) { case 'view': await this.viewSessionDetails(sessions); break; case 'delete': await this.deleteSessions(sessions); break; } } async viewSessionDetails(sessions) { for (const session of sessions) { const progress = this.sessionManager.calculateProgress(session); const timeAgo = this.formatTimeAgo(session.lastSaved); note(`Profile: ${session.profileName}\n` + `Progress: ${progress.percentComplete}% (${progress.completedQuestions}/${progress.totalQuestions} questions)\n` + `Phase: ${progress.currentPhase}/${progress.totalPhases}\n` + `Last saved: ${timeAgo}\n` + `Time remaining: ~${progress.estimatedTimeRemaining} minutes\n` + `Session ID: ${session.id}`, 'Session Details'); } } async deleteSessions(sessions) { const sessionOptions = sessions.map(session => ({ value: session.id, label: session.profileName, hint: `${this.sessionManager.calculateProgress(session).percentComplete}% complete` })); const toDelete = await multiselect({ message: 'Select sessions to delete:', options: sessionOptions }); if (toDelete.length > 0) { const confirmDelete = await confirm({ message: `Delete ${toDelete.length} session(s)? This cannot be undone.` }); if (confirmDelete) { toDelete.forEach(sessionId => { this.sessionManager.deleteSession(sessionId); }); log.success(`Deleted ${toDelete.length} session(s)`); } } } async resumeProfileCreation(session) { note(`Resuming discovery for "${session.profileName}"\n\n` + `You have completed ${session.responses.length} questions.\n` + `Continuing from where you left off...`, 'Resuming Session'); const responses = await this.conductPhilosophicalDiscovery(session); const virtues = await this.deriveVirtuesFromResponses(responses); // Create profile const profile = await this.db.createProfile({ name: session.profileName, philosophical_template: 'ai-derived', ascii_theme: 'default', color_scheme: 'blue', output_directory: null }); // Create virtues for (const virtue of virtues) { await this.db.createVirtue({ ...virtue, profile_id: profile.id }); } this.sessionManager.completeSession(session); outro(this.theme.success(`โœ“ Profile "${profile.name}" created successfully!`)); return profile; } async manageVirtueVersions(profile) { const virtues = this.db.getVirtuesForProfile(profile.id); if (virtues.length === 0) { outro(this.theme.warning('No virtues found for this profile.')); return; } while (true) { // Create virtue selection with version information const virtueOptions = []; for (const virtue of virtues) { const summary = await this.versionService.getVersionSummary(virtue.id); const versionInfo = summary.hasMultipleVersions ? `v${summary.activeVersion}/${summary.totalVersions}` : `v${summary.activeVersion}`; virtueOptions.push({ value: virtue, label: `${virtue.emoji} ${virtue.name}`, hint: `${versionInfo} โ€ข ${new Date(summary.lastModified).toLocaleDateString()}` }); } virtueOptions.push({ value: null, label: this.theme.secondary('โ† Back to profile management'), hint: '' }); const selectedVirtue = await select({ message: 'Select a virtue to manage versions:', options: virtueOptions }); if (selectedVirtue === null) { break; } const virtue = selectedVirtue; // Show virtue version management menu const action = await select({ message: `Manage ${virtue.name} versions:`, options: [ { value: 'history', label: '๐Ÿ“œ View version history', hint: 'Browse and compare versions' }, { value: 'create', label: 'โœจ Create new version', hint: 'Refine virtue definition' }, { value: 'back', label: 'โ† Back to virtue list' } ] }); switch (action) { case 'history': await this.versionUI.showVersionHistory(virtue); break; case 'create': await this.versionUI.createNewVersion(virtue); break; case 'back': continue; } } } formatTimeAgo(date) { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return date.toLocaleDateString(); } async configureAutoExport(profile) { const config = this.autoExportManager.getExportConfig(profile); intro(this.theme.primary('๐Ÿ“„ Configure Automatic Export')); // Show current configuration if (config.enabled) { note(`Current Configuration: Status: โœ… Enabled Target: ${config.targetPath || 'Not set'} Format: ${config.format} Frequency: ${config.frequency} Last Export: ${profile.last_export_date ? new Date(profile.last_export_date).toLocaleDateString() : 'Never'}`, 'Current Settings'); } else { note(`Auto export is currently disabled. When enabled, Virtue CLI will automatically generate markdown files containing your daily tracking data and export them to a location of your choice.`, 'Auto Export Status'); } while (true) { const action = await select({ message: 'What would you like to do?', options: [ { value: 'toggle', label: config.enabled ? 'โŒ Disable Auto Export' : 'โœ… Enable Auto Export' }, ...(config.enabled ? [ { value: 'path', label: '๐Ÿ“‚ Change Target Directory' }, { value: 'format', label: '๐Ÿ“‹ Change Export Format' }, { value: 'frequency', label: 'โฐ Change Export Frequency' }, { value: 'test', label: '๐Ÿงช Test Export Configuration' }, { value: 'preview', label: '๐Ÿ‘๏ธ Preview Export Format' } ] : []), { value: 'back', label: 'โ† Back to Profile Management' } ] }); switch (action) { case 'toggle': await this.toggleAutoExport(profile); return; // Return to refresh the menu case 'path': await this.configureExportPath(profile); return; case 'format': await this.configureExportFormat(profile); return; case 'frequency': await this.configureExportFrequency(profile); return; case 'test': await this.testExportConfiguration(profile); break; case 'preview': await this.previewExportFormat(profile); break; case 'back': return; } } } async toggleAutoExport(profile) { const config = this.autoExportManager.getExportConfig(profile); if (!config.enabled) { // Enable auto export - need to configure path and format intro(this.theme.primary('๐Ÿš€ E