mod backend;
mod config;
mod exec;
use clap::Parser;
use config::Config;
use std::path::Path;
#[derive(Parser)]
#[command(
name = "gloc",
version,
about = "Auto-detect local LLM backends and launch AI CLI tools"
)]
struct Cli {
#[arg(long, value_name = "NAME")]
client: Option<String>,
#[arg(long, value_name = "NAME")]
backend: Option<String>,
#[arg(long, value_name = "NAME")]
model: Option<String>,
#[arg(long, value_name = "URL")]
url: Option<String>,
#[arg(long)]
status: bool,
#[arg(long)]
init: bool,
#[arg(long, value_name = "PATH")]
config: Option<std::path::PathBuf>,
#[arg(long)]
dump_config: bool,
#[arg(last = true)]
passthrough: Vec<String>,
}
fn main() {
let cli = Cli::parse();
if cli.init {
handle_init();
return;
}
let cfg = Config::load(cli.config.as_deref());
if cli.config.is_none() {
auto_export_config();
}
if cli.dump_config {
print!("{}", cfg.dump());
return;
}
let mut active_backend = resolve_backend(&cli, &cfg);
if let Some(ref url) = cli.url {
active_backend.url = url.clone();
}
let (client_name, client) = resolve_client(&cli, &cfg);
let model = resolve_model(&cli, &client, &cfg);
if cli.status {
print_status(&active_backend, &client_name, &client, &model);
return;
}
if let Err(e) = backend::ensure_model_ready(&active_backend, &model, &cfg.settings) {
match e {
backend::ModelError::NotFound(m) => {
eprintln!("gloc: model '{}' not found on {}", m, active_backend.name);
if active_backend.name == "ollama" {
eprintln!(" Run: ollama pull {}", m);
eprintln!(" Or set auto_pull: true in config");
}
std::process::exit(1);
}
backend::ModelError::PullFailed(msg) => {
eprintln!("gloc: failed to pull model: {}", msg);
std::process::exit(1);
}
backend::ModelError::WarmupFailed(msg) => {
eprintln!("gloc: warning: model warmup failed: {}", msg);
}
backend::ModelError::NetworkError(msg) => {
eprintln!("gloc: network error: {}", msg);
std::process::exit(1);
}
}
}
exec::exec_client(&client, &active_backend, &model, &cli.passthrough);
}
fn auto_export_config() {
if let Some(home) = dirs::home_dir() {
let global_dir = home.join(".gobby");
let global_config = global_dir.join("gloc.yaml");
if !global_config.exists() {
let _ = std::fs::create_dir_all(&global_dir);
if let Err(e) = std::fs::write(&global_config, config::DEFAULT_CONFIG_YAML) {
eprintln!("Warning: failed to write {}: {e}", global_config.display());
}
}
}
}
fn handle_init() {
let project_dir = Path::new(".gobby");
let project_config = project_dir.join("gloc.yaml");
if !project_dir.is_dir() {
eprintln!("Error: .gobby/ directory not found. Run from a Gobby project root.");
std::process::exit(1);
}
if project_config.exists() {
let bak = project_dir.join("gloc.yaml.bak");
if let Err(e) = std::fs::rename(&project_config, &bak) {
eprintln!("Failed to backup .gobby/gloc.yaml: {e}");
std::process::exit(1);
}
eprintln!("Backed up .gobby/gloc.yaml -> .gobby/gloc.yaml.bak");
}
if let Err(e) = std::fs::write(&project_config, config::DEFAULT_CONFIG_YAML) {
eprintln!("Failed to write .gobby/gloc.yaml: {e}");
std::process::exit(1);
}
eprintln!("Created .gobby/gloc.yaml");
}
fn resolve_backend(cli: &Cli, cfg: &Config) -> config::Backend {
if let Some(ref name) = cli.backend {
let backend = cfg
.backends
.iter()
.find(|b| b.name == *name)
.unwrap_or_else(|| {
eprintln!("gloc: unknown backend '{}'", name);
eprintln!(
" Available: {}",
cfg.backends
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
std::process::exit(1);
})
.clone();
if !backend::validate_backend(&backend, cfg.settings.probe_timeout_ms) {
eprintln!(
"gloc: backend '{}' at {} is not reachable",
name, backend.url
);
std::process::exit(1);
}
backend
} else {
match backend::detect_backend(&cfg.backends, cfg.settings.probe_timeout_ms) {
Some(backend) => {
eprintln!("gloc: detected {} at {}", backend.name, backend.url);
backend
}
None => {
eprintln!("gloc: no backend detected");
eprintln!(" Start LM Studio or Ollama, or use --backend and --url");
std::process::exit(1);
}
}
}
}
fn resolve_client(cli: &Cli, cfg: &Config) -> (String, config::Client) {
if let Some(ref name) = cli.client {
let client = cfg
.clients
.get(name)
.unwrap_or_else(|| {
eprintln!("gloc: unknown client '{}'", name);
eprintln!(
" Available: {}",
cfg.clients.keys().cloned().collect::<Vec<_>>().join(", ")
);
std::process::exit(1);
})
.clone();
(name.clone(), client)
} else {
cfg.clients
.iter()
.next()
.map(|(name, client)| (name.clone(), client.clone()))
.unwrap_or_else(|| {
eprintln!("gloc: no clients configured");
std::process::exit(1);
})
}
}
fn resolve_model(cli: &Cli, client: &config::Client, cfg: &Config) -> String {
let raw = cli.model.as_deref().unwrap_or(&client.default_model);
cfg.resolve_alias(raw)
}
fn print_status(
backend: &config::Backend,
client_name: &str,
client: &config::Client,
model: &str,
) {
eprintln!("Backend: {} ({})", backend.name, backend.url);
eprintln!("Client: {} ({})", client_name, client.binary);
if let Some(path) = exec::which_binary(&client.binary) {
eprintln!("Binary: {}", path.display());
} else {
eprintln!("Binary: {} (not found in PATH)", client.binary);
}
eprintln!("Model: {}", model);
let env = exec::build_env(client, backend, model);
eprintln!("Env:");
for (key, val) in &env {
if val.is_empty() {
eprintln!(" {}=\"\"", key);
} else {
eprintln!(" {}={}", key, val);
}
}
let args = exec::build_args(client, model, &[]);
if !args.is_empty() {
eprintln!("Args: {} {}", client.binary, args.join(" "));
}
}