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};
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 match matching_preset(endpoint).map(|p| p.display_name) {
99 Some(name) => writeln!(out, " {endpoint} ({name})")?,
100 None => writeln!(out, " {endpoint}")?,
101 }
102 }
103 Ok(Exit::Clean)
104}
105
106fn login(console: &mut dyn Console, args: &LoginArgs, path: &Path) -> Result<Exit> {
108 let endpoint = resolve_endpoint(args)?;
109 let mut store = AuthStore::load(path)?;
110
111 if store.get(&endpoint).is_some() {
112 console.say(&format!("Replacing the key stored for {endpoint}."))?;
113 }
114
115 if let Some(url) = matching_preset(&endpoint).and_then(|preset| preset.key_url()) {
116 console.say(&format!("Get a key: {url}"))?;
117 }
118
119 let key = console.ask_secret(&format!("Paste the key for {endpoint}"))?;
120 if key.trim().is_empty() {
124 console.say("No key entered; nothing was stored.")?;
125 return Ok(Exit::Clean);
126 }
127
128 store.set(&endpoint, &key)?;
129 store.save(path)?;
130 console.say(&format!("✓ Stored a key for {endpoint}"))?;
131 Ok(Exit::Clean)
132}
133
134fn logout<W: Write>(out: &mut W, args: &LogoutArgs, path: &Path) -> Result<Exit> {
136 let mut store = AuthStore::load(path)?;
137
138 if !store.remove(&args.endpoint) {
139 writeln!(out, "No key was stored for {}.", args.endpoint)?;
140 return Ok(Exit::Clean);
141 }
142
143 store.save(path)?;
144 writeln!(out, "✓ Forgot the key for {}", args.endpoint)?;
145 Ok(Exit::Clean)
146}
147
148fn resolve_endpoint(args: &LoginArgs) -> Result<String> {
154 if let Some(endpoint) = &args.endpoint {
155 return Ok(endpoint.clone());
156 }
157
158 let Some(name) = &args.provider else {
159 return Err(anyhow!(
160 "name the endpoint with --endpoint, or a preset with --provider"
161 ));
162 };
163
164 let preset = presets::preset(name).ok_or_else(|| anyhow!("unknown provider `{name}`"))?;
165 if matches!(preset.backend, presets::PresetBackend::Codex(_)) {
166 return Err(anyhow!(
167 "the Codex CLI owns ChatGPT subscription authentication; run `codex login` instead (nothing was stored by drep)"
168 ));
169 }
170 preset
171 .endpoint()
172 .map(str::to_owned)
173 .ok_or_else(|| anyhow!("--provider {name} presumes no host; use --endpoint instead"))
174}
175
176fn matching_preset(endpoint: &str) -> Option<&'static presets::LlmPreset> {
178 let wanted = crate::auth::normalise(endpoint);
179 presets::PRESETS.iter().copied().find(|p| {
180 p.endpoint()
181 .is_some_and(|e| crate::auth::normalise(e) == wanted)
182 })
183}
184
185#[cfg(test)]
186mod tests;