use crate::cli::completion::{generate_completion, list_aliases_for_completion};
use crate::cli::{Cli, Commands};
use crate::config::types::{AddCommandParams, ClaudeSettings, StorageMode};
use crate::config::{ConfigStorage, Configuration, EnvironmentConfig, validate_alias_name};
use crate::interactive::{
handle_interactive_selection, launch_claude_with_env, read_input, read_sensitive_input,
};
use anyhow::{Result, anyhow};
use clap::Parser;
use std::fs;
use std::path::Path;
fn parse_storage_mode(store_str: &str) -> Result<StorageMode> {
match store_str.to_lowercase().as_str() {
"env" => Ok(StorageMode::Env),
"config" => Ok(StorageMode::Config),
_ => Err(anyhow!(
"Invalid storage mode '{}'. Use 'env' or 'config'",
store_str
)),
}
}
#[allow(clippy::type_complexity)]
fn parse_config_from_file(
file_path: &str,
) -> Result<(
String,
String,
String,
Option<String>,
Option<String>,
Option<u32>,
Option<u32>,
Option<u32>,
Option<String>,
Option<String>,
Option<String>,
)> {
let file_content = fs::read_to_string(file_path)
.map_err(|e| anyhow!("Failed to read file '{}': {}", file_path, e))?;
let json: serde_json::Value = serde_json::from_str(&file_content)
.map_err(|e| anyhow!("Failed to parse JSON from file '{}': {}", file_path, e))?;
let env = json.get("env").and_then(|v| v.as_object()).ok_or_else(|| {
anyhow!(
"File '{}' does not contain a valid 'env' section",
file_path
)
})?;
let path = Path::new(file_path);
let alias_name = path
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| anyhow!("Invalid file path: {}", file_path))?
.to_string();
let token = env
.get("ANTHROPIC_AUTH_TOKEN")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("Missing ANTHROPIC_AUTH_TOKEN in file '{}'", file_path))?
.to_string();
let url = env
.get("ANTHROPIC_BASE_URL")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("Missing ANTHROPIC_BASE_URL in file '{}'", file_path))?
.to_string();
let model = env
.get("ANTHROPIC_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let small_fast_model = env
.get("ANTHROPIC_SMALL_FAST_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let max_thinking_tokens = env
.get("ANTHROPIC_MAX_THINKING_TOKENS")
.and_then(|v| v.as_u64())
.map(|u| u as u32);
let api_timeout_ms = env
.get("API_TIMEOUT_MS")
.and_then(|v| v.as_u64())
.map(|u| u as u32);
let claude_code_disable_nonessential_traffic = env
.get("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC")
.and_then(|v| v.as_u64())
.map(|u| u as u32);
let anthropic_default_sonnet_model = env
.get("ANTHROPIC_DEFAULT_SONNET_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let anthropic_default_opus_model = env
.get("ANTHROPIC_DEFAULT_OPUS_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let anthropic_default_haiku_model = env
.get("ANTHROPIC_DEFAULT_HAIKU_MODEL")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
Ok((
alias_name,
token,
url,
model,
small_fast_model,
max_thinking_tokens,
api_timeout_ms,
claude_code_disable_nonessential_traffic,
anthropic_default_sonnet_model,
anthropic_default_opus_model,
anthropic_default_haiku_model,
))
}
fn handle_add_command(mut params: AddCommandParams, storage: &mut ConfigStorage) -> Result<()> {
if let Some(file_path) = ¶ms.from_file {
println!("Importing configuration from file: {}", file_path);
let (
file_alias_name,
file_token,
file_url,
file_model,
file_small_fast_model,
file_max_thinking_tokens,
file_api_timeout_ms,
file_claude_disable_nonessential_traffic,
file_sonnet_model,
file_opus_model,
file_haiku_model,
) = parse_config_from_file(file_path)?;
params.alias_name = file_alias_name;
params.token = Some(file_token);
params.url = Some(file_url);
params.model = file_model;
params.small_fast_model = file_small_fast_model;
params.max_thinking_tokens = file_max_thinking_tokens;
params.api_timeout_ms = file_api_timeout_ms;
params.claude_code_disable_nonessential_traffic = file_claude_disable_nonessential_traffic;
params.anthropic_default_sonnet_model = file_sonnet_model;
params.anthropic_default_opus_model = file_opus_model;
params.anthropic_default_haiku_model = file_haiku_model;
println!(
"Configuration '{}' will be imported from file",
params.alias_name
);
}
validate_alias_name(¶ms.alias_name)?;
if storage.get_configuration(¶ms.alias_name).is_some() && !params.force {
eprintln!("Configuration '{}' already exists.", params.alias_name);
eprintln!("Use --force to overwrite or choose a different alias name.");
return Ok(());
}
if params.interactive && params.from_file.is_some() {
anyhow::bail!("Cannot use --interactive mode with --from-file");
}
let final_token = if params.interactive {
if params.token.is_some() || params.token_arg.is_some() {
eprintln!(
"Warning: Token provided via flags/arguments will be ignored in interactive mode"
);
}
read_sensitive_input("Enter API token (sk-ant-xxx): ")?
} else {
match (¶ms.token, ¶ms.token_arg) {
(Some(t), _) => t.clone(),
(None, Some(t)) => t.clone(),
(None, None) => {
anyhow::bail!(
"Token is required. Use -t flag, provide as argument, or use interactive mode with -i"
);
}
}
};
let final_url = if params.interactive {
if params.url.is_some() || params.url_arg.is_some() {
eprintln!(
"Warning: URL provided via flags/arguments will be ignored in interactive mode"
);
}
read_input("Enter API URL (default: https://api.anthropic.com): ")?
} else {
match (¶ms.url, ¶ms.url_arg) {
(Some(u), _) => u.clone(),
(None, Some(u)) => u.clone(),
(None, None) => "https://api.anthropic.com".to_string(),
}
};
let final_url = if final_url.is_empty() {
"https://api.anthropic.com".to_string()
} else {
final_url
};
let final_model = if params.interactive {
if params.model.is_some() {
eprintln!("Warning: Model provided via flags will be ignored in interactive mode");
}
let model_input = read_input("Enter model name (optional, press enter to skip): ")?;
if model_input.is_empty() {
None
} else {
Some(model_input)
}
} else {
params.model
};
let final_small_fast_model = if params.interactive {
if params.small_fast_model.is_some() {
eprintln!(
"Warning: Small fast model provided via flags will be ignored in interactive mode"
);
}
let small_model_input =
read_input("Enter small fast model name (optional, press enter to skip): ")?;
if small_model_input.is_empty() {
None
} else {
Some(small_model_input)
}
} else {
params.small_fast_model
};
let final_max_thinking_tokens = if params.interactive {
if params.max_thinking_tokens.is_some() {
eprintln!(
"Warning: Max thinking tokens provided via flags will be ignored in interactive mode"
);
}
let tokens_input = read_input(
"Enter maximum thinking tokens (optional, press enter to skip, enter 0 to clear): ",
)?;
if tokens_input.is_empty() {
None
} else if let Ok(tokens) = tokens_input.parse::<u32>() {
if tokens == 0 { None } else { Some(tokens) }
} else {
eprintln!("Warning: Invalid max thinking tokens value, skipping");
None
}
} else {
params.max_thinking_tokens
};
let final_api_timeout_ms = if params.interactive {
if params.api_timeout_ms.is_some() {
eprintln!(
"Warning: API timeout provided via flags will be ignored in interactive mode"
);
}
let timeout_input = read_input(
"Enter API timeout in milliseconds (optional, press enter to skip, enter 0 to clear): ",
)?;
if timeout_input.is_empty() {
None
} else if let Ok(timeout) = timeout_input.parse::<u32>() {
if timeout == 0 { None } else { Some(timeout) }
} else {
eprintln!("Warning: Invalid API timeout value, skipping");
None
}
} else {
params.api_timeout_ms
};
let final_claude_code_disable_nonessential_traffic = if params.interactive {
if params.claude_code_disable_nonessential_traffic.is_some() {
eprintln!(
"Warning: Disable nonessential traffic flag provided via flags will be ignored in interactive mode"
);
}
let flag_input = read_input(
"Enter disable nonessential traffic flag (optional, press enter to skip, enter 0 to clear): ",
)?;
if flag_input.is_empty() {
None
} else if let Ok(flag) = flag_input.parse::<u32>() {
if flag == 0 { None } else { Some(flag) }
} else {
eprintln!("Warning: Invalid disable nonessential traffic flag value, skipping");
None
}
} else {
params.claude_code_disable_nonessential_traffic
};
let final_anthropic_default_sonnet_model = if params.interactive {
if params.anthropic_default_sonnet_model.is_some() {
eprintln!(
"Warning: Default Sonnet model provided via flags will be ignored in interactive mode"
);
}
let model_input =
read_input("Enter default Sonnet model name (optional, press enter to skip): ")?;
if model_input.is_empty() {
None
} else {
Some(model_input)
}
} else {
params.anthropic_default_sonnet_model
};
let final_anthropic_default_opus_model = if params.interactive {
if params.anthropic_default_opus_model.is_some() {
eprintln!(
"Warning: Default Opus model provided via flags will be ignored in interactive mode"
);
}
let model_input =
read_input("Enter default Opus model name (optional, press enter to skip): ")?;
if model_input.is_empty() {
None
} else {
Some(model_input)
}
} else {
params.anthropic_default_opus_model
};
let final_anthropic_default_haiku_model = if params.interactive {
if params.anthropic_default_haiku_model.is_some() {
eprintln!(
"Warning: Default Haiku model provided via flags will be ignored in interactive mode"
);
}
let model_input =
read_input("Enter default Haiku model name (optional, press enter to skip): ")?;
if model_input.is_empty() {
None
} else {
Some(model_input)
}
} else {
params.anthropic_default_haiku_model
};
let is_anthropic_official = final_url.contains("api.anthropic.com");
if is_anthropic_official {
if !final_token.starts_with("sk-ant-api03-") {
eprintln!(
"Warning: For official Anthropic API (api.anthropic.com), token should start with 'sk-ant-api03-'"
);
}
} else {
if final_token.starts_with("sk-ant-api03-") {
eprintln!("Warning: Using official Claude token format with non-official API endpoint");
}
}
let config = Configuration {
alias_name: params.alias_name.clone(),
token: final_token,
url: final_url,
model: final_model,
small_fast_model: final_small_fast_model,
max_thinking_tokens: final_max_thinking_tokens,
api_timeout_ms: final_api_timeout_ms,
claude_code_disable_nonessential_traffic: final_claude_code_disable_nonessential_traffic,
anthropic_default_sonnet_model: final_anthropic_default_sonnet_model,
anthropic_default_opus_model: final_anthropic_default_opus_model,
anthropic_default_haiku_model: final_anthropic_default_haiku_model,
claude_code_experimental_agent_teams: None,
claude_code_disable_1m_context: None,
};
storage.add_configuration(config);
storage.save()?;
println!("Configuration '{}' added successfully", params.alias_name);
if params.force {
println!("(Overwrote existing configuration)");
}
Ok(())
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
if cli.migrate {
ConfigStorage::migrate_from_old_path()?;
return Ok(());
}
if cli.list_aliases {
list_aliases_for_completion()?;
return Ok(());
}
if let Some(ref store_str) = cli.store
&& cli.command.is_none()
{
let mode = match parse_storage_mode(store_str) {
Ok(mode) => mode,
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
};
let mut storage = ConfigStorage::load()?;
storage.default_storage_mode = Some(mode.clone());
storage.save()?;
let mode_str = match mode {
StorageMode::Env => "env",
StorageMode::Config => "config",
};
println!("Default storage mode set to: {}", mode_str);
return Ok(());
}
if let Some(command) = cli.command {
let mut storage = ConfigStorage::load()?;
match command {
Commands::Add {
alias_name,
token,
url,
model,
small_fast_model,
max_thinking_tokens,
api_timeout_ms,
claude_code_disable_nonessential_traffic,
anthropic_default_sonnet_model,
anthropic_default_opus_model,
anthropic_default_haiku_model,
force,
interactive,
token_arg,
url_arg,
from_file,
} => {
let final_alias_name = if from_file.is_some() {
"placeholder".to_string()
} else {
alias_name.unwrap_or_else(|| {
eprintln!("Error: alias_name is required when not using --from-file");
std::process::exit(1);
})
};
let params = AddCommandParams {
alias_name: final_alias_name,
token,
url,
model,
small_fast_model,
max_thinking_tokens,
api_timeout_ms,
claude_code_disable_nonessential_traffic,
anthropic_default_sonnet_model,
anthropic_default_opus_model,
anthropic_default_haiku_model,
force,
interactive,
token_arg,
url_arg,
from_file,
};
handle_add_command(params, &mut storage)?;
}
Commands::Remove { alias_names } => {
let mut removed_count = 0;
let mut not_found_aliases = Vec::new();
for alias_name in &alias_names {
if storage.remove_configuration(alias_name) {
removed_count += 1;
println!("Configuration '{alias_name}' removed successfully");
} else {
not_found_aliases.push(alias_name.clone());
println!("Configuration '{alias_name}' not found");
}
}
if removed_count > 0 {
storage.save()?;
}
if !not_found_aliases.is_empty() {
eprintln!(
"Warning: The following configurations were not found: {}",
not_found_aliases.join(", ")
);
}
if removed_count > 0 {
println!("Successfully removed {removed_count} configuration(s)");
}
}
Commands::List { plain } => {
if plain {
if storage.configurations.is_empty() {
println!("No configurations stored");
} else {
println!("Stored configurations:");
for (alias_name, config) in &storage.configurations {
let mut info = format!("token={}, url={}", config.token, config.url);
if let Some(model) = &config.model {
info.push_str(&format!(", model={model}"));
}
if let Some(small_fast_model) = &config.small_fast_model {
info.push_str(&format!(", small_fast_model={small_fast_model}"));
}
if let Some(max_thinking_tokens) = config.max_thinking_tokens {
info.push_str(&format!(
", max_thinking_tokens={max_thinking_tokens}"
));
}
println!(" {alias_name}: {info}");
}
}
} else {
println!(
"{}",
serde_json::to_string_pretty(&storage.configurations)
.map_err(|e| anyhow!("Failed to serialize configurations: {}", e))?
);
}
}
Commands::Completion { shell } => {
generate_completion(&shell)?;
}
Commands::Use {
alias_name,
resume,
r#continue,
prompt,
} => {
if alias_name == "cc" || alias_name == "official" {
println!("Using official Claude configuration");
let mut settings =
ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
settings.remove_anthropic_env();
settings.save(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
launch_claude_with_env(
EnvironmentConfig::empty(),
None,
None,
r#continue,
)?;
return Ok(());
}
let config = storage
.configurations
.get(&alias_name)
.ok_or_else(|| anyhow!("Configuration '{}' not found", alias_name))?
.clone();
let env_config = EnvironmentConfig::from_config(&config);
let storage_mode = storage.default_storage_mode.clone().unwrap_or_default();
let mut settings =
ClaudeSettings::load(storage.get_claude_settings_dir().map(|s| s.as_str()))?;
settings.switch_to_config_with_mode(
&config,
storage_mode,
storage.get_claude_settings_dir().map(|s| s.as_str()),
)?;
println!("Switched to configuration '{}'", alias_name);
println!(" URL: {}", config.url);
println!(
" Token: {}",
crate::cli::display_utils::format_token_for_display(&config.token)
);
let prompt_str = if prompt.is_empty() {
None
} else {
Some(prompt.join(" "))
};
launch_claude_with_env(
env_config,
prompt_str.as_deref(),
resume.as_deref(),
r#continue,
)?;
}
}
} else {
let storage = ConfigStorage::load()?;
handle_interactive_selection(&storage)?;
}
Ok(())
}