Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions src/cmd/account.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
use std::collections::BTreeMap;

use crate::config::{set_active_profile, valid_account_name, write_config, DEFAULT_PROFILE};
use crate::key::{load_config_if_present, mask_api_key, no_key_error};
use crate::parse::{get_string_flag, FlagValue, ParsedArgs};
use crate::spawn::session_model_label;
use crate::term;

use crate::cmd::dispatch::{config_path, hint};
use crate::cmd::login::run_login;
use crate::cmd::models::{flag_agent, save_agent_account};

pub(crate) fn run_logout(
parsed: &ParsedArgs,
env: &BTreeMap<String, String>,
) -> Result<i32, String> {
let path = config_path(parsed, env);
let Some(mut cfg) = load_config_if_present(&path) else {
return Err(no_key_error());
};
let name =
get_string_flag(&parsed.flags, "profile").unwrap_or_else(|| cfg.active_profile.clone());
let Some(profile) = cfg.profiles.get_mut(&name) else {
return Err(format!("Account \"{name}\" was not found."));
};
profile.api_key = None;
profile.management_key = None;
write_config(&cfg, &path)?;
println!(
"{} cleared keys for {}",
term::ok("Logged out"),
term::accent(&name)
);
Ok(0)
}

pub(crate) fn run_account_use(
parsed: &ParsedArgs,
env: &BTreeMap<String, String>,
name: &str,
) -> Result<i32, String> {
let path = config_path(parsed, env);
if let Some(agent) = flag_agent(parsed) {
return save_agent_account(&path, &agent, name);
}
let cfg = load_config_if_present(&path).ok_or_else(no_key_error)?;
let cfg = set_active_profile(cfg, name)?;
write_config(&cfg, &path)?;
println!(
"{} active account {}",
term::ok("Switched"),
term::accent(name)
);
Ok(0)
}

