reeve-cli 0.1.0

Localhost web dev stack manager: web servers, per-vhost PHP versions, SSL, and DNS — RunCloud, scaled down.
//! Local SSL via a shared mkcert CA. The CA is installed once into the system
//! trust store; per-vhost certs are minted from it into `certs/`, so every
//! backend trusts the same root and browsers show no warning.

use crate::brew::Brew;
use crate::paths;
use anyhow::{bail, Context, Result};
use std::path::PathBuf;
use std::process::Command;

fn mkcert_bin(brew: &Brew) -> PathBuf {
    brew.bin("mkcert")
}

/// Ensure the mkcert formula is installed.
pub fn ensure_installed(brew: &Brew) -> Result<()> {
    if !brew.is_installed("mkcert") {
        println!("Installing mkcert…");
        brew.install("mkcert")?;
    }
    Ok(())
}

/// Path to the mkcert CA root directory (`mkcert -CAROOT`).
pub fn caroot(brew: &Brew) -> Result<PathBuf> {
    let out = Command::new(mkcert_bin(brew))
        .arg("-CAROOT")
        .output()
        .context("Failed to run `mkcert -CAROOT`")?;
    if !out.status.success() {
        bail!("`mkcert -CAROOT` failed");
    }
    Ok(PathBuf::from(
        String::from_utf8_lossy(&out.stdout).trim().to_string(),
    ))
}

/// The CA root certificate path (`rootCA.pem`), if it exists. Useful for
/// `curl --cacert` verification and for pointing tools at the trust anchor.
pub fn ca_cert(brew: &Brew) -> Result<PathBuf> {
    Ok(caroot(brew)?.join("rootCA.pem"))
}

/// Ensure mkcert's local CA exists and is installed into the trust store.
/// `mkcert -install` is idempotent; on macOS the first run may prompt for
/// admin authorization to add the root to the System keychain.
pub fn ensure_ca(brew: &Brew) -> Result<()> {
    ensure_installed(brew)?;
    let status = Command::new(mkcert_bin(brew))
        .arg("-install")
        .status()
        .context("Failed to run `mkcert -install`")?;
    if !status.success() {
        bail!(
            "`mkcert -install` failed. You may need to authorize adding the CA \
             to your trust store, then re-run."
        );
    }
    Ok(())
}

/// Certificate + key paths for a host (the convention the backends expect).
pub fn cert_paths(host: &str) -> Result<(PathBuf, PathBuf)> {
    let dir = paths::certs_dir()?;
    Ok((
        dir.join(format!("{host}.pem")),
        dir.join(format!("{host}-key.pem")),
    ))
}

/// Mint a certificate for `host` from the shared CA into `certs/`.
pub fn mint(brew: &Brew, host: &str) -> Result<()> {
    ensure_installed(brew)?;
    paths::ensure_dirs()?;
    let (cert, key) = cert_paths(host)?;
    // Capture (don't inherit) stdio: mkcert is chatty, and inherited output
    // would corrupt the TUI's alternate screen when minting during `apply`.
    let out = Command::new(mkcert_bin(brew))
        .arg("-cert-file")
        .arg(&cert)
        .arg("-key-file")
        .arg(&key)
        .arg(host)
        .output()
        .with_context(|| format!("Failed to run mkcert for {host}"))?;
    if !out.status.success() {
        bail!(
            "mkcert failed to mint a certificate for {host}: {}",
            String::from_utf8_lossy(&out.stderr).trim()
        );
    }
    Ok(())
}

/// True if both cert and key already exist for a host.
pub fn exists(host: &str) -> bool {
    cert_paths(host)
        .map(|(c, k)| c.exists() && k.exists())
        .unwrap_or(false)
}