use crate::store::settings::Settings;
use anyhow::{Context, Result};
use std::sync::Arc;
pub fn handle_completions(shell: &str) -> Result<()> {
use clap::CommandFactory;
let shell = match shell {
"bash" => clap_complete::Shell::Bash,
"zsh" => clap_complete::Shell::Zsh,
"fish" => clap_complete::Shell::Fish,
"elvish" => clap_complete::Shell::Elvish,
"powershell" => clap_complete::Shell::PowerShell,
_ => {
anyhow::bail!("Unknown shell: {shell}. Supported: bash, zsh, fish, elvish, powershell")
}
};
let mut cmd = crate::cli::CliArgs::command();
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut std::io::stdout());
Ok(())
}
pub async fn handle_install(source: &str) -> Result<()> {
use crate::cli::{ExtCommands, PkgCommands};
if source.starts_with('.')
|| source.starts_with('/')
|| source.starts_with('~')
|| source.starts_with("npm:")
{
super::pkg::handle_pkg(&PkgCommands::Install {
source: source.to_string(),
})?;
} else {
super::ext::handle_ext(&ExtCommands::Install {
source: source.to_string(),
prerelease: false,
})
.await?;
}
Ok(())
}
pub async fn handle_update(check: bool) -> Result<()> {
#[cfg(feature = "self-update")]
{
use self_update::cargo_crate_version;
let current = cargo_crate_version!();
println!("Current version: v{current}");
if check {
return Ok(());
}
println!("Updating oxi...");
let status = tokio::process::Command::new("cargo")
.args(["install", "oxi-cli", "--force"])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.await?;
if !status.success() {
anyhow::bail!("Update failed: cargo install exited with {status}");
}
println!("✅ oxi updated successfully. Restart to use the new version.");
Ok(())
}
#[cfg(not(feature = "self-update"))]
{
let _ = check;
anyhow::bail!("Self-update is not available (compiled without `self-update` feature)");
}
}
pub async fn handle_commit(push: bool, dry_run: bool, context: Option<&str>) -> Result<()> {
use oxi_agent::AgentTool;
use oxi_agent::tools::ToolContext;
use oxi_agent::tools::commit::CommitTool;
use serde_json::json;
let diff_output = tokio::process::Command::new("git")
.args(["diff", "--cached"])
.output()
.await
.with_context(|| "Failed to run git diff --cached")?;
if diff_output.stdout.is_empty() && diff_output.stderr.is_empty() {
let has_changes = tokio::process::Command::new("git")
.args(["status", "--porcelain"])
.output()
.await
.with_context(|| "Failed to run git status")?;
if has_changes.stdout.is_empty() {
anyhow::bail!("Nothing to commit. Working tree is clean.");
}
anyhow::bail!(
"No staged changes. Use `git add` to stage files, or include unstaged changes with `git commit -a`."
);
}
let cwd = std::env::current_dir().context("Failed to get current directory")?;
let ctx = ToolContext::new(cwd.clone());
let tool = CommitTool::unconfigured();
let params = json!({
"dry_run": dry_run,
"push": push,
"context": context.unwrap_or(""),
});
let result = tool
.execute("cli", params, None, &ctx)
.await
.map_err(|e| anyhow::anyhow!("Commit tool failed: {e}"))?;
if dry_run {
println!("{}", result.output);
return Ok(());
}
let message = result
.output
.lines()
.find(|l| {
l.starts_with("feat")
|| l.starts_with("fix")
|| l.starts_with("chore")
|| l.starts_with("docs")
|| l.starts_with("refactor")
|| l.starts_with("test")
|| l.starts_with("perf")
|| l.starts_with("build")
|| l.starts_with("ci")
|| l.starts_with("style")
|| l.starts_with("revert")
})
.unwrap_or("feat: commit")
.to_string();
let status = tokio::process::Command::new("git")
.args(["commit", "-m", &message])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.await
.with_context(|| "Failed to run git commit")?;
if !status.success() {
anyhow::bail!("Commit failed.\nProposed message was:\n{message}");
}
println!("Committed: {message}");
if push {
let push_status = tokio::process::Command::new("git")
.args(["push"])
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::inherit())
.status()
.await
.with_context(|| "Failed to run git push")?;
if !push_status.success() {
anyhow::bail!("Commit succeeded but push failed.");
}
println!("Pushed.");
}
Ok(())
}
pub async fn handle_refresh() -> Result<()> {
use oxi_sdk::ModelCatalog;
use oxi_sdk::ports::catalog::RefreshOutcome;
use oxi_sdk::ports::fs::{CatalogConfig, FileModelCatalog};
let paths = crate::services::OxiPaths::default_paths()?;
let config = CatalogConfig {
cache_path: paths.home.join("cache").join("models-dev.json"),
etag_path: paths.home.join("cache").join("models-dev.json.etag"),
override_path: paths.home.join("catalog").join("overrides.toml"),
mtime_window: std::time::Duration::ZERO,
..Default::default()
};
let cat = FileModelCatalog::init(config).await?;
println!("Refreshing model catalog from models.dev...");
match cat.refresh().await? {
RefreshOutcome::Updated {
provider_count,
model_count,
} => {
println!(
"✓ Catalog updated: {} providers, {} models.",
provider_count, model_count
);
}
RefreshOutcome::Unchanged => {
println!("✓ Catalog already up to date.");
}
RefreshOutcome::Offline { reason } => {
println!("⚠ Catalog refresh skipped (offline: {reason}).");
}
RefreshOutcome::Failed { reason } => {
println!("✗ Catalog refresh failed: {reason}.");
}
}
Ok(())
}
pub async fn handle_models(provider: &Option<String>) -> Result<()> {
use oxi_sdk::ModelCatalog;
if let Some(ref provider_name) = *provider {
let settings = Settings::load().unwrap_or_default();
if let Some(cp) = settings
.custom_providers
.iter()
.find(|cp| cp.name == *provider_name)
{
let auth = crate::store::auth_storage::shared_auth_storage();
let api_key = auth.get_api_key(&cp.name);
if let Some(ref key) = api_key {
match oxi_sdk::fetch_models_blocking(&cp.base_url, key) {
Ok(model_ids) => {
let api_type = match cp.api.to_lowercase().as_str() {
"openai-responses" | "responses" => oxi_sdk::Api::OpenAiResponses,
_ => oxi_sdk::Api::OpenAiCompletions,
};
for model_id in &model_ids {
let model = oxi_sdk::Model {
id: model_id.clone(),
name: model_id.clone(),
api: api_type,
provider: cp.name.clone(),
base_url: cp.base_url.clone(),
reasoning: false,
input: vec![oxi_sdk::InputModality::Text],
cost: oxi_sdk::Cost::default(),
context_window: 128_000,
max_tokens: 8_192,
headers: Default::default(),
compat: None,
};
oxi_sdk::register_model(model);
}
if model_ids.is_empty() {
println!("No models found for provider '{}'.", provider_name);
} else {
println!(
"Models from '{}' ({} fetched):",
provider_name,
model_ids.len()
);
for id in &model_ids {
println!(" {}", id);
}
}
return Ok(());
}
Err(e) => {
eprintln!(
"[oxi] warning: failed to resolve models for {}: {}",
provider_name, e
);
}
}
} else {
eprintln!(
"[oxi] API key not set for provider '{}' (expected: {})",
provider_name, cp.api_key_env
);
}
}
let cat = build_catalog_for_cli().await?;
let models = cat.list_models(provider_name).await?;
if models.is_empty() {
println!(
"No models found for provider '{}' (static or dynamic).",
provider_name
);
} else {
println!(
"Models for provider '{}' ({}):",
provider_name,
models.len()
);
for m in models {
println!(" {} ({})", m.model_id, m.name);
}
}
return Ok(());
}
let cat = build_catalog_for_cli().await?;
let all = cat.search("").await?;
let count = cat.model_count().await?;
println!("Available models ({} total):", count);
for entry in &all {
println!(" {}/{} — {}", entry.provider, entry.model_id, entry.name);
}
Ok(())
}
pub(crate) async fn build_catalog_for_cli() -> Result<Arc<oxi_sdk::FileModelCatalog>> {
use oxi_sdk::ports::fs::CatalogConfig;
let paths = crate::services::OxiPaths::default_paths()?;
let config = CatalogConfig {
cache_path: paths.home.join("cache").join("models-dev.json"),
etag_path: paths.home.join("cache").join("models-dev.json.etag"),
override_path: paths.home.join("catalog").join("overrides.toml"),
fetch_enabled: false,
..Default::default()
};
Ok(oxi_sdk::FileModelCatalog::init(config).await?)
}