mise 2026.9.4

Dev tools, env vars, and tasks in one CLI
use std::path::PathBuf;

use eyre::Result;
use itertools::sorted;

use crate::{backend, config, file};
use crate::{config::Config, config::Settings};

use super::reconcile;

/// Symlink node versions installed by nvm, nodenv, or Homebrew into mise
///
/// Use this to make versions installed by another version manager available to mise.
///
/// This won't overwrite managed installs, runtime aliases, or links from other providers.
#[derive(Debug, usage_rs::Args)]
#[usage(
    verbatim_doc_comment,
    example(
        r###"brew install node@20
mise sync node --brew
mise use -g node@20 # uses Homebrew-provided node"###
    )
)]
pub(super) struct SyncNode {
    #[usage(flatten)]
    _type: SyncNodeType,
}

#[derive(Debug, usage_rs::Args)]
#[usage(group("sync-node-type", required, multiple))]
pub(super) struct SyncNodeType {
    /// Get tool versions from Homebrew
    #[usage(long, group = "sync-node-type")]
    brew: bool,

    /// Get tool versions from nodenv
    #[usage(long, group = "sync-node-type")]
    nodenv: bool,

    /// Get tool versions from nvm
    #[usage(long, group = "sync-node-type")]
    nvm: bool,
}

impl SyncNode {
    pub(super) async fn run(self) -> Result<()> {
        let node = backend::get(&"node".into()).unwrap();
        let mut providers = vec![];
        if self._type.brew {
            providers.push(self.brew_links()?);
        }
        if self._type.nvm {
            providers.push(self.nvm_links()?);
        }
        if self._type.nodenv {
            providers.push(self.nodenv_links()?);
        }
        let mut changed = reconcile::reconcile_all(node.ba(), providers)?.into_iter();
        if self._type.brew {
            for v in changed.next().unwrap_or_default() {
                miseprintln!("Synced node@{} from Homebrew", v);
            }
        }
        if self._type.nvm {
            for v in changed.next().unwrap_or_default() {
                miseprintln!("Synced node@{} from nvm", v);
            }
        }
        if self._type.nodenv {
            for v in changed.next().unwrap_or_default() {
                miseprintln!("Synced node@{} from nodenv", v);
            }
        }
        let config = Config::reset().await?;
        let ts = config.get_toolset().await?;
        config::rebuild_shims_and_runtime_symlinks(
            &config,
            ts,
            &[],
            crate::lockfile::LockfileUpdateMode::Normal,
        )
        .await?;
        Ok(())
    }

    fn brew_links(&self) -> Result<reconcile::ProviderLinks> {
        let brew_opt = PathBuf::from(cmd!("brew", "--prefix").read()?).join("opt");

        let subdirs = file::dir_subdirs(&brew_opt)?;
        let mut links = vec![];
        for entry in sorted(subdirs) {
            if entry.starts_with(".") {
                continue;
            }
            if !entry.starts_with("node@") {
                continue;
            }
            let v = entry.trim_start_matches("node@");
            links.push((v.to_string(), brew_opt.join(&entry)));
        }
        let ownership = reconcile::LinkOwnership::in_namespace(&brew_opt);
        Ok(reconcile::ProviderLinks::new(ownership, links))
    }

    fn nvm_links(&self) -> Result<reconcile::ProviderLinks> {
        let settings = Settings::get();

        let nvm_versions_path = file::replace_path(&settings.node.nvm_dir)
            .join("versions")
            .join("node");

        let subdirs = file::dir_subdirs(&nvm_versions_path)?;
        let mut links = vec![];
        for entry in sorted(subdirs) {
            if entry.starts_with(".") {
                continue;
            }
            let v = entry.trim_start_matches('v');
            links.push((v.to_string(), nvm_versions_path.join(&entry)));
        }
        let ownership = reconcile::LinkOwnership::in_namespace(&nvm_versions_path);
        Ok(reconcile::ProviderLinks::new(ownership, links))
    }

    fn nodenv_links(&self) -> Result<reconcile::ProviderLinks> {
        let settings = Settings::get();

        let nodenv_versions_path = file::replace_path(&settings.node.nodenv_root).join("versions");

        let subdirs = file::dir_subdirs(&nodenv_versions_path)?;
        let mut links = vec![];
        for v in sorted(subdirs) {
            if v.starts_with(".") {
                continue;
            }
            links.push((v.clone(), nodenv_versions_path.join(&v)));
        }
        let ownership = reconcile::LinkOwnership::in_namespace(&nodenv_versions_path);
        Ok(reconcile::ProviderLinks::new(ownership, links))
    }
}