spec_ai_core/
bootstrap_self.rs1pub 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 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 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 self.plugins.get_by_names(&plugin_names)?
66 } else {
67 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 if root_node_id.is_none() {
102 root_node_id = outcome.root_node_id;
103 }
104
105 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 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}