UNPKG

virtue-cli

Version:

Personal character development CLI tool for tracking virtues and philosophical alignment

385 lines 17.5 kB
import { select, confirm, text, intro, outro, spinner } from '@clack/prompts'; import picocolors from 'picocolors'; export class VirtueVersionUI { versionService; theme; constructor(versionService, theme = { primary: picocolors.cyan, secondary: picocolors.gray, success: picocolors.green, warning: picocolors.yellow, error: picocolors.red, highlight: picocolors.magenta }) { this.versionService = versionService; this.theme = theme; } /** * Show version history browser for a virtue */ async showVersionHistory(virtue) { intro(this.theme.primary(`📜 ${virtue.name} - Version History`)); try { const versions = await this.versionService.getVersionHistory(virtue.id); if (versions.length === 0) { outro(this.theme.warning('No versions found for this virtue.')); return; } let selectedVersion = null; while (true) { // Create version selection options const versionOptions = versions.map(version => ({ value: version, label: this.formatVersionLabel(version), hint: version.is_active ? 'Current' : '' })); versionOptions.push({ value: null, label: this.theme.secondary('← Back to virtue management'), hint: '' }); const selection = await select({ message: 'Select a version to view details:', options: versionOptions }); if (selection === null) { break; } selectedVersion = selection; // Show version details and handle actions const action = await this.showVersionDetails(virtue, selectedVersion); if (action === 'activate') { const confirmActivate = await confirm({ message: `Activate version ${selectedVersion.version_number}?`, initialValue: false }); if (confirmActivate) { const activateSpinner = spinner(); activateSpinner.start('Activating version...'); try { await this.versionService.activateVersion(virtue.id, selectedVersion.version_number); activateSpinner.stop(this.theme.success('✓ Version activated')); // Refresh versions list to show new active status const refreshedVersions = await this.versionService.getVersionHistory(virtue.id); versions.splice(0, versions.length, ...refreshedVersions); } catch (error) { activateSpinner.stop(this.theme.error('✗ Failed to activate version')); console.error(error); } } } else if (action === 'compare') { await this.showVersionComparison(virtue, selectedVersion); } else if (action === 'delete') { await this.handleVersionDeletion(virtue, selectedVersion); // Refresh versions list const refreshedVersions = await this.versionService.getVersionHistory(virtue.id); versions.splice(0, versions.length, ...refreshedVersions); } } } catch (error) { outro(this.theme.error(`Failed to load version history: ${error}`)); } } /** * Show detailed view of a specific version */ async showVersionDetails(virtue, version) { const stats = await this.versionService.getVersionStatistics(virtue.id, version.version_number); console.log('\n' + this.renderVersionDetails(version, stats)); if (version.is_active) { const action = await select({ message: 'This is the active version. What would you like to do?', options: [ { value: 'compare', label: '🔍 Compare with other versions' }, { value: 'refine', label: '✏️ Refine this version' }, { value: null, label: '← Back to version list' } ] }); return action; } else { const action = await select({ message: 'What would you like to do with this version?', options: [ { value: 'activate', label: '✅ Make this the active version' }, { value: 'compare', label: '🔍 Compare with active version' }, { value: 'delete', label: '🗑️ Delete this version' }, { value: null, label: '← Back to version list' } ] }); return action; } } /** * Show comparison between two versions */ async showVersionComparison(virtue, selectedVersion) { const versions = await this.versionService.getVersionHistory(virtue.id); const otherVersions = versions.filter(v => v.version_number !== selectedVersion.version_number); if (otherVersions.length === 0) { outro(this.theme.warning('No other versions to compare with.')); return; } const compareWith = await select({ message: `Compare version ${selectedVersion.version_number} with:`, options: [ ...otherVersions.map(v => ({ value: v, label: this.formatVersionLabel(v), hint: v.is_active ? 'Current' : '' })), { value: null, label: '← Cancel comparison' } ] }); if (!compareWith) return; const compareVersion = compareWith; try { const comparison = await this.versionService.compareVersions(virtue.id, compareVersion.version_number, selectedVersion.version_number); console.log('\n' + this.renderVersionComparison(compareVersion, selectedVersion, comparison)); await text({ message: 'Press Enter to continue...', placeholder: '', validate: () => undefined }); } catch (error) { outro(this.theme.error(`Failed to compare versions: ${error}`)); } } /** * Handle version deletion */ async handleVersionDeletion(virtue, version) { if (version.is_active) { outro(this.theme.error('Cannot delete the active version. Please activate another version first.')); return; } const confirmDelete = await confirm({ message: this.theme.warning(`Are you sure you want to delete version ${version.version_number}? This cannot be undone.`), initialValue: false }); if (confirmDelete) { const deleteSpinner = spinner(); deleteSpinner.start('Deleting version...'); try { await this.versionService.deleteVersion(virtue.id, version.version_number); deleteSpinner.stop(this.theme.success('✓ Version deleted')); } catch (error) { deleteSpinner.stop(this.theme.error('✗ Failed to delete version')); console.error(error); } } } /** * Show version creation wizard */ async createNewVersion(virtue, refinementReason) { intro(this.theme.primary(`✨ Create New Version - ${virtue.name}`)); try { const reason = refinementReason || await text({ message: 'Why are you creating a new version?', placeholder: 'e.g., "Updated definition after reading...", "Refined based on practice..."', validate: (value) => { if (!value || value.trim().length < 10) { return 'Please provide a meaningful reason (at least 10 characters)'; } } }); if (!reason) return null; const updateName = await confirm({ message: 'Do you want to update the virtue name?', initialValue: false }); let newName = virtue.name; if (updateName) { const nameInput = await text({ message: 'Enter the new virtue name:', placeholder: virtue.name, initialValue: virtue.name, validate: (value) => { if (!value || value.trim().length < 2) { return 'Virtue name must be at least 2 characters'; } } }); if (!nameInput) return null; newName = nameInput.toString(); } const updateDefinition = await confirm({ message: 'Do you want to update the virtue definition?', initialValue: true }); let newDefinition = virtue.definition; if (updateDefinition) { const definitionInput = await text({ message: 'Enter the new definition:', placeholder: virtue.definition || 'Enter a comprehensive definition...', initialValue: virtue.definition || '', validate: (value) => { if (!value || value.trim().length < 20) { return 'Definition must be at least 20 characters'; } } }); if (!definitionInput) return null; newDefinition = definitionInput.toString(); } const createSpinner = spinner(); createSpinner.start('Creating new version...'); try { const newVersion = await this.versionService.createNewVersion(virtue.id, newName, newDefinition, virtue.sub_dimensions, null, // behavioral indicators - could be enhanced later 'manual', 'User-created version', reason.toString()); createSpinner.stop(this.theme.success('✓ New version created and activated')); const versionWithDetails = await this.versionService.getVersionDetails(virtue.id, newVersion.version_number); outro(this.theme.success(`Version ${newVersion.version_number} is now active`)); return versionWithDetails; } catch (error) { createSpinner.stop(this.theme.error('✗ Failed to create version')); outro(this.theme.error(`Error: ${error}`)); return null; } } catch (error) { outro(this.theme.error(`Failed to create new version: ${error}`)); return null; } } // Rendering helper methods formatVersionLabel(version) { const status = version.is_active ? this.theme.success('●') : this.theme.secondary('○'); const date = new Date(version.created_at).toLocaleDateString(); const source = version.source_type; return `${status} Version ${version.version_number} ── ${date} ── ${source}`; } renderVersionDetails(version, stats) { const lines = [ this.theme.highlight(`Version ${version.version_number} Details`), '─'.repeat(50), '', `${this.theme.primary('📝 Definition:')}`, version.definition || this.theme.secondary('No definition provided'), '' ]; // Sub-dimensions if (version.parsed_sub_dimensions && version.parsed_sub_dimensions.length > 0) { lines.push(`${this.theme.primary('🎯 Sub-dimensions:')}`); for (const subDim of version.parsed_sub_dimensions) { lines.push(`• ${this.theme.highlight(subDim.name)}: ${subDim.definition}`); if (subDim.indicators && subDim.indicators.length > 0) { for (const indicator of subDim.indicators) { lines.push(` - ${indicator}`); } } } lines.push(''); } // Usage statistics lines.push(`${this.theme.primary('📊 Usage Statistics:')}`); lines.push(`• Used in: ${stats.times_used} daily check-ins`); if (stats.times_used > 0) { lines.push(`• Average Want Score: ${stats.average_want_score.toFixed(1)}`); lines.push(`• Average Pull Score: ${stats.average_pull_score.toFixed(1)}`); lines.push(`• Active Period: ${stats.active_days} days`); if (stats.first_used && stats.last_used) { lines.push(`• Used: ${stats.first_used} to ${stats.last_used}`); } } lines.push(''); // Creation context lines.push(`${this.theme.primary('📝 Creation Context:')}`); lines.push(version.source_context || this.theme.secondary('No context provided')); if (version.created_reason) { lines.push(`Reason: ${version.created_reason}`); } lines.push(`Created: ${new Date(version.created_at).toLocaleString()}`); return lines.join('\n'); } renderVersionComparison(fromVersion, toVersion, changes) { const lines = [ this.theme.highlight(`Compare Version ${fromVersion.version_number} → Version ${toVersion.version_number}`), '─'.repeat(60), '' ]; // Definition changes if (changes.definitionChanged) { lines.push(`${this.theme.primary('Definition Changes:')}`); lines.push(`${this.theme.error('- Previous:')} ${fromVersion.definition || 'None'}`); lines.push(`${this.theme.success('+ Current:')} ${toVersion.definition || 'None'}`); lines.push(''); } else { lines.push(`${this.theme.secondary('Definition:')} No changes`); lines.push(''); } // Name changes if (changes.nameChanged) { lines.push(`${this.theme.primary('Name Changes:')}`); lines.push(`${this.theme.error('- Previous:')} ${fromVersion.name}`); lines.push(`${this.theme.success('+ Current:')} ${toVersion.name}`); lines.push(''); } // Sub-dimension changes if (changes.subDimensionsAdded.length > 0 || changes.subDimensionsRemoved.length > 0 || changes.subDimensionsModified.length > 0) { lines.push(`${this.theme.primary('Sub-dimensions Changes:')}`); if (changes.subDimensionsAdded.length > 0) { lines.push(`${this.theme.success('+ Added:')}`); changes.subDimensionsAdded.forEach(name => { lines.push(` • ${name}`); }); } if (changes.subDimensionsRemoved.length > 0) { lines.push(`${this.theme.error('- Removed:')}`); changes.subDimensionsRemoved.forEach(name => { lines.push(` • ${name}`); }); } if (changes.subDimensionsModified.length > 0) { lines.push(`${this.theme.warning('~ Modified:')}`); changes.subDimensionsModified.forEach(name => { lines.push(` • ${name}`); }); } lines.push(''); } // Behavioral indicators changes if (changes.indicatorsAdded > 0 || changes.indicatorsRemoved > 0 || changes.indicatorsModified > 0) { lines.push(`${this.theme.primary('Behavioral Indicators:')}`); if (changes.indicatorsAdded > 0) { lines.push(`${this.theme.success('+')} ${changes.indicatorsAdded} indicators added`); } if (changes.indicatorsRemoved > 0) { lines.push(`${this.theme.error('-')} ${changes.indicatorsRemoved} indicators removed`); } if (changes.indicatorsModified > 0) { lines.push(`${this.theme.warning('~')} ${changes.indicatorsModified} indicators modified`); } lines.push(''); } // Impact analysis placeholder lines.push(`${this.theme.primary('Impact Analysis:')}`); if (changes.definitionChanged || changes.nameChanged || changes.subDimensionsAdded.length > 0 || changes.subDimensionsModified.length > 0) { lines.push('• Significant changes detected - may affect scoring patterns'); } else if (changes.indicatorsAdded > 0 || changes.indicatorsModified > 0) { lines.push('• Minor refinements - should improve assessment accuracy'); } else { lines.push('• No functional changes detected'); } return lines.join('\n'); } } //# sourceMappingURL=VirtueVersionUI.js.map