use anyhow::{Context, Result};
use console::style;
use notify::{Event, EventKind, RecursiveMode, Watcher};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::{Duration, Instant};
use tokio::time::sleep;
fn find_project_root() -> Result<PathBuf> {
let mut current = std::env::current_dir()?;
loop {
let cargo_toml = current.join("Cargo.toml");
let bridgerust_toml = current.join("bridgerust.toml");
if cargo_toml.exists() || bridgerust_toml.exists() {
return Ok(current);
}
match current.parent() {
Some(parent) => current = parent.to_path_buf(),
None => anyhow::bail!("Could not find project root (Cargo.toml or bridgerust.toml)"),
}
}
}
pub async fn handle(target: String, test: bool) -> Result<()> {
println!("{}", style("👀 Watching for changes...").bold().cyan());
println!(" Press Ctrl+C to stop\n");
let project_root = find_project_root()?;
println!(" Project root: {}", project_root.display());
let (tx, rx) = mpsc::channel();
let mut watcher = notify::recommended_watcher(tx).context("Failed to create file watcher")?;
let src_dir = project_root.join("src");
if src_dir.exists() {
watcher
.watch(&src_dir, RecursiveMode::Recursive)
.context("Failed to watch src directory")?;
println!(" {} Watching: {}", style("✓").green(), src_dir.display());
}
let cargo_toml = project_root.join("Cargo.toml");
if cargo_toml.exists() {
watcher
.watch(&cargo_toml, RecursiveMode::NonRecursive)
.context("Failed to watch Cargo.toml")?;
println!(
" {} Watching: {}",
style("✓").green(),
cargo_toml.display()
);
}
let python_dir = project_root.join("python");
let nodejs_dir = project_root.join("nodejs");
let targets: Vec<&str> = match target.as_str() {
"all" => vec!["python", "nodejs"],
"python" => vec!["python"],
"nodejs" => vec!["nodejs"],
_ => anyhow::bail!(
"Invalid target: {}. Use 'python', 'nodejs', or 'all'",
target
),
};
for target_name in &targets {
match *target_name {
"python" => {
let pyproject = python_dir.join("pyproject.toml");
if pyproject.exists() {
watcher
.watch(&pyproject, RecursiveMode::NonRecursive)
.context("Failed to watch pyproject.toml")?;
println!(" {} Watching: {}", style("✓").green(), pyproject.display());
}
}
"nodejs" => {
let package_json = nodejs_dir.join("package.json");
if package_json.exists() {
watcher
.watch(&package_json, RecursiveMode::NonRecursive)
.context("Failed to watch package.json")?;
println!(
" {} Watching: {}",
style("✓").green(),
package_json.display()
);
}
}
_ => {}
}
}
println!("\n{} Waiting for changes...\n", style("→").cyan());
let mut last_build_time = Instant::now();
let debounce_duration = Duration::from_millis(500);
loop {
match rx.try_recv() {
Ok(Ok(Event {
kind: EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_),
paths,
..
})) => {
let relevant_change = paths.iter().any(|path| {
path.extension()
.map(|ext| ext == "rs" || ext == "toml" || ext == "json")
.unwrap_or(false)
});
if relevant_change && last_build_time.elapsed() > debounce_duration {
last_build_time = Instant::now();
println!(
"{}",
style("Change detected, rebuilding...").bold().yellow()
);
for target_name in &targets {
if let Err(e) = build_target(target_name, &project_root).await {
eprintln!(" {} Build failed: {}", style("✗").red(), e);
} else {
println!(" {} {} build completed", style("✓").green(), target_name);
}
}
if test {
println!("{}", style("🧪 Running tests...").bold().cyan());
for target_name in &targets {
if let Err(e) = test_target(target_name, &project_root).await {
eprintln!(" {} Test failed: {}", style("✗").red(), e);
} else {
println!(" {} {} tests passed", style("✓").green(), target_name);
}
}
}
println!("\n{} Waiting for changes...\n", style("→").cyan());
}
}
Ok(Ok(_)) => {} Ok(Err(e)) => {
eprintln!(" {} File watcher error: {}", style("âš ").yellow(), e);
}
Err(mpsc::TryRecvError::Empty) => {} Err(mpsc::TryRecvError::Disconnected) => {
eprintln!("{}", style("File watcher disconnected").red());
break;
}
}
sleep(Duration::from_millis(100)).await;
}
Ok(())
}
async fn build_target(target: &str, project_root: &Path) -> Result<()> {
match target {
"python" => {
let python_dir = project_root.join("python");
let mut cmd = Command::new("maturin");
cmd.current_dir(&python_dir);
cmd.arg("develop");
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let status = cmd.status().context("Failed to run maturin develop")?;
if !status.success() {
anyhow::bail!("Python build failed");
}
}
"nodejs" => {
let nodejs_dir = project_root.join("nodejs");
let mut cmd = Command::new("npm");
cmd.current_dir(&nodejs_dir);
cmd.arg("run");
cmd.arg("build");
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let status = cmd.status().context("Failed to run npm build")?;
if !status.success() {
anyhow::bail!("Node.js build failed");
}
}
_ => unreachable!(),
}
Ok(())
}
async fn test_target(target: &str, project_root: &PathBuf) -> Result<()> {
match target {
"python" => {
let mut cmd = Command::new("python");
cmd.arg("-m");
cmd.arg("pytest");
cmd.current_dir(project_root);
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let status = cmd.status().context("Failed to run pytest")?;
if !status.success() {
anyhow::bail!("Python tests failed");
}
}
"nodejs" => {
let nodejs_dir = project_root.join("nodejs");
let mut cmd = Command::new("npm");
cmd.current_dir(&nodejs_dir);
cmd.arg("test");
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
let status = cmd.status().context("Failed to run npm test")?;
if !status.success() {
anyhow::bail!("Node.js tests failed");
}
}
_ => unreachable!(),
}
Ok(())
}