oph 0.1.0

OpenHealth CLI - Universal development tool for the OpenHealth team
Documentation
use crate::config::{project_root, Environment, DOCKER_PROD_DIR};
use crate::docker;
use crate::ui;
use anyhow::Result;
use clap::Subcommand;
use std::process::Command;

#[derive(Subcommand)]
pub enum ProdCommand {
    /// Initial setup for production environment
    Setup,
    /// Initialize Let's Encrypt SSL certificates
    Ssl,
    /// Start all production services
    Up,
    /// Stop all production services
    Down,
    /// Restart all production services
    Restart,
    /// Show status of production services
    Status,
    /// Show logs from production services (follow mode)
    Logs {
        /// Service name (optional, shows all if not specified)
        service: Option<String>,
    },
    /// Open shell in a service container
    Shell {
        /// Service name (e.g., database, nginx)
        service: String,
    },
    /// Reload nginx configuration without restart
    ReloadNginx,
    /// Force SSL certificate renewal
    RenewSsl,
    /// Stop services and remove volumes (destroys data)
    Clean,
    /// Regenerate docker-compose.yml from .env
    Generate,
}

pub fn run(cmd: ProdCommand) -> Result<()> {
    docker::check_docker()?;
    let env = Environment::Prod;

    match cmd {
        ProdCommand::Setup => setup()?,
        ProdCommand::Ssl => ssl()?,
        ProdCommand::Up => up(env)?,
        ProdCommand::Down => down(env)?,
        ProdCommand::Restart => restart(env)?,
        ProdCommand::Status => status(env)?,
        ProdCommand::Logs { service } => logs(env, service.as_deref())?,
        ProdCommand::Shell { service } => shell(env, &service)?,
        ProdCommand::ReloadNginx => reload_nginx(env)?,
        ProdCommand::RenewSsl => renew_ssl(env)?,
        ProdCommand::Clean => clean(env)?,
        ProdCommand::Generate => generate()?,
    }

    Ok(())
}

fn setup() -> Result<()> {
    ui::info("Setting up production environment...");

    let root = project_root()?;
    let prod_dir = root.join(DOCKER_PROD_DIR);
    let env_example = prod_dir.join(".env.example");
    let env_file = prod_dir.join(".env");

    // Copy .env.example to .env if it doesn't exist
    if env_example.exists() && !env_file.exists() {
        std::fs::copy(&env_example, &env_file)?;
        ui::success("Created .env file from .env.example");
    } else if env_file.exists() {
        ui::warning(".env file already exists, skipping");
    }

    // Generate secrets
    ui::info("Generating secure secrets...");
    let script = prod_dir.join("scripts/generate-secrets.sh");
    if script.exists() {
        let status = Command::new("sh")
            .arg(&script)
            .current_dir(&prod_dir)
            .status()?;

        if status.success() {
            ui::success("Secrets generated!");
        }
    } else {
        ui::warning("generate-secrets.sh script not found, skipping");
    }

    ui::warning("IMPORTANT: Update DOMAIN and EMAIL in .env before continuing");
    Ok(())
}

fn ssl() -> Result<()> {
    ui::info("Initializing Let's Encrypt SSL...");

    let root = project_root()?;
    let prod_dir = root.join(DOCKER_PROD_DIR);
    let script = prod_dir.join("scripts/init-letsencrypt.sh");

    if script.exists() {
        let status = Command::new("sh")
            .arg(&script)
            .current_dir(&prod_dir)
            .status()?;

        if status.success() {
            ui::success("SSL setup complete!");
        }
    } else {
        ui::error("init-letsencrypt.sh script not found");
    }

    Ok(())
}

fn up(env: Environment) -> Result<()> {
    ui::info("Starting production services...");
    let status = docker::compose(env, &["up", "-d"])?;

    if status.success() {
        ui::success("Services started!");
        status_internal(env)?;
    }

    Ok(())
}

fn down(env: Environment) -> Result<()> {
    ui::info("Stopping production services...");
    let status = docker::compose(env, &["down"])?;

    if status.success() {
        ui::success("Services stopped!");
    }

    Ok(())
}

fn restart(env: Environment) -> Result<()> {
    ui::info("Restarting production services...");
    let status = docker::compose(env, &["restart"])?;

    if status.success() {
        ui::success("Services restarted!");
    }

    Ok(())
}

fn status(env: Environment) -> Result<()> {
    ui::info("Production Services Status:");
    status_internal(env)
}

fn status_internal(env: Environment) -> Result<()> {
    docker::compose(env, &["ps"])?;
    Ok(())
}

fn logs(env: Environment, service: Option<&str>) -> Result<()> {
    let args = match service {
        Some(s) => vec!["logs", "-f", s],
        None => vec!["logs", "-f"],
    };

    docker::compose(env, &args)?;
    Ok(())
}

fn shell(env: Environment, service: &str) -> Result<()> {
    docker::exec(env, service, &["sh"])?;
    Ok(())
}

fn reload_nginx(env: Environment) -> Result<()> {
    docker::exec(env, "nginx", &["nginx", "-s", "reload"])?;
    ui::success("Nginx reloaded!");
    Ok(())
}

fn renew_ssl(env: Environment) -> Result<()> {
    docker::exec(env, "certbot", &["certbot", "renew", "--force-renewal"])?;
    docker::exec(env, "nginx", &["nginx", "-s", "reload"])?;
    ui::success("SSL certificates renewed!");
    Ok(())
}

fn clean(env: Environment) -> Result<()> {
    if !ui::confirm_strict("This will destroy ALL production data!") {
        ui::info("Aborted.");
        return Ok(());
    }

    let status = docker::compose(env, &["down", "-v"])?;

    if status.success() {
        ui::success("Cleanup complete!");
    }

    Ok(())
}

fn generate() -> Result<()> {
    ui::info("Regenerating docker-compose.yml...");

    let root = project_root()?;
    let prod_dir = root.join(DOCKER_PROD_DIR);
    let script = prod_dir.join("scripts/generate-compose.sh");

    if script.exists() {
        let status = Command::new("sh")
            .arg(&script)
            .current_dir(&prod_dir)
            .status()?;

        if status.success() {
            ui::success("Done!");
        }
    } else {
        ui::error("generate-compose.sh script not found");
    }

    Ok(())
}