use clap::ArgMatches;
use std::fs;
use std::io::{self, Read, Write};
use crate::config::adder::add_profile;
use crate::config::loader::{config_path, find_profile, load_config};
use crate::config::remover::remove_profile;
use crate::config::schema::{Profile, ProfileAuthMode};
use crate::credentials::store;
fn parse_auth_mode(raw: &str) -> Option<ProfileAuthMode> {
match raw.trim().to_ascii_lowercase().as_str() {
"none" => Some(ProfileAuthMode::None),
"api_key" => Some(ProfileAuthMode::ApiKey),
"openai_account" => Some(ProfileAuthMode::OpenaiAccount),
_ => None,
}
}
fn profile_exists(name: &str) -> bool {
load_config()
.map(|cfg| cfg.profile.iter().any(|profile| profile.name == name))
.unwrap_or(false)
}
fn confirm(message: &str) -> Result<bool, String> {
print!("{message} [y/N]: ");
io::stdout()
.flush()
.map_err(|error| format!("failed to flush stdout: {error}"))?;
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.map_err(|error| format!("failed to read confirmation input: {error}"))?;
Ok(matches!(
input.trim().to_ascii_lowercase().as_str(),
"y" | "yes"
))
}
fn resolve_token_input(set_m: &ArgMatches) -> Result<String, String> {
if let Some(token) = set_m.get_one::<String>("token") {
let trimmed = token.trim();
if trimmed.is_empty() {
return Err("`--token` cannot be empty.".to_string());
}
return Ok(trimmed.to_string());
}
if set_m.get_flag("stdin") {
let mut input = String::new();
io::stdin()
.read_to_string(&mut input)
.map_err(|error| format!("failed reading token from stdin: {error}"))?;
let trimmed = input.trim();
if trimmed.is_empty() {
return Err("no token content was received from stdin.".to_string());
}
return Ok(trimmed.to_string());
}
if let Some(env_var) = set_m.get_one::<String>("env") {
let value = std::env::var(env_var)
.map_err(|_| format!("environment variable '{env_var}' is not set"))?;
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(format!("environment variable '{env_var}' is empty"));
}
return Ok(trimmed.to_string());
}
Err("no token source provided".to_string())
}
fn write_config(cfg: &crate::config::schema::Config) -> Result<(), String> {
let path = config_path();
let serialized = toml::to_string_pretty(cfg)
.map_err(|error| format!("failed to serialize config: {error}"))?;
fs::write(&path, serialized)
.map_err(|error| format!("failed to write '{}': {error}", path.display()))
}
fn run_list() -> bool {
if let Some(cfg) = load_config() {
println!("Configured profiles:");
println!(
"{:<20} {:<10} {:<20} {:<15} {}",
"Name", "Server", "Auth mode", "Model", "Default"
);
println!("{:-<90}", "");
let default_name = cfg.default_profile.clone();
for profile in cfg.profile {
let is_default = default_name
.as_ref()
.map(|default_profile| default_profile == &profile.name)
.unwrap_or(false);
let mark = if is_default { "✓" } else { "" };
println!(
"{:<20} {:<10} {:<20} {:<15} {}",
profile.name,
profile.server,
profile.auth_mode.as_str(),
profile.model,
mark
);
}
true
} else {
eprintln!("❌ No config file found.");
false
}
}
fn run_show(show_m: &ArgMatches) -> bool {
if let Some(name) = show_m.get_one::<String>("name") {
if let Some(cfg) = load_config() {
if let Some(profile) = find_profile(&cfg, name) {
println!("Profile: {}", profile.name);
let is_default = cfg
.default_profile
.as_ref()
.map(|default_profile| default_profile == &profile.name)
.unwrap_or(false);
println!("Default: {}", if is_default { "Yes" } else { "No" });
println!("Server: {}", profile.server);
println!("Model: {}", profile.model);
println!("Auth: {}", profile.auth_mode.as_str());
let token_available = match store::load_profile_token(&profile.name) {
Ok(Some(_)) => true,
Ok(None) => profile.token.is_some(),
Err(error) => {
eprintln!("⚠️ Failed to load profile token from credential store: {error}");
profile.token.is_some()
}
};
println!(
"Token: {}",
if token_available { "present" } else { "(none)" }
);
println!("Timeout: {}", profile.timeout_in_sec);
if let Some(url) = &profile.url {
println!("URL: {}", url);
}
if let Some(description) = &profile.description {
println!("Description: {}", description);
}
true
} else {
eprintln!("❌ Profile '{}' not found.", name);
false
}
} else {
eprintln!("❌ No config file found.");
false
}
} else {
eprintln!("❌ Please provide a profile name. Example: cargo ai profile show openai-prod");
false
}
}
fn run_add(add_m: &ArgMatches) -> bool {
let Some(name) = add_m.get_one::<String>("name") else {
eprintln!("Please provide a profile name. Example: cargo ai profile add <name> ...");
return false;
};
let Some(server) = add_m.get_one::<String>("server") else {
eprintln!("Please provide --server (for example: openai or ollama).");
return false;
};
let Some(model) = add_m.get_one::<String>("model") else {
eprintln!("Please provide --model (for example: gpt-5.2 or mistral).");
return false;
};
let auth_mode = add_m
.get_one::<String>("auth")
.and_then(|raw_mode| parse_auth_mode(raw_mode))
.unwrap_or(ProfileAuthMode::None);
let new_profile = Profile {
name: name.to_string(),
server: server.to_string(),
model: model.to_string(),
url: add_m.get_one::<String>("url").cloned(),
token: None,
timeout_in_sec: 60,
description: add_m.get_one::<String>("description").cloned(),
auth_mode,
};
let set_as_default = add_m.get_flag("default");
if let Err(error) = add_profile(new_profile, false, set_as_default) {
eprintln!("Failed to add profile: {error}");
false
} else {
println!(
"✅ Profile '{}' saved. Auth mode: '{}'.",
name,
auth_mode.as_str()
);
true
}
}
fn run_set(set_m: &ArgMatches) -> bool {
let Some(name) = set_m.get_one::<String>("name") else {
eprintln!("❌ Missing profile name.");
return false;
};
let mut cfg = match load_config() {
Some(cfg) => cfg,
None => {
eprintln!("❌ No config file found.");
return false;
}
};
let Some(profile) = cfg.profile.iter_mut().find(|profile| profile.name == *name) else {
eprintln!("❌ Profile '{}' not found.", name);
return false;
};
let mut metadata_changes: Vec<&str> = Vec::new();
if let Some(server) = set_m.get_one::<String>("server") {
profile.server = server.to_string();
metadata_changes.push("server");
}
if let Some(model) = set_m.get_one::<String>("model") {
profile.model = model.to_string();
metadata_changes.push("model");
}
if let Some(raw_mode) = set_m.get_one::<String>("auth") {
let Some(mode) = parse_auth_mode(raw_mode) else {
eprintln!(
"❌ Invalid auth mode '{}'. Use none|api_key|openai_account.",
raw_mode
);
return false;
};
profile.auth_mode = mode;
metadata_changes.push("auth");
}
if let Some(url) = set_m.get_one::<String>("url") {
profile.url = Some(url.to_string());
metadata_changes.push("url");
} else if set_m.get_flag("clear_url") {
profile.url = None;
metadata_changes.push("url");
}
if let Some(description) = set_m.get_one::<String>("description") {
profile.description = Some(description.to_string());
metadata_changes.push("description");
} else if set_m.get_flag("clear_description") {
profile.description = None;
metadata_changes.push("description");
}
if set_m.get_flag("default") {
cfg.default_profile = Some(name.to_string());
metadata_changes.push("default");
}
let mut token_change: Option<&str> = None;
if set_m.get_flag("clear_token") {
if let Err(error) = store::clear_profile_token(name) {
eprintln!("❌ Failed to clear token for profile '{}': {error}", name);
return false;
}
token_change = Some("cleared");
} else if set_m.get_one::<String>("token").is_some()
|| set_m.get_flag("stdin")
|| set_m.get_one::<String>("env").is_some()
{
let token = match resolve_token_input(set_m) {
Ok(token) => token,
Err(error) => {
eprintln!("❌ Failed to read token input: {error}");
return false;
}
};
if let Err(error) = store::store_profile_token(name, token.as_str()) {
eprintln!("❌ Failed to store token for profile '{}': {error}", name);
return false;
}
token_change = Some("updated");
}
if !metadata_changes.is_empty() {
if let Err(error) = write_config(&cfg) {
eprintln!("❌ Failed to persist profile updates: {error}");
return false;
}
}
println!("✅ Profile '{}' updated.", name);
if !metadata_changes.is_empty() {
println!("Metadata updates: {}", metadata_changes.join(", "));
}
if let Some(token_change) = token_change {
println!("Token: {token_change}");
let auth_mode = cfg
.profile
.iter()
.find(|profile| profile.name == *name)
.map(|profile| profile.auth_mode)
.unwrap_or(ProfileAuthMode::None);
if auth_mode != ProfileAuthMode::ApiKey {
println!(
"ℹ️ Profile auth mode is '{}'. Set `--auth api_key` to use stored API token by default.",
auth_mode.as_str()
);
}
}
true
}
fn run_remove(remove_m: &ArgMatches) -> bool {
if let Some(name) = remove_m.get_one::<String>("name") {
if !profile_exists(name) {
eprintln!("❌ Profile '{}' not found.", name);
return false;
}
let confirmed = match confirm(&format!(
"Are you sure you want to remove profile '{name}'?"
)) {
Ok(confirmed) => confirmed,
Err(error) => {
eprintln!("❌ {error}");
return false;
}
};
if !confirmed {
println!("Operation canceled.");
return true;
}
if let Err(error) = remove_profile(name) {
eprintln!("Failed to remove profile '{}': {error}", name);
false
} else {
true
}
} else {
eprintln!(
"❌ Please provide a profile name to remove. Example: cargo ai profile remove openai-prod"
);
false
}
}
pub fn run(sub_m: &ArgMatches) -> bool {
if sub_m.subcommand_matches("list").is_some() {
run_list()
} else if let Some(show_m) = sub_m.subcommand_matches("show") {
run_show(show_m)
} else if let Some(add_m) = sub_m.subcommand_matches("add") {
run_add(add_m)
} else if let Some(set_m) = sub_m.subcommand_matches("set") {
run_set(set_m)
} else if let Some(remove_m) = sub_m.subcommand_matches("remove") {
run_remove(remove_m)
} else {
eprintln!(
"❌ No profile subcommand found. Try 'cargo ai profile list', 'cargo ai profile show <name>', 'cargo ai profile add ...', or 'cargo ai profile set ...'."
);
false
}
}
#[cfg(test)]
mod tests {
use super::parse_auth_mode;
use crate::config::schema::ProfileAuthMode;
#[test]
fn parse_auth_mode_supports_all_modes() {
assert_eq!(parse_auth_mode("none"), Some(ProfileAuthMode::None));
assert_eq!(parse_auth_mode("api_key"), Some(ProfileAuthMode::ApiKey));
assert_eq!(
parse_auth_mode("openai_account"),
Some(ProfileAuthMode::OpenaiAccount)
);
assert_eq!(parse_auth_mode("wat"), None);
}
}