Skip to main content

claude_wrapper/command/
auth.rs

1//! Authentication subcommand builders.
2//!
3//! Builders for the `claude` auth surface: [`AuthStatusCommand`],
4//! [`AuthLoginCommand`], [`AuthLogoutCommand`], and
5//! [`SetupTokenCommand`]. For detecting which auth strategy the CLI
6//! will use without invoking it, see [`crate::auth`].
7
8#[cfg(any(feature = "async", all(feature = "sync", feature = "json")))]
9use crate::Claude;
10use crate::command::ClaudeCommand;
11#[cfg(any(feature = "async", all(feature = "sync", feature = "json")))]
12use crate::error::Result;
13#[cfg(any(feature = "async", all(feature = "sync", feature = "json")))]
14use crate::exec;
15use crate::exec::CommandOutput;
16
17/// Check authentication status.
18///
19/// # Example
20///
21/// ```no_run
22/// use claude_wrapper::{Claude, ClaudeCommand, AuthStatusCommand};
23///
24/// # async fn example() -> claude_wrapper::Result<()> {
25/// let claude = Claude::builder().build()?;
26/// let status = AuthStatusCommand::new().execute_json(&claude).await?;
27/// println!("logged in: {}", status.logged_in);
28/// # Ok(())
29/// # }
30/// ```
31#[derive(Debug, Clone, Default)]
32pub struct AuthStatusCommand {
33    json: bool,
34}
35
36impl AuthStatusCommand {
37    /// Create a new auth status command.
38    #[must_use]
39    pub fn new() -> Self {
40        Self { json: true }
41    }
42
43    /// Request text output instead of JSON.
44    #[must_use]
45    pub fn text(mut self) -> Self {
46        self.json = false;
47        self
48    }
49
50    /// Execute and parse the JSON result into an [`AuthStatus`](crate::types::AuthStatus).
51    #[cfg(all(feature = "json", feature = "async"))]
52    pub async fn execute_json(&self, claude: &Claude) -> Result<crate::types::AuthStatus> {
53        let mut cmd = self.clone();
54        cmd.json = true;
55
56        let output = exec::run_claude(claude, cmd.args()).await?;
57
58        serde_json::from_str(&output.stdout).map_err(|e| crate::error::Error::Json {
59            message: format!("failed to parse auth status: {e}"),
60            source: e,
61        })
62    }
63
64    /// Blocking mirror of [`AuthStatusCommand::execute_json`].
65    #[cfg(all(feature = "sync", feature = "json"))]
66    pub fn execute_json_sync(&self, claude: &Claude) -> Result<crate::types::AuthStatus> {
67        let mut cmd = self.clone();
68        cmd.json = true;
69
70        let output = exec::run_claude_sync(claude, cmd.args())?;
71
72        serde_json::from_str(&output.stdout).map_err(|e| crate::error::Error::Json {
73            message: format!("failed to parse auth status: {e}"),
74            source: e,
75        })
76    }
77}
78
79impl ClaudeCommand for AuthStatusCommand {
80    type Output = CommandOutput;
81
82    fn args(&self) -> Vec<String> {
83        let mut args = vec!["auth".to_string(), "status".to_string()];
84        if self.json {
85            args.push("--json".to_string());
86        } else {
87            args.push("--text".to_string());
88        }
89        args
90    }
91
92    #[cfg(feature = "async")]
93    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
94        exec::run_claude(claude, self.args()).await
95    }
96}
97
98/// Which billing path the CLI should authenticate against.
99/// Maps to `--claudeai` (subscription) or `--console` (Anthropic
100/// Console / API usage billing) on `claude auth login`.
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum LoginMode {
103    /// Claude subscription account (the CLI's default if neither
104    /// flag is passed; passing this is explicit-form). Maps to
105    /// `--claudeai`.
106    Claudeai,
107    /// Anthropic Console account, billed via API usage. Maps to
108    /// `--console`. Required for teams on Console billing -- the
109    /// default subscription path will sign them into the wrong
110    /// account.
111    Console,
112}
113
114impl LoginMode {
115    fn as_arg(self) -> &'static str {
116        match self {
117            Self::Claudeai => "--claudeai",
118            Self::Console => "--console",
119        }
120    }
121}
122
123/// Authenticate with Claude.
124///
125/// # Billing mode
126///
127/// As of Claude Code 2.1.x the CLI supports two billing paths:
128/// Claude subscription (`--claudeai`, the default) and Anthropic
129/// Console / API usage (`--console`). Use [`Self::mode`] to pin
130/// the path explicitly -- Console-billed teams need to, or
131/// they'll land in the wrong account on first auth.
132///
133/// # Example
134///
135/// ```no_run
136/// use claude_wrapper::{Claude, ClaudeCommand, AuthLoginCommand};
137/// use claude_wrapper::command::auth::LoginMode;
138///
139/// # async fn example() -> claude_wrapper::Result<()> {
140/// let claude = Claude::builder().build()?;
141/// AuthLoginCommand::new()
142///     .mode(LoginMode::Console)
143///     .email("user@example.com")
144///     .execute(&claude)
145///     .await?;
146/// # Ok(())
147/// # }
148/// ```
149#[derive(Debug, Clone, Default)]
150pub struct AuthLoginCommand {
151    email: Option<String>,
152    mode: Option<LoginMode>,
153    force_sso: bool,
154    #[deprecated(
155        since = "0.10.0",
156        note = "the `--sso` flag is a boolean since at least Claude Code 2.1.x; \
157                the value passed via the deprecated `.sso(provider)` was being \
158                emitted as an extra positional and silently doing the wrong thing. \
159                Use `force_sso()` to set the boolean flag instead."
160    )]
161    legacy_sso_value: Option<String>,
162}
163
164impl AuthLoginCommand {
165    /// Create a new auth login command.
166    #[must_use]
167    pub fn new() -> Self {
168        Self::default()
169    }
170
171    /// Set the email address for authentication.
172    #[must_use]
173    pub fn email(mut self, email: impl Into<String>) -> Self {
174        self.email = Some(email.into());
175        self
176    }
177
178    /// Pin the billing path (`--claudeai` or `--console`). The CLI
179    /// defaults to `Claudeai` when neither flag is passed; setting
180    /// this explicitly is the only way Console-billed teams reach
181    /// their account.
182    #[must_use]
183    pub fn mode(mut self, mode: LoginMode) -> Self {
184        self.mode = Some(mode);
185        self
186    }
187
188    /// Force the SSO login flow (`--sso`). Boolean flag with no
189    /// value -- replaces the historical [`Self::sso`] which took a
190    /// provider name and emitted invalid args (the CLI's `--sso`
191    /// has been boolean since at least 2.1.x).
192    #[must_use]
193    pub fn force_sso(mut self) -> Self {
194        self.force_sso = true;
195        self
196    }
197
198    /// **Deprecated.** Set the SSO provider for authentication.
199    ///
200    /// The CLI's `--sso` is a boolean flag with no value (since at
201    /// least Claude Code 2.1.x). Passing a `provider` string caused
202    /// the wrapper to emit `--sso <provider>`, which the CLI parsed
203    /// as `--sso` plus an extra positional that was silently
204    /// ignored or mishandled. Use [`Self::force_sso`] for the
205    /// correct boolean form.
206    ///
207    /// Kept as a compile-error-and-deprecation-warning bridge so
208    /// callers see the change. The value is intentionally ignored
209    /// at args() emit time -- only the boolean intent is preserved.
210    #[deprecated(
211        since = "0.10.0",
212        note = "the `--sso` flag is a boolean since at least Claude Code 2.1.x. \
213                Use `force_sso()` instead. The value passed here is ignored at \
214                emit time; the boolean intent is preserved."
215    )]
216    #[must_use]
217    pub fn sso(mut self, provider: impl Into<String>) -> Self {
218        // Honor the boolean intent (caller clearly wanted SSO);
219        // record the legacy value purely so the deprecation
220        // warning's "this used to break stuff" claim is reproducible
221        // by anyone reading the field.
222        self.force_sso = true;
223        #[allow(deprecated)]
224        {
225            self.legacy_sso_value = Some(provider.into());
226        }
227        self
228    }
229}
230
231impl ClaudeCommand for AuthLoginCommand {
232    type Output = CommandOutput;
233
234    fn args(&self) -> Vec<String> {
235        let mut args = vec!["auth".to_string(), "login".to_string()];
236        if let Some(mode) = self.mode {
237            args.push(mode.as_arg().to_string());
238        }
239        if let Some(ref email) = self.email {
240            args.push("--email".to_string());
241            args.push(email.clone());
242        }
243        if self.force_sso {
244            args.push("--sso".to_string());
245        }
246        args
247    }
248
249    #[cfg(feature = "async")]
250    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
251        exec::run_claude(claude, self.args()).await
252    }
253}
254
255/// Deauthenticate from Claude.
256///
257/// # Example
258///
259/// ```no_run
260/// use claude_wrapper::{Claude, ClaudeCommand, AuthLogoutCommand};
261///
262/// # async fn example() -> claude_wrapper::Result<()> {
263/// let claude = Claude::builder().build()?;
264/// AuthLogoutCommand::new().execute(&claude).await?;
265/// # Ok(())
266/// # }
267/// ```
268#[derive(Debug, Clone, Default)]
269pub struct AuthLogoutCommand;
270
271impl AuthLogoutCommand {
272    /// Create a new auth logout command.
273    #[must_use]
274    pub fn new() -> Self {
275        Self
276    }
277}
278
279impl ClaudeCommand for AuthLogoutCommand {
280    type Output = CommandOutput;
281
282    fn args(&self) -> Vec<String> {
283        vec!["auth".to_string(), "logout".to_string()]
284    }
285
286    #[cfg(feature = "async")]
287    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
288        exec::run_claude(claude, self.args()).await
289    }
290}
291
292/// Set up a long-lived authentication token.
293///
294/// # Example
295///
296/// ```no_run
297/// use claude_wrapper::{Claude, ClaudeCommand, SetupTokenCommand};
298///
299/// # async fn example() -> claude_wrapper::Result<()> {
300/// let claude = Claude::builder().build()?;
301/// SetupTokenCommand::new().execute(&claude).await?;
302/// # Ok(())
303/// # }
304/// ```
305#[derive(Debug, Clone, Default)]
306pub struct SetupTokenCommand;
307
308impl SetupTokenCommand {
309    /// Create a new setup-token command.
310    #[must_use]
311    pub fn new() -> Self {
312        Self
313    }
314}
315
316impl ClaudeCommand for SetupTokenCommand {
317    type Output = CommandOutput;
318
319    fn args(&self) -> Vec<String> {
320        vec!["setup-token".to_string()]
321    }
322
323    #[cfg(feature = "async")]
324    async fn execute(&self, claude: &Claude) -> Result<CommandOutput> {
325        exec::run_claude(claude, self.args()).await
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_auth_status_args() {
335        let cmd = AuthStatusCommand::new();
336        assert_eq!(cmd.args(), vec!["auth", "status", "--json"]);
337    }
338
339    #[test]
340    fn test_auth_status_text() {
341        let cmd = AuthStatusCommand::new().text();
342        assert_eq!(cmd.args(), vec!["auth", "status", "--text"]);
343    }
344
345    #[test]
346    fn test_auth_login_default() {
347        let cmd = AuthLoginCommand::new();
348        assert_eq!(cmd.args(), vec!["auth", "login"]);
349    }
350
351    #[test]
352    fn test_auth_login_with_email() {
353        let cmd = AuthLoginCommand::new().email("user@example.com");
354        assert_eq!(
355            cmd.args(),
356            vec!["auth", "login", "--email", "user@example.com"]
357        );
358    }
359
360    #[test]
361    fn test_auth_login_with_force_sso() {
362        let cmd = AuthLoginCommand::new().force_sso();
363        assert_eq!(cmd.args(), vec!["auth", "login", "--sso"]);
364    }
365
366    #[test]
367    #[allow(deprecated)]
368    fn test_auth_login_deprecated_sso_emits_boolean_only() {
369        // Bug fix: the historical `.sso(provider)` would emit
370        // `--sso <provider>` but the CLI's `--sso` is boolean. The
371        // deprecated method now honors the boolean intent (calls
372        // `force_sso` internally) and drops the value at emit time.
373        let cmd = AuthLoginCommand::new().sso("okta");
374        assert_eq!(cmd.args(), vec!["auth", "login", "--sso"]);
375    }
376
377    #[test]
378    fn test_auth_login_with_mode_claudeai() {
379        let cmd = AuthLoginCommand::new().mode(LoginMode::Claudeai);
380        assert_eq!(cmd.args(), vec!["auth", "login", "--claudeai"]);
381    }
382
383    #[test]
384    fn test_auth_login_with_mode_console() {
385        let cmd = AuthLoginCommand::new().mode(LoginMode::Console);
386        assert_eq!(cmd.args(), vec!["auth", "login", "--console"]);
387    }
388
389    #[test]
390    fn test_auth_login_console_with_email() {
391        let cmd = AuthLoginCommand::new()
392            .mode(LoginMode::Console)
393            .email("ops@example.com");
394        assert_eq!(
395            cmd.args(),
396            vec!["auth", "login", "--console", "--email", "ops@example.com"]
397        );
398    }
399
400    #[test]
401    fn test_auth_logout() {
402        let cmd = AuthLogoutCommand::new();
403        assert_eq!(cmd.args(), vec!["auth", "logout"]);
404    }
405
406    #[test]
407    fn test_setup_token() {
408        let cmd = SetupTokenCommand::new();
409        assert_eq!(cmd.args(), vec!["setup-token"]);
410    }
411}