virtue-cli
Version:
Personal character development CLI tool for tracking virtues and philosophical alignment
1,061 lines (1,059 loc) โข 62.2 kB
JavaScript
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