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