pub(crate) fn run_account(
parsed: &ParsedArgs,
env: &BTreeMap<String, String>,
) -> Result<i32, String> {
let path = config_path(parsed, env);
let sub = parsed
.passthrough
.first()
.map(String::as_str)
.unwrap_or("list");
match sub {
"list" => {
let Some(cfg) = load_config_if_present(&path) else {
return Err(no_key_error());
};
if parsed.flag_true("json") {
let rows: Vec<_> = cfg
.profiles
.iter()
.map(|(name, p)| {
serde_json::json!({
"name": name,
"active": name == &cfg.active_profile,
"default_model": p.default_model(),
"has_key": p.api_key.as_ref().is_some_and(|s| !s.is_empty()),
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&rows).unwrap_or_else(|_| "[]".into())
);
return Ok(0);
}
for (name, profile) in &cfg.profiles {
let marker = if name == &cfg.active_profile {
"*"
} else {
" "
};
println!(
"{marker} {} {} {}",
term::accent(name),
term::model_id(&session_model_label(profile.default_model())),
mask_api_key(profile.api_key.as_deref())
);
}
if cfg.profiles.is_empty() {
println!("{}", term::dim(&hint("No accounts. Run: {bin} login")));
}
Ok(0)
}
"use" => {
let name = parsed
.passthrough
.get(1)
.cloned()
.ok_or_else(|| hint("Usage: {bin} account use <name>"))?;
run_account_use(parsed, env, &name)
}
"add" => {
let name = parsed
.passthrough
.get(1)
.cloned()
.unwrap_or_else(|| DEFAULT_PROFILE.into());
if !valid_account_name(&name) {
return Err(format!(
"Invalid account name \"{name}\". Use letters, digits, \".\", \"_\", \"-\"."
));
}
let mut flags = parsed.flags.clone();
flags.insert("profile".into(), FlagValue::Value(name));
let next = ParsedArgs {
command: "login".into(),
flags,
passthrough: Vec::new(),
};
run_login(&next, env)
}
"rename" => {
let old = parsed
.passthrough
.get(1)
.cloned()
.ok_or_else(|| hint("Usage: {bin} account rename <old> <new>"))?;
let new = parsed
.passthrough
.get(2)
.cloned()
.ok_or_else(|| hint("Usage: {bin} account rename <old> <new>"))?;
if !valid_account_name(&new) {
return Err(format!(
"Invalid account name \"{new}\". Use letters, digits, \".\", \"_\", \"-\"."
));
}
let mut cfg = load_config_if_present(&path).ok_or_else(no_key_error)?;
let profile = cfg
.profiles
.remove(&old)
.ok_or_else(|| format!("Account \"{old}\" was not found."))?;
if cfg.profiles.contains_key(&new) {
return Err(format!("Account \"{new}\" already exists."));
}
cfg.profiles.insert(new.clone(), profile);
if cfg.active_profile == old {
cfg.active_profile = new.clone();
}
write_config(&cfg, &path)?;
println!("{} {old} → {}", term::ok("Renamed"), term::accent(&new));
Ok(0)
}
"remove" => {
let name = parsed
.passthrough
.get(1)
.cloned()
.ok_or_else(|| hint("Usage: {bin} account remove <name>"))?;
let mut cfg = load_config_if_present(&path).ok_or_else(no_key_error)?;
if cfg.active_profile == name {
return Err(hint(&format!(
"\"{name}\" is the active account. Switch first: {{bin}} account use <other>"
)));
}
if cfg.profiles.remove(&name).is_none() {
return Err(format!("Account \"{name}\" was not found."));
}
write_config(&cfg, &path)?;
println!("{} removed {}", term::ok("Removed"), term::accent(&name));
Ok(0)
}
other => Err(format!(
"Unknown account subcommand \"{other}\". Try: list | use | add | rename | remove"
)),
}
}
100 changes: 100 additions & 0 deletions src/cmd/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::collections::BTreeMap;

use crate::help::command_help;
use crate::key::{load_config_if_present, mask_api_key, no_key_error, resolve_api_key};
use crate::parse::{get_string_flag, ParsedArgs};
use crate::term;

use crate::cmd::account::{run_account_use, run_logout};
use crate::cmd::dispatch::{config_path, hint, shift_passthrough};
use crate::cmd::login::run_login;
use crate::cmd::usage::run_whoami;

pub(crate) fn run_auth(parsed: &ParsedArgs, env: &BTreeMap<String, String>) -> Result<i32, String> {
let sub = parsed
.passthrough
.first()
.map(String::as_str)
.filter(|s| *s != "-h" && *s != "--help")
.unwrap_or("");
if sub.is_empty() {
print!("{}", command_help("auth").unwrap_or_default());
return Ok(0);
}
let rest = shift_passthrough(parsed);
match sub {
"login" | "setup" => run_login(&rest, env),
"logout" => run_logout(&rest, env),
"status" => run_whoami(&rest, env),
"token" => run_auth_token(&rest, env),
"switch" => run_auth_switch(&rest, env),
other => Err(format!(
"unknown command \"{other}\" for \"{} auth\"\n\n{}",
crate::help::invoked_bin(),
command_help("auth").unwrap_or_default()
)),
}
}

pub(crate) fn run_auth_token(
parsed: &ParsedArgs,
env: &BTreeMap<String, String>,
) -> Result<i32, String> {
let path = config_path(parsed, env);
let cfg = load_config_if_present(&path).ok_or_else(no_key_error)?;
let name = get_string_flag(&parsed.flags, "profile")
.or_else(|| env.get("ANYROUTER_PROFILE").cloned())
.unwrap_or_else(|| cfg.active_profile.clone());
let profile = cfg
.profiles
.get(&name)
.ok_or_else(|| format!("Account \"{name}\" was not found."))?;
let key = resolve_api_key(&parsed.flags, env, Some(profile)).ok_or_else(no_key_error)?;
if parsed.flag_true("json") {
let value = if parsed.flag_true("masked") {
mask_api_key(Some(&key))
} else {
key.clone()
};
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"account": name,
"token": value,
}))
.unwrap_or_else(|_| "{}".into())
);
return Ok(0);
}
if parsed.flag_true("masked") {
println!("{}", mask_api_key(Some(&key)));
} else {
println!("{key}");
}
Ok(0)
}

pub(crate) fn run_auth_switch(
parsed: &ParsedArgs,
env: &BTreeMap<String, String>,
) -> Result<i32, String> {
let path = config_path(parsed, env);
let cfg = load_config_if_present(&path).ok_or_else(no_key_error)?;
let names: Vec<String> = cfg.profiles.keys().cloned().collect();
let name = parsed
.passthrough
.first()
.cloned()
.filter(|s| !s.is_empty())
.or_else(|| {
if !term::is_interactive() || names.is_empty() {
return None;
}
let current = names.iter().position(|n| n == &cfg.active_profile);
term::pick("Active account", &names, current)
.ok()
.map(|i| names[i].clone())
})
.ok_or_else(|| hint("Usage: {bin} auth switch <account>"))?;
run_account_use(parsed, env, &name)
}
Loading
Loading