jira-cli 0.3.13

Agent-friendly Jira CLI with JSON output, structured exit codes, and schema introspection
Documentation
use owo_colors::OwoColorize;

use crate::api::{ApiError, JiraClient};
use crate::output::{OutputConfig, use_color};

pub async fn list(client: &JiraClient, out: &OutputConfig) -> Result<(), ApiError> {
    let projects = client.list_projects().await?;

    if out.json {
        out.print_data(
            &serde_json::to_string_pretty(&serde_json::json!({
                "total": projects.len(),
                "projects": projects.iter().map(|p| serde_json::json!({
                    "key": p.key,
                    "name": p.name,
                    "id": p.id,
                    "type": p.project_type,
                })).collect::<Vec<_>>(),
            }))
            .expect("failed to serialize JSON"),
        );
    } else {
        if projects.is_empty() {
            out.print_message("No projects found.");
            return Ok(());
        }

        let color = use_color();
        let key_w = projects
            .iter()
            .map(|p| p.key.len())
            .max()
            .unwrap_or(3)
            .max(3)
            + 2;
        let name_w = projects
            .iter()
            .map(|p| p.name.len())
            .max()
            .unwrap_or(4)
            .max(4)
            + 2;

        let header = format!("{:<key_w$} {:<name_w$} {}", "Key", "Name", "Type");
        if color {
            println!("{}", header.bold());
        } else {
            println!("{header}");
        }

        for p in &projects {
            let ptype = p.project_type.as_deref().unwrap_or("-");
            if color {
                println!(
                    "{} {:<name_w$} {}",
                    format!("{:<key_w$}", p.key).yellow(),
                    p.name,
                    ptype,
                );
            } else {
                println!("{:<key_w$} {:<name_w$} {}", p.key, p.name, ptype);
            }
        }
        out.print_message(&format!("{} projects", projects.len()));
    }
    Ok(())
}

pub async fn components(
    client: &JiraClient,
    out: &OutputConfig,
    project_key: &str,
) -> Result<(), ApiError> {
    let comps = client.list_components(project_key).await?;

    if out.json {
        out.print_data(
            &serde_json::to_string_pretty(&serde_json::json!({
                "project": project_key,
                "total": comps.len(),
                "components": comps.iter().map(|c| serde_json::json!({
                    "id": c.id,
                    "name": c.name,
                    "description": c.description,
                })).collect::<Vec<_>>(),
            }))
            .expect("failed to serialize JSON"),
        );
    } else {
        if comps.is_empty() {
            out.print_message(&format!("No components found for project {project_key}."));
            return Ok(());
        }

        let color = use_color();
        let name_w = comps.iter().map(|c| c.name.len()).max().unwrap_or(4).max(4) + 2;
        let id_w = comps.iter().map(|c| c.id.len()).max().unwrap_or(2).max(2) + 2;

        let header = format!("{:<name_w$} {:<id_w$} {}", "Name", "ID", "Description");
        if color {
            println!("{}", header.bold());
        } else {
            println!("{header}");
        }

        for c in &comps {
            let desc = c.description.as_deref().unwrap_or("-");
            if color {
                println!(
                    "{} {:<id_w$} {}",
                    format!("{:<name_w$}", c.name).yellow(),
                    c.id,
                    desc,
                );
            } else {
                println!("{:<name_w$} {:<id_w$} {}", c.name, c.id, desc);
            }
        }
        out.print_message(&format!("{} components", comps.len()));
    }
    Ok(())
}

pub async fn versions(
    client: &JiraClient,
    out: &OutputConfig,
    project_key: &str,
) -> Result<(), ApiError> {
    let vers = client.list_versions(project_key).await?;

    if out.json {
        out.print_data(
            &serde_json::to_string_pretty(&serde_json::json!({
                "project": project_key,
                "total": vers.len(),
                "versions": vers.iter().map(|v| serde_json::json!({
                    "id": v.id,
                    "name": v.name,
                    "description": v.description,
                    "released": v.released,
                    "archived": v.archived,
                    "releaseDate": v.release_date,
                })).collect::<Vec<_>>(),
            }))
            .expect("failed to serialize JSON"),
        );
    } else {
        if vers.is_empty() {
            out.print_message(&format!("No versions found for project {project_key}."));
            return Ok(());
        }

        let color = use_color();
        let name_w = vers.iter().map(|v| v.name.len()).max().unwrap_or(4).max(4) + 2;
        let id_w = vers.iter().map(|v| v.id.len()).max().unwrap_or(2).max(2) + 2;

        let header = format!(
            "{:<name_w$} {:<id_w$} {:<10} {}",
            "Name", "ID", "Released", "Release Date"
        );
        if color {
            println!("{}", header.bold());
        } else {
            println!("{header}");
        }

        for v in &vers {
            let released = match v.released {
                Some(true) => "yes",
                Some(false) => "no",
                None => "-",
            };
            let release_date = v.release_date.as_deref().unwrap_or("-");
            if color {
                println!(
                    "{} {:<id_w$} {:<10} {}",
                    format!("{:<name_w$}", v.name).yellow(),
                    v.id,
                    released,
                    release_date,
                );
            } else {
                println!(
                    "{:<name_w$} {:<id_w$} {:<10} {}",
                    v.name, v.id, released, release_date
                );
            }
        }
        out.print_message(&format!("{} versions", vers.len()));
    }
    Ok(())
}

pub async fn show(client: &JiraClient, out: &OutputConfig, key: &str) -> Result<(), ApiError> {
    let project = client.get_project(key).await?;

    if out.json {
        out.print_data(
            &serde_json::to_string_pretty(&serde_json::json!({
                "key": project.key,
                "name": project.name,
                "id": project.id,
                "type": project.project_type,
            }))
            .expect("failed to serialize JSON"),
        );
    } else {
        let color = use_color();
        let key_str = if color {
            project.key.yellow().bold().to_string()
        } else {
            project.key.clone()
        };
        println!("{key_str}  {}", project.name);
        println!("  ID:   {}", project.id);
        if let Some(ref t) = project.project_type {
            println!("  Type: {t}");
        }
    }
    Ok(())
}