mise 2026.9.1

Dev tools, env vars, and tasks in one CLI
#[cfg(unix)]
use std::collections::BTreeMap;
#[cfg(unix)]
use std::path::Path;
use std::path::PathBuf;

use eyre::{Result, bail};

#[cfg(unix)]
use crate::config::Config;
use crate::config::Settings;
#[cfg(unix)]
use crate::config::config_file::ConfigFile;
#[cfg(unix)]
use crate::config::config_file::mise_toml::MiseToml;
#[cfg(unix)]
use crate::config::{ConfigPathOptions, resolve_target_config_path};
#[cfg(unix)]
use crate::file::display_path;
#[cfg(unix)]
use crate::system;
#[cfg(unix)]
use crate::system::PackageTomlConfig;
#[cfg(unix)]
use crate::system::packages::SystemPackageManager;
#[cfg(unix)]
use crate::system::packages::brew;
#[cfg(unix)]
use toml_edit::{Array, InlineTable, Value};

/// Import installed system packages into `[bootstrap.packages]`
///
/// Currently supports Homebrew formulae only. By default, imports linked
/// formulae whose active keg receipt says they were installed on request.
/// Pass `--all` to import every linked formula, including dependencies.
#[derive(Debug, usage_rs::Args)]
#[usage(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub(crate) struct SystemImport {
    /// Write to the config file for this environment (mise.<ENV>.toml)
    #[usage(long, short, value_name = "ENV", conflicts = ["global", "path"])]
    env: Option<String>,

    /// Write to the global config (~/.config/mise/config.toml)
    #[usage(long, short, conflicts = ["env", "path"])]
    global: bool,

    /// Only import packages for this manager. Currently only `brew` is supported.
    #[usage(long, short, default = "brew", choices("brew"))]
    manager: String,

    /// Import every linked formula, including dependencies
    #[usage(long)]
    all: bool,

    /// Print the config change without writing config
    #[usage(long, short = 'n')]
    dry_run: bool,

    /// Write to this config file or directory
    #[usage(
        long,
        short,
        visible_alias = "file",
        value_name = "PATH",
        conflicts = "global"
    )]
    path: Option<PathBuf>,
}

impl SystemImport {
    pub(crate) async fn run(self) -> Result<()> {
        if Settings::get()
            .system_packages
            .managers
            .as_ref()
            .is_some_and(|enabled| !enabled.contains(&self.manager))
        {
            bail!(
                "manager '{}' is excluded by the system_packages.managers setting",
                self.manager
            );
        }
        self.run_brew().await
    }

    #[cfg(unix)]
    async fn run_brew(self) -> Result<()> {
        debug_assert_eq!(self.manager, "brew");
        let manager = brew::BrewManager::new();
        if !manager.is_available() {
            bail!("brew is not available: {}", manager.unavailable_reason());
        }
        let formulae = brew::linked_formulae(self.all)?;
        if formulae.is_empty() {
            info!("brew: no installed formulae to import");
            return Ok(());
        }

        let path = resolve_target_config_path(ConfigPathOptions {
            global: self.global,
            path: self.path.clone(),
            env: self.env.clone(),
            cwd: None,
            prefer_toml: true,
            prevent_home_local: true,
        })?;

        let configured_taps = configured_brew_taps(&path).await?;
        let config = Config::get().await?;
        let configured_packages = system::package_configs_for_target(&config, &path);
        let target_taps = target_brew_taps(&path)?;
        let target_packages = target_bootstrap_packages(&path)?;
        let mut taps = BTreeMap::new();
        for formula in &formulae {
            let Some((tap, url)) = formula.tap_entry_with_urls(&configured_taps)? else {
                continue;
            };
            if target_taps.contains_key(&tap) {
                continue;
            }
            taps.insert(tap, url);
        }

        if self.dry_run {
            for (tap, url) in &taps {
                miseprintln!(
                    "{}: [bootstrap.brew.taps].\"{}\" = \"{}\"",
                    display_path(&path),
                    tap,
                    url
                );
            }
            for formula in &formulae {
                let key = formula.config_key();
                if target_packages
                    .get(&key)
                    .is_some_and(|package| package.version() == "latest")
                {
                    continue;
                }
                let package = imported_package_value(
                    target_packages.get(&key),
                    configured_packages.get(&key),
                );
                miseprintln!("{}: \"{}\" = {}", display_path(&path), key, package);
            }
            return Ok(());
        }

        let mut cf = if path.exists() {
            MiseToml::from_file(&path)?
        } else {
            MiseToml::init(&path)
        };
        for (tap, url) in &taps {
            cf.update_bootstrap_brew_tap(tap, url)?;
        }
        for formula in &formulae {
            let key = formula.config_key();
            cf.update_bootstrap_package_with_fallback(
                &key,
                "latest",
                configured_packages.get(&key),
            )?;
        }
        cf.save()?;
        info!(
            "{}: imported {} brew formulae",
            display_path(&path),
            formulae.len()
        );
        Ok(())
    }

