virtue-cli
Version:
Personal character development CLI tool for tracking virtues and philosophical alignment
385 lines • 17.5 kB
JavaScript
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