dx_forge/api/lifecycle.rs
1//! Core Lifecycle & System Orchestration APIs
2
3use anyhow::{Context, Result};
4use parking_lot::RwLock;
5use std::sync::{Arc, Once};
6use std::path::PathBuf;
7use std::collections::HashMap;
8
9use crate::orchestrator::{DxTool, ExecutionContext};
10use crate::core::Forge;
11
12// Global forge instance
13static INIT: Once = Once::new();
14static mut FORGE_INSTANCE: Option<Arc<RwLock<Forge>>> = None;
15static mut TOOL_REGISTRY: Option<Arc<RwLock<HashMap<String, Arc<RwLock<Box<dyn DxTool>>>>>>> = None;
16static mut CURRENT_CONTEXT: Option<Arc<RwLock<ExecutionContext>>> = None;
17
18/// Global one-time initialization (dx binary, LSP, editor extension, daemon)
19///
20/// This must be called exactly once at application startup before using any other forge APIs.
21/// It initializes the global forge instance, LSP server, file watchers, and all core systems.
22///
23/// # Example
24/// ```no_run
25/// use dx_forge::initialize_forge;
26///
27/// fn main() -> anyhow::Result<()> {
28/// initialize_forge()?;
29/// // Now forge is ready to use
30/// Ok(())
31/// }
32/// ```
33pub fn initialize_forge() -> Result<()> {
34 let mut init_result = Ok(());
35
36 INIT.call_once(|| {
37 tracing::info!("๐ Initializing Forge v{}", crate::VERSION);
38
39 // Detect project root (walk up to find .dx or .git)
40 let project_root = detect_workspace_root().unwrap_or_else(|_| {
41 std::env::current_dir().expect("Failed to get current directory")
42 });
43
44 tracing::info!("๐ Project root: {:?}", project_root);
45
46 // Create forge instance
47 match Forge::new(&project_root) {
48 Ok(forge) => {
49 unsafe {
50 FORGE_INSTANCE = Some(Arc::new(RwLock::new(forge)));
51 TOOL_REGISTRY = Some(Arc::new(RwLock::new(HashMap::new())));
52
53 // Create initial execution context
54 let forge_path = project_root.join(".dx/forge");
55 let context = ExecutionContext::new(project_root.clone(), forge_path);
56 CURRENT_CONTEXT = Some(Arc::new(RwLock::new(context)));
57 }
58
59 tracing::info!("โ
Forge initialization complete");
60 }
61 Err(e) => {
62 init_result = Err(e).context("Failed to initialize forge");
63 }
64 }
65 });
66
67 init_result
68}
69
70/// Every dx-tool must call this exactly once during startup
71///
72/// Registers a tool with the forge orchestrator. Tools are indexed by name and
73/// version for dependency resolution and execution ordering.
74///
75/// # Arguments
76/// * `tool` - The tool implementation to register
77///
78/// # Returns
79/// A unique tool ID for subsequent operations
80///
81/// # Example
82/// ```no_run
83/// use dx_forge::{register_tool, DxTool};
84///
85/// struct MyTool;
86/// impl DxTool for MyTool {
87/// fn name(&self) -> &str { "my-tool" }
88/// fn version(&self) -> &str { "1.0.0" }
89/// fn priority(&self) -> u32 { 50 }
90/// fn execute(&mut self, _ctx: &dx_forge::ExecutionContext) -> anyhow::Result<dx_forge::ToolOutput> {
91/// Ok(dx_forge::ToolOutput::success())
92/// }
93/// }
94///
95/// fn main() -> anyhow::Result<()> {
96/// dx_forge::initialize_forge()?;
97/// register_tool(Box::new(MyTool))?;
98/// Ok(())
99/// }
100/// ```
101pub fn register_tool(tool: Box<dyn DxTool>) -> Result<String> {
102 ensure_initialized()?;
103
104 let tool_name = tool.name().to_string();
105 let tool_version = tool.version().to_string();
106 let tool_id = format!("{}@{}", tool_name, tool_version);
107
108 tracing::info!("๐ฆ Registering tool: {}", tool_id);
109
110 unsafe {
111 if let Some(registry) = &TOOL_REGISTRY {
112 let tool_arc = Arc::new(RwLock::new(tool));
113 registry.write().insert(tool_id.clone(), tool_arc);
114 }
115 }
116
117 Ok(tool_id)
118}
119
120/// Returns the live, immutable ToolContext for the current operation
121///
122/// Provides access to the execution context including repository state,
123/// changed files, and shared data between tools.
124///
125/// # Returns
126/// A clone of the current execution context
127///
128/// # Example
129/// ```no_run
130/// use dx_forge::get_tool_context;
131///
132/// fn my_operation() -> anyhow::Result<()> {
133/// let ctx = get_tool_context()?;
134/// println!("Working in: {:?}", ctx.repo_root);
135/// Ok(())
136/// }
137/// ```
138pub fn get_tool_context() -> Result<ExecutionContext> {
139 ensure_initialized()?;
140
141 unsafe {
142 if let Some(context) = &CURRENT_CONTEXT {
143 Ok(context.read().clone())
144 } else {
145 anyhow::bail!("Tool context not available")
146 }
147 }
148}
149
150/// Full graceful shutdown with progress reporting and cleanup
151///
152/// Shuts down all running tools, flushes caches, closes file watchers,
153/// and performs cleanup. Should be called before application exit.
154///
155/// # Example
156/// ```no_run
157/// use dx_forge::shutdown_forge;
158///
159/// fn main() -> anyhow::Result<()> {
160/// dx_forge::initialize_forge()?;
161/// // ... do work ...
162/// shutdown_forge()?;
163/// Ok(())
164/// }
165/// ```
166pub fn shutdown_forge() -> Result<()> {
167 tracing::info!("๐ Shutting down Forge...");
168
169 unsafe {
170 // Clear tool registry
171 if let Some(registry) = TOOL_REGISTRY.take() {
172 let count = registry.read().len();
173 tracing::info!("๐ฆ Unregistering {} tools", count);
174 drop(registry);
175 }
176
177 // Drop forge instance (triggers Drop impl cleanup)
178 if let Some(forge) = FORGE_INSTANCE.take() {
179 tracing::info!("๐งน Cleaning up forge instance");
180 drop(forge);
181 }
182
183 // Clear context
184 CURRENT_CONTEXT = None;
185 }
186
187 tracing::info!("โ
Forge shutdown complete");
188 Ok(())
189}
190
191// Helper functions
192
193fn ensure_initialized() -> Result<()> {
194 unsafe {
195 if FORGE_INSTANCE.is_none() {
196 anyhow::bail!("Forge not initialized. Call initialize_forge() first.");
197 }
198 }
199 Ok(())
200}
201
202fn detect_workspace_root() -> Result<PathBuf> {
203 let mut current = std::env::current_dir()?;
204
205 loop {
206 // Check for .dx directory
207 if current.join(".dx").exists() {
208 return Ok(current);
209 }
210
211 // Check for .git directory
212 if current.join(".git").exists() {
213 return Ok(current);
214 }
215
216 // Move up one directory
217 if let Some(parent) = current.parent() {
218 current = parent.to_path_buf();
219 } else {
220 // Reached filesystem root
221 break;
222 }
223 }
224
225 // Default to current directory
226 Ok(std::env::current_dir()?)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use crate::orchestrator::ToolOutput;
233
234 struct TestTool;
235
236 impl DxTool for TestTool {
237 fn name(&self) -> &str { "test-tool" }
238 fn version(&self) -> &str { "1.0.0" }
239 fn priority(&self) -> u32 { 50 }
240 fn execute(&mut self, _ctx: &ExecutionContext) -> Result<ToolOutput> {
241 Ok(ToolOutput::success())
242 }
243 }
244
245 #[test]
246 fn test_lifecycle() {
247 // Note: Can only test once per process due to Once
248 initialize_forge().ok();
249
250 let result = register_tool(Box::new(TestTool));
251 assert!(result.is_ok());
252
253 let ctx = get_tool_context();
254 assert!(ctx.is_ok());
255 }
256}