use anyhow::{Context, Result};
use console::{style, Emoji};
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use std::io::{self, Write};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
pub fn run(port: u16, watch: bool, env: &str) -> 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("Environment:").dim(), style(env).cyan());
println!("{}", style("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim());
if watch {
run_with_watch(port, env)
} else {
run_once(port, env)
}
}
fn run_once(port: u16, env: &str) -> Result<()> {
let mut cmd = Command::new("cargo");
cmd.arg("run")
.env("RUST_LOG", "info")
.env("RUST_BACKTRACE", "1")
.env("APP_ENV", env);
if std::env::var("PORT").is_err() {
cmd.env("PORT", port.to_string());
}
let status = cmd.status().context("Failed to run cargo run")?;
if !status.success() {
anyhow::bail!("Server exited with error code: {:?}", status.code());
}
Ok(())
}
fn run_with_watch(port: u16, env: &str) -> Result<()> {
let project_dir =
std::env::current_dir().context("Failed to get current directory")?;
println!();
println!(
"{} {} watching for .rs/.yaml/.toml changes...",
style("👀").cyan(),
style("Hot reload").dim()
);
println!(" {} Ctrl+C to stop", style("→").cyan());
println!();
let should_restart = Arc::new(AtomicBool::new(false));
let (tx, rx) = std::sync::mpsc::channel();
let mut watcher = RecommendedWatcher::new(
move |res: notify::Result<notify::Event>| {
if let Ok(event) = res {
let _ = tx.send(event);
}
},
Config::default().with_poll_interval(Duration::from_secs(2)),
)
.context("Failed to create file watcher")?;
for dir in &["src", "internal", "manifest", "migrations"] {
let path = project_dir.join(dir);
if path.is_dir() {
let _ = watcher.watch(&path, RecursiveMode::Recursive);
}
}
let last_trigger = Arc::new(std::sync::Mutex::new(std::time::Instant::now()));
let flag = should_restart.clone();
let last = last_trigger.clone();
std::thread::spawn(move || {
for event in rx {
let trigger = event.kind.is_modify() || event.kind.is_create();
let relevant: Vec<_> = event
.paths
.iter()
.filter(|p| {
p.extension()
.map(|e| {
let ext = e.to_string_lossy();
ext == "rs"
|| ext == "yaml"
|| ext == "yml"
|| ext == "toml"
|| ext == "sql"
|| ext == "env"
})
.unwrap_or(false)
})
.collect();
if relevant.is_empty() {
continue;
}
let mut last_time = last.lock().unwrap();
if last_time.elapsed() < Duration::from_secs(1) {
continue;
}
*last_time = std::time::Instant::now();
drop(last_time);
println!();
println!(
"{} {} changed",
style("📝").yellow(),
style("File changed:").yellow()
);
for p in &relevant {
if let Some(name) = p.file_name() {
println!(" - {}", name.to_string_lossy());
}
}
if trigger {
flag.store(true, Ordering::Relaxed);
}
}
});
loop {
while !should_restart.load(Ordering::Acquire) {
std::thread::sleep(Duration::from_millis(300));
}
should_restart.store(false, Ordering::Relaxed);
println!();
print!("{} {} ", Emoji("⚡", ""), style("Building...").cyan());
io::stdout().flush().ok();
let build_result = Command::new("cargo")
.args(["build"])
.current_dir(&project_dir)
.env("RUST_LOG", "info")
.output();
match build_result {
Ok(output) if output.status.success() => {
println!("{}", style("✓ done").green());
println!();
println!(
"{} {} Compiled successfully! Starting server...",
Emoji("🚀", ""),
style("Server").cyan()
);
println!();
run_server(&project_dir, port, env);
println!();
println!(
"{} {} Waiting for changes...",
Emoji("💤", ""),
style("Server stopped").dim()
);
}
Ok(output) => {
println!();
print_build_errors(&output.stderr, &project_dir);
std::println!();
println!(
"{} {} Fix the errors above and this will restart automatically.",
style("⚠").yellow(),
style("Compile error").red().bold()
);
}
Err(e) => {
println!();
println!("{} {} {}", style("❌").red(), style("Error").red(), e);
}
}
}
}
fn run_server(project_dir: &std::path::Path, port: u16, env: &str) {
let mut child = match Command::new("cargo")
.args(["run"])
.current_dir(project_dir)
.env("RUST_LOG", "info")
.env("RUST_BACKTRACE", "1")
.env("PORT", port.to_string())
.env("APP_ENV", env)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
{
Ok(c) => c,
Err(e) => {
println!("{} {} Failed to start server: {}", style("❌").red(), style("Error").red(), e);
return;
}
};
let _ = child.wait();
}
fn print_build_errors(stderr: &[u8], _project_dir: &std::path::Path) {
let stderr_str = String::from_utf8_lossy(stderr);
let lines: Vec<_> = stderr_str.lines().rev().take(25).collect();
let max_width = 120;
for line in lines.iter().rev() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let display = if trimmed.len() > max_width {
&trimmed[..max_width.saturating_sub(3)]
} else {
trimmed
};
let is_error = trimmed.contains("error[E")
|| trimmed.starts_with("error")
|| trimmed.starts_with("Error");
let is_warning = trimmed.contains("warning");
let is_note = trimmed.contains("note:");
if is_error {
println!(" {} {}", style("✗").red(), style(display).red().dim());
} else if is_warning {
println!(" {} {}", style("⚠").yellow(), style(display).yellow().dim());
} else if is_note {
println!(" {} {}", style("→").dim(), style(display).dim());
}
}
}