    #[cfg(not(unix))]
    async fn run_brew(self) -> Result<()> {
        let _ = self.manager;
        bail!("brew import is not supported on windows")
    }
}

#[cfg(unix)]
fn imported_package_value(
    target: Option<&PackageTomlConfig>,
    configured: Option<&PackageTomlConfig>,
) -> Value {
    let options = match target {
        Some(PackageTomlConfig::Options(options)) => Some(options),
        Some(PackageTomlConfig::Version(_)) => None,
        None => match configured {
            Some(PackageTomlConfig::Options(options))
                if !options.os.is_empty() || options.adopt.is_some() =>
            {
                Some(options)
            }
            _ => None,
        },
    };
    let Some(options) = options else {
        return Value::from("latest");
    };
    let mut table = InlineTable::new();
    table.insert("version", Value::from("latest"));
    if !options.os.is_empty() {
        let mut os = Array::new();
        os.extend(options.os.clone());
        table.insert("os", Value::Array(os));
    }
    if let Some(adopt) = options.adopt {
        table.insert("adopt", Value::from(adopt));
    }
    Value::InlineTable(table)
}

#[cfg(unix)]
async fn configured_brew_taps(path: &Path) -> Result<BTreeMap<String, String>> {
    let mut taps = BTreeMap::new();
    let config = Config::get().await?;
    for (tap, url) in system::brew_taps_from_config(&config) {
        taps.insert(tap, url);
    }
    for (tap, url) in target_brew_taps(path)? {
        taps.insert(tap, url);
    }
    Ok(taps)
}

#[cfg(unix)]
fn target_brew_taps(path: &Path) -> Result<BTreeMap<String, String>> {
    let mut taps = BTreeMap::new();
    if path.exists() {
        let cf = MiseToml::from_file(path)?;
        if let Some(sys) = cf.bootstrap_config() {
            for (tap, url) in sys.brew.taps {
                taps.insert(tap, url);
            }
        }
    }
    Ok(taps)
}

#[cfg(unix)]
fn target_bootstrap_packages(path: &Path) -> Result<BTreeMap<String, PackageTomlConfig>> {
    let mut packages = BTreeMap::new();
    if path.exists() {
        let cf = MiseToml::from_file(path)?;
        if let Some(sys) = cf.bootstrap_config() {
            for (spec, package) in sys.packages {
                packages.insert(spec, package);
            }
        }
    }
    Ok(packages)
}

#[cfg(all(test, unix))]
mod tests {
    use super::*;
    use crate::system::PackageOptionsTomlConfig;

    #[test]
    fn dry_run_preserves_inherited_selectors() {
        let inherited = PackageTomlConfig::Options(PackageOptionsTomlConfig {
            version: "1.0.0".to_string(),
            os: vec!["macos".to_string()],
            adopt: None,
        });
        assert_eq!(
            imported_package_value(None, Some(&inherited)).to_string(),
            r#"{ version = "latest", os = ["macos"] }"#
        );

        let adopted = PackageTomlConfig::Options(PackageOptionsTomlConfig {
            version: "1.0.0".to_string(),
            os: vec![],
            adopt: Some(true),
        });
        assert_eq!(
            imported_package_value(None, Some(&adopted)).to_string(),
            r#"{ version = "latest", adopt = true }"#
        );
    }
}

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

    $ <bold>mise bootstrap packages import --manager brew</bold>
    $ <bold>mise bootstrap packages import --manager brew --all</bold>
    $ <bold>mise bootstrap packages import --manager brew --global</bold>
    $ <bold>mise bootstrap packages import --manager brew --dry-run</bold>
"#
);