virtue-cli
Version:
Personal character development CLI tool for tracking virtues and philosophical alignment
257 lines • 8.53 kB
JavaScript
import { randomUUID } from 'crypto';
export class VirtueVersionRepository {
database;
constructor(database) {
this.database = database;
}
get db() {
return this.database.getDatabase();
}
/**
* Create a new version for a virtue
*/
async createVersion(version) {
const id = randomUUID();
const stmt = this.db.prepare(`
INSERT INTO virtue_versions (
id, virtue_id, version_number, name, definition,
sub_dimensions, behavioral_indicators, source_type,
source_context, is_active, created_reason
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(id, version.virtue_id, version.version_number, version.name, version.definition, version.sub_dimensions, version.behavioral_indicators, version.source_type, version.source_context, version.is_active ? 1 : 0, version.created_reason);
return this.getVersion(id);
}
/**
* Get a specific version by ID
*/
getVersion(id) {
const stmt = this.db.prepare('SELECT * FROM virtue_versions WHERE id = ?');
const version = stmt.get(id);
if (version) {
// Convert SQLite boolean
version.is_active = !!version.is_active;
}
return version;
}
/**
* Get all versions for a virtue
*/
getVersionsForVirtue(virtueId) {
const stmt = this.db.prepare(`
SELECT * FROM virtue_versions
WHERE virtue_id = ?
ORDER BY version_number DESC
`);
const versions = stmt.all(virtueId);
// Convert SQLite booleans
return versions.map(v => ({
...v,
is_active: !!v.is_active
}));
}
/**
* Get the active version for a virtue
*/
getActiveVersion(virtueId) {
const stmt = this.db.prepare(`
SELECT * FROM virtue_versions
WHERE virtue_id = ? AND is_active = 1
`);
const version = stmt.get(virtueId);
if (version) {
version.is_active = true;
return version;
}
return null;
}
/**
* Get a specific version by virtue ID and version number
*/
getVersionByNumber(virtueId, versionNumber) {
const stmt = this.db.prepare(`
SELECT * FROM virtue_versions
WHERE virtue_id = ? AND version_number = ?
`);
const version = stmt.get(virtueId, versionNumber);
if (version) {
version.is_active = !!version.is_active;
return version;
}
return null;
}
/**
* Activate a specific version (deactivates all others)
*/
async activateVersion(virtueId, versionNumber) {
const db = this.db;
try {
db.prepare('BEGIN').run();
// Deactivate all versions
db.prepare(`
UPDATE virtue_versions
SET is_active = 0
WHERE virtue_id = ?
`).run(virtueId);
// Activate the specific version
const result = db.prepare(`
UPDATE virtue_versions
SET is_active = 1
WHERE virtue_id = ? AND version_number = ?
`).run(virtueId, versionNumber);
// Update the virtue's current_version
db.prepare(`
UPDATE virtues
SET current_version = ?
WHERE id = ?
`).run(versionNumber, virtueId);
db.prepare('COMMIT').run();
return result.changes > 0;
}
catch (error) {
db.prepare('ROLLBACK').run();
throw error;
}
}
/**
* Get the next version number for a virtue
*/
async getNextVersionNumber(virtueId) {
const stmt = this.db.prepare(`
SELECT MAX(version_number) as max_version
FROM virtue_versions
WHERE virtue_id = ?
`);
const result = stmt.get(virtueId);
return (result.max_version || 0) + 1;
}
/**
* Create a version comparison record
*/
async createComparison(comparison) {
const id = randomUUID();
const stmt = this.db.prepare(`
INSERT INTO virtue_version_comparisons (
id, virtue_id, from_version, to_version, changes_summary
) VALUES (?, ?, ?, ?, ?)
`);
stmt.run(id, comparison.virtue_id, comparison.from_version, comparison.to_version, comparison.changes_summary);
return this.getComparison(id);
}
/**
* Get a comparison by ID
*/
getComparison(id) {
const stmt = this.db.prepare(`
SELECT * FROM virtue_version_comparisons WHERE id = ?
`);
return stmt.get(id);
}
/**
* Get usage statistics for a virtue version
*/
async getVersionStats(virtueId, versionNumber) {
const db = this.db;
// Get usage count and score averages
const usageStmt = db.prepare(`
SELECT
COUNT(*) as times_used,
AVG(json_extract(de.virtue_scores, '$."' || ? || '".want')) as avg_want,
AVG(json_extract(de.virtue_scores, '$."' || ? || '".pull')) as avg_pull,
MIN(de.date) as first_used,
MAX(de.date) as last_used
FROM daily_entries de
WHERE de.profile_id IN (
SELECT profile_id FROM virtues WHERE id = ?
)
AND (
de.virtue_versions_used IS NULL
OR json_extract(de.virtue_versions_used, '$."' || ? || '"') = ?
)
`);
const usageResult = usageStmt.get(virtueId, virtueId, virtueId, virtueId, versionNumber);
// Calculate active days
let activeDays = 0;
if (usageResult.first_used && usageResult.last_used) {
const firstDate = new Date(usageResult.first_used);
const lastDate = new Date(usageResult.last_used);
activeDays = Math.floor((lastDate.getTime() - firstDate.getTime()) / (1000 * 60 * 60 * 24)) + 1;
}
return {
version_number: versionNumber,
times_used: usageResult.times_used || 0,
average_want_score: usageResult.avg_want || 0,
average_pull_score: usageResult.avg_pull || 0,
active_days: activeDays,
first_used: usageResult.first_used,
last_used: usageResult.last_used
};
}
/**
* Get a version with parsed sub-dimensions and indicators
*/
async getVersionWithDetails(virtueId, versionNumber) {
const version = this.getVersionByNumber(virtueId, versionNumber);
if (!version) {
return null;
}
const details = {
...version,
parsed_sub_dimensions: undefined,
parsed_behavioral_indicators: undefined
};
// Parse sub-dimensions
if (version.sub_dimensions) {
try {
details.parsed_sub_dimensions = JSON.parse(version.sub_dimensions);
}
catch {
// Leave as undefined if parsing fails
}
}
// Parse behavioral indicators
if (version.behavioral_indicators) {
try {
details.parsed_behavioral_indicators = JSON.parse(version.behavioral_indicators);
}
catch {
// Leave as undefined if parsing fails
}
}
return details;
}
/**
* Delete a version (if not the last one)
*/
async deleteVersion(virtueId, versionNumber) {
const db = this.db;
// Check if it's the only version
const countStmt = db.prepare(`
SELECT COUNT(*) as count FROM virtue_versions WHERE virtue_id = ?
`);
const { count } = countStmt.get(virtueId);
if (count <= 1) {
throw new Error('Cannot delete the only version of a virtue');
}
// Check if it's the active version
const version = this.getVersionByNumber(virtueId, versionNumber);
if (version?.is_active) {
throw new Error('Cannot delete the active version. Please activate another version first.');
}
const stmt = db.prepare(`
DELETE FROM virtue_versions
WHERE virtue_id = ? AND version_number = ?
`);
const result = stmt.run(virtueId, versionNumber);
// Update total_versions count
if (result.changes > 0) {
db.prepare(`
UPDATE virtues
SET total_versions = total_versions - 1
WHERE id = ?
`).run(virtueId);
}
return result.changes > 0;
}
}
//# sourceMappingURL=VirtueVersionRepository.js.map