use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::format::endpoint_label;
use crate::profile::{AppConfig, ClaudeCredentials, Profile, home_dir, load_config};
pub(crate) fn run(json: bool) -> Result<()> {
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 path = credentials_path(config_dir.as_deref())?;
let creds = read_credentials(&path);
let config = load_config()?;
let matched = resolve_profile(
&config,
creds.as_ref(),
config_dir.is_some(),
session_profile.as_deref(),
);
if json {
emit_json(&config, matched);
} else {
emit_plain(matched);
}
Ok(())
}
fn credentials_path(config_dir: Option<&Path>) -> Result<PathBuf> {
match config_dir {
Some(dir) => Ok(dir.join(".credentials.json")),
None => Ok(home_dir()?.join(".claude").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> {
if let Some(name) = creds
.and_then(ClaudeCredentials::refresh_token)
.and_then(|rt| match_by_refresh_token(config, rt))
{
return Some(name);
}
if let Some(profile) = session_profile.and_then(|n| config.find(n)) {
return Some(profile.name.as_str());
}
if in_session {
return None;
}
creds.and_then(|c| match_credential_less_active(config, c))
}
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, matched: Option<&str>) {
let profile = matched.and_then(|n| config.find(n));
let value = serde_json::json!({
"profile": profile.map(|p| &p.name),
"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;