use anyhow::Result;
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebounceEventResult};
use pforge_config::parse_config;
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
pub async fn execute(config_path: &str, watch: bool) -> Result<()> {
eprintln!("Starting pforge in development mode...");
eprintln!(" Config: {}", config_path);
eprintln!(" Watch: {}", watch);
if !watch {
return super::serve::execute(config_path).await;
}
eprintln!("\n🔄 Hot reload enabled - watching for changes...");
let should_reload = Arc::new(AtomicBool::new(false));
let should_reload_watcher = should_reload.clone();
let (tx, mut rx) = mpsc::channel::<()>(1);
let config_path_owned = config_path.to_string();
let watcher_tx = tx.clone();
std::thread::spawn(move || {
let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime");
rt.block_on(async {
let mut debouncer = new_debouncer(
Duration::from_millis(500),
move |result: DebounceEventResult| {
if let Ok(events) = result {
if !events.is_empty() {
should_reload_watcher.store(true, Ordering::SeqCst);
let _ = watcher_tx.try_send(());
}
}
},
)
.expect("Failed to create file watcher");
let config_file = Path::new(&config_path_owned);
let watch_dir = config_file.parent().unwrap_or(Path::new("."));
debouncer
.watcher()
.watch(watch_dir, RecursiveMode::Recursive)
.expect("Failed to watch directory");
eprintln!(" Watching: {}", watch_dir.display());
loop {
tokio::time::sleep(Duration::from_secs(60)).await;
}
});
});
loop {
eprintln!("\n📦 Loading configuration...");
let config = match parse_config(Path::new(config_path)) {
Ok(c) => c,
Err(e) => {
eprintln!("❌ Configuration error: {}", e);
eprintln!(" Waiting for changes...");
rx.recv().await;
should_reload.store(false, Ordering::SeqCst);
continue;
}
};
eprintln!("✅ Loaded: {} v{}", config.forge.name, config.forge.version);
eprintln!(" Tools: {}", config.tools.len());
let server = pforge_runtime::McpServer::new(config);
tokio::select! {
result = server.run() => {
match result {
Ok(()) => {
eprintln!("Server stopped normally");
break;
}
Err(e) => {
eprintln!("❌ Server error: {}", e);
eprintln!(" Waiting for changes...");
}
}
}
_ = rx.recv() => {
if should_reload.load(Ordering::SeqCst) {
eprintln!("\n🔄 Changes detected - reloading...");
should_reload.store(false, Ordering::SeqCst);
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_dev_execute_invalid_config() {
let result = execute("/nonexistent/config.yaml", false).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_dev_watch_mode_setup() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(
temp_file,
r#"
forge:
name: test-server
version: 0.1.0
transport: stdio
optimization: debug
tools: []
"#
)
.unwrap();
let config_path = temp_file.path();
let config = parse_config(config_path);
assert!(config.is_ok());
}
}