Skip to main content

bot_forge/execution/
verifier.rs

1//! Installation detection and presentation-neutral preview construction.
2
3use std::thread;
4
5use crate::error::ForgeError;
6use crate::execution::environment::prepare_detection_environment;
7use crate::execution::install::check_tool;
8use crate::execution::runtime::from_plan;
9use crate::model::{InstallConfig, InstallPreview, SkillStatus, ToolDef, ToolStatus};
10use crate::planning::ExecutionPlan;
11use crate::skills::agent_dir;
12
13/// Detect current tool and skill state for the components in an execution plan.
14///
15/// # Errors
16///
17/// Returns [`ForgeError`] when detection environment preparation or a bounded worker fails.
18pub fn preview_plan(plan: &ExecutionPlan) -> Result<InstallPreview, ForgeError> {
19    preview_install(&from_plan(plan))
20}
21
22pub(crate) fn preview_install(config: &InstallConfig) -> Result<InstallPreview, ForgeError> {
23    // Prepare the process environment before spawning detection workers. Keeping
24    // this outside the worker pool avoids concurrent process-environment writes.
25    prepare_detection_environment(config);
26    let tool_status = check_tools_bounded(&config.tools)?;
27
28    let mut skill_status = Vec::new();
29    for skill in &config.skills {
30        for agent in &skill.agents {
31            let agent_dir = agent_dir(*agent);
32            let installed = agent_dir
33                .as_ref()
34                .is_some_and(|root| root.join(&skill.name).join("SKILL.md").is_file());
35            skill_status.push(SkillStatus {
36                name: skill.name.clone(),
37                display_name: skill.display_name.clone(),
38                optional: skill.optional,
39                agent: *agent,
40                agent_dir,
41                installed,
42                installable: !skill.source.trim().is_empty(),
43            });
44        }
45    }
46    Ok(InstallPreview {
47        tools: tool_status,
48        skills: skill_status,
49    })
50}
51
52pub(crate) fn check_tools_bounded(tools: &[ToolDef]) -> Result<Vec<ToolStatus>, ForgeError> {
53    if tools.len() <= 1 {
54        return Ok(tools.iter().map(check_tool).collect());
55    }
56    let workers = thread::available_parallelism()
57        .map_or(2, usize::from)
58        .clamp(1, 4)
59        .min(tools.len());
60    let chunk_size = tools.len().div_ceil(workers);
61    let mut indexed = thread::scope(|scope| -> Result<Vec<_>, ForgeError> {
62        let handles = tools
63            .chunks(chunk_size)
64            .enumerate()
65            .map(|(chunk_index, chunk)| {
66                scope.spawn(move || {
67                    chunk
68                        .iter()
69                        .enumerate()
70                        .map(|(offset, tool)| (chunk_index * chunk_size + offset, check_tool(tool)))
71                        .collect::<Vec<_>>()
72                })
73            })
74            .collect::<Vec<_>>();
75        let mut indexed = Vec::with_capacity(tools.len());
76        for handle in handles {
77            indexed.extend(
78                handle
79                    .join()
80                    .map_err(|_| ForgeError::Command("tool check worker panicked".into()))?,
81            );
82        }
83        Ok(indexed)
84    })?;
85    indexed.sort_by_key(|(index, _)| *index);
86    Ok(indexed.into_iter().map(|(_, status)| status).collect())
87}