spec_ai_core/
bootstrap_self.rs

1pub mod plugin;
2pub mod plugins;
3pub mod registry;
4
5use crate::persistence::Persistence;
6use anyhow::{anyhow, Context, Result};
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9
10use plugin::BootstrapMode;
11use plugins::{RustCargoPlugin, ToakTokenizerPlugin, UniversalCodePlugin};
12use registry::PluginRegistry;
13
14#[derive(Debug)]
15pub struct BootstrapOutcome {
16    pub repository_node_id: i64,
17    pub nodes_created: usize,
18    pub edges_created: usize,
19    pub repository_name: String,
20    pub component_count: usize,
21    pub document_count: usize,
22    pub phases: Vec<String>,
23}
24
25pub struct BootstrapSelf<'a> {
26    persistence: &'a Persistence,
27    session_id: &'a str,
28    repo_root: PathBuf,
29    plugins: PluginRegistry,
30}
31
32impl<'a> BootstrapSelf<'a> {
33    pub fn new(persistence: &'a Persistence, session_id: &'a str, repo_root: PathBuf) -> Self {
34        Self {
35            persistence,
36            session_id,
37            repo_root,
38            plugins: PluginRegistry::new(),
39        }
40    }
41
42    pub fn from_environment(persistence: &'a Persistence, session_id: &'a str) -> Result<Self> {
43        let repo_root = resolve_repo_root()?;
44        Ok(Self::new(persistence, session_id, repo_root))
45    }
46
47    /// Initialize the plugin registry with default plugins
48    fn init_plugins(&self) -> Result<()> {
49        self.plugins.register(Arc::new(RustCargoPlugin))?;
50        self.plugins.register(Arc::new(ToakTokenizerPlugin))?;
51        self.plugins.register(Arc::new(UniversalCodePlugin))?;
52        Ok(())
53    }
54
55    /// Run bootstrap with specified plugins, or auto-detect if plugins is None
56    pub fn run_with_plugins_mode(
57        &self,
58        plugins: Option<Vec<String>>,
59        mode: BootstrapMode,
60    ) -> Result<BootstrapOutcome> {
61        self.init_plugins()?;
62
63        let active_plugins = if let Some(plugin_names) = plugins {
64            // Use specified plugins
65            self.plugins.get_by_names(&plugin_names)?
66        } else {
67            // Auto-detect plugins
68            self.plugins.get_enabled(&self.repo_root)?
69        };
70
71        if active_plugins.is_empty() {
72            return Err(anyhow!(
73                "No bootstrap plugins found for repository at {}",
74                self.repo_root.display()
75            ));
76        }
77
78        let context = plugin::PluginContext {
79            persistence: self.persistence,
80            session_id: self.session_id,
81            repo_root: &self.repo_root,
82            mode,
83        };
84
85        let mut total_nodes = 0;
86        let mut total_edges = 0;
87        let mut all_phases = Vec::new();
88        let mut repository_name = String::new();
89        let mut component_count = 0;
90        let mut document_count = 0;
91        let mut root_node_id = None;
92
93        for plugin in active_plugins {
94            let outcome = plugin.run(context.clone())?;
95
96            total_nodes += outcome.nodes_created;
97            total_edges += outcome.edges_created;
98            all_phases.extend(outcome.phases);
99
100            // Use the first plugin's root node ID
101            if root_node_id.is_none() {
102                root_node_id = outcome.root_node_id;
103            }
104
105            // Extract metadata from first plugin that provides it
106            if let Some(name) = outcome
107                .metadata
108                .get("repository_name")
109                .and_then(|v| v.as_str())
110            {
111                repository_name = name.to_string();
112            }
113            if let Some(count) = outcome
114                .metadata
115                .get("component_count")
116                .and_then(|v| v.as_u64())
117            {
118                component_count = component_count.max(count as usize);
119            }
120            if let Some(count) = outcome
121                .metadata
122                .get("document_count")
123                .and_then(|v| v.as_u64())
124            {
125                document_count = document_count.max(count as usize);
126            }
127        }
128
129        let repository_node_id =
130            root_node_id.ok_or_else(|| anyhow!("No repository node created by plugins"))?;
131
132        Ok(BootstrapOutcome {
133            repository_node_id,
134            nodes_created: total_nodes,
135            edges_created: total_edges,
136            repository_name,
137            component_count,
138            document_count,
139            phases: all_phases,
140        })
141    }
142
143    /// Run bootstrap with auto-detection (backward compatibility)
144    pub fn run(&self) -> Result<BootstrapOutcome> {
145        self.run_with_plugins(None)
146    }
147
148    pub fn run_with_plugins(&self, plugins: Option<Vec<String>>) -> Result<BootstrapOutcome> {
149        self.run_with_plugins_mode(plugins, BootstrapMode::Fresh)
150    }
151
152    pub fn refresh_with_plugins(&self, plugins: Option<Vec<String>>) -> Result<BootstrapOutcome> {
153        self.run_with_plugins_mode(plugins, BootstrapMode::Refresh)
154    }
155}
156
157pub fn resolve_repo_root() -> Result<PathBuf> {
158    if let Ok(override_path) = std::env::var("SPEC_AI_BOOTSTRAP_ROOT") {
159        let candidate = PathBuf::from(override_path);
160        if candidate.exists() {
161            return Ok(candidate);
162        }
163    }
164
165    let cwd = std::env::current_dir().context("resolving current directory")?;
166    find_repo_root(&cwd).ok_or_else(|| {
167        anyhow!(
168            "Unable to find repository root starting from {}",
169            cwd.display()
170        )
171    })
172}
173
174fn find_repo_root(start: &Path) -> Option<PathBuf> {
175    let mut current = start.to_path_buf();
176    loop {
177        if current.join(".git").exists() || current.join("Cargo.toml").exists() {
178            return Some(current);
179        }
180        if !current.pop() {
181            break;
182        }
183    }
184    None
185}