devbrain 0.1.0

Local-first CLI to capture, search, and recall developer workflow (commands, errors, and fixes)
mod cli;
mod config;
mod display;
mod error;
mod models;
mod query;
mod storage;
mod utils;

use cli::Commands;
use config::{load_config, Config};
use display::print_entry;
use error::DevbrainError;
use models::{Entry, EntryType};
use query::process_entries;
use std::fs;
use std::io::{self, Write};
use std::process;
use std::str::FromStr;
use storage::Database;

fn main() {
    if let Err(error) = run() {
        eprintln!("Error: {}", error);
        process::exit(1);
    }
}

fn run() -> Result<(), DevbrainError> {
    let cli = cli::parse();
    let config = load_config()?;

    match cli.command {
        Commands::Log { message } => log_message(&config, message)?,
        Commands::LogCmd { command } => log_command(&config, command)?,
        Commands::LogError { error } => log_error(&config, error)?,
        Commands::Search {
            query,
            all,
            limit,
            offset,
            entry_type,
        } => search_entries(&config, query, all, limit, offset, entry_type)?,
        Commands::Timeline {
            all,
            limit,
            offset,
            entry_type,
        } => show_timeline(&config, all, limit, offset, entry_type)?,
        Commands::Errors { all, limit } => {
            show_recent_entries_by_type(&config, all, limit, EntryType::Error)?
        }
        Commands::CmdHistory { all, limit } => {
            show_recent_entries_by_type(&config, all, limit, EntryType::Command)?
        }
        Commands::Last { all } => show_last_entry(&config, all)?,
        Commands::Clear { force } => clear_entries(&config, force)?,
        Commands::Export { output } => export_entries(&config, output)?,
        Commands::Import { input, merge } => import_entries(&config, input, merge)?,
    }

    Ok(())
}

fn log_message(config: &Config, message: String) -> Result<(), DevbrainError> {
    create_and_store_entry(config, EntryType::Log, message)?;
    println!("Logged successfully");

    Ok(())
}

fn log_command(config: &Config, command: String) -> Result<(), DevbrainError> {
    create_and_store_entry(config, EntryType::Command, command)?;
    println!("Command logged");

    Ok(())
}

fn log_error(config: &Config, error: String) -> Result<(), DevbrainError> {
    create_and_store_entry(config, EntryType::Error, error)?;
    println!("Error logged");

    Ok(())
}

fn create_and_store_entry(
    config: &Config,
    entry_type: EntryType,
    content: String,
) -> Result<(), DevbrainError> {
    let content = utils::normalize_input(&content)?;

    let entry = Entry {
        entry_type,
        content,
        project: utils::get_project_name(),
        timestamp: utils::get_timestamp(),
    };

    storage::add_entry(config, entry)?;

    Ok(())
}

fn search_entries(
    config: &Config,
    query: String,
    all: bool,
    limit: Option<usize>,
    offset: Option<usize>,
    entry_type: Option<String>,
) -> Result<(), DevbrainError> {
    let entry_type = validate_entry_type(entry_type)?;
    let db = storage::load_db(config)?;

    let entries = process_entries(
        &db.entries,
        Some(query),
        project_filter(all),
        entry_type,
        offset,
        limit,
    );

    if entries.is_empty() {
        println!("No matching entries found");
        return Ok(());
    }

    print_entries(&entries);

    Ok(())
}

fn show_timeline(
    config: &Config,
    all: bool,
    limit: Option<usize>,
    offset: Option<usize>,
    entry_type: Option<String>,
) -> Result<(), DevbrainError> {
    let entry_type = validate_entry_type(entry_type)?;
    let db = storage::load_db(config)?;

    let entries = process_entries(
        &db.entries,
        None,
        project_filter(all),
        entry_type,
        offset,
        limit,
    );

    if entries.is_empty() {
        println!("No entries found");
        return Ok(());
    }

    print_entries(&entries);

    Ok(())
}

fn show_last_entry(config: &Config, all: bool) -> Result<(), DevbrainError> {
    let db = storage::load_db(config)?;
    let entries = process_entries(&db.entries, None, project_filter(all), None, None, Some(1));

    if let Some(entry) = entries.first() {
        print_entry(entry);
    } else {
        println!("No entries found");
    }

    Ok(())
}

fn show_recent_entries_by_type(
    config: &Config,
    all: bool,
    limit: Option<usize>,
    entry_type: EntryType,
) -> Result<(), DevbrainError> {
    let db = storage::load_db(config)?;
    let entries = process_entries(
        &db.entries,
        None,
        project_filter(all),
        Some(entry_type),
        None,
        limit,
    );

    if entries.is_empty() {
        println!("No entries found");
        return Ok(());
    }

    for entry in entries {
        print_entry(entry);
    }

    Ok(())
}

fn clear_entries(config: &Config, force: bool) -> Result<(), DevbrainError> {
    if !force && !confirm_clear()? {
        println!("Operation cancelled");
        return Ok(());
    }

    let mut db = storage::load_db(config)?;
    db.entries.clear();
    storage::save_db(config, &db)?;
    println!("All entries cleared");

    Ok(())
}

fn export_entries(config: &Config, output: String) -> Result<(), DevbrainError> {
    let db = storage::load_db(config)?;
    let json = serde_json::to_string_pretty(&db)
        .map_err(|error| DevbrainError::ParseError(error.to_string()))?;
    fs::write(&output, json).map_err(|error| DevbrainError::IoError(error.to_string()))?;
    println!("Exported to {}", output);

    Ok(())
}

fn import_entries(config: &Config, input: String, merge: bool) -> Result<(), DevbrainError> {
    let contents =
        fs::read_to_string(&input).map_err(|error| DevbrainError::IoError(error.to_string()))?;
    let imported: Database = serde_json::from_str(&contents)
        .map_err(|error| DevbrainError::ParseError(error.to_string()))?;

    if merge {
        let mut current = storage::load_db(config)?;
        let mut added = 0;

        for entry in imported.entries {
            if !current.entries.contains(&entry) {
                current.entries.push(entry);
                added += 1;
            }
        }

        storage::save_db(config, &current)?;
        println!("Imported successfully ({} new entries added)", added);
    } else {
        storage::save_db(config, &imported)?;
        println!("Data imported successfully (replaced existing data)");
    }

    Ok(())
}

fn confirm_clear() -> Result<bool, DevbrainError> {
    print!("Are you sure you want to delete all entries? (y/n): ");
    io::stdout()
        .flush()
        .map_err(|error| DevbrainError::IoError(error.to_string()))?;

    let mut input = String::new();
    io::stdin()
        .read_line(&mut input)
        .map_err(|error| DevbrainError::IoError(error.to_string()))?;

    Ok(input.trim().eq_ignore_ascii_case("y"))
}

fn project_filter(all: bool) -> Option<String> {
    if all {
        None
    } else {
        Some(utils::get_project_name())
    }
}

fn validate_entry_type(entry_type: Option<String>) -> Result<Option<EntryType>, DevbrainError> {
    match entry_type {
        Some(entry_type) => Ok(Some(EntryType::from_str(&entry_type)?)),
        None => Ok(None),
    }
}

fn print_entries(entries: &[&Entry]) {
    for entry in entries {
        print_entry(entry);
    }
}