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
JavaScript
"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