tangled-cli 0.1.0

CLI for interacting with Tangled, an AT Protocol-based git collaboration platform
use anyhow::Result;
use inquire::{Password, Text};
use serde::Serialize;
use tangled_config::session::SessionManager;

use crate::cli::{AuthCommand, AuthLoginArgs, Cli, OutputFormat};

pub async fn run(cli: &Cli, cmd: AuthCommand) -> Result<()> {
    match cmd {
        AuthCommand::Login(args) => login(cli, args).await,
        AuthCommand::Status => status(cli).await,
        AuthCommand::Logout => logout(cli).await,
    }
}

async fn login(_cli: &Cli, mut args: AuthLoginArgs) -> Result<()> {
    let handle: String = match args.handle.take() {
        Some(h) => h,
        None => Text::new("Handle").prompt()?,
    };

    let use_app_password = args.app_password || args.password.is_some();
    if !use_app_password {
        let mut session = crate::ops::oauth::login(&handle).await?;
        // The token response only carries the DID; keep the handle the user
        // typed (or resolve it back from the PDS for DID input).
        session.handle = if handle.starts_with("did:") {
            let auth = crate::ops::auth::PdsAuth::from_session(&session)?;
            let pds = session.pds.clone().unwrap_or_default();
            crate::ops::repo::resolve_did_to_handle(&pds, &session.did, &auth)
                .await
                .unwrap_or_else(|_| handle.clone())
        } else {
            handle.clone()
        };
        SessionManager::default().save(&session)?;
        println!(
            "Logged in as '{}' ({}) via OAuth",
            session.handle, session.did
        );
        return Ok(());
    }

    let password: String = match args.password.take() {
        Some(p) => p,
        None => Password::new("Password").prompt()?,
    };
    let pds = args
        .pds
        .unwrap_or_else(|| "https://bsky.social".to_string());

    let mut session = match crate::ops::session::login_with_password(
        &pds, &handle, &password,
    )
    .await
    {
        Ok(sess) => sess,
        Err(e) => {
            println!("\x1b[93mIf you're on your own PDS, make sure to pass the --pds flag\x1b[0m");
            return Err(e);
        }
    };
    session.pds = Some(pds.clone());
    SessionManager::default().save(&session)?;
    println!("Logged in as '{}' ({})", session.handle, session.did);
    Ok(())
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct AuthStatusOutput {
    authenticated: bool,
    #[serde(skip_serializing_if = "Option::is_none")]
    handle: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    did: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    auth_type: Option<&'static str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pds: Option<String>,
}

async fn status(cli: &Cli) -> Result<()> {
    let mgr = SessionManager::default();
    match mgr.load()? {
        Some(session) => {
            let (auth_type, auth_display) = if session.oauth.is_some() {
                ("oauth", "OAuth")
            } else {
                ("appPassword", "app password")
            };
            if matches!(cli.format, OutputFormat::Json | OutputFormat::Yaml) {
                return crate::util::print_serialized(
                    cli.format,
                    &AuthStatusOutput {
                        authenticated: true,
                        handle: Some(session.handle),
                        did: Some(session.did),
                        auth_type: Some(auth_type),
                        pds: session.pds,
                    },
                );
            }
            println!(
                "Logged in as '{}' ({}) [{}]",
                session.handle, session.did, auth_display
            );
        }
        None if matches!(
            cli.format,
            OutputFormat::Json | OutputFormat::Yaml
        ) =>
        {
            crate::util::print_serialized(
                cli.format,
                &AuthStatusOutput {
                    authenticated: false,
                    handle: None,
                    did: None,
                    auth_type: None,
                    pds: None,
                },
            )?;
        }
        None => println!("Not logged in. Run: tang auth login"),
    }
    Ok(())
}

async fn logout(_cli: &Cli) -> Result<()> {
    let mgr = SessionManager::default();
    if mgr.load()?.is_some() {
        mgr.clear()?;
        println!("Logged out");
    } else {
        println!("No session found");
    }
    Ok(())
}