mod actions;
mod claude;
mod claude_json;
mod completions;
mod fallback;
mod format;
mod lock;
mod oauth;
mod platform;
mod profile;
mod runtime;
mod spinner;
mod start;
mod tui;
mod update;
mod ureq_error;
mod usage;
mod which;
use std::sync::{Arc, Mutex};
use anyhow::Result;
use crate::actions::{switch_profile, switch_profile_reconciled};
use crate::claude::{LinkState, classify_credentials_link, is_first_login};
use crate::profile::{AppConfig, load_config};
use crate::spinner::Spinner;
use crate::usage::{ActivityStore, OpResult, RefetchQueue};
fn resolve_or_bail(config: &AppConfig, name: &str) -> Result<String> {
config.canonical_name(name).ok_or_else(|| {
let available = config.names().join(", ");
anyhow::anyhow!("profile '{name}' not found\navailable: {available}")
})
}
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.as_slice() {
[cmd, sub] if cmd == "completions" && sub == "install" => {
return completions::install(None);
}
[cmd, sub, shell] if cmd == "completions" && sub == "install" => {
return completions::install(Some(shell));
}
[cmd, shell] if cmd == "completions" => return completions::print_script(shell),
[cmd] if cmd == "__complete" => {
completions::print_profile_names();
return Ok(());
}
[cmd] if cmd == "--help" || cmd == "-h" => {
print_help();
return Ok(());
}
[cmd] if cmd == "--version" || cmd == "-V" => {
println!("clauth {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
[cmd] if cmd == "which" => return which::run(false),
[cmd, flag] if cmd == "which" && flag == "--json" => return which::run(true),
[cmd, _, ..] if cmd == "which" => {
anyhow::bail!("usage: clauth which [--json]");
}
[cmd] if cmd == "start" => {
anyhow::bail!("usage: clauth start <profile> [claude args...]");
}
[cmd, name, rest @ ..] if cmd == "start" => {
platform::init();
let config = load_config()?;
let canonical = resolve_or_bail(&config, name)?;
return start::run(&config, &canonical, rest);
}
[name] => {
platform::init();
let config = load_config()?;
let canonical = resolve_or_bail(&config, name)?;
let outgoing = config.state.active_profile.clone();
let noop_refetch: RefetchQueue = Arc::new(Mutex::new(std::collections::HashSet::new()));
let noop_activity: ActivityStore =
Arc::new(Mutex::new(std::collections::HashMap::new()));
let (op_sender, _op_receiver) = std::sync::mpsc::channel::<OpResult>();
let config = Arc::new(Mutex::new(config));
{
let _spinner = Spinner::start("clauth: rotating tokens…");
if let Some(ref active) = outgoing
&& active != &canonical
{
oauth::rotate_one(&config, active, &noop_activity, &op_sender);
}
oauth::rotate_one(&config, &canonical, &noop_activity, &op_sender);
}
let reconciled = {
let cfg = config.lock().expect("config mutex poisoned");
if let Some(active) = cfg.state.active_profile.as_deref() {
matches!(classify_credentials_link(active)?, LinkState::Diverged)
&& !is_first_login(active)?
} else {
false
}
};
if reconciled {
let active = {
let cfg = config.lock().expect("config mutex poisoned");
cfg.state
.active_profile
.as_deref()
.unwrap_or("")
.to_string()
};
print!(
"active profile '{active}' has uncaptured credentials in ~/.claude \
(a re-login or token rotation). capture them into '{active}' and \
switch to '{canonical}'? [Y/n] "
);
use std::io::Write;
std::io::stdout().flush()?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
let answer = answer.trim().to_ascii_lowercase();
if answer.is_empty() || answer == "y" || answer == "yes" {
let mut cfg = config.lock().expect("config mutex poisoned");
switch_profile_reconciled(&mut cfg, &canonical)?;
} else {
println!("aborted — no changes made");
return Ok(());
}
} else {
let mut cfg = config.lock().expect("config mutex poisoned");
switch_profile(&mut cfg, &canonical)?;
}
{
let _spinner = Spinner::start("clauth: priming usage window…");
let _ = oauth::auto_start_named(
&config,
&canonical,
&noop_refetch,
&noop_activity,
&op_sender,
);
}
println!("switched to '{canonical}'");
return Ok(());
}
[] => {}
_ => anyhow::bail!(
"usage: clauth [profile] | clauth start <profile> [claude args...] | clauth which [--json] | clauth completions <bash|zsh|fish> | clauth completions install [shell]"
),
}
platform::init();
completions::auto_install_once();
update::spawn();
let config = load_config()?;
tui::run(config)
}
fn print_help() {
println!(
"clauth {ver} — Claude Code account switcher\n\n\
Usage:\n \
clauth launch the TUI\n \
clauth <profile> switch to profile by name and exit\n \
clauth start <profile> [args] launch claude with that profile's settings\n \
in an isolated CLAUDE_CONFIG_DIR; extra args go to claude\n \
clauth which [--json] print the profile owning the loaded\n \
credentials.json (CLAUDE_CONFIG_DIR-aware); `unknown` on no match\n \
clauth completions <shell> print shell completion script (bash|zsh|fish)\n \
clauth completions install [shell]\n \
install completions into the user's shell rc\n \
clauth --version print version\n \
clauth --help show this help",
ver = env!("CARGO_PKG_VERSION"),
);
}