use std::path::{Path, PathBuf};
use crate::config::Config;
use crate::vendor::{AuthKind, VendorId};
pub struct Probes<'a> {
pub env_set: &'a dyn Fn(&str) -> bool,
pub exists: &'a dyn Fn(&Path) -> bool,
pub keychain_has_claude: &'a dyn Fn() -> bool,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct VendorStatus {
pub id: &'static str,
pub name: &'static str,
pub short_name: &'static str,
pub kind: AuthKind,
pub enabled: bool,
pub configured: bool,
pub needs_credential: bool,
pub env: String,
pub login: &'static str,
}
pub fn statuses(cfg: &Config) -> Vec<VendorStatus> {
let probes = Probes {
env_set: &|name| std::env::var_os(name).is_some_and(|value| !value.is_empty()),
exists: &|path| path.exists(),
keychain_has_claude: &keychain_has_claude,
};
statuses_with(cfg, &probes)
}
pub fn statuses_with(cfg: &Config, probes: &Probes) -> Vec<VendorStatus> {
VendorId::all()
.iter()
.copied()
.map(|id| {
let needs_credential = id != VendorId::Antigravity;
VendorStatus {
id: id.slug(),
name: id.display_name(),
short_name: id.short_name(),
kind: id.auth_kind(),
enabled: cfg.is_enabled(id),
configured: !needs_credential || credential_present(cfg, id, probes),
needs_credential,
env: cfg.api_key_env_for(id).to_string(),
login: id.login_command(),
}
})
.collect()
}
fn credential_present(cfg: &Config, id: VendorId, probes: &Probes) -> bool {
let env = cfg.api_key_env_for(id);
if !env.is_empty() && (probes.env_set)(env) {
return true;
}
if cfg.inline_api_key(id).is_some() {
return true;
}
match id {
VendorId::Anthropic => {
any_exists(probes, [crate::anthropic::creds::default_path()])
|| (probes.keychain_has_claude)()
}
VendorId::Openai => any_exists(probes, [crate::openai::creds::default_path()]),
VendorId::Copilot => {
any_exists(probes, [crate::copilot::credentials::default_hosts_path()])
}
VendorId::CommandCode => match crate::commandcode::creds::default_paths() {
Ok(paths) => paths.iter().any(|path| (probes.exists)(path)),
Err(_) => false,
},
VendorId::NousResearch => {
(probes.exists)(&crate::nous::credentials::default_credentials_path())
}
VendorId::Kimi => any_exists(probes, [kimi_credentials_path(cfg)]),
VendorId::Cursor => any_exists(
probes,
[
cfg.cursor
.db_path
.clone()
.map_or_else(crate::cursor::db::default_db_path, Ok),
cfg.cursor
.agent_auth_path
.clone()
.map_or_else(crate::cursor::db::default_agent_auth_path, Ok),
],
),
VendorId::Kiro => any_exists(
probes,
[cfg.kiro
.db_path
.clone()
.map_or_else(crate::kiro::db::default_db_path, Ok)],
),
VendorId::Supergrok => (probes.exists)(&cfg.supergrok.grok_binary),
VendorId::Antigravity => true,
VendorId::AnthropicApi
| VendorId::Zai
| VendorId::Openrouter
| VendorId::Deepseek
| VendorId::Kilo
| VendorId::Novita
| VendorId::Moonshot
| VendorId::Grok
| VendorId::Minimax
| VendorId::OpenCodeGo => false,
}
}
fn kimi_credentials_path(cfg: &Config) -> crate::error::Result<PathBuf> {
match &cfg.kimi.credentials_path {
Some(path) => Ok(path.clone()),
None => Ok(crate::kimi::oauth::credentials_path_in(
&crate::cache::home_dir()?,
)),
}
}
fn any_exists<const N: usize>(probes: &Probes, paths: [crate::error::Result<PathBuf>; N]) -> bool {
paths
.iter()
.filter_map(|path| path.as_ref().ok())
.any(|path| (probes.exists)(path))
}
#[cfg(target_os = "macos")]
fn keychain_has_claude() -> bool {
matches!(crate::anthropic::keychain::read_raw(), Ok(Some(_)))
}
#[cfg(not(target_os = "macos"))]
fn keychain_has_claude() -> bool {
false
}
pub fn run(json: bool) -> i32 {
let cfg = match Config::load() {
Ok(cfg) => cfg,
Err(error) => {
eprintln!("vendors: {error}");
return 1;
}
};
let rows = statuses(&cfg);
if json {
match serde_json::to_string(&serde_json::json!({"vendors": rows})) {
Ok(text) => println!("{text}"),
Err(error) => {
eprintln!("vendors: {error}");
return 1;
}
}
return 0;
}
for row in rows {
let state = if !row.enabled {
"off"
} else if row.configured {
"ready"
} else {
"needs credential"
};
println!("{:<14} {:<10} {}", row.id, row.kind.as_str(), state);
}
0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::settings::KEY_VENDORS;
fn probes<'a>(env: &'a dyn Fn(&str) -> bool, exists: &'a dyn Fn(&Path) -> bool) -> Probes<'a> {
Probes {
env_set: env,
exists,
keychain_has_claude: &|| false,
}
}
fn bare<'a>() -> Probes<'a> {
probes(&|_| false, &|_| false)
}
fn row(rows: &[VendorStatus], id: &str) -> VendorStatus {
rows.iter()
.find(|row| row.id == id)
.unwrap_or_else(|| panic!("{id} is missing from the catalog"))
.clone()
}
#[test]
fn every_provider_has_exactly_one_row_in_canonical_order() {
let rows = statuses_with(&Config::default(), &bare());
let ids: Vec<&str> = rows.iter().map(|row| row.id).collect();
let expected: Vec<&str> = VendorId::all().iter().map(|id| id.slug()).collect();
assert_eq!(ids, expected);
}
#[test]
fn a_key_vendor_is_configured_by_its_environment_variable() {
let cfg = Config::default();
let set = |name: &str| name == "ZAI_API_KEY";
let rows = statuses_with(&cfg, &probes(&set, &|_| false));
assert!(row(&rows, "zai").configured);
assert!(!row(&rows, "deepseek").configured);
}
#[test]
fn an_api_key_env_override_is_the_variable_both_reported_and_read() {
let mut cfg = Config::default();
cfg.zai.api_key_env = "WORK_ZAI_KEY".to_string();
let set = |name: &str| name == "WORK_ZAI_KEY";
let rows = statuses_with(&cfg, &probes(&set, &|_| false));
let zai = row(&rows, "zai");
assert_eq!(
zai.env, "WORK_ZAI_KEY",
"the row names the effective variable"
);
assert!(zai.configured, "and is satisfied by it, not by the default");
let stale = |name: &str| name == "ZAI_API_KEY";
let rows = statuses_with(&cfg, &probes(&stale, &|_| false));
assert!(!row(&rows, "zai").configured);
}
#[test]
fn an_inline_key_configures_without_the_environment() {
let mut cfg = Config::default();
cfg.zai.api_key = Some("sk-inline".to_string());
let rows = statuses_with(&cfg, &bare());
assert!(row(&rows, "zai").configured);
}
#[test]
fn an_empty_inline_key_is_not_a_credential() {
let mut cfg = Config::default();
cfg.zai.api_key = Some(String::new());
let rows = statuses_with(&cfg, &bare());
assert!(!row(&rows, "zai").configured);
}
#[test]
fn antigravity_has_nothing_to_configure() {
let rows = statuses_with(&Config::default(), &bare());
let agy = row(&rows, "antigravity");
assert!(!agy.needs_credential);
assert!(agy.configured);
assert_eq!(agy.env, "");
assert_eq!(agy.login, "");
}
#[test]
fn a_keychain_only_claude_login_counts_as_configured() {
let cfg = Config::default();
let with_keychain = Probes {
env_set: &|_| false,
exists: &|_| false,
keychain_has_claude: &|| true,
};
assert!(row(&statuses_with(&cfg, &with_keychain), "anthropic").configured);
assert!(!row(&statuses_with(&cfg, &bare()), "anthropic").configured);
}
#[test]
fn an_oauth_provider_with_no_artifact_names_the_command_that_fixes_it() {
let rows = statuses_with(&Config::default(), &bare());
let codex = row(&rows, "openai");
assert_eq!(codex.kind, AuthKind::Oauth);
assert!(!codex.configured);
assert_eq!(codex.login, "codex login");
}
#[test]
fn enabled_follows_config_not_the_credential() {
let mut cfg = Config::default();
cfg.zai.enabled = false;
let set = |name: &str| name == "ZAI_API_KEY";
let zai = row(&statuses_with(&cfg, &probes(&set, &|_| false)), "zai");
assert!(!zai.enabled, "switched off in config");
assert!(zai.configured, "but its key is still there");
}
#[test]
fn every_key_provider_names_a_variable_and_every_oauth_one_a_login() {
let cfg = Config::default();
for row in statuses_with(&cfg, &bare()) {
match row.kind {
AuthKind::ApiKey => assert!(
!row.env.is_empty(),
"{} authenticates by key but names no variable",
row.id
),
AuthKind::Oauth => assert!(
!row.login.is_empty(),
"{} authenticates by login but names no command",
row.id
),
AuthKind::Local => {}
}
}
}
#[test]
fn the_settings_key_form_covers_only_catalog_key_providers() {
for kv in KEY_VENDORS {
assert_eq!(
kv.id.auth_kind(),
AuthKind::ApiKey,
"{} has a key field in Settings but is not a key provider",
kv.id.slug()
);
assert!(
!kv.id.api_key_env().is_empty(),
"{} has a key field in Settings but names no variable",
kv.id.slug()
);
}
}
#[test]
fn the_json_document_is_keyed_by_vendors_and_uses_wire_names() {
let rows = statuses_with(&Config::default(), &bare());
let text = serde_json::to_string(&serde_json::json!({"vendors": rows})).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&text).unwrap();
let vendors = parsed["vendors"].as_array().unwrap();
assert_eq!(vendors.len(), VendorId::all().len());
assert_eq!(vendors[0]["id"], "anthropic");
assert_eq!(vendors[0]["kind"], "oauth");
let agy = vendors
.iter()
.find(|v| v["id"] == "antigravity")
.expect("antigravity is in the report");
assert_eq!(agy["kind"], "local");
assert_eq!(agy["needs_credential"], false);
}
}