kegani-cli 0.1.4

CLI tool for Kegani framework
Documentation
//! `keg info` command — Show detailed project information

use anyhow::{Context, Result};
use console::{style, Emoji};
use std::collections::HashMap;

/// Show detailed project information
pub fn info(json: bool) -> Result<()> {
    println!();

    if json {
        print_info_json()?;
    } else {
        print_info_pretty()?;
    }

    Ok(())
}

fn print_info_pretty() -> Result<()> {
    println!("{} {}", Emoji("ℹ️", ""), style("Project Information").bold());
    println!("{}", style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim());
    println!();

    let project_dir = std::env::current_dir()?;
    let project_name = project_dir
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    // Project name
    println!("  {} {}", style("Project:").dim(), style(&project_name).cyan().bold());

    // Cargo.toml info
    let cargo_path = project_dir.join("Cargo.toml");
    let cargo_content = if cargo_path.exists() {
        std::fs::read_to_string(&cargo_path).ok()
    } else {
        None
    };

    if let Some(ref content) = cargo_content {
        if let Some(version) = extract_from_cargo(content, "version") {
            println!("  {} {}", style("Version:").dim(), style(&version).cyan());
        }
        if let Some(name) = extract_from_cargo(content, "name") {
            println!("  {} {}", style("Package:").dim(), style(&name).cyan());
        }
    }

    // Project structure
    println!();
    println!("  {} {}", style("Structure:").dim(), style("✓ Found").green());

    let dirs = [
        ("src/", "Source"),
        ("internal/", "Internal"),
        ("migrations/", "Migrations"),
        ("manifest/config/", "Config"),
        ("tests/", "Tests"),
    ];

    for (dir, label) in dirs {
        let path = project_dir.join(dir);
        let status = if path.exists() {
            style("").green()
        } else {
            style("").red()
        };
        println!("    {} {:20} {}", status, label, style(dir).dim());
    }

    // Dependencies
    println!();
    println!("  {} {}", style("Dependencies:").dim(), style("Checking...").dim());

    let rust_version = get_rust_version();
    let cargo_version = get_cargo_version();
    println!("    {} {}", style("Rust:").dim(), style(&rust_version).cyan());
    println!("    {} {}", style("Cargo:").dim(), style(&cargo_version).cyan());

    // Kegani version
    if let Some(ref content) = cargo_content {
        if let Some(v) = get_kegani_version(content) {
            println!("    {} {}", style("Kegani:").dim(), style(&v).cyan());
        }
    }

    // Database
    println!();
    let db_url = std::env::var("DATABASE_URL").ok();
    if let Some(url) = &db_url {
        // Mask password
        let masked = mask_database_url(url);
        println!("  {} {}", style("Database:").dim(), style(&masked).cyan());
    } else {
        println!("  {} {}", style("Database:").dim(), style("Not configured").yellow());
    }

    // Redis
    let redis_url = std::env::var("REDIS_URL").ok();
    if let Some(url) = &redis_url {
        println!("  {} {}", style("Redis:").dim(), style(url).cyan());
    } else {
        println!("  {} {}", style("Redis:").dim(), style("Not configured").yellow());
    }

    println!();
    println!("{} {}", Emoji("", ""), style("Done").dim());
    println!();

    Ok(())
}

fn print_info_json() -> Result<()> {
    let project_dir = std::env::current_dir()?;
    let project_name = project_dir
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "unknown".to_string());

    let cargo_content = std::fs::read_to_string(project_dir.join("Cargo.toml")).ok();

    let mut info = HashMap::new();
    info.insert("project", project_name.clone());
    info.insert("directory", project_dir.to_string_lossy().to_string());

    if let Some(ref cargo) = cargo_content {
        if let Some(version) = extract_from_cargo(cargo, "version") {
            info.insert("version", version);
        }
        if let Some(name) = extract_from_cargo(cargo, "name") {
            info.insert("package", name);
        }
    }

    info.insert("rust_version", get_rust_version());
    info.insert("cargo_version", get_cargo_version());

    if let Some(ref cargo) = cargo_content {
        if let Some(v) = get_kegani_version(cargo) {
            info.insert("kegani_version", v);
        }
    }

    if let Ok(db_url) = std::env::var("DATABASE_URL") {
        info.insert("database_url", mask_database_url(&db_url));
    }

    if let Ok(redis_url) = std::env::var("REDIS_URL") {
        info.insert("redis_url", redis_url);
    }

    let json = serde_json::to_string_pretty(&info).context("Failed to serialize to JSON")?;
    println!("{}", json);

    Ok(())
}

fn extract_from_cargo(content: &str, key: &str) -> Option<String> {
    for line in content.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with(key) && trimmed.contains('=') {
            if let Some(eq_pos) = trimmed.find('=') {
                let value = trimmed[eq_pos + 1..].trim().trim_matches('"').trim_matches('\'');
                return Some(value.to_string());
            }
        }
    }
    None
}

fn get_rust_version() -> String {
    std::process::Command::new("rustc")
        .arg("--version")
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "not found".to_string())
}

fn get_cargo_version() -> String {
    std::process::Command::new("cargo")
        .arg("--version")
        .output()
        .ok()
        .and_then(|o| String::from_utf8(o.stdout).ok())
        .map(|s| s.trim().to_string())
        .unwrap_or_else(|| "not found".to_string())
}

fn get_kegani_version(cargo_content: &str) -> Option<String> {
    extract_from_cargo(cargo_content, "kegani")
}

fn mask_database_url(url: &str) -> String {
    // Replace password with ***
    if let Some(at_pos) = url.find('@') {
        if let Some(colon_pos) = url[..at_pos].find(':') {
            let before = &url[..colon_pos];
            let after = &url[at_pos..];
            return format!("{}:***{}", before, after);
        }
    }
    url.to_string()
}