kegani-cli 0.1.1

CLI tool for Kegani framework
Documentation
//! `keg run` command — Run the application with hot reload
//!
//! Watches .rs files and recompiles when changes are detected.

use anyhow::{Context, Result};
use console::{style, Emoji};
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

/// Run with hot reload
pub fn run(port: u16, watch: bool) -> Result<()> {
    println!();
    println!("{} {}", Emoji("", ""), style("Starting Kegani dev server").bold());
    println!("{}", style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim());
    println!("  {} {}", style("Port:").dim(), style(port).cyan());
    println!("  {} {}", style("Hot reload:").dim(), style(if watch { "enabled" } else { "disabled" }).cyan());
    println!("{}", style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim());

    if watch {
        run_with_watch()
    } else {
        run_once()
    }
}

fn run_once() -> Result<()> {
    let status = Command::new("cargo")
        .args(["run"])
        .current_dir(".")
        .env("RUST_LOG", "info")
        .status()
        .context("Failed to run cargo run")?;

    if !status.success() {
        anyhow::bail!("Server exited with error code: {:?}", status.code());
    }
    Ok(())
}

fn run_with_watch() -> Result<()> {
    // Capture the project directory once at startup.
    // notify's internal thread may change cwd, so we pin it here.
    let project_dir = std::env::current_dir().context("Failed to get current directory")?;

    println!();
    println!("{} {} watching for .rs file changes...", style("👀").cyan(), style("Hot reload").dim());
    println!();

    // Atomic flag: true = something changed, need to restart
    let should_restart = Arc::new(AtomicBool::new(true));

    // Set up file watcher
    let (tx, rx) = std::sync::mpsc::channel();

    let mut watcher = RecommendedWatcher::new(
        move |res: Result<notify::Event, notify::Error>| {
            if let Ok(event) = res {
                let _ = tx.send(event);
            }
        },
        Config::default().with_poll_interval(Duration::from_secs(1)),
    )
    .context("Failed to create file watcher")?;

    // Watch src/, internal/, and manifest/ directories
    for dir in &["src", "internal", "manifest"] {
        let path = project_dir.join(dir);
        if path.is_dir() {
            let _ = watcher.watch(&path, RecursiveMode::Recursive);
        }
    }

    // Background thread: listen for file changes and set the flag
    let flag = should_restart.clone();
    std::thread::spawn(move || {
        for event in rx {
            match event.kind {
                notify::EventKind::Modify(_) | notify::EventKind::Create(_) => {
                    let changed: Vec<_> = event
                        .paths
                        .iter()
                        .filter(|p| {
                            p.extension()
                                .map(|e| e == "rs" || e == "yaml" || e == "toml")
                                .unwrap_or(false)
                        })
                        .collect();

                    if !changed.is_empty() {
                        println!();
                        println!("{} {} changed", style("📝").yellow(), style("File changed:").yellow());
                        for p in &changed {
                            println!("  - {}", p.file_name().unwrap().to_string_lossy());
                        }
                        flag.store(true, Ordering::Relaxed);
                    }
                }
                _ => {}
            }
        }
    });

    // Main loop: wait for flag, run cargo, repeat
    loop {
        // Wait until flag is set
        while !should_restart.load(Ordering::Acquire) {
            std::thread::sleep(Duration::from_millis(500));
        }
        // Reset flag
        should_restart.store(false, Ordering::Relaxed);

        print!("{} {}", Emoji("", ""), style("Building...").cyan());

        let status = Command::new("cargo")
            .args(["run"])
            .current_dir(&project_dir)
            .env("RUST_LOG", "info")
            .status();

        match status {
            Ok(exit) if exit.success() => {
                println!();
                println!("{} {}", Emoji("", ""), style("Server stopped. Waiting for changes...").dim());
            }
            Ok(exit) => {
                println!();
                println!(
                    "{} {} exit code {:?}. Waiting for changes...",
                    style("").yellow(),
                    style("Compile error").red(),
                    exit.code()
                );
            }
            Err(e) => {
                println!();
                println!("{} {} {}. Waiting for changes...", style("").red(), style("Error").red(), e);
            }
        }
    }
}