mise 2026.9.4

Dev tools, env vars, and tasks in one CLI
use crate::cli::args::ToolArg;
use crate::config::{Config, Settings};
use crate::task::task_context_builder::TaskContextBuilder;
use crate::task::task_helpers::canonicalize_path;
use crate::task::{Deps, Task};
use crate::toolset::{InstallOptions, ToolSource, ToolVersion, Toolset};
use eyre::Result;
use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;

/// Handles collection and installation of tools required by tasks
pub(crate) struct TaskToolInstaller<'a> {
    context_builder: &'a TaskContextBuilder,
    cli_tools: &'a [ToolArg],
}

impl<'a> TaskToolInstaller<'a> {
    pub(crate) fn new(context_builder: &'a TaskContextBuilder, cli_tools: &'a [ToolArg]) -> Self {
        Self {
            context_builder,
            cli_tools,
        }
    }

    /// Collect and install all tools needed by tasks
    pub(crate) async fn install_tools(
        &self,
        config: &mut Arc<Config>,
        tasks: &Deps,
        dry_run: bool,
        previewed_tools: &HashSet<ToolVersion>,
    ) -> Result<()> {
        let all_tasks: Vec<_> = tasks.all().collect();
        let all_tool_requests = self.collect_tool_requests(config, all_tasks).await?;

        // Build and install toolset
        let toolset = self
            .build_toolset(config, self.cli_tools.to_vec(), all_tool_requests)
            .await?;
        self.install_toolset(config, toolset, dry_run, previewed_tools)
            .await?;

        Ok(())
    }

    /// Collect every tool request needed to prepare the supplied tasks without
    /// executing their commands or dependency graphs.
    pub(crate) async fn collect_tool_requests<'t>(
        &self,
        config: &Arc<Config>,
        tasks: impl IntoIterator<Item = &'t Task>,
    ) -> Result<Vec<crate::toolset::ToolRequest>> {
        let tasks = tasks.into_iter().collect::<Vec<_>>();
        let mut requests = vec![];
        let mut seen_config_roots = HashSet::new();

        trace!("Collecting tools from {} tasks", tasks.len());

        for task in tasks {
            requests.extend(task.tool_args()?.into_iter().filter_map(|tool| tool.tvr));

            // Task execution combines task-level tools with the config hierarchy
            // that owns the task. Keep pre-installation consistent with that
            // environment, including file and monorepo tasks.
            let config_root = task
                .cf(config)
                .map(|task_cf| task_cf.config_root())
                .or_else(|| task.config_root.clone());
            if let Some(config_root) = config_root {
                let config_root = canonicalize_path(&config_root);
                if seen_config_roots.insert(config_root.clone()) {
                    requests.extend(
                        self.collect_tools_from_dir(&config_root, &task.name)
                            .await?,
                    );
                }
            }
        }

        Ok(requests)
    }

    /// Collect tools from config files found in a directory hierarchy
    async fn collect_tools_from_dir(
        &self,
        dir: &Path,
        task_name: &str,
    ) -> Result<Vec<crate::toolset::ToolRequest>> {
        let (config_paths, idiomatic_filenames) =
            crate::config::load_config_hierarchy_from_dir(dir).await?;
        let task_config_files =
            crate::config::load_config_files_from_paths(&config_paths, &idiomatic_filenames)
                .await?;

        let mut tool_requests: Vec<crate::toolset::ToolRequest> = vec![];
        let mut seen_tools: std::collections::HashSet<String> = std::collections::HashSet::new();

        for (source, cf) in task_config_files.iter() {
            let config_path = canonicalize_path(source);

            // Check cache first for this config file's tool request set
            let trs = {
                let cache = self
                    .context_builder
                    .tool_request_set_cache()
                    .read()
                    .expect("tool_request_set_cache RwLock poisoned");
                cache.get(&config_path).cloned()
            };

            let trs = if let Some(cached) = trs {
                trace!(
                    "Using cached tool request set from {}",
                    config_path.display()
                );
                cached
            } else {
                match cf.to_tool_request_set() {
                    Ok(trs) => {
                        let trs = Arc::new(trs);
                        let mut cache = self
                            .context_builder
                            .tool_request_set_cache()
                            .write()
                            .expect("tool_request_set_cache RwLock poisoned");
                        cache.insert(config_path.clone(), Arc::clone(&trs));
                        trace!("Cached tool request set from {}", config_path.display());
                        trs
                    }
                    Err(e) => {
                        warn!(
                            "Failed to parse tools from {} for task {}: {}",
                            source.display(),
                            task_name,
                            e
                        );
                        continue;
                    }
                }
            };

            for (ba, reqs) in trs.tools.iter() {
                let tool_key = ba.to_string();
                if !seen_tools.contains(&tool_key) {
                    trace!(
                        "Adding tool {} from {} for task {}",
                        ba,
                        source.display(),
                        task_name
                    );
                    tool_requests.extend(reqs.iter().cloned());
                    seen_tools.insert(tool_key);
                }
            }
        }

        trace!(
            "Found {} tool requests in config hierarchy for task {}",
            tool_requests.len(),
            task_name
        );

        Ok(tool_requests)
    }

    /// Build a toolset from CLI tools and collected tool requests
    async fn build_toolset(
        &self,
        config: &Arc<Config>,
        all_tools: Vec<ToolArg>,
        all_tool_requests: Vec<crate::toolset::ToolRequest>,
    ) -> Result<Toolset> {
        let source = ToolSource::Argument;
        let mut ts = Toolset::new(source.clone());

        // Add tools from CLI args and task.tools
        for tool_arg in all_tools {
            if let Some(tvr) = tool_arg.tvr {
                ts.add_version(tvr);
            }
        }

        // Add tools from config files
        for tr in all_tool_requests {
            trace!("Adding tool from config: {}", tr);
            ts.add_version(tr);
        }

        ts.resolve(config).await?;

        Ok(ts)
    }

    /// Install missing versions from the toolset
    async fn install_toolset(
        &self,
        config: &mut Arc<Config>,
        mut ts: Toolset,
        dry_run: bool,
        previewed_tools: &HashSet<ToolVersion>,
    ) -> Result<()> {
        if dry_run {
            for tvl in ts.versions.values_mut() {
                tvl.versions.retain(|tv| !previewed_tools.contains(tv));
            }
        }
        let (_, missing) = ts
            .install_missing_versions(
                config,
                &InstallOptions {
                    dry_run,
                    missing_args_only: !Settings::get().task.run_auto_install,
                    skip_auto_install: !Settings::get().task.run_auto_install
                        || !Settings::get().auto_install,
                    ..Default::default()
                },
            )
            .await?;
        if !dry_run && let Err(err) = crate::shims::ensure_lazy_shims(&missing) {
            warn!("failed to create shims for lazy tools: {err:#}");
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_task_tool_installer_new() {
        let context_builder = TaskContextBuilder::new();
        let cli_tools: Vec<ToolArg> = vec![];
        let installer = TaskToolInstaller::new(&context_builder, &cli_tools);
        assert_eq!(installer.cli_tools.len(), 0);
    }
}