mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Models command handler
//!
//! Orchestrates AI model management operations using ModelService.

use crate::commands::ModelsCommand;
use crate::context::CliContext;
use crate::paths;
use crate::services::ModelService;
use anyhow::{Context, Result};
use console::style;
use dialoguer::Confirm;
use indicatif::{ProgressBar, ProgressStyle};
use std::path::PathBuf;

/// Handle models command
///
/// Manages AI models from HuggingFace Hub.
///
/// # Arguments
///
/// * `_ctx` - CLI execution context (unused for now)
/// * `cmd` - Models subcommand
pub async fn handle_models(_ctx: &mut CliContext, cmd: &ModelsCommand) -> Result<()> {
    println!();
    println!("{}", style("🤖 AI Model Management").bold());
    println!("{}", style("".repeat(50)).dim());
    println!();

    // Determine models directory
    // Use ./models in current directory, or project root if in a project
    let models_dir = if let Ok(project) = _ctx.project() {
        project.root().join(paths::project::MODELS_DIR)
    } else {
        PathBuf::from(paths::project::MODELS_DIR)
    };

    let service = ModelService::with_models_dir(models_dir).context("Failed to initialize model service")?;

    match cmd {
        ModelsCommand::List { verbose, recommended } => {
            handle_list(&service, *verbose, *recommended)?;
        }
        ModelsCommand::Pull {
            name,
            all,
            repo,
            file,
            custom_name,
        } => {
            handle_pull(&service, name.as_deref(), *all, repo, file, custom_name).await?;
        }
        ModelsCommand::Installed { verbose } => {
            handle_installed(&service, *verbose).await?;
        }
        ModelsCommand::Remove { name, yes } => {
            handle_remove(&service, name, *yes).await?;
        }
        ModelsCommand::Info { name } => {
            handle_info(&service, name).await?;
        }
    }

    Ok(())
}

/// Handle list subcommand
fn handle_list(service: &ModelService, verbose: bool, _recommended: bool) -> Result<()> {
    let catalog = service.list_catalog()?;

    if catalog.is_empty() {
        println!("No models found in catalog.");
        return Ok(());
    }

    println!("{} Models available in catalog:", style("📦").bold());
    println!();

    for model in &catalog {
        if verbose {
            println!("  {} {}", style("").cyan(), style(&model.name).bold());
            println!("    Description: {}", model.description);
            println!("    Task:        {}", model.task);
            println!("    Repository:  {}", model.repo);
            println!("    File:        {}", model.filename);
            if let Some(preset) = &model.preprocessing_preset {
                println!("    Preset:      {}", preset);
            }
            println!();
        } else {
            println!(
                "  {} {} - {}",
                style("").cyan(),
                style(&model.name).bold(),
                model.description
            );
        }
    }

    if !verbose {
        println!();
        println!("{}", style("Use --verbose for detailed information").dim());
    }

    println!();
    println!("To download a model: {}", style("mecha10 models pull <name>").cyan());
    println!();

    Ok(())
}

/// Handle pull subcommand
async fn handle_pull(
    service: &ModelService,
    name: Option<&str>,
    all: bool,
    repo: &Option<String>,
    file: &Option<String>,
    custom_name: &Option<String>,
) -> Result<()> {
    let pb = ProgressBar::new_spinner();
    pb.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.green} {msg}")
            .unwrap(),
    );
    pb.enable_steady_tick(std::time::Duration::from_millis(100));

    if all {
        // Download all models from catalog
        println!("Downloading all models from catalog...");
        println!();

        let catalog = service.list_catalog()?;
        let mut paths = Vec::new();

        for entry in catalog {
            pb.set_message(format!("Downloading {}", entry.name));
            match service.pull(&entry.name, Some(&pb)).await {
                Ok(path) => paths.push(path),
                Err(e) => {
                    eprintln!("⚠️  Failed to download {}: {}", entry.name, e);
                }
            }
        }

        pb.finish_and_clear();

        println!("{} Downloaded {} models:", style("").green().bold(), paths.len());
        for path in &paths {
            println!("{}", path.display());
        }
        println!();
    } else if let (Some(repo), Some(file), Some(custom_name)) = (repo, file, custom_name) {
        // Download custom model from HF repo
        pb.set_message(format!("Downloading {} from {}", custom_name, repo));

        let path = service.pull_from_repo(repo, file, custom_name, Some(&pb)).await?;

        pb.finish_and_clear();

        println!(
            "{} Downloaded custom model '{}'",
            style("").green().bold(),
            custom_name
        );
        println!("  Location: {}", path.display());
        println!();
    } else if let Some(name) = name {
        // Download specific model from catalog
        pb.set_message(format!("Downloading {}", name));

        let path = service.pull(name, Some(&pb)).await?;

        pb.finish_and_clear();

        println!("{} Downloaded '{}'", style("").green().bold(), name);
        println!("  Location: {}", path.display());
        println!();
    } else {
        pb.finish_and_clear();
        return Err(anyhow::anyhow!(
            "Please specify a model name, --all, or --repo with --file and --custom-name"
        ));
    }

    Ok(())
}

