devforge 0.3.0

Dev environment orchestrator — docker, health checks, mprocs, custom commands via TOML config
Documentation
use std::env;
use std::fs;
use std::path::Path;
use std::process::{self, Command};

pub fn log(msg: &str) {
    eprintln!("\x1b[32m[devforge]\x1b[0m {msg}");
}

pub fn fatal(msg: &str) -> ! {
    eprintln!("\x1b[31m[devforge]\x1b[0m {msg}");
    process::exit(1);
}

pub fn check_file(path: &Path) {
    if !path.exists() {
        fatal(&format!("Required file not found: {}", path.display()));
    }
}

pub fn require_cmd(name: &str) {
    let found = Command::new("which")
        .arg(name)
        .output()
        .map(|o| o.status.success())
        .unwrap_or(false);
    if !found {
        fatal(&format!("Required command not found: {name}"));
    }
}

pub fn load_dotenv(path: &Path) {
    let contents = match fs::read_to_string(path) {
        Ok(c) => c,
        Err(e) => fatal(&format!("Failed to read {}: {e}", path.display())),
    };
    for line in contents.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if let Some((key, value)) = line.split_once('=') {
            let key = key.trim();
            let value = value.trim().trim_matches('"').trim_matches('\'');
            unsafe { env::set_var(key, value) };
        }
    }
}

pub fn workspace_root() -> std::path::PathBuf {
    let start = env::var("CARGO_MANIFEST_DIR")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| env::current_dir().expect("cannot determine current directory"));

    let mut dir = start.as_path();
    loop {
        if dir.join("devforge.toml").exists() {
            return dir.to_path_buf();
        }
        match dir.parent() {
            Some(parent) => dir = parent,
            None => fatal("Could not find devforge.toml in any parent directory"),
        }
    }
}