mise 2026.2.24

The front-end to your dev env
use crate::backend::SecurityFeature;
use crate::ui::style;
use eyre::Result;
use itertools::Itertools;
use serde_derive::Serialize;

use crate::cli::args::BackendArg;
use crate::config::Config;
use crate::toolset::{ToolSource, ToolVersionOptions, ToolsetBuilder};
use crate::ui::table;

/// Gets information about a tool
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Tool {
    /// Tool name to get information about
    tool: BackendArg,
    /// Output in JSON format
    #[clap(long, short = 'J')]
    json: bool,

    #[clap(flatten)]
    filter: ToolInfoFilter,
}

#[derive(Debug, Clone, clap::Args)]
#[group(multiple = false)]
pub struct ToolInfoFilter {
    /// Only show active versions
    #[clap(long)]
    active: bool,

    /// Only show backend field
    #[clap(long)]
    backend_: bool,

    /// Only show config source
    #[clap(long)]
    config_source: bool,

    /// Only show description field
    #[clap(long)]
    description: bool,

    /// Only show installed versions
    #[clap(long)]
    installed: bool,

    /// Only show requested versions
    #[clap(long)]
    requested: bool,

    /// Only show tool options
    #[clap(long)]
    tool_options: bool,
}

impl Tool {
    pub async fn run(self) -> Result<()> {
        let config = Config::get().await?;
        let mut ts = ToolsetBuilder::new().build(&config).await?;
        ts.resolve(&config).await?;
        let tvl = ts.versions.get(&self.tool);
        let tv = tvl.map(|tvl| tvl.versions.first().unwrap());
        let ba = tv.map(|tv| tv.ba()).unwrap_or_else(|| &self.tool);

        // Check if the backend exists and fail if it doesn't
        let backend = match ba.backend() {
            Ok(b) => Some(b),
            Err(e) => {
                // If no versions are configured for this tool, it's likely invalid
                if tvl.is_none() {
                    return Err(e);
                }
                None
            }
        };

        let (description, security) = if let Some(backend) = &backend {
            (backend.description().await, backend.security_info().await)
        } else {
            (None, vec![])
        };
        let info = ToolInfo {
            backend: ba.full(),
            description,
            installed_versions: ts
                .list_installed_versions(&config)
                .await?
                .into_iter()
                .filter(|(b, _)| b.ba().as_ref() == ba)
                .map(|(_, tv)| tv.version)
                .unique()
                .collect::<Vec<_>>(),
            active_versions: tvl.map(|tvl| {
                tvl.versions
                    .iter()
                    .map(|tv| tv.version.clone())
                    .collect::<Vec<_>>()
            }),
            requested_versions: tvl.map(|tvl| {
                tvl.requests
                    .iter()
                    .map(|tr| tr.version())
                    .collect::<Vec<_>>()
            }),
            config_source: tvl.map(|tvl| tvl.source.clone()),
            tool_options: ba.opts(),
            security,
        };

        if self.json {
            self.output_json(info)
        } else {
            self.output_user(info)
        }
    }

    fn output_json(&self, info: ToolInfo) -> Result<()> {
        if self.filter.backend_ {
            miseprintln!("{}", serde_json::to_string_pretty(&info.backend)?);
        } else if self.filter.description {
            miseprintln!("{}", serde_json::to_string_pretty(&info.description)?);
        } else if self.filter.installed {
            miseprintln!(
                "{}",
                serde_json::to_string_pretty(&info.installed_versions)?
            );
        } else if self.filter.active {
            miseprintln!("{}", serde_json::to_string_pretty(&info.active_versions)?);
        } else if self.filter.requested {
            miseprintln!(
                "{}",
                serde_json::to_string_pretty(&info.requested_versions)?
            );
        } else if self.filter.config_source {
            miseprintln!("{}", serde_json::to_string_pretty(&info.config_source)?);
        } else if self.filter.tool_options {
            miseprintln!("{}", serde_json::to_string_pretty(&info.tool_options)?);
        } else {
            miseprintln!("{}", serde_json::to_string_pretty(&info)?);
        }
        Ok(())
    }

