backburner 0.1.2

Private project memory for work worth coming back to.
mod cli;
mod db;
mod models;
mod output;
mod repository;
mod root;
mod settings;

use std::env;

use anyhow::Result;
use clap::{CommandFactory, Parser};

use crate::cli::{Cli, Command, SessionCommand};
use crate::models::{TaskStatus, parse_plan};
use crate::repository::{CreateTask, Repository, parse_file_ref};

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

fn run() -> Result<()> {
    let cli = Cli::parse_from(env::args());
    match cli.command {
        Command::Help(args) => {
            if args.usage {
                print_advanced_usage();
                Ok(())
            } else {
                Cli::command().print_help()?;
                println!();
                Ok(())
            }
        }
        Command::Init(args) => {
            let cwd = env::current_dir()?;
            let root = root::find_git_root(&cwd)?;
            let db_path = root::init_project(&root)?;
            settings::init(&root)?;
            let conn = db::open(&db_path)?;
            drop(conn);
            if args.json {
                output::json(&serde_json::json!({
                    "root": root,
                    "database": db_path,
                }))
            } else {
                println!("Initialized Backburner at {}.", db_path.display());
                Ok(())
            }
        }
        other => run_project_command(other),
    }
}

fn run_project_command(command: Command) -> Result<()> {
    let cwd = env::current_dir()?;
    let root = root::find_git_root(&cwd)?;
    let db_path = root::ensure_initialized(&root)?;
    let conn = db::open(&db_path)?;
    let mut repository = Repository::new(conn);
    settings::rollover_if_needed(&root, &mut repository)?;

    match command {
        Command::Add(args) => {
            let planned_date_key = args.plan.as_deref().map(parse_plan).transpose()?.flatten();
            let active_session = settings::active_session(&root)?;
            let details = repository.create(CreateTask {
                title: args.title,
                status: if args.backburner {
                    TaskStatus::Backburner
                } else {
                    TaskStatus::Today
                },
                planned_date_key,
                session_key: active_session,
                source: args.source.into(),
                notes: args.notes,
                files: args
                    .files
                    .iter()
                    .map(|value| parse_file_ref(value))
                    .collect(),
                commands: args.commands,
            })?;
            if args.json {
                output::json(&details)
            } else {
                output::show(&details);
                Ok(())
            }
        }
        Command::Today(args) => {
            let active_session = settings::active_session(&root)?;
            repository.promote_due_for(active_session.as_deref())?;
            let tasks =
                repository.list_for_session(TaskStatus::Today, active_session.as_deref())?;
            if args.json {
                output::json(&tasks)
            } else {
                output::list("Today", &tasks);
                Ok(())
            }
        }
        Command::Backburner(args) => {
            let active_session = settings::active_session(&root)?;
            let tasks =
                repository.list_for_session(TaskStatus::Backburner, active_session.as_deref())?;
            if args.json {
                output::json(&tasks)
            } else {
                output::list("Backburner", &tasks);
                Ok(())
            }
        }
        Command::Archive(args) => {
            let tasks = repository.list(TaskStatus::Archived)?;
            if args.json {
                output::json(&tasks)
            } else {
                output::list("Archive", &tasks);
                Ok(())
            }
        }
        Command::Show(args) => {
            let details = repository.details(args.id)?;
            if args.json {
                output::json(&details)
            } else {
                output::show(&details);
                Ok(())
            }
        }
        Command::Done(args) => {
            repository.set_completed(args.id, true)?;
            println!("Marked #{} done.", args.id);
            Ok(())
        }
        Command::Undone(args) => {
            let status = repository.mark_undone(args.id)?;
            println!("Marked #{} undone in {}.", args.id, status);
            Ok(())
        }
        Command::Move(args) => {
            let status = args.status.into();
            repository.move_to(args.id, status)?;
            output::moved(args.id, status);
            Ok(())
        }
        Command::Plan(args) => {
            repository.plan(args.id, parse_plan(&args.plan)?)?;
            println!("Updated plan for #{}.", args.id);
            Ok(())
        }
        Command::Note(args) => {
            repository.add_note(args.id, &args.body, args.source.into())?;
            println!("Added note to #{}.", args.id);
            Ok(())
        }
        Command::Delete(args) => {
            repository.delete(args.id)?;
            println!("Deleted #{}.", args.id);
            Ok(())
        }
        Command::FinishSession(args) => {
            let active_session = settings::active_session(&root)?;
            let session = args.session.as_deref().or(active_session.as_deref());
            let result = repository.finish_session_for(session)?;
            if args.json {
                output::json(&result)
            } else {
                output::finish_session(&result);
                Ok(())
            }
        }
        Command::Context(args) => {
            let active_session = settings::active_session(&root)?;
            let context = repository.context_for(active_session.as_deref())?;
            if args.json {
                output::json(&context)
            } else {
                output::context(&context);
                Ok(())
            }
        }
        Command::Session(args) => match args.command {
            SessionCommand::Start(args) => {
                let name = settings::start_session(&root, &args.name)?;
                println!("Started session {name}.");
                Ok(())
            }
            SessionCommand::End => {
                match settings::end_session(&root)? {
                    Some(name) => println!("Ended session {name}."),
                    None => println!("No active session."),
                }
                Ok(())
            }
        },
        Command::Init(_) | Command::Help(_) => unreachable!(),
    }
}

fn print_advanced_usage() {
    println!(
        r#"Backburner advanced usage

Start a repo:
  bb init

Capture active work:
  bb add "Investigate flaky login redirect"
  bb add "Fix failing auth test"
  bb add "Park this for later" --backburner

Attach restart evidence:
  bb add "Fix auth redirect regression" \
    --file src/auth.rs:42 \
    --cmd "cargo test auth" \
    --note "Fails after token expiry." \
    --source agent

Review and move work:
  bb today
  bb backburner
  bb show 1
  bb move 1 today
  bb plan 1 tomorrow

Close a session:
  bb done 1
  bb finish-session
  bb finish-session refactor-auth
  bb session end

Context:
  bb context --json"#
    );
}