revoke-cli 0.3.0

Command-line interface for managing Revoke microservices infrastructure
use anyhow::{Result, Context};
use colored::Colorize;
use std::process::{Command, Stdio};
use std::collections::HashMap;
use tokio::process::Command as TokioCommand;
use crate::config::Config;
use crate::utils::{print_success, print_error, print_info};

pub async fn handle_dev(
    services: Vec<String>,
    all: bool,
    config: &Config,
) -> Result<()> {
    println!("{}", "Revoke Development Environment".bold());
    println!("{}", "=".repeat(50));
    
    // Check Docker
    check_docker().await?;
    
    // Start infrastructure
    print_info("Starting infrastructure services...");
    start_infrastructure().await?;
    
    // Wait for infrastructure
    tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
    
    // Get services to start
    let services_to_start = if all {
        discover_services()?
    } else if services.is_empty() {
        print_error("No services specified. Use --all or specify services with -s");
        return Ok(());
    } else {
        services
    };
    
    if services_to_start.is_empty() {
        print_info("No services found to start");
        return Ok(());
    }
    
    println!("\n{}", "Starting services:".bold());
    for service in &services_to_start {
        println!("  - {}", service.cyan());
    }
    
    // Start services
    let mut handles = vec![];
    
    for service in services_to_start {
        let service_clone = service.clone();
        let handle = tokio::spawn(async move {
            start_service(&service_clone).await
        });
        handles.push((service, handle));
    }
    
    println!("\n{}", "Services are starting...".dimmed());
    println!("{}", "Press Ctrl+C to stop all services".dimmed());
    
    // Wait for Ctrl+C
    tokio::signal::ctrl_c().await?;
    
    println!("\n{}", "Stopping services...".yellow());
    
    // Cancel all tasks
    for (service, handle) in handles {
        handle.abort();
        println!("  - Stopped {}", service.cyan());
    }
    
    print_success("Development environment stopped");
    
    Ok(())
}

async fn check_docker() -> Result<()> {
    let output = TokioCommand::new("docker")
        .arg("version")
        .output()
        .await;
    
    match output {
        Ok(output) if output.status.success() => {
            print_success("Docker is installed and running");
            Ok(())
        }
        _ => {
            print_error("Docker is not installed or not running");
            Err(anyhow::anyhow!("Docker is required for development environment"))
        }
    }
}

async fn start_infrastructure() -> Result<()> {
    // Check if docker-compose.yml exists
    if !std::path::Path::new("docker-compose.yml").exists() {
        print_info("No docker-compose.yml found, creating default infrastructure...");
        create_default_docker_compose().await?;
    }
    
    // Start docker-compose
    let output = TokioCommand::new("docker-compose")
        .args(&["up", "-d", "consul"])
        .output()
        .await?;
    
    if !output.status.success() {
        print_error("Failed to start infrastructure");
        return Err(anyhow::anyhow!("docker-compose failed"));
    }
    
    print_success("Infrastructure started successfully");
    
    // Wait for Consul to be ready
    print_info("Waiting for Consul to be ready...");
    
    for i in 0..30 {
        let result = reqwest::get("http://localhost:8500/v1/status/leader").await;
        
        if result.is_ok() {
            print_success("Consul is ready");
            break;
        }
        
        if i == 29 {
            print_error("Consul failed to start");
            return Err(anyhow::anyhow!("Consul not ready after 30 seconds"));
        }
        
        tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
    }
    
    Ok(())
}

async fn create_default_docker_compose() -> Result<()> {
    let content = r#"version: '3.8'

services:
  consul:
    image: consul:latest
    ports:
      - "8500:8500"
    command: agent -dev -ui -client=0.0.0.0
    environment:
      - CONSUL_BIND_INTERFACE=eth0

  # Add your services here
"#;
    
    tokio::fs::write("docker-compose.yml", content).await?;
    print_success("Created default docker-compose.yml");
    
    Ok(())
}

fn discover_services() -> Result<Vec<String>> {
    let mut services = Vec::new();
    
    // Check for Cargo workspace
    if let Ok(content) = std::fs::read_to_string("Cargo.toml") {
        if content.contains("[workspace]") {
            // Parse workspace members
            for line in content.lines() {
                if line.trim().starts_with('"') && line.contains("service") {
                    if let Some(service) = line.split('"').nth(1) {
                        services.push(service.to_string());
                    }
                }
            }
        } else {
            // Single service project
            services.push("app".to_string());
        }
    }
    
    Ok(services)
}

async fn start_service(service: &str) -> Result<()> {
    print_info(&format!("Starting {}...", service));
    
    let service_path = if service == "app" {
        ".".to_string()
    } else {
        service.to_string()
    };
    
    // Set environment variables
    let mut envs = HashMap::new();
    envs.insert("CONSUL_ADDR", "http://localhost:8500");
    envs.insert("RUST_LOG", "info");
    
    // Run cargo run
    let mut child = TokioCommand::new("cargo")
        .current_dir(&service_path)
        .args(&["run"])
        .envs(&envs)
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .spawn()
        .context(format!("Failed to start {}", service))?;
    
    print_success(&format!("{} started", service));
    
    // Wait for the process
    let status = child.wait().await?;
    
    if !status.success() {
        print_error(&format!("{} exited with error", service));
    }
    
    Ok(())
}