muthr 0.1.23

A zero-trust orchestrator that automates secure inference and isolated execution of local AI agents.
pub mod config;
pub mod download;
pub mod engine;
pub mod init;
pub mod model;
pub mod preset;
pub mod runtime_config;
pub mod sandbox;
pub mod services;
pub mod theme;
pub mod ui;

use clap::{CommandFactory, Parser, Subcommand};
use clap_complete::{generate, Shell};

use crate::config::ConfigCommands;
use crate::sandbox::ProvisionProfile;

#[derive(Parser)]
#[command(
    name = "muthr",
    version,
    author,
    about = "Manage llama.cpp inference and Lima sandbox VMs for local AI development.",
    long_about = "muthr automates llama.cpp on macOS for local inference and manages isolated Lima sandbox VMs for running AI agents with safe access to your workspace.\n\nPrerequisites: macOS (Apple Silicon), Lima VM, and llama.cpp",
    arg_required_else_help = true,
    propagate_version = true
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    #[command(about = "Start the llama-server inference engine")]
    Serve {
        #[arg(long, help = "Name of the target preset profile to load")]
        profile: Option<String>,
        #[arg(
            short,
            long,
            help = "Port to bind the inference engine server (default from muthr.toml or 8080)"
        )]
        port: Option<u16>,
        #[arg(
            long,
            help = "Run in foreground (blocking mode) instead of as a background daemon"
        )]
        foreground: bool,
    },

    #[command(about = "Stop the background llama-server daemon")]
    Stop,

    #[command(about = "Show the active profile and server status")]
    Status,

    #[command(about = "List available preset profiles")]
    List,

    #[command(about = "Provision and start a Lima sandbox VM for the current project")]
    Up {
        #[arg(
            short,
            long,
            help = "Port where the inference engine is reachable (default from muthr.toml or 8080)"
        )]
        port: Option<u16>,
        #[arg(
            long,
            value_enum,
            help = "Provisioning profile to apply (base = minimal, opencode = full toolchain; default from muthr.toml or base)"
        )]
        provision_profile: Option<ProvisionProfile>,
    },

    #[command(about = "Stop the active project sandbox VM")]
    Down,

    #[command(about = "List all managed sandbox VMs")]
    Ls,

    #[command(about = "Manage the persistent MCP services VM")]
    Services {
        #[command(subcommand)]
        action: ServicesCommands,
    },

    #[command(about = "Download a GGUF model from Hugging Face")]
    Download {
        #[arg(help = "Hugging Face repository (repo/name) or explicit resolve URL")]
        source: String,
        #[arg(help = "Target GGUF filename (required when using repository syntax)")]
        file: Option<String>,
    },

    #[command(about = "Generate shell completion scripts")]
    Completion {
        #[arg(
            value_enum,
            help = "Target shell environment for completion generation"
        )]
        shell: Shell,
    },

    #[command(about = "Browse and select Ghostty terminal themes")]
    Themes,

    #[command(about = "Initialize muthr configurations from the upstream repository")]
    Init {
        #[arg(
            long,
            help = "Custom Git URL for muthr-configs repository source override"
        )]
        git_url: Option<String>,
        #[arg(
            long,
            help = "Force overwrite existing configurations inside ~/.config/muthr/"
        )]
        force: bool,
    },

    #[command(about = "Manage muthr configuration")]
    Config {
        #[command(subcommand)]
        action: ConfigCommands,
    },
}

#[derive(Subcommand)]
pub enum ServicesCommands {
    #[command(about = "Start the MCP services VM")]
    Start,
    #[command(about = "Stop the MCP services VM")]
    Stop,
    #[command(about = "Show the MCP services VM execution status")]
    Status,
    #[command(about = "Restart the MCP services VM execution context")]
    Restart,
}

#[tokio::main]
async fn main() -> color_eyre::Result<()> {
    color_eyre::install()?;
    run().await
}

async fn run() -> Result<(), color_eyre::Report> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Serve {
            profile,
            port,
            foreground,
        } => {
            let cfg = config::load()?;
            let server_port = port.unwrap_or(cfg.port.unwrap_or(8080));
            engine::serve(profile, server_port, foreground).await?
        }
        Commands::Status => engine::status().await?,
        Commands::Stop => engine::stop().await?,
        Commands::List => engine::list()?,
        Commands::Up {
            port,
            provision_profile,
        } => {
            let cfg = config::load()?;
            let up_port = port.unwrap_or(cfg.port.unwrap_or(8080));
            let up_profile = match (provision_profile, cfg.default_provision_profile.as_deref()) {
                (Some(p), _) => p,
                (None, Some("opencode")) => ProvisionProfile::Opencode,
                (None, None) => {
                    let options = vec![
                        crate::ui::ProvisionOption {
                            label: "base",
                            description: "Minimal VM provision — no extra tools installed",
                        },
                        crate::ui::ProvisionOption {
                            label: "opencode",
                            description: "Full toolchain with opencode binary and MCP support",
                        },
                    ];
                    match ui::select_provision_profile(&options) {
                        Some(0) => ProvisionProfile::Base,
                        Some(1) => ProvisionProfile::Opencode,
                        _ => {
                            println!("[INFO] Cancelled.");
                            return Ok(());
                        }
                    }
                }
                _ => ProvisionProfile::Base,
            };
            sandbox::up(up_port, up_profile).await?
        }
        Commands::Down => sandbox::down().await?,
        Commands::Ls => sandbox::list().await?,
        Commands::Services { action } => services::run(action).await?,
        Commands::Download { source, file } => download::download(&source, file.as_deref()).await?,
        Commands::Themes => theme::run()?,
        Commands::Init { git_url, force } => init::run(init::InitCommands { git_url, force })?,
        Commands::Config { action } => match action {
            ConfigCommands::Init { force } => config::init_config(force)?,
            ConfigCommands::Show => {
                let cfg = config::load()?;
                cfg.print_resolved();
            }
        },
        Commands::Completion { shell } => {
            let mut cmd = Cli::command();
            generate(shell, &mut cmd, "muthr", &mut std::io::stdout());
        }
    }

    Ok(())
}