    fn output_user(&self, info: ToolInfo) -> Result<()> {
        if self.filter.backend_ {
            miseprintln!("{}", info.backend);
        } else if self.filter.description {
            if let Some(description) = info.description {
                miseprintln!("{}", description);
            } else {
                miseprintln!("[none]");
            }
        } else if self.filter.installed {
            let active_set = info
                .active_versions
                .as_ref()
                .map(|v| v.iter().collect::<std::collections::HashSet<_>>())
                .unwrap_or_default();
            let installed_with_bold = info
                .installed_versions
                .iter()
                .map(|v| {
                    if active_set.contains(v) {
                        style::nbold(v).to_string()
                    } else {
                        v.to_string()
                    }
                })
                .join(" ");
            miseprintln!("{}", installed_with_bold);
        } else if self.filter.active {
            if let Some(active_versions) = info.active_versions {
                miseprintln!("{}", active_versions.join(" "));
            } else {
                miseprintln!("[none]");
            }
        } else if self.filter.requested {
            if let Some(requested_versions) = info.requested_versions {
                miseprintln!("{}", requested_versions.join(" "));
            } else {
                miseprintln!("[none]");
            }
        } else if self.filter.config_source {
            if let Some(config_source) = info.config_source {
                miseprintln!("{}", config_source);
            } else {
                miseprintln!("[none]");
            }
        } else if self.filter.tool_options {
            if info.tool_options.is_empty() {
                miseprintln!("[none]");
            } else {
                for (k, v) in info.tool_options.opts {
                    miseprintln!("{k}={v:?}");
                }
            }
        } else {
            let mut table = vec![];
            table.push(("Backend:", info.backend));
            if let Some(description) = info.description {
                table.push(("Description:", description));
            }
            // Bold currently active versions within the installed list for clarity
            let active_set = info
                .active_versions
                .as_ref()
                .map(|v| v.iter().collect::<std::collections::HashSet<_>>())
                .unwrap_or_default();
            let installed_with_bold = info
                .installed_versions
                .iter()
                .map(|v| {
                    if active_set.contains(v) {
                        style::nbold(v).to_string()
                    } else {
                        v.to_string()
                    }
                })
                .join(" ");
            table.push(("Installed Versions:", installed_with_bold));
            if let Some(active_versions) = info.active_versions {
                table.push((
                    "Active Version:",
                    style::nbold(active_versions.join(" ")).to_string(),
                ));
            }
            if let Some(requested_versions) = info.requested_versions {
                table.push(("Requested Version:", requested_versions.join(" ")));
            }
            if let Some(config_source) = info.config_source {
                table.push(("Config Source:", config_source.to_string()));
            }
            if info.tool_options.is_empty() {
                table.push(("Tool Options:", "[none]".to_string()));
            } else {
                table.push((
                    "Tool Options:",
                    info.tool_options
                        .opts
                        .into_iter()
                        .map(|(k, v)| format!("{k}={v:?}"))
                        .join(","),
                ));
            }
            if info.security.is_empty() {
                table.push(("Security:", "[none]".to_string()));
            } else {
                let security_str = info
                    .security
                    .iter()
                    .map(|f| match f {
                        SecurityFeature::Checksum { algorithm } => {
                            if let Some(alg) = algorithm {
                                format!("checksum ({})", alg)
                            } else {
                                "checksum".to_string()
                            }
                        }
                        SecurityFeature::GithubAttestations { .. } => {
                            "github_attestations".to_string()
                        }
                        SecurityFeature::Slsa { level } => {
                            if let Some(l) = level {
                                format!("slsa (level {})", l)
                            } else {
                                "slsa".to_string()
                            }
                        }
                        SecurityFeature::Cosign => "cosign".to_string(),
                        SecurityFeature::Minisign { .. } => "minisign".to_string(),
                        SecurityFeature::Gpg => "gpg".to_string(),
                    })
                    .join(", ");
                table.push(("Security:", security_str));
            }
            let mut table = tabled::Table::new(table);
            table::default_style(&mut table, true);
            miseprintln!("{table}");
        }

        Ok(())
    }
}

#[derive(Serialize)]
struct ToolInfo {
    backend: String,
    description: Option<String>,
    installed_versions: Vec<String>,
    requested_versions: Option<Vec<String>>,
    active_versions: Option<Vec<String>>,
    config_source: Option<ToolSource>,
    tool_options: ToolVersionOptions,
    security: Vec<SecurityFeature>,
}

static AFTER_LONG_HELP: &str = color_print::cstr!(
    r#"<bold><underline>Examples:</underline></bold>

    $ <bold>mise tool node</bold>
    Backend:            core
    Installed Versions: 20.0.0 22.0.0
    Active Version:     20.0.0
    Requested Version:  20
    Config Source:      ~/.config/mise/mise.toml
    Tool Options:       [none]
"#
);