muthr 0.1.41

A zero-trust orchestrator that automates secure inference and isolated execution of local AI agents.
use std::fs;
use std::io::{IsTerminal, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use tempfile::NamedTempFile;

pub async fn run(action: crate::ServicesCommands) -> Result<(), color_eyre::Report> {
    match action {
        crate::ServicesCommands::Start { dry_run } => start(dry_run).await?,
        crate::ServicesCommands::Stop { dry_run } => stop(dry_run).await?,
        crate::ServicesCommands::Status { output } => status(output).await?,
        crate::ServicesCommands::Restart { dry_run } => restart(dry_run).await?,
        crate::ServicesCommands::Delete {
            force,
            yes,
            dry_run,
        } => delete(force || yes, dry_run).await?,
    }
    Ok(())
}

pub async fn start(dry_run: bool) -> Result<(), color_eyre::Report> {
    if dry_run {
        eprintln!("info: dry run, skipping services start");
        return Ok(());
    }
    let vm_name = "muthr-services";
    let home = std::env::var("HOME")?;
    let template_path = PathBuf::from(&home).join(".config/muthr/manifests/muthr-services.yaml");

    if !is_vm_running(vm_name) {
        if !is_vm_exists(vm_name) {
            eprintln!("info: creating muthr-services vm");

            if !template_path.exists() {
                return Err(color_eyre::eyre::eyre!(
                    "template not found: {:?}",
                    template_path
                ));
            }

            let content = fs::read_to_string(&template_path)?;
            let mut tmp_yaml = NamedTempFile::new()?;
            tmp_yaml.write_all(content.as_bytes())?;

            let create_status = Command::new("limactl")
                .args(["create", "--name", vm_name])
                .arg(tmp_yaml.path())
                .stdout(Stdio::inherit())
                .stderr(Stdio::inherit())
                .status()?;

            if !create_status.success() {
                return Err(color_eyre::eyre::eyre!(
                    "failed to create muthr-services vm"
                ));
            }

            let start_status = Command::new("limactl")
                .args(["start", vm_name])
                .stdout(Stdio::inherit())
                .stderr(Stdio::inherit())
                .status()?;

            if !start_status.success() {
                return Err(color_eyre::eyre::eyre!("failed to start muthr-services vm"));
            }
        } else {
            eprintln!("info: starting muthr-services vm");
            let status = Command::new("limactl")
                .args(["start", vm_name])
                .stdout(Stdio::inherit())
                .stderr(Stdio::inherit())
                .status()?;

            if !status.success() {
                return Err(color_eyre::eyre::eyre!("failed to start muthr-services vm"));
            }
        }
    } else {
        eprintln!("info: muthr-services vm already running");
        return Ok(());
    }

    if !is_vm_provisioned(vm_name) {
        let cp_status = Command::new("limactl")
            .args([
                "cp",
                &format!("{}/.config/muthr/provision.d/muthr-services.sh", home),
                &format!("{}:/tmp/muthr-services.sh", vm_name),
            ])
            .status()?;

        if !cp_status.success() {
            return Err(color_eyre::eyre::eyre!(
                "failed to copy provision script into services VM"
            ));
        }

        let provision_status = Command::new("limactl")
            .args(["shell", vm_name, "bash", "/tmp/muthr-services.sh", vm_name])
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .status()?;

        if provision_status.success() {
            eprintln!("info: muthr-services vm provisioned");
        } else {
            return Err(color_eyre::eyre::eyre!(
                "muthr-services vm provisioning failed"
            ));
        }
    }

    eprintln!("info: searxng:  http://127.0.0.1:18766");
    eprintln!("info: mcp:      http://127.0.0.1:18765/mcp");

    Ok(())
}

pub async fn stop(dry_run: bool) -> Result<(), color_eyre::Report> {
    if dry_run {
        eprintln!("info: dry run, skipping services stop");
        return Ok(());
    }
    let vm_name = "muthr-services";

    if !is_vm_exists(vm_name) {
        return Ok(());
    }

    let output = Command::new("limactl")
        .arg("stop")
        .arg(vm_name)
        .output()
        .ok();

    match output {
        Some(out) if out.status.success() => {
            eprintln!("info: stopped {}", vm_name);
        }
        _ => {
            eprintln!("warning: acpi stop sequence sent");
        }
    }

    Ok(())
}

pub async fn status(output: crate::OutputFormat) -> Result<(), color_eyre::Report> {
    let vm_name = "muthr-services";

    if !is_vm_exists(vm_name) {
        return Ok(());
    }

    let status = Command::new("limactl")
        .args(["ls", "-f", "{{.Status}}", vm_name])
        .output()
        .ok()
        .and_then(|out| {
            String::from_utf8_lossy(&out.stdout)
                .trim()
                .to_string()
                .split_whitespace()
                .next()
                .map(|s| s.to_string())
        })
        .unwrap_or_else(|| "unknown".to_string());

    match output {
        crate::OutputFormat::Text => eprintln!("muthr-services vm: {}", status.to_lowercase()),
        crate::OutputFormat::Json => {
            let payload = serde_json::json!({"name": vm_name, "status": status.to_lowercase()});
            println!("{}", serde_json::to_string(&payload)?);
        }
        crate::OutputFormat::Ndjson => {
            let payload = serde_json::json!({"name": vm_name, "status": status.to_lowercase()});
            println!("{}", serde_json::to_string(&payload)?);
        }
    }

    Ok(())
}

pub async fn restart(dry_run: bool) -> Result<(), color_eyre::Report> {
    if dry_run {
        eprintln!("info: dry run, skipping services restart");
        return Ok(());
    }
    stop(false).await?;
    tokio::time::sleep(std::time::Duration::from_secs(2)).await;
    start(false).await?;
    Ok(())
}

fn is_vm_exists(vm_name: &str) -> bool {
    let output = Command::new("limactl")
        .args(["ls", "-q"])
        .output()
        .ok()
        .filter(|o| o.status.success());

    match output {
        Some(out) => {
            let stdout = String::from_utf8_lossy(&out.stdout);
            for line in stdout.lines() {
                if line == vm_name {
                    return true;
                }
            }
            false
        }
        None => false,
    }
}

fn is_vm_running(vm_name: &str) -> bool {
    let output = Command::new("limactl")
        .args(["ls", "-f", "{{.Status}}", vm_name])
        .output()
        .ok();

    match output {
        Some(out) if out.status.success() => {
            let status = String::from_utf8_lossy(&out.stdout);
            status.contains("Running")
        }
        _ => false,
    }
}

fn is_vm_provisioned(vm_name: &str) -> bool {
    let output = Command::new("limactl")
        .arg("shell")
        .arg(vm_name)
        .arg("bash")
        .arg("-c")
        .arg("test -f $HOME/mcp-stdio.sh && test -f $HOME/.local/lib/node_modules/mcp-searxng/dist/cli.js")
        .output()
        .ok();

    match output {
        Some(out) => out.status.success(),
        None => false,
    }
}

pub async fn delete(force: bool, dry_run: bool) -> Result<(), color_eyre::Report> {
    if dry_run {
        eprintln!("info: dry run, skipping services delete");
        return Ok(());
    }
    let vm_name = "muthr-services";

    if !is_vm_exists(vm_name) {
        return Ok(());
    }

    if !force && !std::io::stdout().is_terminal() {
        eprintln!("error: terminal required for deletion, use --yes");
        std::process::exit(77);
    }

    eprintln!("info: deleting muthr-services vm");

    let unprotect_status = Command::new("limactl")
        .args(["unprotect", vm_name])
        .output()?;
    if !unprotect_status.status.success() {
        eprintln!("warning: failed to unprotect vm");
    }

    if is_vm_running(vm_name) {
        let stop_output = Command::new("limactl").arg("stop").arg(vm_name).output()?;
        if !stop_output.status.success() {
            eprintln!("warning: failed to stop vm");
        }
    }

    let delete_status = Command::new("limactl").args(["delete", vm_name]).output()?;

    if !delete_status.status.success() {
        return Err(color_eyre::eyre::eyre!(
            "failed to delete muthr-services vm"
        ));
    }

    eprintln!("info: deleted {}", vm_name);
    Ok(())
}