bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Automatic APT source connectivity checks and fallback mirror configuration during install.

use std::collections::BTreeSet;
use std::path::PathBuf;

use crate::backends::apt_mirror::{
    apply_apt_mirror, apt_mirror_preview, check_apt_mirror, check_current_apt_sources,
};
use crate::config::schema::NetworkPolicy;
use crate::error::ForgeError;
use crate::fsutil::create_dir_all;
use crate::model::{InstallConfig, InstallOptions};
use crate::paths::app_home;
use crate::planning::ExecutionPlan;
use crate::ui::input::{PromptOutcome, confirm};
use crate::ui::{self, StatusKind};

pub(crate) struct AptSourceOverride {
    pub(crate) source_file: PathBuf,
    pub(crate) lists_dir: PathBuf,
}

pub(crate) fn configure_for_install(
    plan: &ExecutionPlan,
    config: &InstallConfig,
    options: &InstallOptions,
    required_packages: &BTreeSet<String>,
) -> Result<Option<AptSourceOverride>, ForgeError> {
    if required_packages.is_empty()
        || plan.policy.network != NetworkPolicy::Online
        || !available_for_install(config)
    {
        return Ok(None);
    }

    let preview = apt_mirror_preview(config.apt_mirror.as_ref())?;
    ui::stdout_status(
        StatusKind::Info,
        "Checking the current APT sources in isolated state.",
    );
    match check_current_apt_sources(&preview, required_packages, |message| {
        ui::stderr_status(StatusKind::Warning, message)
    }) {
        Ok(()) => {
            ui::stdout_status(
                StatusKind::Success,
                "Current APT sources are reachable; mirror configuration skipped.",
            );
            return Ok(None);
        }
        Err(error) => ui::stderr_status(
            StatusKind::Warning,
            &format!("Current APT source check failed: {error}"),
        ),
    }

    if options.yes {
        ui::stdout_status(
            StatusKind::Info,
            "APT mirror configuration skipped because writing system sources requires interactive confirmation.",
        );
        return Ok(None);
    }
    if !prompt_confirmed(confirm(
        "The current APT source check failed. Validate and write the configured replacement mirror? Existing sources will not be removed or disabled.",
        false,
    )?)? {
        ui::stdout_status(StatusKind::Info, "APT mirror configuration skipped.");
        return Ok(None);
    }

    ui::stdout_status(
        StatusKind::Info,
        &format!("Validating APT mirror: {}", preview.source_file.display()),
    );
    check_apt_mirror(&preview, |message| {
        ui::stderr_status(StatusKind::Warning, message)
    })?;
    apply_apt_mirror(&preview)?;
    ui::stdout_status(
        StatusKind::Success,
        &format!("APT mirror configured: {}", preview.source_file.display()),
    );
    let lists_dir = app_home().join("apt").join("lists");
    create_dir_all(&lists_dir.join("partial"))?;
    Ok(Some(AptSourceOverride {
        source_file: preview.source_file,
        lists_dir,
    }))
}

fn available_for_install(config: &InstallConfig) -> bool {
    cfg!(target_os = "linux")
        && config
            .apt_mirror
            .as_ref()
            .is_some_and(|mirror| mirror.uri.is_some() || !mirror.rules.is_empty())
}

fn prompt_confirmed(outcome: PromptOutcome<()>) -> Result<bool, ForgeError> {
    match outcome {
        PromptOutcome::Confirmed(()) => Ok(true),
        PromptOutcome::Cancelled => Ok(false),
        PromptOutcome::Interrupted => Err(ForgeError::Command(
            "operation interrupted by Ctrl-C".to_string(),
        )),
        PromptOutcome::Unavailable => Err(ForgeError::Command(
            "a non-interactive terminal cannot confirm this operation; pass --yes explicitly"
                .to_string(),
        )),
    }
}

#[cfg(test)]
mod tests {
    use crate::execution::apt_mirror::prompt_confirmed;
    use crate::ui::input::PromptOutcome;

    #[test]
    fn mirror_replacement_requires_an_explicit_confirmation() {
        assert!(prompt_confirmed(PromptOutcome::Confirmed(())).unwrap());
        assert!(!prompt_confirmed(PromptOutcome::Cancelled).unwrap());
        assert!(prompt_confirmed(PromptOutcome::Interrupted).is_err());
        assert!(prompt_confirmed(PromptOutcome::Unavailable).is_err());
    }
}