emotionctl
Version:
A secure terminal-based journaling system designed as a safe space for developers going through heartbreak, breakups, or betrayal
289 lines • 10.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.JournalManager = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const date_fns_1 = require("date-fns");
const CryptoService_1 = require("./CryptoService");
class JournalManager {
constructor(authManager) {
this.entries = [];
this.config = null;
const configDir = authManager?.getConfigDir() || path.join(require('os').homedir(), '.emotionctl');
this.journalDir = configDir;
this.configFile = path.join(this.journalDir, 'config.json');
this.entriesFile = path.join(this.journalDir, 'entries.json');
} /**
* Initializes the journal
*/
async initialize(password) {
try {
await fs.ensureDir(this.journalDir);
this.config = {
version: '1.0.0',
created: new Date(),
lastModified: new Date(),
entryCount: 0,
encrypted: true
};
this.entries = [];
await this.saveConfig();
await this.saveEntries(password);
}
catch (error) {
throw new Error(`Failed to initialize journal: ${error}`);
}
}
/**
* Loads the journal configuration and entries
*/
async load(password) {
try {
// Load config
if (await fs.pathExists(this.configFile)) {
this.config = await fs.readJson(this.configFile);
}
else {
throw new Error('Journal not found. Run "emotionctl init" first.');
} // Load entries
if (await fs.pathExists(this.entriesFile)) {
const encryptedData = await fs.readJson(this.entriesFile);
if (encryptedData && encryptedData.data && encryptedData.iv && encryptedData.salt) {
try {
const decryptedData = CryptoService_1.CryptoService.decrypt(encryptedData, password);
this.entries = JSON.parse(decryptedData);
// Convert date strings back to Date objects
this.entries = this.entries.map(entry => ({
...entry,
date: new Date(entry.date)
}));
}
catch (decryptError) {
throw new Error(`Failed to decrypt entries. Please check your password. Details: ${decryptError}`);
}
}
else {
this.entries = [];
}
}
else {
this.entries = [];
}
}
catch (error) {
if (error instanceof Error && error.message.includes('Failed to decrypt entries')) {
throw error;
}
throw new Error(`Failed to load journal: ${error}`);
}
}
/**
* Saves the journal configuration
*/
async saveConfig() {
if (!this.config)
return;
try {
this.config.lastModified = new Date();
this.config.entryCount = this.entries.length;
await fs.writeJson(this.configFile, this.config, { spaces: 2 });
}
catch (error) {
throw new Error(`Failed to save config: ${error}`);
}
} /**
* Saves the journal entries
*/
async saveEntries(password) {
try {
const entriesData = JSON.stringify(this.entries, null, 2);
const encryptedData = CryptoService_1.CryptoService.encrypt(entriesData, password);
await fs.writeJson(this.entriesFile, encryptedData, { spaces: 2 });
await this.saveConfig();
}
catch (error) {
throw new Error(`Failed to save entries: ${error}`);
}
}
/**
* Adds a new journal entry
*/
async addEntry(title, content, password, mood, tags) {
try {
const entry = {
id: CryptoService_1.CryptoService.generateId(),
title,
content,
date: new Date(),
mood,
tags: tags || [],
encrypted: true
};
this.entries.push(entry);
await this.saveEntries(password);
return entry;
}
catch (error) {
throw new Error(`Failed to add entry: ${error}`);
}
}
/**
* Gets all journal entries
*/
getEntries() {
return [...this.entries].sort((a, b) => b.date.getTime() - a.date.getTime());
}
/**
* Gets entries by date
*/
getEntriesByDate(date) {
const targetDate = (0, date_fns_1.parseISO)(date);
return this.entries.filter(entry => (0, date_fns_1.format)(entry.date, 'yyyy-MM-dd') === (0, date_fns_1.format)(targetDate, 'yyyy-MM-dd'));
}
/**
* Searches entries by term
*/
searchEntries(term) {
const searchTerm = term.toLowerCase();
return this.entries.filter(entry => entry.title.toLowerCase().includes(searchTerm) ||
entry.content.toLowerCase().includes(searchTerm) ||
entry.tags?.some(tag => tag.toLowerCase().includes(searchTerm)));
}
/**
* Gets an entry by ID
*/
getEntryById(id) {
return this.entries.find(entry => entry.id === id);
}
/**
* Deletes an entry by ID
*/
async deleteEntry(id, password) {
try {
const index = this.entries.findIndex(entry => entry.id === id);
if (index === -1) {
return false;
}
this.entries.splice(index, 1);
await this.saveEntries(password);
return true;
}
catch (error) {
throw new Error(`Failed to delete entry: ${error}`);
}
}
/**
* Updates an entry
*/
async updateEntry(id, updates, password) {
try {
const index = this.entries.findIndex(entry => entry.id === id);
if (index === -1) {
return false;
}
this.entries[index] = { ...this.entries[index], ...updates };
await this.saveEntries(password);
return true;
}
catch (error) {
throw new Error(`Failed to update entry: ${error}`);
}
}
/**
* Creates a backup of the journal
*/
async createBackup(password, outputPath) {
try {
const backupData = {
config: this.config,
entries: this.entries,
timestamp: new Date()
};
const backupJson = JSON.stringify(backupData, null, 2);
const encryptedBackup = CryptoService_1.CryptoService.encrypt(backupJson, password);
const fileName = `emotionctl-backup-${(0, date_fns_1.format)(new Date(), 'yyyy-MM-dd-HHmmss')}.json`;
const filePath = outputPath || path.join(this.journalDir, fileName);
await fs.writeJson(filePath, encryptedBackup, { spaces: 2 });
return filePath;
}
catch (error) {
throw new Error(`Failed to create backup: ${error}`);
}
}
/**
* Restores journal from backup
*/
async restoreFromBackup(backupPath, password) {
try {
const encryptedBackup = await fs.readJson(backupPath);
const decryptedBackup = CryptoService_1.CryptoService.decrypt(encryptedBackup, password);
const backupData = JSON.parse(decryptedBackup);
// Convert date strings back to Date objects
this.config = {
...backupData.config,
created: new Date(backupData.config.created),
lastModified: new Date(backupData.config.lastModified)
};
this.entries = backupData.entries.map(entry => ({
...entry,
date: new Date(entry.date)
}));
await this.saveConfig();
await this.saveEntries(password);
}
catch (error) {
throw new Error(`Failed to restore from backup: ${error}`);
}
}
/**
* Gets journal statistics
*/
getStats() {
if (this.entries.length === 0) {
return { totalEntries: 0, avgWordsPerEntry: 0 };
}
const sortedEntries = this.entries.sort((a, b) => a.date.getTime() - b.date.getTime());
const totalWords = this.entries.reduce((sum, entry) => sum + entry.content.split(/\s+/).length, 0);
return {
totalEntries: this.entries.length,
oldestEntry: sortedEntries[0]?.date,
newestEntry: sortedEntries[sortedEntries.length - 1]?.date,
avgWordsPerEntry: Math.round(totalWords / this.entries.length)
};
}
}
exports.JournalManager = JournalManager;
//# sourceMappingURL=JournalManager.js.map