bitbucket-cli 0.3.11

A powerful command-line interface for Bitbucket Cloud - manage repos, PRs, issues, and pipelines from your terminal with OAuth 2.0
Documentation
use anyhow::Result;
use clap::Subcommand;
use colored::Colorize;
use dialoguer::Input;

use crate::auth::{AuthManager, OAuthFlow};
use crate::config::Config;

#[derive(Subcommand)]
pub enum AuthCommands {
    /// Authenticate with Bitbucket via OAuth 2.0
    Login {
        /// OAuth Client ID
        #[arg(long, env = "BITBUCKET_CLIENT_ID")]
        client_id: Option<String>,

        /// OAuth Client Secret
        #[arg(long, env = "BITBUCKET_CLIENT_SECRET")]
        client_secret: Option<String>,
    },

    /// Remove stored credentials
    Logout,

    /// Show authentication status
    Status,
}

impl AuthCommands {
    pub async fn run(self) -> Result<()> {
        match self {
            AuthCommands::Login {
                client_id,
                client_secret,
            } => {
                let auth_manager = AuthManager::new()?;

                // Resolve consumer credentials from (in priority):
                // 1. CLI flags / env vars
                // 2. Previously stored credentials
                // 3. Interactive prompt (first-time only)
                let stored_consumer = auth_manager
                    .get_credentials()
                    .ok()
                    .flatten()
                    .and_then(|c| {
                        c.oauth_consumer_credentials()
                            .map(|(id, secret)| (id.to_owned(), secret.to_owned()))
                    });

                let client_id = client_id
                    .or_else(|| stored_consumer.as_ref().map(|(id, _)| id.clone()))
                    .or_else(|| {
                        println!();
                        println!("📋 OAuth Consumer Setup Required");
                        println!();
                        println!("To use OAuth authentication, create an OAuth consumer in Bitbucket:");
                        println!("1. Go to: https://bitbucket.org/[workspace]/workspace/settings/oauth-consumers/new");
                        println!("2. Set callback URL to ONE of these (pick any available port):");
                        println!("   • http://127.0.0.1:8080/callback");
                        println!("   • http://127.0.0.1:3000/callback");
                        println!("   • http://127.0.0.1:8888/callback");
                        println!("   • http://127.0.0.1:9000/callback");
                        println!("3. Select required permissions:");
                        println!("   ✓ Account (Read)");
                        println!("   ✓ Repositories (Read)");
                        println!("   ✓ Pull requests (Read, Write)");
                        println!("   ✓ Issues (Read, Write)");
                        println!("   ✓ Pipelines (Read, Write)");
                        println!("4. Copy the Key (Client ID) and Secret");
                        println!();

                        Input::<String>::new()
                            .with_prompt("OAuth Client ID (Key)")
                            .interact_text()
                            .ok()
                    })
                    .ok_or_else(|| anyhow::anyhow!("OAuth Client ID is required"))?;

                let client_secret = client_secret
                    .or_else(|| stored_consumer.map(|(_, secret)| secret))
                    .or_else(|| {
                        Input::<String>::new()
                            .with_prompt("OAuth Client Secret")
                            .interact_text()
                            .ok()
                    })
                    .ok_or_else(|| anyhow::anyhow!("OAuth Client Secret is required"))?;

                let oauth = OAuthFlow::new(client_id, client_secret);
                oauth.authenticate(&auth_manager).await?;

                Ok(())
            }

            AuthCommands::Logout => {
                let auth_manager = AuthManager::new()?;
                auth_manager.clear_credentials()?;

                let mut config = Config::load()?;
                config.clear_auth();
                config.save()?;

                println!("{} Logged out successfully", "✓".green());
                Ok(())
            }

            AuthCommands::Status => {
                let auth_manager = AuthManager::new()?;
                let config = Config::load()?;

                if auth_manager.is_authenticated() {
                    println!("{} Authenticated via OAuth 2.0", "✓".green());

                    if let Ok(Some(credential)) = auth_manager.get_credentials() {
                        if credential.needs_refresh() {
                            println!(
                                "  {} {}",
                                "Status:".dimmed(),
                                "Token needs refresh (will auto-refresh on next use)".yellow()
                            );
                        }
                    }

                    if let Some(username) = config.username() {
                        println!("  {} {}", "Username:".dimmed(), username);
                    }

                    if let Some(workspace) = config.default_workspace() {
                        println!("  {} {}", "Workspace:".dimmed(), workspace);
                    }

                    match crate::api::BitbucketClient::from_stored().await {
                        Ok(client) => match client.get::<serde_json::Value>("/user").await {
                            Ok(user) => {
                                if let Some(display_name) = user.get("display_name") {
                                    println!(
                                        "  {} {}",
                                        "Display name:".dimmed(),
                                        display_name.as_str().unwrap_or("Unknown")
                                    );
                                }
                            }
                            Err(e) => {
                                println!("{} Credentials may be invalid: {}", "âš ".yellow(), e);
                            }
                        },
                        Err(e) => {
                            println!("{} Failed to create client: {}", "✗".red(), e);
                        }
                    }
                } else {
                    println!("{} Not authenticated", "✗".red());
                    println!();
                    println!("Run {} to authenticate", "bitbucket auth login".cyan());
                }

                Ok(())
            }
        }
    }
}