/// Handle installed subcommand
async fn handle_installed(service: &ModelService, verbose: bool) -> Result<()> {
    let installed = service.list_installed().await?;

    if installed.is_empty() {
        println!("No models installed.");
        println!();
        println!("To download models: {}", style("mecha10 models pull <name>").cyan());
        println!();
        return Ok(());
    }

    let count_str = if installed.len() == 1 {
        "1 model".to_string()
    } else {
        format!("{} models", installed.len())
    };
    println!("{} {} installed:", style("📦").bold(), count_str);
    println!();

    for model in &installed {
        if verbose {
            println!("  {} {}", style("").cyan(), style(&model.name).bold());
            println!("    Path: {}", model.path.display());
            println!("    Size: {} bytes", model.size);

            if let Some(entry) = &model.catalog_entry {
                println!("    From catalog: {}", entry.description);
                println!("    Task: {}", entry.task);
            } else {
                println!("    {} Custom model", style("").dim());
            }
            println!();
        } else {
            let size_mb = model.size as f64 / 1_048_576.0;
            let catalog_info = if let Some(entry) = &model.catalog_entry {
                format!(" ({})", entry.task)
            } else {
                " (custom)".to_string()
            };

            println!(
                "  {} {} - {:.2} MB{}",
                style("").cyan(),
                style(&model.name).bold(),
                size_mb,
                style(catalog_info).dim()
            );
        }
    }

    if !verbose {
        println!();
        println!("{}", style("Use --verbose for detailed information").dim());
    }

    println!();

    Ok(())
}

/// Handle remove subcommand
async fn handle_remove(service: &ModelService, name: &str, skip_confirm: bool) -> Result<()> {
    // Check if model exists
    let installed = service.list_installed().await?;
    let model = installed
        .iter()
        .find(|m| m.name == name)
        .ok_or_else(|| anyhow::anyhow!("Model '{}' is not installed", name))?;

    // Confirm removal
    if !skip_confirm {
        let confirm = Confirm::new()
            .with_prompt(format!("Remove model '{}'? ({} bytes)", name, model.size))
            .default(false)
            .interact()?;

        if !confirm {
            println!("Cancelled.");
            return Ok(());
        }
    }

    // Remove model
    service.remove(name).await?;

    println!("{} Removed model '{}'", style("").green().bold(), name);
    println!();

    Ok(())
}

/// Handle info subcommand
async fn handle_info(service: &ModelService, name: &str) -> Result<()> {
    let info = service.info(name).await?;

    println!("{} Model: {}", style("").blue().bold(), style(name).bold());
    println!();

    // Catalog information
    if let Some(entry) = &info.catalog_entry {
        println!("{}", style("Catalog Information:").underlined());
        println!("  Description:  {}", entry.description);
        println!("  Task:         {}", entry.task);
        println!("  Repository:   {}", entry.repo);
        println!("  File:         {}", entry.filename);
        if let Some(preset) = &entry.preprocessing_preset {
            println!("  Preset:       {}", preset);
        }
        println!();
    } else {
        println!("{}", style("Not found in catalog (custom model)").dim());
        println!();
    }

    // Installation information
    if let Some(installed) = &info.installed_info {
        println!("{}", style("Installation:").underlined());
        println!("  Status:   {}", style("Installed").green());
        println!("  Path:     {}", installed.path.display());
        println!(
            "  Size:     {} bytes ({:.2} MB)",
            installed.size,
            installed.size as f64 / 1_048_576.0
        );
        println!();
    } else {
        println!("{}", style("Installation:").underlined());
        println!("  Status:   {}", style("Not installed").yellow());
        println!();
        println!(
            "  To install: {}",
            style(format!("mecha10 models pull {}", name)).cyan()
        );
        println!();
    }

    Ok(())
}