bot-forge 1.0.2

Rust CLI for installing agent skills and developer tools from configurable forms.
Documentation
//! Installation detection and presentation-neutral preview construction.

use std::thread;

use crate::error::ForgeError;
use crate::execution::environment::prepare_detection_environment;
use crate::execution::install::check_tool;
use crate::execution::runtime::from_plan;
use crate::model::{InstallConfig, InstallPreview, SkillStatus, ToolDef, ToolStatus};
use crate::planning::ExecutionPlan;
use crate::skills::agent_dir;

/// Detect current tool and skill state for the components in an execution plan.
///
/// # Errors
///
/// Returns [`ForgeError`] when detection environment preparation or a bounded worker fails.
pub fn preview_plan(plan: &ExecutionPlan) -> Result<InstallPreview, ForgeError> {
    preview_install(&from_plan(plan))
}

pub(crate) fn preview_install(config: &InstallConfig) -> Result<InstallPreview, ForgeError> {
    // Prepare the process environment before spawning detection workers. Keeping
    // this outside the worker pool avoids concurrent process-environment writes.
    prepare_detection_environment(config);
    let tool_status = check_tools_bounded(&config.tools)?;

    let mut skill_status = Vec::new();
    for skill in &config.skills {
        for agent in &skill.agents {
            let agent_dir = agent_dir(*agent);
            let installed = agent_dir
                .as_ref()
                .is_some_and(|root| root.join(&skill.name).join("SKILL.md").is_file());
            skill_status.push(SkillStatus {
                name: skill.name.clone(),
                display_name: skill.display_name.clone(),
                optional: skill.optional,
                agent: *agent,
                agent_dir,
                installed,
                installable: !skill.source.trim().is_empty(),
            });
        }
    }
    Ok(InstallPreview {
        tools: tool_status,
        skills: skill_status,
    })
}

pub(crate) fn check_tools_bounded(tools: &[ToolDef]) -> Result<Vec<ToolStatus>, ForgeError> {
    if tools.len() <= 1 {
        return Ok(tools.iter().map(check_tool).collect());
    }
    let workers = thread::available_parallelism()
        .map_or(2, usize::from)
        .clamp(1, 4)
        .min(tools.len());
    let chunk_size = tools.len().div_ceil(workers);
    let mut indexed = thread::scope(|scope| -> Result<Vec<_>, ForgeError> {
        let handles = tools
            .chunks(chunk_size)
            .enumerate()
            .map(|(chunk_index, chunk)| {
                scope.spawn(move || {
                    chunk
                        .iter()
                        .enumerate()
                        .map(|(offset, tool)| (chunk_index * chunk_size + offset, check_tool(tool)))
                        .collect::<Vec<_>>()
                })
            })
            .collect::<Vec<_>>();
        let mut indexed = Vec::with_capacity(tools.len());
        for handle in handles {
            indexed.extend(
                handle
                    .join()
                    .map_err(|_| ForgeError::Command("tool check worker panicked".into()))?,
            );
        }
        Ok(indexed)
    })?;
    indexed.sort_by_key(|(index, _)| *index);
    Ok(indexed.into_iter().map(|(_, status)| status).collect())
}