use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::format::endpoint_label;
use crate::profile::{AppConfig, ClaudeCredentials, Profile, claude_dir, load_config};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Source {
RefreshMatch,
SessionDir,
CredentialLessActive,
}
impl Source {
pub(crate) fn as_str(self) -> &'static str {
match self {
Source::RefreshMatch => "refresh_match",
Source::SessionDir => "session_dir",
Source::CredentialLessActive => "credential_less_active",
}
}
}
pub(crate) fn run(json: bool) -> Result<()> {
let config = load_config()?;
let resolved = resolve_active(&config);
if json {
emit_json(&config, resolved);
} else {
emit_plain(resolved.as_ref().map(|(name, _)| name.as_str()));
}
Ok(())
}
pub(crate) fn resolve_active(config: &AppConfig) -> Option<(String, Source)> {
let config_dir = std::env::var_os("CLAUDE_CONFIG_DIR").map(PathBuf::from);
let session_profile = config_dir
.as_deref()
.and_then(session_profile_from_config_dir);
let creds = credentials_path(config_dir.as_deref())
.ok()
.and_then(|path| read_credentials(&path));
resolve_profile(
config,
creds.as_ref(),
config_dir.is_some(),
session_profile.as_deref(),
)
.map(|(name, source)| (name.to_string(), source))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum SessionAuth {
IsolatedRuntime(String),
IsolatedCustom,
Global,
}
pub(crate) fn session_auth() -> SessionAuth {
match std::env::var_os("CLAUDE_CONFIG_DIR").filter(|d| !d.is_empty()) {
Some(dir) => match session_profile_from_config_dir(Path::new(&dir)) {
Some(name) => SessionAuth::IsolatedRuntime(name),
None => SessionAuth::IsolatedCustom,
},
None => SessionAuth::Global,
}
}
fn credentials_path(config_dir: Option<&Path>) -> Result<PathBuf> {
match config_dir {
Some(dir) => Ok(dir.join(".credentials.json")),
None => Ok(claude_dir()?.join(".credentials.json")),
}
}
fn session_profile_from_config_dir(dir: &Path) -> Option<String> {
if dir.file_name()? != "runtime" {
return None;
}
let profile_dir = dir.parent()?;
if profile_dir.parent()?.file_name()? != "profiles" {
return None;
}
Some(profile_dir.file_name()?.to_str()?.to_string())
}
fn read_credentials(path: &Path) -> Option<ClaudeCredentials> {
let content = std::fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
fn resolve_profile<'a>(
config: &'a AppConfig,
creds: Option<&ClaudeCredentials>,
in_session: bool,
session_profile: Option<&str>,
) -> Option<(&'a str, Source)> {
if let Some(name) = creds
.and_then(ClaudeCredentials::refresh_token)
.and_then(|rt| match_by_refresh_token(config, rt))
{
return Some((name, Source::RefreshMatch));
}
if let Some(profile) = session_profile.and_then(|n| config.find(n)) {
return Some((profile.name.as_str(), Source::SessionDir));
}
if in_session {
return None;
}
creds
.and_then(|c| match_credential_less_active(config, c))
.map(|name| (name, Source::CredentialLessActive))
}
fn match_credential_less_active<'a>(
config: &'a AppConfig,
creds: &ClaudeCredentials,
) -> Option<&'a str> {
creds.refresh_token()?;
let active = config.state.active_profile.as_deref()?;
config
.find(active)
.filter(|p| p.credentials.is_none())
.map(|p| p.name.as_str())
}
fn match_by_refresh_token<'a>(config: &'a AppConfig, refresh_token: &str) -> Option<&'a str> {
let active = config.state.active_profile.as_deref();
let mut fallback = None;
for p in &config.profiles {
if p.refresh_token() != Some(refresh_token) {
continue;
}
if Some(p.name.as_str()) == active {
return Some(p.name.as_str());
}
fallback.get_or_insert(p.name.as_str());
}
fallback
}
fn emit_plain(matched: Option<&str>) {
match matched {
Some(name) => println!("{name}"),
None => println!("unknown"),
}
}
fn emit_json(config: &AppConfig, resolved: Option<(String, Source)>) {
let profile = resolved.as_ref().and_then(|(name, _)| config.find(name));
let value = serde_json::json!({
"profile": profile.map(|p| &p.name),
"source": resolved.as_ref().map(|(_, s)| s.as_str()),
"tier": profile.map(endpoint_label),
"oauth": profile.map(Profile::is_oauth),
"active": profile.is_some_and(|p| config.is_active(&p.name)),
});
println!("{value}");
}
#[cfg(test)]
#[path = "../tests/inline/which.rs"]
mod tests;