use std::io::Write;
use std::path::Path;
use anyhow::{Result, anyhow};
use clap::{Args, Subcommand};
use crate::Exit;
use crate::auth::{AuthStore, default_path};
use crate::cli::init::presets;
use crate::cli::init::wizard::{Console, Terminal};
#[derive(Debug, Args)]
pub struct AuthArgs {
#[command(subcommand)]
pub command: AuthCommand,
}
#[derive(Debug, Subcommand)]
pub enum AuthCommand {
List,
Login(LoginArgs),
Logout(LogoutArgs),
}
#[derive(Debug, Args)]
pub struct LoginArgs {
#[arg(long, conflicts_with = "provider")]
pub endpoint: Option<String>,
#[arg(long)]
pub provider: Option<String>,
}
#[derive(Debug, Args)]
pub struct LogoutArgs {
#[arg(long)]
pub endpoint: String,
}
pub fn run(args: &AuthArgs) -> Result<Exit> {
let mut out = std::io::stdout().lock();
run_at(&mut out, args, &default_path()?)
}
pub fn run_at<W: Write>(out: &mut W, args: &AuthArgs, path: &Path) -> Result<Exit> {
match &args.command {
AuthCommand::List => list(out, path),
AuthCommand::Login(login) => {
let mut console = Terminal::new(out);
self::login(&mut console, login, path)
}
AuthCommand::Logout(logout) => self::logout(out, logout, path),
}
}
fn list<W: Write>(out: &mut W, path: &Path) -> Result<Exit> {
let store = AuthStore::load(path)?;
if store.is_empty() {
writeln!(out, "No keys stored ({}).", path.display())?;
writeln!(out, "Run `drep auth login --provider <name>` to add one.")?;
return Ok(Exit::Clean);
}
writeln!(out, "Keys stored in {}:", path.display())?;
for endpoint in store.endpoints() {
match matching_preset(endpoint).map(|p| p.display_name) {
Some(name) => writeln!(out, " {endpoint} ({name})")?,
None => writeln!(out, " {endpoint}")?,
}
}
Ok(Exit::Clean)
}
fn login(console: &mut dyn Console, args: &LoginArgs, path: &Path) -> Result<Exit> {
let endpoint = resolve_endpoint(args)?;
let mut store = AuthStore::load(path)?;
if store.get(&endpoint).is_some() {
console.say(&format!("Replacing the key stored for {endpoint}."))?;
}
if let Some(url) = matching_preset(&endpoint).and_then(|preset| preset.key_url()) {
console.say(&format!("Get a key: {url}"))?;
}
let key = console.ask_secret(&format!("Paste the key for {endpoint}"))?;
if key.trim().is_empty() {
console.say("No key entered; nothing was stored.")?;
return Ok(Exit::Clean);
}
store.set(&endpoint, &key)?;
store.save(path)?;
console.say(&format!("✓ Stored a key for {endpoint}"))?;
Ok(Exit::Clean)
}
fn logout<W: Write>(out: &mut W, args: &LogoutArgs, path: &Path) -> Result<Exit> {
let mut store = AuthStore::load(path)?;
if !store.remove(&args.endpoint) {
writeln!(out, "No key was stored for {}.", args.endpoint)?;
return Ok(Exit::Clean);
}
store.save(path)?;
writeln!(out, "✓ Forgot the key for {}", args.endpoint)?;
Ok(Exit::Clean)
}
fn resolve_endpoint(args: &LoginArgs) -> Result<String> {
if let Some(endpoint) = &args.endpoint {
return Ok(endpoint.clone());
}
let Some(name) = &args.provider else {
return Err(anyhow!(
"name the endpoint with --endpoint, or a preset with --provider"
));
};
let preset = presets::preset(name).ok_or_else(|| anyhow!("unknown provider `{name}`"))?;
if matches!(preset.backend, presets::PresetBackend::Codex(_)) {
return Err(anyhow!(
"the Codex CLI owns ChatGPT subscription authentication; run `codex login` instead (nothing was stored by drep)"
));
}
preset
.endpoint()
.map(str::to_owned)
.ok_or_else(|| anyhow!("--provider {name} presumes no host; use --endpoint instead"))
}
fn matching_preset(endpoint: &str) -> Option<&'static presets::LlmPreset> {
let wanted = crate::auth::normalise(endpoint);
presets::PRESETS.iter().copied().find(|p| {
p.endpoint()
.is_some_and(|e| crate::auth::normalise(e) == wanted)
})
}
#[cfg(test)]
mod tests;