agentflow-cli 0.1.2

Git worktree workflow manager with AI agent task monitoring, Kanban board, and terminal UI
#![allow(clippy::significant_drop_tightening)]

use crate::ui::{self, colors, icons};
use anyhow::Result;
use crossterm::style::Stylize;
use flow_db::FeatureStore;

fn default_db_path() -> std::path::PathBuf {
    let config = flow_core::AgentConfig::new();
    config.features_db_path("default")
}

fn open_db(db_path: Option<&str>) -> Result<flow_db::Database> {
    let path = db_path.map_or_else(default_db_path, std::path::PathBuf::from);

    // Ensure parent directory exists
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent)?;
    }

    flow_db::Database::open(&path).map_err(|e| anyhow::anyhow!("{e}"))
}

pub fn list(db_path: Option<&str>) -> Result<()> {
    let db = open_db(db_path)?;
    let (features, stats) = {
        let conn = db.writer().lock().unwrap();
        let features = FeatureStore::get_all(&conn).map_err(|e| anyhow::anyhow!("{e}"))?;
        let stats = FeatureStore::get_stats(&conn).map_err(|e| anyhow::anyhow!("{e}"))?;
        (features, stats)
    };

    if features.is_empty() {
        ui::print_empty("No features found. Use 'flow features add' to create one.");
        return Ok(());
    }

    ui::print_header("  Features  ");

    for feature in &features {
        let status_icon = if feature.passes {
            icons::CHECK.with(colors::GREEN)
        } else if feature.in_progress {
            "".with(colors::CYAN)
        } else {
            icons::DOT.with(colors::YELLOW)
        };

        let deps = if feature.dependencies.is_empty() {
            String::new()
        } else {
            format!(
                " [deps: {}]",
                feature
                    .dependencies
                    .iter()
                    .map(std::string::ToString::to_string)
                    .collect::<Vec<_>>()
                    .join(",")
            )
        };

        println!(
            "  {} {} #{} {} {}{}",
            status_icon,
            format!("[p{}]", feature.priority).with(colors::DIM),
            feature.id.to_string().with(colors::CYAN),
            feature.name.as_str().bold().with(colors::WHITE),
            feature.category.as_str().with(colors::PURPLE),
            deps.with(colors::DIM),
        );
    }

    println!(
        "\n  {} total, {} passing, {} in progress, {} blocked",
        stats.total.to_string().with(colors::WHITE),
        stats.passing.to_string().with(colors::GREEN),
        stats.in_progress.to_string().with(colors::CYAN),
        stats.blocked.to_string().with(colors::RED),
    );

    Ok(())
}

pub fn add(db_path: Option<&str>, name: &str, description: &str, category: &str) -> Result<()> {
    let db = open_db(db_path)?;
    let feature = {
        let conn = db.writer().lock().unwrap();
        let input = flow_core::CreateFeatureInput {
            name: name.to_string(),
            description: description.to_string(),
            priority: None,
            category: category.to_string(),
            steps: vec![],
            dependencies: vec![],
        };
        FeatureStore::create(&conn, &input).map_err(|e| anyhow::anyhow!("{e}"))?
    };
    ui::print_success(&format!(
        "Created feature #{}: {}",
        feature.id, feature.name
    ));
    Ok(())
}

pub fn ready(db_path: Option<&str>) -> Result<()> {
    let db = open_db(db_path)?;
    let features = {
        let conn = db.writer().lock().unwrap();
        FeatureStore::get_ready(&conn).map_err(|e| anyhow::anyhow!("{e}"))?
    };

    if features.is_empty() {
        ui::print_empty("No features ready to work on.");
        return Ok(());
    }

    ui::print_section(icons::ARROW, "Ready Features");
    for feature in &features {
        println!(
            "  {} #{} {}",
            icons::DOT.with(colors::GREEN),
            feature.id.to_string().with(colors::CYAN),
            feature.name.as_str().with(colors::WHITE),
        );
    }

    Ok(())
}

pub fn claim(db_path: Option<&str>, id: i64) -> Result<()> {
    let db = open_db(db_path)?;
    let feature = {
        let conn = db.writer().lock().unwrap();
        FeatureStore::claim_and_get(&conn, id).map_err(|e| anyhow::anyhow!("{e}"))?
    };
    ui::print_success(&format!(
        "Claimed feature #{}: {}",
        feature.id, feature.name
    ));
    Ok(())
}

pub fn pass(db_path: Option<&str>, id: i64) -> Result<()> {
    let db = open_db(db_path)?;
    {
        let conn = db.writer().lock().unwrap();
        FeatureStore::mark_passing(&conn, id).map_err(|e| anyhow::anyhow!("{e}"))?;
    }
    ui::print_success(&format!("Feature #{id} marked as passing"));
    Ok(())
}

pub fn fail(db_path: Option<&str>, id: i64) -> Result<()> {
    let db = open_db(db_path)?;
    {
        let conn = db.writer().lock().unwrap();
        FeatureStore::mark_failing(&conn, id).map_err(|e| anyhow::anyhow!("{e}"))?;
    }
    ui::print_warning(&format!("Feature #{id} marked as failing"));
    Ok(())
}

pub fn graph(db_path: Option<&str>) -> Result<()> {
    let db = open_db(db_path)?;
    let sorted = {
        let conn = db.writer().lock().unwrap();
        let features = FeatureStore::get_all(&conn).map_err(|e| anyhow::anyhow!("{e}"))?;
        flow_resolver::topological_sort(&features).map_err(|e| anyhow::anyhow!("{e}"))?
    };

    ui::print_section(icons::ARROW, "Dependency Graph (topological order)");
    for feature in &sorted {
        let deps = if feature.dependencies.is_empty() {
            String::new()
        } else {
            format!(
                " <- [{}]",
                feature
                    .dependencies
                    .iter()
                    .map(|d| format!("#{d}"))
                    .collect::<Vec<_>>()
                    .join(", ")
            )
        };

        let status = if feature.passes {
            icons::CHECK.with(colors::GREEN)
        } else if feature.in_progress {
            "".with(colors::CYAN)
        } else {
            icons::DOT.with(colors::YELLOW)
        };

        println!(
            "  {} #{} {}{}",
            status,
            feature.id.to_string().with(colors::CYAN),
            feature.name.as_str().with(colors::WHITE),
            deps.with(colors::DIM),
        );
    }

    Ok(())
}