1use std::io::Write;
15use std::path::Path;
16
17use anyhow::{Result, anyhow};
18use clap::{Args, Subcommand};
19
20use crate::Exit;
21use crate::auth::{AuthStore, default_path, display_endpoint};
22use crate::cli::init::presets;
23use crate::cli::init::wizard::{Console, Terminal};
24
25#[derive(Debug, Args)]
26pub struct AuthArgs {
27 #[command(subcommand)]
28 pub command: AuthCommand,
29}
30
31#[derive(Debug, Subcommand)]
32pub enum AuthCommand {
33 List,
35 Login(LoginArgs),
37 Logout(LogoutArgs),
39}
40
41#[derive(Debug, Args)]
42pub struct LoginArgs {
43 #[arg(long, conflicts_with = "provider")]
48 pub endpoint: Option<String>,
49
50 #[arg(long)]
52 pub provider: Option<String>,
53}
54
55#[derive(Debug, Args)]
56pub struct LogoutArgs {
57 #[arg(long)]
59 pub endpoint: String,
60}
61
62pub fn run(args: &AuthArgs) -> Result<Exit> {
64 let mut out = std::io::stdout().lock();
65 run_at(&mut out, args, &default_path()?)
66}
67
68pub fn run_at<W: Write>(out: &mut W, args: &AuthArgs, path: &Path) -> Result<Exit> {
74 match &args.command {
75 AuthCommand::List => list(out, path),
76 AuthCommand::Login(login) => {
77 let mut console = Terminal::new(out);
78 self::login(&mut console, login, path)
79 }
80 AuthCommand::Logout(logout) => self::logout(out, logout, path),
81 }
82}
83
84fn list<W: Write>(out: &mut W, path: &Path) -> Result<Exit> {
86 let store = AuthStore::load(path)?;
87
88 if store.is_empty() {
89 writeln!(out, "No keys stored ({}).", path.display())?;
90 writeln!(out, "Run `drep auth login --provider <name>` to add one.")?;
91 return Ok(Exit::Clean);
92 }
93
94 writeln!(out, "Keys stored in {}:", path.display())?;
95 for endpoint in store.endpoints() {
96 let shown = display_endpoint(endpoint);
99 match matching_preset(endpoint).map(|p| p.display_name) {
100 Some(name) => writeln!(out, " {shown} ({name})")?,
101 None => writeln!(out, " {shown}")?,
102 }
103 }
104 Ok(Exit::Clean)
105}
106
107fn login(console: &mut dyn Console, args: &LoginArgs, path: &Path) -> Result<Exit> {
109 let endpoint = resolve_endpoint(args)?;
110 let shown = display_endpoint(&endpoint);
111 let mut store = AuthStore::load(path)?;
112
113 if store.get(&endpoint).is_some() {
114 console.say(&format!("Replacing the key stored for {shown}."))?;
115 }
116
117 if let Some(url) = matching_preset(&endpoint).and_then(|preset| preset.key_url()) {
118 console.say(&format!("Get a key: {url}"))?;
119 }
120
121 let key = console.ask_secret(&format!("Paste the key for {shown}"))?;
122 if key.trim().is_empty() {
126 console.say("No key entered; nothing was stored.")?;
127 return Ok(Exit::Clean);
128 }
129
130 store.set(&endpoint, &key)?;
131 store.save(path)?;
132 console.say(&format!("✓ Stored a key for {shown}"))?;
133 Ok(Exit::Clean)
134}
135
136fn logout<W: Write>(out: &mut W, args: &LogoutArgs, path: &Path) -> Result<Exit> {
138 let mut store = AuthStore::load(path)?;
139 let shown = display_endpoint(&args.endpoint);
140
141 if !store.remove(&args.endpoint) {
142 writeln!(out, "No key was stored for {shown}.")?;
143 return Ok(Exit::Clean);
144 }
145
146 store.save(path)?;
147 writeln!(out, "✓ Forgot the key for {shown}")?;
148 Ok(Exit::Clean)
149}
150
151fn resolve_endpoint(args: &LoginArgs) -> Result<String> {
157 if let Some(endpoint) = &args.endpoint {
158 return Ok(endpoint.clone());
159 }
160
161 let Some(name) = &args.provider else {
162 return Err(anyhow!(
163 "name the endpoint with --endpoint, or a preset with --provider"
164 ));
165 };
166
167 let preset = presets::preset(name).ok_or_else(|| anyhow!("unknown provider `{name}`"))?;
168 if matches!(preset.backend, presets::PresetBackend::Codex(_)) {
169 return Err(anyhow!(
170 "the Codex CLI owns ChatGPT subscription authentication; run `codex login` instead (nothing was stored by drep)"
171 ));
172 }
173 preset
174 .endpoint()
175 .map(str::to_owned)
176 .ok_or_else(|| anyhow!("--provider {name} presumes no host; use --endpoint instead"))
177}
178
179fn matching_preset(endpoint: &str) -> Option<&'static presets::LlmPreset> {
181 let wanted = crate::auth::normalise(endpoint);
182 presets::PRESETS.iter().copied().find(|p| {
183 p.endpoint()
184 .is_some_and(|e| crate::auth::normalise(e) == wanted)
185 })
186}
187
188#[cfg(test)]
189mod tests;