use clap::Subcommand;
use serde::{Deserialize, Serialize};
use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
type ResolvedConfig = (
u16,
String,
String,
String,
Option<String>,
Option<String>,
Option<String>,
Option<String>,
);
#[derive(Subcommand)]
pub enum ConfigCommands {
Init {
#[arg(long, help = "Force overwrite existing muthr.toml")]
force: bool,
},
Show,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct MuthrConfig {
pub server_port: Option<u16>,
pub workspace_root: Option<String>,
pub model_dir: Option<String>,
pub default_provision_profile: Option<String>,
pub default_engine_runtime: Option<String>,
pub default_engine_profile: Option<String>,
pub default_engine_bind_host: Option<String>,
pub container_host_gateway: Option<String>,
}
impl MuthrConfig {
fn resolve(self) -> Result<ResolvedConfig, color_eyre::Report> {
let server_port = self.server_port.unwrap_or(8080);
let home = dirs::home_dir()
.map(|p| p.to_string_lossy().to_string())
.ok_or_else(|| color_eyre::eyre::eyre!("could not resolve home directory"))?;
let workspace_root = match self.workspace_root {
Some(v) => v,
None => format!("{}/src", home),
};
let model_dir = match self.model_dir {
Some(v) => v,
None => format!("{}/opt/models", home),
};
let provision_profile = self
.default_provision_profile
.unwrap_or_else(|| "opencode".to_string());
let engine_runtime = self.default_engine_runtime.clone();
let engine_profile = self.default_engine_profile.clone();
let engine_bind_host = self.default_engine_bind_host.clone();
let container_host_gateway = self.container_host_gateway.clone();
Ok((
server_port,
workspace_root,
model_dir,
provision_profile,
engine_runtime,
engine_profile,
engine_bind_host,
container_host_gateway,
))
}
pub fn print_resolved(&self) {
let (
server_port,
workspace_root,
model_dir,
provision_profile,
engine_runtime,
engine_profile,
engine_bind_host,
container_host_gateway,
) = match self.clone().resolve() {
Ok(v) => v,
Err(err) => {
eprintln!("error: {}", err);
return;
}
};
println!("server_port {}", server_port);
println!("workspace_root {}", workspace_root);
println!("model_dir {}", model_dir);
println!("provision_profile {}", provision_profile);
println!(
"engine_runtime {}",
engine_runtime.as_deref().unwrap_or("mlxcel")
);
println!(
"engine_profile {}",
engine_profile
.as_deref()
.unwrap_or("mlx-community/Qwen3.5-9B-MLX-4bit")
);
println!(
"engine_bind_host {}",
engine_bind_host.as_deref().unwrap_or("0.0.0.0")
);
println!(
"container_gateway {}",
container_host_gateway.unwrap_or_else(|| "<auto>".to_string())
);
}
}
pub fn load() -> Result<MuthrConfig, color_eyre::Report> {
let home = std::env::var("HOME")?;
let config_path = PathBuf::from(&home).join(".config/muthr/muthr.toml");
let mut config = if config_path.exists() {
let content = fs::read_to_string(&config_path)?;
toml::from_str(&content)?
} else {
MuthrConfig::default()
};
if let Ok(v) = std::env::var("MUTHR_SERVER_PORT") {
config.server_port = v.parse().ok();
}
if let Ok(v) = std::env::var("MUTHR_WORKSPACE_ROOT") {
config.workspace_root = Some(v);
}
if let Ok(v) = std::env::var("MUTHR_MODEL_DIR") {
config.model_dir = Some(v);
}
if let Ok(v) = std::env::var("MUTHR_PROVISION_PROFILE") {
config.default_provision_profile = Some(v);
}
if let Ok(v) = std::env::var("MUTHR_ENGINE_RUNTIME") {
config.default_engine_runtime = Some(v);
}
if let Ok(v) = std::env::var("MUTHR_ENGINE_PROFILE") {
config.default_engine_profile = Some(v);
}
if let Ok(v) = std::env::var("MUTHR_ENGINE_BIND_HOST") {
config.default_engine_bind_host = Some(v);
}
if let Ok(v) = std::env::var("MUTHR_CONTAINER_HOST_GATEWAY") {
config.container_host_gateway = Some(v);
}
Ok(config)
}
pub fn init_config(force: bool) -> Result<(), color_eyre::Report> {
let home = std::env::var("HOME")?;
let config_dir = PathBuf::from(&home).join(".config/muthr");
let config_path = config_dir.join("muthr.toml");
if config_path.exists() && !force {
return Ok(());
}
fs::create_dir_all(&config_dir)?;
fs::set_permissions(&config_dir, fs::Permissions::from_mode(0o700))?;
let template = r##"# muthr configuration
# API port for local inference server
server_port = 8080
# Root used for project-to-sandbox mapping (must NOT be your home directory)
workspace_root = "~/src"
# Base directory for local model files
model_dir = "~/opt/models"
# Default sandbox profile
default_provision_profile = "opencode"
# Inference runtime: "mlxcel" or "llama"
default_engine_runtime = "mlxcel"
# Bind host for inference server (127.0.0.1 for host-only, 0.0.0.0 for sandbox access)
default_engine_bind_host = "0.0.0.0"
# Default model/profile passed to engine start when --profile is omitted.
# MLX examples:
default_engine_profile = "mlx-community/Qwen3.5-9B-MLX-4bit"
# default_engine_profile = "/Users/user/opt/models/majentik/Qwen3.6-35B-A3B-RotorQuant-MLX-4bit"
# llama example:
# default_engine_profile = "/Users/user/opt/models/unsloth/Qwen3.6-35B-A3B-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf"
# Optional explicit container host gateway IP (auto-detected when unset)
# container_host_gateway = "192.168.64.1"
"##;
fs::write(&config_path, template)?;
fs::set_permissions(&config_path, fs::Permissions::from_mode(0o600))?;
crate::ui::log_info(&format!("created {}", config_path.display()));
Ok(())
}