mise 2026.9.4

Dev tools, env vars, and tasks in one CLI
use eyre::Result;

use crate::config::{Config, Settings};
use crate::system;
use crate::system::history::OperationScope;
use crate::ui::prompt;

/// Remove dotfiles applied from `[dotfiles]`
///
/// Removes configured whole-file entries and edits while preserving files
/// mise cannot identify as managed. Modified copies, templates, and plain-line
/// edits require `--force`. Source files and configuration entries are retained.
/// Run this before deleting a declaration so mise can still identify its targets.
#[derive(Debug, usage_rs::Args)]
#[usage(
    verbatim_doc_comment,
    example(
        r###"mise bootstrap dotfiles unapply
mise bootstrap dotfiles unapply ~/.zshrc
mise bootstrap dotfiles unapply --dry-run
mise bootstrap dotfiles unapply --force --yes"###
    )
)]
pub(crate) struct DotfilesUnapply {
    /// Only unapply these targets
    #[usage(value_name = "TARGET")]
    targets: Vec<String>,

    /// Remove modified or otherwise ambiguous managed files and lines
    #[usage(long, short)]
    force: bool,

    /// Print the actions that would run without writing anything
    #[usage(long, short = 'n')]
    dry_run: bool,

    /// Skip the confirmation prompt
    #[usage(long, short)]
    yes: bool,
}

impl DotfilesUnapply {
    pub(crate) async fn run(self) -> Result<()> {
        OperationScope::wrap("bootstrap dotfiles unapply", self.dry_run, self.run_inner()).await
    }

    async fn run_inner(self) -> Result<()> {
        let config = Config::get().await?;
        let all_files = system::files::files_from_config(&config)?;
        let files = all_files
            .iter()
            .filter(|req| {
                system::files::matches_target(&req.target, &req.target_raw, &self.targets)
            })
            .cloned()
            .collect::<Vec<_>>();
        let all_edits = system::edits::edits_from_config(&config)?;
        let edits = all_edits
            .iter()
            .filter(|req| system::edits::matches_target(req, &self.targets))
            .cloned()
            .collect::<Vec<_>>();
        if files.is_empty()
            && edits.is_empty()
            && !self.targets.is_empty()
            && (!all_files.is_empty() || !all_edits.is_empty())
        {
            eyre::bail!(
                "no dotfiles matched target filter: {}",
                self.targets.join(", ")
            );
        }
        if files.is_empty() && edits.is_empty() {
            super::warn_if_dotfiles_ignored();
            info!("no dotfiles configured in [dotfiles]");
            return Ok(());
        }

        let mut edit_opts = system::edits::UnapplyOpts {
            dry_run: self.dry_run,
            verbose: Settings::get().verbose,
            force: self.force,
            yes: self.yes,
        };
        let mut file_opts = system::files::UnapplyOpts {
            dry_run: self.dry_run,
            verbose: Settings::get().verbose,
            force: self.force,
            yes: self.yes,
        };

        // Validate and plan both domains before either mutates the filesystem.
        // Apply writes whole files before edits, so execute the inverse order.
        let edit_plan = system::edits::plan_unapply(&edits, &edit_opts)?;
        let mut file_plan = system::files::plan_unapply(&files, &file_opts)?;
        if !self.dry_run
            && !self.yes
            && console::user_attended_stderr()
            && (!edit_plan.is_empty() || !file_plan.is_empty())
            && !prompt::confirm(format!(
                "dotfiles: unapply {} file(s) and {} edit(s)?",
                file_plan.len(),
                edit_plan.len()
            ))?
            .is_yes()
        {
            info!("dotfiles: skipped");
            return Ok(());
        }
        system::files::resolve_unapply(&config, &mut file_plan, &file_opts)?;
        system::edits::validate_unapply(&edit_plan)?;
        // Confirmation covers the complete validated plan. Suppress the
        // per-domain prompts so declining cannot leave a partial unapply.
        edit_opts.yes = true;
        file_opts.yes = true;
        if !edits.is_empty() {
            system::edits::execute_unapply(&edit_plan, &edit_opts)?;
        }
        if !files.is_empty() {
            system::files::execute_unapply(&file_plan, &file_opts)?;
        }
        Ok(())
    }
}