use crate::claude_auth::ClaudeAuthMode;
use crate::login::LoginConfig;
use crate::model_catalog::{fetch_provider_catalog, is_credential_rejection};
use crate::subscription::{SubscriptionProvider, all_subscription_readers};
#[must_use]
pub fn login_mode_report(login: &LoginConfig) -> Vec<String> {
let uses_external = login.command != "claude";
let selected = if login.args.iter().any(|argument| argument == "setup-token") {
ClaudeAuthMode::SetupToken
} else {
ClaudeAuthMode::Full
};
let mut lines = Vec::new();
for mode in [ClaudeAuthMode::Full, ClaudeAuthMode::SetupToken] {
let marker = if mode == selected { " (default)" } else { "" };
let availability = if uses_external {
let resolved = resolve_in_path(&login.command);
resolved.map_or_else(
|| {
format!(
"UNAVAILABLE — LOGIN_CLI_COMMAND `{}` is not in PATH",
login.command
)
},
|path| format!("via {}", path.display()),
)
} else {
"available (in-process OAuth)".to_string()
};
lines.push(format!(
"login_mode {:<12}: {availability}{marker}; scopes: {}",
mode.name(),
mode.scopes()
));
}
lines
}
fn resolve_in_path(command: &str) -> Option<std::path::PathBuf> {
let candidate = std::path::Path::new(command);
if candidate.is_absolute() {
return candidate.is_file().then(|| candidate.to_path_buf());
}
std::env::var_os("PATH").and_then(|paths| {
std::env::split_paths(&paths)
.map(|directory| directory.join(command))
.find(|path| path.is_file())
})
}
const fn credential_status(was_expired: bool, still_expired: bool, rejected: bool) -> &'static str {
match (was_expired, still_expired, rejected) {
(_, true, true) => "found, token EXPIRED and REJECTED",
(_, true, false) => "found, token EXPIRED on disk but ACCEPTED upstream",
(true, false, true) => "found, token REJECTED after refresh",
(false, _, true) => "found, token REJECTED",
(true, false, false) => "found, token OK (refreshed in memory)",
(false, false, false) => "found, token OK",
}
}
fn credential_location(
provider: SubscriptionProvider,
origin: crate::platform_keychain::Origin,
path: &std::path::Path,
) -> String {
match origin {
crate::platform_keychain::Origin::Keychain => {
crate::platform_keychain::service_name(provider).map_or_else(
|| String::from("platform keychain"),
|service| format!("keychain {service:?}"),
)
}
crate::platform_keychain::Origin::File => path.display().to_string(),
}
}
pub async fn subscription_catalog_diagnostics(
_active_provider: SubscriptionProvider,
claude_home: &str,
user_home: &str,
data_dir: Option<&std::path::Path>,
) -> bool {
let readers = all_subscription_readers(claude_home, user_home);
let client = reqwest::Client::new();
let token_cache = crate::refresh::TokenCache::new();
if let Some(dir) = data_dir {
token_cache.persist_rejections_in(dir);
}
let now_ms = chrono::Utc::now().timestamp_millis();
let mut catalog_error = false;
for reader in readers {
let provider = reader.provider();
let label = format!("{provider} subscription");
let path = match reader.discover_credential_path() {
Some(path) => path,
None if reader.read_token().is_ok() => reader.home().to_path_buf(),
None => {
println!("{label:<23}: {} (MISSING)", reader.home().display());
continue;
}
};
let (disk_token, origin) = match reader.read_token_from() {
Ok(found) => found,
Err(error) => {
println!("{label:<23}: {} (found, NO TOKEN: {error})", path.display());
println!(
"{:<23}: ERROR (credential is unreadable)",
format!("{provider} catalog")
);
catalog_error = true;
continue;
}
};
let was_expired = disk_token.is_expired(now_ms);
let token = token_cache
.get_fresh(&client, provider, disk_token, now_ms)
.await;
let still_expired = token.is_expired(now_ms);
let catalog = fetch_provider_catalog(&client, provider, &token, None).await;
let rejected = catalog
.as_ref()
.is_err_and(|error| is_credential_rejection(error));
let status = credential_status(was_expired, still_expired, rejected);
let location = credential_location(provider, origin, &path);
println!(
"{label:<23}: {location} ({status}, store: {})",
origin.label()
);
if let Some(error) = token_cache.last_refresh_error(provider) {
println!("{:<23}: {error}", format!("{provider} refresh"));
}
match catalog {
Ok(models) => println!(
"{:<23}: OK ({} live model(s))",
format!("{provider} catalog"),
models.len()
),
Err(error) => {
println!("{:<23}: ERROR ({error})", format!("{provider} catalog"));
catalog_error = true;
}
}
}
catalog_error
}
#[cfg(test)]
mod tests {
use super::*;
fn config(command: &str, args: &[&str]) -> LoginConfig {
LoginConfig {
command: command.to_string(),
args: args.iter().map(|value| (*value).to_string()).collect(),
..LoginConfig::default()
}
}
#[test]
fn both_native_modes_are_reported_available() {
let report = login_mode_report(&config("claude", &[])).join("\n");
assert!(report.contains("login_mode full"), "{report}");
assert!(report.contains("login_mode setup-token"), "{report}");
assert_eq!(
report.matches("available (in-process OAuth)").count(),
2,
"{report}"
);
assert!(report.contains("user:inference"), "{report}");
assert!(report.contains("org:create_api_key"), "{report}");
}
#[test]
fn the_configured_mode_is_marked_as_the_default() {
let full = login_mode_report(&config("claude", &[]));
assert!(full[0].contains("(default)"), "{full:?}");
assert!(!full[1].contains("(default)"), "{full:?}");
let narrow = login_mode_report(&config("claude", &["setup-token"]));
assert!(!narrow[0].contains("(default)"), "{narrow:?}");
assert!(narrow[1].contains("(default)"), "{narrow:?}");
}
#[test]
fn a_missing_external_command_is_reported_unavailable() {
let report = login_mode_report(&config("definitely-not-on-path-98765", &[])).join("\n");
assert!(report.contains("UNAVAILABLE"), "{report}");
assert!(report.contains("definitely-not-on-path-98765"), "{report}");
}
#[test]
fn an_absolute_command_path_is_probed_directly() {
let existing = std::env::current_exe().expect("test binary path");
let report = login_mode_report(&config(&existing.to_string_lossy(), &[])).join("\n");
assert!(report.contains("via "), "{report}");
assert!(!report.contains("UNAVAILABLE"), "{report}");
let missing = login_mode_report(&config("/nonexistent/router/login-cli", &[])).join("\n");
assert!(missing.contains("UNAVAILABLE"), "{missing}");
}
#[test]
fn resolve_in_path_finds_a_real_executable() {
assert!(
resolve_in_path("sh").is_some() || cfg!(windows),
"sh should be resolvable"
);
assert!(resolve_in_path("definitely-not-on-path-98765").is_none());
}
#[test]
fn a_keychain_credential_is_described_by_its_entry() {
let location = credential_location(
SubscriptionProvider::Claude,
crate::platform_keychain::Origin::Keychain,
std::path::Path::new("/Users/someone/.claude/.credentials.json"),
);
if cfg!(target_os = "macos") {
assert_eq!(location, "keychain \"Claude Code-credentials\"");
} else {
assert_eq!(location, "platform keychain");
}
assert!(
!location.contains(".credentials.json"),
"a keychain credential must not be reported as a file: {location}"
);
}
#[test]
fn a_storeless_provider_reports_a_generic_store() {
let location = credential_location(
SubscriptionProvider::Gemini,
crate::platform_keychain::Origin::Keychain,
std::path::Path::new("/home/someone/.gemini/oauth_creds.json"),
);
assert_eq!(location, "platform keychain");
}
#[test]
fn a_file_credential_is_described_by_its_path() {
let location = credential_location(
SubscriptionProvider::Codex,
crate::platform_keychain::Origin::File,
std::path::Path::new("/home/someone/.codex/auth.json"),
);
assert_eq!(location, "/home/someone/.codex/auth.json");
}
#[test]
fn a_refreshed_credential_reads_as_ok() {
assert_eq!(
credential_status(true, false, false),
"found, token OK (refreshed in memory)"
);
assert_eq!(credential_status(false, false, false), "found, token OK");
}
#[test]
fn an_expired_and_refused_credential_says_both() {
let status = credential_status(true, true, true);
assert!(status.contains("EXPIRED"), "{status}");
assert!(status.contains("REJECTED"), "{status}");
}
#[test]
fn an_expired_but_accepted_credential_is_not_condemned() {
let status = credential_status(true, true, false);
assert!(status.contains("ACCEPTED upstream"), "{status}");
assert!(!status.contains("REJECTED"), "{status}");
}
#[test]
fn a_refusal_names_whether_a_refresh_preceded_it() {
assert_eq!(
credential_status(true, false, true),
"found, token REJECTED after refresh"
);
assert_eq!(
credential_status(false, false, true),
"found, token REJECTED"
);
}
}