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) = (*std::ptr::addr_of!(TOOL_REGISTRY)).as_ref() {
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) = (*std::ptr::addr_of!(CURRENT_CONTEXT)).as_ref() {
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) = (*std::ptr::addr_of_mut!(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) = (*std::ptr::addr_of_mut!(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 (*std::ptr::addr_of!(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}