5.1 KiB · text History 1294137
//! Published packages, and the forge's own instructions for consuming them.
//!
//! Format labels and summaries come from the server rather than a table
//! here: a new registry format lands server-side and this client lists it
//! correctly without being rebuilt, which is the opposite of how a
//! hardcoded format list ages.

use anyhow::Result;

use crate::cli::PackageCommand;
use crate::commands::Ctx;
use crate::output::{Table, Tone, ago, bold, bytes, heading, note, paint, truncate};

pub async fn run(ctx: &Ctx, command: &PackageCommand) -> Result<()> {
    let repo = ctx.repo()?;

    match command {
        PackageCommand::List => {
            let res = ctx.api.packages(&repo).await?;
            if ctx.emitted(&res.raw)? {
                return Ok(());
            }

            let populated: Vec<_> = res
                .formats
                .iter()
                .filter(|f| !f.packages.is_empty())
                .collect();

            if populated.is_empty() {
                note(&format!("{repo} has published no packages."));
                if !res.available_formats.is_empty() {
                    note(&format!(
                        "This forge can host: {}. See `gg package usage <format>`.",
                        res.available_formats.join(", ")
                    ));
                }
                return Ok(());
            }

            for format in populated {
                println!("{}", bold(&format.label));
                if let Some(summary) = format.summary.as_deref().filter(|s| !s.is_empty()) {
                    note(summary);
                }
                let mut table = Table::new(["NAME", "LATEST", "KIND", "UPDATED", "INSTALL"]);
                for pkg in &format.packages {
                    table.row([
                        pkg.name.clone(),
                        pkg.latest_version.clone().unwrap_or_else(|| "—".into()),
                        pkg.kind.clone().unwrap_or_default(),
                        ago(pkg.updated_at.as_deref()),
                        truncate(pkg.install_line.as_deref().unwrap_or(""), 44),
                    ]);
                }
                table.print();
                println!();
            }
            Ok(())
        }

        PackageCommand::Show { format, name } => {
            let res = ctx.api.package(&repo, format, name).await?;
            if ctx.emitted(&res.raw)? {
                return Ok(());
            }

            heading(&format!("{} ({})", res.name, res.format));
            if let Some(kind) = res.kind.as_deref() {
                println!("kind    {kind}");
            }
            println!("latest  {}", res.latest_version.as_deref().unwrap_or("—"));

            for version in &res.versions {
                println!();
                let yanked = if version.yanked {
                    paint(
                        &format!(
                            " yanked{}",
                            version
                                .yanked_reason
                                .as_deref()
                                .map(|r| format!(": {r}"))
                                .unwrap_or_default()
                        ),
                        Tone::Bad,
                    )
                } else {
                    String::new()
                };
                println!(
                    "{}{}  {}  {}",
                    bold(&version.version),
                    yanked,
                    bytes(version.size),
                    ago(version.pushed_at.as_deref())
                );
                if let Some(line) = version.install_line.as_deref().filter(|l| !l.is_empty()) {
                    println!("  {line}");
                }
                for file in &version.files {
                    println!(
                        "  {}  {}  {}",
                        file.filename,
                        bytes(file.size),
                        file.content_type.as_deref().unwrap_or("")
                    );
                }
            }
            Ok(())
        }

        PackageCommand::Usage { format } => {
            let res = ctx.api.package_usage(&repo, format).await?;
            if ctx.emitted(&res.raw)? {
                return Ok(());
            }

            heading(&res.label);
            if let Some(summary) = res.summary.as_deref().filter(|s| !s.is_empty()) {
                println!("{summary}");
            }
            for url in [&res.repository_url, &res.namespace_url, &res.key_url]
                .into_iter()
                .flatten()
            {
                note(url);
            }

            for section in &res.sections {
                println!("\n{}", bold(&section.title));
                for step in &section.steps {
                    println!("\n  {}", step.label);
                    // Printed verbatim: every URL in here is one only the
                    // server can know, so the client formats and never builds.
                    for line in step.code.lines() {
                        println!("    {line}");
                    }
                }
            }
            Ok(())
        }
    }
}