mod actions;
mod claude;
mod claude_json;
mod completions;
mod fallback;
mod format;
#[cfg(target_os = "macos")]
mod keychain;
mod lock;
mod lockorder;
mod mcp;
mod oauth;
mod oauth_login;
mod platform;
mod plugin_probe;
mod poll;
mod pricing;
mod profile;
mod profile_cache;
mod providers;
mod runtime;
mod spinner;
mod start;
mod status;
mod throughput;
mod tokens;
mod tui;
mod update;
mod usage;
mod which;
#[cfg(test)]
mod testutil;
use anyhow::Result;
use crate::profile::{AppConfig, ThemeName, load_config};
use crate::runtime::Isolation;
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();
dispatch(&args)
}
fn dispatch(args: &[String]) -> Result<()> {
let (theme_override, args) = peel_theme_flag(args);
match args {
[cmd, sub] if cmd == "completions" && sub == "install" => completions::install(None),
[cmd, sub, shell] if cmd == "completions" && sub == "install" => {
completions::install(Some(shell))
}
[cmd, shell] if cmd == "completions" => completions::print_script(shell),
[cmd] if cmd == "__complete" => {
completions::print_profile_names();
Ok(())
}
[cmd] if cmd == "--help" || cmd == "-h" => {
print_help();
Ok(())
}
[cmd] if cmd == "--version" || cmd == "-V" => {
println!("clauth {}", env!("CARGO_PKG_VERSION"));
Ok(())
}
[cmd] if cmd == "which" => which::run(false),
[cmd, flag] if cmd == "which" && flag == "--json" => which::run(true),
[cmd, _, ..] if cmd == "which" => {
anyhow::bail!("usage: clauth which [--json]");
}
[cmd] if cmd == "start" => {
anyhow::bail!("usage: clauth start [--isolated] <profile> [claude args...]");
}
[cmd, flag] if cmd == "start" && flag == "--isolated" => {
anyhow::bail!("usage: clauth start --isolated <profile> [claude args...]");
}
[cmd, flag, name, rest @ ..] if cmd == "start" && flag == "--isolated" => {
cmd_start(name, rest, Isolation::Isolated)
}
[cmd, name, rest @ ..] if cmd == "start" => cmd_start(name, rest, Isolation::Shared),
[cmd, rest @ ..] if cmd == "login" => match parse_login_args(rest) {
Some((name, model)) => cmd_login(name, model),
None => anyhow::bail!("usage: clauth login <profile> [--model <id>]"),
},
[cmd, ..] if cmd == "run" => anyhow::bail!(
"`clauth run` isn't a command — for a headless delegate use \
`clauth start <profile> -p \"<prompt>\"` (or the MCP `delegate` tool)"
),
[cmd] if cmd == "mcp" => mcp::serve(),
[cmd] if cmd == "mcp-await-job" => mcp::await_job(),
[name] => cmd_switch(name),
[] => cmd_tui(theme_override),
_ => anyhow::bail!(
"usage: clauth [profile] | clauth start <profile> [claude args...] | clauth login <profile> [--model <id>] | clauth which [--json] | clauth completions <bash|zsh|fish> | clauth completions install [shell]"
),
}
}
fn peel_theme_flag(args: &[String]) -> (Option<tui::theme::Tier>, &[String]) {
for (i, arg) in args.iter().enumerate() {
if let Some(value) = arg.strip_prefix("--theme=") {
let tier = match value.to_lowercase().as_str() {
"full" => Some(tui::theme::Tier::Full),
"compatible" => Some(tui::theme::Tier::Compatible),
_ => None,
};
if tier.is_some() {
let (_, after) = args.split_at(i);
return (tier, &after[1..]);
}
}
}
(None, args)
}
fn cmd_start(name: &str, rest: &[String], isolation: Isolation) -> Result<()> {
platform::init();
runtime::gc_stale_runtimes();
let config = load_config()?;
let canonical = resolve_or_bail(&config, name)?;
start::run(&config, &canonical, rest, isolation)
}
fn parse_login_args(rest: &[String]) -> Option<(&str, Option<&str>)> {
match rest {
[name] if !name.starts_with("--") => Some((name.as_str(), None)),
[name, flag, value] if flag == "--model" && !name.starts_with("--") => {
Some((name.as_str(), Some(value.as_str())))
}
_ => None,
}
}
#[derive(Debug, PartialEq)]
enum LoginRoute {
New(String),
Reauth(String),
}
fn login_route(config: &AppConfig, raw: &str) -> LoginRoute {
match config.canonical_name(raw.trim()) {
Some(existing) => LoginRoute::Reauth(existing),
None => LoginRoute::New(raw.trim().to_string()),
}
}
fn reauth_confirmed(input: &str) -> bool {
let a = input.trim();
a.eq_ignore_ascii_case("y") || a.eq_ignore_ascii_case("yes")
}
fn cmd_login(name: &str, model: Option<&str>) -> Result<()> {
platform::init();
let mut config = load_config()?;
let route = login_route(&config, name);
let target = match &route {
LoginRoute::Reauth(existing) => existing.clone(),
LoginRoute::New(fresh) => {
actions::validate_profile_name(fresh, &config.names(), None)?;
fresh.clone()
}
};
let reauth = matches!(route, LoginRoute::Reauth(_));
if reauth {
use std::io::{IsTerminal as _, Write as _};
if std::io::stdin().is_terminal() && std::io::stdout().is_terminal() {
print!(
"clauth: profile '{target}' already exists. Re-authenticating replaces its stored credentials. Continue? [y/N] "
);
let _ = std::io::stdout().flush();
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if !reauth_confirmed(&answer) {
println!("clauth: aborted. '{target}' left unchanged.");
return Ok(());
}
}
println!("clauth: re-authenticating existing profile '{target}' — opening a browser…");
} else {
println!("clauth: opening a browser to log in to a new account for '{target}'…");
}
let credentials = oauth_login::login_with(|progress| {
if let oauth_login::LoginProgress::AuthorizeUrl(url) = progress {
println!("\nIf the browser didn't open, visit this URL to authorize:\n{url}\n");
}
})?;
println!(
"clauth: login complete.\n{}",
oauth_login::login_summary(&credentials)
);
let snapshot = actions::CaptureSnapshot {
credentials: Some(credentials),
base_url: None,
api_key: None,
};
if reauth {
actions::overwrite_captured_profile(&mut config, &target, snapshot)?;
} else {
actions::capture_into_profile(&mut config, target.clone(), snapshot)?;
}
if let Some(model) = model {
actions::set_profile_default_model(&mut config, &target, model)?;
}
if reauth {
println!("clauth: re-authenticated '{target}'. Fresh tokens are in place.");
} else {
println!("clauth: captured into profile '{target}'. Switch to it with: clauth {target}");
}
Ok(())
}
fn cmd_switch(name: &str) -> Result<()> {
platform::init();
let config = load_config()?;
let canonical = resolve_or_bail(&config, name)?;
actions::switch_profile_cli(config, &canonical)
}
fn cmd_tui(theme_override: Option<tui::theme::Tier>) -> Result<()> {
platform::init();
runtime::gc_stale_runtimes();
completions::auto_install_once();
let config = load_config()?;
let config_tier = config.state.theme.map(|t| match t {
ThemeName::Full => tui::theme::Tier::Full,
ThemeName::Compatible => tui::theme::Tier::Compatible,
});
tui::theme::init(theme_override.or(config_tier));
tui::run(config)
}
fn print_help() {
println!(
"clauth {ver} — Claude Code account switcher\n\n\
Usage:\n \
clauth [--theme=full|compatible] launch the TUI\n \
clauth <profile> switch to profile by name and exit\n \
clauth start [--isolated] <profile> [args]\n \
launch claude with that profile's settings in a per-profile\n \
CLAUDE_CONFIG_DIR; --isolated injects creds but drops operator\n \
memory/plugins/hooks (run in a clean cwd for a blind session);\n \
extra args go to claude\n \
clauth login <profile> [--model <id>]\n \
add a new account via browser OAuth sign-in and capture it into a\n \
new profile, or re-authenticate an existing one in place (neither\n \
switches to it); --model sets its default model (opus/sonnet/\n \
haiku/opusplan or a full model id)\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\n\n\
Theme:\n \
--theme=full force 24-bit truecolor (default when $COLORTERM=truecolor)\n \
--theme=compatible force xterm-256 palette (safe on all terminals)\n \
Config file: set `theme = \"full\"` in ~/.clauth/profiles.toml",
ver = env!("CARGO_PKG_VERSION"),
);
}
#[cfg(test)]
#[path = "../tests/inline/feature_coverage.rs"]
mod feature_coverage;
#[cfg(test)]
#[path = "../tests/inline/cli.rs"]
mod tests;