Skip to main content

claude_wrapper/command/
auth.rs

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