pattrick 0.7.0

Pattrick is a command line tool for managing Personal Access Tokens (PAT) in Azure DevOps.
Documentation
use chrono::{Duration, Utc};
use clap::{Parser, crate_version};
use output::{print_tokens, write_to_dotenv, write_to_netrc};
use pattrick::{
    DisplayFilterOption, PatTokenDeleteRequest, PatTokenGetRequest, PatTokenListRequest,
};
use pattrick_clap as args;
use reqwest::StatusCode;
use std::error::Error;

mod output;
use pattrick::{PatTokenCreateRequest, PatTokenManager, azure::get_ad_token_for_devops};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let cli = args::Cli::parse();

    env_logger::Builder::new()
        .filter_level(cli.verbose.log_level_filter())
        .init();

    if cli.version {
        println!("pattrick v{}", crate_version!());
        return Ok(());
    }

    if matches!(cli.command, Some(args::Commands::Update)) {
        log::info!("Checking for updates...");
        let current_version = crate_version!();
        let latest_version = pattrick::get_latest_version().await?;
        if latest_version.contains(current_version) {
            println!("You are running the latest version of pattrick (v{current_version})");
            return Ok(());
        }

        log::info!("Found new version: {latest_version}. Updating...");
        let status = tokio::task::spawn_blocking(|| {
            self_update::backends::github::Update::configure()
                .repo_owner("jvanbuel")
                .repo_name("pattrick")
                .bin_name("pattrick")
                .show_download_progress(true)
                .current_version(crate_version!())
                .build()
                .unwrap()
                .update()
        })
        .await?;

        println!("Status of update: {status:?}");
        return Ok(());
    }

    let token_manager = PatTokenManager::new(get_ad_token_for_devops(1).await?).await?;

    match &cli.command {
        Some(args::Commands::Create(create_opts)) => {
            let create_request = PatTokenCreateRequest {
                all_orgs: create_opts.all_orgs,
                display_name: create_opts
                    .name
                    .clone()
                    .unwrap_or_else(|| petname::petname(2, "-").expect("Failed to generate name")),
                scope: create_opts.scope.clone(),
                valid_to: (Utc::now()
                    + Duration::try_seconds(create_opts.lifetime).unwrap_or_default()),
            };
            log::info!("Creating PAT token with request: {:?}", create_request);
            let pat_token = token_manager.create_pat_token(create_request).await?;

            match create_opts.out {
                args::Output::StdOut => {
                    print_tokens(vec![pat_token], create_opts.format);
                }
                args::Output::DotEnv => {
                    write_to_dotenv(pat_token)?;
                }
                args::Output::DotNetrc => {
                    write_to_netrc(pat_token)?;
                }
            }
        }
        Some(args::Commands::List(list_opts)) => {
            let list_request = match list_opts.all {
                true => PatTokenListRequest {
                    display_filter_option: DisplayFilterOption::All,
                },
                false => PatTokenListRequest {
                    display_filter_option: DisplayFilterOption::Active,
                },
            };
            log::info!("Listing PAT tokens with request: {:?}", list_request);
            let pat_tokens = token_manager.list_pat_tokens(list_request).await?;

            print_tokens(pat_tokens, list_opts.format);
        }
        Some(args::Commands::Get(get_opts)) => {
            let get_request = PatTokenGetRequest {
                authorization_id: get_opts.id.clone(),
            };
            let pat_token = token_manager.get_pat_token(get_request).await?;
            print_tokens(vec![pat_token], get_opts.format);
        }
        Some(args::Commands::Delete(delete_opts)) => {
            if delete_opts.id.is_empty() && delete_opts.name.is_empty() {
                return Err("Either id or name must be provided".into());
            }
            let mut authorization_id = "".to_string();
            if !delete_opts.name.is_empty() {
                let token_result = &token_manager
                    .get_pat_token_by_name(&delete_opts.name)
                    .await?;
                match token_result {
                    Some(token) => {
                        authorization_id = token.id.clone();
                    }
                    None => {
                        return Err("No PAT token found with that name".into());
                    }
                }
            }
            if !delete_opts.id.is_empty() {
                authorization_id = delete_opts.id.clone();
            }
            let delete_request = PatTokenDeleteRequest {
                authorization_id: authorization_id.clone(),
            };

            log::info!("Deleting PAT token with request: {:?}", delete_request);
            let status = &token_manager.delete_pat_token(delete_request).await?;

            match status {
                &StatusCode::NO_CONTENT => {
                    println!("✅ Successfully deleted PAT token with id: {authorization_id}");
                }
                _ => println!("Error deleting token: {status}"),
            }
        }
        _ => {}
    }

    Ok(())
}