use crate::pricing::Provider;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
pub static HOME: LazyLock<PathBuf> = LazyLock::new(|| {
if running_as_root()
&& let Some(home) = std::fs::read_to_string("/etc/passwd")
.ok()
.and_then(|text| passwd_home_of(&text, 0))
{
return home;
}
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
});
fn own_dir(dir: Option<PathBuf>) -> Option<PathBuf> {
dir.filter(|d| !foreign_to_root(d))
}
pub fn env_dir(var: &str) -> Option<PathBuf> {
own_dir(std::env::var_os(var).map(PathBuf::from))
}
pub fn config_base() -> PathBuf {
own_dir(dirs::config_dir()).unwrap_or_else(|| HOME.join(".config"))
}
pub fn data_base() -> PathBuf {
own_dir(dirs::data_dir()).unwrap_or_else(|| HOME.join(".local").join("share"))
}
fn cache_base() -> PathBuf {
own_dir(dirs::cache_dir()).unwrap_or_else(|| HOME.join(".cache"))
}
pub fn runtime_base() -> PathBuf {
#[cfg(test)]
if let Some(dir) = test_runtime_base() {
return dir;
}
own_dir(dirs::runtime_dir())
.filter(|dir| writable_dir(dir))
.unwrap_or_else(cache_base)
}
#[cfg(test)]
pub struct RuntimeBase {
previous: Option<PathBuf>,
#[allow(dead_code)]
dir: PathBuf,
}
#[cfg(test)]
thread_local! {
static TEST_RUNTIME_BASE: std::cell::RefCell<Option<PathBuf>> = const { std::cell::RefCell::new(None) };
}
#[cfg(test)]
static TEST_RUNTIME_ROOT: LazyLock<PathBuf> =
LazyLock::new(|| std::env::temp_dir().join(format!("cctop-runtime-{}", std::process::id())));
#[cfg(test)]
fn test_runtime_base() -> Option<PathBuf> {
let claimed = TEST_RUNTIME_BASE.with(|dir| dir.borrow().clone());
if claimed.is_none() {
let _ = std::fs::create_dir_all(&*TEST_RUNTIME_ROOT);
}
Some(claimed.unwrap_or_else(|| TEST_RUNTIME_ROOT.clone()))
}
#[cfg(test)]
pub fn claim_test_runtime_base(name: &str) -> RuntimeBase {
let dir = TEST_RUNTIME_ROOT.join(name);
std::fs::create_dir_all(&dir).expect("a runtime directory for this test");
let previous = TEST_RUNTIME_BASE.with(|claimed| claimed.replace(Some(dir.clone())));
RuntimeBase { previous, dir }
}
#[cfg(test)]
impl Drop for RuntimeBase {
fn drop(&mut self) {
TEST_RUNTIME_BASE.with(|claimed| *claimed.borrow_mut() = self.previous.take());
}
}
fn writable_dir(dir: &std::path::Path) -> bool {
use std::os::unix::ffi::OsStrExt;
let Ok(path) = std::ffi::CString::new(dir.as_os_str().as_bytes()) else {
return false;
};
dir.is_dir() && unsafe { libc::access(path.as_ptr(), libc::W_OK | libc::X_OK) } == 0
}
pub static CLAUDE_CONFIG_DIR: LazyLock<PathBuf> =
LazyLock::new(|| env_dir("CLAUDE_CONFIG_DIR").unwrap_or_else(|| claude_config_dir_in(&HOME)));
pub static CLAUDE_PROJECTS_ROOT: LazyLock<PathBuf> =
LazyLock::new(|| CLAUDE_CONFIG_DIR.join("projects"));
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Profile {
pub provider: Provider,
pub name: String,
pub dir: PathBuf,
pub source: AccountSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountSource {
Directory,
Token,
}
const PROFILED: [(Provider, &str, &str); 2] = [
(Provider::Claude, ".claude", ".credentials.json"),
(Provider::Codex, ".codex", "auth.json"),
];
fn conventions(provider: Provider) -> Option<(&'static str, &'static str)> {
PROFILED
.iter()
.find(|(p, _, _)| *p == provider)
.map(|(_, prefix, credential)| (*prefix, *credential))
}
pub fn profile_env(provider: Provider) -> Option<(&'static str, &'static Path)> {
match provider {
Provider::Claude => Some(("CLAUDE_CONFIG_DIR", CLAUDE_CONFIG_DIR.as_path())),
Provider::Codex => Some(("CODEX_HOME", CODEX_HOME.as_path())),
_ => None,
}
}
pub fn argv_under_profile(argv: Vec<String>, profile: &Profile) -> Vec<String> {
if profile.source == AccountSource::Token {
let exe = std::env::current_exe()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| "cctop".to_string());
let mut out = vec![exe, "as".to_string(), profile.name.clone()];
out.extend(argv);
return out;
}
let Some((var, inherited)) = profile_env(profile.provider) else {
return argv;
};
if profile.dir == inherited {
return argv;
}
let mut out = vec![
"env".to_string(),
format!("{var}={}", profile.dir.display()),
];
out.extend(argv);
out
}
pub fn without_launch_prefix(argv: &[String]) -> &[String] {
let mut rest = argv;
let is_cctop = |a: &String| {
a.rsplit(['/', '\\'])
.next()
.is_some_and(|base| base.starts_with("cctop"))
};
if rest.len() > 3 && is_cctop(&rest[0]) && rest[1] == "as" {
rest = &rest[3..];
}
if rest.first().map(String::as_str) == Some("env") {
let mut after = &rest[1..];
while after
.first()
.is_some_and(|a| a.contains('=') && !a.starts_with('-'))
{
after = &after[1..];
}
if !after.is_empty() {
rest = after;
}
}
rest
}
fn profile_name(dir: &Path, prefix: &str) -> String {
let raw = dir
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
match raw.strip_prefix(prefix) {
Some("") => "default".to_string(),
Some(rest) => rest.trim_start_matches(['-', '_', '.']).to_string(),
None => raw,
}
}
pub fn profiles_in(home: &Path, provider: Provider) -> Vec<Profile> {
let Some((prefix, credential)) = conventions(provider) else {
return Vec::new();
};
let mut out: Vec<Profile> = list_dir(home)
.into_iter()
.filter(|name| name.starts_with(prefix))
.map(|name| home.join(name))
.filter(|dir| dir.join(credential).is_file())
.map(|dir| Profile {
provider,
name: profile_name(&dir, prefix),
dir,
source: AccountSource::Directory,
})
.collect();
out.sort_by(|a, b| (a.name != "default", &a.name).cmp(&(b.name != "default", &b.name)));
out
}
fn discover_mine() -> Vec<Profile> {
let mut out = Vec::new();
for (provider, prefix, _) in PROFILED {
let mut found = profiles_in(&HOME, provider);
if let Some((_, named)) = profile_env(provider)
&& !found.iter().any(|p| p.dir == named)
{
found.insert(
0,
Profile {
provider,
name: profile_name(named, prefix),
dir: named.to_path_buf(),
source: AccountSource::Directory,
},
);
}
out.extend(found);
}
out
}
fn discover_others() -> Vec<Profile> {
let mut out = Vec::new();
for other in OTHER_HOMES.iter() {
for (provider, _, _) in PROFILED {
out.extend(profiles_in(&other.home, provider));
}
}
out
}
#[derive(Clone, Copy)]
struct Found {
mine: &'static [Profile],
others: &'static [Profile],
}
impl Found {
fn in_view(self) -> impl Iterator<Item = &'static Profile> {
self.mine.iter().chain(self.others)
}
}
static FOUND: LazyLock<std::sync::RwLock<Found>> = LazyLock::new(|| {
std::sync::RwLock::new(Found {
mine: Box::leak(discover_mine().into_boxed_slice()),
others: Box::leak(discover_others().into_boxed_slice()),
})
});
fn found() -> Found {
*FOUND
.read()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn refresh_profiles() {
let mine = discover_mine();
let others = discover_others();
let mut found = FOUND
.write()
.unwrap_or_else(|poisoned| poisoned.into_inner());
adopt(&mut found.mine, mine);
adopt(&mut found.others, others);
}
fn adopt(slot: &mut &'static [Profile], fresh: Vec<Profile>) -> bool {
let mut next: Vec<Profile> = slot.iter().filter(|p| fresh.contains(p)).cloned().collect();
for profile in fresh {
if !next.contains(&profile) {
next.push(profile);
}
}
if next.as_slice() == *slot {
return false;
}
*slot = Box::leak(next.into_boxed_slice());
true
}
pub fn profile_named(provider: Provider, name: &str) -> Option<&'static Profile> {
found()
.mine
.iter()
.find(|p| p.provider == provider && p.name == name)
}
pub fn profiles_for(provider: Provider) -> Vec<&'static Profile> {
found()
.mine
.iter()
.filter(|p| p.provider == provider)
.collect()
}
pub fn accounts_for(provider: Provider) -> Vec<Profile> {
refresh_profiles();
let mut out: Vec<Profile> = profiles_for(provider).into_iter().cloned().collect();
if provider != Provider::Claude {
return out;
}
for name in token_account_names() {
if out.iter().any(|p| p.name == name) {
continue;
}
out.push(Profile {
provider,
name,
dir: CLAUDE_CONFIG_DIR.clone(),
source: AccountSource::Token,
});
}
out
}
pub fn launchable_for(provider: Provider) -> Vec<&'static Profile> {
if LAUNCHABLE.lock().map(|l| l.is_empty()).unwrap_or(false) {
refresh_launchable();
}
LAUNCHABLE
.lock()
.map(|l| {
l.iter()
.copied()
.filter(|p| p.provider == provider)
.collect()
})
.unwrap_or_default()
}
pub fn refresh_launchable() {
let Ok(mut known) = LAUNCHABLE.lock() else {
return;
};
for provider in PROFILED.map(|(provider, _, _)| provider) {
for account in accounts_for(provider) {
if !known.iter().any(|p| **p == account) {
known.push(Box::leak(Box::new(account)));
}
}
}
}
static LAUNCHABLE: std::sync::Mutex<Vec<&'static Profile>> = std::sync::Mutex::new(Vec::new());
pub fn claude_login_dir(name: &str) -> PathBuf {
HOME.join(format!(".claude-{name}"))
}
fn token_account_names() -> Vec<String> {
let Ok(text) = std::fs::read_to_string(&*CONFIG_FILE) else {
return Vec::new();
};
account_names_in(&text)
}
fn account_names_in(text: &str) -> Vec<String> {
let Ok(doc) = text.parse::<toml_edit::DocumentMut>() else {
return Vec::new();
};
let Some(accounts) = doc.get("accounts").and_then(|a| a.as_table_like()) else {
return Vec::new();
};
let mut out: Vec<String> = accounts
.iter()
.filter(|(_, item)| {
item.as_table_like()
.and_then(|t| t.get("token"))
.and_then(|t| t.as_str())
.is_some_and(|t| !t.trim().is_empty())
})
.map(|(name, _)| name.to_string())
.collect();
out.sort();
out
}
pub static CONFIG_FILE: LazyLock<PathBuf> =
LazyLock::new(|| config_base().join("cctop").join("config.toml"));
pub static CODEX_HOME: LazyLock<PathBuf> =
LazyLock::new(|| env_dir("CODEX_HOME").unwrap_or_else(|| HOME.join(".codex")));
pub static CODEX_SESSIONS_ROOT: LazyLock<PathBuf> = LazyLock::new(|| CODEX_HOME.join("sessions"));
pub static CURSOR_HOME: LazyLock<PathBuf> =
LazyLock::new(|| env_dir("CURSOR_HOME").unwrap_or_else(|| HOME.join(".cursor")));
pub static CURSOR_PROJECTS_ROOT: LazyLock<PathBuf> = LazyLock::new(|| CURSOR_HOME.join("projects"));
pub static PI_AGENT_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
env_dir("PI_CODING_AGENT_DIR").unwrap_or_else(|| HOME.join(".pi").join("agent"))
});
pub static PI_SESSIONS_ROOT: LazyLock<PathBuf> = LazyLock::new(|| {
env_dir("PI_CODING_AGENT_SESSION_DIR").unwrap_or_else(|| PI_AGENT_DIR.join("sessions"))
});
pub static GEMINI_HOME: LazyLock<PathBuf> =
LazyLock::new(|| env_dir("GEMINI_DIR").unwrap_or_else(|| HOME.join(".gemini")));
pub static GEMINI_CHATS_ROOT: LazyLock<PathBuf> = LazyLock::new(|| GEMINI_HOME.join("tmp"));
pub static WINDSURF_USER_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
if let Some(dir) = env_dir("WINDSURF_USER_DIR") {
return dir;
}
config_base().join("Windsurf").join("User")
});
pub static WINDSURF_WORKSPACE_STORAGE: LazyLock<PathBuf> =
LazyLock::new(|| WINDSURF_USER_DIR.join("workspaceStorage"));
pub static OPENCODE_DATA_DIR: LazyLock<PathBuf> =
LazyLock::new(|| env_dir("OPENCODE_DATA_DIR").unwrap_or_else(|| data_base().join("opencode")));
pub static DEVIN_CLI_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
env_dir("CHISEL_SESSION_DB")
.and_then(|db_path| db_path.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| data_base().join("devin").join("cli"))
});
pub static DEVIN_CONFIG_DIR: LazyLock<PathBuf> = LazyLock::new(|| config_base().join("devin"));
pub static DEVIN_SESSIONS_DB: LazyLock<PathBuf> =
LazyLock::new(|| DEVIN_CLI_DIR.join("sessions.db"));
pub static DEVIN_TRANSCRIPTS_DIR: LazyLock<PathBuf> =
LazyLock::new(|| DEVIN_CLI_DIR.join("transcripts"));
pub static OPENCODE_CONFIG_DIR: LazyLock<PathBuf> = LazyLock::new(|| {
env_dir("OPENCODE_CONFIG_DIR").unwrap_or_else(|| config_base().join("opencode"))
});
pub static CLAUDE_MAC_COWORK_ROOT: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
cfg!(target_os = "macos").then(|| {
HOME.join("Library")
.join("Application Support")
.join("Claude")
.join("local-agent-mode-sessions")
})
});
pub static CLAUDE_MAC_CODE_ROOT: LazyLock<Option<PathBuf>> = LazyLock::new(|| {
cfg!(target_os = "macos").then(|| {
HOME.join("Library")
.join("Application Support")
.join("Claude")
.join("claude-code-sessions")
})
});
pub static CACHE_DIR: LazyLock<PathBuf> = LazyLock::new(|| cache_base().join("cctop"));
pub static COST_CACHE_FILE: LazyLock<PathBuf> = LazyLock::new(|| CACHE_DIR.join("cost-cache.json"));
pub static PRICING_CACHE_FILE: LazyLock<PathBuf> =
LazyLock::new(|| CACHE_DIR.join("litellm-pricing.json"));
pub static UI_PREFS_FILE: LazyLock<PathBuf> = LazyLock::new(|| CACHE_DIR.join("ui-prefs.json"));
pub static EMBEDDING_INDEX_FILE: LazyLock<PathBuf> =
LazyLock::new(|| CACHE_DIR.join("embeddings.bin"));
pub static EMBEDDING_MODEL_DIR: LazyLock<PathBuf> = LazyLock::new(|| CACHE_DIR.join("models"));
pub static BURN_LOG_FILE: LazyLock<PathBuf> = LazyLock::new(|| CACHE_DIR.join("burn-log.json"));
pub const LITELLM_URL: &str =
"https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json";
pub const PRICING_CACHE_MAX_AGE_SECS: u64 = 24 * 60 * 60;
pub static COMPACT_THRESHOLD: LazyLock<f64> = LazyLock::new(|| {
std::env::var("CLAUDE_AUTOCOMPACT_PCT_OVERRIDE")
.ok()
.and_then(|v| v.parse::<f64>().ok())
.map(|p| p / 100.0)
.or_else(|| crate::settings::Settings::load().compact_threshold)
.unwrap_or(0.835)
});
pub const MAX_JSONL_LINE_BYTES: usize = 512 * 1024;
pub const MAX_TOOL_DETAILS: usize = 200;
pub const MAX_SESSION_TOOL_DETAILS: usize = 400;
pub const MAX_TOOL_DETAIL_CHARS: usize = 800;
pub const MAX_DIFF_LINES: usize = 60;
pub const MAX_DIFF_LINE_CHARS: usize = 300;
#[derive(Debug, Clone)]
pub struct OtherHome {
pub home: PathBuf,
pub user: String,
pub uid: Option<u32>,
}
fn all_users_wanted() -> bool {
match std::env::var("CCTOP_ALL_USERS") {
Ok(v) => !matches!(
v.trim().to_ascii_lowercase().as_str(),
"" | "0" | "false" | "no"
),
Err(_) => running_as_root(),
}
}
fn named_homes() -> Vec<(PathBuf, String)> {
let Some(list) = std::env::var_os("CCTOP_HOMES") else {
return Vec::new();
};
std::env::split_paths(&list)
.filter(|p| !p.as_os_str().is_empty())
.map(|path| {
let user = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
(path, user)
})
.collect()
}
fn running_as_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
static PEOPLES_HOMES: LazyLock<Vec<OtherHome>> = LazyLock::new(|| {
let mut seen: HashSet<PathBuf> = HashSet::from([HOME.clone()]);
let mut out = Vec::new();
if let Ok(passwd) = std::fs::read_to_string("/etc/passwd") {
for (home, user) in passwd_homes(&passwd) {
push_home(&mut out, &mut seen, home, user);
}
}
let parent = Path::new("/home");
for name in list_dir(parent) {
push_home(&mut out, &mut seen, parent.join(&name), name);
}
out
});
pub static OTHER_HOMES: LazyLock<Vec<OtherHome>> = LazyLock::new(|| {
let named = named_homes();
if named.is_empty() && !all_users_wanted() {
return Vec::new();
}
let mut seen: HashSet<PathBuf> = HashSet::from([HOME.clone()]);
let mut out = Vec::new();
for (home, user) in named {
push_home(&mut out, &mut seen, home, user);
}
if all_users_wanted() {
for other in PEOPLES_HOMES.iter() {
if seen.insert(other.home.clone()) {
out.push(other.clone());
}
}
}
out
});
fn foreign_to_root(path: &Path) -> bool {
running_as_root()
&& !path.starts_with(&*HOME)
&& PEOPLES_HOMES.iter().any(|o| path.starts_with(&o.home))
}
const UID_MIN: u32 = 1000;
fn passwd_entries(text: &str) -> impl Iterator<Item = (&str, u32, &str)> {
text.lines().filter_map(|line| {
let fields: Vec<&str> = line.split(':').collect();
let [user, _, uid, _, _, home, ..] = fields[..] else {
return None;
};
let uid: u32 = uid.parse().ok()?;
(!user.is_empty() && !home.is_empty()).then_some((user, uid, home))
})
}
fn passwd_homes(text: &str) -> Vec<(PathBuf, String)> {
passwd_entries(text)
.filter(|(_, uid, home)| *uid == 0 || *uid >= UID_MIN || home.starts_with("/home/"))
.map(|(user, _, home)| (PathBuf::from(home), user.to_string()))
.collect()
}
fn passwd_home_of(text: &str, uid: u32) -> Option<PathBuf> {
passwd_entries(text)
.find(|(_, u, _)| *u == uid)
.map(|(_, _, home)| PathBuf::from(home))
}
fn passwd_name_of(text: &str, uid: u32) -> Option<String> {
passwd_entries(text)
.find(|(_, u, _)| *u == uid)
.map(|(user, _, _)| user.to_string())
}
fn push_home(out: &mut Vec<OtherHome>, seen: &mut HashSet<PathBuf>, home: PathBuf, user: String) {
use std::os::unix::fs::MetadataExt;
if home.parent().is_none() || seen.contains(&home) {
return;
}
let Ok(meta) = std::fs::metadata(&home) else {
return;
};
if !meta.is_dir() {
return;
}
seen.insert(home.clone());
out.push(OtherHome {
home,
user,
uid: Some(meta.uid()),
});
}
pub static MY_USER: LazyLock<String> = LazyLock::new(|| {
let uid = unsafe { libc::getuid() };
std::fs::read_to_string("/etc/passwd")
.ok()
.and_then(|text| passwd_name_of(&text, uid))
.or_else(|| std::env::var("USER").ok().filter(|u| !u.is_empty()))
.unwrap_or_else(|| format!("uid {uid}"))
});
pub fn user_label(owner: Option<&str>) -> &str {
owner.unwrap_or(MY_USER.as_str())
}
pub fn owner_for_uid(uid: u32) -> Option<String> {
owner_among(uid, unsafe { libc::getuid() }, &OTHER_HOMES)
}
pub fn owner_among(uid: u32, me: u32, homes: &[OtherHome]) -> Option<String> {
if uid == me {
return None;
}
Some(
homes
.iter()
.find(|o| o.uid == Some(uid))
.map(|o| o.user.clone())
.unwrap_or_else(|| format!("uid {uid}")),
)
}
pub fn roots_across_homes(primary: &Path, derive: impl Fn(&Path) -> PathBuf) -> Vec<PathBuf> {
let mut roots = vec![primary.to_path_buf()];
roots.extend(OTHER_HOMES.iter().map(|o| derive(&o.home)));
roots
}
pub fn claude_config_dir_in(home: &Path) -> PathBuf {
home.join(".claude")
}
fn profile_roots(provider: Provider, leaf: &str) -> Vec<PathBuf> {
refresh_profiles();
let found = found();
let mut roots: Vec<PathBuf> = found
.mine
.iter()
.filter(|p| p.provider == provider)
.map(|p| p.dir.join(leaf))
.collect();
for other in OTHER_HOMES.iter() {
let theirs: Vec<&Profile> = found
.others
.iter()
.filter(|p| p.provider == provider && p.dir.parent() == Some(other.home.as_path()))
.collect();
match theirs.as_slice() {
[] => {
if let Some((prefix, _)) = conventions(provider) {
roots.push(other.home.join(prefix).join(leaf));
}
}
found => roots.extend(found.iter().map(|p| p.dir.join(leaf))),
}
}
roots.dedup();
roots
}
pub fn claude_projects_roots() -> Vec<PathBuf> {
profile_roots(Provider::Claude, "projects")
}
pub fn codex_sessions_roots() -> Vec<PathBuf> {
profile_roots(Provider::Codex, "sessions")
}
pub fn cursor_projects_roots() -> Vec<PathBuf> {
roots_across_homes(&CURSOR_PROJECTS_ROOT, |h| {
h.join(".cursor").join("projects")
})
}
pub fn pi_sessions_roots() -> Vec<PathBuf> {
roots_across_homes(&PI_SESSIONS_ROOT, |h| {
h.join(".pi").join("agent").join("sessions")
})
}
pub fn gemini_chats_roots() -> Vec<PathBuf> {
roots_across_homes(&GEMINI_CHATS_ROOT, |h| h.join(".gemini").join("tmp"))
}
pub fn devin_cli_dirs() -> Vec<PathBuf> {
roots_across_homes(&DEVIN_CLI_DIR, |h| data_dir_in(h).join("devin").join("cli"))
}
fn data_dir_in(home: &Path) -> PathBuf {
home.join(".local").join("share")
}
fn config_dir_in(home: &Path) -> PathBuf {
home.join(".config")
}
pub fn opencode_data_roots() -> Vec<PathBuf> {
roots_across_homes(&OPENCODE_DATA_DIR, |h| data_dir_in(h).join("opencode"))
}
pub fn windsurf_workspace_roots() -> Vec<PathBuf> {
roots_across_homes(&WINDSURF_WORKSPACE_STORAGE, |h| {
config_dir_in(h)
.join("Windsurf")
.join("User")
.join("workspaceStorage")
})
}
pub fn claude_mac_roots(primary: &Option<PathBuf>, leaf: &str) -> Vec<PathBuf> {
let Some(primary) = primary.as_ref() else {
return Vec::new();
};
roots_across_homes(primary, |h| {
h.join("Library")
.join("Application Support")
.join("Claude")
.join(leaf)
})
}
pub fn owner_of(path: &Path) -> Option<&'static str> {
OTHER_HOMES
.iter()
.find(|o| path.starts_with(&o.home))
.map(|o| o.user.as_str())
}
pub fn profile_for(path: &Path) -> Option<&'static str> {
found()
.in_view()
.filter(|p| path.starts_with(&p.dir))
.max_by_key(|p| p.dir.as_os_str().len())
.map(|p| p.name.as_str())
}
pub fn profile_count() -> usize {
PROFILED
.iter()
.map(|(provider, _, _)| {
found()
.in_view()
.filter(|p| p.provider == *provider)
.count()
})
.max()
.unwrap_or(0)
}
pub const CLAUDE_DEFAULT_CTX: u64 = 200_000;
pub const CLAUDE_1M_CTX: u64 = 1_000_000;
pub const CODEX_DEFAULT_CTX: u64 = 258_400;
pub fn is_full_uuid(s: &str) -> bool {
let b = s.as_bytes();
if b.len() != 36 {
return false;
}
for (i, c) in b.iter().enumerate() {
match i {
8 | 13 | 18 | 23 => {
if *c != b'-' {
return false;
}
}
_ => {
if !c.is_ascii_hexdigit() || c.is_ascii_uppercase() {
return false;
}
}
}
}
true
}
pub fn trailing_uuid(stem: &str) -> Option<&str> {
let tail = stem.get(stem.len().checked_sub(36)?..)?;
is_full_uuid(tail).then_some(tail)
}
pub fn dir_exists(p: &Path) -> bool {
p.is_dir()
}
pub fn list_dir(p: &Path) -> Vec<String> {
let Ok(rd) = std::fs::read_dir(p) else {
return Vec::new();
};
let mut out: Vec<String> = rd
.flatten()
.filter_map(|e| e.file_name().into_string().ok())
.collect();
out.sort();
out
}
pub fn rglob(dir: &Path, ext: &str) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
let Ok(rd) = std::fs::read_dir(&d) else {
continue;
};
for entry in rd.flatten() {
let Ok(ft) = entry.file_type() else { continue };
let path = entry.path();
if ft.is_dir() {
stack.push(path);
} else if ft.is_file() && path.to_string_lossy().ends_with(ext) {
out.push(path);
}
}
}
out
}
pub fn file_mtime_ms(p: &Path) -> u64 {
match std::fs::metadata(p) {
Ok(meta) => mtime_ms_of(&meta),
Err(_) => 0,
}
}
pub fn mtime_ms_of(meta: &std::fs::Metadata) -> u64 {
meta.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
#[test]
fn profiles_are_the_directories_holding_credentials() {
let dir = std::env::temp_dir().join(format!("cctop-prof-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let make = |path: &Path, creds: bool| {
std::fs::create_dir_all(path).unwrap();
if creds {
std::fs::write(path.join(".credentials.json"), "{}").unwrap();
}
};
make(&dir.join(".claude"), true);
make(&dir.join(".claude-work"), true);
make(&dir.join(".claude-empty"), false);
make(&dir.join(".claude-work").join(".claude"), true);
make(&dir.join(".config"), true);
let found = profiles_in(&dir, Provider::Claude);
let names: Vec<&str> = found.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["default", "work"], "{found:?}");
assert_eq!(found[0].dir, dir.join(".claude"));
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_transcript_is_attributed_to_the_profile_it_lives_under() {
let dir = tempfile::tempdir().expect("tempdir");
let home = dir.path();
for name in [".claude", ".claude-work"] {
let profile = home.join(name);
std::fs::create_dir_all(profile.join("projects")).expect("mkdir");
std::fs::write(profile.join(".credentials.json"), "{}").expect("write");
}
let found = profiles_in(home, Provider::Claude);
let named = |path: &Path| -> Option<String> {
found
.iter()
.filter(|p| path.starts_with(&p.dir))
.max_by_key(|p| p.dir.as_os_str().len())
.map(|p| p.name.clone())
};
assert_eq!(
named(&home.join(".claude/projects/repo/a.jsonl")).as_deref(),
Some("default")
);
assert_eq!(
named(&home.join(".claude-work/projects/repo/a.jsonl")).as_deref(),
Some("work")
);
assert_eq!(named(&home.join(".codex/sessions/a.jsonl")), None);
}
#[test]
fn codex_profiles_are_the_directories_holding_auth() {
let dir = tempfile::tempdir().expect("tempdir");
let home = dir.path();
for (name, signed_in) in [
(".codex", true),
(".codex-work", true),
(".codex-old", false),
] {
let profile = home.join(name);
std::fs::create_dir_all(profile.join("sessions")).expect("mkdir");
if signed_in {
std::fs::write(profile.join("auth.json"), "{}").expect("write");
}
}
std::fs::create_dir_all(home.join(".claude")).expect("mkdir");
std::fs::write(home.join(".claude").join(".credentials.json"), "{}").expect("write");
let found = profiles_in(home, Provider::Codex);
let names: Vec<&str> = found.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["default", "work"], "{found:?}");
assert_eq!(found[0].dir, home.join(".codex"));
assert!(found.iter().all(|p| p.provider == Provider::Codex));
let named = |path: &Path| -> Option<String> {
found
.iter()
.filter(|p| path.starts_with(&p.dir))
.max_by_key(|p| p.dir.as_os_str().len())
.map(|p| p.name.clone())
};
assert_eq!(
named(&home.join(".codex-work/sessions/2026/a.jsonl")).as_deref(),
Some("work")
);
assert_eq!(
named(&home.join(".codex/sessions/2026/a.jsonl")).as_deref(),
Some("default")
);
}
#[test]
fn the_profile_column_answers_to_one_harness_holding_two_accounts() {
let dir = tempfile::tempdir().expect("tempdir");
let home = dir.path();
let make = |name: &str, credential: &str| {
let path = home.join(name);
std::fs::create_dir_all(&path).expect("mkdir");
std::fs::write(path.join(credential), "{}").expect("write");
};
make(".claude", ".credentials.json");
make(".codex", "auth.json");
let most = |home: &Path| {
PROFILED
.iter()
.map(|(provider, _, _)| profiles_in(home, *provider).len())
.max()
.unwrap_or(0)
};
assert_eq!(most(home), 1);
make(".codex-work", "auth.json");
assert_eq!(most(home), 2);
}
#[test]
fn the_default_profile_leads_and_the_rest_are_ordered() {
let dir = std::env::temp_dir().join(format!("cctop-prof2-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
for name in [".claude-zeta", ".claude-alpha", ".claude"] {
let path = dir.join(name);
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path.join(".credentials.json"), "{}").unwrap();
}
let names: Vec<String> = profiles_in(&dir, Provider::Claude)
.into_iter()
.map(|p| p.name)
.collect();
assert_eq!(names, ["default", "alpha", "zeta"]);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_profile_made_while_running_is_adopted_in_place() {
let dir = tempfile::tempdir().expect("tempdir");
let home = dir.path();
let login = |name: &str| {
let path = home.join(name);
std::fs::create_dir_all(&path).expect("mkdir");
std::fs::write(path.join(".credentials.json"), "{}").expect("write");
};
let names =
|slot: &[Profile]| -> Vec<String> { slot.iter().map(|p| p.name.clone()).collect() };
login(".claude");
let mut slot: &'static [Profile] = &[];
assert!(adopt(&mut slot, profiles_in(home, Provider::Claude)));
let default: &'static Profile = &slot[0];
assert!(!adopt(&mut slot, profiles_in(home, Provider::Claude)));
login(".claude-work");
login(".claude-alpha");
assert!(adopt(&mut slot, profiles_in(home, Provider::Claude)));
assert_eq!(names(slot), ["default", "alpha", "work"]);
login(".claude-beta");
assert!(adopt(&mut slot, profiles_in(home, Provider::Claude)));
assert_eq!(names(slot), ["default", "alpha", "work", "beta"]);
std::fs::remove_file(home.join(".claude-work").join(".credentials.json")).expect("rm");
assert!(adopt(&mut slot, profiles_in(home, Provider::Claude)));
assert_eq!(names(slot), ["default", "alpha", "beta"]);
assert_eq!(default.name, "default");
assert_eq!(default.dir, home.join(".claude"));
}
#[test]
fn token_accounts_are_the_named_ones_with_a_token() {
let text = "\
[accounts.work]\n\
token = \"sk-ant-oat01-w\"\n\
[accounts.blank]\n\
token = \" \"\n\
[accounts.empty]\n\
[accounts.side]\n\
token = \"sk-ant-oat01-s\"\n";
assert_eq!(account_names_in(text), ["side", "work"]);
assert!(account_names_in("").is_empty());
assert!(account_names_in("not = toml [").is_empty());
assert!(account_names_in("accounts = 3").is_empty());
}
#[test]
fn a_token_account_launches_by_name_not_by_token() {
let argv = vec!["claude".to_string(), "--resume".to_string()];
let token = Profile {
provider: Provider::Claude,
name: "work".into(),
dir: PathBuf::from("/home/x/.claude-work"),
source: AccountSource::Token,
};
let launched = argv_under_profile(argv.clone(), &token);
assert_eq!(launched[1..], ["as", "work", "claude", "--resume"]);
assert!(!launched.iter().any(|a| a.contains("CLAUDE")));
assert_eq!(without_launch_prefix(&launched), argv);
let bare: Vec<String> = ["cctop", "as", "work"].map(String::from).into();
assert_eq!(without_launch_prefix(&bare), bare);
let dir = Profile {
source: AccountSource::Directory,
..token
};
assert_eq!(
argv_under_profile(argv, &dir),
[
"env",
"CLAUDE_CONFIG_DIR=/home/x/.claude-work",
"claude",
"--resume"
]
);
}
#[test]
fn an_unreadable_home_still_offers_the_usual_place() {
let missing = Path::new("/nonexistent-home-cctop");
assert!(profiles_in(missing, Provider::Claude).is_empty());
assert_eq!(
claude_config_dir_in(missing).join("projects"),
missing.join(".claude").join("projects")
);
}
use super::*;
#[test]
fn uuid_validation() {
assert!(is_full_uuid("7026d578-8cba-4880-b464-9700f1b77b71"));
assert!(!is_full_uuid("7026D578-8CBA-4880-B464-9700F1B77B71")); assert!(!is_full_uuid("7026d578-8cba-4880-b464-9700f1b77b7")); assert!(!is_full_uuid("7026d5788cba4880b4649700f1b77b71")); }
#[test]
fn passwd_parsing_takes_the_home_field() {
let homes = passwd_homes(
"root:x:0:0:root:/root:/bin/bash\n\
ana:x:1000:1000:Ana,,,:/home/ana:/bin/zsh\n\
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin\n\
# comment\n\
truncated:x:1001",
);
assert_eq!(
homes,
vec![
(PathBuf::from("/root"), "root".to_string()),
(PathBuf::from("/home/ana"), "ana".to_string()),
]
);
}
#[test]
fn passwd_parsing_keeps_a_low_uid_with_a_home_under_home() {
let homes = passwd_homes(
"old:x:500:500::/home/old:/bin/sh\n\
svc:x:500:500::/srv/svc:/bin/sh\n",
);
assert_eq!(homes, vec![(PathBuf::from("/home/old"), "old".to_string())]);
}
#[test]
fn passwd_answers_a_uid_with_its_home_and_name() {
let text = "ana:x:1000:1000::/home/ana:/bin/zsh\n\
root:x:0:0:root:/var/root:/bin/sh\n";
assert_eq!(passwd_home_of(text, 0), Some(PathBuf::from("/var/root")));
assert_eq!(passwd_name_of(text, 1000).as_deref(), Some("ana"));
assert_eq!(passwd_home_of(text, 42), None);
}
#[test]
fn a_home_records_who_owns_it() {
use std::os::unix::fs::MetadataExt;
let dir = tempfile::tempdir().unwrap();
let mut out = Vec::new();
push_home(
&mut out,
&mut HashSet::new(),
dir.path().to_path_buf(),
"me".into(),
);
let uid = std::fs::metadata(dir.path()).unwrap().uid();
assert_eq!(out[0].uid, Some(uid));
assert_eq!(owner_among(uid, uid, &out), None, "my own process is mine");
assert_eq!(owner_among(uid, uid + 1, &out).as_deref(), Some("me"));
}
#[test]
fn only_root_rejects_an_inherited_directory() {
if running_as_root() {
return;
}
let elsewhere = PathBuf::from("/home/somebody-else/.cache");
assert_eq!(own_dir(Some(elsewhere.clone())), Some(elsewhere));
}
#[test]
fn homes_are_deduped_and_must_exist() {
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("ana");
std::fs::create_dir(&real).unwrap();
let mut out = Vec::new();
let mut seen = HashSet::new();
push_home(&mut out, &mut seen, real.clone(), "ana".into());
push_home(&mut out, &mut seen, real, "ana-again".into());
push_home(&mut out, &mut seen, dir.path().join("gone"), "gone".into());
push_home(&mut out, &mut seen, PathBuf::from("/"), "sync".into());
let users: Vec<&str> = out.iter().map(|o| o.user.as_str()).collect();
assert_eq!(users, ["ana"]);
}
#[test]
fn uuid_extraction() {
let stem = "rollout-2026-06-29T10-59-07-019f1075-3f22-7ad0-b496-73dcda6a7a25";
assert_eq!(
trailing_uuid(stem),
Some("019f1075-3f22-7ad0-b496-73dcda6a7a25")
);
}
#[test]
fn uuid_extraction_survives_a_non_ascii_stem() {
let stem = format!("é{}", "a".repeat(35));
assert_eq!(trailing_uuid(&stem), None);
}
#[test]
fn the_profile_a_launch_would_have_used_anyway_gets_no_prefix() {
let (var, inherited) = profile_env(Provider::Claude).unwrap();
let default = Profile {
provider: Provider::Claude,
name: "default".to_string(),
dir: inherited.to_path_buf(),
source: AccountSource::Directory,
};
assert_eq!(
argv_under_profile(vec!["claude".to_string()], &default),
["claude"]
);
let other = Profile {
dir: inherited.with_file_name(".claude-work"),
..default
};
let argv = argv_under_profile(vec!["claude".to_string()], &other);
assert_eq!(argv[0], "env");
assert!(argv[1].starts_with(&format!("{var}=")), "{argv:?}");
}
}