zoi-cli 1.25.3

Advanced Package Manager & Environment Orchestrator
Documentation
//! Logic for the `tree` command.

use std::collections::HashSet;

use anyhow::{Result, anyhow};
use colored::Colorize;

use crate::pkg::install::resolver;

/// Runs the `tree` command to display the dependency tree of one or more
/// packages.
///
/// # Errors
///
/// Returns an error if the dependency tree cannot be resolved.
pub fn run(package_names: &[String]) -> Result<()> {
    if package_names.is_empty() {
        println!("{}", "Please specify at least one package name.".yellow());
        return Ok(());
    }

    println!("{} Resolving dependency tree...", "::".bold().blue());

    let (graph, non_zoi_deps) = resolver::resolve_dependency_graph(
        package_names,
        None,
        false,
        true,
        true,
        None,
        true,
        None
    )?;

    if !non_zoi_deps.is_empty() {
        println!(
            "\n{} External dependencies (non-Zoi):",
            "::".bold().yellow()
        );
        for dep in non_zoi_deps {
            println!("  - {}", dep.dimmed());
        }
    }

    println!("\n{} Dependency tree:", "::".bold().blue());

    let mut visited = HashSet::new();
    for source in package_names {
        if let Some(children) = graph.adj.get("$root") {
            for pkg_id in children {
                let Some(node) = graph.nodes.get(pkg_id) else {
                    continue;
                };
                if source.contains(&node.pkg.name) {
                    print_node(&graph, pkg_id, "", true, &mut visited)?;
                }
            }
        }
    }

    Ok(())
}

/// Recursively prints a node in the dependency tree.
fn print_node(
    graph: &resolver::DependencyGraph,
    pkg_id: &str,
    prefix: &str,
    is_last: bool,
    visited: &mut HashSet<String>
) -> Result<()> {
    let node = graph
        .nodes
        .get(pkg_id)
        .ok_or_else(|| anyhow!("Package not found in graph: {pkg_id}"))?;
    let is_repeated = visited.contains(pkg_id);

    let connector = if is_last { "└── " } else { "├── " };

    let pkg_display = if let Some(sub) = &node.sub_package {
        format!("{}:{}", node.pkg.name.cyan().bold(), sub.yellow())
    } else {
        node.pkg.name.cyan().bold().to_string()
    };

    let version_display = format!("v{}", node.version);
    let repeated_mark = if is_repeated {
        " (*)".dimmed()
    } else {
        "".normal()
    };

    println!(
        "{prefix}{connector}{pkg_display}{repeated_mark} {version_display}"
    );

    if is_repeated {
        return Ok(());
    }
    visited.insert(pkg_id.to_string());

    if let Some(children) = graph.adj.get(pkg_id) {
        let child_count = children.len();
        let mut sorted_children: Vec<_> = children.iter().collect();
        sorted_children.sort();

        for (i, child_id) in sorted_children.iter().enumerate() {
            let new_prefix =
                format!("{}{}", prefix, if is_last { "    " } else { "" });
            print_node(
                graph,
                child_id,
                &new_prefix,
                i == child_count - 1,
                visited
            )?;
        }
    }

    Ok(())
}