UNPKG

emotionctl

Version:

A secure terminal-based journaling system designed as a safe space for developers going through heartbreak, breakups, or betrayal

785 lines 33.1 kB
"use strict"; 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.CLIInterface = void 0; const date_fns_1 = require("date-fns"); const Editor_1 = require("./Editor"); const types_1 = require("../types"); class CLIInterface { constructor(authManager, journalManager) { this.currentPassword = ''; this.inquirer = null; this.chalk = null; this.authManager = authManager; this.journalManager = journalManager; } /** * Dynamically imports inquirer */ async getInquirer() { if (!this.inquirer) { const inquirer = await Promise.resolve().then(() => __importStar(require('inquirer'))); this.inquirer = inquirer.default || inquirer; } return this.inquirer; } /** * Dynamically imports chalk */ async getChalk() { if (!this.chalk) { const chalk = await Promise.resolve().then(() => __importStar(require('chalk'))); this.chalk = chalk.default || chalk; } return this.chalk; } /** * Displays the welcome banner */ async displayBanner() { const chalk = await this.getChalk(); console.clear(); console.log(chalk.cyan('╔══════════════════════════════════════╗')); console.log(chalk.cyan('║ EmotionCtl Journal ║')); console.log(chalk.cyan('║ Your Safe Space for Healing ║')); console.log(chalk.cyan('╚══════════════════════════════════════╝')); console.log(); } /** * Prompts for password with confirmation */ async promptPassword(confirm = false) { const chalk = await this.getChalk(); const inquirer = await this.getInquirer(); const questions = [ { type: 'password', name: 'password', message: 'Enter your journal password:', mask: '*' } ]; if (confirm) { questions.push({ type: 'password', name: 'confirmPassword', message: 'Confirm your password:', mask: '*' }); } const answers = await inquirer.prompt(questions); if (confirm && answers.password !== answers.confirmPassword) { console.log(chalk.red('Passwords do not match. Please try again.')); return this.promptPassword(confirm); } return answers.password; } /** * Authenticates the user */ async authenticate() { const chalk = await this.getChalk(); if (this.currentPassword) { return true; } const password = await this.promptPassword(); const isAuthenticated = await this.authManager.authenticate(password); if (isAuthenticated) { this.currentPassword = password; await this.journalManager.load(password); return true; } else { console.log(chalk.red('Invalid password. Please try again.')); return false; } } /** * Initializes a new journal */ async initializeJournal() { const chalk = await this.getChalk(); await this.displayBanner(); if (await this.authManager.isInitialized()) { console.log(chalk.yellow('Journal is already initialized.')); return; } console.log(chalk.green('Welcome! Let\'s create your safe space for emotional healing.')); console.log(chalk.gray('Your thoughts and feelings will be encrypted and stored securely on your device.')); console.log(chalk.gray('This is your judgment-free zone for processing difficult emotions.')); console.log(); const password = await this.promptPassword(true); try { await this.authManager.initialize(password); await this.journalManager.initialize(password); console.log(chalk.green('✓ Your safe space is ready!')); console.log(chalk.gray('You can now start processing your emotions with: emotionctl write')); console.log(chalk.gray('Remember: Healing isn\'t linear, and every feeling is valid. 💙')); } catch (error) { console.error(chalk.red('Failed to initialize journal:'), error); } } /** * Writes a new journal entry */ async writeEntry(title) { const chalk = await this.getChalk(); if (!await this.authManager.isInitialized()) { console.log(chalk.red('Safe space not set up yet. Run "emotionctl init" to create your secure sanctuary.')); return; } if (!await this.authenticate()) { return; } const questions = []; if (!title) { questions.push({ type: 'input', name: 'title', message: 'What would you like to call this entry?', validate: (input) => input.trim().length > 0 || 'Your entry needs a title' }); } questions.push({ type: 'list', name: 'mood', message: 'How are you feeling right now?', choices: [ { name: `${types_1.MoodType.SAD} Sad - it's okay to feel this way`, value: types_1.MoodType.SAD }, { name: `${types_1.MoodType.ANGRY} Angry - your feelings are valid`, value: types_1.MoodType.ANGRY }, { name: `${types_1.MoodType.ANXIOUS} Anxious - you're not alone in this`, value: types_1.MoodType.ANXIOUS }, { name: `${types_1.MoodType.NEUTRAL} Neutral - processing emotions`, value: types_1.MoodType.NEUTRAL }, { name: `${types_1.MoodType.CALM} Calm - finding peace`, value: types_1.MoodType.CALM }, { name: `${types_1.MoodType.GRATEFUL} Grateful - recognizing growth`, value: types_1.MoodType.GRATEFUL }, { name: `${types_1.MoodType.HAPPY} Happy - celebrating progress`, value: types_1.MoodType.HAPPY }, { name: `${types_1.MoodType.EXCITED} Excited - looking forward`, value: types_1.MoodType.EXCITED }, { name: 'Skip for now', value: undefined } ] }, { type: 'input', name: 'tags', message: 'Tags to help you reflect later (e.g., "healing", "breakthrough", "setback"):', filter: (input) => input.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0) }); const inquirer = await this.getInquirer(); const answers = await inquirer.prompt(questions); // Open editor for content let content; try { console.log(); console.log(chalk.blue('Express yourself freely - your thoughts are safe here.')); content = await this.openEditor(); if (!content.trim()) { console.log(chalk.yellow('Entry cancelled - no content provided.')); return; } } catch (error) { console.error(chalk.red('Failed to open editor:'), error); console.log(chalk.yellow('Falling back to simple text input...')); const inquirer = await this.getInquirer(); const fallbackAnswer = await inquirer.prompt([{ type: 'input', name: 'content', message: 'Write your entry here:', validate: (input) => input.trim().length > 0 || 'It\'s okay to share what you\'re feeling' }]); content = fallbackAnswer.content; } try { const entry = await this.journalManager.addEntry(title || answers.title, content, this.currentPassword, answers.mood, answers.tags); console.log(chalk.green('✓ Your thoughts have been safely stored')); console.log(chalk.gray(`Entry ID: ${entry.id}`)); console.log(chalk.blue('Remember: Every step in processing your emotions is progress. 💙')); } catch (error) { console.error(chalk.red('Failed to save entry:'), error); } } /** * Edits an existing journal entry */ async editEntry(id) { const chalk = await this.getChalk(); if (!await this.authManager.isInitialized()) { console.log(chalk.red('Journal not initialized. Run "emotionctl init" first.')); return; } if (!await this.authenticate()) { return; } try { let entryId = id; if (!entryId) { const entries = this.journalManager.getEntries(); if (entries.length === 0) { console.log(chalk.yellow('No entries to edit.')); return; } const choices = entries.map(entry => ({ name: `${(0, date_fns_1.format)(entry.date, 'PPP')} - ${entry.title}`, value: entry.id })); const inquirer = await this.getInquirer(); const { selectedId } = await inquirer.prompt({ type: 'list', name: 'selectedId', message: 'Select entry to edit:', choices }); entryId = selectedId; } const entry = this.journalManager.getEntryById(entryId); if (!entry) { console.log(chalk.red('Entry not found.')); return; } console.log(chalk.blue(`Editing entry: ${entry.title}`)); console.log(chalk.gray(`Created: ${(0, date_fns_1.format)(entry.date, 'PPP')}`)); console.log(); const inquirer = await this.getInquirer(); const { action } = await inquirer.prompt({ type: 'list', name: 'action', message: 'What would you like to edit?', choices: [ { name: 'Edit content (opens editor)', value: 'content' }, { name: 'Edit title', value: 'title' }, { name: 'Edit mood', value: 'mood' }, { name: 'Edit tags', value: 'tags' }, { name: 'Cancel', value: 'cancel' } ] }); if (action === 'cancel') { console.log(chalk.yellow('Edit cancelled.')); return; } const updates = {}; switch (action) { case 'content': try { console.log(); console.log(chalk.blue('Opening editor with current content...')); const newContent = await this.openEditor(entry.content); if (newContent.trim() === entry.content.trim()) { console.log(chalk.yellow('No changes made to content.')); return; } updates.content = newContent; } catch (error) { console.error(chalk.red('Failed to open editor:'), error); return; } break; case 'title': const inquirer1 = await this.getInquirer(); const { newTitle } = await inquirer1.prompt({ type: 'input', name: 'newTitle', message: 'Enter new title:', default: entry.title, validate: (input) => input.trim().length > 0 || 'Title cannot be empty' }); updates.title = newTitle; break; case 'mood': const inquirer2 = await this.getInquirer(); const { newMood } = await inquirer2.prompt({ type: 'list', name: 'newMood', message: 'Select new mood:', default: entry.mood, choices: [ { name: `${types_1.MoodType.SAD} Sad`, value: types_1.MoodType.SAD }, { name: `${types_1.MoodType.ANGRY} Angry`, value: types_1.MoodType.ANGRY }, { name: `${types_1.MoodType.ANXIOUS} Anxious`, value: types_1.MoodType.ANXIOUS }, { name: `${types_1.MoodType.NEUTRAL} Neutral`, value: types_1.MoodType.NEUTRAL }, { name: `${types_1.MoodType.CALM} Calm`, value: types_1.MoodType.CALM }, { name: `${types_1.MoodType.GRATEFUL} Grateful`, value: types_1.MoodType.GRATEFUL }, { name: `${types_1.MoodType.HAPPY} Happy`, value: types_1.MoodType.HAPPY }, { name: `${types_1.MoodType.EXCITED} Excited`, value: types_1.MoodType.EXCITED }, { name: 'Clear mood', value: undefined } ] }); updates.mood = newMood; break; case 'tags': const inquirer3 = await this.getInquirer(); const { newTags } = await inquirer3.prompt({ type: 'input', name: 'newTags', message: 'Enter tags (comma separated):', default: entry.tags?.join(', ') || '', filter: (input) => input.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0) }); updates.tags = newTags; break; } const success = await this.journalManager.updateEntry(entryId, updates, this.currentPassword); if (success) { console.log(chalk.green('✓ Entry updated successfully')); console.log(chalk.blue('Your healing journey continues. 💙')); } else { console.log(chalk.red('Failed to update entry.')); } } catch (error) { console.error(chalk.red('Failed to edit entry:'), error); } } /** * Reads journal entries */ async readEntries(options) { const chalk = await this.getChalk(); if (!await this.authManager.isInitialized()) { console.log(chalk.red('Journal not initialized. Run "emotionctl init" first.')); return; } if (!await this.authenticate()) { return; } try { let entries; if (options.date) { entries = this.journalManager.getEntriesByDate(options.date); console.log(chalk.blue(`Entries for ${options.date}:`)); } else if (options.search) { entries = this.journalManager.searchEntries(options.search); console.log(chalk.blue(`Search results for "${options.search}":`)); } else if (options.list) { entries = this.journalManager.getEntries(); console.log(chalk.blue('All entries:')); } else { // Show recent entries (last 5) entries = this.journalManager.getEntries().slice(0, 5); console.log(chalk.blue('Recent entries:')); } if (entries.length === 0) { console.log(chalk.yellow('No entries found.')); return; } entries.forEach((entry, index) => { console.log(chalk.cyan(`\n─── Entry ${index + 1} ───`)); console.log(chalk.bold(`Title: ${entry.title}`)); console.log(chalk.gray(`Date: ${(0, date_fns_1.format)(entry.date, 'PPP p')}`)); console.log(chalk.gray(`ID: ${entry.id}`)); if (entry.mood) { console.log(chalk.gray(`Mood: ${entry.mood}`)); } if (entry.tags && entry.tags.length > 0) { console.log(chalk.gray(`Tags: ${entry.tags.join(', ')}`)); } console.log(`\n${entry.content}\n`); }); } catch (error) { console.error(chalk.red('Failed to read entries:'), error); } } /** * Deletes a journal entry */ async deleteEntry(id) { const chalk = await this.getChalk(); if (!await this.authManager.isInitialized()) { console.log(chalk.red('Journal not initialized. Run "emotionctl init" first.')); return; } if (!await this.authenticate()) { return; } try { if (!id) { const entries = this.journalManager.getEntries(); if (entries.length === 0) { console.log(chalk.yellow('No entries to delete.')); return; } const choices = entries.map(entry => ({ name: `${(0, date_fns_1.format)(entry.date, 'PPP')} - ${entry.title}`, value: entry.id })); const inquirer = await this.getInquirer(); const { selectedId } = await inquirer.prompt({ type: 'list', name: 'selectedId', message: 'Select entry to delete:', choices }); id = selectedId; } const entry = this.journalManager.getEntryById(id); if (!entry) { console.log(chalk.red('Entry not found.')); return; } console.log(chalk.yellow(`\nEntry to delete:`)); console.log(chalk.bold(`Title: ${entry.title}`)); console.log(chalk.gray(`Date: ${(0, date_fns_1.format)(entry.date, 'PPP p')}`)); console.log(`Content preview: ${entry.content.substring(0, 100)}...`); const inquirer = await this.getInquirer(); const { confirm } = await inquirer.prompt({ type: 'confirm', name: 'confirm', message: 'Are you sure you want to delete this entry?', default: false }); if (confirm) { const deleted = await this.journalManager.deleteEntry(id, this.currentPassword); if (deleted) { console.log(chalk.green('✓ Entry deleted successfully!')); } else { console.log(chalk.red('Failed to delete entry.')); } } else { console.log(chalk.gray('Delete cancelled.')); } } catch (error) { console.error(chalk.red('Failed to delete entry:'), error); } } /** * Creates a backup */ async createBackup(outputPath) { const chalk = await this.getChalk(); if (!await this.authManager.isInitialized()) { console.log(chalk.red('Journal not initialized. Run "emotionctl init" first.')); return; } if (!await this.authenticate()) { return; } try { const backupPath = await this.journalManager.createBackup(this.currentPassword, outputPath); console.log(chalk.green('✓ Backup created successfully!')); console.log(chalk.gray(`Backup saved to: ${backupPath}`)); } catch (error) { console.error(chalk.red('Failed to create backup:'), error); } } /** * Restores from backup */ async restoreBackup(inputPath) { const chalk = await this.getChalk(); if (!inputPath) { console.log(chalk.red('Please provide the backup file path with --input')); return; } console.log(chalk.yellow('⚠️ Warning: This will overwrite your current journal!')); const inquirer = await this.getInquirer(); const { confirm } = await inquirer.prompt({ type: 'confirm', name: 'confirm', message: 'Are you sure you want to restore from backup?', default: false }); if (!confirm) { console.log(chalk.gray('Restore cancelled.')); return; } const password = await this.promptPassword(); try { await this.journalManager.restoreFromBackup(inputPath, password); console.log(chalk.green('✓ Journal restored successfully!')); } catch (error) { console.error(chalk.red('Failed to restore backup:'), error); } } /** * Changes the password */ async changePassword() { const chalk = await this.getChalk(); if (!await this.authManager.isInitialized()) { console.log(chalk.red('Journal not initialized. Run "emotionctl init" first.')); return; } const currentPassword = await this.promptPassword(); console.log(chalk.blue('Enter your new password:')); const newPassword = await this.promptPassword(true); try { await this.authManager.changePassword(currentPassword, newPassword); this.currentPassword = newPassword; // Re-encrypt all entries with new password await this.journalManager.load(newPassword); console.log(chalk.green('✓ Password changed successfully!')); } catch (error) { console.error(chalk.red('Failed to change password:'), error); } } /** * Interactive mode */ async interactiveMode() { const chalk = await this.getChalk(); await this.displayBanner(); if (!await this.authManager.isInitialized()) { console.log(chalk.yellow('Journal not initialized. Let\'s set it up!')); await this.initializeJournal(); return; } if (!await this.authenticate()) { return; } const stats = this.journalManager.getStats(); console.log(chalk.green('Welcome back! 📖')); console.log(chalk.gray(`Total entries: ${stats.totalEntries}`)); console.log(chalk.gray(`Average words per entry: ${stats.avgWordsPerEntry}`)); console.log(); while (true) { const inquirer = await this.getInquirer(); const { action } = await inquirer.prompt({ type: 'list', name: 'action', message: 'What would you like to do?', choices: [ { name: '📝 Write new entry', value: 'write' }, { name: '📖 Read entries', value: 'read' }, { name: '✏️ Edit entry', value: 'edit' }, { name: '🔍 Search entries', value: 'search' }, { name: '🗑️ Delete entry', value: 'delete' }, { name: '💾 Create backup', value: 'backup' }, { name: '🔧 Change password', value: 'password' }, { name: '📊 View statistics', value: 'stats' }, { name: '🚪 Exit', value: 'exit' } ] }); try { switch (action) { case 'write': await this.displayBanner(); await this.writeEntry(); break; case 'read': await this.displayBanner(); await this.readEntries({ list: true }); break; case 'edit': await this.displayBanner(); await this.editEntry(); break; case 'search': await this.displayBanner(); const inquirer1 = await this.getInquirer(); const { searchTerm } = await inquirer1.prompt({ type: 'input', name: 'searchTerm', message: 'Enter search term:' }); await this.readEntries({ search: searchTerm }); break; case 'delete': await this.displayBanner(); await this.deleteEntry(); break; case 'backup': await this.createBackup(); break; case 'password': await this.displayBanner(); await this.changePassword(); break; case 'stats': await this.displayBanner(); await this.displayStats(); break; case 'exit': await this.displayBanner(); console.log(chalk.blue('Goodbye! 👋')); return; } } catch (error) { console.error(chalk.red('Error:'), error); } console.log(); // Add spacing between actions } } /** * Displays journal statistics */ async displayStats() { const chalk = await this.getChalk(); const stats = this.journalManager.getStats(); console.log(chalk.cyan('\n📊 Journal Statistics')); console.log(chalk.cyan('─────────────────────')); console.log(`Total entries: ${chalk.bold(stats.totalEntries.toString())}`); console.log(`Average words per entry: ${chalk.bold(stats.avgWordsPerEntry.toString())}`); if (stats.oldestEntry) { console.log(`Oldest entry: ${chalk.bold((0, date_fns_1.format)(stats.oldestEntry, 'PPP'))}`); } if (stats.newestEntry) { console.log(`Newest entry: ${chalk.bold((0, date_fns_1.format)(stats.newestEntry, 'PPP'))}`); } console.log(); } /** * Resets the journal (deletes all data) */ async resetJournal() { this.displayBanner(); const chalk = await this.getChalk(); console.log(chalk.red('⚠️ WARNING: This will permanently delete all journal data!')); console.log(chalk.gray('This action cannot be undone unless you have a backup.')); console.log(); const inquirer = await this.getInquirer(); const { confirm } = await inquirer.prompt({ type: 'confirm', name: 'confirm', message: 'Are you absolutely sure you want to reset your journal?', default: false }); if (!confirm) { console.log(chalk.gray('Reset cancelled.')); return; } const { doubleConfirm } = await inquirer.prompt({ type: 'input', name: 'doubleConfirm', message: 'Type "DELETE MY JOURNAL" to confirm:', validate: (input) => input === 'DELETE MY JOURNAL' || 'You must type exactly "DELETE MY JOURNAL" to confirm' }); if (doubleConfirm === 'DELETE MY JOURNAL') { try { const configDir = this.authManager.getConfigDir(); await require('fs-extra').remove(configDir); console.log(chalk.green('✓ Journal reset successfully!')); console.log(chalk.gray('You can now run "emotionctl init" to create a new journal.')); } catch (error) { console.error(chalk.red('Failed to reset journal:'), error); } } else { console.log(chalk.gray('Reset cancelled.')); } } /** * Opens the built-in editor for content input */ async openEditor(initialContent = '') { const chalk = await this.getChalk(); try { console.log(chalk.blue('Opening built-in editor...')); console.log(chalk.gray('Use Ctrl+X to save and exit, Ctrl+G for help')); console.log(chalk.gray('If you can\'t type, try pressing Enter first to activate input')); console.log(); const editor = new Editor_1.Editor(); const content = await editor.edit(initialContent); console.log(chalk.green('Editor closed.')); return content; } catch (error) { console.log(chalk.yellow('Built-in editor failed, falling back to system editor...')); return this.openSystemEditor(initialContent); } } /** * Fallback to system editor (notepad on Windows) */ async openSystemEditor(initialContent = '') { const chalk = await this.getChalk(); const fs = require('fs-extra'); const path = require('path'); const { spawn } = require('child_process'); const os = require('os'); try { // Create a temporary file const tempDir = os.tmpdir(); const tempFile = path.join(tempDir, `emotionctl-${Date.now()}.txt`); // Write initial content to temp file await fs.writeFile(tempFile, initialContent, 'utf8'); console.log(chalk.blue('Opening system editor...')); console.log(chalk.gray(`Temp file: ${tempFile}`)); console.log(chalk.yellow('Close the editor when you\'re done to continue.')); // Determine editor command based on OS let editorCmd, editorArgs; if (process.platform === 'win32') { editorCmd = 'notepad.exe'; editorArgs = [tempFile]; } else if (process.platform === 'darwin') { editorCmd = 'open'; editorArgs = ['-W', '-a', 'TextEdit', tempFile]; } else { editorCmd = process.env.EDITOR || 'nano'; editorArgs = [tempFile]; } // Spawn the editor const child = spawn(editorCmd, editorArgs, { stdio: 'inherit' }); // Wait for editor to close await new Promise((resolve, reject) => { child.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`Editor exited with code ${code}`)); } }); child.on('error', (error) => { reject(error); }); }); // Read the content back const content = await fs.readFile(tempFile, 'utf8'); // Clean up temp file await fs.unlink(tempFile); console.log(chalk.green('Content loaded from editor.')); return content; } catch (error) { throw new Error(`Failed to open system editor: ${error}`); } } } exports.CLIInterface = CLIInterface; //# sourceMappingURL=CLIInterface.js.map