virtue-cli
Version:
Personal character development CLI tool for tracking virtues and philosophical alignment
227 lines ⢠10.1 kB
JavaScript
import { intro, outro, text, log, spinner, isCancel } from '@clack/prompts';
import { ASCIIArt } from '../utils/ascii-art.js';
import { AutoSaveManager } from '../services/AutoSaveManager.js';
import { AutoExportManager } from '../services/AutoExportManager.js';
import { sleep, formatDate, formatDateDisplay, calculateAlignment, calculateCoherence, getCoherenceLevel, validateScore } from '../utils/helpers.js';
import { getTheme } from '../utils/themes.js';
export class DailyCheckin {
db;
profile;
theme = getTheme();
startTime = Date.now();
autoSaveManager;
autoExportManager;
constructor(db, profile) {
this.db = db;
this.profile = profile;
this.autoSaveManager = new AutoSaveManager(db);
this.autoExportManager = new AutoExportManager(db);
}
async run() {
console.clear();
intro(this.theme.primary(`š
Daily Virtue Check-in - ${formatDateDisplay()}`));
// Check if already completed today
const today = formatDate();
const existingEntry = this.db.getDailyEntryByDate(this.profile.id, today);
if (existingEntry) {
log.warning('You\'ve already completed today\'s check-in!');
const coherence = existingEntry.coherence_score;
const level = getCoherenceLevel(coherence);
log.info(`Today's coherence: ${level.icon} ${(coherence * 100).toFixed(1)}% (${level.level})`);
outro(this.theme.muted('Come back tomorrow for your next check-in'));
return;
}
// Get virtues
const virtues = this.db.getVirtuesForProfile(this.profile.id);
if (virtues.length === 0) {
log.error('No virtues found for this profile. Please add virtues first.');
return;
}
// Score each virtue
const scores = {};
for (const [index, virtue] of virtues.entries()) {
await this.scoreVirtue(virtue, scores, index + 1, virtues.length);
// Check for cancellation
if (isCancel(scores[virtue.id])) {
outro(this.theme.muted('Check-in cancelled'));
return;
}
}
// Get optional notes
const notes = await this.captureNotes();
// Calculate coherence
const weights = {};
virtues.forEach(v => weights[v.id] = v.priority_weight);
const coherence = calculateCoherence(scores, weights);
// Show results
await this.showResults(coherence, scores, virtues);
// Save entry
const completionTime = Math.round((Date.now() - this.startTime) / 1000);
await this.saveEntry(scores, notes, coherence, completionTime);
outro(this.theme.success('ā Daily check-in complete!'));
}
async runQuick() {
console.clear();
intro(this.theme.primary(`ā” Quick Check-in - ${formatDateDisplay()}`));
// Check if already completed today
const today = formatDate();
const existingEntry = this.db.getDailyEntryByDate(this.profile.id, today);
if (existingEntry) {
log.warning('You\'ve already completed today\'s check-in!');
return;
}
const virtues = this.db.getVirtuesForProfile(this.profile.id);
if (virtues.length === 0) {
log.error('No virtues found for this profile.');
return;
}
// Quick scoring - just overall alignment
log.info('Rate your overall virtue alignment today (0-10)');
const overallScore = await text({
message: 'Overall alignment:',
placeholder: '7',
validate: validateScore
});
if (isCancel(overallScore)) {
outro(this.theme.muted('Quick check-in cancelled'));
return;
}
const score = parseInt(overallScore);
// Apply same score to all virtues with slight variation
const scores = {};
for (const virtue of virtues) {
const variation = (Math.random() - 0.5) * 2; // -1 to 1
const adjustedScore = Math.max(0, Math.min(10, score + variation));
scores[virtue.id] = {
want: adjustedScore,
pull: adjustedScore * (0.8 + Math.random() * 0.2) // 80-100% of want
};
}
const weights = {};
virtues.forEach(v => weights[v.id] = v.priority_weight);
const coherence = calculateCoherence(scores, weights);
const level = getCoherenceLevel(coherence);
log.success(`Quick check-in recorded! Coherence: ${level.icon} ${(coherence * 100).toFixed(1)}%`);
// Save entry
await this.saveEntry(scores, 'Quick check-in', coherence, 30);
}
async scoreVirtue(virtue, scores, current, total) {
console.clear();
// Progress indicator
const progress = Math.round((current / total) * 100);
console.log(this.theme.muted(`Progress: ${ASCIIArt.progressBar(progress)} ${current}/${total}\n`));
// Virtue header
console.log(this.theme.primary(`${virtue.emoji} ${virtue.name}`));
console.log(this.theme.muted('ā'.repeat(50)));
console.log(virtue.definition);
console.log(this.theme.muted('ā'.repeat(50)));
// Want score
console.log(this.theme.secondary('\nš Psychological Want (aspiration/intention):'));
console.log(this.theme.muted('How much do you WANT to embody this virtue today?'));
const want = await text({
message: 'Score (0-10):',
placeholder: '5',
validate: validateScore
});
if (isCancel(want)) {
scores[virtue.id] = { want: 0, pull: 0 };
return;
}
// Pull score
console.log(this.theme.secondary('\nšÆ Physiological Pull (automatic behavior):'));
console.log(this.theme.muted('How much do you feel PULLED toward these behaviors?'));
const pull = await text({
message: 'Score (0-10):',
placeholder: '5',
validate: validateScore
});
if (isCancel(pull)) {
scores[virtue.id] = { want: parseInt(want), pull: 0 };
return;
}
const wantScore = parseInt(want);
const pullScore = parseInt(pull);
scores[virtue.id] = { want: wantScore, pull: pullScore };
// Show alignment
const alignment = calculateAlignment(wantScore, pullScore);
const alignmentBar = ASCIIArt.alignmentBar(alignment);
console.log(`\nā” Alignment: ${alignmentBar} ${(alignment * 100).toFixed(1)}%`);
await sleep(1000); // Brief pause for reflection
}
async captureNotes() {
console.log('\n');
const notes = await text({
message: 'š Any reflections or notes? (optional)',
placeholder: 'Press enter to skip...'
});
if (isCancel(notes) || !notes) {
return null;
}
return notes;
}
async showResults(coherence, scores, virtues) {
const s = spinner();
s.start(this.theme.primary('š§® Calculating daily coherence...'));
await sleep(1500);
s.stop();
console.clear();
console.log(this.theme.success('š Daily Check-in Complete!\n'));
// Overall coherence
const level = getCoherenceLevel(coherence);
const coherenceBar = ASCIIArt.coherenceBar(coherence);
console.log(this.theme.primary('Overall Coherence'));
console.log(`${level.icon} ${coherenceBar} ${(coherence * 100).toFixed(1)}% (${level.level})`);
// Individual virtue breakdown
console.log('\nāā Virtue Alignment Analysis āā');
console.log('ā ā');
for (const virtue of virtues) {
const score = scores[virtue.id];
if (score) {
const alignment = calculateAlignment(score.want, score.pull);
const percentage = (alignment * 100).toFixed(1);
const bar = ASCIIArt.progressBar(alignment * 100, 10);
const name = virtue.name.padEnd(18);
console.log(`ā ${name} ${percentage.padStart(5)}% ā ${bar}`);
}
}
console.log('ā ā');
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
// Trend comparison
const previousEntry = this.db.getLatestEntry(this.profile.id);
if (previousEntry) {
const trend = ASCIIArt.getTrendIndicator(coherence, previousEntry.coherence_score);
console.log(`\nTrend: ${trend}`);
}
// Streak information
const streak = this.db.calculateCurrentStreak(this.profile.id) + 1; // +1 for today
console.log(this.theme.primary(`\nš„ Current streak: ${streak} days`));
await sleep(2000); // Let user see results
}
async saveEntry(scores, notes, coherence, completionTime) {
const entry = await this.db.createDailyEntry({
profile_id: this.profile.id,
date: formatDate(),
virtue_scores: JSON.stringify(scores),
notes,
coherence_score: coherence,
completion_time_seconds: completionTime
});
// Log session
await this.db.createSession({
profile_id: this.profile.id,
session_type: 'daily',
duration_seconds: completionTime
});
// Auto-save if configured
await this.autoSaveManager.saveDailyEntry(this.profile, entry);
// Auto-export if configured
const exportResult = await this.autoExportManager.triggerDailyExport(this.profile.id, entry);
if (exportResult.success && exportResult.filePath) {
log.success(`ā Exported to: ${exportResult.filePath}`);
}
else if (!exportResult.success && exportResult.error) {
log.warning(`ā ļø Export failed: ${exportResult.error}`);
}
}
}
//# sourceMappingURL=DailyCheckin.js.map