lmrc-cli 0.3.16

CLI tool for scaffolding LMRC Stack infrastructure projects
Documentation
use colored::Colorize;
use std::process::{Command, Stdio};
use std::thread;
use std::time::Duration;

use crate::error::{CliError, Result};

pub async fn start() -> Result<()> {
    println!("\n{}\n", "Starting local development environment...".green().bold());

    // Step 1: Check if in project directory
    if !std::path::Path::new("lmrc.toml").exists() {
        return Err(CliError::Config(
            "lmrc.toml not found. Run this command from project root.".to_string(),
        ));
    }

    // Step 2: Check if docker-compose exists
    if !std::path::Path::new("docker/docker-compose.yml").exists() {
        println!("{}", "⚠ docker-compose.yml not found. Skipping infrastructure services.".yellow());
    } else {
        // Start docker-compose
        println!("{}", "  Starting infrastructure services...".cyan());
        start_docker_compose()?;

        // Wait for services to be healthy
        println!("\n{}", "  Waiting for services to be ready...".cyan());
        wait_for_services()?;
    }

    // Step 3: Build workspace
    println!("\n{}", "  Building workspace...".cyan());
    build_workspace()?;

    // Step 4: Show success message
    print_success_message();

    Ok(())
}

pub async fn stop() -> Result<()> {
    println!("\n{}\n", "Stopping local development environment...".yellow().bold());

    if std::path::Path::new("docker/docker-compose.yml").exists() {
        println!("{}", "  Stopping infrastructure services...".cyan());
        stop_docker_compose()?;
        println!("{}", "✓ Services stopped".green());
    }

    Ok(())
}

pub async fn status() -> Result<()> {
    println!("\n{}\n", "Development Environment Status".cyan().bold());

    if std::path::Path::new("docker/docker-compose.yml").exists() {
        println!("{}", "Infrastructure Services:".cyan());
        show_docker_status()?;
    }

    Ok(())
}

pub async fn logs(service: Option<String>) -> Result<()> {
    if !std::path::Path::new("docker/docker-compose.yml").exists() {
        return Err(CliError::Config("docker-compose.yml not found".to_string()));
    }

    let mut cmd = Command::new("docker");
    cmd.current_dir("docker").arg("compose").arg("logs");

    if let Some(svc) = service {
        cmd.arg(svc);
    } else {
        cmd.arg("--tail=50");
    }

    cmd.arg("-f");
    cmd.status()?;

    Ok(())
}

fn start_docker_compose() -> Result<()> {
    let status = Command::new("docker")
        .current_dir("docker")
        .args(&["compose", "up", "-d"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()?;

    if !status.success() {
        return Err(CliError::Command("Failed to start docker-compose".to_string()));
    }

    Ok(())
}

fn stop_docker_compose() -> Result<()> {
    let status = Command::new("docker")
        .current_dir("docker")
        .args(&["compose", "down"])
        .stdout(Stdio::null())
        .status()?;

    if !status.success() {
        return Err(CliError::Command("Failed to stop docker-compose".to_string()));
    }

    Ok(())
}

fn show_docker_status() -> Result<()> {
    Command::new("docker")
        .current_dir("docker")
        .args(&["compose", "ps"])
        .status()?;

    Ok(())
}

fn wait_for_services() -> Result<()> {
    let services = vec![
        ("PostgreSQL", "postgres", 5432, "pg_isready"),
        ("RabbitMQ", "rabbitmq", 5672, "amqp"),
        ("Redis", "redis", 6379, "redis-cli"),
        ("Vault", "vault", 8200, "http"),
    ];

    for (name, container, port, check_type) in services {
        if is_service_in_compose(container)? {
            print!("    {} {}...", "Checking".cyan(), name);
            std::io::Write::flush(&mut std::io::stdout()).unwrap();

            if wait_for_service(port, check_type, 30) {
                println!(" {} (localhost:{})", "Ready".green(), port);
            } else {
                println!(" {} (timeout)", "Failed".yellow());
            }
        }
    }

    Ok(())
}

fn is_service_in_compose(service_name: &str) -> Result<bool> {
    let output = Command::new("docker")
        .current_dir("docker")
        .args(&["compose", "config", "--services"])
        .output()?;

    let services = String::from_utf8_lossy(&output.stdout);
    Ok(services.lines().any(|s| s.contains(service_name)))
}

fn wait_for_service(port: u16, check_type: &str, timeout_secs: u64) -> bool {
    let start = std::time::Instant::now();

    while start.elapsed().as_secs() < timeout_secs {
        match check_type {
            "pg_isready" => {
                if check_postgres(port) {
                    return true;
                }
            }
            "redis-cli" => {
                if check_redis(port) {
                    return true;
                }
            }
            "http" => {
                if check_http(port) {
                    return true;
                }
            }
            "amqp" => {
                if check_port(port) {
                    return true;
                }
            }
            _ => {
                if check_port(port) {
                    return true;
                }
            }
        }

        thread::sleep(Duration::from_millis(500));
    }

    false
}

fn check_postgres(port: u16) -> bool {
    Command::new("pg_isready")
        .args(&["-h", "localhost", "-p", &port.to_string()])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

fn check_redis(port: u16) -> bool {
    Command::new("redis-cli")
        .args(&["-p", &port.to_string(), "ping"])
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

fn check_http(port: u16) -> bool {
    use std::net::TcpStream;
    TcpStream::connect(format!("localhost:{}", port)).is_ok()
}

fn check_port(port: u16) -> bool {
    use std::net::TcpStream;
    TcpStream::connect(format!("localhost:{}", port)).is_ok()
}

fn build_workspace() -> Result<()> {
    let status = Command::new("cargo")
        .arg("build")
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status()?;

    if !status.success() {
        return Err(CliError::Command("Failed to build workspace".to_string()));
    }

    println!("{}", "  ✓ Workspace built successfully".green());

    Ok(())
}

fn print_success_message() {
    println!("\n{}\n", "✓ Development environment ready!".green().bold());

    println!("{}", "Services:".cyan());

    // Check what services are actually running and show their URLs
    if std::path::Path::new("docker/docker-compose.yml").exists() {
        if let Ok(output) = Command::new("docker")
            .current_dir("docker")
            .args(&["compose", "ps", "--format", "json"])
            .output()
        {
            if output.status.success() {
                // Try to parse and show running services
                println!("  PostgreSQL:       localhost:5432");
                println!("  RabbitMQ AMQP:    localhost:5672");
                println!("  RabbitMQ UI:      http://localhost:15672 (guest/guest)");
                println!("  Redis:            localhost:6379");
                println!("  Vault:            http://localhost:8200 (token: root)");
            }
        }
    }

    println!("\n{}", "Commands:".cyan());
    println!("  Run application:  cargo run -p <app-name>");
    println!("  Run tests:        cargo test");
    println!("  View logs:        lmrc dev logs");
    println!("  Stop services:    lmrc dev stop");

    println!();
}